From 35a96bde514a8897f6f0fcc41c5833bf63df2e2a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 18:29:01 +0200 Subject: Adding upstream version 1.0.2. Signed-off-by: Daniel Baumann --- ...PLEASE DON'T MAKE CHANGES IN THESE FILES.README | 14 + src/2geom/2geom.h | 75 + src/2geom/CMakeLists.txt | 139 + src/2geom/affine.cpp | 522 + src/2geom/affine.h | 244 + src/2geom/angle.h | 408 + src/2geom/basic-intersection.cpp | 493 + src/2geom/basic-intersection.h | 151 + src/2geom/bezier-clipping.cpp | 1163 + src/2geom/bezier-curve.cpp | 516 + src/2geom/bezier-curve.h | 352 + src/2geom/bezier-to-sbasis.h | 94 + src/2geom/bezier-utils.cpp | 997 + src/2geom/bezier-utils.h | 99 + src/2geom/bezier.cpp | 324 + src/2geom/bezier.h | 364 + src/2geom/cairo-path-sink.cpp | 123 + src/2geom/cairo-path-sink.h | 87 + src/2geom/choose.h | 140 + src/2geom/circle.cpp | 337 + src/2geom/circle.h | 165 + src/2geom/concepts.h | 209 + src/2geom/conic_section_clipper.h | 58 + src/2geom/conic_section_clipper_cr.h | 64 + src/2geom/conic_section_clipper_impl.cpp | 574 + src/2geom/conic_section_clipper_impl.h | 346 + src/2geom/conicsec.cpp | 1496 ++ src/2geom/conicsec.h | 518 + src/2geom/convex-hull.cpp | 746 + src/2geom/convex-hull.h | 346 + src/2geom/coord.cpp | 123 + src/2geom/coord.h | 214 + src/2geom/crossing.cpp | 233 + src/2geom/crossing.h | 213 + src/2geom/curve.cpp | 187 + src/2geom/curve.h | 369 + src/2geom/curves.h | 54 + src/2geom/d2-sbasis.cpp | 364 + src/2geom/d2.h | 564 + src/2geom/ellipse.cpp | 676 + src/2geom/ellipse.h | 253 + src/2geom/elliptical-arc-from-sbasis.cpp | 341 + src/2geom/elliptical-arc.cpp | 946 + src/2geom/elliptical-arc.h | 341 + src/2geom/exception.h | 145 + src/2geom/forward.h | 127 + src/2geom/generic-interval.h | 361 + src/2geom/generic-rect.h | 536 + src/2geom/geom.cpp | 395 + src/2geom/geom.h | 66 + src/2geom/int-interval.h | 63 + src/2geom/int-point.h | 180 + src/2geom/int-rect.h | 75 + src/2geom/intersection-graph.cpp | 494 + src/2geom/intersection-graph.h | 157 + src/2geom/intersection.h | 147 + src/2geom/interval.h | 252 + src/2geom/line.cpp | 609 + src/2geom/line.h | 604 + src/2geom/linear.h | 167 + src/2geom/math-utils.h | 110 + src/2geom/nearest-time.cpp | 322 + src/2geom/nearest-time.h | 141 + src/2geom/numeric/fitting-model.h | 521 + src/2geom/numeric/fitting-tool.h | 562 + src/2geom/numeric/linear_system.h | 138 + src/2geom/numeric/matrix.cpp | 154 + src/2geom/numeric/matrix.h | 603 + src/2geom/numeric/symmetric-matrix-fs-operation.h | 102 + src/2geom/numeric/symmetric-matrix-fs-trace.h | 427 + src/2geom/numeric/symmetric-matrix-fs.h | 733 + src/2geom/numeric/vector.h | 594 + src/2geom/ord.h | 80 + src/2geom/path-intersection.cpp | 730 + src/2geom/path-intersection.h | 118 + src/2geom/path-sink.cpp | 104 + src/2geom/path-sink.h | 245 + src/2geom/path.cpp | 1128 + src/2geom/path.h | 874 + src/2geom/pathvector.cpp | 335 + src/2geom/pathvector.h | 301 + src/2geom/piecewise.cpp | 266 + src/2geom/piecewise.h | 945 + src/2geom/point.cpp | 274 + src/2geom/point.h | 427 + src/2geom/polynomial.cpp | 337 + src/2geom/polynomial.h | 264 + src/2geom/ray.h | 192 + src/2geom/rect.cpp | 187 + src/2geom/rect.h | 264 + src/2geom/recursive-bezier-intersection.cpp | 476 + src/2geom/sbasis-2d.cpp | 202 + src/2geom/sbasis-2d.h | 371 + src/2geom/sbasis-curve.h | 157 + src/2geom/sbasis-geometric.cpp | 791 + src/2geom/sbasis-geometric.h | 146 + src/2geom/sbasis-math.cpp | 379 + src/2geom/sbasis-math.h | 99 + src/2geom/sbasis-poly.cpp | 59 + src/2geom/sbasis-poly.h | 56 + src/2geom/sbasis-roots.cpp | 656 + src/2geom/sbasis-to-bezier.cpp | 570 + src/2geom/sbasis-to-bezier.h | 87 + src/2geom/sbasis.cpp | 681 + src/2geom/sbasis.h | 529 + src/2geom/solve-bezier-one-d.cpp | 245 + src/2geom/solve-bezier-parametric.cpp | 189 + src/2geom/solve-bezier.cpp | 304 + src/2geom/solver.h | 88 + src/2geom/svg-path-parser.cpp | 1615 ++ src/2geom/svg-path-parser.h | 199 + src/2geom/svg-path-writer.cpp | 296 + src/2geom/svg-path-writer.h | 122 + src/2geom/sweep-bounds.cpp | 157 + src/2geom/sweep-bounds.h | 62 + src/2geom/sweeper.h | 190 + src/2geom/transforms.cpp | 205 + src/2geom/transforms.h | 370 + src/2geom/utils.cpp | 86 + src/2geom/utils.h | 111 + src/3rdparty/CMakeLists.txt | 7 + src/3rdparty/adaptagrams/CMakeLists.txt | 3 + src/3rdparty/adaptagrams/libavoid/CMakeLists.txt | 58 + src/3rdparty/adaptagrams/libavoid/Doxyfile | 2423 ++ src/3rdparty/adaptagrams/libavoid/LICENSE.LGPL | 460 + src/3rdparty/adaptagrams/libavoid/Makefile.am | 91 + src/3rdparty/adaptagrams/libavoid/README | 35 + src/3rdparty/adaptagrams/libavoid/actioninfo.cpp | 187 + src/3rdparty/adaptagrams/libavoid/actioninfo.h | 88 + src/3rdparty/adaptagrams/libavoid/assertions.h | 60 + .../adaptagrams/libavoid/connectionpin.cpp | 476 + src/3rdparty/adaptagrams/libavoid/connectionpin.h | 303 + src/3rdparty/adaptagrams/libavoid/connector.cpp | 2487 ++ src/3rdparty/adaptagrams/libavoid/connector.h | 547 + src/3rdparty/adaptagrams/libavoid/connend.cpp | 435 + src/3rdparty/adaptagrams/libavoid/connend.h | 267 + src/3rdparty/adaptagrams/libavoid/debug.h | 100 + src/3rdparty/adaptagrams/libavoid/debughandler.h | 138 + src/3rdparty/adaptagrams/libavoid/dllexport.h | 39 + .../adaptagrams/libavoid/doc/description.doc | 73 + src/3rdparty/adaptagrams/libavoid/doc/example.doc | 122 + src/3rdparty/adaptagrams/libavoid/doc/header.html | 19 + src/3rdparty/adaptagrams/libavoid/geometry.cpp | 641 + src/3rdparty/adaptagrams/libavoid/geometry.h | 129 + src/3rdparty/adaptagrams/libavoid/geomtypes.cpp | 761 + src/3rdparty/adaptagrams/libavoid/geomtypes.h | 381 + src/3rdparty/adaptagrams/libavoid/graph.cpp | 785 + src/3rdparty/adaptagrams/libavoid/graph.h | 135 + src/3rdparty/adaptagrams/libavoid/hyperedge.cpp | 388 + src/3rdparty/adaptagrams/libavoid/hyperedge.h | 223 + .../adaptagrams/libavoid/hyperedgeimprover.cpp | 1232 + .../adaptagrams/libavoid/hyperedgeimprover.h | 159 + .../adaptagrams/libavoid/hyperedgetree.cpp | 821 + src/3rdparty/adaptagrams/libavoid/hyperedgetree.h | 143 + src/3rdparty/adaptagrams/libavoid/junction.cpp | 233 + src/3rdparty/adaptagrams/libavoid/junction.h | 197 + src/3rdparty/adaptagrams/libavoid/libavoid.h | 55 + src/3rdparty/adaptagrams/libavoid/libavoid.pc.in | 12 + src/3rdparty/adaptagrams/libavoid/libavoid.sln | 196 + src/3rdparty/adaptagrams/libavoid/libavoid.vcxproj | 198 + src/3rdparty/adaptagrams/libavoid/makepath.cpp | 1554 ++ src/3rdparty/adaptagrams/libavoid/makepath.h | 52 + src/3rdparty/adaptagrams/libavoid/mtst.cpp | 1094 + src/3rdparty/adaptagrams/libavoid/mtst.h | 134 + src/3rdparty/adaptagrams/libavoid/obstacle.cpp | 355 + src/3rdparty/adaptagrams/libavoid/obstacle.h | 150 + src/3rdparty/adaptagrams/libavoid/orthogonal.cpp | 3259 +++ src/3rdparty/adaptagrams/libavoid/orthogonal.h | 39 + src/3rdparty/adaptagrams/libavoid/router.cpp | 3131 +++ src/3rdparty/adaptagrams/libavoid/router.h | 888 + src/3rdparty/adaptagrams/libavoid/scanline.cpp | 562 + src/3rdparty/adaptagrams/libavoid/scanline.h | 136 + src/3rdparty/adaptagrams/libavoid/shape.cpp | 280 + src/3rdparty/adaptagrams/libavoid/shape.h | 165 + .../adaptagrams/libavoid/tests/2junctions.cpp | 108 + .../adaptagrams/libavoid/tests/Makefile.am | 217 + .../libavoid/tests/buildOrthogonalChannelInfo1.cpp | 65 + .../libavoid/tests/checkpointNudging1.cpp | 2592 ++ .../libavoid/tests/checkpointNudging2.cpp | 2606 ++ .../libavoid/tests/checkpointNudging3.cpp | 5242 ++++ .../adaptagrams/libavoid/tests/checkpoints01.cpp | 62 + .../adaptagrams/libavoid/tests/checkpoints02.cpp | 105 + .../adaptagrams/libavoid/tests/checkpoints03.cpp | 96 + .../adaptagrams/libavoid/tests/complex.cpp | 107 + .../adaptagrams/libavoid/tests/connectionpin01.cpp | 68 + .../adaptagrams/libavoid/tests/connectionpin02.cpp | 113 + .../adaptagrams/libavoid/tests/connectionpin03.cpp | 328 + .../adaptagrams/libavoid/tests/connendmove.cpp | 85 + .../adaptagrams/libavoid/tests/corneroverlap01.cpp | 154 + .../adaptagrams/libavoid/tests/endlessLoop01.cpp | 142 + .../adaptagrams/libavoid/tests/example.cpp | 81 + .../libavoid/tests/finalSegmentNudging1.cpp | 5227 ++++ .../libavoid/tests/finalSegmentNudging2.cpp | 397 + .../libavoid/tests/finalSegmentNudging3.cpp | 2658 ++ .../libavoid/tests/forwardFlowingConnectors01.cpp | 380 + .../libavoid/tests/freeFloatingDirection01.cpp | 3321 +++ src/3rdparty/adaptagrams/libavoid/tests/hola01.cpp | 1006 + .../adaptagrams/libavoid/tests/hyperedge01.cpp | 285 + .../adaptagrams/libavoid/tests/hyperedge02.cpp | 99 + .../adaptagrams/libavoid/tests/hyperedgeLoop1.cpp | 87 + .../libavoid/tests/hyperedgeRerouting01.cpp | 150 + .../libavoid/tests/improveHyperedge01.cpp | 109 + .../libavoid/tests/improveHyperedge02.cpp | 90 + .../libavoid/tests/improveHyperedge03.cpp | 90 + .../libavoid/tests/improveHyperedge04.cpp | 113 + .../libavoid/tests/improveHyperedge05.cpp | 154 + .../libavoid/tests/improveHyperedge06.cpp | 92 + .../adaptagrams/libavoid/tests/infinity.cpp | 26 + src/3rdparty/adaptagrams/libavoid/tests/inline.cpp | 73 + .../adaptagrams/libavoid/tests/inlineOverlap09.cpp | 401 + .../adaptagrams/libavoid/tests/inlineOverlap10.cpp | 93 + .../adaptagrams/libavoid/tests/inlineOverlap11.cpp | 187 + .../adaptagrams/libavoid/tests/inlineShapes.cpp | 97 + .../adaptagrams/libavoid/tests/inlineoverlap01.cpp | 25 + .../adaptagrams/libavoid/tests/inlineoverlap02.cpp | 62 + .../adaptagrams/libavoid/tests/inlineoverlap03.cpp | 55 + .../adaptagrams/libavoid/tests/inlineoverlap04.cpp | 62 + .../adaptagrams/libavoid/tests/inlineoverlap05.cpp | 69 + .../adaptagrams/libavoid/tests/inlineoverlap06.cpp | 62 + .../adaptagrams/libavoid/tests/inlineoverlap07.cpp | 75 + .../adaptagrams/libavoid/tests/inlineoverlap08.cpp | 120 + .../adaptagrams/libavoid/tests/junction01.cpp | 55 + .../adaptagrams/libavoid/tests/junction02.cpp | 55 + .../adaptagrams/libavoid/tests/junction03.cpp | 55 + .../adaptagrams/libavoid/tests/junction04.cpp | 100 + .../adaptagrams/libavoid/tests/latesetup.cpp | 94 + .../libavoid/tests/lineSegWrapperCrash1.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash2.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash3.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash4.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash5.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash6.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash7.cpp | 1884 ++ .../libavoid/tests/lineSegWrapperCrash8.cpp | 1884 ++ .../libavoid/tests/msctests/2junctions.vcxproj | 152 + .../msctests/buildOrthogonalChannelInfo1.vcxproj | 152 + .../tests/msctests/checkpointNudging1.vcxproj | 152 + .../tests/msctests/checkpointNudging2.vcxproj | 152 + .../libavoid/tests/msctests/checkpoints01.vcxproj | 152 + .../tests/msctests/connectionpin01.vcxproj | 152 + .../tests/msctests/connectionpin02.vcxproj | 152 + .../tests/msctests/connectionpin03.vcxproj | 152 + .../libavoid/tests/msctests/connendmove.vcxproj | 152 + .../tests/msctests/corneroverlap01.vcxproj | 152 + .../libavoid/tests/msctests/example.vcxproj | 152 + .../tests/msctests/finalSegmentNudging1.vcxproj | 152 + .../tests/msctests/finalSegmentNudging2.vcxproj | 152 + .../tests/msctests/finalSegmentNudging3.vcxproj | 152 + .../tests/msctests/freeFloatingDirection01.vcxproj | 152 + .../libavoid/tests/msctests/junction01.vcxproj | 152 + .../adaptagrams/libavoid/tests/multiconnact.cpp | 75 + src/3rdparty/adaptagrams/libavoid/tests/node1.cpp | 68 + .../adaptagrams/libavoid/tests/nudgeCrossing01.cpp | 2665 ++ .../adaptagrams/libavoid/tests/nudgeintobug.cpp | 40 + .../adaptagrams/libavoid/tests/nudgeold.cpp | 80 + .../libavoid/tests/nudgingSkipsCheckpoint01.cpp | 1507 ++ .../libavoid/tests/nudgingSkipsCheckpoint02.cpp | 3054 +++ .../adaptagrams/libavoid/tests/orderassertion.cpp | 1720 ++ .../adaptagrams/libavoid/tests/orthordering01.cpp | 92 + .../adaptagrams/libavoid/tests/orthordering02.cpp | 92 + .../adaptagrams/libavoid/tests/output/README.txt | 1 + .../libavoid/tests/overlappingRects.cpp | 2327 ++ .../libavoid/tests/penaltyRerouting01.cpp | 369 + .../adaptagrams/libavoid/tests/performance01.cpp | 7665 ++++++ .../libavoid/tests/reallyslowrouting.cpp | 7099 +++++ .../libavoid/tests/removeJunctions01.cpp | 103 + .../libavoid/tests/restrictedNudging.cpp | 68 + .../adaptagrams/libavoid/tests/slowrouting.cpp | 1459 ++ src/3rdparty/adaptagrams/libavoid/tests/tjunct.cpp | 48 + .../adaptagrams/libavoid/tests/treeRootCrash01.cpp | 140 + .../adaptagrams/libavoid/tests/treeRootCrash02.cpp | 243 + .../libavoid/tests/unsatisfiableRangeAssertion.cpp | 25789 ++++++++++++++++++ .../adaptagrams/libavoid/tests/validPaths01.cpp | 457 + .../adaptagrams/libavoid/tests/validPaths02.cpp | 53 + .../libavoid/tests/vertlineassertion.cpp | 2214 ++ src/3rdparty/adaptagrams/libavoid/timer.cpp | 188 + src/3rdparty/adaptagrams/libavoid/timer.h | 104 + src/3rdparty/adaptagrams/libavoid/vertices.cpp | 739 + src/3rdparty/adaptagrams/libavoid/vertices.h | 226 + src/3rdparty/adaptagrams/libavoid/viscluster.cpp | 116 + src/3rdparty/adaptagrams/libavoid/viscluster.h | 136 + src/3rdparty/adaptagrams/libavoid/visibility.cpp | 676 + src/3rdparty/adaptagrams/libavoid/visibility.h | 41 + src/3rdparty/adaptagrams/libavoid/vpsc.cpp | 1500 ++ src/3rdparty/adaptagrams/libavoid/vpsc.h | 341 + src/3rdparty/adaptagrams/libcola/CMakeLists.txt | 30 + src/3rdparty/adaptagrams/libcola/Makefile.am | 60 + src/3rdparty/adaptagrams/libcola/box.cpp | 111 + src/3rdparty/adaptagrams/libcola/box.h | 82 + .../libcola/cc_clustercontainmentconstraints.cpp | 194 + .../libcola/cc_clustercontainmentconstraints.h | 52 + .../libcola/cc_nonoverlapconstraints.cpp | 590 + .../adaptagrams/libcola/cc_nonoverlapconstraints.h | 210 + src/3rdparty/adaptagrams/libcola/cluster.cpp | 719 + src/3rdparty/adaptagrams/libcola/cluster.h | 371 + src/3rdparty/adaptagrams/libcola/cola.cpp | 700 + src/3rdparty/adaptagrams/libcola/cola.h | 1002 + src/3rdparty/adaptagrams/libcola/cola_log.h | 204 + src/3rdparty/adaptagrams/libcola/colafd.cpp | 1681 ++ src/3rdparty/adaptagrams/libcola/commondefs.h | 98 + .../adaptagrams/libcola/compound_constraints.cpp | 1671 ++ .../adaptagrams/libcola/compound_constraints.h | 829 + .../adaptagrams/libcola/conjugate_gradient.cpp | 137 + .../adaptagrams/libcola/conjugate_gradient.h | 36 + .../adaptagrams/libcola/connected_components.cpp | 160 + .../adaptagrams/libcola/connected_components.h | 54 + src/3rdparty/adaptagrams/libcola/convex_hull.cpp | 129 + src/3rdparty/adaptagrams/libcola/convex_hull.h | 31 + .../adaptagrams/libcola/doc/description.doc | 50 + src/3rdparty/adaptagrams/libcola/exceptions.h | 54 + .../adaptagrams/libcola/gradient_projection.cpp | 482 + .../adaptagrams/libcola/gradient_projection.h | 165 + src/3rdparty/adaptagrams/libcola/libcola.pc.in | 11 + src/3rdparty/adaptagrams/libcola/output_svg.cpp | 389 + src/3rdparty/adaptagrams/libcola/output_svg.h | 80 + src/3rdparty/adaptagrams/libcola/pseudorandom.cpp | 48 + src/3rdparty/adaptagrams/libcola/pseudorandom.h | 44 + src/3rdparty/adaptagrams/libcola/shapepair.cpp | 47 + src/3rdparty/adaptagrams/libcola/shapepair.h | 49 + src/3rdparty/adaptagrams/libcola/shortest_paths.h | 245 + src/3rdparty/adaptagrams/libcola/sparse_matrix.h | 140 + src/3rdparty/adaptagrams/libcola/straightener.cpp | 798 + src/3rdparty/adaptagrams/libcola/straightener.h | 389 + .../libcola/tests/FixedRelativeConstraint01.cpp | 51 + src/3rdparty/adaptagrams/libcola/tests/Makefile.am | 68 + .../adaptagrams/libcola/tests/StillOverlap01.cpp | 221 + .../adaptagrams/libcola/tests/StillOverlap02.cpp | 1863 ++ .../adaptagrams/libcola/tests/boundary.cpp | 121 + .../libcola/tests/connected_components.cpp | 69 + .../adaptagrams/libcola/tests/constrained.cpp | 99 + .../adaptagrams/libcola/tests/containment.cpp | 89 + .../adaptagrams/libcola/tests/containment2.cpp | 157 + .../adaptagrams/libcola/tests/convex_hull.cpp | 180 + .../adaptagrams/libcola/tests/cycle_detector.cpp | 386 + .../adaptagrams/libcola/tests/data/1138_bus.txt | 1458 ++ .../libcola/tests/data/uetzNetworkGSC-all.gml | 26139 +++++++++++++++++++ .../adaptagrams/libcola/tests/gml_graph.cpp | 257 + .../adaptagrams/libcola/tests/graphlayouttest.h | 275 + .../adaptagrams/libcola/tests/initialOverlap.cpp | 168 + src/3rdparty/adaptagrams/libcola/tests/invalid.cpp | 80 + .../adaptagrams/libcola/tests/large_graph.cpp | 144 + .../adaptagrams/libcola/tests/makefeasible.cpp | 368 + .../adaptagrams/libcola/tests/makefeasible02.cpp | 932 + .../adaptagrams/libcola/tests/makemovie.sh | 10 + .../libcola/tests/max_acyclic_subgraph.cpp | 346 + .../libcola/tests/overlappingClusters01.cpp | 333 + .../libcola/tests/overlappingClusters02.cpp | 100 + .../libcola/tests/overlappingClusters04.cpp | 61 + .../adaptagrams/libcola/tests/page_bounds.cpp | 102 + src/3rdparty/adaptagrams/libcola/tests/planar.cpp | 287 + .../adaptagrams/libcola/tests/random_graph.cpp | 113 + .../libcola/tests/rectangularClusters01.cpp | 1135 + .../libcola/tests/rectclustershapecontainment.cpp | 2173 ++ src/3rdparty/adaptagrams/libcola/tests/resize.cpp | 132 + src/3rdparty/adaptagrams/libcola/tests/runtest.sh | 8 + .../adaptagrams/libcola/tests/scale_free.cpp | 135 + .../adaptagrams/libcola/tests/shortest_paths.cpp | 140 + .../adaptagrams/libcola/tests/small_graph.cpp | 111 + .../adaptagrams/libcola/tests/sparse_matrix.cpp | 88 + src/3rdparty/adaptagrams/libcola/tests/test_cg.cpp | 164 + .../adaptagrams/libcola/tests/topology.cpp | 177 + src/3rdparty/adaptagrams/libcola/tests/trees.cpp | 103 + .../adaptagrams/libcola/tests/unconstrained.cpp | 71 + .../adaptagrams/libcola/tests/unsatisfiable.cpp | 85 + .../adaptagrams/libcola/tests/view_cd_output.sh | 43 + .../adaptagrams/libcola/tests/view_mas_output.sh | 43 + src/3rdparty/adaptagrams/libcola/unused.h | 26 + src/3rdparty/adaptagrams/libvpsc/CMakeLists.txt | 28 + src/3rdparty/adaptagrams/libvpsc/COPYING | 505 + src/3rdparty/adaptagrams/libvpsc/Makefile.am | 42 + src/3rdparty/adaptagrams/libvpsc/assertions.h | 102 + src/3rdparty/adaptagrams/libvpsc/block.cpp | 647 + src/3rdparty/adaptagrams/libvpsc/block.h | 113 + src/3rdparty/adaptagrams/libvpsc/blocks.cpp | 249 + src/3rdparty/adaptagrams/libvpsc/blocks.h | 96 + src/3rdparty/adaptagrams/libvpsc/cbuffer.cpp | 95 + src/3rdparty/adaptagrams/libvpsc/cbuffer.h | 49 + src/3rdparty/adaptagrams/libvpsc/constraint.cpp | 218 + src/3rdparty/adaptagrams/libvpsc/constraint.h | 139 + .../adaptagrams/libvpsc/doc/description.doc | 36 + src/3rdparty/adaptagrams/libvpsc/exceptions.h | 36 + src/3rdparty/adaptagrams/libvpsc/libvpsc.pc.in | 12 + src/3rdparty/adaptagrams/libvpsc/linesegment.h | 138 + src/3rdparty/adaptagrams/libvpsc/pairing_heap.h | 394 + src/3rdparty/adaptagrams/libvpsc/rectangle.cpp | 744 + src/3rdparty/adaptagrams/libvpsc/rectangle.h | 302 + src/3rdparty/adaptagrams/libvpsc/solve_VPSC.cpp | 561 + src/3rdparty/adaptagrams/libvpsc/solve_VPSC.h | 130 + src/3rdparty/adaptagrams/libvpsc/tests/Makefile.am | 15 + src/3rdparty/adaptagrams/libvpsc/tests/block.cpp | 105 + src/3rdparty/adaptagrams/libvpsc/tests/cycle.cpp | 106 + .../adaptagrams/libvpsc/tests/rectangleoverlap.cpp | 660 + .../adaptagrams/libvpsc/tests/satisfy_inc.cpp | 666 + src/3rdparty/adaptagrams/libvpsc/variable.cpp | 31 + src/3rdparty/adaptagrams/libvpsc/variable.h | 95 + src/3rdparty/autotrace/CMakeLists.txt | 53 + src/3rdparty/autotrace/atou.c | 18 + src/3rdparty/autotrace/atou.h | 9 + src/3rdparty/autotrace/autotrace.c | 386 + src/3rdparty/autotrace/autotrace.h | 413 + src/3rdparty/autotrace/bitmap.c | 8 + src/3rdparty/autotrace/bitmap.h | 13 + src/3rdparty/autotrace/cmdline.h | 56 + src/3rdparty/autotrace/color.c | 109 + src/3rdparty/autotrace/color.h | 46 + src/3rdparty/autotrace/config.h | 118 + src/3rdparty/autotrace/curve.c | 255 + src/3rdparty/autotrace/curve.h | 131 + src/3rdparty/autotrace/datetime.c | 20 + src/3rdparty/autotrace/datetime.h | 1 + src/3rdparty/autotrace/despeckle.c | 710 + src/3rdparty/autotrace/despeckle.h | 54 + src/3rdparty/autotrace/epsilon-equal.c | 22 + src/3rdparty/autotrace/epsilon-equal.h | 17 + src/3rdparty/autotrace/exception.c | 47 + src/3rdparty/autotrace/exception.h | 39 + src/3rdparty/autotrace/filename.c | 121 + src/3rdparty/autotrace/filename.h | 42 + src/3rdparty/autotrace/fit.c | 1442 + src/3rdparty/autotrace/fit.h | 22 + src/3rdparty/autotrace/image-header.h | 18 + src/3rdparty/autotrace/image-proc.c | 493 + src/3rdparty/autotrace/image-proc.h | 21 + src/3rdparty/autotrace/input.c | 227 + src/3rdparty/autotrace/input.h | 95 + src/3rdparty/autotrace/intl.h | 22 + src/3rdparty/autotrace/logreport.c | 9 + src/3rdparty/autotrace/logreport.h | 32 + src/3rdparty/autotrace/median.c | 863 + src/3rdparty/autotrace/module.c | 72 + src/3rdparty/autotrace/output.c | 239 + src/3rdparty/autotrace/output.h | 89 + src/3rdparty/autotrace/private.h | 43 + src/3rdparty/autotrace/pxl-outline.c | 887 + src/3rdparty/autotrace/pxl-outline.h | 58 + src/3rdparty/autotrace/quantize.h | 52 + src/3rdparty/autotrace/spline.c | 160 + src/3rdparty/autotrace/spline.h | 88 + src/3rdparty/autotrace/thin-image.c | 353 + src/3rdparty/autotrace/thin-image.h | 36 + src/3rdparty/autotrace/types.h | 42 + src/3rdparty/autotrace/vector.c | 260 + src/3rdparty/autotrace/vector.h | 65 + src/3rdparty/autotrace/xstd.h | 81 + src/3rdparty/libcroco/CMakeLists.txt | 65 + src/3rdparty/libcroco/README | 11 + src/3rdparty/libcroco/cr-additional-sel.c | 500 + src/3rdparty/libcroco/cr-additional-sel.h | 98 + src/3rdparty/libcroco/cr-attr-sel.c | 232 + src/3rdparty/libcroco/cr-attr-sel.h | 74 + src/3rdparty/libcroco/cr-cascade.c | 213 + src/3rdparty/libcroco/cr-cascade.h | 74 + src/3rdparty/libcroco/cr-declaration.c | 804 + src/3rdparty/libcroco/cr-declaration.h | 136 + src/3rdparty/libcroco/cr-doc-handler.c | 276 + src/3rdparty/libcroco/cr-doc-handler.h | 298 + src/3rdparty/libcroco/cr-enc-handler.c | 184 + src/3rdparty/libcroco/cr-enc-handler.h | 94 + src/3rdparty/libcroco/cr-fonts.c | 943 + src/3rdparty/libcroco/cr-fonts.h | 315 + src/3rdparty/libcroco/cr-input.c | 1184 + src/3rdparty/libcroco/cr-input.h | 174 + src/3rdparty/libcroco/cr-libxml-node-iface.c | 81 + src/3rdparty/libcroco/cr-libxml-node-iface.h | 14 + src/3rdparty/libcroco/cr-node-iface.h | 34 + src/3rdparty/libcroco/cr-num.c | 315 + src/3rdparty/libcroco/cr-num.h | 127 + src/3rdparty/libcroco/cr-om-parser.c | 1154 + src/3rdparty/libcroco/cr-om-parser.h | 98 + src/3rdparty/libcroco/cr-parser.c | 4537 ++++ src/3rdparty/libcroco/cr-parser.h | 130 + src/3rdparty/libcroco/cr-parsing-location.c | 172 + src/3rdparty/libcroco/cr-parsing-location.h | 70 + src/3rdparty/libcroco/cr-prop-list.c | 404 + src/3rdparty/libcroco/cr-prop-list.h | 80 + src/3rdparty/libcroco/cr-pseudo.c | 167 + src/3rdparty/libcroco/cr-pseudo.h | 66 + src/3rdparty/libcroco/cr-rgb.c | 688 + src/3rdparty/libcroco/cr-rgb.h | 94 + src/3rdparty/libcroco/cr-sel-eng.c | 2294 ++ src/3rdparty/libcroco/cr-sel-eng.h | 120 + src/3rdparty/libcroco/cr-selector.c | 330 + src/3rdparty/libcroco/cr-selector.h | 95 + src/3rdparty/libcroco/cr-simple-sel.c | 328 + src/3rdparty/libcroco/cr-simple-sel.h | 131 + src/3rdparty/libcroco/cr-statement.c | 2809 ++ src/3rdparty/libcroco/cr-statement.h | 440 + src/3rdparty/libcroco/cr-string.c | 168 + src/3rdparty/libcroco/cr-string.h | 76 + src/3rdparty/libcroco/cr-style.c | 2850 ++ src/3rdparty/libcroco/cr-style.h | 339 + src/3rdparty/libcroco/cr-stylesheet.c | 312 + src/3rdparty/libcroco/cr-stylesheet.h | 125 + src/3rdparty/libcroco/cr-term.c | 788 + src/3rdparty/libcroco/cr-term.h | 195 + src/3rdparty/libcroco/cr-tknzr.c | 2754 ++ src/3rdparty/libcroco/cr-tknzr.h | 115 + src/3rdparty/libcroco/cr-token.c | 636 + src/3rdparty/libcroco/cr-token.h | 212 + src/3rdparty/libcroco/cr-utils.c | 1330 + src/3rdparty/libcroco/cr-utils.h | 249 + src/3rdparty/libcroco/libcroco.h | 48 + ...PLEASE DON'T MAKE CHANGES IN THESE FILES.README | 9 + src/3rdparty/libdepixelize/CMakeLists.txt | 23 + src/3rdparty/libdepixelize/kopftracer2011.cpp | 665 + src/3rdparty/libdepixelize/kopftracer2011.h | 150 + src/3rdparty/libdepixelize/priv/branchless.h | 58 + src/3rdparty/libdepixelize/priv/colorspace.h | 111 + src/3rdparty/libdepixelize/priv/curvature.h | 115 + .../libdepixelize/priv/homogeneoussplines.h | 472 + src/3rdparty/libdepixelize/priv/integral.h | 61 + src/3rdparty/libdepixelize/priv/iterator.h | 123 + .../libdepixelize/priv/optimization-kopf2011.h | 263 + src/3rdparty/libdepixelize/priv/pixelgraph.h | 555 + src/3rdparty/libdepixelize/priv/point.h | 112 + .../libdepixelize/priv/simplifiedvoronoi.h | 1707 ++ src/3rdparty/libdepixelize/priv/splines-kopf2011.h | 167 + src/3rdparty/libdepixelize/splines.h | 120 + src/3rdparty/libuemf/CMakeLists.txt | 30 + src/3rdparty/libuemf/README | 579 + src/3rdparty/libuemf/symbol_convert.c | 1008 + src/3rdparty/libuemf/symbol_convert.h | 51 + src/3rdparty/libuemf/uemf.c | 5606 ++++ src/3rdparty/libuemf/uemf.h | 3653 +++ src/3rdparty/libuemf/uemf_endian.c | 2270 ++ src/3rdparty/libuemf/uemf_endian.h | 59 + src/3rdparty/libuemf/uemf_print.c | 2704 ++ src/3rdparty/libuemf/uemf_print.h | 177 + src/3rdparty/libuemf/uemf_safe.c | 1204 + src/3rdparty/libuemf/uemf_safe.h | 32 + src/3rdparty/libuemf/uemf_utf.c | 720 + src/3rdparty/libuemf/uemf_utf.h | 55 + src/3rdparty/libuemf/upmf.c | 8660 ++++++ src/3rdparty/libuemf/upmf.h | 3178 +++ src/3rdparty/libuemf/upmf_print.c | 3400 +++ src/3rdparty/libuemf/upmf_print.h | 181 + src/3rdparty/libuemf/uwmf.c | 7039 +++++ src/3rdparty/libuemf/uwmf.h | 2677 ++ src/3rdparty/libuemf/uwmf_endian.c | 1774 ++ src/3rdparty/libuemf/uwmf_endian.h | 34 + src/3rdparty/libuemf/uwmf_print.c | 1635 ++ src/3rdparty/libuemf/uwmf_print.h | 52 + src/CMakeLists.txt | 402 + src/actions/CMakeLists.txt | 12 + src/actions/README | 26 + src/actions/actions-base.cpp | 281 + src/actions/actions-base.h | 19 + src/actions/actions-extra-data.cpp | 67 + src/actions/actions-extra-data.h | 64 + src/actions/actions-file.cpp | 142 + src/actions/actions-file.h | 31 + src/actions/actions-helper.cpp | 48 + src/actions/actions-helper.h | 34 + src/actions/actions-object.cpp | 159 + src/actions/actions-object.h | 30 + src/actions/actions-output.cpp | 309 + src/actions/actions-output.h | 31 + src/actions/actions-selection.cpp | 286 + src/actions/actions-selection.h | 30 + src/actions/actions-transform.cpp | 132 + src/actions/actions-transform.h | 30 + src/actions/actions-window.cpp | 96 + src/actions/actions-window.h | 31 + src/attribute-rel-css.cpp | 212 + src/attribute-rel-css.h | 77 + src/attribute-rel-svg.cpp | 149 + src/attribute-rel-svg.h | 60 + src/attribute-rel-util.cpp | 361 + src/attribute-rel-util.h | 100 + src/attribute-sort-util.cpp | 212 + src/attribute-sort-util.h | 66 + src/attributes.cpp | 637 + src/attributes.h | 600 + src/auto-save.cpp | 198 + src/auto-save.h | 58 + src/axis-manip.cpp | 55 + src/axis-manip.h | 259 + src/bad-uri-exception.h | 43 + src/cms-color-types.h | 77 + src/cms-system.h | 67 + src/color-profile-cms-fns.h | 64 + src/color-rgba.h | 175 + src/color.cpp | 459 + src/color.h | 86 + src/colorspace.h | 53 + src/composite-undo-stack-observer.cpp | 166 + src/composite-undo-stack-observer.h | 182 + src/conditions.cpp | 457 + src/conditions.h | 17 + src/conn-avoid-ref.cpp | 402 + src/conn-avoid-ref.h | 77 + src/console-output-undo-observer.cpp | 60 + src/console-output-undo-observer.h | 49 + src/context-fns.cpp | 250 + src/context-fns.h | 53 + src/debug/CMakeLists.txt | 29 + src/debug/demangle.cpp | 78 + src/debug/demangle.h | 39 + src/debug/event-tracker.h | 225 + src/debug/event.h | 81 + src/debug/gc-heap.h | 53 + src/debug/gdk-event-latency-tracker.cpp | 81 + src/debug/gdk-event-latency-tracker.h | 57 + src/debug/heap.cpp | 62 + src/debug/heap.h | 63 + src/debug/log-display-config.cpp | 79 + src/debug/log-display-config.h | 36 + src/debug/logger.cpp | 231 + src/debug/logger.h | 247 + src/debug/simple-event.h | 87 + src/debug/sysv-heap.cpp | 80 + src/debug/sysv-heap.h | 48 + src/debug/timestamp.cpp | 46 + src/debug/timestamp.h | 39 + src/deptool.cpp | 1459 ++ src/desktop-events.cpp | 551 + src/desktop-events.h | 54 + src/desktop-style.cpp | 1975 ++ src/desktop-style.h | 111 + src/desktop.cpp | 2229 ++ src/desktop.h | 643 + src/device-manager.cpp | 692 + src/device-manager.h | 88 + src/display/CMakeLists.txt | 130 + src/display/README | 17 + src/display/cairo-templates.h | 700 + src/display/cairo-utils.cpp | 1746 ++ src/display/cairo-utils.h | 267 + src/display/canvas-arena.cpp | 385 + src/display/canvas-arena.h | 72 + src/display/canvas-axonomgrid.cpp | 758 + src/display/canvas-axonomgrid.h | 99 + src/display/canvas-bpath.cpp | 253 + src/display/canvas-bpath.h | 114 + src/display/canvas-debug.cpp | 98 + src/display/canvas-debug.h | 42 + src/display/canvas-grid.cpp | 1143 + src/display/canvas-grid.h | 216 + src/display/canvas-rotate.cpp | 291 + src/display/canvas-rotate.h | 51 + src/display/canvas-temporary-item-list.cpp | 82 + src/display/canvas-temporary-item-list.h | 61 + src/display/canvas-temporary-item.cpp | 80 + src/display/canvas-temporary-item.h | 59 + src/display/canvas-text.cpp | 323 + src/display/canvas-text.h | 85 + src/display/curve.cpp | 713 + src/display/curve.h | 106 + src/display/drawing-context.cpp | 158 + src/display/drawing-context.h | 153 + src/display/drawing-group.cpp | 164 + src/display/drawing-group.h | 59 + src/display/drawing-image.cpp | 254 + src/display/drawing-image.h | 66 + src/display/drawing-item.cpp | 1221 + src/display/drawing-item.h | 256 + src/display/drawing-pattern.cpp | 198 + src/display/drawing-pattern.h | 87 + src/display/drawing-shape.cpp | 441 + src/display/drawing-shape.h | 69 + src/display/drawing-surface.cpp | 399 + src/display/drawing-surface.h | 100 + src/display/drawing-text.cpp | 758 + src/display/drawing-text.h | 94 + src/display/drawing.cpp | 258 + src/display/drawing.h | 127 + src/display/gnome-canvas-acetate.cpp | 71 + src/display/gnome-canvas-acetate.h | 49 + src/display/grayscale.cpp | 105 + src/display/grayscale.h | 38 + src/display/guideline.cpp | 301 + src/display/guideline.h | 74 + src/display/nr-3dutils.cpp | 66 + src/display/nr-3dutils.h | 105 + src/display/nr-filter-blend.cpp | 177 + src/display/nr-filter-blend.h | 67 + src/display/nr-filter-colormatrix.cpp | 229 + src/display/nr-filter-colormatrix.h | 79 + src/display/nr-filter-component-transfer.cpp | 246 + src/display/nr-filter-component-transfer.h | 67 + src/display/nr-filter-composite.cpp | 196 + src/display/nr-filter-composite.h | 61 + src/display/nr-filter-convolve-matrix.cpp | 243 + src/display/nr-filter-convolve-matrix.h | 76 + src/display/nr-filter-diffuselighting.cpp | 239 + src/display/nr-filter-diffuselighting.h | 70 + src/display/nr-filter-displacement-map.cpp | 172 + src/display/nr-filter-displacement-map.h | 60 + src/display/nr-filter-flood.cpp | 143 + src/display/nr-filter-flood.h | 60 + src/display/nr-filter-gaussian.cpp | 758 + src/display/nr-filter-gaussian.h | 84 + src/display/nr-filter-image.cpp | 344 + src/display/nr-filter-image.h | 68 + src/display/nr-filter-merge.cpp | 126 + src/display/nr-filter-merge.h | 55 + src/display/nr-filter-morphology.cpp | 264 + src/display/nr-filter-morphology.h | 64 + src/display/nr-filter-offset.cpp | 114 + src/display/nr-filter-offset.h | 56 + src/display/nr-filter-primitive.cpp | 197 + src/display/nr-filter-primitive.h | 157 + src/display/nr-filter-skeleton.cpp | 69 + src/display/nr-filter-skeleton.h | 60 + src/display/nr-filter-slot.cpp | 301 + src/display/nr-filter-slot.h | 131 + src/display/nr-filter-specularlighting.cpp | 248 + src/display/nr-filter-specularlighting.h | 72 + src/display/nr-filter-tile.cpp | 141 + src/display/nr-filter-tile.h | 49 + src/display/nr-filter-turbulence.cpp | 446 + src/display/nr-filter-turbulence.h | 96 + src/display/nr-filter-types.h | 71 + src/display/nr-filter-units.cpp | 176 + src/display/nr-filter-units.h | 169 + src/display/nr-filter-utils.h | 83 + src/display/nr-filter.cpp | 499 + src/display/nr-filter.h | 224 + src/display/nr-light-types.h | 36 + src/display/nr-light.cpp | 120 + src/display/nr-light.h | 174 + src/display/nr-style.cpp | 431 + src/display/nr-style.h | 149 + src/display/nr-svgfonts.cpp | 434 + src/display/nr-svgfonts.h | 71 + src/display/rendermode.h | 46 + src/display/snap-indicator.cpp | 438 + src/display/snap-indicator.h | 71 + src/display/sodipodi-ctrl.cpp | 695 + src/display/sodipodi-ctrl.h | 90 + src/display/sodipodi-ctrlrect.cpp | 323 + src/display/sodipodi-ctrlrect.h | 86 + src/display/sp-canvas-group.h | 44 + src/display/sp-canvas-item.h | 158 + src/display/sp-canvas-util.cpp | 111 + src/display/sp-canvas-util.h | 59 + src/display/sp-canvas.cpp | 2959 +++ src/display/sp-canvas.h | 302 + src/display/sp-ctrlcurve.cpp | 192 + src/display/sp-ctrlcurve.h | 55 + src/display/sp-ctrlline.cpp | 173 + src/display/sp-ctrlline.h | 62 + src/display/sp-ctrlquadr.cpp | 174 + src/display/sp-ctrlquadr.h | 42 + src/document-subset.cpp | 400 + src/document-subset.h | 72 + src/document-undo.cpp | 353 + src/document-undo.h | 94 + src/document.cpp | 2038 ++ src/document.h | 461 + src/doxygen-main.dox | 358 + src/ege-color-prof-tracker.cpp | 628 + src/ege-color-prof-tracker.h | 79 + src/enums.h | 133 + src/event-log.cpp | 485 + src/event-log.h | 162 + src/event.h | 58 + src/extension/CMakeLists.txt | 263 + src/extension/db.cpp | 310 + src/extension/db.h | 95 + src/extension/dbus/CMakeLists.txt | 30 + src/extension/dbus/Notes.txt | 79 + src/extension/dbus/application-interface.cpp | 206 + src/extension/dbus/application-interface.h | 123 + src/extension/dbus/application-interface.xml | 99 + src/extension/dbus/builddocs.sh | 8 + src/extension/dbus/dbus-init.cpp | 235 + src/extension/dbus/dbus-init.h | 55 + src/extension/dbus/doc/config.xsl | 8 + src/extension/dbus/doc/dbus-introspect-docs.dtd | 34 + src/extension/dbus/doc/docbook.css | 80 + src/extension/dbus/doc/inkscapeDbusRef.xml | 81 + src/extension/dbus/doc/inkscapeDbusTerms.xml | 142 + src/extension/dbus/doc/spec-to-docbook.xsl | 545 + src/extension/dbus/document-interface.cpp | 1484 ++ src/extension/dbus/document-interface.h | 421 + src/extension/dbus/document-interface.xml | 1530 ++ src/extension/dbus/org.inkscape.service.in | 6 + src/extension/dbus/proposed-interface.xml | 142 + src/extension/dbus/pytester.py | 291 + src/extension/dbus/wrapper/inkdbus.pc.in | 15 + src/extension/dbus/wrapper/inkscape-dbus-wrapper.c | 787 + src/extension/dbus/wrapper/inkscape-dbus-wrapper.h | 347 + src/extension/dependency.cpp | 358 + src/extension/dependency.h | 98 + src/extension/effect.cpp | 393 + src/extension/effect.h | 147 + src/extension/error-file.cpp | 111 + src/extension/error-file.h | 46 + src/extension/execution-env.cpp | 239 + src/extension/execution-env.h | 121 + src/extension/extension.cpp | 1014 + src/extension/extension.h | 281 + src/extension/find_extension_by_mime.h | 39 + src/extension/implementation/implementation.cpp | 70 + src/extension/implementation/implementation.h | 204 + src/extension/implementation/script.cpp | 978 + src/extension/implementation/script.h | 135 + src/extension/implementation/xslt.cpp | 253 + src/extension/implementation/xslt.h | 66 + src/extension/init.cpp | 295 + src/extension/init.h | 37 + src/extension/input.cpp | 260 + src/extension/input.h | 72 + .../internal/bitmap/adaptiveThreshold.cpp | 58 + src/extension/internal/bitmap/adaptiveThreshold.h | 32 + src/extension/internal/bitmap/addNoise.cpp | 69 + src/extension/internal/bitmap/addNoise.h | 30 + src/extension/internal/bitmap/blur.cpp | 56 + src/extension/internal/bitmap/blur.h | 31 + src/extension/internal/bitmap/channel.cpp | 75 + src/extension/internal/bitmap/channel.h | 32 + src/extension/internal/bitmap/charcoal.cpp | 56 + src/extension/internal/bitmap/charcoal.h | 31 + src/extension/internal/bitmap/colorize.cpp | 67 + src/extension/internal/bitmap/colorize.h | 34 + src/extension/internal/bitmap/contrast.cpp | 57 + src/extension/internal/bitmap/contrast.h | 30 + src/extension/internal/bitmap/crop.cpp | 87 + src/extension/internal/bitmap/crop.h | 35 + src/extension/internal/bitmap/cycleColormap.cpp | 54 + src/extension/internal/bitmap/cycleColormap.h | 29 + src/extension/internal/bitmap/despeckle.cpp | 52 + src/extension/internal/bitmap/despeckle.h | 27 + src/extension/internal/bitmap/edge.cpp | 54 + src/extension/internal/bitmap/edge.h | 29 + src/extension/internal/bitmap/emboss.cpp | 56 + src/extension/internal/bitmap/emboss.h | 31 + src/extension/internal/bitmap/enhance.cpp | 51 + src/extension/internal/bitmap/enhance.h | 28 + src/extension/internal/bitmap/equalize.cpp | 51 + src/extension/internal/bitmap/equalize.h | 28 + src/extension/internal/bitmap/gaussianBlur.cpp | 56 + src/extension/internal/bitmap/gaussianBlur.h | 31 + src/extension/internal/bitmap/imagemagick.cpp | 255 + src/extension/internal/bitmap/imagemagick.h | 49 + src/extension/internal/bitmap/implode.cpp | 54 + src/extension/internal/bitmap/implode.h | 30 + src/extension/internal/bitmap/level.cpp | 60 + src/extension/internal/bitmap/level.h | 32 + src/extension/internal/bitmap/levelChannel.cpp | 82 + src/extension/internal/bitmap/levelChannel.h | 33 + src/extension/internal/bitmap/medianFilter.cpp | 54 + src/extension/internal/bitmap/medianFilter.h | 30 + src/extension/internal/bitmap/modulate.cpp | 59 + src/extension/internal/bitmap/modulate.h | 32 + src/extension/internal/bitmap/negate.cpp | 52 + src/extension/internal/bitmap/negate.h | 28 + src/extension/internal/bitmap/normalize.cpp | 52 + src/extension/internal/bitmap/normalize.h | 28 + src/extension/internal/bitmap/oilPaint.cpp | 54 + src/extension/internal/bitmap/oilPaint.h | 30 + src/extension/internal/bitmap/opacity.cpp | 55 + src/extension/internal/bitmap/opacity.h | 30 + src/extension/internal/bitmap/raise.cpp | 59 + src/extension/internal/bitmap/raise.h | 32 + src/extension/internal/bitmap/reduceNoise.cpp | 57 + src/extension/internal/bitmap/reduceNoise.h | 30 + src/extension/internal/bitmap/sample.cpp | 57 + src/extension/internal/bitmap/sample.h | 31 + src/extension/internal/bitmap/shade.cpp | 59 + src/extension/internal/bitmap/shade.h | 32 + src/extension/internal/bitmap/sharpen.cpp | 56 + src/extension/internal/bitmap/sharpen.h | 31 + src/extension/internal/bitmap/solarize.cpp | 56 + src/extension/internal/bitmap/solarize.h | 30 + src/extension/internal/bitmap/spread.cpp | 54 + src/extension/internal/bitmap/spread.h | 30 + src/extension/internal/bitmap/swirl.cpp | 54 + src/extension/internal/bitmap/swirl.h | 30 + src/extension/internal/bitmap/threshold.cpp | 55 + src/extension/internal/bitmap/threshold.h | 30 + src/extension/internal/bitmap/unsharpmask.cpp | 61 + src/extension/internal/bitmap/unsharpmask.h | 33 + src/extension/internal/bitmap/wave.cpp | 56 + src/extension/internal/bitmap/wave.h | 31 + src/extension/internal/bluredge.cpp | 157 + src/extension/internal/bluredge.h | 47 + src/extension/internal/cairo-ps-out.cpp | 402 + src/extension/internal/cairo-ps-out.h | 63 + src/extension/internal/cairo-render-context.cpp | 1985 ++ src/extension/internal/cairo-render-context.h | 266 + src/extension/internal/cairo-renderer-pdf-out.cpp | 275 + src/extension/internal/cairo-renderer-pdf-out.h | 47 + src/extension/internal/cairo-renderer.cpp | 985 + src/extension/internal/cairo-renderer.h | 83 + src/extension/internal/cdr-input.cpp | 391 + src/extension/internal/cdr-input.h | 55 + src/extension/internal/clear-n_.h | 33 + src/extension/internal/emf-inout.cpp | 3686 +++ src/extension/internal/emf-inout.h | 250 + src/extension/internal/emf-print.cpp | 2217 ++ src/extension/internal/emf-print.h | 99 + src/extension/internal/filter/BUILD_YOUR_OWN | 2 + src/extension/internal/filter/bevels.h | 283 + src/extension/internal/filter/blurs.h | 420 + src/extension/internal/filter/bumps.h | 486 + src/extension/internal/filter/color.h | 1886 ++ src/extension/internal/filter/distort.h | 250 + src/extension/internal/filter/filter-all.cpp | 128 + src/extension/internal/filter/filter-file.cpp | 137 + src/extension/internal/filter/filter.cpp | 235 + src/extension/internal/filter/filter.h | 62 + src/extension/internal/filter/image.h | 114 + src/extension/internal/filter/morphology.h | 326 + src/extension/internal/filter/overlays.h | 148 + src/extension/internal/filter/paint.h | 1029 + src/extension/internal/filter/protrusions.h | 100 + src/extension/internal/filter/shadows.h | 190 + src/extension/internal/filter/textures.h | 159 + src/extension/internal/filter/transparency.h | 400 + src/extension/internal/gdkpixbuf-input.cpp | 254 + src/extension/internal/gdkpixbuf-input.h | 40 + src/extension/internal/gimpgrad.cpp | 292 + src/extension/internal/gimpgrad.h | 50 + src/extension/internal/grid.cpp | 226 + src/extension/internal/grid.h | 47 + src/extension/internal/image-resolution.cpp | 447 + src/extension/internal/image-resolution.h | 42 + src/extension/internal/latex-pstricks-out.cpp | 116 + src/extension/internal/latex-pstricks-out.h | 50 + src/extension/internal/latex-pstricks.cpp | 348 + src/extension/internal/latex-pstricks.h | 81 + src/extension/internal/latex-text-renderer.cpp | 754 + src/extension/internal/latex-text-renderer.h | 98 + src/extension/internal/metafile-inout.cpp | 294 + src/extension/internal/metafile-inout.h | 93 + src/extension/internal/metafile-print.cpp | 464 + src/extension/internal/metafile-print.h | 127 + src/extension/internal/odf.cpp | 2131 ++ src/extension/internal/odf.h | 329 + src/extension/internal/pdfinput/pdf-input.cpp | 988 + src/extension/internal/pdfinput/pdf-input.h | 163 + src/extension/internal/pdfinput/pdf-parser.cpp | 3398 +++ src/extension/internal/pdfinput/pdf-parser.h | 356 + .../internal/pdfinput/poppler-transition-api.h | 87 + src/extension/internal/pdfinput/svg-builder.cpp | 1938 ++ src/extension/internal/pdfinput/svg-builder.h | 250 + src/extension/internal/polyfill/README.md | 19 + src/extension/internal/polyfill/hatch.js | 400 + .../internal/polyfill/hatch_compressed.include | 4 + .../internal/polyfill/hatch_tests/hatch.svg | 63 + .../polyfill/hatch_tests/hatch01_with_js.svg | 134 + .../internal/polyfill/hatch_tests/hatch_test.svg | 11731 +++++++++ src/extension/internal/polyfill/mesh.js | 1188 + .../internal/polyfill/mesh_compressed.include | 4 + src/extension/internal/pov-out.cpp | 742 + src/extension/internal/pov-out.h | 190 + src/extension/internal/svg.cpp | 1045 + src/extension/internal/svg.h | 67 + src/extension/internal/svgz.cpp | 99 + src/extension/internal/svgz.h | 42 + src/extension/internal/text_reassemble.c | 2975 +++ src/extension/internal/text_reassemble.h | 397 + src/extension/internal/vsd-input.cpp | 391 + src/extension/internal/vsd-input.h | 55 + src/extension/internal/wmf-inout.cpp | 3263 +++ src/extension/internal/wmf-inout.h | 238 + src/extension/internal/wmf-print.cpp | 1612 ++ src/extension/internal/wmf-print.h | 90 + src/extension/internal/wpg-input.cpp | 181 + src/extension/internal/wpg-input.h | 53 + src/extension/loader.cpp | 140 + src/extension/loader.h | 77 + src/extension/output.cpp | 253 + src/extension/output.h | 69 + src/extension/patheffect.cpp | 91 + src/extension/patheffect.h | 46 + src/extension/plugins/CMakeLists.txt | 2 + src/extension/plugins/grid2/CMakeLists.txt | 9 + src/extension/plugins/grid2/grid.cpp | 213 + src/extension/plugins/grid2/grid.h | 59 + src/extension/plugins/grid2/libgrid2.inx | 21 + src/extension/prefdialog/parameter-bool.cpp | 141 + src/extension/prefdialog/parameter-bool.h | 77 + src/extension/prefdialog/parameter-color.cpp | 146 + src/extension/prefdialog/parameter-color.h | 77 + src/extension/prefdialog/parameter-float.cpp | 193 + src/extension/prefdialog/parameter-float.h | 79 + src/extension/prefdialog/parameter-int.cpp | 189 + src/extension/prefdialog/parameter-int.h | 74 + src/extension/prefdialog/parameter-notebook.cpp | 285 + src/extension/prefdialog/parameter-notebook.h | 86 + src/extension/prefdialog/parameter-optiongroup.cpp | 351 + src/extension/prefdialog/parameter-optiongroup.h | 103 + src/extension/prefdialog/parameter-path.cpp | 282 + src/extension/prefdialog/parameter-path.h | 75 + src/extension/prefdialog/parameter-string.cpp | 228 + src/extension/prefdialog/parameter-string.h | 66 + src/extension/prefdialog/parameter.cpp | 304 + src/extension/prefdialog/parameter.h | 162 + src/extension/prefdialog/prefdialog.cpp | 241 + src/extension/prefdialog/prefdialog.h | 91 + src/extension/prefdialog/widget-box.cpp | 115 + src/extension/prefdialog/widget-box.h | 60 + src/extension/prefdialog/widget-image.cpp | 87 + src/extension/prefdialog/widget-image.h | 61 + src/extension/prefdialog/widget-label.cpp | 121 + src/extension/prefdialog/widget-label.h | 65 + src/extension/prefdialog/widget-separator.cpp | 43 + src/extension/prefdialog/widget-separator.h | 53 + src/extension/prefdialog/widget-spacer.cpp | 62 + src/extension/prefdialog/widget-spacer.h | 60 + src/extension/prefdialog/widget.cpp | 178 + src/extension/prefdialog/widget.h | 149 + src/extension/print.cpp | 130 + src/extension/print.h | 90 + src/extension/system.cpp | 734 + src/extension/system.h | 104 + src/extension/timer.cpp | 214 + src/extension/timer.h | 75 + src/extract-uri.cpp | 111 + src/extract-uri.h | 51 + src/file-update.cpp | 643 + src/file.cpp | 1303 + src/file.h | 184 + src/fill-or-stroke.h | 17 + src/filter-chemistry.cpp | 601 + src/filter-chemistry.h | 50 + src/filter-enums.cpp | 140 + src/filter-enums.h | 86 + src/gc-anchored.cpp | 95 + src/gc-anchored.h | 177 + src/gc-finalized.cpp | 72 + src/gc-finalized.h | 144 + src/gradient-chemistry.cpp | 1663 ++ src/gradient-chemistry.h | 121 + src/gradient-drag.cpp | 3077 +++ src/gradient-drag.h | 251 + src/graphlayout.cpp | 237 + src/graphlayout.h | 28 + src/guide-snapper.cpp | 104 + src/guide-snapper.h | 57 + src/help.cpp | 86 + src/help.h | 41 + src/helper-fns.h | 104 + src/helper/CMakeLists.txt | 52 + src/helper/README | 8 + src/helper/action-context.cpp | 79 + src/helper/action-context.h | 90 + src/helper/action.cpp | 261 + src/helper/action.h | 101 + src/helper/geom-curves.h | 55 + src/helper/geom-nodetype.cpp | 59 + src/helper/geom-nodetype.h | 57 + src/helper/geom-pathstroke.cpp | 1160 + src/helper/geom-pathstroke.h | 109 + src/helper/geom-pathvectorsatellites.cpp | 247 + src/helper/geom-pathvectorsatellites.h | 59 + src/helper/geom-satellite.cpp | 249 + src/helper/geom-satellite.h | 110 + src/helper/geom.cpp | 893 + src/helper/geom.h | 50 + src/helper/gettext.cpp | 99 + src/helper/gettext.h | 33 + src/helper/mathfns.h | 83 + src/helper/pixbuf-ops.cpp | 165 + src/helper/pixbuf-ops.h | 29 + src/helper/png-write.cpp | 508 + src/helper/png-write.h | 50 + src/helper/sp-marshal.list | 10 + src/helper/stock-items.cpp | 275 + src/helper/stock-items.h | 21 + src/helper/verb-action.cpp | 180 + src/helper/verb-action.h | 87 + src/id-clash.cpp | 442 + src/id-clash.h | 30 + src/include/CMakeLists.txt | 8 + src/include/README | 3 + src/include/glibmm_version.h | 45 + src/include/gtkmm_version.h | 45 + src/include/macros.h | 52 + src/include/source_date_epoch.h | 68 + src/inkgc/CMakeLists.txt | 15 + src/inkgc/README | 8 + src/inkgc/gc-alloc.h | 88 + src/inkgc/gc-core.h | 202 + src/inkgc/gc-managed.h | 60 + src/inkgc/gc-soft-ptr.h | 70 + src/inkgc/gc.cpp | 313 + src/inkscape-application.cpp | 1497 ++ src/inkscape-application.h | 193 + src/inkscape-main.cpp | 262 + src/inkscape-manifest.xml | 22 + src/inkscape-version.cpp.in | 7 + src/inkscape-version.h | 36 + src/inkscape-window.cpp | 194 + src/inkscape-window.h | 77 + src/inkscape.cpp | 1200 + src/inkscape.h | 248 + src/inkscape.rc | 30 + src/inkview-application.cpp | 198 + src/inkview-application.h | 63 + src/inkview-main.cpp | 55 + src/inkview-window.cpp | 452 + src/inkview-window.h | 84 + src/io/CMakeLists.txt | 36 + src/io/README | 35 + src/io/crystalegg.xml | 767 + src/io/dir-util.cpp | 263 + src/io/dir-util.h | 75 + src/io/doc2html.xsl | 64 + src/io/file-export-cmd.cpp | 786 + src/io/file-export-cmd.h | 80 + src/io/file.cpp | 148 + src/io/file.h | 40 + src/io/http.cpp | 153 + src/io/http.h | 41 + src/io/resource-manager.cpp | 450 + src/io/resource-manager.h | 48 + src/io/resource.cpp | 477 + src/io/resource.h | 111 + src/io/stream/Makefile.tst | 48 + src/io/stream/README | 13 + src/io/stream/bufferstream.cpp | 144 + src/io/stream/bufferstream.h | 98 + src/io/stream/gzipstream.cpp | 459 + src/io/stream/gzipstream.h | 124 + src/io/stream/inkscapestream.cpp | 800 + src/io/stream/inkscapestream.h | 668 + src/io/stream/streamtest.cpp | 256 + src/io/stream/stringstream.cpp | 125 + src/io/stream/stringstream.h | 105 + src/io/stream/uristream.cpp | 231 + src/io/stream/uristream.h | 101 + src/io/stream/xsltstream.cpp | 248 + src/io/stream/xsltstream.h | 140 + src/io/sys.cpp | 361 + src/io/sys.h | 69 + src/knot-enums.h | 64 + src/knot-holder-entity.cpp | 456 + src/knot-holder-entity.h | 215 + src/knot-ptr.cpp | 34 + src/knot-ptr.h | 17 + src/knot.cpp | 602 + src/knot.h | 187 + src/knotholder.cpp | 467 + src/knotholder.h | 115 + src/layer-fns.cpp | 207 + src/layer-fns.h | 44 + src/layer-manager.cpp | 356 + src/layer-manager.h | 79 + src/layer-model.cpp | 254 + src/layer-model.h | 101 + src/libnrtype/CMakeLists.txt | 33 + src/libnrtype/FontFactory.cpp | 861 + src/libnrtype/FontFactory.h | 203 + src/libnrtype/FontInstance.cpp | 998 + src/libnrtype/Layout-TNG-Compute.cpp | 2318 ++ src/libnrtype/Layout-TNG-Input.cpp | 242 + src/libnrtype/Layout-TNG-OutIter.cpp | 1172 + src/libnrtype/Layout-TNG-Output.cpp | 913 + src/libnrtype/Layout-TNG-Scanline-Maker.h | 186 + src/libnrtype/Layout-TNG-Scanline-Makers.cpp | 196 + src/libnrtype/Layout-TNG.cpp | 48 + src/libnrtype/Layout-TNG.h | 1209 + src/libnrtype/OpenTypeUtil.cpp | 434 + src/libnrtype/OpenTypeUtil.h | 112 + src/libnrtype/font-glyph.h | 36 + src/libnrtype/font-instance.h | 156 + src/libnrtype/font-lister.cpp | 1256 + src/libnrtype/font-lister.h | 360 + src/libnrtype/font-style.h | 44 + src/line-geometry.cpp | 222 + src/line-geometry.h | 103 + src/line-snapper.cpp | 172 + src/line-snapper.h | 64 + src/livarot/AVL.cpp | 969 + src/livarot/AVL.h | 104 + src/livarot/AlphaLigne.cpp | 308 + src/livarot/AlphaLigne.h | 87 + src/livarot/BitLigne.cpp | 180 + src/livarot/BitLigne.h | 64 + src/livarot/CMakeLists.txt | 44 + src/livarot/Livarot.h | 43 + src/livarot/LivarotDefs.h | 158 + src/livarot/Path.cpp | 939 + src/livarot/Path.h | 416 + src/livarot/PathConversion.cpp | 1579 ++ src/livarot/PathCutting.cpp | 1534 ++ src/livarot/PathOutline.cpp | 1526 ++ src/livarot/PathSimplify.cpp | 1404 + src/livarot/PathStroke.cpp | 763 + src/livarot/README | 17 + src/livarot/Shape.cpp | 2317 ++ src/livarot/Shape.h | 577 + src/livarot/ShapeDraw.cpp | 114 + src/livarot/ShapeMisc.cpp | 1457 ++ src/livarot/ShapeRaster.cpp | 2014 ++ src/livarot/ShapeSweep.cpp | 3319 +++ src/livarot/float-line.cpp | 916 + src/livarot/float-line.h | 145 + src/livarot/int-line.cpp | 1071 + src/livarot/int-line.h | 119 + src/livarot/path-description.cpp | 180 + src/livarot/path-description.h | 185 + src/livarot/sweep-event-queue.h | 60 + src/livarot/sweep-event.cpp | 284 + src/livarot/sweep-event.h | 54 + src/livarot/sweep-tree-list.cpp | 56 + src/livarot/sweep-tree-list.h | 49 + src/livarot/sweep-tree.cpp | 567 + src/livarot/sweep-tree.h | 91 + src/live_effects/CMakeLists.txt | 189 + src/live_effects/README | 8 + src/live_effects/effect-enum.h | 288 + src/live_effects/effect.cpp | 1791 ++ src/live_effects/effect.h | 216 + src/live_effects/lpe-angle_bisector.cpp | 150 + src/live_effects/lpe-angle_bisector.h | 63 + src/live_effects/lpe-attach-path.cpp | 189 + src/live_effects/lpe-attach-path.h | 53 + src/live_effects/lpe-bendpath.cpp | 258 + src/live_effects/lpe-bendpath.h | 77 + src/live_effects/lpe-bool.cpp | 521 + src/live_effects/lpe-bool.h | 70 + src/live_effects/lpe-bounding-box.cpp | 62 + src/live_effects/lpe-bounding-box.h | 38 + src/live_effects/lpe-bspline.cpp | 487 + src/live_effects/lpe-bspline.h | 54 + src/live_effects/lpe-circle_3pts.cpp | 90 + src/live_effects/lpe-circle_3pts.h | 52 + src/live_effects/lpe-circle_with_radius.cpp | 83 + src/live_effects/lpe-circle_with_radius.h | 55 + src/live_effects/lpe-clone-original.cpp | 410 + src/live_effects/lpe-clone-original.h | 58 + src/live_effects/lpe-constructgrid.cpp | 91 + src/live_effects/lpe-constructgrid.h | 42 + src/live_effects/lpe-copy_rotate.cpp | 694 + src/live_effects/lpe-copy_rotate.h | 104 + src/live_effects/lpe-curvestitch.cpp | 198 + src/live_effects/lpe-curvestitch.h | 54 + src/live_effects/lpe-dashed-stroke.cpp | 282 + src/live_effects/lpe-dashed-stroke.h | 36 + src/live_effects/lpe-dynastroke.cpp | 298 + src/live_effects/lpe-dynastroke.h | 68 + src/live_effects/lpe-ellipse_5pts.cpp | 212 + src/live_effects/lpe-ellipse_5pts.h | 51 + src/live_effects/lpe-embrodery-stitch-ordering.cpp | 1141 + src/live_effects/lpe-embrodery-stitch-ordering.h | 314 + src/live_effects/lpe-embrodery-stitch.cpp | 388 + src/live_effects/lpe-embrodery-stitch.h | 77 + src/live_effects/lpe-envelope.cpp | 256 + src/live_effects/lpe-envelope.h | 59 + src/live_effects/lpe-extrude.cpp | 194 + src/live_effects/lpe-extrude.h | 53 + src/live_effects/lpe-fill-between-many.cpp | 215 + src/live_effects/lpe-fill-between-many.h | 48 + src/live_effects/lpe-fill-between-strokes.cpp | 149 + src/live_effects/lpe-fill-between-strokes.h | 40 + src/live_effects/lpe-fillet-chamfer.cpp | 699 + src/live_effects/lpe-fillet-chamfer.h | 88 + src/live_effects/lpe-gears.cpp | 279 + src/live_effects/lpe-gears.h | 40 + src/live_effects/lpe-interpolate.cpp | 184 + src/live_effects/lpe-interpolate.h | 61 + src/live_effects/lpe-interpolate_points.cpp | 90 + src/live_effects/lpe-interpolate_points.h | 52 + src/live_effects/lpe-jointype.cpp | 203 + src/live_effects/lpe-jointype.h | 58 + src/live_effects/lpe-knot.cpp | 735 + src/live_effects/lpe-knot.h | 105 + src/live_effects/lpe-lattice.cpp | 310 + src/live_effects/lpe-lattice.h | 69 + src/live_effects/lpe-lattice2.cpp | 679 + src/live_effects/lpe-lattice2.h | 105 + src/live_effects/lpe-line_segment.cpp | 92 + src/live_effects/lpe-line_segment.h | 65 + src/live_effects/lpe-measure-segments.cpp | 1285 + src/live_effects/lpe-measure-segments.h | 112 + src/live_effects/lpe-mirror_symmetry.cpp | 606 + src/live_effects/lpe-mirror_symmetry.h | 84 + src/live_effects/lpe-offset.cpp | 575 + src/live_effects/lpe-offset.h | 82 + src/live_effects/lpe-parallel.cpp | 176 + src/live_effects/lpe-parallel.h | 76 + src/live_effects/lpe-path_length.cpp | 83 + src/live_effects/lpe-path_length.h | 56 + src/live_effects/lpe-patternalongpath.cpp | 375 + src/live_effects/lpe-patternalongpath.h | 88 + src/live_effects/lpe-perp_bisector.cpp | 180 + src/live_effects/lpe-perp_bisector.h | 71 + src/live_effects/lpe-perspective-envelope.cpp | 576 + src/live_effects/lpe-perspective-envelope.h | 80 + src/live_effects/lpe-powerclip.cpp | 341 + src/live_effects/lpe-powerclip.h | 45 + src/live_effects/lpe-powermask.cpp | 379 + src/live_effects/lpe-powermask.h | 49 + src/live_effects/lpe-powerstroke-interpolators.h | 326 + src/live_effects/lpe-powerstroke.cpp | 869 + src/live_effects/lpe-powerstroke.h | 80 + src/live_effects/lpe-pts2ellipse.cpp | 772 + src/live_effects/lpe-pts2ellipse.h | 108 + src/live_effects/lpe-recursiveskeleton.cpp | 121 + src/live_effects/lpe-recursiveskeleton.h | 51 + src/live_effects/lpe-rough-hatches.cpp | 585 + src/live_effects/lpe-rough-hatches.h | 77 + src/live_effects/lpe-roughen.cpp | 559 + src/live_effects/lpe-roughen.h | 76 + src/live_effects/lpe-ruler.cpp | 204 + src/live_effects/lpe-ruler.h | 84 + src/live_effects/lpe-show_handles.cpp | 249 + src/live_effects/lpe-show_handles.h | 64 + src/live_effects/lpe-simplify.cpp | 313 + src/live_effects/lpe-simplify.h | 57 + src/live_effects/lpe-skeleton.cpp | 124 + src/live_effects/lpe-skeleton.h | 70 + src/live_effects/lpe-sketch.cpp | 384 + src/live_effects/lpe-sketch.h | 82 + src/live_effects/lpe-spiro.cpp | 149 + src/live_effects/lpe-spiro.h | 34 + src/live_effects/lpe-tangent_to_curve.cpp | 201 + src/live_effects/lpe-tangent_to_curve.h | 67 + src/live_effects/lpe-taperstroke.cpp | 557 + src/live_effects/lpe-taperstroke.h | 74 + src/live_effects/lpe-test-doEffect-stack.cpp | 78 + src/live_effects/lpe-test-doEffect-stack.h | 46 + src/live_effects/lpe-text_label.cpp | 62 + src/live_effects/lpe-text_label.h | 52 + src/live_effects/lpe-transform_2pts.cpp | 480 + src/live_effects/lpe-transform_2pts.h | 94 + src/live_effects/lpe-vonkoch.cpp | 316 + src/live_effects/lpe-vonkoch.h | 82 + src/live_effects/lpegroupbbox.cpp | 95 + src/live_effects/lpegroupbbox.h | 34 + src/live_effects/lpeobject-reference.cpp | 158 + src/live_effects/lpeobject-reference.h | 73 + src/live_effects/lpeobject.cpp | 218 + src/live_effects/lpeobject.h | 75 + src/live_effects/parameter/array.cpp | 102 + src/live_effects/parameter/array.h | 162 + src/live_effects/parameter/bool.cpp | 113 + src/live_effects/parameter/bool.h | 57 + src/live_effects/parameter/colorpicker.cpp | 148 + src/live_effects/parameter/colorpicker.h | 62 + src/live_effects/parameter/enum.h | 113 + src/live_effects/parameter/fontbutton.cpp | 106 + src/live_effects/parameter/fontbutton.h | 63 + src/live_effects/parameter/hidden.cpp | 94 + src/live_effects/parameter/hidden.h | 71 + src/live_effects/parameter/item-reference.cpp | 45 + src/live_effects/parameter/item-reference.h | 57 + src/live_effects/parameter/item.cpp | 296 + src/live_effects/parameter/item.h | 81 + src/live_effects/parameter/message.cpp | 127 + src/live_effects/parameter/message.h | 78 + src/live_effects/parameter/originalitem.cpp | 107 + src/live_effects/parameter/originalitem.h | 46 + src/live_effects/parameter/originalitemarray.cpp | 459 + src/live_effects/parameter/originalitemarray.h | 124 + src/live_effects/parameter/originalpath.cpp | 148 + src/live_effects/parameter/originalpath.h | 55 + src/live_effects/parameter/originalpatharray.cpp | 542 + src/live_effects/parameter/originalpatharray.h | 134 + src/live_effects/parameter/parameter.cpp | 208 + src/live_effects/parameter/parameter.h | 158 + src/live_effects/parameter/path-reference.cpp | 44 + src/live_effects/parameter/path-reference.h | 57 + src/live_effects/parameter/path.cpp | 591 + src/live_effects/parameter/path.h | 112 + src/live_effects/parameter/point.cpp | 268 + src/live_effects/parameter/point.h | 77 + .../parameter/powerstrokepointarray.cpp | 308 + src/live_effects/parameter/powerstrokepointarray.h | 93 + src/live_effects/parameter/random.cpp | 212 + src/live_effects/parameter/random.h | 73 + src/live_effects/parameter/satellitesarray.cpp | 575 + src/live_effects/parameter/satellitesarray.h | 114 + src/live_effects/parameter/text.cpp | 175 + src/live_effects/parameter/text.h | 97 + src/live_effects/parameter/togglebutton.cpp | 210 + src/live_effects/parameter/togglebutton.h | 73 + src/live_effects/parameter/transformedpoint.cpp | 212 + src/live_effects/parameter/transformedpoint.h | 86 + src/live_effects/parameter/unit.cpp | 110 + src/live_effects/parameter/unit.h | 56 + src/live_effects/parameter/vector.cpp | 251 + src/live_effects/parameter/vector.h | 87 + src/live_effects/spiro-converters.cpp | 136 + src/live_effects/spiro-converters.h | 74 + src/live_effects/spiro.cpp | 1122 + src/live_effects/spiro.h | 42 + src/live_effects/todo.txt | 11 + src/manipulation/README | 16 + src/media.cpp | 36 + src/media.h | 33 + src/menus-skeleton.h | 42 + src/message-context.cpp | 92 + src/message-context.h | 121 + src/message-stack.cpp | 172 + src/message-stack.h | 190 + src/message.h | 51 + src/mod360.cpp | 50 + src/mod360.h | 27 + src/number-opt-number.h | 137 + src/object-hierarchy.cpp | 184 + src/object-hierarchy.h | 157 + src/object-snapper.cpp | 879 + src/object-snapper.h | 132 + src/object/CMakeLists.txt | 183 + src/object/README | 116 + src/object/box3d-side.cpp | 281 + src/object/box3d-side.h | 65 + src/object/box3d.cpp | 1360 + src/object/box3d.h | 105 + src/object/color-profile.cpp | 1334 + src/object/color-profile.h | 123 + src/object/filters/CMakeLists.txt | 54 + src/object/filters/blend.cpp | 290 + src/object/filters/blend.h | 55 + src/object/filters/colormatrix.cpp | 159 + src/object/filters/colormatrix.h | 55 + src/object/filters/componenttransfer-funcnode.cpp | 213 + src/object/filters/componenttransfer-funcnode.h | 64 + src/object/filters/componenttransfer.cpp | 189 + src/object/filters/componenttransfer.h | 59 + src/object/filters/composite.cpp | 333 + src/object/filters/composite.h | 77 + src/object/filters/convolvematrix.cpp | 318 + src/object/filters/convolvematrix.h | 67 + src/object/filters/diffuselighting.cpp | 326 + src/object/filters/diffuselighting.h | 73 + src/object/filters/displacementmap.cpp | 256 + src/object/filters/displacementmap.h | 63 + src/object/filters/distantlight.cpp | 164 + src/object/filters/distantlight.h | 59 + src/object/filters/flood.cpp | 180 + src/object/filters/flood.h | 55 + src/object/filters/gaussian-blur.cpp | 132 + src/object/filters/gaussian-blur.h | 57 + src/object/filters/image.cpp | 261 + src/object/filters/image.h | 69 + src/object/filters/merge.cpp | 115 + src/object/filters/merge.h | 48 + src/object/filters/mergenode.cpp | 106 + src/object/filters/mergenode.h | 53 + src/object/filters/morphology.cpp | 161 + src/object/filters/morphology.h | 55 + src/object/filters/offset.cpp | 137 + src/object/filters/offset.h | 52 + src/object/filters/pointlight.cpp | 190 + src/object/filters/pointlight.h | 61 + src/object/filters/sp-filter-primitive.cpp | 273 + src/object/filters/sp-filter-primitive.h | 68 + src/object/filters/specularlighting.cpp | 340 + src/object/filters/specularlighting.h | 79 + src/object/filters/spotlight.cpp | 320 + src/object/filters/spotlight.h | 77 + src/object/filters/tile.cpp | 108 + src/object/filters/tile.h | 51 + src/object/filters/turbulence.cpp | 229 + src/object/filters/turbulence.h | 64 + src/object/object-set.cpp | 390 + src/object/object-set.h | 502 + src/object/persp3d-reference.cpp | 111 + src/object/persp3d-reference.h | 69 + src/object/persp3d.cpp | 606 + src/object/persp3d.h | 131 + src/object/sp-anchor.cpp | 201 + src/object/sp-anchor.h | 43 + src/object/sp-clippath.cpp | 303 + src/object/sp-clippath.h | 136 + src/object/sp-conn-end-pair.cpp | 350 + src/object/sp-conn-end-pair.h | 97 + src/object/sp-conn-end.cpp | 309 + src/object/sp-conn-end.h | 68 + src/object/sp-defs.cpp | 106 + src/object/sp-defs.h | 45 + src/object/sp-desc.cpp | 32 + src/object/sp-desc.h | 30 + src/object/sp-dimensions.cpp | 53 + src/object/sp-dimensions.h | 42 + src/object/sp-ellipse.cpp | 767 + src/object/sp-ellipse.h | 113 + src/object/sp-factory.cpp | 367 + src/object/sp-factory.h | 44 + src/object/sp-filter-reference.cpp | 31 + src/object/sp-filter-reference.h | 43 + src/object/sp-filter-units.h | 30 + src/object/sp-filter.cpp | 518 + src/object/sp-filter.h | 109 + src/object/sp-flowdiv.cpp | 467 + src/object/sp-flowdiv.h | 105 + src/object/sp-flowregion.cpp | 398 + src/object/sp-flowregion.h | 63 + src/object/sp-flowtext.cpp | 767 + src/object/sp-flowtext.h | 116 + src/object/sp-font-face.cpp | 825 + src/object/sp-font-face.h | 124 + src/object/sp-font.cpp | 201 + src/object/sp-font.h | 47 + src/object/sp-glyph-kerning.cpp | 191 + src/object/sp-glyph-kerning.h | 75 + src/object/sp-glyph.cpp | 290 + src/object/sp-glyph.h | 73 + src/object/sp-gradient-reference.cpp | 31 + src/object/sp-gradient-reference.h | 42 + src/object/sp-gradient-spread.h | 32 + src/object/sp-gradient-units.h | 30 + src/object/sp-gradient-vector.h | 50 + src/object/sp-gradient.cpp | 1197 + src/object/sp-gradient.h | 239 + src/object/sp-guide.cpp | 579 + src/object/sp-guide.h | 113 + src/object/sp-hatch-path.cpp | 339 + src/object/sp-hatch-path.h | 96 + src/object/sp-hatch.cpp | 809 + src/object/sp-hatch.h | 197 + src/object/sp-image.cpp | 905 + src/object/sp-image.h | 82 + src/object/sp-item-group.cpp | 990 + src/object/sp-item-group.h | 126 + src/object/sp-item-rm-unsatisfied-cns.cpp | 53 + src/object/sp-item-rm-unsatisfied-cns.h | 29 + src/object/sp-item-transform.cpp | 377 + src/object/sp-item-transform.h | 33 + src/object/sp-item-update-cns.cpp | 50 + src/object/sp-item-update-cns.h | 32 + src/object/sp-item.cpp | 1827 ++ src/object/sp-item.h | 480 + src/object/sp-line.cpp | 170 + src/object/sp-line.h | 56 + src/object/sp-linear-gradient.cpp | 142 + src/object/sp-linear-gradient.h | 54 + src/object/sp-lpe-item.cpp | 1293 + src/object/sp-lpe-item.h | 121 + src/object/sp-marker-loc.h | 40 + src/object/sp-marker.cpp | 507 + src/object/sp-marker.h | 108 + src/object/sp-mask.cpp | 337 + src/object/sp-mask.h | 123 + src/object/sp-mesh-array.cpp | 3098 +++ src/object/sp-mesh-array.h | 232 + src/object/sp-mesh-gradient.cpp | 270 + src/object/sp-mesh-gradient.h | 52 + src/object/sp-mesh-patch.cpp | 138 + src/object/sp-mesh-patch.h | 52 + src/object/sp-mesh-row.cpp | 122 + src/object/sp-mesh-row.h | 47 + src/object/sp-metadata.cpp | 143 + src/object/sp-metadata.h | 40 + src/object/sp-missing-glyph.cpp | 140 + src/object/sp-missing-glyph.h | 41 + src/object/sp-namedview.cpp | 1227 + src/object/sp-namedview.h | 146 + src/object/sp-object-group.cpp | 84 + src/object/sp-object-group.h | 47 + src/object/sp-object.cpp | 1689 ++ src/object/sp-object.h | 879 + src/object/sp-offset.cpp | 1221 + src/object/sp-offset.h | 108 + src/object/sp-paint-server-reference.h | 45 + src/object/sp-paint-server.cpp | 92 + src/object/sp-paint-server.h | 106 + src/object/sp-path.cpp | 384 + src/object/sp-path.h | 68 + src/object/sp-pattern.cpp | 697 + src/object/sp-pattern.h | 149 + src/object/sp-polygon.cpp | 183 + src/object/sp-polygon.h | 36 + src/object/sp-polyline.cpp | 132 + src/object/sp-polyline.h | 41 + src/object/sp-radial-gradient.cpp | 252 + src/object/sp-radial-gradient.h | 59 + src/object/sp-rect.cpp | 652 + src/object/sp-rect.h | 91 + src/object/sp-root.cpp | 393 + src/object/sp-root.h | 79 + src/object/sp-script.cpp | 85 + src/object/sp-script.h | 48 + src/object/sp-shape-reference.cpp | 66 + src/object/sp-shape-reference.h | 56 + src/object/sp-shape.cpp | 1304 + src/object/sp-shape.h | 107 + src/object/sp-solid-color.cpp | 86 + src/object/sp-solid-color.h | 49 + src/object/sp-spiral.cpp | 594 + src/object/sp-spiral.h | 81 + src/object/sp-star.cpp | 592 + src/object/sp-star.h | 68 + src/object/sp-stop.cpp | 173 + src/object/sp-stop.h | 72 + src/object/sp-string.cpp | 177 + src/object/sp-string.h | 40 + src/object/sp-style-elem.cpp | 567 + src/object/sp-style-elem.h | 48 + src/object/sp-switch.cpp | 160 + src/object/sp-switch.h | 50 + src/object/sp-symbol.cpp | 165 + src/object/sp-symbol.h | 49 + src/object/sp-tag-use-reference.cpp | 138 + src/object/sp-tag-use-reference.h | 80 + src/object/sp-tag-use.cpp | 149 + src/object/sp-tag-use.h | 55 + src/object/sp-tag.cpp | 143 + src/object/sp-tag.h | 58 + src/object/sp-text.cpp | 1761 ++ src/object/sp-text.h | 145 + src/object/sp-textpath.h | 66 + src/object/sp-title.cpp | 32 + src/object/sp-title.h | 29 + src/object/sp-tref-reference.cpp | 107 + src/object/sp-tref-reference.h | 80 + src/object/sp-tref.cpp | 533 + src/object/sp-tref.h | 85 + src/object/sp-tspan.cpp | 538 + src/object/sp-tspan.h | 59 + src/object/sp-use-reference.cpp | 233 + src/object/sp-use-reference.h | 79 + src/object/sp-use.cpp | 767 + src/object/sp-use.h | 92 + src/object/uri-references.cpp | 290 + src/object/uri-references.h | 169 + src/object/uri.cpp | 459 + src/object/uri.h | 216 + src/object/viewbox.cpp | 278 + src/object/viewbox.h | 63 + src/path-chemistry.cpp | 752 + src/path-chemistry.h | 52 + src/path-prefix.cpp | 146 + src/path-prefix.h | 118 + src/perspective-line.cpp | 41 + src/perspective-line.h | 52 + src/plugin.def | 8 + src/preferences-skeleton.h | 514 + src/preferences.cpp | 991 + src/preferences.h | 811 + src/prefix.cpp | 430 + src/prefix.h | 134 + src/print.cpp | 146 + src/print.h | 77 + src/profile-manager.cpp | 102 + src/profile-manager.h | 57 + src/proj_pt.cpp | 121 + src/proj_pt.h | 172 + src/proofs | 335 + src/pure-transform.cpp | 374 + src/pure-transform.h | 229 + src/rdf.cpp | 1258 + src/rdf.h | 150 + src/remove-last.h | 38 + src/removeoverlap.cpp | 85 + src/removeoverlap.h | 21 + src/rubberband.cpp | 165 + src/rubberband.h | 88 + src/satisfied-guide-cns.cpp | 45 + src/satisfied-guide-cns.h | 37 + src/selcue.cpp | 259 + src/selcue.h | 88 + src/selection-chemistry.cpp | 4480 ++++ src/selection-chemistry.h | 128 + src/selection-describer.cpp | 262 + src/selection-describer.h | 54 + src/selection.cpp | 318 + src/selection.h | 252 + src/seltrans-handles.cpp | 67 + src/seltrans-handles.h | 101 + src/seltrans.cpp | 1712 ++ src/seltrans.h | 214 + src/shortcuts.cpp | 913 + src/shortcuts.h | 86 + src/show-preview.bmp | Bin 0 -> 822 bytes src/snap-candidate.h | 146 + src/snap-enums.h | 105 + src/snap-preferences.cpp | 319 + src/snap-preferences.h | 112 + src/snap.cpp | 801 + src/snap.h | 444 + src/snapped-curve.cpp | 260 + src/snapped-curve.h | 58 + src/snapped-line.cpp | 299 + src/snapped-line.h | 78 + src/snapped-point.cpp | 248 + src/snapped-point.h | 145 + src/snapper.cpp | 53 + src/snapper.h | 166 + src/sp-cursor.cpp | 152 + src/sp-cursor.h | 31 + src/sp-guide-attachment.h | 52 + src/sp-guide-constraint.h | 52 + src/sp-item-notify-moveto.cpp | 89 + src/sp-item-notify-moveto.h | 32 + src/splivarot.cpp | 2509 ++ src/splivarot.h | 72 + src/streq.h | 40 + src/strneq.h | 34 + src/style-enums.h | 704 + src/style-internal.cpp | 3229 +++ src/style-internal.h | 1296 + src/style.cpp | 1730 ++ src/style.h | 390 + src/svg/CMakeLists.txt | 37 + src/svg/HACKING | 7 + src/svg/README | 8 + src/svg/css-ostringstream.cpp | 90 + src/svg/css-ostringstream.h | 84 + src/svg/path-string.cpp | 171 + src/svg/path-string.h | 266 + src/svg/sp-svg.def | 27 + src/svg/stringstream.cpp | 112 + src/svg/stringstream.h | 107 + src/svg/strip-trailing-zeros.cpp | 54 + src/svg/strip-trailing-zeros.h | 29 + src/svg/svg-affine-test.h | 274 + src/svg/svg-affine.cpp | 301 + src/svg/svg-angle.cpp | 134 + src/svg/svg-angle.h | 70 + src/svg/svg-color-test.h | 134 + src/svg/svg-color.cpp | 659 + src/svg/svg-color.h | 24 + src/svg/svg-icc-color.h | 38 + src/svg/svg-length-test.h | 206 + src/svg/svg-length.cpp | 607 + src/svg/svg-length.h | 81 + src/svg/svg-path-geom-test.h | 523 + src/svg/svg-path.cpp | 137 + src/svg/svg.h | 81 + src/svg/test-stubs.cpp | 43 + src/svg/test-stubs.h | 31 + src/syseq.h | 317 + src/text-chemistry-impl.h | 151 + src/text-chemistry.cpp | 649 + src/text-chemistry.h | 45 + src/text-editing.cpp | 2181 ++ src/text-editing.h | 85 + src/text-tag-attributes.h | 192 + src/trace/CMakeLists.txt | 31 + src/trace/README | 18 + src/trace/autotrace/inkscape-autotrace.cpp | 223 + src/trace/autotrace/inkscape-autotrace.h | 101 + src/trace/depixelize/inkscape-depixelize.cpp | 137 + src/trace/depixelize/inkscape-depixelize.h | 100 + src/trace/filterset.cpp | 422 + src/trace/filterset.h | 57 + src/trace/imagemap-gdk.cpp | 223 + src/trace/imagemap-gdk.h | 51 + src/trace/imagemap.cpp | 459 + src/trace/imagemap.h | 393 + src/trace/pool.h | 119 + src/trace/potrace/bitmap.h | 117 + src/trace/potrace/inkscape-potrace.cpp | 661 + src/trace/potrace/inkscape-potrace.h | 141 + src/trace/quantize.cpp | 597 + src/trace/quantize.h | 23 + src/trace/siox.cpp | 1733 ++ src/trace/siox.h | 654 + src/trace/trace.cpp | 613 + src/trace/trace.h | 258 + src/transf_mat_3x4.cpp | 190 + src/transf_mat_3x4.h | 82 + src/ui/CMakeLists.txt | 477 + src/ui/README | 21 + src/ui/cache/README | 3 + src/ui/cache/svg_preview_cache.cpp | 142 + src/ui/cache/svg_preview_cache.h | 65 + src/ui/clipboard.cpp | 1682 ++ src/ui/clipboard.h | 75 + src/ui/contextmenu.cpp | 1008 + src/ui/contextmenu.h | 225 + src/ui/control-manager.cpp | 509 + src/ui/control-manager.h | 96 + src/ui/control-types.h | 58 + src/ui/desktop/README | 27 + src/ui/desktop/menubar.cpp | 635 + src/ui/desktop/menubar.h | 48 + src/ui/dialog-events.cpp | 244 + src/ui/dialog-events.h | 76 + src/ui/dialog/aboutbox.cpp | 224 + src/ui/dialog/aboutbox.h | 66 + src/ui/dialog/align-and-distribute.cpp | 1339 + src/ui/dialog/align-and-distribute.h | 223 + src/ui/dialog/arrange-tab.h | 55 + src/ui/dialog/attrdialog.cpp | 682 + src/ui/dialog/attrdialog.h | 128 + src/ui/dialog/behavior.h | 104 + src/ui/dialog/calligraphic-profile-rename.cpp | 142 + src/ui/dialog/calligraphic-profile-rename.h | 87 + src/ui/dialog/clonetiler.cpp | 2834 ++ src/ui/dialog/clonetiler.h | 226 + src/ui/dialog/color-item.cpp | 751 + src/ui/dialog/color-item.h | 130 + src/ui/dialog/debug.cpp | 259 + src/ui/dialog/debug.h | 101 + src/ui/dialog/desktop-tracker.cpp | 154 + src/ui/dialog/desktop-tracker.h | 69 + src/ui/dialog/dialog-manager.cpp | 310 + src/ui/dialog/dialog-manager.h | 74 + src/ui/dialog/dialog.cpp | 370 + src/ui/dialog/dialog.h | 179 + src/ui/dialog/dock-behavior.cpp | 297 + src/ui/dialog/dock-behavior.h | 104 + src/ui/dialog/document-metadata.cpp | 223 + src/ui/dialog/document-metadata.h | 86 + src/ui/dialog/document-properties.cpp | 1708 ++ src/ui/dialog/document-properties.h | 262 + src/ui/dialog/export.cpp | 1948 ++ src/ui/dialog/export.h | 367 + src/ui/dialog/extension-editor.cpp | 221 + src/ui/dialog/extension-editor.h | 93 + src/ui/dialog/extensions.cpp | 120 + src/ui/dialog/extensions.h | 57 + src/ui/dialog/filedialog.cpp | 201 + src/ui/dialog/filedialog.h | 254 + src/ui/dialog/filedialogimpl-gtkmm.cpp | 867 + src/ui/dialog/filedialogimpl-gtkmm.h | 313 + src/ui/dialog/filedialogimpl-win32.cpp | 1937 ++ src/ui/dialog/filedialogimpl-win32.h | 393 + src/ui/dialog/fill-and-stroke.cpp | 207 + src/ui/dialog/fill-and-stroke.h | 100 + src/ui/dialog/filter-editor.cpp | 120 + src/ui/dialog/filter-editor.h | 53 + src/ui/dialog/filter-effects-dialog.cpp | 3114 +++ src/ui/dialog/filter-effects-dialog.h | 346 + src/ui/dialog/find.cpp | 1076 + src/ui/dialog/find.h | 320 + src/ui/dialog/floating-behavior.cpp | 168 + src/ui/dialog/floating-behavior.h | 92 + src/ui/dialog/font-substitution.cpp | 271 + src/ui/dialog/font-substitution.h | 58 + src/ui/dialog/glyphs.cpp | 822 + src/ui/dialog/glyphs.h | 97 + src/ui/dialog/grid-arrange-tab.cpp | 776 + src/ui/dialog/grid-arrange-tab.h | 148 + src/ui/dialog/guides.cpp | 358 + src/ui/dialog/guides.h | 103 + src/ui/dialog/icon-preview.cpp | 682 + src/ui/dialog/icon-preview.h | 119 + src/ui/dialog/inkscape-preferences.cpp | 2772 ++ src/ui/dialog/inkscape-preferences.h | 620 + src/ui/dialog/input.cpp | 1792 ++ src/ui/dialog/input.h | 47 + src/ui/dialog/knot-properties.cpp | 207 + src/ui/dialog/knot-properties.h | 97 + src/ui/dialog/layer-properties.cpp | 424 + src/ui/dialog/layer-properties.h | 177 + src/ui/dialog/layers.cpp | 1004 + src/ui/dialog/layers.h | 152 + src/ui/dialog/livepatheffect-add.cpp | 933 + src/ui/dialog/livepatheffect-add.h | 147 + src/ui/dialog/livepatheffect-editor.cpp | 634 + src/ui/dialog/livepatheffect-editor.h | 152 + src/ui/dialog/lpe-fillet-chamfer-properties.cpp | 276 + src/ui/dialog/lpe-fillet-chamfer-properties.h | 114 + src/ui/dialog/lpe-powerstroke-properties.cpp | 199 + src/ui/dialog/lpe-powerstroke-properties.h | 92 + src/ui/dialog/memory.cpp | 247 + src/ui/dialog/memory.h | 54 + src/ui/dialog/messages.cpp | 216 + src/ui/dialog/messages.h | 103 + src/ui/dialog/new-from-template.cpp | 72 + src/ui/dialog/new-from-template.h | 45 + src/ui/dialog/object-attributes.cpp | 218 + src/ui/dialog/object-attributes.h | 123 + src/ui/dialog/object-properties.cpp | 605 + src/ui/dialog/object-properties.h | 148 + src/ui/dialog/objects.cpp | 2363 ++ src/ui/dialog/objects.h | 279 + src/ui/dialog/paint-servers.cpp | 505 + src/ui/dialog/paint-servers.h | 88 + src/ui/dialog/panel-dialog.h | 208 + src/ui/dialog/polar-arrange-tab.cpp | 408 + src/ui/dialog/polar-arrange-tab.h | 104 + src/ui/dialog/print-colors-preview-dialog.cpp | 103 + src/ui/dialog/print-colors-preview-dialog.h | 48 + src/ui/dialog/print.cpp | 263 + src/ui/dialog/print.h | 80 + src/ui/dialog/save-template-dialog.cpp | 101 + src/ui/dialog/save-template-dialog.h | 60 + src/ui/dialog/selectorsdialog.cpp | 1508 ++ src/ui/dialog/selectorsdialog.h | 209 + src/ui/dialog/spellcheck.cpp | 815 + src/ui/dialog/spellcheck.h | 301 + src/ui/dialog/styledialog.cpp | 1690 ++ src/ui/dialog/styledialog.h | 208 + src/ui/dialog/svg-fonts-dialog.cpp | 1067 + src/ui/dialog/svg-fonts-dialog.h | 283 + src/ui/dialog/svg-preview.cpp | 476 + src/ui/dialog/svg-preview.h | 123 + src/ui/dialog/swatches.cpp | 1418 + src/ui/dialog/swatches.h | 110 + src/ui/dialog/symbols.cpp | 1403 + src/ui/dialog/symbols.h | 183 + src/ui/dialog/tags.cpp | 1125 + src/ui/dialog/tags.h | 174 + src/ui/dialog/template-load-tab.cpp | 337 + src/ui/dialog/template-load-tab.h | 119 + src/ui/dialog/template-widget.cpp | 152 + src/ui/dialog/template-widget.h | 51 + src/ui/dialog/text-edit.cpp | 585 + src/ui/dialog/text-edit.h | 217 + src/ui/dialog/tile.cpp | 81 + src/ui/dialog/tile.h | 80 + src/ui/dialog/tracedialog.cpp | 380 + src/ui/dialog/tracedialog.h | 69 + src/ui/dialog/transformation.cpp | 1171 + src/ui/dialog/transformation.h | 258 + src/ui/dialog/undo-history.cpp | 406 + src/ui/dialog/undo-history.h | 179 + src/ui/dialog/xml-tree.cpp | 968 + src/ui/dialog/xml-tree.h | 265 + src/ui/drag-and-drop.cpp | 534 + src/ui/drag-and-drop.h | 51 + src/ui/draw-anchor.cpp | 108 + src/ui/draw-anchor.h | 61 + src/ui/event-debug.h | 122 + src/ui/icon-loader.cpp | 137 + src/ui/icon-loader.h | 29 + src/ui/icon-names.h | 32 + src/ui/interface.cpp | 268 + src/ui/interface.h | 76 + src/ui/monitor.cpp | 68 + src/ui/monitor.h | 38 + src/ui/pixmaps/README | 2 + src/ui/pixmaps/cursor-3dbox.xpm | 38 + src/ui/pixmaps/cursor-adj-a.xpm | 38 + src/ui/pixmaps/cursor-adj-h.xpm | 38 + src/ui/pixmaps/cursor-adj-l.xpm | 38 + src/ui/pixmaps/cursor-adj-s.xpm | 38 + src/ui/pixmaps/cursor-calligraphy.xpm | 38 + src/ui/pixmaps/cursor-connector.xpm | 38 + src/ui/pixmaps/cursor-crosshairs.xpm | 38 + src/ui/pixmaps/cursor-dropper-f.xpm | 39 + src/ui/pixmaps/cursor-dropper-s.xpm | 39 + src/ui/pixmaps/cursor-dropping-f.xpm | 39 + src/ui/pixmaps/cursor-dropping-s.xpm | 39 + src/ui/pixmaps/cursor-ellipse.xpm | 40 + src/ui/pixmaps/cursor-eraser.xpm | 38 + src/ui/pixmaps/cursor-gradient-add.xpm | 38 + src/ui/pixmaps/cursor-gradient.xpm | 38 + src/ui/pixmaps/cursor-measure.xpm | 38 + src/ui/pixmaps/cursor-node-d.xpm | 38 + src/ui/pixmaps/cursor-node.xpm | 38 + src/ui/pixmaps/cursor-paintbucket.xpm | 38 + src/ui/pixmaps/cursor-pen.xpm | 38 + src/ui/pixmaps/cursor-pencil.xpm | 38 + src/ui/pixmaps/cursor-rect.xpm | 40 + src/ui/pixmaps/cursor-select-d.xpm | 38 + src/ui/pixmaps/cursor-select-m.xpm | 38 + src/ui/pixmaps/cursor-select.xpm | 38 + src/ui/pixmaps/cursor-spiral.xpm | 38 + src/ui/pixmaps/cursor-spray-move.xpm | 38 + src/ui/pixmaps/cursor-spray.xpm | 38 + src/ui/pixmaps/cursor-star.xpm | 40 + src/ui/pixmaps/cursor-text-insert.xpm | 38 + src/ui/pixmaps/cursor-text.xpm | 38 + src/ui/pixmaps/cursor-tweak-attract.xpm | 38 + src/ui/pixmaps/cursor-tweak-color.xpm | 38 + src/ui/pixmaps/cursor-tweak-less.xpm | 38 + src/ui/pixmaps/cursor-tweak-more.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-in.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-jitter.xpm | 38 + src/ui/pixmaps/cursor-tweak-move-out.xpm | 38 + src/ui/pixmaps/cursor-tweak-move.xpm | 38 + src/ui/pixmaps/cursor-tweak-push.xpm | 38 + src/ui/pixmaps/cursor-tweak-repel.xpm | 38 + src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm | 38 + .../cursor-tweak-rotate-counterclockwise.xpm | 38 + src/ui/pixmaps/cursor-tweak-roughen.xpm | 38 + src/ui/pixmaps/cursor-tweak-scale-down.xpm | 38 + src/ui/pixmaps/cursor-tweak-scale-up.xpm | 38 + src/ui/pixmaps/cursor-tweak-thicken.xpm | 38 + src/ui/pixmaps/cursor-tweak-thin.xpm | 38 + src/ui/pixmaps/cursor-zoom-out.xpm | 38 + src/ui/pixmaps/cursor-zoom.xpm | 38 + src/ui/pixmaps/handles.xpm | 159 + src/ui/pref-pusher.cpp | 70 + src/ui/pref-pusher.h | 80 + src/ui/previewable.h | 64 + src/ui/previewholder.cpp | 447 + src/ui/previewholder.h | 90 + src/ui/selected-color.cpp | 159 + src/ui/selected-color.h | 99 + src/ui/shape-editor-knotholders.cpp | 1980 ++ src/ui/shape-editor.cpp | 218 + src/ui/shape-editor.h | 71 + src/ui/simple-pref-pusher.cpp | 49 + src/ui/simple-pref-pusher.h | 65 + src/ui/tool-factory.cpp | 101 + src/ui/tool-factory.h | 43 + src/ui/tool/commit-events.h | 52 + src/ui/tool/control-point-selection.cpp | 773 + src/ui/tool/control-point-selection.h | 178 + src/ui/tool/control-point.cpp | 637 + src/ui/tool/control-point.h | 411 + src/ui/tool/curve-drag-point.cpp | 231 + src/ui/tool/curve-drag-point.h | 77 + src/ui/tool/event-utils.cpp | 162 + src/ui/tool/event-utils.h | 133 + src/ui/tool/manipulator.cpp | 90 + src/ui/tool/manipulator.h | 174 + src/ui/tool/modifier-tracker.cpp | 94 + src/ui/tool/modifier-tracker.h | 55 + src/ui/tool/multi-path-manipulator.cpp | 888 + src/ui/tool/multi-path-manipulator.h | 154 + src/ui/tool/node-types.h | 49 + src/ui/tool/node.cpp | 1924 ++ src/ui/tool/node.h | 527 + src/ui/tool/path-manipulator.cpp | 1756 ++ src/ui/tool/path-manipulator.h | 180 + src/ui/tool/selectable-control-point.cpp | 147 + src/ui/tool/selectable-control-point.h | 78 + src/ui/tool/selector.cpp | 150 + src/ui/tool/selector.h | 60 + src/ui/tool/shape-record.h | 62 + src/ui/tool/transform-handle-set.cpp | 867 + src/ui/tool/transform-handle-set.h | 142 + src/ui/toolbar/arc-toolbar.cpp | 561 + src/ui/toolbar/arc-toolbar.h | 116 + src/ui/toolbar/box3d-toolbar.cpp | 418 + src/ui/toolbar/box3d-toolbar.h | 106 + src/ui/toolbar/calligraphy-toolbar.cpp | 599 + src/ui/toolbar/calligraphy-toolbar.h | 103 + src/ui/toolbar/connector-toolbar.cpp | 437 + src/ui/toolbar/connector-toolbar.h | 93 + src/ui/toolbar/dropper-toolbar.cpp | 116 + src/ui/toolbar/dropper-toolbar.h | 70 + src/ui/toolbar/eraser-toolbar.cpp | 335 + src/ui/toolbar/eraser-toolbar.h | 95 + src/ui/toolbar/gradient-toolbar.cpp | 1178 + src/ui/toolbar/gradient-toolbar.h | 102 + src/ui/toolbar/lpe-toolbar.cpp | 418 + src/ui/toolbar/lpe-toolbar.h | 101 + src/ui/toolbar/measure-toolbar.cpp | 452 + src/ui/toolbar/measure-toolbar.h | 91 + src/ui/toolbar/mesh-toolbar.cpp | 621 + src/ui/toolbar/mesh-toolbar.h | 97 + src/ui/toolbar/node-toolbar.cpp | 651 + src/ui/toolbar/node-toolbar.h | 115 + src/ui/toolbar/paintbucket-toolbar.cpp | 221 + src/ui/toolbar/paintbucket-toolbar.h | 72 + src/ui/toolbar/pencil-toolbar.cpp | 622 + src/ui/toolbar/pencil-toolbar.h | 99 + src/ui/toolbar/rect-toolbar.cpp | 407 + src/ui/toolbar/rect-toolbar.h | 113 + src/ui/toolbar/select-toolbar.cpp | 508 + src/ui/toolbar/select-toolbar.h | 82 + src/ui/toolbar/snap-toolbar.cpp | 402 + src/ui/toolbar/snap-toolbar.h | 70 + src/ui/toolbar/spiral-toolbar.cpp | 304 + src/ui/toolbar/spiral-toolbar.h | 98 + src/ui/toolbar/spray-toolbar.cpp | 550 + src/ui/toolbar/spray-toolbar.h | 107 + src/ui/toolbar/star-toolbar.cpp | 564 + src/ui/toolbar/star-toolbar.h | 108 + src/ui/toolbar/text-toolbar.cpp | 2540 ++ src/ui/toolbar/text-toolbar.h | 149 + src/ui/toolbar/toolbar.cpp | 102 + src/ui/toolbar/toolbar.h | 67 + src/ui/toolbar/tweak-toolbar.cpp | 347 + src/ui/toolbar/tweak-toolbar.h | 89 + src/ui/toolbar/zoom-toolbar.cpp | 85 + src/ui/toolbar/zoom-toolbar.h | 62 + src/ui/tools-switch.cpp | 195 + src/ui/tools-switch.h | 65 + src/ui/tools/arc-tool.cpp | 488 + src/ui/tools/arc-tool.h | 83 + src/ui/tools/box3d-tool.cpp | 614 + src/ui/tools/box3d-tool.h | 109 + src/ui/tools/calligraphic-tool.cpp | 1203 + src/ui/tools/calligraphic-tool.h | 103 + src/ui/tools/connector-tool.cpp | 1393 + src/ui/tools/connector-tool.h | 168 + src/ui/tools/dropper-tool.cpp | 414 + src/ui/tools/dropper-tool.h | 87 + src/ui/tools/dynamic-base.cpp | 166 + src/ui/tools/dynamic-base.h | 130 + src/ui/tools/eraser-tool.cpp | 1119 + src/ui/tools/eraser-tool.h | 84 + src/ui/tools/flood-tool.cpp | 1254 + src/ui/tools/flood-tool.h | 72 + src/ui/tools/freehand-base.cpp | 1112 + src/ui/tools/freehand-base.h | 164 + src/ui/tools/gradient-tool.cpp | 932 + src/ui/tools/gradient-tool.h | 77 + src/ui/tools/lpe-tool.cpp | 494 + src/ui/tools/lpe-tool.h | 100 + src/ui/tools/measure-tool.cpp | 1466 ++ src/ui/tools/measure-tool.h | 110 + src/ui/tools/mesh-tool.cpp | 1098 + src/ui/tools/mesh-tool.h | 87 + src/ui/tools/node-tool.cpp | 848 + src/ui/tools/node-tool.h | 118 + src/ui/tools/pen-tool.cpp | 2104 ++ src/ui/tools/pen-tool.h | 166 + src/ui/tools/pencil-tool.cpp | 1239 + src/ui/tools/pencil-tool.h | 104 + src/ui/tools/rect-tool.cpp | 503 + src/ui/tools/rect-tool.h | 64 + src/ui/tools/select-tool.cpp | 1171 + src/ui/tools/select-tool.h | 72 + src/ui/tools/spiral-tool.cpp | 444 + src/ui/tools/spiral-tool.h | 65 + src/ui/tools/spray-tool.cpp | 1533 ++ src/ui/tools/spray-tool.h | 151 + src/ui/tools/star-tool.cpp | 461 + src/ui/tools/star-tool.h | 75 + src/ui/tools/text-tool.cpp | 1897 ++ src/ui/tools/text-tool.h | 108 + src/ui/tools/tool-base.cpp | 1630 ++ src/ui/tools/tool-base.h | 284 + src/ui/tools/tweak-tool.cpp | 1535 ++ src/ui/tools/tweak-tool.h | 107 + src/ui/tools/zoom-tool.cpp | 243 + src/ui/tools/zoom-tool.h | 48 + src/ui/util.cpp | 38 + src/ui/util.h | 31 + src/ui/uxmanager.cpp | 246 + src/ui/uxmanager.h | 61 + src/ui/view/README | 51 + src/ui/view/edit-widget-interface.h | 178 + src/ui/view/svg-view-widget.cpp | 266 + src/ui/view/svg-view-widget.h | 89 + src/ui/view/view-widget.cpp | 102 + src/ui/view/view-widget.h | 107 + src/ui/view/view.cpp | 138 + src/ui/view/view.h | 148 + src/ui/widget/alignment-selector.cpp | 78 + src/ui/widget/alignment-selector.h | 53 + src/ui/widget/anchor-selector.cpp | 97 + src/ui/widget/anchor-selector.h | 62 + src/ui/widget/attr-widget.h | 185 + src/ui/widget/button.cpp | 273 + src/ui/widget/button.h | 89 + src/ui/widget/clipmaskicon.cpp | 123 + src/ui/widget/clipmaskicon.h | 86 + src/ui/widget/color-entry.cpp | 157 + src/ui/widget/color-entry.h | 58 + src/ui/widget/color-icc-selector.cpp | 1074 + src/ui/widget/color-icc-selector.h | 75 + src/ui/widget/color-notebook.cpp | 341 + src/ui/widget/color-notebook.h | 89 + src/ui/widget/color-picker.cpp | 144 + src/ui/widget/color-picker.h | 114 + src/ui/widget/color-preview.cpp | 171 + src/ui/widget/color-preview.h | 57 + src/ui/widget/color-scales.cpp | 736 + src/ui/widget/color-scales.h | 110 + src/ui/widget/color-slider.cpp | 536 + src/ui/widget/color-slider.h | 93 + src/ui/widget/color-wheel-selector.cpp | 237 + src/ui/widget/color-wheel-selector.h | 83 + src/ui/widget/combo-box-entry-tool-item.cpp | 691 + src/ui/widget/combo-box-entry-tool-item.h | 157 + src/ui/widget/combo-enums.h | 224 + src/ui/widget/combo-tool-item.cpp | 290 + src/ui/widget/combo-tool-item.h | 136 + src/ui/widget/dash-selector.cpp | 309 + src/ui/widget/dash-selector.h | 112 + src/ui/widget/dock-item.cpp | 528 + src/ui/widget/dock-item.h | 159 + src/ui/widget/dock.cpp | 305 + src/ui/widget/dock.h | 108 + src/ui/widget/entity-entry.cpp | 207 + src/ui/widget/entity-entry.h | 85 + src/ui/widget/entry.cpp | 30 + src/ui/widget/entry.h | 45 + src/ui/widget/filter-effect-chooser.cpp | 194 + src/ui/widget/filter-effect-chooser.h | 95 + src/ui/widget/font-button.cpp | 58 + src/ui/widget/font-button.h | 63 + src/ui/widget/font-selector-toolbar.cpp | 301 + src/ui/widget/font-selector-toolbar.h | 120 + src/ui/widget/font-selector.cpp | 450 + src/ui/widget/font-selector.h | 163 + src/ui/widget/font-variants.cpp | 1461 ++ src/ui/widget/font-variants.h | 222 + src/ui/widget/font-variations.cpp | 179 + src/ui/widget/font-variations.h | 126 + src/ui/widget/frame.cpp | 80 + src/ui/widget/frame.h | 75 + src/ui/widget/highlight-picker.cpp | 169 + src/ui/widget/highlight-picker.h | 73 + src/ui/widget/iconrenderer.cpp | 121 + src/ui/widget/iconrenderer.h | 84 + src/ui/widget/imagetoggler.cpp | 113 + src/ui/widget/imagetoggler.h | 89 + src/ui/widget/ink-color-wheel.cpp | 726 + src/ui/widget/ink-color-wheel.h | 86 + src/ui/widget/ink-flow-box.cpp | 143 + src/ui/widget/ink-flow-box.h | 68 + src/ui/widget/ink-ruler.cpp | 473 + src/ui/widget/ink-ruler.h | 80 + src/ui/widget/ink-spinscale.cpp | 285 + src/ui/widget/ink-spinscale.h | 96 + src/ui/widget/insertordericon.cpp | 118 + src/ui/widget/insertordericon.h | 85 + src/ui/widget/label-tool-item.cpp | 66 + src/ui/widget/label-tool-item.h | 51 + src/ui/widget/labelled.cpp | 110 + src/ui/widget/labelled.h | 89 + src/ui/widget/layer-selector.cpp | 616 + src/ui/widget/layer-selector.h | 114 + src/ui/widget/layertypeicon.cpp | 113 + src/ui/widget/layertypeicon.h | 91 + src/ui/widget/licensor.cpp | 156 + src/ui/widget/licensor.h | 57 + src/ui/widget/notebook-page.cpp | 47 + src/ui/widget/notebook-page.h | 59 + src/ui/widget/object-composite-settings.cpp | 325 + src/ui/widget/object-composite-settings.h | 81 + src/ui/widget/page-sizer.cpp | 781 + src/ui/widget/page-sizer.h | 307 + src/ui/widget/pages-skeleton.h | 154 + src/ui/widget/panel.cpp | 176 + src/ui/widget/panel.h | 139 + src/ui/widget/point.cpp | 180 + src/ui/widget/point.h | 196 + src/ui/widget/preferences-widget.cpp | 1023 + src/ui/widget/preferences-widget.h | 315 + src/ui/widget/preview.cpp | 502 + src/ui/widget/preview.h | 163 + src/ui/widget/random.cpp | 101 + src/ui/widget/random.h | 125 + src/ui/widget/registered-enums.h | 99 + src/ui/widget/registered-widget.cpp | 845 + src/ui/widget/registered-widget.h | 458 + src/ui/widget/registry.cpp | 53 + src/ui/widget/registry.h | 48 + src/ui/widget/rendering-options.cpp | 122 + src/ui/widget/rendering-options.h | 68 + src/ui/widget/rotateable.cpp | 180 + src/ui/widget/rotateable.h | 71 + src/ui/widget/scalar-unit.cpp | 263 + src/ui/widget/scalar-unit.h | 191 + src/ui/widget/scalar.cpp | 173 + src/ui/widget/scalar.h | 188 + src/ui/widget/selected-style.cpp | 1488 ++ src/ui/widget/selected-style.h | 296 + src/ui/widget/spin-button-tool-item.cpp | 532 + src/ui/widget/spin-button-tool-item.h | 95 + src/ui/widget/spin-scale.cpp | 226 + src/ui/widget/spin-scale.h | 110 + src/ui/widget/spin-slider.cpp | 223 + src/ui/widget/spin-slider.h | 106 + src/ui/widget/spinbutton.cpp | 137 + src/ui/widget/spinbutton.h | 117 + src/ui/widget/style-subject.cpp | 189 + src/ui/widget/style-subject.h | 133 + src/ui/widget/style-swatch.cpp | 373 + src/ui/widget/style-swatch.h | 105 + src/ui/widget/text.cpp | 60 + src/ui/widget/text.h | 81 + src/ui/widget/tolerance-slider.cpp | 215 + src/ui/widget/tolerance-slider.h | 89 + src/ui/widget/unit-menu.cpp | 152 + src/ui/widget/unit-menu.h | 146 + src/ui/widget/unit-tracker.cpp | 294 + src/ui/widget/unit-tracker.h | 87 + src/unclump.cpp | 398 + src/unclump.h | 32 + src/undo-stack-observer.h | 74 + src/unicoderange.cpp | 135 + src/unicoderange.h | 33 + src/util/CMakeLists.txt | 40 + src/util/README | 9 + src/util/const_char_ptr.h | 51 + src/util/copy.h | 49 + src/util/ege-appear-time-tracker.cpp | 163 + src/util/ege-appear-time-tracker.h | 90 + src/util/ege-tags.cpp | 171 + src/util/ege-tags.h | 122 + src/util/enums.h | 134 + src/util/expression-evaluator.cpp | 396 + src/util/expression-evaluator.h | 196 + src/util/find-if-before.h | 52 + src/util/find-last-if.h | 52 + src/util/fixed_point.h | 112 + src/util/format.h | 57 + src/util/forward-pointer-iterator.h | 122 + src/util/list-container-test.h | 258 + src/util/list-container.h | 353 + src/util/list-copy.h | 46 + src/util/list.h | 413 + src/util/longest-common-suffix.h | 115 + src/util/reference.h | 50 + src/util/reverse-list.h | 69 + src/util/share.cpp | 45 + src/util/share.h | 112 + src/util/signal-blocker.h | 71 + src/util/ucompose.hpp | 452 + src/util/units.cpp | 578 + src/util/units.h | 231 + src/util/ziptool.cpp | 3039 +++ src/util/ziptool.h | 575 + src/vanishing-point.cpp | 773 + src/vanishing-point.h | 229 + src/verbs.cpp | 3490 +++ src/verbs.h | 665 + src/version.cpp | 85 + src/version.h | 72 + src/widgets/CMakeLists.txt | 45 + src/widgets/README | 10 + src/widgets/desktop-widget.cpp | 2570 ++ src/widgets/desktop-widget.h | 354 + src/widgets/ege-paint-def.cpp | 316 + src/widgets/ege-paint-def.h | 112 + src/widgets/fill-n-stroke-factory.h | 38 + src/widgets/fill-style.cpp | 832 + src/widgets/fill-style.h | 39 + src/widgets/gradient-image.cpp | 285 + src/widgets/gradient-image.h | 65 + src/widgets/gradient-selector.cpp | 660 + src/widgets/gradient-selector.h | 152 + src/widgets/gradient-vector.cpp | 1314 + src/widgets/gradient-vector.h | 90 + src/widgets/ink-action.cpp | 200 + src/widgets/ink-action.h | 61 + src/widgets/mappings.xml | 334 + src/widgets/paint-selector.cpp | 1601 ++ src/widgets/paint-selector.h | 171 + src/widgets/sp-attribute-widget.cpp | 303 + src/widgets/sp-attribute-widget.h | 172 + src/widgets/sp-color-selector.cpp | 334 + src/widgets/sp-color-selector.h | 105 + src/widgets/sp-xmlview-tree.cpp | 866 + src/widgets/sp-xmlview-tree.h | 64 + src/widgets/spinbutton-events.cpp | 163 + src/widgets/spinbutton-events.h | 32 + src/widgets/spw-utilities.cpp | 160 + src/widgets/spw-utilities.h | 48 + src/widgets/stroke-marker-selector.cpp | 567 + src/widgets/stroke-marker-selector.h | 118 + src/widgets/stroke-style.cpp | 1346 + src/widgets/stroke-style.h | 227 + src/widgets/style-utils.h | 35 + src/widgets/swatch-selector.cpp | 168 + src/widgets/swatch-selector.h | 68 + src/widgets/toolbox.cpp | 842 + src/widgets/toolbox.h | 84 + src/widgets/widget-sizes.h | 57 + src/xml/CMakeLists.txt | 55 + src/xml/README | 12 + src/xml/attribute-record.h | 59 + src/xml/comment-node.h | 57 + src/xml/composite-node-observer.cpp | 336 + src/xml/composite-node-observer.h | 109 + src/xml/croco-node-iface.cpp | 86 + src/xml/croco-node-iface.h | 21 + src/xml/document.h | 119 + src/xml/element-node.h | 52 + src/xml/event-fns.h | 37 + src/xml/event.cpp | 526 + src/xml/event.h | 260 + src/xml/helper-observer.cpp | 77 + src/xml/helper-observer.h | 60 + src/xml/invalid-operation-exception.h | 45 + src/xml/log-builder.cpp | 82 + src/xml/log-builder.h | 87 + src/xml/node-event-vector.h | 68 + src/xml/node-fns.cpp | 101 + src/xml/node-fns.h | 84 + src/xml/node-iterators.h | 63 + src/xml/node-observer.h | 179 + src/xml/node.h | 543 + src/xml/pi-node.h | 53 + src/xml/quote-test.h | 87 + src/xml/quote.cpp | 88 + src/xml/quote.h | 19 + src/xml/rebase-hrefs.cpp | 226 + src/xml/rebase-hrefs.h | 61 + src/xml/repr-action-test.h | 111 + src/xml/repr-css.cpp | 519 + src/xml/repr-io.cpp | 1065 + src/xml/repr-sorting.cpp | 74 + src/xml/repr-sorting.h | 51 + src/xml/repr-util.cpp | 661 + src/xml/repr.cpp | 59 + src/xml/repr.h | 246 + src/xml/simple-document.cpp | 137 + src/xml/simple-document.h | 98 + src/xml/simple-node.cpp | 825 + src/xml/simple-node.h | 172 + src/xml/sp-css-attr.h | 31 + src/xml/subtree.cpp | 63 + src/xml/subtree.h | 74 + src/xml/text-node.h | 64 + 2376 files changed, 852812 insertions(+) create mode 100644 src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README create mode 100644 src/2geom/2geom.h create mode 100644 src/2geom/CMakeLists.txt create mode 100644 src/2geom/affine.cpp create mode 100644 src/2geom/affine.h create mode 100644 src/2geom/angle.h create mode 100644 src/2geom/basic-intersection.cpp create mode 100644 src/2geom/basic-intersection.h create mode 100644 src/2geom/bezier-clipping.cpp create mode 100644 src/2geom/bezier-curve.cpp create mode 100644 src/2geom/bezier-curve.h create mode 100644 src/2geom/bezier-to-sbasis.h create mode 100644 src/2geom/bezier-utils.cpp create mode 100644 src/2geom/bezier-utils.h create mode 100644 src/2geom/bezier.cpp create mode 100644 src/2geom/bezier.h create mode 100644 src/2geom/cairo-path-sink.cpp create mode 100644 src/2geom/cairo-path-sink.h create mode 100644 src/2geom/choose.h create mode 100644 src/2geom/circle.cpp create mode 100644 src/2geom/circle.h create mode 100644 src/2geom/concepts.h create mode 100644 src/2geom/conic_section_clipper.h create mode 100644 src/2geom/conic_section_clipper_cr.h create mode 100644 src/2geom/conic_section_clipper_impl.cpp create mode 100644 src/2geom/conic_section_clipper_impl.h create mode 100644 src/2geom/conicsec.cpp create mode 100644 src/2geom/conicsec.h create mode 100644 src/2geom/convex-hull.cpp create mode 100644 src/2geom/convex-hull.h create mode 100644 src/2geom/coord.cpp create mode 100644 src/2geom/coord.h create mode 100644 src/2geom/crossing.cpp create mode 100644 src/2geom/crossing.h create mode 100644 src/2geom/curve.cpp create mode 100644 src/2geom/curve.h create mode 100644 src/2geom/curves.h create mode 100644 src/2geom/d2-sbasis.cpp create mode 100644 src/2geom/d2.h create mode 100644 src/2geom/ellipse.cpp create mode 100644 src/2geom/ellipse.h create mode 100644 src/2geom/elliptical-arc-from-sbasis.cpp create mode 100644 src/2geom/elliptical-arc.cpp create mode 100644 src/2geom/elliptical-arc.h create mode 100644 src/2geom/exception.h create mode 100644 src/2geom/forward.h create mode 100644 src/2geom/generic-interval.h create mode 100644 src/2geom/generic-rect.h create mode 100644 src/2geom/geom.cpp create mode 100644 src/2geom/geom.h create mode 100644 src/2geom/int-interval.h create mode 100644 src/2geom/int-point.h create mode 100644 src/2geom/int-rect.h create mode 100644 src/2geom/intersection-graph.cpp create mode 100644 src/2geom/intersection-graph.h create mode 100644 src/2geom/intersection.h create mode 100644 src/2geom/interval.h create mode 100644 src/2geom/line.cpp create mode 100644 src/2geom/line.h create mode 100644 src/2geom/linear.h create mode 100644 src/2geom/math-utils.h create mode 100644 src/2geom/nearest-time.cpp create mode 100644 src/2geom/nearest-time.h create mode 100644 src/2geom/numeric/fitting-model.h create mode 100644 src/2geom/numeric/fitting-tool.h create mode 100644 src/2geom/numeric/linear_system.h create mode 100644 src/2geom/numeric/matrix.cpp create mode 100644 src/2geom/numeric/matrix.h create mode 100644 src/2geom/numeric/symmetric-matrix-fs-operation.h create mode 100644 src/2geom/numeric/symmetric-matrix-fs-trace.h create mode 100644 src/2geom/numeric/symmetric-matrix-fs.h create mode 100644 src/2geom/numeric/vector.h create mode 100644 src/2geom/ord.h create mode 100644 src/2geom/path-intersection.cpp create mode 100644 src/2geom/path-intersection.h create mode 100644 src/2geom/path-sink.cpp create mode 100644 src/2geom/path-sink.h create mode 100644 src/2geom/path.cpp create mode 100644 src/2geom/path.h create mode 100644 src/2geom/pathvector.cpp create mode 100644 src/2geom/pathvector.h create mode 100644 src/2geom/piecewise.cpp create mode 100644 src/2geom/piecewise.h create mode 100644 src/2geom/point.cpp create mode 100644 src/2geom/point.h create mode 100644 src/2geom/polynomial.cpp create mode 100644 src/2geom/polynomial.h create mode 100644 src/2geom/ray.h create mode 100644 src/2geom/rect.cpp create mode 100644 src/2geom/rect.h create mode 100644 src/2geom/recursive-bezier-intersection.cpp create mode 100644 src/2geom/sbasis-2d.cpp create mode 100644 src/2geom/sbasis-2d.h create mode 100644 src/2geom/sbasis-curve.h create mode 100644 src/2geom/sbasis-geometric.cpp create mode 100644 src/2geom/sbasis-geometric.h create mode 100644 src/2geom/sbasis-math.cpp create mode 100644 src/2geom/sbasis-math.h create mode 100644 src/2geom/sbasis-poly.cpp create mode 100644 src/2geom/sbasis-poly.h create mode 100644 src/2geom/sbasis-roots.cpp create mode 100644 src/2geom/sbasis-to-bezier.cpp create mode 100644 src/2geom/sbasis-to-bezier.h create mode 100644 src/2geom/sbasis.cpp create mode 100644 src/2geom/sbasis.h create mode 100644 src/2geom/solve-bezier-one-d.cpp create mode 100644 src/2geom/solve-bezier-parametric.cpp create mode 100644 src/2geom/solve-bezier.cpp create mode 100644 src/2geom/solver.h create mode 100644 src/2geom/svg-path-parser.cpp create mode 100644 src/2geom/svg-path-parser.h create mode 100644 src/2geom/svg-path-writer.cpp create mode 100644 src/2geom/svg-path-writer.h create mode 100644 src/2geom/sweep-bounds.cpp create mode 100644 src/2geom/sweep-bounds.h create mode 100644 src/2geom/sweeper.h create mode 100644 src/2geom/transforms.cpp create mode 100644 src/2geom/transforms.h create mode 100644 src/2geom/utils.cpp create mode 100644 src/2geom/utils.h create mode 100644 src/3rdparty/CMakeLists.txt create mode 100644 src/3rdparty/adaptagrams/CMakeLists.txt create mode 100644 src/3rdparty/adaptagrams/libavoid/CMakeLists.txt create mode 100644 src/3rdparty/adaptagrams/libavoid/Doxyfile create mode 100644 src/3rdparty/adaptagrams/libavoid/LICENSE.LGPL create mode 100644 src/3rdparty/adaptagrams/libavoid/Makefile.am create mode 100644 src/3rdparty/adaptagrams/libavoid/README create mode 100644 src/3rdparty/adaptagrams/libavoid/actioninfo.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/actioninfo.h create mode 100644 src/3rdparty/adaptagrams/libavoid/assertions.h create mode 100644 src/3rdparty/adaptagrams/libavoid/connectionpin.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/connectionpin.h create mode 100644 src/3rdparty/adaptagrams/libavoid/connector.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/connector.h create mode 100644 src/3rdparty/adaptagrams/libavoid/connend.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/connend.h create mode 100644 src/3rdparty/adaptagrams/libavoid/debug.h create mode 100644 src/3rdparty/adaptagrams/libavoid/debughandler.h create mode 100644 src/3rdparty/adaptagrams/libavoid/dllexport.h create mode 100644 src/3rdparty/adaptagrams/libavoid/doc/description.doc create mode 100644 src/3rdparty/adaptagrams/libavoid/doc/example.doc create mode 100644 src/3rdparty/adaptagrams/libavoid/doc/header.html create mode 100644 src/3rdparty/adaptagrams/libavoid/geometry.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/geometry.h create mode 100644 src/3rdparty/adaptagrams/libavoid/geomtypes.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/geomtypes.h create mode 100644 src/3rdparty/adaptagrams/libavoid/graph.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/graph.h create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedge.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedge.h create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedgeimprover.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedgeimprover.h create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedgetree.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/hyperedgetree.h create mode 100644 src/3rdparty/adaptagrams/libavoid/junction.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/junction.h create mode 100644 src/3rdparty/adaptagrams/libavoid/libavoid.h create mode 100644 src/3rdparty/adaptagrams/libavoid/libavoid.pc.in create mode 100755 src/3rdparty/adaptagrams/libavoid/libavoid.sln create mode 100755 src/3rdparty/adaptagrams/libavoid/libavoid.vcxproj create mode 100644 src/3rdparty/adaptagrams/libavoid/makepath.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/makepath.h create mode 100644 src/3rdparty/adaptagrams/libavoid/mtst.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/mtst.h create mode 100644 src/3rdparty/adaptagrams/libavoid/obstacle.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/obstacle.h create mode 100644 src/3rdparty/adaptagrams/libavoid/orthogonal.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/orthogonal.h create mode 100644 src/3rdparty/adaptagrams/libavoid/router.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/router.h create mode 100644 src/3rdparty/adaptagrams/libavoid/scanline.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/scanline.h create mode 100644 src/3rdparty/adaptagrams/libavoid/shape.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/shape.h create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/2junctions.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/Makefile.am create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/buildOrthogonalChannelInfo1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpointNudging1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpointNudging2.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpointNudging3.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpoints01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpoints02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/checkpoints03.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/complex.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/connectionpin01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/connectionpin02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/connectionpin03.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/connendmove.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/corneroverlap01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/endlessLoop01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/example.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/finalSegmentNudging1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/finalSegmentNudging2.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/finalSegmentNudging3.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/forwardFlowingConnectors01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/freeFloatingDirection01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/hola01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/hyperedge01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/hyperedge02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/hyperedgeLoop1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/hyperedgeRerouting01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge03.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge04.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge05.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/improveHyperedge06.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/infinity.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inline.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineOverlap09.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineOverlap10.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineOverlap11.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineShapes.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap03.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap04.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap05.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap06.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap07.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/inlineoverlap08.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/junction01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/junction02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/junction03.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/junction04.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/latesetup.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash2.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash3.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash4.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash5.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash6.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash7.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/lineSegWrapperCrash8.cpp create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/2junctions.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/buildOrthogonalChannelInfo1.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/checkpointNudging1.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/checkpointNudging2.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/checkpoints01.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/connectionpin01.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/connectionpin02.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/connectionpin03.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/connendmove.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/corneroverlap01.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/example.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/finalSegmentNudging1.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/finalSegmentNudging2.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/finalSegmentNudging3.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/freeFloatingDirection01.vcxproj create mode 100755 src/3rdparty/adaptagrams/libavoid/tests/msctests/junction01.vcxproj create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/multiconnact.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/node1.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/nudgeCrossing01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/nudgeintobug.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/nudgeold.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/nudgingSkipsCheckpoint01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/nudgingSkipsCheckpoint02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/orderassertion.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/orthordering01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/orthordering02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/output/README.txt create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/overlappingRects.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/penaltyRerouting01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/performance01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/reallyslowrouting.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/removeJunctions01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/restrictedNudging.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/slowrouting.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/tjunct.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/treeRootCrash01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/treeRootCrash02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/unsatisfiableRangeAssertion.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/validPaths01.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/validPaths02.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/tests/vertlineassertion.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/timer.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/timer.h create mode 100644 src/3rdparty/adaptagrams/libavoid/vertices.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/vertices.h create mode 100644 src/3rdparty/adaptagrams/libavoid/viscluster.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/viscluster.h create mode 100644 src/3rdparty/adaptagrams/libavoid/visibility.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/visibility.h create mode 100644 src/3rdparty/adaptagrams/libavoid/vpsc.cpp create mode 100644 src/3rdparty/adaptagrams/libavoid/vpsc.h create mode 100644 src/3rdparty/adaptagrams/libcola/CMakeLists.txt create mode 100644 src/3rdparty/adaptagrams/libcola/Makefile.am create mode 100644 src/3rdparty/adaptagrams/libcola/box.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/box.h create mode 100644 src/3rdparty/adaptagrams/libcola/cc_clustercontainmentconstraints.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/cc_clustercontainmentconstraints.h create mode 100644 src/3rdparty/adaptagrams/libcola/cc_nonoverlapconstraints.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/cc_nonoverlapconstraints.h create mode 100644 src/3rdparty/adaptagrams/libcola/cluster.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/cluster.h create mode 100644 src/3rdparty/adaptagrams/libcola/cola.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/cola.h create mode 100644 src/3rdparty/adaptagrams/libcola/cola_log.h create mode 100644 src/3rdparty/adaptagrams/libcola/colafd.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/commondefs.h create mode 100644 src/3rdparty/adaptagrams/libcola/compound_constraints.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/compound_constraints.h create mode 100644 src/3rdparty/adaptagrams/libcola/conjugate_gradient.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/conjugate_gradient.h create mode 100644 src/3rdparty/adaptagrams/libcola/connected_components.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/connected_components.h create mode 100644 src/3rdparty/adaptagrams/libcola/convex_hull.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/convex_hull.h create mode 100644 src/3rdparty/adaptagrams/libcola/doc/description.doc create mode 100644 src/3rdparty/adaptagrams/libcola/exceptions.h create mode 100644 src/3rdparty/adaptagrams/libcola/gradient_projection.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/gradient_projection.h create mode 100644 src/3rdparty/adaptagrams/libcola/libcola.pc.in create mode 100644 src/3rdparty/adaptagrams/libcola/output_svg.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/output_svg.h create mode 100644 src/3rdparty/adaptagrams/libcola/pseudorandom.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/pseudorandom.h create mode 100644 src/3rdparty/adaptagrams/libcola/shapepair.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/shapepair.h create mode 100644 src/3rdparty/adaptagrams/libcola/shortest_paths.h create mode 100644 src/3rdparty/adaptagrams/libcola/sparse_matrix.h create mode 100644 src/3rdparty/adaptagrams/libcola/straightener.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/straightener.h create mode 100755 src/3rdparty/adaptagrams/libcola/tests/FixedRelativeConstraint01.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/Makefile.am create mode 100755 src/3rdparty/adaptagrams/libcola/tests/StillOverlap01.cpp create mode 100755 src/3rdparty/adaptagrams/libcola/tests/StillOverlap02.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/boundary.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/connected_components.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/constrained.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/containment.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/containment2.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/convex_hull.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/cycle_detector.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/data/1138_bus.txt create mode 100644 src/3rdparty/adaptagrams/libcola/tests/data/uetzNetworkGSC-all.gml create mode 100644 src/3rdparty/adaptagrams/libcola/tests/gml_graph.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/graphlayouttest.h create mode 100644 src/3rdparty/adaptagrams/libcola/tests/initialOverlap.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/invalid.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/large_graph.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/makefeasible.cpp create mode 100755 src/3rdparty/adaptagrams/libcola/tests/makefeasible02.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/makemovie.sh create mode 100644 src/3rdparty/adaptagrams/libcola/tests/max_acyclic_subgraph.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/overlappingClusters01.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/overlappingClusters02.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/overlappingClusters04.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/page_bounds.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/planar.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/random_graph.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/rectangularClusters01.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/rectclustershapecontainment.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/resize.cpp create mode 100755 src/3rdparty/adaptagrams/libcola/tests/runtest.sh create mode 100644 src/3rdparty/adaptagrams/libcola/tests/scale_free.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/shortest_paths.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/small_graph.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/sparse_matrix.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/test_cg.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/topology.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/trees.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/unconstrained.cpp create mode 100644 src/3rdparty/adaptagrams/libcola/tests/unsatisfiable.cpp create mode 100755 src/3rdparty/adaptagrams/libcola/tests/view_cd_output.sh create mode 100755 src/3rdparty/adaptagrams/libcola/tests/view_mas_output.sh create mode 100644 src/3rdparty/adaptagrams/libcola/unused.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/CMakeLists.txt create mode 100644 src/3rdparty/adaptagrams/libvpsc/COPYING create mode 100644 src/3rdparty/adaptagrams/libvpsc/Makefile.am create mode 100644 src/3rdparty/adaptagrams/libvpsc/assertions.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/block.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/block.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/blocks.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/blocks.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/cbuffer.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/cbuffer.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/constraint.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/constraint.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/doc/description.doc create mode 100644 src/3rdparty/adaptagrams/libvpsc/exceptions.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/libvpsc.pc.in create mode 100644 src/3rdparty/adaptagrams/libvpsc/linesegment.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/pairing_heap.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/rectangle.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/rectangle.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/solve_VPSC.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/solve_VPSC.h create mode 100644 src/3rdparty/adaptagrams/libvpsc/tests/Makefile.am create mode 100644 src/3rdparty/adaptagrams/libvpsc/tests/block.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/tests/cycle.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/tests/rectangleoverlap.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/tests/satisfy_inc.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/variable.cpp create mode 100644 src/3rdparty/adaptagrams/libvpsc/variable.h create mode 100644 src/3rdparty/autotrace/CMakeLists.txt create mode 100644 src/3rdparty/autotrace/atou.c create mode 100644 src/3rdparty/autotrace/atou.h create mode 100644 src/3rdparty/autotrace/autotrace.c create mode 100644 src/3rdparty/autotrace/autotrace.h create mode 100644 src/3rdparty/autotrace/bitmap.c create mode 100644 src/3rdparty/autotrace/bitmap.h create mode 100644 src/3rdparty/autotrace/cmdline.h create mode 100644 src/3rdparty/autotrace/color.c create mode 100644 src/3rdparty/autotrace/color.h create mode 100644 src/3rdparty/autotrace/config.h create mode 100644 src/3rdparty/autotrace/curve.c create mode 100644 src/3rdparty/autotrace/curve.h create mode 100644 src/3rdparty/autotrace/datetime.c create mode 100644 src/3rdparty/autotrace/datetime.h create mode 100644 src/3rdparty/autotrace/despeckle.c create mode 100644 src/3rdparty/autotrace/despeckle.h create mode 100644 src/3rdparty/autotrace/epsilon-equal.c create mode 100644 src/3rdparty/autotrace/epsilon-equal.h create mode 100644 src/3rdparty/autotrace/exception.c create mode 100644 src/3rdparty/autotrace/exception.h create mode 100644 src/3rdparty/autotrace/filename.c create mode 100644 src/3rdparty/autotrace/filename.h create mode 100644 src/3rdparty/autotrace/fit.c create mode 100644 src/3rdparty/autotrace/fit.h create mode 100644 src/3rdparty/autotrace/image-header.h create mode 100644 src/3rdparty/autotrace/image-proc.c create mode 100644 src/3rdparty/autotrace/image-proc.h create mode 100644 src/3rdparty/autotrace/input.c create mode 100644 src/3rdparty/autotrace/input.h create mode 100644 src/3rdparty/autotrace/intl.h create mode 100644 src/3rdparty/autotrace/logreport.c create mode 100644 src/3rdparty/autotrace/logreport.h create mode 100644 src/3rdparty/autotrace/median.c create mode 100644 src/3rdparty/autotrace/module.c create mode 100644 src/3rdparty/autotrace/output.c create mode 100644 src/3rdparty/autotrace/output.h create mode 100644 src/3rdparty/autotrace/private.h create mode 100644 src/3rdparty/autotrace/pxl-outline.c create mode 100644 src/3rdparty/autotrace/pxl-outline.h create mode 100644 src/3rdparty/autotrace/quantize.h create mode 100644 src/3rdparty/autotrace/spline.c create mode 100644 src/3rdparty/autotrace/spline.h create mode 100644 src/3rdparty/autotrace/thin-image.c create mode 100644 src/3rdparty/autotrace/thin-image.h create mode 100644 src/3rdparty/autotrace/types.h create mode 100644 src/3rdparty/autotrace/vector.c create mode 100644 src/3rdparty/autotrace/vector.h create mode 100644 src/3rdparty/autotrace/xstd.h create mode 100644 src/3rdparty/libcroco/CMakeLists.txt create mode 100644 src/3rdparty/libcroco/README create mode 100644 src/3rdparty/libcroco/cr-additional-sel.c create mode 100644 src/3rdparty/libcroco/cr-additional-sel.h create mode 100644 src/3rdparty/libcroco/cr-attr-sel.c create mode 100644 src/3rdparty/libcroco/cr-attr-sel.h create mode 100644 src/3rdparty/libcroco/cr-cascade.c create mode 100644 src/3rdparty/libcroco/cr-cascade.h create mode 100644 src/3rdparty/libcroco/cr-declaration.c create mode 100644 src/3rdparty/libcroco/cr-declaration.h create mode 100644 src/3rdparty/libcroco/cr-doc-handler.c create mode 100644 src/3rdparty/libcroco/cr-doc-handler.h create mode 100644 src/3rdparty/libcroco/cr-enc-handler.c create mode 100644 src/3rdparty/libcroco/cr-enc-handler.h create mode 100644 src/3rdparty/libcroco/cr-fonts.c create mode 100644 src/3rdparty/libcroco/cr-fonts.h create mode 100644 src/3rdparty/libcroco/cr-input.c create mode 100644 src/3rdparty/libcroco/cr-input.h create mode 100644 src/3rdparty/libcroco/cr-libxml-node-iface.c create mode 100644 src/3rdparty/libcroco/cr-libxml-node-iface.h create mode 100644 src/3rdparty/libcroco/cr-node-iface.h create mode 100644 src/3rdparty/libcroco/cr-num.c create mode 100644 src/3rdparty/libcroco/cr-num.h create mode 100644 src/3rdparty/libcroco/cr-om-parser.c create mode 100644 src/3rdparty/libcroco/cr-om-parser.h create mode 100644 src/3rdparty/libcroco/cr-parser.c create mode 100644 src/3rdparty/libcroco/cr-parser.h create mode 100644 src/3rdparty/libcroco/cr-parsing-location.c create mode 100644 src/3rdparty/libcroco/cr-parsing-location.h create mode 100644 src/3rdparty/libcroco/cr-prop-list.c create mode 100644 src/3rdparty/libcroco/cr-prop-list.h create mode 100644 src/3rdparty/libcroco/cr-pseudo.c create mode 100644 src/3rdparty/libcroco/cr-pseudo.h create mode 100644 src/3rdparty/libcroco/cr-rgb.c create mode 100644 src/3rdparty/libcroco/cr-rgb.h create mode 100644 src/3rdparty/libcroco/cr-sel-eng.c create mode 100644 src/3rdparty/libcroco/cr-sel-eng.h create mode 100644 src/3rdparty/libcroco/cr-selector.c create mode 100644 src/3rdparty/libcroco/cr-selector.h create mode 100644 src/3rdparty/libcroco/cr-simple-sel.c create mode 100644 src/3rdparty/libcroco/cr-simple-sel.h create mode 100644 src/3rdparty/libcroco/cr-statement.c create mode 100644 src/3rdparty/libcroco/cr-statement.h create mode 100644 src/3rdparty/libcroco/cr-string.c create mode 100644 src/3rdparty/libcroco/cr-string.h create mode 100644 src/3rdparty/libcroco/cr-style.c create mode 100644 src/3rdparty/libcroco/cr-style.h create mode 100644 src/3rdparty/libcroco/cr-stylesheet.c create mode 100644 src/3rdparty/libcroco/cr-stylesheet.h create mode 100644 src/3rdparty/libcroco/cr-term.c create mode 100644 src/3rdparty/libcroco/cr-term.h create mode 100644 src/3rdparty/libcroco/cr-tknzr.c create mode 100644 src/3rdparty/libcroco/cr-tknzr.h create mode 100644 src/3rdparty/libcroco/cr-token.c create mode 100644 src/3rdparty/libcroco/cr-token.h create mode 100644 src/3rdparty/libcroco/cr-utils.c create mode 100644 src/3rdparty/libcroco/cr-utils.h create mode 100644 src/3rdparty/libcroco/libcroco.h create mode 100644 src/3rdparty/libdepixelize/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README create mode 100644 src/3rdparty/libdepixelize/CMakeLists.txt create mode 100644 src/3rdparty/libdepixelize/kopftracer2011.cpp create mode 100644 src/3rdparty/libdepixelize/kopftracer2011.h create mode 100644 src/3rdparty/libdepixelize/priv/branchless.h create mode 100644 src/3rdparty/libdepixelize/priv/colorspace.h create mode 100644 src/3rdparty/libdepixelize/priv/curvature.h create mode 100644 src/3rdparty/libdepixelize/priv/homogeneoussplines.h create mode 100644 src/3rdparty/libdepixelize/priv/integral.h create mode 100644 src/3rdparty/libdepixelize/priv/iterator.h create mode 100644 src/3rdparty/libdepixelize/priv/optimization-kopf2011.h create mode 100644 src/3rdparty/libdepixelize/priv/pixelgraph.h create mode 100644 src/3rdparty/libdepixelize/priv/point.h create mode 100644 src/3rdparty/libdepixelize/priv/simplifiedvoronoi.h create mode 100644 src/3rdparty/libdepixelize/priv/splines-kopf2011.h create mode 100644 src/3rdparty/libdepixelize/splines.h create mode 100644 src/3rdparty/libuemf/CMakeLists.txt create mode 100644 src/3rdparty/libuemf/README create mode 100644 src/3rdparty/libuemf/symbol_convert.c create mode 100644 src/3rdparty/libuemf/symbol_convert.h create mode 100644 src/3rdparty/libuemf/uemf.c create mode 100644 src/3rdparty/libuemf/uemf.h create mode 100644 src/3rdparty/libuemf/uemf_endian.c create mode 100644 src/3rdparty/libuemf/uemf_endian.h create mode 100644 src/3rdparty/libuemf/uemf_print.c create mode 100644 src/3rdparty/libuemf/uemf_print.h create mode 100644 src/3rdparty/libuemf/uemf_safe.c create mode 100644 src/3rdparty/libuemf/uemf_safe.h create mode 100644 src/3rdparty/libuemf/uemf_utf.c create mode 100644 src/3rdparty/libuemf/uemf_utf.h create mode 100644 src/3rdparty/libuemf/upmf.c create mode 100644 src/3rdparty/libuemf/upmf.h create mode 100644 src/3rdparty/libuemf/upmf_print.c create mode 100644 src/3rdparty/libuemf/upmf_print.h create mode 100644 src/3rdparty/libuemf/uwmf.c create mode 100644 src/3rdparty/libuemf/uwmf.h create mode 100644 src/3rdparty/libuemf/uwmf_endian.c create mode 100644 src/3rdparty/libuemf/uwmf_endian.h create mode 100644 src/3rdparty/libuemf/uwmf_print.c create mode 100644 src/3rdparty/libuemf/uwmf_print.h create mode 100644 src/CMakeLists.txt create mode 100644 src/actions/CMakeLists.txt create mode 100644 src/actions/README create mode 100644 src/actions/actions-base.cpp create mode 100644 src/actions/actions-base.h create mode 100644 src/actions/actions-extra-data.cpp create mode 100644 src/actions/actions-extra-data.h create mode 100644 src/actions/actions-file.cpp create mode 100644 src/actions/actions-file.h create mode 100644 src/actions/actions-helper.cpp create mode 100644 src/actions/actions-helper.h create mode 100644 src/actions/actions-object.cpp create mode 100644 src/actions/actions-object.h create mode 100644 src/actions/actions-output.cpp create mode 100644 src/actions/actions-output.h create mode 100644 src/actions/actions-selection.cpp create mode 100644 src/actions/actions-selection.h create mode 100644 src/actions/actions-transform.cpp create mode 100644 src/actions/actions-transform.h create mode 100644 src/actions/actions-window.cpp create mode 100644 src/actions/actions-window.h create mode 100644 src/attribute-rel-css.cpp create mode 100644 src/attribute-rel-css.h create mode 100644 src/attribute-rel-svg.cpp create mode 100644 src/attribute-rel-svg.h create mode 100644 src/attribute-rel-util.cpp create mode 100644 src/attribute-rel-util.h create mode 100644 src/attribute-sort-util.cpp create mode 100644 src/attribute-sort-util.h create mode 100644 src/attributes.cpp create mode 100644 src/attributes.h create mode 100644 src/auto-save.cpp create mode 100644 src/auto-save.h create mode 100644 src/axis-manip.cpp create mode 100644 src/axis-manip.h create mode 100644 src/bad-uri-exception.h create mode 100644 src/cms-color-types.h create mode 100644 src/cms-system.h create mode 100644 src/color-profile-cms-fns.h create mode 100644 src/color-rgba.h create mode 100644 src/color.cpp create mode 100644 src/color.h create mode 100644 src/colorspace.h create mode 100644 src/composite-undo-stack-observer.cpp create mode 100644 src/composite-undo-stack-observer.h create mode 100644 src/conditions.cpp create mode 100644 src/conditions.h create mode 100644 src/conn-avoid-ref.cpp create mode 100644 src/conn-avoid-ref.h create mode 100644 src/console-output-undo-observer.cpp create mode 100644 src/console-output-undo-observer.h create mode 100644 src/context-fns.cpp create mode 100644 src/context-fns.h create mode 100644 src/debug/CMakeLists.txt create mode 100644 src/debug/demangle.cpp create mode 100644 src/debug/demangle.h create mode 100644 src/debug/event-tracker.h create mode 100644 src/debug/event.h create mode 100644 src/debug/gc-heap.h create mode 100644 src/debug/gdk-event-latency-tracker.cpp create mode 100644 src/debug/gdk-event-latency-tracker.h create mode 100644 src/debug/heap.cpp create mode 100644 src/debug/heap.h create mode 100644 src/debug/log-display-config.cpp create mode 100644 src/debug/log-display-config.h create mode 100644 src/debug/logger.cpp create mode 100644 src/debug/logger.h create mode 100644 src/debug/simple-event.h create mode 100644 src/debug/sysv-heap.cpp create mode 100644 src/debug/sysv-heap.h create mode 100644 src/debug/timestamp.cpp create mode 100644 src/debug/timestamp.h create mode 100644 src/deptool.cpp create mode 100644 src/desktop-events.cpp create mode 100644 src/desktop-events.h create mode 100644 src/desktop-style.cpp create mode 100644 src/desktop-style.h create mode 100644 src/desktop.cpp create mode 100644 src/desktop.h create mode 100644 src/device-manager.cpp create mode 100644 src/device-manager.h create mode 100644 src/display/CMakeLists.txt create mode 100644 src/display/README create mode 100644 src/display/cairo-templates.h create mode 100644 src/display/cairo-utils.cpp create mode 100644 src/display/cairo-utils.h create mode 100644 src/display/canvas-arena.cpp create mode 100644 src/display/canvas-arena.h create mode 100644 src/display/canvas-axonomgrid.cpp create mode 100644 src/display/canvas-axonomgrid.h create mode 100644 src/display/canvas-bpath.cpp create mode 100644 src/display/canvas-bpath.h create mode 100644 src/display/canvas-debug.cpp create mode 100644 src/display/canvas-debug.h create mode 100644 src/display/canvas-grid.cpp create mode 100644 src/display/canvas-grid.h create mode 100644 src/display/canvas-rotate.cpp create mode 100644 src/display/canvas-rotate.h create mode 100644 src/display/canvas-temporary-item-list.cpp create mode 100644 src/display/canvas-temporary-item-list.h create mode 100644 src/display/canvas-temporary-item.cpp create mode 100644 src/display/canvas-temporary-item.h create mode 100644 src/display/canvas-text.cpp create mode 100644 src/display/canvas-text.h create mode 100644 src/display/curve.cpp create mode 100644 src/display/curve.h create mode 100644 src/display/drawing-context.cpp create mode 100644 src/display/drawing-context.h create mode 100644 src/display/drawing-group.cpp create mode 100644 src/display/drawing-group.h create mode 100644 src/display/drawing-image.cpp create mode 100644 src/display/drawing-image.h create mode 100644 src/display/drawing-item.cpp create mode 100644 src/display/drawing-item.h create mode 100644 src/display/drawing-pattern.cpp create mode 100644 src/display/drawing-pattern.h create mode 100644 src/display/drawing-shape.cpp create mode 100644 src/display/drawing-shape.h create mode 100644 src/display/drawing-surface.cpp create mode 100644 src/display/drawing-surface.h create mode 100644 src/display/drawing-text.cpp create mode 100644 src/display/drawing-text.h create mode 100644 src/display/drawing.cpp create mode 100644 src/display/drawing.h create mode 100644 src/display/gnome-canvas-acetate.cpp create mode 100644 src/display/gnome-canvas-acetate.h create mode 100644 src/display/grayscale.cpp create mode 100644 src/display/grayscale.h create mode 100644 src/display/guideline.cpp create mode 100644 src/display/guideline.h create mode 100644 src/display/nr-3dutils.cpp create mode 100644 src/display/nr-3dutils.h create mode 100644 src/display/nr-filter-blend.cpp create mode 100644 src/display/nr-filter-blend.h create mode 100644 src/display/nr-filter-colormatrix.cpp create mode 100644 src/display/nr-filter-colormatrix.h create mode 100644 src/display/nr-filter-component-transfer.cpp create mode 100644 src/display/nr-filter-component-transfer.h create mode 100644 src/display/nr-filter-composite.cpp create mode 100644 src/display/nr-filter-composite.h create mode 100644 src/display/nr-filter-convolve-matrix.cpp create mode 100644 src/display/nr-filter-convolve-matrix.h create mode 100644 src/display/nr-filter-diffuselighting.cpp create mode 100644 src/display/nr-filter-diffuselighting.h create mode 100644 src/display/nr-filter-displacement-map.cpp create mode 100644 src/display/nr-filter-displacement-map.h create mode 100644 src/display/nr-filter-flood.cpp create mode 100644 src/display/nr-filter-flood.h create mode 100644 src/display/nr-filter-gaussian.cpp create mode 100644 src/display/nr-filter-gaussian.h create mode 100644 src/display/nr-filter-image.cpp create mode 100644 src/display/nr-filter-image.h create mode 100644 src/display/nr-filter-merge.cpp create mode 100644 src/display/nr-filter-merge.h create mode 100644 src/display/nr-filter-morphology.cpp create mode 100644 src/display/nr-filter-morphology.h create mode 100644 src/display/nr-filter-offset.cpp create mode 100644 src/display/nr-filter-offset.h create mode 100644 src/display/nr-filter-primitive.cpp create mode 100644 src/display/nr-filter-primitive.h create mode 100644 src/display/nr-filter-skeleton.cpp create mode 100644 src/display/nr-filter-skeleton.h create mode 100644 src/display/nr-filter-slot.cpp create mode 100644 src/display/nr-filter-slot.h create mode 100644 src/display/nr-filter-specularlighting.cpp create mode 100644 src/display/nr-filter-specularlighting.h create mode 100644 src/display/nr-filter-tile.cpp create mode 100644 src/display/nr-filter-tile.h create mode 100644 src/display/nr-filter-turbulence.cpp create mode 100644 src/display/nr-filter-turbulence.h create mode 100644 src/display/nr-filter-types.h create mode 100644 src/display/nr-filter-units.cpp create mode 100644 src/display/nr-filter-units.h create mode 100644 src/display/nr-filter-utils.h create mode 100644 src/display/nr-filter.cpp create mode 100644 src/display/nr-filter.h create mode 100644 src/display/nr-light-types.h create mode 100644 src/display/nr-light.cpp create mode 100644 src/display/nr-light.h create mode 100644 src/display/nr-style.cpp create mode 100644 src/display/nr-style.h create mode 100644 src/display/nr-svgfonts.cpp create mode 100644 src/display/nr-svgfonts.h create mode 100644 src/display/rendermode.h create mode 100644 src/display/snap-indicator.cpp create mode 100644 src/display/snap-indicator.h create mode 100644 src/display/sodipodi-ctrl.cpp create mode 100644 src/display/sodipodi-ctrl.h create mode 100644 src/display/sodipodi-ctrlrect.cpp create mode 100644 src/display/sodipodi-ctrlrect.h create mode 100644 src/display/sp-canvas-group.h create mode 100644 src/display/sp-canvas-item.h create mode 100644 src/display/sp-canvas-util.cpp create mode 100644 src/display/sp-canvas-util.h create mode 100644 src/display/sp-canvas.cpp create mode 100644 src/display/sp-canvas.h create mode 100644 src/display/sp-ctrlcurve.cpp create mode 100644 src/display/sp-ctrlcurve.h create mode 100644 src/display/sp-ctrlline.cpp create mode 100644 src/display/sp-ctrlline.h create mode 100644 src/display/sp-ctrlquadr.cpp create mode 100644 src/display/sp-ctrlquadr.h create mode 100644 src/document-subset.cpp create mode 100644 src/document-subset.h create mode 100644 src/document-undo.cpp create mode 100644 src/document-undo.h create mode 100644 src/document.cpp create mode 100644 src/document.h create mode 100644 src/doxygen-main.dox create mode 100644 src/ege-color-prof-tracker.cpp create mode 100644 src/ege-color-prof-tracker.h create mode 100644 src/enums.h create mode 100644 src/event-log.cpp create mode 100644 src/event-log.h create mode 100644 src/event.h create mode 100644 src/extension/CMakeLists.txt create mode 100644 src/extension/db.cpp create mode 100644 src/extension/db.h create mode 100644 src/extension/dbus/CMakeLists.txt create mode 100644 src/extension/dbus/Notes.txt create mode 100644 src/extension/dbus/application-interface.cpp create mode 100644 src/extension/dbus/application-interface.h create mode 100644 src/extension/dbus/application-interface.xml create mode 100644 src/extension/dbus/builddocs.sh create mode 100644 src/extension/dbus/dbus-init.cpp create mode 100644 src/extension/dbus/dbus-init.h create mode 100644 src/extension/dbus/doc/config.xsl create mode 100644 src/extension/dbus/doc/dbus-introspect-docs.dtd create mode 100644 src/extension/dbus/doc/docbook.css create mode 100644 src/extension/dbus/doc/inkscapeDbusRef.xml create mode 100644 src/extension/dbus/doc/inkscapeDbusTerms.xml create mode 100644 src/extension/dbus/doc/spec-to-docbook.xsl create mode 100644 src/extension/dbus/document-interface.cpp create mode 100644 src/extension/dbus/document-interface.h create mode 100644 src/extension/dbus/document-interface.xml create mode 100644 src/extension/dbus/org.inkscape.service.in create mode 100644 src/extension/dbus/proposed-interface.xml create mode 100644 src/extension/dbus/pytester.py create mode 100644 src/extension/dbus/wrapper/inkdbus.pc.in create mode 100644 src/extension/dbus/wrapper/inkscape-dbus-wrapper.c create mode 100644 src/extension/dbus/wrapper/inkscape-dbus-wrapper.h create mode 100644 src/extension/dependency.cpp create mode 100644 src/extension/dependency.h create mode 100644 src/extension/effect.cpp create mode 100644 src/extension/effect.h create mode 100644 src/extension/error-file.cpp create mode 100644 src/extension/error-file.h create mode 100644 src/extension/execution-env.cpp create mode 100644 src/extension/execution-env.h create mode 100644 src/extension/extension.cpp create mode 100644 src/extension/extension.h create mode 100644 src/extension/find_extension_by_mime.h create mode 100644 src/extension/implementation/implementation.cpp create mode 100644 src/extension/implementation/implementation.h create mode 100644 src/extension/implementation/script.cpp create mode 100644 src/extension/implementation/script.h create mode 100644 src/extension/implementation/xslt.cpp create mode 100644 src/extension/implementation/xslt.h create mode 100644 src/extension/init.cpp create mode 100644 src/extension/init.h create mode 100644 src/extension/input.cpp create mode 100644 src/extension/input.h create mode 100644 src/extension/internal/bitmap/adaptiveThreshold.cpp create mode 100644 src/extension/internal/bitmap/adaptiveThreshold.h create mode 100644 src/extension/internal/bitmap/addNoise.cpp create mode 100644 src/extension/internal/bitmap/addNoise.h create mode 100644 src/extension/internal/bitmap/blur.cpp create mode 100644 src/extension/internal/bitmap/blur.h create mode 100644 src/extension/internal/bitmap/channel.cpp create mode 100644 src/extension/internal/bitmap/channel.h create mode 100644 src/extension/internal/bitmap/charcoal.cpp create mode 100644 src/extension/internal/bitmap/charcoal.h create mode 100644 src/extension/internal/bitmap/colorize.cpp create mode 100644 src/extension/internal/bitmap/colorize.h create mode 100644 src/extension/internal/bitmap/contrast.cpp create mode 100644 src/extension/internal/bitmap/contrast.h create mode 100644 src/extension/internal/bitmap/crop.cpp create mode 100644 src/extension/internal/bitmap/crop.h create mode 100644 src/extension/internal/bitmap/cycleColormap.cpp create mode 100644 src/extension/internal/bitmap/cycleColormap.h create mode 100644 src/extension/internal/bitmap/despeckle.cpp create mode 100644 src/extension/internal/bitmap/despeckle.h create mode 100644 src/extension/internal/bitmap/edge.cpp create mode 100644 src/extension/internal/bitmap/edge.h create mode 100644 src/extension/internal/bitmap/emboss.cpp create mode 100644 src/extension/internal/bitmap/emboss.h create mode 100644 src/extension/internal/bitmap/enhance.cpp create mode 100644 src/extension/internal/bitmap/enhance.h create mode 100644 src/extension/internal/bitmap/equalize.cpp create mode 100644 src/extension/internal/bitmap/equalize.h create mode 100644 src/extension/internal/bitmap/gaussianBlur.cpp create mode 100644 src/extension/internal/bitmap/gaussianBlur.h create mode 100644 src/extension/internal/bitmap/imagemagick.cpp create mode 100644 src/extension/internal/bitmap/imagemagick.h create mode 100644 src/extension/internal/bitmap/implode.cpp create mode 100644 src/extension/internal/bitmap/implode.h create mode 100644 src/extension/internal/bitmap/level.cpp create mode 100644 src/extension/internal/bitmap/level.h create mode 100644 src/extension/internal/bitmap/levelChannel.cpp create mode 100644 src/extension/internal/bitmap/levelChannel.h create mode 100644 src/extension/internal/bitmap/medianFilter.cpp create mode 100644 src/extension/internal/bitmap/medianFilter.h create mode 100644 src/extension/internal/bitmap/modulate.cpp create mode 100644 src/extension/internal/bitmap/modulate.h create mode 100644 src/extension/internal/bitmap/negate.cpp create mode 100644 src/extension/internal/bitmap/negate.h create mode 100644 src/extension/internal/bitmap/normalize.cpp create mode 100644 src/extension/internal/bitmap/normalize.h create mode 100644 src/extension/internal/bitmap/oilPaint.cpp create mode 100644 src/extension/internal/bitmap/oilPaint.h create mode 100644 src/extension/internal/bitmap/opacity.cpp create mode 100644 src/extension/internal/bitmap/opacity.h create mode 100644 src/extension/internal/bitmap/raise.cpp create mode 100644 src/extension/internal/bitmap/raise.h create mode 100644 src/extension/internal/bitmap/reduceNoise.cpp create mode 100644 src/extension/internal/bitmap/reduceNoise.h create mode 100644 src/extension/internal/bitmap/sample.cpp create mode 100644 src/extension/internal/bitmap/sample.h create mode 100644 src/extension/internal/bitmap/shade.cpp create mode 100644 src/extension/internal/bitmap/shade.h create mode 100644 src/extension/internal/bitmap/sharpen.cpp create mode 100644 src/extension/internal/bitmap/sharpen.h create mode 100644 src/extension/internal/bitmap/solarize.cpp create mode 100644 src/extension/internal/bitmap/solarize.h create mode 100644 src/extension/internal/bitmap/spread.cpp create mode 100644 src/extension/internal/bitmap/spread.h create mode 100644 src/extension/internal/bitmap/swirl.cpp create mode 100644 src/extension/internal/bitmap/swirl.h create mode 100644 src/extension/internal/bitmap/threshold.cpp create mode 100644 src/extension/internal/bitmap/threshold.h create mode 100644 src/extension/internal/bitmap/unsharpmask.cpp create mode 100644 src/extension/internal/bitmap/unsharpmask.h create mode 100644 src/extension/internal/bitmap/wave.cpp create mode 100644 src/extension/internal/bitmap/wave.h create mode 100644 src/extension/internal/bluredge.cpp create mode 100644 src/extension/internal/bluredge.h create mode 100644 src/extension/internal/cairo-ps-out.cpp create mode 100644 src/extension/internal/cairo-ps-out.h create mode 100644 src/extension/internal/cairo-render-context.cpp create mode 100644 src/extension/internal/cairo-render-context.h create mode 100644 src/extension/internal/cairo-renderer-pdf-out.cpp create mode 100644 src/extension/internal/cairo-renderer-pdf-out.h create mode 100644 src/extension/internal/cairo-renderer.cpp create mode 100644 src/extension/internal/cairo-renderer.h create mode 100644 src/extension/internal/cdr-input.cpp create mode 100644 src/extension/internal/cdr-input.h create mode 100644 src/extension/internal/clear-n_.h create mode 100644 src/extension/internal/emf-inout.cpp create mode 100644 src/extension/internal/emf-inout.h create mode 100644 src/extension/internal/emf-print.cpp create mode 100644 src/extension/internal/emf-print.h create mode 100644 src/extension/internal/filter/BUILD_YOUR_OWN create mode 100644 src/extension/internal/filter/bevels.h create mode 100644 src/extension/internal/filter/blurs.h create mode 100644 src/extension/internal/filter/bumps.h create mode 100644 src/extension/internal/filter/color.h create mode 100644 src/extension/internal/filter/distort.h create mode 100644 src/extension/internal/filter/filter-all.cpp create mode 100644 src/extension/internal/filter/filter-file.cpp create mode 100644 src/extension/internal/filter/filter.cpp create mode 100644 src/extension/internal/filter/filter.h create mode 100644 src/extension/internal/filter/image.h create mode 100644 src/extension/internal/filter/morphology.h create mode 100644 src/extension/internal/filter/overlays.h create mode 100644 src/extension/internal/filter/paint.h create mode 100644 src/extension/internal/filter/protrusions.h create mode 100644 src/extension/internal/filter/shadows.h create mode 100644 src/extension/internal/filter/textures.h create mode 100644 src/extension/internal/filter/transparency.h create mode 100644 src/extension/internal/gdkpixbuf-input.cpp create mode 100644 src/extension/internal/gdkpixbuf-input.h create mode 100644 src/extension/internal/gimpgrad.cpp create mode 100644 src/extension/internal/gimpgrad.h create mode 100644 src/extension/internal/grid.cpp create mode 100644 src/extension/internal/grid.h create mode 100644 src/extension/internal/image-resolution.cpp create mode 100644 src/extension/internal/image-resolution.h create mode 100644 src/extension/internal/latex-pstricks-out.cpp create mode 100644 src/extension/internal/latex-pstricks-out.h create mode 100644 src/extension/internal/latex-pstricks.cpp create mode 100644 src/extension/internal/latex-pstricks.h create mode 100644 src/extension/internal/latex-text-renderer.cpp create mode 100644 src/extension/internal/latex-text-renderer.h create mode 100644 src/extension/internal/metafile-inout.cpp create mode 100644 src/extension/internal/metafile-inout.h create mode 100644 src/extension/internal/metafile-print.cpp create mode 100644 src/extension/internal/metafile-print.h create mode 100644 src/extension/internal/odf.cpp create mode 100644 src/extension/internal/odf.h create mode 100644 src/extension/internal/pdfinput/pdf-input.cpp create mode 100644 src/extension/internal/pdfinput/pdf-input.h create mode 100644 src/extension/internal/pdfinput/pdf-parser.cpp create mode 100644 src/extension/internal/pdfinput/pdf-parser.h create mode 100644 src/extension/internal/pdfinput/poppler-transition-api.h create mode 100644 src/extension/internal/pdfinput/svg-builder.cpp create mode 100644 src/extension/internal/pdfinput/svg-builder.h create mode 100644 src/extension/internal/polyfill/README.md create mode 100644 src/extension/internal/polyfill/hatch.js create mode 100644 src/extension/internal/polyfill/hatch_compressed.include create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch.svg create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg create mode 100644 src/extension/internal/polyfill/hatch_tests/hatch_test.svg create mode 100644 src/extension/internal/polyfill/mesh.js create mode 100644 src/extension/internal/polyfill/mesh_compressed.include create mode 100644 src/extension/internal/pov-out.cpp create mode 100644 src/extension/internal/pov-out.h create mode 100644 src/extension/internal/svg.cpp create mode 100644 src/extension/internal/svg.h create mode 100644 src/extension/internal/svgz.cpp create mode 100644 src/extension/internal/svgz.h create mode 100644 src/extension/internal/text_reassemble.c create mode 100644 src/extension/internal/text_reassemble.h create mode 100644 src/extension/internal/vsd-input.cpp create mode 100644 src/extension/internal/vsd-input.h create mode 100644 src/extension/internal/wmf-inout.cpp create mode 100644 src/extension/internal/wmf-inout.h create mode 100644 src/extension/internal/wmf-print.cpp create mode 100644 src/extension/internal/wmf-print.h create mode 100644 src/extension/internal/wpg-input.cpp create mode 100644 src/extension/internal/wpg-input.h create mode 100644 src/extension/loader.cpp create mode 100644 src/extension/loader.h create mode 100644 src/extension/output.cpp create mode 100644 src/extension/output.h create mode 100644 src/extension/patheffect.cpp create mode 100644 src/extension/patheffect.h create mode 100644 src/extension/plugins/CMakeLists.txt create mode 100644 src/extension/plugins/grid2/CMakeLists.txt create mode 100644 src/extension/plugins/grid2/grid.cpp create mode 100644 src/extension/plugins/grid2/grid.h create mode 100644 src/extension/plugins/grid2/libgrid2.inx create mode 100644 src/extension/prefdialog/parameter-bool.cpp create mode 100644 src/extension/prefdialog/parameter-bool.h create mode 100644 src/extension/prefdialog/parameter-color.cpp create mode 100644 src/extension/prefdialog/parameter-color.h create mode 100644 src/extension/prefdialog/parameter-float.cpp create mode 100644 src/extension/prefdialog/parameter-float.h create mode 100644 src/extension/prefdialog/parameter-int.cpp create mode 100644 src/extension/prefdialog/parameter-int.h create mode 100644 src/extension/prefdialog/parameter-notebook.cpp create mode 100644 src/extension/prefdialog/parameter-notebook.h create mode 100644 src/extension/prefdialog/parameter-optiongroup.cpp create mode 100644 src/extension/prefdialog/parameter-optiongroup.h create mode 100644 src/extension/prefdialog/parameter-path.cpp create mode 100644 src/extension/prefdialog/parameter-path.h create mode 100644 src/extension/prefdialog/parameter-string.cpp create mode 100644 src/extension/prefdialog/parameter-string.h create mode 100644 src/extension/prefdialog/parameter.cpp create mode 100644 src/extension/prefdialog/parameter.h create mode 100644 src/extension/prefdialog/prefdialog.cpp create mode 100644 src/extension/prefdialog/prefdialog.h create mode 100644 src/extension/prefdialog/widget-box.cpp create mode 100644 src/extension/prefdialog/widget-box.h create mode 100644 src/extension/prefdialog/widget-image.cpp create mode 100644 src/extension/prefdialog/widget-image.h create mode 100644 src/extension/prefdialog/widget-label.cpp create mode 100644 src/extension/prefdialog/widget-label.h create mode 100644 src/extension/prefdialog/widget-separator.cpp create mode 100644 src/extension/prefdialog/widget-separator.h create mode 100644 src/extension/prefdialog/widget-spacer.cpp create mode 100644 src/extension/prefdialog/widget-spacer.h create mode 100644 src/extension/prefdialog/widget.cpp create mode 100644 src/extension/prefdialog/widget.h create mode 100644 src/extension/print.cpp create mode 100644 src/extension/print.h create mode 100644 src/extension/system.cpp create mode 100644 src/extension/system.h create mode 100644 src/extension/timer.cpp create mode 100644 src/extension/timer.h create mode 100644 src/extract-uri.cpp create mode 100644 src/extract-uri.h create mode 100644 src/file-update.cpp create mode 100644 src/file.cpp create mode 100644 src/file.h create mode 100644 src/fill-or-stroke.h create mode 100644 src/filter-chemistry.cpp create mode 100644 src/filter-chemistry.h create mode 100644 src/filter-enums.cpp create mode 100644 src/filter-enums.h create mode 100644 src/gc-anchored.cpp create mode 100644 src/gc-anchored.h create mode 100644 src/gc-finalized.cpp create mode 100644 src/gc-finalized.h create mode 100644 src/gradient-chemistry.cpp create mode 100644 src/gradient-chemistry.h create mode 100644 src/gradient-drag.cpp create mode 100644 src/gradient-drag.h create mode 100644 src/graphlayout.cpp create mode 100644 src/graphlayout.h create mode 100644 src/guide-snapper.cpp create mode 100644 src/guide-snapper.h create mode 100644 src/help.cpp create mode 100644 src/help.h create mode 100644 src/helper-fns.h create mode 100644 src/helper/CMakeLists.txt create mode 100644 src/helper/README create mode 100644 src/helper/action-context.cpp create mode 100644 src/helper/action-context.h create mode 100644 src/helper/action.cpp create mode 100644 src/helper/action.h create mode 100644 src/helper/geom-curves.h create mode 100644 src/helper/geom-nodetype.cpp create mode 100644 src/helper/geom-nodetype.h create mode 100644 src/helper/geom-pathstroke.cpp create mode 100644 src/helper/geom-pathstroke.h create mode 100644 src/helper/geom-pathvectorsatellites.cpp create mode 100644 src/helper/geom-pathvectorsatellites.h create mode 100644 src/helper/geom-satellite.cpp create mode 100644 src/helper/geom-satellite.h create mode 100644 src/helper/geom.cpp create mode 100644 src/helper/geom.h create mode 100644 src/helper/gettext.cpp create mode 100644 src/helper/gettext.h create mode 100644 src/helper/mathfns.h create mode 100644 src/helper/pixbuf-ops.cpp create mode 100644 src/helper/pixbuf-ops.h create mode 100644 src/helper/png-write.cpp create mode 100644 src/helper/png-write.h create mode 100644 src/helper/sp-marshal.list create mode 100644 src/helper/stock-items.cpp create mode 100644 src/helper/stock-items.h create mode 100644 src/helper/verb-action.cpp create mode 100644 src/helper/verb-action.h create mode 100644 src/id-clash.cpp create mode 100644 src/id-clash.h create mode 100644 src/include/CMakeLists.txt create mode 100644 src/include/README create mode 100644 src/include/glibmm_version.h create mode 100644 src/include/gtkmm_version.h create mode 100644 src/include/macros.h create mode 100644 src/include/source_date_epoch.h create mode 100644 src/inkgc/CMakeLists.txt create mode 100644 src/inkgc/README create mode 100644 src/inkgc/gc-alloc.h create mode 100644 src/inkgc/gc-core.h create mode 100644 src/inkgc/gc-managed.h create mode 100644 src/inkgc/gc-soft-ptr.h create mode 100644 src/inkgc/gc.cpp create mode 100644 src/inkscape-application.cpp create mode 100644 src/inkscape-application.h create mode 100644 src/inkscape-main.cpp create mode 100644 src/inkscape-manifest.xml create mode 100644 src/inkscape-version.cpp.in create mode 100644 src/inkscape-version.h create mode 100644 src/inkscape-window.cpp create mode 100644 src/inkscape-window.h create mode 100644 src/inkscape.cpp create mode 100644 src/inkscape.h create mode 100644 src/inkscape.rc create mode 100644 src/inkview-application.cpp create mode 100644 src/inkview-application.h create mode 100644 src/inkview-main.cpp create mode 100644 src/inkview-window.cpp create mode 100644 src/inkview-window.h create mode 100644 src/io/CMakeLists.txt create mode 100644 src/io/README create mode 100644 src/io/crystalegg.xml create mode 100644 src/io/dir-util.cpp create mode 100644 src/io/dir-util.h create mode 100644 src/io/doc2html.xsl create mode 100644 src/io/file-export-cmd.cpp create mode 100644 src/io/file-export-cmd.h create mode 100644 src/io/file.cpp create mode 100644 src/io/file.h create mode 100644 src/io/http.cpp create mode 100644 src/io/http.h create mode 100644 src/io/resource-manager.cpp create mode 100644 src/io/resource-manager.h create mode 100644 src/io/resource.cpp create mode 100644 src/io/resource.h create mode 100644 src/io/stream/Makefile.tst create mode 100644 src/io/stream/README create mode 100644 src/io/stream/bufferstream.cpp create mode 100644 src/io/stream/bufferstream.h create mode 100644 src/io/stream/gzipstream.cpp create mode 100644 src/io/stream/gzipstream.h create mode 100644 src/io/stream/inkscapestream.cpp create mode 100644 src/io/stream/inkscapestream.h create mode 100644 src/io/stream/streamtest.cpp create mode 100644 src/io/stream/stringstream.cpp create mode 100644 src/io/stream/stringstream.h create mode 100644 src/io/stream/uristream.cpp create mode 100644 src/io/stream/uristream.h create mode 100644 src/io/stream/xsltstream.cpp create mode 100644 src/io/stream/xsltstream.h create mode 100644 src/io/sys.cpp create mode 100644 src/io/sys.h create mode 100644 src/knot-enums.h create mode 100644 src/knot-holder-entity.cpp create mode 100644 src/knot-holder-entity.h create mode 100644 src/knot-ptr.cpp create mode 100644 src/knot-ptr.h create mode 100644 src/knot.cpp create mode 100644 src/knot.h create mode 100644 src/knotholder.cpp create mode 100644 src/knotholder.h create mode 100644 src/layer-fns.cpp create mode 100644 src/layer-fns.h create mode 100644 src/layer-manager.cpp create mode 100644 src/layer-manager.h create mode 100644 src/layer-model.cpp create mode 100644 src/layer-model.h create mode 100644 src/libnrtype/CMakeLists.txt create mode 100644 src/libnrtype/FontFactory.cpp create mode 100644 src/libnrtype/FontFactory.h create mode 100644 src/libnrtype/FontInstance.cpp create mode 100644 src/libnrtype/Layout-TNG-Compute.cpp create mode 100644 src/libnrtype/Layout-TNG-Input.cpp create mode 100644 src/libnrtype/Layout-TNG-OutIter.cpp create mode 100644 src/libnrtype/Layout-TNG-Output.cpp create mode 100644 src/libnrtype/Layout-TNG-Scanline-Maker.h create mode 100644 src/libnrtype/Layout-TNG-Scanline-Makers.cpp create mode 100644 src/libnrtype/Layout-TNG.cpp create mode 100644 src/libnrtype/Layout-TNG.h create mode 100644 src/libnrtype/OpenTypeUtil.cpp create mode 100644 src/libnrtype/OpenTypeUtil.h create mode 100644 src/libnrtype/font-glyph.h create mode 100644 src/libnrtype/font-instance.h create mode 100644 src/libnrtype/font-lister.cpp create mode 100644 src/libnrtype/font-lister.h create mode 100644 src/libnrtype/font-style.h create mode 100644 src/line-geometry.cpp create mode 100644 src/line-geometry.h create mode 100644 src/line-snapper.cpp create mode 100644 src/line-snapper.h create mode 100644 src/livarot/AVL.cpp create mode 100644 src/livarot/AVL.h create mode 100644 src/livarot/AlphaLigne.cpp create mode 100644 src/livarot/AlphaLigne.h create mode 100644 src/livarot/BitLigne.cpp create mode 100644 src/livarot/BitLigne.h create mode 100644 src/livarot/CMakeLists.txt create mode 100644 src/livarot/Livarot.h create mode 100644 src/livarot/LivarotDefs.h create mode 100644 src/livarot/Path.cpp create mode 100644 src/livarot/Path.h create mode 100644 src/livarot/PathConversion.cpp create mode 100644 src/livarot/PathCutting.cpp create mode 100644 src/livarot/PathOutline.cpp create mode 100644 src/livarot/PathSimplify.cpp create mode 100644 src/livarot/PathStroke.cpp create mode 100644 src/livarot/README create mode 100644 src/livarot/Shape.cpp create mode 100644 src/livarot/Shape.h create mode 100644 src/livarot/ShapeDraw.cpp create mode 100644 src/livarot/ShapeMisc.cpp create mode 100644 src/livarot/ShapeRaster.cpp create mode 100644 src/livarot/ShapeSweep.cpp create mode 100644 src/livarot/float-line.cpp create mode 100644 src/livarot/float-line.h create mode 100644 src/livarot/int-line.cpp create mode 100644 src/livarot/int-line.h create mode 100644 src/livarot/path-description.cpp create mode 100644 src/livarot/path-description.h create mode 100644 src/livarot/sweep-event-queue.h create mode 100644 src/livarot/sweep-event.cpp create mode 100644 src/livarot/sweep-event.h create mode 100644 src/livarot/sweep-tree-list.cpp create mode 100644 src/livarot/sweep-tree-list.h create mode 100644 src/livarot/sweep-tree.cpp create mode 100644 src/livarot/sweep-tree.h create mode 100644 src/live_effects/CMakeLists.txt create mode 100644 src/live_effects/README create mode 100644 src/live_effects/effect-enum.h create mode 100644 src/live_effects/effect.cpp create mode 100644 src/live_effects/effect.h create mode 100644 src/live_effects/lpe-angle_bisector.cpp create mode 100644 src/live_effects/lpe-angle_bisector.h create mode 100644 src/live_effects/lpe-attach-path.cpp create mode 100644 src/live_effects/lpe-attach-path.h create mode 100644 src/live_effects/lpe-bendpath.cpp create mode 100644 src/live_effects/lpe-bendpath.h create mode 100644 src/live_effects/lpe-bool.cpp create mode 100644 src/live_effects/lpe-bool.h create mode 100644 src/live_effects/lpe-bounding-box.cpp create mode 100644 src/live_effects/lpe-bounding-box.h create mode 100644 src/live_effects/lpe-bspline.cpp create mode 100644 src/live_effects/lpe-bspline.h create mode 100644 src/live_effects/lpe-circle_3pts.cpp create mode 100644 src/live_effects/lpe-circle_3pts.h create mode 100644 src/live_effects/lpe-circle_with_radius.cpp create mode 100644 src/live_effects/lpe-circle_with_radius.h create mode 100644 src/live_effects/lpe-clone-original.cpp create mode 100644 src/live_effects/lpe-clone-original.h create mode 100644 src/live_effects/lpe-constructgrid.cpp create mode 100644 src/live_effects/lpe-constructgrid.h create mode 100644 src/live_effects/lpe-copy_rotate.cpp create mode 100644 src/live_effects/lpe-copy_rotate.h create mode 100644 src/live_effects/lpe-curvestitch.cpp create mode 100644 src/live_effects/lpe-curvestitch.h create mode 100644 src/live_effects/lpe-dashed-stroke.cpp create mode 100644 src/live_effects/lpe-dashed-stroke.h create mode 100644 src/live_effects/lpe-dynastroke.cpp create mode 100644 src/live_effects/lpe-dynastroke.h create mode 100644 src/live_effects/lpe-ellipse_5pts.cpp create mode 100644 src/live_effects/lpe-ellipse_5pts.h create mode 100644 src/live_effects/lpe-embrodery-stitch-ordering.cpp create mode 100644 src/live_effects/lpe-embrodery-stitch-ordering.h create mode 100644 src/live_effects/lpe-embrodery-stitch.cpp create mode 100644 src/live_effects/lpe-embrodery-stitch.h create mode 100644 src/live_effects/lpe-envelope.cpp create mode 100644 src/live_effects/lpe-envelope.h create mode 100644 src/live_effects/lpe-extrude.cpp create mode 100644 src/live_effects/lpe-extrude.h create mode 100644 src/live_effects/lpe-fill-between-many.cpp create mode 100644 src/live_effects/lpe-fill-between-many.h create mode 100644 src/live_effects/lpe-fill-between-strokes.cpp create mode 100644 src/live_effects/lpe-fill-between-strokes.h create mode 100644 src/live_effects/lpe-fillet-chamfer.cpp create mode 100644 src/live_effects/lpe-fillet-chamfer.h create mode 100644 src/live_effects/lpe-gears.cpp create mode 100644 src/live_effects/lpe-gears.h create mode 100644 src/live_effects/lpe-interpolate.cpp create mode 100644 src/live_effects/lpe-interpolate.h create mode 100644 src/live_effects/lpe-interpolate_points.cpp create mode 100644 src/live_effects/lpe-interpolate_points.h create mode 100644 src/live_effects/lpe-jointype.cpp create mode 100644 src/live_effects/lpe-jointype.h create mode 100644 src/live_effects/lpe-knot.cpp create mode 100644 src/live_effects/lpe-knot.h create mode 100644 src/live_effects/lpe-lattice.cpp create mode 100644 src/live_effects/lpe-lattice.h create mode 100644 src/live_effects/lpe-lattice2.cpp create mode 100644 src/live_effects/lpe-lattice2.h create mode 100644 src/live_effects/lpe-line_segment.cpp create mode 100644 src/live_effects/lpe-line_segment.h create mode 100644 src/live_effects/lpe-measure-segments.cpp create mode 100644 src/live_effects/lpe-measure-segments.h create mode 100644 src/live_effects/lpe-mirror_symmetry.cpp create mode 100644 src/live_effects/lpe-mirror_symmetry.h create mode 100644 src/live_effects/lpe-offset.cpp create mode 100644 src/live_effects/lpe-offset.h create mode 100644 src/live_effects/lpe-parallel.cpp create mode 100644 src/live_effects/lpe-parallel.h create mode 100644 src/live_effects/lpe-path_length.cpp create mode 100644 src/live_effects/lpe-path_length.h create mode 100644 src/live_effects/lpe-patternalongpath.cpp create mode 100644 src/live_effects/lpe-patternalongpath.h create mode 100644 src/live_effects/lpe-perp_bisector.cpp create mode 100644 src/live_effects/lpe-perp_bisector.h create mode 100644 src/live_effects/lpe-perspective-envelope.cpp create mode 100644 src/live_effects/lpe-perspective-envelope.h create mode 100644 src/live_effects/lpe-powerclip.cpp create mode 100644 src/live_effects/lpe-powerclip.h create mode 100644 src/live_effects/lpe-powermask.cpp create mode 100644 src/live_effects/lpe-powermask.h create mode 100644 src/live_effects/lpe-powerstroke-interpolators.h create mode 100644 src/live_effects/lpe-powerstroke.cpp create mode 100644 src/live_effects/lpe-powerstroke.h create mode 100644 src/live_effects/lpe-pts2ellipse.cpp create mode 100644 src/live_effects/lpe-pts2ellipse.h create mode 100644 src/live_effects/lpe-recursiveskeleton.cpp create mode 100644 src/live_effects/lpe-recursiveskeleton.h create mode 100644 src/live_effects/lpe-rough-hatches.cpp create mode 100644 src/live_effects/lpe-rough-hatches.h create mode 100644 src/live_effects/lpe-roughen.cpp create mode 100644 src/live_effects/lpe-roughen.h create mode 100644 src/live_effects/lpe-ruler.cpp create mode 100644 src/live_effects/lpe-ruler.h create mode 100644 src/live_effects/lpe-show_handles.cpp create mode 100644 src/live_effects/lpe-show_handles.h create mode 100644 src/live_effects/lpe-simplify.cpp create mode 100644 src/live_effects/lpe-simplify.h create mode 100644 src/live_effects/lpe-skeleton.cpp create mode 100644 src/live_effects/lpe-skeleton.h create mode 100644 src/live_effects/lpe-sketch.cpp create mode 100644 src/live_effects/lpe-sketch.h create mode 100644 src/live_effects/lpe-spiro.cpp create mode 100644 src/live_effects/lpe-spiro.h create mode 100644 src/live_effects/lpe-tangent_to_curve.cpp create mode 100644 src/live_effects/lpe-tangent_to_curve.h create mode 100644 src/live_effects/lpe-taperstroke.cpp create mode 100644 src/live_effects/lpe-taperstroke.h create mode 100644 src/live_effects/lpe-test-doEffect-stack.cpp create mode 100644 src/live_effects/lpe-test-doEffect-stack.h create mode 100644 src/live_effects/lpe-text_label.cpp create mode 100644 src/live_effects/lpe-text_label.h create mode 100644 src/live_effects/lpe-transform_2pts.cpp create mode 100644 src/live_effects/lpe-transform_2pts.h create mode 100644 src/live_effects/lpe-vonkoch.cpp create mode 100644 src/live_effects/lpe-vonkoch.h create mode 100644 src/live_effects/lpegroupbbox.cpp create mode 100644 src/live_effects/lpegroupbbox.h create mode 100644 src/live_effects/lpeobject-reference.cpp create mode 100644 src/live_effects/lpeobject-reference.h create mode 100644 src/live_effects/lpeobject.cpp create mode 100644 src/live_effects/lpeobject.h create mode 100644 src/live_effects/parameter/array.cpp create mode 100644 src/live_effects/parameter/array.h create mode 100644 src/live_effects/parameter/bool.cpp create mode 100644 src/live_effects/parameter/bool.h create mode 100644 src/live_effects/parameter/colorpicker.cpp create mode 100644 src/live_effects/parameter/colorpicker.h create mode 100644 src/live_effects/parameter/enum.h create mode 100644 src/live_effects/parameter/fontbutton.cpp create mode 100644 src/live_effects/parameter/fontbutton.h create mode 100644 src/live_effects/parameter/hidden.cpp create mode 100644 src/live_effects/parameter/hidden.h create mode 100644 src/live_effects/parameter/item-reference.cpp create mode 100644 src/live_effects/parameter/item-reference.h create mode 100644 src/live_effects/parameter/item.cpp create mode 100644 src/live_effects/parameter/item.h create mode 100644 src/live_effects/parameter/message.cpp create mode 100644 src/live_effects/parameter/message.h create mode 100644 src/live_effects/parameter/originalitem.cpp create mode 100644 src/live_effects/parameter/originalitem.h create mode 100644 src/live_effects/parameter/originalitemarray.cpp create mode 100644 src/live_effects/parameter/originalitemarray.h create mode 100644 src/live_effects/parameter/originalpath.cpp create mode 100644 src/live_effects/parameter/originalpath.h create mode 100644 src/live_effects/parameter/originalpatharray.cpp create mode 100644 src/live_effects/parameter/originalpatharray.h create mode 100644 src/live_effects/parameter/parameter.cpp create mode 100644 src/live_effects/parameter/parameter.h create mode 100644 src/live_effects/parameter/path-reference.cpp create mode 100644 src/live_effects/parameter/path-reference.h create mode 100644 src/live_effects/parameter/path.cpp create mode 100644 src/live_effects/parameter/path.h create mode 100644 src/live_effects/parameter/point.cpp create mode 100644 src/live_effects/parameter/point.h create mode 100644 src/live_effects/parameter/powerstrokepointarray.cpp create mode 100644 src/live_effects/parameter/powerstrokepointarray.h create mode 100644 src/live_effects/parameter/random.cpp create mode 100644 src/live_effects/parameter/random.h create mode 100644 src/live_effects/parameter/satellitesarray.cpp create mode 100644 src/live_effects/parameter/satellitesarray.h create mode 100644 src/live_effects/parameter/text.cpp create mode 100644 src/live_effects/parameter/text.h create mode 100644 src/live_effects/parameter/togglebutton.cpp create mode 100644 src/live_effects/parameter/togglebutton.h create mode 100644 src/live_effects/parameter/transformedpoint.cpp create mode 100644 src/live_effects/parameter/transformedpoint.h create mode 100644 src/live_effects/parameter/unit.cpp create mode 100644 src/live_effects/parameter/unit.h create mode 100644 src/live_effects/parameter/vector.cpp create mode 100644 src/live_effects/parameter/vector.h create mode 100644 src/live_effects/spiro-converters.cpp create mode 100644 src/live_effects/spiro-converters.h create mode 100644 src/live_effects/spiro.cpp create mode 100644 src/live_effects/spiro.h create mode 100644 src/live_effects/todo.txt create mode 100644 src/manipulation/README create mode 100644 src/media.cpp create mode 100644 src/media.h create mode 100644 src/menus-skeleton.h create mode 100644 src/message-context.cpp create mode 100644 src/message-context.h create mode 100644 src/message-stack.cpp create mode 100644 src/message-stack.h create mode 100644 src/message.h create mode 100644 src/mod360.cpp create mode 100644 src/mod360.h create mode 100644 src/number-opt-number.h create mode 100644 src/object-hierarchy.cpp create mode 100644 src/object-hierarchy.h create mode 100644 src/object-snapper.cpp create mode 100644 src/object-snapper.h create mode 100644 src/object/CMakeLists.txt create mode 100644 src/object/README create mode 100644 src/object/box3d-side.cpp create mode 100644 src/object/box3d-side.h create mode 100644 src/object/box3d.cpp create mode 100644 src/object/box3d.h create mode 100644 src/object/color-profile.cpp create mode 100644 src/object/color-profile.h create mode 100644 src/object/filters/CMakeLists.txt create mode 100644 src/object/filters/blend.cpp create mode 100644 src/object/filters/blend.h create mode 100644 src/object/filters/colormatrix.cpp create mode 100644 src/object/filters/colormatrix.h create mode 100644 src/object/filters/componenttransfer-funcnode.cpp create mode 100644 src/object/filters/componenttransfer-funcnode.h create mode 100644 src/object/filters/componenttransfer.cpp create mode 100644 src/object/filters/componenttransfer.h create mode 100644 src/object/filters/composite.cpp create mode 100644 src/object/filters/composite.h create mode 100644 src/object/filters/convolvematrix.cpp create mode 100644 src/object/filters/convolvematrix.h create mode 100644 src/object/filters/diffuselighting.cpp create mode 100644 src/object/filters/diffuselighting.h create mode 100644 src/object/filters/displacementmap.cpp create mode 100644 src/object/filters/displacementmap.h create mode 100644 src/object/filters/distantlight.cpp create mode 100644 src/object/filters/distantlight.h create mode 100644 src/object/filters/flood.cpp create mode 100644 src/object/filters/flood.h create mode 100644 src/object/filters/gaussian-blur.cpp create mode 100644 src/object/filters/gaussian-blur.h create mode 100644 src/object/filters/image.cpp create mode 100644 src/object/filters/image.h create mode 100644 src/object/filters/merge.cpp create mode 100644 src/object/filters/merge.h create mode 100644 src/object/filters/mergenode.cpp create mode 100644 src/object/filters/mergenode.h create mode 100644 src/object/filters/morphology.cpp create mode 100644 src/object/filters/morphology.h create mode 100644 src/object/filters/offset.cpp create mode 100644 src/object/filters/offset.h create mode 100644 src/object/filters/pointlight.cpp create mode 100644 src/object/filters/pointlight.h create mode 100644 src/object/filters/sp-filter-primitive.cpp create mode 100644 src/object/filters/sp-filter-primitive.h create mode 100644 src/object/filters/specularlighting.cpp create mode 100644 src/object/filters/specularlighting.h create mode 100644 src/object/filters/spotlight.cpp create mode 100644 src/object/filters/spotlight.h create mode 100644 src/object/filters/tile.cpp create mode 100644 src/object/filters/tile.h create mode 100644 src/object/filters/turbulence.cpp create mode 100644 src/object/filters/turbulence.h create mode 100644 src/object/object-set.cpp create mode 100644 src/object/object-set.h create mode 100644 src/object/persp3d-reference.cpp create mode 100644 src/object/persp3d-reference.h create mode 100644 src/object/persp3d.cpp create mode 100644 src/object/persp3d.h create mode 100644 src/object/sp-anchor.cpp create mode 100644 src/object/sp-anchor.h create mode 100644 src/object/sp-clippath.cpp create mode 100644 src/object/sp-clippath.h create mode 100644 src/object/sp-conn-end-pair.cpp create mode 100644 src/object/sp-conn-end-pair.h create mode 100644 src/object/sp-conn-end.cpp create mode 100644 src/object/sp-conn-end.h create mode 100644 src/object/sp-defs.cpp create mode 100644 src/object/sp-defs.h create mode 100644 src/object/sp-desc.cpp create mode 100644 src/object/sp-desc.h create mode 100644 src/object/sp-dimensions.cpp create mode 100644 src/object/sp-dimensions.h create mode 100644 src/object/sp-ellipse.cpp create mode 100644 src/object/sp-ellipse.h create mode 100644 src/object/sp-factory.cpp create mode 100644 src/object/sp-factory.h create mode 100644 src/object/sp-filter-reference.cpp create mode 100644 src/object/sp-filter-reference.h create mode 100644 src/object/sp-filter-units.h create mode 100644 src/object/sp-filter.cpp create mode 100644 src/object/sp-filter.h create mode 100644 src/object/sp-flowdiv.cpp create mode 100644 src/object/sp-flowdiv.h create mode 100644 src/object/sp-flowregion.cpp create mode 100644 src/object/sp-flowregion.h create mode 100644 src/object/sp-flowtext.cpp create mode 100644 src/object/sp-flowtext.h create mode 100644 src/object/sp-font-face.cpp create mode 100644 src/object/sp-font-face.h create mode 100644 src/object/sp-font.cpp create mode 100644 src/object/sp-font.h create mode 100644 src/object/sp-glyph-kerning.cpp create mode 100644 src/object/sp-glyph-kerning.h create mode 100644 src/object/sp-glyph.cpp create mode 100644 src/object/sp-glyph.h create mode 100644 src/object/sp-gradient-reference.cpp create mode 100644 src/object/sp-gradient-reference.h create mode 100644 src/object/sp-gradient-spread.h create mode 100644 src/object/sp-gradient-units.h create mode 100644 src/object/sp-gradient-vector.h create mode 100644 src/object/sp-gradient.cpp create mode 100644 src/object/sp-gradient.h create mode 100644 src/object/sp-guide.cpp create mode 100644 src/object/sp-guide.h create mode 100644 src/object/sp-hatch-path.cpp create mode 100644 src/object/sp-hatch-path.h create mode 100644 src/object/sp-hatch.cpp create mode 100644 src/object/sp-hatch.h create mode 100644 src/object/sp-image.cpp create mode 100644 src/object/sp-image.h create mode 100644 src/object/sp-item-group.cpp create mode 100644 src/object/sp-item-group.h create mode 100644 src/object/sp-item-rm-unsatisfied-cns.cpp create mode 100644 src/object/sp-item-rm-unsatisfied-cns.h create mode 100644 src/object/sp-item-transform.cpp create mode 100644 src/object/sp-item-transform.h create mode 100644 src/object/sp-item-update-cns.cpp create mode 100644 src/object/sp-item-update-cns.h create mode 100644 src/object/sp-item.cpp create mode 100644 src/object/sp-item.h create mode 100644 src/object/sp-line.cpp create mode 100644 src/object/sp-line.h create mode 100644 src/object/sp-linear-gradient.cpp create mode 100644 src/object/sp-linear-gradient.h create mode 100755 src/object/sp-lpe-item.cpp create mode 100644 src/object/sp-lpe-item.h create mode 100644 src/object/sp-marker-loc.h create mode 100644 src/object/sp-marker.cpp create mode 100644 src/object/sp-marker.h create mode 100644 src/object/sp-mask.cpp create mode 100644 src/object/sp-mask.h create mode 100644 src/object/sp-mesh-array.cpp create mode 100644 src/object/sp-mesh-array.h create mode 100644 src/object/sp-mesh-gradient.cpp create mode 100644 src/object/sp-mesh-gradient.h create mode 100644 src/object/sp-mesh-patch.cpp create mode 100644 src/object/sp-mesh-patch.h create mode 100644 src/object/sp-mesh-row.cpp create mode 100644 src/object/sp-mesh-row.h create mode 100644 src/object/sp-metadata.cpp create mode 100644 src/object/sp-metadata.h create mode 100644 src/object/sp-missing-glyph.cpp create mode 100644 src/object/sp-missing-glyph.h create mode 100644 src/object/sp-namedview.cpp create mode 100644 src/object/sp-namedview.h create mode 100644 src/object/sp-object-group.cpp create mode 100644 src/object/sp-object-group.h create mode 100644 src/object/sp-object.cpp create mode 100644 src/object/sp-object.h create mode 100644 src/object/sp-offset.cpp create mode 100644 src/object/sp-offset.h create mode 100644 src/object/sp-paint-server-reference.h create mode 100644 src/object/sp-paint-server.cpp create mode 100644 src/object/sp-paint-server.h create mode 100644 src/object/sp-path.cpp create mode 100644 src/object/sp-path.h create mode 100644 src/object/sp-pattern.cpp create mode 100644 src/object/sp-pattern.h create mode 100644 src/object/sp-polygon.cpp create mode 100644 src/object/sp-polygon.h create mode 100644 src/object/sp-polyline.cpp create mode 100644 src/object/sp-polyline.h create mode 100644 src/object/sp-radial-gradient.cpp create mode 100644 src/object/sp-radial-gradient.h create mode 100644 src/object/sp-rect.cpp create mode 100644 src/object/sp-rect.h create mode 100644 src/object/sp-root.cpp create mode 100644 src/object/sp-root.h create mode 100644 src/object/sp-script.cpp create mode 100644 src/object/sp-script.h create mode 100644 src/object/sp-shape-reference.cpp create mode 100644 src/object/sp-shape-reference.h create mode 100644 src/object/sp-shape.cpp create mode 100644 src/object/sp-shape.h create mode 100644 src/object/sp-solid-color.cpp create mode 100644 src/object/sp-solid-color.h create mode 100644 src/object/sp-spiral.cpp create mode 100644 src/object/sp-spiral.h create mode 100644 src/object/sp-star.cpp create mode 100644 src/object/sp-star.h create mode 100644 src/object/sp-stop.cpp create mode 100644 src/object/sp-stop.h create mode 100644 src/object/sp-string.cpp create mode 100644 src/object/sp-string.h create mode 100644 src/object/sp-style-elem.cpp create mode 100644 src/object/sp-style-elem.h create mode 100644 src/object/sp-switch.cpp create mode 100644 src/object/sp-switch.h create mode 100644 src/object/sp-symbol.cpp create mode 100644 src/object/sp-symbol.h create mode 100644 src/object/sp-tag-use-reference.cpp create mode 100644 src/object/sp-tag-use-reference.h create mode 100644 src/object/sp-tag-use.cpp create mode 100644 src/object/sp-tag-use.h create mode 100644 src/object/sp-tag.cpp create mode 100644 src/object/sp-tag.h create mode 100644 src/object/sp-text.cpp create mode 100644 src/object/sp-text.h create mode 100644 src/object/sp-textpath.h create mode 100644 src/object/sp-title.cpp create mode 100644 src/object/sp-title.h create mode 100644 src/object/sp-tref-reference.cpp create mode 100644 src/object/sp-tref-reference.h create mode 100644 src/object/sp-tref.cpp create mode 100644 src/object/sp-tref.h create mode 100644 src/object/sp-tspan.cpp create mode 100644 src/object/sp-tspan.h create mode 100644 src/object/sp-use-reference.cpp create mode 100644 src/object/sp-use-reference.h create mode 100644 src/object/sp-use.cpp create mode 100644 src/object/sp-use.h create mode 100644 src/object/uri-references.cpp create mode 100644 src/object/uri-references.h create mode 100644 src/object/uri.cpp create mode 100644 src/object/uri.h create mode 100644 src/object/viewbox.cpp create mode 100644 src/object/viewbox.h create mode 100644 src/path-chemistry.cpp create mode 100644 src/path-chemistry.h create mode 100644 src/path-prefix.cpp create mode 100644 src/path-prefix.h create mode 100644 src/perspective-line.cpp create mode 100644 src/perspective-line.h create mode 100644 src/plugin.def create mode 100644 src/preferences-skeleton.h create mode 100644 src/preferences.cpp create mode 100644 src/preferences.h create mode 100644 src/prefix.cpp create mode 100644 src/prefix.h create mode 100644 src/print.cpp create mode 100644 src/print.h create mode 100644 src/profile-manager.cpp create mode 100644 src/profile-manager.h create mode 100644 src/proj_pt.cpp create mode 100644 src/proj_pt.h create mode 100644 src/proofs create mode 100644 src/pure-transform.cpp create mode 100644 src/pure-transform.h create mode 100644 src/rdf.cpp create mode 100644 src/rdf.h create mode 100644 src/remove-last.h create mode 100644 src/removeoverlap.cpp create mode 100644 src/removeoverlap.h create mode 100644 src/rubberband.cpp create mode 100644 src/rubberband.h create mode 100644 src/satisfied-guide-cns.cpp create mode 100644 src/satisfied-guide-cns.h create mode 100644 src/selcue.cpp create mode 100644 src/selcue.h create mode 100644 src/selection-chemistry.cpp create mode 100644 src/selection-chemistry.h create mode 100644 src/selection-describer.cpp create mode 100644 src/selection-describer.h create mode 100644 src/selection.cpp create mode 100644 src/selection.h create mode 100644 src/seltrans-handles.cpp create mode 100644 src/seltrans-handles.h create mode 100644 src/seltrans.cpp create mode 100644 src/seltrans.h create mode 100644 src/shortcuts.cpp create mode 100644 src/shortcuts.h create mode 100644 src/show-preview.bmp create mode 100644 src/snap-candidate.h create mode 100644 src/snap-enums.h create mode 100644 src/snap-preferences.cpp create mode 100644 src/snap-preferences.h create mode 100644 src/snap.cpp create mode 100644 src/snap.h create mode 100644 src/snapped-curve.cpp create mode 100644 src/snapped-curve.h create mode 100644 src/snapped-line.cpp create mode 100644 src/snapped-line.h create mode 100644 src/snapped-point.cpp create mode 100644 src/snapped-point.h create mode 100644 src/snapper.cpp create mode 100644 src/snapper.h create mode 100644 src/sp-cursor.cpp create mode 100644 src/sp-cursor.h create mode 100644 src/sp-guide-attachment.h create mode 100644 src/sp-guide-constraint.h create mode 100644 src/sp-item-notify-moveto.cpp create mode 100644 src/sp-item-notify-moveto.h create mode 100644 src/splivarot.cpp create mode 100644 src/splivarot.h create mode 100644 src/streq.h create mode 100644 src/strneq.h create mode 100644 src/style-enums.h create mode 100644 src/style-internal.cpp create mode 100644 src/style-internal.h create mode 100644 src/style.cpp create mode 100644 src/style.h create mode 100644 src/svg/CMakeLists.txt create mode 100644 src/svg/HACKING create mode 100644 src/svg/README create mode 100644 src/svg/css-ostringstream.cpp create mode 100644 src/svg/css-ostringstream.h create mode 100644 src/svg/path-string.cpp create mode 100644 src/svg/path-string.h create mode 100644 src/svg/sp-svg.def create mode 100644 src/svg/stringstream.cpp create mode 100644 src/svg/stringstream.h create mode 100644 src/svg/strip-trailing-zeros.cpp create mode 100644 src/svg/strip-trailing-zeros.h create mode 100644 src/svg/svg-affine-test.h create mode 100644 src/svg/svg-affine.cpp create mode 100644 src/svg/svg-angle.cpp create mode 100644 src/svg/svg-angle.h create mode 100644 src/svg/svg-color-test.h create mode 100644 src/svg/svg-color.cpp create mode 100644 src/svg/svg-color.h create mode 100644 src/svg/svg-icc-color.h create mode 100644 src/svg/svg-length-test.h create mode 100644 src/svg/svg-length.cpp create mode 100644 src/svg/svg-length.h create mode 100644 src/svg/svg-path-geom-test.h create mode 100644 src/svg/svg-path.cpp create mode 100644 src/svg/svg.h create mode 100644 src/svg/test-stubs.cpp create mode 100644 src/svg/test-stubs.h create mode 100644 src/syseq.h create mode 100644 src/text-chemistry-impl.h create mode 100644 src/text-chemistry.cpp create mode 100644 src/text-chemistry.h create mode 100644 src/text-editing.cpp create mode 100644 src/text-editing.h create mode 100644 src/text-tag-attributes.h create mode 100644 src/trace/CMakeLists.txt create mode 100644 src/trace/README create mode 100644 src/trace/autotrace/inkscape-autotrace.cpp create mode 100644 src/trace/autotrace/inkscape-autotrace.h create mode 100644 src/trace/depixelize/inkscape-depixelize.cpp create mode 100644 src/trace/depixelize/inkscape-depixelize.h create mode 100644 src/trace/filterset.cpp create mode 100644 src/trace/filterset.h create mode 100644 src/trace/imagemap-gdk.cpp create mode 100644 src/trace/imagemap-gdk.h create mode 100644 src/trace/imagemap.cpp create mode 100644 src/trace/imagemap.h create mode 100644 src/trace/pool.h create mode 100644 src/trace/potrace/bitmap.h create mode 100644 src/trace/potrace/inkscape-potrace.cpp create mode 100644 src/trace/potrace/inkscape-potrace.h create mode 100644 src/trace/quantize.cpp create mode 100644 src/trace/quantize.h create mode 100644 src/trace/siox.cpp create mode 100644 src/trace/siox.h create mode 100644 src/trace/trace.cpp create mode 100644 src/trace/trace.h create mode 100644 src/transf_mat_3x4.cpp create mode 100644 src/transf_mat_3x4.h create mode 100644 src/ui/CMakeLists.txt create mode 100644 src/ui/README create mode 100644 src/ui/cache/README create mode 100644 src/ui/cache/svg_preview_cache.cpp create mode 100644 src/ui/cache/svg_preview_cache.h create mode 100644 src/ui/clipboard.cpp create mode 100644 src/ui/clipboard.h create mode 100644 src/ui/contextmenu.cpp create mode 100644 src/ui/contextmenu.h create mode 100644 src/ui/control-manager.cpp create mode 100644 src/ui/control-manager.h create mode 100644 src/ui/control-types.h create mode 100644 src/ui/desktop/README create mode 100644 src/ui/desktop/menubar.cpp create mode 100644 src/ui/desktop/menubar.h create mode 100644 src/ui/dialog-events.cpp create mode 100644 src/ui/dialog-events.h create mode 100644 src/ui/dialog/aboutbox.cpp create mode 100644 src/ui/dialog/aboutbox.h create mode 100644 src/ui/dialog/align-and-distribute.cpp create mode 100644 src/ui/dialog/align-and-distribute.h create mode 100644 src/ui/dialog/arrange-tab.h create mode 100644 src/ui/dialog/attrdialog.cpp create mode 100644 src/ui/dialog/attrdialog.h create mode 100644 src/ui/dialog/behavior.h create mode 100644 src/ui/dialog/calligraphic-profile-rename.cpp create mode 100644 src/ui/dialog/calligraphic-profile-rename.h create mode 100644 src/ui/dialog/clonetiler.cpp create mode 100644 src/ui/dialog/clonetiler.h create mode 100644 src/ui/dialog/color-item.cpp create mode 100644 src/ui/dialog/color-item.h create mode 100644 src/ui/dialog/debug.cpp create mode 100644 src/ui/dialog/debug.h create mode 100644 src/ui/dialog/desktop-tracker.cpp create mode 100644 src/ui/dialog/desktop-tracker.h create mode 100644 src/ui/dialog/dialog-manager.cpp create mode 100644 src/ui/dialog/dialog-manager.h create mode 100644 src/ui/dialog/dialog.cpp create mode 100644 src/ui/dialog/dialog.h create mode 100644 src/ui/dialog/dock-behavior.cpp create mode 100644 src/ui/dialog/dock-behavior.h create mode 100644 src/ui/dialog/document-metadata.cpp create mode 100644 src/ui/dialog/document-metadata.h create mode 100644 src/ui/dialog/document-properties.cpp create mode 100644 src/ui/dialog/document-properties.h create mode 100644 src/ui/dialog/export.cpp create mode 100644 src/ui/dialog/export.h create mode 100644 src/ui/dialog/extension-editor.cpp create mode 100644 src/ui/dialog/extension-editor.h create mode 100644 src/ui/dialog/extensions.cpp create mode 100644 src/ui/dialog/extensions.h create mode 100644 src/ui/dialog/filedialog.cpp create mode 100644 src/ui/dialog/filedialog.h create mode 100644 src/ui/dialog/filedialogimpl-gtkmm.cpp create mode 100644 src/ui/dialog/filedialogimpl-gtkmm.h create mode 100644 src/ui/dialog/filedialogimpl-win32.cpp create mode 100644 src/ui/dialog/filedialogimpl-win32.h create mode 100644 src/ui/dialog/fill-and-stroke.cpp create mode 100644 src/ui/dialog/fill-and-stroke.h create mode 100644 src/ui/dialog/filter-editor.cpp create mode 100644 src/ui/dialog/filter-editor.h create mode 100644 src/ui/dialog/filter-effects-dialog.cpp create mode 100644 src/ui/dialog/filter-effects-dialog.h create mode 100644 src/ui/dialog/find.cpp create mode 100644 src/ui/dialog/find.h create mode 100644 src/ui/dialog/floating-behavior.cpp create mode 100644 src/ui/dialog/floating-behavior.h create mode 100644 src/ui/dialog/font-substitution.cpp create mode 100644 src/ui/dialog/font-substitution.h create mode 100644 src/ui/dialog/glyphs.cpp create mode 100644 src/ui/dialog/glyphs.h create mode 100644 src/ui/dialog/grid-arrange-tab.cpp create mode 100644 src/ui/dialog/grid-arrange-tab.h create mode 100644 src/ui/dialog/guides.cpp create mode 100644 src/ui/dialog/guides.h create mode 100644 src/ui/dialog/icon-preview.cpp create mode 100644 src/ui/dialog/icon-preview.h create mode 100644 src/ui/dialog/inkscape-preferences.cpp create mode 100644 src/ui/dialog/inkscape-preferences.h create mode 100644 src/ui/dialog/input.cpp create mode 100644 src/ui/dialog/input.h create mode 100644 src/ui/dialog/knot-properties.cpp create mode 100644 src/ui/dialog/knot-properties.h create mode 100644 src/ui/dialog/layer-properties.cpp create mode 100644 src/ui/dialog/layer-properties.h create mode 100644 src/ui/dialog/layers.cpp create mode 100644 src/ui/dialog/layers.h create mode 100644 src/ui/dialog/livepatheffect-add.cpp create mode 100644 src/ui/dialog/livepatheffect-add.h create mode 100644 src/ui/dialog/livepatheffect-editor.cpp create mode 100644 src/ui/dialog/livepatheffect-editor.h create mode 100644 src/ui/dialog/lpe-fillet-chamfer-properties.cpp create mode 100644 src/ui/dialog/lpe-fillet-chamfer-properties.h create mode 100644 src/ui/dialog/lpe-powerstroke-properties.cpp create mode 100644 src/ui/dialog/lpe-powerstroke-properties.h create mode 100644 src/ui/dialog/memory.cpp create mode 100644 src/ui/dialog/memory.h create mode 100644 src/ui/dialog/messages.cpp create mode 100644 src/ui/dialog/messages.h create mode 100644 src/ui/dialog/new-from-template.cpp create mode 100644 src/ui/dialog/new-from-template.h create mode 100644 src/ui/dialog/object-attributes.cpp create mode 100644 src/ui/dialog/object-attributes.h create mode 100644 src/ui/dialog/object-properties.cpp create mode 100644 src/ui/dialog/object-properties.h create mode 100644 src/ui/dialog/objects.cpp create mode 100644 src/ui/dialog/objects.h create mode 100644 src/ui/dialog/paint-servers.cpp create mode 100644 src/ui/dialog/paint-servers.h create mode 100644 src/ui/dialog/panel-dialog.h create mode 100644 src/ui/dialog/polar-arrange-tab.cpp create mode 100644 src/ui/dialog/polar-arrange-tab.h create mode 100644 src/ui/dialog/print-colors-preview-dialog.cpp create mode 100644 src/ui/dialog/print-colors-preview-dialog.h create mode 100644 src/ui/dialog/print.cpp create mode 100644 src/ui/dialog/print.h create mode 100644 src/ui/dialog/save-template-dialog.cpp create mode 100644 src/ui/dialog/save-template-dialog.h create mode 100644 src/ui/dialog/selectorsdialog.cpp create mode 100644 src/ui/dialog/selectorsdialog.h create mode 100644 src/ui/dialog/spellcheck.cpp create mode 100644 src/ui/dialog/spellcheck.h create mode 100644 src/ui/dialog/styledialog.cpp create mode 100644 src/ui/dialog/styledialog.h create mode 100644 src/ui/dialog/svg-fonts-dialog.cpp create mode 100644 src/ui/dialog/svg-fonts-dialog.h create mode 100644 src/ui/dialog/svg-preview.cpp create mode 100644 src/ui/dialog/svg-preview.h create mode 100644 src/ui/dialog/swatches.cpp create mode 100644 src/ui/dialog/swatches.h create mode 100644 src/ui/dialog/symbols.cpp create mode 100644 src/ui/dialog/symbols.h create mode 100644 src/ui/dialog/tags.cpp create mode 100644 src/ui/dialog/tags.h create mode 100644 src/ui/dialog/template-load-tab.cpp create mode 100644 src/ui/dialog/template-load-tab.h create mode 100644 src/ui/dialog/template-widget.cpp create mode 100644 src/ui/dialog/template-widget.h create mode 100644 src/ui/dialog/text-edit.cpp create mode 100644 src/ui/dialog/text-edit.h create mode 100644 src/ui/dialog/tile.cpp create mode 100644 src/ui/dialog/tile.h create mode 100644 src/ui/dialog/tracedialog.cpp create mode 100644 src/ui/dialog/tracedialog.h create mode 100644 src/ui/dialog/transformation.cpp create mode 100644 src/ui/dialog/transformation.h create mode 100644 src/ui/dialog/undo-history.cpp create mode 100644 src/ui/dialog/undo-history.h create mode 100644 src/ui/dialog/xml-tree.cpp create mode 100644 src/ui/dialog/xml-tree.h create mode 100644 src/ui/drag-and-drop.cpp create mode 100644 src/ui/drag-and-drop.h create mode 100644 src/ui/draw-anchor.cpp create mode 100644 src/ui/draw-anchor.h create mode 100644 src/ui/event-debug.h create mode 100644 src/ui/icon-loader.cpp create mode 100644 src/ui/icon-loader.h create mode 100644 src/ui/icon-names.h create mode 100644 src/ui/interface.cpp create mode 100644 src/ui/interface.h create mode 100644 src/ui/monitor.cpp create mode 100644 src/ui/monitor.h create mode 100644 src/ui/pixmaps/README create mode 100644 src/ui/pixmaps/cursor-3dbox.xpm create mode 100644 src/ui/pixmaps/cursor-adj-a.xpm create mode 100644 src/ui/pixmaps/cursor-adj-h.xpm create mode 100644 src/ui/pixmaps/cursor-adj-l.xpm create mode 100644 src/ui/pixmaps/cursor-adj-s.xpm create mode 100644 src/ui/pixmaps/cursor-calligraphy.xpm create mode 100644 src/ui/pixmaps/cursor-connector.xpm create mode 100644 src/ui/pixmaps/cursor-crosshairs.xpm create mode 100644 src/ui/pixmaps/cursor-dropper-f.xpm create mode 100644 src/ui/pixmaps/cursor-dropper-s.xpm create mode 100644 src/ui/pixmaps/cursor-dropping-f.xpm create mode 100644 src/ui/pixmaps/cursor-dropping-s.xpm create mode 100644 src/ui/pixmaps/cursor-ellipse.xpm create mode 100644 src/ui/pixmaps/cursor-eraser.xpm create mode 100644 src/ui/pixmaps/cursor-gradient-add.xpm create mode 100644 src/ui/pixmaps/cursor-gradient.xpm create mode 100644 src/ui/pixmaps/cursor-measure.xpm create mode 100644 src/ui/pixmaps/cursor-node-d.xpm create mode 100644 src/ui/pixmaps/cursor-node.xpm create mode 100644 src/ui/pixmaps/cursor-paintbucket.xpm create mode 100644 src/ui/pixmaps/cursor-pen.xpm create mode 100644 src/ui/pixmaps/cursor-pencil.xpm create mode 100644 src/ui/pixmaps/cursor-rect.xpm create mode 100644 src/ui/pixmaps/cursor-select-d.xpm create mode 100644 src/ui/pixmaps/cursor-select-m.xpm create mode 100644 src/ui/pixmaps/cursor-select.xpm create mode 100644 src/ui/pixmaps/cursor-spiral.xpm create mode 100644 src/ui/pixmaps/cursor-spray-move.xpm create mode 100644 src/ui/pixmaps/cursor-spray.xpm create mode 100644 src/ui/pixmaps/cursor-star.xpm create mode 100644 src/ui/pixmaps/cursor-text-insert.xpm create mode 100644 src/ui/pixmaps/cursor-text.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-attract.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-color.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-less.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-more.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-in.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-jitter.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move-out.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-move.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-push.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-repel.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-roughen.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-scale-down.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-scale-up.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-thicken.xpm create mode 100644 src/ui/pixmaps/cursor-tweak-thin.xpm create mode 100644 src/ui/pixmaps/cursor-zoom-out.xpm create mode 100644 src/ui/pixmaps/cursor-zoom.xpm create mode 100644 src/ui/pixmaps/handles.xpm create mode 100644 src/ui/pref-pusher.cpp create mode 100644 src/ui/pref-pusher.h create mode 100644 src/ui/previewable.h create mode 100644 src/ui/previewholder.cpp create mode 100644 src/ui/previewholder.h create mode 100644 src/ui/selected-color.cpp create mode 100644 src/ui/selected-color.h create mode 100644 src/ui/shape-editor-knotholders.cpp create mode 100644 src/ui/shape-editor.cpp create mode 100644 src/ui/shape-editor.h create mode 100644 src/ui/simple-pref-pusher.cpp create mode 100644 src/ui/simple-pref-pusher.h create mode 100644 src/ui/tool-factory.cpp create mode 100644 src/ui/tool-factory.h create mode 100644 src/ui/tool/commit-events.h create mode 100644 src/ui/tool/control-point-selection.cpp create mode 100644 src/ui/tool/control-point-selection.h create mode 100644 src/ui/tool/control-point.cpp create mode 100644 src/ui/tool/control-point.h create mode 100644 src/ui/tool/curve-drag-point.cpp create mode 100644 src/ui/tool/curve-drag-point.h create mode 100644 src/ui/tool/event-utils.cpp create mode 100644 src/ui/tool/event-utils.h create mode 100644 src/ui/tool/manipulator.cpp create mode 100644 src/ui/tool/manipulator.h create mode 100644 src/ui/tool/modifier-tracker.cpp create mode 100644 src/ui/tool/modifier-tracker.h create mode 100644 src/ui/tool/multi-path-manipulator.cpp create mode 100644 src/ui/tool/multi-path-manipulator.h create mode 100644 src/ui/tool/node-types.h create mode 100644 src/ui/tool/node.cpp create mode 100644 src/ui/tool/node.h create mode 100644 src/ui/tool/path-manipulator.cpp create mode 100644 src/ui/tool/path-manipulator.h create mode 100644 src/ui/tool/selectable-control-point.cpp create mode 100644 src/ui/tool/selectable-control-point.h create mode 100644 src/ui/tool/selector.cpp create mode 100644 src/ui/tool/selector.h create mode 100644 src/ui/tool/shape-record.h create mode 100644 src/ui/tool/transform-handle-set.cpp create mode 100644 src/ui/tool/transform-handle-set.h create mode 100644 src/ui/toolbar/arc-toolbar.cpp create mode 100644 src/ui/toolbar/arc-toolbar.h create mode 100644 src/ui/toolbar/box3d-toolbar.cpp create mode 100644 src/ui/toolbar/box3d-toolbar.h create mode 100644 src/ui/toolbar/calligraphy-toolbar.cpp create mode 100644 src/ui/toolbar/calligraphy-toolbar.h create mode 100644 src/ui/toolbar/connector-toolbar.cpp create mode 100644 src/ui/toolbar/connector-toolbar.h create mode 100644 src/ui/toolbar/dropper-toolbar.cpp create mode 100644 src/ui/toolbar/dropper-toolbar.h create mode 100644 src/ui/toolbar/eraser-toolbar.cpp create mode 100644 src/ui/toolbar/eraser-toolbar.h create mode 100644 src/ui/toolbar/gradient-toolbar.cpp create mode 100644 src/ui/toolbar/gradient-toolbar.h create mode 100644 src/ui/toolbar/lpe-toolbar.cpp create mode 100644 src/ui/toolbar/lpe-toolbar.h create mode 100644 src/ui/toolbar/measure-toolbar.cpp create mode 100644 src/ui/toolbar/measure-toolbar.h create mode 100644 src/ui/toolbar/mesh-toolbar.cpp create mode 100644 src/ui/toolbar/mesh-toolbar.h create mode 100644 src/ui/toolbar/node-toolbar.cpp create mode 100644 src/ui/toolbar/node-toolbar.h create mode 100644 src/ui/toolbar/paintbucket-toolbar.cpp create mode 100644 src/ui/toolbar/paintbucket-toolbar.h create mode 100644 src/ui/toolbar/pencil-toolbar.cpp create mode 100644 src/ui/toolbar/pencil-toolbar.h create mode 100644 src/ui/toolbar/rect-toolbar.cpp create mode 100644 src/ui/toolbar/rect-toolbar.h create mode 100644 src/ui/toolbar/select-toolbar.cpp create mode 100644 src/ui/toolbar/select-toolbar.h create mode 100644 src/ui/toolbar/snap-toolbar.cpp create mode 100644 src/ui/toolbar/snap-toolbar.h create mode 100644 src/ui/toolbar/spiral-toolbar.cpp create mode 100644 src/ui/toolbar/spiral-toolbar.h create mode 100644 src/ui/toolbar/spray-toolbar.cpp create mode 100644 src/ui/toolbar/spray-toolbar.h create mode 100644 src/ui/toolbar/star-toolbar.cpp create mode 100644 src/ui/toolbar/star-toolbar.h create mode 100644 src/ui/toolbar/text-toolbar.cpp create mode 100644 src/ui/toolbar/text-toolbar.h create mode 100644 src/ui/toolbar/toolbar.cpp create mode 100644 src/ui/toolbar/toolbar.h create mode 100644 src/ui/toolbar/tweak-toolbar.cpp create mode 100644 src/ui/toolbar/tweak-toolbar.h create mode 100644 src/ui/toolbar/zoom-toolbar.cpp create mode 100644 src/ui/toolbar/zoom-toolbar.h create mode 100644 src/ui/tools-switch.cpp create mode 100644 src/ui/tools-switch.h create mode 100644 src/ui/tools/arc-tool.cpp create mode 100644 src/ui/tools/arc-tool.h create mode 100644 src/ui/tools/box3d-tool.cpp create mode 100644 src/ui/tools/box3d-tool.h create mode 100644 src/ui/tools/calligraphic-tool.cpp create mode 100644 src/ui/tools/calligraphic-tool.h create mode 100644 src/ui/tools/connector-tool.cpp create mode 100644 src/ui/tools/connector-tool.h create mode 100644 src/ui/tools/dropper-tool.cpp create mode 100644 src/ui/tools/dropper-tool.h create mode 100644 src/ui/tools/dynamic-base.cpp create mode 100644 src/ui/tools/dynamic-base.h create mode 100644 src/ui/tools/eraser-tool.cpp create mode 100644 src/ui/tools/eraser-tool.h create mode 100644 src/ui/tools/flood-tool.cpp create mode 100644 src/ui/tools/flood-tool.h create mode 100644 src/ui/tools/freehand-base.cpp create mode 100644 src/ui/tools/freehand-base.h create mode 100644 src/ui/tools/gradient-tool.cpp create mode 100644 src/ui/tools/gradient-tool.h create mode 100644 src/ui/tools/lpe-tool.cpp create mode 100644 src/ui/tools/lpe-tool.h create mode 100644 src/ui/tools/measure-tool.cpp create mode 100644 src/ui/tools/measure-tool.h create mode 100644 src/ui/tools/mesh-tool.cpp create mode 100644 src/ui/tools/mesh-tool.h create mode 100644 src/ui/tools/node-tool.cpp create mode 100644 src/ui/tools/node-tool.h create mode 100644 src/ui/tools/pen-tool.cpp create mode 100644 src/ui/tools/pen-tool.h create mode 100644 src/ui/tools/pencil-tool.cpp create mode 100644 src/ui/tools/pencil-tool.h create mode 100644 src/ui/tools/rect-tool.cpp create mode 100644 src/ui/tools/rect-tool.h create mode 100644 src/ui/tools/select-tool.cpp create mode 100644 src/ui/tools/select-tool.h create mode 100644 src/ui/tools/spiral-tool.cpp create mode 100644 src/ui/tools/spiral-tool.h create mode 100644 src/ui/tools/spray-tool.cpp create mode 100644 src/ui/tools/spray-tool.h create mode 100644 src/ui/tools/star-tool.cpp create mode 100644 src/ui/tools/star-tool.h create mode 100644 src/ui/tools/text-tool.cpp create mode 100644 src/ui/tools/text-tool.h create mode 100644 src/ui/tools/tool-base.cpp create mode 100644 src/ui/tools/tool-base.h create mode 100644 src/ui/tools/tweak-tool.cpp create mode 100644 src/ui/tools/tweak-tool.h create mode 100644 src/ui/tools/zoom-tool.cpp create mode 100644 src/ui/tools/zoom-tool.h create mode 100644 src/ui/util.cpp create mode 100644 src/ui/util.h create mode 100644 src/ui/uxmanager.cpp create mode 100644 src/ui/uxmanager.h create mode 100644 src/ui/view/README create mode 100644 src/ui/view/edit-widget-interface.h create mode 100644 src/ui/view/svg-view-widget.cpp create mode 100644 src/ui/view/svg-view-widget.h create mode 100644 src/ui/view/view-widget.cpp create mode 100644 src/ui/view/view-widget.h create mode 100644 src/ui/view/view.cpp create mode 100644 src/ui/view/view.h create mode 100644 src/ui/widget/alignment-selector.cpp create mode 100644 src/ui/widget/alignment-selector.h create mode 100644 src/ui/widget/anchor-selector.cpp create mode 100644 src/ui/widget/anchor-selector.h create mode 100644 src/ui/widget/attr-widget.h create mode 100644 src/ui/widget/button.cpp create mode 100644 src/ui/widget/button.h create mode 100644 src/ui/widget/clipmaskicon.cpp create mode 100644 src/ui/widget/clipmaskicon.h create mode 100644 src/ui/widget/color-entry.cpp create mode 100644 src/ui/widget/color-entry.h create mode 100644 src/ui/widget/color-icc-selector.cpp create mode 100644 src/ui/widget/color-icc-selector.h create mode 100644 src/ui/widget/color-notebook.cpp create mode 100644 src/ui/widget/color-notebook.h create mode 100644 src/ui/widget/color-picker.cpp create mode 100644 src/ui/widget/color-picker.h create mode 100644 src/ui/widget/color-preview.cpp create mode 100644 src/ui/widget/color-preview.h create mode 100644 src/ui/widget/color-scales.cpp create mode 100644 src/ui/widget/color-scales.h create mode 100644 src/ui/widget/color-slider.cpp create mode 100644 src/ui/widget/color-slider.h create mode 100644 src/ui/widget/color-wheel-selector.cpp create mode 100644 src/ui/widget/color-wheel-selector.h create mode 100644 src/ui/widget/combo-box-entry-tool-item.cpp create mode 100644 src/ui/widget/combo-box-entry-tool-item.h create mode 100644 src/ui/widget/combo-enums.h create mode 100644 src/ui/widget/combo-tool-item.cpp create mode 100644 src/ui/widget/combo-tool-item.h create mode 100644 src/ui/widget/dash-selector.cpp create mode 100644 src/ui/widget/dash-selector.h create mode 100644 src/ui/widget/dock-item.cpp create mode 100644 src/ui/widget/dock-item.h create mode 100644 src/ui/widget/dock.cpp create mode 100644 src/ui/widget/dock.h create mode 100644 src/ui/widget/entity-entry.cpp create mode 100644 src/ui/widget/entity-entry.h create mode 100644 src/ui/widget/entry.cpp create mode 100644 src/ui/widget/entry.h create mode 100644 src/ui/widget/filter-effect-chooser.cpp create mode 100644 src/ui/widget/filter-effect-chooser.h create mode 100644 src/ui/widget/font-button.cpp create mode 100644 src/ui/widget/font-button.h create mode 100644 src/ui/widget/font-selector-toolbar.cpp create mode 100644 src/ui/widget/font-selector-toolbar.h create mode 100644 src/ui/widget/font-selector.cpp create mode 100644 src/ui/widget/font-selector.h create mode 100644 src/ui/widget/font-variants.cpp create mode 100644 src/ui/widget/font-variants.h create mode 100644 src/ui/widget/font-variations.cpp create mode 100644 src/ui/widget/font-variations.h create mode 100644 src/ui/widget/frame.cpp create mode 100644 src/ui/widget/frame.h create mode 100644 src/ui/widget/highlight-picker.cpp create mode 100644 src/ui/widget/highlight-picker.h create mode 100644 src/ui/widget/iconrenderer.cpp create mode 100644 src/ui/widget/iconrenderer.h create mode 100644 src/ui/widget/imagetoggler.cpp create mode 100644 src/ui/widget/imagetoggler.h create mode 100644 src/ui/widget/ink-color-wheel.cpp create mode 100644 src/ui/widget/ink-color-wheel.h create mode 100644 src/ui/widget/ink-flow-box.cpp create mode 100644 src/ui/widget/ink-flow-box.h create mode 100644 src/ui/widget/ink-ruler.cpp create mode 100644 src/ui/widget/ink-ruler.h create mode 100644 src/ui/widget/ink-spinscale.cpp create mode 100644 src/ui/widget/ink-spinscale.h create mode 100644 src/ui/widget/insertordericon.cpp create mode 100644 src/ui/widget/insertordericon.h create mode 100644 src/ui/widget/label-tool-item.cpp create mode 100644 src/ui/widget/label-tool-item.h create mode 100644 src/ui/widget/labelled.cpp create mode 100644 src/ui/widget/labelled.h create mode 100644 src/ui/widget/layer-selector.cpp create mode 100644 src/ui/widget/layer-selector.h create mode 100644 src/ui/widget/layertypeicon.cpp create mode 100644 src/ui/widget/layertypeicon.h create mode 100644 src/ui/widget/licensor.cpp create mode 100644 src/ui/widget/licensor.h create mode 100644 src/ui/widget/notebook-page.cpp create mode 100644 src/ui/widget/notebook-page.h create mode 100644 src/ui/widget/object-composite-settings.cpp create mode 100644 src/ui/widget/object-composite-settings.h create mode 100644 src/ui/widget/page-sizer.cpp create mode 100644 src/ui/widget/page-sizer.h create mode 100644 src/ui/widget/pages-skeleton.h create mode 100644 src/ui/widget/panel.cpp create mode 100644 src/ui/widget/panel.h create mode 100644 src/ui/widget/point.cpp create mode 100644 src/ui/widget/point.h create mode 100644 src/ui/widget/preferences-widget.cpp create mode 100644 src/ui/widget/preferences-widget.h create mode 100644 src/ui/widget/preview.cpp create mode 100644 src/ui/widget/preview.h create mode 100644 src/ui/widget/random.cpp create mode 100644 src/ui/widget/random.h create mode 100644 src/ui/widget/registered-enums.h create mode 100644 src/ui/widget/registered-widget.cpp create mode 100644 src/ui/widget/registered-widget.h create mode 100644 src/ui/widget/registry.cpp create mode 100644 src/ui/widget/registry.h create mode 100644 src/ui/widget/rendering-options.cpp create mode 100644 src/ui/widget/rendering-options.h create mode 100644 src/ui/widget/rotateable.cpp create mode 100644 src/ui/widget/rotateable.h create mode 100644 src/ui/widget/scalar-unit.cpp create mode 100644 src/ui/widget/scalar-unit.h create mode 100644 src/ui/widget/scalar.cpp create mode 100644 src/ui/widget/scalar.h create mode 100644 src/ui/widget/selected-style.cpp create mode 100644 src/ui/widget/selected-style.h create mode 100644 src/ui/widget/spin-button-tool-item.cpp create mode 100644 src/ui/widget/spin-button-tool-item.h create mode 100644 src/ui/widget/spin-scale.cpp create mode 100644 src/ui/widget/spin-scale.h create mode 100644 src/ui/widget/spin-slider.cpp create mode 100644 src/ui/widget/spin-slider.h create mode 100644 src/ui/widget/spinbutton.cpp create mode 100644 src/ui/widget/spinbutton.h create mode 100644 src/ui/widget/style-subject.cpp create mode 100644 src/ui/widget/style-subject.h create mode 100644 src/ui/widget/style-swatch.cpp create mode 100644 src/ui/widget/style-swatch.h create mode 100644 src/ui/widget/text.cpp create mode 100644 src/ui/widget/text.h create mode 100644 src/ui/widget/tolerance-slider.cpp create mode 100644 src/ui/widget/tolerance-slider.h create mode 100644 src/ui/widget/unit-menu.cpp create mode 100644 src/ui/widget/unit-menu.h create mode 100644 src/ui/widget/unit-tracker.cpp create mode 100644 src/ui/widget/unit-tracker.h create mode 100644 src/unclump.cpp create mode 100644 src/unclump.h create mode 100644 src/undo-stack-observer.h create mode 100644 src/unicoderange.cpp create mode 100644 src/unicoderange.h create mode 100644 src/util/CMakeLists.txt create mode 100644 src/util/README create mode 100644 src/util/const_char_ptr.h create mode 100644 src/util/copy.h create mode 100644 src/util/ege-appear-time-tracker.cpp create mode 100644 src/util/ege-appear-time-tracker.h create mode 100644 src/util/ege-tags.cpp create mode 100644 src/util/ege-tags.h create mode 100644 src/util/enums.h create mode 100644 src/util/expression-evaluator.cpp create mode 100644 src/util/expression-evaluator.h create mode 100644 src/util/find-if-before.h create mode 100644 src/util/find-last-if.h create mode 100644 src/util/fixed_point.h create mode 100644 src/util/format.h create mode 100644 src/util/forward-pointer-iterator.h create mode 100644 src/util/list-container-test.h create mode 100644 src/util/list-container.h create mode 100644 src/util/list-copy.h create mode 100644 src/util/list.h create mode 100644 src/util/longest-common-suffix.h create mode 100644 src/util/reference.h create mode 100644 src/util/reverse-list.h create mode 100644 src/util/share.cpp create mode 100644 src/util/share.h create mode 100644 src/util/signal-blocker.h create mode 100644 src/util/ucompose.hpp create mode 100644 src/util/units.cpp create mode 100644 src/util/units.h create mode 100644 src/util/ziptool.cpp create mode 100644 src/util/ziptool.h create mode 100644 src/vanishing-point.cpp create mode 100644 src/vanishing-point.h create mode 100644 src/verbs.cpp create mode 100644 src/verbs.h create mode 100644 src/version.cpp create mode 100644 src/version.h create mode 100644 src/widgets/CMakeLists.txt create mode 100644 src/widgets/README create mode 100644 src/widgets/desktop-widget.cpp create mode 100644 src/widgets/desktop-widget.h create mode 100644 src/widgets/ege-paint-def.cpp create mode 100644 src/widgets/ege-paint-def.h create mode 100644 src/widgets/fill-n-stroke-factory.h create mode 100644 src/widgets/fill-style.cpp create mode 100644 src/widgets/fill-style.h create mode 100644 src/widgets/gradient-image.cpp create mode 100644 src/widgets/gradient-image.h create mode 100644 src/widgets/gradient-selector.cpp create mode 100644 src/widgets/gradient-selector.h create mode 100644 src/widgets/gradient-vector.cpp create mode 100644 src/widgets/gradient-vector.h create mode 100644 src/widgets/ink-action.cpp create mode 100644 src/widgets/ink-action.h create mode 100644 src/widgets/mappings.xml create mode 100644 src/widgets/paint-selector.cpp create mode 100644 src/widgets/paint-selector.h create mode 100644 src/widgets/sp-attribute-widget.cpp create mode 100644 src/widgets/sp-attribute-widget.h create mode 100644 src/widgets/sp-color-selector.cpp create mode 100644 src/widgets/sp-color-selector.h create mode 100644 src/widgets/sp-xmlview-tree.cpp create mode 100644 src/widgets/sp-xmlview-tree.h create mode 100644 src/widgets/spinbutton-events.cpp create mode 100644 src/widgets/spinbutton-events.h create mode 100644 src/widgets/spw-utilities.cpp create mode 100644 src/widgets/spw-utilities.h create mode 100644 src/widgets/stroke-marker-selector.cpp create mode 100644 src/widgets/stroke-marker-selector.h create mode 100644 src/widgets/stroke-style.cpp create mode 100644 src/widgets/stroke-style.h create mode 100644 src/widgets/style-utils.h create mode 100644 src/widgets/swatch-selector.cpp create mode 100644 src/widgets/swatch-selector.h create mode 100644 src/widgets/toolbox.cpp create mode 100644 src/widgets/toolbox.h create mode 100644 src/widgets/widget-sizes.h create mode 100644 src/xml/CMakeLists.txt create mode 100644 src/xml/README create mode 100644 src/xml/attribute-record.h create mode 100644 src/xml/comment-node.h create mode 100644 src/xml/composite-node-observer.cpp create mode 100644 src/xml/composite-node-observer.h create mode 100644 src/xml/croco-node-iface.cpp create mode 100644 src/xml/croco-node-iface.h create mode 100644 src/xml/document.h create mode 100644 src/xml/element-node.h create mode 100644 src/xml/event-fns.h create mode 100644 src/xml/event.cpp create mode 100644 src/xml/event.h create mode 100644 src/xml/helper-observer.cpp create mode 100644 src/xml/helper-observer.h create mode 100644 src/xml/invalid-operation-exception.h create mode 100644 src/xml/log-builder.cpp create mode 100644 src/xml/log-builder.h create mode 100644 src/xml/node-event-vector.h create mode 100644 src/xml/node-fns.cpp create mode 100644 src/xml/node-fns.h create mode 100644 src/xml/node-iterators.h create mode 100644 src/xml/node-observer.h create mode 100644 src/xml/node.h create mode 100644 src/xml/pi-node.h create mode 100644 src/xml/quote-test.h create mode 100644 src/xml/quote.cpp create mode 100644 src/xml/quote.h create mode 100644 src/xml/rebase-hrefs.cpp create mode 100644 src/xml/rebase-hrefs.h create mode 100644 src/xml/repr-action-test.h create mode 100644 src/xml/repr-css.cpp create mode 100644 src/xml/repr-io.cpp create mode 100644 src/xml/repr-sorting.cpp create mode 100644 src/xml/repr-sorting.h create mode 100644 src/xml/repr-util.cpp create mode 100644 src/xml/repr.cpp create mode 100644 src/xml/repr.h create mode 100644 src/xml/simple-document.cpp create mode 100644 src/xml/simple-document.h create mode 100644 src/xml/simple-node.cpp create mode 100644 src/xml/simple-node.h create mode 100644 src/xml/sp-css-attr.h create mode 100644 src/xml/subtree.cpp create mode 100644 src/xml/subtree.h create mode 100644 src/xml/text-node.h (limited to 'src') diff --git a/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README b/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README new file mode 100644 index 0000000..e0919de --- /dev/null +++ b/src/2geom/!PLEASE DON'T MAKE CHANGES IN THESE FILES.README @@ -0,0 +1,14 @@ +This is an in-tree copy of lib2geom, a 2D geometry library started +by Inkscape developers. If you want to change code in 2Geom, you should +commit it first to upstream repository and then execute this command: + +rsync -r --existing --exclude CMakeLists.txt /path/to/lib2geom/src/2geom/ /path/to/inkscape/src/2geom/ + +The command above will only update existing files. If you add new files +to 2Geom, you'll need to copy the new files manually. Same if you remove +some files. Note that the trailing slashes are required! + +2Geom's git repository is hosted on GitHub + https://github.com/inkscape/lib2geom +and mirrored on GitLab + https://gitlab.com/inkscape/lib2geom diff --git a/src/2geom/2geom.h b/src/2geom/2geom.h new file mode 100644 index 0000000..813e243 --- /dev/null +++ b/src/2geom/2geom.h @@ -0,0 +1,75 @@ +/** + * \file + * \brief Include everything + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_2GEOM_H +#define LIB2GEOM_SEEN_2GEOM_H + +#include <2geom/forward.h> + +// primitives +#include <2geom/coord.h> +#include <2geom/point.h> +#include <2geom/interval.h> +#include <2geom/rect.h> +#include <2geom/angle.h> +#include <2geom/ray.h> +#include <2geom/line.h> +#include <2geom/affine.h> +#include <2geom/transforms.h> + +// curves and paths +#include <2geom/curves.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> + +// fragments +#include <2geom/d2.h> +#include <2geom/linear.h> +#include <2geom/bezier.h> +#include <2geom/sbasis.h> + +// others +#include <2geom/math-utils.h> +#include <2geom/utils.h> + +#endif // LIB2GEOM_SEEN_2GEOM_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/CMakeLists.txt b/src/2geom/CMakeLists.txt new file mode 100644 index 0000000..e1e708e --- /dev/null +++ b/src/2geom/CMakeLists.txt @@ -0,0 +1,139 @@ +# Override error flag just for this folder +if (CMAKE_BUILD_TYPE MATCHES Strict) + set(CMAKE_CXX_FLAGS_STRICT "${CMAKE_CXX_FLAGS_STRICT} -Wno-error=deprecated-declarations") +endif() + +if(HAVE_SINCOS) + add_definitions(-DHAVE_SINCOS) +endif() + +set(2geom_SRC + affine.cpp + basic-intersection.cpp + bezier.cpp + bezier-clipping.cpp + bezier-curve.cpp + bezier-utils.cpp + cairo-path-sink.cpp + circle.cpp + # conic_section_clipper_impl.cpp + # conicsec.cpp + convex-hull.cpp + coord.cpp + crossing.cpp + curve.cpp + d2-sbasis.cpp + ellipse.cpp + elliptical-arc.cpp + elliptical-arc-from-sbasis.cpp + geom.cpp + intersection-graph.cpp + line.cpp + nearest-time.cpp + numeric/matrix.cpp + path-intersection.cpp + path-sink.cpp + path.cpp + pathvector.cpp + piecewise.cpp + point.cpp + polynomial.cpp + rect.cpp + # recursive-bezier-intersection.cpp + sbasis-2d.cpp + sbasis-geometric.cpp + sbasis-math.cpp + sbasis-poly.cpp + sbasis-roots.cpp + sbasis-to-bezier.cpp + sbasis.cpp + solve-bezier.cpp + solve-bezier-one-d.cpp + solve-bezier-parametric.cpp + svg-path-parser.cpp + svg-path-writer.cpp + sweep-bounds.cpp + transforms.cpp + utils.cpp + + + # ------- + 2geom.h + # Headers + affine.h + angle.h + basic-intersection.h + bezier-curve.h + bezier-to-sbasis.h + bezier-utils.h + bezier.h + cairo-path-sink.h + choose.h + circle.h + concepts.h + conic_section_clipper.h + conic_section_clipper_cr.h + conic_section_clipper_impl.h + conicsec.h + convex-hull.h + coord.h + crossing.h + curve.h + curves.h + d2.h + ellipse.h + elliptical-arc.h + exception.h + forward.h + generic-interval.h + generic-rect.h + geom.h + int-interval.h + int-point.h + int-rect.h + intersection-graph.h + intersection.h + interval.h + line.h + linear.h + math-utils.h + nearest-time.h + ord.h + path-intersection.h + path-sink.h + path.h + pathvector.h + piecewise.h + point.h + polynomial.h + ray.h + rect.h + sbasis-2d.h + sbasis-curve.h + sbasis-geometric.h + sbasis-math.h + sbasis-poly.h + sbasis-to-bezier.h + sbasis.h + solver.h + svg-path-parser.h + svg-path-writer.h + sweep-bounds.h + sweeper.h + transforms.h + utils.h + + numeric/fitting-model.h + numeric/fitting-tool.h + numeric/linear_system.h + numeric/matrix.h + numeric/symmetric-matrix-fs-operation.h + numeric/symmetric-matrix-fs-trace.h + numeric/symmetric-matrix-fs.h + numeric/vector.h +) + +# make lib for 2geom_LIB +add_inkscape_lib(2geom_LIB "${2geom_SRC}") +target_include_directories(2geom_LIB PRIVATE ${DoubleConversion_INCLUDE_DIRS}) +target_link_libraries(2geom_LIB PRIVATE ${DoubleConversion_LIBRARIES}) diff --git a/src/2geom/affine.cpp b/src/2geom/affine.cpp new file mode 100644 index 0000000..48179e8 --- /dev/null +++ b/src/2geom/affine.cpp @@ -0,0 +1,522 @@ +/* + * Authors: + * Lauris Kaplinski + * Michael G. Sloan + * + * This code is in public domain + */ + +#include <2geom/affine.h> +#include <2geom/point.h> +#include <2geom/polynomial.h> +#include <2geom/utils.h> + +namespace Geom { + +/** Creates a Affine given an axis and origin point. + * The axis is represented as two vectors, which represent skew, rotation, and scaling in two dimensions. + * from_basis(Point(1, 0), Point(0, 1), Point(0, 0)) would return the identity matrix. + + \param x_basis the vector for the x-axis. + \param y_basis the vector for the y-axis. + \param offset the translation applied by the matrix. + \return The new Affine. + */ +//NOTE: Inkscape's version is broken, so when including this version, you'll have to search for code with this func +Affine from_basis(Point const &x_basis, Point const &y_basis, Point const &offset) { + return Affine(x_basis[X], x_basis[Y], + y_basis[X], y_basis[Y], + offset [X], offset [Y]); +} + +Point Affine::xAxis() const { + return Point(_c[0], _c[1]); +} + +Point Affine::yAxis() const { + return Point(_c[2], _c[3]); +} + +/// Gets the translation imparted by the Affine. +Point Affine::translation() const { + return Point(_c[4], _c[5]); +} + +void Affine::setXAxis(Point const &vec) { + for(int i = 0; i < 2; i++) + _c[i] = vec[i]; +} + +void Affine::setYAxis(Point const &vec) { + for(int i = 0; i < 2; i++) + _c[i + 2] = vec[i]; +} + +/// Sets the translation imparted by the Affine. +void Affine::setTranslation(Point const &loc) { + for(int i = 0; i < 2; i++) + _c[i + 4] = loc[i]; +} + +/** Calculates the amount of x-scaling imparted by the Affine. This is the scaling applied to + * the original x-axis region. It is \emph{not} the overall x-scaling of the transformation. + * Equivalent to L2(m.xAxis()). */ +double Affine::expansionX() const { + return sqrt(_c[0] * _c[0] + _c[1] * _c[1]); +} + +/** Calculates the amount of y-scaling imparted by the Affine. This is the scaling applied before + * the other transformations. It is \emph{not} the overall y-scaling of the transformation. + * Equivalent to L2(m.yAxis()). */ +double Affine::expansionY() const { + return sqrt(_c[2] * _c[2] + _c[3] * _c[3]); +} + +void Affine::setExpansionX(double val) { + double exp_x = expansionX(); + if (exp_x != 0.0) { //TODO: best way to deal with it is to skip op? + double coef = val / expansionX(); + for (unsigned i = 0; i < 2; ++i) { + _c[i] *= coef; + } + } +} + +void Affine::setExpansionY(double val) { + double exp_y = expansionY(); + if (exp_y != 0.0) { //TODO: best way to deal with it is to skip op? + double coef = val / expansionY(); + for (unsigned i = 2; i < 4; ++i) { + _c[i] *= coef; + } + } +} + +/** Sets this matrix to be the Identity Affine. */ +void Affine::setIdentity() { + _c[0] = 1.0; _c[1] = 0.0; + _c[2] = 0.0; _c[3] = 1.0; + _c[4] = 0.0; _c[5] = 0.0; +} + +/** @brief Check whether this matrix is an identity matrix. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ */ +bool Affine::isIdentity(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) && + are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents a pure translation. + * Will return true for the identity matrix, which represents a zero translation. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + a & b & 1 \end{array}\right]\f$ */ +bool Affine::isTranslation(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) && + are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps); +} +/** @brief Check whether this matrix represents a pure nonzero translation. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + a & b & 1 \end{array}\right]\f$ and \f$a, b \neq 0\f$ */ +bool Affine::isNonzeroTranslation(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) && + are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) && + (!are_near(_c[4], 0.0, eps) || !are_near(_c[5], 0.0, eps)); +} + +/** @brief Check whether this matrix represents pure scaling. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & 0 & 0 \\ + 0 & b & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$. */ +bool Affine::isScale(Coord eps) const { + if (isSingular(eps)) return false; + return are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents pure, nonzero scaling. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & 0 & 0 \\ + 0 & b & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ and \f$a, b \neq 1\f$. */ +bool Affine::isNonzeroScale(Coord eps) const { + if (isSingular(eps)) return false; + return (!are_near(_c[0], 1.0, eps) || !are_near(_c[3], 1.0, eps)) && //NOTE: these are the diags, and the next line opposite diags + are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents pure uniform scaling. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a_1 & 0 & 0 \\ + 0 & a_2 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ where \f$|a_1| = |a_2|\f$. */ +bool Affine::isUniformScale(Coord eps) const { + if (isSingular(eps)) return false; + return are_near(fabs(_c[0]), fabs(_c[3]), eps) && + are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents pure, nonzero uniform scaling. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a_1 & 0 & 0 \\ + 0 & a_2 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ where \f$|a_1| = |a_2|\f$ + * and \f$a_1, a_2 \neq 1\f$. */ +bool Affine::isNonzeroUniformScale(Coord eps) const { + if (isSingular(eps)) return false; + // we need to test both c0 and c3 to handle the case of flips, + // which should be treated as nonzero uniform scales + return !(are_near(_c[0], 1.0, eps) && are_near(_c[3], 1.0, eps)) && + are_near(fabs(_c[0]), fabs(_c[3]), eps) && + are_near(_c[1], 0.0, eps) && are_near(_c[2], 0.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents a pure rotation. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & b & 0 \\ + -b & a & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ and \f$a^2 + b^2 = 1\f$. */ +bool Affine::isRotation(Coord eps) const { + return are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps) && + are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps); +} + +/** @brief Check whether this matrix represents a pure, nonzero rotation. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & b & 0 \\ + -b & a & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$, \f$a^2 + b^2 = 1\f$ and \f$a \neq 1\f$. */ +bool Affine::isNonzeroRotation(Coord eps) const { + return !are_near(_c[0], 1.0, eps) && + are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps) && + are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps); +} + +/** @brief Check whether this matrix represents a non-zero rotation about any point. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & b & 0 \\ + -b & a & 0 \\ + c & d & 1 \end{array}\right]\f$, \f$a^2 + b^2 = 1\f$ and \f$a \neq 1\f$. */ +bool Affine::isNonzeroNonpureRotation(Coord eps) const { + return !are_near(_c[0], 1.0, eps) && + are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps) && + are_near(_c[0]*_c[0] + _c[1]*_c[1], 1.0, eps); +} + +/** @brief For a (possibly non-pure) non-zero-rotation matrix, calculate the rotation center. + * @pre The matrix must be a non-zero-rotation matrix to prevent division by zero, see isNonzeroNonpureRotation(). + * @return The rotation center x, the solution to the equation + * \f$A x = x\f$. */ +Point Affine::rotationCenter() const { + Coord x = (_c[2]*_c[5]+_c[4]-_c[4]*_c[3]) / (1-_c[3]-_c[0]+_c[0]*_c[3]-_c[2]*_c[1]); + Coord y = (_c[1]*x + _c[5]) / (1 - _c[3]); + return Point(x,y); +}; + +/** @brief Check whether this matrix represents pure horizontal shearing. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + k & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$. */ +bool Affine::isHShear(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) && + are_near(_c[3], 1.0, eps) && are_near(_c[4], 0.0, eps) && + are_near(_c[5], 0.0, eps); +} +/** @brief Check whether this matrix represents pure, nonzero horizontal shearing. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + k & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ and \f$k \neq 0\f$. */ +bool Affine::isNonzeroHShear(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[1], 0.0, eps) && + !are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents pure vertical shearing. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & k & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$. */ +bool Affine::isVShear(Coord eps) const { + return are_near(_c[0], 1.0, eps) && are_near(_c[2], 0.0, eps) && + are_near(_c[3], 1.0, eps) && are_near(_c[4], 0.0, eps) && + are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents pure, nonzero vertical shearing. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + 1 & k & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$ and \f$k \neq 0\f$. */ +bool Affine::isNonzeroVShear(Coord eps) const { + return are_near(_c[0], 1.0, eps) && !are_near(_c[1], 0.0, eps) && + are_near(_c[2], 0.0, eps) && are_near(_c[3], 1.0, eps) && + are_near(_c[4], 0.0, eps) && are_near(_c[5], 0.0, eps); +} + +/** @brief Check whether this matrix represents zooming. + * Zooming is any combination of translation and uniform non-flipping scaling. + * It preserves angles, ratios of distances between arbitrary points + * and unit vectors of line segments. + * @param eps Numerical tolerance + * @return True iff the matrix is invertible and of the form + * \f$\left[\begin{array}{ccc} + a & 0 & 0 \\ + 0 & a & 0 \\ + b & c & 1 \end{array}\right]\f$. */ +bool Affine::isZoom(Coord eps) const { + if (isSingular(eps)) return false; + return are_near(_c[0], _c[3], eps) && are_near(_c[1], 0, eps) && are_near(_c[2], 0, eps); +} + +/** @brief Check whether the transformation preserves areas of polygons. + * This means that the transformation can be any combination of translation, rotation, + * shearing and squeezing (non-uniform scaling such that the absolute value of the product + * of Y-scale and X-scale is 1). + * @param eps Numerical tolerance + * @return True iff \f$|\det A| = 1\f$. */ +bool Affine::preservesArea(Coord eps) const +{ + return are_near(descrim2(), 1.0, eps); +} + +/** @brief Check whether the transformation preserves angles between lines. + * This means that the transformation can be any combination of translation, uniform scaling, + * rotation and flipping. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & b & 0 \\ + -b & a & 0 \\ + c & d & 1 \end{array}\right]\f$ or + \f$\left[\begin{array}{ccc} + -a & b & 0 \\ + b & a & 0 \\ + c & d & 1 \end{array}\right]\f$. */ +bool Affine::preservesAngles(Coord eps) const +{ + if (isSingular(eps)) return false; + return (are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps)) || + (are_near(_c[0], -_c[3], eps) && are_near(_c[1], _c[2], eps)); +} + +/** @brief Check whether the transformation preserves distances between points. + * This means that the transformation can be any combination of translation, + * rotation and flipping. + * @param eps Numerical tolerance + * @return True iff the matrix is of the form + * \f$\left[\begin{array}{ccc} + a & b & 0 \\ + -b & a & 0 \\ + c & d & 1 \end{array}\right]\f$ or + \f$\left[\begin{array}{ccc} + -a & b & 0 \\ + b & a & 0 \\ + c & d & 1 \end{array}\right]\f$ and \f$a^2 + b^2 = 1\f$. */ +bool Affine::preservesDistances(Coord eps) const +{ + return ((are_near(_c[0], _c[3], eps) && are_near(_c[1], -_c[2], eps)) || + (are_near(_c[0], -_c[3], eps) && are_near(_c[1], _c[2], eps))) && + are_near(_c[0] * _c[0] + _c[1] * _c[1], 1.0, eps); +} + +/** @brief Check whether this transformation flips objects. + * A transformation flips objects if it has a negative scaling component. */ +bool Affine::flips() const { + return det() < 0; +} + +/** @brief Check whether this matrix is singular. + * Singular matrices have no inverse, which means that applying them to a set of points + * results in a loss of information. + * @param eps Numerical tolerance + * @return True iff the determinant is near zero. */ +bool Affine::isSingular(Coord eps) const { + return are_near(det(), 0.0, eps); +} + +/** @brief Compute the inverse matrix. + * Inverse is a matrix (denoted \f$A^{-1}\f$) such that \f$AA^{-1} = A^{-1}A = I\f$. + * Singular matrices have no inverse (for example a matrix that has two of its columns equal). + * For such matrices, the identity matrix will be returned instead. + * @param eps Numerical tolerance + * @return Inverse of the matrix, or the identity matrix if the inverse is undefined. + * @post (m * m.inverse()).isIdentity() == true */ +Affine Affine::inverse() const { + Affine d; + + double mx = std::max(fabs(_c[0]) + fabs(_c[1]), + fabs(_c[2]) + fabs(_c[3])); // a random matrix norm (either l1 or linfty + if(mx > 0) { + Geom::Coord const determ = det(); + if (!rel_error_bound(std::sqrt(fabs(determ)), mx)) { + Geom::Coord const ideterm = 1.0 / (determ); + + d._c[0] = _c[3] * ideterm; + d._c[1] = -_c[1] * ideterm; + d._c[2] = -_c[2] * ideterm; + d._c[3] = _c[0] * ideterm; + d._c[4] = (-_c[4] * d._c[0] - _c[5] * d._c[2]); + d._c[5] = (-_c[4] * d._c[1] - _c[5] * d._c[3]); + } else { + d.setIdentity(); + } + } else { + d.setIdentity(); + } + + return d; +} + +/** @brief Calculate the determinant. + * @return \f$\det A\f$. */ +Coord Affine::det() const { + // TODO this can overflow + return _c[0] * _c[3] - _c[1] * _c[2]; +} + +/** @brief Calculate the square of the descriminant. + * This is simply the absolute value of the determinant. + * @return \f$|\det A|\f$. */ +Coord Affine::descrim2() const { + return fabs(det()); +} + +/** @brief Calculate the descriminant. + * If the matrix doesn't contain a shearing or non-uniform scaling component, this value says + * how will the length of any line segment change after applying this transformation + * to arbitrary objects on a plane. The new length will be + * @code line_seg.length() * m.descrim()) @endcode + * @return \f$\sqrt{|\det A|}\f$. */ +Coord Affine::descrim() const { + return sqrt(descrim2()); +} + +/** @brief Combine this transformation with another one. + * After this operation, the matrix will correspond to the transformation + * obtained by first applying the original version of this matrix, and then + * applying @a m. */ +Affine &Affine::operator*=(Affine const &o) { + Coord nc[6]; + for(int a = 0; a < 5; a += 2) { + for(int b = 0; b < 2; b++) { + nc[a + b] = _c[a] * o._c[b] + _c[a + 1] * o._c[b + 2]; + } + } + for(int a = 0; a < 6; ++a) { + _c[a] = nc[a]; + } + _c[4] += o._c[4]; + _c[5] += o._c[5]; + return *this; +} + +//TODO: What's this!?! +/** Given a matrix m such that unit_circle = m*x, this returns the + * quadratic form x*A*x = 1. + * @relates Affine */ +Affine elliptic_quadratic_form(Affine const &m) { + double od = m[0] * m[1] + m[2] * m[3]; + Affine ret (m[0]*m[0] + m[1]*m[1], od, + od, m[2]*m[2] + m[3]*m[3], + 0, 0); + return ret; // allow NRVO +} + +Eigen::Eigen(Affine const &m) { + double const B = -m[0] - m[3]; + double const C = m[0]*m[3] - m[1]*m[2]; + + std::vector v = solve_quadratic(1, B, C); + + for (unsigned i = 0; i < v.size(); ++i) { + values[i] = v[i]; + vectors[i] = unit_vector(rot90(Point(m[0] - values[i], m[1]))); + } + for (unsigned i = v.size(); i < 2; ++i) { + values[i] = 0; + vectors[i] = Point(0,0); + } +} + +Eigen::Eigen(double m[2][2]) { + double const B = -m[0][0] - m[1][1]; + double const C = m[0][0]*m[1][1] - m[1][0]*m[0][1]; + + std::vector v = solve_quadratic(1, B, C); + + for (unsigned i = 0; i < v.size(); ++i) { + values[i] = v[i]; + vectors[i] = unit_vector(rot90(Point(m[0][0] - values[i], m[0][1]))); + } + for (unsigned i = v.size(); i < 2; ++i) { + values[i] = 0; + vectors[i] = Point(0,0); + } +} + +/** @brief Nearness predicate for affine transforms. + * @returns True if all entries of matrices are within eps of each other. + * @relates Affine */ +bool are_near(Affine const &a, Affine const &b, Coord eps) +{ + return are_near(a[0], b[0], eps) && are_near(a[1], b[1], eps) && + are_near(a[2], b[2], eps) && are_near(a[3], b[3], eps) && + are_near(a[4], b[4], eps) && are_near(a[5], b[5], eps); +} + +} //namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/affine.h b/src/2geom/affine.h new file mode 100644 index 0000000..470d5fc --- /dev/null +++ b/src/2geom/affine.h @@ -0,0 +1,244 @@ +/** + * \file + * \brief 3x3 affine transformation matrix. + *//* + * Authors: + * Lauris Kaplinski (Original NRAffine definition and related macros) + * Nathan Hurst (Geom::Affine class version of the above) + * Michael G. Sloan (reorganization and additions) + * Krzysztof KosiÅ„ski (removal of boilerplate, docs) + * + * This code is in public domain. + */ + +#ifndef LIB2GEOM_SEEN_AFFINE_H +#define LIB2GEOM_SEEN_AFFINE_H + +#include +#include <2geom/forward.h> +#include <2geom/point.h> +#include <2geom/utils.h> + +namespace Geom { + +/** + * @brief 3x3 matrix representing an affine transformation. + * + * Affine transformations on elements of a vector space are transformations which can be + * expressed in terms of matrix multiplication followed by addition + * (\f$x \mapsto A x + b\f$). They can be thought of as generalizations of linear functions + * (\f$y = a x + b\f$) to vector spaces. Affine transformations of points on a 2D plane preserve + * the following properties: + * + * - Colinearity: if three points lie on the same line, they will still be colinear after + * an affine transformation. + * - Ratios of distances between points on the same line are preserved + * - Parallel lines remain parallel. + * + * All affine transformations on 2D points can be written as combinations of scaling, rotation, + * shearing and translation. They can be represented as a combination of a vector and a 2x2 matrix, + * but this form is inconvenient to work with. A better solution is to represent all affine + * transformations on the 2D plane as 3x3 matrices, where the last column has fixed values. + * \f[ A = \left[ \begin{array}{ccc} + c_0 & c_1 & 0 \\ + c_2 & c_3 & 0 \\ + c_4 & c_5 & 1 \end{array} \right]\f] + * + * We then interpret points as row vectors of the form \f$[p_X, p_Y, 1]\f$. Applying a + * transformation to a point can be written as right-multiplication by a 3x3 matrix + * (\f$p' = pA\f$). This subset of matrices is closed under multiplication - combination + * of any two transforms can be expressed as the multiplication of their matrices. + * In this representation, the \f$c_4\f$ and \f$c_5\f$ coefficients represent + * the translation component of the transformation. + * + * Matrices can be multiplied by other more specific transformations. When multiplying, + * the transformations are applied from left to right, so for example m = a * b + * means: @a m first transforms by a, then by b. + * + * @ingroup Transforms + */ +class Affine + : boost::equality_comparable< Affine // generates operator!= from operator== + , boost::multipliable1< Affine + , MultipliableNoncommutative< Affine, Translate + , MultipliableNoncommutative< Affine, Scale + , MultipliableNoncommutative< Affine, Rotate + , MultipliableNoncommutative< Affine, HShear + , MultipliableNoncommutative< Affine, VShear + , MultipliableNoncommutative< Affine, Zoom + > > > > > > > > +{ + Coord _c[6]; +public: + Affine() { + _c[0] = _c[3] = 1; + _c[1] = _c[2] = _c[4] = _c[5] = 0; + } + + /** @brief Create a matrix from its coefficient values. + * It's rather inconvenient to directly create matrices in this way. Use transform classes + * if your transformation has a geometric interpretation. + * @see Translate + * @see Scale + * @see Rotate + * @see HShear + * @see VShear + * @see Zoom */ + Affine(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, Coord c5) { + _c[0] = c0; _c[1] = c1; + _c[2] = c2; _c[3] = c3; + _c[4] = c4; _c[5] = c5; + } + + /** @brief Access a coefficient by its index. */ + inline Coord operator[](unsigned i) const { return _c[i]; } + inline Coord &operator[](unsigned i) { return _c[i]; } + + /// @name Combine with other transformations + /// @{ + Affine &operator*=(Affine const &m); + // implemented in transforms.cpp + Affine &operator*=(Translate const &t); + Affine &operator*=(Scale const &s); + Affine &operator*=(Rotate const &r); + Affine &operator*=(HShear const &h); + Affine &operator*=(VShear const &v); + Affine &operator*=(Zoom const &); + /// @} + + bool operator==(Affine const &o) const { + for(unsigned i = 0; i < 6; ++i) { + if ( _c[i] != o._c[i] ) return false; + } + return true; + } + + /// @name Get the parameters of the matrix's transform + /// @{ + Point xAxis() const; + Point yAxis() const; + Point translation() const; + Coord expansionX() const; + Coord expansionY() const; + Point expansion() const { return Point(expansionX(), expansionY()); } + /// @} + + /// @name Modify the matrix + /// @{ + void setXAxis(Point const &vec); + void setYAxis(Point const &vec); + + void setTranslation(Point const &loc); + + void setExpansionX(Coord val); + void setExpansionY(Coord val); + void setIdentity(); + /// @} + + /// @name Inspect the matrix's transform + /// @{ + bool isIdentity(Coord eps = EPSILON) const; + + bool isTranslation(Coord eps = EPSILON) const; + bool isScale(Coord eps = EPSILON) const; + bool isUniformScale(Coord eps = EPSILON) const; + bool isRotation(Coord eps = EPSILON) const; + bool isHShear(Coord eps = EPSILON) const; + bool isVShear(Coord eps = EPSILON) const; + + bool isNonzeroTranslation(Coord eps = EPSILON) const; + bool isNonzeroScale(Coord eps = EPSILON) const; + bool isNonzeroUniformScale(Coord eps = EPSILON) const; + bool isNonzeroRotation(Coord eps = EPSILON) const; + bool isNonzeroNonpureRotation(Coord eps = EPSILON) const; + Point rotationCenter() const; + bool isNonzeroHShear(Coord eps = EPSILON) const; + bool isNonzeroVShear(Coord eps = EPSILON) const; + + bool isZoom(Coord eps = EPSILON) const; + bool preservesArea(Coord eps = EPSILON) const; + bool preservesAngles(Coord eps = EPSILON) const; + bool preservesDistances(Coord eps = EPSILON) const; + bool flips() const; + + bool isSingular(Coord eps = EPSILON) const; + /// @} + + /// @name Compute other matrices + /// @{ + Affine withoutTranslation() const { + Affine ret(*this); + ret.setTranslation(Point(0,0)); + return ret; + } + Affine inverse() const; + /// @} + + /// @name Compute scalar values + /// @{ + Coord det() const; + Coord descrim2() const; + Coord descrim() const; + /// @} + inline static Affine identity(); +}; + +/** @brief Print out the Affine (for debugging). + * @relates Affine */ +inline std::ostream &operator<< (std::ostream &out_file, const Geom::Affine &m) { + out_file << "A: " << m[0] << " C: " << m[2] << " E: " << m[4] << "\n"; + out_file << "B: " << m[1] << " D: " << m[3] << " F: " << m[5] << "\n"; + return out_file; +} + +// Affine factories +Affine from_basis(const Point &x_basis, const Point &y_basis, const Point &offset=Point(0,0)); +Affine elliptic_quadratic_form(Affine const &m); + +/** Given a matrix (ignoring the translation) this returns the eigen + * values and vectors. */ +class Eigen{ +public: + Point vectors[2]; + double values[2]; + Eigen(Affine const &m); + Eigen(double M[2][2]); +}; + +/** @brief Create an identity matrix. + * This is a convenience function identical to Affine::identity(). */ +inline Affine identity() { + Affine ret(Affine::identity()); + return ret; // allow NRVO +} + +/** @brief Create an identity matrix. + * @return The matrix + * \f$\left[\begin{array}{ccc} + 1 & 0 & 0 \\ + 0 & 1 & 0 \\ + 0 & 0 & 1 \end{array}\right]\f$. + * @relates Affine */ +inline Affine Affine::identity() { + Affine ret(1.0, 0.0, + 0.0, 1.0, + 0.0, 0.0); + return ret; // allow NRVO +} + +bool are_near(Affine const &a1, Affine const &a2, Coord eps=EPSILON); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_AFFINE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/angle.h b/src/2geom/angle.h new file mode 100644 index 0000000..f0caaba --- /dev/null +++ b/src/2geom/angle.h @@ -0,0 +1,408 @@ +/** + * \file + * \brief Various trigoniometric helper functions + *//* + * Authors: + * Johan Engelen + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2007-2010 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_ANGLE_H +#define LIB2GEOM_SEEN_ANGLE_H + +#include +#include +#include <2geom/exception.h> +#include <2geom/coord.h> +#include <2geom/point.h> + +namespace Geom { + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif +#ifndef M_1_2PI +# define M_1_2PI 0.159154943091895335768883763373 +#endif + +/** @brief Wrapper for angular values. + * + * This class is a convenience wrapper that implements the behavior generally expected of angles, + * like addition modulo \f$2\pi\f$. The value returned from the default conversion + * to double is in the range \f$[-\pi, \pi)\f$ - the convention used by C's + * math library. + * + * This class holds only a single floating point value, so passing it by value will generally + * be faster than passing it by const reference. + * + * @ingroup Primitives + */ +class Angle + : boost::additive< Angle + , boost::additive< Angle, Coord + , boost::equality_comparable< Angle + , boost::equality_comparable< Angle, Coord + > > > > +{ +public: + Angle() : _angle(0) {} + Angle(Coord v) : _angle(v) { _normalize(); } // this can be called implicitly + explicit Angle(Point const &p) : _angle(atan2(p)) { _normalize(); } + Angle(Point const &a, Point const &b) : _angle(angle_between(a, b)) { _normalize(); } + operator Coord() const { return radians(); } + Angle &operator+=(Angle o) { + _angle += o._angle; + _normalize(); + return *this; + } + Angle &operator-=(Angle o) { + _angle -= o._angle; + _normalize(); + return *this; + } + Angle &operator+=(Coord a) { + *this += Angle(a); + return *this; + } + Angle &operator-=(Coord a) { + *this -= Angle(a); + return *this; + } + bool operator==(Angle o) const { + return _angle == o._angle; + } + bool operator==(Coord c) const { + return _angle == Angle(c)._angle; + } + + /** @brief Get the angle as radians. + * @return Number in range \f$[-\pi, \pi)\f$. */ + Coord radians() const { + return _angle >= M_PI ? _angle - 2*M_PI : _angle; + } + /** @brief Get the angle as positive radians. + * @return Number in range \f$[0, 2\pi)\f$. */ + Coord radians0() const { + return _angle; + } + /** @brief Get the angle as degrees in math convention. + * @return Number in range [-180, 180) obtained by scaling the result of radians() + * by \f$180/\pi\f$. */ + Coord degrees() const { return radians() * (180.0 / M_PI); } + /** @brief Get the angle as degrees in clock convention. + * This method converts the angle to the "clock convention": angles start from the +Y axis + * and grow clockwise. This means that 0 corresponds to \f$\pi/2\f$ radians, + * 90 to 0 radians, 180 to \f$-\pi/2\f$ radians, and 270 to \f$\pi\f$ radians. + * @return A number in the range [0, 360). + */ + Coord degreesClock() const { + Coord ret = 90.0 - _angle * (180.0 / M_PI); + if (ret < 0) ret += 360; + return ret; + } + /** @brief Create an angle from its measure in radians. */ + static Angle from_radians(Coord d) { + Angle a(d); + return a; + } + /** @brief Create an angle from its measure in degrees. */ + static Angle from_degrees(Coord d) { + Angle a(d * (M_PI / 180.0)); + return a; + } + /** @brief Create an angle from its measure in degrees in clock convention. + * @see Angle::degreesClock() */ + static Angle from_degrees_clock(Coord d) { + // first make sure d is in [0, 360) + d = std::fmod(d, 360.0); + if (d < 0) d += 360.0; + Coord rad = M_PI/2 - d * (M_PI / 180.0); + if (rad < 0) rad += 2*M_PI; + Angle a; + a._angle = rad; + return a; + } +private: + + void _normalize() { + _angle = std::fmod(_angle, 2*M_PI); + if (_angle < 0) _angle += 2*M_PI; + //_angle -= floor(_angle * (1.0/(2*M_PI))) * 2*M_PI; + } + Coord _angle; // this is always in [0, 2pi) + friend class AngleInterval; +}; + +inline Angle distance(Angle const &a, Angle const &b) { + // the distance cannot be larger than M_PI. + Coord ac = a.radians0(); + Coord bc = b.radians0(); + Coord d = fabs(ac - bc); + return Angle(d > M_PI ? 2*M_PI - d : d); +} + +/** @brief Directed angular interval. + * + * Wrapper for directed angles with defined start and end values. Useful e.g. for representing + * the portion of an ellipse in an elliptical arc. Both extreme angles are contained + * in the interval (it is a closed interval). Angular intervals can also be interptered + * as functions \f$f: [0, 1] \to [-\pi, \pi)\f$, which return the start angle for 0, + * the end angle for 1, and interpolate linearly for other values. Note that such functions + * are not continuous if the interval crosses the angle \f$\pi\f$. + * + * This class can represent all directed angular intervals, including empty ones. + * However, not all possible intervals can be created with the constructors. + * For full control, use the setInitial(), setFinal() and setAngles() methods. + * + * @ingroup Primitives + */ +class AngleInterval + : boost::equality_comparable< AngleInterval > +{ +public: + AngleInterval() {} + /** @brief Create an angular interval from two angles and direction. + * If the initial and final angle are the same, a degenerate interval + * (containing only one angle) will be created. + * @param s Starting angle + * @param e Ending angle + * @param cw Which direction the interval goes. True means that it goes + * in the direction of increasing angles, while false means in the direction + * of decreasing angles. */ + AngleInterval(Angle s, Angle e, bool cw = false) + : _start_angle(s), _end_angle(e), _sweep(cw), _full(false) + {} + AngleInterval(double s, double e, bool cw = false) + : _start_angle(s), _end_angle(e), _sweep(cw), _full(false) + {} + /** @brief Create an angular interval from three angles. + * If the inner angle is exactly equal to initial or final angle, + * the sweep flag will be set to true, i.e. the interval will go + * in the direction of increasing angles. + * + * If the initial and final angle are the same, but the inner angle + * is different, a full angle in the direction of increasing angles + * will be created. + * + * @param s Initial angle + * @param inner Angle contained in the interval + * @param e Final angle */ + AngleInterval(Angle s, Angle inner, Angle e) + : _start_angle(s) + , _end_angle(e) + , _sweep((inner-s).radians0() <= (e-s).radians0()) + , _full(s == e && s != inner) + { + if (_full) { + _sweep = true; + } + } + + /// Get the start angle. + Angle initialAngle() const { return _start_angle; } + /// Get the end angle. + Angle finalAngle() const { return _end_angle; } + /// Check whether the interval goes in the direction of increasing angles. + bool sweep() const { return _sweep; } + /// Check whether the interval contains only a single angle. + bool isDegenerate() const { + return _start_angle == _end_angle && !_full; + } + /// Check whether the interval contains all angles. + bool isFull() const { + return _start_angle == _end_angle && _full; + } + + /** @brief Set the initial angle. + * @param a Angle to set + * @param prefer_full Whether to set a full angular interval when + * the initial angle is set to the final angle */ + void setInitial(Angle a, bool prefer_full = false) { + _start_angle = a; + _full = prefer_full && a == _end_angle; + } + + /** @brief Set the final angle. + * @param a Angle to set + * @param prefer_full Whether to set a full angular interval when + * the initial angle is set to the final angle */ + void setFinal(Angle a, bool prefer_full = false) { + _end_angle = a; + _full = prefer_full && a == _start_angle; + } + /** @brief Set both angles at once. + * The direction (sweep flag) is left unchanged. + * @param s Initial angle + * @param e Final angle + * @param prefer_full Whether to set a full interval when the passed + * initial and final angle are the same */ + void setAngles(Angle s, Angle e, bool prefer_full = false) { + _start_angle = s; + _end_angle = e; + _full = prefer_full && s == e; + } + /// Set whether the interval goes in the direction of increasing angles. + void setSweep(bool s) { _sweep = s; } + + /// Reverse the direction of the interval while keeping contained values the same. + void reverse() { + using std::swap; + swap(_start_angle, _end_angle); + _sweep = !_sweep; + } + /// Get a new interval with reversed direction. + AngleInterval reversed() const { + AngleInterval result(*this); + result.reverse(); + return result; + } + + /// Get an angle corresponding to the specified time value. + Angle angleAt(Coord t) const { + Coord span = extent(); + Angle ret = _start_angle.radians0() + span * (_sweep ? t : -t); + return ret; + } + Angle operator()(Coord t) const { return angleAt(t); } + + /** @brief Compute a time value that would evaluate to the given angle. + * If the start and end angle are exactly the same, NaN will be returned. + * Negative values will be returned for angles between the initial angle + * and the angle exactly opposite the midpoint of the interval. */ + Coord timeAtAngle(Angle a) const { + if (_full) { + Angle ta = _sweep ? a - _start_angle : _start_angle - a; + return ta.radians0() / (2*M_PI); + } + Coord ex = extent(); + Coord outex = 2*M_PI - ex; + if (_sweep) { + Angle midout = _start_angle - outex / 2; + Angle acmp = a - midout, scmp = _start_angle - midout; + if (acmp.radians0() >= scmp.radians0()) { + return (a - _start_angle).radians0() / ex; + } else { + return -(_start_angle - a).radians0() / ex; + } + } else { + Angle midout = _start_angle + outex / 2; + Angle acmp = a - midout, scmp = _start_angle - midout; + if (acmp.radians0() <= scmp.radians0()) { + return (_start_angle - a).radians0() / ex; + } else { + return -(a - _start_angle).radians0() / ex; + } + } + } + + /// Check whether the interval includes the given angle. + bool contains(Angle a) const { + if (_full) return true; + Coord s = _start_angle.radians0(); + Coord e = _end_angle.radians0(); + Coord x = a.radians0(); + if (_sweep) { + if (s < e) return x >= s && x <= e; + return x >= s || x <= e; + } else { + if (s > e) return x <= s && x >= e; + return x <= s || x >= e; + } + } + /** @brief Extent of the angle interval. + * Equivalent to the absolute value of the sweep angle. + * @return Extent in range \f$[0, 2\pi)\f$. */ + Coord extent() const { + if (_full) return 2*M_PI; + return _sweep + ? (_end_angle - _start_angle).radians0() + : (_start_angle - _end_angle).radians0(); + } + /** @brief Get the sweep angle of the interval. + * This is the value you need to add to the initial angle to get the final angle. + * It is positive when sweep is true. Denoted as \f$\Delta\theta\f$ in the SVG + * elliptical arc implementation notes. */ + Coord sweepAngle() const { + if (_full) return _sweep ? 2*M_PI : -2*M_PI; + Coord sa = _end_angle.radians0() - _start_angle.radians0(); + if (_sweep && sa < 0) sa += 2*M_PI; + if (!_sweep && sa > 0) sa -= 2*M_PI; + return sa; + } + + /// Check another interval for equality. + bool operator==(AngleInterval const &other) const { + if (_start_angle != other._start_angle) return false; + if (_end_angle != other._end_angle) return false; + if (_sweep != other._sweep) return false; + if (_full != other._full) return false; + return true; + } + + static AngleInterval create_full(Angle start, bool sweep = true) { + AngleInterval result; + result._start_angle = result._end_angle = start; + result._sweep = sweep; + result._full = true; + return result; + } + +private: + Angle _start_angle; + Angle _end_angle; + bool _sweep; + bool _full; +}; + +/** @brief Given an angle in degrees, return radians + * @relates Angle */ +inline Coord rad_from_deg(Coord deg) { return deg*M_PI/180.0;} +/** @brief Given an angle in radians, return degrees + * @relates Angle */ +inline Coord deg_from_rad(Coord rad) { return rad*180.0/M_PI;} + +} // end namespace Geom + +namespace std { +template <> class iterator_traits {}; +} + +#endif // LIB2GEOM_SEEN_ANGLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/basic-intersection.cpp b/src/2geom/basic-intersection.cpp new file mode 100644 index 0000000..7707037 --- /dev/null +++ b/src/2geom/basic-intersection.cpp @@ -0,0 +1,493 @@ +/** @file + * @brief Basic intersection routines + *//* + * Authors: + * Nathan Hurst + * Marco Cecchetti + * Jean-François Barraud + * + * Copyright 2008-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/basic-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/exception.h> + +#ifdef HAVE_GSL +#include +#include +#endif + +using std::vector; +namespace Geom { + +//#ifdef USE_RECURSIVE_INTERSECTOR + +// void find_intersections(std::vector > &xs, +// D2 const & A, +// D2 const & B) { +// vector BezA, BezB; +// sbasis_to_bezier(BezA, A); +// sbasis_to_bezier(BezB, B); + +// xs.clear(); + +// find_intersections_bezier_recursive(xs, BezA, BezB); +// } +// void find_intersections(std::vector< std::pair > & xs, +// std::vector const& A, +// std::vector const& B, +// double precision){ +// find_intersections_bezier_recursive(xs, A, B, precision); +// } + +//#else + +namespace detail{ namespace bezier_clipping { +void portion(std::vector &B, Interval const &I); +void derivative(std::vector &D, std::vector const &B); +}; }; + +void find_intersections(std::vector > &xs, + D2 const & A, + D2 const & B, + double precision) +{ + find_intersections_bezier_clipping(xs, bezier_points(A), bezier_points(B), precision); +} + +void find_intersections(std::vector > &xs, + D2 const & A, + D2 const & B, + double precision) +{ + vector BezA, BezB; + sbasis_to_bezier(BezA, A); + sbasis_to_bezier(BezB, B); + + find_intersections_bezier_clipping(xs, BezA, BezB, precision); +} + +void find_intersections(std::vector< std::pair > & xs, + std::vector const& A, + std::vector const& B, + double precision) +{ + find_intersections_bezier_clipping(xs, A, B, precision); +} + +//#endif + +/* + * split the curve at the midpoint, returning an array with the two parts + * Temporary storage is minimized by using part of the storage for the result + * to hold an intermediate value until it is no longer needed. + */ +// TODO replace with Bezier method +void split(vector const &p, double t, + vector &left, vector &right) { + const unsigned sz = p.size(); + //Geom::Point Vtemp[sz][sz]; + vector > Vtemp(sz); + for ( size_t i = 0; i < sz; ++i ) + Vtemp[i].reserve(sz); + + /* Copy control points */ + std::copy(p.begin(), p.end(), Vtemp[0].begin()); + + /* Triangle computation */ + for (unsigned i = 1; i < sz; i++) { + for (unsigned j = 0; j < sz - i; j++) { + Vtemp[i][j] = lerp(t, Vtemp[i-1][j], Vtemp[i-1][j+1]); + } + } + + left.resize(sz); + right.resize(sz); + for (unsigned j = 0; j < sz; j++) + left[j] = Vtemp[j][0]; + for (unsigned j = 0; j < sz; j++) + right[j] = Vtemp[sz-1-j][j]; +} + + + +void find_self_intersections(std::vector > &xs, + D2 const &A, + double precision) +{ + std::vector dr = derivative(A[X]).roots(); + { + std::vector dyr = derivative(A[Y]).roots(); + dr.insert(dr.begin(), dyr.begin(), dyr.end()); + } + dr.push_back(0); + dr.push_back(1); + // We want to be sure that we have no empty segments + std::sort(dr.begin(), dr.end()); + std::vector::iterator new_end = std::unique(dr.begin(), dr.end()); + dr.resize( new_end - dr.begin() ); + + std::vector< D2 > pieces; + for (unsigned i = 0; i < dr.size() - 1; ++i) { + pieces.push_back(portion(A, dr[i], dr[i+1])); + } + /*{ + vector l, r, in = A; + for(unsigned i = 0; i < dr.size()-1; i++) { + split(in, (dr[i+1]-dr[i]) / (1 - dr[i]), l, r); + pieces.push_back(l); + in = r; + } + }*/ + + for(unsigned i = 0; i < dr.size()-1; i++) { + for(unsigned j = i+1; j < dr.size()-1; j++) { + std::vector > section; + + find_intersections(section, pieces[i], pieces[j], precision); + for(unsigned k = 0; k < section.size(); k++) { + double l = section[k].first; + double r = section[k].second; +// XXX: This condition will prune out false positives, but it might create some false negatives. Todo: Confirm it is correct. + if(j == i+1) + //if((l == 1) && (r == 0)) + if( ( l > precision ) && (r < precision) )//FIXME: what precision should be used here??? + continue; + xs.push_back(std::make_pair((1-l)*dr[i] + l*dr[i+1], + (1-r)*dr[j] + r*dr[j+1])); + } + } + } + + // Because i is in order, xs should be roughly already in order? + //sort(xs.begin(), xs.end()); + //unique(xs.begin(), xs.end()); +} + +void find_self_intersections(std::vector > &xs, + D2 const &A, + double precision) +{ + D2 in; + sbasis_to_bezier(in, A); + find_self_intersections(xs, in, precision); +} + + +void subdivide(D2 const &a, + D2 const &b, + std::vector< std::pair > const &xs, + std::vector< D2 > &av, + std::vector< D2 > &bv) +{ + if (xs.empty()) { + av.push_back(a); + bv.push_back(b); + return; + } + + std::pair prev = std::make_pair(0., 0.); + for (unsigned i = 0; i < xs.size(); ++i) { + av.push_back(portion(a, prev.first, xs[i].first)); + bv.push_back(portion(b, prev.second, xs[i].second)); + av.back()[X].at0() = bv.back()[X].at0() = lerp(0.5, av.back()[X].at0(), bv.back()[X].at0()); + av.back()[X].at1() = bv.back()[X].at1() = lerp(0.5, av.back()[X].at1(), bv.back()[X].at1()); + av.back()[Y].at0() = bv.back()[Y].at0() = lerp(0.5, av.back()[Y].at0(), bv.back()[Y].at0()); + av.back()[Y].at1() = bv.back()[Y].at1() = lerp(0.5, av.back()[Y].at1(), bv.back()[Y].at1()); + prev = xs[i]; + } + av.push_back(portion(a, prev.first, 1)); + bv.push_back(portion(b, prev.second, 1)); + av.back()[X].at0() = bv.back()[X].at0() = lerp(0.5, av.back()[X].at0(), bv.back()[X].at0()); + av.back()[X].at1() = bv.back()[X].at1() = lerp(0.5, av.back()[X].at1(), bv.back()[X].at1()); + av.back()[Y].at0() = bv.back()[Y].at0() = lerp(0.5, av.back()[Y].at0(), bv.back()[Y].at0()); + av.back()[Y].at1() = bv.back()[Y].at1() = lerp(0.5, av.back()[Y].at1(), bv.back()[Y].at1()); +} + +#ifdef HAVE_GSL +#include + +struct rparams +{ + D2 const &A; + D2 const &B; +}; + +static int +intersect_polish_f (const gsl_vector * x, void *params, + gsl_vector * f) +{ + const double x0 = gsl_vector_get (x, 0); + const double x1 = gsl_vector_get (x, 1); + + Geom::Point dx = ((struct rparams *) params)->A(x0) - + ((struct rparams *) params)->B(x1); + + gsl_vector_set (f, 0, dx[0]); + gsl_vector_set (f, 1, dx[1]); + + return GSL_SUCCESS; +} +#endif + +union dbl_64{ + long long i64; + double d64; +}; + +static double EpsilonBy(double value, int eps) +{ + dbl_64 s; + s.d64 = value; + s.i64 += eps; + return s.d64; +} + + +static void intersect_polish_root (D2 const &A, double &s, + D2 const &B, double &t) { +#ifdef HAVE_GSL + const gsl_multiroot_fsolver_type *T; + gsl_multiroot_fsolver *sol; + + int status; + size_t iter = 0; +#endif + std::vector as, bs; + as = A.valueAndDerivatives(s, 2); + bs = B.valueAndDerivatives(t, 2); + Point F = as[0] - bs[0]; + double best = dot(F, F); + + for(int i = 0; i < 4; i++) { + + /** + we want to solve + J*(x1 - x0) = f(x0) + + |dA(s)[0] -dB(t)[0]| (X1 - X0) = A(s) - B(t) + |dA(s)[1] -dB(t)[1]| + **/ + + // We're using the standard transformation matricies, which is numerically rather poor. Much better to solve the equation using elimination. + + Affine jack(as[1][0], as[1][1], + -bs[1][0], -bs[1][1], + 0, 0); + Point soln = (F)*jack.inverse(); + double ns = s - soln[0]; + double nt = t - soln[1]; + + as = A.valueAndDerivatives(ns, 2); + bs = B.valueAndDerivatives(nt, 2); + F = as[0] - bs[0]; + double trial = dot(F, F); + if (trial > best*0.1) {// we have standards, you know + // At this point we could do a line search + break; + } + best = trial; + s = ns; + t = nt; + } + +#ifdef HAVE_GSL + const size_t n = 2; + struct rparams p = {A, B}; + gsl_multiroot_function f = {&intersect_polish_f, n, &p}; + + double x_init[2] = {s, t}; + gsl_vector *x = gsl_vector_alloc (n); + + gsl_vector_set (x, 0, x_init[0]); + gsl_vector_set (x, 1, x_init[1]); + + T = gsl_multiroot_fsolver_hybrids; + sol = gsl_multiroot_fsolver_alloc (T, 2); + gsl_multiroot_fsolver_set (sol, &f, x); + + do + { + iter++; + status = gsl_multiroot_fsolver_iterate (sol); + + if (status) /* check if solver is stuck */ + break; + + status = + gsl_multiroot_test_residual (sol->f, 1e-12); + } + while (status == GSL_CONTINUE && iter < 1000); + + s = gsl_vector_get (sol->x, 0); + t = gsl_vector_get (sol->x, 1); + + gsl_multiroot_fsolver_free (sol); + gsl_vector_free (x); +#endif + + { + // This code does a neighbourhood search for minor improvements. + double best_v = L1(A(s) - B(t)); + //std::cout << "------\n" << best_v << std::endl; + Point best(s,t); + while (true) { + Point trial = best; + double trial_v = best_v; + for(int nsi = -1; nsi < 2; nsi++) { + for(int nti = -1; nti < 2; nti++) { + Point n(EpsilonBy(best[0], nsi), + EpsilonBy(best[1], nti)); + double c = L1(A(n[0]) - B(n[1])); + //std::cout << c << "; "; + if (c < trial_v) { + trial = n; + trial_v = c; + } + } + } + if(trial == best) { + //std::cout << "\n" << s << " -> " << s - best[0] << std::endl; + //std::cout << t << " -> " << t - best[1] << std::endl; + //std::cout << best_v << std::endl; + s = best[0]; + t = best[1]; + return; + } else { + best = trial; + best_v = trial_v; + } + } + } +} + + +void polish_intersections(std::vector > &xs, + D2 const &A, D2 const &B) +{ + for(unsigned i = 0; i < xs.size(); i++) + intersect_polish_root(A, xs[i].first, + B, xs[i].second); +} + +/** + * Compute the Hausdorf distance from A to B only. + */ +double hausdorfl(D2& A, D2 const& B, + double m_precision, + double *a_t, double* b_t) { + std::vector< std::pair > xs; + std::vector Az, Bz; + sbasis_to_bezier (Az, A); + sbasis_to_bezier (Bz, B); + find_collinear_normal(xs, Az, Bz, m_precision); + double h_dist = 0, h_a_t = 0, h_b_t = 0; + double dist = 0; + Point Ax = A.at0(); + double t = Geom::nearest_time(Ax, B); + dist = Geom::distance(Ax, B(t)); + if (dist > h_dist) { + h_a_t = 0; + h_b_t = t; + h_dist = dist; + } + Ax = A.at1(); + t = Geom::nearest_time(Ax, B); + dist = Geom::distance(Ax, B(t)); + if (dist > h_dist) { + h_a_t = 1; + h_b_t = t; + h_dist = dist; + } + for (size_t i = 0; i < xs.size(); ++i) + { + Point At = A(xs[i].first); + Point Bu = B(xs[i].second); + double distAtBu = Geom::distance(At, Bu); + t = Geom::nearest_time(At, B); + dist = Geom::distance(At, B(t)); + //FIXME: we might miss it due to floating point precision... + if (dist >= distAtBu-.1 && distAtBu > h_dist) { + h_a_t = xs[i].first; + h_b_t = xs[i].second; + h_dist = distAtBu; + } + + } + if(a_t) *a_t = h_a_t; + if(b_t) *b_t = h_b_t; + + return h_dist; +} + +/** + * Compute the symmetric Hausdorf distance. + */ +double hausdorf(D2& A, D2 const& B, + double m_precision, + double *a_t, double* b_t) { + double h_dist = hausdorfl(A, B, m_precision, a_t, b_t); + + double dist = 0; + Point Bx = B.at0(); + double t = Geom::nearest_time(Bx, A); + dist = Geom::distance(Bx, A(t)); + if (dist > h_dist) { + if(a_t) *a_t = t; + if(b_t) *b_t = 0; + h_dist = dist; + } + Bx = B.at1(); + t = Geom::nearest_time(Bx, A); + dist = Geom::distance(Bx, A(t)); + if (dist > h_dist) { + if(a_t) *a_t = t; + if(b_t) *b_t = 1; + h_dist = dist; + } + + return h_dist; +} + +bool non_collinear_segments_intersect(const Point &A, const Point &B, const Point &C, const Point &D) +{ + return cross(D - C, A - C) * cross(D - C, B - C) < 0 && // + cross(B - A, C - A) * cross(B - A, D - A) < 0; +} +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/basic-intersection.h b/src/2geom/basic-intersection.h new file mode 100644 index 0000000..2d0c00d --- /dev/null +++ b/src/2geom/basic-intersection.h @@ -0,0 +1,151 @@ +/** @file + * @brief Basic intersection routines + *//* + * Authors: + * Nathan Hurst + * Marco Cecchetti + * Jean-François Barraud + * + * Copyright 2008-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_BASIC_INTERSECTION_H +#define LIB2GEOM_SEEN_BASIC_INTERSECTION_H + +#include <2geom/point.h> +#include <2geom/bezier.h> +#include <2geom/sbasis.h> +#include <2geom/d2.h> + +#include +#include + +#define USE_RECURSIVE_INTERSECTOR 0 + + +namespace Geom { + +void find_intersections(std::vector > &xs, + D2 const &A, + D2 const &B, + double precision = EPSILON); + +void find_intersections(std::vector > &xs, + D2 const &A, + D2 const &B, + double precision = EPSILON); + +void find_intersections(std::vector< std::pair > &xs, + std::vector const &A, + std::vector const &B, + double precision = EPSILON); + +void find_self_intersections(std::vector > &xs, + D2 const &A, + double precision = EPSILON); + +void find_self_intersections(std::vector > &xs, + D2 const &A, + double precision = EPSILON); + +/* + * find_intersection + * + * input: A, B - set of control points of two Bezier curve + * input: precision - required precision of computation + * output: xs - set of pairs of parameter values + * at which crossing happens + * + * This routine is based on the Bezier Clipping Algorithm, + * see: Sederberg, Nishita, 1990 - Curve intersection using Bezier clipping + */ +void find_intersections_bezier_clipping (std::vector< std::pair > & xs, + std::vector const& A, + std::vector const& B, + double precision = EPSILON); +//#endif + +void subdivide(D2 const &a, + D2 const &b, + std::vector< std::pair > const &xs, + std::vector< D2 > &av, + std::vector< D2 > &bv); + +/* + * find_collinear_normal + * + * input: A, B - set of control points of two Bezier curve + * input: precision - required precision of computation + * output: xs - set of pairs of parameter values + * at which there are collinear normals + * + * This routine is based on the Bezier Clipping Algorithm, + * see: Sederberg, Nishita, 1990 - Curve intersection using Bezier clipping + */ +void find_collinear_normal (std::vector< std::pair >& xs, + std::vector const& A, + std::vector const& B, + double precision = EPSILON); + +void polish_intersections(std::vector > &xs, + D2 const &A, + D2 const &B); + + +/** + * Compute the Hausdorf distance from A to B only. + */ +double hausdorfl(D2& A, D2 const &B, + double m_precision, + double *a_t=NULL, double *b_t=NULL); + +/** + * Compute the symmetric Hausdorf distance. + */ +double hausdorf(D2 &A, D2 const &B, + double m_precision, + double *a_t=NULL, double *b_t=NULL); + +/** + * Check if two line segments intersect. If they are collinear, the result is undefined. + * @return True if line segments AB and CD intersect + */ +bool non_collinear_segments_intersect(const Point &A, const Point &B, const Point &C, const Point &D); +} + +#endif // !LIB2GEOM_SEEN_BASIC_INTERSECTION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-clipping.cpp b/src/2geom/bezier-clipping.cpp new file mode 100644 index 0000000..e7bbcc3 --- /dev/null +++ b/src/2geom/bezier-clipping.cpp @@ -0,0 +1,1163 @@ +/* + * Implement the Bezier clipping algorithm for finding + * Bezier curve intersection points and collinear normals + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + + + +#include <2geom/basic-intersection.h> +#include <2geom/choose.h> +#include <2geom/point.h> +#include <2geom/interval.h> +#include <2geom/bezier.h> +#include <2geom/numeric/matrix.h> +#include <2geom/convex-hull.h> +#include <2geom/line.h> + +#include +#include +#include +#include +//#include + +using std::swap; + + +#define VERBOSE 0 +#define CHECK 0 + + +namespace Geom { + +namespace detail { namespace bezier_clipping { + +//////////////////////////////////////////////////////////////////////////////// +// for debugging +// + +void print(std::vector const& cp, const char* msg = "") +{ + std::cerr << msg << std::endl; + for (size_t i = 0; i < cp.size(); ++i) + std::cerr << i << " : " << cp[i] << std::endl; +} + +template< class charT > +std::basic_ostream & +operator<< (std::basic_ostream & os, const Interval & I) +{ + os << "[" << I.min() << ", " << I.max() << "]"; + return os; +} + +double angle (std::vector const& A) +{ + size_t n = A.size() -1; + double a = std::atan2(A[n][Y] - A[0][Y], A[n][X] - A[0][X]); + return (180 * a / M_PI); +} + +size_t get_precision(Interval const& I) +{ + double d = I.extent(); + double e = 0.1, p = 10; + int n = 0; + while (n < 16 && d < e) + { + p *= 10; + e = 1/p; + ++n; + } + return n; +} + +void range_assertion(int k, int m, int n, const char* msg) +{ + if ( k < m || k > n) + { + std::cerr << "range assertion failed: \n" + << msg << std::endl + << "value: " << k + << " range: " << m << ", " << n << std::endl; + assert (k >= m && k <= n); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// numerical routines + +/* + * Compute the binomial coefficient (n, k) + */ +double binomial(unsigned int n, unsigned int k) +{ + return choose(n, k); +} + +/* + * Compute the determinant of the 2x2 matrix with column the point P1, P2 + */ +double det(Point const& P1, Point const& P2) +{ + return P1[X]*P2[Y] - P1[Y]*P2[X]; +} + +/* + * Solve the linear system [P1,P2] * P = Q + * in case there isn't exactly one solution the routine returns false + */ +bool solve(Point & P, Point const& P1, Point const& P2, Point const& Q) +{ + double d = det(P1, P2); + if (d == 0) return false; + d = 1 / d; + P[X] = det(Q, P2) * d; + P[Y] = det(P1, Q) * d; + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// interval routines + +/* + * Map the sub-interval I in [0,1] into the interval J and assign it to J + */ +void map_to(Interval & J, Interval const& I) +{ + J.setEnds(J.valueAt(I.min()), J.valueAt(I.max())); +} + +//////////////////////////////////////////////////////////////////////////////// +// bezier curve routines + +/* + * Return true if all the Bezier curve control points are near, + * false otherwise + */ +// Bezier.isConstant(precision) +bool is_constant(std::vector const& A, double precision) +{ + for (unsigned int i = 1; i < A.size(); ++i) + { + if(!are_near(A[i], A[0], precision)) + return false; + } + return true; +} + +/* + * Compute the hodograph of the bezier curve B and return it in D + */ +// derivative(Bezier) +void derivative(std::vector & D, std::vector const& B) +{ + D.clear(); + size_t sz = B.size(); + if (sz == 0) return; + if (sz == 1) + { + D.resize(1, Point(0,0)); + return; + } + size_t n = sz-1; + D.reserve(n); + for (size_t i = 0; i < n; ++i) + { + D.push_back(n*(B[i+1] - B[i])); + } +} + +/* + * Compute the hodograph of the Bezier curve B rotated of 90 degree + * and return it in D; we have N(t) orthogonal to B(t) for any t + */ +// rot90(derivative(Bezier)) +void normal(std::vector & N, std::vector const& B) +{ + derivative(N,B); + for (size_t i = 0; i < N.size(); ++i) + { + N[i] = rot90(N[i]); + } +} + +/* + * Compute the portion of the Bezier curve "B" wrt the interval [0,t] + */ +// portion(Bezier, 0, t) +void left_portion(Coord t, std::vector & B) +{ + size_t n = B.size(); + for (size_t i = 1; i < n; ++i) + { + for (size_t j = n-1; j > i-1 ; --j) + { + B[j] = lerp(t, B[j-1], B[j]); + } + } +} + +/* + * Compute the portion of the Bezier curve "B" wrt the interval [t,1] + */ +// portion(Bezier, t, 1) +void right_portion(Coord t, std::vector & B) +{ + size_t n = B.size(); + for (size_t i = 1; i < n; ++i) + { + for (size_t j = 0; j < n-i; ++j) + { + B[j] = lerp(t, B[j], B[j+1]); + } + } +} + +/* + * Compute the portion of the Bezier curve "B" wrt the interval "I" + */ +// portion(Bezier, I) +void portion (std::vector & B , Interval const& I) +{ + if (I.min() == 0) + { + if (I.max() == 1) return; + left_portion(I.max(), B); + return; + } + right_portion(I.min(), B); + if (I.max() == 1) return; + double t = I.extent() / (1 - I.min()); + left_portion(t, B); +} + + +//////////////////////////////////////////////////////////////////////////////// +// tags + +struct intersection_point_tag; +struct collinear_normal_tag; +template +OptInterval clip(std::vector const& A, + std::vector const& B, + double precision); +template +void iterate(std::vector& domsA, + std::vector& domsB, + std::vector const& A, + std::vector const& B, + Interval const& domA, + Interval const& domB, + double precision ); + + +//////////////////////////////////////////////////////////////////////////////// +// intersection + +/* + * Make up an orientation line using the control points c[i] and c[j] + * the line is returned in the output parameter "l" in the form of a 3 element + * vector : l[0] * x + l[1] * y + l[2] == 0; the line is normalized. + */ +// Line(c[i], c[j]) +void orientation_line (std::vector & l, + std::vector const& c, + size_t i, size_t j) +{ + l[0] = c[j][Y] - c[i][Y]; + l[1] = c[i][X] - c[j][X]; + l[2] = cross(c[j], c[i]); + double length = std::sqrt(l[0] * l[0] + l[1] * l[1]); + assert (length != 0); + l[0] /= length; + l[1] /= length; + l[2] /= length; +} + +/* + * Pick up an orientation line for the Bezier curve "c" and return it in + * the output parameter "l" + */ +Line pick_orientation_line (std::vector const &c, double precision) +{ + size_t i = c.size(); + while (--i > 0 && are_near(c[0], c[i], precision)) + {} + + // this should never happen because when a new curve portion is created + // we check that it is not constant; + // however this requires that the precision used in the is_constant + // routine has to be the same used here in the are_near test + assert(i != 0); + + Line line(c[0], c[i]); + return line; + //std::cerr << "i = " << i << std::endl; +} + +/* + * Make up an orientation line for constant bezier curve; + * the orientation line is made up orthogonal to the other curve base line; + * the line is returned in the output parameter "l" in the form of a 3 element + * vector : l[0] * x + l[1] * y + l[2] == 0; the line is normalized. + */ +Line orthogonal_orientation_line (std::vector const &c, + Point const &p, + double precision) +{ + // this should never happen + assert(!is_constant(c, precision)); + + Line line(p, (c.back() - c.front()).cw() + p); + return line; +} + +/* + * Compute the signed distance of the point "P" from the normalized line l + */ +double signed_distance(Point const &p, Line const &l) +{ + Coord a, b, c; + l.coefficients(a, b, c); + return a * p[X] + b * p[Y] + c; +} + +/* + * Compute the min and max distance of the control points of the Bezier + * curve "c" from the normalized orientation line "l". + * This bounds are returned through the output Interval parameter"bound". + */ +Interval fat_line_bounds (std::vector const &c, + Line const &l) +{ + Interval bound(0, 0); + for (size_t i = 0; i < c.size(); ++i) { + bound.expandTo(signed_distance(c[i], l)); + } + return bound; +} + +/* + * return the x component of the intersection point between the line + * passing through points p1, p2 and the line Y = "y" + */ +double intersect (Point const& p1, Point const& p2, double y) +{ + // we are sure that p2[Y] != p1[Y] because this routine is called + // only when the lower or the upper bound is crossed + double dy = (p2[Y] - p1[Y]); + double s = (y - p1[Y]) / dy; + return (p2[X]-p1[X])*s + p1[X]; +} + +/* + * Clip the Bezier curve "B" wrt the fat line defined by the orientation + * line "l" and the interval range "bound", the new parameter interval for + * the clipped curve is returned through the output parameter "dom" + */ +OptInterval clip_interval (std::vector const& B, + Line const &l, + Interval const &bound) +{ + double n = B.size() - 1; // number of sub-intervals + std::vector D; // distance curve control points + D.reserve (B.size()); + for (size_t i = 0; i < B.size(); ++i) + { + const double d = signed_distance(B[i], l); + D.push_back (Point(i/n, d)); + } + //print(D); + + ConvexHull p; + p.swap(D); + //print(p); + + bool plower, phigher; + bool clower, chigher; + double t, tmin = 1, tmax = 0; +// std::cerr << "bound : " << bound << std::endl; + + plower = (p[0][Y] < bound.min()); + phigher = (p[0][Y] > bound.max()); + if (!(plower || phigher)) // inside the fat line + { + if (tmin > p[0][X]) tmin = p[0][X]; + if (tmax < p[0][X]) tmax = p[0][X]; +// std::cerr << "0 : inside " << p[0] +// << " : tmin = " << tmin << ", tmax = " << tmax << std::endl; + } + + for (size_t i = 1; i < p.size(); ++i) + { + clower = (p[i][Y] < bound.min()); + chigher = (p[i][Y] > bound.max()); + if (!(clower || chigher)) // inside the fat line + { + if (tmin > p[i][X]) tmin = p[i][X]; + if (tmax < p[i][X]) tmax = p[i][X]; +// std::cerr << i << " : inside " << p[i] +// << " : tmin = " << tmin << ", tmax = " << tmax +// << std::endl; + } + if (clower != plower) // cross the lower bound + { + t = intersect(p[i-1], p[i], bound.min()); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; + plower = clower; +// std::cerr << i << " : lower " << p[i] +// << " : tmin = " << tmin << ", tmax = " << tmax +// << std::endl; + } + if (chigher != phigher) // cross the upper bound + { + t = intersect(p[i-1], p[i], bound.max()); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; + phigher = chigher; +// std::cerr << i << " : higher " << p[i] +// << " : tmin = " << tmin << ", tmax = " << tmax +// << std::endl; + } + } + + // we have to test the closing segment for intersection + size_t last = p.size() - 1; + clower = (p[0][Y] < bound.min()); + chigher = (p[0][Y] > bound.max()); + if (clower != plower) // cross the lower bound + { + t = intersect(p[last], p[0], bound.min()); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; +// std::cerr << "0 : lower " << p[0] +// << " : tmin = " << tmin << ", tmax = " << tmax << std::endl; + } + if (chigher != phigher) // cross the upper bound + { + t = intersect(p[last], p[0], bound.max()); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; +// std::cerr << "0 : higher " << p[0] +// << " : tmin = " << tmin << ", tmax = " << tmax << std::endl; + } + + if (tmin == 1 && tmax == 0) { + return OptInterval(); + } else { + return Interval(tmin, tmax); + } +} + +/* + * Clip the Bezier curve "B" wrt the Bezier curve "A" for individuating + * intersection points the new parameter interval for the clipped curve + * is returned through the output parameter "dom" + */ +template <> +OptInterval clip (std::vector const& A, + std::vector const& B, + double precision) +{ + Line bl; + if (is_constant(A, precision)) { + Point M = middle_point(A.front(), A.back()); + bl = orthogonal_orientation_line(B, M, precision); + } else { + bl = pick_orientation_line(A, precision); + } + bl.normalize(); + Interval bound = fat_line_bounds(A, bl); + return clip_interval(B, bl, bound); +} + + +/////////////////////////////////////////////////////////////////////////////// +// collinear normal + +/* + * Compute a closed focus for the Bezier curve B and return it in F + * A focus is any curve through which all lines perpendicular to B(t) pass. + */ +void make_focus (std::vector & F, std::vector const& B) +{ + assert (B.size() > 2); + size_t n = B.size() - 1; + normal(F, B); + Point c(1, 1); +#if VERBOSE + if (!solve(c, F[0], -F[n-1], B[n]-B[0])) + { + std::cerr << "make_focus: unable to make up a closed focus" << std::endl; + } +#else + solve(c, F[0], -F[n-1], B[n]-B[0]); +#endif +// std::cerr << "c = " << c << std::endl; + + + // B(t) + c(t) * N(t) + double n_inv = 1 / (double)(n); + Point c0ni; + F.push_back(c[1] * F[n-1]); + F[n] += B[n]; + for (size_t i = n-1; i > 0; --i) + { + F[i] *= -c[0]; + c0ni = F[i]; + F[i] += (c[1] * F[i-1]); + F[i] *= (i * n_inv); + F[i] -= c0ni; + F[i] += B[i]; + } + F[0] *= c[0]; + F[0] += B[0]; +} + +/* + * Compute the projection on the plane (t, d) of the control points + * (t, u, D(t,u)) where D(t,u) = <(B(t) - F(u)), B'(t)> with 0 <= t, u <= 1 + * B is a Bezier curve and F is a focus of another Bezier curve. + * See Sederberg, Nishita, 1990 - Curve intersection using Bezier clipping. + */ +void distance_control_points (std::vector & D, + std::vector const& B, + std::vector const& F) +{ + assert (B.size() > 1); + assert (!F.empty()); + const size_t n = B.size() - 1; + const size_t m = F.size() - 1; + const size_t r = 2 * n - 1; + const double r_inv = 1 / (double)(r); + D.clear(); + D.reserve (B.size() * F.size()); + + std::vector dB; + dB.reserve(n); + for (size_t k = 0; k < n; ++k) + { + dB.push_back (B[k+1] - B[k]); + } + NL::Matrix dBB(n,B.size()); + for (size_t i = 0; i < n; ++i) + for (size_t j = 0; j < B.size(); ++j) + dBB(i,j) = dot (dB[i], B[j]); + NL::Matrix dBF(n, F.size()); + for (size_t i = 0; i < n; ++i) + for (size_t j = 0; j < F.size(); ++j) + dBF(i,j) = dot (dB[i], F[j]); + + size_t l; + double bc; + Point dij; + std::vector d(F.size()); + for (size_t i = 0; i <= r; ++i) + { + for (size_t j = 0; j <= m; ++j) + { + d[j] = 0; + } + const size_t k0 = std::max(i, n) - n; + const size_t kn = std::min(i, n-1); + const double bri = n / binomial(r,i); + for (size_t k = k0; k <= kn; ++k) + { + //if (k > i || (i-k) > n) continue; + l = i - k; +#if CHECK + assert (l <= n); +#endif + bc = bri * binomial(n,l) * binomial(n-1, k); + for (size_t j = 0; j <= m; ++j) + { + //d[j] += bc * dot(dB[k], B[l] - F[j]); + d[j] += bc * (dBB(k,l) - dBF(k,j)); + } + } + double dmin, dmax; + dmin = dmax = d[m]; + for (size_t j = 0; j < m; ++j) + { + if (dmin > d[j]) dmin = d[j]; + if (dmax < d[j]) dmax = d[j]; + } + dij[0] = i * r_inv; + dij[1] = dmin; + D.push_back (dij); + dij[1] = dmax; + D.push_back (dij); + } +} + +/* + * Clip the Bezier curve "B" wrt the focus "F"; the new parameter interval for + * the clipped curve is returned through the output parameter "dom" + */ +OptInterval clip_interval (std::vector const& B, + std::vector const& F) +{ + std::vector D; // distance curve control points + distance_control_points(D, B, F); + //print(D, "D"); +// ConvexHull chD(D); +// std::vector& p = chD.boundary; // convex hull vertices + + ConvexHull p; + p.swap(D); + //print(p, "CH(D)"); + + bool plower, clower; + double t, tmin = 1, tmax = 0; + + plower = (p[0][Y] < 0); + if (p[0][Y] == 0) // on the x axis + { + if (tmin > p[0][X]) tmin = p[0][X]; + if (tmax < p[0][X]) tmax = p[0][X]; +// std::cerr << "0 : on x axis " << p[0] +// << " : tmin = " << tmin << ", tmax = " << tmax << std::endl; + } + + for (size_t i = 1; i < p.size(); ++i) + { + clower = (p[i][Y] < 0); + if (p[i][Y] == 0) // on x axis + { + if (tmin > p[i][X]) tmin = p[i][X]; + if (tmax < p[i][X]) tmax = p[i][X]; +// std::cerr << i << " : on x axis " << p[i] +// << " : tmin = " << tmin << ", tmax = " << tmax +// << std::endl; + } + else if (clower != plower) // cross the x axis + { + t = intersect(p[i-1], p[i], 0); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; + plower = clower; +// std::cerr << i << " : lower " << p[i] +// << " : tmin = " << tmin << ", tmax = " << tmax +// << std::endl; + } + } + + // we have to test the closing segment for intersection + size_t last = p.size() - 1; + clower = (p[0][Y] < 0); + if (clower != plower) // cross the x axis + { + t = intersect(p[last], p[0], 0); + if (tmin > t) tmin = t; + if (tmax < t) tmax = t; +// std::cerr << "0 : lower " << p[0] +// << " : tmin = " << tmin << ", tmax = " << tmax << std::endl; + } + if (tmin == 1 && tmax == 0) { + return OptInterval(); + } else { + return Interval(tmin, tmax); + } +} + +/* + * Clip the Bezier curve "B" wrt the Bezier curve "A" for individuating + * points which have collinear normals; the new parameter interval + * for the clipped curve is returned through the output parameter "dom" + */ +template <> +OptInterval clip (std::vector const& A, + std::vector const& B, + double /*precision*/) +{ + std::vector F; + make_focus(F, A); + return clip_interval(B, F); +} + + + +const double MAX_PRECISION = 1e-8; +const double MIN_CLIPPED_SIZE_THRESHOLD = 0.8; +const Interval UNIT_INTERVAL(0,1); +const OptInterval EMPTY_INTERVAL; +const Interval H1_INTERVAL(0, 0.5); +const Interval H2_INTERVAL(nextafter(0.5, 1.0), 1.0); + +/* + * iterate + * + * input: + * A, B: control point sets of two bezier curves + * domA, domB: real parameter intervals of the two curves + * precision: required computational precision of the returned parameter ranges + * output: + * domsA, domsB: sets of parameter intervals + * + * The parameter intervals are computed by using a Bezier clipping algorithm, + * in case the clipping doesn't shrink the initial interval more than 20%, + * a subdivision step is performed. + * If during the computation both curves collapse to a single point + * the routine exits independently by the precision reached in the computation + * of the curve intervals. + */ +template <> +void iterate (std::vector& domsA, + std::vector& domsB, + std::vector const& A, + std::vector const& B, + Interval const& domA, + Interval const& domB, + double precision ) +{ + // in order to limit recursion + static size_t counter = 0; + if (domA.extent() == 1 && domB.extent() == 1) counter = 0; + if (++counter > 100) return; +#if VERBOSE + std::cerr << std::fixed << std::setprecision(16); + std::cerr << ">> curve subdision performed <<" << std::endl; + std::cerr << "dom(A) : " << domA << std::endl; + std::cerr << "dom(B) : " << domB << std::endl; +// std::cerr << "angle(A) : " << angle(A) << std::endl; +// std::cerr << "angle(B) : " << angle(B) << std::endl; +#endif + + if (precision < MAX_PRECISION) + precision = MAX_PRECISION; + + std::vector pA = A; + std::vector pB = B; + std::vector* C1 = &pA; + std::vector* C2 = &pB; + + Interval dompA = domA; + Interval dompB = domB; + Interval* dom1 = &dompA; + Interval* dom2 = &dompB; + + OptInterval dom; + + if ( is_constant(A, precision) && is_constant(B, precision) ){ + Point M1 = middle_point(C1->front(), C1->back()); + Point M2 = middle_point(C2->front(), C2->back()); + if (are_near(M1,M2)){ + domsA.push_back(domA); + domsB.push_back(domB); + } + return; + } + + size_t iter = 0; + while (++iter < 100 + && (dompA.extent() >= precision || dompB.extent() >= precision)) + { +#if VERBOSE + std::cerr << "iter: " << iter << std::endl; +#endif + dom = clip(*C1, *C2, precision); + + if (dom.empty()) + { +#if VERBOSE + std::cerr << "dom: empty" << std::endl; +#endif + return; + } +#if VERBOSE + std::cerr << "dom : " << dom << std::endl; +#endif + // all other cases where dom[0] > dom[1] are invalid + assert(dom->min() <= dom->max()); + + map_to(*dom2, *dom); + + portion(*C2, *dom); + if (is_constant(*C2, precision) && is_constant(*C1, precision)) + { + Point M1 = middle_point(C1->front(), C1->back()); + Point M2 = middle_point(C2->front(), C2->back()); +#if VERBOSE + std::cerr << "both curves are constant: \n" + << "M1: " << M1 << "\n" + << "M2: " << M2 << std::endl; + print(*C2, "C2"); + print(*C1, "C1"); +#endif + if (are_near(M1,M2)) + break; // append the new interval + else + return; // exit without appending any new interval + } + + + // if we have clipped less than 20% than we need to subdive the curve + // with the largest domain into two sub-curves + if (dom->extent() > MIN_CLIPPED_SIZE_THRESHOLD) + { +#if VERBOSE + std::cerr << "clipped less than 20% : " << dom->extent() << std::endl; + std::cerr << "angle(pA) : " << angle(pA) << std::endl; + std::cerr << "angle(pB) : " << angle(pB) << std::endl; +#endif + std::vector pC1, pC2; + Interval dompC1, dompC2; + if (dompA.extent() > dompB.extent()) + { + pC1 = pC2 = pA; + portion(pC1, H1_INTERVAL); + portion(pC2, H2_INTERVAL); + dompC1 = dompC2 = dompA; + map_to(dompC1, H1_INTERVAL); + map_to(dompC2, H2_INTERVAL); + iterate(domsA, domsB, pC1, pB, + dompC1, dompB, precision); + iterate(domsA, domsB, pC2, pB, + dompC2, dompB, precision); + } + else + { + pC1 = pC2 = pB; + portion(pC1, H1_INTERVAL); + portion(pC2, H2_INTERVAL); + dompC1 = dompC2 = dompB; + map_to(dompC1, H1_INTERVAL); + map_to(dompC2, H2_INTERVAL); + iterate(domsB, domsA, pC1, pA, + dompC1, dompA, precision); + iterate(domsB, domsA, pC2, pA, + dompC2, dompA, precision); + } + return; + } + + swap(C1, C2); + swap(dom1, dom2); +#if VERBOSE + std::cerr << "dom(pA) : " << dompA << std::endl; + std::cerr << "dom(pB) : " << dompB << std::endl; +#endif + } + domsA.push_back(dompA); + domsB.push_back(dompB); +} + + +/* + * iterate + * + * input: + * A, B: control point sets of two bezier curves + * domA, domB: real parameter intervals of the two curves + * precision: required computational precision of the returned parameter ranges + * output: + * domsA, domsB: sets of parameter intervals + * + * The parameter intervals are computed by using a Bezier clipping algorithm, + * in case the clipping doesn't shrink the initial interval more than 20%, + * a subdivision step is performed. + * If during the computation one of the two curve interval length becomes less + * than MAX_PRECISION the routine exits independently by the precision reached + * in the computation of the other curve interval. + */ +template <> +void iterate (std::vector& domsA, + std::vector& domsB, + std::vector const& A, + std::vector const& B, + Interval const& domA, + Interval const& domB, + double precision) +{ + // in order to limit recursion + static size_t counter = 0; + if (domA.extent() == 1 && domB.extent() == 1) counter = 0; + if (++counter > 100) return; +#if VERBOSE + std::cerr << std::fixed << std::setprecision(16); + std::cerr << ">> curve subdision performed <<" << std::endl; + std::cerr << "dom(A) : " << domA << std::endl; + std::cerr << "dom(B) : " << domB << std::endl; +// std::cerr << "angle(A) : " << angle(A) << std::endl; +// std::cerr << "angle(B) : " << angle(B) << std::endl; +#endif + + if (precision < MAX_PRECISION) + precision = MAX_PRECISION; + + std::vector pA = A; + std::vector pB = B; + std::vector* C1 = &pA; + std::vector* C2 = &pB; + + Interval dompA = domA; + Interval dompB = domB; + Interval* dom1 = &dompA; + Interval* dom2 = &dompB; + + OptInterval dom; + + size_t iter = 0; + while (++iter < 100 + && (dompA.extent() >= precision || dompB.extent() >= precision)) + { +#if VERBOSE + std::cerr << "iter: " << iter << std::endl; +#endif + dom = clip(*C1, *C2, precision); + + if (dom.empty()) { +#if VERBOSE + std::cerr << "dom: empty" << std::endl; +#endif + return; + } +#if VERBOSE + std::cerr << "dom : " << dom << std::endl; +#endif + assert(dom->min() <= dom->max()); + + map_to(*dom2, *dom); + + // it's better to stop before losing computational precision + if (iter > 1 && (dom2->extent() <= MAX_PRECISION)) + { +#if VERBOSE + std::cerr << "beyond max precision limit" << std::endl; +#endif + break; + } + + portion(*C2, *dom); + if (iter > 1 && is_constant(*C2, precision)) + { +#if VERBOSE + std::cerr << "new curve portion pC1 is constant" << std::endl; +#endif + break; + } + + + // if we have clipped less than 20% than we need to subdive the curve + // with the largest domain into two sub-curves + if ( dom->extent() > MIN_CLIPPED_SIZE_THRESHOLD) + { +#if VERBOSE + std::cerr << "clipped less than 20% : " << dom->extent() << std::endl; + std::cerr << "angle(pA) : " << angle(pA) << std::endl; + std::cerr << "angle(pB) : " << angle(pB) << std::endl; +#endif + std::vector pC1, pC2; + Interval dompC1, dompC2; + if (dompA.extent() > dompB.extent()) + { + if ((dompA.extent() / 2) < MAX_PRECISION) + { + break; + } + pC1 = pC2 = pA; + portion(pC1, H1_INTERVAL); + if (false && is_constant(pC1, precision)) + { +#if VERBOSE + std::cerr << "new curve portion pC1 is constant" << std::endl; +#endif + break; + } + portion(pC2, H2_INTERVAL); + if (is_constant(pC2, precision)) + { +#if VERBOSE + std::cerr << "new curve portion pC2 is constant" << std::endl; +#endif + break; + } + dompC1 = dompC2 = dompA; + map_to(dompC1, H1_INTERVAL); + map_to(dompC2, H2_INTERVAL); + iterate(domsA, domsB, pC1, pB, + dompC1, dompB, precision); + iterate(domsA, domsB, pC2, pB, + dompC2, dompB, precision); + } + else + { + if ((dompB.extent() / 2) < MAX_PRECISION) + { + break; + } + pC1 = pC2 = pB; + portion(pC1, H1_INTERVAL); + if (is_constant(pC1, precision)) + { +#if VERBOSE + std::cerr << "new curve portion pC1 is constant" << std::endl; +#endif + break; + } + portion(pC2, H2_INTERVAL); + if (is_constant(pC2, precision)) + { +#if VERBOSE + std::cerr << "new curve portion pC2 is constant" << std::endl; +#endif + break; + } + dompC1 = dompC2 = dompB; + map_to(dompC1, H1_INTERVAL); + map_to(dompC2, H2_INTERVAL); + iterate(domsB, domsA, pC1, pA, + dompC1, dompA, precision); + iterate(domsB, domsA, pC2, pA, + dompC2, dompA, precision); + } + return; + } + + swap(C1, C2); + swap(dom1, dom2); +#if VERBOSE + std::cerr << "dom(pA) : " << dompA << std::endl; + std::cerr << "dom(pB) : " << dompB << std::endl; +#endif + } + domsA.push_back(dompA); + domsB.push_back(dompB); +} + + +/* + * get_solutions + * + * input: A, B - set of control points of two Bezier curve + * input: precision - required precision of computation + * input: clip - the routine used for clipping + * output: xs - set of pairs of parameter values + * at which the clipping algorithm converges + * + * This routine is based on the Bezier Clipping Algorithm, + * see: Sederberg - Computer Aided Geometric Design + */ +template +void get_solutions (std::vector< std::pair >& xs, + std::vector const& A, + std::vector const& B, + double precision) +{ + std::pair ci; + std::vector domsA, domsB; + iterate (domsA, domsB, A, B, UNIT_INTERVAL, UNIT_INTERVAL, precision); + if (domsA.size() != domsB.size()) + { + assert (domsA.size() == domsB.size()); + } + xs.clear(); + xs.reserve(domsA.size()); + for (size_t i = 0; i < domsA.size(); ++i) + { +#if VERBOSE + std::cerr << i << " : domA : " << domsA[i] << std::endl; + std::cerr << "extent A: " << domsA[i].extent() << " "; + std::cerr << "precision A: " << get_precision(domsA[i]) << std::endl; + std::cerr << i << " : domB : " << domsB[i] << std::endl; + std::cerr << "extent B: " << domsB[i].extent() << " "; + std::cerr << "precision B: " << get_precision(domsB[i]) << std::endl; +#endif + ci.first = domsA[i].middle(); + ci.second = domsB[i].middle(); + xs.push_back(ci); + } +} + +} /* end namespace bezier_clipping */ } /* end namespace detail */ + + +/* + * find_collinear_normal + * + * input: A, B - set of control points of two Bezier curve + * input: precision - required precision of computation + * output: xs - set of pairs of parameter values + * at which there are collinear normals + * + * This routine is based on the Bezier Clipping Algorithm, + * see: Sederberg, Nishita, 1990 - Curve intersection using Bezier clipping + */ +void find_collinear_normal (std::vector< std::pair >& xs, + std::vector const& A, + std::vector const& B, + double precision) +{ + using detail::bezier_clipping::get_solutions; + using detail::bezier_clipping::collinear_normal_tag; + get_solutions(xs, A, B, precision); +} + + +/* + * find_intersections_bezier_clipping + * + * input: A, B - set of control points of two Bezier curve + * input: precision - required precision of computation + * output: xs - set of pairs of parameter values + * at which crossing happens + * + * This routine is based on the Bezier Clipping Algorithm, + * see: Sederberg, Nishita, 1990 - Curve intersection using Bezier clipping + */ +void find_intersections_bezier_clipping (std::vector< std::pair >& xs, + std::vector const& A, + std::vector const& B, + double precision) +{ + using detail::bezier_clipping::get_solutions; + using detail::bezier_clipping::intersection_point_tag; + get_solutions(xs, A, B, precision); +} + +} // end namespace Geom + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-curve.cpp b/src/2geom/bezier-curve.cpp new file mode 100644 index 0000000..73fafe4 --- /dev/null +++ b/src/2geom/bezier-curve.cpp @@ -0,0 +1,516 @@ +/* Bezier curve implementation + * + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/bezier-curve.h> +#include <2geom/path-sink.h> +#include <2geom/basic-intersection.h> +#include <2geom/nearest-time.h> + +namespace Geom +{ + +/** + * @class BezierCurve + * @brief Two-dimensional Bezier curve of arbitrary order. + * + * Bezier curves are an expansion of the concept of linear interpolation to n points. + * Linear segments in 2Geom are in fact Bezier curves of order 1. + * + * Let \f$\mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\ldots\mathbf{p}_n}\f$ denote a Bezier curve + * of order \f$n\f$ defined by the points \f$\mathbf{p}_0, \mathbf{p}_1, \ldots, \mathbf{p}_n\f$. + * Bezier curve of order 1 is a linear interpolation curve between two points, defined as + * \f[ \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1}(t) = (1-t)\mathbf{p}_0 + t\mathbf{p}_1 \f] + * If we now substitute points \f$\mathbf{p_0}\f$ and \f$\mathbf{p_1}\f$ in this definition + * by linear interpolations, we get the definition of a Bezier curve of order 2, also called + * a quadratic Bezier curve. + * \f{align*}{ \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\mathbf{p}_2}(t) + &= (1-t) \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1}(t) + t \mathbf{B}_{\mathbf{p}_1\mathbf{p}_2}(t) \\ + \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\mathbf{p}_2}(t) + &= (1-t)^2\mathbf{p}_0 + 2(1-t)t\mathbf{p}_1 + t^2\mathbf{p}_2 \f} + * By substituting points for quadratic Bezier curves in the original definition, + * we get a Bezier curve of order 3, called a cubic Bezier curve. + * \f{align*}{ \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\mathbf{p}_2\mathbf{p}_3}(t) + &= (1-t) \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\mathbf{p}_2}(t) + + t \mathbf{B}_{\mathbf{p}_1\mathbf{p}_2\mathbf{p}_3}(t) \\ + \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\mathbf{p}_2\mathbf{p}_3}(t) + &= (1-t)^3\mathbf{p}_0+3(1-t)^2t\mathbf{p}_1+3(1-t)t^2\mathbf{p}_2+t^3\mathbf{p}_3 \f} + * In general, a Bezier curve or order \f$n\f$ can be recursively defined as + * \f[ \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\ldots\mathbf{p}_n}(t) + = (1-t) \mathbf{B}_{\mathbf{p}_0\mathbf{p}_1\ldots\mathbf{p}_{n-1}}(t) + + t \mathbf{B}_{\mathbf{p}_1\mathbf{p}_2\ldots\mathbf{p}_n}(t) \f] + * + * This substitution can be repeated an arbitrary number of times. To picture this, imagine + * the evaluation of a point on the curve as follows: first, all control points are joined with + * straight lines, and a point corresponding to the selected time value is marked on them. + * Then, the marked points are joined with straight lines and the point corresponding to + * the time value is marked. This is repeated until only one marked point remains, which is the + * point at the selected time value. + * + * @image html bezier-curve-evaluation.png "Evaluation of the Bezier curve" + * + * An important property of the Bezier curves is that their parameters (control points) + * have an intuitive geometric interpretation. Because of this, they are frequently used + * in vector graphics editors. + * + * Every Bezier curve is contained in its control polygon (the convex polygon composed + * of its control points). This fact is useful for sweepline algorithms and intersection. + * + * @par Implementation notes + * The order of a Bezier curve is immuable once it has been created. Normally, you should + * know the order at compile time and use the BezierCurveN template. If you need to determine + * the order at runtime, use the BezierCurve::create() function. It will create a BezierCurveN + * for orders 1, 2 and 3 (up to cubic Beziers), so you can later dynamic_cast + * to those types, and for higher orders it will create an instance of BezierCurve. + * + * @relates BezierCurveN + * @ingroup Curves + */ + +/** + * @class BezierCurveN + * @brief Bezier curve with compile-time specified order. + * + * @tparam degree unsigned value indicating the order of the Bezier curve + * + * @relates BezierCurve + * @ingroup Curves + */ + + +BezierCurve::BezierCurve(std::vector const &pts) + : inner(pts) +{ + if (pts.size() < 2) { + THROW_RANGEERROR("Bezier curve must have at least 2 control points"); + } +} + +bool BezierCurve::isDegenerate() const +{ + for (unsigned d = 0; d < 2; ++d) { + Coord ic = inner[d][0]; + for (unsigned i = 1; i < size(); ++i) { + if (inner[d][i] != ic) return false; + } + } + return true; +} + +Coord BezierCurve::length(Coord tolerance) const +{ + switch (order()) + { + case 0: + return 0.0; + case 1: + return distance(initialPoint(), finalPoint()); + case 2: + { + std::vector pts = controlPoints(); + return bezier_length(pts[0], pts[1], pts[2], tolerance); + } + case 3: + { + std::vector pts = controlPoints(); + return bezier_length(pts[0], pts[1], pts[2], pts[3], tolerance); + } + default: + return bezier_length(controlPoints(), tolerance); + } +} + +std::vector +BezierCurve::intersect(Curve const &other, Coord eps) const +{ + std::vector result; + + // in case we encounter an order-1 curve created from a vector + // or a degenerate elliptical arc + if (isLineSegment()) { + LineSegment ls(initialPoint(), finalPoint()); + result = ls.intersect(other); + return result; + } + + // here we are sure that this curve is at least a quadratic Bezier + BezierCurve const *bez = dynamic_cast(&other); + if (bez) { + std::vector > xs; + find_intersections(xs, inner, bez->inner, eps); + for (unsigned i = 0; i < xs.size(); ++i) { + CurveIntersection x(*this, other, xs[i].first, xs[i].second); + result.push_back(x); + } + return result; + } + + // pass other intersection types to the other curve + result = other.intersect(*this, eps); + transpose_in_place(result); + return result; +} + +bool BezierCurve::isNear(Curve const &c, Coord precision) const +{ + if (this == &c) return true; + + BezierCurve const *other = dynamic_cast(&c); + if (!other) return false; + + if (!are_near(inner.at0(), other->inner.at0(), precision)) return false; + if (!are_near(inner.at1(), other->inner.at1(), precision)) return false; + + if (size() == other->size()) { + for (unsigned i = 1; i < order(); ++i) { + if (!are_near(inner.point(i), other->inner.point(i), precision)) { + return false; + } + } + return true; + } else { + // TODO: comparison after degree elevation + return false; + } +} + +bool BezierCurve::operator==(Curve const &c) const +{ + if (this == &c) return true; + + BezierCurve const *other = dynamic_cast(&c); + if (!other) return false; + if (size() != other->size()) return false; + + for (unsigned i = 0; i < size(); ++i) { + if (controlPoint(i) != other->controlPoint(i)) return false; + } + return true; +} + +Coord BezierCurve::nearestTime(Point const &p, Coord from, Coord to) const +{ + return nearest_time(p, inner, from, to); +} + +void BezierCurve::feed(PathSink &sink, bool moveto_initial) const +{ + if (size() > 4) { + Curve::feed(sink, moveto_initial); + return; + } + + Point ip = controlPoint(0); + if (moveto_initial) { + sink.moveTo(ip); + } + switch (size()) { + case 2: + sink.lineTo(controlPoint(1)); + break; + case 3: + sink.quadTo(controlPoint(1), controlPoint(2)); + break; + case 4: + sink.curveTo(controlPoint(1), controlPoint(2), controlPoint(3)); + break; + default: + // TODO: add a path sink method that accepts a vector of control points + // and converts to cubic spline by default + assert(false); + break; + } +} + +BezierCurve *BezierCurve::create(std::vector const &pts) +{ + switch (pts.size()) { + case 0: + case 1: + THROW_LOGICALERROR("BezierCurve::create: too few points in vector"); + return NULL; + case 2: + return new LineSegment(pts[0], pts[1]); + case 3: + return new QuadraticBezier(pts[0], pts[1], pts[2]); + case 4: + return new CubicBezier(pts[0], pts[1], pts[2], pts[3]); + default: + return new BezierCurve(pts); + } +} + +// optimized specializations for LineSegment + +template <> +Curve *BezierCurveN<1>::derivative() const { + double dx = inner[X][1] - inner[X][0], dy = inner[Y][1] - inner[Y][0]; + return new BezierCurveN<1>(Point(dx,dy),Point(dx,dy)); +} + +template<> +Coord BezierCurveN<1>::nearestTime(Point const& p, Coord from, Coord to) const +{ + using std::swap; + + if ( from > to ) swap(from, to); + Point ip = pointAt(from); + Point fp = pointAt(to); + Point v = fp - ip; + Coord l2v = L2sq(v); + if (l2v == 0) return 0; + Coord t = dot( p - ip, v ) / l2v; + if ( t <= 0 ) return from; + else if ( t >= 1 ) return to; + else return from + t*(to-from); +} + +template <> +std::vector BezierCurveN<1>::intersect(Curve const &other, Coord eps) const +{ + std::vector result; + + // only handle intersections with other LineSegments here + if (other.isLineSegment()) { + Line this_line(initialPoint(), finalPoint()); + Line other_line(other.initialPoint(), other.finalPoint()); + result = this_line.intersect(other_line); + filter_line_segment_intersections(result, true, true); + return result; + } + + // pass all other types to the other curve + result = other.intersect(*this, eps); + transpose_in_place(result); + return result; +} + +template <> +int BezierCurveN<1>::winding(Point const &p) const +{ + Point ip = inner.at0(), fp = inner.at1(); + if (p[Y] == std::max(ip[Y], fp[Y])) return 0; + + Point v = fp - ip; + assert(v[Y] != 0); + Coord t = (p[Y] - ip[Y]) / v[Y]; + assert(t >= 0 && t <= 1); + Coord xcross = lerp(t, ip[X], fp[X]); + if (xcross > p[X]) { + return v[Y] > 0 ? 1 : -1; + } + return 0; +} + +template <> +void BezierCurveN<1>::feed(PathSink &sink, bool moveto_initial) const +{ + if (moveto_initial) { + sink.moveTo(controlPoint(0)); + } + sink.lineTo(controlPoint(1)); +} + +template <> +void BezierCurveN<2>::feed(PathSink &sink, bool moveto_initial) const +{ + if (moveto_initial) { + sink.moveTo(controlPoint(0)); + } + sink.quadTo(controlPoint(1), controlPoint(2)); +} + +template <> +void BezierCurveN<3>::feed(PathSink &sink, bool moveto_initial) const +{ + if (moveto_initial) { + sink.moveTo(controlPoint(0)); + } + sink.curveTo(controlPoint(1), controlPoint(2), controlPoint(3)); +} + + +static Coord bezier_length_internal(std::vector &v1, Coord tolerance, int level) +{ + /* The Bezier length algorithm used in 2Geom utilizes a simple fact: + * the Bezier curve is longer than the distance between its endpoints + * but shorter than the length of the polyline formed by its control + * points. When the difference between the two values is smaller than the + * error tolerance, we can be sure that the true value is no further than + * 0.5 * tolerance from their arithmetic mean. When it's larger, we recursively + * subdivide the Bezier curve into two parts and add their lengths. + * + * We cap the maximum number of subdivisions at 256, which corresponds to 8 levels. + */ + Coord lower = distance(v1.front(), v1.back()); + Coord upper = 0.0; + for (size_t i = 0; i < v1.size() - 1; ++i) { + upper += distance(v1[i], v1[i+1]); + } + if (upper - lower <= 2*tolerance || level >= 8) { + return (lower + upper) / 2; + } + + + std::vector v2 = v1; + + /* Compute the right subdivision directly in v1 and the left one in v2. + * Explanation of the algorithm used: + * We have to compute the left and right edges of this triangle in which + * the top row are the control points of the Bezier curve, and each cell + * is equal to the arithmetic mean of the cells directly above it + * to the right and left. This corresponds to subdividing the Bezier curve + * at time value 0.5: the left edge has the control points of the first + * portion of the Bezier curve and the right edge - the second one. + * In the example we subdivide a curve with 5 control points (order 4). + * + * Start: + * 0 1 2 3 4 + * ? ? ? ? + * ? ? ? + * ? ? + * ? + * # means we have overwritten the value, ? means we don't know + * the value yet. Numbers mean the value is at i-th position in the vector. + * + * After loop with i==1 + * # 1 2 3 4 + * 0 ? ? ? -> write 0 to v2[1] + * ? ? ? + * ? ? + * ? + * + * After loop with i==2 + * # # 2 3 4 + * # 1 ? ? + * 0 ? ? -> write 0 to v2[2] + * ? ? + * ? + * + * After loop with i==3 + * # # # 3 4 + * # # 2 ? + * # 1 ? + * 0 ? -> write 0 to v2[3] + * ? + * + * After loop with i==4, we have the right edge of the triangle in v1, + * and we write the last value needed for the left edge in v2[4]. + */ + + for (size_t i = 1; i < v1.size(); ++i) { + for (size_t j = i; j > 0; --j) { + v1[j-1] = 0.5 * (v1[j-1] + v1[j]); + } + v2[i] = v1[0]; + } + + return bezier_length_internal(v1, 0.5 * tolerance, level + 1) + + bezier_length_internal(v2, 0.5 * tolerance, level + 1); +} + +/** @brief Compute the length of a bezier curve given by a vector of its control points + * @relatesalso BezierCurve */ +Coord bezier_length(std::vector const &points, Coord tolerance) +{ + if (points.size() < 2) return 0.0; + std::vector v1 = points; + return bezier_length_internal(v1, tolerance, 0); +} + +static Coord bezier_length_internal(Point a0, Point a1, Point a2, Coord tolerance, int level) +{ + Coord lower = distance(a0, a2); + Coord upper = distance(a0, a1) + distance(a1, a2); + + if (upper - lower <= 2*tolerance || level >= 8) { + return (lower + upper) / 2; + } + + Point // Casteljau subdivision + // b0 = a0, + // c0 = a2, + b1 = 0.5*(a0 + a1), + c1 = 0.5*(a1 + a2), + b2 = 0.5*(b1 + c1); // == c2 + return bezier_length_internal(a0, b1, b2, 0.5 * tolerance, level + 1) + + bezier_length_internal(b2, c1, a2, 0.5 * tolerance, level + 1); +} + +/** @brief Compute the length of a quadratic bezier curve given by its control points + * @relatesalso QuadraticBezier */ +Coord bezier_length(Point a0, Point a1, Point a2, Coord tolerance) +{ + return bezier_length_internal(a0, a1, a2, tolerance, 0); +} + +static Coord bezier_length_internal(Point a0, Point a1, Point a2, Point a3, Coord tolerance, int level) +{ + Coord lower = distance(a0, a3); + Coord upper = distance(a0, a1) + distance(a1, a2) + distance(a2, a3); + + if (upper - lower <= 2*tolerance || level >= 8) { + return (lower + upper) / 2; + } + + Point // Casteljau subdivision + // b0 = a0, + // c0 = a3, + b1 = 0.5*(a0 + a1), + t0 = 0.5*(a1 + a2), + c1 = 0.5*(a2 + a3), + b2 = 0.5*(b1 + t0), + c2 = 0.5*(t0 + c1), + b3 = 0.5*(b2 + c2); // == c3 + return bezier_length_internal(a0, b1, b2, b3, 0.5 * tolerance, level + 1) + + bezier_length_internal(b3, c2, c1, a3, 0.5 * tolerance, level + 1); +} + +/** @brief Compute the length of a cubic bezier curve given by its control points + * @relatesalso CubicBezier */ +Coord bezier_length(Point a0, Point a1, Point a2, Point a3, Coord tolerance) +{ + return bezier_length_internal(a0, a1, a2, a3, tolerance, 0); +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-curve.h b/src/2geom/bezier-curve.h new file mode 100644 index 0000000..9416ba7 --- /dev/null +++ b/src/2geom/bezier-curve.h @@ -0,0 +1,352 @@ +/** + * \file + * \brief Bezier curve + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_BEZIER_CURVE_H +#define LIB2GEOM_SEEN_BEZIER_CURVE_H + +#include <2geom/curve.h> +#include <2geom/sbasis-curve.h> // for non-native winding method +#include <2geom/bezier.h> +#include <2geom/transforms.h> + +namespace Geom +{ + +class BezierCurve : public Curve { +protected: + D2 inner; + BezierCurve() {} + BezierCurve(Bezier const &x, Bezier const &y) : inner(x, y) {} + BezierCurve(std::vector const &pts); + +public: + explicit BezierCurve(D2 const &b) : inner(b) {} + + /// @name Access and modify control points + /// @{ + /** @brief Get the order of the Bezier curve. + * A Bezier curve has order() + 1 control points. */ + unsigned order() const { return inner[X].order(); } + /** @brief Get the number of control points. */ + unsigned size() const { return inner[X].order() + 1; } + /** @brief Access control points of the curve. + * @param ix The (zero-based) index of the control point. Note that the caller is responsible for checking that this value is <= order(). + * @return The control point. No-reference return, use setPoint() to modify control points. */ + Point controlPoint(unsigned ix) const { return Point(inner[X][ix], inner[Y][ix]); } + Point operator[](unsigned ix) const { return Point(inner[X][ix], inner[Y][ix]); } + /** @brief Get the control points. + * @return Vector with order() + 1 control points. */ + std::vector controlPoints() const { return bezier_points(inner); } + D2 const &fragment() const { return inner; } + + /** @brief Modify a control point. + * @param ix The zero-based index of the point to modify. Note that the caller is responsible for checking that this value is <= order(). + * @param v The new value of the point */ + void setPoint(unsigned ix, Point const &v) { + inner[X][ix] = v[X]; + inner[Y][ix] = v[Y]; + } + /** @brief Set new control points. + * @param ps Vector which must contain order() + 1 points. + * Note that the caller is responsible for checking the size of this vector. + * @throws LogicalError Thrown when the size of the vector does not match the order. */ + virtual void setPoints(std::vector const &ps) { + // must be virtual, because HLineSegment will need to redefine it + if (ps.size() != order() + 1) + THROW_LOGICALERROR("BezierCurve::setPoints: incorrect number of points in vector"); + for(unsigned i = 0; i <= order(); i++) { + setPoint(i, ps[i]); + } + } + /// @} + + /// @name Construct a Bezier curve with runtime-determined order. + /// @{ + /** @brief Construct a curve from a vector of control points. + * This will construct the appropriate specialization of BezierCurve (i.e. LineSegment, + * QuadraticBezier or Cubic Bezier) if the number of control points in the passed vector + * does not exceed 4. */ + static BezierCurve *create(std::vector const &pts); + /// @} + + // implementation of virtual methods goes here + virtual Point initialPoint() const { return inner.at0(); } + virtual Point finalPoint() const { return inner.at1(); } + virtual bool isDegenerate() const; + virtual bool isLineSegment() const { return size() == 2; } + virtual void setInitial(Point const &v) { setPoint(0, v); } + virtual void setFinal(Point const &v) { setPoint(order(), v); } + virtual Rect boundsFast() const { return *bounds_fast(inner); } + virtual Rect boundsExact() const { return *bounds_exact(inner); } + virtual OptRect boundsLocal(OptInterval const &i, unsigned deg) const { + if (!i) return OptRect(); + if(i->min() == 0 && i->max() == 1) return boundsFast(); + if(deg == 0) return bounds_local(inner, i); + // TODO: UUUUUUGGGLLY + if(deg == 1 && order() > 1) return OptRect(bounds_local(Geom::derivative(inner[X]), i), + bounds_local(Geom::derivative(inner[Y]), i)); + return OptRect(); + } + virtual Curve *duplicate() const { + return new BezierCurve(*this); + } + virtual Curve *portion(Coord f, Coord t) const { + return new BezierCurve(Geom::portion(inner, f, t)); + } + virtual Curve *reverse() const { + return new BezierCurve(Geom::reverse(inner)); + } + + using Curve::operator*=; + virtual void operator*=(Translate const &tr) { + for (unsigned i = 0; i < size(); ++i) { + inner[X][i] += tr[X]; + inner[Y][i] += tr[Y]; + } + } + virtual void operator*=(Scale const &s) { + for (unsigned i = 0; i < size(); ++i) { + inner[X][i] *= s[X]; + inner[Y][i] *= s[Y]; + } + } + virtual void operator*=(Affine const &m) { + for (unsigned i = 0; i < size(); ++i) { + setPoint(i, controlPoint(i) * m); + } + } + + virtual Curve *derivative() const { + return new BezierCurve(Geom::derivative(inner[X]), Geom::derivative(inner[Y])); + } + virtual int degreesOfFreedom() const { + return 2 * (order() + 1); + } + virtual std::vector roots(Coord v, Dim2 d) const { + return (inner[d] - v).roots(); + } + virtual Coord nearestTime(Point const &p, Coord from = 0, Coord to = 1) const; + virtual Coord length(Coord tolerance) const; + virtual std::vector intersect(Curve const &other, Coord eps = EPSILON) const; + virtual Point pointAt(Coord t) const { return inner.pointAt(t); } + virtual std::vector pointAndDerivatives(Coord t, unsigned n) const { + return inner.valueAndDerivatives(t, n); + } + virtual Coord valueAt(Coord t, Dim2 d) const { return inner[d].valueAt(t); } + virtual D2 toSBasis() const {return inner.toSBasis(); } + virtual bool isNear(Curve const &c, Coord precision) const; + virtual bool operator==(Curve const &c) const; + virtual void feed(PathSink &sink, bool) const; +}; + +template +class BezierCurveN + : public BezierCurve +{ + template + static void assert_degree(BezierCurveN const *) {} + +public: + /// @name Construct Bezier curves + /// @{ + /** @brief Construct a Bezier curve of the specified order with all points zero. */ + BezierCurveN() { + inner = D2(Bezier(Bezier::Order(degree)), Bezier(Bezier::Order(degree))); + } + + /** @brief Construct from 2D Bezier polynomial. */ + explicit BezierCurveN(D2 const &x) { + inner = x; + } + + /** @brief Construct from two 1D Bezier polynomials of the same order. */ + BezierCurveN(Bezier x, Bezier y) { + inner = D2 (x,y); + } + + /** @brief Construct a Bezier curve from a vector of its control points. */ + BezierCurveN(std::vector const &points) { + unsigned ord = points.size() - 1; + if (ord != degree) THROW_LOGICALERROR("BezierCurve does not match number of points"); + for (unsigned d = 0; d < 2; ++d) { + inner[d] = Bezier(Bezier::Order(ord)); + for(unsigned i = 0; i <= ord; i++) + inner[d][i] = points[i][d]; + } + } + + /** @brief Construct a linear segment from its endpoints. */ + BezierCurveN(Point c0, Point c1) { + assert_degree<1>(this); + for(unsigned d = 0; d < 2; d++) + inner[d] = Bezier(c0[d], c1[d]); + } + + /** @brief Construct a quadratic Bezier curve from its control points. */ + BezierCurveN(Point c0, Point c1, Point c2) { + assert_degree<2>(this); + for(unsigned d = 0; d < 2; d++) + inner[d] = Bezier(c0[d], c1[d], c2[d]); + } + + /** @brief Construct a cubic Bezier curve from its control points. */ + BezierCurveN(Point c0, Point c1, Point c2, Point c3) { + assert_degree<3>(this); + for(unsigned d = 0; d < 2; d++) + inner[d] = Bezier(c0[d], c1[d], c2[d], c3[d]); + } + + // default copy + // default assign + + /// @} + + /** @brief Divide a Bezier curve into two curves + * @param t Time value + * @return Pair of Bezier curves \f$(\mathbf{D}, \mathbf{E})\f$ such that + * \f$\mathbf{D}[ [0,1] ] = \mathbf{C}[ [0,t] ]\f$ and + * \f$\mathbf{E}[ [0,1] ] = \mathbf{C}[ [t,1] ]\f$ */ + std::pair subdivide(Coord t) const { + std::pair sx = inner[X].subdivide(t), sy = inner[Y].subdivide(t); + return std::make_pair( + BezierCurveN(sx.first, sy.first), + BezierCurveN(sx.second, sy.second)); + } + + virtual bool isDegenerate() const { + return BezierCurve::isDegenerate(); + } + + virtual bool isLineSegment() const { + return size() == 2; + } + + virtual Curve *duplicate() const { + return new BezierCurveN(*this); + } + virtual Curve *portion(Coord f, Coord t) const { + if (degree == 1) { + return new BezierCurveN<1>(pointAt(f), pointAt(t)); + } else { + return new BezierCurveN(Geom::portion(inner, f, t)); + } + } + virtual Curve *reverse() const { + if (degree == 1) { + return new BezierCurveN<1>(finalPoint(), initialPoint()); + } else { + return new BezierCurveN(Geom::reverse(inner)); + } + } + virtual Curve *derivative() const; + + virtual Coord nearestTime(Point const &p, Coord from = 0, Coord to = 1) const { + return BezierCurve::nearestTime(p, from, to); + } + virtual std::vector intersect(Curve const &other, Coord eps = EPSILON) const { + // call super. this is implemented only to allow specializations + return BezierCurve::intersect(other, eps); + } + virtual int winding(Point const &p) const { + return Curve::winding(p); + } + virtual void feed(PathSink &sink, bool moveto_initial) const { + // call super. this is implemented only to allow specializations + BezierCurve::feed(sink, moveto_initial); + } +}; + +// BezierCurveN<0> is meaningless; specialize it out +template<> class BezierCurveN<0> : public BezierCurveN<1> { private: BezierCurveN();}; + +/** @brief Line segment. + * Line segments are Bezier curves of order 1. They have only two control points, + * the starting point and the ending point. + * @ingroup Curves */ +typedef BezierCurveN<1> LineSegment; + +/** @brief Quadratic (order 2) Bezier curve. + * @ingroup Curves */ +typedef BezierCurveN<2> QuadraticBezier; + +/** @brief Cubic (order 3) Bezier curve. + * @ingroup Curves */ +typedef BezierCurveN<3> CubicBezier; + +template +inline +Curve *BezierCurveN::derivative() const { + return new BezierCurveN(Geom::derivative(inner[X]), Geom::derivative(inner[Y])); +} + +// optimized specializations +template <> inline bool BezierCurveN<1>::isDegenerate() const { + return inner[X][0] == inner[X][1] && inner[Y][0] == inner[Y][1]; +} +template <> inline bool BezierCurveN<1>::isLineSegment() const { return true; } +template <> Curve *BezierCurveN<1>::derivative() const; +template <> Coord BezierCurveN<1>::nearestTime(Point const &, Coord, Coord) const; +template <> std::vector BezierCurveN<1>::intersect(Curve const &, Coord) const; +template <> int BezierCurveN<1>::winding(Point const &) const; +template <> void BezierCurveN<1>::feed(PathSink &sink, bool moveto_initial) const; +template <> void BezierCurveN<2>::feed(PathSink &sink, bool moveto_initial) const; +template <> void BezierCurveN<3>::feed(PathSink &sink, bool moveto_initial) const; + +inline Point middle_point(LineSegment const& _segment) { + return ( _segment.initialPoint() + _segment.finalPoint() ) / 2; +} + +inline Coord length(LineSegment const& seg) { + return distance(seg.initialPoint(), seg.finalPoint()); +} + +Coord bezier_length(std::vector const &points, Coord tolerance = 0.01); +Coord bezier_length(Point p0, Point p1, Point p2, Coord tolerance = 0.01); +Coord bezier_length(Point p0, Point p1, Point p2, Point p3, Coord tolerance = 0.01); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_BEZIER_CURVE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-to-sbasis.h b/src/2geom/bezier-to-sbasis.h new file mode 100644 index 0000000..73c55d9 --- /dev/null +++ b/src/2geom/bezier-to-sbasis.h @@ -0,0 +1,94 @@ +/** + * \file + * \brief Conversion between Bezier control points and SBasis curves + *//* + * Copyright 2006 Nathan Hurst + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_BEZIER_TO_SBASIS_H +#define LIB2GEOM_SEEN_BEZIER_TO_SBASIS_H + +#include <2geom/coord.h> +#include <2geom/point.h> +#include <2geom/d2.h> +#include <2geom/sbasis-to-bezier.h> + +namespace Geom +{ + +#if 0 +inline SBasis bezier_to_sbasis(Coord const *handles, unsigned order) { + if(order == 0) + return Linear(handles[0]); + else if(order == 1) + return Linear(handles[0], handles[1]); + else + return multiply(Linear(1, 0), bezier_to_sbasis(handles, order-1)) + + multiply(Linear(0, 1), bezier_to_sbasis(handles+1, order-1)); +} + + +template +inline D2 handles_to_sbasis(T const &handles, unsigned order) +{ + double v[2][order+1]; + for(unsigned i = 0; i <= order; i++) + for(unsigned j = 0; j < 2; j++) + v[j][i] = handles[i][j]; + return D2(bezier_to_sbasis(v[0], order), + bezier_to_sbasis(v[1], order)); +} +#endif + + +template +inline +D2 handles_to_sbasis(T const& handles, unsigned order) +{ + D2 sbc; + size_t sz = order + 1; + std::vector v; + v.reserve(sz); + for (size_t i = 0; i < sz; ++i) + v.push_back(handles[i]); + bezier_to_sbasis(sbc, v); + return sbc; +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_BEZIER_TO_SBASIS_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-utils.cpp b/src/2geom/bezier-utils.cpp new file mode 100644 index 0000000..181b5b3 --- /dev/null +++ b/src/2geom/bezier-utils.cpp @@ -0,0 +1,997 @@ +/* Bezier interpolation for inkscape drawing code. + * + * Original code published in: + * An Algorithm for Automatically Fitting Digitized Curves + * by Philip J. Schneider + * "Graphics Gems", Academic Press, 1990 + * + * Authors: + * Philip J. Schneider + * Lauris Kaplinski + * Peter Moulder + * + * Copyright (C) 1990 Philip J. Schneider + * Copyright (C) 2001 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2003,2004 Monash University + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#define SP_HUGE 1e5 +#define noBEZIER_DEBUG + +#ifdef HAVE_IEEEFP_H +# include +#endif + +#include <2geom/bezier-utils.h> +#include <2geom/math-utils.h> +#include + +namespace Geom { + +/* Forward declarations */ +static void generate_bezier(Point b[], Point const d[], double const u[], unsigned len, + Point const &tHat1, Point const &tHat2, double tolerance_sq); +static void estimate_lengths(Point bezier[], + Point const data[], double const u[], unsigned len, + Point const &tHat1, Point const &tHat2); +static void estimate_bi(Point b[4], unsigned ei, + Point const data[], double const u[], unsigned len); +static void reparameterize(Point const d[], unsigned len, double u[], Point const bezCurve[]); +static double NewtonRaphsonRootFind(Point const Q[], Point const &P, double u); +static Point darray_center_tangent(Point const d[], unsigned center, unsigned length); +static Point darray_right_tangent(Point const d[], unsigned const len); +static unsigned copy_without_nans_or_adjacent_duplicates(Point const src[], unsigned src_len, Point dest[]); +static void chord_length_parameterize(Point const d[], double u[], unsigned len); +static double compute_max_error_ratio(Point const d[], double const u[], unsigned len, + Point const bezCurve[], double tolerance, + unsigned *splitPoint); +static double compute_hook(Point const &a, Point const &b, double const u, Point const bezCurve[], + double const tolerance); + + +static Point const unconstrained_tangent(0, 0); + + +/* + * B0, B1, B2, B3 : Bezier multipliers + */ + +#define B0(u) ( ( 1.0 - u ) * ( 1.0 - u ) * ( 1.0 - u ) ) +#define B1(u) ( 3 * u * ( 1.0 - u ) * ( 1.0 - u ) ) +#define B2(u) ( 3 * u * u * ( 1.0 - u ) ) +#define B3(u) ( u * u * u ) + +#ifdef BEZIER_DEBUG +# define DOUBLE_ASSERT(x) assert( ( (x) > -SP_HUGE ) && ( (x) < SP_HUGE ) ) +# define BEZIER_ASSERT(b) do { \ + DOUBLE_ASSERT((b)[0][X]); DOUBLE_ASSERT((b)[0][Y]); \ + DOUBLE_ASSERT((b)[1][X]); DOUBLE_ASSERT((b)[1][Y]); \ + DOUBLE_ASSERT((b)[2][X]); DOUBLE_ASSERT((b)[2][Y]); \ + DOUBLE_ASSERT((b)[3][X]); DOUBLE_ASSERT((b)[3][Y]); \ + } while(0) +#else +# define DOUBLE_ASSERT(x) do { } while(0) +# define BEZIER_ASSERT(b) do { } while(0) +#endif + + +/** + * Fit a single-segment Bezier curve to a set of digitized points. + * + * \return Number of segments generated, or -1 on error. + */ +int +bezier_fit_cubic(Point *bezier, Point const *data, int len, double error) +{ + return bezier_fit_cubic_r(bezier, data, len, error, 1); +} + +/** + * Fit a multi-segment Bezier curve to a set of digitized points, with + * possible weedout of identical points and NaNs. + * + * \param max_beziers Maximum number of generated segments + * \param Result array, must be large enough for n. segments * 4 elements. + * + * \return Number of segments generated, or -1 on error. + */ +int +bezier_fit_cubic_r(Point bezier[], Point const data[], int const len, double const error, unsigned const max_beziers) +{ + if(bezier == NULL || + data == NULL || + len <= 0 || + max_beziers >= (1ul << (31 - 2 - 1 - 3))) + return -1; + + Point *uniqued_data = new Point[len]; + unsigned uniqued_len = copy_without_nans_or_adjacent_duplicates(data, len, uniqued_data); + + if ( uniqued_len < 2 ) { + delete[] uniqued_data; + return 0; + } + + /* Call fit-cubic function with recursion. */ + int const ret = bezier_fit_cubic_full(bezier, NULL, uniqued_data, uniqued_len, + unconstrained_tangent, unconstrained_tangent, + error, max_beziers); + delete[] uniqued_data; + return ret; +} + +/** + * Copy points from src to dest, filter out points containing NaN and + * adjacent points with equal x and y. + * \return length of dest + */ +static unsigned +copy_without_nans_or_adjacent_duplicates(Point const src[], unsigned src_len, Point dest[]) +{ + unsigned si = 0; + for (;;) { + if ( si == src_len ) { + return 0; + } + if (!std::isnan(src[si][X]) && + !std::isnan(src[si][Y])) { + dest[0] = Point(src[si]); + ++si; + break; + } + si++; + } + unsigned di = 0; + for (; si < src_len; ++si) { + Point const src_pt = Point(src[si]); + if ( src_pt != dest[di] + && !std::isnan(src_pt[X]) + && !std::isnan(src_pt[Y])) { + dest[++di] = src_pt; + } + } + unsigned dest_len = di + 1; + assert( dest_len <= src_len ); + return dest_len; +} + +/** + * Fit a multi-segment Bezier curve to a set of digitized points, without + * possible weedout of identical points and NaNs. + * + * \pre data is uniqued, i.e. not exist i: data[i] == data[i + 1]. + * \param max_beziers Maximum number of generated segments + * \param Result array, must be large enough for n. segments * 4 elements. + */ +int +bezier_fit_cubic_full(Point bezier[], int split_points[], + Point const data[], int const len, + Point const &tHat1, Point const &tHat2, + double const error, unsigned const max_beziers) +{ + if(!(bezier != NULL) || + !(data != NULL) || + !(len > 0) || + !(max_beziers >= 1) || + !(error >= 0.0)) + return -1; + + if ( len < 2 ) return 0; + + if ( len == 2 ) { + /* We have 2 points, which can be fitted trivially. */ + bezier[0] = data[0]; + bezier[3] = data[len - 1]; + double const dist = distance(bezier[0], bezier[3]) / 3.0; + if (std::isnan(dist)) { + /* Numerical problem, fall back to straight line segment. */ + bezier[1] = bezier[0]; + bezier[2] = bezier[3]; + } else { + bezier[1] = ( is_zero(tHat1) + ? ( 2 * bezier[0] + bezier[3] ) / 3. + : bezier[0] + dist * tHat1 ); + bezier[2] = ( is_zero(tHat2) + ? ( bezier[0] + 2 * bezier[3] ) / 3. + : bezier[3] + dist * tHat2 ); + } + BEZIER_ASSERT(bezier); + return 1; + } + + /* Parameterize points, and attempt to fit curve */ + unsigned splitPoint; /* Point to split point set at. */ + bool is_corner; + { + double *u = new double[len]; + chord_length_parameterize(data, u, len); + if ( u[len - 1] == 0.0 ) { + /* Zero-length path: every point in data[] is the same. + * + * (Clients aren't allowed to pass such data; handling the case is defensive + * programming.) + */ + delete[] u; + return 0; + } + + generate_bezier(bezier, data, u, len, tHat1, tHat2, error); + reparameterize(data, len, u, bezier); + + /* Find max deviation of points to fitted curve. */ + double const tolerance = sqrt(error + 1e-9); + double maxErrorRatio = compute_max_error_ratio(data, u, len, bezier, tolerance, &splitPoint); + + if ( fabs(maxErrorRatio) <= 1.0 ) { + BEZIER_ASSERT(bezier); + delete[] u; + return 1; + } + + /* If error not too large, then try some reparameterization and iteration. */ + if ( 0.0 <= maxErrorRatio && maxErrorRatio <= 3.0 ) { + int const maxIterations = 4; /* std::max times to try iterating */ + for (int i = 0; i < maxIterations; i++) { + generate_bezier(bezier, data, u, len, tHat1, tHat2, error); + reparameterize(data, len, u, bezier); + maxErrorRatio = compute_max_error_ratio(data, u, len, bezier, tolerance, &splitPoint); + if ( fabs(maxErrorRatio) <= 1.0 ) { + BEZIER_ASSERT(bezier); + delete[] u; + return 1; + } + } + } + delete[] u; + is_corner = (maxErrorRatio < 0); + } + + if (is_corner) { + assert(splitPoint < unsigned(len)); + if (splitPoint == 0) { + if (is_zero(tHat1)) { + /* Got spike even with unconstrained initial tangent. */ + ++splitPoint; + } else { + return bezier_fit_cubic_full(bezier, split_points, data, len, unconstrained_tangent, tHat2, + error, max_beziers); + } + } else if (splitPoint == unsigned(len - 1)) { + if (is_zero(tHat2)) { + /* Got spike even with unconstrained final tangent. */ + --splitPoint; + } else { + return bezier_fit_cubic_full(bezier, split_points, data, len, tHat1, unconstrained_tangent, + error, max_beziers); + } + } + } + + if ( 1 < max_beziers ) { + /* + * Fitting failed -- split at max error point and fit recursively + */ + unsigned const rec_max_beziers1 = max_beziers - 1; + + Point recTHat2, recTHat1; + if (is_corner) { + if(!(0 < splitPoint && splitPoint < unsigned(len - 1))) + return -1; + recTHat1 = recTHat2 = unconstrained_tangent; + } else { + /* Unit tangent vector at splitPoint. */ + recTHat2 = darray_center_tangent(data, splitPoint, len); + recTHat1 = -recTHat2; + } + int const nsegs1 = bezier_fit_cubic_full(bezier, split_points, data, splitPoint + 1, + tHat1, recTHat2, error, rec_max_beziers1); + if ( nsegs1 < 0 ) { +#ifdef BEZIER_DEBUG + g_print("fit_cubic[1]: recursive call failed\n"); +#endif + return -1; + } + assert( nsegs1 != 0 ); + if (split_points != NULL) { + split_points[nsegs1 - 1] = splitPoint; + } + unsigned const rec_max_beziers2 = max_beziers - nsegs1; + int const nsegs2 = bezier_fit_cubic_full(bezier + nsegs1*4, + ( split_points == NULL + ? NULL + : split_points + nsegs1 ), + data + splitPoint, len - splitPoint, + recTHat1, tHat2, error, rec_max_beziers2); + if ( nsegs2 < 0 ) { +#ifdef BEZIER_DEBUG + g_print("fit_cubic[2]: recursive call failed\n"); +#endif + return -1; + } + +#ifdef BEZIER_DEBUG + g_print("fit_cubic: success[nsegs: %d+%d=%d] on max_beziers:%u\n", + nsegs1, nsegs2, nsegs1 + nsegs2, max_beziers); +#endif + return nsegs1 + nsegs2; + } else { + return -1; + } +} + + +/** + * Fill in \a bezier[] based on the given data and tangent requirements, using + * a least-squares fit. + * + * Each of tHat1 and tHat2 should be either a zero vector or a unit vector. + * If it is zero, then bezier[1 or 2] is estimated without constraint; otherwise, + * it bezier[1 or 2] is placed in the specified direction from bezier[0 or 3]. + * + * \param tolerance_sq Used only for an initial guess as to tangent directions + * when \a tHat1 or \a tHat2 is zero. + */ +static void +generate_bezier(Point bezier[], + Point const data[], double const u[], unsigned const len, + Point const &tHat1, Point const &tHat2, + double const tolerance_sq) +{ + bool const est1 = is_zero(tHat1); + bool const est2 = is_zero(tHat2); + Point est_tHat1( est1 + ? darray_left_tangent(data, len, tolerance_sq) + : tHat1 ); + Point est_tHat2( est2 + ? darray_right_tangent(data, len, tolerance_sq) + : tHat2 ); + estimate_lengths(bezier, data, u, len, est_tHat1, est_tHat2); + /* We find that darray_right_tangent tends to produce better results + for our current freehand tool than full estimation. */ + if (est1) { + estimate_bi(bezier, 1, data, u, len); + if (bezier[1] != bezier[0]) { + est_tHat1 = unit_vector(bezier[1] - bezier[0]); + } + estimate_lengths(bezier, data, u, len, est_tHat1, est_tHat2); + } +} + + +static void +estimate_lengths(Point bezier[], + Point const data[], double const uPrime[], unsigned const len, + Point const &tHat1, Point const &tHat2) +{ + double C[2][2]; /* Matrix C. */ + double X[2]; /* Matrix X. */ + + /* Create the C and X matrices. */ + C[0][0] = 0.0; + C[0][1] = 0.0; + C[1][0] = 0.0; + C[1][1] = 0.0; + X[0] = 0.0; + X[1] = 0.0; + + /* First and last control points of the Bezier curve are positioned exactly at the first and + last data points. */ + bezier[0] = data[0]; + bezier[3] = data[len - 1]; + + for (unsigned i = 0; i < len; i++) { + /* Bezier control point coefficients. */ + double const b0 = B0(uPrime[i]); + double const b1 = B1(uPrime[i]); + double const b2 = B2(uPrime[i]); + double const b3 = B3(uPrime[i]); + + /* rhs for eqn */ + Point const a1 = b1 * tHat1; + Point const a2 = b2 * tHat2; + + C[0][0] += dot(a1, a1); + C[0][1] += dot(a1, a2); + C[1][0] = C[0][1]; + C[1][1] += dot(a2, a2); + + /* Additional offset to the data point from the predicted point if we were to set bezier[1] + to bezier[0] and bezier[2] to bezier[3]. */ + Point const shortfall + = ( data[i] + - ( ( b0 + b1 ) * bezier[0] ) + - ( ( b2 + b3 ) * bezier[3] ) ); + X[0] += dot(a1, shortfall); + X[1] += dot(a2, shortfall); + } + + /* We've constructed a pair of equations in the form of a matrix product C * alpha = X. + Now solve for alpha. */ + double alpha_l, alpha_r; + + /* Compute the determinants of C and X. */ + double const det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]; + if ( det_C0_C1 != 0 ) { + /* Apparently Kramer's rule. */ + double const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0]; + double const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; + alpha_l = det_X_C1 / det_C0_C1; + alpha_r = det_C0_X / det_C0_C1; + } else { + /* The matrix is under-determined. Try requiring alpha_l == alpha_r. + * + * One way of implementing the constraint alpha_l == alpha_r is to treat them as the same + * variable in the equations. We can do this by adding the columns of C to form a single + * column, to be multiplied by alpha to give the column vector X. + * + * We try each row in turn. + */ + double const c0 = C[0][0] + C[0][1]; + if (c0 != 0) { + alpha_l = alpha_r = X[0] / c0; + } else { + double const c1 = C[1][0] + C[1][1]; + if (c1 != 0) { + alpha_l = alpha_r = X[1] / c1; + } else { + /* Let the below code handle this. */ + alpha_l = alpha_r = 0.; + } + } + } + + /* If alpha negative, use the Wu/Barsky heuristic (see text). (If alpha is 0, you get + coincident control points that lead to divide by zero in any subsequent + NewtonRaphsonRootFind() call.) */ + /// \todo Check whether this special-casing is necessary now that + /// NewtonRaphsonRootFind handles non-positive denominator. + if ( alpha_l < 1.0e-6 || + alpha_r < 1.0e-6 ) + { + alpha_l = alpha_r = distance(data[0], data[len-1]) / 3.0; + } + + /* Control points 1 and 2 are positioned an alpha distance out on the tangent vectors, left and + right, respectively. */ + bezier[1] = alpha_l * tHat1 + bezier[0]; + bezier[2] = alpha_r * tHat2 + bezier[3]; + + return; +} + +static double lensq(Point const p) { + return dot(p, p); +} + +static void +estimate_bi(Point bezier[4], unsigned const ei, + Point const data[], double const u[], unsigned const len) +{ + if(!(1 <= ei && ei <= 2)) + return; + unsigned const oi = 3 - ei; + double num[2] = {0., 0.}; + double den = 0.; + for (unsigned i = 0; i < len; ++i) { + double const ui = u[i]; + double const b[4] = { + B0(ui), + B1(ui), + B2(ui), + B3(ui) + }; + + for (unsigned d = 0; d < 2; ++d) { + num[d] += b[ei] * (b[0] * bezier[0][d] + + b[oi] * bezier[oi][d] + + b[3] * bezier[3][d] + + - data[i][d]); + } + den -= b[ei] * b[ei]; + } + + if (den != 0.) { + for (unsigned d = 0; d < 2; ++d) { + bezier[ei][d] = num[d] / den; + } + } else { + bezier[ei] = ( oi * bezier[0] + ei * bezier[3] ) / 3.; + } +} + +/** + * Given set of points and their parameterization, try to find a better assignment of parameter + * values for the points. + * + * \param d Array of digitized points. + * \param u Current parameter values. + * \param bezCurve Current fitted curve. + * \param len Number of values in both d and u arrays. + * Also the size of the array that is allocated for return. + */ +static void +reparameterize(Point const d[], + unsigned const len, + double u[], + Point const bezCurve[]) +{ + assert( 2 <= len ); + + unsigned const last = len - 1; + assert( bezCurve[0] == d[0] ); + assert( bezCurve[3] == d[last] ); + assert( u[0] == 0.0 ); + assert( u[last] == 1.0 ); + /* Otherwise, consider including 0 and last in the below loop. */ + + for (unsigned i = 1; i < last; i++) { + u[i] = NewtonRaphsonRootFind(bezCurve, d[i], u[i]); + } +} + +/** + * Use Newton-Raphson iteration to find better root. + * + * \param Q Current fitted curve + * \param P Digitized point + * \param u Parameter value for "P" + * + * \return Improved u + */ +static double +NewtonRaphsonRootFind(Point const Q[], Point const &P, double const u) +{ + assert( 0.0 <= u ); + assert( u <= 1.0 ); + + /* Generate control vertices for Q'. */ + Point Q1[3]; + for (unsigned i = 0; i < 3; i++) { + Q1[i] = 3.0 * ( Q[i+1] - Q[i] ); + } + + /* Generate control vertices for Q''. */ + Point Q2[2]; + for (unsigned i = 0; i < 2; i++) { + Q2[i] = 2.0 * ( Q1[i+1] - Q1[i] ); + } + + /* Compute Q(u), Q'(u) and Q''(u). */ + Point const Q_u = bezier_pt(3, Q, u); + Point const Q1_u = bezier_pt(2, Q1, u); + Point const Q2_u = bezier_pt(1, Q2, u); + + /* Compute f(u)/f'(u), where f is the derivative wrt u of distsq(u) = 0.5 * the square of the + distance from P to Q(u). Here we're using Newton-Raphson to find a stationary point in the + distsq(u), hopefully corresponding to a local minimum in distsq (and hence a local minimum + distance from P to Q(u)). */ + Point const diff = Q_u - P; + double numerator = dot(diff, Q1_u); + double denominator = dot(Q1_u, Q1_u) + dot(diff, Q2_u); + + double improved_u; + if ( denominator > 0. ) { + /* One iteration of Newton-Raphson: + improved_u = u - f(u)/f'(u) */ + improved_u = u - ( numerator / denominator ); + } else { + /* Using Newton-Raphson would move in the wrong direction (towards a local maximum rather + than local minimum), so we move an arbitrary amount in the right direction. */ + if ( numerator > 0. ) { + improved_u = u * .98 - .01; + } else if ( numerator < 0. ) { + /* Deliberately asymmetrical, to reduce the chance of cycling. */ + improved_u = .031 + u * .98; + } else { + improved_u = u; + } + } + + if (!std::isfinite(improved_u)) { + improved_u = u; + } else if ( improved_u < 0.0 ) { + improved_u = 0.0; + } else if ( improved_u > 1.0 ) { + improved_u = 1.0; + } + + /* Ensure that improved_u isn't actually worse. */ + { + double const diff_lensq = lensq(diff); + for (double proportion = .125; ; proportion += .125) { + if ( lensq( bezier_pt(3, Q, improved_u) - P ) > diff_lensq ) { + if ( proportion > 1.0 ) { + //g_warning("found proportion %g", proportion); + improved_u = u; + break; + } + improved_u = ( ( 1 - proportion ) * improved_u + + proportion * u ); + } else { + break; + } + } + } + + DOUBLE_ASSERT(improved_u); + return improved_u; +} + +/** + * Evaluate a Bezier curve at parameter value \a t. + * + * \param degree The degree of the Bezier curve: 3 for cubic, 2 for quadratic etc. Must be less + * than 4. + * \param V The control points for the Bezier curve. Must have (\a degree+1) + * elements. + * \param t The "parameter" value, specifying whereabouts along the curve to + * evaluate. Typically in the range [0.0, 1.0]. + * + * Let s = 1 - t. + * BezierII(1, V) gives (s, t) * V, i.e. t of the way + * from V[0] to V[1]. + * BezierII(2, V) gives (s**2, 2*s*t, t**2) * V. + * BezierII(3, V) gives (s**3, 3 s**2 t, 3s t**2, t**3) * V. + * + * The derivative of BezierII(i, V) with respect to t + * is i * BezierII(i-1, V'), where for all j, V'[j] = + * V[j + 1] - V[j]. + */ +Point +bezier_pt(unsigned const degree, Point const V[], double const t) +{ + /** Pascal's triangle. */ + static int const pascal[4][4] = {{1, 0, 0, 0}, + {1, 1, 0, 0}, + {1, 2, 1, 0}, + {1, 3, 3, 1}}; + assert( degree < 4); + double const s = 1.0 - t; + + /* Calculate powers of t and s. */ + double spow[4]; + double tpow[4]; + spow[0] = 1.0; spow[1] = s; + tpow[0] = 1.0; tpow[1] = t; + for (unsigned i = 1; i < degree; ++i) { + spow[i + 1] = spow[i] * s; + tpow[i + 1] = tpow[i] * t; + } + + Point ret = spow[degree] * V[0]; + for (unsigned i = 1; i <= degree; ++i) { + ret += pascal[degree][i] * spow[degree - i] * tpow[i] * V[i]; + } + return ret; +} + +/* + * ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent : + * Approximate unit tangents at endpoints and "center" of digitized curve + */ + +/** + * Estimate the (forward) tangent at point d[first + 0.5]. + * + * Unlike the center and right versions, this calculates the tangent in + * the way one might expect, i.e., wrt increasing index into d. + * \pre (2 \<= len) and (d[0] != d[1]). + **/ +Point +darray_left_tangent(Point const d[], unsigned const len) +{ + assert( len >= 2 ); + assert( d[0] != d[1] ); + return unit_vector( d[1] - d[0] ); +} + +/** + * Estimates the (backward) tangent at d[last - 0.5]. + * + * \note The tangent is "backwards", i.e. it is with respect to + * decreasing index rather than increasing index. + * + * \pre 2 \<= len. + * \pre d[len - 1] != d[len - 2]. + * \pre all[p in d] in_svg_plane(p). + */ +static Point +darray_right_tangent(Point const d[], unsigned const len) +{ + assert( 2 <= len ); + unsigned const last = len - 1; + unsigned const prev = last - 1; + assert( d[last] != d[prev] ); + return unit_vector( d[prev] - d[last] ); +} + +/** + * Estimate the (forward) tangent at point d[0]. + * + * Unlike the center and right versions, this calculates the tangent in + * the way one might expect, i.e., wrt increasing index into d. + * + * \pre 2 \<= len. + * \pre d[0] != d[1]. + * \pre all[p in d] in_svg_plane(p). + * \post is_unit_vector(ret). + **/ +Point +darray_left_tangent(Point const d[], unsigned const len, double const tolerance_sq) +{ + assert( 2 <= len ); + assert( 0 <= tolerance_sq ); + for (unsigned i = 1;;) { + Point const pi(d[i]); + Point const t(pi - d[0]); + double const distsq = dot(t, t); + if ( tolerance_sq < distsq ) { + return unit_vector(t); + } + ++i; + if (i == len) { + return ( distsq == 0 + ? darray_left_tangent(d, len) + : unit_vector(t) ); + } + } +} + +/** + * Estimates the (backward) tangent at d[last]. + * + * \note The tangent is "backwards", i.e. it is with respect to + * decreasing index rather than increasing index. + * + * \pre 2 \<= len. + * \pre d[len - 1] != d[len - 2]. + * \pre all[p in d] in_svg_plane(p). + */ +Point +darray_right_tangent(Point const d[], unsigned const len, double const tolerance_sq) +{ + assert( 2 <= len ); + assert( 0 <= tolerance_sq ); + unsigned const last = len - 1; + for (unsigned i = last - 1;; i--) { + Point const pi(d[i]); + Point const t(pi - d[last]); + double const distsq = dot(t, t); + if ( tolerance_sq < distsq ) { + return unit_vector(t); + } + if (i == 0) { + return ( distsq == 0 + ? darray_right_tangent(d, len) + : unit_vector(t) ); + } + } +} + +/** + * Estimates the (backward) tangent at d[center], by averaging the two + * segments connected to d[center] (and then normalizing the result). + * + * \note The tangent is "backwards", i.e. it is with respect to + * decreasing index rather than increasing index. + * + * \pre (0 \< center \< len - 1) and d is uniqued (at least in + * the immediate vicinity of \a center). + */ +static Point +darray_center_tangent(Point const d[], + unsigned const center, + unsigned const len) +{ + assert( center != 0 ); + assert( center < len - 1 ); + + Point ret; + if ( d[center + 1] == d[center - 1] ) { + /* Rotate 90 degrees in an arbitrary direction. */ + Point const diff = d[center] - d[center - 1]; + ret = rot90(diff); + } else { + ret = d[center - 1] - d[center + 1]; + } + ret.normalize(); + return ret; +} + + +/** + * Assign parameter values to digitized points using relative distances between points. + * + * \pre Parameter array u must have space for \a len items. + */ +static void +chord_length_parameterize(Point const d[], double u[], unsigned const len) +{ + if(!( 2 <= len )) + return; + + /* First let u[i] equal the distance travelled along the path from d[0] to d[i]. */ + u[0] = 0.0; + for (unsigned i = 1; i < len; i++) { + double const dist = distance(d[i], d[i-1]); + u[i] = u[i-1] + dist; + } + + /* Then scale to [0.0 .. 1.0]. */ + double tot_len = u[len - 1]; + if(!( tot_len != 0 )) + return; + if (std::isfinite(tot_len)) { + for (unsigned i = 1; i < len; ++i) { + u[i] /= tot_len; + } + } else { + /* We could do better, but this probably never happens anyway. */ + for (unsigned i = 1; i < len; ++i) { + u[i] = i / (double) ( len - 1 ); + } + } + + /** \todo + * It's been reported that u[len - 1] can differ from 1.0 on some + * systems (amd64), despite it having been calculated as x / x where x + * is isFinite and non-zero. + */ + if (u[len - 1] != 1) { + double const diff = u[len - 1] - 1; + if (fabs(diff) > 1e-13) { + assert(0); // No warnings in 2geom + //g_warning("u[len - 1] = %19g (= 1 + %19g), expecting exactly 1", + // u[len - 1], diff); + } + u[len - 1] = 1; + } + +#ifdef BEZIER_DEBUG + assert( u[0] == 0.0 && u[len - 1] == 1.0 ); + for (unsigned i = 1; i < len; i++) { + assert( u[i] >= u[i-1] ); + } +#endif +} + + + + +/** + * Find the maximum squared distance of digitized points to fitted curve, and (if this maximum + * error is non-zero) set \a *splitPoint to the corresponding index. + * + * \pre 2 \<= len. + * \pre u[0] == 0. + * \pre u[len - 1] == 1.0. + * \post ((ret == 0.0) + * || ((*splitPoint \< len - 1) + * \&\& (*splitPoint != 0 || ret \< 0.0))). + */ +static double +compute_max_error_ratio(Point const d[], double const u[], unsigned const len, + Point const bezCurve[], double const tolerance, + unsigned *const splitPoint) +{ + assert( 2 <= len ); + unsigned const last = len - 1; + assert( bezCurve[0] == d[0] ); + assert( bezCurve[3] == d[last] ); + assert( u[0] == 0.0 ); + assert( u[last] == 1.0 ); + /* I.e. assert that the error for the first & last points is zero. + * Otherwise we should include those points in the below loop. + * The assertion is also necessary to ensure 0 < splitPoint < last. + */ + + double maxDistsq = 0.0; /* Maximum error */ + double max_hook_ratio = 0.0; + unsigned snap_end = 0; + Point prev = bezCurve[0]; + for (unsigned i = 1; i <= last; i++) { + Point const curr = bezier_pt(3, bezCurve, u[i]); + double const distsq = lensq( curr - d[i] ); + if ( distsq > maxDistsq ) { + maxDistsq = distsq; + *splitPoint = i; + } + double const hook_ratio = compute_hook(prev, curr, .5 * (u[i - 1] + u[i]), bezCurve, tolerance); + if (max_hook_ratio < hook_ratio) { + max_hook_ratio = hook_ratio; + snap_end = i; + } + prev = curr; + } + + double const dist_ratio = sqrt(maxDistsq) / tolerance; + double ret; + if (max_hook_ratio <= dist_ratio) { + ret = dist_ratio; + } else { + assert(0 < snap_end); + ret = -max_hook_ratio; + *splitPoint = snap_end - 1; + } + assert( ret == 0.0 + || ( ( *splitPoint < last ) + && ( *splitPoint != 0 || ret < 0. ) ) ); + return ret; +} + +/** + * Whereas compute_max_error_ratio() checks for itself that each data point + * is near some point on the curve, this function checks that each point on + * the curve is near some data point (or near some point on the polyline + * defined by the data points, or something like that: we allow for a + * "reasonable curviness" from such a polyline). "Reasonable curviness" + * means we draw a circle centred at the midpoint of a..b, of radius + * proportional to the length |a - b|, and require that each point on the + * segment of bezCurve between the parameters of a and b be within that circle. + * If any point P on the bezCurve segment is outside of that allowable + * region (circle), then we return some metric that increases with the + * distance from P to the circle. + * + * Given that this is a fairly arbitrary criterion for finding appropriate + * places for sharp corners, we test only one point on bezCurve, namely + * the point on bezCurve with parameter halfway between our estimated + * parameters for a and b. (Alternatives are taking the farthest of a + * few parameters between those of a and b, or even using a variant of + * NewtonRaphsonFindRoot() for finding the maximum rather than minimum + * distance.) + */ +static double +compute_hook(Point const &a, Point const &b, double const u, Point const bezCurve[], + double const tolerance) +{ + Point const P = bezier_pt(3, bezCurve, u); + double const dist = distance((a+b)*.5, P); + if (dist < tolerance) { + return 0; + } + double const allowed = distance(a, b) + tolerance; + return dist / allowed; + /** \todo + * effic: Hooks are very rare. We could start by comparing + * distsq, only resorting to the more expensive L2 in cases of + * uncertainty. + */ +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier-utils.h b/src/2geom/bezier-utils.h new file mode 100644 index 0000000..3e56e6e --- /dev/null +++ b/src/2geom/bezier-utils.h @@ -0,0 +1,99 @@ +/** + * \file + * \brief Bezier fitting algorithms + *//* + * An Algorithm for Automatically Fitting Digitized Curves + * by Philip J. Schneider + * from "Graphics Gems", Academic Press, 1990 + * + * Authors: + * Philip J. Schneider + * Lauris Kaplinski + * + * Copyright (C) 1990 Philip J. Schneider + * Copyright (C) 2001 Lauris Kaplinski and Ximian, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_BEZIER_UTILS_H +#define LIB2GEOM_SEEN_BEZIER_UTILS_H + +#include <2geom/point.h> + +namespace Geom { + +Point bezier_pt(unsigned degree, Point const V[], double t); + +int bezier_fit_cubic(Point bezier[], Point const data[], int len, double error); + +int bezier_fit_cubic_r(Point bezier[], Point const data[], int len, double error, + unsigned max_beziers); + +int bezier_fit_cubic_full(Point bezier[], int split_points[], Point const data[], int len, + Point const &tHat1, Point const &tHat2, + double error, unsigned max_beziers); + +Point darray_left_tangent(Point const d[], unsigned const len); +Point darray_left_tangent(Point const d[], unsigned const len, double const tolerance_sq); +Point darray_right_tangent(Point const d[], unsigned const length, double const tolerance_sq); + +template +static void +cubic_bezier_poly_coeff(iterator b, Point *pc) { + double c[10] = {1, + -3, 3, + 3, -6, 3, + -1, 3, -3, 1}; + + int cp = 0; + + for(int i = 0; i < 4; i++) { + pc[i] = Point(0,0); + ++b; + } + for(int i = 0; i < 4; i++) { + --b; + for(int j = 0; j <= i; j++) { + pc[3 - j] += c[cp]*(*b); + cp++; + } + } +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_BEZIER_UTILS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier.cpp b/src/2geom/bezier.cpp new file mode 100644 index 0000000..0c9d12c --- /dev/null +++ b/src/2geom/bezier.cpp @@ -0,0 +1,324 @@ +/** + * @file + * @brief Bernstein-Bezier polynomial + *//* + * Authors: + * MenTaLguY + * Michael Sloan + * Nathan Hurst + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/bezier.h> +#include <2geom/solver.h> +#include <2geom/concepts.h> + +namespace Geom { + +std::vector Bezier::valueAndDerivatives(Coord t, unsigned n_derivs) const { + /* This is inelegant, as it uses several extra stores. I think there might be a way to + * evaluate roughly in situ. */ + + // initialize return vector with zeroes, such that we only need to replace the non-zero derivs + std::vector val_n_der(n_derivs + 1, Coord(0.0)); + + // initialize temp storage variables + std::valarray d_(order()+1); + for(unsigned i = 0; i < size(); i++) { + d_[i] = c_[i]; + } + + unsigned nn = n_derivs + 1; + if(n_derivs > order()) { + nn = order()+1; // only calculate the non zero derivs + } + for(unsigned di = 0; di < nn; di++) { + //val_n_der[di] = (casteljau_subdivision(t, &d_[0], NULL, NULL, order() - di)); + val_n_der[di] = bernstein_value_at(t, &d_[0], order() - di); + for(unsigned i = 0; i < order() - di; i++) { + d_[i] = (order()-di)*(d_[i+1] - d_[i]); + } + } + + return val_n_der; +} + +void Bezier::subdivide(Coord t, Bezier *left, Bezier *right) const +{ + if (left) { + left->c_.resize(size()); + if (right) { + right->c_.resize(size()); + casteljau_subdivision(t, &const_cast&>(c_)[0], + &left->c_[0], &right->c_[0], order()); + } else { + casteljau_subdivision(t, &const_cast&>(c_)[0], + &left->c_[0], NULL, order()); + } + } else if (right) { + right->c_.resize(size()); + casteljau_subdivision(t, &const_cast&>(c_)[0], + NULL, &right->c_[0], order()); + } +} + +std::pair Bezier::subdivide(Coord t) const +{ + std::pair ret; + subdivide(t, &ret.first, &ret.second); + return ret; +} + +std::vector Bezier::roots() const +{ + std::vector solutions; + find_bezier_roots(solutions, 0, 1); + std::sort(solutions.begin(), solutions.end()); + return solutions; +} + +std::vector Bezier::roots(Interval const &ivl) const +{ + std::vector solutions; + find_bernstein_roots(&const_cast&>(c_)[0], order(), solutions, 0, ivl.min(), ivl.max()); + std::sort(solutions.begin(), solutions.end()); + return solutions; +} + +Bezier Bezier::forward_difference(unsigned k) const +{ + Bezier fd(Order(order()-k)); + unsigned n = fd.size(); + + for(unsigned i = 0; i < n; i++) { + fd[i] = 0; + for(unsigned j = i; j < n; j++) { + fd[i] += (((j)&1)?-c_[j]:c_[j])*choose(n, j-i); + } + } + return fd; +} + +Bezier Bezier::elevate_degree() const +{ + Bezier ed(Order(order()+1)); + unsigned n = size(); + ed[0] = c_[0]; + ed[n] = c_[n-1]; + for(unsigned i = 1; i < n; i++) { + ed[i] = (i*c_[i-1] + (n - i)*c_[i])/(n); + } + return ed; +} + +Bezier Bezier::reduce_degree() const +{ + if(order() == 0) return *this; + Bezier ed(Order(order()-1)); + unsigned n = size(); + ed[0] = c_[0]; + ed[n-1] = c_[n]; // ensure exact endpoints + unsigned middle = n/2; + for(unsigned i = 1; i < middle; i++) { + ed[i] = (n*c_[i] - i*ed[i-1])/(n-i); + } + for(unsigned i = n-1; i >= middle; i--) { + ed[i] = (n*c_[i] - i*ed[n-i])/(i); + } + return ed; +} + +Bezier Bezier::elevate_to_degree(unsigned newDegree) const +{ + Bezier ed = *this; + for(unsigned i = degree(); i < newDegree; i++) { + ed = ed.elevate_degree(); + } + return ed; +} + +Bezier Bezier::deflate() const +{ + if(order() == 0) return *this; + unsigned n = order(); + Bezier b(Order(n-1)); + for(unsigned i = 0; i < n; i++) { + b[i] = (n*c_[i+1])/(i+1); + } + return b; +} + +SBasis Bezier::toSBasis() const +{ + SBasis sb; + bezier_to_sbasis(sb, (*this)); + return sb; + //return bezier_to_sbasis(&c_[0], order()); +} + +Bezier &Bezier::operator+=(Bezier const &other) +{ + if (c_.size() > other.size()) { + c_ += other.elevate_to_degree(degree()).c_; + } else if (c_.size() < other.size()) { + *this = elevate_to_degree(other.degree()); + c_ += other.c_; + } else { + c_ += other.c_; + } + return *this; +} + +Bezier &Bezier::operator-=(Bezier const &other) +{ + if (c_.size() > other.size()) { + c_ -= other.elevate_to_degree(degree()).c_; + } else if (c_.size() < other.size()) { + *this = elevate_to_degree(other.degree()); + c_ -= other.c_; + } else { + c_ -= other.c_; + } + return *this; +} + + + +Bezier operator*(Bezier const &f, Bezier const &g) +{ + unsigned m = f.order(); + unsigned n = g.order(); + Bezier h(Bezier::Order(m+n)); + // h_k = sum_(i+j=k) (m i)f_i (n j)g_j / (m+n k) + + for(unsigned i = 0; i <= m; i++) { + const double fi = choose(m,i)*f[i]; + for(unsigned j = 0; j <= n; j++) { + h[i+j] += fi * choose(n,j)*g[j]; + } + } + for(unsigned k = 0; k <= m+n; k++) { + h[k] /= choose(m+n, k); + } + return h; +} + +Bezier portion(Bezier const &a, double from, double to) +{ + Bezier ret(a); + + bool reverse_result = false; + if (from > to) { + std::swap(from, to); + reverse_result = true; + } + + do { + if (from == 0) { + if (to == 1) { + break; + } + casteljau_subdivision(to, &ret.c_[0], &ret.c_[0], NULL, ret.order()); + break; + } + casteljau_subdivision(from, &ret.c_[0], NULL, &ret.c_[0], ret.order()); + if (to == 1) break; + casteljau_subdivision((to - from) / (1 - from), &ret.c_[0], &ret.c_[0], NULL, ret.order()); + // to protect against numerical inaccuracy in the above expression, we manually set + // the last coefficient to a value evaluated directly from the original polynomial + ret.c_[ret.order()] = a.valueAt(to); + } while(0); + + if (reverse_result) { + std::reverse(&ret.c_[0], &ret.c_[0] + ret.c_.size()); + } + return ret; +} + +Bezier derivative(Bezier const &a) +{ + //if(a.order() == 1) return Bezier(0.0); + if(a.order() == 1) return Bezier(a.c_[1]-a.c_[0]); + Bezier der(Bezier::Order(a.order()-1)); + + for(unsigned i = 0; i < a.order(); i++) { + der.c_[i] = a.order()*(a.c_[i+1] - a.c_[i]); + } + return der; +} + +Bezier integral(Bezier const &a) +{ + Bezier inte(Bezier::Order(a.order()+1)); + + inte[0] = 0; + for(unsigned i = 0; i < inte.order(); i++) { + inte[i+1] = inte[i] + a[i]/(inte.order()); + } + return inte; +} + +OptInterval bounds_fast(Bezier const &b) +{ + OptInterval ret = Interval::from_array(&const_cast(b).c_[0], b.size()); + return ret; +} + +OptInterval bounds_exact(Bezier const &b) +{ + OptInterval ret(b.at0(), b.at1()); + std::vector r = derivative(b).roots(); + for (unsigned i = 0; i < r.size(); ++i) { + ret->expandTo(b.valueAt(r[i])); + } + return ret; +} + +OptInterval bounds_local(Bezier const &b, OptInterval const &i) +{ + //return bounds_local(b.toSBasis(), i); + if (i) { + return bounds_fast(portion(b, i->min(), i->max())); + } else { + return OptInterval(); + } +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/bezier.h b/src/2geom/bezier.h new file mode 100644 index 0000000..fc2fe5f --- /dev/null +++ b/src/2geom/bezier.h @@ -0,0 +1,364 @@ +/** + * @file + * @brief Bernstein-Bezier polynomial + *//* + * Authors: + * MenTaLguY + * Michael Sloan + * Nathan Hurst + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_BEZIER_H +#define LIB2GEOM_SEEN_BEZIER_H + +#include +#include +#include +#include <2geom/choose.h> +#include <2geom/coord.h> +#include <2geom/d2.h> +#include <2geom/math-utils.h> + +namespace Geom { + +/** @brief Compute the value of a Bernstein-Bezier polynomial. + * This method uses a Horner-like fast evaluation scheme. + * @param t Time value + * @param c_ Pointer to coefficients + * @param n Degree of the polynomial (number of coefficients minus one) */ +template +inline T bernstein_value_at(double t, T const *c_, unsigned n) { + double u = 1.0 - t; + double bc = 1; + double tn = 1; + T tmp = c_[0]*u; + for(unsigned i=1; i +inline T casteljau_subdivision(double t, T const *v, T *left, T *right, unsigned order) { + // The Horner-like scheme gives very slightly different results, but we need + // the result of subdivision to match exactly with Bezier's valueAt function. + T val = bernstein_value_at(t, v, order); + + if (!left && !right) { + return val; + } + + if (!right) { + if (left != v) { + std::copy(v, v + order + 1, left); + } + for (std::size_t i = order; i > 0; --i) { + for (std::size_t j = i; j <= order; ++j) { + left[j] = lerp(t, left[j-1], left[j]); + } + } + left[order] = val; + return left[order]; + } + + if (right != v) { + std::copy(v, v + order + 1, right); + } + for (std::size_t i = 1; i <= order; ++i) { + if (left) { + left[i-1] = right[0]; + } + for (std::size_t j = i; j > 0; --j) { + right[j-1] = lerp(t, right[j-1], right[j]); + } + } + right[0] = val; + if (left) { + left[order] = right[0]; + } + return right[0]; +} + +/** + * @brief Polynomial in Bernstein-Bezier basis + * @ingroup Fragments + */ +class Bezier + : boost::arithmetic< Bezier, double + , boost::additive< Bezier + > > +{ +private: + std::valarray c_; + + friend Bezier portion(const Bezier & a, Coord from, Coord to); + friend OptInterval bounds_fast(Bezier const & b); + friend Bezier derivative(const Bezier & a); + friend class Bernstein; + + void + find_bezier_roots(std::vector & solutions, + double l, double r) const; + +protected: + Bezier(Coord const c[], unsigned ord) + : c_(c, ord+1) + {} + +public: + unsigned order() const { return c_.size()-1;} + unsigned degree() const { return order(); } + unsigned size() const { return c_.size();} + + Bezier() {} + Bezier(const Bezier& b) :c_(b.c_) {} + Bezier &operator=(Bezier const &other) { + if ( c_.size() != other.c_.size() ) { + c_.resize(other.c_.size()); + } + c_ = other.c_; + return *this; + } + + struct Order { + unsigned order; + explicit Order(Bezier const &b) : order(b.order()) {} + explicit Order(unsigned o) : order(o) {} + operator unsigned() const { return order; } + }; + + //Construct an arbitrary order bezier + Bezier(Order ord) : c_(0., ord.order+1) { + assert(ord.order == order()); + } + + /// @name Construct Bezier polynomials from their control points + /// @{ + explicit Bezier(Coord c0) : c_(0., 1) { + c_[0] = c0; + } + Bezier(Coord c0, Coord c1) : c_(0., 2) { + c_[0] = c0; c_[1] = c1; + } + Bezier(Coord c0, Coord c1, Coord c2) : c_(0., 3) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3) : c_(0., 4) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4) : c_(0., 5) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, + Coord c5) : c_(0., 6) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + c_[5] = c5; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, + Coord c5, Coord c6) : c_(0., 7) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + c_[5] = c5; c_[6] = c6; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, + Coord c5, Coord c6, Coord c7) : c_(0., 8) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + c_[5] = c5; c_[6] = c6; c_[7] = c7; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, + Coord c5, Coord c6, Coord c7, Coord c8) : c_(0., 9) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + c_[5] = c5; c_[6] = c6; c_[7] = c7; c_[8] = c8; + } + Bezier(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, + Coord c5, Coord c6, Coord c7, Coord c8, Coord c9) : c_(0., 10) { + c_[0] = c0; c_[1] = c1; c_[2] = c2; c_[3] = c3; c_[4] = c4; + c_[5] = c5; c_[6] = c6; c_[7] = c7; c_[8] = c8; c_[9] = c9; + } + + template + Bezier(Iter first, Iter last) { + c_.resize(std::distance(first, last)); + for (std::size_t i = 0; first != last; ++first, ++i) { + c_[i] = *first; + } + } + Bezier(std::vector const &vec) + : c_(&vec[0], vec.size()) + {} + /// @} + + void resize (unsigned int n, Coord v = 0) { + c_.resize (n, v); + } + void clear() { + c_.resize(0); + } + + //IMPL: FragmentConcept + typedef Coord output_type; + bool isZero(double eps=EPSILON) const { + for(unsigned i = 0; i <= order(); i++) { + if( ! are_near(c_[i], 0., eps) ) return false; + } + return true; + } + bool isConstant(double eps=EPSILON) const { + for(unsigned i = 1; i <= order(); i++) { + if( ! are_near(c_[i], c_[0], eps) ) return false; + } + return true; + } + bool isFinite() const { + for(unsigned i = 0; i <= order(); i++) { + if(!std::isfinite(c_[i])) return false; + } + return true; + } + Coord at0() const { return c_[0]; } + Coord &at0() { return c_[0]; } + Coord at1() const { return c_[order()]; } + Coord &at1() { return c_[order()]; } + + Coord valueAt(double t) const { + return bernstein_value_at(t, &c_[0], order()); + } + Coord operator()(double t) const { return valueAt(t); } + + SBasis toSBasis() const; + + Coord &operator[](unsigned ix) { return c_[ix]; } + Coord const &operator[](unsigned ix) const { return const_cast&>(c_)[ix]; } + + void setCoeff(unsigned ix, double val) { c_[ix] = val; } + + // The size of the returned vector equals n_derivs+1. + std::vector valueAndDerivatives(Coord t, unsigned n_derivs) const; + + void subdivide(Coord t, Bezier *left, Bezier *right) const; + std::pair subdivide(Coord t) const; + + std::vector roots() const; + std::vector roots(Interval const &ivl) const; + + Bezier forward_difference(unsigned k) const; + Bezier elevate_degree() const; + Bezier reduce_degree() const; + Bezier elevate_to_degree(unsigned newDegree) const; + Bezier deflate() const; + + // basic arithmetic operators + Bezier &operator+=(double v) { + c_ += v; + return *this; + } + Bezier &operator-=(double v) { + c_ -= v; + return *this; + } + Bezier &operator*=(double v) { + c_ *= v; + return *this; + } + Bezier &operator/=(double v) { + c_ /= v; + return *this; + } + Bezier &operator+=(Bezier const &other); + Bezier &operator-=(Bezier const &other); +}; + + +void bezier_to_sbasis (SBasis &sb, Bezier const &bz); + +Bezier operator*(Bezier const &f, Bezier const &g); +inline Bezier multiply(Bezier const &f, Bezier const &g) { + Bezier result = f * g; + return result; +} + +inline Bezier reverse(const Bezier & a) { + Bezier result = Bezier(Bezier::Order(a)); + for(unsigned i = 0; i <= a.order(); i++) + result[i] = a[a.order() - i]; + return result; +} + +Bezier portion(const Bezier & a, double from, double to); + +// XXX Todo: how to handle differing orders +inline std::vector bezier_points(const D2 & a) { + std::vector result; + for(unsigned i = 0; i <= a[0].order(); i++) { + Point p; + for(unsigned d = 0; d < 2; d++) p[d] = a[d][i]; + result.push_back(p); + } + return result; +} + +Bezier derivative(Bezier const &a); +Bezier integral(Bezier const &a); +OptInterval bounds_fast(Bezier const &b); +OptInterval bounds_exact(Bezier const &b); +OptInterval bounds_local(Bezier const &b, OptInterval const &i); + +inline std::ostream &operator<< (std::ostream &os, const Bezier & b) { + os << "Bezier("; + for(unsigned i = 0; i < b.order(); i++) { + os << format_coord_nice(b[i]) << ", "; + } + os << format_coord_nice(b[b.order()]) << ")"; + return os; +} + +} +#endif // LIB2GEOM_SEEN_BEZIER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/cairo-path-sink.cpp b/src/2geom/cairo-path-sink.cpp new file mode 100644 index 0000000..244a08b --- /dev/null +++ b/src/2geom/cairo-path-sink.cpp @@ -0,0 +1,123 @@ +/** + * @file + * @brief Path sink for Cairo contexts + *//* + * Copyright 2014 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include +#include <2geom/cairo-path-sink.h> +#include <2geom/elliptical-arc.h> + +namespace Geom { + +CairoPathSink::CairoPathSink(cairo_t *cr) + : _cr(cr) +{} + +void CairoPathSink::moveTo(Point const &p) +{ + cairo_move_to(_cr, p[X], p[Y]); + _current_point = p; +} + +void CairoPathSink::lineTo(Point const &p) +{ + cairo_line_to(_cr, p[X], p[Y]); + _current_point = p; +} + +void CairoPathSink::curveTo(Point const &p1, Point const &p2, Point const &p3) +{ + cairo_curve_to(_cr, p1[X], p1[Y], p2[X], p2[Y], p3[X], p3[Y]); + _current_point = p3; +} + +void CairoPathSink::quadTo(Point const &p1, Point const &p2) +{ + // degree-elevate to cubic Bezier, since Cairo doesn't do quad Beziers + // google "Bezier degree elevation" for more info + Point q1 = (1./3.) * _current_point + (2./3.) * p1; + Point q2 = (2./3.) * p1 + (1./3.) * p2; + // q3 = p2 + cairo_curve_to(_cr, q1[X], q1[Y], q2[X], q2[Y], p2[X], p2[Y]); + _current_point = p2; +} + +void CairoPathSink::arcTo(double rx, double ry, double angle, + bool large_arc, bool sweep, Point const &p) +{ + EllipticalArc arc(_current_point, rx, ry, angle, large_arc, sweep, p); + // Cairo only does circular arcs. + // To do elliptical arcs, we must use a temporary transform. + Affine uct = arc.unitCircleTransform(); + + // TODO move Cairo-2Geom matrix conversion into a common location + cairo_matrix_t cm; + cm.xx = uct[0]; + cm.xy = uct[2]; + cm.x0 = uct[4]; + cm.yx = uct[1]; + cm.yy = uct[3]; + cm.y0 = uct[5]; + + cairo_save(_cr); + cairo_transform(_cr, &cm); + if (sweep) { + cairo_arc(_cr, 0, 0, 1, arc.initialAngle(), arc.finalAngle()); + } else { + cairo_arc_negative(_cr, 0, 0, 1, arc.initialAngle(), arc.finalAngle()); + } + _current_point = p; + cairo_restore(_cr); + + /* Note that an extra linear segment will be inserted before the arc + * if Cairo considers the current point distinct from the initial point + * of the arc; we could partially alleviate this by not emitting + * linear segments that are followed by arc segments, but this would require + * buffering the input curves. */ +} + +void CairoPathSink::closePath() +{ + cairo_close_path(_cr); +} + +void CairoPathSink::flush() {} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/cairo-path-sink.h b/src/2geom/cairo-path-sink.h new file mode 100644 index 0000000..9fec7e0 --- /dev/null +++ b/src/2geom/cairo-path-sink.h @@ -0,0 +1,87 @@ +/** + * @file + * @brief Path sink for Cairo contexts + *//* + * Copyright 2014 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_CAIRO_PATH_SINK_H +#define LIB2GEOM_SEEN_CAIRO_PATH_SINK_H + +#include <2geom/path-sink.h> +#include + +namespace Geom { + + +/** @brief Output paths to a Cairo drawing context + * + * This class converts from 2Geom path representation to the Cairo representation. + * Use it to simplify visualizing the results of 2Geom operations with the Cairo library, + * for example: + * @code + * CairoPathSink sink(cr); + * sink.feed(pv); + * cairo_stroke(cr); + * @endcode + * + * Currently the flush method is a no-op, but this is not guaranteed + * to hold forever. + */ +class CairoPathSink + : public PathSink +{ +public: + CairoPathSink(cairo_t *cr); + + void moveTo(Point const &p); + void lineTo(Point const &p); + void curveTo(Point const &c0, Point const &c1, Point const &p); + void quadTo(Point const &c, Point const &p); + void arcTo(Coord rx, Coord ry, Coord angle, + bool large_arc, bool sweep, Point const &p); + void closePath(); + void flush(); + +private: + cairo_t *_cr; + Point _current_point; +}; + +} + +#endif // !LIB2GEOM_SEEN_CAIRO_PATH_SINK_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/choose.h b/src/2geom/choose.h new file mode 100644 index 0000000..64ce76f --- /dev/null +++ b/src/2geom/choose.h @@ -0,0 +1,140 @@ +/** + * \file + * \brief Calculation of binomial cefficients + *//* + * Copyright 2006 Nathan Hurst + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_CHOOSE_H +#define LIB2GEOM_SEEN_CHOOSE_H + +#include + +namespace Geom { + +// XXX: Can we keep only the left terms easily? +// this would more than halve the array +// row index becomes n2 = n/2, row2 = n2*(n2+1)/2, row = row2*2+(n&1)?n2:0 +// we could also leave off the ones + +template +T choose(unsigned n, unsigned k) { + static std::vector pascals_triangle; + static unsigned rows_done = 0; + // indexing is (0,0,), (1,0), (1,1), (2, 0)... + // to get (i, j) i*(i+1)/2 + j + if(/*k < 0 ||*/ k > n) return 0; + if(rows_done <= n) {// we haven't got there yet + if(rows_done == 0) { + pascals_triangle.push_back(1); + rows_done = 1; + } + while(rows_done <= n) { + unsigned p = pascals_triangle.size() - rows_done; + pascals_triangle.push_back(1); + for(unsigned i = 0; i < rows_done-1; i++) { + pascals_triangle.push_back(pascals_triangle[p] + + pascals_triangle[p+1]); + p++; + } + pascals_triangle.push_back(1); + rows_done ++; + } + } + unsigned row = (n*(n+1))/2; + return pascals_triangle[row+k]; +} + +// Is it faster to store them or compute them on demand? +/*template +T choose(unsigned n, unsigned k) { + T r = 1; + for(unsigned i = 1; i <= k; i++) + r = (r*(n-k+i))/i; + return r; + }*/ + + + +template +class BinomialCoefficient +{ + public: + typedef ValueType value_type; + typedef std::vector container_type; + + BinomialCoefficient(unsigned int _n) + : n(_n), m(n >> 1) + { + coefficients.reserve(m+1); + coefficients.push_back(1); + int h = m + 1; + value_type bct = 1; + for (int i = 1; i < h; ++i) + { + bct *= (n-i+1); + bct /= i; + coefficients.push_back(bct); + } + } + + unsigned int size() const + { + return degree() +1; + } + + unsigned int degree() const + { + return n; + } + + value_type operator[] (unsigned int k) const + { + if (k > m) k = n - k; + return coefficients[k]; + } + + private: + const int n; + const unsigned int m; + container_type coefficients; +}; + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_CHOOSE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/circle.cpp b/src/2geom/circle.cpp new file mode 100644 index 0000000..934a8d3 --- /dev/null +++ b/src/2geom/circle.cpp @@ -0,0 +1,337 @@ +/** @file + * @brief Circle shape + *//* + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2008-2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/numeric/fitting-tool.h> +#include <2geom/numeric/fitting-model.h> + +namespace Geom { + +Rect Circle::boundsFast() const +{ + Point rr(_radius, _radius); + Rect bbox(_center - rr, _center + rr); + return bbox; +} + +void Circle::setCoefficients(Coord A, Coord B, Coord C, Coord D) +{ + if (A == 0) { + THROW_RANGEERROR("square term coefficient == 0"); + } + + //std::cerr << "B = " << B << " C = " << C << " D = " << D << std::endl; + + Coord b = B / A; + Coord c = C / A; + Coord d = D / A; + + _center[X] = -b/2; + _center[Y] = -c/2; + Coord r2 = _center[X] * _center[X] + _center[Y] * _center[Y] - d; + + if (r2 < 0) { + THROW_RANGEERROR("ray^2 < 0"); + } + + _radius = std::sqrt(r2); +} + +void Circle::coefficients(Coord &A, Coord &B, Coord &C, Coord &D) const +{ + A = 1; + B = -2 * _center[X]; + C = -2 * _center[Y]; + D = _center[X] * _center[X] + _center[Y] * _center[Y] - _radius * _radius; +} + +std::vector Circle::coefficients() const +{ + std::vector c(4); + coefficients(c[0], c[1], c[2], c[3]); + return c; +} + + +Zoom Circle::unitCircleTransform() const +{ + Zoom ret(_radius, _center / _radius); + return ret; +} + +Zoom Circle::inverseUnitCircleTransform() const +{ + if (_radius == 0) { + THROW_RANGEERROR("degenerate circle does not have an inverse unit circle transform"); + } + + Zoom ret(1/_radius, Translate(-_center)); + return ret; +} + +Point Circle::initialPoint() const +{ + Point p(_center); + p[X] += _radius; + return p; +} + +Point Circle::pointAt(Coord t) const { + return _center + Point::polar(t) * _radius; +} + +Coord Circle::valueAt(Coord t, Dim2 d) const { + Coord delta = (d == X ? std::cos(t) : std::sin(t)); + return _center[d] + delta * _radius; +} + +Coord Circle::timeAt(Point const &p) const { + if (_center == p) return 0; + return atan2(p - _center); +} + +Coord Circle::nearestTime(Point const &p) const { + return timeAt(p); +} + +bool Circle::contains(Rect const &r) const +{ + for (unsigned i = 0; i < 4; ++i) { + if (!contains(r.corner(i))) return false; + } + return true; +} + +bool Circle::contains(Circle const &other) const +{ + Coord cdist = distance(_center, other._center); + Coord rdist = fabs(_radius - other._radius); + return cdist <= rdist; +} + +bool Circle::intersects(Line const &l) const +{ + // http://mathworld.wolfram.com/Circle-LineIntersection.html + Coord dr = l.vector().length(); + Coord r = _radius; + Coord D = cross(l.initialPoint(), l.finalPoint()); + Coord delta = r*r * dr*dr - D*D; + if (delta >= 0) return true; + return false; +} + +bool Circle::intersects(Circle const &other) const +{ + Coord cdist = distance(_center, other._center); + Coord rsum = _radius + other._radius; + return cdist <= rsum; +} + + +std::vector Circle::intersect(Line const &l) const +{ + // http://mathworld.wolfram.com/Circle-LineIntersection.html + Coord dr = l.vector().length(); + Coord dx = l.vector().x(); + Coord dy = l.vector().y(); + Coord D = cross(l.initialPoint() - _center, l.finalPoint() - _center); + Coord delta = _radius*_radius * dr*dr - D*D; + + std::vector result; + if (delta < 0) return result; + if (delta == 0) { + Coord ix = (D*dy) / (dr*dr); + Coord iy = (-D*dx) / (dr*dr); + Point ip(ix, iy); ip += _center; + result.push_back(ShapeIntersection(timeAt(ip), l.timeAt(ip), ip)); + return result; + } + + Coord sqrt_delta = std::sqrt(delta); + Coord signmod = dy < 0 ? -1 : 1; + + Coord i1x = (D*dy + signmod * dx * sqrt_delta) / (dr*dr); + Coord i1y = (-D*dx + fabs(dy) * sqrt_delta) / (dr*dr); + Point i1p(i1x, i1y); i1p += _center; + + Coord i2x = (D*dy - signmod * dx * sqrt_delta) / (dr*dr); + Coord i2y = (-D*dx - fabs(dy) * sqrt_delta) / (dr*dr); + Point i2p(i2x, i2y); i2p += _center; + + result.push_back(ShapeIntersection(timeAt(i1p), l.timeAt(i1p), i1p)); + result.push_back(ShapeIntersection(timeAt(i2p), l.timeAt(i2p), i2p)); + return result; +} + +std::vector Circle::intersect(LineSegment const &l) const +{ + std::vector result = intersect(Line(l)); + filter_line_segment_intersections(result); + return result; +} + +std::vector Circle::intersect(Circle const &other) const +{ + std::vector result; + + if (*this == other) { + THROW_INFINITESOLUTIONS(); + } + if (contains(other)) return result; + if (!intersects(other)) return result; + + // See e.g. http://mathworld.wolfram.com/Circle-CircleIntersection.html + // Basically, we figure out where is the third point of a triangle + // with two points in the centers and with edge lengths equal to radii + Point cv = other._center - _center; + Coord d = cv.length(); + Coord R = radius(), r = other.radius(); + + if (d == R + r) { + Point px = lerp(R / d, _center, other._center); + Coord T = timeAt(px), t = other.timeAt(px); + result.push_back(ShapeIntersection(T, t, px)); + return result; + } + + // q is the distance along the line between centers to the perpendicular line + // that goes through both intersections. + Coord q = (d*d - r*r + R*R) / (2*d); + Point qp = lerp(q/d, _center, other._center); + + // The triangle given by the points: + // _center, qp, intersection + // is a right triangle. Determine the distance between qp and intersection + // using the Pythagorean theorem. + Coord h = std::sqrt(R*R - q*q); + Point qd = (h/d) * cv.cw(); + + // now compute the intersection points + Point x1 = qp + qd; + Point x2 = qp - qd; + + result.push_back(ShapeIntersection(timeAt(x1), other.timeAt(x1), x1)); + result.push_back(ShapeIntersection(timeAt(x2), other.timeAt(x2), x2)); + return result; +} + +/** + @param inner a point whose angle with the circle center is inside the angle that the arc spans + */ +EllipticalArc * +Circle::arc(Point const& initial, Point const& inner, Point const& final) const +{ + // TODO native implementation! + Ellipse e(_center[X], _center[Y], _radius, _radius, 0); + return e.arc(initial, inner, final); +} + +bool Circle::operator==(Circle const &other) const +{ + if (_center != other._center) return false; + if (_radius != other._radius) return false; + return true; +} + +D2 Circle::toSBasis() const +{ + D2 B; + Linear bo = Linear(0, 2 * M_PI); + + B[0] = cos(bo,4); + B[1] = sin(bo,4); + + B = B * _radius + _center; + + return B; +} + + +void Circle::fit(std::vector const& points) +{ + size_t sz = points.size(); + if (sz < 2) { + THROW_RANGEERROR("fitting error: too few points passed"); + } + if (sz == 2) { + _center = points[0] * 0.5 + points[1] * 0.5; + _radius = distance(points[0], points[1]) / 2; + return; + } + + NL::LFMCircle model; + NL::least_squeares_fitter fitter(model, sz); + + for (size_t i = 0; i < sz; ++i) { + fitter.append(points[i]); + } + fitter.update(); + + NL::Vector z(sz, 0.0); + model.instance(*this, fitter.result(z)); +} + + +bool are_near(Circle const &a, Circle const &b, Coord eps) +{ + // to check whether no point on a is further than eps from b, + // we check two things: + // 1. if radii differ by more than eps, there is definitely a point that fails + // 2. if they differ by less, we check the centers. They have to be closer + // together if the radius differs, since the maximum distance will be + // equal to sum of radius difference and distance between centers. + if (!are_near(a.radius(), b.radius(), eps)) return false; + Coord adjusted_eps = eps - fabs(a.radius() - b.radius()); + return are_near(a.center(), b.center(), adjusted_eps); +} + +std::ostream &operator<<(std::ostream &out, Circle const &c) +{ + out << "Circle(" << c.center() << ", " << format_coord_nice(c.radius()) << ")"; + return out; +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/circle.h b/src/2geom/circle.h new file mode 100644 index 0000000..a4d5f20 --- /dev/null +++ b/src/2geom/circle.h @@ -0,0 +1,165 @@ +/** @file + * @brief Circle shape + *//* + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2008-2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_CIRCLE_H +#define LIB2GEOM_SEEN_CIRCLE_H + +#include <2geom/forward.h> +#include <2geom/intersection.h> +#include <2geom/point.h> +#include <2geom/rect.h> +#include <2geom/transforms.h> + +namespace Geom { + +class EllipticalArc; + +/** @brief Set of all points at a fixed distance from the center + * @ingroup Shapes */ +class Circle + : boost::equality_comparable1< Circle + , MultipliableNoncommutative< Circle, Translate + , MultipliableNoncommutative< Circle, Rotate + , MultipliableNoncommutative< Circle, Zoom + > > > > +{ + Point _center; + Coord _radius; + +public: + Circle() {} + Circle(Coord cx, Coord cy, Coord r) + : _center(cx, cy), _radius(r) + {} + Circle(Point const ¢er, Coord r) + : _center(center), _radius(r) + {} + + Circle(Coord A, Coord B, Coord C, Coord D) { + setCoefficients(A, B, C, D); + } + + // Construct the unique circle passing through three points. + //Circle(Point const &a, Point const &b, Point const &c); + + Point center() const { return _center; } + Coord center(Dim2 d) const { return _center[d]; } + Coord radius() const { return _radius; } + Coord area() const { return M_PI * _radius * _radius; } + bool isDegenerate() const { return _radius == 0; } + + void setCenter(Point const &p) { _center = p; } + void setRadius(Coord c) { _radius = c; } + + Rect boundsFast() const; + Rect boundsExact() const { return boundsFast(); } + + Point initialPoint() const; + Point finalPoint() const { return initialPoint(); } + Point pointAt(Coord t) const; + Coord valueAt(Coord t, Dim2 d) const; + Coord timeAt(Point const &p) const; + Coord nearestTime(Point const &p) const; + + bool contains(Point const &p) const { return distance(p, _center) <= _radius; } + bool contains(Rect const &other) const; + bool contains(Circle const &other) const; + + bool intersects(Line const &l) const; + bool intersects(LineSegment const &l) const; + bool intersects(Circle const &other) const; + + std::vector intersect(Line const &other) const; + std::vector intersect(LineSegment const &other) const; + std::vector intersect(Circle const &other) const; + + // build a circle by its implicit equation: + // Ax^2 + Ay^2 + Bx + Cy + D = 0 + void setCoefficients(Coord A, Coord B, Coord C, Coord D); + void coefficients(Coord &A, Coord &B, Coord &C, Coord &D) const; + std::vector coefficients() const; + + Zoom unitCircleTransform() const; + Zoom inverseUnitCircleTransform() const; + + EllipticalArc * + arc(Point const& initial, Point const& inner, Point const& final) const; + + D2 toSBasis() const; + + Circle &operator*=(Translate const &t) { + _center *= t; + return *this; + } + Circle &operator*=(Rotate const &) { + return *this; + } + Circle &operator*=(Zoom const &z) { + _center *= z; + _radius *= z.scale(); + return *this; + } + + bool operator==(Circle const &other) const; + + /** @brief Fit the circle to the passed points using the least squares method. + * @param points Samples at the perimeter of the circle */ + void fit(std::vector const &points); +}; + +bool are_near(Circle const &a, Circle const &b, Coord eps=EPSILON); + +std::ostream &operator<<(std::ostream &out, Circle const &c); + +template <> +struct ShapeTraits { + typedef Coord TimeType; + typedef Interval IntervalType; + typedef Ellipse AffineClosureType; + typedef Intersection<> IntersectionType; +}; + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_CIRCLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/concepts.h b/src/2geom/concepts.h new file mode 100644 index 0000000..de76d0f --- /dev/null +++ b/src/2geom/concepts.h @@ -0,0 +1,209 @@ +/** + * \file + * \brief Template concepts used by 2Geom + *//* + * Copyright 2007 Michael Sloan + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_CONCEPTS_H +#define LIB2GEOM_SEEN_CONCEPTS_H + +#include <2geom/sbasis.h> +#include <2geom/interval.h> +#include <2geom/point.h> +#include <2geom/rect.h> +#include <2geom/intersection.h> +#include +#include +#include <2geom/forward.h> +#include <2geom/transforms.h> + +namespace Geom { + +//forward decls +template struct ResultTraits; + +template <> struct ResultTraits { + typedef OptInterval bounds_type; + typedef SBasis sb_type; +}; + +template <> struct ResultTraits { + typedef OptRect bounds_type; + typedef D2 sb_type; +}; + +//A concept for one-dimensional functions defined on [0,1] +template +struct FragmentConcept { + typedef typename T::output_type OutputType; + typedef typename ResultTraits::bounds_type BoundsType; + typedef typename ResultTraits::sb_type SbType; + T t; + double d; + OutputType o; + bool b; + BoundsType i; + Interval dom; + std::vector v; + unsigned u; + SbType sb; + void constraints() { + t = T(o); + b = t.isZero(d); + b = t.isConstant(d); + b = t.isFinite(); + o = t.at0(); + o = t.at1(); + t.at0() = o; + t.at1() = o; + o = t.valueAt(d); + o = t(d); + v = t.valueAndDerivatives(d, u-1); + //Is a pure derivative (ignoring others) accessor ever much faster? + //u = number of values returned. first val is value. + sb = t.toSBasis(); + t = reverse(t); + i = bounds_fast(t); + i = bounds_exact(t); + i = bounds_local(t, dom); + /*With portion, Interval makes some sense, but instead I'm opting for + doubles, for the following reasons: + A) This way a reversed portion may be specified + B) Performance might be a bit better for piecewise and such + C) Interval version provided below + */ + t = portion(t, d, d); + } +}; + +template +struct ShapeConcept { + typedef typename ShapeTraits::TimeType Time; + typedef typename ShapeTraits::IntervalType Interval; + typedef typename ShapeTraits::AffineClosureType AffineClosure; + typedef typename ShapeTraits::IntersectionType Isect; + + T shape, other; + Time t; + Point p; + AffineClosure ac; + Affine m; + Translate tr; + Coord c; + bool bool_; + std::vector ivec; + + void constraints() { + p = shape.pointAt(t); + c = shape.valueAt(t, X); + ivec = shape.intersect(other); + t = shape.nearestTime(p); + shape *= tr; + ac = shape; + ac *= m; + bool_ = (shape == shape); + bool_ = (shape != other); + bool_ = shape.isDegenerate(); + //bool_ = are_near(shape, other, c); + } +}; + +template +inline T portion(const T& t, const Interval& i) { return portion(t, i.min(), i.max()); } + +template +struct EqualityComparableConcept { + T a, b; + bool bool_; + void constraints() { + bool_ = (a == b); + bool_ = (a != b); + } +}; + +template +struct NearConcept { + T a, b; + double tol; + bool res; + void constraints() { + res = are_near(a, b, tol); + } +}; + +template +struct OffsetableConcept { + T t; + typename T::output_type d; + void constraints() { + t = t + d; t += d; + t = t - d; t -= d; + } +}; + +template +struct ScalableConcept { + T t; + typename T::output_type d; + void constraints() { + t = -t; + t = t * d; t *= d; + t = t / d; t /= d; + } +}; + +template +struct AddableConcept { + T i, j; + void constraints() { + i += j; i = i + j; + i -= j; i = i - j; + } +}; + +template +struct MultiplicableConcept { + T i, j; + void constraints() { + i *= j; i = i * j; + } +}; + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_CONCEPTS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conic_section_clipper.h b/src/2geom/conic_section_clipper.h new file mode 100644 index 0000000..38bba33 --- /dev/null +++ b/src/2geom/conic_section_clipper.h @@ -0,0 +1,58 @@ +/** @file + * @brief Conic section clipping with respect to a rectangle + *//* + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + + + +#ifndef LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_H +#define LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_H + + +#undef CLIP_WITH_CAIRO_SUPPORT +#include <2geom/conic_section_clipper_impl.h> + + +#endif // _2GEOM_CONIC_SECTION_CLIPPER_H_ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conic_section_clipper_cr.h b/src/2geom/conic_section_clipper_cr.h new file mode 100644 index 0000000..6c62494 --- /dev/null +++ b/src/2geom/conic_section_clipper_cr.h @@ -0,0 +1,64 @@ +/** @file + * @brief Conic section clipping with respect to a rectangle + *//* + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + + + +//////////////////////////////////////////////////////////////////////////////// +// This header should be used for graphical debugging purpuse only. // +//////////////////////////////////////////////////////////////////////////////// + + +#ifndef LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_CR_H +#define LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_CR_H + + +#define CLIP_WITH_CAIRO_SUPPORT +#include "conic_section_clipper_impl.h" +#include "conic_section_clipper_impl.cpp" + + +#endif // _2GEOM_CONIC_SECTION_CLIPPER_CR_H_ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conic_section_clipper_impl.cpp b/src/2geom/conic_section_clipper_impl.cpp new file mode 100644 index 0000000..23731af --- /dev/null +++ b/src/2geom/conic_section_clipper_impl.cpp @@ -0,0 +1,574 @@ +/* Conic section clipping with respect to a rectangle + * + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef CLIP_WITH_CAIRO_SUPPORT + #include <2geom/conic_section_clipper.h> +#endif + +namespace Geom +{ + +/* + * Find rectangle-conic crossing points. They are returned in the + * "crossing_points" parameter. + * The method returns true if the conic section intersects at least one + * of the four lines passing through rectangle edges, else it returns false. + */ +bool CLIPPER_CLASS::intersect (std::vector & crossing_points) const +{ + crossing_points.clear(); + + std::vector rts; + std::vector cpts; + // rectangle corners + enum {TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT}; + + bool no_crossing = true; + + // right edge + cs.roots (rts, R.right(), X); + if (!rts.empty()) + { + no_crossing = false; + DBGPRINT ("CLIP: right: rts[0] = ", rts[0]) + DBGPRINTIF ((rts.size() == 2), "CLIP: right: rts[1] = ", rts[1]) + + Point corner1 = R.corner(TOP_RIGHT); + Point corner2 = R.corner(BOTTOM_RIGHT); + + for (size_t i = 0; i < rts.size(); ++i) + { + if (rts[i] < R.top() || rts[i] > R.bottom()) continue; + Point P (R.right(), rts[i]); + if (are_near (P, corner1)) + P = corner1; + else if (are_near (P, corner2)) + P = corner2; + + cpts.push_back (P); + } + if (cpts.size() == 2 && are_near (cpts[0], cpts[1])) + { + cpts[0] = middle_point (cpts[0], cpts[1]); + cpts.pop_back(); + } + } + + // top edge + cs.roots (rts, R.top(), Y); + if (!rts.empty()) + { + no_crossing = false; + DBGPRINT ("CLIP: top: rts[0] = ", rts[0]) + DBGPRINTIF ((rts.size() == 2), "CLIP: top: rts[1] = ", rts[1]) + + Point corner1 = R.corner(TOP_RIGHT); + Point corner2 = R.corner(TOP_LEFT); + + for (size_t i = 0; i < rts.size(); ++i) + { + if (rts[i] < R.left() || rts[i] > R.right()) continue; + Point P (rts[i], R.top()); + if (are_near (P, corner1)) + P = corner1; + else if (are_near (P, corner2)) + P = corner2; + + cpts.push_back (P); + } + if (cpts.size() == 2 && are_near (cpts[0], cpts[1])) + { + cpts[0] = middle_point (cpts[0], cpts[1]); + cpts.pop_back(); + } + } + + // left edge + cs.roots (rts, R.left(), X); + if (!rts.empty()) + { + no_crossing = false; + DBGPRINT ("CLIP: left: rts[0] = ", rts[0]) + DBGPRINTIF ((rts.size() == 2), "CLIP: left: rts[1] = ", rts[1]) + + Point corner1 = R.corner(TOP_LEFT); + Point corner2 = R.corner(BOTTOM_LEFT); + + for (size_t i = 0; i < rts.size(); ++i) + { + if (rts[i] < R.top() || rts[i] > R.bottom()) continue; + Point P (R.left(), rts[i]); + if (are_near (P, corner1)) + P = corner1; + else if (are_near (P, corner2)) + P = corner2; + + cpts.push_back (P); + } + if (cpts.size() == 2 && are_near (cpts[0], cpts[1])) + { + cpts[0] = middle_point (cpts[0], cpts[1]); + cpts.pop_back(); + } + } + + // bottom edge + cs.roots (rts, R.bottom(), Y); + if (!rts.empty()) + { + no_crossing = false; + DBGPRINT ("CLIP: bottom: rts[0] = ", rts[0]) + DBGPRINTIF ((rts.size() == 2), "CLIP: bottom: rts[1] = ", rts[1]) + + Point corner1 = R.corner(BOTTOM_RIGHT); + Point corner2 = R.corner(BOTTOM_LEFT); + + for (size_t i = 0; i < rts.size(); ++i) + { + if (rts[i] < R.left() || rts[i] > R.right()) continue; + Point P (rts[i], R.bottom()); + if (are_near (P, corner1)) + P = corner1; + else if (are_near (P, corner2)) + P = corner2; + + cpts.push_back (P); + } + if (cpts.size() == 2 && are_near (cpts[0], cpts[1])) + { + cpts[0] = middle_point (cpts[0], cpts[1]); + cpts.pop_back(); + } + } + + DBGPRINT ("CLIP: intersect: crossing_points.size (with duplicates) = ", + cpts.size()) + + // remove duplicates + std::sort (cpts.begin(), cpts.end(), Point::LexLess()); + cpts.erase (std::unique (cpts.begin(), cpts.end()), cpts.end()); + + + // Order crossing points on the rectangle edge clockwise, so two consecutive + // crossing points would be the end points of a conic arc all inside or all + // outside the rectangle. + std::map cp_angles; + for (size_t i = 0; i < cpts.size(); ++i) + { + cp_angles.insert (std::make_pair (cs.angle_at (cpts[i]), i)); + } + + std::map::const_iterator pos; + for (pos = cp_angles.begin(); pos != cp_angles.end(); ++pos) + { + crossing_points.push_back (cpts[pos->second]); + } + + DBGPRINT ("CLIP: intersect: crossing_points.size = ", crossing_points.size()) + DBGPRINTCOLL ("CLIP: intersect: crossing_points:", crossing_points) + + return no_crossing; +} // end function intersect + + + +inline +double signed_triangle_area (Point const& p1, Point const& p2, Point const& p3) +{ + return (cross(p2, p3) - cross(p1, p3) + cross(p1, p2)); +} + + +/* + * Test if two crossing points are the end points of a conic arc inner to the + * rectangle. In such a case the method returns true, else it returns false. + * Moreover by the parameter "M" it returns a point inner to the conic arc + * with the given end-points. + * + */ +bool CLIPPER_CLASS::are_paired (Point& M, const Point & P1, const Point & P2) const +{ + using std::swap; + + /* + * we looks for the points on the conic whose tangent is parallel to the + * arc chord P1P2, they will be extrema of the conic arc P1P2 wrt the + * direction orthogonal to the chord + */ + Point dir = P2 - P1; + DBGPRINT ("CLIP: are_paired: first point: ", P1) + DBGPRINT ("CLIP: are_paired: second point: ", P2) + + double grad0 = 2 * cs.coeff(0) * dir[0] + cs.coeff(1) * dir[1]; + double grad1 = cs.coeff(1) * dir[0] + 2 * cs.coeff(2) * dir[1]; + double grad2 = cs.coeff(3) * dir[0] + cs.coeff(4) * dir[1]; + + + /* + * such points are found intersecating the conic section with the line + * orthogonal to "grad": the derivative wrt the "dir" direction + */ + Line gl (grad0, grad1, grad2); + std::vector rts; + rts = cs.roots (gl); + DBGPRINT ("CLIP: are_paired: extrema: rts.size() = ", rts.size()) + + + + std::vector extrema; + for (size_t i = 0; i < rts.size(); ++i) + { + extrema.push_back (gl.pointAt (rts[i])); + } + + if (extrema.size() == 2) + { + // in case we are dealing with an hyperbola we could have two extrema + // on the same side wrt the line passing through P1 and P2, but + // only the nearer extremum is on the arc P1P2 + double side0 = signed_triangle_area (P1, extrema[0], P2); + double side1 = signed_triangle_area (P1, extrema[1], P2); + + if (sgn(side0) == sgn(side1)) + { + if (std::fabs(side0) > std::fabs(side1)) { + swap(extrema[0], extrema[1]); + } + extrema.pop_back(); + } + } + + std::vector inner_points; + for (size_t i = 0; i < extrema.size(); ++i) + { + if (!R.contains (extrema[i])) continue; + // in case we are dealing with an ellipse tangent to two orthogonal + // rectangle edges we could have two extrema on opposite sides wrt the + // line passing through P1P2 and both inner the rectangle; anyway, since + // we order the crossing points clockwise we have only one extremum + // that follows such an ordering wrt P1 and P2; + // remark: the other arc will be selected when we test for the arc P2P1. + double P1angle = cs.angle_at (P1); + double P2angle = cs.angle_at (P2); + double Qangle = cs.angle_at (extrema[i]); + if (P1angle < P2angle && !(P1angle <= Qangle && Qangle <= P2angle)) + continue; + if (P1angle > P2angle && !(P1angle <= Qangle || Qangle <= P2angle)) + continue; + + inner_points.push_back (extrema[i]); + } + + if (inner_points.size() > 1) + { + THROW_LOGICALERROR ("conic section clipper: " + "more than one extremum found"); + } + else if (inner_points.size() == 1) + { + M = inner_points.front(); + return true; + } + + return false; +} + + +/* + * Pair the points contained in the "crossing_points" vector; the paired points + * are put in the paired_points vector so that given a point with an even index + * and the next one they are the end points of a conic arc that is inner to the + * rectangle. In the "inner_points" are returned points that are inner to the + * arc, where the inner point with index k is related to the arc with end + * points with indexes 2k, 2k+1. In case there are unpaired points the are put + * in to the "single_points" vector. + */ +void CLIPPER_CLASS::pairing (std::vector & paired_points, + std::vector & inner_points, + const std::vector & crossing_points) +{ + paired_points.clear(); + paired_points.reserve (crossing_points.size()); + + inner_points.clear(); + inner_points.reserve (crossing_points.size() / 2); + + single_points.clear(); + + // to keep trace of which crossing points have been paired + std::vector paired (crossing_points.size(), false); + + Point M; + + // by the way we have ordered crossing points we need to test one point wrt + // the next point only, for pairing; moreover the last point need to be + // tested wrt the first point; pay attention: one point can be paired both + // with the previous and the next one: this is not an error, think of + // crossing points that are tangent to the rectangle edge (and inner); + for (size_t i = 0; i < crossing_points.size(); ++i) + { + // we need to test the last point wrt the first one + size_t j = (i == 0) ? (crossing_points.size() - 1) : (i-1); + if (are_paired (M, crossing_points[j], crossing_points[i])) + { +#ifdef CLIP_WITH_CAIRO_SUPPORT + cairo_set_source_rgba(cr, 0.1, 0.1, 0.8, 1.0); + draw_line_seg (cr, crossing_points[j], crossing_points[i]); + draw_handle (cr, crossing_points[j]); + draw_handle (cr, crossing_points[i]); + draw_handle (cr, M); + cairo_stroke (cr); +#endif + paired[j] = paired[i] = true; + paired_points.push_back (crossing_points[j]); + paired_points.push_back (crossing_points[i]); + inner_points.push_back (M); + } + } + + // some point are not paired with any point, e.g. a crossing point tangent + // to a rectangle edge but with the conic arc outside the rectangle + for (size_t i = 0; i < paired.size(); ++i) + { + if (!paired[i]) + single_points.push_back (crossing_points[i]); + } + DBGPRINTCOLL ("single_points", single_points) + +} + + +/* + * This method clip the section conic wrt the rectangle and returns the inner + * conic arcs as a vector of RatQuad objects by the "arcs" parameter. + */ +bool CLIPPER_CLASS::clip (std::vector & arcs) +{ + using std::swap; + + arcs.clear(); + std::vector crossing_points; + std::vector paired_points; + std::vector inner_points; + + Line l1, l2; + if (cs.decompose (l1, l2)) + { + bool inner_empty = true; + + DBGINFO ("CLIP: degenerate section conic") + + boost::optional ls1 = Geom::clip (l1, R); + if (ls1) + { + if (ls1->isDegenerate()) + { + single_points.push_back (ls1->initialPoint()); + } + else + { + Point M = middle_point (*ls1); + arcs.push_back + (RatQuad (ls1->initialPoint(), M, ls1->finalPoint(), 1)); + inner_empty = false; + } + } + + boost::optional ls2 = Geom::clip (l2, R); + if (ls2) + { + if (ls2->isDegenerate()) + { + single_points.push_back (ls2->initialPoint()); + } + else + { + Point M = middle_point (*ls2); + arcs.push_back + (RatQuad (ls2->initialPoint(), M, ls2->finalPoint(), 1)); + inner_empty = false; + } + } + + return !inner_empty; + } + + + bool no_crossing = intersect (crossing_points); + + // if the only crossing point is a rectangle corner than the section conic + // is all outside the rectangle + if (crossing_points.size() == 1) + { + for (size_t i = 0; i < 4; ++i) + { + if (crossing_points[0] == R.corner(i)) + { + single_points.push_back (R.corner(i)); + return false; + } + } + } + + // if the conic does not cross any line passing through a rectangle edge or + // it is tangent to only one edge then it is an ellipse + if (no_crossing + || (crossing_points.size() == 1 && single_points.empty())) + { + // if the ellipse centre is inside the rectangle + // then so it is the ellipse + boost::optional c = cs.centre(); + if (c && R.contains (*c)) + { + DBGPRINT ("CLIP: ellipse with centre", *c) + // we set paired and inner points by finding the ellipse + // intersection with its axes; this choice let us having a more + // accurate RatQuad parametric arc + paired_points.resize(4); + std::vector rts; + double angle = cs.axis_angle(); + Line axis1 (*c, angle); + rts = cs.roots (axis1); + if (rts[0] > rts[1]) swap (rts[0], rts[1]); + paired_points[0] = axis1.pointAt (rts[0]); + paired_points[1] = axis1.pointAt (rts[1]); + paired_points[2] = paired_points[1]; + paired_points[3] = paired_points[0]; + Line axis2 (*c, angle + M_PI/2); + rts = cs.roots (axis2); + if (rts[0] > rts[1]) swap (rts[0], rts[1]); + inner_points.push_back (axis2.pointAt (rts[0])); + inner_points.push_back (axis2.pointAt (rts[1])); + } + else if (crossing_points.size() == 1) + { + // so we have a tangent crossing point but the ellipse is outside + // the rectangle + single_points.push_back (crossing_points[0]); + } + } + else + { + // in case the conic section intersects any of the four lines passing + // through the rectangle edges but it does not cross any rectangle edge + // then the conic is all outer of the rectangle + if (crossing_points.empty()) return false; + // else we need to pair crossing points, and to find an arc inner point + // in order to generate a RatQuad object + pairing (paired_points, inner_points, crossing_points); + } + + + // we split arcs until the end-point distance is less than a given value, + // in this way the RatQuad parametrization is enough accurate + std::list points; + std::list::iterator sp, ip, fp; + for (size_t i = 0, j = 0; i < paired_points.size(); i += 2, ++j) + { + //DBGPRINT ("CLIP: clip: P = ", paired_points[i]) + //DBGPRINT ("CLIP: clip: M = ", inner_points[j]) + //DBGPRINT ("CLIP: clip: Q = ", paired_points[i+1]) + + // in case inner point and end points are near is better not split + // the conic arc further or we could get a degenerate RatQuad object + if (are_near (paired_points[i], inner_points[j], 1e-4) + && are_near (paired_points[i+1], inner_points[j], 1e-4)) + { + arcs.push_back (cs.toRatQuad (paired_points[i], + inner_points[j], + paired_points[i+1])); + continue; + } + + // populate the list + points.push_back(paired_points[i]); + points.push_back(inner_points[j]); + points.push_back(paired_points[i+1]); + + // an initial unconditioned splitting + sp = points.begin(); + ip = sp; ++ip; + fp = ip; ++fp; + rsplit (points, sp, ip, size_t(1u)); + rsplit (points, ip, fp, size_t(1u)); + + // length conditioned split + sp = points.begin(); + fp = sp; ++fp; + while (fp != points.end()) + { + rsplit (points, sp, fp, 100.0); + sp = fp; + ++fp; + } + + sp = points.begin(); + ip = sp; ++ip; + fp = ip; ++fp; + //DBGPRINT ("CLIP: points ", j) + //DBGPRINT ("CLIP: points.size = ", points.size()) + while (ip != points.end()) + { +#ifdef CLIP_WITH_CAIRO_SUPPORT + cairo_set_source_rgba(cr, 0.1, 0.1, 0.8, 1.0); + draw_handle (cr, *sp); + draw_handle (cr, *ip); + cairo_stroke (cr); +#endif + //std::cerr << "CLIP: arc: [" << *sp << ", " << *ip << ", " + // << *fp << "]" << std::endl; + arcs.push_back (cs.toRatQuad (*sp, *ip, *fp)); + sp = fp; + ip = sp; ++ip; + fp = ip; ++fp; + } + points.clear(); + } + DBGPRINT ("CLIP: arcs.size() = ", arcs.size()) + return (arcs.size() != 0); +} // end method clip + + +} // end namespace geom + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conic_section_clipper_impl.h b/src/2geom/conic_section_clipper_impl.h new file mode 100644 index 0000000..2e8fd24 --- /dev/null +++ b/src/2geom/conic_section_clipper_impl.h @@ -0,0 +1,346 @@ +/** @file + * @brief Conic section clipping with respect to a rectangle + *//* + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_IMPL_H +#define LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_IMPL_H + + +#include <2geom/conicsec.h> +#include <2geom/line.h> + +#include +#include + + + +#ifdef CLIP_WITH_CAIRO_SUPPORT + #include <2geom/toys/path-cairo.h> + #define CLIPPER_CLASS clipper_cr +#else + #define CLIPPER_CLASS clipper +#endif + +//#define CLIPDBG + +#ifdef CLIPDBG +#include <2geom/toys/path-cairo.h> +#define DBGINFO(msg) \ + std::cerr << msg << std::endl; +#define DBGPRINT(msg, var) \ + std::cerr << msg << var << std::endl; +#define DBGPRINTIF(cond, msg, var) \ + if (cond) \ + std::cerr << msg << var << std::endl; + +#define DBGPRINT2(msg1, var1, msg2, var2) \ + std::cerr << msg1 << var1 << msg2 << var2 << std::endl; + +#define DBGPRINTCOLL(msg, coll) \ + if (coll.size() != 0) \ + std::cerr << msg << ":\n"; \ + for (size_t i = 0; i < coll.size(); ++i) \ + { \ + std::cerr << i << ": " << coll[i] << "\n"; \ + } + +#else +#define DBGINFO(msg) +#define DBGPRINT(msg, var) +#define DBGPRINTIF(cond, msg, var) +#define DBGPRINT2(msg1, var1, msg2, var2) +#define DBGPRINTCOLL(msg, coll) +#endif + + + + +namespace Geom +{ + +class CLIPPER_CLASS +{ + + public: + +#ifdef CLIP_WITH_CAIRO_SUPPORT + clipper_cr (cairo_t* _cr, const xAx & _cs, const Rect & _R) + : cr(_cr), cs(_cs), R(_R) + { + DBGPRINT ("CLIP: right side: ", R.right()) + DBGPRINT ("CLIP: top side: ", R.top()) + DBGPRINT ("CLIP: left side: ", R.left()) + DBGPRINT ("CLIP: bottom side: ", R.bottom()) + } +#else + clipper (const xAx & _cs, const Rect & _R) + : cs(_cs), R(_R) + { + } +#endif + + bool clip (std::vector & arcs); + + bool found_any_isolated_point() const + { + return ( !single_points.empty() ); + } + + const std::vector & isolated_points() const + { + return single_points; + } + + + private: + bool intersect (std::vector & crossing_points) const; + + bool are_paired (Point & M, const Point & P1, const Point & P2) const; + void pairing (std::vector & paired_points, + std::vector & inner_points, + const std::vector & crossing_points); + + Point find_inner_point_by_bisector_line (const Point & P, + const Point & Q) const; + Point find_inner_point (const Point & P, const Point & Q) const; + + std::list::iterator split (std::list & points, + std::list::iterator sp, + std::list::iterator fp) const; + void rsplit (std::list & points, + std::list::iterator sp, + std::list::iterator fp, + size_t k) const; + + void rsplit (std::list & points, + std::list::iterator sp, + std::list::iterator fp, + double length) const; + + private: +#ifdef CLIP_WITH_CAIRO_SUPPORT + cairo_t* cr; +#endif + const xAx & cs; + const Rect & R; + std::vector single_points; +}; + + + + +/* + * Given two point "P", "Q" on the conic section the method computes + * a third point inner to the arc with end-point "P", "Q". + * The new point is found by intersecting the conic with the bisector line + * of the PQ line segment. + */ +inline +Point CLIPPER_CLASS::find_inner_point_by_bisector_line (const Point & P, + const Point & Q) const +{ + DBGPRINT ("CLIP: find_inner_point_by_bisector_line: P = ", P) + DBGPRINT ("CLIP: find_inner_point_by_bisector_line: Q = ", Q) + Line bl = make_bisector_line (LineSegment (P, Q)); + std::vector rts = cs.roots (bl); + //DBGPRINT ("CLIP: find_inner_point: rts.size = ", rts.size()) + double t; + if (rts.size() == 0) + { + THROW_LOGICALERROR ("clipper::find_inner_point_by_bisector_line: " + "no conic-bisector line intersection point"); + } + if (rts.size() == 2) + { + // we suppose that the searched point is the nearest + // to the line segment PQ + t = (std::fabs(rts[0]) < std::fabs(rts[1])) ? rts[0] : rts[1]; + } + else + { + t = rts[0]; + } + return bl.pointAt (t); +} + + +/* + * Given two point "P", "Q" on the conic section the method computes + * a third point inner to the arc with end-point "P", "Q". + * The new point is found by intersecting the conic with the line + * passing through the middle point of the PQ line segment and + * the intersection point of the tangent lines at points P and Q. + */ +inline +Point CLIPPER_CLASS::find_inner_point (const Point & P, const Point & Q) const +{ + + Line l1 = cs.tangent (P); + Line l2 = cs.tangent (Q); + Line l; + // in case we fail to find a crossing point we fall back to the bisector + // method + try + { + OptCrossing oc = intersection(l1, l2); + if (!oc) + { + return find_inner_point_by_bisector_line (P, Q); + } + l.setPoints (l1.pointAt (oc->ta), middle_point (P, Q)); + } + catch (Geom::InfiniteSolutions e) + { + return find_inner_point_by_bisector_line (P, Q); + } + + std::vector rts = cs.roots (l); + double t; + if (rts.size() == 0) + { + return find_inner_point_by_bisector_line (P, Q); + } + // the line "l" origin is set to the tangent crossing point so in case + // we find two intersection points only the nearest belongs to the given arc + // pay attention: in case we are dealing with an hyperbola (remember that + // end points are on the same branch, because they are paired) the tangent + // crossing point belongs to the angle delimited by hyperbola asymptotes + // and containing the given hyperbola branch, so the previous statement is + // still true + if (rts.size() == 2) + { + t = (std::fabs(rts[0]) < std::fabs(rts[1])) ? rts[0] : rts[1]; + } + else + { + t = rts[0]; + } + return l.pointAt (t); +} + + +/* + * Given a list of points on the conic section, and given two consecutive + * points belonging to the list and passed by two list iterators, the method + * finds a new point that is inner to the conic arc which has the two passed + * points as initial and final point. This new point is inserted into the list + * between the two passed points and an iterator pointing to the new point + * is returned. + */ +inline +std::list::iterator CLIPPER_CLASS::split (std::list & points, + std::list::iterator sp, + std::list::iterator fp) const +{ + Point new_point = find_inner_point (*sp, *fp); + std::list::iterator ip = points.insert (fp, new_point); + //std::cerr << "CLIP: split: [" << *sp << ", " << *ip << ", " + // << *fp << "]" << std::endl; + return ip; +} + + +/* + * Given a list of points on the conic section, and given two consecutive + * points belonging to the list and passed by two list iterators, the method + * recursively finds new points that are inner to the conic arc which has + * the two passed points as initial and final point. The recursion stop after + * "k" recursive calls. These new points are inserted into the list between + * the two passed points, and in the order we cross them going from + * the initial to the final arc point. + */ +inline +void CLIPPER_CLASS::rsplit (std::list & points, + std::list::iterator sp, + std::list::iterator fp, + size_t k) const +{ + if (k == 0) + { + //DBGINFO("CLIP: split: no further split") + return; + } + + std::list::iterator ip = split (points, sp, fp); + --k; + rsplit (points, sp, ip, k); + rsplit (points, ip, fp, k); +} + + +/* + * Given a list of points on the conic section, and given two consecutive + * points belonging to the list and passed by two list iterators, the method + * recursively finds new points that are inner to the conic arc which has + * the two passed points as initial and final point. The recursion stop when + * the max distance between the new computed inner point and the two passed + * arc end-points is less then the value specified by the "length" parameter. + * These new points are inserted into the list between the two passed points, + * and in the order we cross them going from the initial to the final arc point. + */ +inline +void CLIPPER_CLASS::rsplit (std::list & points, + std::list::iterator sp, + std::list::iterator fp, + double length) const +{ + std::list::iterator ip = split (points, sp, fp); + double d1 = distance (*sp, *ip); + double d2 = distance (*ip, *fp); + double mdist = std::max (d1, d2); + + if (mdist < length) + { + //DBGINFO("CLIP: split: no further split") + return; + } + + // they have to be called both to keep the number of points in the list + // in the form 2k+1 where k are the sub-arcs the initial arc is split in. + rsplit (points, sp, ip, length); + rsplit (points, ip, fp, length); +} + + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_CONIC_SECTION_CLIPPER_IMPL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conicsec.cpp b/src/2geom/conicsec.cpp new file mode 100644 index 0000000..7172374 --- /dev/null +++ b/src/2geom/conicsec.cpp @@ -0,0 +1,1496 @@ +/* + * Authors: + * Nathan Hurst +#include <2geom/conic_section_clipper.h> +#include <2geom/numeric/fitting-tool.h> +#include <2geom/numeric/fitting-model.h> + + +// File: convert.h +#include +#include +#include + +namespace Geom +{ + +LineSegment intersection(Line l, Rect r) { + boost::optional seg = l.clip(r); + if (seg) { + return *seg; + } else { + return LineSegment(Point(0,0), Point(0,0)); + } +} + +static double det(Point a, Point b) { + return a[0]*b[1] - a[1]*b[0]; +} + +template +static T det(T a, T b, T c, T d) { + return a*d - b*c; +} + +template +static T det(T M[2][2]) { + return M[0][0]*M[1][1] - M[1][0]*M[0][1]; +} + +template +static T det3(T M[3][3]) { + return ( M[0][0] * det(M[1][1], M[1][2], + M[2][1], M[2][2]) + -M[1][0] * det(M[0][1], M[0][2], + M[2][1], M[2][2]) + +M[2][0] * det(M[0][1], M[0][2], + M[1][1], M[1][2])); +} + +static double boxprod(Point a, Point b, Point c) { + return det(a,b) - det(a,c) + det(b,c); +} + +class BadConversion : public std::runtime_error { +public: + BadConversion(const std::string& s) + : std::runtime_error(s) + { } +}; + +template +inline std::string stringify(T x) +{ + std::ostringstream o; + if (!(o << x)) + throw BadConversion("stringify(T)"); + return o.str(); +} + + /* A G4 continuous cubic parametric approximation for rational quadratics. + See + An analysis of cubic approximation schemes for conic sections + Michael Floater + SINTEF + + This is less accurate overall than some of his other schemes, but + produces very smooth joins and is still optimally h^-6 + convergent. + */ + +double RatQuad::lambda() const { + return 2*(6*w*w +1 -std::sqrt(3*w*w+1))/(12*w*w+3); +} + +RatQuad RatQuad::fromPointsTangents(Point P0, Point dP0, + Point P, + Point P2, Point dP2) { + Line Line0 = Line::from_origin_and_vector(P0, dP0); + Line Line2 = Line::from_origin_and_vector(P2, dP2); + try { + OptCrossing oc = intersection(Line0, Line2); + if(!oc) // what to do? + return RatQuad(Point(), Point(), Point(), 0); // need opt really + //assert(0); + Point P1 = Line0.pointAt((*oc).ta); + double triarea = boxprod(P0, P1, P2); +// std::cout << "RatQuad::fromPointsTangents: triarea = " << triarea << std::endl; + if (triarea == 0) + { + return RatQuad(P0, 0.5*(P0+P2), P2, 1); + } + double tau0 = boxprod(P, P1, P2)/triarea; + double tau1 = boxprod(P0, P, P2)/triarea; + double tau2 = boxprod(P0, P1, P)/triarea; + if (tau0 == 0 || tau1 == 0 || tau2 == 0) + { + return RatQuad(P0, 0.5*(P0+P2), P2, 1); + } + double w = tau1/(2*std::sqrt(tau0*tau2)); +// std::cout << "RatQuad::fromPointsTangents: tau0 = " << tau0 << std::endl; +// std::cout << "RatQuad::fromPointsTangents: tau1 = " << tau1 << std::endl; +// std::cout << "RatQuad::fromPointsTangents: tau2 = " << tau2 << std::endl; +// std::cout << "RatQuad::fromPointsTangents: w = " << w << std::endl; + return RatQuad(P0, P1, P2, w); + } catch(Geom::InfiniteSolutions) { + return RatQuad(P0, 0.5*(P0+P2), P2, 1); + } + return RatQuad(Point(), Point(), Point(), 0); // need opt really +} + +RatQuad RatQuad::circularArc(Point P0, Point P1, Point P2) { + return RatQuad(P0, P1, P2, dot(unit_vector(P0 - P1), unit_vector(P0 - P2))); +} + + +CubicBezier RatQuad::toCubic() const { + return toCubic(lambda()); +} + +CubicBezier RatQuad::toCubic(double lamb) const { + return CubicBezier(P[0], + (1-lamb)*P[0] + lamb*P[1], + (1-lamb)*P[2] + lamb*P[1], + P[2]); +} + +Point RatQuad::pointAt(double t) const { + Bezier xt(P[0][0], P[1][0]*w, P[2][0]); + Bezier yt(P[0][1], P[1][1]*w, P[2][1]); + double wt = Bezier(1, w, 1).valueAt(t); + return Point(xt.valueAt(t)/wt, + yt.valueAt(t)/wt); +} + +void RatQuad::split(RatQuad &a, RatQuad &b) const { + a.P[0] = P[0]; + b.P[2] = P[2]; + a.P[1] = (P[0]+w*P[1])/(1+w); + b.P[1] = (w*P[1]+P[2])/(1+w); + a.w = b.w = std::sqrt((1+w)/2); + a.P[2] = b.P[0] = (0.5*a.P[1]+0.5*b.P[1]); +} + + +D2 RatQuad::hermite() const { + SBasis t = Linear(0, 1); + SBasis omt = Linear(1, 0); + + D2 out(omt*omt*P[0][0]+2*omt*t*P[1][0]*w+t*t*P[2][0], + omt*omt*P[0][1]+2*omt*t*P[1][1]*w+t*t*P[2][1]); + for(int dim = 0; dim < 2; dim++) { + out[dim] = divide(out[dim], (omt*omt+2*omt*t*w+t*t), 2); + } + return out; +} + + std::vector RatQuad::homogeneous() const { + std::vector res(3, SBasis()); + Bezier xt(P[0][0], P[1][0]*w, P[2][0]); + bezier_to_sbasis(res[0],xt); + Bezier yt(P[0][1], P[1][1]*w, P[2][1]); + bezier_to_sbasis(res[1],yt); + Bezier wt(1, w, 1); + bezier_to_sbasis(res[2],wt); + return res; +} + +#if 0 + std::string xAx::categorise() const { + double M[3][3] = {{c[0], c[1], c[3]}, + {c[1], c[2], c[4]}, + {c[3], c[4], c[5]}}; + double D = det3(M); + if (c[0] == 0 && c[1] == 0 && c[2] == 0) + return "line"; + std::string res = stringify(D); + double descr = c[1]*c[1] - c[0]*c[2]; + if (descr < 0) { + if (c[0] == c[2] && c[1] == 0) + return res + "circle"; + return res + "ellipse"; + } else if (descr == 0) { + return res + "parabola"; + } else if (descr > 0) { + if (c[0] + c[2] == 0) { + if (D == 0) + return res + "two lines"; + return res + "rectangular hyperbola"; + } + return res + "hyperbola"; + + } + return "no idea!"; +} +#endif + + +std::vector decompose_degenerate(xAx const & C1, xAx const & C2, xAx const & xC0) { + std::vector res; + double A[2][2] = {{2*xC0.c[0], xC0.c[1]}, + {xC0.c[1], 2*xC0.c[2]}}; +//Point B0 = xC0.bottom(); + double const determ = det(A); + //std::cout << determ << "\n"; + if (fabs(determ) >= 1e-20) { // hopeful, I know + Geom::Coord const ideterm = 1.0 / determ; + + double b[2] = {-xC0.c[3], -xC0.c[4]}; + Point B0((A[1][1]*b[0] -A[0][1]*b[1]), + (-A[1][0]*b[0] + A[0][0]*b[1])); + B0 *= ideterm; + Point n0, n1; + // Are these just the eigenvectors of A11? + if(xC0.c[0] == xC0.c[2]) { + double b = 0.5*xC0.c[1]/xC0.c[0]; + double c = xC0.c[2]/xC0.c[0]; + //assert(fabs(b*b-c) > 1e-10); + double d = std::sqrt(b*b-c); + //assert(fabs(b-d) > 1e-10); + n0 = Point(1, b+d); + n1 = Point(1, b-d); + } else if(fabs(xC0.c[0]) > fabs(xC0.c[2])) { + double b = 0.5*xC0.c[1]/xC0.c[0]; + double c = xC0.c[2]/xC0.c[0]; + //assert(fabs(b*b-c) > 1e-10); + double d = std::sqrt(b*b-c); + //assert(fabs(b-d) > 1e-10); + n0 = Point(1, b+d); + n1 = Point(1, b-d); + } else { + double b = 0.5*xC0.c[1]/xC0.c[2]; + double c = xC0.c[0]/xC0.c[2]; + //assert(fabs(b*b-c) > 1e-10); + double d = std::sqrt(b*b-c); + //assert(fabs(b-d) > 1e-10); + n0 = Point(b+d, 1); + n1 = Point(b-d, 1); + } + + Line L0 = Line::from_origin_and_vector(B0, rot90(n0)); + Line L1 = Line::from_origin_and_vector(B0, rot90(n1)); + + std::vector rts = C1.roots(L0); + for(unsigned i = 0; i < rts.size(); i++) { + Point P = L0.pointAt(rts[i]); + res.push_back(P); + } + rts = C1.roots(L1); + for(unsigned i = 0; i < rts.size(); i++) { + Point P = L1.pointAt(rts[i]); + res.push_back(P); + } + } else { + // single or double line + // check for completely zero case (what to do?) + assert(xC0.c[0] || xC0.c[1] || + xC0.c[2] || xC0.c[3] || + xC0.c[4] || xC0.c[5]); + Point trial_pt(0,0); + Point g = xC0.gradient(trial_pt); + if(L2sq(g) == 0) { + trial_pt[0] += 1; + g = xC0.gradient(trial_pt); + if(L2sq(g) == 0) { + trial_pt[1] += 1; + g = xC0.gradient(trial_pt); + if(L2sq(g) == 0) { + trial_pt[0] += 1; + g = xC0.gradient(trial_pt); + if(L2sq(g) == 0) { + trial_pt = Point(1.5,0.5); + g = xC0.gradient(trial_pt); + } + } + } + } + //std::cout << trial_pt << ", " << g << "\n"; + /** + * At this point we have tried up to 4 points: 0,0, 1,0, 1,1, 2,1, 1.5,1.5 + * + * No degenerate conic can pass through these points, so we can assume + * that we've found a perpendicular to the double line. + * Proof: + * any degenerate must consist of at most 2 lines. 1.5,0.5 is not on any pair of lines + * passing through the previous 4 trials. + * + * alternatively, there may be a way to determine this directly from xC0 + */ + assert(L2sq(g) != 0); + + Line Lx = Line::from_origin_and_vector(trial_pt, g); // a line along the gradient + std::vector rts = xC0.roots(Lx); + for(unsigned i = 0; i < rts.size(); i++) { + Point P0 = Lx.pointAt(rts[i]); + //std::cout << P0 << "\n"; + Line L = Line::from_origin_and_vector(P0, rot90(g)); + std::vector cnrts; + // It's very likely that at least one of the conics is degenerate, this will hopefully pick the more generate of the two. + if(fabs(C1.hessian().det()) > fabs(C2.hessian().det())) + cnrts = C1.roots(L); + else + cnrts = C2.roots(L); + for(unsigned j = 0; j < cnrts.size(); j++) { + Point P = L.pointAt(cnrts[j]); + res.push_back(P); + } + } + } + return res; +} + +double xAx_descr(xAx const & C) { + double mC[3][3] = {{C.c[0], (C.c[1])/2, (C.c[3])/2}, + {(C.c[1])/2, C.c[2], (C.c[4])/2}, + {(C.c[3])/2, (C.c[4])/2, C.c[5]}}; + + return det3(mC); +} + + +std::vector intersect(xAx const & C1, xAx const & C2) { + // You know, if either of the inputs are degenerate we should use them first! + if(xAx_descr(C1) == 0) { + return decompose_degenerate(C1, C2, C1); + } + if(xAx_descr(C2) == 0) { + return decompose_degenerate(C1, C2, C2); + } + std::vector res; + SBasis T(Linear(-1,1)); + SBasis S(Linear(1,1)); + SBasis C[3][3] = {{T*C1.c[0]+S*C2.c[0], (T*C1.c[1]+S*C2.c[1])/2, (T*C1.c[3]+S*C2.c[3])/2}, + {(T*C1.c[1]+S*C2.c[1])/2, T*C1.c[2]+S*C2.c[2], (T*C1.c[4]+S*C2.c[4])/2}, + {(T*C1.c[3]+S*C2.c[3])/2, (T*C1.c[4]+S*C2.c[4])/2, T*C1.c[5]+S*C2.c[5]}}; + + SBasis D = det3(C); + std::vector rts = Geom::roots(D); + if(rts.empty()) { + T = Linear(1,1); + S = Linear(-1,1); + SBasis C[3][3] = {{T*C1.c[0]+S*C2.c[0], (T*C1.c[1]+S*C2.c[1])/2, (T*C1.c[3]+S*C2.c[3])/2}, + {(T*C1.c[1]+S*C2.c[1])/2, T*C1.c[2]+S*C2.c[2], (T*C1.c[4]+S*C2.c[4])/2}, + {(T*C1.c[3]+S*C2.c[3])/2, (T*C1.c[4]+S*C2.c[4])/2, T*C1.c[5]+S*C2.c[5]}}; + + D = det3(C); + rts = Geom::roots(D); + } + // at this point we have a T and S and perhaps some roots that represent our degenerate conic + // Let's just pick one randomly (can we do better?) + //for(unsigned i = 0; i < rts.size(); i++) { + if(!rts.empty()) { + unsigned i = 0; + double t = T.valueAt(rts[i]); + double s = S.valueAt(rts[i]); + xAx xC0 = C1*t + C2*s; + //::draw(cr, xC0, screen_rect); // degen + + return decompose_degenerate(C1, C2, xC0); + + + } else { + std::cout << "What?" << std::endl; + ;//std::cout << D << "\n"; + } + return res; +} + + +xAx xAx::fromPoint(Point p) { + return xAx(1., 0, 1., -2*p[0], -2*p[1], dot(p,p)); +} + +xAx xAx::fromDistPoint(Point /*p*/, double /*d*/) { + return xAx();//1., 0, 1., -2*(1+d)*p[0], -2*(1+d)*p[1], dot(p,p)+d*d); +} + +xAx xAx::fromLine(Point n, double d) { + return xAx(n[0]*n[0], 2*n[0]*n[1], n[1]*n[1], 2*d*n[0], 2*d*n[1], d*d); +} + +xAx xAx::fromLine(Line l) { + double dist; + Point norm = l.normalAndDist(dist); + + return fromLine(norm, dist); +} + +xAx xAx::fromPoints(std::vector const &pt) { + Geom::NL::Vector V(pt.size(), -1.0); + Geom::NL::Matrix M(pt.size(), 5); + for(unsigned i = 0; i < pt.size(); i++) { + Geom::Point P = pt[i]; + Geom::NL::VectorView vv = M.row_view(i); + vv[0] = P[0]*P[0]; + vv[1] = P[0]*P[1]; + vv[2] = P[1]*P[1]; + vv[3] = P[0]; + vv[4] = P[1]; + } + + Geom::NL::LinearSystem ls(M, V); + + Geom::NL::Vector x = ls.SV_solve(); + return Geom::xAx(x[0], x[1], x[2], x[3], x[4], 1); + +} + + + +double xAx::valueAt(Point P) const { + return evaluate_at(P[0], P[1]); +} + +xAx xAx::scale(double sx, double sy) const { + return xAx(c[0]*sx*sx, c[1]*sx*sy, c[2]*sy*sy, + c[3]*sx, c[4]*sy, c[5]); +} + +Point xAx::gradient(Point p) const{ + double x = p[0]; + double y = p[1]; + return Point(2*c[0]*x + c[1]*y + c[3], + c[1]*x + 2*c[2]*y + c[4]); +} + +xAx xAx::operator-(xAx const &b) const { + xAx res; + for(int i = 0; i < 6; i++) { + res.c[i] = c[i] - b.c[i]; + } + return res; +} +xAx xAx::operator+(xAx const &b) const { + xAx res; + for(int i = 0; i < 6; i++) { + res.c[i] = c[i] + b.c[i]; + } + return res; +} +xAx xAx::operator+(double const &b) const { + xAx res; + for(int i = 0; i < 5; i++) { + res.c[i] = c[i]; + } + res.c[5] = c[5] + b; + return res; +} + +xAx xAx::operator*(double const &b) const { + xAx res; + for(int i = 0; i < 6; i++) { + res.c[i] = c[i] * b; + } + return res; +} + + std::vector xAx::crossings(Rect r) const { + std::vector res; + for(int ei = 0; ei < 4; ei++) { + Geom::LineSegment ls(r.corner(ei), r.corner(ei+1)); + D2 lssb = ls.toSBasis(); + SBasis edge_curve = evaluate_at(lssb[0], lssb[1]); + std::vector rts = Geom::roots(edge_curve); + for(unsigned eci = 0; eci < rts.size(); eci++) { + res.push_back(lssb.valueAt(rts[eci])); + } + } + return res; +} + + boost::optional xAx::toCurve(Rect const & bnd) const { + std::vector crs = crossings(bnd); + if(crs.size() == 1) { + Point A = crs[0]; + Point dA = rot90(gradient(A)); + if(L2sq(dA) <= 1e-10) { // perhaps a single point? + return boost::optional (); + } + LineSegment ls = intersection(Line::from_origin_and_vector(A, dA), bnd); + return RatQuad::fromPointsTangents(A, dA, ls.pointAt(0.5), ls[1], dA); + } + else if(crs.size() >= 2 && crs.size() < 4) { + Point A = crs[0]; + Point C = crs[1]; + if(crs.size() == 3) { + if(distance(A, crs[2]) > distance(A, C)) + C = crs[2]; + else if(distance(C, crs[2]) > distance(A, C)) + A = crs[2]; + } + Line bisector = make_bisector_line(LineSegment(A, C)); + std::vector bisect_rts = this->roots(bisector); + if(!bisect_rts.empty()) { + int besti = -1; + for(unsigned i =0; i < bisect_rts.size(); i++) { + Point p = bisector.pointAt(bisect_rts[i]); + if(bnd.contains(p)) { + besti = i; + } + } + if(besti >= 0) { + Point B = bisector.pointAt(bisect_rts[besti]); + + Point dA = gradient(A); + Point dC = gradient(C); + if(L2sq(dA) <= 1e-10 || L2sq(dC) <= 1e-10) { + return RatQuad::fromPointsTangents(A, C-A, B, C, A-C); + } + + RatQuad rq = RatQuad::fromPointsTangents(A, rot90(dA), + B, C, rot90(dC)); + return rq; + //std::vector hrq = rq.homogeneous(); + /*SBasis vertex_poly = evaluate_at(hrq[0], hrq[1], hrq[2]); + std::vector rts = roots(vertex_poly); + for(unsigned i = 0; i < rts.size(); i++) { + //draw_circ(cr, Point(rq.pointAt(rts[i]))); + }*/ + } + } + } + return boost::optional(); +} + + std::vector xAx::roots(Point d, Point o) const { + // Find the roots on line l + // form the quadratic Q(t) = 0 by composing l with xAx + double q2 = c[0]*d[0]*d[0] + c[1]*d[0]*d[1] + c[2]*d[1]*d[1]; + double q1 = (2*c[0]*d[0]*o[0] + + c[1]*(d[0]*o[1]+d[1]*o[0]) + + 2*c[2]*d[1]*o[1] + + c[3]*d[0] + c[4]*d[1]); + double q0 = c[0]*o[0]*o[0] + c[1]*o[0]*o[1] + c[2]*o[1]*o[1] + c[3]*o[0] + c[4]*o[1] + c[5]; + std::vector r; + if(q2 == 0) { + if(q1 == 0) { + return r; + } + r.push_back(-q0/q1); + } else { + double desc = q1*q1 - 4*q2*q0; + /*std::cout << q2 << ", " + << q1 << ", " + << q0 << "; " + << desc << "\n";*/ + if (desc < 0) + return r; + else if (desc == 0) + r.push_back(-q1/(2*q2)); + else { + desc = std::sqrt(desc); + double t; + if (q1 == 0) + { + t = -0.5 * desc; + } + else + { + t = -0.5 * (q1 + sgn(q1) * desc); + } + r.push_back(t/q2); + r.push_back(q0/t); + } + } + return r; +} + +std::vector xAx::roots(Line const &l) const { + return roots(l.versor(), l.origin()); +} + +Interval xAx::quad_ex(double a, double b, double c, Interval ivl) { + double cx = -b*0.5/a; + Interval bnds((a*ivl.min()+b)*ivl.min()+c, (a*ivl.max()+b)*ivl.max()+c); + if(ivl.contains(cx)) + bnds.expandTo((a*cx+b)*cx+c); + return bnds; +} + +Geom::Affine xAx::hessian() const { + Geom::Affine m(2*c[0], c[1], + c[1], 2*c[2], + 0, 0); + return m; +} + + +boost::optional solve(double A[2][2], double b[2]) { + double const determ = det(A); + if (determ != 0.0) { // hopeful, I know + Geom::Coord const ideterm = 1.0 / determ; + + return Point ((A[1][1]*b[0] -A[0][1]*b[1]), + (-A[1][0]*b[0] + A[0][0]*b[1]))* ideterm; + } else { + return boost::optional(); + } +} + +boost::optional xAx::bottom() const { + double A[2][2] = {{2*c[0], c[1]}, + {c[1], 2*c[2]}}; + double b[2] = {-c[3], -c[4]}; + return solve(A, b); + //return Point(-c[3], -c[4])*hessian().inverse(); +} + +Interval xAx::extrema(Rect r) const { + if (c[0] == 0 && c[1] == 0 && c[2] == 0) { + Interval ext(valueAt(r.corner(0))); + for(int i = 1; i < 4; i++) + ext |= Interval(valueAt(r.corner(i))); + return ext; + } + double k = r[X].min(); + Interval ext = quad_ex(c[2], c[1]*k+c[4], (c[0]*k + c[3])*k + c[5], r[Y]); + k = r[X].max(); + ext |= quad_ex(c[2], c[1]*k+c[4], (c[0]*k + c[3])*k + c[5], r[Y]); + k = r[Y].min(); + ext |= quad_ex(c[0], c[1]*k+c[3], (c[2]*k + c[4])*k + c[5], r[X]); + k = r[Y].max(); + ext |= quad_ex(c[0], c[1]*k+c[3], (c[2]*k + c[4])*k + c[5], r[X]); + boost::optional B0 = bottom(); + if (B0 && r.contains(*B0)) + ext.expandTo(0); + return ext; +} + + + + + + + + + +/* + * helper functions + */ + +bool at_infinity (Point const& p) +{ + if (p[X] == infinity() || p[X] == -infinity() + || p[Y] == infinity() || p[Y] == -infinity()) + { + return true; + } + return false; +} + +inline +double signed_triangle_area (Point const& p1, Point const& p2, Point const& p3) +{ + return (cross(p2, p3) - cross(p1, p3) + cross(p1, p2)); +} + + + +/* + * Define a conic section by computing the one that fits better with + * N points. + * + * points: points to fit + * + * precondition: there must be at least 5 non-overlapping points + */ +void xAx::set(std::vector const& points) +{ + size_t sz = points.size(); + if (sz < 5) + { + THROW_RANGEERROR("fitting error: too few points passed"); + } + NL::LFMConicSection model; + NL::least_squeares_fitter fitter(model, sz); + + for (size_t i = 0; i < sz; ++i) + { + fitter.append(points[i]); + } + fitter.update(); + + NL::Vector z(sz, 0.0); + model.instance(*this, fitter.result(z)); +} + +/* + * Define a section conic by providing the coordinates of one of its vertex, + * the major axis inclination angle and the coordinates of its foci + * with respect to the unidimensional system defined by the major axis with + * origin set at the provided vertex. + * + * _vertex : section conic vertex V + * _angle : section conic major axis angle + * _dist1: +/-distance btw V and nearest focus + * _dist2: +/-distance btw V and farest focus + * + * prerequisite: _dist1 <= _dist2 + */ +void xAx::set (const Point& _vertex, double _angle, double _dist1, double _dist2) +{ + using std::swap; + + if (_dist2 == infinity() || _dist2 == -infinity()) // parabola + { + if (_dist1 == infinity()) // degenerate to a line + { + Line l(_vertex, _angle); + std::vector lcoeff = l.coefficients(); + coeff(3) = lcoeff[0]; + coeff(4) = lcoeff[1]; + coeff(5) = lcoeff[2]; + return; + } + + // y^2 - 4px == 0 + double cD = -4 * _dist1; + + double cosa = std::cos (_angle); + double sina = std::sin (_angle); + double cca = cosa * cosa; + double ssa = sina * sina; + double csa = cosa * sina; + + coeff(0) = ssa; + coeff(1) = -2 * csa; + coeff(2) = cca; + coeff(3) = cD * cosa; + coeff(4) = cD * sina; + + double VxVx = _vertex[X] * _vertex[X]; + double VxVy = _vertex[X] * _vertex[Y]; + double VyVy = _vertex[Y] * _vertex[Y]; + + coeff(5) = coeff(0) * VxVx + coeff(1) * VxVy + coeff(2) * VyVy + - coeff(3) * _vertex[X] - coeff(4) * _vertex[Y]; + coeff(3) -= (2 * coeff(0) * _vertex[X] + coeff(1) * _vertex[Y]); + coeff(4) -= (2 * coeff(2) * _vertex[Y] + coeff(1) * _vertex[X]); + + return; + } + + if (std::fabs(_dist1) > std::fabs(_dist2)) + { + swap (_dist1, _dist2); + } + if (_dist1 < 0) + { + _angle -= M_PI; + _dist1 = -_dist1; + _dist2 = -_dist2; + } + + // ellipse and hyperbola + double lin_ecc = (_dist2 - _dist1) / 2; + double rx = (_dist2 + _dist1) / 2; + + double cA = rx * rx - lin_ecc * lin_ecc; + double cC = rx * rx; + double cF = - cA * cC; +// std::cout << "cA: " << cA << std::endl; +// std::cout << "cC: " << cC << std::endl; +// std::cout << "cF: " << cF << std::endl; + + double cosa = std::cos (_angle); + double sina = std::sin (_angle); + double cca = cosa * cosa; + double ssa = sina * sina; + double csa = cosa * sina; + + coeff(0) = cca * cA + ssa * cC; + coeff(2) = ssa * cA + cca * cC; + coeff(1) = 2 * csa * (cA - cC); + + Point C (rx * cosa + _vertex[X], rx * sina + _vertex[Y]); + double CxCx = C[X] * C[X]; + double CxCy = C[X] * C[Y]; + double CyCy = C[Y] * C[Y]; + + coeff(3) = -2 * coeff(0) * C[X] - coeff(1) * C[Y]; + coeff(4) = -2 * coeff(2) * C[Y] - coeff(1) * C[X]; + coeff(5) = cF + coeff(0) * CxCx + coeff(1) * CxCy + coeff(2) * CyCy; +} + +/* + * Define a conic section by providing one of its vertex and its foci. + * + * _vertex: section conic vertex + * _focus1: section conic focus + * _focus2: section conic focus + */ +void xAx::set (const Point& _vertex, const Point& _focus1, const Point& _focus2) +{ + if (at_infinity(_vertex)) + { + THROW_RANGEERROR("case not handled: vertex at infinity"); + } + if (at_infinity(_focus2)) + { + if (at_infinity(_focus1)) + { + THROW_RANGEERROR("case not handled: both focus at infinity"); + } + Point VF = _focus1 - _vertex; + double dist1 = L2(VF); + double angle = atan2(VF); + set(_vertex, angle, dist1, infinity()); + return; + } + else if (at_infinity(_focus1)) + { + Point VF = _focus2 - _vertex; + double dist1 = L2(VF); + double angle = atan2(VF); + set(_vertex, angle, dist1, infinity()); + return; + } + assert (are_collinear (_vertex, _focus1, _focus2)); + if (!are_near(_vertex, _focus1)) + { + Point VF = _focus1 - _vertex; + Line axis(_vertex, _focus1); + double angle = atan2(VF); + double dist1 = L2(VF); + double dist2 = distance (_vertex, _focus2); + double t = axis.timeAt(_focus2); + if (t < 0) dist2 = -dist2; +// std::cout << "t = " << t << std::endl; +// std::cout << "dist2 = " << dist2 << std::endl; + set (_vertex, angle, dist1, dist2); + } + else if (!are_near(_vertex, _focus2)) + { + Point VF = _focus2 - _vertex; + double angle = atan2(VF); + double dist1 = 0; + double dist2 = L2(VF); + set (_vertex, angle, dist1, dist2); + } + else + { + coeff(0) = coeff(2) = 1; + coeff(1) = coeff(3) = coeff(4) = coeff(5) = 0; + } +} + +/* + * Define a conic section by passing a focus, the related directrix, + * and the eccentricity (e) + * (e < 1 -> ellipse; e = 1 -> parabola; e > 1 -> hyperbola) + * + * _focus: a focus of the conic section + * _directrix: the directrix related to the given focus + * _eccentricity: the eccentricity parameter of the conic section + */ +void xAx::set (const Point & _focus, const Line & _directrix, double _eccentricity) +{ + Point O = _directrix.pointAt (_directrix.timeAtProjection (_focus)); + //std::cout << "O = " << O << std::endl; + Point OF = _focus - O; + double p = L2(OF); + + coeff(0) = 1 - _eccentricity * _eccentricity; + coeff(1) = 0; + coeff(2) = 1; + coeff(3) = -2 * p; + coeff(4) = 0; + coeff(5) = p * p; + + double angle = atan2 (OF); + + (*this) = rotate (angle); + //std::cout << "O = " << O << std::endl; + (*this) = translate (O); +} + +/* + * Made up a degenerate conic section as a pair of lines + * + * l1, l2: lines that made up the conic section + */ +void xAx::set (const Line& l1, const Line& l2) +{ + std::vector cl1 = l1.coefficients(); + std::vector cl2 = l2.coefficients(); + + coeff(0) = cl1[0] * cl2[0]; + coeff(2) = cl1[1] * cl2[1]; + coeff(5) = cl1[2] * cl2[2]; + coeff(1) = cl1[0] * cl2[1] + cl1[1] * cl2[0]; + coeff(3) = cl1[0] * cl2[2] + cl1[2] * cl2[0]; + coeff(4) = cl1[1] * cl2[2] + cl1[2] * cl2[1]; +} + + + +/* + * Return the section conic kind + */ +xAx::kind_t xAx::kind () const +{ + + xAx conic(*this); + NL::SymmetricMatrix<3> C = conic.get_matrix(); + NL::ConstSymmetricMatrixView<2> A = C.main_minor_const_view(); + + double t1 = trace(A); + double t2 = det(A); + //double T3 = det(C); + int st1 = trace_sgn(A); + int st2 = det_sgn(A); + int sT3 = det_sgn(C); + + //std::cout << "T3 = " << T3 << std::endl; + //std::cout << "sT3 = " << sT3 << std::endl; + //std::cout << "t2 = " << t2 << std::endl; + //std::cout << "t1 = " << t1 << std::endl; + //std::cout << "st2 = " << st2 << std::endl; + + if (sT3 != 0) + { + if (st2 == 0) + { + return PARABOLA; + } + else if (st2 == 1) + { + + if (sT3 * st1 < 0) + { + NL::SymmetricMatrix<2> discr; + discr(0,0) = 4; discr(1,1) = t2; discr(1,0) = t1; + int discr_sgn = - det_sgn (discr); + //std::cout << "t1 * t1 - 4 * t2 = " + // << (t1 * t1 - 4 * t2) << std::endl; + //std::cout << "discr_sgn = " << discr_sgn << std::endl; + if (discr_sgn == 0) + { + return CIRCLE; + } + else + { + return REAL_ELLIPSE; + } + } + else // sT3 * st1 > 0 + { + return IMAGINARY_ELLIPSE; + } + } + else // t2 < 0 + { + if (st1 == 0) + { + return RECTANGULAR_HYPERBOLA; + } + else + { + return HYPERBOLA; + } + } + } + else // T3 == 0 + { + if (st2 == 0) + { + //double T2 = NL::trace<2>(C); + int sT2 = NL::trace_sgn<2>(C); + //std::cout << "T2 = " << T2 << std::endl; + //std::cout << "sT2 = " << sT2 << std::endl; + + if (sT2 == 0) + { + return DOUBLE_LINE; + } + if (sT2 == -1) + { + return TWO_REAL_PARALLEL_LINES; + } + else // T2 > 0 + { + return TWO_IMAGINARY_PARALLEL_LINES; + } + } + else if (st2 == -1) + { + return TWO_REAL_CROSSING_LINES; + } + else // t2 > 0 + { + return TWO_IMAGINARY_CROSSING_LINES; + } + } + return UNKNOWN; +} + +/* + * Return a string representing the conic section kind + */ +std::string xAx::categorise() const +{ + kind_t KIND = kind(); + + switch (KIND) + { + case PARABOLA : + return "parabola"; + case CIRCLE : + return "circle"; + case REAL_ELLIPSE : + return "real ellispe"; + case IMAGINARY_ELLIPSE : + return "imaginary ellispe"; + case RECTANGULAR_HYPERBOLA : + return "rectangular hyperbola"; + case HYPERBOLA : + return "hyperbola"; + case DOUBLE_LINE : + return "double line"; + case TWO_REAL_PARALLEL_LINES : + return "two real parallel lines"; + case TWO_IMAGINARY_PARALLEL_LINES : + return "two imaginary parallel lines"; + case TWO_REAL_CROSSING_LINES : + return "two real crossing lines"; + case TWO_IMAGINARY_CROSSING_LINES : + return "two imaginary crossing lines"; + default : + return "unknown"; + } +} + +/* + * Compute the solutions of the conic section algebraic equation with respect to + * one coordinate after substituting to the other coordinate the passed value + * + * sol: the computed solutions + * v: the provided value + * d: the index of the coordinate the passed value have to be substituted to + */ +void xAx::roots (std::vector& sol, Coord v, Dim2 d) const +{ + sol.clear(); + if (d < 0 || d > Y) + { + THROW_RANGEERROR("dimension parameter out of range"); + } + + // p*t^2 + q*t + r = 0; + double p, q, r; + + if (d == X) + { + p = coeff(2); + q = coeff(4) + coeff(1) * v; + r = coeff(5) + (coeff(0) * v + coeff(3)) * v; + } + else + { + p = coeff(0); + q = coeff(3) + coeff(1) * v; + r = coeff(5) + (coeff(2) * v + coeff(4)) * v; + } + + if (p == 0) + { + if (q == 0) return; + double t = -r/q; + sol.push_back(t); + return; + } + + if (q == 0) + { + if ((p > 0 && r > 0) || (p < 0 && r < 0)) return; + double t = -r / p; + t = std::sqrt (t); + sol.push_back(-t); + sol.push_back(t); + return; + } + + if (r == 0) + { + double t = -q/p; + sol.push_back(0); + sol.push_back(t); + return; + } + + + //std::cout << "p = " << p << ", q = " << q << ", r = " << r << std::endl; + double delta = q * q - 4 * p * r; + if (delta < 0) return; + if (delta == 0) + { + double t = -q / (2 * p); + sol.push_back(t); + return; + } + // else + double srd = std::sqrt(delta); + double t = - (q + sgn(q) * srd) / 2; + sol.push_back (t/p); + sol.push_back (r/t); + +} + +/* + * Return the inclination angle of the major axis of the conic section + */ +double xAx::axis_angle() const +{ + if (coeff(0) == 0 && coeff(1) == 0 && coeff(2) == 0) + { + Line l (coeff(3), coeff(4), coeff(5)); + return l.angle(); + } + if (coeff(1) == 0 && (coeff(0) == coeff(2))) return 0; + + double angle; + + int sgn_discr = det_sgn (get_matrix().main_minor_const_view()); + if (sgn_discr == 0) + { + //std::cout << "rotation_angle: sgn_discr = " + // << sgn_discr << std::endl; + angle = std::atan2 (-coeff(1), 2 * coeff(2)); + if (angle < 0) angle += 2*M_PI; + if (angle >= M_PI) angle -= M_PI; + + } + else + { + angle = std::atan2 (coeff(1), coeff(0) - coeff(2)); + if (angle < 0) angle += 2*M_PI; + angle -= M_PI; + if (angle < 0) angle += 2*M_PI; + angle /= 2; + if (angle >= M_PI) angle -= M_PI; + } + //std::cout << "rotation_angle : angle = " << angle << std::endl; + return angle; +} + +/* + * Translate the conic section by the given vector offset + * + * _offset: represent the vector offset + */ +xAx xAx::translate (const Point & _offset) const +{ + double B = coeff(1) / 2; + double D = coeff(3) / 2; + double E = coeff(4) / 2; + + Point T = - _offset; + + xAx cs; + cs.coeff(0) = coeff(0); + cs.coeff(1) = coeff(1); + cs.coeff(2) = coeff(2); + + Point DE; + DE[0] = coeff(0) * T[0] + B * T[1]; + DE[1] = B * T[0] + coeff(2) * T[1]; + + cs.coeff(3) = (DE[0] + D) * 2; + cs.coeff(4) = (DE[1] + E) * 2; + + cs.coeff(5) = dot (T, DE) + 2 * (T[0] * D + T[1] * E) + coeff(5); + + return cs; +} + + +/* + * Rotate the conic section by the given angle wrt the point (0,0) + * + * angle: represent the rotation angle + */ +xAx xAx::rotate (double angle) const +{ + double c = std::cos(-angle); + double s = std::sin(-angle); + double cc = c * c; + double ss = s * s; + double cs = c * s; + + xAx result; + result.coeff(5) = coeff(5); + + // quadratic terms + double Bcs = coeff(1) * cs; + + result.coeff(0) = coeff(0) * cc + Bcs + coeff(2) * ss; + result.coeff(2) = coeff(0) * ss - Bcs + coeff(2) * cc; + result.coeff(1) = coeff(1) * (cc - ss) + 2 * (coeff(2) - coeff(0)) * cs; + + // linear terms + result.coeff(3) = coeff(3) * c + coeff(4) * s; + result.coeff(4) = coeff(4) * c - coeff(3) * s; + + return result; +} + + +/* + * Decompose a degenerate conic in two lines the conic section is made by. + * Return true if the decomposition is successful, else if it fails. + * + * l1, l2: out parameters where the decomposed conic section is returned + */ +bool xAx::decompose (Line& l1, Line& l2) const +{ + NL::SymmetricMatrix<3> C = get_matrix(); + if (!is_quadratic() || !isDegenerate()) + { + return false; + } + NL::Matrix M(C); + NL::SymmetricMatrix<3> D = -adj(C); + + if (!D.is_zero()) // D == 0 <=> rank(C) < 2 + { + + //if (D.get<0,0>() < 0 || D.get<1,1>() < 0 || D.get<2,2>() < 0) + //{ + //std::cout << "C: \n" << C << std::endl; + //std::cout << "D: \n" << D << std::endl; + + /* + * This case should be impossible because any diagonal element + * of D is a square, but due to non exact aritmethic computation + * it can actually happen; however the algorithm seems to work + * correctly even if some diagonal term is negative, the only + * difference is that we should compute the absolute value of + * diagonal elements. So until we elaborate a better degenerate + * test it's better not rising exception when we have a negative + * diagonal element. + */ + //} + + NL::Vector d(3); + d[0] = std::fabs (D.get<0,0>()); + d[1] = std::fabs (D.get<1,1>()); + d[2] = std::fabs (D.get<2,2>()); + + size_t idx = d.max_index(); + if (d[idx] == 0) + { + THROW_LOGICALERROR ("xAx::decompose: " + "rank 2 but adjoint with null diagonal"); + } + d[0] = D(idx,0); d[1] = D(idx,1); d[2] = D(idx,2); + d.scale (1 / std::sqrt (std::fabs (D(idx,idx)))); + M(1,2) += d[0]; M(2,1) -= d[0]; + M(0,2) -= d[1]; M(2,0) += d[1]; + M(0,1) += d[2]; M(1,0) -= d[2]; + + //std::cout << "C: \n" << C << std::endl; + //std::cout << "D: \n" << D << std::endl; + //std::cout << "d = " << d << std::endl; + //std::cout << "M = " << M << std::endl; + } + + std::pair max_ij = M.max_index(); + std::pair min_ij = M.min_index(); + double abs_max = std::fabs (M(max_ij.first, max_ij.second)); + double abs_min = std::fabs (M(min_ij.first, min_ij.second)); + size_t i_max, j_max; + if (abs_max > abs_min) + { + i_max = max_ij.first; + j_max = max_ij.second; + } + else + { + i_max = min_ij.first; + j_max = min_ij.second; + } + l1.setCoefficients (M(i_max,0), M(i_max,1), M(i_max,2)); + l2.setCoefficients (M(0, j_max), M(1,j_max), M(2,j_max)); + + return true; +} + + +/* + * Return the rectangle that bound the conic section arc characterized by + * the passed points. + * + * P1: the initial point of the arc + * Q: the inner point of the arc + * P2: the final point of the arc + * + * prerequisite: the passed points must lie on the conic + */ +Rect xAx::arc_bound (const Point & P1, const Point & Q, const Point & P2) const +{ + using std::swap; + //std::cout << "BOUND: P1 = " << P1 << std::endl; + //std::cout << "BOUND: Q = " << Q << std::endl; + //std::cout << "BOUND: P2 = " << P2 << std::endl; + + Rect B(P1, P2); + double Qside = signed_triangle_area (P1, Q, P2); + //std::cout << "BOUND: Qside = " << Qside << std::endl; + + Line gl[2]; + bool empty[2] = {false, false}; + + try // if the passed coefficients lead to an equation 0x + 0y + c == 0, + { // with c != 0 the setCoefficients rise an exception + gl[0].setCoefficients (coeff(1), 2 * coeff(2), coeff(4)); + } + catch(Geom::LogicalError const &e) + { + empty[0] = true; + } + + try + { + gl[1].setCoefficients (2 * coeff(0), coeff(1), coeff(3)); + } + catch(Geom::LogicalError const &e) + { + empty[1] = true; + } + + std::vector rts; + std::vector M; + for (size_t dim = 0; dim < 2; ++dim) + { + if (empty[dim]) continue; + rts = roots (gl[dim]); + M.clear(); + for (size_t i = 0; i < rts.size(); ++i) + M.push_back (gl[dim].pointAt (rts[i])); + if (M.size() == 1) + { + double Mside = signed_triangle_area (P1, M[0], P2); + if (sgn(Mside) == sgn(Qside)) + { + //std::cout << "BOUND: M.size() == 1" << std::endl; + B[dim].expandTo(M[0][dim]); + } + } + else if (M.size() == 2) + { + //std::cout << "BOUND: M.size() == 2" << std::endl; + if (M[0][dim] > M[1][dim]) + swap (M[0], M[1]); + + if (M[0][dim] > B[dim].max()) + { + double Mside = signed_triangle_area (P1, M[0], P2); + if (sgn(Mside) == sgn(Qside)) + B[dim].setMax(M[0][dim]); + } + else if (M[1][dim] < B[dim].min()) + { + double Mside = signed_triangle_area (P1, M[1], P2); + if (sgn(Mside) == sgn(Qside)) + B[dim].setMin(M[1][dim]); + } + else + { + double Mside = signed_triangle_area (P1, M[0], P2); + if (sgn(Mside) == sgn(Qside)) + B[dim].setMin(M[0][dim]); + Mside = signed_triangle_area (P1, M[1], P2); + if (sgn(Mside) == sgn(Qside)) + B[dim].setMax(M[1][dim]); + } + } + } + + return B; +} + +/* + * Return all points on the conic section nearest to the passed point "P". + * + * P: the point to compute the nearest one + */ +std::vector xAx::allNearestTimes (const Point &P) const +{ + // TODO: manage the circle - centre case + std::vector points; + + // named C the conic we look for points (x,y) on C such that + // dot (grad (C(x,y)), rot90 (P -(x,y))) == 0; the set of points satisfying + // this equation is still a conic G, so the wanted points can be found by + // intersecting C with G + xAx G (-coeff(1), + 2 * (coeff(0) - coeff(2)), + coeff(1), + -coeff(4) + coeff(1) * P[X] - 2 * coeff(0) * P[Y], + coeff(3) - coeff(1) * P[Y] + 2 * coeff(2) * P[X], + -coeff(3) * P[Y] + coeff(4) * P[X]); + + std::vector crs = intersect (*this, G); + + //std::cout << "NEAREST POINT: crs.size = " << crs.size() << std::endl; + if (crs.empty()) return points; + + size_t idx = 0; + double mindist = distanceSq (crs[0], P); + std::vector dist; + dist.push_back (mindist); + + for (size_t i = 1; i < crs.size(); ++i) + { + dist.push_back (distanceSq (crs[i], P)); + if (mindist > dist.back()) + { + idx = i; + mindist = dist.back(); + } + } + + points.push_back (crs[idx]); + for (size_t i = 0; i < crs.size(); ++i) + { + if (i == idx) continue; + if (dist[i] == mindist) + points.push_back (crs[i]); + } + + return points; +} + + + +bool clip (std::vector & rq, const xAx & cs, const Rect & R) +{ + clipper aclipper (cs, R); + return aclipper.clip (rq); +} + + +} // end namespace Geom + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/conicsec.h b/src/2geom/conicsec.h new file mode 100644 index 0000000..b84edc6 --- /dev/null +++ b/src/2geom/conicsec.h @@ -0,0 +1,518 @@ +/** @file + * @brief Conic Section + *//* + * Authors: + * Nathan Hurst + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef LIB2GEOM_SEEN_CONICSEC_H +#define LIB2GEOM_SEEN_CONICSEC_H + +#include <2geom/exception.h> +#include <2geom/angle.h> +#include <2geom/rect.h> +#include <2geom/affine.h> +#include <2geom/point.h> +#include <2geom/line.h> +#include <2geom/bezier-curve.h> +#include <2geom/numeric/linear_system.h> +#include <2geom/numeric/symmetric-matrix-fs.h> +#include <2geom/numeric/symmetric-matrix-fs-operation.h> + +#include + +#include +#include +#include + + + + +namespace Geom +{ + +class RatQuad{ + /** + * A curve of the form B02*A + B12*B*w + B22*C/(B02 + B12*w + B22) + * These curves can exactly represent a piece conic section less than a certain angle (find out) + * + **/ + +public: + Point P[3]; + double w; + RatQuad() {} + RatQuad(Point a, Point b, Point c, double w) : w(w) { + P[0] = a; + P[1] = b; + P[2] = c; + } + double lambda() const; + + static RatQuad fromPointsTangents(Point P0, Point dP0, + Point P, + Point P2, Point dP2); + static RatQuad circularArc(Point P0, Point P1, Point P2); + + CubicBezier toCubic() const; + CubicBezier toCubic(double lam) const; + + Point pointAt(double t) const; + Point at0() const {return P[0];} + Point at1() const {return P[2];} + + void split(RatQuad &a, RatQuad &b) const; + + D2 hermite() const; + std::vector homogeneous() const; +}; + + + + +class xAx{ +public: + double c[6]; + + enum kind_t + { + PARABOLA, + CIRCLE, + REAL_ELLIPSE, + IMAGINARY_ELLIPSE, + RECTANGULAR_HYPERBOLA, + HYPERBOLA, + DOUBLE_LINE, + TWO_REAL_PARALLEL_LINES, + TWO_IMAGINARY_PARALLEL_LINES, + TWO_REAL_CROSSING_LINES, + TWO_IMAGINARY_CROSSING_LINES, // crossing at a real point + SINGLE_POINT = TWO_IMAGINARY_CROSSING_LINES, + UNKNOWN + }; + + + xAx() {} + + /* + * Define the conic section by its algebraic equation coefficients + * + * c0, .., c5: equation coefficients + */ + xAx (double c0, double c1, double c2, double c3, double c4, double c5) + { + set (c0, c1, c2, c3, c4, c5); + } + + /* + * Define a conic section by its related symmetric matrix + */ + xAx (const NL::ConstSymmetricMatrixView<3> & C) + { + set(C); + } + + /* + * Define a conic section by computing the one that fits better with + * N points. + * + * points: points to fit + * + * precondition: there must be at least 5 non-overlapping points + */ + xAx (std::vector const& points) + { + set (points); + } + + /* + * Define a section conic by providing the coordinates of one of its + * vertex,the major axis inclination angle and the coordinates of its foci + * with respect to the unidimensional system defined by the major axis with + * origin set at the provided vertex. + * + * _vertex : section conic vertex V + * _angle : section conic major axis angle + * _dist1: +/-distance btw V and nearest focus + * _dist2: +/-distance btw V and farest focus + * + * prerequisite: _dist1 <= _dist2 + */ + xAx (const Point& _vertex, double _angle, double _dist1, double _dist2) + { + set (_vertex, _angle, _dist1, _dist2); + } + + /* + * Define a conic section by providing one of its vertex and its foci. + * + * _vertex: section conic vertex + * _focus1: section conic focus + * _focus2: section conic focus + */ + xAx (const Point& _vertex, const Point& _focus1, const Point& _focus2) + { + set(_vertex, _focus1, _focus2); + } + + /* + * Define a conic section by passing a focus, the related directrix, + * and the eccentricity (e) + * (e < 1 -> ellipse; e = 1 -> parabola; e > 1 -> hyperbola) + * + * _focus: a focus of the conic section + * _directrix: the directrix related to the given focus + * _eccentricity: the eccentricity parameter of the conic section + */ + xAx (const Point & _focus, const Line & _directrix, double _eccentricity) + { + set (_focus, _directrix, _eccentricity); + } + + /* + * Made up a degenerate conic section as a pair of lines + * + * l1, l2: lines that made up the conic section + */ + xAx (const Line& l1, const Line& l2) + { + set (l1, l2); + } + + /* + * Define the conic section by its algebraic equation coefficients + * c0, ..., c5: equation coefficients + */ + void set (double c0, double c1, double c2, double c3, double c4, double c5) + { + c[0] = c0; c[1] = c1; c[2] = c2; // xx, xy, yy + c[3] = c3; c[4] = c4; // x, y + c[5] = c5; // 1 + } + + /* + * Define a conic section by its related symmetric matrix + */ + void set (const NL::ConstSymmetricMatrixView<3> & C) + { + set(C(0,0), 2*C(1,0), C(1,1), 2*C(2,0), 2*C(2,1), C(2,2)); + } + + void set (std::vector const& points); + + void set (const Point& _vertex, double _angle, double _dist1, double _dist2); + + void set (const Point& _vertex, const Point& _focus1, const Point& _focus2); + + void set (const Point & _focus, const Line & _directrix, double _eccentricity); + + void set (const Line& l1, const Line& l2); + + + static xAx fromPoint(Point p); + static xAx fromDistPoint(Point p, double d); + static xAx fromLine(Point n, double d); + static xAx fromLine(Line l); + static xAx fromPoints(std::vector const &pts); + + + template + T evaluate_at(T x, T y) const { + return c[0]*x*x + c[1]*x*y + c[2]*y*y + c[3]*x + c[4]*y + c[5]; + } + + double valueAt(Point P) const; + + std::vector implicit_form_coefficients() const { + return std::vector(c, c+6); + } + + template + T evaluate_at(T x, T y, T w) const { + return c[0]*x*x + c[1]*x*y + c[2]*y*y + c[3]*x*w + c[4]*y*w + c[5]*w*w; + } + + xAx scale(double sx, double sy) const; + + Point gradient(Point p) const; + + xAx operator-(xAx const &b) const; + xAx operator+(xAx const &b) const; + xAx operator+(double const &b) const; + xAx operator*(double const &b) const; + + std::vector crossings(Rect r) const; + boost::optional toCurve(Rect const & bnd) const; + std::vector roots(Point d, Point o) const; + + std::vector roots(Line const &l) const; + + static Interval quad_ex(double a, double b, double c, Interval ivl); + + Geom::Affine hessian() const; + + boost::optional bottom() const; + + Interval extrema(Rect r) const; + + + /* + * Return the symmetric matrix related to the conic section. + * Modifying the matrix does not modify the conic section + */ + NL::SymmetricMatrix<3> get_matrix() const + { + NL::SymmetricMatrix<3> C(c); + C(1,0) *= 0.5; C(2,0) *= 0.5; C(2,1) *= 0.5; + return C; + } + + /* + * Return the i-th coefficient of the conic section algebraic equation + * Modifying the returned value does not modify the conic section coefficient + */ + double coeff (size_t i) const + { + return c[i]; + } + + /* + * Return the i-th coefficient of the conic section algebraic equation + * Modifying the returned value modifies the conic section coefficient + */ + double& coeff (size_t i) + { + return c[i]; + } + + kind_t kind () const; + + std::string categorise() const; + + /* + * Return true if the equation: + * c0*x^2 + c1*xy + c2*y^2 + c3*x + c4*y +c5 == 0 + * really defines a conic, false otherwise + */ + bool is_quadratic() const + { + return (coeff(0) != 0 || coeff(1) != 0 || coeff(2) != 0); + } + + /* + * Return true if the conic is degenerate, i.e. if the related matrix + * determinant is null, false otherwise + */ + bool isDegenerate() const + { + return (det_sgn (get_matrix()) == 0); + } + + /* + * Compute the centre of symmetry of the conic section when it exists, + * else it return an uninitialized boost::optional instance. + */ + boost::optional centre() const + { + typedef boost::optional opt_point_t; + + double d = coeff(1) * coeff(1) - 4 * coeff(0) * coeff(2); + if (are_near (d, 0)) return opt_point_t(); + NL::Matrix Q(2, 2); + Q(0,0) = coeff(0); + Q(1,1) = coeff(2); + Q(0,1) = Q(1,0) = coeff(1) * 0.5; + NL::Vector T(2); + T[0] = - coeff(3) * 0.5; + T[1] = - coeff(4) * 0.5; + + NL::LinearSystem ls (Q, T); + NL::Vector sol = ls.SV_solve(); + Point C; + C[0] = sol[0]; + C[1] = sol[1]; + + return opt_point_t(C); + } + + double axis_angle() const; + + void roots (std::vector& sol, Coord v, Dim2 d) const; + + xAx translate (const Point & _offset) const; + + xAx rotate (double angle) const; + + /* + * Rotate the conic section by the given angle wrt the provided point. + * + * _rot_centre: the rotation centre + * _angle: the rotation angle + */ + xAx rotate (const Point & _rot_centre, double _angle) const + { + xAx result + = translate (-_rot_centre).rotate (_angle).translate (_rot_centre); + return result; + } + + /* + * Compute the tangent line of the conic section at the provided point + * + * _point: the conic section point the tangent line pass through + */ + Line tangent (const Point & _point) const + { + NL::Vector pp(3); + pp[0] = _point[0]; pp[1] = _point[1]; pp[2] = 1; + NL::SymmetricMatrix<3> C = get_matrix(); + NL::Vector line = C * pp; + return Line(line[0], line[1], line[2]); + } + + /* + * For a non degenerate conic compute the dual conic. + * TODO: investigate degenerate case + */ + xAx dual () const + { + //assert (! isDegenerate()); + NL::SymmetricMatrix<3> C = get_matrix(); + NL::SymmetricMatrix<3> D = adj(C); + xAx dc(D); + return dc; + } + + bool decompose (Line& l1, Line& l2) const; + + /* + * Generate a RatQuad object from a conic arc. + * + * p0: the initial point of the arc + * p1: the inner point of the arc + * p2: the final point of the arc + */ + RatQuad toRatQuad (const Point & p0, + const Point & p1, + const Point & p2) const + { + Point dp0 = gradient (p0); + Point dp2 = gradient (p2); + return + RatQuad::fromPointsTangents (p0, rot90 (dp0), p1, p2, rot90 (dp2)); + } + + /* + * Return the angle related to the normal gradient computed at the passed + * point. + * + * _point: the point at which computes the angle + * + * prerequisite: the passed point must lie on the conic + */ + double angle_at (const Point & _point) const + { + double angle = atan2 (gradient (_point)); + if (angle < 0) angle += (2*M_PI); + return angle; + } + + /* + * Return true if the given point is contained in the conic arc determined + * by the passed points. + * + * _point: the point to be tested + * _initial: the initial point of the arc + * _inner: an inner point of the arc + * _final: the final point of the arc + * + * prerequisite: the passed points must lie on the conic, the inner point + * has to be strictly contained in the arc, except when the + * initial and final points are equal: in such a case if the + * inner point is also equal to them, then they define an arc + * made up by a single point. + * + */ + bool arc_contains (const Point & _point, const Point & _initial, + const Point & _inner, const Point & _final) const + { + AngleInterval ai(angle_at(_initial), angle_at(_inner), angle_at(_final)); + return ai.contains(angle_at(_point)); + } + + Rect arc_bound (const Point & P1, const Point & Q, const Point & P2) const; + + std::vector allNearestTimes (const Point &P) const; + + /* + * Return the point on the conic section nearest to the passed point "P". + * + * P: the point to compute the nearest one + */ + Point nearestTime (const Point &P) const + { + std::vector points = allNearestTimes (P); + if ( !points.empty() ) + { + return points.front(); + } + // else + THROW_LOGICALERROR ("nearestTime: no nearest point found"); + return Point(); + } + +}; + +std::vector intersect(const xAx & C1, const xAx & C2); + +bool clip (std::vector & rq, const xAx & cs, const Rect & R); + +inline std::ostream &operator<< (std::ostream &out_file, const xAx &x) { + for(int i = 0; i < 6; i++) { + out_file << x.c[i] << ", "; + } + return out_file; +} + +}; + + +#endif // LIB2GEOM_SEEN_CONICSEC_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/2geom/convex-hull.cpp b/src/2geom/convex-hull.cpp new file mode 100644 index 0000000..4f5e067 --- /dev/null +++ b/src/2geom/convex-hull.cpp @@ -0,0 +1,746 @@ +/** @file + * @brief Convex hull of a set of points + *//* + * Authors: + * Nathan Hurst + * Michael G. Sloan + * Krzysztof KosiÅ„ski + * Copyright 2006-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/convex-hull.h> +#include <2geom/exception.h> +#include +#include +#include +#include +#include + +/** Todo: + + modify graham scan to work top to bottom, rather than around angles + + intersection + + minimum distance between convex hulls + + maximum distance between convex hulls + + hausdorf metric? + + check all degenerate cases carefully + + check all algorithms meet all invariants + + generalise rotating caliper algorithm (iterator/circulator?) +*/ + +using std::vector; +using std::map; +using std::pair; +using std::make_pair; +using std::swap; + +namespace Geom { + +ConvexHull::ConvexHull(Point const &a, Point const &b) + : _boundary(2) + , _lower(0) +{ + _boundary[0] = a; + _boundary[1] = b; + std::sort(_boundary.begin(), _boundary.end(), Point::LexLess()); + _construct(); +} + +ConvexHull::ConvexHull(Point const &a, Point const &b, Point const &c) + : _boundary(3) + , _lower(0) +{ + _boundary[0] = a; + _boundary[1] = b; + _boundary[2] = c; + std::sort(_boundary.begin(), _boundary.end(), Point::LexLess()); + _construct(); +} + +ConvexHull::ConvexHull(Point const &a, Point const &b, Point const &c, Point const &d) + : _boundary(4) + , _lower(0) +{ + _boundary[0] = a; + _boundary[1] = b; + _boundary[2] = c; + _boundary[3] = d; + std::sort(_boundary.begin(), _boundary.end(), Point::LexLess()); + _construct(); +} + +ConvexHull::ConvexHull(std::vector const &pts) + : _lower(0) +{ + //if (pts.size() > 16) { // arbitrary threshold + // _prune(pts.begin(), pts.end(), _boundary); + //} else { + _boundary = pts; + std::sort(_boundary.begin(), _boundary.end(), Point::LexLess()); + //} + _construct(); +} + +bool ConvexHull::_is_clockwise_turn(Point const &a, Point const &b, Point const &c) +{ + if (b == c) return false; + return cross(b-a, c-a) > 0; +} + +void ConvexHull::_construct() +{ + // _boundary must already be sorted in LexLess order + if (_boundary.empty()) { + _lower = 0; + return; + } + if (_boundary.size() == 1 || (_boundary.size() == 2 && _boundary[0] == _boundary[1])) { + _boundary.resize(1); + _lower = 1; + return; + } + if (_boundary.size() == 2) { + _lower = 2; + return; + } + + std::size_t k = 2; + for (std::size_t i = 2; i < _boundary.size(); ++i) { + while (k >= 2 && !_is_clockwise_turn(_boundary[k-2], _boundary[k-1], _boundary[i])) { + --k; + } + std::swap(_boundary[k++], _boundary[i]); + } + + _lower = k; + std::sort(_boundary.begin() + k, _boundary.end(), Point::LexGreater()); + _boundary.push_back(_boundary.front()); + for (std::size_t i = _lower; i < _boundary.size(); ++i) { + while (k > _lower && !_is_clockwise_turn(_boundary[k-2], _boundary[k-1], _boundary[i])) { + --k; + } + std::swap(_boundary[k++], _boundary[i]); + } + + _boundary.resize(k-1); +} + +double ConvexHull::area() const +{ + if (size() <= 2) return 0; + + double a = 0; + for (std::size_t i = 0; i < size()-1; ++i) { + a += cross(_boundary[i], _boundary[i+1]); + } + a += cross(_boundary.back(), _boundary.front()); + return fabs(a * 0.5); +} + +OptRect ConvexHull::bounds() const +{ + OptRect ret; + if (empty()) return ret; + ret = Rect(left(), top(), right(), bottom()); + return ret; +} + +Point ConvexHull::topPoint() const +{ + Point ret; + ret[Y] = std::numeric_limits::infinity(); + + for (UpperIterator i = upperHull().begin(); i != upperHull().end(); ++i) { + if (ret[Y] >= i->y()) { + ret = *i; + } else { + break; + } + } + + return ret; +} + +Point ConvexHull::bottomPoint() const +{ + Point ret; + ret[Y] = -std::numeric_limits::infinity(); + + for (LowerIterator j = lowerHull().begin(); j != lowerHull().end(); ++j) { + if (ret[Y] <= j->y()) { + ret = *j; + } else { + break; + } + } + + return ret; +} + +template +bool below_x_monotonic_polyline(Point const &p, Iter first, Iter last, Lex lex) +{ + typename Lex::Secondary above; + Iter f = std::lower_bound(first, last, p, lex); + if (f == last) return false; + if (f == first) { + if (p == *f) return true; + return false; + } + + Point a = *(f-1), b = *f; + if (a[X] == b[X]) { + if (above(p[Y], a[Y]) || above(b[Y], p[Y])) return false; + } else { + // TODO: maybe there is a more numerically stable method + Coord y = lerp((p[X] - a[X]) / (b[X] - a[X]), a[Y], b[Y]); + if (above(p[Y], y)) return false; + } + return true; +} + +bool ConvexHull::contains(Point const &p) const +{ + if (_boundary.empty()) return false; + if (_boundary.size() == 1) { + if (_boundary[0] == p) return true; + return false; + } + + // 1. verify that the point is in the relevant X range + if (p[X] < _boundary[0][X] || p[X] > _boundary[_lower-1][X]) return false; + + // 2. check whether it is below the upper hull + UpperIterator ub = upperHull().begin(), ue = upperHull().end(); + if (!below_x_monotonic_polyline(p, ub, ue, Point::LexLess())) return false; + + // 3. check whether it is above the lower hull + LowerIterator lb = lowerHull().begin(), le = lowerHull().end(); + if (!below_x_monotonic_polyline(p, lb, le, Point::LexGreater())) return false; + + return true; +} + +bool ConvexHull::contains(Rect const &r) const +{ + for (unsigned i = 0; i < 4; ++i) { + if (!contains(r.corner(i))) return false; + } + return true; +} + +bool ConvexHull::contains(ConvexHull const &ch) const +{ + // TODO: requires interiorContains. + // We have to check all points of ch, and each point takes logarithmic time. + // If there are more points in ch that here, it is faster to make the check + // the other way around. + /*if (ch.size() > size()) { + for (iterator i = begin(); i != end(); ++i) { + if (ch.interiorContains(*i)) return false; + } + return true; + }*/ + + for (iterator i = ch.begin(); i != ch.end(); ++i) { + if (!contains(*i)) return false; + } + return true; +} + +void ConvexHull::swap(ConvexHull &other) +{ + _boundary.swap(other._boundary); + std::swap(_lower, other._lower); +} + +void ConvexHull::swap(std::vector &pts) +{ + _boundary.swap(pts); + _lower = 0; + std::sort(_boundary.begin(), _boundary.end(), Point::LexLess()); + _construct(); +} + +#if 0 +/*** SignedTriangleArea + * returns the area of the triangle defined by p0, p1, p2. A clockwise triangle has positive area. + */ +double +SignedTriangleArea(Point p0, Point p1, Point p2) { + return cross((p1 - p0), (p2 - p0)); +} + +class angle_cmp{ +public: + Point o; + angle_cmp(Point o) : o(o) {} + +#if 0 + bool + operator()(Point a, Point b) { + // not remove this check or std::sort could crash + if (a == b) return false; + Point da = a - o; + Point db = b - o; + if (da == -db) return false; + +#if 1 + double aa = da[0]; + double ab = db[0]; + if((da[1] == 0) && (db[1] == 0)) + return da[0] < db[0]; + if(da[1] == 0) + return true; // infinite tangent + if(db[1] == 0) + return false; // infinite tangent + aa = da[0] / da[1]; + ab = db[0] / db[1]; + if(aa > ab) + return true; +#else + //assert((ata > atb) == (aa < ab)); + double aa = atan2(da); + double ab = atan2(db); + if(aa < ab) + return true; +#endif + if(aa == ab) + return L2sq(da) < L2sq(db); + return false; + } +#else + bool operator() (Point const& a, Point const& b) + { + // not remove this check or std::sort could generate + // a segmentation fault because it needs a strict '<' + // but due to round errors a == b doesn't mean dxy == dyx + if (a == b) return false; + Point da = a - o; + Point db = b - o; + if (da == -db) return false; + double dxy = da[X] * db[Y]; + double dyx = da[Y] * db[X]; + if (dxy > dyx) return true; + else if (dxy < dyx) return false; + return L2sq(da) < L2sq(db); + } +#endif +}; + +//Mathematically incorrect mod, but more useful. +int mod(int i, int l) { + return i >= 0 ? + i % l : (i % l) + l; +} +//OPT: usages can often be replaced by conditions + +/*** ConvexHull::add_point + * to add a point we need to find whether the new point extends the boundary, and if so, what it + * obscures. Tarjan? Jarvis?*/ +void +ConvexHull::merge(Point p) { + std::vector out; + + int len = boundary.size(); + + if(len < 2) { + if(boundary.empty() || boundary[0] != p) + boundary.push_back(p); + return; + } + + bool pushed = false; + + bool pre = is_left(p, -1); + for(int i = 0; i < len; i++) { + bool cur = is_left(p, i); + if(pre) { + if(cur) { + if(!pushed) { + out.push_back(p); + pushed = true; + } + continue; + } + else if(!pushed) { + out.push_back(p); + pushed = true; + } + } + out.push_back(boundary[i]); + pre = cur; + } + + boundary = out; +} +//OPT: quickly find an obscured point and find the bounds by extending from there. then push all points not within the bounds in order. + //OPT: use binary searches to find the actual starts/ends, use known rights as boundaries. may require cooperation of find_left algo. + +/*** ConvexHull::is_clockwise + * We require that successive pairs of edges always turn right. + * We return false on collinear points + * proposed algorithm: walk successive edges and require triangle area is positive. + */ +bool +ConvexHull::is_clockwise() const { + if(is_degenerate()) + return true; + Point first = boundary[0]; + Point second = boundary[1]; + for(std::vector::const_iterator it(boundary.begin()+2), e(boundary.end()); + it != e;) { + if(SignedTriangleArea(first, second, *it) > 0) + return false; + first = second; + second = *it; + ++it; + } + return true; +} + +/*** ConvexHull::top_point_first + * We require that the first point in the convex hull has the least y coord, and that off all such points on the hull, it has the least x coord. + * proposed algorithm: track lexicographic minimum while walking the list. + */ +bool +ConvexHull::top_point_first() const { + if(size() <= 1) return true; + std::vector::const_iterator pivot = boundary.begin(); + for(std::vector::const_iterator it(boundary.begin()+1), + e(boundary.end()); + it != e; it++) { + if((*it)[1] < (*pivot)[1]) + pivot = it; + else if(((*it)[1] == (*pivot)[1]) && + ((*it)[0] < (*pivot)[0])) + pivot = it; + } + return pivot == boundary.begin(); +} +//OPT: since the Y values are orderly there should be something like a binary search to do this. + +bool +ConvexHull::meets_invariants() const { + return is_clockwise() && top_point_first(); +} + +/*** ConvexHull::is_degenerate + * We allow three degenerate cases: empty, 1 point and 2 points. In many cases these should be handled explicitly. + */ +bool +ConvexHull::is_degenerate() const { + return boundary.size() < 3; +} + + +int sgn(double x) { + if(x == 0) return 0; + return (x<0)?-1:1; +} + +bool same_side(Point L[2], Point xs[4]) { + int side = 0; + for(int i = 0; i < 4; i++) { + int sn = sgn(SignedTriangleArea(L[0], L[1], xs[i])); + if(sn && !side) + side = sn; + else if(sn != side) return false; + } + return true; +} + +/** find bridging pairs between two convex hulls. + * this code is based on Hormoz Pirzadeh's masters thesis. There is room for optimisation: + * 1. reduce recomputation + * 2. use more efficient angle code + * 3. write as iterator + */ +std::vector > bridges(ConvexHull a, ConvexHull b) { + vector > ret; + + // 1. find maximal points on a and b + int ai = 0, bi = 0; + // 2. find first copodal pair + double ap_angle = atan2(a[ai+1] - a[ai]); + double bp_angle = atan2(b[bi+1] - b[bi]); + Point L[2] = {a[ai], b[bi]}; + while(ai < int(a.size()) || bi < int(b.size())) { + if(ap_angle == bp_angle) { + // In the case of parallel support lines, we must consider all four pairs of copodal points + { + assert(0); // untested + Point xs[4] = {a[ai-1], a[ai+1], b[bi-1], b[bi+1]}; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + xs[2] = b[bi]; + xs[3] = b[bi+2]; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + xs[0] = a[ai]; + xs[1] = a[ai+2]; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + xs[2] = b[bi-1]; + xs[3] = b[bi+1]; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + } + ai++; + ap_angle += angle_between(a[ai] - a[ai-1], a[ai+1] - a[ai]); + L[0] = a[ai]; + bi++; + bp_angle += angle_between(b[bi] - b[bi-1], b[bi+1] - b[bi]); + L[1] = b[bi]; + std::cout << "parallel\n"; + } else if(ap_angle < bp_angle) { + ai++; + ap_angle += angle_between(a[ai] - a[ai-1], a[ai+1] - a[ai]); + L[0] = a[ai]; + Point xs[4] = {a[ai-1], a[ai+1], b[bi-1], b[bi+1]}; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + } else { + bi++; + bp_angle += angle_between(b[bi] - b[bi-1], b[bi+1] - b[bi]); + L[1] = b[bi]; + Point xs[4] = {a[ai-1], a[ai+1], b[bi-1], b[bi+1]}; + if(same_side(L, xs)) ret.push_back(make_pair(ai, bi)); + } + } + return ret; +} + +unsigned find_bottom_right(ConvexHull const &a) { + unsigned it = 1; + while(it < a.boundary.size() && + a.boundary[it][Y] > a.boundary[it-1][Y]) + it++; + return it-1; +} + +/*** ConvexHull sweepline_intersection(ConvexHull a, ConvexHull b); + * find the intersection between two convex hulls. The intersection is also a convex hull. + * (Proof: take any two points both in a and in b. Any point between them is in a by convexity, + * and in b by convexity, thus in both. Need to prove still finite bounds.) + * This algorithm works by sweeping a line down both convex hulls in parallel, working out the left and right edges of the new hull. + */ +ConvexHull sweepline_intersection(ConvexHull const &a, ConvexHull const &b) { + ConvexHull ret; + + unsigned al = 0; + unsigned bl = 0; + + while(al+1 < a.boundary.size() && + (a.boundary[al+1][Y] > b.boundary[bl][Y])) { + al++; + } + while(bl+1 < b.boundary.size() && + (b.boundary[bl+1][Y] > a.boundary[al][Y])) { + bl++; + } + // al and bl now point to the top of the first pair of edges that overlap in y value + //double sweep_y = std::min(a.boundary[al][Y], + // b.boundary[bl][Y]); + return ret; +} + +/*** ConvexHull intersection(ConvexHull a, ConvexHull b); + * find the intersection between two convex hulls. The intersection is also a convex hull. + * (Proof: take any two points both in a and in b. Any point between them is in a by convexity, + * and in b by convexity, thus in both. Need to prove still finite bounds.) + */ +ConvexHull intersection(ConvexHull /*a*/, ConvexHull /*b*/) { + ConvexHull ret; + /* + int ai = 0, bi = 0; + int aj = a.boundary.size() - 1; + int bj = b.boundary.size() - 1; + */ + /*while (true) { + if(a[ai] + }*/ + return ret; +} + +template +T idx_to_pair(pair p, int idx) { + return idx?p.second:p.first; +} + +/*** ConvexHull merge(ConvexHull a, ConvexHull b); + * find the smallest convex hull that surrounds a and b. + */ +ConvexHull merge(ConvexHull a, ConvexHull b) { + ConvexHull ret; + + std::cout << "---\n"; + std::vector > bpair = bridges(a, b); + + // Given our list of bridges {(pb1, qb1), ..., (pbk, qbk)} + // we start with the highest point in p0, q0, say it is p0. + // then the merged hull is p0, ..., pb1, qb1, ..., qb2, pb2, ... + // In other words, either of the two polygons vertices are added in order until the vertex coincides with a bridge point, at which point we swap. + + unsigned state = (a[0][Y] < b[0][Y])?0:1; + ret.boundary.reserve(a.size() + b.size()); + ConvexHull chs[2] = {a, b}; + unsigned idx = 0; + + for(unsigned k = 0; k < bpair.size(); k++) { + unsigned limit = idx_to_pair(bpair[k], state); + std::cout << bpair[k].first << " , " << bpair[k].second << "; " + << idx << ", " << limit << ", s: " + << state + << " \n"; + while(idx <= limit) { + ret.boundary.push_back(chs[state][idx++]); + } + state = 1-state; + idx = idx_to_pair(bpair[k], state); + } + while(idx < chs[state].size()) { + ret.boundary.push_back(chs[state][idx++]); + } + return ret; +} + +ConvexHull graham_merge(ConvexHull a, ConvexHull b) { + ConvexHull result; + + // we can avoid the find pivot step because of top_point_first + if(b.boundary[0] <= a.boundary[0]) + swap(a, b); + + result.boundary = a.boundary; + result.boundary.insert(result.boundary.end(), + b.boundary.begin(), b.boundary.end()); + +/** if we modified graham scan to work top to bottom as proposed in lect754.pdf we could replace the + angle sort with a simple merge sort type algorithm. furthermore, we could do the graham scan + online, avoiding a bunch of memory copies. That would probably be linear. -- njh*/ + result.angle_sort(); + result.graham_scan(); + + return result; +} + +ConvexHull andrew_merge(ConvexHull a, ConvexHull b) { + ConvexHull result; + + // we can avoid the find pivot step because of top_point_first + if(b.boundary[0] <= a.boundary[0]) + swap(a, b); + + result.boundary = a.boundary; + result.boundary.insert(result.boundary.end(), + b.boundary.begin(), b.boundary.end()); + +/** if we modified graham scan to work top to bottom as proposed in lect754.pdf we could replace the + angle sort with a simple merge sort type algorithm. furthermore, we could do the graham scan + online, avoiding a bunch of memory copies. That would probably be linear. -- njh*/ + result.andrew_scan(); + + return result; +} + +//TODO: reinstate +/*ConvexCover::ConvexCover(Path const &sp) : path(&sp) { + cc.reserve(sp.size()); + for(Geom::Path::const_iterator it(sp.begin()), end(sp.end()); it != end; ++it) { + cc.push_back(ConvexHull((*it).begin(), (*it).end())); + } +}*/ + +double ConvexHull::centroid_and_area(Geom::Point& centroid) const { + const unsigned n = boundary.size(); + if (n < 2) + return 0; + if(n < 3) { + centroid = (boundary[0] + boundary[1])/2; + return 0; + } + Geom::Point centroid_tmp(0,0); + double atmp = 0; + for (unsigned i = n-1, j = 0; j < n; i = j, j++) { + const double ai = cross(boundary[j], boundary[i]); + atmp += ai; + centroid_tmp += (boundary[j] + boundary[i])*ai; // first moment. + } + if (atmp != 0) { + centroid = centroid_tmp / (3 * atmp); + } + return atmp / 2; +} + +// TODO: This can be made lg(n) using golden section/fibonacci search three starting points, say 0, +// n/2, n-1 construct a new point, say (n/2 + n)/2 throw away the furthest boundary point iterate +// until interval is a single value +Point const * ConvexHull::furthest(Point direction) const { + Point const * p = &boundary[0]; + double d = dot(*p, direction); + for(unsigned i = 1; i < boundary.size(); i++) { + double dd = dot(boundary[i], direction); + if(d < dd) { + p = &boundary[i]; + d = dd; + } + } + return p; +} + + +// returns (a, (b,c)), three points which define the narrowest diameter of the hull as the pair of +// lines going through b,c, and through a, parallel to b,c TODO: This can be made linear time by +// moving point tc incrementally from the previous value (it can only move in one direction). It +// is currently n*O(furthest) +double ConvexHull::narrowest_diameter(Point &a, Point &b, Point &c) { + Point tb = boundary.back(); + double d = std::numeric_limits::max(); + for(unsigned i = 0; i < boundary.size(); i++) { + Point tc = boundary[i]; + Point n = -rot90(tb-tc); + Point ta = *furthest(n); + double td = dot(n, ta-tb)/dot(n,n); + if(td < d) { + a = ta; + b = tb; + c = tc; + d = td; + } + tb = tc; + } + return d; +} +#endif + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/convex-hull.h b/src/2geom/convex-hull.h new file mode 100644 index 0000000..b4f0788 --- /dev/null +++ b/src/2geom/convex-hull.h @@ -0,0 +1,346 @@ +/** @file + * @brief Convex hull data structures + *//* + * Copyright 2006 Nathan Hurst + * Copyright 2006 Michael G. Sloan + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_CONVEX_HULL_H +#define LIB2GEOM_SEEN_CONVEX_HULL_H + +#include <2geom/point.h> +#include <2geom/rect.h> +#include +#include +#include +#include +#include + +namespace Geom { + +namespace { + +/** @brief Iterator for the lower convex hull. + * This iterator allows us to avoid duplicating any points in the hull + * boundary and still express most algorithms in a concise way. */ +class ConvexHullLowerIterator + : public boost::random_access_iterator_helper + < ConvexHullLowerIterator + , Point + , std::ptrdiff_t + , Point const * + , Point const & + > +{ +public: + typedef ConvexHullLowerIterator Self; + ConvexHullLowerIterator() + : _data(NULL) + , _size(0) + , _x(0) + {} + ConvexHullLowerIterator(std::vector const &pts, std::size_t x) + : _data(&pts[0]) + , _size(pts.size()) + , _x(x) + {} + + Self &operator++() { + *this += 1; + return *this; + } + Self &operator--() { + *this -= 1; + return *this; + } + Self &operator+=(std::ptrdiff_t d) { + _x += d; + return *this; + } + Self &operator-=(std::ptrdiff_t d) { + _x -= d; + return *this; + } + std::ptrdiff_t operator-(Self const &other) const { + return _x - other._x; + } + Point const &operator*() const { + if (_x < _size) { + return _data[_x]; + } else { + return *_data; + } + } + bool operator==(Self const &other) const { + return _data == other._data && _x == other._x; + } + bool operator<(Self const &other) const { + return _data == other._data && _x < other._x; + } + +private: + Point const *_data; + std::size_t _size; + std::size_t _x; +}; + +} // end anonymous namespace + +/** + * @brief Convex hull based on the Andrew's monotone chain algorithm. + * @ingroup Shapes + */ +class ConvexHull { +public: + typedef std::vector::const_iterator iterator; + typedef std::vector::const_iterator const_iterator; + typedef std::vector::const_iterator UpperIterator; + typedef ConvexHullLowerIterator LowerIterator; + + /// @name Construct a convex hull. + /// @{ + + /// Create an empty convex hull. + ConvexHull() {} + /// Construct a singular convex hull. + explicit ConvexHull(Point const &a) + : _boundary(1, a) + , _lower(1) + {} + /// Construct a convex hull of two points. + ConvexHull(Point const &a, Point const &b); + /// Construct a convex hull of three points. + ConvexHull(Point const &a, Point const &b, Point const &c); + /// Construct a convex hull of four points. + ConvexHull(Point const &a, Point const &b, Point const &c, Point const &d); + /// Create a convex hull of a vector of points. + ConvexHull(std::vector const &pts); + + /// Create a convex hull of a range of points. + template + ConvexHull(Iter first, Iter last) + : _lower(0) + { + _prune(first, last, _boundary); + _construct(); + } + /// @} + + /// @name Inspect basic properties. + /// @{ + + /// Check for emptiness. + bool empty() const { return _boundary.empty(); } + /// Get the number of points in the hull. + size_t size() const { return _boundary.size(); } + /// Check whether the hull contains only one point. + bool isSingular() const { return _boundary.size() == 1; } + /// Check whether the hull is a line. + bool isLinear() const { return _boundary.size() == 2; } + /// Check whether the hull has zero area. + bool isDegenerate() const { return _boundary.size() < 3; } + /// Calculate the area of the convex hull. + double area() const; + //Point centroid() const; + //double areaAndCentroid(Point &c); + //FatLine maxDiameter() const; + //FatLine minDiameter() const; + /// @} + + /// @name Inspect bounds and extreme points. + /// @{ + + /// Compute the bounding rectangle of the convex hull. + OptRect bounds() const; + + /// Get the leftmost (minimum X) coordinate of the hull. + Coord left() const { return _boundary[0][X]; } + /// Get the rightmost (maximum X) coordinate of the hull. + Coord right() const { return _boundary[_lower-1][X]; } + /// Get the topmost (minimum Y) coordinate of the hull. + Coord top() const { return topPoint()[Y]; } + /// Get the bottommost (maximum Y) coordinate of the hull. + Coord bottom() const { return bottomPoint()[Y]; } + + /// Get the leftmost (minimum X) point of the hull. + /// If the leftmost edge is vertical, the top point of the edge is returned. + Point leftPoint() const { return _boundary[0]; } + /// Get the rightmost (maximum X) point of the hull. + /// If the rightmost edge is vertical, the bottom point edge is returned. + Point rightPoint() const { return _boundary[_lower-1]; } + /// Get the topmost (minimum Y) point of the hull. + /// If the topmost edge is horizontal, the right point of the edge is returned. + Point topPoint() const; + /// Get the bottommost (maximum Y) point of the hull. + /// If the bottommost edge is horizontal, the left point of the edge is returned. + Point bottomPoint() const; + ///@} + + /// @name Iterate over points. + /// @{ + /** @brief Get the begin iterator to the points that form the hull. + * Points are returned beginning with the leftmost one, going along + * the upper (minimum Y) side, and then along the bottom. + * Thus the points are always ordered clockwise. No point is + * repeated. */ + iterator begin() const { return _boundary.begin(); } + /// Get the end iterator to the points that form the hull. + iterator end() const { return _boundary.end(); } + /// Get the first, leftmost point in the hull. + Point const &front() const { return _boundary.front(); } + /// Get the penultimate point of the lower hull. + Point const &back() const { return _boundary.back(); } + Point const &operator[](std::size_t i) const { + return _boundary[i]; + } + + /** @brief Get an iterator range to the upper part of the hull. + * This returns a range that includes the leftmost point, + * all points of the upper hull, and the rightmost point. */ + boost::iterator_range upperHull() const { + boost::iterator_range r(_boundary.begin(), _boundary.begin() + _lower); + return r; + } + + /** @brief Get an iterator range to the lower part of the hull. + * This returns a range that includes the leftmost point, + * all points of the lower hull, and the rightmost point. */ + boost::iterator_range lowerHull() const { + if (_boundary.empty()) { + boost::iterator_range r(LowerIterator(_boundary, 0), + LowerIterator(_boundary, 0)); + return r; + } + if (_boundary.size() == 1) { + boost::iterator_range r(LowerIterator(_boundary, 0), + LowerIterator(_boundary, 1)); + return r; + } + boost::iterator_range r(LowerIterator(_boundary, _lower - 1), + LowerIterator(_boundary, _boundary.size() + 1)); + return r; + } + /// @} + + /// @name Check for containment and intersection. + /// @{ + /** @brief Check whether the given point is inside the hull. + * This takes logarithmic time. */ + bool contains(Point const &p) const; + /** @brief Check whether the given axis-aligned rectangle is inside the hull. + * A rectangle is inside the hull if all of its corners are inside. */ + bool contains(Rect const &r) const; + /// Check whether the given convex hull is completely contained in this one. + bool contains(ConvexHull const &other) const; + //bool interiorContains(Point const &p) const; + //bool interiorContains(Rect const &r) const; + //bool interiorContains(ConvexHull const &other) const; + //bool intersects(Rect const &r) const; + //bool intersects(ConvexHull const &other) const; + + //ConvexHull &operator|=(ConvexHull const &other); + //ConvexHull &operator&=(ConvexHull const &other); + //ConvexHull &operator*=(Affine const &m); + + //ConvexHull &expand(Point const &p); + //void unifyWith(ConvexHull const &other); + //void intersectWith(ConvexHull const &other); + /// @} + + void swap(ConvexHull &other); + void swap(std::vector &pts); + +private: + void _construct(); + static bool _is_clockwise_turn(Point const &a, Point const &b, Point const &c); + + /// Take a vector of points and produce a pruned sorted vector. + template + static void _prune(Iter first, Iter last, std::vector &out) { + boost::optional ymin, ymax, xmin, xmax; + for (Iter i = first; i != last; ++i) { + Point p = *i; + if (!ymin || Point::LexLess()(p, *ymin)) { + ymin = p; + } + if (!xmin || Point::LexLess()(p, *xmin)) { + xmin = p; + } + if (!ymax || Point::LexGreater()(p, *ymax)) { + ymax = p; + } + if (!xmax || Point::LexGreater()(p, *xmax)) { + xmax = p; + } + } + if (!ymin) return; + + ConvexHull qhull(*xmin, *xmax, *ymin, *ymax); + for (Iter i = first; i != last; ++i) { + if (qhull.contains(*i)) continue; + out.push_back(*i); + } + + out.push_back(*xmin); + out.push_back(*xmax); + out.push_back(*ymin); + out.push_back(*ymax); + std::sort(out.begin(), out.end(), Point::LexLess()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + } + + /// Sequence of points forming the convex hull polygon. + std::vector _boundary; + /// Index one past the rightmost point, where the lower part of the boundary starts. + std::size_t _lower; +}; + +/** @brief Output operator for convex hulls. + * Prints out all the coordinates. */ +inline std::ostream &operator<< (std::ostream &out_file, const Geom::ConvexHull &in_cvx) { + out_file << "ConvexHull("; + for(unsigned i = 0; i < in_cvx.size(); i++) { + out_file << in_cvx[i] << ", "; + } + out_file << ")"; + return out_file; +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_CONVEX_HULL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/coord.cpp b/src/2geom/coord.cpp new file mode 100644 index 0000000..29208d3 --- /dev/null +++ b/src/2geom/coord.cpp @@ -0,0 +1,123 @@ +/** @file + * @brief Conversion between Coord and strings + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +// Most of the code in this file is derived from: +// https://code.google.com/p/double-conversion/ +// The copyright notice for that code is attached below. +// +// Copyright 2010 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include <2geom/coord.h> +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Geom { + +std::string format_coord_shortest(Coord x) +{ + static double_conversion::DoubleToStringConverter conv( + double_conversion::DoubleToStringConverter::UNIQUE_ZERO, + "inf", "NaN", 'e', -3, 6, 0, 0); + std::string ret(' ', 32); + double_conversion::StringBuilder builder(&ret[0], 32); + conv.ToShortest(x, &builder); + ret.resize(builder.position()); + return ret; +} + +std::string format_coord_nice(Coord x) +{ + static double_conversion::DoubleToStringConverter conv( + double_conversion::DoubleToStringConverter::UNIQUE_ZERO, + "inf", "NaN", 'e', -6, 21, 0, 0); + std::string ret(' ', 32); + double_conversion::StringBuilder builder(&ret[0], 32); + conv.ToShortest(x, &builder); + ret.resize(builder.position()); + return ret; +} + +Coord parse_coord(std::string const &s) +{ + static double_conversion::StringToDoubleConverter conv( + double_conversion::StringToDoubleConverter::ALLOW_LEADING_SPACES | + double_conversion::StringToDoubleConverter::ALLOW_TRAILING_SPACES | + double_conversion::StringToDoubleConverter::ALLOW_SPACES_AFTER_SIGN, + 0.0, nan(""), "inf", "NaN"); + int dummy; + return conv.StringToDouble(s.c_str(), s.length(), &dummy); +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/coord.h b/src/2geom/coord.h new file mode 100644 index 0000000..9cc220d --- /dev/null +++ b/src/2geom/coord.h @@ -0,0 +1,214 @@ +/** @file + * @brief Integral and real coordinate types and some basic utilities + *//* + * Authors: + * Nathan Hurst + * Krzysztof KosiÅ„ski + * Copyright 2006-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_COORD_H +#define LIB2GEOM_SEEN_COORD_H + +#include +#include +#include +#include +#include +#include <2geom/forward.h> + +namespace Geom { + +/** @brief 2D axis enumeration (X or Y). + * @ingroup Primitives */ +enum Dim2 { X=0, Y=1 }; + +/** @brief Get the other (perpendicular) dimension. + * @ingroup Primitives */ +inline Dim2 other_dimension(Dim2 d) { return d == Y ? X : Y; } + +// TODO: make a smarter implementation with C++11 +template +struct D2Traits { + typedef typename T::D1Value D1Value; + typedef typename T::D1Reference D1Reference; + typedef typename T::D1ConstReference D1ConstReference; +}; + +/** @brief Axis extraction functor. + * For use with things such as Boost's transform_iterator. + * @ingroup Utilities */ +template +struct GetAxis { + typedef typename D2Traits::D1Value result_type; + typedef T argument_type; + typename D2Traits::D1Value operator()(T const &a) const { + return a[D]; + } +}; + +/** @brief Floating point type used to store coordinates. + * @ingroup Primitives */ +typedef double Coord; + +/** @brief Type used for integral coordinates. + * @ingroup Primitives */ +typedef int IntCoord; + +/** @brief Default "acceptably small" value. + * @ingroup Primitives */ +const Coord EPSILON = 1e-6; //1e-18; + +/** @brief Get a value representing infinity. + * @ingroup Primitives */ +inline Coord infinity() { return std::numeric_limits::infinity(); } + +/** @brief Nearness predicate for values. + * @ingroup Primitives */ +inline bool are_near(Coord a, Coord b, double eps=EPSILON) { return a-b <= eps && a-b >= -eps; } +inline bool rel_error_bound(Coord a, Coord b, double eps=EPSILON) { return a <= eps*b && a >= -eps*b; } + +/** @brief Numerically stable linear interpolation. + * @ingroup Primitives */ +inline Coord lerp(Coord t, Coord a, Coord b) { + return (1 - t) * a + t * b; +} + +/** @brief Traits class used with coordinate types. + * Defines point, interval and rectangle types for the given coordinate type. + * @ingroup Utilities */ +template +struct CoordTraits { + typedef D2 PointType; + typedef GenericInterval IntervalType; + typedef GenericOptInterval OptIntervalType; + typedef GenericRect RectType; + typedef GenericOptRect OptRectType; + + typedef + boost::equality_comparable< IntervalType + , boost::orable< IntervalType + > > + IntervalOps; + + typedef + boost::equality_comparable< RectType + , boost::orable< RectType + , boost::orable< RectType, OptRectType + > > > + RectOps; +}; + +// NOTE: operator helpers for Rect and Interval are defined here. +// This is to avoid increasing their size through multiple inheritance. + +template<> +struct CoordTraits { + typedef IntPoint PointType; + typedef IntInterval IntervalType; + typedef OptIntInterval OptIntervalType; + typedef IntRect RectType; + typedef OptIntRect OptRectType; + + typedef + boost::equality_comparable< IntInterval + , boost::additive< IntInterval + , boost::additive< IntInterval, IntCoord + , boost::orable< IntInterval + > > > > + IntervalOps; + + typedef + boost::equality_comparable< IntRect + , boost::orable< IntRect + , boost::orable< IntRect, OptIntRect + , boost::additive< IntRect, IntPoint + > > > > + RectOps; +}; + +template<> +struct CoordTraits { + typedef Point PointType; + typedef Interval IntervalType; + typedef OptInterval OptIntervalType; + typedef Rect RectType; + typedef OptRect OptRectType; + + typedef + boost::equality_comparable< Interval + , boost::equality_comparable< Interval, IntInterval + , boost::additive< Interval + , boost::multipliable< Interval + , boost::orable< Interval + , boost::arithmetic< Interval, Coord + > > > > > > + IntervalOps; + + typedef + boost::equality_comparable< Rect + , boost::equality_comparable< Rect, IntRect + , boost::orable< Rect + , boost::orable< Rect, OptRect + , boost::additive< Rect, Point + , boost::multipliable< Rect, Affine + > > > > > > + RectOps; +}; + +/** @brief Convert coordinate to shortest possible string. + * @return The shortest string that parses back to the original value. + * @relates Coord */ +std::string format_coord_shortest(Coord x); + +/** @brief Convert coordinate to human-readable string. + * Unlike format_coord_shortest, this function will not omit a leading zero + * before a decimal point or use small negative exponents. The output format + * is similar to Javascript functions. + * @relates Coord */ +std::string format_coord_nice(Coord x); + +/** @brief Parse coordinate string. + * When using this function in conjunction with format_coord_shortest() + * or format_coord_nice(), the value is guaranteed to be preserved exactly. + * @relates Coord */ +Coord parse_coord(std::string const &s); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_COORD_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/crossing.cpp b/src/2geom/crossing.cpp new file mode 100644 index 0000000..e27a2fc --- /dev/null +++ b/src/2geom/crossing.cpp @@ -0,0 +1,233 @@ +#include <2geom/crossing.h> +#include <2geom/path.h> + +namespace Geom { + +//bool edge_involved_in(Edge const &e, Crossing const &c) { +// if(e.path == c.a) { +// if(e.time == c.ta) return true; +// } else if(e.path == c.b) { +// if(e.time == c.tb) return true; +// } +// return false; +//} + +double wrap_dist(double from, double to, double size, bool rev) { + if(rev) { + if(to > from) { + return from + (size - to); + } else { + return from - to; + } + } else { + if(to < from) { + return to + (size - from); + } else { + return to - from; + } + } +} +/* +CrossingGraph create_crossing_graph(PathVector const &p, Crossings const &crs) { + std::vector locs; + CrossingGraph ret; + for(unsigned i = 0; i < crs.size(); i++) { + Point pnt = p[crs[i].a].pointAt(crs[i].ta); + unsigned j = 0; + for(; j < locs.size(); j++) { + if(are_near(pnt, locs[j])) break; + } + if(j == locs.size()) { + ret.push_back(CrossingNode()); + locs.push_back(pnt); + } + ret[j].add_edge(Edge(crs[i].a, crs[i].ta, false)); + ret[j].add_edge(Edge(crs[i].a, crs[i].ta, true)); + ret[j].add_edge(Edge(crs[i].b, crs[i].tb, false)); + ret[j].add_edge(Edge(crs[i].b, crs[i].tb, true)); + } + + for(unsigned i = 0; i < ret.size(); i++) { + for(unsigned j = 0; j < ret[i].edges.size(); j++) { + unsigned pth = ret[i].edges[j].path; + double t = ret[i].edges[j].time; + bool rev = ret[i].edges[j].reverse; + double size = p[pth].size()+1; + double best = size; + unsigned bix = ret.size(); + for(unsigned k = 0; k < ret.size(); k++) { + for(unsigned l = 0; l < ret[k].edges.size(); l++) { + if(ret[i].edges[j].path == ret[k].edges[l].path && (k != i || l != j)) { + double d = wrap_dist(t, ret[i].edges[j].time, size, rev); + if(d < best) { + best = d; + bix = k; + } + } + } + } + if(bix == ret.size()) { + std::cout << "couldn't find an adequate next-crossing node"; + bix = i; + } + ret[i].edges[j].node = bix; + } + } + + return ret; + */ + /* Various incoherent code bits + // list of sets of edges, each set corresponding to those emanating from the path + CrossingGraph ret; + std::vector edges(crs.size()); + + std::vector > used; + unsigned i, j; + do { + first_false(used, i, j); + CrossingNode cn; + do { + unsigned di = i, dj = j; + crossing_dual(di, dj); + if(!used[di,dj]) { + + } + } + + } while(!used[i,j]) + + + for(unsigned j = 0; j < crs[i].size(); j++) { + + edges.push_back(Edge(i, crs[i][j].getOtherTime(i), false)); + edges.push_back(Edge(i, crs[i][j].getOtherTime(i), true)); + } + std::sort(edges.begin(), edges.end(), TimeOrder()); + for(unsigned j = 0; j < edges.size(); ) { + CrossingNode cn; + double t = edges[j].time; + while(j < edges.size() && are_near(edges[j].time, t)) { + cn.edges.push_back(edges[j]); + } + } +*/ +//} + +// provide specific method for Paths because paths can be closed or open. Path::size() is named somewhat wrong... +std::vector bounds(Path const &a) { + std::vector rs; + for (unsigned i = 0; i < a.size_default(); i++) { + OptRect bb = a[i].boundsFast(); + if (bb) { + rs.push_back(*bb); + } + } + return rs; +} + +void merge_crossings(Crossings &a, Crossings &b, unsigned i) { + Crossings n; + sort_crossings(b, i); + n.resize(a.size() + b.size()); + std::merge(a.begin(), a.end(), b.begin(), b.end(), n.begin(), CrossingOrder(i)); + a = n; +} + +void offset_crossings(Crossings &cr, double a, double b) { + for(unsigned i = 0; i < cr.size(); i++) { + cr[i].ta += a; + cr[i].tb += b; + } +} + +Crossings reverse_ta(Crossings const &cr, std::vector max) { + Crossings ret; + for(Crossings::const_iterator i = cr.begin(); i != cr.end(); ++i) { + double mx = max[i->a]; + ret.push_back(Crossing(i->ta > mx+0.01 ? (1 - (i->ta - mx) + mx) : mx - i->ta, + i->tb, !i->dir)); + } + return ret; +} + +Crossings reverse_tb(Crossings const &cr, unsigned split, std::vector max) { + Crossings ret; + for(Crossings::const_iterator i = cr.begin(); i != cr.end(); ++i) { + double mx = max[i->b - split]; + ret.push_back(Crossing(i->ta, i->tb > mx+0.01 ? (1 - (i->tb - mx) + mx) : mx - i->tb, + !i->dir)); + } + return ret; +} + +CrossingSet reverse_ta(CrossingSet const &cr, unsigned split, std::vector max) { + CrossingSet ret; + for(unsigned i = 0; i < cr.size(); i++) { + Crossings res = reverse_ta(cr[i], max); + if(i < split) std::reverse(res.begin(), res.end()); + ret.push_back(res); + } + return ret; +} + +CrossingSet reverse_tb(CrossingSet const &cr, unsigned split, std::vector max) { + CrossingSet ret; + for(unsigned i = 0; i < cr.size(); i++) { + Crossings res = reverse_tb(cr[i], split, max); + if(i >= split) std::reverse(res.begin(), res.end()); + ret.push_back(res); + } + return ret; +} + +// Delete any duplicates in a vector of crossings +// A crossing is considered to be a duplicate when it has both t_a and t_b near to another crossing's t_a and t_b +// For example, duplicates will be found when calculating the intersections of a linesegment with a polygon, if the +// endpoint of that line coincides with a cusp node of the polygon. In that case, an intersection will be found of +// the linesegment with each of the polygon's linesegments extending from the cusp node (i.e. two intersections) +void delete_duplicates(Crossings &crs) { + Crossings::reverse_iterator rit = crs.rbegin(); + + for (rit = crs.rbegin(); rit!= crs.rend(); ++rit) { + Crossings::reverse_iterator rit2 = rit; + while (++rit2 != crs.rend()) { + if (Geom::are_near((*rit).ta, (*rit2).ta) && Geom::are_near((*rit).tb, (*rit2).tb)) { + crs.erase((rit + 1).base()); // This +1 and .base() construction is needed to convert to a regular iterator + break; // out of while loop, and continue with next iteration of for loop + } + } + } +} + +void clean(Crossings &/*cr_a*/, Crossings &/*cr_b*/) { +/* if(cr_a.empty()) return; + + //Remove anything with dupes + + for(Eraser i(&cr_a); !i.ended(); i++) { + const Crossing cur = *i; + Eraser next(i); + next++; + if(are_near(cur, *next)) { + cr_b.erase(std::find(cr_b.begin(), cr_b.end(), cur)); + for(i = next; near(*i, cur); i++) { + cr_b.erase(std::find(cr_b.begin(), cr_b.end(), *i)); + } + continue; + } + } +*/ +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/crossing.h b/src/2geom/crossing.h new file mode 100644 index 0000000..0f007b1 --- /dev/null +++ b/src/2geom/crossing.h @@ -0,0 +1,213 @@ +/** + * @file + * @brief Structure representing the intersection of two curves + *//* + * Authors: + * Michael Sloan + * Marco Cecchetti + * + * Copyright 2006-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_CROSSING_H +#define LIB2GEOM_SEEN_CROSSING_H + +#include +#include <2geom/rect.h> +#include <2geom/sweep-bounds.h> +#include +#include <2geom/pathvector.h> + +namespace Geom { + +//Crossing between one or two paths +struct Crossing { + bool dir; //True: along a, a becomes outside. + double ta, tb; //time on a and b of crossing + unsigned a, b; //storage of indices + Crossing() : dir(false), ta(0), tb(1), a(0), b(1) {} + Crossing(double t_a, double t_b, bool direction) : dir(direction), ta(t_a), tb(t_b), a(0), b(1) {} + Crossing(double t_a, double t_b, unsigned ai, unsigned bi, bool direction) : dir(direction), ta(t_a), tb(t_b), a(ai), b(bi) {} + bool operator==(const Crossing & other) const { return a == other.a && b == other.b && dir == other.dir && ta == other.ta && tb == other.tb; } + bool operator!=(const Crossing & other) const { return !(*this == other); } + + unsigned getOther(unsigned cur) const { return a == cur ? b : a; } + double getTime(unsigned cur) const { return a == cur ? ta : tb; } + double getOtherTime(unsigned cur) const { return a == cur ? tb : ta; } + bool onIx(unsigned ix) const { return a == ix || b == ix; } +}; + +typedef boost::optional OptCrossing; + + +/* +struct Edge { + unsigned node, path; + double time; + bool reverse; + Edge(unsigned p, double t, bool r) : path(p), time(t), reverse(r) {} + bool operator==(Edge const &other) const { return other.path == path && other.time == time && other.reverse == reverse; } +}; + +struct CrossingNode { + std::vector edges; + CrossingNode() : edges(std::vector()) {} + explicit CrossingNode(std::vector es) : edges(es) {} + void add_edge(Edge const &e) { + if(std::find(edges.begin(), edges.end(), e) == edges.end()) + edges.push_back(e); + } + double time_on(unsigned p) { + for(unsigned i = 0; i < edges.size(); i++) + if(edges[i].path == p) return edges[i].time; + std::cout << "CrossingNode time_on failed\n"; + return 0; + } +}; + + +typedef std::vector CrossingGraph; + +struct TimeOrder { + bool operator()(Edge a, Edge b) { + return a.time < b.time; + } +}; + +class Path; +CrossingGraph create_crossing_graph(PathVector const &p, Crossings const &crs); +*/ + +/*inline bool are_near(Crossing a, Crossing b) { + return are_near(a.ta, b.ta) && are_near(a.tb, b.tb); +} + +struct NearF { bool operator()(Crossing a, Crossing b) { return are_near(a, b); } }; +*/ + +struct CrossingOrder { + unsigned ix; + bool rev; + CrossingOrder(unsigned i, bool r = false) : ix(i), rev(r) {} + bool operator()(Crossing a, Crossing b) { + if(rev) + return (ix == a.a ? a.ta : a.tb) < + (ix == b.a ? b.ta : b.tb); + else + return (ix == a.a ? a.ta : a.tb) > + (ix == b.a ? b.ta : b.tb); + } +}; + +typedef std::vector Crossings; + +typedef std::vector CrossingSet; + +template +std::vector bounds(C const &a) { + std::vector rs; + for (unsigned i = 0; i < a.size(); i++) { + OptRect bb = a[i].boundsFast(); + if (bb) { + rs.push_back(*bb); + } + } + return rs; +} +// provide specific method for Paths because paths can be closed or open. Path::size() is named somewhat wrong... +std::vector bounds(Path const &a); + +inline void sort_crossings(Crossings &cr, unsigned ix) { std::sort(cr.begin(), cr.end(), CrossingOrder(ix)); } + +template +struct CrossingTraits { + typedef std::vector VectorT; + static inline VectorT init(T const &x) { return VectorT(1, x); } +}; +template <> +struct CrossingTraits { + typedef PathVector VectorT; + static inline VectorT vector_one(Path const &x) { return VectorT(x); } +}; + +template +struct Crosser { + typedef typename CrossingTraits::VectorT VectorT; + virtual ~Crosser() {} + virtual Crossings crossings(T const &a, T const &b) { + return crossings(CrossingTraits::vector_one(a), CrossingTraits::vector_one(b))[0]; } + virtual CrossingSet crossings(VectorT const &a, VectorT const &b) { + CrossingSet results(a.size() + b.size(), Crossings()); + + std::vector > cull = sweep_bounds(bounds(a), bounds(b)); + for(unsigned i = 0; i < cull.size(); i++) { + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + unsigned jc = j + a.size(); + Crossings cr = crossings(a[i], b[j]); + for(unsigned k = 0; k < cr.size(); k++) { cr[k].a = i; cr[k].b = jc; } + + //Sort & add A-sorted crossings + sort_crossings(cr, i); + Crossings n(results[i].size() + cr.size()); + std::merge(results[i].begin(), results[i].end(), cr.begin(), cr.end(), n.begin(), CrossingOrder(i)); + results[i] = n; + + //Sort & add B-sorted crossings + sort_crossings(cr, jc); + n.resize(results[jc].size() + cr.size()); + std::merge(results[jc].begin(), results[jc].end(), cr.begin(), cr.end(), n.begin(), CrossingOrder(jc)); + results[jc] = n; + } + } + return results; + } +}; +void merge_crossings(Crossings &a, Crossings &b, unsigned i); +void offset_crossings(Crossings &cr, double a, double b); + +Crossings reverse_ta(Crossings const &cr, std::vector max); +Crossings reverse_tb(Crossings const &cr, unsigned split, std::vector max); +CrossingSet reverse_ta(CrossingSet const &cr, unsigned split, std::vector max); +CrossingSet reverse_tb(CrossingSet const &cr, unsigned split, std::vector max); + +void clean(Crossings &cr_a, Crossings &cr_b); +void delete_duplicates(Crossings &crs); + +} // end namespace Geom + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/curve.cpp b/src/2geom/curve.cpp new file mode 100644 index 0000000..8ad0178 --- /dev/null +++ b/src/2geom/curve.cpp @@ -0,0 +1,187 @@ +/* Abstract curve type - implementation of default methods + * + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/curve.h> +#include <2geom/exception.h> +#include <2geom/nearest-time.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/ord.h> +#include <2geom/path-sink.h> + +//#include + +namespace Geom +{ + +Coord Curve::nearestTime(Point const& p, Coord a, Coord b) const +{ + return nearest_time(p, toSBasis(), a, b); +} + +std::vector Curve::allNearestTimes(Point const& p, Coord from, Coord to) const +{ + return all_nearest_times(p, toSBasis(), from, to); +} + +Coord Curve::length(Coord tolerance) const +{ + return ::Geom::length(toSBasis(), tolerance); +} + +int Curve::winding(Point const &p) const +{ + try { + std::vector ts = roots(p[Y], Y); + if(ts.empty()) return 0; + std::sort(ts.begin(), ts.end()); + + // skip endpoint roots when they are local maxima on the Y axis + // this follows the convention used in other winding routines, + // i.e. that the bottommost coordinate is not part of the shape + bool ignore_0 = unitTangentAt(0)[Y] <= 0; + bool ignore_1 = unitTangentAt(1)[Y] >= 0; + + int wind = 0; + for (std::size_t i = 0; i < ts.size(); ++i) { + Coord t = ts[i]; + //std::cout << t << std::endl; + if ((t == 0 && ignore_0) || (t == 1 && ignore_1)) continue; + if (valueAt(t, X) > p[X]) { // root is ray intersection + Point tangent = unitTangentAt(t); + if (tangent[Y] > 0) { + // at the point of intersection, curve goes in +Y direction, + // so it winds in the direction of positive angles + ++wind; + } else if (tangent[Y] < 0) { + --wind; + } + } + } + return wind; + } catch (InfiniteSolutions const &e) { + // this means we encountered a line segment exactly coincident with the point + // skip, since this will be taken care of by endpoint roots in other segments + return 0; + } +} + +std::vector Curve::intersect(Curve const &/*other*/, Coord /*eps*/) const +{ + // TODO: approximate as Bezier + THROW_NOTIMPLEMENTED(); +} + +std::vector Curve::intersectSelf(Coord eps) const +{ + std::vector result; + // Monotonic segments cannot have self-intersections. + // Thus, we can split the curve at roots and intersect the portions. + std::vector splits; + std::unique_ptr deriv(derivative()); + splits = deriv->roots(0, X); + if (splits.empty()) { + return result; + } + deriv.reset(); + splits.push_back(1.); + + boost::ptr_vector parts; + Coord previous = 0; + for (unsigned i = 0; i < splits.size(); ++i) { + if (splits[i] == 0.) continue; + parts.push_back(portion(previous, splits[i])); + previous = splits[i]; + } + + Coord prev_i = 0; + for (unsigned i = 0; i < parts.size()-1; ++i) { + Interval dom_i(prev_i, splits[i]); + prev_i = splits[i]; + + Coord prev_j = 0; + for (unsigned j = i+1; j < parts.size(); ++j) { + Interval dom_j(prev_j, splits[j]); + prev_j = splits[j]; + + std::vector xs = parts[i].intersect(parts[j], eps); + for (unsigned k = 0; k < xs.size(); ++k) { + // to avoid duplicated intersections, skip values at exactly 1 + if (xs[k].first == 1. || xs[k].second == 1.) continue; + + Coord ti = dom_i.valueAt(xs[k].first); + Coord tj = dom_j.valueAt(xs[k].second); + + CurveIntersection real(ti, tj, xs[k].point()); + result.push_back(real); + } + } + } + return result; +} + +Point Curve::unitTangentAt(Coord t, unsigned n) const +{ + std::vector derivs = pointAndDerivatives(t, n); + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Coord length = derivs[deriv_n].length(); + if ( ! are_near(length, 0) ) { + // length of derivative is non-zero, so return unit vector + return derivs[deriv_n] / length; + } + } + return Point (0,0); +}; + +void Curve::feed(PathSink &sink, bool moveto_initial) const +{ + std::vector pts; + sbasis_to_bezier(pts, toSBasis(), 2); //TODO: use something better! + if (moveto_initial) { + sink.moveTo(initialPoint()); + } + sink.curveTo(pts[0], pts[1], pts[2]); +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/curve.h b/src/2geom/curve.h new file mode 100644 index 0000000..4470366 --- /dev/null +++ b/src/2geom/curve.h @@ -0,0 +1,369 @@ +/** + * \file + * \brief Abstract curve type + * + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef LIB2GEOM_SEEN_CURVE_H +#define LIB2GEOM_SEEN_CURVE_H + +#include +#include +#include <2geom/coord.h> +#include <2geom/point.h> +#include <2geom/interval.h> +#include <2geom/sbasis.h> +#include <2geom/d2.h> +#include <2geom/affine.h> +#include <2geom/intersection.h> + +namespace Geom { + +class PathSink; +typedef Intersection<> CurveIntersection; + +/** + * @brief Abstract continuous curve on a plane defined on [0,1]. + * + * Formally, a curve in 2Geom is defined as a function + * \f$\mathbf{C}: [0, 1] \to \mathbb{R}^2\f$ + * (a function that maps the unit interval to points on a 2D plane). Its image (the set of points + * the curve passes through) will be denoted \f$\mathcal{C} = \mathbf{C}[ [0, 1] ]\f$. + * All curve types available in 2Geom are continuous and differentiable on their + * interior, e.g. \f$(0, 1)\f$. Sometimes the curve's image (value set) is referred to as the curve + * itself for simplicity, but keep in mind that it's not strictly correct. + * + * It is common to think of the parameter as time. The curve can then be interpreted as + * describing the position of some moving object from time \f$t=0\f$ to \f$t=1\f$. + * Because of this, the parameter is frequently called the time value. + * + * Some methods return pointers to newly allocated curves. They are expected to be freed + * by the caller when no longer used. Default implementations are provided for some methods. + * + * @ingroup Curves + */ +class Curve + : boost::equality_comparable +{ +public: + virtual ~Curve() {} + + /// @name Evaluate the curve + /// @{ + /** @brief Retrieve the start of the curve. + * @return The point corresponding to \f$\mathbf{C}(0)\f$. */ + virtual Point initialPoint() const = 0; + + /** Retrieve the end of the curve. + * @return The point corresponding to \f$\mathbf{C}(1)\f$. */ + virtual Point finalPoint() const = 0; + + /** @brief Check whether the curve has exactly zero length. + * @return True if the curve's initial point is exactly the same as its final point, and it contains + * no other points (its value set contains only one element). */ + virtual bool isDegenerate() const = 0; + + /// Check whether the curve is a line segment. + virtual bool isLineSegment() const { return false; } + + /** @brief Get the interval of allowed time values. + * @return \f$[0, 1]\f$ */ + virtual Interval timeRange() const { + Interval tr(0, 1); + return tr; + } + + /** @brief Evaluate the curve at a specified time value. + * @param t Time value + * @return \f$\mathbf{C}(t)\f$ */ + virtual Point pointAt(Coord t) const { return pointAndDerivatives(t, 0).front(); } + + /** @brief Evaluate one of the coordinates at the specified time value. + * @param t Time value + * @param d The dimension to evaluate + * @return The specified coordinate of \f$\mathbf{C}(t)\f$ */ + virtual Coord valueAt(Coord t, Dim2 d) const { return pointAt(t)[d]; } + + /** @brief Evaluate the function at the specified time value. Allows curves to be used + * as functors. */ + virtual Point operator() (Coord t) const { return pointAt(t); } + + /** @brief Evaluate the curve and its derivatives. + * This will return a vector that contains the value of the curve and the specified number + * of derivatives. However, the returned vector might contain less elements than specified + * if some derivatives do not exist. + * @param t Time value + * @param n The number of derivatives to compute + * @return Vector of at most \f$n+1\f$ elements of the form \f$[\mathbf{C}(t), + \mathbf{C}'(t), \mathbf{C}''(t), \ldots]\f$ */ + virtual std::vector pointAndDerivatives(Coord t, unsigned n) const = 0; + /// @} + + /// @name Change the curve's endpoints + /// @{ + /** @brief Change the starting point of the curve. + * After calling this method, it is guaranteed that \f$\mathbf{C}(0) = \mathbf{p}\f$, + * and the curve is still continuous. The precise new shape of the curve varies with curve + * type. + * @param p New starting point of the curve */ + virtual void setInitial(Point const &v) = 0; + + /** @brief Change the ending point of the curve. + * After calling this method, it is guaranteed that \f$\mathbf{C}(0) = \mathbf{p}\f$, + * and the curve is still continuous. The precise new shape of the curve varies + * with curve type. + * @param p New ending point of the curve */ + virtual void setFinal(Point const &v) = 0; + /// @} + + /// @name Compute the bounding box + /// @{ + /** @brief Quickly compute the curve's approximate bounding box. + * The resulting rectangle is guaranteed to contain all points belonging to the curve, + * but it might not be the smallest such rectangle. This method is usually fast. + * @return A rectangle that contains all points belonging to the curve. */ + virtual Rect boundsFast() const = 0; + + /** @brief Compute the curve's exact bounding box. + * This method can be dramatically slower than boundsExact() depending on the curve type. + * @return The smallest possible rectangle containing all of the curve's points. */ + virtual Rect boundsExact() const = 0; + + // I have no idea what the 'deg' parameter is for, so this is undocumented for now. + virtual OptRect boundsLocal(OptInterval const &i, unsigned deg) const = 0; + + /** @brief Compute the bounding box of a part of the curve. + * Since this method returns the smallest possible bounding rectangle of the specified portion, + * it can also be rather slow. + * @param a An interval specifying a part of the curve, or nothing. + * If \f$[0, 1] \subseteq a\f$, then the bounding box for the entire curve + * is calculated. + * @return The smallest possible rectangle containing all points in \f$\mathbf{C}[a]\f$, + * or nothing if the supplied interval is empty. */ + OptRect boundsLocal(OptInterval const &a) const { return boundsLocal(a, 0); } + /// @} + + /// @name Create new curves based on this one + /// @{ + /** @brief Create an exact copy of this curve. + * @return Pointer to a newly allocated curve, identical to the original */ + virtual Curve *duplicate() const = 0; + + /** @brief Transform this curve by an affine transformation. + * Because of this method, all curve types must be closed under affine + * transformations. + * @param m Affine describing the affine transformation */ + void transform(Affine const &m) { + *this *= m; + } + + virtual void operator*=(Translate const &tr) { *this *= Affine(tr); } + virtual void operator*=(Scale const &s) { *this *= Affine(s); } + virtual void operator*=(Rotate const &r) { *this *= Affine(r); } + virtual void operator*=(HShear const &hs) { *this *= Affine(hs); } + virtual void operator*=(VShear const &vs) { *this *= Affine(vs); } + virtual void operator*=(Zoom const &z) { *this *= Affine(z); } + virtual void operator*=(Affine const &m) = 0; + + /** @brief Create a curve transformed by an affine transformation. + * This method returns a new curve instead modifying the existing one. + * @param m Affine describing the affine transformation + * @return Pointer to a new, transformed curve */ + virtual Curve *transformed(Affine const &m) const { + Curve *ret = duplicate(); + ret->transform(m); + return ret; + } + + /** @brief Create a curve that corresponds to a part of this curve. + * For \f$a > b\f$, the returned portion will be reversed with respect to the original. + * The returned curve will always be of the same type. + * @param a Beginning of the interval specifying the portion of the curve + * @param b End of the interval + * @return New curve \f$\mathbf{D}\f$ such that: + * - \f$\mathbf{D}(0) = \mathbf{C}(a)\f$ + * - \f$\mathbf{D}(1) = \mathbf{C}(b)\f$ + * - \f$\mathbf{D}[ [0, 1] ] = \mathbf{C}[ [a?b] ]\f$, + * where \f$[a?b] = [\min(a, b), \max(a, b)]\f$ */ + virtual Curve *portion(Coord a, Coord b) const = 0; + + /** @brief A version of that accepts an Interval. */ + Curve *portion(Interval const &i) const { return portion(i.min(), i.max()); } + + /** @brief Create a reversed version of this curve. + * The result corresponds to portion(1, 0), but this method might be faster. + * @return Pointer to a new curve \f$\mathbf{D}\f$ such that + * \f$\forall_{x \in [0, 1]} \mathbf{D}(x) = \mathbf{C}(1-x)\f$ */ + virtual Curve *reverse() const { return portion(1, 0); } + + /** @brief Create a derivative of this curve. + * It's best to think of the derivative in physical terms: if the curve describes + * the position of some object on the plane from time \f$t=0\f$ to \f$t=1\f$ as said in the + * introduction, then the curve's derivative describes that object's speed at the same times. + * The second derivative refers to its acceleration, the third to jerk, etc. + * @return New curve \f$\mathbf{D} = \mathbf{C}'\f$. */ + virtual Curve *derivative() const = 0; + /// @} + + /// @name Advanced operations + /// @{ + /** @brief Compute a time value at which the curve comes closest to a specified point. + * The first value with the smallest distance is returned if there are multiple such points. + * @param p Query point + * @param a Minimum time value to consider + * @param b Maximum time value to consider; \f$a < b\f$ + * @return \f$q \in [a, b]: ||\mathbf{C}(q) - \mathbf{p}|| = + \inf(\{r \in \mathbb{R} : ||\mathbf{C}(r) - \mathbf{p}||\})\f$ */ + virtual Coord nearestTime( Point const& p, Coord a = 0, Coord b = 1 ) const; + + /** @brief A version that takes an Interval. */ + Coord nearestTime(Point const &p, Interval const &i) const { + return nearestTime(p, i.min(), i.max()); + } + + /** @brief Compute time values at which the curve comes closest to a specified point. + * @param p Query point + * @param a Minimum time value to consider + * @param b Maximum time value to consider; \f$a < b\f$ + * @return Vector of points closest and equally far away from the query point */ + virtual std::vector allNearestTimes( Point const& p, Coord from = 0, + Coord to = 1 ) const; + + /** @brief A version that takes an Interval. */ + std::vector allNearestTimes(Point const &p, Interval const &i) { + return allNearestTimes(p, i.min(), i.max()); + } + + /** @brief Compute the arc length of this curve. + * For a curve \f$\mathbf{C}(t) = (C_x(t), C_y(t))\f$, arc length is defined for 2D curves as + * \f[ \ell = \int_{0}^{1} \sqrt { [C_x'(t)]^2 + [C_y'(t)]^2 }\, \text{d}t \f] + * In other words, we divide the curve into infinitely small linear segments + * and add together their lengths. Of course we can't subdivide the curve into + * infinitely many segments on a computer, so this method returns an approximation. + * Not that there is usually no closed form solution to such integrals, so this + * method might be slow. + * @param tolerance Maximum allowed error + * @return Total distance the curve's value travels on the plane when going from 0 to 1 */ + virtual Coord length(Coord tolerance=0.01) const; + + /** @brief Computes time values at which the curve intersects an axis-aligned line. + * @param v The coordinate of the line + * @param d Which axis the coordinate is on. X means a vertical line, Y a horizontal line. */ + virtual std::vector roots(Coord v, Dim2 d) const = 0; + + /** @brief Compute the partial winding number of this curve. + * The partial winding number is equal to the difference between the number + * of roots at which the curve goes in the +Y direction and the number of roots + * at which the curve goes in the -Y direction. This method is mainly useful + * for implementing path winding calculation. It will ignore roots which + * are local maxima on the Y axis. + * @param p Point where the winding number should be determined + * @return Winding number contribution at p */ + virtual int winding(Point const &p) const; + + /// Compute intersections with another curve. + virtual std::vector intersect(Curve const &other, Coord eps = EPSILON) const; + + /// Compute intersections of this curve with itself. + virtual std::vector intersectSelf(Coord eps = EPSILON) const; + + /** @brief Compute a vector tangent to the curve. + * This will return an unit vector (a Point with length() equal to 1) that denotes a vector + * tangent to the curve. This vector is defined as + * \f$ \mathbf{v}(t) = \frac{\mathbf{C}'(t)}{||\mathbf{C}'(t)||} \f$. It is pointed + * in the direction of increasing \f$t\f$, at the specified time value. The method uses + * l'Hopital's rule when the derivative is zero. A zero vector is returned if no non-zero + * derivative could be found. + * @param t Time value + * @param n The maximum order of derivative to consider + * @return Unit tangent vector \f$\mathbf{v}(t)\f$ */ + virtual Point unitTangentAt(Coord t, unsigned n = 3) const; + + /** @brief Convert the curve to a symmetric power basis polynomial. + * Symmetric power basis polynomials (S-basis for short) are numerical representations + * of curves with excellent numerical properties. Most high level operations provided by 2Geom + * are implemented in terms of S-basis operations, so every curve has to provide a method + * to convert it to an S-basis polynomial on two variables. See SBasis class reference + * for more information. */ + virtual D2 toSBasis() const = 0; + /// @} + + /// @name Miscellaneous + /// @{ + /** Return the number of independent parameters required to represent all variations + * of this curve. For example, for Bezier curves it returns the curve's order + * multiplied by 2. */ + virtual int degreesOfFreedom() const { return 0;} + + /** @brief Test equality of two curves. + * Equality means that for any time value, the evaluation of either curve will yield + * the same value. This means non-degenerate curves are not equal to their reverses. + * Note that this tests for exact equality. + * @return True if the curves are identical, false otherwise */ + virtual bool operator==(Curve const &c) const = 0; + + /** @brief Test whether two curves are approximately the same. */ + virtual bool isNear(Curve const &c, Coord precision) const = 0; + + /** @brief Feed the curve to a PathSink */ + virtual void feed(PathSink &sink, bool moveto_initial) const; + /// @} +}; + +inline +Coord nearest_time(Point const& p, Curve const& c) { + return c.nearestTime(p); +} + +// for make benefit glorious library of Boost Pointer Container +inline +Curve *new_clone(Curve const &c) { + return c.duplicate(); +} + +} // end namespace Geom + + +#endif // _2GEOM_CURVE_H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/curves.h b/src/2geom/curves.h new file mode 100644 index 0000000..46fb6d9 --- /dev/null +++ b/src/2geom/curves.h @@ -0,0 +1,54 @@ +/** @file + * @brief Include all curve types + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * + * Copyright 2007-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_CURVES_H +#define LIB2GEOM_SEEN_CURVES_H + +#include <2geom/curve.h> +#include <2geom/sbasis-curve.h> +#include <2geom/bezier-curve.h> +#include <2geom/elliptical-arc.h> + +#endif // LIB2GEOM_SEEN_CURVES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/2geom/d2-sbasis.cpp b/src/2geom/d2-sbasis.cpp new file mode 100644 index 0000000..07eccce --- /dev/null +++ b/src/2geom/d2-sbasis.cpp @@ -0,0 +1,364 @@ +/** + * \file + * \brief Some two-dimensional SBasis operations + *//* + * Authors: + * MenTaLguy + * Jean-François Barraud + * Johan Engelen + * + * Copyright 2007-2012 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/d2.h> +#include <2geom/piecewise.h> + +namespace Geom { + +SBasis L2(D2 const & a, unsigned k) { return sqrt(dot(a, a), k); } + +D2 multiply(Linear const & a, D2 const & b) { + return D2(multiply(a, b[X]), multiply(a, b[Y])); +} + +D2 multiply(SBasis const & a, D2 const & b) { + return D2(multiply(a, b[X]), multiply(a, b[Y])); +} + +D2 truncate(D2 const & a, unsigned terms) { + return D2(truncate(a[X], terms), truncate(a[Y], terms)); +} + +unsigned sbasis_size(D2 const & a) { + return std::max((unsigned) a[0].size(), (unsigned) a[1].size()); +} + +//TODO: Is this sensical? shouldn't it be like pythagorean or something? +double tail_error(D2 const & a, unsigned tail) { + return std::max(a[0].tailError(tail), a[1].tailError(tail)); +} + +Piecewise > sectionize(D2 > const &a) { + Piecewise x = partition(a[0], a[1].cuts), y = partition(a[1], a[0].cuts); + assert(x.size() == y.size()); + Piecewise > ret; + for(unsigned i = 0; i < x.size(); i++) + ret.push_seg(D2(x[i], y[i])); + ret.cuts.insert(ret.cuts.end(), x.cuts.begin(), x.cuts.end()); + return ret; +} + +D2 > make_cuts_independent(Piecewise > const &a) { + D2 > ret; + for(unsigned d = 0; d < 2; d++) { + for(unsigned i = 0; i < a.size(); i++) + ret[d].push_seg(a[i][d]); + ret[d].cuts.insert(ret[d].cuts.end(), a.cuts.begin(), a.cuts.end()); + } + return ret; +} + +Piecewise > rot90(Piecewise > const &M){ + Piecewise > result; + if (M.empty()) return M; + result.push_cut(M.cuts[0]); + for (unsigned i=0; i dot(Piecewise > const &a, Piecewise > const &b) +{ + Piecewise result; + if (a.empty() || b.empty()) return result; + Piecewise > aa = partition(a,b.cuts); + Piecewise > bb = partition(b,a.cuts); + + result.push_cut(aa.cuts.front()); + for (unsigned i=0; i dot(Piecewise > const &a, Point const &b) +{ + Piecewise result; + if (a.empty()) return result; + + result.push_cut(a.cuts.front()); + for (unsigned i = 0; i < a.size(); ++i){ + result.push(dot(a.segs[i],b), a.cuts[i+1]); + } + return result; +} + + +Piecewise cross(Piecewise > const &a, + Piecewise > const &b){ + Piecewise result; + if (a.empty() || b.empty()) return result; + Piecewise > aa = partition(a,b.cuts); + Piecewise > bb = partition(b,a.cuts); + + result.push_cut(aa.cuts.front()); + for (unsigned i=0; i > operator*(Piecewise > const &a, Affine const &m) { + Piecewise > result; + if(a.empty()) return result; + result.push_cut(a.cuts[0]); + for (unsigned i = 0; i < a.size(); i++) { + result.push(a[i] * m, a.cuts[i+1]); + } + return result; +} + +//if tol>0, only force continuity where the jump is smaller than tol. +Piecewise > force_continuity(Piecewise > const &f, double tol, bool closed) +{ + if (f.size()==0) return f; + Piecewise > result=f; + unsigned cur = (closed)? 0:1; + unsigned prev = (closed)? f.size()-1:0; + while(cur > > +split_at_discontinuities (Geom::Piecewise > const & pwsbin, double tol) +{ + using namespace Geom; + std::vector > > ret; + unsigned piece_start = 0; + for (unsigned i=0; i tol){ + Piecewise > piece; + piece.cuts.push_back(pwsbin.cuts[piece_start]); + for (unsigned j = piece_start; j const & a, Coord t, unsigned n) +{ + std::vector derivs = a.valueAndDerivatives(t, n); + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Coord length = derivs[deriv_n].length(); + if ( ! are_near(length, 0) ) { + // length of derivative is non-zero, so return unit vector + return derivs[deriv_n] / length; + } + } + return Point (0,0); +} + +static void set_first_point(Piecewise > &f, Point const &a){ + if ( f.empty() ){ + f.concat(Piecewise >(D2(SBasis(Linear(a[X])), SBasis(Linear(a[Y]))))); + return; + } + for (unsigned dim=0; dim<2; dim++){ + f.segs.front()[dim][0][0] = a[dim]; + } +} +static void set_last_point(Piecewise > &f, Point const &a){ + if ( f.empty() ){ + f.concat(Piecewise >(D2(SBasis(Linear(a[X])), SBasis(Linear(a[Y]))))); + return; + } + for (unsigned dim=0; dim<2; dim++){ + f.segs.back()[dim][0][1] = a[dim]; + } +} + +std::vector > > fuse_nearby_ends(std::vector > > const &f, double tol){ + + if ( f.empty()) return f; + std::vector > > result; + std::vector > pre_result; + for (unsigned i=0; i()); + pre_result.back().push_back(i); + } + } + for (unsigned i=0; i > comp; + for (unsigned j=0; j > new_comp = f.at(pre_result[i][j]); + if ( j>0 ){ + set_first_point( new_comp, comp.segs.back().at1() ); + } + comp.concat(new_comp); + } + if ( L2(comp.firstValue()-comp.lastValue()) < tol ){ + //TODO: check sizes!!! + set_last_point( comp, comp.segs.front().at0() ); + } + result.push_back(comp); + } + return result; +} + +/* + * Computes the intersection of two sets given as (ordered) union of intervals. + */ +static std::vector intersect( std::vector const &a, std::vector const &b){ + std::vector result; + //TODO: use order! + for (unsigned i=0; i < a.size(); i++){ + for (unsigned j=0; j < b.size(); j++){ + OptInterval c( a[i] ); + c &= b[j]; + if ( c ) { + result.push_back( *c ); + } + } + } + return result; +} + +std::vector level_set( D2 const &f, Rect region){ + std::vector regions( 1, region ); + return level_sets( f, regions ).front(); +} +std::vector level_set( D2 const &f, Point p, double tol){ + Rect region(p, p); + region.expandBy( tol ); + return level_set( f, region ); +} +std::vector > level_sets( D2 const &f, std::vector regions){ + std::vector regsX (regions.size(), Interval() ); + std::vector regsY (regions.size(), Interval() ); + for ( unsigned i=0; i < regions.size(); i++ ){ + regsX[i] = regions[i][X]; + regsY[i] = regions[i][Y]; + } + std::vector > x_in_regs = level_sets( f[X], regsX ); + std::vector > y_in_regs = level_sets( f[Y], regsY ); + std::vector >result(regions.size(), std::vector() ); + for (unsigned i=0; i > level_sets( D2 const &f, std::vector pts, double tol){ + std::vector regions( pts.size(), Rect() ); + for (unsigned i=0; i + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_D2_H +#define LIB2GEOM_SEEN_D2_H + +#include +#include +#include +#include <2geom/point.h> +#include <2geom/interval.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include <2geom/concepts.h> + +namespace Geom { +/** + * @brief Adaptor that creates 2D functions from 1D ones. + * @ingroup Fragments + */ +template +class D2 +{ +private: + T f[2]; + +public: + typedef T D1Value; + typedef T &D1Reference; + typedef T const &D1ConstReference; + + D2() {f[X] = f[Y] = T();} + explicit D2(Point const &a) { + f[X] = T(a[X]); f[Y] = T(a[Y]); + } + + D2(T const &a, T const &b) { + f[X] = a; + f[Y] = b; + } + + template + D2(Iter first, Iter last) { + typedef typename std::iterator_traits::value_type V; + typedef typename boost::transform_iterator, Iter> XIter; + typedef typename boost::transform_iterator, Iter> YIter; + + XIter xfirst(first, GetAxis()), xlast(last, GetAxis()); + f[X] = T(xfirst, xlast); + YIter yfirst(first, GetAxis()), ylast(last, GetAxis()); + f[Y] = T(yfirst, ylast); + } + + D2(std::vector const &vec) { + typedef Point V; + typedef std::vector::const_iterator Iter; + typedef boost::transform_iterator, Iter> XIter; + typedef boost::transform_iterator, Iter> YIter; + + XIter xfirst(vec.begin(), GetAxis()), xlast(vec.end(), GetAxis()); + f[X] = T(xfirst, xlast); + YIter yfirst(vec.begin(), GetAxis()), ylast(vec.end(), GetAxis()); + f[Y] = T(yfirst, ylast); + } + + //TODO: ask MenTaLguY about operator= as seen in Point + + T& operator[](unsigned i) { return f[i]; } + T const & operator[](unsigned i) const { return f[i]; } + Point point(unsigned i) const { + Point ret(f[X][i], f[Y][i]); + return ret; + } + + //IMPL: FragmentConcept + typedef Point output_type; + bool isZero(double eps=EPSILON) const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return f[X].isZero(eps) && f[Y].isZero(eps); + } + bool isConstant(double eps=EPSILON) const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return f[X].isConstant(eps) && f[Y].isConstant(eps); + } + bool isFinite() const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return f[X].isFinite() && f[Y].isFinite(); + } + Point at0() const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return Point(f[X].at0(), f[Y].at0()); + } + Point at1() const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return Point(f[X].at1(), f[Y].at1()); + } + Point pointAt(double t) const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return (*this)(t); + } + Point valueAt(double t) const { + // TODO: remove this alias + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return (*this)(t); + } + std::vector valueAndDerivatives(double t, unsigned n) const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + std::vector x = f[X].valueAndDerivatives(t, n), + y = f[Y].valueAndDerivatives(t, n); // always returns a vector of size n+1 + std::vector res(n+1); + for(unsigned i = 0; i <= n; i++) { + res[i] = Point(x[i], y[i]); + } + return res; + } + D2 toSBasis() const { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return D2(f[X].toSBasis(), f[Y].toSBasis()); + } + + Point operator()(double t) const; + Point operator()(double x, double y) const; +}; +template +inline D2 reverse(const D2 &a) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return D2(reverse(a[X]), reverse(a[Y])); +} + +template +inline D2 portion(const D2 &a, Coord f, Coord t) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return D2(portion(a[X], f, t), portion(a[Y], f, t)); +} + +template +inline D2 portion(const D2 &a, Interval i) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return D2(portion(a[X], i), portion(a[Y], i)); +} + +//IMPL: EqualityComparableConcept +template +inline bool +operator==(D2 const &a, D2 const &b) { + BOOST_CONCEPT_ASSERT((EqualityComparableConcept)); + return a[0]==b[0] && a[1]==b[1]; +} +template +inline bool +operator!=(D2 const &a, D2 const &b) { + BOOST_CONCEPT_ASSERT((EqualityComparableConcept)); + return a[0]!=b[0] || a[1]!=b[1]; +} + +//IMPL: NearConcept +template +inline bool +are_near(D2 const &a, D2 const &b, double tol) { + BOOST_CONCEPT_ASSERT((NearConcept)); + return are_near(a[0], b[0], tol) && are_near(a[1], b[1], tol); +} + +//IMPL: AddableConcept +template +inline D2 +operator+(D2 const &a, D2 const &b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] + b[i]; + return r; +} +template +inline D2 +operator-(D2 const &a, D2 const &b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] - b[i]; + return r; +} +template +inline D2 +operator+=(D2 &a, D2 const &b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + + for(unsigned i = 0; i < 2; i++) + a[i] += b[i]; + return a; +} +template +inline D2 +operator-=(D2 &a, D2 const & b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + + for(unsigned i = 0; i < 2; i++) + a[i] -= b[i]; + return a; +} + +//IMPL: ScalableConcept +template +inline D2 +operator-(D2 const & a) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = -a[i]; + return r; +} +template +inline D2 +operator*(D2 const & a, Point const & b) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] * b[i]; + return r; +} +template +inline D2 +operator/(D2 const & a, Point const & b) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + //TODO: b==0? + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] / b[i]; + return r; +} +template +inline D2 +operator*=(D2 &a, Point const & b) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + + for(unsigned i = 0; i < 2; i++) + a[i] *= b[i]; + return a; +} +template +inline D2 +operator/=(D2 &a, Point const & b) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + //TODO: b==0? + for(unsigned i = 0; i < 2; i++) + a[i] /= b[i]; + return a; +} + +template +inline D2 operator*(D2 const & a, double b) { return D2(a[0]*b, a[1]*b); } +template +inline D2 operator*=(D2 & a, double b) { a[0] *= b; a[1] *= b; return a; } +template +inline D2 operator/(D2 const & a, double b) { return D2(a[0]/b, a[1]/b); } +template +inline D2 operator/=(D2 & a, double b) { a[0] /= b; a[1] /= b; return a; } + +template +D2 operator*(D2 const &v, Affine const &m) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + BOOST_CONCEPT_ASSERT((ScalableConcept)); + D2 ret; + for(unsigned i = 0; i < 2; i++) + ret[i] = v[X] * m[i] + v[Y] * m[i + 2] + m[i + 4]; + return ret; +} + +//IMPL: MultiplicableConcept +template +inline D2 +operator*(D2 const & a, T const & b) { + BOOST_CONCEPT_ASSERT((MultiplicableConcept)); + D2 ret; + for(unsigned i = 0; i < 2; i++) + ret[i] = a[i] * b; + return ret; +} + +//IMPL: + +//IMPL: OffsetableConcept +template +inline D2 +operator+(D2 const & a, Point b) { + BOOST_CONCEPT_ASSERT((OffsetableConcept)); + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] + b[i]; + return r; +} +template +inline D2 +operator-(D2 const & a, Point b) { + BOOST_CONCEPT_ASSERT((OffsetableConcept)); + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = a[i] - b[i]; + return r; +} +template +inline D2 +operator+=(D2 & a, Point b) { + BOOST_CONCEPT_ASSERT((OffsetableConcept)); + for(unsigned i = 0; i < 2; i++) + a[i] += b[i]; + return a; +} +template +inline D2 +operator-=(D2 & a, Point b) { + BOOST_CONCEPT_ASSERT((OffsetableConcept)); + for(unsigned i = 0; i < 2; i++) + a[i] -= b[i]; + return a; +} + +template +inline T +dot(D2 const & a, D2 const & b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + BOOST_CONCEPT_ASSERT((MultiplicableConcept)); + + T r; + for(unsigned i = 0; i < 2; i++) + r += a[i] * b[i]; + return r; +} + +/** @brief Calculates the 'dot product' or 'inner product' of \c a and \c b + * @return \f$a \bullet b = a_X b_X + a_Y b_Y\f$. + * @relates D2 */ +template +inline T +dot(D2 const & a, Point const & b) { + BOOST_CONCEPT_ASSERT((AddableConcept)); + BOOST_CONCEPT_ASSERT((ScalableConcept)); + + T r; + for(unsigned i = 0; i < 2; i++) { + r += a[i] * b[i]; + } + return r; +} + +/** @brief Calculates the 'cross product' or 'outer product' of \c a and \c b + * @return \f$a \times b = a_Y b_X - a_X b_Y\f$. + * @relates D2 */ +template +inline T +cross(D2 const & a, D2 const & b) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + BOOST_CONCEPT_ASSERT((MultiplicableConcept)); + + return a[1] * b[0] - a[0] * b[1]; +} + + +//equivalent to cw/ccw, for use in situations where rotation direction doesn't matter. +template +inline D2 +rot90(D2 const & a) { + BOOST_CONCEPT_ASSERT((ScalableConcept)); + return D2(-a[Y], a[X]); +} + +//TODO: concepterize the following functions +template +inline D2 +compose(D2 const & a, T const & b) { + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = compose(a[i],b); + return r; +} + +template +inline D2 +compose_each(D2 const & a, D2 const & b) { + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = compose(a[i],b[i]); + return r; +} + +template +inline D2 +compose_each(T const & a, D2 const & b) { + D2 r; + for(unsigned i = 0; i < 2; i++) + r[i] = compose(a,b[i]); + return r; +} + + +template +inline Point +D2::operator()(double t) const { + Point p; + for(unsigned i = 0; i < 2; i++) + p[i] = (*this)[i](t); + return p; +} + +//TODO: we might want to have this take a Point as the parameter. +template +inline Point +D2::operator()(double x, double y) const { + Point p; + for(unsigned i = 0; i < 2; i++) + p[i] = (*this)[i](x, y); + return p; +} + + +template +D2 derivative(D2 const & a) { + return D2(derivative(a[X]), derivative(a[Y])); +} +template +D2 integral(D2 const & a) { + return D2(integral(a[X]), integral(a[Y])); +} + +/** A function to print out the Point. It just prints out the coords + on the given output stream */ +template +inline std::ostream &operator<< (std::ostream &out_file, const Geom::D2 &in_d2) { + out_file << "X: " << in_d2[X] << " Y: " << in_d2[Y]; + return out_file; +} + +//Some D2 Fragment implementation which requires rect: +template +OptRect bounds_fast(const D2 &a) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return OptRect(bounds_fast(a[X]), bounds_fast(a[Y])); +} +template +OptRect bounds_exact(const D2 &a) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return OptRect(bounds_exact(a[X]), bounds_exact(a[Y])); +} +template +OptRect bounds_local(const D2 &a, const OptInterval &t) { + BOOST_CONCEPT_ASSERT((FragmentConcept)); + return OptRect(bounds_local(a[X], t), bounds_local(a[Y], t)); +} + + + +// SBasis-specific declarations + +inline D2 compose(D2 const & a, SBasis const & b) { + return D2(compose(a[X], b), compose(a[Y], b)); +} + +SBasis L2(D2 const & a, unsigned k); +double L2(D2 const & a); + +D2 multiply(Linear const & a, D2 const & b); +inline D2 operator*(Linear const & a, D2 const & b) { return multiply(a, b); } +D2 multiply(SBasis const & a, D2 const & b); +inline D2 operator*(SBasis const & a, D2 const & b) { return multiply(a, b); } +D2 truncate(D2 const & a, unsigned terms); + +unsigned sbasis_size(D2 const & a); +double tail_error(D2 const & a, unsigned tail); + +//Piecewise > specific declarations + +Piecewise > sectionize(D2 > const &a); +D2 > make_cuts_independent(Piecewise > const &a); +Piecewise > rot90(Piecewise > const &a); +Piecewise dot(Piecewise > const &a, Piecewise > const &b); +Piecewise dot(Piecewise > const &a, Point const &b); +Piecewise cross(Piecewise > const &a, Piecewise > const &b); + +Piecewise > operator*(Piecewise > const &a, Affine const &m); + +Piecewise > force_continuity(Piecewise > const &f, double tol=0, bool closed=false); + +std::vector > > fuse_nearby_ends(std::vector > > const &f, double tol=0); + +std::vector > > split_at_discontinuities (Geom::Piecewise > const & pwsbin, double tol = .0001); + +Point unitTangentAt(D2 const & a, Coord t, unsigned n = 3); + +//bounds specializations with order +inline OptRect bounds_fast(D2 const & s, unsigned order=0) { + OptRect retval; + OptInterval xint = bounds_fast(s[X], order); + if (xint) { + OptInterval yint = bounds_fast(s[Y], order); + if (yint) { + retval = Rect(*xint, *yint); + } + } + return retval; +} +inline OptRect bounds_local(D2 const & s, OptInterval i, unsigned order=0) { + OptRect retval; + OptInterval xint = bounds_local(s[X], i, order); + OptInterval yint = bounds_local(s[Y], i, order); + if (xint && yint) { + retval = Rect(*xint, *yint); + } + return retval; +} + +std::vector level_set( D2 const &f, Rect region); +std::vector level_set( D2 const &f, Point p, double tol); +std::vector > level_sets( D2 const &f, std::vector regions); +std::vector > level_sets( D2 const &f, std::vector pts, double tol); + + +} // end namespace Geom + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/ellipse.cpp b/src/2geom/ellipse.cpp new file mode 100644 index 0000000..ad20623 --- /dev/null +++ b/src/2geom/ellipse.cpp @@ -0,0 +1,676 @@ +/** @file + * @brief Ellipse shape + *//* + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2008-2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/numeric/fitting-tool.h> +#include <2geom/numeric/fitting-model.h> + +namespace Geom { + +Ellipse::Ellipse(Geom::Circle const &c) + : _center(c.center()) + , _rays(c.radius(), c.radius()) + , _angle(0) +{} + +void Ellipse::setCoefficients(double A, double B, double C, double D, double E, double F) +{ + double den = 4*A*C - B*B; + if (den == 0) { + THROW_RANGEERROR("den == 0, while computing ellipse centre"); + } + _center[X] = (B*E - 2*C*D) / den; + _center[Y] = (B*D - 2*A*E) / den; + + // evaluate the a coefficient of the ellipse equation in normal form + // E(x,y) = a*(x-cx)^2 + b*(x-cx)*(y-cy) + c*(y-cy)^2 = 1 + // where b = a*B , c = a*C, (cx,cy) == centre + double num = A * sqr(_center[X]) + + B * _center[X] * _center[Y] + + C * sqr(_center[Y]) + - F; + + + //evaluate ellipse rotation angle + _angle = std::atan2( -B, -(A - C) )/2; + + // evaluate the length of the ellipse rays + double sinrot, cosrot; + sincos(_angle, sinrot, cosrot); + double cos2 = cosrot * cosrot; + double sin2 = sinrot * sinrot; + double cossin = cosrot * sinrot; + + den = A * cos2 + B * cossin + C * sin2; + if (den == 0) { + THROW_RANGEERROR("den == 0, while computing 'rx' coefficient"); + } + double rx2 = num / den; + if (rx2 < 0) { + THROW_RANGEERROR("rx2 < 0, while computing 'rx' coefficient"); + } + _rays[X] = std::sqrt(rx2); + + den = C * cos2 - B * cossin + A * sin2; + if (den == 0) { + THROW_RANGEERROR("den == 0, while computing 'ry' coefficient"); + } + double ry2 = num / den; + if (ry2 < 0) { + THROW_RANGEERROR("ry2 < 0, while computing 'rx' coefficient"); + } + _rays[Y] = std::sqrt(ry2); + + // the solution is not unique so we choose always the ellipse + // with a rotation angle between 0 and PI/2 + makeCanonical(); +} + +Point Ellipse::initialPoint() const +{ + Coord sinrot, cosrot; + sincos(_angle, sinrot, cosrot); + Point p(ray(X) * cosrot + center(X), ray(X) * sinrot + center(Y)); + return p; +} + + +Affine Ellipse::unitCircleTransform() const +{ + Affine ret = Scale(ray(X), ray(Y)) * Rotate(_angle); + ret.setTranslation(center()); + return ret; +} + +Affine Ellipse::inverseUnitCircleTransform() const +{ + if (ray(X) == 0 || ray(Y) == 0) { + THROW_RANGEERROR("a degenerate ellipse doesn't have an inverse unit circle transform"); + } + Affine ret = Translate(-center()) * Rotate(-_angle) * Scale(1/ray(X), 1/ray(Y)); + return ret; +} + + +LineSegment Ellipse::axis(Dim2 d) const +{ + Point a(0, 0), b(0, 0); + a[d] = -1; + b[d] = 1; + LineSegment ls(a, b); + ls.transform(unitCircleTransform()); + return ls; +} + +LineSegment Ellipse::semiaxis(Dim2 d, int sign) const +{ + Point a(0, 0), b(0, 0); + b[d] = sgn(sign); + LineSegment ls(a, b); + ls.transform(unitCircleTransform()); + return ls; +} + +Rect Ellipse::boundsExact() const +{ + Angle extremes[2][2]; + double sinrot, cosrot; + sincos(_angle, sinrot, cosrot); + + extremes[X][0] = std::atan2( -ray(Y) * sinrot, ray(X) * cosrot ); + extremes[X][1] = extremes[X][0] + M_PI; + extremes[Y][0] = std::atan2( ray(Y) * cosrot, ray(X) * sinrot ); + extremes[Y][1] = extremes[Y][0] + M_PI; + + Rect result; + for (unsigned d = 0; d < 2; ++d) { + result[d] = Interval(valueAt(extremes[d][0], d ? Y : X), + valueAt(extremes[d][1], d ? Y : X)); + } + return result; +} + +std::vector Ellipse::coefficients() const +{ + std::vector c(6); + coefficients(c[0], c[1], c[2], c[3], c[4], c[5]); + return c; +} + +void Ellipse::coefficients(Coord &A, Coord &B, Coord &C, Coord &D, Coord &E, Coord &F) const +{ + if (ray(X) == 0 || ray(Y) == 0) { + THROW_RANGEERROR("a degenerate ellipse doesn't have an implicit form"); + } + + double cosrot, sinrot; + sincos(_angle, sinrot, cosrot); + double cos2 = cosrot * cosrot; + double sin2 = sinrot * sinrot; + double cossin = cosrot * sinrot; + double invrx2 = 1 / (ray(X) * ray(X)); + double invry2 = 1 / (ray(Y) * ray(Y)); + + A = invrx2 * cos2 + invry2 * sin2; + B = 2 * (invrx2 - invry2) * cossin; + C = invrx2 * sin2 + invry2 * cos2; + D = -2 * A * center(X) - B * center(Y); + E = -2 * C * center(Y) - B * center(X); + F = A * center(X) * center(X) + + B * center(X) * center(Y) + + C * center(Y) * center(Y) + - 1; +} + + +void Ellipse::fit(std::vector const &points) +{ + size_t sz = points.size(); + if (sz < 5) { + THROW_RANGEERROR("fitting error: too few points passed"); + } + NL::LFMEllipse model; + NL::least_squeares_fitter fitter(model, sz); + + for (size_t i = 0; i < sz; ++i) { + fitter.append(points[i]); + } + fitter.update(); + + NL::Vector z(sz, 0.0); + model.instance(*this, fitter.result(z)); +} + + +EllipticalArc * +Ellipse::arc(Point const &ip, Point const &inner, Point const &fp) +{ + // This is resistant to degenerate ellipses: + // both flags evaluate to false in that case. + + bool large_arc_flag = false; + bool sweep_flag = false; + + // Determination of large arc flag: + // large_arc is false when the inner point is on the same side + // of the center---initial point line as the final point, AND + // is on the same side of the center---final point line as the + // initial point. + // Additionally, large_arc is always false when we have exactly + // 1/2 of an arc, i.e. the cross product of the center -> initial point + // and center -> final point vectors is zero. + // Negating the above leads to the condition for large_arc being true. + Point fv = fp - _center; + Point iv = ip - _center; + Point innerv = inner - _center; + double ifcp = cross(fv, iv); + + if (ifcp != 0 && (sgn(cross(fv, innerv)) != sgn(ifcp) || + sgn(cross(iv, innerv)) != sgn(-ifcp))) + { + large_arc_flag = true; + } + + //cross(-iv, fv) && large_arc_flag + + + // Determination of sweep flag: + // For clarity, let's assume that Y grows up. Then the cross product + // is positive for points on the left side of a vector and negative + // on the right side of a vector. + // + // cross(?, v) > 0 + // o-------------------> + // cross(?, v) < 0 + // + // If the arc is small (large_arc_flag is false) and the final point + // is on the right side of the vector initial point -> center, + // we have to go in the direction of increasing angles + // (counter-clockwise) and the sweep flag is true. + // If the arc is large, the opposite is true, since we have to reach + // the final point going the long way - in the other direction. + // We can express this observation as: + // cross(_center - ip, fp - _center) < 0 xor large_arc flag + // This is equal to: + // cross(-iv, fv) < 0 xor large_arc flag + // But cross(-iv, fv) is equal to cross(fv, iv) due to antisymmetry + // of the cross product, so we end up with the condition below. + if ((ifcp < 0) ^ large_arc_flag) { + sweep_flag = true; + } + + EllipticalArc *ret_arc = new EllipticalArc(ip, ray(X), ray(Y), rotationAngle(), + large_arc_flag, sweep_flag, fp); + return ret_arc; +} + +Ellipse &Ellipse::operator*=(Rotate const &r) +{ + _angle += r.angle(); + _center *= r; + return *this; +} + +Ellipse &Ellipse::operator*=(Affine const& m) +{ + Affine a = Scale(ray(X), ray(Y)) * Rotate(_angle); + Affine mwot = m.withoutTranslation(); + Affine am = a * mwot; + Point new_center = _center * m; + + if (are_near(am.descrim(), 0)) { + double angle; + if (am[0] != 0) { + angle = std::atan2(am[2], am[0]); + } else if (am[1] != 0) { + angle = std::atan2(am[3], am[1]); + } else { + angle = M_PI/2; + } + Point v = Point::polar(angle) * am; + _center = new_center; + _rays[X] = L2(v); + _rays[Y] = 0; + _angle = atan2(v); + return *this; + } else if (mwot.isScale(0) && _angle.radians() == 0) { + _rays[X] = _rays[X] * mwot[0]; + _rays[Y] = _rays[Y] * mwot[3]; + _center = new_center; + return *this; + } + + std::vector coeff = coefficients(); + Affine q( coeff[0], coeff[1]/2, + coeff[1]/2, coeff[2], + 0, 0 ); + + Affine invm = mwot.inverse(); + q = invm * q ; + std::swap(invm[1], invm[2]); + q *= invm; + setCoefficients(q[0], 2*q[1], q[3], 0, 0, -1); + _center = new_center; + + return *this; +} + +Ellipse Ellipse::canonicalForm() const +{ + Ellipse result(*this); + result.makeCanonical(); + return result; +} + +void Ellipse::makeCanonical() +{ + if (_rays[X] == _rays[Y]) { + _angle = 0; + return; + } + + if (_angle < 0) { + _angle += M_PI; + } + if (_angle >= M_PI/2) { + std::swap(_rays[X], _rays[Y]); + _angle -= M_PI/2; + } +} + +Point Ellipse::pointAt(Coord t) const +{ + Point p = Point::polar(t); + p *= unitCircleTransform(); + return p; +} + +Coord Ellipse::valueAt(Coord t, Dim2 d) const +{ + Coord sinrot, cosrot, cost, sint; + sincos(rotationAngle(), sinrot, cosrot); + sincos(t, sint, cost); + + if ( d == X ) { + return ray(X) * cosrot * cost + - ray(Y) * sinrot * sint + + center(X); + } else { + return ray(X) * sinrot * cost + + ray(Y) * cosrot * sint + + center(Y); + } +} + +Coord Ellipse::timeAt(Point const &p) const +{ + // degenerate ellipse is basically a reparametrized line segment + if (ray(X) == 0 || ray(Y) == 0) { + if (ray(X) != 0) { + return asin(Line(axis(X)).timeAt(p)); + } else if (ray(Y) != 0) { + return acos(Line(axis(Y)).timeAt(p)); + } else { + return 0; + } + } + Affine iuct = inverseUnitCircleTransform(); + return Angle(atan2(p * iuct)).radians0(); // return a value in [0, 2pi) +} + +Point Ellipse::unitTangentAt(Coord t) const +{ + Point p = Point::polar(t + M_PI/2); + p *= unitCircleTransform().withoutTranslation(); + p.normalize(); + return p; +} + +bool Ellipse::contains(Point const &p) const +{ + Point tp = p * inverseUnitCircleTransform(); + return tp.length() <= 1; +} + +std::vector Ellipse::intersect(Line const &line) const +{ + + std::vector result; + + if (line.isDegenerate()) return result; + if (ray(X) == 0 || ray(Y) == 0) { + // TODO intersect with line segment. + return result; + } + + // Ax^2 + Bxy + Cy^2 + Dx + Ey + F + Coord A, B, C, D, E, F; + coefficients(A, B, C, D, E, F); + Affine iuct = inverseUnitCircleTransform(); + + // generic case + Coord a, b, c; + line.coefficients(a, b, c); + Point lv = line.vector(); + + if (fabs(lv[X]) > fabs(lv[Y])) { + // y = -a/b x - c/b + Coord q = -a/b; + Coord r = -c/b; + + // substitute that into the ellipse equation, making it quadratic in x + Coord I = A + B*q + C*q*q; // x^2 terms + Coord J = B*r + C*2*q*r + D + E*q; // x^1 terms + Coord K = C*r*r + E*r + F; // x^0 terms + std::vector xs = solve_quadratic(I, J, K); + + for (unsigned i = 0; i < xs.size(); ++i) { + Point p(xs[i], q*xs[i] + r); + result.push_back(ShapeIntersection(atan2(p * iuct), line.timeAt(p), p)); + } + } else { + Coord q = -b/a; + Coord r = -c/a; + + Coord I = A*q*q + B*q + C; + Coord J = A*2*q*r + B*r + D*q + E; + Coord K = A*r*r + D*r + F; + std::vector xs = solve_quadratic(I, J, K); + + for (unsigned i = 0; i < xs.size(); ++i) { + Point p(q*xs[i] + r, xs[i]); + result.push_back(ShapeIntersection(atan2(p * iuct), line.timeAt(p), p)); + } + } + return result; +} + +std::vector Ellipse::intersect(LineSegment const &seg) const +{ + // we simply re-use the procedure for lines and filter out + // results where the line time value is outside of the unit interval. + std::vector result = intersect(Line(seg)); + filter_line_segment_intersections(result); + return result; +} + +std::vector Ellipse::intersect(Ellipse const &other) const +{ + // handle degenerate cases first + if (ray(X) == 0 || ray(Y) == 0) { + + } + // intersection of two ellipses can be solved analytically. + // http://maptools.home.comcast.net/~maptools/BivariateQuadratics.pdf + + Coord A, B, C, D, E, F; + Coord a, b, c, d, e, f; + + // NOTE: the order of coefficients is different to match the convention in the PDF above + // Ax^2 + Bx^2 + Cx + Dy + Exy + F + this->coefficients(A, E, B, C, D, F); + other.coefficients(a, e, b, c, d, f); + + // Assume that Q is the ellipse equation given by uppercase letters + // and R is the equation given by lowercase ones. An intersection exists when + // there is a coefficient mu such that + // mu Q + R = 0 + // + // This can be written in the following way: + // + // | ff cc/2 dd/2 | |1| + // mu Q + R = [1 x y] | cc/2 aa ee/2 | |x| = 0 + // | dd/2 ee/2 bb | |y| + // + // where aa = mu A + a and so on. The determinant can be explicitly written out, + // giving an equation which is cubic in mu and can be solved analytically. + + Coord I, J, K, L; + I = (-E*E*F + 4*A*B*F + C*D*E - A*D*D - B*C*C) / 4; + J = -((E*E - 4*A*B) * f + (2*E*F - C*D) * e + (2*A*D - C*E) * d + + (2*B*C - D*E) * c + (C*C - 4*A*F) * b + (D*D - 4*B*F) * a) / 4; + K = -((e*e - 4*a*b) * F + (2*e*f - c*d) * E + (2*a*d - c*e) * D + + (2*b*c - d*e) * C + (c*c - 4*a*f) * B + (d*d - 4*b*f) * A) / 4; + L = (-e*e*f + 4*a*b*f + c*d*e - a*d*d - b*c*c) / 4; + + std::vector mus = solve_cubic(I, J, K, L); + Coord mu = infinity(); + std::vector result; + + // Now that we have solved for mu, we need to check whether the conic + // determined by mu Q + R is reducible to a product of two lines. If it's not, + // it means that there are no intersections. If it is, the intersections of these + // lines with the original ellipses (if there are any) give the coordinates + // of intersections. + + // Prefer middle root if there are three. + // Out of three possible pairs of lines that go through four points of intersection + // of two ellipses, this corresponds to cross-lines. These intersect the ellipses + // at less shallow angles than the other two options. + if (mus.size() == 3) { + std::swap(mus[1], mus[0]); + } + for (unsigned i = 0; i < mus.size(); ++i) { + Coord aa = mus[i] * A + a; + Coord bb = mus[i] * B + b; + Coord ee = mus[i] * E + e; + Coord delta = ee*ee - 4*aa*bb; + if (delta < 0) continue; + mu = mus[i]; + break; + } + + // if no suitable mu was found, there are no intersections + if (mu == infinity()) return result; + + Coord aa = mu * A + a; + Coord bb = mu * B + b; + Coord cc = mu * C + c; + Coord dd = mu * D + d; + Coord ee = mu * E + e; + Coord ff = mu * F + f; + + unsigned line_num = 0; + Line lines[2]; + + if (aa != 0) { + bb /= aa; cc /= aa; dd /= aa; ee /= aa; /*ff /= aa;*/ + Coord s = (ee + std::sqrt(ee*ee - 4*bb)) / 2; + Coord q = ee - s; + Coord alpha = (dd - cc*q) / (s - q); + Coord beta = cc - alpha; + + line_num = 2; + lines[0] = Line(1, q, alpha); + lines[1] = Line(1, s, beta); + } else if (bb != 0) { + cc /= bb; /*dd /= bb;*/ ee /= bb; ff /= bb; + Coord s = ee; + Coord q = 0; + Coord alpha = cc / ee; + Coord beta = ff * ee / cc; + + line_num = 2; + lines[0] = Line(q, 1, alpha); + lines[1] = Line(s, 1, beta); + } else if (ee != 0) { + line_num = 2; + lines[0] = Line(ee, 0, dd); + lines[1] = Line(0, 1, cc/ee); + } else if (cc != 0 || dd != 0) { + line_num = 1; + lines[0] = Line(cc, dd, ff); + } + + // intersect with the obtained lines and report intersections + for (unsigned li = 0; li < line_num; ++li) { + std::vector as = intersect(lines[li]); + std::vector bs = other.intersect(lines[li]); + + if (!as.empty() && as.size() == bs.size()) { + for (unsigned i = 0; i < as.size(); ++i) { + ShapeIntersection ix(as[i].first, bs[i].first, + middle_point(as[i].point(), bs[i].point())); + result.push_back(ix); + } + } + } + return result; +} + +std::vector Ellipse::intersect(D2 const &b) const +{ + Coord A, B, C, D, E, F; + coefficients(A, B, C, D, E, F); + + Bezier x = A*b[X]*b[X] + B*b[X]*b[Y] + C*b[Y]*b[Y] + D*b[X] + E*b[Y] + F; + std::vector r = x.roots(); + + std::vector result; + for (unsigned i = 0; i < r.size(); ++i) { + Point p = b.valueAt(r[i]); + result.push_back(ShapeIntersection(timeAt(p), r[i], p)); + } + return result; +} + +bool Ellipse::operator==(Ellipse const &other) const +{ + if (_center != other._center) return false; + + Ellipse a = this->canonicalForm(); + Ellipse b = other.canonicalForm(); + + if (a._rays != b._rays) return false; + if (a._angle != b._angle) return false; + + return true; +} + + +bool are_near(Ellipse const &a, Ellipse const &b, Coord precision) +{ + // We want to know whether no point on ellipse a is further than precision + // from the corresponding point on ellipse b. To check this, we compute + // the four extreme points at the end of each ray for each ellipse + // and check whether they are sufficiently close. + + // First, we need to correct the angles on the ellipses, so that they are + // no further than M_PI/4 apart. This can always be done by rotating + // and exchanging axes. + Ellipse ac = a, bc = b; + if (distance(ac.rotationAngle(), bc.rotationAngle()).radians0() >= M_PI/2) { + ac.setRotationAngle(ac.rotationAngle() + M_PI); + } + if (distance(ac.rotationAngle(), bc.rotationAngle()) >= M_PI/4) { + Angle d1 = distance(ac.rotationAngle() + M_PI/2, bc.rotationAngle()); + Angle d2 = distance(ac.rotationAngle() - M_PI/2, bc.rotationAngle()); + Coord adj = d1.radians0() < d2.radians0() ? M_PI/2 : -M_PI/2; + ac.setRotationAngle(ac.rotationAngle() + adj); + ac.setRays(ac.ray(Y), ac.ray(X)); + } + + // Do the actual comparison by computing four points on each ellipse. + Point tps[] = {Point(1,0), Point(0,1), Point(-1,0), Point(0,-1)}; + for (unsigned i = 0; i < 4; ++i) { + if (!are_near(tps[i] * ac.unitCircleTransform(), + tps[i] * bc.unitCircleTransform(), + precision)) + return false; + } + return true; +} + +std::ostream &operator<<(std::ostream &out, Ellipse const &e) +{ + out << "Ellipse(" << e.center() << ", " << e.rays() + << ", " << format_coord_nice(e.rotationAngle()) << ")"; + return out; +} + +} // end namespace Geom + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/2geom/ellipse.h b/src/2geom/ellipse.h new file mode 100644 index 0000000..f6089c9 --- /dev/null +++ b/src/2geom/ellipse.h @@ -0,0 +1,253 @@ +/** @file + * @brief Ellipse shape + *//* + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef LIB2GEOM_SEEN_ELLIPSE_H +#define LIB2GEOM_SEEN_ELLIPSE_H + +#include +#include <2geom/angle.h> +#include <2geom/bezier-curve.h> +#include <2geom/exception.h> +#include <2geom/forward.h> +#include <2geom/line.h> +#include <2geom/transforms.h> + +namespace Geom { + +class EllipticalArc; +class Circle; + +/** @brief Set of points with a constant sum of distances from two foci. + * + * An ellipse can be specified in several ways. Internally, 2Geom uses + * the SVG style representation: center, rays and angle between the +X ray + * and the +X axis. Another popular way is to use an implicit equation, + * which is as follows: + * \f$Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0\f$ + * + * @ingroup Shapes */ +class Ellipse + : boost::multipliable< Ellipse, Translate + , boost::multipliable< Ellipse, Scale + , boost::multipliable< Ellipse, Rotate + , boost::multipliable< Ellipse, Zoom + , boost::multipliable< Ellipse, Affine + , boost::equality_comparable< Ellipse + > > > > > > +{ + Point _center; + Point _rays; + Angle _angle; +public: + Ellipse() {} + Ellipse(Point const &c, Point const &r, Coord angle) + : _center(c) + , _rays(r) + , _angle(angle) + {} + Ellipse(Coord cx, Coord cy, Coord rx, Coord ry, Coord angle) + : _center(cx, cy) + , _rays(rx, ry) + , _angle(angle) + {} + Ellipse(double A, double B, double C, double D, double E, double F) { + setCoefficients(A, B, C, D, E, F); + } + /// Construct ellipse from a circle. + Ellipse(Geom::Circle const &c); + + /// Set center, rays and angle. + void set(Point const &c, Point const &r, Coord angle) { + _center = c; + _rays = r; + _angle = angle; + } + /// Set center, rays and angle as constituent values. + void set(Coord cx, Coord cy, Coord rx, Coord ry, Coord a) { + _center[X] = cx; + _center[Y] = cy; + _rays[X] = rx; + _rays[Y] = ry; + _angle = a; + } + /// Set an ellipse by solving its implicit equation. + void setCoefficients(double A, double B, double C, double D, double E, double F); + /// Set the center. + void setCenter(Point const &p) { _center = p; } + /// Set the center by coordinates. + void setCenter(Coord cx, Coord cy) { _center[X] = cx; _center[Y] = cy; } + /// Set both rays of the ellipse. + void setRays(Point const &p) { _rays = p; } + /// Set both rays of the ellipse as coordinates. + void setRays(Coord x, Coord y) { _rays[X] = x; _rays[Y] = y; } + /// Set one of the rays of the ellipse. + void setRay(Coord r, Dim2 d) { _rays[d] = r; } + /// Set the angle the X ray makes with the +X axis. + void setRotationAngle(Angle a) { _angle = a; } + + Point center() const { return _center; } + Coord center(Dim2 d) const { return _center[d]; } + /// Get both rays as a point. + Point rays() const { return _rays; } + /// Get one ray of the ellipse. + Coord ray(Dim2 d) const { return _rays[d]; } + /// Get the angle the X ray makes with the +X axis. + Angle rotationAngle() const { return _angle; } + /// Get the point corresponding to the +X ray of the ellipse. + Point initialPoint() const; + /// Get the point corresponding to the +X ray of the ellipse. + Point finalPoint() const { return initialPoint(); } + + /** @brief Create an ellipse passing through the specified points + * At least five points have to be specified. */ + void fit(std::vector const& points); + + /** @brief Create an elliptical arc from a section of the ellipse. + * This is mainly useful to determine the flags of the new arc. + * The passed points should lie on the ellipse, otherwise the results + * will be undefined. + * @param ip Initial point of the arc + * @param inner Point in the middle of the arc, used to pick one of two possibilities + * @param fp Final point of the arc + * @return Newly allocated arc, delete when no longer used */ + EllipticalArc *arc(Point const &ip, Point const &inner, Point const &fp); + + /** @brief Return an ellipse with less degrees of freedom. + * The canonical form always has the angle less than \f$\frac{\pi}{2}\f$, + * and zero if the rays are equal (i.e. the ellipse is a circle). */ + Ellipse canonicalForm() const; + void makeCanonical(); + + /** @brief Compute the transform that maps the unit circle to this ellipse. + * Each ellipse can be interpreted as a translated, scaled and rotate unit circle. + * This function returns the transform that maps the unit circle to this ellipse. + * @return Transform from unit circle to the ellipse */ + Affine unitCircleTransform() const; + /** @brief Compute the transform that maps this ellipse to the unit circle. + * This may be a little more precise and/or faster than simply using + * unitCircleTransform().inverse(). An exception will be thrown for + * degenerate ellipses. */ + Affine inverseUnitCircleTransform() const; + + LineSegment majorAxis() const { return ray(X) >= ray(Y) ? axis(X) : axis(Y); } + LineSegment minorAxis() const { return ray(X) < ray(Y) ? axis(X) : axis(Y); } + LineSegment semimajorAxis(int sign = 1) const { + return ray(X) >= ray(Y) ? semiaxis(X, sign) : semiaxis(Y, sign); + } + LineSegment semiminorAxis(int sign = 1) const { + return ray(X) < ray(Y) ? semiaxis(X, sign) : semiaxis(Y, sign); + } + LineSegment axis(Dim2 d) const; + LineSegment semiaxis(Dim2 d, int sign = 1) const; + + /// Get the tight-fitting bounding box of the ellipse. + Rect boundsExact() const; + + /// Get the coefficients of the ellipse's implicit equation. + std::vector coefficients() const; + void coefficients(Coord &A, Coord &B, Coord &C, Coord &D, Coord &E, Coord &F) const; + + /** @brief Evaluate a point on the ellipse. + * The parameter range is \f$[0, 2\pi)\f$; larger and smaller values + * wrap around. */ + Point pointAt(Coord t) const; + /// Evaluate a single coordinate of a point on the ellipse. + Coord valueAt(Coord t, Dim2 d) const; + + /** @brief Find the time value of a point on an ellipse. + * If the point is not on the ellipse, the returned time value will correspond + * to an intersection with a ray from the origin passing through the point + * with the ellipse. Note that this is NOT the nearest point on the ellipse. */ + Coord timeAt(Point const &p) const; + + /// Get the value of the derivative at time t normalized to unit length. + Point unitTangentAt(Coord t) const; + + /// Check whether the ellipse contains the given point. + bool contains(Point const &p) const; + + /// Compute intersections with an infinite line. + std::vector intersect(Line const &line) const; + /// Compute intersections with a line segment. + std::vector intersect(LineSegment const &seg) const; + /// Compute intersections with another ellipse. + std::vector intersect(Ellipse const &other) const; + /// Compute intersections with a 2D Bezier polynomial. + std::vector intersect(D2 const &other) const; + + Ellipse &operator*=(Translate const &t) { + _center *= t; + return *this; + } + Ellipse &operator*=(Scale const &s) { + _center *= s; + _rays *= s; + return *this; + } + Ellipse &operator*=(Zoom const &z) { + _center *= z; + _rays *= z.scale(); + return *this; + } + Ellipse &operator*=(Rotate const &r); + Ellipse &operator*=(Affine const &m); + + /// Compare ellipses for exact equality. + bool operator==(Ellipse const &other) const; +}; + +/** @brief Test whether two ellipses are approximately the same. + * This will check whether no point on ellipse a is further away from + * the corresponding point on ellipse b than precision. + * @relates Ellipse */ +bool are_near(Ellipse const &a, Ellipse const &b, Coord precision = EPSILON); + +/** @brief Outputs ellipse data, useful for debugging. + * @relates Ellipse */ +std::ostream &operator<<(std::ostream &out, Ellipse const &e); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_ELLIPSE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/elliptical-arc-from-sbasis.cpp b/src/2geom/elliptical-arc-from-sbasis.cpp new file mode 100644 index 0000000..c536d89 --- /dev/null +++ b/src/2geom/elliptical-arc-from-sbasis.cpp @@ -0,0 +1,341 @@ +/** @file + * @brief Fitting elliptical arc to SBasis + * + * This file contains the implementation of the function arc_from_sbasis. + *//* + * Copyright 2008 Marco Cecchetti + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/curve.h> +#include <2geom/angle.h> +#include <2geom/utils.h> +#include <2geom/bezier-curve.h> +#include <2geom/elliptical-arc.h> +#include <2geom/sbasis-curve.h> // for non-native methods +#include <2geom/numeric/vector.h> +#include <2geom/numeric/fitting-tool.h> +#include <2geom/numeric/fitting-model.h> +#include + +namespace Geom { + +// forward declaration +namespace detail +{ + struct ellipse_equation; +} + +/* + * make_elliptical_arc + * + * convert a parametric polynomial curve given in symmetric power basis form + * into an EllipticalArc type; in order to be successful the input curve + * has to look like an actual elliptical arc even if a certain tolerance + * is allowed through an ad-hoc parameter. + * The conversion is performed through an interpolation on a certain amount of + * sample points computed on the input curve; + * the interpolation computes the coefficients of the general implicit equation + * of an ellipse (A*X^2 + B*XY + C*Y^2 + D*X + E*Y + F = 0), then from the + * implicit equation we compute the parametric form. + * + */ +class make_elliptical_arc +{ + public: + typedef D2 curve_type; + + /* + * constructor + * + * it doesn't execute the conversion but set the input and output parameters + * + * _ea: the output EllipticalArc that will be generated; + * _curve: the input curve to be converted; + * _total_samples: the amount of sample points to be taken + * on the input curve for performing the conversion + * _tolerance: how much likelihood is required between the input curve + * and the generated elliptical arc; the smaller it is the + * the tolerance the higher it is the likelihood. + */ + make_elliptical_arc( EllipticalArc& _ea, + curve_type const& _curve, + unsigned int _total_samples, + double _tolerance ); + + private: + bool bound_exceeded( unsigned int k, detail::ellipse_equation const & ee, + double e1x, double e1y, double e2 ); + + bool check_bound(double A, double B, double C, double D, double E, double F); + + void fit(); + + bool make_elliptiarc(); + + void print_bound_error(unsigned int k) + { + std::cerr + << "tolerance error" << std::endl + << "at point: " << k << std::endl + << "error value: "<< dist_err << std::endl + << "bound: " << dist_bound << std::endl + << "angle error: " << angle_err + << " (" << angle_tol << ")" << std::endl; + } + + public: + /* + * perform the actual conversion + * return true if the conversion is successful, false on the contrary + */ + bool operator()() + { + // initialize the reference + const NL::Vector & coeff = fitter.result(); + fit(); + if ( !check_bound(1, coeff[0], coeff[1], coeff[2], coeff[3], coeff[4]) ) + return false; + if ( !(make_elliptiarc()) ) return false; + return true; + } + + private: + EllipticalArc& ea; // output elliptical arc + const curve_type & curve; // input curve + Piecewise > dcurve; // derivative of the input curve + NL::LFMEllipse model; // model used for fitting + // perform the actual fitting task + NL::least_squeares_fitter fitter; + // tolerance: the user-defined tolerance parameter; + // tol_at_extr: the tolerance at end-points automatically computed + // on the value of "tolerance", and usually more strict; + // tol_at_center: tolerance at the center of the ellipse + // angle_tol: tolerance for the angle btw the input curve tangent + // versor and the ellipse normal versor at the sample points + double tolerance, tol_at_extr, tol_at_center, angle_tol; + Point initial_point, final_point; // initial and final end-points + unsigned int N; // total samples + unsigned int last; // N-1 + double partitions; // N-1 + std::vector p; // sample points + double dist_err, dist_bound, angle_err; +}; + +namespace detail +{ +/* + * ellipse_equation + * + * this is an helper struct, it provides two routines: + * the first one evaluates the implicit form of an ellipse on a given point + * the second one computes the normal versor at a given point of an ellipse + * in implicit form + */ +struct ellipse_equation +{ + ellipse_equation(double a, double b, double c, double d, double e, double f) + : A(a), B(b), C(c), D(d), E(e), F(f) + { + } + + double operator()(double x, double y) const + { + // A * x * x + B * x * y + C * y * y + D * x + E * y + F; + return (A * x + B * y + D) * x + (C * y + E) * y + F; + } + + double operator()(Point const& p) const + { + return (*this)(p[X], p[Y]); + } + + Point normal(double x, double y) const + { + Point n( 2 * A * x + B * y + D, 2 * C * y + B * x + E ); + return unit_vector(n); + } + + Point normal(Point const& p) const + { + return normal(p[X], p[Y]); + } + + double A, B, C, D, E, F; +}; + +} // end namespace detail + +make_elliptical_arc:: +make_elliptical_arc( EllipticalArc& _ea, + curve_type const& _curve, + unsigned int _total_samples, + double _tolerance ) + : ea(_ea), curve(_curve), + dcurve( unitVector(derivative(curve)) ), + model(), fitter(model, _total_samples), + tolerance(_tolerance), tol_at_extr(tolerance/2), + tol_at_center(0.1), angle_tol(0.1), + initial_point(curve.at0()), final_point(curve.at1()), + N(_total_samples), last(N-1), partitions(N-1), p(N) +{ +} + +/* + * check that the coefficients computed by the fit method satisfy + * the tolerance parameters at the k-th sample point + */ +bool +make_elliptical_arc:: +bound_exceeded( unsigned int k, detail::ellipse_equation const & ee, + double e1x, double e1y, double e2 ) +{ + dist_err = std::fabs( ee(p[k]) ); + dist_bound = std::fabs( e1x * p[k][X] + e1y * p[k][Y] + e2 ); + // check that the angle btw the tangent versor to the input curve + // and the normal versor of the elliptical arc, both evaluate + // at the k-th sample point, are really othogonal + angle_err = std::fabs( dot( dcurve(k/partitions), ee.normal(p[k]) ) ); + //angle_err *= angle_err; + return ( dist_err > dist_bound || angle_err > angle_tol ); +} + +/* + * check that the coefficients computed by the fit method satisfy + * the tolerance parameters at each sample point + */ +bool +make_elliptical_arc:: +check_bound(double A, double B, double C, double D, double E, double F) +{ + detail::ellipse_equation ee(A, B, C, D, E, F); + + // check error magnitude at the end-points + double e1x = (2*A + B) * tol_at_extr; + double e1y = (B + 2*C) * tol_at_extr; + double e2 = ((D + E) + (A + B + C) * tol_at_extr) * tol_at_extr; + if (bound_exceeded(0, ee, e1x, e1y, e2)) + { + print_bound_error(0); + return false; + } + if (bound_exceeded(0, ee, e1x, e1y, e2)) + { + print_bound_error(last); + return false; + } + + // e1x = derivative((ee(x,y), x) | x->tolerance, y->tolerance + e1x = (2*A + B) * tolerance; + // e1y = derivative((ee(x,y), y) | x->tolerance, y->tolerance + e1y = (B + 2*C) * tolerance; + // e2 = ee(tolerance, tolerance) - F; + e2 = ((D + E) + (A + B + C) * tolerance) * tolerance; +// std::cerr << "e1x = " << e1x << std::endl; +// std::cerr << "e1y = " << e1y << std::endl; +// std::cerr << "e2 = " << e2 << std::endl; + + // check error magnitude at sample points + for ( unsigned int k = 1; k < last; ++k ) + { + if ( bound_exceeded(k, ee, e1x, e1y, e2) ) + { + print_bound_error(k); + return false; + } + } + + return true; +} + +/* + * fit + * + * supply the samples to the fitter and compute + * the ellipse implicit equation coefficients + */ +void make_elliptical_arc::fit() +{ + for (unsigned int k = 0; k < N; ++k) + { + p[k] = curve( k / partitions ); + fitter.append(p[k]); + } + fitter.update(); + + NL::Vector z(N, 0.0); + fitter.result(z); +} + +bool make_elliptical_arc::make_elliptiarc() +{ + const NL::Vector & coeff = fitter.result(); + Ellipse e; + try + { + e.setCoefficients(1, coeff[0], coeff[1], coeff[2], coeff[3], coeff[4]); + } + catch(LogicalError const &exc) + { + return false; + } + + Point inner_point = curve(0.5); + + std::unique_ptr arc( e.arc(initial_point, inner_point, final_point) ); + ea = *arc; + + if ( !are_near( e.center(), + ea.center(), + tol_at_center * std::min(e.ray(X),e.ray(Y)) + ) + ) + { + return false; + } + return true; +} + + + +bool arc_from_sbasis(EllipticalArc &ea, D2 const &in, + double tolerance, unsigned num_samples) +{ + make_elliptical_arc convert(ea, in, num_samples, tolerance); + return convert(); +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/elliptical-arc.cpp b/src/2geom/elliptical-arc.cpp new file mode 100644 index 0000000..3603f80 --- /dev/null +++ b/src/2geom/elliptical-arc.cpp @@ -0,0 +1,946 @@ +/* + * SVG Elliptical Arc Class + * + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * Copyright 2008-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include +#include + +#include <2geom/bezier-curve.h> +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/path-sink.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/transforms.h> +#include <2geom/utils.h> + +#include <2geom/numeric/vector.h> +#include <2geom/numeric/fitting-tool.h> +#include <2geom/numeric/fitting-model.h> + +namespace Geom +{ + +/** + * @class EllipticalArc + * @brief Elliptical arc curve + * + * Elliptical arc is a curve taking the shape of a section of an ellipse. + * + * The arc function has two forms: the regular one, mapping the unit interval to points + * on 2D plane (the linear domain), and a second form that maps some interval + * \f$A \subseteq [0,2\pi)\f$ to the same points (the angular domain). The interval \f$A\f$ + * determines which part of the ellipse forms the arc. The arc is said to contain an angle + * if its angular domain includes that angle (and therefore it is defined for that angle). + * + * The angular domain considers each ellipse to be + * a rotated, scaled and translated unit circle: 0 corresponds to \f$(1,0)\f$ on the unit circle, + * \f$\pi/2\f$ corresponds to \f$(0,1)\f$, \f$\pi\f$ to \f$(-1,0)\f$ and \f$3\pi/2\f$ + * to \f$(0,-1)\f$. After the angle is mapped to a point from a unit circle, the point is + * transformed using a matrix of this form + * \f[ M = \left[ \begin{array}{ccc} + r_X \cos(\theta) & -r_Y \sin(\theta) & 0 \\ + r_X \sin(\theta) & r_Y \cos(\theta) & 0 \\ + c_X & c_Y & 1 \end{array} \right] \f] + * where \f$r_X, r_Y\f$ are the X and Y rays of the ellipse, \f$\theta\f$ is its angle of rotation, + * and \f$c_X, c_Y\f$ the coordinates of the ellipse's center - thus mapping the angle + * to some point on the ellipse. Note that for example the point at angluar coordinate 0, + * the center and the point at angular coordinate \f$\pi/4\f$ do not necessarily + * create an angle of \f$\pi/4\f$ radians; it is only the case if both axes of the ellipse + * are of the same length (i.e. it is a circle). + * + * @image html ellipse-angular-coordinates.png "An illustration of the angular domain" + * + * Each arc is defined by five variables: The initial and final point, the ellipse's rays, + * and the ellipse's rotation. Each set of those parameters corresponds to four different arcs, + * with two of them larger than half an ellipse and two of them turning clockwise while traveling + * from initial to final point. The two flags disambiguate between them: "large arc flag" selects + * the bigger arc, while the "sweep flag" selects the arc going in the direction of positive + * angles. Angles always increase when going from the +X axis in the direction of the +Y axis, + * so if Y grows downwards, this means clockwise. + * + * @image html elliptical-arc-flags.png "Meaning of arc flags (Y grows downwards)" + * + * @ingroup Curves + */ + + +/** @brief Compute bounds of an elliptical arc. + * The bounds computation works as follows. The extreme X and Y points + * are either the endpoints or local minima / maxima of the ellipse. + * We already have endpoints, and we can find the local extremes + * by computing a partial derivative with respect to the angle + * and equating that to zero: + * \f{align*}{ + x &= r_x \cos \varphi \cos \theta - r_y \sin \varphi \sin \theta + c_x \\ + \frac{\partial x}{\partial \theta} &= -r_x \cos \varphi \sin \theta - r_y \sin \varphi \cos \theta = 0 \\ + \frac{\sin \theta}{\cos \theta} &= \tan\theta = -\frac{r_y \sin \varphi}{r_x \cos \varphi} \\ + \theta &= \tan^{-1} \frac{-r_y \sin \varphi}{r_x \cos \varphi} + \f} + * The local extremes correspond to two angles separated by \f$\pi\f$. + * Once we compute these angles, we check whether they belong to the arc, + * and if they do, we evaluate the ellipse at these angles. + * The bounding box of the arc is equal to the bounding box of the endpoints + * and the local extrema that belong to the arc. + */ +Rect EllipticalArc::boundsExact() const +{ + if (isChord()) { + return chord().boundsExact(); + } + + Coord coord[2][4] = { + { _initial_point[X], _final_point[X], 0, 0 }, + { _initial_point[Y], _final_point[Y], 0, 0 } + }; + int ncoord[2] = { 2, 2 }; + + Angle extremes[2][2]; + double sinrot, cosrot; + sincos(rotationAngle(), sinrot, cosrot); + + extremes[X][0] = std::atan2( -ray(Y) * sinrot, ray(X) * cosrot ); + extremes[X][1] = extremes[X][0] + M_PI; + extremes[Y][0] = std::atan2( ray(Y) * cosrot, ray(X) * sinrot ); + extremes[Y][1] = extremes[Y][0] + M_PI; + + for (unsigned d = 0; d < 2; ++d) { + for (unsigned i = 0; i < 2; ++i) { + if (containsAngle(extremes[d][i])) { + coord[d][ncoord[d]++] = valueAtAngle(extremes[d][i], d ? Y : X); + } + } + } + + Interval xival = Interval::from_range(coord[X], coord[X] + ncoord[X]); + Interval yival = Interval::from_range(coord[Y], coord[Y] + ncoord[Y]); + Rect result(xival, yival); + return result; +} + + +Point EllipticalArc::pointAtAngle(Coord t) const +{ + Point ret = _ellipse.pointAt(t); + return ret; +} + +Coord EllipticalArc::valueAtAngle(Coord t, Dim2 d) const +{ + return _ellipse.valueAt(t, d); +} + +std::vector EllipticalArc::roots(Coord v, Dim2 d) const +{ + std::vector sol; + + if (isChord()) { + sol = chord().roots(v, d); + return sol; + } + + Interval unit_interval(0, 1); + + double rotx, roty; + if (d == X) { + sincos(rotationAngle(), roty, rotx); + roty = -roty; + } else { + sincos(rotationAngle(), rotx, roty); + } + + double rxrotx = ray(X) * rotx; + double c_v = center(d) - v; + + double a = -rxrotx + c_v; + double b = ray(Y) * roty; + double c = rxrotx + c_v; + //std::cerr << "a = " << a << std::endl; + //std::cerr << "b = " << b << std::endl; + //std::cerr << "c = " << c << std::endl; + + if (a == 0) + { + sol.push_back(M_PI); + if (b != 0) + { + double s = 2 * std::atan(-c/(2*b)); + if ( s < 0 ) s += 2*M_PI; + sol.push_back(s); + } + } + else + { + double delta = b * b - a * c; + //std::cerr << "delta = " << delta << std::endl; + if (delta == 0) { + double s = 2 * std::atan(-b/a); + if ( s < 0 ) s += 2*M_PI; + sol.push_back(s); + } + else if ( delta > 0 ) + { + double sq = std::sqrt(delta); + double s = 2 * std::atan( (-b - sq) / a ); + if ( s < 0 ) s += 2*M_PI; + sol.push_back(s); + s = 2 * std::atan( (-b + sq) / a ); + if ( s < 0 ) s += 2*M_PI; + sol.push_back(s); + } + } + + std::vector arc_sol; + for (unsigned int i = 0; i < sol.size(); ++i ) { + //std::cerr << "s = " << deg_from_rad(sol[i]); + sol[i] = timeAtAngle(sol[i]); + //std::cerr << " -> t: " << sol[i] << std::endl; + if (unit_interval.contains(sol[i])) { + arc_sol.push_back(sol[i]); + } + } + return arc_sol; +} + + +// D(E(t,C),t) = E(t+PI/2,O), where C is the ellipse center +// the derivative doesn't rotate the ellipse but there is a translation +// of the parameter t by an angle of PI/2 so the ellipse points are shifted +// of such an angle in the cw direction +Curve *EllipticalArc::derivative() const +{ + if (isChord()) { + return chord().derivative(); + } + + EllipticalArc *result = static_cast(duplicate()); + result->_ellipse.setCenter(0, 0); + result->_angles.setInitial(result->_angles.initialAngle() + M_PI/2); + result->_angles.setFinal(result->_angles.finalAngle() + M_PI/2); + result->_initial_point = result->pointAtAngle( result->initialAngle() ); + result->_final_point = result->pointAtAngle( result->finalAngle() ); + return result; +} + + +std::vector +EllipticalArc::pointAndDerivatives(Coord t, unsigned int n) const +{ + if (isChord()) { + return chord().pointAndDerivatives(t, n); + } + + unsigned int nn = n+1; // nn represents the size of the result vector. + std::vector result; + result.reserve(nn); + double angle = angleAt(t); + std::unique_ptr ea( static_cast(duplicate()) ); + ea->_ellipse.setCenter(0, 0); + unsigned int m = std::min(nn, 4u); + for ( unsigned int i = 0; i < m; ++i ) + { + result.push_back( ea->pointAtAngle(angle) ); + angle += (sweep() ? M_PI/2 : -M_PI/2); + if ( !(angle < 2*M_PI) ) angle -= 2*M_PI; + } + m = nn / 4; + for ( unsigned int i = 1; i < m; ++i ) + { + for ( unsigned int j = 0; j < 4; ++j ) + result.push_back( result[j] ); + } + m = nn - 4 * m; + for ( unsigned int i = 0; i < m; ++i ) + { + result.push_back( result[i] ); + } + if ( !result.empty() ) // nn != 0 + result[0] = pointAtAngle(angle); + return result; +} + +Point EllipticalArc::pointAt(Coord t) const +{ + if (isChord()) return chord().pointAt(t); + return _ellipse.pointAt(angleAt(t)); +} + +Coord EllipticalArc::valueAt(Coord t, Dim2 d) const +{ + if (isChord()) return chord().valueAt(t, d); + return valueAtAngle(angleAt(t), d); +} + +Curve* EllipticalArc::portion(double f, double t) const +{ + // fix input arguments + if (f < 0) f = 0; + if (f > 1) f = 1; + if (t < 0) t = 0; + if (t > 1) t = 1; + + if (f == t) { + EllipticalArc *arc = new EllipticalArc(); + arc->_initial_point = arc->_final_point = pointAt(f); + return arc; + } + + EllipticalArc *arc = static_cast(duplicate()); + arc->_initial_point = pointAt(f); + arc->_final_point = pointAt(t); + arc->_angles.setAngles(angleAt(f), angleAt(t)); + if (f > t) arc->_angles.setSweep(!sweep()); + if ( _large_arc && fabs(angularExtent() * (t-f)) <= M_PI) { + arc->_large_arc = false; + } + return arc; +} + +// the arc is the same but traversed in the opposite direction +Curve *EllipticalArc::reverse() const +{ + using std::swap; + EllipticalArc *rarc = static_cast(duplicate()); + rarc->_angles.reverse(); + swap(rarc->_initial_point, rarc->_final_point); + return rarc; +} + +#ifdef HAVE_GSL // GSL is required for function "solve_reals" +std::vector EllipticalArc::allNearestTimes( Point const& p, double from, double to ) const +{ + std::vector result; + + if ( from > to ) std::swap(from, to); + if ( from < 0 || to > 1 ) + { + THROW_RANGEERROR("[from,to] interval out of range"); + } + + if ( ( are_near(ray(X), 0) && are_near(ray(Y), 0) ) || are_near(from, to) ) + { + result.push_back(from); + return result; + } + else if ( are_near(ray(X), 0) || are_near(ray(Y), 0) ) + { + LineSegment seg(pointAt(from), pointAt(to)); + Point np = seg.pointAt( seg.nearestTime(p) ); + if ( are_near(ray(Y), 0) ) + { + if ( are_near(rotationAngle(), M_PI/2) + || are_near(rotationAngle(), 3*M_PI/2) ) + { + result = roots(np[Y], Y); + } + else + { + result = roots(np[X], X); + } + } + else + { + if ( are_near(rotationAngle(), M_PI/2) + || are_near(rotationAngle(), 3*M_PI/2) ) + { + result = roots(np[X], X); + } + else + { + result = roots(np[Y], Y); + } + } + return result; + } + else if ( are_near(ray(X), ray(Y)) ) + { + Point r = p - center(); + if ( are_near(r, Point(0,0)) ) + { + THROW_INFINITESOLUTIONS(0); + } + // TODO: implement case r != 0 +// Point np = ray(X) * unit_vector(r); +// std::vector solX = roots(np[X],X); +// std::vector solY = roots(np[Y],Y); +// double t; +// if ( are_near(solX[0], solY[0]) || are_near(solX[0], solY[1])) +// { +// t = solX[0]; +// } +// else +// { +// t = solX[1]; +// } +// if ( !(t < from || t > to) ) +// { +// result.push_back(t); +// } +// else +// { +// +// } + } + + // solve the equation == 0 + // that provides min and max distance points + // on the ellipse E wrt the point p + // after the substitutions: + // cos(t) = (1 - s^2) / (1 + s^2) + // sin(t) = 2t / (1 + s^2) + // where s = tan(t/2) + // we get a 4th degree equation in s + /* + * ry s^4 ((-cy + py) Cos[Phi] + (cx - px) Sin[Phi]) + + * ry ((cy - py) Cos[Phi] + (-cx + px) Sin[Phi]) + + * 2 s^3 (rx^2 - ry^2 + (-cx + px) rx Cos[Phi] + (-cy + py) rx Sin[Phi]) + + * 2 s (-rx^2 + ry^2 + (-cx + px) rx Cos[Phi] + (-cy + py) rx Sin[Phi]) + */ + + Point p_c = p - center(); + double rx2_ry2 = (ray(X) - ray(Y)) * (ray(X) + ray(Y)); + double sinrot, cosrot; + sincos(rotationAngle(), sinrot, cosrot); + double expr1 = ray(X) * (p_c[X] * cosrot + p_c[Y] * sinrot); + Poly coeff; + coeff.resize(5); + coeff[4] = ray(Y) * ( p_c[Y] * cosrot - p_c[X] * sinrot ); + coeff[3] = 2 * ( rx2_ry2 + expr1 ); + coeff[2] = 0; + coeff[1] = 2 * ( -rx2_ry2 + expr1 ); + coeff[0] = -coeff[4]; + +// for ( unsigned int i = 0; i < 5; ++i ) +// std::cerr << "c[" << i << "] = " << coeff[i] << std::endl; + + std::vector real_sol; + // gsl_poly_complex_solve raises an error + // if the leading coefficient is zero + if ( are_near(coeff[4], 0) ) + { + real_sol.push_back(0); + if ( !are_near(coeff[3], 0) ) + { + double sq = -coeff[1] / coeff[3]; + if ( sq > 0 ) + { + double s = std::sqrt(sq); + real_sol.push_back(s); + real_sol.push_back(-s); + } + } + } + else + { + real_sol = solve_reals(coeff); + } + + for ( unsigned int i = 0; i < real_sol.size(); ++i ) + { + real_sol[i] = 2 * std::atan(real_sol[i]); + if ( real_sol[i] < 0 ) real_sol[i] += 2*M_PI; + } + // when s -> Infinity then -> 0 iff coeff[4] == 0 + // so we add M_PI to the solutions being lim arctan(s) = PI when s->Infinity + if ( (real_sol.size() % 2) != 0 ) + { + real_sol.push_back(M_PI); + } + + double mindistsq1 = std::numeric_limits::max(); + double mindistsq2 = std::numeric_limits::max(); + double dsq = 0; + unsigned int mi1 = 0, mi2 = 0; + for ( unsigned int i = 0; i < real_sol.size(); ++i ) + { + dsq = distanceSq(p, pointAtAngle(real_sol[i])); + if ( mindistsq1 > dsq ) + { + mindistsq2 = mindistsq1; + mi2 = mi1; + mindistsq1 = dsq; + mi1 = i; + } + else if ( mindistsq2 > dsq ) + { + mindistsq2 = dsq; + mi2 = i; + } + } + + double t = timeAtAngle(real_sol[mi1]); + if ( !(t < from || t > to) ) + { + result.push_back(t); + } + + bool second_sol = false; + t = timeAtAngle(real_sol[mi2]); + if ( real_sol.size() == 4 && !(t < from || t > to) ) + { + if ( result.empty() || are_near(mindistsq1, mindistsq2) ) + { + result.push_back(t); + second_sol = true; + } + } + + // we need to test extreme points too + double dsq1 = distanceSq(p, pointAt(from)); + double dsq2 = distanceSq(p, pointAt(to)); + if ( second_sol ) + { + if ( mindistsq2 > dsq1 ) + { + result.clear(); + result.push_back(from); + mindistsq2 = dsq1; + } + else if ( are_near(mindistsq2, dsq) ) + { + result.push_back(from); + } + if ( mindistsq2 > dsq2 ) + { + result.clear(); + result.push_back(to); + } + else if ( are_near(mindistsq2, dsq2) ) + { + result.push_back(to); + } + + } + else + { + if ( result.empty() ) + { + if ( are_near(dsq1, dsq2) ) + { + result.push_back(from); + result.push_back(to); + } + else if ( dsq2 > dsq1 ) + { + result.push_back(from); + } + else + { + result.push_back(to); + } + } + } + + return result; +} +#endif + + +void EllipticalArc::_filterIntersections(std::vector &xs, bool is_first) const +{ + Interval unit(0, 1); + std::vector::reverse_iterator i = xs.rbegin(), last = xs.rend(); + while (i != last) { + Coord &t = is_first ? i->first : i->second; + assert(are_near(_ellipse.pointAt(t), i->point(), 1e-5)); + t = timeAtAngle(t); + if (!unit.contains(t)) { + xs.erase((++i).base()); + continue; + } else { + assert(are_near(pointAt(t), i->point(), 1e-5)); + ++i; + } + } +} + +std::vector EllipticalArc::intersect(Curve const &other, Coord eps) const +{ + if (isLineSegment()) { + LineSegment ls(_initial_point, _final_point); + return ls.intersect(other, eps); + } + + std::vector result; + + if (other.isLineSegment()) { + LineSegment ls(other.initialPoint(), other.finalPoint()); + result = _ellipse.intersect(ls); + _filterIntersections(result, true); + return result; + } + + BezierCurve const *bez = dynamic_cast(&other); + if (bez) { + result = _ellipse.intersect(bez->fragment()); + _filterIntersections(result, true); + return result; + } + + EllipticalArc const *arc = dynamic_cast(&other); + if (arc) { + result = _ellipse.intersect(arc->_ellipse); + _filterIntersections(result, true); + arc->_filterIntersections(result, false); + return result; + } + + // in case someone wants to make a custom curve type + result = other.intersect(*this, eps); + transpose_in_place(result); + return result; +} + + +void EllipticalArc::_updateCenterAndAngles() +{ + // See: http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + Point d = initialPoint() - finalPoint(); + Point mid = middle_point(initialPoint(), finalPoint()); + + // if ip = sp, the arc contains no other points + if (initialPoint() == finalPoint()) { + _ellipse = Ellipse(); + _ellipse.setCenter(initialPoint()); + _angles = AngleInterval(); + _large_arc = false; + return; + } + + // rays should be positive + _ellipse.setRays(std::fabs(ray(X)), std::fabs(ray(Y))); + + if (isChord()) { + _ellipse.setRays(L2(d) / 2, 0); + _ellipse.setRotationAngle(atan2(d)); + _ellipse.setCenter(mid); + _angles.setAngles(0, M_PI); + _angles.setSweep(false); + _large_arc = false; + return; + } + + Rotate rot(rotationAngle()); // the matrix in F.6.5.3 + Rotate invrot = rot.inverse(); // the matrix in F.6.5.1 + + Point r = rays(); + Point p = (initialPoint() - mid) * invrot; // x', y' in F.6.5.1 + Point c(0,0); // cx', cy' in F.6.5.2 + + // Correct out-of-range radii + Coord lambda = hypot(p[X]/r[X], p[Y]/r[Y]); + if (lambda > 1) { + r *= lambda; + _ellipse.setRays(r); + _ellipse.setCenter(mid); + } else { + // evaluate F.6.5.2 + Coord rxry = r[X]*r[X] * r[Y]*r[Y]; + Coord pxry = p[X]*p[X] * r[Y]*r[Y]; + Coord rxpy = r[X]*r[X] * p[Y]*p[Y]; + Coord rad = (rxry - pxry - rxpy)/(rxpy + pxry); + // normally rad should never be negative, but numerical inaccuracy may cause this + if (rad > 0) { + rad = std::sqrt(rad); + if (sweep() == _large_arc) { + rad = -rad; + } + c = rad * Point(r[X]*p[Y]/r[Y], -r[Y]*p[X]/r[X]); + _ellipse.setCenter(c * rot + mid); + } else { + _ellipse.setCenter(mid); + } + } + + // Compute start and end angles. + // If the ellipse was enlarged, c will be zero - this is correct. + Point sp((p[X] - c[X]) / r[X], (p[Y] - c[Y]) / r[Y]); + Point ep((-p[X] - c[X]) / r[X], (-p[Y] - c[Y]) / r[Y]); + Point v(1, 0); + + _angles.setInitial(angle_between(v, sp)); + _angles.setFinal(angle_between(v, ep)); + + /*double sweep_angle = angle_between(sp, ep); + if (!sweep() && sweep_angle > 0) sweep_angle -= 2*M_PI; + if (sweep() && sweep_angle < 0) sweep_angle += 2*M_PI;*/ +} + +D2 EllipticalArc::toSBasis() const +{ + if (isChord()) { + return chord().toSBasis(); + } + + D2 arc; + // the interval of parametrization has to be [0,1] + Coord et = initialAngle().radians() + sweepAngle(); + Linear param(initialAngle().radians(), et); + Coord cosrot, sinrot; + sincos(rotationAngle(), sinrot, cosrot); + + // order = 4 seems to be enough to get a perfect looking elliptical arc + SBasis arc_x = ray(X) * cos(param,4); + SBasis arc_y = ray(Y) * sin(param,4); + arc[0] = arc_x * cosrot - arc_y * sinrot + Linear(center(X), center(X)); + arc[1] = arc_x * sinrot + arc_y * cosrot + Linear(center(Y), center(Y)); + + // ensure that endpoints remain exact + for ( int d = 0 ; d < 2 ; d++ ) { + arc[d][0][0] = initialPoint()[d]; + arc[d][0][1] = finalPoint()[d]; + } + + return arc; +} + +// All operations that do not contain skew can be evaluated +// without passing through the implicit form of the ellipse, +// which preserves precision. + +void EllipticalArc::operator*=(Translate const &tr) +{ + _initial_point *= tr; + _final_point *= tr; + _ellipse *= tr; +} + +void EllipticalArc::operator*=(Scale const &s) +{ + _initial_point *= s; + _final_point *= s; + _ellipse *= s; +} + +void EllipticalArc::operator*=(Rotate const &r) +{ + _initial_point *= r; + _final_point *= r; + _ellipse *= r; +} + +void EllipticalArc::operator*=(Zoom const &z) +{ + _initial_point *= z; + _final_point *= z; + _ellipse *= z; +} + +void EllipticalArc::operator*=(Affine const& m) +{ + if (isChord()) { + _initial_point *= m; + _final_point *= m; + _ellipse.setCenter(middle_point(_initial_point, _final_point)); + _ellipse.setRays(0, 0); + _ellipse.setRotationAngle(0); + return; + } + + _initial_point *= m; + _final_point *= m; + _ellipse *= m; + if (m.det() < 0) { + _angles.setSweep(!sweep()); + } + + // ellipse transformation does not preserve its functional form, + // i.e. e.pointAt(0.5)*m and (e*m).pointAt(0.5) can be different. + // We need to recompute start / end angles. + _angles.setInitial(_ellipse.timeAt(_initial_point)); + _angles.setFinal(_ellipse.timeAt(_final_point)); +} + +bool EllipticalArc::operator==(Curve const &c) const +{ + EllipticalArc const *other = dynamic_cast(&c); + if (!other) return false; + if (_initial_point != other->_initial_point) return false; + if (_final_point != other->_final_point) return false; + // TODO: all arcs with ellipse rays which are too small + // and fall back to a line should probably be equal + if (rays() != other->rays()) return false; + if (rotationAngle() != other->rotationAngle()) return false; + if (_large_arc != other->_large_arc) return false; + if (sweep() != other->sweep()) return false; + return true; +} + +bool EllipticalArc::isNear(Curve const &c, Coord precision) const +{ + EllipticalArc const *other = dynamic_cast(&c); + if (!other) { + if (isChord()) { + return c.isNear(chord(), precision); + } + return false; + } + + if (!are_near(_initial_point, other->_initial_point, precision)) return false; + if (!are_near(_final_point, other->_final_point, precision)) return false; + if (isChord() && other->isChord()) return true; + + if (sweep() != other->sweep()) return false; + if (!are_near(_ellipse, other->_ellipse, precision)) return false; + return true; +} + +void EllipticalArc::feed(PathSink &sink, bool moveto_initial) const +{ + if (moveto_initial) { + sink.moveTo(_initial_point); + } + sink.arcTo(ray(X), ray(Y), rotationAngle(), _large_arc, sweep(), _final_point); +} + +int EllipticalArc::winding(Point const &p) const +{ + using std::swap; + + double sinrot, cosrot; + sincos(rotationAngle(), sinrot, cosrot); + + Angle ymin_a = std::atan2( ray(Y) * cosrot, ray(X) * sinrot ); + Angle ymax_a = ymin_a + M_PI; + + Point ymin = pointAtAngle(ymin_a); + Point ymax = pointAtAngle(ymax_a); + if (ymin[Y] > ymax[Y]) { + swap(ymin, ymax); + swap(ymin_a, ymax_a); + } + + Interval yspan(ymin[Y], ymax[Y]); + if (!yspan.lowerContains(p[Y])) return 0; + + bool left = cross(ymax - ymin, p - ymin) > 0; + bool inside = _ellipse.contains(p); + bool includes_ymin = _angles.contains(ymin_a); + bool includes_ymax = _angles.contains(ymax_a); + + AngleInterval rarc(ymin_a, ymax_a, true), + larc(ymax_a, ymin_a, true); + + // we'll compute the result for an arc in the direction of increasing angles + // and then negate if necessary + Angle ia = initialAngle(), fa = finalAngle(); + Point ip = _initial_point, fp = _final_point; + if (!sweep()) { + swap(ia, fa); + swap(ip, fp); + } + + bool initial_left = larc.contains(ia); + bool initial_right = !initial_left; // rarc.contains(ia); + bool final_left = larc.contains(fa); + bool final_right = !final_left; // rarc.contains(fa); + + int result = 0; + if (inside || left) { + if (includes_ymin && final_right) { + Interval ival(ymin[Y], fp[Y]); + if (ival.lowerContains(p[Y])) { + ++result; + } + } + if (initial_right && final_right && !largeArc()) { + Interval ival(ip[Y], fp[Y]); + if (ival.lowerContains(p[Y])) { + ++result; + } + } + if (initial_right && includes_ymax) { + Interval ival(ip[Y], ymax[Y]); + if (ival.lowerContains(p[Y])) { + ++result; + } + } + if (!initial_right && !final_right && includes_ymin && includes_ymax) { + Interval ival(ymax[Y], ymin[Y]); + if (ival.lowerContains(p[Y])) { + ++result; + } + } + } + if (left && !inside) { + if (includes_ymin && initial_left) { + Interval ival(ymin[Y], ip[Y]); + if (ival.lowerContains(p[Y])) { + --result; + } + } + if (initial_left && final_left && !largeArc()) { + Interval ival(ip[Y], fp[Y]); + if (ival.lowerContains(p[Y])) { + --result; + } + } + if (final_left && includes_ymax) { + Interval ival(fp[Y], ymax[Y]); + if (ival.lowerContains(p[Y])) { + --result; + } + } + if (!initial_left && !final_left && includes_ymin && includes_ymax) { + Interval ival(ymax[Y], ymin[Y]); + if (ival.lowerContains(p[Y])) { + --result; + } + } + } + return sweep() ? result : -result; +} + +std::ostream &operator<<(std::ostream &out, EllipticalArc const &ea) +{ + out << "EllipticalArc(" + << ea.initialPoint() << ", " + << format_coord_nice(ea.ray(X)) << ", " << format_coord_nice(ea.ray(Y)) << ", " + << format_coord_nice(ea.rotationAngle()) << ", " + << "large_arc=" << (ea.largeArc() ? "true" : "false") << ", " + << "sweep=" << (ea.sweep() ? "true" : "false") << ", " + << ea.finalPoint() << ")"; + return out; +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/2geom/elliptical-arc.h b/src/2geom/elliptical-arc.h new file mode 100644 index 0000000..d0f9db9 --- /dev/null +++ b/src/2geom/elliptical-arc.h @@ -0,0 +1,341 @@ +/** + * \file + * \brief Elliptical arc curve + * + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_ELLIPTICAL_ARC_H +#define LIB2GEOM_SEEN_ELLIPTICAL_ARC_H + +#include +#include <2geom/affine.h> +#include <2geom/angle.h> +#include <2geom/bezier-curve.h> +#include <2geom/curve.h> +#include <2geom/ellipse.h> +#include <2geom/sbasis-curve.h> // for non-native methods +#include <2geom/utils.h> + +namespace Geom +{ + +class EllipticalArc : public Curve +{ +public: + /** @brief Creates an arc with all variables set to zero. */ + EllipticalArc() + : _initial_point(0,0) + , _final_point(0,0) + , _large_arc(false) + {} + /** @brief Create a new elliptical arc. + * @param ip Initial point of the arc + * @param r Rays of the ellipse as a point + * @param rot Angle of rotation of the X axis of the ellipse in radians + * @param large If true, the large arc is chosen (always >= 180 degrees), otherwise + * the smaller arc is chosen + * @param sweep If true, the clockwise arc is chosen, otherwise the counter-clockwise + * arc is chosen + * @param fp Final point of the arc */ + EllipticalArc( Point const &ip, Point const &r, + Coord rot_angle, bool large_arc, bool sweep, + Point const &fp + ) + : _initial_point(ip) + , _final_point(fp) + , _ellipse(0, 0, r[X], r[Y], rot_angle) + , _angles(0, 0, sweep) + , _large_arc(large_arc) + { + _updateCenterAndAngles(); + } + + /// Create a new elliptical arc, giving the ellipse's rays as separate coordinates. + EllipticalArc( Point const &ip, Coord rx, Coord ry, + Coord rot_angle, bool large_arc, bool sweep, + Point const &fp + ) + : _initial_point(ip) + , _final_point(fp) + , _ellipse(0, 0, rx, ry, rot_angle) + , _angles(0, 0, sweep) + , _large_arc(large_arc) + { + _updateCenterAndAngles(); + } + + /// @name Retrieve basic information + /// @{ + + /** @brief Get a coordinate of the elliptical arc's center. + * @param d The dimension to retrieve + * @return The selected coordinate of the center */ + Coord center(Dim2 d) const { return _ellipse.center(d); } + + /** @brief Get the arc's center + * @return The arc's center, situated on the intersection of the ellipse's rays */ + Point center() const { return _ellipse.center(); } + + /** @brief Get one of the ellipse's rays + * @param d Dimension to retrieve + * @return The selected ray of the ellipse */ + Coord ray(Dim2 d) const { return _ellipse.ray(d); } + + /** @brief Get both rays as a point + * @return Point with X equal to the X ray and Y to Y ray */ + Point rays() const { return _ellipse.rays(); } + + /** @brief Get the defining ellipse's rotation + * @return Angle between the +X ray of the ellipse and the +X axis */ + Angle rotationAngle() const { + return _ellipse.rotationAngle(); + } + + /** @brief Whether the arc is larger than half an ellipse. + * @return True if the arc is larger than \f$\pi\f$, false otherwise */ + bool largeArc() const { return _large_arc; } + + /** @brief Whether the arc turns clockwise + * @return True if the arc makes a clockwise turn when going from initial to final + * point, false otherwise */ + bool sweep() const { return _angles.sweep(); } + + Angle initialAngle() const { return _angles.initialAngle(); } + Angle finalAngle() const { return _angles.finalAngle(); } + /// @} + + /// @name Modify parameters + /// @{ + + /// Change all of the arc's parameters. + void set( Point const &ip, double rx, double ry, + double rot_angle, bool large_arc, bool sweep, + Point const &fp + ) + { + _initial_point = ip; + _final_point = fp; + _ellipse.setRays(rx, ry); + _ellipse.setRotationAngle(rot_angle); + _angles.setSweep(sweep); + _large_arc = large_arc; + _updateCenterAndAngles(); + } + + /// Change all of the arc's parameters. + void set( Point const &ip, Point const &r, + Angle rot_angle, bool large_arc, bool sweep, + Point const &fp + ) + { + _initial_point = ip; + _final_point = fp; + _ellipse.setRays(r); + _ellipse.setRotationAngle(rot_angle); + _angles.setSweep(sweep); + _large_arc = large_arc; + _updateCenterAndAngles(); + } + + /** @brief Change the initial and final point in one operation. + * This method exists because modifying any of the endpoints causes rather costly + * recalculations of the center and extreme angles. + * @param ip New initial point + * @param fp New final point */ + void setEndpoints(Point const &ip, Point const &fp) { + _initial_point = ip; + _final_point = fp; + _updateCenterAndAngles(); + } + /// @} + + /// @name Evaluate the arc as a function + /// @{ + /** Check whether the arc contains the given angle + * @param t The angle to check + * @return True if the arc contains the angle, false otherwise */ + bool containsAngle(Angle angle) const { return _angles.contains(angle); } + + /** @brief Evaluate the arc at the specified angular coordinate + * @param t Angle + * @return Point corresponding to the given angle */ + Point pointAtAngle(Coord t) const; + + /** @brief Evaluate one of the arc's coordinates at the specified angle + * @param t Angle + * @param d The dimension to retrieve + * @return Selected coordinate of the arc at the specified angle */ + Coord valueAtAngle(Coord t, Dim2 d) const; + + /// Compute the curve time value corresponding to the given angular value. + Coord timeAtAngle(Angle a) const { return _angles.timeAtAngle(a); } + + /// Compute the angular domain value corresponding to the given time value. + Angle angleAt(Coord t) const { return _angles.angleAt(t); } + + /** @brief Compute the amount by which the angle parameter changes going from start to end. + * This has range \f$(-2\pi, 2\pi)\f$ and thus cannot be represented as instance + * of the class Angle. Add this to the initial angle to obtain the final angle. */ + Coord sweepAngle() const { return _angles.sweepAngle(); } + + /** @brief Get the elliptical angle spanned by the arc. + * This is basically the absolute value of sweepAngle(). */ + Coord angularExtent() const { return _angles.extent(); } + + /// Get the angular interval of the arc. + AngleInterval angularInterval() const { return _angles; } + + /// Evaluate the arc in the curve domain, i.e. \f$[0, 1]\f$. + virtual Point pointAt(Coord t) const; + + /// Evaluate a single coordinate on the arc in the curve domain. + virtual Coord valueAt(Coord t, Dim2 d) const; + + /** @brief Compute a transform that maps the unit circle to the arc's ellipse. + * Each ellipse can be interpreted as a translated, scaled and rotate unit circle. + * This function returns the transform that maps the unit circle to the arc's ellipse. + * @return Transform from unit circle to the arc's ellipse */ + Affine unitCircleTransform() const { + Affine result = _ellipse.unitCircleTransform(); + return result; + } + + /** @brief Compute a transform that maps the arc's ellipse to the unit circle. */ + Affine inverseUnitCircleTransform() const { + Affine result = _ellipse.inverseUnitCircleTransform(); + return result; + } + /// @} + + /// @name Deal with degenerate ellipses. + /// @{ + /** @brief Check whether both rays are nonzero. + * If they are not, the arc is represented as a line segment instead. */ + bool isChord() const { + return ray(X) == 0 || ray(Y) == 0; + } + + /** @brief Get the line segment connecting the arc's endpoints. + * @return A linear segment with initial and final point corresponding to those of the arc. */ + LineSegment chord() const { return LineSegment(_initial_point, _final_point); } + /// @} + + // implementation of overloads goes here + virtual Point initialPoint() const { return _initial_point; } + virtual Point finalPoint() const { return _final_point; } + virtual Curve* duplicate() const { return new EllipticalArc(*this); } + virtual void setInitial(Point const &p) { + _initial_point = p; + _updateCenterAndAngles(); + } + virtual void setFinal(Point const &p) { + _final_point = p; + _updateCenterAndAngles(); + } + virtual bool isDegenerate() const { + return _initial_point == _final_point; + } + virtual bool isLineSegment() const { return isChord(); } + virtual Rect boundsFast() const { + return boundsExact(); + } + virtual Rect boundsExact() const; + // TODO: native implementation of the following methods + virtual OptRect boundsLocal(OptInterval const &i, unsigned int deg) const { + return SBasisCurve(toSBasis()).boundsLocal(i, deg); + } + virtual std::vector roots(double v, Dim2 d) const; +#ifdef HAVE_GSL + virtual std::vector allNearestTimes( Point const& p, double from = 0, double to = 1 ) const; + virtual double nearestTime( Point const& p, double from = 0, double to = 1 ) const { + if ( are_near(ray(X), ray(Y)) && are_near(center(), p) ) { + return from; + } + return allNearestTimes(p, from, to).front(); + } +#endif + virtual std::vector intersect(Curve const &other, Coord eps=EPSILON) const; + virtual int degreesOfFreedom() const { return 7; } + virtual Curve *derivative() const; + + using Curve::operator*=; + virtual void operator*=(Translate const &tr); + virtual void operator*=(Scale const &s); + virtual void operator*=(Rotate const &r); + virtual void operator*=(Zoom const &z); + virtual void operator*=(Affine const &m); + + virtual std::vector pointAndDerivatives(Coord t, unsigned int n) const; + virtual D2 toSBasis() const; + virtual Curve *portion(double f, double t) const; + virtual Curve *reverse() const; + virtual bool operator==(Curve const &c) const; + virtual bool isNear(Curve const &other, Coord precision) const; + virtual void feed(PathSink &sink, bool moveto_initial) const; + virtual int winding(Point const &p) const; + +private: + void _updateCenterAndAngles(); + void _filterIntersections(std::vector &xs, bool is_first) const; + + Point _initial_point, _final_point; + Ellipse _ellipse; + AngleInterval _angles; + bool _large_arc; +}; // end class EllipticalArc + + +// implemented in elliptical-arc-from-sbasis.cpp +/** @brief Fit an elliptical arc to an SBasis fragment. + * @relates EllipticalArc */ +bool arc_from_sbasis(EllipticalArc &ea, D2 const &in, + double tolerance = EPSILON, unsigned num_samples = 20); + +/** @brief Debug output for elliptical arcs. + * @relates EllipticalArc */ +std::ostream &operator<<(std::ostream &out, EllipticalArc const &ea); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_ELLIPTICAL_ARC_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/exception.h b/src/2geom/exception.h new file mode 100644 index 0000000..ab6f821 --- /dev/null +++ b/src/2geom/exception.h @@ -0,0 +1,145 @@ +/** + * \file + * \brief Defines the different types of exceptions that 2geom can throw. + * + * There are two main exception classes: LogicalError and RangeError. + * Logical errors are 2geom faults/bugs; RangeErrors are 'user' faults, + * e.g. invalid arguments to lib2geom methods. + * This way, the 'user' can distinguish between groups of exceptions + * ('user' is the coder that uses lib2geom) + * + * Several macro's are defined for easily throwing exceptions + * (e.g. THROW_CONTINUITYERROR). + */ +/* Copyright 2007 Johan Engelen + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_EXCEPTION_H +#define LIB2GEOM_SEEN_EXCEPTION_H + +#include +#include +#include + +namespace Geom { + +/** + * Base exception class, all 2geom exceptions should be derived from this one. + */ +class Exception : public std::exception { +public: + Exception(const char * message, const char *file, const int line) { + std::ostringstream os; + os << "lib2geom exception: " << message << " (" << file << ":" << line << ")"; + msgstr = os.str(); + } + + virtual ~Exception() throw() {} // necessary to destroy the string object!!! + + virtual const char* what() const throw () { + return msgstr.c_str(); + } +protected: + std::string msgstr; +}; +#define THROW_EXCEPTION(message) throw(Geom::Exception(message, __FILE__, __LINE__)) + +//----------------------------------------------------------------------- + +class LogicalError : public Exception { +public: + LogicalError(const char * message, const char *file, const int line) + : Exception(message, file, line) {} +}; +#define THROW_LOGICALERROR(message) throw(LogicalError(message, __FILE__, __LINE__)) + +class RangeError : public Exception { +public: + RangeError(const char * message, const char *file, const int line) + : Exception(message, file, line) {} +}; +#define THROW_RANGEERROR(message) throw(RangeError(message, __FILE__, __LINE__)) + +//----------------------------------------------------------------------- +// Special case exceptions. Best used with the defines :) + +class NotImplemented : public LogicalError { +public: + NotImplemented(const char *file, const int line) + : LogicalError("Method not implemented", file, line) {} +}; +#define THROW_NOTIMPLEMENTED(i) throw(NotImplemented(__FILE__, __LINE__)) + +class InvariantsViolation : public LogicalError { +public: + InvariantsViolation(const char *file, const int line) + : LogicalError("Invariants violation", file, line) {} +}; +#define THROW_INVARIANTSVIOLATION(i) throw(InvariantsViolation(__FILE__, __LINE__)) +#define ASSERT_INVARIANTS(e) ((e) ? (void)0 : THROW_INVARIANTSVIOLATION()) + +class NotInvertible : public RangeError { +public: + NotInvertible(const char *file, const int line) + : RangeError("Function does not have a unique inverse", file, line) {} +}; +#define THROW_NOTINVERTIBLE(i) throw(NotInvertible(__FILE__, __LINE__)) + +class InfiniteSolutions : public RangeError { +public: + InfiniteSolutions(const char *file, const int line) + : RangeError("There are infinite solutions", file, line) {} +}; +#define THROW_INFINITESOLUTIONS(i) throw(InfiniteSolutions(__FILE__, __LINE__)) + +class ContinuityError : public RangeError { +public: + ContinuityError(const char *file, const int line) + : RangeError("Non-contiguous path", file, line) {} +}; +#define THROW_CONTINUITYERROR(i) throw(ContinuityError(__FILE__, __LINE__)) + +struct SVGPathParseError : public std::exception { + char const *what() const throw() { return "parse error"; } +}; + + +} // namespace Geom + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/forward.h b/src/2geom/forward.h new file mode 100644 index 0000000..2790924 --- /dev/null +++ b/src/2geom/forward.h @@ -0,0 +1,127 @@ +/** + * \file + * \brief Contains forward declarations of 2geom types + *//* + * Authors: + * Johan Engelen + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2008-2010 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_FORWARD_H +#define LIB2GEOM_SEEN_FORWARD_H + +namespace Geom { + +// primitives +typedef double Coord; +typedef int IntCoord; +class Point; +class IntPoint; +class Line; +class Ray; +template class GenericInterval; +template class GenericOptInterval; +class Interval; +class OptInterval; +typedef GenericInterval IntInterval; +typedef GenericOptInterval OptIntInterval; +template class GenericRect; +template class GenericOptRect; +class Rect; +class OptRect; +typedef GenericRect IntRect; +typedef GenericOptRect OptIntRect; + +// fragments +class Linear; +class Bezier; +class SBasis; +class Poly; + +// shapes +class Circle; +class Ellipse; +class ConvexHull; + +// curves +class Curve; +class SBasisCurve; +class BezierCurve; +template class BezierCurveN; +typedef BezierCurveN<1> LineSegment; +typedef BezierCurveN<2> QuadraticBezier; +typedef BezierCurveN<3> CubicBezier; +class EllipticalArc; + +// paths and path sequences +class Path; +class PathVector; +struct PathTime; +class PathInterval; +struct PathVectorTime; + +// errors +class Exception; +class LogicalError; +class RangeError; +class NotImplemented; +class InvariantsViolation; +class NotInvertible; +class ContinuityError; + +// transforms +class Affine; +class Translate; +class Rotate; +class Scale; +class HShear; +class VShear; +class Zoom; + +// templates +template class D2; +template class Piecewise; + +// misc +class SVGPathSink; +template class SVGPathGenerator; + +} + +#endif // SEEN_GEOM_FORWARD_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/generic-interval.h b/src/2geom/generic-interval.h new file mode 100644 index 0000000..e7892e2 --- /dev/null +++ b/src/2geom/generic-interval.h @@ -0,0 +1,361 @@ +/** + * @file + * @brief Closed interval of generic values + *//* + * Copyright 2011 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_GENERIC_INTERVAL_H +#define LIB2GEOM_SEEN_GENERIC_INTERVAL_H + +#include +#include +#include +#include +#include <2geom/coord.h> + +namespace Geom { + +template +class GenericOptInterval; + +/** + * @brief A range of numbers which is never empty. + * @ingroup Primitives + */ +template +class GenericInterval + : CoordTraits::IntervalOps +{ + typedef typename CoordTraits::IntervalType CInterval; + typedef GenericInterval Self; +protected: + C _b[2]; +public: + /// @name Create intervals. + /// @{ + /** @brief Create an interval that contains only zero. */ + GenericInterval() { _b[0] = 0; _b[1] = 0; } + /** @brief Create an interval that contains a single point. */ + explicit GenericInterval(C u) { _b[0] = _b[1] = u; } + /** @brief Create an interval that contains all points between @c u and @c v. */ + GenericInterval(C u, C v) { + if (u <= v) { + _b[0] = u; _b[1] = v; + } else { + _b[0] = v; _b[1] = u; + } + } + + /** @brief Create an interval containing a range of values. + * The resulting interval will contain all values from the given range. + * The return type of iterators must be convertible to C. The given range + * must not be empty. For potentially empty ranges, see GenericOptInterval. + * @param start Beginning of the range + * @param end End of the range + * @return Interval that contains all values from [start, end). */ + template + static CInterval from_range(InputIterator start, InputIterator end) { + assert(start != end); + CInterval result(*start++); + for (; start != end; ++start) result.expandTo(*start); + return result; + } + /** @brief Create an interval from a C-style array of values it should contain. */ + static CInterval from_array(C const *c, unsigned n) { + CInterval result = from_range(c, c+n); + return result; + } + /// @} + + /// @name Inspect contained values. + /// @{ + C min() const { return _b[0]; } + C max() const { return _b[1]; } + C extent() const { return max() - min(); } + C middle() const { return (max() + min()) / 2; } + bool isSingular() const { return min() == max(); } + C operator[](unsigned i) const { assert(i < 2); return _b[i]; } + C clamp(C val) const { + if (val < min()) return min(); + if (val > max()) return max(); + return val; + } + /// Return the closer end of the interval. + C nearestEnd(C val) const { + C dmin = std::abs(val - min()), dmax = std::abs(val - max()); + return dmin <= dmax ? min() : max(); + } + /// @} + + /// @name Test coordinates and other intervals for inclusion. + /// @{ + /** @brief Check whether the interval includes this number. */ + bool contains(C val) const { + return min() <= val && val <= max(); + } + /** @brief Check whether the interval includes the given interval. */ + bool contains(CInterval const &val) const { + return min() <= val.min() && val.max() <= max(); + } + /** @brief Check whether the intervals have any common elements. */ + bool intersects(CInterval const &val) const { + return contains(val.min()) || contains(val.max()) || val.contains(*this); + } + /// @} + + /// @name Modify the interval. + /// @{ + //TODO: NaN handleage for the next two? + /** @brief Set the lower boundary of the interval. + * When the given number is larger than the interval's largest element, + * it will be reduced to the single number @c val. */ + void setMin(C val) { + if(val > _b[1]) { + _b[0] = _b[1] = val; + } else { + _b[0] = val; + } + } + /** @brief Set the upper boundary of the interval. + * When the given number is smaller than the interval's smallest element, + * it will be reduced to the single number @c val. */ + void setMax(C val) { + if(val < _b[0]) { + _b[1] = _b[0] = val; + } else { + _b[1] = val; + } + } + /// Set both ends of the interval simultaneously + void setEnds(C a, C b) { + if (a <= b) { + _b[0] = a; + _b[1] = b; + } else { + _b[0] = b; + _b[1] = a; + } + } + /** @brief Extend the interval to include the given number. */ + void expandTo(C val) { + if(val < _b[0]) _b[0] = val; + if(val > _b[1]) _b[1] = val; //no else, as we want to handle NaN + } + /** @brief Expand or shrink the interval in both directions by the given amount. + * After this method, the interval's length (extent) will be increased by + * amount * 2. Negative values can be given; they will shrink the interval. + * Shrinking by a value larger than half the interval's length will create a degenerate + * interval containing only the midpoint of the original. */ + void expandBy(C amount) { + _b[0] -= amount; + _b[1] += amount; + if (_b[0] > _b[1]) { + C halfway = (_b[0]+_b[1])/2; + _b[0] = _b[1] = halfway; + } + } + /** @brief Union the interval with another one. + * The resulting interval will contain all points of both intervals. + * It might also contain some points which didn't belong to either - this happens + * when the intervals did not have any common elements. */ + void unionWith(CInterval const &a) { + if(a._b[0] < _b[0]) _b[0] = a._b[0]; + if(a._b[1] > _b[1]) _b[1] = a._b[1]; + } + /// @} + + /// @name Operators + /// @{ + //IMPL: OffsetableConcept + //TODO: rename output_type to something else in the concept + typedef C output_type; + /** @brief Offset the interval by a specified amount */ + Self &operator+=(C amnt) { + _b[0] += amnt; _b[1] += amnt; + return *this; + } + /** @brief Offset the interval by the negation of the specified amount */ + Self &operator-=(C amnt) { + _b[0] -= amnt; _b[1] -= amnt; + return *this; + } + + /** @brief Return an interval mirrored about 0 */ + Self operator-() const { Self r(-_b[1], -_b[0]); return r; } + // IMPL: AddableConcept + /** @brief Add two intervals. + * Sum is defined as the set of points that can be obtained by adding any two values + * from both operands: \f$S = \{x \in A, y \in B: x + y\}\f$ */ + Self &operator+=(CInterval const &o) { + _b[0] += o._b[0]; + _b[1] += o._b[1]; + return *this; + } + /** @brief Subtract two intervals. + * Difference is defined as the set of points that can be obtained by subtracting + * any value from the second operand from any value from the first operand: + * \f$S = \{x \in A, y \in B: x - y\}\f$ */ + Self &operator-=(CInterval const &o) { + // equal to *this += -o + _b[0] -= o._b[1]; + _b[1] -= o._b[0]; + return *this; + } + /** @brief Union two intervals. + * Note that the intersection-and-assignment operator is not defined, + * because the result of an intersection can be empty, while Interval cannot. */ + Self &operator|=(CInterval const &o) { + unionWith(o); + return *this; + } + /** @brief Test for interval equality. */ + bool operator==(CInterval const &other) const { + return min() == other.min() && max() == other.max(); + } + /// @} +}; + +/** @brief Union two intervals + * @relates GenericInterval */ +template +inline GenericInterval unify(GenericInterval const &a, GenericInterval const &b) { + return a | b; +} + +/** + * @brief A range of numbers that can be empty. + * @ingroup Primitives + */ +template +class GenericOptInterval + : public boost::optional::IntervalType> + , boost::orable< GenericOptInterval + , boost::andable< GenericOptInterval + > > +{ + typedef typename CoordTraits::IntervalType CInterval; + typedef typename CoordTraits::OptIntervalType OptCInterval; + typedef boost::optional Base; +public: + /// @name Create optionally empty intervals. + /// @{ + /** @brief Create an empty interval. */ + GenericOptInterval() : Base() {} + /** @brief Wrap an existing interval. */ + GenericOptInterval(GenericInterval const &a) : Base(CInterval(a)) {} + /** @brief Create an interval containing a single point. */ + GenericOptInterval(C u) : Base(CInterval(u)) {} + /** @brief Create an interval containing a range of numbers. */ + GenericOptInterval(C u, C v) : Base(CInterval(u,v)) {} + + /** @brief Create a possibly empty interval containing a range of values. + * The resulting interval will contain all values from the given range. + * The return type of iterators must be convertible to C. The given range + * may be empty. + * @param start Beginning of the range + * @param end End of the range + * @return Interval that contains all values from [start, end), or nothing if the range + * is empty. */ + template + static GenericOptInterval from_range(InputIterator start, InputIterator end) { + if (start == end) { + GenericOptInterval ret; + return ret; + } + GenericOptInterval ret(CInterval::from_range(start, end)); + return ret; + } + /// @} + + /** @brief Check whether this interval is empty. */ + bool empty() { return !*this; } + + /** @brief Union with another interval, gracefully handling empty ones. */ + void unionWith(GenericOptInterval const &a) { + if (a) { + if (*this) { // check that we are not empty + (*this)->unionWith(*a); + } else { + *this = *a; + } + } + } + void intersectWith(GenericOptInterval const &o) { + if (o && *this) { + if (!*this) return; + C u = std::max((*this)->min(), o->min()); + C v = std::min((*this)->max(), o->max()); + if (u <= v) { + *this = CInterval(u, v); + return; + } + } + (*static_cast(this)) = boost::none; + } + GenericOptInterval &operator|=(OptCInterval const &o) { + unionWith(o); + return *this; + } + GenericOptInterval &operator&=(OptCInterval const &o) { + intersectWith(o); + return *this; + } +}; + +/** @brief Intersect two intervals and return a possibly empty range of numbers + * @relates GenericOptInterval */ +template +inline GenericOptInterval intersect(GenericInterval const &a, GenericInterval const &b) { + return GenericOptInterval(a) & GenericOptInterval(b); +} +/** @brief Intersect two intervals and return a possibly empty range of numbers + * @relates GenericOptInterval */ +template +inline GenericOptInterval operator&(GenericInterval const &a, GenericInterval const &b) { + return GenericOptInterval(a) & GenericOptInterval(b); +} + +template +inline std::ostream &operator<< (std::ostream &os, + Geom::GenericInterval const &I) { + os << "Interval("< + * Krzysztof KosiÅ„ski + * Copyright 2007-2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + * Authors of original rect class: + * Lauris Kaplinski + * Nathan Hurst + * bulia byak + * MenTaLguY + */ + +#ifndef LIB2GEOM_SEEN_GENERIC_RECT_H +#define LIB2GEOM_SEEN_GENERIC_RECT_H + +#include +#include +#include +#include <2geom/coord.h> + +namespace Geom { + +template +class GenericOptRect; + +/** + * @brief Axis aligned, non-empty, generic rectangle. + * @ingroup Primitives + */ +template +class GenericRect + : CoordTraits::RectOps +{ + typedef typename CoordTraits::IntervalType CInterval; + typedef typename CoordTraits::PointType CPoint; + typedef typename CoordTraits::RectType CRect; + typedef typename CoordTraits::OptRectType OptCRect; +protected: + CInterval f[2]; +public: + typedef CInterval D1Value; + typedef CInterval &D1Reference; + typedef CInterval const &D1ConstReference; + + /// @name Create rectangles. + /// @{ + /** @brief Create a rectangle that contains only the point at (0,0). */ + GenericRect() { f[X] = f[Y] = CInterval(); } + /** @brief Create a rectangle from X and Y intervals. */ + GenericRect(CInterval const &a, CInterval const &b) { + f[X] = a; + f[Y] = b; + } + /** @brief Create a rectangle from two points. */ + GenericRect(CPoint const &a, CPoint const &b) { + f[X] = CInterval(a[X], b[X]); + f[Y] = CInterval(a[Y], b[Y]); + } + /** @brief Create rectangle from coordinates of two points. */ + GenericRect(C x0, C y0, C x1, C y1) { + f[X] = CInterval(x0, x1); + f[Y] = CInterval(y0, y1); + } + /** @brief Create a rectangle from a range of points. + * The resulting rectangle will contain all points from the range. + * The return type of iterators must be convertible to Point. + * The range must not be empty. For possibly empty ranges, see OptRect. + * @param start Beginning of the range + * @param end End of the range + * @return Rectangle that contains all points from [start, end). */ + template + static CRect from_range(InputIterator start, InputIterator end) { + assert(start != end); + CPoint p1 = *start++; + CRect result(p1, p1); + for (; start != end; ++start) { + result.expandTo(*start); + } + return result; + } + /** @brief Create a rectangle from a C-style array of points it should contain. */ + static CRect from_array(CPoint const *c, unsigned n) { + CRect result = GenericRect::from_range(c, c+n); + return result; + } + /** @brief Create rectangle from origin and dimensions. */ + static CRect from_xywh(C x, C y, C w, C h) { + CPoint xy(x, y); + CPoint wh(w, h); + CRect result(xy, xy + wh); + return result; + } + /** @brief Create rectangle from origin and dimensions. */ + static CRect from_xywh(CPoint const &xy, CPoint const &wh) { + CRect result(xy, xy + wh); + return result; + } + /// Create infinite rectangle. + static CRect infinite() { + CPoint p0(std::numeric_limits::min(), std::numeric_limits::min()); + CPoint p1(std::numeric_limits::max(), std::numeric_limits::max()); + CRect result(p0, p1); + return result; + } + /// @} + + /// @name Inspect dimensions. + /// @{ + CInterval &operator[](unsigned i) { return f[i]; } + CInterval const &operator[](unsigned i) const { return f[i]; } + CInterval &operator[](Dim2 d) { return f[d]; } + CInterval const &operator[](Dim2 d) const { return f[d]; } + + /** @brief Get the corner of the rectangle with smallest coordinate values. + * In 2Geom standard coordinate system, this means upper left. */ + CPoint min() const { CPoint p(f[X].min(), f[Y].min()); return p; } + /** @brief Get the corner of the rectangle with largest coordinate values. + * In 2Geom standard coordinate system, this means lower right. */ + CPoint max() const { CPoint p(f[X].max(), f[Y].max()); return p; } + /** @brief Return the n-th corner of the rectangle. + * Returns corners in the direction of growing angles, starting from + * the one given by min(). For the standard coordinate system used + * in 2Geom (+Y downwards), this means clockwise starting from + * the upper left. */ + CPoint corner(unsigned i) const { + switch(i % 4) { + case 0: return CPoint(f[X].min(), f[Y].min()); + case 1: return CPoint(f[X].max(), f[Y].min()); + case 2: return CPoint(f[X].max(), f[Y].max()); + default: return CPoint(f[X].min(), f[Y].max()); + } + } + + //We should probably remove these - they're coord sys gnostic + /** @brief Return top coordinate of the rectangle (+Y is downwards). */ + C top() const { return f[Y].min(); } + /** @brief Return bottom coordinate of the rectangle (+Y is downwards). */ + C bottom() const { return f[Y].max(); } + /** @brief Return leftmost coordinate of the rectangle (+X is to the right). */ + C left() const { return f[X].min(); } + /** @brief Return rightmost coordinate of the rectangle (+X is to the right). */ + C right() const { return f[X].max(); } + + /** @brief Get the horizontal extent of the rectangle. */ + C width() const { return f[X].extent(); } + /** @brief Get the vertical extent of the rectangle. */ + C height() const { return f[Y].extent(); } + /** @brief Get the ratio of width to height of the rectangle. */ + Coord aspectRatio() const { return Coord(width()) / Coord(height()); } + + /** @brief Get rectangle's width and height as a point. + * @return Point with X coordinate corresponding to the width and the Y coordinate + * corresponding to the height of the rectangle. */ + CPoint dimensions() const { return CPoint(f[X].extent(), f[Y].extent()); } + /** @brief Get the point in the geometric center of the rectangle. */ + CPoint midpoint() const { return CPoint(f[X].middle(), f[Y].middle()); } + + /** @brief Compute rectangle's area. */ + C area() const { return f[X].extent() * f[Y].extent(); } + /** @brief Check whether the rectangle has zero area. */ + bool hasZeroArea() const { return f[X].isSingular() || f[Y].isSingular(); } + + /** @brief Get the larger extent (width or height) of the rectangle. */ + C maxExtent() const { return std::max(f[X].extent(), f[Y].extent()); } + /** @brief Get the smaller extent (width or height) of the rectangle. */ + C minExtent() const { return std::min(f[X].extent(), f[Y].extent()); } + + /** @brief Clamp point to the rectangle. */ + CPoint clamp(CPoint const &p) const { + CPoint result(f[X].clamp(p[X]), f[Y].clamp(p[Y])); + return result; + } + /** @brief Get the nearest point on the edge of the rectangle. */ + CPoint nearestEdgePoint(CPoint const &p) const { + CPoint result = p; + if (!contains(p)) { + result = clamp(p); + } else { + C cx = f[X].nearestEnd(p[X]); + C cy = f[Y].nearestEnd(p[Y]); + if (std::abs(cx - p[X]) <= std::abs(cy - p[Y])) { + result[X] = cx; + } else { + result[Y] = cy; + } + } + return result; + } + /// @} + + /// @name Test other rectangles and points for inclusion. + /// @{ + /** @brief Check whether the rectangles have any common points. */ + bool intersects(GenericRect const &r) const { + return f[X].intersects(r[X]) && f[Y].intersects(r[Y]); + } + /** @brief Check whether the rectangle includes all points in the given rectangle. */ + bool contains(GenericRect const &r) const { + return f[X].contains(r[X]) && f[Y].contains(r[Y]); + } + + /** @brief Check whether the rectangles have any common points. + * Empty rectangles will not intersect with any other rectangle. */ + inline bool intersects(OptCRect const &r) const; + /** @brief Check whether the rectangle includes all points in the given rectangle. + * Empty rectangles will be contained in any non-empty rectangle. */ + inline bool contains(OptCRect const &r) const; + + /** @brief Check whether the given point is within the rectangle. */ + bool contains(CPoint const &p) const { + return f[X].contains(p[X]) && f[Y].contains(p[Y]); + } + /// @} + + /// @name Modify the rectangle. + /// @{ + /** @brief Set the minimum X coordinate of the rectangle. */ + void setLeft(C val) { + f[X].setMin(val); + } + /** @brief Set the maximum X coordinate of the rectangle. */ + void setRight(C val) { + f[X].setMax(val); + } + /** @brief Set the minimum Y coordinate of the rectangle. */ + void setTop(C val) { + f[Y].setMin(val); + } + /** @brief Set the maximum Y coordinate of the rectangle. */ + void setBottom(C val) { + f[Y].setMax(val); + } + /** @brief Set the upper left point of the rectangle. */ + void setMin(CPoint const &p) { + f[X].setMin(p[X]); + f[Y].setMin(p[Y]); + } + /** @brief Set the lower right point of the rectangle. */ + void setMax(CPoint const &p) { + f[X].setMax(p[X]); + f[Y].setMax(p[Y]); + } + /** @brief Enlarge the rectangle to contain the given point. */ + void expandTo(CPoint const &p) { + f[X].expandTo(p[X]); f[Y].expandTo(p[Y]); + } + /** @brief Enlarge the rectangle to contain the argument. */ + void unionWith(CRect const &b) { + f[X].unionWith(b[X]); f[Y].unionWith(b[Y]); + } + /** @brief Enlarge the rectangle to contain the argument. + * Unioning with an empty rectangle results in no changes. */ + void unionWith(OptCRect const &b); + + /** @brief Expand the rectangle in both directions by the specified amount. + * Note that this is different from scaling. Negative values will shrink the + * rectangle. If -amount is larger than + * half of the width, the X interval will contain only the X coordinate + * of the midpoint; same for height. */ + void expandBy(C amount) { + expandBy(amount, amount); + } + /** @brief Expand the rectangle in both directions. + * Note that this is different from scaling. Negative values will shrink the + * rectangle. If -x is larger than + * half of the width, the X interval will contain only the X coordinate + * of the midpoint; same for height. */ + void expandBy(C x, C y) { + f[X].expandBy(x); f[Y].expandBy(y); + } + /** @brief Expand the rectangle by the coordinates of the given point. + * This will expand the width by the X coordinate of the point in both directions + * and the height by Y coordinate of the point. Negative coordinate values will + * shrink the rectangle. If -p[X] is larger than half of the width, + * the X interval will contain only the X coordinate of the midpoint; + * same for height. */ + void expandBy(CPoint const &p) { + expandBy(p[X], p[Y]); + } + /// @} + + /// @name Operators + /// @{ + /** @brief Offset the rectangle by a vector. */ + GenericRect &operator+=(CPoint const &p) { + f[X] += p[X]; + f[Y] += p[Y]; + return *this; + } + /** @brief Offset the rectangle by the negation of a vector. */ + GenericRect &operator-=(CPoint const &p) { + f[X] -= p[X]; + f[Y] -= p[Y]; + return *this; + } + /** @brief Union two rectangles. */ + GenericRect &operator|=(CRect const &o) { + unionWith(o); + return *this; + } + GenericRect &operator|=(OptCRect const &o) { + unionWith(o); + return *this; + } + /** @brief Test for equality of rectangles. */ + bool operator==(CRect const &o) const { return f[X] == o[X] && f[Y] == o[Y]; } + /// @} +}; + +/** + * @brief Axis-aligned generic rectangle that can be empty. + * @ingroup Primitives + */ +template +class GenericOptRect + : public boost::optional::RectType> + , boost::equality_comparable< typename CoordTraits::OptRectType + , boost::equality_comparable< typename CoordTraits::OptRectType, typename CoordTraits::RectType + , boost::orable< typename CoordTraits::OptRectType + , boost::andable< typename CoordTraits::OptRectType + , boost::andable< typename CoordTraits::OptRectType, typename CoordTraits::RectType + > > > > > +{ + typedef typename CoordTraits::IntervalType CInterval; + typedef typename CoordTraits::OptIntervalType OptCInterval; + typedef typename CoordTraits::PointType CPoint; + typedef typename CoordTraits::RectType CRect; + typedef typename CoordTraits::OptRectType OptCRect; + typedef boost::optional Base; +public: + typedef CInterval D1Value; + typedef CInterval &D1Reference; + typedef CInterval const &D1ConstReference; + + /// @name Create potentially empty rectangles. + /// @{ + GenericOptRect() : Base() {} + GenericOptRect(GenericRect const &a) : Base(CRect(a)) {} + GenericOptRect(CPoint const &a, CPoint const &b) : Base(CRect(a, b)) {} + GenericOptRect(C x0, C y0, C x1, C y1) : Base(CRect(x0, y0, x1, y1)) {} + /// Creates an empty OptRect when one of the argument intervals is empty. + GenericOptRect(OptCInterval const &x_int, OptCInterval const &y_int) { + if (x_int && y_int) { + *this = CRect(*x_int, *y_int); + } + // else, stay empty. + } + + /** @brief Create a rectangle from a range of points. + * The resulting rectangle will contain all points from the range. + * If the range contains no points, the result will be an empty rectangle. + * The return type of iterators must be convertible to the corresponding + * point type (Point or IntPoint). + * @param start Beginning of the range + * @param end End of the range + * @return Rectangle that contains all points from [start, end). */ + template + static OptCRect from_range(InputIterator start, InputIterator end) { + OptCRect result; + for (; start != end; ++start) { + result.expandTo(*start); + } + return result; + } + /// @} + + /// @name Check other rectangles and points for inclusion. + /// @{ + /** @brief Check for emptiness. */ + inline bool empty() const { return !*this; }; + /** @brief Check whether the rectangles have any common points. + * Empty rectangles will not intersect with any other rectangle. */ + bool intersects(CRect const &r) const { return r.intersects(*this); } + /** @brief Check whether the rectangle includes all points in the given rectangle. + * Empty rectangles will be contained in any non-empty rectangle. */ + bool contains(CRect const &r) const { return *this && (*this)->contains(r); } + + /** @brief Check whether the rectangles have any common points. + * Empty rectangles will not intersect with any other rectangle. + * Two empty rectangles will not intersect each other. */ + bool intersects(OptCRect const &r) const { return *this && (*this)->intersects(r); } + /** @brief Check whether the rectangle includes all points in the given rectangle. + * Empty rectangles will be contained in any non-empty rectangle. + * An empty rectangle will not contain other empty rectangles. */ + bool contains(OptCRect const &r) const { return *this && (*this)->contains(r); } + + /** @brief Check whether the given point is within the rectangle. + * An empty rectangle will not contain any points. */ + bool contains(CPoint const &p) const { return *this && (*this)->contains(p); } + /// @} + + /// @name Modify the potentially empty rectangle. + /// @{ + /** @brief Enlarge the rectangle to contain the argument. + * If this rectangle is empty, after callng this method it will + * be equal to the argument. */ + void unionWith(CRect const &b) { + if (*this) { + (*this)->unionWith(b); + } else { + *this = b; + } + } + /** @brief Enlarge the rectangle to contain the argument. + * Unioning with an empty rectangle results in no changes. + * If this rectangle is empty, after calling this method it will + * be equal to the argument. */ + void unionWith(OptCRect const &b) { + if (b) unionWith(*b); + } + /** @brief Leave only the area overlapping with the argument. + * If the rectangles do not have any points in common, after calling + * this method the rectangle will be empty. */ + void intersectWith(CRect const &b) { + if (!*this) return; + OptCInterval x = (**this)[X] & b[X], y = (**this)[Y] & b[Y]; + if (x && y) { + *this = CRect(*x, *y); + } else { + *(static_cast(this)) = boost::none; + } + } + /** @brief Leave only the area overlapping with the argument. + * If the argument is empty or the rectangles do not have any points + * in common, after calling this method the rectangle will be empty. */ + void intersectWith(OptCRect const &b) { + if (b) { + intersectWith(*b); + } else { + *(static_cast(this)) = boost::none; + } + } + /** @brief Create or enlarge the rectangle to contain the given point. + * If the rectangle is empty, after calling this method it will be non-empty + * and it will contain only the given point. */ + void expandTo(CPoint const &p) { + if (*this) { + (*this)->expandTo(p); + } else { + *this = CRect(p, p); + } + } + /// @} + + /// @name Operators + /// @{ + /** @brief Union with @a b */ + GenericOptRect &operator|=(OptCRect const &b) { + unionWith(b); + return *this; + } + /** @brief Intersect with @a b */ + GenericOptRect &operator&=(CRect const &b) { + intersectWith(b); + return *this; + } + /** @brief Intersect with @a b */ + GenericOptRect &operator&=(OptCRect const &b) { + intersectWith(b); + return *this; + } + /** @brief Test for equality. + * All empty rectangles are equal. */ + bool operator==(OptCRect const &other) const { + if (!*this != !other) return false; + return *this ? (**this == *other) : true; + } + bool operator==(CRect const &other) const { + if (!*this) return false; + return **this == other; + } + /// @} +}; + +template +inline void GenericRect::unionWith(OptCRect const &b) { + if (b) { + unionWith(*b); + } +} +template +inline bool GenericRect::intersects(OptCRect const &r) const { + return r && intersects(*r); +} +template +inline bool GenericRect::contains(OptCRect const &r) const { + return !r || contains(*r); +} + +template +inline std::ostream &operator<<(std::ostream &out, GenericRect const &r) { + out << "Rect " << r[X] << " x " << r[Y]; + return out; +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_RECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/geom.cpp b/src/2geom/geom.cpp new file mode 100644 index 0000000..71bbbf5 --- /dev/null +++ b/src/2geom/geom.cpp @@ -0,0 +1,395 @@ +/** + * \brief Various geometrical calculations. + */ + +#include <2geom/geom.h> +#include <2geom/point.h> +#include +#include <2geom/rect.h> + +using std::swap; + +namespace Geom { + +enum IntersectorKind { + intersects = 0, + parallel, + coincident, + no_intersection +}; + +/** + * Finds the intersection of the two (infinite) lines + * defined by the points p such that dot(n0, p) == d0 and dot(n1, p) == d1. + * + * If the two lines intersect, then \a result becomes their point of + * intersection; otherwise, \a result remains unchanged. + * + * This function finds the intersection of the two lines (infinite) + * defined by n0.X = d0 and x1.X = d1. The algorithm is as follows: + * To compute the intersection point use kramer's rule: + * \verbatim + * convert lines to form + * ax + by = c + * dx + ey = f + * + * ( + * e.g. a = (x2 - x1), b = (y2 - y1), c = (x2 - x1)*x1 + (y2 - y1)*y1 + * ) + * + * In our case we use: + * a = n0.x d = n1.x + * b = n0.y e = n1.y + * c = d0 f = d1 + * + * so: + * + * adx + bdy = cd + * adx + aey = af + * + * bdy - aey = cd - af + * (bd - ae)y = cd - af + * + * y = (cd - af)/(bd - ae) + * + * repeat for x and you get: + * + * x = (fb - ce)/(bd - ae) \endverbatim + * + * If the denominator (bd-ae) is 0 then the lines are parallel, if the + * numerators are 0 then the lines coincide. + * + * \todo Why not use existing but outcommented code below + * (HAVE_NEW_INTERSECTOR_CODE)? + */ +IntersectorKind +line_intersection(Geom::Point const &n0, double const d0, + Geom::Point const &n1, double const d1, + Geom::Point &result) +{ + double denominator = dot(Geom::rot90(n0), n1); + double X = n1[Geom::Y] * d0 - + n0[Geom::Y] * d1; + /* X = (-d1, d0) dot (n0[Y], n1[Y]) */ + + if (denominator == 0) { + if ( X == 0 ) { + return coincident; + } else { + return parallel; + } + } + + double Y = n0[Geom::X] * d1 - + n1[Geom::X] * d0; + + result = Geom::Point(X, Y) / denominator; + + return intersects; +} + + + +/* ccw exists as a building block */ +int +intersector_ccw(const Geom::Point& p0, const Geom::Point& p1, + const Geom::Point& p2) +/* Determine which way a set of three points winds. */ +{ + Geom::Point d1 = p1 - p0; + Geom::Point d2 = p2 - p0; + /* compare slopes but avoid division operation */ + double c = dot(Geom::rot90(d1), d2); + if(c > 0) + return +1; // ccw - do these match def'n in header? + if(c < 0) + return -1; // cw + + /* Colinear [or NaN]. Decide the order. */ + if ( ( d1[0] * d2[0] < 0 ) || + ( d1[1] * d2[1] < 0 ) ) { + return -1; // p2 < p0 < p1 + } else if ( dot(d1,d1) < dot(d2,d2) ) { + return +1; // p0 <= p1 < p2 + } else { + return 0; // p0 <= p2 <= p1 + } +} + +/** Determine whether the line segment from p00 to p01 intersects the + infinite line passing through p10 and p11. This doesn't find the + point of intersection, use the line_intersect function above, + or the segment_intersection interface below. + + \pre neither segment is zero-length; i.e. p00 != p01 and p10 != p11. +*/ +bool +line_segment_intersectp(Geom::Point const &p00, Geom::Point const &p01, + Geom::Point const &p10, Geom::Point const &p11) +{ + if(p00 == p01) return false; + if(p10 == p11) return false; + + return ((intersector_ccw(p00, p01, p10) * intersector_ccw(p00, p01, p11)) <= 0 ); +} + + +/** Determine whether two line segments intersect. This doesn't find + the point of intersection, use the line_intersect function above, + or the segment_intersection interface below. + + \pre neither segment is zero-length; i.e. p00 != p01 and p10 != p11. +*/ +bool +segment_intersectp(Geom::Point const &p00, Geom::Point const &p01, + Geom::Point const &p10, Geom::Point const &p11) +{ + if(p00 == p01) return false; + if(p10 == p11) return false; + + /* true iff ( (the p1 segment straddles the p0 infinite line) + * and (the p0 segment straddles the p1 infinite line) ). */ + return (line_segment_intersectp(p00, p01, p10, p11) && + line_segment_intersectp(p10, p11, p00, p01)); +} + +/** Determine whether \& where a line segments intersects an (infinite) line. + +If there is no intersection, then \a result remains unchanged. + +\pre neither segment is zero-length; i.e. p00 != p01 and p10 != p11. +**/ +IntersectorKind +line_segment_intersect(Geom::Point const &p00, Geom::Point const &p01, + Geom::Point const &p10, Geom::Point const &p11, + Geom::Point &result) +{ + if(line_segment_intersectp(p00, p01, p10, p11)) { + Geom::Point n0 = (p01 - p00).ccw(); + double d0 = dot(n0,p00); + + Geom::Point n1 = (p11 - p10).ccw(); + double d1 = dot(n1,p10); + return line_intersection(n0, d0, n1, d1, result); + } else { + return no_intersection; + } +} + + +/** Determine whether \& where two line segments intersect. + +If the two segments don't intersect, then \a result remains unchanged. + +\pre neither segment is zero-length; i.e. p00 != p01 and p10 != p11. +**/ +IntersectorKind +segment_intersect(Geom::Point const &p00, Geom::Point const &p01, + Geom::Point const &p10, Geom::Point const &p11, + Geom::Point &result) +{ + if(segment_intersectp(p00, p01, p10, p11)) { + Geom::Point n0 = (p01 - p00).ccw(); + double d0 = dot(n0,p00); + + Geom::Point n1 = (p11 - p10).ccw(); + double d1 = dot(n1,p10); + return line_intersection(n0, d0, n1, d1, result); + } else { + return no_intersection; + } +} + +/** Determine whether \& where two line segments intersect. + +If the two segments don't intersect, then \a result remains unchanged. + +\pre neither segment is zero-length; i.e. p00 != p01 and p10 != p11. +**/ +IntersectorKind +line_twopoint_intersect(Geom::Point const &p00, Geom::Point const &p01, + Geom::Point const &p10, Geom::Point const &p11, + Geom::Point &result) +{ + Geom::Point n0 = (p01 - p00).ccw(); + double d0 = dot(n0,p00); + + Geom::Point n1 = (p11 - p10).ccw(); + double d1 = dot(n1,p10); + return line_intersection(n0, d0, n1, d1, result); +} + +// this is used to compare points for std::sort below +static bool +is_less(Point const &A, Point const &B) +{ + if (A[X] < B[X]) { + return true; + } else if (A[X] == B[X] && A[Y] < B[Y]) { + return true; + } else { + return false; + } +} + +// TODO: this can doubtlessly be improved +static void +eliminate_duplicates_p(std::vector &pts) +{ + unsigned int size = pts.size(); + + if (size < 2) + return; + + if (size == 2) { + if (pts[0] == pts[1]) { + pts.pop_back(); + } + } else { + std::sort(pts.begin(), pts.end(), &is_less); + if (size == 3) { + if (pts[0] == pts[1]) { + pts.erase(pts.begin()); + } else if (pts[1] == pts[2]) { + pts.pop_back(); + } + } else { + // we have size == 4 + if (pts[2] == pts[3]) { + pts.pop_back(); + } + if (pts[0] == pts[1]) { + pts.erase(pts.begin()); + } + } + } +} + +/** Determine whether \& where an (infinite) line intersects a rectangle. + * + * \a c0, \a c1 are diagonal corners of the rectangle and + * \a p1, \a p1 are distinct points on the line + * + * \return A list (possibly empty) of points of intersection. If two such points (say \a r0 and \a + * r1) then it is guaranteed that the order of \a r0, \a r1 along the line is the same as the that + * of \a c0, \a c1 (i.e., the vectors \a r1 - \a r0 and \a p1 - \a p0 point into the same + * direction). + */ +std::vector +rect_line_intersect(Geom::Point const &c0, Geom::Point const &c1, + Geom::Point const &p0, Geom::Point const &p1) +{ + using namespace Geom; + + std::vector results; + + Point A(c0); + Point C(c1); + + Point B(A[X], C[Y]); + Point D(C[X], A[Y]); + + Point res; + + if (line_segment_intersect(p0, p1, A, B, res) == intersects) { + results.push_back(res); + } + if (line_segment_intersect(p0, p1, B, C, res) == intersects) { + results.push_back(res); + } + if (line_segment_intersect(p0, p1, C, D, res) == intersects) { + results.push_back(res); + } + if (line_segment_intersect(p0, p1, D, A, res) == intersects) { + results.push_back(res); + } + + eliminate_duplicates_p(results); + + if (results.size() == 2) { + // sort the results so that the order is the same as that of p0 and p1 + Point dir1 (results[1] - results[0]); + Point dir2 (p1 - p0); + if (dot(dir1, dir2) < 0) { + swap(results[0], results[1]); + } + } + + return results; +} + +/** Determine whether \& where an (infinite) line intersects a rectangle. + * + * \a c0, \a c1 are diagonal corners of the rectangle and + * \a p1, \a p1 are distinct points on the line + * + * \return A list (possibly empty) of points of intersection. If two such points (say \a r0 and \a + * r1) then it is guaranteed that the order of \a r0, \a r1 along the line is the same as the that + * of \a c0, \a c1 (i.e., the vectors \a r1 - \a r0 and \a p1 - \a p0 point into the same + * direction). + */ +boost::optional +rect_line_intersect(Geom::Rect &r, + Geom::LineSegment ls) +{ + std::vector results; + + results = rect_line_intersect(r.min(), r.max(), ls[0], ls[1]); + if(results.size() == 2) { + return LineSegment(results[0], results[1]); + } + return boost::optional(); +} + +boost::optional +rect_line_intersect(Geom::Rect &r, + Geom::Line l) +{ + return rect_line_intersect(r, l.segment(0, 1)); +} + +/** + * polyCentroid: Calculates the centroid (xCentroid, yCentroid) and area of a polygon, given its + * vertices (x[0], y[0]) ... (x[n-1], y[n-1]). It is assumed that the contour is closed, i.e., that + * the vertex following (x[n-1], y[n-1]) is (x[0], y[0]). The algebraic sign of the area is + * positive for counterclockwise ordering of vertices in x-y plane; otherwise negative. + + * Returned values: + 0 for normal execution; + 1 if the polygon is degenerate (number of vertices < 3); + 2 if area = 0 (and the centroid is undefined). + + * for now we require the path to be a polyline and assume it is closed. +**/ + +int centroid(std::vector const &p, Geom::Point& centroid, double &area) { + const unsigned n = p.size(); + if (n < 3) + return 1; + Geom::Point centroid_tmp(0,0); + double atmp = 0; + for (unsigned i = n-1, j = 0; j < n; i = j, j++) { + const double ai = cross(p[j], p[i]); + atmp += ai; + centroid_tmp += (p[j] + p[i])*ai; // first moment. + } + area = atmp / 2; + if (atmp != 0) { + centroid = centroid_tmp / (3 * atmp); + return 0; + } + return 2; +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/geom.h b/src/2geom/geom.h new file mode 100644 index 0000000..6ba8122 --- /dev/null +++ b/src/2geom/geom.h @@ -0,0 +1,66 @@ +/** + * \file + * \brief Various geometrical calculations + * + * Authors: + * Nathan Hurst + * + * Copyright (C) 1999-2002 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ +#ifndef LIB2GEOM_SEEN_GEOM_H +#define LIB2GEOM_SEEN_GEOM_H + +//TODO: move somewhere else + +#include +#include <2geom/forward.h> +#include +#include <2geom/bezier-curve.h> +#include <2geom/line.h> + +namespace Geom { + +boost::optional +rect_line_intersect(Geom::Rect &r, + Geom::LineSegment ls); + +int centroid(std::vector const &p, Geom::Point& centroid, double &area); + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/int-interval.h b/src/2geom/int-interval.h new file mode 100644 index 0000000..0faf48d --- /dev/null +++ b/src/2geom/int-interval.h @@ -0,0 +1,63 @@ +/** + * \file + * \brief Closed interval of integer values + *//* + * Copyright 2011 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_INT_INTERVAL_H +#define LIB2GEOM_SEEN_INT_INTERVAL_H + +#include <2geom/coord.h> +#include <2geom/generic-interval.h> + +namespace Geom { + +/** + * @brief Range of integers that is never empty. + * @ingroup Primitives + */ +typedef GenericInterval IntInterval; + +/** + * @brief Range of integers that can be empty. + * @ingroup Primitives + */ +typedef GenericOptInterval OptIntInterval; + +} // namespace Geom +#endif // !LIB2GEOM_SEEN_INT_INTERVAL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/int-point.h b/src/2geom/int-point.h new file mode 100644 index 0000000..50d3a67 --- /dev/null +++ b/src/2geom/int-point.h @@ -0,0 +1,180 @@ +/** + * \file + * \brief Cartesian point / 2D vector with integer coordinates + *//* + * Copyright 2011 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_INT_POINT_H +#define LIB2GEOM_SEEN_INT_POINT_H + +#include +#include +#include <2geom/coord.h> + +namespace Geom { + +/** + * @brief Two-dimensional point with integer coordinates. + * + * This class is an exact equivalent of Point, except it stores integer coordinates. + * Integer points are useful in contexts related to rasterized graphics, for example + * for bounding boxes when rendering SVG. + * + * @see Point + * @ingroup Primitives */ +class IntPoint + : boost::additive< IntPoint + , boost::totally_ordered< IntPoint + > > +{ + IntCoord _pt[2]; +public: + /// @name Creating integer points + /// @{ + IntPoint() { } + IntPoint(IntCoord x, IntCoord y) { + _pt[X] = x; + _pt[Y] = y; + } + /// @} + + /// @name Access the coordinates of a point + /// @{ + IntCoord operator[](unsigned i) const { + if ( i > Y ) throw std::out_of_range("index out of range"); + return _pt[i]; + } + IntCoord &operator[](unsigned i) { + if ( i > Y ) throw std::out_of_range("index out of range"); + return _pt[i]; + } + IntCoord operator[](Dim2 d) const { return _pt[d]; } + IntCoord &operator[](Dim2 d) { return _pt[d]; } + + IntCoord x() const throw() { return _pt[X]; } + IntCoord &x() throw() { return _pt[X]; } + IntCoord y() const throw() { return _pt[Y]; } + IntCoord &y() throw() { return _pt[Y]; } + /// @} + + /// @name Vector-like arithmetic operations + /// @{ + IntPoint operator-() const { + IntPoint ret(-_pt[X], -_pt[Y]); + return ret; + } + IntPoint &operator+=(IntPoint const &o) { + _pt[X] += o._pt[X]; + _pt[Y] += o._pt[Y]; + return *this; + } + IntPoint &operator-=(IntPoint const &o) { + _pt[X] -= o._pt[X]; + _pt[Y] -= o._pt[Y]; + return *this; + } + /// @} + + /// @name Various utilities + /// @{ + /** @brief Equality operator. */ + bool operator==(IntPoint const &in_pnt) const { + return ((_pt[X] == in_pnt[X]) && (_pt[Y] == in_pnt[Y])); + } + /** @brief Lexicographical ordering for points. + * Y coordinate is regarded as more significant. When sorting according to this + * ordering, the points will be sorted according to the Y coordinate, and within + * points with the same Y coordinate according to the X coordinate. */ + bool operator<(IntPoint const &p) const { + return ( ( _pt[Y] < p[Y] ) || + (( _pt[Y] == p[Y] ) && ( _pt[X] < p[X] ))); + } + /// @} + + /** @brief Lexicographical ordering functor. + * @param d The more significant dimension */ + template struct LexLess; + /** @brief Lexicographical ordering functor. + * @param d The more significant dimension */ + template struct LexGreater; + /** @brief Lexicographical ordering functor with runtime dimension. */ + struct LexLessRt { + LexLessRt(Dim2 d) : dim(d) {} + inline bool operator()(IntPoint const &a, IntPoint const &b) const; + private: + Dim2 dim; + }; + /** @brief Lexicographical ordering functor with runtime dimension. */ + struct LexGreaterRt { + LexGreaterRt(Dim2 d) : dim(d) {} + inline bool operator()(IntPoint const &a, IntPoint const &b) const; + private: + Dim2 dim; + }; +}; + +template<> struct IntPoint::LexLess { + bool operator()(IntPoint const &a, IntPoint const &b) const { + return a[X] < b[X] || (a[X] == b[X] && a[Y] < b[Y]); + } +}; +template<> struct IntPoint::LexLess { + bool operator()(IntPoint const &a, IntPoint const &b) const { + return a[Y] < b[Y] || (a[Y] == b[Y] && a[X] < b[X]); + } +}; +template<> struct IntPoint::LexGreater { + bool operator()(IntPoint const &a, IntPoint const &b) const { + return a[X] > b[X] || (a[X] == b[X] && a[Y] > b[Y]); + } +}; +template<> struct IntPoint::LexGreater { + bool operator()(IntPoint const &a, IntPoint const &b) const { + return a[Y] > b[Y] || (a[Y] == b[Y] && a[X] > b[X]); + } +}; +inline bool IntPoint::LexLessRt::operator()(IntPoint const &a, IntPoint const &b) const { + return dim ? IntPoint::LexLess()(a, b) : IntPoint::LexLess()(a, b); +} +inline bool IntPoint::LexGreaterRt::operator()(IntPoint const &a, IntPoint const &b) const { + return dim ? IntPoint::LexGreater()(a, b) : IntPoint::LexGreater()(a, b); +} + +} // namespace Geom + +#endif // !SEEN_GEOM_INT_POINT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/int-rect.h b/src/2geom/int-rect.h new file mode 100644 index 0000000..567d42d --- /dev/null +++ b/src/2geom/int-rect.h @@ -0,0 +1,75 @@ +/** + * \file + * \brief Axis-aligned rectangle with integer coordinates + *//* + * Copyright 2011 Krzysztof KosiÅ„ski + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_INT_RECT_H +#define LIB2GEOM_SEEN_INT_RECT_H + +#include <2geom/coord.h> +#include <2geom/int-interval.h> +#include <2geom/generic-rect.h> + +namespace Geom { + +typedef GenericRect IntRect; +typedef GenericOptRect OptIntRect; + +// the functions below do not work when defined generically +inline OptIntRect operator&(IntRect const &a, IntRect const &b) { + OptIntRect ret(a); + ret.intersectWith(b); + return ret; +} +inline OptIntRect intersect(IntRect const &a, IntRect const &b) { + return a & b; +} +inline OptIntRect intersect(OptIntRect const &a, OptIntRect const &b) { + return a & b; +} +inline IntRect unify(IntRect const &a, IntRect const &b) { + return a | b; +} +inline OptIntRect unify(OptIntRect const &a, OptIntRect const &b) { + return a | b; +} + +} // end namespace Geom + +#endif // !LIB2GEOM_SEEN_INT_RECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/intersection-graph.cpp b/src/2geom/intersection-graph.cpp new file mode 100644 index 0000000..1d06552 --- /dev/null +++ b/src/2geom/intersection-graph.cpp @@ -0,0 +1,494 @@ +/** + * \file + * \brief Intersection graph for Boolean operations + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/intersection-graph.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/utils.h> +#include +#include + +namespace Geom { + +struct PathIntersectionGraph::IntersectionVertexLess { + bool operator()(IntersectionVertex const &a, IntersectionVertex const &b) const { + return a.pos < b.pos; + } +}; + +/** @class PathIntersectionGraph + * @brief Intermediate data for computing Boolean operations on paths. + * + * This class implements the Greiner-Hormann clipping algorithm, + * with improvements inspired by Foster and Overfelt as well as some + * original contributions. + * + * @ingroup Paths + */ + +PathIntersectionGraph::PathIntersectionGraph(PathVector const &a, PathVector const &b, Coord precision) + : _graph_valid(true) +{ + if (a.empty() || b.empty()) return; + + _pv[0] = a; + _pv[1] = b; + + _prepareArguments(); + bool has_intersections = _prepareIntersectionLists(precision); + if (!has_intersections) return; + + _assignEdgeWindingParities(precision); + _assignComponentStatusFromDegenerateIntersections(); + _removeDegenerateIntersections(); + if (_graph_valid) { + _verify(); + } +} + +void PathIntersectionGraph::_prepareArguments() +{ + // all paths must be closed, otherwise we will miss some intersections + for (int w = 0; w < 2; ++w) { + for (std::size_t i = 0; i < _pv[w].size(); ++i) { + _pv[w][i].close(); + } + } + // remove degenerate segments + for (int w = 0; w < 2; ++w) { + for (std::size_t i = _pv[w].size(); i > 0; --i) { + if (_pv[w][i-1].empty()) { + _pv[w].erase(_pv[w].begin() + (i-1)); + continue; + } + for (std::size_t j = _pv[w][i-1].size(); j > 0; --j) { + if (_pv[w][i-1][j-1].isDegenerate()) { + _pv[w][i-1].erase(_pv[w][i-1].begin() + (j-1)); + } + } + } + } +} + +bool PathIntersectionGraph::_prepareIntersectionLists(Coord precision) +{ + std::vector pxs = _pv[0].intersect(_pv[1], precision); + // NOTE: this early return means that the path data structures will not be created + // if there are no intersections at all! + if (pxs.empty()) return false; + + // prepare intersection lists for each path component + for (unsigned w = 0; w < 2; ++w) { + for (std::size_t i = 0; i < _pv[w].size(); ++i) { + _components[w].push_back(new PathData(w, i)); + } + } + + // create intersection vertices + for (std::size_t i = 0; i < pxs.size(); ++i) { + IntersectionVertex *xa, *xb; + xa = new IntersectionVertex(); + xb = new IntersectionVertex(); + //xa->processed = xb->processed = false; + xa->which = 0; xb->which = 1; + xa->pos = pxs[i].first; + xb->pos = pxs[i].second; + xa->p = xb->p = pxs[i].point(); + xa->neighbor = xb; + xb->neighbor = xa; + xa->next_edge = xb->next_edge = OUTSIDE; + xa->defective = xb->defective = false; + _xs.push_back(xa); + _xs.push_back(xb); + _components[0][xa->pos.path_index].xlist.push_back(*xa); + _components[1][xb->pos.path_index].xlist.push_back(*xb); + } + + // sort components according to time value of intersections + for (unsigned w = 0; w < 2; ++w) { + for (std::size_t i = 0; i < _components[w].size(); ++i) { + _components[w][i].xlist.sort(IntersectionVertexLess()); + } + } + + return true; +} + +void PathIntersectionGraph::_assignEdgeWindingParities(Coord precision) +{ + // determine the winding numbers of path portions between intersections + for (unsigned w = 0; w < 2; ++w) { + unsigned ow = (w+1) % 2; + + for (unsigned li = 0; li < _components[w].size(); ++li) { + IntersectionList &xl = _components[w][li].xlist; + for (ILIter i = xl.begin(); i != xl.end(); ++i) { + ILIter n = cyclic_next(i, xl); + std::size_t pi = i->pos.path_index; + + PathInterval ival = forward_interval(i->pos, n->pos, _pv[w][pi].size()); + PathTime mid = ival.inside(precision); + + Point wpoint = _pv[w][pi].pointAt(mid); + _winding_points.push_back(wpoint); + int wdg = _pv[ow].winding(wpoint); + if (wdg % 2) { + i->next_edge = INSIDE; + } else { + i->next_edge = OUTSIDE; + } + } + } + } +} + +void PathIntersectionGraph::_assignComponentStatusFromDegenerateIntersections() +{ + // If a path has only degenerate intersections, assign its status now. + // This protects against later accidentally picking a point for winding + // determination that is exactly at a removed intersection. + for (unsigned w = 0; w < 2; ++w) { + for (unsigned li = 0; li < _components[w].size(); ++li) { + IntersectionList &xl = _components[w][li].xlist; + bool has_in = false; + bool has_out = false; + for (ILIter i = xl.begin(); i != xl.end(); ++i) { + has_in |= (i->next_edge == INSIDE); + has_out |= (i->next_edge == OUTSIDE); + } + if (has_in && !has_out) { + _components[w][li].status = INSIDE; + } + if (!has_in && has_out) { + _components[w][li].status = OUTSIDE; + } + } + } +} + +void PathIntersectionGraph::_removeDegenerateIntersections() +{ + // remove intersections that don't change between in/out + for (unsigned w = 0; w < 2; ++w) { + for (unsigned li = 0; li < _components[w].size(); ++li) { + IntersectionList &xl = _components[w][li].xlist; + for (ILIter i = xl.begin(); i != xl.end();) { + ILIter n = cyclic_next(i, xl); + if (i->next_edge == n->next_edge) { + bool last_node = (i == n); + ILIter nn = _getNeighbor(n); + IntersectionList &oxl = _getPathData(nn).xlist; + + // When exactly 3 out of 4 edges adjacent to an intersection + // have the same winding, we have a defective intersection, + // which is neither degenerate nor normal. Those can occur in paths + // that contain overlapping segments. We cannot handle that case + // for now, so throw an exception. + if (cyclic_prior(nn, oxl)->next_edge != nn->next_edge) { + _graph_valid = false; + n->defective = true; + nn->defective = true; + ++i; + continue; + } + + oxl.erase(nn); + xl.erase(n); + if (last_node) break; + } else { + ++i; + } + } + } + } +} + +void PathIntersectionGraph::_verify() +{ + for (unsigned w = 0; w < 2; ++w) { + for (unsigned li = 0; li < _components[w].size(); ++li) { + IntersectionList &xl = _components[w][li].xlist; + assert(xl.size() % 2 == 0); + for (ILIter i = xl.begin(); i != xl.end(); ++i) { + ILIter j = cyclic_next(i, xl); + assert(i->next_edge != j->next_edge); + } + } + } +} + +PathVector PathIntersectionGraph::getUnion() +{ + PathVector result = _getResult(false, false); + _handleNonintersectingPaths(result, 0, false); + _handleNonintersectingPaths(result, 1, false); + return result; +} + +PathVector PathIntersectionGraph::getIntersection() +{ + PathVector result = _getResult(true, true); + _handleNonintersectingPaths(result, 0, true); + _handleNonintersectingPaths(result, 1, true); + return result; +} + +PathVector PathIntersectionGraph::getAminusB() +{ + PathVector result = _getResult(false, true); + _handleNonintersectingPaths(result, 0, false); + _handleNonintersectingPaths(result, 1, true); + return result; +} + +PathVector PathIntersectionGraph::getBminusA() +{ + PathVector result = _getResult(true, false); + _handleNonintersectingPaths(result, 1, false); + _handleNonintersectingPaths(result, 0, true); + return result; +} + +PathVector PathIntersectionGraph::getXOR() +{ + PathVector r1, r2; + r1 = getAminusB(); + r2 = getBminusA(); + std::copy(r2.begin(), r2.end(), std::back_inserter(r1)); + return r1; +} + +std::size_t PathIntersectionGraph::size() const +{ + std::size_t result = 0; + for (std::size_t i = 0; i < _components[0].size(); ++i) { + result += _components[0][i].xlist.size(); + } + return result; +} + +std::vector PathIntersectionGraph::intersectionPoints(bool defective) const +{ + std::vector result; + + typedef IntersectionList::const_iterator CILIter; + for (std::size_t i = 0; i < _components[0].size(); ++i) { + for (CILIter j = _components[0][i].xlist.begin(); j != _components[0][i].xlist.end(); ++j) { + if (j->defective == defective) { + result.push_back(j->p); + } + } + } + return result; +} + +void PathIntersectionGraph::fragments(PathVector &in, PathVector &out) const +{ + typedef boost::ptr_vector::const_iterator PIter; + for (unsigned w = 0; w < 2; ++w) { + for (PIter li = _components[w].begin(); li != _components[w].end(); ++li) { + for (CILIter k = li->xlist.begin(); k != li->xlist.end(); ++k) { + CILIter n = cyclic_next(k, li->xlist); + // TODO: investigate why non-contiguous paths are sometimes generated here + Path frag(k->p); + frag.setStitching(true); + PathInterval ival = forward_interval(k->pos, n->pos, _pv[w][k->pos.path_index].size()); + _pv[w][k->pos.path_index].appendPortionTo(frag, ival, k->p, n->p); + if (k->next_edge == INSIDE) { + in.push_back(frag); + } else { + out.push_back(frag); + } + } + } + } +} + +PathVector PathIntersectionGraph::_getResult(bool enter_a, bool enter_b) +{ + typedef boost::ptr_vector::iterator PIter; + PathVector result; + if (_xs.empty()) return result; + + // reset processed status + _ulist.clear(); + for (unsigned w = 0; w < 2; ++w) { + for (PIter li = _components[w].begin(); li != _components[w].end(); ++li) { + for (ILIter k = li->xlist.begin(); k != li->xlist.end(); ++k) { + _ulist.push_back(*k); + } + } + } + + unsigned n_processed = 0; + + while (true) { + // get unprocessed intersection + if (_ulist.empty()) break; + IntersectionVertex &iv = _ulist.front(); + unsigned w = iv.which; + ILIter i = _components[w][iv.pos.path_index].xlist.iterator_to(iv); + + result.push_back(Path(i->p)); + result.back().setStitching(true); + + while (i->_proc_hook.is_linked()) { + ILIter prev = i; + std::size_t pi = i->pos.path_index; + // determine which direction to go + // union: always go outside + // intersection: always go inside + // a minus b: go inside in b, outside in a + // b minus a: go inside in a, outside in b + bool reverse = false; + if (w == 0) { + reverse = (i->next_edge == INSIDE) ^ enter_a; + } else { + reverse = (i->next_edge == INSIDE) ^ enter_b; + } + + // get next intersection + if (reverse) { + i = cyclic_prior(i, _components[w][pi].xlist); + } else { + i = cyclic_next(i, _components[w][pi].xlist); + } + + // append portion of path + PathInterval ival = PathInterval::from_direction( + prev->pos.asPathTime(), i->pos.asPathTime(), + reverse, _pv[i->which][pi].size()); + + _pv[i->which][pi].appendPortionTo(result.back(), ival, prev->p, i->p); + + // mark both vertices as processed + //prev->processed = true; + //i->processed = true; + n_processed += 2; + if (prev->_proc_hook.is_linked()) { + _ulist.erase(_ulist.iterator_to(*prev)); + } + if (i->_proc_hook.is_linked()) { + _ulist.erase(_ulist.iterator_to(*i)); + } + + // switch to the other path + i = _getNeighbor(i); + w = i->which; + } + result.back().close(true); + + assert(!result.back().empty()); + } + + /*if (n_processed != size() * 2) { + std::cerr << "Processed " << n_processed << " intersections, expected " << (size() * 2) << std::endl; + }*/ + assert(n_processed == size() * 2); + + return result; +} + +void PathIntersectionGraph::_handleNonintersectingPaths(PathVector &result, unsigned which, bool inside) +{ + /* Every component that has any intersections will be processed by _getResult. + * Here we take care of paths that don't have any intersections. They are either + * completely inside or completely outside the other pathvector. We test this by + * evaluating the winding rule at the initial point. If inside is true and + * the path is inside, we add it to the result. + */ + unsigned w = which; + unsigned ow = (w+1) % 2; + + for (std::size_t i = 0; i < _pv[w].size(); ++i) { + // the path data vector might have been left empty if there were no intersections at all + bool has_path_data = !_components[w].empty(); + // Skip if the path has intersections + if (has_path_data && !_components[w][i].xlist.empty()) continue; + bool path_inside = false; + + // Use the in/out determination from constructor, if available + if (has_path_data && _components[w][i].status == INSIDE) { + path_inside = true; + } else if (has_path_data && _components[w][i].status == OUTSIDE) { + path_inside = false; + } else { + int wdg = _pv[ow].winding(_pv[w][i].initialPoint()); + path_inside = wdg % 2 != 0; + } + + if (path_inside == inside) { + result.push_back(_pv[w][i]); + } + } +} + +PathIntersectionGraph::ILIter PathIntersectionGraph::_getNeighbor(ILIter iter) +{ + unsigned ow = (iter->which + 1) % 2; + return _components[ow][iter->neighbor->pos.path_index].xlist.iterator_to(*iter->neighbor); +} + +PathIntersectionGraph::PathData & +PathIntersectionGraph::_getPathData(ILIter iter) +{ + return _components[iter->which][iter->pos.path_index]; +} + +std::ostream &operator<<(std::ostream &os, PathIntersectionGraph const &pig) +{ + typedef PathIntersectionGraph::IntersectionList::const_iterator CILIter; + os << "Intersection graph:\n" + << pig._xs.size()/2 << " total intersections\n" + << pig.size() << " considered intersections\n"; + for (std::size_t i = 0; i < pig._components[0].size(); ++i) { + PathIntersectionGraph::IntersectionList const &xl = pig._components[0][i].xlist; + for (CILIter j = xl.begin(); j != xl.end(); ++j) { + os << j->pos << " - " << j->neighbor->pos << " @ " << j->p << "\n"; + } + } + return os; +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/intersection-graph.h b/src/2geom/intersection-graph.h new file mode 100644 index 0000000..332c83f --- /dev/null +++ b/src/2geom/intersection-graph.h @@ -0,0 +1,157 @@ +/** + * \file + * \brief Path intersection graph + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef SEEN_LIB2GEOM_INTERSECTION_GRAPH_H +#define SEEN_LIB2GEOM_INTERSECTION_GRAPH_H + +#include +#include +#include +#include +#include <2geom/forward.h> +#include <2geom/pathvector.h> + +namespace Geom { + +class PathIntersectionGraph +{ + // this is called PathIntersectionGraph so that we can also have a class for polygons, + // e.g. PolygonIntersectionGraph, which is going to be significantly faster +public: + PathIntersectionGraph(PathVector const &a, PathVector const &b, Coord precision = EPSILON); + + PathVector getUnion(); + PathVector getIntersection(); + PathVector getAminusB(); + PathVector getBminusA(); + PathVector getXOR(); + + /// Returns the number of intersections used when computing Boolean operations. + std::size_t size() const; + std::vector intersectionPoints(bool defective = false) const; + std::vector windingPoints() const { + return _winding_points; + } + void fragments(PathVector &in, PathVector &out) const; + bool valid() const { return _graph_valid; } + +private: + enum InOutFlag { + INSIDE, + OUTSIDE, + BOTH + }; + + struct IntersectionVertex { + boost::intrusive::list_member_hook<> _hook; + boost::intrusive::list_member_hook<> _proc_hook; + PathVectorTime pos; + Point p; // guarantees that endpoints are exact + IntersectionVertex *neighbor; + InOutFlag next_edge; + unsigned which; + bool defective; + }; + + typedef boost::intrusive::list + < IntersectionVertex + , boost::intrusive::member_hook + < IntersectionVertex + , boost::intrusive::list_member_hook<> + , &IntersectionVertex::_hook + > + > IntersectionList; + + typedef boost::intrusive::list + < IntersectionVertex + , boost::intrusive::member_hook + < IntersectionVertex + , boost::intrusive::list_member_hook<> + , &IntersectionVertex::_proc_hook + > + > UnprocessedList; + + struct PathData { + IntersectionList xlist; + std::size_t path_index; + int which; + InOutFlag status; + + PathData(int w, std::size_t pi) + : path_index(pi) + , which(w) + , status(BOTH) + {} + }; + + struct IntersectionVertexLess; + typedef IntersectionList::iterator ILIter; + typedef IntersectionList::const_iterator CILIter; + + PathVector _getResult(bool enter_a, bool enter_b); + void _handleNonintersectingPaths(PathVector &result, unsigned which, bool inside); + void _prepareArguments(); + bool _prepareIntersectionLists(Coord precision); + void _assignEdgeWindingParities(Coord precision); + void _assignComponentStatusFromDegenerateIntersections(); + void _removeDegenerateIntersections(); + void _verify(); + + ILIter _getNeighbor(ILIter iter); + PathData &_getPathData(ILIter iter); + + PathVector _pv[2]; + boost::ptr_vector _xs; + boost::ptr_vector _components[2]; + UnprocessedList _ulist; + bool _graph_valid; + std::vector _winding_points; + + friend std::ostream &operator<<(std::ostream &, PathIntersectionGraph const &); +}; + +std::ostream &operator<<(std::ostream &os, PathIntersectionGraph const &pig); + +} // namespace Geom + +#endif // SEEN_LIB2GEOM_PATH_GRAPH_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/intersection.h b/src/2geom/intersection.h new file mode 100644 index 0000000..bbce199 --- /dev/null +++ b/src/2geom/intersection.h @@ -0,0 +1,147 @@ +/** + * \file + * \brief Intersection utilities + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef SEEN_LIB2GEOM_INTERSECTION_H +#define SEEN_LIB2GEOM_INTERSECTION_H + +#include <2geom/coord.h> +#include <2geom/point.h> + +namespace Geom { + + +/** @brief Intersection between two shapes. + */ +template +class Intersection + : boost::totally_ordered< Intersection > +{ +public: + /** @brief Construct from shape references and time values. + * By default, the intersection point will be halfway between the evaluated + * points on the two shapes. */ + template + Intersection(TA const &sa, TB const &sb, TimeA const &ta, TimeB const &tb) + : first(ta) + , second(tb) + , _point(lerp(0.5, sa.pointAt(ta), sb.pointAt(tb))) + {} + + /// Additionally report the intersection point. + Intersection(TimeA const &ta, TimeB const &tb, Point const &p) + : first(ta) + , second(tb) + , _point(p) + {} + + /// Intersection point, as calculated by the intersection algorithm. + Point point() const { + return _point; + } + /// Implicit conversion to Point. + operator Point() const { + return _point; + } + + friend inline void swap(Intersection &a, Intersection &b) { + using std::swap; + swap(a.first, b.first); + swap(a.second, b.second); + swap(a._point, b._point); + } + + bool operator==(Intersection const &other) const { + if (first != other.first) return false; + if (second != other.second) return false; + return true; + } + bool operator<(Intersection const &other) const { + if (first < other.first) return true; + if (first == other.first && second < other.second) return true; + return false; + } + +public: + /// First shape and time value. + TimeA first; + /// Second shape and time value. + TimeB second; +private: + // Recalculation of the intersection point from the time values is in many cases + // less precise than the value obtained directly from the intersection algorithm, + // so we need to store it. + Point _point; +}; + + +// TODO: move into new header? +template +struct ShapeTraits { + typedef Coord TimeType; + typedef Interval IntervalType; + typedef T AffineClosureType; + typedef Intersection<> IntersectionType; +}; + +template inline +std::vector< Intersection > transpose(std::vector< Intersection > const &in) { + std::vector< Intersection > result; + for (std::size_t i = 0; i < in.size(); ++i) { + result.push_back(Intersection(in[i].second, in[i].first, in[i].point())); + } + return result; +} + +template inline +void transpose_in_place(std::vector< Intersection > &xs) { + for (std::size_t i = 0; i < xs.size(); ++i) { + std::swap(xs[i].first, xs[i].second); + } +} + +typedef Intersection<> ShapeIntersection; + + +} // namespace Geom + +#endif // SEEN_LIB2GEOM_INTERSECTION_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/interval.h b/src/2geom/interval.h new file mode 100644 index 0000000..5f51428 --- /dev/null +++ b/src/2geom/interval.h @@ -0,0 +1,252 @@ +/** + * \file + * \brief Simple closed interval class + *//* + * Copyright 2007 Michael Sloan + * + * Original Rect/Range code by: + * Lauris Kaplinski + * Nathan Hurst + * bulia byak + * MenTaLguY + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ +#ifndef LIB2GEOM_SEEN_INTERVAL_H +#define LIB2GEOM_SEEN_INTERVAL_H + +#include +#include +#include +#include <2geom/coord.h> +#include <2geom/math-utils.h> +#include <2geom/generic-interval.h> +#include <2geom/int-interval.h> + +namespace Geom { + +/** + * @brief Range of real numbers that is never empty. + * + * Intervals are closed ranges \f$[a, b]\f$, which means they include their endpoints. + * To use them as open ranges, you can use the interiorContains() methods. + * + * @ingroup Primitives + */ +class Interval + : public GenericInterval +{ + typedef GenericInterval Base; +public: + /// @name Create intervals. + /// @{ + /** @brief Create an interval that contains only zero. */ + Interval() {} + /** @brief Create an interval that contains a single point. */ + explicit Interval(Coord u) : Base(u) {} + /** @brief Create an interval that contains all points between @c u and @c v. */ + Interval(Coord u, Coord v) : Base(u,v) {} + /** @brief Convert from integer interval */ + Interval(IntInterval const &i) : Base(i.min(), i.max()) {} + Interval(Base const &b) : Base(b) {} + + /** @brief Create an interval containing a range of values. + * The resulting interval will contain all values from the given range. + * The return type of iterators must be convertible to Coord. The given range + * must not be empty. For potentially empty ranges, see OptInterval. + * @param start Beginning of the range + * @param end End of the range + * @return Interval that contains all values from [start, end). */ + template + static Interval from_range(InputIterator start, InputIterator end) { + Interval result = Base::from_range(start, end); + return result; + } + /** @brief Create an interval from a C-style array of values it should contain. */ + static Interval from_array(Coord const *c, unsigned n) { + Interval result = from_range(c, c+n); + return result; + } + /// @} + + /// @name Inspect contained values. + /// @{ + /// Check whether both endpoints are finite. + bool isFinite() const { + return std::isfinite(min()) && std::isfinite(max()); + } + /** @brief Map the interval [0,1] onto this one. + * This method simply performs 1D linear interpolation between endpoints. */ + Coord valueAt(Coord t) { + return lerp(t, min(), max()); + } + /** @brief Compute a time value that maps to the given value. + * The supplied value does not need to be in the interval for this method to work. */ + Coord timeAt(Coord v) { + return (v - min()) / extent(); + } + /// Find closest time in [0,1] that maps to the given value. */ + Coord nearestTime(Coord v) { + if (v <= min()) return 0; + if (v >= max()) return 1; + return timeAt(v); + } + /// @} + + /// @name Test coordinates and other intervals for inclusion. + /// @{ + /** @brief Check whether the interior of the interval includes this number. + * Interior means all numbers in the interval except its ends. */ + bool interiorContains(Coord val) const { return min() < val && val < max(); } + /** @brief Check whether the interior of the interval includes the given interval. + * Interior means all numbers in the interval except its ends. */ + bool interiorContains(Interval const &val) const { return min() < val.min() && val.max() < max(); } + /// Check whether the number is contained in the union of the interior and the lower boundary. + bool lowerContains(Coord val) { return min() <= val && val < max(); } + /// Check whether the given interval is contained in the union of the interior and the lower boundary. + bool lowerContains(Interval const &val) const { return min() <= val.min() && val.max() < max(); } + /// Check whether the number is contained in the union of the interior and the upper boundary. + bool upperContains(Coord val) { return min() < val && val <= max(); } + /// Check whether the given interval is contained in the union of the interior and the upper boundary. + bool upperContains(Interval const &val) const { return min() < val.min() && val.max() <= max(); } + /** @brief Check whether the interiors of the intervals have any common elements. + * A single point in common is not considered an intersection. */ + bool interiorIntersects(Interval const &val) const { + return std::max(min(), val.min()) < std::min(max(), val.max()); + } + /// @} + + /// @name Operators + /// @{ + // IMPL: ScalableConcept + /** @brief Scale an interval */ + Interval &operator*=(Coord s) { + using std::swap; + _b[0] *= s; + _b[1] *= s; + if(s < 0) swap(_b[0], _b[1]); + return *this; + } + /** @brief Scale an interval by the inverse of the specified value */ + Interval &operator/=(Coord s) { + using std::swap; + _b[0] /= s; + _b[1] /= s; + if(s < 0) swap(_b[0], _b[1]); + return *this; + } + /** @brief Multiply two intervals. + * Product is defined as the set of points that can be obtained by multiplying + * any value from the second operand by any value from the first operand: + * \f$S = \{x \in A, y \in B: x * y\}\f$ */ + Interval &operator*=(Interval const &o) { + // TODO implement properly + Coord mn = min(), mx = max(); + expandTo(mn * o.min()); + expandTo(mn * o.max()); + expandTo(mx * o.min()); + expandTo(mx * o.max()); + return *this; + } + bool operator==(IntInterval const &ii) const { + return min() == Coord(ii.min()) && max() == Coord(ii.max()); + } + bool operator==(Interval const &other) const { + return Base::operator==(other); + } + /// @} + + /// @name Rounding to integer values + /// @{ + /** @brief Return the smallest integer interval which contains this one. */ + IntInterval roundOutwards() const { + IntInterval ret(floor(min()), ceil(max())); + return ret; + } + /** @brief Return the largest integer interval which is contained in this one. */ + OptIntInterval roundInwards() const { + IntCoord u = ceil(min()), v = floor(max()); + if (u > v) { OptIntInterval e; return e; } + IntInterval ret(u, v); + return ret; + } + /// @} +}; + +/** + * @brief Range of real numbers that can be empty. + * @ingroup Primitives + */ +class OptInterval + : public GenericOptInterval +{ + typedef GenericOptInterval Base; +public: + /// @name Create optionally empty intervals. + /// @{ + /** @brief Create an empty interval. */ + OptInterval() : Base() {} + /** @brief Wrap an existing interval. */ + OptInterval(Interval const &a) : Base(a) {} + /** @brief Create an interval containing a single point. */ + OptInterval(Coord u) : Base(u) {} + /** @brief Create an interval containing a range of numbers. */ + OptInterval(Coord u, Coord v) : Base(u,v) {} + OptInterval(Base const &b) : Base(b) {} + + /** @brief Promote from IntInterval. */ + OptInterval(IntInterval const &i) : Base(Interval(i)) {} + /** @brief Promote from OptIntInterval. */ + OptInterval(OptIntInterval const &i) : Base() { + if (i) *this = Interval(*i); + } +}; + +// functions required for Python bindings +inline Interval unify(Interval const &a, Interval const &b) +{ + Interval r = a | b; + return r; +} +inline OptInterval intersect(Interval const &a, Interval const &b) +{ + OptInterval r = a & b; + return r; +} + +} // end namespace Geom + +#endif //SEEN_INTERVAL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/line.cpp b/src/2geom/line.cpp new file mode 100644 index 0000000..ce9b9cc --- /dev/null +++ b/src/2geom/line.cpp @@ -0,0 +1,609 @@ +/* + * Infinite Straight Line + * + * Copyright 2008 Marco Cecchetti + * Nathan Hurst + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include <2geom/line.h> +#include <2geom/math-utils.h> + +namespace Geom +{ + +/** + * @class Line + * @brief Infinite line on a plane. + * + * A line is specified as two points through which it passes. Lines can be interpreted as functions + * \f$ f: (-\infty, \infty) \to \mathbb{R}^2\f$. Zero corresponds to the first (origin) point, + * one corresponds to the second (final) point. All other points are computed as a linear + * interpolation between those two: \f$p = (1-t) a + t b\f$. Many such functions have the same + * image and therefore represent the same lines; for example, adding \f$b-a\f$ to both points + * yields the same line. + * + * 2Geom can represent the same line in many ways by design: using a different representation + * would lead to precision loss. For example, a line from (1e30, 1e30) to (10,0) would actually + * evaluate to (0,0) at time 1 if it was stored as origin and normalized versor, + * or origin and angle. + * + * @ingroup Primitives + */ + +/** @brief Set the line by solving the line equation. + * A line is a set of points that satisfies the line equation + * \f$Ax + By + C = 0\f$. This function changes the line so that its points + * satisfy the line equation with the given coefficients. */ +void Line::setCoefficients (Coord a, Coord b, Coord c) +{ + // degenerate case + if (a == 0 && b == 0) { + if (c != 0) { + THROW_LOGICALERROR("the passed coefficients give the empty set"); + } + _initial = Point(0,0); + _final = Point(0,0); + return; + } + + // The way final / initial points are set based on coefficients is somewhat unusual. + // This is done to make sure that calling coefficients() will give back + // (almost) the same values. + + // vertical case + if (a == 0) { + // b must be nonzero + _initial = Point(-b/2, -c / b); + _final = _initial; + _final[X] = b/2; + return; + } + + // horizontal case + if (b == 0) { + _initial = Point(-c / a, a/2); + _final = _initial; + _final[Y] = -a/2; + return; + } + + // This gives reasonable results regardless of the magnitudes of a, b and c. + _initial = Point(-b/2,a/2); + _final = Point(b/2,-a/2); + + Point offset(-c/(2*a), -c/(2*b)); + + _initial += offset; + _final += offset; +} + +void Line::coefficients(Coord &a, Coord &b, Coord &c) const +{ + Point v = vector().cw(); + a = v[X]; + b = v[Y]; + c = cross(_initial, _final); +} + +/** @brief Get the implicit line equation coefficients. + * Note that conversion to implicit form always causes loss of + * precision when dealing with lines that start far from the origin + * and end very close to it. It is recommended to normalize the line + * before converting it to implicit form. + * @return Vector with three values corresponding to the A, B and C + * coefficients of the line equation for this line. */ +std::vector Line::coefficients() const +{ + std::vector c(3); + coefficients(c[0], c[1], c[2]); + return c; +} + +/** @brief Find intersection with an axis-aligned line. + * @param v Coordinate of the axis-aligned line + * @param d Which axis the coordinate is on. X means a vertical line, Y means a horizontal line. + * @return Time values at which this line intersects the query line. */ +std::vector Line::roots(Coord v, Dim2 d) const { + std::vector result; + Coord r = root(v, d); + if (std::isfinite(r)) { + result.push_back(r); + } + return result; +} + +Coord Line::root(Coord v, Dim2 d) const +{ + assert(d == X || d == Y); + Point vs = vector(); + if (vs[d] != 0) { + return (v - _initial[d]) / vs[d]; + } else { + return nan(""); + } +} + +boost::optional Line::clip(Rect const &r) const +{ + Point v = vector(); + // handle horizontal and vertical lines first, + // since the root-based code below will break for them + for (unsigned i = 0; i < 2; ++i) { + Dim2 d = (Dim2) i; + Dim2 o = other_dimension(d); + if (v[d] != 0) continue; + if (r[d].contains(_initial[d])) { + Point a, b; + a[o] = r[o].min(); + b[o] = r[o].max(); + a[d] = b[d] = _initial[d]; + if (v[o] > 0) { + return LineSegment(a, b); + } else { + return LineSegment(b, a); + } + } else { + return boost::none; + } + } + + Interval xpart(root(r[X].min(), X), root(r[X].max(), X)); + Interval ypart(root(r[Y].min(), Y), root(r[Y].max(), Y)); + if (!xpart.isFinite() || !ypart.isFinite()) { + return boost::none; + } + + OptInterval common = xpart & ypart; + if (common) { + Point p1 = pointAt(common->min()), p2 = pointAt(common->max()); + LineSegment result(r.clamp(p1), r.clamp(p2)); + return result; + } else { + return boost::none; + } + + /* old implementation using coefficients: + + if (fabs(b) > fabs(a)) { + p0 = Point(r[X].min(), (-c - a*r[X].min())/b); + if (p0[Y] < r[Y].min()) + p0 = Point((-c - b*r[Y].min())/a, r[Y].min()); + if (p0[Y] > r[Y].max()) + p0 = Point((-c - b*r[Y].max())/a, r[Y].max()); + p1 = Point(r[X].max(), (-c - a*r[X].max())/b); + if (p1[Y] < r[Y].min()) + p1 = Point((-c - b*r[Y].min())/a, r[Y].min()); + if (p1[Y] > r[Y].max()) + p1 = Point((-c - b*r[Y].max())/a, r[Y].max()); + } else { + p0 = Point((-c - b*r[Y].min())/a, r[Y].min()); + if (p0[X] < r[X].min()) + p0 = Point(r[X].min(), (-c - a*r[X].min())/b); + if (p0[X] > r[X].max()) + p0 = Point(r[X].max(), (-c - a*r[X].max())/b); + p1 = Point((-c - b*r[Y].max())/a, r[Y].max()); + if (p1[X] < r[X].min()) + p1 = Point(r[X].min(), (-c - a*r[X].min())/b); + if (p1[X] > r[X].max()) + p1 = Point(r[X].max(), (-c - a*r[X].max())/b); + } + return LineSegment(p0, p1); */ +} + +/** @brief Get a time value corresponding to a point. + * @param p Point on the line. If the point is not on the line, + * the returned value will be meaningless. + * @return Time value t such that \f$f(t) = p\f$. + * @see timeAtProjection */ +Coord Line::timeAt(Point const &p) const +{ + Point v = vector(); + // degenerate case + if (v[X] == 0 && v[Y] == 0) { + return 0; + } + + // use the coordinate that will give better precision + if (fabs(v[X]) > fabs(v[Y])) { + return (p[X] - _initial[X]) / v[X]; + } else { + return (p[Y] - _initial[Y]) / v[Y]; + } +} + +/** @brief Create a transformation that maps one line to another. + * This will return a transformation \f$A\f$ such that + * \f$L_1(t) \cdot A = L_2(t)\f$, where \f$L_1\f$ is this line + * and \f$L_2\f$ is the line passed as the parameter. The returned + * transformation will preserve angles. */ +Affine Line::transformTo(Line const &other) const +{ + Affine result = Translate(-_initial); + result *= Rotate(angle_between(vector(), other.vector())); + result *= Scale(other.vector().length() / vector().length()); + result *= Translate(other._initial); + return result; +} + +std::vector Line::intersect(Line const &other) const +{ + std::vector result; + + Point v1 = vector(); + Point v2 = other.vector(); + Coord cp = cross(v1, v2); + if (cp == 0) return result; + + Point odiff = other.initialPoint() - initialPoint(); + Coord t1 = cross(odiff, v2) / cp; + Coord t2 = cross(odiff, v1) / cp; + result.push_back(ShapeIntersection(*this, other, t1, t2)); + return result; +} + +std::vector Line::intersect(Ray const &r) const +{ + Line other(r); + std::vector result = intersect(other); + filter_ray_intersections(result, false, true); + return result; +} + +std::vector Line::intersect(LineSegment const &ls) const +{ + Line other(ls); + std::vector result = intersect(other); + filter_line_segment_intersections(result, false, true); + return result; +} + + + +void filter_line_segment_intersections(std::vector &xs, bool a, bool b) +{ + Interval unit(0, 1); + std::vector::reverse_iterator i = xs.rbegin(), last = xs.rend(); + while (i != last) { + if ((a && !unit.contains(i->first)) || (b && !unit.contains(i->second))) { + xs.erase((++i).base()); + } else { + ++i; + } + } +} + +void filter_ray_intersections(std::vector &xs, bool a, bool b) +{ + Interval unit(0, 1); + std::vector::reverse_iterator i = xs.rbegin(), last = xs.rend(); + while (i != last) { + if ((a && i->first < 0) || (b && i->second < 0)) { + xs.erase((++i).base()); + } else { + ++i; + } + } +} + +namespace detail +{ + +inline +OptCrossing intersection_impl(Point const &v1, Point const &o1, + Point const &v2, Point const &o2) +{ + Coord cp = cross(v1, v2); + if (cp == 0) return OptCrossing(); + + Point odiff = o2 - o1; + + Crossing c; + c.ta = cross(odiff, v2) / cp; + c.tb = cross(odiff, v1) / cp; + return c; +} + + +OptCrossing intersection_impl(Ray const& r1, Line const& l2, unsigned int i) +{ + using std::swap; + + OptCrossing crossing = + intersection_impl(r1.vector(), r1.origin(), + l2.vector(), l2.origin() ); + + if (crossing) { + if (crossing->ta < 0) { + return OptCrossing(); + } else { + if (i != 0) { + swap(crossing->ta, crossing->tb); + } + return crossing; + } + } + if (are_near(r1.origin(), l2)) { + THROW_INFINITESOLUTIONS(); + } else { + return OptCrossing(); + } +} + + +OptCrossing intersection_impl( LineSegment const& ls1, + Line const& l2, + unsigned int i ) +{ + using std::swap; + + OptCrossing crossing = + intersection_impl(ls1.finalPoint() - ls1.initialPoint(), + ls1.initialPoint(), + l2.vector(), + l2.origin() ); + + if (crossing) { + if ( crossing->getTime(0) < 0 + || crossing->getTime(0) > 1 ) + { + return OptCrossing(); + } else { + if (i != 0) { + swap((*crossing).ta, (*crossing).tb); + } + return crossing; + } + } + if (are_near(ls1.initialPoint(), l2)) { + THROW_INFINITESOLUTIONS(); + } else { + return OptCrossing(); + } +} + + +OptCrossing intersection_impl( LineSegment const& ls1, + Ray const& r2, + unsigned int i ) +{ + using std::swap; + + Point direction = ls1.finalPoint() - ls1.initialPoint(); + OptCrossing crossing = + intersection_impl( direction, + ls1.initialPoint(), + r2.vector(), + r2.origin() ); + + if (crossing) { + if ( (crossing->getTime(0) < 0) + || (crossing->getTime(0) > 1) + || (crossing->getTime(1) < 0) ) + { + return OptCrossing(); + } else { + if (i != 0) { + swap(crossing->ta, crossing->tb); + } + return crossing; + } + } + + if ( are_near(r2.origin(), ls1) ) { + bool eqvs = (dot(direction, r2.vector()) > 0); + if ( are_near(ls1.initialPoint(), r2.origin()) && !eqvs) { + crossing->ta = crossing->tb = 0; + return crossing; + } else if ( are_near(ls1.finalPoint(), r2.origin()) && eqvs) { + if (i == 0) { + crossing->ta = 1; + crossing->tb = 0; + } else { + crossing->ta = 0; + crossing->tb = 1; + } + return crossing; + } else { + THROW_INFINITESOLUTIONS(); + } + } else if ( are_near(ls1.initialPoint(), r2) ) { + THROW_INFINITESOLUTIONS(); + } else { + OptCrossing no_crossing; + return no_crossing; + } +} + +} // end namespace detail + + + +OptCrossing intersection(Line const& l1, Line const& l2) +{ + OptCrossing c = detail::intersection_impl( + l1.vector(), l1.origin(), + l2.vector(), l2.origin()); + + if (!c && distance(l1.origin(), l2) == 0) { + THROW_INFINITESOLUTIONS(); + } + return c; +} + +OptCrossing intersection(Ray const& r1, Ray const& r2) +{ + OptCrossing crossing = + detail::intersection_impl( r1.vector(), r1.origin(), + r2.vector(), r2.origin() ); + + if (crossing) + { + if ( crossing->ta < 0 + || crossing->tb < 0 ) + { + OptCrossing no_crossing; + return no_crossing; + } + else + { + return crossing; + } + } + + if ( are_near(r1.origin(), r2) || are_near(r2.origin(), r1) ) + { + if ( are_near(r1.origin(), r2.origin()) + && !are_near(r1.vector(), r2.vector()) ) + { + crossing->ta = crossing->tb = 0; + return crossing; + } + else + { + THROW_INFINITESOLUTIONS(); + } + } + else + { + OptCrossing no_crossing; + return no_crossing; + } +} + + +OptCrossing intersection( LineSegment const& ls1, LineSegment const& ls2 ) +{ + Point direction1 = ls1.finalPoint() - ls1.initialPoint(); + Point direction2 = ls2.finalPoint() - ls2.initialPoint(); + OptCrossing crossing = + detail::intersection_impl( direction1, + ls1.initialPoint(), + direction2, + ls2.initialPoint() ); + + if (crossing) + { + if ( crossing->getTime(0) < 0 + || crossing->getTime(0) > 1 + || crossing->getTime(1) < 0 + || crossing->getTime(1) > 1 ) + { + OptCrossing no_crossing; + return no_crossing; + } + else + { + return crossing; + } + } + + bool eqvs = (dot(direction1, direction2) > 0); + if ( are_near(ls2.initialPoint(), ls1) ) + { + if ( are_near(ls1.initialPoint(), ls2.initialPoint()) && !eqvs ) + { + crossing->ta = crossing->tb = 0; + return crossing; + } + else if ( are_near(ls1.finalPoint(), ls2.initialPoint()) && eqvs ) + { + crossing->ta = 1; + crossing->tb = 0; + return crossing; + } + else + { + THROW_INFINITESOLUTIONS(); + } + } + else if ( are_near(ls2.finalPoint(), ls1) ) + { + if ( are_near(ls1.finalPoint(), ls2.finalPoint()) && !eqvs ) + { + crossing->ta = crossing->tb = 1; + return crossing; + } + else if ( are_near(ls1.initialPoint(), ls2.finalPoint()) && eqvs ) + { + crossing->ta = 0; + crossing->tb = 1; + return crossing; + } + else + { + THROW_INFINITESOLUTIONS(); + } + } + else + { + OptCrossing no_crossing; + return no_crossing; + } +} + +Line make_angle_bisector_line(Line const& l1, Line const& l2) +{ + OptCrossing crossing; + try + { + crossing = intersection(l1, l2); + } + catch(InfiniteSolutions const &e) + { + return l1; + } + if (!crossing) + { + THROW_RANGEERROR("passed lines are parallel"); + } + Point O = l1.pointAt(crossing->ta); + Point A = l1.pointAt(crossing->ta + 1); + double angle = angle_between(l1.vector(), l2.vector()); + Point B = (angle > 0) ? l2.pointAt(crossing->tb + 1) + : l2.pointAt(crossing->tb - 1); + + return make_angle_bisector_line(A, O, B); +} + + + + +} // end namespace Geom + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(substatement-open . 0)) + indent-tabs-mode:nil + c-brace-offset:0 + fill-column:99 + End: + vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : +*/ diff --git a/src/2geom/line.h b/src/2geom/line.h new file mode 100644 index 0000000..5d570d0 --- /dev/null +++ b/src/2geom/line.h @@ -0,0 +1,604 @@ +/** + * \file + * \brief Infinite straight line + *//* + * Authors: + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * Copyright 2008-2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_LINE_H +#define LIB2GEOM_SEEN_LINE_H + +#include +#include +#include <2geom/bezier-curve.h> // for LineSegment +#include <2geom/rect.h> +#include <2geom/crossing.h> +#include <2geom/exception.h> +#include <2geom/ray.h> +#include <2geom/angle.h> +#include <2geom/intersection.h> + +namespace Geom +{ + +// class docs in cpp file +class Line + : boost::equality_comparable< Line > +{ +private: + Point _initial; + Point _final; +public: + /// @name Creating lines. + /// @{ + /** @brief Create a default horizontal line. + * Creates a line with unit speed going in +X direction. */ + Line() + : _initial(0,0), _final(1,0) + {} + /** @brief Create a line with the specified inclination. + * @param origin One of the points on the line + * @param angle Angle of the line in mathematical convention */ + Line(Point const &origin, Coord angle) + : _initial(origin) + { + Point v; + sincos(angle, v[Y], v[X]); + _final = _initial + v; + } + + /** @brief Create a line going through two points. + * The first point will be at time 0, while the second one + * will be at time 1. + * @param a Initial point + * @param b First point */ + Line(Point const &a, Point const &b) + : _initial(a) + , _final(b) + {} + + /** @brief Create a line based on the coefficients of its equation. + @see Line::setCoefficients() */ + Line(double a, double b, double c) { + setCoefficients(a, b, c); + } + + /// Create a line by extending a line segment. + explicit Line(LineSegment const &seg) + : _initial(seg.initialPoint()) + , _final(seg.finalPoint()) + {} + + /// Create a line by extending a ray. + explicit Line(Ray const &r) + : _initial(r.origin()) + , _final(r.origin() + r.vector()) + {} + + /// Create a line normal to a vector at a specified distance from origin. + static Line from_normal_distance(Point const &n, Coord c) { + Point start = c * n.normalized(); + Line l(start, start + rot90(n)); + return l; + } + /** @brief Create a line from origin and unit vector. + * Note that each line direction has two possible unit vectors. + * @param o Point through which the line will pass + * @param v Unit vector of the line's direction */ + static Line from_origin_and_vector(Point const &o, Point const &v) { + Line l(o, o + v); + return l; + } + + Line* duplicate() const { + return new Line(*this); + } + /// @} + + /// @name Retrieve and set the line's parameters. + /// @{ + + /// Get the line's origin point. + Point origin() const { return _initial; } + /** @brief Get the line's raw direction vector. + * The retrieved vector is normalized to unit length. */ + Point vector() const { return _final - _initial; } + /** @brief Get the line's normalized direction vector. + * The retrieved vector is normalized to unit length. */ + Point versor() const { return (_final - _initial).normalized(); } + /// Angle the line makes with the X axis, in mathematical convention. + Coord angle() const { + Point d = _final - _initial; + double a = std::atan2(d[Y], d[X]); + if (a < 0) a += M_PI; + if (a == M_PI) a = 0; + return a; + } + + /** @brief Set the point at zero time. + * The orientation remains unchanged, modulo numeric errors during addition. */ + void setOrigin(Point const &p) { + Point d = p - _initial; + _initial = p; + _final += d; + } + /** @brief Set the speed of the line. + * Origin remains unchanged. */ + void setVector(Point const &v) { + _final = _initial + v; + } + + /** @brief Set the angle the line makes with the X axis. + * Origin remains unchanged. */ + void setAngle(Coord angle) { + Point v; + sincos(angle, v[Y], v[X]); + v *= distance(_initial, _final); + _final = _initial + v; + } + + /// Set a line based on two points it should pass through. + void setPoints(Point const &a, Point const &b) { + _initial = a; + _final = b; + } + + /** @brief Set the coefficients of the line equation. + * The line equation is: \f$ax + by = c\f$. Points that satisfy the equation + * are on the line. */ + void setCoefficients(double a, double b, double c); + + /** @brief Get the coefficients of the line equation as a vector. + * @return STL vector @a v such that @a v[0] contains \f$a\f$, @a v[1] contains \f$b\f$, + * and @a v[2] contains \f$c\f$. */ + std::vector coefficients() const; + + /// Get the coefficients of the line equation by reference. + void coefficients(Coord &a, Coord &b, Coord &c) const; + + /** @brief Check if the line has more than one point. + * A degenerate line can be created if the line is created from a line equation + * that has no solutions. + * @return True if the line has no points or exactly one point */ + bool isDegenerate() const { + return _initial == _final; + } + /// Check if the line is horizontal (y is constant). + bool isHorizontal() const { + return _initial[Y] == _final[Y]; + } + /// Check if the line is vertical (x is constant). + bool isVertical() const { + return _initial[X] == _final[X]; + } + + /** @brief Reparametrize the line so that it has unit speed. + * Note that the direction of the line may also change. */ + void normalize() { + // this helps with the nasty case of a line that starts somewhere far + // and ends very close to the origin + if (L2sq(_final) < L2sq(_initial)) { + std::swap(_initial, _final); + } + Point v = _final - _initial; + v.normalize(); + _final = _initial + v; + } + /** @brief Return a new line reparametrized for unit speed. */ + Line normalized() const { + Point v = _final - _initial; + v.normalize(); + Line ret(_initial, _initial + v); + return ret; + } + /// @} + + /// @name Evaluate the line as a function. + ///@{ + Point initialPoint() const { + return _initial; + } + Point finalPoint() const { + return _final; + } + Point pointAt(Coord t) const { + return lerp(t, _initial, _final);; + } + + Coord valueAt(Coord t, Dim2 d) const { + return lerp(t, _initial[d], _final[d]); + } + + Coord timeAt(Point const &p) const; + + /** @brief Get a time value corresponding to a projection of a point on the line. + * @param p Arbitrary point. + * @return Time value corresponding to a point closest to @c p. */ + Coord timeAtProjection(Point const& p) const { + if ( isDegenerate() ) return 0; + Point v = vector(); + return dot(p - _initial, v) / dot(v, v); + } + + /** @brief Find a point on the line closest to the query point. + * This is an alias for timeAtProjection(). */ + Coord nearestTime(Point const &p) const { + return timeAtProjection(p); + } + + std::vector roots(Coord v, Dim2 d) const; + Coord root(Coord v, Dim2 d) const; + /// @} + + /// @name Create other objects based on this line. + /// @{ + void reverse() { + std::swap(_final, _initial); + } + /** @brief Create a line containing the same points, but in opposite direction. + * @return Line \f$g\f$ such that \f$g(t) = f(1-t)\f$ */ + Line reversed() const { + Line result(_final, _initial); + return result; + } + + /** @brief Same as segment(), but allocate the line segment dynamically. */ + // TODO remove this? + Curve* portion(Coord f, Coord t) const { + LineSegment* seg = new LineSegment(pointAt(f), pointAt(t)); + return seg; + } + + /** @brief Create a segment of this line. + * @param f Time value for the initial point of the segment + * @param t Time value for the final point of the segment + * @return Created line segment */ + LineSegment segment(Coord f, Coord t) const { + return LineSegment(pointAt(f), pointAt(t)); + } + + /// Return the portion of the line that is inside the given rectangle + boost::optional clip(Rect const &r) const; + + /** @brief Create a ray starting at the specified time value. + * The created ray will go in the direction of the line's vector (in the direction + * of increasing time values). + * @param t Time value where the ray should start + * @return Ray starting at t and going in the direction of the vector */ + Ray ray(Coord t) { + Ray result; + result.setOrigin(pointAt(t)); + result.setVector(vector()); + return result; + } + + /** @brief Create a derivative of the line. + * The new line will always be degenerate. Its origin will be equal to this + * line's vector. */ + Line derivative() const { + Point v = vector(); + Line result(v, v); + return result; + } + + /// Create a line transformed by an affine transformation. + Line transformed(Affine const& m) const { + Line l(_initial * m, _final * m); + return l; + } + + /** @brief Get a unit vector normal to the line. + * If Y grows upwards, then this is the left normal. If Y grows downwards, + * then this is the right normal. */ + Point normal() const { + return rot90(vector()).normalized(); + } + + // what does this do? + Point normalAndDist(double & dist) const { + Point n = normal(); + dist = -dot(n, _initial); + return n; + } + + /// Compute an affine matrix representing a reflection about the line. + Affine reflection() const { + Point v = versor(); + Coord x2 = v[X]*v[X], y2 = v[Y]*v[Y], xy = v[X]*v[Y]; + Affine m(x2-y2, 2.*xy, + 2.*xy, y2-x2, + _initial[X], _initial[Y]); + m = Translate(-_initial) * m; + return m; + } + + /** @brief Compute an affine which transforms all points on the line to zero X or Y coordinate. + * This operation is useful in reducing intersection problems to root-finding problems. + * There are many affines which do this transformation. This function returns one that + * preserves angles, areas and distances - a rotation combined with a translation, and + * additionally moves the initial point of the line to (0,0). This way it works without + * problems even for lines perpendicular to the target, though may in some cases have + * lower precision than e.g. a shear transform. + * @param d Which coordinate of points on the line should be zero after the transformation */ + Affine rotationToZero(Dim2 d) const { + Point v = vector(); + if (d == X) { + std::swap(v[X], v[Y]); + } else { + v[Y] = -v[Y]; + } + Affine m = Translate(-_initial) * Rotate(v); + return m; + } + /** @brief Compute a rotation affine which transforms the line to one of the axes. + * @param d Which line should be the axis */ + Affine rotationToAxis(Dim2 d) const { + Affine m = rotationToZero(other_dimension(d)); + return m; + } + + Affine transformTo(Line const &other) const; + /// @} + + std::vector intersect(Line const &other) const; + std::vector intersect(Ray const &r) const; + std::vector intersect(LineSegment const &ls) const; + + template + Line &operator*=(T const &tr) { + BOOST_CONCEPT_ASSERT((TransformConcept)); + _initial *= tr; + _final *= tr; + return *this; + } + + bool operator==(Line const &other) const { + if (distance(pointAt(nearestTime(other._initial)), other._initial) != 0) return false; + if (distance(pointAt(nearestTime(other._final)), other._final) != 0) return false; + return true; + } + + template + friend Line operator*(Line const &l, T const &tr) { + BOOST_CONCEPT_ASSERT((TransformConcept)); + Line result(l); + result *= tr; + return result; + } +}; // end class Line + +/** @brief Removes intersections outside of the unit interval. + * A helper used to implement line segment intersections. + * @param xs Line intersections + * @param a Whether the first time value has to be in the unit interval + * @param b Whether the second time value has to be in the unit interval + * @return Appropriately filtered intersections */ +void filter_line_segment_intersections(std::vector &xs, bool a=false, bool b=true); +void filter_ray_intersections(std::vector &xs, bool a=false, bool b=true); + +/// @brief Compute distance from point to line. +/// @relates Line +inline +double distance(Point const &p, Line const &line) +{ + if (line.isDegenerate()) { + return ::Geom::distance(p, line.initialPoint()); + } else { + Coord t = line.nearestTime(p); + return ::Geom::distance(line.pointAt(t), p); + } +} + +inline +bool are_near(Point const &p, Line const &line, double eps = EPSILON) +{ + return are_near(distance(p, line), 0, eps); +} + +inline +bool are_parallel(Line const &l1, Line const &l2, double eps = EPSILON) +{ + return are_near(cross(l1.vector(), l2.vector()), 0, eps); +} + +/** @brief Test whether two lines are approximately the same. + * This tests for being parallel and the origin of one line being close to the other, + * so it tests whether the images of the lines are similar, not whether the same time values + * correspond to similar points. For example a line from (1,1) to (2,2) and a line from + * (-1,-1) to (0,0) will be the same, because their images match, even though there is + * no time value for which the lines give similar points. + * @relates Line */ +inline +bool are_same(Line const &l1, Line const &l2, double eps = EPSILON) +{ + return are_parallel(l1, l2, eps) && are_near(l1.origin(), l2, eps); +} + +/// Test whether two lines are perpendicular. +/// @relates Line +inline +bool are_orthogonal(Line const &l1, Line const &l2, double eps = EPSILON) +{ + return are_near(dot(l1.vector(), l2.vector()), 0, eps); +} + +// evaluate the angle between l1 and l2 rotating l1 in cw direction +// until it overlaps l2 +// the returned value is an angle in the interval [0, PI[ +inline +double angle_between(Line const& l1, Line const& l2) +{ + double angle = angle_between(l1.vector(), l2.vector()); + if (angle < 0) angle += M_PI; + if (angle == M_PI) angle = 0; + return angle; +} + +inline +double distance(Point const &p, LineSegment const &seg) +{ + double t = seg.nearestTime(p); + return distance(p, seg.pointAt(t)); +} + +inline +bool are_near(Point const &p, LineSegment const &seg, double eps = EPSILON) +{ + return are_near(distance(p, seg), 0, eps); +} + +// build a line passing by _point and orthogonal to _line +inline +Line make_orthogonal_line(Point const &p, Line const &line) +{ + Point d = line.vector().cw(); + Line l(p, p + d); + return l; +} + +// build a line passing by _point and parallel to _line +inline +Line make_parallel_line(Point const &p, Line const &line) +{ + Line result(line); + result.setOrigin(p); + return result; +} + +// build a line passing by the middle point of _segment and orthogonal to it. +inline +Line make_bisector_line(LineSegment const& _segment) +{ + return make_orthogonal_line( middle_point(_segment), Line(_segment) ); +} + +// build the bisector line of the angle between ray(O,A) and ray(O,B) +inline +Line make_angle_bisector_line(Point const &A, Point const &O, Point const &B) +{ + AngleInterval ival(Angle(A-O), Angle(B-O)); + Angle bisect = ival.angleAt(0.5); + return Line(O, bisect); +} + +// prj(P) = rot(v, Point( rot(-v, P-O)[X], 0 )) + O +inline +Point projection(Point const &p, Line const &line) +{ + return line.pointAt(line.nearestTime(p)); +} + +inline +LineSegment projection(LineSegment const &seg, Line const &line) +{ + return line.segment(line.nearestTime(seg.initialPoint()), + line.nearestTime(seg.finalPoint())); +} + +inline +boost::optional clip(Line const &l, Rect const &r) { + return l.clip(r); +} + + +namespace detail +{ + +OptCrossing intersection_impl(Ray const& r1, Line const& l2, unsigned int i); +OptCrossing intersection_impl( LineSegment const& ls1, + Line const& l2, + unsigned int i ); +OptCrossing intersection_impl( LineSegment const& ls1, + Ray const& r2, + unsigned int i ); +} + + +inline +OptCrossing intersection(Ray const& r1, Line const& l2) +{ + return detail::intersection_impl(r1, l2, 0); + +} + +inline +OptCrossing intersection(Line const& l1, Ray const& r2) +{ + return detail::intersection_impl(r2, l1, 1); +} + +inline +OptCrossing intersection(LineSegment const& ls1, Line const& l2) +{ + return detail::intersection_impl(ls1, l2, 0); +} + +inline +OptCrossing intersection(Line const& l1, LineSegment const& ls2) +{ + return detail::intersection_impl(ls2, l1, 1); +} + +inline +OptCrossing intersection(LineSegment const& ls1, Ray const& r2) +{ + return detail::intersection_impl(ls1, r2, 0); + +} + +inline +OptCrossing intersection(Ray const& r1, LineSegment const& ls2) +{ + return detail::intersection_impl(ls2, r1, 1); +} + + +OptCrossing intersection(Line const& l1, Line const& l2); + +OptCrossing intersection(Ray const& r1, Ray const& r2); + +OptCrossing intersection(LineSegment const& ls1, LineSegment const& ls2); + + +} // end namespace Geom + + +#endif // LIB2GEOM_SEEN_LINE_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/linear.h b/src/2geom/linear.h new file mode 100644 index 0000000..75c6e01 --- /dev/null +++ b/src/2geom/linear.h @@ -0,0 +1,167 @@ +/** + * \file + * \brief Linear fragment function class + *//* + * Authors: + * Nathan Hurst + * Michael Sloan + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2006-2015 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_LINEAR_H +#define LIB2GEOM_SEEN_LINEAR_H + +#include <2geom/interval.h> +#include <2geom/math-utils.h> + +namespace Geom { + +class SBasis; + +/** + * @brief Function that interpolates linearly between two values. + * @ingroup Fragments + */ +class Linear + : boost::additive< Linear + , boost::arithmetic< Linear, Coord + , boost::equality_comparable< Linear + > > > +{ +public: + Coord a[2]; + Linear() {a[0]=0; a[1]=0;} + Linear(Coord aa, Coord b) {a[0] = aa; a[1] = b;} + Linear(Coord aa) {a[0] = aa; a[1] = aa;} + + Coord operator[](unsigned i) const { + assert(i < 2); + return a[i]; + } + Coord &operator[](unsigned i) { + assert(i < 2); + return a[i]; + } + + //IMPL: FragmentConcept + typedef Coord output_type; + bool isZero(Coord eps=EPSILON) const { return are_near(a[0], 0., eps) && are_near(a[1], 0., eps); } + bool isConstant(Coord eps=EPSILON) const { return are_near(a[0], a[1], eps); } + bool isFinite() const { return std::isfinite(a[0]) && std::isfinite(a[1]); } + + Coord at0() const { return a[0]; } + Coord &at0() { return a[0]; } + Coord at1() const { return a[1]; } + Coord &at1() { return a[1]; } + + Coord valueAt(Coord t) const { return lerp(t, a[0], a[1]); } + Coord operator()(Coord t) const { return valueAt(t); } + + // not very useful, but required for FragmentConcept + std::vector valueAndDerivatives(Coord t, unsigned n) { + std::vector result(n+1, 0.0); + result[0] = valueAt(t); + if (n >= 1) { + result[1] = a[1] - a[0]; + } + return result; + } + + //defined in sbasis.h + inline SBasis toSBasis() const; + + OptInterval bounds_exact() const { return Interval(a[0], a[1]); } + OptInterval bounds_fast() const { return bounds_exact(); } + OptInterval bounds_local(double u, double v) const { return Interval(valueAt(u), valueAt(v)); } + + double tri() const { + return a[1] - a[0]; + } + double hat() const { + return (a[1] + a[0])/2; + } + + // addition of other Linears + Linear &operator+=(Linear const &other) { + a[0] += other.a[0]; + a[1] += other.a[1]; + return *this; + } + Linear &operator-=(Linear const &other) { + a[0] -= other.a[0]; + a[1] -= other.a[1]; + return *this; + } + + // + Linear &operator+=(Coord x) { + a[0] += x; a[1] += x; + return *this; + } + Linear &operator-=(Coord x) { + a[0] -= x; a[1] -= x; + return *this; + } + Linear &operator*=(Coord x) { + a[0] *= x; a[1] *= x; + return *this; + } + Linear &operator/=(Coord x) { + a[0] /= x; a[1] /= x; + return *this; + } + Linear operator-() const { + Linear ret(-a[0], -a[1]); + return ret; + } + + bool operator==(Linear const &other) const { + return a[0] == other.a[0] && a[1] == other.a[1]; + } +}; + +inline Linear reverse(Linear const &a) { return Linear(a[1], a[0]); } +inline Linear portion(Linear const &a, Coord from, Coord to) { + Linear result(a.valueAt(from), a.valueAt(to)); + return result; +} + +} // end namespace Geom + +#endif //LIB2GEOM_SEEN_LINEAR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/math-utils.h b/src/2geom/math-utils.h new file mode 100644 index 0000000..ee923f5 --- /dev/null +++ b/src/2geom/math-utils.h @@ -0,0 +1,110 @@ +/** + * \file + * \brief Low level math functions and compatibility wrappers + *//* + * Authors: + * Johan Engelen + * Michael G. Sloan + * Krzysztof KosiÅ„ski + * Copyright 2006-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_MATH_UTILS_H +#define LIB2GEOM_SEEN_MATH_UTILS_H + +#include // sincos is usually only available in math.h +#include +#include // for std::pair +#include + +namespace Geom { + +/** @brief Sign function - indicates the sign of a numeric type. + * Mathsy people will know this is basically the derivative of abs, except for the fact + * that it is defined on 0. + * @return -1 when x is negative, 1 when positive, and 0 if equal to 0. */ +template inline int sgn(const T& x) { + return (x < 0 ? -1 : (x > 0 ? 1 : 0) ); +// can we 'optimize' this with: +// return ( (T(0) < x) - (x < T(0)) ); +} + +template inline T sqr(const T& x) {return x * x;} +template inline T cube(const T& x) {return x * x * x;} + +/** Between function - returns true if a number x is within a range: (min < x) && (max > x). + * The values delimiting the range and the number must have the same type. + */ +template inline const T& between (const T& min, const T& max, const T& x) + { return (min < x) && (max > x); } + +/** @brief Returns @a x rounded to the nearest multiple of \f$10^{p}\f$. + + Implemented in terms of round, i.e. we make no guarantees as to what happens if x is + half way between two rounded numbers. + + Note: places is the number of decimal places without using scientific (e) notation, not the + number of significant figures. This function may not be suitable for values of x whose + magnitude is so far from 1 that one would want to use scientific (e) notation. + + places may be negative: e.g. places = -2 means rounding to a multiple of .01 +**/ +inline double decimal_round(double x, int p) { + //TODO: possibly implement with modulus instead? + double const multiplier = ::pow(10.0, p); + return ::round( x * multiplier ) / multiplier; +} + +/** @brief Simultaneously compute a sine and a cosine of the same angle. + * This function can be up to 2 times faster than separate computation, depending + * on the platform. It uses the standard library function sincos() if available. + * @param angle Angle + * @param sin_ Variable that will store the sine + * @param cos_ Variable that will store the cosine */ +inline void sincos(double angle, double &sin_, double &cos_) { +#ifdef HAVE_SINCOS + ::sincos(angle, &sin_, &cos_); +#else + sin_ = ::sin(angle); + cos_ = ::cos(angle); +#endif +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_MATH_UTILS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/nearest-time.cpp b/src/2geom/nearest-time.cpp new file mode 100644 index 0000000..1039210 --- /dev/null +++ b/src/2geom/nearest-time.cpp @@ -0,0 +1,322 @@ +/** @file + * @brief Nearest time routines for D2 and Piecewise> + *//* + * Authors: + * Marco Cecchetti + * + * Copyright 2007-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#include <2geom/nearest-time.h> +#include + +namespace Geom +{ + +Coord nearest_time(Point const &p, D2 const &input, Coord from, Coord to) +{ + Interval domain(from, to); + bool partial = false; + + if (domain.min() < 0 || domain.max() > 1) { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + + if (input.isConstant(0)) return from; + + D2 bez; + if (domain.min() != 0 || domain.max() != 1) { + bez = portion(input, domain) - p; + partial = true; + } else { + bez = input - p; + } + + // find extrema of the function x(t)^2 + y(t)^2 + // use the fact that (f^2)' = 2 f f' + // this reduces the order of the distance function by 1 + D2 deriv = derivative(bez); + std::vector ts = (multiply(bez[X], deriv[X]) + multiply(bez[Y], deriv[Y])).roots(); + + Coord t = -1, mind = infinity(); + for (unsigned i = 0; i < ts.size(); ++i) { + Coord droot = L2sq(bez.valueAt(ts[i])); + if (droot < mind) { + mind = droot; + t = ts[i]; + } + } + + // also check endpoints + Coord dinitial = L2sq(bez.at0()); + Coord dfinal = L2sq(bez.at1()); + + if (dinitial < mind) { + mind = dinitial; + t = 0; + } + if (dfinal < mind) { + //mind = dfinal; + t = 1; + } + + if (partial) { + t = domain.valueAt(t); + } + return t; +} + +//////////////////////////////////////////////////////////////////////////////// +// D2 versions + +/* + * Return the parameter t of the nearest time value on the portion of the curve "c", + * related to the interval [from, to], to the point "p". + * The needed curve derivative "dc" is passed as parameter. + * The function return the first nearest time value to "p" that is found. + */ + +double nearest_time(Point const& p, + D2 const& c, + D2 const& dc, + double from, double to ) +{ + if ( from > to ) std::swap(from, to); + if ( from < 0 || to > 1 ) + { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + if (c.isConstant()) return from; + SBasis dd = dot(c - p, dc); + //std::cout << dd << std::endl; + std::vector zeros = Geom::roots(dd); + + double closest = from; + double min_dist_sq = L2sq(c(from) - p); + for ( size_t i = 0; i < zeros.size(); ++i ) + { + double distsq = L2sq(c(zeros[i]) - p); + if ( min_dist_sq > L2sq(c(zeros[i]) - p) ) + { + closest = zeros[i]; + min_dist_sq = distsq; + } + } + if ( min_dist_sq > L2sq( c(to) - p ) ) + closest = to; + return closest; + +} + +/* + * Return the parameters t of all the nearest points on the portion of + * the curve "c", related to the interval [from, to], to the point "p". + * The needed curve derivative "dc" is passed as parameter. + */ + +std::vector +all_nearest_times(Point const &p, + D2 const &c, + D2 const &dc, + double from, double to) +{ + if (from > to) { + std::swap(from, to); + } + if (from < 0 || to > 1) { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + + std::vector result; + if (c.isConstant()) { + result.push_back(from); + return result; + } + SBasis dd = dot(c - p, dc); + + std::vector zeros = Geom::roots(dd); + std::vector candidates; + candidates.push_back(from); + candidates.insert(candidates.end(), zeros.begin(), zeros.end()); + candidates.push_back(to); + std::vector distsq; + distsq.reserve(candidates.size()); + for (unsigned i = 0; i < candidates.size(); ++i) { + distsq.push_back(L2sq(c(candidates[i]) - p)); + } + unsigned closest = 0; + double dsq = distsq[0]; + for (unsigned i = 1; i < candidates.size(); ++i) { + if (dsq > distsq[i]) { + closest = i; + dsq = distsq[i]; + } + } + for (unsigned i = 0; i < candidates.size(); ++i) { + if (distsq[closest] == distsq[i]) { + result.push_back(candidates[i]); + } + } + return result; +} + + +//////////////////////////////////////////////////////////////////////////////// +// Piecewise< D2 > versions + + +double nearest_time(Point const &p, + Piecewise< D2 > const &c, + double from, double to) +{ + if (from > to) std::swap(from, to); + if (from < c.cuts[0] || to > c.cuts[c.size()]) { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + + unsigned si = c.segN(from); + unsigned ei = c.segN(to); + if (si == ei) { + double nearest = + nearest_time(p, c[si], c.segT(from, si), c.segT(to, si)); + return c.mapToDomain(nearest, si); + } + + double t; + double nearest = nearest_time(p, c[si], c.segT(from, si)); + unsigned int ni = si; + double dsq; + double mindistsq = distanceSq(p, c[si](nearest)); + Rect bb; + for (unsigned i = si + 1; i < ei; ++i) { + bb = *bounds_fast(c[i]); + dsq = distanceSq(p, bb); + if ( mindistsq <= dsq ) continue; + + t = nearest_time(p, c[i]); + dsq = distanceSq(p, c[i](t)); + if (mindistsq > dsq) { + nearest = t; + ni = i; + mindistsq = dsq; + } + } + bb = *bounds_fast(c[ei]); + dsq = distanceSq(p, bb); + if (mindistsq > dsq) { + t = nearest_time(p, c[ei], 0, c.segT(to, ei)); + dsq = distanceSq(p, c[ei](t)); + if (mindistsq > dsq) { + nearest = t; + ni = ei; + } + } + return c.mapToDomain(nearest, ni); +} + +std::vector +all_nearest_times(Point const &p, + Piecewise< D2 > const &c, + double from, double to) +{ + if (from > to) { + std::swap(from, to); + } + if (from < c.cuts[0] || to > c.cuts[c.size()]) { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + + unsigned si = c.segN(from); + unsigned ei = c.segN(to); + if ( si == ei ) + { + std::vector all_nearest = + all_nearest_times(p, c[si], c.segT(from, si), c.segT(to, si)); + for ( unsigned int i = 0; i < all_nearest.size(); ++i ) + { + all_nearest[i] = c.mapToDomain(all_nearest[i], si); + } + return all_nearest; + } + std::vector all_t; + std::vector< std::vector > all_np; + all_np.push_back( all_nearest_times(p, c[si], c.segT(from, si)) ); + std::vector ni; + ni.push_back(si); + double dsq; + double mindistsq = distanceSq( p, c[si](all_np.front().front()) ); + Rect bb; + + for (unsigned i = si + 1; i < ei; ++i) { + bb = *bounds_fast(c[i]); + dsq = distanceSq(p, bb); + if ( mindistsq < dsq ) continue; + all_t = all_nearest_times(p, c[i]); + dsq = distanceSq( p, c[i](all_t.front()) ); + if ( mindistsq > dsq ) + { + all_np.clear(); + all_np.push_back(all_t); + ni.clear(); + ni.push_back(i); + mindistsq = dsq; + } + else if ( mindistsq == dsq ) + { + all_np.push_back(all_t); + ni.push_back(i); + } + } + bb = *bounds_fast(c[ei]); + dsq = distanceSq(p, bb); + if (mindistsq >= dsq) { + all_t = all_nearest_times(p, c[ei], 0, c.segT(to, ei)); + dsq = distanceSq( p, c[ei](all_t.front()) ); + if (mindistsq > dsq) { + for (unsigned int i = 0; i < all_t.size(); ++i) { + all_t[i] = c.mapToDomain(all_t[i], ei); + } + return all_t; + } else if (mindistsq == dsq) { + all_np.push_back(all_t); + ni.push_back(ei); + } + } + std::vector all_nearest; + for (unsigned i = 0; i < all_np.size(); ++i) { + for (unsigned int j = 0; j < all_np[i].size(); ++j) { + all_nearest.push_back( c.mapToDomain(all_np[i][j], ni[i]) ); + } + } + all_nearest.erase(std::unique(all_nearest.begin(), all_nearest.end()), + all_nearest.end()); + return all_nearest; +} + +} // end namespace Geom + + diff --git a/src/2geom/nearest-time.h b/src/2geom/nearest-time.h new file mode 100644 index 0000000..007cd27 --- /dev/null +++ b/src/2geom/nearest-time.h @@ -0,0 +1,141 @@ +/** @file + * @brief Nearest time routines for D2 and Piecewise> + *//* + * Authors: + * Marco Cecchetti + * + * Copyright 2007-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef LIB2GEOM_SEEN_NEAREST_TIME_H +#define LIB2GEOM_SEEN_NEAREST_TIME_H + + +#include + +#include <2geom/d2.h> +#include <2geom/piecewise.h> +#include <2geom/exception.h> +#include <2geom/bezier.h> + + +namespace Geom +{ + +/* + * Given a line L specified by a point A and direction vector v, + * return the point on L nearest to p. Note that the returned value + * is with respect to the _normalized_ direction of v! + */ +inline double nearest_time(Point const &p, Point const &A, Point const &v) +{ + Point d(p - A); + return d[0] * v[0] + d[1] * v[1]; +} + +Coord nearest_time(Point const &p, D2 const &bez, Coord from = 0, Coord to = 1); + +//////////////////////////////////////////////////////////////////////////////// +// D2 versions + +/* + * Return the parameter t of a nearest point on the portion of the curve "c", + * related to the interval [from, to], to the point "p". + * The needed curve derivative "deriv" is passed as parameter. + * The function return the first nearest point to "p" that is found. + */ +double nearest_time(Point const &p, + D2 const &c, D2 const &deriv, + double from = 0, double to = 1); + +inline +double nearest_time(Point const &p, + D2 const &c, + double from = 0, double to = 1 ) +{ + return nearest_time(p, c, Geom::derivative(c), from, to); +} + +/* + * Return the parameters t of all the nearest times on the portion of + * the curve "c", related to the interval [from, to], to the point "p". + * The needed curve derivative "dc" is passed as parameter. + */ +std::vector +all_nearest_times(Point const& p, + D2 const& c, D2 const& dc, + double from = 0, double to = 1 ); + +inline +std::vector +all_nearest_times(Point const &p, + D2 const &c, + double from = 0, double to = 1) +{ + return all_nearest_times(p, c, Geom::derivative(c), from, to); +} + + +//////////////////////////////////////////////////////////////////////////////// +// Piecewise< D2 > versions + +double nearest_time(Point const &p, + Piecewise< D2 > const &c, + double from, double to); + +inline +double nearest_time(Point const& p, Piecewise< D2 > const &c) +{ + return nearest_time(p, c, c.cuts[0], c.cuts[c.size()]); +} + + +std::vector +all_nearest_times(Point const &p, + Piecewise< D2 > const &c, + double from, double to); + +inline +std::vector +all_nearest_times( Point const& p, Piecewise< D2 > const& c ) +{ + return all_nearest_times(p, c, c.cuts[0], c.cuts[c.size()]); +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_NEAREST_TIME_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/fitting-model.h b/src/2geom/numeric/fitting-model.h new file mode 100644 index 0000000..0316f57 --- /dev/null +++ b/src/2geom/numeric/fitting-model.h @@ -0,0 +1,521 @@ +/* + * Fitting Models for Geom Types + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef _NL_FITTING_MODEL_H_ +#define _NL_FITTING_MODEL_H_ + + +#include <2geom/d2.h> +#include <2geom/sbasis.h> +#include <2geom/bezier.h> +#include <2geom/bezier-curve.h> +#include <2geom/polynomial.h> +#include <2geom/ellipse.h> +#include <2geom/circle.h> +#include <2geom/utils.h> +#include <2geom/conicsec.h> + + +namespace Geom { namespace NL { + +/* + * A model is an abstraction for an expression dependent from a parameter where + * the coefficients of this expression are the unknowns of the fitting problem. + * For a ceratain number of parameter values we know the related values + * the expression evaluates to: from each parameter value we get a row of + * the matrix of the fitting problem, from each expression value we get + * the related constant term. + * Example: given the model a*x^2 + b*x + c = 0; from x = 1 we get + * the equation a + b + c = 0, in this example the constant term is always + * the same for each parameter value. + * + * A model is required to implement 3 methods: + * + * - size : returns the number of unknown coefficients that appear in + * the expression of the fitting problem; + * - feed : its input is a parameter value and the related expression value, + * it generates a matrix row and a new entry of the constant vector + * of the fitting problem; + * - instance : it has an input parameter represented by the raw vector + * solution of the fitting problem and an output parameter + * of type InstanceType that return a specific object that is + * generated using the fitting problem solution, in the example + * above the object could be a Poly type. + */ + +/* + * completely unknown models must inherit from this template class; + * example: the model a*x^2 + b*x + c = 0 to be solved wrt a, b, c; + * example: the model A(t) = known_sample_value_at(t) to be solved wrt + * the coefficients of the curve A(t) expressed in S-Basis form; + * parameter type: the type of x and t variable in the examples above; + * value type: the type of the known sample values (in the first example + * is constant ) + * instance type: the type of the objects produced by using + * the fitting raw data solution + */ + + + + +template< typename ParameterType, typename ValueType, typename InstanceType > +class LinearFittingModel +{ + public: + typedef ParameterType parameter_type; + typedef ValueType value_type; + typedef InstanceType instance_type; + + static const bool WITH_FIXED_TERMS = false; + + /* + * a LinearFittingModel must implement the following methods: + * + * void feed( VectorView & vector, + * parameter_type const& sample_parameter ) const; + * + * size_t size() const; + * + * void instance(instance_type &, raw_type const& raw_data) const; + * + */ +}; + + +/* + * partially known models must inherit from this template class + * example: the model a*x^2 + 2*x + c = 0 to be solved wrt a and c + */ +template< typename ParameterType, typename ValueType, typename InstanceType > +class LinearFittingModelWithFixedTerms +{ + public: + typedef ParameterType parameter_type; + typedef ValueType value_type; + typedef InstanceType instance_type; + + static const bool WITH_FIXED_TERMS = true; + + /* + * a LinearFittingModelWithFixedTerms must implement the following methods: + * + * void feed( VectorView & vector, + * value_type & fixed_term, + * parameter_type const& sample_parameter ) const; + * + * size_t size() const; + * + * void instance(instance_type &, raw_type const& raw_data) const; + * + */ + + +}; + + +// incomplete model, it can be inherited to make up different kinds of +// instance type; the raw data is a vector of coefficients of a polynomial +// represented in standard power basis +template< typename InstanceType > +class LFMPowerBasis + : public LinearFittingModel +{ + public: + LFMPowerBasis(size_t degree) + : m_size(degree + 1) + { + } + + void feed( VectorView & coeff, double sample_parameter ) const + { + coeff[0] = 1; + double x_i = 1; + for (size_t i = 1; i < coeff.size(); ++i) + { + x_i *= sample_parameter; + coeff[i] = x_i; + } + } + + size_t size() const + { + return m_size; + } + + private: + size_t m_size; +}; + + +// this model generates Geom::Poly objects +class LFMPoly + : public LFMPowerBasis +{ + public: + LFMPoly(size_t degree) + : LFMPowerBasis(degree) + { + } + + void instance(Poly & poly, ConstVectorView const& raw_data) const + { + poly.clear(); + poly.resize(size()); + for (size_t i = 0; i < raw_data.size(); ++i) + { + poly[i] = raw_data[i]; + } + } +}; + + +// incomplete model, it can be inherited to make up different kinds of +// instance type; the raw data is a vector of coefficients of a polynomial +// represented in standard power basis with leading term coefficient equal to 1 +template< typename InstanceType > +class LFMNormalizedPowerBasis + : public LinearFittingModelWithFixedTerms +{ + public: + LFMNormalizedPowerBasis(size_t _degree) + : m_model( _degree - 1) + { + assert(_degree > 0); + } + + + void feed( VectorView & coeff, + double & known_term, + double sample_parameter ) const + { + m_model.feed(coeff, sample_parameter); + known_term = coeff[m_model.size()-1] * sample_parameter; + } + + size_t size() const + { + return m_model.size(); + } + + private: + LFMPowerBasis m_model; +}; + + +// incomplete model, it can be inherited to make up different kinds of +// instance type; the raw data is a vector of coefficients of the equation +// of an ellipse curve +//template< typename InstanceType > +//class LFMEllipseEquation +// : public LinearFittingModelWithFixedTerms +//{ +// public: +// void feed( VectorView & coeff, double & fixed_term, Point const& p ) const +// { +// coeff[0] = p[X] * p[Y]; +// coeff[1] = p[Y] * p[Y]; +// coeff[2] = p[X]; +// coeff[3] = p[Y]; +// coeff[4] = 1; +// fixed_term = p[X] * p[X]; +// } +// +// size_t size() const +// { +// return 5; +// } +//}; + +// incomplete model, it can be inherited to make up different kinds of +// instance type; the raw data is a vector of coefficients of the equation +// of a conic section +template< typename InstanceType > +class LFMConicEquation + : public LinearFittingModelWithFixedTerms +{ + public: + void feed( VectorView & coeff, double & fixed_term, Point const& p ) const + { + coeff[0] = p[X] * p[Y]; + coeff[1] = p[Y] * p[Y]; + coeff[2] = p[X]; + coeff[3] = p[Y]; + coeff[4] = 1; + fixed_term = p[X] * p[X]; + } + + size_t size() const + { + return 5; + } +}; + +// this model generates Ellipse curves +class LFMConicSection + : public LFMConicEquation +{ + public: + void instance(xAx & c, ConstVectorView const& coeff) const + { + c.set(1, coeff[0], coeff[1], coeff[2], coeff[3], coeff[4]); + } +}; + +// this model generates Ellipse curves +class LFMEllipse + : public LFMConicEquation +{ + public: + void instance(Ellipse & e, ConstVectorView const& coeff) const + { + e.setCoefficients(1, coeff[0], coeff[1], coeff[2], coeff[3], coeff[4]); + } +}; + + +// incomplete model, it can be inherited to make up different kinds of +// instance type; the raw data is a vector of coefficients of the equation +// of a circle curve +template< typename InstanceType > +class LFMCircleEquation + : public LinearFittingModelWithFixedTerms +{ + public: + void feed( VectorView & coeff, double & fixed_term, Point const& p ) const + { + coeff[0] = p[X]; + coeff[1] = p[Y]; + coeff[2] = 1; + fixed_term = p[X] * p[X] + p[Y] * p[Y]; + } + + size_t size() const + { + return 3; + } +}; + + +// this model generates Ellipse curves +class LFMCircle + : public LFMCircleEquation +{ + public: + void instance(Circle & c, ConstVectorView const& coeff) const + { + c.setCoefficients(1, coeff[0], coeff[1], coeff[2]); + } +}; + + +// this model generates SBasis objects +class LFMSBasis + : public LinearFittingModel +{ + public: + LFMSBasis( size_t _order ) + : m_size( 2*(_order+1) ), + m_order(_order) + { + } + + void feed( VectorView & coeff, double t ) const + { + double u0 = 1-t; + double u1 = t; + double s = u0 * u1; + coeff[0] = u0; + coeff[1] = u1; + for (size_t i = 2; i < size(); i+=2) + { + u0 *= s; + u1 *= s; + coeff[i] = u0; + coeff[i+1] = u1; + } + } + + size_t size() const + { + return m_size; + } + + void instance(SBasis & sb, ConstVectorView const& raw_data) const + { + sb.resize(m_order+1); + for (unsigned int i = 0, k = 0; i < raw_data.size(); i+=2, ++k) + { + sb[k][0] = raw_data[i]; + sb[k][1] = raw_data[i+1]; + } + } + + private: + size_t m_size; + size_t m_order; +}; + + +// this model generates D2 objects +class LFMD2SBasis + : public LinearFittingModel< double, Point, D2 > +{ + public: + LFMD2SBasis( size_t _order ) + : mosb(_order) + { + } + + void feed( VectorView & coeff, double t ) const + { + mosb.feed(coeff, t); + } + + size_t size() const + { + return mosb.size(); + } + + void instance(D2 & d2sb, ConstMatrixView const& raw_data) const + { + mosb.instance(d2sb[X], raw_data.column_const_view(X)); + mosb.instance(d2sb[Y], raw_data.column_const_view(Y)); + } + + private: + LFMSBasis mosb; +}; + + +// this model generates Bezier objects +class LFMBezier + : public LinearFittingModel +{ + public: + LFMBezier( size_t _order ) + : m_size(_order + 1), + m_order(_order) + { + binomial_coefficients(m_bc, m_order); + } + + void feed( VectorView & coeff, double t ) const + { + double s = 1; + for (size_t i = 0; i < size(); ++i) + { + coeff[i] = s * m_bc[i]; + s *= t; + } + double u = 1-t; + s = 1; + for (size_t i = size()-1; i > 0; --i) + { + coeff[i] *= s; + s *= u; + } + coeff[0] *= s; + } + + size_t size() const + { + return m_size; + } + + void instance(Bezier & b, ConstVectorView const& raw_data) const + { + assert(b.size() == raw_data.size()); + for (unsigned int i = 0; i < raw_data.size(); ++i) + { + b[i] = raw_data[i]; + } + } + + private: + size_t m_size; + size_t m_order; + std::vector m_bc; +}; + + +// this model generates Bezier curves +template +class LFMBezierCurveN + : public LinearFittingModel< double, Point, BezierCurveN > +{ + public: + LFMBezierCurveN() + : mob(degree+1) + { + } + + void feed( VectorView & coeff, double t ) const + { + mob.feed(coeff, t); + } + + size_t size() const + { + return mob.size(); + } + + void instance(BezierCurveN & bc, ConstMatrixView const& raw_data) const + { + Bezier bx(degree); + Bezier by(degree); + mob.instance(bx, raw_data.column_const_view(X)); + mob.instance(by, raw_data.column_const_view(Y)); + bc = BezierCurveN(bx, by); + } + + private: + LFMBezier mob; +}; + +} // end namespace NL +} // end namespace Geom + + +#endif // _NL_FITTING_MODEL_H_ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/fitting-tool.h b/src/2geom/numeric/fitting-tool.h new file mode 100644 index 0000000..f2e856a --- /dev/null +++ b/src/2geom/numeric/fitting-tool.h @@ -0,0 +1,562 @@ +/* + * Fitting Tools + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef _NL_FITTING_TOOL_H_ +#define _NL_FITTING_TOOL_H_ + + +#include <2geom/numeric/vector.h> +#include <2geom/numeric/matrix.h> + +#include <2geom/point.h> + +#include + + +/* + * The least_square_fitter class represents a tool for solving a fitting + * problem with respect to a given model that represents an expression + * dependent from a parameter where the coefficients of this expression + * are the unknowns of the fitting problem. + * The minimizing solution is found by computing the pseudo-inverse + * of the problem matrix + */ + + +namespace Geom { namespace NL { + +namespace detail { + +template< typename ModelT> +class lsf_base +{ + public: + typedef ModelT model_type; + typedef typename model_type::parameter_type parameter_type; + typedef typename model_type::value_type value_type; + + lsf_base( model_type const& _model, size_t forecasted_samples ) + : m_model(_model), + m_total_samples(0), + m_matrix(forecasted_samples, m_model.size()), + m_psdinv_matrix(NULL) + { + } + + // compute pseudo inverse + void update() + { + if (total_samples() == 0) return; + if (m_psdinv_matrix != NULL) + { + delete m_psdinv_matrix; + } + MatrixView mv(m_matrix, 0, 0, total_samples(), m_matrix.columns()); + m_psdinv_matrix = new Matrix( pseudo_inverse(mv) ); + assert(m_psdinv_matrix != NULL); + } + + size_t total_samples() const + { + return m_total_samples; + } + + bool is_full() const + { + return (total_samples() == m_matrix.rows()); + } + + void clear() + { + m_total_samples = 0; + } + + virtual + ~lsf_base() + { + if (m_psdinv_matrix != NULL) + { + delete m_psdinv_matrix; + } + } + + protected: + const model_type & m_model; + size_t m_total_samples; + Matrix m_matrix; + Matrix* m_psdinv_matrix; + +}; // end class lsf_base + + + + +template< typename ModelT, typename ValueType = typename ModelT::value_type> +class lsf_solution +{ +}; + +// a fitting process on samples with value of type double +// produces a solution of type Vector +template< typename ModelT> +class lsf_solution + : public lsf_base +{ +public: + typedef ModelT model_type; + typedef typename model_type::parameter_type parameter_type; + typedef typename model_type::value_type value_type; + typedef Vector solution_type; + typedef lsf_base base_type; + + using base_type::m_model; + using base_type::m_psdinv_matrix; + using base_type::total_samples; + +public: + lsf_solution( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples), + m_solution(_model.size()) + { + } + + template< typename VectorT > + solution_type& result(VectorT const& sample_values) + { + assert(sample_values.size() == total_samples()); + ConstVectorView sv(sample_values); + m_solution = (*m_psdinv_matrix) * sv; + return m_solution; + } + + // a comparison between old sample values and the new ones is performed + // in order to minimize computation + // prerequisite: + // old_sample_values.size() == new_sample_values.size() + // no update() call can be performed between two result invocations + template< typename VectorT > + solution_type& result( VectorT const& old_sample_values, + VectorT const& new_sample_values ) + { + assert(old_sample_values.size() == total_samples()); + assert(new_sample_values.size() == total_samples()); + Vector diff(total_samples()); + for (size_t i = 0; i < diff.size(); ++i) + { + diff[i] = new_sample_values[i] - old_sample_values[i]; + } + Vector column(m_model.size()); + Vector delta(m_model.size(), 0.0); + for (size_t i = 0; i < diff.size(); ++i) + { + if (diff[i] != 0) + { + column = m_psdinv_matrix->column_view(i); + column.scale(diff[i]); + delta += column; + } + } + m_solution += delta; + return m_solution; + } + + solution_type& result() + { + return m_solution; + } + +private: + solution_type m_solution; + +}; // end class lsf_solution + + +// a fitting process on samples with value of type Point +// produces a solution of type Matrix (with 2 columns) +template< typename ModelT> +class lsf_solution + : public lsf_base +{ +public: + typedef ModelT model_type; + typedef typename model_type::parameter_type parameter_type; + typedef typename model_type::value_type value_type; + typedef Matrix solution_type; + typedef lsf_base base_type; + + using base_type::m_model; + using base_type::m_psdinv_matrix; + using base_type::total_samples; + +public: + lsf_solution( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples), + m_solution(_model.size(), 2) + { + } + + solution_type& result(std::vector const& sample_values) + { + assert(sample_values.size() == total_samples()); + Matrix svm(total_samples(), 2); + for (size_t i = 0; i < total_samples(); ++i) + { + svm(i, X) = sample_values[i][X]; + svm(i, Y) = sample_values[i][Y]; + } + m_solution = (*m_psdinv_matrix) * svm; + return m_solution; + } + + // a comparison between old sample values and the new ones is performed + // in order to minimize computation + // prerequisite: + // old_sample_values.size() == new_sample_values.size() + // no update() call can to be performed between two result invocations + solution_type& result( std::vector const& old_sample_values, + std::vector const& new_sample_values ) + { + assert(old_sample_values.size() == total_samples()); + assert(new_sample_values.size() == total_samples()); + Matrix diff(total_samples(), 2); + for (size_t i = 0; i < total_samples(); ++i) + { + diff(i, X) = new_sample_values[i][X] - old_sample_values[i][X]; + diff(i, Y) = new_sample_values[i][Y] - old_sample_values[i][Y]; + } + Vector column(m_model.size()); + Matrix delta(m_model.size(), 2, 0.0); + VectorView deltax = delta.column_view(X); + VectorView deltay = delta.column_view(Y); + for (size_t i = 0; i < total_samples(); ++i) + { + if (diff(i, X) != 0) + { + column = m_psdinv_matrix->column_view(i); + column.scale(diff(i, X)); + deltax += column; + } + if (diff(i, Y) != 0) + { + column = m_psdinv_matrix->column_view(i); + column.scale(diff(i, Y)); + deltay += column; + } + } + m_solution += delta; + return m_solution; + } + + solution_type& result() + { + return m_solution; + } + +private: + solution_type m_solution; + +}; // end class lsf_solution + + + + +template< typename ModelT, + bool WITH_FIXED_TERMS = ModelT::WITH_FIXED_TERMS > +class lsf_with_fixed_terms +{ +}; + + +// fitting tool for completely unknown models +template< typename ModelT> +class lsf_with_fixed_terms + : public lsf_solution +{ + public: + typedef ModelT model_type; + typedef typename model_type::parameter_type parameter_type; + typedef typename model_type::value_type value_type; + typedef lsf_solution base_type; + typedef typename base_type::solution_type solution_type; + + using base_type::total_samples; + using base_type::is_full; + using base_type::m_matrix; + using base_type::m_total_samples; + using base_type::m_model; + + public: + lsf_with_fixed_terms( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples) + { + } + + void append(parameter_type const& sample_parameter) + { + assert(!is_full()); + VectorView row = m_matrix.row_view(total_samples()); + m_model.feed(row, sample_parameter); + ++m_total_samples; + } + + void append_copy(size_t sample_index) + { + assert(!is_full()); + assert(sample_index < total_samples()); + VectorView dest_row = m_matrix.row_view(total_samples()); + VectorView source_row = m_matrix.row_view(sample_index); + dest_row = source_row; + ++m_total_samples; + } + +}; // end class lsf_with_fixed_terms + + +// fitting tool for partially known models +template< typename ModelT> +class lsf_with_fixed_terms + : public lsf_solution +{ + public: + typedef ModelT model_type; + typedef typename model_type::parameter_type parameter_type; + typedef typename model_type::value_type value_type; + typedef lsf_solution base_type; + typedef typename base_type::solution_type solution_type; + + using base_type::total_samples; + using base_type::is_full; + using base_type::m_matrix; + using base_type::m_total_samples; + using base_type::m_model; + + public: + lsf_with_fixed_terms( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples), + m_vector(forecasted_samples), + m_vector_view(NULL) + { + } + void append(parameter_type const& sample_parameter) + { + assert(!is_full()); + VectorView row = m_matrix.row_view(total_samples()); + m_model.feed(row, m_vector[total_samples()], sample_parameter); + ++m_total_samples; + } + + void append_copy(size_t sample_index) + { + assert(!is_full()); + assert(sample_index < total_samples()); + VectorView dest_row = m_matrix.row_view(total_samples()); + VectorView source_row = m_matrix.row_view(sample_index); + dest_row = source_row; + m_vector[total_samples()] = m_vector[sample_index]; + ++m_total_samples; + } + + void update() + { + base_type::update(); + if (total_samples() == 0) return; + if (m_vector_view != NULL) + { + delete m_vector_view; + } + m_vector_view = new VectorView(m_vector, base_type::total_samples()); + assert(m_vector_view != NULL); + } + + virtual + ~lsf_with_fixed_terms() + { + if (m_vector_view != NULL) + { + delete m_vector_view; + } + } + + protected: + Vector m_vector; + VectorView* m_vector_view; + +}; // end class lsf_with_fixed_terms + + +} // end namespace detail + + + + +template< typename ModelT, + typename ValueType = typename ModelT::value_type, + bool WITH_FIXED_TERMS = ModelT::WITH_FIXED_TERMS > +class least_squeares_fitter +{ +}; + + +template< typename ModelT, typename ValueType > +class least_squeares_fitter + : public detail::lsf_with_fixed_terms +{ + public: + typedef ModelT model_type; + typedef detail::lsf_with_fixed_terms base_type; + typedef typename base_type::parameter_type parameter_type; + typedef typename base_type::value_type value_type; + typedef typename base_type::solution_type solution_type; + + public: + least_squeares_fitter( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples) + { + } +}; // end class least_squeares_fitter + + +template< typename ModelT> +class least_squeares_fitter + : public detail::lsf_with_fixed_terms +{ + public: + typedef ModelT model_type; + typedef detail::lsf_with_fixed_terms base_type; + typedef typename base_type::parameter_type parameter_type; + typedef typename base_type::value_type value_type; + typedef typename base_type::solution_type solution_type; + + using base_type::m_vector_view; + //using base_type::result; // VSC legacy support + solution_type& result( std::vector const& old_sample_values, + std::vector const& new_sample_values ) + { + return base_type::result(old_sample_values, new_sample_values); + } + + solution_type& result() + { + return base_type::result(); + } + + public: + least_squeares_fitter( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples) + { + } + + template< typename VectorT > + solution_type& result(VectorT const& sample_values) + { + assert(sample_values.size() == m_vector_view->size()); + Vector sv(sample_values.size()); + for (size_t i = 0; i < sv.size(); ++i) + sv[i] = sample_values[i] - (*m_vector_view)[i]; + return base_type::result(sv); + } + +}; // end class least_squeares_fitter + + +template< typename ModelT> +class least_squeares_fitter + : public detail::lsf_with_fixed_terms +{ + public: + typedef ModelT model_type; + typedef detail::lsf_with_fixed_terms base_type; + typedef typename base_type::parameter_type parameter_type; + typedef typename base_type::value_type value_type; + typedef typename base_type::solution_type solution_type; + + using base_type::m_vector_view; + //using base_type::result; // VCS legacy support + solution_type& result( std::vector const& old_sample_values, + std::vector const& new_sample_values ) + { + return base_type::result(old_sample_values, new_sample_values); + } + + solution_type& result() + { + return base_type::result(); + } + + + public: + least_squeares_fitter( model_type const& _model, + size_t forecasted_samples ) + : base_type(_model, forecasted_samples) + { + } + + solution_type& result(std::vector const& sample_values) + { + assert(sample_values.size() == m_vector_view->size()); + NL::Matrix sv(sample_values.size(), 2); + for (size_t i = 0; i < sample_values.size(); ++i) + { + sv(i, X) = sample_values[i][X] - (*m_vector_view)[i]; + sv(i, Y) = sample_values[i][Y] - (*m_vector_view)[i]; + } + return base_type::result(sv); + } + +}; // end class least_squeares_fitter + + +} // end namespace NL +} // end namespace Geom + + + +#endif // _NL_FITTING_TOOL_H_ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/linear_system.h b/src/2geom/numeric/linear_system.h new file mode 100644 index 0000000..f793e20 --- /dev/null +++ b/src/2geom/numeric/linear_system.h @@ -0,0 +1,138 @@ +/* + * LinearSystem class wraps some gsl routines for solving linear systems + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef _NL_LINEAR_SYSTEM_H_ +#define _NL_LINEAR_SYSTEM_H_ + + +#include + +#include + +#include <2geom/numeric/matrix.h> +#include <2geom/numeric/vector.h> + + +namespace Geom { namespace NL { + + +class LinearSystem +{ +public: + LinearSystem(MatrixView & _matrix, VectorView & _vector) + : m_matrix(_matrix), m_vector(_vector), m_solution(_matrix.columns()) + { + } + + LinearSystem(Matrix & _matrix, Vector & _vector) + : m_matrix(_matrix), m_vector(_vector), m_solution(_matrix.columns()) + { + } + + const Vector & LU_solve() + { + assert( matrix().rows() == matrix().columns() + && matrix().rows() == vector().size() ); + int s; + gsl_permutation * p = gsl_permutation_alloc(matrix().rows()); + gsl_linalg_LU_decomp (matrix().get_gsl_matrix(), p, &s); + gsl_linalg_LU_solve( matrix().get_gsl_matrix(), + p, + vector().get_gsl_vector(), + m_solution.get_gsl_vector() + ); + gsl_permutation_free(p); + return solution(); + } + + const Vector & SV_solve() + { + assert( matrix().rows() >= matrix().columns() + && matrix().rows() == vector().size() ); + + gsl_matrix* U = matrix().get_gsl_matrix(); + gsl_matrix* V = gsl_matrix_alloc(matrix().columns(), matrix().columns()); + gsl_vector* S = gsl_vector_alloc(matrix().columns()); + gsl_vector* work = gsl_vector_alloc(matrix().columns()); + + gsl_linalg_SV_decomp( U, V, S, work ); + + gsl_vector* b = vector().get_gsl_vector(); + gsl_vector* x = m_solution.get_gsl_vector(); + + gsl_linalg_SV_solve( U, V, S, b, x); + + gsl_matrix_free(V); + gsl_vector_free(S); + gsl_vector_free(work); + + return solution(); + } + + MatrixView & matrix() + { + return m_matrix; + } + + VectorView & vector() + { + return m_vector; + } + + const Vector & solution() const + { + return m_solution; + } + +private: + MatrixView m_matrix; + VectorView m_vector; + Vector m_solution; +}; + + +} } // end namespaces + + +#endif /*_NL_LINEAR_SYSTEM_H_*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/matrix.cpp b/src/2geom/numeric/matrix.cpp new file mode 100644 index 0000000..98ff3b6 --- /dev/null +++ b/src/2geom/numeric/matrix.cpp @@ -0,0 +1,154 @@ +/* + * Matrix, MatrixView, ConstMatrixView classes wrap the gsl matrix routines; + * "views" mimic the semantic of C++ references: any operation performed + * on a "view" is actually performed on the "viewed object" + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#include <2geom/numeric/matrix.h> +#include <2geom/numeric/vector.h> + + +namespace Geom { namespace NL { + +Vector operator*( detail::BaseMatrixImpl const& A, + detail::BaseVectorImpl const& v ) +{ + assert(A.columns() == v.size()); + + Vector result(A.rows(), 0.0); + for (size_t i = 0; i < A.rows(); ++i) + for (size_t j = 0; j < A.columns(); ++j) + result[i] += A(i,j) * v[j]; + + return result; +} + +Matrix operator*( detail::BaseMatrixImpl const& A, + detail::BaseMatrixImpl const& B ) +{ + assert(A.columns() == B.rows()); + + Matrix C(A.rows(), B.columns(), 0.0); + for (size_t i = 0; i < C.rows(); ++i) + for (size_t j = 0; j < C.columns(); ++j) + for (size_t k = 0; k < A.columns(); ++k) + C(i,j) += A(i,k) * B(k, j); + + return C; +} + +Matrix pseudo_inverse(detail::BaseMatrixImpl const& A) +{ + + Matrix U(A); + Matrix V(A.columns(), A.columns()); + Vector s(A.columns()); + gsl_vector* work = gsl_vector_alloc(A.columns()); + + gsl_linalg_SV_decomp( U.get_gsl_matrix(), + V.get_gsl_matrix(), + s.get_gsl_vector(), + work ); + + Matrix P(A.columns(), A.rows(), 0.0); + + int sz = s.size(); + while ( sz-- > 0 && s[sz] == 0 ) {} + ++sz; + if (sz == 0) return P; + VectorView sv(s, sz); + + for (size_t i = 0; i < sv.size(); ++i) + { + VectorView v = V.column_view(i); + v.scale(1/sv[i]); + for (size_t h = 0; h < P.rows(); ++h) + for (size_t k = 0; k < P.columns(); ++k) + P(h,k) += V(h,i) * U(k,i); + } + + return P; +} + + +double trace (detail::BaseMatrixImpl const& A) +{ + if (A.rows() != A.columns()) + { + THROW_RANGEERROR ("NL::Matrix: computing trace: " + "rows() != columns()"); + } + double t = 0; + for (size_t i = 0; i < A.rows(); ++i) + { + t += A(i,i); + } + return t; +} + + +double det (detail::BaseMatrixImpl const& A) +{ + if (A.rows() != A.columns()) + { + THROW_RANGEERROR ("NL::Matrix: computing determinant: " + "rows() != columns()"); + } + + Matrix LU(A); + int s; + gsl_permutation * p = gsl_permutation_alloc(LU.rows()); + gsl_linalg_LU_decomp (LU.get_gsl_matrix(), p, &s); + + double t = 1; + for (size_t i = 0; i < LU.rows(); ++i) + { + t *= LU(i,i); + } + + gsl_permutation_free(p); + return t; +} + + +} } // end namespaces + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/matrix.h b/src/2geom/numeric/matrix.h new file mode 100644 index 0000000..5cebf10 --- /dev/null +++ b/src/2geom/numeric/matrix.h @@ -0,0 +1,603 @@ +/* + * Matrix, MatrixView, ConstMatrixView classes wrap the gsl matrix routines; + * "views" mimic the semantic of C++ references: any operation performed + * on a "view" is actually performed on the "viewed object" + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + + + +#ifndef _NL_MATRIX_H_ +#define _NL_MATRIX_H_ + +#include <2geom/exception.h> +#include <2geom/numeric/vector.h> + +#include +#include // for std::pair +#include // for std::swap +#include +#include +#include +#include + + +namespace Geom { namespace NL { + +namespace detail +{ + +class BaseMatrixImpl +{ + public: + virtual ~BaseMatrixImpl() + { + } + + ConstVectorView row_const_view(size_t i) const + { + return ConstVectorView(gsl_matrix_const_row(m_matrix, i)); + } + + ConstVectorView column_const_view(size_t i) const + { + return ConstVectorView(gsl_matrix_const_column(m_matrix, i)); + } + + const double & operator() (size_t i, size_t j) const + { + return *gsl_matrix_const_ptr(m_matrix, i, j); + } + + const gsl_matrix* get_gsl_matrix() const + { + return m_matrix; + } + + bool is_zero() const + { + return gsl_matrix_isnull(m_matrix); + } + + bool is_positive() const + { + for ( unsigned int i = 0; i < rows(); ++i ) + { + for ( unsigned int j = 0; j < columns(); ++j ) + { + if ( (*this)(i,j) <= 0 ) return false; + } + } + return true; + } + + bool is_negative() const + { + for ( unsigned int i = 0; i < rows(); ++i ) + { + for ( unsigned int j = 0; j < columns(); ++j ) + { + if ( (*this)(i,j) >= 0 ) return false; + } + } + return true; + } + + bool is_non_negative() const + { + for ( unsigned int i = 0; i < rows(); ++i ) + { + for ( unsigned int j = 0; j < columns(); ++j ) + { + if ( (*this)(i,j) < 0 ) return false; + } + } + return true; + } + + double max() const + { + return gsl_matrix_max(m_matrix); + } + + double min() const + { + return gsl_matrix_min(m_matrix); + } + + std::pair + max_index() const + { + std::pair indices; + gsl_matrix_max_index(m_matrix, &(indices.first), &(indices.second)); + return indices; + } + + std::pair + min_index() const + { + std::pair indices; + gsl_matrix_min_index(m_matrix, &(indices.first), &(indices.second)); + return indices; + } + + size_t rows() const + { + return m_rows; + } + + size_t columns() const + { + return m_columns; + } + + std::string str() const; + + protected: + size_t m_rows, m_columns; + gsl_matrix* m_matrix; + +}; // end class BaseMatrixImpl + + +inline +bool operator== (BaseMatrixImpl const& m1, BaseMatrixImpl const& m2) +{ + if (m1.rows() != m2.rows() || m1.columns() != m2.columns()) return false; + + for (size_t i = 0; i < m1.rows(); ++i) + for (size_t j = 0; j < m1.columns(); ++j) + if (m1(i,j) != m2(i,j)) return false; + + return true; +} + +template< class charT > +inline +std::basic_ostream & +operator<< (std::basic_ostream & os, const BaseMatrixImpl & _matrix) +{ + if (_matrix.rows() == 0 || _matrix.columns() == 0) return os; + + os << "[[" << _matrix(0,0); + for (size_t j = 1; j < _matrix.columns(); ++j) + { + os << ", " << _matrix(0,j); + } + os << "]"; + + for (size_t i = 1; i < _matrix.rows(); ++i) + { + os << ", [" << _matrix(i,0); + for (size_t j = 1; j < _matrix.columns(); ++j) + { + os << ", " << _matrix(i,j); + } + os << "]"; + } + os << "]"; + return os; +} + +inline +std::string BaseMatrixImpl::str() const +{ + std::ostringstream oss; + oss << (*this); + return oss.str(); +} + + +class MatrixImpl : public BaseMatrixImpl +{ + public: + + typedef BaseMatrixImpl base_type; + + void set_all( double x ) + { + gsl_matrix_set_all(m_matrix, x); + } + + void set_identity() + { + gsl_matrix_set_identity(m_matrix); + } + + using base_type::operator(); // VSC legacy support + const double & operator() (size_t i, size_t j) const + { + return base_type::operator ()(i, j); + } + + double & operator() (size_t i, size_t j) + { + return *gsl_matrix_ptr(m_matrix, i, j); + } + + using base_type::get_gsl_matrix; + + gsl_matrix* get_gsl_matrix() + { + return m_matrix; + } + + VectorView row_view(size_t i) + { + return VectorView(gsl_matrix_row(m_matrix, i)); + } + + VectorView column_view(size_t i) + { + return VectorView(gsl_matrix_column(m_matrix, i)); + } + + void swap_rows(size_t i, size_t j) + { + gsl_matrix_swap_rows(m_matrix, i, j); + } + + void swap_columns(size_t i, size_t j) + { + gsl_matrix_swap_columns(m_matrix, i, j); + } + + MatrixImpl & transpose() + { + assert(columns() == rows()); + gsl_matrix_transpose(m_matrix); + return (*this); + } + + MatrixImpl & scale(double x) + { + gsl_matrix_scale(m_matrix, x); + return (*this); + } + + MatrixImpl & translate(double x) + { + gsl_matrix_add_constant(m_matrix, x); + return (*this); + } + + MatrixImpl & operator+=(base_type const& _matrix) + { + gsl_matrix_add(m_matrix, _matrix.get_gsl_matrix()); + return (*this); + } + + MatrixImpl & operator-=(base_type const& _matrix) + { + gsl_matrix_sub(m_matrix, _matrix.get_gsl_matrix()); + return (*this); + } + +}; // end class MatrixImpl + +} // end namespace detail + + +using detail::operator==; +using detail::operator<<; + + +template +class ConstBaseSymmetricMatrix; + + +class Matrix: public detail::MatrixImpl +{ + public: + typedef detail::MatrixImpl base_type; + + public: + // the matrix is not initialized + Matrix(size_t n1, size_t n2) + { + m_rows = n1; + m_columns = n2; + m_matrix = gsl_matrix_alloc(n1, n2); + } + + Matrix(size_t n1, size_t n2, double x) + { + m_rows = n1; + m_columns = n2; + m_matrix = gsl_matrix_alloc(n1, n2); + gsl_matrix_set_all(m_matrix, x); + } + + Matrix(Matrix const& _matrix) + : base_type() + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix = gsl_matrix_alloc(rows(), columns()); + gsl_matrix_memcpy(m_matrix, _matrix.get_gsl_matrix()); + } + + explicit + Matrix(base_type::base_type const& _matrix) + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix = gsl_matrix_alloc(rows(), columns()); + gsl_matrix_memcpy(m_matrix, _matrix.get_gsl_matrix()); + } + + template + explicit + Matrix(ConstBaseSymmetricMatrix const& _smatrix) + { + m_rows = N; + m_columns = N; + m_matrix = gsl_matrix_alloc(N, N); + for (size_t i = 0; i < N; ++i) + for (size_t j = 0; j < N ; ++j) + (*gsl_matrix_ptr(m_matrix, i, j)) = _smatrix(i,j); + } + + Matrix & operator=(Matrix const& _matrix) + { + assert( rows() == _matrix.rows() && columns() == _matrix.columns() ); + gsl_matrix_memcpy(m_matrix, _matrix.get_gsl_matrix()); + return *this; + } + + Matrix & operator=(base_type::base_type const& _matrix) + { + assert( rows() == _matrix.rows() && columns() == _matrix.columns() ); + gsl_matrix_memcpy(m_matrix, _matrix.get_gsl_matrix()); + return *this; + } + + template + Matrix & operator=(ConstBaseSymmetricMatrix const& _smatrix) + { + assert (rows() == N && columns() == N); + for (size_t i = 0; i < N; ++i) + for (size_t j = 0; j < N ; ++j) + (*this)(i,j) = _smatrix(i,j); + return *this; + } + + virtual ~Matrix() + { + gsl_matrix_free(m_matrix); + } + + Matrix & transpose() + { + return static_cast( base_type::transpose() ); + } + + Matrix & scale(double x) + { + return static_cast( base_type::scale(x) ); + } + + Matrix & translate(double x) + { + return static_cast( base_type::translate(x) ); + } + + Matrix & operator+=(base_type::base_type const& _matrix) + { + return static_cast( base_type::operator+=(_matrix) ); + } + + Matrix & operator-=(base_type::base_type const& _matrix) + { + return static_cast( base_type::operator-=(_matrix) ); + } + + friend + void swap(Matrix & m1, Matrix & m2); + friend + void swap_any(Matrix & m1, Matrix & m2); + +}; // end class Matrix + + +// warning! this operation invalidates any view of the passed matrix objects +inline +void swap(Matrix & m1, Matrix & m2) +{ + assert(m1.rows() == m2.rows() && m1.columns() == m2.columns()); + using std::swap; + swap(m1.m_matrix, m2.m_matrix); +} + +inline void swap_any(Matrix &m1, Matrix &m2) +{ + using std::swap; + swap(m1.m_matrix, m2.m_matrix); + swap(m1.m_rows, m2.m_rows); + swap(m1.m_columns, m2.m_columns); +} + + + +class ConstMatrixView : public detail::BaseMatrixImpl +{ + public: + typedef detail::BaseMatrixImpl base_type; + + public: + ConstMatrixView(const base_type & _matrix, size_t k1, size_t k2, size_t n1, size_t n2) + : m_matrix_view( gsl_matrix_const_submatrix(_matrix.get_gsl_matrix(), k1, k2, n1, n2) ) + { + m_rows = n1; + m_columns = n2; + m_matrix = const_cast( &(m_matrix_view.matrix) ); + } + + ConstMatrixView(const ConstMatrixView & _matrix) + : base_type(), + m_matrix_view(_matrix.m_matrix_view) + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix = const_cast( &(m_matrix_view.matrix) ); + } + + ConstMatrixView(const base_type & _matrix) + : m_matrix_view(gsl_matrix_const_submatrix(_matrix.get_gsl_matrix(), 0, 0, _matrix.rows(), _matrix.columns())) + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix = const_cast( &(m_matrix_view.matrix) ); + } + + private: + gsl_matrix_const_view m_matrix_view; + +}; // end class ConstMatrixView + + + + +class MatrixView : public detail::MatrixImpl +{ + public: + typedef detail::MatrixImpl base_type; + + public: + MatrixView(base_type & _matrix, size_t k1, size_t k2, size_t n1, size_t n2) + { + m_rows = n1; + m_columns = n2; + m_matrix_view + = gsl_matrix_submatrix(_matrix.get_gsl_matrix(), k1, k2, n1, n2); + m_matrix = &(m_matrix_view.matrix); + } + + MatrixView(const MatrixView & _matrix) + : base_type() + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix_view = _matrix.m_matrix_view; + m_matrix = &(m_matrix_view.matrix); + } + + MatrixView(Matrix & _matrix) + { + m_rows = _matrix.rows(); + m_columns = _matrix.columns(); + m_matrix_view + = gsl_matrix_submatrix(_matrix.get_gsl_matrix(), 0, 0, rows(), columns()); + m_matrix = &(m_matrix_view.matrix); + } + + MatrixView & operator=(MatrixView const& _matrix) + { + assert( rows() == _matrix.rows() && columns() == _matrix.columns() ); + gsl_matrix_memcpy(m_matrix, _matrix.m_matrix); + return *this; + } + + MatrixView & operator=(base_type::base_type const& _matrix) + { + assert( rows() == _matrix.rows() && columns() == _matrix.columns() ); + gsl_matrix_memcpy(m_matrix, _matrix.get_gsl_matrix()); + return *this; + } + + MatrixView & transpose() + { + return static_cast( base_type::transpose() ); + } + + MatrixView & scale(double x) + { + return static_cast( base_type::scale(x) ); + } + + MatrixView & translate(double x) + { + return static_cast( base_type::translate(x) ); + } + + MatrixView & operator+=(base_type::base_type const& _matrix) + { + return static_cast( base_type::operator+=(_matrix) ); + } + + MatrixView & operator-=(base_type::base_type const& _matrix) + { + return static_cast( base_type::operator-=(_matrix) ); + } + + friend + void swap_view(MatrixView & m1, MatrixView & m2); + + private: + gsl_matrix_view m_matrix_view; + +}; // end class MatrixView + + +inline +void swap_view(MatrixView & m1, MatrixView & m2) +{ + assert(m1.rows() == m2.rows() && m1.columns() == m2.columns()); + using std::swap; + swap(m1.m_matrix_view, m2.m_matrix_view); +} + +Vector operator*( detail::BaseMatrixImpl const& A, + detail::BaseVectorImpl const& v ); + +Matrix operator*( detail::BaseMatrixImpl const& A, + detail::BaseMatrixImpl const& B ); + +Matrix pseudo_inverse(detail::BaseMatrixImpl const& A); + +double trace (detail::BaseMatrixImpl const& A); + +double det (detail::BaseMatrixImpl const& A); + +} } // end namespaces + +#endif /*_NL_MATRIX_H_*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/symmetric-matrix-fs-operation.h b/src/2geom/numeric/symmetric-matrix-fs-operation.h new file mode 100644 index 0000000..c5aaa72 --- /dev/null +++ b/src/2geom/numeric/symmetric-matrix-fs-operation.h @@ -0,0 +1,102 @@ +/* + * SymmetricMatrix basic operation + * + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef _NL_SYMMETRIC_MATRIX_FS_OPERATION_H_ +#define _NL_SYMMETRIC_MATRIX_FS_OPERATION_H_ + + +#include <2geom/numeric/symmetric-matrix-fs.h> +#include <2geom/numeric/symmetric-matrix-fs-trace.h> + + + + +namespace Geom { namespace NL { + +template +SymmetricMatrix adj(const ConstBaseSymmetricMatrix & S); + +template <> +inline +SymmetricMatrix<2> adj(const ConstBaseSymmetricMatrix<2> & S) +{ + SymmetricMatrix<2> result; + result.get<0,0>() = S.get<1,1>(); + result.get<1,0>() = -S.get<1,0>(); + result.get<1,1>() = S.get<0,0>(); + return result; +} + +template <> +inline +SymmetricMatrix<3> adj(const ConstBaseSymmetricMatrix<3> & S) +{ + SymmetricMatrix<3> result; + + result.get<0,0>() = S.get<1,1>() * S.get<2,2>() - S.get<1,2>() * S.get<2,1>(); + result.get<1,0>() = S.get<0,2>() * S.get<2,1>() - S.get<0,1>() * S.get<2,2>(); + result.get<1,1>() = S.get<0,0>() * S.get<2,2>() - S.get<0,2>() * S.get<2,0>(); + result.get<2,0>() = S.get<0,1>() * S.get<1,2>() - S.get<0,2>() * S.get<1,1>(); + result.get<2,1>() = S.get<0,2>() * S.get<1,0>() - S.get<0,0>() * S.get<1,2>(); + result.get<2,2>() = S.get<0,0>() * S.get<1,1>() - S.get<0,1>() * S.get<1,0>(); + return result; +} + +template +inline +SymmetricMatrix inverse(const ConstBaseSymmetricMatrix & S) +{ + SymmetricMatrix result = adj(S); + double d = det(S); + assert (d != 0); + result.scale (1/d); + return result; +} + +} /* end namespace NL*/ } /* end namespace Geom*/ + + +#endif // _NL_SYMMETRIC_MATRIX_FS_OPERATION_H_ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/symmetric-matrix-fs-trace.h b/src/2geom/numeric/symmetric-matrix-fs-trace.h new file mode 100644 index 0000000..eff3dd2 --- /dev/null +++ b/src/2geom/numeric/symmetric-matrix-fs-trace.h @@ -0,0 +1,427 @@ +/* + * SymmetricMatrix trace + * + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef _NL_TRACE_H_ +#define _NL_TRACE_H_ + + +#include <2geom/numeric/matrix.h> +#include <2geom/numeric/symmetric-matrix-fs.h> + + + + + +namespace Geom { namespace NL { + + +namespace detail +{ + +/* + * helper routines + */ + +inline +int sgn_prod (int x, int y) +{ + if (x == 0 || y == 0) return 0; + if (x == y) return 1; + return -1; +} + +inline +bool abs_less (double x, double y) +{ + return (std::fabs(x) < std::fabs(y)); +} + + +/* + * trace K-th of symmetric matrix S of order N + */ +template +struct trace +{ + static double evaluate(const ConstBaseSymmetricMatrix &S); +}; + +template +struct trace<1,N> +{ + static + double evaluate (const ConstBaseSymmetricMatrix & S) + { + double t = 0; + for (size_t i = 0; i < N; ++i) + { + t += S(i,i); + } + return t; + } +}; + +template +struct trace +{ + static + double evaluate (const ConstBaseSymmetricMatrix & S) + { + Matrix M(S); + return det(M); + } +}; + +/* + * trace for symmetric matrix of order 2 + */ +template <> +struct trace<1,2> +{ + static + double evaluate (const ConstBaseSymmetricMatrix<2> & S) + { + return (S.get<0,0>() + S.get<1,1>()); + } +}; + +template <> +struct trace<2,2> +{ + static + double evaluate (const ConstBaseSymmetricMatrix<2> & S) + { + return (S.get<0,0>() * S.get<1,1>() - S.get<0,1>() * S.get<1,0>()); + } +}; + + +/* + * trace for symmetric matrix of order 3 + */ +template <> +struct trace<1,3> +{ + static + double evaluate (const ConstBaseSymmetricMatrix<3> & S) + { + return (S.get<0,0>() + S.get<1,1>() + S.get<2,2>()); + } +}; + +template <> +struct trace<2,3> +{ + static + double evaluate (const ConstBaseSymmetricMatrix<3> & S) + { + double a00 = S.get<1,1>() * S.get<2,2>() - S.get<1,2>() * S.get<2,1>(); + double a11 = S.get<0,0>() * S.get<2,2>() - S.get<0,2>() * S.get<2,0>(); + double a22 = S.get<0,0>() * S.get<1,1>() - S.get<0,1>() * S.get<1,0>(); + return (a00 + a11 + a22); + } +}; + +template <> +struct trace<3,3> +{ + static + double evaluate (const ConstBaseSymmetricMatrix<3> & S) + { + double d = S.get<0,0>() * S.get<1,1>() * S.get<2,2>(); + d += (2 * S.get<1,0>() * S.get<2,0>() * S.get<2,1>()); + d -= (S.get<0,0>() * S.get<2,1>() * S.get<2,1>()); + d -= (S.get<1,1>() * S.get<2,0>() * S.get<2,0>()); + d -= (S.get<2,2>() * S.get<1,0>() * S.get<1,0>()); + return d; + } +}; + + +/* + * sign of trace K-th + */ +template +struct trace_sgn +{ + static + int evaluate (const ConstBaseSymmetricMatrix & S) + { + double d = trace::evaluate(S); + return sgn(d); + } +}; + + +/* + * sign of trace for symmetric matrix of order 2 + */ +template <> +struct trace_sgn<2,2> +{ + static + int evaluate (const ConstBaseSymmetricMatrix<2> & S) + { + double m00 = S.get<0,0>(); + double m10 = S.get<1,0>(); + double m11 = S.get<1,1>(); + + int sm00 = sgn (m00); + int sm10 = sgn (m10); + int sm11 = sgn (m11); + + if (sm10 == 0) + { + return sgn_prod (sm00, sm11); + } + else + { + int sm00m11 = sgn_prod (sm00, sm11); + if (sm00m11 == 1) + { + int e00, e10, e11; + double f00 = std::frexp (m00, &e00); + double f10 = std::frexp (m10, &e10); + double f11 = std::frexp (m11, &e11); + + int e0011 = e00 + e11; + int e1010 = e10 << 1; + int ed = e0011 - e1010; + + if (ed > 1) + { + return 1; + } + else if (ed < -1) + { + return -1; + } + else + { + double d = std::ldexp (f00 * f11, ed) - f10 * f10; + //std::cout << "trace_sgn<2,2>: det = " << d << std::endl; + double eps = std::ldexp (1, -50); + if (std::fabs(d) < eps) return 0; + return sgn (d); + } + } + return -1; + } + } +}; + + +/* + * sign of trace for symmetric matrix of order 3 + */ +template <> +struct trace_sgn<2,3> +{ + static + int evaluate (const ConstBaseSymmetricMatrix<3> & S) + { + double eps = std::ldexp (1, -50); + double t[6]; + + t[0] = S.get<1,1>() * S.get<2,2>(); + t[1] = - S.get<1,2>() * S.get<2,1>(); + t[2] = S.get<0,0>() * S.get<2,2>(); + t[3] = - S.get<0,2>() * S.get<2,0>(); + t[4] = S.get<0,0>() * S.get<1,1>(); + t[5] = - S.get<0,1>() * S.get<1,0>(); + + + double* maxp = std::max_element (t, t+6, abs_less); + int em; + std::frexp(*maxp, &em); + double d = 0; + for (size_t i = 0; i < 6; ++i) + { + d += t[i]; + } + double r = std::fabs (std::ldexp (d, -em)); // relative error + //std::cout << "trace_sgn<2,3>: d = " << d << std::endl; + //std::cout << "trace_sgn<2,3>: r = " << r << std::endl; + if (r < eps) return 0; + if (d > 0) return 1; + return -1; + } +}; + +template <> +struct trace_sgn<3,3> +{ + static + int evaluate (const ConstBaseSymmetricMatrix<3> & S) + { + + double eps = std::ldexp (1, -48); + double t[5]; + + t[0] = S.get<0,0>() * S.get<1,1>() * S.get<2,2>(); + t[1] = 2 * S.get<1,0>() * S.get<2,0>() * S.get<2,1>(); + t[2] = -(S.get<0,0>() * S.get<2,1>() * S.get<2,1>()); + t[3] = -(S.get<1,1>() * S.get<2,0>() * S.get<2,0>()); + t[4] = -(S.get<2,2>() * S.get<1,0>() * S.get<1,0>()); + + double* maxp = std::max_element (t, t+5, abs_less); + int em; + std::frexp(*maxp, &em); + double d = 0; + for (size_t i = 0; i < 5; ++i) + { + d += t[i]; + } + //std::cout << "trace_sgn<3,3>: d = " << d << std::endl; + double r = std::fabs (std::ldexp (d, -em)); // relative error + //std::cout << "trace_sgn<3,3>: r = " << r << std::endl; + + if (r < eps) return 0; + if (d > 0) return 1; + return -1; + } +}; // end struct trace_sgn<3,3> + +} // end namespace detail + + +template +inline +double trace (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace::evaluate(_matrix); +} + +template +inline +double trace (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace<1, N>::evaluate(_matrix); +} + +template +inline +double det (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace::evaluate(_matrix); +} + + +template +inline +int trace_sgn (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace_sgn::evaluate(_matrix); +} + +template +inline +int trace_sgn (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace_sgn<1, N>::evaluate(_matrix); +} + +template +inline +int det_sgn (const ConstBaseSymmetricMatrix & _matrix) +{ + return detail::trace_sgn::evaluate(_matrix); +} + +/* +template +inline +size_t rank (const ConstBaseSymmetricMatrix & S) +{ + THROW_NOTIMPLEMENTED(); + return 0; +} + +template <> +inline +size_t rank<2> (const ConstBaseSymmetricMatrix<2> & S) +{ + if (S.is_zero()) return 0; + double d = S.get<0,0>() * S.get<1,1>() - S.get<0,1>() * S.get<1,0>(); + if (d != 0) return 2; + return 1; +} + +template <> +inline +size_t rank<3> (const ConstBaseSymmetricMatrix<3> & S) +{ + if (S.is_zero()) return 0; + + double a20 = S.get<0,1>() * S.get<1,2>() - S.get<0,2>() * S.get<1,1>(); + double a21 = S.get<0,2>() * S.get<1,0>() - S.get<0,0>() * S.get<1,2>(); + double a22 = S.get<0,0>() * S.get<1,1>() - S.get<0,1>() * S.get<1,0>(); + double d = a20 * S.get<2,0>() + a21 * S.get<2,1>() + a22 * S.get<2,2>(); + + if (d != 0) return 3; + + if (a20 != 0 || a21 != 0 || a22 != 0) return 2; + + double a00 = S.get<1,1>() * S.get<2,2>() - S.get<1,2>() * S.get<2,1>(); + if (a00 != 0) return 2; + + double a10 = S.get<0,2>() * S.get<2,1>() - S.get<0,1>() * S.get<2,2>(); + if (a10 != 0) return 2; + + double a11 = S.get<0,0>() * S.get<2,2>() - S.get<0,2>() * S.get<2,0>(); + if (a11 != 0) return 2; + + return 1; +} +*/ + +} /* end namespace NL*/ } /* end namespace Geom*/ + + + + +#endif // _NL_TRACE_H_ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/symmetric-matrix-fs.h b/src/2geom/numeric/symmetric-matrix-fs.h new file mode 100644 index 0000000..2fadd69 --- /dev/null +++ b/src/2geom/numeric/symmetric-matrix-fs.h @@ -0,0 +1,733 @@ +/* + * SymmetricMatrix, ConstSymmetricMatrixView, SymmetricMatrixView template + * classes implement fixed size symmetric matrix; "views" mimic the semantic + * of C++ references: any operation performed on a "view" is actually performed + * on the "viewed object" + * + * Authors: + * Marco Cecchetti + * + * Copyright 2009 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#ifndef _NL_SYMMETRIC_MATRIX_FS_H_ +#define _NL_SYMMETRIC_MATRIX_FS_H_ + + +#include <2geom/numeric/vector.h> +#include <2geom/numeric/matrix.h> +#include <2geom/utils.h> +#include <2geom/exception.h> + +#include + +#include +#include // for std::pair +#include // for std::swap, std::copy +#include +#include + + + +namespace Geom { namespace NL { + + +namespace detail +{ + +template +struct index +{ + static const size_t K = index::K; +}; + +template +struct index +{ + static const size_t K = (((I+1) * I) >> 1) + J; +}; + +} // end namespace detail + + + + +template +class ConstBaseSymmetricMatrix; + +template +class BaseSymmetricMatrix; + +template +class SymmetricMatrix; + +template +class ConstSymmetricMatrixView; + +template +class SymmetricMatrixView; + + + +// declaration needed for friend clause +template +bool operator== (ConstBaseSymmetricMatrix const& _smatrix1, + ConstBaseSymmetricMatrix const& _smatrix2); + + + + +template +class ConstBaseSymmetricMatrix +{ + public: + const static size_t DIM = N; + const static size_t DATA_SIZE = ((DIM+1) * DIM) / 2; + + public: + + ConstBaseSymmetricMatrix (VectorView const& _data) + : m_data(_data) + { + } + + double operator() (size_t i, size_t j) const + { + return m_data[get_index(i,j)]; + } + + template + double get() const + { + BOOST_STATIC_ASSERT ((I < N && J < N)); + return m_data[detail::index::K]; + } + + + size_t rows() const + { + return DIM; + } + + size_t columns() const + { + return DIM; + } + + bool is_zero() const + { + return m_data.is_zero(); + } + + bool is_positive() const + { + return m_data.is_positive(); + } + + bool is_negative() const + { + return m_data.is_negative(); + } + + bool is_non_negative() const + { + return m_data.is_non_negative(); + } + + double min() const + { + return m_data.min(); + } + + double max() const + { + return m_data.max(); + } + + std::pair + min_index() const + { + std::pair indices(0,0); + double min_value = m_data[0]; + for (size_t i = 1; i < DIM; ++i) + { + for (size_t j = 0; j <= i; ++j) + { + if (min_value > (*this)(i,j)) + { + min_value = (*this)(i,j); + indices.first = i; + indices.second = j; + } + } + } + return indices; + } + + std::pair + max_index() const + { + std::pair indices(0,0); + double max_value = m_data[0]; + for (size_t i = 1; i < DIM; ++i) + { + for (size_t j = 0; j <= i; ++j) + { + if (max_value < (*this)(i,j)) + { + max_value = (*this)(i,j); + indices.first = i; + indices.second = j; + } + } + } + return indices; + } + + size_t min_on_row_index (size_t i) const + { + size_t idx = 0; + double min_value = (*this)(i,0); + for (size_t j = 1; j < DIM; ++j) + { + if (min_value > (*this)(i,j)) + { + min_value = (*this)(i,j); + idx = j; + } + } + return idx; + } + + size_t max_on_row_index (size_t i) const + { + size_t idx = 0; + double max_value = (*this)(i,0); + for (size_t j = 1; j < DIM; ++j) + { + if (max_value < (*this)(i,j)) + { + max_value = (*this)(i,j); + idx = j; + } + } + return idx; + } + + size_t min_on_column_index (size_t j) const + { + return min_on_row_index(j); + } + + size_t max_on_column_index (size_t j) const + { + return max_on_row_index(j); + } + + size_t min_on_diag_index () const + { + size_t idx = 0; + double min_value = (*this)(0,0); + for (size_t i = 1; i < DIM; ++i) + { + if (min_value > (*this)(i,i)) + { + min_value = (*this)(i,i); + idx = i; + } + } + return idx; + } + + size_t max_on_diag_index () const + { + size_t idx = 0; + double max_value = (*this)(0,0); + for (size_t i = 1; i < DIM; ++i) + { + if (max_value < (*this)(i,i)) + { + max_value = (*this)(i,i); + idx = i; + } + } + return idx; + } + + std::string str() const; + + ConstSymmetricMatrixView main_minor_const_view() const; + + SymmetricMatrix operator- () const; + + Vector operator* (ConstVectorView _vector) const + { + assert (_vector.size() == DIM); + Vector result(DIM, 0.0); + + for (size_t i = 0; i < DIM; ++i) + { + for (size_t j = 0; j < DIM; ++j) + { + result[i] += (*this)(i,j) * _vector[j]; + } + } + return result; + } + + protected: + static size_t get_index (size_t i, size_t j) + { + if (i < j) return get_index (j, i); + size_t k = (i+1) * i; + k >>= 1; + k += j; + return k; + } + + protected: + ConstVectorView get_data() const + { + return m_data; + } + + friend + bool operator== (ConstBaseSymmetricMatrix const& _smatrix1, + ConstBaseSymmetricMatrix const& _smatrix2); + + protected: + VectorView m_data; + +}; //end ConstBaseSymmetricMatrix + + +template +class BaseSymmetricMatrix : public ConstBaseSymmetricMatrix +{ + public: + typedef ConstBaseSymmetricMatrix base_type; + + + public: + + BaseSymmetricMatrix (VectorView const& _data) + : base_type(_data) + { + } + + double operator() (size_t i, size_t j) const + { + return m_data[base_type::get_index(i,j)]; + } + + double& operator() (size_t i, size_t j) + { + return m_data[base_type::get_index(i,j)]; + } + + template + double& get() + { + BOOST_STATIC_ASSERT ((I < N && J < N)); + return m_data[detail::index::K]; + } + + void set_all (double x) + { + m_data.set_all(x); + } + + SymmetricMatrixView main_minor_view(); + + BaseSymmetricMatrix& transpose() const + { + return (*this); + } + + BaseSymmetricMatrix& translate (double c) + { + m_data.translate(c); + return (*this); + } + + BaseSymmetricMatrix& scale (double c) + { + m_data.scale(c); + return (*this); + } + + BaseSymmetricMatrix& operator+= (base_type const& _smatrix) + { + m_data += (static_cast(_smatrix).m_data); + return (*this); + } + + BaseSymmetricMatrix& operator-= (base_type const& _smatrix) + { + m_data -= (static_cast(_smatrix).m_data); + return (*this); + } + + using base_type::DIM; + using base_type::DATA_SIZE; + using base_type::m_data; + using base_type::operator-; + using base_type::operator*; + +}; //end BaseSymmetricMatrix + + +template +class SymmetricMatrix : public BaseSymmetricMatrix +{ + public: + typedef BaseSymmetricMatrix base_type; + typedef typename base_type::base_type base_base_type; + + using base_type::DIM; + using base_type::DATA_SIZE; + using base_type::m_data; + + public: + SymmetricMatrix () + : base_type (VectorView(m_adata, DATA_SIZE)) + { + } + + explicit + SymmetricMatrix (ConstVectorView _data) + : base_type (VectorView(m_adata, DATA_SIZE)) + { + assert (_data.size() == DATA_SIZE); + m_data = _data; + } + + explicit + SymmetricMatrix (const double _data[DATA_SIZE]) + : base_type (VectorView(m_adata, DATA_SIZE)) + { + std::copy (_data, _data + DATA_SIZE, m_adata); + } + + SymmetricMatrix (SymmetricMatrix const& _smatrix) + : base_type (VectorView(m_adata, DATA_SIZE)) + { + m_data = _smatrix.m_data; + } + + explicit + SymmetricMatrix (base_base_type const& _smatrix) + : base_type (VectorView(m_adata, DATA_SIZE)) + { + m_data = static_cast &>(_smatrix).m_data; + } + + explicit + SymmetricMatrix (ConstMatrixView const& _matrix) + : base_type (VectorView(m_adata, DATA_SIZE)) + { + assert (_matrix.rows() == N && _matrix.columns() == N); + for (size_t i = 0; i < N; ++i) + for (size_t j = 0; j <= i ; ++j) + (*this)(i,j) = _matrix(i,j); + } + + SymmetricMatrix& operator= (SymmetricMatrix const& _smatrix) + { + m_data = _smatrix.m_data; + return (*this); + } + + SymmetricMatrix& operator= (base_base_type const& _smatrix) + { + + m_data = static_cast &>(_smatrix).m_data; + return (*this); + } + + SymmetricMatrix& operator= (ConstMatrixView const& _matrix) + { + assert (_matrix.rows() == N && _matrix.columns() == N); + for (size_t i = 0; i < N; ++i) + for (size_t j = 0; j <= i ; ++j) + (*this)(i,j) = _matrix(i,j); + + return (*this); + } + + // needed for accessing m_adata + friend class ConstSymmetricMatrixView; + friend class SymmetricMatrixView; + private: + double m_adata[DATA_SIZE]; +}; //end SymmetricMatrix + + +template +class ConstSymmetricMatrixView : public ConstBaseSymmetricMatrix +{ + public: + typedef ConstBaseSymmetricMatrix base_type; + + using base_type::DIM; + using base_type::DATA_SIZE; + using base_type::m_data; + + + public: + + explicit + ConstSymmetricMatrixView (ConstVectorView _data) + : base_type (const_vector_view_cast(_data)) + { + assert (_data.size() == DATA_SIZE); + } + + explicit + ConstSymmetricMatrixView (const double _data[DATA_SIZE]) + : base_type (const_vector_view_cast (ConstVectorView (_data, DATA_SIZE))) + { + } + + ConstSymmetricMatrixView (const ConstSymmetricMatrixView & _smatrix) + : base_type (_smatrix.m_data) + { + } + + ConstSymmetricMatrixView (const base_type & _smatrix) + : base_type (static_cast(_smatrix).m_data) + { + } + +}; //end SymmetricMatrix + + +// declaration needed for friend clause +template +void swap_view(SymmetricMatrixView & m1, SymmetricMatrixView & m2); + + +template +class SymmetricMatrixView : public BaseSymmetricMatrix +{ + public: + typedef BaseSymmetricMatrix base_type; + typedef typename base_type::base_type base_base_type; + + using base_type::DIM; + using base_type::DATA_SIZE; + using base_type::m_data; + + public: + + explicit + SymmetricMatrixView (VectorView _data) + : base_type (_data) + { + assert (_data.size() == DATA_SIZE); + } + + explicit + SymmetricMatrixView (double _data[DATA_SIZE]) + : base_type (VectorView (_data, DATA_SIZE)) + { + } + + SymmetricMatrixView (const SymmetricMatrixView & _smatrix) + : base_type (_smatrix.m_data) + { + } + + SymmetricMatrixView (SymmetricMatrix & _smatrix) + : base_type (VectorView (_smatrix.m_adata, DATA_SIZE)) + { + } + + SymmetricMatrixView& operator= (const SymmetricMatrixView & _smatrix) + { + m_data = _smatrix.m_data; + return (*this); + } + + SymmetricMatrixView& operator= (const base_base_type & _smatrix) + { + m_data = static_cast &>(_smatrix).m_data; + return (*this); + } + + friend + void swap_view(SymmetricMatrixView & m1, SymmetricMatrixView & m2); + +}; //end SymmetricMatrix + + + + +/* + * class ConstBaseSymmetricMatrix methods + */ + +template +inline +std::string ConstBaseSymmetricMatrix::str() const +{ + std::ostringstream oss; + oss << (*this); + return oss.str(); +} + +template +inline +ConstSymmetricMatrixView +ConstBaseSymmetricMatrix::main_minor_const_view() const +{ + ConstVectorView data(m_data.get_gsl_vector()->data, DATA_SIZE - DIM); + ConstSymmetricMatrixView mm(data); + return mm; +} + +template +inline +SymmetricMatrix ConstBaseSymmetricMatrix::operator- () const +{ + SymmetricMatrix result; + for (size_t i = 0; i < DATA_SIZE; ++i) + { + result.m_data[i] = -m_data[i]; + } + return result; +} + + +/* + * class ConstBaseSymmetricMatrix friend free functions + */ + +template +inline +bool operator== (ConstBaseSymmetricMatrix const& _smatrix1, + ConstBaseSymmetricMatrix const& _smatrix2) +{ + return (_smatrix1.m_data == _smatrix2.m_data); +} + +/* + * class ConstBaseSymmetricMatrix related free functions + */ + +template< size_t N, class charT > +inline +std::basic_ostream & +operator<< (std::basic_ostream & os, + const ConstBaseSymmetricMatrix & _matrix) +{ + os << "[[" << _matrix(0,0); + for (size_t j = 1; j < N; ++j) + { + os << ", " << _matrix(0,j); + } + os << "]"; + for (size_t i = 1; i < N; ++i) + { + os << "\n [" << _matrix(i,0); + for (size_t j = 1; j < N; ++j) + { + os << ", " << _matrix(i,j); + } + os << "]"; + } + os << "]"; + return os; +} + + +/* + * class ConstBaseSymmetricMatrix specialized methods + */ + +template<> +inline +size_t ConstBaseSymmetricMatrix<2>::get_index (size_t i, size_t j) +{ + return (i+j); +} + +template<> +inline +size_t ConstBaseSymmetricMatrix<3>::get_index (size_t i, size_t j) +{ + size_t k = i + j; + if (i == 2 || j == 2) ++k; + return k; +} + + +/* + * class BaseSymmetricMatrix methods + */ + +template +inline +SymmetricMatrixView BaseSymmetricMatrix::main_minor_view() +{ + VectorView data(m_data.get_gsl_vector()->data, DATA_SIZE - DIM); + SymmetricMatrixView mm(data); + return mm; +} + + +/* + * class SymmetricMatrixView friend free functions + */ + +template +inline +void swap_view(SymmetricMatrixView & m1, SymmetricMatrixView & m2) +{ + swap_view(m1.m_data, m2.m_data); +} + +} /* end namespace NL*/ } /* end namespace Geom*/ + + + + +#endif // _NL_SYMMETRIC_MATRIX_FS_H_ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/numeric/vector.h b/src/2geom/numeric/vector.h new file mode 100644 index 0000000..f28289f --- /dev/null +++ b/src/2geom/numeric/vector.h @@ -0,0 +1,594 @@ +/* + * Vector, VectorView, ConstVectorView classes wrap the gsl vector routines; + * "views" mimic the semantic of C++ references: any operation performed + * on a "view" is actually performed on the "viewed object" + * + * Authors: + * Marco Cecchetti + * + * Copyright 2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + + + +#ifndef _NL_VECTOR_H_ +#define _NL_VECTOR_H_ + +#include +#include // for std::swap +#include +#include +#include + + +#include +#include + + +namespace Geom { namespace NL { + +namespace detail +{ + +class BaseVectorImpl +{ + public: + double const& operator[](size_t i) const + { + return *gsl_vector_const_ptr(m_vector, i); + } + + const gsl_vector* get_gsl_vector() const + { + return m_vector; + } + bool is_zero() const + { + return gsl_vector_isnull(m_vector); + } + + bool is_positive() const + { + for ( size_t i = 0; i < size(); ++i ) + { + if ( (*this)[i] <= 0 ) return false; + } + return true; + } + + bool is_negative() const + { + for ( size_t i = 0; i < size(); ++i ) + { + if ( (*this)[i] >= 0 ) return false; + } + return true; + } + + bool is_non_negative() const + { + for ( size_t i = 0; i < size(); ++i ) + { + if ( (*this)[i] < 0 ) return false; + } + return true; + } + + double max() const + { + return gsl_vector_max(m_vector); + } + + double min() const + { + return gsl_vector_min(m_vector); + } + + size_t max_index() const + { + return gsl_vector_max_index(m_vector); + } + + size_t min_index() const + { + return gsl_vector_min_index(m_vector); + } + + size_t size() const + { + return m_size; + } + + std::string str() const; + + virtual ~BaseVectorImpl() + { + } + + protected: + size_t m_size; + gsl_vector* m_vector; + +}; // end class BaseVectorImpl + + +inline +bool operator== (BaseVectorImpl const& v1, BaseVectorImpl const& v2) +{ + if (v1.size() != v2.size()) return false; + + for (size_t i = 0; i < v1.size(); ++i) + { + if (v1[i] != v2[i]) return false; + } + return true; +} + +template< class charT > +inline +std::basic_ostream & +operator<< (std::basic_ostream & os, const BaseVectorImpl & _vector) +{ + if (_vector.size() == 0 ) return os; + os << "[" << _vector[0]; + for (unsigned int i = 1; i < _vector.size(); ++i) + { + os << ", " << _vector[i]; + } + os << "]"; + return os; +} + +inline +std::string BaseVectorImpl::str() const +{ + std::ostringstream oss; + oss << (*this); + return oss.str(); +} + +inline +double dot(BaseVectorImpl const& v1, BaseVectorImpl const& v2) +{ + double result; + gsl_blas_ddot(v1.get_gsl_vector(), v2.get_gsl_vector(), &result); + return result; +} + + +class VectorImpl : public BaseVectorImpl +{ + public: + typedef BaseVectorImpl base_type; + + public: + void set_all(double x) + { + gsl_vector_set_all(m_vector, x); + } + + void set_basis(size_t i) + { + gsl_vector_set_basis(m_vector, i); + } + + using base_type::operator[]; + + double & operator[](size_t i) + { + return *gsl_vector_ptr(m_vector, i); + } + + using base_type::get_gsl_vector; + + gsl_vector* get_gsl_vector() + { + return m_vector; + } + + void swap_elements(size_t i, size_t j) + { + gsl_vector_swap_elements(m_vector, i, j); + } + + void reverse() + { + gsl_vector_reverse(m_vector); + } + + VectorImpl & scale(double x) + { + gsl_vector_scale(m_vector, x); + return (*this); + } + + VectorImpl & translate(double x) + { + gsl_vector_add_constant(m_vector, x); + return (*this); + } + + VectorImpl & operator+=(base_type const& _vector) + { + gsl_vector_add(m_vector, _vector.get_gsl_vector()); + return (*this); + } + + VectorImpl & operator-=(base_type const& _vector) + { + gsl_vector_sub(m_vector, _vector.get_gsl_vector()); + return (*this); + } + +}; // end class VectorImpl + +} // end namespace detail + + +using detail::operator==; +using detail::operator<<; + +class Vector : public detail::VectorImpl +{ + public: + typedef detail::VectorImpl base_type; + + public: + Vector(size_t n) + { + m_size = n; + m_vector = gsl_vector_alloc(n); + } + + Vector(size_t n, double x) + { + m_size = n; + m_vector = gsl_vector_alloc(n); + gsl_vector_set_all(m_vector, x); + } + + // create a vector with n elements all set to zero + // but the i-th that is set to 1 + Vector(size_t n, size_t i) + { + m_size = n; + m_vector = gsl_vector_alloc(n); + gsl_vector_set_basis(m_vector, i); + } + + Vector(Vector const& _vector) + : base_type() + { + m_size = _vector.size(); + m_vector = gsl_vector_alloc(size()); + gsl_vector_memcpy(m_vector, _vector.m_vector); + } + + explicit + Vector(base_type::base_type const& _vector) + { + m_size = _vector.size(); + m_vector = gsl_vector_alloc(size()); + gsl_vector_memcpy(m_vector, _vector.get_gsl_vector()); + } + + virtual ~Vector() + { + gsl_vector_free(m_vector); + } + + + Vector & operator=(Vector const& _vector) + { + assert( size() == _vector.size() ); + gsl_vector_memcpy(m_vector, _vector.m_vector); + return (*this); + } + + Vector & operator=(base_type::base_type const& _vector) + { + assert( size() == _vector.size() ); + gsl_vector_memcpy(m_vector, _vector.get_gsl_vector()); + return (*this); + } + + Vector & scale(double x) + { + return static_cast( base_type::scale(x) ); + } + + Vector & translate(double x) + { + return static_cast( base_type::translate(x) ); + } + + Vector & operator+=(base_type::base_type const& _vector) + { + return static_cast( base_type::operator+=(_vector) ); + } + + Vector & operator-=(base_type::base_type const& _vector) + { + return static_cast( base_type::operator-=(_vector) ); + } + + friend + void swap(Vector & v1, Vector & v2); + friend + void swap_any(Vector & v1, Vector & v2); + +}; // end class Vector + + +// warning! these operations invalidate any view of the passed vector objects +inline +void swap(Vector & v1, Vector & v2) +{ + assert(v1.size() == v2.size()); + using std::swap; + swap(v1.m_vector, v2.m_vector); +} + +inline +void swap_any(Vector & v1, Vector & v2) +{ + using std::swap; + swap(v1.m_vector, v2.m_vector); + swap(v1.m_size, v2.m_size); +} + + +class ConstVectorView : public detail::BaseVectorImpl +{ + public: + typedef detail::BaseVectorImpl base_type; + + public: + ConstVectorView(const base_type & _vector, size_t n, size_t offset = 0) + : m_vector_view( gsl_vector_const_subvector(_vector.get_gsl_vector(), offset, n) ) + { + m_size = n; + m_vector = const_cast( &(m_vector_view.vector) ); + } + + ConstVectorView(const base_type & _vector, size_t n, size_t offset , size_t stride) + : m_vector_view( gsl_vector_const_subvector_with_stride(_vector.get_gsl_vector(), offset, stride, n) ) + { + m_size = n; + m_vector = const_cast( &(m_vector_view.vector) ); + } + + ConstVectorView(const double* _vector, size_t n, size_t offset = 0) + : m_vector_view( gsl_vector_const_view_array(_vector + offset, n) ) + { + m_size = n; + m_vector = const_cast( &(m_vector_view.vector) ); + } + + ConstVectorView(const double* _vector, size_t n, size_t offset, size_t stride) + : m_vector_view( gsl_vector_const_view_array_with_stride(_vector + offset, stride, n) ) + { + m_size = n; + m_vector = const_cast( &(m_vector_view.vector) ); + } + + explicit + ConstVectorView(gsl_vector_const_view _gsl_vector_view) + : m_vector_view(_gsl_vector_view) + { + m_vector = const_cast( &(m_vector_view.vector) ); + m_size = m_vector->size; + } + + explicit + ConstVectorView(const std::vector& _vector) + : m_vector_view( gsl_vector_const_view_array(&(_vector[0]), _vector.size()) ) + { + m_vector = const_cast( &(m_vector_view.vector) ); + m_size = _vector.size(); + } + + ConstVectorView(const ConstVectorView & _vector) + : base_type(), + m_vector_view(_vector.m_vector_view) + { + m_size = _vector.size(); + m_vector = const_cast( &(m_vector_view.vector) ); + } + + ConstVectorView(const base_type & _vector) + : m_vector_view(gsl_vector_const_subvector(_vector.get_gsl_vector(), 0, _vector.size())) + { + m_size = _vector.size(); + m_vector = const_cast( &(m_vector_view.vector) ); + } + + private: + gsl_vector_const_view m_vector_view; + +}; // end class ConstVectorView + + + + +class VectorView : public detail::VectorImpl +{ + public: + typedef detail::VectorImpl base_type; + + public: + VectorView(base_type & _vector, size_t n, size_t offset = 0, size_t stride = 1) + { + m_size = n; + if (stride == 1) + { + m_vector_view + = gsl_vector_subvector(_vector.get_gsl_vector(), offset, n); + m_vector = &(m_vector_view.vector); + } + else + { + m_vector_view + = gsl_vector_subvector_with_stride(_vector.get_gsl_vector(), offset, stride, n); + m_vector = &(m_vector_view.vector); + } + } + + VectorView(double* _vector, size_t n, size_t offset = 0, size_t stride = 1) + { + m_size = n; + if (stride == 1) + { + m_vector_view + = gsl_vector_view_array(_vector + offset, n); + m_vector = &(m_vector_view.vector); + } + else + { + m_vector_view + = gsl_vector_view_array_with_stride(_vector + offset, stride, n); + m_vector = &(m_vector_view.vector); + } + + } + + VectorView(const VectorView & _vector) + : base_type() + { + m_size = _vector.size(); + m_vector_view = _vector.m_vector_view; + m_vector = &(m_vector_view.vector); + } + + VectorView(Vector & _vector) + { + m_size = _vector.size(); + m_vector_view = gsl_vector_subvector(_vector.get_gsl_vector(), 0, size()); + m_vector = &(m_vector_view.vector); + } + + explicit + VectorView(gsl_vector_view _gsl_vector_view) + : m_vector_view(_gsl_vector_view) + { + m_vector = &(m_vector_view.vector); + m_size = m_vector->size; + } + + explicit + VectorView(std::vector & _vector) + { + m_size = _vector.size(); + m_vector_view = gsl_vector_view_array(&(_vector[0]), _vector.size()); + m_vector = &(m_vector_view.vector); + } + + VectorView & operator=(VectorView const& _vector) + { + assert( size() == _vector.size() ); + gsl_vector_memcpy(m_vector, _vector.get_gsl_vector()); + return (*this); + } + + VectorView & operator=(base_type::base_type const& _vector) + { + assert( size() == _vector.size() ); + gsl_vector_memcpy(m_vector, _vector.get_gsl_vector()); + return (*this); + } + + VectorView & scale(double x) + { + return static_cast( base_type::scale(x) ); + } + + VectorView & translate(double x) + { + return static_cast( base_type::translate(x) ); + } + + VectorView & operator+=(base_type::base_type const& _vector) + { + return static_cast( base_type::operator+=(_vector) ); + } + + VectorView & operator-=(base_type::base_type const& _vector) + { + return static_cast( base_type::operator-=(_vector) ); + } + + friend + void swap_view(VectorView & v1, VectorView & v2); + + private: + gsl_vector_view m_vector_view; + +}; // end class VectorView + + +inline +void swap_view(VectorView & v1, VectorView & v2) +{ + assert( v1.size() == v2.size() ); + using std::swap; + swap(v1.m_vector_view, v2.m_vector_view); // not swap m_vector too +} + +inline +const VectorView & const_vector_view_cast (const ConstVectorView & view) +{ + const detail::BaseVectorImpl & bvi + = static_cast(view); + const VectorView & vv = reinterpret_cast(bvi); + return vv; +} + +inline +VectorView & const_vector_view_cast (ConstVectorView & view) +{ + detail::BaseVectorImpl & bvi + = static_cast(view); + VectorView & vv = reinterpret_cast(bvi); + return vv; +} + + +} } // end namespaces + + +#endif /*_NL_VECTOR_H_*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/ord.h b/src/2geom/ord.h new file mode 100644 index 0000000..e190a4a --- /dev/null +++ b/src/2geom/ord.h @@ -0,0 +1,80 @@ +/** @file + * @brief Comparator template + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_ORD_H +#define LIB2GEOM_SEEN_ORD_H + +namespace { + +enum Cmp { + LESS_THAN=-1, + GREATER_THAN=1, + EQUAL_TO=0 +}; + +static inline Cmp operator-(Cmp x) { + switch(x) { + case LESS_THAN: + return GREATER_THAN; + case GREATER_THAN: + return LESS_THAN; + case EQUAL_TO: + return EQUAL_TO; + } +} + +template +inline Cmp cmp(T1 const &a, T2 const &b) { + if ( a < b ) { + return LESS_THAN; + } else if ( b < a ) { + return GREATER_THAN; + } else { + return EQUAL_TO; + } +} + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path-intersection.cpp b/src/2geom/path-intersection.cpp new file mode 100644 index 0000000..07e38ba --- /dev/null +++ b/src/2geom/path-intersection.cpp @@ -0,0 +1,730 @@ +#include <2geom/path-intersection.h> + +#include <2geom/ord.h> + +//for path_direction: +#include <2geom/sbasis-geometric.h> +#include <2geom/line.h> +#ifdef HAVE_GSL +#include +#include +#endif + +namespace Geom { + +/// Compute winding number of the path at the specified point +int winding(Path const &path, Point const &p) { + return path.winding(p); +} + +/** + * This function should only be applied to simple paths (regions), as otherwise + * a boolean winding direction is undefined. It returns true for fill, false for + * hole. Defaults to using the sign of area when it reaches funny cases. + */ +bool path_direction(Path const &p) { + if(p.empty()) return false; + + /*goto doh; + //could probably be more efficient, but this is a quick job + double y = p.initialPoint()[Y]; + double x = p.initialPoint()[X]; + Cmp res = cmp(p[0].finalPoint()[Y], y); + for(unsigned i = 1; i < p.size(); i++) { + Cmp final_to_ray = cmp(p[i].finalPoint()[Y], y); + Cmp initial_to_ray = cmp(p[i].initialPoint()[Y], y); + // if y is included, these will have opposite values, giving order. + Cmp c = cmp(final_to_ray, initial_to_ray); + if(c != EQUAL_TO) { + std::vector rs = p[i].roots(y, Y); + for(unsigned j = 0; j < rs.size(); j++) { + double nx = p[i].valueAt(rs[j], X); + if(nx > x) { + x = nx; + res = c; + } + } + } else if(final_to_ray == EQUAL_TO) goto doh; + } + return res < 0; + + doh:*/ + //Otherwise fallback on area + + Piecewise > pw = p.toPwSb(); + double area; + Point centre; + Geom::centroid(pw, centre, area); + return area > 0; +} + +//pair intersect code based on njh's pair-intersect + +/** A little sugar for appending a list to another */ +template +void append(T &a, T const &b) { + a.insert(a.end(), b.begin(), b.end()); +} + +/** + * Finds the intersection between the lines defined by A0 & A1, and B0 & B1. + * Returns through the last 3 parameters, returning the t-values on the lines + * and the cross-product of the deltas (a useful byproduct). The return value + * indicates if the time values are within their proper range on the line segments. + */ +bool +linear_intersect(Point const &A0, Point const &A1, Point const &B0, Point const &B1, + double &tA, double &tB, double &det) { + bool both_lines_non_zero = (!are_near(A0, A1)) && (!are_near(B0, B1)); + + // Cramer's rule as cross products + Point Ad = A1 - A0, + Bd = B1 - B0, + d = B0 - A0; + det = cross(Ad, Bd); + + double det_rel = det; // Calculate the determinant of the normalized vectors + if (both_lines_non_zero) { + det_rel /= Ad.length(); + det_rel /= Bd.length(); + } + + if( fabs(det_rel) < 1e-12 ) { // If the cross product is NEARLY zero, + // Then one of the linesegments might have length zero + if (both_lines_non_zero) { + // If that's not the case, then we must have either: + // - parallel lines, with no intersections, or + // - coincident lines, with an infinite number of intersections + // Either is quite useless, so we'll just bail out + return false; + } // Else, one of the linesegments is zero, and we might still be able to calculate a single intersection point + } // Else we haven't bailed out, and we'll try to calculate the intersections + + double detinv = 1.0 / det; + tA = cross(d, Bd) * detinv; + tB = cross(d, Ad) * detinv; + return (tA >= 0.) && (tA <= 1.) && (tB >= 0.) && (tB <= 1.); +} + + +#if 0 +typedef union dbl_64{ + long long i64; + double d64; +}; + +static double EpsilonOf(double value) +{ + dbl_64 s; + s.d64 = value; + if(s.i64 == 0) + { + s.i64++; + return s.d64 - value; + } + else if(s.i64-- < 0) + return s.d64 - value; + else + return value - s.d64; +} +#endif + +#ifdef HAVE_GSL +struct rparams { + Curve const &A; + Curve const &B; +}; + +static int +intersect_polish_f (const gsl_vector * x, void *params, + gsl_vector * f) +{ + const double x0 = gsl_vector_get (x, 0); + const double x1 = gsl_vector_get (x, 1); + + Geom::Point dx = ((struct rparams *) params)->A(x0) - + ((struct rparams *) params)->B(x1); + + gsl_vector_set (f, 0, dx[0]); + gsl_vector_set (f, 1, dx[1]); + + return GSL_SUCCESS; +} +#endif + +static void +intersect_polish_root (Curve const &A, double &s, Curve const &B, double &t) +{ + std::vector as, bs; + as = A.pointAndDerivatives(s, 2); + bs = B.pointAndDerivatives(t, 2); + Point F = as[0] - bs[0]; + double best = dot(F, F); + + for(int i = 0; i < 4; i++) { + + /** + we want to solve + J*(x1 - x0) = f(x0) + + |dA(s)[0] -dB(t)[0]| (X1 - X0) = A(s) - B(t) + |dA(s)[1] -dB(t)[1]| + **/ + + // We're using the standard transformation matricies, which is numerically rather poor. Much better to solve the equation using elimination. + + Affine jack(as[1][0], as[1][1], + -bs[1][0], -bs[1][1], + 0, 0); + Point soln = (F)*jack.inverse(); + double ns = s - soln[0]; + double nt = t - soln[1]; + + if (ns<0) ns=0; + else if (ns>1) ns=1; + if (nt<0) nt=0; + else if (nt>1) nt=1; + + as = A.pointAndDerivatives(ns, 2); + bs = B.pointAndDerivatives(nt, 2); + F = as[0] - bs[0]; + double trial = dot(F, F); + if (trial > best*0.1) // we have standards, you know + // At this point we could do a line search + break; + best = trial; + s = ns; + t = nt; + } + +#ifdef HAVE_GSL + if(0) { // the GSL version is more accurate, but taints this with GPL + int status; + size_t iter = 0; + const size_t n = 2; + struct rparams p = {A, B}; + gsl_multiroot_function f = {&intersect_polish_f, n, &p}; + + double x_init[2] = {s, t}; + gsl_vector *x = gsl_vector_alloc (n); + + gsl_vector_set (x, 0, x_init[0]); + gsl_vector_set (x, 1, x_init[1]); + + const gsl_multiroot_fsolver_type *T = gsl_multiroot_fsolver_hybrids; + gsl_multiroot_fsolver *sol = gsl_multiroot_fsolver_alloc (T, 2); + gsl_multiroot_fsolver_set (sol, &f, x); + + do + { + iter++; + status = gsl_multiroot_fsolver_iterate (sol); + + if (status) /* check if solver is stuck */ + break; + + status = + gsl_multiroot_test_residual (sol->f, 1e-12); + } + while (status == GSL_CONTINUE && iter < 1000); + + s = gsl_vector_get (sol->x, 0); + t = gsl_vector_get (sol->x, 1); + + gsl_multiroot_fsolver_free (sol); + gsl_vector_free (x); + } +#endif +} + +/** + * This uses the local bounds functions of curves to generically intersect two. + * It passes in the curves, time intervals, and keeps track of depth, while + * returning the results through the Crossings parameter. + */ +void pair_intersect(Curve const & A, double Al, double Ah, + Curve const & B, double Bl, double Bh, + Crossings &ret, unsigned depth = 0) { + // std::cout << depth << "(" << Al << ", " << Ah << ")\n"; + OptRect Ar = A.boundsLocal(Interval(Al, Ah)); + if (!Ar) return; + + OptRect Br = B.boundsLocal(Interval(Bl, Bh)); + if (!Br) return; + + if(! Ar->intersects(*Br)) return; + + //Checks the general linearity of the function + if((depth > 12)) { // || (A.boundsLocal(Interval(Al, Ah), 1).maxExtent() < 0.1 + //&& B.boundsLocal(Interval(Bl, Bh), 1).maxExtent() < 0.1)) { + double tA, tB, c; + if(linear_intersect(A.pointAt(Al), A.pointAt(Ah), + B.pointAt(Bl), B.pointAt(Bh), + tA, tB, c)) { + tA = tA * (Ah - Al) + Al; + tB = tB * (Bh - Bl) + Bl; + intersect_polish_root(A, tA, + B, tB); + if(depth % 2) + ret.push_back(Crossing(tB, tA, c < 0)); + else + ret.push_back(Crossing(tA, tB, c > 0)); + return; + } + } + if(depth > 12) return; + double mid = (Bl + Bh)/2; + pair_intersect(B, Bl, mid, + A, Al, Ah, + ret, depth+1); + pair_intersect(B, mid, Bh, + A, Al, Ah, + ret, depth+1); +} + +Crossings pair_intersect(Curve const & A, Interval const &Ad, + Curve const & B, Interval const &Bd) { + Crossings ret; + pair_intersect(A, Ad.min(), Ad.max(), B, Bd.min(), Bd.max(), ret); + return ret; +} + +/** A simple wrapper around pair_intersect */ +Crossings SimpleCrosser::crossings(Curve const &a, Curve const &b) { + Crossings ret; + pair_intersect(a, 0, 1, b, 0, 1, ret); + return ret; +} + + +//same as below but curves not paths +void mono_intersect(Curve const &A, double Al, double Ah, + Curve const &B, double Bl, double Bh, + Crossings &ret, double tol = 0.1, unsigned depth = 0) { + if( Al >= Ah || Bl >= Bh) return; + //std::cout << " " << depth << "[" << Al << ", " << Ah << "]" << "[" << Bl << ", " << Bh << "]"; + + Point A0 = A.pointAt(Al), A1 = A.pointAt(Ah), + B0 = B.pointAt(Bl), B1 = B.pointAt(Bh); + //inline code that this implies? (without rect/interval construction) + Rect Ar = Rect(A0, A1), Br = Rect(B0, B1); + if(!Ar.intersects(Br) || A0 == A1 || B0 == B1) return; + + if(depth > 12 || (Ar.maxExtent() < tol && Ar.maxExtent() < tol)) { + double tA, tB, c; + if(linear_intersect(A.pointAt(Al), A.pointAt(Ah), + B.pointAt(Bl), B.pointAt(Bh), + tA, tB, c)) { + tA = tA * (Ah - Al) + Al; + tB = tB * (Bh - Bl) + Bl; + intersect_polish_root(A, tA, + B, tB); + if(depth % 2) + ret.push_back(Crossing(tB, tA, c < 0)); + else + ret.push_back(Crossing(tA, tB, c > 0)); + return; + } + } + if(depth > 12) return; + double mid = (Bl + Bh)/2; + mono_intersect(B, Bl, mid, + A, Al, Ah, + ret, tol, depth+1); + mono_intersect(B, mid, Bh, + A, Al, Ah, + ret, tol, depth+1); +} + +Crossings mono_intersect(Curve const & A, Interval const &Ad, + Curve const & B, Interval const &Bd) { + Crossings ret; + mono_intersect(A, Ad.min(), Ad.max(), B, Bd.min(), Bd.max(), ret); + return ret; +} + +/** + * Takes two paths and time ranges on them, with the invariant that the + * paths are monotonic on the range. Splits A when the linear intersection + * doesn't exist or is inaccurate. Uses the fact that it is monotonic to + * do very fast local bounds. + */ +void mono_pair(Path const &A, double Al, double Ah, + Path const &B, double Bl, double Bh, + Crossings &ret, double /*tol*/, unsigned depth = 0) { + if( Al >= Ah || Bl >= Bh) return; + std::cout << " " << depth << "[" << Al << ", " << Ah << "]" << "[" << Bl << ", " << Bh << "]"; + + Point A0 = A.pointAt(Al), A1 = A.pointAt(Ah), + B0 = B.pointAt(Bl), B1 = B.pointAt(Bh); + //inline code that this implies? (without rect/interval construction) + Rect Ar = Rect(A0, A1), Br = Rect(B0, B1); + if(!Ar.intersects(Br) || A0 == A1 || B0 == B1) return; + + if(depth > 12 || (Ar.maxExtent() < 0.1 && Ar.maxExtent() < 0.1)) { + double tA, tB, c; + if(linear_intersect(A0, A1, B0, B1, + tA, tB, c)) { + tA = tA * (Ah - Al) + Al; + tB = tB * (Bh - Bl) + Bl; + if(depth % 2) + ret.push_back(Crossing(tB, tA, c < 0)); + else + ret.push_back(Crossing(tA, tB, c > 0)); + return; + } + } + if(depth > 12) return; + double mid = (Bl + Bh)/2; + mono_pair(B, Bl, mid, + A, Al, Ah, + ret, depth+1); + mono_pair(B, mid, Bh, + A, Al, Ah, + ret, depth+1); +} + +/** This returns the times when the x or y derivative is 0 in the curve. */ +std::vector curve_mono_splits(Curve const &d) { + Curve* deriv = d.derivative(); + std::vector rs = deriv->roots(0, X); + append(rs, deriv->roots(0, Y)); + delete deriv; + std::sort(rs.begin(), rs.end()); + return rs; +} + +/** Convenience function to add a value to each entry in a vector of doubles. */ +std::vector offset_doubles(std::vector const &x, double offs) { + std::vector ret; + for(unsigned i = 0; i < x.size(); i++) { + ret.push_back(x[i] + offs); + } + return ret; +} + +/** + * Finds all the monotonic splits for a path. Only includes the split between + * curves if they switch derivative directions at that point. + */ +std::vector path_mono_splits(Path const &p) { + std::vector ret; + if(p.empty()) return ret; + + bool pdx=2, pdy=2; //Previous derivative direction + for(unsigned i = 0; i < p.size(); i++) { + std::vector spl = offset_doubles(curve_mono_splits(p[i]), i); + bool dx = p[i].initialPoint()[X] > (spl.empty()? p[i].finalPoint()[X] : + p.valueAt(spl.front(), X)); + bool dy = p[i].initialPoint()[Y] > (spl.empty()? p[i].finalPoint()[Y] : + p.valueAt(spl.front(), Y)); + //The direction changed, include the split time + if(dx != pdx || dy != pdy) { + ret.push_back(i); + pdx = dx; pdy = dy; + } + append(ret, spl); + } + return ret; +} + +/** + * Applies path_mono_splits to multiple paths, and returns the results such that + * time-set i corresponds to Path i. + */ +std::vector > paths_mono_splits(PathVector const &ps) { + std::vector > ret; + for(unsigned i = 0; i < ps.size(); i++) + ret.push_back(path_mono_splits(ps[i])); + return ret; +} + +/** + * Processes the bounds for a list of paths and a list of splits on them, yielding a list of rects for each. + * Each entry i corresponds to path i of the input. The number of rects in each entry is guaranteed to be the + * number of splits for that path, subtracted by one. + */ +std::vector > split_bounds(PathVector const &p, std::vector > splits) { + std::vector > ret; + for(unsigned i = 0; i < p.size(); i++) { + std::vector res; + for(unsigned j = 1; j < splits[i].size(); j++) + res.push_back(Rect(p[i].pointAt(splits[i][j-1]), p[i].pointAt(splits[i][j]))); + ret.push_back(res); + } + return ret; +} + +/** + * This is the main routine of "MonoCrosser", and implements a monotonic strategy on multiple curves. + * Finds crossings between two sets of paths, yielding a CrossingSet. [0, a.size()) of the return correspond + * to the sorted crossings of a with paths of b. The rest of the return, [a.size(), a.size() + b.size()], + * corresponds to the sorted crossings of b with paths of a. + * + * This function does two sweeps, one on the bounds of each path, and after that cull, one on the curves within. + * This leads to a certain amount of code complexity, however, most of that is factored into the above functions + */ +CrossingSet MonoCrosser::crossings(PathVector const &a, PathVector const &b) { + if(b.empty()) return CrossingSet(a.size(), Crossings()); + CrossingSet results(a.size() + b.size(), Crossings()); + if(a.empty()) return results; + + std::vector > splits_a = paths_mono_splits(a), splits_b = paths_mono_splits(b); + std::vector > bounds_a = split_bounds(a, splits_a), bounds_b = split_bounds(b, splits_b); + + std::vector bounds_a_union, bounds_b_union; + for(unsigned i = 0; i < bounds_a.size(); i++) bounds_a_union.push_back(union_list(bounds_a[i])); + for(unsigned i = 0; i < bounds_b.size(); i++) bounds_b_union.push_back(union_list(bounds_b[i])); + + std::vector > cull = sweep_bounds(bounds_a_union, bounds_b_union); + Crossings n; + for(unsigned i = 0; i < cull.size(); i++) { + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + unsigned jc = j + a.size(); + Crossings res; + + //Sweep of the monotonic portions + std::vector > cull2 = sweep_bounds(bounds_a[i], bounds_b[j]); + for(unsigned k = 0; k < cull2.size(); k++) { + for(unsigned lx = 0; lx < cull2[k].size(); lx++) { + unsigned l = cull2[k][lx]; + mono_pair(a[i], splits_a[i][k-1], splits_a[i][k], + b[j], splits_b[j][l-1], splits_b[j][l], + res, .1); + } + } + + for(unsigned k = 0; k < res.size(); k++) { res[k].a = i; res[k].b = jc; } + + merge_crossings(results[i], res, i); + merge_crossings(results[i], res, jc); + } + } + + return results; +} + +/* This function is similar codewise to the MonoCrosser, the main difference is that it deals with + * only one set of paths and includes self intersection +CrossingSet crossings_among(PathVector const &p) { + CrossingSet results(p.size(), Crossings()); + if(p.empty()) return results; + + std::vector > splits = paths_mono_splits(p); + std::vector > prs = split_bounds(p, splits); + std::vector rs; + for(unsigned i = 0; i < prs.size(); i++) rs.push_back(union_list(prs[i])); + + std::vector > cull = sweep_bounds(rs); + + //we actually want to do the self-intersections, so add em in: + for(unsigned i = 0; i < cull.size(); i++) cull[i].push_back(i); + + for(unsigned i = 0; i < cull.size(); i++) { + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + Crossings res; + + //Sweep of the monotonic portions + std::vector > cull2 = sweep_bounds(prs[i], prs[j]); + for(unsigned k = 0; k < cull2.size(); k++) { + for(unsigned lx = 0; lx < cull2[k].size(); lx++) { + unsigned l = cull2[k][lx]; + mono_pair(p[i], splits[i][k-1], splits[i][k], + p[j], splits[j][l-1], splits[j][l], + res, .1); + } + } + + for(unsigned k = 0; k < res.size(); k++) { res[k].a = i; res[k].b = j; } + + merge_crossings(results[i], res, i); + merge_crossings(results[j], res, j); + } + } + + return results; +} +*/ + + +Crossings curve_self_crossings(Curve const &a) { + Crossings res; + std::vector spl; + spl.push_back(0); + append(spl, curve_mono_splits(a)); + spl.push_back(1); + for(unsigned i = 1; i < spl.size(); i++) + for(unsigned j = i+1; j < spl.size(); j++) + pair_intersect(a, spl[i-1], spl[i], a, spl[j-1], spl[j], res); + return res; +} + +/* +void mono_curve_intersect(Curve const & A, double Al, double Ah, + Curve const & B, double Bl, double Bh, + Crossings &ret, unsigned depth=0) { + // std::cout << depth << "(" << Al << ", " << Ah << ")\n"; + Point A0 = A.pointAt(Al), A1 = A.pointAt(Ah), + B0 = B.pointAt(Bl), B1 = B.pointAt(Bh); + //inline code that this implies? (without rect/interval construction) + if(!Rect(A0, A1).intersects(Rect(B0, B1)) || A0 == A1 || B0 == B1) return; + + //Checks the general linearity of the function + if((depth > 12) || (A.boundsLocal(Interval(Al, Ah), 1).maxExtent() < 0.1 + && B.boundsLocal(Interval(Bl, Bh), 1).maxExtent() < 0.1)) { + double tA, tB, c; + if(linear_intersect(A0, A1, B0, B1, tA, tB, c)) { + tA = tA * (Ah - Al) + Al; + tB = tB * (Bh - Bl) + Bl; + if(depth % 2) + ret.push_back(Crossing(tB, tA, c < 0)); + else + ret.push_back(Crossing(tA, tB, c > 0)); + return; + } + } + if(depth > 12) return; + double mid = (Bl + Bh)/2; + mono_curve_intersect(B, Bl, mid, + A, Al, Ah, + ret, depth+1); + mono_curve_intersect(B, mid, Bh, + A, Al, Ah, + ret, depth+1); +} + +std::vector > curves_mono_splits(Path const &p) { + std::vector > ret; + for(unsigned i = 0; i <= p.size(); i++) { + std::vector spl; + spl.push_back(0); + append(spl, curve_mono_splits(p[i])); + spl.push_back(1); + ret.push_back(spl); + } +} + +std::vector > curves_split_bounds(Path const &p, std::vector > splits) { + std::vector > ret; + for(unsigned i = 0; i < splits.size(); i++) { + std::vector res; + for(unsigned j = 1; j < splits[i].size(); j++) + res.push_back(Rect(p.pointAt(splits[i][j-1]+i), p.pointAt(splits[i][j]+i))); + ret.push_back(res); + } + return ret; +} + +Crossings path_self_crossings(Path const &p) { + Crossings ret; + std::vector > cull = sweep_bounds(bounds(p)); + std::vector > spl = curves_mono_splits(p); + std::vector > bnds = curves_split_bounds(p, spl); + for(unsigned i = 0; i < cull.size(); i++) { + Crossings res; + for(unsigned k = 1; k < spl[i].size(); k++) + for(unsigned l = k+1; l < spl[i].size(); l++) + mono_curve_intersect(p[i], spl[i][k-1], spl[i][k], p[i], spl[i][l-1], spl[i][l], res); + offset_crossings(res, i, i); + append(ret, res); + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + res.clear(); + + std::vector > cull2 = sweep_bounds(bnds[i], bnds[j]); + for(unsigned k = 0; k < cull2.size(); k++) { + for(unsigned lx = 0; lx < cull2[k].size(); lx++) { + unsigned l = cull2[k][lx]; + mono_curve_intersect(p[i], spl[i][k-1], spl[i][k], p[j], spl[j][l-1], spl[j][l], res); + } + } + + //if(fabs(int(i)-j) == 1 || fabs(int(i)-j) == p.size()-1) { + Crossings res2; + for(unsigned k = 0; k < res.size(); k++) { + if(res[k].ta != 0 && res[k].ta != 1 && res[k].tb != 0 && res[k].tb != 1) { + res.push_back(res[k]); + } + } + res = res2; + //} + offset_crossings(res, i, j); + append(ret, res); + } + } + return ret; +} +*/ + +Crossings self_crossings(Path const &p) { + Crossings ret; + std::vector > cull = sweep_bounds(bounds(p)); + for(unsigned i = 0; i < cull.size(); i++) { + Crossings res = curve_self_crossings(p[i]); + offset_crossings(res, i, i); + append(ret, res); + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + res.clear(); + pair_intersect(p[i], 0, 1, p[j], 0, 1, res); + + //if(fabs(int(i)-j) == 1 || fabs(int(i)-j) == p.size()-1) { + Crossings res2; + for(unsigned k = 0; k < res.size(); k++) { + if(res[k].ta != 0 && res[k].ta != 1 && res[k].tb != 0 && res[k].tb != 1) { + res2.push_back(res[k]); + } + } + res = res2; + //} + offset_crossings(res, i, j); + append(ret, res); + } + } + return ret; +} + +void flip_crossings(Crossings &crs) { + for(unsigned i = 0; i < crs.size(); i++) + crs[i] = Crossing(crs[i].tb, crs[i].ta, crs[i].b, crs[i].a, !crs[i].dir); +} + +CrossingSet crossings_among(PathVector const &p) { + CrossingSet results(p.size(), Crossings()); + if(p.empty()) return results; + + SimpleCrosser cc; + + std::vector > cull = sweep_bounds(bounds(p)); + for(unsigned i = 0; i < cull.size(); i++) { + Crossings res = self_crossings(p[i]); + for(unsigned k = 0; k < res.size(); k++) { res[k].a = res[k].b = i; } + merge_crossings(results[i], res, i); + flip_crossings(res); + merge_crossings(results[i], res, i); + for(unsigned jx = 0; jx < cull[i].size(); jx++) { + unsigned j = cull[i][jx]; + + Crossings res = cc.crossings(p[i], p[j]); + for(unsigned k = 0; k < res.size(); k++) { res[k].a = i; res[k].b = j; } + merge_crossings(results[i], res, i); + merge_crossings(results[j], res, j); + } + } + return results; +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path-intersection.h b/src/2geom/path-intersection.h new file mode 100644 index 0000000..f06eeaf --- /dev/null +++ b/src/2geom/path-intersection.h @@ -0,0 +1,118 @@ +/** + * \file + * \brief Path intersection + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_PATH_INTERSECTION_H +#define LIB2GEOM_SEEN_PATH_INTERSECTION_H + +#include <2geom/crossing.h> +#include <2geom/path.h> +#include <2geom/sweep-bounds.h> + +namespace Geom { + +int winding(Path const &path, Point const &p); +bool path_direction(Path const &p); + +inline bool contains(Path const & p, Point const &i, bool evenodd = true) { + return (evenodd ? winding(p, i) % 2 : winding(p, i)) != 0; +} + +template +Crossings curve_sweep(Path const &a, Path const &b) { + T t; + Crossings ret; + std::vector bounds_a = bounds(a), bounds_b = bounds(b); + std::vector > ixs = sweep_bounds(bounds_a, bounds_b); + for(unsigned i = 0; i < a.size(); i++) { + for(std::vector::iterator jp = ixs[i].begin(); jp != ixs[i].end(); ++jp) { + Crossings cc = t.crossings(a[i], b[*jp]); + offset_crossings(cc, i, *jp); + ret.insert(ret.end(), cc.begin(), cc.end()); + } + } + return ret; +} + +Crossings pair_intersect(Curve const & A, Interval const &Ad, + Curve const & B, Interval const &Bd); +Crossings mono_intersect(Curve const & A, Interval const &Ad, + Curve const & B, Interval const &Bd); + +struct SimpleCrosser : public Crosser { + Crossings crossings(Curve const &a, Curve const &b); + Crossings crossings(Path const &a, Path const &b) { return curve_sweep(a, b); } + CrossingSet crossings(PathVector const &a, PathVector const &b) { return Crosser::crossings(a, b); } +}; + +struct MonoCrosser : public Crosser { + Crossings crossings(Path const &a, Path const &b) { return crossings(PathVector(a), PathVector(b))[0]; } + CrossingSet crossings(PathVector const &a, PathVector const &b); +}; + +typedef SimpleCrosser DefaultCrosser; + +std::vector path_mono_splits(Path const &p); + +CrossingSet crossings_among(PathVector const & p); +Crossings self_crossings(Path const & a); + +inline Crossings crossings(Curve const & a, Curve const & b) { + DefaultCrosser c = DefaultCrosser(); + return c.crossings(a, b); +} + +inline Crossings crossings(Path const & a, Path const & b) { + DefaultCrosser c = DefaultCrosser(); + return c.crossings(a, b); +} + +inline CrossingSet crossings(PathVector const & a, PathVector const & b) { + DefaultCrosser c = DefaultCrosser(); + return c.crossings(a, b); +} + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path-sink.cpp b/src/2geom/path-sink.cpp new file mode 100644 index 0000000..77301b7 --- /dev/null +++ b/src/2geom/path-sink.cpp @@ -0,0 +1,104 @@ +/* + * callback interface for SVG path data + * + * Copyright 2007 MenTaLguY + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/path-sink.h> +#include <2geom/exception.h> +#include <2geom/circle.h> +#include <2geom/ellipse.h> + +namespace Geom { + +void PathSink::feed(Curve const &c, bool moveto_initial) +{ + c.feed(*this, moveto_initial); +} + +void PathSink::feed(Path const &path) { + flush(); + moveTo(path.front().initialPoint()); + + // never output the closing segment to the sink + Path::const_iterator iter = path.begin(), last = path.end_open(); + for (; iter != last; ++iter) { + iter->feed(*this, false); + } + if (path.closed()) { + closePath(); + } + flush(); +} + +void PathSink::feed(PathVector const &pv) { + for (PathVector::const_iterator i = pv.begin(); i != pv.end(); ++i) { + feed(*i); + } +} + +void PathSink::feed(Rect const &r) { + moveTo(r.corner(0)); + lineTo(r.corner(1)); + lineTo(r.corner(2)); + lineTo(r.corner(3)); + closePath(); +} + +void PathSink::feed(Circle const &e) { + Coord r = e.radius(); + Point c = e.center(); + Point a = c + Point(0, +r); + Point b = c + Point(0, -r); + + moveTo(a); + arcTo(r, r, 0, false, false, b); + arcTo(r, r, 0, false, false, a); + closePath(); +} + +void PathSink::feed(Ellipse const &e) { + Point s = e.pointAt(0); + moveTo(s); + arcTo(e.ray(X), e.ray(Y), e.rotationAngle(), false, false, e.pointAt(M_PI)); + arcTo(e.ray(X), e.ray(Y), e.rotationAngle(), false, false, s); + closePath(); +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path-sink.h b/src/2geom/path-sink.h new file mode 100644 index 0000000..3bdb007 --- /dev/null +++ b/src/2geom/path-sink.h @@ -0,0 +1,245 @@ +/** + * \file + * \brief callback interface for SVG path data + *//* + * Copyright 2007 MenTaLguY + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_PATH_SINK_H +#define LIB2GEOM_SEEN_PATH_SINK_H + +#include <2geom/forward.h> +#include <2geom/pathvector.h> +#include <2geom/curves.h> +#include + +namespace Geom { + + +/** @brief Callback interface for processing path data. + * + * PathSink provides an interface that allows one to easily write + * code which processes path data, for instance when converting + * between path formats used by different graphics libraries. + * It is also useful for writing algorithms which must do something + * for each curve in the path. + * + * To store a path in a new format, implement the virtual methods + * for segments in a derived class and call feed(). + * + * @ingroup Paths + */ +class PathSink { +public: + /** @brief Move to a different point without creating a segment. + * Usually starts a new subpath. */ + virtual void moveTo(Point const &p) = 0; + /// Output a line segment. + virtual void lineTo(Point const &p) = 0; + /// Output a quadratic Bezier segment. + virtual void curveTo(Point const &c0, Point const &c1, Point const &p) = 0; + /// Output a cubic Bezier segment. + virtual void quadTo(Point const &c, Point const &p) = 0; + /** @brief Output an elliptical arc segment. + * See the EllipticalArc class for the documentation of parameters. */ + virtual void arcTo(Coord rx, Coord ry, Coord angle, + bool large_arc, bool sweep, Point const &p) = 0; + + /// Close the current path with a line segment. + virtual void closePath() = 0; + /** @brief Flush any internal state of the generator. + * This call should implicitly finish the current subpath. + * Calling this method should be idempotent, because the default + * implementations of path() and pathvector() will call it + * multiple times in a row. */ + virtual void flush() = 0; + // Get the current point, e.g. where the initial point of the next segment will be. + //virtual Point currentPoint() const = 0; + + /** @brief Undo the last segment. + * This method is optional. + * @return true true if a segment was erased, false otherwise. */ + virtual bool backspace() { return false; } + + // these have a default implementation + virtual void feed(Curve const &c, bool moveto_initial = true); + /** @brief Output a subpath. + * Calls the appropriate segment methods according to the contents + * of the passed subpath. You can override this function. + * NOTE: if you override only some of the feed() functions, + * always write this in the derived class: + * @code + using PathSink::feed; + @endcode + * Otherwise the remaining methods will be hidden. */ + virtual void feed(Path const &p); + /** @brief Output a path. + * Calls feed() on each path in the vector. You can override this function. */ + virtual void feed(PathVector const &v); + /// Output an axis-aligned rectangle, using moveTo, lineTo and closePath. + virtual void feed(Rect const &); + /// Output a circle as two elliptical arcs. + virtual void feed(Circle const &e); + /// Output an ellipse as two elliptical arcs. + virtual void feed(Ellipse const &e); + + virtual ~PathSink() {} +}; + +/** @brief Store paths to an output iterator + * @ingroup Paths */ +template +class PathIteratorSink : public PathSink { +public: + explicit PathIteratorSink(OutputIterator out) + : _in_path(false), _out(out) {} + + void moveTo(Point const &p) { + flush(); + _path.start(p); + _start_p = p; + _in_path = true; + } +//TODO: what if _in_path = false? + + void lineTo(Point const &p) { + // check for implicit moveto, like in: "M 1,1 L 2,2 z l 2,2 z" + if (!_in_path) { + moveTo(_start_p); + } + _path.template appendNew(p); + } + + void quadTo(Point const &c, Point const &p) { + // check for implicit moveto, like in: "M 1,1 L 2,2 z l 2,2 z" + if (!_in_path) { + moveTo(_start_p); + } + _path.template appendNew(c, p); + } + + void curveTo(Point const &c0, Point const &c1, Point const &p) { + // check for implicit moveto, like in: "M 1,1 L 2,2 z l 2,2 z" + if (!_in_path) { + moveTo(_start_p); + } + _path.template appendNew(c0, c1, p); + } + + void arcTo(Coord rx, Coord ry, Coord angle, + bool large_arc, bool sweep, Point const &p) + { + // check for implicit moveto, like in: "M 1,1 L 2,2 z l 2,2 z" + if (!_in_path) { + moveTo(_start_p); + } + _path.template appendNew(rx, ry, angle, + large_arc, sweep, p); + } + + bool backspace() + { + if (_in_path && _path.size() > 0) { + _path.erase_last(); + return true; + } + return false; + } + + void append(Path const &other) + { + if (!_in_path) { + moveTo(other.initialPoint()); + } + _path.append(other); + } + + void closePath() { + if (_in_path) { + _path.close(); + flush(); + } + } + + void flush() { + if (_in_path) { + _in_path = false; + *_out++ = _path; + _path.clear(); + } + } + + void setStitching(bool s) { + _path.setStitching(s); + } + + using PathSink::feed; + void feed(Path const &other) + { + flush(); + *_out++ = other; + } + +protected: + bool _in_path; + OutputIterator _out; + Path _path; + Point _start_p; +}; + +typedef std::back_insert_iterator SubpathInserter; + +/** @brief Store paths to a PathVector + * @ingroup Paths */ +class PathBuilder : public PathIteratorSink { +private: + PathVector _pathset; +public: + /// Create a builder that outputs to an internal pathvector. + PathBuilder() : PathIteratorSink(SubpathInserter(_pathset)) {} + /// Create a builder that outputs to pathvector given by reference. + PathBuilder(PathVector &pv) : PathIteratorSink(SubpathInserter(pv)) {} + + /// Retrieve the path + PathVector const &peek() const {return _pathset;} + /// Clear the stored path vector + void clear() { _pathset.clear(); } +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path.cpp b/src/2geom/path.cpp new file mode 100644 index 0000000..3288eb4 --- /dev/null +++ b/src/2geom/path.cpp @@ -0,0 +1,1128 @@ +/** @file + * @brief Path - a sequence of contiguous curves (implementation file) + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2014 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/transforms.h> +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/convex-hull.h> +#include <2geom/svg-path-writer.h> +#include <2geom/sweeper.h> +#include +#include + +using std::swap; +using namespace Geom::PathInternal; + +namespace Geom { + +// this represents an empty interval +PathInterval::PathInterval() + : _from(0, 0.0) + , _to(0, 0.0) + , _path_size(1) + , _cross_start(false) + , _reverse(false) +{} + +PathInterval::PathInterval(PathTime const &from, PathTime const &to, bool cross_start, size_type path_size) + : _from(from) + , _to(to) + , _path_size(path_size) + , _cross_start(cross_start) + , _reverse(cross_start ? to >= from : to < from) +{ + if (_reverse) { + _to.normalizeForward(_path_size); + if (_from != _to) { + _from.normalizeBackward(_path_size); + } + } else { + _from.normalizeForward(_path_size); + if (_from != _to) { + _to.normalizeBackward(_path_size); + } + } + + if (_from == _to) { + _reverse = false; + _cross_start = false; + } +} + +bool PathInterval::contains(PathTime const &pos) const { + if (_cross_start) { + if (_reverse) { + return pos >= _to || _from >= pos; + } else { + return pos >= _from || _to >= pos; + } + } else { + if (_reverse) { + return _to <= pos && pos <= _from; + } else { + return _from <= pos && pos <= _to; + } + } +} + +PathInterval::size_type PathInterval::curveCount() const +{ + if (isDegenerate()) return 0; + if (_cross_start) { + if (_reverse) { + return _path_size - _to.curve_index + _from.curve_index + 1; + } else { + return _path_size - _from.curve_index + _to.curve_index + 1; + } + } else { + if (_reverse) { + return _from.curve_index - _to.curve_index + 1; + } else { + return _to.curve_index - _from.curve_index + 1; + } + } +} + +PathTime PathInterval::inside(Coord min_dist) const +{ + // If there is some node further than min_dist (in time coord) from the ends, + // return that node. Otherwise, return the middle. + PathTime result(0, 0.0); + + if (!_cross_start && _from.curve_index == _to.curve_index) { + PathTime result(_from.curve_index, lerp(0.5, _from.t, _to.t)); + return result; + } + // If _cross_start, then we can be sure that at least one node is in the domain. + // If dcurve == 0, it actually means that all curves are included in the domain + + if (_reverse) { + size_type dcurve = (_path_size + _from.curve_index - _to.curve_index) % _path_size; + bool from_close = _from.t < min_dist; + bool to_close = _to.t > 1 - min_dist; + + if (dcurve == 0) { + dcurve = _path_size; + } + + if (dcurve == 1) { + if (from_close || to_close) { + result.curve_index = _from.curve_index; + Coord tmid = _from.t - ((1 - _to.t) + _from.t) * 0.5; + if (tmid < 0) { + result.curve_index = (_path_size + result.curve_index - 1) % _path_size; + tmid += 1; + } + result.t = tmid; + return result; + } + + result.curve_index = _from.curve_index; + return result; + } + + result.curve_index = (_to.curve_index + 1) % _path_size; + if (to_close) { + if (dcurve == 2) { + result.t = 0.5; + } else { + result.curve_index = (result.curve_index + 1) % _path_size; + } + } + return result; + } else { + size_type dcurve = (_path_size + _to.curve_index - _from.curve_index) % _path_size; + bool from_close = _from.t > 1 - min_dist; + bool to_close = _to.t < min_dist; + + if (dcurve == 0) { + dcurve = _path_size; + } + + if (dcurve == 1) { + if (from_close || to_close) { + result.curve_index = _from.curve_index; + Coord tmid = ((1 - _from.t) + _to.t) * 0.5 + _from.t; + if (tmid >= 1) { + result.curve_index = (result.curve_index + 1) % _path_size; + tmid -= 1; + } + result.t = tmid; + return result; + } + + result.curve_index = _to.curve_index; + return result; + } + + result.curve_index = (_from.curve_index + 1) % _path_size; + if (from_close) { + if (dcurve == 2) { + result.t = 0.5; + } else { + result.curve_index = (result.curve_index + 1) % _path_size; + } + } + return result; + } + + result.curve_index = _reverse ? _from.curve_index : _to.curve_index; + return result; +} + +PathInterval PathInterval::from_direction(PathTime const &from, PathTime const &to, bool reversed, size_type path_size) +{ + PathInterval result; + result._from = from; + result._to = to; + result._path_size = path_size; + + if (reversed) { + result._to.normalizeForward(path_size); + if (result._from != result._to) { + result._from.normalizeBackward(path_size); + } + } else { + result._from.normalizeForward(path_size); + if (result._from != result._to) { + result._to.normalizeBackward(path_size); + } + } + + if (result._from == result._to) { + result._reverse = false; + result._cross_start = false; + } else { + result._reverse = reversed; + if (reversed) { + result._cross_start = from < to; + } else { + result._cross_start = to < from; + } + } + return result; +} + + +Path::Path(Rect const &r) + : _data(new PathData()) + , _closing_seg(new ClosingSegment(r.corner(3), r.corner(0))) + , _closed(true) + , _exception_on_stitch(true) +{ + for (unsigned i = 0; i < 3; ++i) { + _data->curves.push_back(new LineSegment(r.corner(i), r.corner(i+1))); + } + _data->curves.push_back(_closing_seg); +} + +Path::Path(ConvexHull const &ch) + : _data(new PathData()) + , _closing_seg(new ClosingSegment(Point(), Point())) + , _closed(true) + , _exception_on_stitch(true) +{ + if (ch.empty()) { + _data->curves.push_back(_closing_seg); + return; + } + + _closing_seg->setInitial(ch.back()); + _closing_seg->setFinal(ch.front()); + + Point last = ch.front(); + + for (std::size_t i = 1; i < ch.size(); ++i) { + _data->curves.push_back(new LineSegment(last, ch[i])); + last = ch[i]; + } + + _data->curves.push_back(_closing_seg); + _closed = true; +} + +Path::Path(Circle const &c) + : _data(new PathData()) + , _closing_seg(NULL) + , _closed(true) + , _exception_on_stitch(true) +{ + Point p1 = c.pointAt(0); + Point p2 = c.pointAt(M_PI); + _data->curves.push_back(new EllipticalArc(p1, c.radius(), c.radius(), 0, false, true, p2)); + _data->curves.push_back(new EllipticalArc(p2, c.radius(), c.radius(), 0, false, true, p1)); + _closing_seg = new ClosingSegment(p1, p1); + _data->curves.push_back(_closing_seg); +} + +Path::Path(Ellipse const &e) + : _data(new PathData()) + , _closing_seg(NULL) + , _closed(true) + , _exception_on_stitch(true) +{ + Point p1 = e.pointAt(0); + Point p2 = e.pointAt(M_PI); + _data->curves.push_back(new EllipticalArc(p1, e.rays(), e.rotationAngle(), false, true, p2)); + _data->curves.push_back(new EllipticalArc(p2, e.rays(), e.rotationAngle(), false, true, p1)); + _closing_seg = new ClosingSegment(p1, p1); + _data->curves.push_back(_closing_seg); +} + +void Path::close(bool c) +{ + if (c == _closed) return; + if (c && _data->curves.size() >= 2) { + // when closing, if last segment is linear and ends at initial point, + // replace it with the closing segment + Sequence::iterator last = _data->curves.end() - 2; + if (last->isLineSegment() && last->finalPoint() == initialPoint()) { + _closing_seg->setInitial(last->initialPoint()); + _data->curves.erase(last); + } + } + _closed = c; +} + +void Path::clear() +{ + _unshare(); + _data->curves.pop_back().release(); + _data->curves.clear(); + _closing_seg->setInitial(Point(0, 0)); + _closing_seg->setFinal(Point(0, 0)); + _data->curves.push_back(_closing_seg); + _closed = false; +} + +OptRect Path::boundsFast() const +{ + OptRect bounds; + if (empty()) { + return bounds; + } + // if the path is not empty, we look for cached bounds + if (_data->fast_bounds) { + return _data->fast_bounds; + } + + bounds = front().boundsFast(); + const_iterator iter = begin(); + // the closing path segment can be ignored, because it will always + // lie within the bbox of the rest of the path + if (iter != end()) { + for (++iter; iter != end(); ++iter) { + bounds.unionWith(iter->boundsFast()); + } + } + _data->fast_bounds = bounds; + return bounds; +} + +OptRect Path::boundsExact() const +{ + OptRect bounds; + if (empty()) + return bounds; + bounds = front().boundsExact(); + const_iterator iter = begin(); + // the closing path segment can be ignored, because it will always lie within the bbox of the rest of the path + if (iter != end()) { + for (++iter; iter != end(); ++iter) { + bounds.unionWith(iter->boundsExact()); + } + } + return bounds; +} + +Piecewise > Path::toPwSb() const +{ + Piecewise > ret; + ret.push_cut(0); + unsigned i = 1; + bool degenerate = true; + // pw> is always open. so if path is closed, add closing segment as well to pwd2. + for (const_iterator it = begin(); it != end_default(); ++it) { + if (!it->isDegenerate()) { + ret.push(it->toSBasis(), i++); + degenerate = false; + } + } + if (degenerate) { + // if path only contains degenerate curves, no second cut is added + // so we need to create at least one segment manually + ret = Piecewise >(initialPoint()); + } + return ret; +} + +template +iter inc(iter const &x, unsigned n) { + iter ret = x; + for (unsigned i = 0; i < n; i++) + ret++; + return ret; +} + +bool Path::operator==(Path const &other) const +{ + if (this == &other) + return true; + if (_closed != other._closed) + return false; + return _data->curves == other._data->curves; +} + +void Path::start(Point const &p) { + if (_data->curves.size() > 1) { + clear(); + } + _closing_seg->setInitial(p); + _closing_seg->setFinal(p); +} + +Interval Path::timeRange() const +{ + Interval ret(0, size_default()); + return ret; +} + +Curve const &Path::curveAt(Coord t, Coord *rest) const +{ + PathTime pos = _factorTime(t); + if (rest) { + *rest = pos.t; + } + return at(pos.curve_index); +} + +Point Path::pointAt(Coord t) const +{ + return pointAt(_factorTime(t)); +} + +Coord Path::valueAt(Coord t, Dim2 d) const +{ + return valueAt(_factorTime(t), d); +} + +Curve const &Path::curveAt(PathTime const &pos) const +{ + return at(pos.curve_index); +} +Point Path::pointAt(PathTime const &pos) const +{ + return at(pos.curve_index).pointAt(pos.t); +} +Coord Path::valueAt(PathTime const &pos, Dim2 d) const +{ + return at(pos.curve_index).valueAt(pos.t, d); +} + +std::vector Path::roots(Coord v, Dim2 d) const +{ + std::vector res; + for (unsigned i = 0; i < size(); i++) { + std::vector temp = (*this)[i].roots(v, d); + for (unsigned j = 0; j < temp.size(); j++) + res.push_back(PathTime(i, temp[j])); + } + return res; +} + + +// The class below implements sweepline optimization for curve intersection in paths. +// Instead of O(N^2), this takes O(N + X), where X is the number of overlaps +// between the bounding boxes of curves. + +struct CurveIntersectionSweepSet +{ +public: + struct CurveRecord { + boost::intrusive::list_member_hook<> _hook; + Curve const *curve; + Rect bounds; + std::size_t index; + unsigned which; + + CurveRecord(Curve const *pc, std::size_t idx, unsigned w) + : curve(pc) + , bounds(curve->boundsFast()) + , index(idx) + , which(w) + {} + }; + + typedef std::vector::const_iterator ItemIterator; + + CurveIntersectionSweepSet(std::vector &result, + Path const &a, Path const &b, Coord precision) + : _result(result) + , _precision(precision) + , _sweep_dir(X) + { + std::size_t asz = a.size(), bsz = b.size(); + _records.reserve(asz + bsz); + + for (std::size_t i = 0; i < asz; ++i) { + _records.push_back(CurveRecord(&a[i], i, 0)); + } + for (std::size_t i = 0; i < bsz; ++i) { + _records.push_back(CurveRecord(&b[i], i, 1)); + } + + OptRect abb = a.boundsFast() | b.boundsFast(); + if (abb && abb->height() > abb->width()) { + _sweep_dir = Y; + } + } + + std::vector const &items() { return _records; } + Interval itemBounds(ItemIterator ii) { + return ii->bounds[_sweep_dir]; + } + + void addActiveItem(ItemIterator ii) { + unsigned w = ii->which; + unsigned ow = (w+1) % 2; + + _active[w].push_back(const_cast(*ii)); + + for (ActiveCurveList::iterator i = _active[ow].begin(); i != _active[ow].end(); ++i) { + if (!ii->bounds.intersects(i->bounds)) continue; + std::vector cx = ii->curve->intersect(*i->curve, _precision); + for (std::size_t k = 0; k < cx.size(); ++k) { + PathTime tw(ii->index, cx[k].first), tow(i->index, cx[k].second); + _result.push_back(PathIntersection( + w == 0 ? tw : tow, + w == 0 ? tow : tw, + cx[k].point())); + } + } + } + void removeActiveItem(ItemIterator ii) { + ActiveCurveList &acl = _active[ii->which]; + acl.erase(acl.iterator_to(*ii)); + } + +private: + typedef boost::intrusive::list + < CurveRecord + , boost::intrusive::member_hook + < CurveRecord + , boost::intrusive::list_member_hook<> + , &CurveRecord::_hook + > + > ActiveCurveList; + + std::vector _records; + std::vector &_result; + ActiveCurveList _active[2]; + Coord _precision; + Dim2 _sweep_dir; +}; + +std::vector Path::intersect(Path const &other, Coord precision) const +{ + std::vector result; + + CurveIntersectionSweepSet cisset(result, *this, other, precision); + Sweeper sweeper(cisset); + sweeper.process(); + + // preprocessing to remove duplicate intersections at endpoints + std::size_t asz = size(), bsz = other.size(); + for (std::size_t i = 0; i < result.size(); ++i) { + result[i].first.normalizeForward(asz); + result[i].second.normalizeForward(bsz); + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + + return result; +} + +int Path::winding(Point const &p) const { + int wind = 0; + + /* To handle all the edge cases, we consider the maximum Y edge of the bounding box + * as not included in box. This way paths that contain linear horizontal + * segments will be treated correctly. */ + for (const_iterator i = begin(); i != end_closed(); ++i) { + Rect bounds = i->boundsFast(); + + if (bounds.height() == 0) continue; + if (p[X] > bounds.right() || !bounds[Y].lowerContains(p[Y])) { + // Ray doesn't intersect bbox, so we ignore this segment + continue; + } + + if (p[X] < bounds.left()) { + /* Ray intersects the curve's bbox, but the point is outside it. + * The winding contribution is exactly the same as that + * of a linear segment with the same initial and final points. */ + Point ip = i->initialPoint(); + Point fp = i->finalPoint(); + Rect eqbox(ip, fp); + + if (eqbox[Y].lowerContains(p[Y])) { + /* The ray intersects the equivalent linear segment. + * Determine winding contribution based on its derivative. */ + if (ip[Y] < fp[Y]) { + wind += 1; + } else if (ip[Y] > fp[Y]) { + wind -= 1; + } else { + // should never happen, because bounds.height() was not zero + assert(false); + } + } + } else { + // point is inside bbox + wind += i->winding(p); + } + } + return wind; +} + +std::vector Path::allNearestTimes(Point const &_point, double from, double to) const +{ + // TODO from and to are not used anywhere. + // rewrite this to simplify. + using std::swap; + + if (from > to) + swap(from, to); + const Path &_path = *this; + unsigned int sz = _path.size(); + if (_path.closed()) + ++sz; + if (from < 0 || to > sz) { + THROW_RANGEERROR("[from,to] interval out of bounds"); + } + double sif, st = modf(from, &sif); + double eif, et = modf(to, &eif); + unsigned int si = static_cast(sif); + unsigned int ei = static_cast(eif); + if (si == sz) { + --si; + st = 1; + } + if (ei == sz) { + --ei; + et = 1; + } + if (si == ei) { + std::vector all_nearest = _path[si].allNearestTimes(_point, st, et); + for (unsigned int i = 0; i < all_nearest.size(); ++i) { + all_nearest[i] = si + all_nearest[i]; + } + return all_nearest; + } + std::vector all_t; + std::vector > all_np; + all_np.push_back(_path[si].allNearestTimes(_point, st)); + std::vector ni; + ni.push_back(si); + double dsq; + double mindistsq = distanceSq(_point, _path[si].pointAt(all_np.front().front())); + Rect bb(Geom::Point(0, 0), Geom::Point(0, 0)); + for (unsigned int i = si + 1; i < ei; ++i) { + bb = (_path[i].boundsFast()); + dsq = distanceSq(_point, bb); + if (mindistsq < dsq) + continue; + all_t = _path[i].allNearestTimes(_point); + dsq = distanceSq(_point, _path[i].pointAt(all_t.front())); + if (mindistsq > dsq) { + all_np.clear(); + all_np.push_back(all_t); + ni.clear(); + ni.push_back(i); + mindistsq = dsq; + } else if (mindistsq == dsq) { + all_np.push_back(all_t); + ni.push_back(i); + } + } + bb = (_path[ei].boundsFast()); + dsq = distanceSq(_point, bb); + if (mindistsq >= dsq) { + all_t = _path[ei].allNearestTimes(_point, 0, et); + dsq = distanceSq(_point, _path[ei].pointAt(all_t.front())); + if (mindistsq > dsq) { + for (unsigned int i = 0; i < all_t.size(); ++i) { + all_t[i] = ei + all_t[i]; + } + return all_t; + } else if (mindistsq == dsq) { + all_np.push_back(all_t); + ni.push_back(ei); + } + } + std::vector all_nearest; + for (unsigned int i = 0; i < all_np.size(); ++i) { + for (unsigned int j = 0; j < all_np[i].size(); ++j) { + all_nearest.push_back(ni[i] + all_np[i][j]); + } + } + all_nearest.erase(std::unique(all_nearest.begin(), all_nearest.end()), all_nearest.end()); + return all_nearest; +} + +std::vector Path::nearestTimePerCurve(Point const &p) const +{ + // return a single nearest time for each curve in this path + std::vector np; + for (const_iterator it = begin(); it != end_default(); ++it) { + np.push_back(it->nearestTime(p)); + } + return np; +} + +PathTime Path::nearestTime(Point const &p, Coord *dist) const +{ + Coord mindist = std::numeric_limits::max(); + PathTime ret; + + if (_data->curves.size() == 1) { + // naked moveto + ret.curve_index = 0; + ret.t = 0; + if (dist) { + *dist = distance(_closing_seg->initialPoint(), p); + } + return ret; + } + + for (size_type i = 0; i < size_default(); ++i) { + Curve const &c = at(i); + if (distance(p, c.boundsFast()) >= mindist) continue; + + Coord t = c.nearestTime(p); + Coord d = distance(c.pointAt(t), p); + if (d < mindist) { + mindist = d; + ret.curve_index = i; + ret.t = t; + } + } + if (dist) { + *dist = mindist; + } + + return ret; +} + +std::vector Path::nodes() const +{ + std::vector result; + size_type path_size = size_closed(); + for (size_type i = 0; i < path_size; ++i) { + result.push_back(_data->curves[i].initialPoint()); + } + return result; +} + +void Path::appendPortionTo(Path &ret, double from, double to) const +{ + if (!(from >= 0 && to >= 0)) { + THROW_RANGEERROR("from and to must be >=0 in Path::appendPortionTo"); + } + if (to == 0) + to = size() + 0.999999; + if (from == to) { + return; + } + double fi, ti; + double ff = modf(from, &fi), tf = modf(to, &ti); + if (tf == 0) { + ti--; + tf = 1; + } + const_iterator fromi = inc(begin(), (unsigned)fi); + if (fi == ti && from < to) { + ret.append(fromi->portion(ff, tf)); + return; + } + const_iterator toi = inc(begin(), (unsigned)ti); + if (ff != 1.) { + // fromv->setInitial(ret.finalPoint()); + ret.append(fromi->portion(ff, 1.)); + } + if (from >= to) { + const_iterator ender = end(); + if (ender->initialPoint() == ender->finalPoint()) + ++ender; + ret.insert(ret.end(), ++fromi, ender); + ret.insert(ret.end(), begin(), toi); + } else { + ret.insert(ret.end(), ++fromi, toi); + } + ret.append(toi->portion(0., tf)); +} + +void Path::appendPortionTo(Path &target, PathInterval const &ival, + boost::optional const &p_from, boost::optional const &p_to) const +{ + assert(ival.pathSize() == size_closed()); + + if (ival.isDegenerate()) { + Point stitch_to = p_from ? *p_from : pointAt(ival.from()); + target.stitchTo(stitch_to); + return; + } + + PathTime const &from = ival.from(), &to = ival.to(); + + bool reverse = ival.reverse(); + int di = reverse ? -1 : 1; + size_type s = size_closed(); + + if (!ival.crossesStart() && from.curve_index == to.curve_index) { + Curve *c = (*this)[from.curve_index].portion(from.t, to.t); + if (p_from) { + c->setInitial(*p_from); + } + if (p_to) { + c->setFinal(*p_to); + } + target.append(c); + } else { + Curve *c_first = (*this)[from.curve_index].portion(from.t, reverse ? 0 : 1); + if (p_from) { + c_first->setInitial(*p_from); + } + target.append(c_first); + + for (size_type i = (from.curve_index + s + di) % s; i != to.curve_index; + i = (i + s + di) % s) + { + if (reverse) { + target.append((*this)[i].reverse()); + } else { + target.append((*this)[i].duplicate()); + } + } + + Curve *c_last = (*this)[to.curve_index].portion(reverse ? 1 : 0, to.t); + if (p_to) { + c_last->setFinal(*p_to); + } + target.append(c_last); + } +} + +Path Path::reversed() const +{ + typedef std::reverse_iterator RIter; + + Path ret(finalPoint()); + if (empty()) return ret; + + ret._data->curves.pop_back(); // this also deletes the closing segment from ret + + RIter iter(_includesClosingSegment() ? _data->curves.end() : _data->curves.end() - 1); + RIter rend(_data->curves.begin()); + + if (_closed) { + // when the path is closed, there are two cases: + if (front().isLineSegment()) { + // 1. initial segment is linear: it becomes the new closing segment. + rend = RIter(_data->curves.begin() + 1); + ret._closing_seg = new ClosingSegment(front().finalPoint(), front().initialPoint()); + } else { + // 2. initial segment is not linear: the closing segment becomes degenerate. + // However, skip it if it's already degenerate. + Point fp = finalPoint(); + ret._closing_seg = new ClosingSegment(fp, fp); + } + } else { + // when the path is open, we reverse all real curves, and add a reversed closing segment. + ret._closing_seg = static_cast(_closing_seg->reverse()); + } + + for (; iter != rend; ++iter) { + ret._data->curves.push_back(iter->reverse()); + } + ret._data->curves.push_back(ret._closing_seg); + ret._closed = _closed; + return ret; +} + + +void Path::insert(iterator pos, Curve const &curve) +{ + _unshare(); + Sequence::iterator seq_pos(seq_iter(pos)); + Sequence source; + source.push_back(curve.duplicate()); + do_update(seq_pos, seq_pos, source); +} + +void Path::erase(iterator pos) +{ + _unshare(); + Sequence::iterator seq_pos(seq_iter(pos)); + + Sequence stitched; + do_update(seq_pos, seq_pos + 1, stitched); +} + +void Path::erase(iterator first, iterator last) +{ + _unshare(); + Sequence::iterator seq_first = seq_iter(first); + Sequence::iterator seq_last = seq_iter(last); + + Sequence stitched; + do_update(seq_first, seq_last, stitched); +} + +void Path::stitchTo(Point const &p) +{ + if (!empty() && _closing_seg->initialPoint() != p) { + if (_exception_on_stitch) { + THROW_CONTINUITYERROR(); + } + _unshare(); + do_append(new StitchSegment(_closing_seg->initialPoint(), p)); + } +} + +void Path::replace(iterator replaced, Curve const &curve) +{ + replace(replaced, replaced + 1, curve); +} + +void Path::replace(iterator first_replaced, iterator last_replaced, Curve const &curve) +{ + _unshare(); + Sequence::iterator seq_first_replaced(seq_iter(first_replaced)); + Sequence::iterator seq_last_replaced(seq_iter(last_replaced)); + Sequence source(1); + source.push_back(curve.duplicate()); + + do_update(seq_first_replaced, seq_last_replaced, source); +} + +void Path::replace(iterator replaced, Path const &path) +{ + replace(replaced, path.begin(), path.end()); +} + +void Path::replace(iterator first, iterator last, Path const &path) +{ + replace(first, last, path.begin(), path.end()); +} + +void Path::snapEnds(Coord precision) +{ + if (!_closed) return; + if (_data->curves.size() > 1 && are_near(_closing_seg->length(precision), 0, precision)) { + _unshare(); + _closing_seg->setInitial(_closing_seg->finalPoint()); + (_data->curves.end() - 1)->setFinal(_closing_seg->finalPoint()); + } +} + +// replace curves between first and last with contents of source, +// +void Path::do_update(Sequence::iterator first, Sequence::iterator last, Sequence &source) +{ + // TODO: handle cases where first > last in closed paths? + bool last_beyond_closing_segment = (last == _data->curves.end()); + + // special case: + // if do_update replaces the closing segment, we have to regenerate it + if (source.empty()) { + if (first == last) return; // nothing to do + + // only removing some segments + if ((!_closed && first == _data->curves.begin()) || (!_closed && last == _data->curves.end() - 1) || last_beyond_closing_segment) { + // just adjust the closing segment + // do nothing + } else if (first->initialPoint() != (last - 1)->finalPoint()) { + if (_exception_on_stitch) { + THROW_CONTINUITYERROR(); + } + source.push_back(new StitchSegment(first->initialPoint(), (last - 1)->finalPoint())); + } + } else { + // replacing + if (first == _data->curves.begin() && last == _data->curves.end()) { + // special case: replacing everything should work the same in open and closed curves + _data->curves.erase(_data->curves.begin(), _data->curves.end() - 1); + _closing_seg->setFinal(source.front().initialPoint()); + _closing_seg->setInitial(source.back().finalPoint()); + _data->curves.transfer(_data->curves.begin(), source.begin(), source.end(), source); + return; + } + + // stitch in front + if (!_closed && first == _data->curves.begin()) { + // not necessary to stitch in front + } else if (first->initialPoint() != source.front().initialPoint()) { + if (_exception_on_stitch) { + THROW_CONTINUITYERROR(); + } + source.insert(source.begin(), new StitchSegment(first->initialPoint(), source.front().initialPoint())); + } + + // stitch at the end + if ((!_closed && last == _data->curves.end() - 1) || last_beyond_closing_segment) { + // repurpose the closing segment as the stitch segment + // do nothing + } else if (source.back().finalPoint() != (last - 1)->finalPoint()) { + if (_exception_on_stitch) { + THROW_CONTINUITYERROR(); + } + source.push_back(new StitchSegment(source.back().finalPoint(), (last - 1)->finalPoint())); + } + } + + // do not erase the closing segment, adjust it instead + if (last_beyond_closing_segment) { + --last; + } + _data->curves.erase(first, last); + _data->curves.transfer(first, source.begin(), source.end(), source); + + // adjust closing segment + if (size_open() == 0) { + _closing_seg->setFinal(_closing_seg->initialPoint()); + } else { + _closing_seg->setInitial(back_open().finalPoint()); + _closing_seg->setFinal(front().initialPoint()); + } + + checkContinuity(); +} + +void Path::do_append(Curve *c) +{ + if (&_data->curves.front() == _closing_seg) { + _closing_seg->setFinal(c->initialPoint()); + } else { + // if we can't freely move the closing segment, we check whether + // the new curve connects with the last non-closing curve + if (c->initialPoint() != _closing_seg->initialPoint()) { + THROW_CONTINUITYERROR(); + } + if (_closed && c->isLineSegment() && + c->finalPoint() == _closing_seg->finalPoint()) + { + // appending a curve that matches the closing segment has no effect + delete c; + return; + } + } + _data->curves.insert(_data->curves.end() - 1, c); + _closing_seg->setInitial(c->finalPoint()); +} + +void Path::checkContinuity() const +{ + Sequence::const_iterator i = _data->curves.begin(), j = _data->curves.begin(); + ++j; + for (; j != _data->curves.end(); ++i, ++j) { + if (i->finalPoint() != j->initialPoint()) { + THROW_CONTINUITYERROR(); + } + } + if (_data->curves.front().initialPoint() != _data->curves.back().finalPoint()) { + THROW_CONTINUITYERROR(); + } +} + +// breaks time value into integral and fractional part +PathTime Path::_factorTime(Coord t) const +{ + size_type sz = size_default(); + if (t < 0 || t > sz) { + THROW_RANGEERROR("parameter t out of bounds"); + } + + PathTime ret; + Coord k; + ret.t = modf(t, &k); + ret.curve_index = k; + if (ret.curve_index == sz) { + --ret.curve_index; + ret.t = 1; + } + return ret; +} + +Piecewise > paths_to_pw(PathVector const &paths) +{ + Piecewise > ret = paths[0].toPwSb(); + for (unsigned i = 1; i < paths.size(); i++) { + ret.concat(paths[i].toPwSb()); + } + return ret; +} + +bool are_near(Path const &a, Path const &b, Coord precision) +{ + if (a.size() != b.size()) return false; + + for (unsigned i = 0; i < a.size(); ++i) { + if (!a[i].isNear(b[i], precision)) return false; + } + return true; +} + +std::ostream &operator<<(std::ostream &out, Path const &path) +{ + SVGPathWriter pw; + pw.feed(path); + out << pw.str(); + return out; +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/path.h b/src/2geom/path.h new file mode 100644 index 0000000..81511b2 --- /dev/null +++ b/src/2geom/path.h @@ -0,0 +1,874 @@ +/** @file + * @brief Path - a sequence of contiguous curves + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_PATH_H +#define LIB2GEOM_SEEN_PATH_H + +#include +#include +#include +#include +#include +#include +#include <2geom/intersection.h> +#include <2geom/curve.h> +#include <2geom/bezier-curve.h> +#include <2geom/transforms.h> + +namespace Geom { + +class Path; +class ConvexHull; + +namespace PathInternal { + +typedef boost::ptr_vector Sequence; + +struct PathData { + Sequence curves; + OptRect fast_bounds; +}; + +template +class BaseIterator + : public boost::random_access_iterator_helper + < BaseIterator

+ , Curve const + , std::ptrdiff_t + , Curve const * + , Curve const & + > +{ + protected: + BaseIterator(P &p, unsigned i) : path(&p), index(i) {} + // default copy, default assign + typedef BaseIterator

Self; + + public: + BaseIterator() : path(NULL), index(0) {} + + bool operator<(BaseIterator const &other) const { + return path == other.path && index < other.index; + } + bool operator==(BaseIterator const &other) const { + return path == other.path && index == other.index; + } + Curve const &operator*() const { + return (*path)[index]; + } + + Self &operator++() { + ++index; + return *this; + } + Self &operator--() { + --index; + return *this; + } + Self &operator+=(std::ptrdiff_t d) { + index += d; + return *this; + } + Self &operator-=(std::ptrdiff_t d) { + index -= d; + return *this; + } + + private: + P *path; + unsigned index; + + friend class ::Geom::Path; +}; + +} + +/** @brief Generalized time value in the path. + * + * This class exists because when mapping the range of multiple curves onto the same interval + * as the curve index, we lose some precision. For instance, a path with 16 curves will + * have 4 bits less precision than a path with 1 curve. If you need high precision results + * in long paths, either use this class and related methods instead of the standard methods + * pointAt(), nearestTime() and so on, or use curveAt() to first obtain the curve, then + * call the method again to obtain a high precision result. + * + * @ingroup Paths */ +struct PathTime + : boost::totally_ordered +{ + typedef PathInternal::Sequence::size_type size_type; + + Coord t; ///< Time value in the curve + size_type curve_index; ///< Index of the curve in the path + + PathTime() : t(0), curve_index(0) {} + PathTime(size_type idx, Coord tval) : t(tval), curve_index(idx) {} + + bool operator<(PathTime const &other) const { + if (curve_index < other.curve_index) return true; + if (curve_index == other.curve_index) { + return t < other.t; + } + return false; + } + bool operator==(PathTime const &other) const { + return curve_index == other.curve_index && t == other.t; + } + /// Convert times at or beyond 1 to 0 on the next curve. + void normalizeForward(size_type path_size) { + if (t >= 1) { + curve_index = (curve_index + 1) % path_size; + t = 0; + } + } + /// Convert times at or before 0 to 1 on the previous curve. + void normalizeBackward(size_type path_size) { + if (t <= 0) { + curve_index = (curve_index - 1) % path_size; + t = 1; + } + } + + Coord asFlatTime() const { return curve_index + t; } +}; + +inline std::ostream &operator<<(std::ostream &os, PathTime const &pos) { + os << pos.curve_index << ": " << format_coord_nice(pos.t); + return os; +} + + +/** @brief Contiguous subset of the path's parameter domain. + * This is a directed interval, which allows one to specify any contiguous subset + * of the path's domain, including subsets that wrap around the initial point + * of the path. + * @ingroup Paths */ +class PathInterval { +public: + typedef PathInternal::Sequence::size_type size_type; + + /** @brief Default interval. + * Default-constructed PathInterval includes only the initial point of the initial segment. */ + PathInterval(); + + /** @brief Construct an interval in the path's parameter domain. + * @param from Initial time + * @param to Final time + * @param cross_start If true, the interval will proceed from the initial to final + * time through the initial point of the path, wrapping around the closing segment; + * otherwise it will not wrap around the closing segment. + * @param path_size Size of the path to which this interval applies, required + * to clean up degenerate cases */ + PathInterval(PathTime const &from, PathTime const &to, bool cross_start, size_type path_size); + + /// Get the time value of the initial point. + PathTime const &initialTime() const { return _from; } + /// Get the time value of the final point. + PathTime const &finalTime() const { return _to; } + + PathTime const &from() const { return _from; } + PathTime const &to() const { return _to; } + + /// Check whether the interval has only one point. + bool isDegenerate() const { return _from == _to; } + /// True if the interval goes in the direction of decreasing time values. + bool reverse() const { return _reverse; } + /// True if the interior of the interval contains the initial point of the path. + bool crossesStart() const { return _cross_start; } + + /// Test a path time for inclusion. + bool contains(PathTime const &pos) const; + + /// Get a time at least @a min_dist away in parameter space from the ends. + /// If no such time exists, the middle point is returned. + PathTime inside(Coord min_dist = EPSILON) const; + + /// Select one of two intervals with given endpoints by parameter direction. + static PathInterval from_direction(PathTime const &from, PathTime const &to, + bool reversed, size_type path_size); + + /// Select one of two intervals with given endpoints by whether it includes the initial point. + static PathInterval from_start_crossing(PathTime const &from, PathTime const &to, + bool cross_start, size_type path_size) { + PathInterval result(from, to, cross_start, path_size); + return result; + } + + size_type pathSize() const { return _path_size; } + size_type curveCount() const; + +private: + PathTime _from, _to; + size_type _path_size; + bool _cross_start, _reverse; +}; + +/// Create an interval in the direction of increasing time value. +/// @relates PathInterval +inline PathInterval forward_interval(PathTime const &from, PathTime const &to, + PathInterval::size_type path_size) +{ + PathInterval result = PathInterval::from_direction(from, to, false, path_size); + return result; +} + +/// Create an interval in the direction of decreasing time value. +/// @relates PathInterval +inline PathInterval backward_interval(PathTime const &from, PathTime const &to, + PathInterval::size_type path_size) +{ + PathInterval result = PathInterval::from_direction(from, to, true, path_size); + return result; +} + +/// Output an interval in the path's domain. +/// @relates PathInterval +inline std::ostream &operator<<(std::ostream &os, PathInterval const &ival) { + os << "PathInterval["; + if (ival.crossesStart()) { + os << ival.from() << " -> 0: 0.0 -> " << ival.to(); + } else { + os << ival.from() << " -> " << ival.to(); + } + os << "]"; + return os; +} + +typedef Intersection PathIntersection; + +template <> +struct ShapeTraits { + typedef PathTime TimeType; + typedef PathInterval IntervalType; + typedef Path AffineClosureType; + typedef PathIntersection IntersectionType; +}; + +/** @brief Sequence of contiguous curves, aka spline. + * + * Path represents a sequence of contiguous curves, also known as a spline. + * It corresponds to a "subpath" in SVG terminology. It can represent both + * open and closed subpaths. The final point of each curve is exactly + * equal to the initial point of the next curve. + * + * The path always contains a linear closing segment that connects + * the final point of the last "real" curve to the initial point of the + * first curve. This way the curves form a closed loop even for open paths. + * If the closing segment has nonzero length and the path is closed, it is + * considered a normal part of the path data. There are three distinct sets + * of end iterators one can use to iterate over the segments: + * + * - Iterating between @a begin() and @a end() will iterate over segments + * which are part of the path. + * - Iterating between @a begin() and @a end_closed() + * will always iterate over a closed loop of segments. + * - Iterating between @a begin() and @a end_open() will always skip + * the final linear closing segment. + * + * If the final point of the last "real" segment coincides exactly with the initial + * point of the first segment, the closing segment will be absent from both + * [begin(), end_open()) and [begin(), end_closed()). + * + * Normally, an exception will be thrown when you try to insert a curve + * that makes the path non-continuous. If you are working with unsanitized + * curve data, you can call setStitching(true), which will insert line segments + * to make the path continuous. + * + * Internally, Path uses copy-on-write data. This is done for two reasons: first, + * copying a Curve requires calling a virtual function, so it's a little more expensive + * that normal copying; and second, it reduces the memory cost of copying the path. + * Therefore you can return Path and PathVector from functions without worrying + * about temporary copies. + * + * Note that this class cannot represent arbitrary shapes, which may contain holes. + * To do that, use PathVector, which is more generic. + * + * It's not very convenient to create a Path directly. To construct paths more easily, + * use PathBuilder. + * + * @ingroup Paths */ +class Path + : boost::equality_comparable< Path > +{ +public: + typedef PathInternal::PathData PathData; + typedef PathInternal::Sequence Sequence; + typedef PathInternal::BaseIterator iterator; + typedef PathInternal::BaseIterator const_iterator; + typedef Sequence::size_type size_type; + typedef Sequence::difference_type difference_type; + + class ClosingSegment : public LineSegment { + public: + ClosingSegment() : LineSegment() {} + ClosingSegment(Point const &p1, Point const &p2) : LineSegment(p1, p2) {} + virtual Curve *duplicate() const { return new ClosingSegment(*this); } + virtual Curve *reverse() const { return new ClosingSegment((*this)[1], (*this)[0]); } + }; + + class StitchSegment : public LineSegment { + public: + StitchSegment() : LineSegment() {} + StitchSegment(Point const &p1, Point const &p2) : LineSegment(p1, p2) {} + virtual Curve *duplicate() const { return new StitchSegment(*this); } + virtual Curve *reverse() const { return new StitchSegment((*this)[1], (*this)[0]); } + }; + + // Path(Path const &other) - use default copy constructor + + /// Construct an empty path starting at the specified point. + explicit Path(Point const &p = Point()) + : _data(new PathData()) + , _closing_seg(new ClosingSegment(p, p)) + , _closed(false) + , _exception_on_stitch(true) + { + _data->curves.push_back(_closing_seg); + } + + /// Construct a path containing a range of curves. + template + Path(Iter first, Iter last, bool closed = false, bool stitch = false) + : _data(new PathData()) + , _closed(closed) + , _exception_on_stitch(!stitch) + { + for (Iter i = first; i != last; ++i) { + _data->curves.push_back(i->duplicate()); + } + if (!_data->curves.empty()) { + _closing_seg = new ClosingSegment(_data->curves.back().finalPoint(), + _data->curves.front().initialPoint()); + } else { + _closing_seg = new ClosingSegment(); + } + _data->curves.push_back(_closing_seg); + } + + /// Create a path from a rectangle. + explicit Path(Rect const &r); + /// Create a path from a convex hull. + explicit Path(ConvexHull const &); + /// Create a path from a circle, using two elliptical arcs. + explicit Path(Circle const &c); + /// Create a path from an ellipse, using two elliptical arcs. + explicit Path(Ellipse const &e); + + virtual ~Path() {} + + // Path &operator=(Path const &other) - use default assignment operator + + /** @brief Swap contents with another path + * @todo Add noexcept specifiers for C++11 */ + void swap(Path &other) throw() { + using std::swap; + swap(other._data, _data); + swap(other._closing_seg, _closing_seg); + swap(other._closed, _closed); + swap(other._exception_on_stitch, _exception_on_stitch); + } + /** @brief Swap contents of two paths. + * @relates Path */ + friend inline void swap(Path &a, Path &b) throw() { a.swap(b); } + + /** @brief Access a curve by index */ + Curve const &operator[](size_type i) const { return _data->curves[i]; } + /** @brief Access a curve by index */ + Curve const &at(size_type i) const { return _data->curves.at(i); } + + /** @brief Access the first curve in the path. + * Since the curve always contains at least a degenerate closing segment, + * it is always safe to use this method. */ + Curve const &front() const { return _data->curves.front(); } + /// Alias for front(). + Curve const &initialCurve() const { return _data->curves.front(); } + /** @brief Access the last curve in the path. */ + Curve const &back() const { return back_default(); } + Curve const &back_open() const { + if (empty()) return _data->curves.back(); + return _data->curves[_data->curves.size() - 2]; + } + Curve const &back_closed() const { + return _closing_seg->isDegenerate() + ? _data->curves[_data->curves.size() - 2] + : _data->curves[_data->curves.size() - 1]; + } + Curve const &back_default() const { + return _includesClosingSegment() + ? back_closed() + : back_open(); + } + Curve const &finalCurve() const { return back_default(); } + + const_iterator begin() const { return const_iterator(*this, 0); } + const_iterator end() const { return end_default(); } + const_iterator end_default() const { return const_iterator(*this, size_default()); } + const_iterator end_open() const { return const_iterator(*this, size_open()); } + const_iterator end_closed() const { return const_iterator(*this, size_closed()); } + iterator begin() { return iterator(*this, 0); } + iterator end() { return end_default(); } + iterator end_default() { return iterator(*this, size_default()); } + iterator end_open() { return iterator(*this, size_open()); } + iterator end_closed() { return iterator(*this, size_closed()); } + + /// Size without the closing segment, even if the path is closed. + size_type size_open() const { return _data->curves.size() - 1; } + + /** @brief Size with the closing segment, if it makes a difference. + * If the closing segment is degenerate, i.e. its initial and final points + * are exactly equal, then it is not included in this size. */ + size_type size_closed() const { + return _closing_seg->isDegenerate() ? _data->curves.size() - 1 : _data->curves.size(); + } + + /// Natural size of the path. + size_type size_default() const { + return _includesClosingSegment() ? size_closed() : size_open(); + } + /// Natural size of the path. + size_type size() const { return size_default(); } + + size_type max_size() const { return _data->curves.max_size() - 1; } + + /** @brief Check whether path is empty. + * The path is empty if it contains only the closing segment, which according + * to the continuity invariant must be degenerate. Note that unlike standard + * containers, two empty paths are not necessarily identical, because the + * degenerate closing segment may be at a different point, affecting the operation + * of methods such as appendNew(). */ + bool empty() const { return (_data->curves.size() == 1); } + + /// Check whether the path is closed. + bool closed() const { return _closed; } + + /** @brief Set whether the path is closed. + * When closing a path where the last segment can be represented as a closing + * segment, the last segment will be removed. When opening a path, the closing + * segment will be erased. This means that closing and then opening a path + * will not always give back the original path. */ + void close(bool closed = true); + + /** @brief Remove all curves from the path. + * The initial and final points of the closing segment are set to (0,0). + * The stitching flag remains unchanged. */ + void clear(); + + /** @brief Get the approximate bounding box. + * The rectangle returned by this method will contain all the curves, but it's not + * guaranteed to be the smallest possible one */ + OptRect boundsFast() const; + + /** @brief Get a tight-fitting bounding box. + * This will return the smallest possible axis-aligned rectangle containing + * all the curves in the path. */ + OptRect boundsExact() const; + + Piecewise > toPwSb() const; + + /// Test paths for exact equality. + bool operator==(Path const &other) const; + + /// Apply a transform to each curve. + template + Path &operator*=(T const &tr) { + BOOST_CONCEPT_ASSERT((TransformConcept)); + _unshare(); + for (std::size_t i = 0; i < _data->curves.size(); ++i) { + _data->curves[i] *= tr; + } + return *this; + } + + template + friend Path operator*(Path const &path, T const &tr) { + BOOST_CONCEPT_ASSERT((TransformConcept)); + Path result(path); + result *= tr; + return result; + } + + /** @brief Get the allowed range of time values. + * @return Values for which pointAt() and valueAt() yield valid results. */ + Interval timeRange() const; + + /** Get the curve at the specified time value. + * @param t Time value + * @param rest Optional storage for the corresponding time value in the curve */ + Curve const &curveAt(Coord t, Coord *rest = NULL) const; + + /// Get the closing segment of the path. + LineSegment const &closingSegment() const { return *_closing_seg; } + + /** @brief Get the point at the specified time value. + * Note that this method has reduced precision with respect to calling pointAt() + * directly on the curve. If you want high precision results, use the version + * that takes a PathTime parameter. + * + * Allowed time values range from zero to the number of curves; you can retrieve + * the allowed range of values with timeRange(). */ + Point pointAt(Coord t) const; + + /// Get one coordinate (X or Y) at the specified time value. + Coord valueAt(Coord t, Dim2 d) const; + + /// Get the curve at the specified path time. + Curve const &curveAt(PathTime const &pos) const; + /// Get the point at the specified path time. + Point pointAt(PathTime const &pos) const; + /// Get one coordinate at the specified path time. + Coord valueAt(PathTime const &pos, Dim2 d) const; + + Point operator()(Coord t) const { return pointAt(t); } + + /// Compute intersections with axis-aligned line. + std::vector roots(Coord v, Dim2 d) const; + + /// Compute intersections with another path. + std::vector intersect(Path const &other, Coord precision = EPSILON) const; + + /** @brief Determine the winding number at the specified point. + * + * The winding number is the number of full turns made by a ray that connects the passed + * point and the path's value (i.e. the result of the pointAt() method) as the time increases + * from 0 to the maximum valid value. Positive numbers indicate turns in the direction + * of increasing angles. + * + * Winding numbers are often used as the definition of what is considered "inside" + * the shape. Typically points with either nonzero winding or odd winding are + * considered to be inside the path. */ + int winding(Point const &p) const; + + std::vector allNearestTimes(Point const &p, Coord from, Coord to) const; + std::vector allNearestTimes(Point const &p) const { + return allNearestTimes(p, 0, size_default()); + } + + PathTime nearestTime(Point const &p, Coord *dist = NULL) const; + std::vector nearestTimePerCurve(Point const &p) const; + + std::vector nodes() const; + + void appendPortionTo(Path &p, Coord f, Coord t) const; + + /** @brief Append a subset of this path to another path. + * An extra stitching segment will be inserted if the start point of the portion + * and the final point of the target path do not match exactly. + * The closing segment of the target path will be modified. */ + void appendPortionTo(Path &p, PathTime const &from, PathTime const &to, bool cross_start = false) const { + PathInterval ival(from, to, cross_start, size_closed()); + appendPortionTo(p, ival, boost::none, boost::none); + } + + /** @brief Append a subset of this path to another path. + * This version allows you to explicitly pass a PathInterval. */ + void appendPortionTo(Path &p, PathInterval const &ival) const { + appendPortionTo(p, ival, boost::none, boost::none); + } + + /** @brief Append a subset of this path to another path, specifying endpoints. + * This method is for use in situations where endpoints of the portion segments + * have to be set exactly, for instance when computing Boolean operations. */ + void appendPortionTo(Path &p, PathInterval const &ival, + boost::optional const &p_from, boost::optional const &p_to) const; + + Path portion(Coord f, Coord t) const { + Path ret; + ret.close(false); + appendPortionTo(ret, f, t); + return ret; + } + + Path portion(Interval const &i) const { return portion(i.min(), i.max()); } + + /** @brief Get a subset of the current path with full precision. + * When @a from is larger (later in the path) than @a to, the returned portion + * will be reversed. If @a cross_start is true, the portion will be reversed + * and will cross the initial point of the path. Therefore, when @a from is larger + * than @a to and @a cross_start is true, the returned portion will not be reversed, + * but will "wrap around" the end of the path. */ + Path portion(PathTime const &from, PathTime const &to, bool cross_start = false) const { + Path ret; + ret.close(false); + appendPortionTo(ret, from, to, cross_start); + return ret; + } + + /** @brief Get a subset of the current path with full precision. + * This version allows you to explicitly pass a PathInterval. */ + Path portion(PathInterval const &ival) const { + Path ret; + ret.close(false); + appendPortionTo(ret, ival); + return ret; + } + + /** @brief Obtain a reversed version of the current path. + * The final point of the current path will become the initial point + * of the reversed path, unless it is closed and has a non-degenerate + * closing segment. In that case, the new initial point will be the final point + * of the last "real" segment. */ + Path reversed() const; + + void insert(iterator pos, Curve const &curve); + + template + void insert(iterator pos, Iter first, Iter last) { + _unshare(); + Sequence::iterator seq_pos(seq_iter(pos)); + Sequence source; + for (; first != last; ++first) { + source.push_back(first->duplicate()); + } + do_update(seq_pos, seq_pos, source); + } + + void erase(iterator pos); + void erase(iterator first, iterator last); + + // erase last segment of path + void erase_last() { erase(iterator(*this, size() - 1)); } + + void start(Point const &p); + + /** @brief Get the first point in the path. */ + Point initialPoint() const { return (*_closing_seg)[1]; } + + /** @brief Get the last point in the path. + * If the path is closed, this is always the same as the initial point. */ + Point finalPoint() const { return (*_closing_seg)[_closed ? 1 : 0]; } + + void setInitial(Point const &p) { + _unshare(); + _closed = false; + _data->curves.front().setInitial(p); + _closing_seg->setFinal(p); + } + void setFinal(Point const &p) { + _unshare(); + _closed = false; + _data->curves[size_open() - 1].setFinal(p); + _closing_seg->setInitial(p); + } + + /** @brief Add a new curve to the end of the path. + * This inserts the new curve right before the closing segment. + * The path takes ownership of the passed pointer, which should not be freed. */ + void append(Curve *curve) { + _unshare(); + stitchTo(curve->initialPoint()); + do_append(curve); + } + + void append(Curve const &curve) { + _unshare(); + stitchTo(curve.initialPoint()); + do_append(curve.duplicate()); + } + void append(D2 const &curve) { + _unshare(); + stitchTo(Point(curve[X][0][0], curve[Y][0][0])); + do_append(new SBasisCurve(curve)); + } + void append(Path const &other) { + replace(end_open(), other.begin(), other.end()); + } + + void replace(iterator replaced, Curve const &curve); + void replace(iterator first, iterator last, Curve const &curve); + void replace(iterator replaced, Path const &path); + void replace(iterator first, iterator last, Path const &path); + + template + void replace(iterator replaced, Iter first, Iter last) { + replace(replaced, replaced + 1, first, last); + } + + template + void replace(iterator first_replaced, iterator last_replaced, Iter first, Iter last) { + _unshare(); + Sequence::iterator seq_first_replaced(seq_iter(first_replaced)); + Sequence::iterator seq_last_replaced(seq_iter(last_replaced)); + Sequence source; + for (; first != last; ++first) { + source.push_back(first->duplicate()); + } + do_update(seq_first_replaced, seq_last_replaced, source); + } + + /** @brief Append a new curve to the path. + * + * This family of methods will automatically use the current final point of the path + * as the first argument of the new curve's constructor. To call this method, + * you'll need to write e.g.: + * @code + path.template appendNew(control1, control2, end_point); + @endcode + * It is important to note that the coordinates passed to appendNew should be finite! + * If one of the coordinates is infinite, 2geom will throw a ContinuityError exception. + */ + template + void appendNew(A a) { + _unshare(); + do_append(new CurveType(finalPoint(), a)); + } + + template + void appendNew(A a, B b) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b)); + } + + template + void appendNew(A a, B b, C c) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c)); + } + + template + void appendNew(A a, B b, C c, D d) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d)); + } + + template + void appendNew(A a, B b, C c, D d, E e) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d, e)); + } + + template + void appendNew(A a, B b, C c, D d, E e, F f) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d, e, f)); + } + + template + void appendNew(A a, B b, C c, D d, E e, F f, G g) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d, e, f, g)); + } + + template + void appendNew(A a, B b, C c, D d, E e, F f, G g, H h) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d, e, f, g, h)); + } + + template + void appendNew(A a, B b, C c, D d, E e, F f, G g, H h, I i) { + _unshare(); + do_append(new CurveType(finalPoint(), a, b, c, d, e, f, g, h, i)); + } + + /** @brief Reduce the closing segment to a point if it's shorter than precision. + * Do this by moving the final point. */ + void snapEnds(Coord precision = EPSILON); + + /// Append a stitching segment ending at the specified point. + void stitchTo(Point const &p); + + /** @brief Verify the continuity invariant. + * If the path is not contiguous, this will throw a CountinuityError. */ + void checkContinuity() const; + + /** @brief Enable or disable the throwing of exceptions when stitching discontinuities. + * Normally stitching will cause exceptions, but when you are working with unsanitized + * curve data, you can disable these exceptions. */ + void setStitching(bool x) { + _exception_on_stitch = !x; + } + +private: + static Sequence::iterator seq_iter(iterator const &iter) { + return iter.path->_data->curves.begin() + iter.index; + } + static Sequence::const_iterator seq_iter(const_iterator const &iter) { + return iter.path->_data->curves.begin() + iter.index; + } + + // whether the closing segment is part of the path + bool _includesClosingSegment() const { + return _closed && !_closing_seg->isDegenerate(); + } + void _unshare() { + // Called before every mutation. + // Ensure we have our own copy of curve data and reset cached values + if (!_data.unique()) { + _data.reset(new PathData(*_data)); + _closing_seg = static_cast(&_data->curves.back()); + } + _data->fast_bounds = OptRect(); + } + PathTime _factorTime(Coord t) const; + + void stitch(Sequence::iterator first_replaced, Sequence::iterator last_replaced, Sequence &sequence); + void do_update(Sequence::iterator first, Sequence::iterator last, Sequence &source); + + // n.b. takes ownership of curve object + void do_append(Curve *curve); + + boost::shared_ptr _data; + ClosingSegment *_closing_seg; + bool _closed; + bool _exception_on_stitch; +}; // end class Path + +Piecewise > paths_to_pw(PathVector const &paths); + +inline Coord nearest_time(Point const &p, Path const &c) { + PathTime pt = c.nearestTime(p); + return pt.curve_index + pt.t; +} + +bool are_near(Path const &a, Path const &b, Coord precision = EPSILON); + +std::ostream &operator<<(std::ostream &out, Path const &path); + +} // end namespace Geom + + +#endif // LIB2GEOM_SEEN_PATH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/pathvector.cpp b/src/2geom/pathvector.cpp new file mode 100644 index 0000000..8872587 --- /dev/null +++ b/src/2geom/pathvector.cpp @@ -0,0 +1,335 @@ +/** @file + * @brief PathVector - a sequence of subpaths + *//* + * Authors: + * Johan Engelen + * Krzysztof KosiÅ„ski + * + * Copyright 2008-2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/affine.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/svg-path-writer.h> +#include <2geom/sweeper.h> + +namespace Geom { + +//PathVector &PathVector::operator+=(PathVector const &other); + +PathVector::size_type PathVector::curveCount() const +{ + size_type n = 0; + for (const_iterator it = begin(); it != end(); ++it) { + n += it->size_default(); + } + return n; +} + +void PathVector::reverse(bool reverse_paths) +{ + if (reverse_paths) { + std::reverse(begin(), end()); + } + for (iterator i = begin(); i != end(); ++i) { + *i = i->reversed(); + } +} + +PathVector PathVector::reversed(bool reverse_paths) const +{ + PathVector ret; + for (const_iterator i = begin(); i != end(); ++i) { + ret.push_back(i->reversed()); + } + if (reverse_paths) { + std::reverse(ret.begin(), ret.end()); + } + return ret; +} + +Path &PathVector::pathAt(Coord t, Coord *rest) +{ + return const_cast(static_cast(this)->pathAt(t, rest)); +} +Path const &PathVector::pathAt(Coord t, Coord *rest) const +{ + PathVectorTime pos = _factorTime(t); + if (rest) { + *rest = Coord(pos.curve_index) + pos.t; + } + return at(pos.path_index); +} +Curve const &PathVector::curveAt(Coord t, Coord *rest) const +{ + PathVectorTime pos = _factorTime(t); + if (rest) { + *rest = pos.t; + } + return at(pos.path_index).at(pos.curve_index); +} +Coord PathVector::valueAt(Coord t, Dim2 d) const +{ + PathVectorTime pos = _factorTime(t); + return at(pos.path_index).at(pos.curve_index).valueAt(pos.t, d); +} +Point PathVector::pointAt(Coord t) const +{ + PathVectorTime pos = _factorTime(t); + return at(pos.path_index).at(pos.curve_index).pointAt(pos.t); +} + +OptRect PathVector::boundsFast() const +{ + OptRect bound; + if (empty()) return bound; + + bound = front().boundsFast(); + for (const_iterator it = ++begin(); it != end(); ++it) { + bound.unionWith(it->boundsFast()); + } + return bound; +} + +OptRect PathVector::boundsExact() const +{ + OptRect bound; + if (empty()) return bound; + + bound = front().boundsExact(); + for (const_iterator it = ++begin(); it != end(); ++it) { + bound.unionWith(it->boundsExact()); + } + return bound; +} + +void PathVector::snapEnds(Coord precision) +{ + for (std::size_t i = 0; i < size(); ++i) { + (*this)[i].snapEnds(precision); + } +} + +// sweepline optimization +// this is very similar to CurveIntersectionSweepSet in path.cpp +// should probably be merged +class PathIntersectionSweepSet { +public: + struct PathRecord { + boost::intrusive::list_member_hook<> _hook; + Path const *path; + std::size_t index; + unsigned which; + + PathRecord(Path const &p, std::size_t i, unsigned w) + : path(&p) + , index(i) + , which(w) + {} + }; + + typedef std::vector::iterator ItemIterator; + + PathIntersectionSweepSet(std::vector &result, + PathVector const &a, PathVector const &b, Coord precision) + : _result(result) + , _precision(precision) + { + _records.reserve(a.size() + b.size()); + for (std::size_t i = 0; i < a.size(); ++i) { + _records.push_back(PathRecord(a[i], i, 0)); + } + for (std::size_t i = 0; i < b.size(); ++i) { + _records.push_back(PathRecord(b[i], i, 1)); + } + } + + std::vector &items() { return _records; } + + Interval itemBounds(ItemIterator ii) { + OptRect r = ii->path->boundsFast(); + if (!r) return Interval(); + return (*r)[X]; + } + + void addActiveItem(ItemIterator ii) { + unsigned w = ii->which; + unsigned ow = (ii->which + 1) % 2; + + for (ActivePathList::iterator i = _active[ow].begin(); i != _active[ow].end(); ++i) { + if (!ii->path->boundsFast().intersects(i->path->boundsFast())) continue; + std::vector px = ii->path->intersect(*i->path, _precision); + for (std::size_t k = 0; k < px.size(); ++k) { + PathVectorTime tw(ii->index, px[k].first), tow(i->index, px[k].second); + _result.push_back(PVIntersection( + w == 0 ? tw : tow, + w == 0 ? tow : tw, + px[k].point())); + } + } + _active[w].push_back(*ii); + } + + void removeActiveItem(ItemIterator ii) { + ActivePathList &apl = _active[ii->which]; + apl.erase(apl.iterator_to(*ii)); + } + +private: + typedef boost::intrusive::list + < PathRecord + , boost::intrusive::member_hook + < PathRecord + , boost::intrusive::list_member_hook<> + , &PathRecord::_hook + > + > ActivePathList; + + std::vector &_result; + std::vector _records; + ActivePathList _active[2]; + Coord _precision; +}; + +std::vector PathVector::intersect(PathVector const &other, Coord precision) const +{ + std::vector result; + + PathIntersectionSweepSet pisset(result, *this, other, precision); + Sweeper sweeper(pisset); + sweeper.process(); + + std::sort(result.begin(), result.end()); + + return result; +} + +int PathVector::winding(Point const &p) const +{ + int wind = 0; + for (const_iterator i = begin(); i != end(); ++i) { + if (!i->boundsFast().contains(p)) continue; + wind += i->winding(p); + } + return wind; +} + +boost::optional PathVector::nearestTime(Point const &p, Coord *dist) const +{ + boost::optional retval; + + Coord mindist = infinity(); + for (size_type i = 0; i < size(); ++i) { + Coord d; + PathTime pos = (*this)[i].nearestTime(p, &d); + if (d < mindist) { + mindist = d; + retval = PathVectorTime(i, pos.curve_index, pos.t); + } + } + + if (dist) { + *dist = mindist; + } + return retval; +} + +std::vector PathVector::allNearestTimes(Point const &p, Coord *dist) const +{ + std::vector retval; + + Coord mindist = infinity(); + for (size_type i = 0; i < size(); ++i) { + Coord d; + PathTime pos = (*this)[i].nearestTime(p, &d); + if (d < mindist) { + mindist = d; + retval.clear(); + } + if (d <= mindist) { + retval.push_back(PathVectorTime(i, pos.curve_index, pos.t)); + } + } + + if (dist) { + *dist = mindist; + } + return retval; +} + +std::vector PathVector::nodes() const +{ + std::vector result; + for (size_type i = 0; i < size(); ++i) { + size_type path_size = (*this)[i].size_closed(); + for (size_type j = 0; j < path_size; ++j) { + result.push_back((*this)[i][j].initialPoint()); + } + } + return result; +} + +PathVectorTime PathVector::_factorTime(Coord t) const +{ + PathVectorTime ret; + Coord rest = 0; + ret.t = modf(t, &rest); + ret.curve_index = rest; + for (; ret.path_index < size(); ++ret.path_index) { + unsigned s = _data.at(ret.path_index).size_default(); + if (s > ret.curve_index) break; + // special case for the last point + if (s == ret.curve_index && ret.path_index + 1 == size()) { + --ret.curve_index; + ret.t = 1; + break; + } + ret.curve_index -= s; + } + return ret; +} + +std::ostream &operator<<(std::ostream &out, PathVector const &pv) +{ + SVGPathWriter wr; + wr.feed(pv); + out << wr.str(); + return out; +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/pathvector.h b/src/2geom/pathvector.h new file mode 100644 index 0000000..0cbe5bf --- /dev/null +++ b/src/2geom/pathvector.h @@ -0,0 +1,301 @@ +/** @file + * @brief PathVector - a sequence of subpaths + *//* + * Authors: + * Johan Engelen + * Krzysztof KosiÅ„ski + * + * Copyright 2008-2014 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_PATHVECTOR_H +#define LIB2GEOM_SEEN_PATHVECTOR_H + +#include +#include +#include +#include <2geom/forward.h> +#include <2geom/path.h> +#include <2geom/transforms.h> + +namespace Geom { + +/** @brief Generalized time value in the path vector. + * + * This class exists because mapping the range of multiple curves onto the same interval + * as the curve index, we lose some precision. For instance, a path with 16 curves will + * have 4 bits less precision than a path with 1 curve. If you need high precision results + * in long paths, use this class and related methods instead of the standard methods + * pointAt(), nearestTime() and so on. + * + * @ingroup Paths */ +struct PathVectorTime + : public PathTime + , boost::totally_ordered +{ + size_type path_index; ///< Index of the path in the vector + + PathVectorTime() : PathTime(0, 0), path_index(0) {} + PathVectorTime(size_type _i, size_type _c, Coord _t) + : PathTime(_c, _t), path_index(_i) {} + PathVectorTime(size_type _i, PathTime const &pos) + : PathTime(pos), path_index(_i) {} + + bool operator<(PathVectorTime const &other) const { + if (path_index < other.path_index) return true; + if (path_index == other.path_index) { + return static_cast(*this) < static_cast(other); + } + return false; + } + bool operator==(PathVectorTime const &other) const { + return path_index == other.path_index + && static_cast(*this) == static_cast(other); + } + + PathTime const &asPathTime() const { + return *static_cast(this); + } +}; + +inline std::ostream &operator<<(std::ostream &os, PathVectorTime const &pvt) { + os << pvt.path_index << ": " << pvt.asPathTime(); + return os; +} + +typedef Intersection PathVectorIntersection; +typedef PathVectorIntersection PVIntersection; ///< Alias to save typing + +template <> +struct ShapeTraits { + typedef PathVectorTime TimeType; + //typedef PathVectorInterval IntervalType; + typedef PathVector AffineClosureType; + typedef PathVectorIntersection IntersectionType; +}; + +/** @brief Sequence of subpaths. + * + * This class corresponds to the SVG notion of a path: + * a sequence of any number of open or closed contiguous subpaths. + * Unlike Path, this class is closed under boolean operations. + * + * If you want to represent an arbitrary shape, this is the best class to use. + * Shapes with a boundary that is composed of only a single contiguous + * component can be represented with Path instead. + * + * @ingroup Paths + */ +class PathVector + : MultipliableNoncommutative< PathVector, Affine + , MultipliableNoncommutative< PathVector, Translate + , MultipliableNoncommutative< PathVector, Scale + , MultipliableNoncommutative< PathVector, Rotate + , MultipliableNoncommutative< PathVector, HShear + , MultipliableNoncommutative< PathVector, VShear + , MultipliableNoncommutative< PathVector, Zoom + , boost::equality_comparable< PathVector + > > > > > > > > +{ + typedef std::vector Sequence; +public: + typedef PathVectorTime Position; + typedef Sequence::iterator iterator; + typedef Sequence::const_iterator const_iterator; + typedef Sequence::size_type size_type; + typedef Path value_type; + typedef Path &reference; + typedef Path const &const_reference; + typedef Path *pointer; + typedef std::ptrdiff_t difference_type; + + PathVector() {} + PathVector(Path const &p) + : _data(1, p) + {} + template + PathVector(InputIter first, InputIter last) + : _data(first, last) + {} + + /// Check whether the vector contains any paths. + bool empty() const { return _data.empty(); } + /// Get the number of paths in the vector. + size_type size() const { return _data.size(); } + /// Get the total number of curves in the vector. + size_type curveCount() const; + + iterator begin() { return _data.begin(); } + iterator end() { return _data.end(); } + const_iterator begin() const { return _data.begin(); } + const_iterator end() const { return _data.end(); } + Path &operator[](size_type index) { + return _data[index]; + } + Path const &operator[](size_type index) const { + return _data[index]; + } + Path &at(size_type index) { + return _data.at(index); + } + Path const &at(size_type index) const { + return _data.at(index); + } + Path &front() { return _data.front(); } + Path const &front() const { return _data.front(); } + Path &back() { return _data.back(); } + Path const &back() const { return _data.back(); } + /// Append a path at the end. + void push_back(Path const &path) { + _data.push_back(path); + } + /// Remove the last path. + void pop_back() { + _data.pop_back(); + } + iterator insert(iterator pos, Path const &p) { + return _data.insert(pos, p); + } + template + void insert(iterator out, InputIter first, InputIter last) { + _data.insert(out, first, last); + } + /// Remove a path from the vector. + iterator erase(iterator i) { + return _data.erase(i); + } + /// Remove a range of paths from the vector. + iterator erase(iterator first, iterator last) { + return _data.erase(first, last); + } + /// Remove all paths from the vector. + void clear() { _data.clear(); } + /** @brief Change the number of paths. + * If the vector size increases, it is passed with paths that contain only + * a degenerate closing segment at (0,0). */ + void resize(size_type n) { _data.resize(n); } + /** @brief Reverse the direction of paths in the vector. + * @param reverse_paths If this is true, the order of paths is reversed as well; + * otherwise each path is reversed, but their order in the + * PathVector stays the same */ + void reverse(bool reverse_paths = true); + /** @brief Get a new vector with reversed direction of paths. + * @param reverse_paths If this is true, the order of paths is reversed as well; + * otherwise each path is reversed, but their order in the + * PathVector stays the same */ + PathVector reversed(bool reverse_paths = true) const; + + /// Get the range of allowed time values. + Interval timeRange() const { + Interval ret(0, curveCount()); return ret; + } + /** @brief Get the first point in the first path of the vector. + * This method will throw an exception if the vector doesn't contain any paths. */ + Point initialPoint() const { + return _data.front().initialPoint(); + } + /** @brief Get the last point in the last path of the vector. + * This method will throw an exception if the vector doesn't contain any paths. */ + Point finalPoint() const { + return _data.back().finalPoint(); + } + Path &pathAt(Coord t, Coord *rest = NULL); + Path const &pathAt(Coord t, Coord *rest = NULL) const; + Curve const &curveAt(Coord t, Coord *rest = NULL) const; + Coord valueAt(Coord t, Dim2 d) const; + Point pointAt(Coord t) const; + + Path &pathAt(PathVectorTime const &pos) { + return const_cast(static_cast(this)->pathAt(pos)); + } + Path const &pathAt(PathVectorTime const &pos) const { + return at(pos.path_index); + } + Curve const &curveAt(PathVectorTime const &pos) const { + return at(pos.path_index).at(pos.curve_index); + } + Point pointAt(PathVectorTime const &pos) const { + return at(pos.path_index).at(pos.curve_index).pointAt(pos.t); + } + Coord valueAt(PathVectorTime const &pos, Dim2 d) const { + return at(pos.path_index).at(pos.curve_index).valueAt(pos.t, d); + } + + OptRect boundsFast() const; + OptRect boundsExact() const; + + template + BOOST_CONCEPT_REQUIRES(((TransformConcept)), (PathVector &)) + operator*=(T const &t) { + if (empty()) return *this; + for (iterator i = begin(); i != end(); ++i) { + *i *= t; + } + return *this; + } + + bool operator==(PathVector const &other) const { + return boost::range::equal(_data, other._data); + } + + void snapEnds(Coord precision = EPSILON); + + std::vector intersect(PathVector const &other, Coord precision = EPSILON) const; + + /** @brief Determine the winding number at the specified point. + * This is simply the sum of winding numbers for constituent paths. */ + int winding(Point const &p) const; + + boost::optional nearestTime(Point const &p, Coord *dist = NULL) const; + std::vector allNearestTimes(Point const &p, Coord *dist = NULL) const; + + std::vector nodes() const; + +private: + PathVectorTime _factorTime(Coord t) const; + + Sequence _data; +}; + +inline OptRect bounds_fast(PathVector const &pv) { return pv.boundsFast(); } +inline OptRect bounds_exact(PathVector const &pv) { return pv.boundsExact(); } + +std::ostream &operator<<(std::ostream &out, PathVector const &pv); + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_PATHVECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/piecewise.cpp b/src/2geom/piecewise.cpp new file mode 100644 index 0000000..090da8e --- /dev/null +++ b/src/2geom/piecewise.cpp @@ -0,0 +1,266 @@ +/* + * piecewise.cpp - Piecewise function class + * + * Copyright 2007 Michael Sloan + * Copyright 2007 JF Barraud + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include <2geom/piecewise.h> +#include +#include + +namespace Geom { + +Piecewise divide(Piecewise const &a, Piecewise const &b, unsigned k) { + Piecewise pa = partition(a, b.cuts), pb = partition(b, a.cuts); + Piecewise ret = Piecewise(); + assert(pa.size() == pb.size()); + ret.cuts = pa.cuts; + for (unsigned i = 0; i < pa.size(); i++) + ret.push_seg(divide(pa[i], pb[i], k)); + return ret; +} + +Piecewise +divide(Piecewise const &a, Piecewise const &b, double tol, unsigned k, double zero) { + Piecewise pa = partition(a, b.cuts), pb = partition(b, a.cuts); + Piecewise ret = Piecewise(); + assert(pa.size() == pb.size()); + for (unsigned i = 0; i < pa.size(); i++){ + Piecewise divi = divide(pa[i], pb[i], tol, k, zero); + divi.setDomain(Interval(pa.cuts[i],pa.cuts[i+1])); + ret.concat(divi); + } + return ret; +} +Piecewise divide(Piecewise const &a, SBasis const &b, double tol, unsigned k, double zero){ + return divide(a,Piecewise(b),tol,k,zero); +} +Piecewise divide(SBasis const &a, Piecewise const &b, double tol, unsigned k, double zero){ + return divide(Piecewise(a),b,tol,k,zero); +} +Piecewise divide(SBasis const &a, SBasis const &b, double tol, unsigned k, double zero) { + if (b.tailError(0)<2*zero){ + //TODO: have a better look at sgn(b). + double sgn= (b(.5)<0.)?-1.:1; + return Piecewise(Linear(sgn/zero)*a); + } + + if (fabs(b.at0())>zero && fabs(b.at1())>zero ){ + SBasis c,r=a; + //TODO: what is a good relative tol? atm, c=a/b +/- (tol/a)%... + + k+=1; + r.resize(k, Linear(0,0)); + c.resize(k, Linear(0,0)); + + //assert(b.at0()!=0 && b.at1()!=0); + for (unsigned i=0; i(c); + } + + Piecewise c0,c1; + c0 = divide(compose(a,Linear(0.,.5)),compose(b,Linear(0.,.5)),tol,k); + c1 = divide(compose(a,Linear(.5,1.)),compose(b,Linear(.5,1.)),tol,k); + c0.setDomain(Interval(0.,.5)); + c1.setDomain(Interval(.5,1.)); + c0.concat(c1); + return c0; +} + + +//-- compose(pw,SBasis) --------------- +/* + the purpose of the following functions is only to reduce the code in piecewise.h + TODO: use a vector > instead of a map. + */ + +std::map compose_pullback(std::vector const &values, SBasis const &g){ + std::map result; + + std::vector > roots = multi_roots(g, values); + for(unsigned i=0; ivalues[i])) i++; + result[0.]=i; + } + if(result.count(1.)==0){ + unsigned i=0; + while (ivalues[i])) i++; + result[1.]=i; + } + return(result); +} + +int compose_findSegIdx(std::map::iterator const &cut, + std::map::iterator const &next, + std::vector const &levels, + SBasis const &g){ + double t0=(*cut).first; + unsigned idx0=(*cut).second; + double t1=(*next).first; + unsigned idx1=(*next).second; + assert(t0 levels[idx0]) { //g([t0,t1]) is a 'bump' over level idx0, + idx=idx0; + } else { //g([t0,t1]) is contained in level idx0!... + idx = (idx0==levels.size())? idx0-1:idx0; + } + + //move idx back from levels f.cuts + idx+=1; + return idx; +} + + +Piecewise pw_compose_inverse(SBasis const &f, SBasis const &g, unsigned order, double zero){ + Piecewise result; + + assert( f.size()>0 && g.size()>0); + SBasis g01 = g; + bool flip = ( g01.at0() > g01.at1() ); + + //OptInterval g_range = bounds_exact(g); + OptInterval g_range( Interval( g.at0(), g.at1() )); + + g01 -= g_range->min(); + g01 /= g_range->extent(); + if ( flip ){ + g01 *= -1.; + g01 += 1.; + } +#if 1 + assert( std::abs( g01.at0() - 0. ) < zero ); + assert( std::abs( g01.at1() - 1. ) < zero ); + //g[0][0] = 0.; + //g[0][1] = 1.; +#endif + + SBasis foginv = compose_inverse( f, g01, order, zero ); + SBasis err = compose( foginv, g01) - f; + + if ( err.tailError(0) < zero ){ + result = Piecewise (foginv); + }else{ + SBasis g_portion = portion( g01, Interval(0.,.5) ); + SBasis f_portion = portion( f, Interval(0.,.5) ); + result = pw_compose_inverse(f_portion, g_portion, order, zero); + + g_portion = portion( g01, Interval(.5, 1.) ); + f_portion = portion( f, Interval(.5, 1.) ); + Piecewise result_next; + result_next = pw_compose_inverse(f_portion, g_portion, order, zero); + result.concat( result_next ); + } + if (flip) { + result = reverse(result); + } + result.setDomain(*g_range); + return result; +} + + +std::vector roots(Piecewise const &f){ + std::vector result; + for (unsigned i=0; i rts=roots(f.segs[i]); + + for (unsigned r=0; r > multi_roots(Piecewise const &f, std::vector const &values) { + std::vector > result(values.size()); + for (unsigned i=0; i > rts = multi_roots(f.segs[i], values); + for(unsigned j=0; j level_set(Piecewise const &f, Interval const &level, double tol){ + std::vector result; + for (unsigned i=0; i resulti = level_set( f[i], level, 0., 1., tol); + for (unsigned j=0; j level_set(Piecewise const &f, double v, double vtol, double tol){ + Interval level ( v-vtol, v+vtol ); + return level_set( f, level, tol); +} + + +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/piecewise.h b/src/2geom/piecewise.h new file mode 100644 index 0000000..6cc1de4 --- /dev/null +++ b/src/2geom/piecewise.h @@ -0,0 +1,945 @@ +/** @file + * @brief Piecewise function class + *//* + * Copyright 2007 Michael Sloan + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_PIECEWISE_H +#define LIB2GEOM_SEEN_PIECEWISE_H + +#include +#include +#include +#include +#include <2geom/concepts.h> +#include <2geom/math-utils.h> +#include <2geom/sbasis.h> + + +namespace Geom { +/** + * @brief Function defined as discrete pieces. + * + * The Piecewise class manages a sequence of elements of a type as segments and + * the ’cuts’ between them. These cuts are time values which separate the pieces. + * This function representation allows for more interesting functions, as it provides + * a viable output for operations such as inversion, which may require multiple + * SBasis to properly invert the original. + * + * As for technical details, while the actual SBasis segments begin on the ï¬rst + * cut and end on the last, the function is deï¬ned throughout all inputs by ex- + * tending the ï¬rst and last segments. The exact switching between segments is + * arbitrarily such that beginnings (t=0) have preference over endings (t=1). This + * only matters if it is discontinuous at the location. + * \f[ + * f(t) \rightarrow \left\{ + * \begin{array}{cc} + * s_1,& t <= c_2 \\ + * s_2,& c_2 <= t <= c_3\\ + * \ldots \\ + * s_n,& c_n <= t + * \end{array}\right. + * \f] + * + * @ingroup Fragments + */ +template +class Piecewise { + BOOST_CLASS_REQUIRE(T, Geom, FragmentConcept); + + public: + std::vector cuts; + std::vector segs; + //segs[i] stretches from cuts[i] to cuts[i+1]. + + Piecewise() {} + + explicit Piecewise(const T &s) { + push_cut(0.); + push_seg(s); + push_cut(1.); + } + + unsigned input_dim(){return 1;} + + typedef typename T::output_type output_type; + + explicit Piecewise(const output_type & v) { + push_cut(0.); + push_seg(T(v)); + push_cut(1.); + } + + inline void reserve(unsigned i) { segs.reserve(i); cuts.reserve(i + 1); } + + inline T const& operator[](unsigned i) const { return segs[i]; } + inline T& operator[](unsigned i) { return segs[i]; } + inline output_type operator()(double t) const { return valueAt(t); } + inline output_type valueAt(double t) const { + unsigned n = segN(t); + return segs[n](segT(t, n)); + } + inline output_type firstValue() const { + return valueAt(cuts.front()); + } + inline output_type lastValue() const { + return valueAt(cuts.back()); + } + + /** + * The size of the returned vector equals n_derivs+1. + */ + std::vector valueAndDerivatives(double t, unsigned n_derivs) const { + unsigned n = segN(t); + std::vector ret, val = segs[n].valueAndDerivatives(segT(t, n), n_derivs); + double mult = 1; + for(unsigned i = 0; i < val.size(); i++) { + ret.push_back(val[i] * mult); + mult /= cuts[n+1] - cuts[n]; + } + return ret; + } + + //TODO: maybe it is not a good idea to have this? + Piecewise operator()(SBasis f); + Piecewise operator()(Piecewisef); + + inline unsigned size() const { return segs.size(); } + inline bool empty() const { return segs.empty(); } + inline void clear() { + segs.clear(); + cuts.clear(); + } + + /**Convenience/implementation hiding function to add segment/cut pairs. + * Asserts that basic size and order invariants are correct + */ + inline void push(const T &s, double to) { + assert(cuts.size() - segs.size() == 1); + push_seg(s); + push_cut(to); + } + inline void push(T &&s, double to) { + assert(cuts.size() - segs.size() == 1); + push_seg(s); + push_cut(to); + } + //Convenience/implementation hiding function to add cuts. + inline void push_cut(double c) { + ASSERT_INVARIANTS(cuts.empty() || c > cuts.back()); + cuts.push_back(c); + } + //Convenience/implementation hiding function to add segments. + inline void push_seg(const T &s) { segs.push_back(s); } + inline void push_seg(T &&s) { segs.emplace_back(s); } + + /**Returns the segment index which corresponds to a 'global' piecewise time. + * Also takes optional low/high parameters to expedite the search for the segment. + */ + inline unsigned segN(double t, int low = 0, int high = -1) const { + high = (high == -1) ? size() : high; + if(t < cuts[0]) return 0; + if(t >= cuts[size()]) return size() - 1; + while(low < high) { + int mid = (high + low) / 2; //Lets not plan on having huge (> INT_MAX / 2) cut sequences + double mv = cuts[mid]; + if(mv < t) { + if(t < cuts[mid + 1]) return mid; else low = mid + 1; + } else if(t < mv) { + if(cuts[mid - 1] < t) return mid - 1; else high = mid - 1; + } else { + return mid; + } + } + return low; + } + + /**Returns the time within a segment, given the 'global' piecewise time. + * Also takes an optional index parameter which may be used for efficiency or to find the time on a + * segment outside its range. If it is left to its default, -1, it will call segN to find the index. + */ + inline double segT(double t, int i = -1) const { + if(i == -1) i = segN(t); + assert(i >= 0); + return (t - cuts[i]) / (cuts[i+1] - cuts[i]); + } + + inline double mapToDomain(double t, unsigned i) const { + return (1-t)*cuts[i] + t*cuts[i+1]; //same as: t * (cuts[i+1] - cuts[i]) + cuts[i] + } + + //Offsets the piecewise domain + inline void offsetDomain(double o) { + assert(std::isfinite(o)); + if(o != 0) + for(unsigned i = 0; i <= size(); i++) + cuts[i] += o; + } + + //Scales the domain of the function by a value. 0 will result in an empty Piecewise. + inline void scaleDomain(double s) { + assert(s > 0); + if(s == 0) { + cuts.clear(); segs.clear(); + return; + } + for(unsigned i = 0; i <= size(); i++) + cuts[i] *= s; + } + + //Retrieves the domain in interval form + inline Interval domain() const { return Interval(cuts.front(), cuts.back()); } + + //Transforms the domain into another interval + inline void setDomain(Interval dom) { + if(empty()) return; + /* dom can not be empty + if(dom.empty()) { + cuts.clear(); segs.clear(); + return; + }*/ + double cf = cuts.front(); + double o = dom.min() - cf, s = dom.extent() / (cuts.back() - cf); + for(unsigned i = 0; i <= size(); i++) + cuts[i] = (cuts[i] - cf) * s + o; + //fix floating point precision errors. + cuts[0] = dom.min(); + cuts[size()] = dom.max(); + } + + //Concatenates this Piecewise function with another, offsetting time of the other to match the end. + inline void concat(const Piecewise &other) { + if(other.empty()) return; + + if(empty()) { + cuts = other.cuts; segs = other.segs; + return; + } + + segs.insert(segs.end(), other.segs.begin(), other.segs.end()); + double t = cuts.back() - other.cuts.front(); + cuts.reserve(cuts.size() + other.size()); + for(unsigned i = 0; i < other.size(); i++) + push_cut(other.cuts[i + 1] + t); + } + + //Like concat, but ensures continuity. + inline void continuousConcat(const Piecewise &other) { + boost::function_requires >(); + if(other.empty()) return; + + if(empty()) { segs = other.segs; cuts = other.cuts; return; } + + typename T::output_type y = segs.back().at1() - other.segs.front().at0(); + double t = cuts.back() - other.cuts.front(); + reserve(size() + other.size()); + for(unsigned i = 0; i < other.size(); i++) + push(other[i] + y, other.cuts[i + 1] + t); + } + + //returns true if the Piecewise meets some basic invariants. + inline bool invariants() const { + // segs between cuts + if(!(segs.size() + 1 == cuts.size() || (segs.empty() && cuts.empty()))) + return false; + // cuts in order + for(unsigned i = 0; i < segs.size(); i++) + if(cuts[i] >= cuts[i+1]) + return false; + return true; + } + +}; + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +inline typename FragmentConcept::BoundsType bounds_fast(const Piecewise &f) { + boost::function_requires >(); + + if(f.empty()) return typename FragmentConcept::BoundsType(); + typename FragmentConcept::BoundsType ret(bounds_fast(f[0])); + for(unsigned i = 1; i < f.size(); i++) + ret.unionWith(bounds_fast(f[i])); + return ret; +} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +inline typename FragmentConcept::BoundsType bounds_exact(const Piecewise &f) { + boost::function_requires >(); + + if(f.empty()) return typename FragmentConcept::BoundsType(); + typename FragmentConcept::BoundsType ret(bounds_exact(f[0])); + for(unsigned i = 1; i < f.size(); i++) + ret.unionWith(bounds_exact(f[i])); + return ret; +} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +inline typename FragmentConcept::BoundsType bounds_local(const Piecewise &f, const OptInterval &_m) { + boost::function_requires >(); + + if(f.empty() || !_m) return typename FragmentConcept::BoundsType(); + Interval const &m = *_m; + if(m.isSingular()) return typename FragmentConcept::BoundsType(f(m.min())); + + unsigned fi = f.segN(m.min()), ti = f.segN(m.max()); + double ft = f.segT(m.min(), fi), tt = f.segT(m.max(), ti); + + if(fi == ti) return bounds_local(f[fi], Interval(ft, tt)); + + typename FragmentConcept::BoundsType ret(bounds_local(f[fi], Interval(ft, 1.))); + for(unsigned i = fi + 1; i < ti; i++) + ret.unionWith(bounds_exact(f[i])); + if(tt != 0.) ret.unionWith(bounds_local(f[ti], Interval(0., tt))); + + return ret; +} + +/** + * Returns a portion of a piece of a Piecewise, given the piece's index and a to/from time. + * \relates Piecewise + */ +template +T elem_portion(const Piecewise &a, unsigned i, double from, double to) { + assert(i < a.size()); + double rwidth = 1 / (a.cuts[i+1] - a.cuts[i]); + return portion( a[i], (from - a.cuts[i]) * rwidth, (to - a.cuts[i]) * rwidth ); +} + +/**Piecewise partition(const Piecewise &pw, std::vector const &c); + * Further subdivides the Piecewise such that there is a cut at every value in c. + * Precondition: c sorted lower to higher. + * + * //Given Piecewise a and b: + * Piecewise ac = a.partition(b.cuts); + * Piecewise bc = b.partition(a.cuts); + * //ac.cuts should be equivalent to bc.cuts + * + * \relates Piecewise + */ +template +Piecewise partition(const Piecewise &pw, std::vector const &c) { + assert(pw.invariants()); + if(c.empty()) return Piecewise(pw); + + Piecewise ret = Piecewise(); + ret.reserve(c.size() + pw.cuts.size() - 1); + + if(pw.empty()) { + ret.cuts = c; + for(unsigned i = 0; i < c.size() - 1; i++) + ret.push_seg(T()); + return ret; + } + + unsigned si = 0, ci = 0; //Segment index, Cut index + + //if the cuts have something earlier than the Piecewise, add portions of the first segment + while(ci < c.size() && c[ci] < pw.cuts.front()) { + bool isLast = (ci == c.size()-1 || c[ci + 1] >= pw.cuts.front()); + ret.push_cut(c[ci]); + ret.push_seg( elem_portion(pw, 0, c[ci], isLast ? pw.cuts.front() : c[ci + 1]) ); + ci++; + } + + ret.push_cut(pw.cuts[0]); + double prev = pw.cuts[0]; //previous cut + //Loop which handles cuts within the Piecewise domain + //Should have the cuts = segs + 1 invariant + while(si < pw.size() && ci <= c.size()) { + if(ci == c.size() && prev <= pw.cuts[si]) { //cuts exhausted, straight copy the rest + ret.segs.insert(ret.segs.end(), pw.segs.begin() + si, pw.segs.end()); + ret.cuts.insert(ret.cuts.end(), pw.cuts.begin() + si + 1, pw.cuts.end()); + return ret; + }else if(ci == c.size() || c[ci] >= pw.cuts[si + 1]) { //no more cuts within this segment, finalize + if(prev > pw.cuts[si]) { //segment already has cuts, so portion is required + ret.push_seg(portion(pw[si], pw.segT(prev, si), 1.0)); + } else { //plain copy is fine + ret.push_seg(pw[si]); + } + ret.push_cut(pw.cuts[si + 1]); + prev = pw.cuts[si + 1]; + si++; + } else if(c[ci] == pw.cuts[si]){ //coincident + //Already finalized the seg with the code immediately above + ci++; + } else { //plain old subdivision + ret.push(elem_portion(pw, si, prev, c[ci]), c[ci]); + prev = c[ci]; + ci++; + } + } + + //input cuts extend further than this Piecewise, extend the last segment. + while(ci < c.size()) { + if(c[ci] > prev) { + ret.push(elem_portion(pw, pw.size() - 1, prev, c[ci]), c[ci]); + prev = c[ci]; + } + ci++; + } + return ret; +} + +/** + * Returns a Piecewise with a defined domain of [min(from, to), max(from, to)]. + * \relates Piecewise + */ +template +Piecewise portion(const Piecewise &pw, double from, double to) { + if(pw.empty() || from == to) return Piecewise(); + + Piecewise ret; + + double temp = from; + from = std::min(from, to); + to = std::max(temp, to); + + unsigned i = pw.segN(from); + ret.push_cut(from); + if(i == pw.size() - 1 || to <= pw.cuts[i + 1]) { //to/from inhabit the same segment + ret.push(elem_portion(pw, i, from, to), to); + return ret; + } + ret.push_seg(portion( pw[i], pw.segT(from, i), 1.0 )); + i++; + unsigned fi = pw.segN(to, i); + ret.reserve(fi - i + 1); + if (to == pw.cuts[fi]) fi-=1; + + ret.segs.insert(ret.segs.end(), pw.segs.begin() + i, pw.segs.begin() + fi); //copy segs + ret.cuts.insert(ret.cuts.end(), pw.cuts.begin() + i, pw.cuts.begin() + fi + 1); //and their cuts + + ret.push_seg( portion(pw[fi], 0.0, pw.segT(to, fi))); + if(to != ret.cuts.back()) ret.push_cut(to); + ret.invariants(); + return ret; +} + +//TODO: seems like these should be mutating +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise remove_short_cuts(Piecewise const &f, double tol) { + if(f.empty()) return f; + Piecewise ret; + ret.reserve(f.size()); + ret.push_cut(f.cuts[0]); + for(unsigned i=0; i= tol || i==f.size()-1) { + ret.push(f[i], f.cuts[i+1]); + } + } + return ret; +} + +//TODO: seems like these should be mutating +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise remove_short_cuts_extending(Piecewise const &f, double tol) { + if(f.empty()) return f; + Piecewise ret; + ret.reserve(f.size()); + ret.push_cut(f.cuts[0]); + double last = f.cuts[0]; // last cut included + for(unsigned i=0; i= tol) { + ret.push(elem_portion(f, i, last, f.cuts[i+1]), f.cuts[i+1]); + last = f.cuts[i+1]; + } + } + return ret; +} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +std::vector roots(const Piecewise &pw) { + std::vector ret; + for(unsigned i = 0; i < pw.size(); i++) { + std::vector sr = roots(pw[i]); + for (unsigned j = 0; j < sr.size(); j++) ret.push_back(sr[j] * (pw.cuts[i + 1] - pw.cuts[i]) + pw.cuts[i]); + + } + return ret; +} + +//IMPL: OffsetableConcept +/** + * ... + * \return \f$ a + b = \f$ + * \relates Piecewise + */ +template +Piecewise operator+(Piecewise const &a, typename T::output_type b) { + boost::function_requires >(); +//TODO:empty + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(a[i] + b); + return ret; +} +template +Piecewise operator-(Piecewise const &a, typename T::output_type b) { + boost::function_requires >(); +//TODO: empty + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(a[i] - b); + return ret; +} +template +Piecewise& operator+=(Piecewise& a, typename T::output_type b) { + boost::function_requires >(); + + if(a.empty()) { a.push_cut(0.); a.push(T(b), 1.); return a; } + + for(unsigned i = 0; i < a.size();i++) + a[i] += b; + return a; +} +template +Piecewise& operator-=(Piecewise& a, typename T::output_type b) { + boost::function_requires >(); + + if(a.empty()) { a.push_cut(0.); a.push(T(-b), 1.); return a; } + + for(unsigned i = 0;i < a.size();i++) + a[i] -= b; + return a; +} + +//IMPL: ScalableConcept +/** + * ... + * \return \f$ -a = \f$ + * \relates Piecewise + */ +template +Piecewise operator-(Piecewise const &a) { + boost::function_requires >(); + + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(- a[i]); + return ret; +} +/** + * ... + * \return \f$ a * b = \f$ + * \relates Piecewise + */ +template +Piecewise operator*(Piecewise const &a, double b) { + boost::function_requires >(); + + if(a.empty()) return Piecewise(); + + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(a[i] * b); + return ret; +} +/** + * ... + * \return \f$ a * b = \f$ + * \relates Piecewise + */ +template +Piecewise operator*(Piecewise const &a, T b) { + boost::function_requires >(); + + if(a.empty()) return Piecewise(); + + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(a[i] * b); + return ret; +} +/** + * ... + * \return \f$ a / b = \f$ + * \relates Piecewise + */ +template +Piecewise operator/(Piecewise const &a, double b) { + boost::function_requires >(); + + //FIXME: b == 0? + if(a.empty()) return Piecewise(); + + Piecewise ret; + ret.segs.reserve(a.size()); + ret.cuts = a.cuts; + for(unsigned i = 0; i < a.size();i++) + ret.push_seg(a[i] / b); + return ret; +} +template +Piecewise& operator*=(Piecewise& a, double b) { + boost::function_requires >(); + + for(unsigned i = 0; i < a.size();i++) + a[i] *= b; + return a; +} +template +Piecewise& operator/=(Piecewise& a, double b) { + boost::function_requires >(); + + //FIXME: b == 0? + + for(unsigned i = 0; i < a.size();i++) + a[i] /= b; + return a; +} + +//IMPL: AddableConcept +/** + * ... + * \return \f$ a + b = \f$ + * \relates Piecewise + */ +template +Piecewise operator+(Piecewise const &a, Piecewise const &b) { + boost::function_requires >(); + + Piecewise pa = partition(a, b.cuts), pb = partition(b, a.cuts); + Piecewise ret; + assert(pa.size() == pb.size()); + ret.segs.reserve(pa.size()); + ret.cuts = pa.cuts; + for (unsigned i = 0; i < pa.size(); i++) + ret.push_seg(pa[i] + pb[i]); + return ret; +} +/** + * ... + * \return \f$ a - b = \f$ + * \relates Piecewise + */ +template +Piecewise operator-(Piecewise const &a, Piecewise const &b) { + boost::function_requires >(); + + Piecewise pa = partition(a, b.cuts), pb = partition(b, a.cuts); + Piecewise ret = Piecewise(); + assert(pa.size() == pb.size()); + ret.segs.reserve(pa.size()); + ret.cuts = pa.cuts; + for (unsigned i = 0; i < pa.size(); i++) + ret.push_seg(pa[i] - pb[i]); + return ret; +} +template +inline Piecewise& operator+=(Piecewise &a, Piecewise const &b) { + a = a+b; + return a; +} +template +inline Piecewise& operator-=(Piecewise &a, Piecewise const &b) { + a = a-b; + return a; +} + +/** + * ... + * \return \f$ a \cdot b = \f$ + * \relates Piecewise + */ +template +Piecewise operator*(Piecewise const &a, Piecewise const &b) { + //function_requires >(); + //function_requires >(); + + Piecewise pa = partition(a, b.cuts); + Piecewise pb = partition(b, a.cuts); + Piecewise ret = Piecewise(); + assert(pa.size() == pb.size()); + ret.segs.reserve(pa.size()); + ret.cuts = pa.cuts; + for (unsigned i = 0; i < pa.size(); i++) + ret.push_seg(pa[i] * pb[i]); + return ret; +} + +/** + * ... + * \return \f$ a \cdot b \f$ + * \relates Piecewise + */ +template +inline Piecewise& operator*=(Piecewise &a, Piecewise const &b) { + a = a * b; + return a; +} + +Piecewise divide(Piecewise const &a, Piecewise const &b, unsigned k); +//TODO: replace divide(a,b,k) by divide(a,b,tol,k)? +//TODO: atm, relative error is ~(tol/a)%. Find a way to make it independent of a. +//Nota: the result is 'truncated' where b is smaller than 'zero': ~ a/max(b,zero). +Piecewise +divide(Piecewise const &a, Piecewise const &b, double tol, unsigned k, double zero=1.e-3); +Piecewise +divide(SBasis const &a, Piecewise const &b, double tol, unsigned k, double zero=1.e-3); +Piecewise +divide(Piecewise const &a, SBasis const &b, double tol, unsigned k, double zero=1.e-3); +Piecewise +divide(SBasis const &a, SBasis const &b, double tol, unsigned k, double zero=1.e-3); + +//Composition: functions called compose_* are pieces of compose that are factored out in pw.cpp. +std::map compose_pullback(std::vector const &cuts, SBasis const &g); +int compose_findSegIdx(std::map::iterator const &cut, + std::map::iterator const &next, + std::vector const &levels, + SBasis const &g); + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise compose(Piecewise const &f, SBasis const &g){ + /// \todo add concept check + Piecewise result; + if (f.empty()) return result; + if (g.isZero()) return Piecewise(f(0)); + if (f.size()==1){ + double t0 = f.cuts[0], width = f.cuts[1] - t0; + return (Piecewise) compose(f.segs[0],compose(Linear(-t0 / width, (1-t0) / width), g)); + } + + //first check bounds... + Interval bs = *bounds_fast(g); + if (f.cuts.front() > bs.max() || bs.min() > f.cuts.back()){ + int idx = (bs.max() < f.cuts[1]) ? 0 : f.cuts.size()-2; + double t0 = f.cuts[idx], width = f.cuts[idx+1] - t0; + return (Piecewise) compose(f.segs[idx],compose(Linear(-t0 / width, (1-t0) / width), g)); + } + + std::vector levels;//we can forget first and last cuts... + levels.insert(levels.begin(),f.cuts.begin()+1,f.cuts.end()-1); + //TODO: use a std::vector > instead of a map. + std::map cuts_pb = compose_pullback(levels,g); + + //-- Compose each piece of g with the relevant seg of f. + result.cuts.push_back(0.); + std::map::iterator cut=cuts_pb.begin(); + std::map::iterator next=cut; next++; + while(next!=cuts_pb.end()){ + //assert(std::abs(int((*cut).second-(*next).second))<1); + //TODO: find a way to recover from this error? the root finder missed some root; + // the levels/variations of f might be too close/fast... + int idx = compose_findSegIdx(cut,next,levels,g); + double t0=(*cut).first; + double t1=(*next).first; + + if (!are_near(t0,t1,EPSILON*EPSILON)) { // prevent adding cuts that are extremely close together and that may cause trouble with rounding e.g. when reversing the path + SBasis sub_g=compose(g, Linear(t0,t1)); + sub_g=compose(Linear(-f.cuts[idx]/(f.cuts[idx+1]-f.cuts[idx]), + (1-f.cuts[idx])/(f.cuts[idx+1]-f.cuts[idx])),sub_g); + result.push(compose(f[idx],sub_g),t1); + } + + cut++; + next++; + } + return(result); +} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise compose(Piecewise const &f, Piecewise const &g){ +/// \todo add concept check + Piecewise result; + for(unsigned i = 0; i < g.segs.size(); i++){ + Piecewise fgi=compose(f, g.segs[i]); + fgi.setDomain(Interval(g.cuts[i], g.cuts[i+1])); + result.concat(fgi); + } + return result; +} + +/* +Piecewise > compose(D2 const &sb2d, Piecewise > const &pwd2sb){ +/// \todo add concept check + Piecewise > result; + result.push_cut(0.); + for(unsigned i = 0; i < pwd2sb.size(); i++){ + result.push(compose_each(sb2d,pwd2sb[i]),i+1); + } + return result; +}*/ + +/** Compose an SBasis with the inverse of another. + * WARNING: It's up to the user to check that the second SBasis is indeed + * invertible (i.e. strictly increasing or decreasing). + * \return \f$ f \cdot g^{-1} \f$ + * \relates Piecewise + */ +Piecewise pw_compose_inverse(SBasis const &f, SBasis const &g, unsigned order, double zero); + + + +template +Piecewise Piecewise::operator()(SBasis f){return compose((*this),f);} +template +Piecewise Piecewise::operator()(Piecewisef){return compose((*this),f);} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise integral(Piecewise const &a) { + Piecewise result; + result.segs.resize(a.segs.size()); + result.cuts = a.cuts; + typename T::output_type c = a.segs[0].at0(); + for(unsigned i = 0; i < a.segs.size(); i++){ + result.segs[i] = integral(a.segs[i])*(a.cuts[i+1]-a.cuts[i]); + result.segs[i]+= c-result.segs[i].at0(); + c = result.segs[i].at1(); + } + return result; +} + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise derivative(Piecewise const &a) { + Piecewise result; + result.segs.resize(a.segs.size()); + result.cuts = a.cuts; + for(unsigned i = 0; i < a.segs.size(); i++){ + result.segs[i] = derivative(a.segs[i])/(a.cuts[i+1]-a.cuts[i]); + } + return result; +} + +std::vector roots(Piecewise const &f); + +std::vector >multi_roots(Piecewise const &f, std::vector const &values); + +//TODO: implement level_sets directly for pwsb instead of sb (and derive it fo sb). +//It should be faster than the reverse as the algorithm may jump over full cut intervals. +std::vector level_set(Piecewise const &f, Interval const &level, double tol=1e-5); +std::vector level_set(Piecewise const &f, double v, double vtol, double tol=1e-5); +//std::vector level_sets(Piecewise const &f, std::vector const &levels, double tol=1e-5); +//std::vector level_sets(Piecewise const &f, std::vector &v, double vtol, double tol=1e-5); + + +/** + * ... + * \return ... + * \relates Piecewise + */ +template +Piecewise reverse(Piecewise const &f) { + Piecewise ret = Piecewise(); + ret.reserve(f.size()); + double start = f.cuts[0]; + double end = f.cuts.back(); + for (unsigned i = 0; i < f.cuts.size(); i++) { + double x = f.cuts[f.cuts.size() - 1 - i]; + ret.push_cut(end - (x - start)); + } + for (unsigned i = 0; i < f.segs.size(); i++) + ret.push_seg(reverse(f[f.segs.size() - i - 1])); + return ret; +} + +/** + * Interpolates between a and b. + * \return a if t = 0, b if t = 1, or an interpolation between a and b for t in [0,1] + * \relates Piecewise + */ +template +Piecewise lerp(double t, Piecewise const &a, Piecewise b) { + // Make sure both paths have the same number of segments and cuts at the same locations + b.setDomain(a.domain()); + Piecewise pA = partition(a, b.cuts); + Piecewise pB = partition(b, a.cuts); + + return (pA*(1-t) + pB*t); +} + +} +#endif //LIB2GEOM_SEEN_PIECEWISE_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/point.cpp b/src/2geom/point.cpp new file mode 100644 index 0000000..cbe53c4 --- /dev/null +++ b/src/2geom/point.cpp @@ -0,0 +1,274 @@ +/** + * \file + * \brief Cartesian point / 2D vector and related operations + *//* + * Authors: + * Michael G. Sloan + * Nathan Hurst + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2006-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include +#include <2geom/angle.h> +#include <2geom/coord.h> +#include <2geom/point.h> +#include <2geom/transforms.h> + +namespace Geom { + +/** + * @class Point + * @brief Two-dimensional point that doubles as a vector. + * + * Points in 2Geom are represented in Cartesian coordinates, e.g. as a pair of numbers + * that store the X and Y coordinates. Each point is also a vector in \f$\mathbb{R}^2\f$ + * from the origin (point at 0,0) to the stored coordinates, + * and has methods implementing several vector operations (like length()). + * + * @section OpNotePoint Operator note + * + * Most operators are provided by Boost operator helpers, so they are not visible in this class. + * If @a p, @a q, @a r denote points, @a s a floating-point scalar, and @a m a transformation matrix, + * then the following operations are available: + * @code + p += q; p -= q; r = p + q; r = p - q; + p *= s; p /= s; q = p * s; q = s * p; q = p / s; + p *= m; q = p * m; q = m * p; + @endcode + * It is possible to left-multiply a point by a matrix, even though mathematically speaking + * this is undefined. The result is a point identical to that obtained by right-multiplying. + * + * @ingroup Primitives */ + +Point Point::polar(Coord angle) { + Point ret; + Coord remainder = Angle(angle).radians0(); + if (are_near(remainder, 0) || are_near(remainder, 2*M_PI)) { + ret[X] = 1; + ret[Y] = 0; + } else if (are_near(remainder, M_PI/2)) { + ret[X] = 0; + ret[Y] = 1; + } else if (are_near(remainder, M_PI)) { + ret[X] = -1; + ret[Y] = 0; + } else if (are_near(remainder, 3*M_PI/2)) { + ret[X] = 0; + ret[Y] = -1; + } else { + sincos(angle, ret[Y], ret[X]); + } + return ret; +} + +/** @brief Normalize the vector representing the point. + * After this method returns, the length of the vector will be 1 (unless both coordinates are + * zero - the zero point will be returned then). The function tries to handle infinite + * coordinates gracefully. If any of the coordinates are NaN, the function will do nothing. + * @post \f$-\epsilon < \left|this\right| - 1 < \epsilon\f$ + * @see unit_vector(Geom::Point const &) */ +void Point::normalize() { + double len = hypot(_pt[0], _pt[1]); + if(len == 0) return; + if(std::isnan(len)) return; + static double const inf = HUGE_VAL; + if(len != inf) { + *this /= len; + } else { + unsigned n_inf_coords = 0; + /* Delay updating pt in case neither coord is infinite. */ + Point tmp; + for ( unsigned i = 0 ; i < 2 ; ++i ) { + if ( _pt[i] == inf ) { + ++n_inf_coords; + tmp[i] = 1.0; + } else if ( _pt[i] == -inf ) { + ++n_inf_coords; + tmp[i] = -1.0; + } else { + tmp[i] = 0.0; + } + } + switch (n_inf_coords) { + case 0: { + /* Can happen if both coords are near +/-DBL_MAX. */ + *this /= 4.0; + len = hypot(_pt[0], _pt[1]); + assert(len != inf); + *this /= len; + break; + } + case 1: { + *this = tmp; + break; + } + case 2: { + *this = tmp * sqrt(0.5); + break; + } + } + } +} + +/** @brief Compute the first norm (Manhattan distance) of @a p. + * This is equal to the sum of absolutes values of the coordinates. + * @return \f$|p_X| + |p_Y|\f$ + * @relates Point */ +Coord L1(Point const &p) { + Coord d = 0; + for ( int i = 0 ; i < 2 ; i++ ) { + d += fabs(p[i]); + } + return d; +} + +/** @brief Compute the infinity norm (maximum norm) of @a p. + * @return \f$\max(|p_X|, |p_Y|)\f$ + * @relates Point */ +Coord LInfty(Point const &p) { + Coord const a(fabs(p[0])); + Coord const b(fabs(p[1])); + return ( a < b || std::isnan(b) + ? b + : a ); +} + +/** @brief True if the point has both coordinates zero. + * NaNs are treated as not equal to zero. + * @relates Point */ +bool is_zero(Point const &p) { + return ( p[0] == 0 && + p[1] == 0 ); +} + +/** @brief True if the point has a length near 1. The are_near() function is used. + * @relates Point */ +bool is_unit_vector(Point const &p, Coord eps) { + return are_near(L2(p), 1.0, eps); +} +/** @brief Return the angle between the point and the +X axis. + * @return Angle in \f$(-\pi, \pi]\f$. + * @relates Point */ +Coord atan2(Point const &p) { + return std::atan2(p[Y], p[X]); +} + +/** @brief Compute the angle between a and b relative to the origin. + * The computation is done by projecting b onto the basis defined by a, rot90(a). + * @return Angle in \f$(-\pi, \pi]\f$. + * @relates Point */ +Coord angle_between(Point const &a, Point const &b) { + return std::atan2(cross(a,b), dot(a,b)); +} + +/** @brief Create a normalized version of a point. + * This is equivalent to copying the point and calling its normalize() method. + * The returned point will be (0,0) if the argument has both coordinates equal to zero. + * If any coordinate is NaN, this function will do nothing. + * @param a Input point + * @return Point on the unit circle in the same direction from origin as a, or the origin + * if a has both coordinates equal to zero + * @relates Point */ +Point unit_vector(Point const &a) +{ + Point ret(a); + ret.normalize(); + return ret; +} +/** @brief Return the "absolute value" of the point's vector. + * This is defined in terms of the default lexicographical ordering. If the point is "larger" + * that the origin (0, 0), its negation is returned. You can check whether + * the points' vectors have the same direction (e.g. lie + * on the same line passing through the origin) using + * @code abs(a).normalize() == abs(b).normalize() @endcode + * To check with some margin of error, use + * @code are_near(abs(a).normalize(), abs(b).normalize()) @endcode + * Although naively this should take the absolute value of each coordinate, such an operation + * is not very useful. + * @relates Point */ +Point abs(Point const &b) +{ + Point ret; + if (b[Y] < 0.0) { + ret = -b; + } else if (b[Y] == 0.0) { + ret = b[X] < 0.0 ? -b : b; + } else { + ret = b; + } + return ret; +} + +/** @brief Transform the point by the specified matrix. */ +Point &Point::operator*=(Affine const &m) { + double x = _pt[X], y = _pt[Y]; + for(int i = 0; i < 2; i++) { + _pt[i] = x * m[i] + y * m[i + 2] + m[i + 4]; + } + return *this; +} + +/** @brief Snap the angle B - A - dir to multiples of \f$2\pi/n\f$. + * The 'dir' argument must be normalized (have unit length), otherwise the result + * is undefined. + * @return Point with the same distance from A as B, with a snapped angle. + * @post distance(A, B) == distance(A, result) + * @post angle_between(result - A, dir) == \f$2k\pi/n, k \in \mathbb{N}\f$ + * @relates Point */ +Point constrain_angle(Point const &A, Point const &B, unsigned int n, Point const &dir) +{ + // for special cases we could perhaps use explicit testing (which might be faster) + if (n == 0.0) { + return B; + } + Point diff(B - A); + double angle = -angle_between(diff, dir); + double k = round(angle * (double)n / (2.0*M_PI)); + return A + dir * Rotate(k * 2.0 * M_PI / (double)n) * L2(diff); +} + +std::ostream &operator<<(std::ostream &out, const Geom::Point &p) +{ + out << "(" << format_coord_nice(p[X]) << ", " + << format_coord_nice(p[Y]) << ")"; + return out; +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/point.h b/src/2geom/point.h new file mode 100644 index 0000000..30afb63 --- /dev/null +++ b/src/2geom/point.h @@ -0,0 +1,427 @@ +/** @file + * @brief Cartesian point / 2D vector and related operations + *//* + * Authors: + * Michael G. Sloan + * Nathan Hurst + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2006-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_POINT_H +#define LIB2GEOM_SEEN_POINT_H + +#include +#include +#include +#include <2geom/forward.h> +#include <2geom/coord.h> +#include <2geom/int-point.h> +#include <2geom/math-utils.h> +#include <2geom/utils.h> + +namespace Geom { + +class Point + : boost::additive< Point + , boost::totally_ordered< Point + , boost::multiplicative< Point, Coord + , MultipliableNoncommutative< Point, Affine + , MultipliableNoncommutative< Point, Translate + , MultipliableNoncommutative< Point, Rotate + , MultipliableNoncommutative< Point, Scale + , MultipliableNoncommutative< Point, HShear + , MultipliableNoncommutative< Point, VShear + , MultipliableNoncommutative< Point, Zoom + > > > > > > > > > > // base class chaining, see documentation for Boost.Operator +{ + Coord _pt[2]; +public: + typedef Coord D1Value; + typedef Coord &D1Reference; + typedef Coord const &D1ConstReference; + + /// @name Create points + /// @{ + /** Construct a point on the origin. */ + Point() + { _pt[X] = _pt[Y] = 0; } + + /** Construct a point from its coordinates. */ + Point(Coord x, Coord y) { + _pt[X] = x; _pt[Y] = y; + } + /** Construct from integer point. */ + Point(IntPoint const &p) { + _pt[X] = p[X]; + _pt[Y] = p[Y]; + } + /** @brief Construct a point from its polar coordinates. + * The angle is specified in radians, in the mathematical convention (increasing + * counter-clockwise from +X). */ + static Point polar(Coord angle, Coord radius) { + Point ret(polar(angle)); + ret *= radius; + return ret; + } + /** @brief Construct an unit vector from its angle. + * The angle is specified in radians, in the mathematical convention (increasing + * counter-clockwise from +X). */ + static Point polar(Coord angle); + /// @} + + /// @name Access the coordinates of a point + /// @{ + Coord operator[](unsigned i) const { return _pt[i]; } + Coord &operator[](unsigned i) { return _pt[i]; } + + Coord operator[](Dim2 d) const throw() { return _pt[d]; } + Coord &operator[](Dim2 d) throw() { return _pt[d]; } + + Coord x() const throw() { return _pt[X]; } + Coord &x() throw() { return _pt[X]; } + Coord y() const throw() { return _pt[Y]; } + Coord &y() throw() { return _pt[Y]; } + /// @} + + /// @name Vector operations + /// @{ + /** @brief Compute the distance from origin. + * @return Length of the vector from origin to this point */ + Coord length() const { return hypot(_pt[0], _pt[1]); } + void normalize(); + Point normalized() const { + Point ret(*this); + ret.normalize(); + return ret; + } + + /** @brief Return a point like this point but rotated -90 degrees. + * If the y axis grows downwards and the x axis grows to the + * right, then this is 90 degrees counter-clockwise. */ + Point ccw() const { + return Point(_pt[Y], -_pt[X]); + } + + /** @brief Return a point like this point but rotated +90 degrees. + * If the y axis grows downwards and the x axis grows to the + * right, then this is 90 degrees clockwise. */ + Point cw() const { + return Point(-_pt[Y], _pt[X]); + } + /// @} + + /// @name Vector-like arithmetic operations + /// @{ + Point operator-() const { + return Point(-_pt[X], -_pt[Y]); + } + Point &operator+=(Point const &o) { + for ( unsigned i = 0 ; i < 2 ; ++i ) { + _pt[i] += o._pt[i]; + } + return *this; + } + Point &operator-=(Point const &o) { + for ( unsigned i = 0 ; i < 2 ; ++i ) { + _pt[i] -= o._pt[i]; + } + return *this; + } + Point &operator*=(Coord s) { + for ( unsigned i = 0 ; i < 2 ; ++i ) _pt[i] *= s; + return *this; + } + Point &operator/=(Coord s) { + //TODO: s == 0? + for ( unsigned i = 0 ; i < 2 ; ++i ) _pt[i] /= s; + return *this; + } + /// @} + + /// @name Affine transformations + /// @{ + Point &operator*=(Affine const &m); + // implemented in transforms.cpp + Point &operator*=(Translate const &t); + Point &operator*=(Scale const &s); + Point &operator*=(Rotate const &r); + Point &operator*=(HShear const &s); + Point &operator*=(VShear const &s); + Point &operator*=(Zoom const &z); + /// @} + + /// @name Conversion to integer points + /// @{ + /** @brief Round to nearest integer coordinates. */ + IntPoint round() const { + IntPoint ret(::round(_pt[X]), ::round(_pt[Y])); + return ret; + } + /** @brief Round coordinates downwards. */ + IntPoint floor() const { + IntPoint ret(::floor(_pt[X]), ::floor(_pt[Y])); + return ret; + } + /** @brief Round coordinates upwards. */ + IntPoint ceil() const { + IntPoint ret(::ceil(_pt[X]), ::ceil(_pt[Y])); + return ret; + } + /// @} + + /// @name Various utilities + /// @{ + /** @brief Check whether both coordinates are finite. */ + bool isFinite() const { + for ( unsigned i = 0 ; i < 2 ; ++i ) { + if(!std::isfinite(_pt[i])) return false; + } + return true; + } + /** @brief Check whether both coordinates are zero. */ + bool isZero() const { + return _pt[X] == 0 && _pt[Y] == 0; + } + /** @brief Check whether the length of the vector is close to 1. */ + bool isNormalized(Coord eps=EPSILON) const { + return are_near(length(), 1.0, eps); + } + /** @brief Equality operator. + * This tests for exact identity (as opposed to are_near()). Note that due to numerical + * errors, this test might return false even if the points should be identical. */ + bool operator==(const Point &in_pnt) const { + return (_pt[X] == in_pnt[X]) && (_pt[Y] == in_pnt[Y]); + } + /** @brief Lexicographical ordering for points. + * Y coordinate is regarded as more significant. When sorting according to this + * ordering, the points will be sorted according to the Y coordinate, and within + * points with the same Y coordinate according to the X coordinate. */ + bool operator<(const Point &p) const { + return _pt[Y] < p[Y] || (_pt[Y] == p[Y] && _pt[X] < p[X]); + } + /// @} + + /** @brief Lexicographical ordering functor. + * @param d The dimension with higher significance */ + template struct LexLess; + template struct LexGreater; + //template , typename Second = std::less > LexOrder; + /** @brief Lexicographical ordering functor with runtime dimension. */ + struct LexLessRt { + LexLessRt(Dim2 d) : dim(d) {} + inline bool operator()(Point const &a, Point const &b) const; + private: + Dim2 dim; + }; + struct LexGreaterRt { + LexGreaterRt(Dim2 d) : dim(d) {} + inline bool operator()(Point const &a, Point const &b) const; + private: + Dim2 dim; + }; + //template , typename Second = std::less > LexOrder +}; + +/** @brief Output operator for points. + * Prints out the coordinates. + * @relates Point */ +std::ostream &operator<<(std::ostream &out, const Geom::Point &p); + +template<> struct Point::LexLess { + typedef std::less Primary; + typedef std::less Secondary; + typedef std::less XOrder; + typedef std::less YOrder; + bool operator()(Point const &a, Point const &b) const { + return a[X] < b[X] || (a[X] == b[X] && a[Y] < b[Y]); + } +}; +template<> struct Point::LexLess { + typedef std::less Primary; + typedef std::less Secondary; + typedef std::less XOrder; + typedef std::less YOrder; + bool operator()(Point const &a, Point const &b) const { + return a[Y] < b[Y] || (a[Y] == b[Y] && a[X] < b[X]); + } +}; +template<> struct Point::LexGreater { + typedef std::greater Primary; + typedef std::greater Secondary; + typedef std::greater XOrder; + typedef std::greater YOrder; + bool operator()(Point const &a, Point const &b) const { + return a[X] > b[X] || (a[X] == b[X] && a[Y] > b[Y]); + } +}; +template<> struct Point::LexGreater { + typedef std::greater Primary; + typedef std::greater Secondary; + typedef std::greater XOrder; + typedef std::greater YOrder; + bool operator()(Point const &a, Point const &b) const { + return a[Y] > b[Y] || (a[Y] == b[Y] && a[X] > b[X]); + } +}; +inline bool Point::LexLessRt::operator()(Point const &a, Point const &b) const { + return dim ? Point::LexLess()(a, b) : Point::LexLess()(a, b); +} +inline bool Point::LexGreaterRt::operator()(Point const &a, Point const &b) const { + return dim ? Point::LexGreater()(a, b) : Point::LexGreater()(a, b); +} + +/** @brief Compute the second (Euclidean) norm of @a p. + * This corresponds to the length of @a p. The result will not overflow even if + * \f$p_X^2 + p_Y^2\f$ is larger that the maximum value that can be stored + * in a double. + * @return \f$\sqrt{p_X^2 + p_Y^2}\f$ + * @relates Point */ +inline Coord L2(Point const &p) { + return p.length(); +} + +/** @brief Compute the square of the Euclidean norm of @a p. + * Warning: this can overflow where L2 won't. + * @return \f$p_X^2 + p_Y^2\f$ + * @relates Point */ +inline Coord L2sq(Point const &p) { + return p[0]*p[0] + p[1]*p[1]; +} + +/** @brief Returns p * Geom::rotate_degrees(90), but more efficient. + * + * Angle direction in 2Geom: If you use the traditional mathematics convention that y + * increases upwards, then positive angles are anticlockwise as per the mathematics convention. If + * you take the common non-mathematical convention that y increases downwards, then positive angles + * are clockwise, as is common outside of mathematics. + * + * There is no function to rotate by -90 degrees: use -rot90(p) instead. + * @relates Point */ +inline Point rot90(Point const &p) { + return Point(-p[Y], p[X]); +} + +/** @brief Linear interpolation between two points. + * @param t Time value + * @param a First point + * @param b Second point + * @return Point on a line between a and b. The ratio of its distance from a + * and the distance between a and b will be equal to t. + * @relates Point */ +inline Point lerp(Coord t, Point const &a, Point const &b) { + return (1 - t) * a + t * b; +} + +/** @brief Return a point halfway between the specified ones. + * @relates Point */ +inline Point middle_point(Point const &p1, Point const &p2) { + return lerp(0.5, p1, p2); +} + +/** @brief Compute the dot product of a and b. + * Dot product can be interpreted as a measure of how parallel the vectors are. + * For perpendicular vectors, it is zero. For parallel ones, its absolute value is highest, + * and the sign depends on whether they point in the same direction (+) or opposite ones (-). + * @return \f$a \cdot b = a_X b_X + a_Y b_Y\f$. + * @relates Point */ +inline Coord dot(Point const &a, Point const &b) { + return a[X] * b[X] + a[Y] * b[Y]; +} + +/** @brief Compute the 2D cross product. + * This is also known as "perp dot product". It will be zero for parallel vectors, + * and the absolute value will be highest for perpendicular vectors. + * @return \f$a \times b = a_X b_Y - a_Y b_X\f$. + * @relates Point*/ +inline Coord cross(Point const &a, Point const &b) +{ + // equivalent implementation: + // return dot(a, b.ccw()); + return a[X] * b[Y] - a[Y] * b[X]; +} + +/// Compute the (Euclidean) distance between points. +/// @relates Point +inline Coord distance (Point const &a, Point const &b) { + return (a - b).length(); +} + +/// Compute the square of the distance between points. +/// @relates Point +inline Coord distanceSq (Point const &a, Point const &b) { + return L2sq(a - b); +} + +//IMPL: NearConcept +/// Test whether two points are no further apart than some threshold. +/// @relates Point +inline bool are_near(Point const &a, Point const &b, double eps = EPSILON) { + // do not use an unqualified calls to distance before the empty + // specialization of iterator_traits is defined - see end of file + return are_near((a - b).length(), 0, eps); +} + +/// Test whether three points lie approximately on the same line. +/// @relates Point +inline bool are_collinear(Point const& p1, Point const& p2, Point const& p3, + double eps = EPSILON) +{ + return are_near( cross(p3, p2) - cross(p3, p1) + cross(p2, p1), 0, eps); +} + +Point unit_vector(Point const &a); +Coord L1(Point const &p); +Coord LInfty(Point const &p); +bool is_zero(Point const &p); +bool is_unit_vector(Point const &p, Coord eps = EPSILON); +double atan2(Point const &p); +double angle_between(Point const &a, Point const &b); +Point abs(Point const &b); +Point constrain_angle(Point const &A, Point const &B, unsigned int n = 4, Geom::Point const &dir = Geom::Point(1,0)); + +} // end namespace Geom + +// This is required to fix a bug in GCC 4.3.3 (and probably others) that causes the compiler +// to try to instantiate the iterator_traits template and fail. Probably it thinks that Point +// is an iterator and tries to use std::distance instead of Geom::distance. +namespace std { +template <> class iterator_traits {}; +} + +#endif // LIB2GEOM_SEEN_POINT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/polynomial.cpp b/src/2geom/polynomial.cpp new file mode 100644 index 0000000..e853b9a --- /dev/null +++ b/src/2geom/polynomial.cpp @@ -0,0 +1,337 @@ +/** + * \file + * \brief Polynomial in canonical (monomial) basis + *//* + * Authors: + * MenTaLguY + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include <2geom/polynomial.h> +#include <2geom/math-utils.h> +#include + +#ifdef HAVE_GSL +#include +#endif + +namespace Geom { + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +Poly Poly::operator*(const Poly& p) const { + Poly result; + result.resize(degree() + p.degree()+1); + + for(unsigned i = 0; i < size(); i++) { + for(unsigned j = 0; j < p.size(); j++) { + result[i+j] += (*this)[i] * p[j]; + } + } + return result; +} + +/*double Poly::eval(double x) const { + return gsl_poly_eval(&coeff[0], size(), x); + }*/ + +void Poly::normalize() { + while(back() == 0) + pop_back(); +} + +void Poly::monicify() { + normalize(); + + double scale = 1./back(); // unitize + + for(unsigned i = 0; i < size(); i++) { + (*this)[i] *= scale; + } +} + + +#ifdef HAVE_GSL +std::vector > solve(Poly const & pp) { + Poly p(pp); + p.normalize(); + gsl_poly_complex_workspace * w + = gsl_poly_complex_workspace_alloc (p.size()); + + gsl_complex_packed_ptr z = new double[p.degree()*2]; + double* a = new double[p.size()]; + for(unsigned int i = 0; i < p.size(); i++) + a[i] = p[i]; + std::vector > roots; + //roots.resize(p.degree()); + + gsl_poly_complex_solve (a, p.size(), w, z); + delete[]a; + + gsl_poly_complex_workspace_free (w); + + for (unsigned int i = 0; i < p.degree(); i++) { + roots.push_back(std::complex (z[2*i] ,z[2*i+1])); + //printf ("z%d = %+.18f %+.18f\n", i, z[2*i], z[2*i+1]); + } + delete[] z; + return roots; +} + +std::vector solve_reals(Poly const & p) { + std::vector > roots = solve(p); + std::vector real_roots; + + for(unsigned int i = 0; i < roots.size(); i++) { + if(roots[i].imag() == 0) // should be more lenient perhaps + real_roots.push_back(roots[i].real()); + } + return real_roots; +} +#endif + +double polish_root(Poly const & p, double guess, double tol) { + Poly dp = derivative(p); + + double fn = p(guess); + while(fabs(fn) > tol) { + guess -= fn/dp(guess); + fn = p(guess); + } + return guess; +} + +Poly integral(Poly const & p) { + Poly result; + + result.reserve(p.size()+1); + result.push_back(0); // arbitrary const + for(unsigned i = 0; i < p.size(); i++) { + result.push_back(p[i]/(i+1)); + } + return result; + +} + +Poly derivative(Poly const & p) { + Poly result; + + if(p.size() <= 1) + return Poly(0); + result.reserve(p.size()-1); + for(unsigned i = 1; i < p.size(); i++) { + result.push_back(i*p[i]); + } + return result; +} + +Poly compose(Poly const & a, Poly const & b) { + Poly result; + + for(unsigned i = a.size(); i > 0; i--) { + result = Poly(a[i-1]) + result * b; + } + return result; + +} + +/* This version is backwards - dividing taylor terms +Poly divide(Poly const &a, Poly const &b, Poly &r) { + Poly c; + r = a; // remainder + + const unsigned k = a.size(); + r.resize(k, 0); + c.resize(k, 0); + + for(unsigned i = 0; i < k; i++) { + double ci = r[i]/b[0]; + c[i] += ci; + Poly bb = ci*b; + std::cout << ci <<"*" << b << ", r= " << r << std::endl; + r -= bb.shifted(i); + } + + return c; +} +*/ + +Poly divide(Poly const &a, Poly const &b, Poly &r) { + Poly c; + r = a; // remainder + assert(b.size() > 0); + + const unsigned k = a.degree(); + const unsigned l = b.degree(); + c.resize(k, 0.); + + for(unsigned i = k; i >= l; i--) { + //assert(i >= 0); + double ci = r.back()/b.back(); + c[i-l] += ci; + Poly bb = ci*b; + //std::cout << ci <<"*(" << b.shifted(i-l) << ") = " + // << bb.shifted(i-l) << " r= " << r << std::endl; + r -= bb.shifted(i-l); + r.pop_back(); + } + //std::cout << "r= " << r << std::endl; + r.normalize(); + c.normalize(); + + return c; +} + +Poly gcd(Poly const &a, Poly const &b, const double /*tol*/) { + if(a.size() < b.size()) + return gcd(b, a); + if(b.size() <= 0) + return a; + if(b.size() == 1) + return a; + Poly r; + divide(a, b, r); + return gcd(b, r); +} + + + + +std::vector solve_quadratic(Coord a, Coord b, Coord c) +{ + std::vector result; + + if (a == 0) { + // linear equation + if (b == 0) return result; + result.push_back(-c/b); + return result; + } + + Coord delta = b*b - 4*a*c; + + if (delta == 0) { + // one root + result.push_back(-b / (2*a)); + } else if (delta > 0) { + // two roots + Coord delta_sqrt = sqrt(delta); + + // Use different formulas depending on sign of b to preserve + // numerical stability. See e.g.: + // http://people.csail.mit.edu/bkph/articles/Quadratics.pdf + int sign = b >= 0 ? 1 : -1; + Coord t = -0.5 * (b + sign * delta_sqrt); + result.push_back(t / a); + result.push_back(c / t); + } + // no roots otherwise + + std::sort(result.begin(), result.end()); + return result; +} + + +std::vector solve_cubic(Coord a, Coord b, Coord c, Coord d) +{ + // based on: + // http://mathworld.wolfram.com/CubicFormula.html + + if (a == 0) { + return solve_quadratic(b, c, d); + } + if (d == 0) { + // divide by x + std::vector result = solve_quadratic(a, b, c); + result.push_back(0); + std::sort(result.begin(), result.end()); + return result; + } + + std::vector result; + + // 1. divide everything by a to bring to canonical form + b /= a; + c /= a; + d /= a; + + // 2. eliminate x^2 term: x^3 + 3Qx - 2R = 0 + Coord Q = (3*c - b*b) / 9; + Coord R = (-27 * d + b * (9*c - 2*b*b)) / 54; + + // 3. compute polynomial discriminant + Coord D = Q*Q*Q + R*R; + Coord term1 = b/3; + + if (D > 0) { + // only one real root + Coord S = cbrt(R + sqrt(D)); + Coord T = cbrt(R - sqrt(D)); + result.push_back(-b/3 + S + T); + } else if (D == 0) { + // 3 real roots, 2 of which are equal + Coord rroot = cbrt(R); + result.reserve(3); + result.push_back(-term1 + 2*rroot); + result.push_back(-term1 - rroot); + result.push_back(-term1 - rroot); + } else { + // 3 distinct real roots + assert(Q < 0); + Coord theta = acos(R / sqrt(-Q*Q*Q)); + Coord rroot = 2 * sqrt(-Q); + result.reserve(3); + result.push_back(-term1 + rroot * cos(theta / 3)); + result.push_back(-term1 + rroot * cos((theta + 2*M_PI) / 3)); + result.push_back(-term1 + rroot * cos((theta + 4*M_PI) / 3)); + } + + std::sort(result.begin(), result.end()); + return result; +} + + +/*Poly divide_out_root(Poly const & p, double x) { + assert(1); + }*/ + +} //namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/polynomial.h b/src/2geom/polynomial.h new file mode 100644 index 0000000..5ab2aa4 --- /dev/null +++ b/src/2geom/polynomial.h @@ -0,0 +1,264 @@ +/** + * \file + * \brief Polynomial in canonical (monomial) basis + *//* + * Authors: + * MenTaLguY + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_POLY_H +#define LIB2GEOM_SEEN_POLY_H +#include +#include +#include +#include +#include +#include <2geom/coord.h> +#include <2geom/utils.h> + +namespace Geom { + +/** @brief Polynomial in canonical (monomial) basis. + * @ingroup Fragments */ +class Poly : public std::vector{ +public: + // coeff; // sum x^i*coeff[i] + + //unsigned size() const { return coeff.size();} + unsigned degree() const { return size()-1;} + + //double operator[](const int i) const { return (*this)[i];} + //double& operator[](const int i) { return (*this)[i];} + + Poly operator+(const Poly& p) const { + Poly result; + const unsigned out_size = std::max(size(), p.size()); + const unsigned min_size = std::min(size(), p.size()); + //result.reserve(out_size); + + for(unsigned i = 0; i < min_size; i++) { + result.push_back((*this)[i] + p[i]); + } + for(unsigned i = min_size; i < size(); i++) + result.push_back((*this)[i]); + for(unsigned i = min_size; i < p.size(); i++) + result.push_back(p[i]); + assert(result.size() == out_size); + return result; + } + Poly operator-(const Poly& p) const { + Poly result; + const unsigned out_size = std::max(size(), p.size()); + const unsigned min_size = std::min(size(), p.size()); + result.reserve(out_size); + + for(unsigned i = 0; i < min_size; i++) { + result.push_back((*this)[i] - p[i]); + } + for(unsigned i = min_size; i < size(); i++) + result.push_back((*this)[i]); + for(unsigned i = min_size; i < p.size(); i++) + result.push_back(-p[i]); + assert(result.size() == out_size); + return result; + } + Poly operator-=(const Poly& p) { + const unsigned out_size = std::max(size(), p.size()); + const unsigned min_size = std::min(size(), p.size()); + resize(out_size); + + for(unsigned i = 0; i < min_size; i++) { + (*this)[i] -= p[i]; + } + for(unsigned i = min_size; i < out_size; i++) + (*this)[i] = -p[i]; + return *this; + } + Poly operator-(const double k) const { + Poly result; + const unsigned out_size = size(); + result.reserve(out_size); + + for(unsigned i = 0; i < out_size; i++) { + result.push_back((*this)[i]); + } + result[0] -= k; + return result; + } + Poly operator-() const { + Poly result; + result.resize(size()); + + for(unsigned i = 0; i < size(); i++) { + result[i] = -(*this)[i]; + } + return result; + } + Poly operator*(const double p) const { + Poly result; + const unsigned out_size = size(); + result.reserve(out_size); + + for(unsigned i = 0; i < out_size; i++) { + result.push_back((*this)[i]*p); + } + assert(result.size() == out_size); + return result; + } + // equivalent to multiply by x^terms, negative terms are disallowed + Poly shifted(unsigned const terms) const { + Poly result; + size_type const out_size = size() + terms; + result.reserve(out_size); + + result.resize(terms, 0.0); + result.insert(result.end(), this->begin(), this->end()); + + assert(result.size() == out_size); + return result; + } + Poly operator*(const Poly& p) const; + + template + T eval(T x) const { + T r = 0; + for(int k = size()-1; k >= 0; k--) { + r = r*x + T((*this)[k]); + } + return r; + } + + template + T operator()(T t) const { return (T)eval(t);} + + void normalize(); + + void monicify(); + Poly() {} + Poly(const Poly& p) : std::vector(p) {} + Poly(const double a) {push_back(a);} + +public: + template + void val_and_deriv(T x, U &pd) const { + pd[0] = back(); + int nc = size() - 1; + int nd = pd.size() - 1; + for(unsigned j = 1; j < pd.size(); j++) + pd[j] = 0.0; + for(int i = nc -1; i >= 0; i--) { + int nnd = std::min(nd, nc-i); + for(int j = nnd; j >= 1; j--) + pd[j] = pd[j]*x + operator[](i); + pd[0] = pd[0]*x + operator[](i); + } + double cnst = 1; + for(int i = 2; i <= nd; i++) { + cnst *= i; + pd[i] *= cnst; + } + } + + static Poly linear(double ax, double b) { + Poly p; + p.push_back(b); + p.push_back(ax); + return p; + } +}; + +inline Poly operator*(double a, Poly const & b) { return b * a;} + +Poly integral(Poly const & p); +Poly derivative(Poly const & p); +Poly divide_out_root(Poly const & p, double x); +Poly compose(Poly const & a, Poly const & b); +Poly divide(Poly const &a, Poly const &b, Poly &r); +Poly gcd(Poly const &a, Poly const &b, const double tol=1e-10); + +/*** solve(Poly p) + * find all p.degree() roots of p. + * This function can take a long time with suitably crafted polynomials, but in practice it should be fast. Should we provide special forms for degree() <= 4? + */ +std::vector > solve(const Poly & p); + +#ifdef HAVE_GSL +/*** solve_reals(Poly p) + * find all real solutions to Poly p. + * currently we just use solve and pick out the suitably real looking values, there may be a better algorithm. + */ +std::vector solve_reals(const Poly & p); +#endif +double polish_root(Poly const & p, double guess, double tol); + + +/** @brief Analytically solve quadratic equation. + * The equation is given in the standard form: ax^2 + bx + c = 0. + * Only real roots are returned. */ +std::vector solve_quadratic(Coord a, Coord b, Coord c); + +/** @brief Analytically solve cubic equation. + * The equation is given in the standard form: ax^3 + bx^2 + cx + d = 0. + * Only real roots are returned. */ +std::vector solve_cubic(Coord a, Coord b, Coord c, Coord d); + + +inline std::ostream &operator<< (std::ostream &out_file, const Poly &in_poly) { + if(in_poly.size() == 0) + out_file << "0"; + else { + for(int i = (int)in_poly.size()-1; i >= 0; --i) { + if(i == 1) { + out_file << "" << in_poly[i] << "*x"; + out_file << " + "; + } else if(i) { + out_file << "" << in_poly[i] << "*x^" << i; + out_file << " + "; + } else + out_file << in_poly[i]; + + } + } + return out_file; +} + +} // namespace Geom + +#endif //LIB2GEOM_SEEN_POLY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/ray.h b/src/2geom/ray.h new file mode 100644 index 0000000..4e60fd8 --- /dev/null +++ b/src/2geom/ray.h @@ -0,0 +1,192 @@ +/** + * \file + * \brief Infinite straight ray + *//* + * Copyright 2008 Marco Cecchetti + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_RAY_H +#define LIB2GEOM_SEEN_RAY_H + +#include +#include <2geom/point.h> +#include <2geom/bezier-curve.h> // for LineSegment +#include <2geom/exception.h> +#include <2geom/math-utils.h> +#include <2geom/transforms.h> +#include <2geom/angle.h> + +namespace Geom +{ + +/** + * @brief Straight ray from a specific point to infinity. + * + * Rays are "half-lines" - they begin at some specific point and extend in a straight line + * to infinity. + * + * @ingroup Primitives + */ +class Ray { +private: + Point _origin; + Point _vector; + +public: + Ray() : _origin(0,0), _vector(1,0) {} + Ray(Point const& origin, Coord angle) + : _origin(origin) + { + sincos(angle, _vector[Y], _vector[X]); + } + Ray(Point const& A, Point const& B) { + setPoints(A, B); + } + Point origin() const { return _origin; } + Point vector() const { return _vector; } + Point versor() const { return _vector.normalized(); } + void setOrigin(Point const &o) { _origin = o; } + void setVector(Point const& v) { _vector = v; } + Coord angle() const { return std::atan2(_vector[Y], _vector[X]); } + void setAngle(Coord a) { sincos(a, _vector[Y], _vector[X]); } + void setPoints(Point const &a, Point const &b) { + _origin = a; + _vector = b - a; + if (are_near(_vector, Point(0,0)) ) + _vector = Point(0,0); + else + _vector.normalize(); + } + bool isDegenerate() const { + return ( _vector[X] == 0 && _vector[Y] == 0 ); + } + Point pointAt(Coord t) const { + return _origin + _vector * t; + } + Coord valueAt(Coord t, Dim2 d) const { + return _origin[d] + _vector[d] * t; + } + std::vector roots(Coord v, Dim2 d) const { + std::vector result; + if ( _vector[d] != 0 ) { + double t = (v - _origin[d]) / _vector[d]; + if (t >= 0) result.push_back(t); + } else if (_vector[(d+1)%2] == v) { + THROW_INFINITESOLUTIONS(); + } + return result; + } + Coord nearestTime(Point const& point) const { + if ( isDegenerate() ) return 0; + double t = dot(point - _origin, _vector); + if (t < 0) t = 0; + return t; + } + Ray reverse() const { + Ray result; + result.setOrigin(_origin); + result.setVector(-_vector); + return result; + } + Curve *portion(Coord f, Coord t) const { + return new LineSegment(pointAt(f), pointAt(t)); + } + LineSegment segment(Coord f, Coord t) const { + return LineSegment(pointAt(f), pointAt(t)); + } + Ray transformed(Affine const& m) const { + return Ray(_origin * m, (_origin + _vector) * m); + } +}; // end class Ray + +inline +double distance(Point const& _point, Ray const& _ray) { + double t = _ray.nearestTime(_point); + return ::Geom::distance(_point, _ray.pointAt(t)); +} + +inline +bool are_near(Point const& _point, Ray const& _ray, double eps = EPSILON) { + return are_near(distance(_point, _ray), 0, eps); +} + +inline +bool are_same(Ray const& r1, Ray const& r2, double eps = EPSILON) { + return are_near(r1.vector(), r2.vector(), eps) + && are_near(r1.origin(), r2.origin(), eps); +} + +// evaluate the angle between r1 and r2 rotating r1 in cw or ccw direction on r2 +// the returned value is an angle in the interval [0, 2PI[ +inline +double angle_between(Ray const& r1, Ray const& r2, bool cw = true) { + double angle = angle_between(r1.vector(), r2.vector()); + if (angle < 0) angle += 2*M_PI; + if (!cw) angle = 2*M_PI - angle; + return angle; +} + +/** + * @brief Returns the angle bisector for the two given rays. + * + * @a r1 is rotated half the way to @a r2 in either clockwise or counter-clockwise direction. + * + * @pre Both passed rays must have the same origin. + * + * @remarks If the versors of both given rays point in the same direction, the direction of the + * angle bisector ray depends on the third parameter: + * - If @a cw is set to @c true, the returned ray will equal the passed rays @a r1 and @a r2. + * - If @a cw is set to @c false, the returned ray will go in the opposite direction. + * + * @throws RangeError if the given rays do not have the same origins + */ +inline +Ray make_angle_bisector_ray(Ray const& r1, Ray const& r2, bool cw = true) +{ + if ( !are_near(r1.origin(), r2.origin()) ) + { + THROW_RANGEERROR("passed rays do not have the same origin"); + } + + Ray bisector(r1.origin(), r1.origin() + r1.vector() * Rotate(angle_between(r1, r2) / 2.0)); + + return (cw ? bisector : bisector.reverse()); +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_RAY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/rect.cpp b/src/2geom/rect.cpp new file mode 100644 index 0000000..5a821e9 --- /dev/null +++ b/src/2geom/rect.cpp @@ -0,0 +1,187 @@ +/* Axis-aligned rectangle + * + * Authors: + * Michael Sloan + * Krzysztof KosiÅ„ski + * Copyright 2007-2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include <2geom/rect.h> +#include <2geom/transforms.h> + +namespace Geom { + +Point align_factors(Align g) { + Point p; + switch (g) { + case ALIGN_XMIN_YMIN: + p[X] = 0.0; + p[Y] = 0.0; + break; + case ALIGN_XMID_YMIN: + p[X] = 0.5; + p[Y] = 0.0; + break; + case ALIGN_XMAX_YMIN: + p[X] = 1.0; + p[Y] = 0.0; + break; + case ALIGN_XMIN_YMID: + p[X] = 0.0; + p[Y] = 0.5; + break; + case ALIGN_XMID_YMID: + p[X] = 0.5; + p[Y] = 0.5; + break; + case ALIGN_XMAX_YMID: + p[X] = 1.0; + p[Y] = 0.5; + break; + case ALIGN_XMIN_YMAX: + p[X] = 0.0; + p[Y] = 1.0; + break; + case ALIGN_XMID_YMAX: + p[X] = 0.5; + p[Y] = 1.0; + break; + case ALIGN_XMAX_YMAX: + p[X] = 1.0; + p[Y] = 1.0; + break; + default: + break; + } + return p; +} + + +/** @brief Transform the rectangle by an affine. + * The result of the transformation might not be axis-aligned. The return value + * of this operation will be the smallest axis-aligned rectangle containing + * all points of the true result. */ +Rect &Rect::operator*=(Affine const &m) { + Point pts[4]; + for (unsigned i=0; i<4; ++i) pts[i] = corner(i) * m; + Coord minx = std::min(std::min(pts[0][X], pts[1][X]), std::min(pts[2][X], pts[3][X])); + Coord miny = std::min(std::min(pts[0][Y], pts[1][Y]), std::min(pts[2][Y], pts[3][Y])); + Coord maxx = std::max(std::max(pts[0][X], pts[1][X]), std::max(pts[2][X], pts[3][X])); + Coord maxy = std::max(std::max(pts[0][Y], pts[1][Y]), std::max(pts[2][Y], pts[3][Y])); + f[X].setMin(minx); f[X].setMax(maxx); + f[Y].setMin(miny); f[Y].setMax(maxy); + return *this; +} + +Affine Rect::transformTo(Rect const &viewport, Aspect const &aspect) const +{ + // 1. translate viewbox to origin + Geom::Affine total = Translate(-min()); + + // 2. compute scale + Geom::Point vdims = viewport.dimensions(); + Geom::Point dims = dimensions(); + Geom::Scale scale(vdims[X] / dims[X], vdims[Y] / dims[Y]); + + if (aspect.align == ALIGN_NONE) { + // apply non-uniform scale + total *= scale * Translate(viewport.min()); + } else { + double uscale = 0; + if (aspect.expansion == EXPANSION_MEET) { + uscale = std::min(scale[X], scale[Y]); + } else { + uscale = std::max(scale[X], scale[Y]); + } + scale = Scale(uscale); + + // compute offset for align + Geom::Point offset = vdims - dims * scale; + offset *= Scale(align_factors(aspect.align)); + total *= scale * Translate(viewport.min() + offset); + } + + return total; +} + +Coord distanceSq(Point const &p, Rect const &rect) +{ + double dx = 0, dy = 0; + if ( p[X] < rect.left() ) { + dx = p[X] - rect.left(); + } else if ( p[X] > rect.right() ) { + dx = rect.right() - p[X]; + } + if (p[Y] < rect.top() ) { + dy = rect.top() - p[Y]; + } else if ( p[Y] > rect.bottom() ) { + dy = p[Y] - rect.bottom(); + } + return dx*dx+dy*dy; +} + +/** @brief Returns the smallest distance between p and rect. + * @relates Rect */ +Coord distance(Point const &p, Rect const &rect) +{ + // copy of distanceSq, because we need to use hypot() + double dx = 0, dy = 0; + if ( p[X] < rect.left() ) { + dx = p[X] - rect.left(); + } else if ( p[X] > rect.right() ) { + dx = rect.right() - p[X]; + } + if (p[Y] < rect.top() ) { + dy = rect.top() - p[Y]; + } else if ( p[Y] > rect.bottom() ) { + dy = p[Y] - rect.bottom(); + } + return hypot(dx, dy); +} + +Coord distanceSq(Point const &p, OptRect const &rect) +{ + if (!rect) return std::numeric_limits::max(); + return distanceSq(p, *rect); +} +Coord distance(Point const &p, OptRect const &rect) +{ + if (!rect) return std::numeric_limits::max(); + return distance(p, *rect); +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/rect.h b/src/2geom/rect.h new file mode 100644 index 0000000..d867414 --- /dev/null +++ b/src/2geom/rect.h @@ -0,0 +1,264 @@ +/** + * \file + * \brief Axis-aligned rectangle + *//* + * Authors: + * Michael Sloan + * Krzysztof KosiÅ„ski + * Copyright 2007-2011 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, output to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + * Authors of original rect class: + * Lauris Kaplinski + * Nathan Hurst + * bulia byak + * MenTaLguY + */ + +#ifndef LIB2GEOM_SEEN_RECT_H +#define LIB2GEOM_SEEN_RECT_H + +#include +#include <2geom/affine.h> +#include <2geom/interval.h> +#include <2geom/int-rect.h> + +namespace Geom { + +/** Values for the parameter of preserveAspectRatio. + * See: http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute */ +enum Align { + ALIGN_NONE, + ALIGN_XMIN_YMIN, + ALIGN_XMID_YMIN, + ALIGN_XMAX_YMIN, + ALIGN_XMIN_YMID, + ALIGN_XMID_YMID, + ALIGN_XMAX_YMID, + ALIGN_XMIN_YMAX, + ALIGN_XMID_YMAX, + ALIGN_XMAX_YMAX +}; + +/** Values for the parameter of preserveAspectRatio. + * See: http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute */ +enum Expansion { + EXPANSION_MEET, + EXPANSION_SLICE +}; + +/// Convert an align specification to coordinate fractions. +Point align_factors(Align align); + +/** @brief Structure that specifies placement of within a viewport. + * Use this to create transformations that preserve aspect. */ +struct Aspect { + Align align; + Expansion expansion; + bool deferred; ///< for SVG compatibility + + Aspect(Align a = ALIGN_NONE, Expansion ex = EXPANSION_MEET) + : align(a), expansion(ex), deferred(false) + {} +}; + +/** + * @brief Axis aligned, non-empty rectangle. + * @ingroup Primitives + */ +class Rect + : public GenericRect +{ + typedef GenericRect Base; +public: + /// @name Create rectangles. + /// @{ + /** @brief Create a rectangle that contains only the point at (0,0). */ + Rect() {} + /** @brief Create a rectangle from X and Y intervals. */ + Rect(Interval const &a, Interval const &b) : Base(a,b) {} + /** @brief Create a rectangle from two points. */ + Rect(Point const &a, Point const &b) : Base(a,b) {} + Rect(Coord x0, Coord y0, Coord x1, Coord y1) : Base(x0, y0, x1, y1) {} + Rect(Base const &b) : Base(b) {} + Rect(IntRect const &ir) : Base(ir.min(), ir.max()) {} + /// @} + + /// @name Inspect dimensions. + /// @{ + /** @brief Check whether the rectangle has zero area up to specified tolerance. + * @param eps Maximum value of the area to consider empty + * @return True if rectangle has an area smaller than tolerance, false otherwise */ + bool hasZeroArea(Coord eps = EPSILON) const { return (area() <= eps); } + /// Check whether the rectangle has finite area + bool isFinite() const { return (*this)[X].isFinite() && (*this)[Y].isFinite(); } + /// Calculate the diameter of the smallest circle that would contain the rectangle. + Coord diameter() const { return distance(corner(0), corner(2)); } + /// @} + + /// @name Test other rectangles and points for inclusion. + /// @{ + /** @brief Check whether the interiors of the rectangles have any common points. */ + bool interiorIntersects(Rect const &r) const { + return f[X].interiorIntersects(r[X]) && f[Y].interiorIntersects(r[Y]); + } + /** @brief Check whether the interior includes the given point. */ + bool interiorContains(Point const &p) const { + return f[X].interiorContains(p[X]) && f[Y].interiorContains(p[Y]); + } + /** @brief Check whether the interior includes all points in the given rectangle. + * Interior of the rectangle is the entire rectangle without its borders. */ + bool interiorContains(Rect const &r) const { + return f[X].interiorContains(r[X]) && f[Y].interiorContains(r[Y]); + } + inline bool interiorContains(OptRect const &r) const; + /// @} + + /// @name Rounding to integer coordinates + /// @{ + /** @brief Return the smallest integer rectangle which contains this one. */ + IntRect roundOutwards() const { + IntRect ir(f[X].roundOutwards(), f[Y].roundOutwards()); + return ir; + } + /** @brief Return the largest integer rectangle which is contained in this one. */ + OptIntRect roundInwards() const { + OptIntRect oir(f[X].roundInwards(), f[Y].roundInwards()); + return oir; + } + /// @} + + /// @name SVG viewbox functionality. + /// @{ + /** @brief Transform contents to viewport. + * Computes an affine that transforms the contents of this rectangle + * to the specified viewport. The aspect parameter specifies how to + * to the transformation (whether the aspect ratio of content + * should be kept and where it should be placed in the viewport). */ + Affine transformTo(Rect const &viewport, Aspect const &aspect = Aspect()) const; + /// @} + + /// @name Operators + /// @{ + Rect &operator*=(Affine const &m); + bool operator==(IntRect const &ir) const { + return f[X] == ir[X] && f[Y] == ir[Y]; + } + bool operator==(Rect const &other) const { + return Base::operator==(other); + } + /// @} +}; + +/** + * @brief Axis-aligned rectangle that can be empty. + * @ingroup Primitives + */ +class OptRect + : public GenericOptRect +{ + typedef GenericOptRect Base; +public: + OptRect() : Base() {} + OptRect(Rect const &a) : Base(a) {} + OptRect(Point const &a, Point const &b) : Base(a, b) {} + OptRect(Coord x0, Coord y0, Coord x1, Coord y1) : Base(x0, y0, x1, y1) {} + OptRect(OptInterval const &x_int, OptInterval const &y_int) : Base(x_int, y_int) {} + OptRect(Base const &b) : Base(b) {} + + OptRect(IntRect const &r) : Base(Rect(r)) {} + OptRect(OptIntRect const &r) : Base() { + if (r) *this = Rect(*r); + } + + Affine transformTo(Rect const &viewport, Aspect const &aspect = Aspect()) { + Affine ret = Affine::identity(); + if (empty()) return ret; + ret = (*this)->transformTo(viewport, aspect); + return ret; + } + + bool operator==(OptRect const &other) const { + return Base::operator==(other); + } + bool operator==(Rect const &other) const { + return Base::operator==(other); + } +}; + +Coord distanceSq(Point const &p, Rect const &rect); +Coord distance(Point const &p, Rect const &rect); +/// Minimum square of distance to rectangle, or infinity if empty. +Coord distanceSq(Point const &p, OptRect const &rect); +/// Minimum distance to rectangle, or infinity if empty. +Coord distance(Point const &p, OptRect const &rect); + +inline bool Rect::interiorContains(OptRect const &r) const { + return !r || interiorContains(static_cast(*r)); +} + +// the functions below do not work when defined generically +inline OptRect operator&(Rect const &a, Rect const &b) { + OptRect ret(a); + ret.intersectWith(b); + return ret; +} +inline OptRect intersect(Rect const &a, Rect const &b) { + return a & b; +} +inline OptRect intersect(OptRect const &a, OptRect const &b) { + return a & b; +} +inline Rect unify(Rect const &a, Rect const &b) { + return a | b; +} +inline OptRect unify(OptRect const &a, OptRect const &b) { + return a | b; +} + +/** @brief Union a list of rectangles + * @deprecated Use OptRect::from_range instead */ +inline Rect union_list(std::vector const &r) { + if(r.empty()) return Rect(Interval(0,0), Interval(0,0)); + Rect ret = r[0]; + for(unsigned i = 1; i < r.size(); i++) + ret.unionWith(r[i]); + return ret; +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_RECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/recursive-bezier-intersection.cpp b/src/2geom/recursive-bezier-intersection.cpp new file mode 100644 index 0000000..e86192f --- /dev/null +++ b/src/2geom/recursive-bezier-intersection.cpp @@ -0,0 +1,476 @@ + + + +#include <2geom/basic-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/exception.h> + + +#include +#include + + +unsigned intersect_steps = 0; + +using std::vector; + +namespace Geom { + +class OldBezier { +public: + std::vector p; + OldBezier() { + } + void split(double t, OldBezier &a, OldBezier &b) const; + Point operator()(double t) const; + + ~OldBezier() {} + + void bounds(double &minax, double &maxax, + double &minay, double &maxay) { + // Compute bounding box for a + minax = p[0][X]; // These are the most likely to be extremal + maxax = p.back()[X]; + if( minax > maxax ) + std::swap(minax, maxax); + for(unsigned i = 1; i < p.size()-1; i++) { + if( p[i][X] < minax ) + minax = p[i][X]; + else if( p[i][X] > maxax ) + maxax = p[i][X]; + } + + minay = p[0][Y]; // These are the most likely to be extremal + maxay = p.back()[Y]; + if( minay > maxay ) + std::swap(minay, maxay); + for(unsigned i = 1; i < p.size()-1; i++) { + if( p[i][Y] < minay ) + minay = p[i][Y]; + else if( p[i][Y] > maxay ) + maxay = p[i][Y]; + } + + } + +}; + +static void +find_intersections_bezier_recursive(std::vector > & xs, + OldBezier a, + OldBezier b); + +void +find_intersections_bezier_recursive( std::vector > &xs, + vector const & A, + vector const & B, + double /*precision*/) { + OldBezier a, b; + a.p = A; + b.p = B; + return find_intersections_bezier_recursive(xs, a,b); +} + + +/* + * split the curve at the midpoint, returning an array with the two parts + * Temporary storage is minimized by using part of the storage for the result + * to hold an intermediate value until it is no longer needed. + */ +void OldBezier::split(double t, OldBezier &left, OldBezier &right) const { + const unsigned sz = p.size(); + //Geom::Point Vtemp[sz][sz]; + std::vector< std::vector< Geom::Point > > Vtemp; + for (size_t i = 0; i < sz; ++i ) + Vtemp[i].reserve(sz); + + /* Copy control points */ + std::copy(p.begin(), p.end(), Vtemp[0].begin()); + + /* Triangle computation */ + for (unsigned i = 1; i < sz; i++) { + for (unsigned j = 0; j < sz - i; j++) { + Vtemp[i][j] = lerp(t, Vtemp[i-1][j], Vtemp[i-1][j+1]); + } + } + + left.p.resize(sz); + right.p.resize(sz); + for (unsigned j = 0; j < sz; j++) + left.p[j] = Vtemp[j][0]; + for (unsigned j = 0; j < sz; j++) + right.p[j] = Vtemp[sz-1-j][j]; +} + +#if 0 +/* + * split the curve at the midpoint, returning an array with the two parts + * Temporary storage is minimized by using part of the storage for the result + * to hold an intermediate value until it is no longer needed. + */ +Point OldBezier::operator()(double t) const { + const unsigned sz = p.size(); + Geom::Point Vtemp[sz][sz]; + + /* Copy control points */ + std::copy(p.begin(), p.end(), Vtemp[0]); + + /* Triangle computation */ + for (unsigned i = 1; i < sz; i++) { + for (unsigned j = 0; j < sz - i; j++) { + Vtemp[i][j] = lerp(t, Vtemp[i-1][j], Vtemp[i-1][j+1]); + } + } + return Vtemp[sz-1][0]; +} +#endif + +// suggested by Sederberg. +Point OldBezier::operator()(double const t) const { + size_t const n = p.size()-1; + Point r; + for(int dim = 0; dim < 2; dim++) { + double const u = 1.0 - t; + double bc = 1; + double tn = 1; + double tmp = p[0][dim]*u; + for(size_t i=1; i= : need boundary case + return !( ( minax > maxbx ) || ( minay > maxby ) + || ( minbx > maxax ) || ( minby > maxay ) ); +} + +/* + * Recursively intersect two curves keeping track of their real parameters + * and depths of intersection. + * The results are returned in a 2-D array of doubles indicating the parameters + * for which intersections are found. The parameters are in the order the + * intersections were found, which is probably not in sorted order. + * When an intersection is found, the parameter value for each of the two + * is stored in the index elements array, and the index is incremented. + * + * If either of the curves has subdivisions left before it is straight + * (depth > 0) + * that curve (possibly both) is (are) subdivided at its (their) midpoint(s). + * the depth(s) is (are) decremented, and the parameter value(s) corresponding + * to the midpoints(s) is (are) computed. + * Then each of the subcurves of one curve is intersected with each of the + * subcurves of the other curve, first by testing the bounding boxes for + * interference. If there is any bounding box interference, the corresponding + * subcurves are recursively intersected. + * + * If neither curve has subdivisions left, the line segments from the first + * to last control point of each segment are intersected. (Actually the + * only the parameter value corresponding to the intersection point is found). + * + * The apriori flatness test is probably more efficient than testing at each + * level of recursion, although a test after three or four levels would + * probably be worthwhile, since many curves become flat faster than their + * asymptotic rate for the first few levels of recursion. + * + * The bounding box test fails much more frequently than it succeeds, providing + * substantial pruning of the search space. + * + * Each (sub)curve is subdivided only once, hence it is not possible that for + * one final line intersection test the subdivision was at one level, while + * for another final line intersection test the subdivision (of the same curve) + * was at another. Since the line segments share endpoints, the intersection + * is robust: a near-tangential intersection will yield zero or two + * intersections. + */ +void recursively_intersect( OldBezier a, double t0, double t1, int deptha, + OldBezier b, double u0, double u1, int depthb, + std::vector > ¶meters) +{ + intersect_steps ++; + //std::cout << deptha << std::endl; + if( deptha > 0 ) + { + OldBezier A[2]; + a.split(0.5, A[0], A[1]); + double tmid = (t0+t1)*0.5; + deptha--; + if( depthb > 0 ) + { + OldBezier B[2]; + b.split(0.5, B[0], B[1]); + double umid = (u0+u1)*0.5; + depthb--; + if( intersect_BB( A[0], B[0] ) ) + recursively_intersect( A[0], t0, tmid, deptha, + B[0], u0, umid, depthb, + parameters ); + if( intersect_BB( A[1], B[0] ) ) + recursively_intersect( A[1], tmid, t1, deptha, + B[0], u0, umid, depthb, + parameters ); + if( intersect_BB( A[0], B[1] ) ) + recursively_intersect( A[0], t0, tmid, deptha, + B[1], umid, u1, depthb, + parameters ); + if( intersect_BB( A[1], B[1] ) ) + recursively_intersect( A[1], tmid, t1, deptha, + B[1], umid, u1, depthb, + parameters ); + } + else + { + if( intersect_BB( A[0], b ) ) + recursively_intersect( A[0], t0, tmid, deptha, + b, u0, u1, depthb, + parameters ); + if( intersect_BB( A[1], b ) ) + recursively_intersect( A[1], tmid, t1, deptha, + b, u0, u1, depthb, + parameters ); + } + } + else + if( depthb > 0 ) + { + OldBezier B[2]; + b.split(0.5, B[0], B[1]); + double umid = (u0 + u1)*0.5; + depthb--; + if( intersect_BB( a, B[0] ) ) + recursively_intersect( a, t0, t1, deptha, + B[0], u0, umid, depthb, + parameters ); + if( intersect_BB( a, B[1] ) ) + recursively_intersect( a, t0, t1, deptha, + B[0], umid, u1, depthb, + parameters ); + } + else // Both segments are fully subdivided; now do line segments + { + double xlk = a.p.back()[X] - a.p[0][X]; + double ylk = a.p.back()[Y] - a.p[0][Y]; + double xnm = b.p.back()[X] - b.p[0][X]; + double ynm = b.p.back()[Y] - b.p[0][Y]; + double xmk = b.p[0][X] - a.p[0][X]; + double ymk = b.p[0][Y] - a.p[0][Y]; + double det = xnm * ylk - ynm * xlk; + if( 1.0 + det == 1.0 ) + return; + else + { + double detinv = 1.0 / det; + double s = ( xnm * ymk - ynm *xmk ) * detinv; + double t = ( xlk * ymk - ylk * xmk ) * detinv; + if( ( s < 0.0 ) || ( s > 1.0 ) || ( t < 0.0 ) || ( t > 1.0 ) ) + return; + parameters.push_back(std::pair(t0 + s * ( t1 - t0 ), + u0 + t * ( u1 - u0 ))); + } + } +} + +inline double log4( double x ) { return log(x)/log(4.); } + +/* + * Wang's theorem is used to estimate the level of subdivision required, + * but only if the bounding boxes interfere at the top level. + * Assuming there is a possible intersection, recursively_intersect is + * used to find all the parameters corresponding to intersection points. + * these are then sorted and returned in an array. + */ + +double Lmax(Point p) { + return std::max(fabs(p[X]), fabs(p[Y])); +} + + +unsigned wangs_theorem(OldBezier /*a*/) { + return 6; // seems a good approximation! + + /* + const double INV_EPS = (1L<<14); // The value of 1.0 / (1L<<14) is enough for most applications + + double la1 = Lmax( ( a.p[2] - a.p[1] ) - (a.p[1] - a.p[0]) ); + double la2 = Lmax( ( a.p[3] - a.p[2] ) - (a.p[2] - a.p[1]) ); + double l0 = std::max(la1, la2); + unsigned ra; + if( l0 * 0.75 * M_SQRT2 + 1.0 == 1.0 ) + ra = 0; + else + ra = (unsigned)ceil( log4( M_SQRT2 * 6.0 / 8.0 * INV_EPS * l0 ) ); + //std::cout << ra << std::endl; + return ra;*/ +} + +struct rparams +{ + OldBezier &A; + OldBezier &B; +}; + +/*static int +intersect_polish_f (const gsl_vector * x, void *params, + gsl_vector * f) +{ + const double x0 = gsl_vector_get (x, 0); + const double x1 = gsl_vector_get (x, 1); + + Geom::Point dx = ((struct rparams *) params)->A(x0) - + ((struct rparams *) params)->B(x1); + + gsl_vector_set (f, 0, dx[0]); + gsl_vector_set (f, 1, dx[1]); + + return GSL_SUCCESS; +}*/ + +/*union dbl_64{ + long long i64; + double d64; +};*/ + +/*static double EpsilonBy(double value, int eps) +{ + dbl_64 s; + s.d64 = value; + s.i64 += eps; + return s.d64; +}*/ + +/* +static void intersect_polish_root (OldBezier &A, double &s, + OldBezier &B, double &t) { + const gsl_multiroot_fsolver_type *T; + gsl_multiroot_fsolver *sol; + + int status; + size_t iter = 0; + + const size_t n = 2; + struct rparams p = {A, B}; + gsl_multiroot_function f = {&intersect_polish_f, n, &p}; + + double x_init[2] = {s, t}; + gsl_vector *x = gsl_vector_alloc (n); + + gsl_vector_set (x, 0, x_init[0]); + gsl_vector_set (x, 1, x_init[1]); + + T = gsl_multiroot_fsolver_hybrids; + sol = gsl_multiroot_fsolver_alloc (T, 2); + gsl_multiroot_fsolver_set (sol, &f, x); + + do + { + iter++; + status = gsl_multiroot_fsolver_iterate (sol); + + if (status) // check if solver is stuck + break; + + status = + gsl_multiroot_test_residual (sol->f, 1e-12); + } + while (status == GSL_CONTINUE && iter < 1000); + + s = gsl_vector_get (sol->x, 0); + t = gsl_vector_get (sol->x, 1); + + gsl_multiroot_fsolver_free (sol); + gsl_vector_free (x); + + // This code does a neighbourhood search for minor improvements. + double best_v = L1(A(s) - B(t)); + //std::cout << "------\n" << best_v << std::endl; + Point best(s,t); + while (true) { + Point trial = best; + double trial_v = best_v; + for(int nsi = -1; nsi < 2; nsi++) { + for(int nti = -1; nti < 2; nti++) { + Point n(EpsilonBy(best[0], nsi), + EpsilonBy(best[1], nti)); + double c = L1(A(n[0]) - B(n[1])); + //std::cout << c << "; "; + if (c < trial_v) { + trial = n; + trial_v = c; + } + } + } + if(trial == best) { + //std::cout << "\n" << s << " -> " << s - best[0] << std::endl; + //std::cout << t << " -> " << t - best[1] << std::endl; + //std::cout << best_v << std::endl; + s = best[0]; + t = best[1]; + return; + } else { + best = trial; + best_v = trial_v; + } + } +}*/ + + +void find_intersections_bezier_recursive( std::vector > &xs, + OldBezier a, OldBezier b) +{ + if( intersect_BB( a, b ) ) + { + recursively_intersect( a, 0., 1., wangs_theorem(a), + b, 0., 1., wangs_theorem(b), + xs); + } + /*for(unsigned i = 0; i < xs.size(); i++) + intersect_polish_root(a, xs[i].first, + b, xs[i].second);*/ + std::sort(xs.begin(), xs.end()); +} + + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-2d.cpp b/src/2geom/sbasis-2d.cpp new file mode 100644 index 0000000..53b09cd --- /dev/null +++ b/src/2geom/sbasis-2d.cpp @@ -0,0 +1,202 @@ +#include <2geom/sbasis-2d.h> +#include <2geom/sbasis-geometric.h> + +namespace Geom{ + +SBasis extract_u(SBasis2d const &a, double u) { + SBasis sb(a.vs, Linear()); + double s = u*(1-u); + + for(unsigned vi = 0; vi < a.vs; vi++) { + double sk = 1; + Linear bo(0,0); + for(unsigned ui = 0; ui < a.us; ui++) { + bo += (extract_u(a.index(ui, vi), u))*sk; + sk *= s; + } + sb[vi] = bo; + } + + return sb; +} + +SBasis extract_v(SBasis2d const &a, double v) { + SBasis sb(a.us, Linear()); + double s = v*(1-v); + + for(unsigned ui = 0; ui < a.us; ui++) { + double sk = 1; + Linear bo(0,0); + for(unsigned vi = 0; vi < a.vs; vi++) { + bo += (extract_v(a.index(ui, vi), v))*sk; + sk *= s; + } + sb[ui] = bo; + } + + return sb; +} + +SBasis compose(Linear2d const &a, D2 const &p) { + D2 omp(-p[X] + 1, -p[Y] + 1); + return multiply(omp[0], omp[1])*a[0] + + multiply(p[0], omp[1])*a[1] + + multiply(omp[0], p[1])*a[2] + + multiply(p[0], p[1])*a[3]; +} + +SBasis +compose(SBasis2d const &fg, D2 const &p) { + SBasis B; + SBasis s[2]; + SBasis ss[2]; + for(unsigned dim = 0; dim < 2; dim++) + s[dim] = p[dim]*(Linear(1) - p[dim]); + ss[1] = Linear(1); + for(unsigned vi = 0; vi < fg.vs; vi++) { + ss[0] = ss[1]; + for(unsigned ui = 0; ui < fg.us; ui++) { + unsigned i = ui + vi*fg.us; + B += ss[0]*compose(fg[i], p); + ss[0] *= s[0]; + } + ss[1] *= s[1]; + } + return B; +} + +D2 +compose_each(D2 const &fg, D2 const &p) { + return D2(compose(fg[X], p), compose(fg[Y], p)); +} + +SBasis2d partial_derivative(SBasis2d const &f, int dim) { + SBasis2d result; + for(unsigned i = 0; i < f.size(); i++) { + result.push_back(Linear2d(0,0,0,0)); + } + result.us = f.us; + result.vs = f.vs; + + for(unsigned i = 0; i < f.us; i++) { + for(unsigned j = 0; j < f.vs; j++) { + Linear2d lin = f.index(i,j); + Linear2d dlin(lin[1+dim]-lin[0], lin[1+2*dim]-lin[dim], lin[3-dim]-lin[2*(1-dim)], lin[3]-lin[2-dim]); + result[i+j*result.us] += dlin; + unsigned di = dim?j:i; + if (di>=1){ + float motpi = dim?-1:1; + Linear2d ds_lin_low( lin[0], -motpi*lin[1], motpi*lin[2], -lin[3] ); + result[(i+dim-1)+(j-dim)*result.us] += di*ds_lin_low; + + Linear2d ds_lin_hi( lin[1+dim]-lin[0], lin[1+2*dim]-lin[dim], lin[3]-lin[2-dim], lin[3-dim]-lin[2-dim] ); + result[i+j*result.us] += di*ds_lin_hi; + } + } + } + return result; +} + +/** + * Finds a path which traces the 0 contour of f, traversing from A to B as a single d2. + * degmax specifies the degree (degree = 2*degmax-1, so a degmax of 2 generates a cubic fit). + * The algorithm is based on dividing out derivatives at each end point and does not use the curvature for fitting. + * It is less accurate than sb2d_cubic_solve, although this may be fixed in the future. + */ +D2 +sb2dsolve(SBasis2d const &f, Geom::Point const &A, Geom::Point const &B, unsigned degmax){ + //g_warning("check f(A)= %f = f(B) = %f =0!", f.apply(A[X],A[Y]), f.apply(B[X],B[Y])); + + SBasis2d dfdu = partial_derivative(f, 0); + SBasis2d dfdv = partial_derivative(f, 1); + Geom::Point dfA(dfdu.apply(A[X],A[Y]),dfdv.apply(A[X],A[Y])); + Geom::Point dfB(dfdu.apply(B[X],B[Y]),dfdv.apply(B[X],B[Y])); + Geom::Point nA = dfA/(dfA[X]*dfA[X]+dfA[Y]*dfA[Y]); + Geom::Point nB = dfB/(dfB[X]*dfB[X]+dfB[Y]*dfB[Y]); + + D2result(SBasis(degmax, Linear()), SBasis(degmax, Linear())); + double fact_k=1; + double sign = 1.; + for(int dim = 0; dim < 2; dim++) + result[dim][0] = Linear(A[dim],B[dim]); + for(unsigned k=1; k. + * The algorithm is based on matching direction and curvature at each end point. + */ +//TODO: handle the case when B is "behind" A for the natural orientation of the level set. +//TODO: more generally, there might be up to 4 solutions. Choose the best one! +D2 +sb2d_cubic_solve(SBasis2d const &f, Geom::Point const &A, Geom::Point const &B){ + D2result;//(Linear(A[X],B[X]),Linear(A[Y],B[Y])); + //g_warning("check 0 = %f = %f!", f.apply(A[X],A[Y]), f.apply(B[X],B[Y])); + + SBasis2d f_u = partial_derivative(f , 0); + SBasis2d f_v = partial_derivative(f , 1); + SBasis2d f_uu = partial_derivative(f_u, 0); + SBasis2d f_uv = partial_derivative(f_v, 0); + SBasis2d f_vv = partial_derivative(f_v, 1); + + Geom::Point dfA(f_u.apply(A[X],A[Y]),f_v.apply(A[X],A[Y])); + Geom::Point dfB(f_u.apply(B[X],B[Y]),f_v.apply(B[X],B[Y])); + + Geom::Point V0 = rot90(dfA); + Geom::Point V1 = rot90(dfB); + + double D2fVV0 = f_uu.apply(A[X],A[Y])*V0[X]*V0[X]+ + 2*f_uv.apply(A[X],A[Y])*V0[X]*V0[Y]+ + f_vv.apply(A[X],A[Y])*V0[Y]*V0[Y]; + double D2fVV1 = f_uu.apply(B[X],B[Y])*V1[X]*V1[X]+ + 2*f_uv.apply(B[X],B[Y])*V1[X]*V1[Y]+ + f_vv.apply(B[X],B[Y])*V1[Y]*V1[Y]; + + std::vector > candidates = cubics_fitting_curvature(A,B,V0,V1,D2fVV0,D2fVV1); + if (candidates.empty()) { + return D2(SBasis(Linear(A[X],B[X])), SBasis(Linear(A[Y],B[Y]))); + } + //TODO: I'm sure std algorithm could do that for me... + double error = -1; + unsigned best = 0; + for (unsigned i=0; ifabs(bounds.min()) ? fabs(bounds.max()) : fabs(bounds.min()) ); + if ( new_error < error || error < 0 ){ + error = new_error; + best = i; + } + } + return candidates[best]; +} + + + + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-2d.h b/src/2geom/sbasis-2d.h new file mode 100644 index 0000000..c7d9b00 --- /dev/null +++ b/src/2geom/sbasis-2d.h @@ -0,0 +1,371 @@ +/** + * \file + * \brief Obsolete 2D SBasis function class + *//* + * Authors: + * Nathan Hurst + * JFBarraud + * + * Copyright 2006-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_2D_H +#define LIB2GEOM_SEEN_SBASIS_2D_H +#include +#include +#include +#include <2geom/d2.h> +#include <2geom/sbasis.h> +#include + +namespace Geom{ + +class Linear2d{ +public: + /* + u 0,1 + v 0,2 + */ + double a[4]; + Linear2d() { + a[0] = 0; + a[1] = 0; + a[2] = 0; + a[3] = 0; + } + Linear2d(double aa) { + for(unsigned i = 0 ; i < 4; i ++) + a[i] = aa; + } + Linear2d(double a00, double a01, double a10, double a11) + { + a[0] = a00; + a[1] = a01; + a[2] = a10; + a[3] = a11; + } + + double operator[](const int i) const { + assert(i >= 0); + assert(i < 4); + return a[i]; + } + double& operator[](const int i) { + assert(i >= 0); + assert(i < 4); + return a[i]; + } + double apply(double u, double v) { + return (a[0]*(1-u)*(1-v) + + a[1]*u*(1-v) + + a[2]*(1-u)*v + + a[3]*u*v); + } +}; + +inline Linear extract_u(Linear2d const &a, double u) { + return Linear(a[0]*(1-u) + + a[1]*u, + a[2]*(1-u) + + a[3]*u); +} +inline Linear extract_v(Linear2d const &a, double v) { + return Linear(a[0]*(1-v) + + a[2]*v, + a[1]*(1-v) + + a[3]*v); +} +inline Linear2d operator-(Linear2d const &a) { + return Linear2d(-a.a[0], -a.a[1], + -a.a[2], -a.a[3]); +} +inline Linear2d operator+(Linear2d const & a, Linear2d const & b) { + return Linear2d(a[0] + b[0], + a[1] + b[1], + a[2] + b[2], + a[3] + b[3]); +} +inline Linear2d operator-(Linear2d const & a, Linear2d const & b) { + return Linear2d(a[0] - b[0], + a[1] - b[1], + a[2] - b[2], + a[3] - b[3]); +} +inline Linear2d& operator+=(Linear2d & a, Linear2d const & b) { + for(unsigned i = 0; i < 4; i++) + a[i] += b[i]; + return a; +} +inline Linear2d& operator-=(Linear2d & a, Linear2d const & b) { + for(unsigned i = 0; i < 4; i++) + a[i] -= b[i]; + return a; +} +inline Linear2d& operator*=(Linear2d & a, double b) { + for(unsigned i = 0; i < 4; i++) + a[i] *= b; + return a; +} + +inline bool operator==(Linear2d const & a, Linear2d const & b) { + for(unsigned i = 0; i < 4; i++) + if(a[i] != b[i]) + return false; + return true; +} +inline bool operator!=(Linear2d const & a, Linear2d const & b) { + for(unsigned i = 0; i < 4; i++) + if(a[i] == b[i]) + return false; + return true; +} +inline Linear2d operator*(double const a, Linear2d const & b) { + return Linear2d(a*b[0], a*b[1], + a*b[2], a*b[3]); +} + +class SBasis2d : public std::vector{ +public: + // vector in u,v + unsigned us, vs; // number of u terms, v terms + SBasis2d() {} + SBasis2d(Linear2d const & bo) + : us(1), vs(1) { + push_back(bo); + } + SBasis2d(SBasis2d const & a) + : std::vector(a), us(a.us), vs(a.vs) {} + + Linear2d& index(unsigned ui, unsigned vi) { + assert(ui < us); + assert(vi < vs); + return (*this)[ui + vi*us]; + } + + Linear2d index(unsigned ui, unsigned vi) const { + if(ui >= us) + return Linear2d(0); + if(vi >= vs) + return Linear2d(0); + return (*this)[ui + vi*us]; + } + + double apply(double u, double v) const { + double s = u*(1-u); + double t = v*(1-v); + Linear2d p; + double tk = 1; +// XXX rewrite as horner + for(unsigned vi = 0; vi < vs; vi++) { + double sk = 1; + for(unsigned ui = 0; ui < us; ui++) { + p += (sk*tk)*index(ui, vi); + sk *= s; + } + tk *= t; + } + return p.apply(u,v); + } + + void clear() { + fill(begin(), end(), Linear2d(0)); + } + + void normalize(); // remove extra zeros + + double tail_error(unsigned tail) const; + + void truncate(unsigned k); +}; + +inline SBasis2d operator-(const SBasis2d& p) { + SBasis2d result; + result.reserve(p.size()); + + for(unsigned i = 0; i < p.size(); i++) { + result.push_back(-p[i]); + } + return result; +} + +inline SBasis2d operator+(const SBasis2d& a, const SBasis2d& b) { + SBasis2d result; + result.us = std::max(a.us, b.us); + result.vs = std::max(a.vs, b.vs); + const unsigned out_size = result.us*result.vs; + result.resize(out_size); + + for(unsigned vi = 0; vi < result.vs; vi++) { + for(unsigned ui = 0; ui < result.us; ui++) { + Linear2d bo; + if(ui < a.us && vi < a.vs) + bo += a.index(ui, vi); + if(ui < b.us && vi < b.vs) + bo += b.index(ui, vi); + result.index(ui, vi) = bo; + } + } + return result; +} + +inline SBasis2d operator-(const SBasis2d& a, const SBasis2d& b) { + SBasis2d result; + result.us = std::max(a.us, b.us); + result.vs = std::max(a.vs, b.vs); + const unsigned out_size = result.us*result.vs; + result.resize(out_size); + + for(unsigned vi = 0; vi < result.vs; vi++) { + for(unsigned ui = 0; ui < result.us; ui++) { + Linear2d bo; + if(ui < a.us && vi < a.vs) + bo += a.index(ui, vi); + if(ui < b.us && vi < b.vs) + bo -= b.index(ui, vi); + result.index(ui, vi) = bo; + } + } + return result; +} + + +inline SBasis2d& operator+=(SBasis2d& a, const Linear2d& b) { + if(a.size() < 1) + a.push_back(b); + else + a[0] += b; + return a; +} + +inline SBasis2d& operator-=(SBasis2d& a, const Linear2d& b) { + if(a.size() < 1) + a.push_back(-b); + else + a[0] -= b; + return a; +} + +inline SBasis2d& operator+=(SBasis2d& a, double b) { + if(a.size() < 1) + a.push_back(Linear2d(b)); + else { + for(unsigned i = 0; i < 4; i++) + a[0] += double(b); + } + return a; +} + +inline SBasis2d& operator-=(SBasis2d& a, double b) { + if(a.size() < 1) + a.push_back(Linear2d(-b)); + else { + a[0] -= b; + } + return a; +} + +inline SBasis2d& operator*=(SBasis2d& a, double b) { + for(unsigned i = 0; i < a.size(); i++) + a[i] *= b; + return a; +} + +inline SBasis2d& operator/=(SBasis2d& a, double b) { + for(unsigned i = 0; i < a.size(); i++) + a[i] *= (1./b); + return a; +} + +SBasis2d operator*(double k, SBasis2d const &a); +SBasis2d operator*(SBasis2d const &a, SBasis2d const &b); + +SBasis2d shift(SBasis2d const &a, int sh); + +SBasis2d shift(Linear2d const &a, int sh); + +SBasis2d truncate(SBasis2d const &a, unsigned terms); + +SBasis2d multiply(SBasis2d const &a, SBasis2d const &b); + +SBasis2d integral(SBasis2d const &c); + +SBasis2d partial_derivative(SBasis2d const &a, int dim); + +SBasis2d sqrt(SBasis2d const &a, int k); + +// return a kth order approx to 1/a) +SBasis2d reciprocal(Linear2d const &a, int k); + +SBasis2d divide(SBasis2d const &a, SBasis2d const &b, int k); + +// a(b(t)) +SBasis2d compose(SBasis2d const &a, SBasis2d const &b); +SBasis2d compose(SBasis2d const &a, SBasis2d const &b, unsigned k); +SBasis2d inverse(SBasis2d const &a, int k); + +// these two should probably be replaced with compose +SBasis extract_u(SBasis2d const &a, double u); +SBasis extract_v(SBasis2d const &a, double v); + +SBasis compose(Linear2d const &a, D2 const &p); + +SBasis compose(SBasis2d const &fg, D2 const &p); + +D2 compose_each(D2 const &fg, D2 const &p); + +inline std::ostream &operator<< (std::ostream &out_file, const Linear2d &bo) { + out_file << "{" << bo[0] << ", " << bo[1] << "}, "; + out_file << "{" << bo[2] << ", " << bo[3] << "}"; + return out_file; +} + +inline std::ostream &operator<< (std::ostream &out_file, const SBasis2d & p) { + for(unsigned i = 0; i < p.size(); i++) { + out_file << p[i] << "s^" << i << " + "; + } + return out_file; +} + +D2 +sb2dsolve(SBasis2d const &f, Geom::Point const &A, Geom::Point const &B, unsigned degmax=2); + +D2 +sb2d_cubic_solve(SBasis2d const &f, Geom::Point const &A, Geom::Point const &B); + +} // end namespace Geom + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-curve.h b/src/2geom/sbasis-curve.h new file mode 100644 index 0000000..cfc4ee9 --- /dev/null +++ b/src/2geom/sbasis-curve.h @@ -0,0 +1,157 @@ +/** + * \file + * \brief Symmetric power basis curve + *//* + * Authors: + * MenTaLguY + * Marco Cecchetti + * Krzysztof KosiÅ„ski + * + * Copyright 2007-2009 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_CURVE_H +#define LIB2GEOM_SEEN_SBASIS_CURVE_H + +#include <2geom/curve.h> +#include <2geom/exception.h> +#include <2geom/nearest-time.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/transforms.h> + +namespace Geom +{ + +/** @brief Symmetric power basis curve. + * + * Symmetric power basis (S-basis for short) polynomials are a versatile numeric + * representation of arbitrary continuous curves. They are the main representation of curves + * in 2Geom. + * + * S-basis is defined for odd degrees and composed of the following polynomials: + * \f{align*}{ + P_k^0(t) &= t^k (1-t)^{k+1} \\ + P_k^1(t) &= t^{k+1} (1-t)^k \f} + * This can be understood more easily with the help of the chart below. Each square + * represents a product of a specific number of \f$t\f$ and \f$(1-t)\f$ terms. Red dots + * are the canonical (monomial) basis, the green dots are the Bezier basis, and the blue + * dots are the S-basis, all of them of degree 7. + * + * @image html sbasis.png "Illustration of the monomial, Bezier and symmetric power bases" + * + * The S-Basis has several important properties: + * - S-basis polynomials are closed under multiplication. + * - Evaluation is fast, using a modified Horner scheme. + * - Degree change is as trivial as in the monomial basis. To elevate, just add extra + * zero coefficients. To reduce the degree, truncate the terms in the highest powers. + * Compare this with Bezier curves, where degree change is complicated. + * - Conversion between S-basis and Bezier basis is numerically stable. + * + * More in-depth information can be found in the following paper: + * J Sanchez-Reyes, "The symmetric analogue of the polynomial power basis". + * ACM Transactions on Graphics, Vol. 16, No. 3, July 1997, pages 319--357. + * http://portal.acm.org/citation.cfm?id=256162 + * + * @ingroup Curves + */ +class SBasisCurve : public Curve { +private: + D2 inner; + +public: + explicit SBasisCurve(D2 const &sb) : inner(sb) {} + explicit SBasisCurve(Curve const &other) : inner(other.toSBasis()) {} + + virtual Curve *duplicate() const { return new SBasisCurve(*this); } + virtual Point initialPoint() const { return inner.at0(); } + virtual Point finalPoint() const { return inner.at1(); } + virtual bool isDegenerate() const { return inner.isConstant(0); } + virtual bool isLineSegment() const { return inner[X].size() == 1; } + virtual Point pointAt(Coord t) const { return inner.valueAt(t); } + virtual std::vector pointAndDerivatives(Coord t, unsigned n) const { + return inner.valueAndDerivatives(t, n); + } + virtual Coord valueAt(Coord t, Dim2 d) const { return inner[d].valueAt(t); } + virtual void setInitial(Point const &v) { + for (unsigned d = 0; d < 2; d++) { inner[d][0][0] = v[d]; } + } + virtual void setFinal(Point const &v) { + for (unsigned d = 0; d < 2; d++) { inner[d][0][1] = v[d]; } + } + virtual Rect boundsFast() const { return *bounds_fast(inner); } + virtual Rect boundsExact() const { return *bounds_exact(inner); } + virtual OptRect boundsLocal(OptInterval const &i, unsigned deg) const { + return bounds_local(inner, i, deg); + } + virtual std::vector roots(Coord v, Dim2 d) const { return Geom::roots(inner[d] - v); } + virtual Coord nearestTime( Point const& p, Coord from = 0, Coord to = 1 ) const { + return nearest_time(p, inner, from, to); + } + virtual std::vector allNearestTimes( Point const& p, Coord from = 0, + Coord to = 1 ) const + { + return all_nearest_times(p, inner, from, to); + } + virtual Coord length(Coord tolerance) const { return ::Geom::length(inner, tolerance); } + virtual Curve *portion(Coord f, Coord t) const { + return new SBasisCurve(Geom::portion(inner, f, t)); + } + + using Curve::operator*=; + virtual void operator*=(Affine const &m) { inner = inner * m; } + + virtual Curve *derivative() const { + return new SBasisCurve(Geom::derivative(inner)); + } + virtual D2 toSBasis() const { return inner; } + virtual bool operator==(Curve const &c) const { + SBasisCurve const *other = dynamic_cast(&c); + if (!other) return false; + return inner == other->inner; + } + virtual bool isNear(Curve const &/*c*/, Coord /*eps*/) const { + THROW_NOTIMPLEMENTED(); + return false; + } + virtual int degreesOfFreedom() const { + return inner[0].degreesOfFreedom() + inner[1].degreesOfFreedom(); + } +}; + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_SBASIS_CURVE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-geometric.cpp b/src/2geom/sbasis-geometric.cpp new file mode 100644 index 0000000..275b096 --- /dev/null +++ b/src/2geom/sbasis-geometric.cpp @@ -0,0 +1,791 @@ +/** Geometric operators on D2 (1D->2D). + * Copyright 2012 JBC Engelen + * Copyright 2007 JF Barraud + * Copyright 2007 N Hurst + * + * The functions defined in this header related to 2d geometric operations such as arc length, + * unit_vector, curvature, and centroid. Most are built on top of unit_vector, which takes an + * arbitrary D2 and returns a D2 with unit length with the same direction. + * + * Todo/think about: + * arclength D2 -> sbasis (giving arclength function) + * does uniform_speed return natural parameterisation? + * integrate sb2d code from normal-bundle + * angle(md<2>) -> sbasis (gives angle from vector - discontinuous?) + * osculating circle center? + * + **/ + +#include <2geom/sbasis-geometric.h> +#include <2geom/sbasis.h> +#include <2geom/sbasis-math.h> +#include <2geom/sbasis-geometric.h> + +//namespace Geom{ +using namespace Geom; +using namespace std; + +//Some utils first. +//TODO: remove this!! +/** + * Return a list of doubles that appear in both a and b to within error tol + * a, b, vector of double + * tol tolerance + */ +static vector +vect_intersect(vector const &a, vector const &b, double tol=0.){ + vector inter; + unsigned i=0,j=0; + while ( ib[j]){ + j+=1; + } + } + return inter; +} + +//------------------------------------------------------------------------------ +static SBasis divide_by_sk(SBasis const &a, int k) { + if ( k>=(int)a.size()){ + //make sure a is 0? + return SBasis(); + } + if(k < 0) return shift(a,-k); + SBasis c; + c.insert(c.begin(), a.begin()+k, a.end()); + return c; +} + +static SBasis divide_by_t0k(SBasis const &a, int k) { + if(k < 0) { + SBasis c = Linear(0,1); + for (int i=2; i<=-k; i++){ + c*=c; + } + c*=a; + return(c); + }else{ + SBasis c = Linear(1,0); + for (int i=2; i<=k; i++){ + c*=c; + } + c*=a; + return(divide_by_sk(c,k)); + } +} + +static SBasis divide_by_t1k(SBasis const &a, int k) { + if(k < 0) { + SBasis c = Linear(1,0); + for (int i=2; i<=-k; i++){ + c*=c; + } + c*=a; + return(c); + }else{ + SBasis c = Linear(0,1); + for (int i=2; i<=k; i++){ + c*=c; + } + c*=a; + return(divide_by_sk(c,k)); + } +} + +static D2 RescaleForNonVanishingEnds(D2 const &MM, double ZERO=1.e-4){ + D2 M = MM; + //TODO: divide by all the s at once!!! + while ((M[0].size()>1||M[1].size()>1) && + fabs(M[0].at0())1||M[1].size()>1) && + fabs(M[0].at0())1||M[1].size()>1) && + fabs(M[0].at1()) RescaleForNonVanishing(D2 const &MM, double ZERO=1.e-4){ + std::vector levels; + levels.push_back(-ZERO); + levels.push_back(ZERO); + //std::vector > mr = multi_roots(MM, levels); + }*/ + + +//================================================================= +//TODO: what's this for?!?! +Piecewise > +Geom::cutAtRoots(Piecewise > const &M, double ZERO){ + vector rts; + for (unsigned i=0; i seg_rts = roots((M.segs[i])[0]); + seg_rts = vect_intersect(seg_rts, roots((M.segs[i])[1]), ZERO); + Linear mapToDom = Linear(M.cuts[i],M.cuts[i+1]); + for (unsigned r=0; r +Geom::atan2(Piecewise > const &vect, double tol, unsigned order){ + Piecewise result; + Piecewise > v = cutAtRoots(vect,tol); + result.cuts.push_back(v.cuts.front()); + for (unsigned i=0; i vi = RescaleForNonVanishingEnds(v.segs[i]); + SBasis x=vi[0], y=vi[1]; + Piecewise angle; + angle = divide (x*derivative(y)-y*derivative(x), x*x+y*y, tol, order); + + //TODO: I don't understand this - sign. + angle = integral(-angle); + Point vi0 = vi.at0(); + angle += -std::atan2(vi0[1],vi0[0]) - angle[0].at0(); + //TODO: deal with 2*pi jumps form one seg to the other... + //TODO: not exact at t=1 because of the integral. + //TODO: force continuity? + + angle.setDomain(Interval(v.cuts[i],v.cuts[i+1])); + result.concat(angle); + } + return result; +} +/** Return a function which gives the angle of vect at each point. + \param vect a piecewise parameteric curve. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + \relates Piecewise, D2 +*/ +Piecewise +Geom::atan2(D2 const &vect, double tol, unsigned order){ + return atan2(Piecewise >(vect),tol,order); +} + +/** tan2 is the pseudo-inverse of atan2. It takes an angle and returns a unit_vector that points in the direction of angle. + \param angle a piecewise function of angle wrt t. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + \relates D2, SBasis +*/ +D2 > +Geom::tan2(SBasis const &angle, double tol, unsigned order){ + return tan2(Piecewise(angle), tol, order); +} + +/** tan2 is the pseudo-inverse of atan2. It takes an angle and returns a unit_vector that points in the direction of angle. + \param angle a piecewise function of angle wrt t. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + \relates Piecewise, D2 +*/ +D2 > +Geom::tan2(Piecewise const &angle, double tol, unsigned order){ + return D2 >(cos(angle, tol, order), sin(angle, tol, order)); +} + +/** Return a Piecewise > which points in the same direction as V_in, but has unit_length. + \param V_in the original path. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + +unitVector(x,y) is computed as (b,-a) where a and b are solutions of: + ax+by=0 (eqn1) and a^2+b^2=1 (eqn2) + + \relates Piecewise, D2 +*/ +Piecewise > +Geom::unitVector(D2 const &V_in, double tol, unsigned order){ + //TODO: Handle vanishing vectors... + // -This approach is numerically bad. Find a stable way to rescale V_in to have non vanishing ends. + // -This done, unitVector will have jumps at zeros: fill the gaps with arcs of circles. + D2 V = RescaleForNonVanishingEnds(V_in); + + if (V[0].isZero(tol) && V[1].isZero(tol)) + return Piecewise >(D2(Linear(1),SBasis())); + SBasis x = V[0], y = V[1]; + SBasis r_eqn1, r_eqn2; + + Point v0 = unit_vector(V.at0()); + Point v1 = unit_vector(V.at1()); + SBasis a = SBasis(order+1, Linear(0.)); + a[0] = Linear(-v0[1],-v1[1]); + SBasis b = SBasis(order+1, Linear(0.)); + b[0] = Linear( v0[0], v1[0]); + + r_eqn1 = -(a*x+b*y); + r_eqn2 = Linear(1.)-(a*a+b*b); + + for (unsigned k=1; k<=order; k++){ + double r0 = (k unitV; + unitV[0] = b; + unitV[1] = -a; + + //is it good? + double rel_tol = std::max(1.,std::max(V_in[0].tailError(0),V_in[1].tailError(0)))*tol; + if (r_eqn1.tailError(order)>rel_tol || r_eqn2.tailError(order)>tol){ + //if not: subdivide and concat results. + Piecewise > unitV0, unitV1; + unitV0 = unitVector(compose(V,Linear(0,.5)),tol,order); + unitV1 = unitVector(compose(V,Linear(.5,1)),tol,order); + unitV0.setDomain(Interval(0.,.5)); + unitV1.setDomain(Interval(.5,1.)); + unitV0.concat(unitV1); + return(unitV0); + }else{ + //if yes: return it as pw. + Piecewise > result; + result=(Piecewise >)unitV; + return result; + } +} + +/** Return a Piecewise > which points in the same direction as V_in, but has unit_length. + \param V_in the original path. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + +unitVector(x,y) is computed as (b,-a) where a and b are solutions of: + ax+by=0 (eqn1) and a^2+b^2=1 (eqn2) + + \relates Piecewise +*/ +Piecewise > +Geom::unitVector(Piecewise > const &V, double tol, unsigned order){ + Piecewise > result; + Piecewise > VV = cutAtRoots(V); + result.cuts.push_back(VV.cuts.front()); + for (unsigned i=0; i > unit_seg; + unit_seg = unitVector(VV.segs[i],tol, order); + unit_seg.setDomain(Interval(VV.cuts[i],VV.cuts[i+1])); + result.concat(unit_seg); + } + return result; +} + +/** returns a function giving the arclength at each point in M. + \param M the Element. + \param tol the maximum error allowed. + \relates Piecewise +*/ +Piecewise +Geom::arcLengthSb(Piecewise > const &M, double tol){ + Piecewise > dM = derivative(M); + Piecewise dMlength = sqrt(dot(dM,dM),tol,3); + Piecewise length = integral(dMlength); + length-=length.segs.front().at0(); + return length; +} + +/** returns a function giving the arclength at each point in M. + \param M the Element. + \param tol the maximum error allowed. + \relates Piecewise, D2 +*/ +Piecewise +Geom::arcLengthSb(D2 const &M, double tol){ + return arcLengthSb(Piecewise >(M), tol); +} + +#if 0 +double +Geom::length(D2 const &M, + double tol){ + Piecewise length = arcLengthSb(M, tol); + return length.segs.back().at1(); +} +double +Geom::length(Piecewise > const &M, + double tol){ + Piecewise length = arcLengthSb(M, tol); + return length.segs.back().at1(); +} +#endif + +/** returns a function giving the curvature at each point in M. + \param M the Element. + \param tol the maximum error allowed. + \relates Piecewise, D2 + \todo claimed incomplete. Check. +*/ +Piecewise +Geom::curvature(D2 const &M, double tol) { + D2 dM=derivative(M); + Piecewise > unitv = unitVector(dM,tol); + Piecewise dMlength = dot(Piecewise >(dM),unitv); + Piecewise k = cross(derivative(unitv),unitv); + k = divide(k,dMlength,tol,3); + return(k); +} + +/** returns a function giving the curvature at each point in M. + \param M the Element. + \param tol the maximum error allowed. + \relates Piecewise + \todo claimed incomplete. Check. +*/ +Piecewise +Geom::curvature(Piecewise > const &V, double tol){ + Piecewise result; + Piecewise > VV = cutAtRoots(V); + result.cuts.push_back(VV.cuts.front()); + for (unsigned i=0; i curv_seg; + curv_seg = curvature(VV.segs[i],tol); + curv_seg.setDomain(Interval(VV.cuts[i],VV.cuts[i+1])); + result.concat(curv_seg); + } + return result; +} + +//================================================================= + +/** Reparameterise M to have unit speed. + \param M the Element. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + \relates Piecewise, D2 +*/ +Piecewise > +Geom::arc_length_parametrization(D2 const &M, + unsigned order, + double tol){ + Piecewise > u; + u.push_cut(0); + + Piecewise s = arcLengthSb(Piecewise >(M),tol); + for (unsigned i=0; i < s.size();i++){ + double t0=s.cuts[i],t1=s.cuts[i+1]; + if ( are_near(s(t0),s(t1)) ) { + continue; + } + D2 sub_M = compose(M,Linear(t0,t1)); + D2 sub_u; + for (unsigned dim=0;dim<2;dim++){ + SBasis sub_s = s.segs[i]; + sub_s-=sub_s.at0(); + sub_s/=sub_s.at1(); + sub_u[dim]=compose_inverse(sub_M[dim],sub_s, order, tol); + } + u.push(sub_u,s(t1)); + } + return u; +} + +/** Reparameterise M to have unit speed. + \param M the Element. + \param tol the maximum error allowed. + \param order the maximum degree to use for approximation + \relates Piecewise +*/ +Piecewise > +Geom::arc_length_parametrization(Piecewise > const &M, + unsigned order, + double tol){ + Piecewise > result; + for (unsigned i=0; i +static double sb_length_integrating(double t, void* param) { + SBasis* pc = (SBasis*)param; + return sqrt((*pc)(t)); +} + +/** Calculates the length of a D2 through gsl integration. + \param B the Element. + \param tol the maximum error allowed. + \param result variable to be incremented with the length of the path + \param abs_error variable to be incremented with the estimated error + \relates D2 +If you only want the length, this routine may be faster/more accurate. +*/ +void Geom::length_integrating(D2 const &B, double &result, double &abs_error, double tol) { + D2 dB = derivative(B); + SBasis dB2 = dot(dB, dB); + + gsl_function F; + gsl_integration_workspace * w + = gsl_integration_workspace_alloc (20); + F.function = &sb_length_integrating; + F.params = (void*)&dB2; + double quad_result, err; + /* We could probably use the non adaptive code here if we removed any cusps first. */ + + gsl_integration_qag (&F, 0, 1, 0, tol, 20, + GSL_INTEG_GAUSS21, w, &quad_result, &err); + + abs_error += err; + result += quad_result; +} + +/** Calculates the length of a D2 through gsl integration. + \param s the Element. + \param tol the maximum error allowed. + \relates D2 +If you only want the total length, this routine faster and more accurate than constructing an arcLengthSb. +*/ +double +Geom::length(D2 const &s, + double tol){ + double result = 0; + double abs_error = 0; + length_integrating(s, result, abs_error, tol); + return result; +} +/** Calculates the length of a Piecewise > through gsl integration. + \param s the Element. + \param tol the maximum error allowed. + \relates Piecewise +If you only want the total length, this routine faster and more accurate than constructing an arcLengthSb. +*/ +double +Geom::length(Piecewise > const &s, + double tol){ + double result = 0; + double abs_error = 0; + for (unsigned i=0; i < s.size();i++){ + length_integrating(s[i], result, abs_error, tol); + } + return result; +} + +/** + * Centroid using sbasis integration. + \param p the Element. + \param centroid on return contains the centroid of the shape + \param area on return contains the signed area of the shape. + \relates Piecewise +This approach uses green's theorem to compute the area and centroid using integrals. For curved shapes this is much faster than converting to polyline. Note that without an uncross operation the output is not the absolute area. + + * Returned values: + 0 for normal execution; + 2 if area is zero, meaning centroid is meaningless. + + */ +unsigned Geom::centroid(Piecewise > const &p, Point& centroid, double &area) { + Point centroid_tmp(0,0); + double atmp = 0; + for(unsigned i = 0; i < p.size(); i++) { + SBasis curl = dot(p[i], rot90(derivative(p[i]))); + SBasis A = integral(curl); + D2 C = integral(multiply(curl, p[i])); + atmp += A.at1() - A.at0(); + centroid_tmp += C.at1()- C.at0(); // first moment. + } +// join ends + centroid_tmp *= 2; + Point final = p[p.size()-1].at1(), initial = p[0].at0(); + const double ai = cross(final, initial); + atmp += ai; + centroid_tmp += (final + initial)*ai; // first moment. + + area = atmp / 2; + if (atmp != 0) { + centroid = centroid_tmp / (3 * atmp); + return 0; + } + return 2; +} + +/** + * Find cubics with prescribed curvatures at both ends. + * + * this requires to solve a system of the form + * + * \f[ + * \lambda_1 = a_0 \lambda_0^2 + c_0 + * \lambda_0 = a_1 \lambda_1^2 + c_1 + * \f] + * + * which is a deg 4 equation in lambda 0. + * Below are basic functions dedicated to solving this assuming a0 and a1 !=0. + */ + +static OptInterval +find_bounds_for_lambda0(double aa0,double aa1,double cc0,double cc1, + int insist_on_speeds_signs){ + + double a0=aa0,a1=aa1,c0=cc0,c1=cc1; + Interval result; + bool flip = a1<0; + if (a1<0){a1=-a1; c1=-c1;} + if (a0<0){a0=-a0; c0=-c0;} + double a = (a0 +solve_lambda0(double a0,double a1,double c0,double c1, + int insist_on_speeds_signs){ + + SBasis p(3, Linear()); + p[0] = Linear( a1*c0*c0+c1, a1*a0*(a0+ 2*c0) +a1*c0*c0 +c1 -1 ); + p[1] = Linear( -a1*a0*(a0+2*c0), -a1*a0*(3*a0+2*c0) ); + p[2] = Linear( a1*a0*a0 ); + + OptInterval domain = find_bounds_for_lambda0(a0,a1,c0,c1,insist_on_speeds_signs); + if ( !domain ) + return std::vector(); + p = compose(p,Linear(domain->min(),domain->max())); + std::vectorrts = roots(p); + for (unsigned i=0; imin() + rts[i] * domain->extent(); + } + return rts; +} + +/** +* \brief returns the cubics fitting direction and curvature of a given +* input curve at two points. +* +* The input can be the +* value, speed, and acceleration +* or +* value, speed, and cross(acceleration,speed) +* of the original curve at the both ends. +* (the second is often technically useful, as it avoids unnecessary division by |v|^2) +* Recall that K=1/R=cross(acceleration,speed)/|speed|^3. +* +* Moreover, a 7-th argument 'insist_on_speed_signs' can be supplied to select solutions: +* If insist_on_speed_signs == 1, only consider solutions where speeds at both ends are positively +* proportional to the given ones. +* If insist_on_speed_signs == 0, allow speeds to point in the opposite direction (both at the same time) +* If insist_on_speed_signs == -1, allow speeds to point in both direction independently. +* +* \relates D2 +*/ +std::vector > +Geom::cubics_fitting_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + double d2M0xdM0, double d2M1xdM1, + int insist_on_speed_signs, + double epsilon){ + std::vector > result; + + //speed of cubic bezier will be lambda0*dM0 and lambda1*dM1, + //with lambda0 and lambda1 s.t. curvature at both ends is the same + //as the curvature of the given curve. + std::vector lambda0,lambda1; + double dM1xdM0=cross(dM1,dM0); + if (fabs(dM1xdM0) solns=solve_lambda0(a0,a1,c0,c1,insist_on_speed_signs); + for (unsigned i=0;i=0. && lbda1>=0.){ + lambda0.push_back( lbda0); + lambda1.push_back( lbda1); + } + //is this solution pointing in the - direction at both ends? + else if (lbda0<=0. && lbda1<=0. && insist_on_speed_signs<=0){ + lambda0.push_back( lbda0); + lambda1.push_back( lbda1); + } + //ok,this solution is pointing in the + and - directions. + else if (insist_on_speed_signs<0){ + lambda0.push_back( lbda0); + lambda1.push_back( lbda1); + } + } + } + } + + for (unsigned i=0; i cubic; + for(unsigned dim=0;dim<2;dim++){ + SBasis c(2, Linear()); + c[0] = Linear(M0[dim],M1[dim]); + c[1] = Linear( M0[dim]-M1[dim]+V0[dim], + -M0[dim]+M1[dim]-V1[dim]); + cubic[dim] = c; + } +#if 0 + Piecewise k = curvature(result); + double dM0_l = dM0.length(); + double dM1_l = dM1.length(); + g_warning("Target radii: %f, %f", dM0_l*dM0_l*dM0_l/d2M0xdM0,dM1_l*dM1_l*dM1_l/d2M1xdM1); + g_warning("Obtained radii: %f, %f",1/k.valueAt(0),1/k.valueAt(1)); +#endif + result.push_back(cubic); + } + return(result); +} + +std::vector > +Geom::cubics_fitting_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + Point const &d2M0, Point const &d2M1, + int insist_on_speed_signs, + double epsilon){ + double d2M0xdM0 = cross(d2M0,dM0); + double d2M1xdM1 = cross(d2M1,dM1); + return cubics_fitting_curvature(M0,M1,dM0,dM1,d2M0xdM0,d2M1xdM1,insist_on_speed_signs,epsilon); +} + +std::vector > +Geom::cubics_with_prescribed_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + double k0, double k1, + int insist_on_speed_signs, + double epsilon){ + double length; + length = dM0.length(); + double d2M0xdM0 = k0*length*length*length; + length = dM1.length(); + double d2M1xdM1 = k1*length*length*length; + return cubics_fitting_curvature(M0,M1,dM0,dM1,d2M0xdM0,d2M1xdM1,insist_on_speed_signs,epsilon); +} + + +namespace Geom { +/** +* \brief returns all the parameter values of A whose tangent passes through P. +* \relates D2 +*/ +std::vector find_tangents(Point P, D2 const &A) { + SBasis crs (cross(A - P, derivative(A))); + return roots(crs); +} + +/** +* \brief returns all the parameter values of A whose normal passes through P. +* \relates D2 +*/ +std::vector find_normals(Point P, D2 const &A) { + SBasis crs (dot(A - P, derivative(A))); + return roots(crs); +} + +/** +* \brief returns all the parameter values of A whose normal is parallel to vector V. +* \relates D2 +*/ +std::vector find_normals_by_vector(Point V, D2 const &A) { + SBasis crs = dot(derivative(A), V); + return roots(crs); +} +/** +* \brief returns all the parameter values of A whose tangent is parallel to vector V. +* \relates D2 +*/ +std::vector find_tangents_by_vector(Point V, D2 const &A) { + SBasis crs = dot(derivative(A), rot90(V)); + return roots(crs); +} + +} +//}; // namespace + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-geometric.h b/src/2geom/sbasis-geometric.h new file mode 100644 index 0000000..7f1e8aa --- /dev/null +++ b/src/2geom/sbasis-geometric.h @@ -0,0 +1,146 @@ +/** + * \file + * \brief two-dimensional geometric operators. + * + * These operators are built on a more 'polynomially robust' + * transformation to map a function that takes a [0,1] parameter to a + * 2d vector into a function that takes the same [0,1] parameter to a + * unit vector with the same direction. + * + * Rather that using (X/sqrt(X))(t) which involves two unstable + * operations, sqrt and divide, this approach forms a curve directly + * from the various tangent directions at each end (angular jet). As + * a result, the final path has a convergence behaviour derived from + * that of the sin and cos series. -- njh + *//* + * Copyright 2007, JFBarraud + * Copyright 2007, njh + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_GEOMETRIC_H +#define LIB2GEOM_SEEN_SBASIS_GEOMETRIC_H + +#include <2geom/d2.h> +#include <2geom/piecewise.h> +#include + +namespace Geom { + +Piecewise > +cutAtRoots(Piecewise > const &M, double tol=1e-4); + +Piecewise +atan2(D2 const &vect, + double tol=.01, unsigned order=3); + +Piecewise +atan2(Piecewise >const &vect, + double tol=.01, unsigned order=3); + +D2 > +tan2(SBasis const &angle, + double tol=.01, unsigned order=3); + +D2 > +tan2(Piecewise const &angle, + double tol=.01, unsigned order=3); + +Piecewise > +unitVector(D2 const &vect, + double tol=.01, unsigned order=3); +Piecewise > +unitVector(Piecewise > const &vect, + double tol=.01, unsigned order=3); + +// Piecewise > +// uniform_speed(D2 const M, +// double tol=.1); + +Piecewise curvature( D2 const &M, double tol=.01); +Piecewise curvature(Piecewise > const &M, double tol=.01); + +Piecewise arcLengthSb( D2 const &M, double tol=.01); +Piecewise arcLengthSb(Piecewise > const &M, double tol=.01); + +double length( D2 const &M, double tol=.01); +double length(Piecewise > const &M, double tol=.01); + +void length_integrating(D2 const &B, double &result, double &abs_error, double tol); + +Piecewise > +arc_length_parametrization(D2 const &M, + unsigned order=3, + double tol=.01); +Piecewise > +arc_length_parametrization(Piecewise > const &M, + unsigned order=3, + double tol=.01); + + +unsigned centroid(Piecewise > const &p, Point& centroid, double &area); + +std::vector > +cubics_fitting_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + double d2M0xdM0, double d2M1xdM1, + int insist_on_speed_signs = 1, + double epsilon = 1e-5); + +std::vector > +cubics_fitting_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + Point const &d2M0, Point const &d2M1, + int insist_on_speed_signs = 1, + double epsilon = 1e-5); + +std::vector > +cubics_with_prescribed_curvature(Point const &M0, Point const &M1, + Point const &dM0, Point const &dM1, + double k0, double k1, + int insist_on_speed_signs = 1, + double error = 1e-5); + + +std::vector find_tangents(Point P, D2 const &A); +std::vector find_tangents_by_vector(Point V, D2 const &A); +std::vector find_normals(Point P, D2 const &A); +std::vector find_normals_by_vector(Point V, D2 const &A); + +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/2geom/sbasis-math.cpp b/src/2geom/sbasis-math.cpp new file mode 100644 index 0000000..b10bf93 --- /dev/null +++ b/src/2geom/sbasis-math.cpp @@ -0,0 +1,379 @@ +/* + * sbasis-math.cpp - some std functions to work with (pw)s-basis + * + * Authors: + * Jean-Francois Barraud + * + * Copyright (C) 2006-2007 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +//this a first try to define sqrt, cos, sin, etc... +//TODO: define a truncated compose(sb,sb, order) and extend it to pw. +//TODO: in all these functions, compute 'order' according to 'tol'. + +#include <2geom/d2.h> +#include <2geom/sbasis-math.h> +#include +#include +//#define ZERO 1e-3 + + +namespace Geom { + + +//-|x|----------------------------------------------------------------------- +/** Return the absolute value of a function pointwise. + \param f function +*/ +Piecewise abs(SBasis const &f){ + return abs(Piecewise(f)); +} +/** Return the absolute value of a function pointwise. + \param f function +*/ +Piecewise abs(Piecewise const &f){ + Piecewise absf=partition(f,roots(f)); + for (unsigned i=0; i max( SBasis const &f, SBasis const &g){ + return max(Piecewise(f),Piecewise(g)); +} +/** Return the greater of the two functions pointwise. + \param f, g two functions +*/ +Piecewise max(Piecewise const &f, SBasis const &g){ + return max(f,Piecewise(g)); +} +/** Return the greater of the two functions pointwise. + \param f, g two functions +*/ +Piecewise max( SBasis const &f, Piecewise const &g){ + return max(Piecewise(f),g); +} +/** Return the greater of the two functions pointwise. + \param f, g two functions +*/ +Piecewise max(Piecewise const &f, Piecewise const &g){ + Piecewise max=partition(f,roots(f-g)); + Piecewise gg =partition(g,max.cuts); + max = partition(max,gg.cuts); + for (unsigned i=0; i +min( SBasis const &f, SBasis const &g){ return -max(-f,-g); } +/** Return the more negative of the two functions pointwise. + \param f, g two functions +*/ +Piecewise +min(Piecewise const &f, SBasis const &g){ return -max(-f,-g); } +/** Return the more negative of the two functions pointwise. + \param f, g two functions +*/ +Piecewise +min( SBasis const &f, Piecewise const &g){ return -max(-f,-g); } +/** Return the more negative of the two functions pointwise. + \param f, g two functions +*/ +Piecewise +min(Piecewise const &f, Piecewise const &g){ return -max(-f,-g); } + + +//-sign(x)--------------------------------------------------------------- +/** Return the sign of the two functions pointwise. + \param f function +*/ +Piecewise signSb(SBasis const &f){ + return signSb(Piecewise(f)); +} +/** Return the sign of the two functions pointwise. + \param f function +*/ +Piecewise signSb(Piecewise const &f){ + Piecewise sign=partition(f,roots(f)); + for (unsigned i=0; i sqrt_internal(SBasis const &f, + double tol, + int order){ + SBasis sqrtf; + if(f.isZero() || order == 0){ + return Piecewise(sqrtf); + } + if (f.at0()<-tol*tol && f.at1()<-tol*tol){ + return sqrt_internal(-f,tol,order); + }else if (f.at0()>tol*tol && f.at1()>tol*tol){ + sqrtf.resize(order+1, Linear(0,0)); + sqrtf[0] = Linear(std::sqrt(f[0][0]), std::sqrt(f[0][1])); + SBasis r = f - multiply(sqrtf, sqrtf); // remainder + for(unsigned i = 1; int(i) <= order && i(sqrtf); + } + + Piecewise sqrtf0,sqrtf1; + sqrtf0 = sqrt_internal(compose(f,Linear(0.,.5)),tol,order); + sqrtf1 = sqrt_internal(compose(f,Linear(.5,1.)),tol,order); + sqrtf0.setDomain(Interval(0.,.5)); + sqrtf1.setDomain(Interval(.5,1.)); + sqrtf0.concat(sqrtf1); + return sqrtf0; +} + +/** Compute the sqrt of a function. + \param f function +*/ +Piecewise sqrt(SBasis const &f, double tol, int order){ + return sqrt(max(f,Linear(tol*tol)),tol,order); +} + +/** Compute the sqrt of a function. + \param f function +*/ +Piecewise sqrt(Piecewise const &f, double tol, int order){ + Piecewise result; + Piecewise zero = Piecewise(Linear(tol*tol)); + zero.setDomain(f.domain()); + Piecewise ff=max(f,zero); + + for (unsigned i=0; i sqrtfi = sqrt_internal(ff.segs[i],tol,order); + sqrtfi.setDomain(Interval(ff.cuts[i],ff.cuts[i+1])); + result.concat(sqrtfi); + } + return result; +} + +//-Yet another sin/cos-------------------------------------------------------------- + +/** Compute the sine of a function. + \param f function + \param tol maximum error + \param order maximum degree polynomial to use +*/ +Piecewise sin( SBasis const &f, double tol, int order){return(cos(-f+M_PI/2,tol,order));} +/** Compute the sine of a function. + \param f function + \param tol maximum error + \param order maximum degree polynomial to use +*/ +Piecewise sin(Piecewise const &f, double tol, int order){return(cos(-f+M_PI/2,tol,order));} + +/** Compute the cosine of a function. + \param f function + \param tol maximum error + \param order maximum degree polynomial to use +*/ +Piecewise cos(Piecewise const &f, double tol, int order){ + Piecewise result; + for (unsigned i=0; i cosfi = cos(f.segs[i],tol,order); + cosfi.setDomain(Interval(f.cuts[i],f.cuts[i+1])); + result.concat(cosfi); + } + return result; +} + +/** Compute the cosine of a function. + \param f function + \param tol maximum error + \param order maximum degree polynomial to use +*/ +Piecewise cos( SBasis const &f, double tol, int order){ + double alpha = (f.at0()+f.at1())/2.; + SBasis x = f-alpha; + double d = x.tailError(0),err=1; + //estimate cos(x)-sum_0^order (-1)^k x^2k/2k! by the first neglicted term + for (int i=1; i<=2*order; i++) err*=d/i; + + if (err(std::cos(alpha)*c-std::sin(alpha)*s); + } + } + Piecewise c0,c1; + c0 = cos(compose(f,Linear(0.,.5)),tol,order); + c1 = cos(compose(f,Linear(.5,1.)),tol,order); + c0.setDomain(Interval(0.,.5)); + c1.setDomain(Interval(.5,1.)); + c0.concat(c1); + return c0; +} + +//--1/x------------------------------------------------------------ +//TODO: this implementation is just wrong. Remove or redo! + +void truncateResult(Piecewise &f, int order){ + if (order>=0){ + for (unsigned k=0; k reciprocalOnDomain(Interval range, double tol){ + Piecewise reciprocal_fn; + //TODO: deduce R from tol... + double R=2.; + SBasis reciprocal1_R=reciprocal(Linear(1,R),3); + double a=range.min(), b=range.max(); + if (a*b<0){ + b=std::max(fabs(a),fabs(b)); + a=0; + }else if (b<0){ + a=-range.max(); + b=-range.min(); + } + + if (a<=tol){ + reciprocal_fn.push_cut(0); + int i0=(int) floor(std::log(tol)/std::log(R)); + a = std::pow(R,i0); + reciprocal_fn.push(Linear(1/a),a); + }else{ + int i0=(int) floor(std::log(a)/std::log(R)); + a = std::pow(R,i0); + reciprocal_fn.cuts.push_back(a); + } + + while (areciprocal_fn_neg; + //TODO: define reverse(pw); + reciprocal_fn_neg.cuts.push_back(-reciprocal_fn.cuts.back()); + for (unsigned i=0; i0){ + reciprocal_fn_neg.concat(reciprocal_fn); + } + reciprocal_fn=reciprocal_fn_neg; + } + + return(reciprocal_fn); +} + +Piecewise reciprocal(SBasis const &f, double tol, int order){ + Piecewise reciprocal_fn=reciprocalOnDomain(*bounds_fast(f), tol); + Piecewise result=compose(reciprocal_fn,f); + truncateResult(result,order); + return(result); +} +Piecewise reciprocal(Piecewise const &f, double tol, int order){ + Piecewise reciprocal_fn=reciprocalOnDomain(*bounds_fast(f), tol); + Piecewise result=compose(reciprocal_fn,f); + truncateResult(result,order); + return(result); +} + +/** + * \brief Returns a Piecewise SBasis with prescribed values at prescribed times. + * + * \param times: vector of times at which the values are given. Should be sorted in increasing order. + * \param values: vector of prescribed values. Should have the same size as times and be sorted accordingly. + * \param smoothness: (defaults to 1) regularity class of the result: 0=piecewise linear, 1=continuous derivative, etc... + */ +Piecewise interpolate(std::vector times, std::vector values, unsigned smoothness){ + assert ( values.size() == times.size() ); + if ( values.empty() ) return Piecewise(); + if ( values.size() == 1 ) return Piecewise(values[0]);//what about time?? + + SBasis sk = shift(Linear(1.),smoothness); + SBasis bump_in = integral(sk); + bump_in -= bump_in.at0(); + bump_in /= bump_in.at1(); + SBasis bump_out = reverse( bump_in ); + + Piecewise result; + result.cuts.push_back(times[0]); + for (unsigned i = 0; i. +//TODO: in all these functions, compute 'order' according to 'tol'. +//TODO: use template to define the pw version automatically from the sb version? + +#ifndef LIB2GEOM_SEEN_SBASIS_MATH_H +#define LIB2GEOM_SEEN_SBASIS_MATH_H + + +#include <2geom/sbasis.h> +#include <2geom/piecewise.h> +#include <2geom/d2.h> + +namespace Geom{ +//-|x|--------------------------------------------------------------- +Piecewise abs( SBasis const &f); +Piecewise abs(Piecewiseconst &f); + +//- max(f,g), min(f,g) ---------------------------------------------- +Piecewise max( SBasis const &f, SBasis const &g); +Piecewise max(Piecewise const &f, SBasis const &g); +Piecewise max( SBasis const &f, Piecewise const &g); +Piecewise max(Piecewise const &f, Piecewise const &g); +Piecewise min( SBasis const &f, SBasis const &g); +Piecewise min(Piecewise const &f, SBasis const &g); +Piecewise min( SBasis const &f, Piecewise const &g); +Piecewise min(Piecewise const &f, Piecewise const &g); + +//-sign(x)--------------------------------------------------------------- +Piecewise signSb( SBasis const &f); +Piecewise signSb(Piecewiseconst &f); + +//-Sqrt--------------------------------------------------------------- +Piecewise sqrt( SBasis const &f, double tol=1e-3, int order=3); +Piecewise sqrt(Piecewiseconst &f, double tol=1e-3, int order=3); + +//-sin/cos-------------------------------------------------------------- +Piecewise cos( SBasis const &f, double tol=1e-3, int order=3); +Piecewise cos(Piecewise const &f, double tol=1e-3, int order=3); +Piecewise sin( SBasis const &f, double tol=1e-3, int order=3); +Piecewise sin(Piecewise const &f, double tol=1e-3, int order=3); +//-Log--------------------------------------------------------------- +Piecewise log( SBasis const &f, double tol=1e-3, int order=3); +Piecewise log(Piecewiseconst &f, double tol=1e-3, int order=3); + +//--1/x------------------------------------------------------------ +//TODO: change this... +Piecewise reciprocalOnDomain(Interval range, double tol=1e-3); +Piecewise reciprocal( SBasis const &f, double tol=1e-3, int order=3); +Piecewise reciprocal(Piecewiseconst &f, double tol=1e-3, int order=3); + +//--interpolate------------------------------------------------------------ +Piecewise interpolate( std::vector times, std::vector values, unsigned smoothness = 1); +} + +#endif //SEEN_GEOM_PW_SB_CALCULUS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-poly.cpp b/src/2geom/sbasis-poly.cpp new file mode 100644 index 0000000..ffee43f --- /dev/null +++ b/src/2geom/sbasis-poly.cpp @@ -0,0 +1,59 @@ +#include <2geom/sbasis-poly.h> + +namespace Geom{ + +/** Changes the basis of p to be sbasis. + \param p the Monomial basis polynomial + \returns the Symmetric basis polynomial + +This algorithm is horribly slow and numerically terrible. Only for testing. +*/ +SBasis poly_to_sbasis(Poly const & p) { + SBasis x = Linear(0, 1); + SBasis r; + + for(int i = p.size()-1; i >= 0; i--) { + r = SBasis(Linear(p[i], p[i])) + multiply(x, r); + } + r.normalize(); + return r; + +} + +/** Changes the basis of p to be monomial. + \param p the Symmetric basis polynomial + \returns the Monomial basis polynomial + +This algorithm is horribly slow and numerically terrible. Only for testing. +*/ +Poly sbasis_to_poly(SBasis const & sb) { + if(sb.isZero()) + return Poly(); + Poly S; // (1-x)x = -1*x^2 + 1*x + 0 + Poly A, B; + B.push_back(0); + B.push_back(1); + A.push_back(1); + A.push_back(-1); + S = A*B; + Poly r; + + for(int i = sb.size()-1; i >= 0; i--) { + r = S*r + sb[i][0]*A + sb[i][1]*B; + } + r.normalize(); + return r; +} + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-poly.h b/src/2geom/sbasis-poly.h new file mode 100644 index 0000000..d18bc36 --- /dev/null +++ b/src/2geom/sbasis-poly.h @@ -0,0 +1,56 @@ +/** @file + * @brief Conversion between SBasis and Poly. Not recommended for general use due to instability. + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_POLY_H +#define LIB2GEOM_SEEN_SBASIS_POLY_H + +#include <2geom/polynomial.h> +#include <2geom/sbasis.h> + +namespace Geom{ + +SBasis poly_to_sbasis(Poly const & p); +Poly sbasis_to_poly(SBasis const & s); + +}; + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-roots.cpp b/src/2geom/sbasis-roots.cpp new file mode 100644 index 0000000..244b7ef --- /dev/null +++ b/src/2geom/sbasis-roots.cpp @@ -0,0 +1,656 @@ +/** + * @file + * @brief Root finding for sbasis functions. + *//* + * Authors: + * Nathan Hurst + * JF Barraud + * Copyright 2006-2007 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + + /* + * It is more efficient to find roots of f(t) = c_0, c_1, ... all at once, rather than iterating. + * + * Todo/think about: + * multi-roots using bernstein method, one approach would be: + sort c + take median and find roots of that + whenever a segment lies entirely on one side of the median, + find the median of the half and recurse. + + in essence we are implementing quicksort on a continuous function + + * the gsl poly roots finder is faster than bernstein too, but we don't use it for 3 reasons: + + a) it requires conversion to poly, which is numerically unstable + + b) it requires gsl (which is currently not a dependency, and would bring in a whole slew of unrelated stuff) + + c) it finds all roots, even complex ones. We don't want to accidentally treat a nearly real root as a real root + +From memory gsl poly roots was about 10 times faster than bernstein in the case where all the roots +are in [0,1] for polys of order 5. I spent some time working out whether eigenvalue root finding +could be done directly in sbasis space, but the maths was too hard for me. -- njh + +jfbarraud: eigenvalue root finding could be done directly in sbasis space ? + +njh: I don't know, I think it should. You would make a matrix whose characteristic polynomial was +correct, but do it by putting the sbasis terms in the right spots in the matrix. normal eigenvalue +root finding makes a matrix that is a diagonal + a row along the top. This matrix has the property +that its characteristic poly is just the poly whose coefficients are along the top row. + +Now an sbasis function is a linear combination of the poly coeffs. So it seems to me that you +should be able to put the sbasis coeffs directly into a matrix in the right spots so that the +characteristic poly is the sbasis. You'll still have problems b) and c). + +We might be able to lift an eigenvalue solver and include that directly into 2geom. Eigenvalues +also allow you to find intersections of multiple curves but require solving n*m x n*m matrices. + + **/ + +#include +#include + +#include <2geom/sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/solver.h> + +using namespace std; + +namespace Geom{ + +/** Find the smallest interval that bounds a + \param a sbasis function + \returns interval + +*/ + +#ifdef USE_SBASIS_OF +OptInterval bounds_exact(SBasisOf const &a) { + Interval result = Interval(a.at0(), a.at1()); + SBasisOf df = derivative(a); + vectorextrema = roots(df); + for (unsigned i=0; iextrema = roots(df); + for (unsigned i=0; i &sb, int order) { +#else +OptInterval bounds_fast(const SBasis &sb, int order) { +#endif + Interval res(0,0); // an empty sbasis is 0. + + for(int j = sb.size()-1; j>=order; j--) { + double a=sb[j][0]; + double b=sb[j][1]; + + double v, t = 0; + v = res.min(); + if (v<0) t = ((b-a)/v+1)*0.5; + if (v>=0 || t<0 || t>1) { + res.setMin(std::min(a,b)); + } else { + res.setMin(lerp(t, a+v*t, b)); + } + + v = res.max(); + if (v>0) t = ((b-a)/v+1)*0.5; + if (v<=0 || t<0 || t>1) { + res.setMax(std::max(a,b)); + }else{ + res.setMax(lerp(t, a+v*t, b)); + } + } + if (order>0) res*=std::pow(.25,order); + return res; +} + +/** Find a small interval that bounds a(t) for t in i to order order + \param sb sbasis function + \param i domain interval + \param order number of terms + \return interval + +*/ +#ifdef USE_SBASIS_OF +OptInterval bounds_local(const SBasisOf &sb, const OptInterval &i, int order) { +#else +OptInterval bounds_local(const SBasis &sb, const OptInterval &i, int order) { +#endif + double t0=i->min(), t1=i->max(), lo=0., hi=0.; + for(int j = sb.size()-1; j>=order; j--) { + double a=sb[j][0]; + double b=sb[j][1]; + + double t = 0; + if (lo<0) t = ((b-a)/lo+1)*0.5; + if (lo>=0 || tt1) { + lo = std::min(a*(1-t0)+b*t0+lo*t0*(1-t0),a*(1-t1)+b*t1+lo*t1*(1-t1)); + }else{ + lo = lerp(t, a+lo*t, b); + } + + if (hi>0) t = ((b-a)/hi+1)*0.5; + if (hi<=0 || tt1) { + hi = std::max(a*(1-t0)+b*t0+hi*t0*(1-t0),a*(1-t1)+b*t1+hi*t1*(1-t1)); + }else{ + hi = lerp(t, a+hi*t, b); + } + } + Interval res = Interval(lo,hi); + if (order>0) res*=std::pow(.25,order); + return res; +} + +//-- multi_roots ------------------------------------ +// goal: solve f(t)=c for several c at once. +/* algo: -compute f at both ends of the given segment [a,b]. + -compute bounds m const &levels,double x,double tol=0.){ + return(upper_bound(levels.begin(),levels.end(),x-tol)-levels.begin()); +} + +#ifdef USE_SBASIS_OF +static void multi_roots_internal(SBasis const &f, + SBasis const &df, +#else +static void multi_roots_internal(SBasis const &f, + SBasis const &df, +#endif + std::vector const &levels, + std::vector > &roots, + double htol, + double vtol, + double a, + double fa, + double b, + double fb){ + + if (f.isZero(0)){ + int idx; + idx=upper_level(levels,0,vtol); + if (idx<(int)levels.size()&&fabs(levels.at(idx))<=vtol){ + roots[idx].push_back(a); + roots[idx].push_back(b); + } + return; + } +////useful? +// if (f.size()==1){ +// int idxa=upper_level(levels,fa); +// int idxb=upper_level(levels,fb); +// if (fa==fb){ +// if (fa==levels[idxa]){ +// roots[a]=idxa; +// roots[b]=idxa; +// } +// return; +// } +// int idx_min=std::min(idxa,idxb); +// int idx_max=std::max(idxa,idxb); +// if (idx_max==levels.size()) idx_max-=1; +// for(int i=idx_min;i<=idx_max; i++){ +// double t=a+(b-a)*(levels[i]-fa)/(fb-fa); +// if(a no root there. + tb_hi=tb_lo=a-1;//default values => no root there. + + if (idxa<(int)levels.size() && fabs(fa-levels.at(idxa))0 && idxa<(int)levels.size()) + ta_hi=a+(levels.at(idxa )-fa)/bs.max(); + if (bs.min()<0 && idxa>0) + ta_lo=a+(levels.at(idxa-1)-fa)/bs.min(); + } + if (idxb<(int)levels.size() && fabs(fb-levels.at(idxb))0 && idxb>0) + tb_lo=b+(levels.at(idxb-1)-fb)/bs.max(); + } + + double t0,t1; + t0=std::min(ta_hi,ta_lo); + t1=std::max(tb_hi,tb_lo); + //hum, rounding errors frighten me! so I add this +tol... + if (t0>t1+htol) return;//no root here. + + if (fabs(t1-t0) > multi_roots(SBasis const &f, + std::vector const &levels, + double htol, + double vtol, + double a, + double b){ + + std::vector > roots(levels.size(), std::vector()); + + SBasis df=derivative(f); + multi_roots_internal(f,df,levels,roots,htol,vtol,a,f(a),b,f(b)); + + return(roots); +} + + +static bool compareIntervalMin( Interval I, Interval J ){ + return I.min()= x +static unsigned upper_level(vector const &levels, double x ){ + return( lower_bound( levels.begin(), levels.end(), Interval(x,x), compareIntervalMax) - levels.begin() ); +} + +static std::vector fuseContiguous(std::vector const &sets, double tol=0.){ + std::vector result; + if (sets.empty() ) return result; + result.push_back( sets.front() ); + for (unsigned i=1; i < sets.size(); i++ ){ + if ( result.back().max() + tol >= sets[i].min() ){ + result.back().unionWith( sets[i] ); + }else{ + result.push_back( sets[i] ); + } + } + return result; +} + +/** level_sets internal method. +* algorithm: (~adaptation of Newton method versus 'accroissements finis') + -compute f at both ends of the given segment [a,b]. + -compute bounds m const &levels, + std::vector > &solsets, + double a, + double fa, + double b, + double fb, + double tol=1e-5){ + + if (f.isZero(0)){ + unsigned idx; + idx=upper_level( levels, 0. ); + if (idxtb_hi + double tb_lo; // f remains above next level for t>tb_lo + + ta_hi=ta_lo=b+1;//default values => no root there. + tb_hi=tb_lo=a-1;//default values => no root there. + + //--- if f(a) belongs to a level.------- + if ( idxa < levels.size() && levels[idxa].contains( fa ) ){ + //find the first time when we may exit this level. + ta_lo = a + ( levels[idxa].min() - fa)/bs.min(); + ta_hi = a + ( levels[idxa].max() - fa)/bs.max(); + if ( ta_lo < a || ta_lo > b ) ta_lo = b; + if ( ta_hi < a || ta_hi > b ) ta_hi = b; + //move to that time for the next iteration. + solsets[idxa].push_back( Interval( a, std::min( ta_lo, ta_hi ) ) ); + }else{ + //--- if f(b) does not belong to a level.------- + if ( idxa == 0 ){ + ta_lo = b; + }else{ + ta_lo = a + ( levels[idxa-1].max() - fa)/bs.min(); + if ( ta_lo < a ) ta_lo = b; + } + if ( idxa == levels.size() ){ + ta_hi = b; + }else{ + ta_hi = a + ( levels[idxa].min() - fa)/bs.max(); + if ( ta_hi < a ) ta_hi = b; + } + } + + //--- if f(b) belongs to a level.------- + if (idxb b || tb_lo < a ) tb_lo = a; + if ( tb_hi > b || tb_hi < a ) tb_hi = a; + //move to that time for the next iteration. + solsets[idxb].push_back( Interval( std::max( tb_lo, tb_hi ), b) ); + }else{ + //--- if f(b) does not belong to a level.------- + if ( idxb == 0 ){ + tb_lo = a; + }else{ + tb_lo = b + ( levels[idxb-1].max() - fb)/bs.max(); + if ( tb_lo > b ) tb_lo = a; + } + if ( idxb == levels.size() ){ + tb_hi = a; + }else{ + tb_hi = b + ( levels[idxb].min() - fb)/bs.min(); + if ( tb_hi > b ) tb_hi = a; + } + + + if ( bs.min() < 0 && idxb < levels.size() ) + tb_hi = b + ( levels[idxb ].min() - fb ) / bs.min(); + if ( bs.max() > 0 && idxb > 0 ) + tb_lo = b + ( levels[idxb-1].max() - fb ) / bs.max(); + } + + //let [t0,t1] be the next interval where to search. + double t0=std::min(ta_hi,ta_lo); + double t1=std::max(tb_hi,tb_lo); + + if (t0>=t1) return;//no root here. + + //if the interval is smaller than our resolution: + //pretend f simultaneously meets all the levels between f(t0) and f(t1)... + if ( t1 - t0 <= tol ){ + Interval f_t0t1 ( f(t0), f(t1) ); + unsigned idxmin = std::min(idxa, idxb); + unsigned idxmax = std::max(idxa, idxb); + //push [t0,t1] into all crossed level. Cheat to avoid overlapping intervals on different levels? + if ( idxmax > idxmin ){ + for (unsigned idx = idxmin; idx < idxmax; idx++){ + solsets[idx].push_back( Interval( t0, t1 ) ); + } + } + if ( idxmax < levels.size() && f_t0t1.intersects( levels[idxmax] ) ){ + solsets[idxmax].push_back( Interval( t0, t1 ) ); + } + return; + } + + //To make sure we finally exit the level jump at least by tol: + t0 = std::min( std::max( t0, a + tol ), b ); + t1 = std::max( std::min( t1, b - tol ), a ); + + double t =(t0+t1)/2; + double ft=f(t); + level_sets_internal( f, df, levels, solsets, t0, f(t0), t, ft ); + level_sets_internal( f, df, levels, solsets, t, ft, t1, f(t1) ); +} + +std::vector > level_sets(SBasis const &f, + std::vector const &levels, + double a, double b, double tol){ + + std::vector > solsets(levels.size(), std::vector()); + + SBasis df=derivative(f); + level_sets_internal(f,df,levels,solsets,a,f(a),b,f(b),tol); + // Fuse overlapping intervals... + for (unsigned i=0; i level_set (SBasis const &f, double level, double vtol, double a, double b, double tol){ + Interval fat_level( level - vtol, level + vtol ); + return level_set(f, fat_level, a, b, tol); +} +std::vector level_set (SBasis const &f, Interval const &level, double a, double b, double tol){ + std::vector levels(1,level); + return level_sets(f,levels, a, b, tol).front() ; +} +std::vector > level_sets (SBasis const &f, std::vector const &levels, double vtol, double a, double b, double tol){ + std::vector fat_levels( levels.size(), Interval()); + for (unsigned i = 0; i < levels.size(); i++){ + fat_levels[i] = Interval( levels[i]-vtol, levels[i]+vtol); + } + return level_sets(f, fat_levels, a, b, tol); +} + + +//------------------------------------- +//------------------------------------- + + +void subdiv_sbasis(SBasis const & s, + std::vector & roots, + double left, double right) { + OptInterval bs = bounds_fast(s); + if(!bs || bs->min() > 0 || bs->max() < 0) + return; // no roots here + if(s.tailError(1) < 1e-7) { + double t = s[0][0] / (s[0][0] - s[0][1]); + roots.push_back(left*(1-t) + t*right); + return; + } + double middle = (left + right)/2; + subdiv_sbasis(compose(s, Linear(0, 0.5)), roots, left, middle); + subdiv_sbasis(compose(s, Linear(0.5, 1.)), roots, middle, right); +} + +// It is faster to use the bernstein root finder for small degree polynomials (<100?. + +std::vector roots1(SBasis const & s) { + std::vector res; + double d = s[0][0] - s[0][1]; + if(d != 0) { + double r = s[0][0] / d; + if(0 <= r && r <= 1) + res.push_back(r); + } + return res; +} + +std::vector roots1(SBasis const & s, Interval const ivl) { + std::vector res; + double d = s[0][0] - s[0][1]; + if(d != 0) { + double r = s[0][0] / d; + if(ivl.contains(r)) + res.push_back(r); + } + return res; +} + +/** Find all t s.t s(t) = 0 + \param a sbasis function + \see Bezier::roots + \returns vector of zeros (roots) + +*/ +std::vector roots(SBasis const & s) { + switch(s.size()) { + case 0: + assert(false); + return std::vector(); + case 1: + return roots1(s); + default: + { + Bezier bz; + sbasis_to_bezier(bz, s); + return bz.roots(); + } + } +} +std::vector roots(SBasis const & s, Interval const ivl) { + switch(s.size()) { + case 0: + assert(false); + return std::vector(); + case 1: + return roots1(s, ivl); + default: + { + Bezier bz; + sbasis_to_bezier(bz, s); + return bz.roots(ivl); + } + } +} + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-to-bezier.cpp b/src/2geom/sbasis-to-bezier.cpp new file mode 100644 index 0000000..010cf7c --- /dev/null +++ b/src/2geom/sbasis-to-bezier.cpp @@ -0,0 +1,570 @@ +/* + * Symmetric Power Basis - Bernstein Basis conversion routines + * + * Authors: + * Marco Cecchetti + * Nathan Hurst + * + * Copyright 2007-2008 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/d2.h> +#include <2geom/choose.h> +#include <2geom/path-sink.h> +#include <2geom/exception.h> +#include <2geom/convex-hull.h> + +#include + + + + +namespace Geom +{ + +/* + * Symmetric Power Basis - Bernstein Basis conversion routines + * + * some remark about precision: + * interval [0,1], subdivisions: 10^3 + * - bezier_to_sbasis : up to degree ~72 precision is at least 10^-5 + * up to degree ~87 precision is at least 10^-3 + * - sbasis_to_bezier : up to order ~63 precision is at least 10^-15 + * precision is at least 10^-14 even beyond order 200 + * + * interval [-1,1], subdivisions: 10^3 + * - bezier_to_sbasis : up to degree ~21 precision is at least 10^-5 + * up to degree ~24 precision is at least 10^-3 + * - sbasis_to_bezier : up to order ~11 precision is at least 10^-5 + * up to order ~13 precision is at least 10^-3 + * + * interval [-10,10], subdivisions: 10^3 + * - bezier_to_sbasis : up to degree ~7 precision is at least 10^-5 + * up to degree ~8 precision is at least 10^-3 + * - sbasis_to_bezier : up to order ~3 precision is at least 10^-5 + * up to order ~4 precision is at least 10^-3 + * + * references: + * this implementation is based on the following article: + * J.Sanchez-Reyes - The Symmetric Analogue of the Polynomial Power Basis + */ + +inline +double binomial(unsigned int n, unsigned int k) +{ + return choose(n, k); +} + +inline +int sgn(unsigned int j, unsigned int k) +{ + assert (j >= k); + // we are sure that j >= k + return ((j-k) & 1u) ? -1 : 1; +} + + +/** Changes the basis of p to be bernstein. + \param p the Symmetric basis polynomial + \returns the Bernstein basis polynomial + + if the degree is even q is the order in the symmetrical power basis, + if the degree is odd q is the order + 1 + n is always the polynomial degree, i. e. the Bezier order + sz is the number of bezier handles. +*/ +void sbasis_to_bezier (Bezier & bz, SBasis const& sb, size_t sz) +{ + assert(sb.size() > 0); + + size_t q, n; + bool even; + if (sz == 0) + { + q = sb.size(); + if (sb[q-1][0] == sb[q-1][1]) + { + even = true; + --q; + n = 2*q; + } + else + { + even = false; + n = 2*q-1; + } + } + else + { + q = (sz > 2*sb.size()-1) ? sb.size() : (sz+1)/2; + n = sz-1; + even = false; + } + bz.clear(); + bz.resize(n+1); + double Tjk; + for (size_t k = 0; k < q; ++k) + { + for (size_t j = k; j < n-k; ++j) // j <= n-k-1 + { + Tjk = binomial(n-2*k-1, j-k); + bz[j] += (Tjk * sb[k][0]); + bz[n-j] += (Tjk * sb[k][1]); // n-k <-> [k][1] + } + } + if (even) + { + bz[q] += sb[q][0]; + } + // the resulting coefficients are with respect to the scaled Bernstein + // basis so we need to divide them by (n, j) binomial coefficient + for (size_t j = 1; j < n; ++j) + { + bz[j] /= binomial(n, j); + } + bz[0] = sb[0][0]; + bz[n] = sb[0][1]; +} + +void sbasis_to_bezier(D2 &bz, D2 const &sb, size_t sz) +{ + if (sz == 0) { + sz = std::max(sb[X].size(), sb[Y].size())*2; + } + sbasis_to_bezier(bz[X], sb[X], sz); + sbasis_to_bezier(bz[Y], sb[Y], sz); +} + +/** Changes the basis of p to be Bernstein. + \param p the D2 Symmetric basis polynomial + \returns the D2 Bernstein basis polynomial + + sz is always the polynomial degree, i. e. the Bezier order +*/ +void sbasis_to_bezier (std::vector & bz, D2 const& sb, size_t sz) +{ + D2 bez; + sbasis_to_bezier(bez, sb, sz); + bz = bezier_points(bez); +} + +/** Changes the basis of p to be Bernstein. + \param p the D2 Symmetric basis polynomial + \returns the D2 Bernstein basis cubic polynomial + +Bezier is always cubic. +For general asymmetric case, fit the SBasis function value at midpoint +For parallel, symmetric case, find the point of closest approach to the midpoint +For parallel, anti-symmetric case, fit the SBasis slope at midpoint +*/ +void sbasis_to_cubic_bezier (std::vector & bz, D2 const& sb) +{ + double delx[2], dely[2]; + double xprime[2], yprime[2]; + double midx = 0; + double midy = 0; + double midx_0, midy_0; + double numer[2], numer_0[2]; + double denom; + double div; + + if ((sb[X].size() == 0) || (sb[Y].size() == 0)) { + THROW_RANGEERROR("size of sb is too small"); + } + + sbasis_to_bezier(bz, sb, 4); // zeroth-order estimate + if ((sb[X].size() < 3) && (sb[Y].size() < 3)) + return; // cubic bezier estimate is exact + Geom::ConvexHull bezhull(bz); + +// calculate first derivatives of x and y wrt t + + for (int i = 0; i < 2; ++i) { + xprime[i] = sb[X][0][1] - sb[X][0][0]; + yprime[i] = sb[Y][0][1] - sb[Y][0][0]; + } + if (sb[X].size() > 1) { + xprime[0] += sb[X][1][0]; + xprime[1] -= sb[X][1][1]; + } + if (sb[Y].size() > 1) { + yprime[0] += sb[Y][1][0]; + yprime[1] -= sb[Y][1][1]; + } + +// calculate midpoint at t = 0.5 + + div = 2; + for (size_t i = 0; i < sb[X].size(); ++i) { + midx += (sb[X][i][0] + sb[X][i][1])/div; + div *= 4; + } + + div = 2; + for (size_t i = 0; i < sb[Y].size(); ++i) { + midy += (sb[Y][i][0] + sb[Y][i][1])/div; + div *= 4; + } + +// is midpoint in hull: if not, the solution will be ill-conditioned, LP Bug 1428683 + + if (!bezhull.contains(Geom::Point(midx, midy))) + return; + +// calculate Bezier control arms + + midx = 8*midx - 4*bz[0][X] - 4*bz[3][X]; // re-define relative to center + midy = 8*midy - 4*bz[0][Y] - 4*bz[3][Y]; + midx_0 = sb[X][1][0] + sb[X][1][1]; // zeroth order estimate + midy_0 = sb[Y][1][0] + sb[Y][1][1]; + + if ((std::abs(xprime[0]) < EPSILON) && (std::abs(yprime[0]) < EPSILON) + && ((std::abs(xprime[1]) > EPSILON) || (std::abs(yprime[1]) > EPSILON))) { // degenerate handle at 0 : use distance of closest approach + numer[0] = midx*xprime[1] + midy*yprime[1]; + denom = 3.0*(xprime[1]*xprime[1] + yprime[1]*yprime[1]); + delx[0] = 0; + dely[0] = 0; + delx[1] = -xprime[1]*numer[0]/denom; + dely[1] = -yprime[1]*numer[0]/denom; + } else if ((std::abs(xprime[1]) < EPSILON) && (std::abs(yprime[1]) < EPSILON) + && ((std::abs(xprime[0]) > EPSILON) || (std::abs(yprime[0]) > EPSILON))) { // degenerate handle at 1 : ditto + numer[1] = midx*xprime[0] + midy*yprime[0]; + denom = 3.0*(xprime[0]*xprime[0] + yprime[0]*yprime[0]); + delx[0] = xprime[0]*numer[1]/denom; + dely[0] = yprime[0]*numer[1]/denom; + delx[1] = 0; + dely[1] = 0; + } else if (std::abs(xprime[1]*yprime[0] - yprime[1]*xprime[0]) > // general case : fit mid fxn value + 0.002 * std::abs(xprime[1]*xprime[0] + yprime[1]*yprime[0])) { // approx. 0.1 degree of angle + double test1 = (bz[1][Y] - bz[0][Y])*(bz[3][X] - bz[0][X]) - (bz[1][X] - bz[0][X])*(bz[3][Y] - bz[0][Y]); + double test2 = (bz[2][Y] - bz[0][Y])*(bz[3][X] - bz[0][X]) - (bz[2][X] - bz[0][X])*(bz[3][Y] - bz[0][Y]); + if (test1*test2 < 0) // reject anti-symmetric case, LP Bug 1428267 & Bug 1428683 + return; + denom = 3.0*(xprime[1]*yprime[0] - yprime[1]*xprime[0]); + for (int i = 0; i < 2; ++i) { + numer_0[i] = xprime[1 - i]*midy_0 - yprime[1 - i]*midx_0; + numer[i] = xprime[1 - i]*midy - yprime[1 - i]*midx; + delx[i] = xprime[i]*numer[i]/denom; + dely[i] = yprime[i]*numer[i]/denom; + if (numer_0[i]*numer[i] < 0) // check for reversal of direction, LP Bug 1544680 + return; + } + if (std::abs((numer[0] - numer_0[0])*numer_0[1]) > 10.0*std::abs((numer[1] - numer_0[1])*numer_0[0]) // check for asymmetry + || std::abs((numer[1] - numer_0[1])*numer_0[0]) > 10.0*std::abs((numer[0] - numer_0[0])*numer_0[1])) + return; + } else if ((xprime[0]*xprime[1] < 0) || (yprime[0]*yprime[1] < 0)) { // symmetric case : use distance of closest approach + numer[0] = midx*xprime[0] + midy*yprime[0]; + denom = 6.0*(xprime[0]*xprime[0] + yprime[0]*yprime[0]); + delx[0] = xprime[0]*numer[0]/denom; + dely[0] = yprime[0]*numer[0]/denom; + delx[1] = -delx[0]; + dely[1] = -dely[0]; + } else { // anti-symmetric case : fit mid slope + // calculate slope at t = 0.5 + midx = 0; + div = 1; + for (size_t i = 0; i < sb[X].size(); ++i) { + midx += (sb[X][i][1] - sb[X][i][0])/div; + div *= 4; + } + midy = 0; + div = 1; + for (size_t i = 0; i < sb[Y].size(); ++i) { + midy += (sb[Y][i][1] - sb[Y][i][0])/div; + div *= 4; + } + if (midx*yprime[0] != midy*xprime[0]) { + denom = midx*yprime[0] - midy*xprime[0]; + numer[0] = midx*(bz[3][Y] - bz[0][Y]) - midy*(bz[3][X] - bz[0][X]); + for (int i = 0; i < 2; ++i) { + delx[i] = xprime[0]*numer[0]/denom; + dely[i] = yprime[0]*numer[0]/denom; + } + } else { // linear case + for (int i = 0; i < 2; ++i) { + delx[i] = (bz[3][X] - bz[0][X])/3; + dely[i] = (bz[3][Y] - bz[0][Y])/3; + } + } + } + bz[1][X] = bz[0][X] + delx[0]; + bz[1][Y] = bz[0][Y] + dely[0]; + bz[2][X] = bz[3][X] - delx[1]; + bz[2][Y] = bz[3][Y] - dely[1]; +} + +/** Changes the basis of p to be sbasis. + \param p the Bernstein basis polynomial + \returns the Symmetric basis polynomial + + if the degree is even q is the order in the symmetrical power basis, + if the degree is odd q is the order + 1 + n is always the polynomial degree, i. e. the Bezier order +*/ +void bezier_to_sbasis (SBasis & sb, Bezier const& bz) +{ + size_t n = bz.order(); + size_t q = (n+1) / 2; + size_t even = (n & 1u) ? 0 : 1; + sb.clear(); + sb.resize(q + even, Linear(0, 0)); + double Tjk; + for (size_t k = 0; k < q; ++k) + { + for (size_t j = k; j < q; ++j) + { + Tjk = sgn(j, k) * binomial(n-j-k, j-k) * binomial(n, k); + sb[j][0] += (Tjk * bz[k]); + sb[j][1] += (Tjk * bz[n-k]); // n-j <-> [j][1] + } + for (size_t j = k+1; j < q; ++j) + { + Tjk = sgn(j, k) * binomial(n-j-k-1, j-k-1) * binomial(n, k); + sb[j][0] += (Tjk * bz[n-k]); + sb[j][1] += (Tjk * bz[k]); // n-j <-> [j][1] + } + } + if (even) + { + for (size_t k = 0; k < q; ++k) + { + Tjk = sgn(q,k) * binomial(n, k); + sb[q][0] += (Tjk * (bz[k] + bz[n-k])); + } + sb[q][0] += (binomial(n, q) * bz[q]); + sb[q][1] = sb[q][0]; + } + sb[0][0] = bz[0]; + sb[0][1] = bz[n]; +} + + +/** Changes the basis of d2 p to be sbasis. + \param p the d2 Bernstein basis polynomial + \returns the d2 Symmetric basis polynomial + + if the degree is even q is the order in the symmetrical power basis, + if the degree is odd q is the order + 1 + n is always the polynomial degree, i. e. the Bezier order +*/ +void bezier_to_sbasis (D2 & sb, std::vector const& bz) +{ + size_t n = bz.size() - 1; + size_t q = (n+1) / 2; + size_t even = (n & 1u) ? 0 : 1; + sb[X].clear(); + sb[Y].clear(); + sb[X].resize(q + even, Linear(0, 0)); + sb[Y].resize(q + even, Linear(0, 0)); + double Tjk; + for (size_t k = 0; k < q; ++k) + { + for (size_t j = k; j < q; ++j) + { + Tjk = sgn(j, k) * binomial(n-j-k, j-k) * binomial(n, k); + sb[X][j][0] += (Tjk * bz[k][X]); + sb[X][j][1] += (Tjk * bz[n-k][X]); + sb[Y][j][0] += (Tjk * bz[k][Y]); + sb[Y][j][1] += (Tjk * bz[n-k][Y]); + } + for (size_t j = k+1; j < q; ++j) + { + Tjk = sgn(j, k) * binomial(n-j-k-1, j-k-1) * binomial(n, k); + sb[X][j][0] += (Tjk * bz[n-k][X]); + sb[X][j][1] += (Tjk * bz[k][X]); + sb[Y][j][0] += (Tjk * bz[n-k][Y]); + sb[Y][j][1] += (Tjk * bz[k][Y]); + } + } + if (even) + { + for (size_t k = 0; k < q; ++k) + { + Tjk = sgn(q,k) * binomial(n, k); + sb[X][q][0] += (Tjk * (bz[k][X] + bz[n-k][X])); + sb[Y][q][0] += (Tjk * (bz[k][Y] + bz[n-k][Y])); + } + sb[X][q][0] += (binomial(n, q) * bz[q][X]); + sb[X][q][1] = sb[X][q][0]; + sb[Y][q][0] += (binomial(n, q) * bz[q][Y]); + sb[Y][q][1] = sb[Y][q][0]; + } + sb[X][0][0] = bz[0][X]; + sb[X][0][1] = bz[n][X]; + sb[Y][0][0] = bz[0][Y]; + sb[Y][0][1] = bz[n][Y]; +} + + +} // end namespace Geom + + +#if 0 +/* +* This version works by inverting a reasonable upper bound on the error term after subdividing the +* curve at $a$. We keep biting off pieces until there is no more curve left. +* +* Derivation: The tail of the power series is $a_ks^k + a_{k+1}s^{k+1} + \ldots = e$. A +* subdivision at $a$ results in a tail error of $e*A^k, A = (1-a)a$. Let this be the desired +* tolerance tol $= e*A^k$ and invert getting $A = e^{1/k}$ and $a = 1/2 - \sqrt{1/4 - A}$ +*/ +void +subpath_from_sbasis_incremental(Geom::OldPathSetBuilder &pb, D2 B, double tol, bool initial) { + const unsigned k = 2; // cubic bezier + double te = B.tail_error(k); + assert(B[0].std::isfinite()); + assert(B[1].std::isfinite()); + + //std::cout << "tol = " << tol << std::endl; + while(1) { + double A = std::sqrt(tol/te); // pow(te, 1./k) + double a = A; + if(A < 1) { + A = std::min(A, 0.25); + a = 0.5 - std::sqrt(0.25 - A); // quadratic formula + if(a > 1) a = 1; // clamp to the end of the segment + } else + a = 1; + assert(a > 0); + //std::cout << "te = " << te << std::endl; + //std::cout << "A = " << A << "; a=" << a << std::endl; + D2 Bs = compose(B, Linear(0, a)); + assert(Bs.tail_error(k)); + std::vector bez = sbasis_to_bezier(Bs, 2); + reverse(bez.begin(), bez.end()); + if (initial) { + pb.start_subpath(bez[0]); + initial = false; + } + pb.push_cubic(bez[1], bez[2], bez[3]); + +// move to next piece of curve + if(a >= 1) break; + B = compose(B, Linear(a, 1)); + te = B.tail_error(k); + } +} + +#endif + +namespace Geom{ + +/** Make a path from a d2 sbasis. + \param p the d2 Symmetric basis polynomial + \returns a Path + + If only_cubicbeziers is true, the resulting path may only contain CubicBezier curves. +*/ +void build_from_sbasis(Geom::PathBuilder &pb, D2 const &B, double tol, bool only_cubicbeziers) { + if (!B.isFinite()) { + THROW_EXCEPTION("assertion failed: B.isFinite()"); + } + if(tail_error(B, 3) < tol || sbasis_size(B) == 2) { // nearly cubic enough + if( !only_cubicbeziers && (sbasis_size(B) <= 1) ) { + pb.lineTo(B.at1()); + } else { + std::vector bez; +// sbasis_to_bezier(bez, B, 4); + sbasis_to_cubic_bezier(bez, B); + pb.curveTo(bez[1], bez[2], bez[3]); + } + } else { + build_from_sbasis(pb, compose(B, Linear(0, 0.5)), tol, only_cubicbeziers); + build_from_sbasis(pb, compose(B, Linear(0.5, 1)), tol, only_cubicbeziers); + } +} + +/** Make a path from a d2 sbasis. + \param p the d2 Symmetric basis polynomial + \returns a Path + + If only_cubicbeziers is true, the resulting path may only contain CubicBezier curves. +*/ +Path +path_from_sbasis(D2 const &B, double tol, bool only_cubicbeziers) { + PathBuilder pb; + pb.moveTo(B.at0()); + build_from_sbasis(pb, B, tol, only_cubicbeziers); + pb.flush(); + return pb.peek().front(); +} + +/** Make a path from a d2 sbasis. + \param p the d2 Symmetric basis polynomial + \returns a Path + + If only_cubicbeziers is true, the resulting path may only contain CubicBezier curves. + TODO: some of this logic should be lifted into svg-path +*/ +PathVector +path_from_piecewise(Geom::Piecewise > const &B, double tol, bool only_cubicbeziers) { + Geom::PathBuilder pb; + if(B.size() == 0) return pb.peek(); + Geom::Point start = B[0].at0(); + pb.moveTo(start); + for(unsigned i = 0; ; i++) { + if ( (i+1 == B.size()) + || !are_near(B[i+1].at0(), B[i].at1(), tol) ) + { + //start of a new path + if (are_near(start, B[i].at1()) && sbasis_size(B[i]) <= 1) { + pb.closePath(); + //last line seg already there (because of .closePath()) + goto no_add; + } + build_from_sbasis(pb, B[i], tol, only_cubicbeziers); + if (are_near(start, B[i].at1())) { + //it's closed, the last closing segment was not a straight line so it needed to be added, but still make it closed here with degenerate straight line. + pb.closePath(); + } + no_add: + if (i+1 >= B.size()) { + break; + } + start = B[i+1].at0(); + pb.moveTo(start); + } else { + build_from_sbasis(pb, B[i], tol, only_cubicbeziers); + } + } + pb.flush(); + return pb.peek(); +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis-to-bezier.h b/src/2geom/sbasis-to-bezier.h new file mode 100644 index 0000000..eadb47b --- /dev/null +++ b/src/2geom/sbasis-to-bezier.h @@ -0,0 +1,87 @@ +/** + * \file + * \brief Conversion between SBasis and Bezier basis polynomials + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_TO_BEZIER_H +#define LIB2GEOM_SEEN_SBASIS_TO_BEZIER_H + +#include <2geom/d2.h> +#include <2geom/pathvector.h> + +#include + +namespace Geom { + +class PathBuilder; + +void sbasis_to_bezier (Bezier &bz, SBasis const &sb, size_t sz = 0); +void sbasis_to_bezier (D2 &bz, D2 const &sb, size_t sz = 0); +void sbasis_to_bezier (std::vector & bz, D2 const& sb, size_t sz = 0); +void sbasis_to_cubic_bezier (std::vector & bz, D2 const& sb); +void bezier_to_sbasis (SBasis & sb, Bezier const& bz); +void bezier_to_sbasis (D2 & sb, std::vector const& bz); +void build_from_sbasis(PathBuilder &pb, D2 const &B, double tol, bool only_cubicbeziers); + +#if 0 +// this produces a degree k bezier from a degree k sbasis +Bezier +sbasis_to_bezier(SBasis const &B, unsigned q = 0); + +// inverse +SBasis bezier_to_sbasis(Bezier const &B); + + +std::vector +sbasis_to_bezier(D2 const &B, unsigned q = 0); +#endif + + +PathVector path_from_piecewise(Piecewise > const &B, double tol, bool only_cubicbeziers = false); + +Path path_from_sbasis(D2 const &B, double tol, bool only_cubicbeziers = false); +inline Path cubicbezierpath_from_sbasis(D2 const &B, double tol) + { return path_from_sbasis(B, tol, true); } + +} // end namespace Geom + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sbasis.cpp b/src/2geom/sbasis.cpp new file mode 100644 index 0000000..3efc227 --- /dev/null +++ b/src/2geom/sbasis.cpp @@ -0,0 +1,681 @@ +/* + * sbasis.cpp - S-power basis function class + supporting classes + * + * Authors: + * Nathan Hurst + * Michael Sloan + * + * Copyright (C) 2006-2007 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include + +#include <2geom/sbasis.h> +#include <2geom/math-utils.h> + +namespace Geom { + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +/** bound the error from term truncation + \param tail first term to chop + \returns the largest possible error this truncation could give +*/ +double SBasis::tailError(unsigned tail) const { + Interval bs = *bounds_fast(*this, tail); + return std::max(fabs(bs.min()),fabs(bs.max())); +} + +/** test all coefficients are finite +*/ +bool SBasis::isFinite() const { + for(unsigned i = 0; i < size(); i++) { + if(!(*this)[i].isFinite()) + return false; + } + return true; +} + +/** Compute the value and the first n derivatives + \param t position to evaluate + \param n number of derivatives (not counting value) + \returns a vector with the value and the n derivative evaluations + +There is an elegant way to compute the value and n derivatives for a polynomial using a variant of horner's rule. Someone will someday work out how for sbasis. +*/ +std::vector SBasis::valueAndDerivatives(double t, unsigned n) const { + std::vector ret(n+1); + ret[0] = valueAt(t); + SBasis tmp = *this; + for(unsigned i = 1; i < n+1; i++) { + tmp.derive(); + ret[i] = tmp.valueAt(t); + } + return ret; +} + + +/** Compute the pointwise sum of a and b (Exact) + \param a,b sbasis functions + \returns sbasis equal to a+b + +*/ +SBasis operator+(const SBasis& a, const SBasis& b) { + const unsigned out_size = std::max(a.size(), b.size()); + const unsigned min_size = std::min(a.size(), b.size()); + SBasis result(out_size, Linear()); + + for(unsigned i = 0; i < min_size; i++) { + result[i] = a[i] + b[i]; + } + for(unsigned i = min_size; i < a.size(); i++) + result[i] = a[i]; + for(unsigned i = min_size; i < b.size(); i++) + result[i] = b[i]; + + assert(result.size() == out_size); + return result; +} + +/** Compute the pointwise difference of a and b (Exact) + \param a,b sbasis functions + \returns sbasis equal to a-b + +*/ +SBasis operator-(const SBasis& a, const SBasis& b) { + const unsigned out_size = std::max(a.size(), b.size()); + const unsigned min_size = std::min(a.size(), b.size()); + SBasis result(out_size, Linear()); + + for(unsigned i = 0; i < min_size; i++) { + result[i] = a[i] - b[i]; + } + for(unsigned i = min_size; i < a.size(); i++) + result[i] = a[i]; + for(unsigned i = min_size; i < b.size(); i++) + result[i] = -b[i]; + + assert(result.size() == out_size); + return result; +} + +/** Compute the pointwise sum of a and b and store in a (Exact) + \param a,b sbasis functions + \returns sbasis equal to a+b + +*/ +SBasis& operator+=(SBasis& a, const SBasis& b) { + const unsigned out_size = std::max(a.size(), b.size()); + const unsigned min_size = std::min(a.size(), b.size()); + a.resize(out_size); + + for(unsigned i = 0; i < min_size; i++) + a[i] += b[i]; + for(unsigned i = min_size; i < b.size(); i++) + a[i] = b[i]; + + assert(a.size() == out_size); + return a; +} + +/** Compute the pointwise difference of a and b and store in a (Exact) + \param a,b sbasis functions + \returns sbasis equal to a-b + +*/ +SBasis& operator-=(SBasis& a, const SBasis& b) { + const unsigned out_size = std::max(a.size(), b.size()); + const unsigned min_size = std::min(a.size(), b.size()); + a.resize(out_size); + + for(unsigned i = 0; i < min_size; i++) + a[i] -= b[i]; + for(unsigned i = min_size; i < b.size(); i++) + a[i] = -b[i]; + + assert(a.size() == out_size); + return a; +} + +/** Compute the pointwise product of a and b (Exact) + \param a,b sbasis functions + \returns sbasis equal to a*b + +*/ +SBasis operator*(SBasis const &a, double k) { + SBasis c(a.size(), Linear()); + for(unsigned i = 0; i < a.size(); i++) + c[i] = a[i] * k; + return c; +} + +/** Compute the pointwise product of a and b and store the value in a (Exact) + \param a,b sbasis functions + \returns sbasis equal to a*b + +*/ +SBasis& operator*=(SBasis& a, double b) { + if (a.isZero()) return a; + if (b == 0) + a.clear(); + else + for(unsigned i = 0; i < a.size(); i++) + a[i] *= b; + return a; +} + +/** multiply a by x^sh in place (Exact) + \param a sbasis function + \param sh power + \returns a + +*/ +SBasis shift(SBasis const &a, int sh) { + size_t n = a.size()+sh; + SBasis c(n, Linear()); + size_t m = std::max(0, sh); + + for(int i = 0; i < sh; i++) + c[i] = Linear(0,0); + for(size_t i = m, j = std::max(0,-sh); i < n; i++, j++) + c[i] = a[j]; + return c; +} + +/** multiply a by x^sh (Exact) + \param a linear function + \param sh power + \returns a* x^sh + +*/ +SBasis shift(Linear const &a, int sh) { + size_t n = 1+sh; + SBasis c(n, Linear()); + + for(int i = 0; i < sh; i++) + c[i] = Linear(0,0); + if(sh >= 0) + c[sh] = a; + return c; +} + +#if 0 +SBasis multiply(SBasis const &a, SBasis const &b) { + // c = {a0*b0 - shift(1, a.Tri*b.Tri), a1*b1 - shift(1, a.Tri*b.Tri)} + + // shift(1, a.Tri*b.Tri) + SBasis c(a.size() + b.size(), Linear(0,0)); + if(a.isZero() || b.isZero()) + return c; + for(unsigned j = 0; j < b.size(); j++) { + for(unsigned i = j; i < a.size()+j; i++) { + double tri = b[j].tri()*a[i-j].tri(); + c[i+1/*shift*/] += Linear(-tri); + } + } + for(unsigned j = 0; j < b.size(); j++) { + for(unsigned i = j; i < a.size()+j; i++) { + for(unsigned dim = 0; dim < 2; dim++) + c[i][dim] += b[j][dim]*a[i-j][dim]; + } + } + c.normalize(); + //assert(!(0 == c.back()[0] && 0 == c.back()[1])); + return c; +} +#else + +/** Compute the pointwise product of a and b adding c (Exact) + \param a,b,c sbasis functions + \returns sbasis equal to a*b+c + +The added term is almost free +*/ +SBasis multiply_add(SBasis const &a, SBasis const &b, SBasis c) { + if(a.isZero() || b.isZero()) + return c; + c.resize(a.size() + b.size(), Linear(0,0)); + for(unsigned j = 0; j < b.size(); j++) { + for(unsigned i = j; i < a.size()+j; i++) { + double tri = b[j].tri()*a[i-j].tri(); + c[i+1/*shift*/] += Linear(-tri); + } + } + for(unsigned j = 0; j < b.size(); j++) { + for(unsigned i = j; i < a.size()+j; i++) { + for(unsigned dim = 0; dim < 2; dim++) + c[i][dim] += b[j][dim]*a[i-j][dim]; + } + } + c.normalize(); + //assert(!(0 == c.back()[0] && 0 == c.back()[1])); + return c; +} + +/** Compute the pointwise product of a and b (Exact) + \param a,b sbasis functions + \returns sbasis equal to a*b + +*/ +SBasis multiply(SBasis const &a, SBasis const &b) { + if(a.isZero() || b.isZero()) { + SBasis c(1, Linear(0,0)); + return c; + } + SBasis c(a.size() + b.size(), Linear(0,0)); + return multiply_add(a, b, c); +} +#endif +/** Compute the integral of a (Exact) + \param a sbasis functions + \returns sbasis integral(a) + +*/ +SBasis integral(SBasis const &c) { + SBasis a; + a.resize(c.size() + 1, Linear(0,0)); + a[0] = Linear(0,0); + + for(unsigned k = 1; k < c.size() + 1; k++) { + double ahat = -c[k-1].tri()/(2*k); + a[k][0] = a[k][1] = ahat; + } + double aTri = 0; + for(int k = c.size()-1; k >= 0; k--) { + aTri = (c[k].hat() + (k+1)*aTri/2)/(2*k+1); + a[k][0] -= aTri/2; + a[k][1] += aTri/2; + } + a.normalize(); + return a; +} + +/** Compute the derivative of a (Exact) + \param a sbasis functions + \returns sbasis da/dt + +*/ +SBasis derivative(SBasis const &a) { + SBasis c; + c.resize(a.size(), Linear(0,0)); + if(a.isZero()) + return c; + + for(unsigned k = 0; k < a.size()-1; k++) { + double d = (2*k+1)*(a[k][1] - a[k][0]); + + c[k][0] = d + (k+1)*a[k+1][0]; + c[k][1] = d - (k+1)*a[k+1][1]; + } + int k = a.size()-1; + double d = (2*k+1)*(a[k][1] - a[k][0]); + if (d == 0 && k > 0) { + c.pop_back(); + } else { + c[k][0] = d; + c[k][1] = d; + } + + return c; +} + +/** Compute the derivative of this inplace (Exact) + +*/ +void SBasis::derive() { // in place version + if(isZero()) return; + for(unsigned k = 0; k < size()-1; k++) { + double d = (2*k+1)*((*this)[k][1] - (*this)[k][0]); + + (*this)[k][0] = d + (k+1)*(*this)[k+1][0]; + (*this)[k][1] = d - (k+1)*(*this)[k+1][1]; + } + int k = size()-1; + double d = (2*k+1)*((*this)[k][1] - (*this)[k][0]); + if (d == 0 && k > 0) { + pop_back(); + } else { + (*this)[k][0] = d; + (*this)[k][1] = d; + } +} + +/** Compute the sqrt of a + \param a sbasis functions + \returns sbasis \f[ \sqrt{a} \f] + +It is recommended to use the piecewise version unless you have good reason. +TODO: convert int k to unsigned k, and remove cast +*/ +SBasis sqrt(SBasis const &a, int k) { + SBasis c; + if(a.isZero() || k == 0) + return c; + c.resize(k, Linear(0,0)); + c[0] = Linear(std::sqrt(a[0][0]), std::sqrt(a[0][1])); + SBasis r = a - multiply(c, c); // remainder + + for(unsigned i = 1; i <= (unsigned)k && i= 0; i--) { + r = multiply_add(r, s, SBasis(Linear(a[i][0])) - b*a[i][0] + b*a[i][1]); + } + return r; +} + +/** Compute a composed with b to k terms + \param a,b sbasis functions + \returns sbasis a(b(t)) + + return a0 + s(a1 + s(a2 +... where s = (1-u)u; ak =(1 - u)a^0_k + ua^1_k +*/ +SBasis compose(SBasis const &a, SBasis const &b, unsigned k) { + SBasis s = multiply((SBasis(Linear(1,1))-b), b); + SBasis r; + + for(int i = a.size()-1; i >= 0; i--) { + r = multiply_add(r, s, SBasis(Linear(a[i][0])) - b*a[i][0] + b*a[i][1]); + } + r.truncate(k); + return r; +} + +SBasis portion(const SBasis &t, double from, double to) { + double fv = t.valueAt(from); + double tv = t.valueAt(to); + SBasis ret = compose(t, Linear(from, to)); + ret.at0() = fv; + ret.at1() = tv; + return ret; +} + +/* +Inversion algorithm. The notation is certainly very misleading. The +pseudocode should say: + +c(v) := 0 +r(u) := r_0(u) := u +for i:=0 to k do + c_i(v) := H_0(r_i(u)/(t_1)^i; u) + c(v) := c(v) + c_i(v)*t^i + r(u) := r(u) ? c_i(u)*(t(u))^i +endfor +*/ + +//#define DEBUG_INVERSION 1 + +/** find the function a^-1 such that a^-1 composed with a to k terms is the identity function + \param a sbasis function + \returns sbasis a^-1 s.t. a^-1(a(t)) = 1 + + The function must have 'unit range'("a00 = 0 and a01 = 1") and be monotonic. +*/ +SBasis inverse(SBasis a, int k) { + assert(a.size() > 0); + double a0 = a[0][0]; + if(a0 != 0) { + a -= a0; + } + double a1 = a[0][1]; + assert(a1 != 0); // not invertable. + + if(a1 != 1) { + a /= a1; + } + SBasis c(k, Linear()); // c(v) := 0 + if(a.size() >= 2 && k == 2) { + c[0] = Linear(0,1); + Linear t1(1+a[1][0], 1-a[1][1]); // t_1 + c[1] = Linear(-a[1][0]/t1[0], -a[1][1]/t1[1]); + } else if(a.size() >= 2) { // non linear + SBasis r = Linear(0,1); // r(u) := r_0(u) := u + Linear t1(1./(1+a[1][0]), 1./(1-a[1][1])); // 1./t_1 + Linear one(1,1); + Linear t1i = one; // t_1^0 + SBasis one_minus_a = SBasis(one) - a; + SBasis t = multiply(one_minus_a, a); // t(u) + SBasis ti(one); // t(u)^0 +#ifdef DEBUG_INVERSION + std::cout << "a=" << a << std::endl; + std::cout << "1-a=" << one_minus_a << std::endl; + std::cout << "t1=" << t1 << std::endl; + //assert(t1 == t[1]); +#endif + + //c.resize(k+1, Linear(0,0)); + for(unsigned i = 0; i < (unsigned)k; i++) { // for i:=0 to k do +#ifdef DEBUG_INVERSION + std::cout << "-------" << i << ": ---------" < + * Michael Sloan + * + * Copyright (C) 2006-2007 authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SBASIS_H +#define LIB2GEOM_SEEN_SBASIS_H +#include +#include +#include + +#include <2geom/linear.h> +#include <2geom/interval.h> +#include <2geom/utils.h> +#include <2geom/exception.h> + +//#define USE_SBASISN 1 + + +#if defined(USE_SBASIS_OF) + +#include "sbasis-of.h" + +#elif defined(USE_SBASISN) + +#include "sbasisN.h" +namespace Geom{ + +/*** An empty SBasis is identically 0. */ +class SBasis : public SBasisN<1>; + +}; +#else + +namespace Geom { + +/** + * @brief Polynomial in symmetric power basis + * @ingroup Fragments + */ +class SBasis { + std::vector d; + void push_back(Linear const&l) { d.push_back(l); } + +public: + // As part of our migration away from SBasis isa vector we provide this minimal set of vector interface methods. + size_t size() const {return d.size();} + typedef std::vector::iterator iterator; + typedef std::vector::const_iterator const_iterator; + Linear operator[](unsigned i) const { + return d[i]; + } + Linear& operator[](unsigned i) { return d.at(i); } + const_iterator begin() const { return d.begin();} + const_iterator end() const { return d.end();} + iterator begin() { return d.begin();} + iterator end() { return d.end();} + bool empty() const { return d.size() == 1 && d[0][0] == 0 && d[0][1] == 0; } + Linear &back() {return d.back();} + Linear const &back() const {return d.back();} + void pop_back() { + if (d.size() > 1) { + d.pop_back(); + } else { + d[0][0] = 0; + d[0][1] = 0; + } + } + void resize(unsigned n) { d.resize(std::max(n, 1));} + void resize(unsigned n, Linear const& l) { d.resize(std::max(n, 1), l);} + void reserve(unsigned n) { d.reserve(n);} + void clear() { + d.resize(1); + d[0][0] = 0; + d[0][1] = 0; + } + void insert(iterator before, const_iterator src_begin, const_iterator src_end) { d.insert(before, src_begin, src_end);} + Linear& at(unsigned i) { return d.at(i);} + //void insert(Linear* before, int& n, Linear const &l) { d.insert(std::vector::iterator(before), n, l);} + bool operator==(SBasis const&B) const { return d == B.d;} + bool operator!=(SBasis const&B) const { return d != B.d;} + + SBasis() + : d(1, Linear(0, 0)) + {} + explicit SBasis(double a) + : d(1, Linear(a, a)) + {} + explicit SBasis(double a, double b) + : d(1, Linear(a, b)) + {} + SBasis(SBasis const &a) + : d(a.d) + {} + SBasis(std::vector const &ls) + : d(ls) + {} + SBasis(Linear const &bo) + : d(1, bo) + {} + SBasis(Linear* bo) + : d(1, bo ? *bo : Linear(0, 0)) + {} + explicit SBasis(size_t n, Linear const&l) : d(n, l) {} + + SBasis(Coord c0, Coord c1, Coord c2, Coord c3) + : d(2) + { + d[0][0] = c0; + d[1][0] = c1; + d[1][1] = c2; + d[0][1] = c3; + } + SBasis(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, Coord c5) + : d(3) + { + d[0][0] = c0; + d[1][0] = c1; + d[2][0] = c2; + d[2][1] = c3; + d[1][1] = c4; + d[0][1] = c5; + } + SBasis(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, Coord c5, + Coord c6, Coord c7) + : d(4) + { + d[0][0] = c0; + d[1][0] = c1; + d[2][0] = c2; + d[3][0] = c3; + d[3][1] = c4; + d[2][1] = c5; + d[1][1] = c6; + d[0][1] = c7; + } + SBasis(Coord c0, Coord c1, Coord c2, Coord c3, Coord c4, Coord c5, + Coord c6, Coord c7, Coord c8, Coord c9) + : d(5) + { + d[0][0] = c0; + d[1][0] = c1; + d[2][0] = c2; + d[3][0] = c3; + d[4][0] = c4; + d[4][1] = c5; + d[3][1] = c6; + d[2][1] = c7; + d[1][1] = c8; + d[0][1] = c9; + } + + // construct from a sequence of coefficients + template + SBasis(Iter first, Iter last) { + assert(std::distance(first, last) % 2 == 0); + assert(std::distance(first, last) >= 2); + for (; first != last; ++first) { + --last; + push_back(Linear(*first, *last)); + } + } + + //IMPL: FragmentConcept + typedef double output_type; + inline bool isZero(double eps=EPSILON) const { + assert(size() > 0); + for(unsigned i = 0; i < size(); i++) { + if(!(*this)[i].isZero(eps)) return false; + } + return true; + } + inline bool isConstant(double eps=EPSILON) const { + assert(size() > 0); + if(!(*this)[0].isConstant(eps)) return false; + for (unsigned i = 1; i < size(); i++) { + if(!(*this)[i].isZero(eps)) return false; + } + return true; + } + + bool isFinite() const; + inline Coord at0() const { return (*this)[0][0]; } + inline Coord &at0() { return (*this)[0][0]; } + inline Coord at1() const { return (*this)[0][1]; } + inline Coord &at1() { return (*this)[0][1]; } + + int degreesOfFreedom() const { return size()*2;} + + double valueAt(double t) const { + assert(size() > 0); + double s = t*(1-t); + double p0 = 0, p1 = 0; + for(unsigned k = size(); k > 0; k--) { + const Linear &lin = (*this)[k-1]; + p0 = p0*s + lin[0]; + p1 = p1*s + lin[1]; + } + return (1-t)*p0 + t*p1; + } + //double valueAndDerivative(double t, double &der) const { + //} + double operator()(double t) const { + return valueAt(t); + } + + std::vector valueAndDerivatives(double t, unsigned n) const; + + SBasis toSBasis() const { return SBasis(*this); } + + double tailError(unsigned tail) const; + +// compute f(g) + SBasis operator()(SBasis const & g) const; + +//MUTATOR PRISON + //remove extra zeros + void normalize() { + while(size() > 1 && back().isZero(0)) + pop_back(); + } + + void truncate(unsigned k) { if(k < size()) resize(std::max(k, 1)); } +private: + void derive(); // in place version +}; + +//TODO: figure out how to stick this in linear, while not adding an sbasis dep +inline SBasis Linear::toSBasis() const { return SBasis(*this); } + +//implemented in sbasis-roots.cpp +OptInterval bounds_exact(SBasis const &a); +OptInterval bounds_fast(SBasis const &a, int order = 0); +OptInterval bounds_local(SBasis const &a, const OptInterval &t, int order = 0); + +/** Returns a function which reverses the domain of a. + \param a sbasis function + \relates SBasis + +useful for reversing a parameteric curve. +*/ +inline SBasis reverse(SBasis const &a) { + SBasis result(a.size(), Linear()); + + for(unsigned k = 0; k < a.size(); k++) + result[k] = reverse(a[k]); + return result; +} + +//IMPL: ScalableConcept +inline SBasis operator-(const SBasis& p) { + if(p.isZero()) return SBasis(); + SBasis result(p.size(), Linear()); + + for(unsigned i = 0; i < p.size(); i++) { + result[i] = -p[i]; + } + return result; +} +SBasis operator*(SBasis const &a, double k); +inline SBasis operator*(double k, SBasis const &a) { return a*k; } +inline SBasis operator/(SBasis const &a, double k) { return a*(1./k); } +SBasis& operator*=(SBasis& a, double b); +inline SBasis& operator/=(SBasis& a, double b) { return (a*=(1./b)); } + +//IMPL: AddableConcept +SBasis operator+(const SBasis& a, const SBasis& b); +SBasis operator-(const SBasis& a, const SBasis& b); +SBasis& operator+=(SBasis& a, const SBasis& b); +SBasis& operator-=(SBasis& a, const SBasis& b); + +//TODO: remove? +/*inline SBasis operator+(const SBasis & a, Linear const & b) { + if(b.isZero()) return a; + if(a.isZero()) return b; + SBasis result(a); + result[0] += b; + return result; +} +inline SBasis operator-(const SBasis & a, Linear const & b) { + if(b.isZero()) return a; + SBasis result(a); + result[0] -= b; + return result; +} +inline SBasis& operator+=(SBasis& a, const Linear& b) { + if(a.isZero()) + a.push_back(b); + else + a[0] += b; + return a; +} +inline SBasis& operator-=(SBasis& a, const Linear& b) { + if(a.isZero()) + a.push_back(-b); + else + a[0] -= b; + return a; + }*/ + +//IMPL: OffsetableConcept +inline SBasis operator+(const SBasis & a, double b) { + if(a.isZero()) return Linear(b, b); + SBasis result(a); + result[0] += b; + return result; +} +inline SBasis operator-(const SBasis & a, double b) { + if(a.isZero()) return Linear(-b, -b); + SBasis result(a); + result[0] -= b; + return result; +} +inline SBasis& operator+=(SBasis& a, double b) { + if(a.isZero()) + a = SBasis(Linear(b,b)); + else + a[0] += b; + return a; +} +inline SBasis& operator-=(SBasis& a, double b) { + if(a.isZero()) + a = SBasis(Linear(-b,-b)); + else + a[0] -= b; + return a; +} + +SBasis shift(SBasis const &a, int sh); +SBasis shift(Linear const &a, int sh); + +inline SBasis truncate(SBasis const &a, unsigned terms) { + SBasis c; + c.insert(c.begin(), a.begin(), a.begin() + std::min(terms, (unsigned)a.size())); + return c; +} + +SBasis multiply(SBasis const &a, SBasis const &b); +// This performs a multiply and accumulate operation in about the same time as multiply. return a*b + c +SBasis multiply_add(SBasis const &a, SBasis const &b, SBasis c); + +SBasis integral(SBasis const &c); +SBasis derivative(SBasis const &a); + +SBasis sqrt(SBasis const &a, int k); + +// return a kth order approx to 1/a) +SBasis reciprocal(Linear const &a, int k); +SBasis divide(SBasis const &a, SBasis const &b, int k); + +inline SBasis operator*(SBasis const & a, SBasis const & b) { + return multiply(a, b); +} + +inline SBasis& operator*=(SBasis& a, SBasis const & b) { + a = multiply(a, b); + return a; +} + +/** Returns the degree of the first non zero coefficient. + \param a sbasis function + \param tol largest abs val considered 0 + \return first non zero coefficient + \relates SBasis +*/ +inline unsigned +valuation(SBasis const &a, double tol=0){ + unsigned val=0; + while( val roots(SBasis const & s); +std::vector roots(SBasis const & s, Interval const inside); +std::vector > multi_roots(SBasis const &f, + std::vector const &levels, + double htol=1e-7, + double vtol=1e-7, + double a=0, + double b=1); + +//--------- Levelset like functions ----------------------------------------------------- + +/** Solve f(t) = v +/- tolerance. The collection of intervals where + * v - vtol <= f(t) <= v+vtol + * is returned (with a precision tol on the boundaries). + \param f sbasis function + \param level the value of v. + \param vtol: error tolerance on v. + \param a, b limit search on domain [a,b] + \param tol: tolerance on the result bounds. + \returns a vector of intervals. +*/ +std::vector level_set (SBasis const &f, + double level, + double vtol = 1e-5, + double a=0., + double b=1., + double tol = 1e-5); + +/** Solve f(t)\in I=[u,v], which defines a collection of intervals (J_k). More precisely, + * a collection (J'_k) is returned with J'_k = J_k up to a given tolerance. + \param f sbasis function + \param level: the given interval of deisred values for f. + \param a, b limit search on domain [a,b] + \param tol: tolerance on the bounds of the result. + \returns a vector of intervals. +*/ +std::vector level_set (SBasis const &f, + Interval const &level, + double a=0., + double b=1., + double tol = 1e-5); + +/** 'Solve' f(t) = v +/- tolerance for several values of v at once. + \param f sbasis function + \param levels vector of values, that should be sorted. + \param vtol: error tolerance on v. + \param a, b limit search on domain [a,b] + \param tol: the bounds of the returned intervals are exact up to that tolerance. + \returns a vector of vectors of intervals. +*/ +std::vector > level_sets (SBasis const &f, + std::vector const &levels, + double a=0., + double b=1., + double vtol = 1e-5, + double tol = 1e-5); + +/** 'Solve' f(t)\in I=[u,v] for several intervals I at once. + \param f sbasis function + \param levels vector of 'y' intervals, that should be disjoints and sorted. + \param a, b limit search on domain [a,b] + \param tol: the bounds of the returned intervals are exact up to that tolerance. + \returns a vector of vectors of intervals. +*/ +std::vector > level_sets (SBasis const &f, + std::vector const &levels, + double a=0., + double b=1., + double tol = 1e-5); + +} +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : +#endif diff --git a/src/2geom/solve-bezier-one-d.cpp b/src/2geom/solve-bezier-one-d.cpp new file mode 100644 index 0000000..3a25e98 --- /dev/null +++ b/src/2geom/solve-bezier-one-d.cpp @@ -0,0 +1,245 @@ + +#include <2geom/solver.h> +#include <2geom/choose.h> +#include <2geom/bezier.h> +#include <2geom/point.h> + +#include +#include +//#include + +/*** Find the zeros of the bernstein function. The code subdivides until it is happy with the + * linearity of the function. This requires an O(degree^2) subdivision for each step, even when + * there is only one solution. + */ + +namespace Geom{ + +template +static int SGN(t x) { return (x > 0 ? 1 : (x < 0 ? -1 : 0)); } + +//const unsigned MAXDEPTH = 23; // Maximum depth for recursion. Using floats means 23 bits precision max + +//const double BEPSILON = ldexp(1.0,(-MAXDEPTH-1)); /*Flatness control value */ +//const double SECANT_EPSILON = 1e-13; // secant method converges much faster, get a bit more precision +/** + * This function is called _a lot_. We have included various manual memory management stuff to reduce the amount of mallocing that goes on. In the future it is possible that this will hurt performance. + **/ +class Bernsteins{ +public: + static const size_t MAX_DEPTH = 53; + size_t degree, N; + std::vector &solutions; + //std::vector bc; + BinomialCoefficient bc; + + Bernsteins(size_t _degree, std::vector & sol) + : degree(_degree), N(degree+1), solutions(sol), bc(degree) + { + } + + unsigned + control_poly_flat_enough(double const *V); + + void + find_bernstein_roots(double const *w, /* The control points */ + unsigned depth, /* The depth of the recursion */ + double left_t, double right_t); +}; +/* + * find_bernstein_roots : Given an equation in Bernstein-Bernstein form, find all + * of the roots in the open interval (0, 1). Return the number of roots found. + */ +void +find_bernstein_roots(double const *w, /* The control points */ + unsigned degree, /* The degree of the polynomial */ + std::vector &solutions, /* RETURN candidate t-values */ + unsigned depth, /* The depth of the recursion */ + double left_t, double right_t, bool /*use_secant*/) +{ + Bernsteins B(degree, solutions); + B.find_bernstein_roots(w, depth, left_t, right_t); +} + +void +find_bernstein_roots(std::vector &solutions, /* RETURN candidate t-values */ + Geom::Bezier const &bz, /* The control points */ + double left_t, double right_t) +{ + Bernsteins B(bz.degree(), solutions); + Geom::Bezier& bzl = const_cast(bz); + double* w = &(bzl[0]); + B.find_bernstein_roots(w, 0, left_t, right_t); +} + + + +void Bernsteins::find_bernstein_roots(double const *w, /* The control points */ + unsigned depth, /* The depth of the recursion */ + double left_t, + double right_t) +{ + + size_t n_crossings = 0; + + int old_sign = SGN(w[0]); + //std::cout << "w[0] = " << w[0] << std::endl; + for (size_t i = 1; i < N; i++) + { + //std::cout << "w[" << i << "] = " << w[i] << std::endl; + int sign = SGN(w[i]); + if (sign != 0) + { + if (sign != old_sign && old_sign != 0) + { + ++n_crossings; + } + old_sign = sign; + } + } + //std::cout << "n_crossings = " << n_crossings << std::endl; + if (n_crossings == 0) return; // no solutions here + + if (n_crossings == 1) /* Unique solution */ + { + //std::cout << "depth = " << depth << std::endl; + /* Stop recursion when the tree is deep enough */ + /* if deep enough, return 1 solution at midpoint */ + if (depth > MAX_DEPTH) + { + //printf("bottom out %d\n", depth); + const double Ax = right_t - left_t; + const double Ay = w[degree] - w[0]; + + solutions.push_back(left_t - Ax*w[0] / Ay); + return; + } + + + double s = 0, t = 1; + double e = 1e-10; + int side = 0; + double r, fs = w[0], ft = w[degree]; + + for (size_t n = 0; n < 100; ++n) + { + r = (fs*t - ft*s) / (fs - ft); + if (fabs(t-s) < e * fabs(t+s)) break; + + double fr = bernstein_value_at(r, w, degree); + + if (fr * ft > 0) + { + t = r; ft = fr; + if (side == -1) fs /= 2; + side = -1; + } + else if (fs * fr > 0) + { + s = r; fs = fr; + if (side == +1) ft /= 2; + side = +1; + } + else break; + } + solutions.push_back(r*right_t + (1-r)*left_t); + return; + + } + + /* Otherwise, solve recursively after subdividing control polygon */ +// double Left[N], /* New left and right */ +// Right[N]; /* control polygons */ + //const double t = 0.5; + double* LR = new double[2*N]; + double* Left = LR; + double* Right = LR + N; + + std::copy(w, w + N, Right); + + Left[0] = Right[0]; + for (size_t i = 1; i < N; ++i) + { + for (size_t j = 0; j < N-i; ++j) + { + Right[j] = (Right[j] + Right[j+1]) * 0.5; + } + Left[i] = Right[0]; + } + + double mid_t = (left_t + right_t) * 0.5; + + + find_bernstein_roots(Left, depth+1, left_t, mid_t); + + + /* Solution is exactly on the subdivision point. */ + if (Right[0] == 0) + { + solutions.push_back(mid_t); + } + + find_bernstein_roots(Right, depth+1, mid_t, right_t); + delete[] LR; +} + +#if 0 +/* + * control_poly_flat_enough : + * Check if the control polygon of a Bernstein curve is flat enough + * for recursive subdivision to bottom out. + * + */ +unsigned +Bernsteins::control_poly_flat_enough(double const *V) +{ + /* Find the perpendicular distance from each interior control point to line connecting V[0] and + * V[degree] */ + + /* Derive the implicit equation for line connecting first */ + /* and last control points */ + const double a = V[0] - V[degree]; + + double max_distance_above = 0.0; + double max_distance_below = 0.0; + double ii = 0, dii = 1./degree; + for (unsigned i = 1; i < degree; i++) { + ii += dii; + /* Compute distance from each of the points to that line */ + const double d = (a + V[i]) * ii - a; + double dist = d*d; + // Find the largest distance + if (d < 0.0) + max_distance_below = std::min(max_distance_below, -dist); + else + max_distance_above = std::max(max_distance_above, dist); + } + + const double abSquared = 1./((a * a) + 1); + + const double intercept_1 = (a - max_distance_above * abSquared); + const double intercept_2 = (a - max_distance_below * abSquared); + + /* Compute bounding interval*/ + const double left_intercept = std::min(intercept_1, intercept_2); + const double right_intercept = std::max(intercept_1, intercept_2); + + const double error = 0.5 * (right_intercept - left_intercept); + //printf("error %g %g %g\n", error, a, BEPSILON * a); + return error < BEPSILON * a; +} +#endif + + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/solve-bezier-parametric.cpp b/src/2geom/solve-bezier-parametric.cpp new file mode 100644 index 0000000..2fb3f41 --- /dev/null +++ b/src/2geom/solve-bezier-parametric.cpp @@ -0,0 +1,189 @@ +#include <2geom/bezier.h> +#include <2geom/point.h> +#include <2geom/solver.h> +#include + +namespace Geom { + +/*** Find the zeros of the parametric function in 2d defined by two beziers X(t), Y(t). The code subdivides until it happy with the linearity of the bezier. This requires an n^2 subdivision for each step, even when there is only one solution. + * + * Perhaps it would be better to subdivide particularly around nodes with changing sign, rather than simply cutting in half. + */ + +#define SGN(a) (((a)<0) ? -1 : 1) + +/* + * Forward declarations + */ +unsigned +crossing_count(Geom::Point const *V, unsigned degree); +static unsigned +control_poly_flat_enough(Geom::Point const *V, unsigned degree); +static double +compute_x_intercept(Geom::Point const *V, unsigned degree); + +const unsigned MAXDEPTH = 64; /* Maximum depth for recursion */ + +const double BEPSILON = ldexp(1.0,-MAXDEPTH-1); /*Flatness control value */ + +unsigned total_steps, total_subs; + +/* + * find_bezier_roots : Given an equation in Bernstein-Bezier form, find all + * of the roots in the interval [0, 1]. Return the number of roots found. + */ +void +find_parametric_bezier_roots(Geom::Point const *w, /* The control points */ + unsigned degree, /* The degree of the polynomial */ + std::vector &solutions, /* RETURN candidate t-values */ + unsigned depth) /* The depth of the recursion */ +{ + total_steps++; + const unsigned max_crossings = crossing_count(w, degree); + switch (max_crossings) { + case 0: /* No solutions here */ + return; + + case 1: + /* Unique solution */ + /* Stop recursion when the tree is deep enough */ + /* if deep enough, return 1 solution at midpoint */ + if (depth >= MAXDEPTH) { + solutions.push_back((w[0][Geom::X] + w[degree][Geom::X]) / 2.0); + return; + } + + // I thought secant method would be faster here, but it'aint. -- njh + + if (control_poly_flat_enough(w, degree)) { + solutions.push_back(compute_x_intercept(w, degree)); + return; + } + break; + } + + /* Otherwise, solve recursively after subdividing control polygon */ + + //Geom::Point Left[degree+1], /* New left and right */ + // Right[degree+1]; /* control polygons */ + std::vector Left( degree+1 ), Right(degree+1); + + casteljau_subdivision(0.5, w, Left.data(), Right.data(), degree); + total_subs ++; + find_parametric_bezier_roots(Left.data(), degree, solutions, depth+1); + find_parametric_bezier_roots(Right.data(), degree, solutions, depth+1); +} + + +/* + * crossing_count: + * Count the number of times a Bezier control polygon + * crosses the 0-axis. This number is >= the number of roots. + * + */ +unsigned +crossing_count(Geom::Point const *V, /* Control pts of Bezier curve */ + unsigned degree) /* Degree of Bezier curve */ +{ + unsigned n_crossings = 0; /* Number of zero-crossings */ + + int old_sign = SGN(V[0][Geom::Y]); + for (unsigned i = 1; i <= degree; i++) { + int sign = SGN(V[i][Geom::Y]); + if (sign != old_sign) + n_crossings++; + old_sign = sign; + } + return n_crossings; +} + + + +/* + * control_poly_flat_enough : + * Check if the control polygon of a Bezier curve is flat enough + * for recursive subdivision to bottom out. + * + */ +static unsigned +control_poly_flat_enough(Geom::Point const *V, /* Control points */ + unsigned degree) /* Degree of polynomial */ +{ + /* Find the perpendicular distance from each interior control point to line connecting V[0] and + * V[degree] */ + + /* Derive the implicit equation for line connecting first */ + /* and last control points */ + const double a = V[0][Geom::Y] - V[degree][Geom::Y]; + const double b = V[degree][Geom::X] - V[0][Geom::X]; + const double c = V[0][Geom::X] * V[degree][Geom::Y] - V[degree][Geom::X] * V[0][Geom::Y]; + + const double abSquared = (a * a) + (b * b); + + //double distance[degree]; /* Distances from pts to line */ + std::vector distance(degree); /* Distances from pts to line */ + for (unsigned i = 1; i < degree; i++) { + /* Compute distance from each of the points to that line */ + double & dist(distance[i-1]); + const double d = a * V[i][Geom::X] + b * V[i][Geom::Y] + c; + dist = d*d / abSquared; + if (d < 0.0) + dist = -dist; + } + + + // Find the largest distance + double max_distance_above = 0.0; + double max_distance_below = 0.0; + for (unsigned i = 0; i < degree-1; i++) { + const double d = distance[i]; + if (d < 0.0) + max_distance_below = std::min(max_distance_below, d); + if (d > 0.0) + max_distance_above = std::max(max_distance_above, d); + } + + const double intercept_1 = (c + max_distance_above) / -a; + const double intercept_2 = (c + max_distance_below) / -a; + + /* Compute bounding interval*/ + const double left_intercept = std::min(intercept_1, intercept_2); + const double right_intercept = std::max(intercept_1, intercept_2); + + const double error = 0.5 * (right_intercept - left_intercept); + + if (error < BEPSILON) + return 1; + + return 0; +} + + + +/* + * compute_x_intercept : + * Compute intersection of chord from first control point to last + * with 0-axis. + * + */ +static double +compute_x_intercept(Geom::Point const *V, /* Control points */ + unsigned degree) /* Degree of curve */ +{ + const Geom::Point A = V[degree] - V[0]; + + return (A[Geom::X]*V[0][Geom::Y] - A[Geom::Y]*V[0][Geom::X]) / -A[Geom::Y]; +} + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/solve-bezier.cpp b/src/2geom/solve-bezier.cpp new file mode 100644 index 0000000..4ff42bb --- /dev/null +++ b/src/2geom/solve-bezier.cpp @@ -0,0 +1,304 @@ + +#include <2geom/solver.h> +#include <2geom/choose.h> +#include <2geom/bezier.h> +#include <2geom/point.h> + +#include +#include + +/*** Find the zeros of a Bezier. The code subdivides until it is happy with the linearity of the + * function. This requires an O(degree^2) subdivision for each step, even when there is only one + * solution. + * + * We try fairly hard to correctly handle multiple roots. + */ + +//#define debug(x) do{x;}while(0) +#define debug(x) + +namespace Geom{ + +template +static int SGN(t x) { return (x > 0 ? 1 : (x < 0 ? -1 : 0)); } + +class Bernsteins{ +public: + static const size_t MAX_DEPTH = 22; + std::vector &solutions; + //std::vector dsolutions; + + Bernsteins(std::vector & sol) + : solutions(sol) + {} + + void subdivide(double const *V, + double t, + double *Left, + double *Right); + + double secant(Bezier const &bz); + + + void find_bernstein_roots(Bezier const &bz, unsigned depth, + double left_t, double right_t); +}; + +template +inline std::ostream &operator<< (std::ostream &out_file, const std::vector & b) { + out_file << "["; + for(unsigned i = 0; i < b.size(); i++) { + out_file << b[i] << ", "; + } + return out_file << "]"; +} + +void convex_hull_marching(Bezier const &src_bz, Bezier bz, + std::vector &solutions, + double left_t, + double right_t) +{ + while(bz.order() > 0 && bz[0] == 0) { + std::cout << "deflate\n"; + bz = bz.deflate(); + solutions.push_back(left_t); + } + std::cout << std::endl; + if (bz.order() > 0) { + + int old_sign = SGN(bz[0]); + + double left_bound = 0; + double dt = 0; + for (size_t i = 1; i < bz.size(); i++) + { + int sign = SGN(bz[i]); + if (sign != old_sign) + { + dt = double(i) / bz.order(); + left_bound = dt * bz[0] / (bz[0] - bz[i]); + break; + } + old_sign = sign; + } + if (dt == 0) return; + std::cout << bz << std::endl; + std::cout << "dt = " << dt << std::endl; + std::cout << "left_t = " << left_t << std::endl; + std::cout << "right_t = " << right_t << std::endl; + std::cout << "left bound = " << left_bound + << " = " << bz(left_bound) << std::endl; + double new_left_t = left_bound * (right_t - left_t) + left_t; + std::cout << "new_left_t = " << new_left_t << std::endl; + Bezier bzr = portion(src_bz, new_left_t, 1); + while(bzr.order() > 0 && bzr[0] == 0) { + std::cout << "deflate\n"; + bzr = bzr.deflate(); + solutions.push_back(new_left_t); + } + if (left_t < new_left_t) { + convex_hull_marching(src_bz, bzr, + solutions, + new_left_t, right_t); + } else { + std::cout << "epsilon reached\n"; + while(bzr.order() > 0 && fabs(bzr[0]) <= 1e-10) { + std::cout << "deflate\n"; + bzr = bzr.deflate(); + std::cout << bzr << std::endl; + solutions.push_back(new_left_t); + } + + } + } +} + +void +Bezier::find_bezier_roots(std::vector &solutions, + double left_t, double right_t) const { + Bezier bz = *this; + //convex_hull_marching(bz, bz, solutions, left_t, right_t); + //return; + + // a constant bezier, even if identically zero, has no roots + if (bz.isConstant()) { + return; + } + + while(bz[0] == 0) { + debug(std::cout << "deflate\n"); + bz = bz.deflate(); + solutions.push_back(0); + } + if (bz.degree() == 1) { + debug(std::cout << "linear\n"); + + if (SGN(bz[0]) != SGN(bz[1])) { + double d = bz[0] - bz[1]; + if(d != 0) { + double r = bz[0] / d; + if(0 <= r && r <= 1) + solutions.push_back(r); + } + } + return; + } + + //std::cout << "initial = " << bz << std::endl; + Bernsteins B(solutions); + B.find_bernstein_roots(bz, 0, left_t, right_t); + //std::cout << solutions << std::endl; +} + +void Bernsteins::find_bernstein_roots(Bezier const &bz, + unsigned depth, + double left_t, + double right_t) +{ + debug(std::cout << left_t << ", " << right_t << std::endl); + size_t n_crossings = 0; + + int old_sign = SGN(bz[0]); + //std::cout << "w[0] = " << bz[0] << std::endl; + for (size_t i = 1; i < bz.size(); i++) + { + //std::cout << "w[" << i << "] = " << w[i] << std::endl; + int sign = SGN(bz[i]); + if (sign != 0) + { + if (sign != old_sign && old_sign != 0) + { + ++n_crossings; + } + old_sign = sign; + } + } + // if last control point is zero, that counts as crossing too + if (SGN(bz[bz.size()-1]) == 0) { + ++n_crossings; + } + + //std::cout << "n_crossings = " << n_crossings << std::endl; + if (n_crossings == 0) return; // no solutions here + + if (n_crossings == 1) /* Unique solution */ + { + //std::cout << "depth = " << depth << std::endl; + /* Stop recursion when the tree is deep enough */ + /* if deep enough, return 1 solution at midpoint */ + if (depth > MAX_DEPTH) + { + //printf("bottom out %d\n", depth); + const double Ax = right_t - left_t; + const double Ay = bz.at1() - bz.at0(); + + solutions.push_back(left_t - Ax*bz.at0() / Ay); + return; + } + + double r = secant(bz); + solutions.push_back(r*right_t + (1-r)*left_t); + return; + } + /* Otherwise, solve recursively after subdividing control polygon */ + Bezier::Order o(bz); + Bezier Left(o), Right = bz; + double split_t = (left_t + right_t) * 0.5; + + // If subdivision is working poorly, split around the leftmost root of the derivative + if (depth > 2) { + debug(std::cout << "derivative mode\n"); + Bezier dbz = derivative(bz); + + debug(std::cout << "initial = " << dbz << std::endl); + std::vector dsolutions = dbz.roots(Interval(left_t, right_t)); + debug(std::cout << "dsolutions = " << dsolutions << std::endl); + + double dsplit_t = 0.5; + if(!dsolutions.empty()) { + dsplit_t = dsolutions[0]; + split_t = left_t + (right_t - left_t)*dsplit_t; + debug(std::cout << "split_value = " << bz(split_t) << std::endl); + debug(std::cout << "splitting around " << dsplit_t << " = " + << split_t << "\n"); + + } + std::pair LR = bz.subdivide(dsplit_t); + Left = LR.first; + Right = LR.second; + } else { + // split at midpoint, because it is cheap + Left[0] = Right[0]; + for (size_t i = 1; i < bz.size(); ++i) + { + for (size_t j = 0; j < bz.size()-i; ++j) + { + Right[j] = (Right[j] + Right[j+1]) * 0.5; + } + Left[i] = Right[0]; + } + } + debug(std::cout << "Solution is exactly on the subdivision point.\n"); + debug(std::cout << Left << " , " << Right << std::endl); + Left = reverse(Left); + while(Right.order() > 0 && fabs(Right[0]) <= 1e-10) { + debug(std::cout << "deflate\n"); + Right = Right.deflate(); + Left = Left.deflate(); + solutions.push_back(split_t); + } + Left = reverse(Left); + if (Right.order() > 0) { + debug(std::cout << Left << " , " << Right << std::endl); + find_bernstein_roots(Left, depth+1, left_t, split_t); + find_bernstein_roots(Right, depth+1, split_t, right_t); + } +} + +double Bernsteins::secant(Bezier const &bz) { + double s = 0, t = 1; + double e = 1e-14; + int side = 0; + double r, fs = bz.at0(), ft = bz.at1(); + + for (size_t n = 0; n < 100; ++n) + { + r = (fs*t - ft*s) / (fs - ft); + if (fabs(t-s) < e * fabs(t+s)) { + debug(std::cout << "error small " << fabs(t-s) + << ", accepting solution " << r + << "after " << n << "iterations\n"); + return r; + } + + double fr = bz.valueAt(r); + + if (fr * ft > 0) + { + t = r; ft = fr; + if (side == -1) fs /= 2; + side = -1; + } + else if (fs * fr > 0) + { + s = r; fs = fr; + if (side == +1) ft /= 2; + side = +1; + } + else break; + } + return r; +} + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/solver.h b/src/2geom/solver.h new file mode 100644 index 0000000..5b082cb --- /dev/null +++ b/src/2geom/solver.h @@ -0,0 +1,88 @@ +/** + * \file + * \brief Finding roots of Bernstein-Bezier polynomials + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_SOLVER_H +#define LIB2GEOM_SEEN_SOLVER_H + +#include <2geom/point.h> +#include <2geom/sbasis.h> +#include + +namespace Geom { + + class Point; + class Bezier; + +unsigned +crossing_count(Geom::Point const *V, /* Control pts of Bezier curve */ + unsigned degree); /* Degree of Bezier curve */ +void +find_parametric_bezier_roots( + Geom::Point const *w, /* The control points */ + unsigned degree, /* The degree of the polynomial */ + std::vector & solutions, /* RETURN candidate t-values */ + unsigned depth); /* The depth of the recursion */ + +unsigned +crossing_count(double const *V, /* Control pts of Bezier curve */ + unsigned degree, /* Degree of Bezier curve */ + double left_t, double right_t); + + +void +find_bernstein_roots( + double const *w, /* The control points */ + unsigned degree, /* The degree of the polynomial */ + std::vector & solutions, /* RETURN candidate t-values */ + unsigned depth, /* The depth of the recursion */ + double left_t=0, double right_t=1, bool use_secant=true); + +}; + +void +find_bernstein_roots(std::vector &solutions, /* RETURN candidate t-values */ + Geom::Bezier const& bz, + double left_t, double right_t); + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/svg-path-parser.cpp b/src/2geom/svg-path-parser.cpp new file mode 100644 index 0000000..0338110 --- /dev/null +++ b/src/2geom/svg-path-parser.cpp @@ -0,0 +1,1615 @@ + +#line 1 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" +/** + * \file + * \brief parse SVG path specifications + * + * Copyright 2007 MenTaLguY + * Copyright 2007 Aaron Spike + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#include +#include +#include +#include + +#include <2geom/point.h> +#include <2geom/svg-path-parser.h> +#include <2geom/angle.h> + +namespace Geom { + + +#line 48 "/home/mc/lib2geom/src/2geom/svg-path-parser.cpp" +static const char _svg_path_actions[] = { + 0, 1, 0, 1, 1, 1, 2, 1, + 3, 1, 4, 1, 5, 1, 15, 2, + 1, 0, 2, 1, 6, 2, 1, 7, + 2, 1, 8, 2, 1, 9, 2, 1, + 10, 2, 1, 11, 2, 1, 12, 2, + 1, 13, 2, 1, 14, 2, 2, 0, + 2, 3, 0, 2, 4, 0, 2, 5, + 0, 3, 1, 6, 0, 3, 1, 7, + 0, 3, 1, 8, 0, 3, 1, 9, + 0, 3, 1, 10, 0, 3, 1, 11, + 0, 3, 1, 12, 0, 3, 1, 13, + 0, 3, 1, 14, 0 +}; + +static const short _svg_path_key_offsets[] = { + 0, 0, 9, 18, 21, 23, 35, 45, + 48, 50, 53, 55, 67, 77, 80, 82, + 91, 103, 112, 121, 130, 133, 135, 147, + 157, 160, 162, 174, 184, 187, 189, 198, + 205, 211, 218, 225, 231, 241, 251, 254, + 256, 268, 278, 281, 283, 295, 304, 316, + 325, 335, 339, 341, 348, 352, 354, 364, + 368, 370, 380, 389, 398, 401, 403, 415, + 425, 428, 430, 442, 452, 455, 457, 469, + 479, 482, 484, 496, 506, 509, 511, 523, + 533, 536, 538, 550, 559, 571, 580, 592, + 601, 613, 622, 634, 643, 647, 649, 658, + 667, 670, 672, 676, 678, 687, 696, 705, + 708, 710, 722, 732, 735, 737, 749, 759, + 762, 764, 776, 786, 789, 791, 803, 812, + 824, 833, 845, 854, 858, 860, 869, 878, + 881, 883, 895, 905, 908, 910, 922, 932, + 935, 937, 949, 959, 962, 964, 976, 985, + 997, 1006, 1018, 1027, 1031, 1033, 1042, 1051, + 1054, 1056, 1068, 1078, 1081, 1083, 1095, 1104, + 1108, 1110, 1119, 1128, 1131, 1133, 1137, 1139, + 1148, 1157, 1166, 1175, 1184, 1196, 1205, 1209, + 1211, 1220, 1229, 1238, 1247, 1251, 1253, 1263, + 1267, 1269, 1279, 1283, 1285, 1295, 1299, 1301, + 1311, 1315, 1317, 1327, 1331, 1333, 1343, 1347, + 1349, 1359, 1363, 1365, 1375, 1379, 1381, 1391, + 1395, 1397, 1407, 1411, 1413, 1423, 1427, 1429, + 1439, 1443, 1445, 1455, 1459, 1461, 1470, 1474, + 1476, 1486, 1498, 1507, 1517, 1524, 1528, 1530, + 1534, 1536, 1546, 1552, 1584, 1614, 1646, 1678, + 1710, 1740, 1772, 1802, 1834, 1864, 1896, 1926, + 1958, 1988, 2020, 2050, 2082, 2112, 2144, 2174, + 2206, 2236, 2268, 2298, 2330, 2360, 2392, 2422, + 2454, 2484, 2508, 2532, 2564, 2594, 2624, 2656 +}; + +static const char _svg_path_trans_keys[] = { + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 13, 32, 43, 45, 46, 9, 10, + 48, 57, 46, 48, 57, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 44, 46, 9, + 10, 43, 45, 48, 57, 46, 48, 57, + 48, 57, 46, 48, 57, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 44, 46, 9, + 10, 43, 45, 48, 57, 46, 48, 57, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 13, 32, 44, 46, 69, + 101, 9, 10, 43, 45, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 13, 32, 43, 45, 46, 9, 10, + 48, 57, 46, 48, 57, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 44, 46, 9, + 10, 43, 45, 48, 57, 46, 48, 57, + 48, 57, 13, 32, 44, 46, 69, 101, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 9, 10, 43, 45, 48, 57, + 46, 48, 57, 48, 57, 13, 32, 44, + 69, 101, 9, 10, 48, 57, 13, 32, + 44, 48, 49, 9, 10, 13, 32, 48, + 49, 9, 10, 13, 32, 44, 48, 49, + 9, 10, 13, 32, 44, 48, 49, 9, + 10, 13, 32, 48, 49, 9, 10, 13, + 32, 44, 46, 9, 10, 43, 45, 48, + 57, 13, 32, 44, 46, 9, 10, 43, + 45, 48, 57, 46, 48, 57, 48, 57, + 13, 32, 44, 46, 69, 101, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 9, 10, 43, 45, 48, 57, 46, 48, + 57, 48, 57, 13, 32, 44, 46, 69, + 101, 9, 10, 43, 45, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 13, 32, 44, 46, 69, 101, 9, 10, + 43, 45, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 13, 32, 44, + 46, 69, 101, 9, 10, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 9, 10, 48, 57, 43, 45, 48, 57, + 48, 57, 13, 32, 44, 46, 9, 10, + 43, 45, 48, 57, 43, 45, 48, 57, + 48, 57, 13, 32, 44, 46, 9, 10, + 43, 45, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 46, 48, + 57, 48, 57, 13, 32, 44, 46, 69, + 101, 9, 10, 43, 45, 48, 57, 13, + 32, 44, 46, 9, 10, 43, 45, 48, + 57, 46, 48, 57, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 9, 10, + 43, 45, 48, 57, 46, 48, 57, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 46, + 48, 57, 48, 57, 13, 32, 44, 46, + 69, 101, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 9, 10, 43, 45, + 48, 57, 46, 48, 57, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 44, 46, 9, + 10, 43, 45, 48, 57, 46, 48, 57, + 48, 57, 13, 32, 44, 46, 69, 101, + 9, 10, 43, 45, 48, 57, 13, 32, + 43, 45, 46, 9, 10, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 43, 45, 46, + 9, 10, 48, 57, 13, 32, 44, 46, + 69, 101, 9, 10, 43, 45, 48, 57, + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 43, 45, 48, 57, 48, + 57, 13, 32, 43, 45, 46, 9, 10, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 46, 48, 57, 48, 57, + 43, 45, 48, 57, 48, 57, 13, 32, + 43, 45, 46, 9, 10, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 46, 48, 57, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 9, 10, + 43, 45, 48, 57, 46, 48, 57, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 46, + 48, 57, 48, 57, 13, 32, 44, 46, + 69, 101, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 9, 10, 43, 45, + 48, 57, 46, 48, 57, 48, 57, 13, + 32, 44, 46, 69, 101, 9, 10, 43, + 45, 48, 57, 13, 32, 43, 45, 46, + 9, 10, 48, 57, 13, 32, 44, 46, + 69, 101, 9, 10, 43, 45, 48, 57, + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 43, 45, + 48, 57, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 46, 48, + 57, 48, 57, 13, 32, 44, 46, 69, + 101, 9, 10, 43, 45, 48, 57, 13, + 32, 44, 46, 9, 10, 43, 45, 48, + 57, 46, 48, 57, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 9, 10, + 43, 45, 48, 57, 46, 48, 57, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 46, + 48, 57, 48, 57, 13, 32, 44, 46, + 69, 101, 9, 10, 43, 45, 48, 57, + 13, 32, 43, 45, 46, 9, 10, 48, + 57, 13, 32, 44, 46, 69, 101, 9, + 10, 43, 45, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 43, 45, 48, 57, 48, + 57, 13, 32, 43, 45, 46, 9, 10, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 46, 48, 57, 48, 57, + 13, 32, 44, 46, 69, 101, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 9, 10, 43, 45, 48, 57, 46, 48, + 57, 48, 57, 13, 32, 44, 46, 69, + 101, 9, 10, 43, 45, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 43, 45, 48, 57, 48, 57, 13, 32, + 43, 45, 46, 9, 10, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 46, 48, 57, 48, 57, 43, 45, 48, + 57, 48, 57, 13, 32, 43, 45, 46, + 9, 10, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 13, 32, + 43, 45, 46, 9, 10, 48, 57, 13, + 32, 43, 45, 46, 9, 10, 48, 57, + 13, 32, 44, 46, 69, 101, 9, 10, + 43, 45, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 43, 45, 48, + 57, 48, 57, 13, 32, 43, 45, 46, + 9, 10, 48, 57, 13, 32, 43, 45, + 46, 9, 10, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 13, 32, + 43, 45, 46, 9, 10, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 44, + 46, 9, 10, 43, 45, 48, 57, 43, + 45, 48, 57, 48, 57, 13, 32, 43, + 45, 46, 9, 10, 48, 57, 43, 45, + 48, 57, 48, 57, 13, 32, 44, 46, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 69, 101, 9, 10, 43, 45, + 48, 57, 13, 32, 43, 45, 46, 9, + 10, 48, 57, 13, 32, 44, 46, 9, + 10, 43, 45, 48, 57, 13, 32, 44, + 48, 49, 9, 10, 43, 45, 48, 57, + 48, 57, 43, 45, 48, 57, 48, 57, + 13, 32, 44, 46, 9, 10, 43, 45, + 48, 57, 13, 32, 77, 109, 9, 10, + 13, 32, 44, 46, 65, 67, 69, 72, + 76, 77, 81, 83, 84, 86, 90, 97, + 99, 101, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 72, 76, + 77, 81, 83, 84, 86, 90, 97, 99, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 72, 76, 77, 81, + 83, 84, 86, 90, 97, 99, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 69, 72, 76, 77, 81, 83, + 84, 86, 90, 97, 99, 101, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 72, 76, 77, 81, 83, 84, + 86, 90, 97, 99, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 69, 72, 76, 77, 81, 83, 84, 86, + 90, 97, 99, 101, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 69, 72, + 76, 77, 81, 83, 84, 86, 90, 97, + 99, 101, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 72, 76, + 77, 81, 83, 84, 86, 90, 97, 99, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 72, 76, 77, 81, + 83, 84, 86, 90, 97, 99, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 69, 72, 76, 77, 81, 83, + 84, 86, 90, 97, 99, 101, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 72, 76, 77, 81, 83, 84, + 86, 90, 97, 99, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 69, 72, 76, 77, 81, 83, 84, 86, + 90, 97, 99, 101, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 69, 72, + 76, 77, 81, 83, 84, 86, 90, 97, + 99, 101, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 72, 76, + 77, 81, 83, 84, 86, 90, 97, 99, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 72, 76, 77, 81, + 83, 84, 86, 90, 97, 99, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 69, 72, 76, 77, 81, 83, + 84, 86, 90, 97, 99, 101, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 72, 76, 77, 81, 83, 84, + 86, 90, 97, 99, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 69, 72, 76, 77, 81, 83, 84, 86, + 90, 97, 99, 101, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 69, 72, + 76, 77, 81, 83, 84, 86, 90, 97, + 99, 101, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 72, 76, + 77, 81, 83, 84, 86, 90, 97, 99, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 69, 72, 76, 77, + 81, 83, 84, 86, 90, 97, 99, 101, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 13, 32, + 44, 46, 65, 67, 72, 76, 77, 81, + 83, 84, 86, 90, 97, 99, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 13, 32, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 13, 32, 44, 46, + 65, 67, 69, 72, 76, 77, 81, 83, + 84, 86, 90, 97, 99, 101, 104, 108, + 109, 113, 115, 116, 118, 122, 9, 10, + 43, 45, 48, 57, 13, 32, 44, 46, + 65, 67, 72, 76, 77, 81, 83, 84, + 86, 90, 97, 99, 104, 108, 109, 113, + 115, 116, 118, 122, 9, 10, 43, 45, + 48, 57, 13, 32, 44, 46, 65, 67, + 72, 76, 77, 81, 83, 84, 86, 90, + 97, 99, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 69, 72, + 76, 77, 81, 83, 84, 86, 90, 97, + 99, 101, 104, 108, 109, 113, 115, 116, + 118, 122, 9, 10, 43, 45, 48, 57, + 13, 32, 44, 46, 65, 67, 72, 76, + 77, 81, 83, 84, 86, 90, 97, 99, + 104, 108, 109, 113, 115, 116, 118, 122, + 9, 10, 43, 45, 48, 57, 0 +}; + +static const char _svg_path_single_lengths[] = { + 0, 5, 5, 1, 0, 6, 4, 1, + 0, 1, 0, 6, 4, 1, 0, 5, + 6, 5, 5, 5, 1, 0, 6, 4, + 1, 0, 6, 4, 1, 0, 5, 5, + 4, 5, 5, 4, 4, 4, 1, 0, + 6, 4, 1, 0, 6, 5, 6, 5, + 6, 2, 0, 3, 2, 0, 4, 2, + 0, 4, 5, 5, 1, 0, 6, 4, + 1, 0, 6, 4, 1, 0, 6, 4, + 1, 0, 6, 4, 1, 0, 6, 4, + 1, 0, 6, 5, 6, 5, 6, 5, + 6, 5, 6, 5, 2, 0, 5, 5, + 1, 0, 2, 0, 5, 5, 5, 1, + 0, 6, 4, 1, 0, 6, 4, 1, + 0, 6, 4, 1, 0, 6, 5, 6, + 5, 6, 5, 2, 0, 5, 5, 1, + 0, 6, 4, 1, 0, 6, 4, 1, + 0, 6, 4, 1, 0, 6, 5, 6, + 5, 6, 5, 2, 0, 5, 5, 1, + 0, 6, 4, 1, 0, 6, 5, 2, + 0, 5, 5, 1, 0, 2, 0, 5, + 5, 5, 5, 5, 6, 5, 2, 0, + 5, 5, 5, 5, 2, 0, 4, 2, + 0, 4, 2, 0, 4, 2, 0, 4, + 2, 0, 4, 2, 0, 4, 2, 0, + 4, 2, 0, 4, 2, 0, 4, 2, + 0, 4, 2, 0, 4, 2, 0, 4, + 2, 0, 4, 2, 0, 5, 2, 0, + 4, 6, 5, 4, 5, 2, 0, 2, + 0, 4, 4, 26, 24, 26, 26, 26, + 24, 26, 24, 26, 24, 26, 24, 26, + 24, 26, 24, 26, 24, 26, 24, 26, + 24, 26, 24, 26, 24, 26, 24, 26, + 24, 22, 22, 26, 24, 24, 26, 24 +}; + +static const char _svg_path_range_lengths[] = { + 0, 2, 2, 1, 1, 3, 3, 1, + 1, 1, 1, 3, 3, 1, 1, 2, + 3, 2, 2, 2, 1, 1, 3, 3, + 1, 1, 3, 3, 1, 1, 2, 1, + 1, 1, 1, 1, 3, 3, 1, 1, + 3, 3, 1, 1, 3, 2, 3, 2, + 2, 1, 1, 2, 1, 1, 3, 1, + 1, 3, 2, 2, 1, 1, 3, 3, + 1, 1, 3, 3, 1, 1, 3, 3, + 1, 1, 3, 3, 1, 1, 3, 3, + 1, 1, 3, 2, 3, 2, 3, 2, + 3, 2, 3, 2, 1, 1, 2, 2, + 1, 1, 1, 1, 2, 2, 2, 1, + 1, 3, 3, 1, 1, 3, 3, 1, + 1, 3, 3, 1, 1, 3, 2, 3, + 2, 3, 2, 1, 1, 2, 2, 1, + 1, 3, 3, 1, 1, 3, 3, 1, + 1, 3, 3, 1, 1, 3, 2, 3, + 2, 3, 2, 1, 1, 2, 2, 1, + 1, 3, 3, 1, 1, 3, 2, 1, + 1, 2, 2, 1, 1, 1, 1, 2, + 2, 2, 2, 2, 3, 2, 1, 1, + 2, 2, 2, 2, 1, 1, 3, 1, + 1, 3, 1, 1, 3, 1, 1, 3, + 1, 1, 3, 1, 1, 3, 1, 1, + 3, 1, 1, 3, 1, 1, 3, 1, + 1, 3, 1, 1, 3, 1, 1, 3, + 1, 1, 3, 1, 1, 2, 1, 1, + 3, 3, 2, 3, 1, 1, 1, 1, + 1, 3, 1, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 1, 1, 3, 3, 3, 3, 3 +}; + +static const short _svg_path_index_offsets[] = { + 0, 0, 8, 16, 19, 21, 31, 39, + 42, 44, 47, 49, 59, 67, 70, 72, + 80, 90, 98, 106, 114, 117, 119, 129, + 137, 140, 142, 152, 160, 163, 165, 173, + 180, 186, 193, 200, 206, 214, 222, 225, + 227, 237, 245, 248, 250, 260, 268, 278, + 286, 295, 299, 301, 307, 311, 313, 321, + 325, 327, 335, 343, 351, 354, 356, 366, + 374, 377, 379, 389, 397, 400, 402, 412, + 420, 423, 425, 435, 443, 446, 448, 458, + 466, 469, 471, 481, 489, 499, 507, 517, + 525, 535, 543, 553, 561, 565, 567, 575, + 583, 586, 588, 592, 594, 602, 610, 618, + 621, 623, 633, 641, 644, 646, 656, 664, + 667, 669, 679, 687, 690, 692, 702, 710, + 720, 728, 738, 746, 750, 752, 760, 768, + 771, 773, 783, 791, 794, 796, 806, 814, + 817, 819, 829, 837, 840, 842, 852, 860, + 870, 878, 888, 896, 900, 902, 910, 918, + 921, 923, 933, 941, 944, 946, 956, 964, + 968, 970, 978, 986, 989, 991, 995, 997, + 1005, 1013, 1021, 1029, 1037, 1047, 1055, 1059, + 1061, 1069, 1077, 1085, 1093, 1097, 1099, 1107, + 1111, 1113, 1121, 1125, 1127, 1135, 1139, 1141, + 1149, 1153, 1155, 1163, 1167, 1169, 1177, 1181, + 1183, 1191, 1195, 1197, 1205, 1209, 1211, 1219, + 1223, 1225, 1233, 1237, 1239, 1247, 1251, 1253, + 1261, 1265, 1267, 1275, 1279, 1281, 1289, 1293, + 1295, 1303, 1313, 1321, 1329, 1336, 1340, 1342, + 1346, 1348, 1356, 1362, 1392, 1420, 1450, 1480, + 1510, 1538, 1568, 1596, 1626, 1654, 1684, 1712, + 1742, 1770, 1800, 1828, 1858, 1886, 1916, 1944, + 1974, 2002, 2032, 2060, 2090, 2118, 2148, 2176, + 2206, 2234, 2258, 2282, 2312, 2340, 2368, 2398 +}; + +static const short _svg_path_indicies[] = { + 0, 0, 2, 2, 3, 0, 4, 1, + 5, 5, 6, 6, 7, 5, 8, 1, + 9, 10, 1, 11, 1, 12, 12, 14, + 15, 16, 16, 12, 13, 11, 1, 17, + 17, 19, 20, 17, 18, 21, 1, 22, + 23, 1, 24, 1, 25, 26, 1, 27, + 1, 28, 28, 30, 31, 32, 32, 28, + 29, 27, 1, 33, 33, 35, 36, 33, + 34, 37, 1, 38, 39, 1, 40, 1, + 41, 41, 42, 42, 43, 41, 44, 1, + 28, 28, 30, 27, 32, 32, 28, 29, + 26, 1, 35, 35, 34, 34, 36, 35, + 37, 1, 45, 45, 46, 46, 47, 45, + 48, 1, 49, 49, 50, 50, 51, 49, + 52, 1, 53, 54, 1, 55, 1, 56, + 56, 58, 59, 60, 60, 56, 57, 55, + 1, 61, 61, 63, 64, 61, 62, 65, + 1, 66, 67, 1, 68, 1, 69, 69, + 71, 72, 73, 73, 69, 70, 68, 1, + 74, 74, 76, 77, 74, 75, 78, 1, + 79, 80, 1, 81, 1, 82, 82, 83, + 84, 84, 82, 81, 1, 85, 85, 86, + 87, 88, 85, 1, 86, 86, 87, 88, + 86, 1, 89, 89, 90, 91, 92, 89, + 1, 93, 93, 94, 95, 96, 93, 1, + 94, 94, 95, 96, 94, 1, 97, 97, + 99, 100, 97, 98, 101, 1, 102, 102, + 104, 105, 102, 103, 106, 1, 107, 108, + 1, 109, 1, 110, 110, 112, 113, 114, + 114, 110, 111, 109, 1, 115, 115, 117, + 118, 115, 116, 119, 1, 120, 121, 1, + 122, 1, 56, 56, 58, 55, 60, 60, + 56, 57, 54, 1, 63, 63, 62, 62, + 64, 63, 65, 1, 69, 69, 71, 68, + 73, 73, 69, 70, 67, 1, 76, 76, + 75, 75, 77, 76, 78, 1, 82, 82, + 83, 81, 84, 84, 82, 80, 1, 123, + 123, 124, 1, 124, 1, 82, 82, 83, + 82, 124, 1, 125, 125, 126, 1, 126, + 1, 69, 69, 71, 72, 69, 70, 126, + 1, 127, 127, 128, 1, 128, 1, 56, + 56, 58, 59, 56, 57, 128, 1, 129, + 129, 130, 130, 131, 129, 132, 1, 133, + 133, 134, 134, 135, 133, 136, 1, 137, + 138, 1, 139, 1, 140, 140, 142, 143, + 144, 144, 140, 141, 139, 1, 145, 145, + 147, 148, 145, 146, 149, 1, 150, 151, + 1, 152, 1, 153, 153, 155, 156, 157, + 157, 153, 154, 152, 1, 158, 158, 160, + 161, 158, 159, 162, 1, 163, 164, 1, + 165, 1, 166, 166, 168, 169, 170, 170, + 166, 167, 165, 1, 171, 171, 173, 174, + 171, 172, 175, 1, 176, 177, 1, 178, + 1, 179, 179, 181, 182, 183, 183, 179, + 180, 178, 1, 184, 184, 186, 187, 184, + 185, 188, 1, 189, 190, 1, 191, 1, + 192, 192, 194, 195, 196, 196, 192, 193, + 191, 1, 197, 197, 199, 200, 197, 198, + 201, 1, 202, 203, 1, 204, 1, 140, + 140, 142, 139, 144, 144, 140, 141, 138, + 1, 147, 147, 146, 146, 148, 147, 149, + 1, 153, 153, 155, 152, 157, 157, 153, + 154, 151, 1, 160, 160, 159, 159, 161, + 160, 162, 1, 166, 166, 168, 165, 170, + 170, 166, 167, 164, 1, 173, 173, 172, + 172, 174, 173, 175, 1, 179, 179, 181, + 178, 183, 183, 179, 180, 177, 1, 186, + 186, 185, 185, 187, 186, 188, 1, 192, + 192, 194, 191, 196, 196, 192, 193, 190, + 1, 199, 199, 198, 198, 200, 199, 201, + 1, 205, 205, 206, 1, 206, 1, 207, + 207, 208, 208, 209, 207, 210, 1, 211, + 211, 212, 212, 213, 211, 214, 1, 215, + 216, 1, 217, 1, 218, 218, 219, 1, + 219, 1, 220, 220, 221, 221, 222, 220, + 223, 1, 224, 224, 225, 225, 226, 224, + 227, 1, 228, 228, 229, 229, 230, 228, + 231, 1, 232, 233, 1, 234, 1, 235, + 235, 237, 238, 239, 239, 235, 236, 234, + 1, 240, 240, 242, 243, 240, 241, 244, + 1, 245, 246, 1, 247, 1, 248, 248, + 250, 251, 252, 252, 248, 249, 247, 1, + 253, 253, 255, 256, 253, 254, 257, 1, + 258, 259, 1, 260, 1, 261, 261, 263, + 264, 265, 265, 261, 262, 260, 1, 266, + 266, 268, 269, 266, 267, 270, 1, 271, + 272, 1, 273, 1, 235, 235, 237, 234, + 239, 239, 235, 236, 233, 1, 242, 242, + 241, 241, 243, 242, 244, 1, 248, 248, + 250, 247, 252, 252, 248, 249, 246, 1, + 255, 255, 254, 254, 256, 255, 257, 1, + 261, 261, 263, 260, 265, 265, 261, 262, + 259, 1, 268, 268, 267, 267, 269, 268, + 270, 1, 274, 274, 275, 1, 275, 1, + 276, 276, 277, 277, 278, 276, 279, 1, + 280, 280, 281, 281, 282, 280, 283, 1, + 284, 285, 1, 286, 1, 287, 287, 289, + 290, 291, 291, 287, 288, 286, 1, 292, + 292, 294, 295, 292, 293, 296, 1, 297, + 298, 1, 299, 1, 300, 300, 302, 303, + 304, 304, 300, 301, 299, 1, 305, 305, + 307, 308, 305, 306, 309, 1, 310, 311, + 1, 312, 1, 313, 313, 315, 316, 317, + 317, 313, 314, 312, 1, 318, 318, 320, + 321, 318, 319, 322, 1, 323, 324, 1, + 325, 1, 287, 287, 289, 286, 291, 291, + 287, 288, 285, 1, 294, 294, 293, 293, + 295, 294, 296, 1, 300, 300, 302, 299, + 304, 304, 300, 301, 298, 1, 307, 307, + 306, 306, 308, 307, 309, 1, 313, 313, + 315, 312, 317, 317, 313, 314, 311, 1, + 320, 320, 319, 319, 321, 320, 322, 1, + 326, 326, 327, 1, 327, 1, 328, 328, + 329, 329, 330, 328, 331, 1, 332, 332, + 333, 333, 334, 332, 335, 1, 336, 337, + 1, 338, 1, 339, 339, 341, 342, 343, + 343, 339, 340, 338, 1, 344, 344, 346, + 347, 344, 345, 348, 1, 349, 350, 1, + 351, 1, 339, 339, 341, 338, 343, 343, + 339, 340, 337, 1, 346, 346, 345, 345, + 347, 346, 348, 1, 352, 352, 353, 1, + 353, 1, 354, 354, 355, 355, 356, 354, + 357, 1, 358, 358, 359, 359, 360, 358, + 361, 1, 362, 363, 1, 364, 1, 365, + 365, 366, 1, 366, 1, 367, 367, 368, + 368, 369, 367, 370, 1, 371, 371, 372, + 372, 373, 371, 374, 1, 375, 375, 376, + 376, 377, 375, 378, 1, 379, 379, 380, + 380, 381, 379, 382, 1, 383, 383, 384, + 384, 385, 383, 386, 1, 12, 12, 14, + 11, 16, 16, 12, 13, 10, 1, 19, + 19, 18, 18, 20, 19, 21, 1, 387, + 387, 388, 1, 388, 1, 389, 389, 390, + 390, 391, 389, 392, 1, 393, 393, 394, + 394, 395, 393, 396, 1, 397, 397, 398, + 398, 399, 397, 400, 1, 401, 401, 402, + 402, 403, 401, 404, 1, 405, 405, 406, + 1, 406, 1, 12, 12, 14, 15, 12, + 13, 406, 1, 407, 407, 408, 1, 408, + 1, 339, 339, 341, 342, 339, 340, 408, + 1, 409, 409, 410, 1, 410, 1, 313, + 313, 315, 316, 313, 314, 410, 1, 411, + 411, 412, 1, 412, 1, 300, 300, 302, + 303, 300, 301, 412, 1, 413, 413, 414, + 1, 414, 1, 287, 287, 289, 290, 287, + 288, 414, 1, 415, 415, 416, 1, 416, + 1, 261, 261, 263, 264, 261, 262, 416, + 1, 417, 417, 418, 1, 418, 1, 248, + 248, 250, 251, 248, 249, 418, 1, 419, + 419, 420, 1, 420, 1, 235, 235, 237, + 238, 235, 236, 420, 1, 421, 421, 422, + 1, 422, 1, 192, 192, 194, 195, 192, + 193, 422, 1, 423, 423, 424, 1, 424, + 1, 179, 179, 181, 182, 179, 180, 424, + 1, 425, 425, 426, 1, 426, 1, 166, + 166, 168, 169, 166, 167, 426, 1, 427, + 427, 428, 1, 428, 1, 153, 153, 155, + 156, 153, 154, 428, 1, 429, 429, 430, + 1, 430, 1, 140, 140, 142, 143, 140, + 141, 430, 1, 431, 431, 432, 1, 432, + 1, 117, 117, 116, 116, 118, 117, 119, + 1, 433, 433, 434, 1, 434, 1, 110, + 110, 112, 113, 110, 111, 434, 1, 110, + 110, 112, 109, 114, 114, 110, 111, 108, + 1, 104, 104, 103, 103, 105, 104, 106, + 1, 435, 435, 437, 438, 435, 436, 439, + 1, 440, 440, 441, 442, 443, 440, 1, + 444, 444, 445, 1, 445, 1, 446, 446, + 447, 1, 447, 1, 28, 28, 30, 31, + 28, 29, 447, 1, 448, 448, 449, 450, + 448, 1, 451, 451, 453, 454, 455, 456, + 457, 458, 459, 460, 461, 462, 463, 464, + 465, 466, 467, 457, 468, 469, 470, 471, + 472, 473, 474, 465, 451, 452, 24, 1, + 475, 475, 41, 43, 476, 477, 478, 479, + 449, 480, 481, 482, 483, 484, 485, 486, + 487, 488, 450, 489, 490, 491, 492, 484, + 475, 42, 44, 1, 493, 493, 495, 496, + 497, 498, 499, 500, 501, 502, 503, 504, + 505, 506, 507, 508, 509, 499, 510, 511, + 512, 513, 514, 515, 516, 507, 493, 494, + 40, 1, 493, 493, 495, 40, 497, 498, + 499, 500, 501, 502, 503, 504, 505, 506, + 507, 508, 509, 499, 510, 511, 512, 513, + 514, 515, 516, 507, 493, 494, 39, 1, + 517, 517, 519, 520, 521, 522, 523, 524, + 525, 526, 527, 528, 529, 530, 531, 532, + 533, 523, 534, 535, 536, 537, 538, 539, + 540, 531, 517, 518, 122, 1, 541, 541, + 49, 51, 476, 477, 478, 479, 449, 480, + 481, 482, 483, 484, 485, 486, 487, 488, + 450, 489, 490, 491, 492, 484, 541, 50, + 52, 1, 542, 542, 544, 545, 546, 547, + 548, 549, 550, 551, 552, 553, 554, 555, + 556, 557, 558, 548, 559, 560, 561, 562, + 563, 564, 565, 556, 542, 543, 204, 1, + 566, 566, 133, 135, 476, 477, 478, 479, + 449, 480, 481, 482, 483, 484, 485, 486, + 487, 488, 450, 489, 490, 491, 492, 484, + 566, 134, 136, 1, 542, 542, 544, 204, + 546, 547, 548, 549, 550, 551, 552, 553, + 554, 555, 556, 557, 558, 548, 559, 560, + 561, 562, 563, 564, 565, 556, 542, 543, + 203, 1, 542, 542, 544, 545, 546, 547, + 549, 550, 551, 552, 553, 554, 555, 556, + 557, 558, 559, 560, 561, 562, 563, 564, + 565, 556, 542, 543, 206, 1, 567, 567, + 569, 570, 571, 572, 573, 574, 575, 576, + 577, 578, 579, 580, 581, 582, 583, 573, + 584, 585, 586, 587, 588, 589, 590, 581, + 567, 568, 217, 1, 591, 591, 211, 213, + 476, 477, 478, 479, 449, 480, 481, 482, + 483, 484, 485, 486, 487, 488, 450, 489, + 490, 491, 492, 484, 591, 212, 214, 1, + 567, 567, 569, 217, 571, 572, 573, 574, + 575, 576, 577, 578, 579, 580, 581, 582, + 583, 573, 584, 585, 586, 587, 588, 589, + 590, 581, 567, 568, 216, 1, 567, 567, + 569, 570, 571, 572, 574, 575, 576, 577, + 578, 579, 580, 581, 582, 583, 584, 585, + 586, 587, 588, 589, 590, 581, 567, 568, + 219, 1, 592, 592, 594, 595, 596, 597, + 598, 599, 600, 601, 602, 603, 604, 605, + 606, 607, 608, 598, 609, 610, 611, 612, + 613, 614, 615, 606, 592, 593, 273, 1, + 616, 616, 228, 230, 476, 477, 478, 479, + 449, 480, 481, 482, 483, 484, 485, 486, + 487, 488, 450, 489, 490, 491, 492, 484, + 616, 229, 231, 1, 592, 592, 594, 273, + 596, 597, 598, 599, 600, 601, 602, 603, + 604, 605, 606, 607, 608, 598, 609, 610, + 611, 612, 613, 614, 615, 606, 592, 593, + 272, 1, 592, 592, 594, 595, 596, 597, + 599, 600, 601, 602, 603, 604, 605, 606, + 607, 608, 609, 610, 611, 612, 613, 614, + 615, 606, 592, 593, 275, 1, 617, 617, + 619, 620, 621, 622, 623, 624, 625, 626, + 627, 628, 629, 630, 631, 632, 633, 623, + 634, 635, 636, 637, 638, 639, 640, 631, + 617, 618, 325, 1, 641, 641, 280, 282, + 476, 477, 478, 479, 449, 480, 481, 482, + 483, 484, 485, 486, 487, 488, 450, 489, + 490, 491, 492, 484, 641, 281, 283, 1, + 617, 617, 619, 325, 621, 622, 623, 624, + 625, 626, 627, 628, 629, 630, 631, 632, + 633, 623, 634, 635, 636, 637, 638, 639, + 640, 631, 617, 618, 324, 1, 617, 617, + 619, 620, 621, 622, 624, 625, 626, 627, + 628, 629, 630, 631, 632, 633, 634, 635, + 636, 637, 638, 639, 640, 631, 617, 618, + 327, 1, 642, 642, 644, 645, 646, 647, + 648, 649, 650, 651, 652, 653, 654, 655, + 656, 657, 658, 648, 659, 660, 661, 662, + 663, 664, 665, 656, 642, 643, 351, 1, + 666, 666, 332, 334, 476, 477, 478, 479, + 449, 480, 481, 482, 483, 484, 485, 486, + 487, 488, 450, 489, 490, 491, 492, 484, + 666, 333, 335, 1, 642, 642, 644, 351, + 646, 647, 648, 649, 650, 651, 652, 653, + 654, 655, 656, 657, 658, 648, 659, 660, + 661, 662, 663, 664, 665, 656, 642, 643, + 350, 1, 642, 642, 644, 645, 646, 647, + 649, 650, 651, 652, 653, 654, 655, 656, + 657, 658, 659, 660, 661, 662, 663, 664, + 665, 656, 642, 643, 353, 1, 667, 667, + 669, 670, 671, 672, 673, 674, 675, 676, + 677, 678, 679, 680, 681, 682, 683, 673, + 684, 685, 686, 687, 688, 689, 690, 681, + 667, 668, 364, 1, 691, 691, 358, 360, + 476, 477, 478, 479, 449, 480, 481, 482, + 483, 484, 485, 486, 487, 488, 450, 489, + 490, 491, 492, 484, 691, 359, 361, 1, + 667, 667, 669, 364, 671, 672, 673, 674, + 675, 676, 677, 678, 679, 680, 681, 682, + 683, 673, 684, 685, 686, 687, 688, 689, + 690, 681, 667, 668, 363, 1, 667, 667, + 669, 670, 671, 672, 674, 675, 676, 677, + 678, 679, 680, 681, 682, 683, 684, 685, + 686, 687, 688, 689, 690, 681, 667, 668, + 366, 1, 692, 692, 693, 694, 695, 696, + 697, 698, 699, 700, 701, 702, 703, 704, + 705, 706, 707, 708, 709, 710, 711, 702, + 692, 1, 712, 712, 476, 477, 478, 479, + 449, 480, 481, 482, 483, 484, 485, 486, + 487, 488, 450, 489, 490, 491, 492, 484, + 712, 1, 451, 451, 453, 24, 455, 456, + 457, 458, 459, 460, 461, 462, 463, 464, + 465, 466, 467, 457, 468, 469, 470, 471, + 472, 473, 474, 465, 451, 452, 23, 1, + 451, 451, 453, 454, 455, 456, 458, 459, + 460, 461, 462, 463, 464, 465, 466, 467, + 468, 469, 470, 471, 472, 473, 474, 465, + 451, 452, 388, 1, 517, 517, 519, 520, + 521, 522, 524, 525, 526, 527, 528, 529, + 530, 531, 532, 533, 534, 535, 536, 537, + 538, 539, 540, 531, 517, 518, 432, 1, + 517, 517, 519, 122, 521, 522, 523, 524, + 525, 526, 527, 528, 529, 530, 531, 532, + 533, 523, 534, 535, 536, 537, 538, 539, + 540, 531, 517, 518, 121, 1, 493, 493, + 495, 496, 497, 498, 500, 501, 502, 503, + 504, 505, 506, 507, 508, 509, 510, 511, + 512, 513, 514, 515, 516, 507, 493, 494, + 445, 1, 0 +}; + +static const short _svg_path_trans_targs[] = { + 2, 0, 3, 4, 172, 2, 3, 4, + 172, 4, 172, 5, 6, 7, 173, 8, + 180, 6, 7, 173, 8, 267, 8, 267, + 235, 10, 16, 11, 12, 13, 17, 14, + 231, 12, 13, 17, 14, 238, 14, 238, + 237, 15, 9, 10, 16, 19, 20, 21, + 44, 19, 20, 21, 44, 21, 44, 22, + 23, 24, 45, 25, 55, 23, 24, 45, + 25, 46, 25, 46, 26, 27, 28, 47, + 29, 52, 27, 28, 47, 29, 48, 29, + 48, 30, 31, 32, 49, 31, 32, 33, + 228, 34, 35, 36, 227, 34, 35, 36, + 227, 37, 38, 226, 39, 225, 37, 38, + 226, 39, 225, 39, 225, 40, 41, 42, + 221, 43, 222, 41, 42, 221, 43, 270, + 43, 270, 239, 50, 51, 53, 54, 56, + 57, 59, 60, 61, 82, 59, 60, 61, + 82, 61, 82, 62, 63, 64, 83, 65, + 216, 63, 64, 83, 65, 84, 65, 84, + 66, 67, 68, 85, 69, 213, 67, 68, + 85, 69, 86, 69, 86, 70, 71, 72, + 87, 73, 210, 71, 72, 87, 73, 88, + 73, 88, 74, 75, 76, 89, 77, 207, + 75, 76, 89, 77, 90, 77, 90, 78, + 79, 80, 91, 81, 204, 79, 80, 91, + 81, 243, 81, 243, 241, 93, 244, 95, + 96, 97, 247, 95, 96, 97, 247, 97, + 247, 245, 99, 248, 15, 9, 10, 16, + 102, 103, 104, 117, 102, 103, 104, 117, + 104, 117, 105, 106, 107, 118, 108, 201, + 106, 107, 118, 108, 119, 108, 119, 109, + 110, 111, 120, 112, 198, 110, 111, 120, + 112, 121, 112, 121, 113, 114, 115, 122, + 116, 195, 114, 115, 122, 116, 251, 116, + 251, 249, 124, 252, 126, 127, 128, 141, + 126, 127, 128, 141, 128, 141, 129, 130, + 131, 142, 132, 192, 130, 131, 142, 132, + 143, 132, 143, 133, 134, 135, 144, 136, + 189, 134, 135, 144, 136, 145, 136, 145, + 137, 138, 139, 146, 140, 186, 138, 139, + 146, 140, 255, 140, 255, 253, 148, 256, + 150, 151, 152, 157, 150, 151, 152, 157, + 152, 157, 153, 154, 155, 158, 156, 183, + 154, 155, 158, 156, 259, 156, 259, 257, + 160, 260, 162, 163, 164, 263, 162, 163, + 164, 263, 164, 263, 261, 166, 264, 19, + 20, 21, 44, 59, 60, 61, 82, 95, + 96, 97, 247, 15, 9, 10, 16, 2, + 3, 4, 172, 175, 268, 102, 103, 104, + 117, 126, 127, 128, 141, 150, 151, 152, + 157, 162, 163, 164, 263, 181, 182, 184, + 185, 187, 188, 190, 191, 193, 194, 196, + 197, 199, 200, 202, 203, 205, 206, 208, + 209, 211, 212, 214, 215, 217, 218, 220, + 269, 223, 224, 37, 38, 226, 39, 225, + 34, 35, 36, 227, 230, 271, 232, 233, + 234, 1, 171, 236, 9, 15, 10, 18, + 58, 174, 94, 100, 1, 101, 125, 149, + 161, 265, 167, 168, 169, 170, 171, 176, + 177, 178, 179, 236, 18, 58, 94, 100, + 101, 125, 149, 161, 265, 167, 168, 169, + 170, 176, 177, 178, 179, 236, 9, 15, + 10, 18, 58, 229, 94, 100, 1, 101, + 125, 149, 161, 265, 167, 168, 169, 170, + 171, 176, 177, 178, 179, 240, 20, 19, + 21, 18, 58, 219, 94, 100, 1, 101, + 125, 149, 161, 265, 167, 168, 169, 170, + 171, 176, 177, 178, 179, 240, 242, 60, + 59, 61, 18, 58, 92, 94, 100, 1, + 101, 125, 149, 161, 265, 167, 168, 169, + 170, 171, 176, 177, 178, 179, 242, 246, + 96, 95, 97, 18, 58, 98, 94, 100, + 1, 101, 125, 149, 161, 265, 167, 168, + 169, 170, 171, 176, 177, 178, 179, 246, + 250, 103, 102, 104, 18, 58, 123, 94, + 100, 1, 101, 125, 149, 161, 265, 167, + 168, 169, 170, 171, 176, 177, 178, 179, + 250, 254, 127, 126, 128, 18, 58, 147, + 94, 100, 1, 101, 125, 149, 161, 265, + 167, 168, 169, 170, 171, 176, 177, 178, + 179, 254, 258, 151, 150, 152, 18, 58, + 159, 94, 100, 1, 101, 125, 149, 161, + 265, 167, 168, 169, 170, 171, 176, 177, + 178, 179, 258, 262, 163, 162, 164, 18, + 58, 165, 94, 100, 1, 101, 125, 149, + 161, 265, 167, 168, 169, 170, 171, 176, + 177, 178, 179, 262, 266, 18, 58, 94, + 100, 1, 101, 125, 149, 161, 265, 167, + 168, 169, 170, 171, 176, 177, 178, 179, + 266 +}; + +static const char _svg_path_trans_actions[] = { + 9, 0, 51, 51, 51, 0, 1, 1, + 1, 0, 0, 0, 3, 15, 3, 15, + 0, 0, 1, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 3, 15, 3, 15, + 0, 0, 1, 0, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 9, 51, 51, + 51, 0, 1, 1, 1, 0, 0, 0, + 3, 15, 3, 15, 0, 0, 1, 0, + 1, 1, 0, 0, 0, 3, 15, 3, + 15, 0, 0, 1, 0, 1, 1, 0, + 0, 0, 3, 3, 0, 0, 0, 0, + 0, 7, 7, 7, 7, 0, 0, 0, + 0, 7, 48, 7, 48, 48, 0, 1, + 0, 1, 1, 0, 0, 0, 3, 15, + 3, 15, 0, 0, 1, 0, 1, 1, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 9, 51, 51, 51, 0, 1, 1, + 1, 0, 0, 0, 3, 15, 3, 15, + 0, 0, 1, 0, 1, 1, 0, 0, + 0, 3, 15, 3, 15, 0, 0, 1, + 0, 1, 1, 0, 0, 0, 3, 15, + 3, 15, 0, 0, 1, 0, 1, 1, + 0, 0, 0, 3, 15, 3, 15, 0, + 0, 1, 0, 1, 1, 0, 0, 0, + 3, 15, 3, 15, 0, 0, 1, 0, + 1, 1, 0, 0, 0, 0, 0, 9, + 51, 51, 51, 0, 1, 1, 1, 0, + 0, 0, 0, 0, 9, 51, 51, 51, + 9, 51, 51, 51, 0, 1, 1, 1, + 0, 0, 0, 3, 15, 3, 15, 0, + 0, 1, 0, 1, 1, 0, 0, 0, + 3, 15, 3, 15, 0, 0, 1, 0, + 1, 1, 0, 0, 0, 3, 15, 3, + 15, 0, 0, 1, 0, 1, 1, 0, + 0, 0, 0, 0, 9, 51, 51, 51, + 0, 1, 1, 1, 0, 0, 0, 3, + 15, 3, 15, 0, 0, 1, 0, 1, + 1, 0, 0, 0, 3, 15, 3, 15, + 0, 0, 1, 0, 1, 1, 0, 0, + 0, 3, 15, 3, 15, 0, 0, 1, + 0, 1, 1, 0, 0, 0, 0, 0, + 9, 51, 51, 51, 0, 1, 1, 1, + 0, 0, 0, 3, 15, 3, 15, 0, + 0, 1, 0, 1, 1, 0, 0, 0, + 0, 0, 9, 51, 51, 51, 0, 1, + 1, 1, 0, 0, 0, 0, 0, 11, + 54, 54, 54, 11, 54, 54, 54, 11, + 54, 54, 54, 11, 54, 54, 54, 11, + 54, 54, 54, 0, 0, 11, 54, 54, + 54, 11, 54, 54, 54, 11, 54, 54, + 54, 11, 54, 54, 54, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 5, 45, 5, 45, 45, + 5, 5, 5, 5, 0, 0, 0, 0, + 0, 0, 0, 18, 57, 18, 57, 18, + 18, 0, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 18, 18, 18, 18, 18, + 18, 18, 18, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 21, 61, 21, + 61, 21, 21, 0, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 42, 89, 42, + 89, 42, 42, 0, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 0, 30, 73, + 30, 73, 30, 30, 0, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 0, 24, + 65, 24, 65, 24, 24, 0, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 0, + 36, 81, 36, 81, 36, 36, 0, 36, + 36, 36, 36, 36, 36, 36, 36, 36, + 36, 36, 36, 36, 36, 36, 36, 36, + 0, 33, 77, 33, 77, 33, 33, 0, + 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, + 33, 0, 39, 85, 39, 85, 39, 39, + 0, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 39, 39, 39, 39, 39, 39, + 39, 39, 0, 27, 69, 27, 69, 27, + 27, 0, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 0, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, + 0 +}; + +static const char _svg_path_eof_actions[] = { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 18, 0, 21, 21, 42, + 0, 30, 0, 30, 30, 24, 0, 24, + 24, 36, 0, 36, 36, 33, 0, 33, + 33, 39, 0, 39, 39, 27, 0, 27, + 27, 13, 0, 18, 18, 42, 42, 21 +}; + +static const int svg_path_start = 234; +static const int svg_path_first_final = 234; + +static const int svg_path_en_main = 234; + + +#line 47 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + + +SVGPathParser::SVGPathParser(PathSink &sink) + : _absolute(false) + , _sink(sink) + , _z_snap_threshold(0) + , _curve(NULL) +{ + reset(); +} + +SVGPathParser::~SVGPathParser() +{ + delete _curve; +} + +void SVGPathParser::reset() { + _absolute = false; + _current = _initial = Point(0, 0); + _quad_tangent = _cubic_tangent = Point(0, 0); + _params.clear(); + delete _curve; + _curve = NULL; + + +#line 1113 "/home/mc/lib2geom/src/2geom/svg-path-parser.cpp" + { + cs = svg_path_start; + } + +#line 73 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + +} + +void SVGPathParser::parse(char const *str, int len) +{ + if (len < 0) { + len = std::strlen(str); + } + _parse(str, str + len, true); +} + +void SVGPathParser::parse(std::string const &s) +{ + _parse(s.c_str(), s.c_str() + s.size(), true); +} + +void SVGPathParser::feed(char const *str, int len) +{ + if (len < 0) { + len = std::strlen(str); + } + _parse(str, str + len, false); +} + +void SVGPathParser::feed(std::string const &s) +{ + _parse(s.c_str(), s.c_str() + s.size(), false); +} + +void SVGPathParser::finish() +{ + char const *empty = ""; + _parse(empty, empty, true); +} + +void SVGPathParser::_push(Coord value) +{ + _params.push_back(value); +} + +Coord SVGPathParser::_pop() +{ + Coord value = _params.back(); + _params.pop_back(); + return value; +} + +bool SVGPathParser::_pop_flag() +{ + return _pop() != 0.0; +} + +Coord SVGPathParser::_pop_coord(Dim2 axis) +{ + if (_absolute) { + return _pop(); + } else { + return _pop() + _current[axis]; + } +} + +Point SVGPathParser::_pop_point() +{ + Coord y = _pop_coord(Y); + Coord x = _pop_coord(X); + return Point(x, y); +} + +void SVGPathParser::_moveTo(Point const &p) +{ + _pushCurve(NULL); // flush + _sink.moveTo(p); + _quad_tangent = _cubic_tangent = _current = _initial = p; +} + +void SVGPathParser::_lineTo(Point const &p) +{ + _pushCurve(new LineSegment(_current, p)); + _quad_tangent = _cubic_tangent = _current = p; +} + +void SVGPathParser::_curveTo(Point const &c0, Point const &c1, Point const &p) +{ + _pushCurve(new CubicBezier(_current, c0, c1, p)); + _quad_tangent = _current = p; + _cubic_tangent = p + ( p - c1 ); +} + +void SVGPathParser::_quadTo(Point const &c, Point const &p) +{ + _pushCurve(new QuadraticBezier(_current, c, p)); + _cubic_tangent = _current = p; + _quad_tangent = p + ( p - c ); +} + +void SVGPathParser::_arcTo(Coord rx, Coord ry, Coord angle, + bool large_arc, bool sweep, Point const &p) +{ + if (_current == p) { + return; // ignore invalid (ambiguous) arc segments where start and end point are the same (per SVG spec) + } + + _pushCurve(new EllipticalArc(_current, fabs(rx), fabs(ry), angle, large_arc, sweep, p)); + _quad_tangent = _cubic_tangent = _current = p; +} + +void SVGPathParser::_closePath() +{ + if (_curve && (!_absolute || !_moveto_was_absolute) && + are_near(_initial, _current, _z_snap_threshold)) + { + _curve->setFinal(_initial); + } + + _pushCurve(NULL); // flush + _sink.closePath(); + _quad_tangent = _cubic_tangent = _current = _initial; +} + +void SVGPathParser::_pushCurve(Curve *c) +{ + if (_curve) { + _sink.feed(*_curve, false); + delete _curve; + } + _curve = c; +} + +void SVGPathParser::_parse(char const *str, char const *strend, bool finish) +{ + char const *p = str; + char const *pe = strend; + char const *eof = finish ? pe : NULL; + char const *start = NULL; + + +#line 1255 "/home/mc/lib2geom/src/2geom/svg-path-parser.cpp" + { + int _klen; + unsigned int _trans; + const char *_acts; + unsigned int _nacts; + const char *_keys; + + if ( p == pe ) + goto _test_eof; + if ( cs == 0 ) + goto _out; +_resume: + _keys = _svg_path_trans_keys + _svg_path_key_offsets[cs]; + _trans = _svg_path_index_offsets[cs]; + + _klen = _svg_path_single_lengths[cs]; + if ( _klen > 0 ) { + const char *_lower = _keys; + const char *_mid; + const char *_upper = _keys + _klen - 1; + while (1) { + if ( _upper < _lower ) + break; + + _mid = _lower + ((_upper-_lower) >> 1); + if ( (*p) < *_mid ) + _upper = _mid - 1; + else if ( (*p) > *_mid ) + _lower = _mid + 1; + else { + _trans += (unsigned int)(_mid - _keys); + goto _match; + } + } + _keys += _klen; + _trans += _klen; + } + + _klen = _svg_path_range_lengths[cs]; + if ( _klen > 0 ) { + const char *_lower = _keys; + const char *_mid; + const char *_upper = _keys + (_klen<<1) - 2; + while (1) { + if ( _upper < _lower ) + break; + + _mid = _lower + (((_upper-_lower) >> 1) & ~1); + if ( (*p) < _mid[0] ) + _upper = _mid - 2; + else if ( (*p) > _mid[1] ) + _lower = _mid + 2; + else { + _trans += (unsigned int)((_mid - _keys)>>1); + goto _match; + } + } + _trans += _klen; + } + +_match: + _trans = _svg_path_indicies[_trans]; + cs = _svg_path_trans_targs[_trans]; + + if ( _svg_path_trans_actions[_trans] == 0 ) + goto _again; + + _acts = _svg_path_actions + _svg_path_trans_actions[_trans]; + _nacts = (unsigned int) *_acts++; + while ( _nacts-- > 0 ) + { + switch ( *_acts++ ) + { + case 0: +#line 209 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + start = p; + } + break; + case 1: +#line 213 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + if (start) { + std::string buf(start, p); + _push(g_ascii_strtod(buf.c_str(), NULL)); + start = NULL; + } else { + std::string buf(str, p); + _push(g_ascii_strtod((_number_part + buf).c_str(), NULL)); + _number_part.clear(); + } + } + break; + case 2: +#line 225 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _push(1.0); + } + break; + case 3: +#line 229 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _push(0.0); + } + break; + case 4: +#line 233 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _absolute = true; + } + break; + case 5: +#line 237 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _absolute = false; + } + break; + case 6: +#line 241 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _moveto_was_absolute = _absolute; + _moveTo(_pop_point()); + } + break; + case 7: +#line 246 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(_pop_point()); + } + break; + case 8: +#line 250 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(Point(_pop_coord(X), _current[Y])); + } + break; + case 9: +#line 254 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(Point(_current[X], _pop_coord(Y))); + } + break; + case 10: +#line 258 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c1 = _pop_point(); + Point c0 = _pop_point(); + _curveTo(c0, c1, p); + } + break; + case 11: +#line 265 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c1 = _pop_point(); + _curveTo(_cubic_tangent, c1, p); + } + break; + case 12: +#line 271 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c = _pop_point(); + _quadTo(c, p); + } + break; + case 13: +#line 277 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + _quadTo(_quad_tangent, p); + } + break; + case 14: +#line 282 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point point = _pop_point(); + bool sweep = _pop_flag(); + bool large_arc = _pop_flag(); + double angle = rad_from_deg(_pop()); + double ry = _pop(); + double rx = _pop(); + + _arcTo(rx, ry, angle, large_arc, sweep, point); + } + break; + case 15: +#line 293 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _closePath(); + } + break; +#line 1449 "/home/mc/lib2geom/src/2geom/svg-path-parser.cpp" + } + } + +_again: + if ( cs == 0 ) + goto _out; + if ( ++p != pe ) + goto _resume; + _test_eof: {} + if ( p == eof ) + { + const char *__acts = _svg_path_actions + _svg_path_eof_actions[cs]; + unsigned int __nacts = (unsigned int) *__acts++; + while ( __nacts-- > 0 ) { + switch ( *__acts++ ) { + case 1: +#line 213 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + if (start) { + std::string buf(start, p); + _push(g_ascii_strtod(buf.c_str(), NULL)); + start = NULL; + } else { + std::string buf(str, p); + _push(g_ascii_strtod((_number_part + buf).c_str(), NULL)); + _number_part.clear(); + } + } + break; + case 6: +#line 241 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _moveto_was_absolute = _absolute; + _moveTo(_pop_point()); + } + break; + case 7: +#line 246 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(_pop_point()); + } + break; + case 8: +#line 250 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(Point(_pop_coord(X), _current[Y])); + } + break; + case 9: +#line 254 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _lineTo(Point(_current[X], _pop_coord(Y))); + } + break; + case 10: +#line 258 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c1 = _pop_point(); + Point c0 = _pop_point(); + _curveTo(c0, c1, p); + } + break; + case 11: +#line 265 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c1 = _pop_point(); + _curveTo(_cubic_tangent, c1, p); + } + break; + case 12: +#line 271 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + Point c = _pop_point(); + _quadTo(c, p); + } + break; + case 13: +#line 277 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point p = _pop_point(); + _quadTo(_quad_tangent, p); + } + break; + case 14: +#line 282 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + Point point = _pop_point(); + bool sweep = _pop_flag(); + bool large_arc = _pop_flag(); + double angle = rad_from_deg(_pop()); + double ry = _pop(); + double rx = _pop(); + + _arcTo(rx, ry, angle, large_arc, sweep, point); + } + break; + case 15: +#line 293 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + { + _closePath(); + } + break; +#line 1555 "/home/mc/lib2geom/src/2geom/svg-path-parser.cpp" + } + } + } + + _out: {} + } + +#line 435 "/home/mc/lib2geom/src/2geom/svg-path-parser.rl" + + + if (finish) { + if (cs < svg_path_first_final) { + throw SVGPathParseError(); + } + } else if (start != NULL) { + _number_part = std::string(start, pe); + } + + if (finish) { + _pushCurve(NULL); + _sink.flush(); + reset(); + } +} + +void parse_svg_path(char const *str, PathSink &sink) +{ + SVGPathParser parser(sink); + parser.parse(str); +} + +void parse_svg_path_file(FILE *fi, PathSink &sink) +{ + static const size_t BUFFER_SIZE = 4096; + char buffer[BUFFER_SIZE]; + size_t bytes_read; + SVGPathParser parser(sink); + + while (true) { + bytes_read = fread(buffer, 1, BUFFER_SIZE, fi); + if (bytes_read < BUFFER_SIZE) { + parser.parse(buffer, bytes_read); + break; + } else { + parser.feed(buffer, bytes_read); + } + } +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/svg-path-parser.h b/src/2geom/svg-path-parser.h new file mode 100644 index 0000000..e25316c --- /dev/null +++ b/src/2geom/svg-path-parser.h @@ -0,0 +1,199 @@ +/** + * \file + * \brief parse SVG path specifications + * + * Copyright 2007 MenTaLguY + * Copyright 2007 Aaron Spike + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_SVG_PATH_PARSER_H +#define LIB2GEOM_SEEN_SVG_PATH_PARSER_H + +#include +#include +#include +#include +#include +#include <2geom/exception.h> +#include <2geom/point.h> +#include <2geom/path-sink.h> +#include <2geom/forward.h> + +namespace Geom { + +/** @brief Read SVG path data and feed it to a PathSink + * + * This class provides an interface to an SVG path data parser written in Ragel. + * It supports parsing the path data either at once or block-by-block. + * Use the parse() functions to parse complete data and the feed() and finish() + * functions to parse partial data. + * + * The parser will call the appropriate methods on the PathSink supplied + * at construction. To store the path in memory as a PathVector, pass + * a PathBuilder. You can also use one of the freestanding helper functions + * if you don't need to parse data block-by-block. + * + * @ingroup Paths + */ +class SVGPathParser { +public: + SVGPathParser(PathSink &sink); + ~SVGPathParser(); + + /** @brief Reset internal state. + * Discards the internal state associated with partially parsed data, + * letting you start from scratch. Note that any partial data written + * to the path sink is not affected - you need to clear it yourself. */ + void reset(); + + /** @brief Parse a C-style string. + * The path sink is flushed and the internal state is reset after this call. + * Note that the state is not reset before this method, so you can use it to + * process the last block of partial data. + * @param str String to parse + * @param len Length of string or -1 if null-terminated */ + void parse(char const *str, int len = -1); + /** @brief Parse an STL string. */ + void parse(std::string const &s); + + /** @brief Parse a part of path data stored in a C-style string. + * This method does not reset internal state, so it can be called multiple + * times to parse successive blocks of a longer SVG path data string. + * To finish parsing, call finish() after the final block or call parse() + * with the last block of data. + * @param str String to parse + * @param len Length of string or -1 if null-terminated */ + void feed(char const *str, int len = -1); + /** @brief Parse a part of path data stored in an STL string. */ + void feed(std::string const &s); + + /** @brief Finalize parsing. + * After the last block of data was submitted with feed(), call this method + * to finalize parsing, flush the path sink and reset internal state. + * You should not call this after parse(). */ + void finish(); + + /** @brief Set the threshold for considering the closing segment degenerate. + * When the current point was reached by a relative command, is closer + * to the initial point of the path than the specified threshold + * and a 'z' is encountered, the last segment will be adjusted instead so that + * the closing segment has exactly zero length. This is useful when reading + * SVG 1.1 paths that have non-linear final segments written in relative + * coordinates, which always suffer from some loss of precision. SVG 2 + * allows alternate placement of 'z' which does not have this problem. */ + void setZSnapThreshold(Coord threshold) { _z_snap_threshold = threshold; } + Coord zSnapThreshold() const { return _z_snap_threshold; } + +private: + bool _absolute; + bool _moveto_was_absolute; + Point _current; + Point _initial; + Point _cubic_tangent; + Point _quad_tangent; + std::vector _params; + PathSink &_sink; + Coord _z_snap_threshold; + Curve *_curve; + + int cs; + std::string _number_part; + + void _push(Coord value); + Coord _pop(); + bool _pop_flag(); + Coord _pop_coord(Geom::Dim2 axis); + Point _pop_point(); + void _moveTo(Point const &p); + void _lineTo(Point const &p); + void _curveTo(Point const &c0, Point const &c1, Point const &p); + void _quadTo(Point const &c, Point const &p); + void _arcTo(double rx, double ry, double angle, + bool large_arc, bool sweep, Point const &p); + void _closePath(); + void _pushCurve(Curve *c); + + void _parse(char const *str, char const *strend, bool finish); +}; + +/** @brief Feed SVG path data to the specified sink + * @ingroup Paths */ +void parse_svg_path(char const *str, PathSink &sink); +/** @brief Feed SVG path data to the specified sink + * @ingroup Paths */ +inline void parse_svg_path(std::string const &str, PathSink &sink) { + parse_svg_path(str.c_str(), sink); +} +/** Feed SVG path data from a C stream to the specified sink + * @ingroup Paths */ +void parse_svg_path_file(FILE *fi, PathSink &sink); + +/** @brief Create path vector from SVG path data stored in a C string + * @ingroup Paths */ +inline PathVector parse_svg_path(char const *str) { + PathVector ret; + SubpathInserter iter(ret); + PathIteratorSink generator(iter); + + parse_svg_path(str, generator); + return ret; +} + +/** @brief Create path vector from a C stream with SVG path data + * @ingroup Paths */ +inline PathVector read_svgd_f(FILE * fi) { + PathVector ret; + SubpathInserter iter(ret); + PathIteratorSink generator(iter); + + parse_svg_path_file(fi, generator); + return ret; +} + +/** @brief Create path vector from SVG path data stored in a file + * @ingroup Paths */ +inline PathVector read_svgd(char const *filename) { + FILE* fi = fopen(filename, "r"); + if(fi == NULL) throw(std::runtime_error("Error opening file")); + PathVector out = read_svgd_f(fi); + fclose(fi); + return out; +} + +} // end namespace Geom + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/svg-path-writer.cpp b/src/2geom/svg-path-writer.cpp new file mode 100644 index 0000000..c484a17 --- /dev/null +++ b/src/2geom/svg-path-writer.cpp @@ -0,0 +1,296 @@ +/** @file + * @brief Path sink which writes an SVG-compatible command string + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include +#include <2geom/coord.h> +#include <2geom/svg-path-writer.h> +#include + +namespace Geom { + +static inline bool is_digit(char c) { + return c >= '0' && c <= '9'; +} + +SVGPathWriter::SVGPathWriter() + : _epsilon(0) + , _precision(-1) + , _optimize(false) + , _use_shorthands(true) + , _command(0) +{ + // always use C locale for number formatting + _ns.imbue(std::locale::classic()); + _ns.unsetf(std::ios::floatfield); +} + +void SVGPathWriter::moveTo(Point const &p) +{ + _setCommand('M'); + _current_pars.push_back(p[X]); + _current_pars.push_back(p[Y]); + + _current = _subpath_start = _quad_tangent = _cubic_tangent = p; + if (!_optimize) { + flush(); + } +} + +void SVGPathWriter::lineTo(Point const &p) +{ + // The weird setting of _current is to avoid drift with many almost-aligned segments + // The additional conditions ensure that the smaller dimension is rounded to zero + bool written = false; + if (_use_shorthands) { + Point r = _current - p; + if (are_near(p[X], _current[X], _epsilon) && std::abs(r[X]) < std::abs(r[Y])) { + // emit vlineto + _setCommand('V'); + _current_pars.push_back(p[Y]); + _current[Y] = p[Y]; + written = true; + } else if (are_near(p[Y], _current[Y], _epsilon) && std::abs(r[Y]) < std::abs(r[X])) { + // emit hlineto + _setCommand('H'); + _current_pars.push_back(p[X]); + _current[X] = p[X]; + written = true; + } + } + + if (!written) { + // emit normal lineto + if (_command != 'M' && _command != 'L') { + _setCommand('L'); + } + _current_pars.push_back(p[X]); + _current_pars.push_back(p[Y]); + _current = p; + } + + _cubic_tangent = _quad_tangent = _current; + if (!_optimize) { + flush(); + } +} + +void SVGPathWriter::quadTo(Point const &c, Point const &p) +{ + bool shorthand = _use_shorthands && are_near(c, _quad_tangent, _epsilon); + + _setCommand(shorthand ? 'T' : 'Q'); + if (!shorthand) { + _current_pars.push_back(c[X]); + _current_pars.push_back(c[Y]); + } + _current_pars.push_back(p[X]); + _current_pars.push_back(p[Y]); + + _current = _cubic_tangent = p; + _quad_tangent = p + (p - c); + if (!_optimize) { + flush(); + } +} + +void SVGPathWriter::curveTo(Point const &p1, Point const &p2, Point const &p3) +{ + bool shorthand = _use_shorthands && are_near(p1, _cubic_tangent, _epsilon); + + _setCommand(shorthand ? 'S' : 'C'); + if (!shorthand) { + _current_pars.push_back(p1[X]); + _current_pars.push_back(p1[Y]); + } + _current_pars.push_back(p2[X]); + _current_pars.push_back(p2[Y]); + _current_pars.push_back(p3[X]); + _current_pars.push_back(p3[Y]); + + _current = _quad_tangent = p3; + _cubic_tangent = p3 + (p3 - p2); + if (!_optimize) { + flush(); + } +} + +void SVGPathWriter::arcTo(double rx, double ry, double angle, + bool large_arc, bool sweep, Point const &p) +{ + _setCommand('A'); + _current_pars.push_back(rx); + _current_pars.push_back(ry); + _current_pars.push_back(deg_from_rad(angle)); + _current_pars.push_back(large_arc ? 1. : 0.); + _current_pars.push_back(sweep ? 1. : 0.); + _current_pars.push_back(p[X]); + _current_pars.push_back(p[Y]); + + _current = _quad_tangent = _cubic_tangent = p; + if (!_optimize) { + flush(); + } +} + +void SVGPathWriter::closePath() +{ + flush(); + if (_optimize) { + _s << "z"; + } else { + _s << " z"; + } + _current = _quad_tangent = _cubic_tangent = _subpath_start; +} + +void SVGPathWriter::flush() +{ + if (_command == 0 || _current_pars.empty()) return; + + if (_optimize) { + _s << _command; + } else { + if (_s.tellp() != 0) { + _s << ' '; + } + _s << _command; + } + + char lastchar = _command; + bool contained_dot = false; + + for (unsigned i = 0; i < _current_pars.size(); ++i) { + // TODO: optimize the use of absolute / relative coords + std::string cs = _formatCoord(_current_pars[i]); + + // Separator handling logic. + // Floating point values can end with a digit or dot + // and start with a digit, a plus or minus sign, or a dot. + // The following cases require a separator: + // * digit-digit + // * digit-dot (only if the previous number didn't contain a dot) + // * dot-digit + if (_optimize) { + // C++11: change to front() + char firstchar = cs[0]; + if (is_digit(lastchar)) { + if (is_digit(firstchar)) { + _s << " "; + } else if (firstchar == '.' && !contained_dot) { + _s << " "; + } + } else if (lastchar == '.' && is_digit(firstchar)) { + _s << " "; + } + _s << cs; + + // C++11: change to back() + lastchar = cs[cs.length()-1]; + contained_dot = cs.find('.') != std::string::npos; + } else { + _s << " " << cs; + } + } + _current_pars.clear(); + _command = 0; +} + +void SVGPathWriter::clear() +{ + _s.clear(); + _s.str(""); + _ns.clear(); + _ns.str(""); + _command = 0; + _current_pars.clear(); + _current = Point(0,0); + _subpath_start = Point(0,0); +} + +void SVGPathWriter::setPrecision(int prec) +{ + _precision = prec; + if (prec < 0) { + _epsilon = 0; + } else { + _epsilon = std::pow(10., -prec); + _ns << std::setprecision(_precision); + } +} + +void SVGPathWriter::_setCommand(char cmd) +{ + if (_command != 0 && _command != cmd) { + flush(); + } + _command = cmd; +} + +std::string SVGPathWriter::_formatCoord(Coord par) +{ + std::string ret; + if (_precision < 0) { + ret = format_coord_shortest(par); + } else { + _ns << par; + ret = _ns.str(); + _ns.clear(); + _ns.str(""); + } + return ret; +} + + +std::string write_svg_path(PathVector const &pv, int prec, bool optimize, bool shorthands) +{ + SVGPathWriter writer; + writer.setPrecision(prec); + writer.setOptimize(optimize); + writer.setUseShorthands(shorthands); + + writer.feed(pv); + return writer.str(); +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/svg-path-writer.h b/src/2geom/svg-path-writer.h new file mode 100644 index 0000000..e639541 --- /dev/null +++ b/src/2geom/svg-path-writer.h @@ -0,0 +1,122 @@ +/** @file + * @brief Path sink which writes an SVG-compatible command string + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2014 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SVG_PATH_WRITER_H +#define LIB2GEOM_SEEN_SVG_PATH_WRITER_H + +#include <2geom/path-sink.h> +#include + +namespace Geom { + +/** @brief Serialize paths to SVG path data strings. + * You can access the generated string by calling the str() method. + * @ingroup Paths + */ +class SVGPathWriter + : public PathSink +{ +public: + SVGPathWriter(); + ~SVGPathWriter() {} + + void moveTo(Point const &p); + void lineTo(Point const &p); + void quadTo(Point const &c, Point const &p); + void curveTo(Point const &c0, Point const &c1, Point const &p); + void arcTo(double rx, double ry, double angle, + bool large_arc, bool sweep, Point const &p); + void closePath(); + void flush(); + + /// Clear any path data written so far. + void clear(); + + /** @brief Set output precision. + * When the parameter is negative, the path writer enters a verbatim mode + * which preserves all values exactly. */ + void setPrecision(int prec); + + /** @brief Enable or disable length optimization. + * + * When set to true, the path writer will optimize the generated path data + * for minimum length. However, this will make the data less readable, + * because spaces between commands and coordinates will be omitted where + * unnecessary for correct parsing. + * + * When set to false, the string will be a straightforward, partially redundant + * representation of the passed commands, optimized for readability. + * Commands and coordinates will always be separated by spaces and the command + * symbol will not be omitted for multiple consecutive commands of the same type. + * + * Length optimization is turned off by default. */ + void setOptimize(bool opt) { _optimize = opt; } + + /** @brief Enable or disable the use of V, H, T and S commands where possible. + * Shorthands are turned on by default. */ + void setUseShorthands(bool use) { _use_shorthands = use; } + + /// Retrieve the generated path data string. + std::string str() const { return _s.str(); } + +private: + void _setCommand(char cmd); + std::string _formatCoord(Coord par); + + std::ostringstream _s, _ns; + std::vector _current_pars; + Point _subpath_start; + Point _current; + Point _quad_tangent; + Point _cubic_tangent; + Coord _epsilon; + int _precision; + bool _optimize; + bool _use_shorthands; + char _command; +}; + +std::string write_svg_path(PathVector const &pv, int prec = -1, bool optimize = false, bool shorthands = true); + +} // namespace Geom + +#endif // LIB2GEOM_SEEN_SVG_PATH_WRITER_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 : diff --git a/src/2geom/sweep-bounds.cpp b/src/2geom/sweep-bounds.cpp new file mode 100644 index 0000000..48f168b --- /dev/null +++ b/src/2geom/sweep-bounds.cpp @@ -0,0 +1,157 @@ +#include <2geom/sweep-bounds.h> + +#include + +namespace Geom { + +struct Event { + double x; + unsigned ix; + bool closing; + Event(double pos, unsigned i, bool c) : x(pos), ix(i), closing(c) {} +// Lexicographic ordering by x then closing + bool operator<(Event const &other) const { + if(x < other.x) return true; + if(x > other.x) return false; + return closing < other.closing; + } + bool operator==(Event const &other) const { + return other.x == x && other.ix == ix && other.closing == closing; + } +}; + +std::vector > fake_cull(unsigned a, unsigned b); + +/** + * \brief Make a list of pairs of self intersections in a list of Rects. + * + * \param rs: vector of Rect. + * \param d: dimension to sweep along + * + * [(A = rs[i], B = rs[j]) for i,J in enumerate(pairs) for j in J] + * then A.left <= B.left + */ + +std::vector > sweep_bounds(std::vector rs, Dim2 d) { + std::vector events; events.reserve(rs.size()*2); + std::vector > pairs(rs.size()); + + for(unsigned i = 0; i < rs.size(); i++) { + events.push_back(Event(rs[i][d].min(), i, false)); + events.push_back(Event(rs[i][d].max(), i, true)); + } + std::sort(events.begin(), events.end()); + + std::vector open; + for(unsigned i = 0; i < events.size(); i++) { + unsigned ix = events[i].ix; + if(events[i].closing) { + std::vector::iterator iter = std::find(open.begin(), open.end(), ix); + //if(iter != open.end()) + open.erase(iter); + } else { + for(unsigned j = 0; j < open.size(); j++) { + unsigned jx = open[j]; + if(rs[jx][1-d].intersects(rs[ix][1-d])) { + pairs[jx].push_back(ix); + } + } + open.push_back(ix); + } + } + return pairs; +} + +/** + * \brief Make a list of pairs of red-blue intersections between two lists of Rects. + * + * \param a: vector of Rect. + * \param b: vector of Rect. + * \param d: dimension to scan along + * + * [(A = rs[i], B = rs[j]) for i,J in enumerate(pairs) for j in J] + * then A.left <= B.left, A in a, B in b + */ +std::vector > sweep_bounds(std::vector a, std::vector b, Dim2 d) { + std::vector > pairs(a.size()); + if(a.empty() || b.empty()) return pairs; + std::vector events[2]; + events[0].reserve(a.size()*2); + events[1].reserve(b.size()*2); + + for(unsigned n = 0; n < 2; n++) { + unsigned sz = n ? b.size() : a.size(); + events[n].reserve(sz*2); + for(unsigned i = 0; i < sz; i++) { + Rect r = n ? b[i] : a[i]; + events[n].push_back(Event(r[d].min(), i, false)); + events[n].push_back(Event(r[d].max(), i, true)); + } + std::sort(events[n].begin(), events[n].end()); + } + + std::vector open[2]; + bool n = events[1].front() < events[0].front(); + {// As elegant as putting the initialiser in the for was, it upsets some legacy compilers (MS VS C++) + unsigned i[] = {0,0}; + for(; i[n] < events[n].size();) { + unsigned ix = events[n][i[n]].ix; + bool closing = events[n][i[n]].closing; + //std::cout << n << "[" << ix << "] - " << (closing ? "closer" : "opener") << "\n"; + if(closing) { + open[n].erase(std::find(open[n].begin(), open[n].end(), ix)); + } else { + if(n) { + //n = 1 + //opening a B, add to all open a + for(unsigned j = 0; j < open[0].size(); j++) { + unsigned jx = open[0][j]; + if(a[jx][1-d].intersects(b[ix][1-d])) { + pairs[jx].push_back(ix); + } + } + } else { + //n = 0 + //opening an A, add all open b + for(unsigned j = 0; j < open[1].size(); j++) { + unsigned jx = open[1][j]; + if(b[jx][1-d].intersects(a[ix][1-d])) { + pairs[ix].push_back(jx); + } + } + } + open[n].push_back(ix); + } + i[n]++; + if(i[n]>=events[n].size()) {break;} + n = (events[!n][i[!n]] < events[n][i[n]]) ? !n : n; + }} + return pairs; +} + +//Fake cull, until the switch to the real sweep is made. +std::vector > fake_cull(unsigned a, unsigned b) { + std::vector > ret; + + std::vector all; + for(unsigned j = 0; j < b; j++) + all.push_back(j); + + for(unsigned i = 0; i < a; i++) + ret.push_back(all); + + return ret; +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sweep-bounds.h b/src/2geom/sweep-bounds.h new file mode 100644 index 0000000..e0ebf29 --- /dev/null +++ b/src/2geom/sweep-bounds.h @@ -0,0 +1,62 @@ +/** + * \file + * \brief Sweepline intersection of groups of rectangles + *//* + * Authors: + * ? + * + * Copyright ?-? authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_SWEEP_H +#define LIB2GEOM_SEEN_SWEEP_H + +#include +#include <2geom/d2.h> + +namespace Geom { + +std::vector > +sweep_bounds(std::vector, Dim2 dim = X); + +std::vector > +sweep_bounds(std::vector, std::vector, Dim2 dim = X); + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/sweeper.h b/src/2geom/sweeper.h new file mode 100644 index 0000000..3532df2 --- /dev/null +++ b/src/2geom/sweeper.h @@ -0,0 +1,190 @@ +/** @file + * @brief Class for implementing sweepline algorithms + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright 2015 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_SWEEPER_H +#define LIB2GEOM_SEEN_SWEEPER_H + +#include <2geom/coord.h> +#include +#include +#include +#include + +namespace Geom { + +// exposition only +template +class SweepVector { +public: + typedef typename std::vector::const_iterator ItemIterator; + + SweepVector(std::vector const &v) + : _items(v) + {} + + std::vector const &items() { return _items; } + Interval itemBounds(ItemIterator /*ii*/) { return Interval(); } + + void addActiveItem(ItemIterator /*ii*/) {} + void removeActiveItem(ItemIterator /*ii*/) {} + +private: + std::vector const &_items; +}; + +/** @brief Generic sweepline algorithm. + * + * This class encapsulates an algorithm that sorts the objects according + * to their bounds, then moves an imaginary line (sweepline) over those + * bounds from left to right. Objects are added to the active list when + * the line starts intersecting their bounds, and removed when it completely + * passes over them. + * + * To use this, create a class that exposes the following methods: + * - Range items() - returns a forward iterable range of items that will be swept. + * - Interval itemBounds(iterator i) - given an iterator from the above range, + * compute the bounding interval of the referenced item in the direction of sweep. + * - void addActiveItem(iterator i) - add an item to the active list. + * - void removeActiveItem(iterator i) - remove an item from the active list. + * + * Create the object, then instantiate this template with the above class + * as the template parameter, pass it the constructed object of the class, + * and call the process() method. + * + * A good choice for the active list is a Boost intrusive list, which allows + * you to get an iterator from a value in constant time. + * + * Look in path.cpp for example usage. + * + * @tparam Item The type of items to sweep + * @tparam SweepTraits Traits class that defines the items' bounds, + * how to interpret them and how to sort the events + * @ingroup Utilities + */ +template +class Sweeper { +public: + typedef typename SweepSet::ItemIterator Iter; + + explicit Sweeper(SweepSet &set) + : _set(set) + { + std::size_t sz = std::distance(set.items().begin(), set.items().end()); + _entry_events.reserve(sz); + _exit_events.reserve(sz); + } + + /** @brief Process entry and exit events. + * This will iterate over all inserted items, calling the methods + * addActiveItem and removeActiveItem on the SweepSet passed at construction + * according to the order of the boundaries of each item. */ + void process() { + if (_set.items().empty()) return; + + Iter last = _set.items().end(); + for (Iter i = _set.items().begin(); i != last; ++i) { + Interval b = _set.itemBounds(i); + // guard against NANs + assert(b.min() == b.min() && b.max() == b.max()); + _entry_events.push_back(Event(b.max(), i)); + _exit_events.push_back(Event(b.min(), i)); + } + + boost::make_heap(_entry_events); + boost::make_heap(_exit_events); + + Event next_entry = _get_next(_entry_events); + Event next_exit = _get_next(_exit_events); + + while (next_entry || next_exit) { + assert(next_exit); + + if (!next_entry || next_exit > next_entry) { + // exit event - remove record from active list + _set.removeActiveItem(next_exit.item); + next_exit = _get_next(_exit_events); + } else { + // entry event - add record to active list + _set.addActiveItem(next_entry.item); + next_entry = _get_next(_entry_events); + } + } + } + +private: + struct Event + : boost::totally_ordered + { + Coord coord; + Iter item; + + Event(Coord c, Iter const &i) + : coord(c), item(i) + {} + Event() + : coord(nan("")), item() + {} + bool operator<(Event const &other) const { return coord < other.coord; } + bool operator==(Event const &other) const { return coord == other.coord; } + operator bool() const { return !std::isnan(coord); } + }; + + static Event _get_next(std::vector &heap) { + if (heap.empty()) { + Event e; + return e; + } + boost::pop_heap(heap); + Event ret = heap.back(); + heap.pop_back(); + return ret; + } + + SweepSet &_set; + std::vector _entry_events; + std::vector _exit_events; +}; + +} // namespace Geom + +#endif // !LIB2GEOM_SEEN_SWEEPER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/transforms.cpp b/src/2geom/transforms.cpp new file mode 100644 index 0000000..41d3952 --- /dev/null +++ b/src/2geom/transforms.cpp @@ -0,0 +1,205 @@ +/** + * @file + * @brief Affine transformation classes + *//* + * Authors: + * ? + * Krzysztof KosiÅ„ski + * Johan Engelen + * + * Copyright ?-2012 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#include +#include <2geom/point.h> +#include <2geom/transforms.h> +#include <2geom/rect.h> + +namespace Geom { + +/** @brief Zoom between rectangles. + * Given two rectangles, compute a zoom that maps one to the other. + * Rectangles are assumed to have the same aspect ratio. */ +Zoom Zoom::map_rect(Rect const &old_r, Rect const &new_r) +{ + Zoom ret; + ret._scale = new_r.width() / old_r.width(); + ret._trans = new_r.min() - old_r.min(); + return ret; +} + +// Point transformation methods. +Point &Point::operator*=(Translate const &t) +{ + _pt[X] += t.vec[X]; + _pt[Y] += t.vec[Y]; + return *this; +} +Point &Point::operator*=(Scale const &s) +{ + _pt[X] *= s.vec[X]; + _pt[Y] *= s.vec[Y]; + return *this; +} +Point &Point::operator*=(Rotate const &r) +{ + double x = _pt[X], y = _pt[Y]; + _pt[X] = x * r.vec[X] - y * r.vec[Y]; + _pt[Y] = y * r.vec[X] + x * r.vec[Y]; + return *this; +} +Point &Point::operator*=(HShear const &h) +{ + _pt[X] += h.f * _pt[X]; + return *this; +} +Point &Point::operator*=(VShear const &v) +{ + _pt[Y] += v.f * _pt[Y]; + return *this; +} +Point &Point::operator*=(Zoom const &z) +{ + _pt[X] += z._trans[X]; + _pt[Y] += z._trans[Y]; + _pt[X] *= z._scale; + _pt[Y] *= z._scale; + return *this; +} + +// Affine multiplication methods. + +/** @brief Combine this transformation with a translation. */ +Affine &Affine::operator*=(Translate const &t) { + _c[4] += t[X]; + _c[5] += t[Y]; + return *this; +} + +/** @brief Combine this transformation with scaling. */ +Affine &Affine::operator*=(Scale const &s) { + _c[0] *= s[X]; _c[1] *= s[Y]; + _c[2] *= s[X]; _c[3] *= s[Y]; + _c[4] *= s[X]; _c[5] *= s[Y]; + return *this; +} + +/** @brief Combine this transformation a rotation. */ +Affine &Affine::operator*=(Rotate const &r) { + // TODO: we just convert the Rotate to an Affine and use the existing operator*=() + // is there a better way? + *this *= (Affine) r; + return *this; +} + +/** @brief Combine this transformation with horizontal shearing (skew). */ +Affine &Affine::operator*=(HShear const &h) { + _c[0] += h.f * _c[1]; + _c[2] += h.f * _c[3]; + _c[4] += h.f * _c[5]; + return *this; +} + +/** @brief Combine this transformation with vertical shearing (skew). */ +Affine &Affine::operator*=(VShear const &v) { + _c[1] += v.f * _c[0]; + _c[3] += v.f * _c[2]; + _c[5] += v.f * _c[4]; + return *this; +} + +Affine &Affine::operator*=(Zoom const &z) { + _c[0] *= z._scale; _c[1] *= z._scale; + _c[2] *= z._scale; _c[3] *= z._scale; + _c[4] += z._trans[X]; _c[5] += z._trans[Y]; + _c[4] *= z._scale; _c[5] *= z._scale; + return *this; +} + +Affine Rotate::around(Point const &p, Coord angle) +{ + Affine result = Translate(-p) * Rotate(angle) * Translate(p); + return result; +} + +Affine reflection(Point const & vector, Point const & origin) +{ + Geom::Point vn = unit_vector(vector); + Coord cx2 = vn[X] * vn[X]; + Coord cy2 = vn[Y] * vn[Y]; + Coord c2xy = 2 * vn[X] * vn[Y]; + Affine mirror ( cx2 - cy2, c2xy, + c2xy, cy2 - cx2, + 0, 0 ); + return Translate(-origin) * mirror * Translate(origin); +} + +// this checks whether the requirements of TransformConcept are satisfied for all transforms. +// if you add a new transform type, include it here! +void check_transforms() +{ +#ifdef BOOST_CONCEPT_ASSERT + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); + BOOST_CONCEPT_ASSERT((TransformConcept)); // Affine is also a transform +#endif + + // check inter-transform multiplication + Affine m; + Translate t(Translate::identity()); + Scale s(Scale::identity()); + Rotate r(Rotate::identity()); + HShear h(HShear::identity()); + VShear v(VShear::identity()); + Zoom z(Zoom::identity()); + + // notice that the first column is always the same and enumerates all transform types, + // while the second one changes to each transform type in turn. + // cppcheck-suppress redundantAssignment + m = t * t; m = t * s; m = t * r; m = t * h; m = t * v; m = t * z; + m = s * t; m = s * s; m = s * r; m = s * h; m = s * v; m = s * z; + m = r * t; m = r * s; m = r * r; m = r * h; m = r * v; m = r * z; + m = h * t; m = h * s; m = h * r; m = h * h; m = h * v; m = h * z; + m = v * t; m = v * s; m = v * r; m = v * h; m = v * v; m = v * z; + m = z * t; m = z * s; m = z * r; m = z * h; m = z * v; m = z * z; +} + +} // namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/transforms.h b/src/2geom/transforms.h new file mode 100644 index 0000000..cc55e29 --- /dev/null +++ b/src/2geom/transforms.h @@ -0,0 +1,370 @@ +/** + * @file + * @brief Affine transformation classes + *//* + * Authors: + * ? + * Krzysztof KosiÅ„ski + * Johan Engelen + * + * Copyright ?-2012 Authors + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + */ + +#ifndef LIB2GEOM_SEEN_TRANSFORMS_H +#define LIB2GEOM_SEEN_TRANSFORMS_H + +#include +#include <2geom/forward.h> +#include <2geom/affine.h> +#include <2geom/angle.h> +#include + +namespace Geom { + +/** @brief Type requirements for transforms. + * @ingroup Concepts */ +template +struct TransformConcept { + T t, t2; + Affine m; + Point p; + bool bool_; + Coord epsilon; + void constraints() { + m = t; //implicit conversion + m *= t; + m = m * t; + m = t * m; + p *= t; + p = p * t; + t *= t; + t = t * t; + t = pow(t, 3); + bool_ = (t == t); + bool_ = (t != t); + t = T::identity(); + t = t.inverse(); + bool_ = are_near(t, t2); + bool_ = are_near(t, t2, epsilon); + } +}; + +/** @brief Base template for transforms. + * This class is an implementation detail and should not be used directly. */ +template +class TransformOperations + : boost::equality_comparable< T + , boost::multipliable< T + > > +{ +public: + template + Affine operator*(T2 const &t) const { + Affine ret(*static_cast(this)); ret *= t; return ret; + } +}; + +/** @brief Integer exponentiation for transforms. + * Negative exponents will yield the corresponding power of the inverse. This function + * can also be applied to matrices. + * @param t Affine or transform to exponantiate + * @param n Exponent + * @return \f$A^n\f$ if @a n is positive, \f$(A^{-1})^n\f$ if negative, identity if zero. + * @ingroup Transforms */ +template +T pow(T const &t, int n) { + BOOST_CONCEPT_ASSERT((TransformConcept)); + if (n == 0) return T::identity(); + T result(T::identity()); + T x(n < 0 ? t.inverse() : t); + if (n < 0) n = -n; + while ( n ) { // binary exponentiation - fast + if ( n & 1 ) { result *= x; --n; } + x *= x; n /= 2; + } + return result; +} + +/** @brief Translation by a vector. + * @ingroup Transforms */ +class Translate + : public TransformOperations< Translate > +{ + Point vec; +public: + /// Create a translation that doesn't do anything. + Translate() : vec(0, 0) {} + /// Construct a translation from its vector. + Translate(Point const &p) : vec(p) {} + /// Construct a translation from its coordinates. + Translate(Coord x, Coord y) : vec(x, y) {} + + operator Affine() const { Affine ret(1, 0, 0, 1, vec[X], vec[Y]); return ret; } + Coord operator[](Dim2 dim) const { return vec[dim]; } + Coord operator[](unsigned dim) const { return vec[dim]; } + Translate &operator*=(Translate const &o) { vec += o.vec; return *this; } + bool operator==(Translate const &o) const { return vec == o.vec; } + + Point vector() const { return vec; } + /// Get the inverse translation. + Translate inverse() const { return Translate(-vec); } + /// Get a translation that doesn't do anything. + static Translate identity() { Translate ret; return ret; } + + friend class Point; +}; + +inline bool are_near(Translate const &a, Translate const &b, Coord eps=EPSILON) { + return are_near(a[X], b[X], eps) && are_near(a[Y], b[Y], eps); +} + +/** @brief Scaling from the origin. + * During scaling, the point (0,0) will not move. To obtain a scale with a different + * invariant point, combine with translation to the origin and back. + * @ingroup Transforms */ +class Scale + : public TransformOperations< Scale > +{ + Point vec; +public: + /// Create a scaling that doesn't do anything. + Scale() : vec(1, 1) {} + /// Create a scaling from two scaling factors given as coordinates of a point. + explicit Scale(Point const &p) : vec(p) {} + /// Create a scaling from two scaling factors. + Scale(Coord x, Coord y) : vec(x, y) {} + /// Create an uniform scaling from a single scaling factor. + explicit Scale(Coord s) : vec(s, s) {} + inline operator Affine() const { Affine ret(vec[X], 0, 0, vec[Y], 0, 0); return ret; } + + Coord operator[](Dim2 d) const { return vec[d]; } + Coord operator[](unsigned d) const { return vec[d]; } + //TODO: should we keep these mutators? add them to the other transforms? + Coord &operator[](Dim2 d) { return vec[d]; } + Coord &operator[](unsigned d) { return vec[d]; } + Scale &operator*=(Scale const &b) { vec[X] *= b[X]; vec[Y] *= b[Y]; return *this; } + bool operator==(Scale const &o) const { return vec == o.vec; } + + Point vector() const { return vec; } + Scale inverse() const { return Scale(1./vec[0], 1./vec[1]); } + static Scale identity() { Scale ret; return ret; } + + friend class Point; +}; + +inline bool are_near(Scale const &a, Scale const &b, Coord eps=EPSILON) { + return are_near(a[X], b[X], eps) && are_near(a[Y], b[Y], eps); +} + +/** @brief Rotation around the origin. + * Combine with translations to the origin and back to get a rotation around a different point. + * @ingroup Transforms */ +class Rotate + : public TransformOperations< Rotate > +{ + Point vec; ///< @todo Convert to storing the angle, as it's more space-efficient. +public: + /// Construct a zero-degree rotation. + Rotate() : vec(1, 0) {} + /** @brief Construct a rotation from its angle in radians. + * Positive arguments correspond to counter-clockwise rotations (if Y grows upwards). */ + explicit Rotate(Coord theta) : vec(Point::polar(theta)) {} + /// Construct a rotation from its characteristic vector. + explicit Rotate(Point const &p) : vec(unit_vector(p)) {} + /// Construct a rotation from the coordinates of its characteristic vector. + explicit Rotate(Coord x, Coord y) { Rotate(Point(x, y)); } + operator Affine() const { Affine ret(vec[X], vec[Y], -vec[Y], vec[X], 0, 0); return ret; } + + /** @brief Get the characteristic vector of the rotation. + * @return A vector that would be obtained by applying this transform to the X versor. */ + Point vector() const { return vec; } + Coord angle() const { return atan2(vec); } + Coord operator[](Dim2 dim) const { return vec[dim]; } + Coord operator[](unsigned dim) const { return vec[dim]; } + Rotate &operator*=(Rotate const &o) { vec *= o; return *this; } + bool operator==(Rotate const &o) const { return vec == o.vec; } + Rotate inverse() const { + Rotate r; + r.vec = Point(vec[X], -vec[Y]); + return r; + } + /// @brief Get a zero-degree rotation. + static Rotate identity() { Rotate ret; return ret; } + /** @brief Construct a rotation from its angle in degrees. + * Positive arguments correspond to clockwise rotations if Y grows downwards. */ + static Rotate from_degrees(Coord deg) { + Coord rad = (deg / 180.0) * M_PI; + return Rotate(rad); + } + static Affine around(Point const &p, Coord angle); + + friend class Point; +}; + +inline bool are_near(Rotate const &a, Rotate const &b, Coord eps=EPSILON) { + return are_near(a[X], b[X], eps) && are_near(a[Y], b[Y], eps); +} + +/** @brief Common base for shearing transforms. + * This class is an implementation detail and should not be used directly. + * @ingroup Transforms */ +template +class ShearBase + : public TransformOperations< S > +{ +protected: + Coord f; + ShearBase(Coord _f) : f(_f) {} +public: + Coord factor() const { return f; } + void setFactor(Coord nf) { f = nf; } + S &operator*=(S const &s) { f += s.f; return static_cast(*this); } + bool operator==(S const &s) const { return f == s.f; } + S inverse() const { S ret(-f); return ret; } + static S identity() { S ret(0); return ret; } + + friend class Point; + friend class Affine; +}; + +/** @brief Horizontal shearing. + * Points on the X axis will not move. Combine with translations to get a shear + * with a different invariant line. + * @ingroup Transforms */ +class HShear + : public ShearBase +{ +public: + explicit HShear(Coord h) : ShearBase(h) {} + operator Affine() const { Affine ret(1, 0, f, 1, 0, 0); return ret; } +}; + +inline bool are_near(HShear const &a, HShear const &b, Coord eps=EPSILON) { + return are_near(a.factor(), b.factor(), eps); +} + +/** @brief Vertical shearing. + * Points on the Y axis will not move. Combine with translations to get a shear + * with a different invariant line. + * @ingroup Transforms */ +class VShear + : public ShearBase +{ +public: + explicit VShear(Coord h) : ShearBase(h) {} + operator Affine() const { Affine ret(1, f, 0, 1, 0, 0); return ret; } +}; + +inline bool are_near(VShear const &a, VShear const &b, Coord eps=EPSILON) { + return are_near(a.factor(), b.factor(), eps); +} + +/** @brief Combination of a translation and uniform scale. + * The translation part is applied first, then the result is scaled from the new origin. + * This way when the class is used to accumulate a zoom transform, trans always points + * to the new origin in original coordinates. + * @ingroup Transforms */ +class Zoom + : public TransformOperations< Zoom > +{ + Coord _scale; + Point _trans; + Zoom() : _scale(1), _trans() {} +public: + /// Construct a zoom from a scaling factor. + explicit Zoom(Coord s) : _scale(s), _trans() {} + /// Construct a zoom from a translation. + explicit Zoom(Translate const &t) : _scale(1), _trans(t.vector()) {} + /// Construct a zoom from a scaling factor and a translation. + Zoom(Coord s, Translate const &t) : _scale(s), _trans(t.vector()) {} + + operator Affine() const { + Affine ret(_scale, 0, 0, _scale, _trans[X] * _scale, _trans[Y] * _scale); + return ret; + } + Zoom &operator*=(Zoom const &z) { + _trans += z._trans / _scale; + _scale *= z._scale; + return *this; + } + bool operator==(Zoom const &z) const { return _scale == z._scale && _trans == z._trans; } + + Coord scale() const { return _scale; } + void setScale(Coord s) { _scale = s; } + Point translation() const { return _trans; } + void setTranslation(Point const &p) { _trans = p; } + Zoom inverse() const { Zoom ret(1/_scale, Translate(-_trans*_scale)); return ret; } + static Zoom identity() { Zoom ret(1.0); return ret; } + static Zoom map_rect(Rect const &old_r, Rect const &new_r); + + friend class Point; + friend class Affine; +}; + +inline bool are_near(Zoom const &a, Zoom const &b, Coord eps=EPSILON) { + return are_near(a.scale(), b.scale(), eps) && + are_near(a.translation(), b.translation(), eps); +} + +/** @brief Specialization of exponentiation for Scale. + * @relates Scale */ +template<> +inline Scale pow(Scale const &s, int n) { + Scale ret(::pow(s[X], n), ::pow(s[Y], n)); + return ret; +} +/** @brief Specialization of exponentiation for Translate. + * @relates Translate */ +template<> +inline Translate pow(Translate const &t, int n) { + Translate ret(t[X] * n, t[Y] * n); + return ret; +} + + +/** @brief Reflects objects about line. + * The line, defined by a vector along the line and a point on it, acts as a mirror. + * @ingroup Transforms + * @see Line::reflection() + */ +Affine reflection(Point const & vector, Point const & origin); + +//TODO: decomposition of Affine into some finite combination of the above classes + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_TRANSFORMS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/utils.cpp b/src/2geom/utils.cpp new file mode 100644 index 0000000..83d93cc --- /dev/null +++ b/src/2geom/utils.cpp @@ -0,0 +1,86 @@ +/** Various utility functions. + * + * Copyright 2008 Marco Cecchetti + * Copyright 2007 Johan Engelen + * Copyright 2006 Michael G. Sloan + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + + +#include <2geom/utils.h> + + +namespace Geom +{ + +// return a vector that contains all the binomial coefficients of degree n +void binomial_coefficients(std::vector& bc, std::size_t n) +{ + size_t s = n+1; + bc.clear(); + bc.resize(s); + bc[0] = 1; + for (size_t i = 1; i < n; ++i) + { + size_t k = i >> 1; + if (i & 1u) + { + bc[k+1] = bc[k] << 1; + } + for (size_t j = k; j > 0; --j) + { + bc[j] += bc[j-1]; + } + } + s >>= 1; + for (size_t i = 0; i < s; ++i) + { + bc[n-i] = bc[i]; + } +} + +} // end namespace Geom + + + + + + + + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/2geom/utils.h b/src/2geom/utils.h new file mode 100644 index 0000000..01579d8 --- /dev/null +++ b/src/2geom/utils.h @@ -0,0 +1,111 @@ +/** + * \file + * \brief Various utility functions. + *//* + * Copyright 2007 Johan Engelen + * Copyright 2006 Michael G. Sloan + * + * This library is free software; you can redistribute it and/or + * modify it either under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation + * (the "LGPL") or, at your option, under the terms of the Mozilla + * Public License Version 1.1 (the "MPL"). If you do not alter this + * notice, a recipient may use your version of this file under either + * the MPL or the LGPL. + * + * You should have received a copy of the LGPL along with this library + * in the file COPYING-LGPL-2.1; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * You should have received a copy of the MPL along with this library + * in the file COPYING-MPL-1.1 + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.1 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY + * OF ANY KIND, either express or implied. See the LGPL or the MPL for + * the specific language governing rights and limitations. + * + */ + +#ifndef LIB2GEOM_SEEN_UTILS_H +#define LIB2GEOM_SEEN_UTILS_H + +#include +#include +#include + +namespace Geom { + +// proper logical xor +inline bool logical_xor (bool a, bool b) { return (a || b) && !(a && b); } + +void binomial_coefficients(std::vector& bc, std::size_t n); + +struct EmptyClass {}; + +/** + * @brief Noncommutative multiplication helper. + * Generates operator*(T, U) from operator*=(T, U). Does not generate operator*(U, T) + * like boost::multipliable does. This makes it suitable for noncommutative cases, + * such as transforms. + */ +template +struct MultipliableNoncommutative : B +{ + friend T operator*(T const &lhs, U const &rhs) { + T nrv(lhs); nrv *= rhs; return nrv; + } +}; + +/** @brief Null output iterator + * Use this if you want to discard a result returned through an output iterator. */ +struct NullIterator + : public boost::output_iterator_helper +{ + NullIterator() {} + + template + void operator=(T const &) {} +}; + +/** @brief Get the next iterator in the container with wrap-around. + * If the iterator would become the end iterator after incrementing, + * return the begin iterator instead. */ +template +Iter cyclic_next(Iter i, Container &c) { + ++i; + if (i == c.end()) { + i = c.begin(); + } + return i; +} + +/** @brief Get the previous iterator in the container with wrap-around. + * If the passed iterator is the begin iterator, return the iterator + * just before the end iterator instead. */ +template +Iter cyclic_prior(Iter i, Container &c) { + if (i == c.begin()) { + i = c.end(); + } + --i; + return i; +} + +} // end namespace Geom + +#endif // LIB2GEOM_SEEN_UTILS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/3rdparty/CMakeLists.txt b/src/3rdparty/CMakeLists.txt new file mode 100644 index 0000000..2040eb6 --- /dev/null +++ b/src/3rdparty/CMakeLists.txt @@ -0,0 +1,7 @@ +#include(ExternalProject) +add_subdirectory(libuemf) +add_subdirectory(libcroco) +add_subdirectory(libdepixelize) +add_subdirectory(adaptagrams) +add_subdirectory(autotrace) + diff --git a/src/3rdparty/adaptagrams/CMakeLists.txt b/src/3rdparty/adaptagrams/CMakeLists.txt new file mode 100644 index 0000000..60db79d --- /dev/null +++ b/src/3rdparty/adaptagrams/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(libavoid) +add_subdirectory(libcola) +add_subdirectory(libvpsc) diff --git a/src/3rdparty/adaptagrams/libavoid/CMakeLists.txt b/src/3rdparty/adaptagrams/libavoid/CMakeLists.txt new file mode 100644 index 0000000..e14f8ea --- /dev/null +++ b/src/3rdparty/adaptagrams/libavoid/CMakeLists.txt @@ -0,0 +1,58 @@ + +set(libavoid_SRC + actioninfo.cpp + connectionpin.cpp + connector.cpp + connend.cpp + geometry.cpp + geomtypes.cpp + graph.cpp + hyperedge.cpp + hyperedgeimprover.cpp + hyperedgetree.cpp + junction.cpp + makepath.cpp + mtst.cpp + obstacle.cpp + orthogonal.cpp + router.cpp + scanline.cpp + shape.cpp + timer.cpp + vertices.cpp + viscluster.cpp + visibility.cpp + vpsc.cpp + + + # ------- + # Headers + actioninfo.h + assertions.h + connectionpin.h + connector.h + connend.h + debug.h + geometry.h + geomtypes.h + graph.h + hyperedge.h + hyperedgeimprover.h + hyperedgetree.h + junction.h + libavoid.h + makepath.h + mtst.h + obstacle.h + orthogonal.h + router.h + scanline.h + shape.h + timer.h + vertices.h + viscluster.h + visibility.h + vpsc.h +) +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/..") +add_inkscape_lib(avoid_LIB "${libavoid_SRC}") diff --git a/src/3rdparty/adaptagrams/libavoid/Doxyfile b/src/3rdparty/adaptagrams/libavoid/Doxyfile new file mode 100644 index 0000000..929c6d3 --- /dev/null +++ b/src/3rdparty/adaptagrams/libavoid/Doxyfile @@ -0,0 +1,2423 @@ +# Doxyfile 1.8.13 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = libavoid + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = YES + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 8 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 0. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = YES + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = YES + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = LIBAVOID_DOC + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = . \ + doc + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. + +FILE_PATTERNS = *.cpp \ + *.h \ + *.doc + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = doc/header.html + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NONE + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /

+ glimmer07@gmail.com +
+ + + + + + + Introduction + +This is the documentation for scripting Inkscape using Dbus. This framework was developed to let users quickly and easily write scripts to create or manipulate images in a variety of languages. Once the API has stabilized there will also be a C library that encapsulates the Dbus functionality. + + +The guiding principles behind the design of this API were: + + +Easy to use: Use of insider terms was limited where possible, and many functions have been simplified to provide a easy entry point for beginning users. Ideally one should not need any experience with Inkscape or even vector graphics to begin using the interface. At the same time, functions that can call arbitrary verbs or manipulate nodes and their attributes directly give knowledgeable users some flexibility. + + +Interactive: Since Dbus ties in with the main loop, users can mix scripting and mouse driven actions seamlessly. This allows for some unique uses but more importantly makes it easier for people to learn the API since they can play around with it in a scripting console, or even a simple python shell. + + +Responsive: Since one of the advantages of scripting is the ability to repeat actions many times with great precision it is obvious that the system would have to be fairly fast. The amount of overhead has been limited where possible and functions have been tested for speed. A system to pause updates and only refresh the display after a large number of operations have been completed, ensures that even very complicated renders will not take too long. + + + + + Concepts + + &Terms; + + + + + Reference + + + D-Bus API Reference + + + + Inkscape provides a D-Bus API for programs to interactively script vector graphics. + + + This API is not yet stable and is likely to change in the future. + + + + &dbus-Application; + &dbus-Document; + &dbus-Proposed; + + + + + + Index + + + + diff --git a/src/extension/dbus/doc/inkscapeDbusTerms.xml b/src/extension/dbus/doc/inkscapeDbusTerms.xml new file mode 100644 index 0000000..45f2d63 --- /dev/null +++ b/src/extension/dbus/doc/inkscapeDbusTerms.xml @@ -0,0 +1,142 @@ + + Connecting to the API + + + Overview + +There are really two Dbus interfaces provided by Inkscape. One is the application interface, which is constant, and allows one to control the Inkscape application as a whole and create new documents or windows. The second is the document interface. A document interface is automatically generated for every open window, and the commands sent to that interface will affect that particular window. + + +So the basic way of connecting goes like this: Connect to the session bus. Connect to the application interface. Request a new document. Connect to the newly created document interface using the name returned in the last step. Manipulate the document however you want (load files, create shapes, save, etc.) After the connection example there is a shortcut that will shorten this process somewhat in certain circumstances. + + + + Connection example + +Here is a basic example of connecting to the Bus and getting a new document. (In python for now because it's easy to read.) + + + +import dbus + +#get the session bus. +bus = dbus.SessionBus() + +#get the object for the application. +inkapp = bus.get_object('org.inkscape', + '/org/inkscape/application') + +#request a new desktop. +desk2 = inkapp.desktop_new(dbus_interface='org.inkscape.application') + +#get the object for that desktop. +inkdoc1 = bus.get_object('org.inkscape', desk2) + +#tell it what interface it is using so we don't have to type it for every method. +doc1 = dbus.Interface(inkdoc1, dbus_interface="org.inkscape.document") + +#use! +doc1.rectangle (0,0,100,100) + + + + + + Shortcut + +Here is a quicker way if you don't need multiple documents open at once. Since Inkscape starts automatically, and it always creates a blank document we can just connect to that. + + +WARNING: This may not always work, it also might connect you to a document that is in use if Inkscape was already running. Only recommended for testing/experimenting. + + + +import dbus + +#get the session bus. +bus = dbus.SessionBus() + +#get object +inkdoc1 = bus.get_object('org.inkscape', '/org/inkscape/desktop_0') + +#get interface +doc1 = dbus.Interface(inkdoc1, dbus_interface="org.inkscape.document") + +#ta-da +doc1.rectangle (0,0,100,100) + + + + + + + + Terminology + + + + Coordinate System + +The coordinate system used by this API may be different than what you are used to (although it is standard in the computer graphics industry.) Simply put the origin (0,0) is in the upper left hand corner of the document. X increases to the right and Y increases downwards. Therefore everything with positive coordinates is in the document. + + +For example: (100,100) would be just below and to the right of the top left corner of the document. + + + + + + Selections + +Selections are extremely useful ways of managing groups of objects and applying effects to all of them at once. Since much of Inkscapes core functionality is built around manipulating selections they are the key to much of this APIs utility. Manipulate the list of selected objects with selection_set(), selection_add(), and selection_box() and then call whatever selection function you need. + + + + + + Groups + +Groups are collections of objects that are treated as a single object. Groups have their own id and can be passed to any function that accepts an object, though some will not have any effect (groups ignore style for instance.) Groups can be transformed and occupy a single level in their layer. Objects within a group can still be modified using their ids, but this will not have any affect on the other group members. Functions like move_to may not work as expected if used on an object that is part of a group that has a transformation applied. + + + + + + Layers and Levels + +The basic idea is that things on top cover up things beneath them. The potentially confusing part is that Inkscape implements this in two ways: layers and levels. Levels are what order objects are in within a single layer. So the highest level object is still below all of the objects in the layer above it. layer_change_level() changes the order of layers and selection_change_level() changes the order of objects within a layer. + + +Changing the level of a selection also deserves some explanation. The selection_change_level() function can work in two ways. It can be absolute, "to_top" and "to_bottom" work like you'd expect, sending the entire selection to the top or bottom of that layer. But it can also be relative. "raise" and "lower" only work if there is another shape overlapping above or beneath the selection at the moment. Also if you have two objects selected and they are both occluded by a third, raising the selection once will only raise the first object in the selection above the third object. In other words selections don't move as a group. + + + + + + Style Strings + +Style strings look something like this: "fill:#ff0000;fill-opacity:1;stroke:#0000ff;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none". It is a string of key value pairs that determines the style of a particular object. Style strings affect most objects. They can be set all at once or specific key value pairs can be added one by one. Style strings can also be merged, with the new string replacing key/value pairs that it contains and leaving the rest as they were. One could also think of it as the new string taking any attributes it does not have and adopting them from the old string. + + + + + + Paths + +A path is a string representing a series of points, and how the line curves between the points. It looks something like this: "m 351.42857,296.64789 a 54.285713,87.14286 0 1 1 -108.57143,0 54.285713,87.14286 0 1 1 108.57143,0 z" and is usually found as an attribute of a shape with the label "d". All shapes except rectangles have this "d" attribute. + + +Just because a shape has a path does not mean it IS a path however. A path object has no attributes except the path and a style. Calling object_to_path() will convert any object to a path, stripping away any other attributes except id and style which stay the same. This will not change the visual appearance but you will no longer be able to use shape handles or affect it by changing any attributes except for "style", "d", and "transform". Some functions may require paths. + + + + + + Nodes + +To be written. + + + + + diff --git a/src/extension/dbus/doc/spec-to-docbook.xsl b/src/extension/dbus/doc/spec-to-docbook.xsl new file mode 100644 index 0000000..5282167 --- /dev/null +++ b/src/extension/dbus/doc/spec-to-docbook.xsl @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + + + + + + + + + + + + + + + + + + + + + + interface + + + + Methods + + + + + + + + + + + Signals + + + + + + + + + + + Implemented Interfaces + + Objects implementing also implements + org.freedesktop.DBus.Introspectable, + org.freedesktop.DBus.Properties + + + + + + + Properties + + + + + + + + + + + Description + + + + + + + Details + + + + + + + + + Signal Details + + + + + + + + + + + Property Details + + + + + + + + + + + + + + + + +: + + + + + + + + + + + + + + + + + + + + + + <anchor role="function"><xsl:attribute name="id"><xsl:value-of select="$basename"/>:<xsl:value-of select="@name"/></xsl:attribute></anchor>The "<xsl:value-of select="@name"/>" property + +'' + + + + + + + + + + + + + +: + + + + + + + + + + + + + + + + + + + + + <anchor role="function"><xsl:attribute name="id"><xsl:value-of select="$basename"/>::<xsl:value-of select="@name"/></xsl:attribute></anchor>The <xsl:value-of select="@name"/> signal + + () + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + : + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Since + + + + + + + + /> + + + + + + + is deprecated since version and should not be used in newly-written code. Use + + + + + : + + + :: + + + . + + + + + + + + + + + + + + + +instead. + + + + + + + + + + + + + + + + + +See also: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +: + + + + + + + + + + + + Errors + + + + : + + + + + + + + + + + + Permissions + + + + + + + + + + + + + + + + + + <anchor role="function"><xsl:attribute name="id"><xsl:value-of select="$basename"/>.<xsl:value-of select="@name"/></xsl:attribute></anchor><xsl:value-of select="@name"/> () + + () + + + + + + + + + + + + + + + + +:'' + + + + + + + + + + + + +::() + + + + + + + + + + + + +.() + + + + + +'' +, + + + + + +'' +, + + + + + + +'' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extension/dbus/document-interface.cpp b/src/extension/dbus/document-interface.cpp new file mode 100644 index 0000000..2384c50 --- /dev/null +++ b/src/extension/dbus/document-interface.cpp @@ -0,0 +1,1484 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is where the implementation of the DBus based document API lives. + * All the methods in here (except in the helper section) are + * designed to be called remotely via DBus. application-interface.cpp + * has the methods used to connect to the bus and get a document instance. + * + * Documentation for these methods is in document-interface.xml + * which is the "gold standard" as to how the interface should work. + * + * Authors: + * Soren Berg + * + * Copyright (C) 2009 Soren Berg + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +//#include "2geom/svg-path-parser.h" //get_node_coordinates +#include "inkscape-application.h" // create_window() + +#include "application-interface.h" +#include "desktop-style.h" //sp_desktop_get_style +#include "desktop.h" +#include "document-interface.h" +#include "document-undo.h" +#include "document.h" // getReprDoc() +#include "file.h" //IO +#include "inkscape.h" //inkscape_find_desktop_by_dkey, activate desktops +#include "layer-fns.h" //LPOS_BELOW +#include "layer-model.h" +#include "print.h" //IO +#include "selection-chemistry.h"// lots of selection functions +#include "selection.h" //selection struct +#include "style.h" //style_write +#include "text-editing.h" +#include "verbs.h" + +#include "helper/action-context.h" +#include "helper/action.h" //sp_action_perform + +#include "display/canvas-text.h" //text +#include "display/sp-canvas.h" //text + +#include "extension/output.h" //IO +#include "extension/system.h" //IO + +#include "live_effects/parameter/text.h" //text + +#include "object/sp-ellipse.h" +#include "object/sp-object.h" +#include "object/sp-root.h" + +#include "util/units.h" + +#include "xml/repr.h" //sp_repr_document_new + +#if 0 +#include +#include +#include +#include +#endif + + enum + { + OBJECT_MOVED_SIGNAL, + LAST_SIGNAL + }; + + static guint signals[LAST_SIGNAL] = { 0 }; + + +/**************************************************************************** + HELPER / SHORTCUT FUNCTIONS +****************************************************************************/ + +/* + * This function or the one below it translates the user input for an object + * into Inkscapes internal representation. It is called by almost every + * method so it should be as fast as possible. + * + * (eg turns "rect2234" to an SPObject or Inkscape::XML::Node) + * + * If the internal representation changes (No more 'id' attributes) this is the + * place to adjust things. + */ +Inkscape::XML::Node * +get_repr_by_name (SPDocument *doc, gchar *name, GError **error) +{ + /* ALTERNATIVE (is this faster if only repr is needed?) + Inkscape::XML::Node *node = sp_repr_lookup_name((doc->root)->repr, name); + */ + SPObject * obj = doc->getObjectById(name); + if (!obj) + { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OBJECT, "Object '%s' not found in document.", name); + return NULL; + } + return obj->getRepr(); +} + +/* + * See comment for get_repr_by_name, above. + */ +SPObject * +get_object_by_name (SPDocument *doc, gchar *name, GError **error) +{ + SPObject * obj = doc->getObjectById(name); + if (!obj) + { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OBJECT, "Object '%s' not found in document.", name); + return NULL; + } + return obj; +} + +/* + * Tests for NULL strings and throws an appropriate error. + * Every method that takes a string parameter (other than the + * name of an object, that's tested separately) should call this. + */ +gboolean +dbus_check_string (gchar *string, GError ** error, const gchar * errorstr) +{ + if (string == NULL) + { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OTHER, "%s", errorstr); + return FALSE; + } + return TRUE; +} + +/* + * This is used to return object values to the user + */ +const gchar * +get_name_from_object (SPObject * obj) +{ + return obj->getRepr()->attribute("id"); +} + +/* + * Some verbs (cut, paste) only work on the active layer. + * This makes sure that the document that is about to receive a command is active. + */ +void +desktop_ensure_active (SPDesktop* desk) { + if (desk != SP_ACTIVE_DESKTOP) + INKSCAPE.activate_desktop (desk); + return; +} + +gdouble +selection_get_center_x (Inkscape::Selection *sel){ + Geom::OptRect box = sel->documentBounds(SPItem::GEOMETRIC_BBOX); + return box ? box->midpoint()[Geom::X] : 0; +} + +gdouble +selection_get_center_y (Inkscape::Selection *sel){ + Geom::OptRect box = sel->documentBounds(SPItem::GEOMETRIC_BBOX); + return box ? box->midpoint()[Geom::Y] : 0; +} + +/* + * This function is used along with selection_restore to + * take advantage of functionality provided by a selection + * for a single object. + * + * It saves the current selection and sets the selection to + * the object specified. Any selection verb can be used on the + * object and then selection_restore is called, restoring the + * original selection. + * + * This should be mostly transparent to the user who need never + * know we never bothered to implement it separately. Although + * they might see the selection box flicker if used in a loop. + */ +std::vector +selection_swap(Inkscape::Selection *sel, gchar *name, GError **error) +{ + std::vector oldsel = std::vector(sel->objects().begin(), sel->objects().end()); + + sel->set(get_object_by_name(sel->layers()->getDocument(), name, error)); + return oldsel; +} + +/* + * See selection_swap, above + */ +void +selection_restore(Inkscape::Selection *sel, std::vector oldsel) +{ + // ... setList used to work here + sel->clear(); + sel->add(oldsel.begin(), oldsel.end()); +} + +/* + * Shortcut for creating a Node. + */ +Inkscape::XML::Node * +dbus_create_node (SPDocument *doc, const gchar *type) +{ + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + return xml_doc->createElement(type); +} + +/* + * Called by the shape creation functions. Gets the default style for the doc + * or sets it arbitrarily if none. + * + * There is probably a better way to do this (use the shape tools default styles) + * but I'm not sure how. + */ +gchar *finish_create_shape (DocumentInterface *doc_interface, GError ** /*error*/, Inkscape::XML::Node *newNode, gchar *desc) +{ + SPCSSAttr *style = NULL; + if (doc_interface->target.getDesktop()) { + style = sp_desktop_get_style(doc_interface->target.getDesktop(), TRUE); + } + if (style) { + Glib::ustring str; + sp_repr_css_write_string(style, str); + newNode->setAttributeOrRemoveIfEmpty("style", str); + } + else { + newNode->setAttribute("style", "fill:#0000ff;fill-opacity:1;stroke:#c900b9;stroke-width:0;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none", true); + } + + doc_interface->target.getSelection()->layers()->currentLayer()->appendChildRepr(newNode); + doc_interface->target.getSelection()->layers()->currentLayer()->updateRepr(); + + if (doc_interface->updates) { + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), 0, (gchar *)desc); + } + + return strdup(newNode->attribute("id")); +} + +/* + * This is the code used internally to call all the verbs. + * + * It handles error reporting and update pausing (which needs some work.) + * This is a good place to improve efficiency as it is called a lot. + * + * document_interface_call_verb is similar but is called by the user. + */ +gboolean +dbus_call_verb (DocumentInterface *doc_interface, int verbid, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + if ( desk ) { + desktop_ensure_active (desk); + } + Inkscape::Verb *verb = Inkscape::Verb::get( verbid ); + if ( verb ) { + SPAction *action = verb->get_action(doc_interface->target); + if ( action ) { + sp_action_perform( action, NULL ); + if (doc_interface->updates) + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), verb->get_code(), verb->get_tip()); + return TRUE; + } + } + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_VERB, "Verb failed to execute"); + return FALSE; +} + +/* + * Check that the desktop is not NULL. If it is NULL, set the error to a useful message. + */ +bool +ensure_desktop_valid(SPDesktop* desk, GError **error) +{ + if (desk) { + return true; + } + + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OTHER, "Document interface action requires a GUI"); + return false; +} + +/**************************************************************************** + DOCUMENT INTERFACE CLASS STUFF +****************************************************************************/ + +G_DEFINE_TYPE(DocumentInterface, document_interface, G_TYPE_OBJECT) + +static void +document_interface_finalize (GObject *object) +{ + G_OBJECT_CLASS (document_interface_parent_class)->finalize (object); +} + + +static void +document_interface_class_init (DocumentInterfaceClass *klass) +{ + GObjectClass *object_class; + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = document_interface_finalize; + signals[OBJECT_MOVED_SIGNAL] = + g_signal_new ("object_moved", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); +} + +static void +document_interface_init (DocumentInterface *doc_interface) +{ + doc_interface->target = Inkscape::ActionContext(); +} + + +DocumentInterface * +document_interface_new (void) +{ + return (DocumentInterface*)g_object_new (TYPE_DOCUMENT_INTERFACE, NULL); +} + + + +/**************************************************************************** + MISC FUNCTIONS +****************************************************************************/ + +gboolean document_interface_delete_all(DocumentInterface *doc_interface, GError ** /*error*/) +{ + sp_edit_clear_all(doc_interface->target.getSelection()); + return TRUE; +} + +gboolean +document_interface_call_verb (DocumentInterface *doc_interface, gchar *verbid, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + if ( desk ) { + desktop_ensure_active (desk); + } + Inkscape::Verb *verb = Inkscape::Verb::getbyid( verbid ); + if ( verb ) { + SPAction *action = verb->get_action(doc_interface->target); + if ( action ) { + sp_action_perform( action, NULL ); + if (doc_interface->updates) { + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), verb->get_code(), verb->get_tip()); + } + return TRUE; + } + } + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_VERB, "Verb '%s' failed to execute or was not found.", verbid); + return FALSE; +} + + +/**************************************************************************** + CREATION FUNCTIONS +****************************************************************************/ + +gchar* +document_interface_rectangle (DocumentInterface *doc_interface, int x, int y, + int width, int height, GError **error) +{ + + + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:rect"); + sp_repr_set_int(newNode, "x", x); //could also use newNode->setAttribute() + sp_repr_set_int(newNode, "y", y); + sp_repr_set_int(newNode, "width", width); + sp_repr_set_int(newNode, "height", height); + return finish_create_shape (doc_interface, error, newNode, (gchar *)"create rectangle"); +} + +gchar* +document_interface_ellipse_center (DocumentInterface *doc_interface, int cx, int cy, + int rx, int ry, GError **error) +{ + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:path"); + newNode->setAttribute("sodipodi:type", "arc"); + sp_repr_set_int(newNode, "sodipodi:cx", cx); + sp_repr_set_int(newNode, "sodipodi:cy", cy); + sp_repr_set_int(newNode, "sodipodi:rx", rx); + sp_repr_set_int(newNode, "sodipodi:ry", ry); + return finish_create_shape (doc_interface, error, newNode, (gchar *)"create circle"); +} + +gchar* +document_interface_polygon (DocumentInterface *doc_interface, int cx, int cy, + int radius, int rotation, int sides, + GError **error) +{ + gdouble rot = ((rotation / 180.0) * M_PI) - M_PI_2; + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:path"); + newNode->setAttribute("inkscape:flatsided", "true"); + newNode->setAttribute("sodipodi:type", "star"); + sp_repr_set_int(newNode, "sodipodi:cx", cx); + sp_repr_set_int(newNode, "sodipodi:cy", cy); + sp_repr_set_int(newNode, "sodipodi:r1", radius); + sp_repr_set_int(newNode, "sodipodi:r2", radius); + sp_repr_set_int(newNode, "sodipodi:sides", sides); + sp_repr_set_int(newNode, "inkscape:randomized", 0); + sp_repr_set_svg_double(newNode, "sodipodi:arg1", rot); + sp_repr_set_svg_double(newNode, "sodipodi:arg2", rot); + sp_repr_set_svg_double(newNode, "inkscape:rounded", 0); + + return finish_create_shape (doc_interface, error, newNode, (gchar *)"create polygon"); +} + +gchar* +document_interface_star (DocumentInterface *doc_interface, int cx, int cy, + int r1, int r2, int sides, gdouble rounded, + gdouble arg1, gdouble arg2, GError **error) +{ + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:path"); + newNode->setAttribute("inkscape:flatsided", "false"); + newNode->setAttribute("sodipodi:type", "star"); + sp_repr_set_int(newNode, "sodipodi:cx", cx); + sp_repr_set_int(newNode, "sodipodi:cy", cy); + sp_repr_set_int(newNode, "sodipodi:r1", r1); + sp_repr_set_int(newNode, "sodipodi:r2", r2); + sp_repr_set_int(newNode, "sodipodi:sides", sides); + sp_repr_set_int(newNode, "inkscape:randomized", 0); + sp_repr_set_svg_double(newNode, "sodipodi:arg1", arg1); + sp_repr_set_svg_double(newNode, "sodipodi:arg2", arg2); + sp_repr_set_svg_double(newNode, "inkscape:rounded", rounded); + + return finish_create_shape (doc_interface, error, newNode, (gchar *)"create star"); +} + +gchar* +document_interface_ellipse (DocumentInterface *doc_interface, int x, int y, + int width, int height, GError **error) +{ + int rx = width/2; + int ry = height/2; + return document_interface_ellipse_center (doc_interface, x+rx, y+ry, rx, ry, error); +} + +gchar* +document_interface_line (DocumentInterface *doc_interface, int x, int y, + int x2, int y2, GError **error) +{ + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:path"); + std::stringstream out; + // Not sure why this works. + out << "m " << x << "," << y << " " << x2 - x << "," << y2 - y; + newNode->setAttribute("d", out.str()); + return finish_create_shape (doc_interface, error, newNode, (gchar *)"create line"); +} + +gchar* +document_interface_spiral (DocumentInterface *doc_interface, int cx, int cy, + int r, int revolutions, GError **error) +{ + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:path"); + newNode->setAttribute("sodipodi:type", "spiral"); + sp_repr_set_int(newNode, "sodipodi:cx", cx); + sp_repr_set_int(newNode, "sodipodi:cy", cy); + sp_repr_set_int(newNode, "sodipodi:radius", r); + sp_repr_set_int(newNode, "sodipodi:revolution", revolutions); + sp_repr_set_int(newNode, "sodipodi:t0", 0); + sp_repr_set_int(newNode, "sodipodi:argument", 0); + sp_repr_set_int(newNode, "sodipodi:expansion", 1); + gchar * retval = finish_create_shape (doc_interface, error, newNode, (gchar *)"create spiral"); + //Makes sure there is no fill for spirals by default. + gchar* newString = g_strconcat(newNode->attribute("style"), ";fill:none", NULL); + newNode->setAttribute("style", newString); + g_free(newString); + return retval; +} + +gchar* +document_interface_text (DocumentInterface *doc_interface, int x, int y, gchar *text, GError **error) +{ + + Inkscape::XML::Node *text_node = dbus_create_node(doc_interface->target.getDocument(), "svg:text"); + sp_repr_set_int(text_node, "x", x); + sp_repr_set_int(text_node, "y", y); + //just a workaround so i can get an spitem from the name + gchar *name = finish_create_shape (doc_interface, error, text_node, (gchar *)"create text"); + + SPItem* text_obj=(SPItem* )get_object_by_name(doc_interface->target.getDocument(), name, error); + sp_te_set_repr_text_multiline(text_obj, text); + + return name; +} + +gchar * +document_interface_image (DocumentInterface *doc_interface, int x, int y, gchar *filename, GError **error) +{ + gchar * uri = g_filename_to_uri (filename, FALSE, error); + if (!uri) + return FALSE; + + Inkscape::XML::Node *newNode = dbus_create_node(doc_interface->target.getDocument(), "svg:image"); + sp_repr_set_int(newNode, "x", x); + sp_repr_set_int(newNode, "y", y); + newNode->setAttribute("xlink:href", uri); + + doc_interface->target.getSelection()->layers()->currentLayer()->appendChildRepr(newNode); + doc_interface->target.getSelection()->layers()->currentLayer()->updateRepr(); + + if (doc_interface->updates) + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), 0, "Imported bitmap."); + + //g_free(uri); + return strdup(newNode->attribute("id")); +} + +gchar *document_interface_node(DocumentInterface *doc_interface, gchar *type, GError ** /*error*/) +{ + SPDocument * doc = doc_interface->target.getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *newNode = xml_doc->createElement(type); + + doc_interface->target.getSelection()->layers()->currentLayer()->appendChildRepr(newNode); + doc_interface->target.getSelection()->layers()->currentLayer()->updateRepr(); + + if (doc_interface->updates) { + Inkscape::DocumentUndo::done(doc, 0, (gchar *)"created empty node"); + } + + return strdup(newNode->attribute("id")); +} + +/**************************************************************************** + ENVIRONMENT FUNCTIONS +****************************************************************************/ +gdouble +document_interface_document_get_width (DocumentInterface *doc_interface) +{ + return doc_interface->target.getDocument()->getWidth().value("px"); +} + +gdouble +document_interface_document_get_height (DocumentInterface *doc_interface) +{ + return doc_interface->target.getDocument()->getHeight().value("px"); +} + +gchar *document_interface_document_get_css(DocumentInterface *doc_interface, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), NULL); + SPCSSAttr *current = desk->current; + Glib::ustring str; + sp_repr_css_write_string(current, str); + return (str.empty() ? NULL : g_strdup (str.c_str())); +} + +gboolean document_interface_document_merge_css(DocumentInterface *doc_interface, + gchar *stylestring, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + SPCSSAttr * style = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(style, stylestring); + sp_desktop_set_style(desk, style); + return TRUE; +} + +gboolean document_interface_document_set_css(DocumentInterface *doc_interface, + gchar *stylestring, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + SPCSSAttr * style = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string (style, stylestring); + //Memory leak? + desk->current = style; + return TRUE; +} + +gboolean +document_interface_document_resize_to_fit_selection (DocumentInterface *doc_interface, + GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_FIT_CANVAS_TO_SELECTION, error); +} + +gboolean +document_interface_document_set_display_area (DocumentInterface *doc_interface, + double x0, + double y0, + double x1, + double y1, + double border, + GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + desk->set_display_area (Geom::Rect( Geom::Point(x0,y0), Geom::Point(x1,y1)), border, false ); + return TRUE; +} + + +GArray * +document_interface_document_get_display_area (DocumentInterface *doc_interface) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + if (!desk) { + return NULL; + } + Geom::Rect const d = desk->get_display_area(); + + GArray * dArr = g_array_new (TRUE, TRUE, sizeof(double)); + + double x0 = d.min()[Geom::X]; + double y0 = d.min()[Geom::Y]; + double x1 = d.max()[Geom::X]; + double y1 = d.max()[Geom::Y]; + g_array_append_val (dArr, x0); // + g_array_append_val (dArr, y0); + g_array_append_val (dArr, x1); + g_array_append_val (dArr, y1); + return dArr; + +} + + +/**************************************************************************** + OBJECT FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_set_attribute (DocumentInterface *doc_interface, char *shape, + char *attribute, char *newval, GError **error) +{ + Inkscape::XML::Node *newNode = get_repr_by_name(doc_interface->target.getDocument(), shape, error); + + /* ALTERNATIVE (is this faster?) + Inkscape::XML::Node *newnode = sp_repr_lookup_name((doc->root)->repr, name); + */ + if (!dbus_check_string(newval, error, "New value string was empty.")) + return FALSE; + + if (!newNode) + return FALSE; + + newNode->setAttribute(attribute, newval, true); + return TRUE; +} + +gboolean +document_interface_set_int_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, + int newval, GError **error) +{ + Inkscape::XML::Node *newNode = get_repr_by_name (doc_interface->target.getDocument(), shape, error); + if (!newNode) + return FALSE; + + sp_repr_set_int (newNode, attribute, newval); + return TRUE; +} + + +gboolean +document_interface_set_double_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, + double newval, GError **error) +{ + Inkscape::XML::Node *newNode = get_repr_by_name (doc_interface->target.getDocument(), shape, error); + + if (!dbus_check_string (attribute, error, "New value string was empty.")) + return FALSE; + if (!newNode) + return FALSE; + + sp_repr_set_svg_double (newNode, attribute, newval); + return TRUE; +} + +gchar * +document_interface_get_attribute (DocumentInterface *doc_interface, char *shape, + char *attribute, GError **error) +{ + Inkscape::XML::Node *newNode = get_repr_by_name(doc_interface->target.getDocument(), shape, error); + + if (!dbus_check_string (attribute, error, "Attribute name empty.")) + return NULL; + if (!newNode) + return NULL; + + return g_strdup(newNode->attribute(attribute)); +} + +gboolean +document_interface_move (DocumentInterface *doc_interface, gchar *name, gdouble x, + gdouble y, GError **error) +{ + std::vector oldsel = selection_swap(doc_interface->target.getSelection(), name, error); + if (oldsel.empty()) + return FALSE; + doc_interface->target.getSelection()->move(x, 0 - y); + selection_restore(doc_interface->target.getSelection(), oldsel); + return TRUE; +} + +gboolean +document_interface_move_to (DocumentInterface *doc_interface, gchar *name, gdouble x, + gdouble y, GError **error) +{ + std::vector oldsel = selection_swap(doc_interface->target.getSelection(), name, error); + if (oldsel.empty()) + return FALSE; + Inkscape::Selection * sel = doc_interface->target.getSelection(); + doc_interface->target.getSelection()->move(x - selection_get_center_x(sel), + 0 - (y - selection_get_center_y(sel))); + selection_restore(doc_interface->target.getSelection(), oldsel); + return TRUE; +} + +gboolean +document_interface_object_to_path (DocumentInterface *doc_interface, + char *shape, GError **error) +{ + std::vector oldsel = selection_swap(doc_interface->target.getSelection(), shape, error); + if (oldsel.empty()) + return FALSE; + dbus_call_verb (doc_interface, SP_VERB_OBJECT_TO_CURVE, error); + selection_restore(doc_interface->target.getSelection(), oldsel); + return TRUE; +} + +gchar * +document_interface_get_path (DocumentInterface *doc_interface, char *pathname, GError **error) +{ + Inkscape::XML::Node *node = get_repr_by_name(doc_interface->target.getDocument(), pathname, error); + + if (!node) + return NULL; + + if (node->attribute("d") == NULL) + { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OBJECT, "Object is not a path."); + return NULL; + } + return strdup(node->attribute("d")); +} + +gboolean +document_interface_transform (DocumentInterface *doc_interface, gchar *shape, + gchar *transformstr, GError **error) +{ + //FIXME: This should merge transformations. + gchar trans[] = "transform"; + document_interface_set_attribute (doc_interface, shape, trans, transformstr, error); + return TRUE; +} + +gchar * +document_interface_get_css (DocumentInterface *doc_interface, gchar *shape, + GError **error) +{ + gchar style[] = "style"; + return document_interface_get_attribute (doc_interface, shape, style, error); +} + +gboolean +document_interface_modify_css (DocumentInterface *doc_interface, gchar *shape, + gchar *cssattrb, gchar *newval, GError **error) +{ + // Doesn't like non-variable strings for some reason. + gchar style[] = "style"; + Inkscape::XML::Node *node = get_repr_by_name(doc_interface->target.getDocument(), shape, error); + + if (!dbus_check_string (cssattrb, error, "Attribute string empty.")) + return FALSE; + if (!node) + return FALSE; + + SPCSSAttr * oldstyle = sp_repr_css_attr (node, style); + sp_repr_css_set_property(oldstyle, cssattrb, newval); + Glib::ustring str; + sp_repr_css_write_string (oldstyle, str); + node->setAttributeOrRemoveIfEmpty (style, str); + return TRUE; +} + +gboolean +document_interface_merge_css (DocumentInterface *doc_interface, gchar *shape, + gchar *stylestring, GError **error) +{ + gchar style[] = "style"; + + Inkscape::XML::Node *node = get_repr_by_name(doc_interface->target.getDocument(), shape, error); + + if (!dbus_check_string (stylestring, error, "Style string empty.")) + return FALSE; + if (!node) + return FALSE; + + SPCSSAttr * newstyle = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string (newstyle, stylestring); + + SPCSSAttr * oldstyle = sp_repr_css_attr (node, style); + + sp_repr_css_merge(oldstyle, newstyle); + Glib::ustring str; + sp_repr_css_write_string (oldstyle, str); + node->setAttributeOrRemoveIfEmpty (style, str); + + return TRUE; +} + +gboolean +document_interface_set_color (DocumentInterface *doc_interface, gchar *shape, + int r, int g, int b, gboolean fill, GError **error) +{ + gchar style[15]; + if (r<0 || r>255 || g<0 || g>255 || b<0 || b>255) + { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OTHER, "Given (%d,%d,%d). All values must be between 0-255 inclusive.", r, g, b); + return FALSE; + } + + if (fill) + snprintf(style, 15, "fill:#%.2x%.2x%.2x", r, g, b); + else + snprintf(style, 15, "stroke:#%.2x%.2x%.2x", r, g, b); + + if (strcmp(shape, "document") == 0) + return document_interface_document_merge_css (doc_interface, style, error); + + return document_interface_merge_css (doc_interface, shape, style, error); +} + +gboolean +document_interface_move_to_layer (DocumentInterface *doc_interface, gchar *shape, + gchar *layerstr, GError **error) +{ + std::vector oldsel = selection_swap(doc_interface->target.getSelection(), shape, error); + if (oldsel.empty()) + return FALSE; + + document_interface_selection_move_to_layer(doc_interface, layerstr, error); + selection_restore(doc_interface->target.getSelection(), oldsel); + return TRUE; +} + +GArray *document_interface_get_node_coordinates(DocumentInterface * /*doc_interface*/, gchar * /*shape*/) +{ + //FIXME: Needs lot's of work. +/* + Inkscape::XML::Node *shapenode = get_repr_by_name (doc_interface->target.getDocument(), shape, error); + if (shapenode == NULL || shapenode->attribute("d") == NULL) { + return FALSE; + } + char * path = strdup(shapenode->attribute("d")); + printf("PATH: %s\n", path); + + Geom::parse_svg_path (path); + return NULL; + */ + return NULL; +} + + +gboolean +document_interface_set_text (DocumentInterface *doc_interface, gchar *name, gchar *text, GError **error) +{ + + SPItem* text_obj=(SPItem* )get_object_by_name(doc_interface->target.getDocument(), name, error); + //TODO verify object type + if (!text_obj) + return FALSE; + sp_te_set_repr_text_multiline(text_obj, text); + return TRUE; + +} + + + +gboolean +document_interface_text_apply_style (DocumentInterface *doc_interface, gchar *name, + int start_pos, int end_pos, gchar *style, gchar *styleval, + GError **error) +{ + + SPItem* text_obj=(SPItem* )get_object_by_name(doc_interface->target.getDocument(), name, error); + + //void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css) + //TODO verify object type + if (!text_obj) + return FALSE; + Inkscape::Text::Layout const *layout = te_get_layout(text_obj); + Inkscape::Text::Layout::iterator start = layout->charIndexToIterator (start_pos); + Inkscape::Text::Layout::iterator end = layout->charIndexToIterator (end_pos); + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, style, styleval); + + sp_te_apply_style(text_obj, + start, + end, + css); + return TRUE; + +} + + +/**************************************************************************** + FILE I/O FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_save (DocumentInterface *doc_interface, GError **error) +{ + SPDocument * doc = doc_interface->target.getDocument(); + printf("1: %s\n2: %s\n3: %s\n", doc->getDocumentURI(), doc->getDocumentBase(), doc->getDocumentName()); + if (doc->getDocumentURI()) + return document_interface_save_as (doc_interface, doc->getDocumentURI(), error); + return FALSE; +} + +gboolean document_interface_load(DocumentInterface *doc_interface, + gchar *filename, GError ** /*error*/) +{ + if (!filename) { + return false; + } + + SPDesktop *desk = doc_interface->target.getDesktop(); + if (desk) { + desktop_ensure_active(desk); + } + + Glib::RefPtr file = Gio::File::create_for_path(filename); + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + app->create_window(file); + + if (doc_interface->updates) { + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), SP_VERB_FILE_OPEN, "Opened File"); + } + return TRUE; +} + +gchar * +document_interface_import (DocumentInterface *doc_interface, + gchar *filename, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + if (desk) { + desktop_ensure_active(desk); + } + const Glib::ustring file(filename); + SPDocument * doc = doc_interface->target.getDocument(); + + SPObject *new_obj = NULL; + new_obj = file_import(doc, file, NULL); + return strdup(new_obj->getRepr()->attribute("id")); +} + +gboolean +document_interface_save_as (DocumentInterface *doc_interface, + const gchar *filename, GError **error) +{ + // FIXME: Isn't there a verb we can use for this instead? + SPDocument * doc = doc_interface->target.getDocument(); + + if (!doc || strlen(filename)<1) { //Safety check + return false; + } + + try { + Inkscape::Extension::save(NULL, doc, filename, + false, false, true, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + } catch (...) { + // FIXME: catch ... is not usually a great idea, why is it needed here? + return false; + } + + return true; +} + +gboolean document_interface_mark_as_unmodified(DocumentInterface *doc_interface, GError ** /*error*/) +{ + SPDocument * doc = doc_interface->target.getDocument(); + if (doc) { + doc->setModifiedSinceSave(FALSE); + } + return TRUE; +} + +/* +gboolean +document_interface_print_to_file (DocumentInterface *doc_interface, GError **error) +{ + SPDocument * doc = doc_interface->target.getDocument(); + sp_print_document_to_file (doc, g_strdup("/home/soren/test.pdf")); + + return TRUE; +} +*/ +/**************************************************************************** + PROGRAM CONTROL FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_close (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_FILE_CLOSE_VIEW, error); +} + +gboolean +document_interface_exit (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_FILE_QUIT, error); +} + +gboolean +document_interface_undo (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_UNDO, error); +} + +gboolean +document_interface_redo (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_REDO, error); +} + + + +/**************************************************************************** + UPDATE FUNCTIONS + FIXME: This would work better by adding a flag to SPDesktop to prevent + updating but that would be very intrusive so for now there is a workaround. + Need to make sure it plays well with verbs because they are used so much. +****************************************************************************/ + +void document_interface_pause_updates(DocumentInterface *doc_interface, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_if_fail(ensure_desktop_valid(desk, error)); + doc_interface->updates = FALSE; + desk->canvas->_drawing_disabled = 1; +} + +void document_interface_resume_updates(DocumentInterface *doc_interface, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_if_fail(ensure_desktop_valid(desk, error)); + doc_interface->updates = TRUE; + desk->canvas->_drawing_disabled = 0; + //FIXME: use better verb than rect. + Inkscape::DocumentUndo::done(doc_interface->target.getDocument(), SP_VERB_CONTEXT_RECT, "Multiple actions"); +} + +void document_interface_update(DocumentInterface *doc_interface, GError ** error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_if_fail(ensure_desktop_valid(desk, error)); + SPDocument *doc = doc_interface->target.getDocument(); + doc->getRoot()->uflags = TRUE; + doc->getRoot()->mflags = TRUE; + desk->enableInteraction(); + doc->ensureUpToDate(); + desk->disableInteraction(); + doc->getRoot()->uflags = FALSE; + doc->getRoot()->mflags = FALSE; + //Inkscape::DocumentUndo::done(doc, SP_VERB_CONTEXT_RECT, "Multiple actions"); +} + +/**************************************************************************** + SELECTION FUNCTIONS FIXME: use call_verb where appropriate (once update system is tested.) +****************************************************************************/ + +gboolean document_interface_selection_get(DocumentInterface *doc_interface, char ***out, GError ** /*error*/) +{ + Inkscape::Selection * sel = doc_interface->target.getSelection(); + auto oldsel = sel->objects(); + + int size = oldsel.size(); + + *out = g_new0 (char *, size + 1); + + int i = 0; + for (auto iter = oldsel.begin(); iter != oldsel.end(); ++iter) { + (*out)[i] = g_strdup((*iter)->getId()); + i++; + } + (*out)[i] = NULL; + + return TRUE; +} + +gboolean +document_interface_selection_add (DocumentInterface *doc_interface, char *name, GError **error) +{ + SPObject * obj = get_object_by_name(doc_interface->target.getDocument(), name, error); + if (!obj) + return FALSE; + + Inkscape::Selection *selection = doc_interface->target.getSelection(); + + selection->add(obj); + return TRUE; +} + +gboolean +document_interface_selection_add_list (DocumentInterface *doc_interface, + char **names, GError **error) +{ + int i; + for (i=0;names[i] != NULL;i++) { + document_interface_selection_add(doc_interface, names[i], error); + } + return TRUE; +} + +gboolean document_interface_selection_set(DocumentInterface *doc_interface, char *name, GError ** /*error*/) +{ + SPDocument * doc = doc_interface->target.getDocument(); + Inkscape::Selection *selection = doc_interface->target.getSelection(); + selection->set(doc->getObjectById(name)); + return TRUE; +} + +gboolean +document_interface_selection_set_list (DocumentInterface *doc_interface, + gchar **names, GError **error) +{ + doc_interface->target.getSelection()->clear(); + int i; + for (i=0;names[i] != NULL;i++) { + document_interface_selection_add(doc_interface, names[i], error); + } + return TRUE; +} + +gboolean document_interface_selection_rotate(DocumentInterface *doc_interface, int angle, GError ** /*error*/) +{ + Inkscape::Selection *selection = doc_interface->target.getSelection(); + selection->rotate(angle); + return TRUE; +} + +gboolean +document_interface_selection_delete (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_DELETE, error); +} + +gboolean document_interface_selection_clear(DocumentInterface *doc_interface, GError ** /*error*/) +{ + doc_interface->target.getSelection()->clear(); + return TRUE; +} + +gboolean +document_interface_select_all (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_SELECT_ALL, error); +} + +gboolean +document_interface_select_all_in_all_layers(DocumentInterface *doc_interface, + GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS, error); +} + +gboolean document_interface_selection_box(DocumentInterface * /*doc_interface*/, int /*x*/, int /*y*/, + int /*x2*/, int /*y2*/, gboolean /*replace*/, + GError ** /*error*/) +{ + //FIXME: implement. + return FALSE; +} + +gboolean +document_interface_selection_invert (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_EDIT_INVERT, error); +} + +gboolean +document_interface_selection_group (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_GROUP, error); +} +gboolean +document_interface_selection_ungroup (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_UNGROUP, error); +} + +gboolean +document_interface_selection_cut (DocumentInterface *doc_interface, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + return dbus_call_verb (doc_interface, SP_VERB_EDIT_CUT, error); +} + +gboolean +document_interface_selection_copy (DocumentInterface *doc_interface, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + return dbus_call_verb (doc_interface, SP_VERB_EDIT_COPY, error); +} + +gboolean +document_interface_selection_paste (DocumentInterface *doc_interface, GError **error) +{ + SPDesktop *desk = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(desk, error), FALSE); + return dbus_call_verb (doc_interface, SP_VERB_EDIT_PASTE, error); +} + +gboolean document_interface_selection_scale(DocumentInterface *doc_interface, gdouble grow, GError ** /*error*/) +{ + Inkscape::Selection *selection = doc_interface->target.getSelection(); + if (!selection) + { + return FALSE; + } + selection->scale(grow); + return TRUE; +} + +gboolean document_interface_selection_move(DocumentInterface *doc_interface, gdouble x, gdouble y, GError ** /*error*/) +{ + doc_interface->target.getSelection()->move(x, 0 - y); //switching coordinate systems. + return TRUE; +} + +gboolean document_interface_selection_move_to(DocumentInterface *doc_interface, gdouble x, gdouble y, GError ** /*error*/) +{ + Inkscape::Selection * sel = doc_interface->target.getSelection(); + + Geom::OptRect sel_bbox = sel->visualBounds(); + if (sel_bbox) { + Geom::Point m( x - selection_get_center_x(sel) , 0 - (y - selection_get_center_y(sel)) ); + sel->moveRelative(m, true); + } + return TRUE; +} + +//FIXME: does not paste in new layer. +// This needs to use lower level cut_impl and paste_impl (messy) +// See the built-in sp_selection_to_next_layer and duplicate. +gboolean +document_interface_selection_move_to_layer (DocumentInterface *doc_interface, + gchar *layerstr, GError **error) +{ + SPDesktop *dt = doc_interface->target.getDesktop(); + g_return_val_if_fail(ensure_desktop_valid(dt, error), FALSE); + + Inkscape::Selection *selection = doc_interface->target.getSelection(); + + // check if something is selected + if (selection->isEmpty()) + return FALSE; + + SPObject *next = get_object_by_name(doc_interface->target.getDocument(), layerstr, error); + + if (!next) + return FALSE; + + if (strcmp("layer", (next->getRepr())->attribute("inkscape:groupmode")) == 0) { + + dt->selection->cut(); + + doc_interface->target.getSelection()->layers()->setCurrentLayer(next); + + sp_selection_paste(dt, TRUE); + } + return TRUE; +} + +GArray * +document_interface_selection_get_center (DocumentInterface *doc_interface) +{ + Inkscape::Selection * sel = doc_interface->target.getSelection(); + + if (sel) + { + gdouble x = selection_get_center_x(sel); + gdouble y = selection_get_center_y(sel); + GArray * intArr = g_array_new (TRUE, TRUE, sizeof(double)); + + g_array_append_val (intArr, x); + g_array_append_val (intArr, y); + return intArr; + } + + return NULL; +} + +gboolean +document_interface_selection_to_path (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_OBJECT_TO_CURVE, error); +} + + +gboolean +document_interface_selection_combine (DocumentInterface *doc_interface, gchar *cmd, char ***newpaths, + GError **error) +{ + if (strcmp(cmd, "union") == 0) + dbus_call_verb (doc_interface, SP_VERB_SELECTION_UNION, error); + else if (strcmp(cmd, "intersection") == 0) + dbus_call_verb (doc_interface, SP_VERB_SELECTION_INTERSECT, error); + else if (strcmp(cmd, "difference") == 0) + dbus_call_verb (doc_interface, SP_VERB_SELECTION_DIFF, error); + else if (strcmp(cmd, "exclusion") == 0) + dbus_call_verb (doc_interface, SP_VERB_SELECTION_SYMDIFF, error); + else if (strcmp(cmd, "division") == 0) + dbus_call_verb (doc_interface, SP_VERB_SELECTION_CUT, error); + else { + g_set_error(error, INKSCAPE_ERROR, INKSCAPE_ERROR_OTHER, "Operation command not recognised"); + return FALSE; + } + + return document_interface_selection_get (doc_interface, newpaths, error); +} + +gboolean +document_interface_selection_change_level (DocumentInterface *doc_interface, gchar *cmd, + GError **error) +{ + if (strcmp(cmd, "raise") == 0) + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_RAISE, error); + if (strcmp(cmd, "lower") == 0) + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_LOWER, error); + if ((strcmp(cmd, "to_top") == 0) || (strcmp(cmd, "to_front") == 0)) + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_TO_FRONT, error); + if ((strcmp(cmd, "to_bottom") == 0) || (strcmp(cmd, "to_back") == 0)) + return dbus_call_verb (doc_interface, SP_VERB_SELECTION_TO_BACK, error); + return TRUE; +} + +/**************************************************************************** + LAYER FUNCTIONS +****************************************************************************/ + +gchar *document_interface_layer_new(DocumentInterface *doc_interface, GError ** /*error*/) +{ + Inkscape::LayerModel * layers = doc_interface->target.getSelection()->layers(); + SPObject *new_layer = Inkscape::create_layer(layers->currentRoot(), layers->currentLayer(), Inkscape::LPOS_BELOW); + layers->setCurrentLayer(new_layer); + return g_strdup(get_name_from_object(new_layer)); +} + +gboolean +document_interface_layer_set (DocumentInterface *doc_interface, + gchar *layerstr, GError **error) +{ + SPObject * obj = get_object_by_name (doc_interface->target.getDocument(), layerstr, error); + + if (!obj) + return FALSE; + + doc_interface->target.getSelection()->layers()->setCurrentLayer (obj); + return TRUE; +} + +gchar **document_interface_layer_get_all(DocumentInterface * /*doc_interface*/) +{ + //FIXME: implement. + return NULL; +} + +gboolean +document_interface_layer_change_level (DocumentInterface *doc_interface, + gchar *cmd, GError **error) +{ + if (strcmp(cmd, "raise") == 0) + return dbus_call_verb (doc_interface, SP_VERB_LAYER_RAISE, error); + if (strcmp(cmd, "lower") == 0) + return dbus_call_verb (doc_interface, SP_VERB_LAYER_LOWER, error); + if ((strcmp(cmd, "to_top") == 0) || (strcmp(cmd, "to_front") == 0)) + return dbus_call_verb (doc_interface, SP_VERB_LAYER_TO_TOP, error); + if ((strcmp(cmd, "to_bottom") == 0) || (strcmp(cmd, "to_back") == 0)) + return dbus_call_verb (doc_interface, SP_VERB_LAYER_TO_BOTTOM, error); + return TRUE; +} + +gboolean +document_interface_layer_next (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_LAYER_NEXT, error); +} + +gboolean +document_interface_layer_previous (DocumentInterface *doc_interface, GError **error) +{ + return dbus_call_verb (doc_interface, SP_VERB_LAYER_PREV, error); +} + + +//////////////signals + + +DocumentInterface *fugly; +gboolean dbus_send_ping (SPDesktop* desk, SPItem *item) +{ + if (!item) return TRUE; + g_signal_emit (desk->dbus_document_interface, signals[OBJECT_MOVED_SIGNAL], 0, item->getId()); + return TRUE; +} + +//////////tree + + +gboolean +document_interface_get_children (DocumentInterface *doc_interface, char *name, char ***out, GError **error) +{ + SPItem* parent=(SPItem* )get_object_by_name(doc_interface->target.getDocument(), name, error); + std::vector children = parent->childList(false); + + int size = children.size(); + + *out = g_new0 (char *, size + 1); + + int i = 0; + for (std::vector::iterator iter = children.begin(), e = children.end(); iter != e; ++iter) { + (*out)[i] = g_strdup((*iter)->getId()); + i++; + } + (*out)[i] = NULL; + + return TRUE; +} + + +gchar* +document_interface_get_parent (DocumentInterface *doc_interface, char *name, GError **error) +{ + SPItem*node=(SPItem* )get_object_by_name(doc_interface->target.getDocument(), name, error); + + SPObject* parent=node->parent; + + return g_strdup(parent->getRepr()->attribute("id")); + +} + +#if 0 +//just pseudo code +gboolean +document_interface_get_xpath (DocumentInterface *doc_interface, char *xpath_expression, char ***out, GError **error){ + SPDocument * doc = doc_interface->target.getDocument(); + Inkscape::XML::Document *repr = doc->getReprDoc(); + + xmlXPathObjectPtr xpathObj; + xmlXPathContextPtr xpathCtx; + xpathCtx = xmlXPathNewContext(repr);//XmlDocPtr + xpathObj = xmlXPathEvalExpression(xmlCharStrdup(xpath_expression), xpathCtx); + + //xpathresult result = xpatheval(repr, xpath_selection); + //convert result to a string array we can return via dbus + return TRUE; +} +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/dbus/document-interface.h b/src/extension/dbus/document-interface.h new file mode 100644 index 0000000..ed31513 --- /dev/null +++ b/src/extension/dbus/document-interface.h @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is where the implementation of the DBus based document API lives. + * All the methods in here (except in the helper section) are + * designed to be called remotly via DBus. application-interface.cpp + * has the methods used to connect to the bus and get a document instance. + * + * Documentation for these methods is in document-interface.xml + * which is the "gold standard" as to how the interface should work. + * + * Authors: + * Soren Berg + * + * Copyright (C) 2009 Soren Berg + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ +#define INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ + +#include +#include +#include +#include + +// this is required so that giomm headers won't barf +#undef DBUS_MESSAGE_TYPE_INVALID +#undef DBUS_MESSAGE_TYPE_METHOD_CALL +#undef DBUS_MESSAGE_TYPE_METHOD_RETURN +#undef DBUS_MESSAGE_TYPE_ERROR +#undef DBUS_MESSAGE_TYPE_SIGNAL + +#include "helper/action-context.h" + +class SPDesktop; +class SPItem; + +#define TYPE_DOCUMENT_INTERFACE (document_interface_get_type ()) +#define DOCUMENT_INTERFACE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TYPE_DOCUMENT_INTERFACE, DocumentInterface)) +#define DOCUMENT_INTERFACE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TYPE_DOCUMENT_INTERFACE, DocumentInterfaceClass)) +#define IS_DOCUMENT_INTERFACE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TYPE_DOCUMENT_INTERFACE)) +#define IS_DOCUMENT_INTERFACE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TYPE_DOCUMENT_INTERFACE)) +#define DOCUMENT_INTERFACE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TYPE_DOCUMENT_INTERFACE, DocumentInterfaceClass)) + +G_BEGIN_DECLS + +typedef struct _DocumentInterface DocumentInterface; +typedef struct _DocumentInterfaceClass DocumentInterfaceClass; + +struct _DocumentInterface { + GObject parent; + Inkscape::ActionContext target; ///< stores information about which document, selection, desktop etc this interface is linked to + gboolean updates; +}; + +struct _DocumentInterfaceClass { + GObjectClass parent; +}; + + + +struct DBUSPoint { + int x; + int y; +}; +/**************************************************************************** + MISC FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_delete_all (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_call_verb (DocumentInterface *doc_interface, + gchar *verbid, GError **error); + +/**************************************************************************** + CREATION FUNCTIONS +****************************************************************************/ + +gchar* +document_interface_rectangle (DocumentInterface *doc_interface, int x, int y, + int width, int height, GError **error); + +gchar* +document_interface_ellipse (DocumentInterface *doc_interface, int x, int y, + int width, int height, GError **error); + +gchar* +document_interface_polygon (DocumentInterface *doc_interface, int cx, int cy, + int radius, int rotation, int sides, + GError **error); + +gchar* +document_interface_star (DocumentInterface *doc_interface, int cx, int cy, + int r1, int r2, int sides, gdouble rounded, + gdouble arg1, gdouble arg2, GError **error); + +gchar* +document_interface_spiral (DocumentInterface *doc_interface, int cx, int cy, + int r, int revolutions, GError **error); + +gchar* +document_interface_line (DocumentInterface *doc_interface, int x, int y, + int x2, int y2, GError **error); + +gchar* +document_interface_text (DocumentInterface *doc_interface, int x, int y, + gchar *text, GError **error); +gboolean +document_interface_set_text (DocumentInterface *doc_interface, gchar *name, + gchar *text, GError **error); +gboolean +document_interface_text_apply_style (DocumentInterface *doc_interface, gchar *name, + int start_pos, int end_pos, gchar *style, gchar *styleval, + GError **error); + +gchar * +document_interface_image (DocumentInterface *doc_interface, int x, int y, + gchar *filename, GError **error); + +gchar* +document_interface_node (DocumentInterface *doc_interface, gchar *svgtype, + GError **error); + + +/**************************************************************************** + ENVIRONMENT FUNCTIONS +****************************************************************************/ +gdouble +document_interface_document_get_width (DocumentInterface *doc_interface); + +gdouble +document_interface_document_get_height (DocumentInterface *doc_interface); + +gchar * +document_interface_document_get_css (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_document_merge_css (DocumentInterface *doc_interface, + gchar *stylestring, GError **error); + +gboolean +document_interface_document_set_css (DocumentInterface *doc_interface, + gchar *stylestring, GError **error); + +gboolean +document_interface_document_resize_to_fit_selection (DocumentInterface *doc_interface, + GError **error); +gboolean +document_interface_document_set_display_area (DocumentInterface *doc_interface, + double x0, + double y0, + double x1, + double y1, + double border, + GError **error); +GArray * +document_interface_document_get_display_area (DocumentInterface *doc_interface); + +/**************************************************************************** + OBJECT FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_set_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, + char *newval, GError **error); + +gboolean +document_interface_set_int_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, + int newval, GError **error); + +gboolean +document_interface_set_double_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, + double newval, GError **error); + +gchar * +document_interface_get_attribute (DocumentInterface *doc_interface, + char *shape, char *attribute, GError **error); + +gboolean +document_interface_move (DocumentInterface *doc_interface, gchar *name, + gdouble x, gdouble y, GError **error); + +gboolean +document_interface_move_to (DocumentInterface *doc_interface, gchar *name, + gdouble x, gdouble y, GError **error); + +gboolean +document_interface_object_to_path (DocumentInterface *doc_interface, + char *shape, GError **error); + +gchar * +document_interface_get_path (DocumentInterface *doc_interface, + char *pathname, GError **error); + +gboolean +document_interface_transform (DocumentInterface *doc_interface, gchar *shape, + gchar *transformstr, GError **error); + +gchar * +document_interface_get_css (DocumentInterface *doc_interface, gchar *shape, + GError **error); + +gboolean +document_interface_modify_css (DocumentInterface *doc_interface, gchar *shape, + gchar *cssattrb, gchar *newval, GError **error); + +gboolean +document_interface_merge_css (DocumentInterface *doc_interface, gchar *shape, + gchar *stylestring, GError **error); + +gboolean +document_interface_set_color (DocumentInterface *doc_interface, gchar *shape, + int r, int g, int b, gboolean fill, GError **error); + +gboolean +document_interface_move_to_layer (DocumentInterface *doc_interface, gchar *shape, + gchar *layerstr, GError **error); + + +GArray * +document_interface_get_node_coordinates (DocumentInterface *doc_interface, gchar *shape); + +/**************************************************************************** + FILE I/O FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_save (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_load (DocumentInterface *doc_interface, + gchar *filename, GError **error); + +gboolean +document_interface_save_as (DocumentInterface *doc_interface, + const gchar *filename, GError **error); + +gboolean +document_interface_mark_as_unmodified (DocumentInterface *doc_interface, GError **error); +/* +gboolean +document_interface_print_to_file (DocumentInterface *doc_interface, GError **error); +*/ + +/**************************************************************************** + PROGRAM CONTROL FUNCTIONS +****************************************************************************/ + +gboolean +document_interface_close (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_exit (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_undo (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_redo (DocumentInterface *doc_interface, GError **error); + + +/**************************************************************************** + UPDATE FUNCTIONS +****************************************************************************/ +void +document_interface_pause_updates (DocumentInterface *doc_interface, GError **error); + +void +document_interface_resume_updates (DocumentInterface *doc_interface, GError **error); + +void +document_interface_update (DocumentInterface *doc_interface, GError **error); + +/**************************************************************************** + SELECTION FUNCTIONS +****************************************************************************/ +gboolean +document_interface_selection_get (DocumentInterface *doc_interface, char ***out, GError **error); + +gboolean +document_interface_selection_add (DocumentInterface *doc_interface, + char *name, GError **error); + +gboolean +document_interface_selection_add_list (DocumentInterface *doc_interface, + char **names, GError **error); + +gboolean +document_interface_selection_set (DocumentInterface *doc_interface, + char *name, GError **error); + +gboolean +document_interface_selection_set_list (DocumentInterface *doc_interface, + gchar **names, GError **error); + +gboolean +document_interface_selection_rotate (DocumentInterface *doc_interface, + int angle, GError **error); + +gboolean +document_interface_selection_delete(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_clear(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_select_all(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_select_all_in_all_layers(DocumentInterface *doc_interface, + GError **error); + +gboolean +document_interface_selection_box (DocumentInterface *doc_interface, int x, int y, + int x2, int y2, gboolean replace, + GError **error); + +gboolean +document_interface_selection_invert (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_group(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_ungroup(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_cut(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_copy(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_paste(DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_scale (DocumentInterface *doc_interface, + gdouble grow, GError **error); + +gboolean +document_interface_selection_move (DocumentInterface *doc_interface, gdouble x, + gdouble y, GError **error); + +gboolean +document_interface_selection_move_to (DocumentInterface *doc_interface, gdouble x, + gdouble y, GError **error); + +gboolean +document_interface_selection_move_to_layer (DocumentInterface *doc_interface, + gchar *layerstr, GError **error); + +GArray * +document_interface_selection_get_center (DocumentInterface *doc_interface); + +gboolean +document_interface_selection_to_path (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_selection_combine (DocumentInterface *doc_interface, gchar *cmd, char ***newpaths, + GError **error); + +gboolean +document_interface_selection_change_level (DocumentInterface *doc_interface, gchar *cmd, + GError **error); + +/**************************************************************************** + LAYER FUNCTIONS +****************************************************************************/ + +gchar * +document_interface_layer_new (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_layer_set (DocumentInterface *doc_interface, + gchar *layerstr, GError **error); + +gchar ** +document_interface_layer_get_all (DocumentInterface *doc_interface); + +gboolean +document_interface_layer_change_level (DocumentInterface *doc_interface, + gchar *cmd, GError **error); + +gboolean +document_interface_layer_next (DocumentInterface *doc_interface, GError **error); + +gboolean +document_interface_layer_previous (DocumentInterface *doc_interface, GError **error); + + + + + + + + +DocumentInterface *document_interface_new (void); +GType document_interface_get_type (void); + +extern DocumentInterface *fugly; +gboolean dbus_send_ping (SPDesktop* desk, SPItem *item); + +gboolean +document_interface_get_children (DocumentInterface *doc_interface, char *name, char ***out, GError **error); + +gchar* +document_interface_get_parent (DocumentInterface *doc_interface, char *name, GError **error); + +gchar* +document_interface_import (DocumentInterface *doc_interface, + gchar *filename, GError **error); + +G_END_DECLS + +#endif // INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ diff --git a/src/extension/dbus/document-interface.xml b/src/extension/dbus/document-interface.xml new file mode 100644 index 0000000..4524cba --- /dev/null +++ b/src/extension/dbus/document-interface.xml @@ -0,0 +1,1530 @@ + + + + + + +c + + + + + + + + + + + The string id of a verb. For example: "EditSelectAll". + + + + + This method allows you to call any Inkscape verb using it's associated string. Every button and menu item has an associated verb, so this allows access to some extra functionality if one is willing to do the prerequisite research. The list of verbs can be found at: + + + + + + + + + + X coordinate for the top left corner of the rectangle. + + + + + Y coordinate for the top left corner of the rectangle. + + + + + Width of the rectangle. + + + + + Height of the rectangle. + + + + + + The name of the new rectangle. + + + + + This method creates a rectangle in the current layer using the current document style. + It is recommended that you save the return value if you will want to modify this particular shape later. + Additional variables include: + cx and cy: set these anywhere from zero to half the width or height respectively of the rectangle to give it rounded corners. + + Coordinate System + + + + + + + X coordinate for the top left corner of the ellipse. + + + + + Y coordinate for the top left corner of the ellipse. + + + + + Width of the ellipse. + + + + + Height of the ellipse. + + + + + + The name of the new ellipse. + + + + + This method creates a ellipse in the current layer using the current document style. + It is recommended that you save the return value if you will want to modify this particular shape later. + Additional variables include: + "sodipodi:start" and "sodipodi:end": set these between 0 and Pi to create wedges or Pacman like shapes. + + Coordinate System + + + + + + + X coordinate for the center of the polygon. + + + + + Y coordinate for the center of the polygon. + + + + + Radius from the center to one of the points. + + + + + Angle in degrees to rotate. 0 will have the first point pointing straight up. + + + + + Number of sides of the polygon. + + + + + + The name of the new polygon. + + + + + This method creates a polygon in the current layer using the current document style. + It is recommended that you save the return value if you will want to modify this particular shape later. + Note: this is actually a star with "sodipodi:flatsided" set to true, which causes it to ignore the arg2 and r2 values. + + Coordinate System + + + + + + + X coordinate for the center of the star. + + + + + Y coordinate for the center of the star. + + + + + distance from the center for the first point. + + + + + distance from the center for the second point. + + + + + Angle in radians for the first point. 0 is 90 degrees to the right of straight up. + + + + + Angle in radians for the second point. 0 is 90 degrees to the right of straight up. + + + + + Number of times to repeat the points around the star. + + + + + How rounded to make the star. 0 to 1 recommended for moderate to medium curves. 10 for extreme curves. + + + + + + The name of the new star. + + + + + This method creates a star in the current layer using the current document style. + It is recommended that you save the return value if you will want to modify this particular shape later. + Stars are quite complicated. Here is how they are represented: There are two points, represented by sodipodi:arg1 and sodipodi:arg2 for angle in radians and sodipodi:r1 and sodipodi:r2 for respective radius from the center point. The further one is a point of the star, the shorter one one of the valleys. This point and valley are repeated according to sodipodi:sides. sodipodi:rounded controls their control handles. + + Coordinate System + + + + + + + X coordinate for the center of the spiral. + + + + + Y coordinate for the center of the spiral. + + + + + Radius of the spiral. + + + + + Number of revolutions. + + + + + + The name of the new spiral. + + + + + This method creates a spiral in the current layer using the current document style. However, fill is automatically set to "none". Stroke is unmodified. + It is recommended that you save the return value if you will want to modify this particular shape later. + Additional variables include: + "sodipodi:expansion": at 1 the spiral gets bigger at a constant rate. Less than one and the loops get tighter and tighter as it goes. More than one and they get looser and looser. This affects the number of revolutions so that it might not actually match the "sodipodi:revolutions" argument. + "sodipodi:t0": at 0 the entire spiral is drawn, at 0.5 it is only drawn %50 of the way (starting from the outside) etc. + "sodipodi:argument": Rotates the spiral. In radians. + + Coordinate System + + + + + + + X coordinate for the first point. + + + + + Y coordinate for the first point. + + + + + X coordinate for the second point. + + + + + Y coordinate for the second point. + + + + + + The name of the new line. + + + + + This method creates a line in the current layer using the current document style. It's a path, so the only attribute it will pay any attention to is "transform". + + Coordinate System + + + + + + + The x coordinate to put the text at. + + + + + The y coordinate to put the text at. + + + + + The text you want. + + + + + + The name of the new text. + + + + + This method creates some text in the current layer. + + + + + + + + The x coordinate to put the image at. + + + + + The y coordinate to put the image at. + + + + + The full path of the image you want. + + + + + + The name of the new image. + + + + + This method imports a non-vector image (such as a jpeg, png, etc.) and places it at the given coordinates. The resulting shape has no style or path but can be treated like a rectangle. With and height can be set explicitly (will deform image) or transform strings or selection_scale() can scale it relatively. + + + + + + + + The path to a valid svg file. + + + + + + The name of the new image. + + + + + Imports the file at pathname. Similar to the image + method. + + + + + + + + + The type of node, probably "svg:path" + + + + + + The name of the new node. + + + + + Make any kind of node you want. Mostly for making paths. (May need to allow updateRepr to be called for it to show up.) + + + + + + + + + + + Document width. + + + + + Retrieve the width of the current document. anything outside the boundary will not be printed or exported but will be saved. + + + + + + + + + Document height. + + + + + Retrieve the height of the current document. anything outside the boundary will not be printed or exported but will be saved. + + + + + + + + + CSS attribute string for the document. + + + + + Get the current style for the document. All new shapes will use this style if it exists. + + Style Strings + + + + + + + A new CSS attribute string for the document. + + + + + Set the current style for the document. All new shapes will use this style if it exists. + + Style Strings + + + + + + + A new CSS attribute string for the document. + + + + + Merge this string with the current style for the document. All new shapes will use this style if it exists. + + Style Strings, merge_css() + + + + + + + Resize the document to contain all of the currently selected objects. + This ensures that the image is not clipped when printing or exporting. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Set display area. + + + + + + + + Get display area. + + + + + + area + + + + + + + + + + + The id of an object. + + + + + The name of the attribute. + + + + + The new value of the attribute. This will overwrite anything already set. To merge styles, see merge_css(). + + + + + Set any attribute, the available attributes depend on what kind of shape the object node represents. See shape creation functions for more details. + + + + + + + + + The id of an object. + + + + + + The text you want. + + + + + set text of text object. + + + + + + + + + The id of an object. + + + + + + start text pos. + + + + + end text pos. + + + + + + css attribute. + + + + + + css attribute value. + + + + + + + set styling of partial text object. + + + + + + + + + The id of an object. + + + + + The name of the attribute. + + + + + The new value of the attribute. This will overwrite anything already set. + + + + + Set any attribute, the available attributes depend on what kind of shape the object node represents. See shape creation functions for more details. + This is a convenience function for set_attribute(). + + + + + + + + The id of an object. + + + + + The name of the attribute. + + + + + The new value of the attribute. This will overwrite anything already set. + + + + + Set any attribute, the available attributes depend on what kind of shape the node represents. See shape creation functions for more details. + This is a convenience function for set_attribute(). + + + + + + + + The id of an object. + + + + + The name of the attribute. + + + + + + The current value of the attribute. String is a copy and must be freed. + + + + + Get the value of any attribute. Not all objects will have every attribute their type supports, some are optional. See shape creation functions for more details. + + + + + + + + The id of an object. + + + + + Distance to move along the x axis. + + + + + Distance to move along the y axis. + + + + + This will move a shape (or any object) relative to it's current location. + This may be accomplished with transformation attributes or by changing x and y attributes depending on the state of the object. + + Coordinate System + + + + + + + The id of an object. + + + + + the x coordinate of the desired location. + + + + + the y coordinate of the desired location. + + + + + This will move a shape (or any object) to an absolute location. The point moved is the center of the bounding box, which is usually similar to the center of the shape. + Note that creating a rectangle or ellipse at 100,100 and calling move_to to move it to 100,100 will not produce the same results. + This may be accomplished with transformation attributes or by changing x and y attributes depending on the state of the object. + + Coordinate System + + + + + + + The id of an object. + + + + + Turns an object into a path. Most objects contain paths (except rectangles) but are not paths themselves. + This will remove every attribute except d (the path attribute) style and id. id will not change. The appearance will be the same as well, it essentially encodes all information about the shape into the path. + After doing this you will no longer be able to modify the shape using shape specific attributes (cx, radius etc.) except transform + Required for certain functions that work on paths (not yet present in this API.) + + Paths + + + + + + + The id of an object. + + + + + + The path of the object. NULL if the object has no path. + + + + + Get the path value of an object. Equivalent to calling get_attribte() with argument "d". Will not turn object into a path if it is not already. + + Paths + + + + + + + The id of any node or object. + + + + + A string that represents a transformation. + + + + + Takes a transformation string ("matrix(0.96629885,0.25742286,-0.25742286,0.96629885,0,0)" or "rotate(45)") and applies it to any shape or path. + Will merge with existing transformations. + + + + + + + + Any object with a style attribute. + + + + + + A CSS Style string + + + + + Retrieve the style of a object. Equivalent to calling get_attribute() for "style". + + Style Strings + + + + + + + Any object with a style attribute. + + + + + An attribute such as "fill" or "stroke-width". + + + + + The new value. + + + + + Set a particular attribute of a style string. Overwrites just that part of the style. + + Style Strings + + + + + + + Any object with a style attribute. + + + + + A full or partial CSS Style string. + + + + + Takes a CSS Style string and merges it with the objects current style, overwriting only the elements present in stylestring. + + Style Strings + + + + + + + Any object, or 'document' to apply to document style. + + + + + The red component. + + + + + The green component. + + + + + The blue component. + + + + + True to change fill color, false for stroke color. + + + + + Modifies the fill or stroke color of an object (or the document style) based on RGB values. + Red green and blue must be between 0-255 inclusive. + + + + + + + + The id of an object. + + + + + A layer name. + + + + + Moves an object to a different layer. + Will error if layer does not exist. + + layer_new() + + + + + + + A object that contains a path ("d") attribute. + + + + + + An array of points. + + + + + Returns an array of all of the X,Y coordinates of the points in the objects path. + If the path is a closed loop the first point is repeated at the end. + + + + + + + + + + Saves the current document with current name or a default name if has not been saved before. + Will overwrite without confirmation. + + + + + + + + The path for the file to be saved as. + + + + + Saves the current document as pathname. + Will overwrite without confirmation. + + + + + + + + The path to a valid svg file. + + + + + Loads the file at pathname. + Will lose all unsaved work in current document. + + + + + + + + Marks the document as unmodified/saved. + Will prevent save confirmation on close if called at end of script. + + + + + + + + + + + + + + Close this document. + You will not be able to send any more commands on this interface. + + + + + + + + Exit Inkscape. + You will not be able to send any more commands on any interface. + + + + + + + + Undo the last action. + + + + + + + + Redo the last undone action. + + + + + + + + + + When updates are paused Inkscape will not draw every change as it is made. Also you will not be able to undo individual actions made while updates were paused and will only be able to undo them in a group. Inkscape may refresh the screen every couple of seconds even with updates off. + The advantage is a 2-5x speed increase, depending on the type of functions being called. This is most useful when creating large numbers of shapes. + + + + + + + + Resume updates after they have been paused. If undo is called at this point it will undo everything that happened since pause_updates() was called. + This will update the display to show any changes that happened while updates were paused, a separate call to update() is not necessary. + + + + + + + + This will update the document once if updates are paused but it will not resume updates. + This could be used to check on the progress of a complex drawing function, or to add in undo steps at certain points in a render. + + + + + + + + + + + List of the ids of currently selected objects. + + + + + Returns the current selection in the form of a list of ids of selected objects. + Manipulating this list will not affect the selection. + + + + + + + + A object to add to the selection. + + + + + Adds a single object to the selection. + + + + + + + + An array of object ids to add to the selection. + + + + + Adds a list of objects to the selection. + + + + + + + + A object to select. + + + + + Replaces the selection with one containing just this object. + + + + + + + + A list of objects to select. + + + + + Replaces the selection with one containing just these objects. + + + + + + + + Angle in degrees to rotate. + + + + + Rotates the selection around the center of it's bounding box. + + + + + + + + Delete all objects in the selection. + + + + + + + + Deselect everything. Selection will be empty. + + + + + + + + Select all objects in current layer. + + + + + + + + Select all objects in every layer. + + + + + + + + X coordinate for the first point. + + + + + Y coordinate for the first point. + + + + + X coordinate for the second point. + + + + + Y coordinate for the second point. + + + + + True to replace selection, false to add to selection. + + + + + This method finds all of the objects inside the box and adds them to the current selection. If replace is true it will clear the old selection first. + + + + + + + + Invert the selection in the current layer. + + + + + + + + Group the selection. + + Groups + + + + + + + Ungroup the selection. + + Groups + + + + + + + Cut the current selection. + + + + + + + + Copy the current selection. + + + + + + + + Paste the current selection at the same location it was cut from. + To paste to a particular location, simply use selection_paste() followed by selection_move_to(). + + + + + + + + The amount to scale the selection, 1 has no effect. Between 0 and 1 will shrink it proportionally. Greater than one will grow it proportionally. + + + + + Scale the selection relative to it's current size. + + + + + + + + Amount to move in the x direction. + + + + + Amount to move in the y direction. + + + + + This will move the selection relative to it's current location. + This may be accomplished with transformation attributes or by changing x and y attributes depending on the state of the objects in the selection. + + Coordinate System + + + + + + + X coordinate to move to. + + + + + Y coordinate to move to. + + + + + This will move the center of the selection to a specific location. + This may be accomplished with transformation attributes or by changing x and y attributes depending on the state of the objects in the selection. + + Coordinate System + + + + + + + layer to move the selection to. + + + + + Move every item in the selection to a different layer. + Will error if layer does not exist. + + Layers and Levels + + + + + + + + Center of the selection. + + + + + Gets the center of the selections bounding box in X,Y coordinates. + + Coordinate System + + + + + + + Turns all the objects in the selection into paths. + + object_to_path() + + + + + + + Type of combination. + + + + + List of the ids of resulting paths after applying the operation. + + + + + Will erase all objects in the selection and replace with a single aggregate path. + There are 5 types that can be passed in: + 'union': The new shape is all of the other shapes put together, even if they don't overlap (paths can have multiple non-contiguous areas.) + 'intersection': The new shape is composed of the area where ALL the objects in the selection overlap. If there is no area where all shapes overlap the new shape will be empty. + 'difference': The area of the second shape is subtracted from the first, only works with two objects. + 'exclusion': The new shape is the area(s) where none of the objects in the selection overlapped. Only works with two objects. + 'division': the first object is split into multiple segments by the second object. Only works with two objects. + + + + + + + + How to change the level + + + + + + True if the objects changed levels. False if they don't(if they were already on top when being raised for example.) + + + + + Will change the level of a selection, respective of other objects in the same layer. Will not affect the overlap of objects in different layers. Will do nothing if the selection contains objects in multiple layers. + There are 4 commands that can be passed in: + "raise" or "lower": Move the selection one level up, or one level down. + "to_top" of "to_bottom": Move the selection above all other objects or below all other objects. + + + + + + + + + + + The name of the new layer. + + + + + Turns all the objects in the selection into paths. + + Layers and Levels + + + + + + + The name of any layer. + + + + + Sets the layer given as the current layer + + Layers and Levels + + + + + + + + list of layers. + + + + + Get a list of all the layers in this document. + + Layers and Levels + + + + + + + How to change the level + + + + + + True if the layer was moved. False if it was not (if it was already on top when being raised for example.) + + + + + Will change the level of a layer, respective of other layers. Will not affect the relative level of objects within the layer. + There are 4 commands that can be passed in: + "raise" or "lower": Move the layer one level up, or one level down. + "to_top" of "to_bottom": Move the layer above all other layers or below all other layers. + + + + + + + + Sets the next (or higher) layer as active. + + Layers and Levels + + + + + + + Sets the previous (or lower) layer as active. + + Layers and Levels + + + + + + + + The id of the object. + + + + + Emitted when an object has been moved. + + + + + + + + + Any node with an "id" attribute. + + + + + The ids of this nodes children, NULL if bottom level. + + + + + Returns the children of any node. This function along with get_parent() can be used to navigate the XML tree. + + + + + + + Any node with an "id" attribute. + + + + + + The id of this nodes parent, NULL if toplevel. + + + + + Returns the parent of any node. This function along with get_children() can be used to navigate the XML tree. + + + + + + diff --git a/src/extension/dbus/org.inkscape.service.in b/src/extension/dbus/org.inkscape.service.in new file mode 100644 index 0000000..9fffa02 --- /dev/null +++ b/src/extension/dbus/org.inkscape.service.in @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +[D-BUS Service] +Name=org.inkscape +Exec=bindir/bin/inkscape + + diff --git a/src/extension/dbus/proposed-interface.xml b/src/extension/dbus/proposed-interface.xml new file mode 100644 index 0000000..829ee7e --- /dev/null +++ b/src/extension/dbus/proposed-interface.xml @@ -0,0 +1,142 @@ + + + + + + + + + + Signals would undoubtedly be a useful thing to have in many circumstances. They are in proposed for two reasons: One, they complicate things for script writers and may conflict with the proposed C wrapper library. Two, I'm not sure how much coding it would take to implement them because I am familiar with neither Dbus signals or Inkscape events. Until I have done more experimenting I don't want to promise anything I'm not sure can be implemented in a timely fashion. + + + + + + + + The id of the object. + + + + + Emitted when an object has been resized. + + + + + + + + The id of the object. + + + + + Emitted when the style of an object has been changed. + + + + + + + + The id of the object. + + + + + Emitted when an object has been created. Possibly useful for working in conjunction with a live user. + + + + + + + + The id of the object. + + + + + Emitted when an object has been added to the selection. Possibly useful for working in conjunction with a live user. + + + + + + + + The x value to begin the path. + + + + + The y value to begin the path. + + + + + Begins a new path, extra nodes can be added with path_append(). + + + + + + + + The name of the path to append to. + + + + + A single letter denoting what type of node is being appended. + + + + + An array of numbers that describe the position and attributes of the path node. + + + + + Adds to an existing path. Close the path by sending "z" and no arguments. + You can no longer append to a path if it is closed. + + + + + + + + + + + A object to remove from the selection. + + + + + Removes a single object from the selection. In proposed because I already have a ton of selection functions and am not sure people would need this. + + + + + + diff --git a/src/extension/dbus/pytester.py b/src/extension/dbus/pytester.py new file mode 100644 index 0000000..fdd7e9a --- /dev/null +++ b/src/extension/dbus/pytester.py @@ -0,0 +1,291 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +##################################################################### +# Python test script for Inkscape DBus API. +# +# Contains many examples of functions and various stress tests etc. +# Multiple tests can be run at once but the output will be a bit chaotic. +# List of test functions can be found at the bottom of the script. +##################################################################### + +import dbus +import random + +##################################################################### +# Various test functions, called at bottom of script +##################################################################### + +def randomrect (document): + document.rectangle( random.randint(0,1000), + random.randint(0,1000), + random.randint(1,100), + random.randint(1,100)) + +def lottarects ( document ): + document.pause_updates() + listy = [] + for x in range(1,2000): + if x == 1000: + print "HALFWAY" + if x == 1: + print "BEGUN" + document.rectangle( 0, 0, 100, 100) + #randomrect(document) + print "DONE" + for x in listy: + print x + selection_set(x) + document.resume_updates() + +def lottaverbs (doc): + doc.pause_updates() + doc.document_set_css ("fill:#ff0000;fill-opacity:.5;stroke:#0000ff;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none") + doc.rectangle( 0, 0, 100, 100) + doc.select_all() + doc.selection_copy() + for x in range(1,2000): + if x == 1000: + print "HALFWAY" + if x == 1: + print "BEGUN" + doc.selection_paste() + #doc.rectangle( 0, 0, 100, 100) + doc.resume_updates() + +def testDrawing (doc): + doc.document_set_css ("fill:#000000;fill-opacity:.5;stroke:#000000;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none") + doc.ellipse( 0, 0, 100, 100) + doc.select_all() + doc.selection_copy() + for x in range(1,2000): + if x == 1000: + print "HALFWAY" + if x == 1: + print "BEGUN" + doc.selection_paste() + newrect = doc.selection_get()[0] + doc.set_color(newrect, 255 - x%255, 0, 200, True) + doc.set_color(newrect, 0, 255 - x%75, x%75, False) + doc.mark_as_unmodified() + + +def testcopypaste (document ): + #document.pause_updates() + print document.rectangle (400, 500, 100, 100) + print document.rectangle (200, 200, 100, 100) + document.select_all() + document.selection_copy() + document.selection_paste() + #document.resume_updates() + +def testShapes (doc): + doc.rectangle (0, 0, 100, 100) + doc.ellipse (100, 100, 100, 100) + doc.star (250, 250, 50, 25, 5, False, .9, 1.4) + doc.polygon (350, 350, 50, 0, 5) + doc.line (400,400,500,500) + doc.spiral (550,550,50,3) + +def testMovement (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (0, 100, 100, 100) + rect3 = doc.rectangle (0, 200, 100, 100) + doc.select_all() + doc.move(rect2, 100,100) + +def testImport (doc): + # CHANGE TO LOCAL SVG FILE! + img1 = doc.image(0,0, "/home/soren/chavatar.jpg") + doc.selection_add(img1) + doc.selection_scale(500) + doc.transform(img1, "rotate(30)") + +def testSelections (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (0, 100, 100, 100) + rect3 = doc.rectangle (0, 200, 100, 100) + rect4 = doc.rectangle (0, 300, 100, 100) + + doc.selection_add (rect1) + center = doc.selection_get_center() + for d in center: + print d + doc.selection_to_path() + doc.get_path(rect1) + doc.selection_move(100.0, 100.0) + doc.selection_set(rect2) + doc.selection_move_to(0.0,0.0) + doc.selection_set(rect3) + doc.move(rect4, 500.0, 500.0) + doc.select_all() + doc.selection_to_path() + result = doc.selection_get() + print len(result) + for d in result: + print d + +def testLevels (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + rect4 = doc.rectangle (60, 60, 100, 100) + + doc.selection_set(rect1) + doc.selection_change_level("raise") + + doc.selection_set(rect4) + doc.selection_change_level("to_bottom") + +def testCombinations (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + rect4 = doc.rectangle (60, 60, 100, 100) + rect5 = doc.rectangle (80, 80, 100, 100) + rect6 = doc.rectangle (100, 100, 100, 100) + rect7 = doc.rectangle (120, 120, 100, 100) + rect8 = doc.rectangle (140, 140, 100, 100) + rect9 = doc.rectangle (160, 160, 100, 100) + rect10 = doc.rectangle (180, 180, 100, 100) + + doc.selection_set_list([rect1, rect2]) + print doc.selection_combine("union") + doc.selection_set_list([rect3, rect4]) + print doc.selection_combine("intersection") + doc.selection_set_list([rect5, rect6]) + print doc.selection_combine("difference") + doc.selection_set_list([rect7, rect8]) + print doc.selection_combine("exclusion") + doc.selection_set_list([rect9, rect10]) + for d in doc.selection_divide(): + print d + +def testTransforms (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + doc.set_attribute(rect1, "transform", "matrix(0.08881734,0.94288151,-0.99604793,0.68505564,245.36153,118.60315)") + doc.selection_set(rect1) + + doc.selection_move_to(200, 200) + +def testLayer (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + print doc.new_layer() + rect2 = doc.rectangle (20, 20, 100, 100) + +def testGetSelection (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + doc.select_all() + result = doc.selection_get() + print result + print len(result) + for d in result: + print d + + +def testDocStyle (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + doc.document_set_css ("fill:#ff0000;fill-opacity:.5;stroke:#0000ff;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none") + rect2 = doc.rectangle (20, 20, 100, 100) + doc.document_set_css ("fill:#ffff00;fill-opacity:1;stroke:#009002;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none") + rect3 = doc.rectangle (40, 40, 100, 100) + doc.document_set_css ("fill:#00ff00;fill-opacity:1") + rect4 = doc.rectangle (60, 60, 100, 100) + +def testStyle (doc): + doc.document_set_css ("fill:#ffff00;fill-opacity:1;stroke:#009002;stroke-width:5;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none") + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + rect4 = doc.rectangle (60, 60, 100, 100) + + doc.modify_css (rect3, "fill-opacity", ".5") + doc.merge_css (rect4, "fill:#0000ff;fill-opacity:.25;") + print doc.get_css (rect4) + +def testLayers (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + layer1 = doc.layer_new() + layer2 = doc.layer_new() + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + doc.selection_add(rect3) + doc.selection_move_to_layer(layer1) + +def testLoadSave (doc): + doc.load("/home/soren/testfile.svg") + rect2 = doc.rectangle (0, 0, 200, 200) + doc.save_as("/home/soren/testsave.svg") + rect1 = doc.rectangle (20, 20, 200, 200) + doc.save() + rect3 = doc.rectangle (40, 40, 200, 200) + doc.save_as("/home/soren/testsave2.svg") + +def testArray (doc): + rect1 = doc.rectangle (0, 0, 100, 100) + rect2 = doc.rectangle (20, 20, 100, 100) + rect3 = doc.rectangle (40, 40, 100, 100) + doc.selection_set_list([rect1, rect2, rect3]) + +def testPath (doc): + cr1 = doc.ellipse(0,0,50,50) + print doc.get_path(cr1) + doc.object_to_path(cr1) + print doc.get_path(cr1) + #doc.get_node_coordinates(cr1) + +# Needs work. +def testText(doc): + print doc.text(200, 200, "svg:text") + + +##################################################################### +# Setup bus connection, create documents. +##################################################################### + +# Connect to bus +bus = dbus.SessionBus() + +# Get interface for default document +inkdoc1 = bus.get_object('org.inkscape', '/org/inkscape/desktop_0') +doc1 = dbus.Interface(inkdoc1, dbus_interface="org.inkscape.document") + +# Create new window and get the interface for that. (optional) +inkapp = bus.get_object('org.inkscape', + '/org/inkscape/application') +desk2 = inkapp.desktop_new(dbus_interface='org.inkscape.application') +inkdoc2 = bus.get_object('org.inkscape', desk2) +doc2 = dbus.Interface(inkdoc2, dbus_interface="org.inkscape.document") + + +##################################################################### +# Call desired test functions +##################################################################### + +#lottaverbs (doc1) +#lottarects (doc1) +#testDrawing (doc1) + +#doc1.pause_updates() + +testShapes(doc1) +#testMovement(doc1) +#testImport(doc1) # EDIT FUNCTION TO OPEN EXISTING FILE! +#testcopypaste (doc1) +#testTransforms (doc1) +#testDocStyle(doc1) +#testLayers(doc1) +#testLoadSave(doc1) +#testArray(doc1) +#testSelections(doc1) +#testCombinations(doc1) +#testText(doc1) +#testPath(doc1) + +#doc1.resume_updates + + +# Prevents asking if you want to save when closing document. +doc1.mark_as_unmodified() + diff --git a/src/extension/dbus/wrapper/inkdbus.pc.in b/src/extension/dbus/wrapper/inkdbus.pc.in new file mode 100644 index 0000000..512e3b3 --- /dev/null +++ b/src/extension/dbus/wrapper/inkdbus.pc.in @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +bindir=@bindir@ +includedir=@includedir@ + +Cflags: -I${includedir}/libinkdbus-0.1 +Requires: gobject-2.0 dbus-glib-1 +Libs: -L${libdir} -linkdbus + +Name: inkdbus +Description: Inkscape DBus Interface Wrapper +Version: @VERSION@ + diff --git a/src/extension/dbus/wrapper/inkscape-dbus-wrapper.c b/src/extension/dbus/wrapper/inkscape-dbus-wrapper.c new file mode 100644 index 0000000..cc9399c --- /dev/null +++ b/src/extension/dbus/wrapper/inkscape-dbus-wrapper.c @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "inkscape-dbus-wrapper.h" +#include +#include +#include + + + +#include "document-client-glue.h" +#include +#include + +// (static.*(\n[^}]*)*(async)+.*(\n[^}]*)*})|typedef void .*; +// http://www.josephkahn.com/music/index.xml + +/* PRIVATE get a connection to the session bus */ +DBusGConnection * +dbus_get_connection() { + GError *error = NULL; + DBusGConnection *connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error); + if (error) { + fprintf(stderr, "Failed to get connection"); + return NULL; + } + else + return connection; +} + +/* PRIVATE create a proxy object for a bus.*/ +DBusGProxy * +dbus_get_proxy(DBusGConnection *connection) { + return dbus_g_proxy_new_for_name (connection, + DBUS_SERVICE_DBUS, + DBUS_PATH_DBUS, + DBUS_INTERFACE_DBUS); +} + +#if 0 +/* PRIVATE register an object on a bus */ +static gpointer +dbus_register_object (DBusGConnection *connection, + DBusGProxy * proxy, + GType object_type, + const DBusGObjectInfo *info, + const gchar *path) +{ + GObject *object = (GObject*)g_object_new (object_type, NULL); + dbus_g_object_type_install_info (object_type, info); + dbus_g_connection_register_g_object (connection, path, object); + return object; +} +#endif + +/**************************************************************************** + DOCUMENT INTERFACE CLASS STUFF +****************************************************************************/ + +struct _DocumentInterface { + GObject parent; + DBusGProxy * proxy; +}; + +G_DEFINE_TYPE(DocumentInterface, document_interface, G_TYPE_OBJECT) + +static void +document_interface_finalize (GObject *object) +{ + G_OBJECT_CLASS (document_interface_parent_class)->finalize (object); +} + + +static void +document_interface_class_init (DocumentInterfaceClass *klass) +{ + GObjectClass *object_class; + object_class = G_OBJECT_CLASS (klass); + object_class->finalize = document_interface_finalize; +} + +static void +document_interface_init (DocumentInterface *object) +{ + object->proxy = NULL; +} + + +DocumentInterface * +document_interface_new (void) +{ + return (DocumentInterface*)g_object_new (TYPE_DOCUMENT_INTERFACE, NULL); +} + +DocumentInterface * +inkscape_desktop_init_dbus () +{ + DBusGConnection *connection; + GError *error; + DBusGProxy *proxy; + +#if !GLIB_CHECK_VERSION(2,36,0) + g_type_init (); +#endif + + error = NULL; + connection = dbus_g_bus_get (DBUS_BUS_SESSION, + &error); + if (connection == NULL) + { + g_printerr ("Failed to open connection to bus: %s\n", + error->message); + g_error_free (error); + exit (1); + } + + proxy = dbus_g_proxy_new_for_name (connection, + "org.inkscape", + "/org/inkscape/desktop_0", + "org.inkscape.document"); + + DocumentInterface * inkdesk = (DocumentInterface *)g_object_new (TYPE_DOCUMENT_INTERFACE, NULL); + inkdesk->proxy = proxy; + return inkdesk; +} + + +//static +gboolean +inkscape_delete_all (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_delete_all (proxy, error); +} + +//static +gboolean +inkscape_call_verb (DocumentInterface *doc, const char * IN_verbid, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_call_verb(proxy, IN_verbid, error); +} + + +//static +gchar * +inkscape_rectangle (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_width, const gint IN_height, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_rectangle (proxy, IN_x, IN_y, IN_width, IN_height, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_ellipse (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_width, const gint IN_height, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_ellipse (proxy, IN_x, IN_y, IN_width, IN_height, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_polygon (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_radius, const gint IN_rotation, const gint IN_sides, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_polygon (proxy, IN_cx, IN_cy, IN_radius, IN_rotation, IN_sides, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_star (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_r1, const gint IN_r2, const gdouble IN_arg1, const gdouble IN_arg2, const gint IN_sides, const gdouble IN_rounded, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_star (proxy, IN_cx, IN_cy, IN_r1, IN_r2, IN_arg1, IN_arg2, IN_sides, IN_rounded, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_spiral (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_r, const gint IN_revolutions, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_spiral (proxy, IN_cx, IN_cy, IN_r, IN_revolutions, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_line (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_x2, const gint IN_y2, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_line (proxy, IN_x, IN_y, IN_x2, IN_y2, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_text (DocumentInterface *doc, const gint IN_x, const gint IN_y, const char * IN_text, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_text (proxy, IN_x, IN_y, IN_text, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_image (DocumentInterface *doc, const gint IN_x, const gint IN_y, const char * IN_text, GError **error) +{ + char * OUT_object_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_image (proxy, IN_x, IN_y, IN_text, &OUT_object_name, error); + return OUT_object_name; +} + +//static +char * +inkscape_node (DocumentInterface *doc, const char * IN_svgtype, GError **error) +{ + char *OUT_node_name; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_node (proxy, IN_svgtype, &OUT_node_name, error); + return OUT_node_name; +} + +//static +gdouble +inkscape_document_get_width (DocumentInterface *doc, GError **error) +{ + gdouble OUT_val; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_document_get_width (proxy, &OUT_val, error); + return OUT_val; +} + +//static +gdouble +inkscape_document_get_height (DocumentInterface *doc, GError **error) +{ + gdouble OUT_val; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_document_get_height (proxy, &OUT_val, error); + return OUT_val; +} + +//static +char * +inkscape_document_get_css (DocumentInterface *doc, GError **error) +{ + char * OUT_css; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_document_get_css (proxy, &OUT_css, error); + return OUT_css; +} + +//static +gboolean +inkscape_document_set_css (DocumentInterface *doc, const char * IN_stylestring, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_document_set_css (proxy, IN_stylestring, error); +} + +//static +gboolean +inkscape_document_merge_css (DocumentInterface *doc, const char * IN_stylestring, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_document_merge_css (proxy, IN_stylestring, error); +} + +//static +gboolean +inkscape_document_resize_to_fit_selection (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_document_resize_to_fit_selection (proxy, error); +} + +//static +gboolean +inkscape_set_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const char * IN_newval, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_set_attribute (proxy, IN_shape, IN_attribute, IN_newval, error); +} + +//static +gboolean +inkscape_set_int_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const gint IN_newval, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_set_int_attribute (proxy, IN_shape, IN_attribute, IN_newval, error); +} + +//static +gboolean +inkscape_set_double_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const gdouble IN_newval, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_set_double_attribute (proxy, IN_shape, IN_attribute, IN_newval, error); +} + +//static +char * +inkscape_get_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, GError **error) +{ + char * OUT_val; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_get_attribute (proxy, IN_shape, IN_attribute, &OUT_val, error); + return OUT_val; +} + +//static +gboolean +inkscape_move (DocumentInterface *doc, const char * IN_shape, const gdouble IN_x, const gdouble IN_y, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_move (proxy, IN_shape, IN_x, IN_y, error); +} + +//static +gboolean +inkscape_move_to (DocumentInterface *doc, const char * IN_shape, const gdouble IN_x, const gdouble IN_y, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_move_to (proxy, IN_shape, IN_x, IN_y, error); +} + +//static +gboolean +inkscape_object_to_path (DocumentInterface *doc, const char * IN_objectname, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_object_to_path (proxy, IN_objectname, error); +} + +//static +char * +inkscape_get_path (DocumentInterface *doc, const char * IN_shape, GError **error) +{ + char * OUT_val; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_get_path (proxy, IN_shape, &OUT_val, error); + return OUT_val; +} + +//static +gboolean +inkscape_transform (DocumentInterface *doc, const char * IN_shape, const char * IN_transformstr, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_transform (proxy, IN_shape, IN_transformstr, error); +} + +//static +char * +inkscape_get_css (DocumentInterface *doc, const char * IN_shape, GError **error) +{ + char * OUT_css; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_get_css (proxy, IN_shape, &OUT_css, error); + return OUT_css; +} + +//static +gboolean +inkscape_modify_css (DocumentInterface *doc, const char * IN_shape, const char * IN_cssattrib, const char * IN_newval, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_modify_css (proxy, IN_shape, IN_cssattrib, IN_newval, error); +} + +//static +gboolean +inkscape_merge_css (DocumentInterface *doc, const char * IN_shape, const char * IN_stylestring, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_merge_css (proxy, IN_shape, IN_stylestring, error); +} + +//static +gboolean +inkscape_set_color (DocumentInterface *doc, const char * IN_shape, const gint IN_red, const gint IN_green, const gint IN_blue, const gboolean IN_fill, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_set_color (proxy, IN_shape, IN_red, IN_green, IN_blue, IN_fill, error); +} + +//static +gboolean +inkscape_move_to_layer (DocumentInterface *doc, const char * IN_objectname, const char * IN_layername, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_move_to_layer (proxy, IN_objectname, IN_layername, error); +} + +//static +GArray* +inkscape_get_node_coordinates (DocumentInterface *doc, const char * IN_shape, GError **error) +{ + GArray* OUT_points; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_get_node_coordinates (proxy, IN_shape, &OUT_points, error); + return OUT_points; +} + +//static +gboolean +inkscape_save (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_save (proxy, error); +} + +//static +gboolean +inkscape_save_as (DocumentInterface *doc, const char * IN_pathname, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_save_as (proxy, IN_pathname, error); +} + +//static +gboolean +inkscape_load (DocumentInterface *doc, const char * IN_pathname, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_load (proxy, IN_pathname, error); +} + +//static +gboolean +inkscape_mark_as_unmodified (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_mark_as_unmodified (proxy, error); +} + +//static +gboolean +inkscape_close (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_close (proxy, error); +} + +//static +gboolean +inkscape_inkscape_exit (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_exit (proxy, error); +} + +//static +gboolean +inkscape_undo (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_undo (proxy, error); +} + +//static +gboolean +inkscape_redo (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_redo (proxy, error); +} + +//static +gboolean +inkscape_pause_updates (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_pause_updates (proxy, error); +} + +//static +gboolean +inkscape_resume_updates (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_resume_updates (proxy, error); +} + +//static +gboolean +inkscape_update (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_update (proxy, error); +} + +//static +char ** +inkscape_selection_get (DocumentInterface *doc, GError **error) +{ + char ** OUT_listy; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_selection_get (proxy, &OUT_listy, error); + return OUT_listy; +} + +//static +gboolean +inkscape_selection_add (DocumentInterface *doc, const char * IN_name, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_add (proxy, IN_name, error); +} + +//static +gboolean +inkscape_selection_add_list (DocumentInterface *doc, const char ** IN_name, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_add_list (proxy, IN_name, error); +} + +//static +gboolean +inkscape_selection_set (DocumentInterface *doc, const char * IN_name, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_set (proxy, IN_name, error); +} + +//static +gboolean +inkscape_selection_set_list (DocumentInterface *doc, const char ** IN_name, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_set_list (proxy, IN_name, error); +} + +//static +gboolean +inkscape_selection_rotate (DocumentInterface *doc, const gint IN_angle, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_rotate (proxy, IN_angle, error); +} + +//static +gboolean +inkscape_selection_delete (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_delete (proxy, error); +} + +//static +gboolean +inkscape_selection_clear (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_clear (proxy, error); +} + +//static +gboolean +inkscape_select_all (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_select_all (proxy, error); +} + +//static +gboolean +inkscape_select_all_in_all_layers (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_select_all_in_all_layers (proxy, error); +} + +//static +gboolean +inkscape_selection_box (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_x2, const gint IN_y2, const gboolean IN_replace, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_box (proxy, IN_x, IN_y, IN_x2, IN_y2, IN_replace, error); +} + +//static +gboolean +inkscape_selection_invert (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_invert (proxy, error); +} + +//static +gboolean +inkscape_selection_group (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_group (proxy, error); +} + +//static +gboolean +inkscape_selection_ungroup (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_ungroup (proxy, error); +} + +//static +gboolean +inkscape_selection_cut (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_cut (proxy, error); +} + +//static +gboolean +inkscape_selection_copy (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_copy (proxy, error); +} + +//static +gboolean +inkscape_selection_paste (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_paste (proxy, error); +} + +//static +gboolean +inkscape_selection_scale (DocumentInterface *doc, const gdouble IN_grow, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_scale (proxy, IN_grow, error); +} + +//static +gboolean +inkscape_selection_move (DocumentInterface *doc, const gdouble IN_x, const gdouble IN_y, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_move (proxy, IN_x, IN_y, error); +} + +//static +gboolean +inkscape_selection_move_to (DocumentInterface *doc, const gdouble IN_x, const gdouble IN_y, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_move_to (proxy, IN_x, IN_y, error); +} + +//static +gboolean +inkscape_selection_move_to_layer (DocumentInterface *doc, const char * IN_layer, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_move_to_layer (proxy, IN_layer, error); +} + +//static +GArray * +inkscape_selection_get_center (DocumentInterface *doc, GError **error) +{ + GArray* OUT_centerpoint; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_selection_get_center (proxy, &OUT_centerpoint, error); + return OUT_centerpoint; +} + +//static +gboolean +inkscape_selection_to_path (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_selection_to_path (proxy, error); +} + +//static +char ** +inkscape_selection_combine (DocumentInterface *doc, const char * IN_type, GError **error) +{ + char ** OUT_newpaths; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_selection_combine (proxy, IN_type, &OUT_newpaths, error); + return OUT_newpaths; +} + +//static +gboolean +inkscape_selection_change_level (DocumentInterface *doc, const char * IN_command, GError **error) +{ + gboolean OUT_objectsmoved; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_selection_change_level (proxy, IN_command, &OUT_objectsmoved, error); + return OUT_objectsmoved; +} + +//static +char * +inkscape_layer_new (DocumentInterface *doc, GError **error) +{ + char * OUT_layername; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_layer_new (proxy, &OUT_layername, error); + return OUT_layername; +} + +//static +gboolean +inkscape_layer_set (DocumentInterface *doc, const char * IN_layer, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_layer_set (proxy, IN_layer, error); +} + +//static +char ** +inkscape_layer_get_all (DocumentInterface *doc, GError **error) +{ + char ** OUT_layers; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_layer_get_all (proxy, &OUT_layers, error); + return OUT_layers; +} + +//static +gboolean +inkscape_layer_change_level (DocumentInterface *doc, const char * IN_command, GError **error) +{ + gboolean OUT_layermoved; + DBusGProxy *proxy = doc->proxy; + org_inkscape_document_layer_change_level (proxy, IN_command, &OUT_layermoved, error); + return OUT_layermoved; +} + +//static +gboolean +inkscape_layer_next (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_layer_next (proxy, error); +} + +//static +gboolean +inkscape_layer_previous (DocumentInterface *doc, GError **error) +{ + DBusGProxy *proxy = doc->proxy; + return org_inkscape_document_layer_previous (proxy, error); +} + +/* +int +main (int argc, char** argv) +{ + gchar * result; + GError *error = NULL; + DocumentInterface * doc = inkscape_desktop_init_dbus (); + result = rectangle (doc->proxy, 10, 10, 100, 100, &error); + printf("RESULT: %s\n", result); + + //dbus_g_proxy_call(doc->proxy, "rectangle", &error, G_TYPE_INT, 100, G_TYPE_INT, 100, G_TYPE_INT, 100, G_TYPE_INT, 100, G_TYPE_INVALID, G_TYPE_INVALID); + printf("yes\n"); +} +*/ + diff --git a/src/extension/dbus/wrapper/inkscape-dbus-wrapper.h b/src/extension/dbus/wrapper/inkscape-dbus-wrapper.h new file mode 100644 index 0000000..e2abd30 --- /dev/null +++ b/src/extension/dbus/wrapper/inkscape-dbus-wrapper.h @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ +#define INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ + +#include +#include + +//#include "document-client-glue-mod.h" + +//#include +//#include + +#define TYPE_DOCUMENT_INTERFACE (document_interface_get_type ()) +#define DOCUMENT_INTERFACE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), TYPE_DOCUMENT_INTERFACE, DocumentInterface)) +#define DOCUMENT_INTERFACE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TYPE_DOCUMENT_INTERFACE, DocumentInterfaceClass)) +#define IS_DOCUMENT_INTERFACE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), TYPE_DOCUMENT_INTERFACE)) +#define IS_DOCUMENT_INTERFACE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TYPE_DOCUMENT_INTERFACE)) +#define DOCUMENT_INTERFACE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TYPE_DOCUMENT_INTERFACE, DocumentInterfaceClass)) + +G_BEGIN_DECLS + +typedef struct _DocumentInterface DocumentInterface; +typedef struct _DocumentInterfaceClass DocumentInterfaceClass; + +struct _DocumentInterface; + +struct _DocumentInterfaceClass { + GObjectClass parent; +}; + + +DocumentInterface *document_interface_new (void); +GType document_interface_get_type (void); + + + +DocumentInterface * +inkscape_desktop_init_dbus (); + +//static +gboolean +inkscape_delete_all (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_call_verb (DocumentInterface *doc, const char * IN_verbid, GError **error); + + +//static +gchar * +inkscape_rectangle (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_width, const gint IN_height, GError **error); + +//static +char * +inkscape_ellipse (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_width, const gint IN_height, GError **error); + +//static +char * +inkscape_polygon (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_radius, const gint IN_rotation, const gint IN_sides, GError **error); + +//static +char * +inkscape_star (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_r1, const gint IN_r2, const gdouble IN_arg1, const gdouble IN_arg2, const gint IN_sides, const gdouble IN_rounded, GError **error); + +//static +char * +inkscape_spiral (DocumentInterface *doc, const gint IN_cx, const gint IN_cy, const gint IN_r, const gint IN_revolutions, GError **error); + +//static +char * +inkscape_line (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_x2, const gint IN_y2, GError **error); + +//static +char * +inkscape_text (DocumentInterface *doc, const gint IN_x, const gint IN_y, const char * IN_text, GError **error); + +//static +char * +inkscape_image (DocumentInterface *doc, const gint IN_x, const gint IN_y, const char * IN_text, GError **error); + +//static +char * +inkscape_node (DocumentInterface *doc, const char * IN_svgtype, GError **error); + +//static +gdouble +inkscape_document_get_width (DocumentInterface *doc, GError **error); + +//static +gdouble +inkscape_document_get_height (DocumentInterface *doc, GError **error); + +//static +char * +inkscape_document_get_css (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_document_set_css (DocumentInterface *doc, const char * IN_stylestring, GError **error); + +//static +gboolean +inkscape_document_merge_css (DocumentInterface *doc, const char * IN_stylestring, GError **error); + +//static +gboolean +inkscape_document_resize_to_fit_selection (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_set_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const char * IN_newval, GError **error); + +//static +gboolean +inkscape_set_int_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const gint IN_newval, GError **error); + +//static +gboolean +inkscape_set_double_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, const gdouble IN_newval, GError **error); + +//static +char * +inkscape_get_attribute (DocumentInterface *doc, const char * IN_shape, const char * IN_attribute, GError **error); + +//static +gboolean +inkscape_move (DocumentInterface *doc, const char * IN_shape, const gdouble IN_x, const gdouble IN_y, GError **error); + +//static +gboolean +inkscape_move_to (DocumentInterface *doc, const char * IN_shape, const gdouble IN_x, const gdouble IN_y, GError **error); + +//static +gboolean +inkscape_object_to_path (DocumentInterface *doc, const char * IN_objectname, GError **error); + +//static +char * +inkscape_get_path (DocumentInterface *doc, const char * IN_shape, GError **error); + +//static +gboolean +inkscape_transform (DocumentInterface *doc, const char * IN_shape, const char * IN_transformstr, GError **error); + +//static +char * +inkscape_get_css (DocumentInterface *doc, const char * IN_shape, GError **error); + +//static +gboolean +inkscape_modify_css (DocumentInterface *doc, const char * IN_shape, const char * IN_cssattrib, const char * IN_newval, GError **error); + +//static +gboolean +inkscape_inkscape_merge_css (DocumentInterface *doc, const char * IN_shape, const char * IN_stylestring, GError **error); + +//static +gboolean +inkscape_set_color (DocumentInterface *doc, const char * IN_shape, const gint IN_red, const gint IN_green, const gint IN_blue, const gboolean IN_fill, GError **error); + +//static +gboolean +inkscape_move_to_layer (DocumentInterface *doc, const char * IN_objectname, const char * IN_layername, GError **error); + +//static +GArray* +inkscape_get_node_coordinates (DocumentInterface *doc, const char * IN_shape, GError **error); + +//static +gboolean +inkscape_save (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_save_as (DocumentInterface *doc, const char * IN_pathname, GError **error); + +//static +gboolean +inkscape_load (DocumentInterface *doc, const char * IN_pathname, GError **error); + +//static +gboolean +inkscape_mark_as_unmodified (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_close (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_inkscape_exit (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_undo (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_redo (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_pause_updates (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_resume_updates (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_update (DocumentInterface *doc, GError **error); + +//static +char ** +inkscape_selection_get (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_add (DocumentInterface *doc, const char * IN_name, GError **error); + +//static +gboolean +inkscape_selection_add_list (DocumentInterface *doc, const char ** IN_name, GError **error); + +//static +gboolean +inkscape_selection_set (DocumentInterface *doc, const char * IN_name, GError **error); + +//static +gboolean +inkscape_selection_set_list (DocumentInterface *doc, const char ** IN_name, GError **error); + +//static +gboolean +inkscape_selection_rotate (DocumentInterface *doc, const gint IN_angle, GError **error); + +//static +gboolean +inkscape_selection_delete (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_clear (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_select_all (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_select_all_in_all_layers (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_box (DocumentInterface *doc, const gint IN_x, const gint IN_y, const gint IN_x2, const gint IN_y2, const gboolean IN_replace, GError **error); + +//static +gboolean +inkscape_selection_invert (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_group (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_ungroup (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_cut (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_copy (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_paste (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_scale (DocumentInterface *doc, const gdouble IN_grow, GError **error); + +//static +gboolean +inkscape_selection_move (DocumentInterface *doc, const gdouble IN_x, const gdouble IN_y, GError **error); + +//static +gboolean +inkscape_selection_move_to (DocumentInterface *doc, const gdouble IN_x, const gdouble IN_y, GError **error); + +//static +gboolean +inkscape_selection_move_to_layer (DocumentInterface *doc, const char * IN_layer, GError **error); + +//static +GArray * +inkscape_selection_get_center (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_selection_to_path (DocumentInterface *doc, GError **error); + +//static +char ** +inkscape_selection_combine (DocumentInterface *doc, const char * IN_type, GError **error); + +//static +gboolean +inkscape_selection_change_level (DocumentInterface *doc, const char * IN_command, GError **error); + +//static +char * +inkscape_layer_new (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_layer_set (DocumentInterface *doc, const char * IN_layer, GError **error); + +//static +char ** +inkscape_layer_get_all (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_layer_change_level (DocumentInterface *doc, const char * IN_command, GError **error); + +//static +gboolean +inkscape_layer_next (DocumentInterface *doc, GError **error); + +//static +gboolean +inkscape_layer_previous (DocumentInterface *doc, GError **error); + +G_END_DECLS + +#endif // INKSCAPE_EXTENSION_DOCUMENT_INTERFACE_H_ diff --git a/src/extension/dependency.cpp b/src/extension/dependency.cpp new file mode 100644 index 0000000..d017aef --- /dev/null +++ b/src/extension/dependency.cpp @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Ted Gould + * + * Copyright (C) 2006 Johan Engelen, johan@shouraizou.nl + * Copyright (C) 2004 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include "dependency.h" +#include "db.h" +#include "extension.h" +#include "io/resource.h" + +namespace Inkscape { +namespace Extension { + +// These strings are for XML attribute comparisons and should not be translated; +// make sure to keep in sync with enum defined in dependency.h +gchar const * Dependency::_type_str[] = { + "executable", + "file", + "extension", +}; + +// These strings are for XML attribute comparisons and should not be translated +// make sure to keep in sync with enum defined in dependency.h +gchar const * Dependency::_location_str[] = { + "path", + "extensions", + "inx", + "absolute", +}; + +/** + \brief Create a dependency using an XML definition + \param in_repr XML definition of the dependency + \param extension Reference to the extension requesting this dependency + \param default_type Default file type of the dependency (unless overridden by XML definition's "type" attribute) + + This function mostly looks for the 'location' and 'type' attributes + and turns them into the enums of the same name. This makes things + a little bit easier to use later. Also, a pointer to the core + content is pulled out -- also to make things easier. +*/ +Dependency::Dependency (Inkscape::XML::Node * in_repr, const Extension *extension, type_t default_type) + : _repr(in_repr) + , _extension(extension) + , _type(default_type) +{ + Inkscape::GC::anchor(_repr); + + if (const gchar * location = _repr->attribute("location")) { + for (int i = 0; i < LOCATION_CNT && location != nullptr; i++) { + if (!strcmp(location, _location_str[i])) { + _location = (location_t)i; + break; + } + } + } else if (const gchar * location = _repr->attribute("reldir")) { // backwards-compatibility + for (int i = 0; i < LOCATION_CNT && location != nullptr; i++) { + if (!strcmp(location, _location_str[i])) { + _location = (location_t)i; + break; + } + } + } + + const gchar * type = _repr->attribute("type"); + for (int i = 0; i < TYPE_CNT && type != nullptr; i++) { + if (!strcmp(type, _type_str[i])) { + _type = (type_t)i; + break; + } + } + + _string = _repr->firstChild()->content(); + + _description = _repr->attribute("description"); + if (_description == nullptr) + _description = _repr->attribute("_description"); + + return; +} + +/** + \brief This dependency is not longer needed + + Unreference the XML structure. +*/ +Dependency::~Dependency () +{ + Inkscape::GC::release(_repr); +} + +/** + \brief Check if the dependency passes. + \return Whether or not the dependency passes. + + This function depends largely on all of the enums. The first level + that is evaluated is the \c _type. + + If the type is \c TYPE_EXTENSION then the id for the extension is + looked up in the database. If the extension is found, and it is + not deactivated, the dependency passes. + + If the type is \c TYPE_EXECUTABLE or \c TYPE_FILE things are getting + even more interesting because now the \c _location variable is also + taken into account. First, the difference between the two is that + the file test for \c TYPE_EXECUTABLE also tests to make sure the + file is executable, besides checking that it exists. + + If the \c _location is \c LOCATION_EXTENSIONS then the \c INKSCAPE_EXTENSIONDIR + is put on the front of the string with \c build_filename. Then the + appropriate filetest is run. + + If the \c _location is \c LOCATION_ABSOLUTE then the file test is + run directly on the string. + + If the \c _location is \c LOCATION_PATH or not specified then the + path is used to find the file. Each entry in the path is stepped + through, attached to the string, and then tested. If the file is + found then a TRUE is returned. If we get all the way through the + path then a FALSE is returned, the command could not be found. +*/ +bool Dependency::check () +{ + if (_string == nullptr) { + return false; + } + + _absolute_location = ""; + + switch (_type) { + case TYPE_EXTENSION: { + Extension * myext = db.get(_string); + if (myext == nullptr) return false; + if (myext->deactivated()) return false; + break; + } + case TYPE_EXECUTABLE: + case TYPE_FILE: { + Glib::FileTest filetest = Glib::FILE_TEST_EXISTS; + + std::string location(_string); + + // get potential file extension for later usage + std::string extension; + size_t index = location.find_last_of("."); + if (index != std::string::npos) { + extension = location.substr(index); + } + + // check interpreted scripts as "file" for backwards-compatibility, even if "executable" was requested + static const std::vector interpreted = {".py", ".pl", ".rb"}; + if (!extension.empty() && + std::find(interpreted.begin(), interpreted.end(), extension) != interpreted.end()) + { + _type = TYPE_FILE; + } + +#ifndef _WIN32 + // There's no executable bit on Windows, so this is unreliable + // glib would search for "executable types" instead, which are only {".exe", ".cmd", ".bat", ".com"}, + // and would therefore miss files without extension and other script files (like .py files) + if (_type == TYPE_EXECUTABLE) { + filetest = Glib::FILE_TEST_IS_EXECUTABLE; + } +#endif + + switch (_location) { + case LOCATION_EXTENSIONS: { + // get_filename will warn if the resource isn't found, while returning an empty string. + std::string temploc = + Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::EXTENSIONS, location.c_str()); + if (!temploc.empty()) { + location = temploc; + _absolute_location = temploc; + break; + } + /* Look for deprecated locations next */ + auto deprloc = g_build_filename("inkex", "deprecated-simple", location.c_str(), NULL); + std::string tempdepr = + Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::EXTENSIONS, deprloc, false, true); + g_free(deprloc); + if (!tempdepr.empty()) { + location = tempdepr; + _absolute_location = tempdepr; + break; + } + + } /* PASS THROUGH!!! */ // TODO: the pass-through seems wrong - either it's relative or not. + case LOCATION_ABSOLUTE: { + // TODO: should we check if the directory actually is absolute and/or sanitize the filename somehow? + if (!Glib::file_test(location, filetest)) { + return false; + } + _absolute_location = location; + break; + } + case LOCATION_INX: { + std::string base_directory = _extension->get_base_directory(); + if (base_directory.empty()) { + g_warning("Dependency '%s' requests location relative to .inx file, " + "which is unknown for extension '%s'", _string, _extension->get_id()); + } + std::string absolute_location = Glib::build_filename(base_directory, location); + if (!Glib::file_test(absolute_location, filetest)) { + return false; + } + _absolute_location = absolute_location; + break; + } + /* The default case is to look in the path */ + case LOCATION_PATH: + default: { + // TODO: we can likely use g_find_program_in_path (or its glibmm equivalent) for executable types + + gchar * path = g_strdup(g_getenv("PATH")); + + if (path == nullptr) { + /* There is no `PATH' in the environment. + The default search path is the current directory */ + path = g_strdup(G_SEARCHPATH_SEPARATOR_S); + } + + gchar * orig_path = path; + + for (; path != nullptr;) { + gchar * local_path; // to have the path after detection of the separator + std::string final_name; + + local_path = path; + path = g_utf8_strchr(path, -1, G_SEARCHPATH_SEPARATOR); + /* Not sure whether this is UTF8 happy, but it would seem + like it considering that I'm searching (and finding) + the ':' character */ + if (path != nullptr) { + path[0] = '\0'; + path++; + } + + if (*local_path == '\0') { + final_name = _string; + } else { + final_name = Glib::build_filename(local_path, _string); + } + + if (Glib::file_test(final_name, filetest)) { + g_free(orig_path); + _absolute_location = final_name; + return true; + } + +#ifdef _WIN32 + // Unfortunately file extensions tend to be different on Windows and we can't know + // which one it is, so try all extensions glib assumes to be executable. + // As we can only guess here, return the version without extension if either one is found, + // so that we don't accidentally override (or conflict with) some g_spawn_* magic. + if (_type == TYPE_EXECUTABLE) { + static const std::vector extensions = {".exe", ".cmd", ".bat", ".com"}; + if (extension.empty() || + std::find(extensions.begin(), extensions.end(), extension) == extensions.end()) + { + for (auto extension : extensions) { + if (Glib::file_test(final_name + extension, filetest)) { + g_free(orig_path); + _absolute_location = final_name; + return true; + } + } + } + } +#endif + } + + g_free(orig_path); + return false; /* Reverse logic in this one */ + } + } /* switch _location */ + break; + } /* TYPE_FILE, TYPE_EXECUTABLE */ + default: + return false; + } /* switch _type */ + + return true; +} + +/** + \brief Accessor to the name attribute. + \return A string containing the name of the dependency. + + Returns the name of the dependency as found in the configuration file. + +*/ +const gchar* Dependency::get_name() +{ + return _string; +} + +/** + \brief Path of this dependency + \return Absolute path to the dependency file + (or an empty string if dependency was not found or is of TYPE_EXTENSION) + + Returns the verified absolute path of the dependency file. + This value is only available after checking the Dependency by calling Dependency::check(). +*/ +std::string Dependency::get_path() +{ + if (_type == TYPE_EXTENSION) { + g_warning("Requested absolute path of dependency '%s' which is of 'extension' type.", _string); + return ""; + } + if (_absolute_location == UNCHECKED) { + g_warning("Requested absolute path of dependency '%s' which is unchecked.", _string); + return ""; + } + + return _absolute_location; +} + +/** + \brief Print out a dependency to a string. +*/ +Glib::ustring Dependency::info_string() +{ + Glib::ustring str = Glib::ustring::compose("%1:\n\t%2: %3\n\t%4: %5\n\t%6: %7", + _("Dependency"), + _("type"), _(_type_str[_type]), + _("location"), _(_location_str[_location]), + _("string"), _string); + + if (_description) { + str += Glib::ustring::compose("\n\t%1: %2\n", _(" description: "), _(_description)); + } + + return str; +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/dependency.h b/src/extension/dependency.h new file mode 100644 index 0000000..e2c2791 --- /dev/null +++ b/src/extension/dependency.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_DEPENDENCY_H__ +#define INKSCAPE_EXTENSION_DEPENDENCY_H__ + +#include +#include "xml/repr.h" + +namespace Inkscape { +namespace Extension { + +class Extension; + +/** \brief A class to represent a dependency for an extension. There + are different things that can be done in a dependency, and + this class takes care of all of them. */ +class Dependency { + +public: + /** \brief All the possible types of dependencies. */ + enum type_t { + TYPE_EXECUTABLE, /**< Look for an executable */ + TYPE_FILE, /**< Look to make sure a file exists */ + TYPE_EXTENSION, /**< Make sure a specific extension is loaded and functional */ + TYPE_CNT /**< Number of types */ + }; + + /** \brief All of the possible locations to look for the dependency. */ + enum location_t { + LOCATION_PATH, /**< Look in the PATH for this dependency - historically this is the default + (it's a bit odd for interpreted script files but makes sense for other executables) */ + LOCATION_EXTENSIONS, /**< Look in the extensions directory + (note: this can be in both, user and system locations!) */ + LOCATION_INX, /**< Look relative to the inx file's location */ + LOCATION_ABSOLUTE, /**< This dependency is already defined in absolute terms */ + LOCATION_CNT /**< Number of locations to look */ + }; + +private: + static constexpr const char *UNCHECKED = "---unchecked---"; + + /** \brief The XML representation of the dependency. */ + Inkscape::XML::Node * _repr; + /** \brief The string that is in the XML tags pulled out. */ + const gchar * _string = nullptr; + /** \brief The description of the dependency for the users. */ + const gchar * _description = nullptr; + /** \brief The absolute path to the dependency file determined while checking this dependency. */ + std::string _absolute_location = UNCHECKED; + + /** \brief Storing the type of this particular dependency. */ + type_t _type = TYPE_FILE; + /** \brief The location to look for this particular dependency. */ + location_t _location = LOCATION_PATH; + + /** \brief Strings to represent the different enum values in + \c type_t in the XML */ + static gchar const * _type_str[TYPE_CNT]; + /** \brief Strings to represent the different enum values in + \c location_t in the XML */ + static gchar const * _location_str[LOCATION_CNT]; + + /** \brief Reference to the extension requesting this dependency. */ + const Extension *_extension; + +public: + Dependency (Inkscape::XML::Node *in_repr, const Extension *extension, type_t type=TYPE_FILE); + virtual ~Dependency (); + bool check(); + const gchar* get_name(); + std::string get_path(); + + Glib::ustring info_string(); +}; /* class Dependency */ + + +} } /* namespace Extension, Inkscape */ + +#endif /* INKSCAPE_EXTENSION_DEPENDENCY_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/effect.cpp b/src/extension/effect.cpp new file mode 100644 index 0000000..1a39d4b --- /dev/null +++ b/src/extension/effect.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * Abhishek Sharma + * + * Copyright (C) 2002-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "effect.h" + +#include "execution-env.h" +#include "inkscape.h" +#include "timer.h" + +#include "helper/action.h" +#include "implementation/implementation.h" +#include "prefdialog/prefdialog.h" +#include "ui/view/view.h" + + + +/* Inkscape::Extension::Effect */ + +namespace Inkscape { +namespace Extension { + +Effect * Effect::_last_effect = nullptr; +Inkscape::XML::Node * Effect::_effects_list = nullptr; +Inkscape::XML::Node * Effect::_filters_list = nullptr; + +#define EFFECTS_LIST "effects-list" +#define FILTERS_LIST "filters-list" + +Effect::Effect (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) + , _id_noprefs(Glib::ustring(get_id()) + ".noprefs") + , _name_noprefs(Glib::ustring(_(get_name())) + _(" (No preferences)")) + , _verb(get_id(), get_name(), nullptr, nullptr, this, true) + , _verb_nopref(_id_noprefs.c_str(), _name_noprefs.c_str(), nullptr, nullptr, this, false) + , _menu_node(nullptr), _workingDialog(true) + , _prefDialog(nullptr) +{ + Inkscape::XML::Node * local_effects_menu = nullptr; + + // This is a weird hack + if (!strcmp(this->get_id(), "org.inkscape.filter.dropshadow")) + return; + + bool hidden = false; + + no_doc = false; + no_live_preview = false; + + if (repr != nullptr) { + + for (Inkscape::XML::Node *child = repr->firstChild(); child != nullptr; child = child->next()) { + if (!strcmp(child->name(), INKSCAPE_EXTENSION_NS "effect")) { + if (child->attribute("needs-document") && !strcmp(child->attribute("needs-document"), "false")) { + no_doc = true; + } + if (child->attribute("needs-live-preview") && !strcmp(child->attribute("needs-live-preview"), "false")) { + no_live_preview = true; + } + if (child->attribute("implements-custom-gui") && !strcmp(child->attribute("implements-custom-gui"), "true")) { + _workingDialog = false; + } + for (Inkscape::XML::Node *effect_child = child->firstChild(); effect_child != nullptr; effect_child = effect_child->next()) { + if (!strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "effects-menu")) { + // printf("Found local effects menu in %s\n", this->get_name()); + local_effects_menu = effect_child->firstChild(); + if (effect_child->attribute("hidden") && !strcmp(effect_child->attribute("hidden"), "true")) { + hidden = true; + } + } + if (!strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "menu-name") || + !strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "_menu-name")) { + // printf("Found local effects menu in %s\n", this->get_name()); + _verb.set_name(effect_child->firstChild()->content()); + } + if (!strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "menu-tip") || + !strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "_menu-tip")) { + // printf("Found local effects menu in %s\n", this->get_name()); + _verb.set_tip(effect_child->firstChild()->content()); + } + } // children of "effect" + break; // there can only be one effect + } // find "effect" + } // children of "inkscape-extension" + } // if we have an XML file + + // \TODO this gets called from the Inkscape::Application constructor, where it initializes the menus. + // But in the constructor, our object isn't quite there yet! + if (Inkscape::Application::exists() && INKSCAPE.use_gui()) { + if (_effects_list == nullptr) + _effects_list = find_menu(INKSCAPE.get_menus(), EFFECTS_LIST); + if (_filters_list == nullptr) + _filters_list = find_menu(INKSCAPE.get_menus(), FILTERS_LIST); + } + + if ((_effects_list != nullptr || _filters_list != nullptr)) { + Inkscape::XML::Document *xml_doc; + xml_doc = _effects_list->document(); + _menu_node = xml_doc->createElement("verb"); + _menu_node->setAttribute("verb-id", this->get_id()); + + if (!hidden) { + if (_filters_list && + local_effects_menu && + local_effects_menu->attribute("name") && + !strcmp(local_effects_menu->attribute("name"), ("Filters"))) { + merge_menu(_filters_list->parent(), _filters_list, local_effects_menu->firstChild(), _menu_node); + } else if (_effects_list) { + merge_menu(_effects_list->parent(), _effects_list, local_effects_menu, _menu_node); + } + } + } + + return; +} + +void +Effect::merge_menu (Inkscape::XML::Node * base, + Inkscape::XML::Node * start, + Inkscape::XML::Node * pattern, + Inkscape::XML::Node * merge) { + Glib::ustring mergename; + Inkscape::XML::Node * tomerge = nullptr; + Inkscape::XML::Node * submenu = nullptr; + + if (pattern == nullptr) { + // Merge the verb name + tomerge = merge; + mergename = get_translation(get_name()); + } else { + gchar const *menuname = pattern->attribute("name"); + if (menuname == nullptr) menuname = pattern->attribute("_name"); + if (menuname == nullptr) return; + + Inkscape::XML::Document *xml_doc; + xml_doc = base->document(); + tomerge = xml_doc->createElement("submenu"); + if (_translation_enabled) { + mergename = get_translation(menuname); + } else { + // Even if the extension author requested the extension not to be translated, + // it still seems desirable to be able to put the extension into the existing (translated) submenus. + mergename = _(menuname); + } + tomerge->setAttribute("name", mergename); + } + + int position = -1; + + if (start != nullptr) { + Inkscape::XML::Node * menupass; + for (menupass = start; menupass != nullptr && strcmp(menupass->name(), "separator"); menupass = menupass->next()) { + gchar const * compare_char = nullptr; + if (!strcmp(menupass->name(), "verb")) { + gchar const * verbid = menupass->attribute("verb-id"); + Inkscape::Verb * verb = Inkscape::Verb::getbyid(verbid); + if (verb == nullptr) { + g_warning("Unable to find verb '%s' which is referred to in the menus.", verbid); + continue; + } + compare_char = verb->get_name(); + } else if (!strcmp(menupass->name(), "submenu")) { + compare_char = menupass->attribute("name"); + if (compare_char == nullptr) + compare_char = menupass->attribute("_name"); + } + + position = menupass->position() + 1; + + /* This will cause us to skip tags we don't understand */ + if (compare_char == nullptr) { + continue; + } + + Glib::ustring compare(_(compare_char)); + + if (mergename == compare) { + Inkscape::GC::release(tomerge); + tomerge = nullptr; + submenu = menupass; + break; + } + + if (mergename < compare) { + position = menupass->position(); + break; + } + } // for menu items + } // start != NULL + + if (tomerge != nullptr) { + if (position != -1) { + base->addChildAtPos(tomerge, position); + } else { + base->appendChild(tomerge); + } + Inkscape::GC::release(tomerge); + } + + if (pattern != nullptr) { + if (submenu == nullptr) + submenu = tomerge; + merge_menu(submenu, submenu->firstChild(), pattern->firstChild(), merge); + } + + return; +} + +Effect::~Effect () +{ + if (get_last_effect() == this) + set_last_effect(nullptr); + if (_menu_node) + Inkscape::GC::release(_menu_node); + return; +} + +bool +Effect::check () +{ + if (!Extension::check()) { + _verb.sensitive(nullptr, false); + _verb.set_tip(Extension::getErrorReason().c_str()); // TODO: insensitive menuitems don't show a tooltip + return false; + } + return true; +} + +bool +Effect::prefs (Inkscape::UI::View::View * doc) +{ + if (_prefDialog != nullptr) { + _prefDialog->raise(); + return true; + } + + if (widget_visible_count() == 0) { + effect(doc); + return true; + } + + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return false; + + Glib::ustring name = get_translation(this->get_name()); + _prefDialog = new PrefDialog(name, nullptr, this); + _prefDialog->show(); + + return true; +} + +/** + \brief The function that 'does' the effect itself + \param doc The Inkscape::UI::View::View to do the effect on + + This function first insures that the extension is loaded, and if not, + loads it. It then calls the implementation to do the actual work. It + also resets the last effect pointer to be this effect. Finally, it + executes a \c SPDocumentUndo::done to commit the changes to the undo + stack. +*/ +void +Effect::effect (Inkscape::UI::View::View * doc) +{ + //printf("Execute effect\n"); + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return; + ExecutionEnv executionEnv(this, doc, nullptr, _workingDialog, true); + execution_env = &executionEnv; + timer->lock(); + executionEnv.run(); + if (executionEnv.wait()) { + executionEnv.commit(); + } else { + executionEnv.cancel(); + } + timer->unlock(); + + return; +} + +/** \brief Sets which effect was called last + \param in_effect The effect that has been called + + This function sets the static variable \c _last_effect and it + ensures that the last effect verb is sensitive. + + If the \c in_effect variable is \c NULL then the last effect + verb is made insesitive. +*/ +void +Effect::set_last_effect (Effect * in_effect) +{ + if (in_effect == nullptr) { + Inkscape::Verb::get(SP_VERB_EFFECT_LAST)->sensitive(nullptr, false); + Inkscape::Verb::get(SP_VERB_EFFECT_LAST_PREF)->sensitive(nullptr, false); + } else if (_last_effect == nullptr) { + Inkscape::Verb::get(SP_VERB_EFFECT_LAST)->sensitive(nullptr, true); + Inkscape::Verb::get(SP_VERB_EFFECT_LAST_PREF)->sensitive(nullptr, true); + } + + _last_effect = in_effect; + return; +} + +Inkscape::XML::Node * +Effect::find_menu (Inkscape::XML::Node * menustruct, const gchar *name) +{ + if (menustruct == nullptr) return nullptr; + for (Inkscape::XML::Node * child = menustruct; + child != nullptr; + child = child->next()) { + if (!strcmp(child->name(), name)) { + return child; + } + Inkscape::XML::Node * firstchild = child->firstChild(); + if (firstchild != nullptr) { + Inkscape::XML::Node *found = find_menu (firstchild, name); + if (found) + return found; + } + } + return nullptr; +} + + +Gtk::VBox * +Effect::get_info_widget() +{ + return Extension::get_info_widget(); +} + +PrefDialog * +Effect::get_pref_dialog () +{ + return _prefDialog; +} + +void +Effect::set_pref_dialog (PrefDialog * prefdialog) +{ + _prefDialog = prefdialog; + return; +} + +SPAction * +Effect::EffectVerb::make_action (Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform, static_cast(this)); +} + +/** \brief Decode the verb code and take appropriate action */ +void +Effect::EffectVerb::perform( SPAction *action, void * data ) +{ + g_return_if_fail(ensure_desktop_valid(action)); + Inkscape::UI::View::View * current_view = sp_action_get_view(action); + + Effect::EffectVerb * ev = reinterpret_cast(data); + Effect * effect = ev->_effect; + + if (effect == nullptr) return; + + if (ev->_showPrefs) { + effect->prefs(current_view); + } else { + effect->effect(current_view); + } + + return; +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/effect.h b/src/extension/effect.h new file mode 100644 index 0000000..39307e4 --- /dev/null +++ b/src/extension/effect.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_EXTENSION_EFFECT_H__ +#define INKSCAPE_EXTENSION_EFFECT_H__ + +#include +#include "verbs.h" +#include "extension.h" + +namespace Gtk { + class VBox; +} + +class SPDocument; + +namespace Inkscape { + + +namespace Extension { +class PrefDialog; + +/** \brief Effects are extensions that take a document and do something + to it in place. This class adds the extra functions required + to make extensions effects. +*/ +class Effect : public Extension { + /** \brief This is the last effect that was used. This is used in + a menu item to rapidly recall the same effect. */ + static Effect * _last_effect; + /** \brief The location of the Extensions and Filters menus on the menu structure + XML file. This is saved so it only has to be discovered + once. */ + static Inkscape::XML::Node * _effects_list; + static Inkscape::XML::Node * _filters_list; + Inkscape::XML::Node *find_menu (Inkscape::XML::Node * menustruct, const gchar *name); + void merge_menu (Inkscape::XML::Node * base, Inkscape::XML::Node * start, Inkscape::XML::Node * pattern, Inkscape::XML::Node * merge); + + /** \brief This is the verb type that is used for all effect's verbs. + It provides convenience functions and maintains a pointer + back to the effect that created it. */ + class EffectVerb : public Inkscape::Verb { + private: + static void perform (SPAction * action, void * mydata); + + /** \brief The effect that this verb represents. */ + Effect * _effect; + /** \brief Whether or not to show preferences on display */ + bool _showPrefs; + /** \brief Name with ellipses if that makes sense */ + gchar * _elip_name; + protected: + SPAction * make_action (Inkscape::ActionContext const & context) override; + public: + /** \brief Use the Verb initializer with the same parameters. */ + EffectVerb(gchar const * id, + gchar const * name, + gchar const * tip, + gchar const * image, + Effect * effect, + bool showPrefs) : + Verb(id, _(name), tip ? _(tip) : nullptr, image, _("Extensions")), + _effect(effect), + _showPrefs(showPrefs), + _elip_name(nullptr) { + /* No clue why, but this is required */ + this->set_default_sensitive(true); + if (_showPrefs && effect != nullptr && effect->widget_visible_count() != 0) { + _elip_name = g_strdup_printf("%s...", _(name)); + set_name(_elip_name); + } + } + + /** \brief Destructor */ + ~EffectVerb() override { + if (_elip_name != nullptr) { + g_free(_elip_name); + } + } + }; + + /** \brief ID used for the verb without preferences */ + Glib::ustring _id_noprefs; + /** \brief Name used for the verb without preferences */ + Glib::ustring _name_noprefs; + + /** \brief The verb representing this effect. */ + EffectVerb _verb; + /** \brief The verb representing this effect. Without preferences. */ + EffectVerb _verb_nopref; + /** \brief Menu node created for this effect */ + Inkscape::XML::Node * _menu_node; + /** \brief Whether a working dialog should be shown */ + bool _workingDialog; + + /** \brief The preference dialog if it is shown */ + PrefDialog * _prefDialog; +public: + Effect(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Effect () override; + + bool check() override; + + bool prefs (Inkscape::UI::View::View * doc); + void effect (Inkscape::UI::View::View * doc); + /** \brief Accessor function for a pointer to the verb */ + Inkscape::Verb * get_verb () { return &_verb; }; + + /** \brief Static function to get the last effect used */ + static Effect * get_last_effect () { return _last_effect; }; + static void set_last_effect (Effect * in_effect); + + static void place_menus (); + void place_menu (Inkscape::XML::Node * menus); + + Gtk::VBox * get_info_widget(); + + bool no_doc; // if true, the effect does not process SVG document at all, so no need to save, read, and watch for errors + bool no_live_preview; // if true, the effect does not need "live preview" checkbox in its dialog + + PrefDialog *get_pref_dialog (); + void set_pref_dialog (PrefDialog * prefdialog); +private: + static gchar * remove_ (gchar * instr); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_EFFECT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/error-file.cpp b/src/extension/error-file.cpp new file mode 100644 index 0000000..ad65457 --- /dev/null +++ b/src/extension/error-file.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/extensions.h" + +#include +#include "inkscape.h" +#include "preferences.h" +#include "extension/extension.h" +#include "io/resource.h" + +#include "error-file.h" + +/** The name and group of the preference to say whether the error + dialog should be shown on startup. */ +#define PREFERENCE_ID "/dialogs/extension-error/show-on-startup" + +namespace Inkscape { +namespace Extension { + +/** \brief An initializer which builds the dialog + + Really a simple function. Basically the message dialog itself gets + built with the first initializer. The next step is to add in the + message, and attach the filename for the error file. After that + the checkbox is built, and has the call back attached to it. Also, + it is set based on the preferences setting for show on startup (really, + it should always be checked if you can see the dialog, but it is + probably good to check anyway). +*/ +ErrorFileNotice::ErrorFileNotice () : + Gtk::MessageDialog( + "", /* message */ + false, /* use markup */ + Gtk::MESSAGE_WARNING, /* dialog type */ + Gtk::BUTTONS_OK, /* buttons */ + true /* modal */ + ) + +{ + // \FIXME change this + /* This is some filler text, needs to change before release */ + Glib::ustring dialog_text(_("One or more extensions failed to load\n\nThe failed extensions have been skipped. Inkscape will continue to run normally but those extensions will be unavailable. For details to troubleshoot this problem, please refer to the error log located at: ")); + gchar * ext_error_file = Inkscape::IO::Resource::log_path(EXTENSION_ERROR_LOG_FILENAME); + dialog_text += ext_error_file; + g_free(ext_error_file); + set_message(dialog_text, true); + + auto vbox = get_content_area(); + + /* This is some filler text, needs to change before release */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + checkbutton = Gtk::manage(new Gtk::CheckButton(_("Show dialog on startup"))); + vbox->pack_start(*checkbutton, true, false, 5); + checkbutton->show(); + checkbutton->set_active(prefs->getBool(PREFERENCE_ID, true)); + + checkbutton->signal_toggled().connect(sigc::mem_fun(this, &ErrorFileNotice::checkbox_toggle)); + + set_resizable(true); + + Inkscape::UI::Dialogs::ExtensionsPanel* extens = new Inkscape::UI::Dialogs::ExtensionsPanel(); + extens->set_full(false); + vbox->pack_start( *extens, true, true ); + extens->show(); + + return; +} + +/** \brief Sets the preferences based on the checkbox value */ +void +ErrorFileNotice::checkbox_toggle () +{ + // std::cout << "Toggle value" << std::endl; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(PREFERENCE_ID, checkbutton->get_active()); +} + +/** \brief Shows the dialog + + This function only shows the dialog if the preferences say that the + user wants to see the dialog, otherwise it just exits. +*/ +int +ErrorFileNotice::run () +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool(PREFERENCE_ID, true)) + return 0; + return Gtk::Dialog::run(); +} + +}; }; /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/error-file.h b/src/extension/error-file.h new file mode 100644 index 0000000..442bc8f --- /dev/null +++ b/src/extension/error-file.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_ERROR_FILE_H__ +#define INKSCAPE_EXTENSION_ERROR_FILE_H__ + +#include +#include + +namespace Inkscape { +namespace Extension { + +/** \brief A warning dialog to say that some extensions failed to load, + will not run if the preference controlling running is turned + off. */ +class ErrorFileNotice : public Gtk::MessageDialog { + /** The checkbutton, this is so we can figure out when it gets checked */ + Gtk::CheckButton * checkbutton; + + void checkbox_toggle(); +public: + ErrorFileNotice (); + int run (); +}; + +}; }; /* namespace Inkscape, Extension */ + +#endif /* INKSCAPE_EXTENSION_ERROR_FILE_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/execution-env.cpp b/src/extension/execution-env.cpp new file mode 100644 index 0000000..9293e56 --- /dev/null +++ b/src/extension/execution-env.cpp @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * Abhishek Sharma + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "execution-env.h" +#include "prefdialog/prefdialog.h" +#include "implementation/implementation.h" + +#include "selection.h" +#include "effect.h" +#include "document.h" +#include "desktop.h" +#include "inkscape.h" +#include "document-undo.h" +#include "desktop.h" +#include "object/sp-namedview.h" + +#include "display/sp-canvas.h" + +namespace Inkscape { +namespace Extension { + +/** \brief Create an execution environment that will allow the effect + to execute independently. + \param effect The effect that we should execute + \param doc The Document to execute on + \param docCache The cache created for that document + \param show_working Show the working dialog + \param show_error Show the error dialog (not working) + + Grabs the selection of the current document so that it can get + restored. Will generate a document cache if one isn't provided. +*/ +ExecutionEnv::ExecutionEnv (Effect * effect, Inkscape::UI::View::View * doc, Implementation::ImplementationDocumentCache * docCache, bool show_working, bool show_errors) : + _state(ExecutionEnv::INIT), + _visibleDialog(nullptr), + _mainloop(nullptr), + _doc(doc), + _docCache(docCache), + _effect(effect), + _show_working(show_working), + _show_errors(show_errors) +{ + genDocCache(); + + return; +} + +/** \brief Destroy an execution environment + + Destroys the dialog if created and the document cache. +*/ +ExecutionEnv::~ExecutionEnv () { + if (_visibleDialog != nullptr) { + _visibleDialog->hide(); + delete _visibleDialog; + _visibleDialog = nullptr; + } + killDocCache(); + return; +} + +/** \brief Generate a document cache if needed + + If there isn't one we create a new one from the implementation + from the effect's implementation. +*/ +void +ExecutionEnv::genDocCache () { + if (_docCache == nullptr) { + // printf("Gen Doc Cache\n"); + _docCache = _effect->get_imp()->newDocCache(_effect, _doc); + } + return; +} + +/** \brief Destroy a document cache + + Just delete it. +*/ +void +ExecutionEnv::killDocCache () { + if (_docCache != nullptr) { + // printf("Killed Doc Cache\n"); + delete _docCache; + _docCache = nullptr; + } + return; +} + +/** \brief Create the working dialog + + Builds the dialog with a message saying that the effect is working. + And make sure to connect to the cancel. +*/ +void +ExecutionEnv::createWorkingDialog () { + if (_visibleDialog != nullptr) { + _visibleDialog->hide(); + delete _visibleDialog; + _visibleDialog = nullptr; + } + + SPDesktop *desktop = (SPDesktop *)_doc; + GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(desktop->canvas)); + if (!toplevel || !gtk_widget_is_toplevel (toplevel)) + return; + Gtk::Window *window = Glib::wrap(GTK_WINDOW(toplevel), false); + + gchar * dlgmessage = g_strdup_printf(_("'%s' working, please wait..."), _(_effect->get_name())); + _visibleDialog = new Gtk::MessageDialog(*window, + dlgmessage, + false, // use markup + Gtk::MESSAGE_INFO, + Gtk::BUTTONS_CANCEL, + true); // modal + _visibleDialog->signal_response().connect(sigc::mem_fun(this, &ExecutionEnv::workingCanceled)); + g_free(dlgmessage); + + Gtk::Dialog *dlg = _effect->get_pref_dialog(); + if (dlg) { + _visibleDialog->set_transient_for(*dlg); + } else { + // ToDo: Do we need to make the window transient for the main window here? + // Currently imossible to test because of GUI freezing during save, + // see https://bugs.launchpad.net/inkscape/+bug/967416 + } + _visibleDialog->show_now(); + + return; +} + +void +ExecutionEnv::workingCanceled( const int /*resp*/) { + cancel(); + undo(); + return; +} + +void +ExecutionEnv::cancel () { + SPDesktop *desktop = (SPDesktop *)_doc; + desktop->clearWaitingCursor(); + _effect->get_imp()->cancelProcessing(); + return; +} + +void +ExecutionEnv::undo () { + DocumentUndo::cancel(_doc->doc()); + return; +} + +void +ExecutionEnv::commit () { + DocumentUndo::done(_doc->doc(), SP_VERB_NONE, _(_effect->get_name())); + Effect::set_last_effect(_effect); + _effect->get_imp()->commitDocument(); + killDocCache(); + return; +} + +void +ExecutionEnv::reselect () { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + if (selection) { + selection->restoreBackup(); + } + } + return; +} + +void +ExecutionEnv::run () { + _state = ExecutionEnv::RUNNING; + if (_show_working) { + createWorkingDialog(); + } + SPDesktop *desktop = (SPDesktop *)_doc; + Inkscape::Selection *selection = desktop->getSelection(); + selection->setBackup(); + desktop->setWaitingCursor(); + _effect->get_imp()->effect(_effect, _doc, _docCache); + desktop->clearWaitingCursor(); + _state = ExecutionEnv::COMPLETE; + selection->restoreBackup(); + // _runComplete.signal(); + return; +} + +void +ExecutionEnv::runComplete () { + _mainloop->quit(); +} + +bool +ExecutionEnv::wait () { + if (_state != ExecutionEnv::COMPLETE) { + if (_mainloop) { + _mainloop = Glib::MainLoop::create(false); + } + + sigc::connection conn = _runComplete.connect(sigc::mem_fun(this, &ExecutionEnv::runComplete)); + _mainloop->run(); + + conn.disconnect(); + } + + return true; +} + + + +} } /* namespace Inkscape, Extension */ + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/execution-env.h b/src/extension/execution-env.h new file mode 100644 index 0000000..15d2cce --- /dev/null +++ b/src/extension/execution-env.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_EXECUTION_ENV_H__ +#define INKSCAPE_EXTENSION_EXECUTION_ENV_H__ + +#include +#include + +#include + +namespace Inkscape { + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI + +namespace Extension { + +class Effect; + +namespace Implementation +{ +class ImplementationDocumentCache; +} + +class ExecutionEnv { +private: + enum state_t { + INIT, //< The context has been initialized + COMPLETE, //< We've completed atleast once + RUNNING //< The effect is currently running + }; + /** \brief What state the execution engine is in. */ + state_t _state; + + /** \brief If there is a working dialog it'll be referenced + right here. */ + Gtk::Dialog * _visibleDialog; + /** \brief Signal that the run is complete. */ + sigc::signal _runComplete; + /** \brief In some cases we need a mainLoop, when we do, this is + a pointer to it. */ + Glib::RefPtr _mainloop; + /** \brief The document that we're working on. */ + Inkscape::UI::View::View * _doc; + /** \brief A document cache if we were passed one. */ + Implementation::ImplementationDocumentCache * _docCache; + + /** \brief The effect that we're executing in this context. */ + Effect * _effect; + + /** \brief Show the working dialog when the effect is executing. */ + bool _show_working; + /** \brief Display errors if they occur. */ + bool _show_errors; +public: + + /** \brief Create a new context for execution of an effect + \param effect The effect to execute + \param doc The document to execute the effect on + \param docCache The implementation cache of the document. May be + NULL in which case it'll be created by the execution + environment. + \prarm show_working Show a small dialog signaling the effect + is working. Allows for user canceling. + \param show_errors If the effect has an error, show it or not. + */ + ExecutionEnv (Effect * effect, + Inkscape::UI::View::View * doc, + Implementation::ImplementationDocumentCache * docCache = nullptr, + bool show_working = true, + bool show_errors = true); + virtual ~ExecutionEnv (); + + /** \brief Starts the execution of the effect + \return Returns whether the effect was executed to completion */ + void run (); + /** \brief Cancel the execution of the effect */ + void cancel (); + /** \brief Commit the changes to the document */ + void commit (); + /** \brief Undoes what the effect completed. */ + void undo (); + /** \brief Wait for the effect to complete if it hasn't. */ + bool wait (); + void reselect (); + + /** \brief Return reference to working dialog (if any) */ + Gtk::Dialog *get_working_dialog () { return _visibleDialog; }; + +private: + void runComplete (); + void createWorkingDialog (); + void workingCanceled (const int resp); + void genDocCache (); + void killDocCache (); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_EXECUTION_ENV_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/extension.cpp b/src/extension/extension.cpp new file mode 100644 index 0000000..6d182af --- /dev/null +++ b/src/extension/extension.cpp @@ -0,0 +1,1014 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * + * Inkscape::Extension::Extension: + * the ability to have features that are more modular so that they + * can be added and removed easily. This is the basis for defining + * those actions. + */ + +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension.h" +#include "implementation/implementation.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "db.h" +#include "dependency.h" +#include "inkscape.h" +#include "timer.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "prefdialog/parameter.h" +#include "prefdialog/widget.h" + +#include "xml/repr.h" + + +namespace Inkscape { +namespace Extension { + +/* Inkscape::Extension::Extension */ + +FILE *Extension::error_file = nullptr; + +/** + \return none + \brief Constructs an Extension from a Inkscape::XML::Node + \param in_repr The repr that should be used to build it + \param base_directory Base directory of extensions that were loaded from a file (.inx file's location) + + This function is the basis of building an extension for Inkscape. It + currently extracts the fields from the Repr that are used in the + extension. The Repr will likely include other children that are + not related to the module directly. If the Repr does not include + a name and an ID the module will be left in an errored state. +*/ +Extension::Extension(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : _gui(true) + , execution_env(nullptr) +{ + g_return_if_fail(in_repr); // should be ensured in system.cpp + repr = in_repr; + Inkscape::GC::anchor(repr); + + if (in_imp == nullptr) { + imp = new Implementation::Implementation(); + } else { + imp = in_imp; + } + + if (base_directory) { + _base_directory = *base_directory; + } + + // get name of the translation catalog ("gettext textdomain") that the extension wants to use for translations + // and lookup the locale directory for it + const char *translationdomain = repr->attribute("translationdomain"); + if (translationdomain) { + _translationdomain = translationdomain; + } else { + _translationdomain = "inkscape"; // default to the Inkscape catalog + } + if (!strcmp(_translationdomain, "none")) { + // special keyword "none" means the extension author does not want translation of extension strings + _translation_enabled = false; + _translationdomain = nullptr; + } else if (!strcmp(_translationdomain, "inkscape")) { + // this is our default domain; we know the location already (also respects INKSCAPE_LOCALEDIR) + _gettext_catalog_dir = bindtextdomain("inkscape", nullptr); + } else { + lookup_translation_catalog(); + } + + // Read XML tree and parse extension + Inkscape::XML::Node *child_repr = repr->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + chname++; + } + + if (!strcmp(chname, "id")) { + const char *id = child_repr->firstChild() ? child_repr->firstChild()->content() : nullptr; + if (id) { + _id = g_strdup(id); + } else { + throw extension_no_id(); + } + } else if (!strcmp(chname, "name")) { + const char *name = child_repr->firstChild() ? child_repr->firstChild()->content() : nullptr; + if (name) { + _name = g_strdup(name); + } else { + throw extension_no_name(); + } + } else if (InxWidget::is_valid_widget_name(chname)) { + InxWidget *widget = InxWidget::make(child_repr, this); + if (widget) { + _widgets.push_back(widget); + } + } else if (!strcmp(chname, "dependency")) { + _deps.push_back(new Dependency(child_repr, this)); + } else if (!strcmp(chname, "script")) { // TODO: should these be parsed in their respective Implementation? + for (Inkscape::XML::Node *child = child_repr->firstChild(); child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::ELEMENT_NODE) { // skip non-element nodes (see LP #1372200) + const char *interpreted = child->attribute("interpreter"); + Dependency::type_t type = interpreted ? Dependency::TYPE_FILE : Dependency::TYPE_EXECUTABLE; + _deps.push_back(new Dependency(child, this, type)); + break; + } + } + } else if (!strcmp(chname, "xslt")) { // TODO: should these be parsed in their respective Implementation? + for (Inkscape::XML::Node *child = child_repr->firstChild(); child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::ELEMENT_NODE) { // skip non-element nodes (see LP #1372200) + _deps.push_back(new Dependency(child, this, Dependency::TYPE_FILE)); + break; + } + } + } else { + // We could do some sanity checking here. + // However, we don't really know which additional elements Extension subclasses might need... + } + + child_repr = child_repr->next(); + } + + // all extensions need an ID and a name + if (!_id) { + throw extension_no_id(); + } + if (!_name) { + throw extension_no_name(); + } + + // filter out extensions that are not compatible with the current platform +#ifndef _WIN32 + if (strstr(_id, "win32")) { + throw extension_not_compatible(); + } +#endif + + // finally register the extension if all checks passed + db.register_ext (this); +} + +/** + \return none + \brief Destroys the Extension + + This function frees all of the strings that could be attached + to the extension and also unreferences the repr. This is better + than freeing it because it may (I wouldn't know why) be referenced + in another place. +*/ +Extension::~Extension () +{ + set_state(STATE_UNLOADED); + + db.unregister_ext(this); + + Inkscape::GC::release(repr); + + g_free(_id); + g_free(_name); + + delete timer; + timer = nullptr; + + for (auto widget : _widgets) { + delete widget; + } + + for (auto & _dep : _deps) { + delete _dep; + } + _deps.clear(); +} + +/** + \return none + \brief A function to set whether the extension should be loaded + or unloaded + \param in_state Which state should the extension be in? + + It checks to see if this is a state change or not. If we're changing + states it will call the appropriate function in the implementation, + load or unload. Currently, there is no error checking in this + function. There should be. +*/ +void +Extension::set_state (state_t in_state) +{ + if (_state == STATE_DEACTIVATED) return; + if (in_state != _state) { + /** \todo Need some more error checking here! */ + switch (in_state) { + case STATE_LOADED: + if (imp->load(this)) + _state = STATE_LOADED; + + if (timer != nullptr) { + delete timer; + } + timer = new ExpirationTimer(this); + + break; + case STATE_UNLOADED: + // std::cout << "Unloading: " << name << std::endl; + imp->unload(this); + _state = STATE_UNLOADED; + + if (timer != nullptr) { + delete timer; + timer = nullptr; + } + break; + case STATE_DEACTIVATED: + _state = STATE_DEACTIVATED; + + if (timer != nullptr) { + delete timer; + timer = nullptr; + } + break; + default: + break; + } + } + + return; +} + +/** + \return The state the extension is in + \brief A getter for the state variable. +*/ +Extension::state_t +Extension::get_state () +{ + return _state; +} + +/** + \return Whether the extension is loaded or not + \brief A quick function to test the state of the extension +*/ +bool +Extension::loaded () +{ + return get_state() == STATE_LOADED; +} + +/** + \return A boolean saying whether the extension passed the checks + \brief A function to check the validity of the extension + + This function chekcs to make sure that there is an id, a name, a + repr and an implementation for this extension. Then it checks all + of the dependencies to see if they pass. Finally, it asks the + implementation to do a check of itself. + + On each check, if there is a failure, it will print a message to the + error log for that failure. It is important to note that the function + keeps executing if it finds an error, to try and get as many of them + into the error log as possible. This should help people debug + installations, and figure out what they need to get for the full + functionality of Inkscape to be available. +*/ +bool +Extension::check () +{ + const char * inx_failure = _(" This is caused by an improper .inx file for this extension." + " An improper .inx file could have been caused by a faulty installation of Inkscape."); + + if (repr == nullptr) { + printFailure(Glib::ustring(_("the XML description of it got lost.")) + inx_failure); + return false; + } + if (imp == nullptr) { + printFailure(Glib::ustring(_("no implementation was defined for the extension.")) + inx_failure); + return false; + } + + bool retval = true; + for (auto _dep : _deps) { + if (_dep->check() == false) { + printFailure(Glib::ustring(_("a dependency was not met."))); + error_file_write(_dep->info_string()); + retval = false; + } + } + + if (retval) { + return imp->check(this); + } + + error_file_write(""); + return retval; +} + +/** \brief A quick function to print out a standard start of extension + errors in the log. + \param reason A string explaining why this failed + + Real simple, just put everything into \c error_file. +*/ +void +Extension::printFailure (Glib::ustring reason) +{ + _error_reason = Glib::ustring::compose(_("Extension \"%1\" failed to load because %2"), _name, reason); + error_file_write(_error_reason); +} + +/** + \return The XML tree that is used to define the extension + \brief A getter for the internal Repr, does not add a reference. +*/ +Inkscape::XML::Node * +Extension::get_repr () +{ + return repr; +} + +/** + \return The textual id of this extension + \brief Get the ID of this extension - not a copy don't delete! +*/ +gchar * +Extension::get_id () const +{ + return _id; +} + +/** + \return The textual name of this extension + \brief Get the name of this extension - not a copy don't delete! +*/ +gchar * +Extension::get_name () const +{ + return _name; +} + +/** + \return None + \brief This function diactivates the extension (which makes it + unusable, but not deleted) + + This function is used to removed an extension from functioning, but + not delete it completely. It sets the state to \c STATE_DEACTIVATED to + mark to the world that it has been deactivated. It also removes + the current implementation and replaces it with a standard one. This + makes it so that we don't have to continually check if there is an + implementation, but we are guaranteed to have a benign one. + + \warning It is important to note that there is no 'activate' function. + Running this function is irreversable. +*/ +void +Extension::deactivate () +{ + set_state(STATE_DEACTIVATED); + + /* Removing the old implementation, and making this use the default. */ + /* This should save some memory */ + delete imp; + imp = new Implementation::Implementation(); + + return; +} + +/** + \return Whether the extension has been deactivated + \brief Find out the status of the extension +*/ +bool +Extension::deactivated () +{ + return get_state() == STATE_DEACTIVATED; +} + +/** Gets the location of the dependency file as an absolute path + * + * Iterates over all dependencies of this extension and finds the one with matching name, + * then returns the absolute path to this dependency file as determined previously. + * + * TODO: This function should not be necessary, but we parse script dependencies twice: + * - Once here in the Extension::Extension() constructor + * - A second time in Script::load() in "script.cpp" when determining the script location + * Theoretically we could return the wrong path if an extension depends on two files with the same name + * in different relative locations. In practice this risk should be close to zero, though. + * + * @return Absolute path of the dependency file + */ +std::string Extension::get_dependency_location(const char *name) +{ + for (auto dep : _deps) { + if (!strcmp(name, dep->get_name())) { + return dep->get_path(); + } + } + + return ""; +} + +/** recursively searches directory for a file named filename; returns true if found */ +static bool _find_filename_recursive(std::string directory, std::string filename) { + Glib::Dir dir(directory); + + std::string name = dir.read_name(); + while (!name.empty()) { + std::string fullpath = Glib::build_filename(directory, name); + // g_message("%s", fullpath.c_str()); + + if (Glib::file_test(fullpath, Glib::FILE_TEST_IS_DIR)) { + if (_find_filename_recursive(fullpath, filename)) { + return true; + } + } else if (name == filename) { + return true; + } + name = dir.read_name(); + } + + return false; +} + +/** Searches for a gettext catalog matching the extension's translationdomain + * + * This function will attempt to find the correct gettext catalog for the translationdomain + * requested by the extension. + * + * For this the following three locations are recursively searched for "${translationdomain}.mo": + * - the 'locale' directory in the .inx file's folder + * - the 'locale' directory in the "extensions" folder containing the .inx + * - the system location for gettext catalogs, i.e. where Inkscape's own catalog is located + * + * If one matching file is found, the directory is assumed to be the correct location and registered with gettext + */ +void Extension::lookup_translation_catalog() { + g_assert(!_base_directory.empty()); + + // get locale folder locations + std::string locale_dir_current_extension; + std::string locale_dir_extensions; + std::string locale_dir_system; + + locale_dir_current_extension = Glib::build_filename(_base_directory, "locale"); + + size_t index = _base_directory.find_last_of("extensions"); + if (index != std::string::npos) { + locale_dir_extensions = Glib::build_filename(_base_directory.substr(0, index+1), "locale"); + } + + locale_dir_system = bindtextdomain("inkscape", nullptr); + + // collect unique locations into vector + std::vector locale_dirs; + if (locale_dir_current_extension != locale_dir_extensions) { + locale_dirs.push_back(std::move(locale_dir_current_extension)); + } + locale_dirs.push_back(std::move(locale_dir_extensions)); + locale_dirs.push_back(std::move(locale_dir_system)); + + // iterate over locations and look for the one that has the correct catalog + std::string search_name; + search_name += _translationdomain; + search_name += ".mo"; + for (auto locale_dir : locale_dirs) { + if (!Glib::file_test(locale_dir, Glib::FILE_TEST_IS_DIR)) { + continue; + } + + if (_find_filename_recursive(locale_dir, search_name)) { + _gettext_catalog_dir = locale_dir; + break; + } + } + + // register catalog with gettext if found, disable translation for this extension otherwise + if (!_gettext_catalog_dir.empty()) { + const char *current_dir = bindtextdomain(_translationdomain, nullptr); + if (_gettext_catalog_dir != current_dir) { + g_info("Binding textdomain '%s' to '%s'.", _translationdomain, _gettext_catalog_dir.c_str()); + bindtextdomain(_translationdomain, _gettext_catalog_dir.c_str()); + bind_textdomain_codeset(_translationdomain, "UTF-8"); + } + } else { + g_warning("Failed to locate message catalog for textdomain '%s'.", _translationdomain); + _translation_enabled = false; + _translationdomain = nullptr; + } +} + +/** Gets a translation within the context of the current extension + * + * Query gettext for the translated version of the input string, + * handling the preferred translation domain of the extension internally. + * + * @param msgid String to translate + * @param msgctxt Context for the translation + * + * @return Translated string (or original string if extension is not supposed to be translated) + */ +const char *Extension::get_translation(const char *msgid, const char *msgctxt) { + if (!_translation_enabled) { + return msgid; + } + + if (!strcmp(msgid, "")) { + g_warning("Attempting to translate an empty string in extension '%s', which is not supported.", _id); + return msgid; + } + + if (msgctxt) { + return g_dpgettext2(_translationdomain, msgctxt, msgid); + } else { + return g_dgettext(_translationdomain, msgid); + } +} + +/** Sets environment suitable for executing this Extension + * + * Currently sets the environment variables INKEX_GETTEXT_DOMAIN and INKEX_GETTEXT_DIRECTORY + * to make the "translationdomain" accessible to child processes spawned by this extension's Implementation. + */ +void Extension::set_environment() { + Glib::unsetenv("INKEX_GETTEXT_DOMAIN"); + Glib::unsetenv("INKEX_GETTEXT_DIRECTORY"); + + if (_translationdomain) { + Glib::setenv("INKEX_GETTEXT_DOMAIN", std::string(_translationdomain)); + } + if (!_gettext_catalog_dir.empty()) { + Glib::setenv("INKEX_GETTEXT_DIRECTORY", _gettext_catalog_dir); + } +} + +/** + \brief A function to get the parameters in a string form + \return An array with all the parameters in it. + +*/ +void +Extension::paramListString (std::list &retlist) +{ + // first collect all widgets in the current extension + std::vector widget_list; + for (auto widget : _widgets) { + widget->get_widgets(widget_list); + } + + // then build a list of parameter strings from parameter names and values, as '--name=value' + for (auto widget : widget_list) { + InxParameter *parameter = dynamic_cast(widget); // filter InxParameters from InxWidgets + if (parameter) { + const char *name = parameter->name(); + std::string value = parameter->value_to_string(); + + if (name && !value.empty()) { // TODO: Shouldn't empty string values be allowed? + std::string parameter_string; + parameter_string += "--"; + parameter_string += name; + parameter_string += "="; + parameter_string += value; + retlist.push_back(parameter_string); + } + } + } + + return; +} + +InxParameter *Extension::get_param(const gchar *name) +{ + if (!name || _widgets.empty()) { + throw Extension::param_not_exist(); + } + + // first collect all widgets in the current extension + std::vector widget_list; + for (auto widget : _widgets) { + widget->get_widgets(widget_list); + } + + // then search for a parameter with a matching name + for (auto widget : widget_list) { + InxParameter *parameter = dynamic_cast(widget); // filter InxParameters from InxWidgets + if (parameter && !strcmp(parameter->name(), name)) { + return parameter; + } + } + + // if execution reaches here, no parameter matching 'name' was found + throw Extension::param_not_exist(); +} + +InxParameter const *Extension::get_param(const gchar *name) const +{ + return const_cast(this)->get_param(name); +} + + +/** + \return The value of the parameter identified by the name + \brief Gets a parameter identified by name with the bool placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +bool +Extension::get_param_bool(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_bool(); +} + +/** + \return The integer value for the parameter specified + \brief Gets a parameter identified by name with the integer placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +int +Extension::get_param_int(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_int(); +} + +/** + \return The float value for the parameter specified + \brief Gets a parameter identified by name with the float in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +float +Extension::get_param_float(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_float(); +} + +/** + \return The string value for the parameter specified + \brief Gets a parameter identified by name with the string placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::get_param_string(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_string(); +} + +/** + \return The string value for the parameter specified + \brief Gets a parameter identified by name with the string placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::get_param_optiongroup(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_optiongroup(); +} + +/** + * This is useful to find out, if a given string \c value is selectable in a optiongroup named \cname. + * + * @param name The name of the optiongroup parameter to get. + * @return true if value exists, false if not + */ +bool +Extension::get_param_optiongroup_contains(const gchar *name, const char *value) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_optiongroup_contains(value); +} + +/** + \return The unsigned integer RGBA value for the parameter specified + \brief Gets a parameter identified by name with the unsigned int placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +guint32 +Extension::get_param_color(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_color(); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the boolean in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +bool +Extension::set_param_bool(const gchar *name, const bool value) +{ + InxParameter *param; + param = get_param(name); + return param->set_bool(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the integer in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +int +Extension::set_param_int(const gchar *name, const int value) +{ + InxParameter *param; + param = get_param(name); + return param->set_int(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the float in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +float +Extension::set_param_float(const gchar *name, const float value) +{ + InxParameter *param; + param = get_param(name); + return param->set_float(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the string in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::set_param_string(const gchar *name, const char *value) +{ + InxParameter *param; + param = get_param(name); + return param->set_string(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the string in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::set_param_optiongroup(const gchar *name, const char *value) +{ + InxParameter *param; + param = get_param(name); + return param->set_optiongroup(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the unsigned integer RGBA value in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + +Look up in the parameters list, const then execute the function on that found parameter. +*/ +guint32 +Extension::set_param_color(const gchar *name, const guint32 color) +{ + InxParameter *param; + param = get_param(name); + return param->set_color(color); +} + + +/** \brief A function to open the error log file. */ +void +Extension::error_file_open () +{ + gchar *ext_error_file = Inkscape::IO::Resource::log_path(EXTENSION_ERROR_LOG_FILENAME); + error_file = Inkscape::IO::fopen_utf8name(ext_error_file, "w+"); + if (!error_file) { + g_warning(_("Could not create extension error log file '%s'"), ext_error_file); + } + g_free(ext_error_file); +}; + +/** \brief A function to close the error log file. */ +void +Extension::error_file_close () +{ + if (error_file) { + fclose(error_file); + } +}; + +/** \brief A function to write to the error log file. */ +void +Extension::error_file_write (Glib::ustring text) +{ + if (error_file) { + g_fprintf(error_file, "%s\n", text.c_str()); + } +}; + +/** \brief A widget to represent the inside of an AutoGUI widget */ +class AutoGUI : public Gtk::VBox { +public: + /** \brief Create an AutoGUI object */ + AutoGUI () : Gtk::VBox() {}; + + /** + * Adds a widget with a tool tip into the autogui. + * + * If there is no widget, nothing happens. Otherwise it is just + * added into the VBox. If there is a tooltip (non-NULL) then it + * is placed on the widget. + * + * @param widg Widget to add. + * @param tooltip Tooltip for the widget. + */ + void addWidget(Gtk::Widget *widg, gchar const *tooltip, int indent) { + if (widg) { + widg->set_margin_start(indent * InxParameter::GUI_INDENTATION); + this->pack_start(*widg, false, true, 0); // fill=true does not have an effect here, but allows the + // child to choose to expand by setting hexpand/vexpand + if (tooltip) { + widg->set_tooltip_text(tooltip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + }; +}; + +/** \brief A function to automatically generate a GUI from the extensions' widgets + \return Generated widget + + This function just goes through each widget, and calls it's 'get_widget'. + Then, each of those is placed into a Gtk::VBox, which is then returned to the calling function. + + If there are no visible parameters, this function just returns NULL. +*/ +Gtk::Widget * +Extension::autogui (SPDocument *doc, Inkscape::XML::Node *node, sigc::signal *changeSignal) +{ + if (!_gui || widget_visible_count() == 0) { + return nullptr; + } + + AutoGUI * agui = Gtk::manage(new AutoGUI()); + agui->set_border_width(InxParameter::GUI_BOX_MARGIN); + agui->set_spacing(InxParameter::GUI_BOX_SPACING); + + // go through the list of widgets and add the all non-hidden ones + for (auto widget : _widgets) { + if (widget->get_hidden()) { + continue; + } + + Gtk::Widget *widg = widget->get_widget(changeSignal); + gchar const *tip = widget->get_tooltip(); + int indent = widget->get_indent(); + + agui->addWidget(widg, tip, indent); + } + + agui->show(); + return agui; +}; + +/* Extension editor dialog stuff */ + +Gtk::VBox * +Extension::get_info_widget() +{ + Gtk::VBox * retval = Gtk::manage(new Gtk::VBox()); + retval->set_border_width(4); + + Gtk::Frame * info = Gtk::manage(new Gtk::Frame("General Extension Information")); + retval->pack_start(*info, true, true, 4); + + auto table = Gtk::manage(new Gtk::Grid()); + table->set_border_width(4); + table->set_column_spacing(4); + + info->add(*table); + + int row = 0; + add_val(_("Name:"), get_translation(_name), table, &row); + add_val(_("ID:"), _id, table, &row); + add_val(_("State:"), _state == STATE_LOADED ? _("Loaded") : _state == STATE_UNLOADED ? _("Unloaded") : _("Deactivated"), table, &row); + + retval->show_all(); + return retval; +} + +void Extension::add_val(Glib::ustring labelstr, Glib::ustring valuestr, Gtk::Grid * table, int * row) +{ + Gtk::Label * label; + Gtk::Label * value; + + (*row)++; + label = Gtk::manage(new Gtk::Label(labelstr, Gtk::ALIGN_START)); + value = Gtk::manage(new Gtk::Label(valuestr, Gtk::ALIGN_START)); + + table->attach(*label, 0, (*row) - 1, 1, 1); + table->attach(*value, 1, (*row) - 1, 1, 1); + + label->show(); + value->show(); + + return; +} + +Gtk::VBox * +Extension::get_params_widget() +{ + Gtk::VBox * retval = Gtk::manage(new Gtk::VBox()); + Gtk::Widget * content = Gtk::manage(new Gtk::Label("Params")); + retval->pack_start(*content, true, true, 4); + content->show(); + retval->show(); + return retval; +} + +unsigned int Extension::widget_visible_count ( ) +{ + unsigned int _visible_count = 0; + for (auto widget : _widgets) { + if (!widget->get_hidden()) { + _visible_count++; + } + } + return _visible_count; +} + +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/extension.h b/src/extension/extension.h new file mode 100644 index 0000000..476be37 --- /dev/null +++ b/src/extension/extension.h @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_H +#define INK_EXTENSION_H + +/** \file + * Frontend to certain, possibly pluggable, actions. + */ + +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include +#include + +namespace Glib { + class ustring; +} + +namespace Gtk { + class Grid; + class VBox; + class Widget; +} + +/** The key that is used to identify that the I/O should be autodetected */ +#define SP_MODULE_KEY_AUTODETECT "autodetect" +/** This is the key for the SVG input module */ +#define SP_MODULE_KEY_INPUT_SVG "org.inkscape.input.svg" +#define SP_MODULE_KEY_INPUT_SVGZ "org.inkscape.input.svgz" +/** Specifies the input module that should be used if none are selected */ +#define SP_MODULE_KEY_INPUT_DEFAULT SP_MODULE_KEY_AUTODETECT +/** The key for outputting standard W3C SVG */ +#define SP_MODULE_KEY_OUTPUT_SVG "org.inkscape.output.svg.plain" +#define SP_MODULE_KEY_OUTPUT_SVGZ "org.inkscape.output.svgz.plain" +/** This is an output file that has SVG data with the Sodipodi namespace extensions */ +#define SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "org.inkscape.output.svg.inkscape" +#define SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE "org.inkscape.output.svgz.inkscape" +/** Which output module should be used? */ +#define SP_MODULE_KEY_OUTPUT_DEFAULT SP_MODULE_KEY_AUTODETECT + +/** Defines the key for Postscript printing */ +#define SP_MODULE_KEY_PRINT_PS "org.inkscape.print.ps" +#define SP_MODULE_KEY_PRINT_CAIRO_PS "org.inkscape.print.ps.cairo" +#define SP_MODULE_KEY_PRINT_CAIRO_EPS "org.inkscape.print.eps.cairo" +/** Defines the key for PDF printing */ +#define SP_MODULE_KEY_PRINT_PDF "org.inkscape.print.pdf" +#define SP_MODULE_KEY_PRINT_CAIRO_PDF "org.inkscape.print.pdf.cairo" +/** Defines the key for LaTeX printing */ +#define SP_MODULE_KEY_PRINT_LATEX "org.inkscape.print.latex" +/** Defines the key for printing with GNOME Print */ +#define SP_MODULE_KEY_PRINT_GNOME "org.inkscape.print.gnome" + +/** Mime type for SVG */ +#define MIME_SVG "image/svg+xml" + +/** Name of the extension error file */ +#define EXTENSION_ERROR_LOG_FILENAME "extension-errors.log" + + +#define INKSCAPE_EXTENSION_URI "http://www.inkscape.org/namespace/inkscape/extension" +#define INKSCAPE_EXTENSION_NS_NC "extension" +#define INKSCAPE_EXTENSION_NS "extension:" + +class SPDocument; + +namespace Inkscape { + +namespace XML { +class Node; +} + +namespace Extension { + +class ExecutionEnv; +class Dependency; +class ExpirationTimer; +class ExpirationTimer; +class InxParameter; +class InxWidget; + +namespace Implementation +{ +class Implementation; +} + + +/** The object that is the basis for the Extension system. This object + contains all of the information that all Extension have. The + individual items are detailed within. This is the interface that + those who want to _use_ the extensions system should use. This + is most likely to be those who are inside the Inkscape program. */ +class Extension { +public: + /** An enumeration to identify if the Extension has been loaded or not. */ + enum state_t { + STATE_LOADED, /**< The extension has been loaded successfully */ + STATE_UNLOADED, /**< The extension has not been loaded */ + STATE_DEACTIVATED /**< The extension is missing something which makes it unusable */ + }; + +private: + gchar *_id = nullptr; /**< The unique identifier for the Extension */ + gchar *_name = nullptr; /**< A user friendly name for the Extension */ + state_t _state = STATE_UNLOADED; /**< Which state the Extension is currently in */ + std::vector _deps; /**< Dependencies for this extension */ + static FILE *error_file; /**< This is the place where errors get reported */ + bool _gui; + std::string _error_reason; /**< Short, textual explanation for the latest error */ + +protected: + Inkscape::XML::Node *repr; /**< The XML description of the Extension */ + Implementation::Implementation * imp; /**< An object that holds all the functions for making this work */ + ExecutionEnv * execution_env; /**< Execution environment of the extension + * (currently only used by Effects) */ + std::string _base_directory; /**< Directory containing the .inx file, + * relative paths in the extension should usually be relative to it */ + ExpirationTimer * timer = nullptr; /**< Timeout to unload after a given time */ + bool _translation_enabled = true; /**< Attempt translation of strings provided by the extension? */ + +private: + const char *_translationdomain = nullptr; /**< Domainname of gettext textdomain that should + * be used for translation of the extension's strings */ + std::string _gettext_catalog_dir; /**< Directory containing the gettext catalog for _translationdomain */ + + void lookup_translation_catalog(); + +public: + Extension(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + virtual ~Extension(); + + void set_state (state_t in_state); + state_t get_state (); + bool loaded (); + virtual bool check (); + Inkscape::XML::Node * get_repr (); + gchar * get_id () const; + gchar * get_name () const; + void deactivate (); + bool deactivated (); + void printFailure (Glib::ustring reason); + std::string const &getErrorReason() { return _error_reason; }; + Implementation::Implementation * get_imp () { return imp; }; + void set_execution_env (ExecutionEnv * env) { execution_env = env; }; + ExecutionEnv *get_execution_env () { return execution_env; }; + std::string get_base_directory() const { return _base_directory; }; + void set_base_directory(std::string base_directory) { _base_directory = base_directory; }; + std::string get_dependency_location(const char *name); + const char *get_translation(const char* msgid, const char *msgctxt=nullptr); + void set_environment(); + +/* Parameter Stuff */ +private: + std::vector _widgets; /**< A list of widgets for this extension. */ + +public: + /** \brief A function to get the number of visible parameters of the extension. + \return The number of visible parameters. */ + unsigned int widget_visible_count ( ); + +public: + /** An error class for when a parameter is looked for that just + * simply doesn't exist */ + class param_not_exist {}; + + /** no valid ID found while parsing XML representation */ + class extension_no_id{}; + + /** no valid name found while parsing XML representation */ + class extension_no_name{}; + + /** extension is not compatible with the current system and should not be loaded */ + class extension_not_compatible{}; + + /** An error class for when a filename already exists, but the user + * doesn't want to overwrite it */ + class no_overwrite {}; + +private: + void make_param (Inkscape::XML::Node * paramrepr); + + /** + * Looks up the parameter with the specified name. + * + * Searches the list of parameters attached to this extension, + * looking for a parameter with a matching name. + * + * This function can throw a 'param_not_exist' exception if the + * name is not found. + * + * @param name Name of the parameter to search for. + * @return Parameter with matching name. + */ + InxParameter *get_param(const gchar *name); + + InxParameter const *get_param(const gchar *name) const; + +public: + bool get_param_bool (const gchar *name) const; + int get_param_int (const gchar *name) const; + float get_param_float (const gchar *name) const; + const char *get_param_string (const gchar *name) const; + const char *get_param_optiongroup (const gchar *name) const; + guint32 get_param_color (const gchar *name) const; + + bool get_param_optiongroup_contains (const gchar *name, const char *value) const; + + bool set_param_bool (const gchar *name, const bool value); + int set_param_int (const gchar *name, const int value); + float set_param_float (const gchar *name, const float value); + const char *set_param_string (const gchar *name, const char *value); + const char *set_param_optiongroup (const gchar *name, const char *value); + guint32 set_param_color (const gchar *name, const guint32 color); + + + /* Error file handling */ +public: + static void error_file_open (); + static void error_file_close (); + static void error_file_write (Glib::ustring text); + +public: + Gtk::Widget *autogui (SPDocument *doc, Inkscape::XML::Node *node, sigc::signal *changeSignal = nullptr); + void paramListString(std::list &retlist); + void set_gui(bool s) { _gui = s; } + bool get_gui() { return _gui; } + + /* Extension editor dialog stuff */ +public: + Gtk::VBox *get_info_widget(); + Gtk::VBox *get_params_widget(); +protected: + inline static void add_val(Glib::ustring labelstr, Glib::ustring valuestr, Gtk::Grid * table, int * row); +}; + + + +/* + +This is a prototype for how collections should work. Whoever gets +around to implementing this gets to decide what a 'folder' and an +'item' really is. That is the joy of implementing it, eh? + +class Collection : public Extension { + +public: + folder get_root (void); + int get_count (folder); + thumbnail get_thumbnail(item); + item[] get_items(folder); + folder[] get_folders(folder); + metadata get_metadata(item); + image get_image(item); + +}; +*/ + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif // INK_EXTENSION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/find_extension_by_mime.h b/src/extension/find_extension_by_mime.h new file mode 100644 index 0000000..f58580e --- /dev/null +++ b/src/extension/find_extension_by_mime.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Find an extension by its mime type. + */ +/* Authors: + * Lauris Kaplinski + * Frank Felfe + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2010 authors + * Copyright (C) 1999-2005 authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "input.h" + +namespace Inkscape { +namespace Extension { +static inline Inkscape::Extension::Extension *find_by_mime(const char *const mime) +{ + + Inkscape::Extension::DB::InputList o; + Inkscape::Extension::db.get_input_list(o); + Inkscape::Extension::DB::InputList::const_iterator i = o.begin(); + while (i != o.end() && strcmp((*i)->get_mimetype(), mime) != 0) { + ++i; + } + return *i; +} +} +} diff --git a/src/extension/implementation/implementation.cpp b/src/extension/implementation/implementation.cpp new file mode 100644 index 0000000..36cd299 --- /dev/null +++ b/src/extension/implementation/implementation.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Author: Ted Gould + Copyright (c) 2003-2005,2007 + + Released under GNU GPL v2+, read the file 'COPYING' for more information. + + This file is the backend to the extensions system. These are + the parts of the system that most users will never see, but are + important for implementing the extensions themselves. This file + contains the base class for all of that. +*/ + +#include "implementation.h" + +#include +#include +#include + +#include "selection.h" +#include "desktop.h" + + +namespace Inkscape { +namespace Extension { +namespace Implementation { + +Gtk::Widget * +Implementation::prefs_input(Inkscape::Extension::Input *module, gchar const */*filename*/) { + return module->autogui(nullptr, nullptr); +} + +Gtk::Widget * +Implementation::prefs_output(Inkscape::Extension::Output *module) { + return module->autogui(nullptr, nullptr); +} + +Gtk::Widget *Implementation::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, ImplementationDocumentCache * /*docCache*/) +{ + if (module->widget_visible_count() == 0) { + return nullptr; + } + + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node const* first_select = nullptr; + if (!selected.empty()) { + const SPItem * item = selected.front(); + first_select = item->getRepr(); + } + + // TODO deal with this broken const correctness: + return module->autogui(current_document, const_cast(first_select), changeSignal); +} // Implementation::prefs_effect + +} /* namespace Implementation */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/implementation/implementation.h b/src/extension/implementation/implementation.h new file mode 100644 index 0000000..ba4f773 --- /dev/null +++ b/src/extension/implementation/implementation.h @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Author: Ted Gould + Copyright (c) 2003-2005,2007 + + Released under GNU GPL v2+, read the file 'COPYING' for more information. + + This file is the backend to the extensions system. These are + the parts of the system that most users will never see, but are + important for implementing the extensions themselves. This file + contains the base class for all of that. +*/ +#ifndef SEEN_INKSCAPE_EXTENSION_IMPLEMENTATION_H +#define SEEN_INKSCAPE_EXTENSION_IMPLEMENTATION_H + +#include +#include +#include +#include <2geom/forward.h> + +namespace Gtk { + class Widget; +} + +class SPDocument; +class SPStyle; + +namespace Inkscape { + +namespace UI { +namespace View { +class View; +} +} + +namespace XML { + class Node; +} + +namespace Extension { + +class Effect; +class Extension; +class Input; +class Output; +class Print; + +namespace Implementation { + +/** + * A cache for the document and this implementation. + */ +class ImplementationDocumentCache { + + /** + * The document that this instance is working on. + */ + Inkscape::UI::View::View * _view; +public: + ImplementationDocumentCache (Inkscape::UI::View::View * view) { return; }; + + virtual ~ImplementationDocumentCache ( ) { return; }; + Inkscape::UI::View::View const * view ( ) { return _view; }; +}; + +/** + * Base class for all implementations of modules. This is whether they are done systematically by + * having something like the scripting system, or they are implemented internally they all derive + * from this class. + */ +class Implementation { +public: + // ----- Constructor / destructor ----- + Implementation() = default; + + virtual ~Implementation() = default; + + // ----- Basic functions for all Extension ----- + virtual bool load(Inkscape::Extension::Extension * /*module*/) { return true; } + + virtual void unload(Inkscape::Extension::Extension * /*module*/) {} + + /** + * Create a new document cache object. + * This function just returns \c NULL. Subclasses are likely + * to reimplement it to do something useful. + * @param ext The extension that is referencing us + * @param doc The document to create the cache of + * @return A new document cache that is valid as long as the document + * is not changed. + */ + virtual ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * /*doc*/) { return nullptr; } + + /** Verify any dependencies. */ + virtual bool check(Inkscape::Extension::Extension * /*module*/) { return true; } + + virtual bool cancelProcessing () { return true; } + virtual void commitDocument () {} + + // ----- Input functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget *prefs_input(Inkscape::Extension::Input *module, + gchar const *filename); + + virtual SPDocument *open(Inkscape::Extension::Input * /*module*/, + gchar const * /*filename*/) { return nullptr; } + + // ----- Output functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget *prefs_output(Inkscape::Extension::Output *module); + virtual void save(Inkscape::Extension::Output * /*module*/, SPDocument * /*doc*/, gchar const * /*filename*/) {} + + // ----- Effect functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, + Inkscape::UI::View::View *view, + sigc::signal *changeSignal, + ImplementationDocumentCache *docCache); + virtual void effect(Inkscape::Extension::Effect * /*module*/, + Inkscape::UI::View::View * /*document*/, + ImplementationDocumentCache * /*docCache*/) {} + + // ----- Print functions ----- + virtual unsigned setup(Inkscape::Extension::Print * /*module*/) { return 0; } + virtual unsigned set_preview(Inkscape::Extension::Print * /*module*/) { return 0; } + + virtual unsigned begin(Inkscape::Extension::Print * /*module*/, + SPDocument * /*doc*/) { return 0; } + virtual unsigned finish(Inkscape::Extension::Print * /*module*/) { return 0; } + + /** + * Tell the printing engine whether text should be text or path. + * Default value is false because most printing engines will support + * paths more than they'll support text. (at least they do today) + * \retval true Render the text as a path + * \retval false Render text using the text function (above) + */ + virtual bool textToPath(Inkscape::Extension::Print * /*ext*/) { return false; } + + /** + * Get "fontEmbedded" param, i.e. tell the printing engine whether fonts should be embedded. + * Only available for Adobe Type 1 fonts in EPS output as of now + * \retval true Fonts have to be embedded in the output so that the user might not need + * to install fonts to have the interpreter read the document correctly + * \retval false Do not embed fonts + */ + virtual bool fontEmbedded(Inkscape::Extension::Print * /*ext*/) { return false; } + + // ----- Rendering methods ----- + virtual unsigned bind(Inkscape::Extension::Print * /*module*/, + Geom::Affine const & /*transform*/, + float /*opacity*/) { return 0; } + virtual unsigned release(Inkscape::Extension::Print * /*module*/) { return 0; } + virtual unsigned comment(Inkscape::Extension::Print * /*module*/, char const * /*comment*/) { return 0; } + virtual unsigned fill(Inkscape::Extension::Print * /*module*/, + Geom::PathVector const & /*pathv*/, + Geom::Affine const & /*ctm*/, + SPStyle const * /*style*/, + Geom::OptRect const & /*pbox*/, + Geom::OptRect const & /*dbox*/, + Geom::OptRect const & /*bbox*/) { return 0; } + virtual unsigned stroke(Inkscape::Extension::Print * /*module*/, + Geom::PathVector const & /*pathv*/, + Geom::Affine const & /*transform*/, + SPStyle const * /*style*/, + Geom::OptRect const & /*pbox*/, + Geom::OptRect const & /*dbox*/, + Geom::OptRect const & /*bbox*/) { return 0; } + virtual unsigned image(Inkscape::Extension::Print * /*module*/, + unsigned char * /*px*/, + unsigned int /*w*/, + unsigned int /*h*/, + unsigned int /*rs*/, + Geom::Affine const & /*transform*/, + SPStyle const * /*style*/) { return 0; } + virtual unsigned text(Inkscape::Extension::Print * /*module*/, + char const * /*text*/, + Geom::Point const & /*p*/, + SPStyle const * /*style*/) { return 0; } + virtual void processPath(Inkscape::XML::Node * /*node*/) {} + + /** + * If detach = true, when saving to a file, don't store URIs realtive to the filename + */ + virtual void setDetachBase(bool detach) {} +}; + + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // __INKSCAPE_EXTENSION_IMPLEMENTATION_H__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/implementation/script.cpp b/src/extension/implementation/script.cpp new file mode 100644 index 0000000..f885caf --- /dev/null +++ b/src/extension/implementation/script.cpp @@ -0,0 +1,978 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Code for handling extensions (i.e. scripts). + * + * Authors: + * Bryce Harrington + * Ted Gould + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2002-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "desktop.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "preferences.h" +#include "script.h" +#include "selection.h" + +#include "extension/db.h" +#include "extension/effect.h" +#include "extension/execution-env.h" +#include "extension/input.h" +#include "extension/output.h" +#include "extension/system.h" +#include "io/resource.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "ui/dialog-events.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/view/view.h" +#include "xml/attribute-record.h" +#include "xml/node.h" + +/* Namespaces */ +namespace Inkscape { +namespace Extension { +namespace Implementation { + +/** \brief Make GTK+ events continue to come through a little bit + + This just keeps coming the events through so that we'll make the GUI + update and look pretty. +*/ +void Script::pump_events () { + while ( Gtk::Main::events_pending() ) { + Gtk::Main::iteration(); + } + return; +} + + +/** \brief A table of what interpreters to call for a given language + + This table is used to keep track of all the programs to execute a + given script. It also tracks the preference to use to overwrite + the given interpreter to a custom one per user. +*/ +const std::map Script::interpreterTab = { +#ifdef _WIN32 + { "perl", {"perl-interpreter", {"wperl" }}}, + { "python", {"python-interpreter", {"pythonw" }}}, +#elif defined __APPLE__ + { "perl", {"perl-interpreter", {"perl" }}}, + { "python", {"python-interpreter", {"python3" }}}, +#else + { "perl", {"perl-interpreter", {"perl" }}}, + { "python", {"python-interpreter", {"python3", "python" }}}, +#endif + { "python2", {"python2-interpreter", {"python2", "python" }}}, + { "ruby", {"ruby-interpreter", {"ruby" }}}, + { "shell", {"shell-interpreter", {"sh" }}}, +}; + + + +/** \brief Look up an interpreter name, and translate to something that + is executable + \param interpNameArg The name of the interpreter that we're looking + for, should be an entry in interpreterTab +*/ +std::string Script::resolveInterpreterExecutable(const Glib::ustring &interpNameArg) +{ + // 0. Do we have a supported interpreter type? + auto interp = interpreterTab.find(interpNameArg); + if (interp == interpreterTab.end()) { + g_critical("Script::resolveInterpreterExecutable(): unknown script interpreter '%s'", interpNameArg.c_str()); + return ""; + } + + std::list searchList; + std::copy(interp->second.defaultvals.begin(), interp->second.defaultvals.end(), std::back_inserter(searchList)); + + // 1. Check preferences for an override. + auto prefs = Inkscape::Preferences::get(); + auto prefInterp = prefs->getString("/extensions/" + Glib::ustring(interp->second.prefstring)); + + if (!prefInterp.empty()) { + searchList.push_front(prefInterp); + } + + // 2. Search for things in the path if they're there or an absolute + for (const auto& binname : searchList) { + auto interpreter_path = Glib::filename_from_utf8(binname); + + if (!Glib::path_is_absolute(interpreter_path)) { + auto found_path = Glib::find_program_in_path(interpreter_path); + if (!found_path.empty()) { + return found_path; + } + } else { + return interpreter_path; + } + } + + // 3. Error + g_critical("Script::resolveInterpreterExecutable(): failed to locate script interpreter '%s'", interpNameArg.c_str()); + return ""; +} + +/** \brief This function creates a script object and sets up the + variables. + \return A script object + + This function just sets the command to NULL. It should get built + officially in the load function. This allows for less allocation + of memory in the unloaded state. +*/ +Script::Script() + : Implementation() + , _canceled(false) + , parent_window(nullptr) +{ +} + +/** + * \brief Destructor + */ +Script::~Script() += default; + + +/** + \return none + \brief This function 'loads' an extension, basically it determines + the full command for the extension and stores that. + \param module The extension to be loaded. + + The most difficult part about this function is finding the actual + command through all of the Reprs. Basically it is hidden down a + couple of layers, and so the code has to move down too. When + the command is actually found, it has its relative directory + solved. + + At that point all of the loops are exited, and there is an + if statement to make sure they didn't exit because of not finding + the command. If that's the case, the extension doesn't get loaded + and should error out at a higher level. +*/ + +bool Script::load(Inkscape::Extension::Extension *module) +{ + if (module->loaded()) { + return true; + } + + helper_extension = ""; + + /* This should probably check to find the executable... */ + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "script")) { + for (child_repr = child_repr->firstChild(); child_repr != nullptr; child_repr = child_repr->next()) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "command")) { + const gchar *interpretstr = child_repr->attribute("interpreter"); + if (interpretstr != nullptr) { + std::string interpString = resolveInterpreterExecutable(interpretstr); + if (interpString.empty()) { + continue; // can't have a script extension with empty interpreter + } + command.push_back(interpString); + } + // TODO: we already parse commands as dependencies in extension.cpp + // can can we optimize this to be less indirect? + const char *script_name = child_repr->firstChild()->content(); + std::string script_location = module->get_dependency_location(script_name); + command.push_back(std::move(script_location)); + } else if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "helper_extension")) { + helper_extension = child_repr->firstChild()->content(); + } + } + + break; + } + child_repr = child_repr->next(); + } + + // TODO: Currently this causes extensions to fail silently, see comment in Extension::set_state() + g_return_val_if_fail(command.size() > 0, false); + + return true; +} + + +/** + \return None. + \brief Unload this puppy! + \param module Extension to be unloaded. + + This function just sets the module to unloaded. It free's the + command if it has been allocated. +*/ +void Script::unload(Inkscape::Extension::Extension */*module*/) +{ + command.clear(); + helper_extension = ""; +} + + + + +/** + \return Whether the check passed or not + \brief Check every dependency that was given to make sure we should keep this extension + \param module The Extension in question + +*/ +bool Script::check(Inkscape::Extension::Extension *module) +{ + int script_count = 0; + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "script")) { + script_count++; + + // check if all helper_extensions attached to this script were registered + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "helper_extension")) { + gchar const *helper = child_repr->firstChild()->content(); + if (Inkscape::Extension::db.get(helper) == nullptr) { + return false; + } + } + + child_repr = child_repr->next(); + } + + break; + } + child_repr = child_repr->next(); + } + + if (script_count == 0) { + return false; + } + + return true; +} + +class ScriptDocCache : public ImplementationDocumentCache { + friend class Script; +protected: + std::string _filename; + int _tempfd; +public: + ScriptDocCache (Inkscape::UI::View::View * view); + ~ScriptDocCache ( ) override; +}; + +ScriptDocCache::ScriptDocCache (Inkscape::UI::View::View * view) : + ImplementationDocumentCache(view), + _filename(""), + _tempfd(0) +{ + try { + _tempfd = Glib::file_open_tmp(_filename, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + SPDesktop *desktop = (SPDesktop *) view; + sp_namedview_document_from_window(desktop); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/svgoutput/disable_optimizations", true); + Inkscape::Extension::save( + Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE), + view->doc(), _filename.c_str(), false, false, false, Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + prefs->setBool("/options/svgoutput/disable_optimizations", false); + return; +} + +ScriptDocCache::~ScriptDocCache ( ) +{ + close(_tempfd); + unlink(_filename.c_str()); +} + +ImplementationDocumentCache *Script::newDocCache( Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * view ) { + return new ScriptDocCache(view); +} + + +/** + \return A dialog for preferences + \brief A stub function right now + \param module Module who's preferences need getting + \param filename Hey, the file you're getting might be important + + This function should really do something, right now it doesn't. +*/ +Gtk::Widget *Script::prefs_input(Inkscape::Extension::Input *module, + const gchar */*filename*/) +{ + return module->autogui(nullptr, nullptr); +} + + + +/** + \return A dialog for preferences + \brief A stub function right now + \param module Module whose preferences need getting + + This function should really do something, right now it doesn't. +*/ +Gtk::Widget *Script::prefs_output(Inkscape::Extension::Output *module) +{ + return module->autogui(nullptr, nullptr); +} + +/** + \return A new document that has been opened + \brief This function uses a filename that is put in, and calls + the extension's command to create an SVG file which is + returned. + \param module Extension to use. + \param filename File to open. + + First things first, this function needs a temporary file name. To + create one of those the function Glib::file_open_tmp is used with + the header of ink_ext_. + + The extension is then executed using the 'execute' function + with the filename assigned and then the temporary filename. + After execution the SVG should be in the temporary file. + + Finally, the temporary file is opened using the SVG input module and + a document is returned. That document has its filename set to + the incoming filename (so that it's not the temporary filename). + That document is then returned from this function. +*/ +SPDocument *Script::open(Inkscape::Extension::Input *module, + const gchar *filenameArg) +{ + std::list params; + module->paramListString(params); + module->set_environment(); + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return nullptr; + } + + std::string lfilename = Glib::filename_from_utf8(filenameArg); + + file_listener fileout; + int data_read = execute(command, params, lfilename, fileout); + fileout.toFile(tempfilename_out); + + SPDocument * mydoc = nullptr; + if (data_read > 10) { + if (helper_extension.size()==0) { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(SP_MODULE_KEY_INPUT_SVG), + tempfilename_out.c_str()); + } else { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(helper_extension.c_str()), + tempfilename_out.c_str()); + } + } // data_read + + if (mydoc != nullptr) { + mydoc->setDocumentBase(nullptr); + mydoc->changeUriAndHrefs(filenameArg); + } + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_out); + + unlink(tempfilename_out.c_str()); + + return mydoc; +} // open + + + +/** + \return none + \brief This function uses an extension to save a document. It first + creates an SVG file of the document, and then runs it through + the script. + \param module Extension to be used + \param doc Document to be saved + \param filename The name to save the final file as + \return false in case of any failure writing the file, otherwise true + + Well, at some point people need to save - it is really what makes + the entire application useful. And, it is possible that someone + would want to use an extension for this, so we need a function to + do that, eh? + + First things first, the document is saved to a temporary file that + is an SVG file. To get the temporary filename Glib::file_open_tmp is used with + ink_ext_ as a prefix. Don't worry, this file gets deleted at the + end of the function. + + After we have the SVG file, then Script::execute is called with + the temporary file name and the final output filename. This should + put the output of the script into the final output file. We then + delete the temporary file. +*/ +void Script::save(Inkscape::Extension::Output *module, + SPDocument *doc, + const gchar *filenameArg) +{ + std::list params; + module->paramListString(params); + module->set_environment(); + + std::string tempfilename_in; + int tempfd_in = 0; + try { + tempfd_in = Glib::file_open_tmp(tempfilename_in, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + throw Inkscape::Extension::Output::save_failed(); + } + + if (helper_extension.size() == 0) { + Inkscape::Extension::save( + Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE), + doc, tempfilename_in.c_str(), false, false, false, + Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + } else { + Inkscape::Extension::save( + Inkscape::Extension::db.get(helper_extension.c_str()), + doc, tempfilename_in.c_str(), false, false, false, + Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + } + + + file_listener fileout; + int data_read = execute(command, params, tempfilename_in, fileout); + + bool success = false; + + if (data_read > 0) { + std::string lfilename = Glib::filename_from_utf8(filenameArg); + success = fileout.toFile(lfilename); + } + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_in); + // FIXME: convert to utf8 (from "filename encoding") and unlink_utf8name + unlink(tempfilename_in.c_str()); + + if (success == false) { + throw Inkscape::Extension::Output::save_failed(); + } + + return; +} + + + +/** + \return none + \brief This function uses an extension as an effect on a document. + \param module Extension to effect with. + \param doc Document to run through the effect. + + This function is a little bit trickier than the previous two. It + needs two temporary files to get its work done. Both of these + files have random names created for them using the Glib::file_open_temp function + with the ink_ext_ prefix in the temporary directory. Like the other + functions, the temporary files are deleted at the end. + + To save/load the two temporary documents (both are SVG) the internal + modules for SVG load and save are used. They are both used through + the module system function by passing their keys into the functions. + + The command itself is built a little bit differently than in other + functions because the effect support selections. So on the command + line a list of all the ids that are selected is included. Currently, + this only works for a single selected object, but there will be more. + The command string is filled with the data, and then after the execution + it is freed. + + The execute function is used at the core of this function + to execute the Script on the two SVG documents (actually only one + exists at the time, the other is created by that script). At that + point both should be full, and the second one is loaded. +*/ +void Script::effect(Inkscape::Extension::Effect *module, + Inkscape::UI::View::View *doc, + ImplementationDocumentCache * docCache) +{ + if (docCache == nullptr) { + docCache = newDocCache(module, doc); + } + ScriptDocCache * dc = dynamic_cast(docCache); + if (dc == nullptr) { + printf("TOO BAD TO LIVE!!!"); + exit(1); + } + if (doc == nullptr) + { + g_warning("Script::effect: View not defined"); + return; + } + + SPDesktop *desktop = reinterpret_cast(doc); + sp_namedview_document_from_window(desktop); + + std::list params; + module->paramListString(params); + module->set_environment(); + + parent_window = module->get_execution_env()->get_working_dialog(); + + if (module->no_doc) { + // this is a no-doc extension, e.g. a Help menu command; + // just run the command without any files, ignoring errors + + Glib::ustring empty; + file_listener outfile; + execute(command, params, empty, outfile); + + return; + } + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + if (desktop) { + Inkscape::Selection * selection = desktop->getSelection(); + if (selection) { + params = selection->params; + module->paramListString(params); + selection->clear(); + } + } + + file_listener fileout; + int data_read = execute(command, params, dc->_filename, fileout); + fileout.toFile(tempfilename_out); + + pump_events(); + + SPDocument * mydoc = nullptr; + if (data_read > 10) { + try { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(SP_MODULE_KEY_INPUT_SVG), + tempfilename_out.c_str()); + } catch (const Inkscape::Extension::Input::open_failed &e) { + g_warning("Extension returned output that could not be parsed: %s", e.what()); + Gtk::MessageDialog warning( + _("The output from the extension could not be parsed."), + false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); + warning.set_transient_for( parent_window ? *parent_window : *(INKSCAPE.active_desktop()->getToplevel()) ); + warning.run(); + } + } // data_read + + pump_events(); + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_out); + + g_unlink(tempfilename_out.c_str()); + + if (mydoc) { + SPDocument* vd=doc->doc(); + if (vd != nullptr) + { + mydoc->changeUriAndHrefs(vd->getDocumentURI()); + + vd->emitReconstructionStart(); + copy_doc(vd->getReprRoot(), mydoc->getReprRoot()); + vd->emitReconstructionFinish(); + + // Getting the named view from the document generated by the extension + SPNamedView *nv = sp_document_namedview(mydoc, nullptr); + + //Check if it has a default layer set up + SPObject *layer = nullptr; + if ( nv != nullptr) + { + if( nv->default_layer_id != 0 ) { + SPDocument *document = desktop->doc(); + //If so, get that layer + if (document != nullptr) + { + layer = document->getObjectById(g_quark_to_string(nv->default_layer_id)); + } + } + desktop->showGrids(nv->grids_visible); + } + + sp_namedview_update_layers_from_document(desktop); + //If that layer exists, + if (layer) { + //set the current layer + desktop->setCurrentLayer(layer); + } + } + mydoc->release(); + } + + return; +} + + + +/** + \brief A function to replace all the elements in an old document + by those from a new document. + document and repinserts them into an emptied old document. + \param oldroot The root node of the old (destination) document. + \param newroot The root node of the new (source) document. + + This function first deletes all the root attributes in the old document followed + by copying all the root attributes from the new document to the old document. + + It then deletes all the elements in the old document by + making two passes, the first to create a list of the old elements and + the second to actually delete them. This two pass approach removes issues + with the list being changed while parsing through it... lots of nasty bugs. + + Then, it copies all the element in the new document into the old document. + + Finally, it copies the attributes in namedview. +*/ +void Script::copy_doc (Inkscape::XML::Node * oldroot, Inkscape::XML::Node * newroot) +{ + if ((oldroot == nullptr) ||(newroot == nullptr)) + { + g_warning("Error on copy_doc: NULL pointer input."); + return; + } + + // For copying attributes in root and in namedview + using Inkscape::Util::List; + using Inkscape::XML::AttributeRecord; + std::vector attribs; + + // Must explicitly copy root attributes. This must be done first since + // copying grid lines calls "SPGuide::set()" which needs to know the + // width, height, and viewBox of the root element. + + // Make a list of all attributes of the old root node. + for (List iter = oldroot->attributeList(); iter; ++iter) { + attribs.push_back(g_quark_to_string(iter->key)); + } + + // Delete the attributes of the old root node. + for (auto attrib : attribs) { + oldroot->removeAttribute(attrib); + } + + // Set the new attributes. + for (List iter = newroot->attributeList(); iter; ++iter) { + gchar const *name = g_quark_to_string(iter->key); + oldroot->setAttribute(name, newroot->attribute(name)); + } + + // Question: Why is the "sodipodi:namedview" special? Treating it as a normal + // element results in crashes. + // Seems to be a bug: + // http://inkscape.13.x6.nabble.com/Effect-that-modifies-the-document-properties-tt2822126.html + + std::vector delete_list; + + // Make list + for (Inkscape::XML::Node * child = oldroot->firstChild(); + child != nullptr; + child = child->next()) { + if (!strcmp("sodipodi:namedview", child->name())) { + for (Inkscape::XML::Node * oldroot_namedview_child = child->firstChild(); + oldroot_namedview_child != nullptr; + oldroot_namedview_child = oldroot_namedview_child->next()) { + delete_list.push_back(oldroot_namedview_child); + } + break; + } + } + + // Unparent (delete) + for (auto & i : delete_list) { + sp_repr_unparent(i); + } + attribs.clear(); + oldroot->mergeFrom(newroot, "id", true, true); +} + +/** \brief This function checks the stderr file, and if it has data, + shows it in a warning dialog to the user + \param filename Filename of the stderr file +*/ +void Script::checkStderr (const Glib::ustring &data, + Gtk::MessageType type, + const Glib::ustring &message) +{ + Gtk::MessageDialog warning(message, false, type, Gtk::BUTTONS_OK, true); + warning.set_resizable(true); + GtkWidget *dlg = GTK_WIDGET(warning.gobj()); + if (parent_window) { + warning.set_transient_for(*parent_window); + } else { + sp_transientize(dlg); + } + + auto vbox = warning.get_content_area(); + + /* Gtk::TextView * textview = new Gtk::TextView(Gtk::TextBuffer::create()); */ + Gtk::TextView * textview = new Gtk::TextView(); + textview->set_editable(false); + textview->set_wrap_mode(Gtk::WRAP_WORD); + textview->show(); + + textview->get_buffer()->set_text(data.c_str()); + + Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow(); + scrollwindow->add(*textview); + scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollwindow->set_shadow_type(Gtk::SHADOW_IN); + scrollwindow->show(); + scrollwindow->set_size_request(0, 60); + + vbox->pack_start(*scrollwindow, true, true, 5 /* fix these */); + + warning.run(); + + delete textview; + delete scrollwindow; + + return; +} + +bool Script::cancelProcessing () { + _canceled = true; + _main_loop->quit(); + Glib::spawn_close_pid(_pid); + + return true; +} + + +/** \brief This is the core of the extension file as it actually does + the execution of the extension. + \param in_command The command to be executed + \param filein Filename coming in + \param fileout Filename of the out file + \return Number of bytes that were read into the output file. + + The first thing that this function does is build the command to be + executed. This consists of the first string (in_command) and then + the filename for input (filein). This file is put on the command + line. + + The next thing that this function does is open a pipe to the + command and get the file handle in the ppipe variable. It then + opens the output file with the output file handle. Both of these + operations are checked extensively for errors. + + After both are opened, then the data is copied from the output + of the pipe into the file out using \a fread and \a fwrite. These two + functions are used because of their primitive nature - they make + no assumptions about the data. A buffer is used in the transfer, + but the output of \a fread is stored so the exact number of bytes + is handled gracefully. + + At the very end (after the data has been copied) both of the files + are closed, and we return to what we were doing. +*/ +int Script::execute (const std::list &in_command, + const std::list &in_params, + const Glib::ustring &filein, + file_listener &fileout) +{ + g_return_val_if_fail(!in_command.empty(), 0); + + std::vector argv; + + bool interpreted = (in_command.size() == 2); + std::string program = in_command.front(); + std::string script = interpreted ? in_command.back() : ""; + std::string working_directory = ""; + + // We should always have an absolute path here: + // - For interpreted scripts, see Script::resolveInterpreterExecutable() + // - For "normal" scripts this should be done as part of the dependency checking, see Dependency::check() + if (!Glib::path_is_absolute(program)) { + g_critical("Script::execute(): Got unexpected relative path '%s'. Please report a bug.", program.c_str()); + return 0; + } + argv.push_back(program); + + if (interpreted) { + // On Windows, Python garbles Unicode command line parameters + // in an useless way. This means extensions fail when Inkscape + // is run from an Unicode directory. + // As a workaround, we set the working directory to the one + // containing the script. + working_directory = Glib::path_get_dirname(script); + script = Glib::path_get_basename(script); + argv.push_back(script); + } + + // assemble the rest of argv + std::copy(in_params.begin(), in_params.end(), std::back_inserter(argv)); + if (!filein.empty()) { + if(Glib::path_is_absolute(filein)) + argv.push_back(filein); + else { + std::vector buildargs; + buildargs.push_back(Glib::get_current_dir()); + buildargs.push_back(filein); + argv.push_back(Glib::build_filename(buildargs)); + } + } + + //for(int i=0;i(0), // no flags + sigc::slot(), + &_pid, // Pid + nullptr, // STDIN + &stdout_pipe, // STDOUT + &stderr_pipe); // STDERR + } catch (Glib::Error &e) { + g_critical("Script::execute(): failed to execute program '%s'.\n\tReason: %s", program.c_str(), e.what().data()); + return 0; + } + + // Create a new MainContext for the loop so that the original context sources are not run here, + // this enforces that only the file_listeners should be read in this new MainLoop + Glib::RefPtr _main_context = Glib::MainContext::create(); + _main_loop = Glib::MainLoop::create(_main_context, false); + + file_listener fileerr; + fileout.init(stdout_pipe, _main_loop); + fileerr.init(stderr_pipe, _main_loop); + + _canceled = false; + _main_loop->run(); + + // Ensure all the data is out of the pipe + while (!fileout.isDead()) { + fileout.read(Glib::IO_IN); + } + while (!fileerr.isDead()) { + fileerr.read(Glib::IO_IN); + } + + if (_canceled) { + // std::cout << "Script Canceled" << std::endl; + return 0; + } + + Glib::ustring stderr_data = fileerr.string(); + if (stderr_data.length() != 0 && + INKSCAPE.use_gui() + ) { + checkStderr(stderr_data, Gtk::MESSAGE_INFO, + _("Inkscape has received additional data from the script executed. " + "The script did not return an error, but this may indicate the results will not be as expected.")); + } + + Glib::ustring stdout_data = fileout.string(); + if (stdout_data.length() == 0) { + return 0; + } + + // std::cout << "Finishing Execution." << std::endl; + return stdout_data.length(); +} + + +void Script::file_listener::init(int fd, Glib::RefPtr main) { + _channel = Glib::IOChannel::create_from_fd(fd); + _channel->set_encoding(); + _conn = main->get_context()->signal_io().connect(sigc::mem_fun(*this, &file_listener::read), _channel, Glib::IO_IN | Glib::IO_HUP | Glib::IO_ERR); + _main_loop = main; + + return; +} + +bool Script::file_listener::read(Glib::IOCondition condition) { + if (condition != Glib::IO_IN) { + _main_loop->quit(); + return false; + } + + Glib::IOStatus status; + Glib::ustring out; + status = _channel->read_line(out); + _string += out; + + if (status != Glib::IO_STATUS_NORMAL) { + _main_loop->quit(); + _dead = true; + return false; + } + + return true; +} + +bool Script::file_listener::toFile(const Glib::ustring &name) { + try { + Glib::RefPtr stdout_file = Glib::IOChannel::create_from_file(name, "w"); + stdout_file->set_encoding(); + stdout_file->write(_string); + } catch (Glib::FileError &e) { + return false; + } + return true; +} + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/extension/implementation/script.h b/src/extension/implementation/script.h new file mode 100644 index 0000000..12e2974 --- /dev/null +++ b/src/extension/implementation/script.h @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code for handling extensions (i.e., scripts) + * + * Authors: + * Bryce Harrington + * Ted Gould + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN +#define INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN + +#include "implementation.h" +#include +#include +#include +#include +#include + +namespace Inkscape { +namespace XML { +class Node; +} // namespace XML + +namespace Extension { +namespace Implementation { + +/** + * Utility class used for loading and launching script extensions + */ +class Script : public Implementation { +public: + + Script(); + ~Script() override; + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + bool check(Inkscape::Extension::Extension *module) override; + + ImplementationDocumentCache * newDocCache(Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * view) override; + + Gtk::Widget *prefs_input(Inkscape::Extension::Input *module, gchar const *filename) override; + SPDocument *open(Inkscape::Extension::Input *module, gchar const *filename) override; + Gtk::Widget *prefs_output(Inkscape::Extension::Output *module) override; + void save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *doc, ImplementationDocumentCache * docCache) override; + bool cancelProcessing () override; + +private: + bool _canceled; + Glib::Pid _pid; + Glib::RefPtr _main_loop; + + /** + * The command that has been derived from + * the configuration file with appropriate directories + */ + std::list command; + + /** + * This is the extension that will be used + * as the helper to read in or write out the + * data + */ + Glib::ustring helper_extension; + + /** + * The window which should be considered as "parent window" of the script execution, + * e.g. when showin warning messages + * + * If set to NULL the main window of the currently active document is used. + */ + Gtk::Window *parent_window; + + void copy_doc(Inkscape::XML::Node * olddoc, Inkscape::XML::Node * newdoc); + void checkStderr (Glib::ustring const& filename, Gtk::MessageType type, Glib::ustring const& message); + + class file_listener { + Glib::ustring _string; + sigc::connection _conn; + Glib::RefPtr _channel; + Glib::RefPtr _main_loop; + bool _dead; + + public: + file_listener () : _dead(false) { }; + virtual ~file_listener () { + _conn.disconnect(); + }; + + bool isDead () { return _dead; } + void init(int fd, Glib::RefPtr main); + bool read(Glib::IOCondition condition); + Glib::ustring string () { return _string; }; + bool toFile(const Glib::ustring &name); + }; + + int execute (const std::list &in_command, + const std::list &in_params, + const Glib::ustring &filein, + file_listener &fileout); + + void pump_events(); + + /** \brief A definition of an interpreter, which can be specified + in the INX file, but we need to know what to call */ + struct interpreter_t { + std::string prefstring; /**< The preferences key that can override the default */ + std::vector defaultvals; /**< The default values to check if the preferences are wrong */ + }; + static const std::map interpreterTab; + + std::string resolveInterpreterExecutable(const Glib::ustring &interpNameArg); + +}; // class Script +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/implementation/xslt.cpp b/src/extension/implementation/xslt.cpp new file mode 100644 index 0000000..dced1dd --- /dev/null +++ b/src/extension/implementation/xslt.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Code for handling XSLT extensions. + */ +/* + * Authors: + * Ted Gould + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xslt.h" + +#include +#include + +#include +#include +#include + +#include "document.h" +#include "file.h" + +#include "extension/extension.h" +#include "extension/output.h" +#include "extension/input.h" + +#include "io/resource.h" + +#include "xml/node.h" +#include "xml/repr.h" + +#include + +Inkscape::XML::Document * sp_repr_do_read (xmlDocPtr doc, const gchar * default_ns); + +/* Namespaces */ +namespace Inkscape { +namespace Extension { +namespace Implementation { + +/* Real functions */ +/** + \return A XSLT object + \brief This function creates a XSLT object and sets up the + variables. + +*/ +XSLT::XSLT() : + Implementation(), + _filename(""), + _parsedDoc(nullptr), + _stylesheet(nullptr) +{ +} + +bool XSLT::check(Inkscape::Extension::Extension *module) +{ + if (load(module)) { + unload(module); + return true; + } else { + return false; + } +} + +bool XSLT::load(Inkscape::Extension::Extension *module) +{ + if (module->loaded()) { return true; } + + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "xslt")) { + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "file")) { + // TODO: we already parse xslt files as dependencies in extension.cpp + // can can we optimize this to be less indirect? + const char *filename = child_repr->firstChild()->content(); + _filename = module->get_dependency_location(filename); + } + child_repr = child_repr->next(); + } + + break; + } + child_repr = child_repr->next(); + } + + _parsedDoc = xmlParseFile(_filename.c_str()); + if (_parsedDoc == nullptr) { return false; } + + _stylesheet = xsltParseStylesheetDoc(_parsedDoc); + + return true; +} + +void XSLT::unload(Inkscape::Extension::Extension *module) +{ + if (!module->loaded()) { return; } + xsltFreeStylesheet(_stylesheet); + // No need to use xmlfreedoc(_parsedDoc), it's handled by xsltFreeStylesheet(_stylesheet); + return; +} + +SPDocument * XSLT::open(Inkscape::Extension::Input */*module*/, + gchar const *filename) +{ + xmlDocPtr filein = xmlParseFile(filename); + if (filein == nullptr) { return nullptr; } + + const char * params[1]; + params[0] = nullptr; + char *oldlocale = g_strdup(std::setlocale(LC_NUMERIC, nullptr)); + std::setlocale(LC_NUMERIC, "C"); + + xmlDocPtr result = xsltApplyStylesheet(_stylesheet, filein, params); + xmlFreeDoc(filein); + + Inkscape::XML::Document * rdoc = sp_repr_do_read( result, SP_SVG_NS_URI); + xmlFreeDoc(result); + std::setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + if (rdoc == nullptr) { + return nullptr; + } + + if (strcmp(rdoc->root()->name(), "svg:svg") != 0) { + return nullptr; + } + + gchar * base = nullptr; + gchar * name = nullptr; + gchar * s = nullptr, * p = nullptr; + s = g_strdup(filename); + p = strrchr(s, '/'); + if (p) { + name = g_strdup(p + 1); + p[1] = '\0'; + base = g_strdup(s); + } else { + base = nullptr; + name = g_strdup(filename); + } + g_free(s); + + SPDocument * doc = SPDocument::createDoc(rdoc, filename, base, name, true, nullptr); + + g_free(base); g_free(name); + + return doc; +} + +void XSLT::save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) +{ + /* TODO: Should we assume filename to be in utf8 or to be a raw filename? + * See JavaFXOutput::save for discussion. + * + * From JavaFXOutput::save (now removed): + * --- + * N.B. The name `filename_utf8' represents the fact that we want it to be in utf8; whereas in + * fact we know that some callers of Extension::save pass something in the filesystem's + * encoding, while others do g_filename_to_utf8 before calling. + * + * In terms of safety, it's best to make all callers pass actual filenames, since in general + * one can't round-trip from filename to utf8 back to the same filename. Whereas the argument + * for passing utf8 filenames is one of convenience: we often want to pass to g_warning or + * store as a string (rather than a byte stream) in XML, or the like. */ + g_return_if_fail(doc != nullptr); + g_return_if_fail(filename != nullptr); + + Inkscape::XML::Node *repr = doc->getReprRoot(); + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + if (!sp_repr_save_rebased_file(repr->document(), tempfilename_out.c_str(), SP_SVG_NS_URI, + doc->getDocumentBase(), filename)) { + throw Inkscape::Extension::Output::save_failed(); + } + + xmlDocPtr svgdoc = xmlParseFile(tempfilename_out.c_str()); + close(tempfd_out); + if (svgdoc == nullptr) { + return; + } + + std::list params; + module->paramListString(params); + const int max_parameters = params.size() * 2; + const char * xslt_params[max_parameters+1] ; + + int count = 0; + for(auto & param : params) { + std::size_t pos = param.find("="); + std::ostringstream parameter; + std::ostringstream value; + parameter << param.substr(2,pos-2); + value << param.substr(pos+1); + xslt_params[count++] = g_strdup_printf("%s", parameter.str().c_str()); + xslt_params[count++] = g_strdup_printf("'%s'", value.str().c_str()); + } + xslt_params[count] = nullptr; + + // workaround for inbox#2208 + char *oldlocale = g_strdup(std::setlocale(LC_NUMERIC, nullptr)); + std::setlocale(LC_NUMERIC, "C"); + xmlDocPtr newdoc = xsltApplyStylesheet(_stylesheet, svgdoc, xslt_params); + //xmlSaveFile(filename, newdoc); + int success = xsltSaveResultToFilename(filename, newdoc, _stylesheet, 0); + std::setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + xmlFreeDoc(newdoc); + xmlFreeDoc(svgdoc); + + xsltCleanupGlobals(); + xmlCleanupParser(); + + if (success < 1) { + throw Inkscape::Extension::Output::save_failed(); + } + + return; +} + + +} /* Implementation */ +} /* module */ +} /* Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/implementation/xslt.h b/src/extension/implementation/xslt.h new file mode 100644 index 0000000..745d7f5 --- /dev/null +++ b/src/extension/implementation/xslt.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code for handling XSLT extensions + * + * Authors: + * Ted Gould + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ +#define __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ + +#include "implementation.h" + +#include "libxml/tree.h" +#include "libxslt/xslt.h" +#include "libxslt/xsltInternals.h" + +namespace Inkscape { +namespace XML { +class Node; +} +} + + +namespace Inkscape { +namespace Extension { +namespace Implementation { + +class XSLT : public Implementation { +private: + std::string _filename; + xmlDocPtr _parsedDoc; + xsltStylesheetPtr _stylesheet; + +public: + XSLT (); + + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + + bool check(Inkscape::Extension::Extension *module) override; + + SPDocument *open(Inkscape::Extension::Input *module, + gchar const *filename) override; + void save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) override; +}; + +} /* Inkscape */ +} /* Extension */ +} /* Implementation */ +#endif /* __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/init.cpp b/src/extension/init.cpp new file mode 100644 index 0000000..5a95fb8 --- /dev/null +++ b/src/extension/init.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is what gets executed to initialize all of the modules. For + * the internal modules this involves executing their initialization + * functions, for external ones it involves reading their .spmodule + * files and bringing them into Sodipodi. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +# include "internal/pdfinput/pdf-input.h" +#endif + +#include "path-prefix.h" + +#include "inkscape.h" + +#include +#include +#include + +#include "system.h" +#include "db.h" +#include "internal/svgz.h" +# include "internal/emf-inout.h" +# include "internal/emf-print.h" +# include "internal/wmf-inout.h" +# include "internal/wmf-print.h" + +#include +#ifdef CAIRO_HAS_PDF_SURFACE +# include "internal/cairo-renderer-pdf-out.h" +#endif +#ifdef CAIRO_HAS_PS_SURFACE +# include "internal/cairo-ps-out.h" +#endif +#include "internal/pov-out.h" +#include "internal/odf.h" +#include "internal/latex-pstricks-out.h" +#include "internal/latex-pstricks.h" +#include "internal/gdkpixbuf-input.h" +#include "internal/bluredge.h" +#include "internal/gimpgrad.h" +#include "internal/grid.h" +#ifdef WITH_LIBWPG +#include "internal/wpg-input.h" +#endif +#ifdef WITH_LIBVISIO +#include "internal/vsd-input.h" +#endif +#ifdef WITH_LIBCDR +#include "internal/cdr-input.h" +#endif +#include "preferences.h" +#include "io/sys.h" +#include "io/resource.h" +#ifdef WITH_DBUS +#include "dbus/dbus-init.h" +#endif + +#ifdef WITH_MAGICK +#include +#include "internal/bitmap/adaptiveThreshold.h" +#include "internal/bitmap/addNoise.h" +#include "internal/bitmap/blur.h" +#include "internal/bitmap/channel.h" +#include "internal/bitmap/charcoal.h" +#include "internal/bitmap/colorize.h" +#include "internal/bitmap/contrast.h" +#include "internal/bitmap/crop.h" +#include "internal/bitmap/cycleColormap.h" +#include "internal/bitmap/despeckle.h" +#include "internal/bitmap/edge.h" +#include "internal/bitmap/emboss.h" +#include "internal/bitmap/enhance.h" +#include "internal/bitmap/equalize.h" +#include "internal/bitmap/gaussianBlur.h" +#include "internal/bitmap/implode.h" +#include "internal/bitmap/level.h" +#include "internal/bitmap/levelChannel.h" +#include "internal/bitmap/medianFilter.h" +#include "internal/bitmap/modulate.h" +#include "internal/bitmap/negate.h" +#include "internal/bitmap/normalize.h" +#include "internal/bitmap/oilPaint.h" +#include "internal/bitmap/opacity.h" +#include "internal/bitmap/raise.h" +#include "internal/bitmap/reduceNoise.h" +#include "internal/bitmap/sample.h" +#include "internal/bitmap/shade.h" +#include "internal/bitmap/sharpen.h" +#include "internal/bitmap/solarize.h" +#include "internal/bitmap/spread.h" +#include "internal/bitmap/swirl.h" +//#include "internal/bitmap/threshold.h" +#include "internal/bitmap/unsharpmask.h" +#include "internal/bitmap/wave.h" +#endif /* WITH_MAGICK */ + +#include "internal/filter/filter.h" + +#include "init.h" + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace Extension { + +/** This is the extension that all files are that are pulled from + the extension directory and parsed */ +#define SP_MODULE_EXTENSION "inx" + +static void check_extensions(); + +/** + * \return none + * \brief Examines the given string preference and checks to see + * that at least one of the registered extensions matches + * it. If not, a default is assigned. + * \param pref_path Preference path to update + * \param pref_default Default string to set + * \param extension_family List of extensions to search + */ +static void +update_pref(Glib::ustring const &pref_path, + gchar const *pref_default) +{ + Glib::ustring pref = Inkscape::Preferences::get()->getString(pref_path); + if (!Inkscape::Extension::db.get( pref.data() ) /*missing*/) { + Inkscape::Preferences::get()->setString(pref_path, pref_default); + } +} + +/** + * Invokes the init routines for internal modules. + * + * This should be a list of all the internal modules that need to initialized. This is just a + * convenient place to put them. + */ +void +init() +{ + /* TODO: Change to Internal */ + Internal::Svg::init(); + Internal::Svgz::init(); + +#ifdef CAIRO_HAS_PDF_SURFACE + Internal::CairoRendererPdfOutput::init(); +#endif +#ifdef CAIRO_HAS_PS_SURFACE + Internal::CairoPsOutput::init(); + Internal::CairoEpsOutput::init(); +#endif +#ifdef HAVE_POPPLER + Internal::PdfInput::init(); +#endif + Internal::PrintEmf::init(); + Internal::Emf::init(); + Internal::PrintWmf::init(); + Internal::Wmf::init(); + Internal::PovOutput::init(); + Internal::OdfOutput::init(); + Internal::PrintLatex::init(); + Internal::LatexOutput::init(); +#ifdef WITH_LIBWPG + Internal::WpgInput::init(); +#endif +#ifdef WITH_LIBVISIO + Internal::VsdInput::init(); +#endif +#ifdef WITH_LIBCDR + Internal::CdrInput::init(); +#endif + + /* Effects */ + Internal::BlurEdge::init(); + Internal::GimpGrad::init(); + Internal::Grid::init(); + +#ifdef WITH_DBUS + Dbus::init(); +#endif + + /* Raster Effects */ +#ifdef WITH_MAGICK + Magick::InitializeMagick(NULL); + + Internal::Bitmap::AdaptiveThreshold::init(); + Internal::Bitmap::AddNoise::init(); + Internal::Bitmap::Blur::init(); + Internal::Bitmap::Channel::init(); + Internal::Bitmap::Charcoal::init(); + Internal::Bitmap::Colorize::init(); + Internal::Bitmap::Contrast::init(); + Internal::Bitmap::Crop::init(); + Internal::Bitmap::CycleColormap::init(); + Internal::Bitmap::Edge::init(); + Internal::Bitmap::Despeckle::init(); + Internal::Bitmap::Emboss::init(); + Internal::Bitmap::Enhance::init(); + Internal::Bitmap::Equalize::init(); + Internal::Bitmap::GaussianBlur::init(); + Internal::Bitmap::Implode::init(); + Internal::Bitmap::Level::init(); + Internal::Bitmap::LevelChannel::init(); + Internal::Bitmap::MedianFilter::init(); + Internal::Bitmap::Modulate::init(); + Internal::Bitmap::Negate::init(); + Internal::Bitmap::Normalize::init(); + Internal::Bitmap::OilPaint::init(); + Internal::Bitmap::Opacity::init(); + Internal::Bitmap::Raise::init(); + Internal::Bitmap::ReduceNoise::init(); + Internal::Bitmap::Sample::init(); + Internal::Bitmap::Shade::init(); + Internal::Bitmap::Sharpen::init(); + Internal::Bitmap::Solarize::init(); + Internal::Bitmap::Spread::init(); + Internal::Bitmap::Swirl::init(); + //Internal::Bitmap::Threshold::init(); + Internal::Bitmap::Unsharpmask::init(); + Internal::Bitmap::Wave::init(); +#endif /* WITH_MAGICK */ + + Internal::Filter::Filter::filters_all(); + + for(auto &filename: get_filenames(EXTENSIONS, {SP_MODULE_EXTENSION})) { + build_from_file(filename.c_str()); + } + + /* this is at the very end because it has several catch-alls + * that are possibly over-ridden by other extensions (such as + * svgz) + */ + Internal::GdkpixbufInput::init(); + + /* now we need to check and make sure everyone is happy */ + check_extensions(); + + /* This is a hack to deal with updating saved outdated module + * names in the prefs... + */ + update_pref("/dialogs/save_as/default", + SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE + // Inkscape::Extension::db.get_output_list() + ); +} + +static void +check_extensions_internal(Extension *in_plug, gpointer in_data) +{ + int *count = (int *)in_data; + + if (in_plug == nullptr) return; + if (!in_plug->deactivated() && !in_plug->check()) { + in_plug->deactivate(); + (*count)++; + } +} + +static void check_extensions() +{ + int count = 1; + + Inkscape::Extension::Extension::error_file_open(); + while (count != 0) { + count = 0; + db.foreach(check_extensions_internal, (gpointer)&count); + } + Inkscape::Extension::Extension::error_file_close(); +} + +} } /* namespace Inkscape::Extension */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/init.h b/src/extension/init.h new file mode 100644 index 0000000..328ee89 --- /dev/null +++ b/src/extension/init.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is what gets executed to initialize all of the modules. For + * the internal modules this invovles executing their initialization + * functions, for external ones it involves reading their .spmodule + * files and bringing them into Sodipodi. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_INIT_H__ +#define INKSCAPE_EXTENSION_INIT_H__ + +namespace Inkscape { +namespace Extension { + +void init (); + +} } /* namespace Inkscape::Extension */ + +#endif /* INKSCAPE_EXTENSION_INIT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/input.cpp b/src/extension/input.cpp new file mode 100644 index 0000000..cf02c27 --- /dev/null +++ b/src/extension/input.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "input.h" + +#include "timer.h" + +#include "implementation/implementation.h" + +#include "prefdialog/prefdialog.h" + +#include "xml/repr.h" + + +/* Inkscape::Extension::Input */ + +namespace Inkscape { +namespace Extension { + +/** + \return None + \brief Builds a SPModuleInput object from a XML description + \param module The module to be initialized + \param repr The XML description in a Inkscape::XML::Node tree + + Okay, so you want to build a SPModuleInput object. + + This function first takes and does the build of the parent class, + which is SPModule. Then, it looks for the section of the + XML description. Under there should be several fields which + describe the input module to excruciating detail. Those are parsed, + copied, and put into the structure that is passed in as module. + Overall, there are many levels of indentation, just to handle the + levels of indentation in the XML file. +*/ +Input::Input (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + mimetype = nullptr; + extension = nullptr; + filetypename = nullptr; + filetypetooltip = nullptr; + output_extension = nullptr; + + if (repr != nullptr) { + Inkscape::XML::Node * child_repr; + + child_repr = repr->firstChild(); + + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "input")) { + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + char const * chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') /* Allow _ for translation of tags */ + chname++; + if (!strcmp(chname, "extension")) { + g_free (extension); + extension = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "mimetype")) { + g_free (mimetype); + mimetype = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypename")) { + g_free (filetypename); + filetypename = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypetooltip")) { + g_free (filetypetooltip); + filetypetooltip = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "output_extension")) { + g_free (output_extension); + output_extension = g_strdup(child_repr->firstChild()->content()); + } + + child_repr = child_repr->next(); + } + + break; + } + + child_repr = child_repr->next(); + } + + } + + return; +} + +/** + \return None + \brief Destroys an Input extension +*/ +Input::~Input () +{ + g_free(mimetype); + g_free(extension); + g_free(filetypename); + g_free(filetypetooltip); + g_free(output_extension); + return; +} + +/** + \return Whether this extension checks out + \brief Validate this extension + + This function checks to make sure that the input extension has + a filename extension and a MIME type. Then it calls the parent + class' check function which also checks out the implementation. +*/ +bool +Input::check () +{ + if (extension == nullptr) + return FALSE; + if (mimetype == nullptr) + return FALSE; + + return Extension::check(); +} + +/** + \return A new document + \brief This function creates a document from a file + \param uri The filename to create the document from + + This function acts as the first step in creating a new document + from a file. The first thing that this does is make sure that the + file actually exists. If it doesn't, a NULL is returned. If the + file exits, then it is opened using the implementation of this extension. +*/ +SPDocument * +Input::open (const gchar *uri) +{ + if (!loaded()) { + set_state(Extension::STATE_LOADED); + } + if (!loaded()) { + return nullptr; + } + timer->touch(); + + SPDocument *const doc = imp->open(this, uri); + + return doc; +} + +/** + \return IETF mime-type for the extension + \brief Get the mime-type that describes this extension +*/ +gchar * +Input::get_mimetype() +{ + return mimetype; +} + +/** + \return Filename extension for the extension + \brief Get the filename extension for this extension +*/ +gchar * +Input::get_extension() +{ + return extension; +} + +/** + \return The name of the filetype supported + \brief Get the name of the filetype supported +*/ +const char * +Input::get_filetypename(bool translated) +{ + const char *name; + + if (filetypename) + name = filetypename; + else + name = get_name(); + + if (name && translated) { + return get_translation(name); + } else { + return name; + } +} + +/** + \return Tooltip giving more information on the filetype + \brief Get the tooltip for more information on the filetype +*/ +const char * +Input::get_filetypetooltip(bool translated) +{ + if (filetypetooltip && translated) { + return get_translation(filetypetooltip); + } else { + return filetypetooltip; + } +} + +/** + \return A dialog to get settings for this extension + \brief Create a dialog for preference for this extension + + Calls the implementation to get the preferences. +*/ +bool +Input::prefs (const gchar *uri) +{ + if (!loaded()) { + set_state(Extension::STATE_LOADED); + } + if (!loaded()) { + return false; + } + + Gtk::Widget * controls; + controls = imp->prefs_input(this, uri); + if (controls == nullptr) { + // std::cout << "No preferences for Input" << std::endl; + return true; + } + + Glib::ustring name = get_translation(this->get_name()); + PrefDialog *dialog = new PrefDialog(name, controls); + int response = dialog->run(); + dialog->hide(); + + delete dialog; + + return (response == Gtk::RESPONSE_OK); +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/input.h b/src/extension/input.h new file mode 100644 index 0000000..5952c3c --- /dev/null +++ b/src/extension/input.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_EXTENSION_INPUT_H__ +#define INKSCAPE_EXTENSION_INPUT_H__ + +#include +#include +#include "extension.h" + +class SPDocument; + +namespace Inkscape { +namespace Extension { + +class Input : public Extension { + gchar *mimetype; /**< What is the mime type this inputs? */ + gchar *extension; /**< The extension of the input files */ + gchar *filetypename; /**< A userfriendly name for the file type */ + gchar *filetypetooltip; /**< A more detailed description of the filetype */ + +public: /* this is a hack for this release, this will be private shortly */ + gchar *output_extension; /**< Setting of what output extension should be used */ + +public: + struct open_failed : public std::exception { + ~open_failed() noexcept override = default; + const char *what() const noexcept override { return "Open failed"; } + }; + struct no_extension_found : public std::exception { + ~no_extension_found() noexcept override = default; + const char *what() const noexcept override { return "No suitable input extension found"; } + }; + struct open_cancelled : public std::exception { + ~open_cancelled() noexcept override = default; + const char *what() const noexcept override { return "Open was cancelled"; } + }; + + Input(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Input() override; + + bool check() override; + + SPDocument * open (gchar const *uri); + gchar * get_mimetype (); + gchar * get_extension (); + const char * get_filetypename (bool translated=false); + const char * get_filetypetooltip (bool translated=false); + bool prefs (gchar const *uri); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_INPUT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/bitmap/adaptiveThreshold.cpp b/src/extension/internal/bitmap/adaptiveThreshold.cpp new file mode 100644 index 0000000..ce4d78a --- /dev/null +++ b/src/extension/internal/bitmap/adaptiveThreshold.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "adaptiveThreshold.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +AdaptiveThreshold::applyEffect(Magick::Image *image) { + image->adaptiveThreshold(_width, _height); +} + +void +AdaptiveThreshold::refreshParameters(Inkscape::Extension::Effect *module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); + _offset = module->get_param_int("offset"); +} + +#include "../clear-n_.h" + +void +AdaptiveThreshold::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Adaptive Threshold") "\n" + "org.inkscape.effect.bitmap.adaptiveThreshold\n" + "5\n" + "5\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Apply adaptive thresholding to selected bitmap(s)") "\n" + "\n" + "\n", new AdaptiveThreshold()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/adaptiveThreshold.h b/src/extension/internal/bitmap/adaptiveThreshold.h new file mode 100644 index 0000000..066f13b --- /dev/null +++ b/src/extension/internal/bitmap/adaptiveThreshold.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class AdaptiveThreshold : public ImageMagick +{ +private: + unsigned int _width; + unsigned int _height; + unsigned _offset; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/addNoise.cpp b/src/extension/internal/bitmap/addNoise.cpp new file mode 100644 index 0000000..26b050f --- /dev/null +++ b/src/extension/internal/bitmap/addNoise.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "addNoise.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +AddNoise::applyEffect(Magick::Image *image) { + Magick::NoiseType noiseType = Magick::UniformNoise; + if (!strcmp(_noiseTypeName, "Uniform Noise")) noiseType = Magick::UniformNoise; + else if (!strcmp(_noiseTypeName, "Gaussian Noise")) noiseType = Magick::GaussianNoise; + else if (!strcmp(_noiseTypeName, "Multiplicative Gaussian Noise")) noiseType = Magick::MultiplicativeGaussianNoise; + else if (!strcmp(_noiseTypeName, "Impulse Noise")) noiseType = Magick::ImpulseNoise; + else if (!strcmp(_noiseTypeName, "Laplacian Noise")) noiseType = Magick::LaplacianNoise; + else if (!strcmp(_noiseTypeName, "Poisson Noise")) noiseType = Magick::PoissonNoise; + + image->addNoise(noiseType); +} + +void +AddNoise::refreshParameters(Inkscape::Extension::Effect *module) { + _noiseTypeName = module->get_param_optiongroup("noiseType"); +} + +#include "../clear-n_.h" + +void +AddNoise::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Add Noise") "\n" + "org.inkscape.effect.bitmap.addNoise\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Add random noise to selected bitmap(s)") "\n" + "\n" + "\n", new AddNoise()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/addNoise.h b/src/extension/internal/bitmap/addNoise.h new file mode 100644 index 0000000..06ce8c3 --- /dev/null +++ b/src/extension/internal/bitmap/addNoise.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class AddNoise : public ImageMagick +{ +private: + const gchar* _noiseTypeName; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/blur.cpp b/src/extension/internal/bitmap/blur.cpp new file mode 100644 index 0000000..b50511c --- /dev/null +++ b/src/extension/internal/bitmap/blur.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "blur.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Blur::applyEffect(Magick::Image *image) { + image->blur(_radius, _sigma); +} + +void +Blur::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Blur::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Blur") "\n" + "org.inkscape.effect.bitmap.blur\n" + "1\n" + "0.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Blur selected bitmap(s)") "\n" + "\n" + "\n", new Blur()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/blur.h b/src/extension/internal/bitmap/blur.h new file mode 100644 index 0000000..da93a42 --- /dev/null +++ b/src/extension/internal/bitmap/blur.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Blur : public ImageMagick +{ +private: + float _radius; + float _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/channel.cpp b/src/extension/internal/bitmap/channel.cpp new file mode 100644 index 0000000..f26a9f9 --- /dev/null +++ b/src/extension/internal/bitmap/channel.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "channel.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Channel::applyEffect(Magick::Image *image) { + Magick::ChannelType layer = Magick::UndefinedChannel; + if (!strcmp(_layerName, "Red Channel")) layer = Magick::RedChannel; + else if (!strcmp(_layerName, "Green Channel")) layer = Magick::GreenChannel; + else if (!strcmp(_layerName, "Blue Channel")) layer = Magick::BlueChannel; + else if (!strcmp(_layerName, "Cyan Channel")) layer = Magick::CyanChannel; + else if (!strcmp(_layerName, "Magenta Channel")) layer = Magick::MagentaChannel; + else if (!strcmp(_layerName, "Yellow Channel")) layer = Magick::YellowChannel; + else if (!strcmp(_layerName, "Black Channel")) layer = Magick::BlackChannel; + else if (!strcmp(_layerName, "Opacity Channel")) layer = Magick::OpacityChannel; + else if (!strcmp(_layerName, "Matte Channel")) layer = Magick::MatteChannel; + + image->channel(layer); +} + +void +Channel::refreshParameters(Inkscape::Extension::Effect *module) { + _layerName = module->get_param_optiongroup("layer"); +} + +#include "../clear-n_.h" + +void +Channel::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Channel") "\n" + "org.inkscape.effect.bitmap.channel\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Extract specific channel from image") "\n" + "\n" + "\n", new Channel()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/channel.h b/src/extension/internal/bitmap/channel.h new file mode 100644 index 0000000..e215344 --- /dev/null +++ b/src/extension/internal/bitmap/channel.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Channel : public ImageMagick { + +private: + const gchar * _layerName; + +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/charcoal.cpp b/src/extension/internal/bitmap/charcoal.cpp new file mode 100644 index 0000000..4da2cba --- /dev/null +++ b/src/extension/internal/bitmap/charcoal.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "charcoal.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Charcoal::applyEffect(Magick::Image* image) { + image->charcoal(_radius, _sigma); +} + +void +Charcoal::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Charcoal::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Charcoal") "\n" + "org.inkscape.effect.bitmap.charcoal\n" + "1\n" + "0.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Apply charcoal stylization to selected bitmap(s)") "\n" + "\n" + "\n", new Charcoal()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/charcoal.h b/src/extension/internal/bitmap/charcoal.h new file mode 100644 index 0000000..c225746 --- /dev/null +++ b/src/extension/internal/bitmap/charcoal.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Charcoal : public ImageMagick +{ +private: + float _radius; + float _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/colorize.cpp b/src/extension/internal/bitmap/colorize.cpp new file mode 100644 index 0000000..2650909 --- /dev/null +++ b/src/extension/internal/bitmap/colorize.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown + * Ted Gould + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "colorize.h" + +#include "color.h" + +#include +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Colorize::applyEffect(Magick::Image *image) { + float r = ((_color >> 24) & 0xff) / 255.0F; + float g = ((_color >> 16) & 0xff) / 255.0F; + float b = ((_color >> 8) & 0xff) / 255.0F; + float a = ((_color ) & 0xff) / 255.0F; + + Magick::ColorRGB mc(r,g,b); + + image->colorize(a * 100, mc); +} + +void +Colorize::refreshParameters(Inkscape::Extension::Effect *module) { + _color = module->get_param_color("color"); +} + +#include "../clear-n_.h" + +void +Colorize::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Colorize") "\n" + "org.inkscape.effect.bitmap.colorize\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Colorize selected bitmap(s) with specified color, using given opacity") "\n" + "\n" + "\n", new Colorize()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/colorize.h b/src/extension/internal/bitmap/colorize.h new file mode 100644 index 0000000..7c16ef5 --- /dev/null +++ b/src/extension/internal/bitmap/colorize.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown + * Ted Gould + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Colorize : public ImageMagick { +private: + unsigned int _opacity; + guint32 _color; + +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/contrast.cpp b/src/extension/internal/bitmap/contrast.cpp new file mode 100644 index 0000000..64d2167 --- /dev/null +++ b/src/extension/internal/bitmap/contrast.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "contrast.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Contrast::applyEffect(Magick::Image *image) { + // the contrast method's argument seems to be binary, so we perform it multiple times + // to get the desired level of effect + for (unsigned int i = 0; i < _sharpen; i ++) + image->contrast(1); +} + +void +Contrast::refreshParameters(Inkscape::Extension::Effect *module) { + _sharpen = module->get_param_int("sharpen"); +} + +#include "../clear-n_.h" + +void +Contrast::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Contrast") "\n" + "org.inkscape.effect.bitmap.contrast\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Increase or decrease contrast in bitmap(s)") "\n" + "\n" + "\n", new Contrast()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/contrast.h b/src/extension/internal/bitmap/contrast.h new file mode 100644 index 0000000..c0e95e4 --- /dev/null +++ b/src/extension/internal/bitmap/contrast.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Contrast : public ImageMagick +{ +private: + unsigned int _sharpen; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/crop.cpp b/src/extension/internal/bitmap/crop.cpp new file mode 100644 index 0000000..c69b402 --- /dev/null +++ b/src/extension/internal/bitmap/crop.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2011 Authors: + * Nicolas Dufour + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "2geom/transforms.h" +#include "extension/effect.h" +#include "extension/system.h" + +#include "crop.h" +#include "selection-chemistry.h" +#include "object/sp-item.h" +#include "object/sp-item-transform.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Crop::applyEffect(Magick::Image *image) { + int width = image->baseColumns() - (_left + _right); + int height = image->baseRows() - (_top + _bottom); + if (width > 0 and height > 0) { + image->crop(Magick::Geometry(width, height, _left, _top, false, false)); + image->page("+0+0"); + } +} + +void +Crop::postEffect(Magick::Image *image, SPItem *item) { + + // Scale bbox + Geom::Scale scale (0,0); + scale = Geom::Scale(image->columns() / (double) image->baseColumns(), + image->rows() / (double) image->baseRows()); + item->scale_rel(scale); + + // Translate proportionaly to the image/bbox ratio + Geom::OptRect bbox(item->desktopGeometricBounds()); + //g_warning("bbox. W:%f, H:%f, X:%f, Y:%f", bbox->dimensions()[Geom::X], bbox->dimensions()[Geom::Y], bbox->min()[Geom::X], bbox->min()[Geom::Y]); + + Geom::Translate translate (0,0); + translate = Geom::Translate(((_left - _right) / 2.0) * (bbox->dimensions()[Geom::X] / (double) image->columns()), + ((_bottom - _top) / 2.0) * (bbox->dimensions()[Geom::Y] / (double) image->rows())); + item->move_rel(translate); +} + +void +Crop::refreshParameters(Inkscape::Extension::Effect *module) { + _top = module->get_param_int("top"); + _bottom = module->get_param_int("bottom"); + _left = module->get_param_int("left"); + _right = module->get_param_int("right"); +} + +#include "../clear-n_.h" + +void +Crop::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Crop") "\n" + "org.inkscape.effect.bitmap.crop\n" + "0\n" + "0\n" + "0\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Crop selected bitmap(s)") "\n" + "\n" + "\n", new Crop()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/crop.h b/src/extension/internal/bitmap/crop.h new file mode 100644 index 0000000..da53878 --- /dev/null +++ b/src/extension/internal/bitmap/crop.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2010 Authors: + * Christopher Brown + * Ted Gould + * Nicolas Dufour + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Crop : public ImageMagick +{ +private: + int _top; + int _bottom; + int _left; + int _right; +public: + void applyEffect(Magick::Image *image) override; + void postEffect(Magick::Image *image, SPItem *item) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/cycleColormap.cpp b/src/extension/internal/bitmap/cycleColormap.cpp new file mode 100644 index 0000000..04741f9 --- /dev/null +++ b/src/extension/internal/bitmap/cycleColormap.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "cycleColormap.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +CycleColormap::applyEffect(Magick::Image *image) { + image->cycleColormap(_amount); +} + +void +CycleColormap::refreshParameters(Inkscape::Extension::Effect *module) { + _amount = module->get_param_int("amount"); +} + +#include "../clear-n_.h" + +void +CycleColormap::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Cycle Colormap") "\n" + "org.inkscape.effect.bitmap.cycleColormap\n" + "180\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Cycle colormap(s) of selected bitmap(s)") "\n" + "\n" + "\n", new CycleColormap()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/cycleColormap.h b/src/extension/internal/bitmap/cycleColormap.h new file mode 100644 index 0000000..0d66b15 --- /dev/null +++ b/src/extension/internal/bitmap/cycleColormap.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class CycleColormap : public ImageMagick { +private: + int _amount; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/despeckle.cpp b/src/extension/internal/bitmap/despeckle.cpp new file mode 100644 index 0000000..7a3069d --- /dev/null +++ b/src/extension/internal/bitmap/despeckle.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "despeckle.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Despeckle::applyEffect(Magick::Image *image) { + image->despeckle(); +} + +void +Despeckle::refreshParameters(Inkscape::Extension::Effect */*module*/) { +} + +#include "../clear-n_.h" + +void +Despeckle::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Despeckle") "\n" + "org.inkscape.effect.bitmap.despeckle\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Reduce speckle noise of selected bitmap(s)") "\n" + "\n" + "\n", new Despeckle()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/despeckle.h b/src/extension/internal/bitmap/despeckle.h new file mode 100644 index 0000000..0c731ee --- /dev/null +++ b/src/extension/internal/bitmap/despeckle.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Despeckle : public ImageMagick { +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/edge.cpp b/src/extension/internal/bitmap/edge.cpp new file mode 100644 index 0000000..58bceec --- /dev/null +++ b/src/extension/internal/bitmap/edge.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "edge.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Edge::applyEffect(Magick::Image *image) { + image->edge(_radius); +} + +void +Edge::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_int("radius"); +} + +#include "../clear-n_.h" + +void +Edge::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Edge") "\n" + "org.inkscape.effect.bitmap.edge\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Highlight edges of selected bitmap(s)") "\n" + "\n" + "\n", new Edge()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/edge.h b/src/extension/internal/bitmap/edge.h new file mode 100644 index 0000000..2c5fe14 --- /dev/null +++ b/src/extension/internal/bitmap/edge.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Edge : public ImageMagick { +private: + unsigned int _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/emboss.cpp b/src/extension/internal/bitmap/emboss.cpp new file mode 100644 index 0000000..b7f2235 --- /dev/null +++ b/src/extension/internal/bitmap/emboss.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "emboss.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Emboss::applyEffect(Magick::Image *image) { + image->emboss(_radius, _sigma); +} + +void +Emboss::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Emboss::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Emboss") "\n" + "org.inkscape.effect.bitmap.emboss\n" + "1.0\n" + "0.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Emboss selected bitmap(s); highlight edges with 3D effect") "\n" + "\n" + "\n", new Emboss()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/emboss.h b/src/extension/internal/bitmap/emboss.h new file mode 100644 index 0000000..18b91bb --- /dev/null +++ b/src/extension/internal/bitmap/emboss.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Emboss : public ImageMagick +{ +private: + float _radius; + float _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/enhance.cpp b/src/extension/internal/bitmap/enhance.cpp new file mode 100644 index 0000000..aff9faf --- /dev/null +++ b/src/extension/internal/bitmap/enhance.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "enhance.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Enhance::applyEffect(Magick::Image *image) { + image->enhance(); +} + +void +Enhance::refreshParameters(Inkscape::Extension::Effect */*module*/) { } + +#include "../clear-n_.h" + +void +Enhance::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Enhance") "\n" + "org.inkscape.effect.bitmap.enhance\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Enhance selected bitmap(s); minimize noise") "\n" + "\n" + "\n", new Enhance()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/enhance.h b/src/extension/internal/bitmap/enhance.h new file mode 100644 index 0000000..dd3d9ff --- /dev/null +++ b/src/extension/internal/bitmap/enhance.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Enhance : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/equalize.cpp b/src/extension/internal/bitmap/equalize.cpp new file mode 100644 index 0000000..423b88b --- /dev/null +++ b/src/extension/internal/bitmap/equalize.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "equalize.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Equalize::applyEffect(Magick::Image *image) { + image->equalize(); +} + +void +Equalize::refreshParameters(Inkscape::Extension::Effect */*module*/) { } + +#include "../clear-n_.h" + +void +Equalize::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Equalize") "\n" + "org.inkscape.effect.bitmap.equalize\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Equalize selected bitmap(s); histogram equalization") "\n" + "\n" + "\n", new Equalize()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/equalize.h b/src/extension/internal/bitmap/equalize.h new file mode 100644 index 0000000..8259ffb --- /dev/null +++ b/src/extension/internal/bitmap/equalize.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Equalize : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/gaussianBlur.cpp b/src/extension/internal/bitmap/gaussianBlur.cpp new file mode 100644 index 0000000..405ee66 --- /dev/null +++ b/src/extension/internal/bitmap/gaussianBlur.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "gaussianBlur.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +GaussianBlur::applyEffect(Magick::Image* image) { + image->gaussianBlur(_width, _sigma); +} + +void +GaussianBlur::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_float("width"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +GaussianBlur::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Gaussian Blur") "\n" + "org.inkscape.effect.bitmap.gaussianBlur\n" + "5.0\n" + "5.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Gaussian blur selected bitmap(s)") "\n" + "\n" + "\n", new GaussianBlur()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/gaussianBlur.h b/src/extension/internal/bitmap/gaussianBlur.h new file mode 100644 index 0000000..3cd3fb8 --- /dev/null +++ b/src/extension/internal/bitmap/gaussianBlur.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class GaussianBlur : public ImageMagick +{ +private: + float _width; + float _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/imagemagick.cpp b/src/extension/internal/bitmap/imagemagick.cpp new file mode 100644 index 0000000..47940a5 --- /dev/null +++ b/src/extension/internal/bitmap/imagemagick.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown + * Ted Gould + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include + +#include + +#include "desktop.h" + +#include "selection.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "imagemagick.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class ImageMagickDocCache: public Inkscape::Extension::Implementation::ImplementationDocumentCache { + friend class ImageMagick; +private: + void readImage(char const *xlink, char const *id, Magick::Image *image); +protected: + Inkscape::XML::Node** _nodes; + + Magick::Image** _images; + int _imageCount; + char** _caches; + unsigned* _cacheLengths; + const char** _originals; + SPItem** _imageItems; +public: + ImageMagickDocCache(Inkscape::UI::View::View * view); + ~ImageMagickDocCache ( ) override; +}; + +ImageMagickDocCache::ImageMagickDocCache(Inkscape::UI::View::View * view) : + Inkscape::Extension::Implementation::ImplementationDocumentCache(view), + _nodes(NULL), + _images(NULL), + _imageCount(0), + _caches(NULL), + _cacheLengths(NULL), + _originals(NULL), + _imageItems(NULL) +{ + SPDesktop *desktop = (SPDesktop*)view; + auto selectedItemList = desktop->selection->items(); + int selectCount = (int) boost::distance(selectedItemList); + + // Init the data-holders + _nodes = new Inkscape::XML::Node*[selectCount]; + _originals = new const char*[selectCount]; + _caches = new char*[selectCount]; + _cacheLengths = new unsigned int[selectCount]; + _images = new Magick::Image*[selectCount]; + _imageCount = 0; + _imageItems = new SPItem*[selectCount]; + + // Loop through selected items + for (auto i = selectedItemList.begin(); i != selectedItemList.end(); ++i) { + SPItem *item = *i; + Inkscape::XML::Node *node = reinterpret_cast(item->getRepr()); + if (!strcmp(node->name(), "image") || !strcmp(node->name(), "svg:image")) + { + _nodes[_imageCount] = node; + char const *xlink = node->attribute("xlink:href"); + char const *id = node->attribute("id"); + _originals[_imageCount] = xlink; + _caches[_imageCount] = (char*)""; + _cacheLengths[_imageCount] = 0; + _images[_imageCount] = new Magick::Image(); + readImage(xlink, id, _images[_imageCount]); + _imageItems[_imageCount] = item; + _imageCount++; + } + } +} + +ImageMagickDocCache::~ImageMagickDocCache ( ) { + if (_nodes) + delete _nodes; + if (_originals) + delete _originals; + if (_caches) + delete _caches; + if (_cacheLengths) + delete _cacheLengths; + if (_images) + delete _images; + if (_imageItems) + delete _imageItems; + return; +} + +void +ImageMagickDocCache::readImage(const char *xlink, const char *id, Magick::Image *image) +{ + // Find if the xlink:href is base64 data, i.e. if the image is embedded + gchar *search = g_strndup(xlink, 30); + if (strstr(search, "base64") != (char*)NULL) { + // 7 = strlen("base64") + strlen(",") + const char* pureBase64 = strstr(xlink, "base64") + 7; + Magick::Blob blob; + blob.base64(pureBase64); + try { + image->read(blob); + } catch (Magick::Exception &error_) { + g_warning("ImageMagick could not read '%s'\nDetails: %s", id, error_.what()); + } + } else { + gchar *path; + if (strncmp (xlink,"file:", 5) == 0) { + path = g_filename_from_uri(xlink, NULL, NULL); + } else { + path = g_strdup(xlink); + } + try { + image->read(path); + } catch (Magick::Exception &error_) { + g_warning("ImageMagick could not read '%s' from '%s'\nDetails: %s", id, path, error_.what()); + } + g_free(path); + } + g_free(search); +} + +bool +ImageMagick::load(Inkscape::Extension::Extension */*module*/) +{ + return true; +} + +Inkscape::Extension::Implementation::ImplementationDocumentCache * +ImageMagick::newDocCache (Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * view) { + return new ImageMagickDocCache(view); +} + +void +ImageMagick::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) +{ + refreshParameters(module); + + if (docCache == NULL) { // should never happen + docCache = newDocCache(module, document); + } + ImageMagickDocCache * dc = dynamic_cast(docCache); + if (dc == NULL) { // should really never happen + printf("AHHHHHHHHH!!!!!"); + exit(1); + } + + for (int i = 0; i < dc->_imageCount; i++) + { + try + { + Magick::Image effectedImage = *dc->_images[i]; // make a copy + + applyEffect(&effectedImage); + + // postEffect can be used to change things on the item itself + // e.g. resize the image element, after the effecti is applied + postEffect(&effectedImage, dc->_imageItems[i]); + +// dc->_nodes[i]->setAttribute("xlink:href", dc->_caches[i], true); + + Magick::Blob *blob = new Magick::Blob(); + effectedImage.write(blob); + + std::string raw_string = blob->base64(); + const int raw_len = raw_string.length(); + const char *raw_i = raw_string.c_str(); + + unsigned new_len = (int)(raw_len * (77.0 / 76.0) + 100); + if (new_len > dc->_cacheLengths[i]) { + dc->_cacheLengths[i] = (int)(new_len * 1.2); + dc->_caches[i] = new char[dc->_cacheLengths[i]]; + } + char *formatted_i = dc->_caches[i]; + const char *src; + + for (src = "data:image/"; *src; ) + *formatted_i++ = *src++; + for (src = effectedImage.magick().c_str(); *src ; ) + *formatted_i++ = *src++; + for (src = ";base64, \n" ; *src; ) + *formatted_i++ = *src++; + + int col = 0; + while (*raw_i) { + *formatted_i++ = *raw_i++; + if (col++ > 76) { + *formatted_i++ = '\n'; + col = 0; + } + } + if (col) { + *formatted_i++ = '\n'; + } + *formatted_i = '\0'; + + dc->_nodes[i]->setAttribute("xlink:href", dc->_caches[i], true); + dc->_nodes[i]->removeAttribute("sodipodi:absref"); + delete blob; + } + catch (Magick::Exception &error_) { + printf("Caught exception: %s \n", error_.what()); + } + + //while(Gtk::Main::events_pending()) { + // Gtk::Main::iteration(); + //} + } +} + +/** \brief A function to get the prefences for the grid + \param moudule Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +ImageMagick::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = NULL; + if (!selected.empty()) { + first_select = (selected.front())->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/imagemagick.h b/src/extension/internal/bitmap/imagemagick.h new file mode 100644 index 0000000..754e195 --- /dev/null +++ b/src/extension/internal/bitmap/imagemagick.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H +#define INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H + +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +class SPItem; + +namespace Magick { +class Image; +} + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { +namespace Bitmap { + +class ImageMagick : public Inkscape::Extension::Implementation::Implementation { +public: + /* Functions to be implemented by subclasses */ + virtual void applyEffect(Magick::Image */*image*/) { }; + virtual void refreshParameters(Inkscape::Extension::Effect */*module*/) { }; + virtual void postEffect(Magick::Image */*image*/, SPItem */*item*/) { }; + + /* Functions implemented from ::Implementation */ + bool load(Inkscape::Extension::Extension *module) override; + Inkscape::Extension::Implementation::ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * doc) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget* prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +#endif // INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H diff --git a/src/extension/internal/bitmap/implode.cpp b/src/extension/internal/bitmap/implode.cpp new file mode 100644 index 0000000..c6c7a5f --- /dev/null +++ b/src/extension/internal/bitmap/implode.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "implode.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Implode::applyEffect(Magick::Image* image) { + image->implode(_factor); +} + +void +Implode::refreshParameters(Inkscape::Extension::Effect* module) { + _factor = module->get_param_float("factor"); +} + +#include "../clear-n_.h" + +void +Implode::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Implode") "\n" + "org.inkscape.effect.bitmap.implode\n" + "10\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Implode selected bitmap(s)") "\n" + "\n" + "\n", new Implode()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/implode.h b/src/extension/internal/bitmap/implode.h new file mode 100644 index 0000000..d9c5adb --- /dev/null +++ b/src/extension/internal/bitmap/implode.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Implode : public ImageMagick +{ +private: + float _factor; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/level.cpp b/src/extension/internal/bitmap/level.cpp new file mode 100644 index 0000000..aaf3168 --- /dev/null +++ b/src/extension/internal/bitmap/level.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "level.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Level::applyEffect(Magick::Image* image) { + Magick::Quantum black_point = Magick::Color::scaleDoubleToQuantum(_black_point / 100.0); + Magick::Quantum white_point = Magick::Color::scaleDoubleToQuantum(_white_point / 100.0); + image->level(black_point, white_point, _mid_point); +} + +void +Level::refreshParameters(Inkscape::Extension::Effect* module) { + _black_point = module->get_param_float("blackPoint"); + _white_point = module->get_param_float("whitePoint"); + _mid_point = module->get_param_float("midPoint"); +} + +#include "../clear-n_.h" + +void +Level::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Level") "\n" + "org.inkscape.effect.bitmap.level\n" + "0\n" + "100\n" + "1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Level selected bitmap(s) by scaling values falling between the given ranges to the full color range") "\n" + "\n" + "\n", new Level()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/level.h b/src/extension/internal/bitmap/level.h new file mode 100644 index 0000000..d372745 --- /dev/null +++ b/src/extension/internal/bitmap/level.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Level : public ImageMagick +{ +private: + float _black_point; + float _white_point; + float _mid_point; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/levelChannel.cpp b/src/extension/internal/bitmap/levelChannel.cpp new file mode 100644 index 0000000..497d6ce --- /dev/null +++ b/src/extension/internal/bitmap/levelChannel.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "levelChannel.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +LevelChannel::applyEffect(Magick::Image* image) { + Magick::ChannelType channel = Magick::UndefinedChannel; + if (!strcmp(_channelName, "Red Channel")) channel = Magick::RedChannel; + else if (!strcmp(_channelName, "Green Channel")) channel = Magick::GreenChannel; + else if (!strcmp(_channelName, "Blue Channel")) channel = Magick::BlueChannel; + else if (!strcmp(_channelName, "Cyan Channel")) channel = Magick::CyanChannel; + else if (!strcmp(_channelName, "Magenta Channel")) channel = Magick::MagentaChannel; + else if (!strcmp(_channelName, "Yellow Channel")) channel = Magick::YellowChannel; + else if (!strcmp(_channelName, "Black Channel")) channel = Magick::BlackChannel; + else if (!strcmp(_channelName, "Opacity Channel")) channel = Magick::OpacityChannel; + else if (!strcmp(_channelName, "Matte Channel")) channel = Magick::MatteChannel; + Magick::Quantum black_point = Magick::Color::scaleDoubleToQuantum(_black_point / 100.0); + Magick::Quantum white_point = Magick::Color::scaleDoubleToQuantum(_white_point / 100.0); + image->levelChannel(channel, black_point, white_point, _mid_point); +} + +void +LevelChannel::refreshParameters(Inkscape::Extension::Effect* module) { + _channelName = module->get_param_optiongroup("channel"); + _black_point = module->get_param_float("blackPoint"); + _white_point = module->get_param_float("whitePoint"); + _mid_point = module->get_param_float("midPoint"); +} + +#include "../clear-n_.h" + +void +LevelChannel::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Level (with Channel)") "\n" + "org.inkscape.effect.bitmap.levelChannel\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0.0\n" + "100.0\n" + "1.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Level the specified channel of selected bitmap(s) by scaling values falling between the given ranges to the full color range") "\n" + "\n" + "\n", new LevelChannel()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/levelChannel.h b/src/extension/internal/bitmap/levelChannel.h new file mode 100644 index 0000000..a619939 --- /dev/null +++ b/src/extension/internal/bitmap/levelChannel.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class LevelChannel : public ImageMagick +{ +private: + float _black_point; + float _white_point; + float _mid_point; + const gchar * _channelName; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/medianFilter.cpp b/src/extension/internal/bitmap/medianFilter.cpp new file mode 100644 index 0000000..cc3dabd --- /dev/null +++ b/src/extension/internal/bitmap/medianFilter.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "medianFilter.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +MedianFilter::applyEffect(Magick::Image* image) { + image->medianFilter(_radius); +} + +void +MedianFilter::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); +} + +#include "../clear-n_.h" + +void +MedianFilter::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Median") "\n" + "org.inkscape.effect.bitmap.medianFilter\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Replace each pixel component with the median color in a circular neighborhood") "\n" + "\n" + "\n", new MedianFilter()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/medianFilter.h b/src/extension/internal/bitmap/medianFilter.h new file mode 100644 index 0000000..60ac31a --- /dev/null +++ b/src/extension/internal/bitmap/medianFilter.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class MedianFilter : public ImageMagick +{ +private: + float _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/modulate.cpp b/src/extension/internal/bitmap/modulate.cpp new file mode 100644 index 0000000..f561d4c --- /dev/null +++ b/src/extension/internal/bitmap/modulate.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "modulate.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Modulate::applyEffect(Magick::Image* image) { + float hue = (_hue * 200 / 360.0) + 100; + image->modulate(_brightness, _saturation, hue); +} + +void +Modulate::refreshParameters(Inkscape::Extension::Effect* module) { + _brightness = module->get_param_float("brightness"); + _saturation = module->get_param_float("saturation"); + _hue = module->get_param_float("hue"); +} + +#include "../clear-n_.h" + +void +Modulate::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("HSB Adjust") "\n" + "org.inkscape.effect.bitmap.modulate\n" + "0\n" + "100\n" + "100\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Adjust the amount of hue, saturation, and brightness in selected bitmap(s)") "\n" + "\n" + "\n", new Modulate()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/modulate.h b/src/extension/internal/bitmap/modulate.h new file mode 100644 index 0000000..f0aaf1b --- /dev/null +++ b/src/extension/internal/bitmap/modulate.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Modulate : public ImageMagick +{ +private: + float _brightness; + float _saturation; + float _hue; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/negate.cpp b/src/extension/internal/bitmap/negate.cpp new file mode 100644 index 0000000..fcec5c0 --- /dev/null +++ b/src/extension/internal/bitmap/negate.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "negate.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Negate::applyEffect(Magick::Image* image) { + image->negate(); +} + +void +Negate::refreshParameters(Inkscape::Extension::Effect* /*module*/) { +} + +#include "../clear-n_.h" + +void +Negate::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Negate") "\n" + "org.inkscape.effect.bitmap.negate\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Negate (take inverse) selected bitmap(s)") "\n" + "\n" + "\n", new Negate()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/negate.h b/src/extension/internal/bitmap/negate.h new file mode 100644 index 0000000..4cde402 --- /dev/null +++ b/src/extension/internal/bitmap/negate.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Negate : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/normalize.cpp b/src/extension/internal/bitmap/normalize.cpp new file mode 100644 index 0000000..f6cf053 --- /dev/null +++ b/src/extension/internal/bitmap/normalize.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "normalize.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Normalize::applyEffect(Magick::Image* image) { + image->normalize(); +} + +void +Normalize::refreshParameters(Inkscape::Extension::Effect* /*module*/) { +} + +#include "../clear-n_.h" + +void +Normalize::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Normalize") "\n" + "org.inkscape.effect.bitmap.normalize\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Normalize selected bitmap(s), expanding color range to the full possible range of color") "\n" + "\n" + "\n", new Normalize()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/normalize.h b/src/extension/internal/bitmap/normalize.h new file mode 100644 index 0000000..2d4a9c2 --- /dev/null +++ b/src/extension/internal/bitmap/normalize.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Normalize : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/oilPaint.cpp b/src/extension/internal/bitmap/oilPaint.cpp new file mode 100644 index 0000000..a5373eb --- /dev/null +++ b/src/extension/internal/bitmap/oilPaint.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "oilPaint.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +OilPaint::applyEffect(Magick::Image* image) { + image->oilPaint(_radius); +} + +void +OilPaint::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_int("radius"); +} + +#include "../clear-n_.h" + +void +OilPaint::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Oil Paint") "\n" + "org.inkscape.effect.bitmap.oilPaint\n" + "3\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Stylize selected bitmap(s) so that they appear to be painted with oils") "\n" + "\n" + "\n", new OilPaint()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/oilPaint.h b/src/extension/internal/bitmap/oilPaint.h new file mode 100644 index 0000000..d3957bd --- /dev/null +++ b/src/extension/internal/bitmap/oilPaint.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class OilPaint : public ImageMagick +{ +private: + float _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/opacity.cpp b/src/extension/internal/bitmap/opacity.cpp new file mode 100644 index 0000000..cac8b65 --- /dev/null +++ b/src/extension/internal/bitmap/opacity.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "opacity.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Opacity::applyEffect(Magick::Image* image) { + Magick::Quantum opacity = Magick::Color::scaleDoubleToQuantum((100 - _opacity) / 100.0); + image->opacity(opacity); +} + +void +Opacity::refreshParameters(Inkscape::Extension::Effect* module) { + _opacity = module->get_param_float("opacity"); +} + +#include "../clear-n_.h" + +void +Opacity::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Opacity") "\n" + "org.inkscape.effect.bitmap.opacity\n" + "80.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Modify opacity channel(s) of selected bitmap(s)") "\n" + "\n" + "\n", new Opacity()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/opacity.h b/src/extension/internal/bitmap/opacity.h new file mode 100644 index 0000000..df6e6ac --- /dev/null +++ b/src/extension/internal/bitmap/opacity.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Opacity : public ImageMagick +{ +private: + float _opacity; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/raise.cpp b/src/extension/internal/bitmap/raise.cpp new file mode 100644 index 0000000..8b3598a --- /dev/null +++ b/src/extension/internal/bitmap/raise.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "raise.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Raise::applyEffect(Magick::Image* image) { + Magick::Geometry geometry(_width, _height, 0, 0); + image->raise(geometry, _raisedFlag); +} + +void +Raise::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); + _raisedFlag = module->get_param_bool("raisedFlag"); +} + +#include "../clear-n_.h" + +void +Raise::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Raise") "\n" + "org.inkscape.effect.bitmap.raise\n" + "6\n" + "6\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Alter lightness the edges of selected bitmap(s) to create a raised appearance") "\n" + "\n" + "\n", new Raise()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/raise.h b/src/extension/internal/bitmap/raise.h new file mode 100644 index 0000000..90023fb --- /dev/null +++ b/src/extension/internal/bitmap/raise.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Raise : public ImageMagick +{ +private: + int _width; + int _height; + bool _raisedFlag; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/reduceNoise.cpp b/src/extension/internal/bitmap/reduceNoise.cpp new file mode 100644 index 0000000..809c693 --- /dev/null +++ b/src/extension/internal/bitmap/reduceNoise.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "reduceNoise.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +ReduceNoise::applyEffect(Magick::Image* image) { + if (_order > -1) + image->reduceNoise(_order); + else + image->reduceNoise(); +} + +void +ReduceNoise::refreshParameters(Inkscape::Extension::Effect* module) { + _order = module->get_param_int("order"); +} + +#include "../clear-n_.h" + +void +ReduceNoise::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Reduce Noise") "\n" + "org.inkscape.effect.bitmap.reduceNoise\n" + "-1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Reduce noise in selected bitmap(s) using a noise peak elimination filter") "\n" + "\n" + "\n", new ReduceNoise()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/reduceNoise.h b/src/extension/internal/bitmap/reduceNoise.h new file mode 100644 index 0000000..3c9d2d6 --- /dev/null +++ b/src/extension/internal/bitmap/reduceNoise.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class ReduceNoise : public ImageMagick +{ +private: + int _order; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sample.cpp b/src/extension/internal/bitmap/sample.cpp new file mode 100644 index 0000000..e59e1af --- /dev/null +++ b/src/extension/internal/bitmap/sample.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "sample.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Sample::applyEffect(Magick::Image* image) { + Magick::Geometry geometry(_width, _height, 0, 0); + image->sample(geometry); +} + +void +Sample::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); +} + +#include "../clear-n_.h" + +void +Sample::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Resample") "\n" + "org.inkscape.effect.bitmap.sample\n" + "100\n" + "100\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Alter the resolution of selected image by resizing it to the given pixel size") "\n" + "\n" + "\n", new Sample()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sample.h b/src/extension/internal/bitmap/sample.h new file mode 100644 index 0000000..c93ab0a --- /dev/null +++ b/src/extension/internal/bitmap/sample.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Sample : public ImageMagick +{ +private: + int _width; + int _height; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/shade.cpp b/src/extension/internal/bitmap/shade.cpp new file mode 100644 index 0000000..f6b2f25 --- /dev/null +++ b/src/extension/internal/bitmap/shade.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "shade.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Shade::applyEffect(Magick::Image* image) { + image->shade(_azimuth, _elevation, !_colorShading); + // I don't know why, but I have to invert colorShading here +} + +void +Shade::refreshParameters(Inkscape::Extension::Effect* module) { + _azimuth = module->get_param_float("azimuth"); + _elevation = module->get_param_float("elevation"); + _colorShading = module->get_param_bool("colorShading"); +} + +#include "../clear-n_.h" + +void +Shade::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Shade") "\n" + "org.inkscape.effect.bitmap.shade\n" + "30\n" + "30\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Shade selected bitmap(s) simulating distant light source") "\n" + "\n" + "\n", new Shade()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/shade.h b/src/extension/internal/bitmap/shade.h new file mode 100644 index 0000000..71ed3db --- /dev/null +++ b/src/extension/internal/bitmap/shade.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Shade : public ImageMagick +{ +private: + float _azimuth; + float _elevation; + bool _colorShading; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sharpen.cpp b/src/extension/internal/bitmap/sharpen.cpp new file mode 100644 index 0000000..aa24ac3 --- /dev/null +++ b/src/extension/internal/bitmap/sharpen.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "sharpen.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Sharpen::applyEffect(Magick::Image* image) { + image->sharpen(_radius, _sigma); +} + +void +Sharpen::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Sharpen::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Sharpen") "\n" + "org.inkscape.effect.bitmap.sharpen\n" + "1.0\n" + "0.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Sharpen selected bitmap(s)") "\n" + "\n" + "\n", new Sharpen()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sharpen.h b/src/extension/internal/bitmap/sharpen.h new file mode 100644 index 0000000..35067ed --- /dev/null +++ b/src/extension/internal/bitmap/sharpen.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Sharpen : public ImageMagick +{ +private: + float _radius; + float _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/solarize.cpp b/src/extension/internal/bitmap/solarize.cpp new file mode 100644 index 0000000..69a1e18 --- /dev/null +++ b/src/extension/internal/bitmap/solarize.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "solarize.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Solarize::applyEffect(Magick::Image* image) { + // Image Magick Quantum depth = 16 + // 655.35 = (2^16 - 1) / 100 + image->solarize(_factor * 655.35); +} + +void +Solarize::refreshParameters(Inkscape::Extension::Effect* module) { + _factor = module->get_param_float("factor"); +} + +#include "../clear-n_.h" + +void +Solarize::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Solarize") "\n" + "org.inkscape.effect.bitmap.solarize\n" + "50\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Solarize selected bitmap(s), like overexposing photographic film") "\n" + "\n" + "\n", new Solarize()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/solarize.h b/src/extension/internal/bitmap/solarize.h new file mode 100644 index 0000000..2e4a51b --- /dev/null +++ b/src/extension/internal/bitmap/solarize.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Solarize : public ImageMagick +{ +private: + float _factor; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/spread.cpp b/src/extension/internal/bitmap/spread.cpp new file mode 100644 index 0000000..86b4432 --- /dev/null +++ b/src/extension/internal/bitmap/spread.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "spread.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Spread::applyEffect(Magick::Image* image) { + image->spread(_amount); +} + +void +Spread::refreshParameters(Inkscape::Extension::Effect* module) { + _amount = module->get_param_int("amount"); +} + +#include "../clear-n_.h" + +void +Spread::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Dither") "\n" + "org.inkscape.effect.bitmap.spread\n" + "3\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Randomly scatter pixels in selected bitmap(s), within the given radius of the original position") "\n" + "\n" + "\n", new Spread()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/spread.h b/src/extension/internal/bitmap/spread.h new file mode 100644 index 0000000..ad77544 --- /dev/null +++ b/src/extension/internal/bitmap/spread.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Spread : public ImageMagick +{ +private: + int _amount; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/swirl.cpp b/src/extension/internal/bitmap/swirl.cpp new file mode 100644 index 0000000..a7681a3 --- /dev/null +++ b/src/extension/internal/bitmap/swirl.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "swirl.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Swirl::applyEffect(Magick::Image* image) { + image->swirl(_degrees); +} + +void +Swirl::refreshParameters(Inkscape::Extension::Effect* module) { + _degrees = module->get_param_int("degrees"); +} + +#include "../clear-n_.h" + +void +Swirl::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Swirl") "\n" + "org.inkscape.effect.bitmap.swirl\n" + "30\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Swirl selected bitmap(s) around center point") "\n" + "\n" + "\n", new Swirl()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/swirl.h b/src/extension/internal/bitmap/swirl.h new file mode 100644 index 0000000..b9ca348 --- /dev/null +++ b/src/extension/internal/bitmap/swirl.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Swirl : public ImageMagick +{ +private: + float _degrees; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/threshold.cpp b/src/extension/internal/bitmap/threshold.cpp new file mode 100644 index 0000000..4f9dec5 --- /dev/null +++ b/src/extension/internal/bitmap/threshold.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "threshold.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Threshold::applyEffect(Magick::Image* image) { + image->threshold(_threshold); +} + +void +Threshold::refreshParameters(Inkscape::Extension::Effect* module) { + _threshold = module->get_param_float("threshold"); +} + +#include "../clear-n_.h" + +void +Threshold::init() +{ + Inkscape::Extension::build_from_mem( + "\n" +// TRANSLATORS: see http://docs.gimp.org/en/gimp-tool-threshold.html + "" N_("Threshold") "\n" + "org.inkscape.effect.bitmap.threshold\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Threshold selected bitmap(s)") "\n" + "\n" + "\n", new Threshold()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/threshold.h b/src/extension/internal/bitmap/threshold.h new file mode 100644 index 0000000..93e15bc --- /dev/null +++ b/src/extension/internal/bitmap/threshold.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Threshold : public ImageMagick +{ +private: + double _threshold; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/unsharpmask.cpp b/src/extension/internal/bitmap/unsharpmask.cpp new file mode 100644 index 0000000..9240803 --- /dev/null +++ b/src/extension/internal/bitmap/unsharpmask.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "unsharpmask.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Unsharpmask::applyEffect(Magick::Image* image) { + float amount = _amount / 100.0; + image->unsharpmask(_radius, _sigma, amount, _threshold); +} + +void +Unsharpmask::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); + _amount = module->get_param_float("amount"); + _threshold = module->get_param_float("threshold"); +} + +#include "../clear-n_.h" + +void +Unsharpmask::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Unsharp Mask") "\n" + "org.inkscape.effect.bitmap.unsharpmask\n" + "5.0\n" + "5.0\n" + "50.0\n" + "5.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Sharpen selected bitmap(s) using unsharp mask algorithms") "\n" + "\n" + "\n", new Unsharpmask()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/unsharpmask.h b/src/extension/internal/bitmap/unsharpmask.h new file mode 100644 index 0000000..34fbfeb --- /dev/null +++ b/src/extension/internal/bitmap/unsharpmask.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Unsharpmask : public ImageMagick +{ +private: + float _radius; + float _sigma; + float _amount; + float _threshold; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/wave.cpp b/src/extension/internal/bitmap/wave.cpp new file mode 100644 index 0000000..5a67574 --- /dev/null +++ b/src/extension/internal/bitmap/wave.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "wave.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Wave::applyEffect(Magick::Image* image) { + image->wave(_amplitude, _wavelength); +} + +void +Wave::refreshParameters(Inkscape::Extension::Effect* module) { + _amplitude = module->get_param_float("amplitude"); + _wavelength = module->get_param_float("wavelength"); +} + +#include "../clear-n_.h" + +void +Wave::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Wave") "\n" + "org.inkscape.effect.bitmap.wave\n" + "25\n" + "150\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "" N_("Alter selected bitmap(s) along sine wave") "\n" + "\n" + "\n", new Wave()); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/wave.h b/src/extension/internal/bitmap/wave.h new file mode 100644 index 0000000..f524c17 --- /dev/null +++ b/src/extension/internal/bitmap/wave.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Wave : public ImageMagick +{ +private: + float _amplitude; + float _wavelength; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bluredge.cpp b/src/extension/internal/bluredge.cpp new file mode 100644 index 0000000..80fa4f2 --- /dev/null +++ b/src/extension/internal/bluredge.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file bluredge.cpp + + A plug-in to add an effect to blur the edges of an object. +*/ +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "desktop.h" +#include "document.h" +#include "selection.h" +#include "helper/action.h" +#include "helper/action-context.h" +#include "preferences.h" +#include "path-chemistry.h" +#include "object/sp-item.h" + +#include "extension/effect.h" +#include "extension/system.h" + + +#include "bluredge.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +BlurEdge::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Blur Edge, I'm loading!" << std::endl; + return TRUE; +} + +/** + \brief This actually blurs the edge. + \param module The effect that was called (unused) + \param desktop What should be edited. +*/ +void +BlurEdge::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *desktop, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + Inkscape::Selection * selection = static_cast(desktop)->selection; + + float width = module->get_param_float("blur-width"); + int steps = module->get_param_int("num-steps"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double old_offset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + // TODO need to properly refcount the items, at least + std::vector items(selection->items().begin(), selection->items().end()); + selection->clear(); + + for(auto spitem : items) { + std::vector new_items(steps); + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node * new_group = xml_doc->createElement("svg:g"); + spitem->getRepr()->parent()->appendChild(new_group); + + double orig_opacity = sp_repr_css_double_property(sp_repr_css_attr(spitem->getRepr(), "style"), "opacity", 1.0); + char opacity_string[64]; + g_ascii_formatd(opacity_string, sizeof(opacity_string), "%f", + orig_opacity / (steps)); + + for (int i = 0; i < steps; i++) { + double offset = (width / (float)(steps - 1) * (float)i) - (width / 2.0); + + new_items[i] = spitem->getRepr()->duplicate(xml_doc); + + SPCSSAttr * css = sp_repr_css_attr(new_items[i], "style"); + sp_repr_css_set_property(css, "opacity", opacity_string); + sp_repr_css_change(new_items[i], css, "style"); + + new_group->appendChild(new_items[i]); + selection->add(new_items[i]); + selection->toCurves(); + + if (offset < 0.0) { + /* Doing an inset here folks */ + offset *= -1.0; + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", offset, "px"); + sp_action_perform(Inkscape::Verb::get(SP_VERB_SELECTION_INSET)->get_action(Inkscape::ActionContext(desktop)), nullptr); + } else if (offset > 0.0) { + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", offset, "px"); + sp_action_perform(Inkscape::Verb::get(SP_VERB_SELECTION_OFFSET)->get_action(Inkscape::ActionContext(desktop)), nullptr); + } + + selection->clear(); + } + + Inkscape::GC::release(new_group); + } + + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", old_offset, "px"); + + selection->clear(); + selection->add(items.begin(), items.end()); + + return; +} + +Gtk::Widget * +BlurEdge::prefs_effect(Inkscape::Extension::Effect * module, Inkscape::UI::View::View * /*view*/, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + return module->autogui(nullptr, nullptr, changeSignal); +} + +#include "clear-n_.h" + +void +BlurEdge::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Inset/Outset Halo") "\n" + "org.inkscape.effect.bluredge\n" + "1.0\n" + "11\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" , new BlurEdge()); + return; +} + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/bluredge.h b/src/extension/internal/bluredge.h new file mode 100644 index 0000000..b74b753 --- /dev/null +++ b/src/extension/internal/bluredge.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class BlurEdge : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect * module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void init (); +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/cairo-ps-out.cpp b/src/extension/internal/cairo-ps-out.cpp new file mode 100644 index 0000000..e003c3d --- /dev/null +++ b/src/extension/internal/cairo-ps-out.cpp @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PS. + * + * Authors: + * Ted Gould + * Ulf Erikson + * Adib Taraben + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#ifdef CAIRO_HAS_PS_SURFACE + +#include "cairo-ps.h" +#include "cairo-ps-out.h" +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "latex-text-renderer.h" +#include +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/output.h" +#include "display/drawing.h" + +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "io/sys.h" +#include "document.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool CairoPsOutput::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_PS)) { + return FALSE; + } else { + return TRUE; + } +} + +bool CairoEpsOutput::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_EPS)) { + return FALSE; + } else { + return TRUE; + } +} + +static bool +ps_print_document_to_file(SPDocument *doc, gchar const *filename, unsigned int level, bool texttopath, bool omittext, + bool filtertobitmap, int resolution, const gchar * const exportId, bool exportDrawing, bool exportCanvas, float bleedmargin_px, bool eps = false) +{ + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = TRUE; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = SP_ITEM(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) + return false; + + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + root->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY); + + /* Create renderer and context */ + CairoRenderer *renderer = new CairoRenderer(); + CairoRenderContext *ctx = renderer->createContext(); + ctx->setPSLevel(level); + ctx->setEPS(eps); + ctx->setTextToPath(texttopath); + ctx->setOmitText(omittext); + ctx->setFilterToBitmap(filtertobitmap); + ctx->setBitmapResolution(resolution); + + bool ret = ctx->setPsTarget(filename); + if(ret) { + /* Render document */ + ret = renderer->setupDocument(ctx, doc, pageBoundingBox, bleedmargin_px, base); + if (ret) { + renderer->renderItem(ctx, root); + ret = ctx->finish(); + } + } + + root->invoke_hide(dkey); + + renderer->destroyContext(ctx); + delete renderer; + + return ret; +} + + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .ps) +*/ +void +CairoPsOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_PS); + if (ext == nullptr) + return; + + int level = CAIRO_PS_LEVEL_2; + try { + const gchar *new_level = mod->get_param_optiongroup("PSlevel"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PS3", new_level) == 0)) { + level = CAIRO_PS_LEVEL_3; + } + } catch(...) {} + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } catch(...) {} + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } catch(...) {} + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } catch(...) {} + + bool new_areaPage = true; + try { + new_areaPage = (strcmp(mod->get_param_optiongroup("area"), "page") == 0); + } catch(...) {} + + bool new_areaDrawing = !new_areaPage; + + float bleedmargin_px = 0.; + try { + bleedmargin_px = mod->get_param_float("bleed"); + } catch(...) {} + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } catch(...) {} + + // Create PS + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = ps_print_document_to_file(doc, final_name, level, new_textToPath, + new_textToLaTeX, new_blurToBitmap, + new_bitmapResolution, new_exportId, + new_areaDrawing, new_areaPage, + bleedmargin_px); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_areaDrawing, new_areaPage, 0., false); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .ps) +*/ +void +CairoEpsOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_EPS); + if (ext == nullptr) + return; + + int level = CAIRO_PS_LEVEL_2; + try { + const gchar *new_level = mod->get_param_optiongroup("PSlevel"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PS3", new_level) == 0)) { + level = CAIRO_PS_LEVEL_3; + } + } catch(...) {} + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } catch(...) {} + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } catch(...) {} + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } catch(...) {} + + bool new_areaPage = true; + try { + new_areaPage = (strcmp(mod->get_param_optiongroup("area"), "page") == 0); + } catch(...) {} + + bool new_areaDrawing = !new_areaPage; + + float bleedmargin_px = 0.; + try { + bleedmargin_px = mod->get_param_float("bleed"); + } catch(...) {} + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } catch(...) {} + + // Create EPS + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = ps_print_document_to_file(doc, final_name, level, new_textToPath, + new_textToLaTeX, new_blurToBitmap, + new_bitmapResolution, new_exportId, + new_areaDrawing, new_areaPage, + bleedmargin_px, true); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_areaDrawing, new_areaPage, 0., false); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + + +bool +CairoPsOutput::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +bool +CairoEpsOutput::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo PS out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoPsOutput::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("PostScript") "\n" + "" SP_MODULE_KEY_PRINT_CAIRO_PS "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "true\n" + "96\n" + "\n" + "" + "" + "" + "0\n" + "\n" + "\n" + ".ps\n" + "image/x-postscript\n" + "" N_("PostScript (*.ps)") "\n" + "" N_("PostScript File") "\n" + "\n" + "", new CairoPsOutput()); + + return; +} + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo EPS out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoEpsOutput::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Encapsulated PostScript") "\n" + "" SP_MODULE_KEY_PRINT_CAIRO_EPS "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "true\n" + "96\n" + "\n" + "" + "" + "" + "0\n" + "\n" + "\n" + ".eps\n" + "image/x-e-postscript\n" + "" N_("Encapsulated PostScript (*.eps)") "\n" + "" N_("Encapsulated PostScript File") "\n" + "\n" + "", new CairoEpsOutput()); + + return; +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* HAVE_CAIRO_PDF */ diff --git a/src/extension/internal/cairo-ps-out.h b/src/extension/internal/cairo-ps-out.h new file mode 100644 index 0000000..ee58179 --- /dev/null +++ b/src/extension/internal/cairo-ps-out.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the print output to write out a file. This + * then makes 'save as...' PS. + * + * Authors: + * Ted Gould + * Ulf Erikson + * Adib Taraben + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_CAIRO_PS_OUT_H +#define EXTENSION_INTERNAL_CAIRO_PS_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoPsOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + static void init(); + bool textToPath(Inkscape::Extension::Print *ext) override; + +}; + +class CairoEpsOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *uri) override; + static void init(); + bool textToPath(Inkscape::Extension::Print *ext) override; + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_PS_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-render-context.cpp b/src/extension/internal/cairo-render-context.cpp new file mode 100644 index 0000000..66690d5 --- /dev/null +++ b/src/extension/internal/cairo-render-context.cpp @@ -0,0 +1,1985 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering with Cairo. + */ +/* + * Author: + * Miklos Erdelyi + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_BACKEND +#define PANGO_ENABLE_BACKEND +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + + +#include +#include +#include <2geom/pathvector.h> + +#include + +#include +#include "display/drawing.h" +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "display/cairo-utils.h" +#include "object/sp-item.h" +#include "object/sp-item-group.h" +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-mask.h" +#include "object/sp-clippath.h" + +#include "util/units.h" +#ifdef _WIN32 +#include "libnrtype/FontFactory.h" // USE_PANGO_WIN32 +#endif + +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "extension/system.h" + +#include "io/sys.h" + +#include "svg/stringstream.h" + +#include + +// include support for only the compiled-in surface types +#ifdef CAIRO_HAS_PDF_SURFACE +#include +#endif +#ifdef CAIRO_HAS_PS_SURFACE +#include +#endif + + +#ifdef CAIRO_HAS_FT_FONT +#include +#endif +#ifdef CAIRO_HAS_WIN32_FONT +#include +#include +#endif + +#include + +//#define TRACE(_args) g_printf _args +//#define TRACE(_args) g_message _args +#define TRACE(_args) +//#define TEST(_args) _args +#define TEST(_args) + +// FIXME: expose these from sp-clippath/mask.cpp +/*struct SPClipPathView { + SPClipPathView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +}; + +struct SPMaskView { + SPMaskView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +};*/ + +namespace Inkscape { +namespace Extension { +namespace Internal { + +static cairo_status_t _write_callback(void *closure, const unsigned char *data, unsigned int length); + +CairoRenderContext::CairoRenderContext(CairoRenderer *parent) : + _dpi(72), + _pdf_level(1), + _ps_level(1), + _eps(false), + _is_texttopath(FALSE), + _is_omittext(FALSE), + _is_filtertobitmap(FALSE), + _bitmapresolution(72), + _stream(nullptr), + _is_valid(FALSE), + _vector_based_target(FALSE), + _cr(nullptr), // Cairo context + _surface(nullptr), + _target(CAIRO_SURFACE_TYPE_IMAGE), + _target_format(CAIRO_FORMAT_ARGB32), + _layout(nullptr), + _state(nullptr), + _renderer(parent), + _render_mode(RENDER_MODE_NORMAL), + _clip_mode(CLIP_MODE_MASK), + _omittext_state(EMPTY) +{ +} + +CairoRenderContext::~CairoRenderContext() +{ + for (std::map::const_iterator iter = font_table.begin(); iter != font_table.end(); ++iter) + font_data_free(iter->second); + + if (_cr) cairo_destroy(_cr); + if (_surface) cairo_surface_destroy(_surface); + if (_layout) g_object_unref(_layout); +} +void CairoRenderContext::font_data_free(gpointer data) +{ + cairo_font_face_t *font_face = (cairo_font_face_t *)data; + if (font_face) { + cairo_font_face_destroy(font_face); + } +} + +CairoRenderer* CairoRenderContext::getRenderer() const +{ + return _renderer; +} + +CairoRenderState* CairoRenderContext::getCurrentState() const +{ + return _state; +} + +CairoRenderState* CairoRenderContext::getParentState() const +{ + // if this is the root node just return it + if (_state_stack.size() == 1) { + return _state; + } else { + return _state_stack[_state_stack.size()-2]; + } +} + +void CairoRenderContext::setStateForStyle(SPStyle const *style) +{ + // only opacity & overflow is stored for now + _state->opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + _state->has_overflow = (style->overflow.set && style->overflow.value != SP_CSS_OVERFLOW_VISIBLE); + _state->has_filtereffect = (style->filter.set != 0) ? TRUE : FALSE; + + if (style->fill.isPaintserver() || style->stroke.isPaintserver()) + _state->merge_opacity = FALSE; + + // disable rendering of opacity if there's a stroke on the fill + if (_state->merge_opacity + && !style->fill.isNone() + && !style->stroke.isNone()) + _state->merge_opacity = FALSE; +} + +/** + * \brief Creates a new render context which will be compatible with the given context's Cairo surface + * + * \param width width of the surface to be created + * \param height height of the surface to be created + */ +CairoRenderContext* +CairoRenderContext::cloneMe(double width, double height) const +{ + g_assert( _is_valid ); + g_assert( width > 0.0 && height > 0.0 ); + + CairoRenderContext *new_context = _renderer->createContext(); + cairo_surface_t *surface = cairo_surface_create_similar(cairo_get_target(_cr), CAIRO_CONTENT_COLOR_ALPHA, + (int)ceil(width), (int)ceil(height)); + new_context->_cr = cairo_create(surface); + new_context->_surface = surface; + new_context->_width = width; + new_context->_height = height; + new_context->_is_valid = TRUE; + + return new_context; +} + +CairoRenderContext* CairoRenderContext::cloneMe() const +{ + g_assert( _is_valid ); + + return cloneMe(_width, _height); +} + +bool CairoRenderContext::setImageTarget(cairo_format_t format) +{ + // format cannot be set on an already initialized surface + if (_is_valid) + return false; + + switch (format) { + case CAIRO_FORMAT_ARGB32: + case CAIRO_FORMAT_RGB24: + case CAIRO_FORMAT_A8: + case CAIRO_FORMAT_A1: + _target_format = format; + _target = CAIRO_SURFACE_TYPE_IMAGE; + return true; + break; + default: + break; + } + + return false; +} + +bool CairoRenderContext::setPdfTarget(gchar const *utf8_fn) +{ +#ifndef CAIRO_HAS_PDF_SURFACE + return false; +#else + _target = CAIRO_SURFACE_TYPE_PDF; + _vector_based_target = TRUE; +#endif + + FILE *osf = nullptr; + FILE *osp = nullptr; + + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError *error = nullptr; + gchar *local_fn = g_filename_from_utf8(utf8_fn, + -1, &bytesRead, &bytesWritten, &error); + gchar const *fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + if (*fn == '|') { + fn += 1; + while (isspace(*fn)) fn += 1; +#ifndef _WIN32 + osp = popen(fn, "w"); +#else + osp = _popen(fn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osp; + } else if (*fn == '>') { + fn += 1; + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osf; + } else { + /* put cwd stuff in here */ + gchar *qn = ( *fn + ? g_strdup_printf("lpr -P %s", fn) /* FIXME: quote fn */ + : g_strdup("lpr") ); +#ifndef _WIN32 + osp = popen(qn, "w"); +#else + osp = _popen(qn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + qn, strerror(errno)); + return false; + } + g_free(qn); + _stream = osp; + } + } + + g_free(local_fn); + + if (_stream) { + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + } + + return true; +} + +bool CairoRenderContext::setPsTarget(gchar const *utf8_fn) +{ +#ifndef CAIRO_HAS_PS_SURFACE + return false; +#else + _target = CAIRO_SURFACE_TYPE_PS; + _vector_based_target = TRUE; +#endif + + FILE *osf = nullptr; + FILE *osp = nullptr; + + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError *error = nullptr; + gchar *local_fn = g_filename_from_utf8(utf8_fn, + -1, &bytesRead, &bytesWritten, &error); + gchar const *fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + if (*fn == '|') { + fn += 1; + while (isspace(*fn)) fn += 1; +#ifndef _WIN32 + osp = popen(fn, "w"); +#else + osp = _popen(fn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osp; + } else if (*fn == '>') { + fn += 1; + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osf; + } else { + /* put cwd stuff in here */ + gchar *qn = ( *fn + ? g_strdup_printf("lpr -P %s", fn) /* FIXME: quote fn */ + : g_strdup("lpr") ); +#ifndef _WIN32 + osp = popen(qn, "w"); +#else + osp = _popen(qn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + qn, strerror(errno)); + return false; + } + g_free(qn); + _stream = osp; + } + } + + g_free(local_fn); + + if (_stream) { + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + } + + return true; +} + +void CairoRenderContext::setPSLevel(unsigned int level) +{ + _ps_level = level; +} + +void CairoRenderContext::setEPS(bool eps) +{ + _eps = eps; +} + +unsigned int CairoRenderContext::getPSLevel() +{ + return _ps_level; +} + +void CairoRenderContext::setPDFLevel(unsigned int level) +{ + _pdf_level = level; +} + +void CairoRenderContext::setTextToPath(bool texttopath) +{ + _is_texttopath = texttopath; +} + +void CairoRenderContext::setOmitText(bool omittext) +{ + _is_omittext = omittext; +} + +bool CairoRenderContext::getOmitText() +{ + return _is_omittext; +} + +void CairoRenderContext::setFilterToBitmap(bool filtertobitmap) +{ + _is_filtertobitmap = filtertobitmap; +} + +bool CairoRenderContext::getFilterToBitmap() +{ + return _is_filtertobitmap; +} + +void CairoRenderContext::setBitmapResolution(int resolution) +{ + _bitmapresolution = resolution; +} + +int CairoRenderContext::getBitmapResolution() +{ + return _bitmapresolution; +} + +cairo_surface_t* +CairoRenderContext::getSurface() +{ + g_assert( _is_valid ); + + return _surface; +} + +bool +CairoRenderContext::saveAsPng(const char *file_name) +{ + cairo_status_t status = cairo_surface_write_to_png(_surface, file_name); + if (status) + return false; + else + return true; +} + +void +CairoRenderContext::setRenderMode(CairoRenderMode mode) +{ + switch (mode) { + case RENDER_MODE_NORMAL: + case RENDER_MODE_CLIP: + _render_mode = mode; + break; + default: + _render_mode = RENDER_MODE_NORMAL; + break; + } +} + +CairoRenderContext::CairoRenderMode +CairoRenderContext::getRenderMode() const +{ + return _render_mode; +} + +void +CairoRenderContext::setClipMode(CairoClipMode mode) +{ + switch (mode) { + case CLIP_MODE_PATH: // Clip is rendered as a path for vector output + case CLIP_MODE_MASK: // Clip is rendered as a bitmap for raster output. + _clip_mode = mode; + break; + default: + _clip_mode = CLIP_MODE_PATH; + break; + } +} + +CairoRenderContext::CairoClipMode +CairoRenderContext::getClipMode() const +{ + return _clip_mode; +} + +CairoRenderState* CairoRenderContext::_createState() +{ + CairoRenderState *state = static_cast(g_try_malloc(sizeof(CairoRenderState))); + g_assert( state != nullptr ); + + state->has_filtereffect = FALSE; + state->merge_opacity = TRUE; + state->opacity = 1.0; + state->need_layer = FALSE; + state->has_overflow = FALSE; + state->parent_has_userspace = FALSE; + state->clip_path = nullptr; + state->mask = nullptr; + + return state; +} + +void CairoRenderContext::pushLayer() +{ + g_assert( _is_valid ); + + TRACE(("--pushLayer\n")); + cairo_push_group(_cr); + + // clear buffer + if (!_vector_based_target) { + cairo_save(_cr); + cairo_set_operator(_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(_cr); + cairo_restore(_cr); + } +} + +void +CairoRenderContext::popLayer(cairo_operator_t composite) +{ + g_assert( _is_valid ); + + float opacity = _state->opacity; + TRACE(("--popLayer w/ opacity %f\n", opacity)); + + /* + At this point, the Cairo source is ready. A Cairo mask must be created if required. + Care must be taken of transformatons as Cairo, like PS and PDF, treats clip paths and + masks independently of the objects they effect while in SVG the clip paths and masks + are defined relative to the objects they are attached to. + Notes: + 1. An SVG object may have both a clip path and a mask! + 2. An SVG clip path can be composed of an object with a clip path. This is not handled properly. + 3. An SVG clipped or masked object may be first drawn off the page and then translated onto + the page (document). This is also not handled properly. + 4. The code converts all SVG masks to bitmaps. This shouldn't be necessary. + 5. Cairo expects a mask to use only the alpha channel. SVG masks combine the RGB luminance with + alpha. This is handled here by doing a pixel by pixel conversion. + */ + + SPClipPath *clip_path = _state->clip_path; + SPMask *mask = _state->mask; + if (clip_path || mask) { + + CairoRenderContext *clip_ctx = nullptr; + cairo_surface_t *clip_mask = nullptr; + + // Apply any clip path first + if (clip_path) { + TRACE((" Applying clip\n")); + if (_render_mode == RENDER_MODE_CLIP) + mask = nullptr; // disable mask when performing nested clipping + + if (_vector_based_target) { + setClipMode(CLIP_MODE_PATH); // Vector + if (!mask) { + cairo_pop_group_to_source(_cr); + _renderer->applyClipPath(this, clip_path); // Uses cairo_clip() + if (opacity == 1.0) + cairo_paint(_cr); + else + cairo_paint_with_alpha(_cr, opacity); + + } else { + // the clipPath will be applied before masking + } + } else { + + // setup a new rendering context + clip_ctx = _renderer->createContext(); + clip_ctx->setImageTarget(CAIRO_FORMAT_A8); + clip_ctx->setClipMode(CLIP_MODE_MASK); // Raster + // This code ties the clipping to the document coordinates. It doesn't allow + // for a clipped object initially drawn off the page and then translated onto + // the page. + if (!clip_ctx->setupSurface(_width, _height)) { + TRACE(("clip: setupSurface failed\n")); + _renderer->destroyContext(clip_ctx); + return; + } + + // clear buffer + cairo_save(clip_ctx->_cr); + cairo_set_operator(clip_ctx->_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(clip_ctx->_cr); + cairo_restore(clip_ctx->_cr); + + // If a mask won't be applied set opacity too. (The clip is represented by a solid Cairo mask.) + if (!mask) + cairo_set_source_rgba(clip_ctx->_cr, 1.0, 1.0, 1.0, opacity); + else + cairo_set_source_rgba(clip_ctx->_cr, 1.0, 1.0, 1.0, 1.0); + + // copy over the correct CTM + // It must be stored in item_transform of current state after pushState. + Geom::Affine item_transform; + if (_state->parent_has_userspace) + item_transform = getParentState()->transform * _state->item_transform; + else + item_transform = _state->item_transform; + + // apply the clip path + clip_ctx->pushState(); + clip_ctx->getCurrentState()->item_transform = item_transform; + _renderer->applyClipPath(clip_ctx, clip_path); + clip_ctx->popState(); + + clip_mask = clip_ctx->getSurface(); + TEST(clip_ctx->saveAsPng("clip_mask.png")); + + if (!mask) { + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + cairo_mask_surface(_cr, clip_mask, 0, 0); + _renderer->destroyContext(clip_ctx); + } + } + } + + // Apply any mask second + if (mask) { + TRACE((" Applying mask\n")); + // create rendering context for mask + CairoRenderContext *mask_ctx = _renderer->createContext(); + + // Fix Me: This is a kludge. PDF and PS output is set to 72 dpi but the + // Cairo surface is expecting the mask to be 96 dpi. + float surface_width = _width; + float surface_height = _height; + if( _vector_based_target ) { + surface_width *= 4.0/3.0; + surface_height *= 4.0/3.0; + } + if (!mask_ctx->setupSurface( surface_width, surface_height )) { + TRACE(("mask: setupSurface failed\n")); + _renderer->destroyContext(mask_ctx); + return; + } + TRACE(("mask surface: %f x %f at %i dpi\n", surface_width, surface_height, _dpi )); + + // Mask should start black, but it is created white. + cairo_set_source_rgba(mask_ctx->_cr, 0.0, 0.0, 0.0, 1.0); + cairo_rectangle(mask_ctx->_cr, 0, 0, surface_width, surface_height); + cairo_fill(mask_ctx->_cr); + + // set rendering mode to normal + setRenderMode(RENDER_MODE_NORMAL); + + // copy the correct CTM to mask context + /* + if (_state->parent_has_userspace) + mask_ctx->setTransform(getParentState()->transform); + else + mask_ctx->setTransform(_state->transform); + */ + // This is probably not correct... but it seems to do the trick. + mask_ctx->setTransform(_state->item_transform); + + // render mask contents to mask_ctx + _renderer->applyMask(mask_ctx, mask); + + TEST(mask_ctx->saveAsPng("mask.png")); + + // composite with clip mask + if (clip_path && _clip_mode == CLIP_MODE_MASK) { + cairo_mask_surface(mask_ctx->_cr, clip_mask, 0, 0); + _renderer->destroyContext(clip_ctx); + } + + cairo_surface_t *mask_image = mask_ctx->getSurface(); + int width = cairo_image_surface_get_width(mask_image); + int height = cairo_image_surface_get_height(mask_image); + int stride = cairo_image_surface_get_stride(mask_image); + unsigned char *pixels = cairo_image_surface_get_data(mask_image); + + // In SVG, the rgb channels as well as the alpha channel is used in masking. + // In Cairo, only the alpha channel is used thus requiring this conversion. + // SVG specifies that RGB be converted to alpha using luminance-to-alpha. + // Notes: This calculation assumes linear RGB values. VERIFY COLOR SPACE! + // The incoming pixel values already include alpha, fill-opacity, etc., + // however, opacity must still be applied. + TRACE(("premul w/ %f\n", opacity)); + const float coeff_r = 0.2125 / 255.0; + const float coeff_g = 0.7154 / 255.0; + const float coeff_b = 0.0721 / 255.0; + for (int row = 0 ; row < height; row++) { + unsigned char *row_data = pixels + (row * stride); + for (int i = 0 ; i < width; i++) { + guint32 *pixel = reinterpret_cast(row_data) + i; + float lum_alpha = (((*pixel & 0x00ff0000) >> 16) * coeff_r + + ((*pixel & 0x0000ff00) >> 8) * coeff_g + + ((*pixel & 0x000000ff) ) * coeff_b ); + // lum_alpha can be slightly greater than 1 due to rounding errors... + // but this should be OK since it doesn't matter what the lower + // six hexadecimal numbers of *pixel are. + *pixel = (guint32)(0xff000000 * lum_alpha * opacity); + } + } + + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + if (_clip_mode == CLIP_MODE_PATH) { + // we have to do the clipping after cairo_pop_group_to_source + _renderer->applyClipPath(this, clip_path); + } + // apply the mask onto the layer + cairo_mask_surface(_cr, mask_image, 0, 0); + _renderer->destroyContext(mask_ctx); + } + } else { + // No clip path or mask + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + if (opacity == 1.0) + cairo_paint(_cr); + else + cairo_paint_with_alpha(_cr, opacity); + } +} +void CairoRenderContext::tagBegin(const char* l){ +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + char* link = g_strdup_printf("uri='%s'", l); + cairo_tag_begin(_cr, CAIRO_TAG_LINK, link); + g_free(link); +#endif +} + +void CairoRenderContext::tagEnd(){ +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + cairo_tag_end(_cr, CAIRO_TAG_LINK); +#endif +} + + +void +CairoRenderContext::addClipPath(Geom::PathVector const &pv, SPIEnum const *fill_rule) +{ + g_assert( _is_valid ); + + // here it should be checked whether the current clip winding changed + // so we could switch back to masked clipping + if (fill_rule->value == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + addPathVector(pv); +} + +void +CairoRenderContext::addClippingRect(double x, double y, double width, double height) +{ + g_assert( _is_valid ); + + cairo_rectangle(_cr, x, y, width, height); + cairo_clip(_cr); +} + +bool +CairoRenderContext::setupSurface(double width, double height) +{ + // Is the surface already set up? + if (_is_valid) + return true; + + if (_vector_based_target && _stream == nullptr) + return false; + + _width = width; + _height = height; + + cairo_surface_t *surface = nullptr; + cairo_matrix_t ctm; + cairo_matrix_init_identity (&ctm); + switch (_target) { + case CAIRO_SURFACE_TYPE_IMAGE: + surface = cairo_image_surface_create(_target_format, (int)ceil(width), (int)ceil(height)); + break; +#ifdef CAIRO_HAS_PDF_SURFACE + case CAIRO_SURFACE_TYPE_PDF: + surface = cairo_pdf_surface_create_for_stream(Inkscape::Extension::Internal::_write_callback, _stream, width, height); + cairo_pdf_surface_restrict_to_version(surface, (cairo_pdf_version_t)_pdf_level); + break; +#endif +#ifdef CAIRO_HAS_PS_SURFACE + case CAIRO_SURFACE_TYPE_PS: + surface = cairo_ps_surface_create_for_stream(Inkscape::Extension::Internal::_write_callback, _stream, width, height); + if(CAIRO_STATUS_SUCCESS != cairo_surface_status(surface)) { + return FALSE; + } + cairo_ps_surface_restrict_to_level(surface, (cairo_ps_level_t)_ps_level); + cairo_ps_surface_set_eps(surface, (cairo_bool_t) _eps); + break; +#endif + default: + return false; + break; + } + + _setSurfaceMetadata(surface); + + return _finishSurfaceSetup (surface, &ctm); +} + +bool +CairoRenderContext::setSurfaceTarget(cairo_surface_t *surface, bool is_vector, cairo_matrix_t *ctm) +{ + if (_is_valid || !surface) + return false; + + _vector_based_target = is_vector; + bool ret = _finishSurfaceSetup (surface, ctm); + if (ret) + cairo_surface_reference (surface); + return ret; +} + +bool +CairoRenderContext::_finishSurfaceSetup(cairo_surface_t *surface, cairo_matrix_t *ctm) +{ + if(surface == nullptr) { + return false; + } + if(CAIRO_STATUS_SUCCESS != cairo_surface_status(surface)) { + return false; + } + + _cr = cairo_create(surface); + if(CAIRO_STATUS_SUCCESS != cairo_status(_cr)) { + return false; + } + if (ctm) + cairo_set_matrix(_cr, ctm); + _surface = surface; + + if (_vector_based_target) { + cairo_scale(_cr, Inkscape::Util::Quantity::convert(1, "px", "pt"), Inkscape::Util::Quantity::convert(1, "px", "pt")); + } else if (cairo_surface_get_content(_surface) != CAIRO_CONTENT_ALPHA) { + // set background color on non-alpha surfaces + // TODO: bgcolor should be derived from SPDocument (see IconImpl) + cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0); + cairo_rectangle(_cr, 0, 0, _width, _height); + cairo_fill(_cr); + } + + _is_valid = TRUE; + + return true; +} + +void +CairoRenderContext::_setSurfaceMetadata(cairo_surface_t *surface) +{ + switch (_target) { +#if defined CAIRO_HAS_PDF_SURFACE && CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + case CAIRO_SURFACE_TYPE_PDF: + if (!_metadata.title.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_TITLE, _metadata.title.c_str()); + } + if (!_metadata.author.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_AUTHOR, _metadata.author.c_str()); + } + if (!_metadata.subject.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_SUBJECT, _metadata.subject.c_str()); + } + if (!_metadata.keywords.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_KEYWORDS, _metadata.keywords.c_str()); + } + if (!_metadata.creator.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_CREATOR, _metadata.creator.c_str()); + } + if (!_metadata.cdate.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_CREATE_DATE, _metadata.cdate.c_str()); + } + if (!_metadata.mdate.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_MOD_DATE, _metadata.mdate.c_str()); + } + break; +#endif +#if defined CAIRO_HAS_PS_SURFACE + case CAIRO_SURFACE_TYPE_PS: + if (!_metadata.title.empty()) { + cairo_ps_surface_dsc_comment(surface, (Glib::ustring("%%Title: ") + _metadata.title).c_str()); + } + if (!_metadata.copyright.empty()) { + cairo_ps_surface_dsc_comment(surface, (Glib::ustring("%%Copyright: ") + _metadata.copyright).c_str()); + } + break; +#endif + default: + g_warning("unsupported target %d\n", _target); + } +} + +bool +CairoRenderContext::finish(bool finish_surface) +{ + g_assert( _is_valid ); + + if (_vector_based_target && finish_surface) + cairo_show_page(_cr); + + cairo_status_t status = cairo_status(_cr); + if (status != CAIRO_STATUS_SUCCESS) + g_critical("error while rendering output: %s", cairo_status_to_string(status)); + + cairo_destroy(_cr); + _cr = nullptr; + + if (finish_surface) + cairo_surface_finish(_surface); + status = cairo_surface_status(_surface); + cairo_surface_destroy(_surface); + _surface = nullptr; + + if (_layout) + g_object_unref(_layout); + + _is_valid = FALSE; + + if (_vector_based_target && _stream) { + /* Flush stream to be sure. */ + (void) fflush(_stream); + + fclose(_stream); + _stream = nullptr; + } + + if (status == CAIRO_STATUS_SUCCESS) + return true; + else + return false; +} + +void +CairoRenderContext::transform(Geom::Affine const &transform) +{ + g_assert( _is_valid ); + + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, transform); + cairo_transform(_cr, &matrix); + + // store new CTM + _state->transform = getTransform(); +} + +void +CairoRenderContext::setTransform(Geom::Affine const &transform) +{ + g_assert( _is_valid ); + + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, transform); + cairo_set_matrix(_cr, &matrix); + _state->transform = transform; +} + +Geom::Affine CairoRenderContext::getTransform() const +{ + g_assert( _is_valid ); + + cairo_matrix_t ctm; + cairo_get_matrix(_cr, &ctm); + Geom::Affine ret; + ret[0] = ctm.xx; + ret[1] = ctm.yx; + ret[2] = ctm.xy; + ret[3] = ctm.yy; + ret[4] = ctm.x0; + ret[5] = ctm.y0; + return ret; +} + +Geom::Affine CairoRenderContext::getParentTransform() const +{ + g_assert( _is_valid ); + + CairoRenderState *parent_state = getParentState(); + return parent_state->transform; +} + +void CairoRenderContext::pushState() +{ + g_assert( _is_valid ); + + cairo_save(_cr); + + CairoRenderState *new_state = _createState(); + // copy current state's transform + new_state->transform = _state->transform; + _state_stack.push_back(new_state); + _state = new_state; +} + +void CairoRenderContext::popState() +{ + g_assert( _is_valid ); + + cairo_restore(_cr); + + g_free(_state_stack.back()); + _state_stack.pop_back(); + + g_assert( !_state_stack.empty()); + _state = _state_stack.back(); +} + +static bool pattern_hasItemChildren(SPPattern *pat) +{ + for (auto& child: pat->children) { + if (SP_IS_ITEM (&child)) { + return true; + } + } + return false; +} + +cairo_pattern_t* +CairoRenderContext::_createPatternPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox) +{ + g_assert( SP_IS_PATTERN(paintserver) ); + + SPPattern *pat = SP_PATTERN (paintserver); + + Geom::Affine ps2user, pcs2dev; + ps2user = Geom::identity(); + pcs2dev = Geom::identity(); + + double x = pat->x(); + double y = pat->y(); + double width = pat->width(); + double height = pat->height(); + double bbox_width_scaler; + double bbox_height_scaler; + + TRACE(("%f x %f pattern\n", width, height)); + + if (pbox && pat->patternUnits() == SPPattern::UNITS_OBJECTBOUNDINGBOX) { + bbox_width_scaler = pbox->width(); + bbox_height_scaler = pbox->height(); + ps2user[4] = x * bbox_width_scaler + pbox->left(); + ps2user[5] = y * bbox_height_scaler + pbox->top(); + } else { + bbox_width_scaler = 1.0; + bbox_height_scaler = 1.0; + ps2user[4] = x; + ps2user[5] = y; + } + + // apply pattern transformation + Geom::Affine pattern_transform(pat->getTransform()); + ps2user *= pattern_transform; + Geom::Point ori (ps2user[4], ps2user[5]); + + // create pattern contents coordinate system + if (pat->viewBox_set) { + Geom::Rect view_box = *pat->viewbox(); + + double x, y, w, h; + x = 0; + y = 0; + w = width * bbox_width_scaler; + h = height * bbox_height_scaler; + + //calculatePreserveAspectRatio(pat->aspect_align, pat->aspect_clip, view_width, view_height, &x, &y, &w, &h); + pcs2dev[0] = w / view_box.width(); + pcs2dev[3] = h / view_box.height(); + pcs2dev[4] = x - view_box.left() * pcs2dev[0]; + pcs2dev[5] = y - view_box.top() * pcs2dev[3]; + } else if (pbox && pat->patternContentUnits() == SPPattern::UNITS_OBJECTBOUNDINGBOX) { + pcs2dev[0] = pbox->width(); + pcs2dev[3] = pbox->height(); + } + + // Calculate the size of the surface which has to be created +#define SUBPIX_SCALE 100 + // Cairo requires an integer pattern surface width/height. + // Subtract 0.5 to prevent small rounding errors from increasing pattern size by one pixel. + // Multiply by SUBPIX_SCALE to allow for less than a pixel precision + double surface_width = MAX(ceil(SUBPIX_SCALE * bbox_width_scaler * width - 0.5), 1); + double surface_height = MAX(ceil(SUBPIX_SCALE * bbox_height_scaler * height - 0.5), 1); + TRACE(("pattern surface size: %f x %f\n", surface_width, surface_height)); + // create new rendering context + CairoRenderContext *pattern_ctx = cloneMe(surface_width, surface_height); + + // adjust the size of the painted pattern to fit exactly the created surface + // this has to be done because of the rounding to obtain an integer pattern surface width/height + double scale_width = surface_width / (bbox_width_scaler * width); + double scale_height = surface_height / (bbox_height_scaler * height); + if (scale_width != 1.0 || scale_height != 1.0 || _vector_based_target) { + TRACE(("needed to scale with %f %f\n", scale_width, scale_height)); + pcs2dev *= Geom::Scale(SUBPIX_SCALE,SUBPIX_SCALE); + ps2user *= Geom::Scale(1.0/SUBPIX_SCALE,1.0/SUBPIX_SCALE); + } + + // despite scaling up/down by subpixel scaler, the origin point of the pattern must be the same + ps2user[4] = ori[Geom::X]; + ps2user[5] = ori[Geom::Y]; + + pattern_ctx->setTransform(pcs2dev); + pattern_ctx->pushState(); + + // create drawing and group + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + + // show items and render them + for (SPPattern *pat_i = pat; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i && SP_IS_OBJECT(pat_i) && pattern_hasItemChildren(pat_i)) { // find the first one with item children + for (auto& child: pat_i->children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_show(drawing, dkey, SP_ITEM_REFERENCE_FLAGS); + _renderer->renderItem(pattern_ctx, SP_ITEM(&child)); + } + } + break; // do not go further up the chain if children are found + } + } + + pattern_ctx->popState(); + + // setup a cairo_pattern_t + cairo_surface_t *pattern_surface = pattern_ctx->getSurface(); + TEST(pattern_ctx->saveAsPng("pattern.png")); + cairo_pattern_t *result = cairo_pattern_create_for_surface(pattern_surface); + cairo_pattern_set_extend(result, CAIRO_EXTEND_REPEAT); + + // set pattern transformation + cairo_matrix_t pattern_matrix; + _initCairoMatrix(&pattern_matrix, ps2user); + cairo_matrix_invert(&pattern_matrix); + cairo_pattern_set_matrix(result, &pattern_matrix); + + delete pattern_ctx; + + // hide all items + for (SPPattern *pat_i = pat; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i && SP_IS_OBJECT(pat_i) && pattern_hasItemChildren(pat_i)) { // find the first one with item children + for (auto& child: pat_i->children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_hide(dkey); + } + } + break; // do not go further up the chain if children are found + } + } + + return result; +} + +cairo_pattern_t* +CairoRenderContext::_createHatchPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox) { + SPHatch const *hatch = dynamic_cast(paintserver); + g_assert( hatch ); + + g_assert(hatch->pitch() > 0); + + // create drawing and group + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + + // TODO need to refactor 'evil' referenced code for const correctness. + SPHatch *evil = const_cast(hatch); + evil->show(drawing, dkey, pbox); + + SPHatch::RenderInfo render_info = hatch->calculateRenderInfo(dkey); + Geom::Rect tile_rect = render_info.tile_rect; + + // Cairo requires an integer pattern surface width/height. + // Subtract 0.5 to prevent small rounding errors from increasing pattern size by one pixel. + // Multiply by SUBPIX_SCALE to allow for less than a pixel precision + const int subpix_scale = 10; + double surface_width = MAX(ceil(subpix_scale * tile_rect.width() - 0.5), 1); + double surface_height = MAX(ceil(subpix_scale * tile_rect.height() - 0.5), 1); + Geom::Affine drawing_scale = Geom::Scale(surface_width / tile_rect.width(), surface_height / tile_rect.height()); + Geom::Affine drawing_transform = Geom::Translate(-tile_rect.min()) * drawing_scale; + + Geom::Affine child_transform = render_info.child_transform; + child_transform *= drawing_transform; + + //The rendering of hatch overflow is implemented by repeated drawing + //of hatch paths over one strip. Within each iteration paths are moved by pitch value. + //The movement progresses from right to left. This gives the same result + //as drawing whole strips in left-to-right order. + gdouble overflow_right_strip = 0.0; + int overflow_steps = 1; + Geom::Affine overflow_transform; + if (hatch->style->overflow.computed == SP_CSS_OVERFLOW_VISIBLE) { + Geom::Interval bounds = hatch->bounds(); + overflow_right_strip = floor(bounds.max() / hatch->pitch()) * hatch->pitch(); + overflow_steps = ceil((overflow_right_strip - bounds.min()) / hatch->pitch()) + 1; + overflow_transform = Geom::Translate(hatch->pitch(), 0.0); + } + + CairoRenderContext *pattern_ctx = cloneMe(surface_width, surface_height); + pattern_ctx->setTransform(child_transform); + pattern_ctx->transform(Geom::Translate(-overflow_right_strip, 0.0)); + pattern_ctx->pushState(); + + std::vector children(evil->hatchPaths()); + + for (int i = 0; i < overflow_steps; i++) { + for (auto path : children) { + _renderer->renderHatchPath(pattern_ctx, *path, dkey); + } + pattern_ctx->transform(overflow_transform); + } + + pattern_ctx->popState(); + + // setup a cairo_pattern_t + cairo_surface_t *pattern_surface = pattern_ctx->getSurface(); + TEST(pattern_ctx->saveAsPng("hatch.png")); + cairo_pattern_t *result = cairo_pattern_create_for_surface(pattern_surface); + cairo_pattern_set_extend(result, CAIRO_EXTEND_REPEAT); + + Geom::Affine pattern_transform; + pattern_transform = render_info.pattern_to_user_transform.inverse() * drawing_transform; + ink_cairo_pattern_set_matrix(result, pattern_transform); + + evil->hide(dkey); + + delete pattern_ctx; + return result; +} + +cairo_pattern_t* +CairoRenderContext::_createPatternForPaintServer(SPPaintServer const *const paintserver, + Geom::OptRect const &pbox, float alpha) +{ + cairo_pattern_t *pattern = nullptr; + bool apply_bbox2user = FALSE; + + if (SP_IS_LINEARGRADIENT (paintserver)) { + + SPLinearGradient *lg=SP_LINEARGRADIENT(paintserver); + + SP_GRADIENT(lg)->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point p1 (lg->x1.computed, lg->y1.computed); + Geom::Point p2 (lg->x2.computed, lg->y2.computed); + if (pbox && SP_GRADIENT(lg)->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + // convert to userspace + Geom::Affine bbox2user(pbox->width(), 0, 0, pbox->height(), pbox->left(), pbox->top()); + p1 *= bbox2user; + p2 *= bbox2user; + } + + // create linear gradient pattern + pattern = cairo_pattern_create_linear(p1[Geom::X], p1[Geom::Y], p2[Geom::X], p2[Geom::Y]); + + // add stops + for (gint i = 0; unsigned(i) < lg->vector.stops.size(); i++) { + float rgb[3]; + lg->vector.stops[i].color.get_rgb_floatv(rgb); + cairo_pattern_add_color_stop_rgba(pattern, lg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], lg->vector.stops[i].opacity * alpha); + } + } else if (SP_IS_RADIALGRADIENT (paintserver)) { + + SPRadialGradient *rg=SP_RADIALGRADIENT(paintserver); + + SP_GRADIENT(rg)->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point c (rg->cx.computed, rg->cy.computed); + Geom::Point f (rg->fx.computed, rg->fy.computed); + double r = rg->r.computed; + double fr = rg->fr.computed; + if (pbox && SP_GRADIENT(rg)->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) + apply_bbox2user = true; + + // create radial gradient pattern + pattern = cairo_pattern_create_radial(f[Geom::X], f[Geom::Y], fr, c[Geom::X], c[Geom::Y], r); + + // add stops + for (gint i = 0; unsigned(i) < rg->vector.stops.size(); i++) { + float rgb[3]; + rg->vector.stops[i].color.get_rgb_floatv(rgb); + cairo_pattern_add_color_stop_rgba(pattern, rg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], rg->vector.stops[i].opacity * alpha); + } + } else if (SP_IS_MESHGRADIENT (paintserver)) { + SPMeshGradient *mg = SP_MESHGRADIENT(paintserver); + + pattern = mg->pattern_new(_cr, pbox, 1.0); + } else if (SP_IS_PATTERN (paintserver)) { + pattern = _createPatternPainter(paintserver, pbox); + } else if ( dynamic_cast(paintserver) ) { + pattern = _createHatchPainter(paintserver, pbox); + } else { + return nullptr; + } + + if (pattern && SP_IS_GRADIENT(paintserver)) { + SPGradient *g = SP_GRADIENT(paintserver); + + // set extend type + SPGradientSpread spread = g->fetchSpread(); + switch (spread) { + case SP_GRADIENT_SPREAD_REPEAT: { + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT); + break; + } + case SP_GRADIENT_SPREAD_REFLECT: { // not supported by cairo-pdf yet + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REFLECT); + break; + } + case SP_GRADIENT_SPREAD_PAD: { // not supported by cairo-pdf yet + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD); + break; + } + default: { + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE); + break; + } + } + + cairo_matrix_t pattern_matrix; + if (g->gradientTransform_set) { + // apply gradient transformation + cairo_matrix_init(&pattern_matrix, + g->gradientTransform[0], g->gradientTransform[1], + g->gradientTransform[2], g->gradientTransform[3], + g->gradientTransform[4], g->gradientTransform[5]); + } else { + cairo_matrix_init_identity (&pattern_matrix); + } + + if (apply_bbox2user) { + // convert to userspace + cairo_matrix_t bbox2user; + cairo_matrix_init (&bbox2user, pbox->width(), 0, 0, pbox->height(), pbox->left(), pbox->top()); + cairo_matrix_multiply (&pattern_matrix, &bbox2user, &pattern_matrix); + } + cairo_matrix_invert(&pattern_matrix); // because Cairo expects a userspace->patternspace matrix + cairo_pattern_set_matrix(pattern, &pattern_matrix); + } + + return pattern; +} + +void +CairoRenderContext::_setFillStyle(SPStyle const *const style, Geom::OptRect const &pbox) +{ + g_return_if_fail( !style->fill.set + || style->fill.isColor() + || style->fill.isPaintserver() ); + + float alpha = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (_state->merge_opacity) { + alpha *= _state->opacity; + TRACE(("merged op=%f\n", alpha)); + } + + SPPaintServer const *paint_server = style->getFillPaintServer(); + if (paint_server && paint_server->isValid()) { + + g_assert(SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) + || SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style)) + || dynamic_cast(SP_STYLE_FILL_SERVER(style))); + + cairo_pattern_t *pattern = _createPatternForPaintServer(paint_server, pbox, alpha); + if (pattern) { + cairo_set_source(_cr, pattern); + cairo_pattern_destroy(pattern); + } + } else if (style->fill.colorSet) { + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + + cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); + + } else { // unset fill is black + g_assert(!style->fill.set + || (paint_server && !paint_server->isValid())); + + cairo_set_source_rgba(_cr, 0, 0, 0, alpha); + } +} + +void +CairoRenderContext::_setStrokeStyle(SPStyle const *style, Geom::OptRect const &pbox) +{ + float alpha = SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + if (_state->merge_opacity) + alpha *= _state->opacity; + + if (style->stroke.isColor() || (style->stroke.isPaintserver() && !style->getStrokePaintServer()->isValid())) { + float rgb[3]; + style->stroke.value.color.get_rgb_floatv(rgb); + + cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); + } else { + g_assert( style->stroke.isPaintserver() + || SP_IS_GRADIENT(SP_STYLE_STROKE_SERVER(style)) + || SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style)) + || dynamic_cast(SP_STYLE_STROKE_SERVER(style))); + + cairo_pattern_t *pattern = _createPatternForPaintServer(SP_STYLE_STROKE_SERVER(style), pbox, alpha); + + if (pattern) { + cairo_set_source(_cr, pattern); + cairo_pattern_destroy(pattern); + } + } + + if (!style->stroke_dasharray.values.empty()) + { + size_t ndashes = style->stroke_dasharray.values.size(); + double* dashes =(double*)malloc(ndashes*sizeof(double)); + for( unsigned i = 0; i < ndashes; ++i ) { + dashes[i] = style->stroke_dasharray.values[i].value; + } + cairo_set_dash(_cr, dashes, ndashes, style->stroke_dashoffset.value); + free(dashes); + } else { + cairo_set_dash(_cr, nullptr, 0, 0.0); // disable dashing + } + + cairo_set_line_width(_cr, style->stroke_width.computed); + + // set line join type + cairo_line_join_t join = CAIRO_LINE_JOIN_MITER; + switch (style->stroke_linejoin.computed) { + case SP_STROKE_LINEJOIN_MITER: + join = CAIRO_LINE_JOIN_MITER; + break; + case SP_STROKE_LINEJOIN_ROUND: + join = CAIRO_LINE_JOIN_ROUND; + break; + case SP_STROKE_LINEJOIN_BEVEL: + join = CAIRO_LINE_JOIN_BEVEL; + break; + } + cairo_set_line_join(_cr, join); + + // set line cap type + cairo_line_cap_t cap = CAIRO_LINE_CAP_BUTT; + switch (style->stroke_linecap.computed) { + case SP_STROKE_LINECAP_BUTT: + cap = CAIRO_LINE_CAP_BUTT; + break; + case SP_STROKE_LINECAP_ROUND: + cap = CAIRO_LINE_CAP_ROUND; + break; + case SP_STROKE_LINECAP_SQUARE: + cap = CAIRO_LINE_CAP_SQUARE; + break; + } + cairo_set_line_cap(_cr, cap); + cairo_set_miter_limit(_cr, MAX(1, style->stroke_miterlimit.value)); +} + +void +CairoRenderContext::_prepareRenderGraphic() +{ + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_is_omittext && _target == CAIRO_SURFACE_TYPE_PDF && _render_mode != RENDER_MODE_CLIP) { + if (_omittext_state == NEW_PAGE_ON_GRAPHIC) { + // better set this immediately (not sure if masks applied during "popLayer" could call + // this function, too, triggering the same code again in error + _omittext_state = GRAPHIC_ON_TOP; + + // As we can not emit the page in the middle of a layer (aka group) - it will not be fully painted yet! - + // the following basically mirrors the calls in CairoRenderer::renderItem (but in reversed order) + // - first traverse all saved states in reversed order (i.e. from deepest nesting to the top) + // and apply clipping / masking to layers on the way (this is done in popLayer) + // - then emit the page using cairo_show_page() + // - finally restore the previous state with proper transforms and appropriate layers again + // + // TODO: While this appears to be an ugly hack it seems to work + // Somebody with a more intimate understanding of cairo and the renderer implementation might + // be able to implement this in a cleaner way, though. + int stack_size = _state_stack.size(); + for (int i = stack_size-1; i > 0; i--) { + if (_state_stack[i]->need_layer) + popLayer(); + cairo_restore(_cr); + _state = _state_stack[i-1]; + } + + cairo_show_page(_cr); + + for (int i = 1; i < stack_size; i++) { + cairo_save(_cr); + _state = _state_stack[i]; + if (_state->need_layer) + pushLayer(); + setTransform(_state->transform); + } + } + _omittext_state = GRAPHIC_ON_TOP; + } +} + +void +CairoRenderContext::_prepareRenderText() +{ + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_is_omittext && _target == CAIRO_SURFACE_TYPE_PDF) { + if (_omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + } +} + +/* We need CairoPaintOrder as markers are rendered in a separate step and may be rendered + * in between fill and stroke. + */ +bool +CairoRenderContext::renderPathVector(Geom::PathVector const & pathv, SPStyle const *style, Geom::OptRect const &pbox, CairoPaintOrder order) +{ + g_assert( _is_valid ); + + _prepareRenderGraphic(); + + if (_render_mode == RENDER_MODE_CLIP) { + if (_clip_mode == CLIP_MODE_PATH) { + addClipPath(pathv, &style->fill_rule); + } else { + setPathVector(pathv); + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + cairo_fill(_cr); + TEST(cairo_surface_write_to_png (_surface, "pathmask.png")); + } + return true; + } + + bool no_fill = style->fill.isNone() || style->fill_opacity.value == 0 || + order == STROKE_ONLY; + bool no_stroke = style->stroke.isNone() || style->stroke_width.computed < 1e-9 || + style->stroke_opacity.value == 0 || order == FILL_ONLY; + + if (no_fill && no_stroke) + return true; + + bool need_layer = ( !_state->merge_opacity && !_state->need_layer && + ( _state->opacity != 1.0 || _state->clip_path != nullptr || _state->mask != nullptr ) ); + bool blend = false; + if (style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) { + need_layer = true; + blend = true; + } + if (!need_layer) + cairo_save(_cr); + else + pushLayer(); + + if (!no_fill) { + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + } + + setPathVector(pathv); + + if (!no_fill && (order == STROKE_OVER_FILL || order == FILL_ONLY)) { + _setFillStyle(style, pbox); + + if (no_stroke) + cairo_fill(_cr); + else + cairo_fill_preserve(_cr); + } + + if (!no_stroke) { + _setStrokeStyle(style, pbox); + + if (no_fill || order == STROKE_OVER_FILL) + cairo_stroke(_cr); + else + cairo_stroke_preserve(_cr); + } + + if (!no_fill && order == FILL_OVER_STROKE) { + _setFillStyle(style, pbox); + + cairo_fill(_cr); + } + + if (need_layer) { + if (blend) { + popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } else { + popLayer(); + } + } else { + cairo_restore(_cr); + } + + return true; +} + +bool CairoRenderContext::renderImage(Inkscape::Pixbuf *pb, + Geom::Affine const &image_transform, SPStyle const *style) +{ + g_assert( _is_valid ); + + if (_render_mode == RENDER_MODE_CLIP) { + return true; + } + + _prepareRenderGraphic(); + + int w = pb->width(); + int h = pb->height(); + + // TODO: reenable merge_opacity if useful + float opacity = _state->opacity; + + cairo_surface_t *image_surface = pb->getSurfaceRaw(); + if (cairo_surface_status(image_surface)) { + TRACE(("Image surface creation failed:\n%s\n", cairo_status_to_string(cairo_surface_status(image_surface)))); + return false; + } + + cairo_save(_cr); + + // scaling by width & height is not needed because it will be done by Cairo + transform(image_transform); + + cairo_set_source_surface(_cr, image_surface, 0.0, 0.0); + + // set clip region so that the pattern will not be repeated (bug in Cairo-PDF) + if (_vector_based_target) { + cairo_new_path(_cr); + cairo_rectangle(_cr, 0, 0, w, h); + cairo_clip(_cr); + } + + // Cairo filter method will be mapped to PS/PDF 'interpolate' true/false). + // See cairo-pdf-surface.c + if (style) { + // See: http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty + // https://drafts.csswg.org/css-images-3/#the-image-rendering + // style.h/style.cpp, drawing-image.cpp + // + // CSS 3 defines: + // 'optimizeSpeed' as alias for "pixelated" + // 'optimizeQuality' as alias for "smooth" + switch (style->image_rendering.computed) { + case SP_CSS_IMAGE_RENDERING_OPTIMIZESPEED: + case SP_CSS_IMAGE_RENDERING_PIXELATED: + // we don't have an implementation for crisp-edges, but it should *not* smooth or blur + case SP_CSS_IMAGE_RENDERING_CRISPEDGES: + cairo_pattern_set_filter(cairo_get_source(_cr), CAIRO_FILTER_NEAREST); + break; + case SP_CSS_IMAGE_RENDERING_OPTIMIZEQUALITY: + case SP_CSS_IMAGE_RENDERING_AUTO: + default: + cairo_pattern_set_filter(cairo_get_source(_cr), CAIRO_FILTER_BEST); + break; + } + } + + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + + cairo_paint(_cr); + + cairo_restore(_cr); + return true; +} + +#define GLYPH_ARRAY_SIZE 64 + +// TODO investigate why the font is being ignored: +unsigned int CairoRenderContext::_showGlyphs(cairo_t *cr, PangoFont * /*font*/, std::vector const &glyphtext, bool path) +{ + cairo_glyph_t glyph_array[GLYPH_ARRAY_SIZE]; + cairo_glyph_t *glyphs = glyph_array; + unsigned int num_glyphs = glyphtext.size(); + if (num_glyphs > GLYPH_ARRAY_SIZE) { + glyphs = (cairo_glyph_t*)g_try_malloc(sizeof(cairo_glyph_t) * num_glyphs); + if(glyphs == nullptr) { + g_warning("CairorenderContext::_showGlyphs: can not allocate memory for %d glyphs.", num_glyphs); + return 0; + } + } + + unsigned int num_invalid_glyphs = 0; + unsigned int i = 0; // is a counter for indexing the glyphs array, only counts the valid glyphs + for (const auto & it_info : glyphtext) { + // skip glyphs which are PANGO_GLYPH_EMPTY (0x0FFFFFFF) + // or have the PANGO_GLYPH_UNKNOWN_FLAG (0x10000000) set + if (it_info.index == 0x0FFFFFFF || it_info.index & 0x10000000) { + TRACE(("INVALID GLYPH found\n")); + g_message("Invalid glyph found, continuing..."); + num_invalid_glyphs++; + continue; + } + glyphs[i].index = it_info.index; + glyphs[i].x = it_info.x; + glyphs[i].y = it_info.y; + i++; + } + + if (path) { + cairo_glyph_path(cr, glyphs, num_glyphs - num_invalid_glyphs); + } else { + cairo_show_glyphs(cr, glyphs, num_glyphs - num_invalid_glyphs); + } + + if (num_glyphs > GLYPH_ARRAY_SIZE) { + g_free(glyphs); + } + + return num_glyphs - num_invalid_glyphs; +} + +bool +CairoRenderContext::renderGlyphtext(PangoFont *font, Geom::Affine const &font_matrix, + std::vector const &glyphtext, SPStyle const *style) +{ + + _prepareRenderText(); + if (_is_omittext) + return true; + + // create a cairo_font_face from PangoFont + // double size = style->font_size.computed; /// \fixme why is this variable never used? + gpointer fonthash = (gpointer)font; + cairo_font_face_t *font_face = nullptr; + if(font_table.find(fonthash)!=font_table.end()) + font_face = font_table[fonthash]; + + FcPattern *fc_pattern = nullptr; + +#ifdef USE_PANGO_WIN32 +# ifdef CAIRO_HAS_WIN32_FONT + LOGFONTA *lfa = pango_win32_font_logfont(font); + LOGFONTW lfw; + + ZeroMemory(&lfw, sizeof(LOGFONTW)); + memcpy(&lfw, lfa, sizeof(LOGFONTA)); + MultiByteToWideChar(CP_OEMCP, MB_PRECOMPOSED, lfa->lfFaceName, LF_FACESIZE, lfw.lfFaceName, LF_FACESIZE); + + if(font_face == NULL) { + font_face = cairo_win32_font_face_create_for_logfontw(&lfw); + font_table[fonthash] = font_face; + } +# endif +#else +# ifdef CAIRO_HAS_FT_FONT + PangoFcFont *fc_font = PANGO_FC_FONT(font); + fc_pattern = fc_font->font_pattern; + if(font_face == nullptr) { + font_face = cairo_ft_font_face_create_for_pattern(fc_pattern); + font_table[fonthash] = font_face; + } +# endif +#endif + + cairo_save(_cr); + cairo_set_font_face(_cr, font_face); + + //if (fc_pattern && FcPatternGetDouble(fc_pattern, FC_PIXEL_SIZE, 0, &size) != FcResultMatch) + // size = 12.0; + + // set the given font matrix + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, font_matrix); + cairo_set_font_matrix(_cr, &matrix); + + if (_render_mode == RENDER_MODE_CLIP) { + if (_clip_mode == CLIP_MODE_MASK) { + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + _showGlyphs(_cr, font, glyphtext, FALSE); + } else { + // just add the glyph paths to the current context + _showGlyphs(_cr, font, glyphtext, TRUE); + } + } else { + + bool fill = false; + if (style->fill.isColor() || style->fill.isPaintserver()) { + fill = true; + } + + bool stroke = false; + if (style->stroke.isColor() || style->stroke.isPaintserver()) { + stroke = true; + } + + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + + // Text never has markers + bool stroke_over_fill = true; + if ( (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL) || + + (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL) || + + (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL) ) { + stroke_over_fill = false; + } + + bool have_path = false; + if (fill && stroke_over_fill) { + _setFillStyle(style, Geom::OptRect()); + if (_is_texttopath) { + _showGlyphs(_cr, font, glyphtext, true); + if (stroke) { + cairo_fill_preserve(_cr); + have_path = true; + } else { + cairo_fill(_cr); + } + } else { + _showGlyphs(_cr, font, glyphtext, false); + } + } + + if (stroke) { + _setStrokeStyle(style, Geom::OptRect()); + if (!have_path) { + _showGlyphs(_cr, font, glyphtext, true); + } + if (fill && _is_texttopath && !stroke_over_fill) { + cairo_stroke_preserve(_cr); + have_path = true; + } else { + cairo_stroke(_cr); + } + } + + if (fill && !stroke_over_fill) { + _setFillStyle(style, Geom::OptRect()); + if (_is_texttopath) { + if (!have_path) { + // Could happen if both 'stroke' and 'stroke_over_fill' are false + _showGlyphs(_cr, font, glyphtext, true); + } + cairo_fill(_cr); + } else { + _showGlyphs(_cr, font, glyphtext, false); + } + } + + } + + cairo_restore(_cr); + +// if (font_face) +// cairo_font_face_destroy(font_face); + + return true; +} + +/* Helper functions */ + +void +CairoRenderContext::setPathVector(Geom::PathVector const &pv) +{ + cairo_new_path(_cr); + addPathVector(pv); +} + +void +CairoRenderContext::addPathVector(Geom::PathVector const &pv) +{ + feed_pathvector_to_cairo(_cr, pv); +} + +void +CairoRenderContext::_concatTransform(cairo_t *cr, double xx, double yx, double xy, double yy, double x0, double y0) +{ + cairo_matrix_t matrix; + + cairo_matrix_init(&matrix, xx, yx, xy, yy, x0, y0); + cairo_transform(cr, &matrix); +} + +void +CairoRenderContext::_initCairoMatrix(cairo_matrix_t *matrix, Geom::Affine const &transform) +{ + matrix->xx = transform[0]; + matrix->yx = transform[1]; + matrix->xy = transform[2]; + matrix->yy = transform[3]; + matrix->x0 = transform[4]; + matrix->y0 = transform[5]; +} + +void +CairoRenderContext::_concatTransform(cairo_t *cr, Geom::Affine const &transform) +{ + _concatTransform(cr, transform[0], transform[1], + transform[2], transform[3], + transform[4], transform[5]); +} + +static cairo_status_t +_write_callback(void *closure, const unsigned char *data, unsigned int length) +{ + size_t written; + FILE *file = (FILE*)closure; + + written = fwrite (data, 1, length, file); + + if (written == length) + return CAIRO_STATUS_SUCCESS; + else + return CAIRO_STATUS_WRITE_ERROR; +} + +#include "clear-n_.h" + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#undef TRACE +#undef TEST + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-render-context.h b/src/extension/internal/cairo-render-context.h new file mode 100644 index 0000000..f91a3f9 --- /dev/null +++ b/src/extension/internal/cairo-render-context.h @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN +#define EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN + +/** \file + * Declaration of CairoRenderContext, a class used for rendering with Cairo. + */ +/* + * Authors: + * Miklos Erdelyi + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include +#include + +#include <2geom/forward.h> +#include <2geom/affine.h> + +#include "style-internal.h" // SPIEnum + +#include + +class SPClipPath; +class SPMask; + +typedef struct _PangoFont PangoFont; +typedef struct _PangoLayout PangoLayout; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +class CairoRenderer; +class CairoRenderContext; +struct CairoRenderState; +struct CairoGlyphInfo; + +// Holds info for rendering a glyph +struct CairoGlyphInfo { + unsigned long index; + double x; + double y; +}; + +struct CairoRenderState { + unsigned int merge_opacity : 1; // whether fill/stroke opacity can be mul'd with item opacity + unsigned int need_layer : 1; // whether object is masked, clipped, and/or has a non-zero opacity + unsigned int has_overflow : 1; + unsigned int parent_has_userspace : 1; // whether the parent's ctm should be applied + float opacity; + bool has_filtereffect; + Geom::Affine item_transform; // this item's item->transform, for correct clipping + + SPClipPath *clip_path; + SPMask* mask; + + Geom::Affine transform; // the CTM +}; + +// Metadata to set on the cairo surface (if the surface supports it) +struct CairoRenderContextMetadata { + Glib::ustring title = ""; + Glib::ustring author = ""; + Glib::ustring subject = ""; + Glib::ustring keywords = ""; + Glib::ustring copyright = ""; + Glib::ustring creator = ""; + Glib::ustring cdate = ""; // currently unused + Glib::ustring mdate = ""; // currently unused +}; + +class CairoRenderContext { + friend class CairoRenderer; +public: + CairoRenderContext *cloneMe() const; + CairoRenderContext *cloneMe(double width, double height) const; + bool finish(bool finish_surface = true); + + CairoRenderer *getRenderer() const; + cairo_t *getCairoContext() const; + + enum CairoRenderMode { + RENDER_MODE_NORMAL, + RENDER_MODE_CLIP + }; + + enum CairoClipMode { + CLIP_MODE_PATH, + CLIP_MODE_MASK + }; + + bool setImageTarget(cairo_format_t format); + bool setPdfTarget(gchar const *utf8_fn); + bool setPsTarget(gchar const *utf8_fn); + /** Set the cairo_surface_t from an external source */ + bool setSurfaceTarget(cairo_surface_t *surface, bool is_vector, cairo_matrix_t *ctm=nullptr); + + void setPSLevel(unsigned int level); + void setEPS(bool eps); + unsigned int getPSLevel(); + void setPDFLevel(unsigned int level); + void setTextToPath(bool texttopath); + bool getTextToPath(); + void setOmitText(bool omittext); + bool getOmitText(); + void setFilterToBitmap(bool filtertobitmap); + bool getFilterToBitmap(); + void setBitmapResolution(int resolution); + int getBitmapResolution(); + + /** Creates the cairo_surface_t for the context with the + given width, height and with the currently set target + surface type. Also sets supported metadata on the surface. */ + bool setupSurface(double width, double height); + + cairo_surface_t *getSurface(); + + /** Saves the contents of the context to a PNG file. */ + bool saveAsPng(const char *file_name); + + /** On targets supporting multiple pages, sends subsequent rendering to a new page*/ + void newPage(); + + /* Render/clip mode setting/query */ + void setRenderMode(CairoRenderMode mode); + CairoRenderMode getRenderMode() const; + void setClipMode(CairoClipMode mode); + CairoClipMode getClipMode() const; + + void addPathVector(Geom::PathVector const &pv); + void setPathVector(Geom::PathVector const &pv); + + void pushLayer(); + void popLayer(cairo_operator_t composite = CAIRO_OPERATOR_CLEAR); + + void tagBegin(const char* link); + void tagEnd(); + + /* Graphics state manipulation */ + void pushState(); + void popState(); + CairoRenderState *getCurrentState() const; + CairoRenderState *getParentState() const; + void setStateForStyle(SPStyle const *style); + + void transform(Geom::Affine const &transform); + void setTransform(Geom::Affine const &transform); + Geom::Affine getTransform() const; + Geom::Affine getParentTransform() const; + + /* Clipping methods */ + void addClipPath(Geom::PathVector const &pv, SPIEnum const *fill_rule); + void addClippingRect(double x, double y, double width, double height); + + /* Rendering methods */ + enum CairoPaintOrder { + STROKE_OVER_FILL, + FILL_OVER_STROKE, + FILL_ONLY, + STROKE_ONLY + }; + + bool renderPathVector(Geom::PathVector const &pathv, SPStyle const *style, Geom::OptRect const &pbox, CairoPaintOrder order = STROKE_OVER_FILL); + bool renderImage(Inkscape::Pixbuf *pb, + Geom::Affine const &image_transform, SPStyle const *style); + bool renderGlyphtext(PangoFont *font, Geom::Affine const &font_matrix, + std::vector const &glyphtext, SPStyle const *style); + + /* More general rendering methods will have to be added (like fill, stroke) */ + +protected: + CairoRenderContext(CairoRenderer *renderer); + virtual ~CairoRenderContext(); + + enum CairoOmitTextPageState { + EMPTY, + GRAPHIC_ON_TOP, + NEW_PAGE_ON_GRAPHIC + }; + + float _width; + float _height; + unsigned short _dpi; + unsigned int _pdf_level; + unsigned int _ps_level; + bool _eps; + bool _is_texttopath; + bool _is_omittext; + bool _is_filtertobitmap; + int _bitmapresolution; + + FILE *_stream; + + unsigned int _is_valid : 1; + unsigned int _vector_based_target : 1; + + cairo_t *_cr; // Cairo context + cairo_surface_t *_surface; + cairo_surface_type_t _target; + cairo_format_t _target_format; + PangoLayout *_layout; + + unsigned int _clip_rule : 8; + unsigned int _clip_winding_failed : 1; + + std::vector _state_stack; + CairoRenderState *_state; // the current state + + CairoRenderer *_renderer; + + CairoRenderMode _render_mode; + CairoClipMode _clip_mode; + + CairoOmitTextPageState _omittext_state; + + CairoRenderContextMetadata _metadata; + + cairo_pattern_t *_createPatternForPaintServer(SPPaintServer const *const paintserver, + Geom::OptRect const &pbox, float alpha); + cairo_pattern_t *_createPatternPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox); + cairo_pattern_t *_createHatchPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox); + + unsigned int _showGlyphs(cairo_t *cr, PangoFont *font, std::vector const &glyphtext, bool is_stroke); + + bool _finishSurfaceSetup(cairo_surface_t *surface, cairo_matrix_t *ctm = nullptr); + void _setSurfaceMetadata(cairo_surface_t *surface); + + void _setFillStyle(SPStyle const *style, Geom::OptRect const &pbox); + void _setStrokeStyle(SPStyle const *style, Geom::OptRect const &pbox); + + void _initCairoMatrix(cairo_matrix_t *matrix, Geom::Affine const &transform); + void _concatTransform(cairo_t *cr, double xx, double yx, double xy, double yy, double x0, double y0); + void _concatTransform(cairo_t *cr, Geom::Affine const &transform); + + void _prepareRenderGraphic(); + void _prepareRenderText(); + + std::map font_table; + static void font_data_free(gpointer data); + + CairoRenderState *_createState(); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer-pdf-out.cpp b/src/extension/internal/cairo-renderer-pdf-out.cpp new file mode 100644 index 0000000..655c76c --- /dev/null +++ b/src/extension/internal/cairo-renderer-pdf-out.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PDF. + * + * Authors: + * Ted Gould + * Ulf Erikson + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2004-2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#ifdef CAIRO_HAS_PDF_SURFACE + +#include "cairo-renderer-pdf-out.h" +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "latex-text-renderer.h" +#include +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/output.h" +#include "display/drawing.h" + +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include <2geom/affine.h> +#include "document.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool CairoRendererPdfOutput::check(Inkscape::Extension::Extension * /*module*/) +{ + bool result = true; + + if (nullptr == Inkscape::Extension::db.get("org.inkscape.output.pdf.cairorenderer")) { + result = false; + } + + return result; +} + +static bool +pdf_render_document_to_file(SPDocument *doc, gchar const *filename, unsigned int level, + bool texttopath, bool omittext, bool filtertobitmap, int resolution, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, float bleedmargin_px) +{ + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = TRUE; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = SP_ITEM(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) { + return false; + } + + /* Create new arena */ + Inkscape::Drawing drawing; + drawing.setExact(true); + unsigned dkey = SPItem::display_key_new(1); + root->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY); + + /* Create renderer and context */ + CairoRenderer *renderer = new CairoRenderer(); + CairoRenderContext *ctx = renderer->createContext(); + ctx->setPDFLevel(level); + ctx->setTextToPath(texttopath); + ctx->setOmitText(omittext); + ctx->setFilterToBitmap(filtertobitmap); + ctx->setBitmapResolution(resolution); + + bool ret = ctx->setPdfTarget (filename); + if(ret) { + /* Render document */ + ret = renderer->setupDocument(ctx, doc, pageBoundingBox, bleedmargin_px, base); + if (ret) { + renderer->renderItem(ctx, root); + ret = ctx->finish(); + } + } + + root->invoke_hide(dkey); + + renderer->destroyContext(ctx); + delete renderer; + + return ret; +} + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .pdf) + + The most interesting thing that this function does is just attach + an '>' on the front of the filename. This is the syntax used to + tell the printing system to save to file. +*/ +void +CairoRendererPdfOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get("org.inkscape.output.pdf.cairorenderer"); + if (ext == nullptr) + return; + + int level = 0; + try { + const gchar *new_level = mod->get_param_optiongroup("PDFversion"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PDF-1.5", new_level) == 0)) { + level = 1; + } + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + bool new_exportCanvas = true; + try { + new_exportCanvas = (strcmp(ext->get_param_optiongroup("area"), "page") == 0); + } catch(...) { + g_warning("Parameter might not exist"); + } + bool new_exportDrawing = !new_exportCanvas; + + float new_bleedmargin_px = 0.; + try { + new_bleedmargin_px = Inkscape::Util::Quantity::convert(mod->get_param_float("bleed"), "mm", "px"); + } + catch(...) { + g_warning("Parameter might not exist"); + } + + // Create PDF file + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = pdf_render_document_to_file(doc, final_name, level, + new_textToPath, new_textToLaTeX, new_blurToBitmap, new_bitmapResolution, + new_exportId, new_exportDrawing, new_exportCanvas, new_bleedmargin_px); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_exportDrawing, new_exportCanvas, new_bleedmargin_px, true); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo PDF out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoRendererPdfOutput::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "Portable Document Format\n" + "org.inkscape.output.pdf.cairorenderer\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "true\n" + "96\n" + "\n" + "" + "" + "" + "0\n" + "\n" + "\n" + ".pdf\n" + "application/pdf\n" + "Portable Document Format (*.pdf)\n" + "PDF File\n" + "\n" + "", new CairoRendererPdfOutput()); + + return; +} + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* HAVE_CAIRO_PDF */ diff --git a/src/extension/internal/cairo-renderer-pdf-out.h b/src/extension/internal/cairo-renderer-pdf-out.h new file mode 100644 index 0000000..9f99012 --- /dev/null +++ b/src/extension/internal/cairo-renderer-pdf-out.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PDF. + * + * Authors: + * Ted Gould + * Ulf Erikson + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H +#define EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoRendererPdfOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + static void init(); +}; + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer.cpp b/src/extension/internal/cairo-renderer.cpp new file mode 100644 index 0000000..faecb24 --- /dev/null +++ b/src/extension/internal/cairo-renderer.cpp @@ -0,0 +1,985 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering with Cairo. + */ +/* + * Author: + * Miklos Erdelyi + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_BACKEND +#define PANGO_ENABLE_BACKEND +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + + +#include +#include + + +#include <2geom/transforms.h> +#include <2geom/pathvector.h> +#include +#include +#include + +// include support for only the compiled-in surface types +#ifdef CAIRO_HAS_PDF_SURFACE +#include +#endif +#ifdef CAIRO_HAS_PS_SURFACE +#include +#endif + +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "document.h" +#include "inkscape-version.h" +#include "rdf.h" +#include "style-internal.h" +#include "display/cairo-utils.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" + +#include "extension/system.h" + +#include "helper/pixbuf-ops.h" +#include "helper/png-write.h" + +#include "io/sys.h" + +#include "include/source_date_epoch.h" + +#include "libnrtype/Layout-TNG.h" + +#include "object/sp-anchor.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-hatch-path.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-symbol.h" +#include "object/sp-text.h" +#include "object/sp-use.h" + +#include "util/units.h" + +//#define TRACE(_args) g_printf _args +#define TRACE(_args) +//#define TEST(_args) _args +#define TEST(_args) + +namespace Inkscape { +namespace Extension { +namespace Internal { + +CairoRenderer::CairoRenderer(void) += default; + +CairoRenderer::~CairoRenderer() +{ + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + + return; +} + +CairoRenderContext* +CairoRenderer::createContext() +{ + CairoRenderContext *new_context = new CairoRenderContext(this); + g_assert( new_context != nullptr ); + + new_context->_state = nullptr; + + // create initial render state + CairoRenderState *state = new_context->_createState(); + state->transform = Geom::identity(); + new_context->_state_stack.push_back(state); + new_context->_state = state; + + return new_context; +} + +void +CairoRenderer::destroyContext(CairoRenderContext *ctx) +{ + delete ctx; +} + +/* + +Here comes the rendering part which could be put into the 'render' methods of SPItems' + +*/ + +/* The below functions are copy&pasted plus slightly modified from *_invoke_print functions. */ +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx); +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx); +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx); +static void sp_use_render(SPUse *use, CairoRenderContext *ctx); +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx); +static void sp_text_render(SPText *text, CairoRenderContext *ctx); +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx); +static void sp_image_render(SPImage *image, CairoRenderContext *ctx); +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx); +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx); + +static void sp_shape_render_invoke_marker_rendering(SPMarker* marker, Geom::Affine tr, SPStyle* style, CairoRenderContext *ctx) +{ + bool render = true; + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + if (style->stroke_width.computed > 1e-9) { + tr = Geom::Scale(style->stroke_width.computed) * tr; + } else { + render = false; // stroke width zero and marker is thus scaled down to zero, skip + } + } + + if (render) { + SPItem* marker_item = sp_item_first_item_child(marker); + if (marker_item) { + tr = (Geom::Affine)marker_item->transform * (Geom::Affine)marker->c2p * tr; + Geom::Affine old_tr = marker_item->transform; + marker_item->transform = tr; + ctx->getRenderer()->renderItem (ctx, marker_item); + marker_item->transform = old_tr; + } + } +} + +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx) +{ + if (!shape->_curve) { + return; + } + + Geom::OptRect pbox = shape->geometricBounds(); + + SPStyle* style = shape->style; + + SPObject *defs = dynamic_cast(shape->document->getDefs()); + if (defs && defs->isAncestorOf(shape)) { + SPObject *parentobj = dynamic_cast(shape->parent); + SPMarker *marker = dynamic_cast(parentobj); + while (!marker && parentobj != defs) { + parentobj = dynamic_cast(parentobj->parent); + marker = dynamic_cast(parentobj); + } + SPObject *origin = nullptr; + if (marker) { + origin = (*marker->hrefList.begin()); + if (origin) { + SPStyle* styleorig = origin->style; + bool iscolorfill = styleorig->fill.isColor() || (styleorig->fill.isPaintserver() && !styleorig->getFillPaintServer()->isValid()); + bool iscolorstroke = styleorig->stroke.isColor() || (styleorig->stroke.isPaintserver() && !styleorig->getStrokePaintServer()->isValid()); + bool fillctxfill = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool fillctxstroke = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + bool strokectxfill = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool strokectxstroke = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + if (fillctxfill || fillctxstroke) { + if (fillctxfill ? iscolorfill : iscolorstroke) { + style->fill.setColor(fillctxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (fillctxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->fill.value.href = fillctxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->fill.setNone(); + } + } + if (strokectxfill || strokectxstroke) { + if (strokectxfill ? iscolorfill : iscolorstroke) { + style->stroke.setColor(strokectxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (strokectxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->stroke.value.href = strokectxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->stroke.setNone(); + } + } + style->fill.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + style->stroke.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + } + } + } + + Geom::PathVector const & pathv = shape->_curve->get_pathvector(); + if (pathv.empty()) { + return; + } + + if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_NORMAL || + (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE)) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + Geom::Affine tr; + if (marker->orient_mode == MARKER_ORIENT_AUTO) { + tr = sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else if (marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + tr = Geom::Rotate::from_degrees( 180.0 ) * sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(pathv.begin()->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx); + } + } + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if ( !shape->_marker[i] ) continue; + SPMarker* marker = shape->_marker[i]; + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_start(path_it->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(path_it->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx); + } + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment */ + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform(*curve_it1, *curve_it2); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(curve_it1->pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx); + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx); + } + } + } + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx); + } + } + + if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + +} + +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + std::vector l(group->childList(false)); + for(auto x : l){ + SPItem *item = dynamic_cast(x); + if (item) { + renderer->renderItem(ctx, item); + } + } +} + +static void sp_use_render(SPUse *use, CairoRenderContext *ctx) +{ + bool translated = false; + CairoRenderer *renderer = ctx->getRenderer(); + + if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { + Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); + ctx->pushState(); + ctx->transform(tp); + translated = true; + } + + if (use->child) { + renderer->renderItem(ctx, use->child); + } + + if (translated) { + ctx->popState(); + } +} + +static void sp_text_render(SPText *text, CairoRenderContext *ctx) +{ + text->layout.showGlyphs(ctx); +} + +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx) +{ + flowtext->layout.showGlyphs(ctx); +} + +static void sp_image_render(SPImage *image, CairoRenderContext *ctx) +{ + if (!image->pixbuf) { + return; + } + if ((image->width.computed <= 0.0) || (image->height.computed <= 0.0)) { + return; + } + + int w = image->pixbuf->width(); + int h = image->pixbuf->height(); + + double x = image->x.computed; + double y = image->y.computed; + double width = image->width.computed; + double height = image->height.computed; + + if (image->aspect_align != SP_ASPECT_NONE) { + calculatePreserveAspectRatio (image->aspect_align, image->aspect_clip, (double)w, (double)h, + &x, &y, &width, &height); + } + + if (image->aspect_clip == SP_ASPECT_SLICE && !ctx->getCurrentState()->has_overflow) { + ctx->addClippingRect(image->x.computed, image->y.computed, image->width.computed, image->height.computed); + } + + Geom::Translate tp(x, y); + Geom::Scale s(width / (double)w, height / (double)h); + Geom::Affine t(s * tp); + + ctx->renderImage(image->pixbuf, t, image->style); +} + +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + std::vector l(a->childList(false)); + if (a->href) + ctx->tagBegin(a->href); + for(auto x : l){ + SPItem *item = dynamic_cast(x); + if (item) { + renderer->renderItem(ctx, item); + } + } + if (a->href) + ctx->tagEnd(); +} + +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx) +{ + if (!symbol->cloned) { + return; + } + + /* Cloned is actually renderable */ + ctx->pushState(); + ctx->transform(symbol->c2p); + + // apply viewbox if set + if (false /*symbol->viewBox_set*/) { + Geom::Affine vb2user; + double x, y, width, height; + double view_width, view_height; + x = 0.0; + y = 0.0; + width = 1.0; + height = 1.0; + + view_width = symbol->viewBox.width(); + view_height = symbol->viewBox.height(); + + calculatePreserveAspectRatio(symbol->aspect_align, symbol->aspect_clip, view_width, view_height, + &x, &y,&width, &height); + + // [itemTransform *] translate(x, y) * scale(w/vw, h/vh) * translate(-vx, -vy); + vb2user = Geom::identity(); + vb2user[0] = width / view_width; + vb2user[3] = height / view_height; + vb2user[4] = x - symbol->viewBox.left() * vb2user[0]; + vb2user[5] = y - symbol->viewBox.top() * vb2user[3]; + + ctx->transform(vb2user); + } + + sp_group_render(symbol, ctx); + ctx->popState(); +} + +static void sp_root_render(SPRoot *root, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + if (!ctx->getCurrentState()->has_overflow && root->parent) + ctx->addClippingRect(root->x.computed, root->y.computed, root->width.computed, root->height.computed); + + ctx->pushState(); + renderer->setStateForItem(ctx, root); + ctx->transform(root->c2p); + sp_group_render(root, ctx); + ctx->popState(); +} + +/** + This function converts the item to a raster image and includes the image into the cairo renderer. + It is only used for filters and then only when rendering filters as bitmaps is requested. +*/ +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx) +{ + + // The code was adapted from sp_selection_create_bitmap_copy in selection-chemistry.cpp + + // Calculate resolution + double res; + /** @TODO reimplement the resolution stuff (WHY?) + */ + res = ctx->getBitmapResolution(); + if(res == 0) { + res = Inkscape::Util::Quantity::convert(1, "in", "px"); + } + TRACE(("sp_asbitmap_render: resolution: %f\n", res )); + + // Get the bounding box of the selection in desktop coordinates. + Geom::OptRect bbox = item->documentVisualBounds(); + + // no bbox, e.g. empty group + if (!bbox) { + return; + } + + Geom::Rect docrect(Geom::Rect(Geom::Point(0, 0), item->document->getDimensions())); + bbox &= docrect; + + // no bbox, e.g. empty group + if (!bbox) { + return; + } + + // The width and height of the bitmap in pixels + unsigned width = ceil(bbox->width() * Inkscape::Util::Quantity::convert(res, "px", "in")); + unsigned height = ceil(bbox->height() * Inkscape::Util::Quantity::convert(res, "px", "in")); + + if (width == 0 || height == 0) return; + + // Scale to exactly fit integer bitmap inside bounding box + double scale_x = bbox->width() / width; + double scale_y = bbox->height() / height; + + // Location of bounding box in document coordinates. + double shift_x = bbox->min()[Geom::X]; + double shift_y = bbox->top(); + + // For default 96 dpi, snap bitmap to pixel grid + if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) { + shift_x = round (shift_x); + shift_y = round (shift_y); + } + + // Calculate the matrix that will be applied to the image so that it exactly overlaps the source objects + + // Matrix to put bitmap in correct place on document + Geom::Affine t_on_document = (Geom::Affine)(Geom::Scale (scale_x, scale_y)) * + (Geom::Affine)(Geom::Translate (shift_x, shift_y)); + + // ctx matrix already includes item transformation. We must substract. + Geom::Affine t_item = item->i2doc_affine(); + Geom::Affine t = t_on_document * t_item.inverse(); + + // Do the export + SPDocument *document = item->document; + + std::unique_ptr pb( + sp_generate_internal_bitmap(document, nullptr, + bbox->min()[Geom::X], bbox->min()[Geom::Y], bbox->max()[Geom::X], bbox->max()[Geom::Y], + width, height, res, res, (guint32) 0xffffff00, item )); + + if (pb) { + //TEST(gdk_pixbuf_save( pb, "bitmap.png", "png", NULL, NULL )); + + ctx->renderImage(pb.get(), t, item->style); + } +} + + +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx) +{ + // Check item's visibility + if (item->isHidden()) { + return; + } + + if(ctx->getFilterToBitmap() && (item->style->filter.set != 0)) { + return sp_asbitmap_render(item, ctx); + } + + SPRoot *root = dynamic_cast(item); + if (root) { + TRACE(("root\n")); + sp_root_render(root, ctx); + } else { + SPSymbol *symbol = dynamic_cast(item); + if (symbol) { + TRACE(("symbol\n")); + sp_symbol_render(symbol, ctx); + } else { + SPAnchor *anchor = dynamic_cast(item); + if (anchor) { + TRACE(("\n")); + sp_anchor_render(anchor, ctx); + } else { + SPShape *shape = dynamic_cast(item); + if (shape) { + TRACE(("shape\n")); + sp_shape_render(shape, ctx); + } else { + SPUse *use = dynamic_cast(item); + if (use) { + TRACE(("use begin---\n")); + sp_use_render(use, ctx); + TRACE(("---use end\n")); + } else { + SPText *text = dynamic_cast(item); + if (text) { + TRACE(("text\n")); + sp_text_render(text, ctx); + } else { + SPFlowtext *flowtext = dynamic_cast(item); + if (flowtext) { + TRACE(("flowtext\n")); + sp_flowtext_render(flowtext, ctx); + } else { + SPImage *image = dynamic_cast(item); + if (image) { + TRACE(("image\n")); + sp_image_render(image, ctx); + } else { + SPGroup *group = dynamic_cast(item); + if (group) { + TRACE(("\n")); + sp_group_render(group, ctx); + } + } + } + } + } + } + } + } + } +} + +void +CairoRenderer::setStateForItem(CairoRenderContext *ctx, SPItem const *item) +{ + ctx->setStateForStyle(item->style); + + CairoRenderState *state = ctx->getCurrentState(); + state->clip_path = item->getClipObject(); + state->mask = item->getMaskObject(); + state->item_transform = Geom::Affine (item->transform); + + // If parent_has_userspace is true the parent state's transform + // has to be used for the mask's/clippath's context. + // This is so because we use the image's/(flow)text's transform for positioning + // instead of explicitly specifying it and letting the renderer do the + // transformation before rendering the item. + if (dynamic_cast(item) || dynamic_cast(item) || dynamic_cast(item)) { + state->parent_has_userspace = TRUE; + } + TRACE(("setStateForItem opacity: %f\n", state->opacity)); +} + +// TODO change this to accept a const SPItem: +void CairoRenderer::renderItem(CairoRenderContext *ctx, SPItem *item) +{ + ctx->pushState(); + setStateForItem(ctx, item); + + CairoRenderState *state = ctx->getCurrentState(); + state->need_layer = ( state->mask || state->clip_path || state->opacity != 1.0 ); + SPStyle* style = item->style; + SPGroup * group = dynamic_cast(item); + bool blend = false; + if (group && style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) { + state->need_layer = true; + blend = true; + } + // Draw item on a temporary surface so a mask, clip-path, or opacity can be applied to it. + if (state->need_layer) { + state->merge_opacity = FALSE; + ctx->pushLayer(); + } + ctx->transform(item->transform); + sp_item_invoke_render(item, ctx); + + if (state->need_layer) + if (blend) { + ctx->popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); // This applies clipping/masking + } else { + ctx->popLayer(); // This applies clipping/masking + } + ctx->popState(); +} + +void CairoRenderer::renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key) { + ctx->pushState(); + ctx->setStateForStyle(hatchPath.style); + ctx->transform(Geom::Translate(hatchPath.offset.computed, 0)); + + SPCurve *curve = hatchPath.calculateRenderCurve(key); + Geom::PathVector const & pathv =curve->get_pathvector(); + if (!pathv.empty()) { + ctx->renderPathVector(pathv, hatchPath.style, Geom::OptRect()); + } + + curve->unref(); + ctx->popState(); +} + +void CairoRenderer::setMetadata(CairoRenderContext *ctx, SPDocument *doc) { + // title + const gchar *title = rdf_get_work_entity(doc, rdf_find_entity("title")); + if (title) { + ctx->_metadata.title = title; + } + + // author + const gchar *author = rdf_get_work_entity(doc, rdf_find_entity("creator")); + if (author) { + ctx->_metadata.author = author; + } + + // subject + const gchar *subject = rdf_get_work_entity(doc, rdf_find_entity("description")); + if (subject) { + ctx->_metadata.subject = subject; + } + + // keywords + const gchar *keywords = rdf_get_work_entity(doc, rdf_find_entity("subject")); + if (keywords) { + ctx->_metadata.keywords = keywords; + } + + // copyright + const gchar *copyright = rdf_get_work_entity(doc, rdf_find_entity("rights")); + if (copyright) { + ctx->_metadata.copyright = copyright; + } + + // creator + ctx->_metadata.creator = Glib::ustring::compose("Inkscape %1 (https://inkscape.org)", + Inkscape::version_string_without_revision); + + // cdate (only used for for reproducible builds hack) + Glib::ustring cdate = ReproducibleBuilds::now_iso_8601(); + if (!cdate.empty()) { + ctx->_metadata.cdate = cdate; + } + + // mdate (currently unused) +} + +bool +CairoRenderer::setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, float bleedmargin_px, SPItem *base) +{ +// PLEASE note when making changes to the boundingbox and transform calculation, corresponding changes should be made to LaTeXTextRenderer::setupDocument !!! + + g_assert( ctx != nullptr ); + + if (!base) { + base = doc->getRoot(); + } + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); + } else { + Geom::OptRect bbox = base->documentVisualBounds(); + if (!bbox) { + g_message("CairoRenderer: empty bounding box."); + return false; + } + d = *bbox; + } + d.expandBy(bleedmargin_px); + + double px_to_ctx_units = 1.0; + if (ctx->_vector_based_target) { + // convert from px to pt + px_to_ctx_units = Inkscape::Util::Quantity::convert(1, "px", "pt"); + } + + ctx->_width = d.width() * px_to_ctx_units; + ctx->_height = d.height() * px_to_ctx_units; + + TRACE(("setupDocument: %f x %f\n", ctx->_width, ctx->_height)); + + setMetadata(ctx, doc); + + bool ret = ctx->setupSurface(ctx->_width, ctx->_height); + + if (ret) { + if (pageBoundingBox) { + // translate to set bleed/margin + Geom::Affine tp( Geom::Translate( bleedmargin_px, bleedmargin_px ) ); + ctx->transform(tp); + } else { + // this transform translates the export drawing to a virtual page (0,0)-(width,height) + Geom::Affine tp(Geom::Translate(-d.min())); + ctx->transform(tp); + } + } + + return ret; +} + +// Apply an SVG clip path +void +CairoRenderer::applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (cp == nullptr) + return; + + CairoRenderContext::CairoRenderMode saved_mode = ctx->getRenderMode(); + ctx->setRenderMode(CairoRenderContext::RENDER_MODE_CLIP); + + // FIXME: the access to the first clippath view to obtain the bbox is completely bogus + Geom::Affine saved_ctm; + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && cp->display->bbox) { + Geom::Rect clip_bbox = *cp->display->bbox; + Geom::Affine t(Geom::Scale(clip_bbox.dimensions())); + t[4] = clip_bbox.left(); + t[5] = clip_bbox.top(); + t *= ctx->getCurrentState()->transform; + saved_ctm = ctx->getTransform(); + ctx->setTransform(t); + } + + TRACE(("BEGIN clip\n")); + SPObject const *co = cp; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast(&child); + if (item) { + + // combine transform of the item in clippath and the item using clippath: + Geom::Affine tempmat = item->transform * ctx->getCurrentState()->item_transform; + + // render this item in clippath + ctx->pushState(); + ctx->transform(tempmat); + setStateForItem(ctx, item); + // TODO fix this call to accept const items + sp_item_invoke_render(const_cast(item), ctx); + ctx->popState(); + } + } + TRACE(("END clip\n")); + + // do clipping only if this was the first call to applyClipPath + if (ctx->getClipMode() == CairoRenderContext::CLIP_MODE_PATH + && saved_mode == CairoRenderContext::RENDER_MODE_NORMAL) + cairo_clip(ctx->_cr); + + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX) + ctx->setTransform(saved_ctm); + + ctx->setRenderMode(saved_mode); +} + +// Apply an SVG mask +void +CairoRenderer::applyMask(CairoRenderContext *ctx, SPMask const *mask) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (mask == nullptr) + return; + + // FIXME: the access to the first mask view to obtain the bbox is completely bogus + // TODO: should the bbox be transformed if maskUnits != userSpaceOnUse ? + if (mask->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && mask->display->bbox) { + Geom::Rect mask_bbox = *mask->display->bbox; + Geom::Affine t(Geom::Scale(mask_bbox.dimensions())); + t[4] = mask_bbox.left(); + t[5] = mask_bbox.top(); + t *= ctx->getCurrentState()->transform; + ctx->setTransform(t); + } + + // Clip mask contents... but... + // The mask's bounding box is the "geometric bounding box" which doesn't allow for + // filters which extend outside the bounding box. So don't clip. + // ctx->addClippingRect(mask_bbox.x0, mask_bbox.y0, mask_bbox.x1 - mask_bbox.x0, mask_bbox.y1 - mask_bbox.y0); + + ctx->pushState(); + + TRACE(("BEGIN mask\n")); + SPObject const *co = mask; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast(&child); + if (item) { + // TODO fix const correctness: + renderItem(ctx, const_cast(item)); + } + } + TRACE(("END mask\n")); + + ctx->popState(); +} + +void +calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, double vp_height, + double *x, double *y, double *width, double *height) +{ + if (aspect_align == SP_ASPECT_NONE) + return; + + double scalex, scaley, scale; + double new_width, new_height; + scalex = *width / vp_width; + scaley = *height / vp_height; + scale = (aspect_clip == SP_ASPECT_MEET) ? MIN(scalex, scaley) : MAX(scalex, scaley); + new_width = vp_width * scale; + new_height = vp_height * scale; + /* Now place viewbox to requested position */ + switch (aspect_align) { + case SP_ASPECT_XMIN_YMIN: + break; + case SP_ASPECT_XMID_YMIN: + *x -= 0.5 * (new_width - *width); + break; + case SP_ASPECT_XMAX_YMIN: + *x -= 1.0 * (new_width - *width); + break; + case SP_ASPECT_XMIN_YMID: + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMID: + *x -= 0.5 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMID: + *x -= 1.0 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMIN_YMAX: + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMAX: + *x -= 0.5 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMAX: + *x -= 1.0 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + default: + break; + } + *width = new_width; + *height = new_height; +} + +#include "clear-n_.h" + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#undef TRACE + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer.h b/src/extension/internal/cairo-renderer.h new file mode 100644 index 0000000..26f2f41 --- /dev/null +++ b/src/extension/internal/cairo-renderer.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN +#define EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN + +/** \file + * Declaration of CairoRenderer, a class used for rendering via a CairoRenderContext. + */ +/* + * Authors: + * Miklos Erdelyi + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include +#include + +//#include "libnrtype/font-instance.h" +#include + +class SPItem; +class SPClipPath; +class SPMask; +class SPHatchPath; + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoRenderer; +class CairoRenderContext; + +class CairoRenderer { +public: + CairoRenderer(); + virtual ~CairoRenderer(); + + CairoRenderContext *createContext(); + void destroyContext(CairoRenderContext *ctx); + + void setStateForItem(CairoRenderContext *ctx, SPItem const *item); + + void applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp); + void applyMask(CairoRenderContext *ctx, SPMask const *mask); + + /** Initializes the CairoRenderContext according to the specified + SPDocument. A set*Target function can only be called on the context + before setupDocument. */ + bool setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, float bleedmargin_px, SPItem *base); + + /** Traverses the object tree and invokes the render methods. */ + void renderItem(CairoRenderContext *ctx, SPItem *item); + void renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key); + +private: + /** Extract metadata from doc and set it on ctx. */ + void setMetadata(CairoRenderContext *ctx, SPDocument *doc); +}; + +// FIXME: this should be a static method of CairoRenderer +void calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, + double vp_height, double *x, double *y, double *width, double *height); + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cdr-input.cpp b/src/extension/internal/cdr-input.cpp new file mode 100644 index 0000000..ab5bcdb --- /dev/null +++ b/src/extension/internal/cdr-input.cpp @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include + +#include "cdr-input.h" + +#ifdef WITH_LIBCDR + +#include +#include + +#include + +// TODO: Drop this check when librevenge is widespread. +#if WITH_LIBCDR01 + #include + + using librevenge::RVNGString; + using librevenge::RVNGFileStream; + using librevenge::RVNGStringVector; +#else + #include + + typedef WPXString RVNGString; + typedef WPXFileStream RVNGFileStream; + typedef libcdr::CDRStringVector RVNGStringVector; +#endif + +#include +#include + +#include "extension/system.h" +#include "extension/input.h" + +#include "document.h" +#include "inkscape.h" + +#include "ui/dialog-events.h" +#include + +#include "ui/view/svg-view-widget.h" + +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +class CdrImportDialog : public Gtk::Dialog { +public: + CdrImportDialog(const std::vector &vec); + ~CdrImportDialog() override; + + bool showDialog(); + unsigned getSelectedPage(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(); + + // Signal handlers + void _onPageNumberChanged(); + void _onSpinButtonPress(GdkEventButton* button_event); + void _onSpinButtonRelease(GdkEventButton* button_event); + + class Gtk::VBox * vbox1; + class Inkscape::UI::View::SVGViewWidget * _previewArea; + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + + class Gtk::HBox * _page_selector_box; + class Gtk::Label * _labelSelect; + class Gtk::Label * _labelTotalPages; + class Gtk::SpinButton * _pageNumberSpin; + + const std::vector &_vec; // Document to be imported + unsigned _current_page; // Current selected page + bool _spinning; // whether SpinButton is pressed (i.e. we're "spinning") +}; + +CdrImportDialog::CdrImportDialog(const std::vector &vec) + : _previewArea(nullptr) + , _vec(vec) + , _current_page(1) + , _spinning(false) +{ + int num_pages = _vec.size(); + if ( num_pages <= 1 ) + return; + + // Dialog settings + this->set_title(_("Page Selector")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + + // Preview area + vbox1 = Gtk::manage(new class Gtk::VBox()); + this->get_content_area()->pack_start(*vbox1); + + // CONTROLS + _page_selector_box = Gtk::manage(new Gtk::HBox()); + + // "Select page:" label + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _page_selector_box->pack_start(*_labelSelect, Gtk::PACK_SHRINK); + + // Adjustment + spinner + auto pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _vec.size(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Gtk::SpinButton(pageNumberSpin_adj, 1, 0)); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_wrap(false); + _page_selector_box->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK); + + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + _page_selector_box->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK); + + vbox1->pack_end(*_page_selector_box, Gtk::PACK_SHRINK); + + // Buttons + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + this->add_action_widget(*cancelbutton, Gtk::RESPONSE_CANCEL); + this->add_action_widget(*okbutton, Gtk::RESPONSE_OK); + + // Show all widgets in dialog + this->show_all(); + + // Connect signals + _pageNumberSpin->signal_value_changed().connect(sigc::mem_fun(*this, &CdrImportDialog::_onPageNumberChanged)); + _pageNumberSpin->signal_button_press_event().connect_notify(sigc::mem_fun(*this, &CdrImportDialog::_onSpinButtonPress)); + _pageNumberSpin->signal_button_release_event().connect_notify(sigc::mem_fun(*this, &CdrImportDialog::_onSpinButtonRelease)); + + _setPreviewPage(); +} + +CdrImportDialog::~CdrImportDialog() = default; + +bool CdrImportDialog::showDialog() +{ + show(); + gint b = run(); + hide(); + if (b == Gtk::RESPONSE_OK || b == Gtk::RESPONSE_ACCEPT) { + return TRUE; + } else { + return FALSE; + } +} + +unsigned CdrImportDialog::getSelectedPage() +{ + return _current_page; +} + +void CdrImportDialog::_onPageNumberChanged() +{ + unsigned page = static_cast(_pageNumberSpin->get_value_as_int()); + _current_page = CLAMP(page, 1U, _vec.size()); + _setPreviewPage(); +} + +void CdrImportDialog::_onSpinButtonPress(GdkEventButton* /*button_event*/) +{ + _spinning = true; +} + +void CdrImportDialog::_onSpinButtonRelease(GdkEventButton* /*button_event*/) +{ + _spinning = false; + _setPreviewPage(); +} + +/** + * \brief Renders the given page's thumbnail + */ +void CdrImportDialog::_setPreviewPage() +{ + if (_spinning) { + return; + } + + SPDocument *doc = SPDocument::createNewDocFromMem(_vec[_current_page-1].cstr(), strlen(_vec[_current_page-1].cstr()), false); + if(!doc) { + g_warning("CDR import: Could not create preview for page %d", _current_page); + gchar const *no_preview_template = R"A( + + + + %s + + )A"; + gchar * no_preview = g_strdup_printf(no_preview_template, _("No preview")); + doc = SPDocument::createNewDocFromMem(no_preview, strlen(no_preview), false); + g_free(no_preview); + } + + if (!doc) { + std::cerr << "CdrImportDialog::_setPreviewPage: No document!" << std::endl; + return; + } + + if (_previewArea) { + _previewArea->setDocument(doc); + } else { + _previewArea = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + vbox1->pack_start(*_previewArea, Gtk::PACK_EXPAND_WIDGET, 0); + } + + _previewArea->setResize(400, 400); + _previewArea->show_all(); +} + +SPDocument *CdrInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGFileStream input(converted_uri); + g_free(converted_uri); + #else + RVNGFileStream input(uri); + #endif + + if (!libcdr::CDRDocument::isSupported(&input)) { + return nullptr; + } + + RVNGStringVector output; +#if WITH_LIBCDR01 + librevenge::RVNGSVGDrawingGenerator generator(output, "svg"); + + if (!libcdr::CDRDocument::parse(&input, &generator)) { +#else + if (!libcdr::CDRDocument::generateSVG(&input, output)) { +#endif + return nullptr; + } + + if (output.empty()) { + return nullptr; + } + + std::vector tmpSVGOutput; + for (unsigned i=0; i\n\n"); + tmpString.append(output[i]); + tmpSVGOutput.push_back(tmpString); + } + + unsigned page_num = 1; + + // If only one page is present, import that one without bothering user + if (tmpSVGOutput.size() > 1) { + CdrImportDialog *dlg = nullptr; + if (INKSCAPE.use_gui()) { + dlg = new CdrImportDialog(tmpSVGOutput); + if (!dlg->showDialog()) { + delete dlg; + throw Input::open_cancelled(); + } + } + + // Get needed page + if (dlg) { + page_num = dlg->getSelectedPage(); + if (page_num < 1) + page_num = 1; + if (page_num > tmpSVGOutput.size()) + page_num = tmpSVGOutput.size(); + } + } + + SPDocument * doc = SPDocument::createNewDocFromMem(tmpSVGOutput[page_num-1].cstr(), strlen(tmpSVGOutput[page_num-1].cstr()), TRUE); + + // Set viewBox if it doesn't exist + if (doc && !doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + return doc; +} + +#include "clear-n_.h" + +void CdrInput::init() +{ + /* CDR */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Corel DRAW Input") "\n" + "org.inkscape.input.cdr\n" + "\n" + ".cdr\n" + "image/x-xcdr\n" + "" N_("Corel DRAW 7-X4 files (*.cdr)") "\n" + "" N_("Open files saved in Corel DRAW 7-X4") "\n" + "\n" + "", new CdrInput()); + + /* CDT */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Corel DRAW templates input") "\n" + "org.inkscape.input.cdt\n" + "\n" + ".cdt\n" + "application/x-xcdt\n" + "" N_("Corel DRAW 7-13 template files (*.cdt)") "\n" + "" N_("Open files saved in Corel DRAW 7-13") "\n" + "\n" + "", new CdrInput()); + + /* CCX */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Corel DRAW Compressed Exchange files input") "\n" + "org.inkscape.input.ccx\n" + "\n" + ".ccx\n" + "application/x-xccx\n" + "" N_("Corel DRAW Compressed Exchange files (*.ccx)") "\n" + "" N_("Open compressed exchange files saved in Corel DRAW") "\n" + "\n" + "", new CdrInput()); + + /* CMX */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Corel DRAW Presentation Exchange files input") "\n" + "org.inkscape.input.cmx\n" + "\n" + ".cmx\n" + "application/x-xcmx\n" + "" N_("Corel DRAW Presentation Exchange files (*.cmx)") "\n" + "" N_("Open presentation exchange files saved in Corel DRAW") "\n" + "\n" + "", new CdrInput()); + + return; + +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBCDR */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/cdr-input.h b/src/extension/internal/cdr-input.h new file mode 100644 index 0000000..546151f --- /dev/null +++ b/src/extension/internal/cdr-input.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_CDROUTPUT_H__ +#define __EXTENSION_INTERNAL_CDROUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBCDR + +#include + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CdrInput : public Inkscape::Extension::Implementation::Implementation { + CdrInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBCDR */ +#endif /* __EXTENSION_INTERNAL_CDROUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/clear-n_.h b/src/extension/internal/clear-n_.h new file mode 100644 index 0000000..ecd8eaa --- /dev/null +++ b/src/extension/internal/clear-n_.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file clear-n_.h + + A way to clear the N_ macro, which is defined as an inline function. + Unfortunately, this makes it so it is hard to use in static strings + where you only want to translate a small part. Including this + turns it back into a a macro. +*/ +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef N_ +#undef N_ +#endif +#define N_(x) x + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/emf-inout.cpp b/src/extension/internal/emf-inout.cpp new file mode 100644 index 0000000..42291a8 --- /dev/null +++ b/src/extension/internal/emf-inout.cpp @@ -0,0 +1,3686 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows-only Enhanced Metafile input and output. + */ +/* Authors: + * Ulf Erikson + * Jon A. Cruz + * David Mathog + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include +#include +#include +#include <3rdparty/libuemf/symbol_convert.h> + +#include "clear-n_.h" +#include "display/drawing-item.h" +#include "display/drawing.h" +#include "document.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "extension/print.h" +#include "extension/system.h" +#include "inkscape.h" // even though it is included indirectly by emf-inout.h +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "print.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "util/units.h" // even though it is included indirectly by emf-inout.h + +#include "emf-print.h" +#include "emf-inout.h" + +#define PRINT_EMF "org.inkscape.print.emf" + +#ifndef U_PS_JOIN_MASK +#define U_PS_JOIN_MASK (U_PS_JOIN_BEVEL|U_PS_JOIN_MITER|U_PS_JOIN_ROUND) +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + +static uint32_t ICMmode = 0; // not used yet, but code to read it from EMF implemented +static uint32_t BLTmode = 0; +float faraway = 10000000; // used in "exclude" clips, hopefully well outside any real drawing! + +Emf::Emf () // The null constructor +{ + return; +} + + +Emf::~Emf () //The destructor +{ + return; +} + + +bool +Emf::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(PRINT_EMF)) + return FALSE; + return TRUE; +} + + +void +Emf::print_document_to_file(SPDocument *doc, const gchar *filename) +{ + Inkscape::Extension::Print *mod; + SPPrintContext context; + const gchar *oldconst; + gchar *oldoutput; + unsigned int ret; + + doc->ensureUpToDate(); + + mod = Inkscape::Extension::get_print(PRINT_EMF); + oldconst = mod->get_param_string("destination"); + oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + +/* Start */ + context.module = mod; + /* fixme: This has to go into module constructor somehow */ + /* Create new arena */ + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = mod->base->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + /* Print document */ + ret = mod->begin(doc); + if (ret) { + g_free(oldoutput); + throw Inkscape::Extension::Output::save_failed(); + } + mod->base->invoke_print(&context); + (void) mod->finish(); + /* Release arena */ + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // deleted by invoke_hide +/* end */ + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); + + return; +} + + +void +Emf::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + + ext = Inkscape::Extension::db.get(PRINT_EMF); + if (ext == nullptr) + return; + + bool new_val = mod->get_param_bool("textToPath"); + bool new_FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); // character position bug + // reserve FixPPT2 for opacity bug. Currently EMF does not export opacity values + bool new_FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); // dashed line bug + bool new_FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); // gradient bug + bool new_FixPPTLinGrad = mod->get_param_bool("FixPPTLinGrad"); // allow native rectangular linear gradient + bool new_FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); // force all patterns as standard EMF hatch + bool new_FixImageRot = mod->get_param_bool("FixImageRot"); // remove rotations on images + + TableGen( //possibly regenerate the unicode-convert tables + mod->get_param_bool("TnrToSymbol"), + mod->get_param_bool("TnrToWingdings"), + mod->get_param_bool("TnrToZapfDingbats"), + mod->get_param_bool("UsePUA") + ); + + ext->set_param_bool("FixPPTCharPos",new_FixPPTCharPos); // Remember to add any new ones to PrintEmf::init or a mysterious failure will result! + ext->set_param_bool("FixPPTDashLine",new_FixPPTDashLine); + ext->set_param_bool("FixPPTGrad2Polys",new_FixPPTGrad2Polys); + ext->set_param_bool("FixPPTLinGrad",new_FixPPTLinGrad); + ext->set_param_bool("FixPPTPatternAsHatch",new_FixPPTPatternAsHatch); + ext->set_param_bool("FixImageRot",new_FixImageRot); + ext->set_param_bool("textToPath", new_val); + + // ensure usage of dot as decimal separator in scanf/printf functions (independently of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + print_document_to_file(doc, filename); + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return; +} + + +/* given the transformation matrix from worldTransform return the scale in the matrix part. Assumes that the + matrix is not used to skew, invert, or make another distorting transformation. */ +double Emf::current_scale(PEMF_CALLBACK_DATA d){ + double scale = + d->dc[d->level].worldTransform.eM11 * d->dc[d->level].worldTransform.eM22 - + d->dc[d->level].worldTransform.eM12 * d->dc[d->level].worldTransform.eM21; + if(scale <= 0.0)scale=1.0; /* something is dreadfully wrong with the matrix, but do not crash over it */ + scale=sqrt(scale); + return(scale); +} + +/* given the transformation matrix from worldTransform and the current x,y position in inkscape coordinates, + generate an SVG transform that gives the same amount of rotation, no scaling, and maps x,y back onto x,y. This is used for + rotating objects when the location of at least one point in that object is known. Returns: + "matrix(a,b,c,d,e,f)" (WITH the double quotes) +*/ +std::string Emf::current_matrix(PEMF_CALLBACK_DATA d, double x, double y, int useoffset){ + SVGOStringStream cxform; + double scale = current_scale(d); + cxform << "\"matrix("; + cxform << d->dc[d->level].worldTransform.eM11/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM12/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM21/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM22/scale; cxform << ","; + if(useoffset){ + /* for the "new" coordinates drop the worldtransform translations, not used here */ + double newx = x * d->dc[d->level].worldTransform.eM11/scale + y * d->dc[d->level].worldTransform.eM21/scale; + double newy = x * d->dc[d->level].worldTransform.eM12/scale + y * d->dc[d->level].worldTransform.eM22/scale; + cxform << x - newx; cxform << ","; + cxform << y - newy; + } + else { + cxform << "0,0"; + } + cxform << ")\""; + return(cxform.str()); +} + +/* given the transformation matrix from worldTransform return the rotation angle in radians. + counter clockwise from the x axis. */ +double Emf::current_rotation(PEMF_CALLBACK_DATA d){ + return -std::atan2(d->dc[d->level].worldTransform.eM12, d->dc[d->level].worldTransform.eM11); +} + +/* Add another 100 blank slots to the hatches array. +*/ +void Emf::enlarge_hatches(PEMF_CALLBACK_DATA d){ + d->hatches.size += 100; + d->hatches.strings = (char **) realloc(d->hatches.strings,d->hatches.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_hatches(PEMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; ihatches.count; i++){ + if(strcmp(test,d->hatches.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a hatch. If a matching hatch already exists nothing happens. If one + does not exist it is added to the hatches list and also entered into . + This is also used to add the path part of the hatches, which they reference with a xlink:href +*/ +uint32_t Emf::add_hatch(PEMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor){ + char hatchname[64]; // big enough + char hpathname[64]; // big enough + char hbkname[64]; // big enough + char tmpcolor[8]; + char bkcolor[8]; + uint32_t idx; + + switch(hatchType){ + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].textColor)); + break; + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + break; + default: + sprintf(tmpcolor,"%6.6X",sethexcolor(hatchColor)); + break; + } + + /* For both bkMode types set the PATH + FOREGROUND COLOR for the indicated standard hatch. + This will be used late to compose, or recompose the transparent or opaque final hatch.*/ + + std::string refpath; // used to reference later the path pieces which are about to be created + sprintf(hpathname,"EMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hpathname); + auto & defs = d->defs; + if(!idx){ // add path/color if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hpathname); + + defs += "\n"; + switch(hatchType){ + case U_HS_HORIZONTAL: + defs += " \n"; + break; + case U_HS_VERTICAL: + defs += " \n"; + break; + case U_HS_FDIAGONAL: + defs += " \n"; + break; + case U_HS_BDIAGONAL: + defs += " \n"; + break; + case U_HS_CROSS: + defs += " \n"; + break; + case U_HS_DIAGCROSS: + defs += " \n"; + defs += " \n"; + break; + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + defs += " \n"; + break; + } + } + + // References to paths possibly just created above. These will be used in the actual patterns. + switch(hatchType){ + case U_HS_HORIZONTAL: + case U_HS_VERTICAL: + case U_HS_CROSS: + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + refpath += " \n"; + break; + case U_HS_FDIAGONAL: + case U_HS_BDIAGONAL: + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + break; + case U_HS_DIAGCROSS: + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + break; + } + + if(d->dc[d->level].bkMode == U_TRANSPARENT || hatchType >= U_HS_SOLIDCLR){ + sprintf(hatchname,"EMFhatch%d_%s",hatchType,tmpcolor); + sprintf(hpathname,"EMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " \n"; + defs += refpath; + defs += " \n"; + idx = d->hatches.count; + } + } + else { // bkMode==U_OPAQUE + /* Set up an object in the defs for this background, if there is not one already there */ + sprintf(bkcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + sprintf(hbkname,"EMFhbkclr_%s",bkcolor); + idx = in_hatches(d,hbkname); + if(!idx){ // add path/color if not already present. Hatchtype is not needed in the name. + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hbkname); + + defs += "\n"; + defs += " \n"; + } + + // this is the pattern, its name will show up in Inkscape's pattern selector + sprintf(hatchname,"EMFhatch%d_%s_%s",hatchType,tmpcolor,bkcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " \n"; + defs += " \n"; + defs += refpath; + defs += " \n"; + idx = d->hatches.count; + } + } + return(idx-1); +} + +/* Add another 100 blank slots to the images array. +*/ +void Emf::enlarge_images(PEMF_CALLBACK_DATA d){ + d->images.size += 100; + d->images.strings = (char **) realloc(d->images.strings,d->images.size * sizeof(char *)); +} + +/* See if the image string is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_images(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; iimages.count; i++){ + if(strcmp(test,d->images.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add an image. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into . + + U_EMRCREATEMONOBRUSH records only work when the bitmap is monochrome. If we hit one that isn't + set idx to 2^32-1 and let the caller handle it. +*/ +uint32_t Emf::add_image(PEMF_CALLBACK_DATA d, void *pEmr, uint32_t cbBits, uint32_t cbBmi, + uint32_t iUsage, uint32_t offBits, uint32_t offBmi){ + + uint32_t idx; + char imagename[64]; // big enough + char imrotname[64]; // big enough + char xywh[64]; // big enough + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + U_RGBQUAD ct2[2]; + uint32_t width, height, colortype, numCt, invert; // if needed these values will be set in get_DIB_params + if(cbBits && cbBmi && (iUsage == U_DIB_RGB_COLORS)){ + // next call returns pointers and values, but allocates no memory + dibparams = get_DIB_params((const char *)pEmr, offBits, offBmi, &px, (const U_RGBQUAD **) &ct, + &numCt, &width, &height, &colortype, &invert); + if(dibparams ==U_BI_RGB){ + // U_EMRCREATEMONOBRUSH uses text/bk colors instead of what is in the color map. + if(((PU_EMR)pEmr)->iType == U_EMR_CREATEMONOBRUSH){ + if(numCt==2){ + ct2[0] = U_RGB2BGR(d->dc[d->level].textColor); + ct2[1] = U_RGB2BGR(d->dc[d->level].bkColor); + ct = &ct2[0]; + } + else { // This record is invalid, nothing more to do here, let caller handle it + return(U_EMR_INVALID); + } + } + + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array in record + height, // Height of pixel array in record + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG || dibparams==U_BI_PNG){ // image was binary png or jpg in source file + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + + sprintf(imagename,"EMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " \n"; + defs += " "; + defs += " \n"; + } + g_free(base64String);//wait until this point to free because it might be a duplicate image + + /* image allows the inner image to be rotated nicely, load this one second only if needed + imagename retained from above + Here comes a dreadful hack. How do we determine if this rotation of the base image has already + been loaded? The image names contain no identifying information, they are just numbered sequentially. + So the rotated name is EMFrotimage###_XXXXXX, where ### is the number of the referred to image, and + XXXX is the rotation in radians x 1000000 and truncated. That is then stored in BASE64 as the "image". + The corresponding SVG generated though is not for an image, but a reference to an image. + The name of the pattern MUST still be EMFimage###_ref or output_style() will not be able to use it. + */ + if(current_rotation(d) >= 0.00001 || current_rotation(d) <= -0.00001){ /* some rotation, allow a little rounding error around 0 degrees */ + int tangle = round(current_rotation(d)*1000000.0); + sprintf(imrotname,"EMFrotimage%d_%d",idx-1,tangle); + base64String = g_base64_encode((guchar*) imrotname, strlen(imrotname) ); + idx = in_images(d, (char *) base64String); // scan for this "image" + if(!idx){ + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + sprintf(imrotname,"EMFimage%d",idx++); + + defs += "\n"; + defs += " gradients.size += 100; + d->gradients.strings = (char **) realloc(d->gradients.strings,d->gradients.size * sizeof(char *)); +} + +/* See if the gradient name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_gradients(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; igradients.count; i++){ + if(strcmp(test,d->gradients.strings[i])==0)return(i+1); + } + return(0); +} + +U_COLORREF trivertex_to_colorref(U_TRIVERTEX tv){ + U_COLORREF uc; + uc.Red = tv.Red >> 8; + uc.Green = tv.Green >> 8; + uc.Blue = tv.Blue >> 8; + uc.Reserved = tv.Alpha >> 8; // Not used + return(uc); +} + +/* (Conditionally) add a gradient. If a matching gradient already exists nothing happens. If one + does not exist it is added to the gradients list and also entered into . + Only call this with H or V gradient, not a triangle. +*/ +uint32_t Emf::add_gradient(PEMF_CALLBACK_DATA d, uint32_t gradientType, U_TRIVERTEX tv1, U_TRIVERTEX tv2){ + char hgradname[64]; // big enough + char tmpcolor1[8]; + char tmpcolor2[8]; + char gradc; + uint32_t idx; + std::string x2,y2; + + U_COLORREF gradientColor1 = trivertex_to_colorref(tv1); + U_COLORREF gradientColor2 = trivertex_to_colorref(tv2); + + + sprintf(tmpcolor1,"%6.6X",sethexcolor(gradientColor1)); + sprintf(tmpcolor2,"%6.6X",sethexcolor(gradientColor2)); + switch(gradientType){ + case U_GRADIENT_FILL_RECT_H: + gradc='H'; + x2="100"; + y2="0"; + break; + case U_GRADIENT_FILL_RECT_V: + gradc='V'; + x2="0"; + y2="100"; + break; + default: // this should never happen, but fill these in to avoid compiler warnings + gradc='!'; + x2="0"; + y2="0"; + break; + } + + /* Even though the gradient was defined as Horizontal or Vertical if the rectangle is rotated it needs to + be at some other alignment, and that needs gradienttransform. Set the name using the same sort of hack + as for add_image. + */ + int tangle = round(current_rotation(d)*1000000.0); + sprintf(hgradname,"LinGrd%c_%s_%s_%d",gradc,tmpcolor1,tmpcolor2,tangle); + + idx = in_gradients(d,hgradname); + if(!idx){ // gradient does not yet exist + if(d->gradients.count == d->gradients.size){ enlarge_gradients(d); } + d->gradients.strings[d->gradients.count++]=strdup(hgradname); + idx = d->gradients.count; + SVGOStringStream stmp; + stmp << " \n"; + stmp << " \n"; + stmp << " \n"; + stmp << " \n"; + d->defs += stmp.str().c_str(); + } + + return(idx-1); +} + +/* Add another 100 blank slots to the clips array. +*/ +void Emf::enlarge_clips(PEMF_CALLBACK_DATA d){ + d->clips.size += 100; + d->clips.strings = (char **) realloc(d->clips.strings,d->clips.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_clips(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; iclips.count; i++){ + if(strcmp(test,d->clips.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a clip. + If a matching clip already exists nothing happens + If one does exist it is added to the clips list, entered into . +*/ +void Emf::add_clips(PEMF_CALLBACK_DATA d, const char *clippath, unsigned int logic){ + int op = combine_ops_to_livarot(logic); + Geom::PathVector combined_vect; + char *combined = nullptr; + if (op >= 0 && d->dc[d->level].clip_id) { + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector old_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + Geom::PathVector new_vect = sp_svg_read_pathv(clippath); + combined_vect = sp_pathvector_boolop(new_vect, old_vect, (bool_op) op , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + combined = sp_svg_write_path(combined_vect); + } + else { + combined = strdup(clippath); // COPY operation, erases everything and starts a new one + } + + uint32_t idx = in_clips(d, combined); + if(!idx){ // add clip if not already present + if(d->clips.count == d->clips.size){ enlarge_clips(d); } + d->clips.strings[d->clips.count++]=strdup(combined); + d->dc[d->level].clip_id = d->clips.count; // one more than the slot where it is actually stored + SVGOStringStream tmp_clippath; + tmp_clippath << "\ndc[d->level].clip_id << "\""; + tmp_clippath << " >"; + tmp_clippath << "\n\t"; + tmp_clippath << "\n"; + d->outdef += tmp_clippath.str().c_str(); + } + else { + d->dc[d->level].clip_id = idx; + } + free(combined); +} + + + +void +Emf::output_style(PEMF_CALLBACK_DATA d, int iType) +{ +// SVGOStringStream tmp_id; + SVGOStringStream tmp_style; + char tmp[1024] = {0}; + + float fill_rgb[3]; + d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); + float stroke_rgb[3]; + d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + + // for U_EMR_BITBLT with no image, try to approximate some of these operations/ + // Assume src color is "white" + if(d->dwRop3){ + switch(d->dwRop3){ + case U_PATINVERT: // invert pattern + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + break; + case U_SRCINVERT: // treat all of these as black + case U_DSTINVERT: + case U_BLACKNESS: + case U_SRCERASE: + case U_NOTSRCCOPY: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + break; + case U_SRCCOPY: // treat all of these as white + case U_NOTSRCERASE: + case U_WHITENESS: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + break; + case U_SRCPAINT: // use the existing color + case U_SRCAND: + case U_MERGECOPY: + case U_MERGEPAINT: + case U_PATPAINT: + case U_PATCOPY: + default: + break; + } + d->dwRop3 = 0; // might as well reset it here, it must be set for each BITBLT + } + + // Implement some of these, the ones where the original screen color does not matter. + // The options that merge screen and pen colors cannot be done correctly because we + // have no way of knowing what color is already on the screen. For those just pass the + // pen color through. + switch(d->dwRop2){ + case U_R2_BLACK: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + break; + case U_R2_NOTMERGEPEN: + case U_R2_MASKNOTPEN: + break; + case U_R2_NOTCOPYPEN: + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + stroke_rgb[0] = 1.0 - stroke_rgb[0]; + stroke_rgb[1] = 1.0 - stroke_rgb[1]; + stroke_rgb[2] = 1.0 - stroke_rgb[2]; + break; + case U_R2_MASKPENNOT: + case U_R2_NOT: + case U_R2_XORPEN: + case U_R2_NOTMASKPEN: + case U_R2_NOTXORPEN: + case U_R2_NOP: + case U_R2_MERGENOTPEN: + case U_R2_COPYPEN: + case U_R2_MASKPEN: + case U_R2_MERGEPENNOT: + case U_R2_MERGEPEN: + break; + case U_R2_WHITE: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + break; + default: + break; + } + + +// tmp_id << "\n\tid=\"" << (d->id++) << "\""; +// d->outsvg += tmp_id.str().c_str(); + d->outsvg += "\n\tstyle=\""; + if (iType == U_EMR_STROKEPATH || !d->dc[d->level].fill_set) { + tmp_style << "fill:none;"; + } else { + switch(d->dc[d->level].fill_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "fill:url(#%s); ",d->hatches.strings[d->dc[d->level].fill_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "fill:url(#EMFimage%d_ref); ",d->dc[d->level].fill_idx); + tmp_style << tmp; + break; + case DRAW_LINEAR_GRADIENT: + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "fill:#%02x%02x%02x;", + SP_COLOR_F_TO_U(fill_rgb[0]), + SP_COLOR_F_TO_U(fill_rgb[1]), + SP_COLOR_F_TO_U(fill_rgb[2]) + ); + tmp_style << tmp; + break; + } + snprintf( + tmp, 1023, + "fill-rule:%s;", + (d->dc[d->level].style.fill_rule.value == SP_WIND_RULE_NONZERO ? "evenodd" : "nonzero") + ); + tmp_style << tmp; + tmp_style << "fill-opacity:1;"; + + // if the stroke is the same as the fill, and the right size not to change the end size of the object, do not do it separately + if( + (d->dc[d->level].fill_set ) && + (d->dc[d->level].stroke_set ) && + (d->dc[d->level].style.stroke_width.value == 1 ) && + (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && + ( + (d->dc[d->level].fill_mode != DRAW_PAINT) || + ( + (fill_rgb[0]==stroke_rgb[0]) && + (fill_rgb[1]==stroke_rgb[1]) && + (fill_rgb[2]==stroke_rgb[2]) + ) + ) + ){ + d->dc[d->level].stroke_set = false; + } + } + + if (iType == U_EMR_FILLPATH || !d->dc[d->level].stroke_set) { + tmp_style << "stroke:none;"; + } else { + switch(d->dc[d->level].stroke_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "stroke:url(#%s); ",d->hatches.strings[d->dc[d->level].stroke_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "stroke:url(#EMFimage%d_ref); ",d->dc[d->level].stroke_idx); + tmp_style << tmp; + break; + case DRAW_LINEAR_GRADIENT: + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "stroke:#%02x%02x%02x;", + SP_COLOR_F_TO_U(stroke_rgb[0]), + SP_COLOR_F_TO_U(stroke_rgb[1]), + SP_COLOR_F_TO_U(stroke_rgb[2]) + ); + tmp_style << tmp; + break; + } + tmp_style << "stroke-width:" << + MAX( 0.001, d->dc[d->level].style.stroke_width.value ) << "px;"; + + tmp_style << "stroke-linecap:" << + ( + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_BUTT ? "butt" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_ROUND ? "round" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_SQUARE ? "square" : + "unknown" + ) << ";"; + + tmp_style << "stroke-linejoin:" << + ( + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_MITER ? "miter" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_ROUND ? "round" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_BEVEL ? "bevel" : + "unknown" + ) << ";"; + + // Set miter limit if known, even if it is not needed immediately (not miter) + tmp_style << "stroke-miterlimit:" << + MAX( 2.0, d->dc[d->level].style.stroke_miterlimit.value ) << ";"; + + if (d->dc[d->level].style.stroke_dasharray.set && + !d->dc[d->level].style.stroke_dasharray.values.empty() ) + { + tmp_style << "stroke-dasharray:"; + for (unsigned i=0; idc[d->level].style.stroke_dasharray.values.size(); i++) { + if (i) + tmp_style << ","; + tmp_style << d->dc[d->level].style.stroke_dasharray.values[i].value; + } + tmp_style << ";"; + tmp_style << "stroke-dashoffset:0;"; + } else { + tmp_style << "stroke-dasharray:none;"; + } + tmp_style << "stroke-opacity:1;"; + } + tmp_style << "\" "; + if (d->dc[d->level].clip_id) + tmp_style << "\n\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\" "; + + d->outsvg += tmp_style.str().c_str(); +} + + +double +Emf::_pix_x_to_point(PEMF_CALLBACK_DATA d, double px) +{ + double scale = (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0); + double tmp; + tmp = ((((double) (px - d->dc[d->level].winorg.x))*scale) + d->dc[d->level].vieworg.x) * d->D2PscaleX; + tmp -= d->ulCornerOutX; //The EMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + +double +Emf::_pix_y_to_point(PEMF_CALLBACK_DATA d, double py) +{ + double scale = (d->dc[d->level].ScaleInY ? d->dc[d->level].ScaleInY : 1.0); + double tmp; + tmp = ((((double) (py - d->dc[d->level].winorg.y))*scale) * d->E2IdirY + d->dc[d->level].vieworg.y) * d->D2PscaleY; + tmp -= d->ulCornerOutY; //The EMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + + +double +Emf::pix_to_x_point(PEMF_CALLBACK_DATA d, double px, double py) +{ + double wpx = px * d->dc[d->level].worldTransform.eM11 + py * d->dc[d->level].worldTransform.eM21 + d->dc[d->level].worldTransform.eDx; + double x = _pix_x_to_point(d, wpx); + + return x; +} + +double +Emf::pix_to_y_point(PEMF_CALLBACK_DATA d, double px, double py) +{ + + double wpy = px * d->dc[d->level].worldTransform.eM12 + py * d->dc[d->level].worldTransform.eM22 + d->dc[d->level].worldTransform.eDy; + double y = _pix_y_to_point(d, wpy); + + return y; + +} + +double +Emf::pix_to_abs_size(PEMF_CALLBACK_DATA d, double px) +{ + double ppx = fabs(px * (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0) * d->D2PscaleX * current_scale(d)); + return ppx; +} + +/* snaps coordinate pairs made up of values near +/-faraway, +/-faraway to exactly faraway. + This eliminates coordinate drift on repeated clipping cycles which use exclude. + It should not affect internals of normal drawings because the value of faraway is so large. +*/ +void +Emf::snap_to_faraway_pair(double *x, double *y) +{ + if((std::abs(std::abs(*x) - faraway)/faraway <= 1e-4) && (std::abs(std::abs(*y) - faraway)/faraway <= 1e-4)){ + *x = (*x > 0 ? faraway : -faraway); + *y = (*y > 0 ? faraway : -faraway); + } +} + +/* returns "x,y" (without the quotes) in inkscape coordinates for a pair of EMF x,y coordinates. + Since exclude clip can go through here, it calls snap_to_faraway_pair for numerical stability. +*/ +std::string Emf::pix_to_xy(PEMF_CALLBACK_DATA d, double x, double y){ + SVGOStringStream cxform; + double tx = pix_to_x_point(d,x,y); + double ty = pix_to_y_point(d,x,y); + snap_to_faraway_pair(&tx,&ty); + cxform << tx; + cxform << ","; + cxform << ty; + return(cxform.str()); +} + + +void +Emf::select_pen(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMRCREATEPEN pEmr = nullptr; + + if (index >= 0 && index < d->n_obj){ + pEmr = (PU_EMRCREATEPEN) d->emf_obj[index].lpEMFR; + } + + if (!pEmr){ return; } + + switch (pEmr->lopn.lopnStyle & U_PS_STYLE_MASK) { + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + SPILength spilength(1.f); + int penstyle = (pEmr->lopn.lopnStyle & U_PS_STYLE_MASK); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + + case U_PS_SOLID: + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (pEmr->lopn.lopnStyle & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + + switch (pEmr->lopn.lopnStyle & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + + d->dc[d->level].stroke_set = true; + + if (pEmr->lopn.lopnStyle == U_PS_NULL) { + d->dc[d->level].style.stroke_width.value = 0; + d->dc[d->level].stroke_set = false; + } else if (pEmr->lopn.lopnWidth.x) { + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, pEmr->lopn.lopnWidth.x ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + //d->dc[d->level].style.stroke_width.value = 1.0; + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } + + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lopn.lopnColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lopn.lopnColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lopn.lopnColor) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); +} + + +void +Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMREXTCREATEPEN pEmr = nullptr; + + if (index >= 0 && index < d->n_obj) + pEmr = (PU_EMREXTCREATEPEN) d->emf_obj[index].lpEMFR; + + if (!pEmr) + return; + + switch (pEmr->elp.elpPenStyle & U_PS_STYLE_MASK) { + case U_PS_USERSTYLE: + { + if (pEmr->elp.elpNumEntries) { + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + for (unsigned int i=0; ielp.elpNumEntries; i++) { + double dash_length = pix_to_abs_size( d, pEmr->elp.elpStyleEntry[i] ); + d->dc[d->level].style.stroke_dasharray.values.emplace_back(dash_length); + } + d->dc[d->level].style.stroke_dasharray.set = true; + } else { + d->dc[d->level].style.stroke_dasharray.set = false; + } + break; + } + + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + int penstyle = (pEmr->elp.elpPenStyle & U_PS_STYLE_MASK); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + SPILength spilength; + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + case U_PS_SOLID: +/* includes these for now, some should maybe not be in here + case U_PS_NULL: + case U_PS_INSIDEFRAME: + case U_PS_ALTERNATE: + case U_PS_STYLE_MASK: +*/ + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (pEmr->elp.elpPenStyle & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; + break; + } + case U_PS_ENDCAP_SQUARE: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; + break; + } + case U_PS_ENDCAP_FLAT: + default: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; + break; + } + } + + switch (pEmr->elp.elpPenStyle & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; + break; + } + case U_PS_JOIN_MITER: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; + break; + } + case U_PS_JOIN_ROUND: + default: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; + break; + } + } + + d->dc[d->level].stroke_set = true; + + if (pEmr->elp.elpPenStyle == U_PS_NULL) { // draw nothing, but fill out all the values with something + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke_width.value = 0; + d->dc[d->level].stroke_set = false; + d->dc[d->level].stroke_mode = DRAW_PAINT; + } + else { + if (pEmr->elp.elpWidth) { + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, pEmr->elp.elpWidth ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + //d->dc[d->level].style.stroke_width.value = 1.0; + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } + + if( pEmr->elp.elpBrushStyle == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->elp.elpColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->elp.elpColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->elp.elpColor) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + } + else if(pEmr->elp.elpBrushStyle == U_BS_HATCHED){ + d->dc[d->level].stroke_idx = add_hatch(d, pEmr->elp.elpHatch, pEmr->elp.elpColor); + d->dc[d->level].stroke_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].stroke_mode = DRAW_PATTERN; + d->dc[d->level].stroke_set = true; + } + else if(pEmr->elp.elpBrushStyle == U_BS_DIBPATTERN || pEmr->elp.elpBrushStyle == U_BS_DIBPATTERNPT){ + d->dc[d->level].stroke_idx = add_image(d, (void *)pEmr, pEmr->cbBits, pEmr->cbBmi, *(uint32_t *) &(pEmr->elp.elpColor), pEmr->offBits, pEmr->offBmi); + d->dc[d->level].stroke_mode = DRAW_IMAGE; + d->dc[d->level].stroke_set = true; + } + else { // U_BS_PATTERN and anything strange that falls in, stroke is solid textColor + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + } + } +} + + +void +Emf::select_brush(PEMF_CALLBACK_DATA d, int index) +{ + uint32_t tidx; + uint32_t iType; + + if (index >= 0 && index < d->n_obj){ + iType = ((PU_EMR) (d->emf_obj[index].lpEMFR))->iType; + if(iType == U_EMR_CREATEBRUSHINDIRECT){ + PU_EMRCREATEBRUSHINDIRECT pEmr = (PU_EMRCREATEBRUSHINDIRECT) d->emf_obj[index].lpEMFR; + if( pEmr->lb.lbStyle == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lb.lbColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lb.lbColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lb.lbColor) ); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + } + else if(pEmr->lb.lbStyle == U_BS_HATCHED){ + d->dc[d->level].fill_idx = add_hatch(d, pEmr->lb.lbHatch, pEmr->lb.lbColor); + d->dc[d->level].fill_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].fill_mode = DRAW_PATTERN; + d->dc[d->level].fill_set = true; + } + } + else if(iType == U_EMR_CREATEDIBPATTERNBRUSHPT || iType == U_EMR_CREATEMONOBRUSH){ + PU_EMRCREATEDIBPATTERNBRUSHPT pEmr = (PU_EMRCREATEDIBPATTERNBRUSHPT) d->emf_obj[index].lpEMFR; + tidx = add_image(d, (void *) pEmr, pEmr->cbBits, pEmr->cbBmi, pEmr->iUsage, pEmr->offBits, pEmr->offBmi); + if(tidx == U_EMR_INVALID){ // This happens if createmonobrush has a DIB that isn't monochrome + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + } +} + + +void +Emf::select_font(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMREXTCREATEFONTINDIRECTW pEmr = nullptr; + + if (index >= 0 && index < d->n_obj) + pEmr = (PU_EMREXTCREATEFONTINDIRECTW) d->emf_obj[index].lpEMFR; + + if (!pEmr)return; + + + /* The logfont information always starts with a U_LOGFONT structure but the U_EMREXTCREATEFONTINDIRECTW + is defined as U_LOGFONT_PANOSE so it can handle one of those if that is actually present. Currently only logfont + is supported, and the remainder, it it really is a U_LOGFONT_PANOSE record, is ignored + */ + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double font_size = pix_to_abs_size( d, pEmr->elfw.elfLogFont.lfHeight ); + /* snap the font_size to the nearest 1/32nd of a point. + (The size is converted from Pixels to points, snapped, and converted back.) + See the notes where d->D2Pscale[XY] are set for the reason why. + Typically this will set the font to the desired exact size. If some peculiar size + was intended this will, at worst, make it .03125 off, which is unlikely to be a problem. */ + font_size = round(20.0 * 0.8 * font_size)/(20.0 * 0.8); + d->level = cur_level; + d->dc[d->level].style.font_size.computed = font_size; + d->dc[d->level].style.font_weight.value = + pEmr->elfw.elfLogFont.lfWeight == U_FW_THIN ? SP_CSS_FONT_WEIGHT_100 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_200 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_LIGHT ? SP_CSS_FONT_WEIGHT_300 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_400 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_MEDIUM ? SP_CSS_FONT_WEIGHT_500 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_SEMIBOLD ? SP_CSS_FONT_WEIGHT_600 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_700 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_800 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_HEAVY ? SP_CSS_FONT_WEIGHT_900 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_NORMAL : + pEmr->elfw.elfLogFont.lfWeight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_BOLD : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_LIGHTER : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_BOLDER : + SP_CSS_FONT_WEIGHT_NORMAL; + d->dc[d->level].style.font_style.value = (pEmr->elfw.elfLogFont.lfItalic ? SP_CSS_FONT_STYLE_ITALIC : SP_CSS_FONT_STYLE_NORMAL); + d->dc[d->level].style.text_decoration_line.underline = pEmr->elfw.elfLogFont.lfUnderline; + d->dc[d->level].style.text_decoration_line.line_through = pEmr->elfw.elfLogFont.lfStrikeOut; + d->dc[d->level].style.text_decoration_line.set = true; + d->dc[d->level].style.text_decoration_line.inherit = false; + // malformed EMF with empty filename may exist, ignore font change if encountered + char *ctmp = U_Utf16leToUtf8((uint16_t *) (pEmr->elfw.elfLogFont.lfFaceName), U_LF_FACESIZE, nullptr); + if(ctmp){ + if (d->dc[d->level].font_name){ free(d->dc[d->level].font_name); } + if(*ctmp){ + d->dc[d->level].font_name = ctmp; + } + else { // Malformed EMF might specify an empty font name + free(ctmp); + d->dc[d->level].font_name = strdup("Arial"); // Default font, EMF spec says device can pick whatever it wants + } + } + d->dc[d->level].style.baseline_shift.value = round((double)((pEmr->elfw.elfLogFont.lfEscapement + 3600) % 3600)) / 10.0; // use baseline_shift instead of text_transform to avoid overflow +} + +void +Emf::delete_object(PEMF_CALLBACK_DATA d, int index) +{ + if (index >= 0 && index < d->n_obj) { + d->emf_obj[index].type = 0; +// We are keeping a copy of the EMR rather than just a structure. Currently that is not necessary as the entire +// EMF is read in at once and is stored in a big malloc. However, in past versions it was handled +// record by record, and we might need to do that again at some point in the future if we start running into EMF +// files too big to fit into memory. + if (d->emf_obj[index].lpEMFR) + free(d->emf_obj[index].lpEMFR); + d->emf_obj[index].lpEMFR = nullptr; + } +} + + +void +Emf::insert_object(PEMF_CALLBACK_DATA d, int index, int type, PU_ENHMETARECORD pObj) +{ + if (index >= 0 && index < d->n_obj) { + delete_object(d, index); + d->emf_obj[index].type = type; + d->emf_obj[index].level = d->level; + d->emf_obj[index].lpEMFR = emr_dup((char *) pObj); + } +} + +/* Identify probable Adobe Illustrator produced EMF files, which do strange things with the scaling. + The few so far observed all had this format. +*/ +int Emf::AI_hack(PU_EMRHEADER pEmr){ + int ret=0; + char *ptr; + ptr = (char *)pEmr; + PU_EMRSETMAPMODE nEmr = (PU_EMRSETMAPMODE) (ptr + pEmr->emr.nSize); + char *string = nullptr; + if(pEmr->nDescription)string = U_Utf16leToUtf8((uint16_t *)((char *) pEmr + pEmr->offDescription), pEmr->nDescription, nullptr); + if(string){ + if((pEmr->nDescription >= 13) && + (0==strcmp("Adobe Systems",string)) && + (nEmr->emr.iType == U_EMR_SETMAPMODE) && + (nEmr->iMode == U_MM_ANISOTROPIC)){ ret=1; } + free(string); + } + return(ret); +} + +/** + \fn create a UTF-32LE buffer and fill it with UNICODE unknown character + \param count number of copies of the Unicode unknown character to fill with +*/ +uint32_t *Emf::unknown_chars(size_t count){ + uint32_t *res = (uint32_t *) malloc(sizeof(uint32_t) * (count + 1)); + if(!res)throw "Inkscape fatal memory allocation error - cannot continue"; + for(uint32_t i=0; idc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + uint32_t width, height, colortype, numCt, invert; // if needed these values will be set in get_DIB_params + if(cbBits && cbBmi && (iUsage == U_DIB_RGB_COLORS)){ + // next call returns pointers and values, but allocates no memory + dibparams = get_DIB_params((const char *)pEmr, offBits, offBmi, &px, (const U_RGBQUAD **) &ct, + &numCt, &width, &height, &colortype, &invert); + if(dibparams ==U_BI_RGB){ + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG){ // image was binary jpg in source file + tmp_image << " xlink:href=\"data:image/jpeg;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(dibparams==U_BI_PNG){ // image was binary png in source file + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + + tmp_image << " transform=" << current_matrix(d, dx, dy, 1); // calculate appropriate offset + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \fn myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA lpData) + \param contents binary contents of an EMF file + \param length length in bytes of contents + \param d Inkscape data structures returned by this call +*/ +//THis was a callback, just build it into a normal function +int Emf::myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA d) +{ + uint32_t off=0; + uint32_t emr_mask; + int OK =1; + int file_status=1; + uint32_t nSize; + uint32_t iType; + const char *blimit = contents + length; + PU_ENHMETARECORD lpEMFR; + TCHUNK_SPECS tsp; + uint32_t tbkMode = U_TRANSPARENT; // holds proposed change to bkMode, if text is involved saving these to the DC must wait until the text is written + U_COLORREF tbkColor = U_RGB(255, 255, 255); // holds proposed change to bkColor + + // code for end user debugging + int eDbgRecord=0; + int eDbgComment=0; + int eDbgFinal=0; + char const* eDbgString = getenv( "INKSCAPE_DBG_EMF" ); + if ( eDbgString != nullptr ) { + if(strstr(eDbgString,"RECORD")){ eDbgRecord = 1; } + if(strstr(eDbgString,"COMMENT")){ eDbgComment = 1; } + if(strstr(eDbgString,"FINAL")){ eDbgFinal = 1; } + } + + /* initialize the tsp for text reassembly */ + tsp.string = nullptr; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.spaces = 0; // this field is only used for debugging + tsp.color.Red = 0; /* RGB Black */ + tsp.color.Green = 0; /* RGB Black */ + tsp.color.Blue = 0; /* RGB Black */ + tsp.color.Reserved = 0; /* not used */ + tsp.italics = 0; + tsp.weight = 80; + tsp.decoration = TXTDECOR_NONE; + tsp.condensed = 100; + tsp.co = 0; + tsp.fi_idx = -1; /* set to an invalid */ + + while(OK){ + if(off>=length)return(0); //normally should exit from while after EMREOF sets OK to false. + + // check record sizes and types thoroughly + int badrec = 0; + if (!U_emf_record_sizeok(contents + off, blimit, &nSize, &iType, 1) || + !U_emf_record_safe(contents + off)){ + badrec = 1; + } + else { + emr_mask = emr_properties(iType); + if (emr_mask == U_EMR_INVALID) { badrec = 1; } + } + if (badrec) { + file_status = 0; + break; + } + + lpEMFR = (PU_ENHMETARECORD)(contents + off); + +// At run time define environment variable INKSCAPE_DBG_EMF to include string RECORD. +// Users may employ this to track down toxic records + if(eDbgRecord){ + std::cout << "record type: " << iType << " name " << U_emr_names(iType) << " length: " << nSize << " offset: " << off <dirty:"<< d->tri->dirty << " emr_mask: " << std::hex << emr_mask << std::dec << std::endl; + + // incompatible change to text drawing detected (color or background change) forces out existing text + // OR + // next record is valid type and forces pending text to be drawn immediately + if ((d->dc[d->level].dirty & DIRTY_TEXT) || ((emr_mask != U_EMR_INVALID) && (emr_mask & U_DRAW_TEXT) && d->tri->dirty)){ + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\ndc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + SVGOStringStream ts; + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + if (d->dc[d->level].clip_id){ + d->outsvg += "\n\n"; + } + } + if(d->dc[d->level].dirty){ //Apply the delayed background changes, clear the flag + d->dc[d->level].bkMode = tbkMode; + memcpy(&(d->dc[d->level].bkColor),&tbkColor, sizeof(U_COLORREF)); + + if(d->dc[d->level].dirty & DIRTY_TEXT){ + // U_COLORREF and TRCOLORREF are exactly the same in memory, but the compiler needs some convincing... + if(tbkMode == U_TRANSPARENT){ (void) trinfo_load_bk(d->tri, BKCLR_NONE, *(TRCOLORREF *) &tbkColor); } + else { (void) trinfo_load_bk(d->tri, BKCLR_LINE, *(TRCOLORREF *) &tbkColor); } // Opaque + } + + /* It is possible to have a series of EMF records that would result in + the following creating hash patterns which are never used. For instance, if + there were a series of records that changed the background color but did nothing + else. + */ + if((d->dc[d->level].stroke_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_STROKE)){ + select_extpen(d, d->dc[d->level].stroke_recidx); + } + + if((d->dc[d->level].fill_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_FILL)){ + select_brush(d, d->dc[d->level].fill_recidx); + } + + d->dc[d->level].dirty = 0; + } + +// std::cout << "BEFORE DRAW logic d->mask: " << std::hex << d->mask << " emr_mask: " << emr_mask << std::dec << std::endl; +/* +std::cout << "BEFORE DRAW" + << " test0 " << ( d->mask & U_DRAW_VISIBLE) + << " test1 " << ( d->mask & U_DRAW_FORCE) + << " test2 " << (emr_mask & U_DRAW_ALTERS) + << " test3 " << (emr_mask & U_DRAW_VISIBLE) + << " test4 " << !(d->mask & U_DRAW_ONLYTO) + << " test5 " << ((d->mask & U_DRAW_ONLYTO) && !(emr_mask & U_DRAW_ONLYTO) ) + << std::endl; +*/ + + if( + (emr_mask != U_EMR_INVALID) && // next record is valid type + (d->mask & U_DRAW_VISIBLE) && // Current set of objects are drawable + ( + (d->mask & U_DRAW_FORCE) || // This draw is forced by STROKE/FILL/STROKEANDFILL PATH + (emr_mask & U_DRAW_ALTERS) || // Next record would alter the drawing environment in some way + ( + (emr_mask & U_DRAW_VISIBLE) && // Next record is visible... + ( + ( !(d->mask & U_DRAW_ONLYTO) ) || // Non *TO records cannot be followed by any Visible + ((d->mask & U_DRAW_ONLYTO) && !(emr_mask & U_DRAW_ONLYTO) )// *TO records can only be followed by other *TO records + ) + ) + ) + ){ +// std::cout << "PATH DRAW at TOP path" << *(d->path) << std::endl; + if(!(d->path.empty())){ + d->outsvg += " drawtype){ // explicit draw type EMR record + output_style(d, d->drawtype); + } + else if(d->mask & U_DRAW_CLOSED){ // implicit draw type + output_style(d, U_EMR_STROKEANDFILLPATH); + } + else { + output_style(d, U_EMR_STROKEPATH); + } + d->outsvg += "\n\t"; + d->outsvg += "\n\td=\""; // this is the ONLY place d=" should be used!!!! One exception, gradientfill. + d->outsvg += d->path; + d->outsvg += " \" /> \n"; + d->path = ""; + } + // reset the flags + d->mask = 0; + d->drawtype = 0; + } +// std::cout << "AFTER DRAW logic d->mask: " << std::hex << d->mask << " emr_mask: " << emr_mask << std::dec << std::endl; + + switch (iType) + { + case U_EMR_HEADER: + { + dbg_str << "\n"; + + d->outdef += "\n"; + + if (d->pDesc) { + d->outdef += "\n"; + } + + PU_EMRHEADER pEmr = (PU_EMRHEADER) lpEMFR; + SVGOStringStream tmp_outdef; + tmp_outdef << "MM100InX = pEmr->rclFrame.right - pEmr->rclFrame.left + 1; + d->MM100InY = pEmr->rclFrame.bottom - pEmr->rclFrame.top + 1; + d->PixelsInX = pEmr->rclBounds.right - pEmr->rclBounds.left + 1; + d->PixelsInY = pEmr->rclBounds.bottom - pEmr->rclBounds.top + 1; + + /* + calculate ratio of Inkscape dpi/EMF device dpi + This can cause problems later due to accuracy limits in the EMF. A high resolution + EMF might have a final D2Pscale[XY] of 0.074998, and adjusting the (integer) device size + by 1 will still not get it exactly to 0.075. Later when the font size is calculated it + can end up as 29.9992 or 22.4994 instead of the intended 30 or 22.5. This is handled by + snapping font sizes to the nearest .01. The best estimate is made by using both values. + */ + if ((pEmr->szlMillimeters.cx + pEmr->szlMillimeters.cy) && ( pEmr->szlDevice.cx + pEmr->szlDevice.cy)){ + d->E2IdirY = 1.0; // assume MM_TEXT, if not, this will be changed later + d->D2PscaleX = d->D2PscaleY = Inkscape::Util::Quantity::convert(1, "mm", "px") * + (double)(pEmr->szlMillimeters.cx + pEmr->szlMillimeters.cy)/ + (double)( pEmr->szlDevice.cx + pEmr->szlDevice.cy); + } + trinfo_load_qe(d->tri, d->D2PscaleX); /* quantization error that will affect text positions */ + + /* Adobe Illustrator files set mapmode to MM_ANISOTROPIC and somehow or other this + converts the rclFrame values from MM_HIMETRIC to MM_HIENGLISH, with another factor of 3 thrown + in for good measure. Ours not to question why... + */ + if(AI_hack(pEmr)){ + d->MM100InX *= 25.4/(10.0*3.0); + d->MM100InY *= 25.4/(10.0*3.0); + d->D2PscaleX *= 25.4/(10.0*3.0); + d->D2PscaleY *= 25.4/(10.0*3.0); + } + + d->MMX = d->MM100InX / 100.0; + d->MMY = d->MM100InY / 100.0; + + d->PixelsOutX = Inkscape::Util::Quantity::convert(d->MMX, "mm", "px"); + d->PixelsOutY = Inkscape::Util::Quantity::convert(d->MMY, "mm", "px"); + + // Upper left corner, from header rclBounds, in device units, usually both 0, but not always + d->ulCornerInX = pEmr->rclBounds.left; + d->ulCornerInY = pEmr->rclBounds.top; + d->ulCornerOutX = d->ulCornerInX * d->D2PscaleX; + d->ulCornerOutY = d->ulCornerInY * d->E2IdirY * d->D2PscaleY; + + tmp_outdef << + " width=\"" << d->MMX << "mm\"\n" << + " height=\"" << d->MMY << "mm\">\n"; + d->outdef += tmp_outdef.str().c_str(); + d->outdef += ""; // temporary end of header + + // d->defs holds any defines which are read in. + + tmp_outsvg << "\n\n\n"; // start of main body + + if (pEmr->nHandles) { + d->n_obj = pEmr->nHandles; + d->emf_obj = new EMF_OBJECT[d->n_obj]; + + // Init the new emf_obj list elements to null, provided the + // dynamic allocation succeeded. + if ( d->emf_obj != nullptr ) + { + for( int i=0; i < d->n_obj; ++i ) + d->emf_obj[i].lpEMFR = nullptr; + } //if + + } else { + d->emf_obj = nullptr; + } + + break; + } + case U_EMR_POLYBEZIER: + { + dbg_str << "\n"; + + PU_EMRPOLYBEZIER pEmr = (PU_EMRPOLYBEZIER) lpEMFR; + uint32_t i,j; + + if (pEmr->cptl<4) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y) << " "; + + for (i=1; icptl; ) { + tmp_str << "\n\tC "; + for (j=0; j<3 && icptl; j++,i++) { + tmp_str << pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y) << " "; + } + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYGON: + { + dbg_str << "\n"; + + PU_EMRPOLYGON pEmr = (PU_EMRPOLYGON) lpEMFR; + uint32_t i; + + if (pEmr->cptl < 2) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y ) << " "; + + for (i=1; icptl; i++) { + tmp_str << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + tmp_path << " z"; + + break; + } + case U_EMR_POLYLINE: + { + dbg_str << "\n"; + + PU_EMRPOLYLINE pEmr = (PU_EMRPOLYLINE) lpEMFR; + uint32_t i; + + if (pEmr->cptl<2) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y ) << " "; + + for (i=1; icptl; i++) { + tmp_str << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYBEZIERTO: + { + dbg_str << "\n"; + + PU_EMRPOLYBEZIERTO pEmr = (PU_EMRPOLYBEZIERTO) lpEMFR; + uint32_t i,j; + + d->mask |= emr_mask; + + for (i=0; icptl;) { + tmp_path << "\n\tC "; + for (j=0; j<3 && icptl; j++,i++) { + tmp_path << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + } + + break; + } + case U_EMR_POLYLINETO: + { + dbg_str << "\n"; + + PU_EMRPOLYLINETO pEmr = (PU_EMRPOLYLINETO) lpEMFR; + uint32_t i; + + d->mask |= emr_mask; + + for (i=0; icptl;i++) { + tmp_path << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + break; + } + case U_EMR_POLYPOLYLINE: + case U_EMR_POLYPOLYGON: + { + if (lpEMFR->iType == U_EMR_POLYPOLYLINE) + dbg_str << "\n"; + if (lpEMFR->iType == U_EMR_POLYPOLYGON) + dbg_str << "\n"; + + PU_EMRPOLYPOLYGON pEmr = (PU_EMRPOLYPOLYGON) lpEMFR; + unsigned int n, i, j; + + d->mask |= emr_mask; + + U_POINTL *aptl = (PU_POINTL) &pEmr->aPolyCounts[pEmr->nPolys]; + + i = 0; + for (n=0; nnPolys && icptl; n++) { + SVGOStringStream poly_path; + + poly_path << "\n\tM " << pix_to_xy( d, aptl[i].x, aptl[i].y) << " "; + i++; + + for (j=1; jaPolyCounts[n] && icptl; j++) { + poly_path << "\n\tL " << pix_to_xy( d, aptl[i].x, aptl[i].y) << " "; + i++; + } + + tmp_str << poly_path.str().c_str(); + if (lpEMFR->iType == U_EMR_POLYPOLYGON) + tmp_str << " z"; + tmp_str << " \n"; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_SETWINDOWEXTEX: + { + dbg_str << "\n"; + + PU_EMRSETWINDOWEXTEX pEmr = (PU_EMRSETWINDOWEXTEX) lpEMFR; + + d->dc[d->level].sizeWnd = pEmr->szlExtent; + + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd.cx = d->PixelsOutX; + d->dc[d->level].sizeWnd.cy = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + } + + /* scales logical to EMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.cx && d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.cx / (double) d->dc[d->level].sizeWnd.cx; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.cy / (double) d->dc[d->level].sizeWnd.cy; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_EMR_SETWINDOWORGEX: + { + dbg_str << "\n"; + + PU_EMRSETWINDOWORGEX pEmr = (PU_EMRSETWINDOWORGEX) lpEMFR; + d->dc[d->level].winorg = pEmr->ptlOrigin; + break; + } + case U_EMR_SETVIEWPORTEXTEX: + { + dbg_str << "\n"; + + PU_EMRSETVIEWPORTEXTEX pEmr = (PU_EMRSETVIEWPORTEXTEX) lpEMFR; + + d->dc[d->level].sizeView = pEmr->szlExtent; + + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView.cx = d->PixelsOutX; + d->dc[d->level].sizeView.cy = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + } + + /* scales logical to EMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.cx && d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.cx / (double) d->dc[d->level].sizeWnd.cx; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.cy / (double) d->dc[d->level].sizeWnd.cy; + if( d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_EMR_SETVIEWPORTORGEX: + { + dbg_str << "\n"; + + PU_EMRSETVIEWPORTORGEX pEmr = (PU_EMRSETVIEWPORTORGEX) lpEMFR; + d->dc[d->level].vieworg = pEmr->ptlOrigin; + break; + } + case U_EMR_SETBRUSHORGEX: dbg_str << "\n"; break; + case U_EMR_EOF: + { + dbg_str << "\n"; + + tmp_outsvg << "\n"; + d->outsvg = d->outdef + d->defs + d->outsvg; + OK=0; + break; + } + case U_EMR_SETPIXELV: dbg_str << "\n"; break; + case U_EMR_SETMAPPERFLAGS: dbg_str << "\n"; break; + case U_EMR_SETMAPMODE: + { + dbg_str << "\n"; + PU_EMRSETMAPMODE pEmr = (PU_EMRSETMAPMODE) lpEMFR; + switch (pEmr->iMode){ + case U_MM_TEXT: + default: + // Use all values from the header. + break; + /* For all of the following the indicated scale this will be encoded in WindowExtEx/ViewportExtex + and show up in ScaleIn[XY] + */ + case U_MM_LOMETRIC: // 1 LU = 0.1 mm, + case U_MM_HIMETRIC: // 1 LU = 0.01 mm + case U_MM_LOENGLISH: // 1 LU = 0.1 in + case U_MM_HIENGLISH: // 1 LU = 0.01 in + case U_MM_TWIPS: // 1 LU = 1/1440 in + d->E2IdirY = -1.0; + // Use d->D2Pscale[XY] values from the header. + break; + case U_MM_ISOTROPIC: // ScaleIn[XY] should be set elsewhere by SETVIEWPORTEXTEX and SETWINDOWEXTEX + case U_MM_ANISOTROPIC: + break; + } + break; + } + case U_EMR_SETBKMODE: + { + dbg_str << "\n"; + PU_EMRSETBKMODE pEmr = (PU_EMRSETBKMODE) lpEMFR; + tbkMode = pEmr->iMode; + if(tbkMode != d->dc[d->level].bkMode){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + } + memcpy(&tbkColor,&(d->dc[d->level].bkColor),sizeof(U_COLORREF)); + } + break; + } + case U_EMR_SETPOLYFILLMODE: + { + dbg_str << "\n"; + + PU_EMRSETPOLYFILLMODE pEmr = (PU_EMRSETPOLYFILLMODE) lpEMFR; + d->dc[d->level].style.fill_rule.value = + (pEmr->iMode == U_ALTERNATE + ? SP_WIND_RULE_NONZERO + : (pEmr->iMode == U_WINDING ? SP_WIND_RULE_INTERSECT : SP_WIND_RULE_NONZERO)); + break; + } + case U_EMR_SETROP2: + { + dbg_str << "\n"; + PU_EMRSETROP2 pEmr = (PU_EMRSETROP2) lpEMFR; + d->dwRop2 = pEmr->iMode; + break; + } + case U_EMR_SETSTRETCHBLTMODE: + { + PU_EMRSETSTRETCHBLTMODE pEmr = (PU_EMRSETSTRETCHBLTMODE) lpEMFR; // from wingdi.h + BLTmode = pEmr->iMode; + dbg_str << "\n"; + break; + } + case U_EMR_SETTEXTALIGN: + { + dbg_str << "\n"; + + PU_EMRSETTEXTALIGN pEmr = (PU_EMRSETTEXTALIGN) lpEMFR; + d->dc[d->level].textAlign = pEmr->iMode; + break; + } + case U_EMR_SETCOLORADJUSTMENT: + dbg_str << "\n"; + break; + case U_EMR_SETTEXTCOLOR: + { + dbg_str << "\n"; + + PU_EMRSETTEXTCOLOR pEmr = (PU_EMRSETTEXTCOLOR) lpEMFR; + d->dc[d->level].textColor = pEmr->crColor; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + } + // not text_dirty, because multicolored complex text is supported in libTERE + break; + } + case U_EMR_SETBKCOLOR: + { + dbg_str << "\n"; + + PU_EMRSETBKCOLOR pEmr = (PU_EMRSETBKCOLOR) lpEMFR; + tbkColor = pEmr->crColor; + if(memcmp(&tbkColor, &(d->dc[d->level].bkColor), sizeof(U_COLORREF))){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + tbkMode = d->dc[d->level].bkMode; + } + break; + } + case U_EMR_OFFSETCLIPRGN: + { + dbg_str << "\n"; + if (d->dc[d->level].clip_id) { // can only offsetan existing clipping path + PU_EMROFFSETCLIPRGN pEmr = (PU_EMROFFSETCLIPRGN) lpEMFR; + U_POINTL off = pEmr->ptlOffset; + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector tmp_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + double ox = pix_to_x_point(d, off.x, off.y) - pix_to_x_point(d, 0, 0); // take into account all active transforms + double oy = pix_to_y_point(d, off.x, off.y) - pix_to_y_point(d, 0, 0); + Geom::Affine tf = Geom::Translate(ox,oy); + tmp_vect *= tf; + char *tmp_path = sp_svg_write_path(tmp_vect); + add_clips(d, tmp_path, U_RGN_COPY); + free(tmp_path); + } + break; + } + case U_EMR_MOVETOEX: + { + dbg_str << "\n"; + + PU_EMRMOVETOEX pEmr = (PU_EMRMOVETOEX) lpEMFR; + + d->mask |= emr_mask; + + d->dc[d->level].cur = pEmr->ptl; + + tmp_path << + "\n\tM " << pix_to_xy( d, pEmr->ptl.x, pEmr->ptl.y ) << " "; + break; + } + case U_EMR_SETMETARGN: dbg_str << "\n"; break; + case U_EMR_EXCLUDECLIPRECT: + { + dbg_str << "\n"; + + PU_EMREXCLUDECLIPRECT pEmr = (PU_EMREXCLUDECLIPRECT) lpEMFR; + U_RECTL rc = pEmr->rclClip; + + SVGOStringStream tmp_path; + //outer rect, clockwise + tmp_path << "M " << faraway << "," << faraway << " "; + tmp_path << "L " << faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << faraway << " "; + tmp_path << "z "; + //inner rect, counterclockwise (sign of Y is reversed) + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_INTERSECTCLIPRECT: + { + dbg_str << "\n"; + + PU_EMRINTERSECTCLIPRECT pEmr = (PU_EMRINTERSECTCLIPRECT) lpEMFR; + U_RECTL rc = pEmr->rclClip; + + SVGOStringStream tmp_path; + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_SCALEVIEWPORTEXTEX: dbg_str << "\n"; break; + case U_EMR_SCALEWINDOWEXTEX: dbg_str << "\n"; break; + case U_EMR_SAVEDC: + dbg_str << "\n"; + + if (d->level < EMF_MAX_DC) { + d->dc[d->level + 1] = d->dc[d->level]; + if(d->dc[d->level].font_name){ + d->dc[d->level + 1].font_name = strdup(d->dc[d->level].font_name); // or memory access problems because font name pointer duplicated + } + d->level = d->level + 1; + } + break; + case U_EMR_RESTOREDC: + { + dbg_str << "\n"; + + PU_EMRRESTOREDC pEmr = (PU_EMRRESTOREDC) lpEMFR; + int old_level = d->level; + if (pEmr->iRelative >= 0) { + if (pEmr->iRelative < d->level) + d->level = pEmr->iRelative; + } + else { + if (d->level + pEmr->iRelative >= 0) + d->level = d->level + pEmr->iRelative; + } + while (old_level > d->level) { + if (!d->dc[old_level].style.stroke_dasharray.values.empty() && + (old_level == 0 || (old_level > 0 && d->dc[old_level].style.stroke_dasharray != + d->dc[old_level - 1].style.stroke_dasharray))) { + d->dc[old_level].style.stroke_dasharray.values.clear(); + } + if(d->dc[old_level].font_name){ + free(d->dc[old_level].font_name); // else memory leak + d->dc[old_level].font_name = nullptr; + } + old_level--; + } + break; + } + case U_EMR_SETWORLDTRANSFORM: + { + dbg_str << "\n"; + + PU_EMRSETWORLDTRANSFORM pEmr = (PU_EMRSETWORLDTRANSFORM) lpEMFR; + d->dc[d->level].worldTransform = pEmr->xform; + break; + } + case U_EMR_MODIFYWORLDTRANSFORM: + { + dbg_str << "\n"; + + PU_EMRMODIFYWORLDTRANSFORM pEmr = (PU_EMRMODIFYWORLDTRANSFORM) lpEMFR; + switch (pEmr->iMode) + { + case U_MWT_IDENTITY: + d->dc[d->level].worldTransform.eM11 = 1.0; + d->dc[d->level].worldTransform.eM12 = 0.0; + d->dc[d->level].worldTransform.eM21 = 0.0; + d->dc[d->level].worldTransform.eM22 = 1.0; + d->dc[d->level].worldTransform.eDx = 0.0; + d->dc[d->level].worldTransform.eDy = 0.0; + break; + case U_MWT_LEFTMULTIPLY: + { +// d->dc[d->level].worldTransform = pEmr->xform * worldTransform; + + float a11 = pEmr->xform.eM11; + float a12 = pEmr->xform.eM12; + float a13 = 0.0; + float a21 = pEmr->xform.eM21; + float a22 = pEmr->xform.eM22; + float a23 = 0.0; + float a31 = pEmr->xform.eDx; + float a32 = pEmr->xform.eDy; + float a33 = 1.0; + + float b11 = d->dc[d->level].worldTransform.eM11; + float b12 = d->dc[d->level].worldTransform.eM12; + //float b13 = 0.0; + float b21 = d->dc[d->level].worldTransform.eM21; + float b22 = d->dc[d->level].worldTransform.eM22; + //float b23 = 0.0; + float b31 = d->dc[d->level].worldTransform.eDx; + float b32 = d->dc[d->level].worldTransform.eDy; + //float b33 = 1.0; + + float c11 = a11*b11 + a12*b21 + a13*b31;; + float c12 = a11*b12 + a12*b22 + a13*b32;; + //float c13 = a11*b13 + a12*b23 + a13*b33;; + float c21 = a21*b11 + a22*b21 + a23*b31;; + float c22 = a21*b12 + a22*b22 + a23*b32;; + //float c23 = a21*b13 + a22*b23 + a23*b33;; + float c31 = a31*b11 + a32*b21 + a33*b31;; + float c32 = a31*b12 + a32*b22 + a33*b32;; + //float c33 = a31*b13 + a32*b23 + a33*b33;; + + d->dc[d->level].worldTransform.eM11 = c11;; + d->dc[d->level].worldTransform.eM12 = c12;; + d->dc[d->level].worldTransform.eM21 = c21;; + d->dc[d->level].worldTransform.eM22 = c22;; + d->dc[d->level].worldTransform.eDx = c31; + d->dc[d->level].worldTransform.eDy = c32; + + break; + } + case U_MWT_RIGHTMULTIPLY: + { +// d->dc[d->level].worldTransform = worldTransform * pEmr->xform; + + float a11 = d->dc[d->level].worldTransform.eM11; + float a12 = d->dc[d->level].worldTransform.eM12; + float a13 = 0.0; + float a21 = d->dc[d->level].worldTransform.eM21; + float a22 = d->dc[d->level].worldTransform.eM22; + float a23 = 0.0; + float a31 = d->dc[d->level].worldTransform.eDx; + float a32 = d->dc[d->level].worldTransform.eDy; + float a33 = 1.0; + + float b11 = pEmr->xform.eM11; + float b12 = pEmr->xform.eM12; + //float b13 = 0.0; + float b21 = pEmr->xform.eM21; + float b22 = pEmr->xform.eM22; + //float b23 = 0.0; + float b31 = pEmr->xform.eDx; + float b32 = pEmr->xform.eDy; + //float b33 = 1.0; + + float c11 = a11*b11 + a12*b21 + a13*b31;; + float c12 = a11*b12 + a12*b22 + a13*b32;; + //float c13 = a11*b13 + a12*b23 + a13*b33;; + float c21 = a21*b11 + a22*b21 + a23*b31;; + float c22 = a21*b12 + a22*b22 + a23*b32;; + //float c23 = a21*b13 + a22*b23 + a23*b33;; + float c31 = a31*b11 + a32*b21 + a33*b31;; + float c32 = a31*b12 + a32*b22 + a33*b32;; + //float c33 = a31*b13 + a32*b23 + a33*b33;; + + d->dc[d->level].worldTransform.eM11 = c11;; + d->dc[d->level].worldTransform.eM12 = c12;; + d->dc[d->level].worldTransform.eM21 = c21;; + d->dc[d->level].worldTransform.eM22 = c22;; + d->dc[d->level].worldTransform.eDx = c31; + d->dc[d->level].worldTransform.eDy = c32; + + break; + } +// case MWT_SET: + default: + d->dc[d->level].worldTransform = pEmr->xform; + break; + } + break; + } + case U_EMR_SELECTOBJECT: + { + dbg_str << "\n"; + + PU_EMRSELECTOBJECT pEmr = (PU_EMRSELECTOBJECT) lpEMFR; + unsigned int index = pEmr->ihObject; + + if (index & U_STOCK_OBJECT) { + switch (index) { + case U_NULL_BRUSH: + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = false; + break; + case U_BLACK_BRUSH: + case U_DKGRAY_BRUSH: + case U_GRAY_BRUSH: + case U_LTGRAY_BRUSH: + case U_WHITE_BRUSH: + { + float val = 0; + switch (index) { + case U_BLACK_BRUSH: + val = 0.0 / 255.0; + break; + case U_DKGRAY_BRUSH: + val = 64.0 / 255.0; + break; + case U_GRAY_BRUSH: + val = 128.0 / 255.0; + break; + case U_LTGRAY_BRUSH: + val = 192.0 / 255.0; + break; + case U_WHITE_BRUSH: + val = 255.0 / 255.0; + break; + } + d->dc[d->level].style.fill.value.color.set( val, val, val ); + + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + break; + } + case U_NULL_PEN: + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = false; + break; + case U_BLACK_PEN: + case U_WHITE_PEN: + { + float val = index == U_BLACK_PEN ? 0 : 1; + d->dc[d->level].style.stroke_dasharray.set = false; + d->dc[d->level].style.stroke_width.value = 1.0; + d->dc[d->level].style.stroke.value.color.set( val, val, val ); + + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + + break; + } + } + } else { + if ( /*index >= 0 &&*/ index < (unsigned int) d->n_obj) { + switch (d->emf_obj[index].type) + { + case U_EMR_CREATEPEN: + select_pen(d, index); + break; + case U_EMR_CREATEBRUSHINDIRECT: + case U_EMR_CREATEDIBPATTERNBRUSHPT: + case U_EMR_CREATEMONOBRUSH: + select_brush(d, index); + break; + case U_EMR_EXTCREATEPEN: + select_extpen(d, index); + break; + case U_EMR_EXTCREATEFONTINDIRECTW: + select_font(d, index); + break; + } + } + } + break; + } + case U_EMR_CREATEPEN: + { + dbg_str << "\n"; + + PU_EMRCREATEPEN pEmr = (PU_EMRCREATEPEN) lpEMFR; + insert_object(d, pEmr->ihPen, U_EMR_CREATEPEN, lpEMFR); + break; + } + case U_EMR_CREATEBRUSHINDIRECT: + { + dbg_str << "\n"; + + PU_EMRCREATEBRUSHINDIRECT pEmr = (PU_EMRCREATEBRUSHINDIRECT) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEBRUSHINDIRECT, lpEMFR); + break; + } + case U_EMR_DELETEOBJECT: + dbg_str << "\n"; + // Objects here are not deleted until the draw completes, new ones may write over an existing one. + break; + case U_EMR_ANGLEARC: + dbg_str << "\n"; + break; + case U_EMR_ELLIPSE: + { + dbg_str << "\n"; + + PU_EMRELLIPSE pEmr = (PU_EMRELLIPSE) lpEMFR; + U_RECTL rclBox = pEmr->rclBox; + + double cx = pix_to_x_point( d, (rclBox.left + rclBox.right)/2.0, (rclBox.bottom + rclBox.top)/2.0 ); + double cy = pix_to_y_point( d, (rclBox.left + rclBox.right)/2.0, (rclBox.bottom + rclBox.top)/2.0 ); + double rx = pix_to_abs_size( d, std::abs(rclBox.right - rclBox.left )/2.0 ); + double ry = pix_to_abs_size( d, std::abs(rclBox.top - rclBox.bottom)/2.0 ); + + SVGOStringStream tmp_ellipse; + tmp_ellipse << "cx=\"" << cx << "\" "; + tmp_ellipse << "cy=\"" << cy << "\" "; + tmp_ellipse << "rx=\"" << rx << "\" "; + tmp_ellipse << "ry=\"" << ry << "\" "; + + d->mask |= emr_mask; + + d->outsvg += " iType); // + d->outsvg += "\n\t"; + d->outsvg += tmp_ellipse.str().c_str(); + d->outsvg += "/> \n"; + d->path = ""; + break; + } + case U_EMR_RECTANGLE: + { + dbg_str << "\n"; + + PU_EMRRECTANGLE pEmr = (PU_EMRRECTANGLE) lpEMFR; + U_RECTL rc = pEmr->rclBox; + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= emr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_EMR_ROUNDRECT: + { + dbg_str << "\n"; + + PU_EMRROUNDRECT pEmr = (PU_EMRROUNDRECT) lpEMFR; + U_RECTL rc = pEmr->rclBox; + U_SIZEL corner = pEmr->szlCorner; + double f = 4.*(sqrt(2) - 1)/3; + double f1 = 1.0 - f; + double cnx = corner.cx/2; + double cny = corner.cy/2; + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n" + << " M " + << pix_to_xy(d, rc.left , rc.top + cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.left , rc.top + cny*f1 ) + << " " + << pix_to_xy(d, rc.left + cnx*f1 , rc.top ) + << " " + << pix_to_xy(d, rc.left + cnx , rc.top ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.right - cnx , rc.top ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.right - cnx*f1 , rc.top ) + << " " + << pix_to_xy(d, rc.right , rc.top + cny*f1 ) + << " " + << pix_to_xy(d, rc.right , rc.top + cny ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.right , rc.bottom - cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.right , rc.bottom - cny*f1 ) + << " " + << pix_to_xy(d, rc.right - cnx*f1 , rc.bottom ) + << " " + << pix_to_xy(d, rc.right - cnx , rc.bottom ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.left + cnx , rc.bottom ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.left + cnx*f1 , rc.bottom ) + << " " + << pix_to_xy(d, rc.left , rc.bottom - cny*f1 ) + << " " + << pix_to_xy(d, rc.left , rc.bottom - cny ) + << "\n"; + tmp_rectangle << " z\n"; + + + d->mask |= emr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_EMR_ARC: + { + dbg_str << "\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + int stat = emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size); + if(!stat){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + d->mask |= emr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_EMR_CHORD: + { + dbg_str << "\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= emr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_EMR_PIE: + { + dbg_str << "\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, center.x, center.y); + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= emr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_EMR_SELECTPALETTE: dbg_str << "\n"; break; + case U_EMR_CREATEPALETTE: dbg_str << "\n"; break; + case U_EMR_SETPALETTEENTRIES: dbg_str << "\n"; break; + case U_EMR_RESIZEPALETTE: dbg_str << "\n"; break; + case U_EMR_REALIZEPALETTE: dbg_str << "\n"; break; + case U_EMR_EXTFLOODFILL: dbg_str << "\n"; break; + case U_EMR_LINETO: + { + dbg_str << "\n"; + + PU_EMRLINETO pEmr = (PU_EMRLINETO) lpEMFR; + + d->mask |= emr_mask; + + tmp_path << + "\n\tL " << pix_to_xy( d, pEmr->ptl.x, pEmr->ptl.y) << " "; + break; + } + case U_EMR_ARCTO: + { + dbg_str << "\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + // draw a line from current position to start, arc from there + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y)<< " "; + + d->mask |= emr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_EMR_POLYDRAW: dbg_str << "\n"; break; + case U_EMR_SETARCDIRECTION: + { + dbg_str << "\n"; + PU_EMRSETARCDIRECTION pEmr = (PU_EMRSETARCDIRECTION) lpEMFR; + if(d->arcdir == U_AD_CLOCKWISE || d->arcdir == U_AD_COUNTERCLOCKWISE){ // EMF file could be corrupt + d->arcdir = pEmr->iArcDirection; + } + break; + } + case U_EMR_SETMITERLIMIT: + { + dbg_str << "\n"; + + PU_EMRSETMITERLIMIT pEmr = (PU_EMRSETMITERLIMIT) lpEMFR; + + //The function takes a float but saves a 32 bit int in the U_EMR_SETMITERLIMIT record. + float miterlimit = *((int32_t *) &(pEmr->eMiterLimit)); + d->dc[d->level].style.stroke_miterlimit.value = miterlimit; //ratio, not a pt size + if (d->dc[d->level].style.stroke_miterlimit.value < 2) + d->dc[d->level].style.stroke_miterlimit.value = 2.0; + break; + } + case U_EMR_BEGINPATH: + { + dbg_str << "\n"; + // The next line should never be needed, should have been handled before main switch + // qualifier added because EMF's encountered where moveto preceded beginpath followed by lineto + if(d->mask & U_DRAW_VISIBLE){ + d->path = ""; + } + d->mask |= emr_mask; + break; + } + case U_EMR_ENDPATH: + { + dbg_str << "\n"; + d->mask &= (0xFFFFFFFF - U_DRAW_ONLYTO); // clear the OnlyTo bit (it might not have been set), prevents any further path extension + break; + } + case U_EMR_CLOSEFIGURE: + { + dbg_str << "\n"; + // EMF may contain multiple closefigures on one path + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + break; + } + case U_EMR_FILLPATH: + { + dbg_str << "\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + if(!(d->mask & U_DRAW_CLOSED)){ // Close a path not explicitly closed by an EMRCLOSEFIGURE, otherwise fill makes no sense + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + } + d->mask |= emr_mask; + d->drawtype = U_EMR_FILLPATH; + } + break; + } + case U_EMR_STROKEANDFILLPATH: + { + dbg_str << "\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + if(!(d->mask & U_DRAW_CLOSED)){ // Close a path not explicitly closed by an EMRCLOSEFIGURE, otherwise fill makes no sense + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + } + d->mask |= emr_mask; + d->drawtype = U_EMR_STROKEANDFILLPATH; + } + break; + } + case U_EMR_STROKEPATH: + { + dbg_str << "\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + d->mask |= emr_mask; + d->drawtype = U_EMR_STROKEPATH; + } + break; + } + case U_EMR_FLATTENPATH: dbg_str << "\n"; break; + case U_EMR_WIDENPATH: dbg_str << "\n"; break; + case U_EMR_SELECTCLIPPATH: + { + dbg_str << "\n"; + PU_EMRSELECTCLIPPATH pEmr = (PU_EMRSELECTCLIPPATH) lpEMFR; + int logic = pEmr->iMode; + + if ((logic < U_RGN_MIN) || (logic > U_RGN_MAX)){ break; } + add_clips(d, d->path.c_str(), logic); // finds an existing one or stores this, sets clip_id + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_ABORTPATH: + { + dbg_str << "\n"; + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_UNDEF69: dbg_str << "\n"; break; + case U_EMR_COMMENT: + { + dbg_str << "\n"; + + PU_EMRCOMMENT pEmr = (PU_EMRCOMMENT) lpEMFR; + + char *szTxt = (char *) pEmr->Data; + + for (uint32_t i = 0; i < pEmr->cbData; i++) { + if ( *szTxt) { + if ( *szTxt >= ' ' && *szTxt < 'z' && *szTxt != '<' && *szTxt != '>' ) { + tmp_str << *szTxt; + } + szTxt++; + } + } + + if (false && strlen(tmp_str.str().c_str())) { + tmp_outsvg << " \n"; + } + + break; + } + case U_EMR_FILLRGN: dbg_str << "\n"; break; + case U_EMR_FRAMERGN: dbg_str << "\n"; break; + case U_EMR_INVERTRGN: dbg_str << "\n"; break; + case U_EMR_PAINTRGN: dbg_str << "\n"; break; + case U_EMR_EXTSELECTCLIPRGN: + { + dbg_str << "\n"; + + PU_EMREXTSELECTCLIPRGN pEmr = (PU_EMREXTSELECTCLIPRGN) lpEMFR; + // the only mode we implement - this clears the clipping region + if (pEmr->iMode == U_RGN_COPY) { + d->dc[d->level].clip_id = 0; + } + break; + } + case U_EMR_BITBLT: + { + dbg_str << "\n"; + + PU_EMRBITBLT pEmr = (PU_EMRBITBLT) lpEMFR; + // Treat all nonImage bitblts as a rectangular write. Definitely not correct, but at + // least it leaves objects where the operations should have been. + if (!pEmr->cbBmiSrc) { + // should be an application of a DIBPATTERNBRUSHPT, use a solid color instead + + if(pEmr->dwRop == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = pEmr->Dest.x; + int32_t dy = pEmr->Dest.y; + int32_t dw = pEmr->cDest.x; + int32_t dh = pEmr->cDest.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= emr_mask; + d->dwRop3 = pEmr->dwRop; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + //source position within the bitmap, in pixels + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_STRETCHBLT: + { + dbg_str << "\n"; + PU_EMRSTRETCHBLT pEmr = (PU_EMRSTRETCHBLT) lpEMFR; + // Always grab image, ignore modes. + if (pEmr->cbBmiSrc) { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + //source position within the bitmap, in pixels + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = pEmr->cSrc.x; // extract the specified amount of the image + int sh = pEmr->cSrc.y; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_MASKBLT: + { + dbg_str << "\n"; + PU_EMRMASKBLT pEmr = (PU_EMRMASKBLT) lpEMFR; + // Always grab image, ignore masks and modes. + if (pEmr->cbBmiSrc) { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; //source position within the bitmap, in pixels + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = 0; // extract all of the image + int sh = 0; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_PLGBLT: dbg_str << "\n"; break; + case U_EMR_SETDIBITSTODEVICE: dbg_str << "\n"; break; + case U_EMR_STRETCHDIBITS: + { + // Some applications use multiple EMF operations, including multiple STRETCHDIBITS to create + // images with transparent regions. PowerPoint does this with rotated images, for instance. + // Parsing all of that to derive a single resultant image object is left for a later version + // of this code. In the meantime, every STRETCHDIBITS goes directly to an image. The Inkscape + // user can sort out transparency later using Gimp, if need be. + + PU_EMRSTRETCHDIBITS pEmr = (PU_EMRSTRETCHDIBITS) lpEMFR; + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y ); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y ); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + int sx = pEmr->Src.x; //source position within the bitmap, in pixels + int sy = pEmr->Src.y; + int sw = pEmr->cSrc.x; // extract the specified amount of the image + int sh = pEmr->cSrc.y; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + + dbg_str << "\n"; + break; + } + case U_EMR_EXTCREATEFONTINDIRECTW: + { + dbg_str << "\n"; + + PU_EMREXTCREATEFONTINDIRECTW pEmr = (PU_EMREXTCREATEFONTINDIRECTW) lpEMFR; + insert_object(d, pEmr->ihFont, U_EMR_EXTCREATEFONTINDIRECTW, lpEMFR); + break; + } + case U_EMR_EXTTEXTOUTA: + case U_EMR_EXTTEXTOUTW: + case U_EMR_SMALLTEXTOUT: + { + dbg_str << "\n"; + + PU_EMREXTTEXTOUTW pEmr = (PU_EMREXTTEXTOUTW) lpEMFR; + PU_EMRSMALLTEXTOUT pEmrS = (PU_EMRSMALLTEXTOUT) lpEMFR; + + double x1,y1; + int roff = sizeof(U_EMRSMALLTEXTOUT); //offset to the start of the variable fields, only used with U_EMR_SMALLTEXTOUT + int cChars; + if(lpEMFR->iType==U_EMR_SMALLTEXTOUT){ + x1 = pEmrS->Dest.x; + y1 = pEmrS->Dest.y; + cChars = pEmrS->cChars; + if(!(pEmrS->fuOptions & U_ETO_NO_RECT)){ roff += sizeof(U_RECTL); } + } + else { + x1 = pEmr->emrtext.ptlReference.x; + y1 = pEmr->emrtext.ptlReference.y; + cChars = 0; + } + uint32_t fOptions = pEmr->emrtext.fOptions; + + if (d->dc[d->level].textAlign & U_TA_UPDATECP) { + x1 = d->dc[d->level].cur.x; + y1 = d->dc[d->level].cur.y; + } + + double x = pix_to_x_point(d, x1, y1); + double y = pix_to_y_point(d, x1, y1); + + /* Rotation issues are handled entirely in libTERE now */ + + uint32_t *dup_wt = nullptr; + + if( lpEMFR->iType==U_EMR_EXTTEXTOUTA){ + /* These should be JUST ASCII, but they might not be... + If it holds Utf-8 or plain ASCII the first call will succeed. + If not, assume that it holds Latin1. + If that fails then something is really screwed up! + */ + dup_wt = U_Utf8ToUtf32le((char *) pEmr + pEmr->emrtext.offString, pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = U_Latin1ToUtf32le((char *) pEmr + pEmr->emrtext.offString, pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(pEmr->emrtext.nChars); + } + else if( lpEMFR->iType==U_EMR_EXTTEXTOUTW){ + dup_wt = U_Utf16leToUtf32le((uint16_t *)((char *) pEmr + pEmr->emrtext.offString), pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(pEmr->emrtext.nChars); + } + else { // U_EMR_SMALLTEXTOUT + if(pEmrS->fuOptions & U_ETO_SMALL_CHARS){ + dup_wt = U_Utf8ToUtf32le((char *) pEmrS + roff, cChars, nullptr); + } + else { + dup_wt = U_Utf16leToUtf32le((uint16_t *)((char *) pEmrS + roff), cChars, nullptr); + } + if(!dup_wt)dup_wt = unknown_chars(cChars); + } + + msdepua(dup_wt); //convert everything in Microsoft's private use area. For Symbol, Wingdings, Dingbats + + if(NonToUnicode(dup_wt, d->dc[d->level].font_name)){ + free(d->dc[d->level].font_name); + d->dc[d->level].font_name = strdup("Times New Roman"); + } + + char *ansi_text; + ansi_text = (char *) U_Utf32leToUtf8((uint32_t *)dup_wt, 0, nullptr); + free(dup_wt); + // Empty string or starts with an invalid escape/control sequence, which is bogus text. Throw it out before g_markup_escape_text can make things worse + if(*((uint8_t *)ansi_text) <= 0x1F){ + free(ansi_text); + ansi_text=nullptr; + } + + if (ansi_text) { + + SVGOStringStream ts; + + gchar *escaped_text = g_markup_escape_text(ansi_text, -1); + + tsp.x = x*0.8; // TERE expects sizes in points. + tsp.y = y*0.8; + tsp.color.Red = d->dc[d->level].textColor.Red; + tsp.color.Green = d->dc[d->level].textColor.Green; + tsp.color.Blue = d->dc[d->level].textColor.Blue; + tsp.color.Reserved = 0; + switch(d->dc[d->level].style.font_style.value){ + case SP_CSS_FONT_STYLE_OBLIQUE: + tsp.italics = FC_SLANT_OBLIQUE; break; + case SP_CSS_FONT_STYLE_ITALIC: + tsp.italics = FC_SLANT_ITALIC; break; + default: + case SP_CSS_FONT_STYLE_NORMAL: + tsp.italics = FC_SLANT_ROMAN; break; + } + switch(d->dc[d->level].style.font_weight.value){ + case SP_CSS_FONT_WEIGHT_100: tsp.weight = FC_WEIGHT_THIN ; break; + case SP_CSS_FONT_WEIGHT_200: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_300: tsp.weight = FC_WEIGHT_LIGHT ; break; + case SP_CSS_FONT_WEIGHT_400: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_500: tsp.weight = FC_WEIGHT_MEDIUM ; break; + case SP_CSS_FONT_WEIGHT_600: tsp.weight = FC_WEIGHT_SEMIBOLD ; break; + case SP_CSS_FONT_WEIGHT_700: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_800: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + case SP_CSS_FONT_WEIGHT_900: tsp.weight = FC_WEIGHT_HEAVY ; break; + case SP_CSS_FONT_WEIGHT_NORMAL: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_BOLD: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_LIGHTER: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_BOLDER: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + default: tsp.weight = FC_WEIGHT_NORMAL ; break; + } + // EMF only supports two types of text decoration + tsp.decoration = TXTDECOR_NONE; + if(d->dc[d->level].style.text_decoration_line.underline){ tsp.decoration |= TXTDECOR_UNDER; } + if(d->dc[d->level].style.text_decoration_line.line_through){ tsp.decoration |= TXTDECOR_STRIKE;} + + // EMF textalignment is a bit strange: 0x6 is center, 0x2 is right, 0x0 is left, the value 0x4 is also drawn left + tsp.taln = ((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_CENTER) ? ALICENTER : + (((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_LEFT) ? ALILEFT : + ALIRIGHT); + tsp.taln |= ((d->dc[d->level].textAlign & U_TA_BASEBIT) ? ALIBASE : + ((d->dc[d->level].textAlign & U_TA_BOTTOM) ? ALIBOT : + ALITOP)); + + // language direction can be encoded two ways, U_TA_RTLREADING is preferred + if( (fOptions & U_ETO_RTLREADING) || (d->dc[d->level].textAlign & U_TA_RTLREADING) ){ tsp.ldir = LDIR_RL; } + else{ tsp.ldir = LDIR_LR; } + + tsp.condensed = FC_WIDTH_NORMAL; // Not implemented well in libTERE (yet) + tsp.ori = d->dc[d->level].style.baseline_shift.value; // For now orientation is always the same as escapement + tsp.ori += 180.0 * current_rotation(d)/ M_PI; // radians to degrees + tsp.string = (uint8_t *) U_strdup(escaped_text); // this will be free'd much later at a trinfo_clear(). + tsp.fs = d->dc[d->level].style.font_size.computed * 0.8; // Font size in points + char *fontspec = TR_construct_fontspec(&tsp, d->dc[d->level].font_name); + tsp.fi_idx = ftinfo_load_fontname(d->tri->fti,fontspec); + free(fontspec); + // when font name includes narrow it may not be set to "condensed". Narrow fonts do not work well anyway though + // as the metrics from fontconfig may not match, or the font may not be present. + if(0<= TR_findcasesub(d->dc[d->level].font_name, (char *) "Narrow")){ tsp.co=1; } + else { tsp.co=0; } + + int status = trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ori is actually escapement + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\ndc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + (void) trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ignore return status, it must work + if (d->dc[d->level].clip_id){ + d->outsvg += "\n\n"; + } + } + + g_free(escaped_text); + free(ansi_text); + } + + break; + } + case U_EMR_POLYBEZIER16: + { + dbg_str << "\n"; + + PU_EMRPOLYBEZIER16 pEmr = (PU_EMRPOLYBEZIER16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i,j; + + if (pEmr->cpts<4) + break; + + d->mask |= emr_mask; + + tmp_str << "\n\tM " << pix_to_xy( d, apts[0].x, apts[0].y ) << " "; + + for (i=1; icpts; ) { + tmp_str << "\n\tC "; + for (j=0; j<3 && icpts; j++,i++) { + tmp_str << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYGON16: + { + dbg_str << "\n"; + + PU_EMRPOLYGON16 pEmr = (PU_EMRPOLYGON16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + SVGOStringStream tmp_poly; + unsigned int i; + unsigned int first = 0; + + d->mask |= emr_mask; + + // skip the first point? + tmp_poly << "\n\tM " << pix_to_xy( d, apts[first].x, apts[first].y ) << " "; + + for (i=first+1; icpts; i++) { + tmp_poly << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + + tmp_path << tmp_poly.str().c_str(); + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + + break; + } + case U_EMR_POLYLINE16: + { + dbg_str << "\n"; + + PU_EMRPOLYLINE16 pEmr = (PU_EMRPOLYLINE16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i; + + if (pEmr->cpts<2) + break; + + d->mask |= emr_mask; + + tmp_str << "\n\tM " << pix_to_xy( d, apts[0].x, apts[0].y ) << " "; + + for (i=1; icpts; i++) { + tmp_str << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYBEZIERTO16: + { + dbg_str << "\n"; + + PU_EMRPOLYBEZIERTO16 pEmr = (PU_EMRPOLYBEZIERTO16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i,j; + + d->mask |= emr_mask; + + for (i=0; icpts;) { + tmp_path << "\n\tC "; + for (j=0; j<3 && icpts; j++,i++) { + tmp_path << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + } + } + + break; + } + case U_EMR_POLYLINETO16: + { + dbg_str << "\n"; + + PU_EMRPOLYLINETO16 pEmr = (PU_EMRPOLYLINETO16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i; + + d->mask |= emr_mask; + + for (i=0; icpts;i++) { + tmp_path << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + } + + break; + } + case U_EMR_POLYPOLYLINE16: + case U_EMR_POLYPOLYGON16: + { + if (lpEMFR->iType == U_EMR_POLYPOLYLINE16) + dbg_str << "\n"; + if (lpEMFR->iType == U_EMR_POLYPOLYGON16) + dbg_str << "\n"; + + PU_EMRPOLYPOLYGON16 pEmr = (PU_EMRPOLYPOLYGON16) lpEMFR; + unsigned int n, i, j; + + d->mask |= emr_mask; + + PU_POINT16 apts = (PU_POINT16) &pEmr->aPolyCounts[pEmr->nPolys]; + + i = 0; + for (n=0; nnPolys && icpts; n++) { + SVGOStringStream poly_path; + + poly_path << "\n\tM " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + i++; + + for (j=1; jaPolyCounts[n] && icpts; j++) { + poly_path << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + i++; + } + + tmp_str << poly_path.str().c_str(); + if (lpEMFR->iType == U_EMR_POLYPOLYGON16) + tmp_str << " z"; + tmp_str << " \n"; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYDRAW16: dbg_str << "\n"; break; + case U_EMR_CREATEMONOBRUSH: + { + dbg_str << "\n"; + + PU_EMRCREATEMONOBRUSH pEmr = (PU_EMRCREATEMONOBRUSH) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEMONOBRUSH, lpEMFR); + break; + } + case U_EMR_CREATEDIBPATTERNBRUSHPT: + { + dbg_str << "\n"; + + PU_EMRCREATEDIBPATTERNBRUSHPT pEmr = (PU_EMRCREATEDIBPATTERNBRUSHPT) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEDIBPATTERNBRUSHPT, lpEMFR); + break; + } + case U_EMR_EXTCREATEPEN: + { + dbg_str << "\n"; + + PU_EMREXTCREATEPEN pEmr = (PU_EMREXTCREATEPEN) lpEMFR; + insert_object(d, pEmr->ihPen, U_EMR_EXTCREATEPEN, lpEMFR); + break; + } + case U_EMR_POLYTEXTOUTA: dbg_str << "\n"; break; + case U_EMR_POLYTEXTOUTW: dbg_str << "\n"; break; + case U_EMR_SETICMMODE: + { + dbg_str << "\n"; + PU_EMRSETICMMODE pEmr = (PU_EMRSETICMMODE) lpEMFR; + ICMmode= pEmr->iMode; + break; + } + case U_EMR_CREATECOLORSPACE: dbg_str << "\n"; break; + case U_EMR_SETCOLORSPACE: dbg_str << "\n"; break; + case U_EMR_DELETECOLORSPACE: dbg_str << "\n"; break; + case U_EMR_GLSRECORD: dbg_str << "\n"; break; + case U_EMR_GLSBOUNDEDRECORD: dbg_str << "\n"; break; + case U_EMR_PIXELFORMAT: dbg_str << "\n"; break; + case U_EMR_DRAWESCAPE: dbg_str << "\n"; break; + case U_EMR_EXTESCAPE: dbg_str << "\n"; break; + case U_EMR_UNDEF107: dbg_str << "\n"; break; + // U_EMR_SMALLTEXTOUT is handled with U_EMR_EXTTEXTOUTA/W above + case U_EMR_FORCEUFIMAPPING: dbg_str << "\n"; break; + case U_EMR_NAMEDESCAPE: dbg_str << "\n"; break; + case U_EMR_COLORCORRECTPALETTE: dbg_str << "\n"; break; + case U_EMR_SETICMPROFILEA: dbg_str << "\n"; break; + case U_EMR_SETICMPROFILEW: dbg_str << "\n"; break; + case U_EMR_ALPHABLEND: dbg_str << "\n"; break; + case U_EMR_SETLAYOUT: dbg_str << "\n"; break; + case U_EMR_TRANSPARENTBLT: dbg_str << "\n"; break; + case U_EMR_UNDEF117: dbg_str << "\n"; break; + case U_EMR_GRADIENTFILL: + { + /* Gradient fill is doable for rectangles because those correspond to linear gradients. However, + the general case for the triangle fill, with a different color in each corner of the triangle, + has no SVG equivalent and cannot be easily emulated with SVG gradients. So the linear gradient + is implemented, and the triangle fill just paints with the color of the first corner. + + This record can hold a series of gradients so we are forced to add path elements directly here, + it cannot wait for the top of the main loop. Any existing path is erased. + + */ + dbg_str << "\n"; + PU_EMRGRADIENTFILL pEmr = (PU_EMRGRADIENTFILL) lpEMFR; + int nV = pEmr->nTriVert; // Number of TriVertex objects + int nG = pEmr->nGradObj; // Number of gradient triangle/rectangle objects + U_TRIVERTEX *tv = (U_TRIVERTEX *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL)); + if( pEmr->ulMode == U_GRADIENT_FILL_RECT_H || + pEmr->ulMode == U_GRADIENT_FILL_RECT_V + ){ + SVGOStringStream tmp_rectangle; + int i,fill_idx; + U_GRADIENT4 *rcs = (U_GRADIENT4 *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL) + sizeof(U_TRIVERTEX)*nV); + for(i=0;iulMode, tv[rcs[i].UpperLeft], tv[rcs[i].LowerRight]); + tmp_rectangle << "\n\tM " << pix_to_xy( d, tv[rcs[i].UpperLeft ].x , tv[rcs[i].UpperLeft ].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].LowerRight].x , tv[rcs[i].UpperLeft ].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].LowerRight].x , tv[rcs[i].LowerRight].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].UpperLeft ].x , tv[rcs[i].LowerRight].y ) << " "; + tmp_rectangle << "\n\tz\""; + tmp_rectangle << "\n\tstyle=\"stroke:none;fill:url(#"; + tmp_rectangle << d->gradients.strings[fill_idx]; + tmp_rectangle << ");\"\n"; + if (d->dc[d->level].clip_id){ + tmp_rectangle << "\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_rectangle << "/>\n"; + } + d->outsvg += tmp_rectangle.str().c_str(); + } + else if(pEmr->ulMode == U_GRADIENT_FILL_TRIANGLE){ + SVGOStringStream tmp_triangle; + char tmpcolor[8]; + int i; + U_GRADIENT3 *tris = (U_GRADIENT3 *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL) + sizeof(U_TRIVERTEX)*nV); + for(i=0;i\n"; + } + d->outsvg += tmp_triangle.str().c_str(); + } + d->path = ""; + // if it is anything else the record is bogus, so ignore it + break; + } + case U_EMR_SETLINKEDUFIS: dbg_str << "\n"; break; + case U_EMR_SETTEXTJUSTIFICATION: dbg_str << "\n"; break; + case U_EMR_COLORMATCHTOTARGETW: dbg_str << "\n"; break; + case U_EMR_CREATECOLORSPACEW: dbg_str << "\n"; break; + default: + dbg_str << "\n"; + break; + } //end of switch +// At run time define environment variable INKSCAPE_DBG_EMF to include string COMMENT. +// Users may employ this to to place a comment for each processed EMR record in the SVG + if(eDbgComment){ + d->outsvg += dbg_str.str().c_str(); + } + d->outsvg += tmp_outsvg.str().c_str(); + d->path += tmp_path.str().c_str(); + + } //end of while +// At run time define environment variable INKSCAPE_DBG_EMF to include string FINAL +// Users may employ this to to show the final SVG derived from the EMF + if(eDbgFinal){ + std::cout << d->outsvg << std::endl; + } + (void) emr_properties(U_EMR_INVALID); // force the release of the lookup table memory, returned value is irrelevant + + return(file_status); +} + +void Emf::free_emf_strings(EMF_STRINGS name){ + if(name.count){ + for(int i=0; i< name.count; i++){ free(name.strings[i]); } + free(name.strings); + } + name.count = 0; + name.size = 0; +} + +SPDocument * +Emf::open( Inkscape::Extension::Input * /*mod*/, const gchar *uri ) +{ + if (uri == nullptr) { + return nullptr; + } + + // ensure usage of dot as decimal separator in scanf/printf functions (indepentendly of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + EMF_CALLBACK_DATA d; + + d.n_obj = 0; //these might not be set otherwise if the input file is corrupt + d.emf_obj = nullptr; + d.dc[0].font_name = strdup("Arial"); // Default font, set only on lowest level, it copies up from there EMF spec says device can pick whatever it wants + + // set up the size default for patterns in defs. This might not be referenced if there are no patterns defined in the drawing. + + d.defs += "\n"; + d.defs += " \n"; + d.defs += " \n"; + + + size_t length; + char *contents; + if(emf_readdata(uri, &contents, &length))return(nullptr); + + d.pDesc = nullptr; + + // set up the text reassembly system + if(!(d.tri = trinfo_init(nullptr)))return(nullptr); + (void) trinfo_load_ft_opts(d.tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + int good = myEnhMetaFileProc(contents,length, &d); + free(contents); + + if (d.pDesc){ free( d.pDesc ); } + +// std::cout << "SVG Output: " << std::endl << d.outsvg << std::endl; + + SPDocument *doc = nullptr; + if (good) { + doc = SPDocument::createNewDocFromMem(d.outsvg.c_str(), strlen(d.outsvg.c_str()), TRUE); + } + + free_emf_strings(d.hatches); + free_emf_strings(d.images); + free_emf_strings(d.gradients); + free_emf_strings(d.clips); + + if (d.emf_obj) { + int i; + for (i=0; i\n" + "" N_("EMF Input") "\n" + "org.inkscape.input.emf\n" + "\n" + ".emf\n" + "image/x-emf\n" + "" N_("Enhanced Metafiles (*.emf)") "\n" + "" N_("Enhanced Metafiles") "\n" + "org.inkscape.output.emf\n" + "\n" + "", new Emf()); + + /* EMF out */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("EMF Output") "\n" + "org.inkscape.output.emf\n" + "true\n" + "true\n" + "true\n" + "true\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "\n" + ".emf\n" + "image/x-emf\n" + "" N_("Enhanced Metafile (*.emf)") "\n" + "" N_("Enhanced Metafile") "\n" + "\n" + "", new Emf()); + + return; +} + + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-inout.h b/src/extension/internal/emf-inout.h new file mode 100644 index 0000000..1891e8b --- /dev/null +++ b/src/extension/internal/emf-inout.h @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile Input/Output + */ +/* Authors: + * Ulf Erikson + * David Mathog + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_EXTENSION_INTERNAL_EMF_H +#define SEEN_EXTENSION_INTERNAL_EMF_H + +#include <3rdparty/libuemf/uemf.h> +#include <3rdparty/libuemf/uemf_safe.h> +#include <3rdparty/libuemf/uemf_endian.h> // for U_emf_record_sizeok() +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/implementation/implementation.h" +#include "style.h" +#include "text_reassemble.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define DIRTY_NONE 0x00 +#define DIRTY_TEXT 0x01 +#define DIRTY_FILL 0x02 +#define DIRTY_STROKE 0x04 + +struct EMF_OBJECT { + int type = 0; + int level = 0; + char *lpEMFR = nullptr; +}; +using PEMF_OBJECT = EMF_OBJECT *; + +struct EMF_STRINGS { + int size = 0; // number of slots allocated in strings + int count = 0; // number of slots used in strings + char **strings = nullptr; // place to store strings +}; +using PEMF_STRINGS = EMF_STRINGS *; + +struct EMF_DEVICE_CONTEXT { + EMF_DEVICE_CONTEXT() : + // SPStyle: class with constructor + font_name(nullptr), + clip_id(0), + stroke_set(false), stroke_mode(0), stroke_idx(0), stroke_recidx(0), + fill_set(false), fill_mode(0), fill_idx(0), fill_recidx(0), + dirty(0), + // sizeWnd, sizeView, winorg, vieworg, + ScaleInX(0), ScaleInY(0), + ScaleOutX(0), ScaleOutY(0), + bkMode(U_TRANSPARENT), + // bkColor, textColor + textAlign(0) + // worldTransform, cur + { + font_name = nullptr; + sizeWnd = sizel_set( 0.0, 0.0 ); + sizeView = sizel_set( 0.0, 0.0 ); + winorg = point32_set( 0.0, 0.0 ); + vieworg = point32_set( 0.0, 0.0 ); + bkColor = U_RGB(255, 255, 255); // default foreground color (white) + textColor = U_RGB(0, 0, 0); // default foreground color (black) + worldTransform.eM11 = 1.0; + worldTransform.eM12 = 0.0; + worldTransform.eM21 = 0.0; + worldTransform.eM22 = 1.0; + worldTransform.eDx = 0.0; + worldTransform.eDy = 0.0; + cur = point32_set( 0, 0 ); + }; + SPStyle style; + char *font_name; + int clip_id; // 0 if none, else 1 + index into clips + bool stroke_set; + int stroke_mode; // enumeration from drawmode, not used if fill_set is not True + int stroke_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int stroke_recidx;// record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + bool fill_set; + int fill_mode; // enumeration from drawmode, not used if fill_set is not True + int fill_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int fill_recidx; // record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + int dirty; // holds the dirty bits for text, stroke, fill + U_SIZEL sizeWnd; + U_SIZEL sizeView; + U_POINTL winorg; + U_POINTL vieworg; + double ScaleInX, ScaleInY; + double ScaleOutX, ScaleOutY; + uint16_t bkMode; + U_COLORREF bkColor; + U_COLORREF textColor; + uint32_t textAlign; + U_XFORM worldTransform; + U_POINTL cur; +}; +using PEMF_DEVICE_CONTEXT = EMF_DEVICE_CONTEXT *; + +#define EMF_MAX_DC 128 + +struct EMF_CALLBACK_DATA { + + EMF_CALLBACK_DATA() : + // dc: array, structure w/ constructor + level(0), + E2IdirY(1.0), + D2PscaleX(1.0), D2PscaleY(1.0), + MM100InX(0), MM100InY(0), + PixelsInX(0), PixelsInY(0), + PixelsOutX(0), PixelsOutY(0), + ulCornerInX(0), ulCornerInY(0), + ulCornerOutX(0), ulCornerOutY(0), + mask(0), + arcdir(U_AD_COUNTERCLOCKWISE), + dwRop2(U_R2_COPYPEN), dwRop3(0), + MMX(0),MMY(0), + drawtype(0), + pDesc(nullptr), + // hatches, images, gradients, struct w/ constructor + tri(nullptr), + n_obj(0) + // emf_obj; + {}; + + Glib::ustring outsvg; + Glib::ustring path; + Glib::ustring outdef; + Glib::ustring defs; + + EMF_DEVICE_CONTEXT dc[EMF_MAX_DC+1]; // FIXME: This should be dynamic.. + int level; + + double E2IdirY; // EMF Y direction relative to Inkscape Y direction. Will be negative for MM_LOMETRIC etc. + double D2PscaleX,D2PscaleY; // EMF device to Inkscape Page scale. + float MM100InX, MM100InY; // size of the drawing in hundredths of a millimeter + float PixelsInX, PixelsInY; // size of the drawing, in EMF device pixels + float PixelsOutX, PixelsOutY; // size of the drawing, in Inkscape pixels + double ulCornerInX,ulCornerInY; // Upper left corner, from header rclBounds, in logical units + double ulCornerOutX,ulCornerOutY; // Upper left corner, in Inkscape pixels + uint32_t mask; // Draw properties + int arcdir; //U_AD_COUNTERCLOCKWISE 1 or U_AD_CLOCKWISE 2 + + uint32_t dwRop2; // Binary raster operation, 0 if none (use brush/pen unmolested) + uint32_t dwRop3; // Ternary raster operation, 0 if none (use brush/pen unmolested) + + float MMX; + float MMY; + + unsigned int drawtype; // one of 0 or U_EMR_FILLPATH, U_EMR_STROKEPATH, U_EMR_STROKEANDFILLPATH + char *pDesc; + // both of these end up in under the names shown here. These structures allow duplicates to be avoided. + EMF_STRINGS hatches; // hold pattern names, all like EMFhatch#_$$$$$$ where # is the EMF hatch code and $$$$$$ is the color + EMF_STRINGS images; // hold images, all like Image#, where # is the slot the image lives. + EMF_STRINGS gradients; // hold gradient names, all like EMF[HV]_$$$$$$_$$$$$$ where $$$$$$ are the colors + EMF_STRINGS clips; // hold clipping paths, referred to be the slot where the clipping path lives + TR_INFO *tri; // Text Reassembly data structure + + + int n_obj; + PEMF_OBJECT emf_obj; +}; +using PEMF_CALLBACK_DATA = EMF_CALLBACK_DATA *; + +class Emf : public Metafile +{ + +public: + + Emf(); // Empty constructor + + ~Emf() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + + static void init();//Initialize the class + +private: + +protected: + static void print_document_to_file(SPDocument *doc, const gchar *filename); + static double current_scale(PEMF_CALLBACK_DATA d); + static std::string current_matrix(PEMF_CALLBACK_DATA d, double x, double y, int useoffset); + static double current_rotation(PEMF_CALLBACK_DATA d); + static void enlarge_hatches(PEMF_CALLBACK_DATA d); + static int in_hatches(PEMF_CALLBACK_DATA d, char *test); + static uint32_t add_hatch(PEMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor); + static void enlarge_images(PEMF_CALLBACK_DATA d); + static int in_images(PEMF_CALLBACK_DATA d, const char *test); + static uint32_t add_image(PEMF_CALLBACK_DATA d, void *pEmr, uint32_t cbBits, uint32_t cbBmi, + uint32_t iUsage, uint32_t offBits, uint32_t offBmi); + static void enlarge_gradients(PEMF_CALLBACK_DATA d); + static int in_gradients(PEMF_CALLBACK_DATA d, const char *test); + static uint32_t add_gradient(PEMF_CALLBACK_DATA d, uint32_t gradientType, U_TRIVERTEX tv1, U_TRIVERTEX tv2); + + static void enlarge_clips(PEMF_CALLBACK_DATA d); + static int in_clips(PEMF_CALLBACK_DATA d, const char *test); + static void add_clips(PEMF_CALLBACK_DATA d, const char *clippath, unsigned int logic); + + static void output_style(PEMF_CALLBACK_DATA d, int iType); + static double _pix_x_to_point(PEMF_CALLBACK_DATA d, double px); + static double _pix_y_to_point(PEMF_CALLBACK_DATA d, double py); + static double pix_to_x_point(PEMF_CALLBACK_DATA d, double px, double py); + static double pix_to_y_point(PEMF_CALLBACK_DATA d, double px, double py); + static double pix_to_abs_size(PEMF_CALLBACK_DATA d, double px); + static void snap_to_faraway_pair(double *x, double *y); + static std::string pix_to_xy(PEMF_CALLBACK_DATA d, double x, double y); + static void select_pen(PEMF_CALLBACK_DATA d, int index); + static void select_extpen(PEMF_CALLBACK_DATA d, int index); + static void select_brush(PEMF_CALLBACK_DATA d, int index); + static void select_font(PEMF_CALLBACK_DATA d, int index); + static void delete_object(PEMF_CALLBACK_DATA d, int index); + static void insert_object(PEMF_CALLBACK_DATA d, int index, int type, PU_ENHMETARECORD pObj); + static int AI_hack(PU_EMRHEADER pEmr); + static uint32_t *unknown_chars(size_t count); + static void common_image_extraction(PEMF_CALLBACK_DATA d, void *pEmr, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, + uint32_t iUsage, uint32_t offBits, uint32_t cbBits, uint32_t offBmi, uint32_t cbBmi); + static int myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA d); + static void free_emf_strings(EMF_STRINGS name); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* EXTENSION_INTERNAL_EMF_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp new file mode 100644 index 0000000..cf75020 --- /dev/null +++ b/src/extension/internal/emf-print.cpp @@ -0,0 +1,2217 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile printing + *//* + * Authors: + * Ulf Erikson + * Jon A. Cruz + * Abhishek Sharma + * David Mathog + * + * Copyright (C) 2006-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include +#include +#include <3rdparty/libuemf/symbol_convert.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> + +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include "util/units.h" + +#include "inkscape-version.h" + +#include "extension/system.h" +#include "extension/print.h" +#include "document.h" +#include "path-prefix.h" + +#include "object/sp-pattern.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-item.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-clippath.h" +#include "style.h" +#include "display/cairo-utils.h" + +#include "splivarot.h" // pieces for union on shapes +#include "2geom/svg-path-parser.h" // to get from SVG text to Geom::Path +#include "display/canvas-bpath.h" // for SPWindRule +#include "display/cairo-utils.h" // for Inkscape::Pixbuf::PF_CAIRO + +#include "emf-print.h" + + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define PXPERMETER 2835 + +/* globals */ +static double PX2WORLD; +static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTLinGrad, FixPPTPatternAsHatch, FixImageRot; +static EMFTRACK *et = nullptr; +static EMFHANDLES *eht = nullptr; + +void PrintEmf::smuggle_adxkyrtl_out(const char *string, uint32_t **adx, double *ky, int *rtl, int *ndx, float scale) +{ + float fdx; + int i; + uint32_t *ladx; + const char *cptr = &string[strlen(string) + 1]; // this works because of the first fake terminator + + *adx = nullptr; + *ky = 0.0; // set a default value + sscanf(cptr, "%7d", ndx); + if (!*ndx) { + return; // this could happen with an empty string + } + cptr += 7; + ladx = (uint32_t *) malloc(*ndx * sizeof(uint32_t)); + if (!ladx) { + g_message("Out of memory"); + } + *adx = ladx; + for (i = 0; i < *ndx; i++, cptr += 7, ladx++) { + sscanf(cptr, "%7f", &fdx); + *ladx = (uint32_t) round(fdx * scale); + } + cptr++; // skip 2nd fake terminator + sscanf(cptr, "%7f", &fdx); + *ky = fdx; + cptr += 7; // advance over ky and its space + sscanf(cptr, "%07d", rtl); +} + +PrintEmf::PrintEmf() +{ + // all of the class variables are initialized elsewhere, many in PrintEmf::Begin, +} + + +unsigned int PrintEmf::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + + +unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) +{ + U_SIZEL szlDev, szlMm; + U_RECTL rclBounds, rclFrame; + char *rec; + gchar const *utf8_fn = mod->get_param_string("destination"); + + // Typically PX2WORLD is 1200/90, using inkscape's default dpi + PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); + FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); + FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); + FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); + FixPPTLinGrad = mod->get_param_bool("FixPPTLinGrad"); + FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); + FixImageRot = mod->get_param_bool("FixImageRot"); + + (void) emf_start(utf8_fn, 1000000, 250000, &et); // Initialize the et structure + (void) htable_create(128, 128, &eht); // Initialize the eht structure + + char *ansi_uri = (char *) utf8_fn; + + + // width and height in px + _width = doc->getWidth().value("px"); + _height = doc->getHeight().value("px"); + _doc_unit_scale = doc->getDocumentScale()[Geom::X]; + + // initialize a few global variables + hbrush = hbrushOld = hpen = 0; + htextalignment = U_TA_BASELINE | U_TA_LEFT; + use_stroke = use_fill = simple_shape = usebk = false; + + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv) { + const char *p1 = nv->attribute("pagecolor"); + char *p2; + uint32_t lc = strtoul(&p1[1], &p2, 16); // it looks like "#ABC123" + if (*p2) { + lc = 0; + } + gv.bgc = _gethexcolor(lc); + gv.rgb[0] = (float) U_RGBAGetR(gv.bgc) / 255.0; + gv.rgb[1] = (float) U_RGBAGetG(gv.bgc) / 255.0; + gv.rgb[2] = (float) U_RGBAGetB(gv.bgc) / 255.0; + } + + bool pageBoundingBox; + pageBoundingBox = mod->get_param_bool("pageBoundingBox"); + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(0, 0, _width, _height); + } else { + SPItem *doc_item = doc->getRoot(); + Geom::OptRect bbox = doc_item->desktopVisualBounds(); + if (bbox) { + d = *bbox; + } + } + + d *= Geom::Scale(Inkscape::Util::Quantity::convert(1, "px", "in")); + + float dwInchesX = d.width(); + float dwInchesY = d.height(); + + // dwInchesX x dwInchesY in micrometer units, 1200 dpi/25.4 -> dpmm + (void) drawing_size((int) ceil(dwInchesX * 25.4), (int) ceil(dwInchesY * 25.4),1200.0/25.4, &rclBounds, &rclFrame); + + // set up the reference device as 100 X A4 horizontal, (1200 dpi/25.4 -> dpmm). Extra digits maintain dpi better in EMF + int MMX = 216; + int MMY = 279; + (void) device_size(MMX, MMY, 1200.0 / 25.4, &szlDev, &szlMm); + int PixelsX = szlDev.cx; + int PixelsY = szlDev.cy; + + // set up the description: (version string)0(file)00 + char buff[1024]; + memset(buff, 0, sizeof(buff)); + char *p1 = strrchr(ansi_uri, '\\'); + char *p2 = strrchr(ansi_uri, '/'); + char *p = MAX(p1, p2); + if (p) { + p++; + } else { + p = ansi_uri; + } + snprintf(buff, sizeof(buff) - 1, "Inkscape %s \1%s\1", Inkscape::version_string, p); + uint16_t *Description = U_Utf8ToUtf16le(buff, 0, nullptr); + int cbDesc = 2 + wchar16len(Description); // also count the final terminator + (void) U_Utf16leEdit(Description, '\1', '\0'); // swap the temporary \1 characters for nulls + + // construct the EMRHEADER record and append it to the EMF in memory + rec = U_EMRHEADER_set(rclBounds, rclFrame, nullptr, cbDesc, Description, szlDev, szlMm, 0); + free(Description); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRHEADER"); + } + + + // Simplest mapping mode, supply all coordinates in pixels + rec = U_EMRSETMAPMODE_set(U_MM_TEXT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRSETMAPMODE"); + } + + + // In earlier versions this was used to scale from inkscape's dpi of 90 to + // the files 1200 dpi, taking into account PX2WORLD which was 20. Now PX2WORLD + // is set so that this matrix is unitary. The usual value of PX2WORLD is 1200/90, + // but might be different if the internal dpi is changed. + + U_XFORM worldTransform; + worldTransform.eM11 = 1.0; + worldTransform.eM12 = 0.0; + worldTransform.eM21 = 0.0; + worldTransform.eM22 = 1.0; + worldTransform.eDx = 0; + worldTransform.eDy = 0; + + rec = U_EMRMODIFYWORLDTRANSFORM_set(worldTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRMODIFYWORLDTRANSFORM"); + } + + + if (true) { + snprintf(buff, sizeof(buff) - 1, "Screen=%dx%dpx, %dx%dmm", PixelsX, PixelsY, MMX, MMY); + rec = textcomment_set(buff); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at textcomment_set 1"); + } + + snprintf(buff, sizeof(buff) - 1, "Drawing=%.1fx%.1fpx, %.1fx%.1fmm", _width, _height, Inkscape::Util::Quantity::convert(dwInchesX, "in", "mm"), Inkscape::Util::Quantity::convert(dwInchesY, "in", "mm")); + rec = textcomment_set(buff); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at textcomment_set 1"); + } + } + + /* set some parameters, else the program that reads the EMF may default to other values */ + + rec = U_EMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETBKMODE_set"); + } + + hpolyfillmode = U_WINDING; + rec = U_EMRSETPOLYFILLMODE_set(U_WINDING); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETPOLYFILLMODE_set"); + } + + // Text alignment: (only changed if RTL text is encountered ) + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the EMF text alignment must always be TA_BASELINE|TA_LEFT. + htextalignment = U_TA_BASELINE | U_TA_LEFT; + rec = U_EMRSETTEXTALIGN_set(U_TA_BASELINE | U_TA_LEFT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTALIGN_set"); + } + + htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + rec = U_EMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTCOLOR_set"); + } + + rec = U_EMRSETROP2_set(U_R2_COPYPEN); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETROP2_set"); + } + + /* miterlimit is set with eah pen, so no need to check for it changes as in WMF */ + + return 0; +} + + +unsigned int PrintEmf::finish(Inkscape::Extension::Print * /*mod*/) +{ + do_clip_if_present(nullptr); // Terminate any open clip. + char *rec; + if (!et) { + return 0; + } + + + // earlier versions had flush of fill here, but it never executed and was removed + + rec = U_EMREOF_set(0, nullptr, et); // generate the EOF record + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::finish"); + } + (void) emf_finish(et, eht); // Finalize and write out the EMF + emf_free(&et); // clean up + htable_free(&eht); // clean up + + return 0; +} + + +unsigned int PrintEmf::comment( + Inkscape::Extension::Print * /*module*/, + const char * /*comment*/) +{ + if (!et) { + return 0; + } + + // earlier versions had flush of fill here, but it never executed and was removed + + return 0; +} + + +// fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. +int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) +{ + float rgb[3]; + char *rec; + U_LOGBRUSH lb; + uint32_t brush, fmode; + MFDrawMode fill_mode; + Inkscape::Pixbuf *pixbuf; + uint32_t brushStyle; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width = 0; // quiets a harmless compiler warning, initialization not otherwise required. + uint32_t height = 0; + + if (!et) { + return 0; + } + + // set a default fill in case we can't figure out a better way to do it + fmode = U_ALTERNATE; + fill_mode = DRAW_PAINT; + brushStyle = U_BS_SOLID; + hatchType = U_HS_SOLIDCLR; + bkColor = U_RGB(0, 0, 0); + if (fcolor) { + hatchColor = *fcolor; + } else { + hatchColor = U_RGB(0, 0, 0); + } + + if (!fcolor && style) { + if (style->fill.isColor()) { + fill_mode = DRAW_PAINT; +#if 0 +// opacity not supported by EMF + float opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (opacity <= 0.0) { + opacity = 0.0; // basically the same as no fill + } +#endif + style->fill.value.color.get_rgb_floatv(rgb); + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); + } else if (SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + fill_mode = DRAW_IMAGE; + } else { // pattern + fill_mode = DRAW_PATTERN; + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + fill_mode = DRAW_PATTERN; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + brushStyle = U_BS_HATCHED; + } else if (SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below just sets the color to the average of the stops + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPLinearGradient *lg = nullptr; + SPRadialGradient *rg = nullptr; + + if (SP_IS_LINEARGRADIENT(paintserver)) { + lg = SP_LINEARGRADIENT(paintserver); + SP_GRADIENT(lg)->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_LINEAR_GRADIENT; + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + rg = SP_RADIALGRADIENT(paintserver); + SP_GRADIENT(rg)->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_RADIAL_GRADIENT; + } else { + // default fill + } + + if (rg) { + if (FixPPTGrad2Polys) { + return hold_gradient(rg, fill_mode); + } else { + hatchColor = avg_stop_color(rg); + } + } else if (lg) { + if (FixPPTGrad2Polys || FixPPTLinGrad) { + return hold_gradient(lg, fill_mode); + } else { + hatchColor = avg_stop_color(lg); + } + } + } + } else { // if (!style) + // default fill + } + + lb = logbrush_set(brushStyle, hatchColor, hatchType); + + switch (fill_mode) { + case DRAW_LINEAR_GRADIENT: // fill with average color unless gradients are converted to slices + case DRAW_RADIAL_GRADIENT: // ditto + case DRAW_PAINT: + case DRAW_PATTERN: + // SVG text has no background attribute, so OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + if (usebk) { + rec = U_EMRSETBKCOLOR_set(bkColor); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETBKCOLOR_set"); + } + rec = U_EMRSETBKMODE_set(U_OPAQUE); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETBKMODE_set"); + } + } + rec = createbrushindirect_set(&brush, eht, lb); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at createbrushindirect_set"); + } + break; + case DRAW_IMAGE: + char *px; + char *rgba_px; + uint32_t cbPx; + uint32_t colortype; + PU_RGBQUAD ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + rec = createdibpatternbrushpt_set(&brush, eht, U_DIB_RGB_COLORS, Bmi, cbPx, px); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at createdibpatternbrushpt_set"); + } + free(px); + free(Bmi); // ct will be NULL because of colortype + break; + } + + hbrush = brush; // need this later for destroy_brush + rec = selectobject_set(brush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at selectobject_set"); + } + + if (fmode != hpolyfillmode) { + hpolyfillmode = fmode; + rec = U_EMRSETPOLYFILLMODE_set(fmode); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETPOLYdrawmode_set"); + } + } + + return 0; +} + + +void PrintEmf::destroy_brush() +{ + char *rec; + // before an object may be safely deleted it must no longer be selected + // select in a stock object to deselect this one, the stock object should + // never be used because we always select in a new one before drawing anythingrestore previous brush, necessary??? Would using a default stock object not work? + rec = selectobject_set(U_NULL_BRUSH, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_brush at selectobject_set"); + } + if (hbrush) { + rec = deleteobject_set(&hbrush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_brush"); + } + hbrush = 0; + } +} + + +int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) +{ + U_EXTLOGPEN *elp; + U_NUM_STYLEENTRY n_dash = 0; + U_STYLEENTRY *dash = nullptr; + char *rec = nullptr; + int linestyle = U_PS_SOLID; + int linecap = 0; + int linejoin = 0; + uint32_t pen; + uint32_t brushStyle; + Inkscape::Pixbuf *pixbuf; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width, height; + char *px = nullptr; + char *rgba_px; + uint32_t cbPx = 0; + uint32_t colortype; + PU_RGBQUAD ct = nullptr; + int numCt = 0; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi = nullptr; + + if (!et) { + return 0; + } + + // set a default stroke in case we can't figure out a better way to do it + brushStyle = U_BS_SOLID; + hatchColor = U_RGB(0, 0, 0); + hatchType = U_HS_HORIZONTAL; + bkColor = U_RGB(0, 0, 0); + + if (style) { + float rgb[3]; + + if (SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->stroke.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + brushStyle = U_BS_DIBPATTERN; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + } else { // pattern + brushStyle = U_BS_HATCHED; + if (usebk) { // OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + rec = U_EMRSETBKCOLOR_set(bkColor); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETBKCOLOR_set"); + } + rec = U_EMRSETBKMODE_set(U_OPAQUE); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETBKMODE_set"); + } + } + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + brushStyle = U_BS_HATCHED; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + } else if (SP_IS_GRADIENT(SP_STYLE_STROKE_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below has no net effect. + + SPPaintServer *paintserver = style->stroke.value.href->getObject(); + if (SP_IS_LINEARGRADIENT(paintserver)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(paintserver); + + SP_GRADIENT(lg)->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point p1(lg->x1.computed, lg->y1.computed); + Geom::Point p2(lg->x2.computed, lg->y2.computed); + + if (lg->gradientTransform_set) { + p1 = p1 * lg->gradientTransform; + p2 = p2 * lg->gradientTransform; + } + hatchColor = avg_stop_color(lg); + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(paintserver); + + SP_GRADIENT(rg)->ensureVector(); // when exporting from commandline, vector is not built + double r = rg->r.computed; + + Geom::Point c(rg->cx.computed, rg->cy.computed); + Geom::Point xhandle_point(r, 0); + Geom::Point yhandle_point(0, -r); + yhandle_point += c; + xhandle_point += c; + if (rg->gradientTransform_set) { + c = c * rg->gradientTransform; + yhandle_point = yhandle_point * rg->gradientTransform; + xhandle_point = xhandle_point * rg->gradientTransform; + } + hatchColor = avg_stop_color(rg); + } else { + // default fill + } + } else if (style->stroke.isColor()) { // test last, always seems to be set, even for other types above + style->stroke.value.color.get_rgb_floatv(rgb); + brushStyle = U_BS_SOLID; + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + hatchType = U_HS_SOLIDCLR; + } else { + // default fill + } + + + + using Geom::X; + using Geom::Y; + + Geom::Point zero(0, 0); + Geom::Point one(1, 1); + Geom::Point p0(zero * transform); + Geom::Point p1(one * transform); + Geom::Point p(p1 - p0); + + double scale = sqrt((p[X] * p[X]) + (p[Y] * p[Y])) / sqrt(2); + + if (!style->stroke_width.computed) { + return 0; //if width is 0 do not (reset) the pen, it should already be NULL_PEN + } + uint32_t linewidth = MAX(1, (uint32_t) round(scale * style->stroke_width.computed * PX2WORLD)); + + if (style->stroke_linecap.computed == 0) { + linecap = U_PS_ENDCAP_FLAT; + } else if (style->stroke_linecap.computed == 1) { + linecap = U_PS_ENDCAP_ROUND; + } else if (style->stroke_linecap.computed == 2) { + linecap = U_PS_ENDCAP_SQUARE; + } + + if (style->stroke_linejoin.computed == 0) { + linejoin = U_PS_JOIN_MITER; + } else if (style->stroke_linejoin.computed == 1) { + linejoin = U_PS_JOIN_ROUND; + } else if (style->stroke_linejoin.computed == 2) { + linejoin = U_PS_JOIN_BEVEL; + } + + if (!style->stroke_dasharray.values.empty()) { + if (FixPPTDashLine) { // will break up line into many smaller lines. Override gradient if that was set, cannot do both. + brushStyle = U_BS_SOLID; + hatchType = U_HS_HORIZONTAL; + } else { + unsigned i = 0; + while ((linestyle != U_PS_USERSTYLE) && (i < style->stroke_dasharray.values.size())) { + if (style->stroke_dasharray.values[i].value > 0.00000001) { + linestyle = U_PS_USERSTYLE; + } + i++; + } + + if (linestyle == U_PS_USERSTYLE) { + n_dash = style->stroke_dasharray.values.size(); + dash = new uint32_t[n_dash]; + for (i = 0; i < n_dash; i++) { + dash[i] = MAX(1, (uint32_t)round(scale * style->stroke_dasharray.values[i].value * PX2WORLD)); + } + } + } + } + + elp = extlogpen_set( + U_PS_GEOMETRIC | linestyle | linecap | linejoin, + linewidth, + brushStyle, + hatchColor, + hatchType, + n_dash, + dash); + + } else { // if (!style) + linejoin = 0; + elp = extlogpen_set( + linestyle, + 1, + U_BS_SOLID, + U_RGB(0, 0, 0), + U_HS_HORIZONTAL, + 0, + nullptr); + } + + rec = extcreatepen_set(&pen, eht, Bmi, cbPx, px, elp); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at extcreatepen_set"); + } + free(elp); + if (Bmi) { + free(Bmi); + } + if (px) { + free(px); // ct will always be NULL + } + + rec = selectobject_set(pen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at selectobject_set"); + } + hpen = pen; // need this later for destroy_pen + + if (linejoin == U_PS_JOIN_MITER) { + float miterlimit = style->stroke_miterlimit.value; // This is a ratio. + + if (miterlimit < 1) { + miterlimit = 1; + } + + rec = U_EMRSETMITERLIMIT_set((uint32_t) miterlimit); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETMITERLIMIT_set"); + } + } + + if (n_dash) { + delete[] dash; + } + return 0; +} + +// set the current pen to the stock object NULL_PEN and then delete the defined pen object, if there is one. +void PrintEmf::destroy_pen() +{ + char *rec = nullptr; + // before an object may be safely deleted it must no longer be selected + // select in a stock object to deselect this one, the stock object should + // never be used because we always select in a new one before drawing anythingrestore previous brush, necessary??? Would using a default stock object not work? + rec = selectobject_set(U_NULL_PEN, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_pen at selectobject_set"); + } + if (hpen) { + rec = deleteobject_set(&hpen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_pen"); + } + hpen = 0; + } +} + +/* Return a Path consisting of just the corner points of the single path in a PathVector. If the +PathVector has more than one path, or that one path is open, or any of its segments are curved, then the +returned PathVector is an empty path. If the input path is already just straight lines and vertices the output will be the +same as the sole path in the input. */ + +Geom::Path PrintEmf::pathv_to_simple_polygon(Geom::PathVector const &pathv, int *vertices) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point P1_lead; + Geom::Point v1,v2; + Geom::Path output; + Geom::Path bad; + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv); + Geom::PathVector::const_iterator pit = pv.begin(); + Geom::PathVector::const_iterator pit2 = pv.begin(); + int first_seg=1; + ++pit2; + *vertices = 0; + if(pit->end_closed() != pit->end_default())return(bad); // path must be closed + if(pit2 != pv.end())return(bad); // there may only be one path + P1_trail = pit->finalPoint(); + Geom::Path::const_iterator cit = pit->begin(); + P1 = cit->initialPoint(); + for(;cit != pit->end_closed();++cit) { + if (!is_straight_curve(*cit)) { + *vertices = 0; + return(bad); + } + P1_lead = cit->finalPoint(); + if(Geom::are_near(P1_lead, P1, 1e-5))continue; // duplicate points at the same coordinate + v1 = unit_vector(P1 - P1_trail); + v2 = unit_vector(P1_lead - P1 ); + if(Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ // P1 is within a straight line + P1 = P1_lead; + continue; + } + // P1 is the center point of a turn of some angle + if(!*vertices){ + output.start( P1 ); + output.close( pit->closed() ); + } + if(!Geom::are_near(P1, P1_trail, 1e-5)){ // possible for P1 to start on the end point + Geom::LineSegment ls(P1_trail, P1); + output.append(ls); + if(first_seg){ + *vertices += 2; + first_seg=0; + } + else { + *vertices += 1; + } + } + P1_trail = P1; + P1 = P1_lead; + } + return(output); +} + +/* Returns the simplified PathVector (no matter what). + Sets is_rect if it is a rectangle. + Sets angle that will rotate side closest to horizontal onto horizontal. +*/ +Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, double *angle) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point v1,v2; + int vertices; + Geom::Path pR = pathv_to_simple_polygon(pathv, &vertices); + *is_rect = false; + if(vertices==4){ // or else it cannot be a rectangle + int vertex_count=0; + /* Get the ends of the LAST line segment. + Find minimum rotation to align rectangle with X,Y axes. (Very degenerate if it is rotated 45 degrees.) */ + *angle = 10.0; /* must be > than the actual angle in radians. */ + for(Geom::Path::iterator cit = pR.begin();; ++cit){ + P1_trail = cit->initialPoint(); + P1 = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + if(v1[Geom::X] > 0){ // only check the 1 or 2 points on vectors aimed the same direction as unit X + double ang = asin(v1[Geom::Y]); // because component is rotation by ang of {1,0| vector + if(fabs(ang) < fabs(*angle))*angle = -ang; // y increases down, flips sign on angle + } + if(cit == pR.end_open())break; + } + + /* For increased numerical stability, snap the angle to the nearest 1/100th of a degree. */ + double convert = 36000.0/ (2.0 * M_PI); + *angle = round(*angle * convert)/convert; + + // at this stage v1 holds the last vector in the path, whichever direction it points. + for(Geom::Path::iterator cit = pR.begin(); ;++cit) { + v2 = v1; + P1_trail = cit->initialPoint(); + P1 = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + // P1 is center of a turn that is not 90 degrees. Limit comes from cos(89.9) = .001745 + if(!Geom::are_near(dot(v1,v2), 0.0, 2e-3))break; + vertex_count++; + if(cit == pR.end_open())break; + } + if(vertex_count == 4){ + *is_rect=true; + } + } + return(pR); +} + +/* Compare a vector with a rectangle's orientation (angle needed to rotate side(s) + closest to horizontal to exactly horizontal) and return: + 0 none of the following + 1 parallel to horizontal + 2 parallel to vertical + 3 antiparallel to horizontal + 4 antiparallel to vertical +*/ +int PrintEmf::vector_rect_alignment(double angle, Geom::Point vtest){ + int stat = 0; + Geom::Point v1 = Geom::unit_vector(vtest); // unit vector to test alignment + Geom::Point v2 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v3 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + if( Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ stat = 1; } + else if(Geom::are_near(dot(v1,v2),-1.0, 1e-5)){ stat = 2; } + else if(Geom::are_near(dot(v1,v3), 1.0, 1e-5)){ stat = 3; } + else if(Geom::are_near(dot(v1,v3),-1.0, 1e-5)){ stat = 4; } + return(stat); +} + +/* retrieve the point at the indicated corner: + 0 UL (and default) + 1 UR + 2 LR + 3 LL + Needed because the start can be any point, and the direction could run either + clockwise or counterclockwise. This should work even if the corners of the rectangle + are slightly displaced. +*/ +Geom::Point PrintEmf::get_pathrect_corner(Geom::Path pathRect, double angle, int corner){ + Geom::Point center(0,0); + for(Geom::Path::iterator cit = pathRect.begin(); ; ++cit) { + center += cit->initialPoint()/4.0; + if(cit == pathRect.end_open())break; + } + + int LR; // 1 if Left, 0 if Right + int UL; // 1 if Lower, 0 if Upper (as viewed on screen, y coordinates increase downwards) + switch(corner){ + case 1: //UR + LR = 0; + UL = 0; + break; + case 2: //LR + LR = 0; + UL = 1; + break; + case 3: //LL + LR = 1; + UL = 1; + break; + default: //UL + LR = 1; + UL = 0; + break; + } + + Geom::Point v1 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side (sign change because Y increases DOWN) + Geom::Point P1; + for(Geom::Path::iterator cit = pathRect.begin(); ; ++cit) { + P1 = cit->initialPoint(); + + if ( ( LR == (dot(P1 - center,v1) > 0 ? 0 : 1) ) + && ( UL == (dot(P1 - center,v2) > 0 ? 1 : 0) ) ) break; + if(cit == pathRect.end_open())break; + } + return(P1); +} + +U_TRIVERTEX PrintEmf::make_trivertex(Geom::Point Pt, U_COLORREF uc){ + U_TRIVERTEX tv; + using Geom::X; + using Geom::Y; + tv.x = (int32_t) round(Pt[X]); + tv.y = (int32_t) round(Pt[Y]); + tv.Red = uc.Red << 8; + tv.Green = uc.Green << 8; + tv.Blue = uc.Blue << 8; + tv.Alpha = uc.Reserved << 8; // EMF will ignore this + return(tv); +} + +/* Examine clip. If there is a (new) one then apply it. If there is one and it is the + same as the preceding one, leave the preceding one active. If style is NULL + terminate the current clip, if any, and return. +*/ +void PrintEmf::do_clip_if_present(SPStyle const *style){ + char *rec; + static SPClipPath *scpActive = nullptr; + if(!style){ + if(scpActive){ // clear the existing clip + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + scpActive=nullptr; + } + } else { + /* The current implementation converts only one level of clipping. If there were more + clips further up the stack they should be combined with the pathvector using "and". Since this + comes up rarely, and would involve a lot of searching (all the way up the stack for every + draw operation), it has not yet been implemented. + + Note, to debug this section of code use print statements on sp_svg_write_path(combined_pathvector). + */ + /* find the first clip_ref at object or up the stack. There may not be one. */ + SPClipPath *scp = nullptr; + SPItem *item = SP_ITEM(style->object); + while(true) { + scp = item->getClipObject(); + if(scp)break; + item = SP_ITEM(item->parent); + if(!item || SP_IS_ROOT(item))break; // this will never be a clipping path + } + + if(scp != scpActive){ // change or remove the clipping + if(scpActive){ // clear the existing clip + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + scpActive = nullptr; + } + + if (scp) { // set the new clip + /* because of units and who knows what other transforms that might be applied above we + need the full transform all the way to the root. + */ + Geom::Affine tf = item->transform; + SPItem *scan_item = item; + while(true) { + scan_item = SP_ITEM(scan_item->parent); + if(!scan_item)break; + tf *= scan_item->transform; + } + tf *= Geom::Scale(_doc_unit_scale);; // Transform must be in PIXELS, no matter what the document unit is. + + /* find the clipping path */ + Geom::PathVector combined_pathvector; + Geom::Affine tfc; // clipping transform, generally not the same as item transform + for (auto& child: scp->children) { + item = SP_ITEM(&child); + if (!item) { + break; + } + if (SP_IS_GROUP(item)) { // not implemented + // return sp_group_render(item); + combined_pathvector = merge_PathVector_with_group(combined_pathvector, item, tfc); + } else if (SP_IS_SHAPE(item)) { + combined_pathvector = merge_PathVector_with_shape(combined_pathvector, item, tfc); + } else { // not implemented + } + } + + if (!combined_pathvector.empty()) { // if clipping path isn't empty, define EMF clipping record + scpActive = scp; // remember for next time + // the sole purpose of this SAVEDC is to let us clear the clipping region later. + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + (void) draw_pathv_to_EMF(combined_pathvector, tf); + rec = U_EMRSELECTCLIPPATH_set(U_RGN_COPY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::do_clip_if_present at U_EMRSELECTCLIPPATH_set"); + } + } + else { + scpActive = nullptr; // no valid path available to draw, so no DC was saved, so no signal to restore + } + } // change or remove clipping + } // scp exists + } // style exists +} + +Geom::PathVector PrintEmf::merge_PathVector_with_group(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform) +{ + Geom::PathVector new_combined_pathvector; + if(!SP_IS_GROUP(item))return(new_combined_pathvector); // sanity test, only a group should be passed in, return empty if something else happens + + new_combined_pathvector = combined_pathvector; + SPGroup *group = SP_GROUP(item); + Geom::Affine tfc = item->transform * transform; + for (auto& child: group->children) { + item = SP_ITEM(&child); + if (!item) { + break; + } + if (SP_IS_GROUP(item)) { + new_combined_pathvector = merge_PathVector_with_group(new_combined_pathvector, item, tfc); // could be endlessly recursive on a badly formed SVG + } else if (SP_IS_SHAPE(item)) { + new_combined_pathvector = merge_PathVector_with_shape(new_combined_pathvector, item, tfc); + } else { // not implemented + } + } + return new_combined_pathvector; +} + +Geom::PathVector PrintEmf::merge_PathVector_with_shape(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform) +{ + Geom::PathVector new_combined_pathvector; + if(!SP_IS_SHAPE(item))return(new_combined_pathvector); // sanity test, only a shape should be passed in, return empty if something else happens + + Geom::Affine tfc = item->transform * transform; + SPShape *shape = SP_SHAPE(item); + if (shape->_curve) { + Geom::PathVector const & new_vect = shape->_curve->get_pathvector(); + if(combined_pathvector.empty()){ + new_combined_pathvector = new_vect * tfc; + } + else { + new_combined_pathvector = sp_pathvector_boolop(new_vect * tfc, combined_pathvector, bool_op_union , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + } + } + return new_combined_pathvector; +} + +unsigned int PrintEmf::fill( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const & /*transform*/, SPStyle const *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + char *rec; + using Geom::X; + using Geom::Y; + Geom::Affine tf = m_tr_stack.top(); + + do_clip_if_present(style); // If clipping is needed set it up + + use_fill = true; + use_stroke = false; + + fill_transform = tf; + + int brush_stat = create_brush(style, nullptr); + + /* native linear gradients are only used if the object is a rectangle AND the gradient is parallel to the sides of the object */ + bool is_Rect = false; + double angle; + int rectDir=0; + Geom::Path pathRect; + if(FixPPTLinGrad && brush_stat && gv.mode == DRAW_LINEAR_GRADIENT){ + Geom::PathVector pvr = pathv * fill_transform; + pathRect = pathv_to_rect(pvr, &is_Rect, &angle); + if(is_Rect){ + /* Gradientfill records can only be used if the gradient is parallel to the sides of the rectangle. + That must be checked here so that we can fall back to another form of gradient fill if it is not + the case. */ + rectDir = vector_rect_alignment(angle, (gv.p2 - gv.p1) * fill_transform); + if(!rectDir)is_Rect = false; + } + if(!is_Rect && !FixPPTGrad2Polys)brush_stat=0; // fall all the way back to a solid fill + } + + if (brush_stat) { // only happens if the style is a gradient + /* + Handle gradients. Uses modified livarot as 2geom boolops is currently broken. + Can handle gradients with multiple stops. + + The overlap is needed to avoid antialiasing artifacts when edges are not strictly aligned on pixel boundaries. + There is an inevitable loss of accuracy saving through an EMF file because of the integer coordinate system. + Keep the overlap quite large so that loss of accuracy does not remove an overlap. + */ + destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below + Geom::Path cutter; + float rgb[3]; + U_COLORREF wc, c1, c2; + FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); + double doff, doff_base, doff_range; + double divisions = 128.0; + int nstops; + int istop = 1; + float opa; // opacity at stop + + SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here + nstops = tg->vector.stops.size(); + tg->vector.stops[0].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[0].opacity; // first stop + c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[nstops - 1].opacity; // last stop + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + doff = 0.0; + doff_base = 0.0; + doff_range = tg->vector.stops[1].offset; // next or last stop + + if (gv.mode == DRAW_RADIAL_GRADIENT) { + Geom::Point xv = gv.p2 - gv.p1; // X' vector + Geom::Point yv = gv.p3 - gv.p1; // Y' vector + Geom::Point xuv = Geom::unit_vector(xv); // X' unit vector + double rx = hypot(xv[X], xv[Y]); + double ry = hypot(yv[X], yv[Y]); + double range = fmax(rx, ry); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* radial gradient might stop part way through the shape, fill with outer color from there to "infinity". + Do this first so that outer colored ring will overlay it. + */ + pathvc = center_elliptical_hole_as_SVG_PathV(gv.p1, rx * (1.0 - overlap / range), ry * (1.0 - overlap / range), asin(xuv[Y])); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_oddEven, frb); + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + + pathvc = center_elliptical_ring_as_SVG_PathV(gv.p1, rx * start / range, ry * start / range, rx * stop / range, ry * stop / range, asin(xuv[Y])); + + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range) { + istop++; + if (istop >= nstops) { + istop = nstops - 1; + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else if (gv.mode == DRAW_LINEAR_GRADIENT) { + if(is_Rect){ + int gMode; + Geom::Point ul, ur, lr; + Geom::Point outUL, outLR; // UL,LR corners of a stop rectangle, in OUTPUT coordinates + U_TRIVERTEX ut[2]; + U_GRADIENT4 ug4; + U_RECTL rcb; + U_XFORM tmpTransform; + double wRect, hRect; + + /* coordinates: upper left, upper right, and lower right corners of the rectangle. + inkscape transform already applied, but needs to be scaled to EMF coordinates. */ + ul = get_pathrect_corner(pathRect, angle, 0) * PX2WORLD; + ur = get_pathrect_corner(pathRect, angle, 1) * PX2WORLD; + lr = get_pathrect_corner(pathRect, angle, 2) * PX2WORLD; + wRect = Geom::distance(ul,ur); + hRect = Geom::distance(ur,lr); + + /* The basic rectangle for all of these is placed with its UL corner at 0,0 with a size wRect,hRect. + Apply a world transform to place/scale it into the appropriate position on the drawing. + Actual gradientfill records are either this entire rectangle or slices of it as defined by the stops. + This rectangle has already been transformed by tf (whatever rotation/scale) Inkscape had applied to it. + */ + + Geom::Affine tf2 = Geom::Rotate(-angle); // the rectangle may be drawn skewed to the coordinate system + tmpTransform.eM11 = tf2[0]; + tmpTransform.eM12 = tf2[1]; + tmpTransform.eM21 = tf2[2]; + tmpTransform.eM22 = tf2[3]; + tmpTransform.eDx = round((ul)[Geom::X]); // use explicit round for better stability + tmpTransform.eDy = round((ul)[Geom::Y]); + + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + + rec = U_EMRMODIFYWORLDTRANSFORM_set(tmpTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRMODIFYWORLDTRANSFORM"); + } + + for(;istopvector.stops[istop].offset; // next or last stop + if(rectDir == 1 || rectDir == 2){ + outUL = Geom::Point(doff_base *wRect, 0 ); + outLR = Geom::Point(doff_range*wRect, hRect); + gMode = U_GRADIENT_FILL_RECT_H; + } + else { + outUL = Geom::Point(0, doff_base *hRect); + outLR = Geom::Point(wRect,doff_range*hRect); + gMode = U_GRADIENT_FILL_RECT_V; + } + + doff_base = doff_range; + rcb.left = round(outUL[X]); // use explicit round for better stability + rcb.top = round(outUL[Y]); + rcb.right = round(outLR[X]); + rcb.bottom = round(outLR[Y]); + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + if(rectDir == 2 || rectDir == 4){ // gradient is reversed, so swap colors + ut[0] = make_trivertex(outUL, c2); + ut[1] = make_trivertex(outLR, c1); + } + else { + ut[0] = make_trivertex(outUL, c1); + ut[1] = make_trivertex(outLR, c2); + } + c1 = c2; // for next stop + ug4.UpperLeft = 0; + ug4.LowerRight= 1; + rec = U_EMRGRADIENTFILL_set(rcb, 2, 1, gMode, ut, (uint32_t *) &ug4 ); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRGRADIENTFILL_set"); + } + } + + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + } + else { + Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector + Geom::Point puv = uv.cw(); // perp. to unit vector + double range = Geom::distance(gv.p1, gv.p2); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* before lower end of gradient, overlap first slice position */ + wc = weight_opacity(c1); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + /* after high end of gradient, overlap last slice position */ + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range) { + istop++; + if (istop >= nstops) { + istop = nstops - 1; + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } + } else { + g_error("Fatal programming error in PrintEmf::fill, invalid gradient type detected"); + } + use_fill = false; // gradients handled, be sure stroke does not use stroke and fill + } else { + /* + Inkscape was not calling create_pen for objects with no border. + This was because it never called stroke() (next method). + PPT, and presumably others, pick whatever they want for the border if it is not specified, so no border can + become a visible border. + To avoid this force the pen to NULL_PEN if we can determine that no pen will be needed after the fill. + */ + if (style->stroke.noneSet || style->stroke_width.computed == 0.0) { + destroy_pen(); //this sets the NULL_PEN + } + + /* postpone fill in case stroke also required AND all stroke paths closed + Dashes converted to line segments will "open" a closed path. + */ + bool all_closed = true; + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (pit.end_default() != pit.end_closed()) { + all_closed = false; + } + } + } + if ( + (style->stroke.isNone() || style->stroke.noneSet || style->stroke_width.computed == 0.0) || + (!style->stroke_dasharray.values.empty() && FixPPTDashLine) || + !all_closed + ) { + print_pathv(pathv, fill_transform); // do any fills. side effect: clears fill_pathv + use_fill = false; + } + } + + return 0; +} + + +unsigned int PrintEmf::stroke( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, const Geom::Affine &/*transform*/, const SPStyle *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + do_clip_if_present(style); // If clipping is needed set it up + + use_stroke = true; + // use_fill was set in ::fill, if it is needed + + if (create_pen(style, tf)) { + return 0; + } + + if (!style->stroke_dasharray.values.empty() && FixPPTDashLine) { + // convert the path, gets its complete length, and then make a new path with parameter length instead of t + Geom::Piecewise > tmp_pathpw; // pathv-> sbasis + Geom::Piecewise > tmp_pathpw2; // sbasis using arc length parameter + Geom::Piecewise > tmp_pathpw3; // new (discontinuous) path, composed of dots/dashes + Geom::Piecewise > first_frag; // first fragment, will be appended at end + int n_dash = style->stroke_dasharray.values.size(); + int i = 0; //dash index + double tlength; // length of tmp_pathpw + double slength = 0.0; // start of gragment + double elength; // end of gragment + for (const auto & i : pathv) { + tmp_pathpw.concat(i.toPwSb()); + } + tlength = length(tmp_pathpw, 0.1); + tmp_pathpw2 = arc_length_parametrization(tmp_pathpw); + + // go around the dash array repeatedly until the entire path is consumed (but not beyond). + while (slength < tlength) { + elength = slength + style->stroke_dasharray.values[i++].value; + if (elength > tlength) { + elength = tlength; + } + Geom::Piecewise > fragment(portion(tmp_pathpw2, slength, elength)); + if (slength) { + tmp_pathpw3.concat(fragment); + } else { + first_frag = fragment; + } + slength = elength; + slength += style->stroke_dasharray.values[i++].value; // the gap + if (i >= n_dash) { + i = 0; + } + } + tmp_pathpw3.concat(first_frag); // may merge line around start point + Geom::PathVector out_pathv = Geom::path_from_piecewise(tmp_pathpw3, 0.01); + print_pathv(out_pathv, tf); + } else { + print_pathv(pathv, tf); + } + + use_stroke = false; + use_fill = false; + + if (usebk) { // OPAQUE was set, revert to TRANSPARENT + usebk = false; + rec = U_EMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSETBKMODE_set"); + } + } + + return 0; +} + + +// Draws simple_shapes, those with closed EMR_* primitives, like polygons, rectangles and ellipses. +// These use whatever the current pen/brush are and need not be followed by a FILLPATH or STROKEPATH. +// For other paths it sets a few flags and returns. +bool PrintEmf::print_simple_shape(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * transform); + + int nodes = 0; + int moves = 0; + int lines = 0; + int curves = 0; + char *rec = nullptr; + + for (auto & pit : pv) { + moves++; + nodes++; + + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + nodes++; + + if (is_straight_curve(*cit)) { + lines++; + } else if (dynamic_cast(&*cit)) { + curves++; + } + } + } + + if (!nodes) { + return false; + } + + U_POINT *lpPoints = new U_POINT[moves + lines + curves * 3]; + int i = 0; + + /** + * For all Subpaths in the + */ + for (auto & pit : pv) { + using Geom::X; + using Geom::Y; + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + int32_t const x0 = (int32_t) round(p0[X]); + int32_t const y0 = (int32_t) round(p0[Y]); + + lpPoints[i].x = x0; + lpPoints[i].y = y0; + i = i + 1; + + /** + * For all segments in the subpath + */ + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + i = i + 1; + } else if (Geom::CubicBezier const *cubic = dynamic_cast(&*cit)) { + std::vector points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + lpPoints[i + 1].x = x2; + lpPoints[i + 1].y = y2; + lpPoints[i + 2].x = x3; + lpPoints[i + 2].y = y3; + i = i + 3; + } + } + } + + bool done = false; + bool closed = (lpPoints[0].x == lpPoints[i - 1].x) && (lpPoints[0].y == lpPoints[i - 1].y); + bool polygon = false; + bool rectangle = false; + bool ellipse = false; + + if (moves == 1 && moves + lines == nodes && closed) { + polygon = true; + // if (nodes==5) { // disable due to LP Bug 407394 + // if (lpPoints[0].x == lpPoints[3].x && lpPoints[1].x == lpPoints[2].x && + // lpPoints[0].y == lpPoints[1].y && lpPoints[2].y == lpPoints[3].y) + // { + // rectangle = true; + // } + // } + } else if (moves == 1 && nodes == 5 && moves + curves == nodes && closed) { + // if (lpPoints[0].x == lpPoints[1].x && lpPoints[1].x == lpPoints[11].x && + // lpPoints[5].x == lpPoints[6].x && lpPoints[6].x == lpPoints[7].x && + // lpPoints[2].x == lpPoints[10].x && lpPoints[3].x == lpPoints[9].x && lpPoints[4].x == lpPoints[8].x && + // lpPoints[2].y == lpPoints[3].y && lpPoints[3].y == lpPoints[4].y && + // lpPoints[8].y == lpPoints[9].y && lpPoints[9].y == lpPoints[10].y && + // lpPoints[5].y == lpPoints[1].y && lpPoints[6].y == lpPoints[0].y && lpPoints[7].y == lpPoints[11].y) + // { // disable due to LP Bug 407394 + // ellipse = true; + // } + } + + if (polygon || ellipse) { + + if (use_fill && !use_stroke) { // only fill + rec = selectobject_set(U_NULL_PEN, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set pen"); + } + } else if (!use_fill && use_stroke) { // only stroke + rec = selectobject_set(U_NULL_BRUSH, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set brush"); + } + } + + if (polygon) { + if (rectangle) { + U_RECTL rcl = rectl_set((U_POINTL) { + lpPoints[0].x, lpPoints[0].y + }, (U_POINTL) { + lpPoints[2].x, lpPoints[2].y + }); + rec = U_EMRRECTANGLE_set(rcl); + } else { + rec = U_EMRPOLYGON_set(U_RCL_DEF, nodes, lpPoints); + } + } else if (ellipse) { + U_RECTL rcl = rectl_set((U_POINTL) { + lpPoints[6].x, lpPoints[3].y + }, (U_POINTL) { + lpPoints[0].x, lpPoints[9].y + }); + rec = U_EMRELLIPSE_set(rcl); + } + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at retangle/ellipse/polygon"); + } + + done = true; + + // replace the handle we moved above, assuming there was something set already + if (use_fill && !use_stroke && hpen) { // only fill + rec = selectobject_set(hpen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set pen"); + } + } else if (!use_fill && use_stroke && hbrush) { // only stroke + rec = selectobject_set(hbrush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set brush"); + } + } + } + + delete[] lpPoints; + + return done; +} + +/** Some parts based on win32.cpp by Lauris Kaplinski . Was a part of Inkscape + in the past (or will be in the future?) Not in current trunk. (4/19/2012) + + Limitations of this code: + 1. Transparency is lost on export. (Apparently a limitation of the EMF format.) + 2. Probably messes up if row stride != w*4 + 3. There is still a small memory leak somewhere, possibly in a pixbuf created in a routine + that calls this one and passes px, but never removes the rest of the pixbuf. The first time + this is called it leaked 5M (in one test) and each subsequent call leaked around 200K more. + If this routine is reduced to + if(1)return(0); + and called for a single 1280 x 1024 image then the program leaks 11M per call, or roughly the + size of two bitmaps. +*/ + +unsigned int PrintEmf::image( + Inkscape::Extension::Print * /* module */, /** not used */ + unsigned char *rgba_px, /** array of pixel values, Gdk::Pixbuf bitmap format */ + unsigned int w, /** width of bitmap */ + unsigned int h, /** height of bitmap */ + unsigned int rs, /** row stride (normally w*4) */ + Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other transforms, use the one from m_tr_stack */ + SPStyle const *style) /** provides indirect link to image object */ +{ + double x1, y1, dw, dh; + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + do_clip_if_present(style); // If clipping is needed set it up + + rec = U_EMRSETSTRETCHBLTMODE_set(U_COLORONCOLOR); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRHEADER"); + } + + x1 = tf_rect[4]; + y1 = tf_rect[5]; + dw = ((double) w) * tf_rect[0]; + dh = ((double) h) * tf_rect[3]; + Geom::Point pLL(x1, y1); + Geom::Point pLL2 = pLL * tf; //location of LL corner in Inkscape coordinates + + char *px; + uint32_t cbPx; + uint32_t colortype; + PU_RGBQUAD ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi; + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, (char *) rgba_px, w, h, w * 4, colortype, 0, 1); + Bmih = bitmapinfoheader_set(w, h, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + + U_POINTL Dest = pointl_set(round(pLL2[Geom::X] * PX2WORLD), round(pLL2[Geom::Y] * PX2WORLD)); + U_POINTL cDest = pointl_set(round(dw * PX2WORLD), round(dh * PX2WORLD)); + U_POINTL Src = pointl_set(0, 0); + U_POINTL cSrc = pointl_set(w, h); + /* map the integer Dest coordinates back into pLL2, so that the rounded part does not destabilize the transform offset below */ + pLL2[Geom::X] = Dest.x; + pLL2[Geom::Y] = Dest.y; + pLL2 /= PX2WORLD; + if (!FixImageRot) { /* Rotate images - some programs cannot read them in correctly if they are rotated */ + tf[4] = tf[5] = 0.0; // get rid of the offset in the transform + Geom::Point pLL2prime = pLL2 * tf; + U_XFORM tmpTransform; + tmpTransform.eM11 = tf[0]; + tmpTransform.eM12 = tf[1]; + tmpTransform.eM21 = tf[2]; + tmpTransform.eM22 = tf[3]; + tmpTransform.eDx = (pLL2[Geom::X] - pLL2prime[Geom::X]) * PX2WORLD; + tmpTransform.eDy = (pLL2[Geom::Y] - pLL2prime[Geom::Y]) * PX2WORLD; + + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + + rec = U_EMRMODIFYWORLDTRANSFORM_set(tmpTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRMODIFYWORLDTRANSFORM"); + } + } + rec = U_EMRSTRETCHDIBITS_set( + U_RCL_DEF, //! Bounding rectangle in device units + Dest, //! Destination UL corner in logical units + cDest, //! Destination W & H in logical units + Src, //! Source UL corner in logical units + cSrc, //! Source W & H in logical units + U_DIB_RGB_COLORS, //! DIBColors Enumeration + U_SRCCOPY, //! RasterOPeration Enumeration + Bmi, //! (Optional) bitmapbuffer (U_BITMAPINFO section) + h * rs, //! size in bytes of px + px //! (Optional) bitmapbuffer (U_BITMAPINFO section) + ); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSTRETCHDIBITS_set"); + } + free(px); + free(Bmi); + if (numCt) { + free(ct); + } + + if (!FixImageRot) { + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRRESTOREDC_set"); + } + } + + return 0; +} + +unsigned int PrintEmf::draw_pathv_to_EMF(Geom::PathVector const &pathv, const Geom::Affine &transform) { + char *rec; + + /* inkscape to EMF scaling is done below, but NOT the rotation/translation transform, + that is handled by the EMF MODIFYWORLDTRANSFORM record + */ + + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * transform); + + rec = U_EMRBEGINPATH_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRBEGINPATH_set"); + } + + /** + * For all Subpaths in the + */ + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + U_POINTL ptl = pointl_set((int32_t) round(p0[X]), (int32_t) round(p0[Y])); + rec = U_EMRMOVETOEX_set(ptl); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRMOVETOEX_set"); + } + + /** + * For all segments in the subpath + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + + ptl = pointl_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + rec = U_EMRLINETO_set(ptl); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRLINETO_set"); + } + } else if (Geom::CubicBezier const *cubic = dynamic_cast(&*cit)) { + std::vector points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + U_POINTL pt[3]; + pt[0].x = x1; + pt[0].y = y1; + pt[1].x = x2; + pt[1].y = y2; + pt[2].x = x3; + pt[2].y = y3; + + rec = U_EMRPOLYBEZIERTO_set(U_RCL_DEF, 3, pt); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRPOLYBEZIERTO_set"); + } + } else { + g_warning("logical error, because pathv_to_linear_and_cubic_beziers was used"); + } + } + + if (pit.end_default() == pit.end_closed()) { // there may be multiples of this on a single path + rec = U_EMRCLOSEFIGURE_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRCLOSEFIGURE_set"); + } + } + + } + + rec = U_EMRENDPATH_set(); // there may be only be one of these on a single path + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRENDPATH_set"); + } + return(0); +} + +// may also be called with a simple_shape or an empty path, whereupon it just returns without doing anything +unsigned int PrintEmf::print_pathv(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + Geom::Affine tf = transform; + char *rec = nullptr; + + simple_shape = print_simple_shape(pathv, tf); + if (simple_shape || pathv.empty()) { + if (use_fill) { + destroy_brush(); // these must be cleared even if nothing is drawn or hbrush,hpen fill up + } + if (use_stroke) { + destroy_pen(); + } + return TRUE; + } + + (void) draw_pathv_to_EMF(pathv, tf); + + // explicit FILL/STROKE commands are needed for each sub section of the path + if (use_fill && !use_stroke) { + rec = U_EMRFILLPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRFILLPATH_set"); + } + } else if (use_fill && use_stroke) { + rec = U_EMRSTROKEANDFILLPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSTROKEANDFILLPATH_set"); + } + } else if (!use_fill && use_stroke) { + rec = U_EMRSTROKEPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSTROKEPATH_set"); + } + } + + // clean out brush and pen, but only after all parts of the draw complete + if (use_fill) { + destroy_brush(); + } + if (use_stroke) { + destroy_pen(); + } + + return TRUE; +} + + +unsigned int PrintEmf::text(Inkscape::Extension::Print * /*mod*/, char const *text, Geom::Point const &p, + SPStyle const *const style) +{ + if (!et || !text) { + return 0; + } + + do_clip_if_present(style); // If clipping is needed set it up + char *rec = nullptr; + int ccount, newfont; + int fix90n = 0; + uint32_t hfont = 0; + Geom::Affine tf = m_tr_stack.top(); + double rot = -1800.0 * std::atan2(tf[1], tf[0]) / M_PI; // 0.1 degree rotation, - sign for MM_TEXT + double rotb = -std::atan2(tf[1], tf[0]); // rotation for baseline offset for superscript/subscript, used below + double dx, dy; + double ky; + + // the dx array is smuggled in like: textw1 w2 w3 ...wn, where the widths are floats 7 characters wide, including the space + int ndx, rtl; + uint32_t *adx; + smuggle_adxkyrtl_out(text, &adx, &ky, &rtl, &ndx, PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); // side effect: free() adx + + uint32_t textalignment; + if (rtl > 0) { + textalignment = U_TA_BASELINE | U_TA_LEFT; + } else { + textalignment = U_TA_BASELINE | U_TA_RIGHT | U_TA_RTLREADING; + } + if (textalignment != htextalignment) { + htextalignment = textalignment; + rec = U_EMRSETTEXTALIGN_set(textalignment); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMRSETTEXTALIGN_set"); + } + } + + char *text2 = strdup(text); // because U_Utf8ToUtf16le calls iconv which does not like a const char * + uint16_t *unicode_text = U_Utf8ToUtf16le(text2, 0, nullptr); + free(text2); + //translates Unicode to NonUnicode, if possible. If any translate, all will, and all to + //the same font, because of code in Layout::print + UnicodeToNon(unicode_text, &ccount, &newfont); + + //PPT gets funky with text within +-1 degree of a multiple of 90, but only for SOME fonts.Snap those to the central value + //Some funky ones: Arial, Times New Roman + //Some not funky ones: Symbol and Verdana. + //Without a huge table we cannot catch them all, so just the most common problem ones. + FontfixParams params; + + if (FixPPTCharPos) { + switch (newfont) { + case CVTSYM: + _lookup_ppt_fontfix("Convert To Symbol", params); + break; + case CVTZDG: + _lookup_ppt_fontfix("Convert To Zapf Dingbats", params); + break; + case CVTWDG: + _lookup_ppt_fontfix("Convert To Wingdings", params); + break; + default: //also CVTNON + _lookup_ppt_fontfix(style->font_family.value(), params); + break; + } + if (params.f2 != 0 || params.f3 != 0) { + int irem = ((int) round(rot)) % 900 ; + if (irem <= 9 && irem >= -9) { + fix90n = 1; //assume vertical + rot = (double)(((int) round(rot)) - irem); + rotb = rot * M_PI / 1800.0; + if (std::abs(rot) == 900.0) { + fix90n = 2; + } + } + } + } + + /* Note that text font sizes are stored into the EMF as fairly small integers and that limits their precision. + The EMF output files produced here have been designed so that the integer valued pt sizes + land right on an integer value in the EMF file, so those are exact. However, something like 18.1 pt will be + somewhat off, so that when it is read back in it becomes 18.11 pt. (For instance.) + */ + int textheight = round(-style->font_size.computed * PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); + + if (!hfont) { + // Get font face name. Use changed font name if unicode mapped to one + // of the special fonts. + uint16_t *wfacename; + if (!newfont) { + wfacename = U_Utf8ToUtf16le(style->font_family.value(), 0, nullptr); + } else { + wfacename = U_Utf8ToUtf16le(FontName(newfont), 0, nullptr); + } + + // Scale the text to the minimum stretch. (It tends to stay within bounding rectangles even if + // it was streteched asymmetrically.) Few applications support text from EMF which is scaled + // differently by height/width, so leave lfWidth alone. + + U_LOGFONT lf = logfont_set( + textheight, + 0, + round(rot), + round(rot), + _translate_weight(style->font_weight.computed), + (style->font_style.computed == SP_CSS_FONT_STYLE_ITALIC), + style->text_decoration_line.underline, + style->text_decoration_line.line_through, + U_DEFAULT_CHARSET, + U_OUT_DEFAULT_PRECIS, + U_CLIP_DEFAULT_PRECIS, + U_DEFAULT_QUALITY, + U_DEFAULT_PITCH | U_FF_DONTCARE, + wfacename); + free(wfacename); + + rec = extcreatefontindirectw_set(&hfont, eht, (char *) &lf, nullptr); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at extcreatefontindirectw_set"); + } + } + + rec = selectobject_set(hfont, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at selectobject_set"); + } + + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + // only change the text color when it needs to be changed + if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { + memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); + rec = U_EMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMRSETTEXTCOLOR_set"); + } + } + + Geom::Point p2 = p * tf; + + //Handle super/subscripts and vertical kerning + /* Previously used this, but vertical kerning was not supported + p2[Geom::X] -= style->baseline_shift.computed * std::sin( rotb ); + p2[Geom::Y] -= style->baseline_shift.computed * std::cos( rotb ); + */ + p2[Geom::X] += ky * std::sin(rotb); + p2[Geom::Y] += ky * std::cos(rotb); + + //Conditionally handle compensation for PPT EMF import bug (affects PPT 2003-2010, at least) + if (FixPPTCharPos) { + if (fix90n == 1) { //vertical + dx = 0.0; + dy = params.f3 * style->font_size.computed * std::cos(rotb); + } else if (fix90n == 2) { //horizontal + dx = params.f2 * style->font_size.computed * std::sin(rotb); + dy = 0.0; + } else { + dx = params.f1 * style->font_size.computed * std::sin(rotb); + dy = params.f1 * style->font_size.computed * std::cos(rotb); + } + p2[Geom::X] += dx; + p2[Geom::Y] += dy; + } + + p2[Geom::X] = (p2[Geom::X] * PX2WORLD); + p2[Geom::Y] = (p2[Geom::Y] * PX2WORLD); + + int32_t const xpos = (int32_t) round(p2[Geom::X]); + int32_t const ypos = (int32_t) round(p2[Geom::Y]); + + + // The number of characters in the string is a bit fuzzy. ndx, the number of entries in adx is + // the number of VISIBLE characters, since some may combine from the UTF (8 originally, + // now 16) encoding. Conversely strlen() or wchar16len() would give the absolute number of + // encoding characters. Unclear if emrtext wants the former or the latter but for now assume the former. + + // This is currently being smuggled in from caller as part of text, works + // MUCH better than the fallback hack below + // uint32_t *adx = dx_set(textheight, U_FW_NORMAL, slen); // dx is needed, this makes one up + char *rec2; + if (rtl > 0) { + rec2 = emrtext_set((U_POINTL) { + xpos, ypos + }, ndx, 2, unicode_text, U_ETO_NONE, U_RCL_DEF, adx); + } else { // RTL text, U_TA_RTLREADING should be enough, but set this one too just in case + rec2 = emrtext_set((U_POINTL) { + xpos, ypos + }, ndx, 2, unicode_text, U_ETO_RTLREADING, U_RCL_DEF, adx); + } + free(unicode_text); + free(adx); + rec = U_EMREXTTEXTOUTW_set(U_RCL_DEF, U_GM_COMPATIBLE, 1.0, 1.0, (PU_EMRTEXT)rec2); + free(rec2); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMREXTTEXTOUTW_set"); + } + + // Must deselect an object before deleting it. Put the default font (back) in. + rec = selectobject_set(U_DEVICE_DEFAULT_FONT, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at selectobject_set"); + } + + if (hfont) { + rec = deleteobject_set(&hfont, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at deleteobject_set"); + } + } + + return 0; +} + +void PrintEmf::init() +{ + /* EMF print */ + Inkscape::Extension::build_from_mem( + "\n" + "Enhanced Metafile Print\n" + "org.inkscape.print.emf\n" + "\n" + "true\n" + "true\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "\n" + "", new PrintEmf()); + + return; +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-print.h b/src/extension/internal/emf-print.h new file mode 100644 index 0000000..e48e332 --- /dev/null +++ b/src/extension/internal/emf-print.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile printing - implementation + */ +/* Authors: + * Ulf Erikson + * David Mathog + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_EMF_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_EMF_PRINT_H + +#include <3rdparty/libuemf/uemf.h> +#include "extension/internal/metafile-print.h" + +#include "splivarot.h" // pieces for union on shapes +#include "display/canvas-bpath.h" // for SPWindRule + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintEmf : public PrintMetafile +{ + uint32_t hbrush, hbrushOld, hpen; + + unsigned int print_pathv (Geom::PathVector const &pathv, const Geom::Affine &transform); + bool print_simple_shape (Geom::PathVector const &pathv, const Geom::Affine &transform); + +public: + PrintEmf(); + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int fill (Inkscape::Extension::Print *module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print * module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int image(Inkscape::Extension::Print *module, + unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style) override; + unsigned int comment(Inkscape::Extension::Print *module, const char * comment) override; + unsigned int text(Inkscape::Extension::Print *module, char const *text, + Geom::Point const &p, SPStyle const *style) override; + + static void init (); +protected: + static void smuggle_adxkyrtl_out(const char *string, uint32_t **adx, double *ky, int *rtl, int *ndx, float scale); + + void do_clip_if_present(SPStyle const *style); + Geom::PathVector merge_PathVector_with_group(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform); + Geom::PathVector merge_PathVector_with_shape(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform); + unsigned int draw_pathv_to_EMF(Geom::PathVector const &pathv, const Geom::Affine &transform); + Geom::Path pathv_to_simple_polygon(Geom::PathVector const &pathv, int *vertices); + Geom::Path pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, double *angle); + Geom::Point get_pathrect_corner(Geom::Path pathRect, double angle, int corner); + U_TRIVERTEX make_trivertex(Geom::Point Pt, U_COLORREF uc); + int vector_rect_alignment(double angle, Geom::Point vtest); + int create_brush(SPStyle const *style, PU_COLORREF fcolor) override; + void destroy_brush() override; + int create_pen(SPStyle const *style, const Geom::Affine &transform) override; + void destroy_pen() override; +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_EMF_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/filter/BUILD_YOUR_OWN b/src/extension/internal/filter/BUILD_YOUR_OWN new file mode 100644 index 0000000..ba6ca5a --- /dev/null +++ b/src/extension/internal/filter/BUILD_YOUR_OWN @@ -0,0 +1,2 @@ +This directory contains filter effects. They're designed to be simle. +Very, very simple. Here is how to build your own. diff --git a/src/extension/internal/filter/bevels.h b/src/extension/internal/filter/bevels.h new file mode 100644 index 0000000..2fce96c --- /dev/null +++ b/src/extension/internal/filter/bevels.h @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ +/* Change the 'BEVELS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Bevel filters + * Diffuse light + * Matte jelly + * Specular light + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Diffuse light filter. + + Basic diffuse bevel to use for building textures + + Filter's parameters: + * Smoothness (0.->10., default 6.) -> blur (stdDeviation) + * Elevation (0->360, default 25) -> feDistantLight (elevation) + * Azimuth (0->360, default 235) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> diffuse (lighting-color) +*/ + +class DiffuseLight : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + DiffuseLight ( ) : Filter() { }; + ~DiffuseLight ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Diffuse Light") "\n" + "org.inkscape.effect.filter.DiffuseLight\n" + "6\n" + "25\n" + "235\n" + "-1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Basic diffuse bevel to use for building textures") "\n" + "\n" + "\n", new DiffuseLight()); + }; + +}; + +gchar const * +DiffuseLight::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", smooth.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* DiffuseLight filter */ + +/** + \brief Custom predefined Matte jelly filter. + + Bulging, matte jelly covering + + Filter's parameters: + * Smoothness (0.0->10., default 7.) -> blur (stdDeviation) + * Brightness (0.0->5., default .9) -> specular (specularConstant) + * Elevation (0->360, default 60) -> feDistantLight (elevation) + * Azimuth (0->360, default 225) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> specular (lighting-color) +*/ + +class MatteJelly : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + MatteJelly ( ) : Filter() { }; + ~MatteJelly ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Matte Jelly") "\n" + "org.inkscape.effect.filter.MatteJelly\n" + "7\n" + "0.9\n" + "60\n" + "225\n" + "-1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Bulging, matte jelly covering") "\n" + "\n" + "\n", new MatteJelly()); + }; + +}; + +gchar const * +MatteJelly::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream bright; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + bright << ext->get_param_float("bright"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* MatteJelly filter */ + +/** + \brief Custom predefined Specular light filter. + + Basic specular bevel to use for building textures + + Filter's parameters: + * Smoothness (0.0->10., default 6.) -> blur (stdDeviation) + * Brightness (0.0->5., default 1.) -> specular (specularConstant) + * Elevation (0->360, default 45) -> feDistantLight (elevation) + * Azimuth (0->360, default 235) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> specular (lighting-color) +*/ + +class SpecularLight : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + SpecularLight ( ) : Filter() { }; + ~SpecularLight ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Specular Light") "\n" + "org.inkscape.effect.filter.SpecularLight\n" + "6\n" + "1\n" + "45\n" + "235\n" + "-1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Basic specular bevel to use for building textures") "\n" + "\n" + "\n", new SpecularLight()); + }; + +}; + +gchar const * +SpecularLight::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream bright; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + bright << ext->get_param_float("bright"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* SpecularLight filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BEVELS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ */ diff --git a/src/extension/internal/filter/blurs.h b/src/extension/internal/filter/blurs.h new file mode 100644 index 0000000..15b9c49 --- /dev/null +++ b/src/extension/internal/filter/blurs.h @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ +/* Change the 'BLURS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Blur filters + * Blur + * Clean edges + * Cross blur + * Feather + * Out of focus + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Blur filter. + + Simple horizontal and vertical blur + + Filter's parameters: + * Horizontal blur (0.01->100., default 2) -> blur (stdDeviation) + * Vertical blur (0.01->100., default 2) -> blur (stdDeviation) + * Blur content only (boolean, default false) -> +*/ + +class Blur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Blur ( ) : Filter() { }; + ~Blur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Blur") "\n" + "org.inkscape.effect.filter.Blur\n" + "2\n" + "2\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Simple vertical and horizontal blur effect") "\n" + "\n" + "\n", new Blur()); + }; + +}; + +gchar const * +Blur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream bbox; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream content; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + + if (ext->get_param_bool("content")) { + bbox << "height=\"1\" width=\"1\" y=\"0\" x=\"0\""; + content << "\n" + << "\n"; + } else { + bbox << "" ; + content << "" ; + } + + + _filter = g_strdup_printf( + "\n" + "\n" + "%s" + "\n", bbox.str().c_str(), hblur.str().c_str(), vblur.str().c_str(), content.str().c_str() ); + + return _filter; +}; /* Blur filter */ + +/** + \brief Custom predefined Clean edges filter. + + Removes or decreases glows and jaggeries around objects edges after applying some filters + + Filter's parameters: + * Strength (0.01->2., default 0.4) -> blur (stdDeviation) +*/ + +class CleanEdges : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CleanEdges ( ) : Filter() { }; + ~CleanEdges ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Clean Edges") "\n" + "org.inkscape.effect.filter.CleanEdges\n" + "0.4\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Removes or decreases glows and jaggeries around objects edges after applying some filters") "\n" + "\n" + "\n", new CleanEdges()); + }; + +}; + +gchar const * +CleanEdges::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + + blur << ext->get_param_float("blur"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n", blur.str().c_str()); + + return _filter; +}; /* CleanEdges filter */ + +/** + \brief Custom predefined Cross blur filter. + + Combine vertical and horizontal blur + + Filter's parameters: + * Brightness (0.->10., default 0) -> composite (k3) + * Fading (0.->1., default 0) -> composite (k4) + * Horizontal blur (0.01->20., default 5) -> blur (stdDeviation) + * Vertical blur (0.01->20., default 5) -> blur (stdDeviation) + * Blend mode (enum, default Darken) -> blend (mode) +*/ + +class CrossBlur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CrossBlur ( ) : Filter() { }; + ~CrossBlur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Cross Blur") "\n" + "org.inkscape.effect.filter.CrossBlur\n" + "0\n" + "0\n" + "5\n" + "5\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Combine vertical and horizontal blur") "\n" + "\n" + "\n", new CrossBlur()); + }; + +}; + +gchar const * +CrossBlur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream bright; + std::ostringstream fade; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream blend; + + bright << ext->get_param_float("bright"); + fade << ext->get_param_float("fade"); + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + blend << ext->get_param_optiongroup("blend"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", bright.str().c_str(), fade.str().c_str(), hblur.str().c_str(), vblur.str().c_str(), blend.str().c_str()); + + return _filter; +}; /* Cross blur filter */ + +/** + \brief Custom predefined Feather filter. + + Blurred mask on the edge without altering the contents + + Filter's parameters: + * Strength (0.01->100., default 5) -> blur (stdDeviation) +*/ + +class Feather : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Feather ( ) : Filter() { }; + ~Feather ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Feather") "\n" + "org.inkscape.effect.filter.Feather\n" + "5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Blurred mask on the edge without altering the contents") "\n" + "\n" + "\n", new Feather()); + }; + +}; + +gchar const * +Feather::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + + blur << ext->get_param_float("blur"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", blur.str().c_str()); + + return _filter; +}; /* Feather filter */ + +/** + \brief Custom predefined Out of Focus filter. + + Blur eroded by white or transparency + + Filter's parameters: + * Horizontal blur (0.01->10., default 3) -> blur (stdDeviation) + * Vertical blur (0.01->10., default 3) -> blur (stdDeviation) + * Dilatation (n-1th value, 0.->100., default 6) -> colormatrix2 (matrix) + * Erosion (nth value, 0.->100., default 2) -> colormatrix2 (matrix) + * Opacity (0.->1., default 1.) -> composite1 (k2) + * Background color (guint, default -1) -> flood (flood-opacity, flood-color) + * Blend type (enum, default normal) -> blend (mode) + * Blend to background (boolean, default false) -> blend (false: in2="flood", true: in2="BackgroundImage") + +*/ + +class ImageBlur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ImageBlur ( ) : Filter() { }; + ~ImageBlur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Out of Focus") "\n" + "org.inkscape.effect.filter.ImageBlur\n" + "\n" + "\n" + "3\n" + "3\n" + "6\n" + "2\n" + "1\n" + "\n" + "\n" + "-1\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "false\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Blur eroded by white or transparency") "\n" + "\n" + "\n", new ImageBlur()); + }; + +}; + +gchar const * +ImageBlur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream opacity; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream blend; + std::ostringstream background; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + dilat << ext->get_param_float("dilat"); + erosion << -ext->get_param_float("erosion"); + opacity << ext->get_param_float("opacity"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + blend << ext->get_param_optiongroup("blend"); + + if (ext->get_param_bool("background")) { + background << "BackgroundImage" ; + } else { + background << "flood" ; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + hblur.str().c_str(), vblur.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), + background.str().c_str(), blend.str().c_str(), opacity.str().c_str()); + + return _filter; +}; /* Out of Focus filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BLURS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ */ diff --git a/src/extension/internal/filter/bumps.h b/src/extension/internal/filter/bumps.h new file mode 100644 index 0000000..21328f4 --- /dev/null +++ b/src/extension/internal/filter/bumps.h @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ +/* Change the 'BUMPS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Bump filters + * Bump + * Wax bump + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Bump filter. + + All purpose bump filter + + Filter's parameters: + Options + * Image simplification (0.01->10., default 0.01) -> blur1 (stdDeviation) + * Bump simplification (0.01->10., default 0.01) -> blur2 (stdDeviation) + * Crop (-50.->50., default 0.) -> composite1 (k3) + * Red (-50.->50., default 0.) -> colormatrix1 (values) + * Green (-50.->50., default 0.) -> colormatrix1 (values) + * Blue (-50.->50., default 0.) -> colormatrix1 (values) + * Bump from background (boolean, default false) -> colormatrix1 (false: in="SourceGraphic", true: in="BackgroundImage") + Lighting + * Lighting type (enum, default specular) -> lighting block + * Height (0.->50., default 5.) -> lighting (surfaceScale) + * Lightness (0.->5., default 1.) -> lighting [diffuselighting (diffuseConstant)|specularlighting (specularConstant)] + * Precision (1->128, default 15) -> lighting (specularExponent) + * Color (guint, default -1 (RGB:255,255,255))-> lighting (lighting-color) + Light source + * Azimuth (0->360, default 225) -> lightsOptions (distantAzimuth) + * Elevation (0->180, default 45) -> lightsOptions (distantElevation) + * X location [point] (-5000->5000, default 526) -> lightsOptions (x) + * Y location [point] (-5000->5000, default 372) -> lightsOptions (y) + * Z location [point] (0->5000, default 150) -> lightsOptions (z) + * X location [spot] (-5000->5000, default 526) -> lightsOptions (x) + * Y location [spot] (-5000->5000, default 372) -> lightsOptions (y) + * Z location [spot] (-5000->5000, default 150) -> lightsOptions (z) + * X target (-5000->5000, default 0) -> lightsOptions (pointsAtX) + * Y target (-5000->5000, default 0) -> lightsOptions (pointsAtX) + * Z target (-5000->0, default -1000) -> lightsOptions (pointsAtX) + * Specular exponent (1->100, default 1) -> lightsOptions (specularExponent) + * Cone angle (0->100, default 50) -> lightsOptions (limitingConeAngle) + Color bump + * Blend type (enum, default normal) -> blend (mode) + * Image color (guint, default -987158017 (RGB:197,41,41)) -> flood (flood-color) + * Color bump (boolean, default false) -> composite2 (false: in="diffuselighting", true in="flood") +*/ + +class Bump : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Bump ( ) : Filter() { }; + ~Bump ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Bump") "\n" + "org.inkscape.effect.filter.Bump\n" + "\n" + "\n" + "0.01\n" + "0.01\n" + "0\n" + "\n" + "0\n" + "0\n" + "0\n" + "false\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "5\n" + "1\n" + "15\n" + "-1\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "225\n" + "45\n" + "\n" + "526\n" + "372\n" + "150\n" + "\n" + "526\n" + "372\n" + "150\n" + "0\n" + "0\n" + "-1000\n" + "1\n" + "50\n" + "\n" + "\n" + "-987158017\n" + "false\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("All purposes bump filter") "\n" + "\n" + "\n", new Bump()); + }; + +}; + +gchar const * +Bump::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simplifyImage; + std::ostringstream simplifyBump; + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream crop; + std::ostringstream bumpSource; + std::ostringstream blend; + + std::ostringstream lightStart; + std::ostringstream lightOptions; + std::ostringstream lightEnd; + + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + std::ostringstream colorize; + + + simplifyImage << ext->get_param_float("simplifyImage"); + simplifyBump << ext->get_param_float("simplifyBump"); + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + crop << ext->get_param_float("crop"); + blend << ext->get_param_optiongroup("blend"); + + guint32 lightingColor = ext->get_param_color("lightingColor"); + guint32 imageColor = ext->get_param_color("imageColor"); + + if (ext->get_param_bool("background")) { + bumpSource << "BackgroundImage" ; + } else { + bumpSource << "blur1" ; + } + + const gchar *lightType = ext->get_param_optiongroup("lightType"); + if ((g_ascii_strcasecmp("specular", lightType) == 0)) { + // Specular + lightStart << "> 24) & 0xff) << "," + << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + << ext->get_param_float("height") << "\" specularConstant=\"" << ext->get_param_float("lightness") + << "\" specularExponent=\"" << ext->get_param_int("precision") << "\" result=\"lighting\">"; + lightEnd << ""; + } else { + // Diffuse + lightStart << "> 24) & 0xff) << "," + << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + << ext->get_param_float("height") << "\" diffuseConstant=\"" << ext->get_param_float("lightness") + << "\" result=\"lighting\">"; + lightEnd << ""; + } + + const gchar *lightSource = ext->get_param_optiongroup("lightSource"); + if ((g_ascii_strcasecmp("distant", lightSource) == 0)) { + // Distant + lightOptions << "get_param_int("distantAzimuth") << "\" elevation=\"" + << ext->get_param_int("distantElevation") << "\" />"; + } else if ((g_ascii_strcasecmp("point", lightSource) == 0)) { + // Point + lightOptions << "get_param_int("pointX") << "\" y=\"" << ext->get_param_int("pointY") + << "\" x=\"" << ext->get_param_int("pointZ") << "\" />"; + } else { + // Spot + lightOptions << "get_param_int("pointX") << "\" y=\"" << ext->get_param_int("pointY") + << "\" z=\"" << ext->get_param_int("pointZ") << "\" pointsAtX=\"" << ext->get_param_int("spotAtX") + << "\" pointsAtY=\"" << ext->get_param_int("spotAtY") << "\" pointsAtZ=\"" << ext->get_param_int("spotAtZ") + << "\" specularExponent=\"" << ext->get_param_int("spotExponent") + << "\" limitingConeAngle=\"" << ext->get_param_int("spotConeAngle") + << "\" />"; + } + + floodRed << ((imageColor >> 24) & 0xff); + floodGreen << ((imageColor >> 16) & 0xff); + floodBlue << ((imageColor >> 8) & 0xff); + floodAlpha << (imageColor & 0xff) / 255.0F; + + if (ext->get_param_bool("colorize")) { + colorize << "flood" ; + } else { + colorize << "blur1" ; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "%s\n" + "%s\n" + "%s\n" + "\n" + "\n" + "\n" + "\n" + "\n", simplifyImage.str().c_str(), bumpSource.str().c_str(), red.str().c_str(), green.str().c_str(), blue.str().c_str(), + crop.str().c_str(), simplifyBump.str().c_str(), + lightStart.str().c_str(), lightOptions.str().c_str(), lightEnd.str().c_str(), + floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + colorize.str().c_str(), blend.str().c_str()); + + return _filter; + +}; /* Bump filter */ + +/** + \brief Custom predefined Wax Bump filter. + + Turns an image to jelly + + Filter's parameters: + Options + * Image simplification (0.01->10., default 1.5) -> blur1 (stdDeviation) + * Bump simplification (0.01->10., default 1) -> blur2 (stdDeviation) + * Crop (-10.->10., default 1.) -> colormatrix2 (4th value of the last line) + * Red (-10.->10., default 0.) -> colormatrix2 (values, substract 0.21) + * Green (-10.->10., default 0.) -> colormatrix2 (values, substract 0.72) + * Blue (-10.->10., default 0.) -> colormatrix2 (values, substract 0.07) + * Background (enum, default color) -> + * color: colormatrix1 (in="flood1") + * image: colormatrix1 (in="SourceGraphic") + * blurred image: colormatrix1 (in="blur1") + * Background opacity (0.->1., default 0) -> colormatrix1 (last value) + Lighting (specular, distant light) + * Color (guint, default -1 (RGB:255,255,255))-> lighting (lighting-color) + * Height (-50.->50., default 5.) -> lighting (surfaceScale) + * Lightness (0.->10., default 1.4) -> lighting [diffuselighting (diffuseConstant)|specularlighting (specularConstant)] + * Precision (0->50, default 35) -> lighting (specularExponent) + * Azimuth (0->360, default 225) -> lightsOptions (distantAzimuth) + * Elevation (0->180, default 60) -> lightsOptions (distantElevation) + * Lighting blend (enum, default screen) -> blend1 (mode) + * Highlight blend (enum, default screen) -> blend2 (mode) + Bump + * Trasparency type (enum [in,atop], default atop) -> composite2 (operator) + * Color (guint, default -520083713 (RGB:225,0,38)) -> flood2 (flood-color) + * Revert bump (boolean, default false) -> composite1 (false: operator="out", true operator="in") +*/ + +class WaxBump : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + WaxBump ( ) : Filter() { }; + ~WaxBump ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Wax Bump") "\n" + "org.inkscape.effect.filter.WaxBump\n" + "\n" + "\n" + "1.5\n" + "1\n" + "1\n" + "\n" + "0\n" + "0\n" + "0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0\n" + "\n" + "\n" + "-1\n" + "5\n" + "1.4\n" + "35\n" + "225\n" + "60\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "-520083713\n" + "false\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Turns an image to jelly") "\n" + "\n" + "\n", new WaxBump()); + }; + +}; + +gchar const * +WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simplifyImage; + std::ostringstream simplifyBump; + std::ostringstream crop; + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + + std::ostringstream background; + std::ostringstream bgopacity; + + std::ostringstream height; + std::ostringstream lightness; + std::ostringstream precision; + std::ostringstream distantAzimuth; + std::ostringstream distantElevation; + + std::ostringstream lightRed; + std::ostringstream lightGreen; + std::ostringstream lightBlue; + + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + + std::ostringstream revert; + std::ostringstream lightingblend; + std::ostringstream highlightblend; + std::ostringstream transparency; + + simplifyImage << ext->get_param_float("simplifyImage"); + simplifyBump << ext->get_param_float("simplifyBump"); + crop << ext->get_param_float("crop"); + + red << ext->get_param_float("red") - 0.21; + green << ext->get_param_float("green") - 0.72; + blue << ext->get_param_float("blue") - 0.07; + + background << ext->get_param_optiongroup("background"); + bgopacity << ext->get_param_float("bgopacity"); + + height << ext->get_param_float("height"); + lightness << ext->get_param_float("lightness"); + precision << ext->get_param_int("precision"); + distantAzimuth << ext->get_param_int("distantAzimuth"); + distantElevation << ext->get_param_int("distantElevation"); + + guint32 lightingColor = ext->get_param_color("lightingColor"); + lightRed << ((lightingColor >> 24) & 0xff); + lightGreen << ((lightingColor >> 16) & 0xff); + lightBlue << ((lightingColor >> 8) & 0xff); + + guint32 imageColor = ext->get_param_color("imageColor"); + floodRed << ((imageColor >> 24) & 0xff); + floodGreen << ((imageColor >> 16) & 0xff); + floodBlue << ((imageColor >> 8) & 0xff); + floodAlpha << (imageColor & 0xff) / 255.0F; + + if (ext->get_param_bool("revert")) { + revert << "in" ; + } else { + revert << "out" ; + } + + lightingblend << ext->get_param_optiongroup("lightingblend"); + highlightblend << ext->get_param_optiongroup("highlightblend"); + transparency << ext->get_param_optiongroup("transparency"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", simplifyImage.str().c_str(), background.str().c_str(), bgopacity.str().c_str(), + red.str().c_str(), green.str().c_str(), blue.str().c_str(), crop.str().c_str(), + floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + revert.str().c_str(), simplifyBump.str().c_str(), + lightRed.str().c_str(), lightGreen.str().c_str(), lightBlue.str().c_str(), + lightness.str().c_str(), height.str().c_str(), precision.str().c_str(), + distantElevation.str().c_str(), distantAzimuth.str().c_str(), + lightingblend.str().c_str(), transparency.str().c_str(), highlightblend.str().c_str() ); + + return _filter; + +}; /* Wax bump filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BUMPS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ */ diff --git a/src/extension/internal/filter/color.h b/src/extension/internal/filter/color.h new file mode 100644 index 0000000..1b0b9e2 --- /dev/null +++ b/src/extension/internal/filter/color.h @@ -0,0 +1,1886 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ +/* Change the 'COLOR' above to be your file name */ + +/* + * Copyright (C) 2013-2015 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Color filters + * Brilliance + * Channel painting + * Color blindness + * Color shift + * Colorize + * Component transfer + * Duochrome + * Extract channel + * Fade to black or white + * Greyscale + * Invert + * Lighting + * Lightness-contrast + * Nudge RGB + * Nudge CMY + * Quadritone + * Simple blend + * Solarize + * Tritone + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Brilliance filter. + + Brilliance filter. + + Filter's parameters: + * Brilliance (1.->10., default 2.) -> colorMatrix (RVB entries) + * Over-saturation (0.->10., default 0.5) -> colorMatrix (6 other entries) + * Lightness (-10.->10., default 0.) -> colorMatrix (last column) + * Inverted (boolean, default false) -> colorMatrix + + Matrix: + St Vi Vi 0 Li + Vi St Vi 0 Li + Vi Vi St 0 Li + 0 0 0 1 0 +*/ +class Brilliance : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Brilliance ( ) : Filter() { }; + ~Brilliance ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Brilliance") "\n" + "org.inkscape.effect.filter.Brilliance\n" + "2\n" + "0.5\n" + "0\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Brightness filter") "\n" + "\n" + "\n", new Brilliance()); + }; +}; + +gchar const * +Brilliance::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream brightness; + std::ostringstream sat; + std::ostringstream lightness; + + if (ext->get_param_bool("invert")) { + brightness << -ext->get_param_float("brightness"); + sat << 1 + ext->get_param_float("sat"); + lightness << -ext->get_param_float("lightness"); + } else { + brightness << ext->get_param_float("brightness"); + sat << -ext->get_param_float("sat"); + lightness << ext->get_param_float("lightness"); + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", brightness.str().c_str(), sat.str().c_str(), sat.str().c_str(), + lightness.str().c_str(), sat.str().c_str(), brightness.str().c_str(), + sat.str().c_str(), lightness.str().c_str(), sat.str().c_str(), + sat.str().c_str(), brightness.str().c_str(), lightness.str().c_str() ); + + return _filter; +}; /* Brilliance filter */ + +/** + \brief Custom predefined Channel Painting filter. + + Channel Painting filter. + + Filter's parameters: + * Saturation (0.->1., default 1.) -> colormatrix1 (values) + * Red (-10.->10., default -1.) -> colormatrix2 (values) + * Green (-10.->10., default 0.5) -> colormatrix2 (values) + * Blue (-10.->10., default 0.5) -> colormatrix2 (values) + * Alpha (-10.->10., default 1.) -> colormatrix2 (values) + * Flood colors (guint, default 16777215) -> flood (flood-opacity, flood-color) + * Inverted (boolean, default false) -> composite1 (operator, true='in', false='out') + + Matrix: + 1 0 0 0 0 + 0 1 0 0 0 + 0 0 1 0 0 + R G B A 0 +*/ +class ChannelPaint : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ChannelPaint ( ) : Filter() { }; + ~ChannelPaint ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Channel Painting") "\n" + "org.inkscape.effect.filter.ChannelPaint\n" + "\n" + "\n" + "1\n" + "-1\n" + "0.5\n" + "0.5\n" + "1\n" + "false\n" + "\n" + "\n" + "16777215\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Replace RGB by any color") "\n" + "\n" + "\n", new ChannelPaint()); + }; +}; + +gchar const * +ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream saturation; + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream alpha; + std::ostringstream invert; + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + + saturation << ext->get_param_float("saturation"); + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + alpha << ext->get_param_float("alpha"); + + guint32 color = ext->get_param_color("color"); + floodRed << ((color >> 24) & 0xff); + floodGreen << ((color >> 16) & 0xff); + floodBlue << ((color >> 8) & 0xff); + floodAlpha << (color & 0xff) / 255.0F; + + if (ext->get_param_bool("invert")) { + invert << "in"; + } else { + invert << "out"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", saturation.str().c_str(), red.str().c_str(), green.str().c_str(), + blue.str().c_str(), alpha.str().c_str(), floodRed.str().c_str(), + floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + invert.str().c_str() ); + + return _filter; +}; /* Channel Painting filter */ + +/** + \brief Custom predefined Color Blindness filter. + + Color Blindness filter. + Based on https://openclipart.org/detail/22299/Color%20Blindness%20filters + + Filter's parameters: + * Blindness type (enum, default Achromatomaly) -> colormatrix +*/ +class ColorBlindness : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorBlindness ( ) : Filter() { }; + ~ColorBlindness ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Color Blindness") "\n" + "org.inkscape.effect.filter.ColorBlindness\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Simulate color blindness") "\n" + "\n" + "\n", new ColorBlindness()); + }; +}; + +gchar const * +ColorBlindness::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + type << ext->get_param_optiongroup("type"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", type.str().c_str()); + + return _filter; +}; /* Color Blindness filter */ + +/** + \brief Custom predefined Color shift filter. + + Rotate and desaturate hue + + Filter's parameters: + * Shift (0->360, default 330) -> color1 (values) + * Saturation (0.->1., default 0.6) -> color2 (values) +*/ + +class ColorShift : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorShift ( ) : Filter() { }; + ~ColorShift ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Color Shift") "\n" + "org.inkscape.effect.filter.ColorShift\n" + "330\n" + "0.6\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Rotate and desaturate hue") "\n" + "\n" + "\n", new ColorShift()); + }; + +}; + +gchar const * +ColorShift::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream shift; + std::ostringstream sat; + + shift << ext->get_param_int("shift"); + sat << ext->get_param_float("sat"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", shift.str().c_str(), sat.str().c_str() ); + + return _filter; +}; /* ColorShift filter */ + +/** + \brief Custom predefined Colorize filter. + + Blend image or object with a flood color. + + Filter's parameters: + * Harsh light (0.->10., default 0) -> composite1 (k1) + * Normal light (0.->10., default 1) -> composite2 (k2) + * Duotone (boolean, default false) -> colormatrix1 (values="0") + * Filtered greys (boolean, default false) -> colormatrix2 (values="0") + * Blend mode 1 (enum, default Multiply) -> blend1 (mode) + * Blend mode 2 (enum, default Screen) -> blend2 (mode) +*/ + +class Colorize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Colorize ( ) : Filter() { }; + ~Colorize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Colorize") "\n" + "org.inkscape.effect.filter.Colorize\n" + "\n" + "\n" + "0\n" + "1\n" + "false\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "-1639776001\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Blend image or object with a flood color") "\n" + "\n" + "\n", new Colorize()); + }; + +}; + +gchar const * +Colorize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream hlight; + std::ostringstream nlight; + std::ostringstream duotone; + std::ostringstream blend1; + std::ostringstream blend2; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + hlight << ext->get_param_float("hlight"); + nlight << ext->get_param_float("nlight"); + blend1 << ext->get_param_optiongroup("blend1"); + blend2 << ext->get_param_optiongroup("blend2"); + if (ext->get_param_bool("duotone")) { + duotone << "0"; + } else { + duotone << "1"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", hlight.str().c_str(), nlight.str().c_str(), duotone.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + blend1.str().c_str(), blend2.str().c_str() ); + + return _filter; +}; /* Colorize filter */ + +/** + \brief Custom predefined ComponentTransfer filter. + + Basic component transfer structure. + + Filter's parameters: + * Type (enum, default identity) -> component function + +*/ +class ComponentTransfer : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ComponentTransfer ( ) : Filter() { }; + ~ComponentTransfer ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Component Transfer") "\n" + "org.inkscape.effect.filter.ComponentTransfer\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Basic component transfer structure") "\n" + "\n" + "\n", new ComponentTransfer()); + }; +}; + +gchar const * +ComponentTransfer::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream CTfunction; + const gchar *type = ext->get_param_optiongroup("type"); + + if ((g_ascii_strcasecmp("identity", type) == 0)) { + CTfunction << "\n" + << "\n" + << "\n" + << "\n"; + } else if ((g_ascii_strcasecmp("table", type) == 0)) { + CTfunction << "\n" + << "\n" + << "\n"; + } else if ((g_ascii_strcasecmp("discrete", type) == 0)) { + CTfunction << "\n" + << "\n" + << "\n"; + } else if ((g_ascii_strcasecmp("linear", type) == 0)) { + CTfunction << "\n" + << "\n" + << "\n"; + } else { //Gamma + CTfunction << "\n" + << "\n" + << "\n"; + } + _filter = g_strdup_printf( + "\n" + "\n" + "%s\n" + "\n" + "\n", CTfunction.str().c_str()); + + return _filter; +}; /* ComponentTransfer filter */ + +/** + \brief Custom predefined Duochrome filter. + + Convert luminance values to a duochrome palette. + + Filter's parameters: + * Fluorescence level (0.->2., default 0) -> composite4 (k2) + * Swap (enum, default "No swap") -> composite1, composite2 (operator) + * Color 1 (guint, default 1364325887) -> flood1 (flood-opacity, flood-color) + * Color 2 (guint, default -65281) -> flood2 (flood-opacity, flood-color) +*/ + +class Duochrome : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Duochrome ( ) : Filter() { }; + ~Duochrome ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Duochrome") "\n" + "org.inkscape.effect.filter.Duochrome\n" + "\n" + "\n" + "0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "1364325887\n" + "\n" + "\n" + "-65281\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Convert luminance values to a duochrome palette") "\n" + "\n" + "\n", new Duochrome()); + }; + +}; + +gchar const * +Duochrome::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a1; + std::ostringstream r1; + std::ostringstream g1; + std::ostringstream b1; + std::ostringstream a2; + std::ostringstream r2; + std::ostringstream g2; + std::ostringstream b2; + std::ostringstream fluo; + std::ostringstream swap1; + std::ostringstream swap2; + guint32 color1 = ext->get_param_color("color1"); + guint32 color2 = ext->get_param_color("color2"); + float fluorescence = ext->get_param_float("fluo"); + const gchar *swaptype = ext->get_param_optiongroup("swap"); + + r1 << ((color1 >> 24) & 0xff); + g1 << ((color1 >> 16) & 0xff); + b1 << ((color1 >> 8) & 0xff); + r2 << ((color2 >> 24) & 0xff); + g2 << ((color2 >> 16) & 0xff); + b2 << ((color2 >> 8) & 0xff); + fluo << fluorescence; + + if ((g_ascii_strcasecmp("full", swaptype) == 0)) { + swap1 << "in"; + swap2 << "out"; + a1 << (color1 & 0xff) / 255.0F; + a2 << (color2 & 0xff) / 255.0F; + } else if ((g_ascii_strcasecmp("color", swaptype) == 0)) { + swap1 << "in"; + swap2 << "out"; + a1 << (color2 & 0xff) / 255.0F; + a2 << (color1 & 0xff) / 255.0F; + } else if ((g_ascii_strcasecmp("alpha", swaptype) == 0)) { + swap1 << "out"; + swap2 << "in"; + a1 << (color2 & 0xff) / 255.0F; + a2 << (color1 & 0xff) / 255.0F; + } else { + swap1 << "out"; + swap2 << "in"; + a1 << (color1 & 0xff) / 255.0F; + a2 << (color2 & 0xff) / 255.0F; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", a1.str().c_str(), r1.str().c_str(), g1.str().c_str(), b1.str().c_str(), swap1.str().c_str(), + a2.str().c_str(), r2.str().c_str(), g2.str().c_str(), b2.str().c_str(), swap2.str().c_str(), + fluo.str().c_str() ); + + return _filter; +}; /* Duochrome filter */ + +/** + \brief Custom predefined Extract Channel filter. + + Extract color channel as a transparent image. + + Filter's parameters: + * Channel (enum, all colors, default Red) -> colormatrix (values) + * Background blend (enum, Normal, Multiply, Screen, default Normal) -> blend (mode) + * Channel to alpha (boolean, default false) -> colormatrix (values) + +*/ +class ExtractChannel : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ExtractChannel ( ) : Filter() { }; + ~ExtractChannel ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Extract Channel") "\n" + "org.inkscape.effect.filter.ExtractChannel\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Extract color channel as a transparent image") "\n" + "\n" + "\n", new ExtractChannel()); + }; +}; + +gchar const * +ExtractChannel::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blend; + std::ostringstream colors; + + blend << ext->get_param_optiongroup("blend"); + + const gchar *channel = ext->get_param_optiongroup("source"); + if (ext->get_param_bool("alpha")) { + if ((g_ascii_strcasecmp("r", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0"; + } else if ((g_ascii_strcasecmp("g", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0"; + } else if ((g_ascii_strcasecmp("b", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0"; + } else if ((g_ascii_strcasecmp("c", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 1 0"; + } else if ((g_ascii_strcasecmp("m", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 1 0"; + } else { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 0"; + } + } else { + if ((g_ascii_strcasecmp("r", channel) == 0)) { + colors << "0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0"; + } else if ((g_ascii_strcasecmp("g", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0"; + } else if ((g_ascii_strcasecmp("b", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0"; + } else if ((g_ascii_strcasecmp("c", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 -1 0 0 1 0"; + } else if ((g_ascii_strcasecmp("m", channel) == 0)) { + colors << "0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 -1 0 1 0"; + } else { + colors << "0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 -1 1 0"; + } + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", colors.str().c_str(), blend.str().c_str() ); + + return _filter; +}; /* ExtractChannel filter */ + +/** + \brief Custom predefined Fade to Black or White filter. + + Fade to black or white. + + Filter's parameters: + * Level (0.->1., default 1.) -> colorMatrix (RVB entries) + * Fade to (enum [black|white], default black) -> colorMatrix (RVB entries) + + Matrix + black white + Lv 0 0 0 0 Lv 0 0 1-lv 0 + 0 Lv 0 0 0 0 Lv 0 1-lv 0 + 0 0 Lv 0 0 0 0 Lv 1-lv 0 + 0 0 0 1 0 0 0 0 1 0 +*/ +class FadeToBW : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + FadeToBW ( ) : Filter() { }; + ~FadeToBW ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Fade to Black or White") "\n" + "org.inkscape.effect.filter.FadeToBW\n" + "1\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Fade to black or white") "\n" + "\n" + "\n", new FadeToBW()); + }; +}; + +gchar const * +FadeToBW::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream level; + std::ostringstream wlevel; + + level << ext->get_param_float("level"); + + const gchar *fadeto = ext->get_param_optiongroup("fadeto"); + if ((g_ascii_strcasecmp("white", fadeto) == 0)) { + // White + wlevel << (1 - ext->get_param_float("level")); + } else { + // Black + wlevel << "0"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", level.str().c_str(), wlevel.str().c_str(), + level.str().c_str(), wlevel.str().c_str(), + level.str().c_str(), wlevel.str().c_str() ); + + return _filter; +}; /* Fade to black or white filter */ + +/** + \brief Custom predefined Greyscale filter. + + Customize greyscale components. + + Filter's parameters: + * Red (-10.->10., default .21) -> colorMatrix (values) + * Green (-10.->10., default .72) -> colorMatrix (values) + * Blue (-10.->10., default .072) -> colorMatrix (values) + * Lightness (-10.->10., default 0.) -> colorMatrix (values) + * Transparent (boolean, default false) -> matrix structure + + Matrix: + normal transparency + R G B St 0 0 0 0 0 0 + R G B St 0 0 0 0 0 0 + R G B St 0 0 0 0 0 0 + 0 0 0 1 0 R G B 1-St 0 +*/ +class Greyscale : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Greyscale ( ) : Filter() { }; + ~Greyscale ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Greyscale") "\n" + "org.inkscape.effect.filter.Greyscale\n" + "0.21\n" + "0.72\n" + "0.072\n" + "0\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Customize greyscale components") "\n" + "\n" + "\n", new Greyscale()); + }; +}; + +gchar const * +Greyscale::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream strength; + std::ostringstream redt; + std::ostringstream greent; + std::ostringstream bluet; + std::ostringstream strengtht; + std::ostringstream transparency; + std::ostringstream line; + + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + strength << ext->get_param_float("strength"); + + redt << - ext->get_param_float("red"); + greent << - ext->get_param_float("green"); + bluet << - ext->get_param_float("blue"); + strengtht << 1 - ext->get_param_float("strength"); + + if (ext->get_param_bool("transparent")) { + line << "0 0 0 0"; + transparency << redt.str().c_str() << " " << greent.str().c_str() << " " << bluet.str().c_str() << " " << strengtht.str().c_str(); + } else { + line << red.str().c_str() << " " << green.str().c_str() << " " << blue.str().c_str() << " " << strength.str().c_str(); + transparency << "0 0 0 1"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", line.str().c_str(), line.str().c_str(), line.str().c_str(), transparency.str().c_str() ); + return _filter; +}; /* Greyscale filter */ + +/** + \brief Custom predefined Invert filter. + + Manage hue, lightness and transparency inversions + + Filter's parameters: + * Invert hue (boolean, default false) -> color1 (values, true: 180, false: 0) + * Invert lightness (boolean, default false) -> color1 (values, true: 180, false: 0; XOR with Invert hue), + color2 (values: from a00 to a22, if 1, set -1 and set 1 in ax4, if -1, set 1 and set 0 in ax4) + * Invert transparency (boolean, default false) -> color2 (values: negate a30, a31 and a32, substract 1 from a33) + * Invert channels (enum, default Red and blue) -> color2 (values -for R&B: swap ax0 and ax2 in the first 3 lines) + * Light transparency (0.->1., default 0.) -> color2 (values: a33=a33-x) +*/ + +class Invert : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Invert ( ) : Filter() { }; + ~Invert ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Invert") "\n" + "org.inkscape.effect.filter.Invert\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0\n" + "false\n" + "false\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Manage hue, lightness and transparency inversions") "\n" + "\n" + "\n", new Invert()); + }; + +}; + +gchar const * +Invert::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream line1; + std::ostringstream line2; + std::ostringstream line3; + + std::ostringstream col5; + std::ostringstream transparency; + std::ostringstream hue; + + if (ext->get_param_bool("hue") ^ ext->get_param_bool("lightness")) { + hue << "\n"; + } else { + hue << ""; + } + + if (ext->get_param_bool("transparency")) { + transparency << "0.21 0.72 0.07 " << 1 - ext->get_param_float("opacify"); + } else { + transparency << "-0.21 -0.72 -0.07 " << 2 - ext->get_param_float("opacify"); + } + + if (ext->get_param_bool("lightness")) { + switch (atoi(ext->get_param_optiongroup("channels"))) { + case 1: + line1 << "0 0 -1"; + line2 << "0 -1 0"; + line3 << "-1 0 0"; + break; + case 2: + line1 << "0 -1 0"; + line2 << "-1 0 0"; + line3 << "0 0 -1"; + break; + case 3: + line1 << "-1 0 0"; + line2 << "0 0 -1"; + line3 << "0 -1 0"; + break; + default: + line1 << "-1 0 0"; + line2 << "0 -1 0"; + line3 << "0 0 -1"; + break; + } + col5 << "1"; + } else { + switch (atoi(ext->get_param_optiongroup("channels"))) { + case 1: + line1 << "0 0 1"; + line2 << "0 1 0"; + line3 << "1 0 0"; + break; + case 2: + line1 << "0 1 0"; + line2 << "1 0 0"; + line3 << "0 0 1"; + break; + case 3: + line1 << "1 0 0"; + line2 << "0 0 1"; + line3 << "0 1 0"; + break; + default: + line1 << "1 0 0"; + line2 << "0 1 0"; + line3 << "0 0 1"; + break; + } + col5 << "0"; + } + + _filter = g_strdup_printf( + "\n" + "%s" + "\n" + "\n", hue.str().c_str(), + line1.str().c_str(), col5.str().c_str(), + line2.str().c_str(), col5.str().c_str(), + line3.str().c_str(), col5.str().c_str(), + transparency.str().c_str() ); + + return _filter; +}; /* Invert filter */ + +/** + \brief Custom predefined Lighting filter. + + Modify lights and shadows separately. + + Filter's parameters: + * Lightness (0.->20., default 1.) -> component (amplitude) + * Shadow (0.->20., default 1.) -> component (exponent) + * Offset (-1.->1., default 0.) -> component (offset) +*/ +class Lighting : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Lighting ( ) : Filter() { }; + ~Lighting ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Lighting") "\n" + "org.inkscape.effect.filter.Lighting\n" + "1\n" + "1\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Modify lights and shadows separately") "\n" + "\n" + "\n", new Lighting()); + }; +}; + +gchar const * +Lighting::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream amplitude; + std::ostringstream exponent; + std::ostringstream offset; + + amplitude << ext->get_param_float("amplitude"); + exponent << ext->get_param_float("exponent"); + offset << ext->get_param_float("offset"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str(), + amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str(), + amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str() ); + + return _filter; +}; /* Lighting filter */ + +/** + \brief Custom predefined Lightness-Contrast filter. + + Modify lightness and contrast separately. + + Filter's parameters: + * Lightness (0.->100., default 0.) -> colorMatrix + * Contrast (0.->100., default 0.) -> colorMatrix + + Matrix: + Co/10 0 0 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 Co/10 0 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 0 Co/10 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 0 0 1 0 +*/ +class LightnessContrast : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + LightnessContrast ( ) : Filter() { }; + ~LightnessContrast ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Lightness-Contrast") "\n" + "org.inkscape.effect.filter.LightnessContrast\n" + "0\n" + "0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Modify lightness and contrast separately") "\n" + "\n" + "\n", new LightnessContrast()); + }; +}; + +gchar const * +LightnessContrast::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream lightness; + std::ostringstream contrast; + std::ostringstream contrast5; + + gfloat c5; + if (ext->get_param_float("contrast") > 0) { + contrast << (1 + ext->get_param_float("contrast") / 10); + c5 = (- ext->get_param_float("contrast") / 20); + } else { + contrast << (1 + ext->get_param_float("contrast") / 100); + c5 =(- ext->get_param_float("contrast") / 200); + } + + contrast5 << c5; + lightness << ((1 - c5) * ext->get_param_float("lightness") / 100); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str(), + contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str(), + contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str() ); + + return _filter; +}; /* Lightness-Contrast filter */ + +/** + \brief Custom predefined Nudge RGB filter. + + Nudge RGB channels separately and blend them to different types of backgrounds + + Filter's parameters: + Offsets + * Red + * x (-100.->100., default -6) -> offset1 (dx) + * y (-100.->100., default -6) -> offset1 (dy) + * Green + * x (-100.->100., default 6) -> offset2 (dx) + * y (-100.->100., default 7) -> offset2 (dy) + * Blue + * x (-100.->100., default 1) -> offset3 (dx) + * y (-100.->100., default -16) -> offset3 (dy) + Color + * Background color (guint, default 255)-> flood (flood-color, flood-opacity) + +*/ +class NudgeRGB : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NudgeRGB ( ) : Filter() { }; + ~NudgeRGB ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Nudge RGB") "\n" + "org.inkscape.effect.filter.NudgeRGB\n" + "\n" + "\n" + "\n" + "-6\n" + "-6\n" + "\n" + "6\n" + "7\n" + "\n" + "1\n" + "-16\n" + "\n" + "\n" + "255\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Nudge RGB channels separately and blend them to different types of backgrounds") "\n" + "\n" + "\n", new NudgeRGB()); + }; +}; + +gchar const * +NudgeRGB::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream rx; + std::ostringstream ry; + std::ostringstream gx; + std::ostringstream gy; + std::ostringstream bx; + std::ostringstream by; + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + + rx << ext->get_param_float("rx"); + ry << ext->get_param_float("ry"); + gx << ext->get_param_float("gx"); + gy << ext->get_param_float("gy"); + bx << ext->get_param_float("bx"); + by << ext->get_param_float("by"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + rx.str().c_str(), ry.str().c_str(), + gx.str().c_str(), gy.str().c_str(), + bx.str().c_str(), by.str().c_str() ); + + return _filter; + +}; /* Nudge RGB filter */ + +/** + \brief Custom predefined Nudge CMY filter. + + Nudge CMY channels separately and blend them to different types of backgrounds + + Filter's parameters: + Offsets + * Cyan + * x (-100.->100., default -6) -> offset1 (dx) + * y (-100.->100., default -6) -> offset1 (dy) + * Magenta + * x (-100.->100., default 6) -> offset2 (dx) + * y (-100.->100., default 7) -> offset2 (dy) + * Yellow + * x (-100.->100., default 1) -> offset3 (dx) + * y (-100.->100., default -16) -> offset3 (dy) + Color + * Background color (guint, default -1)-> flood (flood-color, flood-opacity) +*/ +class NudgeCMY : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NudgeCMY ( ) : Filter() { }; + ~NudgeCMY ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Nudge CMY") "\n" + "org.inkscape.effect.filter.NudgeCMY\n" + "\n" + "\n" + "\n" + "-6\n" + "-6\n" + "\n" + "6\n" + "7\n" + "\n" + "1\n" + "-16\n" + "\n" + "\n" + "-1\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Nudge CMY channels separately and blend them to different types of backgrounds") "\n" + "\n" + "\n", new NudgeCMY()); + }; +}; + +gchar const * +NudgeCMY::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream cx; + std::ostringstream cy; + std::ostringstream mx; + std::ostringstream my; + std::ostringstream yx; + std::ostringstream yy; + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + + cx << ext->get_param_float("cx"); + cy << ext->get_param_float("cy"); + mx << ext->get_param_float("mx"); + my << ext->get_param_float("my"); + yx << ext->get_param_float("yx"); + yy << ext->get_param_float("yy"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + cx.str().c_str(), cy.str().c_str(), + mx.str().c_str(), my.str().c_str(), + yx.str().c_str(), yy.str().c_str() ); + + return _filter; + +}; /* Nudge CMY filter */ + +/** + \brief Custom predefined Quadritone filter. + + Replace hue by two colors. + + Filter's parameters: + * Hue distribution (0->360, default 280) -> colormatrix1 (values) + * Colors (0->360, default 100) -> colormatrix3 (values) + * Blend mode 1 (enum, default Normal) -> blend1 (mode) + * Over-saturation (0.->1., default 0) -> composite1 (k2) + * Blend mode 2 (enum, default Normal) -> blend2 (mode) +*/ + +class Quadritone : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Quadritone ( ) : Filter() { }; + ~Quadritone ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Quadritone Fantasy") "\n" + "org.inkscape.effect.filter.Quadritone\n" + "280\n" + "100\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Replace hue by two colors") "\n" + "\n" + "\n", new Quadritone()); + }; + +}; + +gchar const * +Quadritone::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream dist; + std::ostringstream colors; + std::ostringstream blend1; + std::ostringstream sat; + std::ostringstream blend2; + + dist << ext->get_param_int("dist"); + colors << ext->get_param_int("colors"); + blend1 << ext->get_param_optiongroup("blend1"); + sat << ext->get_param_float("sat"); + blend2 << ext->get_param_optiongroup("blend2"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", dist.str().c_str(), colors.str().c_str(), blend1.str().c_str(), sat.str().c_str(), blend2.str().c_str() ); + + return _filter; +}; /* Quadritone filter */ + + +/** + \brief Custom predefined Simple blend filter. + + Simple blend filter. + + Filter's parameters: + * Color (guint, default 16777215) -> flood1 (flood-opacity, flood-color) + * Blend mode (enum, default Hue) -> blend1 (mode) +*/ +class SimpleBlend : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + SimpleBlend ( ) : Filter() { }; + ~SimpleBlend ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Simple blend") "\n" + "org.inkscape.effect.filter.SimpleBlend\n" + "16777215\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Simple blend filter") "\n" + "\n" + "\n", new SimpleBlend()); + }; +}; + +gchar const * +SimpleBlend::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream blend; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + blend << ext->get_param_optiongroup("blendmode"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n", r.str().c_str(), g.str().c_str(), b.str().c_str(), + a.str().c_str(), blend.str().c_str()); + + return _filter; +}; /* SimpleBlend filter */ + +/** + \brief Custom predefined Solarize filter. + + Classic photographic solarization effect. + + Filter's parameters: + * Type (enum, default "Solarize") -> + Solarize = blend1 (mode="darken"), blend2 (mode="screen") + Moonarize = blend1 (mode="lighten"), blend2 (mode="multiply") [No other access to the blend modes] + * Hue rotation (0->360, default 0) -> colormatrix1 (values) +*/ + + +class Solarize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Solarize ( ) : Filter() { }; + ~Solarize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Solarize") "\n" + "org.inkscape.effect.filter.Solarize\n" + "0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Classic photographic solarization effect") "\n" + "\n" + "\n", new Solarize()); + }; + +}; + +gchar const * +Solarize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream rotate; + std::ostringstream blend1; + std::ostringstream blend2; + + rotate << ext->get_param_int("rotate"); + const gchar *type = ext->get_param_optiongroup("type"); + if ((g_ascii_strcasecmp("solarize", type) == 0)) { + // Solarize + blend1 << "darken"; + blend2 << "screen"; + } else { + // Moonarize + blend1 << "lighten"; + blend2 << "multiply"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", rotate.str().c_str(), blend1.str().c_str(), blend2.str().c_str() ); + + return _filter; +}; /* Solarize filter */ + +/** + \brief Custom predefined Tritone filter. + + Create a custom tritone palette with additional glow, blend modes and hue moving. + + Filter's parameters: + * Option (enum, default Normal) -> + Normal = composite1 (in2="flood"), composite2 (in="p", in2="blend6"), blend6 (in2="composite1") + Enhance hue = Normal + composite2 (in="SourceGraphic") + Phosphorescence = Normal + blend6 (in2="SourceGraphic") composite2 (in="blend6", in2="composite1") + PhosphorescenceB = Normal + blend6 (in2="flood") composite1 (in2="SourceGraphic") + Hue to background = Normal + composite1 (in2="BackgroundImage") [a template with an activated background is needed, or colors become black] + * Hue distribution (0->360, default 0) -> colormatrix1 (values) + * Colors (guint, default -73203457) -> flood (flood-opacity, flood-color) + * Global blend (enum, default Lighten) -> blend5 (mode) [Multiply, Screen, Darken, Lighten only!] + * Glow (0.01->10., default 0.01) -> blur (stdDeviation) + * Glow & blend (enum, default Normal) -> blend6 (mode) [Normal, Multiply and Darken only!] + * Local light (0.->10., default 0) -> composite2 (k1) + * Global light (0.->10., default 1) -> composite2 (k3) [k2 must be fixed to 1]. +*/ + +class Tritone : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Tritone ( ) : Filter() { }; + ~Tritone ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Tritone") "\n" + "org.inkscape.effect.filter.Tritone\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0.01\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "0\n" + "1\n" + "\n" + "\n" + "0\n" + "-73203457\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Create a custom tritone palette with additional glow, blend modes and hue moving") "\n" + "\n" + "\n", new Tritone()); + }; + +}; + +gchar const * +Tritone::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream dist; + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream globalblend; + std::ostringstream glow; + std::ostringstream glowblend; + std::ostringstream llight; + std::ostringstream glight; + std::ostringstream c1in2; + std::ostringstream c2in; + std::ostringstream c2in2; + std::ostringstream b6in2; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + globalblend << ext->get_param_optiongroup("globalblend"); + dist << ext->get_param_int("dist"); + glow << ext->get_param_float("glow"); + glowblend << ext->get_param_optiongroup("glowblend"); + llight << ext->get_param_float("llight"); + glight << ext->get_param_float("glight"); + + const gchar *type = ext->get_param_optiongroup("type"); + if ((g_ascii_strcasecmp("enhue", type) == 0)) { + // Enhance hue + c1in2 << "flood"; + c2in << "SourceGraphic"; + c2in2 << "blend6"; + b6in2 << "composite1"; + } else if ((g_ascii_strcasecmp("phospho", type) == 0)) { + // Phosphorescence + c1in2 << "flood"; + c2in << "blend6"; + c2in2 << "composite1"; + b6in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("phosphoB", type) == 0)) { + // Phosphorescence B + c1in2 << "SourceGraphic"; + c2in << "blend6"; + c2in2 << "composite1"; + b6in2 << "flood"; + } else if ((g_ascii_strcasecmp("htb", type) == 0)) { + // Hue to background + c1in2 << "BackgroundImage"; + c2in << "blend2"; + c2in2 << "blend6"; + b6in2 << "composite1"; + } else { + // Normal + c1in2 << "flood"; + c2in << "blend2"; + c2in2 << "blend6"; + b6in2 << "composite"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", dist.str().c_str(), globalblend.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + c1in2.str().c_str(), glow.str().c_str(), b6in2.str().c_str(), glowblend.str().c_str(), + c2in.str().c_str(), c2in2.str().c_str(), llight.str().c_str(), glight.str().c_str() ); + + return _filter; +}; /* Tritone filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'COLOR' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ */ diff --git a/src/extension/internal/filter/distort.h b/src/extension/internal/filter/distort.h new file mode 100644 index 0000000..b8f66b6 --- /dev/null +++ b/src/extension/internal/filter/distort.h @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ +/* Change the 'DISTORT' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Distort filters + * Felt Feather + * Roughen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined FeltFeather filter. + + Blur and displace edges of shapes and pictures + + Filter's parameters: + * Type (enum, default "In") -> + in = map (in="composite3") + out = map (in="blur") + * Horizontal blur (0.01->30., default 15) -> blur (stdDeviation) + * Vertical blur (0.01->30., default 15) -> blur (stdDeviation) + * Dilatation (n-1th value, 0.->100., default 1) -> colormatrix (matrix) + * Erosion (nth value, 0.->100., default 0) -> colormatrix (matrix) + * Stroke (enum, default "Normal") -> + Normal = composite4 (operator="atop") + Wide = composite4 (operator="over") + Narrow = composite4 (operator="in") + No fill = composite4 (operator="xor") + * Roughness (group) + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->1., default 0.05) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->1., default 0.05) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->100, default 0) -> turbulence (seed) + * Intensity (0.0->100., default 30) -> displacement (scale) +*/ + +class FeltFeather : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + FeltFeather ( ) : Filter() { }; + ~FeltFeather ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Felt Feather") "\n" + "org.inkscape.effect.filter.FeltFeather\n" + "\n" + "\n" + "\n" + "\n" + "15\n" + "15\n" + "1\n" + "0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "5\n" + "5\n" + "3\n" + "0\n" + "30\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Blur and displace edges of shapes and pictures") "\n" + "\n" + "\n", new FeltFeather()); + }; + +}; + +gchar const * +FeltFeather::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream dilat; + std::ostringstream erosion; + + std::ostringstream turbulence; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream intensity; + + std::ostringstream map; + std::ostringstream stroke; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + dilat << ext->get_param_float("dilat"); + erosion << -ext->get_param_float("erosion"); + + turbulence << ext->get_param_optiongroup("turbulence"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + intensity << ext->get_param_float("intensity"); + + stroke << ext->get_param_optiongroup("stroke"); + + const gchar *maptype = ext->get_param_optiongroup("type"); + if (g_ascii_strcasecmp("in", maptype) == 0) { + map << "composite3"; + } else { + map << "blur"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", hblur.str().c_str(), vblur.str().c_str(), + turbulence.str().c_str(), complexity.str().c_str(), variation.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), + map.str().c_str(), intensity.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), stroke.str().c_str() ); + + return _filter; +}; /* Felt feather filter */ + +/** + \brief Custom predefined Roughen filter. + + Small-scale roughening to edges and content + + Filter's parameters: + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->10., default 0.013) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->10., default 0.013) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 5) -> turbulence (numOctaves) + * Variation (1->360, default 1) -> turbulence (seed) + * Intensity (0.0->50., default 6.6) -> displacement (scale) +*/ + +class Roughen : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Roughen ( ) : Filter() { }; + ~Roughen ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Roughen") "\n" + "org.inkscape.effect.filter.Roughen\n" + "\n" + "\n" + "\n" + "\n" + "1.3\n" + "1.3\n" + "5\n" + "0\n" + "6.6\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Small-scale roughening to edges and content") "\n" + "\n" + "\n", new Roughen()); + }; + +}; + +gchar const * +Roughen::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream intensity; + + type << ext->get_param_optiongroup("type"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + intensity << ext->get_param_float("intensity"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", type.str().c_str(), complexity.str().c_str(), variation.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), intensity.str().c_str()); + + return _filter; +}; /* Roughen filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'DISTORT' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ */ diff --git a/src/extension/internal/filter/filter-all.cpp b/src/extension/internal/filter/filter-all.cpp new file mode 100644 index 0000000..5aa3900 --- /dev/null +++ b/src/extension/internal/filter/filter-all.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2008 Authors: + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter.h" + +/* Put your filter here */ +#include "bevels.h" +#include "blurs.h" +#include "bumps.h" +#include "color.h" +#include "distort.h" +#include "image.h" +#include "morphology.h" +#include "overlays.h" +#include "paint.h" +#include "protrusions.h" +#include "shadows.h" +#include "textures.h" +#include "transparency.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +void +Filter::filters_all ( ) +{ + // Here come the filters which are coded in C++ in order to present a parameters dialog + + /* Experimental custom predefined filters */ + + // Bevels + DiffuseLight::init(); + MatteJelly::init(); + SpecularLight::init(); + + // Blurs + Blur::init(); + CleanEdges::init(); + CrossBlur::init(); + Feather::init(); + ImageBlur::init(); + + // Bumps + Bump::init(); + WaxBump::init(); + + // Color + Brilliance::init(); + ChannelPaint::init(); + ColorBlindness::init(); + ColorShift::init(); + Colorize::init(); + ComponentTransfer::init(); + Duochrome::init(); + ExtractChannel::init(); + FadeToBW::init(); + Greyscale::init(); + Invert::init(); + Lighting::init(); + LightnessContrast::init(); + NudgeRGB::init(); + NudgeCMY::init(); + Quadritone::init(); + SimpleBlend::init(); + Solarize::init(); + Tritone::init(); + + // Distort + FeltFeather::init(); + Roughen::init(); + + // Image effect + EdgeDetect::init(); + + // Image paint and draw + Chromolitho::init(); + CrossEngraving::init(); + Drawing::init(); + Electrize::init(); + NeonDraw::init(); + PointEngraving::init(); + Posterize::init(); + PosterizeBasic::init(); + + // Morphology + Crosssmooth::init(); + Outline::init(); + + // Overlays + NoiseFill::init(); + + // Protrusions + Snow::init(); + + // Shadows and glows + ColorizableDropShadow::init(); + + // Textures + InkBlot::init(); + + // Fill and transparency + Blend::init(); + ChannelTransparency::init(); + LightEraser::init(); + Opacity::init(); + Silhouette::init(); + + // Here come the rest of the filters that are read from SVG files in share/filters and + // .config/Inkscape/filters + /* This should always be last, don't put stuff below this + * line. */ + Filter::filters_all_files(); + + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/filter/filter-file.cpp b/src/extension/internal/filter/filter-file.cpp new file mode 100644 index 0000000..afa979f --- /dev/null +++ b/src/extension/internal/filter/filter-file.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2008 Authors: + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter.h" + +#include "io/sys.h" +#include "io/resource.h" +#include "io/stream/inkscapestream.h" + +/* Directory includes */ +#include "path-prefix.h" +#include "inkscape.h" + +/* Extension */ +#include "extension/extension.h" +#include "extension/system.h" + +/* System includes */ +#include +#include + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +void +filters_load_file (Glib::ustring filename, gchar * menuname) +{ + Inkscape::XML::Document *doc = sp_repr_read_file(filename.c_str(), INKSCAPE_EXTENSION_URI); + if (doc == nullptr) { + g_warning("File (%s) is not parseable as XML. Ignored.", filename.c_str()); + return; + } + + Inkscape::XML::Node * root = doc->root(); + if (strcmp(root->name(), "svg:svg")) { + Inkscape::GC::release(doc); + g_warning("File (%s) is not SVG. Ignored.", filename.c_str()); + return; + } + + for (Inkscape::XML::Node * child = root->firstChild(); + child != nullptr; child = child->next()) { + if (!strcmp(child->name(), "svg:defs")) { + for (Inkscape::XML::Node * defs = child->firstChild(); + defs != nullptr; defs = defs->next()) { + if (!strcmp(defs->name(), "svg:filter")) { + Filter::filters_load_node(defs, menuname); + } // oh! a filter + } //defs + } // is defs + } // children of root + + Inkscape::GC::release(doc); + return; +} + +void Filter::filters_all_files() +{ + for(auto &filename: get_filenames(USER, FILTERS, {".svg"})) { + filters_load_file(filename, _("Personal")); + } + for(auto &filename: get_filenames(SYSTEM, FILTERS, {".svg"})) { + filters_load_file(filename, _("Bundled")); + } +} + + +#include "extension/internal/clear-n_.h" + +class mywriter : public Inkscape::IO::BasicWriter { + Glib::ustring _str; +public: + void close() override; + void flush() override; + void put (char ch) override; + gchar const * c_str () { return _str.c_str(); } +}; + +void mywriter::close () { return; } +void mywriter::flush () { return; } +void mywriter::put (char ch) { _str += ch; } + + +void +Filter::filters_load_node (Inkscape::XML::Node *node, gchar * menuname) +{ + gchar const * label = node->attribute("inkscape:label"); + gchar const * menu = node->attribute("inkscape:menu"); + gchar const * menu_tooltip = node->attribute("inkscape:menu-tooltip"); + gchar const * id = node->attribute("id"); + + if (label == nullptr) { + label = id; + } + + gchar * xml_str = g_strdup_printf( + "\n" + "%s\n" + "org.inkscape.effect.filter.%s\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "%s\n" + "\n" + "\n", label, id, menu? menu : menuname, menu_tooltip? menu_tooltip : label); + + // FIXME: Bad hack: since we pull out a single filter node out of SVG file and + // serialize it, it loses the namespace declarations from the root, so we must provide + // one right here for our inkscape attributes + node->setAttribute("xmlns:inkscape", SP_INKSCAPE_NS_URI); + + mywriter writer; + sp_repr_write_stream(node, writer, 0, FALSE, g_quark_from_static_string("svg"), 0, 0); + + Inkscape::Extension::build_from_mem(xml_str, new Filter(g_strdup(writer.c_str()))); + g_free(xml_str); + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + diff --git a/src/extension/internal/filter/filter.cpp b/src/extension/internal/filter/filter.cpp new file mode 100644 index 0000000..45f2d0a --- /dev/null +++ b/src/extension/internal/filter/filter.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "selection.h" +#include "extension/extension.h" +#include "extension/effect.h" +#include "extension/system.h" +#include "xml/repr.h" +#include "xml/simple-node.h" +#include "xml/attribute-record.h" +#include "object/sp-defs.h" + +#include "filter.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +Filter::Filter() : + Inkscape::Extension::Implementation::Implementation(), + _filter(nullptr) { + return; +} + +Filter::Filter(gchar const * filter) : + Inkscape::Extension::Implementation::Implementation(), + _filter(filter) { + return; +} + +Filter::~Filter () { + if (_filter != nullptr) { + _filter = nullptr; + } + + return; +} + +bool Filter::load(Inkscape::Extension::Extension * /*module*/) +{ + return true; +} + +Inkscape::Extension::Implementation::ImplementationDocumentCache *Filter::newDocCache(Inkscape::Extension::Extension * /*ext*/, + Inkscape::UI::View::View * /*doc*/) +{ + return nullptr; +} + +gchar const *Filter::get_filter_text(Inkscape::Extension::Extension * /*ext*/) +{ + return _filter; +} + +Inkscape::XML::Document * +Filter::get_filter (Inkscape::Extension::Extension * ext) { + gchar const * filter = get_filter_text(ext); + return sp_repr_read_mem(filter, strlen(filter), nullptr); +} + +void +Filter::merge_filters( Inkscape::XML::Node * to, Inkscape::XML::Node * from, + Inkscape::XML::Document * doc, + gchar const * srcGraphic, gchar const * srcGraphicAlpha) +{ + if (from == nullptr) return; + + // copy attributes + for ( Inkscape::Util::List iter = from->attributeList() ; + iter ; ++iter ) { + gchar const * attr = g_quark_to_string(iter->key); + //printf("Attribute List: %s\n", attr); + if (!strcmp(attr, "id")) continue; // nope, don't copy that one! + to->setAttribute(attr, from->attribute(attr)); + + if (!strcmp(attr, "in") || !strcmp(attr, "in2") || !strcmp(attr, "in3")) { + if (srcGraphic != nullptr && !strcmp(from->attribute(attr), "SourceGraphic")) { + to->setAttribute(attr, srcGraphic); + } + + if (srcGraphicAlpha != nullptr && !strcmp(from->attribute(attr), "SourceAlpha")) { + to->setAttribute(attr, srcGraphicAlpha); + } + } + } + + // for each child call recursively + for (Inkscape::XML::Node * from_child = from->firstChild(); + from_child != nullptr ; from_child = from_child->next()) { + Glib::ustring name = "svg:"; + name += from_child->name(); + + Inkscape::XML::Node * to_child = doc->createElement(name.c_str()); + to->appendChild(to_child); + merge_filters(to_child, from_child, doc, srcGraphic, srcGraphicAlpha); + + if (from_child == from->firstChild() && !strcmp("filter", from->name()) && srcGraphic != nullptr && to_child->attribute("in") == nullptr) { + to_child->setAttribute("in", srcGraphic); + } + Inkscape::GC::release(to_child); + } +} + +#define FILTER_SRC_GRAPHIC "fbSourceGraphic" +#define FILTER_SRC_GRAPHIC_ALPHA "fbSourceGraphicAlpha" + +void Filter::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, + Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + Inkscape::XML::Document *filterdoc = get_filter(module); + if (filterdoc == nullptr) { + return; // could not parse the XML source of the filter; typically parser will stderr a warning + } + + //printf("Calling filter effect\n"); + Inkscape::Selection * selection = ((SPDesktop *)document)->selection; + + // TODO need to properly refcount the items, at least + std::vector items(selection->items().begin(), selection->items().end()); + + Inkscape::XML::Document * xmldoc = document->doc()->getReprDoc(); + Inkscape::XML::Node * defsrepr = document->doc()->getDefs()->getRepr(); + + for(auto spitem : items) { + Inkscape::XML::Node *node = spitem->getRepr(); + + SPCSSAttr * css = sp_repr_css_attr(node, "style"); + gchar const * filter = sp_repr_css_property(css, "filter", nullptr); + + if (filter == nullptr) { + + Inkscape::XML::Node * newfilterroot = xmldoc->createElement("svg:filter"); + merge_filters(newfilterroot, filterdoc->root(), xmldoc); + defsrepr->appendChild(newfilterroot); + document->doc()->resources_changed_signals[g_quark_from_string("filter")].emit(); + + Glib::ustring url = "url(#"; url += newfilterroot->attribute("id"); url += ")"; + + + Inkscape::GC::release(newfilterroot); + + sp_repr_css_set_property(css, "filter", url.c_str()); + sp_repr_css_set(node, css, "style"); + } else { + if (strncmp(filter, "url(#", strlen("url(#")) || filter[strlen(filter) - 1] != ')') { + // This is not url(#id) -- we can't handle it + continue; + } + + gchar * lfilter = g_strndup(filter + 5, strlen(filter) - 6); + Inkscape::XML::Node * filternode = nullptr; + for (Inkscape::XML::Node * child = defsrepr->firstChild(); child != nullptr; child = child->next()) { + if (!strcmp(lfilter, child->attribute("id"))) { + filternode = child; + break; + } + } + g_free(lfilter); + + // no filter + if (filternode == nullptr) { + g_warning("no assigned filter found!"); + continue; + } + + if (filternode->lastChild() == nullptr) { + // empty filter, we insert + merge_filters(filternode, filterdoc->root(), xmldoc); + } else { + // existing filter, we merge + filternode->lastChild()->setAttribute("result", FILTER_SRC_GRAPHIC); + Inkscape::XML::Node * alpha = xmldoc->createElement("svg:feColorMatrix"); + alpha->setAttribute("result", FILTER_SRC_GRAPHIC_ALPHA); + alpha->setAttribute("in", FILTER_SRC_GRAPHIC); // not required, but we're being explicit + alpha->setAttribute("values", "0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"); + + filternode->appendChild(alpha); + + merge_filters(filternode, filterdoc->root(), xmldoc, FILTER_SRC_GRAPHIC, FILTER_SRC_GRAPHIC_ALPHA); + + Inkscape::GC::release(alpha); + } + } + } + + return; +} + +#include "extension/internal/clear-n_.h" + +void +Filter::filter_init (gchar const * id, gchar const * name, gchar const * submenu, gchar const * tip, gchar const * filter) +{ + gchar * xml_str = g_strdup_printf( + "\n" + "%s\n" + "org.inkscape.effect.filter.%s\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "%s\n" + "\n" + "\n", name, id, submenu, tip); + Inkscape::Extension::build_from_mem(xml_str, new Filter(filter)); + g_free(xml_str); + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/filter/filter.h b/src/extension/internal/filter/filter.h new file mode 100644 index 0000000..cb3ed36 --- /dev/null +++ b/src/extension/internal/filter/filter.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H +#define INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H + +/* + * Copyright (C) 2008 Authors: + * Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "extension/implementation/implementation.h" + +namespace Inkscape { + +namespace XML { + struct Document; +} + +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { +namespace Filter { + +class Filter : public Inkscape::Extension::Implementation::Implementation { +protected: + gchar const * _filter; + virtual gchar const * get_filter_text (Inkscape::Extension::Extension * ext); + +private: + Inkscape::XML::Document * get_filter (Inkscape::Extension::Extension * ext); + void merge_filters (Inkscape::XML::Node * to, Inkscape::XML::Node * from, Inkscape::XML::Document * doc, gchar const * srcGraphic = nullptr, gchar const * srcGraphicAlpha = nullptr); + +public: + Filter(); + Filter(gchar const * filter); + ~Filter() override; + + bool load(Inkscape::Extension::Extension *module) override; + Inkscape::Extension::Implementation::ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * doc) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void filter_init(gchar const * id, gchar const * name, gchar const * submenu, gchar const * tip, gchar const * filter); + static void filters_all(); + + /* File loader related */ + static void filters_all_files(); + static void filters_load_node(Inkscape::XML::Node *node, gchar * menuname); + +}; + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +#endif // INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H diff --git a/src/extension/internal/filter/image.h b/src/extension/internal/filter/image.h new file mode 100644 index 0000000..2a0e334 --- /dev/null +++ b/src/extension/internal/filter/image.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ +/* Change the 'IMAGE' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Image filters + * Edge detect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Edge detect filter. + + Detect color edges in object. + + Filter's parameters: + * Detection type (enum, default Full) -> convolve (kernelMatrix) + * Level (0.01->10., default 1.) -> convolve (divisor) + * Inverted (boolean, default false) -> convolve (bias) +*/ +class EdgeDetect : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + EdgeDetect ( ) : Filter() { }; + ~EdgeDetect ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Edge Detect") "\n" + "org.inkscape.effect.filter.EdgeDetect\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "1.0\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Detect color edges in object") "\n" + "\n" + "\n", new EdgeDetect()); + }; + +}; + +gchar const * +EdgeDetect::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream matrix; + std::ostringstream inverted; + std::ostringstream level; + + const gchar *type = ext->get_param_optiongroup("type"); + + level << 1 / ext->get_param_float("level"); + + if ((g_ascii_strcasecmp("vertical", type) == 0)) { + matrix << "0 0 0 1 -2 1 0 0 0"; + } else if ((g_ascii_strcasecmp("horizontal", type) == 0)) { + matrix << "0 1 0 0 -2 0 0 1 0"; + } else { + matrix << "0 1 0 1 -4 1 0 1 0"; + } + + if (ext->get_param_bool("inverted")) { + inverted << "1"; + } else { + inverted << "0"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", matrix.str().c_str(), inverted.str().c_str(), level.str().c_str()); + + return _filter; +}; + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'IMAGE' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ */ diff --git a/src/extension/internal/filter/morphology.h b/src/extension/internal/filter/morphology.h new file mode 100644 index 0000000..9c29153 --- /dev/null +++ b/src/extension/internal/filter/morphology.h @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ +/* Change the 'MORPHOLOGY' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Morphology filters + * Cross-smooth + * Outline + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Cross-smooth filter. + + Smooth the outside of shapes and pictures. + + Filter's parameters: + * Type (enum, default "Smooth edges") -> + Inner = composite1 (operator="in") + Outer = composite1 (operator="over") + Open = composite1 (operator="XOR") + * Width (0.01->30., default 10.) -> blur (stdDeviation) + * Level (0.2->2., default 1.) -> composite2 (k2) + * Dilatation (1.->100., default 10.) -> colormatrix1 (last-1 value) + * Erosion (1.->100., default 1.) -> colormatrix1 (last value) + * Antialiasing (0.01->1., default 1) -> blur2 (stdDeviation) + * Blur content (boolean, default false) -> blend (true: in="colormatrix2", false: in="SourceGraphic") +*/ + +class Crosssmooth : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Crosssmooth ( ) : Filter() { }; + ~Crosssmooth ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Cross-smooth") "\n" + "org.inkscape.effect.filter.crosssmooth\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "10\n" + "1\n" + "10\n" + "1\n" + "1\n" + "false\n" + + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Smooth edges and angles of shapes") "\n" + "\n" + "\n", new Crosssmooth()); + }; + +}; + +gchar const * +Crosssmooth::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream width; + std::ostringstream level; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream antialias; + std::ostringstream content; + + type << ext->get_param_optiongroup("type"); + width << ext->get_param_float("width"); + level << ext->get_param_float("level"); + dilat << ext->get_param_float("dilat"); + erosion << (1 - ext->get_param_float("erosion")); + antialias << ext->get_param_float("antialias"); + + if (ext->get_param_bool("content")) { + content << "colormatrix2"; + } else { + content << "SourceGraphic"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", width.str().c_str(), type.str().c_str(), level.str().c_str(), + dilat.str().c_str(), erosion.str().c_str(), antialias.str().c_str(), + content.str().c_str()); + + return _filter; +}; /* Cross-smooth filter */ + +/** + \brief Custom predefined Outline filter. + + Adds a colorizable outline + + Filter's parameters: + * Fill image (boolean, default false) -> true: composite2 (in="SourceGraphic"), false: composite2 (in="blur2") + * Hide image (boolean, default false) -> true: composite4 (in="composite3"), false: composite4 (in="SourceGraphic") + * Stroke type (enum, default over) -> composite2 (operator) + * Stroke position (enum, default inside) + * inside -> composite1 (operator="out", in="SourceGraphic", in2="blur1") + * outside -> composite1 (operator="out", in="blur1", in2="SourceGraphic") + * overlayed -> composite1 (operator="xor", in="blur1", in2="SourceGraphic") + * Width 1 (0.01->20., default 4) -> blur1 (stdDeviation) + * Dilatation 1 (1.->100., default 100) -> colormatrix1 (n-1th value) + * Erosion 1 (0.->100., default 1) -> colormatrix1 (nth value 0->-100) + * Width 2 (0.01->20., default 0.5) -> blur2 (stdDeviation) + * Dilatation 2 (1.->100., default 50) -> colormatrix2 (n-1th value) + * Erosion 2 (0.->100., default 5) -> colormatrix2 (nth value 0->-100) + * Antialiasing (0.01->1., default 1) -> blur3 (stdDeviation) + * Color (guint, default 0,0,0,255) -> flood (flood-color, flood-opacity) + * Fill opacity (0.->1., default 1) -> composite5 (k2) + * Stroke opacity (0.->1., default 1) -> composite5 (k3) + +*/ + +class Outline : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Outline ( ) : Filter() { }; + ~Outline ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Outline") "\n" + "org.inkscape.effect.filter.Outline\n" + "\n" + "\n" + "false\n" + "false\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "4\n" + "100\n" + "1\n" + "0.5\n" + "50\n" + "5\n" + "1\n" + "false\n" + "\n" + "\n" + "255\n" + "1\n" + "1\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Adds a colorizable outline") "\n" + "\n" + "\n", new Outline()); + }; + +}; + +gchar const * +Outline::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream width1; + std::ostringstream dilat1; + std::ostringstream erosion1; + std::ostringstream width2; + std::ostringstream dilat2; + std::ostringstream erosion2; + std::ostringstream antialias; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream fopacity; + std::ostringstream sopacity; + std::ostringstream smooth; + + std::ostringstream c1in; + std::ostringstream c1in2; + std::ostringstream c1op; + std::ostringstream c2in; + std::ostringstream c2op; + std::ostringstream c4in; + + + width1 << ext->get_param_float("width1"); + dilat1 << ext->get_param_float("dilat1"); + erosion1 << (- ext->get_param_float("erosion1")); + width2 << ext->get_param_float("width2"); + dilat2 << ext->get_param_float("dilat2"); + erosion2 << (- ext->get_param_float("erosion2")); + antialias << ext->get_param_float("antialias"); + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + fopacity << ext->get_param_float("fopacity"); + sopacity << ext->get_param_float("sopacity"); + + const gchar *position = ext->get_param_optiongroup("position"); + if((g_ascii_strcasecmp("inside", position) == 0)) { + // Indide + c1in << "SourceGraphic"; + c1in2 << "blur1"; + c1op << "out"; + } else if((g_ascii_strcasecmp("outside", position) == 0)) { + // Outside + c1in << "blur1"; + c1in2 << "SourceGraphic"; + c1op << "out"; + } else { + // Overlayed + c1in << "blur1"; + c1in2 << "SourceGraphic"; + c1op << "xor"; + } + + if (ext->get_param_bool("fill")) { + c2in << "SourceGraphic"; + } else { + c2in << "blur2"; + } + + c2op << ext->get_param_optiongroup("type"); + + if (ext->get_param_bool("outline")) { + c4in << "composite3"; + } else { + c4in << "SourceGraphic"; + } + + if (ext->get_param_bool("smooth")) { + smooth << "1 0"; + } else { + smooth << "5 -1"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", width1.str().c_str(), c1in.str().c_str(), c1in2.str().c_str(), c1op.str().c_str(), + dilat1.str().c_str(), erosion1.str().c_str(), + width2.str().c_str(), c2in.str().c_str(), c2op.str().c_str(), + dilat2.str().c_str(), erosion2.str().c_str(), antialias.str().c_str(), smooth.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + c4in.str().c_str(), fopacity.str().c_str(), sopacity.str().c_str() ); + + return _filter; +}; /* Outline filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'MORPHOLOGY' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ */ diff --git a/src/extension/internal/filter/overlays.h b/src/extension/internal/filter/overlays.h new file mode 100644 index 0000000..fd88460 --- /dev/null +++ b/src/extension/internal/filter/overlays.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ +/* Change the 'OVERLAYS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Overlays filters + * Noise fill + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Noise fill filter. + + Basic noise fill and transparency texture + + Filter's parameters: + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (*1000) (0.01->10000., default 20) -> turbulence (baseFrequency [/1000]) + * Vertical frequency (*1000) (0.01->10000., default 40) -> turbulence (baseFrequency [/1000]) + * Complexity (1->5, default 5) -> turbulence (numOctaves) + * Variation (1->360, default 1) -> turbulence (seed) + * Dilatation (1.->50., default 3) -> color (n-1th value) + * Erosion (0.->50., default 1) -> color (nth value 0->-50) + * Color (guint, default 148,115,39,255) -> flood (flood-color, flood-opacity) + * Inverted (boolean, default false) -> composite1 (operator, true="in", false="out") +*/ + +class NoiseFill : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NoiseFill ( ) : Filter() { }; + ~NoiseFill ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Noise Fill") "\n" + "org.inkscape.effect.filter.NoiseFill\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "20\n" + "40\n" + "5\n" + "0\n" + "3\n" + "1\n" + "false\n" + "\n" + "\n" + "354957823\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Basic noise fill and transparency texture") "\n" + "\n" + "\n", new NoiseFill()); + }; + +}; + +gchar const * +NoiseFill::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream inverted; + + type << ext->get_param_optiongroup("type"); + hfreq << (ext->get_param_float("hfreq") / 1000); + vfreq << (ext->get_param_float("vfreq") / 1000); + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + if (ext->get_param_bool("inverted")) + inverted << "out"; + else + inverted << "in"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), inverted.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str()); + + return _filter; +}; /* NoiseFill filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'OVERLAYS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ */ diff --git a/src/extension/internal/filter/paint.h b/src/extension/internal/filter/paint.h new file mode 100644 index 0000000..162d150 --- /dev/null +++ b/src/extension/internal/filter/paint.h @@ -0,0 +1,1029 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ +/* Change the 'PAINT' above to be your file name */ + +/* + * Copyright (C) 2012 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Image paint and draw filters + * Chromolitho + * Cross engraving + * Drawing + * Electrize + * Neon draw + * Point engraving + * Posterize + * Posterize basic + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Chromolitho filter. + + Chromo effect with customizable edge drawing and graininess + + Filter's parameters: + * Drawing (boolean, default checked) -> Checked = blend1 (in="convolve1"), unchecked = blend1 (in="composite1") + * Transparent (boolean, default unchecked) -> Checked = colormatrix5 (in="colormatrix4"), Unchecked = colormatrix5 (in="component1") + * Invert (boolean, default false) -> component1 (tableValues) [adds a trailing 0] + * Dented (boolean, default false) -> component1 (tableValues) [adds intermediate 0s] + * Lightness (0.->10., default 0.) -> composite1 (k1) + * Saturation (0.->1., default 1.) -> colormatrix3 (values) + * Noise reduction (1->1000, default 20) -> convolve (kernelMatrix, central value -1001->-2000, default -1020) + * Drawing blend (enum, default Normal) -> blend1 (mode) + * Smoothness (0.01->10, default 1) -> blur1 (stdDeviation) + * Grain (boolean, default unchecked) -> Checked = blend2 (in="colormatrix2"), Unchecked = blend2 (in="blur1") + * Grain x frequency (0.->1000, default 1000) -> turbulence1 (baseFrequency, first value) + * Grain y frequency (0.->1000, default 1000) -> turbulence1 (baseFrequency, second value) + * Grain complexity (1->5, default 1) -> turbulence1 (numOctaves) + * Grain variation (0->1000, default 0) -> turbulence1 (seed) + * Grain expansion (1.->50., default 1.) -> colormatrix1 (n-1 value) + * Grain erosion (0.->40., default 0.) -> colormatrix1 (nth value) [inverted] + * Grain color (boolean, default true) -> colormatrix2 (values) + * Grain blend (enum, default Normal) -> blend2 (mode) +*/ +class Chromolitho : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Chromolitho ( ) : Filter() { }; + ~Chromolitho ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Chromolitho") "\n" + "org.inkscape.effect.filter.Chromolitho\n" + "\n" + "\n" + "true\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "false\n" + "false\n" + "false\n" + "0\n" + "1\n" + "10\n" + "1\n" + "\n" + "\n" + "true\n" + "1000\n" + "1000\n" + "1\n" + "0\n" + "1\n" + "0\n" + "true\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Chromo effect with customizable edge drawing and graininess") "\n" + "\n" + "\n", new Chromolitho()); + }; +}; + +gchar const * +Chromolitho::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream b1in; + std::ostringstream b2in; + std::ostringstream col3in; + std::ostringstream transf; + std::ostringstream light; + std::ostringstream saturation; + std::ostringstream noise; + std::ostringstream dblend; + std::ostringstream smooth; + std::ostringstream grainxf; + std::ostringstream grainyf; + std::ostringstream grainc; + std::ostringstream grainv; + std::ostringstream gblend; + std::ostringstream grainexp; + std::ostringstream grainero; + std::ostringstream graincol; + + if (ext->get_param_bool("drawing")) + b1in << "convolve1"; + else + b1in << "composite1"; + + if (ext->get_param_bool("transparent")) + col3in << "colormatrix4"; + else + col3in << "component1"; + light << ext->get_param_float("light"); + saturation << ext->get_param_float("saturation"); + noise << (-1000 - ext->get_param_int("noise")); + dblend << ext->get_param_optiongroup("dblend"); + smooth << ext->get_param_float("smooth"); + + if (ext->get_param_bool("dented")) { + transf << "0 1 0 1"; + } else { + transf << "0 1 1"; + } + if (ext->get_param_bool("inverted")) + transf << " 0"; + + if (ext->get_param_bool("grain")) + b2in << "colormatrix2"; + else + b2in << "blur1"; + grainxf << (ext->get_param_float("grainxf") / 1000); + grainyf << (ext->get_param_float("grainyf") / 1000); + grainc << ext->get_param_int("grainc"); + grainv << ext->get_param_int("grainv"); + gblend << ext->get_param_optiongroup("gblend"); + grainexp << ext->get_param_float("grainexp"); + grainero << (-ext->get_param_float("grainero")); + if (ext->get_param_bool("graincol")) + graincol << "1"; + else + graincol << "0"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", light.str().c_str(), noise.str().c_str(), b1in.str().c_str(), dblend.str().c_str(), smooth.str().c_str(), grainxf.str().c_str(), grainyf.str().c_str(), grainc.str().c_str(), grainv.str().c_str(), grainexp.str().c_str(), grainero.str().c_str(), graincol.str().c_str(), b2in.str().c_str(), gblend.str().c_str(), saturation.str().c_str(), transf.str().c_str(), transf.str().c_str(), transf.str().c_str(), col3in.str().c_str()); + + return _filter; +}; /* Chromolitho filter */ + +/** + \brief Custom predefined Cross engraving filter. + + Convert image to an engraving made of vertical and horizontal lines + + Filter's parameters: + * Clean-up (1->500, default 30) -> convolve1 (kernelMatrix, central value -1001->-1500, default -1030) + * Dilatation (1.->50., default 1) -> color2 (n-1th value) + * Erosion (0.->50., default 0) -> color2 (nth value 0->-50) + * Strength (0.->10., default 0.5) -> composite2 (k2) + * Length (0.5->20, default 4) -> blur1 (stdDeviation x), blur2 (stdDeviation y) + * Transparent (boolean, default false) -> composite 4 (in, true->composite3, false->blend) +*/ +class CrossEngraving : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CrossEngraving ( ) : Filter() { }; + ~CrossEngraving ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Cross Engraving") "\n" + "org.inkscape.effect.filter.CrossEngraving\n" + "30\n" + "1\n" + "0\n" + "0.5\n" + "4\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Convert image to an engraving made of vertical and horizontal lines") "\n" + "\n" + "\n", new CrossEngraving()); + }; +}; + +gchar const * +CrossEngraving::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream clean; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream strength; + std::ostringstream length; + std::ostringstream trans; + + clean << (-1000 - ext->get_param_int("clean")); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + strength << ext->get_param_float("strength"); + length << ext->get_param_float("length"); + if (ext->get_param_bool("trans")) + trans << "composite3"; + else + trans << "blend"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", clean.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), strength.str().c_str(), length.str().c_str(), length.str().c_str(), trans.str().c_str()); + + return _filter; +}; /* CrossEngraving filter */ + +/** + \brief Custom predefined Drawing filter. + + Convert images to duochrome drawings. + + Filter's parameters: + * Simplification strength (0.01->20, default 0.6) -> blur1 (stdDeviation) + * Clean-up (1->500, default 10) -> convolve1 (kernelMatrix, central value -1001->-1500, default -1010) + * Erase (0.->6., default 0) -> composite1 (k4) + * Smoothness strength (0.01->20, default 0.6) -> blur2 (stdDeviation) + * Dilatation (1.->50., default 6) -> color2 (n-1th value) + * Erosion (0.->50., default 2) -> color2 (nth value 0->-50) + * translucent (boolean, default false) -> composite 8 (in, true->merge1, false->color5) + + * Blur strength (0.01->20., default 1.) -> blur3 (stdDeviation) + * Blur dilatation (1.->50., default 6) -> color4 (n-1th value) + * Blur erosion (0.->50., default 2) -> color4 (nth value 0->-50) + + * Stroke color (guint, default 64,64,64,255) -> flood2 (flood-color), composite3 (k2) + * Image on stroke (boolean, default false) -> composite2 (in="flood2" true-> in="SourceGraphic") + * Offset (-100->100, default 0) -> offset (val) + + * Fill color (guint, default 200,200,200,255) -> flood3 (flood-opacity), composite5 (k2) + * Image on fill (boolean, default false) -> composite4 (in="flood3" true-> in="SourceGraphic") + +*/ + +class Drawing : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Drawing ( ) : Filter() { }; + ~Drawing ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Drawing") "\n" + "org.inkscape.effect.filter.Drawing\n" + "\n" + "\n" + "\n" + "0.6\n" + "10\n" + "0\n" + "false\n" + "\n" + "0.6\n" + "6\n" + "2\n" + "\n" + "1\n" + "6\n" + "2\n" + "\n" + "\n" + "-1515870721\n" + "false\n" + "\n" + "\n" + "589505535\n" + "false\n" + "0\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Convert images to duochrome drawings") "\n" + "\n" + "\n", new Drawing()); + }; +}; + +gchar const * +Drawing::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simply; + std::ostringstream clean; + std::ostringstream erase; + std::ostringstream smooth; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream translucent; + std::ostringstream offset; + std::ostringstream blur; + std::ostringstream bdilat; + std::ostringstream berosion; + std::ostringstream strokea; + std::ostringstream stroker; + std::ostringstream strokeg; + std::ostringstream strokeb; + std::ostringstream ios; + std::ostringstream filla; + std::ostringstream fillr; + std::ostringstream fillg; + std::ostringstream fillb; + std::ostringstream iof; + + simply << ext->get_param_float("simply"); + clean << (-1000 - ext->get_param_int("clean")); + erase << (ext->get_param_float("erase") / 10); + smooth << ext->get_param_float("smooth"); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + if (ext->get_param_bool("translucent")) + translucent << "merge1"; + else + translucent << "color5"; + offset << ext->get_param_int("offset"); + + blur << ext->get_param_float("blur"); + bdilat << ext->get_param_float("bdilat"); + berosion << (- ext->get_param_float("berosion")); + + guint32 fcolor = ext->get_param_color("fcolor"); + fillr << ((fcolor >> 24) & 0xff); + fillg << ((fcolor >> 16) & 0xff); + fillb << ((fcolor >> 8) & 0xff); + filla << (fcolor & 0xff) / 255.0F; + if (ext->get_param_bool("iof")) + iof << "SourceGraphic"; + else + iof << "flood3"; + + guint32 scolor = ext->get_param_color("scolor"); + stroker << ((scolor >> 24) & 0xff); + strokeg << ((scolor >> 16) & 0xff); + strokeb << ((scolor >> 8) & 0xff); + strokea << (scolor & 0xff) / 255.0F; + if (ext->get_param_bool("ios")) + ios << "SourceGraphic"; + else + ios << "flood2"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", simply.str().c_str(), clean.str().c_str(), erase.str().c_str(), smooth.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), blur.str().c_str(), bdilat.str().c_str(), berosion.str().c_str(), stroker.str().c_str(), strokeg.str().c_str(), strokeb.str().c_str(), ios.str().c_str(), strokea.str().c_str(), offset.str().c_str(), offset.str().c_str(), fillr.str().c_str(), fillg.str().c_str(), fillb.str().c_str(), iof.str().c_str(), filla.str().c_str(), translucent.str().c_str()); + + return _filter; +}; /* Drawing filter */ + + +/** + \brief Custom predefined Electrize filter. + + Electro solarization effects. + + Filter's parameters: + * Simplify (0.01->10., default 2.) -> blur (stdDeviation) + * Effect type (enum: table or discrete, default "table") -> component (type) + * Level (0->10, default 3) -> component (tableValues) + * Inverted (boolean, default false) -> component (tableValues) +*/ +class Electrize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Electrize ( ) : Filter() { }; + ~Electrize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Electrize") "\n" + "org.inkscape.effect.filter.Electrize\n" + "2.0\n" + "\n" + "\n" + "\n" + "\n" + "3\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Electro solarization effects") "\n" + "\n" + "\n", new Electrize()); + }; +}; + +gchar const * +Electrize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream type; + std::ostringstream values; + + blur << ext->get_param_float("blur"); + type << ext->get_param_optiongroup("type"); + + // TransfertComponent table values are calculated based on the effect level and inverted parameters. + int val = 0; + int levels = ext->get_param_int("levels") + 1; + if (ext->get_param_bool("invert")) { + val = 1; + } + values << val; + for ( int step = 1 ; step <= levels ; step++ ) { + if (val == 1) { + val = 0; + } + else { + val = 1; + } + values << " " << val; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", blur.str().c_str(), type.str().c_str(), values.str().c_str(), type.str().c_str(), values.str().c_str(), type.str().c_str(), values.str().c_str()); + + return _filter; +}; /* Electrize filter */ + +/** + \brief Custom predefined Neon draw filter. + + Posterize and draw smooth lines around color shapes + + Filter's parameters: + * Lines type (enum, default smooth) -> + smooth = component2 (type="table"), composite1 (in2="blur2") + hard = component2 (type="discrete"), composite1 (in2="component1") + * Simplify (0.01->20., default 3) -> blur1 (stdDeviation) + * Line width (0.01->20., default 3) -> blur2 (stdDeviation) + * Lightness (0.->10., default 1) -> composite1 (k2) + * Blend (enum [normal, multiply, screen], default normal) -> blend (mode) + * Dark mode (boolean, default false) -> composite2 (true: in2="component2") +*/ +class NeonDraw : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NeonDraw ( ) : Filter() { }; + ~NeonDraw ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Neon Draw") "\n" + "org.inkscape.effect.filter.NeonDraw\n" + "\n" + "\n" + "\n" + "\n" + "3\n" + "3\n" + "1\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Posterize and draw smooth lines around color shapes") "\n" + "\n" + "\n", new NeonDraw()); + }; +}; + +gchar const * +NeonDraw::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blend; + std::ostringstream simply; + std::ostringstream width; + std::ostringstream lightness; + std::ostringstream type; + + type << ext->get_param_optiongroup("type"); + blend << ext->get_param_optiongroup("blend"); + simply << ext->get_param_float("simply"); + width << ext->get_param_float("width"); + lightness << ext->get_param_float("lightness"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", blend.str().c_str(), simply.str().c_str(), width.str().c_str(), type.str().c_str(), type.str().c_str(), type.str().c_str(), lightness.str().c_str()); + + return _filter; +}; /* NeonDraw filter */ + +/** + \brief Custom predefined Point engraving filter. + + Convert image to a transparent point engraving + + Filter's parameters: + + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->1., default 1) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->1., default 1) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->1000, default 0) -> turbulence (seed) + * Noise reduction (-1000->-1500, default -1045) -> convolve (kernelMatrix, central value) + * Noise blend (enum, all blend options, default normal) -> blend (mode) + * Lightness (0.->10., default 2.5) -> composite1 (k1) + * Grain lightness (0.->10., default 1.3) -> composite1 (k2) + * Erase (0.00->1., default 0) -> composite1 (k4) + * Blur (0.01->2., default 0.5) -> blur (stdDeviation) + + * Drawing color (guint32, default rgb(255,255,255)) -> flood1 (flood-color, flood-opacity) + + * Background color (guint32, default rgb(99,89,46)) -> flood2 (flood-color, flood-opacity) +*/ + +class PointEngraving : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + PointEngraving ( ) : Filter() { }; + ~PointEngraving ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Point Engraving") "\n" + "org.inkscape.effect.filter.PointEngraving\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "100\n" + "100\n" + "1\n" + "0\n" + "45\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "2.5\n" + "1.3\n" + "0\n" + "0.5\n" + "\n" + "\n" + "-1\n" + "false\n" + "\n" + "\n" + "1666789119\n" + "false\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Convert image to a transparent point engraving") "\n" + "\n" + "\n", new PointEngraving()); + }; + +}; + +gchar const * +PointEngraving::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream reduction; + std::ostringstream blend; + std::ostringstream lightness; + std::ostringstream grain; + std::ostringstream erase; + std::ostringstream blur; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream br; + std::ostringstream bg; + std::ostringstream bb; + std::ostringstream ba; + std::ostringstream iof; + std::ostringstream iop; + + type << ext->get_param_optiongroup("type"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + reduction << (-1000 - ext->get_param_int("reduction")); + blend << ext->get_param_optiongroup("blend"); + lightness << ext->get_param_float("lightness"); + grain << ext->get_param_float("grain"); + erase << ext->get_param_float("erase"); + blur << ext->get_param_float("blur"); + + guint32 fcolor = ext->get_param_color("fcolor"); + r << ((fcolor >> 24) & 0xff); + g << ((fcolor >> 16) & 0xff); + b << ((fcolor >> 8) & 0xff); + a << (fcolor & 0xff) / 255.0F; + + guint32 pcolor = ext->get_param_color("pcolor"); + br << ((pcolor >> 24) & 0xff); + bg << ((pcolor >> 16) & 0xff); + bb << ((pcolor >> 8) & 0xff); + ba << (pcolor & 0xff) / 255.0F; + + if (ext->get_param_bool("iof")) + iof << "SourceGraphic"; + else + iof << "flood2"; + + if (ext->get_param_bool("iop")) + iop << "SourceGraphic"; + else + iop << "flood1"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", reduction.str().c_str(), blend.str().c_str(), + type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), + lightness.str().c_str(), grain.str().c_str(), erase.str().c_str(), blur.str().c_str(), + br.str().c_str(), bg.str().c_str(), bb.str().c_str(), ba.str().c_str(), iop.str().c_str(), + r.str().c_str(), g.str().c_str(), b.str().c_str(), a.str().c_str(), iof.str().c_str(), + a.str().c_str(), ba.str().c_str() ); + + return _filter; +}; /* Point engraving filter */ + +/** + \brief Custom predefined Poster paint filter. + + Poster and painting effects. + + Filter's parameters: + * Effect type (enum, default "Normal") -> + Normal = feComponentTransfer + Dented = Normal + intermediate values + * Transfer type (enum, default "discrete") -> component (type) + * Levels (0->15, default 5) -> component (tableValues) + * Blend mode (enum, default "Lighten") -> blend (mode) + * Primary simplify (0.01->100., default 4.) -> blur1 (stdDeviation) + * Secondary simplify (0.01->100., default 0.5) -> blur2 (stdDeviation) + * Pre-saturation (0.->1., default 1.) -> color1 (values) + * Post-saturation (0.->1., default 1.) -> color2 (values) + * Simulate antialiasing (boolean, default false) -> blur3 (true->stdDeviation=0.5, false->stdDeviation=0.01) +*/ +class Posterize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Posterize ( ) : Filter() { }; + ~Posterize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Poster Paint") "\n" + "org.inkscape.effect.filter.Posterize\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "5\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "4.0\n" + "0.5\n" + "1.00\n" + "1.00\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Poster and painting effects") "\n" + "\n" + "\n", new Posterize()); + }; +}; + +gchar const * +Posterize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream table; + std::ostringstream blendmode; + std::ostringstream blur1; + std::ostringstream blur2; + std::ostringstream presat; + std::ostringstream postsat; + std::ostringstream transf; + std::ostringstream antialias; + + table << ext->get_param_optiongroup("table"); + blendmode << ext->get_param_optiongroup("blend"); + blur1 << ext->get_param_float("blur1"); + blur2 << ext->get_param_float("blur2"); + presat << ext->get_param_float("presaturation"); + postsat << ext->get_param_float("postsaturation"); + + // TransfertComponent table values are calculated based on the poster type. + transf << "0"; + int levels = ext->get_param_int("levels") + 1; + const gchar *effecttype = ext->get_param_optiongroup("type"); + if (levels == 1) { + if ((g_ascii_strcasecmp("dented", effecttype) == 0)) { + transf << " 1 0 1"; + } else { + transf << " 1"; + } + } else { + for ( int step = 1 ; step <= levels ; step++ ) { + float val = (float) step / levels; + transf << " " << val; + if ((g_ascii_strcasecmp("dented", effecttype) == 0)) { + transf << " " << (val - ((float) 1 / (3 * levels))) << " " << (val + ((float) 1 / (2 * levels))); + } + } + } + transf << " 1"; + + if (ext->get_param_bool("antialiasing")) + antialias << "0.5"; + else + antialias << "0.01"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", blur1.str().c_str(), blur2.str().c_str(), blendmode.str().c_str(), presat.str().c_str(), table.str().c_str(), transf.str().c_str(), table.str().c_str(), transf.str().c_str(), table.str().c_str(), transf.str().c_str(), postsat.str().c_str(), antialias.str().c_str()); + + return _filter; +}; /* Posterize filter */ + +/** + \brief Custom predefined Posterize basic filter. + + Simple posterizing effect + + Filter's parameters: + * Levels (0->20, default 5) -> component1 (tableValues) + * Blur (0.01->20., default 4.) -> blur1 (stdDeviation) +*/ +class PosterizeBasic : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + PosterizeBasic ( ) : Filter() { }; + ~PosterizeBasic ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Posterize Basic") "\n" + "org.inkscape.effect.filter.PosterizeBasic\n" + "5\n" + "4.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Simple posterizing effect") "\n" + "\n" + "\n", new PosterizeBasic()); + }; +}; + +gchar const * +PosterizeBasic::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream transf; + + blur << ext->get_param_float("blur"); + + transf << "0"; + int levels = ext->get_param_int("levels") + 1; + for ( int step = 1 ; step <= levels ; step++ ) { + const float val = (float) step / levels; + transf << " " << val; + } + transf << " 1"; + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", blur.str().c_str(), transf.str().c_str(), transf.str().c_str(), transf.str().c_str()); + + return _filter; +}; /* PosterizeBasic filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'PAINT' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ */ diff --git a/src/extension/internal/filter/protrusions.h b/src/extension/internal/filter/protrusions.h new file mode 100644 index 0000000..590608d --- /dev/null +++ b/src/extension/internal/filter/protrusions.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ +/* Change the 'PROTRUSIONS' above to be your file name */ + +/* + * Copyright (C) 2008 Authors: + * Ted Gould + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Protrusion filters + * Snow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +/** + \brief Custom predefined Snow filter. + + Snow has fallen on object. +*/ +class Snow : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Snow ( ) : Filter() { }; + ~Snow ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + +public: + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Snow Crest") "\n" + "org.inkscape.effect.filter.snow\n" + "3.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Snow has fallen on object") "\n" + "\n" + "\n", new Snow()); + }; + +}; + +gchar const * +Snow::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream drift; + drift << ext->get_param_float("drift"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", drift.str().c_str()); + + return _filter; +}; /* Snow filter */ + + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'PROTRUSIONS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ */ diff --git a/src/extension/internal/filter/shadows.h b/src/extension/internal/filter/shadows.h new file mode 100644 index 0000000..d8aa69e --- /dev/null +++ b/src/extension/internal/filter/shadows.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ +/* Change the 'SHADOWS' above to be your file name */ + +/* + * Copyright (C) 2013 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Shadow filters + * Drop shadow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Drop shadow filter. + + Colorizable Drop shadow. + + Filter's parameters: + * Blur radius (0.->200., default 3) -> blur (stdDeviation) + * Horizontal offset (-50.->50., default 6.0) -> offset (dx) + * Vertical offset (-50.->50., default 6.0) -> offset (dy) + * Blur type (enum, default outer) -> + outer = composite1 (operator="in"), composite2 (operator="over", in1="SourceGraphic", in2="offset") + inner = composite1 (operator="out"), composite2 (operator="atop", in1="offset", in2="SourceGraphic") + innercut = composite1 (operator="in"), composite2 (operator="out", in1="offset", in2="SourceGraphic") + outercut = composite1 (operator="out"), composite2 (operator="in", in1="SourceGraphic", in2="offset") + shadow = composite1 (operator="out"), composite2 (operator="atop", in1="offset", in2="offset") + * Color (guint, default 0,0,0,127) -> flood (flood-opacity, flood-color) + * Use object's color (boolean, default false) -> composite1 (in1, in2) +*/ +class ColorizableDropShadow : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorizableDropShadow ( ) : Filter() { }; + ~ColorizableDropShadow ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Drop Shadow") "\n" + "org.inkscape.effect.filter.ColorDropShadow\n" + "\n" + "\n" + "3.0\n" + "6.0\n" + "6.0\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "127\n" + "false\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Colorizable Drop shadow") "\n" + "\n" + "\n", new ColorizableDropShadow()); + }; + +}; + +gchar const * +ColorizableDropShadow::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream x; + std::ostringstream y; + std::ostringstream comp1in1; + std::ostringstream comp1in2; + std::ostringstream comp1op; + std::ostringstream comp2in1; + std::ostringstream comp2in2; + std::ostringstream comp2op; + + const gchar *type = ext->get_param_optiongroup("type"); + guint32 color = ext->get_param_color("color"); + + blur << ext->get_param_float("blur"); + x << ext->get_param_float("xoffset"); + y << ext->get_param_float("yoffset"); + a << (color & 0xff) / 255.0F; + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + + // Select object or user-defined color + if ((g_ascii_strcasecmp("innercut", type) == 0)) { + if (ext->get_param_bool("objcolor")) { + comp2in1 << "SourceGraphic"; + comp2in2 << "offset"; + } else { + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } + } else { + if (ext->get_param_bool("objcolor")) { + comp1in1 << "SourceGraphic"; + comp1in2 << "flood"; + } else { + comp1in1 << "flood"; + comp1in2 << "SourceGraphic"; + } + } + + // Shadow mode + if ((g_ascii_strcasecmp("outer", type) == 0)) { + comp1op << "in"; + comp2op << "over"; + comp2in1 << "SourceGraphic"; + comp2in2 << "offset"; + } else if ((g_ascii_strcasecmp("inner", type) == 0)) { + comp1op << "out"; + comp2op << "atop"; + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("outercut", type) == 0)) { + comp1op << "in"; + comp2op << "out"; + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("innercut", type) == 0)){ + comp1op << "out"; + comp1in1 << "flood"; + comp1in2 << "SourceGraphic"; + comp2op << "in"; + } else { //shadow + comp1op << "in"; + comp2op << "atop"; + comp2in1 << "offset"; + comp2in2 << "offset"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + comp1in1.str().c_str(), comp1in2.str().c_str(), comp1op.str().c_str(), + blur.str().c_str(), x.str().c_str(), y.str().c_str(), + comp2in1.str().c_str(), comp2in2.str().c_str(), comp2op.str().c_str()); + + return _filter; +}; /* Drop shadow filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'SHADOWS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ */ diff --git a/src/extension/internal/filter/textures.h b/src/extension/internal/filter/textures.h new file mode 100644 index 0000000..45a78a3 --- /dev/null +++ b/src/extension/internal/filter/textures.h @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ +/* Change the 'TEXTURES' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Protrusion filters + * Ink blot + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +/** + \brief Custom predefined Ink Blot filter. + + Inkblot on tissue or rough paper. + + Filter's parameters: + + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Frequency (0.001->1., default 0.04) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->100, default 0) -> turbulence (seed) + * Horizontal inlay (0.01->30., default 10) -> blur1 (stdDeviation x) + * Vertical inlay (0.01->30., default 10) -> blur1 (stdDeviation y) + * Displacement (0.->100., default 50) -> map (scale) + * Blend (0.01->30., default 5) -> blur2 (stdDeviation) + * Stroke (enum, default over) -> composite (operator) + * Arithmetic stroke options + * k1 (-10.->10., default 1.5) + * k2 (-10.->10., default -0.25) + * k3 (-10.->10., default 0.5) +*/ +class InkBlot : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + InkBlot ( ) : Filter() { }; + ~InkBlot ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + +public: + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Ink Blot") "\n" + "org.inkscape.effect.filter.InkBlot\n" + "\n" + "\n" + "\n" + "\n" + "4\n" + "3\n" + "0\n" + "10\n" + "10\n" + "50\n" + "5\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "1.5\n" + "-0.25\n" + "0.5\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Inkblot on tissue or rough paper") "\n" + "\n" + "\n", new InkBlot()); + }; + +}; + +gchar const * +InkBlot::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream freq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream displacement; + std::ostringstream blend; + std::ostringstream stroke; + std::ostringstream custom; + + type << ext->get_param_optiongroup("type"); + freq << ext->get_param_float("freq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + displacement << ext->get_param_float("displacement"); + blend << ext->get_param_float("blend"); + + const gchar *ope = ext->get_param_optiongroup("stroke"); + if (g_ascii_strcasecmp("arithmetic", ope) == 0) { + custom << "k1=\"" << ext->get_param_float("k1") << "\" k2=\"" << ext->get_param_float("k2") << "\" k3=\"" << ext->get_param_float("k3") << "\""; + } else { + custom << ""; + } + + stroke << ext->get_param_optiongroup("stroke"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n", hblur.str().c_str(), vblur.str().c_str(), type.str().c_str(), + freq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), + displacement.str().c_str(), blend.str().c_str(), + custom.str().c_str(), stroke.str().c_str() ); + + return _filter; + +}; /* Ink Blot filter */ + + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'TEXTURES' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ */ diff --git a/src/extension/internal/filter/transparency.h b/src/extension/internal/filter/transparency.h new file mode 100644 index 0000000..87a4c88 --- /dev/null +++ b/src/extension/internal/filter/transparency.h @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ +/* Change the 'TRANSPARENCY' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) + * + * Fill and transparency filters + * Blend + * Channel transparency + * Light eraser + * Opacity + * Silhouette + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Blend filter. + + Blend objects with background images or with themselves + + Filter's parameters: + * Source (enum [SourceGraphic,BackgroundImage], default BackgroundImage) -> blend (in2) + * Mode (enum, all blend modes, default Multiply) -> blend (mode) +*/ + +class Blend : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Blend ( ) : Filter() { }; + ~Blend ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Blend") "\n" + "org.inkscape.effect.filter.Blend\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Blend objects with background images or with themselves") "\n" + "\n" + "\n", new Blend()); + }; + +}; + +gchar const * +Blend::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream source; + std::ostringstream mode; + + source << ext->get_param_optiongroup("source"); + mode << ext->get_param_optiongroup("mode"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n", source.str().c_str(), mode.str().c_str() ); + + return _filter; +}; /* Blend filter */ + +/** + \brief Custom predefined Channel transparency filter. + + Channel transparency filter. + + Filter's parameters: + * Saturation (0.->1., default 1.) -> colormatrix1 (values) + * Red (-10.->10., default -1.) -> colormatrix2 (values) + * Green (-10.->10., default 0.5) -> colormatrix2 (values) + * Blue (-10.->10., default 0.5) -> colormatrix2 (values) + * Alpha (-10.->10., default 1.) -> colormatrix2 (values) + * Flood colors (guint, default 16777215) -> flood (flood-opacity, flood-color) + * Inverted (boolean, default false) -> composite1 (operator, true='in', false='out') + + Matrix: + 1 0 0 0 0 + 0 1 0 0 0 + 0 0 1 0 0 + R G B A 0 +*/ +class ChannelTransparency : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ChannelTransparency ( ) : Filter() { }; + ~ChannelTransparency ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Channel Transparency") "\n" + "org.inkscape.effect.filter.ChannelTransparency\n" + "-1\n" + "0.5\n" + "0.5\n" + "1\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Replace RGB with transparency") "\n" + "\n" + "\n", new ChannelTransparency()); + }; +}; + +gchar const * +ChannelTransparency::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream alpha; + std::ostringstream invert; + + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + alpha << ext->get_param_float("alpha"); + + if (!ext->get_param_bool("invert")) { + invert << "in"; + } else { + invert << "xor"; + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", red.str().c_str(), green.str().c_str(), blue.str().c_str(), alpha.str().c_str(), + invert.str().c_str()); + + return _filter; +}; /* Channel transparency filter */ + +/** + \brief Custom predefined LightEraser filter. + + Make the lightest parts of the object progressively transparent. + + Filter's parameters: + * Expansion (0.->1000., default 50) -> colormatrix (4th value, multiplicator) + * Erosion (1.->1000., default 100) -> colormatrix (first 3 values, multiplicator) + * Global opacity (0.->1., default 1.) -> composite (k2) + * Inverted (boolean, default false) -> colormatrix (values, true: first 3 values positive, 4th negative) + +*/ +class LightEraser : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + LightEraser ( ) : Filter() { }; + ~LightEraser ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Light Eraser") "\n" + "org.inkscape.effect.filter.LightEraser\n" + "50\n" + "100\n" + "1\n" + "false\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Make the lightest parts of the object progressively transparent") "\n" + "\n" + "\n", new LightEraser()); + }; +}; + +gchar const * +LightEraser::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream expand; + std::ostringstream erode; + std::ostringstream opacity; + + opacity << ext->get_param_float("opacity"); + + if (ext->get_param_bool("invert")) { + expand << (ext->get_param_float("erode") * 0.2125) << " " + << (ext->get_param_float("erode") * 0.7154) << " " + << (ext->get_param_float("erode") * 0.0721); + erode << (-ext->get_param_float("expand")); + } else { + expand << (-ext->get_param_float("erode") * 0.2125) << " " + << (-ext->get_param_float("erode") * 0.7154) << " " + << (-ext->get_param_float("erode") * 0.0721); + erode << ext->get_param_float("expand"); + } + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", expand.str().c_str(), erode.str().c_str(), opacity.str().c_str()); + + return _filter; +}; /* Light Eraser filter */ + + +/** + \brief Custom predefined Opacity filter. + + Set opacity and strength of opacity boundaries. + + Filter's parameters: + * Expansion (0.->1000., default 5) -> colormatrix (last-1th value) + * Erosion (0.->1000., default 1) -> colormatrix (last value) + * Global opacity (0.->1., default 1.) -> composite (k2) + +*/ +class Opacity : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Opacity ( ) : Filter() { }; + ~Opacity ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Opacity") "\n" + "org.inkscape.effect.filter.Opacity\n" + "5\n" + "1\n" + "1\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Set opacity and strength of opacity boundaries") "\n" + "\n" + "\n", new Opacity()); + }; +}; + +gchar const * +Opacity::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream matrix; + std::ostringstream opacity; + + opacity << ext->get_param_float("opacity"); + + matrix << (ext->get_param_float("expand")) << " " + << (-ext->get_param_float("erode")); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n", matrix.str().c_str(), opacity.str().c_str()); + + return _filter; +}; /* Opacity filter */ + +/** + \brief Custom predefined Silhouette filter. + + Repaint anything visible monochrome + + Filter's parameters: + * Blur (0.01->50., default 0.01) -> blur (stdDeviation) + * Cutout (boolean, default False) -> composite (false=in, true=out) + * Color (guint, default 0,0,0,255) -> flood (flood-color, flood-opacity) +*/ + +class Silhouette : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Silhouette ( ) : Filter() { }; + ~Silhouette ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Silhouette") "\n" + "org.inkscape.effect.filter.Silhouette\n" + "0.01\n" + "false\n" + "255\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Repaint anything visible monochrome") "\n" + "\n" + "\n", new Silhouette()); + }; + +}; + +gchar const * +Silhouette::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream cutout; + std::ostringstream blur; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + if (ext->get_param_bool("cutout")) + cutout << "out"; + else + cutout << "in"; + blur << ext->get_param_float("blur"); + + _filter = g_strdup_printf( + "\n" + "\n" + "\n" + "\n" + "\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), cutout.str().c_str(), blur.str().c_str()); + + return _filter; +}; /* Silhouette filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'TRANSPARENCY' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ */ diff --git a/src/extension/internal/gdkpixbuf-input.cpp b/src/extension/internal/gdkpixbuf-input.cpp new file mode 100644 index 0000000..08555fb --- /dev/null +++ b/src/extension/internal/gdkpixbuf-input.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include +#include +#include + +#include "document.h" +#include "document-undo.h" +#include "gdkpixbuf-input.h" +#include "image-resolution.h" +#include "preferences.h" +#include "selection-chemistry.h" + +#include "display/cairo-utils.h" + +#include "extension/input.h" +#include "extension/system.h" + +#include "io/dir-util.h" + +#include "object/sp-image.h" +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { + +namespace Extension { +namespace Internal { + +SPDocument * +GdkpixbufInput::open(Inkscape::Extension::Input *mod, char const *uri) +{ + // Determine whether the image should be embedded + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool ask = prefs->getBool( "/dialogs/import/ask"); + bool forcexdpi = prefs->getBool( "/dialogs/import/forcexdpi"); + Glib::ustring link = prefs->getString("/dialogs/import/link"); + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + + // If we asked about import preferences, get values and update preferences. + if (ask) { + ask = !mod->get_param_bool("do_not_ask"); + forcexdpi = (strcmp(mod->get_param_optiongroup("dpi"), "from_default") == 0); + link = mod->get_param_optiongroup("link"); + scale = mod->get_param_optiongroup("scale"); + + prefs->setBool( "/dialogs/import/ask", ask ); + prefs->setBool( "/dialogs/import/forcexdpi", forcexdpi); + prefs->setString("/dialogs/import/link", link ); + prefs->setString("/dialogs/import/scale", scale ); + } + + bool embed = (link == "embed"); + + SPDocument *doc = nullptr; + std::unique_ptr pb(Inkscape::Pixbuf::create_from_file(uri)); + + // TODO: the pixbuf is created again from the base64-encoded attribute in SPImage. + // Find a way to create the pixbuf only once. + + if (pb) { + doc = SPDocument::createNewDoc(nullptr, TRUE, TRUE); + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // no need to undo in this temporary document + + double width = pb->width(); + double height = pb->height(); + double defaultxdpi = prefs->getDouble("/dialogs/import/defaultxdpi/value", Inkscape::Util::Quantity::convert(1, "in", "px")); + + ImageResolution *ir = nullptr; + double xscale = 1; + double yscale = 1; + + + if (!ir && !forcexdpi) { + ir = new ImageResolution(uri); + } + if (ir && ir->ok()) { + xscale = 960.0 / round(10.*ir->x()); // round-off to 0.1 dpi + yscale = 960.0 / round(10.*ir->y()); + // prevent crash on image with too small dpi (bug 1479193) + if (ir->x() <= .05) + xscale = 960.0; + if (ir->y() <= .05) + yscale = 960.0; + } else { + xscale = 96.0 / defaultxdpi; + yscale = 96.0 / defaultxdpi; + } + + width *= xscale; + height *= yscale; + + delete ir; // deleting NULL is safe + + // Create image node + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *image_node = xml_doc->createElement("svg:image"); + sp_repr_set_svg_double(image_node, "width", width); + sp_repr_set_svg_double(image_node, "height", height); + + // Set default value as we honor "preserveAspectRatio". + image_node->setAttribute("preserveAspectRatio", "none"); + + // This is actually 'image-rendering'. + if( scale.compare( "auto" ) != 0 ) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + sp_repr_css_set(image_node, css, "style"); + sp_repr_css_attr_unref( css ); + } + + if (embed) { + sp_embed_image(image_node, pb.get()); + } else { + // convert filename to uri + gchar* _uri = g_filename_to_uri(uri, nullptr, nullptr); + if(_uri) { + image_node->setAttribute("xlink:href", _uri); + g_free(_uri); + } else { + image_node->setAttribute("xlink:href", uri); + } + } + + // Add it to the current layer + Inkscape::XML::Node *layer_node = xml_doc->createElement("svg:g"); + layer_node->setAttribute("inkscape:groupmode", "layer"); + layer_node->setAttribute("inkscape:label", "Image"); + doc->getRoot()->appendChildRepr(layer_node); + layer_node->appendChild(image_node); + Inkscape::GC::release(image_node); + Inkscape::GC::release(layer_node); + fit_canvas_to_drawing(doc); + + // Set viewBox if it doesn't exist + if (!doc->getRoot()->viewBox_set) { + // std::cerr << "Viewbox not set, setting" << std::endl; + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + // restore undo, as now this document may be shown to the user if a bitmap was opened + DocumentUndo::setUndoSensitive(doc, saved); + } else { + printf("GdkPixbuf loader failed\n"); + } + + return doc; +} + +#include "clear-n_.h" + +void +GdkpixbufInput::init() +{ + static std::vector< Gdk::PixbufFormat > formatlist = Gdk::Pixbuf::get_formats(); + for (auto i: formatlist) { + GdkPixbufFormat *pixformat = i.gobj(); + + gchar *name = gdk_pixbuf_format_get_name(pixformat); + gchar *description = gdk_pixbuf_format_get_description(pixformat); + gchar **extensions = gdk_pixbuf_format_get_extensions(pixformat); + gchar **mimetypes = gdk_pixbuf_format_get_mime_types(pixformat); + + for (int i = 0; extensions[i] != nullptr; i++) { + for (int j = 0; mimetypes[j] != nullptr; j++) { + + /* thanks but no thanks, we'll handle SVG extensions... */ + if (strcmp(extensions[i], "svg") == 0) { + continue; + } + if (strcmp(extensions[i], "svgz") == 0) { + continue; + } + if (strcmp(extensions[i], "svg.gz") == 0) { + continue; + } + gchar *caption = g_strdup_printf(_("%s bitmap image import"), name); + + gchar *xmlString = g_strdup_printf( + "\n" + "%s\n" + "org.inkscape.input.gdkpixbuf.%s\n" + + "\n" + "\n" + "\n" + "\n" + + "\n" + "\n" + "\n" + "\n" + + "\n" + "\n" + "\n" + "\n" + "\n" + + "false\n" + "\n" + ".%s\n" + "%s\n" + "%s (*.%s)\n" + "%s\n" + "\n" + "", + caption, + extensions[i], + extensions[i], + mimetypes[j], + name, + extensions[i], + description + ); + + Inkscape::Extension::build_from_mem(xmlString, new GdkpixbufInput()); + g_free(xmlString); + g_free(caption); + }} + + g_free(name); + g_free(description); + g_strfreev(mimetypes); + g_strfreev(extensions); + } +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/gdkpixbuf-input.h b/src/extension/internal/gdkpixbuf-input.h new file mode 100644 index 0000000..1980700 --- /dev/null +++ b/src/extension/internal/gdkpixbuf-input.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H +#define INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class GdkpixbufInput : Inkscape::Extension::Implementation::Implementation { +public: + SPDocument *open(Inkscape::Extension::Input *mod, + char const *uri) override; + static void init(); +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/gimpgrad.cpp b/src/extension/internal/gimpgrad.cpp new file mode 100644 index 0000000..0ecef60 --- /dev/null +++ b/src/extension/internal/gimpgrad.cpp @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Inkscape::Extension::Internal::GimpGrad implementation + */ + +/* + * Authors: + * Ted Gould + * Abhishek Sharma + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "io/sys.h" +#include "extension/system.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "gimpgrad.h" +#include "streq.h" +#include "strneq.h" +#include "document.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocate anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool GimpGrad::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm loading!\n" << std::endl; + return TRUE; +} + +/** + \brief A function to remove what was allocated + \param module Unused + \return None +*/ +void GimpGrad::unload (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Nooo! I'm being unloaded!" << std::endl; + return; +} + +static void append_css_num(Glib::ustring &str, double const num) +{ + CSSOStringStream stream; + stream << num; + str += stream.str(); +} + +/** + \brief A function to turn a color into a gradient stop + \param in_color The color for the stop + \param location Where the stop is placed in the gradient + \return The text that is the stop. Full SVG containing the element. + + This function encapsulates all of the translation of the ColorRGBA + and the location into the gradient. It is really pretty simple except + that the ColorRGBA is in floats that are 0 to 1 and the SVG wants + hex values from 0 to 255 for color. Otherwise mostly this is just + turning the values into strings and returning it. +*/ +static Glib::ustring stop_svg(ColorRGBA const in_color, double const location) +{ + Glib::ustring ret("\n"; + return ret; +} + +/** + \brief Actually open the gradient and turn it into an SPDocument + \param module The input module being used + \param filename The filename of the gradient to be opened + \return A Document with the gradient in it. + + GIMP gradients are pretty simple (atleast the newer format, this + function does not handle the old one yet). They start out with + the like "GIMP Gradient", then name it, and tell how many entries + there are. This function currently ignores the name and the number + of entries just reading until it fails. + + The other small piece of trickery here is that GIMP gradients define + a left position, right position and middle position. SVG gradients + have no middle position in them. In order to handle this case the + left and right colors are averaged in a linear manner and the middle + position is used for that color. + + That is another point, the GIMP gradients support many different types + of gradients -- linear being the most simple. This plugin assumes + that they are all linear. Most GIMP gradients are done this way, + but it is possible to encounter more complex ones -- which won't be + handled correctly. + + The one optimization that this plugin makes that if the right side + of the previous segment is the same color as the left side of the + current segment, then the second one is dropped. This is often + done in GIMP gradients and they are not necissary in SVG. + + What this function does is build up an SVG document with a single + linear gradient in it with all the stops of the colors in the GIMP + gradient that is passed in. This document is then turned into a + document using the \c sp_document_from_mem. That is then returned + to Inkscape. +*/ +SPDocument * +GimpGrad::open (Inkscape::Extension::Input */*module*/, gchar const *filename) +{ + Inkscape::IO::dump_fopen_call(filename, "I"); + FILE *gradient = Inkscape::IO::fopen_utf8name(filename, "r"); + if (gradient == nullptr) { + return nullptr; + } + + { + char tempstr[1024]; + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the read failed" << std::endl; + goto error; + } + if (!streq(tempstr, "GIMP Gradient\n")) { + // std::cout << "This doesn't appear to be a GIMP gradient" << std::endl; + goto error; + } + + /* Name field. */ + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the second read failed" << std::endl; + goto error; + } + if (!strneq(tempstr, "Name: ", 6)) { + goto error; + } + /* Handle very long names. (And also handle nul bytes gracefully: don't use strlen.) */ + while (memchr(tempstr, '\n', sizeof(tempstr) - 1) == nullptr) { + if (fgets(tempstr, sizeof(tempstr), gradient) == nullptr) { + goto error; + } + } + + /* n. segments */ + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the third read failed" << std::endl; + goto error; + } + char *endptr = nullptr; + long const n_segs = strtol(tempstr, &endptr, 10); + if ((*endptr != '\n') + || n_segs < 1) { + /* SVG gradients are allowed to have zero stops (treated as `none'), but gimp 2.2 + * requires at least one segment (i.e. at least two stops) (see gimp_gradient_load in + * gimpgradient-load.c). We try to use the same error handling as gimp, so that + * .ggr files that work in one program work in both programs. */ + goto error; + } + + ColorRGBA prev_color(-1.0, -1.0, -1.0, -1.0); + Glib::ustring outsvg("\n"); + long n_segs_found = 0; + double prev_right = 0.0; + while (fgets(tempstr, 1024, gradient) != nullptr) { + double dbls[3 + 4 + 4]; + gchar *p = tempstr; + for (double & dbl : dbls) { + gchar *end = nullptr; + double const xi = g_ascii_strtod(p, &end); + if (!end || end == p || !g_ascii_isspace(*end)) { + goto error; + } + if (xi < 0 || 1 < xi) { + goto error; + } + dbl = xi; + p = end + 1; + } + + double const left = dbls[0]; + if (left != prev_right) { + goto error; + } + double const middle = dbls[1]; + if (!(left <= middle)) { + goto error; + } + double const right = dbls[2]; + if (!(middle <= right)) { + goto error; + } + + ColorRGBA const leftcolor(dbls[3], dbls[4], dbls[5], dbls[6]); + ColorRGBA const rightcolor(dbls[7], dbls[8], dbls[9], dbls[10]); + g_assert(11 == G_N_ELEMENTS(dbls)); + + /* Interpolation enums: curve shape and colour space. */ + { + /* TODO: Currently we silently ignore type & color, assuming linear interpolation in + * sRGB space (or whatever the default in SVG is). See gimp/app/core/gimpgradient.c + * for how gimp uses these. We could use gimp functions to sample at a few points, and + * add some intermediate stops to convert to the linear/sRGB interpolation */ + int type; /* enum: linear, curved, sine, sphere increasing, sphere decreasing. */ + int color_interpolation; /* enum: rgb, hsv anticlockwise, hsv clockwise. */ + if (sscanf(p, "%8d %8d", &type, &color_interpolation) != 2) { + continue; + } + } + + if (prev_color != leftcolor) { + outsvg += stop_svg(leftcolor, left); + } + if (fabs(middle - .5 * (left + right)) > 1e-4) { + outsvg += stop_svg(leftcolor.average(rightcolor), middle); + } + outsvg += stop_svg(rightcolor, right); + + prev_color = rightcolor; + prev_right = right; + ++n_segs_found; + } + if (prev_right != 1.0) { + goto error; + } + + if (n_segs_found != n_segs) { + goto error; + } + + outsvg += ""; + + // std::cout << "SVG Output: " << outsvg << std::endl; + + fclose(gradient); + + return SPDocument::createNewDocFromMem(outsvg.c_str(), outsvg.length(), TRUE); + } + +error: + fclose(gradient); + return nullptr; +} + +#include "clear-n_.h" + +void GimpGrad::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("GIMP Gradients") "\n" + "org.inkscape.input.gimpgrad\n" + "\n" + ".ggr\n" + "application/x-gimp-gradient\n" + "" N_("GIMP Gradient (*.ggr)") "\n" + "" N_("Gradients used in GIMP") "\n" + "\n" + "\n", new GimpGrad()); + return; +} + +} } } /* namespace Internal; Extension; Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/gimpgrad.h b/src/extension/internal/gimpgrad.h new file mode 100644 index 0000000..8daadef --- /dev/null +++ b/src/extension/internal/gimpgrad.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +// TODO add include guard +#include + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Extension; + +namespace Internal { + +/** + * Implementation class of the GIMP gradient plugin. + * This mostly just creates a namespace for the GIMP gradient plugin today. + */ +class GimpGrad : public Inkscape::Extension::Implementation::Implementation +{ +public: + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + SPDocument *open(Inkscape::Extension::Input *module, gchar const *filename) override; + + static void init(); +}; + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/grid.cpp b/src/extension/internal/grid.cpp new file mode 100644 index 0000000..a4fb576 --- /dev/null +++ b/src/extension/internal/grid.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file grid.cpp + + A plug-in to add a grid creation effect into Inkscape. +*/ +/* + * Copyright (C) 2004-2005 Ted Gould + * Copyright (C) 2007 MenTaLguY + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "desktop.h" + +#include "document.h" +#include "selection.h" +#include "2geom/geom.h" + +#include "svg/path-string.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "util/units.h" + +#include "grid.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +Grid::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Grid, I'm loading!" << std::endl; + return TRUE; +} + +namespace { + +Glib::ustring build_lines(Geom::Rect bounding_area, + Geom::Point const &offset, Geom::Point const &spacing) +{ + Geom::Point point_offset(0.0, 0.0); + + SVG::PathString path_data; + + for ( int axis = Geom::X ; axis <= Geom::Y ; ++axis ) { + point_offset[axis] = offset[axis]; + + for (Geom::Point start_point = bounding_area.min(); + start_point[axis] + offset[axis] <= (bounding_area.max())[axis]; + start_point[axis] += spacing[axis]) { + Geom::Point end_point = start_point; + end_point[1-axis] = (bounding_area.max())[1-axis]; + + path_data.moveTo(start_point + point_offset) + .lineTo(end_point + point_offset); + } + } + // std::cout << "Path data:" << path_data.c_str() << std::endl; + return path_data; +} + +} // namespace + +/** + \brief This actually draws the grid. + \param module The effect that was called (unused) + \param document What should be edited. +*/ +void +Grid::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + Inkscape::Selection * selection = ((SPDesktop *)document)->selection; + + Geom::Rect bounding_area = Geom::Rect(Geom::Point(0,0), Geom::Point(100,100)); + if (selection->isEmpty()) { + /* get page size */ + SPDocument * doc = document->doc(); + bounding_area = Geom::Rect( Geom::Point(0,0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px")) ); + } else { + Geom::OptRect bounds = selection->visualBounds(); + if (bounds) { + bounding_area = *bounds; + } + + Geom::Rect temprec = bounding_area * static_cast(document)->doc2dt(); + + bounding_area = temprec; + } + + double scale = document->doc()->getDocumentScale().inverse()[Geom::X]; + + bounding_area *= Geom::Scale(scale); + Geom::Point spacings( scale * module->get_param_float("xspacing"), + scale * module->get_param_float("yspacing") ); + gdouble line_width = scale * module->get_param_float("lineWidth"); + Geom::Point offsets( scale * module->get_param_float("xoffset"), + scale * module->get_param_float("yoffset") ); + + Glib::ustring path_data(""); + + path_data = build_lines(bounding_area, offsets, spacings); + Inkscape::XML::Document * xml_doc = document->doc()->getReprDoc(); + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node * current_layer = static_cast(document)->currentLayer()->getRepr(); + Inkscape::XML::Node * path = xml_doc->createElement("svg:path"); + + path->setAttribute("d", path_data); + + std::ostringstream stringstream; + stringstream << "fill:none;stroke:#000000;stroke-width:" << line_width << "px"; + path->setAttribute("style", stringstream.str()); + + current_layer->appendChild(path); + Inkscape::GC::release(path); +} + +/** \brief A class to make an adjustment that uses Extension params */ +class PrefAdjustment : public Gtk::Adjustment { + /** Extension that this relates to */ + Inkscape::Extension::Extension * _ext; + /** The string which represents the parameter */ + char * _pref; +public: + /** \brief Make the adjustment using an extension and the string + describing the parameter. */ + PrefAdjustment(Inkscape::Extension::Extension * ext, char * pref) : + Gtk::Adjustment(0.0, 0.0, 10.0, 0.1), _ext(ext), _pref(pref) { + this->set_value(_ext->get_param_float(_pref)); + this->signal_value_changed().connect(sigc::mem_fun(this, &PrefAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class PrefAdjustment */ + +/** \brief A function to respond to the value_changed signal from the + adjustment. + + This function just grabs the value from the adjustment and writes + it to the parameter. Very simple, but yet beautiful. +*/ +void +PrefAdjustment::val_changed () +{ + // std::cout << "Value Changed to: " << this->get_value() << std::endl; + _ext->set_param_float(_pref, this->get_value()); + return; +} + +/** \brief A function to get the prefences for the grid + \param moudule Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +Grid::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = nullptr; + if (!selected.empty()) { + first_select = selected.front()->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + +#include "clear-n_.h" + +void +Grid::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("Grid") "\n" + "org.inkscape.effect.grid\n" + "1.0\n" + "10.0\n" + "10.0\n" + "0.0\n" + "0.0\n" + "\n" + "all\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "" N_("Draw a path which is a grid") "\n" + "\n" + "\n", new Grid()); + return; +} + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/grid.h b/src/extension/internal/grid.h new file mode 100644 index 0000000..16f4cd7 --- /dev/null +++ b/src/extension/internal/grid.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class Grid : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void init (); +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/image-resolution.cpp b/src/extension/internal/image-resolution.cpp new file mode 100644 index 0000000..3ca596c --- /dev/null +++ b/src/extension/internal/image-resolution.cpp @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Daniel Wagenaar + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "util/units.h" +#include "image-resolution.h" + +#define IR_TRY_PNG 1 +#include + +#ifdef HAVE_EXIF +#include +#include +#endif + +#define IR_TRY_EXIV 0 + +#ifdef HAVE_JPEG +#define IR_TRY_JFIF 1 +#include +#include +#endif + +#ifdef WITH_MAGICK +#include +#endif + +#define noIMAGE_RESOLUTION_DEBUG + +#ifdef IMAGE_RESOLUTION_DEBUG +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /* */ +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + +ImageResolution::ImageResolution(char const *fn) { + ok_ = false; + + readpng(fn); + if (!ok_) { + readexiv(fn); + } + if (!ok_) { + readjfif(fn); + } + if (!ok_) { + readexif(fn); + } + if (!ok_) { + readmagick(fn); + } +} + +bool ImageResolution::ok() const { + return ok_; +} + +double ImageResolution::x() const { + return x_; +} + +double ImageResolution::y() const { + return y_; +} + + + +#if IR_TRY_PNG + +static bool haspngheader(FILE *fp) { + unsigned char header[8]; + if ( fread(header, 1, 8, fp) != 8 ) { + return false; + } + + fseek(fp, 0, SEEK_SET); + + if (png_sig_cmp(header, 0, 8)) { + return false; + } + + return true; +} + +// Implementation using libpng +void ImageResolution::readpng(char const *fn) { + FILE *fp = fopen(fn, "rb"); + if (!fp) + return; + + if (!haspngheader(fp)) { + fclose(fp); + return; + } + + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) + return; + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fp); + return; + } + + png_init_io(png_ptr, fp); + png_read_info(png_ptr, info_ptr); + + png_uint_32 res_x, res_y; +#ifdef PNG_INCH_CONVERSIONS_SUPPORTED + debug("PNG_INCH_CONVERSIONS_SUPPORTED"); + res_x = png_get_x_pixels_per_inch(png_ptr, info_ptr); + res_y = png_get_y_pixels_per_inch(png_ptr, info_ptr); + if (res_x != 0 && res_y != 0) { + ok_ = true; + x_ = res_x * 1.0; // FIXME: implicit conversion of png_uint_32 to double ok? + y_ = res_y * 1.0; // FIXME: implicit conversion of png_uint_32 to double ok? + } +#else + debug("PNG_RESOLUTION_METER"); + int unit_type; + // FIXME: png_get_pHYs() fails to return expected values + // with clang (based on LLVM 3.2svn) from Xcode 4.6.3 (OS X 10.7.5) + png_get_pHYs(png_ptr, info_ptr, &res_x, &res_y, &unit_type); + + if (unit_type == PNG_RESOLUTION_METER) { + ok_ = true; + x_ = res_x * 2.54 / 100; + y_ = res_y * 2.54 / 100; + } +#endif + + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fp); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} +#else + +// Dummy implementation +void ImageResolution::readpng(char const *) { +} + +#endif + +#if IR_TRY_EXIF + +static double exifDouble(ExifEntry *entry, ExifByteOrder byte_order) { + switch (entry->format) { + case EXIF_FORMAT_BYTE: { + return double(entry->data[0]); + } + case EXIF_FORMAT_SHORT: { + return double(exif_get_short(entry->data, byte_order)); + } + case EXIF_FORMAT_LONG: { + return double(exif_get_long(entry->data, byte_order)); + } + case EXIF_FORMAT_RATIONAL: { + ExifRational r = exif_get_rational(entry->data, byte_order); + return double(r.numerator) / double(r.denominator); + } + case EXIF_FORMAT_SBYTE: { + return double(*(signed char *)entry->data); + } + case EXIF_FORMAT_SSHORT: { + return double(exif_get_sshort(entry->data, byte_order)); + } + case EXIF_FORMAT_SLONG: { + return double(exif_get_slong(entry->data, byte_order)); + } + case EXIF_FORMAT_SRATIONAL: { + ExifSRational r = exif_get_srational(entry->data, byte_order); + return double(r.numerator) / double(r.denominator); + } + case EXIF_FORMAT_FLOAT: { + return double((reinterpret_cast(entry->data))[0]); + } + case EXIF_FORMAT_DOUBLE: { + return (reinterpret_cast(entry->data))[0]; + } + default: { + return nan(0); + } + } +} + +// Implementation using libexif +void ImageResolution::readexif(char const *fn) { + ExifData *ed; + ed = exif_data_new_from_file(fn); + if (!ed) + return; + + ExifByteOrder byte_order = exif_data_get_byte_order(ed); + + ExifEntry *xres = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_X_RESOLUTION); + ExifEntry *yres = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_Y_RESOLUTION); + ExifEntry *unit = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_RESOLUTION_UNIT); + + if ( xres && yres ) { + x_ = exifDouble(xres, byte_order); + y_ = exifDouble(yres, byte_order); + if (unit) { + double u = exifDouble(unit, byte_order); + if ( u == 3 ) { + x_ *= 2.54; + y_ *= 2.54; + } + } + ok_ = true; + } + exif_data_free(ed); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readexif(char const *) { +} + +#endif + +#if IR_TRY_EXIV + +void ImageResolution::readexiv(char const *fn) { + Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fn); + if (!image.get()) + return; + + image->readMetadata(); + Exiv2::ExifData &exifData = image->exifData(); + if (exifData.empty()) + return; + + Exiv2::ExifData::const_iterator end = exifData.end(); + bool havex = false; + bool havey = false; + bool haveunit = false; + int unit; + for (Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i) { + if (ok_) + break; + if ( i->tag() == 0x011a ) { + // X Resolution + x_ = i->toFloat(); + havex = true; + } else if ( i->tag() == 0x011b ) { + // Y Resolution + y_ = i->toFloat(); + havey = true; + } else if ( i->tag() == 0x0128 ) { + unit = i->toLong(); + } + ok_ = havex && havey && haveunit; + } + if (haveunit) { + if ( unit == 3 ) { + x_ *= 2.54; + y_ *= 2.54; + } + } + ok_ = havex && havey; + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readexiv(char const *) { +} + +#endif + +#if IR_TRY_JFIF + +static void irjfif_error_exit(j_common_ptr cinfo) { + longjmp(*(jmp_buf*)cinfo->client_data, 1); +} + +static void irjfif_emit_message(j_common_ptr, int) { +} + +static void irjfif_output_message(j_common_ptr) { +} + +static void irjfif_format_message(j_common_ptr, char *) { +} + +static void irjfif_reset(j_common_ptr) { +} + +void ImageResolution::readjfif(char const *fn) { + FILE *ifd = fopen(fn, "rb"); + if (!ifd) { + return; + } + + struct jpeg_decompress_struct cinfo; + jmp_buf jbuf; + struct jpeg_error_mgr jerr; + + if (setjmp(jbuf)) { + fclose(ifd); + jpeg_destroy_decompress(&cinfo); + return; + } + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jerr.error_exit = &irjfif_error_exit; + jerr.emit_message = &irjfif_emit_message; + jerr.output_message = &irjfif_output_message; + jerr.format_message = &irjfif_format_message; + jerr.reset_error_mgr = &irjfif_reset; + cinfo.client_data = (void*)&jbuf; + + jpeg_stdio_src(&cinfo, ifd); + jpeg_read_header(&cinfo, TRUE); + + debug("cinfo.[XY]_density"); + if (cinfo.saw_JFIF_marker) { // JFIF APP0 marker was seen + if ( cinfo.density_unit == 1 ) { // dots/inch + x_ = cinfo.X_density; + y_ = cinfo.Y_density; + ok_ = true; + } else if ( cinfo.density_unit == 2 ) { // dots/cm + x_ = cinfo.X_density * 2.54; + y_ = cinfo.Y_density * 2.54; + ok_ = true; + } + /* According to http://www.jpeg.org/public/jfif.pdf (page 7): + * "Xdensity and Ydensity should always be non-zero". + * but in some cases, they are (see LP bug #1275443) */ + if (x_ == 0 or y_ == 0) { + ok_ = false; + } + } + jpeg_destroy_decompress(&cinfo); + fclose(ifd); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readjfif(char const *) { +} + +#endif + +#ifdef WITH_MAGICK +void ImageResolution::readmagick(char const *fn) { + Magick::Image image; + debug("Trying image.read"); + try { + image.read(fn); + } catch (Magick::Error & err) { + debug("ImageMagick error: %s", err.what()); + return; + } catch (std::exception & err) { + debug("ImageResolution::readmagick: %s", err.what()); + return; + } + debug("image.[xy]Resolution"); + std::string const type = image.magick(); + x_ = image.xResolution(); + y_ = image.yResolution(); + +// TODO: find out why the hell the following conversion is necessary + if (type == "BMP") { + x_ = Inkscape::Util::Quantity::convert(x_, "in", "cm"); + y_ = Inkscape::Util::Quantity::convert(y_, "in", "cm"); + } + + if (x_ != 0 && y_ != 0) { + ok_ = true; + } + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + debug("Using default Inkscape import resolution"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readmagick(char const *) { +} + +#endif /* WITH_MAGICK */ + +} +} +} diff --git a/src/extension/internal/image-resolution.h b/src/extension/internal/image-resolution.h new file mode 100644 index 0000000..ad69df0 --- /dev/null +++ b/src/extension/internal/image-resolution.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Daniel Wagenaar + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef IMAGE_RESOLUTION_H + +#define IMAGE_RESOLUTION_H + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class ImageResolution { +public: + ImageResolution(char const *fn); + bool ok() const; + double x() const; + double y() const; +private: + bool ok_; + double x_; + double y_; +private: + void readpng(char const *fn); + void readexif(char const *fn); + void readexiv(char const *fn); + void readjfif(char const *fn); + void readmagick(char const *fn); +}; + +} +} +} + +#endif diff --git a/src/extension/internal/latex-pstricks-out.cpp b/src/extension/internal/latex-pstricks-out.cpp new file mode 100644 index 0000000..5bc4e8d --- /dev/null +++ b/src/extension/internal/latex-pstricks-out.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Michael Forbes + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "latex-pstricks-out.h" +#include +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "display/drawing.h" +#include "object/sp-root.h" + + +#include "document.h" + + +namespace Inkscape { +namespace Extension { +namespace Internal { + +LatexOutput::LatexOutput () // The null constructor +{ + return; +} + +LatexOutput::~LatexOutput () //The destructor +{ + return; +} + +bool LatexOutput::check(Inkscape::Extension::Extension * /*module*/) +{ + bool result = Inkscape::Extension::db.get("org.inkscape.print.latex") != nullptr; + return result; +} + + +void LatexOutput::save(Inkscape::Extension::Output * /*mod2*/, SPDocument *doc, gchar const *filename) +{ + SPPrintContext context; + doc->ensureUpToDate(); + + Inkscape::Extension::Print *mod = Inkscape::Extension::get_print(SP_MODULE_KEY_PRINT_LATEX); + const gchar * oldconst = mod->get_param_string("destination"); + gchar * oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + + // Start + context.module = mod; + // fixme: This has to go into module constructor somehow + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = (mod->base)->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + // Print document + mod->begin(doc); + (mod->base)->invoke_print(&context); + mod->finish(); + // Release things + (mod->base)->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // should have been deleted by invoke_hide + // end + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of postscript out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +LatexOutput::init () +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("LaTeX Output") "\n" + "org.inkscape.output.latex\n" + "\n" + ".tex\n" + "text/x-tex\n" + "" N_("LaTeX With PSTricks macros (*.tex)") "\n" + "" N_("LaTeX PSTricks File") "\n" + "\n" + "", new LatexOutput()); + + return; +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:cpp + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-pstricks-out.h b/src/extension/internal/latex-pstricks-out.h new file mode 100644 index 0000000..670904b --- /dev/null +++ b/src/extension/internal/latex-pstricks-out.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Authors: + * Michael Forbes + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_LATEX_OUT_H +#define EXTENSION_INTERNAL_LATEX_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class LatexOutput : Inkscape::Extension::Implementation::Implementation { //This is a derived class + +public: + LatexOutput(); // Empty constructor + + ~LatexOutput() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + static void init();//Initialize the class +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* EXTENSION_INTERNAL_LATEX_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-pstricks.cpp b/src/extension/internal/latex-pstricks.cpp new file mode 100644 index 0000000..bb680e7 --- /dev/null +++ b/src/extension/internal/latex-pstricks.cpp @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LaTeX Printing + * + * Author: + * Michael Forbes + * Abhishek Sharma + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/pathvector.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/curves.h> +#include +#include +#include "util/units.h" +#include "helper/geom-curves.h" + +#include "extension/print.h" +#include "extension/system.h" +#include "inkscape-version.h" +#include "io/sys.h" +#include "latex-pstricks.h" +#include "style.h" +#include "document.h" +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { + +PrintLatex::PrintLatex (): + _width(0), + _height(0), + _stream(nullptr) +{ +} + +PrintLatex::~PrintLatex () +{ + if (_stream) fclose(_stream); + + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + return; +} + +unsigned int PrintLatex::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + +unsigned int PrintLatex::begin (Inkscape::Extension::Print *mod, SPDocument *doc) +{ + Inkscape::SVGOStringStream os; + int res; + FILE *osf = nullptr; + const gchar * fn = nullptr; + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError* error = nullptr; + + os.setf(std::ios::fixed); + fn = mod->get_param_string("destination"); + gchar* local_fn = g_filename_from_utf8( fn, + -1, &bytesRead, &bytesWritten, &error); + fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", fn, strerror(errno)); + g_free(local_fn); + return 0; + } + _stream = osf; + } + + g_free(local_fn); + + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + + res = fprintf(_stream, "%%LaTeX with PSTricks extensions\n"); + /* flush this to test output stream as early as possible */ + if (fflush(_stream)) { + /*g_print("caught error in sp_module_print_plain_begin\n");*/ + if (ferror(_stream)) { + g_print("Error %d on output stream: %s\n", errno, + g_strerror(errno)); + } + g_print("Printing failed\n"); + /* fixme: should use pclose() for pipes */ + fclose(_stream); + _stream = nullptr; + fflush(stdout); + return 0; + } + + // width and height in pt + _width = doc->getWidth().value("pt"); + _height = doc->getHeight().value("pt"); + + if (res >= 0) { + + os << "%%Creator: Inkscape " << Inkscape::version_string << "\n"; + os << "%%Please note this file requires PSTricks extensions\n"; + + os << "\\psset{xunit=.5pt,yunit=.5pt,runit=.5pt}\n"; + // from now on we can output px, but they will be treated as pt + + os << "\\begin{pspicture}(" << doc->getWidth().value("px") << "," << doc->getHeight().value("px") << ")\n"; + } + + m_tr_stack.push( Geom::Scale(1, -1) * Geom::Translate(0, doc->getHeight().value("px"))); /// @fixme hardcoded doc2dt transform + + return fprintf(_stream, "%s", os.str().c_str()); +} + +unsigned int PrintLatex::finish(Inkscape::Extension::Print * /*mod*/) +{ + if (_stream) { + fprintf(_stream, "\\end{pspicture}\n"); + + // Flush stream to be sure. + fflush(_stream); + + fclose(_stream); + _stream = nullptr; + } + return 0; +} + +unsigned int PrintLatex::bind(Inkscape::Extension::Print * /*mod*/, Geom::Affine const &transform, float /*opacity*/) +{ + if (!m_tr_stack.empty()) { + Geom::Affine tr_top = m_tr_stack.top(); + m_tr_stack.push(transform * tr_top); + } else { + m_tr_stack.push(transform); + } + + return 1; +} + +unsigned int PrintLatex::release(Inkscape::Extension::Print * /*mod*/) +{ + m_tr_stack.pop(); + return 1; +} + +unsigned int PrintLatex::comment(Inkscape::Extension::Print * /*mod*/, + const char * comment) +{ + if (!_stream) { + return 0; // XXX: fixme, returning -1 as unsigned. + } + + return fprintf(_stream, "%%! %s\n",comment); +} + +unsigned int PrintLatex::fill(Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const &transform, SPStyle const *style, + Geom::OptRect const & /*pbox*/, Geom::OptRect const & /*dbox*/, Geom::OptRect const & /*bbox*/) +{ + if (!_stream) { + return 0; // XXX: fixme, returning -1 as unsigned. + } + + if (style->fill.isColor()) { + Inkscape::SVGOStringStream os; + float rgb[3]; + float fill_opacity; + + os.setf(std::ios::fixed); + + fill_opacity=SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + style->fill.value.color.get_rgb_floatv(rgb); + os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; + os << "\\pscustom[linestyle=none,fillstyle=solid,fillcolor=curcolor"; + if (fill_opacity!=1.0) { + os << ",opacity="<stroke.isColor()) { + Inkscape::SVGOStringStream os; + float rgb[3]; + float stroke_opacity; + Geom::Affine tr_stack = m_tr_stack.top(); + double const scale = tr_stack.descrim(); + os.setf(std::ios::fixed); + + stroke_opacity=SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + style->stroke.value.color.get_rgb_floatv(rgb); + os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; + + os << "\\pscustom[linewidth=" << style->stroke_width.computed*scale<< ",linecolor=curcolor"; + + if (stroke_opacity!=1.0) { + os<<",strokeopacity="<stroke_dasharray.set && !style->stroke_dasharray.values.empty()) { + os << ",linestyle=dashed,dash="; + for (unsigned i = 0; i < style->stroke_dasharray.values.size(); i++) { + if ((i)) { + os << " "; + } + os << style->stroke_dasharray.values[i].value; + } + } + + os <<"]\n{\n"; + + print_pathvector(os, pathv, transform); + + os << "}\n}\n"; + + fprintf(_stream, "%s", os.str().c_str()); + } + + return 0; +} + +// FIXME: why is 'transform' argument not used? +void +PrintLatex::print_pathvector(SVGOStringStream &os, Geom::PathVector const &pathv_in, const Geom::Affine & /*transform*/) +{ + if (pathv_in.empty()) + return; + +// Geom::Affine tf=transform; // why was this here? + Geom::Affine tf_stack=m_tr_stack.top(); // and why is transform argument not used? + Geom::PathVector pathv = pathv_in * tf_stack; // generates new path, which is a bit slow, but this doesn't have to be performance optimized + + os << "\\newpath\n"; + + for(const auto & it : pathv) { + + os << "\\moveto(" << it.initialPoint()[Geom::X] << "," << it.initialPoint()[Geom::Y] << ")\n"; + + for(Geom::Path::const_iterator cit = it.begin(); cit != it.end_open(); ++cit) { + print_2geomcurve(os, *cit); + } + + if (it.closed()) { + os << "\\closepath\n"; + } + + } +} + +void +PrintLatex::print_2geomcurve(SVGOStringStream &os, Geom::Curve const &c) +{ + using Geom::X; + using Geom::Y; + + if( is_straight_curve(c) ) + { + os << "\\lineto(" << c.finalPoint()[X] << "," << c.finalPoint()[Y] << ")\n"; + } + else if(Geom::CubicBezier const *cubic_bezier = dynamic_cast(&c)) { + std::vector points = cubic_bezier->controlPoints(); + os << "\\curveto(" << points[1][X] << "," << points[1][Y] << ")(" + << points[2][X] << "," << points[2][Y] << ")(" + << points[3][X] << "," << points[3][Y] << ")\n"; + } + else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + for(const auto & iter : sbasis_path) { + print_2geomcurve(os, iter); + } + } +} + +bool +PrintLatex::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +#include "clear-n_.h" + +void PrintLatex::init() +{ + /* SVG in */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("LaTeX Print") "\n" + "" SP_MODULE_KEY_PRINT_LATEX "\n" + "\n" + "true\n" + "\n" + "", new PrintLatex()); +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/extension/internal/latex-pstricks.h b/src/extension/internal/latex-pstricks.h new file mode 100644 index 0000000..37d08b8 --- /dev/null +++ b/src/extension/internal/latex-pstricks.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX_H__ +#define __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX_H__ + +/* + * LaTeX Printing + * + * Author: + * Michael Forbes + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "extension/implementation/implementation.h" +#include "extension/extension.h" + +#include "svg/stringstream.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintLatex : public Inkscape::Extension::Implementation::Implementation { + + float _width; + float _height; + FILE * _stream; + + std::stack m_tr_stack; + + void print_pathvector(SVGOStringStream &os, Geom::PathVector const &pathv_in, const Geom::Affine & /*transform*/); + void print_2geomcurve(SVGOStringStream &os, Geom::Curve const & c ); + +public: + PrintLatex (); + ~PrintLatex () override; + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int bind(Inkscape::Extension::Print *module, Geom::Affine const &transform, float opacity) override; + unsigned int release(Inkscape::Extension::Print *module) override; + + unsigned int fill (Inkscape::Extension::Print *module, Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print *module, Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int comment(Inkscape::Extension::Print *module, const char * comment) override; + bool textToPath (Inkscape::Extension::Print * ext) override; + + static void init (); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX */ + +/* + Local Variables: + mode:cpp + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-text-renderer.cpp b/src/extension/internal/latex-text-renderer.cpp new file mode 100644 index 0000000..732f9c8 --- /dev/null +++ b/src/extension/internal/latex-text-renderer.cpp @@ -0,0 +1,754 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering LaTeX file (pdf/eps/ps+latex output) + * + * The idea stems from GNUPlot's epslatex terminal output :-) + */ +/* + * Authors: + * Johan Engelen + * Miklos Erdelyi + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "latex-text-renderer.h" + +#include +#include + +#include +#include + +#include "libnrtype/Layout-TNG.h" +#include <2geom/transforms.h> +#include <2geom/rect.h> + +#include "object/sp-item.h" +#include "object/sp-item-group.h" +#include "object/sp-root.h" +#include "object/sp-use.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/sp-rect.h" +#include "style.h" + +#include "text-editing.h" + +#include "util/units.h" + +#include "extension/output.h" +#include "extension/system.h" + +#include "inkscape-version.h" +#include "io/sys.h" +#include "document.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + * This method is called by the PDF, EPS and PS output extensions. + * @param filename This should be the filename without '_tex' extension to which the tex code should be written. Output goes to _tex, note the underscore instead of period. + */ +bool +latex_render_document_text_to_file( SPDocument *doc, gchar const *filename, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, float bleedmargin_px, + bool pdflatex) +{ + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = true; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = dynamic_cast(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) + return false; + + /* Create renderer */ + LaTeXTextRenderer *renderer = new LaTeXTextRenderer(pdflatex); + + bool ret = renderer->setTargetFile(filename); + if (ret) { + /* Render document */ + bool ret = renderer->setupDocument(doc, pageBoundingBox, bleedmargin_px, base); + if (ret) { + renderer->renderItem(root); + } + } + + delete renderer; + + return ret; +} + +LaTeXTextRenderer::LaTeXTextRenderer(bool pdflatex) + : _stream(nullptr), + _filename(nullptr), + _pdflatex(pdflatex), + _omittext_state(EMPTY), + _omittext_page(1) +{ + push_transform(Geom::identity()); +} + +LaTeXTextRenderer::~LaTeXTextRenderer() +{ + if (_stream) { + writePostamble(); + + fclose(_stream); + } + + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + + if (_filename) { + g_free(_filename); + } + + return; +} + +/** This should create the output LaTeX file, and assign it to _stream. + * @return Returns true when successful + */ +bool +LaTeXTextRenderer::setTargetFile(gchar const *filename) { + if (filename != nullptr) { + while (isspace(*filename)) filename += 1; + + _filename = g_path_get_basename(filename); + + gchar *filename_ext = g_strdup_printf("%s_tex", filename); + Inkscape::IO::dump_fopen_call(filename_ext, "K"); + FILE *osf = Inkscape::IO::fopen_utf8name(filename_ext, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", filename_ext, strerror(errno)); + g_free(filename_ext); + return false; + } + _stream = osf; + g_free(filename_ext); + } + + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + + fprintf(_stream, "%%%% Creator: Inkscape %s, www.inkscape.org\n", Inkscape::version_string); + fprintf(_stream, "%%%% PDF/EPS/PS + LaTeX output extension by Johan Engelen, 2010\n"); + fprintf(_stream, "%%%% Accompanies image file '%s' (pdf, eps, ps)\n", _filename); + fprintf(_stream, "%%%%\n"); + /* flush this to test output stream as early as possible */ + if (fflush(_stream)) { + if (ferror(_stream)) { + g_print("Error %d on LaTeX file output stream: %s\n", errno, + g_strerror(errno)); + } + g_print("Output to LaTeX file failed\n"); + /* fixme: should use pclose() for pipes */ + fclose(_stream); + _stream = nullptr; + fflush(stdout); + return false; + } + + writePreamble(); + + return true; +} + +static char const preamble[] = +"%% To include the image in your LaTeX document, write\n" +"%% \\input{.pdf_tex}\n" +"%% instead of\n" +"%% \\includegraphics{.pdf}\n" +"%% To scale the image, write\n" +"%% \\def\\svgwidth{}\n" +"%% \\input{.pdf_tex}\n" +"%% instead of\n" +"%% \\includegraphics[width=]{.pdf}\n" +"%%\n" +"%% Images with a different path to the parent latex file can\n" +"%% be accessed with the `import' package (which may need to be\n" +"%% installed) using\n" +"%% \\usepackage{import}\n" +"%% in the preamble, and then including the image with\n" +"%% \\import{}{.pdf_tex}\n" +"%% Alternatively, one can specify\n" +"%% \\graphicspath{{/}}\n" +"%% \n" +"%% For more information, please see info/svg-inkscape on CTAN:\n" +"%% http://tug.ctan.org/tex-archive/info/svg-inkscape\n" +"%%\n" +"\\begingroup%\n" +" \\makeatletter%\n" +" \\providecommand\\color[2][]{%\n" +" \\errmessage{(Inkscape) Color is used for the text in Inkscape, but the package \'color.sty\' is not loaded}%\n" +" \\renewcommand\\color[2][]{}%\n" +" }%\n" +" \\providecommand\\transparent[1]{%\n" +" \\errmessage{(Inkscape) Transparency is used (non-zero) for the text in Inkscape, but the package \'transparent.sty\' is not loaded}%\n" +" \\renewcommand\\transparent[1]{}%\n" +" }%\n" +" \\providecommand\\rotatebox[2]{#2}%\n" +" \\newcommand*\\fsize{\\dimexpr\\f@size pt\\relax}%\n" +" \\newcommand*\\lineheight[1]{\\fontsize{\\fsize}{#1\\fsize}\\selectfont}%\n"; + +static char const postamble[] = +" \\end{picture}%\n" +"\\endgroup%\n"; + +void +LaTeXTextRenderer::writePreamble() +{ + fprintf(_stream, "%s", preamble); +} +void +LaTeXTextRenderer::writePostamble() +{ + fprintf(_stream, "%s", postamble); +} + +void LaTeXTextRenderer::sp_group_render(SPGroup *group) +{ + std::vector l = (group->childList(false)); + for(auto x : l){ + SPItem *item = dynamic_cast(x); + if (item) { + renderItem(item); + } + } +} + +void LaTeXTextRenderer::sp_use_render(SPUse *use) +{ + bool translated = false; + + if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { + Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); + push_transform(tp); + translated = true; + } + + SPItem *childItem = dynamic_cast(use->child); + if (childItem) { + renderItem(childItem); + } + + if (translated) { + pop_transform(); + } +} + +void LaTeXTextRenderer::sp_text_render(SPText *textobj) +{ + // Nothing to do here... (so don't emit an empty box) + // Also avoids falling out of sync with the CairoRenderer (which won't render anything in this case either) + if (textobj->layout.getActualLength() == 0) + return; + + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + + SPStyle *style = textobj->style; + + // get position and alignment + // Align vertically on the baseline of the font (retrieved from the anchor point) + // Align horizontally on anchorpoint + gchar const *alignment = nullptr; + gchar const *aligntabular = nullptr; + switch (style->text_anchor.computed) { + case SP_CSS_TEXT_ANCHOR_START: + alignment = "[lt]"; + aligntabular = "{l}"; + break; + case SP_CSS_TEXT_ANCHOR_END: + alignment = "[rt]"; + aligntabular = "{r}"; + break; + case SP_CSS_TEXT_ANCHOR_MIDDLE: + default: + alignment = "[t]"; + aligntabular = "{c}"; + break; + } + Geom::Point anchor = textobj->attributes.firstXY() * transform(); + Geom::Point pos(anchor); + + // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) + bool has_color = false; // if the item has no color set, don't force black color + bool has_transparency = false; + // TODO: how to handle ICC colors? + // give priority to fill color + guint32 rgba = 0; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + if (style->fill.set && style->fill.isColor()) { + has_color = true; + rgba = style->fill.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + } else if (style->stroke.set && style->stroke.isColor()) { + has_color = true; + rgba = style->stroke.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + } + if (opacity < 1.0) { + has_transparency = true; + } + + // get rotation + Geom::Affine i2doc = textobj->i2doc_affine(); + Geom::Affine wotransl = i2doc.withoutTranslation(); + double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); + bool has_rotation = !Geom::are_near(degrees,0.); + + // get line-height + float line_height; + if (style->line_height.unit == SP_CSS_UNIT_NONE) { + // unitless 'line-height' (use as-is, computed value is relative value) + line_height = style->line_height.computed; + } else { + // 'line-height' with unit (make relative, computed value is absolute value) + line_height = style->line_height.computed / style->font_size.computed; + } + + // write to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // don't use scientific notation + + os << " \\put(" << pos[Geom::X] << "," << pos[Geom::Y] << "){"; + if (has_color) { + os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; + } + if (_pdflatex && has_transparency) { + os << "\\transparent{" << opacity << "}"; + } + if (has_rotation) { + os << "\\rotatebox{" << degrees << "}{"; + } + os << "\\makebox(0,0)" << alignment << "{"; + if (line_height != 1) { + os << "\\lineheight{" << line_height << "}"; + } + os << "\\smash{"; + os << "\\begin{tabular}[t]" << aligntabular; + + // Walk through all spans in the text object. + // Write span strings to LaTeX, associated with font weight and style. + Inkscape::Text::Layout const &layout = *(te_get_layout (textobj)); + for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); + li != le; li.nextStartOfSpan()) + { + Inkscape::Text::Layout::iterator ln = li; + ln.nextStartOfSpan(); + Glib::ustring uspanstr = sp_te_get_string_multiline (textobj, li, ln); + + // escape ampersands + uspanstr = Glib::Regex::create("&")->replace_literal(uspanstr, 0, "\\&", (Glib::RegexMatchFlags)0); + // escape percent + uspanstr = Glib::Regex::create("%")->replace_literal(uspanstr, 0, "\\%", (Glib::RegexMatchFlags)0); + + const gchar *spanstr = uspanstr.c_str(); + if (!spanstr) { + continue; + } + + bool is_bold = false, is_italic = false, is_oblique = false; + + // newline character only -> don't attempt to add style (will break compilation in LaTeX) + if (g_strcmp0(spanstr, "\n")) { + SPStyle const &spanstyle = *(sp_te_style_at_position (textobj, li)); + if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) + { + is_bold = true; + os << "\\textbf{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) + { + is_italic = true; + os << "\\textit{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) + { + is_oblique = true; + os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... + } + } + + // replace carriage return with double slash + gchar ** splitstr = g_strsplit(spanstr, "\n", 2); + os << splitstr[0]; + if (g_strv_length(splitstr) > 1) + { + os << "\\\\"; + } + g_strfreev(splitstr); + + if (is_oblique) { os << "}"; } // oblique end + if (is_italic) { os << "}"; } // italic end + if (is_bold) { os << "}"; } // bold end + } + + os << "\\end{tabular}"; // tabular end + os << "}"; // smash end + if (has_rotation) { os << "}"; } // rotatebox end + os << "}"; //makebox end + os << "}%\n"; // put end + + fprintf(_stream, "%s", os.str().c_str()); +} + +void LaTeXTextRenderer::sp_flowtext_render(SPFlowtext *flowtext) +{ +/* +Flowtext is possible by using a minipage! :) +Flowing in rectangle is possible, not in arb shape. +*/ + + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + + SPStyle *style = flowtext->style; + + SPItem *frame_item = flowtext->get_frame(nullptr); + SPRect *frame = dynamic_cast(frame_item); + if (!frame_item || !frame) { + g_warning("LaTeX export: non-rectangular flowed text shapes are not supported, skipping text."); + return; // don't know how to handle non-rect frames yet. is quite uncommon for latex users i think + } + + // We will transform the coordinates + Geom::Rect framebox = frame->getRect(); + + // get position and alignment + // Align on topleft corner. + gchar const *alignment = "[lt]"; + gchar const *justification = ""; + switch (flowtext->layout.paragraphAlignment(flowtext->layout.begin())) { + case Inkscape::Text::Layout::LEFT: + justification = "\\raggedright "; + break; + case Inkscape::Text::Layout::RIGHT: + justification = "\\raggedleft "; + break; + case Inkscape::Text::Layout::CENTER: + justification = "\\centering "; + case Inkscape::Text::Layout::FULL: + default: + // no need to add LaTeX code for standard justified output :) + break; + } + + // The topleft Corner was calculated after rotating the text which results in a wrong Coordinate. + // Now, the topleft Corner is rotated after calculating it + Geom::Point pos(framebox.corner(0) * transform()); //topleft corner + + // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) + bool has_color = false; // if the item has no color set, don't force black color + bool has_transparency = false; + // TODO: how to handle ICC colors? + // give priority to fill color + guint32 rgba = 0; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + if (style->fill.set && style->fill.isColor()) { + has_color = true; + rgba = style->fill.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + } else if (style->stroke.set && style->stroke.isColor()) { + has_color = true; + rgba = style->stroke.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + } + if (opacity < 1.0) { + has_transparency = true; + } + + // get rotation + Geom::Affine i2doc = flowtext->i2doc_affine(); + Geom::Affine wotransl = i2doc.withoutTranslation(); + double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); + bool has_rotation = !Geom::are_near(degrees,0.); + + // write to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // don't use scientific notation + + os << " \\put(" << pos[Geom::X] << "," << pos[Geom::Y] << "){"; + if (has_color) { + os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; + } + if (_pdflatex && has_transparency) { + os << "\\transparent{" << opacity << "}"; + } + if (has_rotation) { + os << "\\rotatebox{" << degrees << "}{"; + } + os << "\\makebox(0,0)" << alignment << "{"; + + // Scale the x width correctly + os << "\\begin{minipage}{" << framebox.width() * transform().expansionX() << "\\unitlength}"; + os << justification; + + // Walk through all spans in the text object. + // Write span strings to LaTeX, associated with font weight and style. + Inkscape::Text::Layout const &layout = *(te_get_layout(flowtext)); + for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); + li != le; li.nextStartOfSpan()) + { + SPStyle const &spanstyle = *(sp_te_style_at_position(flowtext, li)); + bool is_bold = false, is_italic = false, is_oblique = false; + + if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) + { + is_bold = true; + os << "\\textbf{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) + { + is_italic = true; + os << "\\textit{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) + { + is_oblique = true; + os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... + } + + Inkscape::Text::Layout::iterator ln = li; + ln.nextStartOfSpan(); + Glib::ustring uspanstr = sp_te_get_string_multiline(flowtext, li, ln); + const gchar *spanstr = uspanstr.c_str(); + if (!spanstr) { + continue; + } + // replace carriage return with double slash + gchar ** splitstr = g_strsplit(spanstr, "\n", -1); + gchar *spanstr_new = g_strjoinv("\\\\ ", splitstr); + os << spanstr_new; + g_strfreev(splitstr); + g_free(spanstr_new); + + if (is_oblique) { os << "}"; } // oblique end + if (is_italic) { os << "}"; } // italic end + if (is_bold) { os << "}"; } // bold end + } + + os << "\\end{minipage}"; + if (has_rotation) { + os << "}"; // rotatebox end + } + os << "}"; //makebox end + os << "}%\n"; // put end + + fprintf(_stream, "%s", os.str().c_str()); +} + +void LaTeXTextRenderer::sp_root_render(SPRoot *root) +{ + push_transform(root->c2p); + sp_group_render(root); + pop_transform(); +} + +void +LaTeXTextRenderer::sp_item_invoke_render(SPItem *item) +{ + // Check item's visibility + if (item->isHidden()) { + return; + } + + SPRoot *root = dynamic_cast(item); + if (root) { + sp_root_render(root); + } else { + SPGroup *group = dynamic_cast(item); + if (group) { + sp_group_render(group); + } else { + SPUse *use = dynamic_cast(item); + if (use) { + sp_use_render(use); + } else { + SPText *text = dynamic_cast(item); + if (text) { + sp_text_render(text); + } else { + SPFlowtext *flowtext = dynamic_cast(item); + if (flowtext) { + sp_flowtext_render(flowtext); + } else { + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && (_omittext_state == EMPTY || _omittext_state == NEW_PAGE_ON_GRAPHIC)) { + writeGraphicPage(); + } + _omittext_state = GRAPHIC_ON_TOP; + } + } + } + } + } +} + +void +LaTeXTextRenderer::renderItem(SPItem *item) +{ + push_transform(item->transform); + sp_item_invoke_render(item); + pop_transform(); +} + +void +LaTeXTextRenderer::writeGraphicPage() { + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // no scientific notation + + // strip pathname, as it is probably desired. Having a specific path in the TeX file is not convenient. + if (_pdflatex) + os << " \\put(0,0){\\includegraphics[width=\\unitlength,page=" << _omittext_page++ << "]{" << _filename << "}}%\n"; + else + os << " \\put(0,0){\\includegraphics[width=\\unitlength]{" << _filename << "}}%\n"; + + fprintf(_stream, "%s", os.str().c_str()); +} + +bool +LaTeXTextRenderer::setupDocument(SPDocument *doc, bool pageBoundingBox, float bleedmargin_px, SPItem *base) +{ +// The boundingbox calculation here should be exactly the same as the one by CairoRenderer::setupDocument ! + + if (!base) { + base = doc->getRoot(); + } + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); + } else { + Geom::OptRect bbox = base->documentVisualBounds(); + if (!bbox) { + g_message("CairoRenderer: empty bounding box."); + return false; + } + d = *bbox; + } + d.expandBy(bleedmargin_px); + + // scale all coordinates, such that the width of the image is 1, this is convenient for scaling the image in LaTeX + double scale = 1/(d.width()); + double _width = d.width() * scale; + double _height = d.height() * scale; + push_transform(Geom::Translate(-d.corner(3)) * Geom::Scale(scale, -scale)); + + // write the info to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // no scientific notation + + // scaling of the image when including it in LaTeX + os << " \\ifx\\svgwidth\\undefined%\n"; + os << " \\setlength{\\unitlength}{" << Inkscape::Util::Quantity::convert(d.width(), "px", "pt") << "bp}%\n"; // note: 'bp' is the Postscript pt unit in LaTeX, see LP bug #792384 + os << " \\ifx\\svgscale\\undefined%\n"; + os << " \\relax%\n"; + os << " \\else%\n"; + os << " \\setlength{\\unitlength}{\\unitlength * \\real{\\svgscale}}%\n"; + os << " \\fi%\n"; + os << " \\else%\n"; + os << " \\setlength{\\unitlength}{\\svgwidth}%\n"; + os << " \\fi%\n"; + os << " \\global\\let\\svgwidth\\undefined%\n"; + os << " \\global\\let\\svgscale\\undefined%\n"; + os << " \\makeatother%\n"; + + os << " \\begin{picture}(" << _width << "," << _height << ")%\n"; + + // set \baselineskip equal to fontsize (the closest we can seem to get to CSS "line-height: 1;") + // and remove column spacing from tabular + os << " \\lineheight{1}%\n"; + os << " \\setlength\\tabcolsep{0pt}%\n"; + + fprintf(_stream, "%s", os.str().c_str()); + + if (!_pdflatex) + writeGraphicPage(); + + return true; +} + +Geom::Affine const & +LaTeXTextRenderer::transform() +{ + return _transform_stack.top(); +} + +void +LaTeXTextRenderer::push_transform(Geom::Affine const &tr) +{ + if(!_transform_stack.empty()){ + Geom::Affine tr_top = _transform_stack.top(); + _transform_stack.push(tr * tr_top); + } else { + _transform_stack.push(tr); + } +} + +void +LaTeXTextRenderer::pop_transform() +{ + _transform_stack.pop(); +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/latex-text-renderer.h b/src/extension/internal/latex-text-renderer.h new file mode 100644 index 0000000..a96a263 --- /dev/null +++ b/src/extension/internal/latex-text-renderer.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN +#define EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN + +/** \file + * Declaration of LaTeXTextRenderer, used for rendering the accompanying LaTeX file when exporting to PDF/EPS/PS + LaTeX + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include <2geom/affine.h> +#include + +class SPItem; +class SPRoot; +class SPGroup; +class SPUse; +class SPText; +class SPFlowtext; + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool latex_render_document_text_to_file(SPDocument *doc, gchar const *filename, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, float bleedmargin_px, + bool pdflatex); + +class LaTeXTextRenderer { +public: + LaTeXTextRenderer(bool pdflatex); + virtual ~LaTeXTextRenderer(); + + bool setTargetFile(gchar const *filename); + + /** Initializes the LaTeXTextRenderer according to the specified + SPDocument. Important to set the boundingbox to the pdf boundingbox */ + bool setupDocument(SPDocument *doc, bool pageBoundingBox, float bleedmargin_px, SPItem *base); + + /** Traverses the object tree and invokes the render methods. */ + void renderItem(SPItem *item); + +protected: + enum LaTeXOmitTextPageState { + EMPTY, + GRAPHIC_ON_TOP, + NEW_PAGE_ON_GRAPHIC + }; + + FILE * _stream; + gchar * _filename; + + bool _pdflatex; /** true if outputting for pdfLaTeX*/ + + LaTeXOmitTextPageState _omittext_state; + gulong _omittext_page; + + void push_transform(Geom::Affine const &transform); + Geom::Affine const & transform(); + void pop_transform(); + std::stack _transform_stack; + + void writePreamble(); + void writePostamble(); + + void writeGraphicPage(); + + void sp_item_invoke_render(SPItem *item); + void sp_root_render(SPRoot *item); + void sp_group_render(SPGroup *group); + void sp_use_render(SPUse *use); + void sp_text_render(SPText *text); + void sp_flowtext_render(SPFlowtext *flowtext); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-inout.cpp b/src/extension/internal/metafile-inout.cpp new file mode 100644 index 0000000..e657845 --- /dev/null +++ b/src/extension/internal/metafile-inout.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile input - common routines + *//* + * Authors: + * David Mathog + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include + +#include "display/curve.h" +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/print.h" +#include "path-prefix.h" +#include "document.h" +#include "util/units.h" +#include "ui/shape-editor.h" +#include "document-undo.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-root.h" +#include "object/sp-namedview.h" +#include "svg/stringstream.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +Metafile::~Metafile() +{ + return; +} + +/** Construct a PNG in memory from an RGB from the EMF file + +from: +http://www.lemoda.net/c/write-png/ + +which was based on: +http://stackoverflow.com/questions/1821806/how-to-encode-png-to-buffer-using-libpng + +gcc -Wall -o testpng testpng.c -lpng + +Originally here, but moved up + +#include +#include +#include +#include +*/ + + +/* Given "bitmap", this returns the pixel of bitmap at the point + ("x", "y"). */ + +pixel_t * Metafile::pixel_at (bitmap_t * bitmap, int x, int y) +{ + return bitmap->pixels + bitmap->width * y + x; +} + + +/* Write "bitmap" to a PNG file specified by "path"; returns 0 on + success, non-zero on error. */ + +void +Metafile::my_png_write_data(png_structp png_ptr, png_bytep data, png_size_t length) +{ + PMEMPNG p=(PMEMPNG)png_get_io_ptr(png_ptr); + + size_t nsize = p->size + length; + + /* allocate or grow buffer */ + if(p->buffer){ p->buffer = (char *) realloc(p->buffer, nsize); } + else{ p->buffer = (char *) malloc(nsize); } + + if(!p->buffer){ png_error(png_ptr, "Write Error"); } + + /* copy new bytes to end of buffer */ + memcpy(p->buffer + p->size, data, length); + p->size += length; +} + +void Metafile::toPNG(PMEMPNG accum, int width, int height, const char *px){ + bitmap_t bmStore; + bitmap_t *bitmap = &bmStore; + accum->buffer=nullptr; // PNG constructed in memory will end up here, caller must free(). + accum->size=0; + bitmap->pixels=(pixel_t *)px; + bitmap->width = width; + bitmap->height = height; + + png_structp png_ptr = nullptr; + png_infop info_ptr = nullptr; + size_t x, y; + png_byte ** row_pointers = nullptr; + /* The following number is set by trial and error only. I cannot + see where it it is documented in the libpng manual. + */ + int pixel_size = 3; + int depth = 8; + + png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (png_ptr == nullptr){ + accum->buffer=nullptr; + return; + } + + info_ptr = png_create_info_struct (png_ptr); + if (info_ptr == nullptr){ + png_destroy_write_struct (&png_ptr, &info_ptr); + accum->buffer=nullptr; + return; + } + + /* Set up error handling. */ + + if (setjmp (png_jmpbuf (png_ptr))) { + png_destroy_write_struct (&png_ptr, &info_ptr); + accum->buffer=nullptr; + return; + } + + /* Set image attributes. */ + + png_set_IHDR ( + png_ptr, + info_ptr, + bitmap->width, + bitmap->height, + depth, + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT + ); + + /* Initialize rows of PNG. */ + + row_pointers = (png_byte **) png_malloc (png_ptr, bitmap->height * sizeof (png_byte *)); + for (y = 0; y < bitmap->height; ++y) { + png_byte *row = + (png_byte *) png_malloc (png_ptr, sizeof (uint8_t) * bitmap->width * pixel_size); + row_pointers[bitmap->height - y - 1] = row; // Row order in EMF is reversed. + for (x = 0; x < bitmap->width; ++x) { + pixel_t * pixel = pixel_at (bitmap, x, y); + *row++ = pixel->red; // R & B channels were set correctly by DIB_to_RGB + *row++ = pixel->green; + *row++ = pixel->blue; + } + } + + /* Write the image data to memory */ + + png_set_rows (png_ptr, info_ptr, row_pointers); + + png_set_write_fn(png_ptr, accum, my_png_write_data, nullptr); + + png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr); + + for (y = 0; y < bitmap->height; y++) { + png_free (png_ptr, row_pointers[y]); + } + png_free (png_ptr, row_pointers); + png_destroy_write_struct(&png_ptr, &info_ptr); + +} + +/* If the viewBox is missing, set one +*/ +void Metafile::setViewBoxIfMissing(SPDocument *doc) { + + if (doc && !doc->getRoot()->viewBox_set) { + bool saved = Inkscape::DocumentUndo::getUndoSensitive(doc); + Inkscape::DocumentUndo::setUndoSensitive(doc, false); + + doc->ensureUpToDate(); + + // Set document unit + Inkscape::XML::Node *repr = sp_document_namedview(doc, nullptr)->getRepr(); + Inkscape::SVGOStringStream os; + Inkscape::Util::Unit const* doc_unit = doc->getWidth().unit; + os << doc_unit->abbr; + repr->setAttribute("inkscape:document-units", os.str()); + + // Set viewBox + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc_unit), doc->getHeight().value(doc_unit))); + doc->ensureUpToDate(); + + // Scale and translate objects + double scale = Inkscape::Util::Quantity::convert(1, "px", doc_unit); + Inkscape::UI::ShapeEditor::blockSetItem(true); + double dh; + if(SP_ACTIVE_DOCUMENT){ // for file menu open or import, or paste from clipboard + dh = SP_ACTIVE_DOCUMENT->getHeight().value("px"); + } + else { // for open via --file on command line + dh = doc->getHeight().value("px"); + } + + // These should not affect input, but they do, so set them to a neutral state + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + + doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(0, dh), true); + Inkscape::UI::ShapeEditor::blockSetItem(false); + + // restore options + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); + + Inkscape::DocumentUndo::setUndoSensitive(doc, saved); + } +} + +/** + \fn Convert EMF/WMF region combining ops to livarot region combining ops + \return combination operators in livarot enumeration, or -1 on no match + \param ops (int) combination operator (Inkscape) +*/ +int Metafile::combine_ops_to_livarot(const int op) +{ + int ret = -1; + switch(op) { + case U_RGN_AND: + ret = bool_op_inters; + break; + case U_RGN_OR: + ret = bool_op_union; + break; + case U_RGN_XOR: + ret = bool_op_symdiff; + break; + case U_RGN_DIFF: + ret = bool_op_diff; + break; + } + return(ret); +} + + + +/* convert an EMF RGB(A) color to 0RGB +inverse of gethexcolor() in emf-print.cpp +*/ +uint32_t Metafile::sethexcolor(U_COLORREF color){ + + uint32_t out; + out = (U_RGBAGetR(color) << 16) + + (U_RGBAGetG(color) << 8 ) + + (U_RGBAGetB(color) ); + return(out); +} + +/* Return the base64 encoded png which is shown for all bad images. +Currently a random 3x4 blotch. +Caller must free. +*/ +gchar *Metafile::bad_image_png(){ + gchar *gstring = g_strdup("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAAA3NCSVQICAjb4U/gAAAALElEQVQImQXBQQ2AMAAAsUJQMSWI2H8qME1yMshojwrvGB8XcHKvR1XtOTc/8HENumHCsOMAAAAASUVORK5CYII="); + return(gstring); +} + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-inout.h b/src/extension/internal/metafile-inout.h new file mode 100644 index 0000000..c742a64 --- /dev/null +++ b/src/extension/internal/metafile-inout.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile input - common functions + *//* + * Authors: + * David Mathog + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H + +#define PNG_SKIP_SETJMP_CHECK // else any further png.h include blows up in the compiler +#include +#include +#include +#include +#include +#include +#include +#include <3rdparty/libuemf/uemf.h> +#include <2geom/affine.h> +#include <2geom/pathvector.h> + +#include "extension/implementation/implementation.h" + +class SPObject; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +/* A coloured pixel. */ +struct pixel_t { + uint8_t red; + uint8_t green; + uint8_t blue; + uint8_t opacity; +}; + +/* A picture. */ +struct bitmap_t { + pixel_t *pixels; + size_t width; + size_t height; +}; + +/* structure to store PNG image bytes */ +struct MEMPNG { + char *buffer; + size_t size; +}; +using PMEMPNG = MEMPNG *; + +class Metafile + : public Inkscape::Extension::Implementation::Implementation +{ +public: + Metafile() = default; + ~Metafile() override; + +protected: + static uint32_t sethexcolor(U_COLORREF color); + static pixel_t *pixel_at (bitmap_t * bitmap, int x, int y); + static void my_png_write_data(png_structp png_ptr, png_bytep data, png_size_t length); + static void toPNG(PMEMPNG accum, int width, int height, const char *px); + static gchar *bad_image_png(); + static void setViewBoxIfMissing(SPDocument *doc); + static int combine_ops_to_livarot(const int op); + + +private: +}; + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ diff --git a/src/extension/internal/metafile-print.cpp b/src/extension/internal/metafile-print.cpp new file mode 100644 index 0000000..0935d3d --- /dev/null +++ b/src/extension/internal/metafile-print.cpp @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile printing - common routines + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include <2geom/rect.h> +#include <2geom/curves.h> +#include <2geom/svg-path-parser.h> + +#include "extension/internal/metafile-print.h" +#include "extension/print.h" +#include "path-prefix.h" +#include "object/sp-gradient.h" +#include "object/sp-image.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "style.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +PrintMetafile::~PrintMetafile() +{ +#ifndef G_OS_WIN32 + // restore default signal handling for SIGPIPE + (void) signal(SIGPIPE, SIG_DFL); +#endif + return; +} + +static std::map _ppt_fixable_fonts = { + {{"Arial"}, { 0.05, -0.055, -0.065}}, + {{"Times New Roman"}, { 0.05, -0.055, -0.065}}, + {{"Lucida Sans"}, {-0.025, -0.055, -0.065}}, + {{"Sans"}, { 0.05, -0.055, -0.065}}, + {{"Microsoft Sans Serif"}, {-0.05, -0.055, -0.065}}, + {{"Serif"}, { 0.05, -0.055, -0.065}}, + {{"Garamond"}, { 0.05, -0.055, -0.065}}, + {{"Century Schoolbook"}, { 0.25, 0.025, 0.025}}, + {{"Verdana"}, { 0.025, 0.0, 0.0}}, + {{"Tahoma"}, { 0.045, 0.025, 0.025}}, + {{"Symbol"}, { 0.025, 0.0, 0.0}}, + {{"Wingdings"}, { 0.05, 0.0, 0.0}}, + {{"Zapf Dingbats"}, { 0.025, 0.0, 0.0}}, + {{"Convert To Symbol"}, { 0.025, 0.0, 0.0}}, + {{"Convert To Wingdings"}, { 0.05, 0.0, 0.0}}, + {{"Convert To Zapf Dingbats"}, { 0.025, 0.0, 0.0}}, + {{"Sylfaen"}, { 0.1, 0.0, 0.0}}, + {{"Palatino Linotype"}, { 0.175, 0.125, 0.125}}, + {{"Segoe UI"}, { 0.1, 0.0, 0.0}}, +}; + +bool PrintMetafile::textToPath(Inkscape::Extension::Print *ext) +{ + return ext->get_param_bool("textToPath"); +} + +unsigned int PrintMetafile::bind(Inkscape::Extension::Print * /*mod*/, Geom::Affine const &transform, float /*opacity*/) +{ + if (!m_tr_stack.empty()) { + Geom::Affine tr_top = m_tr_stack.top(); + m_tr_stack.push(transform * tr_top); + } else { + m_tr_stack.push(transform); + } + + return 1; +} + +unsigned int PrintMetafile::release(Inkscape::Extension::Print * /*mod*/) +{ + m_tr_stack.pop(); + return 1; +} + +// Finds font fix parameters for the given fontname. +void PrintMetafile::_lookup_ppt_fontfix(Glib::ustring const &fontname, FontfixParams ¶ms) +{ + auto it = _ppt_fixable_fonts.find(fontname); + if (it!=_ppt_fixable_fonts.end()) { + params = it->second; + } +} + +U_COLORREF PrintMetafile::_gethexcolor(uint32_t color) +{ + U_COLORREF out; + out = U_RGB( + (color >> 16) & 0xFF, + (color >> 8) & 0xFF, + (color >> 0) & 0xFF + ); + return out; +} + +// Translate Inkscape weights to EMF weights. +uint32_t PrintMetafile::_translate_weight(unsigned inkweight) +{ + switch (inkweight) { + // 400 is tested first, as it is the most common case + case SP_CSS_FONT_WEIGHT_400: return U_FW_NORMAL; + case SP_CSS_FONT_WEIGHT_100: return U_FW_THIN; + case SP_CSS_FONT_WEIGHT_200: return U_FW_EXTRALIGHT; + case SP_CSS_FONT_WEIGHT_300: return U_FW_LIGHT; + case SP_CSS_FONT_WEIGHT_500: return U_FW_MEDIUM; + case SP_CSS_FONT_WEIGHT_600: return U_FW_SEMIBOLD; + case SP_CSS_FONT_WEIGHT_700: return U_FW_BOLD; + case SP_CSS_FONT_WEIGHT_800: return U_FW_EXTRABOLD; + case SP_CSS_FONT_WEIGHT_900: return U_FW_HEAVY; + default: return U_FW_NORMAL; + } +} + +/* opacity weighting of two colors as float. v1 is the color, op is its opacity, v2 is the background color */ +inline float opweight(float v1, float v2, float op) +{ + return v1 * op + v2 * (1.0 - op); +} + +U_COLORREF PrintMetafile::avg_stop_color(SPGradient *gr) +{ + U_COLORREF cr; + int last = gr->vector.stops.size() - 1; + if (last >= 1) { + float rgbs[3]; + float rgbe[3]; + float ops, ope; + + ops = gr->vector.stops[0 ].opacity; + ope = gr->vector.stops[last].opacity; + gr->vector.stops[0 ].color.get_rgb_floatv(rgbs); + gr->vector.stops[last].color.get_rgb_floatv(rgbe); + + /* Replace opacity at start & stop with that fraction background color, then average those two for final color. */ + cr = U_RGB( + 255 * ((opweight(rgbs[0], gv.rgb[0], ops) + opweight(rgbe[0], gv.rgb[0], ope)) / 2.0), + 255 * ((opweight(rgbs[1], gv.rgb[1], ops) + opweight(rgbe[1], gv.rgb[1], ope)) / 2.0), + 255 * ((opweight(rgbs[2], gv.rgb[2], ops) + opweight(rgbe[2], gv.rgb[2], ope)) / 2.0) + ); + } else { + cr = U_RGB(0, 0, 0); // The default fill + } + return cr; +} + +U_COLORREF PrintMetafile::weight_opacity(U_COLORREF c1) +{ + float opa = c1.Reserved / 255.0; + U_COLORREF result = U_RGB( + 255 * opweight((float)c1.Red / 255.0, gv.rgb[0], opa), + 255 * opweight((float)c1.Green / 255.0, gv.rgb[1], opa), + 255 * opweight((float)c1.Blue / 255.0, gv.rgb[2], opa) + ); + return result; +} + +/* t between 0 and 1, values outside that range use the nearest limit */ +U_COLORREF PrintMetafile::weight_colors(U_COLORREF c1, U_COLORREF c2, double t) +{ +#define clrweight(a,b,t) ((1-t)*((double) a) + (t)*((double) b)) + U_COLORREF result; + t = ( t > 1.0 ? 1.0 : ( t < 0.0 ? 0.0 : t)); + result.Red = clrweight(c1.Red, c2.Red, t); + result.Green = clrweight(c1.Green, c2.Green, t); + result.Blue = clrweight(c1.Blue, c2.Blue, t); + result.Reserved = clrweight(c1.Reserved, c2.Reserved, t); + + // now handle the opacity, mix the RGB with background at the weighted opacity + + if (result.Reserved != 255) { + result = weight_opacity(result); + } + + return result; +} + +// Extract hatchType, hatchColor from a name like +// EMFhatch_ +// Where the first one is a number and the second a color in hex. +// hatchType and hatchColor have been set with defaults before this is called. +// +void PrintMetafile::hatch_classify(char *name, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor) +{ + int val; + uint32_t hcolor = 0; + uint32_t bcolor = 0; + + // name should be EMFhatch or WMFhatch but *MFhatch will be accepted + if (0 != strncmp(&name[1], "MFhatch", 7)) { + return; // not anything we can parse + } + name += 8; // EMFhatch already detected + val = 0; + while (*name && isdigit(*name)) { + val = 10 * val + *name - '0'; + name++; + } + *hatchType = val; + if (*name != '_' || val > U_HS_DITHEREDBKCLR) { // wrong syntax, cannot classify + *hatchType = -1; + } else { + name++; + if (2 != sscanf(name, "%X_%X", &hcolor, &bcolor)) { // not a pattern with background + if (1 != sscanf(name, "%X", &hcolor)) { + *hatchType = -1; // not a pattern, cannot classify + } + *hatchColor = _gethexcolor(hcolor); + } else { + *hatchColor = _gethexcolor(hcolor); + *bkColor = _gethexcolor(bcolor); + usebk = true; + } + } + /* Everything > U_HS_SOLIDCLR is solid, just specify the color in the brush rather than messing around with background or textcolor */ + if (*hatchType > U_HS_SOLIDCLR) { + *hatchType = U_HS_SOLIDCLR; + } +} + +// +// Recurse down from a brush pattern, try to figure out what it is. +// If an image is found set a pointer to the epixbuf, else set that to NULL +// If a pattern is found with a name like [EW]MFhatch3_3F7FFF return hatchType=3, hatchColor=3F7FFF (as a uint32_t), +// otherwise hatchType is set to -1 and hatchColor is not defined. +// + +void PrintMetafile::brush_classify(SPObject *parent, int depth, Inkscape::Pixbuf **epixbuf, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor) +{ + if (depth == 0) { + *epixbuf = nullptr; + *hatchType = -1; + *hatchColor = U_RGB(0, 0, 0); + *bkColor = U_RGB(255, 255, 255); + } + depth++; + // first look along the pattern chain, if there is one + if (SP_IS_PATTERN(parent)) { + for (SPPattern *pat_i = SP_PATTERN(parent); pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (SP_IS_IMAGE(pat_i)) { + *epixbuf = ((SPImage *)pat_i)->pixbuf; + return; + } + char temp[32]; // large enough + strncpy(temp, pat_i->getAttribute("id"), sizeof(temp)-1); // Some names may be longer than [EW]MFhatch#_###### + temp[sizeof(temp)-1] = '\0'; + hatch_classify(temp, hatchType, hatchColor, bkColor); + if (*hatchType != -1) { + return; + } + + // still looking? Look at this pattern's children, if there are any + for (auto& child: pat_i->children) { + if (*epixbuf || *hatchType != -1) { + break; + } + brush_classify(&child, depth, epixbuf, hatchType, hatchColor, bkColor); + } + } + } else if (SP_IS_IMAGE(parent)) { + *epixbuf = ((SPImage *)parent)->pixbuf; + return; + } else { // some inkscape rearrangements pass through nodes between pattern and image which are not classified as either. + for (auto& child: parent->children) { + if (*epixbuf || *hatchType != -1) { + break; + } + brush_classify(&child, depth, epixbuf, hatchType, hatchColor, bkColor); + } + } +} + +//swap R/B in 4 byte pixel +void PrintMetafile::swapRBinRGBA(char *px, int pixels) +{ + char tmp; + for (int i = 0; i < pixels * 4; px += 4, i += 4) { + tmp = px[2]; + px[2] = px[0]; + px[0] = tmp; + } +} + +int PrintMetafile::hold_gradient(void *gr, int mode) +{ + gv.mode = mode; + gv.grad = gr; + if (mode == DRAW_RADIAL_GRADIENT) { + SPRadialGradient *rg = (SPRadialGradient *) gr; + gv.r = rg->r.computed; // radius, but of what??? + gv.p1 = Geom::Point(rg->cx.computed, rg->cy.computed); // center + gv.p2 = Geom::Point(gv.r, 0) + gv.p1; // xhandle + gv.p3 = Geom::Point(0, -gv.r) + gv.p1; // yhandle + if (rg->gradientTransform_set) { + gv.p1 = gv.p1 * rg->gradientTransform; + gv.p2 = gv.p2 * rg->gradientTransform; + gv.p3 = gv.p3 * rg->gradientTransform; + } + } else if (mode == DRAW_LINEAR_GRADIENT) { + SPLinearGradient *lg = (SPLinearGradient *) gr; + gv.r = 0; // unused + gv.p1 = Geom::Point(lg->x1.computed, lg->y1.computed); // start + gv.p2 = Geom::Point(lg->x2.computed, lg->y2.computed); // end + gv.p3 = Geom::Point(0, 0); // unused + if (lg->gradientTransform_set) { + gv.p1 = gv.p1 * lg->gradientTransform; + gv.p2 = gv.p2 * lg->gradientTransform; + } + } else { + g_error("Fatal programming error, hold_gradient() in metafile-print.cpp called with invalid draw mode"); + } + return 1; +} + +/* convert from center ellipse to SVGEllipticalArc ellipse + + From: + http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter + A point (x,y) on the arc can be found by: + + {x,y} = {cx,cy} + {cosF,-sinF,sinF,cosF} x {rxcosT,rysinT} + + where + {cx,cy} is the center of the ellipse + F is the rotation angle of the X axis of the ellipse from the true X axis + T is the rotation angle around the ellipse + {,,,} is the rotation matrix + rx,ry are the radii of the ellipse's axes + + For SVG parameterization need two points. + Arbitrarily we can use T=0 and T=pi + Since the sweep is 180 the flags are always 0: + + F is in RADIANS, but the SVGEllipticalArc needs degrees! + +*/ +Geom::PathVector PrintMetafile::center_ellipse_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F) +{ + using Geom::X; + using Geom::Y; + double x1, y1, x2, y2; + Geom::Path SVGep; + + x1 = ctr[X] + cos(F) * rx * cos(0) + sin(-F) * ry * sin(0); + y1 = ctr[Y] + sin(F) * rx * cos(0) + cos(F) * ry * sin(0); + x2 = ctr[X] + cos(F) * rx * cos(M_PI) + sin(-F) * ry * sin(M_PI); + y2 = ctr[Y] + sin(F) * rx * cos(M_PI) + cos(F) * ry * sin(M_PI); + + char text[256]; + snprintf(text, 256, " M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z", + x1, y1, rx, ry, F * 360. / (2.*M_PI), x2, y2, rx, ry, F * 360. / (2.*M_PI), x1, y1); + Geom::PathVector outres = Geom::parse_svg_path(text); + return outres; +} + + +/* rx2,ry2 must be larger than rx1,ry1! + angle is in RADIANS +*/ +Geom::PathVector PrintMetafile::center_elliptical_ring_as_SVG_PathV(Geom::Point ctr, double rx1, double ry1, double rx2, double ry2, double F) +{ + using Geom::X; + using Geom::Y; + double x11, y11, x12, y12; + double x21, y21, x22, y22; + double degrot = F * 360. / (2.*M_PI); + + x11 = ctr[X] + cos(F) * rx1 * cos(0) + sin(-F) * ry1 * sin(0); + y11 = ctr[Y] + sin(F) * rx1 * cos(0) + cos(F) * ry1 * sin(0); + x12 = ctr[X] + cos(F) * rx1 * cos(M_PI) + sin(-F) * ry1 * sin(M_PI); + y12 = ctr[Y] + sin(F) * rx1 * cos(M_PI) + cos(F) * ry1 * sin(M_PI); + + x21 = ctr[X] + cos(F) * rx2 * cos(0) + sin(-F) * ry2 * sin(0); + y21 = ctr[Y] + sin(F) * rx2 * cos(0) + cos(F) * ry2 * sin(0); + x22 = ctr[X] + cos(F) * rx2 * cos(M_PI) + sin(-F) * ry2 * sin(M_PI); + y22 = ctr[Y] + sin(F) * rx2 * cos(M_PI) + cos(F) * ry2 * sin(M_PI); + + char text[512]; + snprintf(text, 512, " M %f,%f A %f %f %f 0 1 %f %f A %f %f %f 0 1 %f %f z M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z", + x11, y11, rx1, ry1, degrot, x12, y12, rx1, ry1, degrot, x11, y11, + x21, y21, rx2, ry2, degrot, x22, y22, rx2, ry2, degrot, x21, y21); + Geom::PathVector outres = Geom::parse_svg_path(text); + + return outres; +} + +/* Elliptical hole in a large square extending from -50k to +50k */ +Geom::PathVector PrintMetafile::center_elliptical_hole_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F) +{ + using Geom::X; + using Geom::Y; + double x1, y1, x2, y2; + Geom::Path SVGep; + + x1 = ctr[X] + cos(F) * rx * cos(0) + sin(-F) * ry * sin(0); + y1 = ctr[Y] + sin(F) * rx * cos(0) + cos(F) * ry * sin(0); + x2 = ctr[X] + cos(F) * rx * cos(M_PI) + sin(-F) * ry * sin(M_PI); + y2 = ctr[Y] + sin(F) * rx * cos(M_PI) + cos(F) * ry * sin(M_PI); + + char text[256]; + snprintf(text, 256, " M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z M 50000,50000 50000,-50000 -50000,-50000 -50000,50000 z", + x1, y1, rx, ry, F * 360. / (2.*M_PI), x2, y2, rx, ry, F * 360. / (2.*M_PI), x1, y1); + Geom::PathVector outres = Geom::parse_svg_path(text); + return outres; +} + +/* rectangular cutter. +ctr "center" of rectangle (might not actually be in the center with respect to leading/trailing edges +pos vector from center to leading edge +neg vector from center to trailing edge +width vector to side edge +*/ +Geom::PathVector PrintMetafile::rect_cutter(Geom::Point ctr, Geom::Point pos, Geom::Point neg, Geom::Point width) +{ + Geom::PathVector outres; + Geom::Path cutter; + cutter.start(ctr + pos - width); + cutter.appendNew(ctr + pos + width); + cutter.appendNew(ctr + neg + width); + cutter.appendNew(ctr + neg - width); + cutter.close(); + outres.push_back(cutter); + return outres; +} + +/* Convert from SPWindRule to livarot's FillRule + This is similar to what sp_selected_path_boolop() does +*/ +FillRule PrintMetafile::SPWR_to_LVFR(SPWindRule wr) +{ + FillRule fr; + if (wr == SP_WIND_RULE_EVENODD) { + fr = fill_oddEven; + } else { + fr = fill_nonZero; + } + return fr; +} + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-print.h b/src/extension/internal/metafile-print.h new file mode 100644 index 0000000..3aeb0a0 --- /dev/null +++ b/src/extension/internal/metafile-print.h @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile printing - common functions + *//* + * Authors: + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_PRINT_H + +#include +#include +#include +#include <3rdparty/libuemf/uemf.h> +#include <2geom/affine.h> +#include <2geom/pathvector.h> + +#include "extension/implementation/implementation.h" +#include "splivarot.h" +#include "display/canvas-bpath.h" + +class SPGradient; +class SPObject; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +enum MFDrawMode {DRAW_PAINT, DRAW_PATTERN, DRAW_IMAGE, DRAW_LINEAR_GRADIENT, DRAW_RADIAL_GRADIENT}; + +struct FontfixParams { + double f1; //Vertical (rotating) offset factor (* font height) + double f2; //Vertical (nonrotating) offset factor (* font height) + double f3; //Horizontal (nonrotating) offset factor (* font height) +}; + +class PrintMetafile + : public Inkscape::Extension::Implementation::Implementation +{ +public: + PrintMetafile() = default; + ~PrintMetafile() override; + + bool textToPath (Inkscape::Extension::Print * ext) override; + unsigned int bind(Inkscape::Extension::Print *module, Geom::Affine const &transform, float opacity) override; + unsigned int release(Inkscape::Extension::Print *module) override; + +protected: + struct GRADVALUES { + Geom::Point p1; // center or start + Geom::Point p2; // xhandle or end + Geom::Point p3; // yhandle or unused + double r; // radius or unused + void *grad; // to access the stops information + int mode; // DRAW_LINEAR_GRADIENT or DRAW_RADIAL_GRADIENT, if GRADVALUES is valid, else any value + U_COLORREF bgc; // document background color, this is as good a place as any to keep it + float rgb[3]; // also background color, but as 0-1 float. + }; + + double _width; + double _height; + double _doc_unit_scale; // to pixels, regardless of the document units + + U_RECTL rc; + + uint32_t htextalignment; + uint32_t hpolyfillmode; // used to minimize redundant records that set this + float htextcolor_rgb[3]; // used to minimize redundant records that set this + + std::stack m_tr_stack; + Geom::PathVector fill_pathv; + Geom::Affine fill_transform; + bool use_stroke; + bool use_fill; + bool simple_shape; + bool usebk; + + GRADVALUES gv; + + static void _lookup_ppt_fontfix(Glib::ustring const &fontname, FontfixParams &); + static U_COLORREF _gethexcolor(uint32_t color); + static uint32_t _translate_weight(unsigned inkweight); + + U_COLORREF avg_stop_color(SPGradient *gr); + U_COLORREF weight_opacity(U_COLORREF c1); + U_COLORREF weight_colors(U_COLORREF c1, U_COLORREF c2, double t); + + void hatch_classify(char *name, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor); + void brush_classify(SPObject *parent, int depth, Inkscape::Pixbuf **epixbuf, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor); + static void swapRBinRGBA(char *px, int pixels); + + int hold_gradient(void *gr, int mode); + static int snprintf_dots(char * s, size_t n, const char * format, ...); + static Geom::PathVector center_ellipse_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F); + static Geom::PathVector center_elliptical_ring_as_SVG_PathV(Geom::Point ctr, double rx1, double ry1, double rx2, double ry2, double F); + static Geom::PathVector center_elliptical_hole_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F); + static Geom::PathVector rect_cutter(Geom::Point ctr, Geom::Point pos, Geom::Point neg, Geom::Point width); + static FillRule SPWR_to_LVFR(SPWindRule wr); + + virtual int create_brush(SPStyle const *style, PU_COLORREF fcolor) = 0; + virtual void destroy_brush() = 0; + virtual int create_pen(SPStyle const *style, const Geom::Affine &transform) = 0; + virtual void destroy_pen() = 0; +}; + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/odf.cpp b/src/extension/internal/odf.cpp new file mode 100644 index 0000000..8d77345 --- /dev/null +++ b/src/extension/internal/odf.cpp @@ -0,0 +1,2131 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * OpenDocument (drawing) input and output + *//* + * Authors: + * Bob Jamison + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* + * This is an an entry in the extensions mechanism to begin to enable + * the inputting and outputting of OpenDocument Format (ODF) files from + * within Inkscape. Although the initial implementations will be very lossy + * due to the differences in the models of SVG and ODF, they will hopefully + * improve greatly with time. People should consider this to be a framework + * that can be continuously upgraded for ever improving fidelity. Potential + * developers should especially look in preprocess() and writeTree() to see how + * the SVG tree is scanned, read, translated, and then written to ODF. + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + * + */ + +#include "odf.h" + +//# System includes +#include +#include +#include +#include + +//# Inkscape includes +#include "clear-n_.h" +#include "inkscape.h" +#include "display/curve.h" +#include <2geom/pathvector.h> +#include <2geom/curves.h> +#include <2geom/transforms.h> +#include +#include "helper/geom-curves.h" +#include "extension/system.h" + +#include "xml/repr.h" +#include "xml/attribute-record.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/uri.h" +#include "style.h" + +#include "svg/svg.h" +#include "text-editing.h" +#include "util/units.h" + + +#include "inkscape-version.h" +#include "document.h" +#include "extension/extension.h" + +#include "io/stream/bufferstream.h" +#include "io/stream/stringstream.h" +#include "io/sys.h" +#include +#include +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ +//# Shorthand notation +typedef Inkscape::IO::BufferOutputStream BufferOutputStream; +typedef Inkscape::IO::OutputStreamWriter OutputStreamWriter; +typedef Inkscape::IO::StringOutputStream StringOutputStream; + + +//######################################################################## +//# C L A S S SingularValueDecomposition +//######################################################################## +#include + +class SVDMatrix +{ +public: + + SVDMatrix() + { + init(); + } + + SVDMatrix(unsigned int rowSize, unsigned int colSize) + { + init(); + rows = rowSize; + cols = colSize; + size = rows * cols; + d = new double[size]; + for (unsigned int i=0 ; i= rows || col >= cols) + return badval; + return d[cols*row + col]; + } + + double operator() (unsigned int row, unsigned int col) const + { + if (row >= rows || col >= cols) + return badval; + return d[cols*row + col]; + } + + unsigned int getRows() + { + return rows; + } + + unsigned int getCols() + { + return cols; + } + + SVDMatrix multiply(const SVDMatrix &other) + { + if (cols != other.rows) + { + SVDMatrix dummy; + return dummy; + } + SVDMatrix result(rows, other.cols); + for (unsigned int i=0 ; i + * For an m-by-n matrix A with m >= n, the singular value decomposition is + * an m-by-n orthogonal matrix U, an n-by-n diagonal matrix S, and + * an n-by-n orthogonal matrix V so that A = U*S*V'. + *

+ * The singular values, sigma[k] = S[k][k], are ordered so that + * sigma[0] >= sigma[1] >= ... >= sigma[n-1]. + *

+ * The singular value decomposition always exists, so the constructor will + * never fail. The matrix condition number and the effective numerical + * rank can be computed from this decomposition. + */ +class SingularValueDecomposition +{ +public: + + /** Construct the singular value decomposition + @param A Rectangular matrix + @return Structure to access U, S and V. + */ + + SingularValueDecomposition (const SVDMatrix &mat) : + A (mat), + U (), + s (nullptr), + s_size (0), + V () + { + calculate(); + } + + virtual ~SingularValueDecomposition() + { + delete[] s; + } + + /** + * Return the left singular vectors + * @return U + */ + SVDMatrix &getU(); + + /** + * Return the right singular vectors + * @return V + */ + SVDMatrix &getV(); + + /** + * Return the s[index] value + */ double getS(unsigned int index); + + /** + * Two norm + * @return max(S) + */ + double norm2(); + + /** + * Two norm condition number + * @return max(S)/min(S) + */ + double cond(); + + /** + * Effective numerical matrix rank + * @return Number of nonnegligible singular values. + */ + int rank(); + +private: + + void calculate(); + + SVDMatrix A; + SVDMatrix U; + double *s; + unsigned int s_size; + SVDMatrix V; + +}; + + +static double svd_hypot(double a, double b) +{ + double r; + + if (fabs(a) > fabs(b)) + { + r = b/a; + r = fabs(a) * sqrt(1+r*r); + } + else if (b != 0) + { + r = a/b; + r = fabs(b) * sqrt(1+r*r); + } + else + { + r = 0.0; + } + return r; +} + + + +void SingularValueDecomposition::calculate() +{ + // Initialize. + int m = A.getRows(); + int n = A.getCols(); + + int nu = (m > n) ? m : n; + s_size = (m+1 < n) ? m+1 : n; + s = new double[s_size]; + U = SVDMatrix(m, nu); + V = SVDMatrix(n, n); + double *e = new double[n]; + double *work = new double[m]; + bool wantu = true; + bool wantv = true; + + // Reduce A to bidiagonal form, storing the diagonal elements + // in s and the super-diagonal elements in e. + + int nct = (m-10) ? nrtx : 0; + for (int k = 0; k < 2; k++) { + if (k < nct) { + + // Compute the transformation for the k-th column and + // place the k-th diagonal in s[k]. + // Compute 2-norm of k-th column without under/overflow. + s[k] = 0; + for (int i = k; i < m; i++) { + s[k] = svd_hypot(s[k],A(i, k)); + } + if (s[k] != 0.0) { + if (A(k, k) < 0.0) { + s[k] = -s[k]; + } + for (int i = k; i < m; i++) { + A(i, k) /= s[k]; + } + A(k, k) += 1.0; + } + s[k] = -s[k]; + } + for (int j = k+1; j < n; j++) { + if ((k < nct) & (s[k] != 0.0)) { + + // Apply the transformation. + + double t = 0; + for (int i = k; i < m; i++) { + t += A(i, k) * A(i, j); + } + t = -t/A(k, k); + for (int i = k; i < m; i++) { + A(i, j) += t*A(i, k); + } + } + + // Place the k-th row of A into e for the + // subsequent calculation of the row transformation. + + e[j] = A(k, j); + } + if (wantu & (k < nct)) { + + // Place the transformation in U for subsequent back + // multiplication. + + for (int i = k; i < m; i++) { + U(i, k) = A(i, k); + } + } + if (k < nrt) { + + // Compute the k-th row transformation and place the + // k-th super-diagonal in e[k]. + // Compute 2-norm without under/overflow. + e[k] = 0; + for (int i = k+1; i < n; i++) { + e[k] = svd_hypot(e[k],e[i]); + } + if (e[k] != 0.0) { + if (e[k+1] < 0.0) { + e[k] = -e[k]; + } + for (int i = k+1; i < n; i++) { + e[i] /= e[k]; + } + e[k+1] += 1.0; + } + e[k] = -e[k]; + if ((k+1 < m) & (e[k] != 0.0)) { + + // Apply the transformation. + + for (int i = k+1; i < m; i++) { + work[i] = 0.0; + } + for (int j = k+1; j < n; j++) { + for (int i = k+1; i < m; i++) { + work[i] += e[j]*A(i, j); + } + } + for (int j = k+1; j < n; j++) { + double t = -e[j]/e[k+1]; + for (int i = k+1; i < m; i++) { + A(i, j) += t*work[i]; + } + } + } + if (wantv) { + + // Place the transformation in V for subsequent + // back multiplication. + + for (int i = k+1; i < n; i++) { + V(i, k) = e[i]; + } + } + } + } + + // Set up the final bidiagonal matrix or order p. + + int p = (n < m+1) ? n : m+1; + if (nct < n) { + s[nct] = A(nct, nct); + } + if (m < p) { + s[p-1] = 0.0; + } + if (nrt+1 < p) { + e[nrt] = A(nrt, p-1); + } + e[p-1] = 0.0; + + // If required, generate U. + + if (wantu) { + for (int j = nct; j < nu; j++) { + for (int i = 0; i < m; i++) { + U(i, j) = 0.0; + } + U(j, j) = 1.0; + } + for (int k = nct-1; k >= 0; k--) { + if (s[k] != 0.0) { + for (int j = k+1; j < nu; j++) { + double t = 0; + for (int i = k; i < m; i++) { + t += U(i, k)*U(i, j); + } + t = -t/U(k, k); + for (int i = k; i < m; i++) { + U(i, j) += t*U(i, k); + } + } + for (int i = k; i < m; i++ ) { + U(i, k) = -U(i, k); + } + U(k, k) = 1.0 + U(k, k); + for (int i = 0; i < k-1; i++) { + U(i, k) = 0.0; + } + } else { + for (int i = 0; i < m; i++) { + U(i, k) = 0.0; + } + U(k, k) = 1.0; + } + } + } + + // If required, generate V. + + if (wantv) { + for (int k = n-1; k >= 0; k--) { + if ((k < nrt) & (e[k] != 0.0)) { + for (int j = k+1; j < nu; j++) { + double t = 0; + for (int i = k+1; i < n; i++) { + t += V(i, k)*V(i, j); + } + t = -t/V(k+1, k); + for (int i = k+1; i < n; i++) { + V(i, j) += t*V(i, k); + } + } + } + for (int i = 0; i < n; i++) { + V(i, k) = 0.0; + } + V(k, k) = 1.0; + } + } + + // Main iteration loop for the singular values. + + int pp = p-1; + //double eps = pow(2.0,-52.0); + //double tiny = pow(2.0,-966.0); + //let's just calculate these now + //a double can be e ± 308.25, so this is safe + double eps = 2.22e-16; + double tiny = 1.6e-291; + while (p > 0) { + int k,kase; + + // Here is where a test for too many iterations would go. + + // This section of the program inspects for + // negligible elements in the s and e arrays. On + // completion the variables kase and k are set as follows. + + // kase = 1 if s(p) and e[k-1] are negligible and k

= -1; k--) { + if (k == -1) { + break; + } + if (fabs(e[k]) <= + tiny + eps*(fabs(s[k]) + fabs(s[k+1]))) { + e[k] = 0.0; + break; + } + } + if (k == p-2) { + kase = 4; + } else { + int ks; + for (ks = p-1; ks >= k; ks--) { + if (ks == k) { + break; + } + double t = (ks != p ? fabs(e[ks]) : 0.) + + (ks != k+1 ? fabs(e[ks-1]) : 0.); + if (fabs(s[ks]) <= tiny + eps*t) { + s[ks] = 0.0; + break; + } + } + if (ks == k) { + kase = 3; + } else if (ks == p-1) { + kase = 1; + } else { + kase = 2; + k = ks; + } + } + k++; + + // Perform the task indicated by kase. + + switch (kase) { + + // Deflate negligible s(p). + + case 1: { + double f = e[p-2]; + e[p-2] = 0.0; + for (int j = p-2; j >= k; j--) { + double t = svd_hypot(s[j],f); + double cs = s[j]/t; + double sn = f/t; + s[j] = t; + if (j != k) { + f = -sn*e[j-1]; + e[j-1] = cs*e[j-1]; + } + if (wantv) { + for (int i = 0; i < n; i++) { + t = cs*V(i, j) + sn*V(i, p-1); + V(i, p-1) = -sn*V(i, j) + cs*V(i, p-1); + V(i, j) = t; + } + } + } + } + break; + + // Split at negligible s(k). + + case 2: { + double f = e[k-1]; + e[k-1] = 0.0; + for (int j = k; j < p; j++) { + double t = svd_hypot(s[j],f); + double cs = s[j]/t; + double sn = f/t; + s[j] = t; + f = -sn*e[j]; + e[j] = cs*e[j]; + if (wantu) { + for (int i = 0; i < m; i++) { + t = cs*U(i, j) + sn*U(i, k-1); + U(i, k-1) = -sn*U(i, j) + cs*U(i, k-1); + U(i, j) = t; + } + } + } + } + break; + + // Perform one qr step. + + case 3: { + + // Calculate the shift. + + double scale = fabs(s[p-1]); + double d = fabs(s[p-2]); + if (d>scale) scale=d; + d = fabs(e[p-2]); + if (d>scale) scale=d; + d = fabs(s[k]); + if (d>scale) scale=d; + d = fabs(e[k]); + if (d>scale) scale=d; + double sp = s[p-1]/scale; + double spm1 = s[p-2]/scale; + double epm1 = e[p-2]/scale; + double sk = s[k]/scale; + double ek = e[k]/scale; + double b = ((spm1 + sp)*(spm1 - sp) + epm1*epm1)/2.0; + double c = (sp*epm1)*(sp*epm1); + double shift = 0.0; + if ((b != 0.0) | (c != 0.0)) { + shift = sqrt(b*b + c); + if (b < 0.0) { + shift = -shift; + } + shift = c/(b + shift); + } + double f = (sk + sp)*(sk - sp) + shift; + double g = sk*ek; + + // Chase zeros. + + for (int j = k; j < p-1; j++) { + double t = svd_hypot(f,g); + double cs = f/t; + double sn = g/t; + if (j != k) { + e[j-1] = t; + } + f = cs*s[j] + sn*e[j]; + e[j] = cs*e[j] - sn*s[j]; + g = sn*s[j+1]; + s[j+1] = cs*s[j+1]; + if (wantv) { + for (int i = 0; i < n; i++) { + t = cs*V(i, j) + sn*V(i, j+1); + V(i, j+1) = -sn*V(i, j) + cs*V(i, j+1); + V(i, j) = t; + } + } + t = svd_hypot(f,g); + cs = f/t; + sn = g/t; + s[j] = t; + f = cs*e[j] + sn*s[j+1]; + s[j+1] = -sn*e[j] + cs*s[j+1]; + g = sn*e[j+1]; + e[j+1] = cs*e[j+1]; + if (wantu && (j < m-1)) { + for (int i = 0; i < m; i++) { + t = cs*U(i, j) + sn*U(i, j+1); + U(i, j+1) = -sn*U(i, j) + cs*U(i, j+1); + U(i, j) = t; + } + } + } + e[p-2] = f; + } + break; + + // Convergence. + + case 4: { + + // Make the singular values positive. + + if (s[k] <= 0.0) { + s[k] = (s[k] < 0.0 ? -s[k] : 0.0); + if (wantv) { + for (int i = 0; i <= pp; i++) { + V(i, k) = -V(i, k); + } + } + } + + // Order the singular values. + + while (k < pp) { + if (s[k] >= s[k+1]) { + break; + } + double t = s[k]; + s[k] = s[k+1]; + s[k+1] = t; + if (wantv && (k < n-1)) { + for (int i = 0; i < n; i++) { + t = V(i, k+1); V(i, k+1) = V(i, k); V(i, k) = t; + } + } + if (wantu && (k < m-1)) { + for (int i = 0; i < m; i++) { + t = U(i, k+1); U(i, k+1) = U(i, k); U(i, k) = t; + } + } + k++; + } + p--; + } + break; + } + } + + delete [] e; + delete [] work; + +} + + +/** + * Return the left singular vectors + * @return U + */ +SVDMatrix &SingularValueDecomposition::getU() +{ + return U; +} + +/** + * Return the right singular vectors + * @return V + */ + +SVDMatrix &SingularValueDecomposition::getV() +{ + return V; +} + +/** + * Return the s[0] value + */ +double SingularValueDecomposition::getS(unsigned int index) +{ + if (index >= s_size) + return 0.0; + return s[index]; +} + +/** + * Two norm + * @return max(S) + */ +double SingularValueDecomposition::norm2() +{ + return s[0]; +} + +/** + * Two norm condition number + * @return max(S)/min(S) + */ + +double SingularValueDecomposition::cond() +{ + return s[0]/s[2]; +} + +/** + * Effective numerical matrix rank + * @return Number of nonnegligible singular values. + */ +int SingularValueDecomposition::rank() +{ + double eps = pow(2.0,-52.0); + double tol = 3.0*s[0]*eps; + int r = 0; + for (int i = 0; i < 3; i++) + { + if (s[i] > tol) + r++; + } + return r; +} + +//######################################################################## +//# E N D C L A S S SingularValueDecomposition +//######################################################################## + + + + + +//#define pxToCm 0.0275 +#define pxToCm 0.03 + + +//######################################################################## +//# O U T P U T +//######################################################################## + +/** + * Get the value of a node/attribute pair + */ +static Glib::ustring getAttribute( Inkscape::XML::Node *node, char const *attrName) +{ + Glib::ustring val; + char const *valstr = node->attribute(attrName); + if (valstr) + val = valstr; + return val; +} + + +static Glib::ustring formatTransform(Geom::Affine &tf) +{ + Glib::ustring str; + if (!tf.isIdentity()) + { + StringOutputStream outs; + OutputStreamWriter out(outs); + out.printf("matrix(%.3f %.3f %.3f %.3f %.3f %.3f)", + tf[0], tf[1], tf[2], tf[3], tf[4], tf[5]); + str = outs.getString(); + } + return str; +} + + +/** + * Get the general transform from SVG pixels to + * ODF cm + */ +static Geom::Affine getODFTransform(const SPItem *item) +{ + //### Get SVG-to-ODF transform + Geom::Affine tf (item->i2doc_affine()); + tf = tf * Geom::Affine(Geom::Scale(pxToCm)); + return tf; +} + + +/** + * Get the bounding box of an item, as mapped onto + * an ODF document, in cm. + */ +static Geom::OptRect getODFBoundingBox(const SPItem *item) +{ + // TODO: geometric or visual? + Geom::OptRect bbox = item->documentVisualBounds(); + if (bbox) { + *bbox *= Geom::Affine(Geom::Scale(pxToCm)); + } + return bbox; +} + + +/** + * Get the transform for an item, including parents, but without + * root viewBox transformation. + */ +static Geom::Affine getODFItemTransform(const SPItem *item) +{ + Geom::Affine itemTransform (item->i2doc_affine() * + SP_ACTIVE_DOCUMENT->getRoot()->c2p.inverse()); + return itemTransform; +} + + +/** + * Get some fun facts from the transform + */ +static void analyzeTransform(Geom::Affine &tf, + double &rotate, double &/*xskew*/, double &/*yskew*/, + double &xscale, double &yscale) +{ + SVDMatrix mat(2, 2); + mat(0, 0) = tf[0]; + mat(0, 1) = tf[1]; + mat(1, 0) = tf[2]; + mat(1, 1) = tf[3]; + + SingularValueDecomposition svd(mat); + + SVDMatrix U = svd.getU(); + SVDMatrix V = svd.getV(); + SVDMatrix Vt = V.transpose(); + SVDMatrix UVt = U.multiply(Vt); + double s0 = svd.getS(0); + double s1 = svd.getS(1); + xscale = s0; + yscale = s1; + rotate = UVt(0,0); +} + +static void gatherText(Inkscape::XML::Node *node, Glib::ustring &buf) +{ + if (node->type() == Inkscape::XML::TEXT_NODE) + { + char *s = (char *)node->content(); + if (s) + buf.append(s); + } + + for (Inkscape::XML::Node *child = node->firstChild() ; + child != nullptr; child = child->next()) + { + gatherText(child, buf); + } + +} + + +/** + * FIRST PASS. + * Method descends into the repr tree, converting image, style, and gradient info + * into forms compatible in ODF. + */ +void OdfOutput::preprocess(ZipFile &zf, Inkscape::XML::Node *node) +{ + Glib::ustring nodeName = node->name(); + Glib::ustring id = getAttribute(node, "id"); + + //### First, check for metadata + if (nodeName == "metadata" || nodeName == "svg:metadata") + { + Inkscape::XML::Node *mchild = node->firstChild() ; + if (!mchild || strcmp(mchild->name(), "rdf:RDF")) + return; + Inkscape::XML::Node *rchild = mchild->firstChild() ; + if (!rchild || strcmp(rchild->name(), "cc:Work")) + return; + for (Inkscape::XML::Node *cchild = rchild->firstChild() ; + cchild ; cchild = cchild->next()) + { + Glib::ustring ccName = cchild->name(); + Glib::ustring ccVal; + gatherText(cchild, ccVal); + //g_message("ccName: %s ccVal:%s", ccName.c_str(), ccVal.c_str()); + metadata[ccName] = ccVal; + } + return; + } + + //Now consider items. + SPObject *reprobj = SP_ACTIVE_DOCUMENT->getObjectByRepr(node); + if (!reprobj) + { + return; + } + if (!SP_IS_ITEM(reprobj)) + { + return; + } + + if (nodeName == "image" || nodeName == "svg:image") { + Glib::ustring href = getAttribute(node, "xlink:href"); + if (href.size() > 0 && imageTable.count(href) == 0) { + try { + auto uri = Inkscape::URI(href.c_str(), docBaseUri.c_str()); + auto mimetype = uri.getMimeType(); + + if (mimetype.substr(0, 6) != "image/") { + return; + } + + auto ext = mimetype.substr(6); + auto newName = Glib::ustring("Pictures/image") + std::to_string(imageTable.size()) + "." + ext; + + imageTable[href] = newName; + + auto ze = zf.newEntry(newName, ""); + ze->setUncompressedData(uri.getContents()); + ze->finish(); + } catch (...) { + g_warning("Could not handle URI '%.100s'", href.c_str()); + } + } + } + + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + preprocess(zf, child); +} + + +/** + * Writes the manifest. Currently it only changes according to the + * file names of images packed into the zip file. + */ +bool OdfOutput::writeManifest(ZipFile &zf) +{ + BufferOutputStream bouts; + OutputStreamWriter outs(bouts); + + time_t tim; + time(&tim); + + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + std::map::iterator iter; + for (iter = imageTable.begin() ; iter!=imageTable.end() ; ++iter) + { + Glib::ustring newName = iter->second; + + // note: mime subtype was added as file extension in OdfOutput::preprocess + Glib::ustring mimesubtype = Inkscape::IO::get_file_extension(newName); + + outs.printf(" \n"); + } + outs.printf("\n"); + + outs.close(); + + //Make our entry + ZipEntry *ze = zf.newEntry("META-INF/manifest.xml", "ODF file manifest"); + ze->setUncompressedData(bouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * This writes the document meta information to meta.xml + */ +bool OdfOutput::writeMeta(ZipFile &zf) +{ + BufferOutputStream bouts; + OutputStreamWriter outs(bouts); + + time_t tim; + time(&tim); + + std::map::iterator iter; + Glib::ustring InkscapeVersion = Glib::ustring("Inkscape.org - ") + Inkscape::version_string; + Glib::ustring creator = InkscapeVersion; + iter = metadata.find("dc:creator"); + if (iter != metadata.end()) + { + creator = iter->second; + } + + Glib::ustring date; + Glib::ustring moddate; + char buf [80]; + time_t rawtime; + struct tm * timeinfo; + time (&rawtime); + timeinfo = localtime (&rawtime); + strftime (buf,80,"%Y-%m-%d %H:%M:%S",timeinfo); + moddate = Glib::ustring(buf); + + iter = metadata.find("dc:date"); + if (iter != metadata.end()) + { + date = iter->second; + } + else + { + date = moddate; + } + + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + Glib::ustring tmp = Glib::ustring::compose(" %1\n", InkscapeVersion); + tmp += Glib::ustring::compose(" %1\n", creator); + tmp += Glib::ustring::compose(" %1\n", date); + tmp += Glib::ustring::compose(" %1\n", moddate); + outs.writeUString(tmp); + for (iter = metadata.begin() ; iter != metadata.end() ; ++iter) + { + Glib::ustring name = iter->first; + Glib::ustring value = iter->second; + if (!name.empty() && !value.empty()) + { + tmp = Glib::ustring::compose(" <%1>%2\n", name, value, name); + outs.writeUString(tmp); + } + } + // outs.writeString(" 2\n"); + // outs.writeString(" PT56S\n"); + // outs.writeString(" \n"); + // outs.writeString(" \n"); + // outs.writeString(" \n"); + // outs.writeString(" \n"); + // outs.writeString(" \n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.close(); + + //Make our entry + ZipEntry *ze = zf.newEntry("meta.xml", "ODF info file"); + ze->setUncompressedData(bouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * Writes an SVG path as an ODF and returns the number of points written + */ +static int +writePath(Writer &outs, Geom::PathVector const &pathv, + Geom::Affine const &tf, double xoff, double yoff) +{ + using Geom::X; + using Geom::Y; + + int nrPoints = 0; + + // convert the path to only lineto's and cubic curveto's: + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * tf * Geom::Translate(xoff, yoff) * Geom::Scale(1000.)); + + for (const auto & pit : pv) { + + double destx = pit.initialPoint()[X]; + double desty = pit.initialPoint()[Y]; + if (fabs(destx)<1.0) destx = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(desty)<1.0) desty = 0.0; + outs.printf("M %.3f %.3f ", destx, desty); + nrPoints++; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) { + + if( is_straight_curve(*cit) ) + { + double destx = cit->finalPoint()[X]; + double desty = cit->finalPoint()[Y]; + if (fabs(destx)<1.0) destx = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(desty)<1.0) desty = 0.0; + outs.printf("L %.3f %.3f ", destx, desty); + } + else if(Geom::CubicBezier const *cubic = dynamic_cast(&*cit)) { + std::vector points = cubic->controlPoints(); + for (unsigned i = 1; i <= 3; i++) { + if (fabs(points[i][X])<1.0) points[i][X] = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(points[i][Y])<1.0) points[i][Y] = 0.0; + } + outs.printf("C %.3f %.3f %.3f %.3f %.3f %.3f ", points[1][X],points[1][Y], points[2][X],points[2][Y], points[3][X],points[3][Y]); + } + else { + g_error ("logical error, because pathv_to_linear_and_cubic_beziers was used"); + } + + nrPoints++; + } + + if (pit.closed()) { + outs.printf("Z"); + } + } + + return nrPoints; +} + +bool OdfOutput::processStyle(SPItem *item, const Glib::ustring &id, const Glib::ustring &gradientNameFill, const Glib::ustring &gradientNameStroke, Glib::ustring& output) +{ + output.clear(); + if (!item) + { + return false; + } + + SPStyle *style = item->style; + if (!style) + { + return false; + } + + StyleInfo si; + + // FILL + if (style->fill.isColor()) + { + guint32 fillCol = style->fill.value.color.toRGBA32( 0 ); + char buf[16]; + int r = (fillCol >> 24) & 0xff; + int g = (fillCol >> 16) & 0xff; + int b = (fillCol >> 8) & 0xff; + snprintf(buf, 15, "#%02x%02x%02x", r, g, b); + si.fillColor = buf; + si.fill = "solid"; + double opacityPercent = 100.0 * + (SP_SCALE24_TO_FLOAT(style->fill_opacity.value)); + snprintf(buf, 15, "%.3f%%", opacityPercent); + si.fillOpacity = buf; + } + else if (style->fill.isPaintserver()) + { + SPGradient *gradient = SP_GRADIENT(SP_STYLE_FILL_SERVER(style)); + if (gradient) + { + si.fill = "gradient"; + } + } + + // STROKE + if (style->stroke.isColor()) + { + guint32 strokeCol = style->stroke.value.color.toRGBA32( 0 ); + char buf[16]; + int r = (strokeCol >> 24) & 0xff; + int g = (strokeCol >> 16) & 0xff; + int b = (strokeCol >> 8) & 0xff; + snprintf(buf, 15, "#%02x%02x%02x", r, g, b); + si.strokeColor = buf; + snprintf(buf, 15, "%.3fpt", style->stroke_width.value); + si.strokeWidth = buf; + si.stroke = "solid"; + double opacityPercent = 100.0 * + (SP_SCALE24_TO_FLOAT(style->stroke_opacity.value)); + snprintf(buf, 15, "%.3f%%", opacityPercent); + si.strokeOpacity = buf; + } + else if (style->stroke.isPaintserver()) + { + SPGradient *gradient = SP_GRADIENT(SP_STYLE_STROKE_SERVER(style)); + if (gradient) + { + si.stroke = "gradient"; + } + } + + //Look for existing identical style; + bool styleMatch = false; + std::vector::iterator iter; + for (iter=styleTable.begin() ; iter!=styleTable.end() ; ++iter) + { + if (si.equals(*iter)) + { + //map to existing styleTable entry + Glib::ustring styleName = iter->name; + styleLookupTable[id] = styleName; + styleMatch = true; + break; + } + } + + // Don't need a new style + if (styleMatch) + { + return false; + } + + Glib::ustring styleName = Glib::ustring::compose("style%1", styleTable.size()); + si.name = styleName; + styleTable.push_back(si); + styleLookupTable[id] = styleName; + + output = Glib::ustring::compose ("\n", si.name); + output += "style; + if (!style) + { + return false; + } + + if ((checkFillGradient? (!style->fill.isPaintserver()) : (!style->stroke.isPaintserver()))) + { + return false; + } + + //## Gradient + SPGradient *gradient = SP_GRADIENT((checkFillGradient?(SP_STYLE_FILL_SERVER(style)) :(SP_STYLE_STROKE_SERVER(style)))); + + if (gradient == nullptr) + { + return false; + } + GradientInfo gi; + SPGradient *grvec = gradient->getVector(FALSE); + for (SPStop *stop = grvec->getFirstStop(); + stop ; stop = stop->getNextStop()) + { + unsigned long rgba = stop->get_rgba32(); + unsigned long rgb = (rgba >> 8) & 0xffffff; + double opacity = (static_cast(rgba & 0xff)) / 256.0; + GradientStop gs(rgb, opacity); + gi.stops.push_back(gs); + } + + Glib::ustring gradientName2; + if (SP_IS_LINEARGRADIENT(gradient)) + { + gi.style = "linear"; + SPLinearGradient *linGrad = SP_LINEARGRADIENT(gradient); + gi.x1 = linGrad->x1.value; + gi.y1 = linGrad->y1.value; + gi.x2 = linGrad->x2.value; + gi.y2 = linGrad->y2.value; + gradientName2 = Glib::ustring::compose("ImportedLinearGradient%1", gradientTable.size()); + } + else if (SP_IS_RADIALGRADIENT(gradient)) + { + gi.style = "radial"; + SPRadialGradient *radGrad = SP_RADIALGRADIENT(gradient); + Geom::OptRect bbox = item->documentVisualBounds(); + gi.cx = (radGrad->cx.value-bbox->left())/bbox->width(); + gi.cy = (radGrad->cy.value-bbox->top())/bbox->height(); + gradientName2 = Glib::ustring::compose("ImportedRadialGradient%1", gradientTable.size()); + } + else + { + g_warning("not a supported gradient type"); + return false; + } + + //Look for existing identical style; + bool gradientMatch = false; + std::vector::iterator iter; + for (iter=gradientTable.begin() ; iter!=gradientTable.end() ; ++iter) + { + if (gi.equals(*iter)) + { + //map to existing gradientTable entry + gradientName = iter->name; + gradientLookupTable[id] = gradientName; + gradientMatch = true; + break; + } + } + + if (gradientMatch) + { + return true; + } + + // No match, let us write a new entry + gradientName = gradientName2; + gi.name = gradientName; + gradientTable.push_back(gi); + gradientLookupTable[id] = gradientName; + + // int gradientCount = gradientTable.size(); + char buf[128]; + if (gi.style == "linear") + { + /* + =================================================================== + LINEAR gradient. We need something that looks like this: + + =================================================================== + */ + if (gi.stops.size() < 2) + { + g_warning("Need at least 2 stops for a linear gradient"); + return false; + } + output += Glib::ustring::compose("\n", + gi.stops[0].opacity * 100.0, gi.stops[1].opacity * 100.0, angle);// draw:border=\"0%%\" + } + else if (gi.style == "radial") + { + /* + =================================================================== + RADIAL gradient. We need something that looks like this: + + + =================================================================== + */ + if (gi.stops.size() < 2) + { + g_warning("Need at least 2 stops for a radial gradient"); + return false; + } + output += Glib::ustring::compose("getObjectByRepr(node); + if (!reprobj) + { + return true; + } + if (!SP_IS_ITEM(reprobj)) + { + return true; + } + SPItem *item = SP_ITEM(reprobj); + + Glib::ustring nodeName = node->name(); + Glib::ustring id = getAttribute(node, "id"); + Geom::Affine tf = getODFTransform(item);//Get SVG-to-ODF transform + Geom::OptRect bbox = getODFBoundingBox(item);//Get ODF bounding box params for item + if (!bbox) { + return true; + } + + double bbox_x = bbox->min()[Geom::X]; + double bbox_y = bbox->min()[Geom::Y]; + double bbox_width = (*bbox)[Geom::X].extent(); + double bbox_height = (*bbox)[Geom::Y].extent(); + + double rotate; + double xskew; + double yskew; + double xscale; + double yscale; + analyzeTransform(tf, rotate, xskew, yskew, xscale, yscale); + + //# Do our stuff + SPCurve *curve = nullptr; + + if (nodeName == "svg" || nodeName == "svg:svg") + { + //# Iterate through the children + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + { + if (!writeTree(couts, souts, child)) + { + return false; + } + } + return true; + } + else if (nodeName == "g" || nodeName == "svg:g") + { + if (!id.empty()) + { + couts.printf("\n", id.c_str()); + } + else + { + couts.printf("\n"); + } + //# Iterate through the children + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + { + if (!writeTree(couts, souts, child)) + { + return false; + } + } + if (!id.empty()) + { + couts.printf(" \n", id.c_str()); + } + else + { + couts.printf("\n"); + } + return true; + } + + //# GRADIENT + Glib::ustring gradientNameFill; + Glib::ustring gradientNameStroke; + Glib::ustring outputFill; + Glib::ustring outputStroke; + Glib::ustring outputStyle; + + processGradient(item, id, tf, gradientNameFill, outputFill, true); + processGradient(item, id, tf, gradientNameStroke, outputStroke, false); + souts.writeUString(outputFill); + souts.writeUString(outputStroke); + + //# STYLE + processStyle(item, id, gradientNameFill, gradientNameStroke, outputStyle); + souts.writeUString(outputStyle); + + //# ITEM DATA + if (nodeName == "image" || nodeName == "svg:image") + { + if (!SP_IS_IMAGE(item)) + { + g_warning(" is not an SPImage."); + return false; + } + + SPImage *img = SP_IMAGE(item); + double ix = img->x.value; + double iy = img->y.value; + double iwidth = img->width.value; + double iheight = img->height.value; + + Geom::Point ibbox_min = Geom::Point(ix, iy) * tf; + ix = ibbox_min.x(); + iy = ibbox_min.y(); + iwidth = xscale * iwidth; + iheight = yscale * iheight; + + Geom::Affine itemTransform = getODFItemTransform(item); + + Glib::ustring itemTransformString = formatTransform(itemTransform); + + Glib::ustring href = getAttribute(node, "xlink:href"); + std::map::iterator iter = imageTable.find(href); + if (iter == imageTable.end()) + { + g_warning("image '%s' not in table", href.c_str()); + return false; + } + Glib::ustring newName = iter->second; + + couts.printf("\n"); + couts.printf(" \n"); + couts.writeString(" \n"); + couts.writeString(" \n"); + couts.writeString("\n"); + return true; + } + else if (SP_IS_SHAPE(item)) + { + curve = SP_SHAPE(item)->getCurve(); + } + else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) + { + curve = te_get_layout(item)->convertToCurves(); + } + + if (curve) + { + //### Default output + couts.writeString("::iterator siter; + siter = styleLookupTable.find(id); + if (siter != styleLookupTable.end()) + { + Glib::ustring styleName = siter->second; + couts.printf("draw:style-name=\"%s\" ", styleName.c_str()); + } + + couts.printf("draw:layer=\"layout\" svg:x=\"%.3fcm\" svg:y=\"%.3fcm\" ", + bbox_x, bbox_y); + couts.printf("svg:width=\"%.3fcm\" svg:height=\"%.3fcm\" ", + bbox_width, bbox_height); + couts.printf("svg:viewBox=\"0.0 0.0 %.3f %.3f\"", + bbox_width * 1000.0, bbox_height * 1000.0); + + couts.printf(" svg:d=\""); + int nrPoints = writePath(couts, curve->get_pathvector(), + tf, bbox_x, bbox_y); + couts.writeString("\""); + + couts.writeString(">\n"); + couts.printf(" \n", nrPoints); + couts.writeString("\n\n"); + + curve->unref(); + } + + return true; +} + + +/** + * Write the header for the content.xml file + */ +bool OdfOutput::writeStyleHeader(Writer &outs) +{ + time_t tim; + time(&tim); + + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + + return true; +} + + +/** + * Write the footer for the style.xml file + */ +bool OdfOutput::writeStyleFooter(Writer &outs) +{ + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + +///TODO: add default document style here + + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString(" \n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString(" \n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString(" \n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + + return true; +} + + +/** + * Write the header for the content.xml file + */ +bool OdfOutput::writeContentHeader(Writer &outs) +{ + time_t tim; + time(&tim); + + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + return true; +} + + +/** + * Write the footer for the content.xml file + */ +bool OdfOutput::writeContentFooter(Writer &outs) +{ + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("\n"); + return true; +} + + +/** + * Write the content.xml file. Writes the namesspace headers, then + * calls writeTree(). + */ +bool OdfOutput::writeContent(ZipFile &zf, Inkscape::XML::Node *node) +{ + //Content.xml stream + BufferOutputStream cbouts; + OutputStreamWriter couts(cbouts); + + if (!writeContentHeader(couts)) + { + return false; + } + + //Style.xml stream + BufferOutputStream sbouts; + OutputStreamWriter souts(sbouts); + + if (!writeStyleHeader(souts)) + { + return false; + } + + //# Descend into the tree, doing all of our conversions + //# to both files at the same time + char *oldlocale = g_strdup (setlocale (LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + if (!writeTree(couts, souts, node)) + { + g_warning("Failed to convert SVG tree"); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + return false; + } + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + + //# Finish content file + if (!writeContentFooter(couts)) + { + return false; + } + + ZipEntry *ze = zf.newEntry("content.xml", "ODF master content file"); + ze->setUncompressedData(cbouts.getBuffer()); + ze->finish(); + + //# Finish style file + if (!writeStyleFooter(souts)) + { + return false; + } + + ze = zf.newEntry("styles.xml", "ODF style file"); + ze->setUncompressedData(sbouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * Resets class to its pristine condition, ready to use again + */ +void OdfOutput::reset() +{ + metadata.clear(); + styleTable.clear(); + styleLookupTable.clear(); + gradientTable.clear(); + gradientLookupTable.clear(); + imageTable.clear(); +} + + +/** + * Descends into the SVG tree, mapping things to ODF when appropriate + */ +void OdfOutput::save(Inkscape::Extension::Output */*mod*/, SPDocument *doc, gchar const *filename) +{ + if (doc != SP_ACTIVE_DOCUMENT) { + g_warning("OdfOutput can only save the active document"); + return; + } + + reset(); + + docBaseUri = Inkscape::URI::from_dirname(doc->getDocumentBase()).str(); + + ZipFile zf; + preprocess(zf, doc->getReprRoot()); + + if (!writeManifest(zf)) + { + g_warning("Failed to write manifest"); + return; + } + + if (!writeContent(zf, doc->getReprRoot())) + { + g_warning("Failed to write content"); + return; + } + + if (!writeMeta(zf)) + { + g_warning("Failed to write metafile"); + return; + } + + if (!zf.writeFile(filename)) + { + return; + } +} + + +/** + * This is the definition of PovRay output. This function just + * calls the extension system with the memory allocated XML that + * describes the data. +*/ +void OdfOutput::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("OpenDocument Drawing Output") "\n" + "org.inkscape.output.odf\n" + "\n" + ".odg\n" + "text/x-povray-script\n" + "" N_("OpenDocument drawing (*.odg)") "\n" + "" N_("OpenDocument drawing file") "\n" + "\n" + "", + new OdfOutput()); +} + +/** + * Make sure that we are in the database + */ +bool OdfOutput::check (Inkscape::Extension::Extension */*module*/) +{ + /* We don't need a Key + if (NULL == Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_POV)) + return FALSE; + */ + + return TRUE; +} + +} //namespace Internal +} //namespace Extension +} //namespace Inkscape + + +//######################################################################## +//# E N D O F F I L E +//######################################################################## + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/odf.h b/src/extension/internal/odf.h new file mode 100644 index 0000000..94cd360 --- /dev/null +++ b/src/extension/internal/odf.h @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * OpenDocument (drawing) input and output + *//* + * Authors: + * Bob Jamison + * Abhishek Sharma + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_ODG_OUT_H +#define EXTENSION_INTERNAL_ODG_OUT_H + +#include + +#include "extension/implementation/implementation.h" + +#include +#include +#include + +#include "object/uri.h" +class SPItem; + +#include + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + +typedef Inkscape::IO::Writer Writer; + +class StyleInfo +{ +public: + + StyleInfo() + { + init(); + } + + StyleInfo(const StyleInfo &other) + { + assign(other); + } + + StyleInfo &operator=(const StyleInfo &other) + { + assign(other); + return *this; + } + + void assign(const StyleInfo &other) + { + name = other.name; + stroke = other.stroke; + strokeColor = other.strokeColor; + strokeWidth = other.strokeWidth; + strokeOpacity = other.strokeOpacity; + fill = other.fill; + fillColor = other.fillColor; + fillOpacity = other.fillOpacity; + } + + void init() + { + name = "none"; + stroke = "none"; + strokeColor = "none"; + strokeWidth = "none"; + strokeOpacity = "none"; + fill = "none"; + fillColor = "none"; + fillOpacity = "none"; + } + + virtual ~StyleInfo() + = default; + + //used for eliminating duplicates in the styleTable + bool equals(const StyleInfo &other) + { + if ( + stroke != other.stroke || + strokeColor != other.strokeColor || + strokeWidth != other.strokeWidth || + strokeOpacity != other.strokeOpacity || + fill != other.fill || + fillColor != other.fillColor || + fillOpacity != other.fillOpacity + ) + return false; + return true; + } + + Glib::ustring name; + Glib::ustring stroke; + Glib::ustring strokeColor; + Glib::ustring strokeWidth; + Glib::ustring strokeOpacity; + Glib::ustring fill; + Glib::ustring fillColor; + Glib::ustring fillOpacity; + +}; + + + + +class GradientStop +{ +public: + GradientStop() : rgb(0), opacity(0) + {} + GradientStop(unsigned long rgbArg, double opacityArg) + { rgb = rgbArg; opacity = opacityArg; } + virtual ~GradientStop() + = default; + GradientStop(const GradientStop &other) + { assign(other); } + virtual GradientStop& operator=(const GradientStop &other) + { assign(other); return *this; } + void assign(const GradientStop &other) + { + rgb = other.rgb; + opacity = other.opacity; + } + unsigned long rgb; + double opacity; +}; + + + +class GradientInfo +{ +public: + + GradientInfo() + { + init(); + } + + GradientInfo(const GradientInfo &other) + { + assign(other); + } + + GradientInfo &operator=(const GradientInfo &other) + { + assign(other); + return *this; + } + + void assign(const GradientInfo &other) + { + name = other.name; + style = other.style; + cx = other.cx; + cy = other.cy; + fx = other.fx; + fy = other.fy; + r = other.r; + x1 = other.x1; + y1 = other.y1; + x2 = other.x2; + y2 = other.y2; + stops = other.stops; + } + + void init() + { + name = "none"; + style = "none"; + cx = 0.0; + cy = 0.0; + fx = 0.0; + fy = 0.0; + r = 0.0; + x1 = 0.0; + y1 = 0.0; + x2 = 0.0; + y2 = 0.0; + stops.clear(); + } + + virtual ~GradientInfo() + = default; + + //used for eliminating duplicates in the styleTable + bool equals(const GradientInfo &other) + { + if ( + name != other.name || + style != other.style || + cx != other.cx || + cy != other.cy || + fx != other.fx || + fy != other.fy || + r != other.r || + x1 != other.x1 || + y1 != other.y1 || + x2 != other.x2 || + y2 != other.y2 + ) + return false; + if (stops.size() != other.stops.size()) + return false; + for (unsigned int i=0 ; i stops; + +}; + + + +/** + * OpenDocument input and output + * + * This is an an entry in the extensions mechanism to begin to enable + * the inputting and outputting of OpenDocument Format (ODF) files from + * within Inkscape. Although the initial implementations will be very lossy + * do to the differences in the models of SVG and ODF, they will hopefully + * improve greatly with time. + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + */ +class OdfOutput : public Inkscape::Extension::Implementation::Implementation +{ + +public: + + bool check (Inkscape::Extension::Extension * module) override; + + void save (Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + + static void init (); + +private: + + std::string docBaseUri; + + void reset(); + + //cc or dc metadata name/value pairs + std::map metadata; + + /* Style table + Uses a two-stage lookup to avoid style duplication. + Use like: + StyleInfo si = styleTable[styleLookupTable[id]]; + but check for errors, of course + */ + //element id -> style entry name + std::map styleLookupTable; + //style entry name -> style info + std::vector styleTable; + + //element id -> gradient entry name + std::map gradientLookupTable; + //gradient entry name -> gradient info + std::vector gradientTable; + + //for renaming image file names + std::map imageTable; + + void preprocess(ZipFile &zf, Inkscape::XML::Node *node); + + bool writeManifest(ZipFile &zf); + + bool writeMeta(ZipFile &zf); + + bool writeStyle(ZipFile &zf); + + bool processStyle(SPItem *item, const Glib::ustring &id, const Glib::ustring &gradientNameFill, const Glib::ustring &gradientNameStroke, Glib::ustring& output); + + bool processGradient(SPItem *item, + const Glib::ustring &id, Geom::Affine &tf, Glib::ustring& gradientName, Glib::ustring& output, bool checkFillGradient = true); + + bool writeStyleHeader(Writer &outs); + + bool writeStyleFooter(Writer &outs); + + bool writeContentHeader(Writer &outs); + + bool writeContentFooter(Writer &outs); + + bool writeTree(Writer &couts, Writer &souts, Inkscape::XML::Node *node); + + bool writeContent(ZipFile &zf, Inkscape::XML::Node *node); + +}; + + +} //namespace Internal +} //namespace Extension +} //namespace Inkscape + + + +#endif /* EXTENSION_INTERNAL_ODG_OUT_H */ + diff --git a/src/extension/internal/pdfinput/pdf-input.cpp b/src/extension/internal/pdfinput/pdf-input.cpp new file mode 100644 index 0000000..a79ceae --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.cpp @@ -0,0 +1,988 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "pdf-input.h" + +#ifdef HAVE_POPPLER +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_POPPLER_CAIRO +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "document-undo.h" +#include "extension/input.h" +#include "extension/system.h" +#include "inkscape.h" +#include "object/sp-root.h" +#include "pdf-parser.h" +#include "svg-builder.h" +#include "ui/dialog-events.h" +#include "ui/widget/frame.h" +#include "ui/widget/spinbutton.h" +#include "util/units.h" + + + +namespace { + +void sanitize_page_number(int &page_num, const int num_pages) { + if (page_num < 1 || page_num > num_pages) { + std::cerr << "Inkscape::Extension::Internal::PdfInput::open: Bad page number " + << page_num + << ". Import first page instead." + << std::endl; + page_num = 1; + } +} + +} + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + * \brief The PDF import dialog + * FIXME: Probably this should be placed into src/ui/dialog + */ + +static const gchar * crop_setting_choices[] = { + //TRANSLATORS: The following are document crop settings for PDF import + // more info: http://www.acrobatusers.com/tech_corners/javascript_corner/tips/2006/page_bounds/ + N_("media box"), + N_("crop box"), + N_("trim box"), + N_("bleed box"), + N_("art box") +}; + +PdfImportDialog::PdfImportDialog(std::shared_ptr doc, const gchar */*uri*/) + : _pdf_doc(std::move(doc)) +{ + assert(_pdf_doc); +#ifdef HAVE_POPPLER_CAIRO + _poppler_doc = NULL; +#endif // HAVE_POPPLER_CAIRO + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + + // Page number + auto _pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _pdf_doc->getNumPages(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(_pageNumberSpin_adj, 1, 1)); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + hbox2 = Gtk::manage(new class Gtk::HBox(false, 0)); + // Disable the page selector when there's only one page + int num_pages = _pdf_doc->getCatalog()->getNumPages(); + if ( num_pages == 1 ) { + _pageNumberSpin->set_sensitive(false); + } else { + // Display total number of pages + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + } + + // Crop settings + _cropCheck = Gtk::manage(new class Gtk::CheckButton(_("Clip to:"))); + _cropTypeCombo = Gtk::manage(new class Gtk::ComboBoxText()); + int num_crop_choices = sizeof(crop_setting_choices) / sizeof(crop_setting_choices[0]); + for ( int i = 0 ; i < num_crop_choices ; i++ ) { + _cropTypeCombo->append(_(crop_setting_choices[i])); + } + _cropTypeCombo->set_active_text(_(crop_setting_choices[0])); + _cropTypeCombo->set_sensitive(false); + + hbox3 = Gtk::manage(new class Gtk::HBox(false, 4)); + vbox2 = Gtk::manage(new class Gtk::VBox(false, 4)); + _pageSettingsFrame = Gtk::manage(new class Inkscape::UI::Widget::Frame(_("Page settings"))); + _labelPrecision = Gtk::manage(new class Gtk::Label(_("Precision of approximating gradient meshes:"))); + _labelPrecisionWarning = Gtk::manage(new class Gtk::Label(_("Note: setting the precision too high may result in a large SVG file and slow performance."))); + _labelPrecisionWarning->set_max_width_chars(50); + +#ifdef HAVE_POPPLER_CAIRO + Gtk::RadioButton::Group group; + _importViaPoppler = Gtk::manage(new class Gtk::RadioButton(group,_("Poppler/Cairo import"))); + _labelViaPoppler = Gtk::manage(new class Gtk::Label(_("Import via external library. Text consists of groups containing cloned glyphs where each glyph is a path. Images are stored internally. Meshes cause entire document to be rendered as a raster image."))); + _labelViaPoppler->set_max_width_chars(50); + _importViaInternal = Gtk::manage(new class Gtk::RadioButton(group,_("Internal import"))); + _labelViaInternal = Gtk::manage(new class Gtk::Label(_("Import via internal (Poppler derived) library. Text is stored as text but white space is missing. Meshes are converted to tiles, the number depends on the precision set below."))); + _labelViaInternal->set_max_width_chars(50); +#endif + + _fallbackPrecisionSlider_adj = Gtk::Adjustment::create(2, 1, 256, 1, 10, 10); + _fallbackPrecisionSlider = Gtk::manage(new class Gtk::Scale(_fallbackPrecisionSlider_adj)); + _fallbackPrecisionSlider->set_value(2.0); + _labelPrecisionComment = Gtk::manage(new class Gtk::Label(_("rough"))); + hbox6 = Gtk::manage(new class Gtk::HBox(false, 4)); + + // Text options + // _labelText = Gtk::manage(new class Gtk::Label(_("Text handling:"))); + // _textHandlingCombo = Gtk::manage(new class Gtk::ComboBoxText()); + // _textHandlingCombo->append(_("Import text as text")); + // _textHandlingCombo->set_active_text(_("Import text as text")); + // hbox5 = Gtk::manage(new class Gtk::HBox(false, 4)); + + // Font option + _localFontsCheck = Gtk::manage(new class Gtk::CheckButton(_("Replace PDF fonts by closest-named installed fonts"))); + + _embedImagesCheck = Gtk::manage(new class Gtk::CheckButton(_("Embed images"))); + vbox3 = Gtk::manage(new class Gtk::VBox(false, 4)); + _importSettingsFrame = Gtk::manage(new class Inkscape::UI::Widget::Frame(_("Import settings"))); + vbox1 = Gtk::manage(new class Gtk::VBox(false, 4)); + _previewArea = Gtk::manage(new class Gtk::DrawingArea()); + hbox1 = Gtk::manage(new class Gtk::HBox(false, 4)); + cancelbutton->set_can_focus(); + cancelbutton->set_can_default(); + cancelbutton->set_relief(Gtk::RELIEF_NORMAL); + okbutton->set_can_focus(); + okbutton->set_can_default(); + okbutton->set_relief(Gtk::RELIEF_NORMAL); + + _labelSelect->set_xalign(0.5); + _labelSelect->set_yalign(0.5); + _labelTotalPages->set_xalign(0.5); + _labelTotalPages->set_yalign(0.5); + _labelPrecision->set_xalign(0.0); + _labelPrecision->set_yalign(0.5); + _labelPrecisionWarning->set_xalign(0.0); + _labelPrecisionWarning->set_yalign(0.5); + _labelPrecisionComment->set_xalign(0.5); + _labelPrecisionComment->set_yalign(0.5); + + _labelSelect->set_margin_start(4); + _labelSelect->set_margin_end(4); + _labelTotalPages->set_margin_start(4); + _labelTotalPages->set_margin_end(4); + _labelPrecision->set_margin_start(4); + _labelPrecision->set_margin_end(4); + _labelPrecisionWarning->set_margin_start(4); + _labelPrecisionWarning->set_margin_end(4); + _labelPrecisionComment->set_margin_start(4); + _labelPrecisionComment->set_margin_end(4); + + _labelSelect->set_justify(Gtk::JUSTIFY_LEFT); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_digits(0); + _pageNumberSpin->set_wrap(false); + _labelTotalPages->set_justify(Gtk::JUSTIFY_LEFT); + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + hbox2->pack_start(*_labelSelect, Gtk::PACK_SHRINK, 4); + hbox2->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK, 4); + hbox2->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK, 4); + _cropCheck->set_can_focus(); + _cropCheck->set_relief(Gtk::RELIEF_NORMAL); + _cropCheck->set_mode(true); + _cropCheck->set_active(false); + _cropTypeCombo->set_border_width(1); + hbox3->pack_start(*_cropCheck, Gtk::PACK_SHRINK, 4); + hbox3->pack_start(*_cropTypeCombo, Gtk::PACK_SHRINK, 0); + vbox2->pack_start(*hbox2); + vbox2->pack_start(*hbox3); + _pageSettingsFrame->add(*vbox2); + _pageSettingsFrame->set_border_width(4); + _labelPrecision->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecision->set_line_wrap(true); + _labelPrecision->set_use_markup(false); + _labelPrecision->set_selectable(false); + _labelPrecisionWarning->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecisionWarning->set_line_wrap(true); + _labelPrecisionWarning->set_use_markup(true); + _labelPrecisionWarning->set_selectable(false); + +#ifdef HAVE_POPPLER_CAIRO + _importViaPoppler->set_can_focus(); + _importViaPoppler->set_relief(Gtk::RELIEF_NORMAL); + _importViaPoppler->set_mode(true); + _importViaPoppler->set_active(false); + _importViaInternal->set_can_focus(); + _importViaInternal->set_relief(Gtk::RELIEF_NORMAL); + _importViaInternal->set_mode(true); + _importViaInternal->set_active(true); + _labelViaPoppler->set_line_wrap(true); + _labelViaInternal->set_line_wrap(true); + _labelViaPoppler->set_xalign(0); + _labelViaInternal->set_xalign(0); +#endif + + _fallbackPrecisionSlider->set_size_request(180,-1); + _fallbackPrecisionSlider->set_can_focus(); + _fallbackPrecisionSlider->set_inverted(false); + _fallbackPrecisionSlider->set_digits(1); + _fallbackPrecisionSlider->set_draw_value(true); + _fallbackPrecisionSlider->set_value_pos(Gtk::POS_TOP); + _labelPrecisionComment->set_size_request(90,-1); + _labelPrecisionComment->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecisionComment->set_line_wrap(false); + _labelPrecisionComment->set_use_markup(false); + _labelPrecisionComment->set_selectable(false); + hbox6->pack_start(*_fallbackPrecisionSlider, Gtk::PACK_SHRINK, 4); + hbox6->pack_start(*_labelPrecisionComment, Gtk::PACK_SHRINK, 0); + // _labelText->set_alignment(0.5,0.5); + // _labelText->set_padding(4,0); + // _labelText->set_justify(Gtk::JUSTIFY_LEFT); + // _labelText->set_line_wrap(false); + // _labelText->set_use_markup(false); + // _labelText->set_selectable(false); + // hbox5->pack_start(*_labelText, Gtk::PACK_SHRINK, 0); + // hbox5->pack_start(*_textHandlingCombo, Gtk::PACK_SHRINK, 0); + _localFontsCheck->set_can_focus(); + _localFontsCheck->set_relief(Gtk::RELIEF_NORMAL); + _localFontsCheck->set_mode(true); + _localFontsCheck->set_active(true); + _embedImagesCheck->set_can_focus(); + _embedImagesCheck->set_relief(Gtk::RELIEF_NORMAL); + _embedImagesCheck->set_mode(true); + _embedImagesCheck->set_active(true); +#ifdef HAVE_POPPLER_CAIRO + vbox3->pack_start(*_importViaPoppler, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelViaPoppler, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_importViaInternal, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelViaInternal, Gtk::PACK_SHRINK, 0); +#endif + vbox3->pack_start(*_localFontsCheck, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_embedImagesCheck, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelPrecision, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*hbox6, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelPrecisionWarning, Gtk::PACK_SHRINK, 0); + // vbox3->pack_start(*hbox5, Gtk::PACK_SHRINK, 4); + _importSettingsFrame->add(*vbox3); + _importSettingsFrame->set_border_width(4); + vbox1->pack_start(*_pageSettingsFrame, Gtk::PACK_SHRINK, 0); + vbox1->pack_start(*_importSettingsFrame, Gtk::PACK_SHRINK, 0); + hbox1->pack_start(*vbox1); + hbox1->pack_start(*_previewArea, Gtk::PACK_SHRINK, 4); + + get_content_area()->set_homogeneous(false); + get_content_area()->set_spacing(0); + get_content_area()->pack_start(*hbox1); + + this->set_title(_("PDF Import Settings")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + this->add_action_widget(*cancelbutton, -6); + this->add_action_widget(*okbutton, -5); + + this->show_all(); + + // Connect signals + _previewArea->signal_draw().connect(sigc::mem_fun(*this, &PdfImportDialog::_onDraw)); + _pageNumberSpin_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_onPageNumberChanged)); + _cropCheck->signal_toggled().connect(sigc::mem_fun(*this, &PdfImportDialog::_onToggleCropping)); + _fallbackPrecisionSlider_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_onPrecisionChanged)); +#ifdef HAVE_POPPLER_CAIRO + _importViaPoppler->signal_toggled().connect(sigc::mem_fun(*this, &PdfImportDialog::_onToggleImport)); +#endif + + _render_thumb = false; +#ifdef HAVE_POPPLER_CAIRO + _cairo_surface = NULL; + _render_thumb = true; + + // Create PopplerDocument + Glib::ustring filename = _pdf_doc->getFileName()->getCString(); + if (!Glib::path_is_absolute(filename)) { + filename = Glib::build_filename(Glib::get_current_dir(),filename); + } + Glib::ustring full_uri = Glib::filename_to_uri(filename); + + if (!full_uri.empty()) { + _poppler_doc = poppler_document_new_from_file(full_uri.c_str(), NULL, NULL); + } + + // Set sensitivity of some widgets based on selected import type. + _onToggleImport(); +#endif + + // Set default preview size + _preview_width = 200; + _preview_height = 300; + + // Init preview + _thumb_data = nullptr; + _pageNumberSpin_adj->set_value(1.0); + _current_page = 1; + _setPreviewPage(_current_page); + + set_default (*okbutton); + set_focus (*okbutton); +} + +PdfImportDialog::~PdfImportDialog() { +#ifdef HAVE_POPPLER_CAIRO + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + if (_poppler_doc) { + g_object_unref(G_OBJECT(_poppler_doc)); + } +#endif + if (_thumb_data) { + gfree(_thumb_data); + } +} + +bool PdfImportDialog::showDialog() { + show(); + gint b = run(); + hide(); + if ( b == Gtk::RESPONSE_OK ) { + return TRUE; + } else { + return FALSE; + } +} + +int PdfImportDialog::getSelectedPage() { + return _current_page; +} + +bool PdfImportDialog::getImportMethod() { +#ifdef HAVE_POPPLER_CAIRO + return _importViaPoppler->get_active(); +#else + return false; +#endif +} + +/** + * \brief Retrieves the current settings into a repr which SvgBuilder will use + * for determining the behaviour desired by the user + */ +void PdfImportDialog::getImportSettings(Inkscape::XML::Node *prefs) { + sp_repr_set_svg_double(prefs, "selectedPage", (double)_current_page); + if (_cropCheck->get_active()) { + Glib::ustring current_choice = _cropTypeCombo->get_active_text(); + int num_crop_choices = sizeof(crop_setting_choices) / sizeof(crop_setting_choices[0]); + int i = 0; + for ( ; i < num_crop_choices ; i++ ) { + if ( current_choice == _(crop_setting_choices[i]) ) { + break; + } + } + sp_repr_set_svg_double(prefs, "cropTo", (double)i); + } else { + sp_repr_set_svg_double(prefs, "cropTo", -1.0); + } + sp_repr_set_svg_double(prefs, "approximationPrecision", + _fallbackPrecisionSlider->get_value()); + if (_localFontsCheck->get_active()) { + prefs->setAttribute("localFonts", "1"); + } else { + prefs->setAttribute("localFonts", "0"); + } + if (_embedImagesCheck->get_active()) { + prefs->setAttribute("embedImages", "1"); + } else { + prefs->setAttribute("embedImages", "0"); + } +#ifdef HAVE_POPPLER_CAIRO + if (_importViaPoppler->get_active()) { + prefs->setAttribute("importviapoppler", "1"); + } else { + prefs->setAttribute("importviapoppler", "0"); + } +#endif +} + +/** + * \brief Redisplay the comment on the current approximation precision setting + * Evenly divides the interval of possible values between the available labels. + */ +void PdfImportDialog::_onPrecisionChanged() { + + static Glib::ustring precision_comments[] = { + Glib::ustring(C_("PDF input precision", "rough")), + Glib::ustring(C_("PDF input precision", "medium")), + Glib::ustring(C_("PDF input precision", "fine")), + Glib::ustring(C_("PDF input precision", "very fine")) + }; + + double min = _fallbackPrecisionSlider_adj->get_lower(); + double max = _fallbackPrecisionSlider_adj->get_upper(); + int num_intervals = sizeof(precision_comments) / sizeof(precision_comments[0]); + double interval_len = ( max - min ) / (double)num_intervals; + double value = _fallbackPrecisionSlider_adj->get_value(); + int comment_idx = (int)floor( ( value - min ) / interval_len ); + _labelPrecisionComment->set_label(precision_comments[comment_idx]); +} + +void PdfImportDialog::_onToggleCropping() { + _cropTypeCombo->set_sensitive(_cropCheck->get_active()); +} + +void PdfImportDialog::_onPageNumberChanged() { + int page = _pageNumberSpin->get_value_as_int(); + _current_page = CLAMP(page, 1, _pdf_doc->getCatalog()->getNumPages()); + _setPreviewPage(_current_page); +} + +#ifdef HAVE_POPPLER_CAIRO +void PdfImportDialog::_onToggleImport() { + if( _importViaPoppler->get_active() ) { + hbox3->set_sensitive(false); + _localFontsCheck->set_sensitive(false); + _embedImagesCheck->set_sensitive(false); + hbox6->set_sensitive(false); + } else { + hbox3->set_sensitive(); + _localFontsCheck->set_sensitive(); + _embedImagesCheck->set_sensitive(); + hbox6->set_sensitive(); + } +} +#endif + + +#ifdef HAVE_POPPLER_CAIRO +/** + * \brief Copies image data from a Cairo surface to a pixbuf + * + * Borrowed from libpoppler, from the file poppler-page.cc + * Copyright (C) 2005, Red Hat, Inc. + * + */ +static void copy_cairo_surface_to_pixbuf (cairo_surface_t *surface, + unsigned char *data, + GdkPixbuf *pixbuf) +{ + int cairo_width, cairo_height, cairo_rowstride; + unsigned char *pixbuf_data, *dst, *cairo_data; + int pixbuf_rowstride, pixbuf_n_channels; + unsigned int *src; + int x, y; + + cairo_width = cairo_image_surface_get_width (surface); + cairo_height = cairo_image_surface_get_height (surface); + cairo_rowstride = cairo_width * 4; + cairo_data = data; + + pixbuf_data = gdk_pixbuf_get_pixels (pixbuf); + pixbuf_rowstride = gdk_pixbuf_get_rowstride (pixbuf); + pixbuf_n_channels = gdk_pixbuf_get_n_channels (pixbuf); + + if (cairo_width > gdk_pixbuf_get_width (pixbuf)) + cairo_width = gdk_pixbuf_get_width (pixbuf); + if (cairo_height > gdk_pixbuf_get_height (pixbuf)) + cairo_height = gdk_pixbuf_get_height (pixbuf); + for (y = 0; y < cairo_height; y++) + { + src = reinterpret_cast(cairo_data + y * cairo_rowstride); + dst = pixbuf_data + y * pixbuf_rowstride; + for (x = 0; x < cairo_width; x++) + { + dst[0] = (*src >> 16) & 0xff; + dst[1] = (*src >> 8) & 0xff; + dst[2] = (*src >> 0) & 0xff; + if (pixbuf_n_channels == 4) + dst[3] = (*src >> 24) & 0xff; + dst += pixbuf_n_channels; + src++; + } + } +} + +#endif + +bool PdfImportDialog::_onDraw(const Cairo::RefPtr& cr) { + // Check if we have a thumbnail at all + if (!_thumb_data) { + return true; + } + + // Create the pixbuf for the thumbnail + Glib::RefPtr thumb; + + if (_render_thumb) { + thumb = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, + 8, _thumb_width, _thumb_height); + } else { + thumb = Gdk::Pixbuf::create_from_data(_thumb_data, Gdk::COLORSPACE_RGB, + false, 8, _thumb_width, _thumb_height, _thumb_rowstride); + } + if (!thumb) { + return true; + } + + // Set background to white + if (_render_thumb) { + thumb->fill(0xffffffff); + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, 0); + cr->paint(); + } +#ifdef HAVE_POPPLER_CAIRO + // Copy the thumbnail image from the Cairo surface + if (_render_thumb) { + copy_cairo_surface_to_pixbuf(_cairo_surface, _thumb_data, thumb->gobj()); + } +#endif + + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, _render_thumb ? 0 : 20); + cr->paint(); + return true; +} + +/** + * \brief Renders the given page's thumbnail using Cairo + */ +void PdfImportDialog::_setPreviewPage(int page) { + + _previewed_page = _pdf_doc->getCatalog()->getPage(page); + g_return_if_fail(_previewed_page); + // Try to get a thumbnail from the PDF if possible + if (!_render_thumb) { + if (_thumb_data) { + gfree(_thumb_data); + _thumb_data = nullptr; + } + if (!_previewed_page->loadThumb(&_thumb_data, + &_thumb_width, &_thumb_height, &_thumb_rowstride)) { + return; + } + // Redraw preview area + _previewArea->set_size_request(_thumb_width, _thumb_height + 20); + _previewArea->queue_draw(); + return; + } +#ifdef HAVE_POPPLER_CAIRO + // Get page size by accounting for rotation + double width, height; + int rotate = _previewed_page->getRotate(); + if ( rotate == 90 || rotate == 270 ) { + height = _previewed_page->getCropWidth(); + width = _previewed_page->getCropHeight(); + } else { + width = _previewed_page->getCropWidth(); + height = _previewed_page->getCropHeight(); + } + // Calculate the needed scaling for the page + double scale_x = (double)_preview_width / width; + double scale_y = (double)_preview_height / height; + double scale_factor = ( scale_x > scale_y ) ? scale_y : scale_x; + // Create new Cairo surface + _thumb_width = (int)ceil( width * scale_factor ); + _thumb_height = (int)ceil( height * scale_factor ); + _thumb_rowstride = _thumb_width * 4; + if (_thumb_data) { + gfree(_thumb_data); + } + _thumb_data = reinterpret_cast(gmalloc(_thumb_rowstride * _thumb_height)); + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + _cairo_surface = cairo_image_surface_create_for_data(_thumb_data, + CAIRO_FORMAT_ARGB32, _thumb_width, _thumb_height, _thumb_rowstride); + cairo_t *cr = cairo_create(_cairo_surface); + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); // Set fill color to white + cairo_paint(cr); // Clear it + cairo_scale(cr, scale_factor, scale_factor); // Use Cairo for resizing the image + // Render page + if (_poppler_doc != NULL) { + PopplerPage *poppler_page = poppler_document_get_page(_poppler_doc, page-1); + poppler_page_render(poppler_page, cr); + g_object_unref(G_OBJECT(poppler_page)); + } + // Clean up + cairo_destroy(cr); + // Redraw preview area + _previewArea->set_size_request(_preview_width, _preview_height); + _previewArea->queue_draw(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_POPPLER_CAIRO +/// helper method +static cairo_status_t + _write_ustring_cb(void *closure, const unsigned char *data, unsigned int length) +{ + Glib::ustring* stream = static_cast(closure); + stream->append(reinterpret_cast(data), length); + + return CAIRO_STATUS_SUCCESS; +} +#endif + +/** + * Parses the selected page of the given PDF document using PdfParser. + */ +SPDocument * +PdfInput::open(::Inkscape::Extension::Input * /*mod*/, const gchar * uri) { + + // Initialize the globalParams variable for poppler + if (!globalParams) { +#ifdef ENABLE_OSX_APP_LOCATIONS + // + // data files for poppler are not relocatable (loaded from + // path defined at build time). This fails to work with relocatable + // application bundles for OS X. + // + // Workaround: + // 1. define $POPPLER_DATADIR env variable in app launcher script + // 2. pass custom $POPPLER_DATADIR via poppler's GlobalParams() + // + // relevant poppler commit: + // + // + // FIXES: Inkscape bug #956282, #1264793 + // TODO: report RFE upstream (full relocation support for OS X packaging) + // + gchar const *poppler_datadir = g_getenv("POPPLER_DATADIR"); + if (poppler_datadir != NULL) { + globalParams = _POPPLER_NEW_GLOBAL_PARAMS(poppler_datadir); + } else { + globalParams = _POPPLER_NEW_GLOBAL_PARAMS(); + } +#else + globalParams = _POPPLER_NEW_GLOBAL_PARAMS(); +#endif // ENABLE_OSX_APP_LOCATIONS + } + + + // Open the file using poppler + // PDFDoc is from poppler. PDFDoc is used for preview and for native import. + std::shared_ptr pdf_doc; + +#ifndef _WIN32 + // poppler does not use glib g_open. So on win32 we must use unicode call. code was copied from + // glib gstdio.c + GooString *filename_goo = new GooString(uri); + pdf_doc = std::make_shared(filename_goo, nullptr, nullptr, nullptr); // TODO: Could ask for password + //delete filename_goo; +#else + wchar_t *wfilename = reinterpret_cast(g_utf8_to_utf16 (uri, -1, NULL, NULL, NULL)); + + if (wfilename == NULL) { + return NULL; + } + + pdf_doc = std::make_shared(wfilename, wcslen(wfilename), nullptr, nullptr, nullptr); // TODO: Could ask for password + g_free (wfilename); +#endif + + if (!pdf_doc->isOk()) { + int error = pdf_doc->getErrorCode(); + if (error == errEncrypted) { + g_message("Document is encrypted."); + } else if (error == errOpenFile) { + g_message("couldn't open the PDF file."); + } else if (error == errBadCatalog) { + g_message("couldn't read the page catalog."); + } else if (error == errDamaged) { + g_message("PDF file was damaged and couldn't be repaired."); + } else if (error == errHighlightFile) { + g_message("nonexistent or invalid highlight file."); + } else if (error == errBadPrinter) { + g_message("invalid printer."); + } else if (error == errPrinting) { + g_message("Error during printing."); + } else if (error == errPermission) { + g_message("PDF file does not allow that operation."); + } else if (error == errBadPageNum) { + g_message("invalid page number."); + } else if (error == errFileIO) { + g_message("file IO error."); + } else { + g_message("Failed to load document from data (error %d)", error); + } + + return nullptr; + } + + + std::unique_ptr dlg; + if (INKSCAPE.use_gui()) { + dlg.reset(new PdfImportDialog(pdf_doc, uri)); + if (!dlg->showDialog()) { + throw Input::open_cancelled(); + } + } + + // Get options + int page_num = 1; + bool is_importvia_poppler = false; + if (dlg) { + page_num = dlg->getSelectedPage(); +#ifdef HAVE_POPPLER_CAIRO + is_importvia_poppler = dlg->getImportMethod(); + // printf("PDF import via %s.\n", is_importvia_poppler ? "poppler" : "native"); +#endif + } else { + page_num = INKSCAPE.get_pdf_page(); +#ifdef HAVE_POPPLER_CAIRO + is_importvia_poppler = INKSCAPE.get_pdf_poppler(); +#endif + } + + // Create Inkscape document from file + SPDocument *doc = nullptr; + bool saved = false; + if(!is_importvia_poppler) + { + // native importer + + // Check page exists + Catalog *catalog = pdf_doc->getCatalog(); + int const num_pages = catalog->getNumPages(); + sanitize_page_number(page_num, num_pages); + Page *page = catalog->getPage(page_num); + if (!page) { + std::cerr << "PDFInput::open: error opening page " << page_num << std::endl; + return nullptr; + } + + // Create document + doc = SPDocument::createNewDoc(nullptr, true, true); + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document + + // Create builder + gchar *docname = g_path_get_basename(uri); + gchar *dot = g_strrstr(docname, "."); + if (dot) { + *dot = 0; + } + SvgBuilder *builder = new SvgBuilder(doc, docname, pdf_doc->getXRef()); + + // Get preferences + Inkscape::XML::Node *prefs = builder->getPreferences(); + if (dlg) + dlg->getImportSettings(prefs); + + // Apply crop settings + _POPPLER_CONST PDFRectangle *clipToBox = nullptr; + double crop_setting = -1.0; + sp_repr_get_double(prefs, "cropTo", &crop_setting); + + if ( crop_setting >= 0.0 ) { // Do page clipping + int crop_choice = (int)crop_setting; + switch (crop_choice) { + case 0: // Media box + clipToBox = page->getMediaBox(); + break; + case 1: // Crop box + clipToBox = page->getCropBox(); + break; + case 2: // Bleed box + clipToBox = page->getBleedBox(); + break; + case 3: // Trim box + clipToBox = page->getTrimBox(); + break; + case 4: // Art box + clipToBox = page->getArtBox(); + break; + default: + break; + } + } + + // Create parser (extension/internal/pdfinput/pdf-parser.h) + PdfParser *pdf_parser = new PdfParser(pdf_doc->getXRef(), builder, page_num-1, page->getRotate(), + page->getResourceDict(), page->getCropBox(), clipToBox); + + // Set up approximation precision for parser. Used for converting Mesh Gradients into tiles. + double color_delta = 2.0; + sp_repr_get_double(prefs, "approximationPrecision", &color_delta); + if ( color_delta <= 0.0 ) { + color_delta = 1.0 / 2.0; + } else { + color_delta = 1.0 / color_delta; + } + for ( int i = 1 ; i <= pdfNumShadingTypes ; i++ ) { + pdf_parser->setApproximationPrecision(i, color_delta, 6); + } + + // Parse the document structure +#if defined(POPPLER_NEW_OBJECT_API) + Object obj = page->getContents(); +#else + Object obj; + page->getContents(&obj); +#endif + if (!obj.isNull()) { + pdf_parser->parse(&obj); + } + + // Cleanup +#if !defined(POPPLER_NEW_OBJECT_API) + obj.free(); +#endif + delete pdf_parser; + delete builder; + g_free(docname); + } + else + { +#ifdef HAVE_POPPLER_CAIRO + // the poppler import + + Glib::ustring full_path = uri; + if (!Glib::path_is_absolute(uri)) { + full_path = Glib::build_filename(Glib::get_current_dir(),uri); + } + Glib::ustring full_uri = Glib::filename_to_uri(full_path); + + GError *error = NULL; + /// @todo handle password + /// @todo check if win32 unicode needs special attention + PopplerDocument* document = poppler_document_new_from_file(full_uri.c_str(), NULL, &error); + PopplerPage* page = nullptr; + + if(error != NULL) { + std::cerr << "PDFInput::open: error opening document: " << full_uri << std::endl; + g_error_free (error); + } + + if (document) { + int const num_pages = poppler_document_get_n_pages(document); + sanitize_page_number(page_num, num_pages); + page = poppler_document_get_page(document, page_num - 1); + } + + if (page) { + double width, height; + poppler_page_get_size(page, &width, &height); + + Glib::ustring output; + cairo_surface_t* surface = cairo_svg_surface_create_for_stream(Inkscape::Extension::Internal::_write_ustring_cb, + &output, width, height); + + // This magical function results in more fine-grain fallbacks. In particular, a mesh + // gradient won't necessarily result in the whole PDF being rasterized. Of course, SVG + // 1.2 never made it as a standard, but hey, we'll take what we can get. This trick was + // found by examining the 'pdftocairo' code. + cairo_svg_surface_restrict_to_version( surface, CAIRO_SVG_VERSION_1_2 ); + + cairo_t* cr = cairo_create(surface); + + poppler_page_render_for_printing(page, cr); + cairo_show_page(cr); + + cairo_destroy(cr); + cairo_surface_destroy(surface); + + doc = SPDocument::createNewDocFromMem(output.c_str(), output.length(), TRUE); + } else if (document) { + std::cerr << "PDFInput::open: error opening page " << page_num << " of document: " << full_uri << std::endl; + } + + // Cleanup + if (document) { + g_object_unref(G_OBJECT(document)); + if (page) { + g_object_unref(G_OBJECT(page)); + } + } + + if (!doc) { + return nullptr; + } + + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document +#endif + } + + // Set viewBox if it doesn't exist + if (!doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + // Restore undo + DocumentUndo::setUndoSensitive(doc, saved); + + return doc; +} + +#include "../clear-n_.h" + +void PdfInput::init() { + /* PDF in */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("PDF Input") "\n" + "org.inkscape.input.pdf\n" + "\n" + ".pdf\n" + "application/pdf\n" + "" N_("Portable Document Format (*.pdf)") "\n" + "" N_("Portable Document Format") "\n" + "\n" + "", new PdfInput()); + + /* AI in */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("AI Input") "\n" + "org.inkscape.input.ai\n" + "\n" + ".ai\n" + "image/x-adobe-illustrator\n" + "" N_("Adobe Illustrator 9.0 and above (*.ai)") "\n" + "" N_("Open files saved in Adobe Illustrator 9.0 and newer versions") "\n" + "\n" + "", new PdfInput()); +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-input.h b/src/extension/internal/pdfinput/pdf-input.h new file mode 100644 index 0000000..66f750d --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +#include + +#include "../../implementation/implementation.h" + +#ifdef HAVE_POPPLER_CAIRO +struct _PopplerDocument; +typedef struct _PopplerDocument PopplerDocument; +#endif + +struct _GdkEventExpose; +typedef _GdkEventExpose GdkEventExpose; + +class Page; +class PDFDoc; + +namespace Gtk { + class Button; + class CheckButton; + class ComboBoxText; + class DrawingArea; + class Frame; + class HBox; + class Scale; + class RadioButton; + class VBox; + class Label; +} + +namespace Inkscape { + +namespace UI { +namespace Widget { + class SpinButton; + class Frame; +} +} + +namespace Extension { +namespace Internal { + +/** + * PDF import using libpoppler. + */ +class PdfImportDialog : public Gtk::Dialog +{ +public: + PdfImportDialog(std::shared_ptr doc, const gchar *uri); + ~PdfImportDialog() override; + + bool showDialog(); + int getSelectedPage(); + bool getImportMethod(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(int page); + + // Signal handlers + bool _onDraw(const Cairo::RefPtr& cr); + void _onPageNumberChanged(); + void _onToggleCropping(); + void _onPrecisionChanged(); +#ifdef HAVE_POPPLER_CAIRO + void _onToggleImport(); +#endif + + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + class Gtk::Label * _labelSelect; + class Inkscape::UI::Widget::SpinButton * _pageNumberSpin; + class Gtk::Label * _labelTotalPages; + class Gtk::HBox * hbox2; + class Gtk::CheckButton * _cropCheck; + class Gtk::ComboBoxText * _cropTypeCombo; + class Gtk::HBox * hbox3; + class Gtk::VBox * vbox2; + class Inkscape::UI::Widget::Frame * _pageSettingsFrame; + class Gtk::Label * _labelPrecision; + class Gtk::Label * _labelPrecisionWarning; +#ifdef HAVE_POPPLER_CAIRO + class Gtk::RadioButton * _importViaPoppler; // Use poppler_cairo importing + class Gtk::Label * _labelViaPoppler; + class Gtk::RadioButton * _importViaInternal; // Use native (poppler based) importing + class Gtk::Label * _labelViaInternal; +#endif + Gtk::Scale * _fallbackPrecisionSlider; + Glib::RefPtr _fallbackPrecisionSlider_adj; + class Gtk::Label * _labelPrecisionComment; + class Gtk::HBox * hbox6; +#if 0 + class Gtk::Label * _labelText; + class Gtk::ComboBoxText * _textHandlingCombo; + class Gtk::HBox * hbox5; +#endif + class Gtk::CheckButton * _localFontsCheck; + class Gtk::CheckButton * _embedImagesCheck; + class Gtk::VBox * vbox3; + class Inkscape::UI::Widget::Frame * _importSettingsFrame; + class Gtk::VBox * vbox1; + class Gtk::DrawingArea * _previewArea; + class Gtk::HBox * hbox1; + + std::shared_ptr _pdf_doc; // Document to be imported + int _current_page; // Current selected page + Page *_previewed_page; // Currently previewed page + unsigned char *_thumb_data; // Thumbnail image data + int _thumb_width, _thumb_height; // Thumbnail size + int _thumb_rowstride; + int _preview_width, _preview_height; // Size of the preview area + bool _render_thumb; // Whether we can/shall render thumbnails +#ifdef HAVE_POPPLER_CAIRO + cairo_surface_t *_cairo_surface; + PopplerDocument *_poppler_doc; +#endif +}; + + +class PdfInput: public Inkscape::Extension::Implementation::Implementation { + PdfInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); +}; + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-parser.cpp b/src/extension/internal/pdfinput/pdf-parser.cpp new file mode 100644 index 0000000..73867e9 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.cpp @@ -0,0 +1,3398 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * Derived from poppler's Gfx.cc, which was derived from Xpdf by 1996-2003 Glyph & Cog, LLC + * Jon A. Cruz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER + +#ifdef USE_GCC_PRAGMAS +#pragma implementation +#endif + +#include +#include +#include +#include +#include + +#include "svg-builder.h" +#include "Gfx.h" +#include "pdf-parser.h" +#include "util/units.h" + +#include "glib/poppler-features.h" +#include "goo/gmem.h" +#include "goo/GooString.h" +#include "GlobalParams.h" +#include "CharTypes.h" +#include "Object.h" +#include "Array.h" +#include "Dict.h" +#include "Stream.h" +#include "Lexer.h" +#include "Parser.h" +#include "GfxFont.h" +#include "GfxState.h" +#include "OutputDev.h" +#include "Page.h" +#include "Annot.h" +#include "Error.h" + +// the MSVC math.h doesn't define this +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +// Default max delta allowed in any color component for a shading fill. +#define defaultShadingColorDelta (dblToCol( 1 / 2.0 )) + +// Default max recursive depth for a shading fill. +#define defaultShadingMaxDepth 6 + +// Max number of operators kept in the history list. +#define maxOperatorHistoryDepth 16 + +//------------------------------------------------------------------------ +// Operator table +//------------------------------------------------------------------------ + +PdfOperator PdfParser::opTab[] = { + {"\"", 3, {tchkNum, tchkNum, tchkString}, + &PdfParser::opMoveSetShowText}, + {"'", 1, {tchkString}, + &PdfParser::opMoveShowText}, + {"B", 0, {tchkNone}, + &PdfParser::opFillStroke}, + {"B*", 0, {tchkNone}, + &PdfParser::opEOFillStroke}, + {"BDC", 2, {tchkName, tchkProps}, + &PdfParser::opBeginMarkedContent}, + {"BI", 0, {tchkNone}, + &PdfParser::opBeginImage}, + {"BMC", 1, {tchkName}, + &PdfParser::opBeginMarkedContent}, + {"BT", 0, {tchkNone}, + &PdfParser::opBeginText}, + {"BX", 0, {tchkNone}, + &PdfParser::opBeginIgnoreUndef}, + {"CS", 1, {tchkName}, + &PdfParser::opSetStrokeColorSpace}, + {"DP", 2, {tchkName, tchkProps}, + &PdfParser::opMarkPoint}, + {"Do", 1, {tchkName}, + &PdfParser::opXObject}, + {"EI", 0, {tchkNone}, + &PdfParser::opEndImage}, + {"EMC", 0, {tchkNone}, + &PdfParser::opEndMarkedContent}, + {"ET", 0, {tchkNone}, + &PdfParser::opEndText}, + {"EX", 0, {tchkNone}, + &PdfParser::opEndIgnoreUndef}, + {"F", 0, {tchkNone}, + &PdfParser::opFill}, + {"G", 1, {tchkNum}, + &PdfParser::opSetStrokeGray}, + {"ID", 0, {tchkNone}, + &PdfParser::opImageData}, + {"J", 1, {tchkInt}, + &PdfParser::opSetLineCap}, + {"K", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeCMYKColor}, + {"M", 1, {tchkNum}, + &PdfParser::opSetMiterLimit}, + {"MP", 1, {tchkName}, + &PdfParser::opMarkPoint}, + {"Q", 0, {tchkNone}, + &PdfParser::opRestore}, + {"RG", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeRGBColor}, + {"S", 0, {tchkNone}, + &PdfParser::opStroke}, + {"SC", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeColor}, + {"SCN", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN}, + &PdfParser::opSetStrokeColorN}, + {"T*", 0, {tchkNone}, + &PdfParser::opTextNextLine}, + {"TD", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMoveSet}, + {"TJ", 1, {tchkArray}, + &PdfParser::opShowSpaceText}, + {"TL", 1, {tchkNum}, + &PdfParser::opSetTextLeading}, + {"Tc", 1, {tchkNum}, + &PdfParser::opSetCharSpacing}, + {"Td", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMove}, + {"Tf", 2, {tchkName, tchkNum}, + &PdfParser::opSetFont}, + {"Tj", 1, {tchkString}, + &PdfParser::opShowText}, + {"Tm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetTextMatrix}, + {"Tr", 1, {tchkInt}, + &PdfParser::opSetTextRender}, + {"Ts", 1, {tchkNum}, + &PdfParser::opSetTextRise}, + {"Tw", 1, {tchkNum}, + &PdfParser::opSetWordSpacing}, + {"Tz", 1, {tchkNum}, + &PdfParser::opSetHorizScaling}, + {"W", 0, {tchkNone}, + &PdfParser::opClip}, + {"W*", 0, {tchkNone}, + &PdfParser::opEOClip}, + {"b", 0, {tchkNone}, + &PdfParser::opCloseFillStroke}, + {"b*", 0, {tchkNone}, + &PdfParser::opCloseEOFillStroke}, + {"c", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opCurveTo}, + {"cm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opConcat}, + {"cs", 1, {tchkName}, + &PdfParser::opSetFillColorSpace}, + {"d", 2, {tchkArray, tchkNum}, + &PdfParser::opSetDash}, + {"d0", 2, {tchkNum, tchkNum}, + &PdfParser::opSetCharWidth}, + {"d1", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetCacheDevice}, + {"f", 0, {tchkNone}, + &PdfParser::opFill}, + {"f*", 0, {tchkNone}, + &PdfParser::opEOFill}, + {"g", 1, {tchkNum}, + &PdfParser::opSetFillGray}, + {"gs", 1, {tchkName}, + &PdfParser::opSetExtGState}, + {"h", 0, {tchkNone}, + &PdfParser::opClosePath}, + {"i", 1, {tchkNum}, + &PdfParser::opSetFlat}, + {"j", 1, {tchkInt}, + &PdfParser::opSetLineJoin}, + {"k", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillCMYKColor}, + {"l", 2, {tchkNum, tchkNum}, + &PdfParser::opLineTo}, + {"m", 2, {tchkNum, tchkNum}, + &PdfParser::opMoveTo}, + {"n", 0, {tchkNone}, + &PdfParser::opEndPath}, + {"q", 0, {tchkNone}, + &PdfParser::opSave}, + {"re", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opRectangle}, + {"rg", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillRGBColor}, + {"ri", 1, {tchkName}, + &PdfParser::opSetRenderingIntent}, + {"s", 0, {tchkNone}, + &PdfParser::opCloseStroke}, + {"sc", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillColor}, + {"scn", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN}, + &PdfParser::opSetFillColorN}, + {"sh", 1, {tchkName}, + &PdfParser::opShFill}, + {"v", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo1}, + {"w", 1, {tchkNum}, + &PdfParser::opSetLineWidth}, + {"y", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo2} +}; + +#define numOps (sizeof(opTab) / sizeof(PdfOperator)) + +namespace { + +GfxPatch blankPatch() +{ + GfxPatch patch; + memset(&patch, 0, sizeof(patch)); // quick-n-dirty + return patch; +} + +} // namespace + +//------------------------------------------------------------------------ +// ClipHistoryEntry +//------------------------------------------------------------------------ + +class ClipHistoryEntry { +public: + + ClipHistoryEntry(GfxPath *clipPath = nullptr, GfxClipType clipType = clipNormal); + virtual ~ClipHistoryEntry(); + + // Manipulate clip path stack + ClipHistoryEntry *save(); + ClipHistoryEntry *restore(); + GBool hasSaves() { return saved != nullptr; } + void setClip(_POPPLER_CONST_83 GfxPath *newClipPath, GfxClipType newClipType = clipNormal); + GfxPath *getClipPath() { return clipPath; } + GfxClipType getClipType() { return clipType; } + +private: + + ClipHistoryEntry *saved; // next clip path on stack + + GfxPath *clipPath; // used as the path to be filled for an 'sh' operator + GfxClipType clipType; + + ClipHistoryEntry(ClipHistoryEntry *other); +}; + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +PdfParser::PdfParser(XRef *xrefA, + Inkscape::Extension::Internal::SvgBuilder *builderA, + int /*pageNum*/, + int rotate, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box, + _POPPLER_CONST PDFRectangle *cropBox) : + xref(xrefA), + builder(builderA), + subPage(gFalse), + printCommands(false), + res(new GfxResources(xref, resDict, nullptr)), // start the resource stack + state(new GfxState(72.0, 72.0, box, rotate, gTrue)), + fontChanged(gFalse), + clip(clipNone), + ignoreUndef(0), + baseMatrix(), + formDepth(0), + parser(nullptr), + colorDeltas(), + maxDepths(), + clipHistory(new ClipHistoryEntry()), + operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + builder->setDocumentSize(Inkscape::Util::Quantity::convert(state->getPageWidth(), "pt", "px"), + Inkscape::Util::Quantity::convert(state->getPageHeight(), "pt", "px")); + + const double *ctm = state->getCTM(); + double scaledCTM[6]; + for (int i = 0; i < 6; ++i) { + baseMatrix[i] = ctm[i]; + scaledCTM[i] = Inkscape::Util::Quantity::convert(ctm[i], "pt", "px"); + } + saveState(); + builder->setTransform((double*)&scaledCTM); + formDepth = 0; + + // set crop box + if (cropBox) { + if (printCommands) + printf("cropBox: %f %f %f %f\n", cropBox->x1, cropBox->y1, cropBox->x2, cropBox->y2); + // do not clip if it's not needed + if (cropBox->x1 != 0.0 || cropBox->y1 != 0.0 || + cropBox->x2 != state->getPageWidth() || cropBox->y2 != state->getPageHeight()) { + + state->moveTo(cropBox->x1, cropBox->y1); + state->lineTo(cropBox->x2, cropBox->y1); + state->lineTo(cropBox->x2, cropBox->y2); + state->lineTo(cropBox->x1, cropBox->y2); + state->closePath(); + state->clip(); + clipHistory->setClip(state->getPath(), clipNormal); + builder->setClipPath(state); + state->clearPath(); + } + } + pushOperator("startPage"); +} + +PdfParser::PdfParser(XRef *xrefA, + Inkscape::Extension::Internal::SvgBuilder *builderA, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box) : + xref(xrefA), + builder(builderA), + subPage(gTrue), + printCommands(false), + res(new GfxResources(xref, resDict, nullptr)), // start the resource stack + state(new GfxState(72, 72, box, 0, gFalse)), + fontChanged(gFalse), + clip(clipNone), + ignoreUndef(0), + baseMatrix(), + formDepth(0), + parser(nullptr), + colorDeltas(), + maxDepths(), + clipHistory(new ClipHistoryEntry()), + operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + + for (int i = 0; i < 6; ++i) { + baseMatrix[i] = state->getCTM()[i]; + } + formDepth = 0; +} + +PdfParser::~PdfParser() { + while(operatorHistory) { + OpHistoryEntry *tmp = operatorHistory->next; + delete operatorHistory; + operatorHistory = tmp; + } + + while (state && state->hasSaves()) { + restoreState(); + } + + if (!subPage) { + //out->endPage(); + } + + while (res) { + popResources(); + } + + if (state) { + delete state; + state = nullptr; + } + + if (clipHistory) { + delete clipHistory; + clipHistory = nullptr; + } +} + +void PdfParser::parse(Object *obj, GBool topLevel) { + Object obj2; + + if (obj->isArray()) { + for (int i = 0; i < obj->arrayGetLength(); ++i) { + _POPPLER_CALL_ARGS(obj2, obj->arrayGet, i); + if (!obj2.isStream()) { + error(errInternal, -1, "Weird page contents"); + _POPPLER_FREE(obj2); + return; + } + _POPPLER_FREE(obj2); + } + } else if (!obj->isStream()) { + error(errInternal, -1, "Weird page contents"); + return; + } + parser = new _POPPLER_NEW_PARSER(xref, obj); + go(topLevel); + delete parser; + parser = nullptr; +} + +void PdfParser::go(GBool /*topLevel*/) +{ + Object obj; + Object args[maxArgs]; + + // scan a sequence of objects + int numArgs = 0; + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isEOF()) { + + // got a command - execute it + if (obj.isCmd()) { + if (printCommands) { + obj.print(stdout); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } + + // Run the operation + execOp(&obj, args, numArgs); + +#if !defined(POPPLER_NEW_OBJECT_API) + _POPPLER_FREE(obj); + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + numArgs = 0; + + // got an argument - save it + } else if (numArgs < maxArgs) { + args[numArgs++] = std::move(obj); + + // too many arguments - something is wrong + } else { + error(errSyntaxError, getPos(), "Too many args in content stream"); + if (printCommands) { + printf("throwing away arg: "); + obj.print(stdout); + printf("\n"); + fflush(stdout); + } + _POPPLER_FREE(obj); + } + + // grab the next object + _POPPLER_CALL(obj, parser->getObj); + } + _POPPLER_FREE(obj); + + // args at end with no command + if (numArgs > 0) { + error(errSyntaxError, getPos(), "Leftover args in content stream"); + if (printCommands) { + printf("%d leftovers:", numArgs); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } +#if !defined(POPPLER_NEW_OBJECT_API) + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + } +} + +void PdfParser::pushOperator(const char *name) +{ + OpHistoryEntry *newEntry = new OpHistoryEntry; + newEntry->name = name; + newEntry->state = nullptr; + newEntry->depth = (operatorHistory != nullptr ? (operatorHistory->depth+1) : 0); + newEntry->next = operatorHistory; + operatorHistory = newEntry; + + // Truncate list if needed + if (operatorHistory->depth > maxOperatorHistoryDepth) { + OpHistoryEntry *curr = operatorHistory; + OpHistoryEntry *prev = nullptr; + while (curr && curr->next != nullptr) { + curr->depth--; + prev = curr; + curr = curr->next; + } + if (prev) { + if (curr->state != nullptr) + delete curr->state; + delete curr; + prev->next = nullptr; + } + } +} + +const char *PdfParser::getPreviousOperator(unsigned int look_back) { + OpHistoryEntry *prev = nullptr; + if (operatorHistory != nullptr && look_back > 0) { + prev = operatorHistory->next; + while (--look_back > 0 && prev != nullptr) { + prev = prev->next; + } + } + if (prev != nullptr) { + return prev->name; + } else { + return ""; + } +} + +void PdfParser::execOp(Object *cmd, Object args[], int numArgs) { + PdfOperator *op; + const char *name; + Object *argPtr; + int i; + + // find operator + name = cmd->getCmd(); + if (!(op = findOp(name))) { + if (ignoreUndef == 0) + error(errSyntaxError, getPos(), "Unknown operator '{0:s}'", name); + return; + } + + // type check args + argPtr = args; + if (op->numArgs >= 0) { + if (numArgs < op->numArgs) { + error(errSyntaxError, getPos(), "Too few ({0:d}) args to '{1:d}' operator", numArgs, name); + return; + } + if (numArgs > op->numArgs) { +#if 0 + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", numArgs, name); +#endif + argPtr += numArgs - op->numArgs; + numArgs = op->numArgs; + } + } else { + if (numArgs > -op->numArgs) { + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", + numArgs, name); + return; + } + } + for (i = 0; i < numArgs; ++i) { + if (!checkArg(&argPtr[i], op->tchk[i])) { + error(errSyntaxError, getPos(), "Arg #{0:d} to '{1:s}' operator is wrong type ({2:s})", + i, name, argPtr[i].getTypeName()); + return; + } + } + + // add to history + pushOperator((char*)&op->name); + + // do it + (this->*op->func)(argPtr, numArgs); +} + +PdfOperator* PdfParser::findOp(const char *name) { + int a = -1; + int b = numOps; + int cmp = -1; + // invariant: opTab[a] < name < opTab[b] + while (b - a > 1) { + const int m = (a + b) / 2; + cmp = strcmp(opTab[m].name, name); + if (cmp < 0) + a = m; + else if (cmp > 0) + b = m; + else + a = b = m; + } + if (cmp != 0) + return nullptr; + return &opTab[a]; +} + +GBool PdfParser::checkArg(Object *arg, TchkType type) { + switch (type) { + case tchkBool: return arg->isBool(); + case tchkInt: return arg->isInt(); + case tchkNum: return arg->isNum(); + case tchkString: return arg->isString(); + case tchkName: return arg->isName(); + case tchkArray: return arg->isArray(); + case tchkProps: return arg->isDict() || arg->isName(); + case tchkSCN: return arg->isNum() || arg->isName(); + case tchkNone: return gFalse; + } + return gFalse; +} + +int PdfParser::getPos() { + return parser ? parser->getPos() : -1; +} + +//------------------------------------------------------------------------ +// graphics state operators +//------------------------------------------------------------------------ + +void PdfParser::opSave(Object /*args*/[], int /*numArgs*/) +{ + saveState(); +} + +void PdfParser::opRestore(Object /*args*/[], int /*numArgs*/) +{ + restoreState(); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opConcat(Object args[], int /*numArgs*/) +{ + state->concatCTM(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + const char *prevOp = getPreviousOperator(); + double a0 = args[0].getNum(); + double a1 = args[1].getNum(); + double a2 = args[2].getNum(); + double a3 = args[3].getNum(); + double a4 = args[4].getNum(); + double a5 = args[5].getNum(); + if (!strcmp(prevOp, "q")) { + builder->setTransform(a0, a1, a2, a3, a4, a5); + } else if (!strcmp(prevOp, "cm") || !strcmp(prevOp, "startPage")) { + // multiply it with the previous transform + double otherMatrix[6]; + if (!builder->getTransform(otherMatrix)) { // invalid transform + // construct identity matrix + otherMatrix[0] = otherMatrix[3] = 1.0; + otherMatrix[1] = otherMatrix[2] = otherMatrix[4] = otherMatrix[5] = 0.0; + } + double c0 = a0*otherMatrix[0] + a1*otherMatrix[2]; + double c1 = a0*otherMatrix[1] + a1*otherMatrix[3]; + double c2 = a2*otherMatrix[0] + a3*otherMatrix[2]; + double c3 = a2*otherMatrix[1] + a3*otherMatrix[3]; + double c4 = a4*otherMatrix[0] + a5*otherMatrix[2] + otherMatrix[4]; + double c5 = a4*otherMatrix[1] + a5*otherMatrix[3] + otherMatrix[5]; + builder->setTransform(c0, c1, c2, c3, c4, c5); + } else { + builder->pushGroup(); + builder->setTransform(a0, a1, a2, a3, a4, a5); + } + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetDash(Object args[], int /*numArgs*/) +{ + double *dash = nullptr; + + Array *a = args[0].getArray(); + int length = a->getLength(); + if (length != 0) { + dash = (double *)gmallocn(length, sizeof(double)); + for (int i = 0; i < length; ++i) { + Object obj; + dash[i] = _POPPLER_CALL_ARGS_DEREF(obj, a->get, i).getNum(); + _POPPLER_FREE(obj); + } + } + state->setLineDash(dash, length, args[1].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFlat(Object args[], int /*numArgs*/) +{ + state->setFlatness((int)args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineJoin(Object args[], int /*numArgs*/) +{ + state->setLineJoin(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineCap(Object args[], int /*numArgs*/) +{ + state->setLineCap(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetMiterLimit(Object args[], int /*numArgs*/) +{ + state->setMiterLimit(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineWidth(Object args[], int /*numArgs*/) +{ + state->setLineWidth(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetExtGState(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, obj4, obj5; + Function *funcs[4] = {nullptr, nullptr, nullptr, nullptr}; + GfxColor backdropColor; + GBool haveBackdropColor = gFalse; + GBool alpha = gFalse; + + _POPPLER_CALL_ARGS(obj1, res->lookupGState, args[0].getName()); + if (obj1.isNull()) { + return; + } + if (!obj1.isDict()) { + error(errSyntaxError, getPos(), "ExtGState '{0:s}' is wrong type"), args[0].getName(); + _POPPLER_FREE(obj1); + return; + } + if (printCommands) { + printf(" gfx state dict: "); + obj1.print(); + printf("\n"); + } + + // transparency support: blend mode, fill/stroke opacity + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "BM").isNull()) { + GfxBlendMode mode = gfxBlendNormal; + if (state->parseBlendMode(&obj2, &mode)) { + state->setBlendMode(mode); + } else { + error(errSyntaxError, getPos(), "Invalid blend mode in ExtGState"); + } + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "ca").isNum()) { + state->setFillOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "CA").isNum()) { + state->setStrokeOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + + // fill/stroke overprint + GBool haveFillOP = gFalse; + if ((haveFillOP = _POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "op").isBool())) { + state->setFillOverprint(obj2.getBool()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "OP").isBool()) { + state->setStrokeOverprint(obj2.getBool()); + if (!haveFillOP) { + state->setFillOverprint(obj2.getBool()); + } + } + _POPPLER_FREE(obj2); + + // stroke adjust + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SA").isBool()) { + state->setStrokeAdjust(obj2.getBool()); + } + _POPPLER_FREE(obj2); + + // transfer function + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "TR2").isNull()) { + _POPPLER_FREE(obj2); + _POPPLER_CALL_ARGS(obj2, obj1.dictLookup, "TR"); + } + if (obj2.isName(const_cast("Default")) || + obj2.isName(const_cast("Identity"))) { + funcs[0] = funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } else if (obj2.isArray() && obj2.arrayGetLength() == 4) { + int pos = 4; + for (int i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj3, obj2.arrayGet, i); + funcs[i] = Function::parse(&obj3); + _POPPLER_FREE(obj3); + if (!funcs[i]) { + pos = i; + break; + } + } + if (pos == 4) { + state->setTransfer(funcs); + } + } else if (obj2.isName() || obj2.isDict() || obj2.isStream()) { + if ((funcs[0] = Function::parse(&obj2))) { + funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid transfer function in ExtGState"); + } + _POPPLER_FREE(obj2); + + // soft mask + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SMask").isNull()) { + if (obj2.isName(const_cast("None"))) { + builder->clearSoftMask(state); + } else if (obj2.isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "S").isName("Alpha")) { + alpha = gTrue; + } else { // "Luminosity" + alpha = gFalse; + } + _POPPLER_FREE(obj3); + funcs[0] = nullptr; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "TR").isNull()) { + funcs[0] = Function::parse(&obj3); + if (funcs[0]->getInputSize() != 1 || + funcs[0]->getOutputSize() != 1) { + error(errSyntaxError, getPos(), "Invalid transfer function in soft mask in ExtGState"); + delete funcs[0]; + funcs[0] = nullptr; + } + } + _POPPLER_FREE(obj3); + if ((haveBackdropColor = _POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "BC").isArray())) { + for (int & i : backdropColor.c) { + i = 0; + } + for (int i = 0; i < obj3.arrayGetLength() && i < gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj4, obj3.arrayGet, i); + if (obj4.isNum()) { + backdropColor.c[i] = dblToCol(obj4.getNum()); + } + _POPPLER_FREE(obj4); + } + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "G").isStream()) { + if (_POPPLER_CALL_ARGS_DEREF(obj4, obj3.streamGetDict()->lookup, "Group").isDict()) { + GfxColorSpace *blendingColorSpace = nullptr; + GBool isolated = gFalse; + GBool knockout = gFalse; + if (!_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "CS").isNull()) { +#if defined(POPPLER_EVEN_NEWER_NEW_COLOR_SPACE_API) + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj5, nullptr, state); +#elif defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + blendingColorSpace = GfxColorSpace::parse(&obj5, NULL, NULL); +#else + blendingColorSpace = GfxColorSpace::parse(&obj5, NULL); +#endif + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "I").isBool()) { + isolated = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "K").isBool()) { + knockout = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (!haveBackdropColor) { + if (blendingColorSpace) { + blendingColorSpace->getDefaultColor(&backdropColor); + } else { + //~ need to get the parent or default color space (?) + for (int & i : backdropColor.c) { + i = 0; + } + } + } + doSoftMask(&obj3, alpha, blendingColorSpace, + isolated, knockout, funcs[0], &backdropColor); + if (funcs[0]) { + delete funcs[0]; + } + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj4); + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj3); + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState"); + } + } + _POPPLER_FREE(obj2); + + _POPPLER_FREE(obj1); +} + +void PdfParser::doSoftMask(Object *str, GBool alpha, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor) { + Dict *dict, *resDict; + double m[6], bbox[4]; + Object obj1, obj2; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BBox"); + if (!obj1.isArray()) { + _POPPLER_FREE(obj1); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + bbox[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // get matrix + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Matrix"); + if (obj1.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + m[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(obj1); + + // get resources + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Resources"); + resDict = obj1.isDict() ? obj1.getDict() : (Dict *)nullptr; + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, gTrue, gTrue, + blendingColorSpace, isolated, knockout, + alpha, transferFunc, backdropColor); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(obj1); +} + +void PdfParser::opSetRenderingIntent(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// color operators +//------------------------------------------------------------------------ + +/** + * Get a newly allocated color space instance by CS operation argument. + * + * Maintains a cache for named color spaces to avoid expensive re-parsing. + */ +GfxColorSpace *PdfParser::lookupColorSpaceCopy(Object &arg) +{ + assert(!arg.isNull()); + + char const *name = arg.isName() ? arg.getName() : nullptr; + GfxColorSpace *colorSpace = nullptr; + + if (name && (colorSpace = colorSpacesCache[name].get())) { + return colorSpace->copy(); + } + + Object *argPtr = &arg; + Object obj; + + if (name) { + _POPPLER_CALL_ARGS(obj, res->lookupColorSpace, name); + if (!obj.isNull()) { + argPtr = &obj; + } + } + +#if defined(POPPLER_EVEN_NEWER_NEW_COLOR_SPACE_API) + colorSpace = GfxColorSpace::parse(res, argPtr, nullptr, state); +#elif defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + colorSpace = GfxColorSpace::parse(argPtr, nullptr, nullptr); +#else + colorSpace = GfxColorSpace::parse(argPtr, nullptr); +#endif + + _POPPLER_FREE(obj); + + if (name && colorSpace) { + colorSpacesCache[name].reset(colorSpace->copy()); + } + + return colorSpace; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + int i; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceCMYKColorSpace()); + for (i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceCMYKColorSpace()); + for (int i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillRGBColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeRGBColor(Object args[], int /*numArgs*/) { + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setFillPattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setFillColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setFillColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (fill)"); + } +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setStrokePattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setStrokeColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setStrokeColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (stroke)"); + } +} + +void PdfParser::opSetFillColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'sc' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetStrokeColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SC' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetFillColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getFillColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getFillColorSpace())->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getFillColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } + GfxPattern *pattern; +#if defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), nullptr, state))) { + state->setFillPattern(pattern); + builder->updateStyle(state); + } +#else + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), NULL))) { + state->setFillPattern(pattern); + builder->updateStyle(state); + } +#endif + + } else { + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } +} + +void PdfParser::opSetStrokeColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getStrokeColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } + GfxPattern *pattern; +#if defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), nullptr, state))) { + state->setStrokePattern(pattern); + builder->updateStyle(state); + } +#else + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), NULL))) { + state->setStrokePattern(pattern); + builder->updateStyle(state); + } +#endif + + } else { + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } +} + +//------------------------------------------------------------------------ +// path segment operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveTo(Object args[], int /*numArgs*/) +{ + state->moveTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opLineTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in lineto"); + return; + } + state->lineTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = args[4].getNum(); + double y3 = args[5].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo1(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto1"); + return; + } + double x1 = state->getCurX(); + double y1 = state->getCurY(); + double x2 = args[0].getNum(); + double y2 = args[1].getNum(); + double x3 = args[2].getNum(); + double y3 = args[3].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo2(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto2"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = x2; + double y3 = y2; + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opRectangle(Object args[], int /*numArgs*/) +{ + double x = args[0].getNum(); + double y = args[1].getNum(); + double w = args[2].getNum(); + double h = args[3].getNum(); + state->moveTo(x, y); + state->lineTo(x + w, y); + state->lineTo(x + w, y + h); + state->lineTo(x, y + h); + state->closePath(); +} + +void PdfParser::opClosePath(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in closepath"); + return; + } + state->closePath(); +} + +//------------------------------------------------------------------------ +// path painting operators +//------------------------------------------------------------------------ + +void PdfParser::opEndPath(Object /*args*/[], int /*numArgs*/) +{ + doEndPath(); +} + +void PdfParser::opStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in stroke")); + return; + } + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opCloseStroke(Object * /*args[]*/, int /*numArgs*/) { + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in closepath/stroke")); + return; + } + state->closePath(); + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in fill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gFalse); + } else { + builder->addPath(state, true, false); + } + } + doEndPath(); +} + +void PdfParser::opEOFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in eofill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gTrue); + } else { + builder->addPath(state, true, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in fill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gFalse); + } else { + builder->addPath(state, true, true); + } + doEndPath(); +} + +void PdfParser::opCloseFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in closepath/fill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gFalse); + } + doEndPath(); +} + +void PdfParser::opEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in eofill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::opCloseEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast("No path in closepath/eofill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::doFillAndStroke(GBool eoFill) { + GBool fillOk = gTrue, strokeOk = gTrue; + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + fillOk = gFalse; + } + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + strokeOk = gFalse; + } + if (fillOk && strokeOk) { + builder->addPath(state, true, true, eoFill); + } else { + doPatternFillFallback(eoFill); + doPatternStrokeFallback(); + } +} + +void PdfParser::doPatternFillFallback(GBool eoFill) { + GfxPattern *pattern; + + if (!(pattern = state->getFillPattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast(pattern), gFalse, eoFill); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type (%d) in fill", + pattern->getType()); + break; + } +} + +void PdfParser::doPatternStrokeFallback() { + GfxPattern *pattern; + + if (!(pattern = state->getStrokePattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast(pattern), gTrue, gFalse); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type ({0:d}) in stroke", + pattern->getType()); + break; + } +} + +void PdfParser::doShadingPatternFillFallback(GfxShadingPattern *sPat, + GBool stroke, GBool eoFill) { + GfxShading *shading; + GfxPath *savedPath; + const double *ctm, *btm, *ptm; + double m[6], ictm[6], m1[6]; + double xMin, yMin, xMax, yMax; + double det; + + shading = sPat->getShading(); + + // save current graphics state + savedPath = state->getPath()->copy(); + saveState(); + + // clip to bbox + if (false ){//shading->getHasBBox()) { + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + state->closePath(); + state->clip(); + //builder->clip(state); + state->setPath(savedPath->copy()); + } + + // clip to current path + if (stroke) { + state->clipToStrokePath(); + //out->clipToStrokePath(state); + } else { + state->clip(); + if (eoFill) { + builder->setClipPath(state, true); + } else { + builder->setClipPath(state); + } + } + + // set the color space + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // background color fill + if (shading->getHasBackground()) { + state->setFillColor(shading->getBackground()); + builder->addPath(state, true, false); + } + state->clearPath(); + + // construct a (pattern space) -> (current space) transform matrix + ctm = state->getCTM(); + btm = baseMatrix; + ptm = sPat->getMatrix(); + // iCTM = invert CTM + det = 1 / (ctm[0] * ctm[3] - ctm[1] * ctm[2]); + ictm[0] = ctm[3] * det; + ictm[1] = -ctm[1] * det; + ictm[2] = -ctm[2] * det; + ictm[3] = ctm[0] * det; + ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) * det; + ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) * det; + // m1 = PTM * BTM = PTM * base transform matrix + m1[0] = ptm[0] * btm[0] + ptm[1] * btm[2]; + m1[1] = ptm[0] * btm[1] + ptm[1] * btm[3]; + m1[2] = ptm[2] * btm[0] + ptm[3] * btm[2]; + m1[3] = ptm[2] * btm[1] + ptm[3] * btm[3]; + m1[4] = ptm[4] * btm[0] + ptm[5] * btm[2] + btm[4]; + m1[5] = ptm[4] * btm[1] + ptm[5] * btm[3] + btm[5]; + // m = m1 * iCTM = (PTM * BTM) * (iCTM) + m[0] = m1[0] * ictm[0] + m1[1] * ictm[2]; + m[1] = m1[0] * ictm[1] + m1[1] * ictm[3]; + m[2] = m1[2] * ictm[0] + m1[3] * ictm[2]; + m[3] = m1[2] * ictm[1] + m1[3] * ictm[3]; + m[4] = m1[4] * ictm[0] + m1[5] * ictm[2] + ictm[4]; + m[5] = m1[4] * ictm[1] + m1[5] * ictm[3] + ictm[5]; + + // set the new matrix + state->concatCTM(m[0], m[1], m[2], m[3], m[4], m[5]); + builder->setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: + doFunctionShFill(static_cast(shading)); + break; + case 2: + case 3: + // no need to implement these + break; + case 4: + case 5: + doGouraudTriangleShFill(static_cast(shading)); + break; + case 6: + case 7: + doPatchMeshShFill(static_cast(shading)); + break; + } + + // restore graphics state + restoreState(); + state->setPath(savedPath); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShFill(Object args[], int /*numArgs*/) +{ + GfxShading *shading = nullptr; + GfxPath *savedPath = nullptr; + double xMin, yMin, xMax, yMax; + double xTemp, yTemp; + double gradientTransform[6]; + double *matrix = nullptr; + GBool savedState = gFalse; + +#if defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + if (!(shading = res->lookupShading(args[0].getName(), nullptr, state))) { + return; + } +#else + if (!(shading = res->lookupShading(args[0].getName(), NULL))) { + return; + } +#endif + + // save current graphics state + if (shading->getType() != 2 && shading->getType() != 3) { + savedPath = state->getPath()->copy(); + saveState(); + savedState = gTrue; + } else { // get gradient transform if possible + // check proper operator sequence + // first there should be one W(*) and then one 'cm' somewhere before 'sh' + GBool seenClip, seenConcat; + seenClip = (clipHistory->getClipPath() != nullptr); + seenConcat = gFalse; + int i = 1; + while (i <= maxOperatorHistoryDepth) { + const char *opName = getPreviousOperator(i); + if (!strcmp(opName, "cm")) { + if (seenConcat) { // more than one 'cm' + break; + } else { + seenConcat = gTrue; + } + } + i++; + } + + if (seenConcat && seenClip) { + if (builder->getTransform(gradientTransform)) { + matrix = (double*)&gradientTransform; + builder->setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); // remove transform + } + } + } + + // clip to bbox + if (shading->getHasBBox()) { + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + if (matrix != nullptr) { + xTemp = matrix[0]*xMin + matrix[2]*yMin + matrix[4]; + yTemp = matrix[1]*xMin + matrix[3]*yMin + matrix[5]; + state->moveTo(xTemp, yTemp); + xTemp = matrix[0]*xMax + matrix[2]*yMin + matrix[4]; + yTemp = matrix[1]*xMax + matrix[3]*yMin + matrix[5]; + state->lineTo(xTemp, yTemp); + xTemp = matrix[0]*xMax + matrix[2]*yMax + matrix[4]; + yTemp = matrix[1]*xMax + matrix[3]*yMax + matrix[5]; + state->lineTo(xTemp, yTemp); + xTemp = matrix[0]*xMin + matrix[2]*yMax + matrix[4]; + yTemp = matrix[1]*xMin + matrix[3]*yMax + matrix[5]; + state->lineTo(xTemp, yTemp); + } + else { + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + } + state->closePath(); + state->clip(); + if (savedState) + builder->setClipPath(state); + else + builder->clip(state); + state->clearPath(); + } + + // set the color space + if (savedState) + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: + doFunctionShFill(static_cast(shading)); + break; + case 2: + case 3: + if (clipHistory->getClipPath()) { + builder->addShadedFill(shading, matrix, clipHistory->getClipPath(), + clipHistory->getClipType() == clipEO ? true : false); + } + break; + case 4: + case 5: + doGouraudTriangleShFill(static_cast(shading)); + break; + case 6: + case 7: + doPatchMeshShFill(static_cast(shading)); + break; + } + + // restore graphics state + if (savedState) { + restoreState(); + state->setPath(savedPath); + } + + delete shading; +} + +void PdfParser::doFunctionShFill(GfxFunctionShading *shading) { + double x0, y0, x1, y1; + GfxColor colors[4]; + + shading->getDomain(&x0, &y0, &x1, &y1); + shading->getColor(x0, y0, &colors[0]); + shading->getColor(x0, y1, &colors[1]); + shading->getColor(x1, y0, &colors[2]); + shading->getColor(x1, y1, &colors[3]); + doFunctionShFill1(shading, x0, y0, x1, y1, colors, 0); +} + +void PdfParser::doFunctionShFill1(GfxFunctionShading *shading, + double x0, double y0, + double x1, double y1, + GfxColor *colors, int depth) { + GfxColor fillColor; + GfxColor color0M, color1M, colorM0, colorM1, colorMM; + GfxColor colors2[4]; + double functionColorDelta = colorDeltas[pdfFunctionShading-1]; + const double *matrix; + double xM, yM; + int nComps, i, j; + + nComps = shading->getColorSpace()->getNComps(); + matrix = shading->getMatrix(); + + // compare the four corner colors + for (i = 0; i < 4; ++i) { + for (j = 0; j < nComps; ++j) { + if (abs(colors[i].c[j] - colors[(i+1)&3].c[j]) > functionColorDelta) { + break; + } + } + if (j < nComps) { + break; + } + } + + // center of the rectangle + xM = 0.5 * (x0 + x1); + yM = 0.5 * (y0 + y1); + + // the four corner colors are close (or we hit the recursive limit) + // -- fill the rectangle; but require at least one subdivision + // (depth==0) to avoid problems when the four outer corners of the + // shaded region are the same color + if ((i == 4 && depth > 0) || depth == maxDepths[pdfFunctionShading-1]) { + + // use the center color + shading->getColor(xM, yM, &fillColor); + state->setFillColor(&fillColor); + + // fill the rectangle + state->moveTo(x0 * matrix[0] + y0 * matrix[2] + matrix[4], + x0 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y0 * matrix[2] + matrix[4], + x1 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y1 * matrix[2] + matrix[4], + x1 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->lineTo(x0 * matrix[0] + y1 * matrix[2] + matrix[4], + x0 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + + // the four corner colors are not close enough -- subdivide the + // rectangle + } else { + + // colors[0] colorM0 colors[2] + // (x0,y0) (xM,y0) (x1,y0) + // +----------+----------+ + // | | | + // | UL | UR | + // color0M | colorMM | color1M + // (x0,yM) +----------+----------+ (x1,yM) + // | (xM,yM) | + // | LL | LR | + // | | | + // +----------+----------+ + // colors[1] colorM1 colors[3] + // (x0,y1) (xM,y1) (x1,y1) + + shading->getColor(x0, yM, &color0M); + shading->getColor(x1, yM, &color1M); + shading->getColor(xM, y0, &colorM0); + shading->getColor(xM, y1, &colorM1); + shading->getColor(xM, yM, &colorMM); + + // upper-left sub-rectangle + colors2[0] = colors[0]; + colors2[1] = color0M; + colors2[2] = colorM0; + colors2[3] = colorMM; + doFunctionShFill1(shading, x0, y0, xM, yM, colors2, depth + 1); + + // lower-left sub-rectangle + colors2[0] = color0M; + colors2[1] = colors[1]; + colors2[2] = colorMM; + colors2[3] = colorM1; + doFunctionShFill1(shading, x0, yM, xM, y1, colors2, depth + 1); + + // upper-right sub-rectangle + colors2[0] = colorM0; + colors2[1] = colorMM; + colors2[2] = colors[2]; + colors2[3] = color1M; + doFunctionShFill1(shading, xM, y0, x1, yM, colors2, depth + 1); + + // lower-right sub-rectangle + colors2[0] = colorMM; + colors2[1] = colorM1; + colors2[2] = color1M; + colors2[3] = colors[3]; + doFunctionShFill1(shading, xM, yM, x1, y1, colors2, depth + 1); + } +} + +void PdfParser::doGouraudTriangleShFill(GfxGouraudTriangleShading *shading) { + double x0, y0, x1, y1, x2, y2; + GfxColor color0, color1, color2; + int i; + + for (i = 0; i < shading->getNTriangles(); ++i) { + shading->getTriangle(i, &x0, &y0, &color0, + &x1, &y1, &color1, + &x2, &y2, &color2); + gouraudFillTriangle(x0, y0, &color0, x1, y1, &color1, x2, y2, &color2, + shading->getColorSpace()->getNComps(), 0); + } +} + +void PdfParser::gouraudFillTriangle(double x0, double y0, GfxColor *color0, + double x1, double y1, GfxColor *color1, + double x2, double y2, GfxColor *color2, + int nComps, int depth) { + double x01, y01, x12, y12, x20, y20; + double gouraudColorDelta = colorDeltas[pdfGouraudTriangleShading-1]; + GfxColor color01, color12, color20; + int i; + + for (i = 0; i < nComps; ++i) { + if (abs(color0->c[i] - color1->c[i]) > gouraudColorDelta || + abs(color1->c[i] - color2->c[i]) > gouraudColorDelta) { + break; + } + } + if (i == nComps || depth == maxDepths[pdfGouraudTriangleShading-1]) { + state->setFillColor(color0); + state->moveTo(x0, y0); + state->lineTo(x1, y1); + state->lineTo(x2, y2); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + x01 = 0.5 * (x0 + x1); + y01 = 0.5 * (y0 + y1); + x12 = 0.5 * (x1 + x2); + y12 = 0.5 * (y1 + y2); + x20 = 0.5 * (x2 + x0); + y20 = 0.5 * (y2 + y0); + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + color01.c[i] = (color0->c[i] + color1->c[i]) / 2; + color12.c[i] = (color1->c[i] + color2->c[i]) / 2; + color20.c[i] = (color2->c[i] + color0->c[i]) / 2; + } + gouraudFillTriangle(x0, y0, color0, x01, y01, &color01, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x1, y1, color1, + x12, y12, &color12, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x12, y12, &color12, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x20, y20, &color20, x12, y12, &color12, + x2, y2, color2, nComps, depth + 1); + } +} + +void PdfParser::doPatchMeshShFill(GfxPatchMeshShading *shading) { + int start, i; + + if (shading->getNPatches() > 128) { + start = 3; + } else if (shading->getNPatches() > 64) { + start = 2; + } else if (shading->getNPatches() > 16) { + start = 1; + } else { + start = 0; + } + for (i = 0; i < shading->getNPatches(); ++i) { + fillPatch(shading->getPatch(i), shading->getColorSpace()->getNComps(), + start); + } +} + +void PdfParser::fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth) { + GfxPatch patch00 = blankPatch(); + GfxPatch patch01 = blankPatch(); + GfxPatch patch10 = blankPatch(); + GfxPatch patch11 = blankPatch(); + GfxColor color = {{0}}; + double xx[4][8]; + double yy[4][8]; + double xxm; + double yym; + double patchColorDelta = colorDeltas[pdfPatchMeshShading - 1]; + + int i; + + for (i = 0; i < nComps; ++i) { + if (std::abs(patch->color[0][0].c[i] - patch->color[0][1].c[i]) + > patchColorDelta || + std::abs(patch->color[0][1].c[i] - patch->color[1][1].c[i]) + > patchColorDelta || + std::abs(patch->color[1][1].c[i] - patch->color[1][0].c[i]) + > patchColorDelta || + std::abs(patch->color[1][0].c[i] - patch->color[0][0].c[i]) + > patchColorDelta) { + break; + } + color.c[i] = GfxColorComp(patch->color[0][0].c[i]); + } + if (i == nComps || depth == maxDepths[pdfPatchMeshShading-1]) { + state->setFillColor(&color); + state->moveTo(patch->x[0][0], patch->y[0][0]); + state->curveTo(patch->x[0][1], patch->y[0][1], + patch->x[0][2], patch->y[0][2], + patch->x[0][3], patch->y[0][3]); + state->curveTo(patch->x[1][3], patch->y[1][3], + patch->x[2][3], patch->y[2][3], + patch->x[3][3], patch->y[3][3]); + state->curveTo(patch->x[3][2], patch->y[3][2], + patch->x[3][1], patch->y[3][1], + patch->x[3][0], patch->y[3][0]); + state->curveTo(patch->x[2][0], patch->y[2][0], + patch->x[1][0], patch->y[1][0], + patch->x[0][0], patch->y[0][0]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + for (i = 0; i < 4; ++i) { + xx[i][0] = patch->x[i][0]; + yy[i][0] = patch->y[i][0]; + xx[i][1] = 0.5 * (patch->x[i][0] + patch->x[i][1]); + yy[i][1] = 0.5 * (patch->y[i][0] + patch->y[i][1]); + xxm = 0.5 * (patch->x[i][1] + patch->x[i][2]); + yym = 0.5 * (patch->y[i][1] + patch->y[i][2]); + xx[i][6] = 0.5 * (patch->x[i][2] + patch->x[i][3]); + yy[i][6] = 0.5 * (patch->y[i][2] + patch->y[i][3]); + xx[i][2] = 0.5 * (xx[i][1] + xxm); + yy[i][2] = 0.5 * (yy[i][1] + yym); + xx[i][5] = 0.5 * (xxm + xx[i][6]); + yy[i][5] = 0.5 * (yym + yy[i][6]); + xx[i][3] = xx[i][4] = 0.5 * (xx[i][2] + xx[i][5]); + yy[i][3] = yy[i][4] = 0.5 * (yy[i][2] + yy[i][5]); + xx[i][7] = patch->x[i][3]; + yy[i][7] = patch->y[i][3]; + } + for (i = 0; i < 4; ++i) { + patch00.x[0][i] = xx[0][i]; + patch00.y[0][i] = yy[0][i]; + patch00.x[1][i] = 0.5 * (xx[0][i] + xx[1][i]); + patch00.y[1][i] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch10.x[2][i] = 0.5 * (xx[2][i] + xx[3][i]); + patch10.y[2][i] = 0.5 * (yy[2][i] + yy[3][i]); + patch00.x[2][i] = 0.5 * (patch00.x[1][i] + xxm); + patch00.y[2][i] = 0.5 * (patch00.y[1][i] + yym); + patch10.x[1][i] = 0.5 * (xxm + patch10.x[2][i]); + patch10.y[1][i] = 0.5 * (yym + patch10.y[2][i]); + patch00.x[3][i] = 0.5 * (patch00.x[2][i] + patch10.x[1][i]); + patch00.y[3][i] = 0.5 * (patch00.y[2][i] + patch10.y[1][i]); + patch10.x[0][i] = patch00.x[3][i]; + patch10.y[0][i] = patch00.y[3][i]; + patch10.x[3][i] = xx[3][i]; + patch10.y[3][i] = yy[3][i]; + } + for (i = 4; i < 8; ++i) { + patch01.x[0][i-4] = xx[0][i]; + patch01.y[0][i-4] = yy[0][i]; + patch01.x[1][i-4] = 0.5 * (xx[0][i] + xx[1][i]); + patch01.y[1][i-4] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch11.x[2][i-4] = 0.5 * (xx[2][i] + xx[3][i]); + patch11.y[2][i-4] = 0.5 * (yy[2][i] + yy[3][i]); + patch01.x[2][i-4] = 0.5 * (patch01.x[1][i-4] + xxm); + patch01.y[2][i-4] = 0.5 * (patch01.y[1][i-4] + yym); + patch11.x[1][i-4] = 0.5 * (xxm + patch11.x[2][i-4]); + patch11.y[1][i-4] = 0.5 * (yym + patch11.y[2][i-4]); + patch01.x[3][i-4] = 0.5 * (patch01.x[2][i-4] + patch11.x[1][i-4]); + patch01.y[3][i-4] = 0.5 * (patch01.y[2][i-4] + patch11.y[1][i-4]); + patch11.x[0][i-4] = patch01.x[3][i-4]; + patch11.y[0][i-4] = patch01.y[3][i-4]; + patch11.x[3][i-4] = xx[3][i]; + patch11.y[3][i-4] = yy[3][i]; + } + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + patch00.color[0][0].c[i] = patch->color[0][0].c[i]; + patch00.color[0][1].c[i] = (patch->color[0][0].c[i] + + patch->color[0][1].c[i]) / 2; + patch01.color[0][0].c[i] = patch00.color[0][1].c[i]; + patch01.color[0][1].c[i] = patch->color[0][1].c[i]; + patch01.color[1][1].c[i] = (patch->color[0][1].c[i] + + patch->color[1][1].c[i]) / 2; + patch11.color[0][1].c[i] = patch01.color[1][1].c[i]; + patch11.color[1][1].c[i] = patch->color[1][1].c[i]; + patch11.color[1][0].c[i] = (patch->color[1][1].c[i] + + patch->color[1][0].c[i]) / 2; + patch10.color[1][1].c[i] = patch11.color[1][0].c[i]; + patch10.color[1][0].c[i] = patch->color[1][0].c[i]; + patch10.color[0][0].c[i] = (patch->color[1][0].c[i] + + patch->color[0][0].c[i]) / 2; + patch00.color[1][0].c[i] = patch10.color[0][0].c[i]; + patch00.color[1][1].c[i] = (patch00.color[1][0].c[i] + + patch01.color[1][1].c[i]) / 2; + patch01.color[1][0].c[i] = patch00.color[1][1].c[i]; + patch11.color[0][0].c[i] = patch00.color[1][1].c[i]; + patch10.color[0][1].c[i] = patch00.color[1][1].c[i]; + } + fillPatch(&patch00, nComps, depth + 1); + fillPatch(&patch10, nComps, depth + 1); + fillPatch(&patch01, nComps, depth + 1); + fillPatch(&patch11, nComps, depth + 1); + } +} + +void PdfParser::doEndPath() { + if (state->isCurPt() && clip != clipNone) { + state->clip(); + if (clip == clipNormal) { + clipHistory->setClip(state->getPath(), clipNormal); + builder->clip(state); + } else { + clipHistory->setClip(state->getPath(), clipEO); + builder->clip(state, true); + } + } + clip = clipNone; + state->clearPath(); +} + +//------------------------------------------------------------------------ +// path clipping operators +//------------------------------------------------------------------------ + +void PdfParser::opClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipNormal; +} + +void PdfParser::opEOClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipEO; +} + +//------------------------------------------------------------------------ +// text object operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginText(Object /*args*/[], int /*numArgs*/) +{ + state->setTextMat(1, 0, 0, 1, 0, 0); + state->textMoveTo(0, 0); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; + builder->beginTextObject(state); +} + +void PdfParser::opEndText(Object /*args*/[], int /*numArgs*/) +{ + builder->endTextObject(state); +} + +//------------------------------------------------------------------------ +// text state operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetCharSpacing(Object args[], int /*numArgs*/) +{ + state->setCharSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFont(Object args[], int /*numArgs*/) +{ + GfxFont *font = res->lookupFont(args[0].getName()); + + if (!font) { + // unsetting the font (drawing no text) is better than using the + // previous one and drawing random glyphs from it + state->setFont(nullptr, args[1].getNum()); + fontChanged = gTrue; + return; + } + if (printCommands) { + printf(" font: tag=%s name='%s' %g\n", + font->getTag()->getCString(), + font->getName() ? font->getName()->getCString() : "???", + args[1].getNum()); + fflush(stdout); + } + + font->incRefCnt(); + state->setFont(font, args[1].getNum()); + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextLeading(Object args[], int /*numArgs*/) +{ + state->setLeading(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRender(Object args[], int /*numArgs*/) +{ + state->setRender(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRise(Object args[], int /*numArgs*/) +{ + state->setRise(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetWordSpacing(Object args[], int /*numArgs*/) +{ + state->setWordSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetHorizScaling(Object args[], int /*numArgs*/) +{ + state->setHorizScaling(args[0].getNum()); + builder->updateTextMatrix(state); + fontChanged = gTrue; +} + +//------------------------------------------------------------------------ +// text positioning operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMove(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = state->getLineY() + args[1].getNum(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMoveSet(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = args[1].getNum(); + state->setLeading(-ty); + ty += state->getLineY(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextMatrix(Object args[], int /*numArgs*/) +{ + state->setTextMat(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + state->textMoveTo(0, 0); + builder->updateTextMatrix(state); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; +} + +void PdfParser::opTextNextLine(Object /*args*/[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +//------------------------------------------------------------------------ +// text string operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowText(Object args[], int /*numArgs*/) +{ + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveSetShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/set/show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + state->setWordSpace(args[0].getNum()); + state->setCharSpace(args[1].getNum()); + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[2].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowSpaceText(Object args[], int /*numArgs*/) +{ + Array *a = nullptr; + Object obj; + int wMode = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show/space"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + wMode = state->getFont()->getWMode(); + a = args[0].getArray(); + for (int i = 0; i < a->getLength(); ++i) { + _POPPLER_CALL_ARGS(obj, a->get, i); + if (obj.isNum()) { + // this uses the absolute value of the font size to match + // Acrobat's behavior + if (wMode) { + state->textShift(0, -obj.getNum() * 0.001 * + fabs(state->getFontSize())); + } else { + state->textShift(-obj.getNum() * 0.001 * + fabs(state->getFontSize()), 0); + } + builder->updateTextShift(state, obj.getNum()); + } else if (obj.isString()) { + doShowText(obj.getString()); + } else { + error(errSyntaxError, getPos(), "Element of show/space array must be number or string"); + } + _POPPLER_FREE(obj); + } +} + +#if POPPLER_CHECK_VERSION(0,64,0) +void PdfParser::doShowText(const GooString *s) { +#else +void PdfParser::doShowText(GooString *s) { +#endif + GfxFont *font; + int wMode; + double riseX, riseY; + CharCode code; + Unicode _POPPLER_CONST_82 *u = nullptr; + double x, y, dx, dy, tdx, tdy; + double originX, originY, tOriginX, tOriginY; + double oldCTM[6], newCTM[6]; + const double *mat; + Object charProc; + Dict *resDict; + Parser *oldParser; +#if POPPLER_CHECK_VERSION(0,64,0) + const char *p; +#else + char *p; +#endif + int len, n, uLen; + + font = state->getFont(); + wMode = font->getWMode(); + + builder->beginString(state); + + // handle a Type 3 char + if (font->getType() == fontType3 && false) {//out->interpretType3Chars()) { + mat = state->getCTM(); + for (int i = 0; i < 6; ++i) { + oldCTM[i] = mat[i]; + } + mat = state->getTextMat(); + newCTM[0] = mat[0] * oldCTM[0] + mat[1] * oldCTM[2]; + newCTM[1] = mat[0] * oldCTM[1] + mat[1] * oldCTM[3]; + newCTM[2] = mat[2] * oldCTM[0] + mat[3] * oldCTM[2]; + newCTM[3] = mat[2] * oldCTM[1] + mat[3] * oldCTM[3]; + mat = font->getFontMatrix(); + newCTM[0] = mat[0] * newCTM[0] + mat[1] * newCTM[2]; + newCTM[1] = mat[0] * newCTM[1] + mat[1] * newCTM[3]; + newCTM[2] = mat[2] * newCTM[0] + mat[3] * newCTM[2]; + newCTM[3] = mat[2] * newCTM[1] + mat[3] * newCTM[3]; + newCTM[0] *= state->getFontSize(); + newCTM[1] *= state->getFontSize(); + newCTM[2] *= state->getFontSize(); + newCTM[3] *= state->getFontSize(); + newCTM[0] *= state->getHorizScaling(); + newCTM[2] *= state->getHorizScaling(); + state->textTransformDelta(0, state->getRise(), &riseX, &riseY); + double curX = state->getCurX(); + double curY = state->getCurY(); + double lineX = state->getLineX(); + double lineY = state->getLineY(); + oldParser = parser; + p = s->getCString(); + len = s->getLength(); + while (len > 0) { + n = font->getNextChar(p, len, &code, + &u, &uLen, /* TODO: This looks like a memory leak for u. */ + &dx, &dy, &originX, &originY); + dx = dx * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dx += state->getWordSpace(); + } + dx *= state->getHorizScaling(); + dy *= state->getFontSize(); + state->textTransformDelta(dx, dy, &tdx, &tdy); + state->transform(curX + riseX, curY + riseY, &x, &y); + saveState(); + state->setCTM(newCTM[0], newCTM[1], newCTM[2], newCTM[3], x, y); + //~ the CTM concat values here are wrong (but never used) + //out->updateCTM(state, 1, 0, 0, 1, 0, 0); + if (false){ /*!out->beginType3Char(state, curX + riseX, curY + riseY, tdx, tdy, + code, u, uLen)) {*/ + _POPPLER_CALL_ARGS(charProc, ((Gfx8BitFont *)font)->getCharProc, code); + if ((resDict = ((Gfx8BitFont *)font)->getResources())) { + pushResources(resDict); + } + if (charProc.isStream()) { + //parse(&charProc, gFalse); // TODO: parse into SVG font + } else { + error(errSyntaxError, getPos(), "Missing or bad Type3 CharProc entry"); + } + //out->endType3Char(state); + if (resDict) { + popResources(); + } + _POPPLER_FREE(charProc); + } + restoreState(); + // GfxState::restore() does *not* restore the current position, + // so we deal with it here using (curX, curY) and (lineX, lineY) + curX += tdx; + curY += tdy; + state->moveTo(curX, curY); + state->textSetPos(lineX, lineY); + p += n; + len -= n; + } + parser = oldParser; + + } else { + state->textTransformDelta(0, state->getRise(), &riseX, &riseY); + p = s->getCString(); + len = s->getLength(); + while (len > 0) { + n = font->getNextChar(p, len, &code, + &u, &uLen, /* TODO: This looks like a memory leak for u. */ + &dx, &dy, &originX, &originY); + + if (wMode) { + dx *= state->getFontSize(); + dy = dy * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dy += state->getWordSpace(); + } + } else { + dx = dx * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dx += state->getWordSpace(); + } + dx *= state->getHorizScaling(); + dy *= state->getFontSize(); + } + state->textTransformDelta(dx, dy, &tdx, &tdy); + originX *= state->getFontSize(); + originY *= state->getFontSize(); + state->textTransformDelta(originX, originY, &tOriginX, &tOriginY); + builder->addChar(state, state->getCurX() + riseX, state->getCurY() + riseY, + dx, dy, tOriginX, tOriginY, code, n, u, uLen); + state->shift(tdx, tdy); + p += n; + len -= n; + } + } + + builder->endString(state); +} + + +//------------------------------------------------------------------------ +// XObject operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opXObject(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, refObj; + +#if POPPLER_CHECK_VERSION(0,64,0) + const char *name = args[0].getName(); +#else + char *name = args[0].getName(); +#endif + _POPPLER_CALL_ARGS(obj1, res->lookupXObject, name); + if (obj1.isNull()) { + return; + } + if (!obj1.isStream()) { + error(errSyntaxError, getPos(), "XObject '{0:s}' is wrong type", name); + _POPPLER_FREE(obj1); + return; + } + _POPPLER_CALL_ARGS(obj2, obj1.streamGetDict()->lookup, "Subtype"); + if (obj2.isName(const_cast("Image"))) { + _POPPLER_CALL_ARGS(refObj, res->lookupXObjectNF, name); + doImage(&refObj, obj1.getStream(), gFalse); + _POPPLER_FREE(refObj); + } else if (obj2.isName(const_cast("Form"))) { + doForm(&obj1); + } else if (obj2.isName(const_cast("PS"))) { + _POPPLER_CALL_ARGS(obj3, obj1.streamGetDict()->lookup, "Level1"); +/* out->psXObject(obj1.getStream(), + obj3.isStream() ? obj3.getStream() : (Stream *)NULL);*/ + } else if (obj2.isName()) { + error(errSyntaxError, getPos(), "Unknown XObject subtype '{0:s}'", obj2.getName()); + } else { + error(errSyntaxError, getPos(), "XObject subtype is missing or wrong type"); + } + _POPPLER_FREE(obj2); + _POPPLER_FREE(obj1); +} + +void PdfParser::doImage(Object * /*ref*/, Stream *str, GBool inlineImg) +{ + Dict *dict; + int width, height; + int bits; + GBool interpolate; + StreamColorSpaceMode csMode; + GBool mask; + GBool invert; + Object maskObj, smaskObj; + GBool haveColorKeyMask, haveExplicitMask, haveSoftMask; + GBool maskInvert; + GBool maskInterpolate; + Object obj1, obj2; + + // get info from the stream + bits = 0; + csMode = streamCSNone; + str->getImageParams(&bits, &csMode); + + // get stream dict + dict = str->getDict(); + + // get size + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "W"); + } + if (obj1.isInt()){ + width = obj1.getInt(); + } + else if (obj1.isReal()) { + width = (int)obj1.getReal(); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "H"); + } + if (obj1.isInt()) { + height = obj1.getInt(); + } + else if (obj1.isReal()){ + height = static_cast(obj1.getReal()); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + + // image interpolation + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "I"); + } + if (obj1.isBool()) + interpolate = obj1.getBool(); + else + interpolate = gFalse; + _POPPLER_FREE(obj1); + maskInterpolate = gFalse; + + // image or mask? + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "IM"); + } + mask = gFalse; + if (obj1.isBool()) { + mask = obj1.getBool(); + } + else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // bit depth + if (bits == 0) { + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BPC"); + } + if (obj1.isInt()) { + bits = obj1.getInt(); + } else if (mask) { + bits = 1; + } else { + goto err2; + } + _POPPLER_FREE(obj1); + } + + // display a mask + if (mask) { + // check for inverted mask + if (bits != 1) { + goto err1; + } + invert = gFalse; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + invert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // draw it + builder->addImageMask(state, str, width, height, invert, interpolate); + + } else { + // get color space and color map + GfxColorSpace *colorSpace; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "CS"); + } + if (!obj1.isNull()) { + colorSpace = lookupColorSpaceCopy(obj1); + } else if (csMode == streamCSDeviceGray) { + colorSpace = new GfxDeviceGrayColorSpace(); + } else if (csMode == streamCSDeviceRGB) { + colorSpace = new GfxDeviceRGBColorSpace(); + } else if (csMode == streamCSDeviceCMYK) { + colorSpace = new GfxDeviceCMYKColorSpace(); + } else { + colorSpace = nullptr; + } + _POPPLER_FREE(obj1); + if (!colorSpace) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + GfxImageColorMap *colorMap = new GfxImageColorMap(bits, &obj1, colorSpace); + _POPPLER_FREE(obj1); + if (!colorMap->isOk()) { + delete colorMap; + goto err1; + } + + // get the mask + int maskColors[2*gfxColorMaxComps]; + haveColorKeyMask = haveExplicitMask = haveSoftMask = gFalse; + Stream *maskStr = nullptr; + int maskWidth = 0; + int maskHeight = 0; + maskInvert = gFalse; + GfxImageColorMap *maskColorMap = nullptr; + _POPPLER_CALL_ARGS(maskObj, dict->lookup, "Mask"); + _POPPLER_CALL_ARGS(smaskObj, dict->lookup, "SMask"); + Dict* maskDict; + if (smaskObj.isStream()) { + // soft mask + if (inlineImg) { + goto err1; + } + maskStr = smaskObj.getStream(); + maskDict = smaskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BPC"); + } + if (!obj1.isInt()) { + goto err2; + } + int maskBits = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "CS"); + } + GfxColorSpace *maskColorSpace = lookupColorSpaceCopy(obj1); + _POPPLER_FREE(obj1); + if (!maskColorSpace || maskColorSpace->getMode() != csDeviceGray) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + maskColorMap = new GfxImageColorMap(maskBits, &obj1, maskColorSpace); + _POPPLER_FREE(obj1); + if (!maskColorMap->isOk()) { + delete maskColorMap; + goto err1; + } + //~ handle the Matte entry + haveSoftMask = gTrue; + } else if (maskObj.isArray()) { + // color key mask + int i; + for (i = 0; i < maskObj.arrayGetLength() && i < 2*gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj1, maskObj.arrayGet, i); + maskColors[i] = obj1.getInt(); + _POPPLER_FREE(obj1); + } + haveColorKeyMask = gTrue; + } else if (maskObj.isStream()) { + // explicit mask + if (inlineImg) { + goto err1; + } + maskStr = maskObj.getStream(); + maskDict = maskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "IM"); + } + if (!obj1.isBool() || !obj1.getBool()) { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + maskInvert = gFalse; + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + maskInvert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + haveExplicitMask = gTrue; + } + + // draw it + if (haveSoftMask) { + builder->addSoftMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskColorMap, maskInterpolate); + delete maskColorMap; + } else if (haveExplicitMask) { + builder->addMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskInvert, maskInterpolate); + } else { + builder->addImage(state, str, width, height, colorMap, interpolate, + haveColorKeyMask ? maskColors : static_cast(nullptr)); + } + delete colorMap; + + _POPPLER_FREE(maskObj); + _POPPLER_FREE(smaskObj); + } + + return; + + err2: + _POPPLER_FREE(obj1); + err1: + error(errSyntaxError, getPos(), "Bad image parameters"); +} + +void PdfParser::doForm(Object *str) { + Dict *dict; + GBool transpGroup, isolated, knockout; + GfxColorSpace *blendingColorSpace; + Object matrixObj, bboxObj; + double m[6], bbox[4]; + Object resObj; + Dict *resDict; + Object obj1, obj2, obj3; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(bboxObj, dict->lookup, "BBox"); + if (!bboxObj.isArray()) { + _POPPLER_FREE(bboxObj); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj1, bboxObj.arrayGet, i); + bbox[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + _POPPLER_FREE(bboxObj); + + // get matrix + _POPPLER_CALL_ARGS(matrixObj, dict->lookup, "Matrix"); + if (matrixObj.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj1, matrixObj.arrayGet, i); + m[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(matrixObj); + + // get resources + _POPPLER_CALL_ARGS(resObj, dict->lookup, "Resources"); + resDict = resObj.isDict() ? resObj.getDict() : (Dict *)nullptr; + + // check for a transparency group + transpGroup = isolated = knockout = gFalse; + blendingColorSpace = nullptr; + if (_POPPLER_CALL_ARGS_DEREF(obj1, dict->lookup, "Group").isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "S").isName("Transparency")) { + transpGroup = gTrue; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "CS").isNull()) { +#if defined(POPPLER_EVEN_NEWER_NEW_COLOR_SPACE_API) + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj3, nullptr, state); +#elif defined(POPPLER_EVEN_NEWER_COLOR_SPACE_API) + blendingColorSpace = GfxColorSpace::parse(&obj3, NULL, NULL); +#else + blendingColorSpace = GfxColorSpace::parse(&obj3, NULL); +#endif + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "I").isBool()) { + isolated = obj3.getBool(); + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "K").isBool()) { + knockout = obj3.getBool(); + } + _POPPLER_FREE(obj3); + } + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, + transpGroup, gFalse, blendingColorSpace, isolated, knockout); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(resObj); +} + +void PdfParser::doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, + GBool transpGroup, GBool softMask, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + GBool alpha, Function *transferFunc, + GfxColor *backdropColor) { + Parser *oldParser; + double oldBaseMatrix[6]; + int i; + + // push new resources on stack + pushResources(resDict); + + // save current graphics state + saveState(); + + // kill any pre-existing path + state->clearPath(); + + if (softMask || transpGroup) { + builder->clearSoftMask(state); + builder->pushTransparencyGroup(state, bbox, blendingColorSpace, + isolated, knockout, softMask); + } + + // save current parser + oldParser = parser; + + // set form transformation matrix + state->concatCTM(matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5]); + builder->setTransform(matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5]); + + // set form bounding box + state->moveTo(bbox[0], bbox[1]); + state->lineTo(bbox[2], bbox[1]); + state->lineTo(bbox[2], bbox[3]); + state->lineTo(bbox[0], bbox[3]); + state->closePath(); + state->clip(); + clipHistory->setClip(state->getPath()); + builder->clip(state); + state->clearPath(); + + if (softMask || transpGroup) { + if (state->getBlendMode() != gfxBlendNormal) { + state->setBlendMode(gfxBlendNormal); + } + if (state->getFillOpacity() != 1) { + builder->setGroupOpacity(state->getFillOpacity()); + state->setFillOpacity(1); + } + if (state->getStrokeOpacity() != 1) { + state->setStrokeOpacity(1); + } + } + + // set new base matrix + for (i = 0; i < 6; ++i) { + oldBaseMatrix[i] = baseMatrix[i]; + baseMatrix[i] = state->getCTM()[i]; + } + + // draw the form + parse(str, gFalse); + + // restore base matrix + for (i = 0; i < 6; ++i) { + baseMatrix[i] = oldBaseMatrix[i]; + } + + // restore parser + parser = oldParser; + + if (softMask || transpGroup) { + builder->popTransparencyGroup(state); + } + + // restore graphics state + restoreState(); + + // pop resource stack + popResources(); + + if (softMask) { + builder->setSoftMask(state, bbox, alpha, transferFunc, backdropColor); + } else if (transpGroup) { + builder->paintTransparencyGroup(state, bbox); + } + + return; +} + +//------------------------------------------------------------------------ +// in-line image operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginImage(Object /*args*/[], int /*numArgs*/) +{ + // build dict/stream + Stream *str = buildImageStream(); + + // display the image + if (str) { + doImage(nullptr, str, gTrue); + + // skip 'EI' tag + int c1 = str->getUndecodedStream()->getChar(); + int c2 = str->getUndecodedStream()->getChar(); + while (!(c1 == 'E' && c2 == 'I') && c2 != EOF) { + c1 = c2; + c2 = str->getUndecodedStream()->getChar(); + } + delete str; + } +} + +Stream *PdfParser::buildImageStream() { + Object dict; + Object obj; + Stream *str; + + // build dictionary +#if defined(POPPLER_NEW_OBJECT_API) + dict = Object(new Dict(xref)); +#else + dict.initDict(xref); +#endif + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isCmd(const_cast("ID")) && !obj.isEOF()) { + if (!obj.isName()) { + error(errSyntaxError, getPos(), "Inline image dictionary key must be a name object"); + _POPPLER_FREE(obj); + } else { + Object obj2; + _POPPLER_CALL(obj2, parser->getObj); + if (obj2.isEOF() || obj2.isError()) { + _POPPLER_FREE(obj); + break; + } + _POPPLER_DICTADD(dict, obj.getName(), obj2); + _POPPLER_FREE(obj); + _POPPLER_FREE(obj2); + } + _POPPLER_CALL(obj, parser->getObj); + } + if (obj.isEOF()) { + error(errSyntaxError, getPos(), "End of file in inline image"); + _POPPLER_FREE(obj); + _POPPLER_FREE(dict); + return nullptr; + } + _POPPLER_FREE(obj); + + // make stream +#if defined(POPPLER_NEW_OBJECT_API) + str = new EmbedStream(parser->getStream(), dict.copy(), gFalse, 0); + str = str->addFilters(dict.getDict()); +#else + str = new EmbedStream(parser->getStream(), &dict, gFalse, 0); + str = str->addFilters(&dict); +#endif + + return str; +} + +void PdfParser::opImageData(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'ID' operator"); +} + +void PdfParser::opEndImage(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'EI' operator"); +} + +//------------------------------------------------------------------------ +// type 3 font operators +//------------------------------------------------------------------------ + +void PdfParser::opSetCharWidth(Object /*args*/[], int /*numArgs*/) +{ +} + +void PdfParser::opSetCacheDevice(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// compatibility operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + ++ignoreUndef; +} + +void PdfParser::opEndIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + if (ignoreUndef > 0) + --ignoreUndef; +} + +//------------------------------------------------------------------------ +// marked content operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginMarkedContent(Object args[], int numArgs) { + if (printCommands) { + printf(" marked content: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + + if(numArgs == 2) { + //out->beginMarkedContent(args[0].getName(),args[1].getDict()); + } else { + //out->beginMarkedContent(args[0].getName()); + } +} + +void PdfParser::opEndMarkedContent(Object /*args*/[], int /*numArgs*/) +{ + //out->endMarkedContent(); +} + +void PdfParser::opMarkPoint(Object args[], int numArgs) { + if (printCommands) { + printf(" mark point: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + + if(numArgs == 2) { + //out->markPoint(args[0].getName(),args[1].getDict()); + } else { + //out->markPoint(args[0].getName()); + } + +} + +//------------------------------------------------------------------------ +// misc +//------------------------------------------------------------------------ + +void PdfParser::saveState() { + bool is_radial = false; + + GfxPattern *pattern = state->getFillPattern(); + if (pattern != nullptr) + if (pattern->getType() == 2 ) { + GfxShadingPattern *shading_pattern = static_cast(pattern); + GfxShading *shading = shading_pattern->getShading(); + if (shading->getType() == 3) + is_radial = true; + } + + builder->saveState(); + if (is_radial) + state->save(); // nasty hack to prevent GfxRadialShading from getting corrupted during copy operation + else + state = state->save(); // see LP Bug 919176 comment 8 + clipHistory = clipHistory->save(); +} + +void PdfParser::restoreState() { + clipHistory = clipHistory->restore(); + state = state->restore(); + builder->restoreState(); +} + +void PdfParser::pushResources(Dict *resDict) { + res = new GfxResources(xref, resDict, res); +} + +void PdfParser::popResources() { + GfxResources *resPtr; + + resPtr = res->getNext(); + delete res; + res = resPtr; +} + +void PdfParser::setDefaultApproximationPrecision() { + for (int i = 1; i <= pdfNumShadingTypes; ++i) { + setApproximationPrecision(i, defaultShadingColorDelta, defaultShadingMaxDepth); + } +} + +void PdfParser::setApproximationPrecision(int shadingType, double colorDelta, + int maxDepth) { + + if (shadingType > pdfNumShadingTypes || shadingType < 1) { + return; + } + colorDeltas[shadingType-1] = dblToCol(colorDelta); + maxDepths[shadingType-1] = maxDepth; +} + +//------------------------------------------------------------------------ +// ClipHistoryEntry +//------------------------------------------------------------------------ + +ClipHistoryEntry::ClipHistoryEntry(GfxPath *clipPathA, GfxClipType clipTypeA) : + saved(nullptr), + clipPath((clipPathA) ? clipPathA->copy() : nullptr), + clipType(clipTypeA) +{ +} + +ClipHistoryEntry::~ClipHistoryEntry() +{ + if (clipPath) { + delete clipPath; + clipPath = nullptr; + } +} + +void ClipHistoryEntry::setClip(_POPPLER_CONST_83 GfxPath *clipPathA, GfxClipType clipTypeA) { + // Free previous clip path + if (clipPath) { + delete clipPath; + } + if (clipPathA) { + clipPath = clipPathA->copy(); + clipType = clipTypeA; + } else { + clipPath = nullptr; + clipType = clipNormal; + } +} + +ClipHistoryEntry *ClipHistoryEntry::save() { + ClipHistoryEntry *newEntry = new ClipHistoryEntry(this); + newEntry->saved = this; + + return newEntry; +} + +ClipHistoryEntry *ClipHistoryEntry::restore() { + ClipHistoryEntry *oldEntry; + + if (saved) { + oldEntry = saved; + saved = nullptr; + delete this; // TODO really should avoid deleting from inside. + } else { + oldEntry = this; + } + + return oldEntry; +} + +ClipHistoryEntry::ClipHistoryEntry(ClipHistoryEntry *other) { + if (other->clipPath) { + this->clipPath = other->clipPath->copy(); + this->clipType = other->clipType; + } else { + this->clipPath = nullptr; + this->clipType = clipNormal; + } + saved = nullptr; +} + +#endif /* HAVE_POPPLER */ diff --git a/src/extension/internal/pdfinput/pdf-parser.h b/src/extension/internal/pdfinput/pdf-parser.h new file mode 100644 index 0000000..b92c415 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.h @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * see git history + * + * Derived from Gfx.h from poppler (?) which derives from Xpdf, Copyright 1996-2003 Glyph & Cog, LLC, which is under GPL2+. + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef PDF_PARSER_H +#define PDF_PARSER_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +#ifdef USE_GCC_PRAGMAS +#pragma interface +#endif + +namespace Inkscape { + namespace Extension { + namespace Internal { + class SvgBuilder; + } + } +} + +// TODO clean up and remove using: +using Inkscape::Extension::Internal::SvgBuilder; + +#include "glib/poppler-features.h" +#include "Object.h" + +#include +#include +#include + +class GooString; +class XRef; +class Array; +class Stream; +class Parser; +class Dict; +class Function; +class OutputDev; +class GfxFontDict; +class GfxFont; +class GfxPattern; +class GfxTilingPattern; +class GfxShadingPattern; +class GfxShading; +class GfxFunctionShading; +class GfxAxialShading; +class GfxRadialShading; +class GfxGouraudTriangleShading; +class GfxPatchMeshShading; +struct GfxPatch; +class GfxState; +struct GfxColor; +class GfxColorSpace; +class Gfx; +class GfxResources; +class PDFRectangle; +class AnnotBorderStyle; + +class PdfParser; + +class ClipHistoryEntry; + +//------------------------------------------------------------------------ + +#ifndef GFX_H +enum GfxClipType { + clipNone, + clipNormal, + clipEO +}; + +enum TchkType { + tchkBool, // boolean + tchkInt, // integer + tchkNum, // number (integer or real) + tchkString, // string + tchkName, // name + tchkArray, // array + tchkProps, // properties (dictionary or name) + tchkSCN, // scn/SCN args (number of name) + tchkNone // used to avoid empty initializer lists +}; +#endif /* GFX_H */ + +#define maxOperatorArgs 33 + +struct PdfOperator { + char name[4]; + int numArgs; + TchkType tchk[maxOperatorArgs]; + void (PdfParser::*func)(Object args[], int numArgs); +}; + +#undef maxOperatorArgs + +struct OpHistoryEntry { + const char *name; // operator's name + GfxState *state; // saved state, NULL if none + GBool executed; // whether the operator has been executed + + OpHistoryEntry *next; // next entry on stack + unsigned depth; // total number of entries descending from this +}; + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +#define pdfFunctionShading 1 +#define pdfAxialShading 2 +#define pdfRadialShading 3 +#define pdfGouraudTriangleShading 4 +#define pdfPatchMeshShading 5 +#define pdfNumShadingTypes 5 + + + +/** + * PDF parsing module using libpoppler's facilities. + */ +class PdfParser { +public: + + // Constructor for regular output. + PdfParser(XRef *xrefA, SvgBuilder *builderA, int pageNum, int rotate, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box, + _POPPLER_CONST PDFRectangle *cropBox); + + // Constructor for a sub-page object. + PdfParser(XRef *xrefA, Inkscape::Extension::Internal::SvgBuilder *builderA, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box); + + virtual ~PdfParser(); + + // Interpret a stream or array of streams. + void parse(Object *obj, GBool topLevel = gTrue); + + // Save graphics state. + void saveState(); + + // Restore graphics state. + void restoreState(); + + // Get the current graphics state object. + GfxState *getState() { return state; } + + // Set the precision of approximation for specific shading fills. + void setApproximationPrecision(int shadingType, double colorDelta, int maxDepth); + +private: + + XRef *xref; // the xref table for this PDF file + SvgBuilder *builder; // SVG generator + GBool subPage; // is this a sub-page object? + GBool printCommands; // print the drawing commands (for debugging) + GfxResources *res; // resource stack + + GfxState *state; // current graphics state + GBool fontChanged; // set if font or text matrix has changed + GfxClipType clip; // do a clip? + int ignoreUndef; // current BX/EX nesting level + double baseMatrix[6]; // default matrix for most recent + // page/form/pattern + int formDepth; + + Parser *parser; // parser for page content stream(s) + + static PdfOperator opTab[]; // table of operators + + int colorDeltas[pdfNumShadingTypes]; + // max deltas allowed in any color component + // for the approximation of shading fills + int maxDepths[pdfNumShadingTypes]; // max recursive depths + + ClipHistoryEntry *clipHistory; // clip path stack + OpHistoryEntry *operatorHistory; // list containing the last N operators + + //! Caches color spaces by name + std::map> colorSpacesCache; + + GfxColorSpace *lookupColorSpaceCopy(Object &); + + void setDefaultApproximationPrecision(); // init color deltas + void pushOperator(const char *name); + OpHistoryEntry *popOperator(); + const char *getPreviousOperator(unsigned int look_back=1); // returns the nth previous operator's name + + void go(GBool topLevel); + void execOp(Object *cmd, Object args[], int numArgs); + PdfOperator *findOp(const char *name); + GBool checkArg(Object *arg, TchkType type); + int getPos(); + + // graphics state operators + void opSave(Object args[], int numArgs); + void opRestore(Object args[], int numArgs); + void opConcat(Object args[], int numArgs); + void opSetDash(Object args[], int numArgs); + void opSetFlat(Object args[], int numArgs); + void opSetLineJoin(Object args[], int numArgs); + void opSetLineCap(Object args[], int numArgs); + void opSetMiterLimit(Object args[], int numArgs); + void opSetLineWidth(Object args[], int numArgs); + void opSetExtGState(Object args[], int numArgs); + void doSoftMask(Object *str, GBool alpha, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor); + void opSetRenderingIntent(Object args[], int numArgs); + + // color operators + void opSetFillGray(Object args[], int numArgs); + void opSetStrokeGray(Object args[], int numArgs); + void opSetFillCMYKColor(Object args[], int numArgs); + void opSetStrokeCMYKColor(Object args[], int numArgs); + void opSetFillRGBColor(Object args[], int numArgs); + void opSetStrokeRGBColor(Object args[], int numArgs); + void opSetFillColorSpace(Object args[], int numArgs); + void opSetStrokeColorSpace(Object args[], int numArgs); + void opSetFillColor(Object args[], int numArgs); + void opSetStrokeColor(Object args[], int numArgs); + void opSetFillColorN(Object args[], int numArgs); + void opSetStrokeColorN(Object args[], int numArgs); + + // path segment operators + void opMoveTo(Object args[], int numArgs); + void opLineTo(Object args[], int numArgs); + void opCurveTo(Object args[], int numArgs); + void opCurveTo1(Object args[], int numArgs); + void opCurveTo2(Object args[], int numArgs); + void opRectangle(Object args[], int numArgs); + void opClosePath(Object args[], int numArgs); + + // path painting operators + void opEndPath(Object args[], int numArgs); + void opStroke(Object args[], int numArgs); + void opCloseStroke(Object args[], int numArgs); + void opFill(Object args[], int numArgs); + void opEOFill(Object args[], int numArgs); + void opFillStroke(Object args[], int numArgs); + void opCloseFillStroke(Object args[], int numArgs); + void opEOFillStroke(Object args[], int numArgs); + void opCloseEOFillStroke(Object args[], int numArgs); + void doFillAndStroke(GBool eoFill); + void doPatternFillFallback(GBool eoFill); + void doPatternStrokeFallback(); + void doShadingPatternFillFallback(GfxShadingPattern *sPat, + GBool stroke, GBool eoFill); + void opShFill(Object args[], int numArgs); + void doFunctionShFill(GfxFunctionShading *shading); + void doFunctionShFill1(GfxFunctionShading *shading, + double x0, double y0, + double x1, double y1, + GfxColor *colors, int depth); + void doGouraudTriangleShFill(GfxGouraudTriangleShading *shading); + void gouraudFillTriangle(double x0, double y0, GfxColor *color0, + double x1, double y1, GfxColor *color1, + double x2, double y2, GfxColor *color2, + int nComps, int depth); + void doPatchMeshShFill(GfxPatchMeshShading *shading); + void fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth); + void doEndPath(); + + // path clipping operators + void opClip(Object args[], int numArgs); + void opEOClip(Object args[], int numArgs); + + // text object operators + void opBeginText(Object args[], int numArgs); + void opEndText(Object args[], int numArgs); + + // text state operators + void opSetCharSpacing(Object args[], int numArgs); + void opSetFont(Object args[], int numArgs); + void opSetTextLeading(Object args[], int numArgs); + void opSetTextRender(Object args[], int numArgs); + void opSetTextRise(Object args[], int numArgs); + void opSetWordSpacing(Object args[], int numArgs); + void opSetHorizScaling(Object args[], int numArgs); + + // text positioning operators + void opTextMove(Object args[], int numArgs); + void opTextMoveSet(Object args[], int numArgs); + void opSetTextMatrix(Object args[], int numArgs); + void opTextNextLine(Object args[], int numArgs); + + // text string operators + void opShowText(Object args[], int numArgs); + void opMoveShowText(Object args[], int numArgs); + void opMoveSetShowText(Object args[], int numArgs); + void opShowSpaceText(Object args[], int numArgs); +#if POPPLER_CHECK_VERSION(0,64,0) + void doShowText(const GooString *s); +#else + void doShowText(GooString *s); +#endif + + + // XObject operators + void opXObject(Object args[], int numArgs); + void doImage(Object *ref, Stream *str, GBool inlineImg); + void doForm(Object *str); + void doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, + GBool transpGroup = gFalse, GBool softMask = gFalse, + GfxColorSpace *blendingColorSpace = nullptr, + GBool isolated = gFalse, GBool knockout = gFalse, + GBool alpha = gFalse, Function *transferFunc = nullptr, + GfxColor *backdropColor = nullptr); + + // in-line image operators + void opBeginImage(Object args[], int numArgs); + Stream *buildImageStream(); + void opImageData(Object args[], int numArgs); + void opEndImage(Object args[], int numArgs); + + // type 3 font operators + void opSetCharWidth(Object args[], int numArgs); + void opSetCacheDevice(Object args[], int numArgs); + + // compatibility operators + void opBeginIgnoreUndef(Object args[], int numArgs); + void opEndIgnoreUndef(Object args[], int numArgs); + + // marked content operators + void opBeginMarkedContent(Object args[], int numArgs); + void opEndMarkedContent(Object args[], int numArgs); + void opMarkPoint(Object args[], int numArgs); + + void pushResources(Dict *resDict); + void popResources(); +}; + +#endif /* HAVE_POPPLER */ + +#endif /* PDF_PARSER_H */ diff --git a/src/extension/internal/pdfinput/poppler-transition-api.h b/src/extension/internal/pdfinput/poppler-transition-api.h new file mode 100644 index 0000000..9671811 --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-transition-api.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO short description + *//* + * Authors: + * see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_POPPLER_TRANSITION_API_H +#define SEEN_POPPLER_TRANSITION_API_H + +#include + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_CONST_83 const +#else +#define _POPPLER_CONST_83 +#endif + +#if POPPLER_CHECK_VERSION(0, 82, 0) +#define _POPPLER_CONST_82 const +#else +#define _POPPLER_CONST_82 +#endif + +#if POPPLER_CHECK_VERSION(0, 76, 0) +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, obj, gFalse) +#else +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, new Lexer(xref, obj), gFalse) +#endif + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) std::unique_ptr(new GlobalParams(args)) +#else +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) new GlobalParams(args) +#endif + + +#if POPPLER_CHECK_VERSION(0, 72, 0) +#define getCString c_str +#endif + +#if POPPLER_CHECK_VERSION(0,71,0) +typedef bool GBool; +#define gTrue true +#define gFalse false +#endif + +#if POPPLER_CHECK_VERSION(0,70,0) +#define _POPPLER_CONST const +#else +#define _POPPLER_CONST +#endif + +#if POPPLER_CHECK_VERSION(0,69,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(key, std::move(obj)) +#elif POPPLER_CHECK_VERSION(0,58,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), std::move(obj)) +#else +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), &obj) +#endif + +#if POPPLER_CHECK_VERSION(0,58,0) +#define POPPLER_NEW_OBJECT_API +#define _POPPLER_FREE(obj) +#define _POPPLER_CALL(ret, func) (ret = func()) +#define _POPPLER_CALL_ARGS(ret, func, ...) (ret = func(__VA_ARGS__)) +#define _POPPLER_CALL_ARGS_DEREF _POPPLER_CALL_ARGS +#else +#define _POPPLER_FREE(obj) (obj).free() +#define _POPPLER_CALL(ret, func) (*func(&ret)) +#define _POPPLER_CALL_ARGS(ret, func, ...) (func(__VA_ARGS__, &ret)) +#define _POPPLER_CALL_ARGS_DEREF(...) (*_POPPLER_CALL_ARGS(__VA_ARGS__)) +#endif + +#if POPPLER_CHECK_VERSION(0, 29, 0) +#define POPPLER_EVEN_NEWER_NEW_COLOR_SPACE_API +#endif + +#if POPPLER_CHECK_VERSION(0, 25, 0) +#define POPPLER_EVEN_NEWER_COLOR_SPACE_API +#endif + +#endif diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp new file mode 100644 index 0000000..1f04ee4 --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -0,0 +1,1938 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Jon A. Cruz + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include + +#ifdef HAVE_POPPLER + +#include "svg-builder.h" +#include "pdf-parser.h" + +#include "document.h" +#include "png.h" + +#include "xml/document.h" +#include "xml/node.h" +#include "xml/repr.h" +#include "svg/svg.h" +#include "svg/path-string.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" +#include "color.h" +#include "util/units.h" +#include "display/nr-filter-utils.h" +#include "libnrtype/font-instance.h" +#include "object/sp-defs.h" + +#include "Function.h" +#include "GfxState.h" +#include "GfxFont.h" +#include "Stream.h" +#include "Page.h" +#include "UnicodeMap.h" +#include "GlobalParams.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +//#define IFTRACE(_code) _code +#define IFTRACE(_code) + +#define TRACE(_args) IFTRACE(g_print _args) + +/** + * \struct SvgTransparencyGroup + * \brief Holds information about a PDF transparency group + */ +struct SvgTransparencyGroup { + double bbox[6]; // TODO should this be 4? + Inkscape::XML::Node *container; + + bool isolated; + bool knockout; + bool for_softmask; + + SvgTransparencyGroup *next; +}; + +/** + * \class SvgBuilder + * + */ + +SvgBuilder::SvgBuilder(SPDocument *document, gchar *docname, XRef *xref) +{ + _is_top_level = true; + _doc = document; + _docname = docname; + _xref = xref; + _xml_doc = _doc->getReprDoc(); + _container = _root = _doc->getReprRoot(); + _root->setAttribute("xml:space", "preserve"); + _init(); + + // Set default preference settings + _preferences = _xml_doc->createElement("svgbuilder:prefs"); + _preferences->setAttribute("embedImages", "1"); + _preferences->setAttribute("localFonts", "1"); +} + +SvgBuilder::SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root) { + _is_top_level = false; + _doc = parent->_doc; + _docname = parent->_docname; + _xref = parent->_xref; + _xml_doc = parent->_xml_doc; + _preferences = parent->_preferences; + _container = this->_root = root; + _init(); +} + +SvgBuilder::~SvgBuilder() = default; + +void SvgBuilder::_init() { + _font_style = nullptr; + _current_font = nullptr; + _font_specification = nullptr; + _font_scaling = 1; + _need_font_update = true; + _in_text_object = false; + _invalidated_style = true; + _current_state = nullptr; + _width = 0; + _height = 0; + + // Fill _availableFontNames (Bug LP #179589) (code cfr. FontLister) + std::vector families; + font_factory::Default()->GetUIFamilies(families); + for (auto & familie : families) { + _availableFontNames.emplace_back(pango_font_family_get_name(familie)); + } + + _transp_group_stack = nullptr; + SvgGraphicsState initial_state; + initial_state.softmask = nullptr; + initial_state.group_depth = 0; + _state_stack.push_back(initial_state); + _node_stack.push_back(_container); + + _ttm[0] = 1; _ttm[1] = 0; _ttm[2] = 0; _ttm[3] = 1; _ttm[4] = 0; _ttm[5] = 0; + _ttm_is_set = false; +} + +void SvgBuilder::setDocumentSize(double width, double height) { + sp_repr_set_svg_double(_root, "width", width); + sp_repr_set_svg_double(_root, "height", height); + this->_width = width; + this->_height = height; +} + +/** + * \brief Sets groupmode of the current container to 'layer' and sets its label if given + */ +void SvgBuilder::setAsLayer(char *layer_name) { + _container->setAttribute("inkscape:groupmode", "layer"); + if (layer_name) { + _container->setAttribute("inkscape:label", layer_name); + } +} + +/** + * \brief Sets the current container's opacity + */ +void SvgBuilder::setGroupOpacity(double opacity) { + sp_repr_set_svg_double(_container, "opacity", CLAMP(opacity, 0.0, 1.0)); +} + +void SvgBuilder::saveState() { + SvgGraphicsState new_state; + new_state.group_depth = 0; + new_state.softmask = _state_stack.back().softmask; + _state_stack.push_back(new_state); + pushGroup(); +} + +void SvgBuilder::restoreState() { + while( _state_stack.back().group_depth > 0 ) { + popGroup(); + } + _state_stack.pop_back(); +} + +Inkscape::XML::Node *SvgBuilder::pushNode(const char *name) { + Inkscape::XML::Node *node = _xml_doc->createElement(name); + _node_stack.push_back(node); + _container = node; + return node; +} + +Inkscape::XML::Node *SvgBuilder::popNode() { + Inkscape::XML::Node *node = nullptr; + if ( _node_stack.size() > 1 ) { + node = _node_stack.back(); + _node_stack.pop_back(); + _container = _node_stack.back(); // Re-set container + } else { + TRACE(("popNode() called when stack is empty\n")); + node = _root; + } + return node; +} + +Inkscape::XML::Node *SvgBuilder::pushGroup() { + Inkscape::XML::Node *saved_container = _container; + Inkscape::XML::Node *node = pushNode("svg:g"); + saved_container->appendChild(node); + Inkscape::GC::release(node); + _state_stack.back().group_depth++; + // Set as a layer if this is a top-level group + if ( _container->parent() == _root && _is_top_level ) { + static int layer_count = 1; + if ( layer_count > 1 ) { + gchar *layer_name = g_strdup_printf("%s%d", _docname, layer_count); + setAsLayer(layer_name); + g_free(layer_name); + } else { + setAsLayer(_docname); + } + } + if (_container->parent()->attribute("inkscape:groupmode") != nullptr) { + _ttm[0] = _ttm[3] = 1.0; // clear ttm if parent is a layer + _ttm[1] = _ttm[2] = _ttm[4] = _ttm[5] = 0.0; + _ttm_is_set = false; + } + return _container; +} + +Inkscape::XML::Node *SvgBuilder::popGroup() { + if (_container != _root) { // Pop if the current container isn't root + popNode(); + _state_stack.back().group_depth--; + } + + return _container; +} + +Inkscape::XML::Node *SvgBuilder::getContainer() { + return _container; +} + +static gchar *svgConvertRGBToText(double r, double g, double b) { + using Inkscape::Filters::clamp; + static gchar tmp[1023] = {0}; + snprintf(tmp, 1023, + "#%02x%02x%02x", + clamp(SP_COLOR_F_TO_U(r)), + clamp(SP_COLOR_F_TO_U(g)), + clamp(SP_COLOR_F_TO_U(b))); + return (gchar *)&tmp; +} + +static gchar *svgConvertGfxRGB(GfxRGB *color) { + double r = (double)color->r / 65535.0; + double g = (double)color->g / 65535.0; + double b = (double)color->b / 65535.0; + return svgConvertRGBToText(r, g, b); +} + +static void svgSetTransform(Inkscape::XML::Node *node, double c0, double c1, + double c2, double c3, double c4, double c5) { + Geom::Affine matrix(c0, c1, c2, c3, c4, c5); + gchar *transform_text = sp_svg_transform_write(matrix); + node->setAttribute("transform", transform_text); + g_free(transform_text); +} + +/** + * \brief Generates a SVG path string from poppler's data structure + */ +static gchar *svgInterpretPath(_POPPLER_CONST_83 GfxPath *path) { + Inkscape::SVG::PathString pathString; + for (int i = 0 ; i < path->getNumSubpaths() ; ++i ) { + _POPPLER_CONST_83 GfxSubpath *subpath = path->getSubpath(i); + if (subpath->getNumPoints() > 0) { + pathString.moveTo(subpath->getX(0), subpath->getY(0)); + int j = 1; + while (j < subpath->getNumPoints()) { + if (subpath->getCurve(j)) { + pathString.curveTo(subpath->getX(j), subpath->getY(j), + subpath->getX(j+1), subpath->getY(j+1), + subpath->getX(j+2), subpath->getY(j+2)); + + j += 3; + } else { + pathString.lineTo(subpath->getX(j), subpath->getY(j)); + ++j; + } + } + if (subpath->isClosed()) { + pathString.closePath(); + } + } + } + + return g_strdup(pathString.c_str()); +} + +/** + * \brief Sets stroke style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties + */ +void SvgBuilder::_setStrokeStyle(SPCSSAttr *css, GfxState *state) { + // Stroke color/pattern + if ( state->getStrokeColorSpace()->getMode() == csPattern ) { + gchar *urltext = _createPattern(state->getStrokePattern(), state, true); + sp_repr_css_set_property(css, "stroke", urltext); + if (urltext) { + g_free(urltext); + } + } else { + GfxRGB stroke_color; + state->getStrokeRGB(&stroke_color); + sp_repr_css_set_property(css, "stroke", svgConvertGfxRGB(&stroke_color)); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getStrokeOpacity(); + sp_repr_css_set_property(css, "stroke-opacity", os_opacity.str().c_str()); + + // Line width + Inkscape::CSSOStringStream os_width; + double lw = state->getLineWidth(); + if (lw > 0.0) { + os_width << lw; + } else { + // emit a stroke which is 1px in toplevel user units + double pxw = Inkscape::Util::Quantity::convert(1.0, "pt", "px"); + os_width << 1.0 / state->transformWidth(pxw); + } + sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str()); + + // Line cap + switch (state->getLineCap()) { + case 0: + sp_repr_css_set_property(css, "stroke-linecap", "butt"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linecap", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linecap", "square"); + break; + } + + // Line join + switch (state->getLineJoin()) { + case 0: + sp_repr_css_set_property(css, "stroke-linejoin", "miter"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linejoin", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linejoin", "bevel"); + break; + } + + // Miterlimit + Inkscape::CSSOStringStream os_ml; + os_ml << state->getMiterLimit(); + sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str()); + + // Line dash + double *dash_pattern; + int dash_length; + double dash_start; + state->getLineDash(&dash_pattern, &dash_length, &dash_start); + if ( dash_length > 0 ) { + Inkscape::CSSOStringStream os_array; + for ( int i = 0 ; i < dash_length ; i++ ) { + os_array << dash_pattern[i]; + if (i < (dash_length - 1)) { + os_array << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", os_array.str().c_str()); + + Inkscape::CSSOStringStream os_offset; + os_offset << dash_start; + sp_repr_css_set_property(css, "stroke-dashoffset", os_offset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +/** + * \brief Sets fill style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties. + */ +void SvgBuilder::_setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd) { + + // Fill color/pattern + if ( state->getFillColorSpace()->getMode() == csPattern ) { + gchar *urltext = _createPattern(state->getFillPattern(), state); + sp_repr_css_set_property(css, "fill", urltext); + if (urltext) { + g_free(urltext); + } + } else { + GfxRGB fill_color; + state->getFillRGB(&fill_color); + sp_repr_css_set_property(css, "fill", svgConvertGfxRGB(&fill_color)); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getFillOpacity(); + sp_repr_css_set_property(css, "fill-opacity", os_opacity.str().c_str()); + + // Fill rule + sp_repr_css_set_property(css, "fill-rule", even_odd ? "evenodd" : "nonzero"); +} + +/** + * \brief Sets blend style properties from poppler's GfxState data structure + * \update a SPCSSAttr with all mix-blend-mode set + */ +void SvgBuilder::_setBlendMode(Inkscape::XML::Node *node, GfxState *state) +{ + SPCSSAttr *css = sp_repr_css_attr(node, "style"); + GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(css, "mix-blend-mode", enum_blend_mode[blendmode].key); + } + Glib::ustring value; + sp_repr_css_write_string(css, value); + node->setAttributeOrRemoveIfEmpty("style", value); + sp_repr_css_attr_unref(css); +} +/** + * \brief Sets style properties from poppler's GfxState data structure + * \return SPCSSAttr with all the relevant properties set + */ +SPCSSAttr *SvgBuilder::_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (fill) { + _setFillStyle(css, state, even_odd); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + + if (stroke) { + _setStrokeStyle(css, state); + } else { + sp_repr_css_set_property(css, "stroke", "none"); + } + + return css; +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * Can be used to do filling and stroking at once. + * + * \param fill whether the path should be filled + * \param stroke whether the path should be stroked + * \param even_odd whether the even-odd rule should be used when filling the path + */ +void SvgBuilder::addPath(GfxState *state, bool fill, bool stroke, bool even_odd) { + Inkscape::XML::Node *path = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(state->getPath()); + path->setAttribute("d", pathtext); + g_free(pathtext); + + // Set style + SPCSSAttr *css = _setStyle(state, fill, stroke, even_odd); + sp_repr_css_change(path, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(path, state); + _container->appendChild(path); + Inkscape::GC::release(path); +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * The path is set to be filled with the given shading. + */ +void SvgBuilder::addShadedFill(GfxShading *shading, double *matrix, GfxPath *path, + bool even_odd) { + + Inkscape::XML::Node *path_node = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(path); + path_node->setAttribute("d", pathtext); + g_free(pathtext); + + // Set style + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar *id = _createGradient(shading, matrix, true); + if (id) { + gchar *urltext = g_strdup_printf ("url(#%s)", id); + sp_repr_css_set_property(css, "fill", urltext); + g_free(urltext); + g_free(id); + } else { + sp_repr_css_attr_unref(css); + Inkscape::GC::release(path_node); + return; + } + if (even_odd) { + sp_repr_css_set_property(css, "fill-rule", "evenodd"); + } + sp_repr_css_set_property(css, "stroke", "none"); + sp_repr_css_change(path_node, css, "style"); + sp_repr_css_attr_unref(css); + + _container->appendChild(path_node); + Inkscape::GC::release(path_node); + + // Remove the clipping path emitted before the 'sh' operator + int up_walk = 0; + Inkscape::XML::Node *node = _container->parent(); + while( node && node->childCount() == 1 && up_walk < 3 ) { + gchar const *clip_path_url = node->attribute("clip-path"); + if (clip_path_url) { + // Obtain clipping path's id from the URL + gchar clip_path_id[32]; + strncpy(clip_path_id, clip_path_url + 5, strlen(clip_path_url) - 6); + clip_path_id[sizeof (clip_path_id) - 1] = '\0'; + SPObject *clip_obj = _doc->getObjectById(clip_path_id); + if (clip_obj) { + clip_obj->deleteObject(); + node->removeAttribute("clip-path"); + TRACE(("removed clipping path: %s\n", clip_path_id)); + } + break; + } + node = node->parent(); + up_walk++; + } +} + +/** + * \brief Clips to the current path set in GfxState + * \param state poppler's data structure + * \param even_odd whether the even-odd rule should be applied + */ +void SvgBuilder::clip(GfxState *state, bool even_odd) { + pushGroup(); + setClipPath(state, even_odd); +} + +void SvgBuilder::setClipPath(GfxState *state, bool even_odd) { + // Create the clipPath repr + Inkscape::XML::Node *clip_path = _xml_doc->createElement("svg:clipPath"); + clip_path->setAttribute("clipPathUnits", "userSpaceOnUse"); + // Create the path + Inkscape::XML::Node *path = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(state->getPath()); + path->setAttribute("d", pathtext); + g_free(pathtext); + if (even_odd) { + path->setAttribute("clip-rule", "evenodd"); + } + clip_path->appendChild(path); + Inkscape::GC::release(path); + // Append clipPath to defs and get id + _doc->getDefs()->getRepr()->appendChild(clip_path); + gchar *urltext = g_strdup_printf ("url(#%s)", clip_path->attribute("id")); + Inkscape::GC::release(clip_path); + _container->setAttribute("clip-path", urltext); + g_free(urltext); +} + +/** + * \brief Fills the given array with the current container's transform, if set + * \param transform array of doubles to be filled + * \return true on success; false on invalid transformation + */ +bool SvgBuilder::getTransform(double *transform) { + Geom::Affine svd; + gchar const *tr = _container->attribute("transform"); + bool valid = sp_svg_transform_read(tr, &svd); + if (valid) { + for ( int i = 0 ; i < 6 ; i++ ) { + transform[i] = svd[i]; + } + return true; + } else { + return false; + } +} + +/** + * \brief Sets the transformation matrix of the current container + */ +void SvgBuilder::setTransform(double c0, double c1, double c2, double c3, + double c4, double c5) { + // do not remember the group which is a layer + if ((_container->attribute("inkscape:groupmode") == nullptr) && !_ttm_is_set) { + _ttm[0] = c0; + _ttm[1] = c1; + _ttm[2] = c2; + _ttm[3] = c3; + _ttm[4] = c4; + _ttm[5] = c5; + _ttm_is_set = true; + } + + // Avoid transforming a group with an already set clip-path + if ( _container->attribute("clip-path") != nullptr ) { + pushGroup(); + } + TRACE(("setTransform: %f %f %f %f %f %f\n", c0, c1, c2, c3, c4, c5)); + svgSetTransform(_container, c0, c1, c2, c3, c4, c5); +} + +void SvgBuilder::setTransform(double const *transform) { + setTransform(transform[0], transform[1], transform[2], transform[3], + transform[4], transform[5]); +} + +/** + * \brief Checks whether the given pattern type can be represented in SVG + * Used by PdfParser to decide when to do fallback operations. + */ +bool SvgBuilder::isPatternTypeSupported(GfxPattern *pattern) { + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // shading pattern + GfxShading *shading = (static_cast(pattern))->getShading(); + int shadingType = shading->getType(); + if ( shadingType == 2 || // axial shading + shadingType == 3 ) { // radial shading + return true; + } + return false; + } else if ( pattern->getType() == 1 ) { // tiling pattern + return true; + } + } + + return false; +} + +/** + * \brief Creates a pattern from poppler's data structure + * Handles linear and radial gradients. Creates a new PdfParser and uses it to + * build a tiling pattern. + * \return a url pointing to the created pattern + */ +gchar *SvgBuilder::_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke) { + gchar *id = nullptr; + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // Shading pattern + GfxShadingPattern *shading_pattern = static_cast(pattern); + const double *ptm; + double m[6] = {1, 0, 0, 1, 0, 0}; + double det; + + // construct a (pattern space) -> (current space) transform matrix + + ptm = shading_pattern->getMatrix(); + det = _ttm[0] * _ttm[3] - _ttm[1] * _ttm[2]; + if (det) { + double ittm[6]; // invert ttm + ittm[0] = _ttm[3] / det; + ittm[1] = -_ttm[1] / det; + ittm[2] = -_ttm[2] / det; + ittm[3] = _ttm[0] / det; + ittm[4] = (_ttm[2] * _ttm[5] - _ttm[3] * _ttm[4]) / det; + ittm[5] = (_ttm[1] * _ttm[4] - _ttm[0] * _ttm[5]) / det; + m[0] = ptm[0] * ittm[0] + ptm[1] * ittm[2]; + m[1] = ptm[0] * ittm[1] + ptm[1] * ittm[3]; + m[2] = ptm[2] * ittm[0] + ptm[3] * ittm[2]; + m[3] = ptm[2] * ittm[1] + ptm[3] * ittm[3]; + m[4] = ptm[4] * ittm[0] + ptm[5] * ittm[2] + ittm[4]; + m[5] = ptm[4] * ittm[1] + ptm[5] * ittm[3] + ittm[5]; + } + id = _createGradient(shading_pattern->getShading(), + m, + !is_stroke); + } else if ( pattern->getType() == 1 ) { // Tiling pattern + id = _createTilingPattern(static_cast(pattern), state, is_stroke); + } + } else { + return nullptr; + } + gchar *urltext = g_strdup_printf ("url(#%s)", id); + g_free(id); + return urltext; +} + +/** + * \brief Creates a tiling pattern from poppler's data structure + * Creates a sub-page PdfParser and uses it to parse the pattern's content stream. + * \return id of the created pattern + */ +gchar *SvgBuilder::_createTilingPattern(GfxTilingPattern *tiling_pattern, + GfxState *state, bool is_stroke) { + + Inkscape::XML::Node *pattern_node = _xml_doc->createElement("svg:pattern"); + // Set pattern transform matrix + const double *p2u = tiling_pattern->getMatrix(); + double m[6] = {1, 0, 0, 1, 0, 0}; + double det; + det = _ttm[0] * _ttm[3] - _ttm[1] * _ttm[2]; // see LP Bug 1168908 + if (det) { + double ittm[6]; // invert ttm + ittm[0] = _ttm[3] / det; + ittm[1] = -_ttm[1] / det; + ittm[2] = -_ttm[2] / det; + ittm[3] = _ttm[0] / det; + ittm[4] = (_ttm[2] * _ttm[5] - _ttm[3] * _ttm[4]) / det; + ittm[5] = (_ttm[1] * _ttm[4] - _ttm[0] * _ttm[5]) / det; + m[0] = p2u[0] * ittm[0] + p2u[1] * ittm[2]; + m[1] = p2u[0] * ittm[1] + p2u[1] * ittm[3]; + m[2] = p2u[2] * ittm[0] + p2u[3] * ittm[2]; + m[3] = p2u[2] * ittm[1] + p2u[3] * ittm[3]; + m[4] = p2u[4] * ittm[0] + p2u[5] * ittm[2] + ittm[4]; + m[5] = p2u[4] * ittm[1] + p2u[5] * ittm[3] + ittm[5]; + } + Geom::Affine pat_matrix(m[0], m[1], m[2], m[3], m[4], m[5]); + gchar *transform_text = sp_svg_transform_write(pat_matrix); + pattern_node->setAttribute("patternTransform", transform_text); + g_free(transform_text); + pattern_node->setAttribute("patternUnits", "userSpaceOnUse"); + // Set pattern tiling + // FIXME: don't ignore XStep and YStep + const double *bbox = tiling_pattern->getBBox(); + sp_repr_set_svg_double(pattern_node, "x", 0.0); + sp_repr_set_svg_double(pattern_node, "y", 0.0); + sp_repr_set_svg_double(pattern_node, "width", bbox[2] - bbox[0]); + sp_repr_set_svg_double(pattern_node, "height", bbox[3] - bbox[1]); + + // Convert BBox for PdfParser + PDFRectangle box; + box.x1 = bbox[0]; + box.y1 = bbox[1]; + box.x2 = bbox[2]; + box.y2 = bbox[3]; + // Create new SvgBuilder and sub-page PdfParser + SvgBuilder *pattern_builder = new SvgBuilder(this, pattern_node); + PdfParser *pdf_parser = new PdfParser(_xref, pattern_builder, tiling_pattern->getResDict(), + &box); + // Get pattern color space + GfxPatternColorSpace *pat_cs = (GfxPatternColorSpace *)( is_stroke ? state->getStrokeColorSpace() + : state->getFillColorSpace() ); + // Set fill/stroke colors if this is an uncolored tiling pattern + GfxColorSpace *cs = nullptr; + if ( tiling_pattern->getPaintType() == 2 && ( cs = pat_cs->getUnder() ) ) { + GfxState *pattern_state = pdf_parser->getState(); + pattern_state->setFillColorSpace(cs->copy()); + pattern_state->setFillColor(state->getFillColor()); + pattern_state->setStrokeColorSpace(cs->copy()); + pattern_state->setStrokeColor(state->getFillColor()); + } + + // Generate the SVG pattern + pdf_parser->parse(tiling_pattern->getContentStream()); + + // Cleanup + delete pdf_parser; + delete pattern_builder; + + // Append the pattern to defs + _doc->getDefs()->getRepr()->appendChild(pattern_node); + gchar *id = g_strdup(pattern_node->attribute("id")); + Inkscape::GC::release(pattern_node); + + return id; +} + +/** + * \brief Creates a linear or radial gradient from poppler's data structure + * \param shading poppler's data structure for the shading + * \param matrix gradient transformation, can be null + * \param for_shading true if we're creating this for a shading operator; false otherwise + * \return id of the created object + */ +gchar *SvgBuilder::_createGradient(GfxShading *shading, double *matrix, bool for_shading) { + Inkscape::XML::Node *gradient; + _POPPLER_CONST Function *func; + int num_funcs; + bool extend0, extend1; + + if ( shading->getType() == 2 ) { // Axial shading + gradient = _xml_doc->createElement("svg:linearGradient"); + GfxAxialShading *axial_shading = static_cast(shading); + double x1, y1, x2, y2; + axial_shading->getCoords(&x1, &y1, &x2, &y2); + sp_repr_set_svg_double(gradient, "x1", x1); + sp_repr_set_svg_double(gradient, "y1", y1); + sp_repr_set_svg_double(gradient, "x2", x2); + sp_repr_set_svg_double(gradient, "y2", y2); + extend0 = axial_shading->getExtend0(); + extend1 = axial_shading->getExtend1(); + num_funcs = axial_shading->getNFuncs(); + func = axial_shading->getFunc(0); + } else if (shading->getType() == 3) { // Radial shading + gradient = _xml_doc->createElement("svg:radialGradient"); + GfxRadialShading *radial_shading = static_cast(shading); + double x1, y1, r1, x2, y2, r2; + radial_shading->getCoords(&x1, &y1, &r1, &x2, &y2, &r2); + // FIXME: the inner circle's radius is ignored here + sp_repr_set_svg_double(gradient, "fx", x1); + sp_repr_set_svg_double(gradient, "fy", y1); + sp_repr_set_svg_double(gradient, "cx", x2); + sp_repr_set_svg_double(gradient, "cy", y2); + sp_repr_set_svg_double(gradient, "r", r2); + extend0 = radial_shading->getExtend0(); + extend1 = radial_shading->getExtend1(); + num_funcs = radial_shading->getNFuncs(); + func = radial_shading->getFunc(0); + } else { // Unsupported shading type + return nullptr; + } + gradient->setAttribute("gradientUnits", "userSpaceOnUse"); + // If needed, flip the gradient transform around the y axis + if (matrix) { + Geom::Affine pat_matrix(matrix[0], matrix[1], matrix[2], matrix[3], + matrix[4], matrix[5]); + if ( !for_shading && _is_top_level ) { + Geom::Affine flip(1.0, 0.0, 0.0, -1.0, 0.0, Inkscape::Util::Quantity::convert(_height, "px", "pt")); + pat_matrix *= flip; + } + gchar *transform_text = sp_svg_transform_write(pat_matrix); + gradient->setAttribute("gradientTransform", transform_text); + g_free(transform_text); + } + + if ( extend0 && extend1 ) { + gradient->setAttribute("spreadMethod", "pad"); + } + + if ( num_funcs > 1 || !_addGradientStops(gradient, shading, func) ) { + Inkscape::GC::release(gradient); + return nullptr; + } + + Inkscape::XML::Node *defs = _doc->getDefs()->getRepr(); + defs->appendChild(gradient); + gchar *id = g_strdup(gradient->attribute("id")); + Inkscape::GC::release(gradient); + + return id; +} + +#define EPSILON 0.0001 +/** + * \brief Adds a stop with the given properties to the gradient's representation + */ +void SvgBuilder::_addStopToGradient(Inkscape::XML::Node *gradient, double offset, + GfxRGB *color, double opacity) { + Inkscape::XML::Node *stop = _xml_doc->createElement("svg:stop"); + SPCSSAttr *css = sp_repr_css_attr_new(); + Inkscape::CSSOStringStream os_opacity; + gchar *color_text = nullptr; + if ( _transp_group_stack != nullptr && _transp_group_stack->for_softmask ) { + double gray = (double)color->r / 65535.0; + gray = CLAMP(gray, 0.0, 1.0); + os_opacity << gray; + color_text = (char*) "#ffffff"; + } else { + os_opacity << opacity; + color_text = svgConvertGfxRGB(color); + } + sp_repr_css_set_property(css, "stop-opacity", os_opacity.str().c_str()); + sp_repr_css_set_property(css, "stop-color", color_text); + + sp_repr_css_change(stop, css, "style"); + sp_repr_css_attr_unref(css); + sp_repr_set_css_double(stop, "offset", offset); + + gradient->appendChild(stop); + Inkscape::GC::release(stop); +} + +static bool svgGetShadingColorRGB(GfxShading *shading, double offset, GfxRGB *result) { + GfxColorSpace *color_space = shading->getColorSpace(); + GfxColor temp; + if ( shading->getType() == 2 ) { // Axial shading + (static_cast(shading))->getColor(offset, &temp); + } else if ( shading->getType() == 3 ) { // Radial shading + (static_cast(shading))->getColor(offset, &temp); + } else { + return false; + } + // Convert it to RGB + color_space->getRGB(&temp, result); + + return true; +} + +#define INT_EPSILON 8 +bool SvgBuilder::_addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func) { + int type = func->getType(); + if ( type == 0 || type == 2 ) { // Sampled or exponential function + GfxRGB stop1, stop2; + if ( !svgGetShadingColorRGB(shading, 0.0, &stop1) || + !svgGetShadingColorRGB(shading, 1.0, &stop2) ) { + return false; + } else { + _addStopToGradient(gradient, 0.0, &stop1, 1.0); + _addStopToGradient(gradient, 1.0, &stop2, 1.0); + } + } else if ( type == 3 ) { // Stitching + auto stitchingFunc = static_cast<_POPPLER_CONST StitchingFunction*>(func); + const double *bounds = stitchingFunc->getBounds(); + const double *encode = stitchingFunc->getEncode(); + int num_funcs = stitchingFunc->getNumFuncs(); + + // Add stops from all the stitched functions + GfxRGB prev_color, color; + svgGetShadingColorRGB(shading, bounds[0], &prev_color); + _addStopToGradient(gradient, bounds[0], &prev_color, 1.0); + for ( int i = 0 ; i < num_funcs ; i++ ) { + svgGetShadingColorRGB(shading, bounds[i + 1], &color); + // Add stops + if (stitchingFunc->getFunc(i)->getType() == 2) { // process exponential fxn + double expE = (static_cast<_POPPLER_CONST ExponentialFunction*>(stitchingFunc->getFunc(i)))->getE(); + if (expE > 1.0) { + expE = (bounds[i + 1] - bounds[i])/expE; // approximate exponential as a single straight line at x=1 + if (encode[2*i] == 0) { // normal sequence + _addStopToGradient(gradient, bounds[i + 1] - expE, &prev_color, 1.0); + } else { // reflected sequence + _addStopToGradient(gradient, bounds[i] + expE, &color, 1.0); + } + } + } + _addStopToGradient(gradient, bounds[i + 1], &color, 1.0); + prev_color = color; + } + } else { // Unsupported function type + return false; + } + + return true; +} + +/** + * \brief Sets _invalidated_style to true to indicate that styles have to be updated + * Used for text output when glyphs are buffered till a font change + */ +void SvgBuilder::updateStyle(GfxState *state) { + if (_in_text_object) { + _invalidated_style = true; + _current_state = state; + } +} + +/* + MatchingChars + Count for how many characters s1 matches sp taking into account + that a space in sp may be removed or replaced by some other tokens + specified in the code. (Bug LP #179589) +*/ +static size_t MatchingChars(std::string s1, std::string sp) +{ + size_t is = 0; + size_t ip = 0; + + while(is < s1.length() && ip < sp.length()) { + if (s1[is] == sp[ip]) { + is++; ip++; + } else if (sp[ip] == ' ') { + ip++; + if (s1[is] == '_') { // Valid matches to spaces in sp. + is++; + } + } else { + break; + } + } + return ip; +} + +/* + SvgBuilder::_BestMatchingFont + Scan the available fonts to find the font name that best matches PDFname. + (Bug LP #179589) +*/ +std::string SvgBuilder::_BestMatchingFont(std::string PDFname) +{ + double bestMatch = 0; + std::string bestFontname = "Arial"; + + for (auto fontname : _availableFontNames) { + // At least the first word of the font name should match. + size_t minMatch = fontname.find(" "); + if (minMatch == std::string::npos) { + minMatch = fontname.length(); + } + + size_t Match = MatchingChars(PDFname, fontname); + if (Match >= minMatch) { + double relMatch = (float)Match / (fontname.length() + PDFname.length()); + if (relMatch > bestMatch) { + bestMatch = relMatch; + bestFontname = fontname; + } + } + } + + if (bestMatch == 0) + return PDFname; + else + return bestFontname; +} + +/** + * This array holds info about translating font weight names to more or less CSS equivalents + */ +static char *font_weight_translator[][2] = { + {(char*) "bold", (char*) "bold"}, + {(char*) "light", (char*) "300"}, + {(char*) "black", (char*) "900"}, + {(char*) "heavy", (char*) "900"}, + {(char*) "ultrabold", (char*) "800"}, + {(char*) "extrabold", (char*) "800"}, + {(char*) "demibold", (char*) "600"}, + {(char*) "semibold", (char*) "600"}, + {(char*) "medium", (char*) "500"}, + {(char*) "book", (char*) "normal"}, + {(char*) "regular", (char*) "normal"}, + {(char*) "roman", (char*) "normal"}, + {(char*) "normal", (char*) "normal"}, + {(char*) "ultralight", (char*) "200"}, + {(char*) "extralight", (char*) "200"}, + {(char*) "thin", (char*) "100"} +}; + +/** + * \brief Updates _font_style according to the font set in parameter state + */ +void SvgBuilder::updateFont(GfxState *state) { + + TRACE(("updateFont()\n")); + _need_font_update = false; + updateTextMatrix(state); // Ensure that we have a text matrix built + + if (_font_style) { + //sp_repr_css_attr_unref(_font_style); + } + _font_style = sp_repr_css_attr_new(); + GfxFont *font = state->getFont(); + // Store original name + if (font->getName()) { + _font_specification = font->getName()->getCString(); + } else { + _font_specification = "Arial"; + } + + // Prune the font name to get the correct font family name + // In a PDF font names can look like this: IONIPB+MetaPlusBold-Italic + char *font_family = nullptr; + char *font_style = nullptr; + char *font_style_lowercase = nullptr; + const char *plus_sign = strstr(_font_specification, "+"); + if (plus_sign) { + font_family = g_strdup(plus_sign + 1); + _font_specification = plus_sign + 1; + } else { + font_family = g_strdup(_font_specification); + } + char *style_delim = nullptr; + if ( ( style_delim = g_strrstr(font_family, "-") ) || + ( style_delim = g_strrstr(font_family, ",") ) ) { + font_style = style_delim + 1; + font_style_lowercase = g_ascii_strdown(font_style, -1); + style_delim[0] = 0; + } + + // Font family + if (font->getFamily()) { // if font family is explicitly given use it. + sp_repr_css_set_property(_font_style, "font-family", font->getFamily()->getCString()); + } else { + int attr_value = 1; + sp_repr_get_int(_preferences, "localFonts", &attr_value); + if (attr_value != 0) { + // Find the font that best matches the stripped down (orig)name (Bug LP #179589). + sp_repr_css_set_property(_font_style, "font-family", _BestMatchingFont(font_family).c_str()); + } else { + sp_repr_css_set_property(_font_style, "font-family", font_family); + } + } + + // Font style + if (font->isItalic()) { + sp_repr_css_set_property(_font_style, "font-style", "italic"); + } else if (font_style) { + if ( strstr(font_style_lowercase, "italic") || + strstr(font_style_lowercase, "slanted") ) { + sp_repr_css_set_property(_font_style, "font-style", "italic"); + } else if (strstr(font_style_lowercase, "oblique")) { + sp_repr_css_set_property(_font_style, "font-style", "oblique"); + } + } + + // Font variant -- default 'normal' value + sp_repr_css_set_property(_font_style, "font-variant", "normal"); + + // Font weight + GfxFont::Weight font_weight = font->getWeight(); + char *css_font_weight = nullptr; + if ( font_weight != GfxFont::WeightNotDefined ) { + if ( font_weight == GfxFont::W400 ) { + css_font_weight = (char*) "normal"; + } else if ( font_weight == GfxFont::W700 ) { + css_font_weight = (char*) "bold"; + } else { + gchar weight_num[4] = "100"; + weight_num[0] = (gchar)( '1' + (font_weight - GfxFont::W100) ); + sp_repr_css_set_property(_font_style, "font-weight", (gchar *)&weight_num); + } + } else if (font_style) { + // Apply the font weight translations + int num_translations = sizeof(font_weight_translator) / ( 2 * sizeof(char *) ); + for ( int i = 0 ; i < num_translations ; i++ ) { + if (strstr(font_style_lowercase, font_weight_translator[i][0])) { + css_font_weight = font_weight_translator[i][1]; + } + } + } else { + css_font_weight = (char*) "normal"; + } + if (css_font_weight) { + sp_repr_css_set_property(_font_style, "font-weight", css_font_weight); + } + g_free(font_family); + if (font_style_lowercase) { + g_free(font_style_lowercase); + } + + // Font stretch + GfxFont::Stretch font_stretch = font->getStretch(); + gchar *stretch_value = nullptr; + switch (font_stretch) { + case GfxFont::UltraCondensed: + stretch_value = (char*) "ultra-condensed"; + break; + case GfxFont::ExtraCondensed: + stretch_value = (char*) "extra-condensed"; + break; + case GfxFont::Condensed: + stretch_value = (char*) "condensed"; + break; + case GfxFont::SemiCondensed: + stretch_value = (char*) "semi-condensed"; + break; + case GfxFont::Normal: + stretch_value = (char*) "normal"; + break; + case GfxFont::SemiExpanded: + stretch_value = (char*) "semi-expanded"; + break; + case GfxFont::Expanded: + stretch_value = (char*) "expanded"; + break; + case GfxFont::ExtraExpanded: + stretch_value = (char*) "extra-expanded"; + break; + case GfxFont::UltraExpanded: + stretch_value = (char*) "ultra-expanded"; + break; + default: + break; + } + if ( stretch_value != nullptr ) { + sp_repr_css_set_property(_font_style, "font-stretch", stretch_value); + } + + // Font size + Inkscape::CSSOStringStream os_font_size; + double css_font_size = _font_scaling * state->getFontSize(); + if ( font->getType() == fontType3 ) { + const double *font_matrix = font->getFontMatrix(); + if ( font_matrix[0] != 0.0 ) { + css_font_size *= font_matrix[3] / font_matrix[0]; + } + } + os_font_size << css_font_size; + sp_repr_css_set_property(_font_style, "font-size", os_font_size.str().c_str()); + + // Writing mode + if ( font->getWMode() == 0 ) { + sp_repr_css_set_property(_font_style, "writing-mode", "lr"); + } else { + sp_repr_css_set_property(_font_style, "writing-mode", "tb"); + } + + _current_font = font; + _invalidated_style = true; +} + +/** + * \brief Shifts the current text position by the given amount (specified in text space) + */ +void SvgBuilder::updateTextShift(GfxState *state, double shift) { + double shift_value = -shift * 0.001 * fabs(state->getFontSize()); + if (state->getFont()->getWMode()) { + _text_position[1] += shift_value; + } else { + _text_position[0] += shift_value; + } +} + +/** + * \brief Updates current text position + */ +void SvgBuilder::updateTextPosition(double tx, double ty) { + Geom::Point new_position(tx, ty); + _text_position = new_position; +} + +/** + * \brief Flushes the buffered characters + */ +void SvgBuilder::updateTextMatrix(GfxState *state) { + _flushText(); + // Update text matrix + const double *text_matrix = state->getTextMat(); + double w_scale = sqrt( text_matrix[0] * text_matrix[0] + text_matrix[2] * text_matrix[2] ); + double h_scale = sqrt( text_matrix[1] * text_matrix[1] + text_matrix[3] * text_matrix[3] ); + double max_scale; + if ( w_scale > h_scale ) { + max_scale = w_scale; + } else { + max_scale = h_scale; + } + // Calculate new text matrix + Geom::Affine new_text_matrix(text_matrix[0] * state->getHorizScaling(), + text_matrix[1] * state->getHorizScaling(), + -text_matrix[2], -text_matrix[3], + 0.0, 0.0); + + if ( fabs( max_scale - 1.0 ) > EPSILON ) { + // Cancel out scaling by font size in text matrix + for ( int i = 0 ; i < 4 ; i++ ) { + new_text_matrix[i] /= max_scale; + } + } + _text_matrix = new_text_matrix; + _font_scaling = max_scale; +} + +/** + * \brief Writes the buffered characters to the SVG document + */ +void SvgBuilder::_flushText() { + // Ignore empty strings + if ( _glyphs.empty()) { + _glyphs.clear(); + return; + } + std::vector::iterator i = _glyphs.begin(); + const SvgGlyph& first_glyph = (*i); + int render_mode = first_glyph.render_mode; + // Ignore invisible characters + if ( render_mode == 3 ) { + _glyphs.clear(); + return; + } + + Inkscape::XML::Node *text_node = _xml_doc->createElement("svg:text"); + // Set text matrix + Geom::Affine text_transform(_text_matrix); + text_transform[4] = first_glyph.position[0]; + text_transform[5] = first_glyph.position[1]; + gchar *transform = sp_svg_transform_write(text_transform); + text_node->setAttribute("transform", transform); + g_free(transform); + + bool new_tspan = true; + bool same_coords[2] = {true, true}; + Geom::Point last_delta_pos; + unsigned int glyphs_in_a_row = 0; + Inkscape::XML::Node *tspan_node = nullptr; + Glib::ustring x_coords; + Glib::ustring y_coords; + Glib::ustring text_buffer; + + // Output all buffered glyphs + while (true) { + const SvgGlyph& glyph = (*i); + std::vector::iterator prev_iterator = i - 1; + // Check if we need to make a new tspan + if (glyph.style_changed) { + new_tspan = true; + } else if ( i != _glyphs.begin() ) { + const SvgGlyph& prev_glyph = (*prev_iterator); + if ( !( ( glyph.dy == 0.0 && prev_glyph.dy == 0.0 && + glyph.text_position[1] == prev_glyph.text_position[1] ) || + ( glyph.dx == 0.0 && prev_glyph.dx == 0.0 && + glyph.text_position[0] == prev_glyph.text_position[0] ) ) ) { + new_tspan = true; + } + } + + // Create tspan node if needed + if ( new_tspan || i == _glyphs.end() ) { + if (tspan_node) { + // Set the x and y coordinate arrays + if ( same_coords[0] ) { + sp_repr_set_svg_double(tspan_node, "x", last_delta_pos[0]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("x", x_coords); + } + if ( same_coords[1] ) { + sp_repr_set_svg_double(tspan_node, "y", last_delta_pos[1]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("y", y_coords); + } + TRACE(("tspan content: %s\n", text_buffer.c_str())); + if ( glyphs_in_a_row > 1 ) { + tspan_node->setAttribute("sodipodi:role", "line"); + } + // Add text content node to tspan + Inkscape::XML::Node *text_content = _xml_doc->createTextNode(text_buffer.c_str()); + tspan_node->appendChild(text_content); + Inkscape::GC::release(text_content); + text_node->appendChild(tspan_node); + // Clear temporary buffers + x_coords.clear(); + y_coords.clear(); + text_buffer.clear(); + Inkscape::GC::release(tspan_node); + glyphs_in_a_row = 0; + } + if ( i == _glyphs.end() ) { + sp_repr_css_attr_unref((*prev_iterator).style); + break; + } else { + tspan_node = _xml_doc->createElement("svg:tspan"); + + /////// + // Create a font specification string and save the attribute in the style + PangoFontDescription *descr = pango_font_description_from_string(glyph.font_specification); + Glib::ustring properFontSpec = font_factory::Default()->ConstructFontSpecification(descr); + pango_font_description_free(descr); + sp_repr_css_set_property(glyph.style, "-inkscape-font-specification", properFontSpec.c_str()); + + // Set style and unref SPCSSAttr if it won't be needed anymore + // assume all nodes in a node share the same style + sp_repr_css_change(text_node, glyph.style, "style"); + if ( glyph.style_changed && i != _glyphs.begin() ) { // Free previous style + sp_repr_css_attr_unref((*prev_iterator).style); + } + } + new_tspan = false; + } + if ( glyphs_in_a_row > 0 ) { + x_coords.append(" "); + y_coords.append(" "); + // Check if we have the same coordinates + const SvgGlyph& prev_glyph = (*prev_iterator); + for ( int p = 0 ; p < 2 ; p++ ) { + if ( glyph.text_position[p] != prev_glyph.text_position[p] ) { + same_coords[p] = false; + } + } + } + // Append the coordinates to their respective strings + Geom::Point delta_pos( glyph.text_position - first_glyph.text_position ); + delta_pos[1] += glyph.rise; + delta_pos[1] *= -1.0; // flip it + delta_pos *= _font_scaling; + Inkscape::CSSOStringStream os_x; + os_x << delta_pos[0]; + x_coords.append(os_x.str()); + Inkscape::CSSOStringStream os_y; + os_y << delta_pos[1]; + y_coords.append(os_y.str()); + last_delta_pos = delta_pos; + + // Append the character to the text buffer + if ( !glyph.code.empty() ) { + text_buffer.append(1, glyph.code[0]); + } + + glyphs_in_a_row++; + ++i; + } + _container->appendChild(text_node); + Inkscape::GC::release(text_node); + + _glyphs.clear(); +} + +void SvgBuilder::beginString(GfxState *state) { + if (_need_font_update) { + updateFont(state); + } + IFTRACE(double *m = state->getTextMat()); + TRACE(("tm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); + IFTRACE(m = state->getCTM()); + TRACE(("ctm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); +} + +/** + * \brief Adds the specified character to the text buffer + * Takes care of converting it to UTF-8 and generates a new style repr if style + * has changed since the last call. + */ +void SvgBuilder::addChar(GfxState *state, double x, double y, + double dx, double dy, + double originX, double originY, + CharCode /*code*/, int /*nBytes*/, Unicode const *u, int uLen) { + + + bool is_space = ( uLen == 1 && u[0] == 32 ); + // Skip beginning space + if ( is_space && _glyphs.empty()) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + // Allow only one space in a row + if ( is_space && (_glyphs[_glyphs.size() - 1].code.size() == 1) && + (_glyphs[_glyphs.size() - 1].code[0] == 32) ) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + + SvgGlyph new_glyph; + new_glyph.is_space = is_space; + new_glyph.position = Geom::Point( x - originX, y - originY ); + new_glyph.text_position = _text_position; + new_glyph.dx = dx; + new_glyph.dy = dy; + Geom::Point delta(dx, dy); + _text_position += delta; + + // Convert the character to UTF-8 since that's our SVG document's encoding + { + gunichar2 uu[8] = {0}; + + for (int i = 0; i < uLen; i++) { + uu[i] = u[i]; + } + + gchar *tmp = g_utf16_to_utf8(uu, uLen, nullptr, nullptr, nullptr); + if ( tmp && *tmp ) { + new_glyph.code = tmp; + } else { + new_glyph.code.clear(); + } + g_free(tmp); + } + + // Copy current style if it has changed since the previous glyph + if (_invalidated_style || _glyphs.empty()) { + new_glyph.style_changed = true; + int render_mode = state->getRender(); + // Set style + bool has_fill = !( render_mode & 1 ); + bool has_stroke = ( render_mode & 3 ) == 1 || ( render_mode & 3 ) == 2; + new_glyph.style = _setStyle(state, has_fill, has_stroke); + // Find a way to handle blend modes on text + /* GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(new_glyph.style, "mix-blend-mode", enum_blend_mode[blendmode].key); + } */ + new_glyph.render_mode = render_mode; + sp_repr_css_merge(new_glyph.style, _font_style); // Merge with font style + _invalidated_style = false; + } else { + new_glyph.style_changed = false; + // Point to previous glyph's style information + const SvgGlyph& prev_glyph = _glyphs.back(); + new_glyph.style = prev_glyph.style; + /* GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(new_glyph.style, "mix-blend-mode", enum_blend_mode[blendmode].key); + } */ + new_glyph.render_mode = prev_glyph.render_mode; + } + new_glyph.font_specification = _font_specification; + new_glyph.rise = state->getRise(); + + _glyphs.push_back(new_glyph); +} + +void SvgBuilder::endString(GfxState * /*state*/) { +} + +void SvgBuilder::beginTextObject(GfxState *state) { + _in_text_object = true; + _invalidated_style = true; // Force copying of current state + _current_state = state; +} + +void SvgBuilder::endTextObject(GfxState * /*state*/) { + _flushText(); + // TODO: clip if render_mode >= 4 + _in_text_object = false; +} + +/** + * Helper functions for supporting direct PNG output into a base64 encoded stream + */ +void png_write_vector(png_structp png_ptr, png_bytep data, png_size_t length) +{ + auto *v_ptr = reinterpret_cast *>(png_get_io_ptr(png_ptr)); // Get pointer to stream + for ( unsigned i = 0 ; i < length ; i++ ) { + v_ptr->push_back(data[i]); + } +} + +/** + * \brief Creates an element containing the given ImageStream as a PNG + * + */ +Inkscape::XML::Node *SvgBuilder::_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only, + bool invert_alpha) { + + // Create PNG write struct + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if ( png_ptr == nullptr ) { + return nullptr; + } + // Create PNG info struct + png_infop info_ptr = png_create_info_struct(png_ptr); + if ( info_ptr == nullptr ) { + png_destroy_write_struct(&png_ptr, nullptr); + return nullptr; + } + // Set error handler + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_write_struct(&png_ptr, &info_ptr); + return nullptr; + } + // Decide whether we should embed this image + int attr_value = 1; + sp_repr_get_int(_preferences, "embedImages", &attr_value); + bool embed_image = ( attr_value != 0 ); + // Set read/write functions + std::vector png_buffer; + FILE *fp = nullptr; + gchar *file_name = nullptr; + if (embed_image) { + png_set_write_fn(png_ptr, &png_buffer, png_write_vector, nullptr); + } else { + static int counter = 0; + file_name = g_strdup_printf("%s_img%d.png", _docname, counter++); + fp = fopen(file_name, "wb"); + if ( fp == nullptr ) { + png_destroy_write_struct(&png_ptr, &info_ptr); + g_free(file_name); + return nullptr; + } + png_init_io(png_ptr, fp); + } + + // Set header data + if ( !invert_alpha && !alpha_only ) { + png_set_invert_alpha(png_ptr); + } + png_color_8 sig_bit; + if (alpha_only) { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_GRAY, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 0; + sig_bit.green = 0; + sig_bit.blue = 0; + sig_bit.gray = 8; + sig_bit.alpha = 0; + } else { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + sig_bit.alpha = 8; + } + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + png_set_bgr(png_ptr); + // Write the file header + png_write_info(png_ptr, info_ptr); + + // Convert pixels + ImageStream *image_stream; + if (alpha_only) { + if (color_map) { + image_stream = new ImageStream(str, width, color_map->getNumPixelComps(), + color_map->getBits()); + } else { + image_stream = new ImageStream(str, width, 1, 1); + } + image_stream->reset(); + + // Convert grayscale values + unsigned char *buffer = new unsigned char[width]; + int invert_bit = invert_alpha ? 1 : 0; + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + if (color_map) { + color_map->getGrayLine(row, buffer, width); + } else { + unsigned char *buf_ptr = buffer; + for ( int x = 0 ; x < width ; x++ ) { + if ( row[x] ^ invert_bit ) { + *buf_ptr++ = 0; + } else { + *buf_ptr++ = 255; + } + } + } + png_write_row(png_ptr, (png_bytep)buffer); + } + delete [] buffer; + } else if (color_map) { + image_stream = new ImageStream(str, width, + color_map->getNumPixelComps(), + color_map->getBits()); + image_stream->reset(); + + // Convert RGB values + unsigned int *buffer = new unsigned int[width]; + if (mask_colors) { + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + color_map->getRGBLine(row, buffer, width); + + unsigned int *dest = buffer; + for ( int x = 0 ; x < width ; x++ ) { + // Check each color component against the mask + for ( int i = 0; i < color_map->getNumPixelComps() ; i++) { + if ( row[i] < mask_colors[2*i] * 255 || + row[i] > mask_colors[2*i + 1] * 255 ) { + *dest = *dest | 0xff000000; + break; + } + } + // Advance to the next pixel + row += color_map->getNumPixelComps(); + dest++; + } + // Write it to the PNG + png_write_row(png_ptr, (png_bytep)buffer); + } + } else { + for ( int i = 0 ; i < height ; i++ ) { + unsigned char *row = image_stream->getLine(); + memset((void*)buffer, 0xff, sizeof(int) * width); + color_map->getRGBLine(row, buffer, width); + png_write_row(png_ptr, (png_bytep)buffer); + } + } + delete [] buffer; + + } else { // A colormap must be provided, so quit + png_destroy_write_struct(&png_ptr, &info_ptr); + if (!embed_image) { + fclose(fp); + g_free(file_name); + } + return nullptr; + } + delete image_stream; + str->close(); + // Close PNG + png_write_end(png_ptr, info_ptr); + png_destroy_write_struct(&png_ptr, &info_ptr); + + // Create repr + Inkscape::XML::Node *image_node = _xml_doc->createElement("svg:image"); + sp_repr_set_svg_double(image_node, "width", 1); + sp_repr_set_svg_double(image_node, "height", 1); + if( !interpolate ) { + SPCSSAttr *css = sp_repr_css_attr_new(); + // This should be changed after CSS4 Images widely supported. + sp_repr_css_set_property(css, "image-rendering", "optimizeSpeed"); + sp_repr_css_change(image_node, css, "style"); + sp_repr_css_attr_unref(css); + } + + // PS/PDF images are placed via a transformation matrix, no preserveAspectRatio used + image_node->setAttribute("preserveAspectRatio", "none"); + + // Set transformation + + svgSetTransform(image_node, 1.0, 0.0, 0.0, -1.0, 0.0, 1.0); + + // Create href + if (embed_image) { + // Append format specification to the URI + auto *base64String = g_base64_encode(png_buffer.data(), png_buffer.size()); + auto png_data = std::string("data:image/png;base64,") + base64String; + g_free(base64String); + image_node->setAttributeOrRemoveIfEmpty("xlink:href", png_data); + } else { + fclose(fp); + image_node->setAttribute("xlink:href", file_name); + g_free(file_name); + } + + return image_node; +} + +/** + * \brief Creates a with the specified width and height and adds to + * If we're not the top-level SvgBuilder, creates a too and adds the mask to it. + * \return the created XML node + */ +Inkscape::XML::Node *SvgBuilder::_createMask(double width, double height) { + Inkscape::XML::Node *mask_node = _xml_doc->createElement("svg:mask"); + mask_node->setAttribute("maskUnits", "userSpaceOnUse"); + sp_repr_set_svg_double(mask_node, "x", 0.0); + sp_repr_set_svg_double(mask_node, "y", 0.0); + sp_repr_set_svg_double(mask_node, "width", width); + sp_repr_set_svg_double(mask_node, "height", height); + // Append mask to defs + if (_is_top_level) { + _doc->getDefs()->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return _doc->getDefs()->getRepr()->lastChild(); + } else { // Work around for renderer bug when mask isn't defined in pattern + static int mask_count = 0; + Inkscape::XML::Node *defs = _root->firstChild(); + if ( !( defs && !strcmp(defs->name(), "svg:defs") ) ) { + // Create node + defs = _xml_doc->createElement("svg:defs"); + _root->addChild(defs, nullptr); + Inkscape::GC::release(defs); + defs = _root->firstChild(); + } + gchar *mask_id = g_strdup_printf("_mask%d", mask_count++); + mask_node->setAttribute("id", mask_id); + g_free(mask_id); + defs->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return defs->lastChild(); + } +} + +void SvgBuilder::addImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, int *mask_colors) +{ + + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, mask_colors); + if (image_node) { + _setBlendMode(image_node, state); + _container->appendChild(image_node); + Inkscape::GC::release(image_node); + } +} + +void SvgBuilder::addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate) { + + // Create a rectangle + Inkscape::XML::Node *rect = _xml_doc->createElement("svg:rect"); + sp_repr_set_svg_double(rect, "x", 0.0); + sp_repr_set_svg_double(rect, "y", 0.0); + sp_repr_set_svg_double(rect, "width", 1.0); + sp_repr_set_svg_double(rect, "height", 1.0); + svgSetTransform(rect, 1.0, 0.0, 0.0, -1.0, 0.0, 1.0); + // Get current fill style and set it on the rectangle + SPCSSAttr *css = sp_repr_css_attr_new(); + _setFillStyle(css, state, false); + sp_repr_css_change(rect, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(rect, state); + + // Scaling 1x1 surfaces might not work so skip setting a mask with this size + if ( width > 1 || height > 1 ) { + Inkscape::XML::Node *mask_image_node = + _createImage(str, width, height, nullptr, interpolate, nullptr, true, invert); + if (mask_image_node) { + // Create the mask + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + Inkscape::GC::release(mask_image_node); + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + rect->setAttribute("mask", mask_url); + g_free(mask_url); + } + } + + // Add the rectangle to the container + _container->appendChild(rect); + Inkscape::GC::release(rect); +} + +void SvgBuilder::addMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, bool invert_mask, + bool mask_interpolate) +{ + + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + nullptr, mask_interpolate, nullptr, true, invert_mask); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Scale the mask to the size of the image + Geom::Affine mask_transform((double)width, 0.0, 0.0, (double)height, 0.0, 0.0); + gchar *transform_text = sp_svg_transform_write(mask_transform); + mask_node->setAttribute("maskTransform", transform_text); + g_free(transform_text); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _container->appendChild(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } + if (image_node) { + _setBlendMode(image_node, state); + Inkscape::GC::release(image_node); + } +} + +void SvgBuilder::addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate) +{ + + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + mask_color_map, mask_interpolate, nullptr, true); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _container->appendChild(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } + if (image_node) { + _setBlendMode(image_node, state); + Inkscape::GC::release(image_node); + } +} + +/** + * \brief Starts building a new transparency group + */ +void SvgBuilder::pushTransparencyGroup(GfxState * /*state*/, double *bbox, + GfxColorSpace * /*blending_color_space*/, + bool isolated, bool knockout, + bool for_softmask) { + + // Push node stack + pushNode("svg:g"); + + // Setup new transparency group + SvgTransparencyGroup *transpGroup = new SvgTransparencyGroup; + for (size_t i = 0; i < 4; i++) { + transpGroup->bbox[i] = bbox[i]; + } + transpGroup->isolated = isolated; + transpGroup->knockout = knockout; + transpGroup->for_softmask = for_softmask; + transpGroup->container = _container; + + // Push onto the stack + transpGroup->next = _transp_group_stack; + _transp_group_stack = transpGroup; +} + +void SvgBuilder::popTransparencyGroup(GfxState * /*state*/) { + // Restore node stack + popNode(); +} + +/** + * \brief Places the current transparency group into the current container + */ +void SvgBuilder::paintTransparencyGroup(GfxState * /*state*/, double * /*bbox*/) { + SvgTransparencyGroup *transpGroup = _transp_group_stack; + _container->appendChild(transpGroup->container); + Inkscape::GC::release(transpGroup->container); + // Pop the stack + _transp_group_stack = transpGroup->next; + delete transpGroup; +} + +/** + * \brief Creates a mask using the current transparency group as its content + */ +void SvgBuilder::setSoftMask(GfxState * /*state*/, double * /*bbox*/, bool /*alpha*/, + Function * /*transfer_func*/, GfxColor * /*backdrop_color*/) { + + // Create mask + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Add the softmask content to it + SvgTransparencyGroup *transpGroup = _transp_group_stack; + mask_node->appendChild(transpGroup->container); + Inkscape::GC::release(transpGroup->container); + // Apply the mask + _state_stack.back().softmask = mask_node; + pushGroup(); + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + _container->setAttribute("mask", mask_url); + g_free(mask_url); + // Pop the stack + _transp_group_stack = transpGroup->next; + delete transpGroup; +} + +void SvgBuilder::clearSoftMask(GfxState * /*state*/) { + if (_state_stack.back().softmask) { + _state_stack.back().softmask = nullptr; + popGroup(); + } +} + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/svg-builder.h b/src/extension/internal/pdfinput/svg-builder.h new file mode 100644 index 0000000..050465d --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.h @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +class SPDocument; +namespace Inkscape { + namespace XML { + struct Document; + class Node; + } +} + +#include <2geom/point.h> +#include <2geom/affine.h> +#include + +#include "CharTypes.h" +class Function; +class GfxState; +struct GfxColor; +class GfxColorSpace; +struct GfxRGB; +class GfxPath; +class GfxPattern; +class GfxTilingPattern; +class GfxShading; +class GfxFont; +class GfxImageColorMap; +class Stream; +class XRef; + +class SPCSSAttr; + +#include +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { + +struct SvgTransparencyGroup; + +/** + * Holds information about the current softmask and group depth for use of libpoppler. + * Could be later used to store other graphics state parameters so that we could + * emit only the differences in style settings from the parent state. + */ +struct SvgGraphicsState { + Inkscape::XML::Node *softmask; // Points to current softmask node + int group_depth; // Depth of nesting groups at this level +}; + +/** + * Holds information about glyphs added by PdfParser which haven't been added + * to the document yet. + */ +struct SvgGlyph { + Geom::Point position; // Absolute glyph coords + Geom::Point text_position; // Absolute glyph coords in text space + double dx; // X advance value + double dy; // Y advance value + double rise; // Text rise parameter + Glib::ustring code; // UTF-8 coded character + bool is_space; + + bool style_changed; // Set to true if style has to be reset + SPCSSAttr *style; + int render_mode; // Text render mode + const char *font_specification; // Pointer to current font specification +}; + +/** + * Builds the inner SVG representation using libpoppler from the calls of PdfParser. + */ +class SvgBuilder { +public: + SvgBuilder(SPDocument *document, gchar *docname, XRef *xref); + SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root); + virtual ~SvgBuilder(); + + // Property setting + void setDocumentSize(double width, double height); // Document size in px + void setAsLayer(char *layer_name=nullptr); + void setGroupOpacity(double opacity); + Inkscape::XML::Node *getPreferences() { + return _preferences; + } + + // Handling the node stack + Inkscape::XML::Node *pushGroup(); + Inkscape::XML::Node *popGroup(); + Inkscape::XML::Node *getContainer(); // Returns current group node + + // Path adding + void addPath(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void addShadedFill(GfxShading *shading, double *matrix, GfxPath *path, bool even_odd=false); + + // Image handling + void addImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, int *mask_colors); + void addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate); + void addMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + bool invert_mask, bool mask_interpolate); + void addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate); + + // Transparency group and soft mask handling + void pushTransparencyGroup(GfxState *state, double *bbox, + GfxColorSpace *blending_color_space, + bool isolated, bool knockout, + bool for_softmask); + void popTransparencyGroup(GfxState *state); + void paintTransparencyGroup(GfxState *state, double *bbox); + void setSoftMask(GfxState *state, double *bbox, bool alpha, + Function *transfer_func, GfxColor *backdrop_color); + void clearSoftMask(GfxState *state); + + // Text handling + void beginString(GfxState *state); + void endString(GfxState *state); + void addChar(GfxState *state, double x, double y, + double dx, double dy, + double originX, double originY, + CharCode code, int nBytes, Unicode const *u, int uLen); + void beginTextObject(GfxState *state); + void endTextObject(GfxState *state); + + bool isPatternTypeSupported(GfxPattern *pattern); + + // State manipulation + void saveState(); + void restoreState(); + void updateStyle(GfxState *state); + void updateFont(GfxState *state); + void updateTextPosition(double tx, double ty); + void updateTextShift(GfxState *state, double shift); + void updateTextMatrix(GfxState *state); + + // Clipping + void clip(GfxState *state, bool even_odd=false); + void setClipPath(GfxState *state, bool even_odd=false); + + // Transforming + void setTransform(double c0, double c1, double c2, double c3, double c4, + double c5); + void setTransform(double const *transform); + bool getTransform(double *transform); + +private: + void _init(); + + // Pattern creation + gchar *_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke=false); + gchar *_createGradient(GfxShading *shading, double *matrix, bool for_shading=false); + void _addStopToGradient(Inkscape::XML::Node *gradient, double offset, + GfxRGB *color, double opacity); + bool _addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func); + gchar *_createTilingPattern(GfxTilingPattern *tiling_pattern, GfxState *state, + bool is_stroke=false); + // Image/mask creation + Inkscape::XML::Node *_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only=false, + bool invert_alpha=false); + Inkscape::XML::Node *_createMask(double width, double height); + // Style setting + SPCSSAttr *_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void _setStrokeStyle(SPCSSAttr *css, GfxState *state); + void _setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd); + void _setBlendMode(Inkscape::XML::Node *node, GfxState *state); + void _flushText(); // Write buffered text into doc + + std::string _BestMatchingFont(std::string PDFname); + + // Handling of node stack + Inkscape::XML::Node *pushNode(const char* name); + Inkscape::XML::Node *popNode(); + std::vector _node_stack; + std::vector _group_depth; // Depth of nesting groups + SvgTransparencyGroup *_transp_group_stack; // Transparency group stack + std::vector _state_stack; + + SPCSSAttr *_font_style; // Current font style + GfxFont *_current_font; + const char *_font_specification; + double _font_scaling; + bool _need_font_update; + Geom::Affine _text_matrix; + Geom::Point _text_position; + std::vector _glyphs; // Added characters + bool _in_text_object; // Whether we are inside a text object + bool _invalidated_style; + GfxState *_current_state; + std::vector _availableFontNames; // Full names, used for matching font names (Bug LP #179589). + + bool _is_top_level; // Whether this SvgBuilder is the top-level one + SPDocument *_doc; + gchar *_docname; // Basename of the URI from which this document is created + XRef *_xref; // Cross-reference table from the PDF doc we're converting from + Inkscape::XML::Document *_xml_doc; + Inkscape::XML::Node *_root; // Root node from the point of view of this SvgBuilder + Inkscape::XML::Node *_container; // Current container (group/pattern/mask) + Inkscape::XML::Node *_preferences; // Preferences container node + double _width; // Document size in px + double _height; // Document size in px + double _ttm[6]; ///< temporary transform matrix + bool _ttm_is_set; +}; + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/polyfill/README.md b/src/extension/internal/polyfill/README.md new file mode 100644 index 0000000..2677a50 --- /dev/null +++ b/src/extension/internal/polyfill/README.md @@ -0,0 +1,19 @@ +# JavaScript polyfills + +This directory contains JavaScript "Polyfills" to support rendering of SVG 2 +features that are not well supported by browsers, but appeared in the 2016 +[specification](https://www.w3.org/TR/2016/CR-SVG2-20160915/pservers.html#MeshGradients) + +The included files are: + - `mesh.js` mesh gradients supporting bicubic meshes and mesh on strokes. + - `mesh_compressed.include` mesh.js minified and wrapped as a C++11 raw string literal. + - `hatch.js` hatch paint server supporting linear and absolute paths hatches + (relative paths are not fully supported) + - `hatch_tests` folder with tests used for `hatch.js` rendering + +## Details +The coding standard used is [semistandard](https://github.com/Flet/semistandard), +a more permissive (allows endrow semicolons) over the famous, open-source +[standardjs](https://standardjs.com/). + +The minifier used for the compressed version is [JavaScript minifier](https://javascript-minifier.com/). diff --git a/src/extension/internal/polyfill/hatch.js b/src/extension/internal/polyfill/hatch.js new file mode 100644 index 0000000..db3d88c --- /dev/null +++ b/src/extension/internal/polyfill/hatch.js @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Use patterns to render a hatch paint server via this polyfill + *//* + * Copyright (C) 2019 Valentin Ionita + * Distributed under GNU General Public License version 2 or later. See . + */ + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const unitObjectBoundingBox = 'objectBoundingBox'; + const unitUserSpace = 'userSpaceOnUse'; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Copy attributes from the hatch with 'id' to the current element + const setReference = (el, id) => { + const attr = [ + 'x', 'y', 'pitch', 'rotate', + 'hatchUnits', 'hatchContentUnits', 'transform' + ]; + const template = document.getElementById(id.slice(1)); + + if (template && template.nodeName === 'hatch') { + attr.forEach(a => { + let t = template.getAttribute(a); + if (el.getAttribute(a) === null && t !== null) { + el.setAttribute(a, t); + } + }); + + if (el.children.length === 0) { + Array.from(template.children).forEach(c => { + el.appendChild(c.cloneNode(true)); + }); + } + } + }; + + // Order pain-order of hatchpaths relative to their pitch + const orderHatchPaths = (paths) => { + const nodeArray = []; + paths.forEach(p => nodeArray.push(p)); + + return nodeArray.sort((a, b) => + // (pitch - a.offset) - (pitch - b.offset) + Number(b.getAttribute('offset')) - Number(a.getAttribute('offset')) + ); + }; + + // Generate x-axis coordinates for the pattern paths + const generatePositions = (width, diagonal, initial, distance) => { + const offset = (diagonal - width) / 2; + const leftDistance = initial + offset; + const rightDistance = width + offset + distance; + const units = Math.round(leftDistance / distance) + 1; + let array = []; + + for (let i = initial - units * distance; i < rightDistance; i += distance) { + array.push(i); + } + + return array; + }; + + // Turn a path array into a tokenized version of it + const parsePath = (data) => { + let array = []; + let i = 0; + let len = data.length; + let last = 0; + + /* + * Last state (last) index map + * 0 => () + * 1 => (x y) + * 2 => (x) + * 3 => (y) + * 4 => (x1 y1 x2 y2 x y) + * 5 => (x2 y2 x y) + * 6 => (_ _ _ _ _ x y) + * 7 => (_) + */ + + while (i < len) { + switch (data[i].toUpperCase()) { + case 'Z': + array.push(data[i]); + i += 1; + last = 0; + break; + case 'M': + case 'L': + case 'T': + array.push(data[i], new Point(Number(data[i + 1]), Number(data[i + 2]))); + i += 3; + last = 1; + break; + case 'H': + array.push(data[i], new Point(Number(data[i + 1]), null)); + i += 2; + last = 2; + break; + case 'V': + array.push(data[i], new Point(null, Number(data[i + 1]))); + i += 2; + last = 3; + break; + case 'C': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])), + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + last = 4; + break; + case 'S': + case 'Q': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])) + ); + i += 5; + last = 5; + break; + case 'A': + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + data[i + 5], new Point(Number(data[i + 6]), Number(data[i + 7])) + ); + i += 8; + last = 6; + break; + case 'B': + array.push(data[i], data[i + 1]); + i += 2; + last = 7; + break; + default: + switch (last) { + case 1: + array.push(new Point(Number(data[i]), Number(data[i + 1]))); + i += 2; + break; + case 2: + array.push(new Point(Number(data[i]), null)); + i += 1; + break; + case 3: + array.push(new Point(null, Number(data[i]))); + i += 1; + break; + case 4: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])), + new Point(Number(data[i + 4]), Number(data[i + 5])) + ); + i += 6; + break; + case 5: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])) + ); + i += 4; + break; + case 6: + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + break; + default: + array.push(data[i]); + i += 1; + } + } + } + + return array; + }; + + const getYDistance = (hatchpath) => { + const path = document.createElementNS(svgNS, 'path'); + let d = hatchpath.getAttribute('d'); + + if (d[0].toUpperCase() !== 'M') { + d = `M 0,0 ${d}`; + } + + path.setAttribute('d', d); + + return path.getPointAtLength(path.getTotalLength()).y - + path.getPointAtLength(0).y; + }; + + // Point class -------------------------------------- + class Point { + constructor (x, y) { + this.x = x; + this.y = y; + } + + toString () { + return `${this.x} ${this.y}`; + } + + isPoint () { + return true; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'hatch_shape_' + i; + shape.setAttribute('id', shapeId); + } + + const fill = shape.getAttribute('fill') || shape.style.fill; + const fillURL = fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const hatch = document.getElementById(fillURL[1]); + + if (hatch && hatch.nodeName === 'hatch') { + const href = hatch.getAttributeNS(xlinkNS, 'href'); + + if (href !== null && href !== '') { + setReference(hatch, href); + } + + // Degenerate hatch, with no hatchpath children + if (hatch.children.length === 0) { + return; + } + + const bbox = shape.getBBox(); + const hatchDiag = Math.ceil(Math.sqrt( + bbox.width * bbox.width + bbox.height * bbox.height + )); + + // Hatch variables + const units = hatch.getAttribute('hatchUnits') || unitObjectBoundingBox; + const contentUnits = hatch.getAttribute('hatchContentUnits') || unitUserSpace; + const rotate = Number(hatch.getAttribute('rotate')) || 0; + const transform = hatch.getAttribute('transform') || + hatch.getAttribute('hatchTransform') || ''; + const hatchpaths = orderHatchPaths(hatch.querySelectorAll('hatchpath,hatchPath')); + const x = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('x')) * bbox.width) || 0 + : Number(hatch.getAttribute('x')) || 0; + const y = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('y')) * bbox.width) || 0 + : Number(hatch.getAttribute('y')) || 0; + let pitch = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('pitch')) * bbox.width) || 0 + : Number(hatch.getAttribute('pitch')) || 0; + + if (contentUnits === unitObjectBoundingBox && bbox.height) { + pitch /= bbox.height; + } + + // A negative value is an error. + // A value of zero disables rendering of the element + if (pitch <= 0) { + console.error('Non-positive pitch'); + return; + } + + // Pattern variables + const pattern = document.createElementNS(svgNS, 'pattern'); + const patternId = `${fillURL[1]}_pattern`; + let patternWidth = bbox.width - bbox.width % pitch; + let patternHeight = 0; + + const xPositions = generatePositions(patternWidth, hatchDiag, x, pitch); + + hatchpaths.forEach(hatchpath => { + let offset = Number(hatchpath.getAttribute('offset')) || 0; + offset = offset > pitch ? (offset % pitch) : offset; + const currentXPositions = xPositions.map(p => p + offset); + + const path = document.createElementNS(svgNS, 'path'); + let d = ''; + + for (let j = 0; j < hatchpath.attributes.length; ++j) { + const attr = hatchpath.attributes.item(j); + if (attr.name !== 'd') { + path.setAttribute(attr.name, attr.value); + } + } + + if (hatchpath.getAttribute('d') === null) { + d += currentXPositions.reduce( + (acc, xPos) => `${acc}M ${xPos} ${y} V ${hatchDiag} `, '' + ); + patternHeight = hatchDiag; + } else { + const hatchData = hatchpath.getAttribute('d'); + const data = parsePath( + hatchData.match(/([+-]?(\d+(\.\d+)?))|[MmZzLlHhVvCcSsQqTtAaBb]/g) + ); + const len = data.length; + const startsWithM = data[0] === 'M'; + const relative = data[0].toLowerCase() === data[0]; + const point = new Point(0, 0); + let yOffset = getYDistance(hatchpath); + + if (data[len - 1].y !== undefined && yOffset < data[len - 1].y) { + yOffset = data[len - 1].y; + } + + // The offset must be positive + if (yOffset <= 0) { + console.error('y offset is non-positive'); + return; + } + patternHeight = bbox.height - bbox.height % yOffset; + + const currentYPositions = generatePositions( + patternHeight, hatchDiag, y, yOffset + ); + + currentXPositions.forEach(xPos => { + point.x = xPos; + + if (!startsWithM && !relative) { + d += `M ${xPos} 0`; + } + + currentYPositions.forEach(yPos => { + point.y = yPos; + + if (relative) { + // Path is relative, set the first point in each path render + d += `M ${xPos} ${yPos} ${hatchData}`; + } else { + // Path is absolute, translate every point + d += data.map(e => e.isPoint && e.isPoint() ? e.add(point) : e) + .map(e => e.isPoint && e.isPoint() ? e.toString() : e) + .reduce((acc, e) => `${acc} ${e}`, ''); + } + }); + }); + + // The hatchpaths are infinite, so they have no fill + path.style.fill = 'none'; + } + + path.setAttribute('d', d); + pattern.appendChild(path); + }); + + setAttributes(pattern, { + 'id': patternId, + 'patternUnits': unitUserSpace, + 'patternContentUnits': contentUnits, + 'width': patternWidth, + 'height': patternHeight, + 'x': bbox.x, + 'y': bbox.y, + 'patternTransform': `rotate(${rotate} ${0} ${0}) ${transform}` + }); + hatch.parentElement.insertBefore(pattern, hatch); + + shape.style.fill = `url(#${patternId})`; + shape.setAttribute('fill', `url(#${patternId})`); + } + } + }); +})(); diff --git a/src/extension/internal/polyfill/hatch_compressed.include b/src/extension/internal/polyfill/hatch_compressed.include new file mode 100644 index 0000000..2db6124 --- /dev/null +++ b/src/extension/internal/polyfill/hatch_compressed.include @@ -0,0 +1,4 @@ +//SPDX-License-Identifier: GPL-2.0-or-later +R"=====( +!function(){const t="http://www.w3.org/2000/svg",e=(t,e,r,n)=>{const u=(e-t)/2,i=r+u,s=t+u+n;let h=[];for(let t=r-(Math.round(i/n)+1)*n;t{let i=n.getAttribute("id");i||(i="hatch_shape_"+u,n.setAttribute("id",i));const s=(n.getAttribute("fill")||n.style.fill).match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(s&&s[1]){const u=document.getElementById(s[1]);if(u&&"hatch"===u.nodeName){const i=u.getAttributeNS("http://www.w3.org/1999/xlink","href");if(null!==i&&""!==i&&((t,e)=>{const r=["x","y","pitch","rotate","hatchUnits","hatchContentUnits","transform"],n=document.getElementById(e.slice(1));n&&"hatch"===n.nodeName&&(r.forEach(e=>{let r=n.getAttribute(e);null===t.getAttribute(e)&&null!==r&&t.setAttribute(e,r)}),0===t.children.length&&Array.from(n.children).forEach(e=>{t.appendChild(e.cloneNode(!0))}))})(u,i),0===u.children.length)return;const h=n.getBBox(),o=Math.ceil(Math.sqrt(h.width*h.width+h.height*h.height)),a=u.getAttribute("hatchUnits")||"objectBoundingBox",c=u.getAttribute("hatchContentUnits")||"userSpaceOnUse",b=Number(u.getAttribute("rotate"))||0,l=u.getAttribute("transform")||u.getAttribute("hatchTransform")||"",m=(t=>{const e=[];return t.forEach(t=>e.push(t)),e.sort((t,e)=>Number(e.getAttribute("offset"))-Number(t.getAttribute("offset")))})(u.querySelectorAll("hatchpath,hatchPath")),d="objectBoundingBox"===a?Number(u.getAttribute("x"))*h.width||0:Number(u.getAttribute("x"))||0,g="objectBoundingBox"===a?Number(u.getAttribute("y"))*h.width||0:Number(u.getAttribute("y"))||0;let p="objectBoundingBox"===a?Number(u.getAttribute("pitch"))*h.width||0:Number(u.getAttribute("pitch"))||0;if("objectBoundingBox"===c&&h.height&&(p/=h.height),p<=0)return void console.error("Non-positive pitch");const N=document.createElementNS(t,"pattern"),f=`${s[1]}_pattern`;let w=h.width-h.width%p,A=0;const y=e(w,o,d,p);m.forEach(n=>{let u=Number(n.getAttribute("offset"))||0;u=u>p?u%p:u;const i=y.map(t=>t+u),s=document.createElementNS(t,"path");let a="";for(let t=0;t`${t}M ${e} ${g} V ${o} `,""),A=o;else{const u=n.getAttribute("d"),c=(t=>{let e=[],n=0,u=t.length,i=0;for(;n{const r=document.createElementNS(t,"path");let n=e.getAttribute("d");return"M"!==n[0].toUpperCase()&&(n=`M 0,0 ${n}`),r.setAttribute("d",n),r.getPointAtLength(r.getTotalLength()).y-r.getPointAtLength(0).y})(n);if(void 0!==c[b-1].y&&p{d.x=t,l||m||(a+=`M ${t} 0`),N.forEach(e=>{d.y=e,a+=m?`M ${t} ${e} ${u}`:c.map(t=>t.isPoint&&t.isPoint()?t.add(d):t).map(t=>t.isPoint&&t.isPoint()?t.toString():t).reduce((t,e)=>`${t} ${e}`,"")})}),s.style.fill="none"}s.setAttribute("d",a),N.appendChild(s)}),((t,e)=>{for(let r in e)t.setAttribute(r,e[r])})(N,{id:f,patternUnits:"userSpaceOnUse",patternContentUnits:c,width:w,height:A,x:h.x,y:h.y,patternTransform:`rotate(${b} 0 0) ${l}`}),u.parentElement.insertBefore(N,u),n.style.fill=`url(#${f})`,n.setAttribute("fill",`url(#${f})`)}}})}(); +)=====" diff --git a/src/extension/internal/polyfill/hatch_tests/hatch.svg b/src/extension/internal/polyfill/hatch_tests/hatch.svg new file mode 100644 index 0000000..7e2f8de --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg new file mode 100644 index 0000000..ca9ea2f --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg @@ -0,0 +1,134 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extension/internal/polyfill/hatch_tests/hatch_test.svg b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg new file mode 100644 index 0000000..49fecfb --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg @@ -0,0 +1,11731 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Simple hatches + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Since hatchUnits="userSpaceOnUse" is usedthe rendering will match when hatched shapeis moved to the point 0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <hatch id="simple1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" stroke-width="2"/></hatch> + <hatch id="simple2" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="15"/></hatch> + <hatch id="simple3" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10"/></hatch> + <hatch id="simple4" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10"/></hatch> + <hatch id="simple5" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 10,5"/></hatch> + <hatch id="simple6" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="m 0,0 5,10 5,-5"/></hatch> + <hatch id="simple7" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 M 5,20"/></hatch>  + + + + + + + + + + + + + + + + + + + + + + + + + + <hatch id="transform1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform2" hatchUnits="userSpaceOnUse" pitch="15" rotate="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch>  + <hatch id="transform4" hatchUnits="userSpaceOnUse" pitch="15" rotate="45"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform7" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform8" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10" rotate="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + <hatch id="transform9" hatchUnits="userSpaceOnUse" pitch="15" rotate="30" x="-5" y="-10" hatchTransform="matrix(0.96592583,-0.25881905,0.25881905,0.96592583,-8.4757068,43.273395)"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></hatch> + + + <hatch id="multiple1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/> <hatchPath stroke="#32ff3f" offset="10"/></hatch> + <hatch id="multiple2" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></hatch> + <hatch id="multiple3" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,17" /> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></hatch> + + + + <hatch id="ref1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5"/></hatch> + <hatch id="ref2" xlink:href="#ref1"></hatch> + <hatch id="ref3" xlink:href="#ref1" pitch="45"></hatch> + + <hatch id="stroke1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" stroke-width="5" stroke-dasharray="10 4 2 4"/></hatch>  + + + + + + + + + <hatch id="overflow1" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,5 -5,15, 0,20"/></hatch> + <hatch id="overflow2" style="overflow:hidden" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></hatch> + <hatch id="overflow3" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></hatch>  + <hatch id="overflow4" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"> <hatchPath stroke="#32ff3f" offset="5" > <hatchPath stroke="#ff0000" offset="20" ></hatch> + <hatch id="degenerate1" pitch="45"></hatch> + <hatch id="degenerate2" xlink:href="#nonexisting" pitch="45"></hatch> + <hatch id="degenerate3" pitch="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,15"/></hatch> + <hatch id="degenerate4" pitch="30"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 -5,15"/></hatch> + Hatch transforms + Multiple hatch paths + Hatch linking + Stroke style + Overflow property + Degenerate cases + + + + diff --git a/src/extension/internal/polyfill/mesh.js b/src/extension/internal/polyfill/mesh.js new file mode 100644 index 0000000..d3ba65f --- /dev/null +++ b/src/extension/internal/polyfill/mesh.js @@ -0,0 +1,1188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Use Canvas to render a mesh gradient, passing the rendering to an image via a data stream. +// Copyright: Tavmjong Bah 2018 +// Contributor: Valentin Ionita 2019 +// Distributed under GNU General Public License version 2 or later. See . + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const xhtmlNS = 'http://www.w3.org/1999/xhtml'; + /* + * Maximum threshold for Bezier step size + * Larger values leave holes, smaller take longer to render. + */ + const maxBezierStep = 2.0; + + // Test if mesh gradients are supported. + if (document.createElementNS(svgNS, 'meshgradient').x) { + return; + } + + /* + * Utility functions ----------------------------- + */ + // Split Bezier using de Casteljau's method. + const splitBezier = (p0, p1, p2, p3) => { + let tmp = new Point((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5); + let p01 = new Point((p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5); + let p12 = new Point((p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5); + let p02 = new Point((tmp.x + p01.x) * 0.5, (tmp.y + p01.y) * 0.5); + let p11 = new Point((tmp.x + p12.x) * 0.5, (tmp.y + p12.y) * 0.5); + let p03 = new Point((p02.x + p11.x) * 0.5, (p02.y + p11.y) * 0.5); + + return ([ + [p0, p01, p02, p03], + [p03, p11, p12, p3] + ]); + }; + + // See Cairo: cairo-mesh-pattern-rasterizer.c + const bezierStepsSquared = (points) => { + let tmp0 = points[0].distSquared(points[1]); + let tmp1 = points[2].distSquared(points[3]); + let tmp2 = points[0].distSquared(points[2]) * 0.25; + let tmp3 = points[1].distSquared(points[3]) * 0.25; + + let max1 = tmp0 > tmp1 ? tmp0 : tmp1; + + let max2 = tmp2 > tmp3 ? tmp2 : tmp3; + + let max = max1 > max2 ? max1 : max2; + + return max * 18; + }; + + // Euclidean distance + const distance = (p0, p1) => Math.sqrt(p0.distSquared(p1)); + + // Weighted average to find Bezier points for linear sides. + const wAvg = (p0, p1) => p0.scale(2.0 / 3.0).add(p1.scale(1.0 / 3.0)); + + // Browsers return a string rather than a transform list for gradientTransform! + const parseTransform = (t) => { + let affine = new Affine(); + let trans, scale, radian, tan, skewx, skewy, rotate; + let transforms = t.match(/(\w+\(\s*[^)]+\))+/g); + + transforms.forEach((i) => { + let c = i.match(/[\w.-]+/g); + let type = c.shift(); + + switch (type) { + case 'translate': + if (c.length === 2) { + trans = new Affine(1, 0, 0, 1, c[0], c[1]); + } else { + console.error('mesh.js: translate does not have 2 arguments!'); + trans = new Affine(1, 0, 0, 1, 0, 0); + } + affine = affine.append(trans); + break; + + case 'scale': + if (c.length === 1) { + scale = new Affine(c[0], 0, 0, c[0], 0, 0); + } else if (c.length === 2) { + scale = new Affine(c[0], 0, 0, c[1], 0, 0); + } else { + console.error('mesh.js: scale does not have 1 or 2 arguments!'); + scale = new Affine(1, 0, 0, 1, 0, 0); + } + affine = affine.append(scale); + break; + + case 'rotate': + if (c.length === 3) { + trans = new Affine(1, 0, 0, 1, c[1], c[2]); + affine = affine.append(trans); + } + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + let cos = Math.cos(radian); + let sin = Math.sin(radian); + if (Math.abs(cos) < 1e-16) { // I hate rounding errors... + cos = 0; + } + if (Math.abs(sin) < 1e-16) { // I hate rounding errors... + sin = 0; + } + rotate = new Affine(cos, sin, -sin, cos, 0, 0); + affine = affine.append(rotate); + } else { + console.error('math.js: No argument to rotate transform!'); + } + if (c.length === 3) { + trans = new Affine(1, 0, 0, 1, -c[1], -c[2]); + affine = affine.append(trans); + } + break; + + case 'skewX': + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + tan = Math.tan(radian); + skewx = new Affine(1, 0, tan, 1, 0, 0); + affine = affine.append(skewx); + } else { + console.error('math.js: No argument to skewX transform!'); + } + break; + + case 'skewY': + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + tan = Math.tan(radian); + skewy = new Affine(1, tan, 0, 1, 0, 0); + affine = affine.append(skewy); + } else { + console.error('math.js: No argument to skewY transform!'); + } + break; + + case 'matrix': + if (c.length === 6) { + affine = affine.append(new Affine(...c)); + } else { + console.error('math.js: Incorrect number of arguments for matrix!'); + } + break; + + default: + console.error('mesh.js: Unhandled transform type: ' + type); + break; + } + }); + + return affine; + }; + + const parsePoints = (s) => { + let points = []; + let values = s.split(/[ ,]+/); + for (let i = 0, imax = values.length - 1; i < imax; i += 2) { + points.push(new Point(parseFloat(values[i]), parseFloat(values[i + 1]))); + } + return points; + }; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Find the slope of point p_k by the values in p_k-1 and p_k+1 + const finiteDifferences = (c0, c1, c2, d01, d12) => { + let slope = [0, 0, 0, 0]; + let slow, shigh; + + for (let k = 0; k < 3; ++k) { + if ((c1[k] < c0[k] && c1[k] < c2[k]) || (c0[k] < c1[k] && c2[k] < c1[k])) { + slope[k] = 0; + } else { + slope[k] = 0.5 * ((c1[k] - c0[k]) / d01 + (c2[k] - c1[k]) / d12); + slow = Math.abs(3.0 * (c1[k] - c0[k]) / d01); + shigh = Math.abs(3.0 * (c2[k] - c1[k]) / d12); + + if (slope[k] > slow) { + slope[k] = slow; + } else if (slope[k] > shigh) { + slope[k] = shigh; + } + } + } + + return slope; + }; + + // Coefficient matrix used for solving linear system + const A = [ + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-3, 3, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, -2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2, -1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0], + [-3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0], + [9, -9, -9, 9, 6, 3, -6, -3, 6, -6, 3, -3, 4, 2, 2, 1], + [-6, 6, 6, -6, -3, -3, 3, 3, -4, 4, -2, 2, -2, -2, -1, -1], + [2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [-6, 6, 6, -6, -4, -2, 4, 2, -3, 3, -3, 3, -2, -1, -2, -1], + [4, -4, -4, 4, 2, 2, -2, -2, 2, -2, 2, -2, 1, 1, 1, 1] + ]; + + // Solve the linear system for bicubic interpolation + const solveLinearSystem = (v) => { + let alpha = []; + + for (let i = 0; i < 16; ++i) { + alpha[i] = 0; + for (let j = 0; j < 16; ++j) { + alpha[i] += A[i][j] * v[j]; + } + } + + return alpha; + }; + + // Evaluate the interpolation parameters at (y, x) + const evaluateSolution = (alpha, x, y) => { + const xx = x * x; + const yy = y * y; + const xxx = x * x * x; + const yyy = y * y * y; + + let result = + alpha[0] + + alpha[1] * x + + alpha[2] * xx + + alpha[3] * xxx + + alpha[4] * y + + alpha[5] * y * x + + alpha[6] * y * xx + + alpha[7] * y * xxx + + alpha[8] * yy + + alpha[9] * yy * x + + alpha[10] * yy * xx + + alpha[11] * yy * xxx + + alpha[12] * yyy + + alpha[13] * yyy * x + + alpha[14] * yyy * xx + + alpha[15] * yyy * xxx; + + return result; + }; + + // Split a patch into 8x8 smaller patches + const splitPatch = (patch) => { + let yPatches = []; + let xPatches = []; + let patches = []; + + // Horizontal splitting + for (let i = 0; i < 4; ++i) { + yPatches[i] = []; + yPatches[i][0] = splitBezier( + patch[0][i], patch[1][i], + patch[2][i], patch[3][i] + ); + + yPatches[i][1] = []; + yPatches[i][1].push(...splitBezier(...yPatches[i][0][0])); + yPatches[i][1].push(...splitBezier(...yPatches[i][0][1])); + + yPatches[i][2] = []; + yPatches[i][2].push(...splitBezier(...yPatches[i][1][0])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][1])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][2])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][3])); + } + + // Vertical splitting + for (let i = 0; i < 8; ++i) { + xPatches[i] = []; + + for (let j = 0; j < 4; ++j) { + xPatches[i][j] = []; + xPatches[i][j][0] = splitBezier( + yPatches[0][2][i][j], yPatches[1][2][i][j], + yPatches[2][2][i][j], yPatches[3][2][i][j] + ); + + xPatches[i][j][1] = []; + xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][0])); + xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][1])); + + xPatches[i][j][2] = []; + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][0])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][1])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][2])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][3])); + } + } + + for (let i = 0; i < 8; ++i) { + patches[i] = []; + + for (let j = 0; j < 8; ++j) { + patches[i][j] = []; + + patches[i][j][0] = xPatches[i][0][2][j]; + patches[i][j][1] = xPatches[i][1][2][j]; + patches[i][j][2] = xPatches[i][2][2][j]; + patches[i][j][3] = xPatches[i][3][2][j]; + } + } + + return patches; + }; + + // Point class ----------------------------------- + class Point { + constructor (x, y) { + this.x = x || 0; + this.y = y || 0; + } + + toString () { + return `(x=${this.x}, y=${this.y})`; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + scale (v) { + if (v.x === undefined) { + return new Point(this.x * v, this.y * v); + } + return new Point(this.x * v.x, this.y * v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + + // Transform by affine + transform (affine) { + let x = this.x * affine.a + this.y * affine.c + affine.e; + let y = this.x * affine.b + this.y * affine.d + affine.f; + return new Point(x, y); + } + } + + /* + * Affine class ------------------------------------- + * + * As defined in the SVG spec + * | a c e | + * | b d f | + * | 0 0 1 | + * + */ + + class Affine { + constructor (a, b, c, d, e, f) { + if (a === undefined) { + this.a = 1; + this.b = 0; + this.c = 0; + this.d = 1; + this.e = 0; + this.f = 0; + } else { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + } + } + + toString () { + return `affine: ${this.a} ${this.c} ${this.e} \n\ + ${this.b} ${this.d} ${this.f}`; + } + + append (v) { + if (!(v instanceof Affine)) { + console.error(`mesh.js: argument to Affine.append is not affine!`); + } + let a = this.a * v.a + this.c * v.b; + let b = this.b * v.a + this.d * v.b; + let c = this.a * v.c + this.c * v.d; + let d = this.b * v.c + this.d * v.d; + let e = this.a * v.e + this.c * v.f + this.e; + let f = this.b * v.e + this.d * v.f + this.f; + return new Affine(a, b, c, d, e, f); + } + } + + // Curve class -------------------------------------- + class Curve { + constructor (nodes, colors) { + this.nodes = nodes; // 4 Bezier points + this.colors = colors; // 2 x 4 colors (two ends x R+G+B+A) + } + + /* + * Paint a Bezier curve + * w is canvas.width + * h is canvas.height + */ + paintCurve (v, w) { + // If inside, see if we need to split + if (bezierStepsSquared(this.nodes) > maxBezierStep) { + const beziers = splitBezier(...this.nodes); + // ([start][end]) + let colors0 = [[], []]; + let colors1 = [[], []]; + + /* + * Linear horizontal interpolation of the middle value for every + * patch exceeding thereshold + */ + for (let i = 0; i < 4; ++i) { + colors0[0][i] = this.colors[0][i]; + colors0[1][i] = (this.colors[0][i] + this.colors[1][i]) / 2; + colors1[0][i] = colors0[1][i]; + colors1[1][i] = this.colors[1][i]; + } + let curve0 = new Curve(beziers[0], colors0); + let curve1 = new Curve(beziers[1], colors1); + curve0.paintCurve(v, w); + curve1.paintCurve(v, w); + } else { + // Directly write data + let x = Math.round(this.nodes[0].x); + if (x >= 0 && x < w) { + let index = (~~this.nodes[0].y * w + x) * 4; + v[index] = Math.round(this.colors[0][0]); + v[index + 1] = Math.round(this.colors[0][1]); + v[index + 2] = Math.round(this.colors[0][2]); + v[index + 3] = Math.round(this.colors[0][3]); // Alpha + } + } + } + } + + // Patch class ------------------------------------- + class Patch { + constructor (nodes, colors) { + this.nodes = nodes; // 4x4 array of points + this.colors = colors; // 2x2x4 colors (four corners x R+G+B+A) + } + + // Split patch horizontally into two patches. + split () { + let nodes0 = [[], [], [], []]; + let nodes1 = [[], [], [], []]; + let colors0 = [ + [[], []], + [[], []] + ]; + let colors1 = [ + [[], []], + [[], []] + ]; + + for (let i = 0; i < 4; ++i) { + const beziers = splitBezier( + this.nodes[0][i], this.nodes[1][i], + this.nodes[2][i], this.nodes[3][i] + ); + + nodes0[0][i] = beziers[0][0]; + nodes0[1][i] = beziers[0][1]; + nodes0[2][i] = beziers[0][2]; + nodes0[3][i] = beziers[0][3]; + nodes1[0][i] = beziers[1][0]; + nodes1[1][i] = beziers[1][1]; + nodes1[2][i] = beziers[1][2]; + nodes1[3][i] = beziers[1][3]; + } + + /* + * Linear vertical interpolation of the middle value for every + * patch exceeding thereshold + */ + for (let i = 0; i < 4; ++i) { + colors0[0][0][i] = this.colors[0][0][i]; + colors0[0][1][i] = this.colors[0][1][i]; + colors0[1][0][i] = (this.colors[0][0][i] + this.colors[1][0][i]) / 2; + colors0[1][1][i] = (this.colors[0][1][i] + this.colors[1][1][i]) / 2; + colors1[0][0][i] = colors0[1][0][i]; + colors1[0][1][i] = colors0[1][1][i]; + colors1[1][0][i] = this.colors[1][0][i]; + colors1[1][1][i] = this.colors[1][1][i]; + } + + return ([new Patch(nodes0, colors0), new Patch(nodes1, colors1)]); + } + + paint (v, w) { + // Check if we need to split + let larger = false; + let step; + for (let i = 0; i < 4; ++i) { + step = bezierStepsSquared([ + this.nodes[0][i], this.nodes[1][i], + this.nodes[2][i], this.nodes[3][i] + ]); + + if (step > maxBezierStep) { + larger = true; + break; + } + } + + if (larger) { + let patches = this.split(); + patches[0].paint(v, w); + patches[1].paint(v, w); + } else { + /* + * Paint a Bezier curve using just the top of the patch. If + * the patch is thin enough this should work. We leave this + * function here in case we want to do something more fancy. + */ + let curve = new Curve([...this.nodes[0]], [...this.colors[0]]); + curve.paintCurve(v, w); + } + } + } + + // Mesh class --------------------------------------- + class Mesh { + constructor (mesh) { + this.readMesh(mesh); + this.type = mesh.getAttribute('type') || 'bilinear'; + } + + // Function to parse an SVG mesh and set the nodes (points) and colors + readMesh (mesh) { + let nodes = [[]]; + let colors = [[]]; + + let x = Number(mesh.getAttribute('x')); + let y = Number(mesh.getAttribute('y')); + nodes[0][0] = new Point(x, y); + + let rows = mesh.children; + for (let i = 0, imax = rows.length; i < imax; ++i) { + // Need to validate if meshrow... + nodes[3 * i + 1] = []; // Need three extra rows for each meshrow. + nodes[3 * i + 2] = []; + nodes[3 * i + 3] = []; + colors[i + 1] = []; // Need one more row than number of meshrows. + + let patches = rows[i].children; + for (let j = 0, jmax = patches.length; j < jmax; ++j) { + let stops = patches[j].children; + for (let k = 0, kmax = stops.length; k < kmax; ++k) { + let l = k; + if (i !== 0) { + ++l; // There is no top if row isn't first row. + } + let path = stops[k].getAttribute('path'); + let parts; + + let type = 'l'; // We need to still find mid-points even if no path. + if (path != null) { + parts = path.match(/\s*([lLcC])\s*(.*)/); + type = parts[1]; + } + let stopNodes = parsePoints(parts[2]); + + switch (type) { + case 'l': + if (l === 0) { // Top + nodes[3 * i][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]); + nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 3][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]); + } + nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]); + } else { // Left + nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]); + nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]); + } + break; + case 'L': + if (l === 0) { // Top + nodes[3 * i][3 * j + 3] = stopNodes[0]; + nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]); + nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 3][3 * j + 3] = stopNodes[0]; + nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[0]; + } + nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]); + } else { // Left + nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]); + nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]); + } + break; + case 'c': + if (l === 0) { // Top + nodes[3 * i][3 * j + 1] = stopNodes[0].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 2] = stopNodes[1].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 1][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = stopNodes[1].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 3][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + nodes[3 * i + 3][3 * j + 2] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 1] = stopNodes[1].add(nodes[3 * i + 3][3 * j + 3]); + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[2].add(nodes[3 * i + 3][3 * j + 3]); + } + } else { // Left + nodes[3 * i + 2][3 * j] = stopNodes[0].add(nodes[3 * i + 3][3 * j]); + nodes[3 * i + 1][3 * j] = stopNodes[1].add(nodes[3 * i + 3][3 * j]); + } + break; + case 'C': + if (l === 0) { // Top + nodes[3 * i][3 * j + 1] = stopNodes[0]; + nodes[3 * i][3 * j + 2] = stopNodes[1]; + nodes[3 * i][3 * j + 3] = stopNodes[2]; + } else if (l === 1) { // Right + nodes[3 * i + 1][3 * j + 3] = stopNodes[0]; + nodes[3 * i + 2][3 * j + 3] = stopNodes[1]; + nodes[3 * i + 3][3 * j + 3] = stopNodes[2]; + } else if (l === 2) { // Bottom + nodes[3 * i + 3][3 * j + 2] = stopNodes[0]; + nodes[3 * i + 3][3 * j + 1] = stopNodes[1]; + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[2]; + } + } else { // Left + nodes[3 * i + 2][3 * j] = stopNodes[0]; + nodes[3 * i + 1][3 * j] = stopNodes[1]; + } + break; + default: + console.error('mesh.js: ' + type + ' invalid path type.'); + } + + if ((i === 0 && j === 0) || k > 0) { + let colorRaw = window.getComputedStyle(stops[k]).stopColor + .match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); + let alphaRaw = window.getComputedStyle(stops[k]).stopOpacity; + let alpha = 255; + if (alphaRaw) { + alpha = Math.floor(alphaRaw * 255); + } + + if (colorRaw) { + if (l === 0) { // upper left corner + colors[i][j] = []; + colors[i][j][0] = Math.floor(colorRaw[1]); + colors[i][j][1] = Math.floor(colorRaw[2]); + colors[i][j][2] = Math.floor(colorRaw[3]); + colors[i][j][3] = alpha; // Alpha + } else if (l === 1) { // upper right corner + colors[i][j + 1] = []; + colors[i][j + 1][0] = Math.floor(colorRaw[1]); + colors[i][j + 1][1] = Math.floor(colorRaw[2]); + colors[i][j + 1][2] = Math.floor(colorRaw[3]); + colors[i][j + 1][3] = alpha; // Alpha + } else if (l === 2) { // lower right corner + colors[i + 1][j + 1] = []; + colors[i + 1][j + 1][0] = Math.floor(colorRaw[1]); + colors[i + 1][j + 1][1] = Math.floor(colorRaw[2]); + colors[i + 1][j + 1][2] = Math.floor(colorRaw[3]); + colors[i + 1][j + 1][3] = alpha; // Alpha + } else if (l === 3) { // lower left corner + colors[i + 1][j] = []; + colors[i + 1][j][0] = Math.floor(colorRaw[1]); + colors[i + 1][j][1] = Math.floor(colorRaw[2]); + colors[i + 1][j][2] = Math.floor(colorRaw[3]); + colors[i + 1][j][3] = alpha; // Alpha + } + } + } + } + + // SVG doesn't use tensor points but we need them for rendering. + nodes[3 * i + 1][3 * j + 1] = new Point(); + nodes[3 * i + 1][3 * j + 2] = new Point(); + nodes[3 * i + 2][3 * j + 1] = new Point(); + nodes[3 * i + 2][3 * j + 2] = new Point(); + + nodes[3 * i + 1][3 * j + 1].x = + (-4.0 * nodes[3 * i][3 * j].x + + 6.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 1][3 * j].x) + + -2.0 * (nodes[3 * i][3 * j + 3].x + nodes[3 * i + 3][3 * j].x) + + 3.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 1][3 * j + 3].x) + + -1.0 * nodes[3 * i + 3][3 * j + 3].x) / 9.0; + nodes[3 * i + 1][3 * j + 2].x = + (-4.0 * nodes[3 * i][3 * j + 3].x + + 6.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 1][3 * j + 3].x) + + -2.0 * (nodes[3 * i][3 * j].x + nodes[3 * i + 3][3 * j + 3].x) + + 3.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 1][3 * j].x) + + -1.0 * nodes[3 * i + 3][3 * j].x) / 9.0; + nodes[3 * i + 2][3 * j + 1].x = + (-4.0 * nodes[3 * i + 3][3 * j].x + + 6.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 2][3 * j].x) + + -2.0 * (nodes[3 * i + 3][3 * j + 3].x + nodes[3 * i][3 * j].x) + + 3.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 2][3 * j + 3].x) + + -1.0 * nodes[3 * i][3 * j + 3].x) / 9.0; + nodes[3 * i + 2][3 * j + 2].x = + (-4.0 * nodes[3 * i + 3][3 * j + 3].x + + 6.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 2][3 * j + 3].x) + + -2.0 * (nodes[3 * i + 3][3 * j].x + nodes[3 * i][3 * j + 3].x) + + 3.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 2][3 * j].x) + + -1.0 * nodes[3 * i][3 * j].x) / 9.0; + + nodes[3 * i + 1][3 * j + 1].y = + (-4.0 * nodes[3 * i][3 * j].y + + 6.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 1][3 * j].y) + + -2.0 * (nodes[3 * i][3 * j + 3].y + nodes[3 * i + 3][3 * j].y) + + 3.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 1][3 * j + 3].y) + + -1.0 * nodes[3 * i + 3][3 * j + 3].y) / 9.0; + nodes[3 * i + 1][3 * j + 2].y = + (-4.0 * nodes[3 * i][3 * j + 3].y + + 6.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 1][3 * j + 3].y) + + -2.0 * (nodes[3 * i][3 * j].y + nodes[3 * i + 3][3 * j + 3].y) + + 3.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 1][3 * j].y) + + -1.0 * nodes[3 * i + 3][3 * j].y) / 9.0; + nodes[3 * i + 2][3 * j + 1].y = + (-4.0 * nodes[3 * i + 3][3 * j].y + + 6.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 2][3 * j].y) + + -2.0 * (nodes[3 * i + 3][3 * j + 3].y + nodes[3 * i][3 * j].y) + + 3.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 2][3 * j + 3].y) + + -1.0 * nodes[3 * i][3 * j + 3].y) / 9.0; + nodes[3 * i + 2][3 * j + 2].y = + (-4.0 * nodes[3 * i + 3][3 * j + 3].y + + 6.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 2][3 * j + 3].y) + + -2.0 * (nodes[3 * i + 3][3 * j].y + nodes[3 * i][3 * j + 3].y) + + 3.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 2][3 * j].y) + + -1.0 * nodes[3 * i][3 * j].y) / 9.0; + } + } + + this.nodes = nodes; // (m*3+1) x (n*3+1) points + this.colors = colors; // (m+1) x (n+1) x 4 colors (R+G+B+A) + } + + // Extracts out each patch and then paints it + paintMesh (v, w) { + let imax = (this.nodes.length - 1) / 3; + let jmax = (this.nodes[0].length - 1) / 3; + + if (this.type === 'bilinear' || imax < 2 || jmax < 2) { + let patch; + + for (let i = 0; i < imax; ++i) { + for (let j = 0; j < jmax; ++j) { + let sliceNodes = []; + for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) { + sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4)); + } + + let sliceColors = []; + sliceColors.push(this.colors[i].slice(j, j + 2)); + sliceColors.push(this.colors[i + 1].slice(j, j + 2)); + + patch = new Patch(sliceNodes, sliceColors); + patch.paint(v, w); + } + } + } else { + // Reference: + // https://en.wikipedia.org/wiki/Bicubic_interpolation#Computation + let d01, d12, patch, sliceNodes, nodes, f, alpha; + const ilast = imax; + const jlast = jmax; + imax++; + jmax++; + + /* + * d = the interpolation data + * d[i][j] = a node record (Point, color_array, color_dx, color_dy) + * d[i][j][0] : Point + * d[i][j][1] : [RGBA] + * d[i][j][2] = dx [RGBA] + * d[i][j][3] = dy [RGBA] + * d[i][j][][k] : color channel k + */ + let d = new Array(imax); + + // Setting the node and the colors + for (let i = 0; i < imax; ++i) { + d[i] = new Array(jmax); + for (let j = 0; j < jmax; ++j) { + d[i][j] = []; + d[i][j][0] = this.nodes[3 * i][3 * j]; + d[i][j][1] = this.colors[i][j]; + } + } + + // Calculate the inner derivatives + for (let i = 0; i < imax; ++i) { + for (let j = 0; j < jmax; ++j) { + // dx + if (i !== 0 && i !== ilast) { + d01 = distance(d[i - 1][j][0], d[i][j][0]); + d12 = distance(d[i + 1][j][0], d[i][j][0]); + d[i][j][2] = finiteDifferences(d[i - 1][j][1], d[i][j][1], + d[i + 1][j][1], d01, d12); + } + + // dy + if (j !== 0 && j !== jlast) { + d01 = distance(d[i][j - 1][0], d[i][j][0]); + d12 = distance(d[i][j + 1][0], d[i][j][0]); + d[i][j][3] = finiteDifferences(d[i][j - 1][1], d[i][j][1], + d[i][j + 1][1], d01, d12); + } + + // dxy is, by standard, set to 0 + } + } + + /* + * Calculate the exterior derivatives + * We fit the exterior derivatives onto parabolas generated by + * the point and the interior derivatives. + */ + for (let j = 0; j < jmax; ++j) { + d[0][j][2] = []; + d[ilast][j][2] = []; + + for (let k = 0; k < 4; ++k) { + d01 = distance(d[1][j][0], d[0][j][0]); + d12 = distance(d[ilast][j][0], d[ilast - 1][j][0]); + + if (d01 > 0) { + d[0][j][2][k] = 2.0 * (d[1][j][1][k] - d[0][j][1][k]) / d01 - + d[1][j][2][k]; + } else { + console.log(`0 was 0! (j: ${j}, k: ${k})`); + d[0][j][2][k] = 0; + } + + if (d12 > 0) { + d[ilast][j][2][k] = 2.0 * (d[ilast][j][1][k] - d[ilast - 1][j][1][k]) / + d12 - d[ilast - 1][j][2][k]; + } else { + console.log(`last was 0! (j: ${j}, k: ${k})`); + d[ilast][j][2][k] = 0; + } + } + } + + for (let i = 0; i < imax; ++i) { + d[i][0][3] = []; + d[i][jlast][3] = []; + + for (let k = 0; k < 4; ++k) { + d01 = distance(d[i][1][0], d[i][0][0]); + d12 = distance(d[i][jlast][0], d[i][jlast - 1][0]); + + if (d01 > 0) { + d[i][0][3][k] = 2.0 * (d[i][1][1][k] - d[i][0][1][k]) / d01 - + d[i][1][3][k]; + } else { + console.log(`0 was 0! (i: ${i}, k: ${k})`); + d[i][0][3][k] = 0; + } + + if (d12 > 0) { + d[i][jlast][3][k] = 2.0 * (d[i][jlast][1][k] - d[i][jlast - 1][1][k]) / + d12 - d[i][jlast - 1][3][k]; + } else { + console.log(`last was 0! (i: ${i}, k: ${k})`); + d[i][jlast][3][k] = 0; + } + } + } + + // Fill patches + for (let i = 0; i < ilast; ++i) { + for (let j = 0; j < jlast; ++j) { + let dLeft = distance(d[i][j][0], d[i + 1][j][0]); + let dRight = distance(d[i][j + 1][0], d[i + 1][j + 1][0]); + let dTop = distance(d[i][j][0], d[i][j + 1][0]); + let dBottom = distance(d[i + 1][j][0], d[i + 1][j + 1][0]); + let r = [[], [], [], []]; + + for (let k = 0; k < 4; ++k) { + f = []; + + f[0] = d[i][j][1][k]; + f[1] = d[i + 1][j][1][k]; + f[2] = d[i][j + 1][1][k]; + f[3] = d[i + 1][j + 1][1][k]; + f[4] = d[i][j][2][k] * dLeft; + f[5] = d[i + 1][j][2][k] * dLeft; + f[6] = d[i][j + 1][2][k] * dRight; + f[7] = d[i + 1][j + 1][2][k] * dRight; + f[8] = d[i][j][3][k] * dTop; + f[9] = d[i + 1][j][3][k] * dBottom; + f[10] = d[i][j + 1][3][k] * dTop; + f[11] = d[i + 1][j + 1][3][k] * dBottom; + f[12] = 0; // dxy + f[13] = 0; // dxy + f[14] = 0; // dxy + f[15] = 0; // dxy + + // get alpha values + alpha = solveLinearSystem(f); + + for (let l = 0; l < 9; ++l) { + r[k][l] = []; + + for (let m = 0; m < 9; ++m) { + // evaluation + r[k][l][m] = evaluateSolution(alpha, l / 8, m / 8); + + if (r[k][l][m] > 255) { + r[k][l][m] = 255; + } else if (r[k][l][m] < 0.0) { + r[k][l][m] = 0.0; + } + } + } + } + + // split the bezier patch into 8x8 patches + sliceNodes = []; + for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) { + sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4)); + } + + nodes = splitPatch(sliceNodes); + + // Create patches and paint the bilinearliy + for (let l = 0; l < 8; ++l) { + for (let m = 0; m < 8; ++m) { + patch = new Patch( + nodes[l][m], + [[ + [r[0][l][m], r[1][l][m], r[2][l][m], r[3][l][m]], + [r[0][l][m + 1], r[1][l][m + 1], r[2][l][m + 1], r[3][l][m + 1]] + ], [ + [r[0][l + 1][m], r[1][l + 1][m], r[2][l + 1][m], r[3][l + 1][m]], + [r[0][l + 1][m + 1], r[1][l + 1][m + 1], r[2][l + 1][m + 1], r[3][l + 1][m + 1]] + ]] + ); + + patch.paint(v, w); + } + } + } + } + } + } + + // Transforms mesh into coordinate space of canvas (t is either Point or Affine). + transform (t) { + if (t instanceof Point) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].add(t); + } + } + } else if (t instanceof Affine) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].transform(t); + } + } + } + } + + // Scale mesh into coordinate space of canvas (t is a Point). + scale (t) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].scale(t); + } + } + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'patchjs_shape' + i; + shape.setAttribute('id', shapeId); + } + + const fillURL = shape.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + const strokeURL = shape.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const mesh = document.getElementById(fillURL[1]); + + if (mesh && mesh.nodeName === 'meshgradient') { + const bbox = shape.getBBox(); + + // Create temporary canvas + let myCanvas = document.createElementNS(xhtmlNS, 'canvas'); + setAttributes(myCanvas, { + 'width': bbox.width, + 'height': bbox.height + }); + + const myContext = myCanvas.getContext('2d'); + let myCanvasImage = myContext.createImageData(bbox.width, bbox.height); + + // Draw a mesh + const myMesh = new Mesh(mesh); + + // Adjust for bounding box if necessary. + if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') { + myMesh.scale(new Point(bbox.width, bbox.height)); + } + + // Apply gradient transform. + const gradientTransform = mesh.getAttribute('gradientTransform'); + if (gradientTransform != null) { + myMesh.transform(parseTransform(gradientTransform)); + } + + // Position to Canvas coordinate. + if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') { + myMesh.transform(new Point(-bbox.x, -bbox.y)); + } + + // Paint + myMesh.paintMesh(myCanvasImage.data, myCanvas.width); + myContext.putImageData(myCanvasImage, 0, 0); + + // Create image element of correct size + const myImage = document.createElementNS(svgNS, 'image'); + setAttributes(myImage, { + 'width': bbox.width, + 'height': bbox.height, + 'x': bbox.x, + 'y': bbox.y + }); + + // Set image to data url + let myPNG = myCanvas.toDataURL(); + myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG); + + // Insert image into document + shape.parentNode.insertBefore(myImage, shape); + shape.style.fill = 'none'; + + // Create clip referencing shape and insert into document + const use = document.createElementNS(svgNS, 'use'); + use.setAttributeNS(xlinkNS, 'xlink:href', '#' + shapeId); + + const clipId = 'patchjs_clip' + i; + const clip = document.createElementNS(svgNS, 'clipPath'); + clip.setAttribute('id', clipId); + clip.appendChild(use); + shape.parentElement.insertBefore(clip, shape); + myImage.setAttribute('clip-path', 'url(#' + clipId + ')'); + + // Force the Garbage Collector to free the space + myCanvasImage = null; + myCanvas = null; + myPNG = null; + } + } + + if (strokeURL && strokeURL[1]) { + const mesh = document.getElementById(strokeURL[1]); + + if (mesh && mesh.nodeName === 'meshgradient') { + const strokeWidth = parseFloat(shape.style.strokeWidth.slice(0, -2)); + const strokeMiterlimit = parseFloat(shape.style.strokeMiterlimit) || + parseFloat(shape.getAttribute('stroke-miterlimit')) || 1; + const phase = strokeWidth * strokeMiterlimit; + + const bbox = shape.getBBox(); + const boxWidth = Math.trunc(bbox.width + phase); + const boxHeight = Math.trunc(bbox.height + phase); + const boxX = Math.trunc(bbox.x - phase / 2); + const boxY = Math.trunc(bbox.y - phase / 2); + + // Create temporary canvas + let myCanvas = document.createElementNS(xhtmlNS, 'canvas'); + setAttributes(myCanvas, { + 'width': boxWidth, + 'height': boxHeight + }); + + const myContext = myCanvas.getContext('2d'); + let myCanvasImage = myContext.createImageData(boxWidth, boxHeight); + + // Draw a mesh + const myMesh = new Mesh(mesh); + + // Adjust for bounding box if necessary. + if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') { + myMesh.scale(new Point(boxWidth, boxHeight)); + } + + // Apply gradient transform. + const gradientTransform = mesh.getAttribute('gradientTransform'); + if (gradientTransform != null) { + myMesh.transform(parseTransform(gradientTransform)); + } + + // Position to Canvas coordinate. + if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') { + myMesh.transform(new Point(-boxX, -boxY)); + } + + // Paint + myMesh.paintMesh(myCanvasImage.data, myCanvas.width); + myContext.putImageData(myCanvasImage, 0, 0); + + // Create image element of correct size + const myImage = document.createElementNS(svgNS, 'image'); + setAttributes(myImage, { + 'width': boxWidth, + 'height': boxHeight, + 'x': 0, + 'y': 0 + }); + + // Set image to data url + let myPNG = myCanvas.toDataURL(); + myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG); + + // Create pattern to hold the stroke image + const patternId = 'pattern_clip' + i; + const myPattern = document.createElementNS(svgNS, 'pattern'); + setAttributes(myPattern, { + 'id': patternId, + 'patternUnits': 'userSpaceOnUse', + 'width': boxWidth, + 'height': boxHeight, + 'x': boxX, + 'y': boxY + }); + myPattern.appendChild(myImage); + + // Insert image into document + mesh.parentNode.appendChild(myPattern); + shape.style.stroke = 'url(#' + patternId + ')'; + + // Force the Garbage Collector to free the space + myCanvasImage = null; + myCanvas = null; + myPNG = null; + } + } + }); +})(); diff --git a/src/extension/internal/polyfill/mesh_compressed.include b/src/extension/internal/polyfill/mesh_compressed.include new file mode 100644 index 0000000..c6eeca0 --- /dev/null +++ b/src/extension/internal/polyfill/mesh_compressed.include @@ -0,0 +1,4 @@ +//SPDX-License-Identifier: GPL-2.0-or-later +R"=====( +!function(){const t="http://www.w3.org/2000/svg",e="http://www.w3.org/1999/xlink",s="http://www.w3.org/1999/xhtml",r=2;if(document.createElementNS(t,"meshgradient").x)return;const n=(t,e,s,r)=>{let n=new x(.5*(e.x+s.x),.5*(e.y+s.y)),o=new x(.5*(t.x+e.x),.5*(t.y+e.y)),i=new x(.5*(s.x+r.x),.5*(s.y+r.y)),a=new x(.5*(n.x+o.x),.5*(n.y+o.y)),h=new x(.5*(n.x+i.x),.5*(n.y+i.y)),l=new x(.5*(a.x+h.x),.5*(a.y+h.y));return[[t,o,a,l],[l,h,i,r]]},o=t=>{let e=t[0].distSquared(t[1]),s=t[2].distSquared(t[3]),r=.25*t[0].distSquared(t[2]),n=.25*t[1].distSquared(t[3]),o=e>s?e:s,i=r>n?r:n;return 18*(o>i?o:i)},i=(t,e)=>Math.sqrt(t.distSquared(e)),a=(t,e)=>t.scale(2/3).add(e.scale(1/3)),h=t=>{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=>{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case"translate":2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error("mesh.js: translate does not have 2 arguments!"),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case"scale":1===l.length?s=new g(l[0],0,0,l[0],0,0):2===l.length?s=new g(l[0],0,0,l[1],0,0):(console.error("mesh.js: scale does not have 1 or 2 arguments!"),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case"rotate":if(3===l.length&&(e=new g(1,0,0,1,l[1],l[2]),h=h.append(e)),l[0]){r=l[0]*Math.PI/180;let t=Math.cos(r),e=Math.sin(r);Math.abs(t)<1e-16&&(t=0),Math.abs(e)<1e-16&&(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error("math.js: No argument to rotate transform!");3===l.length&&(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case"skewX":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),o=new g(1,0,n,1,0,0),h=h.append(o)):console.error("math.js: No argument to skewX transform!");break;case"skewY":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),i=new g(1,n,0,1,0,0),h=h.append(i)):console.error("math.js: No argument to skewY transform!");break;case"matrix":6===l.length?h=h.append(new g(...l)):console.error("math.js: Incorrect number of arguments for matrix!");break;default:console.error("mesh.js: Unhandled transform type: "+d)}}),h},l=t=>{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=>{let o,i,a=[0,0,0,0];for(let h=0;h<3;++h)e[h]o?a[h]=o:a[h]>i&&(a[h]=i));return a},u=[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],[-3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0],[2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0],[0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0],[-3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0],[0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0],[9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1],[-6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1],[2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0],[0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0],[-6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1],[4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1]],f=t=>{let e=[];for(let s=0;s<16;++s){e[s]=0;for(let r=0;r<16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=>{const r=e*e,n=s*s,o=e*e*e,i=s*s*s;return t[0]+t[1]*e+t[2]*r+t[3]*o+t[4]*s+t[5]*s*e+t[6]*s*r+t[7]*s*o+t[8]*n+t[9]*n*e+t[10]*n*r+t[11]*n*o+t[12]*i+t[13]*i*e+t[14]*i*r+t[15]*i*o},y=t=>{let e=[],s=[],r=[];for(let s=0;s<4;++s)e[s]=[],e[s][0]=n(t[0][s],t[1][s],t[2][s],t[3][s]),e[s][1]=[],e[s][1].push(...n(...e[s][0][0])),e[s][1].push(...n(...e[s][0][1])),e[s][2]=[],e[s][2].push(...n(...e[s][1][0])),e[s][2].push(...n(...e[s][1][1])),e[s][2].push(...n(...e[s][1][2])),e[s][2].push(...n(...e[s][1][3]));for(let t=0;t<8;++t){s[t]=[];for(let r=0;r<4;++r)s[t][r]=[],s[t][r][0]=n(e[0][2][t][r],e[1][2][t][r],e[2][2][t][r],e[3][2][t][r]),s[t][r][1]=[],s[t][r][1].push(...n(...s[t][r][0][0])),s[t][r][1].push(...n(...s[t][r][0][1])),s[t][r][2]=[],s[t][r][2].push(...n(...s[t][r][1][0])),s[t][r][2].push(...n(...s[t][r][1][1])),s[t][r][2].push(...n(...s[t][r][1][2])),s[t][r][2].push(...n(...s[t][r][1][3]))}for(let t=0;t<8;++t){r[t]=[];for(let e=0;e<8;++e)r[t][e]=[],r[t][e][0]=s[t][0][2][e],r[t][e][1]=s[t][1][2][e],r[t][e][2]=s[t][2][2][e],r[t][e][3]=s[t][3][2][e]}return r};class x{constructor(t,e){this.x=t||0,this.y=e||0}toString(){return`(x=${this.x}, y=${this.y})`}clone(){return new x(this.x,this.y)}add(t){return new x(this.x+t.x,this.y+t.y)}scale(t){return void 0===t.x?new x(this.x*t,this.y*t):new x(this.x*t.x,this.y*t.y)}distSquared(t){let e=this.x-t.x,s=this.y-t.y;return e*e+s*s}transform(t){let e=this.x*t.a+this.y*t.c+t.e,s=this.x*t.b+this.y*t.d+t.f;return new x(e,s)}}class g{constructor(t,e,s,r,n,o){void 0===t?(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0):(this.a=t,this.b=e,this.c=s,this.d=r,this.e=n,this.f=o)}toString(){return`affine: ${this.a} ${this.c} ${this.e} \n ${this.b} ${this.d} ${this.f}`}append(t){t instanceof g||console.error("mesh.js: argument to Affine.append is not affine!");let e=this.a*t.a+this.c*t.b,s=this.b*t.a+this.d*t.b,r=this.a*t.c+this.c*t.d,n=this.b*t.c+this.d*t.d,o=this.a*t.e+this.c*t.f+this.e,i=this.b*t.e+this.d*t.f+this.f;return new g(e,s,r,n,o,i)}}class w{constructor(t,e){this.nodes=t,this.colors=e}paintCurve(t,e){if(o(this.nodes)>r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t<4;++t)r[0][t]=this.colors[0][t],r[1][t]=(this.colors[0][t]+this.colors[1][t])/2,o[0][t]=r[1][t],o[1][t]=this.colors[1][t];let i=new w(s[0],r),a=new w(s[1],o);i.paintCurve(t,e),a.paintCurve(t,e)}else{let s=Math.round(this.nodes[0].x);if(s>=0&&sr){n=!0;break}if(n){let s=this.split();s[0].paint(t,e),s[1].paint(t,e)}else{new w([...this.nodes[0]],[...this.colors[0]]).paintCurve(t,e)}}}class b{constructor(t){this.readMesh(t),this.type=t.getAttribute("type")||"bilinear"}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute("x")),n=Number(t.getAttribute("y"));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t0){let e=window.getComputedStyle(o[r]).stopColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i),a=window.getComputedStyle(o[r]).stopOpacity,h=255;a&&(h=Math.floor(255*a)),e&&(0===i?(s[t][n]=[],s[t][n][0]=Math.floor(e[1]),s[t][n][1]=Math.floor(e[2]),s[t][n][2]=Math.floor(e[3]),s[t][n][3]=h):1===i?(s[t][n+1]=[],s[t][n+1][0]=Math.floor(e[1]),s[t][n+1][1]=Math.floor(e[2]),s[t][n+1][2]=Math.floor(e[3]),s[t][n+1][3]=h):2===i?(s[t+1][n+1]=[],s[t+1][n+1][0]=Math.floor(e[1]),s[t+1][n+1][1]=Math.floor(e[2]),s[t+1][n+1][2]=Math.floor(e[3]),s[t+1][n+1][3]=h):3===i&&(s[t+1][n]=[],s[t+1][n][0]=Math.floor(e[1]),s[t+1][n][1]=Math.floor(e[2]),s[t+1][n][2]=Math.floor(e[3]),s[t+1][n][3]=h))}}e[3*t+1][3*n+1]=new x,e[3*t+1][3*n+2]=new x,e[3*t+2][3*n+1]=new x,e[3*t+2][3*n+2]=new x,e[3*t+1][3*n+1].x=(-4*e[3*t][3*n].x+6*(e[3*t][3*n+1].x+e[3*t+1][3*n].x)+-2*(e[3*t][3*n+3].x+e[3*t+3][3*n].x)+3*(e[3*t+3][3*n+1].x+e[3*t+1][3*n+3].x)+-1*e[3*t+3][3*n+3].x)/9,e[3*t+1][3*n+2].x=(-4*e[3*t][3*n+3].x+6*(e[3*t][3*n+2].x+e[3*t+1][3*n+3].x)+-2*(e[3*t][3*n].x+e[3*t+3][3*n+3].x)+3*(e[3*t+3][3*n+2].x+e[3*t+1][3*n].x)+-1*e[3*t+3][3*n].x)/9,e[3*t+2][3*n+1].x=(-4*e[3*t+3][3*n].x+6*(e[3*t+3][3*n+1].x+e[3*t+2][3*n].x)+-2*(e[3*t+3][3*n+3].x+e[3*t][3*n].x)+3*(e[3*t][3*n+1].x+e[3*t+2][3*n+3].x)+-1*e[3*t][3*n+3].x)/9,e[3*t+2][3*n+2].x=(-4*e[3*t+3][3*n+3].x+6*(e[3*t+3][3*n+2].x+e[3*t+2][3*n+3].x)+-2*(e[3*t+3][3*n].x+e[3*t][3*n+3].x)+3*(e[3*t][3*n+2].x+e[3*t+2][3*n].x)+-1*e[3*t][3*n].x)/9,e[3*t+1][3*n+1].y=(-4*e[3*t][3*n].y+6*(e[3*t][3*n+1].y+e[3*t+1][3*n].y)+-2*(e[3*t][3*n+3].y+e[3*t+3][3*n].y)+3*(e[3*t+3][3*n+1].y+e[3*t+1][3*n+3].y)+-1*e[3*t+3][3*n+3].y)/9,e[3*t+1][3*n+2].y=(-4*e[3*t][3*n+3].y+6*(e[3*t][3*n+2].y+e[3*t+1][3*n+3].y)+-2*(e[3*t][3*n].y+e[3*t+3][3*n+3].y)+3*(e[3*t+3][3*n+2].y+e[3*t+1][3*n].y)+-1*e[3*t+3][3*n].y)/9,e[3*t+2][3*n+1].y=(-4*e[3*t+3][3*n].y+6*(e[3*t+3][3*n+1].y+e[3*t+2][3*n].y)+-2*(e[3*t+3][3*n+3].y+e[3*t][3*n].y)+3*(e[3*t][3*n+1].y+e[3*t+2][3*n+3].y)+-1*e[3*t][3*n+3].y)/9,e[3*t+2][3*n+2].y=(-4*e[3*t+3][3*n+3].y+6*(e[3*t+3][3*n+2].y+e[3*t+2][3*n+3].y)+-2*(e[3*t+3][3*n].y+e[3*t][3*n+3].y)+3*(e[3*t][3*n+2].y+e[3*t+2][3*n].y)+-1*e[3*t][3*n].y)/9}}this.nodes=e,this.colors=s}paintMesh(t,e){let s=(this.nodes.length-1)/3,r=(this.nodes[0].length-1)/3;if("bilinear"===this.type||s<2||r<2){let n;for(let o=0;o0?2*(w[1][t][1][e]-w[0][t][1][e])/n-w[1][t][2][e]:0,w[x][t][2][e]=o>0?2*(w[x][t][1][e]-w[x-1][t][1][e])/o-w[x-1][t][2][e]:0}for(let t=0;t0?2*(w[t][1][1][e]-w[t][0][1][e])/n-w[t][1][3][e]:0,w[t][g][3][e]=o>0?2*(w[t][g][1][e]-w[t][g-1][1][e])/o-w[t][g-1][3][e]:0}for(let s=0;s255?g[t][e][s]=255:g[t][e][s]<0&&(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t{let o=r.getAttribute("id");o||(o="patchjs_shape"+n,r.setAttribute("id",o));const i=r.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/),a=r.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(i&&i[1]){const a=document.getElementById(i[1]);if(a&&"meshgradient"===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,"canvas");d(l,{width:i.width,height:i.height});const c=l.getContext("2d");let u=c.createImageData(i.width,i.height);const f=new b(a);"objectBoundingBox"===a.getAttribute("gradientUnits")&&f.scale(new x(i.width,i.height));const p=a.getAttribute("gradientTransform");null!=p&&f.transform(h(p)),"userSpaceOnUse"===a.getAttribute("gradientUnits")&&f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,"image");d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,"xlink:href",g),r.parentNode.insertBefore(y,r),r.style.fill="none";const w=document.createElementNS(t,"use");w.setAttributeNS(e,"xlink:href","#"+o);const m="patchjs_clip"+n,M=document.createElementNS(t,"clipPath");M.setAttribute("id",m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute("clip-path","url(#"+m+")"),u=null,l=null,g=null}}if(a&&a[1]){const o=document.getElementById(a[1]);if(o&&"meshgradient"===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute("stroke-miterlimit"))||1),a=r.getBBox(),l=Math.trunc(a.width+i),c=Math.trunc(a.height+i),u=Math.trunc(a.x-i/2),f=Math.trunc(a.y-i/2);let p=document.createElementNS(s,"canvas");d(p,{width:l,height:c});const y=p.getContext("2d");let g=y.createImageData(l,c);const w=new b(o);"objectBoundingBox"===o.getAttribute("gradientUnits")&&w.scale(new x(l,c));const m=o.getAttribute("gradientTransform");null!=m&&w.transform(h(m)),"userSpaceOnUse"===o.getAttribute("gradientUnits")&&w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,"image");d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,"xlink:href",S);const k="pattern_clip"+n,A=document.createElementNS(t,"pattern");d(A,{id:k,patternUnits:"userSpaceOnUse",width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke="url(#"+k+")",g=null,p=null,S=null}}})}(); +)=====" diff --git a/src/extension/internal/pov-out.cpp b/src/extension/internal/pov-out.cpp new file mode 100644 index 0000000..e9d0a0c --- /dev/null +++ b/src/extension/internal/pov-out.cpp @@ -0,0 +1,742 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple utility for exporting Inkscape svg Shapes as PovRay bezier + * prisms. Note that this is output-only, and would thus seem to be + * better placed as an 'export' rather than 'output'. However, Export + * handles all or partial documents, while this outputs ALL shapes in + * the current SVG document. + * + * For information on the PovRay file format, see: + * http://www.povray.org + * + * Authors: + * Bob Jamison + * Abhishek Sharma + * + * Copyright (C) 2004-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pov-out.h" +#include +#include +#include +#include +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include + +#include "object/sp-root.h" +#include "object/sp-path.h" +#include "style.h" + +#include +#include +#include +#include "document.h" +#include "extension/extension.h" + + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + + +//######################################################################## +//# M E S S A G E S +//######################################################################## + +static void err(const char *fmt, ...) +{ + va_list args; + g_log(nullptr, G_LOG_LEVEL_WARNING, "Pov-out err: "); + va_start(args, fmt); + g_logv(nullptr, G_LOG_LEVEL_WARNING, fmt, args); + va_end(args); + g_log(nullptr, G_LOG_LEVEL_WARNING, "\n"); +} + + + + +//######################################################################## +//# U T I L I T Y +//######################################################################## + + + +static double effective_opacity(SPItem const *item) +{ + // TODO investigate this. The early return seems that it would abort early. + // Plus is will emit a warning, which may not be proper here. + double ret = 1.0; + for (SPObject const *obj = item; obj; obj = obj->parent) { + g_return_val_if_fail(obj->style, ret); + ret *= SP_SCALE24_TO_FLOAT(obj->style->opacity.value); + } + return ret; +} + + + + + +//######################################################################## +//# OUTPUT FORMATTING +//######################################################################## + +PovOutput::PovOutput() : + outbuf (), + nrNodes (0), + nrSegments (0), + nrShapes (0), + idIndex (0), + minx (0), + miny (0), + maxx (0), + maxy (0) +{ +} + +/** + * We want to control floating output format + */ +static PovOutput::String dstr(double d) +{ + char dbuf[G_ASCII_DTOSTR_BUF_SIZE+1]; + g_ascii_formatd(dbuf, G_ASCII_DTOSTR_BUF_SIZE, + "%.8f", (gdouble)d); + PovOutput::String s = dbuf; + return s; +} + +#define DSTR(d) (dstr(d).c_str()) + + +/** + * Output data to the buffer, printf()-style + */ +void PovOutput::out(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + gchar *output = g_strdup_vprintf(fmt, args); + va_end(args); + outbuf.append(output); + g_free(output); +} + + + + + +/** + * Output a 2d vector + */ +void PovOutput::vec2(double a, double b) +{ + out("<%s, %s>", DSTR(a), DSTR(b)); +} + + + +/** + * Output a 3d vector + */ +void PovOutput::vec3(double a, double b, double c) +{ + out("<%s, %s, %s>", DSTR(a), DSTR(b), DSTR(c)); +} + + + +/** + * Output a v4d ector + */ +void PovOutput::vec4(double a, double b, double c, double d) +{ + out("<%s, %s, %s, %s>", DSTR(a), DSTR(b), DSTR(c), DSTR(d)); +} + + + +/** + * Output an rgbf color vector + */ +void PovOutput::rgbf(double r, double g, double b, double f) +{ + //"rgbf < %1.3f, %1.3f, %1.3f %1.3f>" + out("rgbf "); + vec4(r, g, b, f); +} + + + +/** + * Output one bezier's start, start-control, end-control, and end nodes + */ +void PovOutput::segment(int segNr, + double startX, double startY, + double startCtrlX, double startCtrlY, + double endCtrlX, double endCtrlY, + double endX, double endY) +{ + //" /*%4d*/ <%f, %f>, <%f, %f>, <%f,%f>, <%f,%f>" + out(" /*%4d*/ ", segNr); + vec2(startX, startY); + out(", "); + vec2(startCtrlX, startCtrlY); + out(", "); + vec2(endCtrlX, endCtrlY); + out(", "); + vec2(endX, endY); +} + + + + + +/** + * Output the file header + */ +bool PovOutput::doHeader() +{ + time_t tim = time(nullptr); + out("/*###################################################################\n"); + out("### This PovRay document was generated by Inkscape\n"); + out("### http://www.inkscape.org\n"); + out("### Created: %s", ctime(&tim)); + out("### Version: %s\n", Inkscape::version_string); + out("#####################################################################\n"); + out("### NOTES:\n"); + out("### ============\n"); + out("### POVRay information can be found at\n"); + out("### http://www.povray.org\n"); + out("###\n"); + out("### The 'AllShapes' objects at the bottom are provided as a\n"); + out("### preview of how the output would look in a trace. However,\n"); + out("### the main intent of this file is to provide the individual\n"); + out("### shapes for inclusion in a POV project.\n"); + out("###\n"); + out("### For an example of how to use this file, look at\n"); + out("### share/examples/istest.pov\n"); + out("###\n"); + out("### If you have any problems with this output, please see the\n"); + out("### Inkscape project at http://www.inkscape.org, or visit\n"); + out("### the #inkscape channel on irc.freenode.net . \n"); + out("###\n"); + out("###################################################################*/\n"); + out("\n\n"); + out("/*###################################################################\n"); + out("## Exports in this file\n"); + out("##==========================\n"); + out("## Shapes : %d\n", nrShapes); + out("## Segments : %d\n", nrSegments); + out("## Nodes : %d\n", nrNodes); + out("###################################################################*/\n"); + out("\n\n\n"); + return true; +} + + + +/** + * Output the file footer + */ +bool PovOutput::doTail() +{ + out("\n\n"); + out("/*###################################################################\n"); + out("### E N D F I L E\n"); + out("###################################################################*/\n"); + out("\n\n"); + return true; +} + + + +/** + * Output the curve data to buffer + */ +bool PovOutput::doCurve(SPItem *item, const String &id) +{ + using Geom::X; + using Geom::Y; + + //### Get the Shape + if (!SP_IS_SHAPE(item))//Bulia's suggestion. Allow all shapes + return true; + + SPShape *shape = SP_SHAPE(item); + if (shape->_curve->is_empty()) { + return true; + } + + nrShapes++; + + PovShapeInfo shapeInfo; + shapeInfo.id = id; + shapeInfo.color = ""; + + //Try to get the fill color of the shape + SPStyle *style = shape->style; + /* fixme: Handle other fill types, even if this means translating gradients to a single + flat colour. */ + if (style) + { + if (style->fill.isColor()) + { + // see color.h for how to parse SPColor + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + double const dopacity = ( SP_SCALE24_TO_FLOAT(style->fill_opacity.value) + * effective_opacity(shape) ); + //gchar *str = g_strdup_printf("rgbf < %1.3f, %1.3f, %1.3f %1.3f>", + // rgb[0], rgb[1], rgb[2], 1.0 - dopacity); + String rgbf = "rgbf <"; + rgbf.append(dstr(rgb[0])); rgbf.append(", "); + rgbf.append(dstr(rgb[1])); rgbf.append(", "); + rgbf.append(dstr(rgb[2])); rgbf.append(", "); + rgbf.append(dstr(1.0 - dopacity)); rgbf.append(">"); + shapeInfo.color += rgbf; + } + } + + povShapes.push_back(shapeInfo); //passed all tests. save the info + + // convert the path to only lineto's and cubic curveto's: + Geom::Affine tf = item->i2dt_affine(); + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers( shape->_curve->get_pathvector() * tf ); + + /* + * We need to know the number of segments (NR_CURVETOs/LINETOs, including + * closing line segment) before we write out segment data. Since we are + * going to skip degenerate (zero length) paths, we need to loop over all + * subpaths and segments first. + */ + int segmentCount = 0; + /** + * For all Subpaths in the + */ + for (const auto & pit : pathv) + { + /** + * For all segments in the subpath, including extra closing segment defined by 2geom + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) + { + + // Skip zero length segments. + if( !cit->isDegenerate() ) ++segmentCount; + } + } + + out("/*###################################################\n"); + out("### PRISM: %s\n", id.c_str()); + out("###################################################*/\n"); + out("#declare %s = prism {\n", id.c_str()); + out(" linear_sweep\n"); + out(" bezier_spline\n"); + out(" 1.0, //top\n"); + out(" 0.0, //bottom\n"); + out(" %d //nr points\n", segmentCount * 4); + int segmentNr = 0; + + nrSegments += segmentCount; + + /** + * at moment of writing, 2geom lacks proper initialization of empty intervals in rect... + */ + Geom::Rect cminmax( pathv.front().initialPoint(), pathv.front().initialPoint() ); + + + /** + * For all Subpaths in the + */ + for (const auto & pit : pathv) + { + + cminmax.expandTo(pit.initialPoint()); + + /** + * For all segments in the subpath, including extra closing segment defined by 2geom + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) + { + + // Skip zero length segments + if( cit->isDegenerate() ) + continue; + + if( is_straight_curve(*cit) ) + { + Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + segment(segmentNr++, + p0[X], p0[Y], p0[X], p0[Y], p1[X], p1[Y], p1[X], p1[Y] ); + nrNodes += 8; + } + else if(Geom::CubicBezier const *cubic = dynamic_cast(&*cit)) + { + std::vector points = cubic->controlPoints(); + Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + segment(segmentNr++, + p0[X],p0[Y], p1[X],p1[Y], p2[X],p2[Y], p3[X],p3[Y]); + nrNodes += 8; + } + else + { + err("logical error, because pathv_to_linear_and_cubic_beziers was used"); + return false; + } + + if (segmentNr < segmentCount) + out(",\n"); + else + out("\n"); + if (segmentNr > segmentCount) + { + err("Too many segments"); + return false; + } + + cminmax.expandTo(cit->finalPoint()); + + } + } + + out("}\n"); + + double cminx = cminmax.min()[X]; + double cmaxx = cminmax.max()[X]; + double cminy = cminmax.min()[Y]; + double cmaxy = cminmax.max()[Y]; + + out("#declare %s_MIN_X = %s;\n", id.c_str(), DSTR(cminx)); + out("#declare %s_CENTER_X = %s;\n", id.c_str(), DSTR((cmaxx+cminx)/2.0)); + out("#declare %s_MAX_X = %s;\n", id.c_str(), DSTR(cmaxx)); + out("#declare %s_WIDTH = %s;\n", id.c_str(), DSTR(cmaxx-cminx)); + out("#declare %s_MIN_Y = %s;\n", id.c_str(), DSTR(cminy)); + out("#declare %s_CENTER_Y = %s;\n", id.c_str(), DSTR((cmaxy+cminy)/2.0)); + out("#declare %s_MAX_Y = %s;\n", id.c_str(), DSTR(cmaxy)); + out("#declare %s_HEIGHT = %s;\n", id.c_str(), DSTR(cmaxy-cminy)); + if (shapeInfo.color.length()>0) + out("#declare %s_COLOR = %s;\n", + id.c_str(), shapeInfo.color.c_str()); + out("/*###################################################\n"); + out("### end %s\n", id.c_str()); + out("###################################################*/\n\n\n\n"); + + if (cminx < minx) + minx = cminx; + if (cmaxx > maxx) + maxx = cmaxx; + if (cminy < miny) + miny = cminy; + if (cmaxy > maxy) + maxy = cmaxy; + + return true; +} + +/** + * Descend the svg tree recursively, translating data + */ +bool PovOutput::doTreeRecursive(SPDocument *doc, SPObject *obj) +{ + + String id; + if (!obj->getId()) + { + char buf[16]; + sprintf(buf, "id%d", idIndex++); + id = buf; + } + else + { + id = obj->getId(); + } + + if (SP_IS_ITEM(obj)) + { + SPItem *item = SP_ITEM(obj); + if (!doCurve(item, id)) + return false; + } + + /** + * Descend into children + */ + for (auto &child: obj->children) + { + if (!doTreeRecursive(doc, &child)) + return false; + } + + return true; +} + +/** + * Output the curve data to buffer + */ +bool PovOutput::doTree(SPDocument *doc) +{ + double bignum = 1000000.0; + minx = bignum; + maxx = -bignum; + miny = bignum; + maxy = -bignum; + + if (!doTreeRecursive(doc, doc->getRoot())) + return false; + + //## Let's make a union of all of the Shapes + if (!povShapes.empty()) + { + String id = "AllShapes"; + char *pfx = (char *)id.c_str(); + out("/*###################################################\n"); + out("### UNION OF ALL SHAPES IN DOCUMENT\n"); + out("###################################################*/\n"); + out("\n\n"); + out("/**\n"); + out(" * Allow the user to redefine the finish{}\n"); + out(" * by declaring it before #including this file\n"); + out(" */\n"); + out("#ifndef (%s_Finish)\n", pfx); + out("#declare %s_Finish = finish {\n", pfx); + out(" phong 0.5\n"); + out(" reflection 0.3\n"); + out(" specular 0.5\n"); + out("}\n"); + out("#end\n"); + out("\n\n"); + out("#declare %s = union {\n", id.c_str()); + for (auto & povShape : povShapes) + { + out(" object { %s\n", povShape.id.c_str()); + out(" texture { \n"); + if (povShape.color.length()>0) + out(" pigment { %s }\n", povShape.color.c_str()); + else + out(" pigment { rgb <0,0,0> }\n"); + out(" finish { %s_Finish }\n", pfx); + out(" } \n"); + out(" } \n"); + } + out("}\n\n\n\n"); + + + double zinc = 0.2 / (double)povShapes.size(); + out("/*#### Same union, but with Z-diffs (actually Y in pov) ####*/\n"); + out("\n\n"); + out("/**\n"); + out(" * Allow the user to redefine the Z-Increment\n"); + out(" */\n"); + out("#ifndef (AllShapes_Z_Increment)\n"); + out("#declare AllShapes_Z_Increment = %s;\n", DSTR(zinc)); + out("#end\n"); + out("\n"); + out("#declare AllShapes_Z_Scale = 1.0;\n"); + out("\n\n"); + out("#declare %s_Z = union {\n", pfx); + + for (auto & povShape : povShapes) + { + out(" object { %s\n", povShape.id.c_str()); + out(" texture { \n"); + if (povShape.color.length()>0) + out(" pigment { %s }\n", povShape.color.c_str()); + else + out(" pigment { rgb <0,0,0> }\n"); + out(" finish { %s_Finish }\n", pfx); + out(" } \n"); + out(" scale <1, %s_Z_Scale, 1>\n", pfx); + out(" } \n"); + out("#declare %s_Z_Scale = %s_Z_Scale + %s_Z_Increment;\n\n", + pfx, pfx, pfx); + } + + out("}\n"); + + out("#declare %s_MIN_X = %s;\n", pfx, DSTR(minx)); + out("#declare %s_CENTER_X = %s;\n", pfx, DSTR((maxx+minx)/2.0)); + out("#declare %s_MAX_X = %s;\n", pfx, DSTR(maxx)); + out("#declare %s_WIDTH = %s;\n", pfx, DSTR(maxx-minx)); + out("#declare %s_MIN_Y = %s;\n", pfx, DSTR(miny)); + out("#declare %s_CENTER_Y = %s;\n", pfx, DSTR((maxy+miny)/2.0)); + out("#declare %s_MAX_Y = %s;\n", pfx, DSTR(maxy)); + out("#declare %s_HEIGHT = %s;\n", pfx, DSTR(maxy-miny)); + out("/*##############################################\n"); + out("### end %s\n", id.c_str()); + out("##############################################*/\n"); + out("\n\n"); + } + + return true; +} + + +//######################################################################## +//# M A I N O U T P U T +//######################################################################## + + + +/** + * Set values back to initial state + */ +void PovOutput::reset() +{ + nrNodes = 0; + nrSegments = 0; + nrShapes = 0; + idIndex = 0; + outbuf.clear(); + povShapes.clear(); +} + + + +/** + * Saves the Shapes of an Inkscape SVG file as PovRay spline definitions + */ +void PovOutput::saveDocument(SPDocument *doc, gchar const *filename_utf8) +{ + reset(); + + //###### SAVE IN POV FORMAT TO BUFFER + //# Lets do the curves first, to get the stats + if (!doTree(doc)) + { + err("Could not output curves for %s", filename_utf8); + return; + } + + String curveBuf = outbuf; + outbuf.clear(); + + if (!doHeader()) + { + err("Could not write header for %s", filename_utf8); + return; + } + + outbuf.append(curveBuf); + + if (!doTail()) + { + err("Could not write footer for %s", filename_utf8); + return; + } + + + + + //###### WRITE TO FILE + Inkscape::IO::dump_fopen_call(filename_utf8, "L"); + FILE *f = Inkscape::IO::fopen_utf8name(filename_utf8, "w"); + if (!f) + return; + + for (String::iterator iter = outbuf.begin() ; iter!=outbuf.end(); ++iter) + { + int ch = *iter; + fputc(ch, f); + } + + fclose(f); +} + + + + +//######################################################################## +//# EXTENSION API +//######################################################################## + + + +#include "clear-n_.h" + + + +/** + * API call to save document +*/ +void +PovOutput::save(Inkscape::Extension::Output */*mod*/, + SPDocument *doc, gchar const *filename_utf8) +{ + /* See comments in JavaFSOutput::save re the name `filename_utf8'. */ + saveDocument(doc, filename_utf8); +} + + + +/** + * Make sure that we are in the database + */ +bool PovOutput::check (Inkscape::Extension::Extension */*module*/) +{ + /* We don't need a Key + if (NULL == Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_POV)) + return FALSE; + */ + + return true; +} + + + +/** + * This is the definition of PovRay output. This function just + * calls the extension system with the memory allocated XML that + * describes the data. +*/ +void +PovOutput::init() +{ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("PovRay Output") "\n" + "org.inkscape.output.pov\n" + "\n" + ".pov\n" + "text/x-povray-script\n" + "" N_("PovRay (*.pov) (paths and shapes only)") "\n" + "" N_("PovRay Raytracer File") "\n" + "\n" + "", + new PovOutput()); +} + + + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/pov-out.h b/src/extension/internal/pov-out.h new file mode 100644 index 0000000..3dee88b --- /dev/null +++ b/src/extension/internal/pov-out.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple utility for exporting Inkscape svg Shapes as PovRay bezier + * prisms. Note that this is output-only, and would thus seem to be + * better placed as an 'export' rather than 'output'. However, Export + * handles all or partial documents, while this outputs ALL shapes in + * the current SVG document. + * + * Authors: + * Bob Jamison + * + * Copyright (C) 2004-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_POV_OUT_H +#define EXTENSION_INTERNAL_POV_OUT_H + +#include +#include "extension/implementation/implementation.h" + +class SPObject; +class SPItem; + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + + + +/** + * Output bezier splines in POVRay format. + * + * For information, @see: + * http://www.povray.org + */ +class PovOutput : public Inkscape::Extension::Implementation::Implementation +{ + + +public: + + PovOutput(); + + /** + * Our internal String definition + */ + typedef Glib::ustring String; + + + /** + * Check whether we can actually output using this module + */ + bool check (Inkscape::Extension::Extension * module) override; + + /** + * API call to perform the output to a file + */ + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, gchar const *filename) override; + + /** + * Inkscape runtime startup call. + */ + static void init(); + + /** + * Reset variables to initial state + */ + void reset(); + +private: + + /** + * Format text to our output buffer + */ + void out(const char *fmt, ...) G_GNUC_PRINTF(2,3); + + /** + * Output a 2d vector + */ + void vec2(double a, double b); + + /** + * Output a 3d vector + */ + void vec3(double a, double b, double c); + + /** + * Output a 4d vector + */ + void vec4(double a, double b, double c, double d); + + /** + * Output an rgbf color vector + */ + void rgbf(double r, double g, double b, double f); + + /** + * Output one bezier's start, start-control, + * end-control, and end nodes + */ + void segment(int segNr, double a0, double a1, + double b0, double b1, + double c0, double c1, + double d0, double d1); + + + /** + * Output the file header + */ + bool doHeader(); + + /** + * Output the file footer + */ + bool doTail(); + + /** + * Output the SVG document's curve data as POV curves + */ + bool doCurve(SPItem *item, const String &id); + bool doTreeRecursive(SPDocument *doc, SPObject *obj); + bool doTree(SPDocument *doc); + + /** + * Actual method to save document + */ + void saveDocument(SPDocument *doc, gchar const *filename); + + + /** + * used for saving information about shapes + */ + class PovShapeInfo + { + public: + PovShapeInfo() + = default; + PovShapeInfo(const PovShapeInfo &other) + { assign(other); } + PovShapeInfo& operator=(const PovShapeInfo &other) + { assign(other); return *this; } + virtual ~PovShapeInfo() + = default; + String id; + String color; + + private: + void assign(const PovShapeInfo &other) + { + id = other.id; + color = other.color; + } + }; + + //A list for saving information about the shapes + std::vector povShapes; + + //For formatted output + String outbuf; + + //For statistics + int nrNodes; + int nrSegments; + int nrShapes; + int idIndex; + + double minx; + double miny; + double maxx; + double maxy; + +}; + + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + + + +#endif /* EXTENSION_INTERNAL_POV_OUT_H */ + diff --git a/src/extension/internal/svg.cpp b/src/extension/internal/svg.cpp new file mode 100644 index 0000000..6eb7863 --- /dev/null +++ b/src/extension/internal/svg.cpp @@ -0,0 +1,1045 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the code that moves all of the SVG loading and saving into + * the module format. Really Inkscape is built to handle these formats + * internally, so this is just calling those internal functions. + * + * Authors: + * Lauris Kaplinski + * Ted Gould + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2002-2003 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include + +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "extension/output.h" +#include "extension/input.h" +#include "extension/system.h" +#include "file.h" +#include "svg.h" +#include "file.h" +#include "display/cairo-utils.h" +#include "extension/system.h" +#include "extension/output.h" +#include "xml/attribute-record.h" +#include "xml/simple-document.h" + +#include "object/sp-image.h" +#include "object/sp-root.h" +#include "object/sp-text.h" + +#include "util/units.h" +#include "selection-chemistry.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#include "clear-n_.h" + + +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; +using Inkscape::XML::Node; + +/* + * Removes all sodipodi and inkscape elements and attributes from an xml tree. + * used to make plain svg output. + */ +static void pruneExtendedNamespaces( Inkscape::XML::Node *repr ) +{ + if (repr) { + if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) { + std::vector attrsRemoved; + for ( List it = repr->attributeList(); it; ++it ) { + const gchar* attrName = g_quark_to_string(it->key); + if ((strncmp("inkscape:", attrName, 9) == 0) || (strncmp("sodipodi:", attrName, 9) == 0)) { + attrsRemoved.push_back(attrName); + } + } + // Can't change the set we're iterating over while we are iterating. + for (auto & it : attrsRemoved) { + repr->removeAttribute(it); + } + } + + std::vector nodesRemoved; + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + if((strncmp("inkscape:", child->name(), 9) == 0) || strncmp("sodipodi:", child->name(), 9) == 0) { + nodesRemoved.push_back(child); + } else { + pruneExtendedNamespaces(child); + } + } + for (auto & it : nodesRemoved) { + repr->removeChild(it); + } + } +} + +/* + * Similar to the above sodipodi and inkscape prune, but used on all documents + * to remove problematic elements (for example Adobe's i:pgf tag) only removes + * known garbage tags. + */ +static void pruneProprietaryGarbage( Inkscape::XML::Node *repr ) +{ + if (repr) { + std::vector nodesRemoved; + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + if((strncmp("i:pgf", child->name(), 5) == 0)) { + nodesRemoved.push_back(child); + g_warning( "An Adobe proprietary tag was found which is known to cause issues. It was removed before saving."); + } else { + pruneProprietaryGarbage(child); + } + } + for (auto & it : nodesRemoved) { + repr->removeChild(it); + } + } +} + +/** + * \return None + * + * \brief Create new markers where necessary to simulate the SVG 2 marker attribute 'orient' + * value 'auto-start-reverse'. + * + * \param repr The current element to check. + * \param defs A pointer to the element. + * \param css The properties of the element to check. + * \param property Which property to check, either 'marker' or 'marker-start'. + * + */ +static void remove_marker_auto_start_reverse(Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs, + SPCSSAttr *css, + Glib::ustring const &property) +{ + Glib::ustring value = sp_repr_css_property (css, property.c_str(), ""); + + if (!value.empty()) { + + // Find reference + static Glib::RefPtr regex = Glib::Regex::create("url\\(#([A-z0-9#]*)\\)"); + Glib::MatchInfo matchInfo; + regex->match(value, matchInfo); + + if (matchInfo.matches()) { + + std::string marker_name = matchInfo.fetch(1); + Inkscape::XML::Node *marker = sp_repr_lookup_child (defs, "id", marker_name.c_str()); + if (marker) { + + // Does marker use "auto-start-reverse"? + if (strncmp(marker->attribute("orient"), "auto-start-reverse", 17)==0) { + + // See if a reversed marker already exists. + Glib::ustring marker_name_reversed = marker_name + "_reversed"; + Inkscape::XML::Node *marker_reversed = + sp_repr_lookup_child (defs, "id", marker_name_reversed.c_str()); + + if (!marker_reversed) { + + // No reversed marker, need to create! + marker_reversed = repr->document()->createElement("svg:marker"); + + // Copy attributes + for (List iter = marker->attributeList(); + iter ; ++iter) { + marker_reversed->setAttribute(g_quark_to_string(iter->key), iter->value); + } + + // Override attributes + marker_reversed->setAttribute("id", marker_name_reversed); + marker_reversed->setAttribute("orient", "auto"); + + // Find transform + const char* refX = marker_reversed->attribute("refX"); + const char* refY = marker_reversed->attribute("refY"); + std::string transform = "rotate(180"; + if (refX) { + transform += ","; + transform += refX; + + if (refY) { + if (refX) { + transform += ","; + transform += refY; + } else { + transform += ",0,"; + transform += refY; + } + } + } + transform += ")"; + + // We can't set a transform on a marker... must create group first. + Inkscape::XML::Node *group = repr->document()->createElement("svg:g"); + group->setAttribute("transform", transform); + marker_reversed->addChild(group, nullptr); + + // Copy all marker content to group. + for (auto child = marker->firstChild() ; child != nullptr ; child = child->next() ) { + auto new_child = child->duplicate(repr->document()); + group->addChild(new_child, nullptr); + new_child->release(); + } + + // Add new marker to . + defs->addChild(marker_reversed, marker); + marker_reversed->release(); + } + + // Change url to reference reversed marker. + std::string marker_url("url(#" + marker_name_reversed + ")"); + sp_repr_css_set_property(css, "marker-start", marker_url.c_str()); + + // Also fix up if property is marker shorthand. + if (property == "marker") { + std::string marker_old_url("url(#" + marker_name + ")"); + sp_repr_css_unset_property(css, "marker"); + sp_repr_css_set_property(css, "marker-mid", marker_old_url.c_str()); + sp_repr_css_set_property(css, "marker-end", marker_old_url.c_str()); + } + + sp_repr_css_set(repr, css, "style"); + + } // Uses auto-start-reverse + } + } + } +} + +// Called by remove_marker_context_paint() for each property value ("marker", "marker-start", ...). +static void remove_marker_context_paint (Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs, + Glib::ustring property) +{ + // Value of 'marker', 'marker-start', ... property. + std::string value("url(#"); + value += repr->attribute("id"); + value += ")"; + + // Generate a list of elements that reference this marker. + std::vector to_fix_fill_stroke = + sp_repr_lookup_property_many(repr->root(), property, value); + + for (auto it: to_fix_fill_stroke) { + + // Figure out value of fill... could be inherited. + SPCSSAttr* css = sp_repr_css_attr_inherited (it, "style"); + Glib::ustring fill = sp_repr_css_property (css, "fill", ""); + Glib::ustring stroke = sp_repr_css_property (css, "stroke", ""); + + // Name of new marker. + Glib::ustring marker_fixed_id = repr->attribute("id"); + if (!fill.empty()) { + marker_fixed_id += "_F" + fill; + } + if (!stroke.empty()) { + marker_fixed_id += "_S" + stroke; + } + + // See if a fixed marker already exists. + // Could be more robust, assumes markers are direct children of . + Inkscape::XML::Node* marker_fixed = sp_repr_lookup_child(defs, "id", marker_fixed_id.c_str()); + + if (!marker_fixed) { + + // Need to create new marker. + + marker_fixed = repr->duplicate(repr->document()); + marker_fixed->setAttribute("id", marker_fixed_id); + + // This needs to be turned into a function that fixes all descendents. + for (auto child = marker_fixed->firstChild() ; child != nullptr ; child = child->next()) { + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( child, "style" ); + + Glib::ustring fill2 = sp_repr_css_property (css, "fill", ""); + if (fill2 == "context-fill" ) { + sp_repr_css_set_property (css, "fill", fill.c_str()); + } + if (fill2 == "context-stroke" ) { + sp_repr_css_set_property (css, "fill", stroke.c_str()); + } + + Glib::ustring stroke2 = sp_repr_css_property (css, "stroke", ""); + if (stroke2 == "context-fill" ) { + sp_repr_css_set_property (css, "stroke", fill.c_str()); + } + if (stroke2 == "context-stroke" ) { + sp_repr_css_set_property (css, "stroke", stroke.c_str()); + } + + sp_repr_css_set(child, css, "style"); + sp_repr_css_attr_unref(css); + } + + defs->addChild(marker_fixed, repr); + marker_fixed->release(); + } + + Glib::ustring marker_value = "url(#" + marker_fixed_id + ")"; + sp_repr_css_set_property (css, property.c_str(), marker_value.c_str()); + sp_repr_css_set (it, css, "style"); + sp_repr_css_attr_unref(css); + } +} + +static void remove_marker_context_paint (Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs) +{ + if (strncmp("svg:marker", repr->name(), 10) == 0) { + + if (!repr->attribute("id")) { + + std::cerr << "remove_marker_context_paint: without 'id'!" << std::endl; + + } else { + + // First see if we need to do anything. + bool need_to_fix = false; + + // This needs to be turned into a function that searches all descendents. + for (auto child = repr->firstChild() ; child != nullptr ; child = child->next()) { + + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( child, "style" ); + Glib::ustring fill = sp_repr_css_property (css, "fill", ""); + Glib::ustring stroke = sp_repr_css_property (css, "stroke", ""); + if (fill == "context-fill" || + fill == "context-stroke" || + stroke == "context-fill" || + stroke == "context-stroke" ) { + need_to_fix = true; + break; + } + sp_repr_css_attr_unref(css); + } + + if (need_to_fix) { + + // Now we need to search document for all elements that use this marker. + remove_marker_context_paint (repr, defs, "marker"); + remove_marker_context_paint (repr, defs, "marker-start"); + remove_marker_context_paint (repr, defs, "marker-mid"); + remove_marker_context_paint (repr, defs, "marker-end"); + } + } + } +} + +/* + * Recursively insert SVG 1.1 fallback for SVG 2 text (ignored by SVG 2 renderers including ours). + * Notes: + * Text must have been layed out. Access via old document. + */ +static void insert_text_fallback( Inkscape::XML::Node *repr, SPDocument *original_doc, Inkscape::XML::Node *defs = nullptr ) +{ + if (repr) { + + if (strncmp("svg:text", repr->name(), 8) == 0) { + + auto id = repr->attribute("id"); + // std::cout << "insert_text_fallback: found text! id: " << (id?id:"null") << std::endl; + + // We need to get original SPText object to access layout. + SPText* text = static_cast(original_doc->getObjectById( id )); + if (text == nullptr) { + std::cerr << "insert_text_fallback: bad cast" << std::endl; + return; + } + + if (!text->has_inline_size() && + !text->has_shape_inside()) { + // No SVG 2 text, nothing to do. + return; + } + + // We will keep this text node but replace all children. + + // For text in a shape, We need to unset 'text-anchor' or SVG 1.1 fallback won't work. + // Note 'text' here refers to original document while 'repr' refers to new document copy. + if (text->has_shape_inside()) { + SPCSSAttr *css = sp_repr_css_attr(repr, "style" ); + sp_repr_css_unset_property(css, "text-anchor"); + sp_repr_css_set(repr, css, "style"); + sp_repr_css_attr_unref(css); + } + + // We need to put trailing white space into it's own tspan for inline size so + // it is excluded during calculation of line position in SVG 1.1 renderers. + bool trim = text->has_inline_size() && + !(text->style->text_anchor.computed == SP_CSS_TEXT_ANCHOR_START); + + // Make a list of children to delete at end: + std::vector old_children; + for (auto child = repr->firstChild(); child; child = child->next()) { + old_children.push_back(child); + } + + // For round-tripping, xml:space (or 'white-space:pre') must be set. + repr->setAttribute("xml:space", "preserve"); + + Geom::Point text_anchor_point = text->layout.characterAnchorPoint(text->layout.begin()); + // std::cout << " text_anchor_point: " << text_anchor_point << std::endl; + + double text_x = 0.0; + double text_y = 0.0; + sp_repr_get_double(repr, "x", &text_x); + sp_repr_get_double(repr, "y", &text_y); + // std::cout << "text_x: " << text_x << " text_y: " << text_y << std::endl; + + // Loop over all lines in layout. + for (auto it = text->layout.begin() ; it != text->layout.end() ; ) { + + // Create a with 'x' and 'y' for each line. + Inkscape::XML::Node *line_tspan = repr->document()->createElement("svg:tspan"); + + // This could be useful if one wants to edit in an old version of Inkscape but we + // need to check if it breaks anything: + // line_tspan->setAttribute("sodipodi:role", "line"); + + // Hide overflow tspan (one line of text). + if (text->layout.isHidden(it)) { + line_tspan->setAttribute("style", "visibility:hidden"); + } + + Geom::Point line_anchor_point = text->layout.characterAnchorPoint(it); + double line_x = line_anchor_point[Geom::X]; + double line_y = line_anchor_point[Geom::Y]; + if (!text->is_horizontal()) { + std::swap(line_x, line_y); // Anchor points rotated & y inverted in vertical layout. + } + + // std::cout << " line_anchor_point: " << line_anchor_point << std::endl; + if (line_tspan->childCount() == 0) { + if (text->is_horizontal()) { + // std::cout << " horizontal: " << text_x << " " << line_anchor_point[Geom::Y] << std::endl; + if (text->has_inline_size()) { + // We use text_x as this is the reference for 'text-anchor' + // (line_x is the start of the line which gives wrong position when 'text-anchor' not start). + sp_repr_set_svg_double(line_tspan, "x", text_x); + } else { + // shape-inside (we don't have to worry about 'text-anchor'). + sp_repr_set_svg_double(line_tspan, "x", line_x); + } + sp_repr_set_svg_double(line_tspan, "y", line_y); // FIXME: this will pick up the wrong end of counter-directional runs + } else { + // std::cout << " vertical: " << line_anchor_point[Geom::X] << " " << text_y << std::endl; + sp_repr_set_svg_double(line_tspan, "x", line_x); // FIXME: this will pick up the wrong end of counter-directional runs + if (text->has_inline_size()) { + sp_repr_set_svg_double(line_tspan, "y", text_y); + } else { + sp_repr_set_svg_double(line_tspan, "y", line_y); + } + } + } + + // Inside line , create s for each change of style or shift. (No shifts in SVG 2 flowed text.) + // For simple lines, this creates an unneeded but so be it. + Inkscape::Text::Layout::iterator it_line_end = it; + it_line_end.nextStartOfLine(); + + // Find last span in line so we can put trailing whitespace in its own tspan for SVG 1.1 fallback. + Inkscape::Text::Layout::iterator it_last_span = it; + it_last_span.nextStartOfLine(); + it_last_span.prevStartOfSpan(); + + Glib::ustring trailing_whitespace; + + // Loop over chunks in line + while (it != it_line_end) { + + Inkscape::XML::Node *span_tspan = repr->document()->createElement("svg:tspan"); + + // use kerning to simulate justification and whatnot + Inkscape::Text::Layout::iterator it_span_end = it; + it_span_end.nextStartOfSpan(); + Inkscape::Text::Layout::OptionalTextTagAttrs attrs; + text->layout.simulateLayoutUsingKerning(it, it_span_end, &attrs); + + // 'dx' and 'dy' attributes are used to simulated justified text. + if (!text->is_horizontal()) { + std::swap(attrs.dx, attrs.dy); + } + TextTagAttributes(attrs).writeTo(span_tspan); + SPObject *source_obj = nullptr; + Glib::ustring::iterator span_text_start_iter; + text->layout.getSourceOfCharacter(it, &source_obj, &span_text_start_iter); + + // Set tspan style + Glib::ustring style_text = (dynamic_cast(source_obj) ? source_obj->parent : source_obj)->style->write( SP_STYLE_FLAG_IFDIFF, SP_STYLE_SRC_UNSET, text->style); + if (!style_text.empty()) { + span_tspan->setAttributeOrRemoveIfEmpty("style", style_text); + } + + // Add text node + SPString *str = dynamic_cast(source_obj); + if (str) { + Glib::ustring *string = &(str->string); // TODO fixme: dangerous, unsafe premature-optimization + SPObject *span_end_obj = nullptr; + Glib::ustring::iterator span_text_end_iter; + text->layout.getSourceOfCharacter(it_span_end, &span_end_obj, &span_text_end_iter); + if (span_end_obj != source_obj) { + if (it_span_end == text->layout.end()) { + span_text_end_iter = span_text_start_iter; + for (int i = text->layout.iteratorToCharIndex(it_span_end) - text->layout.iteratorToCharIndex(it) ; i ; --i) + ++span_text_end_iter; + } else + span_text_end_iter = string->end(); // spans will never straddle a source boundary + } + + if (span_text_start_iter != span_text_end_iter) { + Glib::ustring new_string; + while (span_text_start_iter != span_text_end_iter) + new_string += *span_text_start_iter++; // grr. no substr() with iterators + + if (it == it_last_span && trim) { + // Found last span in line + const auto s = new_string.find_last_not_of(" \t"); // Any other white space characters needed? + trailing_whitespace = new_string.substr(s+1, new_string.length()); + new_string.erase(s+1); + } + + Inkscape::XML::Node *new_text = repr->document()->createTextNode(new_string.c_str()); + span_tspan->appendChild(new_text); + Inkscape::GC::release(new_text); + // std::cout << " new_string: |" << new_string << "|" << std::endl; + } + } + it = it_span_end; + + // Add tspan to document + line_tspan->appendChild(span_tspan); + Inkscape::GC::release(span_tspan); + } + + // Add line tspan to document + repr->appendChild(line_tspan); + Inkscape::GC::release(line_tspan); + + // For center and end justified text, we need to remove any spaces and put them + // into a separate tspan (alignment is done by "text chunk" and spaces at ends of + // line will mess this up). + if (trim && trailing_whitespace.length() != 0) { + + Inkscape::XML::Node *space_tspan = repr->document()->createElement("svg:tspan"); + // Set either 'x' or 'y' to force a new text chunk. To do: this really should + // be positioned at the end of the line (overhanging). + if (text->is_horizontal()) { + sp_repr_set_svg_double(space_tspan, "y", line_y); + } else { + sp_repr_set_svg_double(space_tspan, "x", line_x); + } + Inkscape::XML::Node *space = repr->document()->createTextNode(trailing_whitespace.c_str()); + space_tspan->appendChild(space); + Inkscape::GC::release(space); + line_tspan->appendChild(space_tspan); + Inkscape::GC::release(space_tspan); + } + + } + + for (auto i: old_children) { + repr->removeChild (i); + } + + return; // No need to look at children of + } + + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + insert_text_fallback (child, original_doc, defs); + } + } +} + + +static void insert_mesh_polyfill( Inkscape::XML::Node *repr ) +{ + if (repr) { + + Inkscape::XML::Node *defs = sp_repr_lookup_name (repr, "svg:defs"); + + if (defs == nullptr) { + // We always put meshes in , no defs -> no mesh. + return; + } + + bool has_mesh = false; + for ( Node *child = defs->firstChild(); child; child = child->next() ) { + if (strncmp("svg:meshgradient", child->name(), 16) == 0) { + has_mesh = true; + break; + } + } + + Inkscape::XML::Node *script = sp_repr_lookup_child (repr, "id", "mesh_polyfill"); + + if (has_mesh && script == nullptr) { + + script = repr->document()->createElement("svg:script"); + script->setAttribute ("id", "mesh_polyfill"); + script->setAttribute ("type", "text/javascript"); + repr->root()->appendChild(script); // Must be last + + // Insert JavaScript via raw string literal. + Glib::ustring js = +#include "polyfill/mesh_compressed.include" +; + + Inkscape::XML::Node *script_text = repr->document()->createTextNode(js.c_str()); + script->appendChild(script_text); + } + } +} + + +static void insert_hatch_polyfill( Inkscape::XML::Node *repr ) +{ + if (repr) { + + Inkscape::XML::Node *defs = sp_repr_lookup_name (repr, "svg:defs"); + + if (defs == nullptr) { + // We always put meshes in , no defs -> no mesh. + return; + } + + bool has_hatch = false; + for ( Node *child = defs->firstChild(); child; child = child->next() ) { + if (strncmp("svg:hatch", child->name(), 16) == 0) { + has_hatch = true; + break; + } + } + + Inkscape::XML::Node *script = sp_repr_lookup_child (repr, "id", "hatch_polyfill"); + + if (has_hatch && script == nullptr) { + + script = repr->document()->createElement("svg:script"); + script->setAttribute ("id", "hatch_polyfill"); + script->setAttribute ("type", "text/javascript"); + repr->root()->appendChild(script); // Must be last + + // Insert JavaScript via raw string literal. + Glib::ustring js = +#include "polyfill/hatch_compressed.include" +; + + Inkscape::XML::Node *script_text = repr->document()->createTextNode(js.c_str()); + script->appendChild(script_text); + } + } +} + +/* + * Recursively transform SVG 2 to SVG 1.1, if possible. + */ +static void transform_2_to_1( Inkscape::XML::Node *repr, Inkscape::XML::Node *defs = nullptr ) +{ + if (repr) { + + // std::cout << "transform_2_to_1: " << repr->name() << std::endl; + + // Things we do once per node. ----------------------- + + // Find defs, if does not exist, create. + if (defs == nullptr) { + defs = sp_repr_lookup_name (repr, "svg:defs"); + } + if (defs == nullptr) { + defs = repr->document()->createElement("svg:defs"); + repr->root()->addChild(defs, nullptr); + } + + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( repr, "style" ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Individual items ---------------------------------- + + // SVG 2 marker attribute orient:auto-start-reverse: + if ( prefs->getBool("/options/svgexport/marker_autostartreverse", false) ) { + // Do "marker-start" first for efficiency reasons. + remove_marker_auto_start_reverse(repr, defs, css, "marker-start"); + remove_marker_auto_start_reverse(repr, defs, css, "marker"); + } + + // SVG 2 paint values 'context-fill', 'context-stroke': + if ( prefs->getBool("/options/svgexport/marker_contextpaint", false) ) { + remove_marker_context_paint(repr, defs); + } + + // *** To Do *** + // Context fill & stroke outside of markers + // Paint-Order + // Meshes + // Hatches + + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + transform_2_to_1 (child, defs); + } + + sp_repr_css_attr_unref(css); + } +} + + + + +/** + \return None + \brief What would an SVG editor be without loading/saving SVG + files. This function sets that up. + + For each module there is a call to Inkscape::Extension::build_from_mem + with a rather large XML file passed in. This is a constant string + that describes the module. At the end of this call a module is + returned that is basically filled out. The one thing that it doesn't + have is the key function for the operation. And that is linked at + the end of each call. +*/ +void +Svg::init() +{ + /* SVG in */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVG Input") "\n" + "" SP_MODULE_KEY_INPUT_SVG "\n" + SVG_COMMON_INPUT_PARAMS + "\n" + ".svg\n" + "image/svg+xml\n" + "" N_("Scalable Vector Graphic (*.svg)") "\n" + "" N_("Inkscape native file format and W3C standard") "\n" + "" SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "\n" + "\n" + "", new Svg()); + + /* SVG out Inkscape */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVG Output Inkscape") "\n" + "" SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "\n" + "\n" + ".svg\n" + "image/x-inkscape-svg\n" + "" N_("Inkscape SVG (*.svg)") "\n" + "" N_("SVG format with Inkscape extensions") "\n" + "false\n" + "\n" + "", new Svg()); + + /* SVG out */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVG Output") "\n" + "" SP_MODULE_KEY_OUTPUT_SVG "\n" + "\n" + ".svg\n" + "image/svg+xml\n" + "" N_("Plain SVG (*.svg)") "\n" + "" N_("Scalable Vector Graphics format as defined by the W3C") "\n" + "\n" + "", new Svg()); + + return; +} + + +/** + \return A new document just for you! + \brief This function takes in a filename of a SVG document and + turns it into a SPDocument. + \param mod Module to use + \param uri The path or URI to the file (UTF-8) + + This function is really simple, it just calls sp_document_new... + That's BS, it does all kinds of things for importing documents + that probably should be in a separate function. + + Most of the import code was copied from gdkpixpuf-input.cpp. +*/ +SPDocument * +Svg::open (Inkscape::Extension::Input *mod, const gchar *uri) +{ + // This is only used at the end... but it should go here once uri stuff is fixed. + auto file = Gio::File::create_for_commandline_arg(uri); + const auto path = file->get_path(); + + // Fixing this means fixing a whole string of things. + // if (path.empty()) { + // // We lied, the uri wasn't a uri, try as path. + // file = Gio::File::create_for_path(uri); + // } + + // std::cout << "Svg::open: uri in: " << uri << std::endl; + // std::cout << " : uri: " << file->get_uri() << std::endl; + // std::cout << " : scheme: " << file->get_uri_scheme() << std::endl; + // std::cout << " : path: " << file->get_path() << std::endl; + // std::cout << " : parse: " << file->get_parse_name() << std::endl; + // std::cout << " : base: " << file->get_basename() << std::endl; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Get import preferences. + bool ask_svg = prefs->getBool( "/dialogs/import/ask_svg"); + Glib::ustring import_mode_svg = prefs->getString("/dialogs/import/import_mode_svg"); + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + + // If we popped up a window asking about import preferences, get values from + // there and update preferences. + if(mod->get_gui() && ask_svg) { + ask_svg = !mod->get_param_bool("do_not_ask"); + import_mode_svg = mod->get_param_optiongroup("import_mode_svg"); + scale = mod->get_param_optiongroup("scale"); + + prefs->setBool( "/dialogs/import/ask_svg", ask_svg); + prefs->setString("/dialogs/import/import_mode_svg", import_mode_svg ); + prefs->setString("/dialogs/import/scale", scale ); + } + + // Do we "import" as ? + if (prefs->getBool("/options/onimport", false) && import_mode_svg != "include") { + // We import! + + // New wrapper document. + SPDocument * doc = SPDocument::createNewDoc (nullptr, true, true); + + // Imported document + // SPDocument * ret = SPDocument::createNewDoc(file->get_uri().c_str(), true); + SPDocument * ret = SPDocument::createNewDoc(uri, true); + + // Create image node + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *image_node = xml_doc->createElement("svg:image"); + + // Set default value as we honor "preserveAspectRatio". + image_node->setAttribute("preserveAspectRatio", "none"); + + double svgdpi = mod->get_param_float("svgdpi"); + image_node->setAttribute("inkscape:svg-dpi", Glib::ustring::format(svgdpi)); + + // What is display unit doing here? + Glib::ustring display_unit = doc->getDisplayUnit()->abbr; + double width = ret->getWidth().value(display_unit); + double height = ret->getHeight().value(display_unit); + image_node->setAttribute("width", Glib::ustring::format(width)); + image_node->setAttribute("height", Glib::ustring::format(height)); + + // This is actually "image-rendering" + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + if( scale != "auto") { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + sp_repr_css_set(image_node, css, "style"); + sp_repr_css_attr_unref( css ); + } + + // Do we embed or link? + if (import_mode_svg == "embed") { + std::unique_ptr pb(Inkscape::Pixbuf::create_from_file(uri, svgdpi)); + if(pb) { + sp_embed_svg(image_node, uri); + } + } else { + // Convert filename to uri (why do we need to do this, we claimed it was already a uri). + gchar* _uri = g_filename_to_uri(uri, nullptr, nullptr); + if(_uri) { + // if (strcmp(_uri, uri) != 0) { + // std::cout << "Svg::open: _uri != uri! " << _uri << ":" << uri << std::endl; + // } + image_node->setAttribute("xlink:href", _uri); + g_free(_uri); + } else { + image_node->setAttribute("xlink:href", uri); + } + } + + // Add the image to a layer. + Inkscape::XML::Node *layer_node = xml_doc->createElement("svg:g"); + layer_node->setAttribute("inkscape:groupmode", "layer"); + layer_node->setAttribute("inkscape:label", "Image"); + doc->getRoot()->appendChildRepr(layer_node); + layer_node->appendChild(image_node); + Inkscape::GC::release(image_node); + Inkscape::GC::release(layer_node); + fit_canvas_to_drawing(doc); + + // Set viewBox if it doesn't exist. What is display unit doing here? + if (!doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + return doc; + } + + // We are not importing as . Open as new document. + + // Try to open non-local file (when does this occur?). + if (!file->get_uri_scheme().empty()) { + if (path.empty()) { + try { + char *contents; + gsize length; + file->load_contents(contents, length); + return SPDocument::createNewDocFromMem(contents, length, true); + } catch (Gio::Error &e) { + g_warning("Could not load contents of non-local URI %s\n", uri); + return nullptr; + } + } else { + // Do we ever get here and does this actually work? + uri = path.c_str(); + } + } + + SPDocument *doc = SPDocument::createNewDoc(uri, true); + // SPDocument *doc = SPDocument::createNewDoc(file->get_uri().c_str(), true); + return doc; +} + +/** + \return None + \brief This is the function that does all of the SVG saves in + Inkscape. It detects whether it should do a Inkscape + namespace save internally. + \param mod Extension to use. + \param doc Document to save. + \param uri The filename to save the file to. + + This function first checks its parameters, and makes sure that + we're getting good data. It also checks the module ID of the + incoming module to figure out whether this save should include + the Inkscape namespace stuff or not. The result of that comparison + is stored in the exportExtensions variable. + + If there is not to be Inkscape name spaces a new document is created + without. (I think, I'm not sure on this code) + + All of the internally referenced imageins are also set to relative + paths in the file. And the file is saved. + + This really needs to be fleshed out more, but I don't quite understand + all of this code. I just stole it. +*/ +void +Svg::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + g_return_if_fail(doc != nullptr); + g_return_if_fail(filename != nullptr); + Inkscape::XML::Document *rdoc = doc->getReprDoc(); + + bool const exportExtensions = ( !mod->get_id() + || !strcmp (mod->get_id(), SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE) + || !strcmp (mod->get_id(), SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool const transform_2_to_1_flag = + prefs->getBool("/dialogs/save_as/enable_svgexport", false); + + bool const insert_text_fallback_flag = + prefs->getBool("/options/svgexport/text_insertfallback", true); + bool const insert_mesh_polyfill_flag = + prefs->getBool("/options/svgexport/mesh_insertpolyfill", true); + bool const insert_hatch_polyfill_flag = + prefs->getBool("/options/svgexport/hatch_insertpolyfill", true); + + bool createNewDoc = + !exportExtensions || + transform_2_to_1_flag || + insert_text_fallback_flag || + insert_mesh_polyfill_flag || + insert_hatch_polyfill_flag; + + // We prune the in-use document and deliberately loose data, because there + // is no known use for this data at the present time. + pruneProprietaryGarbage(rdoc->root()); + + if (createNewDoc) { + + // We make a duplicate document so we don't prune the in-use document + // and loose data. Perhaps the user intends to save as inkscape-svg next. + Inkscape::XML::Document *new_rdoc = new Inkscape::XML::SimpleDocument(); + + // Comments and PI nodes are not included in this duplication + // TODO: Move this code into xml/document.h and duplicate rdoc instead of root. + new_rdoc->setAttribute("standalone", "no"); + new_rdoc->setAttribute("version", "2.0"); + + // Get a new xml repr for the svg root node + Inkscape::XML::Node *root = rdoc->root()->duplicate(new_rdoc); + + // Add the duplicated svg node as the document's rdoc + new_rdoc->appendChild(root); + Inkscape::GC::release(root); + + if (!exportExtensions) { + pruneExtendedNamespaces(root); + } + + if (transform_2_to_1_flag) { + transform_2_to_1 (root); + new_rdoc->setAttribute("version", "1.1"); + } + + if (insert_text_fallback_flag) { + insert_text_fallback (root, doc); + } + + if (insert_mesh_polyfill_flag) { + insert_mesh_polyfill (root); + } + + if (insert_hatch_polyfill_flag) { + insert_hatch_polyfill (root); + } + + rdoc = new_rdoc; + } + + if (!sp_repr_save_rebased_file(rdoc, filename, SP_SVG_NS_URI, + doc->getDocumentBase(), // + m_detachbase ? nullptr : filename)) { + throw Inkscape::Extension::Output::save_failed(); + } + + if (createNewDoc) { + Inkscape::GC::release(rdoc); + } + + return; +} + +} } } /* namespace inkscape, module, implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svg.h b/src/extension/internal/svg.h new file mode 100644 index 0000000..a7adba4 --- /dev/null +++ b/src/extension/internal/svg.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the code that moves all of the SVG loading and saving into + * the module format. Really Sodipodi is built to handle these formats + * internally, so this is just calling those internal functions. + * + * Authors: + * Lauris Kaplinski + * Ted Gould + * + * Copyright (C) 2002-2003 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __SVG_H__ +#define __SVG_H__ + +#include "../implementation/implementation.h" + +#define SVG_COMMON_INPUT_PARAMS \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "96.00\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "false\n" + + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class Svg : public Inkscape::Extension::Implementation::Implementation { + bool m_detachbase = false; + +public: + void setDetachBase(bool detach) override { m_detachbase = detach; } + + void save( Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename ) override; + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* __SVG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svgz.cpp b/src/extension/internal/svgz.cpp new file mode 100644 index 0000000..ea00f94 --- /dev/null +++ b/src/extension/internal/svgz.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code to handle compressed SVG loading and saving. Almost identical to svg + * routines, but separated for simpler extension maintenance. + * + * Authors: + * Lauris Kaplinski + * Ted Gould + * Jon A. Cruz + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "svgz.h" +#include "extension/extension.h" +#include "extension/system.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#include "clear-n_.h" + +/** + \return None + \brief What would an SVG editor be without loading/saving SVG + files. This function sets that up. + + For each module there is a call to Inkscape::Extension::build_from_mem + with a rather large XML file passed in. This is a constant string + that describes the module. At the end of this call a module is + returned that is basically filled out. The one thing that it doesn't + have is the key function for the operation. And that is linked at + the end of each call. +*/ +void +Svgz::init() +{ + /* SVGZ in */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVGZ Input") "\n" + "" SP_MODULE_KEY_INPUT_SVGZ "\n" + "" SP_MODULE_KEY_INPUT_SVG "\n" + SVG_COMMON_INPUT_PARAMS + "\n" + ".svgz\n" + "image/svg+xml-compressed\n" + "" N_("Compressed Inkscape SVG (*.svgz)") "\n" + "" N_("SVG file format compressed with GZip") "\n" + "" SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE "\n" + "\n" + "", new Svgz()); + + /* SVGZ out Inkscape */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVGZ Output") "\n" + "" SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE "\n" + "\n" + ".svgz\n" + "image/x-inkscape-svg-compressed\n" + "" N_("Compressed Inkscape SVG (*.svgz)") "\n" + "" N_("Inkscape's native file format compressed with GZip") "\n" + "false\n" + "\n" + "", new Svgz()); + + /* SVGZ out */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("SVGZ Output") "\n" + "" SP_MODULE_KEY_OUTPUT_SVGZ "\n" + "\n" + ".svgz\n" + "image/svg+xml-compressed\n" + "" N_("Compressed plain SVG (*.svgz)") "\n" + "" N_("Scalable Vector Graphics format compressed with GZip") "\n" + "\n" + "\n", new Svgz()); + + return; +} + + +} } } // namespace inkscape, module, implementation + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svgz.h b/src/extension/internal/svgz.h new file mode 100644 index 0000000..e923c4c --- /dev/null +++ b/src/extension/internal/svgz.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code to handle compressed SVG loading and saving. Almost identical to svg + * routines, but separated for simpler extension maintenance. + * + * Authors: + * Lauris Kaplinski + * Ted Gould + * Jon A. Cruz + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SVGZ_H +#define SEEN_SVGZ_H + +#include "svg.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class Svgz : public Svg { +public: + static void init( ); +}; + +} } } // namespace Inkscape, Extension, Implementation +#endif // SEEN_SVGZ_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/text_reassemble.c b/src/extension/internal/text_reassemble.c new file mode 100644 index 0000000..9fdd0a5 --- /dev/null +++ b/src/extension/internal/text_reassemble.c @@ -0,0 +1,2975 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * text_reassemble.c from libTERE + *//* + * Authors: see below + * + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2.0+, read the file 'COPYING' for more information. + */ + + +/** + @file text_reassemble.c + +\verbatim +Method: + 1. For all ordered text objects which are sequential and share the same esc. + 2. For the first only pull x,y,esc and save, these define origin and rotation. + 3. Save the text object. + 4. Phase I: For all saved text objects construct lines. + 5. Check for allowed overlaps on sequential saved text object bounding rectangles. + 6 If found merge second with first, check next one. + 7. If not found, start a new complex (line). + 8. Phase II; for all lines construct paragraphs. + 9. Check alignment and line spacing of preceding line with current line. + 10. if alignment is the same, and line spacing is compatible merge current line into + current paragraph. Reaverage line spacing over all lines in paragraph. Check next one. + 11. If alignment does not match start a new paragraph. + (Test program) + 12. Over all phase II paragraphs + 13. Over all phase I lines in each paragraph. + 14. Over all text objects in each line. + Emit SVG corresponding to this construct to a file dump.svg. + (Conversion to other file types would be modeled on this example.) + 15. Clean up. + (General program) + Like for the Test program, but final representation may not be SVG. + Text object and bounding rectangle memory would all be released. If another set of + text will be processed then hang onto both Freetype and Fontconfig structures. If no + other text will be processed here, then also release Freetype structures. If the caller uses + Fontconfig elsewhere then do not release it, otherwise, do so. + +NOTE ON COORDINATES: x is positive to the right, y is positive down. So (0,0) is the upper left corner, and the +lower left corner of a rectangle has a LARGER Y coordinate than the upper left. Ie, LL=(10,10) UR=(30,5) is typical. +\endverbatim +*/ + +/* + +Compilation of test program (with all debugging output, but not loop testing): +On Windows use: + + gcc -Wall -DWIN32 -DTEST -DDBG_TR_PARA -DDBG_TR_INPUT \ + -I. -I/c/progs/devlibs32/include -I/c/progs/devlibs32/include/freetype2\ + -o text_reassemble text_reassemble.c uemf_utf.c \ + -lfreetype6 -lfontconfig-1 -liconv -lm -L/c/progs/devlibs32/bin + +On Linux use: + + gcc -Wall -DTEST -DDBG_TR_PARA -DDBG_TR_INPUT -I. -I/usr/include/freetype2 -o text_reassemble text_reassemble.c uemf_utf.c -lfreetype -lfontconfig -lm + +Compilation of object file only (Windows): + + gcc -Wall -DWIN32 -c \ + -I. -I/c/progs/devlibs32/include -I/c/progs/devlibs32/include/freetype2\ + text_reassemble.c + +Compilation of object file only (Linux): + gcc -Wall -c -I. -I/usr/include/freetype2 text_reassemble.c + + +Optional compiler switches for development: + -DDBG_TR_PARA draw bounding rectangles for paragraphs in SVG output + -DDBG_TR_INPUT draw input text and their bounding rectangles in SVG output + -DTEST build the test program + -DDBG_LOOP force the test program to cycle 5 times. Useful for finding + memory leaks. Output file is overwritten each time. + + +File: text_reassemble.c +Version: 0.0.18 +Date: 11-MAR-2016 +Author: David Mathog, Biology Division, Caltech +email: mathog@caltech.edu +Copyright: 2016 David Mathog and California Institute of Technology (Caltech) +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "text_reassemble.h" +#include <3rdparty/libuemf/uemf_utf.h> /* For a couple of text functions. Exact copy from libUEMF. */ +#include +#include + +/* Code generated by make_ucd_mn_table.c using: + cat mnlist.txt | ./make_ucd_mn_table >generated.c +*/ +#include +int is_mn_unicode(int test){ +#define MN_TEST_LIMIT 921600 +#define N_SPAGES 225 +#define N_CPAGES 192 +#define N_CPDATA 344 +#define C_PER_S 16 + uint8_t superpages[N_SPAGES]={ + 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x06, + 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0B}; + + uint8_t cpages[N_CPAGES]={ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x19, 0x00, 0x00, + 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x21, 0x00, + 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x25, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, + 0x00, 0x28, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + uint32_t cpage_data[N_CPDATA]={ + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000FFFF, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000F8, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFE0000, 0xBFFFFFFF, 0x000000B6, 0x00000000, + 0x07FF0000, 0x00000000, 0xFFFFF800, 0x00010000, 0x00000000, 0x00000000, 0x9FC00000, 0x00003D9F, + 0x00020000, 0xFFFF0000, 0x000007FF, 0x00000000, 0x00000000, 0x0001FFC0, 0x00000000, 0x000FF800, + 0xFBC00000, 0x00003EEF, 0x0E000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x7FFFFFF0, + 0x00000007, 0x14000000, 0x00FE21FE, 0x0000000C, 0x00000002, 0x10000000, 0x0000201E, 0x0000000C, + 0x00000006, 0x10000000, 0x00023986, 0x00230000, 0x00000006, 0x10000000, 0x000021BE, 0x0000000C, + 0x00000002, 0x90000000, 0x0040201E, 0x0000000C, 0x00000004, 0x00000000, 0x00002001, 0x00000000, + 0x00000000, 0xC0000000, 0x00603DC1, 0x0000000C, 0x00000000, 0x90000000, 0x00003040, 0x0000000C, + 0x00000000, 0x00000000, 0x0000201E, 0x0000000C, 0x00000000, 0x00000000, 0x005C0400, 0x00000000, + 0x00000000, 0x07F20000, 0x00007F80, 0x00000000, 0x00000000, 0x1BF20000, 0x00003F00, 0x00000000, + 0x03000000, 0x02A00000, 0x00000000, 0x7FFE0000, 0xFEFFE0DF, 0x1FFFFFFF, 0x00000040, 0x00000000, + 0x00000000, 0x66FDE000, 0xC3000000, 0x001E0001, 0x20002064, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0xE0000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x001C0000, 0x001C0000, 0x000C0000, 0x000C0000, 0x00000000, 0x3FB00000, 0x200FFE40, 0x00000000, + 0x00003800, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000200, 0x00000000, 0x00000000, + 0x00000000, 0x0E040187, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x01800000, 0x00000000, 0x7F400000, 0x9FF81FE5, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x0000000F, 0x17D00000, 0x00000004, 0x000FF800, 0x00000003, 0x00000B3C, 0x00000000, 0x0003A340, + 0x00000000, 0x00CFF000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFF70000, 0x001021FD, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF, 0xF000007F, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x1FFF0000, 0x0001FFE2, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00038000, + 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF, + 0x00000000, 0x00003C00, 0x00000000, 0x00000000, 0x06000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x3FF08000, 0x80000000, 0x00000000, 0x00000000, 0x00030000, + 0x00000844, 0x00000060, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000010, 0x0003FFFF, + 0x00000000, 0x00003FC0, 0x0003FF80, 0x00000000, 0x00000007, 0x13C80000, 0x00000000, 0x00000000, + 0x00000000, 0x00667E00, 0x00001008, 0x00000000, 0x00000000, 0xC19D0000, 0x00000002, 0x00403000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00002120, + 0x40000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x0000FFFF, 0x0000007F, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x20000000, + 0x0000F06E, 0x87000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000002, 0xFF000000, 0x0000007F, 0x00000000, 0x00000003, 0x06780000, 0x00000000, 0x00000000, + 0x00000007, 0x001FEF80, 0x00000000, 0x00000000, 0x00000003, 0x7FC00000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00BF2800, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00078000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0xF8000380, 0x00000FE7, 0x00003C00, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x0000001C, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000FFFF}; + + int result=0; + + int spage_idx; + int cpage_row, cpage_column, cpage_idx; + int cd_row, cd_column, cd_idx, cd_bit; + + if(test> 12; + cpage_row = superpages[spage_idx]; + cpage_column = (test>>8) & 15; + cpage_idx = C_PER_S*cpage_row + cpage_column; + cd_row = cpages[cpage_idx]; + cd_column = test>>5 & 7; + cd_idx = 8*cd_row + cd_column; + cd_bit = test & 31; + result = cpage_data[cd_idx] & (1 << cd_bit); + } + return(result); +} + + + +/** + \brief Find a (sub)string in a caseinvariant manner, used for locating "Narrow" in font name + \return Returns -1 if no match, else returns the position (numbered from 0) of the first character of the match. + \param string Text to search + \param sub Text to find +*/ +int TR_findcasesub(const char *string, const char *sub){ + int i,j; + int match=0; + for(i=0; string[i]; i++){ + for(match=1,j=0; sub[j] && string[i+j]; j++){ + if(toupper(sub[j]) != toupper(string[i+j])){ + match=0; + break; + } + } + if(match && !sub[j])break; /* matched over the entire substring */ + } + return((match ? i : -1)); +} + +/** + \brief Constrouct a fontspec from a TCHUNK_SPECS and a fontname + \return Returns NULL on error, new fontspec on success + \param tsp pointer to TCHUNK_SPECS to use for information + \param fontname Fontname to use in the new fontspec +*/ + /* construct a font name */ +char *TR_construct_fontspec(const TCHUNK_SPECS *tsp, const char *fontname){ + int newlen = 128 + strlen(fontname); /* too big, but not by much */ + char *newfs = NULL; + newfs = (char *) malloc(newlen); + sprintf(newfs,"%s:slant=%d:weight=%d:size=%f:width=%d",fontname,tsp->italics,tsp->weight,tsp->fs,(tsp->co ? 75 : tsp->condensed)); + return(newfs); +} + + +/** + \brief Reconstrouct a fontspec by substituting a font name into an existing spec + \return Returns NULL on error, new fontspec on success + \param fontspec Original fontspec, only the name will be changed + \param fontname Fontname to substitute into the new fontspec +*/ + /* construct a font name */ +char *TR_reconstruct_fontspec(const char *fontspec, const char *fontname){ + int colon; + int newlen = strlen(fontspec) + strlen(fontname) + 1; /* too big, but not by much */ + char *newfs = NULL; + newfs = (char *) malloc(newlen); + colon = strcspn(fontspec,":"); + if(colon){ sprintf(newfs,"%s%s",fontname,&fontspec[colon]); } + return(newfs); +} + +/** + \brief Find a font in the list that has a glyph for this character, change alternate to match + \return Returns 0 if no match or an error, else returns the glyph index in the new alternate font + \param fti pointer to the FT_INFO structure, may be modified if alternate font is added + \param efsp Pointer to a Pointer to the original FNT_SPECS struct. On return contains the FNT_SPECS corresponding to the glyph_index.. + \param wc Current character (32 bit int) +*/ +int TR_find_alternate_font(FT_INFO *fti, FNT_SPECS **efsp, uint32_t wc){ + int glyph_index=0; /* this is the unknown character glyph */ + uint32_t i; + FcCharSet *cs; + FcResult result = FcResultMatch; + FcPattern *pattern, *fpat; + char *filename; + char *fontname; + char *newfontspec; + int fi_idx; + FNT_SPECS *fsp,*fsp2; + if(!fti || !efsp || !*efsp)return(0); + fsp = *efsp; + for(i=0;iused;i++){ /* first check in alts */ + fsp2 = &fti->fonts[fsp->alts[i].fi_idx]; /* these are in order of descending previous usage */ + glyph_index = FT_Get_Char_Index( fsp2->face, wc); /* we have the face, might as well check that directly */ + if (glyph_index){ /* found a glyph for the character in this font */ + (void) fsp_alts_weight(fsp, i); + *efsp = fsp2; + return(glyph_index); + } + } + + /* it was not in alts, now go through fontset and see if it is in there */ + for(i=1; i< (unsigned int) fsp->fontset->nfont;i++){ /* already know the primary does not have this character */ + result = FcPatternGetCharSet(fsp->fontset->fonts[i], FC_CHARSET, 0, &cs); + if(result != FcResultMatch) return(0); /* some terrible problem, this should never happen */ + if (FcCharSetHasChar(cs, wc)){ /* found a glyph for the character in this font */ + glyph_index = i; + + /* Do a lot of work to find the filename corresponding to the fontset entry. + None of these should ever fail, but if one does, return 0 + */ + if( + !(pattern = FcNameParse((const FcChar8 *)&(fsp->fontspec))) || + !FcConfigSubstitute(NULL, pattern, FcMatchPattern) + )return(0); + FcDefaultSubstitute(pattern); + if( + !(fpat = FcFontRenderPrepare(NULL, pattern, fsp->fontset->fonts[i])) || + (FcPatternGetString( fpat, FC_FILE, 0, (FcChar8 **)&filename) != FcResultMatch) || + (FcPatternGetString( fsp->fontset->fonts[i], FC_FULLNAME, 0, (FcChar8 **)&fontname) != FcResultMatch) + )return(0); + + /* find the font (added from an unrelated fontset, for instance) or insert it as new */ + fi_idx = ftinfo_find_loaded_by_src(fti, (uint8_t *) filename); + if(fi_idx < 0){ + newfontspec = TR_reconstruct_fontspec((char *) fsp->fontspec, fontname); + fi_idx = ftinfo_load_fontname(fti, newfontspec); + free(newfontspec); + if(fi_idx < 0)return(0); /* This could happen if we run out of memory*/ + } + + /* add the new font index to the alts list on the (current) fsp. */ + (void) fsp_alts_insert(fsp, fi_idx); + + /* release FC's own memory related to this call that does not need to be kept around so that face will work */ + FcPatternDestroy(pattern); + + *efsp = &(fti->fonts[fi_idx]); + return(glyph_index); + } + } + + return(0); +} + +/** + \brief Get the advance for the 32 bit character + + \return Returns -1 on error, or advance in units of 1/64th of a Point. + \param fti pointer to the FT_INFO structure, may be modified if alternate font is required + \param fsp Pointer to FNT_SPECS struct. + \param wc Current character (32 bit int) + \param pc Previous character + \param load_flags Controls internal advance: + FT_LOAD_NO_SCALE, internal advance is in 1/64th of a point. (kerning values are still scaled) + FT_LOAD_TARGET_NORMAL internal advance is in 1/64th of a point. The scale + factor seems to be (Font Size in points)*(DPI)/(32.0 pnts)*(72 dpi). + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. + \param ymin If the pointer is defined, the value is adjusted if ymin of wc character is less than the current value. + \param ymax If the pointer is defined, the value is adjusted if ymin of wc character is more than the current value. +*/ +int TR_getadvance(FT_INFO *fti, FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int load_flags, int kern_mode, int *ymin, int *ymax){ + FT_Glyph glyph; + int glyph_index; + int advance=-1; + FT_BBox bbox; + + if(is_mn_unicode(wc))return(0); /* no advance on Unicode Mn characters */ + + glyph_index = FT_Get_Char_Index( fsp->face, wc); + if(!glyph_index){ /* not in primary font, check alternates */ + glyph_index = TR_find_alternate_font(fti, &fsp, wc); + } + if(glyph_index){ + if (!FT_Load_Glyph( fsp->face, glyph_index, load_flags )){ + if ( !FT_Get_Glyph( fsp->face->glyph, &glyph ) ) { + advance = fsp->face->glyph->advance.x; + FT_Glyph_Get_CBox( glyph, FT_GLYPH_BBOX_UNSCALED, &bbox ); + if(ymin && (bbox.yMin < *ymin))*ymin=bbox.yMin; + if(ymax && (bbox.yMax > *ymax))*ymax=bbox.yMax; + if(pc)advance += TR_getkern2(fsp, wc, pc, kern_mode); + FT_Done_Glyph(glyph); + } + } + } + /* If there was no way to determine the width, this returns the error value */ + return(advance); +} + +/** + \brief Get the kerning for a pair of 32 bit characters + \return Returns 0 on error, or kerning value (which may be 0) for the pair in units of 1/64th of a point. + \param fsp Pointer to FNT_SPECS struct. + \param wc Current character (32 bit int) + \param pc Previous character + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int TR_getkern2(FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int kern_mode){ + int this_glyph_index; + int prev_glyph_index; + int kern=0; + FT_Vector akerning; + + this_glyph_index = FT_Get_Char_Index( fsp->face, wc); + prev_glyph_index = FT_Get_Char_Index( fsp->face, pc); + if(!FT_Get_Kerning( fsp->face, + prev_glyph_index, + this_glyph_index, + kern_mode, + &akerning )){ + kern = akerning.x; /* Is sign correct? */ + } + return(kern); +} + +/** + \brief Get the kerning for a pair of 32 bit characters, where one is the last character in the previous text block, and the other is the first in the current text block. + \return Returns 0 on error, or kerning value (which may be 0) for the pair in units of 1/64th of a point. + \param fsp Pointer to FNT_SPECS struct. + \param tsp current text object + \param ptsp previous text object + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int TR_kern_gap(FNT_SPECS *fsp, TCHUNK_SPECS *tsp, TCHUNK_SPECS *ptsp, int kern_mode){ + int kern=0; + uint32_t *text32=NULL; + uint32_t *ptxt32=NULL; + size_t tlen,plen; + while(ptsp && tsp){ + text32 = U_Utf8ToUtf32le((char *) tsp->string, 0, &tlen); + if(!text32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + text32 = U_Latin1ToUtf32le((char *) tsp->string,0, &tlen); + if(!text32)break; + } + ptxt32 = U_Utf8ToUtf32le((char *) ptsp->string,0,&plen); + if(!ptxt32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + ptxt32 = U_Latin1ToUtf32le((char *) ptsp->string,0, &plen); + if(!ptxt32)break; + } + kern = TR_getkern2(fsp, *text32, ptxt32[plen-1], kern_mode); + break; + } + if(text32)free(text32); + if(ptxt32)free(ptxt32); + return(kern); +} + + + + +/** + \brief Find baseline on Y axis of a complex. + If the complex is a TR_TEXT or TR_LINE find its baseline. + If the complex is TR_PARA_[UCLR]J find the baseline of the last line. + If there are multiple text elements in a TR_LINE, the baseline is that of the + element that uses the largest font. This will definitely give the wrong + result if that line starts with a super or subscript that is full font size, but + they are usually smaller. + \return Returns 0 if it cannot determine a baseline, else returns the baseline Y coordinate. + \param tri pointer to the TR_INFO structure holding all TR data + \param src index of the current complex + \param ymax If the pointer is defined, the value is adjusted if ymax of current complex is more than the current value. + \param ymin If the pointer is defined, the value is adjusted if ymin of current complex is less than the current value. +*/ +double TR_baseline(TR_INFO *tri, int src, double *ymax, double *ymin){ + double baseline=0; + volatile double tmp=0.0; /* This MUST be volatile */ + double yheight; + int last; + int i; + int trec; + CX_INFO *cxi=tri->cxi; + BR_INFO *bri=tri->bri; + TP_INFO *tpi=tri->tpi; + FT_INFO *fti=tri->fti; + FNT_SPECS *fsp; + last = cxi->cx[src].kids.used - 1; + switch (cxi->cx[src].type){ + case TR_TEXT: + trec = cxi->cx[src].kids.members[0]; /* for this complex type there is only ever one member */ + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + fsp = &(fti->fonts[tpi->chunks[trec].fi_idx]); + yheight = fsp->face->bbox.yMax - fsp->face->bbox.yMin; + if(ymax){ + tmp = tpi->chunks[trec].fs * ((double)fsp->face->bbox.yMax/yheight); + if(*ymax <= tmp)*ymax = tmp; + } + else if(ymin){ + tmp = tpi->chunks[trec].fs * ((double)-fsp->face->bbox.yMin/yheight); /* yMin in face is negative */ + if(*ymin <= tmp)*ymin = tmp; + } + break; + case TR_LINE: + for(i=last;i>=0;i--){ /* here last is the count of text objects in the complex */ + trec = cxi->cx[src].kids.members[i]; + fsp = &(fti->fonts[tpi->chunks[trec].fi_idx]); + yheight = fsp->face->bbox.yMax - fsp->face->bbox.yMin; + if(ymax){ + tmp = tpi->chunks[trec].fs * (((double)fsp->face->bbox.yMax)/yheight); + /* gcc 4.6.3 had a bizarre optimization error for -O2 and -O3 where *ymax <= tmp was + not true when *ymax == tmp, as verified by examining the binary representations. + This was apparently due to retained excess precision. Making tmp volatile + forces it to be stored into a 64 bit location, dropping the extra 12 bits from + the 80 bit register. */ + if(*ymax <= tmp){ + *ymax = tmp; + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + } + } + else if(ymin){ + tmp = tpi->chunks[trec].fs * (((double)-fsp->face->bbox.yMin)/yheight); /* yMin in face is negative */ + if(*ymin <= tmp){ + *ymin = tmp; + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + } + } + } + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + case TR_PARA_CJ: + case TR_PARA_RJ: + trec = cxi->cx[src].kids.members[last]; + baseline = TR_baseline(tri, trec, ymax, ymin); + break; + } + return(baseline); +} + +/** + \brief Check or set vertical advance on the growing complex relative to the current complex. + Vadvance is a multiplicative factor like 1.25. + The distance between successive baselines is vadvance * max(font_size), where the maximum + is over all text elements in src. + The growing complex is always the last one in the CX_INFO section of the TR_INFO structure. + If an existing vadvance does not match the one which would be required to fit the next complex + to add to the growing one, it terminates a growing complex. (Ie, starts a new paragraph.) + Find baseline on Y axis of a complex. + If the complex is a TR_TEXT or TR_LINE find its baseline. + If the complex is TR_PARA+* find the baseline of the last line. + If there are multiple text elements in a TR_LINE, the baseline is that of the + element that uses the largest font. This will definitely give the wrong + result if that line starts with a super or subscript that is full font size, but + they are usually smaller. + \return Returns 0 on success, !0 on failure. + \param tri pointer to the TR_INFO structure holding all TR data + \param src index of the current complex, to be added to the growing complex. + This lets the value of "src - lines" determine the weight to give to each new vadvance value + as it is merged into the running weighted average. This improves the accuracy of the vertical advance, + since there can be some noise introduced when lines have different maximum font sizes. + \param lines index of the first text block that was added to the growing complex. +*/ +int TR_check_set_vadvance(TR_INFO *tri, int src, int lines){ + int status = 0; + CX_INFO *cxi = tri->cxi; + TP_INFO *tpi = tri->tpi; + double ymax = DBL_MIN; + double ymin = DBL_MIN; + double prevbase; + double thisbase; + double weight; + int trec; + double newV; + int dst; + + dst = cxi->used-1; /* complex being grown */ + + prevbase = TR_baseline(tri, dst, NULL, &ymin); + thisbase = TR_baseline(tri, src, &ymax, NULL); + newV = (thisbase - prevbase)/(ymax + ymin); + trec = cxi->cx[dst].kids.members[0]; /* complex whose first text record holds vadvance for this complex */ + trec = cxi->cx[trec].kids.members[0]; /* text record that halds vadvance for this complex */ + if(tpi->chunks[trec].vadvance){ + /* already set on the first text (only place it is stored.) + See if the line to be added is compatible. + All text fields in a complex have the same advance, so just set/check the first one. + vadvance must be within 1% or do not add a new line */ + if(fabs(1.0 - (tpi->chunks[trec].vadvance/newV)) > 0.01){ + status = 1; + } + else { /* recalculate the weighted vadvance */ + weight = (1.0 / (double) (src - lines)); + tpi->chunks[trec].vadvance = tpi->chunks[trec].vadvance*(1.0-weight) + newV*weight; + } + } + else { /* only happens when src = lines + 1*/ + tpi->chunks[trec].vadvance = newV; + } + return(status); +} + + +/** + \brief Initialize an FT_INFO structure. Sets up a freetype library to use in this context. + \returns a pointer to the FT_INFO structure created, or NULL on error. +*/ +FT_INFO *ftinfo_init(void){ + FT_INFO *fti = NULL; + if(FcInit()){ + fti = (FT_INFO *)calloc(1,sizeof(FT_INFO)); + if(fti){ + if(!FT_Init_FreeType( &(fti->library))){ + fti->space=0; + fti->used=0; + + if(ftinfo_make_insertable(fti)){ + FT_Done_FreeType(fti->library); + free(fti); + fti=NULL; + } + } + else { + free(fti); + fti=NULL; + } + } + if(!fti)FcFini(); + } + return(fti); +} + +/** + \brief Make an FT_INFO structure insertable. Adds storage as needed. + \param fti pointer to the FT_INFO structure + \returns 0 on success, !0 on error. +*/ +int ftinfo_make_insertable(FT_INFO *fti){ + int status=0; + FNT_SPECS *tmp; + if(!fti)return(2); + if(fti->used >= fti->space){ + fti->space += ALLOCINFO_CHUNK; + tmp = (FNT_SPECS *) realloc(fti->fonts, fti->space * sizeof(FNT_SPECS) ); + if(tmp){ + fti->fonts = tmp; + memset(&fti->fonts[fti->used],0,(fti->space - fti->used)*sizeof(FNT_SPECS)); + } + else { + status=1; + } + } + return(status); +} + + +/** + \brief Insert a copy of a FNT_SPECS structure into the FT_INFO structure. + \param fti pointer to the FT_INFO structure. + \param fsp pointer to the FNT_SPECS structure. + \returns 0 on success, !0 on error. +*/ +int ftinfo_insert(FT_INFO *fti, FNT_SPECS *fsp){ + int status=1; + if(!fti)return(2); + if(!fsp)return(3); + if(!(status = ftinfo_make_insertable(fti))){ + memcpy(&(fti->fonts[fti->used]),fsp,sizeof(FNT_SPECS)); + fti->used++; + } + return(status); +} + + + +/** + \brief Release an FT_INFO structure. Release all associated memory. + Use like: fi_ptr = ftinfo_release(fi_ptr) + \param fti pointer to the FT_INFO structure. + \returns NULL. +*/ +FT_INFO *ftinfo_release(FT_INFO *fti){ + (void) ftinfo_clear(fti); + FcFini(); /* shut down FontConfig, release memory, patterns must have already been released or boom! */ + return NULL; +} + + +/** + \brief Clear an FT_INFO structure. Release all Freetype memory but does not release Fontconfig. + This would be called in preference to ftinfo_release() if some other part of the program needed + to continue using Fontconfig. + Use like: fi_ptr = ftinfo_clear(fi_ptr) + \param fti pointer to the FT_INFO structure. + \returns NULL. +*/ +FT_INFO *ftinfo_clear(FT_INFO *fti){ + uint32_t i; + FNT_SPECS *fsp; + if(fti){ + for(i=0;iused;i++){ + fsp = &(fti->fonts[i]); + FT_Done_Face(fsp->face); /* release memory for face controlled by FreeType */ + free(fsp->file); /* release memory holding copies of paths */ + free(fsp->fontspec); /* release memory holding copies of font names */ + FcPatternDestroy(fsp->fpat); /* release memory for FontConfig fpats */ + FcFontSetDestroy(fsp->fontset); + if(fsp->alts){ free(fsp->alts); } + } + free(fti->fonts); + FT_Done_FreeType(fti->library); /* release all other FreeType memory */ + free(fti); + } + return NULL; +} + + +/** + \brief Find the loaded font matching fontspec + \returns index of font on success, -1 if not found + \param tri pointer to the TR_INFO structure. + \param fontspec UTF-8 description of the font, as constructed in trinfo_load_fontname +*/ + +int ftinfo_find_loaded_by_spec(const FT_INFO *fti, const uint8_t *fontspec){ + uint32_t i; + int status = -1; + /* If it is already loaded, do not load it again */ + for(i=0;iused;i++){ + if(0==strcmp((char *) fti->fonts[i].fontspec, (char *)fontspec)){ + status=i; + break; + } + } + return(status); +} + +/** + \brief Find the loaded font matching the source file + \returns index of font on success, -1 if not found + \param tri pointer to the TR_INFO structure. + \param filename UTF-8 file name for the font +*/ + +int ftinfo_find_loaded_by_src(const FT_INFO *fti, const uint8_t *filename){ + uint32_t i; + int status = -1; + /* If it is already loaded, do not load it again */ + for(i=0;iused;i++){ + if(0==strcmp((char *) fti->fonts[i].file, (char *) filename)){ + status=i; + break; + } + } + return(status); +} + + +/** + \brief Load a (new) font by name into a TR_INFO structure or find it if it is already loaded + \returns fi_idx of inserted (or found) font on success, <0 on error. + \param fti pointer to the FT_INFO structure. + \param fontname UTF-8 font name + \param fontspec UTF-8 font specification used for query string. +*/ + +int ftinfo_load_fontname(FT_INFO *fti, const char *fontspec){ + FcPattern *pattern = NULL; + FcPattern *fpat = NULL; + FcFontSet *fontset = NULL; + FcResult result = FcResultMatch; + char *filename; + double fd; + FNT_SPECS *fsp; + int status; + int fi_idx; + + if(!fti)return(-1); + + /* If it is already loaded, do not load it again */ + status = ftinfo_find_loaded_by_spec(fti, (uint8_t *) fontspec); + if(status >= 0){ return(status); } + status = 0; /* was -1, reset to 0 */ + + ftinfo_make_insertable(fti); + fi_idx = fti->used; + + pattern = FcNameParse((const FcChar8 *)fontspec); + while(1) { /* this is NOT a loop, it uses breaks to avoid gotos and deep nesting */ + if(!(pattern)){ status = -2; break; } + if(!FcConfigSubstitute(NULL, pattern, FcMatchPattern)){ status = -3; break; }; + FcDefaultSubstitute(pattern); + /* get a fontset, trimmed to only those with new glyphs as needed, so that missing glyph's may be handled */ + if(!(fontset = FcFontSort (NULL,pattern, FcTrue, NULL, &result)) || (result != FcResultMatch)){ status = -4; break; } + if(!(fpat = FcFontRenderPrepare(NULL, pattern, fontset->fonts[0]))){ status = -405; break; } + if(FcPatternGetString( fpat, FC_FILE, 0, (FcChar8 **)&filename) != FcResultMatch){ status = -5; break; } + if(FcPatternGetDouble( fpat, FC_SIZE, 0, &fd) != FcResultMatch){ status = -6; break; } + + /* copy these into memory for external use */ + fsp = &(fti->fonts[fti->used]); + fsp->fontset = fontset; + fsp->alts = NULL; /* Initially no links to alternate fonts */ + fsp->space = 0; + fsp->file = (uint8_t *) U_strdup((char *) filename); + fsp->fontspec = (uint8_t *) U_strdup((char *) fontspec); + fsp->fpat = fpat; + fsp->fsize = fd; + break; + } + /* release FC's own memory related to this call that does not need to be kept around so that face will work */ + if(pattern)FcPatternDestroy(pattern); /* done with this memory */ + if(status<0){ + if(fontset)FcFontSetDestroy(fontset); + if(fpat)FcPatternDestroy(fpat); + return(status); + } + + /* get the current face */ + if(FT_New_Face( fti->library, (const char *) fsp->file, 0, &(fsp->face) )){ return(-8); } + + if(FT_Set_Char_Size( + fsp->face, /* handle to face object */ + 0, /* char_width in 1/64th of points */ + fd*64, /* char_height in 1/64th of points */ + 72, /* horizontal device resolution, DPI */ + 72) /* vebrical device resolution, DPI */ + ){ return(-9); } + + /* The space advance is needed in various places. Get it now, and get it in the font units, + so that it can be scaled later with the text size */ + status = TR_getadvance(fti, fsp,' ',0,FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, FT_KERNING_UNSCALED, NULL, NULL); + if(status < 0)return(-7); + fsp->spcadv = ((double) status)/(64.0); + + fti->used++; + +/* + char *fs; + int fb; + if(FcPatternGetBool( fpat, FC_OUTLINE, 0, &fb)== FcResultMatch){ printf("outline: %d\n",fb);fflush(stdout); } + if(FcPatternGetBool( fpat, FC_SCALABLE, 0, &fb)== FcResultMatch){ printf("scalable: %d\n",fb);fflush(stdout); } + if(FcPatternGetDouble( fpat, FC_DPI, 0, &fd)== FcResultMatch){ printf("DPI: %f\n",fd);fflush(stdout); } + if(FcPatternGetInteger( fpat, FC_FONTVERSION, 0, &fb)== FcResultMatch){ printf("fontversion: %d\n",fb);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FULLNAME , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FULLNAME : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FAMILY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FAMILY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_STYLE , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("STYLE : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FOUNDRY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FOUNDRY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FAMILYLANG , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FAMILYLANG : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_STYLELANG , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("STYLELANG : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FULLNAMELANG, 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FULLNAMELANG: %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_CAPABILITY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("CAPABILITY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FONTFORMAT , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FONTFORMAT : %s\n",fs);fflush(stdout); } +*/ + + return(fi_idx); +} + +/** + \brief Dump the contents of the TR_INFO structure to stdout. For debugging purposes,not used in production code. + \param tri pointer to the TR_INFO structure. +*/ +void ftinfo_dump(const FT_INFO *fti){ + uint32_t i,j; + FNT_SPECS *fsp; + printf("fti space: %d\n",fti->space); + printf("fti used: %d\n",fti->used); + for(i=0; i< fti->used; i++){ + fsp = &(fti->fonts[i]); + printf("fti font: %6d space: %6d used: %6d spcadv %8f fsize %8f \n",i,fsp->space,fsp->used,fsp->spcadv,fsp->fsize); + printf(" file: %s\n",fsp->file); + printf(" fspc: %s\n",fsp->fontspec); + for(j=0;jused;j++){ + printf(" alts: %6d fi_idx: %6d wgt: %6d\n",j,fsp->alts[j].fi_idx,fsp->alts[j].weight); + } + } + +} + +/** + \brief Make the FNT_SPECS alts structure insertable. Adds storage as needed. + \param fti pointer to the FT_INFO structure + \returns 0 on success, !0 on error. +*/ +int fsp_alts_make_insertable(FNT_SPECS *fsp){ + int status=0; + ALT_SPECS *tmp; + if(!fsp)return(2); + if(fsp->used >= fsp->space){ + fsp->space += ALLOCINFO_CHUNK; + tmp = (ALT_SPECS *) realloc(fsp->alts, fsp->space * sizeof(ALT_SPECS) ); + if(tmp){ + fsp->alts = tmp; + memset(&fsp->alts[fsp->used],0,(fsp->space - fsp->used)*sizeof(ALT_SPECS)); + } + else { + status=1; + } + } + return(status); +} + + +/** + \brief Insert a new ALT_SPECS into the FNT_SPECS alts list. + \param fsp pointer to the FNT_SPECS structure. + \param fi_idx font index to add to the alts list + \returns 0 on success, !0 on error. +*/ +int fsp_alts_insert(FNT_SPECS *fsp, uint32_t fi_idx){ + int status=1; + ALT_SPECS alt; + if(!fsp)return(3); + alt.fi_idx = fi_idx; + alt.weight = 1; /* new ones start with this weight, it can only go up */ + if(!(status = fsp_alts_make_insertable(fsp))){ + fsp->alts[fsp->used] = alt; + fsp->used++; + } + return(status); +} + +/** + \brief Increment the weight of an alts entry by 1, readjust order if necessary + \param fsp pointer to the FNT_SPECS structure. + \param idx index of the alts entry to increment + \returns 0 on success, !0 on error. +*/ +int fsp_alts_weight(FNT_SPECS *fsp, uint32_t a_idx){ + uint32_t i; + ALT_SPECS alt; + if(!fsp)return(1); + if(!fsp->used)return(2); + if(a_idx >= fsp->used)return(3); + /* If a counter hits the limit divide all counts in half. */ + if(fsp->alts[a_idx].weight == UINT32_MAX){ + for(i=0; iused; i++){ fsp->alts[i].weight /= 2; } + } + fsp->alts[a_idx].weight++; + for(i=a_idx; i>0; i--){ + if(fsp->alts[i-1].weight >= fsp->alts[a_idx].weight)break; + alt = fsp->alts[i-1]; + fsp->alts[i-1] = fsp->alts[a_idx]; + fsp->alts[a_idx] = alt; + } + return(0); +} + + + +/** + \brief Make a CHILD_SPECS structure insertable. Adds storage as needed. + \param csp pointer to the CHILD_SPECS structure + \returns 0 on success, !0 on error. +*/ +int csp_make_insertable(CHILD_SPECS *csp){ + int status=0; + int *tmp; + if(!csp)return(2); + if(csp->used >= csp->space){ + csp->space += ALLOCINFO_CHUNK; + tmp = (int *) realloc(csp->members, csp->space * sizeof(int) ); + if(tmp){ + csp->members = tmp; + memset(&csp->members[csp->used],0,(csp->space - csp->used)*sizeof(int)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Add a member to a CHILD_SPECS structure. (Member is an index for either a text object or a complex.) + \param dst pointer to the CHILD_SPECS structure. + \param src index of the member. + \returns 0 on success, !0 on error. +*/ +int csp_insert(CHILD_SPECS *dst, int src){ + int status=1; + if(!dst)return(2); + if(!(status=csp_make_insertable(dst))){ + dst->members[dst->used]=src; + dst->used++; + } + return(status); +} + +/** + \brief Append all the members of one CHILD_SPECS structure to another CHILD_SPECS structure. + Member is an index for either a text object or a complex. + The donor is not modified. + \param dst pointer to the recipient CHILD_SPECS structure. + \param src pointer to the donor CHILD_SPECS structure. + \returns 0 on success, !0 on error. +*/ +int csp_merge(CHILD_SPECS *dst, CHILD_SPECS *src){ + uint32_t i; + int status=1; + if(!dst)return(2); + if(!src)return(3); + for(i=0;iused;i++){ + status = csp_insert(dst, src->members[i]); + if(status)break; + } + return(status); +} + +/** + \brief Release a CHILD_SPECS structure. Release all associated memory. + \param csp pointer to the CHILD_SPECS structure. + \returns NULL. +*/ +void csp_release(CHILD_SPECS *csp){ + if(csp){ + free(csp->members); + csp->space = 0; + csp->used = 0; + } +} + +/** + \brief Clear a CHILD_SPECS structure, making all allocated slots usable. Does not release associated memory. + \param csp pointer to the CHILD_SPECS structure. + \returns NULL. +*/ +void csp_clear(CHILD_SPECS *csp){ + csp->used = 0; +} + + +/** + \brief Initialize an CX_INFO structure. Holds complexes (multiple text objects in known positions and order.) + \returns a pointer to the CX_INFO structure created, or NULL on error. +*/ +CX_INFO *cxinfo_init(void){ + CX_INFO *cxi = NULL; + cxi = (CX_INFO *)calloc(1,sizeof(CX_INFO)); + if(cxi){ + if(cxinfo_make_insertable(cxi)){ + free(cxi); + cxi=NULL; + } + } + return(cxi); +} + +/** + \brief Make a CX_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param cxi pointer to the CX_INFO structure +*/ +int cxinfo_make_insertable(CX_INFO *cxi){ + int status=0; + CX_SPECS *tmp; + if(cxi->used >= cxi->space){ + cxi->space += ALLOCINFO_CHUNK; + tmp = (CX_SPECS *) realloc(cxi->cx, cxi->space * sizeof(CX_SPECS) ); + if(tmp){ + cxi->cx = tmp; + memset(&cxi->cx[cxi->used],0,(cxi->space - cxi->used)*sizeof(CX_SPECS)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Insert a complex into the CX_INFO structure. (Insert may be either TR_TEXT or TR_LINE.) + \returns 0 on success, !0 on error. + \param cxi pointer to the CX_INFO structure (complexes). + \param src index of the complex to insert. + \param src_rt_tidx index of the bounding rectangle + \param type TR_TEXT (index is for tpi->chunks[]) or TR_LINE (index is for cxi->kids[]) +*/ +int cxinfo_insert(CX_INFO *cxi, int src, int src_rt_tidx, enum tr_classes type){ + int status=1; + if(!cxi)return(2); + if(!(status=cxinfo_make_insertable(cxi))){ + cxi->cx[cxi->used].rt_cidx = src_rt_tidx; + cxi->cx[cxi->used].type = type; + status = csp_insert(&(cxi->cx[cxi->used].kids), src); + cxi->used++; + } + return(status); +} + +/** + \brief Append a complex to the CX_INFO structure and give it a type. + \param cxi pointer to the CX_INFO structure (complexes). + \param src index of the complex to append. + \param type TR_LINE (src is an index for tpi->chunks[]) or TR_PARA (src is an index for cxi->kids[]). + \returns 0 on success, !0 on error. +*/ +int cxinfo_append(CX_INFO *cxi, int src, enum tr_classes type){ + int status=1; + if(!cxi)return(2); + if(!(status=cxinfo_make_insertable(cxi))){ + cxi->cx[cxi->used-1].type = type; + status = csp_insert(&(cxi->cx[cxi->used-1].kids), src); + } + return(status); +} + + +/** + \brief Merge a complex dst with N members (N>=1) by adding a second complex src, and change the type. + \param cxi pointer to the CX_INFO structure (complexes). + \param dst index of the complex to expand. + \param src index of the donor complex (which is not modified). + \param type TR_LINE (src is an index for tpi->chunks[]) or TR_PARA (src is an index for cxi->kids[]). + \returns 0 on success, !0 on error. +*/ +int cxinfo_merge(CX_INFO *cxi, int dst, int src, enum tr_classes type){ + int status =1; + if(!cxi)return(2); + if(!cxi->used)return(3); + if(dst < 0 || dst >= (int) cxi->used)return(4); + if(src < 0)return(5); + cxi->cx[dst].type = type; + status = csp_merge(&(cxi->cx[dst].kids), &(cxi->cx[src].kids)); + return(status); +} + +/** + \brief Trim the last complex from thelist of complexes. + \param cxi pointer to the CX_INFO structure (complexes). + \returns 0 on success, !0 on error. +*/ +int cxinfo_trim(CX_INFO *cxi){ + int status = 0; + int last ; + if(!cxi)return(1); + if(!cxi->used)return(2); + last = cxi->used - 1; + csp_clear(&(cxi->cx[last].kids)); + cxi->used--; + return(status); +} + + +/** + \brief Dump the contents of the TR_INFO structure to stdout. For debugging purposes,not used in production code. + \param tri pointer to the TR_INFO structure. +*/ +void cxinfo_dump(const TR_INFO *tri){ + uint32_t i,j,k; + CX_INFO *cxi = tri->cxi; + BR_INFO *bri = tri->bri; + TP_INFO *tpi = tri->tpi; + BRECT_SPECS *bsp; + CX_SPECS *csp; + if(cxi){ + printf("cxi space: %d\n",cxi->space); + printf("cxi used: %d\n",cxi->used); + printf("cxi phase1: %d\n",cxi->phase1); + printf("cxi lines: %d\n",cxi->lines); + printf("cxi paras: %d\n",cxi->paras); + printf("cxi xy: %f , %f\n",tri->x,tri->y); + + for(i=0;iused;i++){ + csp = &(cxi->cx[i]); + bsp = &(bri->rects[csp->rt_cidx]); + printf("cxi cx[%d] type:%d rt_tidx:%d kids_used:%d kids_space:%d\n",i, csp->type, csp->rt_cidx, csp->kids.used, csp->kids.space); + printf("cxi cx[%d] br (LL,UR) (%f,%f),(%f,%f)\n",i,bsp->xll,bsp->yll,bsp->xur,bsp->yur); + for(j=0;jkids.used;j++){ + k = csp->kids.members[j]; + bsp = &(bri->rects[k]); + if(csp->type == TR_TEXT || csp->type == TR_LINE){ + printf("cxi cx[%d] member:%3d tp_idx:%3d ldir:%d rt_tidx:%3d br (LL,UR) (%8.3f,%8.3f),(%8.3f,%8.3f) xy (%8.3f,%8.3f) kern (%8.3f,%8.3f) text:<%s> decor:%5.5x\n", + i, j, k, tpi->chunks[k].ldir, tpi->chunks[k].rt_tidx, + bsp->xll,bsp->yll,bsp->xur,bsp->yur, + tpi->chunks[k].x, tpi->chunks[k].y, + tpi->chunks[k].xkern, tpi->chunks[k].ykern, + tpi->chunks[k].string, tpi->chunks[k].decoration ); + } + else { /* TR_PARA_* */ + printf("cxi cx[%d] member:%d cx_idx:%d\n",i, j, k); + } + } + } + } + return; +} + +/** + \brief Release a CX_INFO structure. Release all associated memory. + use like: cxi = cxiinfo_release(cxi); + \param cxi pointer to the CX_INFO structure. + \returns NULL. +*/ +CX_INFO *cxinfo_release(CX_INFO *cxi){ + uint32_t i; + if(cxi){ + for(i=0;iused;i++){ csp_release(&cxi->cx[i].kids); } + free(cxi->cx); + free(cxi); /* release the overall cxinfo structure */ + } + return NULL; +} + + +/** + \brief Initialize an TP_INFO structure. Holds text objects from which complexes are built. + \returns a pointer to the TP_INFO structure created, or NULL on error. +*/ +TP_INFO *tpinfo_init(void){ + TP_INFO *tpi = NULL; + tpi = (TP_INFO *)calloc(1,sizeof(TP_INFO)); + if(tpi){ + if(tpinfo_make_insertable(tpi)){ + free(tpi); + tpi=NULL; + } + } + return(tpi); +} + + +/** + \brief Make a TP_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param tpi pointer to the TP_INFO structure +*/ +int tpinfo_make_insertable(TP_INFO *tpi){ + int status=0; + TCHUNK_SPECS *tmp; + if(tpi->used >= tpi->space){ + tpi->space += ALLOCINFO_CHUNK; + tmp = (TCHUNK_SPECS *) realloc(tpi->chunks, tpi->space * sizeof(TCHUNK_SPECS) ); + if(tmp){ + tpi->chunks = tmp; + memset(&tpi->chunks[tpi->used],0,(tpi->space - tpi->used)*sizeof(TCHUNK_SPECS)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Insert a copy of a TCHUNK_SPECS structure into a TP_INFO structure. (Insert a text object.) + \returns 0 on success, !0 on error. + \param tpi pointer to the TP_INFO structure + \param tsp pointer to the TCHUNK_SPECS structure +*/ +int tpinfo_insert(TP_INFO *tpi, const TCHUNK_SPECS *tsp){ + int status=1; + TCHUNK_SPECS *ltsp; + if(!tpi)return(2); + if(!tsp)return(3); + if(!(status = tpinfo_make_insertable(tpi))){ + ltsp = &(tpi->chunks[tpi->used]); + memcpy(ltsp,tsp,sizeof(TCHUNK_SPECS)); + if(tsp->co)ltsp->condensed = 75; /* Narrow was set in the font name */ + ltsp->xkern = ltsp->ykern = 0.0; /* kerning will be calculated from the derived layout */ + tpi->used++; + } + return(status); +} + +/** + \brief Release a TP_INFO structure. Release all associated memory. + use like: tpi = tpinfo_release(tpi); + \returns NULL. + \param tpi pointer to the TP_INFO structure. +*/ +TP_INFO *tpinfo_release(TP_INFO *tpi){ + uint32_t i; + if(tpi){ + for(i=0;iused;i++){ + free(tpi->chunks[i].string); } + free(tpi->chunks); /* release the array */ + free(tpi); /* release the overall tpinfo structure */ + } + return NULL; +} + +/** + \brief Initialize an BR_INFO structure. Holds bounding rectangles, for both text objects and complexes. + \returns a pointer to the BR_INFO structure created, or NULL on error. +*/ +BR_INFO *brinfo_init(void){ + BR_INFO *bri = NULL; + bri = (BR_INFO *)calloc(1,sizeof(BR_INFO)); + if(bri){ + if(brinfo_make_insertable(bri)){ + free(bri); + bri=NULL; + } + } + return(bri); +} + +/** + \brief Make a BR_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure +*/ +int brinfo_make_insertable(BR_INFO *bri){ + int status=0; + BRECT_SPECS *tmp; + if(!bri)return(2); + if(bri->used >= bri->space){ + bri->space += ALLOCINFO_CHUNK; + tmp = (BRECT_SPECS *) realloc(bri->rects, bri->space * sizeof(BRECT_SPECS) ); + if(tmp){ bri->rects = tmp; } + else { status = 1;} + } + return(status); +} + +/** + \brief Insert a copy of a BRECT_SPEC structure into a BR_INFO structure. (Insert a bounding rectangle.) + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure + \param element pointer to the BRECT_SPECS structure +*/ +int brinfo_insert(BR_INFO *bri, const BRECT_SPECS *element){ + int status=1; + if(!bri)return(2); + if(!(status=brinfo_make_insertable(bri))){ + memcpy(&(bri->rects[bri->used]),element,sizeof(BRECT_SPECS)); + bri->used++; + } + return(status); +} + +/** + \brief Merge BRECT_SPEC element src into/with BRECT_SPEC element dst. src is unchanged. (Merge two bounding rectangles.) + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. +*/ +int brinfo_merge(BR_INFO *bri, int dst, int src){ + if(!bri)return(1); + if(!bri->used)return(2); + if(dst<0 || dst >= (int) bri->used)return(3); + if(src<0 || src >= (int) bri->used)return(4); + bri->rects[dst].xll = TEREMIN(bri->rects[dst].xll, bri->rects[src].xll); + bri->rects[dst].yll = TEREMAX(bri->rects[dst].yll, bri->rects[src].yll); /* MAX because Y is positive DOWN */ + bri->rects[dst].xur = TEREMAX(bri->rects[dst].xur, bri->rects[src].xur); + bri->rects[dst].yur = TEREMIN(bri->rects[dst].yur, bri->rects[src].yur); /* MIN because Y is positive DOWN */ +/* +printf("bri_Merge into rect:%d (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n",dst, +(bri->rects[dst].xll), +(bri->rects[dst].yll), +(bri->rects[dst].xur), +(bri->rects[dst].yur), +(bri->rects[src].xll), +(bri->rects[src].yll), +(bri->rects[src].xur), +(bri->rects[src].yur)); +*/ + return(0); +} + +/** + \brief Check for an allowable overlap of two bounding rectangles. + Allowable overlap is any area overlap of src and dst bounding rectangles, after + they have been expanded (padded) by allowed edge expansions. (For instance, if + missing spaces must be accounted for.) + The method works backwards: look for all reasons they might not overlap, + if none are found, then the rectangles do overlap. + An overlap here does not count just a line or a point - area must be involved. + \returns 0 on success (overlap detected), 1 on no overlap, anything else is an error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param rp_dst Pointer to edge padding values for dst. + \param rp_src Pointer to edge padding values for src. +*/ +int brinfo_overlap(const BR_INFO *bri, int dst, int src, RT_PAD *rp_dst, RT_PAD *rp_src){ + int status; + BRECT_SPECS *br_dst; + BRECT_SPECS *br_src; + if(!bri || !rp_dst || !rp_src)return(2); + if(!bri->used)return(3); + if(dst<0 || dst>= (int) bri->used)return(4); + if(src<0 || src>= (int) bri->used)return(5); + br_dst=&bri->rects[dst]; + br_src=&bri->rects[src]; + if( /* Test all conditions that exclude overlap, if any are true, then no overlap */ + ((br_dst->xur + rp_dst->right) < (br_src->xll - rp_src->left) ) || /* dst fully to the left */ + ((br_dst->xll - rp_dst->left) > (br_src->xur + rp_src->right) ) || /* dst fully to the right */ + ((br_dst->yur - rp_dst->up) > (br_src->yll + rp_src->down) ) || /* dst fully below (Y is positive DOWN) */ + ((br_dst->yll + rp_dst->down) < (br_src->yur - rp_src->up) ) /* dst fully above (Y is positive DOWN) */ + ){ + status = 1; + } + else { + /* overlap not excluded, so it must occur. + Only accept overlaps that are mostly at one end or the other, not mostly top or bottom. + If the following condition is true then there is no more than a tiny bit of horizontal overlap of src + within dist, which suggests that the two pieces of text may be considered part of one line. + (For a vertical alphabet the same method could be used for up/down.) */ + if( + (br_src->xll >= br_dst->xur - rp_dst->right) || /* src overlaps just a little on the right (L->R language) */ + (br_src->xur <= br_dst->xll + rp_dst->left) /* src overlaps just a little on the left (R->L language) */ + ){ + status = 0; + } + else { /* Too much overlap, reject the overlap */ + status = 1; + } + } +/* +printf("Overlap status:%d\nOverlap trects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +status, +(br_dst->xll - rp_dst->left ), +(br_dst->yll - rp_dst->down ), +(br_dst->xur + rp_dst->right), +(br_dst->yur + rp_dst->up ), +(br_src->xll - rp_src->left ), +(br_src->yll - rp_src->down ), +(br_src->xur + rp_src->right), +(br_src->yur + rp_src->up )); +printf("Overlap brects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +(br_dst->xll), +(br_dst->yll), +(br_dst->xur), +(br_dst->yur), +(br_src->xll), +(br_src->yll), +(br_src->xur), +(br_src->yur)); +printf("Overlap rprect (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +(rp_dst->left), +(rp_dst->down), +(rp_dst->right), +(rp_dst->up), +(rp_src->left), +(rp_src->down), +(rp_src->right), +(rp_src->up)); +*/ + return(status); +} + +/** + \brief Check for various sorts of invalid text elements upstream (language dir changes, draw order backwards from language direction) + \returns 0 on success (not upstream), 1 if upstream, anything else is an error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param ddir direction of dst + \param sdir direction of src +*/ + +int brinfo_upstream(BR_INFO *bri, int dst, int src, int ddir, int sdir){ + int status=0; + BRECT_SPECS *br_dst; + BRECT_SPECS *br_src; + if(!bri)return(2); + if(!bri->used)return(3); + if(dst<0 || dst>= (int) bri->used)return(4); + if(src<0 || src>= (int) bri->used)return(5); + br_dst=&bri->rects[dst]; + br_src=&bri->rects[src]; + if( ddir == LDIR_RL && sdir == LDIR_LR){ + if(br_dst->xur <= (br_src->xll + br_src->xur)/2.0){ status = 1; } + } + else if( ddir == LDIR_LR && sdir == LDIR_RL){ + if((br_src->xll + br_src->xur)/2.0 <= br_dst->xll ){ status = 1; } + } + else if( ddir == LDIR_RL && sdir == LDIR_RL){ + if(br_dst->xur <= (br_src->xll + br_src->xur)/2.0){ status = 1; } + } + else if( ddir == LDIR_LR && sdir == LDIR_LR){ + if((br_src->xll + br_src->xur)/2.0 <= br_dst->xll ){ status = 1; } + } + return(status); +} + + +/** + \brief Try to deduce justification of a paragraph from the bounding rectangles for two successive lines. + \returns one of TR_PARA_ UJ (unknown justified), LJ, CJ, or RJ (left, center, or right justified). + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param slop allowed error in edge alignment. + \param type Preexisting justification for dst, if any. Justification of dst and src must match this or + TR_PARA_UJ is returned even if dst and src have some (other) alignment. +*/ +enum tr_classes brinfo_pp_alignment(const BR_INFO *bri, int dst, int src, double slop, enum tr_classes type){ + enum tr_classes newtype; + BRECT_SPECS *br_dst = & bri->rects[dst]; + BRECT_SPECS *br_src = & bri->rects[src]; + if((br_dst->yur >= br_src->yur) || (br_dst->yll >= br_src->yll)){ /* Y is positive DOWN */ + /* lines in the wrong vertical order, no paragraph possible (Y is positive down) */ + newtype = TR_PARA_UJ; + } + else if(fabs(br_dst->xll - br_src->xll) < slop){ + /* LJ (might also be CJ but LJ takes precedence) */ + newtype = TR_PARA_LJ; + } + else if(fabs(br_dst->xur - br_src->xur) < slop){ + /* RJ */ + newtype = TR_PARA_RJ; + } + else if(fabs( (br_dst->xur + br_dst->xll)/2.0 - (br_src->xur + br_src->xll)/2.0 ) < slop){ + /* CJ */ + newtype = TR_PARA_CJ; + } + else { + /* not aligned */ + newtype = TR_PARA_UJ; + } + /* within a paragraph type can change from unknown to known, but not from one known type to another*/ + if((type != TR_PARA_UJ) && (newtype != type)){ + newtype = TR_PARA_UJ; + } +/* +printf("pp_align newtype:%d brects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +newtype, +(br_dst->xll), +(br_dst->yll), +(br_dst->xur), +(br_dst->yur), +(br_src->xll), +(br_src->yll), +(br_src->xur), +(br_src->yur)); +*/ + return(newtype); +} + +/** + \brief Release a BR_INFO structure. Release all associated memory. + use like: bri = brinfo_release(bri); + \param bri pointer to the BR_INFO structure. + \returns NULL. +*/ +BR_INFO *brinfo_release(BR_INFO *bri){ + if(bri){ + free(bri->rects); + free(bri); /* release the overall brinfo structure */ + } + return NULL; +} + + + +/** + \brief Initialize an TR_INFO structure. Holds all data for text reassembly. + \returns a pointer to the TR_INFO structure created, or NULL on error. +*/ +TR_INFO *trinfo_init(TR_INFO *tri){ + if(tri)return(tri); /* tri is already set, double initialization is not allowed */ + if(!(tri = (TR_INFO *)calloc(1,sizeof(TR_INFO))) || + !(tri->fti = ftinfo_init()) || + !(tri->tpi = tpinfo_init()) || + !(tri->bri = brinfo_init()) || + !(tri->cxi = cxinfo_init()) + ){ tri = trinfo_release(tri); } + tri->out = NULL; /* This will allocate as needed, it might not ever be needed. */ + tri->qe = 0.0; + tri->esc = 0.0; + tri->x = DBL_MAX; + tri->y = DBL_MAX; + tri->dirty = 0; + tri->use_kern = 1; + tri->load_flags = FT_LOAD_NO_SCALE; + tri->kern_mode = FT_KERNING_UNSCALED; + tri->outspace = 0; + tri->outused = 0; + tri->usebk = BKCLR_NONE; + memset(&(tri->bkcolor),0,sizeof(TRCOLORREF)); + return(tri); +} + +/** + \brief Release a TR_INFO structure completely. + Release all associated memory, including FontConfig. + See also trinfo_clear() and trinfo_release_except_FC(). + use like: tri = trinfo_release(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_release(TR_INFO *tri){ + if(tri){ + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->fti)tri->fti=ftinfo_release(tri->fti); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ free(tri->out); tri->out=NULL; }; + free(tri); + } + return(NULL); +} + +/** + \brief Release a TR_INFO structure mostly. + Release all associated memory EXCEPT Fontconfig. + Fontconfig may still be needed elsewhere in a program and there is no way to figure that out here. + See also trinfo_clear() and trinfo_release(). + use like: tri = trinfo_release_except_FC(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_release_except_FC(TR_INFO *tri){ + if(tri){ + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->fti)tri->fti=ftinfo_clear(tri->fti); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ free(tri->out); tri->out=NULL; }; + free(tri); + } + return(NULL); +} + +/** + \brief Clear a TR_INFO structure. + Releases text and rectangle information, but retains font information, both + Freetype information and Fontconfig information. + See also trinfo_release() and trinfo_release_except_FC(). + Use like: tri = trinfo_clear(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_clear(TR_INFO *tri){ + if(tri){ + + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ + free(tri->out); + tri->out = NULL; + tri->outused = 0; + tri->outspace = 0; + }; + /* Do NOT modify: qe, use_kern, usebk, load_flags, kern_mode, or bkcolor. Set the rest back to their defaults */ + tri->esc = 0.0; + tri->x = DBL_MAX; + tri->y = DBL_MAX; + tri->dirty = 0; + if(!(tri->tpi = tpinfo_init()) || /* re-init the pieces just released */ + !(tri->bri = brinfo_init()) || + !(tri->cxi = cxinfo_init()) + ){ + tri = trinfo_release(tri); /* something horrible happened, clean out tri and return NULL */ + } + } + return(tri); +} + + +/** + \brief Set the quantization error value for a TR_INFO structure. + If coordinates have passed through an integer form limits + in accuracy may have been imposed. For instance, if the X coordinate of a point in such a file + is 1000, and the conversion factor from those coordinates to points is .04, then eq is .04. This + just says that single coordinates are only good to within .04, and two coordinates may differ by as much + as .08, just due to quantization error. So if some calculation shows a difference of + .02 it may be interpreted as this sort of error and set to 0.0. + \returns 0 on success, !0 on error. + \param tri pointer to TR_INFO structure + \param qe quantization error. +*/ +int trinfo_load_qe(TR_INFO *tri, double qe){ + if(!tri)return(1); + if(qe<0.0)return(2); + tri->qe=qe; + return(0); +} + +/** + \brief Set the background color and whether or not to use it. + When background color is turned on each line of text is underwritten with a rectangle + of the specified color. The rectangle is the merged bounding rectangle for that line. + \returns 0 on success but nothing changed, >0 on error, <0 on success and a value changed. + \param tri pointer to TR_INFO structure + \param usebk 0 for no background, anything else uses background color + \param bkcolor background color to use +*/ +int trinfo_load_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor){ + int status=0; + if(!tri){ status = 1; } + else { + if((usebk < BKCLR_NONE) || (usebk > BKCLR_ALL)){ status = 2; } + else { + status = trinfo_check_bk(tri, usebk, bkcolor); + tri->usebk = usebk; + tri->bkcolor = bkcolor; + } + } + return(status); +} + +/** + \brief Are the proposed new background and background color a change? + \returns 0 if they are the same, -1 if either is different + \param tri pointer to TR_INFO structure + \param usebk 0 for no background, anything else uses background color + \param bkcolor background color to use +*/ +int trinfo_check_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor){ + int status = 0; + if( (tri->usebk != usebk) || memcmp(&tri->bkcolor,&bkcolor,sizeof(TRCOLORREF))){ status = -1; } + return(status); +} + +/** + \brief Set Freetype parameters and kerning mode (if any) in a TRI_INFO structure. + \returns 0 on success, !0 on error. + \param tri pointer to a TR_INFO structure + \param use_kern 0 if kerning is to be employed, !0 otherwise. + \param load_flags Controls internal advance: + FT_LOAD_NO_SCALE, internal advance is in 1/64th of a point. (kerning values are still scaled) + FT_LOAD_TARGET_NORMAL internal advance is in 1/64th of a point. The scale + factor seems to be (Font Size in points)*(DPI)/(32.0 pnts)*(72 dpi). + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int trinfo_load_ft_opts(TR_INFO *tri, int use_kern, int load_flags, int kern_mode){ + if(!tri)return(1); + tri->use_kern = use_kern; + tri->load_flags = load_flags; + tri->kern_mode = kern_mode; + return(0); +} + +/** + \brief Append text to a TR_INFO struct's output buffer, expanding it if necessary. + \returns 0 on success, !0 on error. + \param tri pointer to a TR_INFO structure + \param src Pointer to a text string. +*/ +int trinfo_append_out(TR_INFO *tri, const char *src){ + size_t slen; + uint8_t *tmp; + if(!src)return(-1); + slen = strlen(src); + if(tri->outused + (int) slen + 1 >= tri->outspace){ + tri->outspace += TEREMAX(ALLOCOUT_CHUNK,slen+1); + tmp = realloc(tri->out, tri->outspace * sizeof(uint8_t) ); + if(tmp){ tri->out = tmp; } + else { return(-1); } + } + memcpy(tri->out + tri->outused, src, slen+1); /* copy the terminator */ + tri->outused += slen; /* do not count the terminator in the length */ + return(0); +} + + +/** + \brief Load a text object into a TR_INFO struct. + \returns 0 on success, !0 on error. -1 means that the escapement is different from the objects already loaded. + \param tri pointer to a TR_INFO structure + \param tsp pointer to a TCHUNK_SPECS structure (text object to load) + \param escapement angle in degrees of the text object. + \param flags special processing flags: + TR_EMFBOT calculate Y coordinates of ALIBOT object compatible with EMF files TA_BOTTOM alignment. +*/ +int trinfo_load_textrec(TR_INFO *tri, const TCHUNK_SPECS *tsp, double escapement, int flags){ + + int status; + double x,y,xe; + double asc,dsc; /* these are the ascender/descender for the actual text */ + int ymin,ymax; + double fasc,fdsc; /* these are the ascender/descender for the font as a whole (text independent) */ + TP_INFO *tpi; + FT_INFO *fti; + BR_INFO *bri; + int current,idx,taln; + uint32_t prev; + uint32_t *text32,*tptr; + FNT_SPECS *fsp; + BRECT_SPECS bsp; + + /* check incoming parameters */ + if(!tri)return(1); + if(!tsp)return(2); + if(!tsp->string)return(3); + fti = tri->fti; + tpi = tri->tpi; + bri = tri->bri; + idx = tsp->fi_idx; + taln = tsp->taln; + if(!fti->used)return(4); + if(idx <0 || idx >= (int) fti->used)return(5); + fsp = &(fti->fonts[idx]); + + if(!tri->dirty){ + tri->x = tsp->x; + tri->y = tsp->y; + tri->esc = escapement; + tri->dirty = 1; + } + else { + if(tri->esc != escapement)return(-1); + } + + + tpinfo_insert(tpi,tsp); + current=tpi->used-1; + ymin = 64000; + ymax = -64000; + + /* The geometry model has origin Y at the top of screen, positive Y is down, maximum positive + Y is at the bottom of the screen. That makes "top" (by positive Y) actually the bottom + (as viewed on the screen.) */ + + escapement *= 2.0 * M_PI / 360.0; /* degrees to radians */ + x = tpi->chunks[current].x - tri->x; /* convert to internal orientation */ + y = tpi->chunks[current].y - tri->y; + tpi->chunks[current].x = x * cos(escapement) - y * sin(escapement); /* coordinate transformation */ + tpi->chunks[current].y = x * sin(escapement) + y * cos(escapement); + +/* Careful! face bbox does NOT scale with FT_Set_Char_Size +printf("Face idx:%d bbox: xMax/Min:%ld,%ld yMax/Min:%ld,%ld UpEM:%d asc/des:%d,%d height:%d size:%f\n", + idx, + fsp->face->bbox.xMax,fsp->face->bbox.xMin, + fsp->face->bbox.yMax,fsp->face->bbox.yMin, + fsp->face->units_per_EM,fsp->face->ascender,fsp->face->descender,fsp->face->height,fsp->fsize); +*/ + + text32 = U_Utf8ToUtf32le((char *) tsp->string,0,NULL); + if(!text32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + text32 = U_Latin1ToUtf32le((char *) tsp->string,0,NULL); + if(!text32)return(5); + } + /* baseline advance is independent of character orientation */ + for(xe=0.0, prev=0, tptr=text32; *tptr; tptr++){ + status = TR_getadvance(fti, fsp, *tptr, (tri->use_kern ? prev: 0), tri->load_flags, tri->kern_mode, &ymin, &ymax); + if(status>=0){ + xe += ((double) status)/64.0; + } + else { return(6); } + prev=*tptr; + } + + /* Some glyphs in fonts have no vertical extent, for instance, Hebrew glyphs in Century Schoolbook L. + Use the 3/4 of the font size as a (very bad) approximation for the actual values. */ + if(ymin==0 && ymax==0){ + ymax = 0.75 * fsp->fsize * 64.0; + } + + asc = ((double) (ymax))/64.0; + dsc = ((double) (ymin))/64.0; /* This is negative */ +/* This did not work very well because the ascender/descender went well beyond the actual characters, causing + overlaps on lines that did not actually overlap (vertically). + asc = ((double) (fsp->face->ascender) )/64.0; + dsc = ((double) (fsp->face->descender))/64.0; +*/ + + free(text32); + + /* find the font ascender descender (general one, not specific for current text) */ + fasc = ((double) (fsp->face->ascender) )/64.0; + fdsc = ((double) (fsp->face->descender))/64.0; + + /* originally the denominator was just 32.0, but it broke when units_per_EM wasn't 2048 */ + double fixscale = tsp->fs/(((double) fsp->face->units_per_EM)/64.0); + if(tri->load_flags & FT_LOAD_NO_SCALE) xe *= fixscale; + + /* now place the rectangle using ALN information */ + if( taln & ALIHORI & ALILEFT ){ + bsp.xll = tpi->chunks[current].x; + bsp.xur = tpi->chunks[current].x + xe; + } + else if( taln & ALIHORI & ALICENTER){ + bsp.xll = tpi->chunks[current].x - xe/2.0; + bsp.xur = tpi->chunks[current].x + xe/2.0; + } + else{ /* taln & ALIHORI & ALIRIGHT */ + bsp.xll = tpi->chunks[current].x - xe; + bsp.xur = tpi->chunks[current].x; + } + tpi->chunks[current].ldir = tsp->ldir; + + if(tri->load_flags & FT_LOAD_NO_SCALE){ + asc *= fixscale; + dsc *= fixscale; + fasc *= fixscale; + fdsc *= fixscale; + } + + + /* From this point forward y is on the baseline, so need to correct it in chunks. The asc/dsc are the general + ones for the font, else the text content will muck around with the baseline in BAD ways. */ + if( taln & ALIVERT & ALITOP ){ tpi->chunks[current].y += fasc; } + else if( taln & ALIVERT & ALIBASE){ } /* no correction required */ + else{ /* taln & ALIVERT & ALIBOT */ + if(flags & TR_EMFBOT){ tpi->chunks[current].y -= 0.35 * tsp->fs; } /* compatible with EMF implementations */ + else { tpi->chunks[current].y += fdsc; } + } + tpi->chunks[current].boff = -dsc; + + /* since y is always on the baseline, the lower left and upper right are easy. These use asc/dsc for the particular text, + so that the bounding box will fit it tightly. */ + bsp.yll = tpi->chunks[current].y - dsc; + bsp.yur = tpi->chunks[current].y - asc; + brinfo_insert(bri,&bsp); + tpi->chunks[current].rt_tidx = bri->used - 1; /* index of rectangle that contains it */ + + return(0); +} + +/** + \brief Fontweight conversion. Fontconfig units to SVG units. + Anything not recognized becomes "normal" == 400. + There is no interpolation because a value that mapped to 775, for instance, most + likely would not display properly because it is intermediate between 700 and 800, and + only those need be supported in SVG viewers. + \returns SVG font weight + \param weight Fontconfig font weight. +*/ +int TR_weight_FC_to_SVG(int weight){ + int ret=400; + if( weight == 0){ ret = 100; } + else if(weight == 40){ ret = 200; } + else if(weight == 50){ ret = 300; } + else if(weight == 80){ ret = 400; } + else if(weight == 100){ ret = 500; } + else if(weight == 180){ ret = 600; } + else if(weight == 200){ ret = 700; } + else if(weight == 205){ ret = 800; } + else if(weight == 210){ ret = 900; } + else { ret = 400; } + return(ret); +} + +/** + \brief Set the padding that will be added to bounding rectangles before checking for overlaps in brinfo_overlap(). + \returns void + \param rt_pad pointer to an RT_PAD structure. + \param up padding for the top of a bounding rectangle. + \param down padding for the bottom of a bounding rectangle. + \param left padding for the left of a bounding rectangle. + \param right padding for the right of a bounding rectangle. +*/ +void TR_rt_pad_set(RT_PAD *rt_pad, double up, double down, double left, double right){ + rt_pad->up = up; + rt_pad->down = down; + rt_pad->left = left; + rt_pad->right = right; +} + +/** + \brief Convert from analyzed complexes to SVG format. + \returns void + \param tri pointer to a TR_INFO struct which will be analyzed. Result is stored in its "out" buffer. +*/ +void TR_layout_2_svg(TR_INFO *tri){ + double x = tri->x; + double y = tri->y; + double dx,dy; + double esc; + double recenter; /* horizontal offset to set things up correctly for CJ and RJ text, is 0 for LJ*/ + double lineheight=1.25; + int cutat; + FT_INFO *fti=tri->fti; /* Font info storage */ + TP_INFO *tpi=tri->tpi; /* Text Info/Position Info storage */ + BR_INFO *bri=tri->bri; /* bounding Rectangle Info storage */ + CX_INFO *cxi=tri->cxi; /* Complexes deduced for this text */ + TCHUNK_SPECS *tsp; /* current text object */ + CX_SPECS *csp; + CX_SPECS *cline_sp; + unsigned int i,j,k,jdx,kdx; + int ldir; + char obuf[1024]; /* big enough for style and so forth */ + char cbuf[16]; /* big enough for one hex color */ + + char stransform[128]; + double newx,newy,tmpx; + uint32_t utmp; + + /* copy the current numeric locale, make a copy because setlocale may stomp on + the memory it points to. Then change it because SVG needs decimal points, + not commas, in floats. Restore on exit from this routine. + */ + char *prev_locale = setlocale(LC_NUMERIC,NULL); + char *hold_locale = malloc(sizeof(char) * (strlen(prev_locale) + 1)); + strcpy(hold_locale,prev_locale); + (void) setlocale(LC_NUMERIC,"POSIX"); + +/* +#define DBG_TR_PARA 0 +#define DBG_TR_INPUT 1 +*/ + /* The debug section below is difficult to see if usebk is anything other than BKCLR_NONE */ +#if DBG_TR_PARA || DBG_TR_INPUT /* enable debugging code, writes extra information into SVG */ + /* put rectangles down for each text string - debugging!!! This will not work properly for any Narrow fonts */ + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + sprintf(stransform,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc), 1.25*x,1.25*y); + for(i=cxi->phase1; iused;i++){ /* over all complex members from phase2 == TR_PARA_* complexes */ + csp = &(cxi->cx[i]); + for(j=0; jkids.used; j++){ /* over all members of these complexes, which are phase1 complexes */ + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + for(k=0; kcx[jdx].kids.used; k++){ /* over all members of the phase1 complex */ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi */ + tsp = &tpi->chunks[kdx]; + ldir = tsp->ldir; + if(!j && !k){ +#if DBG_TR_PARA + TRPRINT(tri, "rects[csp->rt_cidx].xur - bri->rects[csp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].yll - bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].xll),1.25*(bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); +#endif /* DBG_TR_PARA */ + } +#if DBG_TR_INPUT /* debugging code, this section writes the original text objects */ + newx = 1.25*(ldir == LDIR_RL ? bri->rects[tsp->rt_tidx].xur : bri->rects[tsp->rt_tidx].xll); + newy = 1.25*(bri->rects[tsp->rt_tidx].yur); + TRPRINT(tri, "rects[tsp->rt_tidx].xur - bri->rects[tsp->rt_tidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].yll - bri->rects[tsp->rt_tidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].xll),newy); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + + newy = 1.25*(bri->rects[tsp->rt_tidx].yll - tsp->boff); + sprintf(obuf,"fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + sprintf(obuf,"text-anchor:%s;",(tsp->ldir == LDIR_RL ? "end" : "start")); + TRPRINT(tri, obuf); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + sprintf(obuf,"\n\">%s\n",&tsp->string[tsp->spaces]); + TRPRINT(tri, obuf); +#endif /* DBG_TR_INPUT debugging code, original text objects */ + } + } + } +#endif /* DBG_TR_PARA and/or DBG_TR_INPUT */ + + + if(tri->usebk){ + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + sprintf(stransform,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc), 1.25*x,1.25*y); + + for(i=cxi->phase1; iused;i++){ /* over all complex members from phase2 == TR_PARA_* complexes */ + TRPRINT(tri, "\n"); /* group backgrounds for each object in the SVG */ + csp = &(cxi->cx[i]); + for(j=0; jkids.used; j++){ /* over all members of these complexes, which are phase1 complexes */ + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + cline_sp = &(cxi->cx[jdx]); + if(tri->usebk == BKCLR_LINE){ + TRPRINT(tri, "bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[cline_sp->rt_cidx].xur - bri->rects[cline_sp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[cline_sp->rt_cidx].yll - bri->rects[cline_sp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[cline_sp->rt_cidx].xll),1.25*(bri->rects[cline_sp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + + for(k=0; kcx[jdx].kids.used; k++){ /* over all members of the phase1 complex */ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi */ + tsp = &tpi->chunks[kdx]; + ldir = tsp->ldir; + if(!j && !k){ + if(tri->usebk == BKCLR_ALL){ + TRPRINT(tri, "bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[csp->rt_cidx].xur - bri->rects[csp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].yll - bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].xll),1.25*(bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + } + if(tri->usebk == BKCLR_FRAG){ + newx = 1.25*(ldir == LDIR_RL ? bri->rects[tsp->rt_tidx].xur : bri->rects[tsp->rt_tidx].xll); + newy = 1.25*(bri->rects[tsp->rt_tidx].yur); + TRPRINT(tri, "bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[tsp->rt_tidx].xur - bri->rects[tsp->rt_tidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].yll - bri->rects[tsp->rt_tidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",newx,newy); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + } + } + TRPRINT(tri, "\n"); /* end of grouping for backgrounds for each object in the SVG */ + } + } + + + /* over all complex members from phase2. Paragraphs == TR_PARA_* */ + for(i=cxi->phase1; iused;i++){ + csp = &(cxi->cx[i]); + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + + /* over all members of the present Paragraph. Each of these is a line and a phase 1 complex. + It may be either TR_TEXT or TR_LINE */ + for(j=0; jkids.used; j++){ + if(j){ + sprintf(obuf,""); + TRPRINT(tri, obuf); + } + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + recenter = 0; /* mostly to quiet a compiler warning, should always be set below */ + + + /* over all members of the present Line. These are the original text objects which were reassembled. + There will be one for TR_TEXT, more than one for TR_LINE */ + for(k=0; kcx[jdx].kids.used; k++){ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi, for this k */ + tsp = &tpi->chunks[kdx]; /* text chunk for this k */ + ldir = tsp->ldir; /* language direction for this k */ + if(!k){ /* first iteration */ + switch(csp->type){ /* set up the alignment, if there is one */ + case TR_TEXT: + case TR_LINE: + /* these should never occur, this section quiets a compiler warning */ + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + if(ldir == LDIR_RL){ recenter = -(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll); } + else { recenter = 0.0; } + break; + case TR_PARA_CJ: + if(ldir == LDIR_RL){ recenter = -(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll)/2.0; } + else { recenter = +(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll)/2.0; } + break; + case TR_PARA_RJ: + if(ldir == LDIR_RL){ recenter = 0.0; } + else { recenter = +(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll); } + break; + } + if(!j){ + TRPRINT(tri, "fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + if(tsp->vadvance){ lineheight = tsp->vadvance *100.0; } + else { lineheight = 125.0; } + sprintf(obuf,"line-height:%f%%;",lineheight); + TRPRINT(tri, obuf); + TRPRINT(tri, "letter-spacing:0px;"); + TRPRINT(tri, "word-spacing:0px;"); + TRPRINT(tri, "fill:#000000;"); + TRPRINT(tri, "fill-opacity:1;"); + TRPRINT(tri, "stroke:none;"); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + switch(csp->type){ /* set up the alignment, if there is one */ + case TR_TEXT: + case TR_LINE: + /* these should never occur, this section quiets a compiler warning */ + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + sprintf(obuf,"text-align:start;text-anchor:start;"); + break; + case TR_PARA_CJ: + sprintf(obuf,"text-align:center;text-anchor:middle;"); + break; + case TR_PARA_RJ: + sprintf(obuf,"text-align:end;text-anchor:end;"); + break; + } + TRPRINT(tri, obuf); + TRPRINT(tri, "\"\n"); /* End of style specification */ + sprintf(obuf,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc),1.25*x,1.25*y); + TRPRINT(tri, obuf); + tmpx = 1.25*((ldir == LDIR_RL ? bri->rects[kdx].xur : bri->rects[kdx].xll) + recenter); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n>",tmpx,1.25*(bri->rects[kdx].yll - tsp->boff)); + TRPRINT(tri, obuf); + } + tmpx = 1.25*((ldir == LDIR_RL ? bri->rects[kdx].xur : bri->rects[kdx].xll) + recenter); + sprintf(obuf,"",tmpx,1.25*(bri->rects[kdx].yll - tsp->boff)); + TRPRINT(tri, obuf); + } + TRPRINT(tri, "xkern; + dy = 1.25 * tsp->ykern; + + sprintf(obuf,"dx=\"%f\" dy=\"%f\" ",dx, dy); + TRPRINT(tri, obuf); + sprintf(obuf,"style=\"fill:#%2.2X%2.2X%2.2X;",tsp->color.Red,tsp->color.Green,tsp->color.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"font-size:%fpx;",tsp->fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + if(tsp->decoration & TXTDECOR_TMASK){ + sprintf(obuf,"text-decoration:"); + /* multiple text decoration styles may be set */ + utmp = tsp->decoration & TXTDECOR_TMASK; + if(utmp & TXTDECOR_UNDER ){ strcat(obuf," underline"); } + if(utmp & TXTDECOR_OVER ){ strcat(obuf," overline"); } + if(utmp & TXTDECOR_BLINK ){ strcat(obuf," blink"); } + if(utmp & TXTDECOR_STRIKE){ strcat(obuf," line-through");} + if(*obuf){ + /* only a single text decoration line type may be set */ + switch(tsp->decoration & TXTDECOR_LMASK){ + case TXTDECOR_SOLID: break; // "solid" is the CSS 3 default, omitting it remains CSS 2 compatible + case TXTDECOR_DOUBLE: strcat(obuf," double"); break; // these are all CSS3 + case TXTDECOR_DOTTED: strcat(obuf," dotted"); break; + case TXTDECOR_DASHED: strcat(obuf," dashed"); break; + case TXTDECOR_WAVY: strcat(obuf," wavy" ); break; + default: break; + } + if((tsp->decoration & TXTDECOR_CLRSET) && memcmp(&(tsp->decColor),&(tsp->color),sizeof(TRCOLORREF))){ + /* CSS 3, CSS 2 implementations may choke on it. If the specified color matches text color omit, for better CSS 2 compatitiblity. */ + sprintf(cbuf," #%2.2X%2.2X%2.2X",tsp->decColor.Red,tsp->decColor.Green,tsp->decColor.Blue); + strcat(obuf,cbuf); + } + } + strcat(obuf,";"); + TRPRINT(tri,obuf); + } + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;\"",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + TRPRINT(tri, "\n>"); + TRPRINT(tri, (char *) tsp->string); + TRPRINT(tri, ""); + } /* end of k loop */ + } /* end of j loop */ + TRPRINT(tri,"\n"); + } /* end of i loop */ + + /* restore locale and free memory. */ + (void) setlocale(LC_NUMERIC,hold_locale); + free(hold_locale); +} + +/** + \brief Attempt to figure out the original organization, in lines and paragraphs, of the text objects. + The method is: + 1. Generate complexes from the text objects (strings) by overlaps (optionally allowing up to two spaces to be + added) to produce larger rectangles. Complexes that are more or less sequential and have 2 or more text objects + are TR_LINEs, therwise they are TR_TEXT. + 2. Group sequential complexes (TR_LINE or TR_TEXT) into TR_PARA_UJ (paragraphs,by smooth progression in vertical + position down page). + 3. Analyze the paragraphs to classify them as Left/Center/Right justified (possibly with indentation.) If + they do not fall into any of these categories break that one back down into TR_LINE/TR_TEXT. + 4. Return the number of complex text objects. + \returns Number of complexes. (>=1, <= number of text objects.) <0 is an error. + \param tri pointer to the TR_INFO structure holding the data, which will also hold the results. +*/ +int TR_layout_analyze(TR_INFO *tri){ + unsigned int i,j,k; + int ok; + int cxidx; + int src_rt; + int dst_rt; + TP_INFO *tpi; + BR_INFO *bri; + CX_INFO *cxi; + FT_INFO *fti; + BRECT_SPECS bsp; + RT_PAD rt_pad_i; + RT_PAD rt_pad_j; + double ratio; + double qsp,dx,dy; + double spcadv; + enum tr_classes type; + TCHUNK_SPECS *tspi; + TCHUNK_SPECS *tspj; + TCHUNK_SPECS *tspRevEnd=NULL; + TCHUNK_SPECS *tspRevStart=NULL; + CX_SPECS *csp; + CHILD_SPECS *kidp; /* used with preceding complex (see below) */ + CHILD_SPECS *kidc; /* used with current complex (see below) */ + int lastldir,ldir,rev; + + if(!tri)return(-1); + if(!tri->cxi)return(-2); + if(!tri->tpi)return(-3); + if(!tri->bri)return(-4); + if(!tri->fti)return(-5); + tpi=tri->tpi; + cxi=tri->cxi; + bri=tri->bri; + fti=tri->fti; + cxi->lines = 0; + cxi->paras = 0; + cxi->phase1 = 0; + +/* When debugging + ftinfo_dump(fti); +*/ + /* Phase 1. Working sequentially, insert text. Initially as TR_TEXT and then try to extend to TR_LINE by checking + overlaps. When done the complexes will contain a mix of TR_LINE and TR_TEXT. */ + + for(i=0; iused; i++){ + tspi = &(tpi->chunks[i]); + memcpy(&bsp,&(bri->rects[tspi->rt_tidx]),sizeof(BRECT_SPECS)); /* Must make a copy as next call may reallocate rects! */ + (void) brinfo_insert(bri,&bsp); + dst_rt = bri->used-1; + (void) cxinfo_insert(cxi, i, dst_rt, TR_TEXT); + cxidx = cxi->used-1; + + spcadv = fti->fonts[tspi->fi_idx].spcadv * tspi->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + /* for the leading text: pad with no leading and two trailing spaces, leading and trailing depend on direction */ + if(tspi->ldir == LDIR_RL){ TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, tri->qe + 2.0 * spcadv, 0.0); } + else { TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, 0.0, tri->qe + 2.0 * spcadv); } + + for(j=i+1; jused; j++){ + tspj = &(tpi->chunks[j]); + /* Reject font size changes of greater than 50%, these are almost certainly not continuous text. These happen + in math formulas, for instance, where a sum or integral is much larger than the other symbols. */ + ratio = (double)(tspj->fs)/(double)(tspi->fs); + if(ratio >2.0 || ratio <0.5)break; + + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + /* for the trailing text: pad with one leading and trailing spaces (so it should work L->R and R->L) */ + TR_rt_pad_set(&rt_pad_j,tri->qe, tri->qe, spcadv, spcadv); + src_rt = tspj->rt_tidx; + + /* Reject direction changes like [1 <- Hebrew][2 -> English], that is where the direction changes AND the + next logical piece of text is "upstream" positionally of its logical predecessor. The meaning of such + a construct is at best ambiguous. The test is only applied with respect to the first text chunk. This sort + of construct may appear when a valid initial construct like [1->English][2<-Hebrew][3->English] is edited + and the leading chunk of text removed. + + Also reject reversed order text as in (English) (draw order) arranged as . This happens + if the language direction field is incorrect, perhaps due to a corrupt or malformed input file. + */ + if(brinfo_upstream(bri, + dst_rt, /* index into bri for dst */ + src_rt, /* index into bri for src */ + tspi->ldir,tspj->ldir))break; + + if(!brinfo_overlap(bri, + dst_rt, /* index into bri for dst */ + src_rt, /* index into bri for src */ + &rt_pad_i,&rt_pad_j)){ + (void) cxinfo_append(cxi,j,TR_LINE); + (void) brinfo_merge(bri,dst_rt,src_rt); + /* for the leading text: pad with two leading and trailing spaces (so it should work L->R and R->L */ + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + TR_rt_pad_set(&rt_pad_i, tri->qe, tri->qe, + tri->qe + 2.0 * spcadv, tri->qe + 2.0 * spcadv); + } + else { /* either alignment ge*/ + break; + } + } + + /* Bidirectional text will cause complexes to not assemble in one pass. + This happens whenever a change of direction occurs with 2 or more sequential elements in + the opposite direction, + + Let + = LR and - = RL. + + Reading left to right, this happens with +-- or -++. + For instance, the sequence ++-+ ---+ would break into the two complexes shown. + Not until the last element in the second complex is added will the bounding rectangles for the complexes overlap. + + Check for this effect now if there is a preceding complex and the first element of the current complex is + reversed from the last in the preceding. */ + if(cxidx >= 1){ + kidp = &(cxi->cx[cxidx-1].kids); + kidc = &(cxi->cx[cxidx ].kids); + tspi = &(tpi->chunks[ kidp->members[kidp->used - 1] ]); /* here, the last text element in preceding complex */ + tspj = &(tpi->chunks[ kidc->members[0 ] ]); /* here, tge first text element in current complex */ + if(tspi->ldir != tspj->ldir){ + spcadv = fti->fonts[tspi->fi_idx].spcadv * tspi->fs/32.0; + if(tspi->ldir == LDIR_RL){ TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, tri->qe + 2.0 * spcadv, 0.0); } + else { TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, 0.0, tri->qe + 2.0 * spcadv); } + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; + TR_rt_pad_set(&rt_pad_j,tri->qe, tri->qe, spcadv, spcadv); + if(!brinfo_overlap(bri, + cxi->cx[cxidx-1].rt_cidx, /* index into rt for dst cx */ + cxi->cx[cxidx].rt_cidx, /* index into rt for src cx */ + &rt_pad_i,&rt_pad_j)){ + /* Merge the current complex into the preceding one*/ + (void) cxinfo_merge(cxi, cxidx-1, cxidx, TR_LINE); + (void) brinfo_merge(bri,cxi->cx[cxidx-1].rt_cidx,cxi->cx[cxidx].rt_cidx); /* merge the bounding boxes*/ + (void) cxinfo_trim(cxi); + cxi->lines--; /* else the normal line count value is one too high */ + /* remove the current complex */ + } + } + } + + if(cxi->cx[cxidx].type == TR_LINE)cxi->lines++; + i=j-1; /* start up after the last merged entry (there may not be any) */ + } + cxi->phase1 = cxi->used; /* total complexes defined in this phase, all TR_LINE or TR_TEXT */ + + /* phase 1.5, calculate kerning. This is as good a place to do it as any. At this point all kern values + are zero. Each of these pieces is strictly unidirectional, but each piece can have a different direction. + The direction of the line is set by the first text element. The ends of runs of elements which are + reversed with respect to the line direction are special, everything else is simple: + Let: + == L->R, - == R->L, $ == end of text, the rules for kerning on B are: + A B others xkern + [+|$] + + [+|$] Bll - Aur + [-|$] - - [-|$] All - Bur (chs) + + - + [-|$] Bll - Aur (chs) + - + - [+|$] All - Bur + + - -...[-=C] [+|$] All - Cur (chs) + - + +...[+=C] [-|$] Cll - Aur + + chs = change sign, because dx is an absolute direction, and direction of text on RTL is in -x. + + Kerning calculations currently seems unstable for R->L if the kerning extends to the end of the line. If + the first and last characters are back in sync there are no issues. When things go south R->L left justified + text is not justified when read in. + */ + + for(i=0; i < cxi->phase1; i++){ /* over all lines */ + csp = &(cxi->cx[i]); + if(csp->kids.used < 2)continue; /* no kerning possible */ + tspi = &tpi->chunks[csp->kids.members[0]]; /* used here as last tsp. no kerning is applied to the first element */ + lastldir = ldir = tspi->ldir; + rev = 0; /* the first ldir defines forward and reverse */ + for(j=1; jkids.used; j++){ + tspj = &tpi->chunks[csp->kids.members[j]]; + ldir = tspj->ldir; + if(ldir != lastldir){ /* direction change */ + rev = !rev; /* reverse direction tracker */ + if(!rev){ /* back in original orientation */ + if(ldir == LDIR_RL){ tspj->xkern = bri->rects[tspj->rt_tidx].xur - bri->rects[tspRevStart->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspj->rt_tidx].xll - bri->rects[tspRevStart->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspj->rt_tidx].yll - tspj->boff) - + (bri->rects[tspRevStart->rt_tidx].yll - tspRevStart->boff); + } + else { /* now in reversed orientation */ + tspRevStart = tspj; /* Save the beginning of this run (length >=1 ) */ + /* scan forward for the last text object in this orientation, include the first */ + for(k=j; k kids.used; k++){ + if(tpi->chunks[csp->kids.members[k]].ldir == ldir){ tspRevEnd = &tpi->chunks[csp->kids.members[k]]; } + else { break; } + } + if(lastldir == LDIR_RL){ tspj->xkern = bri->rects[tspRevEnd->rt_tidx].xur - bri->rects[tspi->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspRevEnd->rt_tidx].xll - bri->rects[tspi->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspRevEnd->rt_tidx].yll - tspRevEnd->boff) - + (bri->rects[ tspi->rt_tidx].yll - tspi->boff ); + } + } + else { + if(ldir == LDIR_RL){ tspj->xkern = bri->rects[tspj->rt_tidx].xur - bri->rects[tspi->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspj->rt_tidx].xll - bri->rects[tspi->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspj->rt_tidx].yll - tspj->boff) - + (bri->rects[tspi->rt_tidx].yll - tspi->boff); + } + + + /* + Sometimes a font substitution was absolutely terrible, for instance, for Arial Narrow on (most) Linux systems, + The resulting advance (xkern) may be much too large so that it overruns the next text chunk. Since + overlapping text on the same line is almost never encountered, this may be used to detect the bad + substitution so that a more appropriate offset can be used. + Detect this situation as a negative dx < 1/2 a space character's width while |dy| < an entire space width. + The y constraints allow super and subscripts, which overlap in x but are shifted above/below in y. + */ + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; + qsp = 0.25 * spcadv; + dx = tspj->xkern; + dy = tspj->ykern; + if(dy <=qsp && dy >= -qsp){ + if(ldir==LDIR_RL){ + if(dx > 2*qsp)tspj->xkern = 0.0; + } + else { + if(dx < -2*qsp)tspj->xkern = 0.0; + } + } + + /* if x or y kern is less than twice the quantization error it is probably noise, set it to zero */ + if(fabs(tspj->xkern) <= 2.0*tri->qe)tspj->xkern = 0.0; + if(fabs(tspj->ykern) <= 2.0*tri->qe)tspj->ykern = 0.0; + + /* reintroduce spaces on the leading edge of text "j" if the kerning can be in part or in whole replaced + with 1 or 2 spaces */ + if(tspj->ykern == 0.0){ + double spaces = tspj->xkern/spcadv; /* negative on RL language, positive on LR */ + if((ldir == LDIR_RL && (spaces <= -0.9 && spaces >= -2.1)) || + (ldir == LDIR_LR && (spaces >= 0.9 && spaces <= 2.1)) ){ + int ispaces = lround(spaces); + tspj->xkern -= ((double)ispaces*spcadv); + if(ispaces<0)ispaces=-ispaces; + size_t slen = strlen((char *)tspj->string); + uint8_t *newstring = malloc(1 + ispaces + slen); + sprintf((char *)newstring," "); /* start with two spaces, possibly overwrite one in the next line */ + memcpy(newstring+ispaces,tspj->string,slen+1); /* copy existing string to proper position */ + free(tspj->string); + tspj->string = newstring; + tspj->spaces = ispaces; // only needed to fix optional debugging SVG output later + } + } + + tspi = tspj; + lastldir = ldir; + } + } + + + /* Phase 2, try to group sequential lines. There may be "lines" that are still TR_TEXT, as in: + + ... this is a sentence that wraps by one + word. + + And some paragrahs might be single word lines (+ = bullet in the following) + + +verbs + +nouns + +adjectives + + Everything starts out as TR_PARA_UJ and if the next one can be lined up, the type changes to + an aligned paragraph and complexes are appended to the existing one. + */ + + for(i=0; i < cxi->phase1; i++){ + type = TR_PARA_UJ; /* any paragraph alignment will be acceptable */ + /* Must make a copy as next call may reallocate rects, so if we just passed a pointer to something in the structure + it would vaporize part way through the call. */ + memcpy(&bsp,&(bri->rects[cxi->cx[i].rt_cidx]),sizeof(BRECT_SPECS)); + (void) brinfo_insert(bri,&bsp); + dst_rt = bri->used-1; + (void) cxinfo_insert(cxi, i, dst_rt, type); + + cxi->paras++; + ok = 1; + for(j=i+1; ok && (j < cxi->phase1); j++){ + type = brinfo_pp_alignment(bri, cxi->cx[i].rt_cidx, cxi->cx[j].rt_cidx, 3*tri->qe, type); + switch (type){ + case TR_PARA_UJ: /* paragraph type was set and j line does not fit, or no paragraph alignment matched */ + ok = 0; /* force exit from j loop */ + j--; /* this will increment at loop bottom */ + break; + case TR_PARA_LJ: + case TR_PARA_CJ: + case TR_PARA_RJ: + /* two successive lines have been identified (possible following others already in the paragraph */ + if(TR_check_set_vadvance(tri,j,i)){ /* check for compatibility with vadvance if set, set it if it isn't. */ + ok = 0; /* force exit from j loop */ + j--; /* this will increment at loop bottom */ + } + else { + src_rt = cxi->cx[j].rt_cidx; + (void) cxinfo_append(cxi, j, type); + (void) brinfo_merge(bri, dst_rt, src_rt); + } + break; + default: + return(-6); /* programming error */ + } + } + if(j>=cxi->phase1)break; + i=j-1; + } + + +/* When debugging + cxinfo_dump(tri); +*/ + + return(cxi->used); +} + + +/* no doxygen documentation below this point, these pieces are for the text program, not the library. */ + +#if TEST +#define MAXLINE 2048 /* big enough for testing */ +enum OP_TYPES {OPCOM,OPOOPS,OPFONT,OPESC,OPORI,OPXY,OPFS,OPTEXT,OPALN,OPLDIR,OPMUL,OPITA,OPWGT,OPDEC,OPCND,OPBKG,OPCLR,OPDCLR,OPBCLR,OPFLAGS,OPEMIT,OPDONE}; + +int parseit(char *buffer,char **data){ + int pre; + pre = strcspn(buffer,":"); + if(!pre)return(OPOOPS); + *data=&buffer[pre+1]; + buffer[pre]='\0'; + if(*buffer=='#' )return(OPCOM ); + if(0==strcmp("FONT",buffer))return(OPFONT); + if(0==strcmp("ESC" ,buffer))return(OPESC ); + if(0==strcmp("ORI", buffer))return(OPORI ); + if(0==strcmp("XY", buffer))return(OPXY ); + if(0==strcmp("FS", buffer))return(OPFS ); + if(0==strcmp("TEXT",buffer))return(OPTEXT); + if(0==strcmp("ALN", buffer))return(OPALN ); + if(0==strcmp("LDIR",buffer))return(OPLDIR); + if(0==strcmp("MUL", buffer))return(OPMUL ); + if(0==strcmp("ITA", buffer))return(OPITA ); + if(0==strcmp("WGT", buffer))return(OPWGT ); + if(0==strcmp("DEC", buffer))return(OPDEC ); + if(0==strcmp("CND", buffer))return(OPCND ); + if(0==strcmp("BKG", buffer))return(OPBKG ); + if(0==strcmp("CLR", buffer))return(OPCLR ); + if(0==strcmp("DCLR", buffer))return(OPDCLR ); + if(0==strcmp("BCLR",buffer))return(OPBCLR ); + if(0==strcmp("FLAG",buffer))return(OPFLAGS); + if(0==strcmp("EMIT",buffer))return(OPEMIT); + if(0==strcmp("DONE",buffer))return(OPDONE); + return(OPOOPS); +} + +void boom(char *string,int lineno){ + fprintf(stderr,"Fatal error at line %d %s\n",lineno,string); + exit(EXIT_FAILURE); +} + + +void init_as_svg(TR_INFO *tri){ + TRPRINT(tri,"\n"); + TRPRINT(tri,"\n"); + TRPRINT(tri,"\n"); + TRPRINT(tri,"\n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," image/svg+xml\n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri," \n"); + TRPRINT(tri,"\n"); +} + + +void flush_as_svg(TR_INFO *tri, FILE *fp){ + fwrite(tri->out,tri->outused,1,fp); +} + +FILE *close_as_svg(TR_INFO *tri, FILE *fp){ + TRPRINT(tri, " \n"); + TRPRINT(tri, "\n"); + flush_as_svg(tri,fp); + fclose(fp); + return(NULL); +} + + +int main(int argc, char *argv[]){ + char *data; + char inbuf[MAXLINE]; + FILE *fpi = NULL; + FILE *fpo = NULL; + int op; + double fact = 1.0; /* input units to points */ + double escapement = 0.0; /* degrees */ + int lineno = 0; + int ok = 1; + int status; + TCHUNK_SPECS tsp; + TR_INFO *tri=NULL; + int flags=0; + char *infile; + uint32_t utmp32; + TRCOLORREF bkcolor; + int bkmode; + char *fontspec; + + infile=malloc(strlen(argv[1])+1); + strcpy(infile,argv[1]); + + if(argc < 2 || !(fpi = fopen(infile,"r"))){ + printf("Usage: text_reassemble input_file\n"); + printf(" Test program reads an input file containing lines like:\n"); + printf(" FONT:(font for next text)\n"); + printf(" ESC:(escapement angle degrees of text line, up from X axis)\n"); + printf(" ORI:(angle degrees of character orientation, up from X axis)\n"); + printf(" FS:(font size, units)\n"); + printf(" XY:(x,y) X 0 is at left, N is at right, Y 0 is at top, N is at bottom, as page is viewed.\n"); + printf(" TEXT:(UTF8 text)\n"); + printf(" ALN:combination of {LCR}{BLT} = Text is placed on {X,Y} at Left/Center/Right of text, at Bottom,baseLine,Top of text.\n"); + printf(" LDIR:{LR|RL|TB) Left to Right, Right to Left, and Top to Bottom \n"); + printf(" MUL:(float, multiplicative factor to convert FS,XY units to points).\n"); + printf(" ITA:(Italics, 0=normal, 100=italics, 110=oblique).\n"); + printf(" WGT:(Weight, 0-215: 80=normal, 200=bold, 215=ultrablack, 0=thin)).\n"); + printf(" DEC:(this is a bit field. For color see DCLR\n"); + printf(" style: 000 none, 001 underline,002 overline, 004 blink, 008 strike-through\n"); + printf(" line: 000 solid, 010 double, 020 dotted, 040 dashed, 080 wavy)\n"); + printf(" CND:(Condensed 50-200: 100=normal, 50=ultracondensed, 75=condensed, 200=expanded).\n"); + printf(" BKG:(Background color: 0 none, 1 by input fragment, 2 by assembled line, 3 by entire assembly. Use BCLR, THEN BKG) \n"); + printf(" CLR:(Text RGB color, as 6 HEX digits, like: FF0000 (red) or 0000FF (blue)) \n"); + printf(" DCLR:(Decoration color, specify like CLR, except 1000000 or higher disables.)\n"); + printf(" BCLR:(Background RGB color, specify like CLR.) \n"); + printf(" FLAG: Special processing options. 1 EMF compatible text alignment.\n"); + printf(" EMIT:(Process everything up to this point, then start clean for remaining input).\n"); + printf(" DONE:(no more input, process it).\n"); + printf(" # comment\n"); + printf("\n"); + printf(" The output is a summary of how the pieces are to be assembled into complex text.\n"); + printf("\n"); + printf(" egrep pattern: '^LOAD:|^FONT:|^ESC:|^ORI:|^FS:|^XY:|^TEXT:|^ALN:|^LDIR:|^MUL:|^ITA:|^WGT:|^DEC:|^CND:|^BKG:|^CLR:|^BCLR:|^DCLR:|^FLAG:|^EMIT:^DONE:'\n"); + exit(EXIT_FAILURE); + } + + tri = trinfo_init(tri); /* If it loops the trinfo_clear at the end will reset tri to the proper state, do NOT call trinfo_init twice! */ + +#ifdef DBG_LOOP + int ldx; + for(ldx=0;ldx<5;ldx++){ + if(fpi)fclose(fpi); + fpi = fopen(infile,"r"); +#endif + tsp.string = NULL; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.color.Red = tsp.decColor.Red = 0; /* RGBA Black */ + tsp.color.Green = tsp.decColor.Green = 0; /* RGBA Black */ + tsp.color.Blue = tsp.decColor.Blue = 0; /* RGBA Black */ + tsp.color.Reserved = tsp.decColor.Reserved = 0; /* unused */ + tsp.italics = 0; + tsp.weight = 80; + tsp.condensed = 100; + tsp.decoration = 0; /* none */ + tsp.spaces = 0; /* none */ + tsp.fi_idx = -1; /* set to an invalid */ + tsp.rt_tidx = -1; /* set to an invalid */ + tsp.xkern = tsp.ykern = 0.0; + /* no need to set rt_tidx */ + + + + if(!tri){ + fprintf(stderr,"Fatal error, could not initialize data structures\n"); + exit(EXIT_FAILURE); + } + (void) trinfo_load_ft_opts(tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + fpo=fopen("dump.svg","wb"); + init_as_svg(tri); + + while(ok){ + lineno++; + if(!fgets(inbuf,MAXLINE,fpi))boom("Unexpected end of file - no DONE:",lineno); + inbuf[strlen(inbuf)-1]='\0'; /* step on the EOL character */ + op = parseit(inbuf,&data); + switch(op){ + case OPCOM: /* ignore comments*/ + break; + case OPFONT: + /* If the font name includes "Narrow" condensed may not have been set */ + if(0<= TR_findcasesub(data, "Narrow")){ + tsp.co=1; + } + else { + tsp.co=0; + } + fontspec = TR_construct_fontspec(&tsp, data); + if((tsp.fi_idx = ftinfo_load_fontname(tri->fti, fontspec)) < 0 )boom("Font load failed",lineno); + free(fontspec); + break; + case OPESC: + if(1 != sscanf(data,"%lf",&escapement))boom("Invalid ESC:",lineno); + break; + case OPORI: + if(1 != sscanf(data,"%lf",&tsp.ori))boom("Invalid ORI:",lineno); + break; + case OPFS: + if(1 != sscanf(data,"%lf",&tsp.fs) || tsp.fs <= 0.0)boom("Invalid FS:",lineno); + tsp.fs *= fact; + break; + case OPXY: + if(2 != sscanf(data,"%lf,%lf",&tsp.x,&tsp.y) )boom("Invalid XY:",lineno); + tsp.x *= fact; + tsp.y *= fact; + break; + case OPTEXT: + tsp.string = (uint8_t *) U_strdup(data); + /* FreeType parameters match inkscape*/ + status = trinfo_load_textrec(tri, &tsp, escapement,flags); + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + if(trinfo_load_textrec(tri, &tsp, escapement,flags)){ boom("Text load failed",lineno); } + } + else if(status){ boom("Text load failed",lineno); } + break; + case OPALN: + tsp.taln=0; + switch (*data++){ + case 'L': tsp.taln |= ALILEFT; break; + case 'C': tsp.taln |= ALICENTER; break; + case 'R': tsp.taln |= ALIRIGHT; break; + default: boom("Invalid ALN:",lineno); + } + switch (*data++){ + case 'T': tsp.taln |= ALITOP; break; + case 'L': tsp.taln |= ALIBASE; break; + case 'B': tsp.taln |= ALIBOT; break; + default: boom("Invalid ALN:",lineno); + } + break; + case OPLDIR: + tsp.ldir=0; + if(0==strcmp("LR",data)){ tsp.ldir=LDIR_LR; break;} + if(0==strcmp("RL",data)){ tsp.ldir=LDIR_RL; break;} + if(0==strcmp("TB",data)){ tsp.ldir=LDIR_TB; break;} + boom("Invalid LDIR:",lineno); + break; + case OPMUL: + if(1 != sscanf(data,"%lf",&fact) || fact <= 0.0)boom("Invalid MUL:",lineno); + (void) trinfo_load_qe(tri,fact); + break; + case OPITA: + if(1 != sscanf(data,"%d",&tsp.italics) || tsp.italics < 0 || tsp.italics>110)boom("Invalid ITA:",lineno); + break; + case OPWGT: + if(1 != sscanf(data,"%d",&tsp.weight) || tsp.weight < 0 || tsp.weight > 215)boom("Invalid WGT:",lineno); + break; + case OPDEC: + if(1 != sscanf(data,"%X",(unsigned int *) &tsp.decoration))boom("Invalid DEC:",lineno); + break; + case OPCND: + if(1 != sscanf(data,"%d",&tsp.condensed) || tsp.condensed < 50 || tsp.condensed > 200)boom("Invalid CND:",lineno); + break; + case OPBKG: + if(1 != sscanf(data,"%d",&bkmode) )boom("Invalid BKG:",lineno); + (void) trinfo_load_bk(tri,bkmode,bkcolor); + break; + case OPCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid CLR:",lineno); + tsp.color.Red = (utmp32 >> 16) & 0xFF; + tsp.color.Green = (utmp32 >> 8) & 0xFF; + tsp.color.Blue = (utmp32 >> 0) & 0xFF; + tsp.color.Reserved = 0; + break; + case OPDCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid DCLR:",lineno); + if(utmp32 >= 0x1000000){ + tsp.decColor.Red = tsp.decColor.Green = tsp.decColor.Blue = tsp.decColor.Reserved = 0; + tsp.decoration &= ~TXTDECOR_CLRSET; + } + else { + tsp.decColor.Red = (utmp32 >> 16) & 0xFF; + tsp.decColor.Green = (utmp32 >> 8) & 0xFF; + tsp.decColor.Blue = (utmp32 >> 0) & 0xFF; + tsp.decColor.Reserved = 0; + tsp.decoration |= TXTDECOR_CLRSET; + } + break; + case OPBCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid BCLR:",lineno); + bkcolor.Red = (utmp32 >> 16) & 0xFF; + bkcolor.Green = (utmp32 >> 8) & 0xFF; + bkcolor.Blue = (utmp32 >> 0) & 0xFF; + bkcolor.Reserved = 0; + break; + case OPFLAGS: + if(1 != sscanf(data,"%d",&flags) )boom("Invalid FLAG:",lineno); + break; + case OPEMIT: + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + break; + case OPDONE: + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + ok = 0; + break; + case OPOOPS: + default: + boom("Input line cannot be parsed",lineno); + break; + } + + } + + if(fpo){ + fpo=close_as_svg(tri, fpo); + } + + +#ifdef DBG_LOOP + tri = trinfo_clear(tri); + ok = 1; + } +#endif /* DBG_LOOP */ + + fclose(fpi); + tri = trinfo_release(tri); + free(infile); + + exit(EXIT_SUCCESS); +} +#endif /* TEST */ + +#ifdef __cplusplus +} +#endif diff --git a/src/extension/internal/text_reassemble.h b/src/extension/internal/text_reassemble.h new file mode 100644 index 0000000..25b556a --- /dev/null +++ b/src/extension/internal/text_reassemble.h @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * text_reassemble.h from libTERE + *//* + * Authors: see below + * + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2.0+, read the file 'COPYING' for more information. + */ +/** + @file text_reassemble.h libTERE headers. + +See text_reassemble.c for notes + +File: text_reassemble.h +Version: 0.0.13 +Date: 06-FEB-2014 +Author: David Mathog, Biology Division, Caltech +email: mathog@caltech.edu +Copyright: 2014 David Mathog and California Institute of Technology (Caltech) +*/ + +#ifndef _TEXT_REASSEMBLE_ +#define _TEXT_REASSEMBLE_ + +#ifdef __cplusplus +extern "C" { +#endif + + +#include //NOLINT +#include //NOLINT +#include //NOLINT +#include //NOLINT +#include //NOLINT +#include +#include +#include +#include FT_FREETYPE_H +#include FT_GLYPH_H + +/** \cond */ +#define TEREMIN(A,B) (A < B ? A : B) +#define TEREMAX(A,B) (A > B ? A : B) + +#ifndef M_PI +# define M_PI 3.14159265358979323846 /* pi */ +#endif +#define ALLOCINFO_CHUNK 32 +#define ALLOCOUT_CHUNK 8192 +#define TRPRINT trinfo_append_out +/** \endcond */ + +/** \defgroup color background options + Text is underwritten with the background color not at all, + by reassembled line, or by full assembly . + @{ +*/ +#define BKCLR_NONE 0x00 /**< text is not underwritten with background color (default) */ +#define BKCLR_FRAG 0x01 /**< each fragment of text is underwritten with background color */ +#define BKCLR_LINE 0x02 /**< each line of text is underwritten with background color */ +#define BKCLR_ALL 0x03 /**< entire assembly is underwritten with background color */ +/** @} */ + +/** \defgroup decoration options + One of these values may be present in the decoration field. + Unused bits may be used by end user code. + These values are SVG specific. Other applications could use the text + decoration field for a different set of bits, so long as it provided its own + output function. + @{ +*/ +#define TXTDECOR_NONE 0x000 /**< text is not decorated (default) */ +#define TXTDECOR_UNDER 0x001 /**< underlined */ +#define TXTDECOR_OVER 0x002 /**< overlined */ +#define TXTDECOR_BLINK 0x004 /**< blinking text */ +#define TXTDECOR_STRIKE 0x008 /**< strike through */ +#define TXTDECOR_TMASK 0x00F /**< Mask for selecting bits above */ + +#define TXTDECOR_SOLID 0x000 /**< draw as single solid line */ +#define TXTDECOR_DOUBLE 0x010 /**< draw as double solid line */ +#define TXTDECOR_DOTTED 0x020 /**< draw as single dotted line */ +#define TXTDECOR_DASHED 0x040 /**< draw as single dashed line */ +#define TXTDECOR_WAVY 0x080 /**< draw as single wavy line */ +#define TXTDECOR_LMASK 0x0F0 /**< Mask for selecting these bits */ + +#define TXTDECOR_CLRSET 0x100 /**< decoration has its own color */ + +/** @} */ + + + + + +/** \defgroup text alignment types + Location of text's {X,Y} coordinate on bounding rectangle. + Values are compatible with Fontconfig. + @{ +*/ +#define ALILEFT 0x01 /**< text object horizontal alignment = left */ +#define ALICENTER 0x02 /**< text object horizontal alignment = center */ +#define ALIRIGHT 0x04 /**< text object horizontal alignment = right */ +#define ALIHORI 0x07 /**< text object horizontal alignment mask */ +#define ALITOP 0x08 /**< text object vertical alignment = top */ +#define ALIBASE 0x10 /**< text object vertical alignment = baseline */ +#define ALIBOT 0x20 /**< text object vertical alignment = bottom */ +#define ALIVERT 0x38 /**< text object vertical alignment mask */ +/** @} */ + +/** \defgroup language direction types + @{ +*/ +#define LDIR_LR 0x00 /**< left to right */ +#define LDIR_RL 0x01 /**< right to left */ +#define LDIR_TB 0x02 /**< top to bottom */ +/** @} */ + +/** \defgroup special processing flags + @{ +*/ +#define TR_EMFBOT 0x01 /**< use an approximation compatible with EMF file's "BOTTOM" text orientation, which is not the "bottom" for Freetype fonts */ +/** @} */ + +/** \enum tr_classes +classification of complexes + @{ +*/ +enum tr_classes { + TR_TEXT, /**< simple text object */ + TR_LINE, /**< linear assembly of TR_TEXTs */ + TR_PARA_UJ, /**< sequential assembly of TR_LINEs and TR_TEXTs into a paragraph - + unknown justification properties */ + TR_PARA_LJ, /**< ditto, left justified */ + TR_PARA_CJ, /**< ditto, center justified */ + TR_PARA_RJ /**< ditto, right justified */ + }; +/** @} */ + +/** + \brief alt font entries. +*/ +typedef struct { + uint32_t fi_idx; /**< index into FT_INFO fonts, for fonts added for missing glyphs */ + uint32_t weight; /**< integer weight for alt fonts, kept sorted into descending order */ +} ALT_SPECS; + +/** + \brief Information for a font instance. +*/ +typedef struct { + FcFontSet *fontset; /**< all matching fonts (for fallback on missing glyphs) */ + ALT_SPECS *alts; /**< index into FT_INFO fonts, for fonts added for missing glyphs */ + uint32_t space; /**< alts storage slots allocated */ + uint32_t used; /**< alts storage slots in use */ + FT_Face face; /**< font face structures (FT_FACE is a pointer!) */ + uint8_t *file; /**< pointer to font paths to files */ + uint8_t *fontspec; /**< pointer to a font specification (name:italics, etc.) */ + FcPattern *fpat; /**< current font, must hang onto this or faces operations break */ + double spcadv; /**< advance equal to a space, in points at font's face size */ + double fsize; /**< font's face size in points */ +} FNT_SPECS; + +/** + \brief Information for all font instances. +*/ +typedef struct { + FT_Library library; /**< Fontconfig handle */ + FNT_SPECS *fonts; /**< Array of fontinfo structures */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} FT_INFO; + +typedef struct { + uint8_t Red; //!< Red color (0-255) + uint8_t Green; //!< Green color (0-255) + uint8_t Blue; //!< Blue color (0-255) + uint8_t Reserved; //!< Not used +} TRCOLORREF; + +/** + \brief Information for a single text object +*/ +typedef struct { + uint8_t *string; /**< UTF-8 text */ + double ori; /**< Orientation, angle of characters with respect to baseline in degrees */ + double fs; /**< font size of text */ + double x; /**< x coordinate, relative to TR_INFO x,y, in points */ + double y; /**< y coordinate, relative to TR_INFO x,y, in points */ + double xkern; /**< x kern relative to preceding text chunk in complex (if any) */ + double ykern; /**< y kern relative to preceding text chunk in complex (if any) */ + double boff; /**< Y LL corner - boff finds baseline */ + double vadvance; /**< Line spacing typically 1.25 or 1.2, only set on the first text + element in a complex */ + TRCOLORREF color; /**< RGB */ + int taln; /**< text alignment with respect to x,y */ + int ldir; /**< language direction LDIR_* */ + int italics; /**< italics, as in FontConfig */ + int weight; /**< weight, as in FontConfig */ + int condensed; /**< condensed, as in FontConfig */ + int decoration; /**< text decorations, ignored during assembly, used during output */ + int spaces; /**< count of spaces converted from wide kerning (1 or 2) */ + TRCOLORREF decColor; /**< text decoration color, ignored during assembly, used during output */ + int co; /**< condensed override, if set Font name included narrow */ + int rt_tidx; /**< index of rectangle that contains it */ + int fi_idx; /**< index of the font it uses */ +} TCHUNK_SPECS; + +/** + \brief Information for all text objects. + Coordinates here are INTERNAL, after offset/rotate using values in TR_INFO. +*/ +typedef struct { + TCHUNK_SPECS *chunks; /**< text chunks */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} TP_INFO; + +/** + \brief Information for a single bounding rectangle. + Coordinates here are INTERNAL, after offset/rotate using values in TR_INFO. +*/ +typedef struct { + double xll; /**< x rectangle lower left corner */ + double yll; /**< y " */ + double xur; /**< x upper right corner */ + double yur; /**< y " */ + double xbearing; /**< x bearing of the leftmost character */ +} BRECT_SPECS; + +/** + \brief Information for all bounding rectangles. +*/ +typedef struct { + BRECT_SPECS *rects; /**< bounding rectangles */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} BR_INFO; + +/** + \brief List of all members of a single complex. +*/ +typedef struct { + int *members; /**< array of immediate children (for TR_PARA_* these are indices + for TR_TEXT or TR_LINE complexes also in cxi. For TR_TEXT + and TR_LINE these are indices to the actual text in tpi.) */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} CHILD_SPECS; + +/** + \brief Information for a single complex. +*/ +typedef struct { + int rt_cidx; /**< index of rectangle that contains all members */ + enum tr_classes type; /**< classification of the complex */ + CHILD_SPECS kids; /**< immediate child nodes of this complex, for type TR_TEXT the + idx refers to the tpi data. otherwise, cxi data */ +} CX_SPECS; + +/** + \brief Information for all complexes. +*/ +typedef struct { + CX_SPECS *cx; /**< complexes */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ + uint32_t phase1; /**< Number of complexes (lines + text fragments) entered in phase 1 */ + uint32_t lines; /**< Number of lines in phase 1 */ + uint32_t paras; /**< Number of complexes (paras) entered in phase 2 */ +} CX_INFO; + +/** + \brief Information for the entire text reassembly system. +*/ +typedef struct { + FT_INFO *fti; /**< Font info storage */ + TP_INFO *tpi; /**< Text Info/Position Info storage */ + BR_INFO *bri; /**< Bounding Rectangle Info storage */ + CX_INFO *cxi; /**< Complex Info storage */ + uint8_t *out; /**< buffer to hold formatted output */ + double qe; /**< quantization error in points. */ + double esc; /**< escapement angle in DEGREES */ + double x; /**< x coordinate of first text object, in points */ + double y; /**< y coordinate of first text object, in points */ + int dirty; /**< 1 if text records are loaded */ + int use_kern; /**< 1 if kerning is used, 0 if not */ + int load_flags; /**< FT_LOAD_NO_SCALE or FT_LOAD_TARGET_NORMAL */ + int kern_mode; /**< FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED */ + uint32_t outspace; /**< storage in output buffer allocated */ + uint32_t outused; /**< storage in output buffer in use */ + int usebk; /**< On output write the background color under the text */ + TRCOLORREF bkcolor; /**< RGB background color */ +} TR_INFO; + +/* padding added to rectangles before overlap test */ +/** + \brief Information for one padding record. (Padding is added to bounding rectangles before overlap tests.) +*/ +typedef struct { + double up; /**< to top */ + double down; /**< to bottom */ + double left; /**< to left */ + double right; /**< to right */ +} RT_PAD; + +/** \cond */ +/* + iconv() has a funny cast on some older systems, on most recent ones + it is just char **. This tries to work around the issue. If you build this + on another funky system this code may need to be modified, or define ICONV_CAST + on the compile line(but it may be tricky). +*/ +#ifdef SOL8 +#define ICONV_CAST (const char **) +#endif //SOL8 +#if !defined(ICONV_CAST) +#define ICONV_CAST (char **) +#endif //ICONV_CAST +/** \endcond */ + +/* Prototypes */ +int TR_findcasesub(const char *string, const char *sub); +char *TR_construct_fontspec(const TCHUNK_SPECS *tsp, const char *fontname); +char *TR_reconstruct_fontspec(const char *fontspec, const char *fontname); +int TR_find_alternate_font(FT_INFO *fti, FNT_SPECS **efsp, uint32_t wc); +int TR_getadvance(FT_INFO *fti, FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int load_flags, int kern_mode, int *ymin, int *ymax); +int TR_getkern2(FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int kern_mode); +int TR_kern_gap(FNT_SPECS *fsp, TCHUNK_SPECS *tsp, TCHUNK_SPECS *ptsp, int kern_mode); +void TR_rt_pad_set(RT_PAD *rt_pad, double up, double down, double left, double right); +double TR_baseline(TR_INFO *tri, int src, double *AscMax, double *DscMax); +int TR_check_set_vadvance(TR_INFO *tri, int src, int lines); +int TR_layout_analyze(TR_INFO *tri); +void TR_layout_2_svg(TR_INFO *tri); +int TR_weight_FC_to_SVG(int weight); + +FT_INFO *ftinfo_init(void); +int ftinfo_make_insertable(FT_INFO *fti); +int ftinfo_insert(FT_INFO *fti, FNT_SPECS *fsp); +FT_INFO *ftinfo_release(FT_INFO *fti); +FT_INFO *ftinfo_clear(FT_INFO *fti); +int ftinfo_find_loaded_by_spec(const FT_INFO *fti, const uint8_t *fname); +int ftinfo_find_loaded_by_src(const FT_INFO *fti, const uint8_t *filename); +int ftinfo_load_fontname(FT_INFO *fti, const char *fontspec); +void ftinfo_dump(const FT_INFO *fti); + +int fsp_alts_make_insertable(FNT_SPECS *fsp); +int fsp_alts_insert(FNT_SPECS *fsp, uint32_t fi_idx); +int fsp_alts_weight(FNT_SPECS *fsp, uint32_t a_idx); + +int csp_make_insertable(CHILD_SPECS *csp); +int csp_insert(CHILD_SPECS *csp, int src); +int csp_merge(CHILD_SPECS *dst, CHILD_SPECS *src); +void csp_release(CHILD_SPECS *csp); +void csp_clear(CHILD_SPECS *csp); + +CX_INFO *cxinfo_init(void); +int cxinfo_make_insertable(CX_INFO *cxi); +int cxinfo_insert(CX_INFO *cxi, int src, int src_rt_idx, enum tr_classes type); +int cxinfo_append(CX_INFO *cxi, int src, enum tr_classes type); +int cxinfo_merge(CX_INFO *cxi, int dst, int src, enum tr_classes type); +int cxinfo_trim(CX_INFO *cxi); +CX_INFO *cxinfo_release(CX_INFO *cxi); +void cxinfo_dump(const TR_INFO *tri); + +TP_INFO *tpinfo_init(void); +int tpinfo_make_insertable(TP_INFO *tpi); +int tpinfo_insert(TP_INFO *tpi, const TCHUNK_SPECS *tsp); +TP_INFO *tpinfo_release(TP_INFO *tpi); + +BR_INFO *brinfo_init(void); +int brinfo_make_insertable(BR_INFO *bri); +int brinfo_insert(BR_INFO *bri, const BRECT_SPECS *element); +int brinfo_merge(BR_INFO *bri, int dst, int src); +enum tr_classes + brinfo_pp_alignment(const BR_INFO *bri, int dst, int src, double slop, enum tr_classes type); +int brinfo_overlap(const BR_INFO *bri, int dst, int src, RT_PAD *rp_dst, RT_PAD *rp_src); +BR_INFO *brinfo_release(BR_INFO *bri); + +TR_INFO *trinfo_init(TR_INFO *tri); +TR_INFO *trinfo_release(TR_INFO *tri); +TR_INFO *trinfo_release_except_FC(TR_INFO *tri); +TR_INFO *trinfo_clear(TR_INFO *tri); +int trinfo_load_qe(TR_INFO *tri, double qe); +int trinfo_load_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor); +int trinfo_load_ft_opts(TR_INFO *tri, int use_kern, int load_flags, int kern_mode); +int trinfo_load_textrec(TR_INFO *tri, const TCHUNK_SPECS *tsp, double escapement, int flags); +int trinfo_check_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor); +int trinfo_append_out(TR_INFO *tri, const char *src); + +int is_mn_unicode(int test); + + +#ifdef __cplusplus +} +#endif +#endif /* _TEXT_REASSEMBLE_ */ diff --git a/src/extension/internal/vsd-input.cpp b/src/extension/internal/vsd-input.cpp new file mode 100644 index 0000000..c91f799 --- /dev/null +++ b/src/extension/internal/vsd-input.cpp @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include + +#include "vsd-input.h" + +#ifdef WITH_LIBVISIO + +#include +#include + +#include + +// TODO: Drop this check when librevenge is widespread. +#if WITH_LIBVISIO01 + #include + + using librevenge::RVNGString; + using librevenge::RVNGFileStream; + using librevenge::RVNGStringVector; +#else + #include + + typedef WPXString RVNGString; + typedef WPXFileStream RVNGFileStream; + typedef libvisio::VSDStringVector RVNGStringVector; +#endif + +#include + +#include "extension/system.h" +#include "extension/input.h" + +#include "document.h" +#include "inkscape.h" + +#include "ui/dialog-events.h" +#include + +#include "ui/view/svg-view-widget.h" + +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +class VsdImportDialog : public Gtk::Dialog { +public: + VsdImportDialog(const std::vector &vec); + ~VsdImportDialog() override; + + bool showDialog(); + unsigned getSelectedPage(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(); + + // Signal handlers + void _onPageNumberChanged(); + void _onSpinButtonPress(GdkEventButton* button_event); + void _onSpinButtonRelease(GdkEventButton* button_event); + + class Gtk::VBox * vbox1; + class Inkscape::UI::View::SVGViewWidget * _previewArea; + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + + class Gtk::HBox * _page_selector_box; + class Gtk::Label * _labelSelect; + class Gtk::Label * _labelTotalPages; + class Gtk::SpinButton * _pageNumberSpin; + + const std::vector &_vec; // Document to be imported + unsigned _current_page; // Current selected page + bool _spinning; // whether SpinButton is pressed (i.e. we're "spinning") +}; + +VsdImportDialog::VsdImportDialog(const std::vector &vec) + : _previewArea(nullptr) + , _vec(vec) + , _current_page(1) + , _spinning(false) +{ + int num_pages = _vec.size(); + if ( num_pages <= 1 ) + return; + + + // Dialog settings + this->set_title(_("Page Selector")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + + // Preview area + vbox1 = Gtk::manage(new class Gtk::VBox()); + this->get_content_area()->pack_start(*vbox1); + + // CONTROLS + _page_selector_box = Gtk::manage(new Gtk::HBox()); + + // Labels + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _page_selector_box->pack_start(*_labelSelect, Gtk::PACK_SHRINK); + + // Adjustment + spinner + auto _pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _vec.size(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Gtk::SpinButton(_pageNumberSpin_adj, 1, 0)); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_wrap(false); + _page_selector_box->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK); + + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + _page_selector_box->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK); + + vbox1->pack_end(*_page_selector_box, Gtk::PACK_SHRINK); + + // Buttons + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + this->add_action_widget(*cancelbutton, Gtk::RESPONSE_CANCEL); + this->add_action_widget(*okbutton, Gtk::RESPONSE_OK); + + // Show all widgets in dialog + this->show_all(); + + // Connect signals + _pageNumberSpin->signal_value_changed().connect(sigc::mem_fun(*this, &VsdImportDialog::_onPageNumberChanged)); + _pageNumberSpin->signal_button_press_event().connect_notify(sigc::mem_fun(*this, &VsdImportDialog::_onSpinButtonPress)); + _pageNumberSpin->signal_button_release_event().connect_notify(sigc::mem_fun(*this, &VsdImportDialog::_onSpinButtonRelease)); + + _setPreviewPage(); +} + +VsdImportDialog::~VsdImportDialog() = default; + +bool VsdImportDialog::showDialog() +{ + show(); + gint b = run(); + hide(); + if (b == Gtk::RESPONSE_OK || b == Gtk::RESPONSE_ACCEPT) { + return TRUE; + } else { + return FALSE; + } +} + +unsigned VsdImportDialog::getSelectedPage() +{ + return _current_page; +} + +void VsdImportDialog::_onPageNumberChanged() +{ + unsigned page = static_cast(_pageNumberSpin->get_value_as_int()); + _current_page = CLAMP(page, 1U, _vec.size()); + _setPreviewPage(); +} + +void VsdImportDialog::_onSpinButtonPress(GdkEventButton* /*button_event*/) +{ + _spinning = true; +} + +void VsdImportDialog::_onSpinButtonRelease(GdkEventButton* /*button_event*/) +{ + _spinning = false; + _setPreviewPage(); +} + +/** + * \brief Renders the given page's thumbnail + */ +void VsdImportDialog::_setPreviewPage() +{ + if (_spinning) { + return; + } + + SPDocument *doc = SPDocument::createNewDocFromMem(_vec[_current_page-1].cstr(), strlen(_vec[_current_page-1].cstr()), false); + if(!doc) { + g_warning("VSD import: Could not create preview for page %d", _current_page); + gchar const *no_preview_template = R"A( + + + + %s + + )A"; + gchar * no_preview = g_strdup_printf(no_preview_template, _("No preview")); + doc = SPDocument::createNewDocFromMem(no_preview, strlen(no_preview), false); + g_free(no_preview); + } + + if (!doc) { + std::cerr << "VsdImportDialog::_setPreviewPage: No document!" << std::endl; + return; + } + + if (_previewArea) { + _previewArea->setDocument(doc); + } else { + _previewArea = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + vbox1->pack_start(*_previewArea, Gtk::PACK_EXPAND_WIDGET, 0); + } + + _previewArea->setResize(400, 400); + _previewArea->show_all(); +} + +SPDocument *VsdInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGFileStream input(converted_uri); + g_free(converted_uri); + #else + RVNGFileStream input(uri); + #endif + + if (!libvisio::VisioDocument::isSupported(&input)) { + return nullptr; + } + + RVNGStringVector output; +#if WITH_LIBVISIO01 + librevenge::RVNGSVGDrawingGenerator generator(output, "svg"); + + if (!libvisio::VisioDocument::parse(&input, &generator)) { +#else + if (!libvisio::VisioDocument::generateSVG(&input, output)) { +#endif + return nullptr; + } + + if (output.empty()) { + return nullptr; + } + + std::vector tmpSVGOutput; + for (unsigned i=0; i\n\n"); + tmpString.append(output[i]); + tmpSVGOutput.push_back(tmpString); + } + + unsigned page_num = 1; + + // If only one page is present, import that one without bothering user + if (tmpSVGOutput.size() > 1) { + VsdImportDialog *dlg = nullptr; + if (INKSCAPE.use_gui()) { + dlg = new VsdImportDialog(tmpSVGOutput); + if (!dlg->showDialog()) { + delete dlg; + throw Input::open_cancelled(); + } + } + + // Get needed page + if (dlg) { + page_num = dlg->getSelectedPage(); + if (page_num < 1) + page_num = 1; + if (page_num > tmpSVGOutput.size()) + page_num = tmpSVGOutput.size(); + } + } + + SPDocument * doc = SPDocument::createNewDocFromMem(tmpSVGOutput[page_num-1].cstr(), strlen(tmpSVGOutput[page_num-1].cstr()), TRUE); + + // Set viewBox if it doesn't exist + if (doc && !doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + return doc; +} + +#include "clear-n_.h" + +void VsdInput::init() +{ + /* VSD */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("VSD Input") "\n" + "org.inkscape.input.vsd\n" + "\n" + ".vsd\n" + "application/vnd.visio\n" + "" N_("Microsoft Visio Diagram (*.vsd)") "\n" + "" N_("File format used by Microsoft Visio 6 and later") "\n" + "\n" + "", new VsdInput()); + + /* VDX */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("VDX Input") "\n" + "org.inkscape.input.vdx\n" + "\n" + ".vdx\n" + "application/vnd.visio\n" + "" N_("Microsoft Visio XML Diagram (*.vdx)") "\n" + "" N_("File format used by Microsoft Visio 2010 and later") "\n" + "\n" + "", new VsdInput()); + + /* VSDM */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("VSDM Input") "\n" + "org.inkscape.input.vsdm\n" + "\n" + ".vsdm\n" + "application/vnd.visio\n" + "" N_("Microsoft Visio 2013 drawing (*.vsdm)") "\n" + "" N_("File format used by Microsoft Visio 2013 and later") "\n" + "\n" + "", new VsdInput()); + + /* VSDX */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("VSDX Input") "\n" + "org.inkscape.input.vsdx\n" + "\n" + ".vsdx\n" + "application/vnd.visio\n" + "" N_("Microsoft Visio 2013 drawing (*.vsdx)") "\n" + "" N_("File format used by Microsoft Visio 2013 and later") "\n" + "\n" + "", new VsdInput()); + + return; + +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBVISIO */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/vsd-input.h b/src/extension/internal/vsd-input.h new file mode 100644 index 0000000..f30c905 --- /dev/null +++ b/src/extension/internal/vsd-input.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_VSDOUTPUT_H__ +#define __EXTENSION_INTERNAL_VSDOUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBVISIO + +#include + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class VsdInput : public Inkscape::Extension::Implementation::Implementation { + VsdInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBVISIO */ +#endif /* __EXTENSION_INTERNAL_VSDOUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/wmf-inout.cpp b/src/extension/internal/wmf-inout.cpp new file mode 100644 index 0000000..e5e2279 --- /dev/null +++ b/src/extension/internal/wmf-inout.cpp @@ -0,0 +1,3263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows-only Enhanced Metafile input and output. + */ +/* Authors: + * Ulf Erikson + * Jon A. Cruz + * David Mathog + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +//#include //This must precede text_reassemble.h or it blows up in pngconf.h when compiling +#include +#include +#include +#include <3rdparty/libuemf/symbol_convert.h> + +#include "document.h" +#include "object/sp-root.h" // even though it is included indirectly by wmf-inout.h +#include "object/sp-path.h" +#include "print.h" +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "display/drawing.h" +#include "display/drawing-item.h" +#include "clear-n_.h" +#include "svg/svg.h" +#include "util/units.h" // even though it is included indirectly by wmf-inout.h +#include "inkscape.h" // even though it is included indirectly by wmf-inout.h + + +#include "wmf-inout.h" +#include "wmf-print.h" + +#define PRINT_WMF "org.inkscape.print.wmf" + +#ifndef U_PS_JOIN_MASK +#define U_PS_JOIN_MASK (U_PS_JOIN_BEVEL|U_PS_JOIN_MITER|U_PS_JOIN_ROUND) +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +static bool clipset = false; +static uint32_t BLTmode=0; + +Wmf::Wmf () // The null constructor +{ + return; +} + + +Wmf::~Wmf () //The destructor +{ + return; +} + + +bool +Wmf::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(PRINT_WMF)) + return FALSE; + return TRUE; +} + + +void +Wmf::print_document_to_file(SPDocument *doc, const gchar *filename) +{ + Inkscape::Extension::Print *mod; + SPPrintContext context; + const gchar *oldconst; + gchar *oldoutput; + + doc->ensureUpToDate(); + + mod = Inkscape::Extension::get_print(PRINT_WMF); + oldconst = mod->get_param_string("destination"); + oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + +/* Start */ + context.module = mod; + /* fixme: This has to go into module constructor somehow */ + /* Create new arena */ + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = mod->base->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + /* Print document */ + if (mod->begin(doc)) { + g_free(oldoutput); + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; + throw Inkscape::Extension::Output::save_failed(); + } + mod->base->invoke_print(&context); + mod->finish(); + /* Release arena */ + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // deleted by invoke_hide +/* end */ + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); + + return; +} + + +void +Wmf::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + + ext = Inkscape::Extension::db.get(PRINT_WMF); + if (ext == nullptr) + return; + + bool new_val = mod->get_param_bool("textToPath"); + bool new_FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); // character position bug + // reserve FixPPT2 for opacity bug. Currently WMF does not export opacity values + bool new_FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); // dashed line bug + bool new_FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); // gradient bug + bool new_FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); // force all patterns as standard WMF hatch + + TableGen( //possibly regenerate the unicode-convert tables + mod->get_param_bool("TnrToSymbol"), + mod->get_param_bool("TnrToWingdings"), + mod->get_param_bool("TnrToZapfDingbats"), + mod->get_param_bool("UsePUA") + ); + + ext->set_param_bool("FixPPTCharPos",new_FixPPTCharPos); // Remember to add any new ones to PrintWmf::init or a mysterious failure will result! + ext->set_param_bool("FixPPTDashLine",new_FixPPTDashLine); + ext->set_param_bool("FixPPTGrad2Polys",new_FixPPTGrad2Polys); + ext->set_param_bool("FixPPTPatternAsHatch",new_FixPPTPatternAsHatch); + ext->set_param_bool("textToPath", new_val); + + // ensure usage of dot as decimal separator in scanf/printf functions (independently of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + print_document_to_file(doc, filename); + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return; +} + + +/* WMF has no worldTransform, so this always returns 1.0. Retain it to keep WMF and WMF in sync as much as possible.*/ +double Wmf::current_scale(PWMF_CALLBACK_DATA /*d*/){ + return 1.0; +} + +/* WMF has no worldTransform, so this always returns an Identity rotation matrix, but the offsets may have values.*/ +std::string Wmf::current_matrix(PWMF_CALLBACK_DATA d, double x, double y, int useoffset){ + SVGOStringStream cxform; + double scale = current_scale(d); + cxform << "\"matrix("; + cxform << 1.0/scale; cxform << ","; + cxform << 0.0; cxform << ","; + cxform << 0.0; cxform << ","; + cxform << 1.0/scale; cxform << ","; + if(useoffset){ cxform << x; cxform << ","; cxform << y; } + else { cxform << "0,0"; } + cxform << ")\""; + return(cxform.str()); +} + +/* WMF has no worldTransform, so this always returns 0. Retain it to keep WMF and WMF in sync as much as possible.*/ +double Wmf::current_rotation(PWMF_CALLBACK_DATA /*d*/){ + return 0.0; +} + +/* Add another 100 blank slots to the hatches array. +*/ +void Wmf::enlarge_hatches(PWMF_CALLBACK_DATA d){ + d->hatches.size += 100; + d->hatches.strings = (char **) realloc(d->hatches.strings,d->hatches.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_hatches(PWMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; ihatches.count; i++){ + if(strcmp(test,d->hatches.strings[i])==0)return(i+1); + } + return(0); +} + +class TagEmitter +{ +public: + TagEmitter(Glib::ustring & p_defs, char * p_tmpcolor, char * p_hpathname): + defs(p_defs), tmpcolor(p_tmpcolor), hpathname(p_hpathname) + { + }; + void append(const char *prefix, const char * inner) + { + defs += " "; + defs += prefix; + defs += hpathname; + defs += inner; + defs += tmpcolor; + defs += "\" />\n"; + } + +protected: + Glib::ustring & defs; + char * tmpcolor, * hpathname; +}; + +/* (Conditionally) add a hatch. If a matching hatch already exists nothing happens. If one + does not exist it is added to the hatches list and also entered into . + This is also used to add the path part of the hatches, which they reference with a xlink:href +*/ +uint32_t Wmf::add_hatch(PWMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor){ + char hatchname[64]; // big enough + char hpathname[64]; // big enough + char hbkname[64]; // big enough + char tmpcolor[8]; + char bkcolor[8]; + uint32_t idx; + + switch(hatchType){ + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].textColor)); + break; + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + break; + default: + sprintf(tmpcolor,"%6.6X",sethexcolor(hatchColor)); + break; + } + auto & defs = d->defs; + TagEmitter a(defs, tmpcolor, hpathname); + + /* For both bkMode types set the PATH + FOREGROUND COLOR for the indicated standard hatch. + This will be used late to compose, or recompose the transparent or opaque final hatch.*/ + + std::string refpath; // used to reference later the path pieces which are about to be created + sprintf(hpathname,"WMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hpathname); + if(!idx){ // add path/color if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hpathname); + + defs += "\n"; + switch(hatchType){ + case U_HS_HORIZONTAL: + a.append("\n"; + break; + case U_HS_FDIAGONAL: + case U_HS_BDIAGONAL: + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + break; + case U_HS_DIAGCROSS: + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + refpath += " \n"; + break; + } + + if(d->dc[d->level].bkMode == U_TRANSPARENT || hatchType >= U_HS_SOLIDCLR){ + sprintf(hatchname,"WMFhatch%d_%s",hatchType,tmpcolor); + sprintf(hpathname,"WMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " \n"; + defs += refpath; + defs += " \n"; + idx = d->hatches.count; + } + } + else { // bkMode==U_OPAQUE + /* Set up an object in the defs for this background, if there is not one already there */ + sprintf(bkcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + sprintf(hbkname,"WMFhbkclr_%s",bkcolor); + idx = in_hatches(d,hbkname); + if(!idx){ // add path/color if not already present. Hatchtype is not needed in the name. + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hbkname); + + defs += "\n"; + defs += " \n"; + } + + // this is the pattern, its name will show up in Inkscape's pattern selector + sprintf(hatchname,"WMFhatch%d_%s_%s",hatchType,tmpcolor,bkcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " \n"; + defs += " \n"; + defs += refpath; + defs += " \n"; + idx = d->hatches.count; + } + } + return(idx-1); +} + +/* Add another 100 blank slots to the images array. +*/ +void Wmf::enlarge_images(PWMF_CALLBACK_DATA d){ + d->images.size += 100; + d->images.strings = (char **) realloc(d->images.strings,d->images.size * sizeof(char *)); +} + +/* See if the image string is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_images(PWMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; iimages.count; i++){ + if(strcmp(test,d->images.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add an image from a DIB. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into . + +*/ +uint32_t Wmf::add_dib_image(PWMF_CALLBACK_DATA d, const char *dib, uint32_t iUsage){ + + uint32_t idx; + char imagename[64]; // big enough + char xywh[64]; // big enough + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + uint32_t numCt; + int32_t width, height, colortype, invert; // if needed these values will be set by wget_DIB_params + if(iUsage == U_DIB_RGB_COLORS){ + // next call returns pointers and values, but allocates no memory + dibparams = wget_DIB_params(dib, &px, &ct, &numCt, &width, &height, &colortype, &invert); + if(dibparams == U_BI_RGB){ + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array in record + height, // Height of pixel array in record + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG || dibparams==U_BI_PNG){ // image was binary png or jpg in source file + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + + sprintf(imagename,"WMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " \n"; + defs += " "; + defs += " \n"; + } + g_free(base64String); //wait until this point to free because it might be a duplicate image + return(idx-1); +} + +/* (Conditionally) add an image from a Bitmap16. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into . + +*/ +uint32_t Wmf::add_bm16_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px){ + + uint32_t idx; + char imagename[64]; // big enough + char xywh[64]; // big enough + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const U_RGBQUAD *ct = nullptr; // color table, always NULL here + int32_t width, height, colortype, numCt, invert; + numCt = 0; + width = Bm16.Width; // bitmap width in pixels. + height = Bm16.Height; // bitmap height in scan lines. + colortype = Bm16.BitsPixel; // seems to be BitCount Enumeration + invert = 0; + if(colortype < 16)return(U_WMR_INVALID); // these would need a colortable if they were a dib, no idea what bm16 is supposed to do instead. + + if(!DIB_to_RGBA(// This is not really a dib, but close enough so that it still works. + px, // DIB pixel array + ct, // DIB color table (always NULL here) + numCt, // DIB color table number of entries (always 0) + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + + gchar *base64String=nullptr; + if(mempng.buffer){ // image was Bm16 in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=g_strdup(base64String); + + sprintf(imagename,"WMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " \n"; + defs += " \n"; + } + g_free(base64String); //wait until this point to free because it might be a duplicate image + return(idx-1); +} + +/* Add another 100 blank slots to the clips array. +*/ +void Wmf::enlarge_clips(PWMF_CALLBACK_DATA d){ + d->clips.size += 100; + d->clips.strings = (char **) realloc(d->clips.strings,d->clips.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_clips(PWMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; iclips.count; i++){ + if(strcmp(test,d->clips.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a clip. + If a matching clip already exists nothing happens + If one does exist it is added to the clips list, entered into . +*/ +void Wmf::add_clips(PWMF_CALLBACK_DATA d, const char *clippath, unsigned int logic){ + int op = combine_ops_to_livarot(logic); + Geom::PathVector combined_vect; + char *combined = nullptr; + if (op >= 0 && d->dc[d->level].clip_id) { + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector old_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + Geom::PathVector new_vect = sp_svg_read_pathv(clippath); + combined_vect = sp_pathvector_boolop(new_vect, old_vect, (bool_op) op , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + combined = sp_svg_write_path(combined_vect); + } + else { + combined = strdup(clippath); // COPY operation, erases everything and starts a new one + } + + uint32_t idx = in_clips(d, combined); + if(!idx){ // add clip if not already present + if(d->clips.count == d->clips.size){ enlarge_clips(d); } + d->clips.strings[d->clips.count++]=strdup(combined); + d->dc[d->level].clip_id = d->clips.count; // one more than the slot where it is actually stored + SVGOStringStream tmp_clippath; + tmp_clippath << "\ndc[d->level].clip_id << "\""; + tmp_clippath << " >"; + tmp_clippath << "\n\t"; + tmp_clippath << "\n"; + d->outdef += tmp_clippath.str().c_str(); + } + else { + d->dc[d->level].clip_id = idx; + } + free(combined); +} + + + +void +Wmf::output_style(PWMF_CALLBACK_DATA d) +{ +// SVGOStringStream tmp_id; + SVGOStringStream tmp_style; + char tmp[1024] = {0}; + + float fill_rgb[3]; + d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); + float stroke_rgb[3]; + d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + + // for U_WMR_BITBLT with no image, try to approximate some of these operations/ + // Assume src color is "white" + if(d->dwRop3){ + switch(d->dwRop3){ + case U_PATINVERT: // treat all of these as black + case U_SRCINVERT: + case U_DSTINVERT: + case U_BLACKNESS: + case U_SRCERASE: + case U_NOTSRCCOPY: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + break; + case U_SRCCOPY: // treat all of these as white + case U_NOTSRCERASE: + case U_PATCOPY: + case U_WHITENESS: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + break; + case U_SRCPAINT: // use the existing color + case U_SRCAND: + case U_MERGECOPY: + case U_MERGEPAINT: + case U_PATPAINT: + default: + break; + } + d->dwRop3 = 0; // might as well reset it here, it must be set for each BITBLT + } + + // Implement some of these, the ones where the original screen color does not matter. + // The options that merge screen and pen colors cannot be done correctly because we + // have no way of knowing what color is already on the screen. For those just pass the + // pen color through. + switch(d->dwRop2){ + case U_R2_BLACK: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + break; + case U_R2_NOTMERGEPEN: + case U_R2_MASKNOTPEN: + break; + case U_R2_NOTCOPYPEN: + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + stroke_rgb[0] = 1.0 - stroke_rgb[0]; + stroke_rgb[1] = 1.0 - stroke_rgb[1]; + stroke_rgb[2] = 1.0 - stroke_rgb[2]; + break; + case U_R2_MASKPENNOT: + case U_R2_NOT: + case U_R2_XORPEN: + case U_R2_NOTMASKPEN: + case U_R2_NOTXORPEN: + case U_R2_NOP: + case U_R2_MERGENOTPEN: + case U_R2_COPYPEN: + case U_R2_MASKPEN: + case U_R2_MERGEPENNOT: + case U_R2_MERGEPEN: + break; + case U_R2_WHITE: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + break; + default: + break; + } + + +// tmp_id << "\n\tid=\"" << (d->id++) << "\""; +// d->outsvg += tmp_id.str().c_str(); + d->outsvg += "\n\tstyle=\""; + if (!d->dc[d->level].fill_set || ( d->mask & U_DRAW_NOFILL)) { // nofill are lines and arcs + tmp_style << "fill:none;"; + } else { + switch(d->dc[d->level].fill_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "fill:url(#%s); ",d->hatches.strings[d->dc[d->level].fill_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "fill:url(#WMFimage%d_ref); ",d->dc[d->level].fill_idx); + tmp_style << tmp; + break; + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "fill:#%02x%02x%02x;", + SP_COLOR_F_TO_U(fill_rgb[0]), + SP_COLOR_F_TO_U(fill_rgb[1]), + SP_COLOR_F_TO_U(fill_rgb[2]) + ); + tmp_style << tmp; + break; + } + snprintf( + tmp, 1023, + "fill-rule:%s;", + (d->dc[d->level].style.fill_rule.value == SP_WIND_RULE_NONZERO ? "evenodd" : "nonzero") + ); + tmp_style << tmp; + tmp_style << "fill-opacity:1;"; + + // if the stroke is the same as the fill, and the right size not to change the end size of the object, do not do it separately + if( + (d->dc[d->level].fill_set ) && + (d->dc[d->level].stroke_set ) && + (d->dc[d->level].style.stroke_width.value == 1 ) && + (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && + ( + (d->dc[d->level].fill_mode != DRAW_PAINT) || + ( + (fill_rgb[0]==stroke_rgb[0]) && + (fill_rgb[1]==stroke_rgb[1]) && + (fill_rgb[2]==stroke_rgb[2]) + ) + ) + ){ + d->dc[d->level].stroke_set = false; + } + } + + if (!d->dc[d->level].stroke_set) { + tmp_style << "stroke:none;"; + } else { + switch(d->dc[d->level].stroke_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "stroke:url(#%s); ",d->hatches.strings[d->dc[d->level].stroke_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "stroke:url(#WMFimage%d_ref); ",d->dc[d->level].stroke_idx); + tmp_style << tmp; + break; + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "stroke:#%02x%02x%02x;", + SP_COLOR_F_TO_U(stroke_rgb[0]), + SP_COLOR_F_TO_U(stroke_rgb[1]), + SP_COLOR_F_TO_U(stroke_rgb[2]) + ); + tmp_style << tmp; + break; + } + if(d->dc[d->level].style.stroke_width.value){ + tmp_style << "stroke-width:" << + MAX( 0.001, d->dc[d->level].style.stroke_width.value ) << "px;"; + } + else { // In a WMF a 0 width pixel means "1 pixel" + tmp_style << "stroke-width:" << pix_to_abs_size( d, 1 ) << "px;"; + } + + tmp_style << "stroke-linecap:" << + ( + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_BUTT ? "butt" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_ROUND ? "round" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_SQUARE ? "square" : + "unknown" + ) << ";"; + + tmp_style << "stroke-linejoin:" << + ( + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_MITER ? "miter" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_ROUND ? "round" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_BEVEL ? "bevel" : + "unknown" + ) << ";"; + + // Set miter limit if known, even if it is not needed immediately (not miter) + tmp_style << "stroke-miterlimit:" << + MAX( 2.0, d->dc[d->level].style.stroke_miterlimit.value ) << ";"; + + if (d->dc[d->level].style.stroke_dasharray.set && + !d->dc[d->level].style.stroke_dasharray.values.empty()) + { + tmp_style << "stroke-dasharray:"; + for (unsigned i=0; idc[d->level].style.stroke_dasharray.values.size(); i++) { + if (i) + tmp_style << ","; + tmp_style << d->dc[d->level].style.stroke_dasharray.values[i].value; + } + tmp_style << ";"; + tmp_style << "stroke-dashoffset:0;"; + } else { + tmp_style << "stroke-dasharray:none;"; + } + tmp_style << "stroke-opacity:1;"; + } + tmp_style << "\" "; + if (d->dc[d->level].clip_id) + tmp_style << "\n\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\" "; + + d->outsvg += tmp_style.str().c_str(); +} + + +double +Wmf::_pix_x_to_point(PWMF_CALLBACK_DATA d, double px) +{ + double scale = (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0); + double tmp; + tmp = ((((double) (px - d->dc[d->level].winorg.x))*scale) + d->dc[d->level].vieworg.x) * d->D2PscaleX; + tmp -= d->ulCornerOutX; //The WMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + +double +Wmf::_pix_y_to_point(PWMF_CALLBACK_DATA d, double py) +{ + double scale = (d->dc[d->level].ScaleInY ? d->dc[d->level].ScaleInY : 1.0); + double tmp; + tmp = ((((double) (py - d->dc[d->level].winorg.y))*scale) * d->E2IdirY + d->dc[d->level].vieworg.y) * d->D2PscaleY; + tmp -= d->ulCornerOutY; //The WMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + + +double +Wmf::pix_to_x_point(PWMF_CALLBACK_DATA d, double px, double /*py*/) +{ + double x = _pix_x_to_point(d, px); + return x; +} + +double +Wmf::pix_to_y_point(PWMF_CALLBACK_DATA d, double /*px*/, double py) +{ + double y = _pix_y_to_point(d, py); + return y; + +} + +double +Wmf::pix_to_abs_size(PWMF_CALLBACK_DATA d, double px) +{ + double ppx = fabs(px * (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0) * d->D2PscaleX * current_scale(d)); + return ppx; +} + +/* returns "x,y" (without the quotes) in inkscape coordinates for a pair of WMF x,y coordinates +*/ +std::string Wmf::pix_to_xy(PWMF_CALLBACK_DATA d, double x, double y){ + SVGOStringStream cxform; + cxform << pix_to_x_point(d,x,y); + cxform << ","; + cxform << pix_to_y_point(d,x,y); + return(cxform.str()); +} + + +void +Wmf::select_pen(PWMF_CALLBACK_DATA d, int index) +{ + int width; + char *record = nullptr; + U_PEN up; + + if (index < 0 && index >= d->n_obj){ return; } + record = d->wmf_obj[index].record; + if(!record){ return; } + d->dc[d->level].active_pen = index; + + (void) U_WMRCREATEPENINDIRECT_get(record, &up); + width = up.Widthw[0]; // width is stored in the first 16 bits of the 32. + + switch (up.Style & U_PS_STYLE_MASK) { + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + int penstyle = (up.Style & U_PS_STYLE_MASK); + SPILength spilength(1.f); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + + case U_PS_SOLID: + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (up.Style & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + + switch (up.Style & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + + + double pen_width; + if (up.Style == U_PS_NULL) { + d->dc[d->level].stroke_set = false; + pen_width =0.0; + } else if (width) { + d->dc[d->level].stroke_set = true; + int cur_level = d->level; + d->level = d->wmf_obj[index].level; // this object may have been defined in some other DC. + pen_width = pix_to_abs_size( d, width ); + d->level = cur_level; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + d->dc[d->level].stroke_set = true; + int cur_level = d->level; + d->level = d->wmf_obj[index].level; // this object may have been defined in some other DC. + pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + } + d->dc[d->level].style.stroke_width.value = pen_width; + + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(up.Color) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(up.Color) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(up.Color) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); +} + + +void +Wmf::select_brush(PWMF_CALLBACK_DATA d, int index) +{ + uint8_t iType; + char *record; + const char *membrush; + + if (index < 0 || index >= d->n_obj)return; + record = d->wmf_obj[index].record; + if(!record)return; + d->dc[d->level].active_brush = index; + + iType = *(uint8_t *)(record + offsetof(U_METARECORD, iType ) ); + if(iType == U_WMR_CREATEBRUSHINDIRECT){ + U_WLOGBRUSH lb; + (void) U_WMRCREATEBRUSHINDIRECT_get(record, &membrush); + memcpy(&lb, membrush, U_SIZE_WLOGBRUSH); + if(lb.Style == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(lb.Color) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(lb.Color) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(lb.Color) ); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + } + else if(lb.Style == U_BS_HATCHED){ + d->dc[d->level].fill_idx = add_hatch(d, lb.Hatch, lb.Color); + d->dc[d->level].fill_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].fill_mode = DRAW_PATTERN; + d->dc[d->level].fill_set = true; + } + else if(lb.Style == U_BS_NULL){ + d->dc[d->level].fill_mode = DRAW_PAINT; // set it to something + d->dc[d->level].fill_set = false; + } + } + else if(iType == U_WMR_DIBCREATEPATTERNBRUSH){ + uint32_t tidx; + uint16_t Style; + uint16_t cUsage; + const char *Bm16h = nullptr; // Pointer to Bitmap16 header (px follows) + const char *dib = nullptr; // Pointer to DIB + (void) U_WMRDIBCREATEPATTERNBRUSH_get(record, &Style, &cUsage, &Bm16h, &dib); + if(dib || Bm16h){ + if(dib){ tidx = add_dib_image(d, dib, cUsage); } + else { + U_BITMAP16 Bm16; + const char *px; + memcpy(&Bm16, Bm16h, U_SIZE_BITMAP16); + px = Bm16h + U_SIZE_BITMAP16; + tidx = add_bm16_image(d, Bm16, px); + } + if(tidx == U_WMR_INVALID){ // Problem with the image, for instance, an unsupported bitmap16 type + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + else { + g_message("Please send WMF file to developers - select_brush U_WMR_DIBCREATEPATTERNBRUSH not bm16 or dib, not handled"); + } + } + else if(iType == U_WMR_CREATEPATTERNBRUSH){ + uint32_t tidx; + int cbPx; + U_BITMAP16 Bm16h; + const char *px; + if(U_WMRCREATEPATTERNBRUSH_get(record, &Bm16h, &cbPx, &px)){ + tidx = add_bm16_image(d, Bm16h, px); + if(tidx == 0xFFFFFFFF){ // Problem with the image, for instance, an unsupported bitmap16 type + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + } +} + + +void +Wmf::select_font(PWMF_CALLBACK_DATA d, int index) +{ + char *record = nullptr; + const char *memfont; + const char *facename; + U_FONT font; + + if (index < 0 || index >= d->n_obj)return; + record = d->wmf_obj[index].record; + if (!record)return; + d->dc[d->level].active_font = index; + + + (void) U_WMRCREATEFONTINDIRECT_get(record, &memfont); + memcpy(&font,memfont,U_SIZE_FONT_CORE); //make sure it is in a properly aligned structure before touching it + facename = memfont + U_SIZE_FONT_CORE; + + /* The logfont information always starts with a U_LOGFONT structure but the U_WMRCREATEFONTINDIRECT + is defined as U_LOGFONT_PANOSE so it can handle one of those if that is actually present. Currently only logfont + is supported, and the remainder, it it really is a U_LOGFONT_PANOSE record, is ignored + */ + int cur_level = d->level; + d->level = d->wmf_obj[index].level; + double font_size = pix_to_abs_size( d, font.Height ); + /* snap the font_size to the nearest 1/32nd of a point. + (The size is converted from Pixels to points, snapped, and converted back.) + See the notes where d->D2Pscale[XY] are set for the reason why. + Typically this will set the font to the desired exact size. If some peculiar size + was intended this will, at worst, make it .03125 off, which is unlikely to be a problem. */ + font_size = round(20.0 * 0.8 * font_size)/(20.0 * 0.8); + d->level = cur_level; + d->dc[d->level].style.font_size.computed = font_size; + d->dc[d->level].style.font_weight.value = + font.Weight == U_FW_THIN ? SP_CSS_FONT_WEIGHT_100 : + font.Weight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_200 : + font.Weight == U_FW_LIGHT ? SP_CSS_FONT_WEIGHT_300 : + font.Weight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_400 : + font.Weight == U_FW_MEDIUM ? SP_CSS_FONT_WEIGHT_500 : + font.Weight == U_FW_SEMIBOLD ? SP_CSS_FONT_WEIGHT_600 : + font.Weight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_700 : + font.Weight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_800 : + font.Weight == U_FW_HEAVY ? SP_CSS_FONT_WEIGHT_900 : + font.Weight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_NORMAL : + font.Weight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_BOLD : + font.Weight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_LIGHTER : + font.Weight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_BOLDER : + SP_CSS_FONT_WEIGHT_NORMAL; + d->dc[d->level].style.font_style.value = (font.Italic ? SP_CSS_FONT_STYLE_ITALIC : SP_CSS_FONT_STYLE_NORMAL); + d->dc[d->level].style.text_decoration_line.underline = font.Underline; + d->dc[d->level].style.text_decoration_line.line_through = font.StrikeOut; + d->dc[d->level].style.text_decoration_line.set = true; + d->dc[d->level].style.text_decoration_line.inherit = false; + + // malformed WMF with empty filename may exist, ignore font change if encountered + if(d->dc[d->level].font_name)free(d->dc[d->level].font_name); + if(*facename){ + d->dc[d->level].font_name = strdup(facename); + } + else { // Malformed WMF might specify an empty font name + d->dc[d->level].font_name = strdup("Arial"); // Default font, WMF spec says device can pick whatever it wants + } + d->dc[d->level].style.baseline_shift.value = round((double)((font.Escapement + 3600) % 3600) / 10.0); // use baseline_shift instead of text_transform to avoid overflow +} + +/* Find the first free hole where an object may be stored. + If there are not any return -1. This is a big error, possibly from a corrupt WMF file. +*/ +int Wmf::insertable_object(PWMF_CALLBACK_DATA d) +{ + int index = d->low_water; // Start looking from here, it may already have been filled + while(index < d->n_obj && d->wmf_obj[index].record != nullptr){ index++; } + if(index >= d->n_obj)return(-1); // this is a big problem, percolate it back up so the program can get out of this gracefully + d->low_water = index; // Could probably be index+1 + return(index); +} + +void +Wmf::delete_object(PWMF_CALLBACK_DATA d, int index) +{ + if (index >= 0 && index < d->n_obj) { + // If the active object is deleted set default draw values + if(index == d->dc[d->level].active_pen){ // Use default pen: solid, black, 1 pixel wide + d->dc[d->level].active_pen = -1; + d->dc[d->level].style.stroke_dasharray.set = false; + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; // U_PS_ENDCAP_SQUARE + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; + d->dc[d->level].stroke_set = true; + d->dc[d->level].style.stroke_width.value = 1.0; + d->dc[d->level].style.stroke.value.color.set( 0, 0, 0 ); + } + else if(index == d->dc[d->level].active_brush){ + d->dc[d->level].active_brush = -1; + d->dc[d->level].fill_set = false; + } + else if(index == d->dc[d->level].active_font){ + d->dc[d->level].active_font = -1; + if(d->dc[d->level].font_name){ free(d->dc[d->level].font_name);} + d->dc[d->level].font_name = strdup("Arial"); // Default font, WMF spec says device can pick whatever it wants + d->dc[d->level].style.font_size.computed = 16.0; + d->dc[d->level].style.font_weight.value = SP_CSS_FONT_WEIGHT_400; + d->dc[d->level].style.font_style.value = SP_CSS_FONT_STYLE_NORMAL; + d->dc[d->level].style.text_decoration_line.underline = false; + d->dc[d->level].style.text_decoration_line.line_through = false; + d->dc[d->level].style.baseline_shift.value = 0; + } + + + d->wmf_obj[index].type = 0; +// We are keeping a copy of the WMR rather than just a structure. Currently that is not necessary as the entire +// WMF is read in at once and is stored in a big malloc. However, in past versions it was handled +// record by record, and we might need to do that again at some point in the future if we start running into WMF +// files too big to fit into memory. + if (d->wmf_obj[index].record) + free(d->wmf_obj[index].record); + d->wmf_obj[index].record = nullptr; + if(index < d->low_water)d->low_water = index; + } +} + + +// returns the new index, or -1 on error. +int Wmf::insert_object(PWMF_CALLBACK_DATA d, int type, const char *record) +{ + int index = insertable_object(d); + if(index>=0){ + d->wmf_obj[index].type = type; + d->wmf_obj[index].level = d->level; + d->wmf_obj[index].record = wmr_dup(record); + } + return(index); +} + + +/** + \fn create a UTF-32LE buffer and fill it with UNICODE unknown character + \param count number of copies of the Unicode unknown character to fill with +*/ +uint32_t *Wmf::unknown_chars(size_t count){ + uint32_t *res = (uint32_t *) malloc(sizeof(uint32_t) * (count + 1)); + if(!res)throw "Inkscape fatal memory allocation error - cannot continue"; + for(uint32_t i=0; idc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // color table + uint32_t numCt; + int32_t width, height, colortype, invert; // if needed these values will be set in wget_DIB_params + if(iUsage == U_DIB_RGB_COLORS){ + // next call returns pointers and values, but allocates no memory + dibparams = wget_DIB_params(dib, &px, &ct, &numCt, &width, &height, &colortype, &invert); + if(dibparams == U_BI_RGB){ + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG){ // image was binary jpg in source file + tmp_image << " xlink:href=\"data:image/jpeg;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(dibparams==U_BI_PNG){ // image was binary png in source file + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + tmp_image << " transform=" << current_matrix(d, 0.0, 0.0, 0); // returns an identity matrix, no offsets. + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \brief store SVG for an image given the pixmap and various coordinate information + \param d + \param Bm16 core Bitmap16 header + \param px pointer to Bitmap16 image data + \param dx (double) destination x in inkscape pixels + \param dy (double) destination y in inkscape pixels + \param dw (double) destination width in inkscape pixels + \param dh (double) destination height in inkscape pixels + \param sx (int) source x in src image pixels + \param sy (int) source y in src image pixels + \param iUsage +*/ +void Wmf::common_bm16_to_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh){ + + SVGOStringStream tmp_image; + + tmp_image << "\n\t dc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const U_RGBQUAD *ct = nullptr; // color table + int32_t width, height, colortype, numCt, invert; + + numCt = 0; + width = Bm16.Width; // bitmap width in pixels. + height = Bm16.Height; // bitmap height in scan lines. + colortype = Bm16.BitsPixel; // seems to be BitCount Enumeration + invert = 0; + + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + + if(colortype < 16)return; // these would need a colortable if they were a dib, no idea what bm16 is supposed to do instead. + + if(!DIB_to_RGBA(// This is not really a dib, but close enough so that it still works. + px, // DIB pixel array + ct, // DIB color table (always NULL here) + numCt, // DIB color table number of entries (always 0) + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + + gchar *base64String=nullptr; + if(mempng.buffer){ // image was Bm16 in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + tmp_image << " transform=" << current_matrix(d, 0.0, 0.0, 0); // returns an identity matrix, no offsets. + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \fn myMetaFileProc(char *contents, unsigned int length, PWMF_CALLBACK_DATA lpData) + \returns 1 on success, 0 on error + \param contents binary contents of an WMF file + \param length length in bytes of contents + \param d Inkscape data structures returned by this call +*/ +//THis was a callback, just build it into a normal function +int Wmf::myMetaFileProc(const char *contents, unsigned int length, PWMF_CALLBACK_DATA d) +{ + uint32_t off=0; + uint32_t wmr_mask; + int OK =1; + int file_status=1; + TCHUNK_SPECS tsp; + uint8_t iType; + int nSize; // size of the current record, in bytes, or an error value if <=0 + const char *blimit = contents + length; // 1 byte past the end of the last record + + /* variables used to retrieve data from WMF records */ + uint16_t utmp16; + U_POINT16 pt16; // any point + U_RECT16 rc; // any rectangle, usually a bounding rectangle + U_POINT16 Dst; // Destination coordinates + U_POINT16 cDst; // Destination w,h, if different from Src + U_POINT16 Src; // Source coordinates + U_POINT16 cSrc; // Source w,h, if different from Dst + U_POINT16 cwh; // w,h, if Src and Dst use the same values + uint16_t cUsage; // colorusage enumeration + uint32_t dwRop3; // raster operations, these are only barely supported here + const char *dib; // DIB style image structure + U_BITMAP16 Bm16; // Bitmap16 style image structure + const char *px; // Image for Bm16 + uint16_t cPts; // number of points in the next variable + const char *points; // any list of U_POINT16, may not be aligned + int16_t tlen; // length of returned text, in bytes + const char *text; // returned text, Latin1 encoded + uint16_t Opts; + const int16_t *dx; // character spacing for one text mode, inkscape ignores this + double left, right, top, bottom; // values used, because a bounding rect can have values reversed L<->R, T<->B + + uint16_t tbkMode = U_TRANSPARENT; // holds proposed change to bkMode, if text is involved saving these to the DC must wait until the text is written + U_COLORREF tbkColor = U_RGB(255, 255, 255); // holds proposed change to bkColor + + // code for end user debugging + int wDbgRecord=0; + int wDbgComment=0; + int wDbgFinal=0; + char const* wDbgString = getenv( "INKSCAPE_DBG_WMF" ); + if ( wDbgString != nullptr ) { + if(strstr(wDbgString,"RECORD")){ wDbgRecord = 1; } + if(strstr(wDbgString,"COMMENT")){ wDbgComment = 1; } + if(strstr(wDbgString,"FINAL")){ wDbgFinal = 1; } + } + + /* initialize the tsp for text reassembly */ + tsp.string = nullptr; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.spaces = 0; // this field is only used for debugging + tsp.color.Red = 0; /* RGB Black */ + tsp.color.Green = 0; /* RGB Black */ + tsp.color.Blue = 0; /* RGB Black */ + tsp.color.Reserved = 0; /* not used */ + tsp.italics = 0; + tsp.weight = 80; + tsp.decoration = TXTDECOR_NONE; + tsp.condensed = 100; + tsp.co = 0; + tsp.fi_idx = -1; /* set to an invalid */ + tsp.rt_tidx = -1; /* set to an invalid */ + + SVGOStringStream dbg_str; + + /* There is very little information in WMF headers, get what is there. In many cases pretty much everything will have to + default. If there is no placeable header we know pretty much nothing about the size of the page, in which case + assume that it is 1440 WMF pixels/inch and make the page A4 landscape. That is almost certainly the wrong page size + but it has to be set to something, and nothing horrible happens if the drawing goes off the page. */ + { + + U_WMRPLACEABLE Placeable; + U_WMRHEADER Header; + off = 0; + nSize = wmfheader_get(contents, blimit, &Placeable, &Header); + if (!nSize) { + return(0); + } + if(!Header.nObjects){ Header.nObjects = 256; }// there _may_ be WMF files with no objects, more likely it is corrupt. Try to use it anyway. + d->n_obj = Header.nObjects; + d->wmf_obj = new WMF_OBJECT[d->n_obj]; + d->low_water = 0; // completely empty at this point, so start searches at 0 + + // Init the new wmf_obj list elements to null, provided the + // dynamic allocation succeeded. + if ( d->wmf_obj != nullptr ) + { + for( int i=0; i < d->n_obj; ++i ) + d->wmf_obj[i].record = nullptr; + } //if + + if(!Placeable.Inch){ Placeable.Inch= 1440; } + if(!Placeable.Dst.right && !Placeable.Dst.left){ // no page size has been supplied + // This is gross, scan forward looking for a SETWINDOWEXT record, use the first one found to + // define the page size + int hold_nSize = off = nSize; + Placeable.Dst.left = 0; + Placeable.Dst.top = 0; + while(OK){ + nSize = U_WMRRECSAFE_get(contents + off, blimit); + if(nSize){ + iType = *(uint8_t *)(contents + off + offsetof(U_METARECORD, iType ) ); + if(iType == U_WMR_SETWINDOWEXT){ + OK=0; + (void) U_WMRSETWINDOWEXT_get(contents + off, &Dst); + Placeable.Dst.right = Dst.x; + Placeable.Dst.bottom = Dst.y; + } + else if(iType == U_WMR_EOF){ + OK=0; + // Really messed up WMF, have to set the page to something, make it A4 horizontal + Placeable.Dst.right = round(((double) Placeable.Inch) * 297.0/25.4); + Placeable.Dst.bottom = round(((double) Placeable.Inch) * 210.0/25.4); + } + else { + off += nSize; + } + } + else { + return(0); + } + } + off=0; + nSize = hold_nSize; + OK=1; + } + + // drawing size in WMF pixels + d->PixelsInX = Placeable.Dst.right - Placeable.Dst.left + 1; + d->PixelsInY = Placeable.Dst.bottom - Placeable.Dst.top + 1; + + /* + Set values for Window and ViewPort extents to 0 - not defined yet. + */ + d->dc[d->level].sizeView.x = d->dc[d->level].sizeWnd.x = 0; + d->dc[d->level].sizeView.y = d->dc[d->level].sizeWnd.y = 0; + + /* Upper left corner in device units, usually both 0, but not always. + If a placeable header is used, and later a windoworg/windowext are found, then + the placeable information will be ignored. + */ + d->ulCornerInX = Placeable.Dst.left; + d->ulCornerInY = Placeable.Dst.top; + + d->E2IdirY = 1.0; // assume MM_ANISOTROPIC, if not, this will be changed later + d->D2PscaleX = d->D2PscaleY = Inkscape::Util::Quantity::convert(1, "in", "px")/(double) Placeable.Inch; + trinfo_load_qe(d->tri, d->D2PscaleX); /* quantization error that will affect text positions */ + + // drawing size in Inkscape pixels + d->PixelsOutX = d->PixelsInX * d->D2PscaleX; + d->PixelsOutY = d->PixelsInY * d->D2PscaleY; + + // Upper left corner in Inkscape units + d->ulCornerOutX = d->ulCornerInX * d->D2PscaleX; + d->ulCornerOutY = d->ulCornerInY * d->E2IdirY * d->D2PscaleY; + + d->dc[0].style.stroke_width.value = pix_to_abs_size( d, 1 ); // This could not be set until the size of the WMF was known + dbg_str << "\n"; + + d->outdef += "\n"; + + SVGOStringStream tmp_outdef; + tmp_outdef << "PixelsOutX, "px", "mm") << "mm\"\n" << + " height=\"" << Inkscape::Util::Quantity::convert(d->PixelsOutY, "px", "mm") << "mm\">\n"; + d->outdef += tmp_outdef.str().c_str(); + d->outdef += ""; // temporary end of header + + // d->defs holds any defines which are read in. + + + } + + + + while(OK){ + if (off>=length) { + return(0); //normally should exit from while after WMREOF sets OK to false. + } + contents += nSize; // pointer to the start of the next record + off += nSize; // offset from beginning of buffer to the start of the next record + + /* Currently this is a weaker check than for EMF, it only checks the size of the constant part + of the record */ + nSize = U_WMRRECSAFE_get(contents, blimit); + if(!nSize) { + file_status = 0; + break; + } + + iType = *(uint8_t *)(contents + offsetof(U_METARECORD, iType ) ); + + wmr_mask = U_wmr_properties(iType); + if (wmr_mask == U_WMR_INVALID) { + file_status = 0; + break; + } +// At run time define environment variable INKSCAPE_DBG_WMF to include string RECORD. +// Users may employ this to track down toxic records + if(wDbgRecord){ + std::cout << "record type: " << iType << " name " << U_wmr_names(iType) << " length: " << nSize << " offset: " << off <dirty:"<< d->tri->dirty << " wmr_mask: " << std::hex << wmr_mask << std::dec << std::endl; + + // incompatible change to text drawing detected (color or background change) forces out existing text + // OR + // next record is valid type and forces pending text to be drawn immediately + if ((d->dc[d->level].dirty & DIRTY_TEXT) || ((wmr_mask != U_WMR_INVALID) && (wmr_mask & U_DRAW_TEXT) && d->tri->dirty)){ + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\ndc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + SVGOStringStream ts; + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + if (d->dc[d->level].clip_id){ + d->outsvg += "\n\n"; + } + } + if(d->dc[d->level].dirty){ //Apply the delayed background changes, clear the flag + d->dc[d->level].bkMode = tbkMode; + memcpy(&(d->dc[d->level].bkColor),&tbkColor, sizeof(U_COLORREF)); + + if(d->dc[d->level].dirty & DIRTY_TEXT){ + // U_COLORREF and TRCOLORREF are exactly the same in memory, but the compiler needs some convincing... + if(tbkMode == U_TRANSPARENT){ (void) trinfo_load_bk(d->tri, BKCLR_NONE, *(TRCOLORREF *) &tbkColor); } + else { (void) trinfo_load_bk(d->tri, BKCLR_LINE, *(TRCOLORREF *) &tbkColor); } // Opaque + } + + /* It is possible to have a series of EMF records that would result in + the following creating hash patterns which are never used. For instance, if + there were a series of records that changed the background color but did nothing + else. + */ + if((d->dc[d->level].fill_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_FILL)){ + select_brush(d, d->dc[d->level].fill_recidx); + } + + d->dc[d->level].dirty = 0; + } + +//std::cout << "BEFORE DRAW logic d->mask: " << std::hex << d->mask << " wmr_mask: " << wmr_mask << std::dec << std::endl; +/* +std::cout << "BEFORE DRAW" + << " test0 " << ( d->mask & U_DRAW_VISIBLE) + << " test1 " << ( d->mask & U_DRAW_FORCE) + << " test2 " << (wmr_mask & U_DRAW_ALTERS) + << " test2.5 " << ((d->mask & U_DRAW_NOFILL) != (wmr_mask & U_DRAW_NOFILL) ) + << " test3 " << (wmr_mask & U_DRAW_VISIBLE) + << " test4 " << !(d->mask & U_DRAW_ONLYTO) + << " test5 " << ((d->mask & U_DRAW_ONLYTO) && !(wmr_mask & U_DRAW_ONLYTO) ) + << std::endl; +*/ + /* spurious moveto records should not affect the drawing. However, they set the NOFILL + bit and that messes up the logic about when to emit a path. So prune out any + stray moveto records. That is those which were never followed by a lineto. + */ + if((d->mask & U_DRAW_NOFILL) && !(d->mask & U_DRAW_VISIBLE) && + !(wmr_mask & U_DRAW_ONLYTO) && (wmr_mask & U_DRAW_VISIBLE)){ + d->mask ^= U_DRAW_NOFILL; + } + + if( + (wmr_mask != U_WMR_INVALID) && // next record is valid type + (d->mask & U_DRAW_VISIBLE) && // This record is drawable + ( + (d->mask & U_DRAW_FORCE) || // This draw is forced by STROKE/FILL/STROKEANDFILL PATH + (wmr_mask & U_DRAW_ALTERS) || // Next record would alter the drawing environment in some way + ((d->mask & U_DRAW_NOFILL) != (wmr_mask & U_DRAW_NOFILL)) || // Fill<->!Fill requires a draw between + ( (wmr_mask & U_DRAW_VISIBLE) && // Next record is visible... + ( + ( !(d->mask & U_DRAW_ONLYTO) ) || // Non *TO records cannot be followed by any Visible + ((d->mask & U_DRAW_ONLYTO) && !(wmr_mask & U_DRAW_ONLYTO) )// *TO records can only be followed by other *TO records + ) + ) + ) + ){ +// std::cout << "PATH DRAW at TOP <<+++++++++++++++++++++++++++++++++++++" << std::endl; + if(!(d->path.empty())){ + d->outsvg += " outsvg += "\n\t"; + d->outsvg += "\n\td=\""; // this is the ONLY place d=" should be used!!!! + d->outsvg += d->path; + d->outsvg += " \" /> \n"; + d->path = ""; //reset the path + } + // reset the flags + d->mask = 0; + d->drawtype = 0; + } +// std::cout << "AFTER DRAW logic d->mask: " << std::hex << d->mask << " wmr_mask: " << wmr_mask << std::dec << std::endl; + switch (iType) + { + case U_WMR_EOF: + { + dbg_str << "\n"; + + d->outsvg = d->outdef + d->defs + "\n\n\n" + d->outsvg + "\n"; + OK=0; + break; + } + case U_WMR_SETBKCOLOR: + { + dbg_str << "\n"; + nSize = U_WMRSETBKCOLOR_get(contents, &tbkColor); + if(memcmp(&tbkColor, &(d->dc[d->level].bkColor), sizeof(U_COLORREF))){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + tbkMode = d->dc[d->level].bkMode; + } + break; + } + case U_WMR_SETBKMODE:{ + dbg_str << "\n"; + nSize = U_WMRSETBKMODE_get(contents, &tbkMode); + if(tbkMode != d->dc[d->level].bkMode){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + } + memcpy(&tbkColor,&(d->dc[d->level].bkColor),sizeof(U_COLORREF)); + } + break; + } + case U_WMR_SETMAPMODE: + { + dbg_str << "\n"; + nSize = U_WMRSETMAPMODE_get(contents, &utmp16); + switch (utmp16){ + case U_MM_TEXT: + default: + // Use all values from the header. + break; + /* For all of the following the indicated scale this will be encoded in WindowExtEx/ViewportExtex + and show up in ScaleIn[XY] + */ + case U_MM_LOMETRIC: // 1 LU = 0.1 mm, + case U_MM_HIMETRIC: // 1 LU = 0.01 mm + case U_MM_LOENGLISH: // 1 LU = 0.1 in + case U_MM_HIENGLISH: // 1 LU = 0.01 in + case U_MM_TWIPS: // 1 LU = 1/1440 in + d->E2IdirY = -1.0; + // Use d->D2Pscale[XY] values from the header. + break; + case U_MM_ISOTROPIC: // ScaleIn[XY] should be set elsewhere by SETVIEWPORTEXTEX and SETWINDOWEXTEX + case U_MM_ANISOTROPIC: + break; + } + break; + } + case U_WMR_SETROP2: + { + dbg_str << "\n"; + nSize = U_WMRSETROP2_get(contents, &utmp16); + d->dwRop2 = utmp16; + break; + } + case U_WMR_SETRELABS: dbg_str << "\n"; break; + case U_WMR_SETPOLYFILLMODE: + { + dbg_str << "\n"; + nSize = U_WMRSETPOLYFILLMODE_get(contents, &utmp16); + d->dc[d->level].style.fill_rule.value = + (utmp16 == U_ALTERNATE ? SP_WIND_RULE_NONZERO + : utmp16 == U_WINDING ? SP_WIND_RULE_INTERSECT : SP_WIND_RULE_NONZERO); + break; + } + case U_WMR_SETSTRETCHBLTMODE: + { + dbg_str << "\n"; + nSize = U_WMRSETSTRETCHBLTMODE_get(contents, &utmp16); + BLTmode = utmp16; + break; + } + case U_WMR_SETTEXTCHAREXTRA: dbg_str << "\n"; break; + case U_WMR_SETTEXTCOLOR: + { + dbg_str << "\n"; + nSize = U_WMRSETTEXTCOLOR_get(contents, &(d->dc[d->level].textColor)); + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + } + // not text_dirty, because multicolored complex text is supported in libTERE + break; + } + case U_WMR_SETTEXTJUSTIFICATION: dbg_str << "\n"; break; + case U_WMR_SETWINDOWORG: + { + dbg_str << "\n"; + nSize = U_WMRSETWINDOWORG_get(contents, &d->dc[d->level].winorg); + d->ulCornerOutX = 0.0; // In the examples seen to date if this record is used with a placeable header, that header is ignored + d->ulCornerOutY = 0.0; + break; + } + case U_WMR_SETWINDOWEXT: + { + dbg_str << "\n"; + + nSize = U_WMRSETWINDOWEXT_get(contents, &d->dc[d->level].sizeWnd); + + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd.x = d->PixelsOutX; + d->dc[d->level].sizeWnd.y = d->PixelsOutY; + } + } + else { + /* There are a lot WMF files in circulation with the x,y values in the setwindowext reversed. If this is detected, swap them. + There is a remote possibility that the strange scaling this implies was intended, and those will be rendered incorrectly */ + double Ox = d->PixelsOutX; + double Oy = d->PixelsOutY; + double Wx = d->dc[d->level].sizeWnd.x; + double Wy = d->dc[d->level].sizeWnd.y; + if(Wx != Wy && Geom::are_near(Ox/Wy, Oy/Wx, 1.01/MIN(Wx,Wy)) ){ + int tmp; + tmp = d->dc[d->level].sizeWnd.x; + d->dc[d->level].sizeWnd.x = d->dc[d->level].sizeWnd.y; + d->dc[d->level].sizeWnd.y = tmp; + } + } + + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + /* Previously it used sizeWnd, but that always resulted in scale = 1 if no viewport ever appeared, and in most files, it did not */ + d->dc[d->level].sizeView.x = d->PixelsInX - 1; + d->dc[d->level].sizeView.y = d->PixelsInY - 1; + } + + /* scales logical to WMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.x && d->dc[d->level].sizeWnd.y) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.x / (double) d->dc[d->level].sizeWnd.x; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.y / (double) d->dc[d->level].sizeWnd.y; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_WMR_SETVIEWPORTORG: + { + dbg_str << "\n"; + nSize = U_WMRSETVIEWPORTORG_get(contents, &d->dc[d->level].vieworg); + break; + } + case U_WMR_SETVIEWPORTEXT: + { + dbg_str << "\n"; + + nSize = U_WMRSETVIEWPORTEXT_get(contents, &d->dc[d->level].sizeView); + + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + d->dc[d->level].sizeView.x = d->PixelsOutX; + d->dc[d->level].sizeView.y = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + } + + /* scales logical to WMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.x && d->dc[d->level].sizeWnd.y) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.x / (double) d->dc[d->level].sizeWnd.x; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.y / (double) d->dc[d->level].sizeWnd.y; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_WMR_OFFSETWINDOWORG: dbg_str << "\n"; break; + case U_WMR_SCALEWINDOWEXT: dbg_str << "\n"; break; + case U_WMR_OFFSETVIEWPORTORG: dbg_str << "\n"; break; + case U_WMR_SCALEVIEWPORTEXT: dbg_str << "\n"; break; + case U_WMR_LINETO: + { + dbg_str << "\n"; + + nSize = U_WMRLINETO_get(contents, &pt16); + + d->mask |= wmr_mask; + + tmp_path << "\n\tL " << pix_to_xy( d, pt16.x, pt16.y) << " "; + break; + } + case U_WMR_MOVETO: + { + dbg_str << "\n"; + + nSize = U_WMRLINETO_get(contents, &pt16); + + d->mask |= wmr_mask; + + d->dc[d->level].cur = pt16; + + tmp_path << + "\n\tM " << pix_to_xy( d, pt16.x, pt16.y ) << " "; + break; + } + case U_WMR_EXCLUDECLIPRECT: + { + dbg_str << "\n"; + + U_RECT16 rc; + nSize = U_WMREXCLUDECLIPRECT_get(contents, &rc); + + SVGOStringStream tmp_path; + float faraway = 10000000; // hopefully well outside any real drawing! + //outer rect, clockwise + tmp_path << "M " << faraway << "," << faraway << " "; + tmp_path << "L " << faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << faraway << " "; + tmp_path << "z "; + //inner rect, counterclockwise (sign of Y is reversed) + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_WMR_INTERSECTCLIPRECT: + { + dbg_str << "\n"; + + nSize = U_WMRINTERSECTCLIPRECT_get(contents, &rc); + + SVGOStringStream tmp_path; + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_WMR_ARC: + { + dbg_str << "\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRARC_get(contents, &ArcStart, &ArcEnd, &rc); + + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + int stat = wmr_arc_points(rc, ArcStart, ArcEnd,&f1, f2, ¢er, &start, &end, &size); + if(!stat){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + d->mask |= wmr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_WMR_ELLIPSE: + { + dbg_str << "\n"; + + nSize = U_WMRELLIPSE_get(contents, &rc); + + double cx = pix_to_x_point( d, (rc.left + rc.right)/2.0, (rc.bottom + rc.top)/2.0 ); + double cy = pix_to_y_point( d, (rc.left + rc.right)/2.0, (rc.bottom + rc.top)/2.0 ); + double rx = pix_to_abs_size( d, std::abs(rc.right - rc.left )/2.0 ); + double ry = pix_to_abs_size( d, std::abs(rc.top - rc.bottom)/2.0 ); + + SVGOStringStream tmp_ellipse; + tmp_ellipse << "cx=\"" << cx << "\" "; + tmp_ellipse << "cy=\"" << cy << "\" "; + tmp_ellipse << "rx=\"" << rx << "\" "; + tmp_ellipse << "ry=\"" << ry << "\" "; + + d->mask |= wmr_mask; + + d->outsvg += " outsvg += "\n\t"; + d->outsvg += tmp_ellipse.str().c_str(); + d->outsvg += "/> \n"; + d->path = ""; + break; + } + case U_WMR_FLOODFILL: dbg_str << "\n"; break; + case U_WMR_PIE: + { + dbg_str << "\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRPIE_get(contents, &ArcStart, &ArcEnd, &rc); + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!wmr_arc_points(rc, ArcStart, ArcEnd, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, center.x, center.y); + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= wmr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_WMR_RECTANGLE: + { + dbg_str << "\n"; + + nSize = U_WMRRECTANGLE_get(contents, &rc); + U_sanerect16(rc, &left, &top, &right, &bottom); + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, left , top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, right, top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, right, bottom ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, left, bottom ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_ROUNDRECT: + { + dbg_str << "\n"; + + int16_t Height,Width; + nSize = U_WMRROUNDRECT_get(contents, &Width, &Height, &rc); + U_sanerect16(rc, &left, &top, &right, &bottom); + double f = 4.*(sqrt(2) - 1)/3; + double f1 = 1.0 - f; + double cnx = Width/2; + double cny = Height/2; + + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n" + << " M " + << pix_to_xy(d, left , top + cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, left , top + cny*f1 ) + << " " + << pix_to_xy(d, left + cnx*f1 , top ) + << " " + << pix_to_xy(d, left + cnx , top ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, right - cnx , top ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, right - cnx*f1 , top ) + << " " + << pix_to_xy(d, right , top + cny*f1 ) + << " " + << pix_to_xy(d, right , top + cny ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, right , bottom - cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, right , bottom - cny*f1 ) + << " " + << pix_to_xy(d, right - cnx*f1 , bottom ) + << " " + << pix_to_xy(d, right - cnx , bottom ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, left + cnx , bottom ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, left + cnx*f1 , bottom ) + << " " + << pix_to_xy(d, left , bottom - cny*f1 ) + << " " + << pix_to_xy(d, left , bottom - cny ) + << "\n"; + tmp_rectangle << " z\n"; + + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_PATBLT: + { + dbg_str << "\n"; + // Treat this like any other rectangle, ie, ignore the dwRop3 + nSize = U_WMRPATBLT_get(contents, &Dst, &cwh, &dwRop3); + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, Dst.x , Dst.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x + cwh.x, Dst.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x + cwh.x, Dst.y + cwh.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x, Dst.y + cwh.y ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_SAVEDC: + { + dbg_str << "\n"; + + if (d->level < WMF_MAX_DC) { + d->dc[d->level + 1] = d->dc[d->level]; + if(d->dc[d->level].font_name){ + d->dc[d->level + 1].font_name = strdup(d->dc[d->level].font_name); // or memory access problems because font name pointer duplicated + } + d->level = d->level + 1; + } + break; + } + case U_WMR_SETPIXEL: dbg_str << "\n"; break; + case U_WMR_OFFSETCLIPRGN: + { + dbg_str << "\n"; + U_POINT16 off; + nSize = U_WMROFFSETCLIPRGN_get(contents,&off); + if (d->dc[d->level].clip_id) { // can only offset an existing clipping path + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector tmp_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + double ox = pix_to_x_point(d, off.x, off.y) - pix_to_x_point(d, 0, 0); // take into account all active transforms + double oy = pix_to_y_point(d, off.x, off.y) - pix_to_y_point(d, 0, 0); + Geom::Affine tf = Geom::Translate(ox,oy); + tmp_vect *= tf; + char *tmp_path = sp_svg_write_path(tmp_vect); + add_clips(d, tmp_path, U_RGN_COPY); + free(tmp_path); + } + break; + } + // U_WMR_TEXTOUT should be here, but has been moved down to merge with U_WMR_EXTTEXTOUT + case U_WMR_BITBLT: + { + dbg_str << "\n"; + nSize = U_WMRBITBLT_get(contents,&Dst,&cwh,&Src,&dwRop3,&Bm16,&px); + if(!px){ + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cwh.x; + int32_t dh = cwh.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { /* Not done yet, Bm16 image present */ } + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cwh.x); + double dh = pix_to_abs_size( d, cwh.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_bm16_to_image(d,Bm16,px,dx,dy,dw,dh,sx,sy,sw,sh); + break; + } + case U_WMR_STRETCHBLT: + { + dbg_str << "\n"; + nSize = U_WMRSTRETCHBLT_get(contents,&Dst,&cDst,&Src,&cSrc,&dwRop3,&Bm16,&px); + if(!px){ + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cDst.x; + int32_t dh = cDst.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { /* Not done yet, Bm16 image present */ } + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_bm16_to_image(d,Bm16,px,dx,dy,dw,dh,sx,sy,sw,sh); + break; + } + case U_WMR_POLYGON: + case U_WMR_POLYLINE: + { + dbg_str << "\n"; + nSize = U_WMRPOLYGON_get(contents, &cPts, &points); + uint32_t i; + + if (cPts < 2)break; + + d->mask |= wmr_mask; + memcpy(&pt16,points,U_SIZE_POINT16); points += U_SIZE_POINT16; + + tmp_str << "\n\tM " << pix_to_xy( d, pt16.x, pt16.y) << " "; + + for (i=1; i\n"; + uint16_t Escape, elen; + nSize = U_WMRESCAPE_get(contents, &Escape, &elen, &text); + if(elen>=4){ + uint32_t utmp4; + memcpy(&utmp4, text ,4); + if(Escape == U_MFE_SETLINECAP){ + switch (utmp4 & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + } + else if(Escape == U_MFE_SETLINEJOIN){ + switch (utmp4 & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + } + else if(Escape == U_MFE_SETMITERLIMIT){ + //The function takes a float but uses a 32 bit int in the record. + float miterlimit = utmp4; + d->dc[d->level].style.stroke_miterlimit.value = miterlimit; //ratio, not a pt size + if (d->dc[d->level].style.stroke_miterlimit.value < 2) + d->dc[d->level].style.stroke_miterlimit.value = 2.0; + } + } + break; + } + case U_WMR_RESTOREDC: + { + dbg_str << "\n"; + + int16_t DC; + nSize = U_WMRRESTOREDC_get(contents, &DC); + int old_level = d->level; + if (DC >= 0) { + if (DC < d->level) + d->level = DC; + } + else { + if (d->level + DC >= 0) + d->level = d->level + DC; + } + while (old_level > d->level) { + if (!d->dc[old_level].style.stroke_dasharray.values.empty() && + (old_level == 0 || (old_level > 0 && d->dc[old_level].style.stroke_dasharray != + d->dc[old_level - 1].style.stroke_dasharray))) { + d->dc[old_level].style.stroke_dasharray.values.clear(); + } + if(d->dc[old_level].font_name){ + free(d->dc[old_level].font_name); // else memory leak + d->dc[old_level].font_name = nullptr; + } + old_level--; + } + break; + } + case U_WMR_FILLREGION: dbg_str << "\n"; break; + case U_WMR_FRAMEREGION: dbg_str << "\n"; break; + case U_WMR_INVERTREGION: dbg_str << "\n"; break; + case U_WMR_PAINTREGION: dbg_str << "\n"; break; + case U_WMR_SELECTCLIPREGION: + { + dbg_str << "\n"; + nSize = U_WMRSELECTCLIPREGION_get(contents, &utmp16); + if (utmp16 == U_RGN_COPY) + clipset = false; + break; + } + case U_WMR_SELECTOBJECT: + { + dbg_str << "\n"; + + nSize = U_WMRSELECTOBJECT_get(contents, &utmp16); + unsigned int index = utmp16; + + // WMF has no stock objects + if ( /*index >= 0 &&*/ index < (unsigned int) d->n_obj) { + switch (d->wmf_obj[index].type) + { + case U_WMR_CREATEPENINDIRECT: + select_pen(d, index); + break; + case U_WMR_CREATEBRUSHINDIRECT: + case U_WMR_DIBCREATEPATTERNBRUSH: + case U_WMR_CREATEPATTERNBRUSH: // <- this one did not display properly on XP, DIBCREATEPATTERNBRUSH works + select_brush(d, index); + break; + case U_WMR_CREATEFONTINDIRECT: + select_font(d, index); + break; + case U_WMR_CREATEPALETTE: + case U_WMR_CREATEBITMAPINDIRECT: + case U_WMR_CREATEBITMAP: + case U_WMR_CREATEREGION: + /* these do not do anything, but their objects must be kept in the count */ + break; + } + } + break; + } + case U_WMR_SETTEXTALIGN: + { + dbg_str << "\n"; + nSize = U_WMRSETTEXTALIGN_get(contents, &(d->dc[d->level].textAlign)); + break; + } + case U_WMR_DRAWTEXT: dbg_str << "\n"; break; + case U_WMR_CHORD: + { + dbg_str << "\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRCHORD_get(contents, &ArcStart, &ArcEnd, &rc); + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!wmr_arc_points(rc, ArcStart, ArcEnd, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0 ; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= wmr_mask; + } + else { + dbg_str << "\n"; + } + break; + } + case U_WMR_SETMAPPERFLAGS: dbg_str << "\n"; break; + case U_WMR_TEXTOUT: + case U_WMR_EXTTEXTOUT: + { + if(iType == U_WMR_TEXTOUT){ + dbg_str << "\n"; + nSize = U_WMRTEXTOUT_get(contents, &Dst, &tlen, &text); + Opts=0; + } + else { + dbg_str << "\n"; + nSize = U_WMREXTTEXTOUT_get(contents, &Dst, &tlen, &Opts, &text, &dx, &rc ); + } + uint32_t fOptions = Opts; + + double x1,y1; + int cChars; + x1 = Dst.x; + y1 = Dst.y; + cChars = tlen; + + if (d->dc[d->level].textAlign & U_TA_UPDATECP) { + x1 = d->dc[d->level].cur.x; + y1 = d->dc[d->level].cur.y; + } + + double x = pix_to_x_point(d, x1, y1); + double y = pix_to_y_point(d, x1, y1); + + /* Rotation issues are handled entirely in libTERE now */ + + uint32_t *dup_wt = nullptr; + + dup_wt = U_Latin1ToUtf32le(text, cChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(cChars); + + msdepua(dup_wt); //convert everything in Microsoft's private use area. For Symbol, Wingdings, Dingbats + + if(NonToUnicode(dup_wt, d->dc[d->level].font_name)){ + free(d->dc[d->level].font_name); + d->dc[d->level].font_name = strdup("Times New Roman"); + } + + char *ansi_text; + ansi_text = (char *) U_Utf32leToUtf8((uint32_t *)dup_wt, 0, nullptr); + free(dup_wt); + // Empty text or starts with an invalid escape/control sequence, which is bogus text. Throw it out before g_markup_escape_text can make things worse + if(*((uint8_t *)ansi_text) <= 0x1F){ + free(ansi_text); + ansi_text=nullptr; + } + + if (ansi_text) { + + SVGOStringStream ts; + + gchar *escaped_text = g_markup_escape_text(ansi_text, -1); + + tsp.x = x*0.8; // TERE expects sizes in points + tsp.y = y*0.8; + tsp.color.Red = d->dc[d->level].textColor.Red; + tsp.color.Green = d->dc[d->level].textColor.Green; + tsp.color.Blue = d->dc[d->level].textColor.Blue; + tsp.color.Reserved = 0; + switch(d->dc[d->level].style.font_style.value){ + case SP_CSS_FONT_STYLE_OBLIQUE: + tsp.italics = FC_SLANT_OBLIQUE; break; + case SP_CSS_FONT_STYLE_ITALIC: + tsp.italics = FC_SLANT_ITALIC; break; + default: + case SP_CSS_FONT_STYLE_NORMAL: + tsp.italics = FC_SLANT_ROMAN; break; + } + switch(d->dc[d->level].style.font_weight.value){ + case SP_CSS_FONT_WEIGHT_100: tsp.weight = FC_WEIGHT_THIN ; break; + case SP_CSS_FONT_WEIGHT_200: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_300: tsp.weight = FC_WEIGHT_LIGHT ; break; + case SP_CSS_FONT_WEIGHT_400: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_500: tsp.weight = FC_WEIGHT_MEDIUM ; break; + case SP_CSS_FONT_WEIGHT_600: tsp.weight = FC_WEIGHT_SEMIBOLD ; break; + case SP_CSS_FONT_WEIGHT_700: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_800: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + case SP_CSS_FONT_WEIGHT_900: tsp.weight = FC_WEIGHT_HEAVY ; break; + case SP_CSS_FONT_WEIGHT_NORMAL: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_BOLD: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_LIGHTER: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_BOLDER: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + default: tsp.weight = FC_WEIGHT_NORMAL ; break; + } + // WMF only supports two types of text decoration + tsp.decoration = TXTDECOR_NONE; + if(d->dc[d->level].style.text_decoration_line.underline){ tsp.decoration |= TXTDECOR_UNDER; } + if(d->dc[d->level].style.text_decoration_line.line_through){ tsp.decoration |= TXTDECOR_STRIKE;} + + // WMF textalignment is a bit strange: 0x6 is center, 0x2 is right, 0x0 is left, the value 0x4 is also drawn left + tsp.taln = ((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_CENTER) ? ALICENTER : + (((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_LEFT) ? ALILEFT : + ALIRIGHT); + tsp.taln |= ((d->dc[d->level].textAlign & U_TA_BASEBIT) ? ALIBASE : + ((d->dc[d->level].textAlign & U_TA_BOTTOM) ? ALIBOT : + ALITOP)); + + // language direction can be encoded two ways, U_TA_RTLREADING is preferred + if( (fOptions & U_ETO_RTLREADING) || (d->dc[d->level].textAlign & U_TA_RTLREADING) ){ tsp.ldir = LDIR_RL; } + else{ tsp.ldir = LDIR_LR; } + + tsp.condensed = FC_WIDTH_NORMAL; // Not implemented well in libTERE (yet) + tsp.ori = d->dc[d->level].style.baseline_shift.value; // For now orientation is always the same as escapement + // There is no world transform, so ori need not be further rotated + tsp.string = (uint8_t *) U_strdup(escaped_text); // this will be free'd much later at a trinfo_clear(). + tsp.fs = d->dc[d->level].style.font_size.computed * 0.8; // Font size in points + char *fontspec = TR_construct_fontspec(&tsp, d->dc[d->level].font_name); + tsp.fi_idx = ftinfo_load_fontname(d->tri->fti,fontspec); + free(fontspec); + // when font name includes narrow it may not be set to "condensed". Narrow fonts do not work well anyway though + // as the metrics from fontconfig may not match, or the font may not be present. + if(0<= TR_findcasesub(d->dc[d->level].font_name, (char *) "Narrow")){ tsp.co=1; } + else { tsp.co=0; } + + int status; + + status = trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ori is actually escapement + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\ndc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + (void) trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ignore return status, it must work + if (d->dc[d->level].clip_id){ + d->outsvg += "\n\n"; + } + } + + g_free(escaped_text); + free(ansi_text); + } + + break; + } + case U_WMR_SETDIBTODEV: dbg_str << "\n"; break; + case U_WMR_SELECTPALETTE: dbg_str << "\n"; break; + case U_WMR_REALIZEPALETTE: dbg_str << "\n"; break; + case U_WMR_ANIMATEPALETTE: dbg_str << "\n"; break; + case U_WMR_SETPALENTRIES: dbg_str << "\n"; break; + case U_WMR_POLYPOLYGON: + { + dbg_str << "\n"; + uint16_t nPolys; + const uint16_t *aPolyCounts; + const char *Points; + int cpts; /* total number of points in Points*/ + nSize = U_WMRPOLYPOLYGON_get(contents, &nPolys, &aPolyCounts, &Points); + int n, i, j; + + d->mask |= wmr_mask; + + U_POINT16 apt; + for (n=cpts=0; n < nPolys; n++) { cpts += aPolyCounts[n]; } + i = 0; // offset in BYTES + cpts *= U_SIZE_POINT16; // limit for offset i, in BYTES + + for (n=0; n < nPolys && i\n"; break; + case U_WMR_3A: + case U_WMR_3B: + case U_WMR_3C: + case U_WMR_3D: + case U_WMR_3E: + case U_WMR_3F: + { + dbg_str << "\n"; + break; + } + case U_WMR_DIBBITBLT: + { + dbg_str << "\n"; + nSize = U_WMRDIBBITBLT_get(contents, &Dst, &cwh, &Src, &dwRop3, &dib); + + // Treat all nonImage bitblts as a rectangular write. Definitely not correct, but at + // least it leaves objects where the operations should have been. + if (!dib) { + // should be an application of a DIBPATTERNBRUSHPT, use a solid color instead + + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cwh.x; + int32_t dh = cwh.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + // usageSrc not defined, implicitly it must be U_DIB_RGB_COLORS + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh,U_DIB_RGB_COLORS); + } + break; + } + case U_WMR_DIBSTRETCHBLT: + { + dbg_str << "\n"; + nSize = U_WMRDIBSTRETCHBLT_get(contents, &Dst, &cDst, &Src, &cSrc, &dwRop3, &dib); + // Always grab image, ignore modes. + if (dib) { + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + // usageSrc not defined, implicitly it must be U_DIB_RGB_COLORS + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh, U_DIB_RGB_COLORS); + } + break; + } + case U_WMR_DIBCREATEPATTERNBRUSH: + { + dbg_str << "\n"; + insert_object(d, U_WMR_DIBCREATEPATTERNBRUSH, contents); + break; + } + case U_WMR_STRETCHDIB: + { + dbg_str << "\n"; + nSize = U_WMRSTRETCHDIB_get(contents, &Dst, &cDst, &Src, &cSrc, &cUsage, &dwRop3, &dib); + double dx = pix_to_x_point( d, Dst.x, Dst.y ); + double dy = pix_to_y_point( d, Dst.x, Dst.y ); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + int sx = Src.x; //source position within the bitmap, in pixels + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + uint32_t iUsageSrc; + iUsageSrc = cUsage; + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh,iUsageSrc); + + break; + } + case U_WMR_44: + case U_WMR_45: + case U_WMR_46: + case U_WMR_47: + { + dbg_str << "\n"; + break; + } + case U_WMR_EXTFLOODFILL: dbg_str << "\n"; break; + case U_WMR_49: + case U_WMR_4A: + case U_WMR_4B: + case U_WMR_4C: + case U_WMR_4D: + case U_WMR_4E: + case U_WMR_4F: + case U_WMR_50: + case U_WMR_51: + case U_WMR_52: + case U_WMR_53: + case U_WMR_54: + case U_WMR_55: + case U_WMR_56: + case U_WMR_57: + case U_WMR_58: + case U_WMR_59: + case U_WMR_5A: + case U_WMR_5B: + case U_WMR_5C: + case U_WMR_5D: + case U_WMR_5E: + case U_WMR_5F: + case U_WMR_60: + case U_WMR_61: + case U_WMR_62: + case U_WMR_63: + case U_WMR_64: + case U_WMR_65: + case U_WMR_66: + case U_WMR_67: + case U_WMR_68: + case U_WMR_69: + case U_WMR_6A: + case U_WMR_6B: + case U_WMR_6C: + case U_WMR_6D: + case U_WMR_6E: + case U_WMR_6F: + case U_WMR_70: + case U_WMR_71: + case U_WMR_72: + case U_WMR_73: + case U_WMR_74: + case U_WMR_75: + case U_WMR_76: + case U_WMR_77: + case U_WMR_78: + case U_WMR_79: + case U_WMR_7A: + case U_WMR_7B: + case U_WMR_7C: + case U_WMR_7D: + case U_WMR_7E: + case U_WMR_7F: + case U_WMR_80: + case U_WMR_81: + case U_WMR_82: + case U_WMR_83: + case U_WMR_84: + case U_WMR_85: + case U_WMR_86: + case U_WMR_87: + case U_WMR_88: + case U_WMR_89: + case U_WMR_8A: + case U_WMR_8B: + case U_WMR_8C: + case U_WMR_8D: + case U_WMR_8E: + case U_WMR_8F: + case U_WMR_90: + case U_WMR_91: + case U_WMR_92: + case U_WMR_93: + case U_WMR_94: + case U_WMR_95: + case U_WMR_96: + case U_WMR_97: + case U_WMR_98: + case U_WMR_99: + case U_WMR_9A: + case U_WMR_9B: + case U_WMR_9C: + case U_WMR_9D: + case U_WMR_9E: + case U_WMR_9F: + case U_WMR_A0: + case U_WMR_A1: + case U_WMR_A2: + case U_WMR_A3: + case U_WMR_A4: + case U_WMR_A5: + case U_WMR_A6: + case U_WMR_A7: + case U_WMR_A8: + case U_WMR_A9: + case U_WMR_AA: + case U_WMR_AB: + case U_WMR_AC: + case U_WMR_AD: + case U_WMR_AE: + case U_WMR_AF: + case U_WMR_B0: + case U_WMR_B1: + case U_WMR_B2: + case U_WMR_B3: + case U_WMR_B4: + case U_WMR_B5: + case U_WMR_B6: + case U_WMR_B7: + case U_WMR_B8: + case U_WMR_B9: + case U_WMR_BA: + case U_WMR_BB: + case U_WMR_BC: + case U_WMR_BD: + case U_WMR_BE: + case U_WMR_BF: + case U_WMR_C0: + case U_WMR_C1: + case U_WMR_C2: + case U_WMR_C3: + case U_WMR_C4: + case U_WMR_C5: + case U_WMR_C6: + case U_WMR_C7: + case U_WMR_C8: + case U_WMR_C9: + case U_WMR_CA: + case U_WMR_CB: + case U_WMR_CC: + case U_WMR_CD: + case U_WMR_CE: + case U_WMR_CF: + case U_WMR_D0: + case U_WMR_D1: + case U_WMR_D2: + case U_WMR_D3: + case U_WMR_D4: + case U_WMR_D5: + case U_WMR_D6: + case U_WMR_D7: + case U_WMR_D8: + case U_WMR_D9: + case U_WMR_DA: + case U_WMR_DB: + case U_WMR_DC: + case U_WMR_DD: + case U_WMR_DE: + case U_WMR_DF: + case U_WMR_E0: + case U_WMR_E1: + case U_WMR_E2: + case U_WMR_E3: + case U_WMR_E4: + case U_WMR_E5: + case U_WMR_E6: + case U_WMR_E7: + case U_WMR_E8: + case U_WMR_E9: + case U_WMR_EA: + case U_WMR_EB: + case U_WMR_EC: + case U_WMR_ED: + case U_WMR_EE: + case U_WMR_EF: + { + dbg_str << "\n"; + break; + } + case U_WMR_DELETEOBJECT: + { + dbg_str << "\n"; + nSize = U_WMRDELETEOBJECT_get(contents, &utmp16); + delete_object(d, utmp16); + break; + } + case U_WMR_F1: + case U_WMR_F2: + case U_WMR_F3: + case U_WMR_F4: + case U_WMR_F5: + case U_WMR_F6: + { + dbg_str << "\n"; + break; + } + case U_WMR_CREATEPALETTE: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEPALETTE, contents); + break; + } + case U_WMR_F8: dbg_str << "\n"; break; + case U_WMR_CREATEPATTERNBRUSH: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEPATTERNBRUSH, contents); + break; + } + case U_WMR_CREATEPENINDIRECT: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEPENINDIRECT, contents); + break; + } + case U_WMR_CREATEFONTINDIRECT: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEFONTINDIRECT, contents); + break; + } + case U_WMR_CREATEBRUSHINDIRECT: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEBRUSHINDIRECT, contents); + break; + } + case U_WMR_CREATEBITMAPINDIRECT: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEBITMAPINDIRECT, contents); + break; + } + case U_WMR_CREATEBITMAP: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEBITMAP, contents); + break; + } + case U_WMR_CREATEREGION: + { + dbg_str << "\n"; + insert_object(d, U_WMR_CREATEREGION, contents); + break; + } + default: + dbg_str << "\n"; + break; + } //end of switch +// At run time define environment variable INKSCAPE_DBG_WMF to include string COMMENT. +// Users may employ this to to place a comment for each processed WMR record in the SVG + if(wDbgComment){ + d->outsvg += dbg_str.str().c_str(); + } + d->path += tmp_path.str().c_str(); + if(!nSize){ // There was some problem with the processing of this record, it is not safe to continue + file_status = 0; + break; + } + + } //end of while on OK +// At run time define environment variable INKSCAPE_DBG_WMF to include string FINAL +// Users may employ this to to show the final SVG derived from the WMF + if(wDbgFinal){ + std::cout << d->outsvg << std::endl; + } + (void) U_wmr_properties(U_WMR_INVALID); // force the release of the lookup table memory, returned value is irrelevant + + return(file_status); +} + +void Wmf::free_wmf_strings(WMF_STRINGS name){ + if(name.count){ + for(int i=0; i< name.count; i++){ free(name.strings[i]); } + free(name.strings); + } + name.count = 0; + name.size = 0; +} + +SPDocument * +Wmf::open( Inkscape::Extension::Input * /*mod*/, const gchar *uri ) +{ + + if (uri == nullptr) { + return nullptr; + } + + // ensure usage of dot as decimal separator in scanf/printf functions (indepentendly of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + WMF_CALLBACK_DATA d; + + d.n_obj = 0; //these might not be set otherwise if the input file is corrupt + d.wmf_obj=nullptr; + + // Default font, WMF spec says device can pick whatever it wants. + // WMF files that do not specify a font are unlikely to look very good! + d.dc[0].style.font_size.computed = 16.0; + d.dc[0].style.font_weight.value = SP_CSS_FONT_WEIGHT_400; + d.dc[0].style.font_style.value = SP_CSS_FONT_STYLE_NORMAL; + d.dc[0].style.text_decoration_line.underline = false; + d.dc[0].style.text_decoration_line.line_through = false; + d.dc[0].style.baseline_shift.value = 0; + + // Default pen, WMF files that do not specify a pen are unlikely to look very good! + d.dc[0].style.stroke_dasharray.set = false; + d.dc[0].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; // U_PS_ENDCAP_SQUARE; + d.dc[0].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; + d.dc[0].style.stroke_width.value = 1.0; // will be reset to something reasonable once WMF drawing size is known + d.dc[0].style.stroke.value.color.set( 0, 0, 0 ); + d.dc[0].stroke_set = true; + + // Default brush is none - no fill. WMF files that do not specify a brush are unlikely to look very good! + d.dc[0].fill_set = false; + + d.dc[0].font_name = strdup("Arial"); // Default font, set only on lowest level, it copies up from there WMF spec says device can pick whatever it wants + + // set up the size default for patterns in defs. This might not be referenced if there are no patterns defined in the drawing. + + d.defs += "\n"; + d.defs += " \n"; + d.defs += " \n"; + + + size_t length; + char *contents; + if(wmf_readdata(uri, &contents, &length))return(nullptr); + + // set up the text reassembly system + if(!(d.tri = trinfo_init(nullptr)))return(nullptr); + (void) trinfo_load_ft_opts(d.tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + int good = myMetaFileProc(contents,length, &d); + free(contents); + +// std::cout << "SVG Output: " << std::endl << d.outsvg << std::endl; + + SPDocument *doc = nullptr; + if (good) { + doc = SPDocument::createNewDocFromMem(d.outsvg.c_str(), strlen(d.outsvg.c_str()), TRUE); + } + + free_wmf_strings(d.hatches); + free_wmf_strings(d.images); + free_wmf_strings(d.clips); + + if (d.wmf_obj) { + int i; + for (i=0; i\n" + "" N_("WMF Input") "\n" + "org.inkscape.input.wmf\n" + "\n" + ".wmf\n" + "image/x-wmf\n" + "" N_("Windows Metafiles (*.wmf)") "\n" + "" N_("Windows Metafiles") "\n" + "org.inkscape.output.wmf\n" + "\n" + "", new Wmf()); + + /* WMF out */ + Inkscape::Extension::build_from_mem( + "\n" + "" N_("WMF Output") "\n" + "org.inkscape.output.wmf\n" + "true\n" + "true\n" + "true\n" + "true\n" + "false\n" + "false\n" + "false\n" + "false\n" + "false\n" + "\n" + ".wmf\n" + "image/x-wmf\n" + "" N_("Windows Metafile (*.wmf)") "\n" + "" N_("Windows Metafile") "\n" + "\n" + "", new Wmf()); + + return; +} + + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-inout.h b/src/extension/internal/wmf-inout.h new file mode 100644 index 0000000..6190129 --- /dev/null +++ b/src/extension/internal/wmf-inout.h @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile Input/Output + */ +/* Authors: + * Ulf Erikson + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_EXTENSION_INTERNAL_WMF_H +#define SEEN_EXTENSION_INTERNAL_WMF_H + +#include <3rdparty/libuemf/uwmf.h> +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/implementation/implementation.h" +#include "style.h" +#include "text_reassemble.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define DIRTY_NONE 0x00 +#define DIRTY_TEXT 0x01 +#define DIRTY_FILL 0x02 +#define DIRTY_STROKE 0x04 // not used currently + +struct WMF_OBJECT { + int type = 0; + int level = 0; + char *record = nullptr; +}; +using PWMF_OBJECT = WMF_OBJECT *; + +struct WMF_STRINGS { + int size = 0; // number of slots allocated in strings + int count = 0; // number of slots used in strings + char **strings = nullptr; // place to store strings +}; +using PWMF_STRINGS = WMF_STRINGS *; + +struct WMF_DEVICE_CONTEXT { + WMF_DEVICE_CONTEXT() : + // SPStyle: class with constructor + font_name(nullptr), + clip_id(0), + stroke_set(false), stroke_mode(0), stroke_idx(0), stroke_recidx(0), + fill_set(false), fill_mode(0), fill_idx(0), fill_recidx(0), + dirty(0), + active_pen(-1), active_brush(-1), active_font(-1), // -1 when the default is used + // sizeWnd, sizeView, winorg, vieworg, + ScaleInX(0), ScaleInY(0), + ScaleOutX(0), ScaleOutY(0), + bkMode(U_TRANSPARENT), + // bkColor, textColor + textAlign(0) + // worldTransform, cur + { + font_name = nullptr; + sizeWnd = point16_set( 0.0, 0.0 ); + sizeView = point16_set( 0.0, 0.0 ); + winorg = point16_set( 0.0, 0.0 ); + vieworg = point16_set( 0.0, 0.0 ); + bkColor = U_RGB(255, 255, 255); // default foreground color (white) + textColor = U_RGB(0, 0, 0); // default foreground color (black) + cur = point16_set( 0.0, 0.0 ); + }; + SPStyle style; + char *font_name; + int clip_id; // 0 if none, else 1 + index into clips + bool stroke_set; + int stroke_mode; // enumeration from drawmode, not used if fill_set is not True + int stroke_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int stroke_recidx;// record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + bool fill_set; + int fill_mode; // enumeration from drawmode, not used if fill_set is not True + int fill_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int fill_recidx; // record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + int dirty; // holds the dirty bits for text, stroke, fill + int active_pen; // used when the active object is deleted to set the default values, -1 is none active + int active_brush; // ditto + int active_font; // ditto. also used to hold object number in case font needs to be remade due to textcolor change. + U_POINT16 sizeWnd; + U_POINT16 sizeView; + U_POINT16 winorg; + U_POINT16 vieworg; + double ScaleInX, ScaleInY; + double ScaleOutX, ScaleOutY; + uint16_t bkMode; + U_COLORREF bkColor; + U_COLORREF textColor; + uint16_t textAlign; + U_POINT16 cur; +}; +using PWMF_DEVICE_CONTEXT = WMF_DEVICE_CONTEXT *; + +#define WMF_MAX_DC 128 + + +// like this causes a mysterious crash on the return from Wmf::open +//typedef struct emf_callback_data { +// this fixes it, so some confusion between this struct and the one in emf-inout??? +//typedef struct wmf_callback_data { +// as does this +struct WMF_CALLBACK_DATA { + + WMF_CALLBACK_DATA() : + // dc: array, structure w/ constructor + level(0), + E2IdirY(1.0), + D2PscaleX(1.0), D2PscaleY(1.0), + PixelsInX(0), PixelsInY(0), + PixelsOutX(0), PixelsOutY(0), + ulCornerInX(0), ulCornerInY(0), + ulCornerOutX(0), ulCornerOutY(0), + mask(0), + arcdir(U_AD_COUNTERCLOCKWISE), + dwRop2(U_R2_COPYPEN), dwRop3(0), + id(0), drawtype(0), + // hatches, images, gradients, struct w/ constructor + tri(nullptr), + n_obj(0), + low_water(0) + //wmf_obj + {}; + + Glib::ustring outsvg; + Glib::ustring path; + Glib::ustring outdef; + Glib::ustring defs; + + WMF_DEVICE_CONTEXT dc[WMF_MAX_DC+1]; // FIXME: This should be dynamic.. + int level; + + double E2IdirY; // WMF Y direction relative to Inkscape Y direction. Will be negative for MM_LOMETRIC etc. + double D2PscaleX,D2PscaleY; // WMF device to Inkscape Page scale. + float PixelsInX, PixelsInY; // size of the drawing, in WMF device pixels + float PixelsOutX, PixelsOutY; // size of the drawing, in Inkscape pixels + double ulCornerInX,ulCornerInY; // Upper left corner, from header rclBounds, in logical units + double ulCornerOutX,ulCornerOutY; // Upper left corner, in Inkscape pixels + uint32_t mask; // Draw properties + int arcdir; // U_AD_COUNTERCLOCKWISE 1 or U_AD_CLOCKWISE 2 + + uint32_t dwRop2; // Binary raster operation, 0 if none (use brush/pen unmolested) + uint32_t dwRop3; // Ternary raster operation, 0 if none (use brush/pen unmolested) + + unsigned int id; + unsigned int drawtype; // one of 0 or U_WMR_FILLPATH, U_WMR_STROKEPATH, U_WMR_STROKEANDFILLPATH + // both of these end up in under the names shown here. These structures allow duplicates to be avoided. + WMF_STRINGS hatches; // hold pattern names, all like WMFhatch#_$$$$$$ where # is the WMF hatch code and $$$$$$ is the color + WMF_STRINGS images; // hold images, all like Image#, where # is the slot the image lives. + WMF_STRINGS clips; // hold clipping paths, referred to be the slot where the clipping path lives + TR_INFO *tri; // Text Reassembly data structure + + + int n_obj; + int low_water; // first object slot which _might_ be unoccupied. Everything below is filled. + PWMF_OBJECT wmf_obj; +}; +using PWMF_CALLBACK_DATA = WMF_CALLBACK_DATA *; + +class Wmf : public Metafile +{ + +public: + Wmf(); // Empty constructor + + ~Wmf() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + + static void init();//Initialize the class + +private: +protected: + static void print_document_to_file(SPDocument *doc, const gchar *filename); + static double current_scale(PWMF_CALLBACK_DATA d); + static std::string current_matrix(PWMF_CALLBACK_DATA d, double x, double y, int useoffset); + static double current_rotation(PWMF_CALLBACK_DATA d); + static void enlarge_hatches(PWMF_CALLBACK_DATA d); + static int in_hatches(PWMF_CALLBACK_DATA d, char *test); + static uint32_t add_hatch(PWMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor); + static void enlarge_images(PWMF_CALLBACK_DATA d); + static int in_images(PWMF_CALLBACK_DATA d, char *test); + static uint32_t add_dib_image(PWMF_CALLBACK_DATA d, const char *dib, uint32_t iUsage); + static uint32_t add_bm16_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px); + + static void enlarge_clips(PWMF_CALLBACK_DATA d); + static int in_clips(PWMF_CALLBACK_DATA d, const char *test); + static void add_clips(PWMF_CALLBACK_DATA d, const char *clippath, unsigned int logic); + + static void output_style(PWMF_CALLBACK_DATA d); + static double _pix_x_to_point(PWMF_CALLBACK_DATA d, double px); + static double _pix_y_to_point(PWMF_CALLBACK_DATA d, double py); + static double pix_to_x_point(PWMF_CALLBACK_DATA d, double px, double py); + static double pix_to_y_point(PWMF_CALLBACK_DATA d, double px, double py); + static double pix_to_abs_size(PWMF_CALLBACK_DATA d, double px); + static std::string pix_to_xy(PWMF_CALLBACK_DATA d, double x, double y); + static void select_brush(PWMF_CALLBACK_DATA d, int index); + static void select_font(PWMF_CALLBACK_DATA d, int index); + static void select_pen(PWMF_CALLBACK_DATA d, int index); + static int insertable_object(PWMF_CALLBACK_DATA d); + static void delete_object(PWMF_CALLBACK_DATA d, int index); + static int insert_object(PWMF_CALLBACK_DATA d, int type, const char *record); + static uint32_t *unknown_chars(size_t count); + static void common_dib_to_image(PWMF_CALLBACK_DATA d, const char *dib, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, uint32_t iUsage); + static void common_bm16_to_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh); + static int myMetaFileProc(const char *contents, unsigned int length, PWMF_CALLBACK_DATA d); + static void free_wmf_strings(WMF_STRINGS name); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* EXTENSION_INTERNAL_WMF_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-print.cpp b/src/extension/internal/wmf-print.cpp new file mode 100644 index 0000000..7d2bbbb --- /dev/null +++ b/src/extension/internal/wmf-print.cpp @@ -0,0 +1,1612 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile printing + */ +/* Authors: + * Ulf Erikson + * Jon A. Cruz + * Abhishek Sharma + * David Mathog + * + * Copyright (C) 2006-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/elliptical-arc.h> + +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include "helper/geom.h" +#include "helper/geom-curves.h" + +#include "inkscape-version.h" + +#include "util/units.h" + +#include "extension/system.h" +#include "extension/print.h" +#include "document.h" +#include "path-prefix.h" + +#include "object/sp-pattern.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-root.h" +#include "object/sp-item.h" + +#include "splivarot.h" // pieces for union on shapes +#include <2geom/svg-path-parser.h> // to get from SVG text to Geom::Path +#include "display/canvas-bpath.h" // for SPWindRule +#include "display/cairo-utils.h" // for Inkscape::Pixbuf::PF_CAIRO + +#include "wmf-print.h" + +#include +#include <3rdparty/libuemf/symbol_convert.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define PXPERMETER 2835 +#define MAXDISP 2.0 // This should be set in the output dialog. This is ok for experimenting, no more than 2 pixel deviation. Not actually used at present + + +/* globals */ +static double PX2WORLD; // value set in begin() +static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTPatternAsHatch; +static WMFTRACK *wt = nullptr; +static WMFHANDLES *wht = nullptr; + +void PrintWmf::smuggle_adxky_out(const char *string, int16_t **adx, double *ky, int *rtl, int *ndx, float scale) +{ + float fdx; + int i; + int16_t *ladx; + const char *cptr = &string[strlen(string) + 1]; // this works because of the first fake terminator + + *adx = nullptr; + *ky = 0.0; // set a default value + sscanf(cptr, "%7d", ndx); + if (!*ndx) { + return; // this could happen with an empty string + } + cptr += 7; + ladx = (int16_t *) malloc(*ndx * sizeof(int16_t)); + if (!ladx) { + g_error("Out of memory"); + } + *adx = ladx; + for (i = 0; i < *ndx; i++, cptr += 7, ladx++) { + sscanf(cptr, "%7f", &fdx); + *ladx = (int16_t) round(fdx * scale); + } + cptr++; // skip 2nd fake terminator + sscanf(cptr, "%7f", &fdx); + *ky = fdx; + cptr += 7; // advance over ky and its space + sscanf(cptr, "%07d", rtl); +} + +PrintWmf::PrintWmf() +{ + // all of the class variables are initialized elsewhere, many in PrintWmf::Begin, +} + + +unsigned int PrintWmf::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + + +unsigned int PrintWmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) +{ + char *rec; + gchar const *utf8_fn = mod->get_param_string("destination"); + + // Typically PX2WORLD is 1200/90, using inkscape's default dpi + PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); + FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); + FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); + FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); + FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); + + (void) wmf_start(utf8_fn, 1000000, 250000, &wt); // Initialize the wt structure + (void) wmf_htable_create(128, 128, &wht); // Initialize the wht structure + + // WMF header the only things that can be set are the page size in inches (w,h) and the dpi + // width and height in px + _width = doc->getWidth().value("px"); + _height = doc->getHeight().value("px"); + + // initialize a few global variables + hbrush = hpen = 0; + htextalignment = U_TA_BASELINE | U_TA_LEFT; + use_stroke = use_fill = simple_shape = usebk = false; + + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv) { + const char *p1 = nv->attribute("pagecolor"); + char *p2; + uint32_t lc = strtoul(&p1[1], &p2, 16); // it looks like "#ABC123" + if (*p2) { + lc = 0; + } + gv.bgc = _gethexcolor(lc); + gv.rgb[0] = (float) U_RGBAGetR(gv.bgc) / 255.0; + gv.rgb[1] = (float) U_RGBAGetG(gv.bgc) / 255.0; + gv.rgb[2] = (float) U_RGBAGetB(gv.bgc) / 255.0; + } + + bool pageBoundingBox; + pageBoundingBox = mod->get_param_bool("pageBoundingBox"); + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(0, 0, _width, _height); + } else { + SPItem *doc_item = doc->getRoot(); + Geom::OptRect bbox = doc_item->desktopVisualBounds(); + if (bbox) { + d = *bbox; + } + } + + d *= Geom::Scale(Inkscape::Util::Quantity::convert(1, "px", "in")); // 90 dpi inside inkscape, wmf file will be 1200 dpi + + /* -1/1200 in next two lines so that WMF read in will write out again at exactly the same size */ + float dwInchesX = d.width() - 1.0 / 1200.0; + float dwInchesY = d.height() - 1.0 / 1200.0; + int dwPxX = round(dwInchesX * 1200.0); + int dwPxY = round(dwInchesY * 1200.0); +#if 0 + float dwInchesX = d.width(); + float dwInchesY = d.height(); + int dwPxX = round(d.width() * 1200.0); + int dwPxY = round(d.height() * 1200.0); +#endif + + U_PAIRF *ps = U_PAIRF_set(dwInchesX, dwInchesY); + rec = U_WMRHEADER_set(ps, 1200); // Example: drawing is A4 horizontal, 1200 dpi + free(ps); + if (!rec) { + g_warning("Failed in PrintWmf::begin at WMRHEADER"); + return -1; + } + (void) wmf_header_append((U_METARECORD *)rec, wt, 1); + + rec = U_WMRSETWINDOWEXT_set(point16_set(dwPxX, dwPxY)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETWINDOWEXT"); + return -1; + } + + rec = U_WMRSETWINDOWORG_set(point16_set(0, 0)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETWINDOWORG"); + return -1; + } + + rec = U_WMRSETMAPMODE_set(U_MM_ANISOTROPIC); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETMAPMODE"); + return -1; + } + + /* set some parameters, else the program that reads the WMF may default to other values */ + + rec = U_WMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETBKMODE"); + return -1; + } + + hpolyfillmode = U_WINDING; + rec = U_WMRSETPOLYFILLMODE_set(U_WINDING); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETPOLYFILLMODE"); + return -1; + } + + // Text alignment: (only changed if RTL text is encountered ) + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the WMF text alignment must always be TA_BASELINE|TA_LEFT. + rec = U_WMRSETTEXTALIGN_set(U_TA_BASELINE | U_TA_LEFT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETTEXTALIGN_set"); + return -1; + } + + htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + rec = U_WMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETTEXTCOLOR_set"); + return -1; + } + + rec = U_WMRSETROP2_set(U_R2_COPYPEN); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETROP2"); + return -1; + } + + hmiterlimit = 5; + rec = wmiterlimit_set(5); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wmiterlimit_set"); + return -1; + } + + + // create a pen as object 0. We never use it (except by mistake). Its purpose it to make all of the other object indices >=1 + U_PEN up = U_PEN_set(U_PS_SOLID, 1, colorref_set(0, 0, 0)); + uint32_t Pen; + rec = wcreatepenindirect_set(&Pen, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatepenindirect_set"); + return -1; + } + + // create a null pen. If no specific pen is set, this is used + up = U_PEN_set(U_PS_NULL, 1, colorref_set(0, 0, 0)); + rec = wcreatepenindirect_set(&hpen_null, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatepenindirect_set"); + return -1; + } + destroy_pen(); // make this pen active + + // create a null brush. If no specific brush is set, this is used + U_WLOGBRUSH lb = U_WLOGBRUSH_set(U_BS_NULL, U_RGB(0, 0, 0), U_HS_HORIZONTAL); + rec = wcreatebrushindirect_set(&hbrush_null, wht, lb); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatebrushindirect_set"); + return -1; + } + destroy_brush(); // make this brush active + + return 0; +} + + +unsigned int PrintWmf::finish(Inkscape::Extension::Print * /*mod*/) +{ + char *rec; + if (!wt) { + return 0; + } + + // get rid of null brush + rec = wdeleteobject_set(&hbrush_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set null brush"); + } + + // get rid of null pen + rec = wdeleteobject_set(&hpen_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set null pen"); + } + + // get rid of object 0, which was a pen that was used to shift the other object indices to >=1. + hpen = 0; + rec = wdeleteobject_set(&hpen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set filler object"); + } + + rec = U_WMREOF_set(); // generate the EOF record + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish"); + } + (void) wmf_finish(wt); // Finalize and write out the WMF + uwmf_free(&wt); // clean up + wmf_htable_free(&wht); // clean up + + return 0; +} + + +unsigned int PrintWmf::comment(Inkscape::Extension::Print * /*module*/, const char * /*comment*/) +{ + if (!wt) { + return 0; + } + + // earlier versions had flush of fill here, but it never executed and was removed + + return 0; +} + + +// fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. +int PrintWmf::create_brush(SPStyle const *style, U_COLORREF *fcolor) +{ + float rgb[3]; + char *rec; + U_WLOGBRUSH lb; + uint32_t brush, fmode; + MFDrawMode fill_mode; + Inkscape::Pixbuf *pixbuf; + uint32_t brushStyle; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width = 0; // quiets a harmless compiler warning, initialization not otherwise required. + uint32_t height = 0; + + if (!wt) { + return 0; + } + + // set a default fill in case we can't figure out a better way to do it + fmode = U_ALTERNATE; + fill_mode = DRAW_PAINT; + brushStyle = U_BS_SOLID; + hatchType = U_HS_SOLIDCLR; + bkColor = U_RGB(0, 0, 0); + if (fcolor) { + hatchColor = *fcolor; + } else { + hatchColor = U_RGB(0, 0, 0); + } + + if (!fcolor && style) { + if (style->fill.isColor()) { + fill_mode = DRAW_PAINT; + /* Dead assignment: Value stored to 'opacity' is never read + float opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (opacity <= 0.0) { + opacity = 0.0; // basically the same as no fill + } + */ + style->fill.value.color.get_rgb_floatv(rgb); + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); + } else if (SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + fill_mode = DRAW_IMAGE; + } else { // pattern + fill_mode = DRAW_PATTERN; + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + fill_mode = DRAW_PATTERN; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + brushStyle = U_BS_HATCHED; + } else if (SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below just sets the color to the average of the stops + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPLinearGradient *lg = nullptr; + SPRadialGradient *rg = nullptr; + + if (SP_IS_LINEARGRADIENT(paintserver)) { + lg = SP_LINEARGRADIENT(paintserver); + SP_GRADIENT(lg)->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_LINEAR_GRADIENT; + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + rg = SP_RADIALGRADIENT(paintserver); + SP_GRADIENT(rg)->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_RADIAL_GRADIENT; + } else { + // default fill + } + + if (rg) { + if (FixPPTGrad2Polys) { + return hold_gradient(rg, fill_mode); + } else { + hatchColor = avg_stop_color(rg); + } + } else if (lg) { + if (FixPPTGrad2Polys) { + return hold_gradient(lg, fill_mode); + } else { + hatchColor = avg_stop_color(lg); + } + } + } + } else { // if (!style) + // default fill + } + + switch (fill_mode) { + case DRAW_LINEAR_GRADIENT: // fill with average color unless gradients are converted to slices + case DRAW_RADIAL_GRADIENT: // ditto + case DRAW_PAINT: + case DRAW_PATTERN: + // SVG text has no background attribute, so OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + if (usebk) { + rec = U_WMRSETBKCOLOR_set(bkColor); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETBKCOLOR_set"); + } + rec = U_WMRSETBKMODE_set(U_OPAQUE); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETBKMODE_set"); + } + } + lb = U_WLOGBRUSH_set(brushStyle, hatchColor, hatchType); + rec = wcreatebrushindirect_set(&brush, wht, lb); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at createbrushindirect_set"); + } + break; + case DRAW_IMAGE: + char *px; + char *rgba_px; + uint32_t cbPx; + uint32_t colortype; + U_RGBQUAD *ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + U_BITMAPINFO *Bmi; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + rec = wcreatedibpatternbrush_srcdib_set(&brush, wht, U_DIB_RGB_COLORS, Bmi, cbPx, px); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at createdibpatternbrushpt_set"); + } + free(px); + free(Bmi); // ct will be NULL because of colortype + break; + } + + hbrush = brush; // need this later for destroy_brush + rec = wselectobject_set(brush, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at wselectobject_set"); + } + + if (fmode != hpolyfillmode) { + hpolyfillmode = fmode; + rec = U_WMRSETPOLYFILLMODE_set(fmode); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETPOLYFILLMODE_set"); + } + } + + return 0; +} + + +void PrintWmf::destroy_brush() +{ + char *rec; + // WMF lets any object be deleted whenever, and the chips fall where they may... + if (hbrush) { + rec = wdeleteobject_set(&hbrush, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_brush"); + } + hbrush = 0; + } + + // (re)select the null brush + + rec = wselectobject_set(hbrush_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_brush"); + } +} + + +int PrintWmf::create_pen(SPStyle const *style, const Geom::Affine &transform) +{ + char *rec = nullptr; + uint32_t pen; + uint32_t penstyle; + U_COLORREF penColor; + U_PEN up; + int modstyle; + + if (!wt) { + return 0; + } + + // set a default stroke in case we can't figure out a better way to do it + penstyle = U_PS_SOLID; + modstyle = 0; + penColor = U_RGB(0, 0, 0); + uint32_t linewidth = 1; + + if (style) { // override some or all of the preceding + float rgb[3]; + + // WMF does not support hatched, bitmap, or gradient pens, just set the color. + style->stroke.value.color.get_rgb_floatv(rgb); + penColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + using Geom::X; + using Geom::Y; + + Geom::Point zero(0, 0); + Geom::Point one(1, 1); + Geom::Point p0(zero * transform); + Geom::Point p1(one * transform); + Geom::Point p(p1 - p0); + + double scale = sqrt((p[X] * p[X]) + (p[Y] * p[Y])) / sqrt(2); + + if (!style->stroke_width.computed) { + return 0; //if width is 0 do not (reset) the pen, it should already be NULL_PEN + } + linewidth = MAX(1, (uint32_t) round(scale * style->stroke_width.computed * PX2WORLD)); + + // most WMF readers will ignore linecap and linejoin, but set them anyway. Inkscape itself can read them back in. + + if (style->stroke_linecap.computed == 0) { + modstyle |= U_PS_ENDCAP_FLAT; + } else if (style->stroke_linecap.computed == 1) { + modstyle |= U_PS_ENDCAP_ROUND; + } else { + modstyle |= U_PS_ENDCAP_SQUARE; + } + + if (style->stroke_linejoin.computed == 0) { + float miterlimit = style->stroke_miterlimit.value; // This is a ratio. + if (miterlimit < 1) { + miterlimit = 1; + } + + // most WMF readers will ignore miterlimit, but set it anyway. Inkscape itself can read it back in + if ((uint32_t)miterlimit != hmiterlimit) { + hmiterlimit = (uint32_t)miterlimit; + rec = wmiterlimit_set((uint32_t) miterlimit); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wmiterlimit_set"); + } + } + modstyle |= U_PS_JOIN_MITER; + } else if (style->stroke_linejoin.computed == 1) { + modstyle |= U_PS_JOIN_ROUND; + } else { + modstyle |= U_PS_JOIN_BEVEL; + } + + if (!style->stroke_dasharray.values.empty()) { + if (!FixPPTDashLine) { // if this is set code elsewhere will break dots/dashes into many smaller lines. + int n_dash = style->stroke_dasharray.values.size(); + /* options are dash, dot, dashdot and dashdotdot. Try to pick the closest one. */ + int mark_short=INT_MAX; + int mark_long =0; + int i; + for (i=0;istroke_dasharray.values[i].value; + if (mark > mark_long) { + mark_long = mark; + } + if (mark < mark_short) { + mark_short = mark; + } + } + if(mark_long == mark_short){ // only one mark size + penstyle = U_PS_DOT; + } + else if (n_dash==2) { + penstyle = U_PS_DASH; + } + else if (n_dash==4) { + penstyle = U_PS_DASHDOT; + } + else { + penstyle = U_PS_DASHDOTDOT; + } + } + } + + } + + up = U_PEN_set(penstyle | modstyle, linewidth, penColor); + rec = wcreatepenindirect_set(&pen, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wcreatepenindirect_set"); + } + + rec = wselectobject_set(pen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wselectobject_set"); + } + hpen = pen; // need this later for destroy_pen + + return 0; +} + +// delete the defined pen object +void PrintWmf::destroy_pen() +{ + char *rec = nullptr; + // WMF lets any object be deleted whenever, and the chips fall where they may... + if (hpen) { + rec = wdeleteobject_set(&hpen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_pen"); + } + hpen = 0; + } + + // (re)select the null pen + + rec = wselectobject_set(hpen_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_pen"); + } +} + + +unsigned int PrintWmf::fill( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const & /*transform*/, SPStyle const *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + using Geom::X; + using Geom::Y; + + Geom::Affine tf = m_tr_stack.top(); + + use_fill = true; + use_stroke = false; + + fill_transform = tf; + + if (create_brush(style, nullptr)) { + /* + Handle gradients. Uses modified livarot as 2geom boolops is currently broken. + Can handle gradients with multiple stops. + + The overlap is needed to avoid antialiasing artifacts when edges are not strictly aligned on pixel boundaries. + There is an inevitable loss of accuracy saving through an WMF file because of the integer coordinate system. + Keep the overlap quite large so that loss of accuracy does not remove an overlap. + */ + destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below + Geom::Path cutter; + float rgb[3]; + U_COLORREF wc, c1, c2; + FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); + double doff, doff_base, doff_range; + double divisions = 128.0; + int nstops; + int istop = 1; + float opa; // opacity at stop + + SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here + nstops = tg->vector.stops.size(); + tg->vector.stops[0].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[0].opacity; + c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[nstops - 1].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + doff = 0.0; + doff_base = 0.0; + doff_range = tg->vector.stops[1].offset; // next or last stop + + if (gv.mode == DRAW_RADIAL_GRADIENT) { + Geom::Point xv = gv.p2 - gv.p1; // X' vector + Geom::Point yv = gv.p3 - gv.p1; // Y' vector + Geom::Point xuv = Geom::unit_vector(xv); // X' unit vector + double rx = hypot(xv[X], xv[Y]); + double ry = hypot(yv[X], yv[Y]); + double range = fmax(rx, ry); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* radial gradient might stop part way through the shape, fill with outer color from there to "infinity". + Do this first so that outer colored ring will overlay it. + */ + pathvc = center_elliptical_hole_as_SVG_PathV(gv.p1, rx * (1.0 - overlap / range), ry * (1.0 - overlap / range), asin(xuv[Y])); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_oddEven, frb); + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + + pathvc = center_elliptical_ring_as_SVG_PathV(gv.p1, rx * start / range, ry * start / range, rx * stop / range, ry * stop / range, asin(xuv[Y])); + + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range - doff_base) { + istop++; + if (istop >= nstops) { + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else if (gv.mode == DRAW_LINEAR_GRADIENT) { + Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector + Geom::Point puv = uv.cw(); // perp. to unit vector + double range = Geom::distance(gv.p1, gv.p2); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* before lower end of gradient, overlap first slice position */ + wc = weight_opacity(c1); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + /* after high end of gradient, overlap last slice position */ + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range - doff_base) { + istop++; + if (istop >= nstops) { + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else { + g_error("Fatal programming error in PrintWmf::fill, invalid gradient type detected"); + } + use_fill = false; // gradients handled, be sure stroke does not use stroke and fill + } else { + /* + Inkscape was not calling create_pen for objects with no border. + This was because it never called stroke() (next method). + PPT, and presumably others, pick whatever they want for the border if it is not specified, so no border can + become a visible border. + To avoid this force the pen to NULL_PEN if we can determine that no pen will be needed after the fill. + */ + if (style->stroke.noneSet || style->stroke_width.computed == 0.0) { + destroy_pen(); //this sets the NULL_PEN + } + + /* postpone fill in case stroke also required AND all stroke paths closed + Dashes converted to line segments will "open" a closed path. + */ + bool all_closed = true; + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (pit.end_default() != pit.end_closed()) { + all_closed = false; + } + } + } + if ( + (style->stroke.isNone() || style->stroke.noneSet || style->stroke_width.computed == 0.0) || + (!style->stroke_dasharray.values.empty() && FixPPTDashLine) || + !all_closed + ) { + print_pathv(pathv, fill_transform); // do any fills. side effect: clears fill_pathv + use_fill = false; + } + } + + return 0; +} + + +unsigned int PrintWmf::stroke( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, const Geom::Affine &/*transform*/, const SPStyle *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + use_stroke = true; + // use_fill was set in ::fill, if it is needed, if not, the null brush is used, it should be already set + + if (create_pen(style, tf)) { + return 0; + } + + if (!style->stroke_dasharray.values.empty() && FixPPTDashLine) { + // convert the path, gets its complete length, and then make a new path with parameter length instead of t + Geom::Piecewise > tmp_pathpw; // pathv-> sbasis + Geom::Piecewise > tmp_pathpw2; // sbasis using arc length parameter + Geom::Piecewise > tmp_pathpw3; // new (discontinuous) path, composed of dots/dashes + Geom::Piecewise > first_frag; // first fragment, will be appended at end + int n_dash = style->stroke_dasharray.values.size(); + int i = 0; //dash index + double tlength; // length of tmp_pathpw + double slength = 0.0; // start of gragment + double elength; // end of gragment + for (const auto & i : pathv) { + tmp_pathpw.concat(i.toPwSb()); + } + tlength = length(tmp_pathpw, 0.1); + tmp_pathpw2 = arc_length_parametrization(tmp_pathpw); + + // go around the dash array repeatedly until the entire path is consumed (but not beyond). + while (slength < tlength) { + elength = slength + style->stroke_dasharray.values[i++].value; + if (elength > tlength) { + elength = tlength; + } + Geom::Piecewise > fragment(portion(tmp_pathpw2, slength, elength)); + if (slength) { + tmp_pathpw3.concat(fragment); + } else { + first_frag = fragment; + } + slength = elength; + slength += style->stroke_dasharray.values[i++].value; // the gap + if (i >= n_dash) { + i = 0; + } + } + tmp_pathpw3.concat(first_frag); // may merge line around start point + Geom::PathVector out_pathv = Geom::path_from_piecewise(tmp_pathpw3, 0.01); + print_pathv(out_pathv, tf); + } else { + print_pathv(pathv, tf); + } + + use_stroke = false; + use_fill = false; + + if (usebk) { // OPAQUE was set, revert to TRANSPARENT + usebk = false; + rec = U_WMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::stroke at U_WMRSETBKMODE_set"); + } + } + + return 0; +} + + +// Draws simple_shapes, those with closed WMR_* primitives, like polygons, rectangles and ellipses. +// These use whatever the current pen/brush are and need not be followed by a FILLPATH or STROKEPATH. +// For other paths it sets a few flags and returns. +bool PrintWmf::print_simple_shape(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + + Geom::PathVector pv = pathv_to_linear(pathv * transform, MAXDISP); + + int nodes = 0; + int moves = 0; + int lines = 0; + int curves = 0; + char *rec = nullptr; + + for (const auto & pit : pv) { + moves++; + nodes++; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + nodes++; + + if (is_straight_curve(*cit)) { + lines++; + } else if (dynamic_cast(&*cit)) { + curves++; + } + } + } + + if (!nodes) { + return false; + } + + U_POINT16 *lpPoints = new U_POINT16[moves + lines + curves * 3]; + int i = 0; + + /** For all Subpaths in the */ + + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + int32_t const x0 = (int32_t) round(p0[X]); + int32_t const y0 = (int32_t) round(p0[Y]); + + lpPoints[i].x = x0; + lpPoints[i].y = y0; + i = i + 1; + + /** For all segments in the subpath */ + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + i = i + 1; + } else if (Geom::CubicBezier const *cubic = dynamic_cast(&*cit)) { + std::vector points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + lpPoints[i + 1].x = x2; + lpPoints[i + 1].y = y2; + lpPoints[i + 2].x = x3; + lpPoints[i + 2].y = y3; + i = i + 3; + } + } + } + + bool done = false; + bool closed = (lpPoints[0].x == lpPoints[i - 1].x) && (lpPoints[0].y == lpPoints[i - 1].y); + bool polygon = false; + bool rectangle = false; + bool ellipse = false; + + if (moves == 1 && moves + lines == nodes && closed) { + polygon = true; + // if (nodes==5) { // disable due to LP Bug 407394 + // if (lpPoints[0].x == lpPoints[3].x && lpPoints[1].x == lpPoints[2].x && + // lpPoints[0].y == lpPoints[1].y && lpPoints[2].y == lpPoints[3].y) + // { + // rectangle = true; + // } + // } + } else if (moves == 1 && nodes == 5 && moves + curves == nodes && closed) { + // if (lpPoints[0].x == lpPoints[1].x && lpPoints[1].x == lpPoints[11].x && + // lpPoints[5].x == lpPoints[6].x && lpPoints[6].x == lpPoints[7].x && + // lpPoints[2].x == lpPoints[10].x && lpPoints[3].x == lpPoints[9].x && lpPoints[4].x == lpPoints[8].x && + // lpPoints[2].y == lpPoints[3].y && lpPoints[3].y == lpPoints[4].y && + // lpPoints[8].y == lpPoints[9].y && lpPoints[9].y == lpPoints[10].y && + // lpPoints[5].y == lpPoints[1].y && lpPoints[6].y == lpPoints[0].y && lpPoints[7].y == lpPoints[11].y) + // { // disable due to LP Bug 407394 + // ellipse = true; + // } + } + + if (polygon || ellipse) { + // pens and brushes already set by caller, do not touch them + + if (polygon) { + if (rectangle) { + U_RECT16 rcl = U_RECT16_set((U_POINT16) { + lpPoints[0].x, lpPoints[0].y + }, (U_POINT16) { + lpPoints[2].x, lpPoints[2].y + }); + rec = U_WMRRECTANGLE_set(rcl); + } else { + rec = U_WMRPOLYGON_set(nodes, lpPoints); + } + } else if (ellipse) { + U_RECT16 rcl = U_RECT16_set((U_POINT16) { + lpPoints[6].x, lpPoints[3].y + }, (U_POINT16) { + lpPoints[0].x, lpPoints[9].y + }); + rec = U_WMRELLIPSE_set(rcl); + } + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_simple_shape at retangle/ellipse/polygon"); + } + + done = true; + + } + + delete[] lpPoints; + + return done; +} + +/** Some parts based on win32.cpp by Lauris Kaplinski . Was a part of Inkscape + in the past (or will be in the future?) Not in current trunk. (4/19/2012) + + Limitations of this code: + 1. Images lose their rotation, one corner stays in the same place. + 2. Transparency is lost on export. (A limitation of the WMF format.) + 3. Probably messes up if row stride != w*4 + 4. There is still a small memory leak somewhere, possibly in a pixbuf created in a routine + that calls this one and passes px, but never removes the rest of the pixbuf. The first time + this is called it leaked 5M (in one test) and each subsequent call leaked around 200K more. + If this routine is reduced to + if(1)return(0); + and called for a single 1280 x 1024 image then the program leaks 11M per call, or roughly the + size of two bitmaps. +*/ + +unsigned int PrintWmf::image( + Inkscape::Extension::Print * /* module */, /** not used */ + unsigned char *rgba_px, /** array of pixel values, Gdk::Pixbuf bitmap format */ + unsigned int w, /** width of bitmap */ + unsigned int h, /** height of bitmap */ + unsigned int rs, /** row stride (normally w*4) */ + Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other transforms, use the one from m_tr_stack */ + SPStyle const * /*style*/) /** provides indirect link to image object */ +{ + double x1, y1, dw, dh; + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + rec = U_WMRSETSTRETCHBLTMODE_set(U_COLORONCOLOR); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::image at EMRHEADER"); + } + + x1 = tf_rect[4]; + y1 = tf_rect[5]; + dw = ((double) w) * tf_rect[0]; + dh = ((double) h) * tf_rect[3]; + Geom::Point pLL(x1, y1); + Geom::Point pLL2 = pLL * tf; //location of LL corner in Inkscape coordinates + + /* adjust scale of w and h. This works properly when there is no rotation. The values are + a bit strange when there is rotation, but since WMF cannot handle rotation in any case, all + answers are equally wrong. + */ + Geom::Point pWH(dw, dh); + Geom::Point pWH2 = pWH * tf.withoutTranslation(); + + char *px; + uint32_t cbPx; + uint32_t colortype; + U_RGBQUAD *ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + U_BITMAPINFO *Bmi; + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, (char *) rgba_px, w, h, w * 4, colortype, 0, 1); + Bmih = bitmapinfoheader_set(w, h, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + + U_POINT16 Dest = point16_set(round(pLL2[Geom::X] * PX2WORLD), round(pLL2[Geom::Y] * PX2WORLD)); + U_POINT16 cDest = point16_set(round(pWH2[Geom::X] * PX2WORLD), round(pWH2[Geom::Y] * PX2WORLD)); + U_POINT16 Src = point16_set(0, 0); + U_POINT16 cSrc = point16_set(w, h); + rec = U_WMRSTRETCHDIB_set( + Dest, //! Destination UL corner in logical units + cDest, //! Destination W & H in logical units + Src, //! Source UL corner in logical units + cSrc, //! Source W & H in logical units + U_DIB_RGB_COLORS, //! DIBColors Enumeration + U_SRCCOPY, //! RasterOPeration Enumeration + Bmi, //! (Optional) bitmapbuffer (U_BITMAPINFO section) + h * rs, //! size in bytes of px + px //! (Optional) bitmapbuffer (U_BITMAPINFO section) + ); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::image at U_WMRSTRETCHDIB_set"); + } + free(px); + free(Bmi); + if (numCt) { + free(ct); + } + return 0; +} + +// may also be called with a simple_shape or an empty path, whereupon it just returns without doing anything +unsigned int PrintWmf::print_pathv(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + char *rec = nullptr; + U_POINT16 *pt16hold, *pt16ptr; + uint16_t *n16hold; + uint16_t *n16ptr; + + simple_shape = print_simple_shape(pathv, transform); + if (!simple_shape && !pathv.empty()) { + // WMF does not have beziers, need to convert to ONLY linears with something like this: + Geom::PathVector pv = pathv_to_linear(pathv * transform, MAXDISP); + + /** For all Subpaths in the */ + + /* If the path consists entirely of closed subpaths use one polypolygon. + Otherwise use a mix of polygon or polyline separately on each path. + If the polyline turns out to be single line segments, use a series of MOVETO/LINETO instead, + because WMF has no POLYPOLYLINE. + The former allows path delimited donuts and the like, which + cannot be represented in WMF with polygon or polyline because there is no external way to combine paths + as there is in EMF or SVG. + For polygons specify the last point the same as the first. The WMF/EMF manuals say that the + reading program SHOULD close the path, which allows a conforming program not to, potentially rendering + a closed path as an open one. */ + int nPolys = 0; + int totPoints = 0; + for (const auto & pit : pv) { + totPoints += 1 + pit.size_default(); // big array, will hold all points, for all polygons. Size_default ignores first point in each path. + if (pit.end_default() == pit.end_closed()) { + nPolys++; + } else { + nPolys = 0; + break; + } + } + + if (nPolys > 1) { // a single polypolygon, a single polygon falls through to the else + pt16hold = pt16ptr = (U_POINT16 *) malloc(totPoints * sizeof(U_POINT16)); + if (!pt16ptr) { + return(false); + } + + n16hold = n16ptr = (uint16_t *) malloc(nPolys * sizeof(uint16_t)); + if (!n16ptr) { + free(pt16hold); + return(false); + } + + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + + *n16ptr++ = pit.size_default(); // points in the subpath + + /** For each segment in the subpath */ + + Geom::Point p1 = pit.initialPoint(); // This point is special, it isn't in the iterator + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + Geom::Point p1 = cit->finalPoint(); + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + } + + } + rec = U_WMRPOLYPOLYGON_set(nPolys, n16hold, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRPOLYPOLYGON_set"); + } + free(pt16hold); + free(n16hold); + } else { // one or more polyline or polygons (but not all polygons, that would be the preceding case) + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + /* Malformatted Polylines with a sequence like M L M M L have been seen, the 2nd M does nothing + and that point must not go into the output. */ + if (!(pit.size_default())) { + continue; + } + /* Figure out how many points there are, make an array big enough to hold them, and store + all the points. This is the same for open or closed path. This gives the upper bound for + the number of points. The actual number used is calculated on the fly. + */ + int nPoints = 1 + pit.size_default(); + + pt16hold = pt16ptr = (U_POINT16 *) malloc(nPoints * sizeof(U_POINT16)); + if (!pt16ptr) { + break; + } + + /** For each segment in the subpath */ + + Geom::Point p1 = pit.initialPoint(); // This point is special, it isn't in the iterator + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + nPoints = 1; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit, nPoints++) { + Geom::Point p1 = cit->finalPoint(); + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + } + + if (pit.end_default() == pit.end_closed()) { + rec = U_WMRPOLYGON_set(nPoints, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRPOLYGON_set"); + } + } else if (nPoints > 2) { + rec = U_WMRPOLYLINE_set(nPoints, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_POLYLINE_set"); + } + } else if (nPoints == 2) { + rec = U_WMRMOVETO_set(pt16hold[0]); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRMOVETO_set"); + } + rec = U_WMRLINETO_set(pt16hold[1]); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRLINETO_set"); + } + } + free(pt16hold); + } + } + } + + // WMF has no fill or stroke commands, the draw does it with active pen/brush + + // clean out brush and pen, but only after all parts of the draw complete + if (use_fill) { + destroy_brush(); + } + if (use_stroke) { + destroy_pen(); + } + + return TRUE; +} + + +unsigned int PrintWmf::text(Inkscape::Extension::Print * /*mod*/, char const *text, Geom::Point const &p, + SPStyle const *const style) +{ + if (!wt || !text) { + return 0; + } + + char *rec = nullptr; + int ccount, newfont; + int fix90n = 0; + uint32_t hfont = 0; + Geom::Affine tf = m_tr_stack.top(); + double rot = -1800.0 * std::atan2(tf[1], tf[0]) / M_PI; // 0.1 degree rotation, - sign for MM_TEXT + double rotb = -std::atan2(tf[1], tf[0]); // rotation for baseline offset for superscript/subscript, used below + double dx, dy; + double ky; + + // the dx array is smuggled in like: textw1 w2 w3 ...wn, where the widths are floats 7 characters wide, including the space + int ndx = 0; + int rtl = 0; + int16_t *adx; + smuggle_adxky_out(text, &adx, &ky, &rtl, &ndx, PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); // side effect: free() adx + + uint32_t textalignment; + if (rtl > 0) { + textalignment = U_TA_BASELINE | U_TA_LEFT; + } else { + textalignment = U_TA_BASELINE | U_TA_RIGHT | U_TA_RTLREADING; + } + if (textalignment != htextalignment) { + htextalignment = textalignment; + rec = U_WMRSETTEXTALIGN_set(textalignment); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMRSETTEXTALIGN_set"); + } + } + + char *text2 = strdup(text); // because U_Utf8ToUtf16le calls iconv which does not like a const char * + uint16_t *unicode_text = U_Utf8ToUtf16le(text2, 0, nullptr); + free(text2); + //translates Unicode as Utf16le to NonUnicode, if possible. If any translate, all will, and all to + //the same font, because of code in Layout::print + UnicodeToNon(unicode_text, &ccount, &newfont); + // The preceding hopefully handled conversions to symbol, wingdings or zapf dingbats. Now slam everything + // else down into latin1, which is all WMF can handle. If the language isn't English expect terrible results. + char *latin1_text = U_Utf16leToLatin1(unicode_text, 0, nullptr); + free(unicode_text); + + // in some cases a UTF string may reduce to NO latin1 characters, which returns NULL + if(!latin1_text){free(adx); return 0; } + + //PPT gets funky with text within +-1 degree of a multiple of 90, but only for SOME fonts.Snap those to the central value + //Some funky ones: Arial, Times New Roman + //Some not funky ones: Symbol and Verdana. + //Without a huge table we cannot catch them all, so just the most common problem ones. + FontfixParams params; + + if (FixPPTCharPos) { + switch (newfont) { + case CVTSYM: + _lookup_ppt_fontfix("Convert To Symbol", params); + break; + case CVTZDG: + _lookup_ppt_fontfix("Convert To Zapf Dingbats", params); + break; + case CVTWDG: + _lookup_ppt_fontfix("Convert To Wingdings", params); + break; + default: //also CVTNON + _lookup_ppt_fontfix(style->font_family.value(), params); + break; + } + if (params.f2 != 0 || params.f3 != 0) { + int irem = ((int) round(rot)) % 900 ; + if (irem <= 9 && irem >= -9) { + fix90n = 1; //assume vertical + rot = (double)(((int) round(rot)) - irem); + rotb = rot * M_PI / 1800.0; + if (std::abs(rot) == 900.0) { + fix90n = 2; + } + } + } + } + + /* + Note that text font sizes are stored into the WMF as fairly small integers and that limits their precision. + The WMF output files produced here have been designed so that the integer valued pt sizes + land right on an integer value in the WMF file, so those are exact. However, something like 18.1 pt will be + somewhat off, so that when it is read back in it becomes 18.11 pt. (For instance.) + */ + int textheight = round(-style->font_size.computed * PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); + if (!hfont) { + + // Get font face name. Use changed font name if unicode mapped to one + // of the special fonts. + char *facename; + if (!newfont) { + facename = U_Utf8ToLatin1(style->font_family.value(), 0, nullptr); + } else { + facename = U_Utf8ToLatin1(FontName(newfont), 0, nullptr); + } + + // Scale the text to the minimum stretch. (It tends to stay within bounding rectangles even if + // it was streteched asymmetrically.) Few applications support text from WMF which is scaled + // differently by height/width, so leave lfWidth alone. + + U_FONT *puf = U_FONT_set( + textheight, + 0, + round(rot), + round(rot), + _translate_weight(style->font_weight.computed), + (style->font_style.computed == SP_CSS_FONT_STYLE_ITALIC), + style->text_decoration_line.underline, + style->text_decoration_line.line_through, + U_DEFAULT_CHARSET, + U_OUT_DEFAULT_PRECIS, + U_CLIP_DEFAULT_PRECIS, + U_DEFAULT_QUALITY, + U_DEFAULT_PITCH | U_FF_DONTCARE, + facename); + free(facename); + + rec = wcreatefontindirect_set(&hfont, wht, puf); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wcreatefontindirect_set"); + } + free(puf); + } + + rec = wselectobject_set(hfont, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wselectobject_set"); + } + + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + // only change the text color when it needs to be changed + if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { + memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); + rec = U_WMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMRSETTEXTCOLOR_set"); + } + } + + + // Text alignment: + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the WMF text alignment must always be TA_BASELINE|TA_LEFT. + // this is set at the beginning of the file and never changed + + // Transparent text background, never changes, set at the beginning of the file + + Geom::Point p2 = p * tf; + + //Handle super/subscripts and vertical kerning + /* Previously used this, but vertical kerning was not supported + p2[Geom::X] -= style->baseline_shift.computed * std::sin( rotb ); + p2[Geom::Y] -= style->baseline_shift.computed * std::cos( rotb ); + */ + p2[Geom::X] += ky * std::sin(rotb); + p2[Geom::Y] += ky * std::cos(rotb); + + //Conditionally handle compensation for PPT WMF import bug (affects PPT 2003-2010, at least) + if (FixPPTCharPos) { + if (fix90n == 1) { //vertical + dx = 0.0; + dy = params.f3 * style->font_size.computed * std::cos(rotb); + } else if (fix90n == 2) { //horizontal + dx = params.f2 * style->font_size.computed * std::sin(rotb); + dy = 0.0; + } else { + dx = params.f1 * style->font_size.computed * std::sin(rotb); + dy = params.f1 * style->font_size.computed * std::cos(rotb); + } + p2[Geom::X] += dx; + p2[Geom::Y] += dy; + } + + p2[Geom::X] = (p2[Geom::X] * PX2WORLD); + p2[Geom::Y] = (p2[Geom::Y] * PX2WORLD); + + int32_t const xpos = (int32_t) round(p2[Geom::X]); + int32_t const ypos = (int32_t) round(p2[Geom::Y]); + + // The number of characters in the string is a bit fuzzy. ndx, the number of entries in adx is + // the number of VISIBLE characters, since some may combine from the UTF (8 originally, + // now 16) encoding. Conversely strlen() or wchar16len() would give the absolute number of + // encoding characters. Unclear if emrtext wants the former or the latter but for now assume the former. + + // This is currently being smuggled in from caller as part of text, works + // MUCH better than the fallback hack below + // uint32_t *adx = dx_set(textheight, U_FW_NORMAL, slen); // dx is needed, this makes one up + if (rtl > 0) { + rec = U_WMREXTTEXTOUT_set((U_POINT16) { + (int16_t) xpos, (int16_t) ypos + }, + ndx, U_ETO_NONE, latin1_text, adx, U_RCL16_DEF); + } else { // RTL text, U_TA_RTLREADING should be enough, but set this one too just in case + rec = U_WMREXTTEXTOUT_set((U_POINT16) { + (int16_t) xpos, (int16_t) ypos + }, + ndx, U_ETO_RTLREADING, latin1_text, adx, U_RCL16_DEF); + } + free(latin1_text); + free(adx); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMREXTTEXTOUTW_set"); + } + + rec = wdeleteobject_set(&hfont, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wdeleteobject_set"); + } + + return 0; +} + +void PrintWmf::init() +{ + /* WMF print */ + Inkscape::Extension::build_from_mem( + "\n" + "Windows Metafile Print\n" + "org.inkscape.print.wmf\n" + "\n" + "true\n" + "true\n" + "false\n" + "false\n" + "false\n" + "false\n" + "\n" + "", new PrintWmf()); + + return; +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-print.h b/src/extension/internal/wmf-print.h new file mode 100644 index 0000000..92f5577 --- /dev/null +++ b/src/extension/internal/wmf-print.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile printing - implementation + */ +/* Author: + * Ulf Erikson + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_WMF_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_WMF_PRINT_H + +#include <3rdparty/libuemf/uwmf.h> +#include "extension/internal/metafile-print.h" + +#include "splivarot.h" // pieces for union on shapes +#include "display/canvas-bpath.h" // for SPWindRule + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintWmf : public PrintMetafile +{ + uint32_t hbrush, hpen, hbrush_null, hpen_null; + uint32_t hmiterlimit; // used to minimize redundant records that set this + + unsigned int print_pathv (Geom::PathVector const &pathv, const Geom::Affine &transform); + bool print_simple_shape (Geom::PathVector const &pathv, const Geom::Affine &transform); + +public: + PrintWmf(); + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int fill (Inkscape::Extension::Print *module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print * module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int image(Inkscape::Extension::Print *module, + unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style) override; + unsigned int comment(Inkscape::Extension::Print *module, const char * comment) override; + unsigned int text(Inkscape::Extension::Print *module, char const *text, + Geom::Point const &p, SPStyle const *style) override; + + static void init (); +protected: + static void smuggle_adxky_out(const char *string, int16_t **adx, double *ky, int *rtl, int *ndx, float scale); + + int create_brush(SPStyle const *style, PU_COLORREF fcolor) override; + void destroy_brush() override; + int create_pen(SPStyle const *style, const Geom::Affine &transform) override; + void destroy_pen() override; +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_WMF_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wpg-input.cpp b/src/extension/internal/wpg-input.cpp new file mode 100644 index 0000000..2041cd2 --- /dev/null +++ b/src/extension/internal/wpg-input.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Ted Gould + * Abhishek Sharma + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +/* libwpg + * Copyright (C) 2006 Ariya Hidayat (ariya@kde.org) + * Copyright (C) 2005 Fridrich Strba (fridrich.strba@bluewin.ch) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For further information visit http://libwpg.sourceforge.net + */ + +/* "This product is not manufactured, approved, or supported by + * Corel Corporation or Corel Corporation Limited." + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include + +#ifdef WITH_LIBWPG + +#include "wpg-input.h" +#include "extension/system.h" +#include "extension/input.h" +#include "document.h" +#include "object/sp-root.h" +#include "util/units.h" +#include + +// Take a guess and fallback to 0.2.x if no configure has run +#if !defined(WITH_LIBWPG03) && !defined(WITH_LIBWPG02) +#define WITH_LIBWPG02 1 +#endif + +#include "libwpg/libwpg.h" +#if WITH_LIBWPG03 + #include + + using librevenge::RVNGString; + using librevenge::RVNGFileStream; + using librevenge::RVNGInputStream; +#else + #include "libwpd-stream/libwpd-stream.h" + + typedef WPXString RVNGString; + typedef WPXFileStream RVNGFileStream; + typedef WPXInputStream RVNGInputStream; +#endif + +using namespace libwpg; + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +SPDocument *WpgInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGInputStream* input = new RVNGFileStream(converted_uri); + g_free(converted_uri); + #else + RVNGInputStream* input = new RVNGFileStream(uri); + #endif + +#if WITH_LIBWPG03 + if (input->isStructured()) { + RVNGInputStream* olestream = input->getSubStreamByName("PerfectOffice_MAIN"); +#else + if (input->isOLEStream()) { + RVNGInputStream* olestream = input->getDocumentOLEStream("PerfectOffice_MAIN"); +#endif + + if (olestream) { + delete input; + input = olestream; + } + } + + if (!WPGraphics::isSupported(input)) { + //! \todo Dialog here + // fprintf(stderr, "ERROR: Unsupported file format (unsupported version) or file is encrypted!\n"); + // printf("I'm giving up not supported\n"); + delete input; + return nullptr; + } + +#if WITH_LIBWPG03 + librevenge::RVNGStringVector vec; + librevenge::RVNGSVGDrawingGenerator generator(vec, ""); + + if (!libwpg::WPGraphics::parse(input, &generator) || vec.empty() || vec[0].empty()) { + delete input; + return nullptr; + } + + RVNGString output("\n\n"); + output.append(vec[0]); +#else + RVNGString output; + if (!libwpg::WPGraphics::generateSVG(input, output)) { + delete input; + return NULL; + } +#endif + + //printf("I've got a doc: \n%s", painter.document.c_str()); + + SPDocument * doc = SPDocument::createNewDocFromMem(output.cstr(), strlen(output.cstr()), TRUE); + + // Set viewBox if it doesn't exist + if (doc && !doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + delete input; + return doc; +} + +#include "clear-n_.h" + +void WpgInput::init() { + Inkscape::Extension::build_from_mem( + "\n" + "" N_("WPG Input") "\n" + "org.inkscape.input.wpg\n" + "\n" + ".wpg\n" + "image/x-wpg\n" + "" N_("WordPerfect Graphics (*.wpg)") "\n" + "" N_("Vector graphics format used by Corel WordPerfect") "\n" + "\n" + "", new WpgInput()); +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBWPG */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/wpg-input.h b/src/extension/internal/wpg-input.h new file mode 100644 index 0000000..67e4d91 --- /dev/null +++ b/src/extension/internal/wpg-input.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_WPGOUTPUT_H__ +#define __EXTENSION_INTERNAL_WPGOUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBWPG + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class WpgInput : public Inkscape::Extension::Implementation::Implementation { + WpgInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBWPG */ +#endif /* __EXTENSION_INTERNAL_WPGOUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/loader.cpp b/src/extension/loader.cpp new file mode 100644 index 0000000..d409d09 --- /dev/null +++ b/src/extension/loader.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Loader for external plug-ins. + * + * Authors: + * Moritz Eberl + * + * Copyright (C) 2016 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "loader.h" + +#include + +#include "system.h" +#include +#include "dependency.h" +#include "inkscape-version.h" + +namespace Inkscape { +namespace Extension { + +typedef Implementation::Implementation *(*_getImplementation)(); +typedef const gchar *(*_getInkscapeVersion)(); + +bool Loader::load_dependency(Dependency *dep) +{ + GModule *module = nullptr; + module = g_module_open(dep->get_name(), (GModuleFlags)0); + if (module == nullptr) { + return false; + } + return true; +} + +/** + * @brief Load the actual implementation of a plugin supplied by the plugin. + * @param doc The xml representation of the INX extension configuration. + * @return The implementation of the extension loaded from the plugin. + */ +Implementation::Implementation *Loader::load_implementation(Inkscape::XML::Document *doc) +{ + try { + + Inkscape::XML::Node *repr = doc->root(); + Inkscape::XML::Node *child_repr = repr->firstChild(); + + // Iterate over the xml content + while (child_repr != nullptr) { + char const *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + + // Deal with dependencies if we have them + if (!strcmp(chname, "dependency")) { + Dependency dep = Dependency(child_repr, nullptr); // TODO: Why is "this" not an extension? + // try to load it + bool success = load_dependency(&dep); + if( !success ){ + // Could not load dependency, we abort + const char *res = g_module_error(); + g_warning("Unable to load dependency %s of plugin %s.\nDetails: %s\n", dep.get_name(), "", res); + return nullptr; + } + } + + // Found a plugin to load + if (!strcmp(chname, "plugin")) { + + // The name of the plugin is actually the library file we want to load + if (const gchar *name = child_repr->attribute("name")) { + GModule *module = nullptr; + _getImplementation GetImplementation = nullptr; + _getInkscapeVersion GetInkscapeVersion = nullptr; + + // build the path where to look for the plugin + gchar *path = g_build_filename(_baseDirectory.c_str(), name, (char *) nullptr); + module = g_module_open(path, G_MODULE_BIND_LOCAL); + g_free(path); + + if (module == nullptr) { + // we were not able to load the plugin, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get a handle to the version function of the module + if (g_module_symbol(module, "GetInkscapeVersion", (gpointer *) &GetInkscapeVersion) == FALSE) { + // This didn't work, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get a handle to the function that delivers the implementation + if (g_module_symbol(module, "GetImplementation", (gpointer *) &GetImplementation) == FALSE) { + // This didn't work, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get version and test against this version + const gchar* version = GetInkscapeVersion(); + if( strcmp(version, version_string) != 0) { + // The versions are different, display warning. + g_warning("Plugin was built against Inkscape version %s, this is %s. The plugin might not be compatible.", version, version_string); + } + + + Implementation::Implementation *i = GetImplementation(); + return i; + } + } + + child_repr = child_repr->next(); + } + } catch (std::exception &e) { + g_warning("Unable to load extension."); + } + return nullptr; +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/extension/loader.h b/src/extension/loader.h new file mode 100644 index 0000000..c6adbe2 --- /dev/null +++ b/src/extension/loader.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Loader for external plug-ins. + *//* + * + * Authors: + * Moritz Eberl + * + * Copyright (C) 2016 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_LOADER_H_ +#define INKSCAPE_EXTENSION_LOADER_H_ + +#include "extension.h" + + +namespace Inkscape { + +namespace XML { +class Document; +} + +namespace Extension { + +/** This class contains the mechanism to load c++ plugins dynamically. +*/ +class Loader { + +public: + /** + * Sets a base directory where to look for the actual plugin to load. + * + * @param dir is the path where the plugin should be loaded from. + */ + void set_base_directory(std::string dir) { + _baseDirectory = dir; + } + + /** + * Loads plugin dependencies which are needed for the plugin to load. + * + * @param dep + */ + bool load_dependency(Dependency *dep); + + /** + * Load the actual implementation of a plugin supplied by the plugin. + * + * @param doc The xml representation of the INX extension configuration. + * @return The implementation of the extension loaded from the plugin. + */ + Implementation::Implementation *load_implementation(Inkscape::XML::Document *doc); + +private: + std::string _baseDirectory; /**< The base directory to load a plugin from */ + + +}; + +} // namespace Extension +} // namespace Inkscape */ + +#endif // _LOADER_H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/extension/output.cpp b/src/extension/output.cpp new file mode 100644 index 0000000..07c1120 --- /dev/null +++ b/src/extension/output.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "output.h" + +#include "document.h" + +#include "implementation/implementation.h" + +#include "prefdialog/prefdialog.h" + +#include "xml/repr.h" + + +/* Inkscape::Extension::Output */ + +namespace Inkscape { +namespace Extension { + +/** + \return None + \brief Builds a SPModuleOutput object from a XML description + \param module The module to be initialized + \param repr The XML description in a Inkscape::XML::Node tree + + Okay, so you want to build a SPModuleOutput object. + + This function first takes and does the build of the parent class, + which is SPModule. Then, it looks for the section of the + XML description. Under there should be several fields which + describe the output module to excruciating detail. Those are parsed, + copied, and put into the structure that is passed in as module. + Overall, there are many levels of indentation, just to handle the + levels of indentation in the XML file. +*/ +Output::Output (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + mimetype = nullptr; + extension = nullptr; + filetypename = nullptr; + filetypetooltip = nullptr; + dataloss = TRUE; + + if (repr != nullptr) { + Inkscape::XML::Node * child_repr; + + child_repr = repr->firstChild(); + + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "output")) { + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + char const * chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') /* Allow _ for translation of tags */ + chname++; + if (!strcmp(chname, "extension")) { + g_free (extension); + extension = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "mimetype")) { + g_free (mimetype); + mimetype = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypename")) { + g_free (filetypename); + filetypename = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypetooltip")) { + g_free (filetypetooltip); + filetypetooltip = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "dataloss")) { + if (!strcmp(child_repr->firstChild()->content(), "false")) { + dataloss = FALSE; + } + } + + child_repr = child_repr->next(); + } + + break; + } + + child_repr = child_repr->next(); + } + + } +} + +/** + \brief Destroy an output extension +*/ +Output::~Output () +{ + g_free(mimetype); + g_free(extension); + g_free(filetypename); + g_free(filetypetooltip); + return; +} + +/** + \return Whether this extension checks out + \brief Validate this extension + + This function checks to make sure that the output extension has + a filename extension and a MIME type. Then it calls the parent + class' check function which also checks out the implementation. +*/ +bool +Output::check () +{ + if (extension == nullptr) + return FALSE; + if (mimetype == nullptr) + return FALSE; + + return Extension::check(); +} + +/** + \return IETF mime-type for the extension + \brief Get the mime-type that describes this extension +*/ +gchar * +Output::get_mimetype() +{ + return mimetype; +} + +/** + \return Filename extension for the extension + \brief Get the filename extension for this extension +*/ +gchar * +Output::get_extension() +{ + return extension; +} + +/** + \return The name of the filetype supported + \brief Get the name of the filetype supported +*/ +const char * +Output::get_filetypename(bool translated) +{ + const char *name; + + if (filetypename) + name = filetypename; + else + name = get_name(); + + if (name && translated) { + return get_translation(name); + } else { + return name; + } +} + +/** + \return Tooltip giving more information on the filetype + \brief Get the tooltip for more information on the filetype +*/ +const char * +Output::get_filetypetooltip(bool translated) +{ + if (filetypetooltip && translated) { + return get_translation(filetypetooltip); + } else { + return filetypetooltip; + } +} + +/** + \return A dialog to get settings for this extension + \brief Create a dialog for preference for this extension + + Calls the implementation to get the preferences. +*/ +bool +Output::prefs () +{ + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return false; + + Gtk::Widget * controls; + controls = imp->prefs_output(this); + if (controls == nullptr) { + // std::cout << "No preferences for Output" << std::endl; + return true; + } + + Glib::ustring title = get_translation(this->get_name()); + PrefDialog *dialog = new PrefDialog(title, controls); + int response = dialog->run(); + dialog->hide(); + + delete dialog; + + return (response == Gtk::RESPONSE_OK); +} + +/** + \return None + \brief Save a document as a file + \param doc Document to save + \param filename File to save the document as + + This function does a little of the dirty work involved in saving + a document so that the implementation only has to worry about getting + bits on the disk. + + The big thing that it does is remove and read the fields that are + only used at runtime and shouldn't be saved. One that may surprise + people is the output extension. This is not saved so that the IDs + could be changed, and old files will still work properly. +*/ +void +Output::save(SPDocument *doc, gchar const *filename, bool detachbase) +{ + imp->setDetachBase(detachbase); + imp->save(this, doc, filename); + + return; +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/output.h b/src/extension/output.h new file mode 100644 index 0000000..8210858 --- /dev/null +++ b/src/extension/output.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + + +#ifndef INKSCAPE_EXTENSION_OUTPUT_H__ +#define INKSCAPE_EXTENSION_OUTPUT_H__ + +#include "extension.h" +class SPDocument; + +namespace Inkscape { +namespace Extension { + +class Output : public Extension { + gchar *mimetype; /**< What is the mime type this inputs? */ + gchar *extension; /**< The extension of the input files */ + gchar *filetypename; /**< A userfriendly name for the file type */ + gchar *filetypetooltip; /**< A more detailed description of the filetype */ + bool dataloss; /**< The extension causes data loss on save */ + +public: + class save_failed {}; /**< Generic failure for an undescribed reason */ + class save_cancelled {}; /**< Saving was cancelled */ + class no_extension_found {}; /**< Failed because we couldn't find an extension to match the filename */ + class file_read_only {}; /**< The existing file can not be opened for writing */ + class export_id_not_found { /**< The object ID requested for export could not be found in the document */ + public: + const gchar * const id; + export_id_not_found(const gchar * const id = nullptr) : id{id} {}; + }; + + Output(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Output () override; + + bool check() override; + + void save (SPDocument *doc, + gchar const *filename, + bool detachbase = false); + bool prefs (); + gchar * get_mimetype(); + gchar * get_extension(); + const char * get_filetypename(bool translated=false); + const char * get_filetypetooltip(bool translated=false); + bool causes_dataloss() { return dataloss; }; +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_OUTPUT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/patheffect.cpp b/src/extension/patheffect.cpp new file mode 100644 index 0000000..3ed53e7 --- /dev/null +++ b/src/extension/patheffect.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "patheffect.h" + +#include "db.h" + +#include "object/sp-defs.h" + +#include "xml/repr.h" + + +namespace Inkscape { +namespace Extension { + +PathEffect::PathEffect (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + +} + +PathEffect::~PathEffect (void) += default; + +void +PathEffect::processPath (SPDocument * /*doc*/, Inkscape::XML::Node * /*path*/, Inkscape::XML::Node * /*def*/) +{ + + +} + +void +PathEffect::processPathEffects (SPDocument * doc, Inkscape::XML::Node * path) +{ + gchar const * patheffectlist = path->attribute("inkscape:path-effects"); + if (patheffectlist == nullptr) + return; + + gchar ** patheffects = g_strsplit(patheffectlist, ";", 128); + Inkscape::XML::Node * defs = doc->getDefs()->getRepr(); + + for (int i = 0; (i < 128) && (patheffects[i] != nullptr); i++) { + gchar * patheffect = patheffects[i]; + + // This is weird, they should all be references... but anyway + if (patheffect[0] != '#') continue; + + Inkscape::XML::Node * prefs = sp_repr_lookup_child(defs, "id", &(patheffect[1])); + if (prefs == nullptr) { + + continue; + } + + gchar const * ext_id = prefs->attribute("extension"); + if (ext_id == nullptr) { + + continue; + } + + Inkscape::Extension::PathEffect * peffect; + peffect = dynamic_cast(Inkscape::Extension::db.get(ext_id)); + if (peffect != nullptr) { + peffect->processPath(doc, path, prefs); + } + } + + g_strfreev(patheffects); + return; +} + + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/patheffect.h b/src/extension/patheffect.h new file mode 100644 index 0000000..b2dd7a0 --- /dev/null +++ b/src/extension/patheffect.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_PATHEFFECT_H__ +#define INKSCAPE_EXTENSION_PATHEFFECT_H__ + +#include "document.h" +#include "extension.h" + +namespace Inkscape { +namespace Extension { + +class PathEffect : public Extension { + +public: + PathEffect(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~PathEffect() override; + + void processPath (SPDocument * doc, + Inkscape::XML::Node * path, + Inkscape::XML::Node * def); + static void processPathEffects (SPDocument * doc, + Inkscape::XML::Node * path); +}; /* PathEffect */ + + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_PATHEFFECT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/CMakeLists.txt b/src/extension/plugins/CMakeLists.txt new file mode 100644 index 0000000..dc15b4a --- /dev/null +++ b/src/extension/plugins/CMakeLists.txt @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +add_subdirectory(grid2) diff --git a/src/extension/plugins/grid2/CMakeLists.txt b/src/extension/plugins/grid2/CMakeLists.txt new file mode 100644 index 0000000..1d23d83 --- /dev/null +++ b/src/extension/plugins/grid2/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(grid_PART_SRCS grid.cpp) + +include_directories( ${CMAKE_BINARY_DIR}/src ) + +add_library(grid2 SHARED EXCLUDE_FROM_ALL ${grid_PART_SRCS}) + +target_link_libraries(grid2 inkscape_base) + diff --git a/src/extension/plugins/grid2/grid.cpp b/src/extension/plugins/grid2/grid.cpp new file mode 100644 index 0000000..53dcecd --- /dev/null +++ b/src/extension/plugins/grid2/grid.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file grid.cpp + + A plug-in to add a grid creation effect into Inkscape. +*/ +/* + * Copyright (C) 2004-2005 Ted Gould + * Copyright (C) 2007 MenTaLguY + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "desktop.h" + +#include "document.h" +#include "selection.h" +#include "2geom/geom.h" + +#include "object/sp-object.h" + +#include "svg/path-string.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "util/units.h" + +#include "grid.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +Grid::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Grid, I'm loading!" << std::endl; + return TRUE; +} + +namespace { + +Glib::ustring build_lines(Geom::Rect bounding_area, + Geom::Point const &offset, Geom::Point const &spacing) +{ + + std::cout << "Building lines" << std::endl; + + Geom::Point point_offset(0.0, 0.0); + + SVG::PathString path_data; + + for ( int axis = Geom::X ; axis <= Geom::Y ; ++axis ) { + point_offset[axis] = offset[axis]; + + for (Geom::Point start_point = bounding_area.min(); + start_point[axis] + offset[axis] <= (bounding_area.max())[axis]; + start_point[axis] += spacing[axis]) { + Geom::Point end_point = start_point; + end_point[1-axis] = (bounding_area.max())[1-axis]; + + path_data.moveTo(start_point + point_offset) + .lineTo(end_point + point_offset); + } + } + std::cout << "Path data:" << path_data.c_str() << std::endl; + return path_data; +} + +} // namespace + +/** + \brief This actually draws the grid. + \param module The effect that was called (unused) + \param document What should be edited. +*/ +void +Grid::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + + std::cout << "Executing effect" << std::endl; + + Inkscape::Selection * selection = ((SPDesktop *)document)->selection; + + Geom::Rect bounding_area = Geom::Rect(Geom::Point(0,0), Geom::Point(100,100)); + if (selection->isEmpty()) { + /* get page size */ + SPDocument * doc = document->doc(); + bounding_area = Geom::Rect( Geom::Point(0,0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px")) ); + } else { + Geom::OptRect bounds = selection->visualBounds(); + if (bounds) { + bounding_area = *bounds; + } + + gdouble doc_height = (document->doc())->getHeight().value("px"); + Geom::Rect temprec = Geom::Rect(Geom::Point(bounding_area.min()[Geom::X], doc_height - bounding_area.min()[Geom::Y]), + Geom::Point(bounding_area.max()[Geom::X], doc_height - bounding_area.max()[Geom::Y])); + + bounding_area = temprec; + } + + double scale = document->doc()->getDocumentScale().inverse()[Geom::X]; + + bounding_area *= Geom::Scale(scale); + Geom::Point spacings( scale * module->get_param_float("xspacing"), + scale * module->get_param_float("yspacing") ); + gdouble line_width = scale * module->get_param_float("lineWidth"); + Geom::Point offsets( scale * module->get_param_float("xoffset"), + scale * module->get_param_float("yoffset") ); + + Glib::ustring path_data(""); + + path_data = build_lines(bounding_area, offsets, spacings); + Inkscape::XML::Document * xml_doc = document->doc()->getReprDoc(); + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node * current_layer = static_cast(document)->currentLayer()->getRepr(); + Inkscape::XML::Node * path = xml_doc->createElement("svg:path"); + + path->setAttribute("d", path_data); + + std::ostringstream stringstream; + stringstream << "fill:none;stroke:#000000;stroke-width:" << line_width << "px"; + path->setAttribute("style", stringstream.str()); + + current_layer->appendChild(path); + Inkscape::GC::release(path); +} + +/** \brief A class to make an adjustment that uses Extension params */ +class PrefAdjustment : public Gtk::Adjustment { + /** Extension that this relates to */ + Inkscape::Extension::Extension * _ext; + /** The string which represents the parameter */ + char * _pref; +public: + /** \brief Make the adjustment using an extension and the string + describing the parameter. */ + PrefAdjustment(Inkscape::Extension::Extension * ext, char * pref) : + Gtk::Adjustment(0.0, 0.0, 10.0, 0.1), _ext(ext), _pref(pref) { + this->set_value(_ext->get_param_float(_pref)); + this->signal_value_changed().connect(sigc::mem_fun(this, &PrefAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class PrefAdjustment */ + +/** \brief A function to respond to the value_changed signal from the + adjustment. + + This function just grabs the value from the adjustment and writes + it to the parameter. Very simple, but yet beautiful. +*/ +void +PrefAdjustment::val_changed () +{ + // std::cout << "Value Changed to: " << this->get_value() << std::endl; + _ext->set_param_float(_pref, this->get_value()); + return; +} + +/** \brief A function to get the prefences for the grid + \param moudule Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +Grid::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = nullptr; + if (!selected.empty()) { + first_select = selected.front()->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + + + + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/grid2/grid.h b/src/extension/plugins/grid2/grid.h new file mode 100644 index 0000000..f6d7d57 --- /dev/null +++ b/src/extension/plugins/grid2/grid.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __GRID_H + +#include "extension/implementation/implementation.h" + + +#include +#include +#include "inkscape-version.cpp" + + + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class Grid : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +extern "C" G_MODULE_EXPORT Inkscape::Extension::Implementation::Implementation* GetImplementation() { return new Inkscape::Extension::Internal::Grid(); } +extern "C" G_MODULE_EXPORT const gchar* GetInkscapeVersion() { return Inkscape::version_string; } +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/grid2/libgrid2.inx b/src/extension/plugins/grid2/libgrid2.inx new file mode 100644 index 0000000..db95cd5 --- /dev/null +++ b/src/extension/plugins/grid2/libgrid2.inx @@ -0,0 +1,21 @@ + + + +<_name>Grid2 +org.inkscape.effect.grid2 +1.0 +10.0 +10.0 +0.0 +0.0 + + all + + + + + +<_menu-tip>Draw a path which is a grid + + + diff --git a/src/extension/prefdialog/parameter-bool.cpp b/src/extension/prefdialog/parameter-bool.cpp new file mode 100644 index 0000000..609e727 --- /dev/null +++ b/src/extension/prefdialog/parameter-bool.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-bool.h" + +#include +#include + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + +namespace Inkscape { +namespace Extension { + +ParamBool::ParamBool(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + if (!strcmp(value, "true")) { + _value = true; + } else if (!strcmp(value, "false")) { + _value = false; + } else { + g_warning("Invalid default value ('%s') for parameter '%s' in extension '%s'", + value, _name, _extension->get_id()); + } + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getBool(pref_name(), _value); +} + +bool ParamBool::set(bool in) +{ + _value = in; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(pref_name(), _value); + + return _value; +} + +bool ParamBool::get() const +{ + return _value; +} + +/** + * A check button which is Param aware. It works with the + * parameter to change it's value as the check button changes + * value. + */ +class ParamBoolCheckButton : public Gtk::CheckButton { +public: + /** + * Initialize the check button. + * This function sets the value of the checkbox to be that of the + * parameter, and then sets up a callback to \c on_toggle. + * + * @param param Which parameter to adjust on changing the check button + */ + ParamBoolCheckButton(ParamBool *param, char *label, sigc::signal *changeSignal) + : Gtk::CheckButton(label) + , _pref(param) + , _changeSignal(changeSignal) { + this->set_active(_pref->get()); + this->signal_toggled().connect(sigc::mem_fun(this, &ParamBoolCheckButton::on_toggle)); + return; + } + + /** + * A function to respond to the check box changing. + * Adjusts the value of the preference to match that in the check box. + */ + void on_toggle (); + +private: + /** Param to change. */ + ParamBool *_pref; + sigc::signal *_changeSignal; +}; + +void ParamBoolCheckButton::on_toggle() +{ + _pref->set(this->get_active()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } + return; +} + +std::string ParamBool::value_to_string() const +{ + if (_value) { + return "true"; + } + return "false"; +} + +Gtk::Widget *ParamBool::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + auto hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + hbox->set_homogeneous(false); + + ParamBoolCheckButton * checkbox = Gtk::manage(new ParamBoolCheckButton(this, _text, changeSignal)); + checkbox->show(); + hbox->pack_start(*checkbox, false, false); + + hbox->show(); + + return dynamic_cast(hbox); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-bool.h b/src/extension/prefdialog/parameter-bool.h new file mode 100644 index 0000000..52fe06a --- /dev/null +++ b/src/extension/prefdialog/parameter-bool.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INK_EXTENSION_PARAMBOOL_H +#define SEEN_INK_EXTENSION_PARAMBOOL_H +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +/** + * A boolean parameter. + */ +class ParamBool : public InxParameter { +public: + ParamBool(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** + * Returns the current state/value. + */ + bool get() const; + + /** + * A function to set the state/value. + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place pref_name() is used. + * + * @param in The value to set to + */ + bool set(bool in); + + /** + * Creates a bool check button for a bool parameter. + * Builds a hbox with a label and a check button in it. + */ + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + /** + * Appends 'true' or 'false'. + * @todo investigate. Returning a value that can then be appended would probably work better/safer. + */ + std::string value_to_string() const override; + +private: + /** Internal value. */ + bool _value = true; +}; + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_PARAMBOOL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-color.cpp b/src/extension/prefdialog/parameter-color.cpp new file mode 100644 index 0000000..a17ca1b --- /dev/null +++ b/src/extension/prefdialog/parameter-color.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen + * Christopher Brown + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-color.h" + +#include +#include + +#include +#include +#include + +#include "color.h" +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/color-notebook.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +ParamColor::ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + unsigned int _value = 0x000000ff; // default to black + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = strtoul(value, nullptr, 0); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getUInt(pref_name(), _value); + + _color.setValue(_value); + + _color_changed = _color.signal_changed.connect(sigc::mem_fun(this, &ParamColor::_onColorChanged)); + // TODO: SelectedColor does not properly emit signal_changed after dragging, so we also need the following + _color_released = _color.signal_released.connect(sigc::mem_fun(this, &ParamColor::_onColorChanged)); + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "colorbutton")) { + _mode = COLOR_BUTTON; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +ParamColor::~ParamColor() +{ + _color_changed.disconnect(); + _color_released.disconnect(); +} + +unsigned int ParamColor::set(unsigned int in) +{ + _color.setValue(in); + + return in; +} + +Gtk::Widget *ParamColor::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + if (changeSignal) { + _changeSignal = new sigc::signal(*changeSignal); + } + + Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox(false, GUI_PARAM_WIDGETS_SPACING)); + if (_mode == COLOR_BUTTON) { + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + Gdk::RGBA rgba; + rgba.set_red_u (((_color.value() >> 24) & 255) << 8); + rgba.set_green_u(((_color.value() >> 16) & 255) << 8); + rgba.set_blue_u (((_color.value() >> 8) & 255) << 8); + rgba.set_alpha_u(((_color.value() >> 0) & 255) << 8); + + // TODO: It would be nicer to have a custom Gtk::ColorButton() implementation here, + // that wraps an Inkscape::UI::Widget::ColorNotebook into a new dialog + _color_button = Gtk::manage(new Gtk::ColorButton(rgba)); + _color_button->set_title(_text); + _color_button->set_use_alpha(); + _color_button->show(); + hbox->pack_end(*_color_button, false, false); + + _color_button->signal_color_set().connect(sigc::mem_fun(this, &ParamColor::_onColorButtonChanged)); + } else { + Gtk::Widget *selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(_color)); + hbox->pack_start(*selector, true, true, 0); + selector->show(); + } + hbox->show(); + return hbox; + +} + +void ParamColor::_onColorChanged() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setUInt(pref_name(), _color.value()); + + if (_changeSignal) + _changeSignal->emit(); +} + +void ParamColor::_onColorButtonChanged() +{ + Gdk::RGBA rgba = _color_button->get_rgba(); + unsigned int value = ((rgba.get_red_u() >> 8) << 24) + + ((rgba.get_green_u() >> 8) << 16) + + ((rgba.get_blue_u() >> 8) << 8) + + ((rgba.get_alpha_u() >> 8) << 0); + set(value); +} + +std::string ParamColor::value_to_string() const +{ + char value_string[16]; + snprintf(value_string, 16, "%u", _color.value()); + return value_string; +} + +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-color.h b/src/extension/prefdialog/parameter-color.h new file mode 100644 index 0000000..4fef4b6 --- /dev/null +++ b/src/extension/prefdialog/parameter-color.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INK_EXTENSION_PARAMCOLOR_H__ +#define SEEN_INK_EXTENSION_PARAMCOLOR_H__ +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" +#include "ui/selected-color.h" + +namespace Gtk { +class Widget; +class ColorButton; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamColor : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, COLOR_BUTTON + }; + + ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + ~ParamColor() override; + + /** Returns \c _value, with a \i const to protect it. */ + unsigned int get() const { return _color.value(); } + + unsigned int set(unsigned int in); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + std::string value_to_string() const override; + + sigc::signal *_changeSignal; + +private: + void _onColorChanged(); + void _onColorButtonChanged(); + + /** Internal value of this parameter */ + Inkscape::UI::SelectedColor _color; + + sigc::connection _color_changed; + sigc::connection _color_released; + + Gtk::ColorButton *_color_button; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; // class ParamColor + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_PARAMCOLOR_H__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-float.cpp b/src/extension/prefdialog/parameter-float.cpp new file mode 100644 index 0000000..ad8dc94 --- /dev/null +++ b/src/extension/prefdialog/parameter-float.cpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-float.h" + +#include +#include + +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/spin-scale.h" +#include "ui/widget/spinbutton.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +ParamFloat::ParamFloat(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = g_ascii_strtod(value, nullptr); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getDouble(pref_name(), _value); + + // parse and apply limits + const char *min = xml->attribute("min"); + if (min) { + _min = g_ascii_strtod(min, nullptr); + } + + const char *max = xml->attribute("max"); + if (max) { + _max = g_ascii_strtod(max, nullptr); + } + + if (_value < _min) { + _value = _min; + } + + if (_value > _max) { + _value = _max; + } + + // parse precision + const char *precision = xml->attribute("precision"); + if (precision != nullptr) { + _precision = strtol(precision, nullptr, 0); + } + + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "full")) { + _mode = FULL; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set to. + */ +float ParamFloat::set(float in) +{ + _value = in; + if (_value > _max) { + _value = _max; + } + if (_value < _min) { + _value = _min; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(pref_name(), _value); + + return _value; +} + +std::string ParamFloat::value_to_string() const +{ + char value_string[G_ASCII_DTOSTR_BUF_SIZE]; + // TODO: Some strange rounding is going on here, resulting in parameter values quite different + // from the original string value. Needs some investigation to make it less bad. + // See also https://gitlab.gnome.org/GNOME/glib/issues/964 + g_ascii_dtostr(value_string, G_ASCII_DTOSTR_BUF_SIZE, _value); + return value_string; +} + +/** A class to make an adjustment that uses Extension params. */ +class ParamFloatAdjustment : public Gtk::Adjustment { + /** The parameter to adjust. */ + ParamFloat *_pref; + sigc::signal *_changeSignal; +public: + /** Make the adjustment using an extension and the string + describing the parameter. */ + ParamFloatAdjustment(ParamFloat *param, sigc::signal *changeSignal) + : Gtk::Adjustment(0.0, param->min(), param->max(), 0.1, 1.0, 0) + , _pref(param) + , _changeSignal(changeSignal) { + this->set_value(_pref->get()); + this->signal_value_changed().connect(sigc::mem_fun(this, &ParamFloatAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class ParamFloatAdjustment */ + +/** + * A function to respond to the value_changed signal from the adjustment. + * + * This function just grabs the value from the adjustment and writes + * it to the parameter. Very simple, but yet beautiful. + */ +void ParamFloatAdjustment::val_changed() +{ + _pref->set(this->get_value()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } + return; +} + +/** + * Creates a Float Adjustment for a float parameter. + * + * Builds a hbox with a label and a float adjustment in it. + */ +Gtk::Widget *ParamFloat::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox(false, GUI_PARAM_WIDGETS_SPACING)); + + auto pfa = new ParamFloatAdjustment(this, changeSignal); + Glib::RefPtr fadjust(pfa); + + if (_mode == FULL) { + + Glib::ustring text; + if (_text != nullptr) + text = _text; + UI::Widget::SpinScale *scale = Gtk::manage(new UI::Widget::SpinScale(text, fadjust, _precision)); + scale->set_size_request(400, -1); + scale->show(); + hbox->pack_start(*scale, true, true); + + } + else if (_mode == DEFAULT) { + + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + auto spin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(fadjust, 0.1, _precision)); + spin->show(); + hbox->pack_start(*spin, false, false); + } + + hbox->show(); + + return dynamic_cast(hbox); +} + + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-float.h b/src/extension/prefdialog/parameter-float.h new file mode 100644 index 0000000..4c28cb5 --- /dev/null +++ b/src/extension/prefdialog/parameter-float.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMFLOAT_H_SEEN +#define INK_EXTENSION_PARAMFLOAT_H_SEEN + +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamFloat : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, FULL + }; + + ParamFloat(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** Returns \c _value. */ + float get() const { return _value; } + + float set(float in); + + float max () { return _max; } + + float min () { return _min; } + + float precision () { return _precision; } + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + std::string value_to_string() const override; + +private: + /** Internal value. */ + float _value = 0; + + /** limits */ + // TODO: do these defaults make sense or should we be unbounded by default? + float _min = 0; + float _max = 10; + + /** numeric precision (i.e. number of digits) */ + int _precision = 1; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* INK_EXTENSION_PARAMFLOAT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-int.cpp b/src/extension/prefdialog/parameter-int.cpp new file mode 100644 index 0000000..8a8e49e --- /dev/null +++ b/src/extension/prefdialog/parameter-int.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-int.h" + +#include +#include + +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-scale.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + + +ParamInt::ParamInt(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = strtol(value, nullptr, 0); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getInt(pref_name(), _value); + + // parse and apply limits + const char *min = xml->attribute("min"); + if (min) { + _min = strtol(min, nullptr, 0); + } + + const char *max = xml->attribute("max"); + if (max) { + _max = strtol(max, nullptr, 0); + } + + if (_value < _min) { + _value = _min; + } + + if (_value > _max) { + _value = _max; + } + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "full")) { + _mode = FULL; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +/** + * A function to set the \c _value. + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set to. + */ +int ParamInt::set(int in) +{ + _value = in; + if (_value > _max) { + _value = _max; + } + if (_value < _min) { + _value = _min; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(pref_name(), _value); + + return _value; +} + +/** A class to make an adjustment that uses Extension params. */ +class ParamIntAdjustment : public Gtk::Adjustment { + /** The parameter to adjust. */ + ParamInt *_pref; + sigc::signal *_changeSignal; +public: + /** Make the adjustment using an extension and the string describing the parameter. */ + ParamIntAdjustment(ParamInt *param, sigc::signal *changeSignal) + : Gtk::Adjustment(0.0, param->min(), param->max(), 1.0, 10.0, 0) + , _pref(param) + , _changeSignal(changeSignal) + { + this->set_value(_pref->get()); + this->signal_value_changed().connect(sigc::mem_fun(this, &ParamIntAdjustment::val_changed)); + }; + + void val_changed (); +}; /* class ParamIntAdjustment */ + +/** + * A function to respond to the value_changed signal from the adjustment. + * + * This function just grabs the value from the adjustment and writes + * it to the parameter. Very simple, but yet beautiful. + */ +void ParamIntAdjustment::val_changed() +{ + _pref->set((int)this->get_value()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } +} + +/** + * Creates a Int Adjustment for a int parameter. + * + * Builds a hbox with a label and a int adjustment in it. + */ +Gtk::Widget * +ParamInt::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox(false, GUI_PARAM_WIDGETS_SPACING)); + + auto pia = new ParamIntAdjustment(this, changeSignal); + Glib::RefPtr fadjust(pia); + + if (_mode == FULL) { + + Glib::ustring text; + if (_text != nullptr) + text = _text; + UI::Widget::SpinScale *scale = Gtk::manage(new UI::Widget::SpinScale(text, fadjust, 0)); + scale->set_size_request(400, -1); + scale->show(); + hbox->pack_start(*scale, true, true); + } else if (_mode == DEFAULT) { + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + auto spin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(fadjust, 1.0, 0)); + spin->show(); + hbox->pack_start(*spin, false, false); + } + + hbox->show(); + + return dynamic_cast(hbox); +} + +std::string ParamInt::value_to_string() const +{ + char value_string[32]; + snprintf(value_string, 32, "%d", _value); + return value_string; +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-int.h b/src/extension/prefdialog/parameter-int.h new file mode 100644 index 0000000..da43eb7 --- /dev/null +++ b/src/extension/prefdialog/parameter-int.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMINT_H_SEEN +#define INK_EXTENSION_PARAMINT_H_SEEN + +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould + * Johan Engelen * + * Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamInt : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, FULL + }; + + ParamInt(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** Returns \c _value. */ + int get() const { return _value; } + + int set(int in); + + int max () { return _max; } + + int min () { return _min; } + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + std::string value_to_string() const override; + +private: + /** Internal value. */ + int _value = 0; + + /** limits */ + // TODO: do these defaults make sense or should we be unbounded by default? + int _min = 0; + int _max = 10; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* INK_EXTENSION_PARAMINT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-notebook.cpp b/src/extension/prefdialog/parameter-notebook.cpp new file mode 100644 index 0000000..050c84f --- /dev/null +++ b/src/extension/prefdialog/parameter-notebook.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Notebook and NotebookPage parameters for extensions. + */ + +/* + * Authors: + * Johan Engelen + * Jon A. Cruz + * + * Copyright (C) 2006 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-notebook.h" + +#include + +#include +#include + +#include "preferences.h" + +#include "extension/extension.h" + +#include "xml/node.h" + +namespace Inkscape { +namespace Extension { + + +ParamNotebook::ParamNotebookPage::ParamNotebookPage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read XML tree of page and parse parameters + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + chname++; + } + + if (InxWidget::is_valid_widget_name(chname)) { + InxWidget *widget = InxWidget::make(child_repr, _extension); + if (widget) { + _children.push_back(widget); + } + } else if (child_repr->type() == XML::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') in notebook page in extension '%s'.", + chname, _extension->get_id()); + } else if (child_repr->type() != XML::COMMENT_NODE){ + g_warning("Invalid child element found in notebook page in extension '%s'.", _extension->get_id()); + } + + child_repr = child_repr->next(); + } + } +} + + +/** + * Creates a notebookpage widget for a notebook. + * + * Builds a notebook page (a vbox) and puts parameters on it. + */ +Gtk::Widget *ParamNotebook::ParamNotebookPage::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::VBox * vbox = Gtk::manage(new Gtk::VBox); + vbox->set_border_width(GUI_BOX_MARGIN); + vbox->set_spacing(GUI_BOX_SPACING); + + // add parameters onto page (if any) + for (auto child : _children) { + Gtk::Widget *child_widget = child->get_widget(changeSignal); + if (child_widget) { + int indent = child->get_indent(); + child_widget->set_margin_start(indent *GUI_INDENTATION); + vbox->pack_start(*child_widget, false, true, 0); // fill=true does not have an effect here, but allows the + // child to choose to expand by setting hexpand/vexpand + + const char *tooltip = child->get_tooltip(); + if (tooltip) { + child_widget->set_tooltip_text(tooltip); + } + } + } + + vbox->show(); + + return dynamic_cast(vbox); +} + +/** End ParamNotebookPage **/ + + + +/** ParamNotebook **/ + +ParamNotebook::ParamNotebook(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read XML tree to add pages (allow _page for backwards compatibility) + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (chname && (!strcmp(chname, INKSCAPE_EXTENSION_NS "page") || + !strcmp(chname, INKSCAPE_EXTENSION_NS "_page") )) { + ParamNotebookPage *page; + page = new ParamNotebookPage(child_repr, ext); + + if (page) { + _children.push_back(page); + } + } else if (child_repr->type() == XML::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') for parameter '%s' in extension '%s'. Expected 'page'.", + chname, _name, _extension->get_id()); + } else if (child_repr->type() != XML::COMMENT_NODE){ + g_warning("Invalid child element found in parameter '%s' in extension '%s'. Expected 'page'.", + _name, _extension->get_id()); + } + child_repr = child_repr->next(); + } + } + if (_children.empty()) { + g_warning("No (valid) pages for parameter '%s' in extension '%s'", _name, _extension->get_id()); + } + + // check for duplicate page names + std::unordered_set names; + for (auto child : _children) { + ParamNotebookPage *page = static_cast(child); + auto ret = names.emplace(page->_name); + if (!ret.second) { + g_warning("Duplicate page name ('%s') for parameter '%s' in extension '%s'.", + page->_name, _name, _extension->get_id()); + } + } + + // get value (initialize with value of first page if pref is empty) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getString(pref_name()); + + if (_value.empty()) { + if (!_children.empty()) { + ParamNotebookPage *first_page = dynamic_cast(_children[0]); + _value = first_page->_name; + } + } +} + + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The number of the page to set as new value. + */ +const Glib::ustring& ParamNotebook::set(const int in) +{ + int i = in < _children.size() ? in : _children.size()-1; + ParamNotebookPage *page = dynamic_cast(_children[i]); + + if (page) { + _value = page->_name; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_name(), _value); + } + + return _value; +} + +std::string ParamNotebook::value_to_string() const +{ + return _value; +} + + +/** A special category of Gtk::Notebook to handle notebook parameters. */ +class NotebookWidget : public Gtk::Notebook { +private: + ParamNotebook *_pref; +public: + /** + * Build a notebookpage preference for the given parameter. + * @param pref Where to get the string (pagename) from, and where to put it when it changes. + */ + NotebookWidget(ParamNotebook *pref) + : Gtk::Notebook() + , _pref(pref) + , activated(false) + { + // don't have to set the correct page: this is done in ParamNotebook::get_widget hook function + this->signal_switch_page().connect(sigc::mem_fun(this, &NotebookWidget::changed_page)); + } + + void changed_page(Gtk::Widget *page, guint pagenum); + + bool activated; +}; + +/** + * Respond to the selected page of notebook changing. + * This function responds to the changing by reporting it to + * ParamNotebook. The change is only reported when the notebook + * is actually visible. This to exclude 'fake' changes when the + * notebookpages are added or removed. + */ +void NotebookWidget::changed_page(Gtk::Widget * /*page*/, guint pagenum) +{ + if (get_visible()) { + _pref->set((int)pagenum); + } +} + +/** + * Creates a Notebook widget for a notebook parameter. + * + * Builds a notebook and puts pages in it. + */ +Gtk::Widget *ParamNotebook::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + NotebookWidget *notebook = Gtk::manage(new NotebookWidget(this)); + + // add pages (if any) and switch to previously selected page + int current_page = -1; + int selected_page = -1; + for (auto child : _children) { + ParamNotebookPage *page = dynamic_cast(child); + g_assert(child); // A ParamNotebook has only children of type ParamNotebookPage. + // If we receive a non-page child here something is very wrong! + current_page++; + + Gtk::Widget *page_widget = page->get_widget(changeSignal); + + Glib::ustring page_text = page->_text; + if (page->_translatable != NO) { // translate unless explicitly marked untranslatable + page_text = page->get_translation(page_text.c_str()); + } + + notebook->append_page(*page_widget, page_text); + + if (_value == page->_name) { + selected_page = current_page; + } + } + if (selected_page >= 0) { + notebook->set_current_page(selected_page); + } + + notebook->show(); + + return static_cast(notebook); +} + + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-notebook.h b/src/extension/prefdialog/parameter-notebook.h new file mode 100644 index 0000000..b64e5c6 --- /dev/null +++ b/src/extension/prefdialog/parameter-notebook.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMNOTEBOOK_H_SEEN +#define INK_EXTENSION_PARAMNOTEBOOK_H_SEEN + +/** \file + * Notebook parameter for extensions. + */ + +/* + * Author: + * Johan Engelen + * Jon A. Cruz + * + * Copyright (C) 2006 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +#include + +#include + + +namespace Gtk { +class Widget; +} + + +namespace Inkscape { +namespace Extension { + +class Extension; + + +/** A class to represent a notebook parameter of an extension. */ +class ParamNotebook : public InxParameter { +private: + /** Internal value. */ + Glib::ustring _value; + + /** + * A class to represent the pages of a notebook parameter of an extension. + */ + class ParamNotebookPage : public InxParameter { + friend class ParamNotebook; + public: + ParamNotebookPage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + // ParamNotebookPage is not a real parameter (it has no value), so make sure it does not return one + std::string value_to_string() const override { return ""; }; + }; /* class ParamNotebookPage */ + +public: + ParamNotebook(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + + std::string value_to_string() const override; + + const Glib::ustring& get() { return _value; } + const Glib::ustring& set(const int in); +}; /* class ParamNotebook */ + + + + + +} // namespace Extension +} // namespace Inkscape + +#endif /* INK_EXTENSION_PARAMNOTEBOOK_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-optiongroup.cpp b/src/extension/prefdialog/parameter-optiongroup.cpp new file mode 100644 index 0000000..954627e --- /dev/null +++ b/src/extension/prefdialog/parameter-optiongroup.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + *extension parameter for options with multiple predefined value choices + * + * Currently implemented as either Gtk::RadioButton or Gtk::ComboBoxText + */ + +/* + * Author: + * Johan Engelen + * + * Copyright (C) 2006-2007 Johan Engelen + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-optiongroup.h" + +#include + +#include +#include +#include + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + + +namespace Inkscape { +namespace Extension { + +ParamOptionGroup::ParamOptionGroup(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read valid optiongroup choices from XML tree, i,e. + // - %1", escaped_url)); + } else { + label->set_text(newtext); + } + label->set_line_wrap(); + label->set_xalign(0); + + // TODO: Ugly "fix" for gtk3 width/height calculation of labels. + // - If not applying any limits long labels will make the window grow horizontally until it uses up + // most of the available space (i.e. most of the screen area) which is ridiculously wide. + // - By using "set_default_size(0,0)" in prefidalog.cpp we tell the window to shrink as much as possible, + // however this can result in a much too narrow dialog instead and a lot of unnecessary wrapping. + // - Here we set a lower limit of GUI_MAX_LINE_LENGTH characters per line that long texts will always use. + // This means texts can not shrink anymore (they can still grow, though) and it's also necessary + // to prevent https://bugzilla.gnome.org/show_bug.cgi?id=773572 + int len = newtext.length(); + label->set_width_chars(len > GUI_MAX_LINE_LENGTH ? GUI_MAX_LINE_LENGTH : len); + + label->show(); + + Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox()); + hbox->pack_start(*label, true, true); + hbox->show(); + + return hbox; +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-label.h b/src/extension/prefdialog/widget-label.h new file mode 100644 index 0000000..1c16550 --- /dev/null +++ b/src/extension/prefdialog/widget-label.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Description widget for extensions + *//* + * Authors: + * Ted Gould + * Johan Engelen * + * Patrick Storz + * + * Copyright (C) 2005-2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_LABEL_H +#define SEEN_INK_EXTENSION_WIDGET_LABEL_H + +#include "widget.h" + +#include + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A label widget */ +class WidgetLabel : public InxWidget { +public: + enum AppearanceMode { + DEFAULT, HEADER, URL + }; + + WidgetLabel(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; +private: + /** \brief Internal value. */ + Glib::ustring _value; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_LABEL_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-separator.cpp b/src/extension/prefdialog/widget-separator.cpp new file mode 100644 index 0000000..d78894b --- /dev/null +++ b/src/extension/prefdialog/widget-separator.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Separator widget for extensions + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-separator.h" + +#include + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetSeparator::WidgetSeparator(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetSeparator::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Separator *separator = Gtk::manage(new Gtk::Separator()); + separator->show(); + + return dynamic_cast(separator); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-separator.h b/src/extension/prefdialog/widget-separator.h new file mode 100644 index 0000000..9533aba --- /dev/null +++ b/src/extension/prefdialog/widget-separator.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Separator widget for extensions + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H +#define SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H + +#include "widget.h" + +#include + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A separator widget */ +class WidgetSeparator : public InxWidget { +public: + WidgetSeparator(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-spacer.cpp b/src/extension/prefdialog/widget-spacer.cpp new file mode 100644 index 0000000..8ffa6ac --- /dev/null +++ b/src/extension/prefdialog/widget-spacer.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Spacer widget for extensions + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-spacer.h" + +#include + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetSpacer::WidgetSpacer(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ + // get size + const char *size = xml->attribute("size"); + if (size) { + _size = strtol(size, nullptr, 0); + if (_size == 0) { + if (!strcmp(size, "expand")) { + _expand = true; + } else { + g_warning("Invalid value ('%s') for size spacer in extension '%s'", size, _extension->get_id()); + } + } + } +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetSpacer::get_widget(sigc::signal *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *spacer = Gtk::manage(new Gtk::Box()); + spacer->set_border_width(_size/2); + + if (_expand) { + spacer->set_hexpand(); + spacer->set_vexpand(); + } + + spacer->show(); + + return dynamic_cast(spacer); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-spacer.h b/src/extension/prefdialog/widget-spacer.h new file mode 100644 index 0000000..467b5f9 --- /dev/null +++ b/src/extension/prefdialog/widget-spacer.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Spacer widget for extensions + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_SPACER_H +#define SEEN_INK_EXTENSION_WIDGET_SPACER_H + +#include "widget.h" + +#include + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A separator widget */ +class WidgetSpacer : public InxWidget { +public: + WidgetSpacer(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal *changeSignal) override; + +private: + /** size of the spacer in px */ + int _size = GUI_BOX_MARGIN; + + /** should the spacer be flexible and expand? */ + bool _expand = false; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_SPACER_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget.cpp b/src/extension/prefdialog/widget.cpp new file mode 100644 index 0000000..18639c5 --- /dev/null +++ b/src/extension/prefdialog/widget.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Parameters for extensions. + *//* + * Author: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" +#include "widget.h" +#include "widget-box.h" +#include "widget-image.h" +#include "widget-label.h" +#include "widget-separator.h" +#include "widget-spacer.h" + +#include +#include + +#include + +#include "extension/extension.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +InxWidget *InxWidget::make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext) +{ + InxWidget *widget = nullptr; + + const char *name = in_repr->name(); + if (!strncmp(name, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + name += strlen(INKSCAPE_EXTENSION_NS); + } + if (name[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + name++; + } + + // decide on widget type based on tag name + // keep in sync with list of names supported in InxWidget::is_valid_widget_name() below + if (!name) { + // we can't create a widget without name + g_warning("InxWidget without name in extension '%s'.", in_ext->get_id()); + } else if (!strcmp(name, "hbox") || !strcmp(name, "vbox")) { + widget = new WidgetBox(in_repr, in_ext); + } else if (!strcmp(name, "image")) { + widget = new WidgetImage(in_repr, in_ext); + } else if (!strcmp(name, "label")) { + widget = new WidgetLabel(in_repr, in_ext); + } else if (!strcmp(name, "separator")) { + widget = new WidgetSeparator(in_repr, in_ext); + } else if (!strcmp(name, "spacer")) { + widget = new WidgetSpacer(in_repr, in_ext); + } else if (!strcmp(name, "param")) { + widget = InxParameter::make(in_repr, in_ext); + } else { + g_warning("Unknown widget name ('%s') in extension '%s'", name, in_ext->get_id()); + } + + // Note: widget could equal nullptr + return widget; +} + +bool InxWidget::is_valid_widget_name(const char *name) +{ + // keep in sync with names supported in InxWidget::make() above + static const std::vector valid_names = + {"hbox", "vbox", "image", "label", "separator", "spacer", "param"}; + + if (std::find(valid_names.begin(), valid_names.end(), name) != valid_names.end()) { + return true; + } else { + return false; + } +} + + +InxWidget::InxWidget(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *ext) + : _extension(ext) +{ + // translatable (optional) + const char *translatable = in_repr->attribute("translatable"); + if (translatable) { + if (!strcmp(translatable, "yes")) { + _translatable = YES; + } else if (!strcmp(translatable, "no")) { + _translatable = NO; + } else { + g_warning("Invalid value ('%s') for translatable attribute of widget '%s' in extension '%s'", + translatable, in_repr->name(), _extension->get_id()); + } + } + + // context (optional) + const char *context = in_repr->attribute("context"); + if (!context) { + context = in_repr->attribute("msgctxt"); // backwards-compatibility with previous name + } + if (context) { + _context = g_strdup(context); + } + + // gui-hidden (optional) + const char *gui_hidden = in_repr->attribute("gui-hidden"); + if (gui_hidden != nullptr) { + if (strcmp(gui_hidden, "true") == 0) { + _hidden = true; + } + } + + // indent (optional) + const char *indent = in_repr->attribute("indent"); + if (indent != nullptr) { + _indent = strtol(indent, nullptr, 0); + } + + // appearance (optional, does not apply to all parameters) + const char *appearance = in_repr->attribute("appearance"); + if (appearance) { + _appearance = g_strdup(appearance); + } +} + +InxWidget::~InxWidget() +{ + for (auto child : _children) { + delete child; + } + + g_free(_context); + _context = nullptr; + + g_free(_appearance); + _appearance = nullptr; +} + +Gtk::Widget * +InxWidget::get_widget(sigc::signal * /*changeSignal*/) +{ + // if we end up here we're missing a definition of ::get_widget() in one of the subclasses + g_critical("InxWidget::get_widget called from widget of type '%s' in extension '%s'", + typeid(this).name(), _extension->get_id()); + g_assert_not_reached(); + return nullptr; +} + +const char *InxWidget::get_translation(const char* msgid) { + return _extension->get_translation(msgid, _context); +} + +void InxWidget::get_widgets(std::vector &list) +{ + list.push_back(this); + for (auto child : _children) { + child->get_widgets(list); + } +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/widget.h b/src/extension/prefdialog/widget.h new file mode 100644 index 0000000..843655c --- /dev/null +++ b/src/extension/prefdialog/widget.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Base class for extension widgets. + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_H +#define SEEN_INK_EXTENSION_WIDGET_H + +#include +#include + +#include + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class Extension; + + +/** + * Base class to represent all widgets of an extension (including parameters) + */ +class InxWidget { +public: + InxWidget(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext); + + virtual ~InxWidget(); + + /** + * Creates a new extension widget for usage in a prefdialog. + * + * The type of widget created is parsed from the XML representation passed in, + * and the suitable subclass constructor is called. + * + * For specialized widget types (like parameters) we defer to the subclass function of the same name. + * + * @param in_repr The XML representation describing the widget. + * @param in_ext The extension the widget belongs to. + * @return a pointer to a new Widget if applicable, null otherwise.. + */ + static InxWidget *make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext); + + /** Checks if name is a valid widget name, i.e. a widget can be constructed from it using make() */ + static bool is_valid_widget_name(const char *name); + + /** Return the instance's GTK::Widget representation for usage in a GUI + * + * @param changeSignal Can be used to subscribe to parameter changes. + * Will be emitted whenever a parameter value changes. + * + * @teturn A Gtk::Widget for the \a InxWidget. \c nullptr if the widget is hidden. + */ + virtual Gtk::Widget *get_widget(sigc::signal *changeSignal); + + virtual const char *get_tooltip() const { return nullptr; } // tool-tips are exclusive to InxParameters for now + + /** Indicates if the widget is hidden or not */ + bool get_hidden() const { return _hidden; } + + /** Indentation level of the widget */ + int get_indent() const { return _indent; } + + + /** + * Recursively construct a list containing the current widget and all of it's child widgets (if it has any) + * + * @param list Reference to a vector of pointers to \a InxWidget that will be appended with the new \a InxWidgets + */ + virtual void get_widgets(std::vector &list); + + + /** Recommended margin of boxes containing multiple widgets (in px) */ + const static int GUI_BOX_MARGIN = 10; + /** Recommended spacing between multiple widgets packed into a box (in px) */ + const static int GUI_BOX_SPACING = 4; + /** Recommended indentation width of widgets(in px) */ + const static int GUI_INDENTATION = 12; + /** Recommended maximum line length for wrapping textual wdgets (in chars) */ + const static int GUI_MAX_LINE_LENGTH = 60; + +protected: + enum Translatable { + UNSET, YES, NO + }; + + /** Which extension is this Widget attached to. */ + Inkscape::Extension::Extension *_extension = nullptr; + + /** Child widgets of this widget (might be empty if there are none) */ + std::vector _children; + + /** Whether the widget is visible. */ + bool _hidden = false; + + /** Indentation level of the widget. */ + int _indent = 0; + + /** Appearance of the widget (not used by all widgets). */ + char *_appearance = nullptr; + + /** Is widget translatable? */ + Translatable _translatable = UNSET; + + /** context for translation of translatable strings. */ + char *_context = nullptr; + + + /* **** member functions **** */ + + /** gets the gettext translation for msgid + * + * Handles translation domain of the extension and message context of the widget internally + * + * @param msgid String to translate + * @return Translated string + */ + const char *get_translation(const char* msgid); +}; + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_WIDGET_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/print.cpp b/src/extension/print.cpp new file mode 100644 index 0000000..d9f7406 --- /dev/null +++ b/src/extension/print.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "implementation/implementation.h" +#include "print.h" + +/* Inkscape::Extension::Print */ + +namespace Inkscape { +namespace Extension { + +Print::Print (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) + , base(nullptr) + , drawing(nullptr) + , root(nullptr) + , dkey(0) +{ +} + +Print::~Print () += default; + +bool +Print::check () +{ + return Extension::check(); +} + +unsigned int +Print::setup () +{ + return imp->setup(this); +} + +unsigned int +Print::set_preview () +{ + return imp->set_preview(this); +} + +unsigned int +Print::begin (SPDocument *doc) +{ + return imp->begin(this, doc); +} + +unsigned int +Print::finish () +{ + return imp->finish(this); +} + +unsigned int +Print::bind (const Geom::Affine &transform, float opacity) +{ + return imp->bind (this, transform, opacity); +} + +unsigned int +Print::release () +{ + return imp->release(this); +} + +unsigned int +Print::comment (char const *comment) +{ + return imp->comment(this, comment); +} + +unsigned int +Print::fill (Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return imp->fill (this, pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +Print::stroke (Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return imp->stroke (this, pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +Print::image (unsigned char *px, unsigned int w, unsigned int h, unsigned int rs, + const Geom::Affine &transform, const SPStyle *style) +{ + return imp->image (this, px, w, h, rs, transform, style); +} + +unsigned int +Print::text (char const *text, Geom::Point const &p, SPStyle const *style) +{ + return imp->text (this, text, p, style); +} + +bool +Print::textToPath () +{ + return imp->textToPath(this); +} + +//whether embed font in print output (EPS especially) +bool +Print::fontEmbedded () +{ + return imp->fontEmbedded(this); +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/print.h b/src/extension/print.h new file mode 100644 index 0000000..56ec367 --- /dev/null +++ b/src/extension/print.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould + * Abhishek Sharma + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_PRINT_H__ +#define INKSCAPE_EXTENSION_PRINT_H__ + +#include "extension.h" + +class SPItem; +class SPStyle; + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +namespace Extension { + +class Print : public Extension { + +public: /* TODO: These are public for the short term, but this should be fixed */ + SPItem *base; + Inkscape::Drawing *drawing; + Inkscape::DrawingItem *root; + unsigned int dkey; + +public: + Print(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Print() override; + + bool check() override; + + /* FALSE means user hit cancel */ + unsigned int setup (); + unsigned int set_preview (); + + unsigned int begin (SPDocument *doc); + unsigned int finish (); + + /* Rendering methods */ + unsigned int bind (Geom::Affine const &transform, + float opacity); + unsigned int release (); + unsigned int comment (const char * comment); + unsigned int fill (Geom::PathVector const &pathv, + Geom::Affine const &ctm, + SPStyle const *style, + Geom::OptRect const &pbox, + Geom::OptRect const &dbox, + Geom::OptRect const &bbox); + unsigned int stroke (Geom::PathVector const &pathv, + Geom::Affine const &transform, + SPStyle const *style, + Geom::OptRect const &pbox, + Geom::OptRect const &dbox, + Geom::OptRect const &bbox); + unsigned int image (unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style); + unsigned int text (char const *text, + Geom::Point const &p, + SPStyle const *style); + bool textToPath (); + bool fontEmbedded (); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_PRINT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/system.cpp b/src/extension/system.cpp new file mode 100644 index 0000000..08de0b4 --- /dev/null +++ b/src/extension/system.cpp @@ -0,0 +1,734 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is file is kind of the junk file. Basically everything that + * didn't fit in one of the other well defined areas, well, it's now + * here. Which is good in someways, but this file really needs some + * definition. Hopefully that will come ASAP. + * + * Authors: + * Ted Gould + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006-2007 Johan Engelen + * Copyright (C) 2002-2004 Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/interface.h" + +#include "system.h" +#include "preferences.h" +#include "extension.h" +#include "db.h" +#include "input.h" +#include "output.h" +#include "effect.h" +#include "patheffect.h" +#include "print.h" +#include "implementation/script.h" +#include "implementation/xslt.h" +#include "xml/rebase-hrefs.h" +#include "io/sys.h" +#include "inkscape.h" +#include "document-undo.h" +#include "loader.h" + +#include + +namespace Inkscape { +namespace Extension { + +static void open_internal(Inkscape::Extension::Extension *in_plug, gpointer in_data); +static void save_internal(Inkscape::Extension::Extension *in_plug, gpointer in_data); + +/** + * \return A new document created from the filename passed in + * \brief This is a generic function to use the open function of + * a module (including Autodetect) + * \param key Identifier of which module to use + * \param filename The file that should be opened + * + * First things first, are we looking at an autodetection? Well if that's the case then the module + * needs to be found, and that is done with a database lookup through the module DB. The foreach + * function is called, with the parameter being a gpointer array. It contains both the filename + * (to find its extension) and where to write the module when it is found. + * + * If there is no autodetection, then the module database is queried with the key given. + * + * If everything is cool at this point, the module is loaded, and there is possibility for + * preferences. If there is a function, then it is executed to get the dialog to be displayed. + * After it is finished the function continues. + * + * Lastly, the open function is called in the module itself. + */ +SPDocument *open(Extension *key, gchar const *filename) +{ + Input *imod = nullptr; + + if (key == nullptr) { + gpointer parray[2]; + parray[0] = (gpointer)filename; + parray[1] = (gpointer)&imod; + db.foreach(open_internal, (gpointer)&parray); + } else { + imod = dynamic_cast(key); + } + + bool last_chance_svg = false; + if (key == nullptr && imod == nullptr) { + last_chance_svg = true; + imod = dynamic_cast(db.get(SP_MODULE_KEY_INPUT_SVG)); + } + + if (imod == nullptr) { + throw Input::no_extension_found(); + } + + // Hide pixbuf extensions depending on user preferences. + //g_warning("Extension: %s", imod->get_id()); + + bool show = true; + if (strlen(imod->get_id()) > 21) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool ask = prefs->getBool("/dialogs/import/ask"); + bool ask_svg = prefs->getBool("/dialogs/import/ask_svg"); + Glib::ustring id = Glib::ustring(imod->get_id(), 22); + if (id.compare("org.inkscape.input.svg") == 0) { + if (ask_svg && prefs->getBool("/options/onimport", false)) { + show = true; + imod->set_gui(true); + } else { + show = false; + imod->set_gui(false); + } + } else if(strlen(imod->get_id()) > 27) { + id = Glib::ustring(imod->get_id(), 28); + if (!ask && id.compare( "org.inkscape.input.gdkpixbuf") == 0) { + show = false; + imod->set_gui(false); + } + } + } + imod->set_state(Extension::STATE_LOADED); + + if (!imod->loaded()) { + throw Input::open_failed(); + } + + if (!imod->prefs(filename)) { + throw Input::open_cancelled(); + } + + SPDocument *doc = imod->open(filename); + + if (!doc) { + throw Input::open_failed(); + } + + if (last_chance_svg) { + if ( INKSCAPE.use_gui() ) { + sp_ui_error_dialog(_("Format autodetect failed. The file is being opened as SVG.")); + } else { + g_warning("%s", _("Format autodetect failed. The file is being opened as SVG.")); + } + } + + doc->setDocumentUri(filename); + if (!show) { + imod->set_gui(true); + } + + return doc; +} + +/** + * \return none + * \brief This is the function that searches each module to see + * if it matches the filename for autodetection. + * \param in_plug The module to be tested + * \param in_data An array of pointers containing the filename, and + * the place to put a successfully found module. + * + * Basically this function only looks at input modules as it is part of the open function. If the + * module is an input module, it then starts to take it apart, and the data that is passed in. + * Because the data being passed in is in such a weird format, there are a few casts to make it + * easier to use. While it looks like a lot of local variables, they'll all get removed by the + * compiler. + * + * First thing that is checked is if the filename is shorter than the extension itself. There is + * no way for a match in that case. If it's long enough then there is a string compare of the end + * of the filename (for the length of the extension), and the extension itself. If this passes + * then the pointer passed in is set to the current module. + */ +static void +open_internal(Extension *in_plug, gpointer in_data) +{ + if (!in_plug->deactivated() && dynamic_cast(in_plug)) { + gpointer *parray = (gpointer *)in_data; + gchar const *filename = (gchar const *)parray[0]; + Input **pimod = (Input **)parray[1]; + + // skip all the rest if we already found a function to open it + // since they're ordered by preference now. + if (!*pimod) { + gchar const *ext = dynamic_cast(in_plug)->get_extension(); + + gchar *filenamelower = g_utf8_strdown(filename, -1); + gchar *extensionlower = g_utf8_strdown(ext, -1); + + if (g_str_has_suffix(filenamelower, extensionlower)) { + *pimod = dynamic_cast(in_plug); + } + + g_free(filenamelower); + g_free(extensionlower); + } + } + + return; +} + +/** + * \return None + * \brief This is a generic function to use the save function of + * a module (including Autodetect) + * \param key Identifier of which module to use + * \param doc The document to be saved + * \param filename The file that the document should be saved to + * \param official (optional) whether to set :output_module and :modified in the + * document; is true for normal save, false for temporary saves + * + * First things first, are we looking at an autodetection? Well if that's the case then the module + * needs to be found, and that is done with a database lookup through the module DB. The foreach + * function is called, with the parameter being a gpointer array. It contains both the filename + * (to find its extension) and where to write the module when it is found. + * + * If there is no autodetection the module database is queried with the key given. + * + * If everything is cool at this point, the module is loaded, and there is possibility for + * preferences. If there is a function, then it is executed to get the dialog to be displayed. + * After it is finished the function continues. + * + * Lastly, the save function is called in the module itself. + */ +void +save(Extension *key, SPDocument *doc, gchar const *filename, bool setextension, bool check_overwrite, bool official, + Inkscape::Extension::FileSaveMethod save_method) +{ + Output *omod; + if (key == nullptr) { + gpointer parray[2]; + parray[0] = (gpointer)filename; + parray[1] = (gpointer)&omod; + omod = nullptr; + db.foreach(save_internal, (gpointer)&parray); + + /* This is a nasty hack, but it is required to ensure that + autodetect will always save with the Inkscape extensions + if they are available. */ + if (omod != nullptr && !strcmp(omod->get_id(), SP_MODULE_KEY_OUTPUT_SVG)) { + omod = dynamic_cast(db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE)); + } + /* If autodetect fails, save as Inkscape SVG */ + if (omod == nullptr) { + // omod = dynamic_cast(db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE)); use exception and let user choose + } + } else { + omod = dynamic_cast(key); + } + + if (!dynamic_cast(omod)) { + g_warning("Unable to find output module to handle file: %s\n", filename); + throw Output::no_extension_found(); + } + + omod->set_state(Extension::STATE_LOADED); + if (!omod->loaded()) { + throw Output::save_failed(); + } + + if (!omod->prefs()) { + throw Output::save_cancelled(); + } + + gchar *fileName = nullptr; + if (setextension) { + gchar *lowerfile = g_utf8_strdown(filename, -1); + gchar *lowerext = g_utf8_strdown(omod->get_extension(), -1); + + if (!g_str_has_suffix(lowerfile, lowerext)) { + fileName = g_strdup_printf("%s%s", filename, omod->get_extension()); + } + + g_free(lowerfile); + g_free(lowerext); + } + + if (fileName == nullptr) { + fileName = g_strdup(filename); + } + + if (check_overwrite && !sp_ui_overwrite_file(fileName)) { + g_free(fileName); + throw Output::no_overwrite(); + } + + // test if the file exists and is writable + // the test only checks the file attributes and might pass where ACL does not allow writes + if (Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS) && !Inkscape::IO::file_is_writable(filename)) { + g_free(fileName); + throw Output::file_read_only(); + } + + Inkscape::XML::Node *repr = doc->getReprRoot(); + + + // remember attributes in case this is an unofficial save and/or overwrite fails + gchar *saved_uri = g_strdup(doc->getDocumentURI()); + gchar *saved_output_extension = nullptr; + gchar *saved_dataloss = nullptr; + bool saved_modified = doc->isModifiedSinceSave(); + saved_output_extension = g_strdup(get_file_save_extension(save_method).c_str()); + saved_dataloss = g_strdup(repr->attribute("inkscape:dataloss")); + if (official) { + // The document is changing name/uri. + doc->changeUriAndHrefs(fileName); + } + + // Update attributes: + { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + // also save the extension for next use + store_file_extension_in_prefs (omod->get_id(), save_method); + // set the "dataloss" attribute if the chosen extension is lossy + repr->removeAttribute("inkscape:dataloss"); + if (omod->causes_dataloss()) { + repr->setAttribute("inkscape:dataloss", "true"); + } + } + doc->setModifiedSinceSave(false); + } + + try { + omod->save(doc, fileName); + } + catch(...) { + // revert attributes in case of official and overwrite + if(check_overwrite && official) { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + store_file_extension_in_prefs (saved_output_extension, save_method); + repr->setAttribute("inkscape:dataloss", saved_dataloss); + } + doc->changeUriAndHrefs(saved_uri); + } + doc->setModifiedSinceSave(saved_modified); + // free used resources + g_free(saved_output_extension); + g_free(saved_dataloss); + g_free(saved_uri); + + g_free(fileName); + + throw; + } + + // If it is an unofficial save, set the modified attributes back to what they were. + if ( !official) { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + store_file_extension_in_prefs (saved_output_extension, save_method); + repr->setAttribute("inkscape:dataloss", saved_dataloss); + } + doc->setModifiedSinceSave(saved_modified); + + g_free(saved_output_extension); + g_free(saved_dataloss); + } + + g_free(fileName); + return; +} + +/** + * \return none + * \brief This is the function that searches each module to see + * if it matches the filename for autodetection. + * \param in_plug The module to be tested + * \param in_data An array of pointers containing the filename, and + * the place to put a successfully found module. + * + * Basically this function only looks at output modules as it is part of the open function. If the + * module is an output module, it then starts to take it apart, and the data that is passed in. + * Because the data being passed in is in such a weird format, there are a few casts to make it + * easier to use. While it looks like a lot of local variables, they'll all get removed by the + * compiler. + * + * First thing that is checked is if the filename is shorter than the extension itself. There is + * no way for a match in that case. If it's long enough then there is a string compare of the end + * of the filename (for the length of the extension), and the extension itself. If this passes + * then the pointer passed in is set to the current module. + */ +static void +save_internal(Extension *in_plug, gpointer in_data) +{ + if (!in_plug->deactivated() && dynamic_cast(in_plug)) { + gpointer *parray = (gpointer *)in_data; + gchar const *filename = (gchar const *)parray[0]; + Output **pomod = (Output **)parray[1]; + + // skip all the rest if we already found someone to save it + // since they're ordered by preference now. + if (!*pomod) { + gchar const *ext = dynamic_cast(in_plug)->get_extension(); + + gchar *filenamelower = g_utf8_strdown(filename, -1); + gchar *extensionlower = g_utf8_strdown(ext, -1); + + if (g_str_has_suffix(filenamelower, extensionlower)) { + *pomod = dynamic_cast(in_plug); + } + + g_free(filenamelower); + g_free(extensionlower); + } + } + + return; +} + +Print * +get_print(gchar const *key) +{ + return dynamic_cast(db.get(key)); +} + +/** + * \return true if extension successfully parsed, false otherwise + * A true return value does not guarantee an extension was actually registered, + * but indicates no errors occurred while parsing the extension. + * \brief Creates a module from a Inkscape::XML::Document describing the module + * \param doc The XML description of the module + * + * This function basically has two segments. The first is that it goes through the Repr tree + * provided, and determines what kind of module this is, and what kind of implementation to use. + * All of these are then stored in two enums that are defined in this function. This makes it + * easier to add additional types (which will happen in the future, I'm sure). + * + * Second, there is case statements for these enums. The first one is the type of module. This is + * the one where the module is actually created. After that, then the implementation is applied to + * get the load and unload functions. If there is no implementation then these are not set. This + * case could apply to modules that are built in (like the SVG load/save functions). + */ +bool +build_from_reprdoc(Inkscape::XML::Document *doc, Implementation::Implementation *in_imp, std::string* baseDir) +{ + enum { + MODULE_EXTENSION, + MODULE_XSLT, + MODULE_PLUGIN, + MODULE_UNKNOWN_IMP + } module_implementation_type = MODULE_UNKNOWN_IMP; + enum { + MODULE_INPUT, + MODULE_OUTPUT, + MODULE_FILTER, + MODULE_PRINT, + MODULE_PATH_EFFECT, + MODULE_UNKNOWN_FUNC + } module_functional_type = MODULE_UNKNOWN_FUNC; + + g_return_val_if_fail(doc != nullptr, false); + + Inkscape::XML::Node *repr = doc->root(); + + if (strcmp(repr->name(), INKSCAPE_EXTENSION_NS "inkscape-extension")) { + g_warning("Extension definition started with <%s> instead of <" INKSCAPE_EXTENSION_NS "inkscape-extension>. Extension will not be created. See http://wiki.inkscape.org/wiki/index.php/Extensions for reference.\n", repr->name()); + return false; + } + + Inkscape::XML::Node *child_repr = repr->firstChild(); + while (child_repr != nullptr) { + char const *element_name = child_repr->name(); + /* printf("Child: %s\n", child_repr->name()); */ + if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "input")) { + module_functional_type = MODULE_INPUT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "output")) { + module_functional_type = MODULE_OUTPUT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "effect")) { + module_functional_type = MODULE_FILTER; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "print")) { + module_functional_type = MODULE_PRINT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "path-effect")) { + module_functional_type = MODULE_PATH_EFFECT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "script")) { + module_implementation_type = MODULE_EXTENSION; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "xslt")) { + module_implementation_type = MODULE_XSLT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "plugin")) { + module_implementation_type = MODULE_PLUGIN; + } + + //Inkscape::XML::Node *old_repr = child_repr; + child_repr = child_repr->next(); + //Inkscape::GC::release(old_repr); + } + + Implementation::Implementation *imp; + if (in_imp == nullptr) { + switch (module_implementation_type) { + case MODULE_EXTENSION: { + Implementation::Script *script = new Implementation::Script(); + imp = static_cast(script); + break; + } + case MODULE_XSLT: { + Implementation::XSLT *xslt = new Implementation::XSLT(); + imp = static_cast(xslt); + break; + } + case MODULE_PLUGIN: { + Inkscape::Extension::Loader loader = Inkscape::Extension::Loader(); + if( baseDir != nullptr){ + loader.set_base_directory ( *baseDir ); + } + imp = loader.load_implementation(doc); + break; + } + default: { + imp = nullptr; + break; + } + } + } else { + imp = in_imp; + } + + Extension *module = nullptr; + try { + switch (module_functional_type) { + case MODULE_INPUT: { + module = new Input(repr, imp, baseDir); + break; + } + case MODULE_OUTPUT: { + module = new Output(repr, imp, baseDir); + break; + } + case MODULE_FILTER: { + module = new Effect(repr, imp, baseDir); + break; + } + case MODULE_PRINT: { + module = new Print(repr, imp, baseDir); + break; + } + case MODULE_PATH_EFFECT: { + module = new PathEffect(repr, imp, baseDir); + break; + } + default: { + g_warning("Extension of unknown type!"); // TODO: Should not happen! Is this even useful? + module = new Extension(repr, imp, baseDir); + break; + } + } + } catch (const Extension::extension_no_id& e) { + g_warning("Building extension failed. Extension does not have a valid ID"); + } catch (const Extension::extension_no_name& e) { + g_warning("Building extension failed. Extension does not have a valid name"); + } catch (const Extension::extension_not_compatible& e) { + return true; // This is not an actual error; just silently ignore the extension + } + + if (module) { + return true; + } + + return false; +} + +/** + * \brief This function creates a module from a filename of an + * XML description. + * \param filename The file holding the XML description of the module. + * + * This function calls build_from_reprdoc with using sp_repr_read_file to create the reprdoc. + */ +void +build_from_file(gchar const *filename) +{ + std::string dir = Glib::path_get_dirname(filename); + + Inkscape::XML::Document *doc = sp_repr_read_file(filename, INKSCAPE_EXTENSION_URI); + if (!doc) { + g_critical("Inkscape::Extension::build_from_file() - XML description loaded from '%s' not valid.", filename); + return; + } + + if (!build_from_reprdoc(doc, nullptr, &dir)) { + g_warning("Inkscape::Extension::build_from_file() - Could not parse extension from '%s'.", filename); + } + + Inkscape::GC::release(doc); +} + +/** + * \brief This function creates a module from a buffer holding an + * XML description. + * \param buffer The buffer holding the XML description of the module. + * + * This function calls build_from_reprdoc with using sp_repr_read_mem to create the reprdoc. It + * finds the length of the buffer using strlen. + */ +void +build_from_mem(gchar const *buffer, Implementation::Implementation *in_imp) +{ + Inkscape::XML::Document *doc = sp_repr_read_mem(buffer, strlen(buffer), INKSCAPE_EXTENSION_URI); + if (!doc) { + g_critical("Inkscape::Extension::build_from_mem() - XML description loaded from memory buffer not valid."); + return; + } + + if (!build_from_reprdoc(doc, in_imp, nullptr)) { + g_critical("Inkscape::Extension::build_from_mem() - Could not parse extension from memory buffer."); + } + + Inkscape::GC::release(doc); +} + +/* + * TODO: Is it guaranteed that the returned extension is valid? If so, we can remove the check for + * filename_extension in sp_file_save_dialog(). + */ +Glib::ustring +get_file_save_extension (Inkscape::Extension::FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring extension; + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + extension = prefs->getString("/dialogs/save_as/default"); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + extension = prefs->getString("/dialogs/save_copy/default"); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + extension = SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE; + break; + case FILE_SAVE_METHOD_EXPORT: + /// \todo no default extension set for Export? defaults to SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE is ok? + break; + } + + if(extension.empty()) { + extension = SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE; + } + + return extension; +} + +Glib::ustring +get_file_save_path (SPDocument *doc, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring path; + bool use_current_dir = true; + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + { + use_current_dir = prefs->getBool("/dialogs/save_as/use_current_dir", true); + if (doc->getDocumentURI() && use_current_dir) { + path = Glib::path_get_dirname(doc->getDocumentURI()); + } else { + path = prefs->getString("/dialogs/save_as/path"); + } + break; + } + case FILE_SAVE_METHOD_TEMPORARY: + path = prefs->getString("/dialogs/save_as/path"); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + use_current_dir = prefs->getBool("/dialogs/save_copy/use_current_dir", prefs->getBool("/dialogs/save_as/use_current_dir", true)); + if (doc->getDocumentURI() && use_current_dir) { + path = Glib::path_get_dirname(doc->getDocumentURI()); + } else { + path = prefs->getString("/dialogs/save_copy/path"); + } + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + if (doc->getDocumentURI()) { + path = Glib::path_get_dirname(doc->getDocumentURI()); + } else { + // FIXME: should we use the save_as path here or something else? Maybe we should + // leave this as a choice to the user. + path = prefs->getString("/dialogs/save_as/path"); + } + break; + case FILE_SAVE_METHOD_EXPORT: + /// \todo no default path set for Export? + // defaults to g_get_home_dir() + break; + } + + if(path.empty()) { + path = g_get_home_dir(); // Is this the most sensible solution? Note that we should avoid + // g_get_current_dir because this leads to problems on OS X where + // Inkscape opens the dialog inside application bundle when it is + // invoked for the first teim. + } + + return path; +} + +void +store_file_extension_in_prefs (Glib::ustring extension, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + prefs->setString("/dialogs/save_as/default", extension); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + prefs->setString("/dialogs/save_copy/default", extension); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + case FILE_SAVE_METHOD_EXPORT: + // do nothing + break; + } +} + +void +store_save_path_in_prefs (Glib::ustring path, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + prefs->setString("/dialogs/save_as/path", path); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + prefs->setString("/dialogs/save_copy/path", path); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + case FILE_SAVE_METHOD_EXPORT: + // do nothing + break; + } +} + +} } /* namespace Inkscape::Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/system.h b/src/extension/system.h new file mode 100644 index 0000000..ea70a0f --- /dev/null +++ b/src/extension/system.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is file is kind of the junk file. Basically everything that + * didn't fit in one of the other well defined areas, well, it's now + * here. Which is good in someways, but this file really needs some + * definition. Hopefully that will come ASAP. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_SYSTEM_H__ +#define INKSCAPE_EXTENSION_SYSTEM_H__ + +#include + +class SPDocument; + +namespace Inkscape { + +namespace Extension { +class Extension; +class Print; + +namespace Implementation { +class Implementation; +} + +/** + * Used to distinguish between the various invocations of the save dialogs (and thus to determine + * the file type and save path offered in the dialog) + */ +enum FileSaveMethod { + FILE_SAVE_METHOD_SAVE_AS, + FILE_SAVE_METHOD_SAVE_COPY, + FILE_SAVE_METHOD_EXPORT, + // Fallback for special cases (e.g., when saving a document for the first time or after saving + // it in a lossy format) + FILE_SAVE_METHOD_INKSCAPE_SVG, + // For saving temporary files; we return the same data as for FILE_SAVE_METHOD_SAVE_AS + FILE_SAVE_METHOD_TEMPORARY, +}; + +SPDocument *open(Extension *key, gchar const *filename); +void save(Extension *key, SPDocument *doc, gchar const *filename, + bool setextension, bool check_overwrite, bool official, + Inkscape::Extension::FileSaveMethod save_method); +Print *get_print(gchar const *key); +void build_from_file(gchar const *filename); +void build_from_mem(gchar const *buffer, Implementation::Implementation *in_imp); + +/** + * Determine the desired default file extension depending on the given file save method. + * The returned string is guaranteed to be non-empty. + * + * @param method the file save method of the dialog + * @return the corresponding default file extension + */ +Glib::ustring get_file_save_extension (FileSaveMethod method); + +/** + * Determine the desired default save path depending on the given FileSaveMethod. + * The returned string is guaranteed to be non-empty. + * + * @param method the file save method of the dialog + * @param doc the file's document + * @return the corresponding default save path + */ +Glib::ustring get_file_save_path (SPDocument *doc, FileSaveMethod method); + +/** + * Write the given file extension back to prefs so that it can be used later on. + * + * @param extension the file extension which should be written to prefs + * @param method the file save method of the dialog + */ +void store_file_extension_in_prefs (Glib::ustring extension, FileSaveMethod method); + +/** + * Write the given path back to prefs so that it can be used later on. + * + * @param path the path which should be written to prefs + * @param method the file save method of the dialog + */ +void store_save_path_in_prefs (Glib::ustring path, FileSaveMethod method); + +} } /* namespace Inkscape::Extension */ + +#endif /* INKSCAPE_EXTENSION_SYSTEM_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/timer.cpp b/src/extension/timer.cpp new file mode 100644 index 0000000..7ed856e --- /dev/null +++ b/src/extension/timer.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Here is where the extensions can get timed on when they load and + * unload. All of the timing is done in here. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "extension.h" +#include "timer.h" + +namespace Inkscape { +namespace Extension { + +#define TIMER_SCALE_VALUE 20 + +ExpirationTimer * ExpirationTimer::timer_list = nullptr; +ExpirationTimer * ExpirationTimer::idle_start = nullptr; +long ExpirationTimer::timeout = 240; +bool ExpirationTimer::timer_started = false; + +/** \brief Create a new expiration timer + \param in_extension Which extension this timer is related to + + This function creates the timer, and sets the time to the current + time, plus what ever the current timeout is. Also, if this is + the first timer extension, the timer is kicked off. This function + also sets up the circularly linked list of all the timers. +*/ +ExpirationTimer::ExpirationTimer (Extension * in_extension): + locked(0), + extension(in_extension) +{ + /* Fix Me! */ + if (timer_list == nullptr) { + next = this; + timer_list = this; + } else { + next = timer_list->next; + timer_list->next = this; + } + + expiration.assign_current_time(); + expiration += timeout; + + if (!timer_started) { + Glib::signal_timeout().connect(sigc::ptr_fun(&timer_func), timeout * 1000 / TIMER_SCALE_VALUE); + timer_started = true; + } + + return; +} + +/** \brief Deletes a \c ExpirationTimer + + The most complex thing that this function does is remove the timer + from the circularly linked list. If this is the only entry in the + list that is easy, otherwise all the entries must be found, and this + one removed from the list. +*/ +ExpirationTimer::~ExpirationTimer() +{ + if (this != next) { + /* This will remove this entry from the circularly linked + list. */ + ExpirationTimer * prev; + for (prev = timer_list; + prev->next != this; + prev = prev->next){}; + prev->next = next; + + if (idle_start == this) + idle_start = next; + + /* This may occur more than you think, just because the guy + doing most of the deletions is the idle function, who tracks + where it is looking using the \c timer_list variable. */ + if (timer_list == this) + timer_list = next; + } else { + /* If we're the only entry in the list, the list needs to go + to NULL */ + timer_list = nullptr; + idle_start = nullptr; + } + + return; +} + +/** \brief Touches the timer to extend the length before it expires + + Basically it adds more time to the timer. One thing that is kinda + tricky is that it adds half the time remaining back into the timer. + This allows for some extensions that are used regularly to having + extended expiration times. So, in the end, they stay loaded longer. + Extensions that are only used once will expire at a standard rate + set by \c timeout. +*/ +void +ExpirationTimer::touch () +{ + Glib::TimeVal current; + current.assign_current_time(); + + long time_left = (long)(expiration.as_double() - current.as_double()); + if (time_left < 0) time_left = 0; + time_left /= 2; + + expiration = current + timeout + time_left; + return; +} + +/** \brief Check to see if the timer has expired + + Checks the time against the current time. +*/ +bool +ExpirationTimer::expired () const +{ + if (locked > 0) return false; + + Glib::TimeVal current; + current.assign_current_time(); + return expiration < current; +} + +// int idle_cnt = 0; + +/** \brief This function goes in the idle loop to find expired extensions + \return Whether the function should be requeued or not + + This function first insures that there is a timer list, and then checks + to see if the one on the top of the list has expired. If it has + expired it unloads the module. By unloading the module, the timer + gets deleted (happens in the unload function). If the list is + no empty, the function returns that it should be dequeued and sets + the \c timer_started variable so that the timer will be reissued when + a timer is added. If there is entries left, but the next one is + where this function started, then the timer is set up. The timer + will then re-add the idle loop function when it runs. +*/ +bool +ExpirationTimer::idle_func () +{ + // std::cout << "Idle func pass: " << idle_cnt++ << " timer list: " << timer_list << std::endl; + + /* see if this is the last */ + if (timer_list == nullptr) { + timer_started = false; + return false; + } + + /* evaluate current */ + if (timer_list->expired()) { + timer_list->extension->set_state(Extension::STATE_UNLOADED); + } + + /* see if this is the last */ + if (timer_list == nullptr) { + timer_started = false; + return false; + } + + if (timer_list->next == idle_start) { + /* if so, set up the timer and return FALSE */ + /* Note: This may cause one to be missed on the evaluation if + the one before it expires and it is last in the list. + While this could be taken care of, it isn't worth the + complexity for this lazy removal that we're doing. It + should get picked up next time */ + Glib::signal_timeout().connect(sigc::ptr_fun(&timer_func), timeout * 1000 / TIMER_SCALE_VALUE); + return false; + } + + /* If nothing else, continue on */ + timer_list = timer_list->next; + return true; +} + +/** \brief A timer function to set up the idle function + \return Always false -- to disable the timer + + This function sets up the idle loop when it runs. The idle loop is + the one that unloads all the extensions. +*/ +bool +ExpirationTimer::timer_func () +{ + // std::cout << "Timer func" << std::endl; + idle_start = timer_list; + // idle_cnt = 0; + Glib::signal_idle().connect(sigc::ptr_fun(&idle_func)); + return false; +} + +}; }; /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/timer.h b/src/extension/timer.h new file mode 100644 index 0000000..a4f9bfe --- /dev/null +++ b/src/extension/timer.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Here is where the extensions can get timed on when they load and + * unload. All of the timing is done in here. + * + * Authors: + * Ted Gould + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_TIMER_H__ +#define INKSCAPE_EXTENSION_TIMER_H__ + +#include +#include +#include + +namespace Inkscape { +namespace Extension { + +class Extension; + +class ExpirationTimer { + /** \brief Circularly linked list of all timers */ + static ExpirationTimer * timer_list; + /** \brief Which timer was on top when we started the idle loop */ + static ExpirationTimer * idle_start; + /** \brief What the current timeout is */ + static long timeout; + /** \brief Has the timer been started? */ + static bool timer_started; + + /** \brief Is this extension locked from being unloaded? */ + int locked; + /** \brief Next entry in the list */ + ExpirationTimer * next; + /** \brief When this timer expires */ + Glib::TimeVal expiration; + /** \brief What extension this function relates to */ + Extension * extension; + + bool expired() const; + + static bool idle_func (); + static bool timer_func (); + +public: + ExpirationTimer(Extension * in_extension); + virtual ~ExpirationTimer(); + + void touch (); + void lock () { locked++; }; + void unlock () { locked--; }; + + /** \brief Set the timeout variable */ + static void set_timeout (long in_seconds) { timeout = in_seconds; }; +}; + +}; }; /* namespace Inkscape, Extension */ + +#endif /* INKSCAPE_EXTENSION_TIMER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extract-uri.cpp b/src/extract-uri.cpp new file mode 100644 index 0000000..65401df --- /dev/null +++ b/src/extract-uri.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include + +#include "extract-uri.h" + +// FIXME: kill this ugliness when we have a proper CSS parser + +// Functions as per 4.3.4 of CSS 2.1 +// http://www.w3.org/TR/CSS21/syndata.html#uri +std::string extract_uri(char const *s, char const **endptr) +{ + std::string result; + + if (!s) + return result; + + gchar const *sb = s; + if ( strlen(sb) < 4 || strncmp(sb, "url", 3) != 0 ) { + return result; + } + + sb += 3; + + if ( endptr ) { + *endptr = nullptr; + } + + // This first whitespace technically is not allowed. + // Just left in for now for legacy behavior. + while ( ( *sb == ' ' ) || + ( *sb == '\t' ) ) + { + sb++; + } + + if ( *sb == '(' ) { + sb++; + while ( ( *sb == ' ' ) || + ( *sb == '\t' ) ) + { + sb++; + } + + gchar delim = ')'; + if ( (*sb == '\'' || *sb == '"') ) { + delim = *sb; + sb++; + } + + if (!*sb) { + return result; + } + + gchar const* se = sb; + while ( *se && (*se != delim) ) { + se++; + } + + // we found the delimiter + if ( *se ) { + if ( delim == ')' ) { + if ( endptr ) { + *endptr = se + 1; + } + + // back up for any trailing whitespace + while (se > sb && g_ascii_isspace(se[-1])) + { + se--; + } + + result = std::string(sb, se); + } else { + gchar const* tail = se + 1; + while ( ( *tail == ' ' ) || + ( *tail == '\t' ) ) + { + tail++; + } + if ( *tail == ')' ) { + if ( endptr ) { + *endptr = tail + 1; + } + result = std::string(sb, se); + } + } + } + } + + return result; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extract-uri.h b/src/extract-uri.h new file mode 100644 index 0000000..ee773f4 --- /dev/null +++ b/src/extract-uri.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_EXTRACT_URI_H +#define SEEN_EXTRACT_URI_H + +#include + +/** + * Parse functional URI notation, as per 4.3.4 of CSS 2.1 + * + * http://www.w3.org/TR/CSS21/syndata.html#uri + * + * > The format of a URI value is 'url(' followed by optional white space + * > followed by an optional single quote (') or double quote (") character + * > followed by the URI itself, followed by an optional single quote (') + * > or double quote (") character followed by optional white space + * > followed by ')'. The two quote characters must be the same. + * + * Example: + * \verbatim + url = extract_uri("url('foo')bar", &out); + -> url == "foo" + -> out == "bar" + \endverbatim + * + * @param s String which starts with "url(" + * @param[out] endptr points to \c s + N, where N is the number of characters parsed + * @return URL string, or empty string on failure + */ +std::string extract_uri(char const *s, char const **endptr = nullptr); + + +#endif /* !SEEN_EXTRACT_URI_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/file-update.cpp b/src/file-update.cpp new file mode 100644 index 0000000..e3d2e7c --- /dev/null +++ b/src/file-update.cpp @@ -0,0 +1,643 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** + * @file-update + * Operations to bump files from the pre-0.92 era into the 0.92+ era + * (90dpi vs 96dpi, line height problems, and related bugs) + */ +/* Authors: + * Tavmjong Bah + * Marc Jeanmougin + * su_v + */ +#include +#include +#include + +#include + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "file.h" +#include "inkscape.h" +#include "message-stack.h" +#include "message.h" +#include "preferences.h" +#include "print.h" +#include "proj_pt.h" +#include "selection-chemistry.h" +#include "text-editing.h" +#include "verbs.h" + +#include "display/canvas-grid.h" + +#include "extension/effect.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "extension/system.h" + +#include "io/dir-util.h" +#include "io/sys.h" + +#include "object/persp3d.h" +#include "object/sp-defs.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-guide.h" +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "style.h" + +#include "ui/shape-editor.h" + + +using Inkscape::DocumentUndo; + +int sp_file_convert_dpi_method_commandline = -1; // Unset + +bool is_line(SPObject *i) +{ + if (!(i->getAttribute("sodipodi:role"))) + return false; + return !strcmp(i->getAttribute("sodipodi:role"), "line"); +} + + +void fix_blank_line(SPObject *o) +{ + if (SP_IS_TEXT(o)) + ((SPText *)o)->rebuildLayout(); + else if (SP_IS_FLOWTEXT(o)) + ((SPFlowtext *)o)->rebuildLayout(); + + SPIFontSize fontsize = o->style->font_size; + SPILengthOrNormal lineheight = o->style->line_height; + std::vector cl = o->childList(false); + bool beginning = true; + for (std::vector::const_iterator ci = cl.begin(); ci != cl.end(); ++ci) { + SPObject *i = *ci; + if ((SP_IS_TSPAN(i) && is_line(i)) || SP_IS_FLOWPARA(i) || SP_IS_FLOWDIV(i)) { + if (sp_text_get_length((SPItem *)i) <= 1) { // empty line + Inkscape::Text::Layout::iterator pos = te_get_layout((SPItem*)(o))->charIndexToIterator( + ((SP_IS_FLOWPARA(i) || SP_IS_FLOWDIV(i))?0:((ci==cl.begin())?0:1)) + sp_text_get_length_upto(o,i) ); + sp_te_insert((SPItem *)o, pos, "\u00a0"); //"\u00a0" + gchar *l = g_strdup_printf("%f", lineheight.value); + gchar *f = g_strdup_printf("%f", fontsize.value); + i->style->line_height.readIfUnset(l); + if (!beginning) + i->style->font_size.read(f); + else + i->style->font_size.readIfUnset(f); + g_free(l); + g_free(f); + } else { + beginning = false; + fontsize = i->style->font_size; + lineheight = o->style->line_height; + } + } + } +} + +void fix_line_spacing(SPObject *o) +{ + SPILengthOrNormal lineheight = o->style->line_height; + bool inner = false; + std::vector cl = o->childList(false); + for (auto i : cl) { + if ((SP_IS_TSPAN(i) && is_line(i)) || SP_IS_FLOWPARA(i) || SP_IS_FLOWDIV(i)) { + // if no line-height attribute, set it + gchar *l = g_strdup_printf("%f", lineheight.value); + i->style->line_height.readIfUnset(l); + g_free(l); + } + inner = true; + } + if (inner) { + if (SP_IS_TEXT(o)) { + o->style->line_height.read("0.00%"); + } else { + o->style->line_height.read("0.01%"); + } + } +} + +void fix_font_name(SPObject *o) +{ + std::vector cl = o->childList(false); + for (auto ci : cl) + fix_font_name(ci); + std::string prev = o->style->font_family.value(); + if (prev == "Sans") + o->style->font_family.read("sans-serif"); + else if (prev == "Serif") + o->style->font_family.read("serif"); + else if (prev == "Monospace") + o->style->font_family.read("monospace"); +} + + +void fix_font_size(SPObject *o) +{ + SPIFontSize fontsize = o->style->font_size; + if (!fontsize.set) + return; + bool inner = false; + std::vector cl = o->childList(false); + for (auto i : cl) { + fix_font_size(i); + if ((SP_IS_TSPAN(i) && is_line(i)) || SP_IS_FLOWPARA(i) || SP_IS_FLOWDIV(i)) { + inner = true; + gchar *s = g_strdup_printf("%f", fontsize.value); + if (fontsize.set) + i->style->font_size.readIfUnset(s); + g_free(s); + } + } + if (inner && (SP_IS_TEXT(o) || SP_IS_FLOWTEXT(o))) + o->style->font_size.clear(); +} + + + +// helper function +void sp_file_text_run_recursive(void (*f)(SPObject *), SPObject *o) +{ + if (SP_IS_TEXT(o) || SP_IS_FLOWTEXT(o)) + f(o); + else { + std::vector cl = o->childList(false); + for (auto ci : cl) + sp_file_text_run_recursive(f, ci); + } +} + +void fix_update(SPObject *o) { + o->style->write(); + o->updateRepr(); +} + +void sp_file_convert_text_baseline_spacing(SPDocument *doc) +{ + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC,"C"); + sp_file_text_run_recursive(fix_blank_line, doc->getRoot()); + sp_file_text_run_recursive(fix_line_spacing, doc->getRoot()); + sp_file_text_run_recursive(fix_font_size, doc->getRoot()); + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + sp_file_text_run_recursive(fix_update, doc->getRoot()); +} + + +/** + * Implements a fix for https://gitlab.com/inkscape/inkscape/-/issues/45 + * Line spacing for empty lines was handled differently before 1.0 + * and in particular with the first empty lines or with how style attributes + * are processed in empty lines (line = tspan with sodipodi:role="line") + * + * This function "fixes" a text element in a old document by removing the + * first empty lines and style attrs on other empty lines. + * + * */ +void _fix_pre_v1_empty_lines(SPObject *o) +{ + std::vector cl = o->childList(false); + bool begin = true; + std::string cur_y = ""; + for (auto ci : cl) { + if (!SP_IS_TSPAN(ci)) + continue; + if (!is_line(ci)) + continue; + if (!ci->childList(false).empty()) { + if (begin) + cur_y = ci->getAttribute("y") ? ci->getAttribute("y") : cur_y; + begin = false; + } else { + ci->removeAttribute("style"); + ci->updateRepr(); + if (begin) { + ci->deleteObject(); + } + } + if (cur_y != "") + o->setAttribute("y", cur_y); + } +} + + + +void sp_file_fix_empty_lines(SPDocument *doc) +{ + sp_file_text_run_recursive(_fix_pre_v1_empty_lines, doc->getRoot()); + sp_file_text_run_recursive(fix_update, doc->getRoot()); +} + + + +void sp_file_convert_font_name(SPDocument *doc) +{ + sp_file_text_run_recursive(fix_font_name, doc->getRoot()); + sp_file_text_run_recursive(fix_update, doc->getRoot()); +} + + +// Quick and dirty internal backup function +bool sp_file_save_backup( Glib::ustring uri ) { + + Glib::ustring out = uri; + out.insert(out.find(".svg"),"_backup"); + + FILE *filein = Inkscape::IO::fopen_utf8name(uri.c_str(), "rb"); + if (!filein) { + std::cerr << "sp_file_save_backup: failed to open: " << uri << std::endl; + return false; + } + + FILE *fileout = Inkscape::IO::fopen_utf8name(out.c_str(), "wb"); + if (!fileout) { + std::cerr << "sp_file_save_backup: failed to open: " << out << std::endl; + fclose( filein ); + return false; + } + + int ch; + while ((ch = fgetc(filein)) != EOF) { + fputc(ch, fileout); + } + fflush(fileout); + + bool return_value = true; + if (ferror(fileout)) { + std::cerr << "sp_file_save_backup: error when writing to: " << out << std::endl; + return_value = false; + } + + fclose(filein); + fclose(fileout); + + return return_value; +} + + +void sp_file_convert_dpi(SPDocument *doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPRoot *root = doc->getRoot(); + + // See if we need to offer the user a fix for the 90->96 px per inch change. + // std::cout << "SPFileOpen:" << std::endl; + // std::cout << " Version: " << sp_version_to_string(root->version.inkscape) << std::endl; + // std::cout << " SVG file from old Inkscape version detected: " + // << sp_version_to_string(root->version.inkscape) << std::endl; + static const double ratio = 90.0/96.0; + + bool need_fix_viewbox = false; + bool need_fix_units = false; + bool need_fix_guides = false; + bool need_fix_grid_mm = false; + bool need_fix_box3d = false; + bool did_scaling = false; + + // Check if potentially need viewbox or unit fix + switch (root->width.unit) { + case SVGLength::PC: + case SVGLength::PT: + case SVGLength::MM: + case SVGLength::CM: + case SVGLength::INCH: + need_fix_viewbox = true; + break; + case SVGLength::NONE: + case SVGLength::PX: + need_fix_units = true; + break; + case SVGLength::EM: + case SVGLength::EX: + case SVGLength::PERCENT: + // OK + break; + default: + std::cerr << "sp_file_convert_dpi: Unhandled width unit!" << std::endl; + } + + switch (root->height.unit) { + case SVGLength::PC: + case SVGLength::PT: + case SVGLength::MM: + case SVGLength::CM: + case SVGLength::INCH: + need_fix_viewbox = true; + break; + case SVGLength::NONE: + case SVGLength::PX: + need_fix_units = true; + break; + case SVGLength::EM: + case SVGLength::EX: + case SVGLength::PERCENT: + // OK + break; + default: + std::cerr << "sp_file_convert_dpi: Unhandled height unit!" << std::endl; + } + + if (need_fix_units && need_fix_viewbox) { + std::cerr << "Different units in document size !" << std::endl; + if (root->viewBox_set) + need_fix_viewbox = false; + else + need_fix_units = false; + } + + // std::cout << "Absolute SVG units in root? " << (need_fix_viewbox?"true":"false") << std::endl; + // std::cout << "User units in root? " << (need_fix_units ?"true":"false") << std::endl; + + if ((!root->viewBox_set && need_fix_viewbox) || need_fix_units) { + int response = FILE_DPI_UNCHANGED; // default + + /******** UI *******/ + bool backup = prefs->getBool("/options/dpifixbackup", true); + if (INKSCAPE.use_gui() && sp_file_convert_dpi_method_commandline == -1) { + Gtk::Dialog scale_dialog(_("Convert legacy Inkscape file")); + scale_dialog.set_transient_for( *(INKSCAPE.active_desktop()->getToplevel()) ); + scale_dialog.set_border_width(10); + scale_dialog.set_resizable(false); + Gtk::Label explanation; + explanation.set_markup(Glib::ustring("") + doc->getDocumentName() + "\n" + + _("was created in an older version of Inkscape (90 DPI) and we need " + "to make it compatible with newer versions (96 DPI). Tell us about this file:\n")); + explanation.set_line_wrap(true); + explanation.set_size_request(600,-1); + Gtk::RadioButton::Group c1, c2; + + Gtk::Label choice1_label; + choice1_label.set_markup( + _("This file contains digital artwork for screen display. (Choose if unsure.)")); + Gtk::RadioButton choice1(c1); + choice1.add(choice1_label); + Gtk::RadioButton choice2(c1, _("This file is intended for physical output, such as paper or 3D prints.")); + Gtk::Label choice2_1_label; + choice2_1_label.set_markup(_("The appearance of elements such as clips, masks, filters, and clones\n" + "is most important. (Choose if unsure.)")); + Gtk::RadioButton choice2_1(c2); + choice2_1.add(choice2_1_label); + Gtk::RadioButton choice2_2(c2, _("The accuracy of the physical unit size and position values of objects\n" + "in the file is most important. (Experimental.)")); + Gtk::CheckButton backup_button(_("Create a backup file in same directory.")); + Gtk::Expander moreinfo(_("More details...")); + Gtk::Label moreinfo_text("", Gtk::ALIGN_START); + moreinfo_text.set_markup( + // TRANSLATORS: Please don't translate link unless the page exists in your language. Add your language + // code to the link this way: https://inkscape.org/[lang]/learn/faq#dpi_change + _("We've updated Inkscape to follow the CSS standard of 96 DPI for " + "better browser compatibility; we used to use 90 DPI. Digital artwork for screen\n" + "display will be converted to 96 DPI without scaling and should be unaffected.\n" + "Artwork drawn at 90 DPI for a specific physical size will be too small if " + "converted to 96 DPI without any scaling. There are two scaling methods:\n\n" + "Scaling the whole document: The least error-prone method, this preserves " + "the appearance of the artwork, including filters and the position of masks, etc. \n" + "The scale of the artwork relative to the document size may not be accurate.\n\n" + "Scaling individual elements in the artwork: This method is less reliable " + "and can result in a changed appearance, \nbut is better for physical output that " + "relies on accurate sizes and positions (for example, for 3D printing.)\n\n" + "More information about this change are available in the Inkscape FAQ" + "")); + moreinfo_text.set_line_wrap(true); + moreinfo_text.set_margin_bottom(20); + moreinfo_text.set_margin_top(20); + moreinfo_text.set_margin_start(30); + moreinfo_text.set_margin_end(15); + + Gtk::Box b; + b.set_border_width(0); + + b.pack_start(choice2_1, false, false, 4); + b.pack_start(choice2_2, false, false, 4); + choice2_1.show(); + choice2_2.show(); + + b.set_halign(Gtk::ALIGN_START); + b.set_valign(Gtk::ALIGN_START); + b.set_hexpand(false); + b.set_vexpand(false); + b.set_margin_start(30); + + Gtk::Box *content = scale_dialog.get_content_area(); + Gtk::Button *ok_button = scale_dialog.add_button(_("OK"), GTK_RESPONSE_ACCEPT); + backup_button.set_active(backup); + content->pack_start(explanation, false, false, 5); + content->pack_start(choice1, false, false, 5); + content->pack_start(choice2, false, false, 5); + content->pack_start(b, false, false, 5); + content->pack_start(backup_button, false, false, 5); + content->pack_start(moreinfo, false, false, 5); + moreinfo.add(moreinfo_text); + scale_dialog.show_all_children(); + b.hide(); + choice1.signal_clicked().connect(sigc::mem_fun(b, &Gtk::Box::hide)); + choice2.signal_clicked().connect(sigc::mem_fun(b, &Gtk::Box::show)); + + response = prefs->getInt("/options/dpiupdatemethod", FILE_DPI_UNCHANGED); + if ( response != FILE_DPI_UNCHANGED ) { + choice2.set_active(); + b.show(); + if ( response == FILE_DPI_DOCUMENT_SCALED) + choice2_2.set_active(); + } + ok_button->grab_focus(); + + int status = scale_dialog.run(); + if ( status == GTK_RESPONSE_ACCEPT ) { + backup = backup_button.get_active(); + prefs->setBool("/options/dpifixbackup", backup); + response = choice1.get_active() ? FILE_DPI_UNCHANGED : choice2_1.get_active() ? FILE_DPI_VIEWBOX_SCALED : FILE_DPI_DOCUMENT_SCALED; + prefs->setInt("/options/dpiupdatemethod", response); + } else if (sp_file_convert_dpi_method_commandline != -1) { + response = sp_file_convert_dpi_method_commandline; + } else { + response = FILE_DPI_UNCHANGED; + } + } else { // GUI with explicit option + response = FILE_DPI_UNCHANGED; + } + + if (backup && (response != FILE_DPI_UNCHANGED)) { + const char* uri = doc->getDocumentURI(); + if (uri) { + sp_file_save_backup(Glib::ustring(uri)); + } + } + + if (!(response == FILE_DPI_UNCHANGED && need_fix_units)) { + need_fix_guides = true; // Only fix guides if drawing scaled + need_fix_box3d = true; + } + + if (response == FILE_DPI_VIEWBOX_SCALED) { + double ratio_viewbox = need_fix_units ? 1.0 : ratio; + + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value("px") * ratio_viewbox, + doc->getHeight().value("px") * ratio_viewbox)); + Inkscape::Util::Quantity width = // maybe set it to mm ? + Inkscape::Util::Quantity(doc->getWidth().value("px") / ratio, "px"); + Inkscape::Util::Quantity height = Inkscape::Util::Quantity(doc->getHeight().value("px") / ratio, "px"); + if (need_fix_units) + doc->setWidthAndHeight(width, height, false); + + } else if (response == FILE_DPI_DOCUMENT_SCALED) { + + Inkscape::Util::Quantity width = // maybe set it to mm ? + Inkscape::Util::Quantity(doc->getWidth().value("px") / ratio, "px"); + Inkscape::Util::Quantity height = Inkscape::Util::Quantity(doc->getHeight().value("px") / ratio, "px"); + if (need_fix_units) + doc->setWidthAndHeight(width, height, false); + + if (!root->viewBox_set) { + // Save preferences + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + + Inkscape::UI::ShapeEditor::blockSetItem(true); + doc->getRoot()->scaleChildItemsRec(Geom::Scale(1 / ratio), Geom::Point(0, 0), false); + Inkscape::UI::ShapeEditor::blockSetItem(false); + + // Restore preferences + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); + + did_scaling = true; + } + + } else { // FILE_DPI_UNCHANGED + if (need_fix_units) + need_fix_grid_mm = true; + } + } + + // Fix guides and grids and perspective + for (SPObject *child = root->firstChild(); child; child = child->getNext()) { + SPNamedView *nv = dynamic_cast(child); + if (nv) { + if (need_fix_guides) { + // std::cout << "Fixing guides" << std::endl; + for (SPObject *child2 = nv->firstChild(); child2; child2 = child2->getNext()) { + SPGuide *gd = dynamic_cast(child2); + if (gd) { + gd->moveto(gd->getPoint() / ratio, true); + } + } + } + + for (auto grid : nv->grids) { + Inkscape::CanvasXYGrid *xy = dynamic_cast(grid); + if (xy) { + // std::cout << "A grid: " << xy->getSVGName() << std::endl; + // std::cout << " Origin: " << xy->origin + // << " Spacing: " << xy->spacing << std::endl; + // std::cout << (xy->isLegacy()?" Legacy":" Not Legacy") << std::endl; + Geom::Scale scale = doc->getDocumentScale(); + if (xy->isLegacy()) { + if (xy->isPixel()) { + if (need_fix_grid_mm) { + xy->Scale(Geom::Scale(1, 1)); // See note below + } else { + scale *= Geom::Scale(ratio, ratio); + xy->Scale(scale.inverse()); /* *** */ + } + } else { + if (need_fix_grid_mm) { + xy->Scale(Geom::Scale(ratio, ratio)); + } else { + xy->Scale(scale.inverse()); /* *** */ + } + } + } else { + if (need_fix_guides) { + if (did_scaling) { + xy->Scale(Geom::Scale(ratio, ratio).inverse()); + } else { + // HACK: Scaling the document does not seem to cause + // grids defined in document units to be updated. + // This forces an update. + xy->Scale(Geom::Scale(1, 1)); + } + } + } + } + } + } // If SPNamedView + + SPDefs *defs = dynamic_cast(child); + if (defs && need_fix_box3d) { + for (SPObject *child = defs->firstChild(); child; child = child->getNext()) { + Persp3D *persp3d = dynamic_cast(child); + if (persp3d) { + std::vector tokens; + + const gchar *vp_x = persp3d->getAttribute("inkscape:vp_x"); + const gchar *vp_y = persp3d->getAttribute("inkscape:vp_y"); + const gchar *vp_z = persp3d->getAttribute("inkscape:vp_z"); + const gchar *vp_o = persp3d->getAttribute("inkscape:persp3d-origin"); + // std::cout << "Found Persp3d: " + // << " vp_x: " << vp_x + // << " vp_y: " << vp_y + // << " vp_z: " << vp_z << std::endl; + Proj::Pt2 pt_x(vp_x); + Proj::Pt2 pt_y(vp_y); + Proj::Pt2 pt_z(vp_z); + Proj::Pt2 pt_o(vp_o); + pt_x = pt_x * (1.0 / ratio); + pt_y = pt_y * (1.0 / ratio); + pt_z = pt_z * (1.0 / ratio); + pt_o = pt_o * (1.0 / ratio); + persp3d->setAttribute("inkscape:vp_x", pt_x.coord_string()); + persp3d->setAttribute("inkscape:vp_y", pt_y.coord_string()); + persp3d->setAttribute("inkscape:vp_z", pt_z.coord_string()); + persp3d->setAttribute("inkscape:persp3d-origin", pt_o.coord_string()); + } + } + } + } // Look for SPNamedView and SPDefs loop + + // desktop->getDocument()->ensureUpToDate(); // Does not update box3d! + DocumentUndo::done(doc, SP_VERB_NONE, _("Update Document")); +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/file.cpp b/src/file.cpp new file mode 100644 index 0000000..70a2a4b --- /dev/null +++ b/src/file.cpp @@ -0,0 +1,1303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * File/Print operations. + */ +/* Authors: + * Lauris Kaplinski + * Chema Celorio + * bulia byak + * Bruno Dilly + * Stephen Silver + * Jon A. Cruz + * Abhishek Sharma + * David Xiong + * Tavmjong Bah + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 1999-2016 Authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** @file + * @note This file needs to be cleaned up extensively. + * What it probably needs is to have one .h file for + * the API, and two or more .cpp files for the implementations. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include + +#include "file.h" +#include "inkscape-application.h" +#include "inkscape-window.h" + +#include "desktop.h" +#include "document-undo.h" +#include "event-log.h" +#include "id-clash.h" +#include "inkscape-version.h" +#include "inkscape.h" +#include "message-stack.h" +#include "path-prefix.h" +#include "print.h" +#include "rdf.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "extension/db.h" +#include "extension/effect.h" +#include "extension/input.h" +#include "extension/output.h" + +#include "helper/png-write.h" + +#include "io/file.h" +#include "io/resource.h" +#include "io/resource-manager.h" +#include "io/sys.h" + +#include "object/sp-defs.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "style.h" + +#include "ui/dialog/font-substitution.h" +#include "ui/dialog/filedialog.h" +#include "ui/interface.h" +#include "ui/tools/tool-base.h" +#include "widgets/desktop-widget.h" + +#include "xml/rebase-hrefs.h" +#include "xml/sp-css-attr.h" + +using Inkscape::DocumentUndo; +using Inkscape::IO::Resource::TEMPLATES; +using Inkscape::IO::Resource::USER; + +#ifdef WITH_DBUS +#include "extension/dbus/dbus-init.h" +#endif + +#ifdef _WIN32 +#include +#endif + +//#define INK_DUMP_FILENAME_CONV 1 +#undef INK_DUMP_FILENAME_CONV + +//#define INK_DUMP_FOPEN 1 +#undef INK_DUMP_FOPEN + +void dump_str(gchar const *str, gchar const *prefix); +void dump_ustr(Glib::ustring const &ustr); + + +/*###################### +## N E W +######################*/ + +/** + * Create a blank document and add it to the desktop + * Input: empty string or template file name. + */ +SPDesktop *sp_file_new(const std::string &templ) +{ + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + SPDocument* doc = app->document_new (templ); + if (!doc) { + std::cerr << "sp_file_new: failed to open document: " << templ << std::endl; + } + InkscapeWindow* win = app->window_open (doc); + + SPDesktop* desktop = win->get_desktop(); + +#ifdef WITH_DBUS + Inkscape::Extension::Dbus::dbus_init_desktop_interface(desktop); +#endif + + return desktop; +} + +Glib::ustring sp_file_default_template_uri() +{ + return Inkscape::IO::Resource::get_filename(TEMPLATES, "default.svg", true); +} + +SPDesktop* sp_file_new_default() +{ + Glib::ustring templateUri = sp_file_default_template_uri(); + SPDesktop* desk = sp_file_new(sp_file_default_template_uri()); + //rdf_add_from_preferences( SP_ACTIVE_DOCUMENT ); + + return desk; +} + + +/*###################### +## D E L E T E +######################*/ + +/** + * Perform document closures preceding an exit() + */ +void sp_file_exit() +{ + if (SP_ACTIVE_DESKTOP == nullptr) { + // We must be in console mode + auto app = Gio::Application::get_default(); + g_assert(app); + app->quit(); + } else { + sp_ui_close_all(); + // no need to call inkscape_exit here; last document being closed will take care of that + } +} + + +/** + * Handle prompting user for "do you want to revert"? Revert on "OK" + */ +void sp_file_revert_dialog() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + g_assert(desktop != nullptr); + + SPDocument *doc = desktop->getDocument(); + g_assert(doc != nullptr); + + Inkscape::XML::Node *repr = doc->getReprRoot(); + g_assert(repr != nullptr); + + gchar const *uri = doc->getDocumentURI(); + if (!uri) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved yet. Cannot revert.")); + return; + } + + bool do_revert = true; + if (doc->isModifiedSinceSave()) { + Glib::ustring tmpString = Glib::ustring::compose(_("Changes will be lost! Are you sure you want to reload document %1?"), uri); + bool response = desktop->warnDialog (tmpString); + if (!response) { + do_revert = false; + } + } + + bool reverted = false; + if (do_revert) { + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + reverted = app->document_revert (doc); + } + + if (reverted) { + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Document reverted.")); + } else { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not reverted.")); + } +} + +void dump_str(gchar const *str, gchar const *prefix) +{ + Glib::ustring tmp; + tmp = prefix; + tmp += " ["; + size_t const total = strlen(str); + for (unsigned i = 0; i < total; i++) { + gchar *const tmp2 = g_strdup_printf(" %02x", (0x0ff & str[i])); + tmp += tmp2; + g_free(tmp2); + } + + tmp += "]"; + g_message("%s", tmp.c_str()); +} + +void dump_ustr(Glib::ustring const &ustr) +{ + char const *cstr = ustr.c_str(); + char const *data = ustr.data(); + Glib::ustring::size_type const byteLen = ustr.bytes(); + Glib::ustring::size_type const dataLen = ustr.length(); + Glib::ustring::size_type const cstrLen = strlen(cstr); + + g_message(" size: %lu\n length: %lu\n bytes: %lu\n clen: %lu", + gulong(ustr.size()), gulong(dataLen), gulong(byteLen), gulong(cstrLen) ); + g_message( " ASCII? %s", (ustr.is_ascii() ? "yes":"no") ); + g_message( " UTF-8? %s", (ustr.validate() ? "yes":"no") ); + + try { + Glib::ustring tmp; + for (Glib::ustring::size_type i = 0; i < ustr.bytes(); i++) { + tmp = " "; + if (i < dataLen) { + Glib::ustring::value_type val = ustr.at(i); + gchar* tmp2 = g_strdup_printf( (((val & 0xff00) == 0) ? " %02x" : "%04x"), val ); + tmp += tmp2; + g_free( tmp2 ); + } else { + tmp += " "; + } + + if (i < byteLen) { + int val = (0x0ff & data[i]); + gchar *tmp2 = g_strdup_printf(" %02x", val); + tmp += tmp2; + g_free( tmp2 ); + if ( val > 32 && val < 127 ) { + tmp2 = g_strdup_printf( " '%c'", (gchar)val ); + tmp += tmp2; + g_free( tmp2 ); + } else { + tmp += " . "; + } + } else { + tmp += " "; + } + + if ( i < cstrLen ) { + int val = (0x0ff & cstr[i]); + gchar* tmp2 = g_strdup_printf(" %02x", val); + tmp += tmp2; + g_free(tmp2); + if ( val > 32 && val < 127 ) { + tmp2 = g_strdup_printf(" '%c'", (gchar) val); + tmp += tmp2; + g_free( tmp2 ); + } else { + tmp += " . "; + } + } else { + tmp += " "; + } + + g_message( "%s", tmp.c_str() ); + } + } catch (...) { + g_message("XXXXXXXXXXXXXXXXXX Exception" ); + } + g_message("---------------"); +} + +/** + * Display an file Open selector. Open a document if OK is pressed. + * Can select single or multiple files for opening. + */ +void +sp_file_open_dialog(Gtk::Window &parentWindow, gpointer /*object*/, gpointer /*data*/) +{ + //# Get the current directory for finding files + static Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if(open_path.empty()) + { + Glib::ustring attr = prefs->getString("/dialogs/open/path"); + if (!attr.empty()) open_path = attr; + } + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + +#ifdef _WIN32 + //# If no open path, default to our win32 documents folder + if (open_path.empty()) + { + // The path to the My Documents folder is read from the + // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal" + HKEY key = NULL; + if(RegOpenKeyExA(HKEY_CURRENT_USER, + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", + 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) + { + WCHAR utf16path[_MAX_PATH]; + DWORD value_type; + DWORD data_size = sizeof(utf16path); + if(RegQueryValueExW(key, L"Personal", NULL, &value_type, + (BYTE*)utf16path, &data_size) == ERROR_SUCCESS) + { + g_assert(value_type == REG_SZ); + gchar *utf8path = g_utf16_to_utf8( + (const gunichar2*)utf16path, -1, NULL, NULL, NULL); + if(utf8path) + { + open_path = Glib::ustring(utf8path); + g_free(utf8path); + } + } + } + } +#endif + + //# If no open path, default to our home directory + if (open_path.empty()) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog + Inkscape::UI::Dialog::FileOpenDialog *openDialogInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + parentWindow, open_path, + Inkscape::UI::Dialog::SVG_TYPES, + _("Select file to open")); + + //# Show the dialog + bool const success = openDialogInstance->show(); + + //# Save the folder the user selected for later + open_path = openDialogInstance->getCurrentDirectory(); + + if (!success) + { + delete openDialogInstance; + return; + } + + // FIXME: This is silly to have separate code paths for opening one vs many files! + + //# User selected something. Get name and type + Glib::ustring fileName = openDialogInstance->getFilename(); + + Inkscape::Extension::Extension *fileType = + openDialogInstance->getSelectionType(); + + //# Code to check & open if multiple files. + std::vector flist = openDialogInstance->getFilenames(); + + //# We no longer need the file dialog object - delete it + delete openDialogInstance; + openDialogInstance = nullptr; + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + //# Iterate through filenames if more than 1 + if (flist.size() > 1) + { + for (const auto & i : flist) + { + fileName = i; + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + if ( newFileName.size() > 0 ) + fileName = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + +#ifdef INK_DUMP_FILENAME_CONV + g_message("Opening File %s\n", fileName.c_str()); +#endif + + Glib::RefPtr file = Gio::File::create_for_path(fileName); + app->create_window (file); + } + + return; + } + + + if (!fileName.empty()) + { + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + fileName = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + open_path = Glib::path_get_dirname (fileName); + open_path.append(G_DIR_SEPARATOR_S); + prefs->setString("/dialogs/open/path", open_path); + + Glib::RefPtr file = Gio::File::create_for_path(fileName); + app->create_window (file); + } + + return; +} + + +/*###################### +## V A C U U M +######################*/ + +/** + * Remove unreferenced defs from the defs section of the document. + */ +void sp_file_vacuum(SPDocument *doc) +{ + unsigned int diff = doc->vacuumDocument(); + + DocumentUndo::done(doc, SP_VERB_FILE_VACUUM, + _("Clean up document")); + + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (dt != nullptr) { + // Show status messages when in GUI mode + if (diff > 0) { + dt->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, + ngettext("Removed %i unused definition in <defs>.", + "Removed %i unused definitions in <defs>.", + diff), + diff); + } else { + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No unused definitions in <defs>.")); + } + } +} + + + +/*###################### +## S A V E +######################*/ + +/** + * This 'save' function called by the others below + * + * \param official whether to set :output_module and :modified in the + * document; is true for normal save, false for temporary saves + */ +static bool +file_save(Gtk::Window &parentWindow, SPDocument *doc, const Glib::ustring &uri, + Inkscape::Extension::Extension *key, bool checkoverwrite, bool official, + Inkscape::Extension::FileSaveMethod save_method) +{ + if (!doc || uri.size()<1) //Safety check + return false; + + Inkscape::Version save = doc->getRoot()->version.inkscape; + doc->getReprRoot()->setAttribute("inkscape:version", Inkscape::version_string); + try { + Inkscape::Extension::save(key, doc, uri.c_str(), + false, + checkoverwrite, official, + save_method); + } catch (Inkscape::Extension::Output::no_extension_found &e) { + gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str()); + gchar *text = g_strdup_printf(_("No Inkscape extension found to save document (%s). This may have been caused by an unknown filename extension."), safeUri); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + g_free(safeUri); + // Restore Inkscape version + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (Inkscape::Extension::Output::file_read_only &e) { + gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str()); + gchar *text = g_strdup_printf(_("File %s is write protected. Please remove write protection and try again."), safeUri); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + g_free(safeUri); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (Inkscape::Extension::Output::save_failed &e) { + gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str()); + gchar *text = g_strdup_printf(_("File %s could not be saved."), safeUri); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + g_free(safeUri); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (Inkscape::Extension::Output::save_cancelled &e) { + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (Inkscape::Extension::Output::export_id_not_found &e) { + gchar *text = g_strdup_printf(_("File could not be saved:\nNo object with ID '%s' found."), e.id); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (Inkscape::Extension::Output::no_overwrite &e) { + return sp_file_save_dialog(parentWindow, doc, save_method); + } catch (std::exception &e) { + gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str()); + gchar *text = g_strdup_printf(_("File %s could not be saved.\n\n" + "The following additional information was returned by the output extension:\n" + "'%s'"), safeUri, e.what()); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + g_free(safeUri); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } catch (...) { + g_critical("Extension '%s' threw an unspecified exception.", key->get_id()); + gchar *safeUri = Inkscape::IO::sanitizeString(uri.c_str()); + gchar *text = g_strdup_printf(_("File %s could not be saved."), safeUri); + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Document not saved.")); + sp_ui_error_dialog(text); + g_free(text); + g_free(safeUri); + doc->getReprRoot()->setAttribute("inkscape:version", sp_version_to_string( save )); + return false; + } + + if (SP_ACTIVE_DESKTOP) { + if (! SP_ACTIVE_DESKTOP->event_log) { + g_message("file_save: ->event_log == NULL. please report to bug #967416"); + } + if (! SP_ACTIVE_DESKTOP->messageStack()) { + g_message("file_save: ->messageStack() == NULL. please report to bug #967416"); + } + } else { + g_message("file_save: SP_ACTIVE_DESKTOP == NULL. please report to bug #967416"); + } + + SP_ACTIVE_DESKTOP->event_log->rememberFileSave(); + Glib::ustring msg; + if (doc->getDocumentURI() == nullptr) { + msg = Glib::ustring::format(_("Document saved.")); + } else { + msg = Glib::ustring::format(_("Document saved."), " ", doc->getDocumentURI()); + } + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::NORMAL_MESSAGE, msg.c_str()); + return true; +} + + +/** + * Check if a string ends with another string. + * \todo Find a better code file to put this general purpose method + */ +static bool hasEnding (Glib::ustring const &fullString, Glib::ustring const &ending) +{ + if (fullString.length() > ending.length()) { + return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending)); + } else { + return false; + } +} + +/** + * Display a SaveAs dialog. Save the document if OK pressed. + */ +bool +sp_file_save_dialog(Gtk::Window &parentWindow, SPDocument *doc, Inkscape::Extension::FileSaveMethod save_method) +{ + Inkscape::Extension::Output *extension = nullptr; + bool is_copy = (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY); + + // Note: default_extension has the format "org.inkscape.output.svg.inkscape", whereas + // filename_extension only uses ".svg" + Glib::ustring default_extension; + Glib::ustring filename_extension = ".svg"; + + default_extension= Inkscape::Extension::get_file_save_extension(save_method); + //g_message("%s: extension name: '%s'", __FUNCTION__, default_extension); + + extension = dynamic_cast + (Inkscape::Extension::db.get(default_extension.c_str())); + + if (extension) + filename_extension = extension->get_extension(); + + Glib::ustring save_path = Inkscape::Extension::get_file_save_path(doc, save_method); + + if (!Inkscape::IO::file_test(save_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + save_path.clear(); + + if (save_path.empty()) + save_path = g_get_home_dir(); + + Glib::ustring save_loc = save_path; + save_loc.append(G_DIR_SEPARATOR_S); + + int i = 1; + if ( !doc->getDocumentURI() ) { + // We are saving for the first time; create a unique default filename + save_loc = save_loc + _("drawing") + filename_extension; + + while (Inkscape::IO::file_test(save_loc.c_str(), G_FILE_TEST_EXISTS)) { + save_loc = save_path; + save_loc.append(G_DIR_SEPARATOR_S); + save_loc = save_loc + Glib::ustring::compose(_("drawing-%1"), i++) + filename_extension; + } + } else { + save_loc.append(Glib::path_get_basename(doc->getDocumentURI())); + } + + // convert save_loc from utf-8 to locale + // is this needed any more, now that everything is handled in + // Inkscape::IO? + Glib::ustring save_loc_local = Glib::filename_from_utf8(save_loc); + + if (!save_loc_local.empty()) + save_loc = save_loc_local; + + //# Show the SaveAs dialog + char const * dialog_title; + if (is_copy) { + dialog_title = (char const *) _("Select file to save a copy to"); + } else { + dialog_title = (char const *) _("Select file to save to"); + } + gchar* doc_title = doc->getRoot()->title(); + Inkscape::UI::Dialog::FileSaveDialog *saveDialog = + Inkscape::UI::Dialog::FileSaveDialog::create( + parentWindow, + save_loc, + Inkscape::UI::Dialog::SVG_TYPES, + dialog_title, + default_extension, + doc_title ? doc_title : "", + save_method + ); + + saveDialog->setSelectionType(extension); + + bool success = saveDialog->show(); + if (!success) { + delete saveDialog; + if(doc_title) g_free(doc_title); + return success; + } + + // set new title here (call RDF to ensure metadata and title element are updated) + rdf_set_work_entity(doc, rdf_find_entity("title"), saveDialog->getDocTitle().c_str()); + + Glib::ustring fileName = saveDialog->getFilename(); + Inkscape::Extension::Extension *selectionType = saveDialog->getSelectionType(); + + delete saveDialog; + saveDialog = nullptr; + if(doc_title) g_free(doc_title); + + if (!fileName.empty()) { + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if (!newFileName.empty()) + fileName = newFileName; + else + g_warning( "Error converting filename for saving to UTF-8." ); + + Inkscape::Extension::Output *omod = dynamic_cast(selectionType); + if (omod) { + Glib::ustring save_extension = (omod->get_extension()) ? (omod->get_extension()) : ""; + if ( !hasEnding(fileName, save_extension) ) { + fileName += save_extension; + } + } + + // FIXME: does the argument !is_copy really convey the correct meaning here? + success = file_save(parentWindow, doc, fileName, selectionType, TRUE, !is_copy, save_method); + + if (success && doc->getDocumentURI()) { + // getDocumentURI does not return an actual URI... it is an UTF-8 encoded filename (!) + std::string filename = Glib::filename_from_utf8(doc->getDocumentURI()); + Glib::ustring uri = Glib::filename_to_uri(filename); + + Glib::RefPtr recent = Gtk::RecentManager::get_default(); + recent->add_item(uri); + } + + save_path = Glib::path_get_dirname(fileName); + Inkscape::Extension::store_save_path_in_prefs(save_path, save_method); + + return success; + } + + + return false; +} + + +/** + * Save a document, displaying a SaveAs dialog if necessary. + */ +bool +sp_file_save_document(Gtk::Window &parentWindow, SPDocument *doc) +{ + bool success = true; + + if (doc->isModifiedSinceSave()) { + if ( doc->getDocumentURI() == nullptr ) + { + // Hier sollte in Argument mitgegeben werden, das anzeigt, da� das Dokument das erste + // Mal gespeichert wird, so da� als default .svg ausgew�hlt wird und nicht die zuletzt + // benutzte "Save as ..."-Endung + return sp_file_save_dialog(parentWindow, doc, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG); + } else { + Glib::ustring extension = Inkscape::Extension::get_file_save_extension(Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + Glib::ustring fn = g_strdup(doc->getDocumentURI()); + // Try to determine the extension from the uri; this may not lead to a valid extension, + // but this case is caught in the file_save method below (or rather in Extension::save() + // further down the line). + Glib::ustring ext = ""; + Glib::ustring::size_type pos = fn.rfind('.'); + if (pos != Glib::ustring::npos) { + // FIXME: this could/should be more sophisticated (see FileSaveDialog::appendExtension()), + // but hopefully it's a reasonable workaround for now + ext = fn.substr( pos ); + } + success = file_save(parentWindow, doc, fn, Inkscape::Extension::db.get(ext.c_str()), FALSE, TRUE, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + if (success == false) { + // give the user the chance to change filename or extension + return sp_file_save_dialog(parentWindow, doc, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG); + } + } + } else { + Glib::ustring msg; + if ( doc->getDocumentURI() == nullptr ) + { + msg = Glib::ustring::format(_("No changes need to be saved.")); + } else { + msg = Glib::ustring::format(_("No changes need to be saved."), " ", doc->getDocumentURI()); + } + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::WARNING_MESSAGE, msg.c_str()); + success = TRUE; + } + + return success; +} + + +/** + * Save a document. + */ +bool +sp_file_save(Gtk::Window &parentWindow, gpointer /*object*/, gpointer /*data*/) +{ + if (!SP_ACTIVE_DOCUMENT) + return false; + + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Saving document...")); + + sp_namedview_document_from_window(SP_ACTIVE_DESKTOP); + return sp_file_save_document(parentWindow, SP_ACTIVE_DOCUMENT); +} + + +/** + * Save a document, always displaying the SaveAs dialog. + */ +bool +sp_file_save_as(Gtk::Window &parentWindow, gpointer /*object*/, gpointer /*data*/) +{ + if (!SP_ACTIVE_DOCUMENT) + return false; + sp_namedview_document_from_window(SP_ACTIVE_DESKTOP); + return sp_file_save_dialog(parentWindow, SP_ACTIVE_DOCUMENT, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); +} + + + +/** + * Save a copy of a document, always displaying a sort of SaveAs dialog. + */ +bool +sp_file_save_a_copy(Gtk::Window &parentWindow, gpointer /*object*/, gpointer /*data*/) +{ + if (!SP_ACTIVE_DOCUMENT) + return false; + sp_namedview_document_from_window(SP_ACTIVE_DESKTOP); + return sp_file_save_dialog(parentWindow, SP_ACTIVE_DOCUMENT, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY); +} + +/** + * Save a copy of a document as template. + */ +bool +sp_file_save_template(Gtk::Window &parentWindow, Glib::ustring name, + Glib::ustring author, Glib::ustring description, Glib::ustring keywords, + bool isDefault) +{ + if (!SP_ACTIVE_DOCUMENT || name.length() == 0) + return true; + + auto document = SP_ACTIVE_DOCUMENT; + + DocumentUndo::setUndoSensitive(document, false); + + auto root = document->getReprRoot(); + auto xml_doc = document->getReprDoc(); + + auto templateinfo_node = xml_doc->createElement("inkscape:templateinfo"); + Inkscape::GC::release(templateinfo_node); + + auto element_node = xml_doc->createElement("inkscape:name"); + Inkscape::GC::release(element_node); + + element_node->appendChild(xml_doc->createTextNode(name.c_str())); + templateinfo_node->appendChild(element_node); + + if (author.length() != 0) { + + element_node = xml_doc->createElement("inkscape:author"); + Inkscape::GC::release(element_node); + + element_node->appendChild(xml_doc->createTextNode(author.c_str())); + templateinfo_node->appendChild(element_node); + } + + if (description.length() != 0) { + + element_node = xml_doc->createElement("inkscape:shortdesc"); + Inkscape::GC::release(element_node); + + element_node->appendChild(xml_doc->createTextNode(description.c_str())); + templateinfo_node->appendChild(element_node); + + } + + element_node = xml_doc->createElement("inkscape:date"); + Inkscape::GC::release(element_node); + + element_node->appendChild(xml_doc->createTextNode( + Glib::DateTime::create_now_local().format("%F").c_str())); + templateinfo_node->appendChild(element_node); + + if (keywords.length() != 0) { + + element_node = xml_doc->createElement("inkscape:keywords"); + Inkscape::GC::release(element_node); + + element_node->appendChild(xml_doc->createTextNode(keywords.c_str())); + templateinfo_node->appendChild(element_node); + + } + + root->appendChild(templateinfo_node); + + // Escape filenames for windows users, but filenames are not URIs so + // Allow UTF-8 and don't escape spaces witch are popular chars. + auto encodedName = Glib::uri_escape_string(name, " ", true); + encodedName.append(".svg"); + + auto filename = Inkscape::IO::Resource::get_path_ustring(USER, TEMPLATES, encodedName.c_str()); + + auto operation_confirmed = sp_ui_overwrite_file(filename.c_str()); + + if (operation_confirmed) { + + file_save(parentWindow, document, filename, + Inkscape::Extension::db.get(".svg"), false, false, + Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG); + + if (isDefault) { + // save as "default.svg" by default (so it works independently of UI language), unless + // a localized template like "default.de.svg" is already present (which overrides "default.svg") + Glib::ustring default_svg_localized = Glib::ustring("default.") + _("en") + ".svg"; + filename = Inkscape::IO::Resource::get_path_ustring(USER, TEMPLATES, default_svg_localized.c_str()); + + if (!Inkscape::IO::file_test(filename.c_str(), G_FILE_TEST_EXISTS)) { + filename = Inkscape::IO::Resource::get_path_ustring(USER, TEMPLATES, "default.svg"); + } + + file_save(parentWindow, document, filename, + Inkscape::Extension::db.get(".svg"), false, false, + Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG); + } + } + + // remove this node from current document after saving it as template + root->removeChild(templateinfo_node); + + DocumentUndo::setUndoSensitive(document, true); + + return operation_confirmed; +} + + + +/*###################### +## I M P O R T +######################*/ + +/** + * Paste the contents of a document into the active desktop. + * @param clipdoc The document to paste + * @param in_place Whether to paste the selection where it was when copied + * @pre @c clipdoc is not empty and items can be added to the current layer + */ +void sp_import_document(SPDesktop *desktop, SPDocument *clipdoc, bool in_place) +{ + //TODO: merge with file_import() + + SPDocument *target_document = desktop->getDocument(); + Inkscape::XML::Node *root = clipdoc->getReprRoot(); + Inkscape::XML::Node *target_parent = desktop->currentLayer()->getRepr(); + + // copy definitions + desktop->doc()->importDefs(clipdoc); + + Inkscape::XML::Node* clipboard = nullptr; + // copy objects + std::vector pasted_objects; + for (Inkscape::XML::Node *obj = root->firstChild() ; obj ; obj = obj->next()) { + // Don't copy metadata, defs, named views and internal clipboard contents to the document + if (!strcmp(obj->name(), "svg:defs")) { + continue; + } + if (!strcmp(obj->name(), "svg:metadata")) { + continue; + } + if (!strcmp(obj->name(), "sodipodi:namedview")) { + continue; + } + if (!strcmp(obj->name(), "inkscape:clipboard")) { + clipboard = obj; + continue; + } + Inkscape::XML::Node *obj_copy = obj->duplicate(target_document->getReprDoc()); + target_parent->appendChild(obj_copy); + Inkscape::GC::release(obj_copy); + + pasted_objects.push_back(obj_copy); + + // if we are pasting a clone to an already existing object, its + // transform is wrong (see ui/clipboard.cpp) + if(obj_copy->attribute("transform-with-parent") && target_document->getObjectById(obj->attribute("xlink:href")+1) ){ + obj_copy->setAttribute("transform",obj_copy->attribute("transform-with-parent")); + } + if(obj_copy->attribute("transform-with-parent")) + obj_copy->removeAttribute("transform-with-parent"); + } + + std::vector pasted_objects_not; + if(clipboard) //???? Removed dead code can cause any bug, need to reimplement undead + for (Inkscape::XML::Node *obj = clipboard->firstChild() ; obj ; obj = obj->next()) { + if(target_document->getObjectById(obj->attribute("id"))) continue; + Inkscape::XML::Node *obj_copy = obj->duplicate(target_document->getReprDoc()); + SPObject * pasted = desktop->currentLayer()->appendChildRepr(obj_copy); + Inkscape::GC::release(obj_copy); + SPLPEItem * pasted_lpe_item = dynamic_cast(pasted); + if (pasted_lpe_item){ + pasted_lpe_item->forkPathEffectsIfNecessary(1); + } + pasted_objects_not.push_back(obj_copy); + } + Inkscape::Selection *selection = desktop->getSelection(); + selection->setReprList(pasted_objects_not); + Geom::Affine doc2parent = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + selection->applyAffine(desktop->dt2doc() * doc2parent * desktop->doc2dt(), true, false, false); + selection->deleteItems(); + + // Change the selection to the freshly pasted objects + selection->setReprList(pasted_objects); + for (auto item : selection->items()) { + SPLPEItem *pasted_lpe_item = dynamic_cast(item); + if (pasted_lpe_item) { + pasted_lpe_item->forkPathEffectsIfNecessary(1); + } + } + // Apply inverse of parent transform + selection->applyAffine(desktop->dt2doc() * doc2parent * desktop->doc2dt(), true, false, false); + + + + // Update (among other things) all curves in paths, for bounds() to work + target_document->ensureUpToDate(); + + // move selection either to original position (in_place) or to mouse pointer + Geom::OptRect sel_bbox = selection->visualBounds(); + if (sel_bbox) { + // get offset of selection to original position of copied elements + Geom::Point pos_original; + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + if (clipnode) { + Geom::Point min, max; + sp_repr_get_point(clipnode, "min", &min); + sp_repr_get_point(clipnode, "max", &max); + pos_original = Geom::Point(min[Geom::X], max[Geom::Y]); + } + Geom::Point offset = pos_original - sel_bbox->corner(3); + + if (!in_place) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + sp_event_context_discard_delayed_snap_event(desktop->event_context); + + // get offset from mouse pointer to bbox center, snap to grid if enabled + Geom::Point mouse_offset = desktop->point() - sel_bbox->midpoint(); + offset = m.multipleOfGridPitch(mouse_offset - offset, sel_bbox->midpoint() + offset) + offset; + m.unSetup(); + } + + selection->moveRelative(offset); + } + target_document->emitReconstructionFinish(); +} + + +/** + * Import a resource. Called by sp_file_import() + */ +SPObject * +file_import(SPDocument *in_doc, const Glib::ustring &uri, + Inkscape::Extension::Extension *key) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + bool cancelled = false; + + // store mouse pointer location before opening any dialogs, so we can drop the item where initially intended + auto pointer_location = desktop->point(); + + //DEBUG_MESSAGE( fileImport, "file_import( in_doc:%p uri:[%s], key:%p", in_doc, uri, key ); + SPDocument *doc; + try { + doc = Inkscape::Extension::open(key, uri.c_str()); + } catch (Inkscape::Extension::Input::no_extension_found &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_failed &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_cancelled &e) { + doc = nullptr; + cancelled = true; + } + + if (doc != nullptr) { + Inkscape::XML::rebase_hrefs(doc, in_doc->getDocumentBase(), true); + Inkscape::XML::Document *xml_in_doc = in_doc->getReprDoc(); + + prevent_id_clashes(doc, in_doc); + + SPCSSAttr *style = sp_css_attr_from_object(doc->getRoot()); + + // Count the number of top-level items in the imported document. + guint items_count = 0; + SPObject *o = nullptr; + for (auto& child: doc->getRoot()->children) { + if (SP_IS_ITEM(&child)) { + items_count++; + o = &child; + } + } + + //ungroup if necessary + bool did_ungroup = false; + while(items_count==1 && o && SP_IS_GROUP(o) && o->children.size()==1){ + std::vectorv; + sp_item_group_ungroup(SP_GROUP(o),v,false); + o = v.empty() ? nullptr : v[0]; + did_ungroup=true; + } + + // Create a new group if necessary. + Inkscape::XML::Node *newgroup = nullptr; + if ((style && style->attributeList()) || items_count > 1) { + newgroup = xml_in_doc->createElement("svg:g"); + sp_repr_css_set(newgroup, style, "style"); + } + + // Determine the place to insert the new object. + // This will be the current layer, if possible. + // FIXME: If there's no desktop (command line run?) we need + // a document:: method to return the current layer. + // For now, we just use the root in this case. + SPObject *place_to_insert; + if (desktop) { + place_to_insert = desktop->currentLayer(); + } else { + place_to_insert = in_doc->getRoot(); + } + + in_doc->importDefs(doc); + + // Construct a new object representing the imported image, + // and insert it into the current document. + SPObject *new_obj = nullptr; + for (auto& child: doc->getRoot()->children) { + if (SP_IS_ITEM(&child)) { + Inkscape::XML::Node *newitem = did_ungroup ? o->getRepr()->duplicate(xml_in_doc) : child.getRepr()->duplicate(xml_in_doc); + + // convert layers to groups, and make sure they are unlocked + // FIXME: add "preserve layers" mode where each layer from + // import is copied to the same-named layer in host + newitem->removeAttribute("inkscape:groupmode"); + newitem->removeAttribute("sodipodi:insensitive"); + + if (newgroup) newgroup->appendChild(newitem); + else new_obj = place_to_insert->appendChildRepr(newitem); + } + + // don't lose top-level defs or style elements + else if (child.getRepr()->type() == Inkscape::XML::ELEMENT_NODE) { + const gchar *tag = child.getRepr()->name(); + if (!strcmp(tag, "svg:style")) { + in_doc->getRoot()->appendChildRepr(child.getRepr()->duplicate(xml_in_doc)); + } + } + } + in_doc->emitReconstructionFinish(); + if (newgroup) new_obj = place_to_insert->appendChildRepr(newgroup); + + // release some stuff + if (newgroup) Inkscape::GC::release(newgroup); + if (style) sp_repr_css_attr_unref(style); + + // select and move the imported item + if (new_obj && SP_IS_ITEM(new_obj)) { + Inkscape::Selection *selection = desktop->getSelection(); + selection->set(SP_ITEM(new_obj)); + + // preserve parent and viewBox transformations + // c2p is identity matrix at this point unless ensureUpToDate is called + doc->ensureUpToDate(); + Geom::Affine affine = doc->getRoot()->c2p * SP_ITEM(place_to_insert)->i2doc_affine().inverse(); + selection->applyAffine(desktop->dt2doc() * affine * desktop->doc2dt(), true, false, false); + + // move to mouse pointer + { + desktop->getDocument()->ensureUpToDate(); + Geom::OptRect sel_bbox = selection->visualBounds(); + if (sel_bbox) { + Geom::Point m( pointer_location - sel_bbox->midpoint() ); + selection->moveRelative(m, false); + } + } + } + + DocumentUndo::done(in_doc, SP_VERB_FILE_IMPORT, + _("Import")); + return new_obj; + } else if (!cancelled) { + gchar *text = g_strdup_printf(_("Failed to load the requested file %s"), uri.c_str()); + sp_ui_error_dialog(text); + g_free(text); + } + + return nullptr; +} + + +/** + * Display an Open dialog, import a resource if OK pressed. + */ +void +sp_file_import(Gtk::Window &parentWindow) +{ + static Glib::ustring import_path; + + SPDocument *doc = SP_ACTIVE_DOCUMENT; + if (!doc) + return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if(import_path.empty()) + { + Glib::ustring attr = prefs->getString("/dialogs/import/path"); + if (!attr.empty()) import_path = attr; + } + + //# Test if the import_path directory exists + if (!Inkscape::IO::file_test(import_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + import_path = ""; + + //# If no open path, default to our home directory + if (import_path.empty()) + { + import_path = g_get_home_dir(); + import_path.append(G_DIR_SEPARATOR_S); + } + + // Create new dialog (don't use an old one, because parentWindow has probably changed) + Inkscape::UI::Dialog::FileOpenDialog *importDialogInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + parentWindow, + import_path, + Inkscape::UI::Dialog::IMPORT_TYPES, + (char const *)_("Select file to import")); + + bool success = importDialogInstance->show(); + if (!success) { + delete importDialogInstance; + return; + } + + typedef std::vector pathnames; + pathnames flist(importDialogInstance->getFilenames()); + + // Get file name and extension type + Glib::ustring fileName = importDialogInstance->getFilename(); + Inkscape::Extension::Extension *selection = importDialogInstance->getSelectionType(); + + delete importDialogInstance; + importDialogInstance = nullptr; + + //# Iterate through filenames if more than 1 + if (flist.size() > 1) + { + for (const auto & i : flist) + { + fileName = i; + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + if (!newFileName.empty()) + fileName = newFileName; + else + g_warning("ERROR CONVERTING IMPORT FILENAME TO UTF-8"); + +#ifdef INK_DUMP_FILENAME_CONV + g_message("Importing File %s\n", fileName.c_str()); +#endif + file_import(doc, fileName, selection); + } + + return; + } + + + if (!fileName.empty()) { + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if (!newFileName.empty()) + fileName = newFileName; + else + g_warning("ERROR CONVERTING IMPORT FILENAME TO UTF-8"); + + import_path = Glib::path_get_dirname(fileName); + import_path.append(G_DIR_SEPARATOR_S); + prefs->setString("/dialogs/import/path", import_path); + + file_import(doc, fileName, selection); + } + + return; +} + +/*###################### +## P R I N T +######################*/ + + +/** + * Print the current document, if any. + */ +void +sp_file_print(Gtk::Window& parentWindow) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + if (doc) + sp_print_document(parentWindow, doc); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/file.h b/src/file.h new file mode 100644 index 0000000..6759768 --- /dev/null +++ b/src/file.h @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_FILE_H +#define SEEN_SP_FILE_H + +/* + * File/Print operations + * + * Authors: + * Lauris Kaplinski + * Chema Celorio + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "extension/system.h" + +class SPDesktop; +class SPDocument; +class SPObject; + +namespace Inkscape { + namespace Extension { + class Extension; + } +} + +namespace Gtk { +class Window; +} + +// Get the name of the default template uri +Glib::ustring sp_file_default_template_uri(); + +/*###################### +## N E W +######################*/ + +/** + * Creates a new Inkscape document and window. + * Return value is a pointer to the newly created desktop. + */ +SPDesktop* sp_file_new (const std::string &templ); +SPDesktop* sp_file_new_default (); + +/*###################### +## D E L E T E +######################*/ + +/** + * Close the document/view + */ +void sp_file_exit (); + +/*###################### +## O P E N +######################*/ + +/** + * Displays a file open dialog. Calls sp_file_open on + * an OK. + */ +void sp_file_open_dialog (Gtk::Window &parentWindow, void* object, void* data); + +/** + * Reverts file to disk-copy on "YES" + */ +void sp_file_revert_dialog (); + +/*###################### +## S A V E +######################*/ + +/** + * + */ +bool sp_file_save (Gtk::Window &parentWindow, void* object, void* data); + +/** + * Saves the given document. Displays a file select dialog + * to choose the new name. + */ +bool sp_file_save_as (Gtk::Window &parentWindow, void* object, void* data); + +/** + * Saves a copy of the given document. Displays a file select dialog + * to choose a name for the copy. + */ +bool sp_file_save_a_copy (Gtk::Window &parentWindow, void* object, void* data); + +/** + * Save a copy of a document as template. + */ +bool +sp_file_save_template(Gtk::Window &parentWindow, Glib::ustring name, + Glib::ustring author, Glib::ustring description, Glib::ustring keywords, + bool isDefault); + +/** + * Saves the given document. Displays a file select dialog + * if needed. + */ +bool sp_file_save_document (Gtk::Window &parentWindow, SPDocument *document); + +/* Do the saveas dialog with a document as the parameter */ +bool sp_file_save_dialog (Gtk::Window &parentWindow, SPDocument *doc, Inkscape::Extension::FileSaveMethod save_method); + + +/*###################### +## I M P O R T +######################*/ + +void sp_import_document(SPDesktop *desktop, SPDocument *clipdoc, bool in_place); + +/** + * Displays a file selector dialog, to allow the + * user to import data into the current document. + */ +void sp_file_import (Gtk::Window &parentWindow); + +/** + * Imports a resource + */ +SPObject* file_import(SPDocument *in_doc, const Glib::ustring &uri, + Inkscape::Extension::Extension *key); + +/*###################### +## E X P O R T +######################*/ + +/** + * Displays a FileExportDialog for the user, with an + * additional type selection, to allow the user to export + * the a document as a given type. + */ +//bool sp_file_export_dialog (Gtk::Window &parentWindow); + + +/*###################### +## P R I N T +######################*/ + +/* These functions are redundant now, but +would be useful as instance methods +*/ + +/** + * + */ +void sp_file_print (Gtk::Window& parentWindow); + +/*##################### +## U T I L I T Y +#####################*/ + +/** + * clean unused defs out of file + */ +void sp_file_vacuum (SPDocument *doc); +void sp_file_convert_text_baseline_spacing(SPDocument *doc); +void sp_file_convert_font_name(SPDocument *doc); +void sp_file_convert_dpi(SPDocument *doc); +void sp_file_fix_empty_lines(SPDocument *doc); +enum File_DPI_Fix { FILE_DPI_UNCHANGED = 0, FILE_DPI_VIEWBOX_SCALED, FILE_DPI_DOCUMENT_SCALED }; +extern int sp_file_convert_dpi_method_commandline; + +#endif // SEEN_SP_FILE_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 : diff --git a/src/fill-or-stroke.h b/src/fill-or-stroke.h new file mode 100644 index 0000000..43849b3 --- /dev/null +++ b/src/fill-or-stroke.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Definition of the FillOrStroke enum. + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_FILL_OR_STROKE_H +#define SEEN_FILL_OR_STROKE_H + +/** \post STROKE == 0, FILL != 0. */ +enum FillOrStroke { STROKE = 0, FILL = 1 }; + +#endif // !SEEN_FILL_OR_STROKE_H diff --git a/src/filter-chemistry.cpp b/src/filter-chemistry.cpp new file mode 100644 index 0000000..ed102b9 --- /dev/null +++ b/src/filter-chemistry.cpp @@ -0,0 +1,601 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Various utility methods for filters + * + * Authors: + * Hugo Rodrigues + * bulia byak + * Niko Kiirala + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006-2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter-chemistry.h" + +#include +#include + +#include "desktop-style.h" +#include "document.h" +#include "filter-enums.h" +#include "style.h" + +#include "object/sp-defs.h" +#include "object/sp-item.h" + +#include "object/filters/blend.h" +#include "object/filters/gaussian-blur.h" + +/** + * Count how many times the filter is used by the styles of o and its + * descendants + */ +static guint count_filter_hrefs(SPObject *o, SPFilter *filter) +{ + if (!o) + return 1; + + guint i = 0; + + SPStyle *style = o->style; + if (style + && style->filter.set + && style->getFilter() == filter) + { + i ++; + } + + for (auto& child: o->children) { + i += count_filter_hrefs(&child, filter); + } + + return i; +} + +/** + * Sets a suitable filter effects area according to given blur radius, + * expansion and object size. + */ +static void set_filter_area(Inkscape::XML::Node *repr, gdouble radius, + double expansion, double expansionX, + double expansionY, double width, double height) +{ + // TODO: make this more generic, now assumed, that only the blur + // being added can affect the required filter area + + double rx = radius * (expansionY != 0 ? (expansion / expansionY) : 1); + double ry = radius * (expansionX != 0 ? (expansion / expansionX) : 1); + + if (width != 0 && height != 0) { + // If not within the default 10% margin (see + // http://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion), specify margins + // The 2.4 is an empirical coefficient: at that distance the cutoff is practically invisible + // (the opacity at 2.4*radius is about 3e-3) + double xmargin = 2.4 * (rx) / width; + double ymargin = 2.4 * (ry) / height; + + // TODO: set it in UserSpaceOnUse instead? + sp_repr_set_svg_double(repr, "x", -xmargin); + sp_repr_set_svg_double(repr, "width", 1 + 2 * xmargin); + sp_repr_set_svg_double(repr, "y", -ymargin); + sp_repr_set_svg_double(repr, "height", 1 + 2 * ymargin); + } +} + +SPFilter *new_filter(SPDocument *document) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new filter + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:filter"); + + // Inkscape now supports both sRGB and linear color-interpolation-filters. + // But, for the moment, keep sRGB as default value for new filters + // (historically set to sRGB and doesn't require conversion between + // filter cairo surfaces and other types of cairo surfaces). + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "color-interpolation-filters", "sRGB"); + sp_repr_css_change(repr, css, "style"); + sp_repr_css_attr_unref(css); + + // Append the new filter node to defs + defs->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPFilter *f = SP_FILTER( document->getObjectByRepr(repr) ); + + + g_assert(f != nullptr); + g_assert(SP_IS_FILTER(f)); + + return f; +} + +SPFilterPrimitive * +filter_add_primitive(SPFilter *filter, const Inkscape::Filters::FilterPrimitiveType type) +{ + Inkscape::XML::Document *xml_doc = filter->document->getReprDoc(); + + //create filter primitive node + Inkscape::XML::Node *repr; + repr = xml_doc->createElement(FPConverter.get_key(type).c_str()); + + // set default values + switch(type) { + case Inkscape::Filters::NR_FILTER_BLEND: + repr->setAttribute("mode", "normal"); + break; + case Inkscape::Filters::NR_FILTER_COLORMATRIX: + break; + case Inkscape::Filters::NR_FILTER_COMPONENTTRANSFER: + break; + case Inkscape::Filters::NR_FILTER_COMPOSITE: + break; + case Inkscape::Filters::NR_FILTER_CONVOLVEMATRIX: + repr->setAttribute("order", "3 3"); + repr->setAttribute("kernelMatrix", "0 0 0 0 0 0 0 0 0"); + break; + case Inkscape::Filters::NR_FILTER_DIFFUSELIGHTING: + break; + case Inkscape::Filters::NR_FILTER_DISPLACEMENTMAP: + break; + case Inkscape::Filters::NR_FILTER_FLOOD: + break; + case Inkscape::Filters::NR_FILTER_GAUSSIANBLUR: + repr->setAttribute("stdDeviation", "1"); + break; + case Inkscape::Filters::NR_FILTER_IMAGE: + break; + case Inkscape::Filters::NR_FILTER_MERGE: + break; + case Inkscape::Filters::NR_FILTER_MORPHOLOGY: + break; + case Inkscape::Filters::NR_FILTER_OFFSET: + repr->setAttribute("dx", "0"); + repr->setAttribute("dy", "0"); + break; + case Inkscape::Filters::NR_FILTER_SPECULARLIGHTING: + break; + case Inkscape::Filters::NR_FILTER_TILE: + break; + case Inkscape::Filters::NR_FILTER_TURBULENCE: + break; + default: + break; + } + + //set primitive as child of filter node + // XML tree being used directly while/where it shouldn't be... + filter->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE( filter->document->getObjectByRepr(repr) ); + + g_assert(prim != nullptr); + g_assert(SP_IS_FILTER_PRIMITIVE(prim)); + + return prim; +} + +/** + * Creates a filter with blur primitive of specified radius for an item with the given matrix expansion, width and height + */ +SPFilter * +new_filter_gaussian_blur (SPDocument *document, gdouble radius, double expansion, double expansionX, double expansionY, double width, double height) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new filter + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:filter"); + //repr->setAttribute("inkscape:collect", "always"); + + set_filter_area(repr, radius, expansion, expansionX, expansionY, + width, height); + + /* Inkscape now supports both sRGB and linear color-interpolation-filters. + * But, for the moment, keep sRGB as default value for new filters. + * historically set to sRGB and doesn't require conversion between + * filter cairo surfaces and other types of cairo surfaces. lp:1127103 */ + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "color-interpolation-filters", "sRGB"); + sp_repr_css_change(repr, css, "style"); + sp_repr_css_attr_unref(css); + + //create feGaussianBlur node + Inkscape::XML::Node *b_repr; + b_repr = xml_doc->createElement("svg:feGaussianBlur"); + //b_repr->setAttribute("inkscape:collect", "always"); + + double stdDeviation = radius; + if (expansion != 0) + stdDeviation /= expansion; + + //set stdDeviation attribute + sp_repr_set_svg_double(b_repr, "stdDeviation", stdDeviation); + + //set feGaussianBlur as child of filter node + repr->appendChild(b_repr); + Inkscape::GC::release(b_repr); + + // Append the new filter node to defs + defs->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPFilter *f = SP_FILTER( document->getObjectByRepr(repr) ); + SPGaussianBlur *b = SP_GAUSSIANBLUR( document->getObjectByRepr(b_repr) ); + + g_assert(f != nullptr); + g_assert(SP_IS_FILTER(f)); + g_assert(b != nullptr); + g_assert(SP_IS_GAUSSIANBLUR(b)); + + return f; +} + + +/** + * Creates a simple filter with a blend primitive and a blur primitive of specified radius for + * an item with the given matrix expansion, width and height + */ +static SPFilter * +new_filter_blend_gaussian_blur (SPDocument *document, const char *blendmode, gdouble radius, double expansion, + double expansionX, double expansionY, double width, double height) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new filter + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:filter"); + repr->setAttribute("inkscape:collect", "always"); + + /* Inkscape now supports both sRGB and linear color-interpolation-filters. + * But, for the moment, keep sRGB as default value for new filters. + * historically set to sRGB and doesn't require conversion between + * filter cairo surfaces and other types of cairo surfaces. lp:1127103 */ + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "color-interpolation-filters", "sRGB"); + sp_repr_css_change(repr, css, "style"); + sp_repr_css_attr_unref(css); + + // Append the new filter node to defs + defs->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPFilter *f = SP_FILTER( document->getObjectByRepr(repr) ); + // Gaussian blur primitive + if(radius != 0) { + set_filter_area(repr, radius, expansion, expansionX, expansionY, width, height); + //create feGaussianBlur node + Inkscape::XML::Node *b_repr; + b_repr = xml_doc->createElement("svg:feGaussianBlur"); + b_repr->setAttribute("inkscape:collect", "always"); + + double stdDeviation = radius; + if (expansion != 0) + stdDeviation /= expansion; + + //set stdDeviation attribute + sp_repr_set_svg_double(b_repr, "stdDeviation", stdDeviation); + + //set feGaussianBlur as child of filter node + repr->appendChild(b_repr); + Inkscape::GC::release(b_repr); + + SPGaussianBlur *b = SP_GAUSSIANBLUR( document->getObjectByRepr(b_repr) ); + g_assert(b != nullptr); + g_assert(SP_IS_GAUSSIANBLUR(b)); + } + // Blend primitive + if(strcmp(blendmode, "normal")) { + Inkscape::XML::Node *b_repr; + b_repr = xml_doc->createElement("svg:feBlend"); + b_repr->setAttribute("inkscape:collect", "always"); + b_repr->setAttribute("mode", blendmode); + b_repr->setAttribute("in2", "BackgroundImage"); + + // set feBlend as child of filter node + repr->appendChild(b_repr); + Inkscape::GC::release(b_repr); + + // Enable background image buffer for document + Inkscape::XML::Node *root = b_repr->root(); + if (!root->attribute("enable-background")) { + root->setAttribute("enable-background", "new"); + } + + SPFeBlend *b = SP_FEBLEND(document->getObjectByRepr(b_repr)); + g_assert(b != nullptr); + g_assert(SP_IS_FEBLEND(b)); + } + + g_assert(f != nullptr); + g_assert(SP_IS_FILTER(f)); + + return f; +} + +/** + * Creates a simple filter for the given item with blend and blur primitives, using the + * specified mode and radius, respectively + */ +SPFilter * +new_filter_simple_from_item (SPDocument *document, SPItem *item, const char *mode, gdouble radius) +{ + Geom::OptRect const r = item->desktopGeometricBounds(); + + double width; + double height; + if (r) { + width = r->dimensions()[Geom::X]; + height= r->dimensions()[Geom::Y]; + } else { + width = height = 0; + } + + Geom::Affine i2dt (item->i2dt_affine () ); + + return (new_filter_blend_gaussian_blur (document, mode, radius, i2dt.descrim(), i2dt.expansionX(), i2dt.expansionY(), width, height)); +} + +/** + * Modifies the gaussian blur applied to the item. + * If no filters are applied to given item, creates a new blur filter. + * If a filter is applied and it contains a blur, modify that blur. + * If the filter doesn't contain blur, a blur is added to the filter. + * Should there be more references to modified filter, that filter is + * duplicated, so that other elements referring that filter are not modified. + */ +/* TODO: this should be made more generic, not just for blurs */ +SPFilter *modify_filter_gaussian_blur_from_item(SPDocument *document, SPItem *item, + gdouble radius) +{ + if (!item->style || !item->style->filter.set) { + return new_filter_simple_from_item(document, item, "normal", radius); + } + + SPFilter *filter = SP_FILTER(item->style->getFilter()); + if (!filter) { + // We reach here when filter.set is true, but the href is not found in the document + return new_filter_simple_from_item(document, item, "normal", radius); + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // If there are more users for this filter, duplicate it + if (filter->hrefcount > count_filter_hrefs(item, filter)) { + Inkscape::XML::Node *repr = item->style->getFilter()->getRepr()->duplicate(xml_doc); + SPDefs *defs = document->getDefs(); + defs->appendChild(repr); + + filter = SP_FILTER( document->getObjectByRepr(repr) ); + Inkscape::GC::release(repr); + } + + // Determine the required standard deviation value + Geom::Affine i2d (item->i2dt_affine ()); + double expansion = i2d.descrim(); + double stdDeviation = radius; + if (expansion != 0) + stdDeviation /= expansion; + + // Get the object size + Geom::OptRect const r = item->desktopGeometricBounds(); + double width; + double height; + if (r) { + width = r->dimensions()[Geom::X]; + height= r->dimensions()[Geom::Y]; + } else { + width = height = 0; + } + + // Set the filter effects area + Inkscape::XML::Node *repr = item->style->getFilter()->getRepr(); + set_filter_area(repr, radius, expansion, i2d.expansionX(), + i2d.expansionY(), width, height); + + // Search for gaussian blur primitives. If found, set the stdDeviation + // of the first one and return. + Inkscape::XML::Node *primitive = repr->firstChild(); + while (primitive) { + if (strcmp("svg:feGaussianBlur", primitive->name()) == 0) { + sp_repr_set_svg_double(primitive, "stdDeviation", + stdDeviation); + return filter; + } + primitive = primitive->next(); + } + + // If there were no gaussian blur primitives, create a new one + + //create feGaussianBlur node + Inkscape::XML::Node *b_repr; + b_repr = xml_doc->createElement("svg:feGaussianBlur"); + //b_repr->setAttribute("inkscape:collect", "always"); + + //set stdDeviation attribute + sp_repr_set_svg_double(b_repr, "stdDeviation", stdDeviation); + + //set feGaussianBlur as child of filter node + filter->getRepr()->appendChild(b_repr); + Inkscape::GC::release(b_repr); + + return filter; +} + +void remove_filter (SPObject *item, bool recursive) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_unset_property(css, "filter"); + if (recursive) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } else { + sp_repr_css_change(item->getRepr(), css, "style"); + } + sp_repr_css_attr_unref(css); +} + +/** + * Removes the first feGaussianBlur from the filter attached to given item. + * Should this leave us with an empty filter, remove that filter. + */ +/* TODO: the removed filter primitive may had had a named result image, so + * after removing, the filter may be in erroneous state, this situation should + * be handled gracefully */ +void remove_filter_gaussian_blur (SPObject *item) +{ + if (item->style && item->style->filter.set && item->style->getFilter()) { + // Search for the first blur primitive and remove it. (if found) + Inkscape::XML::Node *repr = item->style->getFilter()->getRepr(); + Inkscape::XML::Node *primitive = repr->firstChild(); + while (primitive) { + if (strcmp("svg:feGaussianBlur", primitive->name()) == 0) { + sp_repr_unparent(primitive); + break; + } + primitive = primitive->next(); + } + + // If there are no more primitives left in this filter, discard it. + if (repr->childCount() == 0) { + remove_filter(item, false); + } + } +} + +/** + * Removes blend primitive from the filter attached to given item. + * Get if the filter have a < 1.0 blending filter and if it remove it + * @params: the item to remove filtered blend + */ +/* TODO: the removed filter primitive may had had a named result image, so + * after removing, the filter may be in erroneous state, this situation should + * be handled gracefully */ +void remove_filter_legacy_blend(SPObject *item) +{ + if (!item) { + return; + } + if (item->style && item->style->filter.set && item->style->getFilter()) { + // Search for the first blur primitive and remove it. (if found) + size_t blurcount = 0; + size_t blendcount = 0; + size_t total = 0; + // determine whether filter is simple (blend and/or blur) or complex + SPFeBlend *blend = nullptr; + for (auto &primitive_obj:item->style->getFilter()->children) { + SPFilterPrimitive *primitive = dynamic_cast(&primitive_obj); + if (primitive) { + if (dynamic_cast(primitive)) { + blend = dynamic_cast(primitive); + ++blendcount; + } + SPGaussianBlur *spgausian = dynamic_cast(primitive); + if (spgausian) { + ++blurcount; + } + ++total; + } + } + if (blend && total == 2 && blurcount == 1) { + blend->deleteObject(true); + } else if (total == 1) { + remove_filter(item, false); + } + } +} + +/** + * Get if the filter have a < 1.0 blending filter + * @params: the item to get filtered blend + */ +SPBlendMode filter_get_legacy_blend(SPObject *item) +{ + auto blend = SP_CSS_BLEND_NORMAL; + if (!item) { + return blend; + } + if (item->style && item->style->filter.set && item->style->getFilter()) { + // Search for the first blur primitive and remove it. (if found) + size_t blurcount = 0; + size_t blendcount = 0; + size_t total = 0; + // determine whether filter is simple (blend and/or blur) or complex + for (auto &primitive_obj:item->style->getFilter()->children) { + SPFilterPrimitive *primitive = dynamic_cast(&primitive_obj); + if (primitive) { + SPFeBlend *spblend = dynamic_cast(primitive); + if (spblend) { + ++blendcount; + blend = spblend->blend_mode; + } + SPGaussianBlur *spgausian = dynamic_cast(primitive); + if (spgausian) { + ++blurcount; + } + ++total; + } + } + if (!((blend && total == 2 && blurcount == 1) || total == 1)) { + blend = SP_CSS_BLEND_NORMAL; + } + } + return blend; +} + +bool filter_is_single_gaussian_blur(SPFilter *filter) +{ + return (filter->children.size() == 1 && + SP_IS_GAUSSIANBLUR(&filter->children.front())); +} + +double get_single_gaussian_blur_radius(SPFilter *filter) +{ + if (filter->children.size() == 1 && + SP_IS_GAUSSIANBLUR(&filter->children.front())) { + + SPGaussianBlur *gb = SP_GAUSSIANBLUR(filter->firstChild()); + double x = gb->stdDeviation.getNumber(); + double y = gb->stdDeviation.getOptNumber(); + if (x > 0 && y > 0) { + return MAX(x, y); + } + return x; + } + return 0.0; +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/filter-chemistry.h b/src/filter-chemistry.h new file mode 100644 index 0000000..de25cb6 --- /dev/null +++ b/src/filter-chemistry.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Various utility methods for filters + * + * Authors: + * Hugo Rodrigues + * bulia byak + * Niko Kiirala + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_FILTER_CHEMISTRY_H +#define SEEN_SP_FILTER_CHEMISTRY_H + +#include "display/nr-filter-types.h" +#include "style-enums.h" + +class SPDocument; +class SPFilter; +class SPFilterPrimitive; +class SPItem; +class SPObject; + +SPFilterPrimitive *filter_add_primitive(SPFilter *filter, Inkscape::Filters::FilterPrimitiveType); +SPFilter *new_filter (SPDocument *document); +SPFilter *new_filter_gaussian_blur (SPDocument *document, double stdDeviation, double expansion, double expansionX, double expansionY, double width, double height); +SPFilter *new_filter_simple_from_item (SPDocument *document, SPItem *item, const char *mode, double stdDeviation); +SPFilter *modify_filter_gaussian_blur_from_item (SPDocument *document, SPItem *item, double stdDeviation); +void remove_filter (SPObject *item, bool recursive); +void remove_filter_gaussian_blur (SPObject *item); +void remove_filter_legacy_blend(SPObject *item); +SPBlendMode filter_get_legacy_blend(SPObject *item); +bool filter_is_single_gaussian_blur(SPFilter *filter); +double get_single_gaussian_blur_radius(SPFilter *filter); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/filter-enums.cpp b/src/filter-enums.cpp new file mode 100644 index 0000000..827680b --- /dev/null +++ b/src/filter-enums.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Conversion data for filter and filter primitive enumerations + * + * Authors: + * Nicholas Bishop + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "filter-enums.h" + +using Inkscape::Util::EnumData; +using Inkscape::Util::EnumDataConverter; + +const EnumData FPData[Inkscape::Filters::NR_FILTER_ENDPRIMITIVETYPE] = { + {Inkscape::Filters::NR_FILTER_BLEND, _("Blend"), "svg:feBlend"}, + {Inkscape::Filters::NR_FILTER_COLORMATRIX, _("Color Matrix"), "svg:feColorMatrix"}, + {Inkscape::Filters::NR_FILTER_COMPONENTTRANSFER, _("Component Transfer"), "svg:feComponentTransfer"}, + {Inkscape::Filters::NR_FILTER_COMPOSITE, _("Composite"), "svg:feComposite"}, + {Inkscape::Filters::NR_FILTER_CONVOLVEMATRIX, _("Convolve Matrix"), "svg:feConvolveMatrix"}, + {Inkscape::Filters::NR_FILTER_DIFFUSELIGHTING, _("Diffuse Lighting"), "svg:feDiffuseLighting"}, + {Inkscape::Filters::NR_FILTER_DISPLACEMENTMAP, _("Displacement Map"), "svg:feDisplacementMap"}, + {Inkscape::Filters::NR_FILTER_FLOOD, _("Flood"), "svg:feFlood"}, + {Inkscape::Filters::NR_FILTER_GAUSSIANBLUR, _("Gaussian Blur"), "svg:feGaussianBlur"}, + {Inkscape::Filters::NR_FILTER_IMAGE, _("Image"), "svg:feImage"}, + {Inkscape::Filters::NR_FILTER_MERGE, _("Merge"), "svg:feMerge"}, + {Inkscape::Filters::NR_FILTER_MORPHOLOGY, _("Morphology"), "svg:feMorphology"}, + {Inkscape::Filters::NR_FILTER_OFFSET, _("Offset"), "svg:feOffset"}, + {Inkscape::Filters::NR_FILTER_SPECULARLIGHTING, _("Specular Lighting"), "svg:feSpecularLighting"}, + {Inkscape::Filters::NR_FILTER_TILE, _("Tile"), "svg:feTile"}, + {Inkscape::Filters::NR_FILTER_TURBULENCE, _("Turbulence"), "svg:feTurbulence"} +}; +const EnumDataConverter FPConverter(FPData, Inkscape::Filters::NR_FILTER_ENDPRIMITIVETYPE); + +const EnumData FPInputData[FPINPUT_END] = { + {FPINPUT_SOURCEGRAPHIC, _("Source Graphic"), "SourceGraphic"}, + {FPINPUT_SOURCEALPHA, _("Source Alpha"), "SourceAlpha"}, + {FPINPUT_BACKGROUNDIMAGE, _("Background Image"), "BackgroundImage"}, + {FPINPUT_BACKGROUNDALPHA, _("Background Alpha"), "BackgroundAlpha"}, + {FPINPUT_FILLPAINT, _("Fill Paint"), "FillPaint"}, + {FPINPUT_STROKEPAINT, _("Stroke Paint"), "StrokePaint"}, +}; +const EnumDataConverter FPInputConverter(FPInputData, FPINPUT_END); + +const EnumData ColorMatrixTypeData[Inkscape::Filters::COLORMATRIX_ENDTYPE] = { + {Inkscape::Filters::COLORMATRIX_MATRIX, _("Matrix"), "matrix"}, + {Inkscape::Filters::COLORMATRIX_SATURATE, _("Saturate"), "saturate"}, + {Inkscape::Filters::COLORMATRIX_HUEROTATE, _("Hue Rotate"), "hueRotate"}, + {Inkscape::Filters::COLORMATRIX_LUMINANCETOALPHA, _("Luminance to Alpha"), "luminanceToAlpha"} +}; +const EnumDataConverter ColorMatrixTypeConverter(ColorMatrixTypeData, Inkscape::Filters::COLORMATRIX_ENDTYPE); + +// feComposite +const EnumData CompositeOperatorData[COMPOSITE_ENDOPERATOR] = { + {COMPOSITE_DEFAULT, _("Default"), "" }, + {COMPOSITE_OVER, _("Over"), "over" }, + {COMPOSITE_IN, _("In"), "in" }, + {COMPOSITE_OUT, _("Out"), "out" }, + {COMPOSITE_ATOP, _("Atop"), "atop" }, + {COMPOSITE_XOR, _("XOR"), "xor" }, +#ifdef WITH_CSSCOMPOSITE +// New CSS + {COMPOSITE_CLEAR, _("Clear"), "clear" }, + {COMPOSITE_COPY, _("Copy"), "copy" }, + {COMPOSITE_DESTINATION, _("Destination"), "destination" }, + {COMPOSITE_DESTINATION_OVER, _("Destination Over"),"destination-over" }, + {COMPOSITE_DESTINATION_IN, _("Destination In"), "destination-in" }, + {COMPOSITE_DESTINATION_OUT, _("Destination Out"), "destination-out" }, + {COMPOSITE_DESTINATION_ATOP, _("Destination Atop"),"destination-atop" }, + {COMPOSITE_LIGHTER, _("Lighter"), "lighter" }, +#endif + {COMPOSITE_ARITHMETIC, _("Arithmetic"), "arithmetic" } +}; +const EnumDataConverter CompositeOperatorConverter(CompositeOperatorData, COMPOSITE_ENDOPERATOR); + +// feComponentTransfer +const EnumData ComponentTransferTypeData[Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR] = { + {Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY, _("Identity"), "identity"}, + {Inkscape::Filters::COMPONENTTRANSFER_TYPE_TABLE, _("Table"), "table"}, + {Inkscape::Filters::COMPONENTTRANSFER_TYPE_DISCRETE, _("Discrete"), "discrete"}, + {Inkscape::Filters::COMPONENTTRANSFER_TYPE_LINEAR, _("Linear"), "linear"}, + {Inkscape::Filters::COMPONENTTRANSFER_TYPE_GAMMA, _("Gamma"), "gamma"}, +}; +const EnumDataConverter ComponentTransferTypeConverter(ComponentTransferTypeData, Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR); + +// feConvolveMatrix +const EnumData ConvolveMatrixEdgeModeData[Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_ENDTYPE] = { + {Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE, _("Duplicate"), "duplicate"}, + {Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_WRAP, _("Wrap"), "wrap"}, + {Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_NONE, C_("Convolve matrix, edge mode", "None"), "none"} +}; +const EnumDataConverter ConvolveMatrixEdgeModeConverter(ConvolveMatrixEdgeModeData, Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_ENDTYPE); + +// feDisplacementMap +const EnumData DisplacementMapChannelData[DISPLACEMENTMAP_CHANNEL_ENDTYPE] = { + {DISPLACEMENTMAP_CHANNEL_RED, _("Red"), "R"}, + {DISPLACEMENTMAP_CHANNEL_GREEN, _("Green"), "G"}, + {DISPLACEMENTMAP_CHANNEL_BLUE, _("Blue"), "B"}, + {DISPLACEMENTMAP_CHANNEL_ALPHA, _("Alpha"), "A"} +}; +const EnumDataConverter DisplacementMapChannelConverter(DisplacementMapChannelData, DISPLACEMENTMAP_CHANNEL_ENDTYPE); + +// feMorphology +const EnumData MorphologyOperatorData[Inkscape::Filters::MORPHOLOGY_OPERATOR_END] = { + {Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE, _("Erode"), "erode"}, + {Inkscape::Filters::MORPHOLOGY_OPERATOR_DILATE, _("Dilate"), "dilate"} +}; +const EnumDataConverter MorphologyOperatorConverter(MorphologyOperatorData, Inkscape::Filters::MORPHOLOGY_OPERATOR_END); + +// feTurbulence +const EnumData TurbulenceTypeData[Inkscape::Filters::TURBULENCE_ENDTYPE] = { + {Inkscape::Filters::TURBULENCE_FRACTALNOISE, _("Fractal Noise"), "fractalNoise"}, + {Inkscape::Filters::TURBULENCE_TURBULENCE, _("Turbulence"), "turbulence"} +}; +const EnumDataConverter TurbulenceTypeConverter(TurbulenceTypeData, Inkscape::Filters::TURBULENCE_ENDTYPE); + +// Light source +const EnumData LightSourceData[LIGHT_ENDSOURCE] = { + {LIGHT_DISTANT, _("Distant Light"), "svg:feDistantLight"}, + {LIGHT_POINT, _("Point Light"), "svg:fePointLight"}, + {LIGHT_SPOT, _("Spot Light"), "svg:feSpotLight"} +}; +const EnumDataConverter LightSourceConverter(LightSourceData, LIGHT_ENDSOURCE); + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/filter-enums.h b/src/filter-enums.h new file mode 100644 index 0000000..6136346 --- /dev/null +++ b/src/filter-enums.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_FILTER_ENUMS_H__ +#define __SP_FILTER_ENUMS_H__ + +/* + * Conversion data for filter and filter primitive enumerations + * + * Authors: + * Nicholas Bishop + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/nr-filter-blend.h" +#include "display/nr-filter-colormatrix.h" +#include "display/nr-filter-component-transfer.h" +#include "display/nr-filter-composite.h" +#include "display/nr-filter-convolve-matrix.h" +#include "display/nr-filter-morphology.h" +#include "display/nr-filter-turbulence.h" +#include "display/nr-filter-types.h" +#include "object/filters/displacementmap.h" +#include "util/enums.h" + +// Filter primitives +extern const Inkscape::Util::EnumData FPData[Inkscape::Filters::NR_FILTER_ENDPRIMITIVETYPE]; +extern const Inkscape::Util::EnumDataConverter FPConverter; + +enum FilterPrimitiveInput { + FPINPUT_SOURCEGRAPHIC, + FPINPUT_SOURCEALPHA, + FPINPUT_BACKGROUNDIMAGE, + FPINPUT_BACKGROUNDALPHA, + FPINPUT_FILLPAINT, + FPINPUT_STROKEPAINT, + FPINPUT_END +}; + +extern const Inkscape::Util::EnumData FPInputData[FPINPUT_END]; +extern const Inkscape::Util::EnumDataConverter FPInputConverter; + +// ColorMatrix type +extern const Inkscape::Util::EnumData ColorMatrixTypeData[Inkscape::Filters::COLORMATRIX_ENDTYPE]; +extern const Inkscape::Util::EnumDataConverter ColorMatrixTypeConverter; +// ComponentTransfer type +extern const Inkscape::Util::EnumData ComponentTransferTypeData[Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR]; +extern const Inkscape::Util::EnumDataConverter ComponentTransferTypeConverter; +// Composite operator +extern const Inkscape::Util::EnumData CompositeOperatorData[COMPOSITE_ENDOPERATOR]; +extern const Inkscape::Util::EnumDataConverter CompositeOperatorConverter; +// ConvolveMatrix edgeMode +extern const Inkscape::Util::EnumData ConvolveMatrixEdgeModeData[Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_ENDTYPE]; +extern const Inkscape::Util::EnumDataConverter ConvolveMatrixEdgeModeConverter; +// DisplacementMap channel +extern const Inkscape::Util::EnumData DisplacementMapChannelData[4]; +extern const Inkscape::Util::EnumDataConverter DisplacementMapChannelConverter; +// Morphology operator +extern const Inkscape::Util::EnumData MorphologyOperatorData[Inkscape::Filters::MORPHOLOGY_OPERATOR_END]; +extern const Inkscape::Util::EnumDataConverter MorphologyOperatorConverter; +// Turbulence type +extern const Inkscape::Util::EnumData TurbulenceTypeData[Inkscape::Filters::TURBULENCE_ENDTYPE]; +extern const Inkscape::Util::EnumDataConverter TurbulenceTypeConverter; +// Lighting +enum LightSource { + LIGHT_DISTANT, + LIGHT_POINT, + LIGHT_SPOT, + LIGHT_ENDSOURCE +}; +extern const Inkscape::Util::EnumData LightSourceData[LIGHT_ENDSOURCE]; +extern const Inkscape::Util::EnumDataConverter LightSourceConverter; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gc-anchored.cpp b/src/gc-anchored.cpp new file mode 100644 index 0000000..12eaa83 --- /dev/null +++ b/src/gc-anchored.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::GC::Anchored - base class for anchored GC-managed objects + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "gc-anchored.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" +#include "debug/demangle.h" +#include "util/format.h" + +namespace Inkscape { + +namespace GC { + +namespace { + +typedef Debug::SimpleEvent RefCountEvent; + +class BaseAnchorEvent : public RefCountEvent { +public: + BaseAnchorEvent(Anchored const *object, int bias, + char const *name) + : RefCountEvent(name) + { + _addProperty("base", Util::format("%p", Core::base(const_cast(object))).pointer()); + _addProperty("pointer", Util::format("%p", object).pointer()); + _addProperty("class", Debug::demangle(typeid(*object).name())); + _addProperty("new-refcount", object->_anchored_refcount() + bias); + } +}; + +class AnchorEvent : public BaseAnchorEvent { +public: + AnchorEvent(Anchored const *object) + : BaseAnchorEvent(object, 1, "gc-anchor") + {} +}; + +class ReleaseEvent : public BaseAnchorEvent { +public: + ReleaseEvent(Anchored const *object) + : BaseAnchorEvent(object, -1, "gc-release") + {} +}; + +} + +Anchored::Anchor *Anchored::_new_anchor() const { + return new Anchor(this); +} + +void Anchored::_free_anchor(Anchored::Anchor *anchor) const { + delete anchor; +} + +void Anchored::anchor() const { + Debug::EventTracker tracker(this); + if (!_anchor) { + _anchor = _new_anchor(); + } + _anchor->refcount++; +} + +void Anchored::release() const { + Debug::EventTracker tracker(this); + g_return_if_fail(_anchor); + if (!--_anchor->refcount) { + _free_anchor(_anchor); + _anchor = nullptr; + } +} + +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gc-anchored.h b/src/gc-anchored.h new file mode 100644 index 0000000..8a7b2d7 --- /dev/null +++ b/src/gc-anchored.h @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY + * * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_ANCHORED_H +#define SEEN_INKSCAPE_GC_ANCHORED_H + +#include "inkgc/gc-managed.h" + +namespace Inkscape { + +namespace GC { + +/** + * A base class for anchored objects. + * + * Objects are managed by our mark-and-sweep collector, but are anchored + * against garbage collection so long as their reference count is nonzero. + * + * Object and member destructors will not be called on destruction + * unless a subclass also inherits from Inkscape::GC::Finalized. + * + * New instances of anchored objects should be created using the C++ new + * operator. Under normal circumstances they should not be created on + * the stack. + * + * A newly created anchored object begins with a refcount of one, and + * will not be collected unless the refcount is zero. + * + * NOTE: If you create an object yourself, it is already anchored for + * you. You do not need to anchor it a second time. + * + * Note that a cycle involving an anchored object (with nonzero refcount) + * cannot be collected. To avoid this, don't increment refcounts for + * pointers between two GC-managed objects. + * + * @see Inkscape::GC::Managed + * @see Inkscape::GC::Finalized + * @see Inkscape::GC::anchor + * @see Inkscape::GC::release + */ + +class Anchored { +public: + void anchor() const; + void release() const; + + // for debugging + unsigned _anchored_refcount() const { + return ( _anchor ? _anchor->refcount : 0 ); + } + + Anchored(Anchored const &) = delete; // no copy + void operator=(Anchored const &) = delete; // no assign + +protected: + Anchored() : _anchor(nullptr) { anchor(); } // initial refcount of one + virtual ~Anchored() = default; + +private: + struct Anchor : public Managed { + Anchor() : refcount(0),base(nullptr) {} + Anchor(Anchored const *obj) : refcount(0) { + base = Core::base(const_cast(obj)); + } + int refcount; + void *base; + }; + + mutable Anchor *_anchor; + + Anchor *_new_anchor() const; + void _free_anchor(Anchor *anchor) const; +}; + +/** + * @brief Increments the reference count of a anchored object. + * + * This function template generates functions which take + * a reference to a anchored object of a given type, increment + * that object's reference count, and return a reference to the + * object of the same type as the function's parameter. + * + * @param m a reference to a anchored object + * + * @return the reference to the object + */ +template +static R &anchor(R &r) { + static_cast(const_cast(r)).anchor(); + return r; +} + +/** + * @brief Increments the reference count of a anchored object. + * + * This function template generates functions which take + * a pointer to a anchored object of a given type, increment + * that object's reference count, and return a pointer to the + * object of the same type as the function's parameter. + * + * @param m a pointer to anchored object + * + * @return the pointer to the object + */ +template +static R *anchor(R *r) { + static_cast(const_cast(r))->anchor(); + return r; +} + +/** + * @brief Decrements the reference count of a anchored object. + * + * This function template generates functions which take + * a reference to a anchored object of a given type, increment + * that object's reference count, and return a reference to the + * object of the same type as the function's parameter. + * + * The return value is safe to use since the object, even if + * its refcount has reached zero, will not actually be collected + * until there are no references to it in local variables or + * parameters. + * + * @param m a reference to a anchored object + * + * @return the reference to the object + */ +template +static R &release(R &r) { + static_cast(const_cast(r)).release(); + return r; +} + +/** + * @brief Decrements the reference count of a anchored object. + * + * This function template generates functions which take + * a pointer to a anchored object of a given type, increment + * that object's reference count, and return a pointer to the + * object of the same type as the function's parameter. + * + * The return value is safe to use since the object, even if + * its refcount has reached zero, will not actually be collected + * until there are no references to it in local variables or + * parameters. + * + * @param m a pointer to a anchored object + * + * @return the pointer to the object + */ +template +static R *release(R *r) { + static_cast(const_cast(r))->release(); + return r; +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gc-finalized.cpp b/src/gc-finalized.cpp new file mode 100644 index 0000000..8bba510 --- /dev/null +++ b/src/gc-finalized.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Inkscape::GC::Finalized - mixin for GC-managed objects with non-trivial + * destructors + * + * Copyright 2006 MenTaLguY + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * See the file COPYING for details. + * + */ + +#include +#include "debug/simple-event.h" +#include "debug/event-tracker.h" +#include "util/format.h" +#include "gc-finalized.h" + +namespace Inkscape { + +namespace GC { + +namespace { + +// workaround for g++ 4.0.2 +typedef Debug::SimpleEvent BaseEvent; + +class FinalizerEvent : public BaseEvent { +public: + FinalizerEvent(Finalized *object) + : BaseEvent("gc-finalizer") + { + _addProperty("base", Util::format("%p", Core::base(object)).pointer()); + _addProperty("pointer", Util::format("%p", object).pointer()); + _addProperty("class", typeid(*object).name()); + } +}; + +} + +void Finalized::_invoke_dtor(void *base, void *offset) { + Finalized *object=_unoffset(base, offset); + Debug::EventTracker tracker(object); + object->~Finalized(); +} + +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gc-finalized.h b/src/gc-finalized.h new file mode 100644 index 0000000..6bd702c --- /dev/null +++ b/src/gc-finalized.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::GC::Finalized - mixin for GC-managed objects with non-trivial + * destructors + *//* + * Authors: + * see git history + * MenTaLguY + * + * Copyright (C) 2004-2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_FINALIZED_H +#define SEEN_INKSCAPE_GC_FINALIZED_H + +#include +#include +#include "inkgc/gc-core.h" + +namespace Inkscape { + +namespace GC { + +/* @brief A mix-in ensuring that an object's destructor will get called before + * the garbage collector destroys it + * + * Normally, the garbage collector does not call destructors before destroying + * an object. On construction, this "mix-in" will register a finalizer + * function to call destructors before derived objects are destroyed. + * + * This works pretty well, with the following caveats: + * + * 1. The garbage collector uses strictly topologically-ordered + * finalization; if objects with finalizers reference each other + * directly or indirectly, the collector will refuse to finalize (and + * therefore free) them. You'll see a warning on the console if this + * happens. + * + * The best way to limit this effect is to only make "leaf" objects + * (i.e. those that don't point to other finalizable objects) + * finalizable, and otherwise use GC::soft_ptr<> instead of a regular + * pointer for "backreferences" (e.g. parent pointers in a tree + * structure), so that those references can be cleared to break any + * finalization cycles. + * + * @see Inkscape::GC::soft_ptr<> + * + * 2. Because there is no guarantee when the collector will destroy + * objects, it is impossible to tell in advance when the destructor + * will get called. It may not get called until the very end + * of the program, or ever. + * + * 3. If allocated in arrays, only the first object in the array will + * have its destructor called, unless you make other arrangements by + * registering your own finalizer instead. + * + * 4. Similarly, putting a finalized object as a member in another + * garbage collected but non-finalized object will cause the member + * object's destructor not to be called when the parent object is + * collected, unless you register the finalizer yourself (by "member" + * we mean an actual by-value member, not a reference or a pointer). + */ +class Finalized { +public: + Finalized() { + void *base=Core::base(this); + if (base) { // only if we are managed by the collector + CleanupFunc old_cleanup; + void *old_data; + + // the finalization callback needs to know the value of 'this' + // to call the destructor, but registering a real pointer to + // ourselves would pin us forever and prevent us from being + // finalized; instead we use an offset-from-base-address + + Core::register_finalizer_ignore_self(base, _invoke_dtor, + _offset(base, this), + &old_cleanup, &old_data); + + if (old_cleanup) { + // If there was already a finalizer registered for our + // base address, there are two main possibilities: + // + // 1. one of our members is also a GC::Finalized and had + // already registered a finalizer -- keep ours, since + // it will call that member's destructor, too + // + // 2. someone else registered a finalizer and we will have + // to trust that they will call the destructor -- put + // the existing finalizer back + // + // It's also possible that a member's constructor was called + // after ours (e.g. via placement new). Don't do that. + + if ( old_cleanup != _invoke_dtor ) { + Core::register_finalizer_ignore_self(base, + old_cleanup, old_data, + nullptr, nullptr); + } + } + } + } + + virtual ~Finalized() { + // make sure the destructor won't get invoked twice + Core::register_finalizer_ignore_self(Core::base(this), + nullptr, nullptr, nullptr, nullptr); + } + +private: + /// invoke the destructor for an object given a base and offset pair + static void _invoke_dtor(void *base, void *offset); + + /// turn 'this' pointer into an offset-from-base-address (stored as void *) + static void *_offset(void *base, Finalized *self) { + return reinterpret_cast( + reinterpret_cast(self) - reinterpret_cast(base) + ); + } + /// reconstitute 'this' given an offset-from-base-address in a void * + static Finalized *_unoffset(void *base, void *offset) { + return reinterpret_cast( + reinterpret_cast(base) + + reinterpret_cast(offset) + ); + } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gradient-chemistry.cpp b/src/gradient-chemistry.cpp new file mode 100644 index 0000000..700544e --- /dev/null +++ b/src/gradient-chemistry.cpp @@ -0,0 +1,1663 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Various utility methods for gradients + * + * Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2010 Authors + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include <2geom/transforms.h> +#include <2geom/bezier-curve.h> +#include <2geom/crossing.h> +#include <2geom/line.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "style.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" + +#include "ui/tools/tool-base.h" + +#include "widgets/gradient-vector.h" + +#define noSP_GR_VERBOSE + +using Inkscape::DocumentUndo; + +namespace { + +Inkscape::PaintTarget paintTargetItems[] = {Inkscape::FOR_FILL, Inkscape::FOR_STROKE}; + +std::vector vectorOfPaintTargets(paintTargetItems, paintTargetItems + (sizeof(paintTargetItems) / sizeof(paintTargetItems[0]))); + +} // namespace + +namespace Inkscape { + +std::vector const &allPaintTargets() +{ + return vectorOfPaintTargets; +} + +} // namespace Inkscape + +// Terminology: +// +// "vector" is a gradient that has stops but not position coords. It can be referenced by one or +// more privates. Objects should not refer to it directly. It has no radial/linear distinction. +// +// "array" is a gradient that has mesh rows and patches. It may or may not have "x" and "y" attributes. +// An array does have spacial information so it cannot be normalized like a "vector". +// +// "shared" is either a "vector" or "array" that is shared between multiple objects. +// +// "private" is a gradient that is not shared. A private linear or radial gradient has no stops but +// has position coords (e.g. center, radius etc for a radial); it references a "vector" for the +// actual colors. A mesh may or may not reference an array. Each private is only used by one object. + +static void sp_gradient_repr_set_link(Inkscape::XML::Node *repr, SPGradient *gr); + +SPGradient *sp_gradient_ensure_vector_normalized(SPGradient *gr) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_ensure_vector_normalized(%p)", gr); +#endif + g_return_val_if_fail(gr != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(gr), NULL); + g_return_val_if_fail(!SP_IS_MESHGRADIENT(gr), NULL); + + /* If we are already normalized vector, just return */ + if (gr->state == SP_GRADIENT_STATE_VECTOR) return gr; + /* Fail, if we have wrong state set */ + if (gr->state != SP_GRADIENT_STATE_UNKNOWN) { + g_warning("file %s: line %d: Cannot normalize private gradient to vector (%s)", __FILE__, __LINE__, gr->getId()); + return nullptr; + } + + /* First make sure we have vector directly defined (i.e. gr has its own stops) */ + if ( !gr->hasStops() ) { + /* We do not have stops ourselves, so flatten stops as well */ + gr->ensureVector(); + g_assert(gr->vector.built); + // this adds stops from gr->vector as children to gr + gr->repr_write_vector (); + } + + /* If gr hrefs some other gradient, remove the href */ + if (gr->ref){ + if (gr->ref->getObject()) { + // We are hrefing someone, so require flattening + gr->updateRepr(SP_OBJECT_WRITE_EXT | SP_OBJECT_WRITE_ALL); + sp_gradient_repr_set_link(gr->getRepr(), nullptr); + } + } + + /* Everything is OK, set state flag */ + gr->state = SP_GRADIENT_STATE_VECTOR; + return gr; +} + +/** + * Creates new private gradient for the given shared gradient. + */ + +static SPGradient *sp_gradient_get_private_normalized(SPDocument *document, SPGradient *shared, SPGradientType type) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_get_private_normalized(%p, %p, %d)", document, shared, type); +#endif + + g_return_val_if_fail(document != nullptr, NULL); + g_return_val_if_fail(shared != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(shared), NULL); + g_return_val_if_fail(shared->hasStops() || shared->hasPatches(), NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + // create a new private gradient of the requested type + Inkscape::XML::Node *repr; + if (type == SP_GRADIENT_TYPE_LINEAR) { + repr = xml_doc->createElement("svg:linearGradient"); + } else if(type == SP_GRADIENT_TYPE_RADIAL) { + repr = xml_doc->createElement("svg:radialGradient"); + } else { + repr = xml_doc->createElement("svg:meshgradient"); + } + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // link to shared + sp_gradient_repr_set_link(repr, shared); + + /* Append the new private gradient to defs */ + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPGradient *gr = static_cast(document->getObjectByRepr(repr)); + g_assert(gr != nullptr); + g_assert(SP_IS_GRADIENT(gr)); + + return gr; +} + +/** +Count how many times gr is used by the styles of o and its descendants +*/ +static guint count_gradient_hrefs(SPObject *o, SPGradient *gr) +{ + if (!o) + return 1; + + guint i = 0; + + SPStyle *style = o->style; + if (style + && style->fill.isPaintserver() + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) + && SP_GRADIENT(SP_STYLE_FILL_SERVER(style)) == gr) + { + i ++; + } + if (style + && style->stroke.isPaintserver() + && SP_IS_GRADIENT(SP_STYLE_STROKE_SERVER(style)) + && SP_GRADIENT(SP_STYLE_STROKE_SERVER(style)) == gr) + { + i ++; + } + + for (auto& child: o->children) { + i += count_gradient_hrefs(&child, gr); + } + + return i; +} + + +/** + * If gr has other users, create a new shared; also check if gr links to shared, relink if not + */ +static SPGradient *sp_gradient_fork_private_if_necessary(SPGradient *gr, SPGradient *shared, + SPGradientType type, SPObject *o) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_fork_private_if_necessary(%p, %p, %d, %p)", gr, shared, type, o); +#endif + g_return_val_if_fail(gr != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(gr), NULL); + + // Orphaned gradient, no shared with stops or patches at the end of the line; this used to be + // an assert + if ( !shared || !(shared->hasStops() || shared->hasPatches()) ) { + std::cerr << "sp_gradient_fork_private_if_necessary: Orphaned gradient" << std::endl; + return (gr); + } + + // user is the object that uses this gradient; normally it's item but for tspans, we + // check its ancestor text so that tspans don't get different gradients from their + // texts. + SPObject *user = o; + while (SP_IS_TSPAN(user)) { + user = user->parent; + } + + // Check the number of uses of the gradient within this object; + // if we are private and there are no other users, + if (!shared->isSwatch() && (gr->hrefcount <= count_gradient_hrefs(user, gr))) { + // check shared + if ( gr != shared && gr->ref->getObject() != shared ) { + /* our href is not the shared, and shared is different from gr; relink */ + sp_gradient_repr_set_link(gr->getRepr(), shared); + } + return gr; + } + + SPDocument *doc = gr->document; + SPObject *defs = doc->getDefs(); + + if ((gr->hasStops()) || + (gr->hasPatches()) || + (gr->state != SP_GRADIENT_STATE_UNKNOWN) || + (gr->parent != SP_OBJECT(defs)) || + (gr->hrefcount > 1)) { + + // we have to clone a fresh new private gradient for the given shared + + // create an empty one + SPGradient *gr_new = sp_gradient_get_private_normalized(doc, shared, type); + + // copy all the attributes to it + Inkscape::XML::Node *repr_new = gr_new->getRepr(); + Inkscape::XML::Node *repr = gr->getRepr(); + repr_new->setAttribute("gradientUnits", repr->attribute("gradientUnits")); + repr_new->setAttribute("gradientTransform", repr->attribute("gradientTransform")); + if (SP_IS_RADIALGRADIENT(gr)) { + repr_new->setAttribute("cx", repr->attribute("cx")); + repr_new->setAttribute("cy", repr->attribute("cy")); + repr_new->setAttribute("fx", repr->attribute("fx")); + repr_new->setAttribute("fy", repr->attribute("fy")); + repr_new->setAttribute("r", repr->attribute("r" )); + repr_new->setAttribute("fr", repr->attribute("fr")); + repr_new->setAttribute("spreadMethod", repr->attribute("spreadMethod")); + } else if (SP_IS_LINEARGRADIENT(gr)) { + repr_new->setAttribute("x1", repr->attribute("x1")); + repr_new->setAttribute("y1", repr->attribute("y1")); + repr_new->setAttribute("x2", repr->attribute("x2")); + repr_new->setAttribute("y2", repr->attribute("y2")); + repr_new->setAttribute("spreadMethod", repr->attribute("spreadMethod")); + } else { // Mesh + repr_new->setAttribute("x", repr->attribute("x")); + repr_new->setAttribute("y", repr->attribute("y")); + repr_new->setAttribute("type", repr->attribute("type")); + + // We probably want a completely separate mesh gradient so + // copy the children and unset the link to the shared. + for ( Inkscape::XML::Node *child = repr->firstChild() ; child ; child = child->next() ) { + Inkscape::XML::Node *copy = child->duplicate(doc->getReprDoc()); + repr_new->appendChild( copy ); + Inkscape::GC::release( copy ); + } + sp_gradient_repr_set_link(repr_new, nullptr); + } + return gr_new; + } else { + return gr; + } +} + +SPGradient *sp_gradient_fork_vector_if_necessary(SPGradient *gr) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_fork_vector_if_necessary(%p)", gr); +#endif + // Some people actually prefer their gradient vectors to be shared... + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/options/forkgradientvectors/value", true)) + return gr; + + if (gr->hrefcount > 1) { + SPDocument *doc = gr->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = gr->getRepr()->duplicate(xml_doc); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + SPGradient *gr_new = static_cast(doc->getObjectByRepr(repr)); + gr_new = sp_gradient_ensure_vector_normalized (gr_new); + Inkscape::GC::release(repr); + return gr_new; + } + return gr; +} + +/** + * Obtain the vector from the gradient. A forked vector will be created and linked to this gradient if another gradient uses it. + */ +SPGradient *sp_gradient_get_forked_vector_if_necessary(SPGradient *gradient, bool force_vector) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_get_forked_vector_if_necessary(%p, %d)", gradient, force_vector); +#endif + SPGradient *vector = gradient->getVector(force_vector); + vector = sp_gradient_fork_vector_if_necessary (vector); + if ( gradient != vector && gradient->ref->getObject() != vector ) { + sp_gradient_repr_set_link(gradient->getRepr(), vector); + } + return vector; +} + + +/** + * Convert an item's gradient to userspace _without_ preserving coords, setting them to defaults + * instead. No forking or reapplying is done because this is only called for newly created privates. + * @return The new gradient. + */ +SPGradient *sp_gradient_reset_to_userspace(SPGradient *gr, SPItem *item) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_reset_to_userspace(%p, %p)", gr, item); +#endif + Inkscape::XML::Node *repr = gr->getRepr(); + + // calculate the bbox of the item + item->document->ensureUpToDate(); + Geom::OptRect bbox = item->visualBounds(); // we need "true" bbox without item_i2d_affine + + if (!bbox) + return gr; + + Geom::Coord const width = bbox->dimensions()[Geom::X]; + Geom::Coord const height = bbox->dimensions()[Geom::Y]; + + Geom::Point const center = bbox->midpoint(); + + if (SP_IS_RADIALGRADIENT(gr)) { + sp_repr_set_svg_double(repr, "cx", center[Geom::X]); + sp_repr_set_svg_double(repr, "cy", center[Geom::Y]); + sp_repr_set_svg_double(repr, "fx", center[Geom::X]); + sp_repr_set_svg_double(repr, "fy", center[Geom::Y]); + sp_repr_set_svg_double(repr, "r", width/2); + + // we want it to be elliptic, not circular + Geom::Affine squeeze = Geom::Translate (-center) * + Geom::Scale(1, height/width) * + Geom::Translate (center); + + gr->gradientTransform = squeeze; + { + gchar *c=sp_svg_transform_write(gr->gradientTransform); + gr->setAttribute("gradientTransform", c); + g_free(c); + } + } else if (SP_IS_LINEARGRADIENT(gr)) { + + // Assume horizontal gradient by default (as per SVG 1.1) + Geom::Point pStart = center - Geom::Point(width/2, 0); + Geom::Point pEnd = center + Geom::Point(width/2, 0); + + // Get the preferred gradient angle from prefs + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double angle = prefs->getDouble("/dialogs/gradienteditor/angle", 0.0); + + if (angle != 0.0) { + + Geom::Line grl(center, Geom::rad_from_deg(angle)); + Geom::LineSegment bbl1(bbox->corner(0), bbox->corner(1)); + Geom::LineSegment bbl2(bbox->corner(1), bbox->corner(2)); + Geom::LineSegment bbl3(bbox->corner(2), bbox->corner(3)); + Geom::LineSegment bbl4(bbox->corner(3), bbox->corner(0)); + + // Find where our gradient line intersects the bounding box. + if (intersection(bbl1, grl)) { + pStart = bbl1.pointAt((*intersection(bbl1, grl)).ta); + pEnd = bbl3.pointAt((*intersection(bbl3, grl)).ta); + if (intersection(bbl1, grl.ray(grl.angle()))) { + std::swap(pStart, pEnd); + } + } else if (intersection(bbl2, grl)) { + pStart = bbl2.pointAt((*intersection(bbl2, grl)).ta); + pEnd = bbl4.pointAt((*intersection(bbl4, grl)).ta); + if (intersection(bbl2, grl.ray(grl.angle()))) { + std::swap(pStart, pEnd); + } + } + + } + + sp_repr_set_svg_double(repr, "x1", pStart[Geom::X]); + sp_repr_set_svg_double(repr, "y1", pStart[Geom::Y]); + sp_repr_set_svg_double(repr, "x2", pEnd[Geom::X]); + sp_repr_set_svg_double(repr, "y2", pEnd[Geom::Y]); + + } else { + // Mesh + // THIS IS BEING CALLED TWICE WHENEVER A NEW GRADIENT IS CREATED, WRITING HERE CAUSES PROBLEMS + // IN SPMeshNodeArray::create() + //sp_repr_set_svg_double(repr, "x", bbox->min()[Geom::X]); + //sp_repr_set_svg_double(repr, "y", bbox->min()[Geom::Y]); + + // We don't create a shared array gradient. + SPMeshGradient* mg = SP_MESHGRADIENT( gr ); + mg->array.create( mg, item, bbox ); + } + + // set the gradientUnits + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + + return gr; +} + +/** + * Convert an item's gradient to userspace if necessary, also fork it if necessary. + * @return The new gradient. + */ +SPGradient *sp_gradient_convert_to_userspace(SPGradient *gr, SPItem *item, gchar const *property) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_convert_to_userspace(%p, %p, \"%s\")", gr, item, property); +#endif + g_return_val_if_fail(SP_IS_GRADIENT(gr), NULL); + + if ( gr && gr->isSolid() ) { + return gr; + } + + // First, fork it if it is shared + if (SP_IS_LINEARGRADIENT(gr)) { + gr = sp_gradient_fork_private_if_necessary(gr, gr->getVector(), SP_GRADIENT_TYPE_LINEAR, item); + } else if (SP_IS_RADIALGRADIENT(gr)) { + gr = sp_gradient_fork_private_if_necessary(gr, gr->getVector(), SP_GRADIENT_TYPE_RADIAL, item); + } else { + gr = sp_gradient_fork_private_if_necessary(gr, gr->getArray(), SP_GRADIENT_TYPE_MESH, item); + } + + if (gr->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + + Inkscape::XML::Node *repr = gr->getRepr(); + + // calculate the bbox of the item + item->document->ensureUpToDate(); + Geom::Affine bbox2user; + Geom::OptRect bbox = item->visualBounds(); // we need "true" bbox without item_i2d_affine + if ( bbox ) { + bbox2user = Geom::Affine(bbox->dimensions()[Geom::X], 0, + 0, bbox->dimensions()[Geom::Y], + bbox->min()[Geom::X], bbox->min()[Geom::Y]); + } else { + // would be degenerate otherwise + bbox2user = Geom::identity(); + } + + /* skew is the additional transform, defined by the proportions of the item, that we need + * to apply to the gradient in order to work around this weird bit from SVG 1.1 + * (http://www.w3.org/TR/SVG11/pservers.html#LinearGradients): + * + * When gradientUnits="objectBoundingBox" and gradientTransform is the identity + * matrix, the stripes of the linear gradient are perpendicular to the gradient + * vector in object bounding box space (i.e., the abstract coordinate system where + * (0,0) is at the top/left of the object bounding box and (1,1) is at the + * bottom/right of the object bounding box). When the object's bounding box is not + * square, the stripes that are conceptually perpendicular to the gradient vector + * within object bounding box space will render non-perpendicular relative to the + * gradient vector in user space due to application of the non-uniform scaling + * transformation from bounding box space to user space. + */ + Geom::Affine skew = bbox2user; + double exp = skew.descrim(); + skew[0] /= exp; + skew[1] /= exp; + skew[2] /= exp; + skew[3] /= exp; + skew[4] = 0; + skew[5] = 0; + + // apply skew to the gradient + gr->gradientTransform = skew; + { + gchar *c=sp_svg_transform_write(gr->gradientTransform); + gr->setAttribute("gradientTransform", c); + g_free(c); + } + + // Matrix to convert points to userspace coords; postmultiply by inverse of skew so + // as to cancel it out when it's applied to the gradient during rendering + Geom::Affine point_convert = bbox2user * skew.inverse(); + + if (SP_IS_LINEARGRADIENT(gr)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(gr); + + Geom::Point p1_b = Geom::Point(lg->x1.computed, lg->y1.computed); + Geom::Point p2_b = Geom::Point(lg->x2.computed, lg->y2.computed); + + Geom::Point p1_u = p1_b * point_convert; + Geom::Point p2_u = p2_b * point_convert; + + sp_repr_set_svg_double(repr, "x1", p1_u[Geom::X]); + sp_repr_set_svg_double(repr, "y1", p1_u[Geom::Y]); + sp_repr_set_svg_double(repr, "x2", p2_u[Geom::X]); + sp_repr_set_svg_double(repr, "y2", p2_u[Geom::Y]); + + // set the gradientUnits + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + + } else if (SP_IS_RADIALGRADIENT(gr)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(gr); + + // original points in the bbox coords + Geom::Point c_b = Geom::Point(rg->cx.computed, rg->cy.computed); + Geom::Point f_b = Geom::Point(rg->fx.computed, rg->fy.computed); + double r_b = rg->r.computed; + + // converted points in userspace coords + Geom::Point c_u = c_b * point_convert; + Geom::Point f_u = f_b * point_convert; + double r_u = r_b * point_convert.descrim(); + + sp_repr_set_svg_double(repr, "cx", c_u[Geom::X]); + sp_repr_set_svg_double(repr, "cy", c_u[Geom::Y]); + sp_repr_set_svg_double(repr, "fx", f_u[Geom::X]); + sp_repr_set_svg_double(repr, "fy", f_u[Geom::Y]); + sp_repr_set_svg_double(repr, "r", r_u); + + // set the gradientUnits + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + + } else { + std::cerr << "sp_gradient_convert_to_userspace: Conversion of mesh to userspace not implemented" << std::endl; + } + } + + // apply the gradient to the item (may be necessary if we forked it); not recursive + // generally because grouped items will be taken care of later (we're being called + // from sp_item_adjust_paint_recursive); however text and all its children should all + // refer to one gradient, hence the recursive call for text (because we can't/don't + // want to access tspans and set gradients on them separately) + if (SP_IS_TEXT(item)) { + sp_style_set_property_url(item, property, gr, true); + } else { + sp_style_set_property_url(item, property, gr, false); + } + + return gr; +} + +void sp_gradient_transform_multiply(SPGradient *gradient, Geom::Affine postmul, bool set) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_transform_multiply(%p, , %d)", gradient, set); +#endif + if (set) { + gradient->gradientTransform = postmul; + } else { + gradient->gradientTransform *= postmul; // fixme: get gradient transform by climbing to hrefs? + } + gradient->gradientTransform_set = TRUE; + + gchar *c=sp_svg_transform_write(gradient->gradientTransform); + gradient->setAttribute("gradientTransform", c); + g_free(c); +} + +SPGradient *getGradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + SPStyle *style = item->style; + SPGradient *gradient = nullptr; + + switch (fill_or_stroke) + { + case Inkscape::FOR_FILL: + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + gradient = SP_GRADIENT(server); + } + } + break; + case Inkscape::FOR_STROKE: + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( SP_IS_GRADIENT(server) ) { + gradient = SP_GRADIENT(server); + } + } + break; + } + + return gradient; +} + +SPStop *sp_last_stop(SPGradient *gradient) +{ + for (SPStop *stop = gradient->getFirstStop(); stop != nullptr; stop = stop->getNextStop()) { + if (stop->getNextStop() == nullptr) + return stop; + } + return nullptr; +} + +SPStop *sp_get_stop_i(SPGradient *gradient, guint stop_i) +{ + SPStop *stop = gradient->getFirstStop(); + if (!stop) { + return nullptr; + } + + // if this is valid but weird gradient without an offset-zero stop element, + // inkscape has created a handle for the start of gradient anyway, + // so when it asks for stop N that corresponds to stop element N-1 + if (stop->offset != 0) + { + stop_i--; + } + + for (guint i = 0; i < stop_i; i++) { + if (!stop) { + return nullptr; + } + stop = stop->getNextStop(); + } + + return stop; +} + +guint32 average_color(guint32 c1, guint32 c2, gdouble p) +{ + guint32 r = static_cast(SP_RGBA32_R_U (c1) * (1 - p) + SP_RGBA32_R_U (c2) * p); + guint32 g = static_cast(SP_RGBA32_G_U (c1) * (1 - p) + SP_RGBA32_G_U (c2) * p); + guint32 b = static_cast(SP_RGBA32_B_U (c1) * (1 - p) + SP_RGBA32_B_U (c2) * p); + guint32 a = static_cast(SP_RGBA32_A_U (c1) * (1 - p) + SP_RGBA32_A_U (c2) * p); + + return SP_RGBA32_U_COMPOSE(r, g, b, a); +} + +SPStop *sp_vector_add_stop(SPGradient *vector, SPStop* prev_stop, SPStop* next_stop, gfloat offset) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_vector_add_stop(%p, %p, %p, %f)", vector, prev_stop, next_stop, offset); +#endif + + Inkscape::XML::Node *new_stop_repr = nullptr; + new_stop_repr = prev_stop->getRepr()->duplicate(vector->getRepr()->document()); + vector->getRepr()->addChild(new_stop_repr, prev_stop->getRepr()); + + SPStop *newstop = reinterpret_cast(vector->document->getObjectByRepr(new_stop_repr)); + newstop->offset = offset; + sp_repr_set_css_double( newstop->getRepr(), "offset", (double)offset); + guint32 const c1 = prev_stop->get_rgba32(); + guint32 const c2 = next_stop->get_rgba32(); + guint32 cnew = average_color (c1, c2, (offset - prev_stop->offset) / (next_stop->offset - prev_stop->offset)); + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color (c, sizeof(c), cnew); + gdouble opacity = (gdouble) SP_RGBA32_A_F (cnew); + os << "stop-color:" << c << ";stop-opacity:" << opacity <<";"; + newstop->setAttributeOrRemoveIfEmpty("style", os.str()); + Inkscape::GC::release(new_stop_repr); + + return newstop; +} + +void sp_item_gradient_edit_stop(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !SP_IS_GRADIENT(gradient)) { + return; + } + + SPGradient *vector = gradient->getVector(); + switch (point_type) { + case POINT_LG_BEGIN: + case POINT_RG_CENTER: + case POINT_RG_FOCUS: + { + GtkWidget *dialog = sp_gradient_vector_editor_new (vector, vector->getFirstStop()); + gtk_widget_show (dialog); + } + break; + + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + { + GtkWidget *dialog = sp_gradient_vector_editor_new (vector, sp_last_stop (vector)); + gtk_widget_show (dialog); + } + break; + + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + { + GtkWidget *dialog = sp_gradient_vector_editor_new (vector, sp_get_stop_i (vector, point_i)); + gtk_widget_show (dialog); + } + break; + default: + g_warning( "Unhandled gradient handle" ); + break; + } +} + +guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !SP_IS_GRADIENT(gradient)) { + return 0; + } + + if (SP_IS_LINEARGRADIENT(gradient) || SP_IS_RADIALGRADIENT(gradient) ) { + + SPGradient *vector = gradient->getVector(); + + if (!vector) // orphan! + return 0; // what else to do? + + switch (point_type) { + case POINT_LG_BEGIN: + case POINT_RG_CENTER: + case POINT_RG_FOCUS: + { + SPStop *first = vector->getFirstStop(); + if (first) { + return first->get_rgba32(); + } + } + break; + + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + { + SPStop *last = sp_last_stop (vector); + if (last) { + return last->get_rgba32(); + } + } + break; + + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + { + SPStop *stopi = sp_get_stop_i (vector, point_i); + if (stopi) { + return stopi->get_rgba32(); + } + } + break; + + default: + g_warning( "Bad linear/radial gradient handle type" ); + break; + } + return 0; + } else if (SP_IS_MESHGRADIENT(gradient)) { + + // Mesh gradient + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + + switch (point_type) { + case POINT_MG_CORNER: { + if (point_i >= mg->array.corners.size()) { + return 0; + } + SPMeshNode const* cornerpoint = mg->array.corners[ point_i ]; + + if (cornerpoint) { + SPColor color = cornerpoint->color; + double opacity = cornerpoint->opacity; + return color.toRGBA32( opacity ); + } else { + return 0; + } + break; + } + + case POINT_MG_HANDLE: + case POINT_MG_TENSOR: + { + // Do nothing. Handles and tensors don't have color + break; + } + + default: + g_warning( "Bad mesh handle type" ); + } + return 0; + } + + return 0; +} + +void sp_item_gradient_stop_set_style(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke, SPCSSAttr *stop) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_item_gradient_stop_set_style(%p, %d, %d, %d, %p)", item, point_type, point_i, fill_or_stroke, stop); +#endif + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !SP_IS_GRADIENT(gradient)) + return; + + if (SP_IS_LINEARGRADIENT(gradient) || SP_IS_RADIALGRADIENT(gradient) ) { + + SPGradient *vector = gradient->getVector(); + + if (!vector) // orphan! + return; + + vector = sp_gradient_fork_vector_if_necessary (vector); + if ( gradient != vector && gradient->ref->getObject() != vector ) { + sp_gradient_repr_set_link(gradient->getRepr(), vector); + } + + switch (point_type) { + case POINT_LG_BEGIN: + case POINT_RG_CENTER: + case POINT_RG_FOCUS: + { + SPStop *first = vector->getFirstStop(); + if (first) { + sp_repr_css_change(first->getRepr(), stop, "style"); + } + } + break; + + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + { + SPStop *last = sp_last_stop (vector); + if (last) { + sp_repr_css_change(last->getRepr(), stop, "style"); + } + } + break; + + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + { + SPStop *stopi = sp_get_stop_i (vector, point_i); + if (stopi) { + sp_repr_css_change(stopi->getRepr(), stop, "style"); + } + } + break; + + default: + g_warning( "Bad linear/radial gradient handle type" ); + break; + } + } else { + + // Mesh gradient + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + + bool changed = false; + switch (point_type) { + case POINT_MG_CORNER: { + + // Update mesh array (which is not updated automatically when stop is changed?) + gchar const* color_str = sp_repr_css_property( stop, "stop-color", nullptr ); + if( color_str ) { + SPColor color( 0 ); + SPIPaint paint; + paint.read( color_str ); + if( paint.isColor() ) { + color = paint.value.color; + } + mg->array.corners[ point_i ]->color = color; + changed = true; + } + gchar const* opacity_str = sp_repr_css_property( stop, "stop-opacity", nullptr ); + if( opacity_str ) { + std::stringstream os( opacity_str ); + double opacity = 1.0; + os >> opacity; + mg->array.corners[ point_i ]->opacity = opacity; + changed = true; + } + // Update stop + if( changed ) { + SPStop *stopi = mg->array.corners[ point_i ]->stop; + if (stopi) { + sp_repr_css_change(stopi->getRepr(), stop, "style"); + } else { + std::cerr << "sp_item_gradient_stop_set_style: null stopi" << std::endl; + } + } + break; + } + + case POINT_MG_HANDLE: + case POINT_MG_TENSOR: + { + // Do nothing. Handles and tensors don't have colors. + break; + } + + default: + g_warning( "Bad mesh handle type" ); + } + } +} + +void sp_item_gradient_reverse_vector(SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_item_gradient_reverse_vector(%p, %d)", item, fill_or_stroke); +#endif + SPGradient *gradient = getGradient(item, fill_or_stroke); + if (!gradient || !SP_IS_GRADIENT(gradient)) + return; + + SPGradient *vector = gradient->getVector(); + if (!vector) // orphan! + return; + + vector = sp_gradient_fork_vector_if_necessary (vector); + if ( gradient != vector && gradient->ref->getObject() != vector ) { + sp_gradient_repr_set_link(gradient->getRepr(), vector); + } + + std::vector child_objects; + std::vectorchild_reprs; + std::vector offsets; + double offset; + for (auto& child: vector->children) { + child_reprs.push_back(child.getRepr()); + child_objects.push_back(&child); + offset=0; + sp_repr_get_double(child.getRepr(), "offset", &offset); + offsets.push_back(offset); + } + + std::vector child_copies; + for (auto repr:child_reprs) { + Inkscape::XML::Document *xml_doc = vector->getRepr()->document(); + child_copies.push_back(repr->duplicate(xml_doc)); + } + + + for (auto i:child_objects) { + i->deleteObject(); + } + + std::vector::reverse_iterator o_it = offsets.rbegin(); + for (auto c_it = child_copies.rbegin(); c_it != child_copies.rend(); ++c_it, ++o_it) { + vector->appendChildRepr(*c_it); + sp_repr_set_svg_double (*c_it, "offset", 1 - *o_it); + Inkscape::GC::release(*c_it); + } +} + +void sp_item_gradient_invert_vector_color(SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_item_gradient_invert_vector_color(%p, %d)", item, fill_or_stroke); +#endif + SPGradient *gradient = getGradient(item, fill_or_stroke); + if (!gradient || !SP_IS_GRADIENT(gradient)) + return; + + SPGradient *vector = gradient->getVector(); + if (!vector) // orphan! + return; + + vector = sp_gradient_fork_vector_if_necessary (vector); + if ( gradient != vector && gradient->ref->getObject() != vector ) { + sp_gradient_repr_set_link(gradient->getRepr(), vector); + } + + for (auto& child: vector->children) { + if (SP_IS_STOP(&child)) { + guint32 color = SP_STOP(&child)->get_rgba32(); + //g_message("Stop color %d", color); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(color)), + (255 - SP_RGBA32_G_U(color)), + (255 - SP_RGBA32_B_U(color)), + SP_RGBA32_A_U(color) + ) + ); + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stop-color", c); + sp_repr_css_change(child.getRepr(), css, "style"); + sp_repr_css_attr_unref (css); + } + } +} + +/** +Set the position of point point_type of the gradient applied to item (either fill_or_stroke) to +p_w (in desktop coordinates). Write_repr if you want the change to become permanent. +*/ +void sp_item_gradient_set_coords(SPItem *item, GrPointType point_type, guint point_i, Geom::Point p_w, Inkscape::PaintTarget fill_or_stroke, bool write_repr, bool scale) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_item_gradient_set_coords(%p, %d, %d, (%f, %f), ...)", item, point_type, point_i, p_w[Geom::X], p_w[Geom::Y] ); +#endif + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !SP_IS_GRADIENT(gradient)) + return; + + // Needed only if units are set to SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX + gradient = sp_gradient_convert_to_userspace(gradient, item, (fill_or_stroke == Inkscape::FOR_FILL) ? "fill" : "stroke"); + + Geom::Affine i2d (item->i2dt_affine ()); + Geom::Point p = p_w * i2d.inverse(); + p *= (gradient->gradientTransform).inverse(); + // now p is in gradient's original coordinates + + Inkscape::XML::Node *repr = gradient->getRepr(); + + if (SP_IS_LINEARGRADIENT(gradient)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(gradient); + switch (point_type) { + case POINT_LG_BEGIN: + if (scale) { + lg->x2.computed += (lg->x1.computed - p[Geom::X]); + lg->y2.computed += (lg->y1.computed - p[Geom::Y]); + } + lg->x1.computed = p[Geom::X]; + lg->y1.computed = p[Geom::Y]; + if (write_repr) { + if (scale) { + sp_repr_set_svg_double(repr, "x2", lg->x2.computed); + sp_repr_set_svg_double(repr, "y2", lg->y2.computed); + } + sp_repr_set_svg_double(repr, "x1", lg->x1.computed); + sp_repr_set_svg_double(repr, "y1", lg->y1.computed); + } else { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case POINT_LG_END: + if (scale) { + lg->x1.computed += (lg->x2.computed - p[Geom::X]); + lg->y1.computed += (lg->y2.computed - p[Geom::Y]); + } + lg->x2.computed = p[Geom::X]; + lg->y2.computed = p[Geom::Y]; + if (write_repr) { + if (scale) { + sp_repr_set_svg_double(repr, "x1", lg->x1.computed); + sp_repr_set_svg_double(repr, "y1", lg->y1.computed); + } + sp_repr_set_svg_double(repr, "x2", lg->x2.computed); + sp_repr_set_svg_double(repr, "y2", lg->y2.computed); + } else { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case POINT_LG_MID: + { + // using X-coordinates only to determine the offset, assuming p has been snapped to the vector from begin to end. + Geom::Point begin(lg->x1.computed, lg->y1.computed); + Geom::Point end(lg->x2.computed, lg->y2.computed); + double offset = Geom::LineSegment(begin, end).nearestTime(p); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (lg, false); + lg->ensureVector(); + lg->vector.stops.at(point_i).offset = offset; + SPStop* stopi = sp_get_stop_i(vector, point_i); + stopi->offset = offset; + if (write_repr) { + sp_repr_set_css_double(stopi->getRepr(), "offset", stopi->offset); + } else { + stopi->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } + break; + default: + g_warning( "Bad linear gradient handle type" ); + break; + } + } else if (SP_IS_RADIALGRADIENT(gradient)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(gradient); + Geom::Point c (rg->cx.computed, rg->cy.computed); + Geom::Point c_w = c * gradient->gradientTransform * i2d; // now in desktop coords + if ((point_type == POINT_RG_R1 || point_type == POINT_RG_R2) && Geom::L2 (p_w - c_w) < 1e-3) { + // prevent setting a radius too close to the center + return; + } + Geom::Affine new_transform; + bool transform_set = false; + + switch (point_type) { + case POINT_RG_CENTER: + rg->fx.computed = p[Geom::X] + (rg->fx.computed - rg->cx.computed); + rg->fy.computed = p[Geom::Y] + (rg->fy.computed - rg->cy.computed); + rg->cx.computed = p[Geom::X]; + rg->cy.computed = p[Geom::Y]; + if (write_repr) { + sp_repr_set_svg_double(repr, "fx", rg->fx.computed); + sp_repr_set_svg_double(repr, "fy", rg->fy.computed); + sp_repr_set_svg_double(repr, "cx", rg->cx.computed); + sp_repr_set_svg_double(repr, "cy", rg->cy.computed); + } else { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case POINT_RG_FOCUS: + rg->fx.computed = p[Geom::X]; + rg->fy.computed = p[Geom::Y]; + if (write_repr) { + sp_repr_set_svg_double(repr, "fx", rg->fx.computed); + sp_repr_set_svg_double(repr, "fy", rg->fy.computed); + } else { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case POINT_RG_R1: + { + Geom::Point r1_w = (c + Geom::Point(rg->r.computed, 0)) * gradient->gradientTransform * i2d; + double r1_angle = Geom::atan2(r1_w - c_w); + double move_angle = Geom::atan2(p_w - c_w) - r1_angle; + double move_stretch = Geom::L2(p_w - c_w) / Geom::L2(r1_w - c_w); + + Geom::Affine move = Geom::Affine (Geom::Translate (-c_w)) * + Geom::Affine (Geom::Rotate(-r1_angle)) * + Geom::Affine (Geom::Scale(move_stretch, scale? move_stretch : 1)) * + Geom::Affine (Geom::Rotate(r1_angle)) * + Geom::Affine (Geom::Rotate(move_angle)) * + Geom::Affine (Geom::Translate (c_w)); + + new_transform = gradient->gradientTransform * i2d * move * i2d.inverse(); + transform_set = true; + + break; + } + case POINT_RG_R2: + { + Geom::Point r2_w = (c + Geom::Point(0, -rg->r.computed)) * gradient->gradientTransform * i2d; + double r2_angle = Geom::atan2(r2_w - c_w); + double move_angle = Geom::atan2(p_w - c_w) - r2_angle; + double move_stretch = Geom::L2(p_w - c_w) / Geom::L2(r2_w - c_w); + + Geom::Affine move = Geom::Affine (Geom::Translate (-c_w)) * + Geom::Affine (Geom::Rotate(-r2_angle)) * + Geom::Affine (Geom::Scale(move_stretch, scale? move_stretch : 1)) * + Geom::Affine (Geom::Rotate(r2_angle)) * + Geom::Affine (Geom::Rotate(move_angle)) * + Geom::Affine (Geom::Translate (c_w)); + + new_transform = gradient->gradientTransform * i2d * move * i2d.inverse(); + transform_set = true; + + break; + } + case POINT_RG_MID1: + { + Geom::Point start = Geom::Point (rg->cx.computed, rg->cy.computed); + Geom::Point end = Geom::Point (rg->cx.computed + rg->r.computed, rg->cy.computed); + double offset = Geom::LineSegment(start, end).nearestTime(p); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (rg, false); + rg->ensureVector(); + rg->vector.stops.at(point_i).offset = offset; + SPStop* stopi = sp_get_stop_i(vector, point_i); + stopi->offset = offset; + if (write_repr) { + sp_repr_set_css_double(stopi->getRepr(), "offset", stopi->offset); + } else { + stopi->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + break; + } + case POINT_RG_MID2: + { + Geom::Point start = Geom::Point (rg->cx.computed, rg->cy.computed); + Geom::Point end = Geom::Point (rg->cx.computed, rg->cy.computed - rg->r.computed); + double offset = Geom::LineSegment(start, end).nearestTime(p); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary(rg, false); + rg->ensureVector(); + rg->vector.stops.at(point_i).offset = offset; + SPStop* stopi = sp_get_stop_i(vector, point_i); + stopi->offset = offset; + if (write_repr) { + sp_repr_set_css_double(stopi->getRepr(), "offset", stopi->offset); + } else { + stopi->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + break; + } + default: + g_warning( "Bad radial gradient handle type" ); + break; + } + + if (transform_set) { + gradient->gradientTransform = new_transform; + gradient->gradientTransform_set = TRUE; + if (write_repr) { + gchar *s=sp_svg_transform_write(gradient->gradientTransform); + gradient->setAttribute("gradientTransform", s); + g_free(s); + } else { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + } else if (SP_IS_MESHGRADIENT(gradient)) { + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + //Geom::Affine new_transform; + //bool transform_set = false; + + switch (point_type) { + case POINT_MG_CORNER: + { + mg->array.corners[ point_i ]->p = p; + // Handles are moved in gradient-drag.cpp + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + case POINT_MG_HANDLE: { + mg->array.handles[ point_i ]->p = p; + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + case POINT_MG_TENSOR: { + mg->array.tensors[ point_i ]->p = p; + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + default: + g_warning( "Bad mesh handle type" ); + } + if( write_repr ) { + //std::cout << "Write mesh repr" << std::endl; + mg->array.write( mg ); + } + } + +} + +SPGradient *sp_item_gradient_get_vector(SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (gradient) { + return gradient->getVector(); + } + return nullptr; +} + +SPGradientSpread sp_item_gradient_get_spread(SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD; + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (gradient) { + spread = gradient->fetchSpread(); + } + return spread; +} + + +/** +Returns the position of point point_type of the gradient applied to item (either fill_or_stroke), +in desktop coordinates. +*/ +Geom::Point getGradientCoords(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); +#ifdef SP_GR_VERBOSE + g_message("getGradientCoords(%p, %d, %d, %d, %p)", item, point_type, point_i, fill_or_stroke, gradient); +#endif + + Geom::Point p (0, 0); + + if (!gradient) + return p; + + if (SP_IS_LINEARGRADIENT(gradient)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(gradient); + switch (point_type) { + case POINT_LG_BEGIN: + p = Geom::Point (lg->x1.computed, lg->y1.computed); + break; + case POINT_LG_END: + p = Geom::Point (lg->x2.computed, lg->y2.computed); + break; + case POINT_LG_MID: + { + if (lg->vector.stops.size() < point_i) { + g_message("POINT_LG_MID bug trigger, see LP bug #453067"); + break; + } + gdouble offset = lg->vector.stops.at(point_i).offset; + p = (1-offset) * Geom::Point(lg->x1.computed, lg->y1.computed) + offset * Geom::Point(lg->x2.computed, lg->y2.computed); + } + break; + default: + g_warning( "Bad linear gradient handle type" ); + break; + } + } else if (SP_IS_RADIALGRADIENT(gradient)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(gradient); + switch (point_type) { + case POINT_RG_CENTER: + p = Geom::Point (rg->cx.computed, rg->cy.computed); + break; + case POINT_RG_FOCUS: + p = Geom::Point (rg->fx.computed, rg->fy.computed); + break; + case POINT_RG_R1: + p = Geom::Point (rg->cx.computed + rg->r.computed, rg->cy.computed); + break; + case POINT_RG_R2: + p = Geom::Point (rg->cx.computed, rg->cy.computed - rg->r.computed); + break; + case POINT_RG_MID1: + { + if (rg->vector.stops.size() < point_i) { + g_message("POINT_RG_MID1 bug trigger, see LP bug #453067"); + break; + } + gdouble offset = rg->vector.stops.at(point_i).offset; + p = (1-offset) * Geom::Point (rg->cx.computed, rg->cy.computed) + offset * Geom::Point(rg->cx.computed + rg->r.computed, rg->cy.computed); + } + break; + case POINT_RG_MID2: + { + if (rg->vector.stops.size() < point_i) { + g_message("POINT_RG_MID2 bug trigger, see LP bug #453067"); + break; + } + gdouble offset = rg->vector.stops.at(point_i).offset; + p = (1-offset) * Geom::Point (rg->cx.computed, rg->cy.computed) + offset * Geom::Point(rg->cx.computed, rg->cy.computed - rg->r.computed); + } + break; + default: + g_warning( "Bad radial gradient handle type" ); + break; + } + } else if (SP_IS_MESHGRADIENT(gradient)) { + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + switch (point_type) { + + case POINT_MG_CORNER: + p = mg->array.corners[ point_i ]->p; + break; + + case POINT_MG_HANDLE: { + p = mg->array.handles[ point_i ]->p; + break; + } + + case POINT_MG_TENSOR: { + p = mg->array.tensors[ point_i ]->p; + break; + } + + default: + g_warning( "Bad mesh handle type" ); + } + } + + + if (SP_GRADIENT(gradient)->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + item->document->ensureUpToDate(); + Geom::OptRect bbox = item->visualBounds(); // we need "true" bbox without item_i2d_affine + if (bbox) { + p *= Geom::Affine(bbox->dimensions()[Geom::X], 0, + 0, bbox->dimensions()[Geom::Y], + bbox->min()[Geom::X], bbox->min()[Geom::Y]); + } + } + p *= Geom::Affine(gradient->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + return p; +} + +/** + * Sets item fill or stroke to the gradient of the specified type with given vector, creating + * new private gradient, if needed. + * gr has to be a normalized vector. + */ + +SPGradient *sp_item_set_gradient(SPItem *item, SPGradient *gr, SPGradientType type, Inkscape::PaintTarget fill_or_stroke) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_item_set_gradient(%p, %p, %d, %d)", item, gr, type, fill_or_stroke); +#endif + g_return_val_if_fail(item != nullptr, NULL); + g_return_val_if_fail(SP_IS_ITEM(item), NULL); + g_return_val_if_fail(gr != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(gr), NULL); + g_return_val_if_fail(gr->state == SP_GRADIENT_STATE_VECTOR, NULL); + + SPStyle *style = item->style; + g_assert(style != nullptr); + + SPPaintServer *ps = nullptr; + if ((fill_or_stroke == Inkscape::FOR_FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver()) { + ps = (fill_or_stroke == Inkscape::FOR_FILL) ? SP_STYLE_FILL_SERVER(style) : SP_STYLE_STROKE_SERVER(style); + } + + if (ps + && ( (type == SP_GRADIENT_TYPE_LINEAR && SP_IS_LINEARGRADIENT(ps)) || + (type == SP_GRADIENT_TYPE_RADIAL && SP_IS_RADIALGRADIENT(ps)) ) ) + { + + /* Current fill style is the gradient of the required type */ + SPGradient *current = SP_GRADIENT(ps); + + //g_message("hrefcount %d count %d\n", current->hrefcount, count_gradient_hrefs(item, current)); + + if (!current->isSwatch() + && (current->hrefcount == 1 || + current->hrefcount == count_gradient_hrefs(item, current))) { + + // current is private and it's either used once, or all its uses are by children of item; + // so just change its href to vector + + if ( current != gr && current->getVector() != gr ) { + // href is not the vector + sp_gradient_repr_set_link(current->getRepr(), gr); + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + return current; + + } else { + + // the gradient is not private, or it is shared with someone else; + // normalize it (this includes creating new private if necessary) + SPGradient *normalized = sp_gradient_fork_private_if_necessary(current, gr, type, item); + + g_return_val_if_fail(normalized != nullptr, NULL); + + if (normalized != current) { + + /* We have to change object style here; recursive because this is used from + * fill&stroke and must work for groups etc. */ + sp_style_set_property_url(item, (fill_or_stroke == Inkscape::FOR_FILL) ? "fill" : "stroke", normalized, true); + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + return normalized; + } + + } else { + /* Current fill style is not a gradient or wrong type, so construct everything */ + /* This is where mesh gradients are constructed. */ + g_assert(SP_IS_GRADIENT(gr)); // TEMP + SPGradient *constructed = sp_gradient_get_private_normalized(item->document, gr, type); + constructed = sp_gradient_reset_to_userspace(constructed, item); + sp_style_set_property_url(item, ( (fill_or_stroke == Inkscape::FOR_FILL) ? "fill" : "stroke" ), constructed, true); + item->requestDisplayUpdate(( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )); + return constructed; + } +} + +static void sp_gradient_repr_set_link(Inkscape::XML::Node *repr, SPGradient *link) +{ +#ifdef SP_GR_VERBOSE + g_message("sp_gradient_repr_set_link(%p, %p)", repr, link); +#endif + g_return_if_fail(repr != nullptr); + if (link) { + g_return_if_fail(SP_IS_GRADIENT(link)); + } + + if (link) { + Glib::ustring ref("#"); + ref += link->getId(); + repr->setAttribute("xlink:href", ref); + } else { + repr->removeAttribute("xlink:href"); + } +} + + +static void addStop( Inkscape::XML::Node *parent, Glib::ustring const &color, gint opacity, gchar const *offset ) +{ +#ifdef SP_GR_VERBOSE + g_message("addStop(%p, %s, %d, %s)", parent, color.c_str(), opacity, offset); +#endif + Inkscape::XML::Node *stop = parent->document()->createElement("svg:stop"); + { + gchar *tmp = g_strdup_printf( "stop-color:%s;stop-opacity:%d;", color.c_str(), opacity ); + stop->setAttribute( "style", tmp ); + g_free(tmp); + } + + stop->setAttribute( "offset", offset ); + + parent->appendChild(stop); + Inkscape::GC::release(stop); +} + +/* + * Get default normalized gradient vector of document, create if there is none + */ +SPGradient *sp_document_default_gradient_vector( SPDocument *document, SPColor const &color, bool singleStop ) +{ + SPDefs *defs = document->getDefs(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:linearGradient"); + + if ( !singleStop ) { + repr->setAttribute("inkscape:collect", "always"); + // set here, but removed when it's edited in the gradient editor + // to further reduce clutter, we could + // (1) here, search gradients by color and return what is found without duplication + // (2) in fill & stroke, show only one copy of each gradient in list + } + + Glib::ustring colorStr = color.toString(); + addStop( repr, colorStr, 1, "0" ); + if ( !singleStop ) { + addStop( repr, colorStr, 0, "1" ); + } + + defs->getRepr()->addChild(repr, nullptr); + Inkscape::GC::release(repr); + + /* fixme: This does not look like nice */ + SPGradient *gr = static_cast(document->getObjectByRepr(repr)); + g_assert(gr != nullptr); + g_assert(SP_IS_GRADIENT(gr)); + /* fixme: Maybe add extra sanity check here */ + gr->state = SP_GRADIENT_STATE_VECTOR; + + return gr; +} + +SPGradient *sp_gradient_vector_for_object( SPDocument *const doc, SPDesktop *const desktop, + SPObject *const o, Inkscape::PaintTarget const fill_or_stroke, bool singleStop ) +{ + SPColor color; + if ( (o == nullptr) || (o->style == nullptr) ) { + color = sp_desktop_get_color(desktop, (fill_or_stroke == Inkscape::FOR_FILL)); + } else { + // take the color of the object + SPStyle const &style = *(o->style); + SPIPaint const &paint = *style.getFillOrStroke(fill_or_stroke == Inkscape::FOR_FILL); + if (paint.isPaintserver()) { + SPObject *server = (fill_or_stroke == Inkscape::FOR_FILL) ? o->style->getFillPaintServer() : o->style->getStrokePaintServer(); + if ( SP_IS_LINEARGRADIENT(server) || SP_IS_RADIALGRADIENT(server) ) { + return SP_GRADIENT(server)->getVector(true); + } else { + color = sp_desktop_get_color(desktop, (fill_or_stroke == Inkscape::FOR_FILL)); + } + } else if (paint.isColor()) { + color = paint.value.color; + } else { + // if o doesn't use flat color, then take current color of the desktop. + color = sp_desktop_get_color(desktop, (fill_or_stroke == Inkscape::FOR_FILL)); + } + } + + return sp_document_default_gradient_vector( doc, color, singleStop ); +} + +void sp_gradient_invert_selected_gradients(SPDesktop *desktop, Inkscape::PaintTarget fill_or_stroke) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + auto list= selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + sp_item_gradient_invert_vector_color(*i, fill_or_stroke); + } + + // we did an undoable action + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Invert gradient colors")); +} + +void sp_gradient_reverse_selected_gradients(SPDesktop *desktop) +{ + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::UI::Tools::ToolBase *ev = desktop->getEventContext(); + + if (!ev) { + return; + } + + GrDrag *drag = ev->get_drag(); + + // First try selected dragger + if (drag && !drag->selected.empty()) { + drag->selected_reverse_vector(); + } else { // If no drag or no dragger selected, act on selection (both fill and stroke gradients) + auto list= selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + sp_item_gradient_reverse_vector(*i, Inkscape::FOR_FILL); + sp_item_gradient_reverse_vector(*i, Inkscape::FOR_STROKE); + } + } + + // we did an undoable action + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Reverse gradient")); +} + +void sp_gradient_unset_swatch(SPDesktop *desktop, std::string id) +{ + SPDocument *doc = desktop ? desktop->doc() : nullptr; + + if (doc) { + const std::vector gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( id == grad->getId() ) { + grad->setSwatch(false); + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, + _("Delete swatch")); + break; + } + } + } +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gradient-chemistry.h b/src/gradient-chemistry.h new file mode 100644 index 0000000..c80b637 --- /dev/null +++ b/src/gradient-chemistry.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_CHEMISTRY_H +#define SEEN_SP_GRADIENT_CHEMISTRY_H + +/* + * Various utility methods for gradients + * + * Author: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Jon A. Cruz + * + * Copyright (C) 2010 Authors + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-gradient.h" + +class SPCSSAttr; +class SPItem; +class SPGradient; +class SPDesktop; + + +/** + * Either normalizes given gradient to vector, or returns fresh normalized + * vector - in latter case, original gradient is flattened and stops cleared. + * No transrefing - i.e. caller shouldn't hold reference to original and + * does not get one to new automatically (doc holds ref of every object anyways) + */ +SPGradient *sp_gradient_ensure_vector_normalized(SPGradient *gradient); + + +/** + * Sets item fill or stroke to the gradient of the specified type with given vector, creating + * new private gradient, if needed. + * gr has to be a normalized vector. + */ +SPGradient *sp_item_set_gradient(SPItem *item, SPGradient *gr, SPGradientType type, Inkscape::PaintTarget fill_or_stroke); + +/* + * Get default normalized gradient vector of document, create if there is none + */ +SPGradient *sp_document_default_gradient_vector( SPDocument *document, SPColor const &color, bool singleStop ); + +/** + * Return the preferred vector for \a o, made from (in order of preference) its current vector, + * current fill or stroke color, or from desktop style if \a o is NULL or doesn't have style. + */ +SPGradient *sp_gradient_vector_for_object( SPDocument *doc, SPDesktop *desktop, SPObject *o, Inkscape::PaintTarget fill_or_stroke, bool singleStop = false ); + +void sp_object_ensure_fill_gradient_normalized (SPObject *object); +void sp_object_ensure_stroke_gradient_normalized (SPObject *object); + +SPGradient *sp_gradient_convert_to_userspace (SPGradient *gr, SPItem *item, const char *property); +SPGradient *sp_gradient_reset_to_userspace (SPGradient *gr, SPItem *item); + +SPGradient *sp_gradient_fork_vector_if_necessary (SPGradient *gr); +SPGradient *sp_gradient_get_forked_vector_if_necessary(SPGradient *gradient, bool force_vector); + + +SPStop* sp_last_stop(SPGradient *gradient); +SPStop* sp_get_stop_i(SPGradient *gradient, unsigned int i); +unsigned int sp_number_of_stops(SPGradient const *gradient); +unsigned int sp_number_of_stops_before_stop(SPGradient const *gradient, SPStop *target); + +guint32 average_color(guint32 c1, guint32 c2, double p = 0.5); + +SPStop *sp_vector_add_stop(SPGradient *vector, SPStop* prev_stop, SPStop* next_stop, gfloat offset); + +void sp_gradient_transform_multiply(SPGradient *gradient, Geom::Affine postmul, bool set); + +void sp_gradient_reverse_selected_gradients(SPDesktop *desktop); + +void sp_gradient_invert_selected_gradients(SPDesktop *desktop, Inkscape::PaintTarget fill_or_stroke); + +void sp_gradient_unset_swatch(SPDesktop *desktop, std::string id); + +/** + * Fetches either the fill or the stroke gradient from the given item. + * + * @param fill_or_stroke the gradient type (fill/stroke) to get. + * @return the specified gradient if set, null otherwise. + */ +SPGradient *getGradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke); + + +void sp_item_gradient_set_coords(SPItem *item, GrPointType point_type, unsigned int point_i, Geom::Point p_desk, Inkscape::PaintTarget fill_or_stroke, bool write_repr, bool scale); + +/** + * Returns the position of point point_type of the gradient applied to item (either fill_or_stroke), + * in desktop coordinates. +*/ +Geom::Point getGradientCoords(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke); + +SPGradient *sp_item_gradient_get_vector(SPItem *item, Inkscape::PaintTarget fill_or_stroke); +SPGradientSpread sp_item_gradient_get_spread(SPItem *item, Inkscape::PaintTarget fill_or_stroke); + +void sp_item_gradient_stop_set_style(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke, SPCSSAttr *stop); +guint32 sp_item_gradient_stop_query_style(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke); +void sp_item_gradient_edit_stop(SPItem *item, GrPointType point_type, unsigned int point_i, Inkscape::PaintTarget fill_or_stroke); +void sp_item_gradient_reverse_vector(SPItem *item, Inkscape::PaintTarget fill_or_stroke); +void sp_item_gradient_invert_vector_color(SPItem *item, Inkscape::PaintTarget fill_or_stroke); + +#endif // SEEN_SP_GRADIENT_CHEMISTRY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/gradient-drag.cpp b/src/gradient-drag.cpp new file mode 100644 index 0000000..9e607f5 --- /dev/null +++ b/src/gradient-drag.cpp @@ -0,0 +1,3077 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * On-canvas gradient dragging + * + * Authors: + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005,2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "inkscape.h" +#include "knot.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" +#include "verbs.h" + +#include "display/sp-canvas-util.h" +#include "display/sp-canvas.h" +#include "display/sp-ctrlcurve.h" +#include "display/sp-ctrlline.h" + +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-namedview.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg.h" + +#include "ui/control-manager.h" +#include "ui/tools/tool-base.h" + +#include "xml/sp-css-attr.h" + +using Inkscape::ControlManager; +using Inkscape::CtrlLineType; +using Inkscape::DocumentUndo; +using Inkscape::allPaintTargets; +using Inkscape::CTLINE_PRIMARY; +using Inkscape::CTLINE_SECONDARY; + +guint32 const GR_KNOT_COLOR_NORMAL = 0xffffff00; +guint32 const GR_KNOT_COLOR_MOUSEOVER = 0xff000000; +guint32 const GR_KNOT_COLOR_SELECTED = 0x0000ff00; +guint32 const GR_KNOT_COLOR_HIGHLIGHT = 0xffffff00; +guint32 const GR_KNOT_COLOR_MESHCORNER = 0xbfbfbf00; + +// absolute distance between gradient points for them to become a single dragger when the drag is created: +#define MERGE_DIST 0.1 + +// knot shapes corresponding to GrPointType enum (in sp-gradient.h) +SPKnotShapeType gr_knot_shapes [] = { + SP_KNOT_SHAPE_SQUARE, // POINT_LG_BEGIN + SP_KNOT_SHAPE_CIRCLE, // POINT_LG_END + SP_KNOT_SHAPE_DIAMOND, // POINT_LG_MID + SP_KNOT_SHAPE_SQUARE, // POINT_RG_CENTER + SP_KNOT_SHAPE_CIRCLE, // POINT_RG_R1 + SP_KNOT_SHAPE_CIRCLE, // POINT_RG_R2 + SP_KNOT_SHAPE_CROSS, // POINT_RG_FOCUS + SP_KNOT_SHAPE_DIAMOND, // POINT_RG_MID1 + SP_KNOT_SHAPE_DIAMOND, // POINT_RG_MID2 + SP_KNOT_SHAPE_DIAMOND, // POINT_MG_CORNER + SP_KNOT_SHAPE_CIRCLE, // POINT_MG_HANDLE + SP_KNOT_SHAPE_SQUARE // POINT_MG_TENSOR +}; + +const gchar *gr_knot_descr [] = { + N_("Linear gradient start"), //POINT_LG_BEGIN + N_("Linear gradient end"), + N_("Linear gradient mid stop"), + N_("Radial gradient center"), + N_("Radial gradient radius"), + N_("Radial gradient radius"), + N_("Radial gradient focus"), // POINT_RG_FOCUS + N_("Radial gradient mid stop"), + N_("Radial gradient mid stop"), + N_("Mesh gradient corner"), + N_("Mesh gradient handle"), + N_("Mesh gradient tensor") +}; + +static void +gr_drag_sel_changed(Inkscape::Selection */*selection*/, gpointer data) +{ + GrDrag *drag = (GrDrag *) data; + drag->updateDraggers (); + drag->updateLines (); + drag->updateLevels (); +} + +static void gr_drag_sel_modified(Inkscape::Selection */*selection*/, guint /*flags*/, gpointer data) +{ + GrDrag *drag = (GrDrag *) data; + if (drag->local_change) { + drag->refreshDraggers (); // Needed to move mesh handles and toggle visibility + drag->local_change = false; + } else { + drag->updateDraggers (); + } + drag->updateLines (); + drag->updateLevels (); +} + +/** + * When a _query_style_signal is received, check that \a property requests fill/stroke/opacity (otherwise + * skip), and fill the \a style with the averaged color of all draggables of the selected dragger, if + * any. + */ +static int gr_drag_style_query(SPStyle *style, int property, gpointer data) +{ + GrDrag *drag = (GrDrag *) data; + + if (property != QUERY_STYLE_PROPERTY_FILL && property != QUERY_STYLE_PROPERTY_STROKE && property != QUERY_STYLE_PROPERTY_MASTEROPACITY) { + return QUERY_STYLE_NOTHING; + } + + if (drag->selected.empty()) { + return QUERY_STYLE_NOTHING; + } else { + int ret = QUERY_STYLE_NOTHING; + + float cf[4]; + cf[0] = cf[1] = cf[2] = cf[3] = 0; + + int count = 0; + for(auto d : drag->selected) { //for all selected draggers + for(auto draggable : d->draggables) { //for all draggables of dragger + if (ret == QUERY_STYLE_NOTHING) { + ret = QUERY_STYLE_SINGLE; + } else if (ret == QUERY_STYLE_SINGLE) { + ret = QUERY_STYLE_MULTIPLE_AVERAGED; + } + + guint32 c = sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); + cf[0] += SP_RGBA32_R_F (c); + cf[1] += SP_RGBA32_G_F (c); + cf[2] += SP_RGBA32_B_F (c); + cf[3] += SP_RGBA32_A_F (c); + + count ++; + } + } + + if (count) { + cf[0] /= count; + cf[1] /= count; + cf[2] /= count; + cf[3] /= count; + + // set both fill and stroke with our stop-color and stop-opacity + style->fill.clear(); + style->fill.setColor( cf[0], cf[1], cf[2] ); + style->fill.set = TRUE; + style->stroke.clear(); + style->stroke.setColor( cf[0], cf[1], cf[2] ); + style->stroke.set = TRUE; + + style->fill_opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); + style->fill_opacity.set = TRUE; + style->stroke_opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); + style->stroke_opacity.set = TRUE; + + style->opacity.value = SP_SCALE24_FROM_FLOAT (cf[3]); + style->opacity.set = TRUE; + } + + return ret; + } +} + +Glib::ustring GrDrag::makeStopSafeColor( gchar const *str, bool &isNull ) +{ + Glib::ustring colorStr; + if ( str ) { + isNull = false; + colorStr = str; + Glib::ustring::size_type pos = colorStr.find("url(#"); + if ( pos != Glib::ustring::npos ) { + Glib::ustring targetName = colorStr.substr(pos + 5, colorStr.length() - 6); + std::vector gradients = desktop->doc()->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( targetName == grad->getId() ) { + SPGradient *vect = grad->getVector(); + SPStop *firstStop = (vect) ? vect->getFirstStop() : grad->getFirstStop(); + if (firstStop) { + Glib::ustring stopColorStr = firstStop->getColor().toString(); + if ( !stopColorStr.empty() ) { + colorStr = stopColorStr; + } + } + break; + } + } + } + } else { + isNull = true; + } + + return colorStr; +} + +bool GrDrag::styleSet( const SPCSSAttr *css ) +{ + if (selected.empty()) { + return false; + } + + SPCSSAttr *stop = sp_repr_css_attr_new(); + + // See if the css contains interesting properties, and if so, translate them into the format + // acceptable for gradient stops + + // any of color properties, in order of increasing priority: + if (css->attribute("flood-color")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("flood-color")); + } + + if (css->attribute("lighting-color")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("lighting-color")); + } + + if (css->attribute("color")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("color")); + } + + if (css->attribute("stroke") && strcmp(css->attribute("stroke"), "none")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("stroke")); + } + + if (css->attribute("fill") && strcmp(css->attribute("fill"), "none")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("fill")); + } + + if (css->attribute("stop-color")) { + sp_repr_css_set_property (stop, "stop-color", css->attribute("stop-color")); + } + + // Make sure the style is allowed for gradient stops. + if ( !sp_repr_css_property_is_unset( stop, "stop-color") ) { + bool stopIsNull = false; + Glib::ustring tmp = makeStopSafeColor( sp_repr_css_property( stop, "stop-color", "" ), stopIsNull ); + if ( !stopIsNull && !tmp.empty() ) { + sp_repr_css_set_property( stop, "stop-color", tmp.c_str() ); + } + } + + + if (css->attribute("stop-opacity")) { // direct setting of stop-opacity has priority + sp_repr_css_set_property(stop, "stop-opacity", css->attribute("stop-opacity")); + } else { // multiply all opacity properties: + gdouble accumulated = 1.0; + accumulated *= sp_svg_read_percentage(css->attribute("flood-opacity"), 1.0); + accumulated *= sp_svg_read_percentage(css->attribute("opacity"), 1.0); + accumulated *= sp_svg_read_percentage(css->attribute("stroke-opacity"), 1.0); + accumulated *= sp_svg_read_percentage(css->attribute("fill-opacity"), 1.0); + + Inkscape::CSSOStringStream os; + os << accumulated; + sp_repr_css_set_property(stop, "stop-opacity", os.str().c_str()); + + if ((css->attribute("fill") && !css->attribute("stroke") && !strcmp(css->attribute("fill"), "none")) || + (css->attribute("stroke") && !css->attribute("fill") && !strcmp(css->attribute("stroke"), "none"))) { + sp_repr_css_set_property(stop, "stop-opacity", "0"); // if a single fill/stroke property is set to none, don't change color, set opacity to 0 + } + } + + if (!stop->attributeList()) { // nothing for us here, pass it on + sp_repr_css_attr_unref(stop); + return false; + } + + for(auto d : selected) { //for all selected draggers + for(auto draggable : d->draggables) { //for all draggables of dragger + local_change = true; + sp_item_gradient_stop_set_style(draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke, stop); + } + } + + //sp_repr_css_print(stop); + sp_repr_css_attr_unref(stop); + return true; +} + +guint32 GrDrag::getColor() +{ + if (selected.empty()) return 0; + + float cf[4]; + cf[0] = cf[1] = cf[2] = cf[3] = 0; + + int count = 0; + + for(auto d : selected) { //for all selected draggers + for(auto draggable : d->draggables) { //for all draggables of dragger + guint32 c = sp_item_gradient_stop_query_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); + cf[0] += SP_RGBA32_R_F (c); + cf[1] += SP_RGBA32_G_F (c); + cf[2] += SP_RGBA32_B_F (c); + cf[3] += SP_RGBA32_A_F (c); + + count ++; + } + } + + if (count) { + cf[0] /= count; + cf[1] /= count; + cf[2] /= count; + cf[3] /= count; + } + + return SP_RGBA32_F_COMPOSE(cf[0], cf[1], cf[2], cf[3]); +} + +// TODO refactor early returns +SPStop *GrDrag::addStopNearPoint(SPItem *item, Geom::Point mouse_p, double tolerance) +{ + gfloat new_stop_offset = 0; // type of SPStop.offset = gfloat + SPGradient *gradient = nullptr; + //bool r1_knot = false; + + // For Mesh + int divide_row = -1; + int divide_column = -1; + double divide_coord = 0.5; + + bool addknot = false; + + for (std::vector::const_iterator it = allPaintTargets().begin(); (it != allPaintTargets().end()) && !addknot; ++it) + { + Inkscape::PaintTarget fill_or_stroke = *it; + gradient = getGradient(item, fill_or_stroke); + if (SP_IS_LINEARGRADIENT(gradient)) { + Geom::Point begin = getGradientCoords(item, POINT_LG_BEGIN, 0, fill_or_stroke); + Geom::Point end = getGradientCoords(item, POINT_LG_END, 0, fill_or_stroke); + Geom::LineSegment ls(begin, end); + double offset = ls.nearestTime(mouse_p); + Geom::Point nearest = ls.pointAt(offset); + double dist_screen = Geom::distance(mouse_p, nearest); + if ( dist_screen < tolerance ) { + // calculate the new stop offset + new_stop_offset = distance(begin, nearest) / distance(begin, end); + // add the knot + addknot = true; + } + } else if (SP_IS_RADIALGRADIENT(gradient)) { + Geom::Point begin = getGradientCoords(item, POINT_RG_CENTER, 0, fill_or_stroke); + Geom::Point end = getGradientCoords(item, POINT_RG_R1, 0, fill_or_stroke); + Geom::LineSegment ls(begin, end); + double offset = ls.nearestTime(mouse_p); + Geom::Point nearest = ls.pointAt(offset); + double dist_screen = Geom::distance(mouse_p, nearest); + if ( dist_screen < tolerance ) { + // calculate the new stop offset + new_stop_offset = distance(begin, nearest) / distance(begin, end); + // add the knot + addknot = true; + //r1_knot = true; + } else { + end = getGradientCoords(item, POINT_RG_R2, 0, fill_or_stroke); + ls = Geom::LineSegment(begin, end); + offset = ls.nearestTime(mouse_p); + nearest = ls.pointAt(offset); + dist_screen = Geom::distance(mouse_p, nearest); + if ( dist_screen < tolerance ) { + // calculate the new stop offset + new_stop_offset = distance(begin, nearest) / distance(begin, end); + // add the knot + addknot = true; + //r1_knot = false; + } + } + } else if (SP_IS_MESHGRADIENT(gradient)) { + + // add_stop_near_point() + // Find out which curve pointer is over and use that curve to determine + // which row or column will be divided. + // This is silly as we already should know which line we are over... + // but that information is not saved (sp_gradient_context_is_over_line). + + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + Geom::Affine transform = Geom::Affine(mg->gradientTransform)*(Geom::Affine)item->i2dt_affine(); + + guint rows = mg->array.patch_rows(); + guint columns = mg->array.patch_columns(); + + double closest = 1e10; + for( guint i = 0; i < rows; ++i ) { + for( guint j = 0; j < columns; ++j ) { + + SPMeshPatchI patch( &(mg->array.nodes), i, j ); + Geom::Point p[4]; + + // Top line + { + p[0] = patch.getPoint( 0, 0 ) * transform; + p[1] = patch.getPoint( 0, 1 ) * transform; + p[2] = patch.getPoint( 0, 2 ) * transform; + p[3] = patch.getPoint( 0, 3 ) * transform; + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + Geom::Coord coord = b.nearestTime( mouse_p ); + Geom::Point nearest = b( coord ); + double dist_screen = Geom::L2 ( mouse_p - nearest ); + if ( dist_screen < closest ) { + closest = dist_screen; + divide_row = -1; + divide_column = j; + divide_coord = coord; + } + } + + // Right line (only for last column) + if( j == columns - 1 ) { + p[0] = patch.getPoint( 1, 0 ) * transform; + p[1] = patch.getPoint( 1, 1 ) * transform; + p[2] = patch.getPoint( 1, 2 ) * transform; + p[3] = patch.getPoint( 1, 3 ) * transform; + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + Geom::Coord coord = b.nearestTime( mouse_p ); + Geom::Point nearest = b( coord ); + double dist_screen = Geom::L2 ( mouse_p - nearest ); + if ( dist_screen < closest ) { + closest = dist_screen; + divide_row = i; + divide_column = -1; + divide_coord = coord; + } + } + + // Bottom line (only for last row) + if( i == rows - 1 ) { + p[0] = patch.getPoint( 2, 0 ) * transform; + p[1] = patch.getPoint( 2, 1 ) * transform; + p[2] = patch.getPoint( 2, 2 ) * transform; + p[3] = patch.getPoint( 2, 3 ) * transform; + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + Geom::Coord coord = b.nearestTime( mouse_p ); + Geom::Point nearest = b( coord ); + double dist_screen = Geom::L2 ( mouse_p - nearest ); + if ( dist_screen < closest ) { + closest = dist_screen; + divide_row = -1; + divide_column = j; + divide_coord = 1.0 - coord; + } + } + + // Left line + { + p[0] = patch.getPoint( 3, 0 ) * transform; + p[1] = patch.getPoint( 3, 1 ) * transform; + p[2] = patch.getPoint( 3, 2 ) * transform; + p[3] = patch.getPoint( 3, 3 ) * transform; + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + Geom::Coord coord = b.nearestTime( mouse_p ); + Geom::Point nearest = b( coord ); + double dist_screen = Geom::L2 ( mouse_p - nearest ); + if ( dist_screen < closest ) { + closest = dist_screen; + divide_row = i; + divide_column = -1; + divide_coord = 1.0 - coord; + } + } + + } // End loop over columns + } // End loop rows + + if( closest < tolerance ) { + addknot = true; + } + + } // End if mesh + + } + + if (addknot) { + + if( SP_IS_LINEARGRADIENT(gradient) || SP_IS_RADIALGRADIENT( gradient ) ) { + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + SPStop* prev_stop = vector->getFirstStop(); + SPStop* next_stop = prev_stop->getNextStop(); + guint i = 1; + while ( (next_stop) && (next_stop->offset < new_stop_offset) ) { + prev_stop = next_stop; + next_stop = next_stop->getNextStop(); + i++; + } + if (!next_stop) { + // logical error: the endstop should have offset 1 and should always be more than this offset here + return nullptr; + } + + + SPStop *newstop = sp_vector_add_stop (vector, prev_stop, next_stop, new_stop_offset); + gradient->ensureVector(); + updateDraggers(); + + // so that it does not automatically update draggers in idle loop, as this would deselect + local_change = true; + + // select the newly created stop + selectByStop(newstop); + + return newstop; + + } else { + + SPMeshGradient *mg = SP_MESHGRADIENT(gradient); + + if( divide_row > -1 ) { + mg->array.split_row( divide_row, divide_coord ); + } else { + mg->array.split_column( divide_column, divide_coord ); + } + + // Update repr + mg->array.write( mg ); + mg->array.built = false; + mg->ensureArray(); + // How do we do this? + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH, + _("Added patch row or column")); + + } // Mesh + } + + return nullptr; +} + + +bool GrDrag::dropColor(SPItem */*item*/, gchar const *c, Geom::Point p) +{ + // Note: not sure if a null pointer can come in for the style, but handle that just in case + bool stopIsNull = false; + Glib::ustring toUse = makeStopSafeColor( c, stopIsNull ); + + // first, see if we can drop onto one of the existing draggers + for(auto d : draggers) { //for all draggers + if (Geom::L2(p - d->point)*desktop->current_zoom() < 5) { + SPCSSAttr *stop = sp_repr_css_attr_new (); + sp_repr_css_set_property( stop, "stop-color", stopIsNull ? nullptr : toUse.c_str() ); + sp_repr_css_set_property( stop, "stop-opacity", "1" ); + for(auto draggable : d->draggables) { //for all draggables of dragger + local_change = true; + sp_item_gradient_stop_set_style (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke, stop); + } + sp_repr_css_attr_unref(stop); + return true; + } + } + + // now see if we're over line and create a new stop + bool over_line = false; + if (!lines.empty()) { + for (std::vector::const_iterator l = lines.begin(); l != lines.end() && (!over_line); ++l) { + SPCtrlLine *line = *l; + Geom::LineSegment ls(line->s, line->e); + Geom::Point nearest = ls.pointAt(ls.nearestTime(p)); + double dist_screen = Geom::L2(p - nearest) * desktop->current_zoom(); + if (line->item && dist_screen < 5) { + SPStop *stop = addStopNearPoint(line->item, p, 5/desktop->current_zoom()); + if (stop) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "stop-color", stopIsNull ? nullptr : toUse.c_str() ); + sp_repr_css_set_property( css, "stop-opacity", "1" ); + sp_repr_css_change(stop->getRepr(), css, "style"); + return true; + } + } + } + } + + return false; +} + + +GrDrag::GrDrag(SPDesktop *desktop) : + keep_selection(false), + local_change(false), + desktop(desktop), + hor_levels(), + vert_levels(), + draggers(0), + lines(0), + selection(desktop->getSelection()), + sel_changed_connection(), + sel_modified_connection(), + style_set_connection(), + style_query_connection() +{ + sel_changed_connection = selection->connectChangedFirst( + sigc::bind( + sigc::ptr_fun(&gr_drag_sel_changed), + (gpointer)this ) + + ); + sel_modified_connection = selection->connectModifiedFirst( + sigc::bind( + sigc::ptr_fun(&gr_drag_sel_modified), + (gpointer)this ) + ); + + style_set_connection = desktop->connectSetStyle( sigc::mem_fun(*this, &GrDrag::styleSet) ); + + style_query_connection = desktop->connectQueryStyle( + sigc::bind( + sigc::ptr_fun(&gr_drag_style_query), + (gpointer)this ) + ); + + updateDraggers(); + updateLines(); + updateLevels(); + + if (desktop->gr_item) { + GrDragger *dragger = getDraggerFor(desktop->gr_item, desktop->gr_point_type, desktop->gr_point_i, desktop->gr_fill_or_stroke); + if (dragger) { + setSelected(dragger); + } + } +} + +GrDrag::~GrDrag() +{ + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + this->style_set_connection.disconnect(); + this->style_query_connection.disconnect(); + + if (! this->selected.empty()) { + GrDraggable *draggable = (*(this->selected.begin()))->draggables[0]; + desktop->gr_item = draggable->item; + desktop->gr_point_type = draggable->point_type; + desktop->gr_point_i = draggable->point_i; + desktop->gr_fill_or_stroke = draggable->fill_or_stroke; + } else { + desktop->gr_item = nullptr; + desktop->gr_point_type = POINT_LG_BEGIN; + desktop->gr_point_i = 0; + desktop->gr_fill_or_stroke = Inkscape::FOR_FILL; + } + + deselect_all(); + for (auto dragger : this->draggers) { + delete dragger; + } + this->draggers.clear(); + this->selected.clear(); + + for (std::vector::const_iterator it = this->lines.begin(); it != this->lines.end(); ++it) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(*it)); + } + this->lines.clear(); +} + +GrDraggable::GrDraggable(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke) : + item(item), + point_type(point_type), + point_i(point_i), + fill_or_stroke(fill_or_stroke) +{ + //g_object_ref(G_OBJECT(item)); + sp_object_ref(item); +} + +GrDraggable::~GrDraggable() +{ + //g_object_unref (G_OBJECT (this->item)); + sp_object_unref(this->item); +} + + +SPObject *GrDraggable::getServer() +{ + SPObject *server = nullptr; + if (item) { + switch (fill_or_stroke) { + case Inkscape::FOR_FILL: + server = item->style->getFillPaintServer(); + break; + case Inkscape::FOR_STROKE: + server = item->style->getStrokePaintServer(); + break; + } + } + + return server; +} + +static void gr_knot_moved_handler(SPKnot *knot, Geom::Point const &ppointer, guint state, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + + // Dragger must have at least one draggable + GrDraggable *draggable = (GrDraggable *) dragger->draggables[0]; + if (!draggable) return; + + // Find mesh corner that corresponds to dragger (only checks first draggable) and highlight it. + GrDragger *dragger_corner = dragger->getMgCorner(); + if (dragger_corner) { + dragger_corner->highlightCorner(true); + } + + // Set-up snapping + SPDesktop *desktop = dragger->parent->desktop; + SnapManager &m = desktop->namedview->snap_manager; + double snap_dist = m.snapprefs.getObjectTolerance() / dragger->parent->desktop->current_zoom(); + + Geom::Point p = ppointer; + + if (state & GDK_SHIFT_MASK) { + // with Shift; unsnap if we carry more than one draggable + if (dragger->draggables.size()>1) { + // create a new dragger + GrDragger *dr_new = new GrDragger (dragger->parent, dragger->point, nullptr); + dragger->parent->draggers.insert(dragger->parent->draggers.begin(), dr_new); + // relink to it all but the first draggable in the list + std::vector::const_iterator i = dragger->draggables.begin(); + for ( ++i ; i != dragger->draggables.end(); ++i ) { + GrDraggable *draggable = *i; + dr_new->addDraggable (draggable); + } + dr_new->updateKnotShape(); + if(dragger->draggables.size()>1){ + GrDraggable *tmp = dragger->draggables[0]; + dragger->draggables.clear(); + dragger->draggables.push_back(tmp); + } + dragger->updateKnotShape(); + dragger->updateTip(); + } + } else if (!(state & GDK_CONTROL_MASK)) { + // without Shift or Ctrl; see if we need to snap to another dragger + for (std::vector::const_iterator di = dragger->parent->draggers.begin(); di != dragger->parent->draggers.end() ; ++di) { + GrDragger *d_new = *di; + if (dragger->mayMerge(d_new) && Geom::L2 (d_new->point - p) < snap_dist) { + + // Merge draggers: + for (auto draggable : dragger->draggables) { + // copy draggable to d_new: + GrDraggable *da_new = new GrDraggable (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); + d_new->addDraggable (da_new); + } + + // unlink and delete this dragger + dragger->parent->draggers.erase(std::remove(dragger->parent->draggers.begin(),dragger->parent->draggers.end(), dragger),dragger->parent->draggers.end()); + d_new->parent->draggers.erase(std::remove(d_new->parent->draggers.begin(),d_new->parent->draggers.end(), dragger),d_new->parent->draggers.end()); + d_new->parent->selected.erase(dragger); + delete dragger; + + // throw out delayed snap context + Inkscape::UI::Tools::sp_event_context_discard_delayed_snap_event(SP_ACTIVE_DESKTOP->event_context); + + // update the new merged dragger + d_new->fireDraggables(true, false, true); + d_new->parent->updateLines(); + d_new->parent->setSelected (d_new); + d_new->updateKnotShape (); + d_new->updateTip (); + d_new->updateDependencies(true); + DocumentUndo::done(d_new->parent->desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, _("Merge gradient handles")); + return; + } + } + } + + if (!((state & GDK_SHIFT_MASK) || (state & GDK_CONTROL_MASK))) { + m.setup(desktop); + Inkscape::SnappedPoint s = m.freeSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + if (s.getSnapped()) { + p = s.getPoint(); + knot->moveto(p); + } + } else if (state & GDK_CONTROL_MASK) { + IntermSnapResults isr; + Inkscape::SnapCandidatePoint scp = Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + unsigned snaps = abs(prefs->getInt("/options/rotationsnapsperpi/value", 12)); + /* 0 means no snapping. */ + + for (std::vector::const_iterator i = dragger->draggables.begin(); i != dragger->draggables.end(); ++i) { + GrDraggable *draggable = *i; + + Geom::Point dr_snap(Geom::infinity(), Geom::infinity()); + + if (draggable->point_type == POINT_LG_BEGIN || draggable->point_type == POINT_LG_END) { + for (std::vector::const_iterator di = dragger->parent->draggers.begin() ; di != dragger->parent->draggers.end() ; ++di) { + GrDragger *d_new = *di; + if (d_new == dragger) + continue; + if (d_new->isA (draggable->item, + draggable->point_type == POINT_LG_BEGIN? POINT_LG_END : POINT_LG_BEGIN, + draggable->fill_or_stroke)) { + // found the other end of the linear gradient; + if (state & GDK_SHIFT_MASK) { + // moving linear around center + Geom::Point center = Geom::Point (0.5*(d_new->point + dragger->point)); + dr_snap = center; + } else { + // moving linear around the other end + dr_snap = d_new->point; + } + } + } + } else if (draggable->point_type == POINT_RG_R1 || draggable->point_type == POINT_RG_R2 || draggable->point_type == POINT_RG_FOCUS) { + for (std::vector::const_iterator di = dragger->parent->draggers.begin(); di != dragger->parent->draggers.end(); ++di) { + GrDragger *d_new = *di; + if (d_new == dragger) + continue; + if (d_new->isA (draggable->item, + POINT_RG_CENTER, + draggable->fill_or_stroke)) { + // found the center of the radial gradient; + dr_snap = d_new->point; + } + } + } else if (draggable->point_type == POINT_RG_CENTER) { + // radial center snaps to hor/vert relative to its original position + dr_snap = dragger->point_original; + } else if (draggable->point_type == POINT_MG_CORNER || + draggable->point_type == POINT_MG_HANDLE || + draggable->point_type == POINT_MG_TENSOR ) { + // std::cout << " gr_knot_moved_handler: Got mesh point!" << std::endl; + } + + // dr_snap contains the origin of the gradient, whereas p will be the new endpoint which we will try to snap now + Inkscape::SnappedPoint sp; + if (dr_snap.isFinite()) { + m.setup(desktop); + if (state & GDK_MOD1_MASK) { + // with Alt, snap to the original angle and its perpendiculars + sp = m.constrainedAngularSnap(scp, dragger->point_original, dr_snap, 2); + } else { + // with Ctrl, snap to M_PI/snaps + sp = m.constrainedAngularSnap(scp, boost::optional(), dr_snap, snaps); + } + m.unSetup(); + isr.points.push_back(sp); + } + } + + m.setup(desktop, false); // turn of the snap indicator temporarily + Inkscape::SnappedPoint bsp = m.findBestSnap(scp, isr, true); + m.unSetup(); + if (!bsp.getSnapped()) { + // If we didn't truly snap to an object or to a grid, then we will still have to look for the + // closest projection onto one of the constraints. findBestSnap() will not do this for us + for (std::list::const_iterator i = isr.points.begin(); i != isr.points.end(); ++i) { + if (i == isr.points.begin() || (Geom::L2((*i).getPoint() - p) < Geom::L2(bsp.getPoint() - p))) { + bsp.setPoint((*i).getPoint()); + bsp.setTarget(Inkscape::SNAPTARGET_CONSTRAINED_ANGLE); + } + } + } + //p = isr.points.front().getPoint(); + p = bsp.getPoint(); + knot->moveto(p); + } + + GrDrag *drag = dragger->parent; // There is just one GrDrag. + drag->keep_selection = (drag->selected.find(dragger)!=drag->selected.end()); + bool scale_radial = (state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK); + + if (drag->keep_selection) { + Geom::Point diff = p - dragger->point; + drag->selected_move_nowrite (diff[Geom::X], diff[Geom::Y], scale_radial); + } else { + Geom::Point p_old = dragger->point; + dragger->point = p; + dragger->fireDraggables (false, scale_radial); + dragger->updateDependencies(false); + dragger->moveMeshHandles( p_old, MG_NODE_NO_SCALE ); + } +} + + +static void gr_midpoint_limits(GrDragger *dragger, SPObject *server, Geom::Point *begin, Geom::Point *end, Geom::Point *low_lim, Geom::Point *high_lim, std::vector &moving) +{ + + GrDrag *drag = dragger->parent; + // a midpoint dragger can (logically) only contain one GrDraggable + GrDraggable *draggable = dragger->draggables[0]; + + // get begin and end points between which dragging is allowed: + // the draglimits are between knot(lowest_i - 1) and knot(highest_i + 1) + moving.push_back(dragger); + + guint lowest_i = draggable->point_i; + guint highest_i = draggable->point_i; + GrDragger *lowest_dragger = dragger; + GrDragger *highest_dragger = dragger; + if (dragger->isSelected()) { + GrDragger* d_add; + while ( true ) + { + d_add = drag->getDraggerFor(draggable->item, draggable->point_type, lowest_i - 1, draggable->fill_or_stroke); + if ( d_add && drag->selected.find(d_add)!=drag->selected.end() ) { + lowest_i = lowest_i - 1; + moving.insert(moving.begin(),d_add); + lowest_dragger = d_add; + } else { + break; + } + } + + while ( true ) + { + d_add = drag->getDraggerFor(draggable->item, draggable->point_type, highest_i + 1, draggable->fill_or_stroke); + if ( d_add && drag->selected.find(d_add)!=drag->selected.end() ) { + highest_i = highest_i + 1; + moving.push_back(d_add); + highest_dragger = d_add; + } else { + break; + } + } + } + + if ( SP_IS_LINEARGRADIENT(server) ) { + guint num = SP_LINEARGRADIENT(server)->vector.stops.size(); + GrDragger *d_temp; + if (lowest_i == 1) { + d_temp = drag->getDraggerFor (draggable->item, POINT_LG_BEGIN, 0, draggable->fill_or_stroke); + } else { + d_temp = drag->getDraggerFor (draggable->item, POINT_LG_MID, lowest_i - 1, draggable->fill_or_stroke); + } + if (d_temp) + *begin = d_temp->point; + + d_temp = drag->getDraggerFor (draggable->item, POINT_LG_MID, highest_i + 1, draggable->fill_or_stroke); + if (d_temp == nullptr) { + d_temp = drag->getDraggerFor (draggable->item, POINT_LG_END, num-1, draggable->fill_or_stroke); + } + if (d_temp) + *end = d_temp->point; + } else if ( SP_IS_RADIALGRADIENT(server) ) { + guint num = SP_RADIALGRADIENT(server)->vector.stops.size(); + GrDragger *d_temp; + if (lowest_i == 1) { + d_temp = drag->getDraggerFor (draggable->item, POINT_RG_CENTER, 0, draggable->fill_or_stroke); + } else { + d_temp = drag->getDraggerFor (draggable->item, draggable->point_type, lowest_i - 1, draggable->fill_or_stroke); + } + if (d_temp) + *begin = d_temp->point; + + d_temp = drag->getDraggerFor (draggable->item, draggable->point_type, highest_i + 1, draggable->fill_or_stroke); + if (d_temp == nullptr) { + d_temp = drag->getDraggerFor (draggable->item, (draggable->point_type==POINT_RG_MID1) ? POINT_RG_R1 : POINT_RG_R2, num-1, draggable->fill_or_stroke); + } + if (d_temp) + *end = d_temp->point; + } + + *low_lim = dragger->point - (lowest_dragger->point - *begin); + *high_lim = dragger->point - (highest_dragger->point - *end); +} + +/** + * Called when a midpoint knot is dragged. + */ +static void gr_knot_moved_midpoint_handler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + GrDrag *drag = dragger->parent; + // a midpoint dragger can (logically) only contain one GrDraggable + GrDraggable *draggable = dragger->draggables[0]; + + // FIXME: take from prefs + double snap_fraction = 0.1; + + Geom::Point p = ppointer; + Geom::Point begin(0,0), end(0,0); + Geom::Point low_lim(0,0), high_lim(0,0); + + SPObject *server = draggable->getServer(); + + std::vector moving; + gr_midpoint_limits(dragger, server, &begin, &end, &low_lim, &high_lim, moving); + + if (state & GDK_CONTROL_MASK) { + Geom::LineSegment ls(low_lim, high_lim); + p = ls.pointAt(round(ls.nearestTime(p) / snap_fraction) * snap_fraction); + } else { + Geom::LineSegment ls(low_lim, high_lim); + p = ls.pointAt(ls.nearestTime(p)); + if (!(state & GDK_SHIFT_MASK)) { + Inkscape::Snapper::SnapConstraint cl(low_lim, high_lim - low_lim); + SPDesktop *desktop = dragger->parent->desktop; + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.constrainedSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE, cl); + m.unSetup(); + } + } + Geom::Point displacement = p - dragger->point; + + for (auto drg : moving) { + SPKnot *drgknot = drg->knot; + Geom::Point this_move = displacement; + if (state & GDK_MOD1_MASK) { + // FIXME: unify all these profiles (here, in nodepath, in tweak) in one place + double alpha = 1.0; + if (Geom::L2(drg->point - dragger->point) + Geom::L2(drg->point - begin) - 1e-3 > Geom::L2(dragger->point - begin)) { // drg is on the end side from dragger + double x = Geom::L2(drg->point - dragger->point)/Geom::L2(end - dragger->point); + this_move = (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5) * this_move; + } else { // drg is on the begin side from dragger + double x = Geom::L2(drg->point - dragger->point)/Geom::L2(begin - dragger->point); + this_move = (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5) * this_move; + } + } + drg->point += this_move; + drgknot->moveto(drg->point); + drg->fireDraggables (false); + drg->updateDependencies(false); + } + + drag->keep_selection = dragger->isSelected(); +} + + + +static void gr_knot_mousedown_handler(SPKnot */*knot*/, unsigned int /*state*/, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + GrDrag *drag = dragger->parent; + + // Turn off all mesh handle highlighting + for(auto d : drag->draggers) { //for all selected draggers + d->highlightCorner(false); + } + + // Highlight only mesh corner corresponding to grabbed corner or handle + GrDragger *dragger_corner = dragger->getMgCorner(); + if (dragger_corner) { + dragger_corner->highlightCorner(true); + } + + dragger->parent->desktop->canvas->forceFullRedrawAfterInterruptions(5); +} + +/** + * Called when the mouse releases a dragger knot; changes gradient writing to repr, updates other draggers if needed. + */ +static void gr_knot_ungrabbed_handler(SPKnot *knot, unsigned int state, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + + dragger->parent->desktop->canvas->endForcedFullRedraws(); + + dragger->point_original = dragger->point = knot->pos; + + if ((state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK)) { + dragger->fireDraggables (true, true); + } else { + dragger->fireDraggables (true); + } + dragger->moveMeshHandles( dragger->point_original, MG_NODE_NO_SCALE ); + + for (std::set::const_iterator it = dragger->parent->selected.begin(); it != dragger->parent->selected.end() ; ++it ) { + if (*it == dragger) + continue; + (*it)->fireDraggables (true); + } + + // make this dragger selected + if (!dragger->parent->keep_selection) { + dragger->parent->setSelected (dragger); + } + dragger->parent->keep_selection = false; + + dragger->updateDependencies(true); + + // we did an undoable action + DocumentUndo::done(dragger->parent->desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, _("Move gradient handle")); +} + +/** + * Called when a dragger knot is clicked; selects the dragger or deletes it depending on the + * state of the keyboard keys. + */ +static void gr_knot_clicked_handler(SPKnot */*knot*/, guint state, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + GrDraggable *draggable = dragger->draggables[0]; + if (!draggable) return; + + if ( (state & GDK_CONTROL_MASK) && (state & GDK_MOD1_MASK ) ) { + // delete this knot from vector + SPGradient *gradient = getGradient(draggable->item, draggable->fill_or_stroke); + gradient = gradient->getVector(); + if (gradient->vector.stops.size() > 2) { // 2 is the minimum + SPStop *stop = nullptr; + switch (draggable->point_type) { // if we delete first or last stop, move the next/previous to the edge + + case POINT_LG_BEGIN: + case POINT_RG_CENTER: + stop = gradient->getFirstStop(); + { + SPStop *next = stop->getNextStop(); + if (next) { + next->offset = 0; + sp_repr_set_css_double(next->getRepr(), "offset", 0); + } + } + break; + + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + stop = sp_last_stop(gradient); + { + SPStop *prev = stop->getPrevStop(); + if (prev) { + prev->offset = 1; + sp_repr_set_css_double(prev->getRepr(), "offset", 1); + } + } + break; + + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + stop = sp_get_stop_i(gradient, draggable->point_i); + break; + + default: + return; + + } + + gradient->getRepr()->removeChild(stop->getRepr()); + DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT, + _("Delete gradient stop")); + } + } else { + // select the dragger + + dragger->point_original = dragger->point; + + if ( state & GDK_SHIFT_MASK ) { + dragger->parent->setSelected (dragger, true, false); + } else { + dragger->parent->setSelected (dragger); + } + } +} + +/** + * Called when a dragger knot is doubleclicked; + */ +static void gr_knot_doubleclicked_handler(SPKnot */*knot*/, guint /*state*/, gpointer data) +{ + GrDragger *dragger = (GrDragger *) data; + + dragger->point_original = dragger->point; + + if (dragger->draggables.empty()) + return; + + /* + * 2012/4 - Do nothing (gradient editor to be disabled) + */ + //GrDraggable *draggable = (GrDraggable *) dragger->draggables->data; + //sp_item_gradient_edit_stop (draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); +} + +/** + * Act upon all draggables of the dragger, setting them to the dragger's point. + */ +void GrDragger::fireDraggables(bool write_repr, bool scale_radial, bool merging_focus) +{ + for (auto draggable : this->draggables) { + // set local_change flag so that selection_changed callback does not regenerate draggers + this->parent->local_change = true; + + // change gradient, optionally writing to repr; prevent focus from moving if it's snapped + // to the center, unless it's the first update upon merge when we must snap it to the point + if (merging_focus || + !(draggable->point_type == POINT_RG_FOCUS && this->isA(draggable->item, POINT_RG_CENTER, draggable->point_i, draggable->fill_or_stroke))) + { + sp_item_gradient_set_coords (draggable->item, draggable->point_type, draggable->point_i, this->point, draggable->fill_or_stroke, write_repr, scale_radial); + } + } +} + +void GrDragger::updateControlSizesOverload(SPKnot * knot) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int sizes[] = {4, 6, 8, 10, 12, 14, 16}; + std::vector sizeTable = std::vector(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + int size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 7); + int knot_size = sizeTable[size - 1]; + if(knot->shape == SP_KNOT_SHAPE_TRIANGLE){ + knot_size *= 2.2; + knot_size = floor(knot_size); + if ( knot_size % 2 == 0 ){ + knot_size += 1; + } + } + knot->setSize(knot_size); +} + +void GrDragger::updateControlSizes() +{ + updateControlSizesOverload(this->knot); + this->knot->updateCtrl(); + this->updateKnotShape(); +} + +/** + * Checks if the dragger has a draggable with this point_type. + */ +bool GrDragger::isA(GrPointType point_type) +{ + for (auto draggable : this->draggables) { + if (draggable->point_type == point_type) { + return true; + } + } + return false; +} + +/** + * Checks if the dragger has a draggable with this item, point_type + point_i (number), fill_or_stroke. + */ +bool GrDragger::isA(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke) +{ + for (auto draggable : this->draggables) { + if ( (draggable->point_type == point_type) && (draggable->point_i == point_i) && (draggable->item == item) && (draggable->fill_or_stroke == fill_or_stroke) ) { + return true; + } + } + return false; +} + +/** + * Checks if the dragger has a draggable with this item, point_type, fill_or_stroke. + */ +bool GrDragger::isA(SPItem *item, GrPointType point_type, Inkscape::PaintTarget fill_or_stroke) +{ + for (auto draggable : this->draggables) { + if ( (draggable->point_type == point_type) && (draggable->item == item) && (draggable->fill_or_stroke == fill_or_stroke) ) { + return true; + } + } + return false; +} + +bool GrDraggable::mayMerge(GrDraggable *da2) +{ + if ((this->item == da2->item) && (this->fill_or_stroke == da2->fill_or_stroke)) { + // we must not merge the points of the same gradient! + if (!((this->point_type == POINT_RG_FOCUS && da2->point_type == POINT_RG_CENTER) || + (this->point_type == POINT_RG_CENTER && da2->point_type == POINT_RG_FOCUS))) { + // except that we can snap center and focus together + return false; + } + } + // disable merging of midpoints. + if ( (this->point_type == POINT_LG_MID) || (da2->point_type == POINT_LG_MID) + || (this->point_type == POINT_RG_MID1) || (da2->point_type == POINT_RG_MID1) + || (this->point_type == POINT_RG_MID2) || (da2->point_type == POINT_RG_MID2) ) + return false; + + return true; +} + +bool GrDragger::mayMerge(GrDragger *other) +{ + if (this == other) + return false; + + for (auto da1 : this->draggables) { + for (auto da2 : other->draggables) { + if (!da1->mayMerge(da2)) + return false; + } + } + return true; +} + +bool GrDragger::mayMerge(GrDraggable *da2) +{ + for (auto da1 : this->draggables) { + if (!da1->mayMerge(da2)) + return false; + } + return true; +} + +/** + * Update mesh handles when mesh corner is moved. + * pc_old: old position of corner (could be changed to dp if we figure out transforms). + * op: how other nodes (handles, tensors) should be moved. + * Scaling takes place only between a selected and an unselected corner, + * other wise a handle is displaced the same distance as the adjacent corner. + * If a side is a line, then the handles are always placed 1/3 of side length + * from each corner. + * + * Ooops, needs to be reimplemented. + */ +void +GrDragger::moveMeshHandles ( Geom::Point pc_old, MeshNodeOperation op ) +{ + // This routine might more properly be in mesh-context.cpp but moving knots is + // handled here rather than there. + + // We need to update two places: + // 1. In SPMeshArrayI with object coordinates + // 2. In Drager/Knots with desktop coordinates. + + // This routine is more complicated than it might need to be inorder to allow + // corner points to be selected in multiple meshes at the same time... with some + // sharing the same dragger (overkill, perhaps?). + + // If no corner point in GrDragger then do nothing. + if( !isA (POINT_MG_CORNER ) ) return; + + GrDrag *drag = this->parent; + + // We need a list of selected corners per mesh if scaling. + std::map > selected_corners; + // scaling was disabled so #if 0'ing out for now. +#if 0 + const bool scale = false; + if( scale ) { + + for( std::set::const_iterator it = drag->selected.begin(); it != drag->selected.end(); ++it ) { + GrDragger *dragger = *it; + for (std::vector::const_iterator it2 = dragger->draggables.begin(); it2 != dragger->draggables.end(); ++it2 ) { + GrDraggable *draggable = *it2; + + // Check draggable is of type POINT_MG_CORNER (don't allow selection of POINT_MG_HANDLE) + if( draggable->point_type != POINT_MG_CORNER ) continue; + + // Must be a mesh gradient + SPGradient *gradient = getGradient(draggable->item, draggable->fill_or_stroke); + if ( !SP_IS_MESHGRADIENT( gradient ) ) continue; + + selected_corners[ gradient ].push_back( draggable->point_i ); + } + } + } +#endif + + // Now we do the handle moves. + + // Loop over all draggables in moved corner + std::map > dragger_corners; + for (auto draggable : draggables) { + SPItem *item = draggable->item; + gint point_type = draggable->point_type; + gint point_i = draggable->point_i; + Inkscape::PaintTarget + fill_or_stroke = draggable->fill_or_stroke; + + // Check draggable is of type POINT_MG_CORNER (don't allow selection of POINT_MG_HANDLE) + if( point_type != POINT_MG_CORNER ) continue; + + // Must be a mesh gradient + SPGradient *gradient = getGradient(item, fill_or_stroke); + if ( !SP_IS_MESHGRADIENT( gradient ) ) continue; + SPMeshGradient *mg = SP_MESHGRADIENT( gradient ); + + // pc_old is the old corner position in desktop coordinates, we need it in gradient coordinate. + gradient = sp_gradient_convert_to_userspace (gradient, item, (fill_or_stroke == Inkscape::FOR_FILL) ? "fill" : "stroke"); + Geom::Affine i2d ( item->i2dt_affine() ); + Geom::Point pcg_old = pc_old * i2d.inverse(); + pcg_old *= (gradient->gradientTransform).inverse(); + + mg->array.update_handles( point_i, selected_corners[ gradient ], pcg_old, op ); + mg->array.write( mg ); + + // Move on-screen knots + for( guint i = 0; i < mg->array.handles.size(); ++i ) { + GrDragger *handle = drag->getDraggerFor( item, POINT_MG_HANDLE, i, fill_or_stroke ); + SPKnot *knot = handle->knot; + Geom::Point pk = getGradientCoords( item, POINT_MG_HANDLE, i, fill_or_stroke ); + knot->moveto(pk); + + } + + for( guint i = 0; i < mg->array.tensors.size(); ++i ) { + + GrDragger *handle = drag->getDraggerFor( item, POINT_MG_TENSOR, i, fill_or_stroke ); + SPKnot *knot = handle->knot; + Geom::Point pk = getGradientCoords( item, POINT_MG_TENSOR, i, fill_or_stroke ); + knot->moveto(pk); + + } + + } // Loop over draggables. +} + + +/** + * Updates the statusbar tip of the dragger knot, based on its draggables. + */ +void GrDragger::updateTip() +{ + g_return_if_fail(this->knot != nullptr); + + if (this->knot && this->knot->tip) { + g_free (this->knot->tip); + this->knot->tip = nullptr; + } + + if (this->draggables.size() == 1) { + GrDraggable *draggable = this->draggables[0]; + char *item_desc = draggable->item->detailedDescription(); + switch (draggable->point_type) { + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + this->knot->tip = g_strdup_printf (_("%s %d for: %s%s; drag with Ctrl to snap offset; click with Ctrl+Alt to delete stop"), + _(gr_knot_descr[draggable->point_type]), + draggable->point_i, + item_desc, + (draggable->fill_or_stroke == Inkscape::FOR_STROKE) ? _(" (stroke)") : ""); + break; + + case POINT_MG_CORNER: + case POINT_MG_HANDLE: + case POINT_MG_TENSOR: + this->knot->tip = g_strdup_printf (_("%s for: %s%s"), + _(gr_knot_descr[draggable->point_type]), + item_desc, + (draggable->fill_or_stroke == Inkscape::FOR_STROKE) ? _(" (stroke)") : ""); + break; + + default: + this->knot->tip = g_strdup_printf (_("%s for: %s%s; drag with Ctrl to snap angle, with Ctrl+Alt to preserve angle, with Ctrl+Shift to scale around center"), + _(gr_knot_descr[draggable->point_type]), + item_desc, + (draggable->fill_or_stroke == Inkscape::FOR_STROKE) ? _(" (stroke)") : ""); + break; + } + g_free(item_desc); + } else if (draggables.size() == 2 && isA (POINT_RG_CENTER) && isA (POINT_RG_FOCUS)) { + this->knot->tip = g_strdup_printf ("%s", _("Radial gradient center and focus; drag with Shift to separate focus")); + } else { + int length = this->draggables.size(); + this->knot->tip = g_strdup_printf (ngettext("Gradient point shared by %d gradient; drag with Shift to separate", + "Gradient point shared by %d gradients; drag with Shift to separate", + length), + length); + } +} + +/** + * Adds a draggable to the dragger. + */ +void GrDragger::updateKnotShape() +{ + if (draggables.empty()) + return; + GrDraggable *last = draggables.back(); + + g_object_set (G_OBJECT (this->knot->item), "shape", gr_knot_shapes[last->point_type], NULL); + + // For highlighting mesh handles corresponding to selected corner + if (this->knot->shape == SP_KNOT_SHAPE_TRIANGLE) { + this->knot->setFill(GR_KNOT_COLOR_HIGHLIGHT, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER); + if (gr_knot_shapes[last->point_type] == SP_KNOT_SHAPE_CIRCLE) { + g_object_set (G_OBJECT (this->knot->item), "shape", SP_KNOT_SHAPE_TRIANGLE, NULL); + } + } +} + +/** + * Adds a draggable to the dragger. + */ +void GrDragger::addDraggable(GrDraggable *draggable) +{ + this->draggables.insert(this->draggables.begin(), draggable); + + this->updateTip(); +} + + +/** + * Moves this dragger to the point of the given draggable, acting upon all other draggables. + */ +void GrDragger::moveThisToDraggable(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, bool write_repr) +{ + if (draggables.empty()) + return; + + GrDraggable *dr_first = draggables[0]; + + this->point = getGradientCoords(dr_first->item, dr_first->point_type, dr_first->point_i, dr_first->fill_or_stroke); + this->point_original = this->point; + + this->knot->moveto(this->point); + + for (auto da : draggables) { + if ( (da->item == item) && + (da->point_type == point_type) && + (point_i == -1 || da->point_i == point_i) && + (da->fill_or_stroke == fill_or_stroke) ) { + // Don't move initial draggable + continue; + } + sp_item_gradient_set_coords(da->item, da->point_type, da->point_i, this->point, da->fill_or_stroke, write_repr, false); + } + // FIXME: here we should also call this->updateDependencies(write_repr); to propagate updating, but how to prevent loops? +} + + +/** + * Moves all midstop draggables that depend on this one. + */ +void GrDragger::updateMidstopDependencies(GrDraggable *draggable, bool write_repr) +{ + SPObject *server = draggable->getServer(); + if (!server) + return; + guint num = SP_GRADIENT(server)->vector.stops.size(); + if (num <= 2) return; + + if ( SP_IS_LINEARGRADIENT(server) ) { + for ( guint i = 1; i < num - 1; i++ ) { + this->moveOtherToDraggable (draggable->item, POINT_LG_MID, i, draggable->fill_or_stroke, write_repr); + } + } else if ( SP_IS_RADIALGRADIENT(server) ) { + for ( guint i = 1; i < num - 1; i++ ) { + this->moveOtherToDraggable (draggable->item, POINT_RG_MID1, i, draggable->fill_or_stroke, write_repr); + this->moveOtherToDraggable (draggable->item, POINT_RG_MID2, i, draggable->fill_or_stroke, write_repr); + } + } +} + + +/** + * Moves all draggables that depend on this one. + */ +void GrDragger::updateDependencies(bool write_repr) +{ + for (auto draggable : draggables) { + switch (draggable->point_type) { + case POINT_LG_BEGIN: + { + // the end point is dependent only when dragging with ctrl+shift + this->moveOtherToDraggable (draggable->item, POINT_LG_END, -1, draggable->fill_or_stroke, write_repr); + + this->updateMidstopDependencies (draggable, write_repr); + } + break; + case POINT_LG_END: + { + // the begin point is dependent only when dragging with ctrl+shift + this->moveOtherToDraggable (draggable->item, POINT_LG_BEGIN, 0, draggable->fill_or_stroke, write_repr); + + this->updateMidstopDependencies (draggable, write_repr); + } + break; + case POINT_LG_MID: + // no other nodes depend on mid points. + break; + case POINT_RG_R2: + this->moveOtherToDraggable (draggable->item, POINT_RG_R1, -1, draggable->fill_or_stroke, write_repr); + this->moveOtherToDraggable (draggable->item, POINT_RG_FOCUS, -1, draggable->fill_or_stroke, write_repr); + this->updateMidstopDependencies (draggable, write_repr); + break; + case POINT_RG_R1: + this->moveOtherToDraggable (draggable->item, POINT_RG_R2, -1, draggable->fill_or_stroke, write_repr); + this->moveOtherToDraggable (draggable->item, POINT_RG_FOCUS, -1, draggable->fill_or_stroke, write_repr); + this->updateMidstopDependencies (draggable, write_repr); + break; + case POINT_RG_CENTER: + this->moveOtherToDraggable (draggable->item, POINT_RG_R1, -1, draggable->fill_or_stroke, write_repr); + this->moveOtherToDraggable (draggable->item, POINT_RG_R2, -1, draggable->fill_or_stroke, write_repr); + this->moveOtherToDraggable (draggable->item, POINT_RG_FOCUS, -1, draggable->fill_or_stroke, write_repr); + this->updateMidstopDependencies (draggable, write_repr); + break; + case POINT_RG_FOCUS: + // nothing can depend on that + break; + case POINT_RG_MID1: + this->moveOtherToDraggable (draggable->item, POINT_RG_MID2, draggable->point_i, draggable->fill_or_stroke, write_repr); + break; + case POINT_RG_MID2: + this->moveOtherToDraggable (draggable->item, POINT_RG_MID1, draggable->point_i, draggable->fill_or_stroke, write_repr); + break; + default: + break; + } + } +} + + + +GrDragger::GrDragger(GrDrag *parent, Geom::Point p, GrDraggable *draggable) + : point(p), + point_original(p) +{ + this->draggables.clear(); + + this->parent = parent; + + // create the knot + this->knot = new SPKnot(parent->desktop, nullptr); + this->knot->setMode(SP_KNOT_MODE_XOR); + guint32 fill_color = GR_KNOT_COLOR_NORMAL; + if (draggable && draggable->point_type == POINT_MG_CORNER) { + fill_color = GR_KNOT_COLOR_MESHCORNER; + } + this->knot->setFill(fill_color, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER); + this->knot->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f); + this->updateControlSizesOverload(this->knot); + this->knot->updateCtrl(); + + // move knot to the given point + this->knot->setPosition(p, SP_KNOT_STATE_NORMAL); + this->knot->show(); + + // connect knot's signals + if ( (draggable) // it can be NULL if a node in unsnapped (eg. focus point unsnapped from center) + // luckily, midstops never snap to other nodes so are never unsnapped... + && ( (draggable->point_type == POINT_LG_MID) + || (draggable->point_type == POINT_RG_MID1) + || (draggable->point_type == POINT_RG_MID2) ) ) + { + this->_moved_connection = this->knot->moved_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_moved_midpoint_handler), this)); + } else { + this->_moved_connection = this->knot->moved_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_moved_handler), this)); + } + + this->sizeUpdatedConn = ControlManager::getManager().connectCtrlSizeChanged(sigc::mem_fun(*this, &GrDragger::updateControlSizes)); + this->_clicked_connection = this->knot->click_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_clicked_handler), this)); + this->_doubleclicked_connection = this->knot->doubleclicked_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_doubleclicked_handler), this)); + this->_mousedown_connection = this->knot->mousedown_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_mousedown_handler), this)); + this->_ungrabbed_connection = this->knot->ungrabbed_signal.connect(sigc::bind(sigc::ptr_fun(gr_knot_ungrabbed_handler), this)); + + // add the initial draggable + if (draggable) { + this->addDraggable (draggable); + } + + updateKnotShape(); +} + +GrDragger::~GrDragger() +{ + // unselect if it was selected + // Hmm, this causes a race condition as it triggers a call to gradient_selection_changed which + // can be executed while a list of draggers is being deleted. It doesn't actually seem to be + // necessary. + //this->parent->setDeselected(this); + + // disconnect signals + this->sizeUpdatedConn.disconnect(); + this->_moved_connection.disconnect(); + this->_clicked_connection.disconnect(); + this->_doubleclicked_connection.disconnect(); + this->_mousedown_connection.disconnect(); + this->_ungrabbed_connection.disconnect(); + + /* unref should call destroy */ + knot_unref(this->knot); + + // delete all draggables + for (auto draggable : this->draggables) { + delete draggable; + } + this->draggables.clear(); +} + +/** + * Select the dragger which has the given draggable. + */ +GrDragger *GrDrag::getDraggerFor(GrDraggable *d) { + for (auto dragger : this->draggers) { + for (std::vector::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j ) { + if (d == *j) { + return dragger; + } + } + } + return nullptr; +} + +/** + * Select the dragger which has the given draggable. + */ +GrDragger *GrDrag::getDraggerFor(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke) +{ + for (auto dragger : this->draggers) { + for (std::vector::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j ) { + GrDraggable *da2 = *j; + if ( (da2->item == item) && + (da2->point_type == point_type) && + (point_i == -1 || da2->point_i == point_i) && // -1 means this does not matter + (da2->fill_or_stroke == fill_or_stroke)) { + return (dragger); + } + } + } + return nullptr; +} + + +void GrDragger::moveOtherToDraggable(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, bool write_repr) +{ + GrDragger *d = this->parent->getDraggerFor(item, point_type, point_i, fill_or_stroke); + if (d && d != this) { + d->moveThisToDraggable(item, point_type, point_i, fill_or_stroke, write_repr); + } +} + +/** + * Find mesh corner corresponding to given dragger. + */ +GrDragger* GrDragger::getMgCorner(){ + GrDraggable *draggable = (GrDraggable *) this->draggables[0]; + if (draggable) { + + // If corner, we already found it! + if (draggable->point_type == POINT_MG_CORNER) { + return this; + } + + // The mapping between handles and corners is complex... so find corner by bruit force. + SPGradient *gradient = getGradient(draggable->item, draggable->fill_or_stroke); + SPMeshGradient *mg = dynamic_cast(gradient); + if (mg) { + std::vector< std::vector< SPMeshNode* > > nodes = mg->array.nodes; + for (guint i = 0; i < nodes.size(); ++i) { + for (guint j = 0; j < nodes[i].size(); ++j) { + if (nodes[i][j]->set && nodes[i][j]->node_type == MG_NODE_TYPE_HANDLE) { + if (draggable->point_i == (gint)nodes[i][j]->draggable) { + + if (nodes.size() > i+1 && nodes[i+1].size() > j && nodes[i+1][j]->node_type == MG_NODE_TYPE_CORNER) { + return this->parent->getDraggerFor(draggable->item, POINT_MG_CORNER, nodes[i+1][j]->draggable, draggable->fill_or_stroke); + } + + if (j != 0 && nodes.size() > i && nodes[i].size() > j-1 && nodes[i][j-1]->node_type == MG_NODE_TYPE_CORNER) { + return this->parent->getDraggerFor(draggable->item, POINT_MG_CORNER, nodes[i][j-1]->draggable, draggable->fill_or_stroke); + } + + if (i != 0 && nodes.size() > i-1 && nodes[i-1].size() > j && nodes[i-1][j]->node_type == MG_NODE_TYPE_CORNER) { + return this->parent->getDraggerFor(draggable->item, POINT_MG_CORNER, nodes[i-1][j]->draggable, draggable->fill_or_stroke); + } + + if (nodes.size() > i && nodes[i].size() > j+1 && nodes[i][j+1]->node_type == MG_NODE_TYPE_CORNER) { + return this->parent->getDraggerFor(draggable->item, POINT_MG_CORNER, nodes[i][j+1]->draggable, draggable->fill_or_stroke); + } + } + } + } + } + } + } + return nullptr; +} + +/** + * Highlight mesh node + */ +void GrDragger::highlightNode(SPMeshNode *node, bool highlight, Geom::Point corner_pos, int index) +{ + GrPointType type = POINT_MG_TENSOR; + if (node->node_type == MG_NODE_TYPE_HANDLE) { + type = POINT_MG_HANDLE; + } + + GrDraggable *draggable = (GrDraggable *) this->draggables[0]; + GrDragger *d = this->parent->getDraggerFor(draggable->item, type, node->draggable, draggable->fill_or_stroke); + if (d && node->draggable < G_MAXUINT) { + Geom::Point end = d->knot->pos; + Geom::Ray ray = Geom::Ray(corner_pos, end); + if (d->knot->desktop->is_yaxisdown()) { + end *= Geom::Scale(1, -1); + corner_pos *= Geom::Scale(1, -1); + ray.setPoints(corner_pos, end); + } + double angl = ray.angle(); + + if (highlight && knot->fill[SP_KNOT_VISIBLE] == GR_KNOT_COLOR_HIGHLIGHT && abs(angl - knot->angle) > Geom::rad_from_deg(10.0)){ + return; + } + + SPKnot *knot = d->knot; + if (highlight) { + knot->setFill(GR_KNOT_COLOR_HIGHLIGHT, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER); + } else { + knot->setFill(GR_KNOT_COLOR_NORMAL, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER, GR_KNOT_COLOR_MOUSEOVER); + } + + if (type == POINT_MG_HANDLE) { + if (highlight) { + knot->setShape(SP_KNOT_SHAPE_TRIANGLE); + } else { + knot->setShape(SP_KNOT_SHAPE_CIRCLE); + } + } else { + //Code for tensors + return; + } + + this->updateControlSizesOverload(knot); + knot->setAngle(angl); + knot->updateCtrl(); + d->updateKnotShape(); + } +} + +/** + * Highlight handles for mesh corner corresponding to this dragger. + */ +void GrDragger::highlightCorner(bool highlight) +{ + // Must be a mesh gradient + GrDraggable *draggable = (GrDraggable *) this->draggables[0]; + if (draggable && draggable->point_type == POINT_MG_CORNER) { + SPGradient *gradient = getGradient(draggable->item, draggable->fill_or_stroke); + if (SP_IS_MESHGRADIENT( gradient )) { + Geom::Point corner_point = this->point; + gint corner = draggable->point_i; + SPMeshGradient *mg = SP_MESHGRADIENT( gradient ); + SPMeshNodeArray mg_arr = mg->array; + std::vector< std::vector< SPMeshNode* > > nodes = mg_arr.nodes; + // Find number of patch rows and columns + guint mrow = mg_arr.patch_rows(); + guint mcol = mg_arr.patch_columns(); + // Number of corners in a row of patches. + guint ncorners = mcol + 1; + // Find corner row/column + guint crow = corner / ncorners; + guint ccol = corner % ncorners; + // Find node row/column + guint nrow = crow * 3; + guint ncol = ccol * 3; + + bool patch[4]; + patch[0] = patch[1] = patch[2] = patch[3] = false; + if (ccol > 0 && crow > 0 ) patch[0] = true; + if (ccol < mcol && crow > 0 ) patch[1] = true; + if (ccol < mcol && crow < mrow ) patch[2] = true; + if (ccol > 0 && crow < mrow ) patch[3] = true; + if (patch[0] || patch[1]) { + highlightNode(nodes[nrow - 1][ncol], highlight, corner_point, 0); + } + if (patch[1] || patch[2]) { + highlightNode(nodes[nrow][ncol + 1], highlight, corner_point, 1); + } + if (patch[2] || patch[3]) { + highlightNode(nodes[nrow + 1][ncol], highlight, corner_point, 2); + } + if (patch[3] || patch[0]) { + highlightNode(nodes[nrow][ncol - 1], highlight, corner_point, 3); + } + // Highlight tensors + /* + if( patch[0] ) highlightNode(nodes[nrow-1][ncol-1], highlight, corner_point, point_i); + if( patch[1] ) highlightNode(nodes[nrow-1][ncol+1], highlight, corner_point, point_i); + if( patch[2] ) highlightNode(nodes[nrow+1][ncol+1], highlight, corner_point, point_i); + if( patch[3] ) highlightNode(nodes[nrow+1][ncol-1], highlight, corner_point, point_i); + */ + } + } +} + +/** + * Draw this dragger as selected. + */ +void GrDragger::select() +{ + this->knot->fill [SP_KNOT_STATE_NORMAL] = GR_KNOT_COLOR_SELECTED; + g_object_set (G_OBJECT (this->knot->item), "fill_color", GR_KNOT_COLOR_SELECTED, NULL); + highlightCorner(true); +} + +/** + * Draw this dragger as normal (deselected). + */ +void GrDragger::deselect() +{ + guint32 fill_color = isA(POINT_MG_CORNER) ? GR_KNOT_COLOR_MESHCORNER : GR_KNOT_COLOR_NORMAL; + this->knot->fill [SP_KNOT_STATE_NORMAL] = fill_color; + g_object_set (G_OBJECT (this->knot->item), "fill_color", fill_color, NULL); + highlightCorner(false); +} + +bool +GrDragger::isSelected() +{ + return parent->selected.find(this) != parent->selected.end(); +} + +/** + * Deselect all stops/draggers (private). + */ +void GrDrag::deselect_all() +{ + for (auto it : selected) + it->deselect(); + selected.clear(); +} + +/** + * Deselect all stops/draggers (public; emits signal). + */ +void GrDrag::deselectAll() +{ + deselect_all(); + this->desktop->emitToolSubselectionChanged(nullptr); +} + +/** + * Select all stops/draggers. + */ +void GrDrag::selectAll() +{ + for (auto d : this->draggers) { + setSelected (d, true, true); + } +} + +/** + * Select all stops/draggers that match the coords. + */ +void GrDrag::selectByCoords(std::vector coords) +{ + for (auto d : this->draggers) { + for (auto coord : coords) { + if (Geom::L2 (d->point - coord) < 1e-4) { + setSelected (d, true, true); + } + } + } +} + +/** + * Select draggers by stop + */ +void GrDrag::selectByStop(SPStop *stop, bool add_to_selection, bool override ) +{ + for (auto dragger : this->draggers) { + + for (std::vector::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j) { + + GrDraggable *d = *j; + SPGradient *gradient = getGradient(d->item, d->fill_or_stroke); + SPGradient *vector = gradient->getVector(false); + SPStop *stop_i = sp_get_stop_i(vector, d->point_i); + + if (stop_i == stop) { + setSelected(dragger, add_to_selection, override); + } + } + } +} +/** + * Select all stops/draggers that fall within the rect. + */ +void GrDrag::selectRect(Geom::Rect const &r) +{ + for (auto d : this->draggers) { + if (r.contains(d->point)) { + setSelected (d, true, true); + } + } +} + +/** + * Select a dragger. + * @param dragger The dragger to select. + * @param add_to_selection If true, add to selection, otherwise deselect others. + * @param override If true, always select this node, otherwise toggle selected status. +*/ +void GrDrag::setSelected(GrDragger *dragger, bool add_to_selection, bool override) +{ + GrDragger *seldragger = nullptr; + + // Don't allow selecting a mesh handle or mesh tensor. + // We might want to rethink since a dragger can have draggables of different types. + if ( dragger->isA( POINT_MG_HANDLE ) || dragger->isA( POINT_MG_TENSOR ) ) return; + + if (add_to_selection) { + if (!dragger) return; + if (override) { + selected.insert(dragger); + dragger->select(); + seldragger = dragger; + } else { // toggle + if(selected.find(dragger)!=selected.end()) { + selected.erase(dragger); + dragger->deselect(); + if (!selected.empty()) { + seldragger = *(selected.begin()); // select the dragger that is first in the list + } + } else { + selected.insert(dragger); + dragger->select(); + seldragger = dragger; + } + } + } else { + deselect_all(); + if (dragger) { + selected.insert(dragger); + dragger->select(); + seldragger = dragger; + } + } + if (seldragger) { + this->desktop->emitToolSubselectionChanged((gpointer) seldragger); + } +} + +/** + * Deselect a dragger. + * @param dragger The dragger to deselect. + */ +void GrDrag::setDeselected(GrDragger *dragger) +{ + if (selected.find(dragger) != selected.end()) { + selected.erase(dragger); + dragger->deselect(); + } + this->desktop->emitToolSubselectionChanged((gpointer) (selected.empty() ? NULL :*(selected.begin()))); +} + + + +/** + * Create a line from p1 to p2 and add it to the lines list. + */ +void GrDrag::addLine(SPItem *item, Geom::Point p1, Geom::Point p2, Inkscape::PaintTarget fill_or_stroke) +{ + CtrlLineType type = (fill_or_stroke == Inkscape::FOR_FILL) ? CTLINE_PRIMARY : CTLINE_SECONDARY; + SPCtrlLine *line = ControlManager::getManager().createControlLine(this->desktop->getControls(), p1, p2, type); + + sp_canvas_item_move_to_z(line, 0); + line->item = item; + line->is_fill = (fill_or_stroke == Inkscape::FOR_FILL); + sp_canvas_item_show(line); + this->lines.push_back(line); +} + + + +/** + * Create a curve from p0 to p3 and add it to the lines list. Used for mesh sides. + */ +void GrDrag::addCurve(SPItem *item, Geom::Point p0, Geom::Point p1, Geom::Point p2, Geom::Point p3, + int corner0, int corner1, int handle0, int handle1, Inkscape::PaintTarget fill_or_stroke) + +{ + // Highlight curve if one of its draggers has a mouse over it. + bool highlight = false; + GrDragger* dragger0 = getDraggerFor(item, POINT_MG_CORNER, corner0, fill_or_stroke); + GrDragger* dragger1 = getDraggerFor(item, POINT_MG_CORNER, corner1, fill_or_stroke); + GrDragger* dragger2 = getDraggerFor(item, POINT_MG_HANDLE, handle0, fill_or_stroke); + GrDragger* dragger3 = getDraggerFor(item, POINT_MG_HANDLE, handle1, fill_or_stroke); + if ((dragger0->knot && (dragger0->knot->flags & SP_KNOT_MOUSEOVER)) || + (dragger1->knot && (dragger1->knot->flags & SP_KNOT_MOUSEOVER)) || + (dragger2->knot && (dragger2->knot->flags & SP_KNOT_MOUSEOVER)) || + (dragger3->knot && (dragger3->knot->flags & SP_KNOT_MOUSEOVER)) ) { + highlight = true; + } + + // CtrlLineType only sets color + CtrlLineType type = (fill_or_stroke == Inkscape::FOR_FILL) ? CTLINE_PRIMARY : CTLINE_SECONDARY; + if (highlight) { + type = (fill_or_stroke == Inkscape::FOR_FILL) ? CTLINE_SECONDARY : CTLINE_PRIMARY; + } + SPCtrlCurve *line = ControlManager::getManager().createControlCurve(this->desktop->getControls(), p0, p1, p2, p3, type); + line->corner0 = corner0; + line->corner1 = corner1; + + sp_canvas_item_move_to_z(line, 0); + line->item = item; + line->is_fill = (fill_or_stroke == Inkscape::FOR_FILL); + sp_canvas_item_show (line); + this->lines.push_back(line); +} + + +/** + * If there already exists a dragger within MERGE_DIST of p, add the draggable to it; otherwise create + * new dragger and add it to draggers list. + */ +GrDragger* GrDrag::addDragger(GrDraggable *draggable) +{ + Geom::Point p = getGradientCoords(draggable->item, draggable->point_type, draggable->point_i, draggable->fill_or_stroke); + + for (auto dragger : this->draggers) { + if (dragger->mayMerge (draggable) && Geom::L2 (dragger->point - p) < MERGE_DIST) { + // distance is small, merge this draggable into dragger, no need to create new dragger + dragger->addDraggable (draggable); + dragger->updateKnotShape(); + return dragger; + } + } + + GrDragger *new_dragger = new GrDragger(this, p, draggable); + // fixme: draggers should be added AFTER the last one: this way tabbing through them will be from begin to end. + this->draggers.push_back(new_dragger); + return new_dragger; +} + +/** + * Add draggers for the radial gradient rg on item. + */ +void GrDrag::addDraggersRadial(SPRadialGradient *rg, SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + rg->ensureVector(); + addDragger (new GrDraggable (item, POINT_RG_CENTER, 0, fill_or_stroke)); + guint num = rg->vector.stops.size(); + if (num > 2) { + for ( guint i = 1; i < num - 1; i++ ) { + addDragger (new GrDraggable (item, POINT_RG_MID1, i, fill_or_stroke)); + } + } + addDragger (new GrDraggable (item, POINT_RG_R1, num-1, fill_or_stroke)); + if (num > 2) { + for ( guint i = 1; i < num - 1; i++ ) { + addDragger (new GrDraggable (item, POINT_RG_MID2, i, fill_or_stroke)); + } + } + addDragger (new GrDraggable (item, POINT_RG_R2, num - 1, fill_or_stroke)); + addDragger (new GrDraggable (item, POINT_RG_FOCUS, 0, fill_or_stroke)); +} + +/** + * Add draggers for the linear gradient lg on item. + */ +void GrDrag::addDraggersLinear(SPLinearGradient *lg, SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + lg->ensureVector(); + addDragger(new GrDraggable (item, POINT_LG_BEGIN, 0, fill_or_stroke)); + guint num = lg->vector.stops.size(); + if (num > 2) { + for ( guint i = 1; i < num - 1; i++ ) { + addDragger(new GrDraggable (item, POINT_LG_MID, i, fill_or_stroke)); + } + } + addDragger(new GrDraggable (item, POINT_LG_END, num - 1, fill_or_stroke)); +} + +/** + *Add draggers for the mesh gradient mg on item + */ +void GrDrag::addDraggersMesh(SPMeshGradient *mg, SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + mg->ensureArray(); + std::vector< std::vector< SPMeshNode* > > nodes = mg->array.nodes; + + // Show/hide mesh on fill/stroke. This doesn't work at the moment... and prevents node color updating. + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_handles = (prefs->getBool("/tools/mesh/show_handles", true)); + bool edit_fill = (prefs->getBool("/tools/mesh/edit_fill", true)); + bool edit_stroke = (prefs->getBool("/tools/mesh/edit_stroke", true)); + + // Make sure we have at least one patch defined. + if( mg->array.patch_rows() == 0 || mg->array.patch_columns() == 0 ) { + + std::cerr << "Empty Mesh, No Draggers to Add" << std::endl; + return; + } + + guint icorner = 0; + guint ihandle = 0; + guint itensor = 0; + mg->array.corners.clear(); + mg->array.handles.clear(); + mg->array.tensors.clear(); + + if( (fill_or_stroke == Inkscape::FOR_FILL && !edit_fill) || + (fill_or_stroke == Inkscape::FOR_STROKE && !edit_stroke) ) { + return; + } + + for(auto & node : nodes) { + for(auto & j : node) { + + // std::cout << " Draggers: " << i << " " << j << " " << nodes[i][j]->node_type << std::endl; + switch ( j->node_type ) { + + case MG_NODE_TYPE_CORNER: + { + mg->array.corners.push_back( j ); + GrDraggable *corner = new GrDraggable (item, POINT_MG_CORNER, icorner, fill_or_stroke); + addDragger ( corner ); + j->draggable = icorner; + ++icorner; + break; + } + + case MG_NODE_TYPE_HANDLE: + { + mg->array.handles.push_back( j ); + GrDraggable *handle = new GrDraggable (item, POINT_MG_HANDLE, ihandle, fill_or_stroke); + GrDragger* dragger = addDragger ( handle ); + + if( !show_handles || !j->set ) { + dragger->knot->hide(); + } + j->draggable = ihandle; + ++ihandle; + break; + } + + case MG_NODE_TYPE_TENSOR: + { + mg->array.tensors.push_back( j ); + GrDraggable *tensor = new GrDraggable (item, POINT_MG_TENSOR, itensor, fill_or_stroke); + GrDragger* dragger = addDragger ( tensor ); + if( !show_handles || !j->set ) { + dragger->knot->hide(); + } + j->draggable = itensor; + ++itensor; + break; + } + + default: + std::cerr << "Bad Mesh draggable type" << std::endl; + break; + } + } + } + + mg->array.draggers_valid = true; +} + +/** + * Refresh draggers, moving and toggling visibility as necessary. + * Does not regenerate draggers (as does updateDraggersMesh()). + */ +void GrDrag::refreshDraggersMesh(SPMeshGradient *mg, SPItem *item, Inkscape::PaintTarget fill_or_stroke) +{ + mg->ensureArray(); + std::vector< std::vector< SPMeshNode* > > nodes = mg->array.nodes; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_handles = (prefs->getBool("/tools/mesh/show_handles", true)); + + // Make sure we have at least one patch defined. + if( mg->array.patch_rows() == 0 || mg->array.patch_columns() == 0 ) { + + std::cerr << "GrDrag::refreshDraggersMesh: Empty Mesh, No Draggers to refresh!" << std::endl; + return; + } + + guint ihandle = 0; + guint itensor = 0; + + for(auto & node : nodes) { + for(auto & j : node) { + + // std::cout << " Draggers: " << i << " " << j << " " << nodes[i][j]->node_type << std::endl; + + switch ( j->node_type ) { + + case MG_NODE_TYPE_CORNER: + // Do nothing, corners are always shown. + break; + + case MG_NODE_TYPE_HANDLE: + { + GrDragger* dragger = getDraggerFor(item, POINT_MG_HANDLE, ihandle, fill_or_stroke); + if (dragger) { + Geom::Point pk = getGradientCoords( item, POINT_MG_HANDLE, ihandle, fill_or_stroke); + dragger->knot->moveto(pk); + if( !show_handles || !j->set ) { + dragger->knot->hide(); + } else { + dragger->knot->show(); + } + } else { + // This can happen if a draggable is not visible. + // std::cerr << "GrDrag::refreshDraggersMesh: No dragger!" << std::endl; + } + ++ihandle; + break; + } + + case MG_NODE_TYPE_TENSOR: + { + GrDragger* dragger = getDraggerFor(item, POINT_MG_TENSOR, itensor, fill_or_stroke); + if (dragger) { + Geom::Point pk = getGradientCoords( item, POINT_MG_TENSOR, itensor, fill_or_stroke); + dragger->knot->moveto(pk); + if( !show_handles || !j->set ) { + dragger->knot->hide(); + } else { + dragger->knot->show(); + } + } else { + // This can happen if a draggable is not visible. + // std::cerr << "GrDrag::refreshDraggersMesh: No dragger!" << std::endl; + } + ++itensor; + break; + } + + default: + std::cerr << "Bad Mesh draggable type" << std::endl; + break; + } + } + } +} + +/** + * Artificially grab the knot of this dragger; used by the gradient context. + * Not used at the moment. + */ +void GrDrag::grabKnot(GrDragger *dragger, gint x, gint y, guint32 etime) +{ + if (dragger) { + dragger->knot->startDragging(dragger->point, x, y, etime); + } +} + +/** + * Artificially grab the knot of the dragger with this draggable; used by the gradient context. + * This allows setting the final point from the end of the drag when creating a new gradient. + */ +void GrDrag::grabKnot(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, gint x, gint y, guint32 etime) +{ + GrDragger *dragger = getDraggerFor(item, point_type, point_i, fill_or_stroke); + if (dragger) { + dragger->knot->startDragging(dragger->point, x, y, etime); + } +} + +/** + * Regenerates the draggers list from the current selection; is called when selection is changed or + * modified, also when a radial dragger needs to update positions of other draggers in the gradient. + */ +void GrDrag::updateDraggers() +{ + selected.clear(); + // delete old draggers + for (auto dragger : this->draggers) { + delete dragger; + } + this->draggers.clear(); + + g_return_if_fail(this->selection != nullptr); + auto list = this->selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + SPItem *item = *i; + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = style->getFillPaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( server->isSolid() + || (SP_GRADIENT(server)->getVector() && SP_GRADIENT(server)->getVector()->isSolid())) { + // Suppress "gradientness" of solid paint + } else if ( SP_IS_LINEARGRADIENT(server) ) { + addDraggersLinear( SP_LINEARGRADIENT(server), item, Inkscape::FOR_FILL ); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + addDraggersRadial( SP_RADIALGRADIENT(server), item, Inkscape::FOR_FILL ); + } else if ( SP_IS_MESHGRADIENT(server) ) { + addDraggersMesh( SP_MESHGRADIENT(server), item, Inkscape::FOR_FILL ); + } + } + } + + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = style->getStrokePaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( server->isSolid() + || (SP_GRADIENT(server)->getVector() && SP_GRADIENT(server)->getVector()->isSolid())) { + // Suppress "gradientness" of solid paint + } else if ( SP_IS_LINEARGRADIENT(server) ) { + addDraggersLinear( SP_LINEARGRADIENT(server), item, Inkscape::FOR_STROKE ); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + addDraggersRadial( SP_RADIALGRADIENT(server), item, Inkscape::FOR_STROKE ); + } else if ( SP_IS_MESHGRADIENT(server) ) { + addDraggersMesh( SP_MESHGRADIENT(server), item, Inkscape::FOR_STROKE ); + } + } + } + } +} + + +/** + * Refresh draggers, moving and toggling visibility as necessary. + * Does not regenerate draggers (as does updateDraggers()). + * Only applies to mesh gradients. + */ +void GrDrag::refreshDraggers() +{ + + g_return_if_fail(this->selection != nullptr); + auto list = this->selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + SPItem *item = *i; + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = style->getFillPaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( SP_IS_MESHGRADIENT(server) ) { + refreshDraggersMesh( SP_MESHGRADIENT(server), item, Inkscape::FOR_FILL ); + } + } + } + + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = style->getStrokePaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( SP_IS_MESHGRADIENT(server) ) { + refreshDraggersMesh( SP_MESHGRADIENT(server), item, Inkscape::FOR_STROKE ); + } + } + } + } +} + + +/** + * Returns true if at least one of the draggers' knots has the mouse hovering above it. + */ +bool GrDrag::mouseOver() +{ + static bool mouse_out = false; + // added knot mouse out for future use + for (auto d : this->draggers) { + if (d->knot && (d->knot->flags & SP_KNOT_MOUSEOVER)) { + mouse_out = true; + updateLines(); + return true; + } + } + if(mouse_out == true){ + updateLines(); + mouse_out = false; + } + return false; +} + +/** + * Regenerates the lines list from the current selection; is called on each move of a dragger, so that + * lines are always in sync with the actual gradient. + */ +void GrDrag::updateLines() +{ + // delete old lines + for (std::vector::const_iterator i = this->lines.begin(); i != this->lines.end(); ++i) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(*i)); + } + this->lines.clear(); + + g_return_if_fail(this->selection != nullptr); + + auto list = this->selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + SPItem *item = *i; + + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( server->isSolid() + || (SP_GRADIENT(server)->getVector() && SP_GRADIENT(server)->getVector()->isSolid())) { + // Suppress "gradientness" of solid paint + } else if ( SP_IS_LINEARGRADIENT(server) ) { + addLine(item, getGradientCoords(item, POINT_LG_BEGIN, 0, Inkscape::FOR_FILL), getGradientCoords(item, POINT_LG_END, 0, Inkscape::FOR_FILL), Inkscape::FOR_FILL); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + Geom::Point center = getGradientCoords(item, POINT_RG_CENTER, 0, Inkscape::FOR_FILL); + addLine(item, center, getGradientCoords(item, POINT_RG_R1, 0, Inkscape::FOR_FILL), Inkscape::FOR_FILL); + addLine(item, center, getGradientCoords(item, POINT_RG_R2, 0, Inkscape::FOR_FILL), Inkscape::FOR_FILL); + } else if ( SP_IS_MESHGRADIENT(server) ) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool edit_fill = (prefs->getBool("/tools/mesh/edit_fill", true)); + + SPMeshGradient *mg = SP_MESHGRADIENT(server); + + if (edit_fill) { + guint rows = mg->array.patch_rows(); + guint columns = mg->array.patch_columns(); + for ( guint i = 0; i < rows; ++i ) { + for ( guint j = 0; j < columns; ++j ) { + + std::vector h; + + SPMeshPatchI patch( &(mg->array.nodes), i, j ); + + // clockwise around patch, used to find corner dragger + int corner0 = i * (columns + 1) + j; + int corner1 = corner0 + 1; + int corner2 = corner1 + columns + 1; + int corner3 = corner2 - 1; + // clockwise around patch, used to find handle dragger + int handle0 = 2*j + i*(2+4*columns); + int handle1 = handle0 + 1; + int handle2 = j + i*(2+4*columns) + 2*columns + 1; + int handle3 = j + i*(2+4*columns) + 3*columns + 2; + int handle4 = handle1 + (2+4*columns); + int handle5 = handle0 + (2+4*columns); + int handle6 = handle3 - 1; + int handle7 = handle2 - 1; + + // Top line + h = patch.getPointsForSide( 0 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner0, corner1, handle0, handle1, Inkscape::FOR_FILL ); + + // Right line + if( j == columns - 1 ) { + h = patch.getPointsForSide( 1 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner1, corner2, handle2, handle3, Inkscape::FOR_FILL ); + } + + // Bottom line + if( i == rows - 1 ) { + h = patch.getPointsForSide( 2 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner2, corner3, handle4, handle5, Inkscape::FOR_FILL ); + } + + // Left line + h = patch.getPointsForSide( 3 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner3, corner0, handle6, handle7, Inkscape::FOR_FILL ); + } + } + } + } + } + } + + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( server && SP_IS_GRADIENT( server ) ) { + if ( server->isSolid() + || (SP_GRADIENT(server)->getVector() && SP_GRADIENT(server)->getVector()->isSolid())) { + // Suppress "gradientness" of solid paint + } else if ( SP_IS_LINEARGRADIENT(server) ) { + addLine(item, getGradientCoords(item, POINT_LG_BEGIN, 0, Inkscape::FOR_STROKE), getGradientCoords(item, POINT_LG_END, 0, Inkscape::FOR_STROKE), Inkscape::FOR_STROKE); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + Geom::Point center = getGradientCoords(item, POINT_RG_CENTER, 0, Inkscape::FOR_STROKE); + addLine(item, center, getGradientCoords(item, POINT_RG_R1, 0, Inkscape::FOR_STROKE), Inkscape::FOR_STROKE); + addLine(item, center, getGradientCoords(item, POINT_RG_R2, 0, Inkscape::FOR_STROKE), Inkscape::FOR_STROKE); + } else if ( SP_IS_MESHGRADIENT(server) ) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool edit_stroke = (prefs->getBool("/tools/mesh/edit_stroke", true)); + + if (edit_stroke) { + + // MESH FIXME: TURN ROUTINE INTO FUNCTION AND CALL FOR BOTH FILL AND STROKE. + SPMeshGradient *mg = SP_MESHGRADIENT(server); + + guint rows = mg->array.patch_rows(); + guint columns = mg->array.patch_columns(); + for ( guint i = 0; i < rows; ++i ) { + for ( guint j = 0; j < columns; ++j ) { + + std::vector h; + + SPMeshPatchI patch( &(mg->array.nodes), i, j ); + + // clockwise around patch, used to find corner dragger + int corner0 = i * (columns + 1) + j; + int corner1 = corner0 + 1; + int corner2 = corner1 + columns + 1; + int corner3 = corner2 - 1; + // clockwise around patch, used to find handle dragger + int handle0 = 2*j + i*(2+4*columns); + int handle1 = handle0 + 1; + int handle2 = j + i*(2+4*columns) + 2*columns + 1; + int handle3 = j + i*(2+4*columns) + 3*columns + 2; + int handle4 = handle1 + (2+4*columns); + int handle5 = handle0 + (2+4*columns); + int handle6 = handle3 - 1; + int handle7 = handle2 - 1; + + // Top line + h = patch.getPointsForSide( 0 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner0, corner1, handle0, handle1, Inkscape::FOR_STROKE); + + // Right line + if( j == columns - 1 ) { + h = patch.getPointsForSide( 1 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner1, corner2, handle2, handle3, Inkscape::FOR_STROKE); + } + + // Bottom line + if( i == rows - 1 ) { + h = patch.getPointsForSide( 2 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner2, corner3, handle4, handle5, Inkscape::FOR_STROKE); + } + + // Left line + h = patch.getPointsForSide( 3 ); + for( guint p = 0; p < 4; ++p ) { + h[p] *= Geom::Affine(mg->gradientTransform) * (Geom::Affine)item->i2dt_affine(); + } + addCurve (item, h[0], h[1], h[2], h[3], corner3, corner0, handle6, handle7,Inkscape::FOR_STROKE); + } + } + } + } + } + } + } +} + +/** + * Regenerates the levels list from the current selection. + * Levels correspond to bounding box edges and midpoints. + */ +void GrDrag::updateLevels() +{ + hor_levels.clear(); + vert_levels.clear(); + + g_return_if_fail (this->selection != nullptr); + + auto list = this->selection->items(); + for (auto i = list.begin(); i != list.end(); ++i) { + SPItem *item = *i; + Geom::OptRect rect = item->desktopVisualBounds(); + if (rect) { + // Remember the edges of the bbox and the center axis + hor_levels.push_back(rect->min()[Geom::Y]); + hor_levels.push_back(rect->max()[Geom::Y]); + hor_levels.push_back(rect->midpoint()[Geom::Y]); + vert_levels.push_back(rect->min()[Geom::X]); + vert_levels.push_back(rect->max()[Geom::X]); + vert_levels.push_back(rect->midpoint()[Geom::X]); + } + } +} + +void GrDrag::selected_reverse_vector() +{ + if (selected.empty()) + return; + + for(auto draggable : (*(selected.begin()))->draggables) { + sp_item_gradient_reverse_vector (draggable->item, draggable->fill_or_stroke); + } +} + +void GrDrag::selected_move_nowrite(double x, double y, bool scale_radial) +{ + selected_move (x, y, false, scale_radial); +} + +void GrDrag::selected_move(double x, double y, bool write_repr, bool scale_radial) +{ + if (selected.empty()) + return; + + bool did = false; + + for(auto d : selected) { + if (!d->isA(POINT_LG_MID) && !d->isA(POINT_RG_MID1) && !d->isA(POINT_RG_MID2)) { + // if this is an endpoint, + + // Moving an rg center moves its focus and radii as well. + // therefore, if this is a focus or radius and if selection + // contains the center as well, do not move this one + if (d->isA(POINT_RG_R1) || d->isA(POINT_RG_R2) || + (d->isA(POINT_RG_FOCUS) && !d->isA(POINT_RG_CENTER))) { + bool skip_radius_with_center = false; + for(auto d_new : selected) { + if (d_new->isA (( d->draggables[0])->item, + POINT_RG_CENTER, + 0, + (d->draggables[0])->fill_or_stroke)) { + // FIXME: here we take into account only the first draggable! + skip_radius_with_center = true; + } + } + if (skip_radius_with_center) + continue; + } + + did = true; + Geom::Point p_old = d->point; + d->point += Geom::Point (x, y); + d->point_original = d->point; + d->knot->moveto(d->point); + + d->fireDraggables (write_repr, scale_radial); + d->moveMeshHandles( p_old, MG_NODE_NO_SCALE ); + d->updateDependencies(write_repr); + } + } + + if (write_repr && did) { + // we did an undoable action + DocumentUndo::maybeDone(desktop->getDocument(), "grmoveh", SP_VERB_CONTEXT_GRADIENT, + _("Move gradient handle(s)")); + return; + } + + if (!did) { // none of the end draggers are selected, so let's try to move the mids + + GrDragger *dragger = *(selected.begin()); + // a midpoint dragger can (logically) only contain one GrDraggable + GrDraggable *draggable = dragger->draggables[0]; + + Geom::Point begin(0,0), end(0,0); + Geom::Point low_lim(0,0), high_lim(0,0); + + SPObject *server = draggable->getServer(); + std::vector moving; + gr_midpoint_limits(dragger, server, &begin, &end, &low_lim, &high_lim, moving); + + Geom::LineSegment ls(low_lim, high_lim); + Geom::Point p = ls.pointAt(ls.nearestTime(dragger->point + Geom::Point(x,y))); + Geom::Point displacement = p - dragger->point; + + for(auto drg : moving) { + SPKnot *drgknot = drg->knot; + drg->point += displacement; + drgknot->moveto(drg->point); + drg->fireDraggables (true); + drg->updateDependencies(true); + did = true; + } + + if (write_repr && did) { + // we did an undoable action + DocumentUndo::maybeDone(desktop->getDocument(), "grmovem", SP_VERB_CONTEXT_GRADIENT, + _("Move gradient mid stop(s)")); + } + } +} + +void GrDrag::selected_move_screen(double x, double y) +{ + gdouble zoom = desktop->current_zoom(); + gdouble zx = x / zoom; + gdouble zy = y / zoom; + + selected_move (zx, zy); +} + +/** + * Select the knot next to the last selected one and deselect all other selected. + */ +GrDragger *GrDrag::select_next() +{ + GrDragger *d = nullptr; + if (selected.empty() || (++find(draggers.begin(),draggers.end(),*(selected.begin())))==draggers.end()) { + if (!draggers.empty()) + d = draggers[0]; + } else { + d = *(++find(draggers.begin(),draggers.end(),*(selected.begin()))); + } + if (d) + setSelected (d); + return d; +} + +/** + * Select the knot previous from the last selected one and deselect all other selected. + */ +GrDragger *GrDrag::select_prev() +{ + GrDragger *d = nullptr; + if (selected.empty() || draggers[0] == (*(selected.begin()))) { + if (!draggers.empty()) + d = draggers[draggers.size()-1]; + } else { + d = *(--find(draggers.begin(),draggers.end(),*(selected.begin()))); + } + if (d) + setSelected (d); + return d; +} + + +// FIXME: i.m.o. an ugly function that I just made to work, but... aargh! (Johan) +void GrDrag::deleteSelected(bool just_one) +{ + if (selected.empty()) return; + + SPDocument *document = nullptr; + + struct StructStopInfo { + SPStop * spstop; + GrDraggable * draggable; + SPGradient * gradient; + SPGradient * vector; + }; + + std::vector midstoplist;// list of stops that must be deleted (will be deleted first) + std::vector endstoplist;// list of stops that must be deleted + + while (!selected.empty()) { + GrDragger *dragger = *(selected.begin()); + for(auto draggable : dragger->draggables) { + SPGradient *gradient = getGradient(draggable->item, draggable->fill_or_stroke); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + + switch (draggable->point_type) { + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + { + SPStop *stop = sp_get_stop_i(vector, draggable->point_i); + // check if already present in list. (e.g. when both RG_MID1 and RG_MID2 were selected) + bool present = false; + for (auto i:midstoplist) { + if ( i == stop ) { + present = true; + break; // no need to search further. + } + } + if (!present) + midstoplist.push_back(stop); + } + break; + case POINT_LG_BEGIN: + case POINT_LG_END: + case POINT_RG_CENTER: + case POINT_RG_R1: + case POINT_RG_R2: + { + SPStop *stop = nullptr; + if ( (draggable->point_type == POINT_LG_BEGIN) || (draggable->point_type == POINT_RG_CENTER) ) { + stop = vector->getFirstStop(); + } else { + stop = sp_last_stop(vector); + } + if (stop) { + StructStopInfo *stopinfo = new StructStopInfo; + stopinfo->spstop = stop; + stopinfo->draggable = draggable; + stopinfo->gradient = gradient; + stopinfo->vector = vector; + // check if already present in list. (e.g. when both R1 and R2 were selected) + bool present = false; + for (auto i : endstoplist) { + if ( i->spstop == stopinfo->spstop ) { + present = true; + break; // no need to search further. + } + } + if (!present) + endstoplist.push_back(stopinfo); + else + delete stopinfo; + } + } + break; + + default: + break; + } + } + selected.erase(dragger); + if ( just_one ) break; // iterate once if just_one is set. + } + for (auto stop:midstoplist) { + document = stop->document; + Inkscape::XML::Node * parent = stop->getRepr()->parent(); + parent->removeChild(stop->getRepr()); + } + + for (auto stopinfo:endstoplist) { + document = stopinfo->spstop->document; + + // 2 is the minimum, cannot delete more than that without deleting the whole vector + // cannot use vector->vector.stops.size() because the vector might be invalidated by deletion of a midstop + // manually count the children, don't know if there already exists a function for this... + int len = 0; + for (auto& child: stopinfo->vector->children) + { + if ( SP_IS_STOP(&child) ) { + len ++; + } + } + if (len > 2) + { + switch (stopinfo->draggable->point_type) { + case POINT_LG_BEGIN: + { + stopinfo->vector->getRepr()->removeChild(stopinfo->spstop->getRepr()); + + SPLinearGradient *lg = SP_LINEARGRADIENT(stopinfo->gradient); + Geom::Point oldbegin = Geom::Point (lg->x1.computed, lg->y1.computed); + Geom::Point end = Geom::Point (lg->x2.computed, lg->y2.computed); + SPStop *stop = stopinfo->vector->getFirstStop(); + gdouble offset = stop->offset; + Geom::Point newbegin = oldbegin + offset * (end - oldbegin); + lg->x1.computed = newbegin[Geom::X]; + lg->y1.computed = newbegin[Geom::Y]; + + Inkscape::XML::Node *repr = stopinfo->gradient->getRepr(); + sp_repr_set_svg_double(repr, "x1", lg->x1.computed); + sp_repr_set_svg_double(repr, "y1", lg->y1.computed); + stop->offset = 0; + sp_repr_set_css_double(stop->getRepr(), "offset", 0); + + // iterate through midstops to set new offset values such that they won't move on canvas. + SPStop *laststop = sp_last_stop(stopinfo->vector); + stop = stop->getNextStop(); + while ( stop != laststop ) { + stop->offset = (stop->offset - offset)/(1 - offset); + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + stop = stop->getNextStop(); + } + } + break; + case POINT_LG_END: + { + stopinfo->vector->getRepr()->removeChild(stopinfo->spstop->getRepr()); + + SPLinearGradient *lg = SP_LINEARGRADIENT(stopinfo->gradient); + Geom::Point begin = Geom::Point (lg->x1.computed, lg->y1.computed); + Geom::Point oldend = Geom::Point (lg->x2.computed, lg->y2.computed); + SPStop *laststop = sp_last_stop(stopinfo->vector); + gdouble offset = laststop->offset; + Geom::Point newend = begin + offset * (oldend - begin); + lg->x2.computed = newend[Geom::X]; + lg->y2.computed = newend[Geom::Y]; + + Inkscape::XML::Node *repr = stopinfo->gradient->getRepr(); + sp_repr_set_svg_double(repr, "x2", lg->x2.computed); + sp_repr_set_svg_double(repr, "y2", lg->y2.computed); + laststop->offset = 1; + sp_repr_set_css_double(laststop->getRepr(), "offset", 1); + + // iterate through midstops to set new offset values such that they won't move on canvas. + SPStop *stop = stopinfo->vector->getFirstStop(); + stop = stop->getNextStop(); + while ( stop != laststop ) { + stop->offset = stop->offset / offset; + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + stop = stop->getNextStop(); + } + } + break; + case POINT_RG_CENTER: + { + SPStop *newfirst = stopinfo->spstop->getNextStop(); + if (newfirst) { + newfirst->offset = 0; + sp_repr_set_css_double(newfirst->getRepr(), "offset", 0); + } + stopinfo->vector->getRepr()->removeChild(stopinfo->spstop->getRepr()); + } + break; + case POINT_RG_R1: + case POINT_RG_R2: + { + stopinfo->vector->getRepr()->removeChild(stopinfo->spstop->getRepr()); + + SPRadialGradient *rg = SP_RADIALGRADIENT(stopinfo->gradient); + double oldradius = rg->r.computed; + SPStop *laststop = sp_last_stop(stopinfo->vector); + gdouble offset = laststop->offset; + double newradius = offset * oldradius; + rg->r.computed = newradius; + + Inkscape::XML::Node *repr = rg->getRepr(); + sp_repr_set_svg_double(repr, "r", rg->r.computed); + laststop->offset = 1; + sp_repr_set_css_double(laststop->getRepr(), "offset", 1); + + // iterate through midstops to set new offset values such that they won't move on canvas. + SPStop *stop = stopinfo->vector->getFirstStop(); + stop = stop->getNextStop(); + while ( stop != laststop ) { + stop->offset = stop->offset / offset; + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + stop = stop->getNextStop(); + } + } + break; + default: + break; + } + } + else + { // delete the gradient from the object. set fill to unset FIXME: set to fill of unselected node? + SPCSSAttr *css = sp_repr_css_attr_new (); + + // stopinfo->spstop is the selected stop + Inkscape::XML::Node *unselectedrepr = stopinfo->vector->getRepr()->firstChild(); + if (unselectedrepr == stopinfo->spstop->getRepr() ) { + unselectedrepr = unselectedrepr->next(); + } + + if (unselectedrepr == nullptr) { + if (stopinfo->draggable->fill_or_stroke == Inkscape::FOR_FILL) { + sp_repr_css_unset_property (css, "fill"); + } else { + sp_repr_css_unset_property (css, "stroke"); + } + } else { + SPCSSAttr *stopcss = sp_repr_css_attr(unselectedrepr, "style"); + if (stopinfo->draggable->fill_or_stroke == Inkscape::FOR_FILL) { + sp_repr_css_set_property(css, "fill", sp_repr_css_property(stopcss, "stop-color", "inkscape:unset")); + sp_repr_css_set_property(css, "fill-opacity", sp_repr_css_property(stopcss, "stop-opacity", "1")); + } else { + sp_repr_css_set_property(css, "stroke", sp_repr_css_property(stopcss, "stop-color", "inkscape:unset")); + sp_repr_css_set_property(css, "stroke-opacity", sp_repr_css_property(stopcss, "stop-opacity", "1")); + } + sp_repr_css_attr_unref (stopcss); + } + + sp_repr_css_change(stopinfo->draggable->item->getRepr(), css, "style"); + sp_repr_css_attr_unref (css); + } + + delete stopinfo; + } + + if (document) { + DocumentUndo::done( document, SP_VERB_CONTEXT_GRADIENT, _("Delete gradient stop(s)") ); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/gradient-drag.h b/src/gradient-drag.h new file mode 100644 index 0000000..7940b80 --- /dev/null +++ b/src/gradient-drag.h @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_DRAG_H +#define SEEN_GRADIENT_DRAG_H + +/* + * On-canvas gradient dragging + * + * Authors: + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Tavmjong Bah + * + * Copyright (C) 2012 Authors + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include + +#include <2geom/point.h> + +#include "object/sp-gradient.h" // TODO refactor enums to external .h file +#include "object/sp-mesh-array.h" + +class SPKnot; + +class SPDesktop; +class SPCSSAttr; +class SPLinearGradient; +class SPMeshGradient; +class SPItem; +class SPObject; +class SPRadialGradient; +class SPStop; + +namespace Inkscape { +class Selection; +} // namespace Inkscape + +/** +This class represents a single draggable point of a gradient. It remembers the item +which has the gradient, whether it's fill or stroke, the point type (from the +GrPointType enum), and the point number (needed if more than 2 stops are present). +*/ +struct GrDraggable { + GrDraggable(SPItem *item, GrPointType point_type, guint point_i, Inkscape::PaintTarget fill_or_stroke); + virtual ~GrDraggable(); + + SPItem *item; + GrPointType point_type; + gint point_i; // the stop number of this point ( = 0 POINT_LG_BEGIN and POINT_RG_CENTER) + Inkscape::PaintTarget fill_or_stroke; + + SPObject *getServer(); + + bool mayMerge(GrDraggable *da2); + + inline int equals(GrDraggable *other) { + return ((item == other->item) && (point_type == other->point_type) && (point_i == other->point_i) && (fill_or_stroke == other->fill_or_stroke)); + } +}; + +class GrDrag; + +/** +This class holds together a visible on-canvas knot and a list of draggables that need to +be moved when the knot moves. Normally there's one draggable in the list, but there may +be more when draggers are snapped together. +*/ +struct GrDragger { + GrDragger(GrDrag *parent, Geom::Point p, GrDraggable *draggable); + virtual ~GrDragger(); + + GrDrag *parent; + + SPKnot *knot; + + // position of the knot, desktop coords + Geom::Point point; + // position of the knot before it began to drag; updated when released + Geom::Point point_original; + + std::vector draggables; + + void addDraggable(GrDraggable *draggable); + + void updateKnotShape(); + void updateTip(); + + void select(); + void deselect(); + bool isSelected(); + + /* Given one GrDraggable, these all update other draggables belonging to same GrDragger */ + void moveThisToDraggable(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, bool write_repr); + void moveOtherToDraggable(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, bool write_repr); + void updateMidstopDependencies(GrDraggable *draggable, bool write_repr); + void updateDependencies(bool write_repr); + + /* Update handles/tensors when mesh corner moved */ + void moveMeshHandles( Geom::Point pc_old, MeshNodeOperation op ); + + /* Following are for highlighting mesh handles when corner node is selected. */ + GrDragger *getMgCorner(); + void highlightNode(SPMeshNode *node, bool highlight, Geom::Point corner_pos, int index); + void highlightCorner(bool highlight); + + bool mayMerge(GrDragger *other); + bool mayMerge(GrDraggable *da2); + + bool isA(GrPointType point_type); + bool isA(SPItem *item, GrPointType point_type, Inkscape::PaintTarget fill_or_stroke); + bool isA(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke); + + void fireDraggables(bool write_repr, bool scale_radial = false, bool merging_focus = false); + +protected: + void updateControlSizesOverload(SPKnot * knot); + void updateControlSizes(); + sigc::connection sizeUpdatedConn; + +private: + sigc::connection _moved_connection; + sigc::connection _clicked_connection; + sigc::connection _doubleclicked_connection; + sigc::connection _mousedown_connection; + sigc::connection _ungrabbed_connection; +}; + +struct SPCtrlLine; +/** +This is the root class of the gradient dragging machinery. It holds lists of GrDraggers +and of lines (simple canvas items). It also remembers one of the draggers as selected. +*/ +class GrDrag { +public: // FIXME: make more of this private! + + GrDrag(SPDesktop *desktop); + virtual ~GrDrag(); + + bool isNonEmpty() {return !draggers.empty();} + bool hasSelection() {return !selected.empty();} + guint numSelected() {return selected.size();} + guint numDraggers() {return draggers.size();} + + guint singleSelectedDraggerNumDraggables() { + return (selected.empty()? 0 : (*(selected.begin()))->draggables.size() ); + } + + guint singleSelectedDraggerSingleDraggableType() { + return (selected.empty() ? 0 : ((*(selected.begin()))->draggables[0]->point_type)); + } + + // especially the selection must be private, fix gradient-context to remove direct access to it + std::set selected; // list of GrDragger* + void setSelected(GrDragger *dragger, bool add_to_selection = false, bool override = true); + void setDeselected(GrDragger *dragger); + void deselectAll(); + void selectAll(); + void selectByCoords(std::vector coords); + void selectByStop(SPStop *stop, bool add_to_selection = true, bool override = true); + void selectRect(Geom::Rect const &r); + + bool dropColor(SPItem *item, gchar const *c, Geom::Point p); + + SPStop *addStopNearPoint(SPItem *item, Geom::Point mouse_p, double tolerance); + + void deleteSelected(bool just_one = false); + + guint32 getColor(); + + bool keep_selection; + + GrDragger *getDraggerFor(GrDraggable *d); + GrDragger *getDraggerFor(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke); + + void grabKnot(GrDragger *dragger, gint x, gint y, guint32 etime); + void grabKnot(SPItem *item, GrPointType point_type, gint point_i, Inkscape::PaintTarget fill_or_stroke, gint x, gint y, guint32 etime); + + bool local_change; + + SPDesktop *desktop; + + // lists of edges of selection bboxes, to snap draggers to + std::vector hor_levels; + std::vector vert_levels; + + std::vector draggers; + std::vector lines; + + void updateDraggers(); + void refreshDraggers(); + void updateLines(); + void updateLevels(); + + bool mouseOver(); + + void selected_move_nowrite(double x, double y, bool scale_radial); + void selected_move(double x, double y, bool write_repr = true, bool scale_radial = false); + void selected_move_screen(double x, double y); + + GrDragger *select_next(); + GrDragger *select_prev(); + + void selected_reverse_vector(); + +private: + void deselect_all(); + + void addLine( SPItem *item, Geom::Point p1, Geom::Point p2, Inkscape::PaintTarget fill_or_stroke); + void addCurve(SPItem *item, Geom::Point p0, Geom::Point p1, Geom::Point p2, Geom::Point p3, + int corner0, int corner1, int handle0, int handle1, Inkscape::PaintTarget fill_or_stroke); + + GrDragger *addDragger(GrDraggable *draggable); + + void addDraggersRadial(SPRadialGradient *rg, SPItem *item, Inkscape::PaintTarget fill_or_stroke); + void addDraggersLinear(SPLinearGradient *lg, SPItem *item, Inkscape::PaintTarget fill_or_stroke); + void addDraggersMesh( SPMeshGradient *mg, SPItem *item, Inkscape::PaintTarget fill_or_stroke); + void refreshDraggersMesh(SPMeshGradient *mg, SPItem *item, Inkscape::PaintTarget fill_or_stroke); + + bool styleSet( const SPCSSAttr *css ); + + Glib::ustring makeStopSafeColor( gchar const *str, bool &isNull ); + + Inkscape::Selection *selection; + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + + sigc::connection style_set_connection; + sigc::connection style_query_connection; +}; + +#endif // SEEN_GRADIENT_DRAG_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/graphlayout.cpp b/src/graphlayout.cpp new file mode 100644 index 0000000..82de08a --- /dev/null +++ b/src/graphlayout.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Interface between Inkscape code (SPItem) and graphlayout functions. + */ +/* + * Authors: + * Tim Dwyer + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include <2geom/transforms.h> + +#include "conn-avoid-ref.h" +#include "desktop.h" +#include "graphlayout.h" +#include "inkscape.h" + +#include "3rdparty/adaptagrams/libavoid/router.h" + +#include "3rdparty/adaptagrams/libcola/cola.h" +#include "3rdparty/adaptagrams/libcola/connected_components.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "style.h" + +using namespace cola; +using namespace vpsc; + +/** + * Returns true if item is a connector + */ +bool isConnector(SPItem const * const item) { + SPPath * path = nullptr; + if (SP_IS_PATH(item)) { + path = SP_PATH(item); + } + return path && path->connEndPair.isAutoRoutingConn(); +} + +struct CheckProgress: TestConvergence { + CheckProgress(double d, unsigned i, std::list & selected, Rectangles & rs, + std::map & nodelookup) + : TestConvergence(d, i) + , selected(selected) + , rs(rs) + , nodelookup(nodelookup) + {} + bool operator()(const double new_stress, std::valarray & X, std::valarray & Y) override { + /* This is where, if we wanted to animate the layout, we would need to update + * the positions of all objects and redraw the canvas and maybe sleep a bit + cout << "stress="<getDouble("/tools/connector/length", 100.0); + double directed_edge_height_modifier = 1.0; + + bool directed = prefs->getBool("/tools/connector/directedlayout"); + bool avoid_overlaps = prefs->getBool("/tools/connector/avoidoverlaplayout"); + + for (SPItem * item: selected) { + std::map::iterator i_iter=nodelookup.find(item->getId()); + if (i_iter == nodelookup.end()) continue; + unsigned u = i_iter->second; + std::vector nlist = item->getAvoidRef().getAttachedConnectors(Avoid::runningFrom); + std::list connectors; + + connectors.insert(connectors.end(), nlist.begin(), nlist.end()); + + for (SPItem * conn: connectors) { + SPItem * iv; + SPItem * items[2]; + assert(isConnector(conn)); + SP_PATH(conn)->connEndPair.getAttachedItems(items); + if (items[0] == item) { + iv = items[1]; + } else { + iv = items[0]; + } + + if (iv == nullptr) { + // The connector is not attached to anything at the + // other end so we should just ignore it. + continue; + } + + // If iv not in nodelookup we again treat the connector + // as disconnected and continue + std::map::iterator v_pair = nodelookup.find(iv->getId()); + if (v_pair != nodelookup.end()) { + unsigned v = v_pair->second; + //cout << "Edge: (" << u <<","<style->marker_end.set) { + if (directed && strcmp(conn->style->marker_end.value(), "none")) { + constraints.push_back(new SeparationConstraint(YDIM, v, u, + ideal_connector_length * directed_edge_height_modifier)); + } + } + } + } + } + EdgeLengths elengths(es.size(), 1); + std::vector cs; + connectedComponents(rs, es, cs); + for (Component * c: cs) { + if (c->edges.size() < 2) continue; + CheckProgress test(0.0001, 100, selected, rs, nodelookup); + ConstrainedMajorizationLayout alg(c->rects, c->edges, nullptr, ideal_connector_length, elengths, &test); + if (avoid_overlaps) alg.setAvoidOverlaps(); + alg.setConstraints(&constraints); + alg.run(); + } + separateComponents(cs); + + for (SPItem * item: selected) { + if (!isConnector(item)) { + std::map::iterator i = nodelookup.find(item->getId()); + if (i != nodelookup.end()) { + Rectangle * r = rs[i->second]; + Geom::OptRect item_box = item->desktopVisualBounds(); + if (item_box) { + Geom::Point const curr(item_box->midpoint()); + Geom::Point const dest(r->getCentreX(),r->getCentreY()); + item->move_rel(Geom::Translate(dest - curr)); + } + } + } + } + for (CompoundConstraint * c: constraints) { + delete c; + } + for (Rectangle * r: rs) { + delete r; + } +} +// vim: set cindent +// vim: ts=4 sw=4 et tw=0 wm=0 + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/graphlayout.h b/src/graphlayout.h new file mode 100644 index 0000000..b403b77 --- /dev/null +++ b/src/graphlayout.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * graph layout functions. + */ +/* + * Authors: + * Tim Dwyer + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GRAPHLAYOUT_H +#define SEEN_GRAPHLAYOUT_H + +#include + +class SPItem; + +void graphlayout(std::vector const &items); + +bool isConnector(SPItem const *const item); + +void filterConnectors(std::vector const &items, std::list &filtered); + +#endif // SEEN_GRAPHLAYOUT_H diff --git a/src/guide-snapper.cpp b/src/guide-snapper.cpp new file mode 100644 index 0000000..7278cd0 --- /dev/null +++ b/src/guide-snapper.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Snapping things to guides. + * + * Authors: + * Lauris Kaplinski + * Frank Felfe + * Carl Hetherington + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +Inkscape::GuideSnapper::GuideSnapper(SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d) +{ + +} + +/** + * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels + */ +Geom::Coord Inkscape::GuideSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getGuideTolerance() / zoom; +} + +bool Inkscape::GuideSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getGuideTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +Inkscape::GuideSnapper::LineList Inkscape::GuideSnapper::_getSnapLines(Geom::Point const &/*p*/) const +{ + LineList s; + + if ( nullptr == _snapmanager->getNamedView() || ThisSnapperMightSnap() == false) { + return s; + } + + SPGuide const *guide_to_ignore = _snapmanager->getGuideToIgnore(); + std::vector guides = _snapmanager->getNamedView()->guides; + for(auto guide : guides) { + if (guide != guide_to_ignore) { + s.push_back(std::pair(guide->getNormal(), guide->getPoint())); + } + } + + return s; +} + +/** + * \return true if this Snapper will snap at least one kind of point. + */ +bool Inkscape::GuideSnapper::ThisSnapperMightSnap() const +{ + if (_snapmanager->getNamedView() == nullptr) { + return false; + } + + return (_snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GUIDE) && _snapmanager->getNamedView()->showguides); +} + +void Inkscape::GuideSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const +{ + SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GUIDE, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line); + isr.guide_lines.push_back(dummy); +} + +void Inkscape::GuideSnapper::_addSnappedLinesOrigin(IntermSnapResults &isr, Geom::Point const &origin, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(origin, source, source_num, Inkscape::SNAPTARGET_GUIDE_ORIGIN, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void Inkscape::GuideSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GUIDE_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void Inkscape::GuideSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GUIDE, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/guide-snapper.h b/src/guide-snapper.h new file mode 100644 index 0000000..94863fa --- /dev/null +++ b/src/guide-snapper.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GUIDE_SNAPPER_H +#define SEEN_GUIDE_SNAPPER_H +/* + * Authors: + * Lauris Kaplinski + * Frank Felfe + * Carl Hetherington + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "line-snapper.h" + +class SPNamedView; + +namespace Inkscape +{ + +/** + * Snap to guides. + */ +class GuideSnapper : public LineSnapper +{ +public: + GuideSnapper(SnapManager *sm, Geom::Coord const d); + bool ThisSnapperMightSnap() const override; + + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + +private: + LineList _getSnapLines(Geom::Point const &p) const override; + void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const override; + void _addSnappedLinesOrigin(IntermSnapResults &isr, Geom::Point const &origin, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; +}; + +} + +#endif + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/help.cpp b/src/help.cpp new file mode 100644 index 0000000..479dbfc --- /dev/null +++ b/src/help.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Help/About window + * + * Authors: + * Lauris Kaplinski + * bulia byak + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2000-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "help.h" + +#include +#include +#include + +#include "inkscape-application.h" + +#include "include/gtkmm_version.h" +#include "io/resource.h" +#include "ui/interface.h" // sp_ui_error_dialog +#include "ui/dialog/aboutbox.h" + +void sp_help_about() +{ + Inkscape::UI::Dialog::AboutBox::show_about(); +} + +/** Open an URL in the the default application + * + * See documentation of gtk_show_uri_on_window() for details + * + * @param url URL to be opened + * @param window Parent window for which the URL is opened + */ +// TODO: Do we really need a window reference here? It's the way recommended by gtk, though. +void sp_help_open_url(const Glib::ustring &url, Gtk::Window *window) +{ +#if GTKMM_CHECK_VERSION(3,24,0) + try { + window->show_uri(url, GDK_CURRENT_TIME); + } catch (const Glib::Error &e) { + g_warning("Unable to show '%s': %s", url.c_str(), e.what().c_str()); + } +#else + GError *error = nullptr; + gtk_show_uri_on_window(window->gobj(), url.c_str(), GDK_CURRENT_TIME, &error); + if (error) { + g_warning("Unable to show '%s': %s", url.c_str(), error->message); + g_error_free(error); + } +#endif +} + +void sp_help_open_tutorial(Glib::ustring name) +{ + Glib::ustring filename = name + ".svg"; + + filename = Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::TUTORIALS, filename.c_str(), true); + if (!filename.empty()) { + Glib::RefPtr file = Gio::File::create_for_path(filename); + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + app->create_window(file, false, false); + } else { + // TRANSLATORS: Please don't translate link unless the page exists in your language. Add your language code to + // the link this way: https://inkscape.org/[lang]/learn/tutorials/ + sp_ui_error_dialog(_("The tutorial files are not installed.\nFor Linux, you may need to install " + "'inkscape-tutorials'; for Windows, please re-run the setup and select 'Tutorials'.\nThe " + "tutorials can also be found online at https://inkscape.org/en/learn/tutorials/")); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/help.h b/src/help.h new file mode 100644 index 0000000..568ef94 --- /dev/null +++ b/src/help.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2003 authors + * Copyright (C) 2000-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_HELP_H +#define SEEN_HELP_H + +namespace Glib { +class ustring; +} + +namespace Gtk { +class Window; +} + +/** + * Help/About window. + */ +void sp_help_about(); +void sp_help_open_url(const Glib::ustring &url, Gtk::Window *window); +void sp_help_open_tutorial(Glib::ustring name); + +#endif // !SEEN_HELP_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/helper-fns.h b/src/helper-fns.h new file mode 100644 index 0000000..27dfc73 --- /dev/null +++ b/src/helper-fns.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_HELPER_FNS_H +#define SEEN_HELPER_FNS_H +/* + * Authors: + * Felipe Corrêa da Silva Sanches + * + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +// calling helperfns_read_number(string, false), it's not obvious, what +// that false stands for. helperfns_read_number(string, HELPERFNS_NO_WARNING) +// can be more clear. +#define HELPERFNS_NO_WARNING false + +/* convert ascii representation to double + * the function can only be used to convert numbers as given by gui elements that use localized representation + * @param value ascii representation of the number + * @return the converted number + * + * Setting warning to false disables conversion error warnings from + * this function. This can be useful in places, where the input type + * is not known beforehand. For example, see sp_feColorMatrix_set in + * sp-fecolormatrix.cpp */ +inline double helperfns_read_number(gchar const *value, bool warning = true) { + if (!value) { + g_warning("Called helperfns_read_number with value==null_ptr, this can lead to unexpected behaviour."); + return 0; + } + char *end; + double ret = g_ascii_strtod(value, &end); + if (*end) { + if (warning) { + g_warning("helper-fns::helperfns_read_number() Unable to convert \"%s\" to number", value); + } + // We could leave this out, too. If strtod can't convert + // anything, it will return zero. + ret = 0; + } + return ret; +} + +inline bool helperfns_read_bool(gchar const *value, bool default_value){ + if (!value) return default_value; + switch(value[0]){ + case 't': + if (strncmp(value, "true", 4) == 0) return true; + break; + case 'f': + if (strncmp(value, "false", 5) == 0) return false; + break; + } + return default_value; +} + +/* convert ascii representation to double + * the function can only be used to convert numbers as given by gui elements that use localized representation + * numbers are delimited by space + * @param value ascii representation of the number + * @return the vector of the converted numbers + */ +inline std::vector helperfns_read_vector(const gchar* value){ + std::vector v; + + gchar const* beg = value; + while(isspace(*beg) || (*beg == ',')) beg++; + while(*beg) + { + char *end; + double ret = g_ascii_strtod(beg, &end); + if (end==beg){ + g_warning("helper-fns::helperfns_read_vector() Unable to convert \"%s\" to number", beg); + // We could leave this out, too. If strtod can't convert + // anything, it will return zero. + // ret = 0; + break; + } + v.push_back(ret); + + beg = end; + while(isspace(*beg) || (*beg == ',')) beg++; + } + return v; +} + +#endif /* !SEEN_HELPER_FNS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/CMakeLists.txt b/src/helper/CMakeLists.txt new file mode 100644 index 0000000..60e42ac --- /dev/null +++ b/src/helper/CMakeLists.txt @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include(${CMAKE_SOURCE_DIR}/CMakeScripts/UseGlibMarshal.cmake) + +GLIB_MARSHAL(sp_marshal sp-marshal "${CMAKE_CURRENT_BINARY_DIR}/helper") + +set(sp_marshal_SRC + ${CMAKE_CURRENT_BINARY_DIR}/sp-marshal.cpp + ${CMAKE_CURRENT_BINARY_DIR}/sp-marshal.h +) + +set(helper_SRC + action.cpp + action-context.cpp + geom.cpp + geom-nodetype.cpp + geom-pathstroke.cpp + geom-pathvectorsatellites.cpp + geom-satellite.cpp + gettext.cpp + pixbuf-ops.cpp + png-write.cpp + stock-items.cpp + verb-action.cpp + #units-test.cpp + + # we generate this file and it's .h counter-part + ${sp_marshal_SRC} + + + # ------- + # Headers + action.h + action-context.h + geom-curves.h + geom-nodetype.h + geom-pathstroke.h + geom-pathvectorsatellites.h + geom-satellite.h + geom.h + gettext.h + mathfns.h + pixbuf-ops.h + png-write.h + stock-items.h + verb-action.h +) + +set_source_files_properties(sp_marshal_SRC PROPERTIES GENERATED true) + +# add_inkscape_lib(helper_LIB "${helper_SRC}") +add_inkscape_source("${helper_SRC}") diff --git a/src/helper/README b/src/helper/README new file mode 100644 index 0000000..d9a6b90 --- /dev/null +++ b/src/helper/README @@ -0,0 +1,8 @@ + + +This directory contains a variety of helper code. + +To do: + +* Merge with 'util'. +* Move individual files to more appropriate directories. diff --git a/src/helper/action-context.cpp b/src/helper/action-context.cpp new file mode 100644 index 0000000..36b6914 --- /dev/null +++ b/src/helper/action-context.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ActionContext implementation. + * + * Author: + * Eric Greveson + * + * Copyright (C) 2013 Eric Greveson + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "document.h" +#include "layer-model.h" +#include "selection.h" +#include "helper/action-context.h" + +namespace Inkscape { + +ActionContext::ActionContext() + : _selection(nullptr) + , _view(nullptr) +{ +} + +ActionContext::ActionContext(Selection *selection) + : _selection(selection) + , _view(nullptr) +{ +} + +ActionContext::ActionContext(UI::View::View *view) + : _selection(nullptr) + , _view(view) +{ + SPDesktop *desktop = static_cast(view); + if (desktop) { + _selection = desktop->selection; + } +} + +SPDocument *ActionContext::getDocument() const +{ + if (_selection == nullptr) { + return nullptr; + } + + // Should be the same as the view's document, if view is non-NULL + return _selection->layers()->getDocument(); +} + +Selection *ActionContext::getSelection() const +{ + return _selection; +} + +UI::View::View *ActionContext::getView() const +{ + return _view; +} + +SPDesktop *ActionContext::getDesktop() const +{ + return static_cast(_view); +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/helper/action-context.h b/src/helper/action-context.h new file mode 100644 index 0000000..d3468ea --- /dev/null +++ b/src/helper/action-context.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Inkscape UI action context implementation + *//* + * Author: + * Eric Greveson + * + * Copyright (C) 2013 Eric Greveson + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ACTION_CONTEXT_H +#define SEEN_INKSCAPE_ACTION_CONTEXT_H + +class SPDesktop; +class SPDocument; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI + +/** This structure contains all the document/view context required + for an action. Some actions may be executed on a document without + requiring a GUI, hence not providing the info directly through + Inkscape::UI::View::View. Actions that do require GUI objects should + check to see if the relevant pointers are NULL before attempting to + use them. + + TODO: we store a UI::View::View* because that's what the actions and verbs + used to take as parameters in their methods. Why is this? They almost + always seemed to cast straight to an SPDesktop* - so shouldn't we actually + be storing an SPDesktop*? Is there a case where a non-SPDesktop + UI::View::View is used by the actions? YES: Command-line wihtout GUI. + + ActionContext is designed to be copyable, so it may be used with stack + storage if required. */ +class ActionContext { + // NB: Only one of these is typically set - selection model if in console mode, view if in GUI mode + Selection *_selection; /**< The selection model to which this action applies, if running in console mode. May be NULL. */ + UI::View::View *_view; /**< The view to which this action applies. May be NULL (e.g. if running in console mode). */ + +public: + /** Construct without any document or GUI */ + ActionContext(); + + /** Construct an action context for when the app is being run without + any GUI, i.e. in console mode */ + ActionContext(Selection *selection); + + /** Construct an action context for when the app is being run in GUI mode */ + ActionContext(UI::View::View *view); + + /** Get the document for the action context. May be NULL. Prefer this + function to getView()->doc() if the action doesn't require a GUI. */ + SPDocument *getDocument() const; + + /** Get the selection for the action context. May be NULL. Should be + non-NULL if getDocument() is non-NULL. */ + Selection *getSelection() const; + + /** Get the view for the action context. May be NULL. Guaranteed to be + NULL if running in console mode. */ + UI::View::View *getView() const; + + /** Get the desktop for the action context. May be NULL. Guaranteed to be + NULL if running in console mode. */ + SPDesktop *getDesktop() const; +}; + +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/action.cpp b/src/helper/action.cpp new file mode 100644 index 0000000..6b09a91 --- /dev/null +++ b/src/helper/action.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape UI action implementation + *//* + * Authors: + * see git history + * Lauris Kaplinski + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include + +#include "debug/logger.h" +#include "debug/timestamp.h" +#include "debug/simple-event.h" +#include "debug/event-tracker.h" +#include "ui/view/view.h" +#include "desktop.h" +#include "document.h" +#include "verbs.h" + +static void sp_action_finalize (GObject *object); + +G_DEFINE_TYPE(SPAction, sp_action, G_TYPE_OBJECT); + +/** + * SPAction vtable initialization. + */ +static void +sp_action_class_init (SPActionClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + object_class->finalize = sp_action_finalize; +} + +/** + * Callback for SPAction object initialization. + */ +static void +sp_action_init (SPAction *action) +{ + action->sensitive = 0; + action->active = 0; + action->context = Inkscape::ActionContext(); + action->id = action->name = action->tip = nullptr; + action->image = nullptr; + + new (&action->signal_perform) sigc::signal(); + new (&action->signal_set_sensitive) sigc::signal(); + new (&action->signal_set_active) sigc::signal(); + new (&action->signal_set_name) sigc::signal(); +} + +/** + * Called before SPAction object destruction. + */ +static void +sp_action_finalize (GObject *object) +{ + SPAction *action = SP_ACTION(object); + + g_free (action->image); + g_free (action->tip); + g_free (action->name); + g_free (action->id); + + action->signal_perform.~signal(); + action->signal_set_sensitive.~signal(); + action->signal_set_active.~signal(); + action->signal_set_name.~signal(); + + G_OBJECT_CLASS(sp_action_parent_class)->finalize (object); +} + +/** + * Create new SPAction object and set its properties. + */ +SPAction * +sp_action_new(Inkscape::ActionContext const &context, + const gchar *id, + const gchar *name, + const gchar *tip, + const gchar *image, + Inkscape::Verb * verb) +{ + SPAction *action = (SPAction *)g_object_new(SP_TYPE_ACTION, nullptr); + + action->context = context; + action->sensitive = TRUE; + action->id = g_strdup (id); + action->name = g_strdup (name); + action->tip = g_strdup (tip); + action->image = g_strdup (image); + action->verb = verb; + + return action; +} + +namespace { + +using Inkscape::Debug::SimpleEvent; +using Inkscape::Debug::Event; +using Inkscape::Debug::timestamp; + +typedef SimpleEvent ActionEventBase; + +class ActionEvent : public ActionEventBase { +public: + ActionEvent(SPAction const *action) + : ActionEventBase("action") + { + _addProperty("timestamp", timestamp()); + SPDocument *document = action->context.getDocument(); + if (document) { + _addProperty("document", document->serial()); + } + _addProperty("verb", action->id); + } +}; + +} + +/** + * Executes an action. + * @param action The action to be executed. + * @param data ignored. + */ +void sp_action_perform(SPAction *action, void * /*data*/) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + Inkscape::Debug::EventTracker tracker(action); + action->signal_perform.emit(); +} + +/** + * Change activation in all actions that can be taken with the action. + */ +void +sp_action_set_active (SPAction *action, unsigned int active) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + action->signal_set_active.emit(active); +} + +/** + * Change sensitivity in all actions that can be taken with the action. + */ +void +sp_action_set_sensitive (SPAction *action, unsigned int sensitive) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + action->signal_set_sensitive.emit(sensitive); +} + +void +sp_action_set_name (SPAction *action, Glib::ustring const &name) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + g_free(action->name); + action->name = g_strdup(name.data()); + action->signal_set_name.emit(name); +} + +/** + * Return Document associated with the action. + */ +SPDocument * +sp_action_get_document (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getDocument(); +} + +/** + * Return Selection associated with the action + */ +Inkscape::Selection * +sp_action_get_selection (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getSelection(); +} + +/** + * Return View associated with the action, if any. + */ +Inkscape::UI::View::View * +sp_action_get_view (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getView(); +} + +/** + * Return Desktop associated with the action, if any. + */ +SPDesktop * +sp_action_get_desktop (SPAction *action) +{ + // TODO: this slightly horrible storage of a UI::View::View*, and + // casting to an SPDesktop*, is only done because that's what was + // already the norm in the Inkscape codebase. This seems wrong. Surely + // we should store an SPDesktop* in the first place? Is there a case + // of actions being carried out on a View that is not an SPDesktop? + return static_cast(sp_action_get_view(action)); +} + +/** + * \brief Create a toolbutton whose "clicked" signal performs an Inkscape verb + * + * \param[in] verb_code The code (e.g., SP_VERB_EDIT_SELECT_ALL) for the verb we want + * + * \todo This should really attach the toolbutton to an application action instead of + * hooking up the "clicked" signal. This should probably wait until we've + * migrated to Gtk::Application + */ +Gtk::ToolButton * +SPAction::create_toolbutton_for_verb(unsigned int verb_code, + Inkscape::ActionContext &context) +{ + // Get display properties for the verb + auto verb = Inkscape::Verb::get(verb_code); + auto target_action = verb->get_action(context); + auto icon_name = verb->get_image() ? verb->get_image() : Glib::ustring(); + + // Create a button with the required display properties + auto button = Gtk::manage(new Gtk::ToolButton(verb->get_tip())); + auto icon_widget = sp_get_icon_image(icon_name, "/toolbox/small"); + button->set_icon_widget(*icon_widget); + button->set_tooltip_text(verb->get_tip()); + + // Hook up signal handler + auto button_clicked_cb = sigc::bind(sigc::ptr_fun(&sp_action_perform), + target_action, nullptr); + button->signal_clicked().connect(button_clicked_cb); + + return button; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/helper/action.h b/src/helper/action.h new file mode 100644 index 0000000..1f871db --- /dev/null +++ b/src/helper/action.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape UI action implementation + *//* + * Authors: + * see git history + * Lauris Kaplinski + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SP_ACTION_H +#define SEEN_INKSCAPE_SP_ACTION_H + +#include + +#include "helper/action-context.h" +#include +#include + +#define SP_TYPE_ACTION (sp_action_get_type()) +#define SP_ACTION(o) (G_TYPE_CHECK_INSTANCE_CAST((o), SP_TYPE_ACTION, SPAction)) +#define SP_ACTION_CLASS(o) (G_TYPE_CHECK_CLASS_CAST((o), SP_TYPE_ACTION, SPActionClass)) +#define SP_IS_ACTION(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), SP_TYPE_ACTION)) + +namespace Gtk { +class ToolButton; +} + +class SPDesktop; +class SPDocument; +namespace Inkscape { + +class Selection; +class Verb; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI +} + +/** All the data that is required to be an action. This + structure identifies the action and has the data to + create menus and toolbars for the action */ +struct SPAction : public GObject { + unsigned sensitive : 1; /**< Value to track whether the action is sensitive */ + unsigned active : 1; /**< Value to track whether the action is active */ + Inkscape::ActionContext context; /**< The context (doc/view) to which this action is attached */ + gchar *id; /**< The identifier for the action */ + gchar *name; /**< Full text name of the action */ + gchar *tip; /**< A tooltip to describe the action */ + gchar *image; /**< An image to visually identify the action */ + Inkscape::Verb *verb; /**< The verb that produced this action */ + + sigc::signal signal_perform; + sigc::signal signal_set_sensitive; + sigc::signal signal_set_active; + sigc::signal signal_set_name; + + static Gtk::ToolButton * create_toolbutton_for_verb(unsigned int verb_code, + Inkscape::ActionContext &context); +}; + +/** The action class is the same as its parent. */ +struct SPActionClass { + GObjectClass parent_class; /**< Parent Class */ +}; + +GType sp_action_get_type(); + +SPAction *sp_action_new(Inkscape::ActionContext const &context, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image, + Inkscape::Verb *verb); + +void sp_action_perform(SPAction *action, void *data); +void sp_action_set_active(SPAction *action, unsigned active); +void sp_action_set_sensitive(SPAction *action, unsigned sensitive); +void sp_action_set_name(SPAction *action, Glib::ustring const &name); +SPDocument *sp_action_get_document(SPAction *action); +Inkscape::Selection *sp_action_get_selection(SPAction *action); +Inkscape::UI::View::View *sp_action_get_view(SPAction *action); +SPDesktop *sp_action_get_desktop(SPAction *action); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-curves.h b/src/helper/geom-curves.h new file mode 100644 index 0000000..08403e2 --- /dev/null +++ b/src/helper/geom-curves.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_CURVES_H +#define INKSCAPE_HELPER_GEOM_CURVES_H + +/** + * @file + * Specific curve type functions for Inkscape, not provided by lib2geom. + */ +/* + * Author: + * Johan Engelen + * + * Copyright (C) 2008-2009 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/line.h> +#include <2geom/bezier-curve.h> + +/// \todo un-inline this function +inline bool is_straight_curve(Geom::Curve const & c) +{ + if( dynamic_cast(&c) ) + { + return true; + } + // the curve can be a quad/cubic bezier, but could still be a perfect straight line + // if the control points are exactly on the line connecting the initial and final points. + Geom::BezierCurve const *curve = dynamic_cast(&c); + if (curve) { + Geom::Line line(curve->initialPoint(), curve->finalPoint()); + std::vector pts = curve->controlPoints(); + for (unsigned i = 1; i < pts.size() - 1; ++i) { + if (!are_near(pts[i], line)) + return false; + } + return true; + } + + return false; +} + +#endif // INKSCAPE_HELPER_GEOM_CURVES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-nodetype.cpp b/src/helper/geom-nodetype.cpp new file mode 100644 index 0000000..e04b08d --- /dev/null +++ b/src/helper/geom-nodetype.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Specific nodetype geometry functions for Inkscape, not provided my lib2geom. + * + * Author: + * Johan Engelen + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper/geom-nodetype.h" + +#include <2geom/curve.h> + +namespace Geom { + +/* + * NOTE: THIS METHOD NEVER RETURNS "NODE_SYMM". + * Returns the nodetype between c_incoming and c_outgoing. Location of the node is + * at c_incoming.pointAt(1) == c_outgoing.pointAt(0). If these two are unequal, + * the returned type is NODE_NONE. + * Comparison is based on the unitTangent, does not work for NODE_SYMM! + */ +NodeType get_nodetype(Curve const &c_incoming, Curve const &c_outgoing) +{ + if ( !are_near(c_incoming.pointAt(1), c_outgoing.pointAt(0)) ) + return NODE_NONE; + + Geom::Curve *crv = c_incoming.reverse(); + Geom::Point deriv_1 = -crv->unitTangentAt(0); + delete crv; + Geom::Point deriv_2 = c_outgoing.unitTangentAt(0); + double this_angle_L2 = Geom::L2(deriv_1); + double next_angle_L2 = Geom::L2(deriv_2); + double both_angles_L2 = Geom::L2(deriv_1 + deriv_2); + if ( (this_angle_L2 > 1e-6) && + (next_angle_L2 > 1e-6) && + ((this_angle_L2 + next_angle_L2 - both_angles_L2) < 1e-3) ) + { + return NODE_SMOOTH; + } + + return NODE_CUSP; +} + +} // end namespace Geom + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-nodetype.h b/src/helper/geom-nodetype.h new file mode 100644 index 0000000..d7d4d4c --- /dev/null +++ b/src/helper/geom-nodetype.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_NODETYPE_H +#define INKSCAPE_HELPER_GEOM_NODETYPE_H + +/** + * @file + * Specific nodetype geometry functions for Inkscape, not provided my lib2geom. + */ +/* + * Author: + * Johan Engelen + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +namespace Geom { + +/** + * What kind of node is this? This is the value for the node->type + * field. NodeType indicates the degree of continuity required for + * the node. I think that the corresponding integer indicates which + * derivate is connected. (Thus 2 means that the node is continuous + * to the second derivative, i.e. has matching endpoints and tangents) + */ +enum NodeType { +/** Discontinuous node, usually either start or endpoint of a path */ + NODE_NONE, +/** This node continuously joins two segments, but the unit tangent is discontinuous.*/ + NODE_CUSP, +/** This node continuously joins two segments, with continuous *unit* tangent. */ + NODE_SMOOTH, +/** This node is symmetric. I.e. continuously joins two segments with continuous derivative */ + NODE_SYMM +}; + + +NodeType get_nodetype(Curve const &c_incoming, Curve const &c_outgoing); + + +} // namespace Geom + +#endif // INKSCAPE_HELPER_GEOM_NODETYPE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-pathstroke.cpp b/src/helper/geom-pathstroke.cpp new file mode 100644 index 0000000..e706667 --- /dev/null +++ b/src/helper/geom-pathstroke.cpp @@ -0,0 +1,1160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Liam P. White + * Tavmjong Bah + * Alexander Brock + * + * Copyright (C) 2014-2015 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/path-sink.h> +#include <2geom/sbasis-to-bezier.h> // cubicbezierpath_from_sbasis +#include <2geom/path-intersection.h> +#include <2geom/circle.h> + +#include "helper/geom-pathstroke.h" + +namespace Geom { + +static Point intersection_point(Point origin_a, Point vector_a, Point origin_b, Point vector_b) +{ + Coord denom = cross(vector_a, vector_b); + if (!are_near(denom,0.)) { + Coord t = (cross(vector_b, origin_a) + cross(origin_b, vector_b)) / denom; + return origin_a + vector_a*t; + } + return Point(infinity(), infinity()); +} + +/** +* Find circle that touches inside of the curve, with radius matching the curvature, at time value \c t. +* Because this method internally uses unitTangentAt, t should be smaller than 1.0 (see unitTangentAt). +*/ +static Circle touching_circle( D2 const &curve, double t, double tol=0.01 ) +{ + D2 dM=derivative(curve); + if ( are_near(L2sq(dM(t)), tol) ) { + dM=derivative(dM); + } + if ( are_near(L2sq(dM(t)), tol) ) { // try second time + dM=derivative(dM); + } + Piecewise > unitv = unitVector(dM,tol); + Piecewise dMlength = dot(Piecewise >(dM),unitv); + Piecewise k = cross(derivative(unitv),unitv); + k = divide(k,dMlength,tol,3); + double curv = k(t); // note that this value is signed + + Geom::Point normal = unitTangentAt(curve, t).cw(); + double radius = 1/curv; + Geom::Point center = curve(t) + radius*normal; + return Geom::Circle(center, fabs(radius)); +} + + +// Area of triangle given three corner points +static double area( Geom::Point a, Geom::Point b, Geom::Point c ) { + + using Geom::X; + using Geom::Y; + return( 0.5 * fabs( ( a[X]*(b[Y]-c[Y]) + b[X]*(c[Y]-a[Y]) + c[X]*(a[Y]-b[Y]) ) ) ); +} + +// Alternative touching circle routine directly using Beziers. Works only at end points. +static Circle touching_circle( CubicBezier const &curve, bool start ) { + + double k = 0; + Geom::Point p; + Geom::Point normal; + if ( start ) { + double distance = Geom::distance( curve[1], curve[0] ); + k = 4.0/3.0 * area( curve[0], curve[1], curve[2] ) / + (distance * distance * distance); + if( Geom::cross(curve[0]-curve[1], curve[1]-curve[2]) < 0 ) { + k = -k; + } + p = curve[0]; + normal = Geom::Point(curve[1] - curve[0]).cw(); + normal.normalize(); + // std::cout << "Start k: " << k << " d: " << distance << " normal: " << normal << std::endl; + } else { + double distance = Geom::distance( curve[3], curve[2] ); + k = 4.0/3.0 * area( curve[1], curve[2], curve[3] ) / + (distance * distance * distance); + if( Geom::cross(curve[1]-curve[2], curve[2]-curve[3]) < 0 ) { + k = -k; + } + p = curve[3]; + normal = Geom::Point(curve[3] - curve[2]).cw(); + normal.normalize(); + // std::cout << "End k: " << k << " d: " << distance << " normal: " << normal << std::endl; + } + if( k == 0 ) { + return Geom::Circle( Geom::Point(0,std::numeric_limits::infinity()), + std::numeric_limits::infinity()); + } else { + double radius = 1/k; + Geom::Point center = p + normal * radius; + return Geom::Circle( center, fabs(radius) ); + } +} +} + +namespace { + +// Internal data structure + +struct join_data { + join_data(Geom::Path &_res, Geom::Path const&_outgoing, Geom::Point _in_tang, Geom::Point _out_tang, double _miter, double _width) + : res(_res), outgoing(_outgoing), in_tang(_in_tang), out_tang(_out_tang), miter(_miter), width(_width) {}; + + // contains the current path that is being built on + Geom::Path &res; + + // contains the next curve to append + Geom::Path const& outgoing; + + // input tangents + Geom::Point in_tang; + Geom::Point out_tang; + + // line parameters + double miter; + double width; // half stroke width +}; + +// Join functions must append the outgoing path + +typedef void join_func(join_data jd); + +void bevel_join(join_data jd) +{ + jd.res.appendNew(jd.outgoing.initialPoint()); + jd.res.append(jd.outgoing); +} + +void round_join(join_data jd) +{ + jd.res.appendNew(jd.width, jd.width, 0, false, jd.width <= 0, jd.outgoing.initialPoint()); + jd.res.append(jd.outgoing); +} + +void miter_join_internal(join_data jd, bool clip) +{ + using namespace Geom; + + Curve const& incoming = jd.res.back(); + Curve const& outgoing = jd.outgoing.front(); + Path &res = jd.res; + double width = jd.width, miter = jd.miter; + + Point tang1 = jd.in_tang; + Point tang2 = jd.out_tang; + Point p = intersection_point(incoming.finalPoint(), tang1, outgoing.initialPoint(), tang2); + + bool satisfied = false; + bool inc_ls = res.back_open().degreesOfFreedom() <= 4; + + if (p.isFinite()) { + // check size of miter + Point point_on_path = incoming.finalPoint() + rot90(tang1)*width; + // SVG defines miter length as distance between inner intersection and outer intersection, + // which is twice the distance from p to point_on_path but width is half stroke width. + satisfied = distance(p, point_on_path) <= miter * width; + if (satisfied) { + // miter OK, check to see if we can do a relocation + if (inc_ls) { + res.setFinal(p); + } else { + res.appendNew(p); + } + } else if (clip) { + // std::cout << " Clipping ------------ " << std::endl; + // miter needs clipping, find two points + Point bisector_versor = Line(point_on_path, p).versor(); + Point point_limit = point_on_path + miter * width * bisector_versor; + // std::cout << " bisector_versor: " << bisector_versor << std::endl; + // std::cout << " point_limit: " << point_limit << std::endl; + Point p1 = intersection_point(incoming.finalPoint(), tang1, point_limit, bisector_versor.cw()); + Point p2 = intersection_point(outgoing.initialPoint(), tang2, point_limit, bisector_versor.cw()); + // std::cout << " p1: " << p1 << std::endl; + // std::cout << " p2: " << p2 << std::endl; + if (inc_ls) { + res.setFinal(p1); + } else { + res.appendNew(p1); + } + res.appendNew(p2); + } + } + + res.appendNew(outgoing.initialPoint()); + + // check if we can do another relocation + bool out_ls = outgoing.degreesOfFreedom() <= 4; + + if ((satisfied || clip) && out_ls) { + res.setFinal(outgoing.finalPoint()); + } else { + res.append(outgoing); + } + + // either way, add the rest of the path + res.insert(res.end(), ++jd.outgoing.begin(), jd.outgoing.end()); +} + +void miter_join(join_data jd) { miter_join_internal(jd, false); } +void miter_clip_join(join_data jd) { miter_join_internal(jd, true); } + +Geom::Point pick_solution(std::vector points, Geom::Point tang2, Geom::Point endPt) +{ + assert(points.size() == 2); + Geom::Point sol; + if ( dot(tang2, points[0].point() - endPt) > 0 ) { + // points[0] is bad, choose points[1] + sol = points[1]; + } else if ( dot(tang2, points[1].point() - endPt) > 0 ) { // points[0] could be good, now check points[1] + // points[1] is bad, choose points[0] + sol = points[0]; + } else { + // both points are good, choose nearest + sol = ( distanceSq(endPt, points[0].point()) < distanceSq(endPt, points[1].point()) ) + ? points[0].point() : points[1].point(); + } + return sol; +} + +// Arcs line join. If two circles don't intersect, expand inner circle. +Geom::Point expand_circle( Geom::Circle &inner_circle, Geom::Circle const &outer_circle, Geom::Point const &start_pt, Geom::Point const &start_tangent ) { + // std::cout << "expand_circle:" << std::endl; + // std::cout << " outer_circle: radius: " << outer_circle.radius() << " center: " << outer_circle.center() << std::endl; + // std::cout << " start: point: " << start_pt << " tangent: " << start_tangent << std::endl; + + if( !(outer_circle.contains(start_pt) ) ) { + // std::cout << " WARNING: Outer circle does not contain starting point!" << std::endl; + return Geom::Point(0,0); + } + + Geom::Line secant1(start_pt, start_pt + start_tangent); + std::vector chord1_pts = outer_circle.intersect(secant1); + // std::cout << " chord1: " << chord1_pts[0].point() << ", " << chord1_pts[1].point() << std::endl; + Geom::LineSegment chord1(chord1_pts[0].point(), chord1_pts[1].point()); + + Geom::Line bisector = make_bisector_line( chord1 ); + std::vector chord2_pts = outer_circle.intersect(bisector); + // std::cout << " chord2: " << chord2_pts[0].point() << ", " << chord2_pts[1].point() << std::endl; + Geom::LineSegment chord2(chord2_pts[0].point(), chord2_pts[1].point()); + + // Find D, point on chord2 and on circle closest to start point + Geom::Coord d0 = Geom::distance(chord2_pts[0].point(),start_pt); + Geom::Coord d1 = Geom::distance(chord2_pts[1].point(),start_pt); + // std::cout << " d0: " << d0 << " d1: " << d1 << std::endl; + Geom::Point d = (d0 < d1) ? chord2_pts[0].point() : chord2_pts[1].point(); + Geom::Line da(d,start_pt); + + // Chord through start point and point D + std::vector chord3_pts = outer_circle.intersect(da); + // std::cout << " chord3: " << chord3_pts[0].point() << ", " << chord3_pts[1].point() << std::endl; + + // Find farthest point on chord3 and on circle (could be more robust) + Geom::Coord d2 = Geom::distance(chord3_pts[0].point(),d); + Geom::Coord d3 = Geom::distance(chord3_pts[1].point(),d); + // std::cout << " d2: " << d2 << " d3: " << d3 << std::endl; + + // Find point P, the intersection of outer circle and new inner circle + Geom::Point p = (d2 > d3) ? chord3_pts[0].point() : chord3_pts[1].point(); + + // Find center of new circle: it is at the intersection of the bisector + // of the chord defined by the start point and point P and a line through + // the start point and parallel to the first bisector. + Geom::LineSegment chord4(start_pt,p); + Geom::Line bisector2 = make_bisector_line( chord4 ); + Geom::Line diameter = make_parallel_line( start_pt, bisector ); + std::vector center_new = bisector2.intersect( diameter ); + // std::cout << " center_new: " << center_new[0].point() << std::endl; + Geom::Coord r_new = Geom::distance( center_new[0].point(), start_pt ); + // std::cout << " r_new: " << r_new << std::endl; + + inner_circle.setCenter( center_new[0].point() ); + inner_circle.setRadius( r_new ); + return p; +} + +// Arcs line join. If two circles don't intersect, adjust both circles so they just touch. +// Increase (decrease) the radius of circle 1 and decrease (increase) of circle 2 by the same amount keeping the given points and tangents fixed. +Geom::Point adjust_circles( Geom::Circle &circle1, Geom::Circle &circle2, Geom::Point const &point1, Geom::Point const &point2, Geom::Point const &tan1, Geom::Point const &tan2 ) { + + Geom::Point n1 = (circle1.center() - point1).normalized(); // Always points towards center + Geom::Point n2 = (circle2.center() - point2).normalized(); + Geom::Point sum_n = n1 + n2; + + double r1 = circle1.radius(); + double r2 = circle2.radius(); + double delta_r = r2 - r1; + Geom::Point c1 = circle1.center(); + Geom::Point c2 = circle2.center(); + Geom::Point delta_c = c2 - c1; + + // std::cout << "adjust_circles:" << std::endl; + // std::cout << " norm: " << n1 << "; " << n2 << std::endl; + // std::cout << " sum_n: " << sum_n << std::endl; + // std::cout << " delta_r: " << delta_r << std::endl; + // std::cout << " delta_c: " << delta_c << std::endl; + + // Quadratic equation + double A = 4 - sum_n.length() * sum_n.length(); + double B = 4.0 * delta_r - 2.0 * Geom::dot( delta_c, sum_n ); + double C = delta_r * delta_r - delta_c.length() * delta_c.length(); + + double s1 = 0; + double s2 = 0; + + if( fabs(A) < 0.01 ) { + // std::cout << " A near zero! $$$$$$$$$$$$$$$$$$" << std::endl; + if( B != 0 ) { + s1 = -C/B; + s2 = -s1; + } + } else { + s1 = (-B + sqrt(B*B - 4*A*C))/(2*A); + s2 = (-B - sqrt(B*B - 4*A*C))/(2*A); + } + + double dr = (fabs(s1)<=fabs(s2)?s1:s2); + + // std::cout << " A: " << A << std::endl; + // std::cout << " B: " << B << std::endl; + // std::cout << " C: " << C << std::endl; + // std::cout << " s1: " << s1 << " s2: " << s2 << " dr: " << dr << std::endl; + + circle1 = Geom::Circle( c1 - dr*n1, r1-dr ); + circle2 = Geom::Circle( c2 + dr*n2, r2+dr ); + + // std::cout << " C1: " << circle1 << std::endl; + // std::cout << " C2: " << circle2 << std::endl; + // std::cout << " d': " << Geom::Point( circle1.center() - circle2.center() ).length() << std::endl; + + Geom::Line bisector( circle1.center(), circle2.center() ); + std::vector points = circle1.intersect( bisector ); + Geom::Point p0 = points[0].point(); + Geom::Point p1 = points[1].point(); + // std::cout << " points: " << p0 << "; " << p1 << std::endl; + if( std::abs( Geom::distance( p0, circle2.center() ) - circle2.radius() ) < + std::abs( Geom::distance( p1, circle2.center() ) - circle2.radius() ) ) { + return p0; + } else { + return p1; + } +} + +void extrapolate_join_internal(join_data jd, int alternative) +{ + // std::cout << "\nextrapolate_join: entrance: alternative: " << alternative << std::endl; + using namespace Geom; + + Geom::Path &res = jd.res; + Geom::Curve const& incoming = res.back(); + Geom::Curve const& outgoing = jd.outgoing.front(); + Geom::Point startPt = incoming.finalPoint(); + Geom::Point endPt = outgoing.initialPoint(); + Geom::Point tang1 = jd.in_tang; + Geom::Point tang2 = jd.out_tang; + // width is half stroke-width + double width = jd.width, miter = jd.miter; + + // std::cout << " startPt: " << startPt << " endPt: " << endPt << std::endl; + // std::cout << " tang1: " << tang1 << " tang2: " << tang2 << std::endl; + // std::cout << " dot product: " << Geom::dot( tang1, tang2 ) << std::endl; + // std::cout << " width: " << width << std::endl; + + // CIRCLE CALCULATION TESTING + Geom::Circle circle1 = touching_circle(Geom::reverse(incoming.toSBasis()), 0.); + Geom::Circle circle2 = touching_circle(outgoing.toSBasis(), 0); + // std::cout << " circle1: " << circle1 << std::endl; + // std::cout << " circle2: " << circle2 << std::endl; + if( Geom::CubicBezier const * in_bezier = dynamic_cast(&incoming) ) { + Geom::Circle circle_test1 = touching_circle(*in_bezier, false); + if( !Geom::are_near( circle1, circle_test1, 0.01 ) ) { + // std::cout << " Circle 1 error!!!!!!!!!!!!!!!!!" << std::endl; + // std::cout << " " << circle_test1 << std::endl; + } + } + if( Geom::CubicBezier const * out_bezier = dynamic_cast(&outgoing) ) { + Geom::Circle circle_test2 = touching_circle(*out_bezier, true); + if( !Geom::are_near( circle2, circle_test2, 0.01 ) ) { + // std::cout << " Circle 2 error!!!!!!!!!!!!!!!!!" << std::endl; + // std::cout << " " << circle_test2 << std::endl; + } + } + // END TESTING + + Geom::Point center1 = circle1.center(); + Geom::Point center2 = circle2.center(); + double side1 = tang1[Geom::X]*(startPt[Geom::Y]-center1[Geom::Y]) - tang1[Geom::Y]*(startPt[Geom::X]-center1[Geom::X]); + double side2 = tang2[Geom::X]*( endPt[Geom::Y]-center2[Geom::Y]) - tang2[Geom::Y]*( endPt[Geom::X]-center2[Geom::X]); + // std::cout << " side1: " << side1 << " side2: " << side2 << std::endl; + + bool inc_ls = !circle1.center().isFinite(); + bool out_ls = !circle2.center().isFinite(); + + std::vector points; + + Geom::EllipticalArc *arc1 = nullptr; + Geom::EllipticalArc *arc2 = nullptr; + Geom::LineSegment *seg1 = nullptr; + Geom::LineSegment *seg2 = nullptr; + Geom::Point sol; + Geom::Point p1; + Geom::Point p2; + + if (!inc_ls && !out_ls) { + // std::cout << " two circles" << std::endl; + + // See if tangent is backwards (radius < width/2 and circle is inside stroke). + Geom::Point node_on_path = startPt + Geom::rot90(tang1)*width; + // std::cout << " node_on_path: " << node_on_path << " -------------- " << std::endl; + bool b1 = false; + bool b2 = false; + if (circle1.radius() < width && distance( circle1.center(), node_on_path ) < width) { + b1 = true; + } + if (circle2.radius() < width && distance( circle2.center(), node_on_path ) < width) { + b2 = true; + } + // std::cout << " b1: " << (b1?"true":"false") + // << " b2: " << (b2?"true":"false") << std::endl; + + // Two circles + points = circle1.intersect(circle2); + + if (points.size() != 2) { + // std::cout << " Circles do not intersect, do backup" << std::endl; + switch (alternative) { + case 1: + { + // Fallback to round if one path has radius smaller than half line width. + if(b1 || b2) { + // std::cout << "At one least path has radius smaller than half line width." << std::endl; + return( round_join(jd) ); + } + + Point p; + if( circle2.contains( startPt ) && !circle1.contains( endPt ) ) { + // std::cout << "Expand circle1" << std::endl; + p = expand_circle( circle1, circle2, startPt, tang1 ); + points.emplace_back( 0, 0, p ); + points.emplace_back( 0, 0, p ); + } else if( circle1.contains( endPt ) && !circle2.contains( startPt ) ) { + // std::cout << "Expand circle2" << std::endl; + p = expand_circle( circle2, circle1, endPt, tang2 ); + points.emplace_back( 0, 0, p ); + points.emplace_back( 0, 0, p ); + } else { + // std::cout << "Either both points inside or both outside" << std::endl; + return( miter_clip_join(jd) ); + } + break; + + } + case 2: + { + // Fallback to round if one path has radius smaller than half line width. + if(b1 || b2) { + // std::cout << "At one least path has radius smaller than half line width." << std::endl; + return( round_join(jd) ); + } + + if( ( circle2.contains( startPt ) && !circle1.contains( endPt ) ) || + ( circle1.contains( endPt ) && !circle2.contains( startPt ) ) ) { + + Geom::Point apex = adjust_circles( circle1, circle2, startPt, endPt, tang1, tang2 ); + points.emplace_back( 0, 0, apex ); + points.emplace_back( 0, 0, apex ); + } else { + // std::cout << "Either both points inside or both outside" << std::endl; + return( miter_clip_join(jd) ); + } + + break; + } + case 3: + if( side1 > 0 ) { + Geom::Line secant(startPt, startPt + tang1); + points = circle2.intersect(secant); + circle1.setRadius(std::numeric_limits::infinity()); + circle1.setCenter(Geom::Point(0,std::numeric_limits::infinity())); + } else { + Geom::Line secant(endPt, endPt + tang2); + points = circle1.intersect(secant); + circle2.setRadius(std::numeric_limits::infinity()); + circle2.setCenter(Geom::Point(0,std::numeric_limits::infinity())); + } + break; + + + case 0: + default: + // Do nothing + break; + } + } + + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + if( circle1.radius() != std::numeric_limits::infinity() ) { + arc1 = circle1.arc(startPt, 0.5*(startPt+sol), sol); + } else { + seg1 = new Geom::LineSegment(startPt, sol); + } + if( circle2.radius() != std::numeric_limits::infinity() ) { + arc2 = circle2.arc(sol, 0.5*(sol+endPt), endPt); + } else { + seg2 = new Geom::LineSegment(sol, endPt); + } + } + } else if (inc_ls && !out_ls) { + // Line and circle + // std::cout << " line circle" << std::endl; + points = circle2.intersect(Line(incoming.initialPoint(), incoming.finalPoint())); + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + arc2 = circle2.arc(sol, 0.5*(sol+endPt), endPt); + } + } else if (!inc_ls && out_ls) { + // Circle and line + // std::cout << " circle line" << std::endl; + points = circle1.intersect(Line(outgoing.initialPoint(), outgoing.finalPoint())); + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + arc1 = circle1.arc(startPt, 0.5*(sol+startPt), sol); + } + } + if (points.size() != 2) { + // std::cout << " no solutions" << std::endl; + // no solutions available, fall back to miter + return miter_join(jd); + } + + // We have a solution, thus sol is defined. + p1 = sol; + + // See if we need to clip. Miter length is measured along a circular arc that is tangent to the + // bisector of the incoming and out going angles and passes through the end point (sol) of the + // line join. + + // Center of circle is intersection of a line orthogonal to bisector and a line bisecting + // a chord connecting the path end point (point_on_path) and the join end point (sol). + Geom::Point point_on_path = startPt + Geom::rot90(tang1)*width; + Geom::Line bisector = make_angle_bisector_line(startPt, point_on_path, endPt); + Geom::Line ortho = make_orthogonal_line(point_on_path, bisector); + + Geom::LineSegment chord(point_on_path, sol); + Geom::Line bisector_chord = make_bisector_line(chord); + + Geom::Line limit_line; + double miter_limit = width * miter; + bool clipped = false; + + if (are_parallel(bisector_chord, ortho)) { + // No intersection (can happen if curvatures are equal but opposite) + if (Geom::distance(point_on_path, sol) > miter_limit) { + clipped = true; + Geom::Point temp = bisector.versor(); + Geom::Point limit_point = point_on_path + miter_limit * temp; + limit_line = make_parallel_line( limit_point, ortho ); + } + } else { + Geom::Point center = + Geom::intersection_point( bisector_chord.pointAt(0), bisector_chord.versor(), + ortho.pointAt(0), ortho.versor() ); + Geom::Coord radius = distance(center, point_on_path); + Geom::Circle circle_center(center, radius); + + double limit_angle = miter_limit / radius; + + Geom::Ray start_ray(center, point_on_path); + Geom::Ray end_ray(center, sol); + Geom::Line limit_line(center, 0); // Angle set below + + if (Geom::cross(start_ray.versor(), end_ray.versor()) < 0) { + limit_line.setAngle(start_ray.angle() - limit_angle); + } else { + limit_line.setAngle(start_ray.angle() + limit_angle); + } + + Geom::EllipticalArc *arc_center = circle_center.arc(point_on_path, 0.5*(point_on_path + sol), sol); + if (arc_center && arc_center->sweepAngle() > limit_angle) { + // We need to clip + clipped = true; + + if (!inc_ls) { + // Incoming circular + points = circle1.intersect(limit_line); + if (points.size() == 2) { + p1 = pick_solution(points, tang2, endPt); + delete arc1; + arc1 = circle1.arc(startPt, 0.5*(p1+startPt), p1); + } + } else { + p1 = Geom::intersection_point(startPt, tang1, limit_line.pointAt(0), limit_line.versor()); + } + + if (!out_ls) { + // Outgoing circular + points = circle2.intersect(limit_line); + if (points.size() == 2) { + p2 = pick_solution(points, tang1, endPt); + delete arc2; + arc2 = circle2.arc(p2, 0.5*(p2+endPt), endPt); + } + } else { + p2 = Geom::intersection_point(endPt, tang2, limit_line.pointAt(0), limit_line.versor()); + } + } + } + + // Add initial + if (arc1) { + res.append(*arc1); + } else if (seg1 ) { + res.append(*seg1); + } else { + // Straight line segment: move last point + res.setFinal(p1); + } + + if (clipped) { + res.appendNew(p2); + } + + // Add outgoing + if (arc2) { + res.append(*arc2); + res.append(outgoing); + } else if (seg2 ) { + res.append(*seg2); + res.append(outgoing); + } else { + // Straight line segment: + res.appendNew(outgoing.finalPoint()); + } + + // add the rest of the path + res.insert(res.end(), ++jd.outgoing.begin(), jd.outgoing.end()); + + delete arc1; + delete arc2; + delete seg1; + delete seg2; +} + +void extrapolate_join( join_data jd) { extrapolate_join_internal(jd, 0); } +void extrapolate_join_alt1(join_data jd) { extrapolate_join_internal(jd, 1); } +void extrapolate_join_alt2(join_data jd) { extrapolate_join_internal(jd, 2); } +void extrapolate_join_alt3(join_data jd) { extrapolate_join_internal(jd, 3); } + + +void tangents(Geom::Point tang[2], Geom::Curve const& incoming, Geom::Curve const& outgoing) +{ + Geom::Point tang1 = Geom::unitTangentAt(reverse(incoming.toSBasis()), 0.); + Geom::Point tang2 = outgoing.unitTangentAt(0.); + tang[0] = tang1, tang[1] = tang2; +} + +// Offsetting a line segment is mathematically stable and quick to do +Geom::LineSegment offset_line(Geom::LineSegment const& l, double width) +{ + Geom::Point tang1 = Geom::rot90(l.unitTangentAt(0)); + Geom::Point tang2 = Geom::rot90(unitTangentAt(reverse(l.toSBasis()), 0.)); + + Geom::Point start = l.initialPoint() + tang1 * width; + Geom::Point end = l.finalPoint() - tang2 * width; + + return Geom::LineSegment(start, end); +} + +void get_cubic_data(Geom::CubicBezier const& bez, double time, double& len, double& rad) +{ + // get derivatives + std::vector derivs = bez.pointAndDerivatives(time, 3); + + Geom::Point der1 = derivs[1]; // first deriv (tangent vector) + Geom::Point der2 = derivs[2]; // second deriv (tangent's tangent) + double l = Geom::L2(der1); // length + + len = rad = 0; + + // TODO: we might want to consider using Geom::touching_circle to determine the + // curvature radius here. Less code duplication, but slower + + if (Geom::are_near(l, 0, 1e-4)) { + l = Geom::L2(der2); + Geom::Point der3 = derivs.at(3); // try second time + if (Geom::are_near(l, 0, 1e-4)) { + l = Geom::L2(der3); + if (Geom::are_near(l, 0)) { + return; // this isn't a segment... + } + rad = 1e8; + } else { + rad = -l * (Geom::dot(der2, der2) / Geom::cross(der2, der3)); + } + } else { + rad = -l * (Geom::dot(der1, der1) / Geom::cross(der1, der2)); + } + len = l; +} + +double _offset_cubic_stable_sub( + Geom::CubicBezier const& bez, + Geom::CubicBezier& c, + const Geom::Point& start_normal, + const Geom::Point& end_normal, + const Geom::Point& start_new, + const Geom::Point& end_new, + const double start_rad, + const double end_rad, + const double start_len, + const double end_len, + const double width, + const double width_correction) { + using Geom::X; + using Geom::Y; + + double start_off = 1, end_off = 1; + // correction of the lengths of the tangent to the offset + if (!Geom::are_near(start_rad, 0)) + start_off += (width + width_correction) / start_rad; + if (!Geom::are_near(end_rad, 0)) + end_off += (width + width_correction) / end_rad; + + // We don't change the direction of the control points + if (start_off < 0) { + start_off = 0; + } + if (end_off < 0) { + end_off = 0; + } + start_off *= start_len; + end_off *= end_len; + // -------- + + Geom::Point mid1_new = start_normal.ccw()*start_off; + mid1_new = Geom::Point(start_new[X] + mid1_new[X]/3., start_new[Y] + mid1_new[Y]/3.); + Geom::Point mid2_new = end_normal.ccw()*end_off; + mid2_new = Geom::Point(end_new[X] - mid2_new[X]/3., end_new[Y] - mid2_new[Y]/3.); + + // create the estimate curve + c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new); + + // check the tolerance for our estimate to be a parallel curve + + double worst_residual = 0; + for (size_t ii = 3; ii <= 7; ii+=2) { + const double t = static_cast(ii) / 10; + const Geom::Point req = bez.pointAt(t); + const Geom::Point chk = c.pointAt(c.nearestTime(req)); + const double current_residual = (chk-req).length() - std::abs(width); + if (std::abs(current_residual) > std::abs(worst_residual)) { + worst_residual = current_residual; + } + } + return worst_residual; +} + +void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels) +{ + using Geom::X; + using Geom::Y; + + const Geom::Point start_pos = bez.initialPoint(); + const Geom::Point end_pos = bez.finalPoint(); + + const Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0)); + const Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.)); + + // offset the start and end control points out by the width + const Geom::Point start_new = start_pos + start_normal*width; + const Geom::Point end_new = end_pos + end_normal*width; + + // -------- + double start_rad, end_rad; + double start_len, end_len; // tangent lengths + get_cubic_data(bez, 0, start_len, start_rad); + get_cubic_data(bez, 1, end_len, end_rad); + + + Geom::CubicBezier c; + + double best_width_correction = 0; + double best_residual = _offset_cubic_stable_sub( + bez, c, + start_normal, end_normal, + start_new, end_new, + start_rad, end_rad, + start_len, end_len, + width, best_width_correction); + double stepsize = std::abs(width)/2; + bool seen_success = false; + double stepsize_threshold = 0; + // std::cout << "Residual from " << best_residual << " "; + size_t ii = 0; + for (; ii < 100 && stepsize > stepsize_threshold; ++ii) { + bool success = false; + const double width_correction = best_width_correction - (best_residual > 0 ? 1 : -1) * stepsize; + Geom::CubicBezier current_curve; + const double residual = _offset_cubic_stable_sub( + bez, current_curve, + start_normal, end_normal, + start_new, end_new, + start_rad, end_rad, + start_len, end_len, + width, width_correction); + if (std::abs(residual) < std::abs(best_residual)) { + best_residual = residual; + best_width_correction = width_correction; + c = current_curve; + success = true; + if (std::abs(best_residual) < tol/4) { + break; + } + } + + if (success) { + if (!seen_success) { + seen_success = true; + //std::cout << "Stepsize factor: " << std::abs(width) / stepsize << std::endl; + stepsize_threshold = stepsize / 1000; + } + } + else { + stepsize /= 2; + } + if (std::abs(best_width_correction) >= std::abs(width)/2) { + //break; // Seems to prevent some numerical instabilities, not clear if useful + } + } + + // reached maximum recursive depth + // don't bother with any more correction + if (levels == 0) { + try { + p.append(c); + } + catch (...) { + if ((p.finalPoint() - c.initialPoint()).length() < 1e-6) { + c.setInitial(p.finalPoint()); + } + else { + auto line = Geom::LineSegment(p.finalPoint(), c.initialPoint()); + p.append(line); + } + p.append(c); + } + + return; + } + + // We find the point on our new curve (c) for which the distance between + // (c) and (bez) differs the most from the desired distance (width). + double worst_err = std::abs(best_residual); + double worst_time = .5; + for (size_t ii = 1; ii <= 9; ++ii) { + const double t = static_cast(ii) / 10; + const Geom::Point req = bez.pointAt(t); + // We use the exact solution with nearestTime because it is numerically + // much more stable than simply assuming that the point on (c) closest + // to bez.pointAt(t) is given by c.pointAt(t) + const Geom::Point chk = c.pointAt(c.nearestTime(req)); + + Geom::Point const diff = req - chk; + const double err = std::abs(diff.length() - std::abs(width)); + if (err > worst_err) { + worst_err = err; + worst_time = t; + } + } + + if (worst_err < tol) { + if (Geom::are_near(start_new, p.finalPoint())) { + p.setFinal(start_new); // if it isn't near, we throw + } + + // we're good, curve is accurate enough + p.append(c); + return; + } else { + // split the curve in two + std::pair s = bez.subdivide(worst_time); + offset_cubic(p, s.first, width, tol, levels - 1); + offset_cubic(p, s.second, width, tol, levels - 1); + } +} + +void offset_quadratic(Geom::Path& p, Geom::QuadraticBezier const& bez, double width, double tol, size_t levels) +{ + // cheat + // it's faster + // seriously + std::vector points = bez.controlPoints(); + Geom::Point b1 = points[0] + (2./3) * (points[1] - points[0]); + Geom::Point b2 = b1 + (1./3) * (points[2] - points[0]); + Geom::CubicBezier cub = Geom::CubicBezier(points[0], b1, b2, points[2]); + offset_cubic(p, cub, width, tol, levels); +} + +void offset_curve(Geom::Path& res, Geom::Curve const* current, double width, double tolerance) +{ + size_t levels = 8; + + if (current->isDegenerate()) return; // don't do anything + + // TODO: we can handle SVGEllipticalArc here as well, do that! + + if (Geom::BezierCurve const *b = dynamic_cast(current)) { + size_t order = b->order(); + switch (order) { + case 1: + res.append(offset_line(static_cast(*current), width)); + break; + case 2: { + Geom::QuadraticBezier const& q = static_cast(*current); + offset_quadratic(res, q, width, tolerance, levels); + break; + } + case 3: { + Geom::CubicBezier const& cb = static_cast(*current); + offset_cubic(res, cb, width, tolerance, levels); + break; + } + default: { + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), tolerance); + for (const auto & i : sbasis_path) + offset_curve(res, &i, width, tolerance); + break; + } + } + } else { + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), 0.1); + for (const auto & i : sbasis_path) + offset_curve(res, &i, width, tolerance); + } +} + +typedef void cap_func(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width); + +void flat_cap(Geom::PathBuilder& res, Geom::Path const&, Geom::Path const& against_dir, double) +{ + res.lineTo(against_dir.initialPoint()); +} + +void round_cap(Geom::PathBuilder& res, Geom::Path const&, Geom::Path const& against_dir, double width) +{ + res.arcTo(width / 2., width / 2., 0., true, false, against_dir.initialPoint()); +} + +void square_cap(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width) +{ + width /= 2.; + Geom::Point normal_1 = -Geom::unitTangentAt(Geom::reverse(with_dir.back().toSBasis()), 0.); + Geom::Point normal_2 = -against_dir[0].unitTangentAt(0.); + res.lineTo(with_dir.finalPoint() + normal_1*width); + res.lineTo(against_dir.initialPoint() + normal_2*width); + res.lineTo(against_dir.initialPoint()); +} + +void peak_cap(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width) +{ + width /= 2.; + Geom::Point normal_1 = -Geom::unitTangentAt(Geom::reverse(with_dir.back().toSBasis()), 0.); + Geom::Point normal_2 = -against_dir[0].unitTangentAt(0.); + Geom::Point midpoint = ((with_dir.finalPoint() + normal_1*width) + (against_dir.initialPoint() + normal_2*width)) * 0.5; + res.lineTo(midpoint); + res.lineTo(against_dir.initialPoint()); +} + +} // namespace + +namespace Inkscape { + +Geom::PathVector outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join, + LineCapType butt, + double tolerance) +{ + if (input.size() == 0) return Geom::PathVector(); // nope, don't even try + + Geom::PathBuilder res; + Geom::Path with_dir = half_outline(input, width/2., miter, join, tolerance); + Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join, tolerance); + res.moveTo(with_dir[0].initialPoint()); + res.append(with_dir); + + cap_func *cf; + switch (butt) { + case BUTT_ROUND: + cf = &round_cap; + break; + case BUTT_SQUARE: + cf = &square_cap; + break; + case BUTT_PEAK: + cf = &peak_cap; + break; + default: + cf = &flat_cap; + } + + // glue caps + if (!input.closed()) { + cf(res, with_dir, against_dir, width); + } else { + res.closePath(); + res.moveTo(against_dir.initialPoint()); + } + + res.append(against_dir); + + if (!input.closed()) { + cf(res, against_dir, with_dir, width); + } + + res.closePath(); + res.flush(); + return res.peek(); +} + +Geom::Path half_outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join, + double tolerance) +{ + if (tolerance <= 0) { + if (std::abs(width) > 0) { + tolerance = 5.0 * (std::abs(width)/100); + } + else { + tolerance = 1e-4; + } + } + Geom::Path res; + if (input.size() == 0) return res; + + Geom::Point tang1 = input[0].unitTangentAt(0); + Geom::Point start = input.initialPoint() + tang1 * width; + Geom::Path temp; + Geom::Point tang[2]; + + res.setStitching(true); + temp.setStitching(true); + + res.start(start); + + // Do two curves at a time for efficiency, since the join function needs to know the outgoing curve as well + const Geom::Curve &closingline = input.back_closed(); + const size_t k = (are_near(closingline.initialPoint(), closingline.finalPoint()) && input.closed() ) + ?input.size_open():input.size_default(); + + for (size_t u = 0; u < k; u += 2) { + temp.clear(); + + offset_curve(temp, &input[u], width, tolerance); + + // on the first run through, there isn't a join + if (u == 0) { + res.append(temp); + } else { + tangents(tang, input[u-1], input[u]); + outline_join(res, temp, tang[0], tang[1], width, miter, join); + } + + // odd number of paths + if (u < k - 1) { + temp.clear(); + offset_curve(temp, &input[u+1], width, tolerance); + tangents(tang, input[u], input[u+1]); + outline_join(res, temp, tang[0], tang[1], width, miter, join); + } + } + if (input.closed()) { + Geom::Curve const &c1 = res.back(); + Geom::Curve const &c2 = res.front(); + temp.clear(); + temp.append(c1); + Geom::Path temp2; + temp2.append(c2); + tangents(tang, input.back(), input.front()); + outline_join(temp, temp2, tang[0], tang[1], width, miter, join); + res.erase(res.begin()); + res.erase_last(); + res.append(temp); + res.close(); + } + return res; +} + +void outline_join(Geom::Path &res, Geom::Path const& temp, Geom::Point in_tang, Geom::Point out_tang, double width, double miter, Inkscape::LineJoinType join) +{ + if (res.size() == 0 || temp.size() == 0) + return; + Geom::Curve const& outgoing = temp.front(); + if (Geom::are_near(res.finalPoint(), outgoing.initialPoint(), 0.01)) { + // if the points are /that/ close, just ignore this one + res.setFinal(temp.initialPoint()); + res.append(temp); + return; + } + + join_data jd(res, temp, in_tang, out_tang, miter, width); + if (!(Geom::cross(in_tang, out_tang) > 0)) { + join = Inkscape::JOIN_BEVEL; + } + join_func *jf; + switch (join) { + case Inkscape::JOIN_BEVEL: + jf = &bevel_join; + break; + case Inkscape::JOIN_ROUND: + jf = &round_join; + break; + case Inkscape::JOIN_EXTRAPOLATE: + jf = &extrapolate_join; + break; + case Inkscape::JOIN_EXTRAPOLATE1: + jf = &extrapolate_join_alt1; + break; + case Inkscape::JOIN_EXTRAPOLATE2: + jf = &extrapolate_join_alt2; + break; + case Inkscape::JOIN_EXTRAPOLATE3: + jf = &extrapolate_join_alt3; + break; + case Inkscape::JOIN_MITER_CLIP: + jf = &miter_clip_join; + break; + default: + jf = &miter_join; + } + jf(jd); + } + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/helper/geom-pathstroke.h b/src/helper/geom-pathstroke.h new file mode 100644 index 0000000..73d35b4 --- /dev/null +++ b/src/helper/geom-pathstroke.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_PATH_STROKE_H +#define INKSCAPE_HELPER_PATH_STROKE_H + +/* Authors: + * Liam P. White + * Tavmjong Bah + * + * Copyright (C) 2014-2015 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/path.h> +#include <2geom/pathvector.h> + +namespace Inkscape { + +enum LineJoinType { + JOIN_BEVEL, + JOIN_ROUND, + JOIN_MITER, + JOIN_MITER_CLIP, + JOIN_EXTRAPOLATE, + JOIN_EXTRAPOLATE1, + JOIN_EXTRAPOLATE2, + JOIN_EXTRAPOLATE3, +}; + +enum LineCapType { + BUTT_FLAT, + BUTT_ROUND, + BUTT_SQUARE, + BUTT_PEAK, // This is not a line ending supported by the SVG standard. +}; + +/** + * Strokes the path given by @a input. + * Joins may behave oddly if the width is negative. + * + * @param[in] input Input path. + * @param[in] width Stroke width. + * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. + * @param[in] join Line join type used during offset. Member of LineJoinType enum. + * @param[in] cap Line cap type used during stroking. Member of LineCapType enum. + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. + * + * @return Stroked path. + * If the input path is closed, the resultant vector will contain two paths. + * Otherwise, there should be only one in the output. + */ +Geom::PathVector outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join = JOIN_BEVEL, + LineCapType cap = BUTT_FLAT, + double tolerance = -1); + +/** + * Offset the input path by @a width. + * Joins may behave oddly if the width is negative. + * + * @param[in] input Input path. + * @param[in] width Amount to offset. + * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. + * @param[in] join Line join type used during offset. Member of LineJoinType enum. + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. + * + * @return Offsetted output. + */ +Geom::Path half_outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join = JOIN_BEVEL, + double tolerance = -1); + +/** + * Builds a join on the provided path. + * Joins may behave oddly if the width is negative. + * + * @param[inout] res The path to build the join on. + * The outgoing path (or a portion thereof) will be appended after the join is created. + * Previous segments may be modified as an optimization, beware! + * + * @param[in] outgoing The segment to append on the outgoing portion of the join. + * @param[in] in_tang The end tangent to consider on the input path. + * @param[in] out_tang The begin tangent to consider on the output path. + * @param[in] width + * @param[in] miter + * @param[in] join + */ +void outline_join(Geom::Path &res, Geom::Path const& outgoing, Geom::Point in_tang, Geom::Point out_tang, double width, double miter, LineJoinType join); + +} // namespace Inkscape + +#endif // INKSCAPE_HELPER_PATH_STROKE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/helper/geom-pathvectorsatellites.cpp b/src/helper/geom-pathvectorsatellites.cpp new file mode 100644 index 0000000..dd625b1 --- /dev/null +++ b/src/helper/geom-pathvectorsatellites.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PathVectorSatellites a class to manage satellites -per node extra data- in a pathvector + *//* + * Authors: see git history + * Jabiertxof + * Nathan Hurst + * Johan Engelen + * Josh Andler + * suv + * Mc- + * Liam P. White + * Krzysztof KosiÅ„ski + * This code is in public domain + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "util/units.h" + +Geom::PathVector PathVectorSatellites::getPathVector() const +{ + return _pathvector; +} + +void PathVectorSatellites::setPathVector(Geom::PathVector pathv) +{ + _pathvector = pathv; +} + +Satellites PathVectorSatellites::getSatellites() +{ + return _satellites; +} + +void PathVectorSatellites::setSatellites(Satellites satellites) +{ + _satellites = satellites; +} + +size_t PathVectorSatellites::getTotalSatellites() +{ + size_t counter = 0; + for (auto & _satellite : _satellites) { + counter += _satellite.size(); + } + return counter; +} + +std::pair PathVectorSatellites::getIndexData(size_t index) +{ + size_t counter = 0; + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if (index == counter) { + return std::make_pair(i,j); + } + counter++; + } + } + return std::make_pair(0,0); +} + +void PathVectorSatellites::setSelected(std::vector selected) +{ + size_t counter = 0; + for (auto & _satellite : _satellites) { + for (auto & j : _satellite) { + if (find (selected.begin(), selected.end(), counter) != selected.end()) { + j.setSelected(true); + } else { + j.setSelected(false); + } + counter++; + } + } +} + +void PathVectorSatellites::updateSteps(size_t steps, bool apply_no_radius, bool apply_with_radius, bool only_selected) +{ + for (auto & _satellite : _satellites) { + for (auto & j : _satellite) { + if ((!apply_no_radius && j.amount == 0) || + (!apply_with_radius && j.amount != 0)) + { + continue; + } + if (only_selected) { + if (j.selected) { + j.steps = steps; + } + } else { + j.steps = steps; + } + } + } +} + +void PathVectorSatellites::updateAmount(double radius, bool apply_no_radius, bool apply_with_radius, bool only_selected, + bool use_knot_distance, bool flexible) +{ + double power = 0; + if (!flexible) { + power = radius; + } else { + power = radius / 100; + } + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + boost::optional previous_index = boost::none; + if (j == 0 && _pathvector[i].closed()) { + previous_index = count_path_nodes(_pathvector[i]) - 1; + } else if (!_pathvector[i].closed() || j != 0) { + previous_index = j - 1; + } + if (!_pathvector[i].closed() && j == 0) { + _satellites[i][j].amount = 0; + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + continue; + } + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + + if (_satellites[i][j].selected || !only_selected) { + if (!use_knot_distance && !flexible) { + if (previous_index) { + _satellites[i][j].amount = _satellites[i][j].radToLen(power, _pathvector[i][*previous_index], _pathvector[i][j]); + if (power && !_satellites[i][j].amount) { + g_warning("Seems a too high radius value"); + } + } else { + _satellites[i][j].amount = 0.0; + } + } else { + _satellites[i][j].amount = power; + } + } + } + } +} + +void PathVectorSatellites::convertUnit(Glib::ustring in, Glib::ustring to, bool apply_no_radius, bool apply_with_radius) +{ + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if (!_pathvector[i].closed() && j == 0) { + _satellites[i][j].amount = 0; + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + continue; + } + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + _satellites[i][j].amount = Inkscape::Util::Quantity::convert(_satellites[i][j].amount, in.c_str(), to.c_str()); + } + } +} + +void PathVectorSatellites::updateSatelliteType(SatelliteType satellitetype, bool apply_no_radius, bool apply_with_radius, + bool only_selected) +{ + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + if (!only_selected) { + _satellites[i][j].satellite_type = satellitetype; + } + continue; + } + if (only_selected) { + Geom::Point satellite_point = _pathvector[i].pointAt(j); + if (_satellites[i][j].selected) { + _satellites[i][j].satellite_type = satellitetype; + } + } else { + _satellites[i][j].satellite_type = satellitetype; + } + } + } +} + +void PathVectorSatellites::recalculateForNewPathVector(Geom::PathVector const pathv, Satellite const S) +{ + // pathv && _pathvector came here: + // * with diferent number of nodes + // * without empty subpats + // * _pathvector and satellites (old data) are paired + Satellites satellites; + bool found = false; + //TODO evaluate fix on nodes at same position + size_t number_nodes = count_pathvector_nodes(pathv); + size_t previous_number_nodes = getTotalSatellites(); + for (const auto & i : pathv) { + std::vector path_satellites; + size_t count = count_path_nodes(i); + for (size_t j = 0; j < count; j++) { + found = false; + for (size_t k = 0; k < _pathvector.size(); k++) { + size_t count2 = count_path_nodes(_pathvector[k]); + for (size_t l = 0; l < count2; l++) { + if (Geom::are_near(_pathvector[k][l].initialPoint(), i[j].initialPoint())) { + path_satellites.push_back(_satellites[k][l]); + found = true; + break; + } + } + if (found) { + break; + } + } + if (!found) { + path_satellites.push_back(S); + } + } + satellites.push_back(path_satellites); + } + setPathVector(pathv); + setSatellites(satellites); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-pathvectorsatellites.h b/src/helper/geom-pathvectorsatellites.h new file mode 100644 index 0000000..3009081 --- /dev/null +++ b/src/helper/geom-pathvectorsatellites.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief PathVectorSatellites a class to manage satellites -per node extra data- in a pathvector + *//* + * Authors: see git history + * Jabiertxof + * Nathan Hurst + * Johan Engelen + * Josh Andler + * suv + * Mc- + * Liam P. White + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef SEEN_PATHVECTORSATELLITES_H +#define SEEN_PATHVECTORSATELLITES_H + +#include +#include <2geom/path.h> +#include <2geom/pathvector.h> + +typedef std::vector > Satellites; +///@brief PathVectorSatellites a class to manage satellites in a pathvector +class PathVectorSatellites { +public: + Geom::PathVector getPathVector() const; + void setPathVector(Geom::PathVector pathv); + Satellites getSatellites(); + void setSatellites(Satellites satellites); + size_t getTotalSatellites(); + void setSelected(std::vector selected); + void updateSteps(size_t steps, bool apply_no_radius, bool apply_with_radius, bool only_selected); + void updateAmount(double radius, bool apply_no_radius, bool apply_with_radius, bool only_selected, + bool use_knot_distance, bool flexible); + void convertUnit(Glib::ustring in, Glib::ustring to, bool apply_no_radius, bool apply_with_radius); + void updateSatelliteType(SatelliteType satellitetype, bool apply_no_radius, bool apply_with_radius, bool only_selected); + std::pair getIndexData(size_t index); + void recalculateForNewPathVector(Geom::PathVector const pathv, Satellite const S); +private: + Geom::PathVector _pathvector; + Satellites _satellites; +}; + +#endif //SEEN_PATHVECTORSATELLITES_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-satellite.cpp b/src/helper/geom-satellite.cpp new file mode 100644 index 0000000..0d8ca83 --- /dev/null +++ b/src/helper/geom-satellite.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief Satellite a per node holder of data. + *//* + * Authors: + * see git history + * 2015 Jabier Arraiza Cenoz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/curve.h> +#include <2geom/nearest-time.h> +#include <2geom/path-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/ray.h> +#include +// log cache +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +///@brief Satellite a per node holder of data. +Satellite::Satellite() = default; + + +Satellite::Satellite(SatelliteType satellite_type) + : satellite_type(satellite_type), + is_time(false), + selected(false), + has_mirror(false), + hidden(true), + amount(0.0), + angle(0.0), + steps(0) +{} + +Satellite::~Satellite() = default; + +///Calculate the time in curve_in with a size of A +//TODO: find a better place to it +double timeAtArcLength(double const A, Geom::Curve const &curve_in) +{ + if ( A == 0 || curve_in.isDegenerate()) { + return 0; + } + + Geom::D2 d2_in = curve_in.toSBasis(); + double t = 0; + double length_part = curve_in.length(); + if (A >= length_part || curve_in.isLineSegment()) { + if (length_part != 0) { + t = A / length_part; + } + } else if (!curve_in.isLineSegment()) { + std::vector t_roots = roots(Geom::arcLengthSb(d2_in) - A); + if (!t_roots.empty()) { + t = t_roots[0]; + } + } + return t; +} + +///Calculate the size in curve_in with a point at A +//TODO: find a better place to it +double arcLengthAt(double const A, Geom::Curve const &curve_in) +{ + if ( A == 0 || curve_in.isDegenerate()) { + return 0; + } + + double s = 0; + double length_part = curve_in.length(); + if (A > length_part || curve_in.isLineSegment()) { + s = (A * length_part); + } else if (!curve_in.isLineSegment()) { + Geom::Curve *curve = curve_in.portion(0.0, A); + s = curve->length(); + delete curve; + } + return s; +} + +///Convert a arc radius of a fillet/chamfer to his satellite length -point position where fillet/chamfer knot be on original curve +double Satellite::radToLen( + double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out) const +{ + double len = 0; + Geom::D2 d2_in = curve_in.toSBasis(); + Geom::D2 d2_out = curve_out.toSBasis(); + Geom::Piecewise > offset_curve0 = + Geom::Piecewise >(d2_in) + + rot90(unitVector(derivative(d2_in))) * (A); + Geom::Piecewise > offset_curve1 = + Geom::Piecewise >(d2_out) + + rot90(unitVector(derivative(d2_out))) * (A); + offset_curve0[0][0].normalize(); + offset_curve0[0][1].normalize(); + Geom::Path p0 = path_from_piecewise(offset_curve0, 0.1)[0]; + offset_curve1[0][0].normalize(); + offset_curve1[0][1].normalize(); + Geom::Path p1 = path_from_piecewise(offset_curve1, 0.1)[0]; + Geom::Crossings cs = Geom::crossings(p0, p1); + if (cs.size() > 0) { + Geom::Point cp = p0(cs[0].ta); + double p0pt = nearest_time(cp, curve_out); + len = arcLengthAt(p0pt, curve_out); + } else { + if (A > 0) { + len = radToLen(A * -1, curve_in, curve_out); + } + } + return len; +} + +///Convert a satellite length -point position where fillet/chamfer knot be on original curve- to a arc radius of fillet/chamfer +double Satellite::lenToRad( + double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out, + Satellite const previousSatellite) const +{ + double time_in = (previousSatellite).time(A, true, curve_in); + double time_out = timeAtArcLength(A, curve_out); + Geom::Point start_arc_point = curve_in.pointAt(time_in); + Geom::Point end_arc_point = curve_out.pointAt(time_out); + Geom::Curve *knot_curve1 = curve_in.portion(0, time_in); + Geom::Curve *knot_curve2 = curve_out.portion(time_out, 1); + Geom::CubicBezier const *cubic1 = dynamic_cast(&*knot_curve1); + Geom::Ray ray1(start_arc_point, curve_in.pointAt(1)); + if (cubic1) { + ray1.setPoints((*cubic1)[2], start_arc_point); + } + Geom::CubicBezier const *cubic2 = dynamic_cast(&*knot_curve2); + Geom::Ray ray2(curve_out.pointAt(0), end_arc_point); + if (cubic2) { + ray2.setPoints(end_arc_point, (*cubic2)[1]); + } + bool ccw_toggle = cross(curve_in.pointAt(1) - start_arc_point, + end_arc_point - start_arc_point) < 0; + double distance_arc = + Geom::distance(start_arc_point, middle_point(start_arc_point, end_arc_point)); + double angle = angle_between(ray1, ray2, ccw_toggle); + double divisor = std::sin(angle / 2.0); + if (divisor > 0) { + return distance_arc / divisor; + } + return 0; +} + +///Get the time position of the satellite in curve_in +double Satellite::time(Geom::Curve const &curve_in, bool inverse) const +{ + double t = amount; + if (!is_time) { + t = time(t, inverse, curve_in); + } else if (inverse) { + t = 1-t; + } + if (t > 1) { + t = 1; + } + return t; +} + +///Get the time from a length A in other curve, a boolean inverse given to reverse time +double Satellite::time(double A, bool inverse, + Geom::Curve const &curve_in) const +{ + if (A == 0 && inverse) { + return 1; + } + if (A == 0 && !inverse) { + return 0; + } + if (!inverse) { + return timeAtArcLength(A, curve_in); + } + double length_part = curve_in.length(); + A = length_part - A; + return timeAtArcLength(A, curve_in); +} + +///Get the length of the satellite in curve_in +double Satellite::arcDistance(Geom::Curve const &curve_in) const +{ + double s = amount; + if (is_time) { + s = arcLengthAt(s, curve_in); + } + return s; +} + +///Get the point position of the satellite +Geom::Point Satellite::getPosition(Geom::Curve const &curve_in, bool inverse) const +{ + double t = time(curve_in, inverse); + return curve_in.pointAt(t); +} + +///Set the position of the satellite from a given point P +void Satellite::setPosition(Geom::Point const p, Geom::Curve const &curve_in, bool inverse) +{ + Geom::Curve * curve = const_cast(&curve_in); + if (inverse) { + curve = curve->reverse(); + } + double A = Geom::nearest_time(p, *curve); + if (!is_time) { + A = arcLengthAt(A, *curve); + } + amount = A; +} + + +///Map a satellite type with gchar +void Satellite::setSatelliteType(gchar const *A) +{ + std::map gchar_map_to_satellite_type = + boost::assign::map_list_of("F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + std::map::iterator it = gchar_map_to_satellite_type.find(std::string(A)); + if (it != gchar_map_to_satellite_type.end()) { + satellite_type = it->second; + } +} + +///Map a gchar with satelliteType +gchar const *Satellite::getSatelliteTypeGchar() const +{ + std::map satellite_type_to_gchar_map = + boost::assign::map_list_of(FILLET, "F")(INVERSE_FILLET, "IF")(CHAMFER, "C")(INVERSE_CHAMFER, "IC")(INVALID_SATELLITE, "KO"); + return satellite_type_to_gchar_map.at(satellite_type); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-satellite.h b/src/helper/geom-satellite.h new file mode 100644 index 0000000..5fbcae1 --- /dev/null +++ b/src/helper/geom-satellite.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Satellite -- a per node holder of data. + *//* + * Authors: + * see git history + * Jabier Arraiza Cenoz + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SATELLITE_H +#define SEEN_SATELLITE_H + +#include +#include +#include <2geom/sbasis-geometric.h> +#include "util/enums.h" + + +enum SatelliteType { + FILLET = 0, //Fillet + INVERSE_FILLET, //Inverse Fillet + CHAMFER, //Chamfer + INVERSE_CHAMFER, //Inverse Chamfer + INVALID_SATELLITE // Invalid Satellite +}; +/** + * @brief Satellite a per node holder of data. + */ + +class Satellite { +public: + + Satellite(); + Satellite(SatelliteType satellite_type); + + virtual ~Satellite(); + void setIsTime(bool set_is_time) + { + is_time = set_is_time; + } + void setSelected(bool set_selected) + { + selected = set_selected; + } + void setHasMirror(bool set_has_mirror) + { + has_mirror = set_has_mirror; + } + void setHidden(bool set_hidden) + { + hidden = set_hidden; + } + void setAmount(double set_amount) + { + amount = set_amount; + } + void setAngle(double set_angle) + { + angle = set_angle; + } + void setSteps(size_t set_steps) + { + steps = set_steps; + } + double lenToRad(double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out, + Satellite const previousSatellite) const; + double radToLen(double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out) const; + + double time(Geom::Curve const &curve_in, bool inverse = false) const; + double time(double A, bool inverse, Geom::Curve const &curve_in) const; + double arcDistance(Geom::Curve const &curve_in) const; + + void setPosition(Geom::Point const p, Geom::Curve const &curve_in, bool inverse = false); + Geom::Point getPosition(Geom::Curve const &curve_in, bool inverse = false) const; + + void setSatelliteType(gchar const *A); + gchar const *getSatelliteTypeGchar() const; + SatelliteType satellite_type; + //The value stored could be a time value of the satellite in the curve or a length of distance to the node from the satellite + //"is_time" tells us if it's a time or length value + bool is_time; + bool selected; + bool has_mirror; + bool hidden; + //in "amount" we store the time or distance used in the satellite + double amount; + double angle; + size_t steps; +}; + +double timeAtArcLength(double const A, Geom::Curve const &curve_in); +double arcLengthAt(double const A, Geom::Curve const &curve_in); + +#endif // SEEN_SATELLITE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom.cpp b/src/helper/geom.cpp new file mode 100644 index 0000000..5e8e763 --- /dev/null +++ b/src/helper/geom.cpp @@ -0,0 +1,893 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Specific geometry functions for Inkscape, not provided my lib2geom. + * + * Author: + * Johan Engelen + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include <2geom/curves.h> +#include <2geom/sbasis-to-bezier.h> + +using Geom::X; +using Geom::Y; + +//################################################################################# +// BOUNDING BOX CALCULATIONS + +/* Fast bbox calculation */ +/* Thanks to Nathan Hurst for suggesting it */ +static void +cubic_bbox (Geom::Coord x000, Geom::Coord y000, Geom::Coord x001, Geom::Coord y001, Geom::Coord x011, Geom::Coord y011, Geom::Coord x111, Geom::Coord y111, Geom::Rect &bbox) +{ + Geom::Coord a, b, c, D; + + bbox[0].expandTo(x111); + bbox[1].expandTo(y111); + + // It already contains (x000,y000) and (x111,y111) + // All points of the Bezier lie in the convex hull of (x000,y000), (x001,y001), (x011,y011) and (x111,y111) + // So, if it also contains (x001,y001) and (x011,y011) we don't have to compute anything else! + // Note that we compute it for the X and Y range separately to make it easier to use them below + bool containsXrange = bbox[0].contains(x001) && bbox[0].contains(x011); + bool containsYrange = bbox[1].contains(y001) && bbox[1].contains(y011); + + /* + * xttt = s * (s * (s * x000 + t * x001) + t * (s * x001 + t * x011)) + t * (s * (s * x001 + t * x011) + t * (s * x011 + t * x111)) + * xttt = s * (s2 * x000 + s * t * x001 + t * s * x001 + t2 * x011) + t * (s2 * x001 + s * t * x011 + t * s * x011 + t2 * x111) + * xttt = s * (s2 * x000 + 2 * st * x001 + t2 * x011) + t * (s2 * x001 + 2 * st * x011 + t2 * x111) + * xttt = s3 * x000 + 2 * s2t * x001 + st2 * x011 + s2t * x001 + 2st2 * x011 + t3 * x111 + * xttt = s3 * x000 + 3s2t * x001 + 3st2 * x011 + t3 * x111 + * xttt = s3 * x000 + (1 - s) 3s2 * x001 + (1 - s) * (1 - s) * 3s * x011 + (1 - s) * (1 - s) * (1 - s) * x111 + * xttt = s3 * x000 + (3s2 - 3s3) * x001 + (3s - 6s2 + 3s3) * x011 + (1 - 2s + s2 - s + 2s2 - s3) * x111 + * xttt = (x000 - 3 * x001 + 3 * x011 - x111) * s3 + + * ( 3 * x001 - 6 * x011 + 3 * x111) * s2 + + * ( 3 * x011 - 3 * x111) * s + + * ( x111) + * xttt' = (3 * x000 - 9 * x001 + 9 * x011 - 3 * x111) * s2 + + * ( 6 * x001 - 12 * x011 + 6 * x111) * s + + * ( 3 * x011 - 3 * x111) + */ + + if (!containsXrange) { + a = 3 * x000 - 9 * x001 + 9 * x011 - 3 * x111; + b = 6 * x001 - 12 * x011 + 6 * x111; + c = 3 * x011 - 3 * x111; + + /* + * s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; + */ + if (fabs (a) < Geom::EPSILON) { + /* s = -c / b */ + if (fabs (b) > Geom::EPSILON) { + double s; + s = -c / b; + if ((s > 0.0) && (s < 1.0)) { + double t = 1.0 - s; + double xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + } + } else { + /* s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; */ + D = b * b - 4 * a * c; + if (D >= 0.0) { + Geom::Coord d, s, t, xttt; + /* Have solution */ + d = sqrt (D); + s = (-b + d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + s = (-b - d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + } + } + } + + if (!containsYrange) { + a = 3 * y000 - 9 * y001 + 9 * y011 - 3 * y111; + b = 6 * y001 - 12 * y011 + 6 * y111; + c = 3 * y011 - 3 * y111; + + if (fabs (a) < Geom::EPSILON) { + /* s = -c / b */ + if (fabs (b) > Geom::EPSILON) { + double s; + s = -c / b; + if ((s > 0.0) && (s < 1.0)) { + double t = 1.0 - s; + double yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + } + } else { + /* s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; */ + D = b * b - 4 * a * c; + if (D >= 0.0) { + Geom::Coord d, s, t, yttt; + /* Have solution */ + d = sqrt (D); + s = (-b + d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + s = (-b - d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + } + } + } +} + +Geom::OptRect +bounds_fast_transformed(Geom::PathVector const & pv, Geom::Affine const & t) +{ + return bounds_exact_transformed(pv, t); //use this as it is faster for now! :) +// return Geom::bounds_fast(pv * t); +} + +Geom::OptRect +bounds_exact_transformed(Geom::PathVector const & pv, Geom::Affine const & t) +{ + if (pv.empty()) + return Geom::OptRect(); + + Geom::Point initial = pv.front().initialPoint() * t; + Geom::Rect bbox(initial, initial); // obtain well defined bbox as starting point to unionWith + + for (const auto & it : pv) { + bbox.expandTo(it.initialPoint() * t); + + // don't loop including closing segment, since that segment can never increase the bbox + for (Geom::Path::const_iterator cit = it.begin(); cit != it.end_open(); ++cit) { + Geom::Curve const &c = *cit; + + unsigned order = 0; + if (Geom::BezierCurve const* b = dynamic_cast(&c)) { + order = b->order(); + } + + if (order == 1) { // line segment + bbox.expandTo(c.finalPoint() * t); + + // TODO: we can make the case for quadratics faster by degree elevating them to + // cubic and then taking the bbox of that. + + } else if (order == 3) { // cubic bezier + Geom::CubicBezier const &cubic_bezier = static_cast(c); + Geom::Point c0 = cubic_bezier[0] * t; + Geom::Point c1 = cubic_bezier[1] * t; + Geom::Point c2 = cubic_bezier[2] * t; + Geom::Point c3 = cubic_bezier[3] * t; + cubic_bbox(c0[0], c0[1], c1[0], c1[1], c2[0], c2[1], c3[0], c3[1], bbox); + } else { + // should handle all not-so-easy curves: + Geom::Curve *ctemp = cit->transformed(t); + bbox.unionWith( ctemp->boundsExact()); + delete ctemp; + } + } + } + //return Geom::bounds_exact(pv * t); + return bbox; +} + + + +static void +geom_line_wind_distance (Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1, Geom::Point const &pt, int *wind, Geom::Coord *best) +{ + Geom::Coord Ax, Ay, Bx, By, Dx, Dy, s; + Geom::Coord dist2; + + /* Find distance */ + Ax = x0; + Ay = y0; + Bx = x1; + By = y1; + Dx = x1 - x0; + Dy = y1 - y0; + const Geom::Coord Px = pt[X]; + const Geom::Coord Py = pt[Y]; + + if (best) { + s = ((Px - Ax) * Dx + (Py - Ay) * Dy) / (Dx * Dx + Dy * Dy); + if (s <= 0.0) { + dist2 = (Px - Ax) * (Px - Ax) + (Py - Ay) * (Py - Ay); + } else if (s >= 1.0) { + dist2 = (Px - Bx) * (Px - Bx) + (Py - By) * (Py - By); + } else { + Geom::Coord Qx, Qy; + Qx = Ax + s * Dx; + Qy = Ay + s * Dy; + dist2 = (Px - Qx) * (Px - Qx) + (Py - Qy) * (Py - Qy); + } + + if (dist2 < (*best * *best)) *best = sqrt (dist2); + } + + if (wind) { + /* Find wind */ + if ((Ax >= Px) && (Bx >= Px)) return; + if ((Ay >= Py) && (By >= Py)) return; + if ((Ay < Py) && (By < Py)) return; + if (Ay == By) return; + /* Ctach upper y bound */ + if (Ay == Py) { + if (Ax < Px) *wind -= 1; + return; + } else if (By == Py) { + if (Bx < Px) *wind += 1; + return; + } else { + Geom::Coord Qx; + /* Have to calculate intersection */ + Qx = Ax + Dx * (Py - Ay) / Dy; + if (Qx < Px) { + *wind += (Dy > 0.0) ? 1 : -1; + } + } + } +} + +static void +geom_cubic_bbox_wind_distance (Geom::Coord x000, Geom::Coord y000, + Geom::Coord x001, Geom::Coord y001, + Geom::Coord x011, Geom::Coord y011, + Geom::Coord x111, Geom::Coord y111, + Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *best, + Geom::Coord tolerance) +{ + Geom::Coord x0, y0, x1, y1, len2; + int needdist, needwind; + + const Geom::Coord Px = pt[X]; + const Geom::Coord Py = pt[Y]; + + needdist = 0; + needwind = 0; + + if (bbox) cubic_bbox (x000, y000, x001, y001, x011, y011, x111, y111, *bbox); + + x0 = std::min (x000, x001); + x0 = std::min (x0, x011); + x0 = std::min (x0, x111); + y0 = std::min (y000, y001); + y0 = std::min (y0, y011); + y0 = std::min (y0, y111); + x1 = std::max (x000, x001); + x1 = std::max (x1, x011); + x1 = std::max (x1, x111); + y1 = std::max (y000, y001); + y1 = std::max (y1, y011); + y1 = std::max (y1, y111); + + if (best) { + /* Quickly adjust to endpoints */ + len2 = (x000 - Px) * (x000 - Px) + (y000 - Py) * (y000 - Py); + if (len2 < (*best * *best)) *best = (Geom::Coord) sqrt (len2); + len2 = (x111 - Px) * (x111 - Px) + (y111 - Py) * (y111 - Py); + if (len2 < (*best * *best)) *best = (Geom::Coord) sqrt (len2); + + if (((x0 - Px) < *best) && ((y0 - Py) < *best) && ((Px - x1) < *best) && ((Py - y1) < *best)) { + /* Point is inside sloppy bbox */ + /* Now we have to decide, whether subdivide */ + /* fixme: (Lauris) */ + if (((y1 - y0) > 5.0) || ((x1 - x0) > 5.0)) { + needdist = 1; + } + } + } + if (!needdist && wind) { + if ((y1 >= Py) && (y0 < Py) && (x0 < Px)) { + /* Possible intersection at the left */ + /* Now we have to decide, whether subdivide */ + /* fixme: (Lauris) */ + if (((y1 - y0) > 5.0) || ((x1 - x0) > 5.0)) { + needwind = 1; + } + } + } + + if (needdist || needwind) { + Geom::Coord x00t, x0tt, xttt, x1tt, x11t, x01t; + Geom::Coord y00t, y0tt, yttt, y1tt, y11t, y01t; + Geom::Coord s, t; + + t = 0.5; + s = 1 - t; + + x00t = s * x000 + t * x001; + x01t = s * x001 + t * x011; + x11t = s * x011 + t * x111; + x0tt = s * x00t + t * x01t; + x1tt = s * x01t + t * x11t; + xttt = s * x0tt + t * x1tt; + + y00t = s * y000 + t * y001; + y01t = s * y001 + t * y011; + y11t = s * y011 + t * y111; + y0tt = s * y00t + t * y01t; + y1tt = s * y01t + t * y11t; + yttt = s * y0tt + t * y1tt; + + geom_cubic_bbox_wind_distance (x000, y000, x00t, y00t, x0tt, y0tt, xttt, yttt, pt, nullptr, wind, best, tolerance); + geom_cubic_bbox_wind_distance (xttt, yttt, x1tt, y1tt, x11t, y11t, x111, y111, pt, nullptr, wind, best, tolerance); + } else { + geom_line_wind_distance (x000, y000, x111, y111, pt, wind, best); + } +} + +static void +geom_curve_bbox_wind_distance(Geom::Curve const & c, Geom::Affine const &m, + Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox, + Geom::Point &p0) // pass p0 through as it represents the last endpoint added (the finalPoint of last curve) +{ + unsigned order = 0; + if (Geom::BezierCurve const* b = dynamic_cast(&c)) { + order = b->order(); + } + if (order == 1) { + Geom::Point pe = c.finalPoint() * m; + if (bbox) { + bbox->expandTo(pe); + } + if (dist || wind) { + if (wind) { // we need to pick fill, so do what we're told + geom_line_wind_distance (p0[X], p0[Y], pe[X], pe[Y], pt, wind, dist); + } else { // only stroke is being picked; skip this segment if it's totally outside the viewbox + Geom::Rect swept(p0, pe); + if (!viewbox || swept.intersects(*viewbox)) + geom_line_wind_distance (p0[X], p0[Y], pe[X], pe[Y], pt, wind, dist); + } + } + p0 = pe; + } + else if (order == 3) { + Geom::CubicBezier const& cubic_bezier = static_cast(c); + Geom::Point p1 = cubic_bezier[1] * m; + Geom::Point p2 = cubic_bezier[2] * m; + Geom::Point p3 = cubic_bezier[3] * m; + + // get approximate bbox from handles (convex hull property of beziers): + Geom::Rect swept(p0, p3); + swept.expandTo(p1); + swept.expandTo(p2); + + if (!viewbox || swept.intersects(*viewbox)) { // we see this segment, so do full processing + geom_cubic_bbox_wind_distance ( p0[X], p0[Y], + p1[X], p1[Y], + p2[X], p2[Y], + p3[X], p3[Y], + pt, + bbox, wind, dist, tolerance); + } else { + if (wind) { // if we need fill, we can just pretend it's a straight line + geom_line_wind_distance (p0[X], p0[Y], p3[X], p3[Y], pt, wind, dist); + } else { // otherwise, skip it completely + } + } + p0 = p3; + } else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + //recurse to convert the new path resulting from the sbasis to svgd + for (const auto & iter : sbasis_path) { + geom_curve_bbox_wind_distance(iter, m, pt, bbox, wind, dist, tolerance, viewbox, p0); + } + } +} + +/* Calculates... + and returns ... in *wind and the distance to ... in *dist. + Returns bounding box in *bbox if bbox!=NULL. + */ +void +pathv_matrix_point_bbox_wind_distance (Geom::PathVector const & pathv, Geom::Affine const &m, Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox) +{ + if (pathv.empty()) { + if (wind) *wind = 0; + if (dist) *dist = Geom::infinity(); + return; + } + + // remember last point of last curve + Geom::Point p0(0,0); + + // remembering the start of subpath + Geom::Point p_start(0,0); + bool start_set = false; + + for (const auto & it : pathv) { + + if (start_set) { // this is a new subpath + if (wind && (p0 != p_start)) // for correct fill picking, each subpath must be closed + geom_line_wind_distance (p0[X], p0[Y], p_start[X], p_start[Y], pt, wind, dist); + } + p0 = it.initialPoint() * m; + p_start = p0; + start_set = true; + if (bbox) { + bbox->expandTo(p0); + } + + // loop including closing segment if path is closed + for (Geom::Path::const_iterator cit = it.begin(); cit != it.end_default(); ++cit) { + geom_curve_bbox_wind_distance(*cit, m, pt, bbox, wind, dist, tolerance, viewbox, p0); + } + } + + if (start_set) { + if (wind && (p0 != p_start)) // for correct picking, each subpath must be closed + geom_line_wind_distance (p0[X], p0[Y], p_start[X], p_start[Y], pt, wind, dist); + } +} + +//################################################################################# + +/* + * Converts all segments in all paths to Geom::LineSegment or Geom::HLineSegment or + * Geom::VLineSegment or Geom::CubicBezier. + */ +Geom::PathVector +pathv_to_linear_and_cubic_beziers( Geom::PathVector const &pathv ) +{ + Geom::PathVector output; + + for (const auto & pit : pathv) { + output.push_back( Geom::Path() ); + output.back().setStitching(true); + output.back().start( pit.initialPoint() ); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::LineSegment l(cit->initialPoint(), cit->finalPoint()); + output.back().append(l); + } else { + Geom::BezierCurve const *curve = dynamic_cast(&*cit); + if (curve && curve->order() == 3) { + Geom::CubicBezier b((*curve)[0], (*curve)[1], (*curve)[2], (*curve)[3]); + output.back().append(b); + } else { + // convert all other curve types to cubicbeziers + Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(cit->toSBasis(), 0.1); + cubicbezier_path.close(false); + output.back().append(cubicbezier_path); + } + } + } + + output.back().close( pit.closed() ); + } + + return output; +} + +/* + * Converts all segments in all paths to Geom::LineSegment. There is an intermediate + * stage where some may be converted to beziers. maxdisp is the maximum displacement from + * the line segment to the bezier curve; ** maxdisp is not used at this moment **. + * + * This is NOT a terribly fast method, but it should give a solution close to the one with the + * fewest points. + */ +Geom::PathVector +pathv_to_linear( Geom::PathVector const &pathv, double /*maxdisp*/) +{ + Geom::PathVector output; + Geom::PathVector tmppath = pathv_to_linear_and_cubic_beziers(pathv); + + // Now all path segments are either already lines, or they are beziers. + + for (const auto & pit : tmppath) { + output.push_back( Geom::Path() ); + output.back().start( pit.initialPoint() ); + output.back().close( pit.closed() ); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::LineSegment ls(cit->initialPoint(), cit->finalPoint()); + output.back().append(ls); + } + else { /* all others must be Bezier curves */ + Geom::BezierCurve const *curve = dynamic_cast(&*cit); + std::vector bzrpoints = curve->controlPoints(); + Geom::Point A = bzrpoints[0]; + Geom::Point B = bzrpoints[1]; + Geom::Point C = bzrpoints[2]; + Geom::Point D = bzrpoints[3]; + std::vector pointlist; + pointlist.push_back(A); + recursive_bezier4( + A[X], A[Y], + B[X], B[Y], + C[X], C[Y], + D[X], D[Y], + pointlist, + 0); + pointlist.push_back(D); + Geom::Point r1 = pointlist[0]; + for (unsigned int i=1; i( pitCubic.initialPoint() ); + pitCubic.close(true); + } + for (Geom::Path::iterator cit = pitCubic.begin(); cit != pitCubic.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::CubicBezier b(cit->initialPoint(), cit->pointAt(0.3334) + Geom::Point(cubicGap,cubicGap), cit->finalPoint(), cit->finalPoint()); + output.back().append(b); + } else { + Geom::BezierCurve const *curve = dynamic_cast(&*cit); + if (curve && curve->order() == 3) { + Geom::CubicBezier b((*curve)[0], (*curve)[1], (*curve)[2], (*curve)[3]); + output.back().append(b); + } else { + // convert all other curve types to cubicbeziers + Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(cit->toSBasis(), 0.1); + output.back().append(cubicbezier_path); + } + } + } + } + + return output; +} + +//Study move to 2Geom +size_t +count_pathvector_nodes(Geom::PathVector const &pathv) { + size_t tot = 0; + for (auto subpath : pathv) { + tot += count_path_nodes(subpath); + } + return tot; +} +size_t count_path_nodes(Geom::Path const &path) +{ + size_t tot = path.size_closed(); + if (path.closed()) { + const Geom::Curve &closingline = path.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + tot -= 1; + } + } + return tot; +} + +// The next routine is modified from curv4_div::recursive_bezier from file agg_curves.cpp +//---------------------------------------------------------------------------- +// Anti-Grain Geometry (AGG) - Version 2.5 +// A high quality rendering engine for C++ +// Copyright (C) 2002-2006 Maxim Shemanarev +// Contact: mcseem@antigrain.com +// mcseemagg@yahoo.com +// http://antigrain.com +// +// AGG is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// AGG is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with AGG; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +//---------------------------------------------------------------------------- +void +recursive_bezier4(const double x1, const double y1, + const double x2, const double y2, + const double x3, const double y3, + const double x4, const double y4, + std::vector &m_points, + int level) + { + // some of these should be parameters, but do it this way for now. + const double curve_collinearity_epsilon = 1e-30; + const double curve_angle_tolerance_epsilon = 0.01; + double m_cusp_limit = 0.0; + double m_angle_tolerance = 0.0; + double m_approximation_scale = 1.0; + double m_distance_tolerance_square = 0.5 / m_approximation_scale; + m_distance_tolerance_square *= m_distance_tolerance_square; + enum curve_recursion_limit_e { curve_recursion_limit = 32 }; +#define calc_sq_distance(A,B,C,D) ((A-C)*(A-C) + (B-D)*(B-D)) + + if(level > curve_recursion_limit) + { + return; + } + + + // Calculate all the mid-points of the line segments + //---------------------- + double x12 = (x1 + x2) / 2; + double y12 = (y1 + y2) / 2; + double x23 = (x2 + x3) / 2; + double y23 = (y2 + y3) / 2; + double x34 = (x3 + x4) / 2; + double y34 = (y3 + y4) / 2; + double x123 = (x12 + x23) / 2; + double y123 = (y12 + y23) / 2; + double x234 = (x23 + x34) / 2; + double y234 = (y23 + y34) / 2; + double x1234 = (x123 + x234) / 2; + double y1234 = (y123 + y234) / 2; + + + // Try to approximate the full cubic curve by a single straight line + //------------------ + double dx = x4-x1; + double dy = y4-y1; + + double d2 = fabs(((x2 - x4) * dy - (y2 - y4) * dx)); + double d3 = fabs(((x3 - x4) * dy - (y3 - y4) * dx)); + double da1, da2, k; + + switch((int(d2 > curve_collinearity_epsilon) << 1) + + int(d3 > curve_collinearity_epsilon)) + { + case 0: + // All collinear OR p1==p4 + //---------------------- + k = dx*dx + dy*dy; + if(k == 0) + { + d2 = calc_sq_distance(x1, y1, x2, y2); + d3 = calc_sq_distance(x4, y4, x3, y3); + } + else + { + k = 1 / k; + da1 = x2 - x1; + da2 = y2 - y1; + d2 = k * (da1*dx + da2*dy); + da1 = x3 - x1; + da2 = y3 - y1; + d3 = k * (da1*dx + da2*dy); + if(d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1) + { + // Simple collinear case, 1---2---3---4 + // We can leave just two endpoints + return; + } + if(d2 <= 0) d2 = calc_sq_distance(x2, y2, x1, y1); + else if(d2 >= 1) d2 = calc_sq_distance(x2, y2, x4, y4); + else d2 = calc_sq_distance(x2, y2, x1 + d2*dx, y1 + d2*dy); + + if(d3 <= 0) d3 = calc_sq_distance(x3, y3, x1, y1); + else if(d3 >= 1) d3 = calc_sq_distance(x3, y3, x4, y4); + else d3 = calc_sq_distance(x3, y3, x1 + d3*dx, y1 + d3*dy); + } + if(d2 > d3) + { + if(d2 < m_distance_tolerance_square) + { + m_points.emplace_back(x2, y2); + return; + } + } + else + { + if(d3 < m_distance_tolerance_square) + { + m_points.emplace_back(x3, y3); + return; + } + } + break; + + case 1: + // p1,p2,p4 are collinear, p3 is significant + //---------------------- + if(d3 * d3 <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y4 - y3, x4 - x3) - atan2(y3 - y2, x3 - x2)); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + + if(da1 < m_angle_tolerance) + { + m_points.emplace_back(x2, y2); + m_points.emplace_back(x3, y3); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x3, y3); + return; + } + } + } + break; + + case 2: + // p1,p3,p4 are collinear, p2 is significant + //---------------------- + if(d2 * d2 <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y3 - y2, x3 - x2) - atan2(y2 - y1, x2 - x1)); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + + if(da1 < m_angle_tolerance) + { + m_points.emplace_back(x2, y2); + m_points.emplace_back(x3, y3); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x2, y2); + return; + } + } + } + break; + + case 3: + // Regular case + //----------------- + if((d2 + d3)*(d2 + d3) <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + // If the curvature doesn't exceed the distance_tolerance value + // we tend to finish subdivisions. + //---------------------- + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle & Cusp Condition + //---------------------- + k = atan2(y3 - y2, x3 - x2); + da1 = fabs(k - atan2(y2 - y1, x2 - x1)); + da2 = fabs(atan2(y4 - y3, x4 - x3) - k); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + if(da2 >= M_PI) da2 = 2*M_PI - da2; + + if(da1 + da2 < m_angle_tolerance) + { + // Finally we can stop the recursion + //---------------------- + m_points.emplace_back(x23, y23); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x2, y2); + return; + } + + if(da2 > m_cusp_limit) + { + m_points.emplace_back(x3, y3); + return; + } + } + } + break; + } + + // Continue subdivision + //---------------------- + recursive_bezier4(x1, y1, x12, y12, x123, y123, x1234, y1234, m_points, level + 1); + recursive_bezier4(x1234, y1234, x234, y234, x34, y34, x4, y4, m_points, level + 1); +} + +void +swap(Geom::Point &A, Geom::Point &B){ + Geom::Point tmp = A; + A = B; + B = tmp; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom.h b/src/helper/geom.h new file mode 100644 index 0000000..58f323e --- /dev/null +++ b/src/helper/geom.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_H +#define INKSCAPE_HELPER_GEOM_H + +/** + * @file + * Specific geometry functions for Inkscape, not provided my lib2geom. + */ +/* + * Author: + * Johan Engelen + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <2geom/rect.h> +#include <2geom/affine.h> + +Geom::OptRect bounds_fast_transformed(Geom::PathVector const & pv, Geom::Affine const & t); +Geom::OptRect bounds_exact_transformed(Geom::PathVector const & pv, Geom::Affine const & t); + +void pathv_matrix_point_bbox_wind_distance ( Geom::PathVector const & pathv, Geom::Affine const &m, Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox); + +size_t count_pathvector_nodes(Geom::PathVector const &pathv ); +size_t count_path_nodes(Geom::Path const &path); +Geom::PathVector pathv_to_linear_and_cubic_beziers( Geom::PathVector const &pathv ); +Geom::PathVector pathv_to_linear( Geom::PathVector const &pathv, double maxdisp ); +Geom::PathVector pathv_to_cubicbezier( Geom::PathVector const &pathv); +void recursive_bezier4(const double x1, const double y1, const double x2, const double y2, + const double x3, const double y3, const double x4, const double y4, + std::vector &pointlist, + int level); +void swap(Geom::Point &A, Geom::Point &B); +#endif // INKSCAPE_HELPER_GEOM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/gettext.cpp b/src/helper/gettext.cpp new file mode 100644 index 0000000..4c154a7 --- /dev/null +++ b/src/helper/gettext.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * helper functions for gettext + *//* + * Authors: + * see git history + * Patrick Storz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef _WIN32 +#include +#endif + +#ifdef ENABLE_BINRELOC +#include "prefix.h" +#endif + +#include +#include +#include + +namespace Inkscape { + +/** does all required gettext initialization and takes care of the respective locale directory paths */ +void initialize_gettext() { +#ifdef _WIN32 + gchar *datadir = g_win32_get_package_installation_directory_of_module(NULL); + + // obtain short path to executable dir and pass it + // to bindtextdomain (it doesn't understand UTF-8) + gchar *shortdatadir = g_win32_locale_filename_from_utf8(datadir); + gchar *localepath = g_build_filename(shortdatadir, PACKAGE_LOCALE_DIR, NULL); + bindtextdomain(GETTEXT_PACKAGE, localepath); + g_free(shortdatadir); + g_free(localepath); + + g_free(datadir); +#else +# ifdef ENABLE_BINRELOC + bindtextdomain(GETTEXT_PACKAGE, BR_LOCALEDIR("")); +# else + bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR_ABSOLUTE); +# endif +#endif + + // Allow the user to override the locale directory by setting + // the environment variable INKSCAPE_LOCALEDIR. + char const *inkscape_localedir = g_getenv("INKSCAPE_LOCALEDIR"); + if (inkscape_localedir != nullptr) { + bindtextdomain(GETTEXT_PACKAGE, inkscape_localedir); + } + + // common setup + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + textdomain(GETTEXT_PACKAGE); +} + +/** set gettext codeset to UTF8 */ +void bind_textdomain_codeset_utf8() { + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); +} + +/** set gettext codeset to codeset of the system console + * - on *nix this is typically the current locale, + * - on Windows we don't care and simply use UTF8 + * any conversion would need to happen in our console wrappers (see winconsole.cpp) anyway + * as we have no easy way of determining console encoding from inkscape/inkview.exe process; + * for now do something even easier - switch console encoding to UTF8 and be done with it! + * this also works nicely on MSYS consoles where UTF8 encoding is used by default, too */ +void bind_textdomain_codeset_console() { +#ifdef _WIN32 + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); +#else + std::string charset; + Glib::get_charset(charset); + bind_textdomain_codeset(GETTEXT_PACKAGE, charset.c_str()); +#endif +} + +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/helper/gettext.h b/src/helper/gettext.h new file mode 100644 index 0000000..e5745ee --- /dev/null +++ b/src/helper/gettext.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief helper functions for gettext + *//* + * Authors: + * see git history + * Patrick Storz + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GETTEXT_HELPER_H +#define SEEN_GETTEXT_HELPER_H + +namespace Inkscape { + void initialize_gettext(); + void bind_textdomain_codeset_utf8(); + void bind_textdomain_codeset_console(); +} + +#endif // SEEN_GETTEXT_HELPER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/helper/mathfns.h b/src/helper/mathfns.h new file mode 100644 index 0000000..3fa51a0 --- /dev/null +++ b/src/helper/mathfns.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ... some mathmatical functions + * + * Authors: + * Johan Engelen + * + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_MATHFNS_H +#define SEEN_INKSCAPE_UTIL_MATHFNS_H + +#include <2geom/point.h> + +namespace Inkscape { + +namespace Util { + +/** + * Returns area in triangle given by points; may be negative. + */ +inline double +triangle_area (Geom::Point p1, Geom::Point p2, Geom::Point p3) +{ + using Geom::X; + using Geom::Y; + return (p1[X]*p2[Y] + p1[Y]*p3[X] + p2[X]*p3[Y] - p2[Y]*p3[X] - p1[Y]*p2[X] - p1[X]*p3[Y]); +} + +/** + * \return x rounded to the nearest multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_nearest_multiple_plus(double x, double const c1, double const c0) +{ + return floor((x - c0) / c1 + .5) * c1 + c0; +} + +/** + * \return x rounded to the lower multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_lower_multiple_plus(double x, double const c1, double const c0 = 0) +{ + return floor((x - c0) / c1) * c1 + c0; +} + +/** + * \return x rounded to the upper multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_upper_multiple_plus(double x, double const c1, double const c0 = 0) +{ + return ceil((x - c0) / c1) * c1 + c0; +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/pixbuf-ops.cpp b/src/helper/pixbuf-ops.cpp new file mode 100644 index 0000000..4c5b969 --- /dev/null +++ b/src/helper/pixbuf-ops.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Helpers for SPItem -> gdk_pixbuf related stuff + * + * Authors: + * John Cliff + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2008 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> + +#include "helper/png-write.h" +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "document.h" +#include "object/sp-root.h" +#include "object/sp-defs.h" +#include "object/sp-use.h" +#include "util/units.h" +#include "inkscape.h" + +#include "helper/pixbuf-ops.h" + +#include + +// TODO look for copy-n-paste duplication of this function: +/** + * Hide all items except @item, recursively, skipping groups and defs. + */ +static void hide_other_items_recursively(SPObject *o, SPItem *i, unsigned dkey) +{ + SPItem *item = dynamic_cast(o); + if ( item + && !dynamic_cast(item) + && !dynamic_cast(item) + && !dynamic_cast(item) + && !dynamic_cast(item) + && (i != o) ) + { + item->invoke_hide(dkey); + } + + // recurse + if (i != o) { + for (auto& child: o->children) { + hide_other_items_recursively(&child, i, dkey); + } + } +} + + +// The following is a mutation of the flood fill code, the marker preview, and random other samplings. +// The dpi settings don't do anything yet, but I want them to, and was wanting to keep reasonably close +// to the call for the interface to the png writing. + +bool sp_export_jpg_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long bgcolor, double quality, SPItem* item) +{ + std::unique_ptr pixbuf( + sp_generate_internal_bitmap(doc, filename, x0, y0, x1, y1, + width, height, xdpi, ydpi, bgcolor, item)); + + gchar c[32]; + g_snprintf(c, 32, "%f", quality); + gboolean saved = gdk_pixbuf_save(pixbuf->getPixbufRaw(), filename, "jpeg", nullptr, "quality", c, NULL); + + return saved; +} + + +/** + generates a bitmap from given items + the bitmap is stored in RAM and not written to file + @param x0 area left in document coordinates + @param y0 area top in document coordinates + @param x1 area right in document coordinates + @param y1 area bottom in document coordinates + @param width bitmap width in pixels + @param height bitmap height in pixels + @param xdpi + @param ydpi + @return the created GdkPixbuf structure or NULL if no memory is allocable +*/ +Inkscape::Pixbuf *sp_generate_internal_bitmap(SPDocument *doc, gchar const */*filename*/, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long /*bgcolor*/, + SPItem *item_only) + +{ + if (width == 0 || height == 0) return nullptr; + + Inkscape::Pixbuf *inkpb = nullptr; + /* Create new drawing for offscreen rendering*/ + Inkscape::Drawing drawing; + drawing.setExact(true); + unsigned dkey = SPItem::display_key_new(1); + + doc->ensureUpToDate(); + + Geom::Rect screen=Geom::Rect(Geom::Point(x0,y0), Geom::Point(x1, y1)); + + Geom::Point origin = screen.min(); + + Geom::Scale scale(Inkscape::Util::Quantity::convert(xdpi, "px", "in"), Inkscape::Util::Quantity::convert(ydpi, "px", "in")); + Geom::Affine affine = scale * Geom::Translate(-origin * scale); + + /* Create ArenaItems and set transform */ + Inkscape::DrawingItem *root = doc->getRoot()->invoke_show( drawing, dkey, SP_ITEM_SHOW_DISPLAY); + root->setTransform(affine); + drawing.setRoot(root); + + // We show all and then hide all items we don't want, instead of showing only requested items, + // because that would not work if the shown item references something in defs + if (item_only) { + hide_other_items_recursively(doc->getRoot(), item_only, dkey); + // TODO: The following line forces 100% opacity as required by sp_asbitmap_render() in cairo-renderer.cpp + // Make it conditional if 'item_only' is ever used by other callers which need to retain opacity + item_only->get_arenaitem(dkey)->setOpacity(1.0); + } + + Geom::IntRect final_bbox = Geom::IntRect::from_xywh(0, 0, width, height); + drawing.update(final_bbox); + + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + Inkscape::DrawingContext dc(surface, Geom::Point(0,0)); + + // render items + drawing.render(dc, final_bbox, Inkscape::DrawingItem::RENDER_BYPASS_CACHE); + + inkpb = new Inkscape::Pixbuf(surface); + } + else + { + long long size = (long long) height * (long long) cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + g_warning("sp_generate_internal_bitmap: not enough memory to create pixel buffer. Need %lld.", size); + cairo_surface_destroy(surface); + } + doc->getRoot()->invoke_hide(dkey); + +// gdk_pixbuf_save (pixbuf, "C:\\temp\\internal.jpg", "jpeg", NULL, "quality","100", NULL); + + return inkpb; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/pixbuf-ops.h b/src/helper/pixbuf-ops.h new file mode 100644 index 0000000..d56579e --- /dev/null +++ b/src/helper/pixbuf-ops.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_PIXBUF_OPS_H__ +#define __SP_PIXBUF_OPS_H__ + +/* + * Helpers for SPItem -> gdk_pixbuf related stuff + * + * Authors: + * John Cliff + * + * Copyright (C) 2008 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +class SPDocument; +namespace Inkscape { class Pixbuf; } + + bool sp_export_jpg_file (SPDocument *doc, gchar const *filename, double x0, double y0, double x1, double y1, + unsigned int width, unsigned int height, double xdpi, double ydpi, unsigned long bgcolor, double quality, SPItem *item_only = nullptr); + +Inkscape::Pixbuf *sp_generate_internal_bitmap(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long bgcolor, SPItem *item_only = nullptr); + +#endif diff --git a/src/helper/png-write.cpp b/src/helper/png-write.cpp new file mode 100644 index 0000000..ba726bf --- /dev/null +++ b/src/helper/png-write.cpp @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PNG file format utilities + * + * Authors: + * Lauris Kaplinski + * Whoever wrote this example in libpng documentation + * Peter Bostrom + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <2geom/rect.h> +#include <2geom/transforms.h> + +#include + +#include "document.h" +#include "inkscape.h" +#include "png-write.h" +#include "preferences.h" +#include "rdf.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "io/sys.h" + +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "ui/interface.h" +#include "util/units.h" + +/* This is an example of how to use libpng to read and write PNG files. + * The file libpng.txt is much more verbose then this. If you have not + * read it, do so first. This was designed to be a starting point of an + * implementation. This is not officially part of libpng, and therefore + * does not require a copyright notice. + * + * This file does not currently compile, because it is missing certain + * parts, like allocating memory to hold an image. You will have to + * supply these parts to get it to compile. For an example of a minimal + * working PNG reader/writer, see pngtest.c, included in this distribution. + */ + +struct SPEBP { + unsigned long int width, height, sheight; + guint32 background; + Inkscape::Drawing *drawing; // it is assumed that all unneeded items are hidden + guchar *px; + unsigned (*status)(float, void *); + void *data; +}; + +/* write a png file */ + +struct SPPNGBD { + guchar const *px; + int rowstride; +}; + +/** + * A simple wrapper to list png_text. + */ +class PngTextList { +public: + PngTextList() : count(0), textItems(nullptr) {} + ~PngTextList(); + + void add(gchar const* key, gchar const* text); + gint getCount() {return count;} + png_text* getPtext() {return textItems;} + +private: + gint count; + png_text* textItems; +}; + +PngTextList::~PngTextList() { + for (gint i = 0; i < count; i++) { + if (textItems[i].key) { + g_free(textItems[i].key); + } + if (textItems[i].text) { + g_free(textItems[i].text); + } + } +} + +void PngTextList::add(gchar const* key, gchar const* text) +{ + if (count < 0) { + count = 0; + textItems = nullptr; + } + png_text* tmp = (count > 0) ? g_try_renew(png_text, textItems, count + 1): g_try_new(png_text, 1); + if (tmp) { + textItems = tmp; + count++; + + png_text* item = &(textItems[count - 1]); + item->compression = PNG_TEXT_COMPRESSION_NONE; + item->key = g_strdup(key); + item->text = g_strdup(text); + item->text_length = 0; +#ifdef PNG_iTXt_SUPPORTED + item->itxt_length = 0; + item->lang = nullptr; + item->lang_key = nullptr; +#endif // PNG_iTXt_SUPPORTED + } else { + g_warning("Unable to allocate array for %d PNG text data.", count); + textItems = nullptr; + count = 0; + } +} + +static bool +sp_png_write_rgba_striped(SPDocument *doc, + gchar const *filename, unsigned long int width, unsigned long int height, double xdpi, double ydpi, + int (* get_rows)(guchar const **rows, void **to_free, int row, int num_rows, void *data, int color_type, int bit_depth, int antialias), + void *data, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + g_return_val_if_fail(filename != nullptr, false); + g_return_val_if_fail(data != nullptr, false); + + struct SPEBP *ebp = (struct SPEBP *) data; + FILE *fp; + png_structp png_ptr; + png_infop info_ptr; + png_color_8 sig_bit; + png_uint_32 r; + + /* open the file */ + + Inkscape::IO::dump_fopen_call(filename, "M"); + fp = Inkscape::IO::fopen_utf8name(filename, "wb"); + if(fp == nullptr) return false; + + /* Create and initialize the png_struct with the desired error handler + * functions. If you want to use the default stderr and longjump method, + * you can supply NULL for the last three parameters. We also check that + * the library version is compatible with the one used at compile time, + * in case we are using dynamically linked libraries. REQUIRED. + */ + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + + if (png_ptr == nullptr) { + fclose(fp); + return false; + } + + /* Allocate/initialize the image information data. REQUIRED */ + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == nullptr) { + fclose(fp); + png_destroy_write_struct(&png_ptr, nullptr); + return false; + } + + /* Set error handling. REQUIRED if you aren't supplying your own + * error handling functions in the png_create_write_struct() call. + */ + if (setjmp(png_jmpbuf(png_ptr))) { + // If we get here, we had a problem reading the file + fclose(fp); + png_destroy_write_struct(&png_ptr, &info_ptr); + return false; + } + + /* set up the output control if you are using standard C streams */ + png_init_io(png_ptr, fp); + + /* Set the image information here. Width and height are up to 2^31, + * bit_depth is one of 1, 2, 4, 8, or 16, but valid values also depend on + * the color_type selected. color_type is one of PNG_COLOR_TYPE_GRAY, + * PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE, PNG_COLOR_TYPE_RGB, + * or PNG_COLOR_TYPE_RGB_ALPHA. interlace is either PNG_INTERLACE_NONE or + * PNG_INTERLACE_ADAM7, and the compression_type and filter_type MUST + * currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED + */ + + png_set_compression_level(png_ptr, zlib); + + png_set_IHDR(png_ptr, info_ptr, + width, + height, + bit_depth, + color_type, + interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + + if ((color_type&2) && bit_depth == 16) { + // otherwise, if we are dealing with a color image then + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + // if the image has an alpha channel then + if (color_type&4) + sig_bit.alpha = 8; + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + } + + PngTextList textList; + + textList.add("Software", "www.inkscape.org"); // Made by Inkscape comment + { + const gchar* pngToDc[] = {"Title", "title", + "Author", "creator", + "Description", "description", + //"Copyright", "", + "Creation Time", "date", + //"Disclaimer", "", + //"Warning", "", + "Source", "source" + //"Comment", "" + }; + for (size_t i = 0; i < G_N_ELEMENTS(pngToDc); i += 2) { + struct rdf_work_entity_t * entity = rdf_find_entity ( pngToDc[i + 1] ); + if (entity) { + gchar const* data = rdf_get_work_entity(doc, entity); + if (data && *data) { + textList.add(pngToDc[i], data); + } + } else { + g_warning("Unable to find entity [%s]", pngToDc[i + 1]); + } + } + + + struct rdf_license_t *license = rdf_get_license(doc); + if (license) { + if (license->name && license->uri) { + gchar* tmp = g_strdup_printf("%s %s", license->name, license->uri); + textList.add("Copyright", tmp); + g_free(tmp); + } else if (license->name) { + textList.add("Copyright", license->name); + } else if (license->uri) { + textList.add("Copyright", license->uri); + } + } + } + if (textList.getCount() > 0) { + png_set_text(png_ptr, info_ptr, textList.getPtext(), textList.getCount()); + } + + /* other optional chunks like cHRM, bKGD, tRNS, tIME, oFFs, pHYs, */ + /* note that if sRGB is present the cHRM chunk must be ignored + * on read and must be written in accordance with the sRGB profile */ + if(xdpi < 0.0254 ) xdpi = 0.0255; + if(ydpi < 0.0254 ) ydpi = 0.0255; + + png_set_pHYs(png_ptr, info_ptr, unsigned(xdpi / 0.0254 ), unsigned(ydpi / 0.0254 ), PNG_RESOLUTION_METER); + + /* Write the file header information. REQUIRED */ + png_write_info(png_ptr, info_ptr); + + /* Once we write out the header, the compression type on the text + * chunks gets changed to PNG_TEXT_COMPRESSION_NONE_WR or + * PNG_TEXT_COMPRESSION_zTXt_WR, so it doesn't get written out again + * at the end. + */ + + /* set up the transformations you want. Note that these are + * all optional. Only call them if you want them. + */ + + /* --- CUT --- */ + + /* The easiest way to write the image (you may have a different memory + * layout, however, so choose what fits your needs best). You need to + * use the first method if you aren't handling interlacing yourself. + */ + + png_bytep* row_pointers = new png_bytep[ebp->sheight]; + int number_of_passes = interlace ? png_set_interlace_handling(png_ptr) : 1; + + for(int i=0;i(height)) { + void *to_free; + int n = get_rows((unsigned char const **) row_pointers, &to_free, r, height-r, data, color_type, bit_depth, antialiasing); + if (!n) break; + png_write_rows(png_ptr, row_pointers, n); + g_free(to_free); + r += n; + } + } + + delete[] row_pointers; + + /* You can write optional chunks like tEXt, zTXt, and tIME at the end + * as well. + */ + + /* It is REQUIRED to call this to finish writing the rest of the file */ + png_write_end(png_ptr, info_ptr); + + /* if you allocated any text comments, free them here */ + + /* clean up after the write, and free any memory allocated */ + png_destroy_write_struct(&png_ptr, &info_ptr); + + /* close the file */ + fclose(fp); + + /* that's it */ + return true; +} + + +/** + * + */ +static int +sp_export_get_rows(guchar const **rows, void **to_free, int row, int num_rows, void *data, int color_type, int bit_depth, int antialiasing) +{ + struct SPEBP *ebp = (struct SPEBP *) data; + + if (ebp->status) { + if (!ebp->status((float) row / ebp->height, ebp->data)) return 0; + } + + num_rows = MIN(num_rows, static_cast(ebp->sheight)); + num_rows = MIN(num_rows, static_cast(ebp->height - row)); + + /* Set area of interest */ + // bbox is now set to the entire image to prevent discontinuities + // in the image when blur is used (the borders may still be a bit + // off, but that's less noticeable). + Geom::IntRect bbox = Geom::IntRect::from_xywh(0, row, ebp->width, num_rows); + + /* Update to renderable state */ + ebp->drawing->update(bbox); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, ebp->width); + unsigned char *px = g_new(guchar, num_rows * stride); + + cairo_surface_t *s = cairo_image_surface_create_for_data( + px, CAIRO_FORMAT_ARGB32, ebp->width, num_rows, stride); + Inkscape::DrawingContext dc(s, bbox.min()); + dc.setSource(ebp->background); + dc.setOperator(CAIRO_OPERATOR_SOURCE); + dc.paint(); + dc.setOperator(CAIRO_OPERATOR_OVER); + + /* Render */ + ebp->drawing->render(dc, bbox, 0, antialiasing); + cairo_surface_destroy(s); + + // PNG stores data as unpremultiplied big-endian RGBA, which means + // it's identical to the GdkPixbuf format. + convert_pixels_argb32_to_pixbuf(px, ebp->width, num_rows, stride); + + // If a custom bit depth or color type is asked, then convert rgb to grayscale, etc. + const guchar* new_data = pixbuf_to_png(rows, px, num_rows, ebp->width, stride, color_type, bit_depth); + *to_free = (void*) new_data; + free(px); + + return num_rows; +} + +/** + * Hide all items that are not listed in list, recursively, skipping groups and defs. + */ +static void hide_other_items_recursively(SPObject *o, const std::vector &list, unsigned dkey) +{ + if ( SP_IS_ITEM(o) + && !SP_IS_DEFS(o) + && !SP_IS_ROOT(o) + && !SP_IS_GROUP(o) + && list.end()==find(list.begin(),list.end(),o)) + { + SP_ITEM(o)->invoke_hide(dkey); + } + + // recurse + if (list.end()==find(list.begin(),list.end(),o)) { + for (auto& child: o->children) { + hide_other_items_recursively(&child, list, dkey); + } + } +} + + +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), + void *data, bool force_overwrite, + const std::vector &items_only, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + return sp_export_png_file(doc, filename, Geom::Rect(Geom::Point(x0,y0),Geom::Point(x1,y1)), + width, height, xdpi, ydpi, bgcolor, status, data, force_overwrite, items_only, interlace, color_type, bit_depth, zlib, antialiasing); +} + +/** + * Export an area to a PNG file + * + * @param area Area in document coordinates + */ +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + Geom::Rect const &area, + unsigned long width, unsigned long height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned (*status)(float, void *), + void *data, bool force_overwrite, + const std::vector &items_only, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + g_return_val_if_fail(doc != nullptr, EXPORT_ERROR); + g_return_val_if_fail(filename != nullptr, EXPORT_ERROR); + g_return_val_if_fail(width >= 1, EXPORT_ERROR); + g_return_val_if_fail(height >= 1, EXPORT_ERROR); + g_return_val_if_fail(!area.hasZeroArea(), EXPORT_ERROR); + + + if (!force_overwrite && !sp_ui_overwrite_file(filename)) { + // aborted overwrite + return EXPORT_ABORTED; + } + + doc->ensureUpToDate(); + + /* Calculate translation by transforming to document coordinates (flipping Y)*/ + Geom::Point translation = -area.min(); + + /* This calculation is only valid when assumed that (x0,y0)= area.corner(0) and (x1,y1) = area.corner(2) + * 1) a[0] * x0 + a[2] * y1 + a[4] = 0.0 + * 2) a[1] * x0 + a[3] * y1 + a[5] = 0.0 + * 3) a[0] * x1 + a[2] * y1 + a[4] = width + * 4) a[1] * x0 + a[3] * y0 + a[5] = height + * 5) a[1] = 0.0; + * 6) a[2] = 0.0; + * + * (1,3) a[0] * x1 - a[0] * x0 = width + * a[0] = width / (x1 - x0) + * (2,4) a[3] * y0 - a[3] * y1 = height + * a[3] = height / (y0 - y1) + * (1) a[4] = -a[0] * x0 + * (2) a[5] = -a[3] * y1 + */ + + Geom::Affine const affine(Geom::Translate(translation) + * Geom::Scale(width / area.width(), + height / area.height())); + + struct SPEBP ebp; + ebp.width = width; + ebp.height = height; + ebp.background = bgcolor; + + /* Create new drawing */ + Inkscape::Drawing drawing; + drawing.setExact(true); // export with maximum blur rendering quality + unsigned const dkey = SPItem::display_key_new(1); + + // Create ArenaItems and set transform + drawing.setRoot(doc->getRoot()->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY)); + drawing.root()->setTransform(affine); + ebp.drawing = &drawing; + + // We show all and then hide all items we don't want, instead of showing only requested items, + // because that would not work if the shown item references something in defs + if (!items_only.empty()) { + hide_other_items_recursively(doc->getRoot(), items_only, dkey); + } + + ebp.status = status; + ebp.data = data; + + bool write_status = false;; + + ebp.sheight = 64; + ebp.px = g_try_new(guchar, 4 * ebp.sheight * width); + + if (ebp.px) { + write_status = sp_png_write_rgba_striped(doc, filename, width, height, xdpi, ydpi, sp_export_get_rows, &ebp, interlace, color_type, bit_depth, zlib, antialiasing); + g_free(ebp.px); + } + + // Hide items, this releases arenaitem + doc->getRoot()->invoke_hide(dkey); + + return write_status ? EXPORT_OK : EXPORT_ERROR; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/png-write.h b/src/helper/png-write.h new file mode 100644 index 0000000..c11ee77 --- /dev/null +++ b/src/helper/png-write.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PNG_WRITE_H +#define SEEN_SP_PNG_WRITE_H + +/* + * PNG file format utilities + * + * Authors: + * Lauris Kaplinski + * Peter Bostrom + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include // Only for gchar. + +#include <2geom/forward.h> + +class SPDocument; +class SPItem; + +enum ExportResult { + EXPORT_ERROR = 0, + EXPORT_OK, + EXPORT_ABORTED +}; + +/** + * Export the given document as a Portable Network Graphics (PNG) file. + * + * @return EXPORT_OK if succeeded, EXPORT_ABORTED if no action was taken, EXPORT_ERROR (false) if an error occurred. + */ +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), void *data, bool force_overwrite = false, const std::vector &items_only = std::vector(), + bool interlace = false, int color_type = 6, int bit_depth = 8, int zlib = 6, int antialiasing = 2); + +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + Geom::Rect const &area, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), void *data, bool force_overwrite = false, const std::vector &items_only = std::vector(), + bool interlace = false, int color_type = 6, int bit_depth = 8, int zlib = 6, int antialiasing = 2); + +#endif // SEEN_SP_PNG_WRITE_H diff --git a/src/helper/sp-marshal.list b/src/helper/sp-marshal.list new file mode 100644 index 0000000..340d2c5 --- /dev/null +++ b/src/helper/sp-marshal.list @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# marshallers for inkscape +VOID:POINTER,UINT +BOOLEAN:POINTER +BOOLEAN:POINTER,UINT +BOOLEAN:POINTER,POINTER +INT:POINTER,POINTER +DOUBLE:POINTER,UINT +VOID:INT,INT +VOID:STRING,STRING diff --git a/src/helper/stock-items.cpp b/src/helper/stock-items.cpp new file mode 100644 index 0000000..ca96d6b --- /dev/null +++ b/src/helper/stock-items.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Stock-items + * + * Stock Item management code + * + * Authors: + * John Cliff + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright 2004 John Cliff + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_SS_VERBOSE + +#include + +#include "path-prefix.h" + +#include +#include "inkscape.h" + +#include "io/sys.h" +#include "stock-items.h" + +#include "object/sp-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-marker.h" +#include "object/sp-defs.h" + +static SPObject *sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc); +static SPObject *sp_marker_load_from_svg(gchar const *name, SPDocument *current_doc); +static SPObject *sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc); + + +// FIXME: these should be merged with the icon loading code so they +// can share a common file/doc cache. This function should just +// take the dir to look in, and the file to check for, and cache +// against that, rather than the existing copy/paste code seen here. + +static SPObject * sp_marker_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *markers = g_build_filename(INKSCAPE_MARKERSDIR, "/markers.svg", NULL); + if (Inkscape::IO::file_test(markers, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(markers, FALSE); + } + g_free(markers); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + if (!edoc && doc) { + /* Get the marker we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_MARKER(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *mark_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(mark_repr, nullptr); + SPObject *cloned_item = current_doc->getObjectByRepr(mark_repr); + Inkscape::GC::release(mark_repr); + return cloned_item; + } + } + return nullptr; +} + + +static SPObject * +sp_pattern_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *patterns = g_build_filename(INKSCAPE_PAINTDIR, "/patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(patterns, FALSE); + } + if (!doc) { + gchar *patterns = g_build_filename(CREATE_PAINTDIR, "/patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(patterns, FALSE); + } + g_free(patterns); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + } + if (!edoc && doc) { + /* Get the pattern we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_PATTERN(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *pat_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(pat_repr, nullptr); + Inkscape::GC::release(pat_repr); + return object; + } + } + return nullptr; +} + + +static SPObject * +sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *gradients = g_build_filename(INKSCAPE_PAINTDIR, "/gradients.svg", NULL); + if (Inkscape::IO::file_test(gradients, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(gradients, FALSE); + } + if (!doc) { + gchar *gradients = g_build_filename(CREATE_PAINTDIR, "/gradients.svg", NULL); + if (Inkscape::IO::file_test(gradients, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(gradients, FALSE); + } + g_free(gradients); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + } + if (!edoc && doc) { + /* Get the gradient we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_GRADIENT(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *pat_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(pat_repr, nullptr); + Inkscape::GC::release(pat_repr); + return object; + } + } + return nullptr; +} + +// get_stock_item returns a pointer to an instance of the desired stock object in the current doc +// if necessary it will import the object. Copes with name clashes through use of the inkscape:stockid property +// This should be set to be the same as the id in the library file. + +SPObject *get_stock_item(gchar const *urn, gboolean stock) +{ + g_assert(urn != nullptr); + + /* check its an inkscape URN */ + if (!strncmp (urn, "urn:inkscape:", 13)) { + + gchar const *e = urn + 13; + int a = 0; + gchar * name = g_strdup(e); + gchar *name_p = name; + while (*name_p != ':' && *name_p != '\0'){ + name_p++; + a++; + } + + if (*name_p ==':') { + name_p++; + } + + gchar * base = g_strndup(e, a); + + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPDefs *defs = doc->getDefs(); + if (!defs) { + g_free(base); + return nullptr; + } + SPObject *object = nullptr; + if (!strcmp(base, "marker") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_MARKER(&child)) + { + object = &child; + } + } + } + else if (!strcmp(base,"pattern") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_PATTERN(&child)) + { + object = &child; + } + } + } + else if (!strcmp(base,"gradient") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_GRADIENT(&child)) + { + object = &child; + } + } + } + + if (object == nullptr) { + + if (!strcmp(base, "marker")) { + object = sp_marker_load_from_svg(name_p, doc); + } + else if (!strcmp(base, "pattern")) { + object = sp_pattern_load_from_svg(name_p, doc); + } + else if (!strcmp(base, "gradient")) { + object = sp_gradient_load_from_svg(name_p, doc); + } + } + + g_free(base); + g_free(name); + + if (object) { + object->setAttribute("inkscape:isstock", "true"); + } + + return object; + } + + else { + + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *object = doc->getObjectById(urn); + + return object; + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/helper/stock-items.h b/src/helper/stock-items.h new file mode 100644 index 0000000..0d1bb20 --- /dev/null +++ b/src/helper/stock-items.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * John Cliff + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INK_STOCK_ITEMS_H +#define SEEN_INK_STOCK_ITEMS_H + +#include + +class SPObject; + +SPObject *get_stock_item(gchar const *urn, gboolean stock=FALSE); + +#endif // SEEN_INK_STOCK_ITEMS_H diff --git a/src/helper/verb-action.cpp b/src/helper/verb-action.cpp new file mode 100644 index 0000000..48fd8cd --- /dev/null +++ b/src/helper/verb-action.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Deprecated Gtk::Action that provides a widget for an Inkscape verb + */ +/* Authors: + * MenTaLguY + * Lauris Kaplinski + * bulia byak + * Frank Felfe + * John Cliff + * David Turner + * Josh Andler + * Jon A. Cruz + * Maximilian Albert + * Tavmjong Bah + * Abhishek Sharma + * Kris De Gussem + * Jabiertxo Arraiza + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "verb-action.h" + +#include + +#include + +#include "shortcuts.h" +#include "verbs.h" +#include "helper/action.h" +#include "ui/widget/button.h" +#include "widgets/toolbox.h" + +static GtkToolItem * sp_toolbox_button_item_new_from_verb_with_doubleclick( GtkWidget *t, GtkIconSize size, Inkscape::UI::Widget::ButtonType type, + Inkscape::Verb *verb, Inkscape::Verb *doubleclick_verb, + Inkscape::UI::View::View *view); + +GtkToolItem * sp_toolbox_button_item_new_from_verb_with_doubleclick(GtkWidget *t, GtkIconSize size, Inkscape::UI::Widget::ButtonType type, + Inkscape::Verb *verb, Inkscape::Verb *doubleclick_verb, + Inkscape::UI::View::View *view) +{ + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if (!action) { + return nullptr; + } + + SPAction *doubleclick_action; + if (doubleclick_verb) { + doubleclick_action = doubleclick_verb->get_action(Inkscape::ActionContext(view)); + } else { + doubleclick_action = nullptr; + } + + /* fixme: Handle sensitive/unsensitive */ + /* fixme: Implement Inkscape::UI::Widget::Button construction from action */ + auto b = Gtk::manage(new Inkscape::UI::Widget::Button(size, type, action, doubleclick_action)); + b->show(); + auto b_toolitem = Gtk::manage(new Gtk::ToolItem()); + b_toolitem->add(*b); + + unsigned int shortcut = sp_shortcut_get_primary(verb); + if (shortcut != GDK_KEY_VoidSymbol) { + gchar *key = sp_shortcut_get_label(shortcut); + gchar *tip = g_strdup_printf ("%s (%s)", action->tip, key); + if ( t ) { + gtk_toolbar_insert(GTK_TOOLBAR(t), b_toolitem->gobj(), -1); + b->set_tooltip_text(tip); + } + g_free(tip); + g_free(key); + } else { + if ( t ) { + gtk_toolbar_insert(GTK_TOOLBAR(t), b_toolitem->gobj(), -1); + b->set_tooltip_text(action->tip); + } + } + + return GTK_TOOL_ITEM(b_toolitem->gobj()); +} + +Glib::RefPtr VerbAction::create(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view) +{ + Glib::RefPtr result; + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if ( action ) { + //SPAction* action2 = verb2 ? verb2->get_action(Inkscape::ActionContext(view)) : 0; + result = Glib::RefPtr(new VerbAction(verb, verb2, view)); + } + + return result; +} + +VerbAction::VerbAction(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view) : + Gtk::Action(Glib::ustring(verb->get_id()), verb->get_image(), Glib::ustring(g_dpgettext2(nullptr, "ContextVerb", verb->get_name())), Glib::ustring(_(verb->get_tip()))), + verb(verb), + verb2(verb2), + view(view), + active(false) +{ +} + +VerbAction::~VerbAction() += default; + +Gtk::Widget* VerbAction::create_menu_item_vfunc() +{ + Gtk::Widget* widg = Gtk::Action::create_menu_item_vfunc(); +// g_message("create_menu_item_vfunc() = %p for '%s'", widg, verb->get_id()); + return widg; +} + +Gtk::Widget* VerbAction::create_tool_item_vfunc() +{ +// Gtk::Widget* widg = Gtk::Action::create_tool_item_vfunc(); + GtkIconSize toolboxSize = Inkscape::UI::ToolboxFactory::prefToSize("/toolbox/tools/small"); + GtkWidget* toolbox = nullptr; + auto holder = Glib::wrap(sp_toolbox_button_item_new_from_verb_with_doubleclick( toolbox, toolboxSize, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + verb, + verb2, + view )); + + auto button_widget = static_cast(holder->get_child()); + + if ( active ) { + button_widget->toggle_set_down(active); + } + button_widget->show_all(); + +// g_message("create_tool_item_vfunc() = %p for '%s'", holder, verb->get_id()); + return holder; +} + +void VerbAction::connect_proxy_vfunc(Gtk::Widget* proxy) +{ +// g_message("connect_proxy_vfunc(%p) for '%s'", proxy, verb->get_id()); + Gtk::Action::connect_proxy_vfunc(proxy); +} + +void VerbAction::disconnect_proxy_vfunc(Gtk::Widget* proxy) +{ +// g_message("disconnect_proxy_vfunc(%p) for '%s'", proxy, verb->get_id()); + Gtk::Action::disconnect_proxy_vfunc(proxy); +} + +void VerbAction::set_active(bool active) +{ + this->active = active; + Glib::SListHandle proxies = get_proxies(); + for (auto proxie : proxies) { + Gtk::ToolItem* ti = dynamic_cast(proxie); + if (ti) { + // *should* have one child that is the Inkscape::UI::Widget::Button + auto child = dynamic_cast(ti->get_child()); + if (child) { + child->toggle_set_down(active); + } + } + } +} + +void VerbAction::on_activate() +{ + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if ( action ) { + sp_action_perform(action, nullptr); + } + } +} + + diff --git a/src/helper/verb-action.h b/src/helper/verb-action.h new file mode 100644 index 0000000..1c0171f --- /dev/null +++ b/src/helper/verb-action.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * A deprecated Gtk::Action that provides a widget for an Inkscape + * Verb. + */ +/* Authors: + * MenTaLguY + * Lauris Kaplinski + * bulia byak + * Frank Felfe + * John Cliff + * David Turner + * Josh Andler + * Jon A. Cruz + * Maximilian Albert + * Tavmjong Bah + * Abhishek Sharma + * Kris De Gussem + * Jabiertxo Arraiza + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_VERB_ACTION_H +#define SEEN_VERB_ACTION_H + +#include + +namespace Inkscape { +class Verb; + +namespace UI { +namespace View { +class View; +} +} +} + +/** + * \brief A deprecated Gtk::Action that provides a widget for an Inkscape Verb + * + * \deprecated In new code, you should create a Gtk::ToolItem instead of using this + */ +class VerbAction : public Gtk::Action { +public: + static Glib::RefPtr create(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view); + + ~VerbAction() override; + virtual void set_active(bool active = true); + +protected: + Gtk::Widget* create_menu_item_vfunc() override; + Gtk::Widget* create_tool_item_vfunc() override; + + void connect_proxy_vfunc(Gtk::Widget* proxy) override; + void disconnect_proxy_vfunc(Gtk::Widget* proxy) override; + + void on_activate() override; + +private: + Inkscape::Verb* verb; + Inkscape::Verb* verb2; + Inkscape::UI::View::View *view; + bool active; + + VerbAction(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/id-clash.cpp b/src/id-clash.cpp new file mode 100644 index 0000000..5528ed0 --- /dev/null +++ b/src/id-clash.cpp @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Routines for resolving ID clashes when importing or pasting. + * + * Authors: + * Stephen Silver + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include + +#include "extract-uri.h" +#include "id-clash.h" + +#include "live_effects/lpeobject.h" +#include "object/sp-gradient.h" +#include "object/sp-object.h" +#include "object/sp-paint-server.h" +#include "object/sp-root.h" +#include "style.h" + +enum ID_REF_TYPE { REF_HREF, REF_STYLE, REF_SHAPES, REF_URL, REF_CLIPBOARD }; + +struct IdReference { + ID_REF_TYPE type; + SPObject *elem; + const char *attr; // property or href-like attribute +}; + +typedef std::map > refmap_type; + +typedef std::pair id_changeitem_type; +typedef std::list id_changelist_type; + +const char *href_like_attributes[] = { + "inkscape:connection-end", + "inkscape:connection-start", + "inkscape:href", + "inkscape:path-effect", + "inkscape:perspectiveID", + "inkscape:tiled-clone-of", + "xlink:href", +}; +#define NUM_HREF_LIKE_ATTRIBUTES (sizeof(href_like_attributes) / sizeof(*href_like_attributes)) + +const SPIPaint SPStyle::* SPIPaint_members[] = { + //&SPStyle::color, + reinterpret_cast(&SPStyle::fill), + reinterpret_cast(&SPStyle::stroke), +}; +const char* SPIPaint_properties[] = { + //"color", + "fill", + "stroke", +}; +#define NUM_SPIPAINT_PROPERTIES (sizeof(SPIPaint_properties) / sizeof(*SPIPaint_properties)) + +const SPIShapes SPStyle::* SPIShapes_members[] = { + reinterpret_cast(&SPStyle::shape_inside), + reinterpret_cast(&SPStyle::shape_subtract), +}; +const char *SPIShapes_properties[] = { + "shape-inside", + "shape-subtract", +}; +#define NUM_SPISHAPES_PROPERTIES (sizeof(SPIShapes_properties) / sizeof(*SPIShapes_properties)) + +const char* other_url_properties[] = { + "clip-path", + "color-profile", + "cursor", + "marker-end", + "marker-mid", + "marker-start", + "mask", +}; +#define NUM_OTHER_URL_PROPERTIES (sizeof(other_url_properties) / sizeof(*other_url_properties)) + +const char* clipboard_properties[] = { + //"color", + "fill", + "filter", + "stroke", + "marker-end", + "marker-mid", + "marker-start" +}; +#define NUM_CLIPBOARD_PROPERTIES (sizeof(clipboard_properties) / sizeof(*clipboard_properties)) + +/** + * Given an reference (idref), make it point to to_obj instead + */ +static void +fix_ref(IdReference const &idref, SPObject *to_obj, const char *old_id) { + switch (idref.type) { + case REF_HREF: { + gchar *new_uri = g_strdup_printf("#%s", to_obj->getId()); + idref.elem->setAttribute(idref.attr, new_uri); + g_free(new_uri); + break; + } + case REF_STYLE: { + sp_style_set_property_url(idref.elem, idref.attr, to_obj, false); + break; + } + case REF_SHAPES: { + SPCSSAttr* css = sp_repr_css_attr (idref.elem->getRepr(), "style"); + std::string prop = sp_repr_css_property (css, idref.attr, nullptr); + std::string oid; oid.append("url(#").append(old_id).append(")"); + auto pos = prop.find(oid); + if (pos != std::string::npos) { + std::string nid; nid.append("url(#").append(to_obj->getId()).append(")"); + prop.replace(pos, oid.size(), nid); + sp_repr_css_set_property (css, idref.attr, prop.c_str()); + sp_repr_css_set (idref.elem->getRepr(), css, "style"); + } else { + std::cerr << "Failed to switch id -- shouldn't happen" << std::endl; + } + break; + } + case REF_URL: { + gchar *url = g_strdup_printf("url(#%s)", to_obj->getId()); + idref.elem->setAttribute(idref.attr, url); + g_free(url); + break; + } + case REF_CLIPBOARD: { + SPCSSAttr *style = sp_repr_css_attr(idref.elem->getRepr(), "style"); + gchar *url = g_strdup_printf("url(#%s)", to_obj->getId()); + sp_repr_css_set_property(style, idref.attr, url); + g_free(url); + Glib::ustring style_string; + sp_repr_css_write_string(style, style_string); + idref.elem->setAttributeOrRemoveIfEmpty("style", style_string); + break; + } + } +} + +/** + * Build a table of places where IDs are referenced, for a given element. + * FIXME: There are some types of references not yet dealt with here + * (e.g., ID selectors in CSS stylesheets, and references in scripts). + */ +static void +find_references(SPObject *elem, refmap_type &refmap) +{ + if (elem->cloned) return; + Inkscape::XML::Node *repr_elem = elem->getRepr(); + if (!repr_elem) return; + if (repr_elem->type() != Inkscape::XML::ELEMENT_NODE) return; + + /* check for references in inkscape:clipboard elements */ + if (!std::strcmp(repr_elem->name(), "inkscape:clipboard")) { + SPCSSAttr *css = sp_repr_css_attr(repr_elem, "style"); + if (css) { + for (auto attr : clipboard_properties) { + const gchar *value = sp_repr_css_property(css, attr, nullptr); + if (value) { + auto uri = extract_uri(value); + if (uri[0] == '#') { + IdReference idref = { REF_CLIPBOARD, elem, attr }; + refmap[uri.c_str() + 1].push_back(idref); + } + } + } + + } + return; // nothing more to do for inkscape:clipboard elements + } + + /* check for xlink:href="#..." and similar */ + for (auto attr : href_like_attributes) { + const gchar *val = repr_elem->attribute(attr); + if (val && val[0] == '#') { + std::string id(val+1); + IdReference idref = { REF_HREF, elem, attr }; + refmap[id].push_back(idref); + } + } + + SPStyle *style = elem->style; + + /* check for url(#...) references in 'fill' or 'stroke' */ + for (unsigned i = 0; i < NUM_SPIPAINT_PROPERTIES; ++i) { + const SPIPaint SPStyle::*prop = SPIPaint_members[i]; + const SPIPaint *paint = &(style->*prop); + if (paint->isPaintserver() && paint->value.href) { + const SPObject *obj = paint->value.href->getObject(); + if (obj) { + const gchar *id = obj->getId(); + IdReference idref = { REF_STYLE, elem, SPIPaint_properties[i] }; + refmap[id].push_back(idref); + } + } + } + + /* check for shape-inside/shape-subtract that contain multiple url(#..) each */ + for (unsigned i = 0; i < NUM_SPISHAPES_PROPERTIES; ++i) { + const SPIShapes SPStyle::*prop = SPIShapes_members[i]; + const SPIShapes *shapes = &(style->*prop); + for (const auto &shape_id : shapes->shape_ids) { + IdReference idref = { REF_SHAPES, elem, SPIShapes_properties[i] }; + refmap[shape_id].push_back(idref); + } + } + + /* check for url(#...) references in 'filter' */ + const SPIFilter *filter = &(style->filter); + if (filter->href) { + const SPObject *obj = filter->href->getObject(); + if (obj) { + const gchar *id = obj->getId(); + IdReference idref = { REF_STYLE, elem, "filter" }; + refmap[id].push_back(idref); + } + } + + /* check for url(#...) references in markers */ + const gchar *markers[4] = { "", "marker-start", "marker-mid", "marker-end" }; + for (unsigned i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) { + const gchar *value = style->marker_ptrs[i]->value(); + if (value) { + auto uri = extract_uri(value); + if (uri[0] == '#') { + IdReference idref = { REF_STYLE, elem, markers[i] }; + refmap[uri.c_str() + 1].push_back(idref); + } + } + } + + /* check for other url(#...) references */ + for (auto attr : other_url_properties) { + const gchar *value = repr_elem->attribute(attr); + if (value) { + auto uri = extract_uri(value); + if (uri[0] == '#') { + IdReference idref = { REF_URL, elem, attr }; + refmap[uri.c_str() + 1].push_back(idref); + } + } + } + + // recurse + for (auto& child: elem->children) + { + find_references(&child, refmap); + } +} + +/** + * Change any IDs that clash with IDs in the current document, and make + * a list of those changes that will require fixing up references. + */ +static void +change_clashing_ids(SPDocument *imported_doc, SPDocument *current_doc, + SPObject *elem, refmap_type const &refmap, + id_changelist_type *id_changes) +{ + const gchar *id = elem->getId(); + bool fix_clashing_ids = true; + + if (id && current_doc->getObjectById(id)) { + // Choose a new ID. + // To try to preserve any meaningfulness that the original ID + // may have had, the new ID is the old ID followed by a hyphen + // and one or more digits. + + if (SP_IS_GRADIENT(elem)) { + SPObject *cd_obj = current_doc->getObjectById(id); + + if (cd_obj && SP_IS_GRADIENT(cd_obj)) { + SPGradient *cd_gr = SP_GRADIENT(cd_obj); + if ( cd_gr->isEquivalent(SP_GRADIENT(elem))) { + fix_clashing_ids = false; + } + } + } + + LivePathEffectObject *lpeobj = dynamic_cast(elem); + if (lpeobj) { + SPObject *cd_obj = current_doc->getObjectById(id); + LivePathEffectObject *cd_lpeobj = dynamic_cast(cd_obj); + if (cd_lpeobj && lpeobj->is_similar(cd_lpeobj)) { + fix_clashing_ids = false; + } + } + + if (fix_clashing_ids) { + std::string old_id(id); + std::string new_id(old_id + '-'); + for (;;) { + new_id += "0123456789"[std::rand() % 10]; + const char *str = new_id.c_str(); + if (current_doc->getObjectById(str) == nullptr && + imported_doc->getObjectById(str) == nullptr) break; + } + // Change to the new ID + + elem->setAttribute("id", new_id); + // Make a note of this change, if we need to fix up refs to it + if (refmap.find(old_id) != refmap.end()) + id_changes->push_back(id_changeitem_type(elem, old_id)); + } + } + + + // recurse + for (auto& child: elem->children) + { + change_clashing_ids(imported_doc, current_doc, &child, refmap, id_changes); + } +} + +/** + * Fix up references to changed IDs. + */ +static void +fix_up_refs(refmap_type const &refmap, const id_changelist_type &id_changes) +{ + id_changelist_type::const_iterator pp; + const id_changelist_type::const_iterator pp_end = id_changes.end(); + for (pp = id_changes.begin(); pp != pp_end; ++pp) { + SPObject *obj = pp->first; + refmap_type::const_iterator pos = refmap.find(pp->second); + std::list::const_iterator it; + const std::list::const_iterator it_end = pos->second.end(); + for (it = pos->second.begin(); it != it_end; ++it) { + fix_ref(*it, obj, pp->second.c_str()); + } + } +} + +/** + * This function resolves ID clashes between the document being imported + * and the current open document: IDs in the imported document that would + * clash with IDs in the existing document are changed, and references to + * those IDs are updated accordingly. + */ +void +prevent_id_clashes(SPDocument *imported_doc, SPDocument *current_doc) +{ + refmap_type refmap; + id_changelist_type id_changes; + SPObject *imported_root = imported_doc->getRoot(); + + find_references(imported_root, refmap); + change_clashing_ids(imported_doc, current_doc, imported_root, refmap, + &id_changes); + fix_up_refs(refmap, id_changes); +} + +/* + * Change any references of svg:def from_obj into to_obj + */ +void +change_def_references(SPObject *from_obj, SPObject *to_obj) +{ + refmap_type refmap; + SPDocument *current_doc = from_obj->document; + std::string old_id(from_obj->getId()); + + find_references(current_doc->getRoot(), refmap); + + refmap_type::const_iterator pos = refmap.find(old_id); + if (pos != refmap.end()) { + std::list::const_iterator it; + const std::list::const_iterator it_end = pos->second.end(); + for (it = pos->second.begin(); it != it_end; ++it) { + fix_ref(*it, to_obj, from_obj->getId()); + } + } +} + +/* + * Change the id of a SPObject to new_name + * If there is an id clash then rename to something similar + */ +void rename_id(SPObject *elem, Glib::ustring const &new_name) +{ + if (new_name.empty()){ + g_message("Invalid Id, will not change."); + return; + } + gchar *id = g_strdup(new_name.c_str()); //id is not empty here as new_name is check to be not empty + g_strcanon (id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_'); + Glib::ustring new_name2 = id; //will not fail as id can not be NULL, see length check on new_name + if (!isalnum (new_name2[0])) { + g_message("Invalid Id, will not change."); + g_free (id); + return; + } + + SPDocument *current_doc = elem->document; + refmap_type refmap; + find_references(current_doc->getRoot(), refmap); + + std::string old_id(elem->getId()); + if (current_doc->getObjectById(id)) { + // Choose a new ID. + // To try to preserve any meaningfulness that the original ID + // may have had, the new ID is the old ID followed by a hyphen + // and one or more digits. + new_name2 += '-'; + for (;;) { + new_name2 += "0123456789"[std::rand() % 10]; + if (current_doc->getObjectById(new_name2) == nullptr) + break; + } + } + g_free (id); + // Change to the new ID + elem->setAttribute("id", new_name2); + // Make a note of this change, if we need to fix up refs to it + id_changelist_type id_changes; + if (refmap.find(old_id) != refmap.end()) { + id_changes.push_back(id_changeitem_type(elem, old_id)); + } + + fix_up_refs(refmap, id_changes); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/id-clash.h b/src/id-clash.h new file mode 100644 index 0000000..1fe1af8 --- /dev/null +++ b/src/id-clash.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_ID_CLASH_H +#define SEEN_ID_CLASH_H + +#include "document.h" + +void prevent_id_clashes(SPDocument *imported_doc, SPDocument *current_doc); +void rename_id(SPObject *elem, Glib::ustring const &newname); +void change_def_references(SPObject *replace_obj, SPObject *with_obj); + +#endif /* !SEEN_ID_CLASH_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/include/CMakeLists.txt b/src/include/CMakeLists.txt new file mode 100644 index 0000000..5e08ebd --- /dev/null +++ b/src/include/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(include_SRC + glibmm_version.h + gtkmm_version.h + macros.h +) + +add_inkscape_source("${io_SRC}") diff --git a/src/include/README b/src/include/README new file mode 100644 index 0000000..336b3e2 --- /dev/null +++ b/src/include/README @@ -0,0 +1,3 @@ +Configuration, defines, macros and similar that do not fit anywhere else. + +Try to keep this to a minimum and move to more suitable locations whenever possible. diff --git a/src/include/glibmm_version.h b/src/include/glibmm_version.h new file mode 100644 index 0000000..39bf2c6 --- /dev/null +++ b/src/include/glibmm_version.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * macro for checking glibmm version + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2020 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GLIBMM_VERSION +#define SEEN_GLIBMM_VERSION + +#include + +#ifndef GLIBMM_CHECK_VERSION + +#if !defined(GLIBMM_MAJOR_VERSION) || !defined(GLIBMM_MINOR_VERSION) || !defined(GLIBMM_MICRO_VERSION) + #error "Missing defines for glibmm version (GLIBMM_MAJOR_VERSION / GLIBMM_MINOR_VERSION / GLIBMM_MICRO_VERSION)" +#endif + +/** + * Check glibmm version + * + * This is adapted from the upstream gtk macro for use with glibmm + * + * @major: major version (e.g. 1 for version 1.2.5) + * @minor: minor version (e.g. 2 for version 1.2.5) + * @micro: micro version (e.g. 5 for version 1.2.5) + * + * Returns %TRUE if the version of the glibmm header files + * is the same as or newer than the passed-in version. + * + * Returns: %TRUE if glibmm headers are new enough + */ +#define GLIBMM_CHECK_VERSION(major,minor,micro) \ + (GLIBMM_MAJOR_VERSION > (major) || \ + (GLIBMM_MAJOR_VERSION == (major) && GLIBMM_MINOR_VERSION > (minor)) || \ + (GLIBMM_MAJOR_VERSION == (major) && GLIBMM_MINOR_VERSION == (minor) && \ + GLIBMM_MICRO_VERSION >= (micro))) + +#endif // #ifndef GLIBMM_CHECK_VERSION + +#endif // SEEN_GLIBMM_VERSION diff --git a/src/include/gtkmm_version.h b/src/include/gtkmm_version.h new file mode 100644 index 0000000..a23223c --- /dev/null +++ b/src/include/gtkmm_version.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * macro for checking gtkmm version + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GTKMM_VERSION +#define SEEN_GTKMM_VERSION + +#include + +#ifndef GTKMM_CHECK_VERSION + +#if !defined(GTKMM_MAJOR_VERSION) || !defined(GTKMM_MINOR_VERSION) || !defined(GTKMM_MICRO_VERSION) + #error "Missing defines for gtkmm version (GTKMM_MAJOR_VERSION / GTKMM_MINOR_VERSION / GTKMM_MICRO_VERSION)" +#endif + +/** + * Check GtkMM version + * + * This is adapted from the upstream Gtk+ macro for use with GtkMM + * + * @major: major version (e.g. 1 for version 1.2.5) + * @minor: minor version (e.g. 2 for version 1.2.5) + * @micro: micro version (e.g. 5 for version 1.2.5) + * + * Returns %TRUE if the version of the GTK+ header files + * is the same as or newer than the passed-in version. + * + * Returns: %TRUE if GTK+ headers are new enough + */ +#define GTKMM_CHECK_VERSION(major,minor,micro) \ + (GTKMM_MAJOR_VERSION > (major) || \ + (GTKMM_MAJOR_VERSION == (major) && GTKMM_MINOR_VERSION > (minor)) || \ + (GTKMM_MAJOR_VERSION == (major) && GTKMM_MINOR_VERSION == (minor) && \ + GTKMM_MICRO_VERSION >= (micro))) + +#endif // #ifndef GTKMM_CHECK_VERSION + +#endif // SEEN_GTKMM_VERSION diff --git a/src/include/macros.h b/src/include/macros.h new file mode 100644 index 0000000..aa1719d --- /dev/null +++ b/src/include/macros.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_MACROS_H +#define SEEN_MACROS_H + +/** + * Useful macros for inkscape + * + * Author: + * Lauris Kaplinski + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// I'm of the opinion that this file should be removed, so I will in the future take the necessary steps to wipe it out. +// Macros are not in general bad, but these particular ones are rather ugly. --Liam + +#define sp_signal_disconnect_by_data(o,d) g_signal_handlers_disconnect_matched(o, G_SIGNAL_MATCH_DATA, 0, 0, 0, 0, d) + +// "primary" modifier: Ctrl on Linux/Windows and Cmd on macOS. +// note: Could query this at runtime with +// `gdk_keymap_get_modifier_mask(..., GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR)` +#ifdef GDK_WINDOWING_QUARTZ +#define INK_GDK_PRIMARY_MASK GDK_MOD2_MASK +#else +#define INK_GDK_PRIMARY_MASK GDK_CONTROL_MASK +#endif + +// all modifiers used by Inkscape +#define INK_GDK_MODIFIER_MASK (GDK_SHIFT_MASK | INK_GDK_PRIMARY_MASK | GDK_MOD1_MASK) + +// keyboard modifiers in an event +#define MOD__SHIFT(event) ((event)->key.state & GDK_SHIFT_MASK) +#define MOD__CTRL(event) ((event)->key.state & INK_GDK_PRIMARY_MASK) +#define MOD__ALT(event) ((event)->key.state & GDK_MOD1_MASK) +#define MOD__SHIFT_ONLY(event) (((event)->key.state & INK_GDK_MODIFIER_MASK) == GDK_SHIFT_MASK) +#define MOD__CTRL_ONLY(event) (((event)->key.state & INK_GDK_MODIFIER_MASK) == INK_GDK_PRIMARY_MASK) +#define MOD__ALT_ONLY(event) (((event)->key.state & INK_GDK_MODIFIER_MASK) == GDK_MOD1_MASK) + +#endif // SEEN_MACROS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/include/source_date_epoch.h b/src/include/source_date_epoch.h new file mode 100644 index 0000000..9080526 --- /dev/null +++ b/src/include/source_date_epoch.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Functions to parse the "SOURCE_DATE_EPOCH" environment variable for reproducible build hacks, see + * https://reproducible-builds.org/docs/source-date-epoch/ + *//* + * Authors: + * Patrick Storz + * + * Copyright (C) 2019 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SOURCE_DATE_EPOCH +#define SEEN_SOURCE_DATE_EPOCH + +#include +#include +#include +#include + +namespace ReproducibleBuilds { + +/** parse current time from SOURCE_DATE_EPOCH environment variable + * + * \return current time (or zero if SOURCE_DATE_EPOCH unset) + */ +time_t now() +{ + time_t now = 0; + + char *source_date_epoch = std::getenv("SOURCE_DATE_EPOCH"); + if (source_date_epoch) { + std::istringstream iss(source_date_epoch); + iss >> now; + if (iss.fail() || !iss.eof()) { + std::cerr << "Error: Cannot parse SOURCE_DATE_EPOCH as integer\n"; + exit(27); + } + } + + return now; +} + +/** like ReproducibleBuilds::now() but returns a ISO 8601 formatted string + * + * \return current time as ISO 8601 formatted string (or empty string if SOURCE_DATE_EPOCH unset) + */ +Glib::ustring now_iso_8601() +{ + Glib::ustring now_formatted; + + time_t now = ReproducibleBuilds::now(); + if (now) { + const tm *now_struct; + char buffer[25]; + + now_struct = gmtime(&now); + if (strftime(buffer, 25, "%Y-%m-%dT%H:%M:%S", now_struct)) { + now_formatted = buffer; + } + } + + return now_formatted; +} + +} // namespace ReproducibleBuilds + +#endif // SEEN_SOURCE_DATE_EPOCH diff --git a/src/inkgc/CMakeLists.txt b/src/inkgc/CMakeLists.txt new file mode 100644 index 0000000..f0ec2ff --- /dev/null +++ b/src/inkgc/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(libgc_SRC + gc.cpp + + # ------- + # Headers + gc-alloc.h + ../gc-anchored.h + gc-core.h + gc-managed.h + gc-soft-ptr.h +) + +add_inkscape_lib(gc_LIB "${libgc_SRC}") +# add_inkscape_source("${libgc_SRC}") diff --git a/src/inkgc/README b/src/inkgc/README new file mode 100644 index 0000000..c1104bc --- /dev/null +++ b/src/inkgc/README @@ -0,0 +1,8 @@ + + +This directory contains code related to the Boehm Garbage Collector. + +To do: + +* Replace this code with C++11 smart pointers. + diff --git a/src/inkgc/gc-alloc.h b/src/inkgc/gc-alloc.h new file mode 100644 index 0000000..8935c12 --- /dev/null +++ b/src/inkgc/gc-alloc.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::GC::Alloc - GC-aware STL allocator + *//* + * Authors: + * see git history + * MenTaLguY + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_ALLOC_H +#define SEEN_INKSCAPE_GC_ALLOC_H + +#include +#include +#include "inkgc/gc-core.h" + +namespace Inkscape { + +namespace GC { + +template +class Alloc { +public: + typedef T value_type; + typedef T *pointer; + typedef T const *const_pointer; + typedef T &reference; + typedef T const &const_reference; + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + + template + struct rebind { typedef Alloc other; }; + + Alloc() = default; + template Alloc(Alloc const &) {} + + pointer address(reference r) { return &r; } + const_pointer address(const_reference r) { return &r; } + + size_type max_size() const { + return std::numeric_limits::max() / sizeof(T); + } + + pointer allocate(size_type count, void const * =nullptr) { + return static_cast(::operator new(count * sizeof(T), SCANNED, collect)); + } + + void construct(pointer p, const_reference value) { + new (static_cast(p)) T(value); + } + void destroy(pointer p) { p->~T(); } + + void deallocate(pointer p, size_type) { ::operator delete(p, GC); } +}; + +// allocators with the same collection policy are interchangeable + +template +bool operator==(Alloc const &, Alloc const &) { + return collect1 == collect2; +} + +template +bool operator!=(Alloc const &, Alloc const &) { + return collect1 != collect2; +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkgc/gc-core.h b/src/inkgc/gc-core.h new file mode 100644 index 0000000..cbd4b7e --- /dev/null +++ b/src/inkgc/gc-core.h @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Wrapper for Boehm GC + */ +/* Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_CORE_H +#define SEEN_INKSCAPE_GC_CORE_H + +#include +#include + +# include + +namespace Inkscape { +namespace GC { + +enum ScanPolicy { + SCANNED, + ATOMIC +}; + +enum CollectionPolicy { + AUTO, + MANUAL +}; + +enum Delete { + GC +}; + +typedef void (*CleanupFunc)(void *mem, void *data); + +struct Ops { + void (*do_init)(); + void *(*malloc)(std::size_t size); + void *(*malloc_atomic)(std::size_t size); + void *(*malloc_uncollectable)(std::size_t size); + void *(*malloc_atomic_uncollectable)(std::size_t size); + void *(*base)(void *ptr); + void (*register_finalizer_ignore_self)(void *base, + CleanupFunc func, void *data, + CleanupFunc *old_func, + void **old_data); + int (*general_register_disappearing_link)(void **p_ptr, + void const *base); + int (*unregister_disappearing_link)(void **p_ptr); + std::size_t (*get_heap_size)(); + std::size_t (*get_free_bytes)(); + void (*gcollect)(); + void (*enable)(); + void (*disable)(); + void (*free)(void *ptr); +}; + +struct Core { +public: + static void init(); + static inline void *malloc(std::size_t size) { + return _ops.malloc(size); + } + static inline void *malloc_atomic(std::size_t size) { + return _ops.malloc_atomic(size); + } + static inline void *malloc_uncollectable(std::size_t size) { + return _ops.malloc_uncollectable(size); + } + static inline void *malloc_atomic_uncollectable(std::size_t size) { + return _ops.malloc_atomic_uncollectable(size); + } + static inline void *base(void *ptr) { + return _ops.base(ptr); + } + static inline void register_finalizer_ignore_self(void *base, + CleanupFunc func, + void *data, + CleanupFunc *old_func, + void **old_data) + { + return _ops.register_finalizer_ignore_self(base, func, data, + old_func, old_data); + } + static inline int general_register_disappearing_link(void **p_ptr, + void *base) + { + return _ops.general_register_disappearing_link(p_ptr, base); + } + static inline int unregister_disappearing_link(void **p_ptr) { + return _ops.unregister_disappearing_link(p_ptr); + } + static inline std::size_t get_heap_size() { + return _ops.get_heap_size(); + } + static inline std::size_t get_free_bytes() { + return _ops.get_free_bytes(); + } + static inline void gcollect() { + _ops.gcollect(); + } + static inline void enable() { + _ops.enable(); + } + static inline void disable() { + _ops.disable(); + } + static inline void free(void *ptr) { + return _ops.free(ptr); + } +private: + static Ops _ops; +}; + +inline void init() { + Core::init(); +} + +void request_early_collection(); + +} +} + +inline void *operator new(std::size_t size, + Inkscape::GC::ScanPolicy scan, + Inkscape::GC::CollectionPolicy collect, + Inkscape::GC::CleanupFunc cleanup=nullptr, + void *data=nullptr) +{ + using namespace Inkscape::GC; + + void *mem; + if ( collect == AUTO ) { + if ( scan == SCANNED ) { + mem = Core::malloc(size); + } else { + mem = Core::malloc_atomic(size); + } + } else { + if ( scan == SCANNED ) { + mem = Core::malloc_uncollectable(size); + } else { + mem = Core::malloc_atomic_uncollectable(size); + } + } + if (!mem) { + throw std::bad_alloc(); + } + if (cleanup) { + Core::register_finalizer_ignore_self(mem, cleanup, data, nullptr, nullptr); + } + return mem; +} + +inline void *operator new(std::size_t size, + Inkscape::GC::ScanPolicy scan, + Inkscape::GC::CleanupFunc cleanup=nullptr, + void *data=nullptr) +{ + return operator new(size, scan, Inkscape::GC::AUTO, cleanup, data); +} + +inline void *operator new[](std::size_t size, + Inkscape::GC::ScanPolicy scan, + Inkscape::GC::CollectionPolicy collect, + Inkscape::GC::CleanupFunc cleanup=nullptr, + void *data=nullptr) +{ + return operator new(size, scan, collect, cleanup, data); +} + +inline void *operator new[](std::size_t size, + Inkscape::GC::ScanPolicy scan, + Inkscape::GC::CleanupFunc cleanup=nullptr, + void *data=nullptr) +{ + return operator new[](size, scan, Inkscape::GC::AUTO, cleanup, data); +} + +inline void operator delete(void *mem, Inkscape::GC::Delete) { + Inkscape::GC::Core::free(mem); +} + +inline void operator delete[](void *mem, Inkscape::GC::Delete) { + operator delete(mem, Inkscape::GC::GC); +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkgc/gc-managed.h b/src/inkgc/gc-managed.h new file mode 100644 index 0000000..2c2ef7c --- /dev/null +++ b/src/inkgc/gc-managed.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Base class for GC-managed objects + *//* + * Authors: + * see git history + * MenTaLguY + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_MANAGED_H +#define SEEN_INKSCAPE_GC_MANAGED_H + +#include "inkgc/gc-core.h" + +namespace Inkscape { + +namespace GC { + +/** @brief A base class for objects for whom the normal new and delete + * operators should use the garbage-collected allocator + */ +template +class Managed { +public: + void *operator new(std::size_t size, + ScanPolicy scan=default_scan, + CollectionPolicy collect=default_collect) + { + return ::operator new(size, scan, collect); + } + + void *operator new[](std::size_t size, + ScanPolicy scan=default_scan, + CollectionPolicy collect=default_collect) + { + return ::operator new[](size, scan, collect); + } + + void operator delete(void *p) { return ::operator delete(p, GC); } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkgc/gc-soft-ptr.h b/src/inkgc/gc-soft-ptr.h new file mode 100644 index 0000000..b245d81 --- /dev/null +++ b/src/inkgc/gc-soft-ptr.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * MenTaLguY + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_GC_SOFT_PTR_H +#define SEEN_INKSCAPE_GC_SOFT_PTR_H + +#include "inkgc/gc-core.h" + +namespace Inkscape { + +namespace GC { + +/** + * A class for pointers which can be automatically cleared to break + * finalization cycles. + */ +template +class soft_ptr { +public: + soft_ptr(T *pointer=nullptr) : _pointer(pointer) { + _register(); + } + + operator T *() const { return static_cast(_pointer); } + T &operator*() const { return *static_cast(_pointer); } + T *operator->() const { return static_cast(_pointer); } + T &operator[](int i) const { return static_cast(_pointer)[i]; } + + soft_ptr &operator=(T *pointer) { + _pointer = pointer; + return *this; + } + + // default copy + +private: + void _register() { + void *base=Core::base(this); + if (base) { + Core::general_register_disappearing_link(&_pointer, base); + } + } + + void *_pointer; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkgc/gc.cpp b/src/inkgc/gc.cpp new file mode 100644 index 0000000..a290423 --- /dev/null +++ b/src/inkgc/gc.cpp @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Wrapper for Boehm GC. + */ +/* Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "inkgc/gc-core.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Inkscape { +namespace GC { + +namespace { + +void display_warning(char *msg, GC_word arg) { + g_warning(msg, arg); +} + +void do_init() { + GC_set_no_dls(1); + GC_set_all_interior_pointers(1); + GC_set_finalize_on_demand(0); + + GC_INIT(); + + GC_set_warn_proc(&display_warning); +} + +void *debug_malloc(std::size_t size) { + return GC_debug_malloc(size, GC_EXTRAS); +} + +void *debug_malloc_atomic(std::size_t size) { + return GC_debug_malloc_atomic(size, GC_EXTRAS); +} + +void *debug_malloc_uncollectable(std::size_t size) { + return GC_debug_malloc_uncollectable(size, GC_EXTRAS); +} + +void *debug_malloc_atomic_uncollectable(std::size_t size) { + return GC_debug_malloc_uncollectable(size, GC_EXTRAS); +} + +std::ptrdiff_t compute_debug_base_fixup() { + char *base=reinterpret_cast(GC_debug_malloc(1, GC_EXTRAS)); + char *real_base=reinterpret_cast(GC_base(base)); + GC_debug_free(base); + return base - real_base; +} + +inline std::ptrdiff_t const &debug_base_fixup() { + static std::ptrdiff_t fixup=compute_debug_base_fixup(); + return fixup; +} + +void *debug_base(void *ptr) { + char *base=reinterpret_cast(GC_base(ptr)); + return base + debug_base_fixup(); +} + +int debug_general_register_disappearing_link(void **p_ptr, void const *base) { + char const *real_base = reinterpret_cast(base) - debug_base_fixup(); +#if (GC_MAJOR_VERSION >= 7 && GC_MINOR_VERSION >= 4) + return GC_general_register_disappearing_link(p_ptr, real_base); +#else // compatibility with older Boehm GC versions + return GC_general_register_disappearing_link(p_ptr, const_cast(real_base)); +#endif +} + +void dummy_do_init() {} + +void *dummy_base(void *) { return nullptr; } + +void dummy_register_finalizer(void *, CleanupFunc, void *, + CleanupFunc *old_func, void **old_data) +{ + if (old_func) { + *old_func = nullptr; + } + if (old_data) { + *old_data = nullptr; + } +} + +int dummy_general_register_disappearing_link(void **, void const *) { return false; } + +int dummy_unregister_disappearing_link(void **/*link*/) { return false; } + +std::size_t dummy_get_heap_size() { return 0; } + +std::size_t dummy_get_free_bytes() { return 0; } + +void dummy_gcollect() {} + +void dummy_enable() {} + +void dummy_disable() {} + +Ops enabled_ops = { + &do_init, + &GC_malloc, + &GC_malloc_atomic, + &GC_malloc_uncollectable, + &GC_malloc_atomic_uncollectable, + &GC_base, + &GC_register_finalizer_ignore_self, +#if (GC_MAJOR_VERSION >= 7 && GC_MINOR_VERSION >= 4) + &GC_general_register_disappearing_link, +#else // compatibility with older Boehm GC versions + (int (*)(void**, const void*))(&GC_general_register_disappearing_link), +#endif + &GC_unregister_disappearing_link, + &GC_get_heap_size, + &GC_get_free_bytes, + &GC_gcollect, + &GC_enable, + &GC_disable, + &GC_free +}; + +Ops debug_ops = { + &do_init, + &debug_malloc, + &debug_malloc_atomic, + &debug_malloc_uncollectable, + &debug_malloc_atomic_uncollectable, + &debug_base, + &GC_debug_register_finalizer_ignore_self, + &debug_general_register_disappearing_link, + &GC_unregister_disappearing_link, + &GC_get_heap_size, + &GC_get_free_bytes, + &GC_gcollect, + &GC_enable, + &GC_disable, + &GC_debug_free +}; + +Ops disabled_ops = { + &dummy_do_init, + &std::malloc, + &std::malloc, + &std::malloc, + &std::malloc, + &dummy_base, + &dummy_register_finalizer, + &dummy_general_register_disappearing_link, + &dummy_unregister_disappearing_link, + &dummy_get_heap_size, + &dummy_get_free_bytes, + &dummy_gcollect, + &dummy_enable, + &dummy_disable, + &std::free +}; + +class InvalidGCModeError : public std::runtime_error { +public: + InvalidGCModeError(const char *mode) + : runtime_error(std::string("Unknown GC mode \"") + mode + "\"") + {} +}; + +Ops const &get_ops() { + char *mode_string=std::getenv("_INKSCAPE_GC"); + if (mode_string) { + if (!std::strcmp(mode_string, "enable")) { + return enabled_ops; + } else if (!std::strcmp(mode_string, "debug")) { + return debug_ops; + } else if (!std::strcmp(mode_string, "disable")) { + return disabled_ops; + } else { + throw InvalidGCModeError(mode_string); + } + } else { + return enabled_ops; + } +} + +void die_because_not_initialized() { + g_error("Attempt to use GC allocator before call to Inkscape::GC::init()"); +} + +void *stub_malloc(std::size_t) { + die_because_not_initialized(); + return nullptr; +} + +void *stub_base(void *) { + die_because_not_initialized(); + return nullptr; +} + +void stub_register_finalizer_ignore_self(void *, CleanupFunc, void *, + CleanupFunc *, void **) +{ + die_because_not_initialized(); +} + +int stub_general_register_disappearing_link(void **, void const *) { + die_because_not_initialized(); + return 0; +} + +int stub_unregister_disappearing_link(void **) { + die_because_not_initialized(); + return 0; +} + +std::size_t stub_get_heap_size() { + die_because_not_initialized(); + return 0; +} + +std::size_t stub_get_free_bytes() { + die_because_not_initialized(); + return 0; +} + +void stub_gcollect() { + die_because_not_initialized(); +} + +void stub_enable() { + die_because_not_initialized(); +} + +void stub_disable() { + die_because_not_initialized(); +} + +void stub_free(void *) { + die_because_not_initialized(); +} + +} + +Ops Core::_ops = { + nullptr, + &stub_malloc, + &stub_malloc, + &stub_malloc, + &stub_malloc, + &stub_base, + &stub_register_finalizer_ignore_self, + &stub_general_register_disappearing_link, + &stub_unregister_disappearing_link, + &stub_get_heap_size, + &stub_get_free_bytes, + &stub_gcollect, + &stub_enable, + &stub_disable, + &stub_free +}; + +void Core::init() { + try { + _ops = get_ops(); + } catch (InvalidGCModeError &e) { + g_warning("%s; enabling normal collection", e.what()); + _ops = enabled_ops; + } + + _ops.do_init(); +} + + +namespace { + +bool collection_requested=false; +bool collection_task() { + Core::gcollect(); + Core::gcollect(); + collection_requested=false; + return false; +} + +} + +void request_early_collection() { + if (!collection_requested) { + collection_requested=true; + Glib::signal_idle().connect(sigc::ptr_fun(&collection_task)); + } +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkscape-application.cpp b/src/inkscape-application.cpp new file mode 100644 index 0000000..92410a3 --- /dev/null +++ b/src/inkscape-application.cpp @@ -0,0 +1,1497 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The main Inkscape application. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include +#include + +#include // Internationalization + +#ifdef HAVE_CONFIG_H +# include "config.h" // Defines ENABLE_NLS +#endif + +#include "inkscape-application.h" +#include "inkscape-window.h" + +#include "auto-save.h" // Auto-save +#include "desktop.h" // Access to window +#include "file.h" // sp_file_convert_dpi +#include "inkscape.h" // Inkscape::Application + +#include "include/glibmm_version.h" + +#include "inkgc/gc-core.h" // Garbage Collecting init + +#include "io/file.h" // File open (command line). +#include "io/resource.h" // TEMPLATE +#include "io/resource-manager.h" // Fix up references. + +#include "object/sp-root.h" // Inkscape version. + +#include "ui/interface.h" // sp_ui_error_dialog +#include "ui/dialog/font-substitution.h" // Warn user about font substitution. +#include "ui/widget/panel.h" // Panel prep +#include "widgets/desktop-widget.h" // Close without saving dialog + +#include "util/units.h" // Redimension window + +#include "actions/actions-base.h" // Actions +#include "actions/actions-file.h" // Actions +#include "actions/actions-object.h" // Actions +#include "actions/actions-output.h" // Actions +#include "actions/actions-selection.h" // Actions +#include "actions/actions-transform.h" // Actions +#include "actions/actions-window.h" // Actions + +#ifdef GDK_WINDOWING_QUARTZ +#include +#endif + +#ifdef WITH_DBUS +# include "extension/dbus/dbus-init.h" +#endif + +#ifdef ENABLE_NLS +// Native Language Support - shouldn't this always be used? +#include "helper/gettext.h" // gettext init +#endif // ENABLE_NLS + +#include "io/resource.h" +using Inkscape::IO::Resource::UIS; + +// This is a bit confusing as there are two ways to handle command line arguments and files +// depending on if the Gio::APPLICATION_HANDLES_OPEN and/or Gio::APPLICATION_HANDLES_COMMAND_LINE +// flags are set. If the open flag is set and the command line not, the all the remainng arguments +// after calling on_handle_local_options() are assumed to be filenames. + +// Add document to app. +void +InkscapeApplication::document_add(SPDocument* document) +{ + if (document) { + auto it = _documents.find(document); + if (it == _documents.end()) { + _documents[document] = std::vector(); + } else { + // Should never happen. + std::cerr << "InkscapeApplication::add_document: Document already opened!" << std::endl; + } + } else { + // Should never happen! + std::cerr << "InkscapeApplication::add_document: No document!" << std::endl; + } +} + +// New document, add it to app. TODO: This should really be open_document with option to strip template data. +SPDocument* +InkscapeApplication::document_new(const std::string &Template) +{ + // Open file + SPDocument *document = ink_file_new(Template); + if (document) { + document_add(document); + + // Set viewBox if it doesn't exist. + if (!document->getRoot()->viewBox_set) { + document->setViewBox(); + } + + } else { + std::cerr << "InkscapeApplication::new_document: failed to open new document!" << std::endl; + } + + return document; +} + + +// Open a document, add it to app. +SPDocument* +InkscapeApplication::document_open(const Glib::RefPtr& file, bool *cancelled) +{ + // Open file + SPDocument *document = ink_file_open(file, cancelled); + + if (document) { + document->setVirgin(false); // Prevents replacing document in same window during file open. + + document_add (document); + } else if (cancelled == nullptr || !(*cancelled)) { + std::cerr << "InkscapeApplication::document_open: Failed to open: " << file->get_parse_name() << std::endl; + } + + return document; +} + + +// Open a document, add it to app. +SPDocument* +InkscapeApplication::document_open(const std::string& data) +{ + // Open file + SPDocument *document = ink_file_open(data); + + if (document) { + document->setVirgin(false); // Prevents replacing document in same window during file open. + + document_add (document); + } else { + std::cerr << "InkscapeApplication::document_open: Failed to open memory document." << std::endl; + } + + return document; +} + + +/** Swap out one document for another in a window... maybe this should disappear. + * Does not delete old document! + */ +bool +InkscapeApplication::document_swap(InkscapeWindow* window, SPDocument* document) +{ + if (!document || !window) { + std::cerr << "InkscapeAppliation::swap_document: Missing window or document!" << std::endl; + return false; + } + + SPDesktop* desktop = window->get_desktop(); + SPDocument* old_document = window->get_document(); + desktop->change_document(document); + document->emitResizedSignal(document->getWidth().value("px"), document->getHeight().value("px")); + + // We need to move window from the old document to the new document. + + // Find old document + auto it = _documents.find(old_document); + if (it != _documents.end()) { + + // Remove window from document map. + auto it2 = std::find(it->second.begin(), it->second.end(), window); + if (it2 != it->second.end()) { + it->second.erase(it2); + } else { + std::cerr << "InkscapeApplication::swap_document: Window not found!" << std::endl; + } + + } else { + std::cerr << "InkscapeApplication::swap_document: Document not in map!" << std::endl; + } + + // Find new document + it = _documents.find(document); + if (it != _documents.end()) { + it->second.push_back(window); + } else { + std::cerr << "InkscapeApplication::swap_document: Document not in map!" << std::endl; + } + + // To be removed (add/delete once per window)! + INKSCAPE.add_document(document); + INKSCAPE.remove_document(old_document); + + // ActionContext should be removed once verbs are gone but we use it for now. + Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); + _active_document = document; + _active_selection = context.getSelection(); + _active_view = context.getView(); + _active_window = window; + return true; +} + +/** Revert document: open saved document and swap it for each window. + */ +bool +InkscapeApplication::document_revert(SPDocument* document) +{ + // Find saved document. + gchar const *path = document->getDocumentURI(); + if (!path) { + std::cerr << "InkscapeApplication::revert_document: Document never saved, cannot revert." << std::endl; + return false; + } + + // Open saved document. + Glib::RefPtr file = Gio::File::create_for_path(document->getDocumentURI()); + SPDocument* new_document = document_open (file); + if (!new_document) { + std::cerr << "InkscapeApplication::revert_document: Cannot open saved document!" << std::endl; + return false; + } + + // Allow overwriting current document. + document->setVirgin(true); + + auto it = _documents.find(document); + if (it != _documents.end()) { + + // Swap reverted document in all windows. + for (auto it2 : it->second) { + + SPDesktop* desktop = it2->get_desktop(); + + // Remember current zoom and view. + double zoom = desktop->current_zoom(); + Geom::Point c = desktop->get_display_area().midpoint(); + + bool reverted = document_swap (it2, new_document); + + if (reverted) { + desktop->zoom_absolute_center_point (c, zoom); + } else { + std::cerr << "InkscapeApplication::revert_document: Revert failed!" << std::endl; + } + } + + document_close (document); + } else { + std::cerr << "InkscapeApplication::revert_document: Document not found!" << std::endl; + return false; + } + return true; +} + + + +/** Close a document, remove from app. No checking is done on modified status, etc. + */ +void +InkscapeApplication::document_close(SPDocument* document) +{ + if (document) { + + auto it = _documents.find(document); + if (it != _documents.end()) { + if (it->second.size() != 0) { + std::cerr << "InkscapeApplication::close_document: Window vector not empty!" << std::endl; + } + _documents.erase(it); + } else { + std::cerr << "InkscapeApplication::close_document: Document not registered with application." << std::endl; + } + + delete document; + + } else { + std::cerr << "InkscapeApplication::close_document: No document!" << std::endl; + } +} + + +/** Return number of windows with document. + */ +unsigned +InkscapeApplication::document_window_count(SPDocument* document) +{ + unsigned count = 0; + + auto it = _documents.find(document); + if (it != _documents.end()) { + count = it->second.size(); + } else { + std::cerr << "InkscapeApplication::document_window_count: Document not in map!" << std::endl; + } + + return count; +} + +/** Fix up a document if necessary (Only fixes that require GUI). + */ +void +InkscapeApplication::document_fix(InkscapeWindow* window) +{ + // Most fixes are handled when document is opened in SPDocument::createDoc(). + // But some require the GUI to be present. These are handled here. + + if (_with_gui) { + + SPDocument* document = window->get_document(); + + // Perform a fixup pass for hrefs. + if ( Inkscape::ResourceManager::getManager().fixupBrokenLinks(document) ) { + Glib::ustring msg = _("Broken links have been changed to point to existing files."); + SPDesktop* desktop = window->get_desktop(); + if (desktop != nullptr) { + desktop->showInfoDialog(msg); + } + } + + // Fix dpi (pre-92 files). + if ( sp_version_inside_range( document->getRoot()->version.inkscape, 0, 1, 0, 92 ) ) { + sp_file_convert_dpi(document); + } + + // Check for font substitutions, requires text to have been rendered. + Inkscape::UI::Dialog::FontSubstitution::getInstance().checkFontSubstitutions(document); + } +} + + +/** Get a list of open documents (from document map). + */ +std::vector +InkscapeApplication::get_documents() +{ + std::vector documents; + for (auto &i : _documents) { + documents.push_back(i.first); + } + return documents; +} + + + +// Take an already open document and create a new window, adding window to document map. +InkscapeWindow* +InkscapeApplication::window_open(SPDocument* document) +{ + // Once we've removed Inkscape::Application (separating GUI from non-GUI stuff) + // it will be more easy to start up the GUI after-the-fact. Until then, prevent + // opening a window if GUI not selected at start-up time. + if (!_with_gui) { + std::cerr << "InkscapeApplication::window_open: Not in gui mode!" << std::endl; + return nullptr; + } + + InkscapeWindow* window = new InkscapeWindow(document); + // TODO Add window to application. (Instead of in InkscapeWindow constructor.) + + SPDesktop* desktop = window->get_desktop(); + + // To be removed (add once per window)! + INKSCAPE.add_document(document); + + // ActionContext should be removed once verbs are gone but we use it for now. + Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); + _active_selection = context.getSelection(); + _active_view = context.getView(); + _active_document = document; + _active_window = window; + + auto it = _documents.find(document); + if (it != _documents.end()) { + it->second.push_back(window); + } else { + std::cerr << "InkscapeApplication::window_open: Document not in map!" << std::endl; + } + + document_fix(window); // May need flag to prevent this from being called more than once. + + return window; +} + + +// Close a window. Does not delete document. +void +InkscapeApplication::window_close(InkscapeWindow* window) +{ + // std::cout << "InkscapeApplication::close_window" << std::endl; + // dump(); + + if (window) { + + SPDocument* document = window->get_document(); + if (document) { + + // To be removed (remove once per window)! + bool last = INKSCAPE.remove_document(document); + + // Leave active document alone (maybe should find new active window and reset variables). + _active_selection = nullptr; + _active_view = nullptr; + _active_window = nullptr; + + // Remove window from document map. + auto it = _documents.find(document); + if (it != _documents.end()) { + auto it2 = std::find(it->second.begin(), it->second.end(), window); + if (it2 != it->second.end()) { + it->second.erase(it2); + delete window; // Results in call to SPDesktop::destroy() + } else { + std::cerr << "InkscapeApplication::close_window: window not found!" << std::endl; + } + } else { + std::cerr << "InkscapeApplication::close_window: document not in map!" << std::endl; + } + } else { + std::cerr << "InkscapeApplication::close_window: No document!" << std::endl; + } + + } else { + std::cerr << "InkscapeApplication::close_window: No window!" << std::endl; + } + + // dump(); +} + + +// Closes active window (useful for scripting). +void +InkscapeApplication::window_close_active() +{ + if (_active_window) { + window_close (_active_window); + } else { + std::cerr << "InkscapeApplication::window_close_active: no active window!" << std::endl; + } +} + + +/** Update windows in response to: + * - New active window + * - Document change + * - Selection change + */ +void +InkscapeApplication::windows_update(SPDocument* document) +{ + // Find windows: + auto it = _documents.find( document ); + if (it != _documents.end()) { + std::vector windows = it->second; + // std::cout << "InkscapeApplication::update_windows: windows size: " << windows.size() << std::endl; + // Loop over InkscapeWindows. + // Loop over DialogWindows. TBD + } else { + // std::cout << "InkscapeApplication::update_windows: no windows found" << std::endl; + } +} + +/** Debug function + */ +void +InkscapeApplication::dump() +{ + std::cout << "InkscapeApplication::dump()" << std::endl; + std::cout << " Documents: " << _documents.size() << std::endl; + for (auto i : _documents) { + std::cout << " Document: " << (i.first->getDocumentName()?i.first->getDocumentName():"unnamed") << std::endl; + for (auto j : i.second) { + std::cout << " Window: " << j->get_title() << std::endl; + } + } +} + + +template +ConcreteInkscapeApplication& +ConcreteInkscapeApplication::get_instance() +{ + static ConcreteInkscapeApplication instance; + return instance; +} + +template +void +ConcreteInkscapeApplication::_start_main_option_section(const Glib::ustring& section_name) +{ +#ifndef _WIN32 + // Avoid outputting control characters to non-tty destinations. + // + // However, isatty() is not useful on Windows + // - it doesn't recognize mintty and similar terminals + // - it doesn't work in cmd.exe either, where we have to use the inkscape.com wrapper, connecting stdout to a pipe + if (!isatty(fileno(stdout))) { + return; + } +#endif + + if (section_name.empty()) { + this->add_main_option_entry(T::OPTION_TYPE_BOOL, Glib::ustring("\b\b ")); + } else { + this->add_main_option_entry(T::OPTION_TYPE_BOOL, Glib::ustring("\b\b \n") + section_name + ":"); + } +} + +// Note: We tried using Gio::APPLICATION_CAN_OVERRIDE_APP_ID instead of +// Gio::APPLICATION_NON_UNIQUE. The advantages of this is that copy/paste between windows would be +// more reliable and that we wouldn't have multiple Inkscape instance writing to the preferences +// file at the same time (if started as separate processes). This caused problems with batch +// processing files and with extensions as they rely on having multiple instances of Inkscape +// running independently. In principle one can use --gapplication-app-id to run a new instance of +// Inkscape but this with our current structure fails with the error message: +// "g_application_set_application_id: assertion '!application->priv->is_registered' failed". +// It also require generating new id's for each separate Inkscape instance required. + +template +ConcreteInkscapeApplication::ConcreteInkscapeApplication() + : T("org.inkscape.Inkscape", + Gio::APPLICATION_HANDLES_OPEN | // Use default file opening. + Gio::APPLICATION_NON_UNIQUE ) + , InkscapeApplication() +{ + + // ==================== Initializations ===================== + // Garbage Collector + Inkscape::GC::init(); + +#ifdef ENABLE_NLS + // Native Language Support (shouldn't this always be used?). + Inkscape::initialize_gettext(); +#endif + + // Autosave + Inkscape::AutoSave::getInstance().init(this); + + // Don't set application name for now. We don't use it anywhere but + // it overrides the name used for adding recently opened files and breaks the Gtk::RecentFilter + // Glib::set_application_name(N_("Inkscape - A Vector Drawing Program")); // After gettext() init. + + // ======================== Actions ========================= + add_actions_base(this); // actions that are GUI independent + add_actions_file(this); // actions for file handling + add_actions_object(this); // actions for object manipulation + add_actions_output(this); // actions for file export + add_actions_selection(this); // actions for object selection + add_actions_transform(this); // actions for transforming selected objects + add_actions_window(this); // actions for windows + + // ====================== Command Line ====================== + + // Will automatically handle character conversions. + // Note: OPTION_TYPE_FILENAME => std::string, OPTION_TYPE_STRING => Glib::ustring. + +#if GLIBMM_CHECK_VERSION(2,56,0) + // Additional informational strings for --help output + // TODO: Claims to be translated automatically, but seems broken, so pass already translated strings + this->set_option_context_parameter_string(_("file1 [file2 [fileN]]")); + this->set_option_context_summary(_("Process (or open) one or more files.")); + this->set_option_context_description(Glib::ustring("\n") + _("Examples:") + '\n' + + " " + Glib::ustring::compose(_("Export input SVG (%1) to PDF (%2) format:"), "in.svg", "out.pdf") + '\n' + + '\t' + "inkscape --export-filename=out.pdf in.svg\n" + + " " + Glib::ustring::compose(_("Export input files (%1) to PNG format keeping original name (%2):"), "in1.svg, in2.svg", "in1.png, in2.png") + '\n' + + '\t' + "inkscape --export-type=png in1.svg in2.svg\n" + + " " + Glib::ustring::compose(_("See %1 and %2 for more details."), "'man inkscape'", "http://wiki.inkscape.org/wiki/index.php/Using_the_Command_Line")); +#endif + + // General + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "version", 'V', N_("Print Inkscape version"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "system-data-directory", '\0', N_("Print system data directory"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "user-data-directory", '\0', N_("Print user data directory"), ""); + + // Open/Import + _start_main_option_section(_("File import")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "pipe", 'p', N_("Read input file from standard input (stdin)"), ""); + this->add_main_option_entry(T::OPTION_TYPE_INT, "pdf-page", '\0', N_("PDF page number to import"), N_("PAGE")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "pdf-poppler", '\0', N_("Use poppler when importing via commandline"), ""); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "convert-dpi-method", '\0', N_("Method used to convert pre-0.92 document dpi, if needed: [none|scale-viewbox|scale-document]"), "[...]"); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "no-convert-text-baseline-spacing", '\0', N_("Do not fix pre-0.92 document's text baseline spacing on opening"), ""); + + // Export - File and File Type + _start_main_option_section(_("File export")); + this->add_main_option_entry(T::OPTION_TYPE_FILENAME, "export-filename", 'o', N_("Output file name (file type is guessed from extension)"),N_("EXPORT-FILENAME")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-overwrite", '\0', N_("Overwrite input file"), ""); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-type", '\0', N_("File type(s) to export: [svg,png,ps,eps,pdf,emf,wmf,xaml]"), "[...]"); + + // Export - Geometry + _start_main_option_section(_("Export geometry")); // B = PNG, S = SVG, P = PS/EPS/PDF + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-page", 'C', N_("Area to export is page"), ""); // BSP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-drawing", 'D', N_("Area to export is whole drawing (ignoring page size)"), ""); // BSP + this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-area", 'a', N_("Area to export in SVG user units"), N_("x0:y0:x1:y1")); // BSP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-area-snap", '\0', N_("Snap the bitmap export area outwards to the nearest integer values"), ""); // Bxx + this->add_main_option_entry(T::OPTION_TYPE_DOUBLE, "export-dpi", 'd', N_("Resolution for bitmaps and rasterized filters; default is 96"), N_("DPI")); // BxP + this->add_main_option_entry(T::OPTION_TYPE_INT, "export-width", 'w', N_("Bitmap width in pixels (overrides --export-dpi)"), N_("WIDTH")); // Bxx + this->add_main_option_entry(T::OPTION_TYPE_INT, "export-height", 'h', N_("Bitmap height in pixels (overrides --export-dpi)"), N_("HEIGHT")); // Bxx + this->add_main_option_entry(T::OPTION_TYPE_INT, "export-margin", '\0', N_("Margin around export area: units of page size for SVG, mm for PS/EPS/PDF"), N_("MARGIN")); // xSP + + // Export - Options + _start_main_option_section(_("Export options")); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-id", 'i', N_("ID(s) of object(s) to export"), N_("OBJECT-ID[;OBJECT-ID]*")); // BSP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-id-only", 'j', N_("Hide all objects except object with ID selected by export-id"), ""); // BSx + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-plain-svg", 'l', N_("Remove Inkscape-specific SVG attributes/properties"), ""); // xSx + this->add_main_option_entry(T::OPTION_TYPE_INT, "export-ps-level", '\0', N_("Postscript level (2 or 3); default is 3"), N_("PS-Level")); // xxP + this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-pdf-version", '\0', N_("PDF version (1.4 or 1.5)"), N_("PDF-VERSION")); // xxP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-text-to-path", 'T', N_("Convert text to paths (PS/EPS/PDF/SVG)"), ""); // xxP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-latex", '\0', N_("Export text separately to LaTeX file (PS/EPS/PDF)"), ""); // xxP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-ignore-filters", '\0', N_("Render objects without filters instead of rasterizing (PS/EPS/PDF)"), ""); // xxP + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "export-use-hints", 't', N_("Use stored filename and DPI hints when exporting object selected by --export-id"), ""); // Bxx + this->add_main_option_entry(T::OPTION_TYPE_STRING, "export-background", 'b', N_("Background color for exported bitmaps (any SVG color string)"), N_("COLOR")); // Bxx + this->add_main_option_entry(T::OPTION_TYPE_DOUBLE, "export-background-opacity", 'y', N_("Background opacity for exported bitmaps (0.0 to 1.0, or 1 to 255)"), N_("VALUE")); // Bxx + + // Query - Geometry + _start_main_option_section(_("Query object/document geometry")); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "query-id", 'I', N_("ID(s) of object(s) to be queried"), N_("OBJECT-ID[,OBJECT-ID]*")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-all", 'S', N_("Print bounding boxes of all objects"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-x", 'X', N_("X coordinate of drawing or object (if specified by --query-id)"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-y", 'Y', N_("Y coordinate of drawing or object (if specified by --query-id)"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-width", 'W', N_("Width of drawing or object (if specified by --query-id)"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "query-height", 'H', N_("Height of drawing or object (if specified by --query-id)"), ""); + + // Processing + _start_main_option_section(_("Advanced file processing")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "vacuum-defs", '\0', N_("Remove unused definitions from the section(s) of document"), ""); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "select", '\0', N_("Select objects: comma-separated list of IDs"), N_("OBJECT-ID[,OBJECT-ID]*")); + + // Actions + _start_main_option_section(); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "actions", 'a', N_("List of actions (with optional arguments) to execute"), N_("ACTION(:ARG)[;ACTION(:ARG)]*")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "action-list", '\0', N_("List all available actions"), ""); + + // Verbs + _start_main_option_section(); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "verb", '\0', N_("List of verbs to execute"), N_("VERB[;VERB]*")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "verb-list", '\0', N_("List all available verbs"), ""); + + // Interface + _start_main_option_section(_("Interface")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "with-gui", 'g', N_("With graphical user interface (required by some actions/verbs)"), ""); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "batch-process", '\0', N_("Close GUI after executing all actions/verbs"),""); + _start_main_option_section(); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "shell", '\0', N_("Start Inkscape in interactive shell mode"), ""); + +#ifdef WITH_DBUS + _start_main_option_section(_("D-Bus")); + this->add_main_option_entry(T::OPTION_TYPE_BOOL, "dbus-listen", '\0', N_("Enter a listening loop for D-Bus messages in console mode"), ""); + this->add_main_option_entry(T::OPTION_TYPE_STRING, "dbus-name", '\0', N_("Specify the D-Bus name; default is 'org.inkscape'"), N_("BUS-NAME")); +#endif // WITH_DBUS + + Gio::Application::signal_handle_local_options().connect(sigc::mem_fun(*this, &InkscapeApplication::on_handle_local_options)); + + // This is normally called for us... but after the "handle_local_options" signal is emitted. If + // we want to rely on actions for handling options, we need to call it here. This appears to + // have no unwanted side-effect. It will also trigger the call to on_startup(). + T::register_application(); +} + +template +void +ConcreteInkscapeApplication::on_startup() +{ + T::on_startup(); +} + +// Here are things that should be in on_startup() but cannot be as we don't set _with_gui until +// on_handle_local_options() is called. +template<> +void +ConcreteInkscapeApplication::on_startup2() +{ + Inkscape::Application::create(false); +} + +#ifdef GDK_WINDOWING_QUARTZ +static gboolean osx_openfile_callback(GtkosxApplication *, gchar const *, + ConcreteInkscapeApplication *); +static gboolean osx_quit_callback(GtkosxApplication *, ConcreteInkscapeApplication *); +#endif + +template<> +void +ConcreteInkscapeApplication::on_startup2() +{ + // This should be completely rewritten. + Inkscape::Application::create(_with_gui); + + if (!_with_gui) { + return; + } + + // ======================= Actions (GUI) ====================== + add_action("new", sigc::mem_fun(*this, &InkscapeApplication::on_new )); + add_action("quit", sigc::mem_fun(*this, &InkscapeApplication::on_quit )); + + // ========================= GUI Init ========================= + Gtk::Window::set_default_icon_name("org.inkscape.Inkscape"); + Inkscape::UI::Widget::Panel::prep(); + + // ========================= Builder ========================== + // App menus deprecated in 3.32. This whole block of code should be + // removed after confirming this code isn't required. + _builder = Gtk::Builder::create(); + + Glib::ustring app_builder_file = get_filename(UIS, "inkscape-application.glade"); + + try + { + _builder->add_from_file(app_builder_file); + } + catch (const Glib::Error& ex) + { + std::cerr << "InkscapeApplication: " << app_builder_file << " file not read! " << ex.what() << std::endl; + } + + auto object = _builder->get_object("menu-application"); + auto menu = Glib::RefPtr::cast_dynamic(object); + if (!menu) { + std::cerr << "InkscapeApplication: failed to load application menu!" << std::endl; + } else { + // set_app_menu(menu); + } + +#ifdef GDK_WINDOWING_QUARTZ + GtkosxApplication *osxapp = gtkosx_application_get(); + g_signal_connect(G_OBJECT(osxapp), "NSApplicationOpenFile", G_CALLBACK(osx_openfile_callback), this); + g_signal_connect(G_OBJECT(osxapp), "NSApplicationBlockTermination", G_CALLBACK(osx_quit_callback), this); +#endif +} + +/** We should not create a window if T is Gio::Applicaton. + */ +template +InkscapeWindow* +ConcreteInkscapeApplication::create_window(SPDocument *document, bool replace) +{ + std::cerr << "ConcreteInkscapeApplication::create_window: Should not be called!" << std::endl; + return nullptr; +} + +/** Create a window given a document. This is used internally in InkscapeApplication. + */ +template<> +InkscapeWindow* +ConcreteInkscapeApplication::create_window(SPDocument *document, bool replace) +{ + SPDocument *old_document = _active_document; + InkscapeWindow* window = InkscapeApplication::get_active_window(); + + if (replace && old_document && window) { + document_swap (window, document); + + // Delete old document if no longer attached to any window. + auto it = _documents.find (old_document); + if (it != _documents.end()) { + if (it->second.size() == 0) { + document_close (old_document); + } + } + + document->emitResizedSignal(document->getWidth().value("px"), document->getHeight().value("px")); + + } else { + window = window_open (document); + } + window->show(); + + return window; +} + + +/** We should not create a window if T is Gio::Applicaton. +*/ +template +void +ConcreteInkscapeApplication::create_window(const Glib::RefPtr& file, + bool add_to_recent, + bool replace_empty) +{ + std::cerr << "ConcreteInkscapeApplication::create_window: Should not be called!" << std::endl; +} + + +/** Create a window given a Gio::File. This is what most external functions should call. + The booleans are only false when opening a help file. +*/ +template<> +void +ConcreteInkscapeApplication::create_window(const Glib::RefPtr& file, + bool add_to_recent, + bool replace_empty) +{ + SPDocument* document = nullptr; + InkscapeWindow* window = nullptr; + bool cancelled = false; + + if (file) { + document = document_open(file, &cancelled); + if (document) { + + if (add_to_recent) { + auto recentmanager = Gtk::RecentManager::get_default(); + recentmanager->add_item (file->get_uri()); + } + + SPDocument* old_document = _active_document; + bool replace = replace_empty && old_document && old_document->getVirgin(); + // virgin == true => an empty document (template). + + window = create_window (document, replace); + + } else if (!cancelled) { + std::cerr << "ConcreteInkscapeApplication::create_window: Failed to load: " + << file->get_parse_name() << std::endl; + + gchar *text = g_strdup_printf(_("Failed to load the requested file %s"), file->get_parse_name().c_str()); + sp_ui_error_dialog(text); + g_free(text); + } + + } else { + std::string Template = + Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::TEMPLATES, "default.svg", true); + document = document_new (Template); + if (document) { + window = window_open (document); + } else { + std::cerr << "ConcreteInkscapeApplication::create_window: Failed to open default template! " << Template << std::endl; + } + } + + _active_document = document; + _active_window = window; + +#ifdef WITH_DBUS + if (window) { + SPDesktop* desktop = window->get_desktop(); + if (desktop) { + Inkscape::Extension::Dbus::dbus_init_desktop_interface(desktop); + } else { + std::cerr << "ConcreteInkscapeApplication::create_window: Failed to create desktop!" << std::endl; + } + } +#endif +} + +/** No need to destroy window if T is Gio::Application. + */ +template +bool +ConcreteInkscapeApplication::destroy_window(InkscapeWindow* window) +{ + std::cerr << "ConcreteInkscapeApplication::destroy_window: Should not be called!"; + return false; +} + +/** Destroy a window. Aborts if document needs saving. + * Returns true if window destroyed. + */ +template<> +bool +ConcreteInkscapeApplication::destroy_window(InkscapeWindow* window) +{ + SPDocument* document = window->get_document(); + + // Remove document if no windows left. + if (document) { + auto it = _documents.find(document); + if (it != _documents.end()) { + + // If only one window for document: + if (it->second.size() == 1) { + // Check if document needs saving. + bool abort = window->get_desktop_widget()->shutdown(); + if (abort) { + return false; + } + } + + window_close(window); + + if (it->second.size() == 0) { + document_close (document); + } + + } else { + std::cerr << "ConcreteInkscapeApplication::destroy_window: Could not find document!" << std::endl; + } + } + + // Debug + // auto windows = get_windows(); + // std::cout << "destroy_windows: app windows size: " << windows.size() << std::endl; + + return true; +} + +/* Close all windows and exit. +**/ +template +void +ConcreteInkscapeApplication::destroy_all() +{ + std::cerr << "ConcreteInkscapeApplication::destroy_all: Should not be called!"; +} + +template<> +void +ConcreteInkscapeApplication::destroy_all() +{ + while (_documents.size() != 0) { + auto it = _documents.begin(); + if (!it->second.empty()) { + auto it2 = it->second.begin(); + if (!destroy_window (*it2)) return; // If destroy aborted, we need to stop exit. + } + } +} + +/** Common processing for documents + */ +template +void +ConcreteInkscapeApplication::process_document(SPDocument* document, std::string output_path) +{ + // Add to Inkscape::Application... + INKSCAPE.add_document(document); + + // Are we doing one file at a time? In that case, we don't recreate new windows for each file. + bool replace = _use_pipe || _batch_process; + + // Open window if needed (reuse window if we are doing one file at a time inorder to save overhead). + if (_with_gui) { + _active_window = create_window(document, replace); + } + + // ActionContext should be removed once verbs are gone but we use it for now. + Inkscape::ActionContext context = INKSCAPE.action_context_for_document(document); + _active_document = document; + _active_selection = context.getSelection(); + _active_view = context.getView(); + + document->ensureUpToDate(); // Or queries don't work! + + // process_file + for (auto action: _command_line_actions) { + if (!Gio::Application::has_action(action.first)) { + std::cerr << "ConcreteInkscapeApplication::process_document: Unknown action name: " << action.first << std::endl; + } + Gio::Application::activate_action( action.first, action.second ); + } + + if (_use_shell) { + shell(); + } + + // Only if --export-filename, --export-type --export-overwrite, or --export-use-hints are used. + if (_auto_export) { + // Save... can't use action yet. + _file_export.do_export(document, output_path); + } +} + + +// Open document window with default document or pipe. Either this or on_open() is called. +template +void +ConcreteInkscapeApplication::on_activate() +{ + on_startup2(); + + std::string output; + + // Create new document, either from pipe or from template. + SPDocument *document = nullptr; + + if (_use_pipe) { + + // Create document from pipe in. + std::istreambuf_iterator begin(std::cin), end; + std::string s(begin, end); + document = document_open (s); + output = "-"; + + } else { + + // Create a blank document from template + std::string Template = + Inkscape::IO::Resource::get_filename(Inkscape::IO::Resource::TEMPLATES, "default.svg", true); + document = document_new (Template); + } + + if (!document) { + std::cerr << "ConcreteInksacpeApplication::on_activate: failed to created document!" << std::endl; + return; + } + + // Process document (command line actions, shell, create window) + process_document (document, output); +} + +// Open document window for each file. Either this or on_activate() is called. +// type_vec_files == std::vector > +template +void +ConcreteInkscapeApplication::on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) +{ + on_startup2(); + if(_pdf_poppler) + INKSCAPE.set_pdf_poppler(_pdf_poppler); + if(_pdf_page) + INKSCAPE.set_pdf_page(_pdf_page); + + if (files.size() > 1 && !_file_export.export_filename.empty()) { + std::cerr << "ConcreteInkscapeApplication::on_open: " + "Can't use '--export-filename' with multiple input files " + "(output file would be overwritten for each input file). " + "Please use '--export-type' instead and rename manually." + << std::endl; + return; + } + + for (auto file : files) { + + // Open file + SPDocument *document = document_open (file); + if (!document) { + std::cerr << "ConcreteInkscapeApplication::on_open: failed to create document!" << std::endl; + continue; + } + + // Process document (command line actions, shell, create window) + process_document (document, file->get_path()); + } + + if (_batch_process) { + // If with_gui, we've reused a window for each file. We must quit to destroy it. + Gio::Application::quit(); + } +} + +template +void +ConcreteInkscapeApplication::parse_actions(const Glib::ustring& input, action_vector_t& action_vector) +{ + const auto re_colon = Glib::Regex::create("\\s*:\\s*"); + + // Split action list + std::vector tokens = Glib::Regex::split_simple("\\s*;\\s*", input); + for (auto token : tokens) { + // Note: split into 2 tokens max ("param:value"); allows value to contain colon (e.g. abs. paths on Windows) + std::vector tokens2 = re_colon->split(token, 0, static_cast(0), 2); + std::string action; + std::string value; + if (tokens2.size() > 0) { + action = tokens2[0]; + } + if (tokens2.size() > 1) { + value = tokens2[1]; + } + + Glib::RefPtr action_ptr = Gio::Application::lookup_action(action); + if (action_ptr) { + // Doesn't seem to be a way to test this using the C++ binding without Glib-CRITICAL errors. + const GVariantType* gtype = g_action_get_parameter_type(action_ptr->gobj()); + if (gtype) { + // With value. + Glib::VariantType type = action_ptr->get_parameter_type(); + if (type.get_string() == "b") { + bool b = false; + if (value == "1" || value == "true" || value.empty()) { + b = true; + } else if (value == "0" || value == "false") { + b = false; + } else { + std::cerr << "InkscapeApplication::parse_actions: Invalid boolean value: " << action << ":" << value << std::endl; + } + action_vector.push_back( + std::make_pair( action, Glib::Variant::create(b))); + } else if (type.get_string() == "i") { + action_vector.push_back( + std::make_pair( action, Glib::Variant::create(std::stoi(value)))); + } else if (type.get_string() == "d") { + action_vector.push_back( + std::make_pair( action, Glib::Variant::create(std::stod(value)))); + } else if (type.get_string() == "s") { + action_vector.push_back( + std::make_pair( action, Glib::Variant::create(value) )); + } else { + std::cerr << "InkscapeApplication::parse_actions: unhandled action value: " + << action << ": " << type.get_string() << std::endl; + } + } else { + // Stateless (i.e. no value). + action_vector.push_back( std::make_pair( action, Glib::VariantBase() ) ); + } + } else { + // Assume a verb + // std::cerr << "InkscapeApplication::parse_actions: '" + // << action << "' is not a valid action! Assuming verb!" << std::endl; + action_vector.push_back( + std::make_pair("verb", Glib::Variant::create(action))); + } + } +} + +// Once we don't need to create a window just to process verbs! +template +void +ConcreteInkscapeApplication::shell() +{ + std::cout << "Inkscape interactive shell mode. Type 'action-list' to list all actions. " + << "Type 'quit' to quit." << std::endl; + std::cout << " Input of the form:" << std::endl; + std::cout << " action1:arg1; action2:arg2; verb1; verb2; ..." << std::endl; + if (!_with_gui) { + std::cout << "Only verbs that don't require a desktop may be used." << std::endl; + } + + std::string input; + while (true) { + std::cout << "> "; + std::string input; + std::getline(std::cin, input); + + if (std::cin.eof() || input == "quit") break; + + action_vector_t action_vector; + parse_actions(input, action_vector); + for (auto action: action_vector) { + Gio::Application::activate_action( action.first, action.second ); + } + + // This would allow displaying the results of actions on the fly... but it needs to be well + // vetted first. + Glib::RefPtr context = Glib::MainContext::get_default(); + while (context->iteration(false)) {}; + } +} + + +// ========================= Callbacks ========================== + +/* + * Handle command line options. + * + * Options are processed in the order they appear in this function. + * We process in order: Print -> GUI -> Open -> Query -> Process -> Export. + * For each file without GUI: Open -> Query -> Process -> Export + * More flexible processing can be done via actions. + */ +template +int +ConcreteInkscapeApplication::on_handle_local_options(const Glib::RefPtr& options) +{ + if (!options) { + std::cerr << "InkscapeApplication::on_handle_local_options: options is null!" << std::endl; + return -1; // Keep going + } + + // ===================== QUERY ===================== + // These are processed first as they result in immediate program termination. + if (options->contains("version")) { + T::activate_action("inkscape-version"); + return EXIT_SUCCESS; + } + + if (options->contains("system-data-directory")) { + T::activate_action("system-data-directory"); + return EXIT_SUCCESS; + } + + if (options->contains("user-data-directory")) { + T::activate_action("user-data-directory"); + return EXIT_SUCCESS; + } + + if (options->contains("verb-list")) { + T::activate_action("verb-list"); + return EXIT_SUCCESS; + } + + if (options->contains("action-list")) { + T::activate_action("action-list"); + return EXIT_SUCCESS; + } + + // For options without arguments. + auto base = Glib::VariantBase(); + + // ================== GUI and Shell ================ + + // Use of most commmand line options turns off use of gui unless explicitly requested! + // Listed in order that they appear in constructor. + if (options->contains("pipe") || + + options->contains("export-filename") || + options->contains("export-overwrite") || + options->contains("export-type") || + + options->contains("export-area-page") || + options->contains("export-area-drawing") || + options->contains("export-area") || + options->contains("export-area-snap") || + options->contains("export-dpi") || + options->contains("export-width") || + options->contains("export-height") || + options->contains("export-margin") || + options->contains("export-height") || + + options->contains("export-id") || + options->contains("export-id-only") || + options->contains("export-plain-svg") || + options->contains("export-ps-level") || + options->contains("export-pdf-version") || + options->contains("export-text-to_path") || + options->contains("export-latex") || + options->contains("export-ignore-filters") || + options->contains("export-use-hints") || + options->contains("export-background") || + options->contains("export-background-opacity") || + options->contains("export-text-to_path") || + + options->contains("query-id") || + options->contains("query-x") || + options->contains("query-all") || + options->contains("query-y") || + options->contains("query-width") || + options->contains("query-height") || + + options->contains("vacuum-defs") || + options->contains("select") || + options->contains("actions") || + options->contains("verb") || + options->contains("shell") + ) { + _with_gui = false; + } + + if (options->contains("with-gui") || + options->contains("batch-process") + ) { + _with_gui = true; // Override turning GUI off + } + + if (options->contains("batch-process")) _batch_process = true; + if (options->contains("shell")) _use_shell = true; + if (options->contains("pipe")) _use_pipe = true; + + + // Enable auto-export + if (options->contains("export-filename") || + options->contains("export-type") || + options->contains("export-overwrite") || + options->contains("export-use-hints") + ) { + _auto_export = true; + } + + // ==================== ACTIONS ==================== + // Actions as an argument string: e.g.: --actions="query-id:rect1;query-x". + // Actions will be processed in order that they are given in argument. + Glib::ustring actions; + if (options->contains("actions")) { + options->lookup_value("actions", actions); + parse_actions(actions, _command_line_actions); + } + + + // ================= OPEN/IMPORT =================== + + if (options->contains("pdf-poppler")) { + _pdf_poppler = true; + } + if (options->contains("pdf-page")) { // Maybe useful for other file types? + int page = 0; + options->lookup_value("pdf-page", page); + _pdf_page = page; + } + + if (options->contains("convert-dpi-method")) { + Glib::ustring method; + options->lookup_value("convert-dpi-method", method); + if (!method.empty()) { + _command_line_actions.push_back( + std::make_pair("convert-dpi-method", Glib::Variant::create(method))); + } + } + + if (options->contains("no-convert-text-baseline-spacing")) _command_line_actions.push_back(std::make_pair("no-convert-baseline", base)); + + + // ===================== QUERY ===================== + + // 'query-id' should be processed first! Can be a comma-separated list. + if (options->contains("query-id")) { + Glib::ustring query_id; + options->lookup_value("query-id", query_id); + if (!query_id.empty()) { + _command_line_actions.push_back( + std::make_pair("select-by-id", Glib::Variant::create(query_id))); + } + } + + if (options->contains("query-all")) _command_line_actions.push_back(std::make_pair("query-all", base)); + if (options->contains("query-x")) _command_line_actions.push_back(std::make_pair("query-x", base)); + if (options->contains("query-y")) _command_line_actions.push_back(std::make_pair("query-y", base)); + if (options->contains("query-width")) _command_line_actions.push_back(std::make_pair("query-width", base)); + if (options->contains("query-height")) _command_line_actions.push_back(std::make_pair("query-height",base)); + + + // =================== PROCESS ===================== + + // Note: this won't work with --verb="FileSave,FileClose" unless some additional verb changes the file. FIXME + // One can use --verb="FileVacuum,FileSave,FileClose". + if (options->contains("vacuum-defs")) _command_line_actions.push_back(std::make_pair("vacuum-defs", base)); + + if (options->contains("select")) { + Glib::ustring select; + options->lookup_value("select", select); + if (!select.empty()) { + _command_line_actions.push_back( + std::make_pair("select", Glib::Variant::create(select))); + } + } + + if (options->contains("verb")) { + Glib::ustring verb; + options->lookup_value("verb", verb); + if (!verb.empty()) { + _command_line_actions.push_back( + std::make_pair("verb", Glib::Variant::create(verb))); + } + } + + + // ==================== EXPORT ===================== + if (options->contains("export-filename")) { + options->lookup_value("export-filename", _file_export.export_filename); + } + + if (options->contains("export-type")) { + options->lookup_value("export-type", _file_export.export_type); + } + + if (options->contains("export-overwrite")) _file_export.export_overwrite = true; + + // Export - Geometry + if (options->contains("export-area")) { + options->lookup_value("export-area", _file_export.export_area); + } + + if (options->contains("export-area-drawing")) _file_export.export_area_drawing = true; + if (options->contains("export-area-page")) _file_export.export_area_page = true; + + if (options->contains("export-margin")) { + options->lookup_value("export-margin", _file_export.export_margin); + } + + if (options->contains("export-area-snap")) _file_export.export_area_snap = true; + + if (options->contains("export-width")) { + options->lookup_value("export-width", _file_export.export_width); + } + + if (options->contains("export-height")) { + options->lookup_value("export-height", _file_export.export_height); + } + + // Export - Options + if (options->contains("export-id")) { + options->lookup_value("export-id", _file_export.export_id); + } + + if (options->contains("export-id-only")) _file_export.export_id_only = true; + if (options->contains("export-plain-svg")) _file_export.export_plain_svg = true; + + if (options->contains("export-dpi")) { + options->lookup_value("export-dpi", _file_export.export_dpi); + } + + if (options->contains("export-ignore-filters")) _file_export.export_ignore_filters = true; + if (options->contains("export-text-to-path")) _file_export.export_text_to_path = true; + + if (options->contains("export-ps-level")) { + options->lookup_value("export-ps-level", _file_export.export_ps_level); + } + + if (options->contains("export-pdf-version")) { + options->lookup_value("export-pdf-version", _file_export.export_pdf_level); + } + + if (options->contains("export-latex")) _file_export.export_latex = true; + if (options->contains("export-use-hints")) _file_export.export_use_hints = true; + + if (options->contains("export-background")) { + options->lookup_value("export-background",_file_export.export_background); + } + + if (options->contains("export-background-opacity")) { + options->lookup_value("export-background-opacity", _file_export.export_background_opacity); + } + + + // ==================== D-BUS ====================== + +#ifdef WITH_DBUS + // Before initializing extensions, we must set the DBus bus name if required + if (options->contains("dbus-listen")) { + std::string dbus_name; + options->lookup_value("dbus-name", dbus_name); + if (!dbus_name.empty()) { + Inkscape::Extension::Dbus::dbus_set_bus_name(dbus_name.c_str()); + } + } +#endif + + return -1; // Keep going +} + +// ======================== Actions ========================= + +template +void +ConcreteInkscapeApplication::on_new() +{ + create_window(); +} + +template void ConcreteInkscapeApplication::on_quit(){ T::quit(); } + +template<> +void +ConcreteInkscapeApplication::on_quit() +{ + // Delete all windows (quit() doesn't do this). + std::vector windows = get_windows(); + for (auto window: windows) { + // Do something + } + + quit(); +} + +template +void +ConcreteInkscapeApplication::print_action_list() +{ + std::vector actions = T::list_actions(); + std::sort(actions.begin(), actions.end()); + for (auto action : actions) { + std::cout << std::left << std::setw(20) << action + << ": " << _action_extra_data.get_tooltip_for_action(action) << std::endl; + } +} + +// ======================== macOS ============================= + +#ifdef GDK_WINDOWING_QUARTZ +/** + * On macOS, handle dropping files on Inkscape.app icon and "Open With" file association. + */ +static gboolean osx_openfile_callback(GtkosxApplication *osxapp, gchar const *path, + ConcreteInkscapeApplication *app) +{ + auto ptr = Gio::File::create_for_path(path); + g_return_val_if_fail(ptr, false); + app->create_window(ptr); + return true; +} + +/** + * Handle macOS terminating the application + */ +static gboolean osx_quit_callback(GtkosxApplication *, ConcreteInkscapeApplication *app) +{ + app->destroy_all(); + return true; +} +#endif + +template class ConcreteInkscapeApplication; +template class ConcreteInkscapeApplication; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape-application.h b/src/inkscape-application.h new file mode 100644 index 0000000..7087987 --- /dev/null +++ b/src/inkscape-application.h @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The main Inkscape application. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INKSCAPE_APPLICATION_H +#define INKSCAPE_APPLICATION_H + +/* + * The main Inkscape application. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include + +#include "document.h" +#include "selection.h" + +#include "actions/actions-extra-data.h" +#include "helper/action.h" +#include "io/file-export-cmd.h" // File export (non-verb) + +typedef std::vector > action_vector_t; + +class InkscapeWindow; +class SPDocument; +class SPDesktop; + +class InkscapeApplication +{ +public: + virtual void on_startup() = 0; + virtual void on_startup2() = 0; + virtual InkFileExportCmd* file_export() = 0; + virtual int on_handle_local_options(const Glib::RefPtr& options) = 0; + virtual void on_new() = 0; + virtual void on_quit() = 0; + + // Gio::Actions need to know what document, selection, view to work on. + // In headless mode, these are set for each file processed. + // With GUI, these are set everytime the cursor enters an InkscapeWindow. + SPDocument* get_active_document() { return _active_document; }; + void set_active_document(SPDocument* document) { _active_document = document; }; + + Inkscape::Selection* get_active_selection() { return _active_selection; } + void set_active_selection(Inkscape::Selection* selection) + {_active_selection = selection;}; + + // A view should track selection and canvas to document transform matrix. This is partially + // redundant with the selection functions above. Maybe we should get rid of view altogether. + // Canvas to document transform matrix should be stored in InkscapeWindow. + Inkscape::UI::View::View* get_active_view() { return _active_view; } + void set_active_view(Inkscape::UI::View::View* view) { _active_view = view; } + + // The currently focused window (nominally corresponding to _active_document). + // A window must have a document but a document may have zero, one, or more windows. + // This will replace _active_view. + InkscapeWindow* get_active_window() { return _active_window; } + void set_active_window(InkscapeWindow* window) { _active_window = window; } + + /****** Document ******/ + /* Except for document_fix(), these should not require a GUI! */ + void document_add(SPDocument* document); + + SPDocument* document_new(const std::string &Template); + SPDocument* document_open(const Glib::RefPtr& file, bool *cancelled = nullptr); + SPDocument* document_open(const std::string& data); + bool document_swap(InkscapeWindow* window, SPDocument* document); + bool document_revert(SPDocument* document); + void document_close(SPDocument* document); + unsigned document_window_count(SPDocument* document); + + void document_fix(InkscapeWindow* window); + + std::vector get_documents(); + + /******* Window *******/ + InkscapeWindow* window_open(SPDocument* document); + void window_close(InkscapeWindow* window); + void window_close_active(); + + // Update all windows connected to a document. + void windows_update(SPDocument* document); + + + /****** Actions *******/ + InkActionExtraData& get_action_extra_data() { return _action_extra_data; } + + /******* Debug ********/ + void dump(); + + // These are needed to cast Glib::RefPtr to Glib::RefPtr, + // Presumably, Gtk/Gio::Application takes care of ref counting in ConcreteInkscapeApplication + // so we just provide dummies (and there is only one application in the application!). + // void reference() { /*printf("reference()\n" );*/ } + // void unreference() { /*printf("unreference()\n");*/ } + +protected: + bool _with_gui = true; + bool _batch_process = false; // Temp + bool _use_shell = false; + bool _use_pipe = false; + bool _auto_export = false; + int _pdf_page = 1; + int _pdf_poppler = false; + InkscapeApplication() = default; + + // Documents are owned by the application which is responsible for opening/saving/exporting. WIP + // std::vector _documents; For a true headless version + std::map > _documents; + + // We keep track of these things so we don't need a window to find them (for headless operation). + SPDocument* _active_document = nullptr; + Inkscape::Selection* _active_selection = nullptr; + Inkscape::UI::View::View* _active_view = nullptr; + InkscapeWindow* _active_window = nullptr; + + InkFileExportCmd _file_export; + + // Actions from the command line or file. + action_vector_t _command_line_actions; + + // Extra data associated with actions (Label, Section, Tooltip/Help). + InkActionExtraData _action_extra_data; +}; + +// T can be either: +// Gio::Application (window server is not present, required for CI testing) or +// Gtk::Application (window server is present). +// With Gtk::Application, one can still run "headless" by not creating any windows. +template class ConcreteInkscapeApplication : public T, public InkscapeApplication +{ +public: + static ConcreteInkscapeApplication& get_instance(); + +private: + ConcreteInkscapeApplication(); + +public: + InkFileExportCmd* file_export() override { return &_file_export; } + InkscapeWindow* create_window(SPDocument *document, bool replace); + void create_window(const Glib::RefPtr& file = Glib::RefPtr(), + bool add_to_recent = true, bool replace_empty = true); + bool destroy_window(InkscapeWindow* window); + void destroy_all(); + void print_action_list(); + +protected: + void on_startup() override; + void on_startup2() override; + void on_activate() override; + void on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) override; + void process_document(SPDocument* document, std::string output_path); + void parse_actions(const Glib::ustring& input, action_vector_t& action_vector); + +private: + // Callbacks + int on_handle_local_options(const Glib::RefPtr& options) override; + + // Actions + void on_new() override; + void on_quit() override; + void on_about(); + + void shell(); + + void _start_main_option_section(const Glib::ustring& section_name = ""); + + Glib::RefPtr _builder; + +}; + +#endif // INKSCAPE_APPLICATION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape-main.cpp b/src/inkscape-main.cpp new file mode 100644 index 0000000..2518f33 --- /dev/null +++ b/src/inkscape-main.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape - an ambitious vector drawing program + * + * Authors: + * Tavmjong Bah + * + * (C) 2018 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef _WIN32 +#include // SetDllDirectoryW, SetConsoleOutputCP +#include // _O_BINARY +#endif + +#include "inkscape-application.h" +#include "path-prefix.h" + +#include "io/resource.h" + +static void set_extensions_env() +{ + // add inkscape to PATH, so the correct version is always available to extensions by simply calling "inkscape" + gchar *program_dir = get_program_dir(); + if (program_dir) { + gchar const *path = g_getenv("PATH"); + gchar *new_path = g_strdup_printf("%s" G_SEARCHPATH_SEPARATOR_S "%s", program_dir, path); + g_setenv("PATH", new_path, true); + g_free(new_path); + } + g_free(program_dir); + + // add various locations to PYTHONPATH so extensions find their modules + auto extensiondir_user = get_path_ustring(Inkscape::IO::Resource::USER, Inkscape::IO::Resource::EXTENSIONS); + auto extensiondir_system = get_path_ustring(Inkscape::IO::Resource::SYSTEM, Inkscape::IO::Resource::EXTENSIONS); + + auto pythonpath = extensiondir_user + G_SEARCHPATH_SEPARATOR + extensiondir_system; + + auto pythonpath_old = Glib::getenv("PYTHONPATH"); + if (!pythonpath_old.empty()) { + pythonpath += G_SEARCHPATH_SEPARATOR + pythonpath_old; + } + + pythonpath += G_SEARCHPATH_SEPARATOR + Glib::build_filename(extensiondir_system, "inkex", "deprecated-simple"); + + Glib::setenv("PYTHONPATH", pythonpath); + + // Python 2.x attempts to encode output as ASCII by default when sent to a pipe. + Glib::setenv("PYTHONIOENCODING", "UTF-8"); + +#ifdef _WIN32 + // add inkscape directory to DLL search path so dynamically linked extension modules find their libraries + // should be fixed in Python 3.8 (https://github.com/python/cpython/commit/2438cdf0e932a341c7613bf4323d06b91ae9f1f1) + gchar *installation_dir = get_program_dir(); + wchar_t *installation_dir_w = (wchar_t *)g_utf8_to_utf16(installation_dir, -1, NULL, NULL, NULL); + SetDllDirectoryW(installation_dir_w); + g_free(installation_dir); + g_free(installation_dir_w); +#endif +} + +static void set_macos_app_bundle_env(gchar const *program_dir) +{ + std::string bundle_contents_dir; + bundle_contents_dir.assign(program_dir).append("/.."); // /Contents + + // use bundle identifier + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html + auto app_support_dir = Glib::getenv("HOME") + "/Library/Application Support/org.inkscape.Inkscape"; + + auto bundle_resources_dir = bundle_contents_dir + "/Resources"; + auto bundle_resources_etc_dir = bundle_resources_dir + "/etc"; + auto bundle_resources_bin_dir = bundle_resources_dir + "/bin"; + auto bundle_resources_lib_dir = bundle_resources_dir + "/lib"; + auto bundle_resources_share_dir = bundle_resources_dir + "/share"; + + // failsafe: Check if the expected content is really there, using GIO modules + // as an indicator. + // This is also helpful to developers as it enables the possibility to + // 1. cmake -DCMAKE_INSTALL_PREFIX=Inkscape.app/Contents/Resources + // 2. move binary to Inkscape.app/Contents/MacOS and set rpath + // 3. copy Info.plist + // to ease up on testing and get correct application behavior (like dock icon). + if (!Glib::file_test(bundle_resources_lib_dir + "/gio/modules", Glib::FILE_TEST_EXISTS)) { + // doesn't look like a standalone bundle + return; + } + + // XDG + // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + Glib::setenv("XDG_DATA_HOME", app_support_dir + "/share"); + Glib::setenv("XDG_DATA_DIRS", bundle_resources_share_dir); + Glib::setenv("XDG_CONFIG_HOME", app_support_dir + "/config"); + Glib::setenv("XDG_CONFIG_DIRS", bundle_resources_etc_dir + "/xdg"); + Glib::setenv("XDG_CACHE_HOME", app_support_dir + "/cache"); + + // GTK + // https://developer.gnome.org/gtk3/stable/gtk-running.html + Glib::setenv("GTK_EXE_PREFIX", bundle_resources_dir); + Glib::setenv("GTK_DATA_PREFIX", bundle_resources_dir); + + // GDK + Glib::setenv("GDK_PIXBUF_MODULE_FILE", bundle_resources_lib_dir + "/gdk-pixbuf-2.0/2.10.0/loaders.cache"); + + // Inkscape + Glib::setenv("INKSCAPE_LOCALEDIR", bundle_resources_share_dir + "/locale"); + + // fontconfig + Glib::setenv("FONTCONFIG_PATH", bundle_resources_etc_dir + "/fonts"); + + // GIO + Glib::setenv("GIO_MODULE_DIR", bundle_resources_lib_dir + "/gio/modules"); + + // GNOME introspection + Glib::setenv("GI_TYPELIB_PATH", bundle_resources_lib_dir + "/girepository-1.0"); + + // PATH + Glib::setenv("PATH", bundle_resources_bin_dir + ":" + Glib::getenv("PATH")); + + // DYLD_LIBRARY_PATH + // This is required to make Python GTK bindings work as they use dlopen() + // to load libraries. + Glib::setenv("DYLD_LIBRARY_PATH", bundle_resources_lib_dir + ":" + + bundle_resources_lib_dir + "/gdk-pixbuf-2.0/2.10.0/loaders"); +} + +/** + * Convert some legacy 0.92.x command line options to 1.0.x options. + * @param[in,out] argc The main() argc argument, will be modified + * @param[in,out] argv The main() argv argument, will be modified + */ +static void convert_legacy_options(int &argc, char **&argv) +{ + static std::vector argv_new; + char *file = nullptr; + + for (int i = 0; i < argc; ++i) { + if (g_str_equal(argv[i], "--without-gui") || g_str_equal(argv[i], "-z")) { + std::cerr << "Warning: Option --without-gui= is deprecated" << std::endl; + continue; + } + + if (g_str_has_prefix(argv[i], "--file=")) { + std::cerr << "Warning: Option --file= is deprecated" << std::endl; + file = argv[i] + 7; + continue; + } + + bool found_legacy_export = false; + + for (char const *type : { "png", "pdf", "ps", "eps", "emf", "wmf", "plain-svg" }) { + auto s = std::string("--export-").append(type).append("="); + if (g_str_has_prefix(argv[i], s.c_str())) { + std::cerr << "Warning: Option " << s << " is deprecated" << std::endl; + + if (g_str_equal(type, "plain-svg")) { + argv_new.push_back(g_strdup("--export-plain-svg")); + type = "svg"; + } + + argv_new.push_back(g_strdup_printf("--export-type=%s", type)); + argv_new.push_back(g_strdup_printf("--export-filename=%s", argv[i] + s.size())); + + found_legacy_export = true; + break; + } + } + + if (found_legacy_export) { + continue; + } + + argv_new.push_back(argv[i]); + } + + if (file) { + argv_new.push_back(file); + } + + argc = argv_new.size(); + argv = argv_new.data(); +} + +int main(int argc, char *argv[]) +{ + convert_legacy_options(argc, argv); + +#ifdef __APPLE__ + { // Check if we're inside an application bundle and adjust environment + // accordingly. + + gchar *program_dir = get_program_dir(); + if (g_str_has_suffix(program_dir, "Contents/MacOS")) { + + // Step 1 + // Remove macOS session identifier from command line arguments. + // Code adopted from GIMP's app/main.c + + int new_argc = 0; + for (int i = 0; i < argc; i++) { + // Rewrite argv[] without "-psn_..." argument. + if (!g_str_has_prefix(argv[i], "-psn_")) { + argv[new_argc] = argv[i]; + new_argc++; + } + } + if (argc > new_argc) { + argv[new_argc] = nullptr; // glib expects null-terminated array + argc = new_argc; + } + + // Step 2 + // In the past, a launch script/wrapper was used to setup necessary environment + // variables to facilitate relocatability for the application bundle. Starting + // with Catalina, this approach is no longer feasible due to new security checks + // that get misdirected by using a launcher. The launcher needs to go and the + // binary needs to setup the environment itself. + + set_macos_app_bundle_env(program_dir); + } + + g_free(program_dir); + } +#elif defined _WIN32 + // temporarily switch console encoding to UTF8 while Inkscape runs + // as everything else is a mess and it seems to work just fine + const unsigned int initial_cp = GetConsoleOutputCP(); + SetConsoleOutputCP(CP_UTF8); + fflush(stdout); // empty buffer, just to be safe (see warning in documentation for _setmode) + _setmode(_fileno(stdout), _O_BINARY); // binary mode seems required for this to work properly +#endif + + set_extensions_env(); + + int ret; + if (gtk_init_check(NULL, NULL)) { + g_set_prgname("org.inkscape.Inkscape"); + ret = (ConcreteInkscapeApplication::get_instance()).run(argc, argv); + } else { + ret = (ConcreteInkscapeApplication::get_instance()).run(argc, argv); + } + +#ifdef _WIN32 + // switch back to initial console encoding + SetConsoleOutputCP(initial_cp); +#endif + + return ret; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape-manifest.xml b/src/inkscape-manifest.xml new file mode 100644 index 0000000..61756f0 --- /dev/null +++ b/src/inkscape-manifest.xml @@ -0,0 +1,22 @@ + + + + + ${PROGRAM_DESCRIPTION} + + + + true + + + + + + + + + + diff --git a/src/inkscape-version.cpp.in b/src/inkscape-version.cpp.in new file mode 100644 index 0000000..0b70e69 --- /dev/null +++ b/src/inkscape-version.cpp.in @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace Inkscape { + char const *version_string = "@INKSCAPE_VERSION@" " " "(${INKSCAPE_REVISION})"; + char const *version_string_without_revision = "@INKSCAPE_VERSION@"; +} + diff --git a/src/inkscape-version.h b/src/inkscape-version.h new file mode 100644 index 0000000..7d1b44f --- /dev/null +++ b/src/inkscape-version.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Mini static library that contains the version of Inkscape + * + * This is better than a header file, because it only requires a recompile + * of a single file and a relink to update the version. + */ +/* Authors: + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2008 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_INKSCAPE_VERSION_H +#define SEEN_INKSCAPE_INKSCAPE_VERSION_H + +namespace Inkscape { + +extern char const *version_string; ///< full version string +extern char const *version_string_without_revision; ///< version string excluding revision and date + +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkscape-window.cpp b/src/inkscape-window.cpp new file mode 100644 index 0000000..6123472 --- /dev/null +++ b/src/inkscape-window.cpp @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape - An SVG editor. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + + +#include "inkscape-window.h" +#include "inkscape.h" // SP_ACTIVE_DESKTOP +#include "enums.h" // PREFS_WINDOW_GEOMETRY_NONE +#include "shortcuts.h" +#include "inkscape-application.h" + +#include "object/sp-namedview.h" // TODO Remove need for this! + +#include "ui/drag-and-drop.h" // Move to canvas? +#include "ui/interface.h" // main menu, sp_ui_close_view() + +#include "ui/monitor.h" // get_monitor_geometry_at_point() + +#include "ui/desktop/menubar.h" + +#include "ui/drag-and-drop.h" + +#include "widgets/desktop-widget.h" + +InkscapeWindow::InkscapeWindow(SPDocument* document) + : _document(document) + , _app(nullptr) +{ + if (!_document) { + std::cerr << "InkscapeWindow::InkscapeWindow: null document!" << std::endl; + return; + } + + _app = &(ConcreteInkscapeApplication::get_instance()); + _app->add_window(*this); + + set_resizable(true); + + ink_drag_setup(this); + + // =============== Build interface =============== + + // Main box + _mainbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + _mainbox->set_name("DesktopMainBox"); + _mainbox->show(); + add(*_mainbox); + + // Desktop widget (=> MultiPaned) + _desktop_widget = sp_desktop_widget_new(_document); + _desktop_widget->window = this; + gtk_widget_show(GTK_WIDGET(_desktop_widget)); + _desktop = _desktop_widget->desktop; + + // Menu bar (must come after desktop widget creation as we need _desktop) + // _menubar = build_menubar(_desktop); + // _menubar->set_name("MenuBar"); + // _menubar->show_all(); + + // Pallet + + // Status bar + + // _mainbox->pack_start(*_menubar, false, false); + gtk_box_pack_start(GTK_BOX(_mainbox->gobj()), GTK_WIDGET(_desktop_widget), true, true, 0); // Can't use Glib::wrap() + + // ================== Callbacks ================== + signal_delete_event().connect( sigc::mem_fun(*_desktop, &SPDesktop::onDeleteUI)); + signal_window_state_event().connect(sigc::mem_fun(*_desktop, &SPDesktop::onWindowStateEvent)); + signal_focus_in_event().connect( sigc::mem_fun(*_desktop_widget, &SPDesktopWidget::onFocusInEvent)); + + // =================== Actions =================== + + + // ================ Window Options ============== + setup_view(); +} + +// Change a document, leaving desktop/view the same. (Eventually move all code here.) +void +InkscapeWindow::change_document(SPDocument* document) +{ + if (!_app) { + std::cerr << "Inkscapewindow::change_document: app is nullptr!" << std::endl; + return; + } + + _document = document; + _app->set_active_document(_document); + + setup_view(); +} + +// Sets up the window and view according to user preferences and of the just loaded document +void +InkscapeWindow::setup_view() +{ + // Make sure the GdkWindow is fully initialized before resizing/moving + // (ensures the monitor it'll be shown on is known) + realize(); + + // Resize the window to match the document properties + sp_namedview_window_from_document(_desktop); // This should probably be a member function here. + + // Must show before setting zoom and view! (crashes otherwise) + // + // Showing after resizing/moving allows the window manager to correct an invalid size/position of the window + // TODO: This does *not* work when called from 'change_document()', i.e. when the window is already visible. + // This can result in off-screen windows! We previously worked around this by hiding and re-showing + // the window, but a call to hide() causes Inkscape to just exit since the migration to Gtk::Application + show(); + + sp_namedview_zoom_and_view_from_document(_desktop); + sp_namedview_update_layers_from_document(_desktop); + + SPNamedView *nv = _desktop->namedview; + if (nv && nv->lockguides) { + nv->lockGuides(); + } +} + +/** + * Return true if this is the Cmd-Q shortcut on macOS + */ +inline bool is_Cmd_Q(GdkEventKey *event) +{ +#ifdef GDK_WINDOWING_QUARTZ + return (event->keyval == 'q' && event->state == (GDK_MOD2_MASK | GDK_META_MASK)); +#else + return false; +#endif +} + +bool +InkscapeWindow::on_key_press_event(GdkEventKey* event) +{ + // Need to call base class method first or text tool won't work! + // Intercept Cmd-Q on macOS to not bypass confirmation dialog + bool done = !is_Cmd_Q(event) && Gtk::Window::on_key_press_event(event); + if (done) { + return true; + } + + unsigned shortcut = sp_shortcut_get_for_event(event); + return sp_shortcut_invoke (shortcut, _desktop); +} + +bool +InkscapeWindow::on_focus_in_event(GdkEventFocus* event) +{ + if (_app) { + _app->set_active_document(_document); + _app->set_active_view(_desktop); + _app->set_active_selection(_desktop->selection); + _app->windows_update(_document); + } else { + std::cerr << "Inkscapewindow::on_focus_in_event: app is nullptr!" << std::endl; + } + + return Gtk::ApplicationWindow::on_focus_in_event(event); +} + +// Called when a window is closed via the 'X' in the window bar. +bool +InkscapeWindow::on_delete_event(GdkEventAny* event) +{ + if (_app) { + _app->destroy_window(this); + } + return true; +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape-window.h b/src/inkscape-window.h new file mode 100644 index 0000000..96fcfa9 --- /dev/null +++ b/src/inkscape-window.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape - An SVG editor. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#ifndef INKSCAPE_WINDOW_H +#define INKSCAPE_WINDOW_H + +#include + +#include "inkscape-application.h" + +class SPDocument; +class SPDesktop; +class SPDesktopWidget; + +namespace Inkscape { +namespace UI { +namespace View { +// class SVGViewWidget; +} +} +} + + +class InkscapeWindow : public Gtk::ApplicationWindow { + +public: + InkscapeWindow(SPDocument* document); + + SPDocument* get_document() { return _document; } + SPDesktop* get_desktop() { return _desktop; } + SPDesktopWidget* get_desktop_widget() { return _desktop_widget; } + + void change_document(SPDocument* document); + +private: + ConcreteInkscapeApplication* _app; + + SPDocument* _document; + SPDesktop* _desktop; + SPDesktopWidget* _desktop_widget; + + Gtk::Box* _mainbox; + Gtk::MenuBar* _menubar; + + void setup_view(); + + // Callbacks + bool on_key_press_event(GdkEventKey* event) override; + bool on_focus_in_event(GdkEventFocus* event) override; + bool on_delete_event(GdkEventAny* event) override; +}; + +#endif // INKSCAPE_WINDOW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape.cpp b/src/inkscape.cpp new file mode 100644 index 0000000..9d5add7 --- /dev/null +++ b/src/inkscape.cpp @@ -0,0 +1,1200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Interface to main application. + */ +/* Authors: + * Lauris Kaplinski + * bulia byak + * Liam P. White + * + * Copyright (C) 1999-2014 authors + * c++ port Copyright (C) 2003 Nathan Hurst + * c++ification Copyright (C) 2014 Liam P. White + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "desktop.h" +#include "device-manager.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "path-prefix.h" + +#include "debug/simple-event.h" +#include "debug/event-tracker.h" + +#include "extension/db.h" +#include "extension/init.h" +#include "extension/output.h" +#include "extension/system.h" + +#include "helper/action-context.h" + +#include "io/resource.h" +#include "io/resource-manager.h" +#include "io/sys.h" + +#include "libnrtype/FontFactory.h" + +#include "object/sp-root.h" +#include "object/sp-style-elem.h" + +#include "svg/svg-color.h" + +#include "object/sp-root.h" +#include "object/sp-style-elem.h" + +#include "ui/dialog/debug.h" +#include "ui/tools/tool-base.h" + +/* Backbones of configuration xml data */ +#include "menus-skeleton.h" + +#include + +// Inkscape::Application static members +Inkscape::Application * Inkscape::Application::_S_inst = nullptr; +bool Inkscape::Application::_crashIsHappening = false; + +#define DESKTOP_IS_ACTIVE(d) (INKSCAPE._desktops != nullptr && !INKSCAPE._desktops->empty() && ((d) == INKSCAPE._desktops->front())) + +static void (* segv_handler) (int) = SIG_DFL; +static void (* abrt_handler) (int) = SIG_DFL; +static void (* fpe_handler) (int) = SIG_DFL; +static void (* ill_handler) (int) = SIG_DFL; +#ifndef _WIN32 +static void (* bus_handler) (int) = SIG_DFL; +#endif + +#define MENUS_FILE "menus.xml" + +#define SP_INDENT 8 + +/** C++ification TODO list + * - _S_inst should NOT need to be assigned inside the constructor, but if it isn't the Filters+Extensions menus break. + * - Application::_deskops has to be a pointer because of a signal bug somewhere else. Basically, it will attempt to access a deleted object in sp_ui_close_all(), + * but if it's a pointer we can stop and return NULL in Application::active_desktop() + * - These functions are calling Application::create for no good reason I can determine: + * + * Inkscape::UI::Dialog::SVGPreview::SVGPreview() + * src/ui/dialog/filedialogimpl-gtkmm.cpp:542:9 + */ + + +class InkErrorHandler : public Inkscape::ErrorReporter { +public: + InkErrorHandler(bool useGui) : Inkscape::ErrorReporter(), + _useGui(useGui) + {} + ~InkErrorHandler() override = default; + + void handleError( Glib::ustring const& primary, Glib::ustring const& secondary ) const override + { + if (_useGui) { + Gtk::MessageDialog err(primary, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); + err.set_secondary_text(secondary); + err.run(); + } else { + g_message("%s", primary.data()); + g_message("%s", secondary.data()); + } + } + +private: + bool _useGui; +}; + +void inkscape_ref(Inkscape::Application & in) +{ + in.refCount++; +} + +void inkscape_unref(Inkscape::Application & in) +{ + in.refCount--; + + if (&in == Inkscape::Application::_S_inst) { + if (in.refCount <= 0) { + delete Inkscape::Application::_S_inst; + } + } else { + g_error("Attempt to unref an Application (=%p) not the current instance (=%p) (maybe it's already been destroyed?)", + &in, Inkscape::Application::_S_inst); + } +} + +namespace Inkscape { + +/** + * Defined only for debugging purposes. If we are certain the bugs are gone we can remove this + * and the references in inkscape_ref and inkscape_unref. + */ +Application* +Application::operator &() const +{ + return const_cast(this); +} +/** + * Creates a new Inkscape::Application global object. + */ +void +Application::create(bool use_gui) +{ + if (!Application::exists()) { + new Application(use_gui); + } else { + // g_assert_not_reached(); Can happen with InkscapeApplication + } +} + + +/** + * Checks whether the current Inkscape::Application global object exists. + */ +bool +Application::exists() +{ + return Application::_S_inst != nullptr; +} + +/** + * Returns the current Inkscape::Application global object. + * \pre Application::_S_inst != NULL + */ +Application& +Application::instance() +{ + if (!exists()) { + g_error("Inkscape::Application does not yet exist."); + } + return *Application::_S_inst; +} + +/* \brief Constructor for the application. + * Creates a new Inkscape::Application. + * + * \pre Application::_S_inst == NULL + */ + +Application::Application(bool use_gui) : + _use_gui(use_gui) +{ + using namespace Inkscape::IO::Resource; + /* fixme: load application defaults */ + + segv_handler = signal (SIGSEGV, Application::crash_handler); + abrt_handler = signal (SIGABRT, Application::crash_handler); + fpe_handler = signal (SIGFPE, Application::crash_handler); + ill_handler = signal (SIGILL, Application::crash_handler); +#ifndef _WIN32 + bus_handler = signal (SIGBUS, Application::crash_handler); +#endif + + // \TODO: this belongs to Application::init but if it isn't here + // then the Filters and Extensions menus don't work. + _S_inst = this; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + InkErrorHandler* handler = new InkErrorHandler(use_gui); + prefs->setErrorHandler(handler); + { + Glib::ustring msg; + Glib::ustring secondary; + if (prefs->getLastError( msg, secondary )) { + handler->handleError(msg, secondary); + } + } + + if (use_gui) { + using namespace Inkscape::IO::Resource; + auto icon_theme = Gtk::IconTheme::get_default(); + icon_theme->prepend_search_path(get_path_ustring(SYSTEM, ICONS)); + icon_theme->prepend_search_path(get_path_ustring(USER, ICONS)); + add_gtk_css(); + /* Load the preferences and menus */ + load_menus(); + Inkscape::DeviceManager::getManager().loadConfig(); + } + + Inkscape::ResourceManager::getManager(); + + /* set language for user interface according setting in preferences */ + Glib::ustring ui_language = prefs->getString("/ui/language"); + if(!ui_language.empty()) + { + setenv("LANGUAGE", ui_language, true); + } + + /* DebugDialog redirection. On Linux, default to OFF, on Win32, default to ON. + * Use only if use_gui is enabled + */ +#ifdef _WIN32 +#define DEFAULT_LOG_REDIRECT true +#else +#define DEFAULT_LOG_REDIRECT false +#endif + + if (use_gui && prefs->getBool("/dialogs/debug/redirect", DEFAULT_LOG_REDIRECT)) + { + Inkscape::UI::Dialog::DebugDialog::getInstance()->captureLogMessages(); + } + + if (use_gui) + { + Inkscape::UI::Tools::init_latin_keys_group(); + /* Check for global remapping of Alt key */ + mapalt(guint(prefs->getInt("/options/mapalt/value", 0))); + trackalt(guint(prefs->getInt("/options/trackalt/value", 0))); + } + + /* Initialize the extensions */ + Inkscape::Extension::init(); + + /* Initialize font factory */ + font_factory *factory = font_factory::Default(); + if (prefs->getBool("/options/font/use_fontsdir_system", true)) { + char const *fontsdir = get_path(SYSTEM, FONTS); + factory->AddFontsDir(fontsdir); + } + if (prefs->getBool("/options/font/use_fontsdir_user", true)) { + char const *fontsdir = get_path(USER, FONTS); + factory->AddFontsDir(fontsdir); + } + Glib::ustring fontdirs_pref = prefs->getString("/options/font/custom_fontdirs"); + std::vector fontdirs = Glib::Regex::split_simple("\\|", fontdirs_pref); + for (auto &fontdir : fontdirs) { + factory->AddFontsDir(fontdir.c_str()); + } +} + +Application::~Application() +{ + if (_desktops) { + g_error("FATAL: desktops still in list on application destruction!"); + } + + Inkscape::Preferences::unload(); + + if (_menus) { + Inkscape::GC::release(_menus); + _menus = nullptr; + } + + _S_inst = nullptr; // this will probably break things + + refCount = 0; + // gtk_main_quit (); +} + + +Glib::ustring Application::get_symbolic_colors() +{ + Glib::ustring css_str; + gchar colornamed[64]; + gchar colornamedsuccess[64]; + gchar colornamedwarning[64]; + gchar colornamederror[64]; + gchar colornamed_inverse[64]; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetbase = 0x2E3436ff; + guint32 colorsetbase_inverse = colorsetbase ^ 0xffffff00; + guint32 colorsetsuccess = 0x4AD589ff; + guint32 colorsetwarning = 0xF57900ff; + guint32 colorseterror = 0xCC0000ff; + colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); + colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + sp_svg_write_color(colornamed, sizeof(colornamed), colorsetbase); + sp_svg_write_color(colornamedsuccess, sizeof(colornamedsuccess), colorsetsuccess); + sp_svg_write_color(colornamedwarning, sizeof(colornamedwarning), colorsetwarning); + sp_svg_write_color(colornamederror, sizeof(colornamederror), colorseterror); + colorsetbase_inverse = colorsetbase ^ 0xffffff00; + sp_svg_write_color(colornamed_inverse, sizeof(colornamed_inverse), colorsetbase_inverse); + css_str += "*{-gtk-icon-palette: success "; + css_str += colornamedsuccess; + css_str += ", warning "; + css_str += colornamedwarning; + css_str += ", error "; + css_str += colornamederror; + css_str += ";}"; + css_str += "#InkRuler,"; + /* ":not(.rawstyle) > image" works only on images in first level of widget container + if in the future we use a complex widget with more levels and we dont want to tweak the color + here, retaining default we can add more lines like ":not(.rawstyle) > > image" */ + css_str += ":not(.rawstyle) > image"; + css_str += "{color:"; + css_str += colornamed; + css_str += ";}"; + css_str += ".dark .forcebright :not(.rawstyle) > image,"; + css_str += ".dark .forcebright image:not(.rawstyle),"; + css_str += ".bright .forcedark :not(.rawstyle) > image,"; + css_str += ".bright .forcedark image:not(.rawstyle),"; + css_str += ".dark :not(.rawstyle) > image.forcebright,"; + css_str += ".dark image.forcebright:not(.rawstyle),"; + css_str += ".bright :not(.rawstyle) > image.forcedark,"; + css_str += ".bright image.forcedark:not(.rawstyle),"; + css_str += ".inverse :not(.rawstyle) > image,"; + css_str += ".inverse image:not(.rawstyle)"; + css_str += "{color:"; + css_str += colornamed_inverse; + css_str += ";}"; + return css_str; +} + +/** + * \brief Add our CSS style sheets + */ +void Application::add_gtk_css() +{ + using namespace Inkscape::IO::Resource; + // Add style sheet (GTK3) + auto const screen = Gdk::Screen::get_default(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gchar *gtkThemeName = nullptr; + gchar *gtkIconThemeName = nullptr; + Glib::ustring themeiconname; + gboolean gtkApplicationPreferDarkTheme; + GtkSettings *settings = gtk_settings_get_default(); + if (settings) { + g_object_get(settings, "gtk-icon-theme-name", >kIconThemeName, NULL); + g_object_get(settings, "gtk-theme-name", >kThemeName, NULL); + g_object_get(settings, "gtk-application-prefer-dark-theme", >kApplicationPreferDarkTheme, NULL); + g_object_set(settings, "gtk-application-prefer-dark-theme", + prefs->getBool("/theme/preferDarkTheme", gtkApplicationPreferDarkTheme), NULL); + prefs->setString("/theme/defaultTheme", Glib::ustring(gtkThemeName)); + prefs->setString("/theme/defaultIconTheme", Glib::ustring(gtkIconThemeName)); + Glib::ustring gtkthemename = prefs->getString("/theme/gtkTheme"); + if (gtkthemename != "") { + g_object_set(settings, "gtk-theme-name", gtkthemename.c_str(), NULL); + } else { + prefs->setString("/theme/gtkTheme", Glib::ustring(gtkThemeName)); + } + themeiconname = prefs->getString("/theme/iconTheme"); + if (themeiconname != "") { + g_object_set(settings, "gtk-icon-theme-name", themeiconname.c_str(), NULL); + } else { + prefs->setString("/theme/iconTheme", Glib::ustring(gtkIconThemeName)); + } + + } + + g_free(gtkThemeName); + g_free(gtkIconThemeName); + + Glib::ustring style = get_filename(UIS, "style.css"); + if (!style.empty()) { + auto provider = Gtk::CssProvider::create(); + try { + provider->load_from_path(style); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(), + ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + Glib::ustring gtkthemename = prefs->getString("/theme/gtkTheme"); + gtkthemename += ".css"; + style = get_filename(UIS, gtkthemename.c_str(), false, true); + if (!style.empty()) { + if (themeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, themeprovider); + } + if (!themeprovider) { + themeprovider = Gtk::CssProvider::create(); + } + try { + themeprovider->load_from_path(style); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_path(): failed to load '%s'\n(%s)", style.c_str(), + ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, themeprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + } + + if (!colorizeprovider) { + colorizeprovider = Gtk::CssProvider::create(); + } + Glib::ustring css_str = ""; + if (prefs->getBool("/theme/symbolicIcons", false)) { + css_str = get_symbolic_colors(); + } + try { + colorizeprovider->load_from_data(css_str); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, colorizeprovider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +void Application::readStyleSheets(bool forceupd) +{ + SPDocument *document = SP_ACTIVE_DOCUMENT; + Inkscape::XML::Node *root = document->getReprRoot(); + std::vector styles; + for (unsigned i = 0; i < root->childCount(); ++i) { + Inkscape::XML::Node *child = root->nthChild(i); + if (child && strcmp(child->name(), "svg:style") == 0) { + styles.push_back(child); + } + } + if (forceupd || styles.size() > 1) { + document->setStyleSheet(nullptr); + for (auto style : styles) { + gchar const *id = style->attribute("id"); + if (id) { + SPStyleElem *styleelem = dynamic_cast(document->getObjectById(id)); + styleelem->read_content(); + } + } + document->getRoot()->emitModified(SP_OBJECT_MODIFIED_CASCADE); + } +} + +/** Sets the keyboard modifier to map to Alt. + * + * Zero switches off mapping, as does '1', which is the default. + */ +void Application::mapalt(guint maskvalue) +{ + if ( maskvalue < 2 || maskvalue > 5 ) { // MOD5 is the highest defined in gdktypes.h + _mapalt = 0; + } else { + _mapalt = (GDK_MOD1_MASK << (maskvalue-1)); + } +} + +void +Application::crash_handler (int /*signum*/) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Logger; + + static bool recursion = false; + + /* + * reset all signal handlers: any further crashes should just be allowed + * to crash normally. + * */ + signal (SIGSEGV, segv_handler ); + signal (SIGABRT, abrt_handler ); + signal (SIGFPE, fpe_handler ); + signal (SIGILL, ill_handler ); +#ifndef _WIN32 + signal (SIGBUS, bus_handler ); +#endif + + /* Stop bizarre loops */ + if (recursion) { + abort (); + } + recursion = true; + + _crashIsHappening = true; + + EventTracker > tracker("crash"); + tracker.set >("emergency-save"); + + fprintf(stderr, "\nEmergency save activated!\n"); + + time_t sptime = time (nullptr); + struct tm *sptm = localtime (&sptime); + gchar sptstr[256]; + strftime(sptstr, 256, "%Y_%m_%d_%H_%M_%S", sptm); + + gint count = 0; + gchar *curdir = g_get_current_dir(); // This one needs to be freed explicitly + std::vector savednames; + std::vector failednames; + for (std::map::iterator iter = INKSCAPE._document_set.begin(), e = INKSCAPE._document_set.end(); + iter != e; + ++iter) { + SPDocument *doc = iter->first; + Inkscape::XML::Node *repr; + repr = doc->getReprRoot(); + if (doc->isModifiedSinceSave()) { + const gchar *docname; + char n[64]; + + /* originally, the document name was retrieved from + * the sodipod:docname attribute */ + docname = doc->getDocumentName(); + if (docname) { + /* Removes an emergency save suffix if present: /(.*)\.[0-9_]*\.[0-9_]*\.[~\.]*$/\1/ */ + const char* d0 = strrchr ((char*)docname, '.'); + if (d0 && (d0 > docname)) { + const char* d = d0; + unsigned int dots = 0; + while ((isdigit (*d) || *d=='_' || *d=='.') && d>docname && dots<2) { + d -= 1; + if (*d=='.') dots++; + } + if (*d=='.' && d>docname && dots==2) { + size_t len = MIN (d - docname, 63); + memcpy (n, docname, len); + n[len] = '\0'; + docname = n; + } + } + } + if (!docname || !*docname) docname = "emergency"; + + // Emergency filename + char c[1024]; + g_snprintf (c, 1024, "%.256s.%s.%d.svg", docname, sptstr, count); + + // Find a location + const char* locations[] = { + doc->getDocumentBase(), + g_get_home_dir(), + g_get_tmp_dir(), + curdir, + }; + FILE *file = nullptr; + for(auto & location : locations) { + if (!location) continue; // It seems to be okay, but just in case + gchar * filename = g_build_filename(location, c, NULL); + Inkscape::IO::dump_fopen_call(filename, "E"); + file = Inkscape::IO::fopen_utf8name(filename, "w"); + if (file) { + g_snprintf (c, 1024, "%s", filename); // we want the complete path to be stored in c (for reporting purposes) + break; + } + } + + // Save + if (file) { + sp_repr_save_stream (repr->document(), file, SP_SVG_NS_URI); + savednames.push_back(g_strdup (c)); + fclose (file); + } else { + failednames.push_back((doc->getDocumentName()) ? g_strdup(doc->getDocumentName()) : g_strdup (_("Untitled document"))); + } + count++; + } + } + g_free(curdir); + + if (!savednames.empty()) { + fprintf (stderr, "\nEmergency save document locations:\n"); + for (auto i:savednames) { + fprintf (stderr, " %s\n", i); + } + } + if (!failednames.empty()) { + fprintf (stderr, "\nFailed to do emergency save for documents:\n"); + for (auto i:failednames) { + fprintf (stderr, " %s\n", i); + } + } + + // do not save the preferences since they can be in a corrupted state + Inkscape::Preferences::unload(false); + + fprintf (stderr, "Emergency save completed. Inkscape will close now.\n"); + fprintf (stderr, "If you can reproduce this crash, please file a bug at https://inkscape.org/report\n"); + fprintf (stderr, "with a detailed description of the steps leading to the crash, so we can fix it.\n"); + + /* Show nice dialog box */ + + char const *istr = _("Inkscape encountered an internal error and will close now.\n"); + char const *sstr = _("Automatic backups of unsaved documents were done to the following locations:\n"); + char const *fstr = _("Automatic backup of the following documents failed:\n"); + gint nllen = strlen ("\n"); + gint len = strlen (istr) + strlen (sstr) + strlen (fstr); + for (auto i:savednames) { + len = len + SP_INDENT + strlen (i) + nllen; + } + for (auto i:failednames) { + len = len + SP_INDENT + strlen (i) + nllen; + } + len += 1; + gchar *b = g_new (gchar, len); + gint pos = 0; + len = strlen (istr); + memcpy (b + pos, istr, len); + pos += len; + if (!savednames.empty()) { + len = strlen (sstr); + memcpy (b + pos, sstr, len); + pos += len; + for (auto i:savednames) { + memset (b + pos, ' ', SP_INDENT); + pos += SP_INDENT; + len = strlen(i); + memcpy (b + pos, i, len); + pos += len; + memcpy (b + pos, "\n", nllen); + pos += nllen; + } + } + if (!failednames.empty()) { + len = strlen (fstr); + memcpy (b + pos, fstr, len); + pos += len; + for (auto i:failednames) { + memset (b + pos, ' ', SP_INDENT); + pos += SP_INDENT; + len = strlen(i); + memcpy (b + pos, i, len); + pos += len; + memcpy (b + pos, "\n", nllen); + pos += nllen; + } + } + *(b + pos) = '\0'; + + if ( exists() && instance().use_gui() ) { + GtkWidget *msgbox = gtk_message_dialog_new (nullptr, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", b); + gtk_dialog_run (GTK_DIALOG (msgbox)); + gtk_widget_destroy (msgbox); + } + else + { + g_message( "Error: %s", b ); + } + g_free (b); + + tracker.clear(); + Logger::shutdown(); + + fflush(stderr); // make sure buffers are empty before crashing (otherwise output might be suppressed) + + /* on exit, allow restored signal handler to take over and crash us */ +} + +/** + * Menus management + * + */ +bool Application::load_menus() +{ + using namespace Inkscape::IO::Resource; + Glib::ustring filename = get_filename(UIS, MENUS_FILE); + + _menus = sp_repr_read_file(filename.c_str(), nullptr); + if ( !_menus ) { + _menus = sp_repr_read_mem(menus_skeleton, MENUS_SKELETON_SIZE, nullptr); + } + return (_menus != nullptr); +} + + +void +Application::selection_modified (Inkscape::Selection *selection, guint flags) +{ + g_return_if_fail (selection != nullptr); + + if (DESKTOP_IS_ACTIVE (selection->desktop())) { + signal_selection_modified.emit(selection, flags); + } +} + + +void +Application::selection_changed (Inkscape::Selection * selection) +{ + g_return_if_fail (selection != nullptr); + + if (DESKTOP_IS_ACTIVE (selection->desktop())) { + signal_selection_changed.emit(selection); + } +} + +void +Application::subselection_changed (SPDesktop *desktop) +{ + g_return_if_fail (desktop != nullptr); + + if (DESKTOP_IS_ACTIVE (desktop)) { + signal_subselection_changed.emit(desktop); + } +} + + +void +Application::selection_set (Inkscape::Selection * selection) +{ + g_return_if_fail (selection != nullptr); + + if (DESKTOP_IS_ACTIVE (selection->desktop())) { + signal_selection_set.emit(selection); + signal_selection_changed.emit(selection); + } +} + + +void +Application::eventcontext_set (Inkscape::UI::Tools::ToolBase * eventcontext) +{ + g_return_if_fail (eventcontext != nullptr); + g_return_if_fail (SP_IS_EVENT_CONTEXT (eventcontext)); + + if (DESKTOP_IS_ACTIVE (eventcontext->desktop)) { + signal_eventcontext_set.emit(eventcontext); + } +} + + +void +Application::add_desktop (SPDesktop * desktop) +{ + g_return_if_fail (desktop != nullptr); + if (_desktops == nullptr) { + _desktops = new std::vector; + } + + if (std::find(_desktops->begin(), _desktops->end(), desktop) != _desktops->end()) { + g_error("Attempted to add desktop already in list."); + } + + _desktops->insert(_desktops->begin(), desktop); + + signal_activate_desktop.emit(desktop); + signal_eventcontext_set.emit(desktop->getEventContext()); + signal_selection_set.emit(desktop->getSelection()); + signal_selection_changed.emit(desktop->getSelection()); +} + + + +void +Application::remove_desktop (SPDesktop * desktop) +{ + g_return_if_fail (desktop != nullptr); + + if (std::find (_desktops->begin(), _desktops->end(), desktop) == _desktops->end() ) { + g_error("Attempted to remove desktop not in list."); + } + + desktop->setEventContext(""); + + if (DESKTOP_IS_ACTIVE (desktop)) { + signal_deactivate_desktop.emit(desktop); + if (_desktops->size() > 1) { + SPDesktop * new_desktop = *(++_desktops->begin()); + _desktops->erase(std::find(_desktops->begin(), _desktops->end(), new_desktop)); + _desktops->insert(_desktops->begin(), new_desktop); + + signal_activate_desktop.emit(new_desktop); + signal_eventcontext_set.emit(new_desktop->getEventContext()); + signal_selection_set.emit(new_desktop->getSelection()); + signal_selection_changed.emit(new_desktop->getSelection()); + } else { + signal_eventcontext_set.emit(nullptr); + if (desktop->getSelection()) + desktop->getSelection()->clear(); + } + } + + _desktops->erase(std::find(_desktops->begin(), _desktops->end(), desktop)); + + // if this was the last desktop, shut down the program + if (_desktops->empty()) { + this->exit(); + delete _desktops; + _desktops = nullptr; + } +} + + + +void +Application::activate_desktop (SPDesktop * desktop) +{ + g_return_if_fail (desktop != nullptr); + + if (DESKTOP_IS_ACTIVE (desktop)) { + return; + } + + std::vector::iterator i; + + if ((i = std::find (_desktops->begin(), _desktops->end(), desktop)) == _desktops->end()) { + g_error("Tried to activate desktop not added to list."); + } + + SPDesktop *current = _desktops->front(); + + signal_deactivate_desktop.emit(current); + + _desktops->erase (i); + _desktops->insert (_desktops->begin(), desktop); + + signal_activate_desktop.emit(desktop); + signal_eventcontext_set.emit(desktop->getEventContext()); + signal_selection_set(desktop->getSelection()); + signal_selection_changed(desktop->getSelection()); +} + + +/** + * Resends ACTIVATE_DESKTOP for current desktop; needed when a new desktop has got its window that dialogs will transientize to + */ +void +Application::reactivate_desktop (SPDesktop * desktop) +{ + g_return_if_fail (desktop != nullptr); + + if (DESKTOP_IS_ACTIVE (desktop)) { + signal_activate_desktop.emit(desktop); + } +} + + + +SPDesktop * +Application::find_desktop_by_dkey (unsigned int dkey) +{ + for (auto & _desktop : *_desktops) { + if (_desktop->dkey == dkey){ + return _desktop; + } + } + return nullptr; +} + + +unsigned int +Application::maximum_dkey() +{ + unsigned int dkey = 0; + + for (auto & _desktop : *_desktops) { + if (_desktop->dkey > dkey){ + dkey = _desktop->dkey; + } + } + return dkey; +} + + + +SPDesktop * +Application::next_desktop () +{ + SPDesktop *d = nullptr; + unsigned int dkey_current = (_desktops->front())->dkey; + + if (dkey_current < maximum_dkey()) { + // find next existing + for (unsigned int i = dkey_current + 1; i <= maximum_dkey(); ++i) { + d = find_desktop_by_dkey (i); + if (d) { + break; + } + } + } else { + // find first existing + for (unsigned int i = 0; i <= maximum_dkey(); ++i) { + d = find_desktop_by_dkey (i); + if (d) { + break; + } + } + } + + g_assert (d); + return d; +} + + + +SPDesktop * +Application::prev_desktop () +{ + SPDesktop *d = nullptr; + unsigned int dkey_current = (_desktops->front())->dkey; + + if (dkey_current > 0) { + // find prev existing + for (signed int i = dkey_current - 1; i >= 0; --i) { + d = find_desktop_by_dkey (i); + if (d) { + break; + } + } + } + if (!d) { + // find last existing + d = find_desktop_by_dkey (maximum_dkey()); + } + + g_assert (d); + return d; +} + + + +void +Application::switch_desktops_next () +{ + next_desktop()->presentWindow(); +} + +void +Application::switch_desktops_prev() +{ + prev_desktop()->presentWindow(); +} + +void +Application::dialogs_hide() +{ + signal_dialogs_hide.emit(); + _dialogs_toggle = false; +} + + + +void +Application::dialogs_unhide() +{ + signal_dialogs_unhide.emit(); + _dialogs_toggle = true; +} + + + +void +Application::dialogs_toggle() +{ + if (_dialogs_toggle) { + dialogs_hide(); + } else { + dialogs_unhide(); + } +} + +void +Application::external_change() +{ + signal_external_change.emit(); +} + +/** + * fixme: These need probably signals too + */ +void +Application::add_document (SPDocument *document) +{ + g_return_if_fail (document != nullptr); + + // try to insert the pair into the list + if (!(_document_set.insert(std::make_pair(document, 1)).second)) { + //insert failed, this key (document) is already in the list + for (auto & iter : _document_set) { + if (iter.first == document) { + // found this document in list, increase its count + iter.second ++; + } + } + } else { + // insert succeeded, this document is new. + + // Create a selection model tied to the document for running without a GUI. + // We create the model even if there is a GUI as there might not be a window + // tied to the document (which would have its own selection model) as in the + // case where a verb requires a GUI where it's not really needed (conversion + // of verbs to actions will eliminate this need). + g_assert(_selection_models.find(document) == _selection_models.end()); + _selection_models[document] = new AppSelectionModel(document); + } +} + + +// returns true if this was last reference to this document, so you can delete it +bool +Application::remove_document (SPDocument *document) +{ + g_return_val_if_fail (document != nullptr, false); + + for (std::map::iterator iter = _document_set.begin(); + iter != _document_set.end(); + ++iter) { + if (iter->first == document) { + // found this document in list, decrease its count + iter->second --; + if (iter->second < 1) { + // this was the last one, remove the pair from list + _document_set.erase (iter); + + // also remove the selection model + std::map::iterator sel_iter = _selection_models.find(document); + if (sel_iter != _selection_models.end()) { + _selection_models.erase(sel_iter); + } + + return true; + } else { + return false; + } + } + } + + return false; +} + +SPDesktop * +Application::active_desktop() +{ + if (!_desktops || _desktops->empty()) { + return nullptr; + } + + return _desktops->front(); +} + +SPDocument * +Application::active_document() +{ + if (SP_ACTIVE_DESKTOP) { + return SP_ACTIVE_DESKTOP->getDocument(); + } else if (!_document_set.empty()) { + // If called from the command line there will be no desktop + // So 'fall back' to take the first listed document in the Inkscape instance + return _document_set.begin()->first; + } + + return nullptr; +} + +bool +Application::sole_desktop_for_document(SPDesktop const &desktop) { + SPDocument const* document = desktop.doc(); + if (!document) { + return false; + } + for (auto other_desktop : *_desktops) { + SPDocument *other_document = other_desktop->doc(); + if ( other_document == document && other_desktop != &desktop ) { + return false; + } + } + return true; +} + +Inkscape::UI::Tools::ToolBase * +Application::active_event_context () +{ + if (SP_ACTIVE_DESKTOP) { + return SP_ACTIVE_DESKTOP->getEventContext(); + } + + return nullptr; +} + +Inkscape::ActionContext +Application::active_action_context() +{ + if (SP_ACTIVE_DESKTOP) { + return Inkscape::ActionContext(SP_ACTIVE_DESKTOP); + } + + SPDocument *doc = active_document(); + if (!doc) { + return Inkscape::ActionContext(); + } + + return action_context_for_document(doc); +} + +Inkscape::ActionContext +Application::action_context_for_document(SPDocument *doc) +{ + // If there are desktops, check them first to see if the document is bound to one of them + if (_desktops != nullptr) { + for (auto desktop : *_desktops) { + if (desktop->doc() == doc) { + return Inkscape::ActionContext(desktop); + } + } + } + + // Document is not associated with any desktops - maybe we're in command-line mode + std::map::iterator sel_iter = _selection_models.find(doc); + if (sel_iter == _selection_models.end()) { + std::cout << "Application::action_context_for_document: no selection model" << std::endl; + return Inkscape::ActionContext(); + } + return Inkscape::ActionContext(sel_iter->second->getSelection()); +} + + +/*##################### +# HELPERS +#####################*/ + +void +Application::refresh_display () +{ + for (auto & _desktop : *_desktops) { + _desktop->requestRedraw(); + } +} + + +/** + * Handler for Inkscape's Exit verb. This emits the shutdown signal, + * saves the preferences if appropriate, and quits. + */ +void +Application::exit () +{ + //emit shutdown signal so that dialogs could remember layout + signal_shut_down.emit(); + + Inkscape::Preferences::unload(); + //gtk_main_quit (); +} + + + + +Inkscape::XML::Node * +Application::get_menus() +{ + Inkscape::XML::Node *repr = _menus->root(); + g_assert (!(strcmp (repr->name(), "inkscape"))); + return repr->firstChild(); +} + +void +Application::get_all_desktops(std::list< SPDesktop* >& listbuf) +{ + listbuf.insert(listbuf.end(), _desktops->begin(), _desktops->end()); +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkscape.h b/src/inkscape.h new file mode 100644 index 0000000..70a6ce0 --- /dev/null +++ b/src/inkscape.h @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __INKSCAPE_H__ +#define __INKSCAPE_H__ + +/* + * Interface to main application + * + * Authors: + * Lauris Kaplinski + * Liam P. White + * + * Copyright (C) 1999-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layer-model.h" +#include "selection.h" +#include +#include +#include +#include +#include +#include + +class SPDesktop; +class SPDocument; +struct SPColor; + +namespace Inkscape { + +class Application; +namespace UI { +namespace Tools { + +class ToolBase; + +} // namespace Tools +} // namespace UI + +class ActionContext; + +namespace XML { +class Node; +struct Document; +} // namespace XML + +} // namespace Inkscape + +void inkscape_ref (Inkscape::Application & in); +void inkscape_unref(Inkscape::Application & in); + +#define INKSCAPE (Inkscape::Application::instance()) +#define SP_ACTIVE_EVENTCONTEXT (INKSCAPE.active_event_context()) +#define SP_ACTIVE_DOCUMENT (INKSCAPE.active_document()) +#define SP_ACTIVE_DESKTOP (INKSCAPE.active_desktop()) + +class AppSelectionModel { + Inkscape::LayerModel _layer_model; + Inkscape::Selection *_selection; + +public: + AppSelectionModel(SPDocument *doc) { + _layer_model.setDocument(doc); + // TODO: is this really how we should manage the lifetime of the selection? + // I just copied this from the initialization of the Selection in SPDesktop. + // When and how is it actually released? + _selection = Inkscape::GC::release(new Inkscape::Selection(&_layer_model, nullptr)); + } + + Inkscape::Selection *getSelection() const { return _selection; } +}; + +namespace Inkscape { + +class Application { +public: + static Application& instance(); + static bool exists(); + static void create(bool use_gui); + + // returns the mask of the keyboard modifier to map to Alt, zero if no mapping + // Needs to be a guint because gdktypes.h does not define a 'no-modifier' value + guint mapalt() const { return _mapalt; } + + // Sets the keyboard modifier to map to Alt. Zero switches off mapping, as does '1', which is the default + void mapalt(guint maskvalue); + + guint trackalt() const { return _trackalt; } + void trackalt(guint trackvalue) { _trackalt = trackvalue; } + + bool use_gui() const { return _use_gui; } + void use_gui(gboolean guival) { _use_gui = guival; } + + // no setter for this -- only we can control this variable + static bool isCrashing() { return _crashIsHappening; } + + // useful functions + void application_init (gboolean use_gui); + void load_config (const gchar *filename, Inkscape::XML::Document *config, const gchar *skeleton, + unsigned int skel_size, const gchar *e_notreg, const gchar *e_notxml, + const gchar *e_notsp, const gchar *warn); + + bool load_menus(); + bool save_menus(); + Inkscape::XML::Node * get_menus(); + + Inkscape::UI::Tools::ToolBase * active_event_context(); + SPDocument * active_document(); + SPDesktop * active_desktop(); + Glib::RefPtr themeprovider; + Glib::RefPtr colorizeprovider; + // Use this function to get selection model etc for a document + Inkscape::ActionContext action_context_for_document(SPDocument *doc); + Inkscape::ActionContext active_action_context(); + + bool sole_desktop_for_document(SPDesktop const &desktop); + + // Inkscape desktop stuff + void add_desktop(SPDesktop * desktop); + void remove_desktop(SPDesktop* desktop); + void activate_desktop (SPDesktop * desktop); + void switch_desktops_next (); + void switch_desktops_prev (); + void get_all_desktops (std::list< SPDesktop* >& listbuf); + void reactivate_desktop (SPDesktop * desktop); + SPDesktop * find_desktop_by_dkey (unsigned int dkey); + unsigned int maximum_dkey(); + SPDesktop * next_desktop (); + SPDesktop * prev_desktop (); + + void dialogs_hide (); + void dialogs_unhide (); + void dialogs_toggle (); + + void external_change (); + void selection_modified (Inkscape::Selection *selection, guint flags); + void selection_changed (Inkscape::Selection * selection); + void subselection_changed (SPDesktop *desktop); + void selection_set (Inkscape::Selection * selection); + void readStyleSheets(bool forceupd = false); + Glib::ustring get_symbolic_colors(); + void eventcontext_set (Inkscape::UI::Tools::ToolBase * eventcontext); + + // Moved document add/remove functions into public inkscape.h as they are used + // (rightly or wrongly) by console-mode functions + void add_document (SPDocument *document); + bool remove_document (SPDocument *document); + + // fixme: This has to be rethought + void refresh_display (); + + // fixme: This also + void exit (); + + static void crash_handler(int signum); + + // nobody should be accessing our reference count, so it's made private. + friend void ::inkscape_ref (Application & in); + friend void ::inkscape_unref(Application & in); + + // signals + + // one of selections changed + sigc::signal signal_selection_changed; + // one of subselections (text selection, gradient handle, etc) changed + sigc::signal signal_subselection_changed; + // one of selections modified + sigc::signal signal_selection_modified; + // one of selections set + sigc::signal signal_selection_set; + // tool switched + sigc::signal signal_eventcontext_set; + // some desktop got focus + sigc::signal signal_activate_desktop; + // some desktop lost focus + sigc::signal signal_deactivate_desktop; + // user change theme + sigc::signal signal_change_theme; + // these are orphaned signals (nothing emits them and nothing connects to them) + sigc::signal signal_destroy_document; + sigc::signal signal_color_set; + + // inkscape is quitting + sigc::signal signal_shut_down; + // user pressed F12 + sigc::signal signal_dialogs_hide; + // user pressed F12 + sigc::signal signal_dialogs_unhide; + // a document was changed by some external means (undo or XML editor); this + // may not be reflected by a selection change and thus needs a separate signal + sigc::signal signal_external_change; + + void set_pdf_poppler(bool p) { + _pdf_poppler = p; + } + bool get_pdf_poppler() { + return _pdf_poppler; + } + void set_pdf_page(gint page) { + _pdf_page = page; + } + gint get_pdf_page() { + return _pdf_page; + } + + void add_gtk_css(); + void add_icon_theme(); + + private: + static Inkscape::Application * _S_inst; + + Application(bool use_gui); + ~Application(); + + Application(Application const&); // no copy + Application& operator=(Application const&); // no assign + Application* operator&() const; // no pointer access + + Inkscape::XML::Document *_menus = nullptr; + std::map _document_set; + std::map _selection_models; + std::vector *_desktops = nullptr; + + unsigned refCount = 1; + bool _dialogs_toggle = true; + guint _mapalt = GDK_MOD1_MASK; + guint _trackalt = false; + static bool _crashIsHappening; + bool _use_gui = false; + gint _pdf_page = 1; + bool _pdf_poppler = false; +}; + +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/inkscape.rc b/src/inkscape.rc new file mode 100644 index 0000000..677dfb1 --- /dev/null +++ b/src/inkscape.rc @@ -0,0 +1,30 @@ +APPLICATION_ICON ICON "${CMAKE_SOURCE_DIR}/share/branding/inkscape.ico" +1000 BITMAP "${CMAKE_SOURCE_DIR}/src/show-preview.bmp" +1 24 "${CMAKE_BINARY_DIR}/src/${FILE_NAME}-manifest.xml" + +1 VERSIONINFO + FILEVERSION ${INKSCAPE_VERSION_MAJOR},${INKSCAPE_VERSION_MINOR},${INKSCAPE_VERSION_PATCH},0 + PRODUCTVERSION ${INKSCAPE_VERSION_MAJOR},${INKSCAPE_VERSION_MINOR},${INKSCAPE_VERSION_PATCH},0 + FILEOS 0x40004 + FILETYPE 1 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "Comments", "Published under the GNU GPL" + VALUE "CompanyName", "Inkscape project" + VALUE "FileDescription", "${PROGRAM_DESCRIPTION}" + VALUE "FileVersion", "${INKSCAPE_VERSION}" + VALUE "InternalName", "${PROGRAM_NAME}" + VALUE "LegalCopyright", "© ${COPYRIGHT_YEAR} Inkscape project" + VALUE "OriginalFilename", "${FILE_NAME}.exe" + VALUE "ProductName", "${PROGRAM_NAME}" + VALUE "ProductVersion", "${INKSCAPE_VERSION}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1252 + END +END diff --git a/src/inkview-application.cpp b/src/inkview-application.cpp new file mode 100644 index 0000000..7823d9f --- /dev/null +++ b/src/inkview-application.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkview - An SVG file viewer. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // Defines ENABLE_NLS +#endif + +#include + +#include // Internationalization + +#include + +#include "inkview-application.h" + +#include "inkscape.h" // Inkscape::Application +#include "inkscape-version.h" // Inkscape version +#include "include/glibmm_version.h" +#include "inkgc/gc-core.h" // Garbage Collecting init +#include "inkview-window.h" + +#ifdef ENABLE_NLS +// Native Language Support - shouldn't this always be used? +#include "helper/gettext.h" // gettext init +#endif // ENABLE_NLS + +// This is a bit confusing as there are two ways to handle command line arguments and files +// depending on if the Gio::APPLICATION_HANDLES_OPEN and/or Gio::APPLICATION_HANDLES_COMMAND_LINE +// flags are set. If the open flag is set and the command line not, the all the remainng arguments +// after calling on_handle_local_options() are assumed to be filenames. + +InkviewApplication::InkviewApplication() + : Gtk::Application("org.inkscape.Inkview", + Gio::APPLICATION_HANDLES_OPEN | // Use default file opening. + Gio::APPLICATION_NON_UNIQUE ) // Allows different instances of Inkview to run at same time. + , fullscreen(false) + , recursive(false) + , timer(0) + , scale(1.0) + , preload(false) +{ + // ==================== Initializations ===================== + // Garbage Collector + Inkscape::GC::init(); + +#ifdef ENABLE_NLS + // Native Language Support (shouldn't this always be used?). + Inkscape::initialize_gettext(); +#endif + + Glib::set_application_name(N_("Inkview - An SVG File Viewer")); // After gettext() init. + +#if GLIBMM_CHECK_VERSION(2,56,0) + // Additional informational strings for --help output + // TODO: Claims to be translated automatically, but seems broken, so pass already translated strings + set_option_context_parameter_string(_("path1 [path2 pathN]]")); + set_option_context_summary(_("Open one or more SVG files (or folders containing SVG files) for viewing.")); +#endif + + // Will automatically handle character conversions. + // Note: OPTION_TYPE_FILENAME => std::string, OPTION_TYPE_STRING => Glib::ustring. + + add_main_option_entry(OPTION_TYPE_BOOL, "version", 'V', N_("Print Inkview version"), ""); + add_main_option_entry(OPTION_TYPE_BOOL, "fullscreen", 'f', N_("Launch in fullscreen mode"), ""); + add_main_option_entry(OPTION_TYPE_BOOL, "recursive", 'r', N_("Search folders recursively"), ""); + add_main_option_entry(OPTION_TYPE_INT, "timer", 't', N_("Change image every NUMBER seconds"), N_("NUMBER")); + add_main_option_entry(OPTION_TYPE_DOUBLE, "scale", 's', N_("Scale image by factor NUMBER"), N_("NUMBER")); + add_main_option_entry(OPTION_TYPE_BOOL, "preload", 'p', N_("Preload files"), ""); + + signal_handle_local_options().connect(sigc::mem_fun(*this, &InkviewApplication::on_handle_local_options)); + + // This is normally called for us... but after the "handle_local_options" signal is emitted. If + // we want to rely on actions for handling options, we need to call it here. This appears to + // have no unwanted side-effect. It will also trigger the call to on_startup(). + register_application(); +} + +Glib::RefPtr InkviewApplication::create() +{ + return Glib::RefPtr(new InkviewApplication()); +} + +void +InkviewApplication::on_startup() +{ + Gtk::Application::on_startup(); + + // Inkscape::Application should disappear! + Inkscape::Application::create(true); +} + + +// Open document window with default document. Either this or on_open() is called. +void +InkviewApplication::on_activate() +{ + // show file chooser dialog if no files/folders are given on the command line + // TODO: A FileChooserNative would be preferential, but offers no easy way to allow files AND folders + Glib::ustring title = _("Select Files or Folders to view"); + Gtk::FileChooserDialog file_chooser(title + "…", Gtk::FILE_CHOOSER_ACTION_OPEN); + file_chooser.add_button(_("Select"), 42); // use custom response ID that is not intercepted by the file chooser + // (allows to pick files AND folders) + file_chooser.set_select_multiple(); + + Glib::RefPtr file_filter = Gtk::FileFilter::create(); + file_filter->add_pattern("*.svg"); + file_filter->set_name(_("Scalable Vector Graphics")); + file_chooser.add_filter(file_filter); + + int res = file_chooser.run(); + if (res == 42) { + auto files = file_chooser.get_files(); + if (!files.empty()) { + on_open(files, ""); + } + } +} + +// Open document window for each file. Either this or on_activate() is called. +void +InkviewApplication::on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) +{ + try { + window = new InkviewWindow(files, fullscreen, recursive, timer, scale, preload); + } catch (const InkviewWindow::NoValidFilesException&) { + std::cerr << _("Error") << ": " << _("No (valid) files to open.") << std::endl; + exit(1); + } + + window->show_all(); + add_window(*window); +} + + +// ========================= Callbacks ========================== + +/* + * Handle command line options callback. + */ +int +InkviewApplication::on_handle_local_options(const Glib::RefPtr& options) +{ + if (!options) { + std::cerr << "InkviewApplication::on_handle_local_options: options is null!" << std::endl; + return -1; // Keep going + } + + if (options->contains("version")) { + std::cout << "Inkscape " << Inkscape::version_string << std::endl; + return EXIT_SUCCESS; + } + + if (options->contains("fullscreen")) { + fullscreen = true; + } + + if (options->contains("recursive")) { + recursive = true; + } + + if (options->contains("timer")) { + options->lookup_value("timer", timer); + } + + if (options->contains("scale")) { + options->lookup_value("scale", scale); + } + + if (options->contains("preload")) { + options->lookup_value("preload", preload); + } + + return -1; // Keep going +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkview-application.h b/src/inkview-application.h new file mode 100644 index 0000000..d22ea7c --- /dev/null +++ b/src/inkview-application.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkview - An SVG file viewer. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + + +#ifndef INKVIEW_APPLICATION_H +#define INKVIEW_APPLICATION_H + +#include + +class InkviewWindow; + +class InkviewApplication : public Gtk::Application +{ +protected: + InkviewApplication(); + +public: + static Glib::RefPtr create(); + +protected: + void on_startup() override; + void on_activate() override; + void on_open(const Gio::Application::type_vec_files& files, const Glib::ustring& hint) override; + +private: + // Callbacks + int on_handle_local_options(const Glib::RefPtr& options); + + // Command line options + bool fullscreen; + bool recursive; + int timer; + double scale; + bool preload; + + InkviewWindow* window; +}; + +#endif // INKVIEW_APPLICATION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkview-main.cpp b/src/inkview-main.cpp new file mode 100644 index 0000000..5d60945 --- /dev/null +++ b/src/inkview-main.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkview - An SVG file viewer. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#ifdef _WIN32 +#include // SetConsoleOutputCP +#include // _O_BINARY +#endif + +#include "inkview-application.h" + +int main(int argc, char *argv[]) +{ +#ifdef _WIN32 + // temporarily switch console encoding to UTF8 while Inkview runs + // as everything else is a mess and it seems to work just fine + const unsigned int initial_cp = GetConsoleOutputCP(); + SetConsoleOutputCP(CP_UTF8); + fflush(stdout); // empty buffer, just to be safe (see warning in documentation for _setmode) + _setmode(_fileno(stdout), _O_BINARY); // binary mode seems required for this to work properly +#endif + + auto application = InkviewApplication::create(); + int ret = application->run(argc, argv); + +#ifdef _WIN32 + // switch back to initial console encoding + SetConsoleOutputCP(initial_cp); +#endif + + return ret; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkview-window.cpp b/src/inkview-window.cpp new file mode 100644 index 0000000..57e7f47 --- /dev/null +++ b/src/inkview-window.cpp @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkview - An SVG file viewer. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + + +#include "inkview-window.h" + +#include + +#include "document.h" + +#include "ui/monitor.h" +#include "ui/view/svg-view-widget.h" + +#include "util/units.h" + +InkviewWindow::InkviewWindow(const Gio::Application::type_vec_files files, + bool fullscreen, + bool recursive, + int timer, + double scale, + bool preload + ) + : _files(files) + , _fullscreen(fullscreen) + , _recursive(recursive) + , _timer(timer) + , _scale(scale) + , _preload(preload) + , _index(-1) + , _view(nullptr) + , _controlwindow(nullptr) +{ + _files = create_file_list(_files); + + if (_preload) { + preload_documents(); + } + + if (_files.empty()) { + throw NoValidFilesException(); + } + + _documents.resize( _files.size(), nullptr); // We keep _documents and _files in sync. + + // Callbacks + signal_key_press_event().connect(sigc::mem_fun(*this, &InkviewWindow::key_press), false); + + if (_timer) { + Glib::signal_timeout().connect_seconds(sigc::mem_fun(*this, &InkviewWindow::on_timer), _timer); + } + + // Actions + add_action( "show_first", sigc::mem_fun(*this, &InkviewWindow::show_first) ); + add_action( "show_prev", sigc::mem_fun(*this, &InkviewWindow::show_prev) ); + add_action( "show_next", sigc::mem_fun(*this, &InkviewWindow::show_next) ); + add_action( "show_last", sigc::mem_fun(*this, &InkviewWindow::show_last) ); + + // ToDo: Add Pause, Resume. + + if (_fullscreen) { + Gtk::Window::fullscreen(); + } + + // Show first file + activate_action( "show_first" ); +} + +std::vector > +InkviewWindow::create_file_list(const std::vector >& files) +{ + std::vector > valid_files; + + static bool first = true; + + for (auto file : files) { + Gio::FileType type = file->query_file_type(); + switch (type) { + case Gio::FILE_TYPE_NOT_KNOWN: + std::cerr << "InkviewWindow: File or directory does not exist: " + << file->get_basename() << std::endl; + break; + + case Gio::FILE_TYPE_REGULAR: + { + // Only look at SVG and SVGZ files. + std::string basename = file->get_basename(); + std::string extension = basename.substr(basename.find_last_of(".") + 1); + if (extension == "svg" || extension == "svgz") { + valid_files.push_back(file); + } + break; + } + + case Gio::FILE_TYPE_DIRECTORY: + { + if (_recursive || first) { + // No easy way to get children of directory! + Glib::RefPtr children = file->enumerate_children(); + Glib::RefPtr info; + std::vector > input; + while ((info = children->next_file())) { + input.push_back(children->get_child(info)); + } + auto new_files = create_file_list(input); + valid_files.insert(valid_files.end(), new_files.begin(), new_files.end()); + } + break; + } + default: + std::cerr << "InkviewWindow: Unknown file type: " << type << std::endl; + } + first = false; + } + + return valid_files; +} + +void +InkviewWindow::update_title() +{ + Glib::ustring title(_documents[_index]->getDocumentName()); + + if (_documents.size() > 1) { + title += Glib::ustring::compose(" (%1/%2)", _index+1, _documents.size()); + } + + set_title(title); +} + +// Returns true if successfully shows document. +bool +InkviewWindow::show_document(SPDocument* document) +{ + document->ensureUpToDate(); // Crashes on some documents if this isn't called! + + // Resize window: (Might be better to use get_monitor_geometry_at_window(this)) + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + int width = MIN((int)document->getWidth().value("px") * _scale, monitor_geometry.get_width()); + int height = MIN((int)document->getHeight().value("px") * _scale, monitor_geometry.get_height()); + resize (width, height); + + if (_view) { + _view->setDocument(document); + } else { + _view = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(document)); + add (*_view); + } + + update_title(); + + return true; +} + + +// Load document, if fail, remove entry from lists. +SPDocument* +InkviewWindow::load_document() +{ + SPDocument* document = _documents[_index]; + + if (!document) { + // We need to load document. ToDo: Pass Gio::File. Is get_base_name() better? + document = SPDocument::createNewDoc (_files[_index]->get_parse_name().c_str(), true, false); + if (document) { + // We've successfully loaded it! + _documents[_index] = document; + } + } + + if (!document) { + // Failed to load document, remove from vectors. + _documents.erase(std::next(_documents.begin(), _index)); + _files.erase( std::next( _files.begin(), _index)); + } + + return document; +} + + + +void +InkviewWindow::preload_documents() +{ + for (auto it =_files.begin(); it != _files.end(); ) { + + SPDocument* document = + SPDocument::createNewDoc ((*it)->get_parse_name().c_str(), true, false); + if (document) { + _documents.push_back(document); + ++it; + } else { + it = _files.erase(it); + } + } +} + +static std::string window_markup = R"( + + + + + + + True + True + + + True + go-first + + + + + + + True + True + + + True + go-previous + + + + + + + True + False + + + True + go-next + + + + + + + True + False + + + True + go-last + + + + + + + + +)"; + +void +InkviewWindow::show_control() +{ + if (!_controlwindow) { + + auto builder = Gtk::Builder::create(); + try + { + builder->add_from_string(window_markup); + } + catch (const Glib::Error& err) + { + std::cerr << "InkviewWindow::show_control: builder failed: " << err.what() << std::endl; + return; + } + + + builder->get_widget("ControlWindow", _controlwindow); + if (!_controlwindow) { + std::cerr << "InkviewWindow::show_control: Control Window not found!" << std::endl; + return; + } + + // Need to give control window access to viewer window's actions. + Glib::RefPtr viewer = get_action_group("win"); + if (viewer) { + _controlwindow->insert_action_group("viewer", viewer); + } + + // Gtk::Button not derived from Gtk::Actionable due to ABI issues. Must use Gtk. + // Fixed in Gtk4. In Gtk4 this can be replaced by setting the action in the interface. + Gtk::Button* button; + builder->get_widget("show-first", button); + gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_first"); + builder->get_widget("show-prev", button); + gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_prev"); + builder->get_widget("show-next", button); + gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_next"); + builder->get_widget("show-last", button); + gtk_actionable_set_action_name( GTK_ACTIONABLE(button->gobj()), "viewer.show_last"); + + _controlwindow->set_resizable(false); + _controlwindow->set_transient_for(*this); + _controlwindow->show_all(); + + } else { + _controlwindow->present(); + } +} + +// Next document +void +InkviewWindow::show_next() +{ + ++_index; + + SPDocument* document = nullptr; + + while (_index < _documents.size() && !document) { + document = load_document(); + } + + if (document) { + // Show new document + show_document(document); + } else { + // Failed to load new document, keep current. + --_index; + } +} + +// Previous document +void +InkviewWindow::show_prev() +{ + SPDocument* document = nullptr; + int old_index = _index; + + while (_index > 0 && !document) { + --_index; + document = load_document(); + } + + if (document) { + // Show new document + show_document(document); + } else { + // Failed to load new document, keep current. + _index = old_index; + } +} + +// Show first document +void +InkviewWindow::show_first() +{ + _index = -1; + show_next(); +} + +// Show last document +void +InkviewWindow::show_last() +{ + _index = _documents.size(); + show_prev(); +} + +bool +InkviewWindow::key_press(GdkEventKey* event) +{ + switch (event->keyval) { + case GDK_KEY_Up: + case GDK_KEY_Home: + show_first(); + break; + + case GDK_KEY_Down: + case GDK_KEY_End: + show_last(); + break; + + case GDK_KEY_F11: + if (_fullscreen) { + unfullscreen(); + _fullscreen = false; + } else { + fullscreen(); + _fullscreen = true; + } + break; + + case GDK_KEY_Return: + show_control(); + break; + + case GDK_KEY_KP_Page_Down: + case GDK_KEY_Page_Down: + case GDK_KEY_Right: + case GDK_KEY_space: + show_next(); + break; + + case GDK_KEY_KP_Page_Up: + case GDK_KEY_Page_Up: + case GDK_KEY_Left: + case GDK_KEY_BackSpace: + show_prev(); + break; + + case GDK_KEY_Escape: + case GDK_KEY_q: + case GDK_KEY_Q: + close(); + break; + + default: + break; + } + return false; +} + +bool +InkviewWindow::on_timer() +{ + show_next(); + + // Stop if at end. + if (_index >= _documents.size() - 1) { + return false; + } + return true; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/inkview-window.h b/src/inkview-window.h new file mode 100644 index 0000000..8cf6915 --- /dev/null +++ b/src/inkview-window.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkview - An SVG file viewer. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#ifndef INKVIEW_WINDOW_H +#define INKVIEW_WINDOW_H + +#include + +class SPDocument; + +namespace Inkscape { +namespace UI { +namespace View { + class SVGViewWidget; +} +} +} + + +class InkviewWindow : public Gtk::ApplicationWindow { + +public: + InkviewWindow(const Gio::Application::type_vec_files files, + bool fullscreen, bool recursive, int timer, double scale, bool preload); + + class NoValidFilesException : public std::exception {}; + +private: + std::vector > + create_file_list(const std::vector >& files); + void update_title(); + bool show_document(SPDocument* document); + SPDocument* load_document(); + void preload_documents(); + + Gio::Application::type_vec_files _files; + bool _fullscreen; + bool _recursive; + int _timer; + double _scale; + bool _preload; + + int _index; + std::vector _documents; + + Inkscape::UI::View::SVGViewWidget* _view; + Gtk::Window* _controlwindow; + + // Callbacks + void show_control(); + void show_next(); + void show_prev(); + void show_first(); + void show_last(); + + bool key_press(GdkEventKey* event); + bool on_timer(); +}; + +#endif // INKVIEW_WINDOW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt new file mode 100644 index 0000000..3a5d8ae --- /dev/null +++ b/src/io/CMakeLists.txt @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(io_SRC + dir-util.cpp + file.cpp + file-export-cmd.cpp + resource.cpp + resource-manager.cpp + stream/bufferstream.cpp + stream/gzipstream.cpp + stream/inkscapestream.cpp + stream/stringstream.cpp + stream/uristream.cpp + stream/xsltstream.cpp + sys.cpp + http.cpp + + # ------- + # Headers + dir-util.h + file.h + file-export-cmd.h + resource.h + resource-manager.h + stream/bufferstream.h + stream/gzipstream.h + stream/inkscapestream.h + stream/stringstream.h + stream/uristream.h + stream/xsltstream.h + sys.h + http.h +) + +# add_inkscape_lib(io_LIB "${io_SRC}") +add_inkscape_source("${io_SRC}") diff --git a/src/io/README b/src/io/README new file mode 100644 index 0000000..9d971b7 --- /dev/null +++ b/src/io/README @@ -0,0 +1,35 @@ + +This directory contains code related to input and output. + + +Note that input and output to particular file formats may be implemented elsewhere: + +1. Internal extensions. See src/extensions/internal + + Input: CDR, EMF, WMF VSD WPG + Output: SVG, PNG (via Cairo), PS, EPS, PDF, POV, ODF, EMF, WMF, XAML, LaTex. + +2. External extensions (Python). See share/extensions + + Input: PS, EPS, PDF, SK, XAML, DXF, DIA, AI, ? + Output: SVG (Layers/Optimized), SK1, XCF, HTML5, SIF, PTL, HPGL, DXF, FXG, XAML(?), CANVAS, ? + +3. SVG (XML) low level code: src/xml/repr-io.h + + +To do: + +1. Move all file related code here (other than extensions). +2. Move extension input/output code into subdirectories within src/extensions/internal and share/extensions. +3. Separate out creating a document and creating a document window. The former belongs here, the later in src/ui. +4. Use std::string for all file names and use glibmm file utilities. +5. Use Glib::ustring for URI's and use Inkscape's URI utilities (if not available in glibmm). +6. Rewrite file export code to share a common base and to allow easy export of objects. Should be Gio::Action based. + Things like cropping, selecting an object, hiding other objects, etc. should be done before the document is passed + to the file type specific export code. This way, we can use the same export options for all file types (where they + make sense). Only type specific options (e.g. PS Level) should be handled by the type specific code. + + +Files to move here: + src/util/ziptool.h (used by ODF, note SVG uses Inkscape::IO::GzipInputStream, can that be used instead?). + src/helper/png-write.h diff --git a/src/io/crystalegg.xml b/src/io/crystalegg.xml new file mode 100644 index 0000000..0e916e7 --- /dev/null +++ b/src/io/crystalegg.xml @@ -0,0 +1,767 @@ + + + + + + The Crystal Egg + H. G.Wells + + + + + + + There was, until a year ago, a little and very grimy-looking shop +near Seven Dials over which, in weather-worn yellow lettering, the name +of "C. Cave, Naturalist and Dealer in Antiquities," was inscribed. The +contents of its window were curiously variegated. They comprised some +elephant tusks and an imperfect set of chessmen, beads and weapons, a +box of eyes, two skulls of tigers and one human, several moth-eaten +stuffed monkeys (one holding a lamp), an old-fashioned cabinet, a +flyblown ostrich egg or so, some fishing-tackle, and an extraordinarily +dirty, empty glass fish tank. There was also, at the moment the story +begins, a mass of crystal, worked into the shape of an egg and +brilliantly polished. And at that two people, who stood outside the +window, were looking, one of them a tall, thin clergyman, the other a +black-bearded young man of dusky complexion and unobtrusive costume. The +dusky young man spoke with eager gestulation, and seemed anxious for his +companion to purchase the article. + + + + While they were there, Mr. Cave came into his shop, his beard still +wagging with the bread and butter of his tea. When he saw these men and +the object of their regard, his countenance fell. He glanced guiltily +over his shoulder, and softly shut the door. He was a little old man, +with pale face and peculiar watery blue eyes; his hair was a dirty grey, +and he wore a shabby blue frock-coat, an ancient silk hat, and carpet +slippers very much down at heel. He remained watching the two men as +they talked. The clergyman went deep into his trouser pocket, examined a +handful of money, and showed his teeth in an agreeable smile. Mr. Cave +seemed still more depressed when they came into the shop. + + + + The clergyman, without any ceremony, asked the price of the crystal +egg. Mr. Cave glanced nervously towards the door leading into the +parlour, and said five pounds. The clergyman protested that the price +was high, to his companion as well as to Mr. Cave -- it was, indeed, +very much more than Mr. Cave had intended to ask, when he had stocked +the article -- and an attempt at bargaining ensued. Mr. Cave stepped to +the shop-door, and held it open. "Five pounds is my price," he said, as +though he wished to save himself the trouble of unprofitable discussion. +As he did so, the upper portion of a woman's face appeared above the +blind in the glass upper panel of the door leading into the parlour, and +stared curiously at the two customers. "Five pounds is my price," said +Mr. Cave, with a quiver in his voice. + + + + The swarthy young man had so far remained a spectator, watching Cave +keenly. Now he spoke. "Give him five pounds," he said. The clergyman +glanced at him to see if he were in earnest, and, when he looked at Mr. +Cave again, he saw that the latter's face was white. "It's a lot of +money," said the clergyman, and, diving into his pocket, began counting +his resources. He had little more than thirty shillings, and he appealed +to his companion, with whom he seemed to be on terms of considerable +intimacy. This gave Mr. Cave an opportunity of collecting his thoughts, +and he began to explain in an agitated manner that the crystal was not, +as a matter of fact, entirely free for sale. His two customers were +naturally surprised at this, and inquired why he had not thought of that +before he began to bargain. Mr. Cave became confused, but he stuck to +his story, that the crystal was not in the market that afternoon, that a +probable purchaser of it had already appeared. The two, treating this as +an attempt to raise the price still further, made as if they would leave +the shop. But at this point the parlour door opened, and the owner of +the dark fringe and the little eyes appeared. + + + + She was a coarse-featured, corpulent woman, younger and very much +larger than Mr. Cave; she walked heavily, and her face was flushed. +"That crystal is for sale, she said. "And five pounds is a good enough +price for it. I can't think what you're about, Cave, not to take the +gentleman's offer!" + + + + Mr. Cave, greatly perturbed by the irruption, looked angrily at her +over the rims of his spectacles, and, without excessive assurance, +asserted his right to manage his business in his own way. An altercation +began. The two customers watched the scene with interest and some +amusement, occasionally assisting Mrs. Cave with suggestions. Mr. Cave, +hard driven, persisted in a confused and impossible story of an enquiry +for the crystal that morning, and his agitation became painful. But he +stuck to his point with extraordinary persistence. +It was the young Oriental who ended this curious controversy. He +proposed that they should call again in the course of two days -- so as +to give the alleged enquirer a fair chance. "And then we must insist," +said the clergyman. "Five pounds." Mrs. Cave took it on herself to +apologise for her husband, explaining that he was sometimes "a little +odd," and as the two customers left, the couple prepared for a free +discussion of the incident in all its bearings. + + + + Mrs. Cave talked to her husband with singular directness. The poor +little man, quivering with emotion, muddled himself between his stories, +maintaining on the one hand that he had another customer in view, and on +the other asserting that the crystal was honestly worth ten guineas. +"Why did you ask five pounds?" said his wife. "Do let me manage my +business my own way!" said Mr. Cave. + + + + Mr. Cave had living with him a step-daughter and a step-son, and at +supper that night the transaction was re-discussed. None of them had a +high opinion of Mr. Cave's business methods, and this action seemed a +culminating folly. + + + + "It's my opinion he's refused that crystal before," said the +step-son, a loose-limbed lout of eighteen. + + + + "But Five Pounds!" said the step-daughter, an argumentative young +woman of six-and-twenty. + + + + Mr. Cave's answers were wretched; he could only mumble weak +assertions that he knew his own business best. They drove him from his +half-eaten supper into the shop, to close it for the night, his ears +aflame and tears of vexation behind his spectacles. "Why had he left the +crystal in the window so long? The folly of it!" That was the trouble +closest in his mind. For a time he could see no way of evading sale. + + + + After supper his step-daughter and step-son smartened themselves up +and went out and his wife retired upstairs to reflect upon the business +aspects of the crystal, over a little sugar and lemon and so forth in +hot water. Mr. Cave went into the shop, and stayed there until late, +ostensibly to make ornamental rockeries for gold-fish cases but really +for a private purpose that will be better explained later. The next day +Mrs. Cave found that the crystal had been removed from the window, and +was lying behind some second-hand books on angling. She replaced it in a +conspicuous position. But she did not argue further about it, as a +nervous headache disinclined her from debate. Mr. Cave was always +disinclined. The day passed disagreeably. Mr. Cave was, if anything, +more absent-minded than usual, and uncommonly irritable withal. In the +afternoon, when his wife was taking her customary sleep, he removed the +crystal from the window again. + + + + The next day Mr. Cave had to deliver a consignment of dog-fish at one +of the hospital schools, where they were needed for dissection. In his +absence Mrs. Cave's mind reverted to the topic of the crystal, and the +methods of expenditure suitable to a windfall of five pounds. She had +already devised some very agreeable expedients, among others a dress of +green silk for herself and a trip to Richmond, when a jangling of the +front door bell summoned her into the shop. The customer was an +examination coach who came to complain of the non-delivery of certain +frogs asked for the previous day. Mrs. Cave did not approve of this +particular branch of Mr. Cave's business, and the gentleman, who had +called in a somewhat aggressive mood, retired after a brief exchange of +words -- entirely civil so far as he was concerned. Mrs. Cave's eye then +naturally turned to the window; for the sight of the crystal was an +assurance of the five pounds and of her dreams. What was her surprise to +find it gone! + + + + She went to the place behind the locker on the counter, where she had +discovered it the day before. It was not there; and she immediately +began an eager search about the shop. + + + + When Mr. Cave returned from his business with the dog-fish, about a +quarter to two in the afternoon, he found the shop in some confusion, +and his wife, extremely exasperated and on her knees behind the counter, +routing among his taxidermic material. Her face came up hot and angry +over the counter, as the jangling bell announced his return, and she +forthwith accused him of "hiding it." + + + + "Hid what?" asked Mr. Cave. + + + + "The crystal!" + + + + At that Mr. Cave, apparently much surprised, rushed to the window. +"Isn't it here?" he said. "Great Heavens! what has become of it?" + + + + Just then, Mr. Cave's step-son re-entered the shop from the inner +room -- he had come home a minute or so before Mr. Cave -- and he was +blaspheming freely. He was apprenticed to a second-hand furniture dealer +down the road, but he had his meals at home, and he was naturally +annoyed to find no dinner ready. + + + + But, when he heard of the loss of the crystal, he forgot his meal, +and his anger was diverted from his mother to his step-father. Their +first idea, of course, was that he had hidden it. But Mr. Cave stoutly +denied all knowledge of its fate -- freely offering his bedabbled +affidavit in the matter -- and at last was worked up to the point of +accusing, first, his wife and then his step-son of having taken it with +a view to a private sale. So began an exceedingly acrimonious and +emotional discussion, which ended for Mrs. Cave in a peculiar nervous +condition midway between hysterics and amuck, and caused the step-son to +be half-an-hour late at the furniture establishment in the afternoon. +Mr. Cave took refuge from his wife's emotions in the shop. + + + + In the evening the matter was resumed, with less passion and in a +judicial spirit, under the presidency of the step-daughter. The supper +passed unhappily and culminated in a painful scene. Mr. Cave gave way at +last to extreme exasperation, and went out banging the front door +violently. The rest of the family, having discussed him with the freedom +his absence warranted, hunted the house from garret to cellar, hoping to +light upon the crystal. + + + + The next day the two customers called again. They were received by +Mrs. Cave almost in tears. It transpired that no one could imagine all +that she had stood from Cave at various times in her married pilgrimage. +. . . She also gave a garbled account of the disappearance. The +clergyman and the Oriental laughed silently at one another, and said it +was very extraordinary. As Mrs. Cave seemed disposed to give them the +complete history of her life they made to leave the shop. Thereupon Mrs. +Cave, still clinging to hope, asked for the clergyman's address, so +that, if she could get anything out of Cave, she might communicate it. +The address was duly given, but apparently was afterwards mislaid. Mrs. +Cave can remember nothing about it. + + + + In the evening of that day, the Caves seem to have exhausted their +emotions, and Mr. Cave, who had been out in the afternoon, supped in a +gloomy isolation that contrasted pleasantly with the impassioned +controversy of the previous days. For some time matters were very badly +strained in the Cave household, but neither crystal nor customer +reappeared. + + + + Now, without mincing the matter, we must admit that Mr. Cave was a +liar. He knew perfectly well where the crystal was. It was in the rooms +of Mr. Jacoby Wace, Assistant Demonstrator at St. Catherine's Hospital, +Westbourne Street. It stood on the sideboard partially covered by a +black velvet cloth, and beside a decanter of American whisky. It is from +Mr. Wace, indeed, that the particulars upon which this narrative is +based were derived. Cave had taken off the thing to the hospital hidden +in the dog-fish sack, and there had pressed the young investigator to +keep it for him. Mr. Wace was a little dubious at first. His +relationship to Cave was peculiar. He had a taste for singular +characters, and he had more than once invited the old man to smoke and +drink in his rooms, and to unfold his rather amusing views of life in +general and of his wife in particular. Mr. Wace had encountered Mrs. +Cave, too, on occasions when Mr. Cave was not at home to attend to him. +He knew the constant interference to which Cave was subjected, and +having weighed the story judicially, he decided to give the crystal a +refuge. Mr. Cave promised to explain the reasons for his remarkable +affection for the crystal more fully +on a later occasion, but he spoke distinctly of seeing visions therein. +He called on Mr. Wace the same evening. + + + + He told a complicated story. The crystal he said had come into his +possession with other oddments at the forced sale of another curiosity +dealer's effects, and not knowing what its value might be, he had +ticketed it at ten shillings. It had hung upon his hands at that price +for some months, and he was thinking of "reducing the figure," when he +made a singular discovery. + + + + At that time his health was very bad -- and it must be borne in mind +that, throughout all this experience, his physical condition was one of +ebb -- and he was in considerable distress by reason of the negligence, +the positive ill-treatment even, he received from his wife and +step-children. His wife was vain, extravagant, unfeeling and had a +growing taste for private drinking; his step-daughter was mean and +over-reaching; and his step-son had conceived a violent dislike for him, +and lost no chance of showing it. The requirements of his business +pressed heavily upon him, and Mr. Wace does not think that he was +altogether free from occasional intemperance. He had begun life in a +comfortable position, he was a man of fair education, and he suffered, +for weeks at a stretch, from melancholia and insomnia. Afraid to disturb +his family, he would slip quietly from his wife's side, when his +thoughts became intolerable, and wander about the house. And about three +o'clock one morning, late in August, chance directed him into the shop. + + + + The dirty little place was impenetrably black except in one spot, +where he perceived an unusual glow of light. Approaching this, he +discovered it to be the crystal egg, which was standing on the corner of +the counter towards the window. A thin ray smote through a crack in the +shutters, impinged upon the object, and seemed as it were to fill its +entire interior. + + + + It occurred to Mr. Cave that this was not in accordance with the laws +of optics as he had known them in his younger days. He could understand +the rays being refracted by the crystal and coming to a focus in its +interior, but this diffusion jarred with his physical conceptions. He +approached the crystal nearly, peering into it and round it, with a +transient revival of the scientific curiosity that in his youth had +determined his choice of a calling. He was surprised to find the light +not steady, but writhing within the substance of the egg, as though that +object was a hollow sphere of some luminous vapour. In moving about to +get different points of view, he suddenly found that he had come between +it and the ray, and that the crystal none the less remained luminous. +Greatly astonished, he lifted it out of the light ray and carried it to +the darkest part of the shop. It remained +bright for some four or five minutes, when it slowly faded and went out. +He placed it in the thin streak of daylight, and its luminousness was +almost immediately restored. + + + + So far, at least, Mr. Wace was able to verify the remarkable story of +Mr. Cave. He has himself repeatedly held this crystal in a ray of light +(which had to be of a less diameter than one millimetre). And in a +perfect darkness, such as could be produced by velvet wrapping, the +crystal did undoubtedly appear very faintly phosphorescent. It would +seem, however, that the luminousness was of some exceptional sort, and +not equally visible to all eyes; for Mr. Harbinger -- whose name will be +familiar to the scientific reader in connection with the Pasteur +Institute -- was quite unable to see any light whatever. And Mr. Wace's +own capacity for its appreciation was out of comparison inferior to that +of Mr. Cave's. Even with Mr. Cave the power varied very considerably: +his vision was most vivid during states of extreme weakness and fatigue. + + + + Now, from the outset this light in the crystal exercised a curious +fascination upon Mr. Cave. And it says more for his loneliness of soul +than a volume of pathetic writing could do, that he told no human being +of his curious observations. He seems to have been living in such an +atmosphere of petty spite that to admit the existence of a pleasure +would have been to risk the loss of it. He found that as the dawn +advanced, and the amount of diffused light increased, the crystal became +to all appearance non-luminous. And for some time he was unable to see +anything in it, except at night-time, in dark corners of the shop. + + + + But the use of an old velvet cloth, which he used as a background for +a collection of minerals, occurred to him, and by doubling this, and +putting it over his head and hands, he was able to get a sight of the +luminous movement within the crystal even in the day-time. He was very +cautious lest he should be thus discovered by his wife, and he practised +this occupation only in the afternoons, while she was asleep upstairs, +and then circumspectly in a hollow under the counter. And one day, +turning the crystal about in his hands, he saw something. It came and +went like a flash, but it gave him the impression that the object had +for a moment opened to him the view of a wide and spacious and strange +country; and, turning it about, he did, just as the light faded, see the +same vision again. + + + + Now, it would be tedious and unnecessary to state all the phases of +Mr. Cave's discovery from this point. Suffice that the effect was this: +the crystal, being peered into at an angle of about 137 degrees from the +direction of the illuminating ray, gave a clear and consistent picture +of a wide and peculiar country-side. It was not dream-like at +all: it produced a definite impression of reality, and the better the +light the more real and solid it seemed. It was a moving picture: that +is to say, certain objects moved in it, but slowly in an orderly manner +like real things, and, according as the direction of the lighting and +vision changed, the picture changed also. It must, indeed, have been +like looking through an oval glass at a view, and turning the glass +about to get at different aspects. + + + + Mr. Cave's statements, Mr. Wace assures me, were extremely +circumstantial, and entirely free from any of that emotional quality +that taints hallucinatory impressions. But it must be remembered that +all the efforts of Mr. Wace to see any similar clarity in the faint +opalescence of the crystal were wholly unsuccessful, try as he would. +The difference in intensity of the impressions received by the two men +was very great, and it is quite conceivable that what was a view to Mr. +Cave was a mere blurred nebulosity to Mr. Wace. + + + + The view, as Mr. Cave described it, was invariably of an extensive +plain, and he seemed always to be looking at it from a considerable +height, as if from a tower or a mast. To the east and to the west the +plain was bounded at a remote distance by vast reddish cliffs, which +reminded him of those he had seen in some picture; but what the picture +was Mr. Wace was unable to ascertain. These cliffs passed north and +south -- he could tell the points of the compass by the stars that were +visible of a night -- receding in an almost illimitable perspective and +fading into the mists of the distance before they met. He was nearer the +eastern set of cliffs, on the occasion of his first vision the sun was +rising over them, and black against the sunlight and pale against their +shadow appeared a multitude of soaring forms that Mr. Cave regarded as +birds. A vast range of buildings spread below him; he seemed to be +looking down upon them; and, as they approached the blurred and +refracted edge of the picture, they became indistinct. There were also +trees curious in shape, and in colouring, a deep mossy green and an +exquisite grey, beside a wide and shining canal. And something great and +brilliantly coloured flew across the picture. But the first time Mr. +Cave saw these pictures he saw only in flashes, his hands shook, his +head moved, the vision came and went, and grew foggy and indistinct. And +at first he had the greatest difficulty in finding the picture again +once the direction of it was lost. + + + + His next clear vision, which came about a week after the first, the +interval having yielded nothing but tantalising glimpses and some useful +experience, showed him the view down the length of the valley. The view +was different, but he had a curious persuasion, which his subsequent +observations abundantly confirmed, that he was regarding this strange +world from exactly the same spot, although he was looking +in a different direction. The long facade of the great building, whose +roof he had looked down upon before, was now receding in perspective. He +recognised the roof. In the front of the facade was a terrace of massive +proportions and extraordinary length, and down the middle of the +terrace, at certain intervals, stood huge but very graceful masts, +bearing small shiny objects which reflected the setting sun. The import +of these small objects did not occur to Mr. Cave until some time after, +as he was describing the scene to Mr. Wace. The terrace overhung a +thicket of the most luxuriant and graceful vegetation, and beyond this +was a wide grassy lawn on which certain broad creatures, in form like +beetles but enormously larger, reposed. Beyond this again was a richly +decorated causeway of pinkish stone; and beyond that, and lined with +dense red weeds, and passing up the valley exactly parallel with the +distant cliffs, was a broad and mirror-like expanse of water. The air +seemed full of squadrons of great birds, manoeuvring in stately curves; +and across the river was a multitude of splendid buildings, richly +coloured and glittering with metallic tracery and facets, among a forest +of moss-like and lichenous trees. And suddenly something flapped +repeatedly across the vision, like the fluttering of a jewelled fan or +the beating of a wing, and a face, or rather the upper part of a face +with very large eyes, came as it were close to his own and as if on the +other side of the crystal. Mr. Cave was so startled and so impressed by +the absolute reality of these eyes, that he drew his head back from the +crystal to look behind it. He had become so absorbed in watching that he +was quite surprised to find himself in the cool darkness of his little +shop, with its familiar odour of methyl, mustiness, and decay. And, as +he blinked about him, the glowing crystal faded, and went out. + + + + Such were the first general impressions of Mr. Cave. The story is +curiously direct and circumstantial. From the outset, when the valley +first flashed momentarily on his senses, his imagination was strangely +affected, and, as he began to appreciate the details of the scene he +saw, his wonder rose to the point of a passion. He went about his +business listless and distraught, thinking only of the time when he +should be able to return to his watching. And then a few weeks after his +first sight of the valley came the two customers, the stress and +excitement of their offer, and the narrow escape of the crystal from +sale, as I have already told. + + + + Now, while the thing was Mr. Cave's secret, it remained a mere +wonder, a thing to creep to covertly and peep at, as a child might peep +upon a forbidden garden. But Mr. Wace has, for a young scientific +investigator, a particularly lucid and consecutive habit of mind. +Directly the crystal and its story came to him, and he had satisfied +himself, by seeing the phosphorescence with his own eyes, that there +really was a certain evidence for Mr. Cave's statements, he proceeded to +develop the matter systematically. Mr. Cave was only too eager to come +and feast his eyes on this wonderland he saw, and he came every night +from half-past eight until half-past ten, and sometimes, in Mr. Wace's +absence, during the day. On Sunday afternoons, also, he came. From the +outset Mr. Wace made copious notes, and it was due to his scientific +method that the relation between the direction from which the initiating +ray entered the crystal and the orientation of the picture were proved. +And, by covering the crystal in a box perforated only with a small +aperture to admit the exciting ray, and by substituting black holland +for his buff blinds, he greatly improved the conditions of the +observations; so that in a little while they were able to survey the +valley in any direction they desired. + + + + So having cleared the way, we may give a brief account of this +visionary world within the crystal. The things were in all cases seen by +Mr. Cave, and the method of working was invariably for him to watch the +crystal and report what he saw, while Mr. Wace (who as a science student +had learnt the trick of writing in the dark) wrote a brief note of his +report. When the crystal faded, it was put into its box in the proper +position and the electric light turned on. Mr. Wace asked questions, and +suggested observations to clear up difficult points. Nothing, indeed, +could have been less visionary and more matter-of-fact. + + + + The attention of Mr. Cave had been speedily directed to the bird-like +creatures he had seen so abundantly present in each of his earlier +visions. His first impression was soon corrected, and he considered for +a time that they might represent a diurnal species of bat. Then he +thought, grotesquely enough, that they might be cherubs. Their heads +were round, and curiously human, and it was the eyes of one of them that +had so startled him on his second observation. They had broad, silvery +wings, not feathered, but glistening almost as brilliantly as new-killed +fish and with the same subtle play of colour, and these wings were not +built on the plan of a bird-wing or bat, Mr. Wace learned, but supported +by curved ribs radiating from the body. (A sort of butterfly wing with +curved ribs seems best to express their appearance.) The body was small, +but fitted with two bunches of prehensile organs, like long tentacles, +immediately under the mouth. Incredible as it appeared to Mr. Wace, the +persuasion at last became irresistible, that it was these creatures +which owned the great quasi-human buildings and the magnificent garden +that made the broad valley so splendid. And Mr. Cave perceived that the +buildings, with other peculiarities, had no doors, but that the great +circular windows, which +opened freely, gave the creatures egress and entrance. They would alight +upon their tentacles, fold their wings to a smallness almost rod-like, +and hop into the interior. But among them was a multitude of +smaller-winged creatures, like great dragon-flies and moths and flying +beetles, and across the greensward brilliantly-coloured gigantic +ground-beetles crawled lazily to and fro. Moreover, on the causeways and +terraces, large-headed creatures similar to the greater winged flies, +but wingless, were visible, hopping busily upon their hand-like tangle +of tentacles. + + + + Allusion has already been made to the glittering objects upon masts +that stood upon the terrace of the nearer building. It dawned upon Mr. +Cave, after regarding one of these masts very fixedly on one +particularly vivid day, that the glittering object there was a crystal +exactly like that into which he peered. And a still more careful +scrutiny convinced him that each one in a vista of nearly twenty carried +a similar object. + + + + Occasionally one of the large flying creatures would flutter up to +one, and, folding its wings and coiling a number of its tentacles about +the mast, would regard the crystal fixedly for a space, -- sometimes for +as long as fifteen minutes. And a series of observations, made at the +suggestion of Mr. Wace, convinced both watchers that, so far as this +visionary world was concerned, the crystal into which they peered +actually stood at the summit of the end-most mast on the terrace, and +that on one occasion at least one of these inhabitants of this other +world had looked into Mr. Cave's face while he was making these +observations. + + + + So much for the essential facts of this very singular story. Unless +we dismiss it all as the ingenious fabrication of Mr. Wace, we have to +believe one of two things: either that Mr. Cave's crystal was in two +worlds at once, and that, while it was carried about in one, it remained +stationary in the other, which seems altogether absurd; or else that it +had some peculiar relation of sympathy with another and exactly similar +crystal in this other world, so that what was seen in the interior of +the one in this world, was, under suitable conditions, visible to an +observer in the corresponding crystal in the other world; and vice +versa. At present, indeed, we do not know of any way in which two +crystals could so come en rapport, but nowadays we know enough to +understand that the thing is not altogether impossible. This view of the +crystals as en rapport was the supposition that occurred to Mr. Wace, +and to me at least it seems extremely plausible. . . . + + + + And where was this other world? On this, also, the alert intelligence +of Mr. Wace speedily threw light. After sunset, the sky darkened +rapidly -- there was a very brief twilight interval indeed -- and the +stars shone out. They were recognisably the same as those we see, +arranged in the same constellations. Mr. Cave recognised the Bear, the +Pleiades, Aldebaran, and Sirius: so that the other world must be +somewhere in the solar system, and, at the utmost, only a few hundreds +of millions of miles from our own. Following up this clue, Mr. Wace +learned that the midnight sky was a darker blue even than our midwinter +sky, and that the sun seemed a little smaller. And there were two small +moons! "like our moon but smaller, and quite differently marked" one of +which moved so rapidly that its motion was clearly visible as one +regarded it. These moons were never high in the sky, but vanished as +they rose: that is, every time they revolved they were eclipsed because +they were so near their primary planet. And all this answers quite +completely, although. Mr. Cave did not know it, to what must be the +condition of things on Mars. + + + + Indeed, it seems an exceedingly plausible conclusion that peering +into this crystal Mr. Cave did actually see the planet Mars and its +inhabitants. And, if that be the case, then the evening star that shone +so brilliantly in the sky of that distant vision, was neither more nor +less than our own familiar earth. + + + + For a time the Martians -- if they were Martians -- do not seem to +have known of Mr. Cave's inspection. Once or twice one would come to +peer, and go away very shortly to some other mast, as though the vision +was unsatisfactory. During this time Mr. Cave was able to watch the +proceedings of these winged people without being disturbed by their +attentions, and, although his report is necessarily vague and +fragmentary, it is nevertheless very suggestive. Imagine the impression +of humanity a Martian observer would get who, after a difficult process +of preparation and with considerable fatigue to the eyes, was able to +peer at London from the steeple of St. Martin's Church for stretches, at +longest, of four minutes at a time. Mr. Cave was unable to ascertain if +the winged Martians were the same as the Martians who hopped about the +causeways and terraces, and if the latter could put on wings at will. He +several times saw certain clumsy bipeds, dimly suggestive of apes, white +and partially translucent, feeding among certain of the lichenous trees, +and once some of these fled before one of the hopping, round-headed +Martians. The latter caught one in its tentacles, and then the picture +faded suddenly and left Mr. Cave most tantalisingly in the dark. On +another occasion a vast thing, that Mr. Cave thought at first was some +gigantic insect, appeared advancing along the causeway beside the canal +with extraordinary rapidity. As this drew nearer Mr. Cave perceived that +it was a mechanism of shining +metals and of extraordinary complexity. And then, when he looked again, +it had passed out of sight. + + + + After a time Mr. Wace aspired to attract the attention of the +Martians, and the next time that the strange eyes of one of them +appeared close to the crystal Mr. Cave cried out and sprang away, and +they immediately turned on the light and began to gesticulate in a +manner suggestive of signalling. But when at last Mr. Cave examined the +crystal again the Martian had departed. + + + + Thus far these observations had progressed in early November, and +then Mr. Cave, feeling that the suspicions of his family about the +crystal were allayed, began to take it to and fro with him in order +that, as occasion arose in the daytime or night, he might comfort +himself with what was fast becoming the most real thing in his existence. + + + + In December Mr. Wace's work in connection with a forthcoming +examination became heavy, the sittings were reluctantly suspended for a +week, and for ten or eleven days -- he is not quite sure which -- he saw +nothing of Cave. He then grew anxious to resume these investigations, +and, the stress of his seasonal labours being abated, he went down to +Seven Dials. At the corner he noticed a shutter before a bird fancier's +window, and then another at a cobbler's. Mr. Cave's shop was closed. + + + + He rapped and the door was opened by the step-son in black. He at +once called Mrs. Cave, who was, Mr. Wace could not but observe, in cheap +but ample widow's weeds of the most imposing pattern. Without any very +great surprise Mr. Wace learnt that Cave was dead and already buried. +She was in tears, and her voice was a little thick. She had just +returned from Highgate. Her mind seemed occupied with her own prospects +and the honourable details of the obsequies, but Mr. Wace was at last +able to learn the particulars of Cave's death. He had been found dead in +his shop in the early morning, the day after his last visit to Mr. Wace, +and the crystal had been clasped in his stone-cold hands. His face was +smiling, said Mrs. Cave, and the velvet cloth from the minerals lay on +the floor at his feet. He must have been dead five or six hours when he +was found. + + + + This came as a great shock to Wace, and he began to reproach himself +bitterly for having neglected the plain symptoms of the old man's +ill-health. But his chief thought was of the crystal. He approached that +topic in a gingerly manner, because he knew Mrs. Cave's peculiarities. +He was dumbfoundered to learn that it was sold. + + + + Mrs. Cave's first impulse, directly Cave's body had been taken +upstairs, had been to write to the mad clergyman who had offered five +pounds for the crystal, informing him of its recovery; but after a +violent hunt in which her daughter joined her, they were convinced +of the loss of his address. As they were without the means required to +mourn and bury Cave in the elaborate style the dignity of an old Seven +Dials inhabitant demands, they had appealed to a friendly +fellow-tradesman in Great Portland Street. He had very kindly taken over +a portion of the stock at a valuation. The valuation was his own and the +crystal egg was included in one of the lots. Mr. Wace, after a few +suitable consolatory observations, a little offhandedly proffered +perhaps, hurried at once to Great Portland Street. But there he learned +that the crystal egg had already been sold to a tall, dark man in grey. +And there the material facts in this curious, and to me at least very +suggestive, story come abruptly to an end. The Great Portland Street +dealer did not know who the tall dark man in grey was, nor had he +observed him with sufficient attention to describe him minutely. He did +not even know which way this person had gone after leaving the shop. For +a time Mr. Wace remained in the shop, trying the dealer's patience with +hopeless questions, venting his own exasperation. And at last, realising +abruptly that the whole thing had passed out of his hands, had vanished +like a vision of the night, he returned to his own rooms, a little +astonished to find the notes he had made still tangible and visible upon +his untidy table. + + + + His annoyance and disappointment were naturally very great. He made a +second call (equally ineffectual) upon the Great Portland Street dealer, +and he resorted to advertisements in such periodicals as were likely to +come into the hands of a bric-a-brac collector. He also wrote letters to +The Daily Chronicle and Nature, but both those periodicals, suspecting a +hoax, asked him to reconsider his action before they printed, and he was +advised that such a strange story, unfortunately so bare of supporting +evidence, might imperil his reputation as an investigator. Moreover, the +calls of his proper work were urgent. So that after a month or so, save +for an occasional reminder to certain dealers, he had reluctantly to +abandon the quest for the crystal egg, and from that day to this it +remains undiscovered. Occasionally, however, he tells me, and I can +quite believe him, he has bursts of zeal, in which he abandons his more +urgent occupation and resumes the search. + + + + Whether or not it will remain lost for ever, with the material and +origin of it, are things equally speculative at the present time. If the +present purchaser is a collector, one would have expected the enquiries +of Mr. Wace to have readied him through the dealers. He has been able to +discover Mr. Cave's clergyman and "Oriental" -- no other than the Rev. +James Parker and the young Prince of Bosso-Kuni in Java. I am obliged to +them for certain particulars. The object of the Prince was simply +curiosity -- and extravagance. He was so eager to buy, +because Cave was so oddly reluctant to sell. It is just as possible that +the buyer in the second instance was simply a casual purchaser and not a +collector at all, and the crystal egg, for all I know, may at the +present moment be within a mile of me, decorating a drawing-room or +serving as a paper-weight -- its remarkable functions all unknown. +Indeed, it is partly with the idea of such a possibility that I have +thrown this narrative into a form that will give it a chance of being +read by the ordinary consumer of fiction. + + + + My own ideas in the matter are practically identical with those of +Mr. Wace. I believe the crystal on the mast in Mars and the crystal egg +of Mr. Cave's to be in some physical, but at present quite inexplicable, +way en rapport, and we both believe further that the terrestrial crystal +must have been -- possibly at some remote date -- sent hither from that +planet, in order to give the Martians a near view of our affairs. +Possibly the fellows to the crystals in the other masts are also on our +globe. No theory of hallucination suffices for the facts. + + + + + + diff --git a/src/io/dir-util.cpp b/src/io/dir-util.cpp new file mode 100644 index 0000000..69a01f1 --- /dev/null +++ b/src/io/dir-util.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** + * @file + * Utility functions for filenames. + */ + +#include +#include +#include +#include +#include "dir-util.h" + +std::string sp_relative_path_from_path( std::string const &path, std::string const &base) +{ + std::string result; + if ( !base.empty() && !path.empty() ) { + size_t base_len = base.length(); + while (base_len != 0 + && (base[base_len - 1] == G_DIR_SEPARATOR)) + { + --base_len; + } + + if ( (path.substr(0, base_len) == base.substr(0, base_len)) + && (path[base_len] == G_DIR_SEPARATOR)) + { + size_t retPos = base_len + 1; + while ( (retPos < path.length()) && (path[retPos] == G_DIR_SEPARATOR) ) { + retPos++; + } + if ( (retPos + 1) < path.length() ) { + result = path.substr(retPos); + } + } + + } + if ( result.empty() ) { + result = path; + } + return result; +} + +char const *sp_extension_from_path(char const *const path) +{ + if (path == nullptr) { + return nullptr; + } + + char const *p = path; + while (*p != '\0') p++; + + while ((p >= path) && (*p != G_DIR_SEPARATOR) && (*p != '.')) p--; + if (* p != '.') return nullptr; + p++; + + return p; +} + + +/* current == "./", parent == "../" */ +static char const dots[] = {'.', '.', G_DIR_SEPARATOR, '\0'}; +static char const *const parent = dots; +static char const *const current = dots + 1; + +char *inkscape_rel2abs(const char *path, const char *base, char *result, const size_t size) +{ + const char *pp, *bp; + /* endp points the last position which is safe in the result buffer. */ + const char *endp = result + size - 1; + char *rp; + int length; + if (*path == G_DIR_SEPARATOR) + { + if (strlen (path) >= size) + goto erange; + strcpy (result, path); + goto finish; + } + else if (*base != G_DIR_SEPARATOR || !size) + { + errno = EINVAL; + return (nullptr); + } + else if (size == 1) + goto erange; + if (!strcmp (path, ".") || !strcmp (path, current)) + { + if (strlen (base) >= size) + goto erange; + strcpy (result, base); + /* rp points the last char. */ + rp = result + strlen (base) - 1; + if (*rp == G_DIR_SEPARATOR) + *rp = 0; + else + rp++; + /* rp point NULL char */ + if (*++path == G_DIR_SEPARATOR) + { + /* Append G_DIR_SEPARATOR to the tail of path name. */ + *rp++ = G_DIR_SEPARATOR; + if (rp > endp) + goto erange; + *rp = 0; + } + goto finish; + } + bp = base + strlen (base); + if (*(bp - 1) == G_DIR_SEPARATOR) + --bp; + /* up to root. */ + for (pp = path; *pp && *pp == '.';) + { + if (!strncmp (pp, parent, 3)) + { + pp += 3; + while (bp > base && *--bp != G_DIR_SEPARATOR) + ; + } + else if (!strncmp (pp, current, 2)) + { + pp += 2; + } + else if (!strncmp (pp, "..\0", 3)) + { + pp += 2; + while (bp > base && *--bp != G_DIR_SEPARATOR) + ; + } + else + break; + } + /* down to leaf. */ + length = bp - base; + if (length >= static_cast(size)) + goto erange; + strncpy (result, base, length); + rp = result + length; + if (*pp || *(pp - 1) == G_DIR_SEPARATOR || length == 0) + *rp++ = G_DIR_SEPARATOR; + if (rp + strlen (pp) > endp) + goto erange; + strcpy (rp, pp); +finish: + return result; +erange: + errno = ERANGE; + return (nullptr); +} + +char *inkscape_abs2rel(const char *path, const char *base, char *result, const size_t size) +{ + const char *pp, *bp, *branch; + // endp points the last position which is safe in the result buffer. + const char *endp = result + size - 1; + char *rp; + + if (*path != G_DIR_SEPARATOR) + { + if (strlen (path) >= size) + goto erange; + strcpy (result, path); + goto finish; + } + else if (*base != G_DIR_SEPARATOR || !size) + { + errno = EINVAL; + return (nullptr); + } + else if (size == 1) + goto erange; + /* seek to branched point. */ + branch = path; + for (pp = path, bp = base; *pp && *bp && *pp == *bp; pp++, bp++) + if (*pp == G_DIR_SEPARATOR) + branch = pp; + if (((*pp == 0) || ((*pp == G_DIR_SEPARATOR) && (*(pp + 1) == 0))) && + ((*bp == 0) || ((*bp == G_DIR_SEPARATOR) && (*(bp + 1) == 0)))) + { + rp = result; + *rp++ = '.'; + if (*pp == G_DIR_SEPARATOR || *(pp - 1) == G_DIR_SEPARATOR) + *rp++ = G_DIR_SEPARATOR; + if (rp > endp) + goto erange; + *rp = 0; + goto finish; + } + if (((*pp == 0) && (*bp == G_DIR_SEPARATOR)) || ((*pp == G_DIR_SEPARATOR) && (*bp == 0))) + branch = pp; + /* up to root. */ + rp = result; + for (bp = base + (branch - path); *bp; bp++) + if (*bp == G_DIR_SEPARATOR && *(bp + 1) != 0) + { + if (rp + 3 > endp) + goto erange; + *rp++ = '.'; + *rp++ = '.'; + *rp++ = G_DIR_SEPARATOR; + } + if (rp > endp) + goto erange; + *rp = 0; + /* down to leaf. */ + if (*branch) + { + if (rp + strlen (branch + 1) > endp) + goto erange; + strcpy (rp, branch + 1); + } + else + *--rp = 0; +finish: + return result; +erange: + errno = ERANGE; + return (nullptr); +} + +char *prepend_current_dir_if_relative(gchar const *uri) +{ + if (!uri) { + return nullptr; + } + + gchar *full_path = (gchar *) g_malloc (1001); + gchar *cwd = g_get_current_dir(); + + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError* error = nullptr; + gchar* cwd_utf8 = g_filename_to_utf8 ( cwd, + -1, + &bytesRead, + &bytesWritten, + &error); + + inkscape_rel2abs (uri, cwd_utf8, full_path, 1000); + gchar *ret = g_strdup (full_path); + g_free (full_path); + g_free (cwd); + return ret; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 : diff --git a/src/io/dir-util.h b/src/io/dir-util.h new file mode 100644 index 0000000..91f07c8 --- /dev/null +++ b/src/io/dir-util.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2016 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIR_UTIL_H +#define SEEN_DIR_UTIL_H + +/* + * path-util.h + * + * here are functions sp_relative_path & cousins + * maybe they are already implemented in standard libs + * + */ + +#include +#include + +/** + * Returns a form of \a path relative to \a base if that is easy to construct (eg if \a path + * appears to be in the directory specified by \a base), otherwise returns \a path. + * + * @param path is expected to be an absolute path. + * @param base is expected to be either empty or the absolute path of a directory. + * + * @return a relative version of the path, if reasonable. + * + * @see inkscape_abs2rel for a more sophisticated version. + * @see prepend_current_dir_if_relative. +*/ +std::string sp_relative_path_from_path(std::string const &path, std::string const &base); + +char const *sp_extension_from_path(char const *path); + +/** + * Convert a relative path name into absolute. If path is already absolute, does nothing except copying path to result. + * + * @param path relative path. + * @param base base directory (must be absolute path). + * @param result result buffer. + * @param size size of result buffer. + * + * @return != NULL: absolute path + * == NULL: error + * + * based on functions by Shigio Yamaguchi. + * FIXME:TODO: force it to also do path normalization of the entire resulting path, + * i.e. get rid of any .. and . in any place, even if 'path' is already absolute + * (now it returns it unchanged in this case) + * + */ +char *inkscape_rel2abs(char const *path, char const *base, char *result, size_t const size); + +char *inkscape_abs2rel(char const *path, char const *base, char *result, size_t const size); + +char *prepend_current_dir_if_relative(char const *filename); + + +#endif // !SEEN_DIR_UTIL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/doc2html.xsl b/src/io/doc2html.xsl new file mode 100644 index 0000000..9a98c23 --- /dev/null +++ b/src/io/doc2html.xsl @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + +

+ +

+ + + +

+ + + +

+
+ + + +
+ +
+ + +

+ +

+
+ + diff --git a/src/io/file-export-cmd.cpp b/src/io/file-export-cmd.cpp new file mode 100644 index 0000000..7670e50 --- /dev/null +++ b/src/io/file-export-cmd.cpp @@ -0,0 +1,786 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * File export from the command line. This code, greatly modified, use to be in main.cpp. It should + * be replaced by code shared with the file dialog (Gio::Actions?). + * + * Copyright (C) 2018 Tavmjong Bah + * + * Git blame shows that bulia byak is the main author of the original export code from + * main.cpp. Other authors of note include Nicolas Dufour, Vinicius dos Santos Oliveira, and Bob + * Jamison; none of whom bothered to add their names to the copyright of main.cc. + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include "file-export-cmd.h" + +#include // PNG export + +#include "document.h" +#include "object/object-set.h" +#include "object/sp-item.h" +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-object-group.h" +#include "path-chemistry.h" // sp_item_list_to_curves +#include "text-editing.h" // te_update_layout_now_recursive +#include "selection-chemistry.h" // fit_canvas_to_drawing +#include "svg/svg-color.h" // Background color +#include "helper/png-write.h" // PNG Export + +#include "extension/extension.h" +#include "extension/system.h" +#include "extension/db.h" +#include "extension/output.h" +#include "extension/init.h" + +InkFileExportCmd::InkFileExportCmd() + : export_overwrite(false) + , export_area_drawing(false) + , export_area_page(false) + , export_margin(0) + , export_area_snap(false) + , export_use_hints(false) + , export_width(0) + , export_height(0) + , export_dpi(0) + , export_ignore_filters(false) + , export_text_to_path(false) + , export_ps_level(3) + , export_pdf_level("1.5") + , export_latex(false) + , export_id_only(false) + , export_background_opacity(-1) // default is unset != actively set to 0 + , export_plain_svg(false) +{ +} + +void +InkFileExportCmd::do_export(SPDocument* doc, std::string filename_in) +{ + std::string export_type_filename; + std::vector export_type_list; + + // Get export type from filename supplied with --export-filename + if (!export_filename.empty() && export_filename != "-") { + auto extension_pos = export_filename.find_last_of('.'); + if (extension_pos == std::string::npos) { + if (export_type.empty()) { + std::cerr << "InkFileExportCmd::do_export: No export type specified. " + << "Append a supported file extension to filename provided with --export-filename or " + << "provide one or more extensions separately using --export-type" << std::endl; + return; + } else { + // no extension is fine if --export-type is given + } + } else { + export_type_filename = export_filename.substr(extension_pos+1); + export_filename.erase(extension_pos); + } + } + + // Get export type(s) from string supplied with --export-type + if (!export_type.empty()) { + export_type_list = Glib::Regex::split_simple("[,;]", export_type); + } + + // Determine actual type(s) for export. + if (export_use_hints) { + // Override type if --export-use-hints is used (hints presume PNG export for now) + // TODO: There's actually no reason to presume. We could allow to export to any format using hints! + if (export_id.empty() && !export_area_drawing) { + std::cerr << "InkFileExportCmd::do_export: " + << "--export-use-hints can only be used with --export-id or --export-area-drawing." << std::endl; + return; + } + if (export_type_list.size() > 1 || (export_type_list.size() == 1 && export_type_list[0] != "png")) { + std::cerr << "InkFileExportCmd::do_export: --export-use-hints can only be used with PNG export! " + << "Ignoring --export-type=" << export_type << "." << std::endl; + } + if (!export_filename.empty()) { + std::cerr << "InkFileExportCmd::do_export: --export-filename is ignored when using --export-use-hints!" << std::endl; + } + export_type_list.clear(); + export_type_list.emplace_back("png"); + } else if (export_type_list.empty()) { + if (!export_type_filename.empty()) { + export_type_list.emplace_back(export_type_filename); // use extension from filename + } else { + export_type_list.emplace_back("svg"); // fall-back to SVG by default + } + } + + for (auto const& type: export_type_list) { + g_info("exporting '%s' to type '%s'", filename_in.c_str(), type.c_str()); + + export_type_current = type; + + // Check for consistency between extension of --export-filename and --export-type if both are given + if (!export_type_filename.empty() && (type != export_type_filename)) { + std::cerr << "InkFileExportCmd::do_export: " + << "Ignoring extension of export filename (" << export_type_filename << ") " + << "as it does not match the current export type (" << type << ")." << std::endl; + } + + if (type == "svg") { + do_export_svg(doc, filename_in); + } else if (type == "png") { + do_export_png(doc, filename_in); + } else if (type == "ps") { + do_export_ps_pdf(doc, filename_in, "image/x-postscript"); + } else if (type == "eps") { + do_export_ps_pdf(doc, filename_in, "image/x-e-postscript"); + } else if (type == "pdf") { + do_export_ps_pdf(doc, filename_in, "application/pdf"); + } else if (type == "emf") { + do_export_win_metafile(doc, filename_in, "image/x-emf"); + } else if (type == "wmf") { + do_export_win_metafile(doc, filename_in, "image/x-wmf"); + } else if (type == "xaml") { + do_export_win_metafile(doc, filename_in, "text/xml+xaml"); + } else { + std::cerr << "InkFileExportCmd::export: Unknown export type: " << type + << ". Allowed values: [svg,png,ps,eps,pdf,emf,wmf,xaml]." << std::endl; + } + } +} + + +// File names use std::string. HTML5 and presumably SVG 2 allows UTF-8 characters. Do we need to convert "object_id" here? +std::string +InkFileExportCmd::get_filename_out(std::string filename_in, std::string object_id) +{ + // Pipe out + if (export_filename == "-") { + return "-"; + } + + // Use filename provided as --export-filename if given (and append proper extension). + if (!export_filename.empty()) { + return export_filename + "." + export_type_current; + } + + // Check for pipe + if (filename_in == "-") { + return "-"; + } + + // Construct output filename from input filename and export_type. + auto extension_pos = filename_in.find_last_of('.'); + if (extension_pos == std::string::npos) { + std::cerr << "InkFileExportCmd::get_filename_out: cannot determine input file type from filename extension: " << filename_in << std::endl; + return (std::string()); + } + + std::string extension = filename_in.substr(extension_pos+1); + if (export_overwrite && export_type_current == extension) { + return filename_in; + } else { + std::string tag; + if (export_type_current == extension) { + tag = "_out"; + } + if (!object_id.empty()) { + tag = "_" + object_id; + } + return (filename_in.substr(0,extension_pos) + tag + "." + export_type_current); + } + + // We need a valid file name to write to unless we're using PNG export hints. + // if (!(export_type == "png" && export_use_hints)) { + + // // Check for file name. + // if (filename_out.empty()) { + // std::cerr << "InkFileExportCmd::do_export: Could not determine output file name!" << std::endl; + // return (std::string()); + // } + + // // Check if directory exists. + // std::string directory = Glib::path_get_dirname(filename_out); + // if (!Glib::file_test(directory, Glib::FILE_TEST_IS_DIR)) { + // std::cerr << "InkFileExportCmd::do_export: File path includes directory that does not exist! " << directory << std::endl; + // return (std::string()); + // } + // } +} + +/** + * Perform an SVG export + * + * \param doc Document to export. + */ +int +InkFileExportCmd::do_export_svg(SPDocument* doc, std::string filename_in) +{ + // Start with options that are once per document. + if (export_text_to_path) { + std::vector items; + SPRoot *root = doc->getRoot(); + doc->ensureUpToDate(); + for (auto& iter: root->children) { + SPItem* item = (SPItem*) &iter; + if (! (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item) || SP_IS_GROUP(item))) { + continue; + } + + te_update_layout_now_recursive(item); + items.push_back(item); + } + + std::vector selected; // Not used + std::vector to_select; // Not used + + sp_item_list_to_curves(items, selected, to_select); + } + + if (export_margin != 0) { + gdouble margin = export_margin; + doc->ensureUpToDate(); + SPNamedView *nv; + Inkscape::XML::Node *nv_repr; + if ((nv = sp_document_namedview(doc, nullptr)) && (nv_repr = nv->getRepr())) { + sp_repr_set_svg_double(nv_repr, "fit-margin-top", margin); + sp_repr_set_svg_double(nv_repr, "fit-margin-left", margin); + sp_repr_set_svg_double(nv_repr, "fit-margin-right", margin); + sp_repr_set_svg_double(nv_repr, "fit-margin-bottom", margin); + } + } + + if (export_area_drawing) { + fit_canvas_to_drawing(doc, export_margin != 0 ? true : false); + } else if (export_area_page || export_id.empty() ) { + if (export_margin) { + doc->ensureUpToDate(); + doc->fitToRect(*(doc->preferredBounds()), true); + } + } + + + // Export each object in list (or root if empty). Use ';' so in future it could be possible to selected multiple objects to export together. + std::vector objects = Glib::Regex::split_simple("\\s*;\\s*", export_id); + if (objects.empty()) { + objects.emplace_back(); // So we do loop at least once for root. + } + + for (auto object : objects) { + + std::string filename_out = get_filename_out(filename_in, object); + if (filename_out.empty()) { + return 1; + } + + if(!object.empty()) { + doc->ensureUpToDate(); + + // "crop" the document to the specified object, cleaning as we go. + SPObject *obj = doc->getObjectById(object); + if (obj == nullptr) { + std::cerr << "InkFileExportCmd::do_export_svg: Object " << object << " not found in document, nothing to export." << std::endl; + return 1; + } + if (export_id_only) { + // If -j then remove all other objects to complete the "crop" + doc->getRoot()->cropToObject(obj); + } + if (!(export_area_page || export_area_drawing)) { + Inkscape::ObjectSet s(doc); + s.set(obj); + s.fitCanvas(export_margin ? true : false); + } + } + + if (export_plain_svg) { + + try { + Inkscape::Extension::save(Inkscape::Extension::db.get("org.inkscape.output.svg.plain"), doc, filename_out.c_str(), false, + false, false, Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY); + } catch (Inkscape::Extension::Output::save_failed &e) { + std::cerr << "InkFileExportCmd::do_export_svg: Failed to save SVG to: " << filename_out << std::endl; + return 1; + } + + } else { + + // Export as inkscape SVG. + try { + Inkscape::Extension::save(Inkscape::Extension::db.get("org.inkscape.output.svg.inkscape"), doc, filename_out.c_str(), false, + false, false, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG); + } catch (Inkscape::Extension::Output::save_failed &e) { + std::cerr << "InkFileExportCmd::do_export_svg: Failed to save Inkscape SVG to: " << filename_out << std::endl; + return 1; + } + } + } + return 0; +} + +guint32 InkFileExportCmd::get_bgcolor(SPDocument *doc) { + guint32 bgcolor = 0x00000000; + if (!export_background.empty()) { + // override the page color + bgcolor = sp_svg_read_color(export_background.c_str(), 0xffffff00); + // default is opaque if a color is given on commandline + if (export_background_opacity < -.5 ) { + export_background_opacity = 255; + } + } else { + // read from namedview + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")){ + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + } + + if (export_background_opacity > -.5) { // if the value is manually set + if (export_background_opacity > 1.0) { + float value = CLAMP (export_background_opacity, 1.0f, 255.0f); + bgcolor |= (guint32) floor(value); + } else { + float value = CLAMP (export_background_opacity, 0.0f, 1.0f); + bgcolor |= SP_COLOR_F_TO_U(value); + } + } else { + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv && nv->attribute("inkscape:pageopacity")){ + double opacity = 1.0; + sp_repr_get_double (nv, "inkscape:pageopacity", &opacity); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } // else it's transparent + } + return bgcolor; +} + +/** + * Perform a PNG export + * + * \param doc Document to export. + */ +int +InkFileExportCmd::do_export_png(SPDocument *doc, std::string filename_in) +{ + bool filename_from_hint = false; + gdouble dpi = 0.0; + guint32 bgcolor = get_bgcolor(doc); + + // Export each object in list (or root if empty). Use ';' so in future it could be possible to selected multiple objects to export together. + std::vector objects = Glib::Regex::split_simple("\\s*;\\s*", export_id); + if (objects.empty()) { + objects.emplace_back(); // So we do loop at least once for root. + } + + for (auto object_id : objects) { + + std::string filename_out = get_filename_out(filename_in, object_id); + + std::vector items; + + // Find export object. (Either root or object with specified id.) + SPObject *object = doc->getRoot(); + if (!object_id.empty()) { + object = doc->getObjectById(object_id); + } + + if (!object) { + std::cerr << "InkFileExport::do_export_png: " + << "Object with id=\"" << object_id + << "\" was not found in the document. Skipping." << std::endl; + continue; + } + + if (!SP_IS_ITEM (object)) { + std::cerr << "InkFileExportCmd::do_export_png: " + << "Object with id=\"" << object_id + << "\" is not a visible item. Skipping." << std::endl; + continue; + } + + items.push_back(SP_ITEM(object)); // There is only one item, why do this? + + if (export_id_only) { + std::cerr << "Exporting only object with id=\"" + << object_id << "\"; all other objects hidden." << std::endl; + } + + // Find file name and dpi from hints. + if (export_use_hints) { + + // Retrieve export filename hint. + const gchar *fn_hint = object->getRepr()->attribute("inkscape:export-filename"); + if (fn_hint) { + filename_out = fn_hint; + filename_from_hint = true; + } else { + std::cerr << "InkFileExport::do_export_png: " + << "Export filename hint not found for object " << object_id << ". Skipping." << std::endl; + continue; + } + + // Retrieve export dpi hint. Only xdpi as ydpi is always the same now. + const gchar *dpi_hint = object->getRepr()->attribute("inkscape:export-xdpi"); + if (dpi_hint) { + if (export_dpi || export_width || export_height) { + std::cerr << "InkFileExport::do_export_png: " + << "Using bitmap dimensions from the command line " + << "(--export-dpi, --export-width, or --export-height). " + << "DPI hint " << dpi_hint << " is ignored." << std::endl; + } else { + dpi = atof(dpi_hint); + } + } else { + std::cerr << "InkFileExport::do_export_png: " + << "Export DPI hint not found for the object." << std::endl; + } + } + + // ------------------------- File name ------------------------- + + // Check we have a filename. + if (filename_out.empty()) { + std::cerr << "InkFileExport::do_export_png: " + << "No valid export filename given and no filename hint. Skipping." << std::endl; + continue; + } + + if (filename_from_hint) { + //Make relative paths go from the document location, if possible: + if (!Glib::path_is_absolute(filename_out) && doc->getDocumentURI()) { + std::string dirname = Glib::path_get_dirname(doc->getDocumentURI()); + if (!dirname.empty()) { + filename_out = Glib::build_filename(dirname, filename_out); + } + } + } + + // Check if directory exists + std::string directory = Glib::path_get_dirname(filename_out); + if (!Glib::file_test(directory, Glib::FILE_TEST_IS_DIR)) { + std::cerr << "File path " << filename_out << " includes directory that doesn't exist. Skipping." << std::endl; + continue; + } + + // -------------------------- DPI ------------------------------- + + if (export_dpi != 0.0 && dpi == 0.0) { + dpi = export_dpi; + if ((dpi < 0.1) || (dpi > 10000.0)) { + std::cerr << "InkFileExport::do_export_png: " + << "DPI value " << export_dpi + << " out of range [0.1 - 10000.0]. Skipping."; + continue; + } + } + + // default dpi + if (dpi == 0.0) { + dpi = Inkscape::Util::Quantity::convert(1, "in", "px"); + } + + // ------------------------- Area ------------------------------- + + Geom::Rect area; + doc->ensureUpToDate(); + + // Three choices: 1. Command-line export_area 2. Page area 3. Drawing area + if (!export_area.empty()) { + + // Export area command-line + + /* Try to parse area (given in SVG pixels) */ + gdouble x0,y0,x1,y1; + if (sscanf(export_area.c_str(), "%lg:%lg:%lg:%lg", &x0, &y0, &x1, &y1) != 4) { + g_warning("Cannot parse export area '%s'; use 'x0:y0:x1:y1'. Nothing exported.", export_area.c_str()); + return 1; // If it fails once, it will fail for all objects. + } + area = Geom::Rect(Geom::Interval(x0,x1), Geom::Interval(y0,y1)); + + } else if (export_area_page || (!export_area_drawing && object_id.empty())) { + + // Export area page (explicit or if no object is given). + Geom::Point origin(doc->getRoot()->x.computed, doc->getRoot()->y.computed); + area = Geom::Rect(origin, origin + doc->getDimensions()); + + } else { + + // Export area drawing (explicit or if object is given). + Geom::OptRect areaMaybe = static_cast(object)->documentVisualBounds(); + if (areaMaybe) { + area = *areaMaybe; + } else { + std::cerr << "InkFileExport::do_export_png: " + << "Unable to determine a valid bounding box. Skipping." << std::endl; + continue; + } + } + + if (export_area_snap) { + area = area.roundOutwards(); + } + // End finding area. + + // -------------------------- Width and Height --------------------------------- + + unsigned long int width = 0; + unsigned long int height = 0; + double xdpi = dpi; + double ydpi = dpi; + + if (export_height != 0) { + height = export_height; + if ((height < 1) || (height > PNG_UINT_31_MAX)) { + std::cerr << "InkFileExport::do_export_png: " + << "Export height " << height << " out of range (1 to " << PNG_UINT_31_MAX << ")" << std::endl; + continue; + } + ydpi = Inkscape::Util::Quantity::convert(height, "in", "px") / area.height(); + xdpi = ydpi; + dpi = ydpi; + } + + if (export_width != 0) { + width = export_width; + if ((width < 1) || (width > PNG_UINT_31_MAX)) { + std::cerr << "InkFileExport::do_export_png: " + << "Export width " << width << " out of range (1 to " << PNG_UINT_31_MAX << ")." << std::endl; + continue; + } + xdpi = Inkscape::Util::Quantity::convert(width, "in", "px") / area.width(); + ydpi = export_height ? ydpi : xdpi; + dpi = xdpi; + } + + if (width == 0) { + width = (unsigned long int) (Inkscape::Util::Quantity::convert(area.width(), "px", "in") * dpi + 0.5); + } + + if (height == 0) { + height = (unsigned long int) (Inkscape::Util::Quantity::convert(area.height(), "px", "in") * dpi + 0.5); + } + + if ((width < 1) || (height < 1) || (width > PNG_UINT_31_MAX) || (height > PNG_UINT_31_MAX)) { + std::cerr << "InkFileExport::do_export_png: Dimensions " << width << "x" << height << " are out of range (1 to " << PNG_UINT_31_MAX << ")." << std::endl; + continue; + } + + // ---------------------- Generate the PNG ------------------------------- + + // Do we really need to print this? + std::cerr << "Background RRGGBBAA: " << std::hex << bgcolor << std::dec << std::endl; + std::cerr << "Area " + << area[Geom::X][0] << ":" << area[Geom::Y][0] << ":" + << area[Geom::X][1] << ":" << area[Geom::Y][1] << " exported to " + << width << " x " << height << " pixels (" << dpi << " dpi)" << std::endl; + + reverse(items.begin(),items.end()); // But there was only one item! + + if( sp_export_png_file(doc, filename_out.c_str(), area, width, height, xdpi, ydpi, + bgcolor, nullptr, nullptr, true, export_id_only ? items : std::vector()) == 1 ) { + } else { + std::cerr << "InkFileExport::do_export_png: Failed to export to " << filename_out << std::endl; + continue; + } + + } // End loop over objects. + return 0; +} + + +/** + * Perform a PDF/PS/EPS export + * + * \param doc Document to export. + * \param filename File to write to. + * \param mime MIME type to export as. + */ +int +InkFileExportCmd::do_export_ps_pdf(SPDocument* doc, std::string filename_in, std::string mime_type) +{ + // Check if we support mime type. + Inkscape::Extension::DB::OutputList o; + Inkscape::Extension::db.get_output_list(o); + Inkscape::Extension::DB::OutputList::const_iterator i = o.begin(); + while (i != o.end() && strcmp( (*i)->get_mimetype(), mime_type.c_str() ) != 0) { + i++; + } + + if (i == o.end()) { + std::cerr << "InkFileExportCmd::do_export_ps_pdf: Could not find an extension to export to MIME type: " << mime_type << std::endl; + return 1; + } + + // Start with options that are once per document. + + // Set export options. + if (export_text_to_path) { + (*i)->set_param_optiongroup("textToPath", "paths"); + } else if (export_latex) { + (*i)->set_param_optiongroup("textToPath", "LaTeX"); + } else { + (*i)->set_param_optiongroup("textToPath", "embed"); + } + + if (export_ignore_filters) { + (*i)->set_param_bool("blurToBitmap", false); + } else { + (*i)->set_param_bool("blurToBitmap", true); + + gdouble dpi = 96.0; + if (export_dpi) { + dpi = export_dpi; + if ((dpi < 1) || (dpi > 10000.0)) { + g_warning("DPI value %lf out of range [1 - 10000]. Using 96 dpi instead.", export_dpi); + dpi = 96; + } + } + + (*i)->set_param_int("resolution", (int) dpi); + } + + (*i)->set_param_float("bleed", export_margin); + + // handle --export-pdf-version + if (mime_type == "application/pdf") { + bool set_export_pdf_version_fail = true; + const gchar *pdfver_param_name = "PDFversion"; + if (!export_pdf_level.empty()) { + // combine "PDF " and the given command line + std::string version_gui_string = std::string("PDF-") + export_pdf_level; + try { + // first, check if the given pdf version is selectable in the ComboBox + if ((*i)->get_param_optiongroup_contains("PDFversion", version_gui_string.c_str())) { + (*i)->set_param_optiongroup(pdfver_param_name, version_gui_string.c_str()); + set_export_pdf_version_fail = false; + } else { + g_warning("Desired PDF export version \"%s\" not supported! Hint: input one of the versions found in the pdf export dialog e.g. \"1.4\".", + export_pdf_level.c_str()); + } + } catch (...) { + // can be thrown along the way: + // throw Extension::param_not_exist(); + // throw Extension::param_not_enum_param(); + g_warning("Parameter or Enum \"%s\" might not exist", pdfver_param_name); + } + } + + // set default pdf export version to 1.4, also if something went wrong + if(set_export_pdf_version_fail) { + (*i)->set_param_optiongroup(pdfver_param_name, "PDF-1.4"); + } + } + + if (mime_type == "image/x-postscript" || mime_type == "image/x-e-postscript") { + if ( export_ps_level < 2 || export_ps_level > 3 ) { + g_warning("Only supported PostScript levels are 2 and 3." + " Defaulting to 2."); + export_ps_level = 2; + } + + (*i)->set_param_optiongroup("PSlevel", (export_ps_level == 3) ? "PS3" : "PS2"); + } + + + // Export each object in list (or root if empty). Use ';' so in future it could be possible to selected multiple objects to export together. + std::vector objects = Glib::Regex::split_simple("\\s*;\\s*", export_id); + if (objects.empty()) { + objects.emplace_back(); // So we do loop at least once for root. + } + + for (auto object : objects) { + + std::string filename_out = get_filename_out(filename_in, object); + if (filename_out.empty()) { + return 1; + } + + // Export only object with given id. + if (!object.empty()) { + SPObject *o = doc->getObjectById(object); + if (o == nullptr) { + std::cerr << "InkFileExportCmd::do_export_ps_pdf: Object " << object << " not found in document, nothing to export." << std::endl; + return 1; + } + (*i)->set_param_string ("exportId", object.c_str()); + } else { + (*i)->set_param_string ("exportId", ""); + } + + // Set export area. + if (export_area_page && export_area_drawing) { + std::cerr << "You cannot use --export-area-page and --export-area-drawing at the same time; only the former will take effect." << std::endl;; + export_area_drawing = false; + } + + if (export_area_drawing) { + (*i)->set_param_optiongroup ("area", "drawing"); + } + + if (export_area_page) { + if (export_type == "eps") { + std::cerr << "EPS cannot have its bounding box extend beyond its content, so if your drawing is smaller than the page, --export-area-page will clip it to drawing." << std::endl; + } + (*i)->set_param_optiongroup ("area", "page"); + } + + if (!export_area_drawing && !export_area_page) { + // Neither is set. + if (export_type == "eps" || !object.empty()) { + // Default to drawing for EPS or if object is specified (latter matches behavior for other export types). + (*i)->set_param_optiongroup("area", "drawing"); + } else { + (*i)->set_param_optiongroup("area", "page"); + } + } + + try { + (*i)->save(doc, filename_out.c_str()); + } catch(...) { + std::cerr << "Failed to save PS/EPS/PDF to: " << filename_out << std::endl; + return 1; + } + } + + return 0; +} + +/** + * Export a document to EMF or WMF + * + * \param doc Document to export. + * \param filename to export to. + * \param mime MIME type to export as (should be "image/x-emf" or "image/x-wmf") + */ +int +InkFileExportCmd::do_export_win_metafile(SPDocument* doc, std::string filename_in, std::string mime_type) +{ + std::string filename_out = get_filename_out(filename_in); + + // Check if we support mime type. + Inkscape::Extension::DB::OutputList o; + Inkscape::Extension::db.get_output_list(o); + Inkscape::Extension::DB::OutputList::const_iterator i = o.begin(); + while (i != o.end() && strcmp( (*i)->get_mimetype(), mime_type.c_str() ) != 0) { + i++; + } + + if (i == o.end()) + { + std::cerr << "InkFileExportCmd::do_export_win_metafile_common: Could not find an extension to export to MIME type: " << mime_type << std::endl; + return 1; + } + + (*i)->save(doc, filename_out.c_str()); + return 0; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/file-export-cmd.h b/src/io/file-export-cmd.h new file mode 100644 index 0000000..6d63ccb --- /dev/null +++ b/src/io/file-export-cmd.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * File export from the command line. This code use to be in main.cpp. It should be + * replaced by shared code (Gio::Actions?) for export from the file dialog. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INK_FILE_EXPORT_CMD_H +#define INK_FILE_EXPORT_CMD_H + +#include +#include + +class SPDocument; + +class InkFileExportCmd { + +public: + InkFileExportCmd(); + + void do_export(SPDocument* doc, std::string filename_in=""); + +private: + + guint32 get_bgcolor(SPDocument *doc); + std::string get_filename_out(std::string filename_in="", std::string object_id=""); + int do_export_svg( SPDocument* doc, std::string filename_in); + int do_export_png( SPDocument* doc, std::string filename_in); + int do_export_ps_pdf(SPDocument* doc, std::string filename_in, std::string mime_type); + int do_export_win_metafile(SPDocument* doc, std::string filename_in, std::string mime_type); + + Glib::ustring export_type_current; + +public: + // Should be private, but this is just temporary code (I hope!). + + // One-to-one correspondence with command line options + std::string export_filename; // Only if one file is processed! + + Glib::ustring export_type; + bool export_overwrite; + + Glib::ustring export_area; + bool export_area_drawing; + bool export_area_page; + int export_margin; + bool export_area_snap; + int export_width; + int export_height; + + double export_dpi; + bool export_ignore_filters; + bool export_text_to_path; + int export_ps_level; + Glib::ustring export_pdf_level; + bool export_latex; + Glib::ustring export_id; + bool export_id_only; + bool export_use_hints; + Glib::ustring export_background; + double export_background_opacity; + bool export_plain_svg; +}; + +#endif // INK_FILE_EXPORT_CMD_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/file.cpp b/src/io/file.cpp new file mode 100644 index 0000000..a4b0699 --- /dev/null +++ b/src/io/file.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * File operations (independent of GUI) + * + * Copyright (C) 2018, 2019 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include "file.h" + +#include +#include + +#include "document.h" +#include "document-undo.h" + +#include "extension/system.h" // Extension::open() +#include "extension/extension.h" +#include "extension/db.h" +#include "extension/output.h" +#include "extension/input.h" + +#include "object/sp-root.h" + +#include "xml/repr.h" + + +/** + * Create a blank document, remove any template data. + * Input: Empty string or template file name. + */ +SPDocument* +ink_file_new(const std::string &Template) +{ + SPDocument *doc = SPDocument::createNewDoc ((Template.empty() ? nullptr : Template.c_str()), true, true ); + + if (doc) { + // Remove all the template info from xml tree + Inkscape::XML::Node *myRoot = doc->getReprRoot(); + Inkscape::XML::Node *nodeToRemove; + + nodeToRemove = sp_repr_lookup_name(myRoot, "inkscape:templateinfo"); + if (nodeToRemove != nullptr) { + Inkscape::DocumentUndo::ScopedInsensitive no_undo(doc); + sp_repr_unparent(nodeToRemove); + delete nodeToRemove; + } + nodeToRemove = sp_repr_lookup_name(myRoot, "inkscape:_templateinfo"); // backwards-compatibility + if (nodeToRemove != nullptr) { + Inkscape::DocumentUndo::ScopedInsensitive no_undo(doc); + sp_repr_unparent(nodeToRemove); + delete nodeToRemove; + } + } else { + std::cout << "ink_file_new: Did not create new document!" << std::endl; + } + + return doc; +} + +/** + * Open a document from memory. + */ +SPDocument* +ink_file_open(const Glib::ustring& data) +{ + SPDocument *doc = SPDocument::createNewDocFromMem (data.c_str(), data.length(), true); + + if (doc == nullptr) { + std::cerr << "ink_file_open: cannot open file in memory (pipe?)" << std::endl; + } else { + + // This is the only place original values should be set. + SPRoot *root = doc->getRoot(); + root->original.inkscape = root->version.inkscape; + root->original.svg = root->version.svg; + } + + return doc; +} + +/** + * Open a document. + */ +SPDocument* +ink_file_open(const Glib::RefPtr& file, bool *cancelled_param) +{ + bool cancelled = false; + + SPDocument *doc = nullptr; + + std::string path = file->get_path(); + + // TODO: It's useless to catch these exceptions here (and below) unless we do something with them. + // If we can't properly handle them (e.g. by showing a user-visible message) don't catch them! + try { + doc = Inkscape::Extension::open(nullptr, path.c_str()); + } catch (Inkscape::Extension::Input::no_extension_found &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_failed &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_cancelled &e) { + cancelled = true; + doc = nullptr; + } + + // Try to open explicitly as SVG. + // TODO: Why is this necessary? Shouldn't this be handled by the first call already? + if (doc == nullptr && !cancelled) { + try { + doc = Inkscape::Extension::open(Inkscape::Extension::db.get(SP_MODULE_KEY_INPUT_SVG), path.c_str()); + } catch (Inkscape::Extension::Input::no_extension_found &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_failed &e) { + doc = nullptr; + } catch (Inkscape::Extension::Input::open_cancelled &e) { + cancelled = true; + doc = nullptr; + } + } + + if (doc != nullptr) { + // This is the only place original values should be set. + SPRoot *root = doc->getRoot(); + root->original.inkscape = root->version.inkscape; + root->original.svg = root->version.svg; + } else if (!cancelled) { + std::cerr << "ink_file_open: '" << path << "' cannot be opened!" << std::endl; + } + + if (cancelled_param) { + *cancelled_param = cancelled; + } + return doc; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/file.h b/src/io/file.h new file mode 100644 index 0000000..5d2ec6d --- /dev/null +++ b/src/io/file.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * File operations (independent of GUI) + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INK_FILE_IO_H +#define INK_FILE_IO_H + +#include + +namespace Gio { +class File; +} + +namespace Glib { +class ustring; + +template +class RefPtr; +} + +class SPDocument; + +SPDocument* ink_file_new(const std::string &Template = nullptr); +SPDocument* ink_file_open(const Glib::ustring &data); +SPDocument* ink_file_open(const Glib::RefPtr& file, bool *cancelled = nullptr); + +// To do: +// ink_file_save() +// ink_file_export() +// ink_file_import() + + + +#endif // INK_FILE_IO_H diff --git a/src/io/http.cpp b/src/io/http.cpp new file mode 100644 index 0000000..6f28db7 --- /dev/null +++ b/src/io/http.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::IO::HTTP - make internet requests using libsoup + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * How to use: + * + * #include "io/http.cpp" + * void _async_test_call(Glib::ustring filename) { + * g_warning("HTTP request saved to %s", filename.c_str()); + * } + * uint timeout = 20 * 60 * 60; // 20 hours + * Glib::ustring filename = Inkscape::IO::HTTP::get_file("https://media.inkscape.org/media/messages.xml", timeout, _async_test_call); + * + */ + +#include +#include +#include +#include + +#include "io/sys.h" +#include "io/http.h" +#include "io/resource.h" + +typedef std::function callback; + +namespace Resource = Inkscape::IO::Resource; + +namespace Inkscape { +namespace IO { +namespace HTTP { + +void _save_data_as_file(Glib::ustring filename, const char *result) { + FILE *fileout = Inkscape::IO::fopen_utf8name(filename.c_str(), "wb"); + if (!fileout) { + g_warning("HTTP Cache: Can't open %s for write.", filename.c_str()); + return; + } + + fputs(result, fileout); + fflush(fileout); + if (ferror(fileout)) { + g_warning("HTTP Cache: Error writing data to %s.", filename.c_str()); + } + + fclose(fileout); +} + +void _get_file_callback(SoupSession *session, SoupMessage *msg, gpointer user_data) { + auto data = static_cast*>(user_data); + data->first(data->second); + delete data; +} + +/* + * Downloads a file, caches it in the local filesystem and then returns the + * new filename of the cached file. + * + * Returns: filename where the file has been (blocking) or will be (async) stored. + * + * uri - The uri of the desired resource, the filename comes from this uri + * timeout - Number of seconds to keep the cache, default is forever + * func - An optional function, if given the function becomes asynchronous + * the returned filename will be where the file 'hopes' to be saved + * and this function will be called when the http request is complete. + * + * NOTE: If the cache file exists, then it's returned without any async request + * your func will be called in a blocking way BEFORE this function returns. + * + */ +Glib::ustring get_file(Glib::ustring uri, unsigned int timeout, callback func) { + + SoupURI *s_uri = soup_uri_new(uri.c_str()); + std::string path = std::string(soup_uri_decode(soup_uri_get_path(s_uri))); + std::string filepart; + + // Parse the url into a filename suitable for caching. + if(path.back() == '/') { + filepart = path.replace(path.begin(), path.end(), '/', '_'); + filepart += ".url"; + } else { + filepart = path.substr(path.rfind("/") + 1); + } + + const char *ret = get_path(Resource::CACHE, Resource::NONE, filepart.c_str()); + Glib::ustring filename = Glib::ustring(ret); + + // We test first if the cache already exists + if(file_test(filename.c_str(), G_FILE_TEST_EXISTS) && timeout > 0) { + GStatBuf st; + if(g_stat(filename.c_str(), &st) != -1) { + time_t changed = st.st_mtime; + time_t now = time(nullptr); + // The cache hasn't timed out, so return the filename. + if(now - changed < timeout) { + if(func) { + // Non-async func callback return, may block. + func(filename); + } + return filename; + } + g_debug("HTTP Cache is stale: %s", filename.c_str()); + } + } + + // Only then do we get the http request + SoupMessage *msg = soup_message_new_from_uri("GET", s_uri); + SoupSession *session = soup_session_new(); + +#ifdef DEBUG_HTTP + SoupLogger *logger; + logger = soup_logger_new(SOUP_LOGGER_LOG_BODY, -1); + soup_session_add_feature(session, SOUP_SESSION_FEATURE (logger)); + g_object_unref (logger); +#endif + + if(func) { + auto *user_data = new std::pair(func, filename); + soup_session_queue_message(session, msg, _get_file_callback, user_data); + } else { + guint status = soup_session_send_message (session, msg); + if(status == SOUP_STATUS_OK) { + g_debug("HTTP Cache saved to: %s", filename.c_str()); + _save_data_as_file(filename, msg->response_body->data); + } else { + g_warning("Can't download %s", uri.c_str()); + } + } + return filename; +} + + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/http.h b/src/io/http.h new file mode 100644 index 0000000..398d699 --- /dev/null +++ b/src/io/http.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::IO::HTTP - make internet requests using libsoup + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_IO_HTTP_H +#define SEEN_INKSCAPE_IO_HTTP_H + +#include +#include + +/** + * simple libsoup based resource API + */ + +namespace Inkscape { +namespace IO { +namespace HTTP { + + Glib::ustring get_file(Glib::ustring uri, unsigned int timeout=0, std::function func=nullptr); + +} +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/resource-manager.cpp b/src/io/resource-manager.cpp new file mode 100644 index 0000000..0fe346a --- /dev/null +++ b/src/io/resource-manager.cpp @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ResourceManager - tracks external resources such as image and css files. + * + * Copyright 2011 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "resource-manager.h" + +#include "document.h" +#include "document-undo.h" +#include "verbs.h" + +#include "object/sp-object.h" + +#include "xml/node.h" + +namespace Inkscape { + +static std::vector splitPath( std::string const &path ) +{ + std::vector parts; + + std::string prior; + std::string tmp = path; + while ( !tmp.empty() && (tmp != prior) ) { + prior = tmp; + + parts.push_back( Glib::path_get_basename(tmp) ); + tmp = Glib::path_get_dirname(tmp); + } + if ( !parts.empty() ) { + std::reverse(parts.begin(), parts.end()); + if ( (parts[0] == ".") && (path[0] != '.') ) { + parts.erase(parts.begin()); + } + } + + return parts; +} + +static std::string convertPathToRelative( std::string const &path, std::string const &docbase ) +{ + std::string result = path; + + if ( !path.empty() && Glib::path_is_absolute(path) ) { + // Whack the parts into pieces + + std::vector parts = splitPath(path); + std::vector baseParts = splitPath(docbase); + + // TODO debug g_message("+++++++++++++++++++++++++"); + for ( std::vector::iterator it = parts.begin(); it != parts.end(); ++it ) { + // TODO debug g_message(" [%s]", it->c_str()); + } + // TODO debug g_message(" - - - - - - - - - - - - - - - "); + for ( std::vector::iterator it = baseParts.begin(); it != baseParts.end(); ++it ) { + // TODO debug g_message(" [%s]", it->c_str()); + } + // TODO debug g_message("+++++++++++++++++++++++++"); + + if ( !parts.empty() && !baseParts.empty() && (parts[0] == baseParts[0]) ) { + // Both paths have the same root. We can proceed. + while ( !parts.empty() && !baseParts.empty() && (parts[0] == baseParts[0]) ) { + parts.erase( parts.begin() ); + baseParts.erase( baseParts.begin() ); + } + + // TODO debug g_message("+++++++++++++++++++++++++"); + for ( std::vector::iterator it = parts.begin(); it != parts.end(); ++it ) { + // TODO debug g_message(" [%s]", it->c_str()); + } + // TODO debug g_message(" - - - - - - - - - - - - - - - "); + for ( std::vector::iterator it = baseParts.begin(); it != baseParts.end(); ++it ) { + // TODO debug g_message(" [%s]", it->c_str()); + } + // TODO debug g_message("+++++++++++++++++++++++++"); + + if ( !parts.empty() ) { + result.clear(); + + for ( size_t i = 0; i < baseParts.size(); ++i ) { + parts.insert(parts.begin(), ".."); + } + result = Glib::build_filename( parts ); + // TODO debug g_message("----> [%s]", result.c_str()); + } + } + } + + return result; +} + + +class ResourceManagerImpl : public ResourceManager { +public: + ResourceManagerImpl(); + ~ResourceManagerImpl() override; + + bool fixupBrokenLinks(SPDocument *doc) override; + + + /** + * Walk all links in a document and create a listing of unique broken links. + * + * @return a list of all broken links. + */ + std::vector findBrokenLinks(SPDocument *doc); + + /** + * Resolve broken links as a whole and return a map for those that can be found. + * + * Note: this will allow for future enhancements including relinking to new locations + * with the most broken files found, etc. + * + * @return a map of found links. + */ + std::map locateLinks(Glib::ustring const & docbase, std::vector const & brokenLinks); + + + /** + * Try to parse href into a local filename using standard methods. + * + * @return true if successful. + */ + bool extractFilepath( Glib::ustring const &href, std::string &uri ); + + /** + * Try to parse href into a local filename using some non-standard methods. + * This means the href is likely invalid and should be rewritten. + * + * @return true if successful. + */ + bool reconstructFilepath( Glib::ustring const &href, std::string &uri ); + + bool searchUpwards( std::string const &base, std::string const &subpath, std::string &dest ); + +protected: +}; + + +ResourceManagerImpl::ResourceManagerImpl() + : ResourceManager() +{ +} + +ResourceManagerImpl::~ResourceManagerImpl() += default; + +bool ResourceManagerImpl::extractFilepath( Glib::ustring const &href, std::string &uri ) +{ + bool isFile = false; + + uri.clear(); + + std::string scheme = Glib::uri_parse_scheme(href); + if ( !scheme.empty() ) { + // TODO debug g_message("Scheme is now [%s]", scheme.c_str()); + if ( scheme == "file" ) { + // TODO debug g_message("--- is a file URI [%s]", href.c_str()); + + // throws Glib::ConvertError: + try { + uri = Glib::filename_from_uri(href); + // TODO debug g_message(" [%s]", uri.c_str()); + isFile = true; + } catch(Glib::ConvertError e) { + g_warning("%s", e.what().c_str()); + } + } + } else { + // No scheme. Assuming it is a file path (absolute or relative). + // throws Glib::ConvertError: + uri = Glib::filename_from_utf8( href ); + isFile = true; + } + + return isFile; +} + +bool ResourceManagerImpl::reconstructFilepath( Glib::ustring const &href, std::string &uri ) +{ + bool isFile = false; + + uri.clear(); + + std::string scheme = Glib::uri_parse_scheme(href); + if ( !scheme.empty() ) { + if ( scheme == "file" ) { + // try to build a relative filename for URIs like "file:image.png" + // they're not standard conformant but not uncommon + Glib::ustring href_new = Glib::ustring(href, 5); + uri = Glib::filename_from_utf8(href_new); + // TODO debug g_message("reconstructed path for '%s' into '%s'", href.c_str(), uri.c_str()); + isFile = true; + } + } + return isFile; +} + + +std::vector ResourceManagerImpl::findBrokenLinks( SPDocument *doc ) +{ + std::vector result; + std::set uniques; + + if ( doc ) { + std::vector images = doc->getResourceList("image"); + for (auto image : images) { + Inkscape::XML::Node *ir = image->getRepr(); + + gchar const *href = ir->attribute("xlink:href"); + if ( href && ( uniques.find(href) == uniques.end() ) ) { + std::string uri; + if ( extractFilepath( href, uri ) ) { + if ( Glib::path_is_absolute(uri) ) { + if ( !Glib::file_test(uri, Glib::FILE_TEST_EXISTS) ) { + result.emplace_back(href); + uniques.insert(href); + } + } else { + std::string combined = Glib::build_filename(doc->getDocumentBase(), uri); + if ( !Glib::file_test(combined, Glib::FILE_TEST_EXISTS) ) { + result.emplace_back(href); + uniques.insert(href); + } + } + } else if ( reconstructFilepath( href, uri ) ) { + result.emplace_back(href); + uniques.insert(href); + } + } + } + } + + return result; +} + + +std::map ResourceManagerImpl::locateLinks(Glib::ustring const & docbase, std::vector const & brokenLinks) +{ + std::map result; + + + // Note: we use a vector because we want them to stay in order: + std::vector priorLocations; + + Glib::RefPtr recentMgr = Gtk::RecentManager::get_default(); + std::vector< Glib::RefPtr > recentItems = recentMgr->get_items(); + for (auto & recentItem : recentItems) { + Glib::ustring uri = recentItem->get_uri(); + std::string scheme = Glib::uri_parse_scheme(uri); + if ( scheme == "file" ) { + try { + std::string path = Glib::filename_from_uri(uri); + path = Glib::path_get_dirname(path); + if ( std::find(priorLocations.begin(), priorLocations.end(), path) == priorLocations.end() ) { + // TODO debug g_message(" ==>[%s]", path.c_str()); + priorLocations.push_back(path); + } + } catch (Glib::ConvertError e) { + g_warning("%s", e.what().c_str()); + } + } + } + + // At the moment we expect this list to contain file:// references, or simple relative or absolute paths. + for (const auto & brokenLink : brokenLinks) { + // TODO debug g_message("========{%s}", it->c_str()); + + std::string uri; + if ( extractFilepath( brokenLink, uri ) || reconstructFilepath( brokenLink, uri ) ) { + // We were able to get some path. Check it + std::string origPath = uri; + + if ( !Glib::path_is_absolute(uri) ) { + uri = Glib::build_filename(docbase, uri); + // TODO debug g_message(" not absolute. Fixing up as [%s]", uri.c_str()); + } + + bool exists = Glib::file_test(uri, Glib::FILE_TEST_EXISTS); + + // search in parent folders + if (!exists) { + exists = searchUpwards( docbase, origPath, uri ); + } + + // Check if the MRU bases point us to it. + if ( !exists ) { + if ( !Glib::path_is_absolute(origPath) ) { + for ( std::vector::iterator it = priorLocations.begin(); !exists && (it != priorLocations.end()); ++it ) { + exists = searchUpwards( *it, origPath, uri ); + } + } + } + + if ( exists ) { + if ( Glib::path_is_absolute( uri ) ) { + // TODO debug g_message("Need to convert to relative if possible [%s]", uri.c_str()); + uri = convertPathToRelative( uri, docbase ); + } + + bool isAbsolute = Glib::path_is_absolute( uri ); + Glib::ustring replacement = isAbsolute ? Glib::filename_to_uri( uri ) : Glib::filename_to_utf8( uri ); + result[brokenLink] = replacement; + } + } + } + + return result; +} + +bool ResourceManagerImpl::fixupBrokenLinks(SPDocument *doc) +{ + bool changed = false; + if ( doc ) { + // TODO debug g_message("FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP FIXUP"); + // TODO debug g_message(" base is [%s]", doc->getDocumentBase()); + + std::vector brokenHrefs = findBrokenLinks(doc); + if ( !brokenHrefs.empty() ) { + // TODO debug g_message(" FOUND SOME LINKS %d", static_cast(brokenHrefs.size())); + for ( std::vector::iterator it = brokenHrefs.begin(); it != brokenHrefs.end(); ++it ) { + // TODO debug g_message(" [%s]", it->c_str()); + } + } + + Glib::ustring base; + if (doc->getDocumentBase()) { + base = doc->getDocumentBase(); + } + + std::map mapping = locateLinks(base, brokenHrefs); + for ( std::map::iterator it = mapping.begin(); it != mapping.end(); ++it ) + { + // TODO debug g_message(" [%s] ==> {%s}", it->first.c_str(), it->second.c_str()); + } + + bool savedUndoState = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, true); + + std::vector images = doc->getResourceList("image"); + for (auto image : images) { + Inkscape::XML::Node *ir = image->getRepr(); + + gchar const *href = ir->attribute("xlink:href"); + if ( href ) { + // TODO debug g_message(" consider [%s]", href); + + if ( mapping.find(href) != mapping.end() ) { + // TODO debug g_message(" Found a replacement"); + + ir->setAttributeOrRemoveIfEmpty( "xlink:href", mapping[href] ); + if ( ir->attribute( "sodipodi:absref" ) ) { + ir->removeAttribute("sodipodi:absref"); // Remove this attribute + } + + SPObject *updated = doc->getObjectByRepr(ir); + if (updated) { + // force immediate update of dependent attributes + updated->updateRepr(); + } + + changed = true; + } + } + } + if ( changed ) { + DocumentUndo::done( doc, SP_VERB_DIALOG_XML_EDITOR, _("Fixup broken links") ); + } + DocumentUndo::setUndoSensitive(doc, savedUndoState); + } + + return changed; +} + + +bool ResourceManagerImpl::searchUpwards( std::string const &base, std::string const &subpath, std::string &dest ) +{ + bool exists = false; + // TODO debug g_message("............"); + + std::vector parts = splitPath(subpath); + std::vector baseParts = splitPath(base); + + while ( !exists && !baseParts.empty() ) { + std::vector current; + current.insert(current.begin(), parts.begin(), parts.end()); + // TODO debug g_message(" ---{%s}", Glib::build_filename( baseParts ).c_str()); + while ( !exists && !current.empty() ) { + std::vector combined; + combined.insert( combined.end(), baseParts.begin(), baseParts.end() ); + combined.insert( combined.end(), current.begin(), current.end() ); + std::string filepath = Glib::build_filename( combined ); + exists = Glib::file_test(filepath, Glib::FILE_TEST_EXISTS); + // TODO debug g_message(" ...[%s] %s", filepath.c_str(), (exists ? "XXX" : "")); + if ( exists ) { + dest = filepath; + } + current.erase( current.begin() ); + } + baseParts.pop_back(); + } + + return exists; +} + + +static ResourceManagerImpl* theInstance = nullptr; + +ResourceManager::ResourceManager() += default; + +ResourceManager::~ResourceManager() = default; + +ResourceManager& ResourceManager::getManager() { + if ( !theInstance ) { + theInstance = new ResourceManagerImpl(); + } + + return *theInstance; +} + + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/resource-manager.h b/src/io/resource-manager.h new file mode 100644 index 0000000..7c1e242 --- /dev/null +++ b/src/io/resource-manager.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ResourceManager - Manages external resources such as image and css files. + * + * Copyright 2011 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_RESOURCE_MANAGER_H +#define SEEN_INKSCAPE_RESOURCE_MANAGER_H + +class SPDocument; + +namespace Inkscape { + +class ResourceManager { + +public: + static ResourceManager& getManager(); + + virtual bool fixupBrokenLinks(SPDocument *doc) = 0; + +protected: + ResourceManager(); + virtual ~ResourceManager(); + +private: + ResourceManager(ResourceManager const &) = delete; // no copy + void operator=(ResourceManager const &) = delete; // no assign +}; + + + +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_RESOURCE_MANAGER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/resource.cpp b/src/io/resource.cpp new file mode 100644 index 0000000..d7b9731 --- /dev/null +++ b/src/io/resource.cpp @@ -0,0 +1,477 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::IO::Resource - simple resource API + *//* + * Authors: + * MenTaLguY + * Martin Owens + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef _WIN32 +#include // for SHGetSpecialFolderLocation +#endif + +#include +#include +#include +#include + +#include "path-prefix.h" +#include "io/sys.h" +#include "io/resource.h" + +using Inkscape::IO::file_test; + +namespace Inkscape { + +namespace IO { + +namespace Resource { + +#define INKSCAPE_PROFILE_DIR "inkscape" + +gchar *_get_path(Domain domain, Type type, char const *filename) +{ + gchar *path=nullptr; + switch (domain) { + case SYSTEM: { + gchar const* temp = nullptr; + switch (type) { + case EXTENSIONS: temp = INKSCAPE_EXTENSIONDIR; break; + case FILTERS: temp = INKSCAPE_FILTERDIR; break; + case FONTS: temp = INKSCAPE_FONTSDIR; break; + case ICONS: temp = INKSCAPE_ICONSDIR; break; + case KEYS: temp = INKSCAPE_KEYSDIR; break; + case MARKERS: temp = INKSCAPE_MARKERSDIR; break; + case NONE: g_assert_not_reached(); break; + case PAINT: temp = INKSCAPE_PAINTDIR; break; + case PALETTES: temp = INKSCAPE_PALETTESDIR; break; + case SCREENS: temp = INKSCAPE_SCREENSDIR; break; + case SYMBOLS: temp = INKSCAPE_SYMBOLSDIR; break; + case TEMPLATES: temp = INKSCAPE_TEMPLATESDIR; break; + case THEMES: temp = INKSCAPE_THEMEDIR; break; + case TUTORIALS: temp = INKSCAPE_TUTORIALSDIR; break; + case UIS: temp = INKSCAPE_UIDIR; break; + case PIXMAPS: temp = INKSCAPE_PIXMAPSDIR; break; + default: temp = ""; + } + path = g_strdup(temp); + } break; + case CREATE: { + gchar const* temp = nullptr; + switch (type) { + case PAINT: temp = CREATE_PAINTDIR; break; + case PALETTES: temp = CREATE_PALETTESDIR; break; + default: temp = ""; + } + path = g_strdup(temp); + } break; + case CACHE: { + path = g_build_filename(g_get_user_cache_dir(), "inkscape", NULL); + } break; + case USER: { + char const *name=nullptr; + switch (type) { + case EXTENSIONS: name = "extensions"; break; + case FILTERS: name = "filters"; break; + case FONTS: name = "fonts"; break; + case ICONS: name = "icons"; break; + case KEYS: name = "keys"; break; + case MARKERS: name = "markers"; break; + case NONE: name = ""; break; + case PAINT: name = "paint"; break; + case PALETTES: name = "palettes"; break; + case SYMBOLS: name = "symbols"; break; + case TEMPLATES: name = "templates"; break; + case THEMES: name = "themes"; break; + case UIS: name = "ui"; break; + case PIXMAPS: name = "pixmaps"; break; + default: return _get_path(SYSTEM, type, filename); + } + path = profile_path(name); + } break; + } + + + if (filename && path) { + gchar *temp=g_build_filename(path, filename, NULL); + g_free(path); + path = temp; + } + + return path; +} + + + +Util::ptr_shared get_path(Domain domain, Type type, char const *filename) +{ + char *path = _get_path(domain, type, filename); + Util::ptr_shared result=Util::share_string(path); + g_free(path); + return result; +} +Glib::ustring get_path_ustring(Domain domain, Type type, char const *filename) +{ + Glib::ustring result; + char *path = _get_path(domain, type, filename); + if(path) { + result = Glib::ustring(path); + g_free(path); + } + return result; +} + +/* + * Same as get_path, but checks for file's existence and falls back + * from USER to SYSTEM modes. + * + * type - The type of file to get, such as extension, template, ui etc + * filename - The filename to get, i.e. preferences.xml + * localized - Prefer a localized version of the file, i.e. default.de.svg instead of default.svg. + * (will use gettext to determine the preferred language of the user) + * silent - do not warn if file doesnt exist + * + */ +Glib::ustring get_filename(Type type, char const *filename, bool localized, bool silent) +{ + Glib::ustring result; + + char *user_filename = nullptr; + char *sys_filename = nullptr; + char *user_filename_localized = nullptr; + char *sys_filename_localized = nullptr; + + // TRANSLATORS: 'en' is an ISO 639-1 language code. + // Replace with language code for your language, i.e. the name of your .po file + localized = localized && strcmp(_("en"), "en"); + + if (localized) { + Glib::ustring localized_filename = filename; + localized_filename.insert(localized_filename.rfind('.'), "."); + localized_filename.insert(localized_filename.rfind('.'), _("en")); + + user_filename_localized = _get_path(USER, type, localized_filename.c_str()); + sys_filename_localized = _get_path(SYSTEM, type, localized_filename.c_str()); + } + user_filename = _get_path(USER, type, filename); + sys_filename = _get_path(SYSTEM, type, filename); + + // impose the following load order: + // USER (localized) > USER > SYSTEM (localized) > SYSTEM + if (localized && file_test(user_filename_localized, G_FILE_TEST_EXISTS)) { + result = Glib::ustring(user_filename_localized); + g_info("Found localized version of resource file '%s' in profile directory:\n\t%s", filename, result.c_str()); + } else if (file_test(user_filename, G_FILE_TEST_EXISTS)) { + result = Glib::ustring(user_filename); + g_info("Found resource file '%s' in profile directory:\n\t%s", filename, result.c_str()); + } else if (localized && file_test(sys_filename_localized, G_FILE_TEST_EXISTS)) { + result = Glib::ustring(sys_filename_localized); + g_info("Found localized version of resource file '%s' in system directory:\n\t%s", filename, result.c_str()); + } else if (file_test(sys_filename, G_FILE_TEST_EXISTS)) { + result = Glib::ustring(sys_filename); + g_info("Found resource file '%s' in system directory:\n\t%s", filename, result.c_str()); + } else if (!silent) { + if (localized) { + g_warning("Failed to find resource file '%s'. Looked in:\n\t%s\n\t%s\n\t%s\n\t%s", + filename, user_filename_localized, user_filename, sys_filename_localized, sys_filename); + } else { + g_warning("Failed to find resource file '%s'. Looked in:\n\t%s\n\t%s", + filename, user_filename, sys_filename); + } + } + + g_free(user_filename); + g_free(sys_filename); + g_free(user_filename_localized); + g_free(sys_filename_localized); + + return result; +} + +/* + * Similar to get_filename, but takes a path (or filename) for relative resolution + * + * path - A directory or filename that is considered local to the path resolution. + * filename - The filename that we are looking for. + */ +Glib::ustring get_filename(Glib::ustring path, Glib::ustring filename) +{ + // Test if it's a filename and get the parent directory instead + if (Glib::file_test(path, Glib::FILE_TEST_IS_REGULAR)) { + return get_filename(g_path_get_dirname(path.c_str()), filename); + } + if (g_path_is_absolute(filename.c_str())) { + if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + return filename; + } + } else { + Glib::ustring ret = Glib::build_filename(path, filename); + if (Glib::file_test(ret, Glib::FILE_TEST_EXISTS)) { + return ret; + } + } + return Glib::ustring(); +} + +/* + * Gets all the files in a given type, for all domain types. + * + * domain - Optional domain (overload), will check return domains if not. + * type - The type of files, e.g. TEMPLATES + * extensions - A list of extensions to return, e.g. xml, svg + * exclusions - A list of names to exclude e.g. default.xml + */ +std::vector get_filenames(Type type, std::vector extensions, std::vector exclusions) +{ + std::vector ret; + get_filenames_from_path(ret, get_path_ustring(USER, type), extensions, exclusions); + get_filenames_from_path(ret, get_path_ustring(SYSTEM, type), extensions, exclusions); + get_filenames_from_path(ret, get_path_ustring(CREATE, type), extensions, exclusions); + return ret; +} + +std::vector get_filenames(Domain domain, Type type, std::vector extensions, std::vector exclusions) +{ + std::vector ret; + get_filenames_from_path(ret, get_path_ustring(domain, type), extensions, exclusions); + return ret; +} +std::vector get_filenames(Glib::ustring path, std::vector extensions, std::vector exclusions) +{ + std::vector ret; + get_filenames_from_path(ret, path, extensions, exclusions); + return ret; +} + +/* + * Gets all folders inside each type, for all domain types. + * + * domain - Optional domain (overload), will check return domains if not. + * type - The type of files, e.g. TEMPLATES + * extensions - A list of extensions to return, e.g. xml, svg + * exclusions - A list of names to exclude e.g. default.xml + */ +std::vector get_foldernames(Type type, std::vector exclusions) +{ + std::vector ret; + get_foldernames_from_path(ret, get_path_ustring(USER, type), exclusions); + get_foldernames_from_path(ret, get_path_ustring(SYSTEM, type), exclusions); + get_foldernames_from_path(ret, get_path_ustring(CREATE, type), exclusions); + return ret; +} + +std::vector get_foldernames(Domain domain, Type type, std::vector exclusions) +{ + std::vector ret; + get_foldernames_from_path(ret, get_path_ustring(domain, type), exclusions); + return ret; +} +std::vector get_foldernames(Glib::ustring path, std::vector exclusions) +{ + std::vector ret; + get_foldernames_from_path(ret, path, exclusions); + return ret; +} + + +/* + * Get all the files from a specific path and any sub-dirs, populating &files vector + * + * &files - Output list to populate, will be populated with full paths + * path - The directory to parse, will add nothing if directory doesn't exist + * extensions - Only add files with these extensions, they must be duplicated + * exclusions - Exclude files that exactly match these names. + */ +void get_filenames_from_path(std::vector &files, Glib::ustring path, std::vector extensions, std::vector exclusions) +{ + if(!Glib::file_test(path, Glib::FILE_TEST_IS_DIR)) { + return; + } + + Glib::Dir dir(path); + std::string file = dir.read_name(); + while (!file.empty()){ + // If not extensions are specified, don't reject ANY files. + bool reject = !extensions.empty(); + + // Unreject any file which has one of the extensions. + for (auto &ext: extensions) { + reject ^= Glib::str_has_suffix(file, ext); + } + + // Reject any file which matches the exclusions. + for (auto &exc: exclusions) { + reject |= Glib::str_has_prefix(file, exc); + } + + // Reject any filename which isn't a regular file + Glib::ustring filename = Glib::build_filename(path, file); + + if(Glib::file_test(filename, Glib::FILE_TEST_IS_DIR)) { + get_filenames_from_path(files, filename, extensions, exclusions); + } else if(Glib::file_test(filename, Glib::FILE_TEST_IS_REGULAR) && !reject) { + files.push_back(filename); + } + file = dir.read_name(); + } +} + +/* + * Get all the files from a specific path and any sub-dirs, populating &files vector + * + * &folders - Output list to populate, will be poulated with full paths + * path - The directory to parse, will add nothing if directory doesn't exist + * exclusions - Exclude files that exactly match these names. + */ +void get_foldernames_from_path(std::vector &folders, Glib::ustring path, + std::vector exclusions) +{ + if (!Glib::file_test(path, Glib::FILE_TEST_IS_DIR)) { + return; + } + + Glib::Dir dir(path); + std::string file = dir.read_name(); + while (!file.empty()) { + // If not extensions are specified, don't reject ANY files. + bool reject = false; + + // Reject any file which matches the exclusions. + for (auto &exc : exclusions) { + reject |= Glib::str_has_prefix(file, exc); + } + + // Reject any filename which isn't a regular file + Glib::ustring filename = Glib::build_filename(path, file); + + if (Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) && !reject) { + folders.push_back(filename); + } + file = dir.read_name(); + } +} + + +/** + * Get, or guess, or decide the location where the preferences.xml + * file should be located. This also indicates where all other inkscape + * shared files may optionally exist. + */ +char *profile_path(const char *filename) +{ + static const gchar *prefdir = nullptr; + + if (!prefdir) { + // Check if profile directory is overridden using environment variable + gchar const *userenv = g_getenv("INKSCAPE_PROFILE_DIR"); + if (userenv) { + prefdir = g_strdup(userenv); + } + +#ifdef _WIN32 + // prefer c:\Documents and Settings\UserName\Application Data\ to c:\Documents and Settings\userName\; + // TODO: CSIDL_APPDATA is C:\Users\UserName\AppData\Roaming these days + // should we migrate to AppData\Local? Then we can simply use the portable g_get_user_config_dir() + if (!prefdir) { + ITEMIDLIST *pidl = 0; + if ( SHGetFolderLocation( NULL, CSIDL_APPDATA, NULL, 0, &pidl ) == S_OK ) { + gchar * utf8Path = NULL; + + { + wchar_t pathBuf[MAX_PATH+1]; + g_assert(sizeof(wchar_t) == sizeof(gunichar2)); + + if ( SHGetPathFromIDListW( pidl, pathBuf ) ) { + utf8Path = g_utf16_to_utf8( (gunichar2*)(&pathBuf[0]), -1, NULL, NULL, NULL ); + } + } + + if ( utf8Path ) { + if (!g_utf8_validate(utf8Path, -1, NULL)) { + g_warning( "SHGetPathFromIDListW() resulted in invalid UTF-8"); + g_free( utf8Path ); + utf8Path = 0; + } else { + prefdir = utf8Path; + } + } + } + + if (prefdir) { + const char *prefdir_profile = g_build_filename(prefdir, INKSCAPE_PROFILE_DIR, NULL); + g_free((void *)prefdir); + prefdir = prefdir_profile; + } + } +#endif + if (!prefdir) { + prefdir = g_build_filename(g_get_user_config_dir(), INKSCAPE_PROFILE_DIR, NULL); + // In case the XDG user config dir of the moment does not yet exist... + int mode = S_IRWXU; +#ifdef S_IRGRP + mode |= S_IRGRP; +#endif +#ifdef S_IXGRP + mode |= S_IXGRP; +#endif +#ifdef S_IXOTH + mode |= S_IXOTH; +#endif + if ( g_mkdir_with_parents(prefdir, mode) == -1 ) { + int problem = errno; + g_warning("Unable to create profile directory (%s) (%d)", g_strerror(problem), problem); + } else { + gchar const *userDirs[] = { "keys", "templates", "icons", "extensions", "ui", + "symbols", "paint", "themes", "palettes", nullptr }; + for (gchar const** name = userDirs; *name; ++name) { + gchar *dir = g_build_filename(prefdir, *name, NULL); + g_mkdir_with_parents(dir, mode); + g_free(dir); + } + } + } + } + return g_build_filename(prefdir, filename, NULL); +} + +/* + * We return the profile_path because that is where most documentation + * days log files will be generated in inkscape 0.92 + */ +char *log_path(const char *filename) +{ + return profile_path(filename); +} + +char *homedir_path(const char *filename) +{ + static const gchar *homedir = nullptr; + homedir = g_get_home_dir(); + + return g_build_filename(homedir, filename, NULL); +} + +} + +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/resource.h b/src/io/resource.h new file mode 100644 index 0000000..fc1f066 --- /dev/null +++ b/src/io/resource.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * MenTaLguY + * Martin Owens + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_IO_RESOURCE_H +#define SEEN_INKSCAPE_IO_RESOURCE_H + +#include +#include +#include "util/share.h" + +namespace Inkscape { + +namespace IO { + +/** + * simple resource API + */ +namespace Resource { + +enum Type { + EXTENSIONS, + FONTS, + ICONS, + KEYS, + MARKERS, + NONE, + PAINT, + PALETTES, + SCREENS, + TEMPLATES, + TUTORIALS, + SYMBOLS, + FILTERS, + THEMES, + UIS, + PIXMAPS, +}; + +enum Domain { + SYSTEM, + CREATE, + CACHE, + USER +}; + +Util::ptr_shared get_path(Domain domain, Type type, + char const *filename=nullptr); + +Glib::ustring get_path_ustring(Domain domain, Type type, + char const *filename=nullptr); + +Glib::ustring get_filename(Type type, char const *filename, bool localized = false, bool silent = false); +Glib::ustring get_filename(Glib::ustring path, Glib::ustring filename); + +std::vector get_filenames(Type type, + std::vector extensions={}, + std::vector exclusions={}); + +std::vector get_filenames(Domain domain, Type type, + std::vector extensions={}, + std::vector exclusions={}); + +std::vector get_filenames(Glib::ustring path, + std::vector extensions={}, + std::vector exclusions={}); + +std::vector get_foldernames(Type type, std::vector exclusions = {}); + +std::vector get_foldernames(Domain domain, Type type, std::vector exclusions = {}); + +std::vector get_foldernames(Glib::ustring path, std::vector exclusions = {}); + +void get_filenames_from_path(std::vector &files, + Glib::ustring path, + std::vector extensions={}, + std::vector exclusions={}); + +void get_foldernames_from_path(std::vector &files, Glib::ustring path, + std::vector exclusions = {}); + + +char *profile_path(const char *filename); +char *homedir_path(const char *filename); +char *log_path(const char *filename); + +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/stream/Makefile.tst b/src/io/stream/Makefile.tst new file mode 100644 index 0000000..2e3142d --- /dev/null +++ b/src/io/stream/Makefile.tst @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +############################################## +# +# Test makefile for InkscapeStreams +# +############################################## + + +CC = gcc +CXX = g++ + + +INC = -I. -I.. + +XSLT_CFLAGS = `pkg-config --cflags libxslt` +XSLT_LIBS = `pkg-config --libs libxslt` + +GLIBMM_CFLAGS = `pkg-config --cflags glibmm-2.4` +GLIBMM_LIBS = `pkg-config --libs glibmm-2.4` + +CFLAGS = -g $(GLIBMM_CFLAGS) $(XSLT_CFLAGS) +LIBS = $(GLIBMM_LIBS) $(XSLT_LIBS) ../uri.o -lz + +OBJ = \ +inkscapestream.o \ +base64stream.o \ +gzipstream.o \ +stringstream.o \ +uristream.o \ +xsltstream.o \ +ftos.o + +all: streamtest + +streamtest: inkscapestream.h libstream.a streamtest.o + $(CXX) -o streamtest streamtest.o libstream.a $(LIBS) + +libstream.a: $(OBJ) + ar crv libstream.a $(OBJ) + + +.cpp.o: + $(CXX) $(CFLAGS) $(INC) -c -o $@ $< + +clean: + -$(RM) *.o *.a + -$(RM) streamtest + diff --git a/src/io/stream/README b/src/io/stream/README new file mode 100644 index 0000000..67d8721 --- /dev/null +++ b/src/io/stream/README @@ -0,0 +1,13 @@ + +This directory contains code related to streams. + +Base class: + inkscapestream.h Used by other stream handling code and odf, filter-file + +Derived classes: + bufferstream.h Used by odf + gzipstream.h Used by repr-io.cpp + stringstream.h Used by rerp-io.cpp and ui/clipboard.cpp + Note file with same name in src/svg + uristream.h Used by repr-io.cpp + xsltstream.h Used by none one (except testfile) diff --git a/src/io/stream/bufferstream.cpp b/src/io/stream/bufferstream.cpp new file mode 100644 index 0000000..1723ee1 --- /dev/null +++ b/src/io/stream/bufferstream.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** + * @file + * Phoebe DOM Implementation. + * + * This is a C++ approximation of the W3C DOM model, which follows + * fairly closely the specifications in the various .idl files, copies of + * which are provided for reference. Most important is this one: + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + *//* + * Authors: + * see git history + * Bob Jamison + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ + +/** + * This class provided buffered endpoints for input and output. + */ + +#include "bufferstream.h" + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# B U F F E R I N P U T S T R E A M +//######################################################################### +/** + * + */ +BufferInputStream::BufferInputStream( + const std::vector &sourceBuffer) + : buffer(sourceBuffer) +{ + position = 0; + closed = false; +} + +/** + * + */ +BufferInputStream::~BufferInputStream() += default; + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int BufferInputStream::available() +{ + if (closed) + return -1; + return buffer.size() - position; +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void BufferInputStream::close() +{ + closed = true; +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int BufferInputStream::get() +{ + if (closed) + return -1; + if (position >= (int)buffer.size()) + return -1; + int ch = (int) buffer[position++]; + return ch; +} + + + + +//######################################################################### +//# B U F F E R O U T P U T S T R E A M +//######################################################################### + +/** + * + */ +BufferOutputStream::BufferOutputStream() +{ + closed = false; +} + +/** + * + */ +BufferOutputStream::~BufferOutputStream() += default; + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void BufferOutputStream::close() +{ + closed = true; +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void BufferOutputStream::flush() +{ + //nothing to do +} + +/** + * Writes the specified byte to this output stream. + */ +int BufferOutputStream::put(char ch) +{ + if (closed) + return -1; + buffer.push_back(ch); + return 1; +} + + + + +} //namespace IO +} //namespace Inkscape + +//######################################################################### +//# E N D O F F I L E +//######################################################################### diff --git a/src/io/stream/bufferstream.h b/src/io/stream/bufferstream.h new file mode 100644 index 0000000..811ab0d --- /dev/null +++ b/src/io/stream/bufferstream.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** + * @file + * Phoebe DOM Implementation. + * + * This is a C++ approximation of the W3C DOM model, which follows + * fairly closely the specifications in the various .idl files, copies of + * which are provided for reference. Most important is this one: + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + *//* + * Authors: + * see git history + * Bob Jamison + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_BUFFERSTREAM_H +#define SEEN_BUFFERSTREAM_H + + +#include +#include "inkscapestream.h" + + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# S T R I N G I N P U T S T R E A M +//######################################################################### + +/** + * This class is for reading character from a DOMString + * + */ +class BufferInputStream : public InputStream +{ + +public: + + BufferInputStream(const std::vector &sourceBuffer); + ~BufferInputStream() override; + int available() override; + void close() override; + int get() override; + +private: + const std::vector &buffer; + long position; + bool closed; + +}; // class BufferInputStream + + + + +//######################################################################### +//# B U F F E R O U T P U T S T R E A M +//######################################################################### + +/** + * This class is for sending a stream to a character buffer + * + */ +class BufferOutputStream : public OutputStream +{ + +public: + + BufferOutputStream(); + ~BufferOutputStream() override; + void close() override; + void flush() override; + int put(char ch) override; + virtual std::vector &getBuffer() + { return buffer; } + + virtual void clear() + { buffer.clear(); } + +private: + std::vector buffer; + bool closed; + +}; // class BufferOutputStream + + + +} //namespace IO +} //namespace Inkscape + + + +#endif // SEEN_BUFFERSTREAM_H diff --git a/src/io/stream/gzipstream.cpp b/src/io/stream/gzipstream.cpp new file mode 100644 index 0000000..03960b0 --- /dev/null +++ b/src/io/stream/gzipstream.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * Zlib-enabled input and output streams + *//* + * Authors: + * see git history + * Bob Jamison + * + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* + * This is a thin wrapper of libz calls, in order + * to provide a simple interface to our developers + * for gzip input and output. + */ + +#include "gzipstream.h" +#include +#include +#include +#include + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# G Z I P I N P U T S T R E A M +//######################################################################### + +#define OUT_SIZE 4000 + +/** + * + */ +GzipInputStream::GzipInputStream(InputStream &sourceStream) + : BasicInputStream(sourceStream), + loaded(false), + totalIn(0), + totalOut(0), + outputBuf(nullptr), + srcBuf(nullptr), + crc(0), + srcCrc(0), + srcSiz(0), + srcConsumed(0), + srcLen(0), + outputBufPos(0), + outputBufLen(0) +{ + memset( &d_stream, 0, sizeof(d_stream) ); +} + +/** + * + */ +GzipInputStream::~GzipInputStream() +{ + close(); + if ( srcBuf ) { + delete[] srcBuf; + srcBuf = nullptr; + } + if ( outputBuf ) { + delete[] outputBuf; + outputBuf = nullptr; + } +} + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int GzipInputStream::available() +{ + if (closed || !outputBuf) + return 0; + return outputBufLen - outputBufPos; +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void GzipInputStream::close() +{ + if (closed) + return; + + int zerr = inflateEnd(&d_stream); + if (zerr != Z_OK) { + printf("inflateEnd: Some kind of problem: %d\n", zerr); + } + + if ( srcBuf ) { + delete[] srcBuf; + srcBuf = nullptr; + } + if ( outputBuf ) { + delete[] outputBuf; + outputBuf = nullptr; + } + closed = true; +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int GzipInputStream::get() +{ + int ch = -1; + if (closed) { + // leave return value -1 + } + else if (!loaded && !load()) { + closed=true; + } else { + loaded = true; + + if ( outputBufPos >= outputBufLen ) { + // time to read more, if we can + fetchMore(); + } + + if ( outputBufPos < outputBufLen ) { + ch = (int)outputBuf[outputBufPos++]; + } + } + + return ch; +} + +#define FTEXT 0x01 +#define FHCRC 0x02 +#define FEXTRA 0x04 +#define FNAME 0x08 +#define FCOMMENT 0x10 + +bool GzipInputStream::load() +{ + crc = crc32(0L, Z_NULL, 0); + + std::vector inputBuf; + while (true) + { + int ch = source.get(); + if (ch<0) + break; + inputBuf.push_back(static_cast(ch & 0xff)); + } + long inputBufLen = inputBuf.size(); + + if (inputBufLen < 19) //header + tail + 1 + { + return false; + } + + srcLen = inputBuf.size(); + srcBuf = new (std::nothrow) Byte [srcLen]; + if (!srcBuf) { + return false; + } + + outputBuf = new (std::nothrow) unsigned char [OUT_SIZE]; + if ( !outputBuf ) { + delete[] srcBuf; + srcBuf = nullptr; + return false; + } + outputBufLen = 0; // Not filled in yet + + std::vector::iterator iter; + Bytef *p = srcBuf; + for (iter=inputBuf.begin() ; iter != inputBuf.end() ; ++iter) + { + *p++ = *iter; + } + + int headerLen = 10; + + //Magic + //int val = (int)srcBuf[0]; + ////printf("val:%x\n", val); + //val = (int)srcBuf[1]; + ////printf("val:%x\n", val); + + ////Method + //val = (int)srcBuf[2]; + ////printf("val:%x\n", val); + + //flags + int flags = static_cast(srcBuf[3]); + + ////time + //val = (int)srcBuf[4]; + //val = (int)srcBuf[5]; + //val = (int)srcBuf[6]; + //val = (int)srcBuf[7]; + + ////xflags + //val = (int)srcBuf[8]; + ////OS + //val = (int)srcBuf[9]; + +// if ( flags & FEXTRA ) { +// headerLen += 2; +// int xlen = +// TODO deal with optional header parts +// } + if ( flags & FNAME ) { + int cur = 10; + while ( srcBuf[cur] ) + { + cur++; + headerLen++; + } + headerLen++; + } + + + srcCrc = ((0x0ff & srcBuf[srcLen - 5]) << 24) + | ((0x0ff & srcBuf[srcLen - 6]) << 16) + | ((0x0ff & srcBuf[srcLen - 7]) << 8) + | ((0x0ff & srcBuf[srcLen - 8]) << 0); + //printf("srcCrc:%lx\n", srcCrc); + + srcSiz = ((0x0ff & srcBuf[srcLen - 1]) << 24) + | ((0x0ff & srcBuf[srcLen - 2]) << 16) + | ((0x0ff & srcBuf[srcLen - 3]) << 8) + | ((0x0ff & srcBuf[srcLen - 4]) << 0); + //printf("srcSiz:%lx/%ld\n", srcSiz, srcSiz); + + //outputBufLen = srcSiz + srcSiz/100 + 14; + + unsigned char *data = srcBuf + headerLen; + unsigned long dataLen = srcLen - (headerLen + 8); + //printf("%x %x\n", data[0], data[dataLen-1]); + + d_stream.zalloc = (alloc_func)nullptr; + d_stream.zfree = (free_func)nullptr; + d_stream.opaque = (voidpf)nullptr; + d_stream.next_in = data; + d_stream.avail_in = dataLen; + d_stream.next_out = outputBuf; + d_stream.avail_out = OUT_SIZE; + + int zerr = inflateInit2(&d_stream, -MAX_WBITS); + if ( zerr == Z_OK ) + { + zerr = fetchMore(); + } else { + printf("inflateInit2: Some kind of problem: %d\n", zerr); + } + + + return (zerr == Z_OK) || (zerr == Z_STREAM_END); +} + + +int GzipInputStream::fetchMore() +{ + // TODO assumes we aren't called till the buffer is empty + d_stream.next_out = outputBuf; + d_stream.avail_out = OUT_SIZE; + outputBufLen = 0; + outputBufPos = 0; + + int zerr = inflate( &d_stream, Z_SYNC_FLUSH ); + if ( zerr == Z_OK || zerr == Z_STREAM_END ) { + outputBufLen = OUT_SIZE - d_stream.avail_out; + if ( outputBufLen ) { + crc = crc32(crc, const_cast(outputBuf), outputBufLen); + } + //printf("crc:%lx\n", crc); +// } else if ( zerr != Z_STREAM_END ) { +// // TODO check to be sure this won't happen for partial end reads +// printf("inflate: Some kind of problem: %d\n", zerr); + } + + return zerr; +} + +//######################################################################### +//# G Z I P O U T P U T S T R E A M +//######################################################################### + +/** + * + */ +GzipOutputStream::GzipOutputStream(OutputStream &destinationStream) + : BasicOutputStream(destinationStream) +{ + + totalIn = 0; + totalOut = 0; + crc = crc32(0L, Z_NULL, 0); + + //Gzip header + destination.put(0x1f); + destination.put(0x8b); + + //Say it is compressed + destination.put(Z_DEFLATED); + + //flags + destination.put(0); + + //time + destination.put(0); + destination.put(0); + destination.put(0); + destination.put(0); + + //xflags + destination.put(0); + + //OS code - from zutil.h + //destination.put(OS_CODE); + //apparently, we should not explicitly include zutil.h + destination.put(0); + +} + +/** + * + */ +GzipOutputStream::~GzipOutputStream() +{ + close(); +} + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void GzipOutputStream::close() +{ + if (closed) + return; + + flush(); + + //# Send the CRC + uLong outlong = crc; + for (int n = 0; n < 4; n++) + { + destination.put(static_cast(outlong & 0xff)); + outlong >>= 8; + } + //# send the file length + outlong = totalIn & 0xffffffffL; + for (int n = 0; n < 4; n++) + { + destination.put(static_cast(outlong & 0xff)); + outlong >>= 8; + } + + destination.close(); + closed = true; +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void GzipOutputStream::flush() +{ + if (closed || inputBuf.empty()) + { + return; + } + + uLong srclen = inputBuf.size(); + Bytef *srcbuf = new (std::nothrow) Bytef [srclen]; + if (!srcbuf) + { + return; + } + + uLong destlen = srclen; + Bytef *destbuf = new (std::nothrow) Bytef [(destlen + (srclen/100) + 13)]; + if (!destbuf) + { + delete[] srcbuf; + return; + } + + std::vector::iterator iter; + Bytef *p = srcbuf; + for (iter=inputBuf.begin() ; iter != inputBuf.end() ; ++iter) + *p++ = *iter; + + crc = crc32(crc, const_cast(srcbuf), srclen); + + int zerr = compress(destbuf, static_cast(&destlen), srcbuf, srclen); + if (zerr != Z_OK) + { + printf("Some kind of problem\n"); + } + + totalOut += destlen; + //skip the redundant zlib header and checksum + for (uLong i=2; i + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "inkscapestream.h" +#include + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# G Z I P I N P U T S T R E A M +//######################################################################### + +/** + * This class is for deflating a gzip-compressed InputStream source + * + */ +class GzipInputStream : public BasicInputStream +{ + +public: + + GzipInputStream(InputStream &sourceStream); + + ~GzipInputStream() override; + + int available() override; + + void close() override; + + int get() override; + +private: + + bool load(); + int fetchMore(); + + bool loaded; + + long totalIn; + long totalOut; + + unsigned char *outputBuf; + unsigned char *srcBuf; + + unsigned long crc; + unsigned long srcCrc; + unsigned long srcSiz; + unsigned long srcConsumed; + unsigned long srcLen; + long outputBufPos; + long outputBufLen; + + z_stream d_stream; +}; // class GzipInputStream + + + + +//######################################################################### +//# G Z I P O U T P U T S T R E A M +//######################################################################### + +/** + * This class is for gzip-compressing data going to the + * destination OutputStream + * + */ +class GzipOutputStream : public BasicOutputStream +{ + +public: + + GzipOutputStream(OutputStream &destinationStream); + + ~GzipOutputStream() override; + + void close() override; + + void flush() override; + + int put(char ch) override; + +private: + + std::vector inputBuf; + + long totalIn; + long totalOut; + unsigned long crc; + +}; // class GzipOutputStream + + + + + + + +} // namespace IO +} // namespace Inkscape + + +#endif /* __INKSCAPE_IO_GZIPSTREAM_H__ */ diff --git a/src/io/stream/inkscapestream.cpp b/src/io/stream/inkscapestream.cpp new file mode 100644 index 0000000..bc6dc1d --- /dev/null +++ b/src/io/stream/inkscapestream.cpp @@ -0,0 +1,800 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Our base input/output stream classes. These are is directly + * inherited from iostreams, and includes any extra + * functionality that we might need. + * + * Authors: + * Bob Jamison + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "inkscapestream.h" + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# U T I L I T Y +//######################################################################### + +void pipeStream(InputStream &source, OutputStream &dest) +{ + for (;;) + { + int ch = source.get(); + if (ch<0) + break; + dest.put(ch); + } + dest.flush(); +} + +//######################################################################### +//# B A S I C I N P U T S T R E A M +//######################################################################### + + +/** + * + */ +BasicInputStream::BasicInputStream(InputStream &sourceStream) + : source(sourceStream) +{ + closed = false; +} + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int BasicInputStream::available() +{ + if (closed) + return 0; + return source.available(); +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void BasicInputStream::close() +{ + if (closed) + return; + source.close(); + closed = true; +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int BasicInputStream::get() +{ + if (closed) + return -1; + return source.get(); +} + + + +//######################################################################### +//# B A S I C O U T P U T S T R E A M +//######################################################################### + +/** + * + */ +BasicOutputStream::BasicOutputStream(OutputStream &destinationStream) + : destination(destinationStream) +{ + closed = false; +} + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void BasicOutputStream::close() +{ + if (closed) + return; + destination.close(); + closed = true; +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void BasicOutputStream::flush() +{ + if (closed) + return; + destination.flush(); +} + +/** + * Writes the specified byte to this output stream. + */ +int BasicOutputStream::put(char ch) +{ + if (closed) + return -1; + destination.put(ch); + return 1; +} + + + +//######################################################################### +//# B A S I C R E A D E R +//######################################################################### +/** + * + */ +BasicReader::BasicReader(Reader &sourceReader) +{ + source = &sourceReader; +} + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this reader without blocking by the next caller of a method for + * this reader. + */ +int BasicReader::available() +{ + if (source) + return source->available(); + else + return 0; +} + + +/** + * Closes this reader and releases any system resources + * associated with the reader. + */ +void BasicReader::close() +{ + if (source) + source->close(); +} + +/** + * Reads the next byte of data from the reader. + */ +char BasicReader::get() +{ + if (source) + return source->get(); + else + return (char)-1; +} + + +/** + * Reads a line of data from the reader. + */ +Glib::ustring BasicReader::readLine() +{ + Glib::ustring str; + while (available() > 0) + { + char ch = get(); + if (ch == '\n') + break; + str.push_back(ch); + } + return str; +} + +/** + * Reads a line of data from the reader. + */ +Glib::ustring BasicReader::readWord() +{ + Glib::ustring str; + while (available() > 0) + { + char ch = get(); + if (!std::isprint(ch)) + break; + str.push_back(ch); + } + return str; +} + + +static bool getLong(Glib::ustring &str, long *val) +{ + const char *begin = str.raw().c_str(); + char *end; + long ival = strtol(begin, &end, 10); + if (str == end) + return false; + *val = ival; + return true; +} + +static bool getULong(Glib::ustring &str, unsigned long *val) +{ + const char *begin = str.raw().c_str(); + char *end; + unsigned long ival = strtoul(begin, &end, 10); + if (str == end) + return false; + *val = ival; + return true; +} + +static bool getDouble(Glib::ustring &str, double *val) +{ + const char *begin = str.raw().c_str(); + char *end; + double ival = strtod(begin, &end); + if (str == end) + return false; + *val = ival; + return true; +} + + + +const Reader &BasicReader::readBool (bool& val ) +{ + Glib::ustring buf = readWord(); + if (buf == "true") + val = true; + else + val = false; + return *this; +} + +const Reader &BasicReader::readShort (short& val ) +{ + Glib::ustring buf = readWord(); + long ival; + if (getLong(buf, &ival)) + val = (short) ival; + return *this; +} + +const Reader &BasicReader::readUnsignedShort (unsigned short& val ) +{ + Glib::ustring buf = readWord(); + unsigned long ival; + if (getULong(buf, &ival)) + val = (unsigned short) ival; + return *this; +} + +const Reader &BasicReader::readInt (int& val ) +{ + Glib::ustring buf = readWord(); + long ival; + if (getLong(buf, &ival)) + val = (int) ival; + return *this; +} + +const Reader &BasicReader::readUnsignedInt (unsigned int& val ) +{ + Glib::ustring buf = readWord(); + unsigned long ival; + if (getULong(buf, &ival)) + val = (unsigned int) ival; + return *this; +} + +const Reader &BasicReader::readLong (long& val ) +{ + Glib::ustring buf = readWord(); + long ival; + if (getLong(buf, &ival)) + val = ival; + return *this; +} + +const Reader &BasicReader::readUnsignedLong (unsigned long& val ) +{ + Glib::ustring buf = readWord(); + unsigned long ival; + if (getULong(buf, &ival)) + val = ival; + return *this; +} + +const Reader &BasicReader::readFloat (float& val ) +{ + Glib::ustring buf = readWord(); + double ival; + if (getDouble(buf, &ival)) + val = (float)ival; + return *this; +} + +const Reader &BasicReader::readDouble (double& val ) +{ + Glib::ustring buf = readWord(); + double ival; + if (getDouble(buf, &ival)) + val = ival; + return *this; +} + + + +//######################################################################### +//# I N P U T S T R E A M R E A D E R +//######################################################################### + + +InputStreamReader::InputStreamReader(InputStream &inputStreamSource) + : inputStream(inputStreamSource) +{ +} + + + +/** + * Close the underlying OutputStream + */ +void InputStreamReader::close() +{ + inputStream.close(); +} + +/** + * Flush the underlying OutputStream + */ +int InputStreamReader::available() +{ + return inputStream.available(); +} + +/** + * Overloaded to receive its bytes from an InputStream + * rather than a Reader + */ +char InputStreamReader::get() +{ + char ch = inputStream.get(); + return ch; +} + + + +//######################################################################### +//# S T D R E A D E R +//######################################################################### + + +/** + * + */ +StdReader::StdReader() +{ + inputStream = new StdInputStream(); +} + +/** + * + */ +StdReader::~StdReader() +{ + delete inputStream; +} + + + +/** + * Close the underlying OutputStream + */ +void StdReader::close() +{ + inputStream->close(); +} + +/** + * Flush the underlying OutputStream + */ +int StdReader::available() +{ + return inputStream->available(); +} + +/** + * Overloaded to receive its bytes from an InputStream + * rather than a Reader + */ +char StdReader::get() +{ + char ch = inputStream->get(); + return ch; +} + + + + + +//######################################################################### +//# B A S I C W R I T E R +//######################################################################### + +/** + * + */ +BasicWriter::BasicWriter(Writer &destinationWriter) +{ + destination = &destinationWriter; +} + +/** + * Closes this writer and releases any system resources + * associated with this writer. + */ +void BasicWriter::close() +{ + if (destination) + destination->close(); +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void BasicWriter::flush() +{ + if (destination) + destination->flush(); +} + +/** + * Writes the specified byte to this output writer. + */ +void BasicWriter::put(char ch) +{ + if (destination) + destination->put(ch); +} + +/** + * Provide printf()-like formatting + */ +Writer &BasicWriter::printf(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + gchar *buf = g_strdup_vprintf(fmt, args); + va_end(args); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} +/** + * Writes the specified character to this output writer. + */ +Writer &BasicWriter::writeChar(char ch) +{ + put(ch); + return *this; +} + + +/** + * Writes the specified unicode string to this output writer. + */ +Writer &BasicWriter::writeUString(const Glib::ustring &str) +{ + writeStdString(str.raw()); + return *this; +} + +/** + * Writes the specified standard string to this output writer. + */ +Writer &BasicWriter::writeStdString(const std::string &str) +{ + for (char it : str) { + put(it); + } + return *this; +} + +/** + * Writes the specified character string to this output writer. + */ +Writer &BasicWriter::writeString(const char *str) +{ + std::string tmp; + if (str) + tmp = str; + else + tmp = "null"; + writeStdString(tmp); + return *this; +} + + + + +/** + * + */ +Writer &BasicWriter::writeBool (bool val ) +{ + if (val) + writeString("true"); + else + writeString("false"); + return *this; +} + + +/** + * + */ +Writer &BasicWriter::writeShort (short val ) +{ + gchar *buf = g_strdup_printf("%d", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + + + +/** + * + */ +Writer &BasicWriter::writeUnsignedShort (unsigned short val ) +{ + gchar *buf = g_strdup_printf("%u", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeInt (int val) +{ + gchar *buf = g_strdup_printf("%d", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeUnsignedInt (unsigned int val) +{ + gchar *buf = g_strdup_printf("%u", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeLong (long val) +{ + gchar *buf = g_strdup_printf("%ld", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeUnsignedLong(unsigned long val) +{ + gchar *buf = g_strdup_printf("%lu", val); + if (buf) { + writeString(buf); + g_free(buf); + } + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeFloat(float val) +{ +#if 1 + gchar *buf = g_strdup_printf("%8.3f", val); + if (buf) { + writeString(buf); + g_free(buf); + } +#else + std::string tmp = ftos(val, 'g', 8, 3, 0); + writeStdString(tmp); +#endif + return *this; +} + +/** + * + */ +Writer &BasicWriter::writeDouble(double val) +{ +#if 1 + gchar *buf = g_strdup_printf("%8.3f", val); + if (buf) { + writeString(buf); + g_free(buf); + } +#else + std::string tmp = ftos(val, 'g', 8, 3, 0); + writeStdString(tmp); +#endif + return *this; +} + + +Writer& operator<< (Writer &writer, char val) + { return writer.writeChar(val); } + +Writer& operator<< (Writer &writer, Glib::ustring &val) + { return writer.writeUString(val); } + +Writer& operator<< (Writer &writer, std::string &val) + { return writer.writeStdString(val); } + +Writer& operator<< (Writer &writer, char const *val) + { return writer.writeString(val); } + +Writer& operator<< (Writer &writer, bool val) + { return writer.writeBool(val); } + +Writer& operator<< (Writer &writer, short val) + { return writer.writeShort(val); } + +Writer& operator<< (Writer &writer, unsigned short val) + { return writer.writeUnsignedShort(val); } + +Writer& operator<< (Writer &writer, int val) + { return writer.writeInt(val); } + +Writer& operator<< (Writer &writer, unsigned int val) + { return writer.writeUnsignedInt(val); } + +Writer& operator<< (Writer &writer, long val) + { return writer.writeLong(val); } + +Writer& operator<< (Writer &writer, unsigned long val) + { return writer.writeUnsignedLong(val); } + +Writer& operator<< (Writer &writer, float val) + { return writer.writeFloat(val); } + +Writer& operator<< (Writer &writer, double val) + { return writer.writeDouble(val); } + + + +//######################################################################### +//# O U T P U T S T R E A M W R I T E R +//######################################################################### + + +OutputStreamWriter::OutputStreamWriter(OutputStream &outputStreamDest) + : outputStream(outputStreamDest) +{ +} + + + +/** + * Close the underlying OutputStream + */ +void OutputStreamWriter::close() +{ + flush(); + outputStream.close(); +} + +/** + * Flush the underlying OutputStream + */ +void OutputStreamWriter::flush() +{ + outputStream.flush(); +} + +/** + * Overloaded to redirect the output chars from the next Writer + * in the chain to an OutputStream instead. + */ +void OutputStreamWriter::put(char ch) +{ + outputStream.put(ch); +} + +//######################################################################### +//# S T D W R I T E R +//######################################################################### + + +/** + * + */ +StdWriter::StdWriter() +{ + outputStream = new StdOutputStream(); +} + + +/** + * + */ +StdWriter::~StdWriter() +{ + delete outputStream; +} + + + +/** + * Close the underlying OutputStream + */ +void StdWriter::close() +{ + flush(); + outputStream->close(); +} + +/** + * Flush the underlying OutputStream + */ +void StdWriter::flush() +{ + outputStream->flush(); +} + +/** + * Overloaded to redirect the output chars from the next Writer + * in the chain to an OutputStream instead. + */ +void StdWriter::put(char ch) +{ + outputStream->put(ch); +} + + +} // namespace IO +} // namespace Inkscape + + +//######################################################################### +//# E N D O F F I L E +//######################################################################### diff --git a/src/io/stream/inkscapestream.h b/src/io/stream/inkscapestream.h new file mode 100644 index 0000000..68c28ba --- /dev/null +++ b/src/io/stream/inkscapestream.h @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_IO_INKSCAPESTREAM_H +#define SEEN_INKSCAPE_IO_INKSCAPESTREAM_H +/* + * Authors: + * Bob Jamison + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +namespace Inkscape +{ +namespace IO +{ + +class StreamException : public std::exception +{ +public: + + StreamException(const char *theReason) noexcept + { reason = theReason; } + StreamException(Glib::ustring &theReason) noexcept + { reason = theReason; } + ~StreamException() noexcept override + = default; + char const *what() const noexcept override + { return reason.c_str(); } + +private: + Glib::ustring reason; + +}; + +//######################################################################### +//# I N P U T S T R E A M +//######################################################################### + +/** + * This interface is the base of all input stream classes. Users who wish + * to make an InputStream that is part of a chain should inherit from + * BasicInputStream. Inherit from this class to make a source endpoint, + * such as a URI or buffer. + * + */ +class InputStream +{ + +public: + + /** + * Constructor. + */ + InputStream() = default; + + /** + * Destructor + */ + virtual ~InputStream() = default; + + /** + * Return the number of bytes that are currently available + * to be read + */ + virtual int available() = 0; + + /** + * Do whatever it takes to 'close' this input stream + * The most likely implementation of this method will be + * for endpoints that use a resource for their data. + */ + virtual void close() = 0; + + /** + * Read one byte from this input stream. This is a blocking + * call. If no data is currently available, this call will + * not return until it exists. If the user does not want + * their code to block, then the usual solution is: + * if (available() > 0) + * myChar = get(); + * This call returns -1 on end-of-file. + */ + virtual int get() = 0; + +}; // class InputStream + + + + +/** + * This is the class that most users should inherit, to provide + * their own streams. + * + */ +class BasicInputStream : public InputStream +{ + +public: + + BasicInputStream(InputStream &sourceStream); + + ~BasicInputStream() override = default; + + int available() override; + + void close() override; + + int get() override; + +protected: + + bool closed; + + InputStream &source; + +private: + + +}; // class BasicInputStream + + + +/** + * Convenience class for reading from standard input + */ +class StdInputStream : public InputStream +{ +public: + + int available() override + { return 0; } + + void close() override + { /* do nothing */ } + + int get() override + { return getchar(); } + +}; + + + + + + +//######################################################################### +//# O U T P U T S T R E A M +//######################################################################### + +/** + * This interface is the base of all input stream classes. Users who wish + * to make an OutputStream that is part of a chain should inherit from + * BasicOutputStream. Inherit from this class to make a destination endpoint, + * such as a URI or buffer. + */ +class OutputStream +{ + +public: + + /** + * Constructor. + */ + OutputStream() = default; + + /** + * Destructor + */ + virtual ~OutputStream() = default; + + /** + * This call should + * 1. flush itself + * 2. close itself + * 3. close the destination stream + */ + virtual void close() = 0; + + /** + * This call should push any pending data it might have to + * the destination stream. It should NOT call flush() on + * the destination stream. + */ + virtual void flush() = 0; + + /** + * Send one byte to the destination stream. + */ + virtual int put(char ch) = 0; + + +}; // class OutputStream + + +/** + * This is the class that most users should inherit, to provide + * their own output streams. + */ +class BasicOutputStream : public OutputStream +{ + +public: + + BasicOutputStream(OutputStream &destinationStream); + + ~BasicOutputStream() override = default; + + void close() override; + + void flush() override; + + int put(char ch) override; + +protected: + + bool closed; + + OutputStream &destination; + + +}; // class BasicOutputStream + + + +/** + * Convenience class for writing to standard output + */ +class StdOutputStream : public OutputStream +{ +public: + + void close() override + { } + + void flush() override + { } + + int put(char ch) override + {return putchar(ch); } + +}; + + + + +//######################################################################### +//# R E A D E R +//######################################################################### + + +/** + * This interface and its descendants are for unicode character-oriented input + * + */ +class Reader +{ + +public: + + /** + * Constructor. + */ + Reader() = default; + + /** + * Destructor + */ + virtual ~Reader() = default; + + + virtual int available() = 0; + + virtual void close() = 0; + + virtual char get() = 0; + + virtual Glib::ustring readLine() = 0; + + virtual Glib::ustring readWord() = 0; + + /* Input formatting */ + virtual const Reader& readBool (bool& val ) = 0; + virtual const Reader& operator>> (bool& val ) = 0; + + virtual const Reader& readShort (short &val) = 0; + virtual const Reader& operator>> (short &val) = 0; + + virtual const Reader& readUnsignedShort (unsigned short &val) = 0; + virtual const Reader& operator>> (unsigned short &val) = 0; + + virtual const Reader& readInt (int &val) = 0; + virtual const Reader& operator>> (int &val) = 0; + + virtual const Reader& readUnsignedInt (unsigned int &val) = 0; + virtual const Reader& operator>> (unsigned int &val) = 0; + + virtual const Reader& readLong (long &val) = 0; + virtual const Reader& operator>> (long &val) = 0; + + virtual const Reader& readUnsignedLong (unsigned long &val) = 0; + virtual const Reader& operator>> (unsigned long &val) = 0; + + virtual const Reader& readFloat (float &val) = 0; + virtual const Reader& operator>> (float &val) = 0; + + virtual const Reader& readDouble (double &val) = 0; + virtual const Reader& operator>> (double &val) = 0; + +}; // interface Reader + + + +/** + * This class and its descendants are for unicode character-oriented input + * + */ +class BasicReader : public Reader +{ + +public: + + BasicReader(Reader &sourceStream); + + ~BasicReader() override = default; + + int available() override; + + void close() override; + + char get() override; + + Glib::ustring readLine() override; + + Glib::ustring readWord() override; + + /* Input formatting */ + const Reader& readBool (bool& val ) override; + const Reader& operator>> (bool& val ) override + { return readBool(val); } + + const Reader& readShort (short &val) override; + const Reader& operator>> (short &val) override + { return readShort(val); } + + const Reader& readUnsignedShort (unsigned short &val) override; + const Reader& operator>> (unsigned short &val) override + { return readUnsignedShort(val); } + + const Reader& readInt (int &val) override; + const Reader& operator>> (int &val) override + { return readInt(val); } + + const Reader& readUnsignedInt (unsigned int &val) override; + const Reader& operator>> (unsigned int &val) override + { return readUnsignedInt(val); } + + const Reader& readLong (long &val) override; + const Reader& operator>> (long &val) override + { return readLong(val); } + + const Reader& readUnsignedLong (unsigned long &val) override; + const Reader& operator>> (unsigned long &val) override + { return readUnsignedLong(val); } + + const Reader& readFloat (float &val) override; + const Reader& operator>> (float &val) override + { return readFloat(val); } + + const Reader& readDouble (double &val) override; + const Reader& operator>> (double &val) override + { return readDouble(val); } + + +protected: + + Reader *source; + + BasicReader() + { source = nullptr; } + +private: + +}; // class BasicReader + + + +/** + * Class for placing a Reader on an open InputStream + * + */ +class InputStreamReader : public BasicReader +{ +public: + + InputStreamReader(InputStream &inputStreamSource); + + /*Overload these 3 for your implementation*/ + int available() override; + + void close() override; + + char get() override; + + +private: + + InputStream &inputStream; + + +}; + +/** + * Convenience class for reading formatted from standard input + * + */ +class StdReader : public BasicReader +{ +public: + + StdReader(); + + ~StdReader() override; + + /*Overload these 3 for your implementation*/ + int available() override; + + void close() override; + + char get() override; + + +private: + + InputStream *inputStream; + + +}; + + + + + +//######################################################################### +//# W R I T E R +//######################################################################### + +/** + * This interface and its descendants are for unicode character-oriented output + * + */ +class Writer +{ + +public: + + /** + * Constructor. + */ + Writer() = default; + + /** + * Destructor + */ + virtual ~Writer() = default; + + virtual void close() = 0; + + virtual void flush() = 0; + + virtual void put(char ch) = 0; + + /* Formatted output */ + virtual Writer& printf(char const *fmt, ...) G_GNUC_PRINTF(2,3) = 0; + + virtual Writer& writeChar(char val) = 0; + + virtual Writer& writeUString(const Glib::ustring &val) = 0; + + virtual Writer& writeStdString(const std::string &val) = 0; + + virtual Writer& writeString(const char *str) = 0; + + virtual Writer& writeBool (bool val ) = 0; + + virtual Writer& writeShort (short val ) = 0; + + virtual Writer& writeUnsignedShort (unsigned short val ) = 0; + + virtual Writer& writeInt (int val ) = 0; + + virtual Writer& writeUnsignedInt (unsigned int val ) = 0; + + virtual Writer& writeLong (long val ) = 0; + + virtual Writer& writeUnsignedLong (unsigned long val ) = 0; + + virtual Writer& writeFloat (float val ) = 0; + + virtual Writer& writeDouble (double val ) = 0; + + + +}; // interface Writer + + +/** + * This class and its descendants are for unicode character-oriented output + * + */ +class BasicWriter : public Writer +{ + +public: + + BasicWriter(Writer &destinationWriter); + + ~BasicWriter() override = default; + + /*Overload these 3 for your implementation*/ + void close() override; + + void flush() override; + + void put(char ch) override; + + + + /* Formatted output */ + Writer &printf(char const *fmt, ...) override G_GNUC_PRINTF(2,3); + + Writer& writeChar(char val) override; + + Writer& writeUString(const Glib::ustring &val) override; + + Writer& writeStdString(const std::string &val) override; + + Writer& writeString(const char *str) override; + + Writer& writeBool (bool val ) override; + + Writer& writeShort (short val ) override; + + Writer& writeUnsignedShort (unsigned short val ) override; + + Writer& writeInt (int val ) override; + + Writer& writeUnsignedInt (unsigned int val ) override; + + Writer& writeLong (long val ) override; + + Writer& writeUnsignedLong (unsigned long val ) override; + + Writer& writeFloat (float val ) override; + + Writer& writeDouble (double val ) override; + + +protected: + + Writer *destination; + + BasicWriter() + { destination = nullptr; } + +private: + +}; // class BasicWriter + + + +Writer& operator<< (Writer &writer, char val); + +Writer& operator<< (Writer &writer, Glib::ustring &val); + +Writer& operator<< (Writer &writer, std::string &val); + +Writer& operator<< (Writer &writer, char const *val); + +Writer& operator<< (Writer &writer, bool val); + +Writer& operator<< (Writer &writer, short val); + +Writer& operator<< (Writer &writer, unsigned short val); + +Writer& operator<< (Writer &writer, int val); + +Writer& operator<< (Writer &writer, unsigned int val); + +Writer& operator<< (Writer &writer, long val); + +Writer& operator<< (Writer &writer, unsigned long val); + +Writer& operator<< (Writer &writer, float val); + +Writer& operator<< (Writer &writer, double val); + + + + +/** + * Class for placing a Writer on an open OutputStream + * + */ +class OutputStreamWriter : public BasicWriter +{ +public: + + OutputStreamWriter(OutputStream &outputStreamDest); + + /*Overload these 3 for your implementation*/ + void close() override; + + void flush() override; + + void put(char ch) override; + + +private: + + OutputStream &outputStream; + + +}; + + +/** + * Convenience class for writing to standard output + */ +class StdWriter : public BasicWriter +{ +public: + StdWriter(); + + ~StdWriter() override; + + + void close() override; + + + void flush() override; + + + void put(char ch) override; + + +private: + + OutputStream *outputStream; + +}; + +//######################################################################### +//# U T I L I T Y +//######################################################################### + +void pipeStream(InputStream &source, OutputStream &dest); + + + +} // namespace IO +} // namespace Inkscape + + +#endif // SEEN_INKSCAPE_IO_INKSCAPESTREAM_H diff --git a/src/io/stream/streamtest.cpp b/src/io/stream/streamtest.cpp new file mode 100644 index 0000000..5d27058 --- /dev/null +++ b/src/io/stream/streamtest.cpp @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2015 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include +#include // realpath +#include // mkdtemp, realpath +#include // chdir +#include // strlen, strncpy, strrchr + +#include "inkscapestream.h" +#include "base64stream.h" +#include "gzipstream.h" +#include "stringstream.h" +#include "uristream.h" +#include "xsltstream.h" + +// quick way to pass the name of the executable into the test +char myself[PATH_MAX]; + +// names and path storage for other tests +char *xmlname = "crystalegg.xml"; +char xmlpath[PATH_MAX]; +char *xslname = "doc2html.xsl"; +char xslpath[PATH_MAX]; + +bool testUriStream() +{ + printf("######### UriStream copy ############\n"); + Inkscape::URI inUri(myself); + Inkscape::IO::UriInputStream ins(inUri); + Inkscape::URI outUri("streamtest.copy"); + Inkscape::IO::UriOutputStream outs(outUri); + + pipeStream(ins, outs); + + ins.close(); + outs.close(); + + return true; +} + +bool testWriter() +{ + printf("######### OutputStreamWriter ############\n"); + Inkscape::IO::StdOutputStream outs; + Inkscape::IO::OutputStreamWriter writer(outs); + + writer << "Hello, world! " << 123.45 << " times\n"; + + writer.printf("There are %f quick brown foxes in %d states\n", 123.45, 88); + + return true; +} + +bool testStdWriter() +{ + printf("######### StdWriter ############\n"); + Inkscape::IO::StdWriter writer; + + writer << "Hello, world! " << 123.45 << " times\n"; + + writer.printf("There are %f quick brown foxes in %d states\n", 123.45, 88); + + return true; +} + +bool testBase64() +{ + printf("######### Base64 Out ############\n"); + Inkscape::URI plainInUri(xmlpath); + Inkscape::IO::UriInputStream ins1(plainInUri); + + Inkscape::URI b64OutUri("crystalegg.xml.b64"); + Inkscape::IO::UriOutputStream outs1(b64OutUri); + Inkscape::IO::Base64OutputStream b64Outs(outs1); + + pipeStream(ins1, b64Outs); + + ins1.close(); + b64Outs.close(); + + printf("######### Base64 In ############\n"); + Inkscape::URI b64InUri("crystalegg.xml.b64"); + Inkscape::IO::UriInputStream ins2(b64InUri); + Inkscape::IO::Base64InputStream b64Ins(ins2); + + Inkscape::URI plainOutUri("crystalegg.xml.b64dec"); + Inkscape::IO::UriOutputStream outs2(plainOutUri); + + pipeStream(b64Ins, outs2); + + outs2.close(); + b64Ins.close(); + + return true; +} + +bool testXslt() +{ + printf("######### XSLT Sheet ############\n"); + Inkscape::URI xsltSheetUri(xslpath); + Inkscape::IO::UriInputStream xsltSheetIns(xsltSheetUri); + Inkscape::IO::XsltStyleSheet stylesheet(xsltSheetIns); + xsltSheetIns.close(); + + Inkscape::URI sourceUri(xmlpath); + Inkscape::IO::UriInputStream xmlIns(sourceUri); + + printf("######### XSLT Input ############\n"); + Inkscape::URI destUri("test.html"); + Inkscape::IO::UriOutputStream xmlOuts(destUri); + + Inkscape::IO::XsltInputStream xsltIns(xmlIns, stylesheet); + pipeStream(xsltIns, xmlOuts); + xsltIns.close(); + xmlOuts.close(); + + + printf("######### XSLT Output ############\n"); + + Inkscape::IO::UriInputStream xmlIns2(sourceUri); + + Inkscape::URI destUri2("test2.html"); + Inkscape::IO::UriOutputStream xmlOuts2(destUri2); + + Inkscape::IO::XsltOutputStream xsltOuts(xmlOuts2, stylesheet); + pipeStream(xmlIns2, xsltOuts); + xmlIns2.close(); + xsltOuts.close(); + + return true; +} + +bool testGzip() +{ + + printf("######### Gzip Output ############\n"); + Inkscape::URI gzUri("test.gz"); + Inkscape::URI sourceUri(xmlpath); + Inkscape::IO::UriInputStream sourceIns(sourceUri); + Inkscape::IO::UriOutputStream gzOuts(gzUri); + + Inkscape::IO::GzipOutputStream gzipOuts(gzOuts); + pipeStream(sourceIns, gzipOuts); + sourceIns.close(); + gzipOuts.close(); + + printf("######### Gzip Input ############\n"); + + Inkscape::IO::UriInputStream gzIns(gzUri); + Inkscape::URI destUri("crystalegg2.xml"); + Inkscape::IO::UriOutputStream destOuts(destUri); + + Inkscape::IO::GzipInputStream gzipIns(gzIns); + pipeStream(gzipIns, destOuts); + gzipIns.close(); + destOuts.close(); + + return true; +} + +bool doTest() +{ + if (!testUriStream()) + { + return false; + } + if (!testWriter()) + { + return false; + } + if (!testStdWriter()) + { + return false; + } + if (!testBase64()) + { + return false; + } + if (!testXslt()) + { + return false; + } + if (!testGzip()) + { + return false; + } + return true; +} + +void path_init(char *path, char *name) +{ + if (strlen(name)>PATH_MAX-strlen(myself)) + { + printf("merging paths would be too long\n"); + exit(1); + } + strncpy(path,myself,PATH_MAX); + char * ptr = strrchr(path,'/'); + if (!ptr) + { + printf("path '%s' is missing any slashes\n",path); + exit(1); + } + strncpy(ptr+1,name,strlen(name)+1); + path[PATH_MAX-1] = '\0'; + printf("'%s'\n",path); +} + + +int main(int argc, char **argv) +{ + if (!realpath(argv[0],myself)) + { + perror("realpath"); + return 1; + } + path_init(xmlpath,xmlname); + path_init(xslpath,xslname); + + // create temp files somewhere else instead of current dir + // TODO: clean them up too + char * testpath = strdup("/tmp/streamtest-XXXXXX"); + char * testpath2; + testpath2 = mkdtemp(testpath); + free(testpath); + if (!testpath2) + { + perror("mkdtemp"); + return 1; + } + if (chdir(testpath2)) + { + perror("chdir"); + return 1; + } + + if (!doTest()) + { + printf("#### Test failed\n"); + return 1; + } + else + { + printf("##### Test succeeded\n"); + } + return 0; +} diff --git a/src/io/stream/stringstream.cpp b/src/io/stream/stringstream.cpp new file mode 100644 index 0000000..0259869 --- /dev/null +++ b/src/io/stream/stringstream.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Our base String stream classes. We implement these to + * be based on Glib::ustring + * + * Authors: + * Bob Jamison + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "stringstream.h" + +namespace Inkscape +{ +namespace IO +{ + + +//######################################################################### +//# S T R I N G I N P U T S T R E A M +//######################################################################### + + +/** + * + */ +StringInputStream::StringInputStream(Glib::ustring &sourceString) + : buffer(sourceString) +{ + position = 0; +} + +/** + * + */ +StringInputStream::~StringInputStream() += default; + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int StringInputStream::available() +{ + return buffer.size() - position; +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void StringInputStream::close() +{ +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int StringInputStream::get() +{ + if (position >= (int)buffer.size()) + return -1; + int ch = (int) buffer[position++]; + return ch; +} + + + + +//######################################################################### +//# S T R I N G O U T P U T S T R E A M +//######################################################################### + +/** + * + */ +StringOutputStream::StringOutputStream() += default; + +/** + * + */ +StringOutputStream::~StringOutputStream() += default; + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void StringOutputStream::close() +{ +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void StringOutputStream::flush() +{ + //nothing to do +} + +/** + * Writes the specified byte to this output stream. + */ +int StringOutputStream::put(char ch) +{ + buffer.push_back(ch); + return 1; +} + + +} // namespace IO +} // namespace Inkscape + + +//######################################################################### +//# E N D O F F I L E +//######################################################################### diff --git a/src/io/stream/stringstream.h b/src/io/stream/stringstream.h new file mode 100644 index 0000000..3afb9a8 --- /dev/null +++ b/src/io/stream/stringstream.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __INKSCAPE_IO_STRINGSTREAM_H__ +#define __INKSCAPE_IO_STRINGSTREAM_H__ + +#include + +#include "inkscapestream.h" + + +namespace Inkscape +{ +namespace IO +{ + + +//######################################################################### +//# S T R I N G I N P U T S T R E A M +//######################################################################### + +/** + * This class is for reading character from a Glib::ustring + * + */ +class StringInputStream : public InputStream +{ + +public: + + StringInputStream(Glib::ustring &sourceString); + + ~StringInputStream() override; + + int available() override; + + void close() override; + + int get() override; + +private: + + Glib::ustring &buffer; + + long position; + +}; // class StringInputStream + + + + +//######################################################################### +//# S T R I N G O U T P U T S T R E A M +//######################################################################### + +/** + * This class is for sending a stream to a Glib::ustring + * + */ +class StringOutputStream : public OutputStream +{ + +public: + + StringOutputStream(); + + ~StringOutputStream() override; + + void close() override; + + void flush() override; + + int put(char ch) override; + + virtual Glib::ustring &getString() + { return buffer; } + + virtual void clear() + { buffer = ""; } + +private: + + Glib::ustring buffer; + + +}; // class StringOutputStream + + + + + + + +} // namespace IO +} // namespace Inkscape + + + +#endif /* __INKSCAPE_IO_STRINGSTREAM_H__ */ diff --git a/src/io/stream/uristream.cpp b/src/io/stream/uristream.cpp new file mode 100644 index 0000000..1596919 --- /dev/null +++ b/src/io/stream/uristream.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Our base String stream classes. We implement these to + * be based on Glib::ustring + * + * Authors: + * Bob Jamison + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "uristream.h" +#include "io/sys.h" +#include +#include + + +namespace Inkscape +{ +namespace IO +{ + +/* + * URI scheme types + */ +#define SCHEME_NONE 0 +#define SCHEME_FILE 1 +#define SCHEME_DATA 2 + +/* + * A temporary modification of Jon Cruz's portable fopen(). + * Simplified a bit, since we will always use binary +*/ + +#define FILE_READ 1 +#define FILE_WRITE 2 + +static FILE *fopen_utf8name( char const *utf8name, int mode ) +{ + FILE *fp = nullptr; + if (!utf8name) + { + return nullptr; + } + if (mode!=FILE_READ && mode!=FILE_WRITE) + { + return nullptr; + } + +#ifndef _WIN32 + gchar *filename = g_filename_from_utf8( utf8name, -1, nullptr, nullptr, nullptr ); + if ( filename ) { + if (mode == FILE_READ) + fp = std::fopen(filename, "rb"); + else + fp = std::fopen(filename, "wb"); + g_free(filename); + } +#else + { + gunichar2 *wideName = g_utf8_to_utf16( utf8name, -1, NULL, NULL, NULL ); + if ( wideName ) { + if (mode == FILE_READ) + fp = _wfopen( (wchar_t*)wideName, L"rb" ); + else + fp = _wfopen( (wchar_t*)wideName, L"wb" ); + g_free( wideName ); + } else { + gchar *safe = Inkscape::IO::sanitizeString(utf8name); + g_message("Unable to convert filename from UTF-8 to UTF-16 [%s]", safe); + g_free(safe); + } + } +#endif + + return fp; +} + + + +//######################################################################### +//# F I L E I N P U T S T R E A M +//######################################################################### + + +/** + * + */ +FileInputStream::FileInputStream(FILE *source) + : inf(source) +{ + if (!inf) { + Glib::ustring err = "FileInputStream passed NULL"; + throw StreamException(err); + } +} + +/** + * + */ +FileInputStream::~FileInputStream() +{ + close(); +} + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int FileInputStream::available() +{ + return 0; +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void FileInputStream::close() +{ + if (!inf) + return; + fflush(inf); + fclose(inf); + inf=nullptr; +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int FileInputStream::get() +{ + int retVal = -1; + if (!inf || feof(inf)) + { + retVal = -1; + } + else + { + retVal = fgetc(inf); + } + + return retVal; +} + + + + +//######################################################################### +//# F I L E O U T P U T S T R E A M +//######################################################################### + +FileOutputStream::FileOutputStream(FILE *fp) + : ownsFile(false) + , outf(fp) +{ + if (!outf) { + Glib::ustring err = "FileOutputStream given null file "; + throw StreamException(err); + } +} + +/** + * + */ +FileOutputStream::~FileOutputStream() +{ + close(); +} + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void FileOutputStream::close() +{ + if (!outf) + return; + fflush(outf); + if ( ownsFile ) + fclose(outf); + outf=nullptr; +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void FileOutputStream::flush() +{ + if (!outf) + return; + fflush(outf); +} + +/** + * Writes the specified byte to this output stream. + */ +int FileOutputStream::put(char ch) +{ + unsigned char uch; + + if (!outf) + return -1; + uch = (unsigned char)(ch & 0xff); + if (fputc(uch, outf) == EOF) { + Glib::ustring err = "ERROR writing to file "; + throw StreamException(err); + } + + return 1; +} + + + + + +} // namespace IO +} // namespace Inkscape + + +//######################################################################### +//# E N D O F F I L E +//######################################################################### + +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/stream/uristream.h b/src/io/stream/uristream.h new file mode 100644 index 0000000..f0544d8 --- /dev/null +++ b/src/io/stream/uristream.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_IO_URISTREAM_H +#define SEEN_INKSCAPE_IO_URISTREAM_H +/** + * @file + * This should be the only way that we provide sources/sinks + * to any input/output stream. + */ +/* + * Authors: + * Bob Jamison + * + * Copyright (C) 2004 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "object/uri.h" + +#include "inkscapestream.h" + + +namespace Inkscape +{ + +class URI; + +namespace IO +{ + +//######################################################################### +//# F I L E I N P U T S T R E A M +//######################################################################### + +/** + * This class is for receiving a stream of data from a file + */ +class FileInputStream : public InputStream +{ + +public: + FileInputStream(FILE *source); + + ~FileInputStream() override; + + int available() override; + + void close() override; + + int get() override; + +private: + FILE *inf; //for file: uris + +}; // class FileInputStream + + + + +//######################################################################### +//# F I L E O U T P U T S T R E A M +//######################################################################### + +/** + * This class is for sending a stream to a destination file + */ +class FileOutputStream : public OutputStream +{ + +public: + + FileOutputStream(FILE *fp); + + ~FileOutputStream() override; + + void close() override; + + void flush() override; + + int put(char ch) override; + +private: + + bool ownsFile; + + FILE *outf; //for file: uris + +}; // class FileOutputStream + + + + + +} // namespace IO +} // namespace Inkscape + + +#endif // SEEN_INKSCAPE_IO_URISTREAM_H + +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/io/stream/xsltstream.cpp b/src/io/stream/xsltstream.cpp new file mode 100644 index 0000000..882db30 --- /dev/null +++ b/src/io/stream/xsltstream.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * XSL Transforming input and output classes + * + * Authors: + * Bob Jamison + * + * Copyright (C) 2004-2008 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "xsltstream.h" +#include "stringstream.h" +#include + + + + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# X S L T S T Y L E S H E E T +//######################################################################### + +/** + * + */ +XsltStyleSheet::XsltStyleSheet(InputStream &xsltSource) + + : stylesheet(nullptr) +{ + if (!read(xsltSource)) { + throw StreamException("read failed"); + } +} + +/** + * + */ +XsltStyleSheet::XsltStyleSheet() + : stylesheet(nullptr) +{ +} + + + +/** + * + */ +bool XsltStyleSheet::read(InputStream &xsltSource) +{ + StringOutputStream outs; + pipeStream(xsltSource, outs); + std::string strBuf = outs.getString().raw(); + xmlDocPtr doc = xmlParseMemory(strBuf.c_str(), strBuf.size()); + stylesheet = xsltParseStylesheetDoc(doc); + //following not necessary. handled by xsltFreeStylesheet(stylesheet); + //xmlFreeDoc(doc); + if (!stylesheet) + return false; + return true; +} + + +/** + * + */ +XsltStyleSheet::~XsltStyleSheet() +{ + if (stylesheet) + xsltFreeStylesheet(stylesheet); +} + + + +//######################################################################### +//# X S L T I N P U T S T R E A M +//######################################################################### + + +/** + * + */ +XsltInputStream::XsltInputStream(InputStream &xmlSource, XsltStyleSheet &sheet) + : BasicInputStream(xmlSource), stylesheet(sheet) +{ + //Load the data + StringOutputStream outs; + pipeStream(source, outs); + std::string strBuf = outs.getString().raw(); + + //Do the processing + const char *params[1]; + params[0] = nullptr; + xmlDocPtr srcDoc = xmlParseMemory(strBuf.c_str(), strBuf.size()); + xmlDocPtr resDoc = xsltApplyStylesheet(stylesheet.stylesheet, srcDoc, params); + xmlDocDumpFormatMemory(resDoc, &outbuf, &outsize, 1); + outpos = 0; + + //Free our mem + xmlFreeDoc(resDoc); + xmlFreeDoc(srcDoc); +} + +/** + * + */ +XsltInputStream::~XsltInputStream() +{ + xmlFree(outbuf); +} + +/** + * Returns the number of bytes that can be read (or skipped over) from + * this input stream without blocking by the next caller of a method for + * this input stream. + */ +int XsltInputStream::available() +{ + return outsize - outpos; +} + + +/** + * Closes this input stream and releases any system resources + * associated with the stream. + */ +void XsltInputStream::close() +{ + closed = true; +} + +/** + * Reads the next byte of data from the input stream. -1 if EOF + */ +int XsltInputStream::get() +{ + if (closed) + return -1; + if (outpos >= outsize) + return -1; + int ch = (int) outbuf[outpos++]; + return ch; +} + + + + + + +//######################################################################### +//# X S L T O U T P U T S T R E A M +//######################################################################### + +/** + * + */ +XsltOutputStream::XsltOutputStream(OutputStream &dest, XsltStyleSheet &sheet) + : BasicOutputStream(dest), stylesheet(sheet) +{ + flushed = false; +} + +/** + * + */ +XsltOutputStream::~XsltOutputStream() +{ + //do not automatically close +} + +/** + * Closes this output stream and releases any system resources + * associated with this stream. + */ +void XsltOutputStream::close() +{ + flush(); + destination.close(); +} + +/** + * Flushes this output stream and forces any buffered output + * bytes to be written out. + */ +void XsltOutputStream::flush() +{ + if (flushed) + { + destination.flush(); + return; + } + + //Do the processing + xmlChar *resbuf; + int resSize; + const char *params[1]; + params[0] = nullptr; + xmlDocPtr srcDoc = xmlParseMemory(outbuf.raw().c_str(), outbuf.size()); + xmlDocPtr resDoc = xsltApplyStylesheet(stylesheet.stylesheet, srcDoc, params); + xmlDocDumpFormatMemory(resDoc, &resbuf, &resSize, 1); + /* + xmlErrorPtr err = xmlGetLastError(); + if (err) + { + throw StreamException(err->message); + } + */ + + for (int i=0 ; i + * + * Copyright (C) 2004-2008 Inkscape.org + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "inkscapestream.h" + +#include +#include + + +namespace Inkscape +{ +namespace IO +{ + +//######################################################################### +//# X S L T S T Y L E S H E E T +//######################################################################### +/** + * This is a container for reusing a loaded stylesheet + */ +class XsltStyleSheet +{ + +public: + + /** + * Constructor with loading + */ + XsltStyleSheet(InputStream &source); + + /** + * Simple constructor, no loading + */ + XsltStyleSheet(); + + /** + * Loader + */ + bool read(InputStream &source); + + /** + * Destructor + */ + virtual ~XsltStyleSheet(); + + xsltStylesheetPtr stylesheet; + + +}; // class XsltStyleSheet + + +//######################################################################### +//# X S L T I N P U T S T R E A M +//######################################################################### + +/** + * This class is for transforming stream input by a given stylesheet + */ +class XsltInputStream : public BasicInputStream +{ + +public: + + XsltInputStream(InputStream &xmlSource, XsltStyleSheet &stylesheet); + + ~XsltInputStream() override; + + int available() override; + + void close() override; + + int get() override; + + +private: + + XsltStyleSheet &stylesheet; + + xmlChar *outbuf; + int outsize; + int outpos; + +}; // class XsltInputStream + + + + +//######################################################################### +//# X S L T O U T P U T S T R E A M +//######################################################################### + +/** + * This class is for transforming stream output by a given stylesheet + */ +class XsltOutputStream : public BasicOutputStream +{ + +public: + + XsltOutputStream(OutputStream &destination, XsltStyleSheet &stylesheet); + + ~XsltOutputStream() override; + + void close() override; + + void flush() override; + + int put(char ch) override; + +private: + + XsltStyleSheet &stylesheet; + + Glib::ustring outbuf; + + bool flushed; + +}; // class XsltOutputStream + + + +} // namespace IO +} // namespace Inkscape + + +#endif /* __INKSCAPE_IO_XSLTSTREAM_H__ */ diff --git a/src/io/sys.cpp b/src/io/sys.cpp new file mode 100644 index 0000000..ee1956f --- /dev/null +++ b/src/io/sys.cpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* + * System abstraction utility routines + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include + +#include "preferences.h" +#include "sys.h" + +//#define INK_DUMP_FILENAME_CONV 1 +#undef INK_DUMP_FILENAME_CONV + +//#define INK_DUMP_FOPEN 1 +#undef INK_DUMP_FOPEN + +void dump_str(gchar const *str, gchar const *prefix); +void dump_ustr(Glib::ustring const &ustr); + +extern guint update_in_progress; + + +void Inkscape::IO::dump_fopen_call( char const *utf8name, char const *id ) +{ +#ifdef INK_DUMP_FOPEN + Glib::ustring str; + for ( int i = 0; utf8name[i]; i++ ) + { + if ( utf8name[i] == '\\' ) + { + str += "\\\\"; + } + else if ( (utf8name[i] >= 0x20) && ((0x0ff & utf8name[i]) <= 0x7f) ) + { + str += utf8name[i]; + } + else + { + gchar tmp[32]; + g_snprintf( tmp, sizeof(tmp), "\\x%02x", (0x0ff & utf8name[i]) ); + str += tmp; + } + } + g_message( "fopen call %s for [%s]", id, str.data() ); +#else + (void)utf8name; + (void)id; +#endif +} + +FILE *Inkscape::IO::fopen_utf8name( char const *utf8name, char const *mode ) +{ + FILE* fp = nullptr; + + if (Glib::ustring( utf8name ) == Glib::ustring("-")) { + // user requests to use pipes + + Glib::ustring how( mode ); + if ( how.find("w") != Glib::ustring::npos ) { +#ifdef _WIN32 + setmode(fileno(stdout), O_BINARY); +#endif + return stdout; + } else { + return stdin; + } + } + + gchar *filename = g_filename_from_utf8( utf8name, -1, nullptr, nullptr, nullptr ); + if ( filename ) + { + // ensure we open the file in binary mode (not needed in POSIX but doesn't hurt either) + Glib::ustring how( mode ); + if ( how.find("b") == Glib::ustring::npos ) + { + how.append("b"); + } + // when opening a file for writing: create parent directories if they don't exist already + if ( how.find("w") != Glib::ustring::npos ) + { + gchar *dirname = g_path_get_dirname(utf8name); + if (g_mkdir_with_parents(dirname, 0777)) { + g_warning("Could not create directory '%s'", dirname); + } + g_free(dirname); + } + fp = g_fopen(filename, how.c_str()); + g_free(filename); + filename = nullptr; + } + return fp; +} + + +int Inkscape::IO::mkdir_utf8name( char const *utf8name ) +{ + int retval = -1; + gchar *filename = g_filename_from_utf8( utf8name, -1, nullptr, nullptr, nullptr ); + if ( filename ) + { + retval = g_mkdir(filename, S_IRWXU | S_IRGRP | S_IXGRP); // The mode argument is ignored on Windows. + g_free(filename); + filename = nullptr; + } + return retval; +} + +bool Inkscape::IO::file_test( char const *utf8name, GFileTest test ) +{ + bool exists = false; + + // in case the file to check is a pipe it doesn't need to exist + if (g_strcmp0(utf8name, "-") == 0 && G_FILE_TEST_IS_REGULAR) + return true; + + if ( utf8name ) { + gchar *filename = nullptr; + if (utf8name && !g_utf8_validate(utf8name, -1, nullptr)) { + /* FIXME: Trying to guess whether or not a filename is already in utf8 is unreliable. + If any callers pass non-utf8 data (e.g. using g_get_home_dir), then change caller to + use simple g_file_test. Then add g_return_val_if_fail(g_utf_validate(...), false) + to beginning of this function. */ + filename = g_strdup(utf8name); + // Looks like g_get_home_dir isn't safe. + //g_warning("invalid UTF-8 detected internally. HUNT IT DOWN AND KILL IT!!!"); + } else { + filename = g_filename_from_utf8 ( utf8name, -1, nullptr, nullptr, nullptr ); + } + if ( filename ) { + exists = g_file_test (filename, test); + g_free(filename); + filename = nullptr; + } else { + g_warning( "Unable to convert filename in IO:file_test" ); + } + } + + return exists; +} + +bool Inkscape::IO::file_is_writable( char const *utf8name) +{ + bool success = true; + + if ( utf8name) { + gchar *filename = nullptr; + if (utf8name && !g_utf8_validate(utf8name, -1, nullptr)) { + /* FIXME: Trying to guess whether or not a filename is already in utf8 is unreliable. + If any callers pass non-utf8 data (e.g. using g_get_home_dir), then change caller to + use simple g_file_test. Then add g_return_val_if_fail(g_utf_validate(...), false) + to beginning of this function. */ + filename = g_strdup(utf8name); + // Looks like g_get_home_dir isn't safe. + //g_warning("invalid UTF-8 detected internally. HUNT IT DOWN AND KILL IT!!!"); + } else { + filename = g_filename_from_utf8 ( utf8name, -1, nullptr, nullptr, nullptr ); + } + if ( filename ) { + GStatBuf st; + if (g_file_test (filename, G_FILE_TEST_EXISTS)){ + if (g_lstat (filename, &st) == 0) { + success = ((st.st_mode & S_IWRITE) != 0); + } + } + g_free(filename); + filename = nullptr; + } else { + g_warning( "Unable to convert filename in IO:file_test" ); + } + } + + return success; +} + +/**Checks if directory of file exists, useful + * because inkscape doesn't create directories.*/ +bool Inkscape::IO::file_directory_exists( char const *utf8name ){ + bool exists = true; + + if ( utf8name) { + gchar *filename = nullptr; + if (utf8name && !g_utf8_validate(utf8name, -1, nullptr)) { + /* FIXME: Trying to guess whether or not a filename is already in utf8 is unreliable. + If any callers pass non-utf8 data (e.g. using g_get_home_dir), then change caller to + use simple g_file_test. Then add g_return_val_if_fail(g_utf_validate(...), false) + to beginning of this function. */ + filename = g_strdup(utf8name); + // Looks like g_get_home_dir isn't safe. + //g_warning("invalid UTF-8 detected internally. HUNT IT DOWN AND KILL IT!!!"); + } else { + filename = g_filename_from_utf8 ( utf8name, -1, nullptr, nullptr, nullptr ); + } + if ( filename ) { + gchar *dirname = g_path_get_dirname(filename); + exists = Inkscape::IO::file_test( dirname, G_FILE_TEST_EXISTS); + g_free(filename); + g_free(dirname); + filename = nullptr; + dirname = nullptr; + } else { + g_warning( "Unable to convert filename in IO:file_test" ); + } + } + + return exists; + +} + +/** Wrapper around g_dir_open, but taking a utf8name as first argument. */ +GDir * +Inkscape::IO::dir_open(gchar const *const utf8name, guint const flags, GError **const error) +{ + gchar *const opsys_name = g_filename_from_utf8(utf8name, -1, nullptr, nullptr, error); + if (opsys_name) { + GDir *ret = g_dir_open(opsys_name, flags, error); + g_free(opsys_name); + return ret; + } else { + return nullptr; + } +} + +/** + * Like g_dir_read_name, but returns a utf8name (which must be freed, unlike g_dir_read_name). + * + * N.B. Skips over any dir entries that fail to convert to utf8. + */ +gchar * +Inkscape::IO::dir_read_utf8name(GDir *dir) +{ + for (;;) { + gchar const *const opsys_name = g_dir_read_name(dir); + if (!opsys_name) { + return nullptr; + } + gchar *utf8_name = g_filename_to_utf8(opsys_name, -1, nullptr, nullptr, nullptr); + if (utf8_name) { + return utf8_name; + } + } +} + + +gchar* Inkscape::IO::locale_to_utf8_fallback( const gchar *opsysstring, + gssize len, + gsize *bytes_read, + gsize *bytes_written, + GError **error ) +{ + gchar *result = nullptr; + if ( opsysstring ) { + gchar *newFileName = g_locale_to_utf8( opsysstring, len, bytes_read, bytes_written, error ); + if ( newFileName ) { + if ( !g_utf8_validate(newFileName, -1, nullptr) ) { + g_warning( "input filename did not yield UTF-8" ); + g_free( newFileName ); + } else { + result = newFileName; + } + newFileName = nullptr; + } else if ( g_utf8_validate(opsysstring, -1, nullptr) ) { + // This *might* be a case that we want + // g_warning( "input failed filename->utf8, fell back to original" ); + // TODO handle cases when len >= 0 + result = g_strdup( opsysstring ); + } else { + gchar const *charset = nullptr; + g_get_charset(&charset); + g_warning( "input filename conversion failed for file with locale charset '%s'", charset ); + } + } + return result; +} + +void +Inkscape::IO::spawn_async_with_pipes( const std::string& working_directory, + const Glib::ArrayHandle& argv, + Glib::SpawnFlags flags, + const sigc::slot& child_setup, + Glib::Pid* child_pid, + int* standard_input, + int* standard_output, + int* standard_error) +{ + Glib::spawn_async_with_pipes(working_directory, + argv, + flags, + child_setup, + child_pid, + standard_input, + standard_output, + standard_error); +} + + +gchar* Inkscape::IO::sanitizeString( gchar const * str ) +{ + gchar *result = nullptr; + if ( str ) { + if ( g_utf8_validate(str, -1, nullptr) ) { + result = g_strdup(str); + } else { + guchar scratch[8]; + Glib::ustring buf; + guchar const *ptr = (guchar const*)str; + while ( *ptr ) + { + if ( *ptr == '\\' ) + { + buf.append("\\\\"); + } else if ( *ptr < 0x80 ) { + buf += (char)(*ptr); + } else { + g_snprintf((gchar*)scratch, sizeof(scratch), "\\x%02x", *ptr); + buf.append((const char*)scratch); + } + ptr++; + } + result = g_strdup(buf.c_str()); + } + } + return result; +} + +/* + * Returns the file extension of a path/filename + */ +Glib::ustring Inkscape::IO::get_file_extension(Glib::ustring path) +{ + Glib::ustring::size_type period_location = path.find_last_of("."); + return path.substr(period_location); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/io/sys.h b/src/io/sys.h new file mode 100644 index 0000000..f159ac6 --- /dev/null +++ b/src/io/sys.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SYS_H +#define SEEN_SYS_H + +/* + * System abstraction utility routines + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include + +/*##################### +## U T I L I T Y +#####################*/ + +namespace Inkscape { +namespace IO { + +void dump_fopen_call( char const *utf8name, char const *id ); + +FILE *fopen_utf8name( char const *utf8name, char const *mode ); + +int mkdir_utf8name( char const *utf8name ); + +bool file_test( char const *utf8name, GFileTest test ); + +bool file_directory_exists( char const *utf8name ); + +bool file_is_writable( char const *utf8name); + +GDir *dir_open(gchar const *utf8name, guint flags, GError **error); + +gchar *dir_read_utf8name(GDir *dir); + +gchar* locale_to_utf8_fallback( const gchar *opsysstring, + gssize len, + gsize *bytes_read, + gsize *bytes_written, + GError **error ); + +gchar* sanitizeString( gchar const * str ); + +void spawn_async_with_pipes (const std::string& working_directory, + const Glib::ArrayHandle& argv, + Glib::SpawnFlags flags, + const sigc::slot& child_setup, + Glib::Pid* child_pid, + int* standard_input, + int* standard_output, + int* standard_error); + +Glib::ustring get_file_extension(Glib::ustring path); + +} +} + + +#endif // SEEN_SYS_H diff --git a/src/knot-enums.h b/src/knot-enums.h new file mode 100644 index 0000000..47ae978 --- /dev/null +++ b/src/knot-enums.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_KNOT_ENUMS_H +#define SEEN_KNOT_ENUMS_H + +/** + * @file + * Some enums used by SPKnot and by related types \& functions. + */ +/* + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +enum SPKnotShapeType { + SP_KNOT_SHAPE_SQUARE, + SP_KNOT_SHAPE_DIAMOND, + SP_KNOT_SHAPE_CIRCLE, + SP_KNOT_SHAPE_TRIANGLE, + SP_KNOT_SHAPE_CROSS, + SP_KNOT_SHAPE_BITMAP, + SP_KNOT_SHAPE_IMAGE +}; + +enum SPKnotModeType { + SP_KNOT_MODE_COLOR, + SP_KNOT_MODE_XOR +}; + +enum SPKnotStateType { + SP_KNOT_STATE_NORMAL, + SP_KNOT_STATE_MOUSEOVER, + SP_KNOT_STATE_DRAGGING, + SP_KNOT_STATE_SELECTED, + SP_KNOT_STATE_HIDDEN +}; + +#define SP_KNOT_VISIBLE_STATES 4 + +enum { + SP_KNOT_VISIBLE = 1 << 0, + SP_KNOT_MOUSEOVER = 1 << 1, + SP_KNOT_DRAGGING = 1 << 2, + SP_KNOT_GRABBED = 1 << 3, + SP_KNOT_SELECTED = 1 << 4 +}; + + +#endif /* !SEEN_KNOT_ENUMS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/knot-holder-entity.cpp b/src/knot-holder-entity.cpp new file mode 100644 index 0000000..15edc33 --- /dev/null +++ b/src/knot-holder-entity.cpp @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * KnotHolderEntity definition. + * + * Authors: + * Mitsuru Oka + * Maximilian Albert + * Abhishek Sharma + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2001 Mitsuru Oka + * Copyright (C) 2004 Monash University + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "knot-holder-entity.h" + +#include "desktop.h" +#include "inkscape.h" +#include "knotholder.h" +#include "preferences.h" +#include "snap.h" +#include "style.h" + +#include "live_effects/effect.h" +#include "object/sp-hatch.h" +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-pattern.h" + + +int KnotHolderEntity::counter = 0; + +void KnotHolderEntity::create(SPDesktop *desktop, SPItem *item, KnotHolder *parent, Inkscape::ControlType type, + const gchar *tip, + SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + if (!desktop) { + desktop = SP_ACTIVE_DESKTOP; + } + knot = new SPKnot(desktop, tip); + + this->parent_holder = parent; + this->item = item; // TODO: remove the item either from here or from knotholder.cpp + this->desktop = desktop; + + my_counter = KnotHolderEntity::counter++; + + g_object_set(G_OBJECT(knot->item), "shape", shape, NULL); + g_object_set(G_OBJECT(knot->item), "mode", mode, NULL); + + // TODO base more appearance from this type instead of passing in arbitrary values. + knot->item->ctrlType = type; + + knot->fill [SP_KNOT_STATE_NORMAL] = color; + g_object_set (G_OBJECT(knot->item), "fill_color", color, NULL); + + update_knot(); + knot->show(); + + _mousedown_connection = knot->mousedown_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_mousedown_handler)); + _moved_connection = knot->moved_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_moved_handler)); + _click_connection = knot->click_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_clicked_handler)); + _ungrabbed_connection = knot->ungrabbed_signal.connect(sigc::mem_fun(*parent_holder, &KnotHolder::knot_ungrabbed_handler)); +} + + +KnotHolderEntity::~KnotHolderEntity() +{ + _mousedown_connection.disconnect(); + _moved_connection.disconnect(); + _click_connection.disconnect(); + _ungrabbed_connection.disconnect(); + + /* unref should call destroy */ + if (knot) { + //g_object_unref(knot); + knot_unref(knot); + } else { + // FIXME: This shouldn't occur. Perhaps it is caused by LPE PointParams being knotholder entities, too + // If so, it will likely be fixed with upcoming refactoring efforts. + g_return_if_fail(knot); + } +} + +void +KnotHolderEntity::update_knot() +{ + Geom::Point knot_pos(knot_get()); + if (knot_pos.isFinite()) { + Geom::Point dp(knot_pos * parent_holder->getEditTransform() * item->i2dt_affine()); + + _moved_connection.block(); + knot->setPosition(dp, SP_KNOT_STATE_NORMAL); + _moved_connection.unblock(); + } else { + // knot coords are non-finite, hide knot + knot->hide(); + } +} + +Geom::Point +KnotHolderEntity::snap_knot_position(Geom::Point const &p, guint state) +{ + if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held + return p; + } + + Geom::Affine const i2dt (parent_holder->getEditTransform() * item->i2dt_affine()); + Geom::Point s = p * i2dt; + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, item); + m.freeSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + return s * i2dt.inverse(); +} + +Geom::Point +KnotHolderEntity::snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, guint state) +{ + if (state & GDK_SHIFT_MASK) { // Don't snap when shift-key is held + return p; + } + + Geom::Affine const i2d (parent_holder->getEditTransform() * item->i2dt_affine()); + Geom::Point s = p * i2d; + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, item); + + // constrainedSnap() will first project the point p onto the constraint line and then try to snap along that line. + // This way the constraint is already enforced, no need to worry about that later on + Inkscape::Snapper::SnapConstraint transformed_constraint = Inkscape::Snapper::SnapConstraint(constraint.getPoint() * i2d, (constraint.getPoint() + constraint.getDirection()) * i2d - constraint.getPoint() * i2d); + m.constrainedSnapReturnByRef(s, Inkscape::SNAPSOURCE_NODE_HANDLE, transformed_constraint); + m.unSetup(); + + return s * i2d.inverse(); +} + +void +LPEKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + Inkscape::LivePathEffect::Effect *effect = _effect; + if (effect) { + effect->refresh_widgets = true; + effect->writeParamsToSVG(); + } +} + +/* Pattern manipulation */ + +/* TODO: this pattern manipulation is not able to handle general transformation matrices. Only matrices that are the result of a pure scale times a pure rotation. */ + +void +PatternKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used. + Geom::Point p_snapped = snap_knot_position(p, state); + + if ( state & GDK_CONTROL_MASK ) { + if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) { + p_snapped[Geom::Y] = origin[Geom::Y]; + } else { + p_snapped[Geom::X] = origin[Geom::X]; + } + } + + if (state) { + Geom::Point const q = p_snapped - knot_get(); + item->adjust_pattern(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + } + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static Geom::Point sp_pattern_knot_get(SPPattern const *pat, gdouble x, gdouble y) +{ + return Geom::Point(x, y) * pat->getTransform(); +} + +bool +PatternKnotHolderEntity::knot_missing() const +{ + SPPattern *pat = _pattern(); + return (pat == nullptr); +} + +SPPattern* +PatternKnotHolderEntity::_pattern() const +{ + return _fill ? SP_PATTERN(item->style->getFillPaintServer()) : SP_PATTERN(item->style->getStrokePaintServer()); +} + +Geom::Point +PatternKnotHolderEntityXY::knot_get() const +{ + SPPattern *pat = _pattern(); + return sp_pattern_knot_get(pat, 0, 0); +} + +Geom::Point +PatternKnotHolderEntityAngle::knot_get() const +{ + SPPattern *pat = _pattern(); + return sp_pattern_knot_get(pat, pat->width(), 0); +} + +Geom::Point +PatternKnotHolderEntityScale::knot_get() const +{ + SPPattern *pat = _pattern(); + return sp_pattern_knot_get(pat, pat->width(), pat->height()); +} + +void +PatternKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + SPPattern *pat = _fill ? SP_PATTERN(item->style->getFillPaintServer()) : SP_PATTERN(item->style->getStrokePaintServer()); + + // get the angle from pattern 0,0 to the cursor pos + Geom::Point transform_origin = sp_pattern_knot_get(pat, 0, 0); + gdouble theta = atan2(p - transform_origin); + gdouble theta_old = atan2(knot_get() - transform_origin); + + if ( state & GDK_CONTROL_MASK ) { + /* Snap theta */ + double snaps_radian = M_PI/snaps; + theta = std::round(theta/snaps_radian) * snaps_radian; + } + + Geom::Affine rot = Geom::Translate(-transform_origin) + * Geom::Rotate(theta - theta_old) + * Geom::Translate(transform_origin); + item->adjust_pattern(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +PatternKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + SPPattern *pat = _pattern(); + + // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used. + Geom::Point p_snapped = snap_knot_position(p, state); + + // Get the new scale from the position of the knotholder + Geom::Affine transform = pat->getTransform(); + Geom::Affine transform_inverse = transform.inverse(); + Geom::Point d = p_snapped * transform_inverse; + Geom::Point d_origin = origin * transform_inverse; + Geom::Point origin_dt; + gdouble pat_x = pat->width(); + gdouble pat_y = pat->height(); + if ( state & GDK_CONTROL_MASK ) { + // if ctrl is pressed: use 1:1 scaling + d = d_origin * (d.length() / d_origin.length()); + } + + Geom::Affine rot = Geom::Translate(-origin_dt) + * Geom::Scale(d.x() / pat_x, d.y() / pat_y) + * Geom::Translate(origin_dt) + * transform; + + item->adjust_pattern(rot, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/* Hatch manipulation */ +bool HatchKnotHolderEntity::knot_missing() const +{ + SPHatch *hatch = _hatch(); + return (hatch == nullptr); +} + +SPHatch *HatchKnotHolderEntity::_hatch() const +{ + return _fill ? SP_HATCH(item->style->getFillPaintServer()) : SP_HATCH(item->style->getStrokePaintServer()); +} + +static Geom::Point sp_hatch_knot_get(SPHatch const *hatch, gdouble x, gdouble y) +{ + return Geom::Point(x, y) * hatch->hatchTransform(); +} + +Geom::Point HatchKnotHolderEntityXY::knot_get() const +{ + SPHatch *hatch = _hatch(); + return sp_hatch_knot_get(hatch, 0, 0); +} + +Geom::Point HatchKnotHolderEntityAngle::knot_get() const +{ + SPHatch *hatch = _hatch(); + return sp_hatch_knot_get(hatch, hatch->pitch(), 0); +} + +Geom::Point HatchKnotHolderEntityScale::knot_get() const +{ + SPHatch *hatch = _hatch(); + return sp_hatch_knot_get(hatch, hatch->pitch(), hatch->pitch()); +} + +void HatchKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + Geom::Point p_snapped = snap_knot_position(p, state); + + if (state & GDK_CONTROL_MASK) { + if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) { + p_snapped[Geom::Y] = origin[Geom::Y]; + } else { + p_snapped[Geom::X] = origin[Geom::X]; + } + } + + if (state) { + Geom::Point const q = p_snapped - knot_get(); + item->adjust_hatch(Geom::Translate(q), false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + } + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void HatchKnotHolderEntityAngle::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + SPHatch *hatch = _hatch(); + + // get the angle from hatch 0,0 to the cursor pos + Geom::Point transform_origin = sp_hatch_knot_get(hatch, 0, 0); + gdouble theta = atan2(p - transform_origin); + gdouble theta_old = atan2(knot_get() - transform_origin); + + if (state & GDK_CONTROL_MASK) { + /* Snap theta */ + double snaps_radian = M_PI/snaps; + theta = std::round(theta/snaps_radian) * snaps_radian; + } + + Geom::Affine rot = + Geom::Translate(-transform_origin) * Geom::Rotate(theta - theta_old) * Geom::Translate(transform_origin); + item->adjust_hatch(rot, false, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void HatchKnotHolderEntityScale::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + SPHatch *hatch = _hatch(); + + // FIXME: this snapping should be done together with knowing whether control was pressed. + // If GDK_CONTROL_MASK, then constrained snapping should be used. + Geom::Point p_snapped = snap_knot_position(p, state); + + // Get the new scale from the position of the knotholder + Geom::Affine transform = hatch->hatchTransform(); + Geom::Affine transform_inverse = transform.inverse(); + Geom::Point d = p_snapped * transform_inverse; + Geom::Point d_origin = origin * transform_inverse; + Geom::Point origin_dt; + gdouble hatch_pitch = hatch->pitch(); + if (state & GDK_CONTROL_MASK) { + // if ctrl is pressed: use 1:1 scaling + d = d_origin * (d.length() / d_origin.length()); + } + + Geom::Affine scale = Geom::Translate(-origin_dt) * Geom::Scale(d.x() / hatch_pitch, d.y() / hatch_pitch) * + Geom::Translate(origin_dt) * transform; + + item->adjust_hatch(scale, true, _fill ? TRANSFORM_FILL : TRANSFORM_STROKE); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/* Filter manipulation */ +void FilterKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + // FIXME: this snapping should be done together with knowing whether control was pressed. If GDK_CONTROL_MASK, then constrained snapping should be used. + Geom::Point p_snapped = snap_knot_position(p, state); + + if ( state & GDK_CONTROL_MASK ) { + if (fabs((p - origin)[Geom::X]) > fabs((p - origin)[Geom::Y])) { + p_snapped[Geom::Y] = origin[Geom::Y]; + } else { + p_snapped[Geom::X] = origin[Geom::X]; + } + } + + if (state) { + SPFilter *filter = (item->style && item->style->filter.href) ? dynamic_cast(item->style->getFilter()) : nullptr; + if(!filter) return; + Geom::OptRect orig_bbox = item->visualBounds(); + std::unique_ptr new_bbox(_topleft ? new Geom::Rect(p,orig_bbox->max()) : new Geom::Rect(orig_bbox->min(), p)); + + if (!filter->width._set) { + filter->width.set(SVGLength::PERCENT, 1.2); + } + if (!filter->height._set) { + filter->height.set(SVGLength::PERCENT, 1.2); + } + if (!filter->x._set) { + filter->x.set(SVGLength::PERCENT, -0.1); + } + if (!filter->y._set) { + filter->y.set(SVGLength::PERCENT, -0.1); + } + + if(_topleft) { + float x_a = filter->width.computed; + float y_a = filter->height.computed; + filter->height.scale(new_bbox->height()/orig_bbox->height()); + filter->width.scale(new_bbox->width()/orig_bbox->width()); + float x_b = filter->width.computed; + float y_b = filter->height.computed; + filter->x.set(filter->x.unit, filter->x.computed + x_a - x_b); + filter->y.set(filter->y.unit, filter->y.computed + y_a - y_b); + } else { + filter->height.scale(new_bbox->height()/orig_bbox->height()); + filter->width.scale(new_bbox->width()/orig_bbox->width()); + } + filter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + //filter-> + + //item-> //adjust FER + } + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point FilterKnotHolderEntity::knot_get() const +{ + SPFilter *filter = (item->style && item->style->filter.href) ? dynamic_cast(item->style->getFilter()) : nullptr; + if(!filter) return Geom::Point(Geom::infinity(), Geom::infinity()); + Geom::OptRect r = item->visualBounds(); + if (_topleft) return Geom::Point(r->min()); + else return Geom::Point(r->max()); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/knot-holder-entity.h b/src/knot-holder-entity.h new file mode 100644 index 0000000..0a63bf7 --- /dev/null +++ b/src/knot-holder-entity.h @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_KNOT_HOLDER_ENTITY_H +#define SEEN_KNOT_HOLDER_ENTITY_H +/* + * Authors: + * Mitsuru Oka + * Maximilian Albert + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2001 Mitsuru Oka + * Copyright (C) 2004 Monash University + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +#include "display/sp-canvas-item.h" +#include "knot.h" +#include "snapper.h" + +class SPHatch; +class SPItem; +class SPKnot; +class SPDesktop; +class SPPattern; +class KnotHolder; + +namespace Inkscape { +namespace LivePathEffect { + class Effect; +} // namespace LivePathEffect +} // namespace Inkscape + +typedef void (* SPKnotHolderSetFunc) (SPItem *item, Geom::Point const &p, Geom::Point const &origin, unsigned int state); +typedef Geom::Point (* SPKnotHolderGetFunc) (SPItem *item); + +/** + * KnotHolderEntity definition. + */ +class KnotHolderEntity { +public: + KnotHolderEntity(): + knot(nullptr), + item(nullptr), + desktop(nullptr), + parent_holder(nullptr), + my_counter(0), + handler_id(0), + _click_handler_id(0), + _ungrab_handler_id(0) + {} + virtual ~KnotHolderEntity(); + + virtual void create(SPDesktop *desktop, SPItem *item, KnotHolder *parent, + Inkscape::ControlType type = Inkscape::CTRL_TYPE_UNKNOWN, + char const*tip = "", + SPKnotShapeType shape = SP_KNOT_SHAPE_DIAMOND, + SPKnotModeType mode = SP_KNOT_MODE_XOR, + guint32 color = 0xffffff00); + + /* the get/set/click handlers are virtual functions; each handler class for a knot + should be derived from KnotHolderEntity and override these functions */ + virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0; + virtual void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, unsigned int state) = 0; + virtual bool knot_missing() const { return false; } + virtual Geom::Point knot_get() const = 0; + virtual void knot_click(unsigned int /*state*/) {} + + void update_knot(); + +//private: + Geom::Point snap_knot_position(Geom::Point const &p, unsigned int state); + Geom::Point snap_knot_position_constrained(Geom::Point const &p, Inkscape::Snapper::SnapConstraint const &constraint, unsigned int state); + + SPKnot *knot; + SPItem *item; + SPDesktop *desktop; + + KnotHolder *parent_holder; + + int my_counter; + static int counter; + + /** Connection to \a knot's "moved" signal. */ + unsigned int handler_id; + /** Connection to \a knot's "clicked" signal. */ + unsigned int _click_handler_id; + /** Connection to \a knot's "ungrabbed" signal. */ + unsigned int _ungrab_handler_id; + +private: + sigc::connection _mousedown_connection; + sigc::connection _moved_connection; + sigc::connection _click_connection; + sigc::connection _ungrabbed_connection; +}; + +// derived KnotHolderEntity class for LPEs +class LPEKnotHolderEntity : public KnotHolderEntity { +public: + LPEKnotHolderEntity(Inkscape::LivePathEffect::Effect *effect) : _effect(effect) {}; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; +protected: + Inkscape::LivePathEffect::Effect *_effect; +}; + +/* pattern manipulation */ + +class PatternKnotHolderEntity : public KnotHolderEntity { + public: + PatternKnotHolderEntity(bool fill) : KnotHolderEntity(), _fill(fill) {} + bool knot_missing() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{}; + + protected: + // true if the entity tracks fill, false for stroke + bool _fill; + SPPattern *_pattern() const; +}; + +class PatternKnotHolderEntityXY : public PatternKnotHolderEntity { +public: + PatternKnotHolderEntityXY(bool fill) : PatternKnotHolderEntity(fill) {} + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class PatternKnotHolderEntityAngle : public PatternKnotHolderEntity { +public: + PatternKnotHolderEntityAngle(bool fill) : PatternKnotHolderEntity(fill) {} + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class PatternKnotHolderEntityScale : public PatternKnotHolderEntity { +public: + PatternKnotHolderEntityScale(bool fill) : PatternKnotHolderEntity(fill) {} + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +/* Hatch manipulation */ +class HatchKnotHolderEntity : public KnotHolderEntity { + public: + HatchKnotHolderEntity(bool fill) + : KnotHolderEntity() + , _fill(fill) + { + } + bool knot_missing() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override{}; + + protected: + // true if the entity tracks fill, false for stroke + bool _fill; + SPHatch *_hatch() const; +}; + +class HatchKnotHolderEntityXY : public HatchKnotHolderEntity { + public: + HatchKnotHolderEntityXY(bool fill) + : HatchKnotHolderEntity(fill) + { + } + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class HatchKnotHolderEntityAngle : public HatchKnotHolderEntity { + public: + HatchKnotHolderEntityAngle(bool fill) + : HatchKnotHolderEntity(fill) + { + } + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class HatchKnotHolderEntityScale : public HatchKnotHolderEntity { + public: + HatchKnotHolderEntityScale(bool fill) + : HatchKnotHolderEntity(fill) + { + } + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + + +/* Filter manipulation */ +class FilterKnotHolderEntity : public KnotHolderEntity { + public: + FilterKnotHolderEntity(bool topleft) : KnotHolderEntity(), _topleft(topleft) {} + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +private: + bool _topleft; // true for topleft point, false for bottomright +}; + +#endif /* !SEEN_KNOT_HOLDER_ENTITY_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/knot-ptr.cpp b/src/knot-ptr.cpp new file mode 100644 index 0000000..8e275ac --- /dev/null +++ b/src/knot-ptr.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include +#include +#include "knot-ptr.h" + +static std::list deleted_knots; + +void knot_deleted_callback(void* knot) { + if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) == deleted_knots.end()) { + deleted_knots.push_back(knot); + } +} + +void knot_created_callback(void* knot) { + std::list::iterator it = std::find(deleted_knots.begin(), deleted_knots.end(), knot); + if (it != deleted_knots.end()) { + deleted_knots.erase(it); + } +} + +void check_if_knot_deleted(void* knot) { + if (std::find(deleted_knots.begin(), deleted_knots.end(), knot) != deleted_knots.end()) { + g_warning("Accessed knot after it was freed at %p", knot); + } +} diff --git a/src/knot-ptr.h b/src/knot-ptr.h new file mode 100644 index 0000000..5141822 --- /dev/null +++ b/src/knot-ptr.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef KNOT_PTR_DETECTOR +#define KNOT_PTR_DETECTOR + +void knot_deleted_callback(void* knot); +void knot_created_callback(void* knot); +void check_if_knot_deleted(void* knot); + +#endif diff --git a/src/knot.cpp b/src/knot.cpp new file mode 100644 index 0000000..5dc22cb --- /dev/null +++ b/src/knot.cpp @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SPKnot implementation + * + * Authors: + * Lauris Kaplinski + * bulia byak + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif +#include +#include +#include "display/sodipodi-ctrl.h" +#include "desktop.h" + +#include "knot.h" +#include "knot-ptr.h" +#include "document.h" +#include "document-undo.h" +#include "message-stack.h" +#include "message-context.h" +#include "ui/tools/node-tool.h" +#include + +using Inkscape::DocumentUndo; + +#define KNOT_EVENT_MASK (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | \ + GDK_POINTER_MOTION_MASK | \ + GDK_POINTER_MOTION_HINT_MASK | \ + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK) + +const gchar *nograbenv = getenv("INKSCAPE_NO_GRAB"); +static bool nograb = (nograbenv && *nograbenv && (*nograbenv != '0')); + +static bool grabbed = false; +static bool moved = false; + +static gint xp = 0, yp = 0; // where drag started +static gint tolerance = 0; +static bool within_tolerance = false; + +static bool transform_escaped = false; // true iff resize or rotate was cancelled by esc. + +void knot_ref(SPKnot* knot) { + knot->ref_count++; +} + +void knot_unref(SPKnot* knot) { + if (--knot->ref_count < 1) { + delete knot; + } +} + + +static int sp_knot_handler(SPCanvasItem *item, GdkEvent *event, SPKnot *knot); + +SPKnot::SPKnot(SPDesktop *desktop, gchar const *tip) + : ref_count(1) +{ + this->desktop = nullptr; + this->item = nullptr; + this->owner = nullptr; + this->flags = 0; + + this->size = 8; + this->angle = 0; + this->pos = Geom::Point(0, 0); + this->grabbed_rel_pos = Geom::Point(0, 0); + this->anchor = SP_ANCHOR_CENTER; + this->shape = SP_KNOT_SHAPE_SQUARE; + this->mode = SP_KNOT_MODE_XOR; + this->tip = nullptr; + this->_event_handler_id = 0; + this->pressure = 0; + + this->fill[SP_KNOT_STATE_NORMAL] = 0xffffff00; + this->fill[SP_KNOT_STATE_MOUSEOVER] = 0xff0000ff; + this->fill[SP_KNOT_STATE_DRAGGING] = 0xff0000ff; + this->fill[SP_KNOT_STATE_SELECTED] = 0x0000ffff; + + this->stroke[SP_KNOT_STATE_NORMAL] = 0x01000000; + this->stroke[SP_KNOT_STATE_MOUSEOVER] = 0x01000000; + this->stroke[SP_KNOT_STATE_DRAGGING] = 0x01000000; + this->stroke[SP_KNOT_STATE_SELECTED] = 0x01000000; + + this->image[SP_KNOT_STATE_NORMAL] = nullptr; + this->image[SP_KNOT_STATE_MOUSEOVER] = nullptr; + this->image[SP_KNOT_STATE_DRAGGING] = nullptr; + this->image[SP_KNOT_STATE_SELECTED] = nullptr; + + this->cursor[SP_KNOT_STATE_NORMAL] = nullptr; + this->cursor[SP_KNOT_STATE_MOUSEOVER] = nullptr; + this->cursor[SP_KNOT_STATE_DRAGGING] = nullptr; + this->cursor[SP_KNOT_STATE_SELECTED] = nullptr; + + this->saved_cursor = nullptr; + this->pixbuf = nullptr; + + + this->desktop = desktop; + this->flags = SP_KNOT_VISIBLE; + + if (tip) { + this->tip = g_strdup (tip); + } + + this->item = sp_canvas_item_new(desktop->getControls(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 9, + "angle", 0.0, + "filled", TRUE, + "fill_color", 0xffffff00, + "stroked", TRUE, + "stroke_color", 0x01000000, + "mode", SP_KNOT_MODE_XOR, + NULL); + + this->_event_handler_id = g_signal_connect(G_OBJECT(this->item), "event", + G_CALLBACK(sp_knot_handler), this); + knot_created_callback(this); +} + +SPKnot::~SPKnot() { + auto display = gdk_display_get_default(); + auto seat = gdk_display_get_default_seat(display); + auto device = gdk_seat_get_pointer(seat); + + if ((this->flags & SP_KNOT_GRABBED) && gdk_display_device_is_grabbed(display, device)) { + // This happens e.g. when deleting a node in node tool while dragging it + gdk_seat_ungrab(seat); + } + + if (this->_event_handler_id > 0) { + g_signal_handler_disconnect(G_OBJECT (this->item), this->_event_handler_id); + this->_event_handler_id = 0; + } + + if (this->item) { + sp_canvas_item_destroy(this->item); + this->item = nullptr; + } + + for (auto & i : this->cursor) { + if (i) { + g_object_unref(i); + i = nullptr; + } + } + + if (this->tip) { + g_free(this->tip); + this->tip = nullptr; + } + + // FIXME: cannot snap to destroyed knot (lp:1309050) + //sp_event_context_discard_delayed_snap_event(this->desktop->event_context); + knot_deleted_callback(this); +} + +void SPKnot::startDragging(Geom::Point const &p, gint x, gint y, guint32 etime) { + // save drag origin + xp = x; + yp = y; + within_tolerance = true; + + this->grabbed_rel_pos = p - this->pos; + this->drag_origin = this->pos; + + if (!nograb) { + sp_canvas_item_grab(this->item, KNOT_EVENT_MASK, this->cursor[SP_KNOT_STATE_DRAGGING], etime); + } + this->setFlag(SP_KNOT_GRABBED, TRUE); + + grabbed = TRUE; +} + +void SPKnot::selectKnot(bool select){ + setFlag(SP_KNOT_SELECTED, select); +} + +/** + * Called to handle events on knots. + */ +static int sp_knot_handler(SPCanvasItem */*item*/, GdkEvent *event, SPKnot *knot) +{ + g_assert(knot != nullptr); + g_assert(SP_IS_KNOT(knot)); + + /* Run client universal event handler, if present */ + bool consumed = knot->event_signal.emit(knot, event); + + if (consumed) { + return true; + } + + bool key_press_event_unconsumed = FALSE; + + knot_ref(knot); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (event->button.button == 1) { + knot->doubleclicked_signal.emit(knot, event->button.state); + + grabbed = FALSE; + moved = FALSE; + consumed = TRUE; + } + break; + case GDK_BUTTON_PRESS: + if ((event->button.button == 1) && knot->desktop && knot->desktop->event_context && !knot->desktop->event_context->space_panning) { + Geom::Point const p = knot->desktop->w2d(Geom::Point(event->button.x, event->button.y)); + knot->startDragging(p, (gint) event->button.x, (gint) event->button.y, event->button.time); + knot->mousedown_signal.emit(knot, event->button.state); + consumed = TRUE; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && knot->desktop && knot->desktop->event_context && !knot->desktop->event_context->space_panning) { + // If we have any pending snap event, then invoke it now + if (knot->desktop->event_context->_delayed_snap_event) { + sp_event_context_snap_watchdog_callback(knot->desktop->event_context->_delayed_snap_event); + } + sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); + knot->pressure = 0; + + if (transform_escaped) { + transform_escaped = false; + consumed = TRUE; + } else { + knot->setFlag(SP_KNOT_GRABBED, FALSE); + + if (!nograb) { + sp_canvas_item_ungrab(knot->item); + } + + if (moved) { + knot->setFlag(SP_KNOT_DRAGGING, FALSE); + knot->ungrabbed_signal.emit(knot, event->button.state); + } else { + knot->click_signal.emit(knot, event->button.state); + } + + grabbed = FALSE; + moved = FALSE; + consumed = TRUE; + } + } + Inkscape::UI::Tools::sp_update_helperpath(); + break; + case GDK_MOTION_NOTIFY: + if (!(event->motion.state & GDK_BUTTON1_MASK) && knot->flags & SP_KNOT_DRAGGING) { + // If we have any pending snap event, then invoke it now + if (knot->desktop->event_context->_delayed_snap_event) { + sp_event_context_snap_watchdog_callback(knot->desktop->event_context->_delayed_snap_event); + } + sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); + knot->pressure = 0; + + if (transform_escaped) { + transform_escaped = false; + consumed = TRUE; + } else { + knot->setFlag(SP_KNOT_GRABBED, FALSE); + + if (!nograb) { + sp_canvas_item_ungrab(knot->item); + } + + if (moved) { + knot->setFlag(SP_KNOT_DRAGGING, FALSE); + knot->ungrabbed_signal.emit(knot, event->motion.state); + } else { + knot->click_signal.emit(knot, event->motion.state); + } + + grabbed = FALSE; + moved = FALSE; + consumed = TRUE; + Inkscape::UI::Tools::sp_update_helperpath(); + } + } else if (grabbed && knot->desktop && knot->desktop->event_context && + !knot->desktop->event_context->space_panning) { + consumed = TRUE; + + if ( within_tolerance + && ( abs( (gint) event->motion.x - xp ) < tolerance ) + && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &knot->pressure)) { + knot->pressure = CLAMP (knot->pressure, 0, 1); + } else { + knot->pressure = 0.5; + } + + if (!moved) { + knot->setFlag(SP_KNOT_DRAGGING, TRUE); + knot->grabbed_signal.emit(knot, event->button.state); + } + + sp_event_context_snap_delay_handler(knot->desktop->event_context, nullptr, knot, (GdkEventMotion *)event, Inkscape::UI::Tools::DelayedSnapEvent::KNOT_HANDLER); + sp_knot_handler_request_position(event, knot); + moved = TRUE; + } + break; + case GDK_ENTER_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, TRUE); + knot->setFlag(SP_KNOT_GRABBED, FALSE); + + if (knot->tip && knot->desktop && knot->desktop->event_context) { + knot->desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, knot->tip); + } + + grabbed = FALSE; + moved = FALSE; + consumed = TRUE; + break; + case GDK_LEAVE_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, FALSE); + knot->setFlag(SP_KNOT_GRABBED, FALSE); + + if (knot->tip && knot->desktop && knot->desktop->event_context) { + knot->desktop->event_context->defaultMessageContext()->clear(); + } + + grabbed = FALSE; + moved = FALSE; + consumed = TRUE; + break; + case GDK_KEY_PRESS: // keybindings for knot + switch (Inkscape::UI::Tools::get_latin_keyval(&event->key)) { + case GDK_KEY_Escape: + knot->setFlag(SP_KNOT_GRABBED, FALSE); + + if (!nograb) { + sp_canvas_item_ungrab(knot->item); + } + + if (moved) { + knot->setFlag(SP_KNOT_DRAGGING, FALSE); + + knot->ungrabbed_signal.emit(knot, event->button.state); + + DocumentUndo::undo(knot->desktop->getDocument()); + knot->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Node or handle drag canceled.")); + transform_escaped = true; + consumed = TRUE; + } + + grabbed = FALSE; + moved = FALSE; + + sp_event_context_discard_delayed_snap_event(knot->desktop->event_context); + break; + default: + consumed = FALSE; + key_press_event_unconsumed = TRUE; + break; + } + break; + default: + break; + } + + knot_unref(knot); + + if (key_press_event_unconsumed) { + return false; // e.g. in case "%" was pressed to toggle snapping, or Q for quick zoom (while dragging a handle) + } else { + return consumed || grabbed; + } +} + +void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt = knot->desktop->w2d(motion_w); + Geom::Point p = motion_dt - knot->grabbed_rel_pos; + + knot->requestPosition(p, event->motion.state); + knot->desktop->scroll_to_point (motion_dt); + knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different! + + if (event->motion.state & GDK_BUTTON1_MASK) { + Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); + } +} + +void SPKnot::show() { + this->setFlag(SP_KNOT_VISIBLE, TRUE); +} + +void SPKnot::hide() { + this->setFlag(SP_KNOT_VISIBLE, FALSE); +} + +void SPKnot::requestPosition(Geom::Point const &p, guint state) { + bool done = this->request_signal.emit(this, &const_cast(p), state); + + /* If user did not complete, we simply move knot to new position */ + if (!done) { + this->setPosition(p, state); + } +} + +void SPKnot::setPosition(Geom::Point const &p, guint state) { + this->pos = p; + + if (this->item) { + SP_CTRL(this->item)->moveto(p); + } + + this->moved_signal.emit(this, p, state); +} + +void SPKnot::moveto(Geom::Point const &p) { + this->pos = p; + + if (this->item) { + SP_CTRL(this->item)->moveto(p); + } +} + +Geom::Point SPKnot::position() const { + return this->pos; +} + +void SPKnot::setFlag(guint flag, bool set) { + if (set) { + this->flags |= flag; + } else { + this->flags &= ~flag; + } + + switch (flag) { + case SP_KNOT_VISIBLE: + if (set) { + sp_canvas_item_show(this->item); + } else { + sp_canvas_item_hide(this->item); + } + break; + case SP_KNOT_MOUSEOVER: + case SP_KNOT_DRAGGING: + case SP_KNOT_SELECTED: + this->_setCtrlState(); + break; + case SP_KNOT_GRABBED: + break; + default: + g_assert_not_reached(); + break; + } +} + +void SPKnot::updateCtrl() { + if (!this->item) { + return; + } + + g_object_set(this->item, "shape", this->shape, NULL); + g_object_set(this->item, "mode", this->mode, NULL); + g_object_set(this->item, "size", this->size, NULL); + g_object_set(this->item, "angle", this->angle, NULL); + g_object_set(this->item, "anchor", this->anchor, NULL); + + if (this->pixbuf) { + g_object_set(this->item, "pixbuf", this->pixbuf, NULL); + } + + this->_setCtrlState(); +} + +void SPKnot::_setCtrlState() { + int state = SP_KNOT_STATE_NORMAL; + + if (this->flags & SP_KNOT_DRAGGING) { + state = SP_KNOT_STATE_DRAGGING; + } else if (this->flags & SP_KNOT_MOUSEOVER) { + state = SP_KNOT_STATE_MOUSEOVER; + } else if (this->flags & SP_KNOT_SELECTED) { + state = SP_KNOT_STATE_SELECTED; + } + g_object_set(this->item, "fill_color", this->fill[state], NULL); + g_object_set(this->item, "stroke_color", this->stroke[state], NULL); +} + + +void SPKnot::setSize(guint i) { + size = i; +} + +void SPKnot::setShape(guint i) { + shape = (SPKnotShapeType) i; +} + +void SPKnot::setAnchor(guint i) { + anchor = (SPAnchorType) i; +} + +void SPKnot::setMode(guint i) { + mode = (SPKnotModeType) i; +} + +void SPKnot::setPixbuf(gpointer p) { + pixbuf = p; +} + +void SPKnot::setAngle(double i) { + angle = i; +} + +void SPKnot::setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) { + fill[SP_KNOT_STATE_NORMAL] = normal; + fill[SP_KNOT_STATE_MOUSEOVER] = mouseover; + fill[SP_KNOT_STATE_DRAGGING] = dragging; + fill[SP_KNOT_STATE_SELECTED] = selected; +} + +void SPKnot::setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected) { + stroke[SP_KNOT_STATE_NORMAL] = normal; + stroke[SP_KNOT_STATE_MOUSEOVER] = mouseover; + stroke[SP_KNOT_STATE_DRAGGING] = dragging; + stroke[SP_KNOT_STATE_SELECTED] = selected; +} + +void SPKnot::setImage(guchar* normal, guchar* mouseover, guchar* dragging, guchar* selected) { + image[SP_KNOT_STATE_NORMAL] = normal; + image[SP_KNOT_STATE_MOUSEOVER] = mouseover; + image[SP_KNOT_STATE_DRAGGING] = dragging; + image[SP_KNOT_STATE_SELECTED] = selected; +} + +void SPKnot::setCursor(GdkCursor* normal, GdkCursor* mouseover, GdkCursor* dragging, GdkCursor* selected) { + if (cursor[SP_KNOT_STATE_NORMAL]) { + g_object_unref(cursor[SP_KNOT_STATE_NORMAL]); + } + + cursor[SP_KNOT_STATE_NORMAL] = normal; + + if (normal) { + g_object_ref(normal); + } + + if (cursor[SP_KNOT_STATE_MOUSEOVER]) { + g_object_unref(cursor[SP_KNOT_STATE_MOUSEOVER]); + } + + cursor[SP_KNOT_STATE_MOUSEOVER] = mouseover; + + if (mouseover) { + g_object_ref(mouseover); + } + + if (cursor[SP_KNOT_STATE_DRAGGING]) { + g_object_unref(cursor[SP_KNOT_STATE_DRAGGING]); + } + + cursor[SP_KNOT_STATE_DRAGGING] = dragging; + + if (dragging) { + g_object_ref(dragging); + } + + if (cursor[SP_KNOT_STATE_SELECTED]) { + g_object_unref(cursor[SP_KNOT_STATE_SELECTED]); + } + + cursor[SP_KNOT_STATE_SELECTED] = selected; + + if (selected) { + g_object_ref(selected); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/knot.h b/src/knot.h new file mode 100644 index 0000000..2242149 --- /dev/null +++ b/src/knot.h @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_KNOT_H +#define SEEN_SP_KNOT_H + +/** \file + * Declarations for SPKnot: Desktop-bound visual control object. + */ +/* + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include + +#include "knot-enums.h" +#include "enums.h" + +class SPDesktop; +class SPItem; +struct SPCanvasItem; + +typedef struct _GdkCursor GdkCursor; +typedef union _GdkEvent GdkEvent; +typedef unsigned int guint32; + +#define SP_KNOT(obj) (dynamic_cast(static_cast(obj))) +#define SP_IS_KNOT(obj) (dynamic_cast(static_cast(obj)) != NULL) + + +/** + * Desktop-bound visual control object. + * + * A knot is a draggable object, with callbacks to change something by + * dragging it, visuably represented by a canvas item (mostly square). + */ +class SPKnot { +public: + SPKnot(SPDesktop *desktop, char const *tip); + virtual ~SPKnot(); + + SPKnot(SPKnot const&) = delete; + SPKnot& operator=(SPKnot const&) = delete; + + int ref_count; // FIXME encapsulation + + SPDesktop *desktop; /**< Desktop we are on. */ + SPCanvasItem *item; /**< Our CanvasItem. */ + SPItem *owner; /**< Optional Owner Item */ + unsigned int flags; + + unsigned int size; /**< Always square. */ + double angle; /**< Angle of mesh handle. */ + Geom::Point pos; /**< Our desktop coordinates. */ + Geom::Point grabbed_rel_pos; /**< Grabbed relative position. */ + Geom::Point drag_origin; /**< Origin of drag. */ + SPAnchorType anchor; /**< Anchor. */ + + SPKnotShapeType shape; /**< Shape type. */ + SPKnotModeType mode; + + guint32 fill[SP_KNOT_VISIBLE_STATES]; + guint32 stroke[SP_KNOT_VISIBLE_STATES]; + unsigned char *image[SP_KNOT_VISIBLE_STATES]; + + GdkCursor *cursor[SP_KNOT_VISIBLE_STATES]; + + GdkCursor *saved_cursor; + void* pixbuf; + + char *tip; + + unsigned long _event_handler_id; + + double pressure; /**< The tablet pen pressure when the knot is being dragged. */ + + // FIXME: signals should NOT need to emit the object they came from, the callee should + // be able to figure that out + sigc::signal click_signal; + sigc::signal doubleclicked_signal; + sigc::signal mousedown_signal; + sigc::signal grabbed_signal; + sigc::signal ungrabbed_signal; + sigc::signal moved_signal; + sigc::signal event_signal; + + sigc::signal request_signal; + + + //TODO: all the members above should eventualle become private, accessible via setters/getters + void setSize(unsigned int i); + void setShape(unsigned int i); + void setAnchor(unsigned int i); + void setMode(unsigned int i); + void setPixbuf(void* p); + void setAngle(double i); + + void setFill(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected); + void setStroke(guint32 normal, guint32 mouseover, guint32 dragging, guint32 selected); + void setImage(unsigned char* normal, unsigned char* mouseover, unsigned char* dragging, unsigned char* selected); + + void setCursor(GdkCursor* normal, GdkCursor* mouseover, GdkCursor* dragging, GdkCursor* selected); + + /** + * Show knot on its canvas. + */ + void show(); + + /** + * Hide knot on its canvas. + */ + void hide(); + + /** + * Set flag in knot, with side effects. + */ + void setFlag(unsigned int flag, bool set); + + /** + * Update knot's pixbuf and set its control state. + */ + void updateCtrl(); + + /** + * Request or set new position for knot. + */ + void requestPosition(Geom::Point const &pos, unsigned int state); + + /** + * Update knot for dragging and tell canvas an item was grabbed. + */ + void startDragging(Geom::Point const &p, int x, int y, guint32 etime); + + /** + * Move knot to new position and emits "moved" signal. + */ + void setPosition(Geom::Point const &p, unsigned int state); + + /** + * Move knot to new position, without emitting a MOVED signal. + */ + void moveto(Geom::Point const &p); + /** + * Select knot. + */ + void selectKnot(bool select); + + /** + * Returns position of knot. + */ + Geom::Point position() const; + +private: + /** + * Set knot control state (dragging/mouseover/normal). + */ + void _setCtrlState(); +}; + +void knot_ref(SPKnot* knot); +void knot_unref(SPKnot* knot); + +#define SP_KNOT_IS_VISIBLE(k) ((k->flags & SP_KNOT_VISIBLE) != 0) +#define SP_KNOT_IS_SELECTED(k) ((k->flags & SP_KNOT_SELECTED) != 0) +#define SP_KNOT_IS_MOUSEOVER(k) ((k->flags & SP_KNOT_MOUSEOVER) != 0) +#define SP_KNOT_IS_DRAGGING(k) ((k->flags & SP_KNOT_DRAGGING) != 0) +#define SP_KNOT_IS_GRABBED(k) ((k->flags & SP_KNOT_GRABBED) != 0) + +void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot); + +#endif // SEEN_SP_KNOT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/knotholder.cpp b/src/knotholder.cpp new file mode 100644 index 0000000..f86a6eb --- /dev/null +++ b/src/knotholder.cpp @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Container for SPKnot visual handles. + * + * Authors: + * Mitsuru Oka + * bulia byak + * Maximilian Albert + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2001-2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "knotholder.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "knot-holder-entity.h" +#include "knot.h" +#include "verbs.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" + +#include "object/box3d.h" +#include "object/sp-ellipse.h" +#include "object/sp-hatch.h" +#include "object/sp-offset.h" +#include "object/sp-pattern.h" +#include "object/sp-rect.h" +#include "object/sp-shape.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "style.h" + +#include "display/sp-canvas.h" + +#include "ui/control-manager.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/tools/arc-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/rect-tool.h" +#include "ui/tools/spiral-tool.h" +#include "ui/tools/tweak-tool.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using Inkscape::ControlManager; +using Inkscape::DocumentUndo; + +class SPDesktop; + +KnotHolder::KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + desktop(desktop), + item(item), + //XML Tree being used directly for item->getRepr() while it shouldn't be... + repr(item ? item->getRepr() : nullptr), + entity(), + sizeUpdatedConn(), + released(relhandler), + local_change(FALSE), + dragging(false), + _edit_transform(Geom::identity()) +{ + if (!desktop || !item) { + g_print ("Error! Throw an exception, please!\n"); + } + + sp_object_ref(item); + + sizeUpdatedConn = ControlManager::getManager().connectCtrlSizeChanged(sigc::mem_fun(*this, &KnotHolder::updateControlSizes)); +} + +KnotHolder::~KnotHolder() { + sp_object_unref(item); + + for (auto & i : entity) + { + delete i; + i = NULL; + } + entity.clear(); // is this necessary? + sizeUpdatedConn.disconnect(); +} + +void +KnotHolder::setEditTransform(Geom::Affine edit_transform) +{ + _edit_transform = edit_transform; +} + +void KnotHolder::updateControlSizes() +{ + ControlManager &mgr = ControlManager::getManager(); + + for (auto e : entity) { + mgr.updateItem(e->knot->item); + } +} + +void KnotHolder::update_knots() +{ + for (auto e = entity.begin(); e != entity.end(); ) { + // check if pattern was removed without deleting the knot + if ((*e)->knot_missing()) { + delete (*e); + e = entity.erase(e); + } else { + (*e)->update_knot(); + ++e; + } + } +} + +/** + * Returns true if at least one of the KnotHolderEntities has the mouse hovering above it. + */ +bool KnotHolder::knot_mouseover() const { + for (auto i : entity) { + const SPKnot *knot = i->knot; + + if (knot && (knot->flags & SP_KNOT_MOUSEOVER)) { + return true; + } + } + + return false; +} + +void +KnotHolder::knot_mousedown_handler(SPKnot *knot, guint state) +{ + if (!(state & GDK_SHIFT_MASK)) { + unselect_knots(); + } + for(auto e : this->entity) { + if (!(state & GDK_SHIFT_MASK)) { + e->knot->selectKnot(false); + } + if (e->knot == knot) { + if (!(e->knot->flags & SP_KNOT_SELECTED) || !(state & GDK_SHIFT_MASK)){ + e->knot->selectKnot(true); + } else { + e->knot->selectKnot(false); + } + } + } +} + +void +KnotHolder::knot_clicked_handler(SPKnot *knot, guint state) +{ + SPItem *saved_item = this->item; + + for(auto e : this->entity) { + if (e->knot == knot) + // no need to test whether knot_click exists since it's virtual now + e->knot_click(state); + } + + { + SPShape *savedShape = dynamic_cast(saved_item); + if (savedShape) { + savedShape->set_shape(); + } + } + + this->update_knots(); + + unsigned int object_verb = SP_VERB_NONE; + + // TODO extract duplicated blocks; + if (dynamic_cast(saved_item)) { + object_verb = SP_VERB_CONTEXT_RECT; + } else if (dynamic_cast(saved_item)) { + object_verb = SP_VERB_CONTEXT_3DBOX; + } else if (dynamic_cast(saved_item)) { + object_verb = SP_VERB_CONTEXT_ARC; + } else if (dynamic_cast(saved_item)) { + object_verb = SP_VERB_CONTEXT_STAR; + } else if (dynamic_cast(saved_item)) { + object_verb = SP_VERB_CONTEXT_SPIRAL; + } else { + SPOffset *offset = dynamic_cast(saved_item); + if (offset) { + if (offset->sourceHref) { + object_verb = SP_VERB_SELECTION_LINKED_OFFSET; + } else { + object_verb = SP_VERB_SELECTION_DYNAMIC_OFFSET; + } + } + } + + // for drag, this is done by ungrabbed_handler, but for click we must do it here + + if (saved_item) { //increasingly aggressive sanity checks + if (saved_item->document) { + // enum is unsigned so can't be less than SP_VERB_INVALID + if (object_verb <= SP_VERB_LAST) { + DocumentUndo::done(saved_item->document, object_verb, + _("Change handle")); + } + } + } // else { abort(); } +} + +void +KnotHolder::transform_selected(Geom::Affine transform){ + for (auto & i : entity) { + SPKnot *knot = i->knot; + if (knot->flags & SP_KNOT_SELECTED) { + knot_moved_handler(knot, knot->pos * transform , 0); + knot->selectKnot(true); + } + } +} + +void +KnotHolder::unselect_knots(){ + if (tools_isactive(desktop, TOOLS_NODES)) { + Inkscape::UI::Tools::NodeTool *nt = static_cast(desktop->event_context); + if (nt) { + for(auto i=nt->_shape_editors.begin();i!=nt->_shape_editors.end();++i){ + Inkscape::UI::ShapeEditor * shape_editor = i->second; + if (shape_editor && shape_editor->has_knotholder()) { + KnotHolder * knotholder = shape_editor->knotholder; + if (knotholder) { + for(auto e : knotholder->entity) { + if (e->knot->flags & SP_KNOT_SELECTED) { + e->knot->selectKnot(false); + } + } + } + } + } + } + } +} + +void +KnotHolder::knot_moved_handler(SPKnot *knot, Geom::Point const &p, guint state) +{ + if (this->dragging == false) { + this->dragging = true; + } + + // this was a local change and the knotholder does not need to be recreated: + this->local_change = TRUE; + + for(auto e : this->entity) { + if (e->knot == knot) { + Geom::Point const q = p * item->i2dt_affine().inverse() * _edit_transform.inverse(); + e->knot_set(q, e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state); + break; + } + } + + SPShape *shape = dynamic_cast(item); + if (shape) { + shape->set_shape(); + } + + this->update_knots(); +} + +void +KnotHolder::knot_ungrabbed_handler(SPKnot *knot, guint state) +{ + this->dragging = false; + + if (this->released) { + this->released(this->item); + } else { + // if a point is dragged while not selected, it should select itself, + // even if it was just unselected in the mousedown event handler. + if (!(knot->flags & SP_KNOT_SELECTED)) { + knot->selectKnot(true); + } else { + for(auto e : this->entity) { + if (e->knot == knot) { + e->knot_ungrabbed(e->knot->position(), e->knot->drag_origin * item->i2dt_affine().inverse() * _edit_transform.inverse(), state); + break; + } + } + } + + SPObject *object = (SPObject *) this->item; + + // Caution: this call involves a screen update, which may process events, and as a + // result the knotholder may be destructed. So, after the updateRepr, we cannot use any + // fields of this knotholder (such as this->item), but only values we have saved beforehand + // (such as object). + object->updateRepr(); + + /* do cleanup tasks (e.g., for LPE items write the parameter values + * that were changed by dragging the handle to SVG) + */ + SPLPEItem *lpeItem = dynamic_cast(object); + if (lpeItem) { + // This writes all parameters to SVG. Is this sufficiently efficient or should we only + // write the ones that were changed? + Inkscape::LivePathEffect::Effect *lpe = lpeItem->getCurrentLPE(); + if (lpe) { + LivePathEffectObject *lpeobj = lpe->getLPEObj(); + lpeobj->updateRepr(); + } + } + + SPFilter *filter = (object->style) ? dynamic_cast(object->style->getFilter()) : nullptr; + if (filter) { + filter->updateRepr(); + } + + unsigned int object_verb = SP_VERB_NONE; + + // TODO extract duplicated blocks: + if (dynamic_cast(object)) { + object_verb = SP_VERB_CONTEXT_RECT; + } else if (dynamic_cast(object)) { + object_verb = SP_VERB_CONTEXT_3DBOX; + } else if (dynamic_cast(object)) { + object_verb = SP_VERB_CONTEXT_ARC; + } else if (dynamic_cast(object)) { + object_verb = SP_VERB_CONTEXT_STAR; + } else if (dynamic_cast(object)) { + object_verb = SP_VERB_CONTEXT_SPIRAL; + } else { + SPOffset *offset = dynamic_cast(object); + if (offset) { + if (offset->sourceHref) { + object_verb = SP_VERB_SELECTION_LINKED_OFFSET; + } else { + object_verb = SP_VERB_SELECTION_DYNAMIC_OFFSET; + } + } + } + DocumentUndo::done(object->document, object_verb, _("Move handle")); + } +} + +void KnotHolder::add(KnotHolderEntity *e) +{ + // g_message("Adding a knot at %p", e); + entity.push_back(e); + updateControlSizes(); +} + +void KnotHolder::add_pattern_knotholder() +{ + if ((item->style->fill.isPaintserver()) && dynamic_cast(item->style->getFillPaintServer())) { + PatternKnotHolderEntityXY *entity_xy = new PatternKnotHolderEntityXY(true); + PatternKnotHolderEntityAngle *entity_angle = new PatternKnotHolderEntityAngle(true); + PatternKnotHolderEntityScale *entity_scale = new PatternKnotHolderEntityScale(true); + entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + // TRANSLATORS: This refers to the pattern that's inside the object + _("Move the pattern fill inside the object"), SP_KNOT_SHAPE_CROSS); + + entity_scale->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Scale the pattern fill; uniformly if with Ctrl"), SP_KNOT_SHAPE_SQUARE, + SP_KNOT_MODE_XOR); + + entity_angle->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Rotate the pattern fill; with Ctrl to snap angle"), SP_KNOT_SHAPE_CIRCLE, + SP_KNOT_MODE_XOR); + + entity.push_back(entity_xy); + entity.push_back(entity_angle); + entity.push_back(entity_scale); + } + + if ((item->style->stroke.isPaintserver()) && dynamic_cast(item->style->getStrokePaintServer())) { + PatternKnotHolderEntityXY *entity_xy = new PatternKnotHolderEntityXY(false); + PatternKnotHolderEntityAngle *entity_angle = new PatternKnotHolderEntityAngle(false); + PatternKnotHolderEntityScale *entity_scale = new PatternKnotHolderEntityScale(false); + entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + // TRANSLATORS: This refers to the pattern that's inside the object + _("Move the pattern stroke inside the object"), SP_KNOT_SHAPE_CROSS); + + entity_scale->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Scale the pattern stroke; uniformly if with Ctrl"), SP_KNOT_SHAPE_SQUARE, + SP_KNOT_MODE_XOR); + + entity_angle->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Rotate the pattern stroke; with Ctrl to snap angle"), + SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR); + + entity.push_back(entity_xy); + entity.push_back(entity_angle); + entity.push_back(entity_scale); + } + updateControlSizes(); +} + +void KnotHolder::add_hatch_knotholder() +{ + if ((item->style->fill.isPaintserver()) && dynamic_cast(item->style->getFillPaintServer())) { + HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(true); + HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(true); + HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(true); + entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + // TRANSLATORS: This refers to the hatch that's inside the object + _("Move the hatch fill inside the object"), SP_KNOT_SHAPE_CROSS); + + entity_scale->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Scale the hatch fill; uniformly if with Ctrl"), SP_KNOT_SHAPE_SQUARE, + SP_KNOT_MODE_XOR); + + entity_angle->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Rotate the hatch fill; with Ctrl to snap angle"), SP_KNOT_SHAPE_CIRCLE, + SP_KNOT_MODE_XOR); + + entity.push_back(entity_xy); + entity.push_back(entity_angle); + entity.push_back(entity_scale); + } + + if ((item->style->stroke.isPaintserver()) && dynamic_cast(item->style->getStrokePaintServer())) { + HatchKnotHolderEntityXY *entity_xy = new HatchKnotHolderEntityXY(false); + HatchKnotHolderEntityAngle *entity_angle = new HatchKnotHolderEntityAngle(false); + HatchKnotHolderEntityScale *entity_scale = new HatchKnotHolderEntityScale(false); + entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + // TRANSLATORS: This refers to the pattern that's inside the object + _("Move the hatch stroke inside the object"), SP_KNOT_SHAPE_CROSS); + + entity_scale->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Scale the hatch stroke; uniformly if with Ctrl"), SP_KNOT_SHAPE_SQUARE, + SP_KNOT_MODE_XOR); + + entity_angle->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Rotate the hatch stroke; with Ctrl to snap angle"), SP_KNOT_SHAPE_CIRCLE, + SP_KNOT_MODE_XOR); + + entity.push_back(entity_xy); + entity.push_back(entity_angle); + entity.push_back(entity_scale); + } + updateControlSizes(); +} + +void KnotHolder::add_filter_knotholder() { + FilterKnotHolderEntity *entity_tl = new FilterKnotHolderEntity(true); + FilterKnotHolderEntity *entity_br = new FilterKnotHolderEntity(false); + entity_tl->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Resize the filter effect region"), SP_KNOT_SHAPE_DIAMOND); + entity_br->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Resize the filter effect region"), SP_KNOT_SHAPE_DIAMOND); + entity.push_back(entity_tl); + entity.push_back(entity_br); + updateControlSizes(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/knotholder.h b/src/knotholder.h new file mode 100644 index 0000000..f3951b7 --- /dev/null +++ b/src/knotholder.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_KNOTHOLDER_H +#define SEEN_SP_KNOTHOLDER_H + +/* + * KnotHolder - Hold SPKnot list and manage signals + * + * Author: + * Mitsuru Oka + * Maximilian Albert + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2001 Mitsuru Oka + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/forward.h> +#include <2geom/affine.h> +#include +#include + +namespace Inkscape { +namespace UI { +class ShapeEditor; +} +namespace XML { +class Node; +} +namespace LivePathEffect { +class PowerStrokePointArrayParamKnotHolderEntity; +class SatellitesArrayParam; +class FilletChamferKnotHolderEntity; +} +} + +class KnotHolderEntity; +class SPItem; +class SPDesktop; +class SPKnot; + +/* fixme: Think how to make callbacks most sensitive (Lauris) */ +typedef void (* SPKnotHolderReleasedFunc) (SPItem *item); + +class KnotHolder { +public: + KnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + virtual ~KnotHolder(); + + KnotHolder() = delete; // declared but not defined + + void update_knots(); + void unselect_knots(); + void knot_mousedown_handler(SPKnot *knot, unsigned int state); + void knot_moved_handler(SPKnot *knot, Geom::Point const &p, unsigned int state); + void knot_clicked_handler(SPKnot *knot, unsigned int state); + void knot_ungrabbed_handler(SPKnot *knot, unsigned int state); + void transform_selected(Geom::Affine transform); + void add(KnotHolderEntity *e); + + void add_pattern_knotholder(); + void add_hatch_knotholder(); + void add_filter_knotholder(); + + void setEditTransform(Geom::Affine edit_transform); + Geom::Affine getEditTransform() const { return _edit_transform; } + + bool knot_mouseover() const; + + friend class Inkscape::UI::ShapeEditor; // FIXME why? + friend class Inkscape::LivePathEffect::SatellitesArrayParam; // why? + friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; // why? + friend class Inkscape::LivePathEffect::FilletChamferKnotHolderEntity; // why? + +protected: + + void updateControlSizes(); + + SPDesktop *desktop; + SPItem *item; // TODO: Remove this and keep the actual item (e.g., SPRect etc.) in the item-specific knotholders + Inkscape::XML::Node *repr; ///< repr of the item, for setting and releasing listeners. + std::list entity; + + sigc::connection sizeUpdatedConn; + + SPKnotHolderReleasedFunc released; + + bool local_change; ///< if true, no need to recreate knotholder if repr was changed. + + bool dragging; + + Geom::Affine _edit_transform; +}; + +/** +void knot_clicked_handler(SPKnot *knot, guint state, gpointer data); +void knot_moved_handler(SPKnot *knot, Geom::Point const *p, guint state, gpointer data); +void knot_ungrabbed_handler(SPKnot *knot, unsigned int state, KnotHolder *kh); +**/ + +#endif // SEEN_SP_KNOTHOLDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-fns.cpp b/src/layer-fns.cpp new file mode 100644 index 0000000..81c722d --- /dev/null +++ b/src/layer-fns.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::SelectionDescriber - shows messages describing selection + * + * Authors: + * MenTaLguY + * Jon A. Cruz + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document.h" +#include "layer-fns.h" + +#include "object/sp-item-group.h" + +#include "util/find-last-if.h" + +#include "xml/repr.h" + +// TODO move the documentation comments into the .h file + +namespace Inkscape { + +namespace { + +static bool is_layer(SPObject &object) { + return SP_IS_GROUP(&object) && + SP_GROUP(&object)->layerMode() == SPGroup::LAYER; +} + +/** Finds the next sibling layer for a \a layer + * + * @returns NULL if there are no further layers under a parent + */ +static SPObject *next_sibling_layer(SPObject *layer) { + if (layer->parent == nullptr) { + return nullptr; + } + SPObject::ChildrenList &list = layer->parent->children; + auto l = std::find_if(++list.iterator_to(*layer), list.end(), &is_layer); + return l != list.end() ? &*l : nullptr; +} + +/** Finds the previous sibling layer for a \a layer + * + * @returns NULL if there are no further layers under a parent + */ +static SPObject *previous_sibling_layer(SPObject *layer) { + using Inkscape::Algorithms::find_last_if; + + SPObject::ChildrenList &list = layer->parent->children; + auto l = find_last_if(list.begin(), list.iterator_to(*layer), &is_layer); + return l != list.iterator_to(*layer) ? &*(l) : nullptr; +} + +/** Finds the first child of a \a layer + * + * @returns the layer itself if layer has no sublayers + */ +static SPObject *first_descendant_layer(SPObject *layer) { + while (true) { + auto first_descendant = std::find_if(layer->children.begin(), layer->children.end(), &is_layer); + if (first_descendant == layer->children.end()) { + break; + } + layer = &*first_descendant; + } + + return layer; +} + +/** Finds the last (topmost) child of a \a layer + * + * @returns NULL if layer has no sublayers + */ +static SPObject *last_child_layer(SPObject *layer) { + using Inkscape::Algorithms::find_last_if; + + auto l = find_last_if(layer->children.begin(), layer->children.end(), &is_layer); + return l != layer->children.end() ? &*l : nullptr; +} + +static SPObject *last_elder_layer(SPObject *root, SPObject *layer) { + using Inkscape::Algorithms::find_last_if; + SPObject *result = nullptr; + + while ( layer != root ) { + SPObject *sibling(previous_sibling_layer(layer)); + if (sibling) { + result = sibling; + break; + } + layer = layer->parent; + } + + return result; +} + +} + +/** Finds the next layer under \a root, relative to \a layer in + * depth-first order. + * + * @returns NULL if there are no further layers under \a root + */ +SPObject *next_layer(SPObject *root, SPObject *layer) { + g_return_val_if_fail(layer != nullptr, NULL); + SPObject *result = nullptr; + + SPObject *sibling = next_sibling_layer(layer); + if (sibling) { + result = first_descendant_layer(sibling); + } else if ( layer->parent != root ) { + result = layer->parent; + } + + return result; +} + + +/** Finds the previous layer under \a root, relative to \a layer in + * depth-first order. + * + * @returns NULL if there are no prior layers under \a root. + */ +SPObject *previous_layer(SPObject *root, SPObject *layer) { + using Inkscape::Algorithms::find_last_if; + + g_return_val_if_fail(layer != nullptr, NULL); + SPObject *result = nullptr; + + SPObject *child = last_child_layer(layer); + if (child) { + result = child; + } else if ( layer != root ) { + SPObject *sibling = previous_sibling_layer(layer); + if (sibling) { + result = sibling; + } else { + result = last_elder_layer(root, layer->parent); + } + } + + return result; +} + +/** +* Creates a new layer. Advances to the next layer id indicated + * by the string "layerNN", then creates a new group object of + * that id with attribute inkscape:groupmode='layer', and finally + * appends the new group object to \a root after object \a layer. + * + * \pre \a root should be either \a layer or an ancestor of it + */ +SPObject *create_layer(SPObject *root, SPObject *layer, LayerRelativePosition position) { + SPDocument *document = root->document; + + static int layer_suffix=1; + gchar *id=nullptr; + do { + g_free(id); + id = g_strdup_printf("layer%d", layer_suffix++); + } while (document->getObjectById(id)); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:g"); + repr->setAttribute("inkscape:groupmode", "layer"); + repr->setAttribute("id", id); + g_free(id); + + if ( LPOS_CHILD == position ) { + root = layer; + SPObject *child_layer = Inkscape::last_child_layer(layer); + if ( nullptr != child_layer ) { + layer = child_layer; + } + } + + if ( root == layer ) { + root->getRepr()->appendChild(repr); + } else { + Inkscape::XML::Node *layer_repr = layer->getRepr(); + layer_repr->parent()->addChild(repr, layer_repr); + + if ( LPOS_BELOW == position ) { + SP_ITEM(document->getObjectByRepr(repr))->lowerOne(); + } + } + + return document->getObjectByRepr(repr); +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-fns.h b/src/layer-fns.h new file mode 100644 index 0000000..1b9cd7e --- /dev/null +++ b/src/layer-fns.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * assorted functions related to layers + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_LAYER_FNS_H +#define SEEN_INKSCAPE_LAYER_FNS_H + +class SPObject; + +namespace Inkscape { + +enum LayerRelativePosition { + LPOS_ABOVE, + LPOS_BELOW, + LPOS_CHILD, +}; + +SPObject *create_layer(SPObject *root, SPObject *layer, LayerRelativePosition position); + +SPObject *next_layer(SPObject *root, SPObject *layer); + +SPObject *previous_layer(SPObject *root, SPObject *layer); + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-manager.cpp b/src/layer-manager.cpp new file mode 100644 index 0000000..58dcf83 --- /dev/null +++ b/src/layer-manager.cpp @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::LayerManager - a view of a document's layers, relative + * to a particular desktop + * + * Copyright 2006 MenTaLguY + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include + + +#include "desktop.h" +#include "document.h" +#include "gc-finalized.h" +#include "layer-manager.h" +#include "selection.h" + +#include "inkgc/gc-managed.h" + +#include "object/sp-item-group.h" + +#include "xml/node-observer.h" + +namespace Inkscape { + + +using Inkscape::XML::Node; + +class LayerManager::LayerWatcher : public Inkscape::XML::NodeObserver { +public: + LayerWatcher(LayerManager* mgr, SPObject* obj, sigc::connection c) : + _mgr(mgr), + _obj(obj), + _connection(c), + _lockedAttr(g_quark_from_string("sodipodi:insensitive")), + _labelAttr(g_quark_from_string("inkscape:label")) + {} + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override {} + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override {} + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override {} + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &/*node*/, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + if ( name == _lockedAttr || name == _labelAttr ) { + if ( _mgr && _obj ) { + _mgr->_objectModified( _obj, 0 ); + } + } + } + + LayerManager* _mgr; + SPObject* _obj; + sigc::connection _connection; + GQuark _lockedAttr; + GQuark _labelAttr; +}; + +/* +namespace { + +Util::ptr_shared stringify_node(Node const &node); + +Util::ptr_shared stringify_obj(SPObject const &obj) { + gchar *string; + + if (obj.id) { + string = g_strdup_printf("SPObject(%p)=%s repr(%p)", &obj, obj.id, obj.repr); + } else { + string = g_strdup_printf("SPObject(%p) repr(%p)", &obj, obj.repr); + } + + Util::ptr_shared result=Util::share_string(string); + g_free(string); + return result; + +} + +typedef Debug::SimpleEvent DebugLayer; + +class DebugLayerNote : public DebugLayer { +public: + DebugLayerNote(Util::ptr_shared descr) + : DebugLayer(Util::share_static_string("layer-note")) + { + _addProperty("descr", descr); + } +}; + +class DebugLayerRebuild : public DebugLayer { +public: + DebugLayerRebuild() + : DebugLayer(Util::share_static_string("rebuild-layers")) + { + } +}; + +class DebugLayerObj : public DebugLayer { +public: + DebugLayerObj(SPObject const& obj, Util::ptr_shared name) + : DebugLayer(name) + { + _addProperty("layer", stringify_obj(obj)); + } +}; + +class DebugAddLayer : public DebugLayerObj { +public: + DebugAddLayer(SPObject const &obj) + : DebugLayerObj(obj, Util::share_static_string("add-layer")) + { + } +}; + + +} // end of namespace +*/ + +LayerManager::LayerManager(SPDesktop *desktop) +: _desktop(desktop), _document(nullptr) +{ + _layer_connection = desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayerManager::_selectedLayerChanged) ); + + sigc::bound_mem_functor1 first = sigc::mem_fun(*this, &LayerManager::_setDocument); + + // This next line has problems on gcc 4.0.2 + sigc::slot base2 = first; + + sigc::slot slot2 = sigc::hide<0>( base2 ); + _document_connection = desktop->connectDocumentReplaced( slot2 ); + + _setDocument(desktop->doc()); +} + +LayerManager::~LayerManager() +{ + _layer_connection.disconnect(); + _document_connection.disconnect(); + _resource_connection.disconnect(); + _document = nullptr; +} + +void LayerManager::setCurrentLayer( SPObject* obj ) +{ + //g_return_if_fail( _desktop->currentRoot() ); + if ( _desktop->currentRoot() ) { + _desktop->setCurrentLayer( obj ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/selection/layerdeselect", true)) { + _desktop->getSelection()->clear(); + } + } +} + +/* + * Return a unique layer name similar to param label + * A unique name is made by substituting or appending the label's number suffix with + * the next unique larger number suffix not already used for any layer name + */ +Glib::ustring LayerManager::getNextLayerName( SPObject* obj, gchar const *label) +{ + Glib::ustring incoming( label ? label : "Layer 1" ); + Glib::ustring result(incoming); + Glib::ustring base(incoming); + Glib::ustring split(" "); + guint startNum = 1; + + gint pos = base.length()-1; + while (pos >= 0 && g_ascii_isdigit(base[pos])) { + pos-- ; + } + + gchar* numpart = g_strdup(base.substr(pos+1).c_str()); + if ( numpart ) { + gchar* endPtr = nullptr; + guint64 val = g_ascii_strtoull( numpart, &endPtr, 10); + if ( ((val > 0) || (endPtr != numpart)) && (val < 65536) ) { + base.erase( pos+1); + result = incoming; + startNum = static_cast(val); + split = ""; + } + g_free(numpart); + } + + std::set currentNames; + std::vector layers = _document->getResourceList("layer"); + SPObject *root=_desktop->currentRoot(); + if ( root ) { + for (auto layer : layers) { + if (layer != obj) + currentNames.insert( layer->label() ? Glib::ustring(layer->label()) : Glib::ustring() ); + } + } + + // Not sure if we need to cap it, but we'll just be paranoid for the moment + // Intentionally unsigned + guint endNum = startNum + 3000; + for ( guint i = startNum; (i < endNum) && (currentNames.find(result) != currentNames.end()); i++ ) { + result = Glib::ustring::format(base, split, i); + } + + return result; +} + +void LayerManager::renameLayer( SPObject* obj, gchar const *label, bool uniquify ) +{ + Glib::ustring incoming( label ? label : "" ); + Glib::ustring result(incoming); + + if (uniquify) { + result = getNextLayerName(obj, label); + } + + obj->setLabel( result.c_str() ); +} + + + +void LayerManager::_setDocument(SPDocument *document) { + if (_document) { + _resource_connection.disconnect(); + } + _document = document; + if (document) { + _resource_connection = document->connectResourcesChanged("layer", sigc::mem_fun(*this, &LayerManager::_rebuild)); + } + _rebuild(); +} + +void LayerManager::_objectModified( SPObject* obj, guint /*flags*/ ) +{ + _details_changed_signal.emit( obj ); +} + +void LayerManager::_rebuild() { +// Debug::EventTracker tracker1(); + + while ( !_watchers.empty() ) { + LayerWatcher* one = _watchers.back(); + _watchers.pop_back(); + if ( one->_obj ) { + Node* node = one->_obj->getRepr(); + if ( node ) { + node->removeObserver(*one); + } + one->_connection.disconnect(); + } + } + + _clear(); + + if (!_document) // http://sourceforge.net/mailarchive/forum.php?thread_name=5747bce9a7ed077c1b4fc9f0f4f8a5e0%40localhost&forum_name=inkscape-devel + return; + + std::vector layers = _document->getResourceList("layer"); + + SPObject *root=_desktop->currentRoot(); + if ( root ) { + _addOne(root); + + std::set layersToAdd; + + for ( std::vector::const_iterator iter = layers.begin(); iter != layers.end(); ++iter ) { + SPObject *layer = *iter; + bool needsAdd = false; + std::set additional; + + if ( root->isAncestorOf(layer) ) { + needsAdd = true; + for ( SPObject* curr = layer; curr && (curr != root) && needsAdd; curr = curr->parent ) { + if ( SP_IS_GROUP(curr) ) { + SPGroup* group = SP_GROUP(curr); + if ( group->layerMode() == SPGroup::LAYER ) { + // If we have a layer-group as the one or a parent, ensure it is listed as a valid layer. + needsAdd &= ( std::find(layers.begin(),layers.end(),curr) != layers.end() ); + // XML Tree being used here directly while it shouldn't be... + if ( (!(group->getRepr())) || (!(group->getRepr()->parent())) ) { + needsAdd = false; + } + } else { + // If a non-layer group is a parent of layer groups, then show it also as a layer. + // TODO add the magic Inkscape group mode? + // XML Tree being used directly while it shouldn't be... + if ( group->getRepr() && group->getRepr()->parent() ) { + additional.insert(group); + } else { + needsAdd = false; + } + } + } + } + } + if ( needsAdd ) { + if ( !includes(layer) ) { + layersToAdd.insert(SP_GROUP(layer)); + } + for (auto it : additional) { + if ( !includes(it) ) { + layersToAdd.insert(it); + } + } + } + } + + for (auto layer : layersToAdd) { + // Filter out objects in the middle of being deleted + + // Such may have been the cause of bug 1339397. + // See http://sourceforge.net/tracker/index.php?func=detail&aid=1339397&group_id=93438&atid=604306 + + SPObject const *higher = layer; + while ( higher && (higher->parent != root) ) { + higher = higher->parent; + } + Node const* node = higher ? higher->getRepr() : nullptr; + if ( node && node->parent() ) { +// Debug::EventTracker tracker(*layer); + + sigc::connection connection = layer->connectModified(sigc::mem_fun(*this, &LayerManager::_objectModified)); + + LayerWatcher *eye = new LayerWatcher(this, layer, connection); + _watchers.push_back( eye ); + layer->getRepr()->addObserver(*eye); + + _addOne(layer); + } + } + } +} + +// Connected to the desktop's CurrentLayerChanged signal +void LayerManager::_selectedLayerChanged(SPObject *layer) +{ + // notify anyone who's listening to this instead of directly to the desktop + _layer_changed_signal.emit(layer); +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-manager.h b/src/layer-manager.h new file mode 100644 index 0000000..7bc9d2e --- /dev/null +++ b/src/layer-manager.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::LayerManager - a view of a document's layers, relative + * to a particular desktop + * + * Copyright 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_LAYER_MANAGER_H +#define SEEN_INKSCAPE_LAYER_MANAGER_H + +#include +#include + +#include "document-subset.h" +#include "gc-finalized.h" +#include "inkgc/gc-soft-ptr.h" + +class SPDesktop; +class SPDocument; + +namespace Inkscape { + +class LayerManager : public DocumentSubset, + public GC::Finalized +{ +public: + LayerManager(SPDesktop *desktop); + ~LayerManager() override; + + void setCurrentLayer( SPObject* obj ); + void renameLayer( SPObject* obj, char const *label, bool uniquify ); + Glib::ustring getNextLayerName( SPObject* obj, char const *label); + + sigc::connection connectCurrentLayerChanged(const sigc::slot & slot) { + return _layer_changed_signal.connect(slot); + } + + sigc::connection connectLayerDetailsChanged(const sigc::slot & slot) { + return _details_changed_signal.connect(slot); + } + +private: + friend class LayerWatcher; + class LayerWatcher; + + void _objectModified( SPObject* obj, unsigned int flags ); + void _setDocument(SPDocument *document); + void _rebuild(); + void _selectedLayerChanged(SPObject *layer); + + sigc::connection _layer_connection; + sigc::connection _document_connection; + sigc::connection _resource_connection; + + GC::soft_ptr _desktop; + SPDocument *_document; + + std::vector _watchers; + + sigc::signal _layer_changed_signal; + sigc::signal _details_changed_signal; +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-model.cpp b/src/layer-model.cpp new file mode 100644 index 0000000..82f93e9 --- /dev/null +++ b/src/layer-model.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Editable view implementation + * + * Authors: + * Lauris Kaplinski + * MenTaLguY + * bulia byak + * Ralf Stephan + * John Bintz + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2007 Jon A. Cruz + * Copyright (C) 2006-2008 Johan Engelen + * Copyright (C) 2006 John Bintz + * Copyright (C) 2004 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layer-model.h" + +#include "document.h" +#include "layer-fns.h" +#include "object-hierarchy.h" + +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-item-group.h" +#include "object/sp-root.h" + +// Callbacks +static void _layer_activated(SPObject *layer, Inkscape::LayerModel *layer_model); +static void _layer_deactivated(SPObject *layer, Inkscape::LayerModel *layer_model); +static void _layer_changed(SPObject *top, SPObject *bottom, Inkscape::LayerModel *layer_model); + +namespace Inkscape { + +LayerModel::LayerModel() + : _doc( nullptr ) + , _layer_hierarchy( nullptr ) + , _display_key( 0 ) +{ +} + +LayerModel::~LayerModel() +{ + if (_layer_hierarchy) { + delete _layer_hierarchy; +// _layer_hierarchy = NULL; //this should be here, but commented to find other bug somewhere else. + } +} + +void LayerModel::setDocument(SPDocument *doc) +{ + _doc = doc; + if (_layer_hierarchy) { + _layer_hierarchy->clear(); + delete _layer_hierarchy; + } + _layer_hierarchy = new Inkscape::ObjectHierarchy(nullptr); + _layer_hierarchy->connectAdded(sigc::bind(sigc::ptr_fun(_layer_activated), this)); + _layer_hierarchy->connectRemoved(sigc::bind(sigc::ptr_fun(_layer_deactivated), this)); + _layer_hierarchy->connectChanged(sigc::bind(sigc::ptr_fun(_layer_changed), this)); + _layer_hierarchy->setTop(doc->getRoot()); +} + +void LayerModel::setDisplayKey(unsigned int display_key) +{ + _display_key = display_key; +} + +SPDocument *LayerModel::getDocument() +{ + return _doc; +} + +/** + * Returns current root (=bottom) layer. + */ +SPObject *LayerModel::currentRoot() const +{ + return _layer_hierarchy ? _layer_hierarchy->top() : nullptr; +} + +/** + * Returns current top layer. + */ +SPObject *LayerModel::currentLayer() const +{ + return _layer_hierarchy ? _layer_hierarchy->bottom() : nullptr; +} + +/** + * Resets the bottom layer to the current root + */ +void LayerModel::reset() { + if (_layer_hierarchy) { + _layer_hierarchy->setBottom(currentRoot()); + } +} + +/** + * Sets the current layer of the desktop. + * + * Make \a object the top layer. + */ +void LayerModel::setCurrentLayer(SPObject *object) { + g_return_if_fail(SP_IS_GROUP(object)); + g_return_if_fail( currentRoot() == object || (currentRoot() && currentRoot()->isAncestorOf(object)) ); + // printf("Set Layer to ID: %s\n", object->getId()); + _layer_hierarchy->setBottom(object); +} + +void LayerModel::toggleHideAllLayers(bool hide) { + + for ( SPObject* obj = Inkscape::previous_layer(currentRoot(), currentRoot()); obj; obj = Inkscape::previous_layer(currentRoot(), obj) ) { + SP_ITEM(obj)->setHidden(hide); + } +} + +void LayerModel::toggleLockAllLayers(bool lock) { + + for ( SPObject* obj = Inkscape::previous_layer(currentRoot(), currentRoot()); obj; obj = Inkscape::previous_layer(currentRoot(), obj) ) { + SP_ITEM(obj)->setLocked(lock); + } +} + +void LayerModel::toggleLockOtherLayers(SPObject *object) { + g_return_if_fail(SP_IS_GROUP(object)); + g_return_if_fail( currentRoot() == object || (currentRoot() && currentRoot()->isAncestorOf(object)) ); + + bool othersLocked = false; + std::vector layers; + for ( SPObject* obj = Inkscape::next_layer(currentRoot(), object); obj; obj = Inkscape::next_layer(currentRoot(), obj) ) { + // Don't lock any ancestors, since that would in turn lock the layer as well + if (!obj->isAncestorOf(object)) { + layers.push_back(obj); + othersLocked |= !SP_ITEM(obj)->isLocked(); + } + } + for ( SPObject* obj = Inkscape::previous_layer(currentRoot(), object); obj; obj = Inkscape::previous_layer(currentRoot(), obj) ) { + if (!obj->isAncestorOf(object)) { + layers.push_back(obj); + othersLocked |= !SP_ITEM(obj)->isLocked(); + } + } + + SPItem *item = SP_ITEM(object); + if ( item->isLocked() ) { + item->setLocked(false); + } + + for (auto & layer : layers) { + SP_ITEM(layer)->setLocked(othersLocked); + } +} + + +void LayerModel::toggleLayerSolo(SPObject *object) { + g_return_if_fail(SP_IS_GROUP(object)); + g_return_if_fail( currentRoot() == object || (currentRoot() && currentRoot()->isAncestorOf(object)) ); + + bool othersShowing = false; + std::vector layers; + for ( SPObject* obj = Inkscape::next_layer(currentRoot(), object); obj; obj = Inkscape::next_layer(currentRoot(), obj) ) { + // Don't hide ancestors, since that would in turn hide the layer as well + if (!obj->isAncestorOf(object)) { + layers.push_back(obj); + othersShowing |= !SP_ITEM(obj)->isHidden(); + } + } + for ( SPObject* obj = Inkscape::previous_layer(currentRoot(), object); obj; obj = Inkscape::previous_layer(currentRoot(), obj) ) { + if (!obj->isAncestorOf(object)) { + layers.push_back(obj); + othersShowing |= !SP_ITEM(obj)->isHidden(); + } + } + + + SPItem *item = SP_ITEM(object); + if ( item->isHidden() ) { + item->setHidden(false); + } + + for (auto & layer : layers) { + SP_ITEM(layer)->setHidden(othersShowing); + } +} + +/** + * Return layer that contains \a object. + */ +SPObject *LayerModel::layerForObject(SPObject *object) { + g_return_val_if_fail(object != nullptr, NULL); + + SPObject *root=currentRoot(); + object = object->parent; + while ( object && object != root && !isLayer(object) ) { + // Objects in defs have no layer and are NOT in the root layer + if(SP_IS_DEFS(object)) + return nullptr; + object = object->parent; + } + return object; +} + +/** + * True if object is a layer. + */ +bool LayerModel::isLayer(SPObject *object) const { + return ( SP_IS_GROUP(object) + && ( SP_GROUP(object)->effectiveLayerMode(_display_key) + == SPGroup::LAYER ) ); +} + +} // namespace Inkscape + + +/// Callback +static void +_layer_activated(SPObject *layer, Inkscape::LayerModel *layer_model) { + g_return_if_fail(SP_IS_GROUP(layer)); + layer_model->_layer_activated_signal.emit(layer); +} + +/// Callback +static void +_layer_deactivated(SPObject *layer, Inkscape::LayerModel *layer_model) { + g_return_if_fail(SP_IS_GROUP(layer)); + layer_model->_layer_deactivated_signal.emit(layer); +} + +/// Callback +static void +_layer_changed(SPObject *top, SPObject *bottom, Inkscape::LayerModel *layer_model) +{ + layer_model->_layer_changed_signal.emit (top, bottom); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/layer-model.h b/src/layer-model.h new file mode 100644 index 0000000..8e71538 --- /dev/null +++ b/src/layer-model.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_LAYER_MODEL_H +#define SEEN_INKSCAPE_LAYER_MODEL_H + +/* + * Authors: + * Lauris Kaplinski + * Frank Felfe + * bulia byak + * Ralf Stephan + * John Bintz + * Johan Engelen + * Jon A. Cruz get + * Abhishek Sharma + * Eric Greveson + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2006 John Bintz + * Copyright (C) 1999-2013 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include +#include + +class SPDocument; +class SPObject; + +namespace Inkscape { + +class ObjectHierarchy; + +namespace XML { + class Node; +} + +/** + * The layer model for a document. + * + * This class represents the layer model for a document, typically (but + * not necessarily) displayed in an SPDesktop. + * + * It also implements its own asynchronous notification signals that + * UI elements can listen to. + */ +class LayerModel +{ + SPDocument *_doc; + Inkscape::ObjectHierarchy *_layer_hierarchy; + unsigned int _display_key; + +public: + /** Construct a layer model */ + LayerModel(); + + /** Destructor */ + ~LayerModel(); + + // Set document + void setDocument(SPDocument *doc); + + // Set display key. For GUI apps. + void setDisplayKey(unsigned int display_key); + + // Get the document that this layer model refers to. May be NULL. + SPDocument *getDocument(); + + // TODO look into making these return a more specific subclass: + SPObject *currentRoot() const; + SPObject *currentLayer() const; + + void reset(); + void setCurrentLayer(SPObject *object); + void toggleLayerSolo(SPObject *object); + void toggleHideAllLayers(bool hide); + void toggleLockAllLayers(bool lock); + void toggleLockOtherLayers(SPObject *object); + SPObject *layerForObject(SPObject *object); + bool isLayer(SPObject *object) const; + + sigc::signal _layer_activated_signal; + sigc::signal _layer_deactivated_signal; + sigc::signal _layer_changed_signal; +}; + +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/CMakeLists.txt b/src/libnrtype/CMakeLists.txt new file mode 100644 index 0000000..42ff9d2 --- /dev/null +++ b/src/libnrtype/CMakeLists.txt @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(nrtype_SRC + FontFactory.cpp + FontInstance.cpp + font-lister.cpp + Layout-TNG.cpp + Layout-TNG-Compute.cpp + Layout-TNG-Input.cpp + Layout-TNG-OutIter.cpp + Layout-TNG-Output.cpp + Layout-TNG-Scanline-Makers.cpp + OpenTypeUtil.cpp + + # ------- + # Headers + font-glyph.h + font-instance.h + font-lister.h + font-style.h + FontFactory.h + Layout-TNG-Scanline-Maker.h + Layout-TNG.h + OpenTypeUtil.cpp +) + +add_inkscape_lib(nrtype_LIB "${nrtype_SRC}") + +# we have circular references between nrtype_LIB and inkscape_base! +# this workaround prevents undefined references in nrtype_LIB when building static libraries (likely link order problem) +if(NOT BUILD_SHARED_LIBS) + target_link_libraries(nrtype_LIB inkscape_base) +endif() diff --git a/src/libnrtype/FontFactory.cpp b/src/libnrtype/FontFactory.cpp new file mode 100644 index 0000000..a495a52 --- /dev/null +++ b/src/libnrtype/FontFactory.cpp @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * fred + * bulia byak + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include + +#include + +#include + +#include +#include +#include + +#include "io/sys.h" + +#include "libnrtype/FontFactory.h" +#include "libnrtype/font-instance.h" +#include "libnrtype/OpenTypeUtil.h" + +typedef std::unordered_map FaceMapType; + +// need to avoid using the size field +size_t font_descr_hash::operator()( PangoFontDescription *const &x) const { + int h = 0; + char const *theF = sp_font_description_get_family(x); + h += (theF)?g_str_hash(theF):0; + h *= 1128467; + h += (int)pango_font_description_get_style(x); + h *= 1128467; + h += (int)pango_font_description_get_variant(x); + h *= 1128467; + h += (int)pango_font_description_get_weight(x); + h *= 1128467; + h += (int)pango_font_description_get_stretch(x); +#if PANGO_VERSION_CHECK(1,41,1) + char const *theV = pango_font_description_get_variations(x); + h *= 1128467; + h += (theV)?g_str_hash(theV):0; +#endif + return h; +} + +bool font_descr_equal::operator()( PangoFontDescription *const&a, PangoFontDescription *const &b) const { + //if ( pango_font_description_equal(a,b) ) return true; + char const *fa = sp_font_description_get_family(a); + char const *fb = sp_font_description_get_family(b); + if ( ( fa && fb == nullptr ) || ( fb && fa == nullptr ) ) return false; + if ( fa && fb && strcmp(fa,fb) != 0 ) return false; + if ( pango_font_description_get_style(a) != pango_font_description_get_style(b) ) return false; + if ( pango_font_description_get_variant(a) != pango_font_description_get_variant(b) ) return false; + if ( pango_font_description_get_weight(a) != pango_font_description_get_weight(b) ) return false; + if ( pango_font_description_get_stretch(a) != pango_font_description_get_stretch(b) ) return false; +#if PANGO_VERSION_CHECK(1,41,1) + if ( g_strcmp0( pango_font_description_get_variations(a), + pango_font_description_get_variations(b) ) != 0 ) return false; +#endif + return true; +} + +// User must free return value. +PangoFontDescription* ink_font_description_from_style(SPStyle const *style) +{ + PangoFontDescription *descr = pango_font_description_new(); + + pango_font_description_set_family(descr, style->font_family.value()); + + // This duplicates Layout::EnumConversionItem... perhaps we can share code? + switch ( style->font_style.computed ) { + case SP_CSS_FONT_STYLE_ITALIC: + pango_font_description_set_style(descr, PANGO_STYLE_ITALIC); + break; + + case SP_CSS_FONT_STYLE_OBLIQUE: + pango_font_description_set_style(descr, PANGO_STYLE_OBLIQUE); + break; + + case SP_CSS_FONT_STYLE_NORMAL: + default: + pango_font_description_set_style(descr, PANGO_STYLE_NORMAL); + break; + } + + switch( style->font_weight.computed ) { + case SP_CSS_FONT_WEIGHT_100: + pango_font_description_set_weight(descr, PANGO_WEIGHT_THIN); + break; + + case SP_CSS_FONT_WEIGHT_200: + pango_font_description_set_weight(descr, PANGO_WEIGHT_ULTRALIGHT); + break; + + case SP_CSS_FONT_WEIGHT_300: + pango_font_description_set_weight(descr, PANGO_WEIGHT_LIGHT); + break; + + case SP_CSS_FONT_WEIGHT_400: + case SP_CSS_FONT_WEIGHT_NORMAL: + pango_font_description_set_weight(descr, PANGO_WEIGHT_NORMAL); + break; + + case SP_CSS_FONT_WEIGHT_500: + pango_font_description_set_weight(descr, PANGO_WEIGHT_MEDIUM); + break; + + case SP_CSS_FONT_WEIGHT_600: + pango_font_description_set_weight(descr, PANGO_WEIGHT_SEMIBOLD); + break; + + case SP_CSS_FONT_WEIGHT_700: + case SP_CSS_FONT_WEIGHT_BOLD: + pango_font_description_set_weight(descr, PANGO_WEIGHT_BOLD); + break; + + case SP_CSS_FONT_WEIGHT_800: + pango_font_description_set_weight(descr, PANGO_WEIGHT_ULTRABOLD); + break; + + case SP_CSS_FONT_WEIGHT_900: + pango_font_description_set_weight(descr, PANGO_WEIGHT_HEAVY); + break; + + case SP_CSS_FONT_WEIGHT_LIGHTER: + case SP_CSS_FONT_WEIGHT_BOLDER: + default: + g_warning("FaceFromStyle: Unrecognized font_weight.computed value"); + pango_font_description_set_weight(descr, PANGO_WEIGHT_NORMAL); + break; + } + // PANGO_WIEGHT_ULTRAHEAVY not used (not CSS2) + + switch (style->font_stretch.computed) { + case SP_CSS_FONT_STRETCH_ULTRA_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_ULTRA_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_EXTRA_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXTRA_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_SEMI_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_SEMI_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_NORMAL: + pango_font_description_set_stretch(descr, PANGO_STRETCH_NORMAL); + break; + + case SP_CSS_FONT_STRETCH_SEMI_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_SEMI_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_EXTRA_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXTRA_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_ULTRA_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_ULTRA_EXPANDED); + + case SP_CSS_FONT_STRETCH_WIDER: + case SP_CSS_FONT_STRETCH_NARROWER: + default: + g_warning("FaceFromStyle: Unrecognized font_stretch.computed value"); + pango_font_description_set_stretch(descr, PANGO_STRETCH_NORMAL); + break; + } + + switch ( style->font_variant.computed ) { + case SP_CSS_FONT_VARIANT_SMALL_CAPS: + pango_font_description_set_variant(descr, PANGO_VARIANT_SMALL_CAPS); + break; + + case SP_CSS_FONT_VARIANT_NORMAL: + default: + pango_font_description_set_variant(descr, PANGO_VARIANT_NORMAL); + break; + } + +#if PANGO_VERSION_CHECK(1,41,1) + // Check if not empty as Pango will add @ to string even if empty (bug in Pango?). + if (!style->font_variation_settings.axes.empty()) { + pango_font_description_set_variations(descr, style->font_variation_settings.toString().c_str()); + } +#endif + + return descr; +} + +/////////////////// helper functions + +static void noop(...) {} +//#define PANGO_DEBUG g_print +#define PANGO_DEBUG noop + + +///////////////////// FontFactory +#ifndef USE_PANGO_WIN32 +// the substitute function to tell fontconfig to enforce outline fonts +static void FactorySubstituteFunc(FcPattern *pattern,gpointer /*data*/) +{ + FcPatternAddBool(pattern, "FC_OUTLINE",FcTrue); + //char *fam = NULL; + //FcPatternGetString(pattern, "FC_FAMILY",0, &fam); + //printf("subst_f on %s\n",fam); +} +#endif + + +font_factory *font_factory::lUsine = nullptr; + +font_factory *font_factory::Default() +{ + if ( lUsine == nullptr ) lUsine = new font_factory; + return lUsine; +} + +font_factory::font_factory() : + nbEnt(0), // Note: this "ents" cache only keeps fonts from being unreffed, does not speed up access + maxEnt(32), + ents(static_cast(g_malloc(maxEnt*sizeof(font_entry)))), +#ifdef USE_PANGO_WIN32 + fontServer(pango_win32_font_map_for_display()), + pangoFontCache(pango_win32_font_map_get_font_cache(fontServer)), + hScreenDC(pango_win32_get_dc()), +#else + fontServer(pango_ft2_font_map_new()), +#endif + fontContext(pango_font_map_create_context(fontServer)), + fontSize(512), + loadedPtr(new FaceMapType()) +{ +#ifndef USE_PANGO_WIN32 + pango_ft2_font_map_set_resolution(PANGO_FT2_FONT_MAP(fontServer), + 72, 72); + pango_ft2_font_map_set_default_substitute(PANGO_FT2_FONT_MAP(fontServer), + FactorySubstituteFunc, + this, + nullptr); +#endif + +} + +font_factory::~font_factory() +{ + for (int i = 0;i < nbEnt;i++) ents[i].f->Unref(); + if ( ents ) g_free(ents); + + g_object_unref(fontServer); +#ifdef USE_PANGO_WIN32 + pango_win32_shutdown_display(); +#else + //pango_ft2_shutdown_display(); +#endif + //g_object_unref(fontContext); + + if (loadedPtr) { + FaceMapType* tmp = static_cast(loadedPtr); + delete tmp; + loadedPtr = nullptr; + } +} + + +Glib::ustring font_factory::ConstructFontSpecification(PangoFontDescription *font) +{ + Glib::ustring pangoString; + + g_assert(font); + + if (font) { + // Once the format for the font specification is decided, it must be + // kept.. if it is absolutely necessary to change it, the attribute + // it is written to needs to have a new version so the legacy files + // can be read. + + PangoFontDescription *copy = pango_font_description_copy(font); + + pango_font_description_unset_fields (copy, PANGO_FONT_MASK_SIZE); + char * copyAsString = pango_font_description_to_string(copy); + pangoString = copyAsString; + g_free(copyAsString); + copyAsString = nullptr; + + pango_font_description_free(copy); + + } + + return pangoString; +} + +Glib::ustring font_factory::ConstructFontSpecification(font_instance *font) +{ + Glib::ustring pangoString; + + g_assert(font); + + if (font) { + pangoString = ConstructFontSpecification(font->descr); + } + + return pangoString; +} + +/* + * Wrap calls to pango_font_description_get_family + * and replace some of the pango font names with generic css names + * http://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#generic-font-families + * + * This function should be called in place of pango_font_description_get_family() + */ +const char *sp_font_description_get_family(PangoFontDescription const *fontDescr) { + + static std::map fontNameMap; + std::map::iterator it; + + if (fontNameMap.empty()) { + fontNameMap.insert(std::make_pair("Sans", "sans-serif")); + fontNameMap.insert(std::make_pair("Serif", "serif")); + fontNameMap.insert(std::make_pair("Monospace", "monospace")); + } + + const char *pangoFamily = pango_font_description_get_family(fontDescr); + + if (pangoFamily && ((it = fontNameMap.find(pangoFamily)) != fontNameMap.end())) { + return (it->second).c_str(); + } + + return pangoFamily; +} + +Glib::ustring font_factory::GetUIFamilyString(PangoFontDescription const *fontDescr) +{ + Glib::ustring family; + + g_assert(fontDescr); + + if (fontDescr) { + // For now, keep it as family name taken from pango + const char *pangoFamily = sp_font_description_get_family(fontDescr); + + if( pangoFamily ) { + family = pangoFamily; + } + } + + return family; +} + +Glib::ustring font_factory::GetUIStyleString(PangoFontDescription const *fontDescr) +{ + Glib::ustring style; + + g_assert(fontDescr); + + if (fontDescr) { + PangoFontDescription *fontDescrCopy = pango_font_description_copy(fontDescr); + + pango_font_description_unset_fields(fontDescrCopy, PANGO_FONT_MASK_FAMILY); + pango_font_description_unset_fields(fontDescrCopy, PANGO_FONT_MASK_SIZE); + + // For now, keep it as style name taken from pango + char *fontDescrAsString = pango_font_description_to_string(fontDescrCopy); + style = fontDescrAsString; + g_free(fontDescrAsString); + fontDescrAsString = nullptr; + pango_font_description_free(fontDescrCopy); + } + + return style; +} + + +///// + +// Calculate a Style "value" based on CSS values for ordering styles. +static int StyleNameValue( const Glib::ustring &style ) +{ + + PangoFontDescription *pfd = pango_font_description_from_string ( style.c_str() ); + int value = + pango_font_description_get_weight ( pfd ) * 1000000 + + pango_font_description_get_style ( pfd ) * 10000 + + pango_font_description_get_stretch( pfd ) * 100 + + pango_font_description_get_variant( pfd ); + pango_font_description_free ( pfd ); + return value; +} + +// Determines order in which styles are presented (sorted by CSS style values) +//static bool StyleNameCompareInternal(const StyleNames &style1, const StyleNames &style2) +//{ +// return( StyleNameValue( style1.CssName ) < StyleNameValue( style2.CssName ) ); +//} + +static gint StyleNameCompareInternalGlib(gconstpointer a, gconstpointer b) +{ + return( StyleNameValue( ((StyleNames *)a)->CssName ) < + StyleNameValue( ((StyleNames *)b)->CssName ) ? -1 : 1 ); +} + +static bool ustringPairSort(std::pair const& first, std::pair const& second) +{ + // well, this looks weird. + return first.second < second.second; +} + +void font_factory::GetUIFamilies(std::vector& out) +{ + // Gather the family names as listed by Pango + PangoFontFamily** families = nullptr; + int numFamilies = 0; + pango_font_map_list_families(fontServer, &families, &numFamilies); + + std::vector > sorted; + + // not size_t + for (int currentFamily = 0; currentFamily < numFamilies; ++currentFamily) { + const char* displayName = pango_font_family_get_name(families[currentFamily]); + + if (displayName == nullptr || *displayName == '\0') { + std::cerr << "font_factory::GetUIFamilies: Missing displayName! " << std::endl; + continue; + } + if (!g_utf8_validate(displayName, -1, nullptr)) { + // TODO: can can do anything about this or does it always indicate broken fonts that should not be used? + std::cerr << "font_factory::GetUIFamilies: Illegal characters in displayName. "; + std::cerr << "Ignoring font '" << displayName << "'" << std::endl; + continue; + } + sorted.emplace_back(families[currentFamily], displayName); + } + + std::sort(sorted.begin(), sorted.end(), ustringPairSort); + + for (auto & i : sorted) { + out.push_back(i.first); + } +} + +GList* font_factory::GetUIStyles(PangoFontFamily * in) +{ + GList* ret = nullptr; + // Gather the styles for this family + PangoFontFace** faces = nullptr; + int numFaces = 0; + if (in == nullptr) { + std::cerr << "font_factory::GetUIStyles(): PangoFontFamily is NULL" << std::endl; + return ret; + } + + pango_font_family_list_faces(in, &faces, &numFaces); + + for (int currentFace = 0; currentFace < numFaces; currentFace++) { + + // If the face has a name, describe it, and then use the + // description to get the UI family and face strings + const gchar* displayName = pango_font_face_get_face_name(faces[currentFace]); + // std::cout << "Display Name: " << displayName << std::endl; + if (displayName == nullptr || *displayName == '\0') { + std::cerr << "font_factory::GetUIStyles: Missing displayName! " << std::endl; + continue; + } + + PangoFontDescription *faceDescr = pango_font_face_describe(faces[currentFace]); + if (faceDescr) { + Glib::ustring familyUIName = GetUIFamilyString(faceDescr); + Glib::ustring styleUIName = GetUIStyleString(faceDescr); + // std::cout << " " << familyUIName << " styleUIName: " << styleUIName << " displayName: " << displayName << std::endl; + + // Disable synthesized (faux) font faces except for CSS generic faces + if (pango_font_face_is_synthesized(faces[currentFace]) ) { + if (familyUIName.compare( "sans-serif" ) != 0 && + familyUIName.compare( "serif" ) != 0 && + familyUIName.compare( "monospace" ) != 0 && + familyUIName.compare( "fantasy" ) != 0 && + familyUIName.compare( "cursive" ) != 0 ) { + continue; + } + } + + // Pango breaks the 1 to 1 mapping between Pango weights and CSS weights by + // adding Semi-Light (as of 1.36.7), Book (as of 1.24), and Ultra-Heavy (as of + // 1.24). We need to map these weights to CSS weights. Book and Ultra-Heavy + // are rarely used. Semi-Light (350) is problematic as it is halfway between + // Light (300) and Normal (400) and if care is not taken it is converted to + // Normal, rather than Light. + // + // Note: The ultimate solution to handling various weight in the same + // font family is to support the @font rules from CSS. + // + // Additional notes, helpful for debugging: + // Pango's FC backend: + // Weights defined in fontconfig/fontconfig.h + // String equivalents in src/fcfreetype.c + // Weight set from os2->usWeightClass + // Use Fontforge: Element->Font Info...->OS/2->Misc->Weight Class to check font weight + size_t f = styleUIName.find( "Book" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 4, "Normal" ); + } + f = styleUIName.find( "Semi-Light" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 10, "Light" ); + } + f = styleUIName.find( "Ultra-Heavy" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 11, "Heavy" ); + } + + bool exists = false; + for(GList *temp = ret; temp; temp = temp->next) { + if( ((StyleNames*)temp->data)->CssName.compare( styleUIName ) == 0 ) { + exists = true; + std::cerr << "Warning: Font face with same CSS values already added: " + << familyUIName << " " << styleUIName + << " (" << ((StyleNames*)temp->data)->DisplayName + << ", " << displayName << ")" << std::endl; + break; + } + } + + if (!exists && !familyUIName.empty() && !styleUIName.empty()) { + // Add the style information + ret = g_list_append(ret, new StyleNames(styleUIName, displayName)); + } + } + pango_font_description_free(faceDescr); + } + g_free(faces); + + // Sort the style lists + ret = g_list_sort( ret, StyleNameCompareInternalGlib ); + return ret; +} + + +font_instance* font_factory::FaceFromStyle(SPStyle const *style) +{ + font_instance *font = nullptr; + + g_assert(style); + + if (style) { + + // First try to use the font specification if it is set + char const *val; + if (style->font_specification.set + && (val = style->font_specification.value()) + && val[0]) { + + font = FaceFromFontSpecification(val); + } + + // If that failed, try using the CSS information in the style + if (!font) { + PangoFontDescription* temp_descr = + ink_font_description_from_style(style); + font = Face(temp_descr); + pango_font_description_free(temp_descr); + } + } + + return font; +} + +font_instance *font_factory::FaceFromDescr(char const *family, char const *style) +{ + PangoFontDescription *temp_descr = pango_font_description_from_string(style); + pango_font_description_set_family(temp_descr,family); + font_instance *res = Face(temp_descr); + pango_font_description_free(temp_descr); + return res; +} + +font_instance* font_factory::FaceFromPangoString(char const *pangoString) +{ + font_instance *fontInstance = nullptr; + + g_assert(pangoString); + + if (pangoString) { + + // Create a font description from the string - this may fail or + // produce unexpected results if the string does not have a good format + PangoFontDescription *descr = pango_font_description_from_string(pangoString); + + if (descr) { + if (sp_font_description_get_family(descr) != nullptr) { + fontInstance = Face(descr); + } + pango_font_description_free(descr); + } + } + + return fontInstance; +} + +font_instance* font_factory::FaceFromFontSpecification(char const *fontSpecification) +{ + font_instance *font = nullptr; + + g_assert(fontSpecification); + + if (fontSpecification) { + // How the string is used to reconstruct a font depends on how it + // was constructed in ConstructFontSpecification. As it stands, + // the font specification is a pango-created string + font = FaceFromPangoString(fontSpecification); + } + + return font; +} + +font_instance *font_factory::Face(PangoFontDescription *descr, bool canFail) +{ +#ifdef USE_PANGO_WIN32 + // damn Pango fudges the size, so we need to unfudge. See source of pango_win32_font_map_init() + pango_font_description_set_size(descr, (int) (fontSize*PANGO_SCALE*72/GetDeviceCaps(pango_win32_get_dc(),LOGPIXELSY))); // mandatory huge size (hinting workaround) +#else + pango_font_description_set_size(descr, (int) (fontSize*PANGO_SCALE)); // mandatory huge size (hinting workaround) +#endif + + font_instance *res = nullptr; + + FaceMapType& loadedFaces = *static_cast(loadedPtr); + if ( loadedFaces.find(descr) == loadedFaces.end() ) { + // not yet loaded + PangoFont *nFace = nullptr; + + // workaround for bug #1025565. + // fonts without families blow up Pango. + if (sp_font_description_get_family(descr) != nullptr) { + nFace = pango_font_map_load_font(fontServer,fontContext,descr); + } + else { + g_warning("%s", _("Ignoring font without family that will crash Pango")); + } + + if ( nFace ) { + // duplicate FcPattern, the hard way + res = new font_instance(); + // store the descr of the font we asked for, since this is the key where we intend + // to put the font_instance at in the unordered_map. the descr of the returned + // pangofont may differ from what was asked, so we don't know (at this + // point) whether loadedFaces[that_descr] is free or not (and overwriting + // an entry will bring deallocation problems) + res->descr = pango_font_description_copy(descr); + res->parent = this; + res->InstallFace(nFace); + if ( res->pFont == nullptr ) { + // failed to install face -> bitmap font + // printf("face failed\n"); + res->parent = nullptr; + delete res; + res = nullptr; + if ( canFail ) { + char *tc = pango_font_description_to_string(descr); + PANGO_DEBUG("falling back from %s to 'sans-serif' because InstallFace failed\n",tc); + g_free(tc); + pango_font_description_set_family(descr,"sans-serif"); + res = Face(descr,false); + } + } else { + loadedFaces[res->descr]=res; + res->Ref(); + AddInCache(res); + } + } else { + // no match + if ( canFail ) { + PANGO_DEBUG("falling back to 'sans-serif'\n"); + PangoFontDescription *new_descr = pango_font_description_new(); + pango_font_description_set_family(new_descr, "sans-serif"); + res = Face(new_descr, false); + pango_font_description_free(new_descr); + } else { + g_critical("Could not load any face for font '%s'.", pango_font_description_to_string(descr)); + } + } + + } else { + // already here + res = loadedFaces[descr]; + res->Ref(); + AddInCache(res); + } + if (res) { + res->InitTheFace(); + } + return res; +} + +// Not used, need to add variations if ever used. +// font_instance *font_factory::Face(char const *family, int variant, int style, int weight, int stretch, int /*size*/, int /*spacing*/) +// { +// // std::cout << "font_factory::Face(family, variant, style, weight, stretch,)" << std::endl; +// PangoFontDescription *temp_descr = pango_font_description_new(); +// pango_font_description_set_family(temp_descr,family); +// pango_font_description_set_weight(temp_descr,(PangoWeight)weight); +// pango_font_description_set_stretch(temp_descr,(PangoStretch)stretch); +// pango_font_description_set_style(temp_descr,(PangoStyle)style); +// pango_font_description_set_variant(temp_descr,(PangoVariant)variant); +// font_instance *res = Face(temp_descr); +// pango_font_description_free(temp_descr); +// return res; +// } + +void font_factory::UnrefFace(font_instance *who) +{ + if ( who ) { + FaceMapType& loadedFaces = *static_cast(loadedPtr); + + if ( loadedFaces.find(who->descr) == loadedFaces.end() ) { + // not found + char *tc = pango_font_description_to_string(who->descr); + g_warning("unrefFace %p=%s: failed\n",who,tc); + g_free(tc); + } else { + loadedFaces.erase(loadedFaces.find(who->descr)); + // printf("unrefFace %p: success\n",who); + } + } +} + +void font_factory::AddInCache(font_instance *who) +{ + if ( who == nullptr ) return; + for (int i = 0;i < nbEnt;i++) ents[i].age *= 0.9; + for (int i = 0;i < nbEnt;i++) { + if ( ents[i].f == who ) { + // printf("present\n"); + ents[i].age += 1.0; + return; + } + } + if ( nbEnt > maxEnt ) { + printf("cache sur-plein?\n"); + return; + } + who->Ref(); + if ( nbEnt == maxEnt ) { // cache is filled, unref the oldest-accessed font in it + int bi = 0; + double ba = ents[bi].age; + for (int i = 1;i < nbEnt;i++) { + if ( ents[i].age < ba ) { + bi = i; + ba = ents[bi].age; + } + } + ents[bi].f->Unref(); + ents[bi]=ents[--nbEnt]; + } + ents[nbEnt].f = who; + ents[nbEnt].age = 1.0; + nbEnt++; +} + +void font_factory::AddFontsDir(char const *utf8dir) +{ +#ifdef USE_PANGO_WIN32 + g_info("Adding additional font directories only supported for fontconfig backend."); +#else + if (!Inkscape::IO::file_test(utf8dir, G_FILE_TEST_IS_DIR)) { + g_warning("Fonts dir '%s' does not exist and will be ignored.", utf8dir); + return; + } + + gchar *dir; +# ifdef _WIN32 + dir = g_win32_locale_filename_from_utf8(utf8dir); +# else + dir = g_filename_from_utf8(utf8dir, -1, nullptr, nullptr, nullptr); +# endif + + FcConfig *conf = nullptr; +# if PANGO_VERSION_CHECK(1,38,0) + conf = pango_fc_font_map_get_config(PANGO_FC_FONT_MAP(fontServer)); +# endif + FcBool res = FcConfigAppFontAddDir(conf, (FcChar8 const *)dir); + if (res == FcTrue) { + g_info("Fonts dir '%s' added successfully.", utf8dir); +# if PANGO_VERSION_CHECK(1,38,0) + pango_fc_font_map_config_changed(PANGO_FC_FONT_MAP(fontServer)); +# endif + } else { + g_warning("Could not add fonts dir '%s'.", utf8dir); + } + + g_free(dir); +#endif +} + +void font_factory::AddFontFile(char const *utf8file) +{ +#ifdef USE_PANGO_WIN32 + g_info("Adding additional font only supported for fontconfig backend."); +#else + if (!Inkscape::IO::file_test(utf8file, G_FILE_TEST_IS_REGULAR)) { + g_warning("Font file '%s' does not exist and will be ignored.", utf8file); + return; + } + + gchar *file; +# ifdef _WIN32 + file = g_win32_locale_filename_from_utf8(utf8file); +# else + file = g_filename_from_utf8(utf8file, -1, nullptr, nullptr, nullptr); +# endif + + FcConfig *conf = nullptr; +# if PANGO_VERSION_CHECK(1,38,0) + conf = pango_fc_font_map_get_config(PANGO_FC_FONT_MAP(fontServer)); +# endif + FcBool res = FcConfigAppFontAddFile(conf, (FcChar8 const *)file); + if (res == FcTrue) { + g_info("Font file '%s' added successfully.", utf8file); +# if PANGO_VERSION_CHECK(1,38,0) + pango_fc_font_map_config_changed(PANGO_FC_FONT_MAP(fontServer)); +# endif + } else { + g_warning("Could not add font file '%s'.", utf8file); + } + + g_free(file); +#endif +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/FontFactory.h b/src/libnrtype/FontFactory.h new file mode 100644 index 0000000..547b8aa --- /dev/null +++ b/src/libnrtype/FontFactory.h @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * FontFactory.h + * testICU + * + */ + +#ifndef my_font_factory +#define my_font_factory + +#include +#include +#include + +#ifdef _WIN32 +//#define USE_PANGO_WIN32 // disable for Bug 165665 +#endif + +#include +#include "style.h" + +/* Freetype */ +#ifdef USE_PANGO_WIN32 +#include +#else +#include +#include +#include FT_FREETYPE_H +#endif + + +class font_instance; + +namespace Glib +{ + class ustring; +} + +// the font_factory keeps a hashmap of all the loaded font_instances, and uses the PangoFontDescription +// as index (nota: since pango already does that, using the PangoFont could work too) +struct font_descr_hash : public std::unary_function { + size_t operator()(PangoFontDescription *const &x) const; +}; +struct font_descr_equal : public std::binary_function { + bool operator()(PangoFontDescription *const &a, PangoFontDescription *const &b) const; +}; + +// Constructs a PangoFontDescription from SPStyle. Font size is not included. +// User must free return value. +PangoFontDescription* ink_font_description_from_style(SPStyle const *style); + +// Wraps calls to pango_font_description_get_family with some name substitution +const char *sp_font_description_get_family(PangoFontDescription const *fontDescr); + +// Class for style strings: both CSS and as suggested by font. +class StyleNames { + +public: + StyleNames() = default;; + StyleNames( Glib::ustring name ) : + CssName( name ), DisplayName( name ) {}; + StyleNames( Glib::ustring cssname, Glib::ustring displayname ) : + CssName(std::move( cssname )), DisplayName(std::move( displayname )) {}; + +public: + Glib::ustring CssName; // Style as Pango/CSS would write it. + Glib::ustring DisplayName; // Style as Font designer named it. +}; + +// Map type for gathering UI family and style names +// typedef std::map > FamilyToStylesMap; + +class font_factory { +public: + static font_factory *lUsine; /**< The default font_factory; i cannot think of why we would + * need more than one. + * + * ("l'usine" is french for "the factory".) + */ + + /** A little cache for fonts, so that you don't loose your time looking up fonts in the font list + * each font in the cache is refcounted once (and deref'd when removed from the cache). */ + struct font_entry { + font_instance *f; + double age; + }; + int nbEnt; ///< Number of entries. + int maxEnt; ///< Cache size. + font_entry *ents; + + // Pango data. Backend-specific structures are cast to these opaque types. + PangoFontMap *fontServer; + PangoContext *fontContext; +#ifdef USE_PANGO_WIN32 + PangoWin32FontCache *pangoFontCache; + HDC hScreenDC; +#endif + double fontSize; /**< The huge fontsize used as workaround for hinting. + * Different between freetype and win32. */ + + font_factory(); + virtual ~font_factory(); + + /// Returns the default font_factory. + static font_factory* Default(); + + /// Constructs a pango string for use with the fontStringMap (see below) + Glib::ustring ConstructFontSpecification(PangoFontDescription *font); + Glib::ustring ConstructFontSpecification(font_instance *font); + + /// Returns strings to be used in the UI for family and face (or "style" as the column is labeled) + Glib::ustring GetUIFamilyString(PangoFontDescription const *fontDescr); + Glib::ustring GetUIStyleString(PangoFontDescription const *fontDescr); + + // Helpfully inserts all font families into the provided vector + void GetUIFamilies(std::vector& out); + // Retrieves style information about a family in a newly allocated GList. + GList* GetUIStyles(PangoFontFamily * in); + + /// Retrieve a font_instance from a style object, first trying to use the font-specification, the CSS information + font_instance* FaceFromStyle(SPStyle const *style); + + // Various functions to get a font_instance from different descriptions. + font_instance* FaceFromDescr(char const *family, char const *style); + font_instance* FaceFromUIStrings(char const *uiFamily, char const *uiStyle); + font_instance* FaceFromPangoString(char const *pangoString); + font_instance* FaceFromFontSpecification(char const *fontSpecification); + font_instance* Face(PangoFontDescription *descr, bool canFail=true); + font_instance* Face(char const *family, + int variant=PANGO_VARIANT_NORMAL, int style=PANGO_STYLE_NORMAL, + int weight=PANGO_WEIGHT_NORMAL, int stretch=PANGO_STRETCH_NORMAL, + int size=10, int spacing=0); + + /// Semi-private: tells the font_factory that the font_instance 'who' has died and should be removed from loadedFaces + void UnrefFace(font_instance* who); + + // internal + void AddInCache(font_instance *who); + + /// Add a directory from which to include additional fonts + void AddFontsDir(char const *utf8dir); + + /// Add a an additional font. + void AddFontFile(char const *utf8file); + +private: + void* loadedPtr; + + + // The following two commented out maps were an attempt to allow Inkscape to use font faces + // that could not be distinguished by CSS values alone. In practice, they never were that + // useful as PangoFontDescription, which is used throughout our code, cannot distinguish + // between faces anymore than raw CSS values (with the exception of two additional weight + // values). + // + // During various works, for example to handle font-family lists and fonts that are not + // installed on the system, the code has become less reliant on these maps. And in the work to + // catch style information to speed up start up times, the maps were not being filled. + // I've removed all code that used these maps as of Oct 2014 in the experimental branch. + // The commented out maps are left here as a reminder of the path that was attempted. + // + // One possible method to keep track of font faces would be to use the 'display name', keeping + // pointers to the appropriate PangoFontFace. The font_factory loadedFaces map indexing would + // have to be changed to incorporate 'display name' (InkscapeFontDescription?). + + + // These two maps are used for translating between what's in the UI and a pango + // font description. This is necessary because Pango cannot always + // reproduce these structures from the names it gave us in the first place. + + // Key: A string produced by font_factory::ConstructFontSpecification + // Value: The associated PangoFontDescription + // typedef std::map PangoStringToDescrMap; + // PangoStringToDescrMap fontInstanceMap; + + // Key: Family name in UI + Style name in UI + // Value: The associated string that should be produced with font_factory::ConstructFontSpecification + // typedef std::map UIStringToPangoStringMap; + // UIStringToPangoStringMap fontStringMap; +}; + + +#endif /* my_font_factory */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/FontInstance.cpp b/src/libnrtype/FontInstance.cpp new file mode 100644 index 0000000..a89716c --- /dev/null +++ b/src/libnrtype/FontInstance.cpp @@ -0,0 +1,998 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * fred + * bulia byak + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include +#include FT_OUTLINE_H +#include FT_BBOX_H +#include FT_TRUETYPE_TAGS_H +#include FT_TRUETYPE_TABLES_H +#include FT_GLYPH_H +#include FT_MULTIPLE_MASTERS_H + +#include + +#include + +#include <2geom/pathvector.h> +#include <2geom/path-sink.h> +#include "libnrtype/font-glyph.h" +#include "libnrtype/font-instance.h" + +#include "display/cairo-utils.h" // Inkscape::Pixbuf + +#ifndef USE_PANGO_WIN32 +/* + * Outline extraction + */ + +struct FT2GeomData { + FT2GeomData(Geom::PathBuilder &b, double s) + : builder(b) + , last(0, 0) + , scale(s) + {} + Geom::PathBuilder &builder; + Geom::Point last; + double scale; +}; + +// outline as returned by freetype +static int ft2_move_to(FT_Vector const *to, void * i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + // printf("m t=%f %f\n",p[0],p[1]); + user->builder.moveTo(p * user->scale); + user->last = p; + return 0; +} + +static int ft2_line_to(FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + // printf("l t=%f %f\n",p[0],p[1]); + user->builder.lineTo(p * user->scale); + user->last = p; + return 0; +} + +static int ft2_conic_to(FT_Vector const *control, FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y), c(control->x, control->y); + user->builder.quadTo(c * user->scale, p * user->scale); + // printf("b c=%f %f t=%f %f\n",c[0],c[1],p[0],p[1]); + user->last = p; + return 0; +} + +static int ft2_cubic_to(FT_Vector const *control1, FT_Vector const *control2, FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + Geom::Point c1(control1->x, control1->y); + Geom::Point c2(control2->x, control2->y); + // printf("c c1=%f %f c2=%f %f t=%f %f\n",c1[0],c1[1],c2[0],c2[1],p[0],p[1]); + //user->theP->CubicTo(p,3*(c1-user->last),3*(p-c2)); + user->builder.curveTo(c1 * user->scale, c2 * user->scale, p * user->scale); + user->last = p; + return 0; +} +#endif + +/* *** END #if HACK *** */ + +/* + * + */ + +font_instance::font_instance() +{ + //printf("font instance born\n"); + _ascent = _ascent_max = 0.8; + _descent = _descent_max = 0.2; + _xheight = 0.5; + + // Default baseline values, alphabetic is reference + _baselines[ SP_CSS_BASELINE_AUTO ] = 0.0; + _baselines[ SP_CSS_BASELINE_ALPHABETIC ] = 0.0; + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; +} + +font_instance::~font_instance() +{ + if ( parent ) { + parent->UnrefFace(this); + parent = nullptr; + } + + //printf("font instance death\n"); + if ( pFont ) { + FreeTheFace(); + g_object_unref(pFont); + pFont = nullptr; + } + + if ( descr ) { + pango_font_description_free(descr); + descr = nullptr; + } + + // if ( theFace ) FT_Done_Face(theFace); // owned by pFont. don't touch + theFace = nullptr; + + for (int i=0;ipangoFontCache,lf); + g_free(lf); + + XFORM identity = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + SetWorldTransform(parent->hScreenDC, &identity); + SetGraphicsMode(parent->hScreenDC, GM_COMPATIBLE); + SelectObject(parent->hScreenDC,theFace); + +#else + + theFace = pango_fc_font_lock_face(PANGO_FC_FONT(pFont)); + if ( theFace ) { + FT_Select_Charmap(theFace, ft_encoding_unicode); + FT_Select_Charmap(theFace, ft_encoding_symbol); + } + +#endif + +#ifndef USE_PANGO_WIN32 + if (loadgsub) { + readOpenTypeGsubTable( theFace, openTypeTables ); + fulloaded = true; + } + readOpenTypeFvarAxes( theFace, openTypeVarAxes ); + readOpenTypeSVGTable( theFace, openTypeSVGGlyphs ); + + if (openTypeSVGGlyphs.size() > 0 ) { + fontHasSVG = true; + } + +#if PANGO_VERSION_CHECK(1,41,1) +#if FREETYPE_MAJOR == 2 && FREETYPE_MINOR >= 8 // 2.8 does not seem to work even though it has some support. + + // 'font-variation-settings' support. + // The font returned from pango_fc_font_lock_face does not include variation settings. We must set them. + + // We need to: + // Extract axes with values from Pango font description. + // Replace default axis values with extracted values. + + char const *var = pango_font_description_get_variations( descr ); + if (var) { + + Glib::ustring variations(var); + + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( theFace ) && // Font has variables + FT_Get_MM_Var(theFace, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master(theFace, &mmtype) !=0) { // It's not an Adobe MM font + + // std::cout << " Multiple Masters: variables: " << mmvar->num_axis + // << " named styles: " << mmvar->num_namedstyles << std::endl; + + // Get the required values from Pango Font Description + // Need to check format of values from Pango, for the moment accept any format. + Glib::RefPtr regex = Glib::Regex::create("(\\w{4})=([-+]?\\d*\\.?\\d+([eE][-+]?\\d+)?)"); + Glib::MatchInfo matchInfo; + + const FT_UInt num_axis = openTypeVarAxes.size(); + FT_Fixed w[num_axis]; + for (int i = 0; i < num_axis; ++i) w[i] = 0; + + std::vector tokens = Glib::Regex::split_simple(",", variations); + for (auto token: tokens) { + + regex->match(token, matchInfo); + if (matchInfo.matches()) { + + float value = std::stod(matchInfo.fetch(2)); // Should clamp value + + // Translate the "named" axes. + Glib::ustring name = matchInfo.fetch(1); + if (name == "wdth") name = "Width" ; // 'font-stretch' + if (name == "wght") name = "Weight" ; // 'font-weight' + if (name == "opsz") name = "Optical size"; // 'font-optical-sizing' + if (name == "slnt") name = "Slant" ; // 'font-style' + if (name == "ital") name = "Italic" ; // 'font-style' + + auto it = openTypeVarAxes.find(name); + if (it != openTypeVarAxes.end()) { + it->second.set_val = value; + w[it->second.index] = value * 65536; + } + } + } + + // Set design coordinates + FT_Error err; + err = FT_Set_Var_Design_Coordinates (theFace, num_axis, w); + if (err) { + std::cerr << "font_instance::InitTheFace(): Error in call to FT_Set_Var_Design_Coordinates(): " << err << std::endl; + } + + // FT_Done_MM_Var(mmlib, mmvar); + } + } + +#endif // FreeType +#endif // Pango +#endif // !USE_PANGO_WIN32 + + FindFontMetrics(); + } + +#ifdef USE_PANGO_WIN32 + // Someone (probably pango or cairo) sets the world transform during initialization and does not reset it. + // Work around this by explicitly setting it again (even if the font is already initialized) + XFORM identity = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + SetWorldTransform(parent->hScreenDC, &identity); + SetGraphicsMode(parent->hScreenDC, GM_COMPATIBLE); + SelectObject(parent->hScreenDC,theFace); +#endif + +} + +void font_instance::FreeTheFace() +{ +#ifdef USE_PANGO_WIN32 + SelectObject(parent->hScreenDC,GetStockObject(SYSTEM_FONT)); + pango_win32_font_cache_unload(parent->pangoFontCache,theFace); +#else + pango_fc_font_unlock_face(PANGO_FC_FONT(pFont)); +#endif + theFace=nullptr; +} + +void font_instance::InstallFace(PangoFont* iFace) +{ + if ( !iFace ) { + return; + } + pFont=iFace; + iFace = nullptr; + + InitTheFace(); + + if ( pFont && IsOutlineFont() == false ) { + FreeTheFace(); + if ( pFont ) { + g_object_unref(pFont); + } + pFont=nullptr; + } +} + +bool font_instance::IsOutlineFont() +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); +#ifdef USE_PANGO_WIN32 + TEXTMETRIC tm; + return GetTextMetrics(parent->hScreenDC,&tm) && tm.tmPitchAndFamily&(TMPF_TRUETYPE|TMPF_DEVICE); +#else + return FT_IS_SCALABLE(theFace); +#endif +} + +int font_instance::MapUnicodeChar(gunichar c) +{ + int res = 0; + if ( pFont ) { +#ifdef USE_PANGO_WIN32 + res = pango_win32_font_get_glyph_index(pFont, c); +#else + theFace = pango_fc_font_lock_face(PANGO_FC_FONT(pFont)); + if ( c > 0xf0000 ) { + res = CLAMP(c, 0xf0000, 0x1fffff) - 0xf0000; + } else { + res = FT_Get_Char_Index(theFace, c); + } + pango_fc_font_unlock_face(PANGO_FC_FONT(pFont)); +#endif + } + return res; +} + + +#ifdef USE_PANGO_WIN32 +static inline Geom::Point pointfx_to_nrpoint(const POINTFX &p, double scale) +{ + return Geom::Point(*(long*)&p.x / 65536.0 * scale, + *(long*)&p.y / 65536.0 * scale); +} +#endif + +void font_instance::LoadGlyph(int glyph_id) +{ + if ( pFont == nullptr ) { + return; + } + InitTheFace(); +#ifndef USE_PANGO_WIN32 + if ( !FT_IS_SCALABLE(theFace) ) { + return; // bitmap font + } +#endif + + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + Geom::PathBuilder path_builder; + + if ( nbGlyph >= maxGlyph ) { + maxGlyph=2*nbGlyph+1; + glyphs=(font_glyph*)realloc(glyphs,maxGlyph*sizeof(font_glyph)); + } + font_glyph n_g; + n_g.pathvector=nullptr; + n_g.bbox[0]=n_g.bbox[1]=n_g.bbox[2]=n_g.bbox[3]=0; + n_g.h_advance = 0; + n_g.v_advance = 0; + n_g.h_width = 0; + n_g.v_width = 0; + bool doAdd=false; + +#ifdef USE_PANGO_WIN32 + +#ifndef GGO_UNHINTED // For compatibility with old SDKs. +#define GGO_UNHINTED 0x0100 +#endif + + MAT2 identity = {{0,1},{0,0},{0,0},{0,1}}; + OUTLINETEXTMETRIC otm; + GetOutlineTextMetrics(parent->hScreenDC, sizeof(otm), &otm); + GLYPHMETRICS metrics; + DWORD bufferSize=GetGlyphOutline (parent->hScreenDC, glyph_id, GGO_GLYPH_INDEX | GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + double scale=1.0/parent->fontSize; + n_g.h_advance = metrics.gmCellIncX * scale; + n_g.v_advance = otm.otmTextMetrics.tmHeight * scale; + n_g.h_width = metrics.gmBlackBoxX * scale; + n_g.v_width = metrics.gmBlackBoxY * scale; + if ( bufferSize == GDI_ERROR) { + // shit happened + } else if ( bufferSize == 0) { + // character has no visual representation, but is valid (eg whitespace) + doAdd=true; + } else { + char *buffer = new char[bufferSize]; + if ( GetGlyphOutline (parent->hScreenDC, glyph_id, GGO_GLYPH_INDEX | GGO_NATIVE | GGO_UNHINTED, &metrics, bufferSize, buffer, &identity) <= 0 ) { + // shit happened + } else { + // Platform SDK is rubbish, read KB87115 instead + DWORD polyOffset=0; + while ( polyOffset < bufferSize ) { + TTPOLYGONHEADER const *polyHeader=(TTPOLYGONHEADER const *)(buffer+polyOffset); + if (polyOffset+polyHeader->cb > bufferSize) break; + + if (polyHeader->dwType == TT_POLYGON_TYPE) { + path_builder.moveTo(pointfx_to_nrpoint(polyHeader->pfxStart, scale)); + DWORD curveOffset=polyOffset+sizeof(TTPOLYGONHEADER); + + while ( curveOffset < polyOffset+polyHeader->cb ) { + TTPOLYCURVE const *polyCurve=(TTPOLYCURVE const *)(buffer+curveOffset); + POINTFX const *p=polyCurve->apfx; + POINTFX const *endp=p+polyCurve->cpfx; + + switch (polyCurve->wType) { + case TT_PRIM_LINE: + while ( p != endp ) + path_builder.lineTo(pointfx_to_nrpoint(*p++, scale)); + break; + + case TT_PRIM_QSPLINE: + { + g_assert(polyCurve->cpfx >= 2); + + // The list of points specifies one or more control points and ends with the end point. + // The intermediate points (on the curve) are the points between the control points. + Geom::Point this_control = pointfx_to_nrpoint(*p++, scale); + while ( p+1 != endp ) { // Process all "midpoints" (all points except the last) + Geom::Point new_control = pointfx_to_nrpoint(*p++, scale); + path_builder.quadTo(this_control, (new_control+this_control)/2); + this_control = new_control; + } + Geom::Point end = pointfx_to_nrpoint(*p++, scale); + path_builder.quadTo(this_control, end); + } + break; + + case 3: // TT_PRIM_CSPLINE + g_assert(polyCurve->cpfx % 3 == 0); + while ( p != endp ) { + path_builder.curveTo(pointfx_to_nrpoint(p[0], scale), + pointfx_to_nrpoint(p[1], scale), + pointfx_to_nrpoint(p[2], scale)); + p += 3; + } + break; + } + curveOffset += sizeof(TTPOLYCURVE)+sizeof(POINTFX)*(polyCurve->cpfx-1); + } + } + polyOffset += polyHeader->cb; + } + doAdd=true; + } + delete [] buffer; + } +#else + if (FT_Load_Glyph (theFace, glyph_id, FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP)) { + // shit happened + } else { + if ( FT_HAS_HORIZONTAL(theFace) ) { + n_g.h_advance=((double)theFace->glyph->metrics.horiAdvance)/((double)theFace->units_per_EM); + n_g.h_width=((double)theFace->glyph->metrics.width)/((double)theFace->units_per_EM); + } else { + n_g.h_width=n_g.h_advance=((double)(theFace->bbox.xMax-theFace->bbox.xMin))/((double)theFace->units_per_EM); + } + if ( FT_HAS_VERTICAL(theFace) ) { + n_g.v_advance=((double)theFace->glyph->metrics.vertAdvance)/((double)theFace->units_per_EM); + n_g.v_width=((double)theFace->glyph->metrics.height)/((double)theFace->units_per_EM); + } else { + // CSS3 Writing modes dictates that if vertical font metrics are missing we must + // synthisize them. No method is specified. The SVG 1.1 spec suggests using the em + // height (which is not theFace->height as that includes leading). The em height + // is ascender + descender (descender positive). Note: The "Requirements for + // Japanese Text Layout" W3C document says that Japanese kanji should be "set + // solid" which implies that vertical (and horizontal) advance should be 1em. + n_g.v_width=n_g.v_advance= 1.0; + } + if ( theFace->glyph->format == ft_glyph_format_outline ) { + FT_Outline_Funcs ft2_outline_funcs = { + ft2_move_to, + ft2_line_to, + ft2_conic_to, + ft2_cubic_to, + 0, 0 + }; + FT2GeomData user(path_builder, 1.0/((double)theFace->units_per_EM)); + FT_Outline_Decompose (&theFace->glyph->outline, &ft2_outline_funcs, &user); + } + doAdd=true; + } +#endif + path_builder.flush(); + + if ( doAdd ) { + Geom::PathVector pv = path_builder.peek(); + // close all paths + for (auto & i : pv) { + i.close(); + } + if ( !pv.empty() ) { + n_g.pathvector = new Geom::PathVector(pv); + Geom::OptRect bounds = bounds_exact(*n_g.pathvector); + if (bounds) { + n_g.bbox[0] = bounds->left(); + n_g.bbox[1] = bounds->top(); + n_g.bbox[2] = bounds->right(); + n_g.bbox[3] = bounds->bottom(); + } + } + glyphs[nbGlyph]=n_g; + id_to_no[glyph_id]=nbGlyph; + nbGlyph++; + } + } else { + } +} + +bool font_instance::FontMetrics(double &ascent,double &descent,double &xheight) +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } + + ascent = _ascent; + descent = _descent; + xheight = _xheight; + + return true; +} + +bool font_instance::FontDecoration( double &underline_position, double &underline_thickness, + double &linethrough_position, double &linethrough_thickness) +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( !GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) { + return false; + } + double scale=1.0/parent->fontSize; + underline_position = fabs(otm.otmsUnderscorePosition *scale); + underline_thickness = fabs(otm.otmsUnderscoreSize *scale); + linethrough_position = fabs(otm.otmsStrikeoutPosition *scale); + linethrough_thickness = fabs(otm.otmsStrikeoutSize *scale); +#else + if ( theFace->units_per_EM == 0 ) { + return false; // bitmap font + } + underline_position = fabs(((double)theFace->underline_position )/((double)theFace->units_per_EM)); + underline_thickness = fabs(((double)theFace->underline_thickness)/((double)theFace->units_per_EM)); + // there is no specific linethrough information, mock it up from other font fields + linethrough_position = fabs(((double)theFace->ascender / 3.0 )/((double)theFace->units_per_EM)); + linethrough_thickness = fabs(((double)theFace->underline_thickness)/((double)theFace->units_per_EM)); +#endif + return true; +} + + +bool font_instance::FontSlope(double &run, double &rise) +{ + run = 0.0; + rise = 1.0; + + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } + +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( !GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) return false; + run=otm.otmsCharSlopeRun; + rise=otm.otmsCharSlopeRise; +#else + if ( !FT_IS_SCALABLE(theFace) ) { + return false; // bitmap font + } + + TT_HoriHeader *hhea = (TT_HoriHeader*)FT_Get_Sfnt_Table(theFace, ft_sfnt_hhea); + if (hhea == nullptr) { + return false; + } + run = hhea->caret_Slope_Run; + rise = hhea->caret_Slope_Rise; +#endif + return true; +} + +Geom::OptRect font_instance::BBox(int glyph_id) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no = id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no < 0 ) { + return Geom::OptRect(); + } else { + Geom::Point rmin(glyphs[no].bbox[0],glyphs[no].bbox[1]); + Geom::Point rmax(glyphs[no].bbox[2],glyphs[no].bbox[3]); + return Geom::Rect(rmin, rmax); + } +} + +Geom::PathVector* font_instance::PathVector(int glyph_id) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no = id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no < 0 ) return nullptr; + return glyphs[no].pathvector; +} + +Inkscape::Pixbuf* font_instance::PixBuf(int glyph_id) +{ + Inkscape::Pixbuf* pixbuf = nullptr; + + auto glyph_iter = openTypeSVGGlyphs.find(glyph_id); + if (glyph_iter != openTypeSVGGlyphs.end()) { + + // Glyphs are layed out in the +x, -y quadrant (assuming viewBox origin is 0,0). + // We need to shift the viewBox by the height inorder to generate pixbuf! + // To do: glyphs must draw overflow so we actually need larger pixbuf! + // To do: cache pixbuf. + // To do: Error handling. + + pixbuf = glyph_iter->second.pixbuf; + if (!pixbuf) { + Glib::ustring svg = glyph_iter->second.svg; + + // std::cout << svg << std::endl; + + // Create new viewbox which determines pixbuf size. + Glib::ustring viewbox("viewBox=\"0 "); + viewbox += std::to_string(-_design_units); + viewbox += " "; + viewbox += std::to_string(_design_units); + viewbox += " "; + viewbox += std::to_string(_design_units*2); + viewbox += "\""; + + // Search for existing viewbox + Glib::RefPtr regex = + Glib::Regex::create("viewBox=\"\\s*(\\d*\\.?\\d+)\\s*,?\\s*(\\d*\\.?\\d+)\\s*,?\\s*(\\d+\\.?\\d+)\\s*,?\\s*(\\d+\\.?\\d+)\\s*\""); + Glib::MatchInfo matchInfo; + regex->match(svg, matchInfo); + + if (matchInfo.matches()) { + // We have viewBox! We must transform so viewBox corresponds to design units. + + // Replace viewbox + svg = regex->replace_literal(svg, 0, viewbox, static_cast(0)); + + // Insert group with required transform to map glyph to new viewbox. + double x = std::stod(matchInfo.fetch(1)); + double y = std::stod(matchInfo.fetch(2)); + double w = std::stod(matchInfo.fetch(3)); + double h = std::stod(matchInfo.fetch(4)); + // std::cout << " x: " << x + // << " y: " << y + // << " w: " << w + // << " h: " << h << std::endl; + + if (w <= 0.0 or h <= 0.0) { + std::cerr << "font_instance::PixBuf: Invalid glyph width or height!" << std::endl; + } else { + + double xscale = _design_units/w; + double yscale = _design_units/h; + double xtrans = _design_units/w * x; + double ytrans = _design_units/h * y; + + if (xscale != 1.0 || yscale != 1.0) { + Glib::ustring group = ""; + + // Insert start group tag after initial + Glib::RefPtr regex = Glib::Regex::create("<\\s*svg.*?>"); + regex->match(svg, matchInfo); + if (matchInfo.matches()) { + int start = -1; + int end = -1; + matchInfo.fetch_pos(0, start, end); + svg.insert(end, group); + } else { + std::cerr << "font_instance::PixBuf: Could not find tag!" << std::endl; + } + + // Insert end group tag before final (To do: make sure it is final ) + regex = Glib::Regex::create("<\\s*\\/\\s*svg.*?>"); + regex->match(svg, matchInfo); + if (matchInfo.matches()) { + int start = -1; + int end = -1; + matchInfo.fetch_pos(0, start, end); + svg.insert(start, ""); + } else { + std::cerr << "font_instance::PixBuf: Could not find tag!" << std::endl; + } + } + } + + } else { + // No viewBox! We insert one. (To do: Look at 'width' and 'height' to see if we must scale.) + Glib::RefPtr regex = Glib::Regex::create("<\\s*svg"); + viewbox.insert(0, "replace_literal(svg, 0, viewbox, static_cast(0)); + } + + // std::cout << svg << std::endl; + + // Finally create pixbuf! + pixbuf = Inkscape::Pixbuf::create_from_buffer(svg); + + // And cache it. + glyph_iter->second.pixbuf = pixbuf; + } + } + + return pixbuf; +} + +double font_instance::Advance(int glyph_id, bool vertical) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no=id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no >= 0 ) { + if ( vertical ) { + return glyphs[no].v_advance; + } else { + return glyphs[no].h_advance; + } + } + return 0; +} + +// Internal function to find baselines +void font_instance::FindFontMetrics() { + + // CSS2 recommends using the OS/2 values sTypoAscender and sTypoDescender for the Typographic + // ascender and descender values: + // http://www.w3.org/TR/CSS2/visudet.html#sTypoAscender + // On Windows, the typographic ascender and descender are taken from the otmMacAscent and + // otmMacDescent values: + // http://microsoft.public.win32.programmer.gdi.narkive.com/LV6k4BDh/msdn-documentation-outlinetextmetrics-clarification + // The otmAscent and otmDescent values are the maximum ascent and maximum descent of all the + // glyphs in a font. + if ( theFace ) { + +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) { + double scale=1.0/parent->fontSize; + _ascent = fabs(otm.otmMacAscent * scale); + _descent = fabs(otm.otmMacDescent * scale); + _xheight = fabs(otm.otmsXHeight * scale); + _ascent_max = fabs(otm.otmAscent * scale); + _descent_max = fabs(otm.otmDescent * scale); + _design_units = parent->fontSize; + + // In CSS em size is ascent + descent... which should be 1. If not, + // adjust so it is. + double em = _ascent + _descent; + if( em > 0 ) { + _ascent /= em; + _descent /= em; + } + + // May not be necessary but if OS/2 table missing or not version 2 or higher, + // xheight might be zero. + if( _xheight == 0.0 ) { + _xheight = 0.5; + } + + // Baselines defined relative to alphabetic. + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; // Recommendation + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; // Guess + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; // Guess + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; // Definition + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; // Definition + + + MAT2 identity = {{0,1},{0,0},{0,0},{0,1}}; + GLYPHMETRICS metrics; + int retval; + + // Better math baseline: + // Try center of minus sign + retval = GetGlyphOutline (parent->hScreenDC, 0x2212, GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + // If no minus sign, try hyphen + if( retval <= 0 ) + retval = GetGlyphOutline (parent->hScreenDC, '-', GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + + if( retval > 0 ) { + double math = (metrics.gmptGlyphOrigin.y + 0.5 * metrics.gmBlackBoxY) * scale; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = math; + } + + // Find hanging baseline... assume it is at top of 'म'. + retval = GetGlyphOutline (parent->hScreenDC, 0x092E, GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + if( retval > 0 ) { + double hanging = metrics.gmptGlyphOrigin.y * scale; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = hanging; + } + } + +#else + + if ( theFace->units_per_EM != 0 ) { // If zero then it's a bitmap font. + + TT_OS2* os2 = (TT_OS2*)FT_Get_Sfnt_Table( theFace, ft_sfnt_os2 ); + if( os2 ) { + _ascent = fabs(((double)os2->sTypoAscender) / ((double)theFace->units_per_EM)); + _descent = fabs(((double)os2->sTypoDescender)/ ((double)theFace->units_per_EM)); + } else { + _ascent = fabs(((double)theFace->ascender) / ((double)theFace->units_per_EM)); + _descent = fabs(((double)theFace->descender) / ((double)theFace->units_per_EM)); + } + _ascent_max = fabs(((double)theFace->ascender) / ((double)theFace->units_per_EM)); + _descent_max = fabs(((double)theFace->descender) / ((double)theFace->units_per_EM)); + _design_units = theFace->units_per_EM; + + // In CSS em size is ascent + descent... which should be 1. If not, + // adjust so it is. + double em = _ascent + _descent; + if( em > 0 ) { + _ascent /= em; + _descent /= em; + } + + // x-height + if( os2 && os2->version >= 0x0002 && os2->version != 0xffffu ) { + // Only os/2 version 2 and above have sxHeight, 0xffff marks "old Mac fonts" without table + _xheight = fabs(((double)os2->sxHeight) / ((double)theFace->units_per_EM)); + } else { + // Measure 'x' height in font. Recommended option by XSL standard if no sxHeight. + FT_UInt index = FT_Get_Char_Index( theFace, 'x' ); + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + _xheight = (fabs)(((double)theFace->glyph->metrics.height/(double)theFace->units_per_EM)); + } else { + // No 'x' in font! + _xheight = 0.5; + } + } + + // Baselines defined relative to alphabetic. + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; // Recommendation + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; // Guess + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; // Guess + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; // Definition + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; // Definition + + // Better math baseline: + // Try center of minus sign + FT_UInt index = FT_Get_Char_Index( theFace, 0x2212 ); //'−' + // If no minus sign, try hyphen + if( index == 0 ) + index = FT_Get_Char_Index( theFace, '-' ); + + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + FT_Glyph aglyph; + FT_Get_Glyph( theFace->glyph, &aglyph ); + FT_BBox acbox; + FT_Glyph_Get_CBox( aglyph, FT_GLYPH_BBOX_UNSCALED, &acbox ); + double math = (acbox.yMin + acbox.yMax)/2.0/(double)theFace->units_per_EM; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = math; + // std::cout << "Math baseline: - bbox: y_min: " << acbox.yMin + // << " y_max: " << acbox.yMax + // << " math: " << math << std::endl; + FT_Done_Glyph(aglyph); + } + + // Find hanging baseline... assume it is at top of 'म'. + index = FT_Get_Char_Index( theFace, 0x092E ); // 'म' + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + FT_Glyph aglyph; + FT_Get_Glyph( theFace->glyph, &aglyph ); + FT_BBox acbox; + FT_Glyph_Get_CBox( aglyph, FT_GLYPH_BBOX_UNSCALED, &acbox ); + double hanging = (double)acbox.yMax/(double)theFace->units_per_EM; + _baselines[ SP_CSS_BASELINE_HANGING ] = hanging; + // std::cout << "Hanging baseline: प: " << hanging << std::endl; + FT_Done_Glyph(aglyph); + } + } +#endif + // const gchar *family = pango_font_description_get_family(descr); + // std::cout << "Font: " << (family?family:"null") << std::endl; + // std::cout << " ascent: " << _ascent << std::endl; + // std::cout << " descent: " << _descent << std::endl; + // std::cout << " x-height: " << _xheight << std::endl; + // std::cout << " max ascent: " << _ascent_max << std::endl; + // std::cout << " max descent: " << _descent_max << std::endl; + // std::cout << " Baselines:" << std::endl; + // std::cout << " alphabetic: " << _baselines[ SP_CSS_BASELINE_ALPHABETIC ] << std::endl; + // std::cout << " ideographic: " << _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] << std::endl; + // std::cout << " hanging: " << _baselines[ SP_CSS_BASELINE_HANGING ] << std::endl; + // std::cout << " math: " << _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] << std::endl; + // std::cout << " central: " << _baselines[ SP_CSS_BASELINE_CENTRAL ] << std::endl; + // std::cout << " middle: " << _baselines[ SP_CSS_BASELINE_MIDDLE ] << std::endl; + // std::cout << " text_before: " << _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] << std::endl; + // std::cout << " text_after: " << _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] << std::endl; + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/Layout-TNG-Compute.cpp b/src/libnrtype/Layout-TNG-Compute.cpp new file mode 100644 index 0000000..ed604d5 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Compute.cpp @@ -0,0 +1,2318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::Calculator - text layout engine meaty bits + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "Layout-TNG.h" +#include "style.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include "object/sp-object.h" +#include "Layout-TNG-Scanline-Maker.h" +#include +#include "livarot/Shape.h" + +namespace Inkscape { +namespace Text { + +//#define DEBUG_LAYOUT_TNG_COMPUTE +//#define DEBUG_GLYPH + +//#define IFTRACE(_code) _code +#define IFTRACE(_code) + +#define TRACE(_args) IFTRACE(g_print _args) + +/** \brief private to Layout. Does the real work of text flowing. + +This class does a standard greedy paragraph wrapping algorithm. + +Very high-level overview: + +
+foreach(paragraph) {
+  call pango_itemize() (_buildPangoItemizationForPara())
+  break into spans, without dealing with wrapping (_buildSpansForPara())
+  foreach(line in flow shape) {
+    foreach(chunk in flow shape) {   (in _buildChunksInScanRun())
+      // this inner loop in _measureUnbrokenSpan()
+      if the line height changed discard the line and start again
+      keep adding characters until we run out of space in the chunk, then back up to the last word boundary
+      (do sensible things if there is no previous word break)
+    }
+    push all the glyphs, chars, spans, chunks and line to output (not completely trivial because we must draw rtl in character order) (in _outputLine())
+  }
+  push the paragraph (in calculate())
+}
+
+ +...and all of that needs to work vertically too, and with all the little details that make life annoying +*/ +class Layout::Calculator +{ + class SpanPosition; + friend class SpanPosition; + Layout &_flow; + ScanlineMaker *_scanline_maker; + unsigned _current_shape_index; /// index into Layout::_input_wrap_shapes + PangoContext *_pango_context; + Direction _block_progression; + + /** + * For y= attributes in tspan elements et al, we do the adjustment by moving each + * glyph individually by this number. The spec means that this is maintained across + * paragraphs. + * + * To do non-flow text layout, only the first "y" attribute is normally used. If there is only one + * "y" attribute in a other than the first , it is ignored. This allows Inkscape to + * insert a new line anywhere. On output, the Inkscape determined "y" is written out so other SVG + * viewers know where to place the . + */ + double _y_offset; + + /** to stop pango from hinting its output, the font factory creates all fonts very large. + All numbers returned from pango have to be divided by this number \em and divided by + PANGO_SCALE. See font_factory::font_factory(). */ + double _font_factory_size_multiplier; + + /** Temporary storage associated with each item in Layout::_input_stream. */ + struct InputItemInfo { + bool in_sub_flow; + Layout *sub_flow; // this is only set for the first input item in a sub-flow + + InputItemInfo() : in_sub_flow(false), sub_flow(nullptr) {} + + /* fixme: I don't like the fact that InputItemInfo etc. use the default copy constructor and + * operator= (and thus don't involve incrementing reference counts), yet they provide a free method + * that does delete or Unref. + * + * I suggest using the garbage collector to manage deletion. + */ + void free() + { + if (sub_flow) { + delete sub_flow; + sub_flow = nullptr; + } + } + }; + + /** Temporary storage associated with each item returned by the call to + pango_itemize(). */ + struct PangoItemInfo { + PangoItem *item; + font_instance *font; + + PangoItemInfo() : item(nullptr), font(nullptr) {} + + /* fixme: I don't like the fact that InputItemInfo etc. use the default copy constructor and + * operator= (and thus don't involve incrementing reference counts), yet they provide a free method + * that does delete or Unref. + * + * I suggest using the garbage collector to manage deletion. + */ + void free() + { + if (item) { + pango_item_free(item); + item = nullptr; + } + if (font) { + font->Unref(); + font = nullptr; + } + } + }; + + + /** These spans have approximately the same definition as that used for + * Layout::Span (constant font, direction, etc), except that they are from + * before we have located the line breaks, so bear no relation to chunks. + * They are guaranteed to be in at most one PangoItem (spans with no text in + * them will not have an associated PangoItem), exactly one input object and + * will only have one change of x, y, dx, dy or rotate attribute, which will + * be at the beginning. An UnbrokenSpan can cross a chunk boundary, c.f. + * BrokenSpan. + */ + struct UnbrokenSpan { + PangoGlyphString *glyph_string; + int pango_item_index; /// index into _para.pango_items, or -1 if this is style only + unsigned input_index; /// index into Layout::_input_stream + Glib::ustring::const_iterator input_stream_first_character; + double font_size; + FontMetrics line_height; /// This is not the CSS line-height attribute! + double line_height_multiplier; /// calculated from the font-height css property + double baseline_shift; /// calculated from the baseline-shift css property + SPCSSTextOrientation text_orientation; + unsigned text_bytes; + unsigned char_index_in_para; /// the index of the first character in this span in the paragraph, for looking up char_attributes + SVGLength x, y, dx, dy, rotate; // these are reoriented copies of the attributes. We change span when we encounter one. + + UnbrokenSpan() : glyph_string(nullptr) {} + void free() + { + if (glyph_string) + pango_glyph_string_free(glyph_string); + glyph_string = nullptr; + } + }; + + + /** Used to provide storage for anything that applies to the current + paragraph only. Since we're only processing one paragraph at a time, + there's only one instantiation of this struct, on the stack of + calculate(). */ + struct ParagraphInfo { + unsigned first_input_index; ///< Index into Layout::_input_stream. + Direction direction; + Alignment alignment; + std::vector input_items; + std::vector pango_items; + std::vector char_attributes; ///< For every character in the paragraph. + std::vector unbroken_spans; + + template static void free_sequence(T &seq) + { + for (typename T::iterator it(seq.begin()); it != seq.end(); ++it) { + it->free(); + } + seq.clear(); + } + + void free() + { + free_sequence(input_items); + free_sequence(pango_items); + free_sequence(unbroken_spans); + } + }; + + + /** + * A useful little iterator for moving char-by-char across spans. + */ + struct UnbrokenSpanPosition { + std::vector::iterator iter_span; + unsigned char_byte; + unsigned char_index; + + void increment(); ///< Step forward by one character. + + inline bool operator== (UnbrokenSpanPosition const &other) const + {return char_byte == other.char_byte && iter_span == other.iter_span;} + inline bool operator!= (UnbrokenSpanPosition const &other) const + {return char_byte != other.char_byte || iter_span != other.iter_span;} + }; + + /** + * The line breaking algorithm will convert each UnbrokenSpan into one + * or more of these. A BrokenSpan will never cross a chunk boundary, + * c.f. UnbrokenSpan. + */ + struct BrokenSpan { + UnbrokenSpanPosition start; + UnbrokenSpanPosition end; // the end of this will always be the same as the start of the next + unsigned start_glyph_index; + unsigned end_glyph_index; + double width; + unsigned whitespace_count; + bool ends_with_whitespace; + double each_whitespace_width; + double letter_spacing; // Save so we can subtract from width at end of line (for center justification) + double word_spacing; + void setZero(); + }; + + /** The definition of a chunk used here is the same as that used in Layout: + A collection of contiguous broken spans on the same line. (One chunk per line + unless shape splits line into several sections... then one chunk per section. */ + struct ChunkInfo { + std::vector broken_spans; + double scanrun_width; + double text_width; ///< Total width used by the text (excluding justification). + double x; + int whitespace_count; + }; + + void _buildPangoItemizationForPara(ParagraphInfo *para) const; + static double _computeFontLineHeight( SPStyle const *style ); // Returns line_height_multiplier + unsigned _buildSpansForPara(ParagraphInfo *para) const; + bool _goToNextWrapShape(); + void _createFirstScanlineMaker(); + + bool _findChunksForLine(ParagraphInfo const ¶, + UnbrokenSpanPosition *start_span_pos, + std::vector *chunk_info, + FontMetrics *line_box_height, + FontMetrics const *strut_height); + + bool _buildChunksInScanRun(ParagraphInfo const ¶, + UnbrokenSpanPosition const &start_span_pos, + ScanlineMaker::ScanRun const &scan_run, + std::vector *chunk_info, + FontMetrics *line_height) const; + + bool _measureUnbrokenSpan(ParagraphInfo const ¶, + BrokenSpan *span, + BrokenSpan *last_break_span, + BrokenSpan *last_emergency_break_span, + double maximum_width) const; + + double _getChunkLeftWithAlignment(ParagraphInfo const ¶, + std::vector::const_iterator it_chunk, + double *add_to_each_whitespace) const; + + void _outputLine(ParagraphInfo const ¶, + FontMetrics const &line_height, + std::vector const &chunk_info, + bool hidden); + + static inline PangoLogAttr const &_charAttributes(ParagraphInfo const ¶, + UnbrokenSpanPosition const &span_pos) + { + return para.char_attributes[span_pos.iter_span->char_index_in_para + span_pos.char_index]; + } + +#ifdef DEBUG_LAYOUT_TNG_COMPUTE + static void dumpPangoItemsOut(ParagraphInfo *para); + static void dumpUnbrokenSpans(ParagraphInfo *para); +#endif //DEBUG_LAYOUT_TNG_COMPUTE + +public: + Calculator(Layout *text_flow) + : _flow(*text_flow) {} + + bool calculate(); +}; + + +/** + * Computes the width of a single UnbrokenSpan (pointed to by span->start.iter_span) + * and outputs its vital statistics into the other fields of \a span. + * Measuring will stop if maximum_width is reached and in that case the + * function will return false. In other cases where a line break must be + * done immediately the function will also return false. On return + * \a last_break_span will contain the vital statistics for the span only + * up to the last line breaking change. If there are no line breaking + * characters in the span then \a last_break_span will not be altered. + * Similarly, \a last_emergency_break_span will contain the vital + * statistics for the span up to the last inter-character boundary, + * or will be unaltered if there is none. + * + * An unbroken span corresponds to at most one PangoItem + */ +bool Layout::Calculator::_measureUnbrokenSpan(ParagraphInfo const ¶, + BrokenSpan *span, + BrokenSpan *last_break_span, + BrokenSpan *last_emergency_break_span, + double maximum_width) const +{ + TRACE((" start _measureUnbrokenSpan %g\n", maximum_width)); + span->setZero(); + + if (span->start.iter_span->dx._set && span->start.char_byte == 0){ + if(para.direction == RIGHT_TO_LEFT){ + span->width -= span->start.iter_span->dx.computed; + } else { + span->width += span->start.iter_span->dx.computed; + } + } + + if (span->start.iter_span->pango_item_index == -1) { + // if this is a style-only span there's no text in it + // so we don't need to do very much at all + span->end.iter_span++; + return true; + } + + if (_flow._input_stream[span->start.iter_span->input_index]->Type() == CONTROL_CODE) { + + InputStreamControlCode const *control_code = static_cast(_flow._input_stream[span->start.iter_span->input_index]); + + if (control_code->code == SHAPE_BREAK || control_code->code == PARAGRAPH_BREAK) { + *last_emergency_break_span = *last_break_span = *span; + return false; + } + + if (control_code->code == ARBITRARY_GAP) { // Not used! + if (span->width + control_code->width > maximum_width) + return false; + TRACE((" fitted control code, width = %f\n", control_code->width)); + span->width += control_code->width; + span->end.increment(); + } + return true; + } + + if (_flow._input_stream[span->start.iter_span->input_index]->Type() != TEXT_SOURCE) + return true; // never happens + + InputStreamTextSource const *text_source = static_cast(_flow._input_stream[span->start.iter_span->input_index]); + + if (_directions_are_orthogonal(_block_progression, text_source->styleGetBlockProgression())) { + // TODO: block-progression altered in the middle + // Measure the precomputed flow from para.input_items + span->end.iter_span++; // for now, skip to the next span + return true; + } + + // a normal span going with a normal block-progression + double font_size_multiplier = span->start.iter_span->font_size / (PANGO_SCALE * _font_factory_size_multiplier); + double soft_hyphen_glyph_width = 0.0; + bool soft_hyphen_in_word = false; + bool is_soft_hyphen = false; + IFTRACE(int char_count = 0); + + // if we're not at the start of the span we need to pre-init glyph_index + span->start_glyph_index = 0; + while (span->start_glyph_index < (unsigned)span->start.iter_span->glyph_string->num_glyphs + && span->start.iter_span->glyph_string->log_clusters[span->start_glyph_index] < (int)span->start.char_byte) + span->start_glyph_index++; + span->end_glyph_index = span->start_glyph_index; + + // go char-by-char summing the width, while keeping track of the previous break point + do { + PangoLogAttr const &char_attributes = _charAttributes(para, span->end); + + // guint32 c = *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte); + // std::cout << " char_byte: " << span->end.char_byte + // << " char_index: " << span->end.char_index + // << " c: " << c << " " << char(c==10 ? 'â¤' : c) + // << " line: " << std::boolalpha << char_attributes.is_line_break + // << " mandatory: " << std::boolalpha << char_attributes.is_mandatory_break // Note, break is before character! + // << " char: " << std::boolalpha << char_attributes.is_char_break + // << std::endl; + + if (char_attributes.is_mandatory_break && span->end != span->start) { + TRACE((" is_mandatory_break ************\n")); + *last_emergency_break_span = *last_break_span = *span; + TRACE((" span %ld end of para; width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + return false; + } + + if (char_attributes.is_line_break) { + TRACE((" is_line_break ************\n")); + // a suitable position to break at, record where we are + *last_emergency_break_span = *last_break_span = *span; + if (soft_hyphen_in_word) { + // if there was a previous soft hyphen we're not going to need it any more so we can remove it + span->width -= soft_hyphen_glyph_width; + if (!is_soft_hyphen) + soft_hyphen_in_word = false; + } + } else if (char_attributes.is_char_break) { + *last_emergency_break_span = *span; + } + // todo: break between chars if necessary (ie no word breaks present) when doing rectangular flowing + + // sum the glyph widths, letter spacing, word spacing, and textLength adjustment to get the character width + double char_width = 0.0; + while (span->end_glyph_index < (unsigned)span->end.iter_span->glyph_string->num_glyphs + && span->end.iter_span->glyph_string->log_clusters[span->end_glyph_index] <= (int)span->end.char_byte) { + + PangoGlyphInfo *info = &(span->end.iter_span->glyph_string->glyphs[span->end_glyph_index]); + double glyph_width = font_size_multiplier * info->geometry.width; + + // Advance does not include kerning but Pango gives wrong advances for vertical text + // with upright orientation (pre 1.44.0). + font_instance *font = para.pango_items[span->end.iter_span->pango_item_index].font; + double font_size = span->start.iter_span->font_size; + double glyph_h_advance = font_size * font->Advance(info->glyph, false); + double glyph_v_advance = font_size * font->Advance(info->glyph, true ); + + if (_block_progression == LEFT_TO_RIGHT || _block_progression == RIGHT_TO_LEFT) { + // Vertical text + + if( text_source->style->text_orientation.computed == SP_CSS_TEXT_ORIENTATION_SIDEWAYS || + (text_source->style->text_orientation.computed == SP_CSS_TEXT_ORIENTATION_MIXED && + para.pango_items[span->end.iter_span->pango_item_index].item->analysis.gravity == PANGO_GRAVITY_SOUTH) ) { + // Sideways orientation + char_width += glyph_width; + } else { + // Upright orientation + guint32 c = *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte); + if (g_unichar_type (c) != G_UNICODE_NON_SPACING_MARK) { + // Non-spacing marks should not contribute to width. Fonts may not report the correct advance, especially if the 'vmtx' table is missing. + if (pango_version_check(1,44,0) != nullptr) { + // Pango >= 1.44.0 + char_width += glyph_width; + } else { + // Pango < 1.44.0 glyph_width returned is horizontal width, not vertical. + char_width += glyph_v_advance; + } + } + } + } else { + // Horizontal text + char_width += glyph_width; + } + span->end_glyph_index++; + } + + if (char_attributes.is_cursor_position) + char_width += text_source->style->letter_spacing.computed * _flow.getTextLengthMultiplierDue(); + if (char_attributes.is_white) + char_width += text_source->style->word_spacing.computed * _flow.getTextLengthMultiplierDue(); + char_width += _flow.getTextLengthIncrementDue(); + span->width += char_width; + IFTRACE(char_count++); + + if (char_attributes.is_white) { + span->whitespace_count++; + span->each_whitespace_width = char_width; + } + span->ends_with_whitespace = char_attributes.is_white; + + is_soft_hyphen = (UNICODE_SOFT_HYPHEN == *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte)); + if (is_soft_hyphen) + soft_hyphen_glyph_width = char_width; + + // Go to next character (resets end.char_byte to zero if at end) + span->end.increment(); + + // Width should not include letter_spacing (or word_spacing) after last letter at end of line. + // word_spacing is attached to white space that is already removed from line end (?) + double test_width = span->width - text_source->style->letter_spacing.computed; + + // Save letter_spacing and word_spacing for subtraction later if span is last span in line. + span->letter_spacing = text_source->style->letter_spacing.computed; + span->word_spacing = text_source->style->word_spacing.computed; + + if (test_width > maximum_width && !char_attributes.is_white) { // whitespaces don't matter, we can put as many as we want at eol + TRACE((" span %ld exceeded scanrun; width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + return false; + } + + } while (span->end.char_byte != 0); // while we haven't wrapped to the next span + + TRACE((" fitted span %ld width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + TRACE((" end _measureUnbrokenSpan %g\n", maximum_width)); + return true; +} + +/* *********************************************************************************************************/ +// Per-line functions (output) + +/** Uses the paragraph alignment and the chunk information to work out + * where the actual left of the final chunk must be. Also sets + * \a add_to_each_whitespace to be the amount of x to add at each + * whitespace character to make full justification work. + */ +double Layout::Calculator::_getChunkLeftWithAlignment(ParagraphInfo const ¶, + std::vector::const_iterator it_chunk, + double *add_to_each_whitespace) const +{ + *add_to_each_whitespace = 0.0; + if (_flow._input_wrap_shapes.empty()) { + switch (para.alignment) { + case FULL: + case LEFT: + default: + return it_chunk->x; + case RIGHT: + return it_chunk->x - it_chunk->text_width; + case CENTER: + return it_chunk->x - it_chunk->text_width/ 2; + } + } + + switch (para.alignment) { + case FULL: + if (!it_chunk->broken_spans.empty() + && it_chunk->broken_spans.back().end.iter_span != para.unbroken_spans.end()) { // don't justify the last chunk in the para + if (it_chunk->whitespace_count) + *add_to_each_whitespace = (it_chunk->scanrun_width - it_chunk->text_width) / it_chunk->whitespace_count; + //else + //add_to_each_charspace = something + } + return it_chunk->x; + case LEFT: + default: + return it_chunk->x; + case RIGHT: + return it_chunk->x + it_chunk->scanrun_width - it_chunk->text_width; + case CENTER: + return it_chunk->x + (it_chunk->scanrun_width - it_chunk->text_width) / 2; + } +} + +/** + * Once we've got here we have finished making changes to the line + * and are ready to output the final result to #_flow. + * This method takes its input parameters and does that. + */ +void Layout::Calculator::_outputLine(ParagraphInfo const ¶, + FontMetrics const &line_height, + std::vector const &chunk_info, + bool hidden) +{ + TRACE((" Start _outputLine: ascent %f, descent %f, top of box %f\n", line_height.ascent, line_height.descent, _scanline_maker->yCoordinate() )); + if (chunk_info.empty()) { + TRACE((" line too short to fit anything on it, go to next\n")); + return; + } + + // we've finished fiddling about with ascents and descents: create the output + TRACE((" found line fit; creating output\n")); + Layout::Line new_line; + new_line.in_paragraph = _flow._paragraphs.size() - 1; + new_line.baseline_y = _scanline_maker->yCoordinate(); + new_line.hidden = hidden; + + // The y coordinate is at the beginning edge of the line box (top for horizontal text, left + // edge for vertical lr text, right edge for vertical rl text. We align, by default to the + // alphabetic baseline for horizontal text and the central baseline for vertical text. + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + new_line.baseline_y -= 0.5 * line_height.emSize(); + } else if ( _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + new_line.baseline_y += 0.5 * line_height.emSize(); + } else { + new_line.baseline_y += line_height.getTypoAscent(); + } + + + TRACE((" initial new_line.baseline_y: %f\n", new_line.baseline_y )); + + new_line.in_shape = _current_shape_index; + _flow._lines.push_back(new_line); + + for (std::vector::const_iterator it_chunk = chunk_info.begin() ; it_chunk != chunk_info.end() ; it_chunk++) { + double add_to_each_whitespace; + // add the chunk to the list + Layout::Chunk new_chunk; + new_chunk.in_line = _flow._lines.size() - 1; + + TRACE((" New chunk: in_line: %d\n", new_chunk.in_line)); + if (hidden) { + new_chunk.left_x = it_chunk->x; // Don't align. We'll place below last shape. + } else { + new_chunk.left_x = _getChunkLeftWithAlignment(para, it_chunk, &add_to_each_whitespace); + } + + // we may also have y move orders to deal with here (dx, dy and rotate are done per span) + + // Comment updated: 23 July 2010: + // We must handle two cases: + // + // 1. Inkscape SVG where the first line is placed by the read-in "y" value and the + // rest are determined by 'font-size' and 'line-height' (and not by any + // y-kerning). s in this case are marked by sodipodi:role="line". This + // allows new lines to be inserted in the middle of a object. On output, + // new "y" values are calculated for each that represents a new line. Line + // spacing is already handled by the calling routine. + // + // 2. Plain SVG where each or is placed by its own "x" and "y" values. + // Note that in this case Inkscape treats each object with any included + // s as a single line of text. This can be confusing in the code below. + + if (!it_chunk->broken_spans.empty() // Not empty paragraph + && it_chunk->broken_spans.front().start.char_byte == 0 ) { // Beginning of unbroken span + + // If empty or new line (sodipode:role="line") + if( _flow._characters.empty() || + _flow._characters.back().chunk(&_flow).in_line != _flow._lines.size() - 1) { + + // This is the Inkscape SVG case. + // + // If "y" attribute is set, use it (initial "y" attributes in + // other than the first have already been stripped for + // marked with role="line", see sp-text.cpp: SPText::_buildLayoutInput). + // NOTE: for vertical text, "y" is the user-space "x" value. + if( it_chunk->broken_spans.front().start.iter_span->y._set ) { + + // Use set "y" attribute for baseline + new_line.baseline_y = it_chunk->broken_spans.front().start.iter_span->y.computed; + + TRACE((" chunk new_line.baseline_y: %f\n", new_line.baseline_y )); + + // Save baseline + _flow._lines.back().baseline_y = new_line.baseline_y; + + // Calculate new top of box... given specified baseline. + double top_of_line_box = new_line.baseline_y; + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + top_of_line_box += 0.5 * line_height.emSize(); + } else if (_block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + top_of_line_box -= 0.5 * line_height.emSize(); + } else { + top_of_line_box -= line_height.getTypoAscent(); + } + TRACE((" y attribute set, next line top_of_line_box: %f\n", top_of_line_box )); + // Set the initial y coordinate of the for this line (see above). + _scanline_maker->setNewYCoordinate(top_of_line_box); + } + + // Reset relative y_offset ("dy" attribute is relative but should be reset at + // the beginning of each line since each line will have a new "y" written out.) + _y_offset = 0.0; + + } else { + + // This is the plain SVG case + // + // "x" and "y" are used to place text, simulating lines as necessary + if( it_chunk->broken_spans.front().start.iter_span->y._set ) { + _y_offset = it_chunk->broken_spans.front().start.iter_span->y.computed - new_line.baseline_y; + } + } + } + _flow._chunks.push_back(new_chunk); + + double current_x; + double direction_sign; + Direction previous_direction = para.direction; + double counter_directional_width_remaining = 0.0; + float glyph_rotate = 0.0; + if (para.direction == LEFT_TO_RIGHT) { + direction_sign = +1.0; + current_x = 0.0; + } else { + direction_sign = -1.0; + if (para.alignment == FULL && !_flow._input_wrap_shapes.empty()){ + current_x = it_chunk->scanrun_width; + } + else { + current_x = it_chunk->text_width; + } + } + + // Loop over broken spans; a broken span is part of no more than one PangoItem. + for (std::vector::const_iterator it_span = it_chunk->broken_spans.begin() ; it_span != it_chunk->broken_spans.end() ; it_span++) { + + // begin adding spans to the list + UnbrokenSpan const &unbroken_span = *it_span->start.iter_span; + double x_in_span_last = 0.0; // set at the END when a new cluster starts + double x_in_span = 0.0; // set from the preceding at the START when a new cluster starts. + + // for (int i = 0; i < unbroken_span.glyph_string->num_glyphs; ++i) { + // std::cout << "Unbroken span: " << unbroken_span.glyph_string->glyphs[i].glyph << std::endl; + // } + + if (it_span->start.char_byte == 0) { + // Start of an unbroken span, we might have dx, dy or rotate still to process + // (x and y are done per chunk) + if (unbroken_span.dx._set) current_x += unbroken_span.dx.computed; + if (unbroken_span.dy._set) _y_offset += unbroken_span.dy.computed; + if (unbroken_span.rotate._set) glyph_rotate = unbroken_span.rotate.computed * (M_PI/180); + } + + if (_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE + && unbroken_span.pango_item_index == -1) { + // style only, nothing to output + continue; + } + + Layout::Span new_span; + + new_span.in_chunk = _flow._chunks.size() - 1; + new_span.line_height = unbroken_span.line_height; + new_span.in_input_stream_item = unbroken_span.input_index; + new_span.baseline_shift = 0.0; + new_span.block_progression = _block_progression; + new_span.text_orientation = unbroken_span.text_orientation; + if ((_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE) && (new_span.font = para.pango_items[unbroken_span.pango_item_index].font)) + { + new_span.font->Ref(); + new_span.font_size = unbroken_span.font_size; + new_span.direction = para.pango_items[unbroken_span.pango_item_index].item->analysis.level & 1 ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; + new_span.input_stream_first_character = Glib::ustring::const_iterator(unbroken_span.input_stream_first_character.base() + it_span->start.char_byte); + } else { // a control code + new_span.font = nullptr; + new_span.font_size = new_span.line_height.emSize(); + new_span.direction = para.direction; + } + + if (new_span.direction == para.direction) { + current_x -= counter_directional_width_remaining; + counter_directional_width_remaining = 0.0; + } else if (new_span.direction != previous_direction) { + // measure width of spans we need to switch round + counter_directional_width_remaining = 0.0; + std::vector::const_iterator it_following_span; + for (it_following_span = it_span ; it_following_span != it_chunk->broken_spans.end() ; it_following_span++) { + if (it_following_span->start.iter_span->pango_item_index == -1) break; + Layout::Direction following_span_progression = static_cast(_flow._input_stream[it_following_span->start.iter_span->input_index])->styleGetBlockProgression(); + if (!Layout::_directions_are_orthogonal(following_span_progression, _block_progression)) { + if (new_span.direction != (para.pango_items[it_following_span->start.iter_span->pango_item_index].item->analysis.level & 1 ? RIGHT_TO_LEFT : LEFT_TO_RIGHT)) break; + } + counter_directional_width_remaining += direction_sign * (it_following_span->width + it_following_span->whitespace_count * add_to_each_whitespace); + } + current_x += counter_directional_width_remaining; + counter_directional_width_remaining = 0.0; // we want to go increasingly negative + } + new_span.x_start = current_x; + new_span.y_offset = _y_offset; // Offset from baseline due to 'y' and 'dy' attributes (used to simulate multiline text). + + if (_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE) { + // the span is set up, push the glyphs and chars + + InputStreamTextSource const *text_source = static_cast(_flow._input_stream[unbroken_span.input_index]); + Glib::ustring::const_iterator iter_source_text = Glib::ustring::const_iterator(unbroken_span.input_stream_first_character.base() + it_span->start.char_byte) ; + unsigned char_index_in_unbroken_span = it_span->start.char_index; + double font_size_multiplier = new_span.font_size / (PANGO_SCALE * _font_factory_size_multiplier); + int log_cluster_size_glyphs = 0; // Number of glyphs in this log_cluster + int log_cluster_size_chars = 0; // Number of characters in this log_cluster + unsigned end_byte = 0; + + // Get some pointers (constant for an unbroken span). + font_instance *font = para.pango_items[unbroken_span.pango_item_index].font; + PangoItem *pango_item = para.pango_items[unbroken_span.pango_item_index].item; + + // Loop over glyphs in span + double old_delta_x = 0.0; + +#ifdef DEBUG_GLYPH + std::cout << "\nGlyphs in span: x_start: " << new_span.x_start << " y_offset: " << new_span.y_offset + << " PangoItem flags: " << (int)pango_item->analysis.flags << " Gravity: " << (int)pango_item->analysis.gravity << std::endl; + std::cout << " Unicode Glyph h_advance v_advance width cluster orientation new_glyph delta" << std::endl; + std::cout << " (hex) No. start x y x y" << std::endl; + std::cout << " -------------------------------------------------------------------------------------------------" << std::endl; +#endif + + for (unsigned glyph_index = it_span->start_glyph_index ; glyph_index < it_span->end_glyph_index ; glyph_index++) { + + unsigned char_byte = iter_source_text.base() - unbroken_span.input_stream_first_character.base(); + bool newcluster = false; + if (unbroken_span.glyph_string->glyphs[glyph_index].attr.is_cluster_start) { + newcluster = true; + x_in_span = x_in_span_last; + } + + if (unbroken_span.glyph_string->log_clusters[glyph_index] < (int)unbroken_span.text_bytes + && *iter_source_text == UNICODE_SOFT_HYPHEN + && glyph_index + 1 != it_span->end_glyph_index) { + // if we're looking at a soft hyphen and it's not the last glyph in the + // chunk we don't draw the glyph but we still need to add to _characters + Layout::Character new_character; + new_character.the_char = *iter_source_text; + new_character.in_span = _flow._spans.size(); // the span hasn't been added yet, so no -1 + new_character.char_attributes = para.char_attributes[unbroken_span.char_index_in_para + char_index_in_unbroken_span]; + new_character.in_glyph = -1; + _flow._characters.push_back(new_character); + iter_source_text++; + char_index_in_unbroken_span++; + while (glyph_index < (unsigned)unbroken_span.glyph_string->num_glyphs + && unbroken_span.glyph_string->log_clusters[glyph_index] == (int)char_byte) + glyph_index++; + glyph_index--; + continue; + } + + // create the Layout::Glyph + PangoGlyphInfo *unbroken_span_glyph_info = &unbroken_span.glyph_string->glyphs[glyph_index]; + double glyph_width = font_size_multiplier * unbroken_span_glyph_info->geometry.width; + + Layout::Glyph new_glyph; + new_glyph.glyph = unbroken_span_glyph_info->glyph; + new_glyph.in_character = _flow._characters.size(); + new_glyph.rotation = glyph_rotate; + new_glyph.orientation = ORIENTATION_UPRIGHT; // Only effects vertical text + new_glyph.hidden = hidden; // SVG 2 overflow + + // Advance does not include kerning but Pango <= 1.43 gives wrong advances for verical upright text. + double glyph_h_advance = new_span.font_size * font->Advance(new_glyph.glyph, false); + double glyph_v_advance = new_span.font_size * font->Advance(new_glyph.glyph, true ); + +#ifdef DEBUG_GLYPH + + bool is_cluster_start = unbroken_span_glyph_info->attr.is_cluster_start; + std::cout << " " << std::hex << std::setw(6) << *iter_source_text << std::dec + << " " << std::setw(6) << new_glyph.glyph + << std::fixed << std::showpoint << std::setprecision(2) + << " " << std::setw(6) << glyph_h_advance + << " " << std::setw(6) << glyph_v_advance + << " " << std::setw(6) << glyph_width + << " " << std::setw(6) << std::boolalpha << is_cluster_start; // << std::endl; +#endif + + // We may have scaled font size to fit textLength; now, if + // @lengthAdjust=spacingAndGlyphs, this scaling must be only horizontal, + // not vertical, so we unscale it back vertically during output + if (_flow.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) + new_glyph.vertical_scale = 1.0 / _flow.getTextLengthMultiplierDue(); + else + new_glyph.vertical_scale = 1.0; + + // Position glyph -------------------- + new_glyph.x = current_x; + new_glyph.y =_y_offset; + new_glyph.advance = glyph_width; + + if (*iter_source_text == '\n') { + // Line feeds should take zero space but they are given 'space' width. + new_glyph.advance = 0.0; + } + + // y-coordinate is flipped between vertical and horizontal text... + // delta_y is common offset but applied with opposite sign + double delta_x = unbroken_span_glyph_info->geometry.x_offset * font_size_multiplier; + double delta_y = unbroken_span_glyph_info->geometry.y_offset * font_size_multiplier - unbroken_span.baseline_shift; + SPCSSBaseline dominant_baseline = _flow._blockBaseline(); + + if (_block_progression == LEFT_TO_RIGHT || _block_progression == RIGHT_TO_LEFT) { + // Vertical text + + // Default dominant baseline is determined by overall block (i.e. ) 'text-orientation' value. + if( _flow._blockTextOrientation() != SP_CSS_TEXT_ORIENTATION_SIDEWAYS ) { + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_CENTRAL; + } else { + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_ALPHABETIC; + } + + // TODO: Should also check 'glyph_orientation_vertical' if 'text-orientation' is unset... + if( new_span.text_orientation == SP_CSS_TEXT_ORIENTATION_SIDEWAYS || + (new_span.text_orientation == SP_CSS_TEXT_ORIENTATION_MIXED && pango_item->analysis.gravity == PANGO_GRAVITY_SOUTH) ) { + + // Sideways orientation (Latin characters, CJK punctuation), 90deg rotation done at output stage. + +#ifdef DEBUG_GLYPH + std::cout << " Sideways" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y + << std::endl; +#endif + + new_glyph.orientation = ORIENTATION_SIDEWAYS; + + new_glyph.x += delta_x; + new_glyph.y -= delta_y; + + // Multiplying by font-size could cause slight differences in + // positioning for different baselines if the font size varies within a + // line of text (e.g. sub-scripts and super-scripts). + new_glyph.y -= new_span.font_size * font->GetBaselines()[ dominant_baseline ]; + + } else { + // Upright orientation + + // This is complicated as Pango (1.44) doesn't do what is expected in + // the 'x' direction (which points downward for vertical text). If one + // subtracts delta_x, Latin glyphs are aligned so their ink rectangle + // is at the top of the grid, rather than their em box. The initial + // character is positioned so its base is at the alignment + // point. Non-spacing-marks are positioned an em-box too low. + + // What we do: + // * Ignore delta_x on first glyph, the baseline will be on grid. + // * Shift non-spacing-marks up by an embox. + // * Further shift non-spacing-marks up by the difference in delta_x's of the mark and previous character + // (the relative difference is correct). + // * Set the advance for non-spacing-marks to 0 (as many fonts don't include vertical metrics). + +#ifdef DEBUG_GLYPH + std::cout << " Upright" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y + << std::endl; +#endif + + if (pango_version_check(1,44,0) != nullptr) { + // Pango < 1.44.0 + new_glyph.advance = glyph_v_advance; + new_glyph.x += delta_x; + // Glyph reference point is center (shift left edge to center glyph). + new_glyph.y -= glyph_h_advance/2.0; + } + + new_glyph.y -= delta_y; + + // Adjust for alignment point (top of em box, horizontal center). + new_glyph.x += new_span.line_height.ascent; // Moves baseline so em-box on grid. + new_glyph.y -= new_span.font_size * (font->GetBaselines()[ dominant_baseline ] - + font->GetBaselines()[ SP_CSS_BASELINE_CENTRAL ] ); + + if (g_unichar_type (*iter_source_text) == G_UNICODE_NON_SPACING_MARK) { + + if (pango_version_check(1,44,0) == nullptr) { + // Pango >= 1.44 + new_glyph.x -= new_span.line_height.emSize(); + new_glyph.x -= delta_x; + new_glyph.x += old_delta_x; + } + new_glyph.advance = 0; // Many fonts report a non-zero advance for marks, especially if the 'vmtx' table is missing. + } else { + old_delta_x = delta_x; + } + } + } else { + // Horizontal text + +#ifdef DEBUG_GLYPH + std::cout << " Horizontal" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y + << std::endl; +#endif + + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_ALPHABETIC; + + new_glyph.x += delta_x; + new_glyph.y += delta_y; + + new_glyph.y += new_span.font_size * font->GetBaselines()[ dominant_baseline ]; + } + + // Correct for right to left text + if (new_span.direction == RIGHT_TO_LEFT) { + + // The following commented out code is from 2005. Subtracting cluster width gives wrong placement if more + // than one glyph has a horizontal advance. See GitHub issue 469. I leave the old code here in case switching to + // subtracting only the glyph width causes unforseen bugs. + + // // pango wanted to give us glyphs in visual order but we refused, so we need to work + // // out where the cluster start is ourselves + + // // Add up widths of remaining glyphs in span. + // double cluster_width = 0.0; + // std::cout << " glyph_index: " << glyph_index << " end_glyph_index: " << it_span->end_glyph_index << std::endl; + // for (unsigned rtl_index = glyph_index; rtl_index < it_span->end_glyph_index ; rtl_index++) { + // if (unbroken_span.glyph_string->glyphs[rtl_index].attr.is_cluster_start && rtl_index != glyph_index) { + // break; + // } + // cluster_width += font_size_multiplier * unbroken_span.glyph_string->glyphs[rtl_index].geometry.width; + // } + // new_glyph.x -= cluster_width; + + new_glyph.x -= font_size_multiplier * unbroken_span.glyph_string->glyphs[glyph_index].geometry.width; + } + + // Store glyph data + _flow._glyphs.push_back(new_glyph); + + // Create the Layout::Character(s) + if (newcluster) { + newcluster = false; + + // Figure out how many glyphs are in the log_cluster. + log_cluster_size_glyphs = 0; + for (; log_cluster_size_glyphs + glyph_index < it_span->end_glyph_index; log_cluster_size_glyphs++){ + if(unbroken_span.glyph_string->log_clusters[glyph_index ] != + unbroken_span.glyph_string->log_clusters[glyph_index + log_cluster_size_glyphs]) break; + } + + // Find where the text ends for this log_cluster. + end_byte = it_span->start.iter_span->text_bytes; // Upper limit + for(int next_glyph_index = glyph_index+1; next_glyph_index < unbroken_span.glyph_string->num_glyphs; next_glyph_index++){ + if(unbroken_span.glyph_string->glyphs[next_glyph_index].attr.is_cluster_start){ + end_byte = unbroken_span.glyph_string->log_clusters[next_glyph_index]; + break; + } + } + + // Figure out how many characters are in the log_cluster. + log_cluster_size_chars = 0; + Glib::ustring::const_iterator lclist = iter_source_text; + unsigned lcb = char_byte; + while (lcb < end_byte){ + log_cluster_size_chars++; + lclist++; + lcb = lclist.base() - unbroken_span.input_stream_first_character.base(); + } + } + + double advance_width = new_glyph.advance; + while (char_byte < end_byte) { + + /* Hack to survive ligatures: in log_cluster keep the number of available chars >= number of glyphs remaining. + When there are no ligatures these two sizes are always the same. + */ + if (log_cluster_size_chars < log_cluster_size_glyphs) { + log_cluster_size_glyphs--; + break; + } + + // Store character info + Layout::Character new_character; + new_character.the_char = *iter_source_text; + new_character.in_span = _flow._spans.size(); + new_character.x = x_in_span; + new_character.char_attributes = para.char_attributes[unbroken_span.char_index_in_para + char_index_in_unbroken_span]; + new_character.in_glyph = (hidden ? -1 : _flow._glyphs.size() - 1); + _flow._characters.push_back(new_character); + + // Letter/word spacing and justification + if (new_character.char_attributes.is_white) + advance_width += text_source->style->word_spacing.computed * _flow.getTextLengthMultiplierDue() + add_to_each_whitespace; // justification + if (new_character.char_attributes.is_cursor_position) + advance_width += text_source->style->letter_spacing.computed * _flow.getTextLengthMultiplierDue(); + advance_width += _flow.getTextLengthIncrementDue(); + + // Update counters + iter_source_text++; + char_index_in_unbroken_span++; + char_byte = iter_source_text.base() - unbroken_span.input_stream_first_character.base(); + log_cluster_size_chars--; + } + + // Update x position variables + advance_width *= direction_sign; + if (new_span.direction != para.direction) { + counter_directional_width_remaining -= advance_width; + current_x -= advance_width; + x_in_span_last -= advance_width; + } else { + current_x += advance_width; + x_in_span_last += advance_width; + } + } // Loop over glyphs in span + + } else if (_flow._input_stream[unbroken_span.input_index]->Type() == CONTROL_CODE) { + current_x += static_cast(_flow._input_stream[unbroken_span.input_index])->width; + } + + new_span.x_end = new_span.x_start + x_in_span_last; + _flow._spans.push_back(new_span); + previous_direction = new_span.direction; + } + // end adding spans to the list, on to the next chunk... + } + TRACE((" End _outputLine\n")); +} + +/** + * Initialises the ScanlineMaker for the first shape in the flow, + * or the infinite version if we're not doing wrapping. + */ +void Layout::Calculator::_createFirstScanlineMaker() +{ + _current_shape_index = 0; + InputStreamTextSource const *text_source = static_cast(_flow._input_stream.front()); + if (_flow._input_wrap_shapes.empty()) { + // create the special no-wrapping infinite scanline maker + double initial_x = 0, initial_y = 0; + if (!text_source->x.empty()) { + initial_x = text_source->x.front().computed; + } + if (!text_source->y.empty()) { + initial_y = text_source->y.front().computed; + } + _scanline_maker = new InfiniteScanlineMaker(initial_x, initial_y, _block_progression); + TRACE((" wrapping disabled\n")); + } + else { + _scanline_maker = new ShapeScanlineMaker(_flow._input_wrap_shapes[_current_shape_index].shape, _block_progression); + TRACE((" begin wrap shape 0\n")); + + // 'inline-size' uses an infinitely high (wide) shape. We must set initial y. (We only need to do it here as there is only one shape.) + if (_flow.wrap_mode == WRAP_INLINE_SIZE) { + _block_progression = _flow._blockProgression(); + if( _block_progression == RIGHT_TO_LEFT || + _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, CJK + if (!text_source->x.empty()) { + double initial_x = text_source->x.front().computed; + _scanline_maker->setNewYCoordinate(initial_x); + } else { + std::cerr << "Layout::Calculator::_createFirstScanlineMaker: no x value with 'inline-size'!" << std::endl; + _scanline_maker->setNewYCoordinate(0); + } + } else { + // Horizontal text + if (!text_source->y.empty()) { + double initial_y = text_source->y.front().computed; + _scanline_maker->setNewYCoordinate(initial_y); + } else { + std::cerr << "Layout::Calculator::_createFirstScanlineMaker: no y value with 'inline-size'!" << std::endl; + _scanline_maker->setNewYCoordinate(0); + } + } + } + } +} + +void Layout::Calculator::UnbrokenSpanPosition::increment() +{ + gchar const *text_base = &*iter_span->input_stream_first_character.base(); + char_byte = g_utf8_next_char(text_base + char_byte) - text_base; + char_index++; + if (char_byte == iter_span->text_bytes) { + iter_span++; + char_index = char_byte = 0; + } +} + +void Layout::Calculator::BrokenSpan::setZero() +{ + end = start; + width = 0.0; + whitespace_count = 0; + end_glyph_index = start_glyph_index = 0; + ends_with_whitespace = false; + each_whitespace_width = 0.0; + letter_spacing = 0.0; + word_spacing = 0.0; +} + +///** +// * For sections of text with a block-progression different to the rest +// * of the flow, the best thing to do is to detect them in advance and +// * create child TextFlow objects with just the rotated text. In the +// * parent we then effectively use ARBITRARY_GAP fields during the +// * flowing (because we don't allow wrapping when the block-progression +// * changes) and copy the actual text in during the output phase. +// * +// * NB: this code not enabled yet. +// */ +//void Layout::Calculator::_initialiseInputItems(ParagraphInfo *para) const +//{ +// Direction prev_block_progression = _block_progression; +// int run_start_input_index = para->first_input_index; +// +// para->free_sequence(para->input_items); +// for(int input_index = para->first_input_index ; input_index < (int)_flow._input_stream.size() ; input_index++) { +// InputItemInfo input_item; +// +// input_item.in_sub_flow = false; +// input_item.sub_flow = NULL; +// if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { +// Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); +// if ( control_code->code == SHAPE_BREAK +// || control_code->code == PARAGRAPH_BREAK) +// break; // stop at the end of the paragraph +// // all other control codes we'll pick up later +// +// } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE) { +// Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[input_index]); +// Direction this_block_progression = text_source->styleGetBlockProgression(); +// if (this_block_progression != prev_block_progression) { +// if (prev_block_progression != _block_progression) { +// // need to back up so that control codes belong outside the block-progression change +// int run_end_input_index = input_index - 1; +// while (run_end_input_index > run_start_input_index +// && _flow._input_stream[run_end_input_index]->Type() != TEXT_SOURCE) +// run_end_input_index--; +// // now create the sub-flow +// input_item.sub_flow = new Layout; +// for (int sub_input_index = run_start_input_index ; sub_input_index <= run_end_input_index ; sub_input_index++) { +// input_item.in_sub_flow = true; +// if (_flow._input_stream[sub_input_index]->Type() == CONTROL_CODE) { +// Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[sub_input_index]); +// input_item.sub_flow->appendControlCode(control_code->code, control_code->source, control_code->width, control_code->ascent, control_code->descent); +// } else if (_flow._input_stream[sub_input_index]->Type() == TEXT_SOURCE) { +// Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[sub_input_index]); +// input_item.sub_flow->appendText(*text_source->text, text_source->style, text_source->source, NULL, 0, text_source->text_begin, text_source->text_end); +// Layout::InputStreamTextSource *sub_flow_text_source = static_cast(input_item.sub_flow->_input_stream.back()); +// sub_flow_text_source->x = text_source->x; // this is easier than going via optionalattrs for the appendText() call +// sub_flow_text_source->y = text_source->y; // should these actually be allowed anyway? You'll almost never get the results you expect +// sub_flow_text_source->dx = text_source->dx; // (not that it's very clear what you should expect, anyway) +// sub_flow_text_source->dy = text_source->dy; +// sub_flow_text_source->rotate = text_source->rotate; +// } +// } +// input_item.sub_flow->calculateFlow(); +// } +// run_start_input_index = input_index; +// } +// prev_block_progression = this_block_progression; +// } +// para->input_items.push_back(input_item); +// } +//} + +/** + * Take all the text from \a _para.first_input_index to the end of the + * paragraph and stitch it together so that pango_itemize() can be called on + * the whole thing. + * + * Input: para.first_input_index. + * Output: para.direction, para.pango_items, para.char_attributes. + * Returns: the number of spans created by pango_itemize + */ +void Layout::Calculator::_buildPangoItemizationForPara(ParagraphInfo *para) const +{ + TRACE(("pango version string: %s\n", pango_version_string() )); +#if PANGO_VERSION_CHECK(1,37,1) + TRACE((" ... compiled for font features\n")); +#endif + + Glib::ustring para_text; + PangoAttrList *attributes_list; + unsigned input_index; + + para->free_sequence(para->pango_items); + para->char_attributes.clear(); + + TRACE(("itemizing para, first input %d\n", para->first_input_index)); + + attributes_list = pango_attr_list_new(); + for(input_index = para->first_input_index ; input_index < _flow._input_stream.size() ; input_index++) { + if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { + Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); + if ( control_code->code == SHAPE_BREAK + || control_code->code == PARAGRAPH_BREAK) + break; // stop at the end of the paragraph + // all other control codes we'll pick up later + + } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE) { + Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[input_index]); + + // create the font_instance + font_instance *font = text_source->styleGetFontInstance(); + if (font == nullptr) + continue; // bad news: we'll have to ignore all this text because we know of no font to render it + + PangoAttribute *attribute_font_description = pango_attr_font_desc_new(font->descr); + attribute_font_description->start_index = para_text.bytes(); + +#if PANGO_VERSION_CHECK(1,37,1) + PangoAttribute *attribute_font_features = + pango_attr_font_features_new( text_source->style->getFontFeatureString().c_str()); + attribute_font_features->start_index = para_text.bytes(); +#endif + para_text.append(&*text_source->text_begin.base(), text_source->text_length); // build the combined text + attribute_font_description->end_index = para_text.bytes(); + pango_attr_list_insert(attributes_list, attribute_font_description); + +#if PANGO_VERSION_CHECK(1,37,1) + attribute_font_features->end_index = para_text.bytes(); + pango_attr_list_insert(attributes_list, attribute_font_features); +#endif + + // Set language + SPObject * object = text_source->source; + if (!object->lang.empty()) { + PangoLanguage* language = pango_language_from_string(object->lang.c_str()); + PangoAttribute *attribute_language = pango_attr_language_new( language ); + pango_attr_list_insert(attributes_list, attribute_language); + } + + // ownership of attribute is assumed by the list + font->Unref(); + } + } + + TRACE(("whole para: \"%s\"\n", para_text.data())); + TRACE(("%d input sources used\n", input_index - para->first_input_index)); + // do the pango_itemize() + GList *pango_items_glist = nullptr; + para->direction = LEFT_TO_RIGHT; // CSS default + if (_flow._input_stream[para->first_input_index]->Type() == TEXT_SOURCE) { + Layout::InputStreamTextSource const *text_source = static_cast(_flow._input_stream[para->first_input_index]); + + para->direction = (text_source->style->direction.computed == SP_CSS_DIRECTION_LTR) ? LEFT_TO_RIGHT : RIGHT_TO_LEFT; + PangoDirection pango_direction = (text_source->style->direction.computed == SP_CSS_DIRECTION_LTR) ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; + pango_items_glist = pango_itemize_with_base_dir(_pango_context, pango_direction, para_text.data(), 0, para_text.bytes(), attributes_list, nullptr); + } + + if( pango_items_glist == nullptr ) { + // Type wasn't TEXT_SOURCE or direction was not set. + pango_items_glist = pango_itemize(_pango_context, para_text.data(), 0, para_text.bytes(), attributes_list, nullptr); + } + + pango_attr_list_unref(attributes_list); + + // convert the GList to our vector<> and make the font_instance for each PangoItem at the same time + para->pango_items.reserve(g_list_length(pango_items_glist)); + TRACE(("para itemizes to %d sections\n", g_list_length(pango_items_glist))); + for (GList *current_pango_item = pango_items_glist ; current_pango_item != nullptr ; current_pango_item = current_pango_item->next) { + PangoItemInfo new_item; + new_item.item = (PangoItem*)current_pango_item->data; + PangoFontDescription *font_description = pango_font_describe(new_item.item->analysis.font); + new_item.font = (font_factory::Default())->Face(font_description); + pango_font_description_free(font_description); // Face() makes a copy + para->pango_items.push_back(new_item); + } + g_list_free(pango_items_glist); + + // and get the character attributes on everything + para->char_attributes.resize(para_text.length() + 1); + pango_get_log_attrs(para_text.data(), para_text.bytes(), -1, nullptr, &*para->char_attributes.begin(), para->char_attributes.size()); + + TRACE(("end para itemize, direction = %d\n", para->direction)); +} + +/** + * Finds the value of line_height_multiplier given the 'line-height' property. The result of + * multiplying \a l by \a line_height_multiplier is the inline box height as specified in css2 + * section 10.8. http://www.w3.org/TR/CSS2/visudet.html#line-height + * + * The 'computed' value of 'line-height' does not have a consistent meaning. We need to find the + * 'used' value and divide that by the font size. + */ +double Layout::Calculator::_computeFontLineHeight( SPStyle const *style ) +{ + // This is a bit backwards... we should be returning the absolute height + // but as the code expects line_height_multiplier we return that. + if (style->line_height.normal) { + return (LINE_HEIGHT_NORMAL); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + // Special case per CSS, computed value is multiplier + return style->line_height.computed; + } else { + // Normal case, computed value is absolute height. Turn it into multiplier. + return style->line_height.computed / style->font_size.computed; + } +} + +bool compareGlyphWidth(const PangoGlyphInfo &a, const PangoGlyphInfo &b) +{ + bool retval = false; + if ( b.geometry.width == 0 && (a.geometry.width > 0))retval = true; + return (retval); +} + + +/** + * Split the paragraph into spans. Also call pango_shape() on them. + * + * Input: para->first_input_index, para->pango_items + * Output: para->spans + * Returns: the index of the beginning of the following paragraph in _flow._input_stream + */ +unsigned Layout::Calculator::_buildSpansForPara(ParagraphInfo *para) const +{ + unsigned pango_item_index = 0; + unsigned char_index_in_para = 0; + unsigned byte_index_in_para = 0; + unsigned input_index; + + TRACE(("build spans\n")); + para->free_sequence(para->unbroken_spans); + + for(input_index = para->first_input_index ; input_index < _flow._input_stream.size() ; input_index++) { + if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { + Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); + + if ( control_code->code == SHAPE_BREAK + || control_code->code == PARAGRAPH_BREAK) { + + // Add span to be used to calculate line spacing of blank lines. + UnbrokenSpan new_span; + new_span.pango_item_index = -1; + new_span.input_index = input_index; + + // No pango object, so find font and line height ourselves. + SPObject * object = control_code->source; + if (object) { + SPStyle * style = object->style; + if (style) { + new_span.font_size = style->font_size.computed * _flow.getTextLengthMultiplierDue(); + font_factory * factory = font_factory::Default(); + font_instance * font = factory->FaceFromStyle( style ); + new_span.line_height_multiplier = _computeFontLineHeight( object->style ); + new_span.line_height.set( font ); + new_span.line_height *= new_span.font_size; + } + } + new_span.text_bytes = 0; + new_span.char_index_in_para = char_index_in_para; + para->unbroken_spans.push_back(new_span); + TRACE(("add empty span for break %lu\n", para->unbroken_spans.size() - 1)); + break; // stop at the end of the paragraph + + } else if (control_code->code == ARBITRARY_GAP) { // Not used! + + UnbrokenSpan new_span; + new_span.pango_item_index = -1; + new_span.input_index = input_index; + new_span.line_height.ascent = control_code->ascent * _flow.getTextLengthMultiplierDue(); + new_span.line_height.descent = control_code->descent * _flow.getTextLengthMultiplierDue(); + new_span.text_bytes = 0; + new_span.char_index_in_para = char_index_in_para; + para->unbroken_spans.push_back(new_span); + TRACE(("add gap span %lu\n", para->unbroken_spans.size() - 1)); + } + } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE && pango_item_index < para->pango_items.size()) { + Layout::InputStreamTextSource const *text_source = static_cast(_flow._input_stream[input_index]); + unsigned char_index_in_source = 0; + unsigned span_start_byte_in_source = 0; + + // we'll need to make several spans from each text source, based on the rules described about the UnbrokenSpan definition + for ( ; ; ) { + /* we need to change spans at every change of PangoItem, source stream change, + or change in one of the attributes altering position/rotation. */ + + unsigned const pango_item_bytes = ( pango_item_index >= para->pango_items.size() + ? 0 + : ( para->pango_items[pango_item_index].item->offset + + para->pango_items[pango_item_index].item->length + - byte_index_in_para ) ); + unsigned const text_source_bytes = ( text_source->text_end.base() + - text_source->text_begin.base() + - span_start_byte_in_source ); + TRACE(("New Span\n")); + UnbrokenSpan new_span; + new_span.text_bytes = std::min(text_source_bytes, pango_item_bytes); + new_span.input_stream_first_character = Glib::ustring::const_iterator(text_source->text_begin.base() + span_start_byte_in_source); + new_span.char_index_in_para = char_index_in_para + char_index_in_source; + new_span.input_index = input_index; + + // cut at attribute changes as well + new_span.x._set = false; + new_span.y._set = false; + new_span.dx._set = false; + new_span.dy._set = false; + new_span.rotate._set = false; + if (_block_progression == TOP_TO_BOTTOM || _block_progression == BOTTOM_TO_TOP) { + // Horizontal text + if (text_source->x.size() > char_index_in_source) new_span.x = text_source->x[char_index_in_source]; + if (text_source->y.size() > char_index_in_source) new_span.y = text_source->y[char_index_in_source]; + if (text_source->dx.size() > char_index_in_source) new_span.dx = text_source->dx[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + if (text_source->dy.size() > char_index_in_source) new_span.dy = text_source->dy[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + } else { + // Vertical text + if (text_source->x.size() > char_index_in_source) new_span.y = text_source->x[char_index_in_source]; + if (text_source->y.size() > char_index_in_source) new_span.x = text_source->y[char_index_in_source]; + if (text_source->dx.size() > char_index_in_source) new_span.dy = text_source->dx[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + if (text_source->dy.size() > char_index_in_source) new_span.dx = text_source->dy[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + } + if (text_source->rotate.size() > char_index_in_source) new_span.rotate = text_source->rotate[char_index_in_source]; + else if (char_index_in_source == 0) new_span.rotate = 0.f; + if (input_index == 0 && para->unbroken_spans.empty() && !new_span.y._set && _flow._input_wrap_shapes.empty()) { + // if we don't set an explicit y some of the automatic wrapping code takes over and moves the text vertically + // so that the top of the letters is at zero, not the baseline + new_span.y = 0.0; + } + Glib::ustring::const_iterator iter_text = new_span.input_stream_first_character; + iter_text++; + for (unsigned i = char_index_in_source + 1 ; ; i++, iter_text++) { + if (iter_text >= text_source->text_end) break; + if (iter_text.base() - new_span.input_stream_first_character.base() >= (int)new_span.text_bytes) break; + if ( i >= text_source->x.size() && i >= text_source->y.size() + && i >= text_source->dx.size() && i >= text_source->dy.size() + && i >= text_source->rotate.size()) break; + if ( (text_source->x.size() > i && text_source->x[i]._set) + || (text_source->y.size() > i && text_source->y[i]._set) + || (text_source->dx.size() > i && text_source->dx[i]._set && text_source->dx[i].computed != 0.0) + || (text_source->dy.size() > i && text_source->dy[i]._set && text_source->dy[i].computed != 0.0) + || (text_source->rotate.size() > i && text_source->rotate[i]._set + && (i == 0 || text_source->rotate[i].computed != text_source->rotate[i - 1].computed))) { + new_span.text_bytes = iter_text.base() - new_span.input_stream_first_character.base(); + break; + } + } + + // now we know the length, do some final calculations and add the UnbrokenSpan to the list + new_span.font_size = text_source->style->font_size.computed * _flow.getTextLengthMultiplierDue(); + if (new_span.text_bytes) { + new_span.glyph_string = pango_glyph_string_new(); + /* Some assertions intended to help diagnose bug #1277746. */ + g_assert( 0 < new_span.text_bytes ); + g_assert( span_start_byte_in_source < text_source->text->bytes() ); + g_assert( span_start_byte_in_source + new_span.text_bytes <= text_source->text->bytes() ); + g_assert( memchr(text_source->text->data() + span_start_byte_in_source, '\0', static_cast(new_span.text_bytes)) + == nullptr ); + + /* Notes as of 4/29/13. Pango_shape is not generating English language ligatures, but it is generating + them for Hebrew (and probably other similar languages). In the case observed 3 unicode characters (a base + and 2 Mark, nonspacings) are merged into two glyphs (the base + first Mn, the 2nd Mn). All of these map + from glyph to first character of the log_cluster range. This destroys the 1:1 correspondence between + characters and glyphs. A big chunk of the conditional code which immediately follows this call + is there to clean up the resulting mess. + */ + + // Convert characters to glyphs + pango_shape(text_source->text->data() + span_start_byte_in_source, + new_span.text_bytes, + ¶->pango_items[pango_item_index].item->analysis, + new_span.glyph_string); + + if (para->pango_items[pango_item_index].item->analysis.level & 1) { + // Right to left text (Arabic, Hebrew, etc.) + + // pango_shape() will reorder glyphs in rtl sections into visual order + // (start offsets in accending order) which messes us up because the svg + // spec requires us to draw glyphs in logical order so let's reverse the + // glyphstring. + + const unsigned nglyphs = new_span.glyph_string->num_glyphs; + std::vector infos(nglyphs); + std::vector clusters(nglyphs); + + for (int i = 0; i < nglyphs; ++i) { + std::copy(&new_span.glyph_string->glyphs[i], &new_span.glyph_string->glyphs[i+1], infos.end() - i - 1); + std::copy(&new_span.glyph_string->log_clusters[i], &new_span.glyph_string->log_clusters[i+1], clusters.end() - i - 1); + } + + std::copy(infos.begin(), infos.end(), new_span.glyph_string->glyphs); + std::copy(clusters.begin(), clusters.end(), new_span.glyph_string->log_clusters); + + // We've messed up the flag that tells a glyph it is first in a cluster. + for (int i = 0; i < nglyphs; ++i) { + + // Set flag for start of cluster, we skip all other glyphs in cluster below. + new_span.glyph_string->glyphs[i].attr.is_cluster_start = 1; + + // Find index of first glyph in next cluster + int j = i + 1; + while( (j < nglyphs) && + (new_span.glyph_string->log_clusters[j] == new_span.glyph_string->log_clusters[i]) + ) { + new_span.glyph_string->glyphs[j].attr.is_cluster_start = 0; // Zero + j++; + } + + // Move on to next cluster. + i = j; + } + + } // End right to left text. + + // The following sorting doesn't seem to be necessary, and causes + // https://gitlab.com/inkscape/inkscape/-/issues/394 ... + + /* + CAREFUL, within a log_cluster the order of glyphs may not map 1:1, or + even in the same order, to the original unicode characters!!! Among + other things, diacritical mark glyphs can end up sequentially in front of the base + character glyph. That makes determining kerning, even approximately, difficult + later on. + + To resolve this to the extent possible sort the glyphs within the same + log_cluster into descending order by width in a special manner before copying. Diacritical marks + and similar have zero width and the glyph they modify has nonzero width. The order + of the zero width ones does not matter. A logical cluster is sorted into sequential order + [base] [zw_modifier1] [zw_modifier2] + where all the modifiers have zero width and the base does not. This works for languages like Hebrew. + + Pango also creates log clusters for languages like Telugu having many glyphs with nonzero widths. + Since these are nonzero, their order is not modified. + + If some language mixes these modes, having a log cluster having something like + [base1] [zw_modifier1] [base2] [zw_modifier2] + the result will be incorrect: + base1] [base2] [zw_modifier1] [zw_modifier2] + + If ligatures other than with Mark, nonspacing are ever implemented in Pango this will screw up, for instance + changing "fi" to "if". + */ + + // If it is necessary to move zero width glyphs.. then it applies to both right-to-left and left-to-right text. + // const unsigned nglyphs = new_span.glyph_string->num_glyphs; + // for (int i = 0; i < nglyphs; ++i) { + + // // Zero flag for start of cluster, we zero the rest below, and then reset it after sorting. + // new_span.glyph_string->glyphs[i].attr.is_cluster_start = 0; + + // // Find index of first glyph in next cluster + // int j = i + 1; + // while( (j < nglyphs) && + // (new_span.glyph_string->log_clusters[j] == new_span.glyph_string->log_clusters[i]) + // ) { + // new_span.glyph_string->glyphs[j].attr.is_cluster_start = 0; // Zero + // j++; + // } + + // if (j - i) { + // // More than one glyph in cluster -> sort. + // std::sort(&(new_span.glyph_string->glyphs[i]), &(new_span.glyph_string->glyphs[j]), compareGlyphWidth); + // } + + // // Now we're sorted, set flag for start of cluster. + // new_span.glyph_string->glyphs[i].attr.is_cluster_start = 1; + + // // Move on to next cluster. + // i = j; + // } + /* glyphs[].x_offset values are probably out of order within any log_clusters, apparently harmless */ + + + new_span.pango_item_index = pango_item_index; + new_span.line_height_multiplier = _computeFontLineHeight( text_source->style ); + new_span.line_height.set( para->pango_items[pango_item_index].font ); + new_span.line_height *= new_span.font_size; + + // At some point we may want to calculate baseline_shift here (to take advantage + // of otm features like superscript baseline), but for now we use style baseline_shift. + new_span.baseline_shift = text_source->style->baseline_shift.computed; + new_span.text_orientation = (SPCSSTextOrientation)text_source->style->text_orientation.computed; + + // TODO: metrics for vertical text + TRACE(("add text span %lu \"%s\"\n", para->unbroken_spans.size(), text_source->text->raw().substr(span_start_byte_in_source, new_span.text_bytes).c_str())); + TRACE((" %d glyphs\n", new_span.glyph_string->num_glyphs)); + } else { + // if there's no text we still need to initialise the styles + new_span.pango_item_index = -1; + font_instance *font = text_source->styleGetFontInstance(); + if (font) { + new_span.line_height_multiplier = _computeFontLineHeight( text_source->style ); + new_span.line_height.set( font ); + new_span.line_height *= new_span.font_size; + font->Unref(); + } else { + new_span.line_height *= 0.0; // Set all to zero + new_span.line_height_multiplier = LINE_HEIGHT_NORMAL; + } + TRACE(("add style init span %lu\n", para->unbroken_spans.size())); + } + para->unbroken_spans.push_back(new_span); + + // calculations for moving to the next UnbrokenSpan + byte_index_in_para += new_span.text_bytes; + char_index_in_source += g_utf8_strlen(&*new_span.input_stream_first_character.base(), new_span.text_bytes); + + if (new_span.text_bytes >= pango_item_bytes) { // end of pango item + pango_item_index++; + if (pango_item_index == para->pango_items.size()) break; // end of paragraph + } + if (new_span.text_bytes == text_source_bytes) + break; // end of source + // else attribute changed + span_start_byte_in_source += new_span.text_bytes; + } + char_index_in_para += char_index_in_source; + } + } + TRACE(("end build spans\n")); + return input_index; +} + +/** + * Moves onto next shape with a new scanline_maker. + * If there is no next shape, creates an infinite scanline maker to stash remaining text. + * Returns false if an infinite scanline maker is created. + */ +bool Layout::Calculator::_goToNextWrapShape() +{ + if (_flow._input_wrap_shapes.size() == 0) { + // Shouldn't happen. + std::cerr << "Layout::Calculator::_goToNextWrapShape() called for text without shapes!" << std::endl; + return false; + } + + if (_current_shape_index >= _flow._input_wrap_shapes.size()) { + // Shouldn't happen. + std::cerr << "Layout::Calculator::_goToNextWrapShape(): shape index too large!" << std::endl; + } + + _current_shape_index++; + + delete _scanline_maker; + _scanline_maker = nullptr; + + if (_current_shape_index < _flow._input_wrap_shapes.size()) { + _scanline_maker = new ShapeScanlineMaker(_flow._input_wrap_shapes[_current_shape_index].shape, _block_progression); + TRACE(("begin wrap shape %u\n", _current_shape_index)); + return true; + } else { + // Out of shapes, create infinite scanline maker to stash overflow. + + // First find a suitable position for overflow text. (index - 1 exists since we just incremented index) + double x = _flow._input_wrap_shapes[_current_shape_index - 1].shape->leftX; + double y = _flow._input_wrap_shapes[_current_shape_index - 1].shape->bottomY; + + _scanline_maker = new InfiniteScanlineMaker(x, y, _block_progression); + TRACE(("out of wrap shapes, stash leftover\n")); + return false; + } + + // Shouldn't reach +} + +/** + * Given \a para filled in and \a start_span_pos set, keeps trying to + * find somewhere it can fit the next line of text. The process of finding + * the text that fits will involve creating one or more entries in + * \a chunk_info describing the bounds of the fitted text and several + * bits of information that will prove useful when we come to output the + * line to #_flow. Returns with \a start_span_pos set to the end of the + * text that was fitted, \a chunk_info completely filled out and + * \a line_box_height set with the largest ascent and the largest + * descent (individually per CSS) on the line. The line_box_height + * can never be smaller than the line_box_strut (which is determined + * by the block level value of line_height). The return + * value is false only if we've run out of shapes to wrap inside (and + * hence stashed overflow). + */ +bool Layout::Calculator::_findChunksForLine(ParagraphInfo const ¶, + UnbrokenSpanPosition *start_span_pos, + std::vector *chunk_info, + FontMetrics *line_box_height, + FontMetrics const *strut_height) +{ + TRACE((" begin _findChunksForLine: chunks: %lu, em size: %f\n", chunk_info->size(), line_box_height->emSize() )); + + // CSS 2.1 dictates that the minimum line height (i.e. the strut height) + // is found from the block element. + *line_box_height = *strut_height; + TRACE((" initial line_box_height (em size): %f\n", line_box_height->emSize() )); + + bool truncated = false; + + UnbrokenSpanPosition span_pos; + for( ; ; ) { + // Get regions where one can place one line of text (can be more than one, if filling a + // donut for example). + std::vector scan_runs; + scan_runs = _scanline_maker->makeScanline(*line_box_height); // 1 scan run with "InfiniteScanlineMaker" + + // If scan_runs is empty, we must have reached the bottom of a shape. Go to next shape. + while (scan_runs.empty()) { + // Reset for new shape. + *line_box_height = *strut_height; + + // Only used by ShapeScanlineMaker + if (!_goToNextWrapShape()) { + truncated = true; + } + + // If we've run out of shapes, this will be the infinite line scanline maker with one scan_run). + scan_runs = _scanline_maker->makeScanline(*line_box_height); + } + + + TRACE((" finding line fit y=%f, %lu scan runs\n", scan_runs.front().y, scan_runs.size())); + chunk_info->clear(); + chunk_info->reserve(scan_runs.size()); + if (para.direction == RIGHT_TO_LEFT) std::reverse(scan_runs.begin(), scan_runs.end()); + unsigned scan_run_index; + span_pos = *start_span_pos; + for (scan_run_index = 0 ; scan_run_index < scan_runs.size() ; scan_run_index++) { + // Returns false if some text in line requires a taller line_box_height. + // (We try again with a larger line_box_height.) + if (!_buildChunksInScanRun(para, span_pos, scan_runs[scan_run_index], chunk_info, line_box_height)) { + break; + } + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty()) { + span_pos = chunk_info->back().broken_spans.back().end; + } + } + + if (scan_run_index == scan_runs.size()) break; // ie when buildChunksInScanRun() succeeded + + } // End for loop + + *start_span_pos = span_pos; + TRACE((" final line_box_height: %f\n", line_box_height->emSize() )); + TRACE((" end _findChunksForLine: chunks: %lu, truncated: %s\n", chunk_info->size(), truncated ? "true" : "false")); + return !truncated; +} + +/** + * Given a scan run and a first character, append one or more chunks to + * the \a chunk_info vector that describe all the spans and other detail + * necessary to output the greatest amount of text that will fit on this scan + * line (greedy line breaking algorithm). Each chunk contains one or more + * BrokenSpan structures that link back to UnbrokenSpan structures that link + * to the text itself. Normally there will be either one or zero (if the + * scanrun is too short to fit any text) chunk added to \a chunk_info by + * each call to this method, but we will add more than one if an x or y + * attribute has been set on a tspan. \a line_height must be set on input, + * and if it needs to be made larger and the #_scanline_maker can't do + * an in-situ resize then it will be set to the required value and the + * method will return false. + */ +bool Layout::Calculator::_buildChunksInScanRun(ParagraphInfo const ¶, + UnbrokenSpanPosition const &start_span_pos, + ScanlineMaker::ScanRun const &scan_run, + std::vector *chunk_info, + FontMetrics *line_height) const +{ + TRACE((" begin _buildChunksInScanRun: chunks: %lu, em size: %f\n", chunk_info->size(), line_height->emSize() )); + + FontMetrics line_height_saved = *line_height; // Store for recalculating line height if chunks are backed out + + ChunkInfo new_chunk; + new_chunk.text_width = 0.0; + new_chunk.whitespace_count = 0; + new_chunk.scanrun_width = scan_run.width(); + new_chunk.x = scan_run.x_start; + + // we haven't done anything yet so the last valid break position is the beginning + BrokenSpan last_span_at_break, last_span_at_emergency_break; + last_span_at_break.start = start_span_pos; + last_span_at_break.setZero(); + last_span_at_emergency_break.start = start_span_pos; + last_span_at_emergency_break.setZero(); + + TRACE((" trying chunk from %f to %g\n", scan_run.x_start, scan_run.x_end)); + BrokenSpan new_span; + new_span.end = start_span_pos; + while (new_span.end.iter_span != para.unbroken_spans.end()) { // this loops once for each UnbrokenSpan + new_span.start = new_span.end; + + // force a chunk change at x or y attribute change + if ((new_span.start.iter_span->x._set || new_span.start.iter_span->y._set) && new_span.start.char_byte == 0) { + + if (new_span.start.iter_span != start_span_pos.iter_span) + chunk_info->push_back(new_chunk); + + new_chunk.x += new_chunk.text_width; + new_chunk.text_width = 0.0; + new_chunk.whitespace_count = 0; + new_chunk.broken_spans.clear(); + if (new_span.start.iter_span->x._set) new_chunk.x = new_span.start.iter_span->x.computed; + // y doesn't need to be done until output time + } + + // see if this span is too tall to fit on the current line + FontMetrics new_span_height = new_span.start.iter_span->line_height; + new_span_height.computeEffective( new_span.start.iter_span->line_height_multiplier ); + + /* floating point 80-bit/64-bit rounding problems require epsilon. See + discussion http://inkscape.gristle.org/2005-03-16.txt around 22:00 */ + if ( new_span_height.ascent > line_height->ascent + std::numeric_limits::epsilon() || + new_span_height.descent > line_height->descent + std::numeric_limits::epsilon() ) { + // Take larger of each of the two ascents and two descents per CSS + line_height->max(new_span_height); + + // Currently always true for flowed text and false for Inkscape multiline text. + if (!_scanline_maker->canExtendCurrentScanline(*line_height)) { + return false; + } + } + + bool span_fitted = _measureUnbrokenSpan(para, &new_span, &last_span_at_break, &last_span_at_emergency_break, new_chunk.scanrun_width - new_chunk.text_width); + + new_chunk.text_width += new_span.width; + new_chunk.whitespace_count += new_span.whitespace_count; + new_chunk.broken_spans.push_back(new_span); // if !span_fitted we'll correct ourselves below + + if (!span_fitted) break; + + if (new_span.end.iter_span == para.unbroken_spans.end()) { + last_span_at_break = new_span; + break; + } + + PangoLogAttr const &char_attributes = _charAttributes(para, new_span.end); + if (char_attributes.is_mandatory_break) { + last_span_at_break = new_span; + break; + } + } + + TRACE((" chunk complete, used %f width (%d whitespaces, %lu brokenspans)\n", new_chunk.text_width, new_chunk.whitespace_count, new_chunk.broken_spans.size())); + chunk_info->push_back(new_chunk); + + if (scan_run.width() >= 4.0 * line_height->emSize() && last_span_at_break.end == start_span_pos) { + /* **non-SVG spec bit**: See bug #1191102 + If the user types a very long line with no spaces, the way the spec + is written at the moment means that when the length of the text + exceeds the available width of all remaining areas, the text is + completely hidden. This condition alters that behaviour so that if + the length of the line is greater than four times the line-height + and there are no spaces, it'll be emergency-wrapped at the last + character. One could read the SVG Tiny 1.2 draft as permitting this + sort of behaviour, but it's still a bit dodgy. The hard-coding of + 4x is not nice, either. */ + last_span_at_break = last_span_at_emergency_break; + } + + if (!chunk_info->back().broken_spans.empty() && last_span_at_break.end != chunk_info->back().broken_spans.back().end) { + // need to back out spans until we come to the one with the last break in it + while (!chunk_info->empty() && last_span_at_break.start.iter_span != chunk_info->back().broken_spans.back().start.iter_span) { + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().width; + chunk_info->back().whitespace_count -= chunk_info->back().broken_spans.back().whitespace_count; + chunk_info->back().broken_spans.pop_back(); + if (chunk_info->back().broken_spans.empty()) + chunk_info->pop_back(); + } + if (!chunk_info->empty()) { + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().width; + chunk_info->back().whitespace_count -= chunk_info->back().broken_spans.back().whitespace_count; + if (last_span_at_break.start == last_span_at_break.end) { + chunk_info->back().broken_spans.pop_back(); // last break was at an existing boundary + if (chunk_info->back().broken_spans.empty()) + chunk_info->pop_back(); + } else { + chunk_info->back().broken_spans.back() = last_span_at_break; + chunk_info->back().text_width += last_span_at_break.width; + chunk_info->back().whitespace_count += last_span_at_break.whitespace_count; + } + TRACE((" correction: fitted span %lu width = %f\n", last_span_at_break.start.iter_span - para.unbroken_spans.begin(), last_span_at_break.width)); + } + } + + // Recalculate line_box_height after backing out chunks + *line_height = line_height_saved; + for (const auto & it_chunk : *chunk_info) { + for (const auto & broken_span : it_chunk.broken_spans) { + FontMetrics span_height = broken_span.start.iter_span->line_height; + TRACE((" brokenspan line_height: %f\n", span_height.emSize() )); + span_height.computeEffective( broken_span.start.iter_span->line_height_multiplier ); + line_height->max( span_height ); + } + } + TRACE((" line_box_height: %f\n", line_height->emSize())); + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty() && chunk_info->back().broken_spans.back().ends_with_whitespace) { + // for justification we need to discard space occupied by the single whitespace at the end of the chunk + TRACE((" backing out whitespace\n")); + chunk_info->back().broken_spans.back().ends_with_whitespace = false; + chunk_info->back().broken_spans.back().width -= chunk_info->back().broken_spans.back().each_whitespace_width; + chunk_info->back().broken_spans.back().whitespace_count--; + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().each_whitespace_width; + chunk_info->back().whitespace_count--; + } + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty() ) { + // for justification we need to discard line-spacing and word-spacing at end of the chunk + chunk_info->back().broken_spans.back().width -= chunk_info->back().broken_spans.back().letter_spacing; + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().letter_spacing; + TRACE((" width after subtracting last letter_spacing: %f\n", chunk_info->back().broken_spans.back().width)); + } + + TRACE((" end _buildChunksInScanRun: chunks: %lu\n", chunk_info->size())); + return true; +} + +#ifdef DEBUG_LAYOUT_TNG_COMPUTE +/** + * For debugging, not called in distributed code + * + * Input: para->first_input_index, para->pango_items + */ +void Layout::Calculator::dumpPangoItemsOut(ParagraphInfo *para){ + std::cout << "Pango items: " << para->pango_items.size() << std::endl; + font_factory * factory = font_factory::Default(); + for(unsigned pidx = 0 ; pidx < para->pango_items.size(); pidx++){ + std::cout + << "idx: " << pidx + << " offset: " + << para->pango_items[pidx].item->offset + << " length: " + << para->pango_items[pidx].item->length + << " font: " + << factory->ConstructFontSpecification( para->pango_items[pidx].font ) + << std::endl; + } +} + +/** + * For debugging, not called in distributed code + * + * Input: para->first_input_index, para->pango_items + */ +void Layout::Calculator::dumpUnbrokenSpans(ParagraphInfo *para){ + std::cout << "Unbroken Spans: " << para->unbroken_spans.size() << std::endl; + for(unsigned uidx = 0 ; uidx < para->unbroken_spans.size(); uidx++){ + std::cout + << "idx: " << uidx + << " pango_item_index: " << para->unbroken_spans[uidx].pango_item_index + << " input_index: " << para->unbroken_spans[uidx].input_index + << " char_index_in_para: " << para->unbroken_spans[uidx].char_index_in_para + << " text_bytes: " << para->unbroken_spans[uidx].text_bytes + << std::endl; + } +} +#endif //DEBUG_LAYOUT_TNG_COMPUTE + +/** The management function to start the whole thing off. */ +bool Layout::Calculator::calculate() +{ + if (_flow._input_stream.empty()) + return false; + /** + * hm, why do we want assert (crash) the application, now do simply return false + * \todo check if this is the correct behaviour + * g_assert(_flow._input_stream.front()->Type() == TEXT_SOURCE); + */ + if (_flow._input_stream.front()->Type() != TEXT_SOURCE) + { + g_warning("flow text is not of type TEXT_SOURCE. Abort."); + return false; + } + TRACE(("begin calculate()\n")); + + _flow._clearOutputObjects(); + + _pango_context = (font_factory::Default())->fontContext; + + _font_factory_size_multiplier = (font_factory::Default())->fontSize; + + _block_progression = _flow._blockProgression(); + if( _block_progression == RIGHT_TO_LEFT || _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, CJK + switch (_flow._blockTextOrientation()) { + case SP_CSS_TEXT_ORIENTATION_MIXED: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_EAST); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_NATURAL); + break; + case SP_CSS_TEXT_ORIENTATION_UPRIGHT: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_EAST); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_STRONG); + break; + case SP_CSS_TEXT_ORIENTATION_SIDEWAYS: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_SOUTH); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_STRONG); + break; + default: + std::cerr << "Layout::Calculator: Unhandled text orientation!" << std::endl; + } + } else { + // Horizontal text + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_AUTO); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_NATURAL); + } + + // Minimum line box height determined by block container. + FontMetrics strut_height = _flow.strut; + _y_offset = 0.0; + _createFirstScanlineMaker(); + + ParagraphInfo para; + FontMetrics line_box_height; // Current value of line box height for line. + bool keep_going = true; // Set false if we ran out of space and had to stash overflow. + for(para.first_input_index = 0 ; para.first_input_index < _flow._input_stream.size() ; ) { + + // jump to the next wrap shape if this is a SHAPE_BREAK control code + if (_flow._input_stream[para.first_input_index]->Type() == CONTROL_CODE) { + InputStreamControlCode const *control_code = static_cast(_flow._input_stream[para.first_input_index]); + if (control_code->code == SHAPE_BREAK) { + TRACE(("shape break control code\n")); + if (!_goToNextWrapShape()) { + std::cerr << "Layout::Calculator::calculate: Found SHAPE_BREAK but out of shapes!" << std::endl; + } + continue; // Go to next paragraph (paragraph only contained control code). + } + } + + // Break things up into little pango units with unique direction, gravity, etc. + _buildPangoItemizationForPara(¶); + + // Do shaping (convert characters to glyphs) + unsigned para_end_input_index = _buildSpansForPara(¶); + + if (_flow._input_stream[para.first_input_index]->Type() == TEXT_SOURCE) + para.alignment = static_cast(_flow._input_stream[para.first_input_index])->styleGetAlignment(para.direction, !_flow._input_wrap_shapes.empty()); + else + para.alignment = para.direction == LEFT_TO_RIGHT ? LEFT : RIGHT; + + TRACE(("para prepared, adding as #%lu\n", _flow._paragraphs.size())); + Layout::Paragraph new_paragraph; + new_paragraph.base_direction = para.direction; + new_paragraph.alignment = para.alignment; + _flow._paragraphs.push_back(new_paragraph); + + // start scanning lines + UnbrokenSpanPosition span_pos; + span_pos.iter_span = para.unbroken_spans.begin(); + span_pos.char_byte = 0; + span_pos.char_index = 0; + + do { // Until end of paragraph + TRACE(("begin line\n")); + + std::vector line_chunk_info; + + // Fill line. + // If we've run out of space, we've put the remaining text in a single line and + // returned false. If we ran out of space on previous paragraph, we continue with + // single-line scan-line maker. + bool flowed =_findChunksForLine(para, &span_pos, &line_chunk_info, &line_box_height, &strut_height ); + if (!flowed) { + keep_going = false; + } + + if (line_box_height.emSize() < 0.001 && line_chunk_info.empty()) { + // We need to avoid an infinite (or semi-infinite) loop. + std::cerr << "Layout::Calculator::calculate: No room for text and line advance is very small" << std::endl; + return false; // For the moment + } + + + // For Inkscape multi-line text (using role="line") we run into a problem if the first + // line is empty - namely, there is no character to attach a 'y' attribute value. The + // result is that the code that takes a baseline position (e.g. 'y') and finds the top + // of the layout box is bypassed resulting in wrongly placed text (we layout the text + // relative to the top of the box as this is required for text-in-a-shape). We don't + // know how to find the top of the box from the 'y' position until we have found the + // line height parameters for the given line (after calling _findChunksForLine() just + // above). + if (para.first_input_index == 0 && (_flow.wrap_mode == WRAP_NONE)) { + + // Calculate new top of box... given specified baseline. + double top_of_line_box = _scanline_maker->yCoordinate(); // Set in constructor. + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + top_of_line_box += 0.5 * line_box_height.emSize(); + } else if (_block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + top_of_line_box -= 0.5 * line_box_height.emSize(); + } else { + top_of_line_box -= line_box_height.getTypoAscent(); + } + TRACE((" y attribute set, next line top_of_line_box: %f\n", top_of_line_box )); + // Set the initial y coordinate of the for this line (see above). + _scanline_maker->setNewYCoordinate(top_of_line_box); + } + + // !keep_going --> truncated --> hidden + _outputLine(para, line_box_height, line_chunk_info, !keep_going); + + _scanline_maker->setLineHeight( line_box_height ); + _scanline_maker->completeLine(); // Increments y by line height + TRACE(("end line\n")); + } while (span_pos.iter_span != para.unbroken_spans.end()); + + TRACE(("para %lu end\n\n", _flow._paragraphs.size() - 1)); + if (keep_going) { + // We have more to do, setup next section. + bool is_empty_para = _flow._characters.empty() || _flow._characters.back().line(&_flow).in_paragraph != _flow._paragraphs.size() - 1; + if ((is_empty_para && para_end_input_index + 1 >= _flow._input_stream.size()) + || para_end_input_index + 1 < _flow._input_stream.size()) { + // we need a span just for the para if it's either an empty last para or a break in the middle + Layout::Span new_span; + if (_flow._spans.empty()) { + new_span.font = nullptr; + new_span.font_size = line_box_height.emSize(); + new_span.line_height = line_box_height; + new_span.x_end = 0.0; + } else { + new_span = _flow._spans.back(); + if (_flow._chunks[new_span.in_chunk].in_line != _flow._lines.size() - 1) + new_span.x_end = 0.0; + } + new_span.in_chunk = _flow._chunks.size() - 1; + if (new_span.font) + new_span.font->Ref(); + new_span.x_start = new_span.x_end; + new_span.baseline_shift = 0.0; + new_span.direction = para.direction; + new_span.block_progression = _block_progression; + if (para_end_input_index == _flow._input_stream.size()) + new_span.in_input_stream_item = _flow._input_stream.size() - 1; + else + new_span.in_input_stream_item = para_end_input_index; + _flow._spans.push_back(new_span); + } + if (para_end_input_index + 1 < _flow._input_stream.size()) { + // we've got to add an invisible character between paragraphs so that we can position iterators + // (and hence cursors) both before and after the paragraph break + Layout::Character new_character; + new_character.the_char = '@'; + new_character.in_span = _flow._spans.size() - 1; + new_character.char_attributes.is_line_break = 1; + new_character.char_attributes.is_mandatory_break = 1; + new_character.char_attributes.is_char_break = 1; + new_character.char_attributes.is_white = 1; + new_character.char_attributes.is_cursor_position = 1; + new_character.char_attributes.is_word_start = 0; + new_character.char_attributes.is_word_end = 1; + new_character.char_attributes.is_sentence_start = 0; + new_character.char_attributes.is_sentence_end = 1; + new_character.char_attributes.is_sentence_boundary = 1; + new_character.char_attributes.backspace_deletes_character = 1; + new_character.x = _flow._spans.back().x_end - _flow._spans.back().x_start; + new_character.in_glyph = -1; + _flow._characters.push_back(new_character); + } + } + + para.free(); + para.first_input_index = para_end_input_index + 1; + } // Loop over paras + + para.free(); + if (_scanline_maker) { + delete _scanline_maker; + } + + _flow._input_truncated = !keep_going; + + if (_flow.textLength._set) { + // Calculate the adjustment needed to meet the textLength + double actual_length = _flow.getActualLength(); + double difference = _flow.textLength.computed - actual_length; + _flow.textLengthMultiplier = (actual_length + difference) / actual_length; + _flow.textLengthIncrement = difference / (_flow._characters.size() == 1? 1 : _flow._characters.size() - 1); + } + + return true; +} + +void Layout::_calculateCursorShapeForEmpty() +{ + _empty_cursor_shape.position = Geom::Point(0, 0); + _empty_cursor_shape.height = 0.0; + _empty_cursor_shape.rotation = 0.0; + if (_input_stream.empty() || _input_stream.front()->Type() != TEXT_SOURCE) + return; + + InputStreamTextSource const *text_source = static_cast(_input_stream.front()); + + font_instance *font = text_source->styleGetFontInstance(); + double font_size = text_source->style->font_size.computed; + double caret_slope_run = 0.0, caret_slope_rise = 1.0; + FontMetrics line_height; + if (font) { + const_cast(font)->FontSlope(caret_slope_run, caret_slope_rise); + font->FontMetrics(line_height.ascent, line_height.descent, line_height.xheight); + line_height *= font_size; + font->Unref(); + } + + double caret_slope = atan2(caret_slope_run, caret_slope_rise); + _empty_cursor_shape.height = font_size / cos(caret_slope); + _empty_cursor_shape.rotation = caret_slope; + + if (_input_wrap_shapes.empty()) { + _empty_cursor_shape.position = Geom::Point(text_source->x.empty() || !text_source->x.front()._set ? 0.0 : text_source->x.front().computed, + text_source->y.empty() || !text_source->y.front()._set ? 0.0 : text_source->y.front().computed); + } else if (wrap_mode == WRAP_INLINE_SIZE) { + // 'inline-size' has a wrap shape of an "infinite" rectangle, we need the place where the text should begin. + double x = 0; + double y = 0; + if (!text_source->x.empty()) + x = text_source->x.front().computed; + if (!text_source->y.empty()) + y = text_source->y.front().computed; + _empty_cursor_shape.position = Geom::Point(x, y); + } else { + Direction block_progression = text_source->styleGetBlockProgression(); + ShapeScanlineMaker scanline_maker(_input_wrap_shapes.front().shape, block_progression); + std::vector scan_runs = scanline_maker.makeScanline(line_height); + if (!scan_runs.empty()) { + if (block_progression == LEFT_TO_RIGHT || block_progression == RIGHT_TO_LEFT) { + // Vertical text + _empty_cursor_shape.position = Geom::Point(scan_runs.front().y + font_size, scan_runs.front().x_start); + } else { + // Horizontal text + _empty_cursor_shape.position = Geom::Point(scan_runs.front().x_start, scan_runs.front().y + font_size); + } + } + } +} + +bool Layout::calculateFlow() +{ + TRACE(("begin calculateFlow()\n")); + Layout::Calculator calc = Calculator(this); + bool result = calc.calculate(); + + if (textLengthIncrement != 0) { + TRACE(("Recalculating layout the second time to fit textLength!\n")); + result = calc.calculate(); + } + + if (_characters.empty()) { + _calculateCursorShapeForEmpty(); + } + return result; +} + +}//namespace Text +}//namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Input.cpp b/src/libnrtype/Layout-TNG-Input.cpp new file mode 100644 index 0000000..a90f96c --- /dev/null +++ b/src/libnrtype/Layout-TNG-Input.cpp @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine input functions + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include +#include "Layout-TNG.h" +#include "style.h" +#include "svg/svg-length.h" +#include "FontFactory.h" + + +namespace Inkscape { +namespace Text { + +void Layout::_clearInputObjects() +{ + for(auto & it : _input_stream) { + delete it; + } + + _input_stream.clear(); + _input_wrap_shapes.clear(); +} + +// this function does nothing more than store all its parameters for future reference +void Layout::appendText(Glib::ustring const &text, + SPStyle *style, + SPObject *source, + OptionalTextTagAttrs const *optional_attributes, + unsigned optional_attributes_offset, + Glib::ustring::const_iterator text_begin, + Glib::ustring::const_iterator text_end) +{ + if (style == nullptr) return; + + InputStreamTextSource *new_source = new InputStreamTextSource; + + new_source->source = source; + new_source->text = &text; + new_source->text_begin = text_begin; + new_source->text_end = text_end; + new_source->style = style; + sp_style_ref(style); + + new_source->text_length = 0; + for ( ; text_begin != text_end && text_begin != text.end() ; ++text_begin) + new_source->text_length++; // save this because calculating the length of a UTF-8 string is expensive + + if (optional_attributes) { + // we need to fill in x and y even if the text is empty so that empty paragraphs can be positioned correctly + _copyInputVector(optional_attributes->x, optional_attributes_offset, &new_source->x, std::max(1, new_source->text_length)); + _copyInputVector(optional_attributes->y, optional_attributes_offset, &new_source->y, std::max(1, new_source->text_length)); + _copyInputVector(optional_attributes->dx, optional_attributes_offset, &new_source->dx, new_source->text_length); + _copyInputVector(optional_attributes->dy, optional_attributes_offset, &new_source->dy, new_source->text_length); + _copyInputVector(optional_attributes->rotate, optional_attributes_offset, &new_source->rotate, new_source->text_length); + if (!optional_attributes->rotate.empty() && optional_attributes_offset >= optional_attributes->rotate.size()) { + SVGLength last_rotate; + last_rotate = 0.f; + for (auto it : optional_attributes->rotate) + if (it._set) + last_rotate = it; + new_source->rotate.resize(1, last_rotate); + } + new_source->textLength._set = optional_attributes->textLength._set; + new_source->textLength.value = optional_attributes->textLength.value; + new_source->textLength.computed = optional_attributes->textLength.computed; + new_source->textLength.unit = optional_attributes->textLength.unit; + new_source->lengthAdjust = optional_attributes->lengthAdjust; + } + + _input_stream.push_back(new_source); +} + +void Layout::_copyInputVector(std::vector const &input_vector, unsigned input_offset, std::vector *output_vector, size_t max_length) +{ + output_vector->clear(); + if (input_offset >= input_vector.size()) return; + output_vector->reserve(std::min(max_length, input_vector.size() - input_offset)); + while (input_offset < input_vector.size() && max_length != 0) { + if (!input_vector[input_offset]._set) + break; + output_vector->push_back(input_vector[input_offset]); + input_offset++; + max_length--; + } +} + +// just save what we've been given, really +void Layout::appendControlCode(TextControlCode code, SPObject *source, double width, double ascent, double descent) +{ + InputStreamControlCode *new_code = new InputStreamControlCode; + + new_code->source = source; + new_code->code = code; + new_code->width = width; + new_code->ascent = ascent; + new_code->descent = descent; + + _input_stream.push_back(new_code); +} + +// more saving of the parameters +void Layout::appendWrapShape(Shape const *shape, DisplayAlign display_align) +{ + _input_wrap_shapes.emplace_back(); + _input_wrap_shapes.back().shape = shape; + _input_wrap_shapes.back().display_align = display_align; +} + +Layout::Direction Layout::InputStreamTextSource::styleGetBlockProgression() const +{ + switch( style->writing_mode.computed ) { + case SP_CSS_WRITING_MODE_LR_TB: + case SP_CSS_WRITING_MODE_RL_TB: + return TOP_TO_BOTTOM; + + case SP_CSS_WRITING_MODE_TB_RL: + return RIGHT_TO_LEFT; + + case SP_CSS_WRITING_MODE_TB_LR: + return LEFT_TO_RIGHT; + + default: + std::cerr << "Layout::InputTextStream::styleGetBlockProgression: invalid writing mode." << std::endl; + } + return TOP_TO_BOTTOM; +} + +SPCSSTextOrientation Layout::InputStreamTextSource::styleGetTextOrientation() const +{ + return ((SPCSSTextOrientation)style->text_orientation.computed); +} + +SPCSSBaseline Layout::InputStreamTextSource::styleGetDominantBaseline() const +{ + return ((SPCSSBaseline)style->dominant_baseline.computed); +} + +static Layout::Alignment text_anchor_to_alignment(unsigned anchor, Layout::Direction para_direction) +{ + switch (anchor) { + default: + case SP_CSS_TEXT_ANCHOR_START: return para_direction == Layout::LEFT_TO_RIGHT ? Layout::LEFT : Layout::RIGHT; + case SP_CSS_TEXT_ANCHOR_MIDDLE: return Layout::CENTER; + case SP_CSS_TEXT_ANCHOR_END: return para_direction == Layout::LEFT_TO_RIGHT ? Layout::RIGHT : Layout::LEFT; + } +} + +Layout::Alignment Layout::InputStreamTextSource::styleGetAlignment(Layout::Direction para_direction, bool try_text_align) const +{ + if (!try_text_align) + return text_anchor_to_alignment(style->text_anchor.computed, para_direction); + + // there's no way to tell the difference between text-anchor set higher up the cascade to the default and + // text-anchor never set anywhere in the cascade, so in order to detect which of text-anchor or text-align + // to use we'll have to run up the style tree ourselves. + SPStyle const *this_style = style; + + for ( ; ; ) { + // If both text-align and text-anchor are set at the same level, text-align takes + // precedence because it is the most expressive. + if (this_style->text_align.set) { + switch (style->text_align.computed) { + default: + case SP_CSS_TEXT_ALIGN_START: return para_direction == LEFT_TO_RIGHT ? LEFT : RIGHT; + case SP_CSS_TEXT_ALIGN_END: return para_direction == LEFT_TO_RIGHT ? RIGHT : LEFT; + case SP_CSS_TEXT_ALIGN_LEFT: return LEFT; + case SP_CSS_TEXT_ALIGN_RIGHT: return RIGHT; + case SP_CSS_TEXT_ALIGN_CENTER: return CENTER; + case SP_CSS_TEXT_ALIGN_JUSTIFY: return FULL; + } + } + if (this_style->text_anchor.set) + return text_anchor_to_alignment(this_style->text_anchor.computed, para_direction); + if (this_style->object == nullptr || this_style->object->parent == nullptr) break; + this_style = this_style->object->parent->style; + if (this_style == nullptr) break; + } + return para_direction == LEFT_TO_RIGHT ? LEFT : RIGHT; +} + +font_instance *Layout::InputStreamTextSource::styleGetFontInstance() const +{ + PangoFontDescription *descr = styleGetFontDescription(); + if (descr == nullptr) return nullptr; + font_instance *res = (font_factory::Default())->Face(descr); + pango_font_description_free(descr); + return res; +} + +PangoFontDescription *Layout::InputStreamTextSource::styleGetFontDescription() const +{ + // This use to be done by code here but it duplicated more complete code in FontFactory.cpp. + PangoFontDescription *descr = ink_font_description_from_style( style ); + + // Font size not yet set +#ifdef USE_PANGO_WIN32 + + // Damn Pango fudges the size, so we need to unfudge. See source of pango_win32_font_map_init() + pango_font_description_set_size(descr, + (int) ((font_factory::Default())->fontSize*PANGO_SCALE*72 / GetDeviceCaps(pango_win32_get_dc(),LOGPIXELSY)) + ); + + // We unset stretch on Win32, because pango-win32 has no concept of it + // (Windows doesn't really provide any useful field it could use). + // If we did set stretch, then any text with a font-stretch attribute would + // end up falling back to a default. + pango_font_description_unset_fields(descr, PANGO_FONT_MASK_STRETCH); + +#else + + // mandatory huge size (hinting workaround) + pango_font_description_set_size(descr, (int) ((font_factory::Default())->fontSize*PANGO_SCALE)); + +#endif + + return descr; +} + +Layout::InputStreamTextSource::~InputStreamTextSource() +{ + sp_style_unref(style); +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG-OutIter.cpp b/src/libnrtype/Layout-TNG-OutIter.cpp new file mode 100644 index 0000000..a9ec782 --- /dev/null +++ b/src/libnrtype/Layout-TNG-OutIter.cpp @@ -0,0 +1,1172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine output functions using iterators + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG.h" +#include "livarot/Path.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include <2geom/transforms.h> +#include <2geom/line.h> +#include "style.h" + +namespace Inkscape { +namespace Text { + +// Comment 18 Sept 2019: +// Cursor code might be simpler if Character was turned into a proper +// class and kept track of its absolute postion and extent. This would +// make handling multi-line text (including multi-line text using +// 'white-space:pre') easier. This would also avoid problems where +// 'dx','dy' moved the character a long distance from its nominal +// position. + +Layout::iterator Layout::_cursorXOnLineToIterator(unsigned line_index, double local_x, double local_y) const +{ + unsigned char_index = _lineToCharacter(line_index); + int best_char_index = -1; + double best_difference = DBL_MAX; + + if (char_index == _characters.size()) return end(); + for ( ; char_index < _characters.size() ; char_index++) { + if (_characters[char_index].chunk(this).in_line != line_index) break; + //if (_characters[char_index].char_attributes.is_mandatory_break) break; + if (!_characters[char_index].char_attributes.is_cursor_position) continue; + + double delta_x = + _characters[char_index].x + + _characters[char_index].span(this).x_start + + _characters[char_index].chunk(this).left_x - + local_x; + + double delta_y = + _characters[char_index].span(this).y_offset + + _characters[char_index].line(this).baseline_y - + local_y; + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_difference = this_difference; + best_char_index = char_index; + } + } + + // also try the very end of a para (not lines though because the space wraps) + if (char_index == _characters.size() || _characters[char_index].char_attributes.is_mandatory_break) { + + double delta_x = 0.0; + double delta_y = 0.0; + + if (char_index == 0) { + delta_x = _spans.front().x_end + _chunks.front().left_x - local_x; + delta_y = _spans.front().y_offset + _spans.front().line(this).baseline_y - local_y; + } else { + delta_x = _characters[char_index - 1].span(this).x_end + _characters[char_index - 1].chunk(this).left_x - local_x; + delta_y = _characters[char_index - 1].span(this).y_offset + _characters[char_index - 1].line(this).baseline_y - local_y; + } + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_char_index = char_index; + best_difference = this_difference; + } + } + + + if (best_char_index == -1) { + best_char_index = char_index; + } + + if (best_char_index == _characters.size()) { + return end(); + } + + return iterator(this, best_char_index); +} + +double Layout::_getChunkWidth(unsigned chunk_index) const +{ + double chunk_width = 0.0; + unsigned span_index; + if (chunk_index) { + span_index = _lineToSpan(_chunks[chunk_index].in_line); + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk < chunk_index ; span_index++){}; + } else { + span_index = 0; + } + + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + + return chunk_width; +} + +/* getting the cursor position for a mouse click is not as simple as it might +seem. The two major problems are flows set up in multiple columns and large +dy adjustments such that text does not belong to the line it appears to. In +the worst case it's possible to have two characters on top of each other, in +which case the one we pick is arbitrary. + +This is a 3-stage (2 pass) algorithm: +1) search all the spans to see if the point is contained in one, if so take + that. Note that this will collect all clicks from the current UI because + of how the hit detection of nrarena objects works. +2) if that fails, run through all the chunks finding a best guess of the one + the user wanted. This is the one whose y coordinate is nearest, or if + there's a tie, the x. +3) search in that chunk using x-coordinate only to find the position. +*/ +Layout::iterator Layout::getNearestCursorPositionTo(double x, double y) const +{ + if (_lines.empty()) return begin(); + double local_x = x; + double local_y = y; + + if (_path_fitted) { + Path::cut_position position = const_cast(_path_fitted)->PointToCurvilignPosition(Geom::Point(x, y)); + local_x = const_cast(_path_fitted)->PositionToLength(position.piece, position.t); + return _cursorXOnLineToIterator(0, local_x + _chunks.front().left_x); + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + local_x = y; + local_y = x; + } + + // stage 1: + for (const auto & _span : _spans) { + double span_left, span_right; + if (_span.x_start < _span.x_end) { + span_left = _span.x_start; + span_right = _span.x_end; + } else { + span_left = _span.x_end; + span_right = _span.x_start; + } + + double y_line = _span.line(this).baseline_y + _span.baseline_shift + _span.y_offset; + if ( local_x >= _chunks[_span.in_chunk].left_x + span_left + && local_x <= _chunks[_span.in_chunk].left_x + span_right + && local_y >= y_line - _span.line_height.ascent + && local_y <= y_line + _span.line_height.descent) { + return _cursorXOnLineToIterator(_chunks[_span.in_chunk].in_line, local_x, local_y); + } + } + + // stage 2: + unsigned span_index = 0; + unsigned chunk_index; + int best_chunk_index = -1; + double best_y_range = DBL_MAX; + double best_x_range = DBL_MAX; + for (chunk_index = 0 ; chunk_index < _chunks.size() ; chunk_index++) { + FontMetrics line_height; + line_height *= 0.0; // Set all metrics to zero. + double chunk_width = 0.0; + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + line_height.max(_spans[span_index].line_height); + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + double this_y_range; + if (local_y < _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent) + this_y_range = _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent - local_y; + else if (local_y > _lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent) + this_y_range = local_y - (_lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent); + else + this_y_range = 0.0; + if (this_y_range <= best_y_range) { + if (this_y_range < best_y_range) best_x_range = DBL_MAX; + double this_x_range; + if (local_x < _chunks[chunk_index].left_x) + this_x_range = _chunks[chunk_index].left_x - local_y; + else if (local_x > _chunks[chunk_index].left_x + chunk_width) + this_x_range = local_x - (_chunks[chunk_index].left_x + chunk_width); + else + this_x_range = 0.0; + if (this_x_range < best_x_range) { + best_y_range = this_y_range; + best_x_range = this_x_range; + best_chunk_index = chunk_index; + } + } + } + + // stage 3: + if (best_chunk_index == -1) return begin(); // never happens + return _cursorXOnLineToIterator(_chunks[best_chunk_index].in_line, local_x, local_y); +} + +Layout::iterator Layout::getLetterAt(double x, double y) const +{ + Geom::Point point(x, y); + + double rotation; + for (iterator it = begin() ; it != end() ; it.nextCharacter()) { + Geom::Rect box = characterBoundingBox(it, &rotation); + // todo: rotation + if (box.contains(point)) return it; + } + return end(); +} + +Layout::iterator Layout::sourceToIterator(SPObject *source /*, Glib::ustring::const_iterator text_iterator*/) const +{ + unsigned source_index; + if (_characters.empty()) return end(); + for (source_index = 0 ; source_index < _input_stream.size() ; source_index++) + if (_input_stream[source_index]->source == source) break; + if (source_index == _input_stream.size()) return end(); + + unsigned char_index = _sourceToCharacter(source_index); + + // Fix a bug when hidding content in flow box element + if (char_index >= _characters.size()) + return end(); + + if (_input_stream[source_index]->Type() != TEXT_SOURCE) + return iterator(this, char_index); + + return iterator(this, char_index); + /* This code was never used, the text_iterator argument was "NULL" in all calling code + InputStreamTextSource const *text_source = static_cast(_input_stream[source_index]); + + if (text_iterator <= text_source->text_begin) return iterator(this, char_index); + if (text_iterator >= text_source->text_end) { + if (source_index == _input_stream.size() - 1) return end(); + return iterator(this, _sourceToCharacter(source_index + 1)); + } + Glib::ustring::const_iterator iter_text = text_source->text_begin; + for ( ; char_index < _characters.size() ; char_index++) { + if (iter_text == text_iterator) + return iterator(this, char_index); + iter_text++; + } + return end(); // never happens + */ +} + +Geom::OptRect Layout::glyphBoundingBox(iterator const &it, double *rotation) const +{ + if (rotation) *rotation = _glyphs[it._glyph_index].rotation; + return _glyphs[it._glyph_index].span(this).font->BBox(_glyphs[it._glyph_index].glyph); +} + +Geom::Point Layout::characterAnchorPoint(iterator const &it) const +{ + if (_characters.empty()) + return _empty_cursor_shape.position; + if (it._char_index == _characters.size()) { + return Geom::Point(_chunks.back().left_x + _spans.back().x_end, _lines.back().baseline_y + _spans.back().baseline_shift); + } else { + return Geom::Point(_characters[it._char_index].chunk(this).left_x + + _spans[_characters[it._char_index].in_span].x_start + + _characters[it._char_index].x, + _characters[it._char_index].line(this).baseline_y + + _characters[it._char_index].span(this).baseline_shift); + } +} + +boost::optional Layout::baselineAnchorPoint() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + if (this->_blockProgression() == LEFT_TO_RIGHT || this->_blockProgression() == RIGHT_TO_LEFT) { + left_pt = Geom::Point(left_pt[Geom::Y], left_pt[Geom::X]); + right_pt = Geom::Point(right_pt[Geom::Y], right_pt[Geom::X]); + } + + switch (this->paragraphAlignment(pos)) { + case LEFT: + case FULL: + return left_pt; + break; + case CENTER: + return (left_pt + right_pt)/2; // middle point + break; + case RIGHT: + return right_pt; + break; + default: + return boost::optional(); + break; + } +} + +Geom::Path Layout::baseline() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + if (this->_blockProgression() == LEFT_TO_RIGHT || this->_blockProgression() == RIGHT_TO_LEFT) { + left_pt = Geom::Point(left_pt[Geom::Y], left_pt[Geom::X]); + right_pt = Geom::Point(right_pt[Geom::Y], right_pt[Geom::X]); + } + + Geom::Path baseline; + baseline.start(left_pt); + baseline.appendNew(right_pt); + + return baseline; +} + + +Geom::Point Layout::chunkAnchorPoint(iterator const &it) const +{ + unsigned chunk_index; + + if (_chunks.empty()) + return Geom::Point(0.0, 0.0); + + if (_characters.empty()) + chunk_index = 0; + else if (it._char_index == _characters.size()) + chunk_index = _chunks.size() - 1; + else chunk_index = _characters[it._char_index].span(this).in_chunk; + + Alignment alignment = _paragraphs[_lines[_chunks[chunk_index].in_line].in_paragraph].alignment; + if (alignment == LEFT || alignment == FULL) + return Geom::Point(_chunks[chunk_index].left_x, _lines[_chunks[chunk_index].in_line].baseline_y); + + double chunk_width = _getChunkWidth(chunk_index); + if (alignment == RIGHT) + return Geom::Point(_chunks[chunk_index].left_x + chunk_width, _lines[_chunks[chunk_index].in_line].baseline_y); + //centre + return Geom::Point(_chunks[chunk_index].left_x + chunk_width * 0.5, _lines[_chunks[chunk_index].in_line].baseline_y); +} + +Geom::Rect Layout::characterBoundingBox(iterator const &it, double *rotation) const +{ + Geom::Point top_left, bottom_right; + unsigned char_index = it._char_index; + + if (_path_fitted) { + double cluster_half_width = 0.0; + for (int glyph_index = _characters[char_index].in_glyph ; _glyphs.size() != glyph_index ; glyph_index++) { + if (_glyphs[glyph_index].in_character != char_index) break; + cluster_half_width += _glyphs[glyph_index].advance; + } + cluster_half_width *= 0.5; + + double midpoint_offset = _characters[char_index].span(this).x_start + _characters[char_index].x + cluster_half_width; + int unused = 0; + Path::cut_position *midpoint_otp = const_cast(_path_fitted)->CurvilignToPosition(1, &midpoint_offset, unused); + if (midpoint_offset >= 0.0 && midpoint_otp != nullptr && midpoint_otp[0].piece >= 0) { + Geom::Point midpoint; + Geom::Point tangent; + Span const &span = _characters[char_index].span(this); + + const_cast(_path_fitted)->PointAndTangentAt(midpoint_otp[0].piece, midpoint_otp[0].t, midpoint, tangent); + top_left[Geom::X] = midpoint[Geom::X] - cluster_half_width; + top_left[Geom::Y] = midpoint[Geom::Y] - span.line_height.ascent; + bottom_right[Geom::X] = midpoint[Geom::X] + cluster_half_width; + bottom_right[Geom::Y] = midpoint[Geom::Y] + span.line_height.descent; + Geom::Point normal = tangent.cw(); + top_left += span.baseline_shift * normal; + bottom_right += span.baseline_shift * normal; + if (rotation) + *rotation = atan2(tangent[1], tangent[0]); + } + g_free(midpoint_otp); + } else { + if (it._char_index == _characters.size()) { + top_left[Geom::X] = bottom_right[Geom::X] = _chunks.back().left_x + _spans.back().x_end; + char_index--; + } else { + double span_x = _spans[_characters[it._char_index].in_span].x_start + _characters[it._char_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[it._char_index].x; + if (it._char_index + 1 == _characters.size() || _characters[it._char_index + 1].in_span != _characters[it._char_index].in_span) + bottom_right[Geom::X] = _spans[_characters[it._char_index].in_span].x_end + _characters[it._char_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[it._char_index + 1].x; + } + + double baseline_y = _characters[char_index].line(this).baseline_y + _characters[char_index].span(this).baseline_shift; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = _spans[_characters[char_index].in_span].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = baseline_y - _spans[_characters[char_index].in_span].line_height.ascent; + bottom_right[Geom::Y] = baseline_y + _spans[_characters[char_index].in_span].line_height.descent; + } + + if (rotation) { + if (it._glyph_index == -1) + *rotation = 0.0; + else if (it._glyph_index == (int)_glyphs.size()) + *rotation = _glyphs.back().rotation; + else + *rotation = _glyphs[it._glyph_index].rotation; + } + } + + return Geom::Rect(top_left, bottom_right); +} + +std::vector Layout::createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const +{ + std::vector quads; + unsigned char_index; + unsigned end_char_index; + + if (it_start._char_index < it_end._char_index) { + char_index = it_start._char_index; + end_char_index = it_end._char_index; + } else { + char_index = it_end._char_index; + end_char_index = it_start._char_index; + } + for ( ; char_index < end_char_index ; ) { + if (_characters[char_index].in_glyph == -1) { + char_index++; + continue; + } + double char_rotation = _glyphs[_characters[char_index].in_glyph].rotation; + unsigned span_index = _characters[char_index].in_span; + + Geom::Point top_left, bottom_right; + if (_path_fitted || char_rotation != 0.0) { + Geom::Rect box = characterBoundingBox(iterator(this, char_index), &char_rotation); + top_left = box.min(); + bottom_right = box.max(); + char_index++; + } else { // for straight text we can be faster by combining all the character boxes in a span into one box + double span_x = _spans[span_index].x_start + _spans[span_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[char_index].x; + while (char_index < end_char_index && _characters[char_index].in_span == span_index) + char_index++; + if (char_index == _characters.size() || _characters[char_index].in_span != span_index) + bottom_right[Geom::X] = _spans[span_index].x_end + _spans[span_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[char_index].x; + + double baseline_y = _spans[span_index].line(this).baseline_y + _spans[span_index].baseline_shift; + double vertical_scale = _glyphs.back().vertical_scale; + double offset_y = _spans[span_index].y_offset; + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = vertical_scale * _spans[span_index].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = offset_y + baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = offset_y + baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = offset_y + baseline_y - vertical_scale * _spans[span_index].line_height.ascent; + bottom_right[Geom::Y] = offset_y + baseline_y + vertical_scale * _spans[span_index].line_height.descent; + } + } + + Geom::Rect char_box(top_left, bottom_right); + if (char_box.dimensions()[Geom::X] == 0.0 || char_box.dimensions()[Geom::Y] == 0.0) + continue; + Geom::Point center_of_rotation((top_left[Geom::X] + bottom_right[Geom::X]) * 0.5, + top_left[Geom::Y] + _spans[span_index].line_height.ascent); + Geom::Affine total_transform = Geom::Translate(-center_of_rotation) * Geom::Rotate(char_rotation) * Geom::Translate(center_of_rotation) * transform; + for(int i = 0; i < 4; i ++) + quads.push_back(char_box.corner(i) * total_transform); + } + return quads; +} + +void Layout::queryCursorShape(iterator const &it, Geom::Point &position, double &height, double &rotation) const +{ + if (_characters.empty()) { + position = _empty_cursor_shape.position; + height = _empty_cursor_shape.height; + rotation = _empty_cursor_shape.rotation; + } else { + // we want to cursor to be positioned where the left edge of a character that is about to be typed will be. + // this means x & rotation are the current values but y & height belong to the previous character. + // this isn't quite right because dx attributes will be moved along, but it's good enough + Span const *span; + if (_path_fitted) { + // text on a path + double x; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + x = span->x_end + _chunks.back().left_x - _chunks[0].left_x; + } else { + span = &_spans[_characters[it._char_index].in_span]; + x = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x - _chunks[0].left_x; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) + x -= span->line_height.descent; + if (it._char_index != 0) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + double path_length = const_cast(_path_fitted)->Length(); + double x_on_path = x; + if (x_on_path < 0.0) x_on_path = 0.0; + + int unused = 0; + // as far as I know these functions are const, they're just not marked as such + Path::cut_position *path_parameter_list = const_cast(_path_fitted)->CurvilignToPosition(1, &x_on_path, unused); + Path::cut_position path_parameter; + if (path_parameter_list != nullptr && path_parameter_list[0].piece >= 0) + path_parameter = path_parameter_list[0]; + else { + path_parameter.piece = _path_fitted->descr_cmd.size() - 1; + path_parameter.t = 0.9999; // 1.0 will get the wrong tangent + } + g_free(path_parameter_list); + + Geom::Point point; + Geom::Point tangent; + const_cast(_path_fitted)->PointAndTangentAt(path_parameter.piece, path_parameter.t, point, tangent); + if (x < 0.0) + point += x * tangent; + if (x > path_length ) + point += (x - path_length) * tangent; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + position[Geom::X] = point[Geom::Y] - tangent[Geom::X] * span->baseline_shift; + position[Geom::Y] = point[Geom::X] + tangent[Geom::Y] * span->baseline_shift; + } else { + rotation = atan2(tangent); + position[Geom::X] = point[Geom::X] - tangent[Geom::Y] * span->baseline_shift; + position[Geom::Y] = point[Geom::Y] + tangent[Geom::X] * span->baseline_shift; + } + + } else { + // text is not on a path + + bool last_char_is_newline = false; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_end; + rotation = _glyphs.empty() ? 0.0 : _glyphs.back().rotation; + + // Check if last character is new line. + if (_characters.back().the_char == '\n') { + last_char_is_newline = true; + position[Geom::X] = chunkAnchorPoint(it)[Geom::X]; + } + } else { + span = &_spans[_characters[it._char_index].in_span]; + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x; + if (it._glyph_index == -1) { + rotation = 0.0; + } else if(it._glyph_index == 0) { + rotation = _glyphs.empty() ? 0.0 : _glyphs[0].rotation; + } else{ + rotation = _glyphs[it._glyph_index - 1].rotation; + } + // the first char in a line wants to have the y of the new line, so in that case we don't switch to the previous span + if (it._char_index != 0 && _characters[it._char_index - 1].chunk(this).in_line == _chunks[span->in_chunk].in_line) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + position[Geom::Y] = span->line(this).baseline_y + span->baseline_shift + span->y_offset; + + if (last_char_is_newline) { + // Move cursor to empty new line. + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + // Vertical text + position[Geom::Y] -= vertical_scale * span->line_height.emSize(); + } else { + position[Geom::Y] += vertical_scale * span->line_height.emSize(); + } + } + } + + // up to now *position is the baseline point, not the final point which will be the bottom of the descent + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + // Vertical text + height = vertical_scale * span->line_height.emSize(); + rotation += M_PI / 2; + std::swap(position[Geom::X], position[Geom::Y]); + position[Geom::X] -= vertical_scale * sin(rotation) * height * 0.5; + position[Geom::Y] += vertical_scale * cos(rotation) * height * 0.5; + } else { + // Horizontal text + double caret_slope_run = 0.0, caret_slope_rise = 1.0; + if (span->font) + const_cast(span->font)->FontSlope(caret_slope_run, caret_slope_rise); + double caret_slope = atan2(caret_slope_run, caret_slope_rise); + height = vertical_scale * (span->line_height.emSize()) / cos(caret_slope); + rotation += caret_slope; + position[Geom::X] -= sin(rotation) * vertical_scale * span->line_height.descent; + position[Geom::Y] += cos(rotation) * vertical_scale * span->line_height.descent; + } + } +} + +bool Layout::isHidden(iterator const &it) const +{ + return _characters[it._char_index].line(this).hidden; +} + + +void Layout::getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator) const +{ + if (it._char_index >= _characters.size()) { + *source = nullptr; + return; + } + InputStreamItem *stream_item = _input_stream[_spans[_characters[it._char_index].in_span].in_input_stream_item]; + *source = stream_item->source; + if (text_iterator && stream_item->Type() == TEXT_SOURCE) { + InputStreamTextSource *text_source = dynamic_cast(stream_item); + + // In order to return a non-const iterator in text_iterator, do the const_cast here. + // Note that, although ugly, it is safe because we do not write to *iterator anywhere. + Glib::ustring::iterator text_iter = const_cast(text_source->text)->begin(); + + unsigned char_index = it._char_index; + unsigned original_input_source_index = _spans[_characters[char_index].in_span].in_input_stream_item; + // confusing algorithm because the iterator goes forwards while the index goes backwards. + // It's just that it's faster doing it that way + while (char_index && _spans[_characters[char_index - 1].in_span].in_input_stream_item == original_input_source_index) { + ++text_iter; + char_index--; + } + + if (text_iterator) { + *text_iterator = text_iter; + } + } +} + +void Layout::simulateLayoutUsingKerning(iterator const &from, iterator const &to, OptionalTextTagAttrs *result) const +{ + SVGLength zero_length; + zero_length = 0.0; + + result->x.clear(); + result->y.clear(); + result->dx.clear(); + result->dy.clear(); + result->rotate.clear(); + if (to._char_index <= from._char_index) + return; + result->dx.reserve(to._char_index - from._char_index); + result->dy.reserve(to._char_index - from._char_index); + result->rotate.reserve(to._char_index - from._char_index); + for (unsigned char_index = from._char_index ; char_index < to._char_index ; char_index++) { + if (!_characters[char_index].char_attributes.is_char_break) + continue; + if (char_index == 0) + continue; + if (_characters[char_index].chunk(this).in_line != _characters[char_index - 1].chunk(this).in_line) + continue; + + unsigned prev_cluster_char_index; + for (prev_cluster_char_index = char_index - 1 ; + prev_cluster_char_index != 0 && !_characters[prev_cluster_char_index].char_attributes.is_cursor_position ; + prev_cluster_char_index--){}; + if (_characters[char_index].span(this).in_chunk == _characters[char_index - 1].span(this).in_chunk) { + // dx is zero for the first char in a chunk + // this algorithm works by comparing the summed widths of the glyphs with the observed + // difference in x coordinates of characters, and subtracting the two to produce the x kerning. + double glyphs_width = 0.0; + if (_characters[prev_cluster_char_index].in_glyph != -1) + for (int glyph_index = _characters[prev_cluster_char_index].in_glyph ; glyph_index < _characters[char_index].in_glyph ; glyph_index++) + glyphs_width += _glyphs[glyph_index].advance; + if (_characters[char_index].span(this).direction == RIGHT_TO_LEFT) + glyphs_width = -glyphs_width; + + double dx = (_characters[char_index].x + _characters[char_index].span(this).x_start + - _characters[prev_cluster_char_index].x - _characters[prev_cluster_char_index].span(this).x_start) + - glyphs_width; + + + InputStreamItem *input_item = _input_stream[_characters[char_index].span(this).in_input_stream_item]; + if (input_item->Type() == TEXT_SOURCE) { + SPStyle const *style = static_cast(input_item)->style; + if (_characters[char_index].char_attributes.is_white) + dx -= style->word_spacing.computed * getTextLengthMultiplierDue(); + if (_characters[char_index].char_attributes.is_cursor_position) + dx -= style->letter_spacing.computed * getTextLengthMultiplierDue(); + dx -= getTextLengthIncrementDue(); + } + + if (fabs(dx) > 0.0001) { + result->dx.resize(char_index - from._char_index + 1, zero_length); + result->dx.back() = dx; + } + } + double dy = _characters[char_index].span(this).baseline_shift - _characters[prev_cluster_char_index].span(this).baseline_shift; + if (fabs(dy) > 0.0001) { + result->dy.resize(char_index - from._char_index + 1, zero_length); + result->dy.back() = dy; + } + if (_characters[char_index].in_glyph != -1 && _glyphs[_characters[char_index].in_glyph].rotation != 0.0) { + result->rotate.resize(char_index - from._char_index + 1, zero_length); + result->rotate.back() = _glyphs[_characters[char_index].in_glyph].rotation; + } + } +} + +#define PREV_START_OF_ITEM(this_func) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + _char_index--; \ + return this_func(); \ + } +// end of macro + +#define THIS_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + unsigned original_item; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _char_index--; \ + original_item = item_getter; \ + } else { \ + original_item = item_getter; \ + _char_index--; \ + } \ + while (item_getter == original_item) { \ + if (_char_index == 0) { \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } \ + _char_index--; \ + } \ + _char_index++; \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define NEXT_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == _parent_layout->_characters.size()) return false; \ + unsigned original_item = item_getter; \ + for( ; ; ) { \ + _char_index++; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + if (item_getter != original_item) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::prevStartOfSpan() + PREV_START_OF_ITEM(thisStartOfSpan); + +bool Layout::iterator::thisStartOfSpan() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + +bool Layout::iterator::nextStartOfSpan() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + + +bool Layout::iterator::prevStartOfChunk() + PREV_START_OF_ITEM(thisStartOfChunk); + +bool Layout::iterator::thisStartOfChunk() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + +bool Layout::iterator::nextStartOfChunk() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + + +bool Layout::iterator::prevStartOfLine() + PREV_START_OF_ITEM(thisStartOfLine); + +bool Layout::iterator::thisStartOfLine() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + +bool Layout::iterator::nextStartOfLine() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + + +bool Layout::iterator::prevStartOfShape() + PREV_START_OF_ITEM(thisStartOfShape); + +bool Layout::iterator::thisStartOfShape() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + +bool Layout::iterator::nextStartOfShape() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + + +bool Layout::iterator::prevStartOfParagraph() + PREV_START_OF_ITEM(thisStartOfParagraph); + +bool Layout::iterator::thisStartOfParagraph() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + +bool Layout::iterator::nextStartOfParagraph() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + + +bool Layout::iterator::prevStartOfSource() + PREV_START_OF_ITEM(thisStartOfSource); + +bool Layout::iterator::thisStartOfSource() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + +bool Layout::iterator::nextStartOfSource() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + + +bool Layout::iterator::thisEndOfLine() +{ + if (_char_index == _parent_layout->_characters.size()) return false; + if (nextStartOfLine()) + { + if (_char_index && _parent_layout->_characters[_char_index - 1].char_attributes.is_white) + return prevCursorPosition(); + return true; + } + if (_char_index && _parent_layout->_characters[_char_index - 1].chunk(_parent_layout).in_line != _parent_layout->_lines.size() - 1) + return prevCursorPosition(); // for when the last paragraph is empty + return false; +} + +void Layout::iterator::beginCursorUpDown() +{ + if (_char_index == _parent_layout->_characters.size()) + _x_coordinate = _parent_layout->_chunks.back().left_x + _parent_layout->_spans.back().x_end; + else + _x_coordinate = _parent_layout->_characters[_char_index].x + _parent_layout->_characters[_char_index].span(_parent_layout).x_start + _parent_layout->_characters[_char_index].chunk(_parent_layout).left_x; + _cursor_moving_vertically = true; +} + +bool Layout::iterator::nextLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + if (_char_index == _parent_layout->_characters.size()) + return false; + unsigned line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index == _parent_layout->_lines.size() - 1) + return false; // nowhere to go + else + n = MIN (n, static_cast(_parent_layout->_lines.size() - 1 - line_index)); + if (_parent_layout->_lines[line_index + n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index + n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index + n, _x_coordinate)._char_index; + if (_char_index == _parent_layout->_characters.size()) + _glyph_index = _parent_layout->_glyphs.size(); + else + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +bool Layout::iterator::prevLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + int line_index; + if (_char_index == _parent_layout->_characters.size()) + line_index = _parent_layout->_lines.size() - 1; + else + line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index <= 0) + return false; // nowhere to go + else + n = MIN (n, static_cast(line_index)); + if (_parent_layout->_lines[line_index - n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index - n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index - n, _x_coordinate)._char_index; + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +#define NEXT_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index + 1 >= _parent_layout->_characters.size()) { \ + _char_index = _parent_layout->_characters.size(); \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + _char_index++; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define PREV_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index == 0) { \ + _glyph_index = 0; \ + return false; \ + } \ + _char_index--; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::nextCursorPosition() + NEXT_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::prevCursorPosition() + PREV_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::nextStartOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::prevStartOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::nextEndOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::prevEndOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::nextStartOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::prevStartOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::nextEndOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::prevEndOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::_cursorLeftOrRightLocalX(Direction direction) +{ + // the only reason this function is so complicated is to enable visual cursor + // movement moving in to or out of counterdirectional runs + if (_parent_layout->_characters.empty()) return false; + unsigned old_span_index; + Direction old_span_direction; + if (_char_index == _parent_layout->_characters.size()) + old_span_index = _parent_layout->_spans.size() - 1; + else + old_span_index = _parent_layout->_characters[_char_index].in_span; + old_span_direction = _parent_layout->_spans[old_span_index].direction; + Direction para_direction = _parent_layout->_spans[old_span_index].paragraph(_parent_layout).base_direction; + + int scan_direction; + unsigned old_char_index = _char_index; + if (old_span_direction != para_direction + && ((_char_index == 0 && direction == para_direction) + || (_char_index == _parent_layout->_characters.size() && direction != para_direction))) { + // the end of the text is actually in the middle because of reordering. Do cleverness + scan_direction = direction == para_direction ? +1 : -1; + } else { + if (direction == old_span_direction) { + if (!nextCursorPosition()) return false; + } else { + if (!prevCursorPosition()) return false; + } + + unsigned new_span_index = _parent_layout->_characters[_char_index].in_span; + if (new_span_index == old_span_index) return true; + if (old_span_direction != _parent_layout->_spans[new_span_index].direction) { + // we must jump to the other end of a counterdirectional run + scan_direction = direction == para_direction ? +1 : -1; + } else if (_parent_layout->_spans[old_span_index].in_chunk != _parent_layout->_spans[new_span_index].in_chunk) { + // we might have to do a weird jump when we would have crossed a chunk/line break + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) + return true; + if (old_span_direction == para_direction) + return true; + scan_direction = direction == para_direction ? +1 : -1; + } else + return true; // same direction, same chunk: no cleverness required + } + + unsigned new_span_index = old_span_index; + for ( ; ; ) { + if (scan_direction > 0) { + if (new_span_index == _parent_layout->_spans.size() - 1) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index++; + } else { + if (new_span_index == 0) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index--; + } + if (_parent_layout->_spans[new_span_index].direction == para_direction) { + if (para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + if (_parent_layout->_spans[new_span_index].in_chunk != _parent_layout->_spans[old_span_index].in_chunk) { + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph == _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph + && para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + } + + // found the correct span, now find the correct character + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) { + if (new_span_index > old_span_index) + _char_index = _parent_layout->_spanToCharacter(new_span_index); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else { + if (_parent_layout->_spans[new_span_index].direction != direction) { + if (new_span_index >= _parent_layout->_spans.size() - 1) + _char_index = _parent_layout->_characters.size(); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else + _char_index = _parent_layout->_spanToCharacter(new_span_index); + } + if (_char_index == _parent_layout->_characters.size()) { + _glyph_index = _parent_layout->_glyphs.size(); + return false; + } + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return _char_index != 0; +} + +bool Layout::iterator::_cursorLeftOrRightLocalXByWord(Direction direction) +{ + bool r; + while ((r = _cursorLeftOrRightLocalX(direction)) + && !_parent_layout->_characters[_char_index].char_attributes.is_word_start){}; + return r; +} + +bool Layout::iterator::cursorUp(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return nextLineCursor(n); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDown(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return prevLineCursor(n); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeft() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return nextLineCursor(); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRight() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return prevLineCursor(); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorUpWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDownWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeftWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRightWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +}//namespace Text +}//namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Output.cpp b/src/libnrtype/Layout-TNG-Output.cpp new file mode 100644 index 0000000..0daf453 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Output.cpp @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine output functions + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "Layout-TNG.h" +#include "display/drawing-text.h" +#include "style.h" +#include "print.h" +#include "extension/print.h" +#include "livarot/Path.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include "extension/internal/cairo-render-context.h" +#include "display/curve.h" +#include <2geom/pathvector.h> +#include <3rdparty/libuemf/symbol_convert.h> + + +using Inkscape::Extension::Internal::CairoRenderContext; +using Inkscape::Extension::Internal::CairoGlyphInfo; + +namespace Inkscape { +namespace Text { + +/* + dx array (character widths) and + ky (vertical kerning for entire span) + rtl (+1 for LTR, -1 RTL) + + are smuggled through to the EMF (ignored by others) as: + textN w1 w2 w3 ...wNy1 y2 y3 .. yN + The ndx, widths, y kern, and rtl are all 7 characters wide. ndx and rtl are ints, the widths and ky are + formatted as ' 6f'. +*/ +char *smuggle_adxkyrtl_in(const char *string, int ndx, float *adx, float ky, float rtl){ + int slen = strlen(string); + /* holds: string + fake terminator (one \0) + Number of widths (ndx) + series of widths (ndx entries) + fake terminator (one \0) + y kern value (one float) + rtl value (one float) + real terminator (two \0) + */ + int newsize=slen + 1 + 7 + 7*ndx + 1 + 7 + 7 + 2; + newsize = 8*((7 + newsize)/8); // suppress valgrind messages if it is a multiple of 8 bytes??? + char *smuggle=(char *)malloc(newsize); + strcpy(smuggle,string); // text to pass, includes the first fake terminator + char *cptr = smuggle + slen + 1; // immediately after the first fake terminator + sprintf(cptr,"%07d",ndx); // number of widths to pass + cptr+=7; // advance over ndx + for(int i=0; iUnref(); + _spans.clear(); + _characters.clear(); + _glyphs.clear(); + _path_fitted = nullptr; +} + +void Layout::FontMetrics::set(font_instance *font) +{ + if( font != nullptr ) { + ascent = font->GetTypoAscent(); + descent = font->GetTypoDescent(); + xheight = font->GetXHeight(); + ascent_max = font->GetMaxAscent(); + descent_max = font->GetMaxDescent(); + } +} + +void Layout::FontMetrics::max(FontMetrics const &other) +{ + if (other.ascent > ascent ) ascent = other.ascent; + if (other.descent > descent ) descent = other.descent; + if( other.xheight > xheight ) xheight = other.xheight; + if( other.ascent_max > ascent_max ) ascent_max = other.ascent_max; + if( other.descent_max > descent_max ) descent_max = other.descent_max; +} + +void Layout::FontMetrics::computeEffective( const double &line_height_multiplier ) { + double half_leading = 0.5 * (line_height_multiplier - 1.0) * emSize(); + ascent += half_leading; + descent += half_leading; +} + +void Layout::_getGlyphTransformMatrix(int glyph_index, Geom::Affine *matrix) const +{ + Span const &span = _glyphs[glyph_index].span(this); + double rotation = _glyphs[glyph_index].rotation; + if ( (span.block_progression == LEFT_TO_RIGHT || span.block_progression == RIGHT_TO_LEFT) && + _glyphs[glyph_index].orientation == ORIENTATION_SIDEWAYS ) { + // Vertical sideways text + rotation += M_PI/2.0; + } + double sin_rotation = sin(rotation); + double cos_rotation = cos(rotation); + (*matrix)[0] = span.font_size * cos_rotation; + (*matrix)[1] = span.font_size * sin_rotation; + (*matrix)[2] = span.font_size * sin_rotation; + (*matrix)[3] = -span.font_size * cos_rotation * (_glyphs[glyph_index].vertical_scale); // unscale vertically so the specified text height is preserved if lengthAdjust=spacingAndGlyphs + if (span.block_progression == LEFT_TO_RIGHT || span.block_progression == RIGHT_TO_LEFT) { + // Vertical text + // This effectively swaps x for y which changes handedness of coordinate system. This is a bit strange + // and not what one would expect but the compute code already reverses y so OK. + (*matrix)[4] = _lines[_chunks[span.in_chunk].in_line].baseline_y + _glyphs[glyph_index].y; + (*matrix)[5] = _chunks[span.in_chunk].left_x + _glyphs[glyph_index].x; + } else { + // Horizontal text + (*matrix)[4] = _chunks[span.in_chunk].left_x + _glyphs[glyph_index].x; + (*matrix)[5] = _lines[_chunks[span.in_chunk].in_line].baseline_y + _glyphs[glyph_index].y; + } +} + +void Layout::show(DrawingGroup *in_arena, Geom::OptRect const &paintbox) const +{ + int glyph_index = 0; + double phase0 = 0.0; + for (unsigned span_index = 0 ; span_index < _spans.size() ; span_index++) { + if (_input_stream[_spans[span_index].in_input_stream_item]->Type() != TEXT_SOURCE) continue; + + if (_spans[span_index].line(this).hidden) continue; // Line corresponds to text overflow. Don't show! + + InputStreamTextSource const *text_source = static_cast(_input_stream[_spans[span_index].in_input_stream_item]); + + text_source->style->text_decoration_data.tspan_width = _spans[span_index].width(); + text_source->style->text_decoration_data.ascender = _spans[span_index].line_height.getTypoAscent(); + text_source->style->text_decoration_data.descender = _spans[span_index].line_height.getTypoDescent(); + + if(!span_index || + (_chunks[_spans[span_index].in_chunk].in_line != _chunks[_spans[span_index-1].in_chunk].in_line)){ + text_source->style->text_decoration_data.tspan_line_start = true; + } + else { + text_source->style->text_decoration_data.tspan_line_start = false; + } + if((span_index == _spans.size() -1) || + (_chunks[_spans[span_index].in_chunk].in_line != _chunks[_spans[span_index+1].in_chunk].in_line)){ + text_source->style->text_decoration_data.tspan_line_end = true; + } + else { + text_source->style->text_decoration_data.tspan_line_end = false; + } + if(_spans[span_index].font){ + double underline_thickness, underline_position, line_through_thickness,line_through_position; + _spans[span_index].font->FontDecoration(underline_position, underline_thickness, line_through_position, line_through_thickness); + text_source->style->text_decoration_data.underline_thickness = underline_thickness; + text_source->style->text_decoration_data.underline_position = underline_position; + text_source->style->text_decoration_data.line_through_thickness = line_through_thickness; + text_source->style->text_decoration_data.line_through_position = line_through_position; + } + else { // can this case ever occur? + text_source->style->text_decoration_data.underline_thickness = + text_source->style->text_decoration_data.underline_position = + text_source->style->text_decoration_data.line_through_thickness = + text_source->style->text_decoration_data.line_through_position = 0.0; + } + + DrawingText *nr_text = new DrawingText(in_arena->drawing()); + + bool first_line_glyph = true; + while (glyph_index < (int)_glyphs.size() && _characters[_glyphs[glyph_index].in_character].in_span == span_index) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph != -1) { + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + if(first_line_glyph && text_source->style->text_decoration_data.tspan_line_start){ + first_line_glyph = false; + phase0 = glyph_matrix.translation()[Geom::X]; + } + // Save the starting coordinates for the line - these are needed for figuring out + // dot/dash/wave phase. + // Use maximum ascent and descent to ensure glyphs that extend outside the embox + // are fully drawn. + (void) nr_text->addComponent(_spans[span_index].font, _glyphs[glyph_index].glyph, glyph_matrix, + _glyphs[glyph_index].advance, + _spans[span_index].line_height.getMaxAscent(), + _spans[span_index].line_height.getMaxDescent(), + glyph_matrix.translation()[Geom::X] - phase0 + ); + } + glyph_index++; + } + nr_text->setStyle(text_source->style); + nr_text->setItemBounds(paintbox); + // Text spans must be painted in the right order (see inkscape/685) + in_arena->appendChild(nr_text); + // Set item bounds without filter enlargement + in_arena->setItemBounds(paintbox); + } +} + +Geom::OptRect Layout::bounds(Geom::Affine const &transform, int start, int length) const +{ + Geom::OptRect bbox; + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_glyphs[glyph_index].hidden) continue; // To do: This and the next line should represent the same thing, use on or the other. + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1) continue; + if (start != -1 && (int) _glyphs[glyph_index].in_character < start) continue; + if (length != -1) { + if (start == -1) + start = 0; + if ((int) _glyphs[glyph_index].in_character > start + length) continue; + } + // this could be faster + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + Geom::Affine total_transform = glyph_matrix; + total_transform *= transform; + if(_glyphs[glyph_index].span(this).font) { + Geom::OptRect glyph_rect = _glyphs[glyph_index].span(this).font->BBox(_glyphs[glyph_index].glyph); + if (glyph_rect) { + bbox.unionWith(*glyph_rect * total_transform); + } + } + } + return bbox; +} + +/* This version is much simpler than the old one +*/ +void Layout::print(SPPrintContext *ctx, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox, + Geom::Affine const &ctm) const +{ +bool text_to_path = ctx->module->textToPath(); +#define MAX_DX 2048 +float hold_dx[MAX_DX]; // For smuggling dx values (character widths) into print functions, unlikely any simple text output will be longer than this. + +Geom::Affine glyph_matrix; + + if (_input_stream.empty()) return; + if (!_glyphs.size()) return; // yes, this can happen. + if (text_to_path || _path_fitted) { + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1)continue; //invisible glyphs + Span const &span = _spans[_characters[_glyphs[glyph_index].in_character].in_span]; + Geom::PathVector const * pv = span.font->PathVector(_glyphs[glyph_index].glyph); + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + if (pv) { + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + Geom::PathVector temp_pv = (*pv) * glyph_matrix; + if (!text_source->style->fill.isNone()) + ctx->fill(temp_pv, ctm, text_source->style, pbox, dbox, bbox); + if (!text_source->style->stroke.isNone()) + ctx->stroke(temp_pv, ctm, text_source->style, pbox, dbox, bbox); + } + } + } + else { + /* index by characters, referencing glyphs and spans only as needed */ + double char_x; + int doUTN = CanUTN(); // Unicode to Nonunicode translation enabled if true + Direction block_progression = _blockProgression(); + int oldtarget = 0; + int ndx = 0; + double rtl = 1.0; // 1 L->R, -1 R->L, constant across a span. 1.0 for t->b b->t??? + + for (unsigned char_index = 0 ; char_index < _characters.size() ; ) { + Glib::ustring text_string; // accumulate text for record in this + Geom::Point g_pos(0,0); // all strings are output at (0,0) because we do the translation using the matrix + int glyph_index = _characters[char_index].in_glyph; + if(glyph_index == -1){ // if the character maps to an invisible glyph we cannot know its geometry, so skip it and move on + char_index++; + continue; + } + float ky = _glyphs[glyph_index].y; // For smuggling y kern value for span // same value for all positions in a span + unsigned span_index = _characters[char_index].in_span; + Span const &span = _spans[span_index]; + char_x = 0.0; + Glib::ustring::const_iterator text_iter = span.input_stream_first_character; + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + glyph_matrix = Geom::Scale(1.0, -1.0) * (Geom::Affine)Geom::Rotate(_glyphs[glyph_index].rotation); + if (block_progression == LEFT_TO_RIGHT || block_progression == RIGHT_TO_LEFT) { + glyph_matrix[4] = span.line(this).baseline_y + span.baseline_shift; + // since we're outputting character codes, not glyphs, we want the character x + glyph_matrix[5] = span.chunk(this).left_x + span.x_start + _characters[_glyphs[glyph_index].in_character].x; + } else { + glyph_matrix[4] = span.chunk(this).left_x + span.x_start + _characters[_glyphs[glyph_index].in_character].x; + glyph_matrix[5] = span.line(this).baseline_y + span.baseline_shift; + } + switch(span.direction){ + case Layout::TOP_TO_BOTTOM: + case Layout::BOTTOM_TO_TOP: + case Layout::LEFT_TO_RIGHT: rtl = 1.0; break; + case Layout::RIGHT_TO_LEFT: rtl = -1.0; break; + } + if(doUTN){ + oldtarget=SingleUnicodeToNon(*text_iter); // this should only ever be with a 1:1 glyph:character situation + } + + // accumulate a record to write + + unsigned lc_index = char_index; + unsigned hold_iisi = _spans[span_index].in_input_stream_item; + int newtarget = 0; + while(true){ + glyph_index = _characters[lc_index].in_glyph; + if(glyph_index == -1){ // end of a line within a paragraph, for instance + lc_index++; + break; + } + + // always append if here + text_string += *text_iter; + + // figure out char widths, used by EMF, not currently used elsewhere + double cwidth; + if(lc_index == _glyphs[glyph_index].in_character){ // Glyph width is used only for the first character, these may be 0 + cwidth = rtl * _glyphs[glyph_index].advance; // advance might be zero + } + else { + cwidth = 0; + } + char_x += cwidth; +/* +std:: cout << "DEBUG Layout::print in while " +<< " char_index " << char_index +<< " lc_index " << lc_index +<< " character " << std::hex << (int) *text_iter << std::dec +<< " glyph_index " << glyph_index +<< " glyph_xy " << _glyphs[glyph_index].x << " , " << _glyphs[glyph_index].y +<< " span_index " << span_index +<< " hold_iisi " << hold_iisi +<< std::endl; //DEBUG +*/ + if(ndx < MAX_DX){ + hold_dx[ndx++] = fabs(cwidth); + } + else { // silently truncate any text line silly enough to be longer than MAX_DX + lc_index = _characters.size(); + break; + } + + + // conditions that prevent this character from joining the record + lc_index++; + if(lc_index >= _characters.size()) break; // nothing more to process, so it must be the end of the record + ++text_iter; + if(doUTN)newtarget=SingleUnicodeToNon(*text_iter); // this should only ever be with a 1:1 glyph:character situation + if(newtarget != oldtarget)break; // change in unicode to nonunicode translation status + // MUST exit on any major span change, but not on some little events, like a font substitution event irrelevant for the file save + unsigned next_span_index = _characters[lc_index].in_span; + if(span_index != next_span_index){ + /* on major changes break out of loop. + 1st case usually indicates an entire input line has been processed (out of several in a paragraph) + 2nd case usually indicates that a format change within a line (font/size/color/etc) is present. + */ +/* +std:: cout << "DEBUG Layout::print in while --- " +<< " char_index " << char_index +<< " lc_index " << lc_index +<< " cwidth " << cwidth +<< " _char.x (next) " << (lc_index < _characters.size() ? _characters[lc_index].x : -1) +<< " char_x (end this)" << char_x +<< " diff " << fabs(char_x - _characters[lc_index].x) +<< " oldy " << ky +<< " nexty " << _glyphs[_characters[lc_index].in_glyph].y +<< std::endl; //DEBUG +*/ + if(hold_iisi != _spans[next_span_index].in_input_stream_item)break; // major change, font, size, color, etc, must exit + if(fabs(char_x - _spans[next_span_index].x_start) >= 1e-4)break; // xkerning change + if(ky != _glyphs[_characters[lc_index].in_glyph].y)break; // ykerning change + /* + None of the above? Then this is a minor "pangito", update span_index and keep going. + The font used by the display may have failed over, but print does not care and can continue to use + whatever was specified in the XML. + */ + span_index = next_span_index; + text_iter = _spans[span_index].input_stream_first_character; + } + + } + // write it + ctx->bind(glyph_matrix, 1.0); + + // the dx array is smuggled through to the EMF driver (ignored by others) as: + // textw1 w2 w3 ...wn + // where the widths are floats 7 characters wide, including the space + + char *smuggle_string=smuggle_adxkyrtl_in(text_string.c_str(),ndx, &hold_dx[0], ky, rtl); + ctx->text(smuggle_string, g_pos, text_source->style); + free(smuggle_string); + ctx->release(); + ndx = 0; + char_index = lc_index; + } + } +} + + +void Layout::showGlyphs(CairoRenderContext *ctx) const +{ + if (_input_stream.empty()) return; + + bool clip_mode = false;//(ctx->getRenderMode() == CairoRenderContext::RENDER_MODE_CLIP); + std::vector glyphtext; + + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; ) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1) { + // invisible glyphs + unsigned same_character = _glyphs[glyph_index].in_character; + while (_glyphs[glyph_index].in_character == same_character) { + glyph_index++; + if (glyph_index == _glyphs.size()) + return; + } + continue; + } + Span const &span = _spans[_characters[_glyphs[glyph_index].in_character].in_span]; + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + if (clip_mode) { + Geom::PathVector const *pathv = span.font->PathVector(_glyphs[glyph_index].glyph); + if (pathv) { + Geom::PathVector pathv_trans = (*pathv) * glyph_matrix; + SPStyle const *style = text_source->style; + ctx->renderPathVector(pathv_trans, style, Geom::OptRect()); + } + glyph_index++; + continue; + } + + Geom::Affine font_matrix = glyph_matrix; + font_matrix[4] = 0; + font_matrix[5] = 0; + + Glib::ustring::const_iterator span_iter = span.input_stream_first_character; + unsigned char_index = _glyphs[glyph_index].in_character; + unsigned original_span = _characters[char_index].in_span; + while (char_index && _characters[char_index - 1].in_span == original_span) { + char_index--; + ++span_iter; + } + + // try to output as many characters as possible in one go + Glib::ustring span_string; + unsigned this_span_index = _characters[_glyphs[glyph_index].in_character].in_span; + unsigned int first_index = glyph_index; + glyphtext.clear(); + do { + span_string += *span_iter; + ++span_iter; + + unsigned same_character = _glyphs[glyph_index].in_character; + while (glyph_index < _glyphs.size() && _glyphs[glyph_index].in_character == same_character) { + if (glyph_index != first_index) + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + + CairoGlyphInfo info; + info.index = _glyphs[glyph_index].glyph; + // this is the translation for x,y-offset + info.x = glyph_matrix[4]; + info.y = glyph_matrix[5]; + + glyphtext.push_back(info); + + glyph_index++; + } + } while (glyph_index < _glyphs.size() + && _path_fitted == nullptr + && (font_matrix * glyph_matrix.inverse()).isIdentity() + && _characters[_glyphs[glyph_index].in_character].in_span == this_span_index); + + // remove vertical flip + Geom::Affine flip_matrix; + flip_matrix.setIdentity(); + flip_matrix[3] = -1.0; + font_matrix = flip_matrix * font_matrix; + + SPStyle const *style = text_source->style; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + + if (opacity != 1.0) { + ctx->pushState(); + ctx->setStateForStyle(style); + ctx->pushLayer(); + } + if (glyph_index - first_index > 0) + ctx->renderGlyphtext(span.font->pFont, font_matrix, glyphtext, style); + if (opacity != 1.0) { + ctx->popLayer(); + ctx->popState(); + } + } +} + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT +// these functions are for dumpAsText() only. No need to translate +static char const *direction_to_text(Layout::Direction d) +{ + switch (d) { + case Layout::LEFT_TO_RIGHT: return "ltr"; + case Layout::RIGHT_TO_LEFT: return "rtl"; + case Layout::TOP_TO_BOTTOM: return "ttb"; + case Layout::BOTTOM_TO_TOP: return "btt"; + } + return "???"; +} + +static char const *style_to_text(PangoStyle s) +{ + switch (s) { + case PANGO_STYLE_NORMAL: return "upright"; + case PANGO_STYLE_ITALIC: return "italic"; + case PANGO_STYLE_OBLIQUE: return "oblique"; + } + return "???"; +} + +static std::string weight_to_text(PangoWeight w) +{ + switch (w) { + case PANGO_WEIGHT_THIN : return "thin"; + case PANGO_WEIGHT_ULTRALIGHT: return "ultralight"; + case PANGO_WEIGHT_LIGHT : return "light"; +#if PANGO_VERSION_CHECK(1,36,6) + case PANGO_WEIGHT_SEMILIGHT : return "semilight"; +#endif + case PANGO_WEIGHT_BOOK : return "book"; + case PANGO_WEIGHT_NORMAL : return "normalweight"; + case PANGO_WEIGHT_MEDIUM : return "medium"; + case PANGO_WEIGHT_SEMIBOLD : return "semibold"; + case PANGO_WEIGHT_BOLD : return "bold"; + case PANGO_WEIGHT_ULTRABOLD : return "ultrabold"; + case PANGO_WEIGHT_HEAVY : return "heavy"; + case PANGO_WEIGHT_ULTRAHEAVY: return "ultraheavy"; + } + return std::to_string(w); +} +#endif //DEBUG_TEXTLAYOUT_DUMPASTEXT + +Glib::ustring Layout::getFontFamily(unsigned span_index) const +{ + if (span_index >= _spans.size()) + return ""; + + if (_spans[span_index].font) { + return sp_font_description_get_family(_spans[span_index].font->descr); + } + + return ""; +} + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT +Glib::ustring Layout::dumpAsText() const +{ + Glib::ustring result; + Glib::ustring::const_iterator icc; + char line[256]; + + result = Glib::ustring::compose("spans %1\nchars %2\nglyphs %3\n", _spans.size(), _characters.size(), _glyphs.size()); + if(_characters.size() > 1){ + unsigned lastspan=5000; + for(unsigned j = 0; j < _characters.size() ; j++){ + if(lastspan != _characters[j].in_span){ + lastspan = _characters[j].in_span; + icc = _spans[lastspan].input_stream_first_character; + } + snprintf(line, sizeof(line), "char %4u: '%c' 0x%4.4x x=%8.4f glyph=%3d span=%3d\n", j, *icc, *icc, _characters[j].x, _characters[j].in_glyph, _characters[j].in_span); + result += line; + ++icc; + } + } + if(_glyphs.size()){ + for(unsigned j = 0; j < _glyphs.size() ; j++){ + snprintf(line, sizeof(line), "glyph %4u: %4d (%8.4f,%8.4f) rot=%8.4f cx=%8.4f char=%4d\n", + j, _glyphs[j].glyph, _glyphs[j].x, _glyphs[j].y, _glyphs[j].rotation, _glyphs[j].width, _glyphs[j].in_character); + result += line; + } + } + + for (unsigned span_index = 0 ; span_index < _spans.size() ; span_index++) { + result += Glib::ustring::compose("==== span %1 \n", span_index) + + Glib::ustring::compose(" in para %1 (direction=%2)\n", _lines[_chunks[_spans[span_index].in_chunk].in_line].in_paragraph, + direction_to_text(_paragraphs[_lines[_chunks[_spans[span_index].in_chunk].in_line].in_paragraph].base_direction)) + + Glib::ustring::compose(" in source %1 (type=%2, cookie=%3)\n", _spans[span_index].in_input_stream_item, + _input_stream[_spans[span_index].in_input_stream_item]->Type(), + _input_stream[_spans[span_index].in_input_stream_item]->source) + + Glib::ustring::compose(" in line %1 (baseline=%2, shape=%3)\n", _chunks[_spans[span_index].in_chunk].in_line, + _lines[_chunks[_spans[span_index].in_chunk].in_line].baseline_y, + _lines[_chunks[_spans[span_index].in_chunk].in_line].in_shape) + + Glib::ustring::compose(" in chunk %1 (x=%2, baselineshift=%3)\n", _spans[span_index].in_chunk, _chunks[_spans[span_index].in_chunk].left_x, _spans[span_index].baseline_shift); + + if (_spans[span_index].font) { +#if PANGO_VERSION_CHECK(1,41,1) + const char* variations = pango_font_description_get_variations(_spans[span_index].font->descr); + result += Glib::ustring::compose( + " font '%1' %2 %3 %4 %5\n", + sp_font_description_get_family(_spans[span_index].font->descr), + _spans[span_index].font_size, + style_to_text( pango_font_description_get_style(_spans[span_index].font->descr) ), + weight_to_text( pango_font_description_get_weight(_spans[span_index].font->descr) ), + (variations?variations:"") + ); +#else + result += Glib::ustring::compose( + " font '%1' %2 %3 %4\n", + sp_font_description_get_family(_spans[span_index].font->descr), + _spans[span_index].font_size, + style_to_text( pango_font_description_get_style(_spans[span_index].font->descr) ), + weight_to_text( pango_font_description_get_weight(_spans[span_index].font->descr) ) + ); +#endif + } + result += Glib::ustring::compose(" x_start = %1, x_end = %2\n", _spans[span_index].x_start, _spans[span_index].x_end) + + Glib::ustring::compose(" line height: ascent %1, descent %2\n", _spans[span_index].line_height.ascent, _spans[span_index].line_height.descent) + + Glib::ustring::compose(" direction %1, block-progression %2\n", direction_to_text(_spans[span_index].direction), direction_to_text(_spans[span_index].block_progression)) + + " ** characters:\n"; + Glib::ustring::const_iterator iter_char = _spans[span_index].input_stream_first_character; + // very inefficient code. what the hell, it's only debug stuff. + for (unsigned char_index = 0 ; char_index < _characters.size() ; char_index++) { + union {const PangoLogAttr* pattr; const unsigned* uattr;} u; + u.pattr = &_characters[char_index].char_attributes; + if (_characters[char_index].in_span != span_index) continue; + if (_input_stream[_spans[span_index].in_input_stream_item]->Type() != TEXT_SOURCE) { + snprintf(line, sizeof(line), " %u: control x=%f flags=%03x glyph=%d\n", char_index, _characters[char_index].x, *u.uattr, _characters[char_index].in_glyph); + } else { // some text has empty tspans, iter_char cannot be dereferenced + snprintf(line, sizeof(line), " %u: '%c' 0x%4.4x x=%f flags=%03x glyph=%d\n", char_index, *iter_char, *iter_char, _characters[char_index].x, *u.uattr, _characters[char_index].in_glyph); + ++iter_char; + } + result += line; + } + result += " ** glyphs:\n"; + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_characters[_glyphs[glyph_index].in_character].in_span != span_index) continue; + snprintf(line, sizeof(line), " %u: %d (%f,%f) rot=%f cx=%f char=%d\n", glyph_index, _glyphs[glyph_index].glyph, _glyphs[glyph_index].x, _glyphs[glyph_index].y, _glyphs[glyph_index].rotation, _glyphs[glyph_index].width, _glyphs[glyph_index].in_character); + result += line; + } + result += "\n"; + } + result += "EOT\n"; + return result; +} +#endif //DEBUG_TEXTLAYOUT_DUMPASTEXT + +void Layout::fitToPathAlign(SVGLength const &startOffset, Path const &path) +{ + double offset = 0.0; + + if (startOffset._set) { + if (startOffset.unit == SVGLength::PERCENT) + offset = startOffset.computed * const_cast(path).Length(); + else + offset = startOffset.computed; + } + + Alignment alignment = _paragraphs.empty() ? LEFT : _paragraphs.front().alignment; + switch (alignment) { + case CENTER: + offset -= _getChunkWidth(0) * 0.5; + break; + case RIGHT: + offset -= _getChunkWidth(0); + break; + default: + break; + } + + if (_characters.empty()) { + int unused = 0; + Path::cut_position *point_otp = const_cast(path).CurvilignToPosition(1, &offset, unused); + if (offset >= 0.0 && point_otp != nullptr && point_otp[0].piece >= 0) { + Geom::Point point; + Geom::Point tangent; + const_cast(path).PointAndTangentAt(point_otp[0].piece, point_otp[0].t, point, tangent); + _empty_cursor_shape.position = point; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + _empty_cursor_shape.rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + } else { + _empty_cursor_shape.rotation = atan2(tangent[Geom::Y], tangent[Geom::X]); + } + } + } + + for (unsigned char_index = 0 ; char_index < _characters.size() ; ) { + Span const &span = _characters[char_index].span(this); + + size_t next_cluster_char_index = 0; // TODO refactor to not bump via for loops + for (next_cluster_char_index = char_index + 1 ; next_cluster_char_index < _characters.size() ; next_cluster_char_index++) { + if (_characters[next_cluster_char_index].in_glyph != -1 && _characters[next_cluster_char_index].char_attributes.is_cursor_position) + { + break; + } + } + + size_t next_cluster_glyph_index = 0; + if (next_cluster_char_index == _characters.size()) { + next_cluster_glyph_index = _glyphs.size(); + } else { + next_cluster_glyph_index = _characters[next_cluster_char_index].in_glyph; + } + + double start_offset = offset + span.x_start + _characters[char_index].x; + double cluster_width = 0.0; + size_t const current_cluster_glyph_index = _characters[char_index].in_glyph; + for (size_t glyph_index = current_cluster_glyph_index ; glyph_index < next_cluster_glyph_index ; glyph_index++) + { + cluster_width += _glyphs[glyph_index].advance; + } + // TODO block progression? + if (span.direction == RIGHT_TO_LEFT) + { + start_offset -= cluster_width; + } + double end_offset = start_offset + cluster_width; + + int unused = 0; + double midpoint_offset = (start_offset + end_offset) * 0.5; + // as far as I know these functions are const, they're just not marked as such + Path::cut_position *midpoint_otp = const_cast(path).CurvilignToPosition(1, &midpoint_offset, unused); + if (midpoint_offset >= 0.0 && midpoint_otp != nullptr && midpoint_otp[0].piece >= 0) { + Geom::Point midpoint; + Geom::Point tangent; + const_cast(path).PointAndTangentAt(midpoint_otp[0].piece, midpoint_otp[0].t, midpoint, tangent); + + if (start_offset >= 0.0 && end_offset >= 0.0) { + Path::cut_position *start_otp = const_cast(path).CurvilignToPosition(1, &start_offset, unused); + if (start_otp != nullptr && start_otp[0].piece >= 0) { + Path::cut_position *end_otp = const_cast(path).CurvilignToPosition(1, &end_offset, unused); + if (end_otp != nullptr && end_otp[0].piece >= 0) { + bool on_same_subpath = true; + for (const auto & pt : path.pts) { + if (pt.piece <= start_otp[0].piece) continue; + if (pt.piece >= end_otp[0].piece) break; + if (pt.isMoveTo == polyline_moveto) { + on_same_subpath = false; + break; + } + } + if (on_same_subpath) { + // both points were on the same subpath (without this test the angle is very weird) + Geom::Point startpoint, endpoint; + const_cast(path).PointAt(start_otp[0].piece, start_otp[0].t, startpoint); + const_cast(path).PointAt(end_otp[0].piece, end_otp[0].t, endpoint); + if (endpoint != startpoint) { + tangent = endpoint - startpoint; + tangent.normalize(); + } + } + g_free(end_otp); + } + g_free(start_otp); + } + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + for (size_t glyph_index = current_cluster_glyph_index; glyph_index < next_cluster_glyph_index ; glyph_index++) { + _glyphs[glyph_index].x = midpoint[Geom::Y] - tangent[Geom::X] * _glyphs[glyph_index].y - span.chunk(this).left_x; + _glyphs[glyph_index].y = midpoint[Geom::X] + tangent[Geom::Y] * _glyphs[glyph_index].y - _lines.front().baseline_y; + _glyphs[glyph_index].rotation += rotation; + } + } else { + double rotation = atan2(tangent[Geom::Y], tangent[Geom::X]); + for (size_t glyph_index = current_cluster_glyph_index; glyph_index < next_cluster_glyph_index ; glyph_index++) { + double tangent_shift = -cluster_width * 0.5 + _glyphs[glyph_index].x - (_characters[char_index].x + span.x_start); + if (span.direction == RIGHT_TO_LEFT) + { + tangent_shift += cluster_width; + } + _glyphs[glyph_index].x = midpoint[Geom::X] + tangent[Geom::X] * tangent_shift - tangent[Geom::Y] * _glyphs[glyph_index].y - span.chunk(this).left_x; + _glyphs[glyph_index].y = midpoint[Geom::Y] + tangent[Geom::Y] * tangent_shift + tangent[Geom::X] * _glyphs[glyph_index].y - _lines.front().baseline_y; + _glyphs[glyph_index].rotation += rotation; + } + } + _input_truncated = false; + } else { // outside the bounds of the path: hide the glyphs + _characters[char_index].in_glyph = -1; + _input_truncated = true; + } + g_free(midpoint_otp); + + char_index = next_cluster_char_index; + } + + for (auto & _span : _spans) { + _span.x_start += offset; + _span.x_end += offset; + } + + _path_fitted = &path; +} + +SPCurve *Layout::convertToCurves(iterator const &from_glyph, iterator const &to_glyph) const +{ + std::list cc; + + for (int glyph_index = from_glyph._glyph_index ; glyph_index < to_glyph._glyph_index ; glyph_index++) { + Geom::Affine glyph_matrix; + Span const &span = _glyphs[glyph_index].span(this); + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + + Geom::PathVector const * pathv = span.font->PathVector(_glyphs[glyph_index].glyph); + if (pathv) { + Geom::PathVector pathv_trans = (*pathv) * glyph_matrix; + SPCurve *c = new SPCurve(pathv_trans); + if (c) cc.push_back(c); + } + } + SPCurve *curve = new SPCurve(cc); + + for (auto i:cc) { + /* fixme: This is dangerous, as we are mixing art_alloc and g_new */ + i->unref(); + } + + return curve; +} + +void Layout::transform(Geom::Affine const &transform) +{ + // this is all massively oversimplified + // I can't actually think of anybody who'll want to use it at the moment, so it'll stay simple + for (auto & _glyph : _glyphs) { + Geom::Point point(_glyph.x, _glyph.y); + point *= transform; + _glyph.x = point[0]; + _glyph.y = point[1]; + } +} + +double Layout::getTextLengthIncrementDue() const +{ + if (textLength._set && textLengthIncrement != 0 && lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) { + return textLengthIncrement; + } + return 0; +} + + +double Layout::getTextLengthMultiplierDue() const +{ + if (textLength._set && textLengthMultiplier != 1 && (lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS)) { + return textLengthMultiplier; + } + return 1; +} + +double Layout::getActualLength() const +{ + double length = 0; + for (std::vector::const_iterator it_span = _spans.begin() ; it_span != _spans.end() ; it_span++) { + // take x_end of the last span of each chunk + if (it_span == _spans.end() - 1 || (it_span + 1)->in_chunk != it_span->in_chunk) + length += it_span->x_end; + } + return length; + + +} + +}//namespace Text +}//namespace Inkscape + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics &f) { + out << " emSize: " << f.emSize() + << " ascent: " << f.ascent + << " descent: " << f.descent + << " xheight: " << f.xheight; + return out; +} + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics *f) { + out << " emSize: " << f->emSize() + << " ascent: " << f->ascent + << " descent: " << f->descent + << " xheight: " << f->xheight; + return out; +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Scanline-Maker.h b/src/libnrtype/Layout-TNG-Scanline-Maker.h new file mode 100644 index 0000000..c8c8232 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Scanline-Maker.h @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::ScanlineMaker - text layout engine shape measurers + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __LAYOUT_TNG_SCANLINE_MAKER_H__ +#define __LAYOUT_TNG_SCANLINE_MAKER_H__ + +#include +#include +#include "libnrtype/Layout-TNG.h" + +class Shape; + +namespace Inkscape { +namespace Text { + +/** \brief private to Layout. Generates lists of chunks within a shape. + +This is the abstract base class for taking a given shape and scanning through +it line-by-line to get the horizontal extents of each chunk for a line of a +given height. There are two specialisations: One for real shapes and one that +turns off wrapping by simulating an infinite shape. In due course there will +be a further specialisation to optimise for the common case where the shape +is a rectangle. +*/ +class Layout::ScanlineMaker +{ +public: + virtual ~ScanlineMaker() = default; + + struct ScanRun { + double y; /// that's the top of the scan run, not the baseline + double x_start; // these are not flipped according to the text direction + double x_end; + inline double width() const {return std::abs(x_start - x_end);} + }; + + /** Returns a list of chunks on the current line which can fit text with + the given properties. It is up to the caller to discard any chunks which + are too narrow for its needs. This function may change the y coordinate + between calls if the new height too big to fit in the space remaining in + this shape. Returns an empty vector if there is no space left in the + current shape. */ + virtual std::vector makeScanline(Layout::FontMetrics const &line_height) =0; + + /** Indicates that the caller has successfully filled the current line + and hence that the next call to makeScanline() should return lines on + the next lower line. There is no error return, the next call to + makeScanline() will give an error if there is no more space. */ + virtual void completeLine() =0; + + /** Returns the y coordinate of the top of the scanline that will be + returned by the next call to makeScanline(). */ + virtual double yCoordinate() = 0; + + /** Forces an arbitrary change in the stored y coordinate of the object. + The next call to makeScanline() will return runs whose top is at + the new coordinate. */ + virtual void setNewYCoordinate(double new_y) =0; + + /** Tests whether the caller can fit a new line with the given metrics + into exactly the space returned by the previous call to makeScanline(). + This saves the caller from having to discard its wrapping solution and + starting at the beginning of the line again when a larger font is seen. + The metrics given here are considered to be the ones that are being + used now, and hence is the line advance height used by completeLine(). + */ + virtual bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) =0; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + virtual void setLineHeight(Layout::FontMetrics const &line_height) =0; +}; + +/** \brief private to Layout. Generates infinite scanlines for when you don't want wrapping + +This is a 'fake' scanline maker which will always return infinite results, +effectively turning off wrapping. It's a very simple implementation. + +It does have the curious property, however, that the input coordinates are +'real' x and y, but the outputs are rotated according to the +\a block_progression. +*/ +class Layout::InfiniteScanlineMaker : public Layout::ScanlineMaker +{ +public: + InfiniteScanlineMaker(double initial_x, double initial_y, Layout::Direction block_progression); + ~InfiniteScanlineMaker() override; + + /** Returns a single infinite run at the current location */ + std::vector makeScanline(Layout::FontMetrics const &line_height) override; + + /** Increments the current y by the current line height */ + void completeLine() override; + + double yCoordinate() override + {return _y;} + + /** Just changes y */ + void setNewYCoordinate(double new_y) override; + + /** Always true, but has to save the new height */ + bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) override; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + void setLineHeight(Layout::FontMetrics const &line_height) override; + +private: + double _x, _y; + Layout::FontMetrics _current_line_height; + bool _negative_block_progression; /// if true, indicates that completeLine() should decrement rather than increment, ie block-progression is either rl or bt +}; + +/** \brief private to Layout. Generates scanlines inside an arbitrary shape + +This is the 'perfect', and hence slowest, implementation of a +Layout::ScanlineMaker, which will return exact bounds for any given +input shape. +*/ +class Layout::ShapeScanlineMaker : public Layout::ScanlineMaker +{ +public: + ShapeScanlineMaker(Shape const *shape, Layout::Direction block_progression); + ~ShapeScanlineMaker() override; + + std::vector makeScanline(Layout::FontMetrics const &line_height) override; + + void completeLine() override; + + double yCoordinate() override; + + void setNewYCoordinate(double new_y) override; + + /** never true */ + bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) override; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + void setLineHeight(Layout::FontMetrics const &line_height) override; + +private: + /** To generate scanlines for top-to-bottom text it is easiest if we + simply rotate the given shape by a multiple of 90 degrees. This stores + that. If no rotation was needed we can simply store the pointer we were + given and set shape_needs_freeing appropriately. */ + Shape *_rotated_shape; + + /// see #rotated_shape; + bool _shape_needs_freeing; + + // Shape::BeginRaster() needs floats rather than doubles + float _bounding_box_top, _bounding_box_bottom; + float _y; + float _rasterizer_y; + int _current_rasterization_point; + float _current_line_height; + + bool _negative_block_progression; /// if true, indicates that completeLine() should decrement rather than increment, ie block-progression is either rl or bt +}; + +}//namespace Text +}//namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Scanline-Makers.cpp b/src/libnrtype/Layout-TNG-Scanline-Makers.cpp new file mode 100644 index 0000000..8398bf1 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Scanline-Makers.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::ScanlineMaker - text layout engine shape measurers + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG-Scanline-Maker.h" +#include "livarot/Shape.h" +#include "livarot/float-line.h" +#include + +namespace Inkscape { +namespace Text { + +// *********************** infinite version + +Layout::InfiniteScanlineMaker::InfiniteScanlineMaker(double initial_x, double initial_y, Layout::Direction block_progression) +{ + _current_line_height.setZero(); + switch (block_progression) { + case LEFT_TO_RIGHT: + case RIGHT_TO_LEFT: + _x = initial_y; + _y = initial_x; + break; + default: + _x = initial_x; + _y = initial_y; + break; + } + _negative_block_progression = block_progression == RIGHT_TO_LEFT || block_progression == BOTTOM_TO_TOP; + +} + +Layout::InfiniteScanlineMaker::~InfiniteScanlineMaker() += default; + +std::vector Layout::InfiniteScanlineMaker::makeScanline(Layout::FontMetrics const &line_height) +{ + std::vector runs(1); + runs[0].x_start = _x; + runs[0].x_end = std::numeric_limits::max(); // we could use DBL_MAX, but this just seems safer + runs[0].y = _y; + _current_line_height = line_height; + return runs; +} + +void Layout::InfiniteScanlineMaker::completeLine() +{ + if (_negative_block_progression) + _y -= _current_line_height.emSize(); + else + _y += _current_line_height.emSize(); + _current_line_height.setZero(); +} + +void Layout::InfiniteScanlineMaker::setNewYCoordinate(double new_y) +{ + _y = new_y; +} + +bool Layout::InfiniteScanlineMaker::canExtendCurrentScanline(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height; + return true; +} + +void Layout::InfiniteScanlineMaker::setLineHeight(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height; +} + +// *********************** real shapes version + +Layout::ShapeScanlineMaker::ShapeScanlineMaker(Shape const *shape, Layout::Direction block_progression) +{ + if (block_progression == TOP_TO_BOTTOM) { + _rotated_shape = const_cast(shape); + _shape_needs_freeing = false; + } else { + Shape *temp_rotated_shape = new Shape; + _shape_needs_freeing = true; + temp_rotated_shape->Copy(const_cast(shape)); + switch (block_progression) { + case BOTTOM_TO_TOP: temp_rotated_shape->Transform(Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)); break; // reflect about x axis + case LEFT_TO_RIGHT: temp_rotated_shape->Transform(Geom::Affine(0.0, 1.0, 1.0, 0.0, 0.0, 0.0)); break; // reflect about y=x + case RIGHT_TO_LEFT: temp_rotated_shape->Transform(Geom::Affine(0.0, -1.0, 1.0, 0.0, 0.0, 0.0)); break; // reflect about y=-x + default: break; + } + _rotated_shape = new Shape; + _rotated_shape->ConvertToShape(temp_rotated_shape); + delete temp_rotated_shape; + } + _rotated_shape->CalcBBox(true); + _bounding_box_top = _rotated_shape->topY; + _bounding_box_bottom = _rotated_shape->bottomY; + _y = _rasterizer_y = _bounding_box_top; + _current_rasterization_point = 0; + _rotated_shape->BeginRaster(_y, _current_rasterization_point); + _negative_block_progression = block_progression == RIGHT_TO_LEFT || block_progression == BOTTOM_TO_TOP; +} + + +Layout::ShapeScanlineMaker::~ShapeScanlineMaker() +{ + _rotated_shape->EndRaster(); + if (_shape_needs_freeing) + delete _rotated_shape; +} + +std::vector Layout::ShapeScanlineMaker::makeScanline(Layout::FontMetrics const &line_height) +{ + if (_y > _bounding_box_bottom) + return std::vector(); + + if (_y < _bounding_box_top) + _y = _bounding_box_top; + + FloatLigne line_rasterization; + FloatLigne line_decent_length_runs; + float line_text_height = (float)(line_height.emSize()); + if (line_text_height < 0.001) + line_text_height = 0.001; // Scan() doesn't work for zero height so this will have to do + + _current_line_height = (float)line_height.emSize(); + + // I think what's going on here is that we're moving the top of the scanline to the given position... + _rotated_shape->Scan(_rasterizer_y, _current_rasterization_point, _y, line_text_height); + // ...then actually retrieving the scanline (which alters the first two parameters) + _rotated_shape->Scan(_rasterizer_y, _current_rasterization_point, _y + line_text_height , &line_rasterization, true, line_text_height); + // sanitise the raw rasterisation, which could have weird overlaps + line_rasterization.Flatten(); + // line_rasterization.Affiche(); + // cut out runs that cover less than 90% of the line + line_decent_length_runs.Over(&line_rasterization, 0.9 * line_text_height); + + if (line_decent_length_runs.runs.empty()) + { + if (line_rasterization.runs.empty()) + return std::vector(); // stop the flow + // make up a pointless run: anything that's not an empty vector + std::vector result(1); + result[0].x_start = line_rasterization.runs[0].st; + result[0].x_end = line_rasterization.runs[0].st; + result[0].y = _negative_block_progression ? - _y : _y; + return result; + } + + // convert the FloatLigne to what we use: vector + std::vector result(line_decent_length_runs.runs.size()); + for (unsigned i = 0 ; i < result.size() ; i++) { + result[i].x_start = line_decent_length_runs.runs[i].st; + result[i].x_end = line_decent_length_runs.runs[i].en; + result[i].y = _negative_block_progression ? - _y : _y; + } + + return result; +} + +void Layout::ShapeScanlineMaker::completeLine() +{ + _y += _current_line_height; +} + +double Layout::ShapeScanlineMaker::yCoordinate() +{ + if (_negative_block_progression) return - _y; + return _y; +} + +void Layout::ShapeScanlineMaker::setNewYCoordinate(double new_y) +{ + _y = (float)new_y; + if (_negative_block_progression) _y = - _y; + // what will happen with the rasteriser if we move off the shape? + // it's not an important question because doesn't have a y attribute +} + +bool Layout::ShapeScanlineMaker::canExtendCurrentScanline(Layout::FontMetrics const &/*line_height*/) +{ + //we actually could return true if only the leading changed, but that's too much effort for something that rarely happens + return false; +} + +void Layout::ShapeScanlineMaker::setLineHeight(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height.emSize(); +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG.cpp b/src/libnrtype/Layout-TNG.cpp new file mode 100644 index 0000000..96d805d --- /dev/null +++ b/src/libnrtype/Layout-TNG.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine misc + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG.h" + +namespace Inkscape { +namespace Text { + +const gunichar Layout::UNICODE_SOFT_HYPHEN = 0x00AD; +const double Layout::LINE_HEIGHT_NORMAL = 1.25; + +Layout::Layout() = default; + +Layout::~Layout() +{ + clear(); +} + +void Layout::clear() +{ + _clearInputObjects(); + _clearOutputObjects(); + + textLength._set = false; + textLengthMultiplier = 1; + textLengthIncrement = 0; + lengthAdjust = LENGTHADJUST_SPACING; +} + +bool Layout::_directions_are_orthogonal(Direction d1, Direction d2) +{ + if (d1 == BOTTOM_TO_TOP) d1 = TOP_TO_BOTTOM; + if (d2 == BOTTOM_TO_TOP) d2 = TOP_TO_BOTTOM; + if (d1 == RIGHT_TO_LEFT) d1 = LEFT_TO_RIGHT; + if (d2 == RIGHT_TO_LEFT) d2 = LEFT_TO_RIGHT; + return d1 != d2; +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG.h b/src/libnrtype/Layout-TNG.h new file mode 100644 index 0000000..6ab2df8 --- /dev/null +++ b/src/libnrtype/Layout-TNG.h @@ -0,0 +1,1209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __LAYOUT_TNG_H__ +#define __LAYOUT_TNG_H__ + +//#define DEBUG_TEXTLAYOUT_DUMPASTEXT + +#include <2geom/d2.h> +#include <2geom/affine.h> +#include +#include +#include +#include +#include +#include +#include "style-enums.h" + +namespace Inkscape { + namespace Extension { + namespace Internal { + class CairoRenderContext; + } + } +} + +using Inkscape::Extension::Internal::CairoRenderContext; + +class SPStyle; +class SPObject; +class Shape; +struct SPPrintContext; +class Path; +class SPCurve; +class font_instance; +typedef struct _PangoFontDescription PangoFontDescription; + +namespace Inkscape { +class DrawingGroup; + +namespace Text { + +/** \brief Generates the layout for either wrapped or non-wrapped text and stores the result + +Use this class for all your text output needs. It takes text with formatting +markup as input and turns that into the glyphs and their necessary positions. +It stores the glyphs internally, but maintains enough information to both +retrieve your own rendering information if you wish and to perform visual +text editing where the output refers back to where it came from. + +Usage: +-# Construct +-# Set the text using appendText() and appendControlCode() +-# If you want text wrapping, call appendWrapShape() a few times +-# Call calculateFlow() +-# You can go several directions from here, but the most interesting + things start with creating a Layout::iterator with begin() or end(). + +Terminology, in descending order of size: +- Flow: Not often used, but when it is it means all the text +- Shape: A Shape object which is used to represent one of the regions inside + which to flow the text. Can overlap with... +- Paragraph: Err...A paragraph. Contains one or more... +- Line: An entire horizontal line with a common baseline. Contains one or + more... +- Chunk: You only get more than one of these when a shape is sufficiently + complex that the text has to flow either side of some obstruction in + the middle. A chunk is the base unit for wrapping. Contains one or more... +- Span: A convenient subset of a chunk with the same font, style, + directionality, block progression and input stream. Fill and outline + need not be constant because that's a later rendering stage. +- This is where it gets weird because a span will contain one or more + elements of both of the following, which can overlap with each other in + any way: + - Character: a single Unicode codepoint from an input stream. Many arabic + characters contain multiple glyphs + - Glyph: a rendering primitive for font engines. A ligature glyph will + represent multiple characters. + +Other terminology: +- Input stream: An object representing a single call to appendText() or + appendControlCode(). +- Control code: Metadata in the text stream to signify items that occupy + real space (unlike style changes) but don't belong in the text string. + Paragraph breaks are in this category. See Layout::TextControlCode. +- SVG1.1: The W3C Recommendation "Scalable Vector Graphics (SVG) 1.1" + http://www.w3.org/TR/SVG11/ +- 'left', 'down', etc: These terms are generally used to mean what they + mean in left-to-right, top-to-bottom text but rotated or reflected for + the current directionality. Thus, the 'width' of a ttb line is actually + its height, and the (internally stored) y coordinate of a glyph is + actually its x coordinate. Confusing to the reader but much simpler in + the code. All public methods use real x and y. + +Comments: +- There's a strong emphasis on international support in this class, but + that's primarily because once you can display all the insane things + required by various languages, simple things like styling text are + almost trivial. +- There are a few places (appendText() is one) where pointers are held to + caller-owned objects and used for quite a long time. This is messy but + is safe for our usage scenario and in many cases the cost of copying the + objects is quite high. +- "Why isn't foo here?": Ask yourself if it's possible to implement foo + externally using iterators. However this may not mean that it doesn't + belong as a member, though. +- I've used floats rather than doubles to store relative distances in some + places (internal only) where it would save significant amounts of memory. + The SVG spec allows you to do this as long as intermediate calculations + are done double. Very very long lines might not finish precisely where + you want, but that's to be expected with any typesetting. Also, + SVGLength only uses floats. +- If you look at the six arrays for holding the output data you'll realise + that there's no O(1) way to drill down from a paragraph to find its + starting glyph. This was a conscious decision to reduce complexity and + to save memory. Drilling down isn't actually that slow because a binary + chop will work nicely. Add this to the realisation that most of the + times you do this will be in response to user actions and hence you only + need to be faster than the user and I think the design makes sense. +- There are a massive number of functions acting on Layout::iterator. A + large number are trivial and will be inline, but is it really necessary + to have all these, especially when some can be implemented by the caller + using the others? +- The separation of methods between Layout and Layout::iterator is a + bit arbitrary, because many methods could go in either. I've used the STL + model where the iterator itself can only move around; the base class is + required to do anything interesting. +- I use Pango internally, not Pangomm. The reason for this is lots of + Pangomm methods take Glib::ustrings as input and then output byte offsets + within the strings. There's simply no way to use byte offsets with + ustrings without some very entertaining reinterpret_cast<>s. The Pangomm + docs seem to be lacking quite a lot of things mentioned in the Pango + docs, too. +*/ +class Layout { +public: + class iterator; + friend class iterator; + class Calculator; + friend class Calculator; + class ScanlineMaker; + class InfiniteScanlineMaker; + class ShapeScanlineMaker; + + Layout(); + virtual ~Layout(); + + /** Used to specify any particular text direction required. Used for + both the 'direction' and 'block-progression' CSS attributes. */ + enum Direction {LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP}; + + /** Used to specify orientation of glyphs in vertical text. */ + enum Orientation {ORIENTATION_UPRIGHT, ORIENTATION_SIDEWAYS}; + + + /** Display alignment for shapes. See appendWrapShape(). */ + enum DisplayAlign {DISPLAY_ALIGN_BEFORE, DISPLAY_ALIGN_CENTER, DISPLAY_ALIGN_AFTER}; + + /** lengthAdjust values */ + enum LengthAdjust {LENGTHADJUST_SPACING, LENGTHADJUST_SPACINGANDGLYPHS}; + + enum WrapMode { + WRAP_NONE, // No wrapping or wrapping via role="line". + WRAP_WHITE_SPACE, // Wrapping via 'white-space' property. + WRAP_INLINE_SIZE, // Wrapping via 'inline-size' property. + WRAP_SHAPE_INSIDE // Wrapping via 'shape-inside' propertry. + } wrap_mode = WRAP_NONE; + + /** The optional attributes which can be applied to a SVG text or + related tag. See appendText(). See SVG1.1 section 10.4 for the + definitions of all these members. See sp_svg_length_list_read() for + the standard way to make these vectors. It is the responsibility of + the caller to deal with the inheritance of these values using its + knowledge of the parse tree. */ + struct OptionalTextTagAttrs { + std::vector x; + std::vector y; + std::vector dx; + std::vector dy; + std::vector rotate; + SVGLength textLength; + LengthAdjust lengthAdjust; + }; + + /** Control codes which can be embedded in the text to be flowed. See + appendControlCode(). */ + enum TextControlCode { + PARAGRAPH_BREAK, /// forces the flow to move on to the next line + SHAPE_BREAK, /// forces the flow to ignore the remainder of the current shape (from #flow_inside_shapes) and continue at the top of the one after. + ARBITRARY_GAP /// inserts an arbitrarily-sized hole in the flow in line with the current text. + }; + + /** For expressing paragraph alignment. These values are rotated in the + case of vertical text, but are not dependent on whether the paragraph is + rtl or ltr, thus LEFT is always either left or top. */ + enum Alignment {LEFT, CENTER, RIGHT, FULL, NONE}; + + /** The CSS spec allows line-height:normal to be whatever the user agent + thinks will look good. This is our value, as a multiple of font-size. */ + static const double LINE_HEIGHT_NORMAL; + + // ************************** describing the stuff to flow ************************* + + /** \name Input + Methods for describing the text you want to flow, its style, and the + shapes to flow in to. + */ + //@{ + + /** Empties everything stored in this class and resets it to its + original state, like when it was created. All iterators on this + object will be invalidated (but can be revalidated using + validateIterator(). */ + void clear(); + + /** Queries whether any calls have been made to appendText() or + appendControlCode() since the object was last cleared. */ + bool inputExists() const + {return !_input_stream.empty();} + + bool _input_truncated = false; + bool inputTruncated() const + {return _input_truncated;} + + /** adds a new piece of text to the end of the current list of text to + be processed. This method can only add text of a consistent style. + To add lots of different styles, call it lots of times. + \param text The text. \b Note: only a \em pointer is stored. Do not + mess with the text until after you have called + calculateFlow(). + \param style The font style. Layout will hold a reference to this + object for the duration of its ownership, ie until you + call clear() or the class is destroyed. Must not be NULL. + \param source Pointer to object that is source of text. + \param optional_attributes A structure containing additional options + for this text. See OptionalTextTagAttrs. The values are + copied to internal storage before this method returns. + \param optional_attributes_offset It is convenient for callers to be + able to use the same \a optional_attributes structure for + several sequential text fields, in which case the vectors + will need to be offset. This parameter causes the nth + element of all the vectors to be read as if it were the + first. + \param text_begin Used for selecting only a substring of \a text + to process. + \param text_end Used for selecting only a substring of \a text + to process. + */ + void appendText(Glib::ustring const &text, SPStyle *style, SPObject *source, OptionalTextTagAttrs const *optional_attributes, unsigned optional_attributes_offset, Glib::ustring::const_iterator text_begin, Glib::ustring::const_iterator text_end); + inline void appendText(Glib::ustring const &text, SPStyle *style, SPObject *source, OptionalTextTagAttrs const *optional_attributes = nullptr, unsigned optional_attributes_offset = 0) + {appendText(text, style, source, optional_attributes, optional_attributes_offset, text.begin(), text.end());} + + /** Control codes are metadata in the text stream to signify items + that occupy real space (unlike style changes) but don't belong in the + text string. See TextControlCode for the types available. + + A control code \em cannot be the first item in the input stream. Use + appendText() with an empty string to set up the paragraph properties. + \param code A member of the TextFlowControlCode enumeration. + \param width The width in pixels that this item occupies. + \param ascent The number of pixels above the text baseline that this + control code occupies. + \param descent The number of pixels below the text baseline that this + control code occupies. + \param source Pointer to object that is source of control code. + Note that for some control codes (eg tab) the values of the \a width, + \a ascender and \a descender are implied by the surrounding text (and + in the case of tabs, the values set in tab_stops) so the values you pass + here are ignored. + */ + void appendControlCode(TextControlCode code, SPObject *source, double width = 0.0, double ascent = 0.0, double descent = 0.0); + + /** Stores another shape inside which to flow the text. If this method + is never called then no automatic wrapping is done and lines will + continue to infinity if necessary. Text can be flowed inside multiple + shapes in sequence, like with frames in a DTP package. If the text flows + past the end of the last shape all remaining text is ignored. + + \param shape The Shape to use next in the flow. The storage for this + is managed by the caller, and need only be valid for + the duration of the call to calculateFlow(). + \param display_align The vertical alignment of the text within this + shape. See XSL1.0 section 7.13.4. The behaviour of + settings other than DISPLAY_ALIGN_BEFORE when using + non-rectangular shapes is undefined. + */ + void appendWrapShape(Shape const *shape, DisplayAlign display_align = DISPLAY_ALIGN_BEFORE); + + // ************************** textLength and friends ************************* + + /** Gives the length target of this layout, as given by textLength attribute. + + FIXME: by putting it here we only support @textLength on text and flowRoot, not on any + spans inside. For spans, we will need to add markers of start and end of a textLength span + into the _input_stream. These spans can nest (SVG 1.1, section 10.5). After a first layout + calculation, we would go through the input stream and, for each end of a textLength span, + go through its items, choose those where it wasn't yet set by a nested span, calculate + their number of characters, divide the length deficit by it, and set set the + textLengthMultiplier for those characters only. For now we do this for the entire layout, + without dealing with spans. + */ + SVGLength textLength; + + /** How do we meet textLength if specified: by letterspacing or by scaling horizontally */ + LengthAdjust lengthAdjust = LENGTHADJUST_SPACING; + + /** By how much each character needs to be wider or narrower, using the specified lengthAdjust + strategy, for the layout to meet its textLength target. Is set to non-zero after the layout + is calculated for the first time, then it is recalculated with each glyph getting its adjustment. */ + /** This one is used by scaling strategies: each glyph width is multiplied by this */ + double textLengthMultiplier = 1; + /** This one is used by letterspacing strategy: to each glyph width, this is added */ + double textLengthIncrement = 0; + + /** Get the actual spacing increment if it's due with the current values of above stuff, otherwise 0 */ + double getTextLengthIncrementDue() const; + /** Get the actual scale multiplier if it's due with the current values of above stuff, otherwise 1 */ + double getTextLengthMultiplierDue() const; + + /** Get actual length of layout, by summing span lengths. For one-line non-flowed text, just + the width; for multiline non-flowed, sum of lengths of all lines; for flowed text, sum of + scanline widths for all non-last lines plus text width of last line. */ + double getActualLength() const; + + + // ************************** doing the actual flowing ************************* + + /** \name Processing + The method to do the actual work of converting text into glyphs. + */ + //@{ + + /** Takes all the stuff you set with the members above here and creates + a load of glyphs for use with the members below here. All iterators on + this object will be invalidated (but can be fixed with validateIterator(). + The implementation just creates a new Layout::Calculator and calls its + Calculator::Calculate() method, so if you want more details on the + internals, go there. + \return false on failure. + */ + bool calculateFlow(); + + //@} + + // ************************** operating on the output glyphs ************************* + + /** \name Output + Methods for reading and interpreting the output glyphs. See also + Layout::iterator. + */ + //@{ + + /** Returns true if there are some glyphs in this object, ie whether + computeFlow() has been called on a non-empty input since the object was + created or the last call to clear(). */ + inline bool outputExists() const + {return !_characters.empty();} + + /** Adds all the output glyphs to \a in_arena using the given \a paintbox. + \param in_arena The arena to add the glyphs group to + \param paintbox The current rendering tile + */ + void show(DrawingGroup *in_arena, Geom::OptRect const &paintbox) const; + + /** Calculates the smallest rectangle completely enclosing all the + glyphs. + \param bounding_box Where to store the box + \param transform The transform to be applied to the entire object + prior to calculating its bounds. + */ + Geom::OptRect bounds(Geom::Affine const &transform, int start = -1, int length = -1) const; + + /** Sends all the glyphs to the given print context. + \param ctx I have + \param pbox no idea + \param dbox what these + \param bbox parameters + \param ctm do yet + */ + void print(SPPrintContext *ctx, Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox, Geom::Affine const &ctm) const; + + /** Renders all the glyphs to the given Cairo rendering context. + \param ctx The Cairo rendering context to be used + */ + void showGlyphs(CairoRenderContext *ctx) const; + + /** Returns the font family of the indexed span */ + Glib::ustring getFontFamily(unsigned span_index) const; + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + /** debug and unit test method. Creates a textual representation of the + contents of this object. The output is designed to be both human-readable + and comprehensible when diffed with a known-good dump. */ + Glib::ustring dumpAsText() const; +#endif + + /** Moves all the glyphs in the structure so that the baseline of all + the characters sits neatly along the path specified. If the text has + more than one line the results are undefined. The 'align' means to + use the SVG align method as documented in SVG1.1 section 10.13.2. + NB: njh has suggested that it would be cool if we could flow from + shape to path and back again. This is possible, so this method will be + removed at some point. + A pointer to \a path is retained by the class for use by the cursor + positioning functions. */ + void fitToPathAlign(SVGLength const &startOffset, Path const &path); + + /** Convert the specified range of characters into their bezier + outlines. + */ + SPCurve* convertToCurves(iterator const &from_glyph, iterator const &to_glyph) const; + inline SPCurve* convertToCurves() const; + + /** Apply the given transform to all the output presently stored in + this object. This only transforms the glyph positions, The glyphs + themselves will not be transformed. */ + void transform(Geom::Affine const &transform); + + //@} + + // ********** + + /** \name Output (Iterators) + Methods for operating with the Layout::iterator class. The method + names ending with 'Index' return 0-based offsets of the number of + items since the beginning of the flow. + */ + //@{ + + /** Returns an iterator pointing at the first glyph of the flowed output. + The first glyph is also the first character, line, paragraph, etc. */ + inline iterator begin() const; + + /** Returns an iterator pointing just past the end of the last glyph, + which is also just past the end of the last chunk, span, etc, etc. */ + inline iterator end() const; + + /** Returns an iterator pointing at the given character index. This + index should be related to the result from a prior call to + iteratorToCharIndex(). */ + inline iterator charIndexToIterator(int char_index) const; + + /** Returns the character index from the start of the flow represented + by the given iterator. This number isn't very useful, except for when + editing text it will stay valid across calls to computeFlow() and will + change in predictable ways when characters are added and removed. It's + also useful when transitioning old code. */ + inline int iteratorToCharIndex(iterator const &it) const; + + /** Checks the validity of the given iterator over the current layout. + If it points to a position out of the bounds for this layout it will + be corrected to the nearest valid position. If you pass an iterator + belonging to a different layout it will be converted to one for this + layout. */ + inline void validateIterator(iterator *it) const; + + /** Returns an iterator pointing to the cursor position for a mouse + click at the given coordinates. */ + iterator getNearestCursorPositionTo(double x, double y) const; + inline iterator getNearestCursorPositionTo(Geom::Point const &point) const; + + /** Returns an iterator pointing to the letter whose bounding box contains + the given coordinates. end() if the point is not over any letter. The + iterator will \em not point at the specific glyph within the character. */ + iterator getLetterAt(double x, double y) const; + inline iterator getLetterAt(Geom::Point &point) const; + + /* Returns an iterator pointing to the character in the output which + was created from the given input. If the character at the given byte + offset was removed (soft hyphens, for example) the next character after + it is returned. If no input was added with the given object, end() is + returned. If more than one input has the same object, the first will + be used regardless of the value of \a text_iterator. If + \a text_iterator is out of bounds, the first or last character belonging + to the given input will be returned accordingly. + iterator sourceToIterator(SPObject *source, Glib::ustring::const_iterator text_iterator) const; + */ + + /** Returns an iterator pointing to the first character in the output + which was created from the given source. If \a source object is invalid, + end() is returned. If more than one input has the same object, the + first one will be used. */ + iterator sourceToIterator(SPObject *source) const; + + // many functions acting on iterators, most of which are obvious + // also most of them don't check that \a it != end(). Be careful. + + /** Returns the bounding box of the given glyph, and its rotation. + The centre of rotation is the horizontal centre of the box at the + text baseline. */ + Geom::OptRect glyphBoundingBox(iterator const &it, double *rotation) const; + + /** Returns the zero-based line number of the character pointed to by + \a it. */ + inline unsigned lineIndex(iterator const &it) const; + + /** Returns the zero-based number of the shape which contains the + character pointed to by \a it. */ + inline unsigned shapeIndex(iterator const &it) const; + + /** Returns true if the character at \a it is a whitespace, as defined + by Pango. This is not meant to be used for picking out words from the + output, use iterator::nextStartOfWord() and friends instead. */ + inline bool isWhitespace(iterator const &it) const; + + /** Returns character pointed to by \a it. If \a it == end() the result is undefined. */ + inline gchar characterAt(iterator const &it) const; + + /** Returns true if the text at \a it is hidden (i.e. overflowed). */ + bool isHidden(iterator const &it) const; + + /** Discovers where the character pointed to by \a it came from, by + retrieving the object that was passed to the call to appendText() or + appendControlCode() which generated that output. If \a it == end() + then NULL is returned as the object. If the character was generated + from a call to appendText() then the optional \a text_iterator + parameter is set to point to the actual character, otherwise + \a text_iterator is unaltered. */ + void getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator = nullptr) const; + + /** For latin text, the left side of the character, on the baseline */ + Geom::Point characterAnchorPoint(iterator const &it) const; + + /** For left aligned text, the leftmost end of the baseline + For rightmost text, the rightmost... you probably got it by now ;-)*/ + boost::optional baselineAnchorPoint() const; + + Geom::Path baseline() const; + + /** This is that value to apply to the x,y attributes of tspan role=line + elements, and hence it takes alignment into account. */ + Geom::Point chunkAnchorPoint(iterator const &it) const; + + /** Returns the box extents (not ink extents) of the given character. + The centre of rotation is at the horizontal centre of the box on the + text baseline. */ + Geom::Rect characterBoundingBox(iterator const &it, double *rotation = nullptr) const; + + /** Basically uses characterBoundingBox() on all the characters from + \a start to \a end and returns the union of these boxes. The return value + is a list of zero or more quadrilaterals specified by a group of four + points for each, thus size() is always a multiple of four. */ + std::vector createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const; + + /** Returns true if \a it points to a character which is a valid cursor + position, as defined by Pango. */ + inline bool isCursorPosition(iterator const &it) const; + + /** Gets the ideal cursor shape for a given iterator. The result is + undefined if \a it is not at a valid cursor position. + \param it The location in the output + \param position The pixel location of the centre of the 'bottom' of + the cursor. + \param height The height in pixels of the surrounding text + \param rotation The angle to draw from \a position. Radians, zero up, + increasing clockwise. + */ + void queryCursorShape(iterator const &it, Geom::Point &position, double &height, double &rotation) const; + + /** Returns true if \a it points to a character which is a the start of + a word, as defined by Pango. */ + inline bool isStartOfWord(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the end of + a word, as defined by Pango. */ + inline bool isEndOfWord(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the start of + a sentence, as defined by Pango. */ + inline bool isStartOfSentence(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the end of + a sentence, as defined by Pango. */ + inline bool isEndOfSentence(iterator const &it) const; + + /** Returns the zero-based number of the paragraph containing the + character pointed to by \a it. */ + inline unsigned paragraphIndex(iterator const &it) const; + + /** Returns the actual alignment used for the paragraph containing + the character pointed to by \a it. This means that the CSS 'start' + and 'end' are correctly translated into LEFT or RIGHT according to + the paragraph's directionality. For vertical text, LEFT is top + alignment and RIGHT is bottom. */ + inline Alignment paragraphAlignment(iterator const &it) const; + + /** Returns kerning information which could cause the current output + to be exactly reproduced if the letter and word spacings were zero and + full justification was not used. The x and y arrays are not used, but + they are cleared. The dx applied to the first character in a chunk + will always be zero. If the region between \a from and \a to crosses + a line break then the results may be surprising, and are undefined. + Trailing zeros on the returned arrays will be trimmed. */ + void simulateLayoutUsingKerning(iterator const &from, iterator const &to, OptionalTextTagAttrs *result) const; + + //@} + + + /** + * Keep track of font metrics. Two use cases: + * 1. Keep track of ascent, descent, and x-height of an individual font. + * 2. Keep track of effective ascent and descent that includes half-leading. + * + * Note: Leading refers to the "external" leading which is added (subtracted) due to + * a computed value of 'line-height' that differs from 'font-size'. "Internal" leading + * which is specified inside a font is not used in CSS. The 'font-size' is based on + * the font's em size which is 'ascent' + 'descent'. + * + * This structure was renamed (and modified) from "LineHeight". + * + * It's useful for this to be public so that ScanlineMaker can use it. + */ + class FontMetrics { + + public: + FontMetrics() { reset(); } + + void reset() { + ascent = 0.8; + descent = 0.2; + xheight = 0.5; + ascent_max = 0.8; + descent_max = 0.2; + } + + void set( font_instance *font ); + + // CSS 2.1 dictates that font-size is based on em-size which is defined as ascent + descent + inline double emSize() const {return ascent + descent;} + // Alternatively name function for use 2. + inline double lineSize() const { return ascent + descent; } + inline void setZero() {ascent = descent = xheight = ascent_max = descent_max = 0.0;} + + // For scaling for 'font-size'. + inline FontMetrics& operator*=(double x) { + ascent *= x; descent *= x; xheight *= x; ascent_max *= x; descent_max *= x; + return *this; + } + + /// Save the larger values of ascent and descent between this and other. Needed for laying + /// out a line with mixed font-sizes, fonts, or line spacings. + void max(FontMetrics const &other); + + /// Calculate the effective ascent and descent including half "leading". + void computeEffective( const double &line_height ); + + inline double getTypoAscent() const {return ascent; } + inline double getTypoDescent() const {return descent; } + inline double getXHeight() const {return xheight; } + inline double getMaxAscent() const {return ascent_max; } + inline double getMaxDescent() const {return descent_max; } + + // private: + double ascent; // Typographic ascent. + double descent; // Typographic descent. (Normally positive). + double xheight; // Height of 'x' measured from alphabetic baseline. + double ascent_max; // Maximum ascent of all glyphs in font. + double descent_max; // Maximum descent of all glyphs in font. + + }; // End FontMetrics + + /** The strut is the minimum value used in calculating line height. */ + FontMetrics strut; + +private: + /** Erases all the stuff set by the owner as input, ie #_input_stream + and #_input_wrap_shapes. */ + void _clearInputObjects(); + + /** Erases all the stuff output by computeFlow(). Glyphs and things. */ + void _clearOutputObjects(); + + static const gunichar UNICODE_SOFT_HYPHEN; + + // ******************* input flow + + enum InputStreamItemType {TEXT_SOURCE, CONTROL_CODE}; + + class InputStreamItem { + public: + virtual ~InputStreamItem() = default; + virtual InputStreamItemType Type() =0; + SPObject *source; + }; + + /** Represents a text item in the input stream. See #_input_stream. + Most of the members are copies of the values passed to appendText(). */ + class InputStreamTextSource : public InputStreamItem { + public: + InputStreamItemType Type() override {return TEXT_SOURCE;} + ~InputStreamTextSource() override; + Glib::ustring const *text; /// owned by the caller + Glib::ustring::const_iterator text_begin, text_end; + int text_length; /// in characters, from text_start to text_end only + SPStyle *style; + /** These vectors can (often will) be shorter than the text + in this source, but never longer. */ + std::vector x; + std::vector y; + std::vector dx; + std::vector dy; + std::vector rotate; + SVGLength textLength; + LengthAdjust lengthAdjust; + Glib::ustring lang; + + // a few functions for some of the more complicated style accesses + /// The return value must be freed with pango_font_description_free() + PangoFontDescription *styleGetFontDescription() const; + font_instance *styleGetFontInstance() const; + Direction styleGetBlockProgression() const; + SPCSSTextOrientation styleGetTextOrientation() const; + SPCSSBaseline styleGetDominantBaseline() const; + Alignment styleGetAlignment(Direction para_direction, bool try_text_align) const; + }; + + /** Represents a control code item in the input stream. See + #_input_streams. All the members are copies of the values passed to + appendControlCode(). */ + class InputStreamControlCode : public InputStreamItem { + public: + InputStreamItemType Type() override {return CONTROL_CODE;} + TextControlCode code; + double ascent; + double descent; + double width; + }; + + /** This is our internal storage for all the stuff passed to the + appendText() and appendControlCode() functions. */ + std::vector _input_stream; + + /** The parameters to appendText() are allowed to be a little bit + complex. This copies them to be the right length and starting at zero. + We also don't want to write five bits of identical code just with + different variable names. */ + static void _copyInputVector(std::vector const &input_vector, unsigned input_offset, std::vector *output_vector, size_t max_length); + + /** The overall block-progression of the whole flow. */ + inline Direction _blockProgression() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetBlockProgression(); + return TOP_TO_BOTTOM; + } + + /** The overall text-orientation of the whole flow. */ + inline SPCSSTextOrientation _blockTextOrientation() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetTextOrientation(); + return SP_CSS_TEXT_ORIENTATION_MIXED; + } + + /** The overall text-orientation of the whole flow. */ + inline SPCSSBaseline _blockBaseline() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetDominantBaseline(); + return SP_CSS_BASELINE_AUTO; + } + + /** so that LEFT_TO_RIGHT == RIGHT_TO_LEFT but != TOP_TO_BOTTOM */ + static bool _directions_are_orthogonal(Direction d1, Direction d2); + + /** If the output is empty callers still want to be able to call + queryCursorShape() and get a valid answer so, while #_input_wrap_shapes + can still be considered valid, we need to precompute the cursor shape + for this case. */ + void _calculateCursorShapeForEmpty(); + + struct CursorShape { + Geom::Point position; + double height; + double rotation; + } _empty_cursor_shape; + + // ******************* input shapes + + struct InputWrapShape { + Shape const *shape; /// as passed to Layout::appendWrapShape() + DisplayAlign display_align; /// as passed to Layout::appendWrapShape() + }; + std::vector _input_wrap_shapes; + + // ******************* output + + /** as passed to fitToPathAlign() */ + Path const *_path_fitted = nullptr; + + struct Glyph; + struct Character; + struct Span; + struct Chunk; + struct Line; + struct Paragraph; + + // A glyph + struct Glyph { + int glyph; + unsigned in_character; + bool hidden; + float x; /// relative to the start of the chunk + float y; /// relative to the current line's baseline + float rotation; /// absolute, modulo any object transforms, which we don't know about + Orientation orientation; /// Orientation of glyph in vertical text + float advance; /// for positioning next glyph + float vertical_scale; /// to implement lengthAdjust="spacingAndGlyphs" that must scale glyphs only horizontally; instead we change font size and then undo that change vertically only + inline Span const & span (Layout const *l) const {return l->_spans[l->_characters[in_character].in_span];} + inline Chunk const & chunk(Layout const *l) const {return l->_chunks[l->_spans[l->_characters[in_character].in_span].in_chunk];} + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[l->_spans[l->_characters[in_character].in_span].in_chunk].in_line];} + }; + + // A unicode character + struct Character { + unsigned in_span; + float x; /// relative to the start of the *span* (so we can do block-progression) + PangoLogAttr char_attributes; + gchar the_char = '#'; + int in_glyph; /// will be -1 if this character has no visual representation + inline Span const & span (Layout const *l) const {return l->_spans[in_span];} + inline Chunk const & chunk (Layout const *l) const {return l->_chunks[l->_spans[in_span].in_chunk];} + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[l->_spans[in_span].in_chunk].in_line];} + inline Paragraph const & paragraph(Layout const *l) const {return l->_paragraphs[l->_lines[l->_chunks[l->_spans[in_span].in_chunk].in_line].in_paragraph];} + // to get the advance width of a character, subtract the x values if it's in the middle of a span, or use span.x_end if it's at the end + }; + + // A collection of characters that share the same style and position start ( or x, y attributes). + struct Span { + unsigned in_chunk; + font_instance *font; + float font_size; + float x_start; /// relative to the start of the chunk + float x_end; /// relative to the start of the chunk + float y_offset; /// relative to line baseline (without baseline shift) + inline float width() const {return std::abs(x_start - x_end);} + FontMetrics line_height; + double baseline_shift; /// relative to the line's baseline (CSS) + SPCSSTextOrientation text_orientation; + Direction direction; /// See CSS3 section 3.2. Either rtl or ltr + Direction block_progression; /// See CSS3 section 3.2. The direction in which lines go. + unsigned in_input_stream_item; + Glib::ustring::const_iterator input_stream_first_character; + inline Chunk const & chunk (Layout const *l) const {return l->_chunks[in_chunk]; } + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[in_chunk].in_line]; } + inline Paragraph const & paragraph(Layout const *l) const {return l->_paragraphs[l->_lines[l->_chunks[in_chunk].in_line].in_paragraph];} + }; + + // A part of a line that is not broken. + struct Chunk { + unsigned in_line; + double left_x; + }; + + // A line of text. Depending on the shape, it may contain one or more chunks. + struct Line { + unsigned in_paragraph; + double baseline_y; + unsigned in_shape; + bool hidden; + }; + + // A paragraph. SVG 2 does not contain native paragraphs. + struct Paragraph { + Direction base_direction; /// can be overridden by child Span objects + Alignment alignment; + }; + + std::vector _paragraphs; + std::vector _lines; + std::vector _chunks; + std::vector _spans; + std::vector _characters; + std::vector _glyphs; + + /** gets the overall matrix that transforms the given glyph from local + space to world space. */ + void _getGlyphTransformMatrix(int glyph_index, Geom::Affine *matrix) const; + + // loads of functions to drill down the object tree, all of them + // annoyingly similar and all of them requiring predicate functors. + // I'll be buggered if I can find a way to make it work with + // functions or with a templated functor, so macros it is. +#define EMIT_PREDICATE(name, object_type, index_generator) \ + class name { \ + Layout const * const _flow; \ + public: \ + inline name(Layout const *flow) : _flow(flow) {} \ + inline bool operator()(object_type const &object, unsigned index) \ + {g_assert(_flow); return index_generator < index;} \ + } +// end of macro + EMIT_PREDICATE(PredicateLineToSpan, Span, _flow->_chunks[object.in_chunk].in_line); + EMIT_PREDICATE(PredicateLineToCharacter, Character, _flow->_chunks[_flow->_spans[object.in_span].in_chunk].in_line); + EMIT_PREDICATE(PredicateSpanToCharacter, Character, object.in_span); + EMIT_PREDICATE(PredicateSourceToCharacter, Character, _flow->_spans[object.in_span].in_input_stream_item); + + inline unsigned _lineToSpan(unsigned line_index) const + {return std::lower_bound(_spans.begin(), _spans.end(), line_index, PredicateLineToSpan(this)) - _spans.begin();} + inline unsigned _lineToCharacter(unsigned line_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), line_index, PredicateLineToCharacter(this)) - _characters.begin();} + inline unsigned _spanToCharacter(unsigned span_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), span_index, PredicateSpanToCharacter(this)) - _characters.begin();} + inline unsigned _sourceToCharacter(unsigned source_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), source_index, PredicateSourceToCharacter(this)) - _characters.begin();} + + /** given an x and y coordinate and a line number, returns an iterator + pointing to the closest cursor position on that line to the + coordinate. + ('y' is needed to handle cases where multiline text is simulated via the 'y' attribute.) */ + iterator _cursorXOnLineToIterator(unsigned line_index, double local_x, double local_y = 0) const; + + /** calculates the width of a chunk, which is the largest x + coordinate (start or end) of the spans contained within it. */ + double _getChunkWidth(unsigned chunk_index) const; +}; + +/** \brief Holds a position within the glyph output of Layout. + +Used to access the output of a Layout, query information and generally +move around in it. See Layout for a glossary of the names of functions. + +I'm not going to document all the methods because most of their names make +their function self-evident. + +A lot of the functions would do the same thing in a naive implementation +for latin-only text, for example nextCharacter(), nextCursorPosition() and +cursorRight(). Generally it's fairly obvious which one you should use in a +given situation, but sometimes you might need to put some thought in to it. + +All the methods return false if the requested action would have caused the +current position to move out of bounds. In this case the position is moved +to either begin() or end(), depending on which direction you were going. + +Note that some characters do not have a glyph representation (eg line +breaks), so if you try using prev/nextGlyph() from one of these you're +heading for a crash. +*/ +class Layout::iterator { +public: + friend class Layout; + // this is just so you can create uninitialised iterators - don't actually try to use one + iterator() : + _parent_layout(nullptr), + _glyph_index(-1), + _char_index(0), + _cursor_moving_vertically(false), + _x_coordinate(0.0){} + // no copy constructor required, the default does what we want + bool operator== (iterator const &other) const + {return _glyph_index == other._glyph_index && _char_index == other._char_index;} + bool operator!= (iterator const &other) const + {return _glyph_index != other._glyph_index || _char_index != other._char_index;} + + /* mustn't compare _glyph_index in these operators because for characters + that don't have glyphs (line breaks, elided soft hyphens, etc), the glyph + index is -1 which makes them not well-ordered. To be honest, iterating by + glyphs is not very useful and should be avoided. */ + bool operator< (iterator const &other) const + {return _char_index < other._char_index;} + bool operator<= (iterator const &other) const + {return _char_index <= other._char_index;} + bool operator> (iterator const &other) const + {return _char_index > other._char_index;} + bool operator>= (iterator const &other) const + {return _char_index >= other._char_index;} + + /* **** visual-oriented methods **** */ + + //glyphs + inline bool prevGlyph(); + inline bool nextGlyph(); + + //span + bool prevStartOfSpan(); + bool thisStartOfSpan(); + bool nextStartOfSpan(); + + //chunk + bool prevStartOfChunk(); + bool thisStartOfChunk(); + bool nextStartOfChunk(); + + //line + bool prevStartOfLine(); + bool thisStartOfLine(); + bool nextStartOfLine(); + bool thisEndOfLine(); + + //shape + bool prevStartOfShape(); + bool thisStartOfShape(); + bool nextStartOfShape(); + + /* **** text-oriented methods **** */ + + //characters + inline bool nextCharacter(); + inline bool prevCharacter(); + + bool nextCursorPosition(); + bool prevCursorPosition(); + bool nextLineCursor(int n = 1); + bool prevLineCursor(int n = 1); + + //words + bool nextStartOfWord(); + bool prevStartOfWord(); + bool nextEndOfWord(); + bool prevEndOfWord(); + + //sentences + bool nextStartOfSentence(); + bool prevStartOfSentence(); + bool nextEndOfSentence(); + bool prevEndOfSentence(); + + //paragraphs + bool prevStartOfParagraph(); + bool thisStartOfParagraph(); + bool nextStartOfParagraph(); + //no endOfPara methods because that's just the previous char + + //sources + bool prevStartOfSource(); + bool thisStartOfSource(); + bool nextStartOfSource(); + + //logical cursor movement + bool cursorUp(int n = 1); + bool cursorDown(int n = 1); + bool cursorLeft(); + bool cursorRight(); + + //logical cursor movement (by word or paragraph) + bool cursorUpWithControl(); + bool cursorDownWithControl(); + bool cursorLeftWithControl(); + bool cursorRightWithControl(); + +private: + Layout const *_parent_layout; + int _glyph_index; /// index into Layout::glyphs, or -1 + unsigned _char_index; /// index into Layout::character + bool _cursor_moving_vertically; + /** for cursor up/down movement we must maintain the x position where + we started so the cursor doesn't 'drift' left or right with the repeated + quantization to character boundaries. */ + double _x_coordinate; + + inline iterator(Layout const *p, unsigned c, int g) + : _parent_layout(p), _glyph_index(g), _char_index(c), _cursor_moving_vertically(false), _x_coordinate(0.0) {} + inline iterator(Layout const *p, unsigned c) + : _parent_layout(p), _glyph_index(p->_characters[c].in_glyph), _char_index(c), _cursor_moving_vertically(false), _x_coordinate(0.0) {} + // no dtor required + void beginCursorUpDown(); /// stores the current x coordinate so that the cursor won't drift. See #_x_coordinate + + /** moves forward or backwards one cursor position according to the + directionality of the current paragraph, but ignoring block progression. + Helper for the cursor*() functions. */ + bool _cursorLeftOrRightLocalX(Direction direction); + + /** moves forward or backwards by until the next character with + is_word_start according to the directionality of the current paragraph, + but ignoring block progression. Helper for the cursor*WithControl() + functions. */ + bool _cursorLeftOrRightLocalXByWord(Direction direction); +}; + +// ************************** inline methods + +inline SPCurve* Layout::convertToCurves() const + {return convertToCurves(begin(), end());} + +inline Layout::iterator Layout::begin() const + {return iterator(this, 0, 0);} + +inline Layout::iterator Layout::end() const + {return iterator(this, _characters.size(), _glyphs.size());} + +inline Layout::iterator Layout::charIndexToIterator(int char_index) const +{ + if (char_index < 0) return begin(); + if (char_index >= (int)_characters.size()) return end(); + return iterator(this, char_index); +} + +inline int Layout::iteratorToCharIndex(Layout::iterator const &it) const + {return it._char_index;} + +inline void Layout::validateIterator(Layout::iterator *it) const +{ + it->_parent_layout = this; + if (it->_char_index >= _characters.size()) { + it->_char_index = _characters.size(); + it->_glyph_index = _glyphs.size(); + } else + it->_glyph_index = _characters[it->_char_index].in_glyph; +} + +inline Layout::iterator Layout::getNearestCursorPositionTo(Geom::Point const &point) const + {return getNearestCursorPositionTo(point[0], point[1]);} + +inline Layout::iterator Layout::getLetterAt(Geom::Point &point) const + {return getLetterAt(point[0], point[1]);} + +inline unsigned Layout::lineIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _lines.size() - 1 : _characters[it._char_index].chunk(this).in_line;} + +inline unsigned Layout::shapeIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _input_wrap_shapes.size() - 1 : _characters[it._char_index].line(this).in_shape;} + +inline bool Layout::isWhitespace(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_white;} + +inline gchar Layout::characterAt(iterator const &it) const +{ + return _characters[it._char_index].the_char; +} + +inline bool Layout::isCursorPosition(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_cursor_position;} + +inline bool Layout::isStartOfWord(iterator const &it) const + {return it._char_index != _characters.size() && _characters[it._char_index].char_attributes.is_word_start;} + +inline bool Layout::isEndOfWord(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_word_end;} + +inline bool Layout::isStartOfSentence(iterator const &it) const + {return it._char_index != _characters.size() && _characters[it._char_index].char_attributes.is_sentence_start;} + +inline bool Layout::isEndOfSentence(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_sentence_end;} + +inline unsigned Layout::paragraphIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _paragraphs.size() - 1 : _characters[it._char_index].line(this).in_paragraph;} + +inline Layout::Alignment Layout::paragraphAlignment(iterator const &it) const + {return (_paragraphs.size() == 0) ? NONE : _paragraphs[paragraphIndex(it)].alignment;} + +inline bool Layout::iterator::nextGlyph() +{ + _cursor_moving_vertically = false; + if (_glyph_index >= (int)_parent_layout->_glyphs.size() - 1) { + if (_glyph_index == (int)_parent_layout->_glyphs.size()) return false; + _char_index = _parent_layout->_characters.size(); + _glyph_index = _parent_layout->_glyphs.size(); + } + else _char_index = _parent_layout->_glyphs[++_glyph_index].in_character; + return true; +} + +inline bool Layout::iterator::prevGlyph() +{ + _cursor_moving_vertically = false; + if (_glyph_index == 0) return false; + _char_index = _parent_layout->_glyphs[--_glyph_index].in_character; + return true; +} + +inline bool Layout::iterator::nextCharacter() +{ + _cursor_moving_vertically = false; + if (_char_index + 1 >= _parent_layout->_characters.size()) { + if (_char_index == _parent_layout->_characters.size()) return false; + _char_index = _parent_layout->_characters.size(); + _glyph_index = _parent_layout->_glyphs.size(); + } + else _glyph_index = _parent_layout->_characters[++_char_index].in_glyph; + return true; +} + +inline bool Layout::iterator::prevCharacter() +{ + _cursor_moving_vertically = false; + if (_char_index == 0) return false; + _glyph_index = _parent_layout->_characters[--_char_index].in_glyph; + return true; +} + +}//namespace Text +}//namespace Inkscape + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics &f); +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics *f); + + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/OpenTypeUtil.cpp b/src/libnrtype/OpenTypeUtil.cpp new file mode 100644 index 0000000..a456626 --- /dev/null +++ b/src/libnrtype/OpenTypeUtil.cpp @@ -0,0 +1,434 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef USE_PANGO_WIN32 + +#include "OpenTypeUtil.h" + + +#include // For debugging + +// FreeType +#include FT_FREETYPE_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H + +// Harfbuzz +#include +#include + +// SVG in OpenType +#include "util/ziptool.h" + + +// Utilities used in this file + +void dump_tag( guint32 *tag, Glib::ustring prefix = "", bool lf=true ) { + std::cout << prefix + << ((char)((*tag & 0xff000000)>>24)) + << ((char)((*tag & 0x00ff0000)>>16)) + << ((char)((*tag & 0x0000ff00)>> 8)) + << ((char)((*tag & 0x000000ff) )); + if( lf ) { + std::cout << std::endl; + } +} + +Glib::ustring extract_tag( guint32 *tag ) { + Glib::ustring tag_name; + tag_name += ((char)((*tag & 0xff000000)>>24)); + tag_name += ((char)((*tag & 0x00ff0000)>>16)); + tag_name += ((char)((*tag & 0x0000ff00)>> 8)); + tag_name += ((char)((*tag & 0x000000ff) )); + return tag_name; +} + + +// TODO: Ideally, we should use the HB_VERSION_ATLEAST macro here, +// but this was only released in harfbuzz >= 0.9.30 +// #if HB_VERSION_ATLEAST(1,2,3) +#if HB_VERSION_MAJOR*10000 + HB_VERSION_MINOR*100 + HB_VERSION_MICRO >= 10203 +void get_glyphs( hb_font_t* font, hb_set_t* set, Glib::ustring& characters) { + + // There is a unicode to glyph mapping function but not the inverse! + hb_codepoint_t codepoint = -1; + while (hb_set_next (set, &codepoint)) { + for (hb_codepoint_t unicode_i = 0; unicode_i < 0xffff; ++unicode_i) { + hb_codepoint_t glyph = 0; + hb_font_get_nominal_glyph (font, unicode_i, &glyph); + if (glyph == codepoint) { + characters += (gunichar)unicode_i; + continue; + } + } + } +} +#endif + +// Make a list of all tables found in the GSUB +// This list includes all tables regardless of script or language. +void readOpenTypeGsubTable (const FT_Face ft_face, + std::map& tables + ) { + + // std::cout << "readOpenTypeGsubTable: Entrance: " + // << (ft_face->family_name?ft_face->family_name:"null") << std::endl; + tables.clear(); + + // Use Harfbuzz, Pango's equivalent calls are deprecated. + auto const hb_face = hb_ft_face_create(ft_face, nullptr); + + // First time to get size of array + auto script_count = hb_ot_layout_table_get_script_tags(hb_face, HB_OT_TAG_GSUB, 0, nullptr, nullptr); + auto const hb_scripts = g_new(hb_tag_t, script_count + 1); + + // Second time to fill array (this two step process was not necessary with Pango). + hb_ot_layout_table_get_script_tags(hb_face, HB_OT_TAG_GSUB, 0, &script_count, hb_scripts); + + for(unsigned int i = 0; i < script_count; ++i) { + // std::cout << " Script: " << extract_tag(&hb_scripts[i]) << std::endl; + auto language_count = hb_ot_layout_script_get_language_tags(hb_face, HB_OT_TAG_GSUB, i, 0, nullptr, nullptr); + + if(language_count > 0) { + auto const hb_languages = g_new(hb_tag_t, language_count + 1); + hb_ot_layout_script_get_language_tags(hb_face, HB_OT_TAG_GSUB, i, 0, &language_count, hb_languages); + + for(unsigned int j = 0; j < language_count; ++j) { + // std::cout << " Language: " << extract_tag(&hb_languages[j]) << std::endl; + auto feature_count = hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, j, 0, nullptr, nullptr); + auto const hb_features = g_new(hb_tag_t, feature_count + 1); + hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, j, 0, &feature_count, hb_features); + + for(unsigned int k = 0; k < feature_count; ++k) { + // std::cout << " Feature: " << extract_tag(&hb_features[k]) << std::endl; + tables[ extract_tag(&hb_features[k])]; + } + + g_free(hb_features); + } + + g_free(hb_languages); + + } else { + + // Even if no languages are present there is still the default. + // std::cout << " Language: " << " (dflt)" << std::endl; + auto feature_count = hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + 0, nullptr, nullptr); + auto const hb_features = g_new(hb_tag_t, feature_count + 1); + hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + 0, &feature_count, hb_features); + + for(unsigned int k = 0; k < feature_count; ++k) { + // std::cout << " Feature: " << extract_tag(&hb_features[k]) << std::endl; + tables[ extract_tag(&hb_features[k])]; + } + + g_free(hb_features); + } + } + +// TODO: Ideally, we should use the HB_VERSION_ATLEAST macro here, +// but this was only released in harfbuzz >= 0.9.30 +// #if HB_VERSION_ATLEAST(1,2,3) +#if HB_VERSION_MAJOR*10000 + HB_VERSION_MINOR*100 + HB_VERSION_MICRO >= 10203 + // Find glyphs in OpenType substitution tables ('gsub'). + // Note that pango's functions are just dummies. Must use harfbuzz. + + // Loop over all tables + for (auto table: tables) { + + // Only look at style substitution tables ('salt', 'ss01', etc. but not 'ssty'). + // Also look at character substitution tables ('cv01', etc.). + bool style = + table.first == "case" /* Case-Sensitive Forms */ || + table.first == "salt" /* Stylistic Alternatives */ || + table.first == "swsh" /* Swash */ || + table.first == "cwsh" /* Contextual Swash */ || + table.first == "ornm" /* Ornaments */ || + table.first == "nalt" /* Alternative Annotation */ || + table.first == "hist" /* Historical Forms */ || + (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't')) || + (table.first[0] == 'c' && table.first[1] == 'v'); + + bool ligature = ( table.first == "liga" || // Standard ligatures + table.first == "clig" || // Common ligatures + table.first == "dlig" || // Discretionary ligatures + table.first == "hlig" || // Historical ligatures + table.first == "calt" ); // Contextual alternatives + + bool numeric = ( table.first == "lnum" || // Lining numerals + table.first == "onum" || // Old style + table.first == "pnum" || // Proportional + table.first == "tnum" || // Tabular + table.first == "frac" || // Diagonal fractions + table.first == "afrc" || // Stacked fractions + table.first == "ordn" || // Ordinal fractions + table.first == "zero" ); // Slashed zero + + if (style || ligature || numeric) { + + unsigned int feature_index; + if ( hb_ot_layout_language_find_feature (hb_face, HB_OT_TAG_GSUB, + 0, // Assume one script exists with index 0 + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + HB_TAG(table.first[0], + table.first[1], + table.first[2], + table.first[3]), + &feature_index ) ) { + + // std::cout << "Table: " << table.first << std::endl; + // std::cout << " Found feature, number: " << feature_index << std::endl; + unsigned int lookup_indexes[32]; + unsigned int lookup_count = 32; + int count = hb_ot_layout_feature_get_lookups (hb_face, HB_OT_TAG_GSUB, + feature_index, + 0, // Start + &lookup_count, + lookup_indexes ); + // std::cout << " Lookup count: " << count << " total: " << lookup_count << std::endl; + + hb_font_t *hb_font = hb_font_create (hb_face); // MOVE THIS OUT OF LOOPS? + + for (int i = 0; i < count; ++i) { + hb_set_t* glyphs_before = hb_set_create(); + hb_set_t* glyphs_input = hb_set_create(); + hb_set_t* glyphs_after = hb_set_create(); + hb_set_t* glyphs_output = hb_set_create(); + + hb_ot_layout_lookup_collect_glyphs (hb_face, HB_OT_TAG_GSUB, + lookup_indexes[i], + glyphs_before, + glyphs_input, + glyphs_after, + glyphs_output ); + + // std::cout << " Populations: " + // << " " << hb_set_get_population (glyphs_before) + // << " " << hb_set_get_population (glyphs_input) + // << " " << hb_set_get_population (glyphs_after) + // << " " << hb_set_get_population (glyphs_output) + // << std::endl; + + // Without this, all functions return 0, etc. + hb_ft_font_set_funcs (hb_font); + + get_glyphs (hb_font, glyphs_before, tables[table.first].before); + get_glyphs (hb_font, glyphs_input, tables[table.first].input ); + get_glyphs (hb_font, glyphs_after, tables[table.first].after ); + get_glyphs (hb_font, glyphs_output, tables[table.first].output); + + // std::cout << " Before: " << tables[table.first].before.c_str() << std::endl; + // std::cout << " Input: " << tables[table.first].input.c_str() << std::endl; + // std::cout << " After: " << tables[table.first].after.c_str() << std::endl; + // std::cout << " Output: " << tables[table.first].output.c_str() << std::endl; + + hb_set_destroy (glyphs_before); + hb_set_destroy (glyphs_input); + hb_set_destroy (glyphs_after); + hb_set_destroy (glyphs_output); + + } // End count (lookups) + + hb_font_destroy (hb_font); + + } else { + // std::cout << " Did not find '" << table.first << "'!" << std::endl; + } + } + + } +#else + std::cerr << "Requires Harfbuzz 1.2.3 for visualizing alternative glyph OpenType tables. " + << "Compiled with: " << HB_VERSION_STRING << "." << std::endl; +#endif + + g_free(hb_scripts); + hb_face_destroy (hb_face); +} + +// Harfbuzz now as API for variations (Version 2.2, Nov 29 2018). +// Make a list of all Variation axes with ranges. +void readOpenTypeFvarAxes(const FT_Face ft_face, + std::map& axes) { + +#if FREETYPE_MAJOR *10000 + FREETYPE_MINOR*100 + FREETYPE_MICRO >= 20701 + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( ft_face ) && // Font has variables + FT_Get_MM_Var( ft_face, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master( ft_face, &mmtype) !=0) { // It's not an Adobe MM font + + FT_Fixed coords[mmvar->num_axis]; + FT_Get_Var_Design_Coordinates( ft_face, mmvar->num_axis, coords ); + + for (size_t i = 0; i < mmvar->num_axis; ++i) { + FT_Var_Axis* axis = &mmvar->axis[i]; + axes[axis->name] = OTVarAxis(FTFixedToDouble(axis->minimum), + FTFixedToDouble(axis->maximum), + FTFixedToDouble(coords[i]), + i); + } + + // for (auto a: axes) { + // std::cout << " " << a.first + // << " min: " << a.second.minimum + // << " max: " << a.second.maximum + // << " set: " << a.second.set_val << std::endl; + // } + + } + +#endif /* FREETYPE Version */ +} + + +// Harfbuzz now as API for named variations (Version 2.2, Nov 29 2018). +// Make a list of all Named instances with axis values. +void readOpenTypeFvarNamed(const FT_Face ft_face, + std::map& named) { + +#if FREETYPE_MAJOR *10000 + FREETYPE_MINOR*100 + FREETYPE_MICRO >= 20701 + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( ft_face ) && // Font has variables + FT_Get_MM_Var( ft_face, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master( ft_face, &mmtype) !=0) { // It's not an Adobe MM font + + std::cout << " Multiple Masters: variables: " << mmvar->num_axis + << " named styles: " << mmvar->num_namedstyles << std::endl; + + // const FT_UInt numNames = FT_Get_Sfnt_Name_Count(ft_face); + // std::cout << " number of names: " << numNames << std::endl; + // FT_SfntName ft_name; + // for (FT_UInt i = 0; i < numNames; ++i) { + + // if (FT_Get_Sfnt_Name(ft_face, i, &ft_name) != 0) { + // continue; + // } + + // Glib::ustring name; + // for (size_t j = 0; j < ft_name.string_len; ++j) { + // name += (char)ft_name.string[j]; + // } + // std::cout << " " << i << ": " << name << std::endl; + // } + + } + +#endif /* FREETYPE Version */ +} + +#define HB_OT_TAG_SVG HB_TAG('S','V','G',' ') + +// Get SVG glyphs out of an OpenType font. +void readOpenTypeSVGTable(const FT_Face ft_face, + std::map& glyphs) { + + // Harfbuzz has some support for SVG fonts but it is not exposed until version 2.1 (Oct 30, 2018). + // We do it the hard way! + hb_face_t *hb_face = hb_ft_face_create_cached (ft_face); + hb_blob_t *hb_blob = hb_face_reference_table (hb_face, HB_OT_TAG_SVG); + + if (!hb_blob) { + // No SVG table in font! + return; + } + + unsigned int svg_length = hb_blob_get_length (hb_blob); + if (svg_length == 0) { + // No SVG glyphs in table! + return; + } + + const char* data = hb_blob_get_data(hb_blob, &svg_length); + if (!data) { + std::cerr << "readOpenTypeSVGTable: Failed to get data! " + << (ft_face->family_name?ft_face->family_name:"Unknown family") << std::endl; + return; + } + + // OpenType fonts use Big Endian +#if 0 + uint16_t version = ((data[0] & 0xff) << 8) + (data[1] & 0xff); + // std::cout << "Version: " << version << std::endl; +#endif + uint32_t offset = ((data[2] & 0xff) << 24) + ((data[3] & 0xff) << 16) + ((data[4] & 0xff) << 8) + (data[5] & 0xff); + + // std::cout << "Offset: " << offset << std::endl; + // Bytes 6-9 are reserved. + + uint16_t entries = ((data[offset] & 0xff) << 8) + (data[offset+1] & 0xff); + // std::cout << "Number of entries: " << entries << std::endl; + + for (int entry = 0; entry < entries; ++entry) { + uint32_t base = offset + 2 + entry * 12; + + uint16_t startGlyphID = ((data[base ] & 0xff) << 8) + (data[base+1] & 0xff); + uint16_t endGlyphID = ((data[base+2] & 0xff) << 8) + (data[base+3] & 0xff); + uint32_t offsetGlyph = ((data[base+4] & 0xff) << 24) + ((data[base+5] & 0xff) << 16) +((data[base+6] & 0xff) << 8) + (data[base+7] & 0xff); + uint32_t lengthGlyph = ((data[base+8] & 0xff) << 24) + ((data[base+9] & 0xff) << 16) +((data[base+10] & 0xff) << 8) + (data[base+11] & 0xff); + + // std::cout << "Entry " << entry << ": Start: " << startGlyphID << " End: " << endGlyphID + // << " Offset: " << offsetGlyph << " Length: " << lengthGlyph << std::endl; + + std::string svg; + + // static cast is needed as hb_blob_get_length returns char but we are comparing to a value greater than allowed by char. + if (lengthGlyph > 1 && data[offsetGlyph] == 0x1f && static_cast(data[offsetGlyph + 1]) == 0x8b) { + // Glyph is gzipped + + std::vector buffer; + for (unsigned int c = offsetGlyph; c < offsetGlyph + lengthGlyph; ++c) { + buffer.push_back(data[offset + c]); + } + + GzipFile zipped; + zipped.readBuffer(buffer); + + std::vector unzipped_data = zipped.getData(); + for (auto i : unzipped_data) { + svg += (char)i; + } + + } else { + // Glyph is not compressed + + for (unsigned int c = offsetGlyph; c < offsetGlyph + lengthGlyph; ++c) { + svg += (unsigned char) data[offset + c]; + } + } + + for (unsigned int i = startGlyphID; i < endGlyphID+1; ++i) { + glyphs[i].svg = svg; + } + + // for (auto glyph : glyphs) { + // std::cout << "Glyph: " << glyph.first << std::endl; + // std::cout << glyph.second.svg << std::endl; + // } + } +} + +#endif /* !USE_PANGO_WIND32 */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/OpenTypeUtil.h b/src/libnrtype/OpenTypeUtil.h new file mode 100644 index 0000000..e52526b --- /dev/null +++ b/src/libnrtype/OpenTypeUtil.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_OPENTYPEUTIL_H +#define SEEN_OPENTYPEUTIL_H + +#ifndef USE_PANGO_WIN32 + +#include + +#include +#include FT_FREETYPE_H + +#include + +/* + * A set of utilities to extract data from OpenType fonts. + * + * Isolates dependencies on FreeType, Harfbuzz, and Pango. + * All three provide variable amounts of access to data. + */ + +// OpenType substitution +class OTSubstitution { +public: + OTSubstitution() = default;; + Glib::ustring before; + Glib::ustring input; + Glib::ustring after; + Glib::ustring output; +}; + +// An OpenType fvar axis. +class OTVarAxis { +public: + OTVarAxis() + : minimum(0) + , maximum(1000) + , set_val(500) + , index(-1) {}; + + OTVarAxis(double _minimum, double _maximum, double _set_val, int _index) + : minimum(_minimum) + , maximum(_maximum) + , set_val(_set_val) + , index (_index) {}; + + double minimum; + double maximum; + double set_val; + int index; // Index in OpenType file (since we use a map). +}; + +// A particular instance of a variable font. +// A map indexed by axis name with value. +class OTVarInstance { + std::map axes; +}; + +inline double FTFixedToDouble (FT_Fixed value) { + return static_cast(value) / 65536.0; +} + +inline FT_Fixed FTDoubleToFixed (double value) { + return static_cast(value * 65536); +} + + +namespace Inkscape { class Pixbuf; } + +class SVGTableEntry { +public: + SVGTableEntry() : pixbuf(nullptr) {}; + std::string svg; + Inkscape::Pixbuf* pixbuf; +}; + +// This would be better if one had std::vector instead of OTSubstitution where each +// entry corresponded to one substitution (e.g. ff -> ff) but Harfbuzz at the moment cannot return +// individual substitutions. See Harfbuzz issue #673. +void readOpenTypeGsubTable (const FT_Face ft_face, + std::map& tables); + +void readOpenTypeFvarAxes (const FT_Face ft_face, + std::map& axes); + +void readOpenTypeFvarNamed (const FT_Face ft_face, + std::map& named); + +void readOpenTypeSVGTable (const FT_Face ft_face, + std::map& glyphs); + +#endif /* !USE_PANGO_WIND32 */ +#endif /* !SEEN_OPENTYPEUTIL_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-glyph.h b/src/libnrtype/font-glyph.h new file mode 100644 index 0000000..5dcb933 --- /dev/null +++ b/src/libnrtype/font-glyph.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_GLYPH_H +#define SEEN_LIBNRTYPE_FONT_GLYPH_H + +#include <2geom/forward.h> + +// the info for a glyph in a font. it's totally resolution- and fontsize-independent +struct font_glyph { + double h_advance, h_width; // width != advance because of kerning adjustements + double v_advance, v_width; + double bbox[4]; // bbox of the path (and the artbpath), not the bbox of the glyph + // as the fonts sometimes contain + Geom::PathVector* pathvector; // outline as 2geom pathvector, for text->curve stuff (should be unified with livarot) +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_GLYPH_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-instance.h b/src/libnrtype/font-instance.h new file mode 100644 index 0000000..102920c --- /dev/null +++ b/src/libnrtype/font-instance.h @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_INSTANCE_H +#define SEEN_LIBNRTYPE_FONT_INSTANCE_H + +#include + +#include +#include + +#include <2geom/d2.h> + +#include "FontFactory.h" +#include "font-style.h" +#include "OpenTypeUtil.h" + +namespace Inkscape { class Pixbuf; } + +class font_factory; +struct font_glyph; + +// the font_instance are the template of several raster_font; they provide metrics and outlines +// that are drawn by the raster_font, so the raster_font needs info relative to the way the +// font need to be drawn. note that fontsize is a scale factor in the transform matrix +// of the style +class font_instance { +public: + // the real source of the font + PangoFont* pFont = nullptr; + // depending on the rendering backend, different temporary data + + // that's the font's fingerprint; this particular PangoFontDescription gives the entry at which this font_instance + // resides in the font_factory loadedFaces unordered_map + PangoFontDescription* descr = nullptr; + // refcount + int refCount = 0; + // font_factory owning this font_instance + font_factory* parent = nullptr; + + // common glyph definitions for all the rasterfonts + std::map id_to_no; + int nbGlyph = 0; + int maxGlyph = 0; + + font_glyph* glyphs = nullptr; + + // font is loaded with GSUB in 2 pass + bool fulloaded = false; + + // Map of OpenType tables found in font. + std::map openTypeTables; + + // Maps for font variations. + std::map openTypeVarAxes; // Axes with ranges + + // Map of SVG in OpenType glyphs + std::map openTypeSVGGlyphs; + + // Does OpenType font contain SVG glyphs? + bool fontHasSVG = false; + + font_instance(); + virtual ~font_instance(); + + void Ref(); + void Unref(); + + bool IsOutlineFont(); // utility + void InstallFace(PangoFont* iFace); // utility; should reset the pFont field if loading failed + // in case the PangoFont is a bitmap font, for example. that way, the calling function + // will be able to check the validity of the font before installing it in loadedFaces + void InitTheFace(bool loadgsub = false); + + int MapUnicodeChar(gunichar c); // calls the relevant unicode->glyph index function + void LoadGlyph(int glyph_id); // the main backend-dependent function + // loads the given glyph's info + + // nota: all coordinates returned by these functions are on a [0..1] scale; you need to multiply + // by the fontsize to get the real sizes + + // Return 2geom pathvector for glyph. Deallocated when font instance dies. + Geom::PathVector* PathVector(int glyph_id); + + // Return font has SVG OpenType enties. + bool FontHasSVG() { return fontHasSVG; }; + + // Return pixbuf of SVG glyph or nullptr if no SVG glyph exists. + Inkscape::Pixbuf* PixBuf(int glyph_id); + + + // Horizontal advance if 'vertical' is false, vertical advance if true. + double Advance(int glyph_id, bool vertical); + + double GetTypoAscent() { return _ascent; } + double GetTypoDescent() { return _descent; } + double GetXHeight() { return _xheight; } + double GetMaxAscent() { return _ascent_max; } + double GetMaxDescent() { return _descent_max; } + const double* GetBaselines() { return _baselines; } + int GetDesignUnits() { return _design_units; } + + bool FontMetrics(double &ascent, double &descent, double &leading); + bool FontDecoration(double &underline_position, double &underline_thickness, + double &linethrough_position, double &linethrough_thickness); + bool FontSlope(double &run, double &rise); + // for generating slanted cursors for oblique fonts + Geom::OptRect BBox(int glyph_id); + +private: + void FreeTheFace(); + // Find ascent, descent, x-height, and baselines. + void FindFontMetrics(); + + // Temp: make public +public: +#ifdef USE_PANGO_WIN32 + HFONT theFace = nullptr; +#else + FT_Face theFace = nullptr; + // it's a pointer in fact; no worries to ref/unref it, pango does its magic + // as long as pFont is valid, theFace is too +#endif + +private: + // Font metrics in em-box units + double _ascent; // Typographic ascent. + double _descent; // Typographic descent. + double _xheight; // x-height of font. + double _ascent_max; // Maximum ascent of all glyphs in font. + double _descent_max; // Maximum descent of all glyphs in font. + int _design_units; // Design units, (units per em, typically 1000 or 2048). + + // Baselines + double _baselines[SP_CSS_BASELINE_SIZE]; +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_INSTANCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-lister.cpp b/src/libnrtype/font-lister.cpp new file mode 100644 index 0000000..dcd94d9 --- /dev/null +++ b/src/libnrtype/font-lister.cpp @@ -0,0 +1,1256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +#include + +#include "font-lister.h" +#include "FontFactory.h" + +#include "desktop.h" +#include "desktop-style.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-object.h" + +// Following are needed to limit the source of updating font data to text and containers. +#include "object/sp-root.h" +#include "object/sp-object-group.h" +#include "object/sp-anchor.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "object/sp-textpath.h" +#include "object/sp-tref.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" + +#include "xml/repr.h" + + +//#define DEBUG_FONT + +// CSS dictates that font family names are case insensitive. +// This should really implement full Unicode case unfolding. +bool familyNamesAreEqual(const Glib::ustring &a, const Glib::ustring &b) +{ + return (a.casefold().compare(b.casefold()) == 0); +} + +static const char* sp_font_family_get_name(PangoFontFamily* family) +{ + const char* name = pango_font_family_get_name(family); + if (strncmp(name, "Sans", 4) == 0 && strlen(name) == 4) + return "sans-serif"; + if (strncmp(name, "Serif", 5) == 0 && strlen(name) == 5) + return "serif"; + if (strncmp(name, "Monospace", 9) == 0 && strlen(name) == 9) + return "monospace"; + return name; +} + +namespace Inkscape { + +FontLister::FontLister() + : current_family_row (0) + , current_family ("sans-serif") + , current_style ("Normal") + , block (false) +{ + font_list_store = Gtk::ListStore::create(FontList); + font_list_store->freeze_notify(); + + /* Create default styles for use when font-family is unknown on system. */ + default_styles = g_list_append(nullptr, new StyleNames("Normal")); + default_styles = g_list_append(default_styles, new StyleNames("Italic")); + default_styles = g_list_append(default_styles, new StyleNames("Bold")); + default_styles = g_list_append(default_styles, new StyleNames("Bold Italic")); + + // Get sorted font families from Pango + std::vector familyVector; + font_factory::Default()->GetUIFamilies(familyVector); + + // Traverse through the family names and set up the list store + for (auto & i : familyVector) { + const char* displayName = sp_font_family_get_name(i); + + if (displayName == nullptr || *displayName == '\0') { + continue; + } + + Glib::ustring familyName = displayName; + if (!familyName.empty()) { + Gtk::TreeModel::iterator treeModelIter = font_list_store->append(); + (*treeModelIter)[FontList.family] = familyName; + + // we don't set this now (too slow) but the style will be cached if the user + // ever decides to use this font + (*treeModelIter)[FontList.styles] = NULL; + // store the pango representation for generating the style + (*treeModelIter)[FontList.pango_family] = i; + (*treeModelIter)[FontList.onSystem] = true; + } + } + + font_list_store->thaw_notify(); + + style_list_store = Gtk::ListStore::create(FontStyleList); + + // Initialize style store with defaults + style_list_store->freeze_notify(); + style_list_store->clear(); + for (GList *l = default_styles; l; l = l->next) { + Gtk::TreeModel::iterator treeModelIter = style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)l->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)l->data)->DisplayName; + } + style_list_store->thaw_notify(); +} + +FontLister::~FontLister() +{ + // Delete default_styles + for (GList *l = default_styles; l; l = l->next) { + delete ((StyleNames *)l->data); + } + + // Delete other styles + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter; + GList *styles = row[FontList.styles]; + for (GList *l = styles; l; l = l->next) { + delete ((StyleNames *)l->data); + } + ++iter; + } +} + +FontLister *FontLister::get_instance() +{ + static Inkscape::FontLister *instance = new Inkscape::FontLister(); + return instance; +} + +// To do: remove model (not needed for C++ version). +// Ensures the style list for a particular family has been created. +void FontLister::ensureRowStyles(Glib::RefPtr model, Gtk::TreeModel::iterator const iter) +{ + Gtk::TreeModel::Row row = *iter; + if (!row[FontList.styles]) { + if (row[FontList.pango_family]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } else { + row[FontList.styles] = default_styles; + } + } +} + +Glib::ustring FontLister::get_font_family_markup(Gtk::TreeIter const &iter) +{ + Gtk::TreeModel::Row row = *iter; + + Glib::ustring family = row[FontList.family]; + bool onSystem = row[FontList.onSystem]; + + Glib::ustring family_escaped = Glib::Markup::escape_text( family ); + Glib::ustring markup; + + if (!onSystem) { + markup = ""; + + // See if font-family is on system (separately for each family in font stack). + std::vector tokens = Glib::Regex::split_simple("\\s*,\\s*", family); + + for (auto token: tokens) { + bool found = false; + Gtk::TreeModel::Children children = get_font_list()->children(); + for (auto iter2: children) { + Gtk::TreeModel::Row row2 = *iter2; + Glib::ustring family2 = row2[FontList.family]; + bool onSystem2 = row2[FontList.onSystem]; + if (onSystem2 && familyNamesAreEqual(token, family2)) { + found = true; + break; + } + } + + if (found) { + markup += Glib::Markup::escape_text (token); + markup += ", "; + } else { + markup += ""; + markup += Glib::Markup::escape_text (token); + markup += ""; + markup += ", "; + } + } + + // Remove extra comma and space from end. + if (markup.size() >= 2) { + markup.resize(markup.size() - 2); + } + markup += ""; + + } else { + markup = family_escaped; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_sample = prefs->getInt("/tools/text/show_sample_in_list", 1); + if (show_sample) { + + Glib::ustring sample = prefs->getString("/tools/text/font_sample"); + + markup += " "; + markup += sample; + markup += ""; + } + + // std::cout << "Markup: " << markup << std::endl; + return markup; +} + +// Example of how to use "foreach_iter" +// bool +// FontLister::print_document_font( const Gtk::TreeModel::iterator &iter ) { +// Gtk::TreeModel::Row row = *iter; +// if( !row[FontList.onSystem] ) { +// std::cout << " Not on system: " << row[FontList.family] << std::endl; +// return false; +// } +// return true; +// } +// font_list_store->foreach_iter( sigc::mem_fun(*this, &FontLister::print_document_font )); + +/* Used to insert a font that was not in the document and not on the system into the font list. */ +void FontLister::insert_font_family(Glib::ustring new_family) +{ + GList *styles = default_styles; + + /* In case this is a fallback list, check if first font-family on system. */ + std::vector tokens = Glib::Regex::split_simple(",", new_family); + if (!tokens.empty() && !tokens[0].empty()) { + Gtk::TreeModel::iterator iter2 = font_list_store->get_iter("0"); + while (iter2 != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter2; + if (row[FontList.onSystem] && familyNamesAreEqual(tokens[0], row[FontList.family])) { + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + styles = row[FontList.styles]; + break; + } + ++iter2; + } + } + + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = new_family; + (*treeModelIter)[FontList.styles] = styles; + (*treeModelIter)[FontList.onSystem] = false; + (*treeModelIter)[FontList.pango_family] = NULL; + + current_family = new_family; + current_family_row = 0; + current_style = "Normal"; + + emit_update(); +} + +void FontLister::update_font_list(SPDocument *document) +{ + SPObject *root = document->getRoot(); + if (!root) { + return; + } + + font_list_store->freeze_notify(); + + /* Find if current row is in document or system part of list */ + gboolean row_is_system = false; + if (current_family_row > -1) { + Gtk::TreePath path; + path.push_back(current_family_row); + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + row_is_system = (*iter)[FontList.onSystem]; + // std::cout << " In: row: " << current_family_row << " " << (*iter)[FontList.family] << std::endl; + } + } + + /* Clear all old document font-family entries */ + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter; + if (!row[FontList.onSystem]) { + // std::cout << " Not on system: " << row[FontList.family] << std::endl; + iter = font_list_store->erase(iter); + } else { + // std::cout << " First on system: " << row[FontList.family] << std::endl; + break; + } + } + + /* Get "font-family"s and styles used in document. */ + std::map> font_data; + update_font_data_recursive(*root, font_data); + + /* Insert separator */ + if (!font_data.empty()) { + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = "#"; + (*treeModelIter)[FontList.onSystem] = false; + } + + /* Insert font-family's in document. */ + for (auto i: font_data) { + + GList *styles = default_styles; + + /* See if font-family (or first in fallback list) is on system. If so, get styles. */ + std::vector tokens = Glib::Regex::split_simple(",", i.first); + if (!tokens.empty() && !tokens[0].empty()) { + + Gtk::TreeModel::iterator iter2 = font_list_store->get_iter("0"); + while (iter2 != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter2; + if (row[FontList.onSystem] && familyNamesAreEqual(tokens[0], row[FontList.family])) { + // Found font on system, set style list to system font style list. + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + + // Add new styles (from 'font-variation-settings', these are not include in GetUIStyles()). + for (auto j: i.second) { + // std::cout << " Inserting: " << j << std::endl; + + bool exists = false; + for(GList *temp = row[FontList.styles]; temp; temp = temp->next) { + if( ((StyleNames*)temp->data)->CssName.compare( j ) == 0 ) { + exists = true; + break; + } + } + + if (!exists) { + row[FontList.styles] = g_list_append(row[FontList.styles], new StyleNames(j,j)); + } + } + + styles = row[FontList.styles]; + break; + } + ++iter2; + } + } + + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = reinterpret_cast(g_strdup((i.first).c_str())); + (*treeModelIter)[FontList.styles] = styles; + (*treeModelIter)[FontList.onSystem] = false; // false if document font + (*treeModelIter)[FontList.pango_family] = NULL; // CHECK ME (set to pango_family if on system?) + + } + + /* Now we do a song and dance to find the correct row as the row corresponding + * to the current_family may have changed. We can't simply search for the + * family name in the list since it can occur twice, once in the document + * font family part and once in the system font family part. Above we determined + * which part it is in. + */ + if (current_family_row > -1) { + int start = 0; + if (row_is_system) + start = font_data.size(); + int length = font_list_store->children().size(); + for (int i = 0; i < length; ++i) { + int row = i + start; + if (row >= length) + row -= length; + Gtk::TreePath path; + path.push_back(row); + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + if (familyNamesAreEqual(current_family, (*iter)[FontList.family])) { + current_family_row = row; + break; + } + } + } + } + // std::cout << " Out: row: " << current_family_row << " " << current_family << std::endl; + + font_list_store->thaw_notify(); + emit_update(); +} + +void FontLister::update_font_data_recursive(SPObject& r, std::map> &font_data) +{ + // Text nodes (i.e. the content of or ) do not have their own style. + if (r.getRepr()->type() == Inkscape::XML::TEXT_NODE) { + return; + } + + PangoFontDescription* descr = ink_font_description_from_style( r.style ); + const gchar* font_family_char = pango_font_description_get_family(descr); + if (font_family_char) { + Glib::ustring font_family(font_family_char); + pango_font_description_unset_fields( descr, PANGO_FONT_MASK_FAMILY); + + gchar* font_style_char = pango_font_description_to_string(descr); + Glib::ustring font_style(font_style_char); + g_free(font_style_char); + + if (!font_family.empty() && !font_style.empty()) { + font_data[font_family].insert(font_style); + } + } else { + // We're starting from root and looking at all elements... we should probably white list text/containers. + std::cerr << "FontLister::update_font_data_recursive: descr without font family! " << (r.getId()?r.getId():"null") << std::endl; + } + pango_font_description_free(descr); + + if (SP_IS_GROUP(&r) || + SP_IS_ANCHOR(&r) || + SP_IS_ROOT(&r) || + SP_IS_TEXT(&r) || + SP_IS_TSPAN(&r) || + SP_IS_TEXTPATH(&r) || + SP_IS_TREF(&r) || + SP_IS_FLOWTEXT(&r) || + SP_IS_FLOWDIV(&r) || + SP_IS_FLOWPARA(&r) || + SP_IS_FLOWLINE(&r)) { + for (auto& child: r.children) { + update_font_data_recursive(child, font_data); + } + } +} + +void FontLister::emit_update() +{ + if (block) return; + + block = true; + update_signal.emit (); + block = false; +} + + +Glib::ustring FontLister::canonize_fontspec(Glib::ustring fontspec) +{ + + // Pass fontspec to and back from Pango to get a the fontspec in + // canonical form. -inkscape-font-specification relies on the + // Pango constructed fontspec not changing form. If it does, + // this is the place to fix it. + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + gchar *canonized = pango_font_description_to_string(descr); + Glib::ustring Canonized = canonized; + g_free(canonized); + pango_font_description_free(descr); + + // Pango canonized strings remove space after comma between family names. Put it back. + // But don't add a space inside a 'font-variation-settings' declaration (this breaks Pango). + size_t i = 0; + while ((i = Canonized.find_first_of(",@", i)) != std::string::npos ) { + if (Canonized[i] == '@') // Found start of 'font-variation-settings'. + break; + Canonized.replace(i, 1, ", "); + i += 2; + } + + return Canonized; +} + +Glib::ustring FontLister::system_fontspec(Glib::ustring fontspec) +{ + // Find what Pango thinks is the closest match. + Glib::ustring out = fontspec; + + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + font_instance *res = (font_factory::Default())->Face(descr); + if (res && res->pFont) { + PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont); + out = sp_font_description_get_family(nFaceDesc); + } + pango_font_description_free(descr); + + return out; +} + +std::pair FontLister::ui_from_fontspec(Glib::ustring fontspec) +{ + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + const gchar *family = pango_font_description_get_family(descr); + if (!family) + family = "sans-serif"; + Glib::ustring Family = family; + + // PANGO BUG... + // A font spec of Delicious, 500 Italic should result in a family of 'Delicious' + // and a style of 'Medium Italic'. It results instead with: a family of + // 'Delicious, 500' with a style of 'Medium Italic'. We chop of any weight numbers + // at the end of the family: match ",[1-9]00^". + Glib::RefPtr weight = Glib::Regex::create(",[1-9]00$"); + Family = weight->replace(Family, 0, "", Glib::REGEX_MATCH_PARTIAL); + + // Pango canonized strings remove space after comma between family names. Put it back. + size_t i = 0; + while ((i = Family.find(",", i)) != std::string::npos) { + Family.replace(i, 1, ", "); + i += 2; + } + + pango_font_description_unset_fields(descr, PANGO_FONT_MASK_FAMILY); + gchar *style = pango_font_description_to_string(descr); + Glib::ustring Style = style; + pango_font_description_free(descr); + g_free(style); + + return std::make_pair(Family, Style); +} + +std::pair FontLister::selection_update() +{ +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::selection_update: entrance" << std::endl; +#endif + // Get fontspec from a selection, preferences, or thin air. + Glib::ustring fontspec; + SPStyle query(SP_ACTIVE_DOCUMENT); + + // Directly from stored font specification. + int result = + sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONT_SPECIFICATION); + + //std::cout << " Attempting selected style" << std::endl; + if (result != QUERY_STYLE_NOTHING && query.font_specification.set) { + fontspec = query.font_specification.value(); + //std::cout << " fontspec from query :" << fontspec << ":" << std::endl; + } + + // From style + if (fontspec.empty()) { + //std::cout << " Attempting desktop style" << std::endl; + int rfamily = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTFAMILY); + int rstyle = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + + // Must have text in selection + if (rfamily != QUERY_STYLE_NOTHING && rstyle != QUERY_STYLE_NOTHING) { + fontspec = fontspec_from_style(&query); + } + //std::cout << " fontspec from style :" << fontspec << ":" << std::endl; + } + + // From preferences + if (fontspec.empty()) { + //std::cout << " Attempting preferences" << std::endl; + query.readFromPrefs("/tools/text"); + fontspec = fontspec_from_style(&query); + //std::cout << " fontspec from prefs :" << fontspec << ":" << std::endl; + } + + // From thin air + if (fontspec.empty()) { + //std::cout << " Attempting thin air" << std::endl; + fontspec = current_family + ", " + current_style; + //std::cout << " fontspec from thin air :" << fontspec << ":" << std::endl; + } + + std::pair ui = ui_from_fontspec(fontspec); + set_font_family(ui.first); + set_font_style(ui.second); + +#ifdef DEBUG_FONT + std::cout << " family_row: :" << current_family_row << ":" << std::endl; + std::cout << " family: :" << current_family << ":" << std::endl; + std::cout << " style: :" << current_style << ":" << std::endl; + std::cout << "FontLister::selection_update: exit" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + + emit_update(); + + return std::make_pair(current_family, current_style); +} + + +// Set fontspec. If check is false, best style match will not be done. +void FontLister::set_fontspec(Glib::ustring new_fontspec, bool /*check*/) +{ + std::pair ui = ui_from_fontspec(new_fontspec); + Glib::ustring new_family = ui.first; + Glib::ustring new_style = ui.second; + +#ifdef DEBUG_FONT + std::cout << "FontLister::set_fontspec: family: " << new_family + << " style:" << new_style << std::endl; +#endif + + set_font_family(new_family, false, false); + set_font_style(new_style, false); + + emit_update(); +} + + +// TODO: use to determine font-selector best style +// TODO: create new function new_font_family(Gtk::TreeModel::iterator iter) +std::pair FontLister::new_font_family(Glib::ustring new_family, bool /*check_style*/) +{ +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::new_font_family: " << new_family << std::endl; +#endif + + // No need to do anything if new family is same as old family. + if (familyNamesAreEqual(new_family, current_family)) { +#ifdef DEBUG_FONT + std::cout << "FontLister::new_font_family: exit: no change in family." << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return std::make_pair(current_family, current_style); + } + + // We need to do two things: + // 1. Update style list for new family. + // 2. Select best valid style match to old style. + + // For finding style list, use list of first family in font-family list. + GList *styles = nullptr; + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(new_family, row[FontList.family])) { + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + styles = row[FontList.styles]; + break; + } + ++iter; + } + + // Newly typed in font-family may not yet be in list... use default list. + // TODO: if font-family is list, check if first family in list is on system + // and set style accordingly. + if (styles == nullptr) { + styles = default_styles; + } + + // Update style list. + style_list_store->freeze_notify(); + style_list_store->clear(); + + for (GList *l = styles; l; l = l->next) { + Gtk::TreeModel::iterator treeModelIter = style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)l->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)l->data)->DisplayName; + } + + style_list_store->thaw_notify(); + + // Find best match to the style from the old font-family to the + // styles available with the new font. + // TODO: Maybe check if an exact match exists before using Pango. + Glib::ustring best_style = get_best_style_match(new_family, current_style); + +#ifdef DEBUG_FONT + std::cout << "FontLister::new_font_family: exit: " << new_family << " " << best_style << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return std::make_pair(new_family, best_style); +} + +std::pair FontLister::set_font_family(Glib::ustring new_family, bool check_style, + bool emit) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::set_font_family: " << new_family << std::endl; +#endif + + std::pair ui = new_font_family(new_family, check_style); + current_family = ui.first; + current_style = ui.second; + +#ifdef DEBUG_FONT + std::cout << " family_row: :" << current_family_row << ":" << std::endl; + std::cout << " family: :" << current_family << ":" << std::endl; + std::cout << " style: :" << current_style << ":" << std::endl; + std::cout << "FontLister::set_font_family: end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + if (emit) { + emit_update(); + } + return ui; +} + + +std::pair FontLister::set_font_family(int row, bool check_style, bool emit) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::set_font_family( row ): " << row << std::endl; +#endif + + current_family_row = row; + Gtk::TreePath path; + path.push_back(row); + Glib::ustring new_family = current_family; + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + new_family = (*iter)[FontList.family]; + } + + std::pair ui = set_font_family(new_family, check_style, emit); + +#ifdef DEBUG_FONT + std::cout << "FontLister::set_font_family( row ): end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return ui; +} + + +void FontLister::set_font_style(Glib::ustring new_style, bool emit) +{ + +// TODO: Validate input using Pango. If Pango doesn't recognize a style it will +// attach the "invalid" style to the font-family. + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister:set_font_style: " << new_style << std::endl; +#endif + current_style = new_style; + +#ifdef DEBUG_FONT + std::cout << " family: " << current_family << std::endl; + std::cout << " style: " << current_style << std::endl; + std::cout << "FontLister::set_font_style: end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + if (emit) { + emit_update(); + } +} + + +// We do this ourselves as we can't rely on FontFactory. +void FontLister::fill_css(SPCSSAttr *css, Glib::ustring fontspec) +{ + if (fontspec.empty()) { + fontspec = get_fontspec(); + } + + std::pair ui = ui_from_fontspec(fontspec); + + Glib::ustring family = ui.first; + + + // Font spec is single quoted... for the moment + Glib::ustring fontspec_quoted(fontspec); + css_quote(fontspec_quoted); + sp_repr_css_set_property(css, "-inkscape-font-specification", fontspec_quoted.c_str()); + + // Font families needs to be properly quoted in CSS (used unquoted in font-lister) + css_font_family_quote(family); + sp_repr_css_set_property(css, "font-family", family.c_str()); + + PangoFontDescription *desc = pango_font_description_from_string(fontspec.c_str()); + PangoWeight weight = pango_font_description_get_weight(desc); + switch (weight) { + case PANGO_WEIGHT_THIN: + sp_repr_css_set_property(css, "font-weight", "100"); + break; + case PANGO_WEIGHT_ULTRALIGHT: + sp_repr_css_set_property(css, "font-weight", "200"); + break; + case PANGO_WEIGHT_LIGHT: + sp_repr_css_set_property(css, "font-weight", "300"); + break; +#if PANGO_VERSION_CHECK(1,36,6) + case PANGO_WEIGHT_SEMILIGHT: + sp_repr_css_set_property(css, "font-weight", "350"); + break; +#endif + case PANGO_WEIGHT_BOOK: + sp_repr_css_set_property(css, "font-weight", "380"); + break; + case PANGO_WEIGHT_NORMAL: + sp_repr_css_set_property(css, "font-weight", "normal"); + break; + case PANGO_WEIGHT_MEDIUM: + sp_repr_css_set_property(css, "font-weight", "500"); + break; + case PANGO_WEIGHT_SEMIBOLD: + sp_repr_css_set_property(css, "font-weight", "600"); + break; + case PANGO_WEIGHT_BOLD: + sp_repr_css_set_property(css, "font-weight", "bold"); + break; + case PANGO_WEIGHT_ULTRABOLD: + sp_repr_css_set_property(css, "font-weight", "800"); + break; + case PANGO_WEIGHT_HEAVY: + sp_repr_css_set_property(css, "font-weight", "900"); + break; + case PANGO_WEIGHT_ULTRAHEAVY: + sp_repr_css_set_property(css, "font-weight", "1000"); + break; + } + + PangoStyle style = pango_font_description_get_style(desc); + switch (style) { + case PANGO_STYLE_NORMAL: + sp_repr_css_set_property(css, "font-style", "normal"); + break; + case PANGO_STYLE_OBLIQUE: + sp_repr_css_set_property(css, "font-style", "oblique"); + break; + case PANGO_STYLE_ITALIC: + sp_repr_css_set_property(css, "font-style", "italic"); + break; + } + + PangoStretch stretch = pango_font_description_get_stretch(desc); + switch (stretch) { + case PANGO_STRETCH_ULTRA_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "ultra-condensed"); + break; + case PANGO_STRETCH_EXTRA_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "extra-condensed"); + break; + case PANGO_STRETCH_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "condensed"); + break; + case PANGO_STRETCH_SEMI_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "semi-condensed"); + break; + case PANGO_STRETCH_NORMAL: + sp_repr_css_set_property(css, "font-stretch", "normal"); + break; + case PANGO_STRETCH_SEMI_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "semi-expanded"); + break; + case PANGO_STRETCH_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "expanded"); + break; + case PANGO_STRETCH_EXTRA_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "extra-expanded"); + break; + case PANGO_STRETCH_ULTRA_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "ultra-expanded"); + break; + } + + PangoVariant variant = pango_font_description_get_variant(desc); + switch (variant) { + case PANGO_VARIANT_NORMAL: + sp_repr_css_set_property(css, "font-variant", "normal"); + break; + case PANGO_VARIANT_SMALL_CAPS: + sp_repr_css_set_property(css, "font-variant", "small-caps"); + break; + } + +#if PANGO_VERSION_CHECK(1,41,1) + // Convert Pango variations string to CSS format + const char* str = pango_font_description_get_variations(desc); + + std::string variations; + + if (str) { + + std::vector tokens = Glib::Regex::split_simple(",", str); + + Glib::RefPtr regex = Glib::Regex::create("(\\w{4})=([-+]?\\d*\\.?\\d+([eE][-+]?\\d+)?)"); + Glib::MatchInfo matchInfo; + for (auto token: tokens) { + regex->match(token, matchInfo); + if (matchInfo.matches()) { + variations += "'"; + variations += matchInfo.fetch(1); + variations += "' "; + variations += matchInfo.fetch(2); + variations += ", "; + } + } + if (variations.length() >= 2) { // Remove last comma/space + variations.pop_back(); + variations.pop_back(); + } + } + + if (!variations.empty()) { + sp_repr_css_set_property(css, "font-variation-settings", variations.c_str()); + } else { + sp_repr_css_unset_property(css, "font-variation-settings" ); + } +#endif + pango_font_description_free(desc); +} + + +Glib::ustring FontLister::fontspec_from_style(SPStyle *style) +{ + + PangoFontDescription* descr = ink_font_description_from_style( style ); + Glib::ustring fontspec = pango_font_description_to_string( descr ); + pango_font_description_free(descr); + + //std::cout << "FontLister:fontspec_from_style: " << fontspec << std::endl; + + return fontspec; +} + + +Gtk::TreeModel::Row FontLister::get_row_for_font(Glib::ustring family) +{ + + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(family, row[FontList.family])) { + return row; + } + + ++iter; + } + + throw FAMILY_NOT_FOUND; +} + +Gtk::TreePath FontLister::get_path_for_font(Glib::ustring family) +{ + return font_list_store->get_path(get_row_for_font(family)); +} + +bool FontLister::is_path_for_font(Gtk::TreePath path, Glib::ustring family) +{ + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + return familyNamesAreEqual(family, (*iter)[FontList.family]); + } + + return false; +} + +Gtk::TreeModel::Row FontLister::get_row_for_style(Glib::ustring style) +{ + + Gtk::TreeModel::iterator iter = style_list_store->get_iter("0"); + while (iter != style_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(style, row[FontStyleList.cssStyle])) { + return row; + } + + ++iter; + } + + throw STYLE_NOT_FOUND; +} + +static gint compute_distance(const PangoFontDescription *a, const PangoFontDescription *b) +{ + + // Weight: multiples of 100 + gint distance = abs(pango_font_description_get_weight(a) - + pango_font_description_get_weight(b)); + + distance += 10000 * abs(pango_font_description_get_stretch(a) - + pango_font_description_get_stretch(b)); + + PangoStyle style_a = pango_font_description_get_style(a); + PangoStyle style_b = pango_font_description_get_style(b); + if (style_a != style_b) { + if ((style_a == PANGO_STYLE_OBLIQUE && style_b == PANGO_STYLE_ITALIC) || + (style_b == PANGO_STYLE_OBLIQUE && style_a == PANGO_STYLE_ITALIC)) { + distance += 1000; // Oblique and italic are almost the same + } else { + distance += 100000; // Normal vs oblique/italic, not so similar + } + } + + // Normal vs small-caps + distance += 1000000 * abs(pango_font_description_get_variant(a) - + pango_font_description_get_variant(b)); + return distance; +} + +// This is inspired by pango_font_description_better_match, but that routine +// always returns false if variant or stretch are different. This means, for +// example, that PT Sans Narrow with style Bold Condensed is never matched +// to another font-family with Bold style. +gboolean font_description_better_match(PangoFontDescription *target, PangoFontDescription *old_desc, PangoFontDescription *new_desc) +{ + if (old_desc == nullptr) + return true; + if (new_desc == nullptr) + return false; + + int old_distance = compute_distance(target, old_desc); + int new_distance = compute_distance(target, new_desc); + //std::cout << "font_description_better_match: old: " << old_distance << std::endl; + //std::cout << " new: " << new_distance << std::endl; + + return (new_distance < old_distance); +} + +// void +// font_description_dump( PangoFontDescription* target ) { +// std::cout << " Font: " << pango_font_description_to_string( target ) << std::endl; +// std::cout << " style: " << pango_font_description_get_style( target ) << std::endl; +// std::cout << " weight: " << pango_font_description_get_weight( target ) << std::endl; +// std::cout << " variant: " << pango_font_description_get_variant( target ) << std::endl; +// std::cout << " stretch: " << pango_font_description_get_stretch( target ) << std::endl; +// std::cout << " gravity: " << pango_font_description_get_gravity( target ) << std::endl; +// } + +/* Returns style string */ +// TODO: Remove or turn into function to be used by new_font_family. +Glib::ustring FontLister::get_best_style_match(Glib::ustring family, Glib::ustring target_style) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::get_best_style_match: " << family << " : " << target_style << std::endl; +#endif + + Glib::ustring fontspec = family + ", " + target_style; + + Gtk::TreeModel::Row row; + try + { + row = get_row_for_font(family); + } + catch (...) + { + std::cerr << "FontLister::get_best_style_match(): can't find family: " << family << std::endl; + return (target_style); + } + + PangoFontDescription *target = pango_font_description_from_string(fontspec.c_str()); + PangoFontDescription *best = nullptr; + + //font_description_dump( target ); + + GList *styles = default_styles; + if (row[FontList.onSystem] && !row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + styles = row[FontList.styles]; + } + + for (GList *l = styles; l; l = l->next) { + Glib::ustring fontspec = family + ", " + ((StyleNames *)l->data)->CssName; + PangoFontDescription *candidate = pango_font_description_from_string(fontspec.c_str()); + //font_description_dump( candidate ); + //std::cout << " " << font_description_better_match( target, best, candidate ) << std::endl; + if (font_description_better_match(target, best, candidate)) { + pango_font_description_free(best); + best = candidate; + //std::cout << " ... better: " << std::endl; + } else { + pango_font_description_free(candidate); + //std::cout << " ... not better: " << std::endl; + } + } + + Glib::ustring best_style = target_style; + if (best) { + pango_font_description_unset_fields(best, PANGO_FONT_MASK_FAMILY); + best_style = pango_font_description_to_string(best); + } + + if (target) + pango_font_description_free(target); + if (best) + pango_font_description_free(best); + + +#ifdef DEBUG_FONT + std::cout << " Returning: " << best_style << std::endl; + std::cout << "FontLister::get_best_style_match: exit" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return best_style; +} + +const Glib::RefPtr FontLister::get_font_list() const +{ + return font_list_store; +} + +const Glib::RefPtr FontLister::get_style_list() const +{ + return style_list_store; +} + +} // namespace Inkscape + +// Helper functions + +// Separator function (if true, a separator will be drawn) +bool font_lister_separator_func(const Glib::RefPtr& model, + const Gtk::TreeModel::iterator& iter) { + + // Of what use is 'model', can we avoid using font_lister? + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreeModel::Row row = *iter; + Glib::ustring entry = row[font_lister->FontList.family]; + return entry == "#"; +} + +// Needed until Text toolbar updated +gboolean font_lister_separator_func2(GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) +{ + gchar *text = nullptr; + gtk_tree_model_get(model, iter, 0, &text, -1); // Column 0: FontList.family + bool result = (text && strcmp(text, "#") == 0); + g_free(text); + return result; +} + +// Draw system fonts in dark blue, missing fonts with red strikeout. +// Used by both FontSelector and Text toolbar. +void font_lister_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Glib::ustring markup = font_lister->get_font_family_markup(iter); + renderer->set_property("markup", markup); +} + +// Needed until Text toolbar updated +void font_lister_cell_data_func2(GtkCellLayout * /*cell_layout*/, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer /*data*/) +{ + gchar *family; + gboolean onSystem = false; + gtk_tree_model_get(model, iter, 0, &family, 2, &onSystem, -1); + gchar* family_escaped = g_markup_escape_text(family, -1); + Glib::ustring markup; + + if (!onSystem) { + markup = ""; + + /* See if font-family on system */ + std::vector tokens = Glib::Regex::split_simple("\\s*,\\s*", family); + for (auto token : tokens) { + + GtkTreeIter iter; + gboolean valid; + gboolean onSystem = true; + gboolean found = false; + for (valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter); + valid; + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter)) { + + gchar *token_family = nullptr; + gtk_tree_model_get(model, &iter, 0, &token_family, 2, &onSystem, -1); + if (onSystem && familyNamesAreEqual(token, token_family)) { + found = true; + g_free(token_family); + break; + } + g_free(token_family); + } + if (found) { + markup += g_markup_escape_text(token.c_str(), -1); + markup += ", "; + } else { + markup += ""; + markup += g_markup_escape_text(token.c_str(), -1); + markup += ""; + markup += ", "; + } + } + // Remove extra comma and space from end. + if (markup.size() >= 2) { + markup.resize(markup.size() - 2); + } + markup += ""; + // std::cout << markup << std::endl; + } else { + markup = family_escaped; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_sample = prefs->getInt("/tools/text/show_sample_in_list", 1); + if (show_sample) { + + Glib::ustring sample = prefs->getString("/tools/text/font_sample"); + gchar* sample_escaped = g_markup_escape_text(sample.data(), -1); + + markup += " "; + markup += sample_escaped; + markup += ""; + g_free(sample_escaped); + } + + g_object_set(G_OBJECT(cell), "markup", markup.c_str(), NULL); + g_free(family); + g_free(family_escaped); +} + +// Draw Face name with face style. +void font_lister_style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreeModel::Row row = *iter; + + Glib::ustring family = font_lister->get_font_family(); + Glib::ustring style = row[font_lister->FontStyleList.cssStyle]; + + Glib::ustring style_escaped = Glib::Markup::escape_text( style ); + Glib::ustring font_desc = family + ", " + style; + Glib::ustring markup; + + markup = "" + style_escaped + ""; + std::cout << " markup: " << markup << std::endl; + + renderer->set_property("markup", markup); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/font-lister.h b/src/libnrtype/font-lister.h new file mode 100644 index 0000000..769f021 --- /dev/null +++ b/src/libnrtype/font-lister.h @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef FONT_LISTER_H +#define FONT_LISTER_H + +/* + * Font selection widgets + * + * Authors: + * Chris Lahey + * Lauris Kaplinski + * Tavmjong Bah + * + * Copyright (C) 1999-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include +#include // For strescape() + +#include +#include +#include + +class SPObject; +class SPDocument; +class SPCSSAttr; +class SPStyle; + +namespace Gtk { +class CellRenderer; +} + +namespace Inkscape { + +/** + * This class enumerates fonts using libnrtype into reusable data stores and + * allows for random access to the font-family list and the font-style list. + * Setting the font-family updates the font-style list. "Style" in this case + * refers to everything but family and size (e.g. italic/oblique, weight). + * + * This class handles font-family lists and fonts that are not on the system, + * where there is not an entry in the fontInstanceMap. + * + * This class uses the idea of "font_spec". This is a plain text string as used by + * Pango. It is similar to the CSS font shorthand except that font-family comes + * first and in this class the font-size is not used. + * + * This class uses the FontFactory class to get a list of system fonts + * and to find best matches via Pango. The Pango interface is only setup + * to deal with fonts that are on the system so care must be taken. For + * example, best matches should only be done with the first font-family + * in a font-family list. If the first font-family is not on the system + * then a generic font-family should be used (sans-serif -> Sans). + * + * This class is used by the UI interface (text-toolbar, font-select, etc.). + * Those items can change the selected font family and style here. When that + * happens. this class emits a signal for those items to update their displayed + * values. + * + * This class is a singleton (one instance per Inkscape session). Since fonts + * used in a document are added to the list, there really should be one + * instance per document. + * + * "Font" includes family and style. It should not be used when one + * means font-family. + */ + +class FontLister { +public: + enum Exceptions { + FAMILY_NOT_FOUND, + STYLE_NOT_FOUND + }; + + virtual ~FontLister(); + + /** + * GtkTreeModelColumnRecord for the font-family list Gtk::ListStore + */ + struct FontListClass : public Gtk::TreeModelColumnRecord { + /** + * Column containing the family name + */ + Gtk::TreeModelColumn family; + + /** + * Column containing the styles for each family name. + */ + Gtk::TreeModelColumn styles; + + /** + * Column containing flag if font is on system + */ + Gtk::TreeModelColumn onSystem; + + /** + * Not actually a column. + * Necessary for quick initialization of FontLister, + * we initially store the pango family and if the + * font style is actually used we'll cache it in + * %styles. + */ + Gtk::TreeModelColumn pango_family; + + FontListClass() + { + add(family); + add(styles); + add(onSystem); + add(pango_family); + } + }; + + FontListClass FontList; + + struct FontStyleListClass : public Gtk::TreeModelColumnRecord { + /** + * Column containing the styles as Font designer used. + */ + Gtk::TreeModelColumn displayStyle; + + /** + * Column containing the styles in CSS/Pango format. + */ + Gtk::TreeModelColumn cssStyle; + + FontStyleListClass() + { + add(cssStyle); + add(displayStyle); + } + }; + + FontStyleListClass FontStyleList; + + /** + * @return the ListStore with the family names + * + * The return is const and the function is declared as const. + * The ListStore is ready to be used after class instantiation + * and should not be modified. + */ + const Glib::RefPtr get_font_list() const; + + /** + * @return the ListStore with the styles + */ + const Glib::RefPtr get_style_list() const; + + /** + * Inserts a font family or font-fallback list (for use when not + * already in document or on system). + */ + void insert_font_family(Glib::ustring new_family); + + /** + * Updates font list to include fonts in document. + */ + void update_font_list(SPDocument *document); + +public: + static Inkscape::FontLister *get_instance(); + + /** + * Takes a hand written font spec and returns a Pango generated one in + * standard form. + */ + Glib::ustring canonize_fontspec(Glib::ustring fontspec); + + /** + * Find closest system font to given font. + */ + Glib::ustring system_fontspec(Glib::ustring fontspec); + + /** + * Gets font-family and style from fontspec. + * font-family and style returned. + */ + std::pair ui_from_fontspec(Glib::ustring fontspec); + + /** + * Sets font-family and style after a selection change. + * New font-family and style returned. + */ + std::pair selection_update(); + + /** + * Sets current_fontspec, etc. If check is false, won't + * try to find best style match (assumes style in fontspec + * valid for given font-family). + */ + void set_fontspec(Glib::ustring fontspec, bool check = true); + + Glib::ustring get_fontspec() { return (canonize_fontspec(current_family + ", " + current_style)); } + + /** + * Changes font-family, updating style list and attempting to find + * closest style to current_style style (if check_style is true). + * New font-family and style returned. + * Does NOT update current_family and current_style. + * (For potential use in font-selector which doesn't update until + * "Apply" button clicked.) + */ + std::pair new_font_family(Glib::ustring family, bool check_style = true); + + /** + * Sets font-family, updating style list and attempting + * to find closest style to old current_style. + * New font-family and style returned. + * Updates current_family and current_style. + * Calls new_font_family(). + * (For use in text-toolbar where update is immediate.) + */ + std::pair set_font_family(Glib::ustring family, bool check_style = true, + bool emit = true); + + /** + * Sets font-family from row in list store. + * The row can be used to determine if we are in the + * document or system part of the font-family list. + * This is needed to handle scrolling through the + * font-family list correctly. + * Calls set_font_family(). + */ + std::pair set_font_family(int row, bool check_style = true, bool emit = true); + + Glib::ustring get_font_family() + { + return current_family; + } + + int get_font_family_row() + { + return current_family_row; + } + + /** + * Sets style. Does not validate style for family. + */ + void set_font_style(Glib::ustring style, bool emit = true); + + Glib::ustring get_font_style() + { + return current_style; + } + + Glib::ustring fontspec_from_style(SPStyle *style); + + /** + * Fill css using given fontspec (doesn't need to be member function). + */ + void fill_css(SPCSSAttr *css, Glib::ustring fontspec = ""); + + Gtk::TreeModel::Row get_row_for_font() { return get_row_for_font (current_family); } + + Gtk::TreeModel::Row get_row_for_font(Glib::ustring family); + + Gtk::TreePath get_path_for_font(Glib::ustring family); + + bool is_path_for_font(Gtk::TreePath path, Glib::ustring family); + + Gtk::TreeModel::Row get_row_for_style() { return get_row_for_style (current_style); } + + Gtk::TreeModel::Row get_row_for_style(Glib::ustring style); + + Gtk::TreePath get_path_for_style(Glib::ustring style); + + std::pair get_paths(Glib::ustring family, Glib::ustring style); + + /** + * Return best style match for new font given style for old font. + */ + Glib::ustring get_best_style_match(Glib::ustring family, Glib::ustring style); + + /** + * Ensures the style list for a particular family has been created. + */ + void ensureRowStyles(Glib::RefPtr model, Gtk::TreeModel::iterator const iter); + + /** + * Get markup for font-family. + */ + Glib::ustring get_font_family_markup(Gtk::TreeIter const &iter); + + /** + * Let users of FontLister know to update GUI. + * This is to allow synchronization of changes across multiple widgets. + * Handlers should block signals. + * Input is fontspec to set. + */ + sigc::connection connectUpdate(sigc::slot slot) { + return update_signal.connect(slot); + } + + bool blocked() { return block; } + +private: + FontLister(); + + void update_font_data_recursive(SPObject& r, std::map> &font_data); + + Glib::RefPtr font_list_store; + Glib::RefPtr style_list_store; + + /** + * Info for currently selected font (what is shown in the UI). + * May include font-family lists and fonts not on system. + */ + int current_family_row; + Glib::ustring current_family; + Glib::ustring current_style; + + /** + * If a font-family is not on system, this list of styles is used. + */ + GList *default_styles; + + bool block; + void emit_update(); + sigc::signal update_signal; +}; + +} // namespace Inkscape + +// Helper functions +bool font_lister_separator_func (const Glib::RefPtr& model, + const Gtk::TreeModel::iterator& iter); + +gboolean font_lister_separator_func2(GtkTreeModel *model, + GtkTreeIter *iter, + gpointer /*data*/); + +void font_lister_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + +void font_lister_cell_data_func2(GtkCellLayout * /*cell_layout*/, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer /*data*/); + +void font_lister_style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/font-style.h b/src/libnrtype/font-style.h new file mode 100644 index 0000000..23a26d6 --- /dev/null +++ b/src/libnrtype/font-style.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_STYLE_H +#define SEEN_LIBNRTYPE_FONT_STYLE_H + +#include <2geom/affine.h> +#include + +// structure that holds data describing how to render glyphs of a font + +// Different raster styles. +struct font_style { + Geom::Affine transform; // the ctm. contains the font-size + bool vertical; // should be rendered vertically or not? + // good font support would take the glyph alternates for vertical mode, when present + double stroke_width; // if 0, the glyph is filled; otherwise stroked + JoinType stroke_join; + ButtType stroke_cap; + float stroke_miter_limit; + int nbDash; + double dash_offset; + double* dashes; +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_STYLE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/line-geometry.cpp b/src/line-geometry.cpp new file mode 100644 index 0000000..1951bd5 --- /dev/null +++ b/src/line-geometry.cpp @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Routines for dealing with lines (intersections, etc.) + * + * Authors: + * Maximilian Albert + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "line-geometry.h" +#include "desktop.h" +#include "desktop-style.h" + +#include "display/sp-canvas.h" +#include "display/sp-ctrlline.h" +#include "display/sodipodi-ctrl.h" +#include "ui/control-manager.h" + +using Inkscape::ControlManager; + +namespace Box3D { + +/** + * Draw a line beginning at 'start'. If is_endpoint is true, use 'vec' as the endpoint + * of the segment. Otherwise interpret it as the direction of the line. + * FIXME: Think of a better way to distinguish between the two constructors of lines. + */ +Line::Line(Geom::Point const &start, Geom::Point const &vec, bool is_endpoint): + pt(start) +{ + if (is_endpoint) + v_dir = vec - start; + else + v_dir = vec; + normal = v_dir.ccw(); + d0 = Geom::dot(normal, pt); +} + +Line::Line(Line const &line) += default; + +Line &Line::operator=(Line const &line) = default; + +boost::optional Line::intersect(Line const &line) { + Geom::Coord denom = Geom::dot(v_dir, line.normal); + boost::optional no_point; + if (fabs(denom) < 1e-6) + return no_point; + + Geom::Coord lambda = (line.d0 - Geom::dot(pt, line.normal)) / denom; + return pt + lambda * v_dir; +} + +void Line::set_direction(Geom::Point const &dir) +{ + v_dir = dir; + normal = v_dir.ccw(); + d0 = Geom::dot(normal, pt); +} + +Geom::Point Line::closest_to(Geom::Point const &pt) +{ + /* return the intersection of this line with a perpendicular line passing through pt */ + boost::optional result = this->intersect(Line(pt, (this->v_dir).ccw(), false)); + g_return_val_if_fail (result, Geom::Point (0.0, 0.0)); + return *result; +} + +double Line::lambda (Geom::Point const pt) +{ + double sign = (Geom::dot (pt - this->pt, this->v_dir) > 0) ? 1.0 : -1.0; + double lambda = sign * Geom::L2 (pt - this->pt); + // FIXME: It may speed things up (but how much?) if we assume that + // pt lies on the line and thus skip the following test + Geom::Point test = point_from_lambda (lambda); + if (!pts_coincide (pt, test)) { + g_warning ("Point does not lie on line.\n"); + return 0; + } + return lambda; +} + +/* The coordinates of w with respect to the basis {v1, v2} */ +std::pair coordinates (Geom::Point const &v1, Geom::Point const &v2, Geom::Point const &w) +{ + double det = determinant (v1, v2);; + if (fabs (det) < epsilon) { + // vectors are not linearly independent; we indicate this in the return value(s) + return std::make_pair (HUGE_VAL, HUGE_VAL); + } + + double lambda1 = determinant (w, v2) / det; + double lambda2 = determinant (v1, w) / det; + return std::make_pair (lambda1, lambda2); +} + +/* whether w lies inside the sector spanned by v1 and v2 */ +bool lies_in_sector (Geom::Point const &v1, Geom::Point const &v2, Geom::Point const &w) +{ + std::pair coords = coordinates (v1, v2, w); + if (coords.first == HUGE_VAL) { + // catch the case that the vectors are not linearly independent + // FIXME: Can we assume that it's safe to return true if the vectors point in different directions? + return (Geom::dot (v1, v2) < 0); + } + return (coords.first >= 0 && coords.second >= 0); +} + +bool lies_in_quadrangle (Geom::Point const &A, Geom::Point const &B, Geom::Point const &C, Geom::Point const &D, Geom::Point const &pt) +{ + return (lies_in_sector (D - A, B - A, pt - A) && lies_in_sector (D - C, B - C, pt - C)); +} + +static double pos_angle (Geom::Point v, Geom::Point w) +{ + return fabs (Geom::atan2 (v) - Geom::atan2 (w)); +} + +/* + * Returns the two corners of the quadrangle A, B, C, D spanning the edge that is hit by a semiline + * starting at pt and going into direction dir. + * If none of the sides is hit, it returns a pair containing two identical points. + */ +std::pair +side_of_intersection (Geom::Point const &A, Geom::Point const &B, Geom::Point const &C, Geom::Point const &D, + Geom::Point const &pt, Geom::Point const &dir) +{ + Geom::Point dir_A (A - pt); + Geom::Point dir_B (B - pt); + Geom::Point dir_C (C - pt); + Geom::Point dir_D (D - pt); + + std::pair result; + double angle = -1; + double tmp_angle; + + if (lies_in_sector (dir_A, dir_B, dir)) { + result = std::make_pair (A, B); + angle = pos_angle (dir_A, dir_B); + } + if (lies_in_sector (dir_B, dir_C, dir)) { + tmp_angle = pos_angle (dir_B, dir_C); + if (tmp_angle > angle) { + angle = tmp_angle; + result = std::make_pair (B, C); + } + } + if (lies_in_sector (dir_C, dir_D, dir)) { + tmp_angle = pos_angle (dir_C, dir_D); + if (tmp_angle > angle) { + angle = tmp_angle; + result = std::make_pair (C, D); + } + } + if (lies_in_sector (dir_D, dir_A, dir)) { + tmp_angle = pos_angle (dir_D, dir_A); + if (tmp_angle > angle) { + angle = tmp_angle; + result = std::make_pair (D, A); + } + } + if (angle == -1) { + // no intersection found; return a pair containing two identical points + return std::make_pair (A, A); + } else { + return result; + } +} + +boost::optional Line::intersection_with_viewbox (SPDesktop *desktop) +{ + Geom::Rect vb = desktop->get_display_area(); + /* remaining viewbox corners */ + Geom::Point ul (vb.min()[Geom::X], vb.max()[Geom::Y]); + Geom::Point lr (vb.max()[Geom::X], vb.min()[Geom::Y]); + + std::pair e = side_of_intersection (vb.min(), lr, vb.max(), ul, this->pt, this->v_dir); + if (e.first == e.second) { + // perspective line lies outside the canvas + return boost::optional(); + } + + Line line (e.first, e.second); + return this->intersect (line); +} + +void create_canvas_point(Geom::Point const &pos, unsigned int size, guint32 rgba) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPCanvasItem * canvas_pt = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRL, + "size", size, + "filled", 1, + "fill_color", rgba, + "stroked", 1, + "stroke_color", 0x000000ff, + NULL); + SP_CTRL(canvas_pt)->moveto(pos); +} + +void create_canvas_line(Geom::Point const &p1, Geom::Point const &p2, guint32 rgba) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPCtrlLine *line = ControlManager::getManager().createControlLine(desktop->getControls(), p1, p2); + line->setRgba32(rgba); + sp_canvas_item_show(line); +} + +} // namespace Box3D + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/line-geometry.h b/src/line-geometry.h new file mode 100644 index 0000000..65e5c24 --- /dev/null +++ b/src/line-geometry.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Routines for dealing with lines (intersections, etc.) + * + * Authors: + * Maximilian Albert + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LINE_GEOMETRY_H +#define SEEN_LINE_GEOMETRY_H + +#include <2geom/point.h> +#include + +#include "axis-manip.h" // FIXME: This is only for Box3D::epsilon; move that to a better location +#include "object/persp3d.h" + +class SPDesktop; +typedef unsigned int guint32; + +namespace Box3D { + +class Line { +public: + Line(Geom::Point const &start, Geom::Point const &vec, bool is_endpoint = true); + Line(Line const &line); + virtual ~Line() = default; + Line &operator=(Line const &line); + virtual boost::optional intersect(Line const &line); + inline Geom::Point direction () { return v_dir; } + + Geom::Point closest_to(Geom::Point const &pt); // returns the point on the line closest to pt + + friend inline std::ostream &operator<< (std::ostream &out_file, const Line &in_line); + boost::optional intersection_with_viewbox (SPDesktop *desktop); + inline bool lie_on_same_side (Geom::Point const &A, Geom::Point const &B) { + /* If A is a point in the plane and n is the normal vector of the line then + the sign of dot(A, n) specifies the half-plane in which A lies. + Thus A and B lie on the same side if the dot products have equal sign. */ + return ((Geom::dot(A, normal) - d0) * (Geom::dot(B, normal) - d0)) > 0; + } + + double lambda (Geom::Point const pt); + inline Geom::Point point_from_lambda (double const lambda) { + return (pt + lambda * Geom::unit_vector (v_dir)); } + +protected: + void set_direction(Geom::Point const &dir); + inline static bool pts_coincide (Geom::Point const pt1, Geom::Point const pt2) + { + return (Geom::L2 (pt2 - pt1) < epsilon); + } + + Geom::Point pt; + Geom::Point v_dir; + Geom::Point normal; + Geom::Coord d0; +}; + +inline double determinant (Geom::Point const &a, Geom::Point const &b) +{ + return (a[Geom::X] * b[Geom::Y] - a[Geom::Y] * b[Geom::X]); +} +std::pair coordinates (Geom::Point const &v1, Geom::Point const &v2, Geom::Point const &w); +bool lies_in_sector (Geom::Point const &v1, Geom::Point const &v2, Geom::Point const &w); +bool lies_in_quadrangle (Geom::Point const &A, Geom::Point const &B, Geom::Point const &C, Geom::Point const &D, Geom::Point const &pt); +std::pair side_of_intersection (Geom::Point const &A, Geom::Point const &B, + Geom::Point const &C, Geom::Point const &D, + Geom::Point const &pt, Geom::Point const &dir); + +/*** For debugging purposes: Draw a knot/node of specified size and color at the given position ***/ +void create_canvas_point(Geom::Point const &pos, unsigned int size = 4, guint32 rgba = 0xff00007f); + +/*** For debugging purposes: Draw a line between the specified points ***/ +void create_canvas_line(Geom::Point const &p1, Geom::Point const &p2, guint32 rgba = 0xff00007f); + + +/** A function to print out the Line. It just prints out the coordinates of start point and + direction on the given output stream */ +inline std::ostream &operator<< (std::ostream &out_file, const Line &in_line) { + out_file << "Start: " << in_line.pt << " Direction: " << in_line.v_dir; + return out_file; +} + +} // namespace Box3D + + +#endif /* !SEEN_LINE_GEOMETRY_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/line-snapper.cpp b/src/line-snapper.cpp new file mode 100644 index 0000000..3a06542 --- /dev/null +++ b/src/line-snapper.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LineSnapper class. + * + * Authors: + * Diederik van Lierop + * And others... + * + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/line.h> + +#include "line-snapper.h" +#include "snap.h" + +Inkscape::LineSnapper::LineSnapper(SnapManager *sm, Geom::Coord const d) : Snapper(sm, d) +{ +} + +void Inkscape::LineSnapper::freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &/*bbox_to_snap*/, + std::vector const */*it*/, + std::vector */*unselected_nodes*/) const +{ + if (!(_snap_enabled && _snapmanager->snapprefs.isSourceSnappable(p.getSourceType())) ) { + return; + } + + /* Get the lines that we will try to snap to */ + const LineList lines = _getSnapLines(p.getPoint()); + + for (const auto & line : lines) { + Geom::Point const p1 = line.second; // point at guide/grid line + Geom::Point const p2 = p1 + Geom::rot90(line.first); // 2nd point at guide/grid line + assert(line.first != Geom::Point(0,0)); // we cannot project on an linesegment of zero length + + Geom::Point const p_proj = Geom::projection(p.getPoint(), Geom::Line(p1, p2)); + Geom::Coord const dist = Geom::L2(p_proj - p.getPoint()); + //Store any line that's within snapping range + if (dist < getSnapperTolerance()) { + _addSnappedLine(isr, p_proj, dist, p.getSourceType(), p.getSourceNum(), line.first, line.second); + // For any line that's within range, we will also look at it's "point on line" p1. For guides + // this point coincides with its origin; for grids this is of no use, but we cannot + // discern between grids and guides here + Geom::Coord const dist_p1 = Geom::L2(p1 - p.getPoint()); + if (dist_p1 < getSnapperTolerance()) { + _addSnappedLinesOrigin(isr, p1, dist_p1, p.getSourceType(), p.getSourceNum(), false); + // Only relevant for guides; grids don't have an origin per line + // Therefore _addSnappedLinesOrigin() will only be implemented for guides + } + + // Here we will try to snap either tangentially or perpendicularly to a grid/guide line + // For this we need to know where the origin is located of the line that is currently being rotated, + std::vector > const origins_and_vectors = p.getOriginsAndVectors(); + // Now we will iterate over all the origins and vectors and see which of these will get use a tangential or perpendicular snap + for (const auto & origins_and_vector : origins_and_vectors) { + if (origins_and_vector.second) { // if "second" is true then "first" is a vector, otherwise it's a point + // When snapping a line with a constant vector (constant direction) to a guide or grid line, + // then either all points will be perpendicular/tangential or none at all. This is not very useful + continue; + } + + //Geom::Point origin_doc = _snapmanager->getDesktop()->dt2doc((*it_origin_or_vector).first); // "first" contains a Geom::Point, denoting either a point + Geom::Point origin = origins_and_vector.first; // "first" contains a Geom::Point, denoting either a point + + // We won't try to snap tangentially; a line being tangential to another line can be achieved by snapping both its endpoints + // individually to the other line. There's no need to have an explicit tangential snap here, that would be redundant + + if (_snapmanager->snapprefs.getSnapPerp()) { // Find the point that leads to a perpendicular snap + Geom::Point const origin_proj = Geom::projection(origin, Geom::Line(p1, p2)); + Geom::Coord dist = Geom::L2(origin_proj - p.getPoint()); + if (dist < getSnapperTolerance()) { + _addSnappedLinePerpendicularly(isr, origin_proj, dist, p.getSourceType(), p.getSourceNum(), false); + } + } + } + } + } +} + +void Inkscape::LineSnapper::constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &/*bbox_to_snap*/, + SnapConstraint const &c, + std::vector const */*it*/, + std::vector */*unselected_nodes*/) const + +{ + if (_snap_enabled == false || _snapmanager->snapprefs.isSourceSnappable(p.getSourceType()) == false) { + return; + } + + // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping + Geom::Point pp = c.projection(p.getPoint()); + + /* Get the lines that we will try to snap to */ + const LineList lines = _getSnapLines(pp); + + for (const auto & line : lines) { + Geom::Point const point_on_line = c.hasPoint() ? c.getPoint() : pp; + Geom::Line gridguide_line(line.second, line.second + Geom::rot90(line.first)); + + if (c.isCircular()) { + // Find the intersections between the line and the circular constraint + // First, project the origin of the circle onto the line + Geom::Point const origin = c.getPoint(); + Geom::Point const p_proj = Geom::projection(origin, gridguide_line); + Geom::Coord dist = Geom::L2(p_proj - origin); // distance from circle origin to constraint line + Geom::Coord radius = c.getRadius(); + if (dist == radius) { + // Only one point of intersection; + _addSnappedPoint(isr, p_proj, Geom::L2(pp - p_proj), p.getSourceType(), p.getSourceNum(), true); + } else if (dist < radius) { + // Two points of intersection, symmetrical with respect to the projected point + // Calculate half the length of the linesegment between the two points of intersection + Geom::Coord l = sqrt(radius*radius - dist*dist); + Geom::Coord d = Geom::L2(gridguide_line.versor()); // length of versor, needed to normalize the versor + if (d > 0) { + Geom::Point v = l*gridguide_line.versor()/d; + _addSnappedPoint(isr, p_proj + v, Geom::L2(p.getPoint() - (p_proj + v)), p.getSourceType(), p.getSourceNum(), true); + _addSnappedPoint(isr, p_proj - v, Geom::L2(p.getPoint() - (p_proj - v)), p.getSourceType(), p.getSourceNum(), true); + } + } + } else { + // Find the intersections between the line and the linear constraint + Geom::Line constraint_line(point_on_line, point_on_line + c.getDirection()); + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(constraint_line, gridguide_line); + } + catch (Geom::InfiniteSolutions &e) + { + // We're probably dealing with parallel lines, so snapping doesn't make any sense here + continue; // jump to the next iterator in the for-loop + } + + if (inters) { + Geom::Point t = constraint_line.pointAt((*inters).ta); + const Geom::Coord dist = Geom::L2(t - p.getPoint()); + if (dist < getSnapperTolerance()) { + // When doing a constrained snap, we're already at an intersection. + // This snappoint is therefore fully constrained, so there's no need + // to look for additional intersections; just return the snapped point + // and forget about the line + _addSnappedPoint(isr, t, dist, p.getSourceType(), p.getSourceNum(), true); + } + } + } + } +} + +// Will only be overridden in the guide-snapper class, because grid lines don't have an origin; the +// grid-snapper classes will use this default empty method +void Inkscape::LineSnapper::_addSnappedLinesOrigin(IntermSnapResults &/*isr*/, Geom::Point const &/*origin*/, Geom::Coord const &/*snapped_distance*/, SnapSourceType const &/*source_type*/, long /*source_num*/, bool /*constrained_snap*/) const +{ +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/line-snapper.h b/src/line-snapper.h new file mode 100644 index 0000000..ea98124 --- /dev/null +++ b/src/line-snapper.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LINE_SNAPPER_H +#define SEEN_LINE_SNAPPER_H +/* + * Authors: + * Carl Hetherington + * Diederik van Lierop + * + * Copyright (C) 1999-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snapper.h" + +namespace Inkscape { + +class SnapCandidatePoint; + +/** + * Superclass for snappers to horizontal and vertical lines. + */ +class LineSnapper : public Snapper +{ +public: + LineSnapper(SnapManager *sm, Geom::Coord const d); + + void freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const override; + + void constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const override; + +protected: + typedef std::list > LineList; + //first point is a vector normal to the line + //second point is a point on the line + +private: + /** + * \param p Point that we are trying to snap. + * \return List of lines that we should try snapping to. + */ + virtual LineList _getSnapLines(Geom::Point const &p) const = 0; + + virtual void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const = 0; + + // Will only be implemented for guide lines, because grid lines don't have an origin + virtual void _addSnappedLinesOrigin(IntermSnapResults &isr, Geom::Point const &origin, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const; + + virtual void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const = 0; + virtual void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const = 0; +}; + +} + +#endif /* !SEEN_LINE_SNAPPER_H */ diff --git a/src/livarot/AVL.cpp b/src/livarot/AVL.cpp new file mode 100644 index 0000000..9bf6eb4 --- /dev/null +++ b/src/livarot/AVL.cpp @@ -0,0 +1,969 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "AVL.h" + +/* + * the algorithm explanation for this code comes from purists.org, which seems to have disappeared since + * it's a classic AVL tree rebalancing, nothing fancy + */ + +AVLTree::AVLTree() +{ + MakeNew(); +} + +AVLTree::~AVLTree() +{ + MakeDelete(); +} + +void AVLTree::MakeNew() +{ + for (int i = 0; i < 2; i++) + { + elem[i] = nullptr; + child[i] = nullptr; + } + + parent = nullptr; + balance = 0; +} + +void AVLTree::MakeDelete() +{ + for (int i = 0; i < 2; i++) { + if (elem[i]) { + elem[i]->elem[1 - i] = elem[1 - i]; + } + elem[i] = nullptr; + } +} + +AVLTree *AVLTree::Leftmost() +{ + return leafFromParent(nullptr, LEFT); +} + +AVLTree *AVLTree::leaf(AVLTree *from, Side s) +{ + if (from == child[1 - s]) { + if (child[s]) { + return child[s]->leafFromParent(this, s); + } + else if (parent) { + return parent->leaf(this, s); + } + } + else if (from == child[s]) { + if (parent) { + return parent->leaf(this, s); + } + } + + return nullptr; +} + +AVLTree *AVLTree::leafFromParent(AVLTree */*from*/, Side s) +{ + if (child[s]) { + return child[s]->leafFromParent(this, s); + } + + return this; +} + +int +AVLTree::RestoreBalances (AVLTree * from, AVLTree * &racine) +{ + if (from == nullptr) + { + if (parent) + return parent->RestoreBalances (this, racine); + } + else + { + if (balance == 0) + { + if (from == child[LEFT]) + balance = 1; + if (from == child[RIGHT]) + balance = -1; + if (parent) + return parent->RestoreBalances (this, racine); + return avl_no_err; + } + else if (balance > 0) + { + if (from == child[RIGHT]) + { + balance = 0; + return avl_no_err; + } + if (child[LEFT] == nullptr) + { +// cout << "mierda\n"; + return avl_bal_err; + } + AVLTree *a = this; + AVLTree *b = child[LEFT]; + AVLTree *e = child[RIGHT]; + AVLTree *c = child[LEFT]->child[LEFT]; + AVLTree *d = child[LEFT]->child[RIGHT]; + if (child[LEFT]->balance > 0) + { + AVLTree *r = parent; + + a->parent = b; + b->child[RIGHT] = a; + a->child[RIGHT] = e; + if (e) + e->parent = a; + a->child[LEFT] = d; + if (d) + d->parent = a; + b->child[LEFT] = c; + if (c) + c->parent = b; + b->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = b; + if (r->child[RIGHT] == a) + r->child[RIGHT] = b; + } + if (racine == a) + racine = b; + + a->balance = 0; + b->balance = 0; + return avl_no_err; + } + else + { + if (child[LEFT]->child[RIGHT] == nullptr) + { + // cout << "mierda\n"; + return avl_bal_err; + } + AVLTree *f = child[LEFT]->child[RIGHT]->child[LEFT]; + AVLTree *g = child[LEFT]->child[RIGHT]->child[RIGHT]; + AVLTree *r = parent; + + a->parent = d; + d->child[RIGHT] = a; + b->parent = d; + d->child[LEFT] = b; + a->child[LEFT] = g; + if (g) + g->parent = a; + a->child[RIGHT] = e; + if (e) + e->parent = a; + b->child[LEFT] = c; + if (c) + c->parent = b; + b->child[RIGHT] = f; + if (f) + f->parent = b; + + d->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = d; + if (r->child[RIGHT] == a) + r->child[RIGHT] = d; + } + if (racine == a) + racine = d; + + int old_bal = d->balance; + d->balance = 0; + if (old_bal == 0) + { + a->balance = 0; + b->balance = 0; + } + else if (old_bal > 0) + { + a->balance = -1; + b->balance = 0; + } + else if (old_bal < 0) + { + a->balance = 0; + b->balance = 1; + } + return avl_no_err; + } + } + else if (balance < 0) + { + if (from == child[LEFT]) + { + balance = 0; + return avl_no_err; + } + if (child[RIGHT] == nullptr) + { +// cout << "mierda\n"; + return avl_bal_err; + } + AVLTree *a = this; + AVLTree *b = child[RIGHT]; + AVLTree *e = child[LEFT]; + AVLTree *c = child[RIGHT]->child[RIGHT]; + AVLTree *d = child[RIGHT]->child[LEFT]; + AVLTree *r = parent; + if (child[RIGHT]->balance < 0) + { + + a->parent = b; + b->child[LEFT] = a; + a->child[LEFT] = e; + if (e) + e->parent = a; + a->child[RIGHT] = d; + if (d) + d->parent = a; + b->child[RIGHT] = c; + if (c) + c->parent = b; + b->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = b; + if (r->child[RIGHT] == a) + r->child[RIGHT] = b; + } + if (racine == a) + racine = b; + a->balance = 0; + b->balance = 0; + return avl_no_err; + } + else + { + if (child[RIGHT]->child[LEFT] == nullptr) + { +// cout << "mierda\n"; + return avl_bal_err; + } + AVLTree *f = child[RIGHT]->child[LEFT]->child[RIGHT]; + AVLTree *g = child[RIGHT]->child[LEFT]->child[LEFT]; + + a->parent = d; + d->child[LEFT] = a; + b->parent = d; + d->child[RIGHT] = b; + a->child[RIGHT] = g; + if (g) + g->parent = a; + a->child[LEFT] = e; + if (e) + e->parent = a; + b->child[RIGHT] = c; + if (c) + c->parent = b; + b->child[LEFT] = f; + if (f) + f->parent = b; + + d->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = d; + if (r->child[RIGHT] == a) + r->child[RIGHT] = d; + } + if (racine == a) + racine = d; + int old_bal = d->balance; + d->balance = 0; + if (old_bal == 0) + { + a->balance = 0; + b->balance = 0; + } + else if (old_bal > 0) + { + a->balance = 0; + b->balance = -1; + } + else if (old_bal < 0) + { + a->balance = 1; + b->balance = 0; + } + return avl_no_err; + } + } + } + return avl_no_err; +} + +int +AVLTree::RestoreBalances (int diff, AVLTree * &racine) +{ + if (balance > 0) + { + if (diff < 0) + { + balance = 0; + if (parent) + { + if (this == parent->child[RIGHT]) + return parent->RestoreBalances (1, racine); + if (this == parent->child[LEFT]) + return parent->RestoreBalances (-1, racine); + } + return avl_no_err; + } + else if (diff == 0) + { + } + else if (diff > 0) + { + if (child[LEFT] == nullptr) + { +// cout << "un probleme\n"; + return avl_bal_err; + } + AVLTree *r = parent; + AVLTree *a = this; + AVLTree *b = child[RIGHT]; + AVLTree *e = child[LEFT]; + AVLTree *f = e->child[RIGHT]; + AVLTree *g = e->child[LEFT]; + if (e->balance > 0) + { + e->child[RIGHT] = a; + e->child[LEFT] = g; + a->child[RIGHT] = b; + a->child[LEFT] = f; + if (a) + a->parent = e; + if (g) + g->parent = e; + if (b) + b->parent = a; + if (f) + f->parent = a; + e->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = e; + if (r->child[RIGHT] == a) + r->child[RIGHT] = e; + } + if (racine == this) + racine = e; + e->balance = 0; + a->balance = 0; + if (r) + { + if (e == r->child[RIGHT]) + return r->RestoreBalances (1, racine); + if (e == r->child[LEFT]) + return r->RestoreBalances (-1, racine); + } + return avl_no_err; + } + else if (e->balance == 0) + { + e->child[RIGHT] = a; + e->child[LEFT] = g; + a->child[RIGHT] = b; + a->child[LEFT] = f; + if (a) + a->parent = e; + if (g) + g->parent = e; + if (b) + b->parent = a; + if (f) + f->parent = a; + e->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = e; + if (r->child[RIGHT] == a) + r->child[RIGHT] = e; + } + if (racine == this) + racine = e; + e->balance = -1; + a->balance = 1; + return avl_no_err; + } + else if (e->balance < 0) + { + if (child[LEFT]->child[RIGHT] == nullptr) + { +// cout << "un probleme\n"; + return avl_bal_err; + } + AVLTree *i = child[LEFT]->child[RIGHT]->child[RIGHT]; + AVLTree *j = child[LEFT]->child[RIGHT]->child[LEFT]; + + f->child[RIGHT] = a; + f->child[LEFT] = e; + a->child[RIGHT] = b; + a->child[LEFT] = i; + e->child[RIGHT] = j; + e->child[LEFT] = g; + if (b) + b->parent = a; + if (i) + i->parent = a; + if (g) + g->parent = e; + if (j) + j->parent = e; + if (a) + a->parent = f; + if (e) + e->parent = f; + f->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = f; + if (r->child[RIGHT] == a) + r->child[RIGHT] = f; + } + if (racine == this) + racine = f; + int oBal = f->balance; + f->balance = 0; + if (oBal > 0) + { + a->balance = -1; + e->balance = 0; + } + else if (oBal == 0) + { + a->balance = 0; + e->balance = 0; + } + else if (oBal < 0) + { + a->balance = 0; + e->balance = 1; + } + if (r) + { + if (f == r->child[RIGHT]) + return r->RestoreBalances (1, racine); + if (f == r->child[LEFT]) + return r->RestoreBalances (-1, racine); + } + return avl_no_err; + } + } + } + else if (balance == 0) + { + if (diff < 0) + { + balance = -1; + } + else if (diff == 0) + { + } + else if (diff > 0) + { + balance = 1; + } + return avl_no_err; + } + else if (balance < 0) + { + if (diff < 0) + { + if (child[RIGHT] == nullptr) + { +// cout << "un probleme\n"; + return avl_bal_err; + } + AVLTree *r = parent; + AVLTree *a = this; + AVLTree *b = child[LEFT]; + AVLTree *e = child[RIGHT]; + AVLTree *f = e->child[LEFT]; + AVLTree *g = e->child[RIGHT]; + if (e->balance < 0) + { + e->child[LEFT] = a; + e->child[RIGHT] = g; + a->child[LEFT] = b; + a->child[RIGHT] = f; + if (a) + a->parent = e; + if (g) + g->parent = e; + if (b) + b->parent = a; + if (f) + f->parent = a; + e->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = e; + if (r->child[RIGHT] == a) + r->child[RIGHT] = e; + } + if (racine == this) + racine = e; + e->balance = 0; + a->balance = 0; + if (r) + { + if (e == r->child[RIGHT]) + return r->RestoreBalances (1, racine); + if (e == r->child[LEFT]) + return r->RestoreBalances (-1, racine); + } + return avl_no_err; + } + else if (e->balance == 0) + { + e->child[LEFT] = a; + e->child[RIGHT] = g; + a->child[LEFT] = b; + a->child[RIGHT] = f; + if (a) + a->parent = e; + if (g) + g->parent = e; + if (b) + b->parent = a; + if (f) + f->parent = a; + e->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = e; + if (r->child[RIGHT] == a) + r->child[RIGHT] = e; + } + if (racine == this) + racine = e; + e->balance = 1; + a->balance = -1; + return avl_no_err; + } + else if (e->balance > 0) + { + if (child[RIGHT]->child[LEFT] == nullptr) + { +// cout << "un probleme\n"; + return avl_bal_err; + } + AVLTree *i = child[RIGHT]->child[LEFT]->child[LEFT]; + AVLTree *j = child[RIGHT]->child[LEFT]->child[RIGHT]; + + f->child[LEFT] = a; + f->child[RIGHT] = e; + a->child[LEFT] = b; + a->child[RIGHT] = i; + e->child[LEFT] = j; + e->child[RIGHT] = g; + if (b) + b->parent = a; + if (i) + i->parent = a; + if (g) + g->parent = e; + if (j) + j->parent = e; + if (a) + a->parent = f; + if (e) + e->parent = f; + f->parent = r; + if (r) + { + if (r->child[LEFT] == a) + r->child[LEFT] = f; + if (r->child[RIGHT] == a) + r->child[RIGHT] = f; + } + if (racine == this) + racine = f; + int oBal = f->balance; + f->balance = 0; + if (oBal > 0) + { + a->balance = 0; + e->balance = -1; + } + else if (oBal == 0) + { + a->balance = 0; + e->balance = 0; + } + else if (oBal < 0) + { + a->balance = 1; + e->balance = 0; + } + if (r) + { + if (f == r->child[RIGHT]) + return r->RestoreBalances (1, racine); + if (f == r->child[LEFT]) + return r->RestoreBalances (-1, racine); + } + return avl_no_err; + } + } + else if (diff == 0) + { + } + else if (diff > 0) + { + balance = 0; + if (parent) + { + if (this == parent->child[RIGHT]) + return parent->RestoreBalances (1, racine); + if (this == parent->child[LEFT]) + return parent->RestoreBalances (-1, racine); + } + return avl_no_err; + } + } + return avl_no_err; +} + +/* + * removal + */ +int +AVLTree::Remove (AVLTree * &racine, bool rebalance) +{ + AVLTree *startNode = nullptr; + int remDiff = 0; + int res = Remove (racine, startNode, remDiff); + if (res == avl_no_err && rebalance && startNode) + res = startNode->RestoreBalances (remDiff, racine); + return res; +} + +int +AVLTree::Remove (AVLTree * &racine, AVLTree * &startNode, int &diff) +{ + if (elem[LEFT]) + elem[LEFT]->elem[RIGHT] = elem[RIGHT]; + if (elem[RIGHT]) + elem[RIGHT]->elem[LEFT] = elem[LEFT]; + elem[LEFT] = elem[RIGHT] = nullptr; + + if (child[LEFT] && child[RIGHT]) + { + AVLTree *newMe = child[LEFT]->leafFromParent(this, RIGHT); + if (newMe == nullptr || newMe->child[RIGHT]) + { +// cout << "pas normal\n"; + return avl_rm_err; + } + if (newMe == child[LEFT]) + { + startNode = newMe; + diff = -1; + newMe->child[RIGHT] = child[RIGHT]; + child[RIGHT]->parent = newMe; + newMe->parent = parent; + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = newMe; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = newMe; + } + } + else + { + AVLTree *oParent = newMe->parent; + startNode = oParent; + diff = 1; + + oParent->child[RIGHT] = newMe->child[LEFT]; + if (newMe->child[LEFT]) + newMe->child[LEFT]->parent = oParent; + + newMe->parent = parent; + newMe->child[LEFT] = child[LEFT]; + newMe->child[RIGHT] = child[RIGHT]; + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = newMe; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = newMe; + } + if (child[LEFT]) + child[LEFT]->parent = newMe; + if (child[RIGHT]) + child[RIGHT]->parent = newMe; + } + newMe->balance = balance; + if (racine == this) + racine = newMe; + } + else if (child[LEFT]) + { + startNode = parent; + diff = 0; + if (parent) + { + if (this == parent->child[LEFT]) + diff = -1; + if (this == parent->child[RIGHT]) + diff = 1; + } + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = child[LEFT]; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = child[LEFT]; + } + if (child[LEFT]->parent == this) + child[LEFT]->parent = parent; + if (racine == this) + racine = child[LEFT]; + } + else if (child[RIGHT]) + { + startNode = parent; + diff = 0; + if (parent) + { + if (this == parent->child[LEFT]) + diff = -1; + if (this == parent->child[RIGHT]) + diff = 1; + } + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = child[RIGHT]; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = child[RIGHT]; + } + if (child[RIGHT]->parent == this) + child[RIGHT]->parent = parent; + if (racine == this) + racine = child[RIGHT]; + } + else + { + startNode = parent; + diff = 0; + if (parent) + { + if (this == parent->child[LEFT]) + diff = -1; + if (this == parent->child[RIGHT]) + diff = 1; + } + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = nullptr; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = nullptr; + } + if (racine == this) + racine = nullptr; + } + parent = child[RIGHT] = child[LEFT] = nullptr; + balance = 0; + return avl_no_err; +} + +/* + * insertion + */ +int +AVLTree::Insert (AVLTree * &racine, int insertType, AVLTree * insertL, + AVLTree * insertR, bool rebalance) +{ + int res = Insert (racine, insertType, insertL, insertR); + if (res == avl_no_err && rebalance) + res = RestoreBalances ((AVLTree *) nullptr, racine); + return res; +} + +int +AVLTree::Insert (AVLTree * &racine, int insertType, AVLTree * insertL, + AVLTree * insertR) +{ + if (racine == nullptr) + { + racine = this; + return avl_no_err; + } + else + { + if (insertType == not_found) + { +// cout << "pb avec l'arbre de raster\n"; + return avl_ins_err; + } + else if (insertType == found_on_left) + { + if (insertR == nullptr || insertR->child[LEFT]) + { +// cout << "ngou?\n"; + return avl_ins_err; + } + insertR->child[LEFT] = this; + parent = insertR; + insertOn(LEFT, insertR); + } + else if (insertType == found_on_right) + { + if (insertL == nullptr || insertL->child[RIGHT]) + { +// cout << "ngou?\n"; + return avl_ins_err; + } + insertL->child[RIGHT] = this; + parent = insertL; + insertOn(RIGHT, insertL); + } + else if (insertType == found_between) + { + if (insertR == nullptr || insertL == nullptr + || (insertR->child[LEFT] != nullptr && insertL->child[RIGHT] != nullptr)) + { +// cout << "ngou?\n"; + return avl_ins_err; + } + if (insertR->child[LEFT] == nullptr) + { + insertR->child[LEFT] = this; + parent = insertR; + } + else if (insertL->child[RIGHT] == nullptr) + { + insertL->child[RIGHT] = this; + parent = insertL; + } + insertBetween (insertL, insertR); + } + else if (insertType == found_exact) + { + if (insertL == nullptr) + { +// cout << "ngou?\n"; + return avl_ins_err; + } + // et on insere + + if (insertL->child[RIGHT]) + { + insertL = insertL->child[RIGHT]->leafFromParent(insertL, LEFT); + if (insertL->child[LEFT]) + { +// cout << "ngou?\n"; + return avl_ins_err; + } + insertL->child[LEFT] = this; + this->parent = insertL; + insertBetween (insertL->elem[LEFT], insertL); + } + else + { + insertL->child[RIGHT] = this; + parent = insertL; + insertBetween (insertL, insertL->elem[RIGHT]); + } + } + else + { + // cout << "code incorrect\n"; + return avl_ins_err; + } + } + return avl_no_err; +} + +void +AVLTree::Relocate (AVLTree * to) +{ + if (elem[LEFT]) + elem[LEFT]->elem[RIGHT] = to; + if (elem[RIGHT]) + elem[RIGHT]->elem[LEFT] = to; + to->elem[LEFT] = elem[LEFT]; + to->elem[RIGHT] = elem[RIGHT]; + + if (parent) + { + if (parent->child[LEFT] == this) + parent->child[LEFT] = to; + if (parent->child[RIGHT] == this) + parent->child[RIGHT] = to; + } + if (child[RIGHT]) + { + child[RIGHT]->parent = to; + } + if (child[LEFT]) + { + child[LEFT]->parent = to; + } + to->parent = parent; + to->child[RIGHT] = child[RIGHT]; + to->child[LEFT] = child[LEFT]; +} + + +void AVLTree::insertOn(Side s, AVLTree *of) +{ + elem[1 - s] = of; + if (of) + of->elem[s] = this; +} + +void AVLTree::insertBetween(AVLTree *l, AVLTree *r) +{ + if (l) + l->elem[RIGHT] = this; + if (r) + r->elem[LEFT] = this; + elem[LEFT] = l; + elem[RIGHT] = r; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/AVL.h b/src/livarot/AVL.h new file mode 100644 index 0000000..5e0856c --- /dev/null +++ b/src/livarot/AVL.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * AVL.h + * nlivarot + * + * Created by fred on Mon Jun 16 2003. + * + */ + +#ifndef my_avl +#define my_avl + +#include +#include "LivarotDefs.h" + +/* + * base class providing AVL tree functionnality, that is binary balanced tree + * there is no Find() function because the class only deal with topological info + * subclasses of this class have to implement a Find(), and most certainly to + * override the Insert() function + */ + +class AVLTree +{ +public: + + AVLTree *elem[2]; + + // left most node (ie, with smallest key) in the subtree of this node + AVLTree *Leftmost(); + +protected: + + AVLTree *child[2]; + + AVLTree(); + virtual ~AVLTree(); + + // constructor/destructor meant to be called for an array of AVLTree created by malloc + void MakeNew(); + void MakeDelete(); + + // insertion of the present node in the tree + // insertType is the insertion type (defined in LivarotDefs.h: not_found, found_exact, found_on_left, etc) + // insertL is the node in the tree that is immediatly before the current one, NULL is the present node goes to the + // leftmost position. if insertType == found_exact, insertL should be the node with ak key + // equal to that of the present node + int Insert(AVLTree * &racine, int insertType, AVLTree *insertL, + AVLTree * insertR, bool rebalance); + + // called when this node is relocated to a new position in memory, to update pointers to him + void Relocate(AVLTree *to); + + // removal of the present element racine is the tree's root; it's a reference because if the + // node is the root, removal of the node will change the root + // rebalance==true if rebalancing is needed + int Remove(AVLTree * &racine, bool rebalance = true); + +private: + + AVLTree *parent; + + int balance; + + // insertion gruntwork. + int Insert(AVLTree * &racine, int insertType, AVLTree *insertL, AVLTree *insertR); + + // rebalancing functions. both are recursive, but the depth of the trees we'll use should not be a problem + // this one is for rebalancing after insertions + int RestoreBalances(AVLTree *from, AVLTree * &racine); + // this one is for removals + int RestoreBalances(int diff, AVLTree * &racine); + + // startNode is the node where the rebalancing starts; rebalancing then moves up the tree to the root + // diff is the change in "subtree height", as needed for the rebalancing + // racine is the reference to the root, since rebalancing can change it too + int Remove(AVLTree * &racine, AVLTree * &startNode, int &diff); + + void insertOn(Side s, AVLTree *of); + void insertBetween(AVLTree *l, AVLTree *r); + AVLTree *leaf(AVLTree *from, Side s); + AVLTree *leafFromParent(AVLTree *from, Side s); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/AlphaLigne.cpp b/src/livarot/AlphaLigne.cpp new file mode 100644 index 0000000..7ae72c1 --- /dev/null +++ b/src/livarot/AlphaLigne.cpp @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "AlphaLigne.h" + +#include +#include +#include +#include + +AlphaLigne::AlphaLigne(int iMin,int iMax) +{ + min=iMin; + max=iMax; + if ( max < min+1 ) max=min+1; + steps=nullptr; + nbStep=maxStep=0; + before.x=min-1; + before.delta=0; + after.x=max+1; + after.delta=0; +} +AlphaLigne::~AlphaLigne() +{ + g_free(steps); + steps=nullptr; + nbStep=maxStep=0; +} +void AlphaLigne::Affiche() +{ + printf("%i steps\n",nbStep); + for (int i=0;i %f %f / %f\n",spos,sval,epos,eval,tPente); + if ( sval == eval ) return 0; + // compute the footprint of [spos,epos] on the line of pixels + float curStF=floor(spos); + float curEnF=floor(epos); + int curSt=(int)curStF; + int curEn=(int)curEnF; + + // update curMin and curMax + if ( curSt > max ) { + // we're on the right of the visible portion of the line: bail out! + if ( eval < sval ) curMax=max; + return 0; + } + if ( curSt < curMin ) curMin=curSt; + if ( ceil(epos) > curMax ) curMax=(int)ceil(epos); + + // clamp the changed portion to [min,max], no need for bigger + if ( curMax > max ) curMax=max; + if ( curMin < min ) curMin=min; + + // total amount of change in pixel coverage from before the right to after the run + float needed=eval-sval; + float needC=/*(int)ldexpf(*/needed/*,24)*/; + + if ( curEn < min ) { + // the added portion is entirely on the left, so we only have to change the initial coverage for the line + before.delta+=needC; + return 0; + } + + // add the steps + // the pixels from [curSt..curEn] (included) intersect with [spos;epos] + // since we're dealing with delta in the coverage, there is also a curEn+1 delta, since the curEn pixel intersect + // with [spos;epos] and thus has some delta with respect to its next pixel + // lots of different cases... ugly + if ( curSt == curEn ) { + if ( curSt+1 < min ) { + before.delta+=needC; + } else { + if ( nbStep+2 >= maxStep ) { + maxStep=2*nbStep+2; + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + float stC=/*(int)ldexpf(*/(eval-sval)*(0.5*(epos-spos)+curStF+1-epos)/*,24)*/; + steps[nbStep].x=curSt; + steps[nbStep].delta=stC; + nbStep++; + steps[nbStep].x=curSt+1; + steps[nbStep].delta=needC-stC; // au final, on a toujours le bon delta, meme avec une arete completement verticale + nbStep++; + } + } else if ( curEn == curSt+1 ) { + if ( curSt+2 < min ) { + before.delta+=needC; + } else { + if ( nbStep+3 >= maxStep ) { + maxStep=2*nbStep+3; + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + float stC=/*(int)ldexpf(*/0.5*tPente*(curEnF-spos)*(curEnF-spos)/*,24)*/; + float enC=/*(int)ldexpf(*/tPente-0.5*tPente*((spos-curStF)*(spos-curStF)+(curEnF+1.0-epos)*(curEnF+1.0-epos))/*,24)*/; + steps[nbStep].x=curSt; + steps[nbStep].delta=stC; + nbStep++; + steps[nbStep].x=curEn; + steps[nbStep].delta=enC; + nbStep++; + steps[nbStep].x=curEn+1; + steps[nbStep].delta=needC-stC-enC; + nbStep++; + } + } else { + float stC=/*(int)ldexpf(*/0.5*tPente*(curStF+1-spos)*(curStF+1-spos)/*,24)*/; + float stFC=/*(int)ldexpf(*/tPente-0.5*tPente*(spos-curStF)*(spos-curStF)/*,24)*/; + float enC=/*(int)ldexpf(*/tPente-0.5*tPente*(curEnF+1.0-epos)*(curEnF+1.0-epos)/*,24)*/; + float miC=/*(int)ldexpf(*/tPente/*,24)*/; + if ( curSt < min ) { + if ( curEn > max ) { + if ( nbStep+(max-min) >= maxStep ) { + maxStep=2*nbStep+(max-min); + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + float bfd=min-curSt-1; + bfd*=miC; + before.delta+=stC+bfd; + for (int i=min;i= maxStep ) { + maxStep=2*nbStep+(curEn-min)+2; + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + float bfd=min-curSt-1; + bfd*=miC; + before.delta+=stC+bfd; + for (int i=min;i max ) { + if ( nbStep+3+(max-curSt) >= maxStep ) { + maxStep=2*nbStep+3+(curEn-curSt); + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + steps[nbStep].x=curSt; + steps[nbStep].delta=stC; + nbStep++; + steps[nbStep].x=curSt+1; + steps[nbStep].delta=stFC; + nbStep++; + for (int i=curSt+2;i= maxStep ) { + maxStep=2*nbStep+3+(curEn-curSt); + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + steps[nbStep].x=curSt; + steps[nbStep].delta=stC; + nbStep++; + steps[nbStep].x=curSt+1; + steps[nbStep].delta=stFC; + nbStep++; + for (int i=curSt+2;i max ) { + if ( eval < sval ) curMax=max; + return 0; // en dehors des limites (attention a ne pas faire ca avec curEn) + } + if ( curEn < min ) { + before.delta+=eval-sval; + return 0; // en dehors des limites (attention a ne pas faire ca avec curEn) + } + + if ( curSt < curMin ) curMin=curSt; +// int curEn=(int)curEnF; + if ( ceil(epos) > curMax-1 ) curMax=1+(int)ceil(epos); + if ( curSt < min ) { + before.delta+=eval-sval; + } else { + AddRun(curSt,/*(int)ldexpf(*/(((float)(curSt+1))-spos)*tPente/*,24)*/); + AddRun(curSt+1,/*(int)ldexpf(*/(spos-((float)(curSt)))*tPente/*,24)*/); + } + return 0; +} + +void AlphaLigne::Flatten() +{ + // just sort + if ( nbStep > 0 ) qsort(steps,nbStep,sizeof(alpha_step),CmpStep); +} +void AlphaLigne::AddRun(int st,float pente) +{ + if ( nbStep >= maxStep ) { + maxStep=2*nbStep+1; + steps=(alpha_step*)g_realloc(steps,maxStep*sizeof(alpha_step)); + } + int nStep=nbStep++; + steps[nStep].x=st; + steps[nStep].delta=pente; +} + +void AlphaLigne::Raster(raster_info &dest,void* color,RasterInRunFunc worker) +{ + // start by checking if there are actually pixels in need of rasterization + if ( curMax <= curMin ) return; + if ( dest.endPix <= curMin || dest.startPix >= curMax ) return; + + int nMin=curMin,nMax=curMax; + float alpSum=before.delta; // alpSum will be the pixel coverage value, so we start at before.delta + int curStep=0; + + // first add all the deltas up to the first pixel in need of rasterization + while ( curStep < nbStep && steps[curStep].x < nMin ) { + alpSum+=steps[curStep].delta; + curStep++; + } + // just in case, if the line bounds are greater than the buffer bounds. + if ( nMin < dest.startPix ) { + for (;( curStep < nbStep && steps[curStep].x < dest.startPix) ;curStep++) alpSum+=steps[curStep].delta; + nMin=dest.startPix; + } + if ( nMax > dest.endPix ) nMax=dest.endPix; + + // raster! + int curPos=dest.startPix; + for (;curStep 0 && steps[curStep].x > curPos ) { + // we're going to change the pixel position curPos, and alpSum is > 0: rasterization needed from + // the last position (curPos) up to the pixel we're moving to (steps[curStep].x) + int nst=curPos,nen=steps[curStep].x; +//Buffer::RasterRun(dest,color,nst,alpSum,nen,alpSum); + (worker)(dest,color,nst,alpSum,nen,alpSum); + } + // add coverage deltas + alpSum+=steps[curStep].delta; + curPos=steps[curStep].x; + if ( curPos >= nMax ) break; + } + // if we ended the line with alpSum > 0, we need to raster from curPos to the right edge + if ( alpSum > 0 && curPos < nMax ) { + int nst=curPos,nen=max; + (worker)(dest,color,nst,alpSum,nen,alpSum); +//Buffer::RasterRun(dest,color,nst,alpSum,nen,alpSum); + } +} diff --git a/src/livarot/AlphaLigne.h b/src/livarot/AlphaLigne.h new file mode 100644 index 0000000..a192e1c --- /dev/null +++ b/src/livarot/AlphaLigne.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef my_alpha_ligne +#define my_alpha_ligne + +#include "LivarotDefs.h" + +/* + * pixel coverage of a line, libart style: each pixel coverage is obtained from the coverage of the previous one by + * adding a delta given by a step. the goal is to have only a limited number of positions where the delta != 0, so that + * you only have to store a limited number of steps. + */ + +// a step +struct alpha_step { + int x; // position + float delta; // increase or decrease in pixel coverage with respect to the coverage of the previous pixel +}; + + +class AlphaLigne { +public: + // bounds of the line + // necessary since the visible portion of the canvas is bounded, and you need to compute + // the value of the pixel "just before the visible portion of the line" + int min,max; + int length; + + // before is the step containing the delta relative to a pixel infinitely far on the left of the line + // thus the initial pixel coverage is before.delta + alpha_step before,after; + // array of steps + int nbStep,maxStep; + alpha_step* steps; + + // bounds of the portion of the line that has received some coverage + int curMin,curMax; + + // iMin and iMax are the bounds of the visible portion of the line + AlphaLigne(int iMin,int iMax); + virtual ~AlphaLigne(); + + // empties the line + void Reset(); + + // add some coverage. + // pente is (eval-sval)/(epos-spos), because you can compute it once per edge, and thus spare the + // CPU some potentially costly divisions + int AddBord(float spos,float sval,float epos,float eval,float iPente); + // version where you don't have the pente parameter + int AddBord(float spos,float sval,float epos,float eval); + + // sorts the steps in increasing order. needed before you raster the line + void Flatten(); + + // debug dump of the steps + void Affiche(); + + // private + void AddRun(int st,float pente); + + // raster the line in the buffer given in "dest", with the rasterization primitive worker + // worker() is given the color parameter each time it is called. the type of the function is + // defined in LivarotDefs.h + void Raster(raster_info &dest,void* color,RasterInRunFunc worker); + + // also private. that's the comparison function given to qsort() + static int CmpStep(const void * p1, const void * p2) { + alpha_step* d1=(alpha_step*)p1; + alpha_step* d2=(alpha_step*)p2; + return d1->x - d2->x ; + }; +}; + + +#endif + diff --git a/src/livarot/BitLigne.cpp b/src/livarot/BitLigne.cpp new file mode 100644 index 0000000..2e44359 --- /dev/null +++ b/src/livarot/BitLigne.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "BitLigne.h" + +#include +#include +#include +#include +#include +#include +#include + +BitLigne::BitLigne(int ist,int ien,float iScale) +{ + scale=iScale; + invScale=1/iScale; + st=ist; + en=ien; + if ( en <= st ) en=st+1; + stBit=(int)floor(((float)st)*invScale); // round to pixel boundaries in the canvas + enBit=(int)ceil(((float)en)*invScale); + int nbBit=enBit-stBit; + if ( nbBit&31 ) { + nbInt=nbBit/32+1; + } else { + nbInt=nbBit/32; + } + nbInt+=1; + fullB=(uint32_t*)g_malloc(nbInt*sizeof(uint32_t)); + partB=(uint32_t*)g_malloc(nbInt*sizeof(uint32_t)); + + curMin=en; + curMax=st; +} +BitLigne::~BitLigne() +{ + g_free(fullB); + g_free(partB); +} + +void BitLigne::Reset() +{ + curMin=en; + curMax=st+1; + memset(fullB,0,nbInt*sizeof(uint32_t)); + memset(partB,0,nbInt*sizeof(uint32_t)); +} +int BitLigne::AddBord(float spos,float epos,bool full) +{ + if ( spos >= epos ) return 0; + + // separation of full and not entirely full bits is a bit useless + // the goal is to obtain a set of bits that are "on the edges" of the polygon, so that their coverage + // will be 1/2 on the average. in practice it's useless for anything but the even-odd fill rule + int ffBit,lfBit; // first and last bit of the portion of the line that is entirely covered + ffBit=(int)(ceil(invScale*spos)); + lfBit=(int)(floor(invScale*epos)); + int fpBit,lpBit; // first and last bit of the portion of the line that is not entirely but partially covered + fpBit=(int)(floor(invScale*spos)); + lpBit=(int)(ceil(invScale*epos)); + + // update curMin and curMax to reflect the start and end pixel that need to be updated on the canvas + if ( floor(spos) < curMin ) curMin=(int)floor(spos); + if ( ceil(epos) > curMax ) curMax=(int)ceil(epos); + + // clamp to the line + if ( ffBit < stBit ) ffBit=stBit; + if ( ffBit > enBit ) ffBit=enBit; + if ( lfBit < stBit ) lfBit=stBit; + if ( lfBit > enBit ) lfBit=enBit; + if ( fpBit < stBit ) fpBit=stBit; + if ( fpBit > enBit ) fpBit=enBit; + if ( lpBit < stBit ) lpBit=stBit; + if ( lpBit > enBit ) lpBit=enBit; + + // offset to get actual bit position in the array + ffBit-=stBit; + lfBit-=stBit; + fpBit-=stBit; + lpBit-=stBit; + + // get the end and start indices of the elements of fullB and partB that will receives coverage + int ffPos=ffBit>>5; + int lfPos=lfBit>>5; + int fpPos=fpBit>>5; + int lpPos=lpBit>>5; + // get bit numbers in the last and first changed elements of the fullB and partB arrays + int ffRem=ffBit&31; + int lfRem=lfBit&31; + int fpRem=fpBit&31; + int lpRem=lpBit&31; + // add the coverage + // note that the "full" bits are always a subset of the "not empty" bits, ie of the partial bits + // the function is a bit lame: since there is at most one bit that is partial but not full, or no full bit, + // it does 2 times the optimal amount of work when the coverage is full. but i'm too lazy to change that... + if ( fpPos == lpPos ) { // only one element of the arrays is modified + // compute the vector of changed bits in the element + uint32_t add=0xFFFFFFFF; + if ( lpRem < 32 ) {add>>=32-lpRem;add<<=32-lpRem; } + if ( lpRem <= 0 ) add=0; + if ( fpRem > 0) {add<<=fpRem;add>>=fpRem;} + // and put it in the line + fullB[fpPos]&=~(add); // partial is exclusive from full, so partial bits are removed from fullB + partB[fpPos]|=add; // and added to partB + if ( full ) { // if the coverage is full, add the vector of full bits + if ( ffBit <= lfBit ) { + add=0xFFFFFFFF; + if ( lfRem < 32 ) {add>>=32-lfRem;add<<=32-lfRem;} + if ( lfRem <= 0 ) add=0; + if ( ffRem > 0 ) {add<<=ffRem;add>>=ffRem;} + fullB[ffPos]|=add; + partB[ffPos]&=~(add); + } + } + } else { + // first and last elements are differents, so add what appropriate to each + uint32_t add=0xFFFFFFFF; + if ( fpRem > 0 ) {add<<=fpRem;add>>=fpRem;} + fullB[fpPos]&=~(add); + partB[fpPos]|=add; + + add=0xFFFFFFFF; + if ( lpRem < 32 ) {add>>=32-lpRem;add<<=32-lpRem;} + if ( lpRem <= 0 ) add=0; + fullB[lpPos]&=~(add); + partB[lpPos]|=add; + + // and fill what's in between with partial bits + if ( lpPos > fpPos+1 ) memset(fullB+(fpPos+1),0x00,(lpPos-fpPos-1)*sizeof(uint32_t)); + if ( lpPos > fpPos+1 ) memset(partB+(fpPos+1),0xFF,(lpPos-fpPos-1)*sizeof(uint32_t)); + + if ( full ) { // is the coverage is full, do your magic + if ( ffBit <= lfBit ) { + if ( ffPos == lfPos ) { + add=0xFFFFFFFF; + if ( lfRem < 32 ) {add>>=32-lfRem;add<<=32-lfRem;} + if ( lfRem <= 0 ) add=0; + if ( ffRem > 0 ) {add<<=ffRem;add>>=ffRem;} + fullB[ffPos]|=add; + partB[ffPos]&=~(add); + } else { + add=0xFFFFFFFF; + if ( ffRem > 0 ) {add<<=ffRem;add>>=ffRem;} + fullB[ffPos]|=add; + partB[ffPos]&=~add; + + add=0xFFFFFFFF; + if ( lfRem < 32 ) {add>>=32-lfRem;add<<=32-lfRem;} + if ( lfRem <= 0 ) add=0; + fullB[lfPos]|=add; + partB[lfPos]&=~add; + + if ( lfPos > ffPos+1 ) memset(fullB+(ffPos+1),0xFF,(lfPos-ffPos-1)*sizeof(uint32_t)); + if ( lfPos > ffPos+1 ) memset(partB+(ffPos+1),0x00,(lfPos-ffPos-1)*sizeof(uint32_t)); + } + } + } + } + return 0; +} + + +void BitLigne::Affiche() +{ + for (int i=0;i bit in the line + // scale is: bit -> canvas, ie the size (width) of a bit + float scale,invScale; + + BitLigne(int ist,int ien,float iScale=0.25); // default scale is 1/4 for 4x4 supersampling + virtual ~BitLigne(); + + // reset the line to full empty + void Reset(); + + // put coverage from spos to epos (in canvas coordinates) + // full==true means that the bits from (fractional) position spos to epos are entirely covered + // full==false means the bits are not entirely covered, ie this is an edge + // see the Scan() and AvanceEdge() functions to see the difference + int AddBord(float spos,float epos,bool full); + + // debug dump + void Affiche(); + +}; + +#endif + + diff --git a/src/livarot/CMakeLists.txt b/src/livarot/CMakeLists.txt new file mode 100644 index 0000000..185ae8a --- /dev/null +++ b/src/livarot/CMakeLists.txt @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(livarot_SRC + AlphaLigne.cpp + AVL.cpp + BitLigne.cpp + float-line.cpp + int-line.cpp + PathConversion.cpp + Path.cpp + PathCutting.cpp + path-description.cpp + PathOutline.cpp + PathSimplify.cpp + PathStroke.cpp + Shape.cpp + ShapeDraw.cpp + ShapeMisc.cpp + ShapeRaster.cpp + ShapeSweep.cpp + sweep-event.cpp + sweep-tree.cpp + sweep-tree-list.cpp + + + # ------- + # Headers + AVL.h + AlphaLigne.h + BitLigne.h + Livarot.h + LivarotDefs.h + Path.h + Shape.h + float-line.h + int-line.h + path-description.h + sweep-event-queue.h + sweep-event.h + sweep-tree-list.h + sweep-tree.h +) + +add_inkscape_lib(livarot_LIB "${livarot_SRC}") diff --git a/src/livarot/Livarot.h b/src/livarot/Livarot.h new file mode 100644 index 0000000..80cc6ba --- /dev/null +++ b/src/livarot/Livarot.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Livarot.h + * nlivarot + * + * Created by fred on Sun Jul 27 2003. + * + */ + +#include "LivarotDefs.h" + +#include "Shape.h" +#include "Path.h" +#include "Buffer.h" + +#include "Ligne.h" +#include "AlphaLigne.h" +#include "BitLigne.h" + +#include "Bounding.h" +#include "Region.h" + +#include "VoronoiGraph.h" +#include "VoronoiConstr.h" + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/LivarotDefs.h b/src/livarot/LivarotDefs.h new file mode 100644 index 0000000..1cabb74 --- /dev/null +++ b/src/livarot/LivarotDefs.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef my_defs +#define my_defs + +#include + +// error codes (mostly obsolete) +enum +{ + avl_no_err = 0, // 0 is the error code for "everything OK" + avl_bal_err = 1, + avl_rm_err = 2, + avl_ins_err = 3, + shape_euler_err = 4, // computations result in a non-eulerian graph, thus the function cannot do a proper polygon + // despite the rounding sheme, this still happen with uber-complex graphs + // note that coordinates are stored in double => double precision for the computation is not even + // enough to get exact results (need quadruple precision, i think). + shape_input_err = 5, // the function was given an incorrect input (not a polygon, or not eulerian) + shape_nothing_to_do = 6 // the function had nothing to do (zero offset, etc) +}; + +// return codes for the find function in the AVL tree (private) +enum +{ + not_found = 0, + found_exact = 1, + found_on_left = 2, + found_on_right = 3, + found_between = 4 +}; + +// types of cap for stroking polylines +enum butt_typ +{ + butt_straight, // straight line + butt_square, // half square + butt_round, // half circle + butt_pointy // a little pointy hat +}; +// types of joins for stroking paths +enum join_typ +{ + join_straight, // a straight line + join_round, // arc of circle (in fact, one or two quadratic bezier curve chunks) + join_pointy // a miter join (uses the miter parameter) +}; +typedef enum butt_typ ButtType; +typedef enum join_typ JoinType; + +enum fill_typ +{ + fill_oddEven = 0, + fill_nonZero = 1, + fill_positive = 2, + fill_justDont = 3 +}; +typedef enum fill_typ FillRule; + +// stupid version of dashes: in dash x is plain, dash x+1 must be empty, so the gap field is extremely redundant +struct one_dash +{ + bool gap; + double length; +}; + +// color definition structures for the rasterizations primitives (not present here) +struct std_color +{ + uint32_t uCol; + uint16_t iColA, iColR, iColG, iColB; + double fColA, fColR, fColG, fColB; + uint32_t iColATab[256]; +}; + +struct grad_stop +{ + double at; + double ca, cr, cg, cb; + double iSize; +}; + +// linear gradient for filling polygons +struct lin_grad +{ + int type; // 0= gradient appears once + // 1= repeats itself start-end/start-end/start-end... + // 2= repeats itself start-end/end-start/start-end... + double u, v, w; // u*x+v*y+w = position in the gradient (clipped to [0;1]) +// double caa,car,cag,cab; // color at gradient position 0 +// double cba,cbr,cbg,cbb; // color at gradient position 1 + int nbStop; + grad_stop stops[2]; +}; + +// radial gradient (color is funciton of r^2, need to be corrected with a sqrt() to be r) +struct rad_grad +{ + int type; // 0= gradient appears once + // 1= repeats itself start-end/start-end/start-end... + // 2= repeats itself start-end/end-start/start-end... + double mh, mv; // center + double rxx, rxy, ryx, ryy; // 1/radius + int nbStop; + grad_stop stops[2]; +}; + +// functions types for an arbitrary filling shader +typedef void (*InitColorFunc) (int ph, int pv, void *); // init for position ph,pv; the last parameter is a pointer + // on the gen_color structure +typedef void (*NextPixelColorFunc) (void *); // go to next pixel and update the color +typedef void (*NextLigneColorFunc) (void *); // go to next line (the h-coordinate must be the ph passed in + // the InitColorFunc) +typedef void (*GotoPixelColorFunc) (int ph, void *); // move to h-coordinate ph +typedef void (*GotoLigneColorFunc) (int pv, void *); // move to v-coordinate pv (the h-coordinate must be the ph passed + // in the InitColorFunc) + +// an arbitrary shader +struct gen_color +{ + double colA, colR, colG, colB; + InitColorFunc iFunc; + NextPixelColorFunc npFunc; + NextLigneColorFunc nlFunc; + GotoPixelColorFunc gpFunc; + GotoLigneColorFunc glFunc; +}; + +// info for a run of pixel to fill +struct raster_info { + int startPix,endPix; // start and end pixel from the polygon POV + int sth,stv; // coordinates for the first pixel in the run, in (possibly another) POV + uint32_t* buffer; // pointer to the first pixel in the run +}; +typedef void (*RasterInRunFunc) (raster_info &dest,void *data,int nst,float vst,int nen,float ven); // init for position ph,pv; the last parameter is a pointer + + +enum Side { + LEFT = 0, + RIGHT = 1 +}; + +enum FirstOrLast { + FIRST = 0, + LAST = 1 +}; + +#endif diff --git a/src/livarot/Path.cpp b/src/livarot/Path.cpp new file mode 100644 index 0000000..03e17db --- /dev/null +++ b/src/livarot/Path.cpp @@ -0,0 +1,939 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "Path.h" +#include "livarot/path-description.h" + +/* + * manipulation of the path data: path description and polyline + * grunt work... + * at the end of this file, 2 utilitary functions to get the point and tangent to path associated with a (command no;abcissis) + */ + + +Path::Path() +{ + descr_flags = 0; + pending_bezier_cmd = -1; + pending_moveto_cmd = -1; + + back = false; +} + +Path::~Path() +{ + for (auto & i : descr_cmd) { + delete i; + } +} + +// debug function do dump the path contents on stdout +void Path::Affiche() +{ + std::cout << "path: " << descr_cmd.size() << " commands." << std::endl; + for (auto i : descr_cmd) { + i->dump(std::cout); + std::cout << std::endl; + } + + std::cout << std::endl; +} + +void Path::Reset() +{ + for (auto & i : descr_cmd) { + delete i; + } + + descr_cmd.clear(); + pending_bezier_cmd = -1; + pending_moveto_cmd = -1; + descr_flags = 0; +} + +void Path::Copy(Path * who) +{ + ResetPoints(); + + for (auto & i : descr_cmd) { + delete i; + } + + descr_cmd.clear(); + + for (auto i : who->descr_cmd) + { + descr_cmd.push_back(i->clone()); + } +} + +void Path::CloseSubpath() +{ + descr_flags &= ~(descr_doing_subpath); + pending_moveto_cmd = -1; +} + +int Path::ForcePoint() +{ + if (descr_flags & descr_adding_bezier) { + EndBezierTo (); + } + + if ( (descr_flags & descr_doing_subpath) == 0 ) { + return -1; + } + + if (descr_cmd.empty()) { + return -1; + } + + descr_cmd.push_back(new PathDescrForced); + return descr_cmd.size() - 1; +} + + +void Path::InsertForcePoint(int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + ForcePoint(); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrForced); +} + +int Path::Close() +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } else { + // Nothing to close. + return -1; + } + + descr_cmd.push_back(new PathDescrClose); + + descr_flags &= ~(descr_doing_subpath); + pending_moveto_cmd = -1; + + return descr_cmd.size() - 1; +} + +int Path::MoveTo(Geom::Point const &iPt) +{ + if ( descr_flags & descr_adding_bezier ) { + EndBezierTo(iPt); + } + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + pending_moveto_cmd = descr_cmd.size(); + + descr_cmd.push_back(new PathDescrMoveTo(iPt)); + + descr_flags |= descr_doing_subpath; + return descr_cmd.size() - 1; +} + +void Path::InsertMoveTo(Geom::Point const &iPt, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + MoveTo(iPt); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrMoveTo(iPt)); +} + +int Path::LineTo(Geom::Point const &iPt) +{ + if (descr_flags & descr_adding_bezier) { + EndBezierTo (iPt); + } + if (!( descr_flags & descr_doing_subpath )) { + return MoveTo (iPt); + } + + descr_cmd.push_back(new PathDescrLineTo(iPt)); + return descr_cmd.size() - 1; +} + +void Path::InsertLineTo(Geom::Point const &iPt, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + LineTo(iPt); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrLineTo(iPt)); +} + +int Path::CubicTo(Geom::Point const &iPt, Geom::Point const &iStD, Geom::Point const &iEnD) +{ + if (descr_flags & descr_adding_bezier) { + EndBezierTo(iPt); + } + if ( (descr_flags & descr_doing_subpath) == 0) { + return MoveTo (iPt); + } + + descr_cmd.push_back(new PathDescrCubicTo(iPt, iStD, iEnD)); + return descr_cmd.size() - 1; +} + + +void Path::InsertCubicTo(Geom::Point const &iPt, Geom::Point const &iStD, Geom::Point const &iEnD, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + CubicTo(iPt,iStD,iEnD); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrCubicTo(iPt, iStD, iEnD)); +} + +int Path::ArcTo(Geom::Point const &iPt, double iRx, double iRy, double angle, + bool iLargeArc, bool iClockwise) +{ + if (descr_flags & descr_adding_bezier) { + EndBezierTo(iPt); + } + if ( (descr_flags & descr_doing_subpath) == 0 ) { + return MoveTo(iPt); + } + + descr_cmd.push_back(new PathDescrArcTo(iPt, iRx, iRy, angle, iLargeArc, iClockwise)); + return descr_cmd.size() - 1; +} + + +void Path::InsertArcTo(Geom::Point const &iPt, double iRx, double iRy, double angle, + bool iLargeArc, bool iClockwise, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + ArcTo(iPt, iRx, iRy, angle, iLargeArc, iClockwise); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrArcTo(iPt, iRx, iRy, + angle, iLargeArc, iClockwise)); +} + +int Path::TempBezierTo() +{ + if (descr_flags & descr_adding_bezier) { + CancelBezier(); + } + if ( (descr_flags & descr_doing_subpath) == 0) { + // No starting point -> bad. + return -1; + } + pending_bezier_cmd = descr_cmd.size(); + + descr_cmd.push_back(new PathDescrBezierTo(Geom::Point(0, 0), 0)); + descr_flags |= descr_adding_bezier; + descr_flags |= descr_delayed_bezier; + return descr_cmd.size() - 1; +} + +void Path::CancelBezier() +{ + descr_flags &= ~(descr_adding_bezier); + descr_flags &= ~(descr_delayed_bezier); + if (pending_bezier_cmd < 0) { + return; + } + + /* FIXME: I think there's a memory leak here */ + descr_cmd.resize(pending_bezier_cmd); + pending_bezier_cmd = -1; +} + +int Path::EndBezierTo() +{ + if (descr_flags & descr_delayed_bezier) { + CancelBezier (); + } else { + pending_bezier_cmd = -1; + descr_flags &= ~(descr_adding_bezier); + descr_flags &= ~(descr_delayed_bezier); + } + return -1; +} + +int Path::EndBezierTo(Geom::Point const &iPt) +{ + if ( (descr_flags & descr_adding_bezier) == 0 ) { + return LineTo(iPt); + } + if ( (descr_flags & descr_doing_subpath) == 0 ) { + return MoveTo(iPt); + } + if ( (descr_flags & descr_delayed_bezier) == 0 ) { + return EndBezierTo(); + } + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[pending_bezier_cmd]); + nData->p = iPt; + pending_bezier_cmd = -1; + descr_flags &= ~(descr_adding_bezier); + descr_flags &= ~(descr_delayed_bezier); + return -1; +} + + +int Path::IntermBezierTo(Geom::Point const &iPt) +{ + if ( (descr_flags & descr_adding_bezier) == 0 ) { + return LineTo (iPt); + } + + if ( (descr_flags & descr_doing_subpath) == 0) { + return MoveTo (iPt); + } + + descr_cmd.push_back(new PathDescrIntermBezierTo(iPt)); + + PathDescrBezierTo *nBData = dynamic_cast(descr_cmd[pending_bezier_cmd]); + nBData->nb++; + return descr_cmd.size() - 1; +} + + +void Path::InsertIntermBezierTo(Geom::Point const &iPt, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + IntermBezierTo(iPt); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrIntermBezierTo(iPt)); +} + + +int Path::BezierTo(Geom::Point const &iPt) +{ + if ( descr_flags & descr_adding_bezier ) { + EndBezierTo(iPt); + } + + if ( (descr_flags & descr_doing_subpath) == 0 ) { + return MoveTo (iPt); + } + + pending_bezier_cmd = descr_cmd.size(); + + descr_cmd.push_back(new PathDescrBezierTo(iPt, 0)); + descr_flags |= descr_adding_bezier; + descr_flags &= ~(descr_delayed_bezier); + return descr_cmd.size() - 1; +} + + +void Path::InsertBezierTo(Geom::Point const &iPt, int iNb, int at) +{ + if ( at < 0 || at > int(descr_cmd.size()) ) { + return; + } + + if ( at == int(descr_cmd.size()) ) { + BezierTo(iPt); + return; + } + + descr_cmd.insert(descr_cmd.begin() + at, new PathDescrBezierTo(iPt, iNb)); +} + + +/* + * points of the polyline + */ +void +Path::SetBackData (bool nVal) +{ + if (! back) { + if (nVal) { + back = true; + ResetPoints(); + } + } else { + if (! nVal) { + back = false; + ResetPoints(); + } + } +} + + +void Path::ResetPoints() +{ + pts.clear(); +} + + +int Path::AddPoint(Geom::Point const &iPt, bool mvto) +{ + if (back) { + return AddPoint (iPt, -1, 0.0, mvto); + } + + if ( !mvto && !pts.empty() && pts.back().p == iPt ) { + return -1; + } + + int const n = pts.size(); + pts.emplace_back(mvto ? polyline_moveto : polyline_lineto, iPt); + return n; +} + + +int Path::ReplacePoint(Geom::Point const &iPt) +{ + if (pts.empty()) { + return -1; + } + + int const n = pts.size() - 1; + pts[n] = path_lineto(polyline_lineto, iPt); + return n; +} + + +int Path::AddPoint(Geom::Point const &iPt, int ip, double it, bool mvto) +{ + if (! back) { + return AddPoint (iPt, mvto); + } + + if ( !mvto && !pts.empty() && pts.back().p == iPt ) { + return -1; + } + + int const n = pts.size(); + pts.emplace_back(mvto ? polyline_moveto : polyline_lineto, iPt, ip, it); + return n; +} + +int Path::AddForcedPoint(Geom::Point const &iPt) +{ + if (back) { + return AddForcedPoint (iPt, -1, 0.0); + } + + if ( pts.empty() || pts.back().isMoveTo != polyline_lineto ) { + return -1; + } + + int const n = pts.size(); + pts.emplace_back(polyline_forced, pts[n - 1].p); + return n; +} + + +int Path::AddForcedPoint(Geom::Point const &iPt, int /*ip*/, double /*it*/) +{ + /* FIXME: ip & it aren't used. Is this deliberate? */ + if (!back) { + return AddForcedPoint (iPt); + } + + if ( pts.empty() || pts.back().isMoveTo != polyline_lineto ) { + return -1; + } + + int const n = pts.size(); + pts.emplace_back(polyline_forced, pts[n - 1].p, pts[n - 1].piece, pts[n - 1].t); + return n; +} + +void Path::PolylineBoundingBox(double &l, double &t, double &r, double &b) +{ + l = t = r = b = 0.0; + if ( pts.empty() ) { + return; + } + + std::vector::const_iterator i = pts.begin(); + l = r = i->p[Geom::X]; + t = b = i->p[Geom::Y]; + ++i; + + for (; i != pts.end(); ++i) { + r = std::max(r, i->p[Geom::X]); + l = std::min(l, i->p[Geom::X]); + b = std::max(b, i->p[Geom::Y]); + t = std::min(t, i->p[Geom::Y]); + } +} + + +/** + * \param piece Index of a one of our commands. + * \param at Distance along the segment that corresponds to `piece' (0 <= at <= 1) + * \param pos Filled in with the point at `at' on `piece'. + */ + +void Path::PointAt(int piece, double at, Geom::Point &pos) +{ + if (piece < 0 || piece >= int(descr_cmd.size())) { + // this shouldn't happen: the piece we are asked for doesn't + // exist in the path + pos = Geom::Point(0,0); + return; + } + + PathDescr const *theD = descr_cmd[piece]; + int const typ = theD->getType(); + Geom::Point tgt; + double len; + double rad; + + if (typ == descr_moveto) { + + return PointAt (piece + 1, 0.0, pos); + + } else if (typ == descr_close || typ == descr_forced) { + + return PointAt (piece - 1, 1.0, pos); + + } else if (typ == descr_lineto) { + + PathDescrLineTo const *nData = dynamic_cast(theD); + TangentOnSegAt(at, PrevPoint (piece - 1), *nData, pos, tgt, len); + + } else if (typ == descr_arcto) { + + PathDescrArcTo const *nData = dynamic_cast(theD); + TangentOnArcAt(at,PrevPoint (piece - 1), *nData, pos, tgt, len, rad); + + } else if (typ == descr_cubicto) { + + PathDescrCubicTo const *nData = dynamic_cast(theD); + TangentOnCubAt(at, PrevPoint (piece - 1), *nData, false, pos, tgt, len, rad); + + } else if (typ == descr_bezierto || typ == descr_interm_bezier) { + + int bez_st = piece; + while (bez_st >= 0) { + int nt = descr_cmd[bez_st]->getType(); + if (nt == descr_bezierto) + break; + bez_st--; + } + if ( bez_st < 0 ) { + // Didn't find the beginning of the spline (bad). + // [pas trouvé le dubut de la spline (mauvais)] + return PointAt(piece - 1, 1.0, pos); + } + + PathDescrBezierTo *stB = dynamic_cast(descr_cmd[bez_st]); + if ( piece > bez_st + stB->nb ) { + // The spline goes past the authorized number of commands (bad). + // [la spline sort du nombre de commandes autorisé (mauvais)] + return PointAt(piece - 1, 1.0, pos); + } + + int k = piece - bez_st; + Geom::Point const bStPt = PrevPoint(bez_st - 1); + if (stB->nb == 1 || k <= 0) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[bez_st + 1]); + TangentOnBezAt(at, bStPt, *nData, *stB, false, pos, tgt, len, rad); + } else { + // forcement plus grand que 1 + if (k == 1) { + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + 1]); + PathDescrIntermBezierTo *nnextI = dynamic_cast(descr_cmd[bez_st + 2]); + PathDescrBezierTo fin(0.5 * (nextI->p + nnextI->p), 1); + TangentOnBezAt(at, bStPt, *nextI, fin, false, pos, tgt, len, rad); + } else if (k == stB->nb) { + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + k]); + PathDescrIntermBezierTo *prevI = dynamic_cast(descr_cmd[bez_st + k - 1]); + Geom::Point stP = 0.5 * ( prevI->p + nextI->p ); + TangentOnBezAt(at, stP, *nextI, *stB, false, pos, tgt, len, rad); + } else { + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + k]); + PathDescrIntermBezierTo *prevI = dynamic_cast(descr_cmd[bez_st + k - 1]); + PathDescrIntermBezierTo *nnextI = dynamic_cast(descr_cmd[bez_st + k + 1]); + Geom::Point stP = 0.5 * ( prevI->p + nextI->p ); + PathDescrBezierTo fin(0.5 * (nextI->p + nnextI->p), 1); + TangentOnBezAt(at, stP, *nextI, fin, false, pos, tgt, len, rad); + } + } + } +} + + +void Path::PointAndTangentAt(int piece, double at, Geom::Point &pos, Geom::Point &tgt) +{ + if (piece < 0 || piece >= int(descr_cmd.size())) { + // this shouldn't happen: the piece we are asked for doesn't exist in the path + pos = Geom::Point(0, 0); + return; + } + + PathDescr const *theD = descr_cmd[piece]; + int typ = theD->getType(); + double len; + double rad; + if (typ == descr_moveto) { + + return PointAndTangentAt(piece + 1, 0.0, pos, tgt); + + } else if (typ == descr_close ) { + + int cp = piece - 1; + while ( cp >= 0 && (descr_cmd[cp]->getType()) != descr_moveto ) { + cp--; + } + if ( cp >= 0 ) { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[cp]); + PathDescrLineTo dst(nData->p); + TangentOnSegAt(at, PrevPoint (piece - 1), dst, pos, tgt, len); + } + + } else if ( typ == descr_forced) { + + return PointAndTangentAt(piece - 1, 1.0, pos,tgt); + + } else if (typ == descr_lineto) { + + PathDescrLineTo const *nData = dynamic_cast(theD); + TangentOnSegAt(at, PrevPoint (piece - 1), *nData, pos, tgt, len); + + } else if (typ == descr_arcto) { + + PathDescrArcTo const *nData = dynamic_cast(theD); + TangentOnArcAt (at,PrevPoint (piece - 1), *nData, pos, tgt, len, rad); + + } else if (typ == descr_cubicto) { + + PathDescrCubicTo const *nData = dynamic_cast(theD); + TangentOnCubAt (at, PrevPoint (piece - 1), *nData, false, pos, tgt, len, rad); + + } else if (typ == descr_bezierto || typ == descr_interm_bezier) { + int bez_st = piece; + while (bez_st >= 0) { + int nt = descr_cmd[bez_st]->getType(); + if (nt == descr_bezierto) break; + bez_st--; + } + if ( bez_st < 0 ) { + return PointAndTangentAt(piece - 1, 1.0, pos, tgt); + // Didn't find the beginning of the spline (bad). + // [pas trouvé le dubut de la spline (mauvais)] + } + + PathDescrBezierTo* stB = dynamic_cast(descr_cmd[bez_st]); + if ( piece > bez_st + stB->nb ) { + return PointAndTangentAt(piece - 1, 1.0, pos, tgt); + // The spline goes past the number of authorized commands (bad). + // [la spline sort du nombre de commandes autorisé (mauvais)] + } + + int k = piece - bez_st; + Geom::Point const bStPt(PrevPoint( bez_st - 1 )); + if (stB->nb == 1 || k <= 0) { + PathDescrIntermBezierTo* nData = dynamic_cast(descr_cmd[bez_st + 1]); + TangentOnBezAt (at, bStPt, *nData, *stB, false, pos, tgt, len, rad); + } else { + // forcement plus grand que 1 + if (k == 1) { + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + 1]); + PathDescrIntermBezierTo *nnextI = dynamic_cast(descr_cmd[bez_st + 2]); + PathDescrBezierTo fin(0.5 * (nextI->p + nnextI->p), 1); + TangentOnBezAt(at, bStPt, *nextI, fin, false, pos, tgt, len, rad); + } else if (k == stB->nb) { + PathDescrIntermBezierTo *prevI = dynamic_cast(descr_cmd[bez_st + k - 1]); + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + k]); + Geom::Point stP = 0.5 * ( prevI->p + nextI->p ); + TangentOnBezAt(at, stP, *nextI, *stB, false, pos, tgt, len, rad); + } else { + PathDescrIntermBezierTo *prevI = dynamic_cast(descr_cmd[bez_st + k - 1]); + PathDescrIntermBezierTo *nextI = dynamic_cast(descr_cmd[bez_st + k]); + PathDescrIntermBezierTo *nnextI = dynamic_cast(descr_cmd[bez_st + k + 1]); + Geom::Point stP = 0.5 * ( prevI->p + nextI->p ); + PathDescrBezierTo fin(0.5 * (nnextI->p + nnextI->p), 1); + TangentOnBezAt(at, stP, *nextI, fin, false, pos, tgt, len, rad); + } + } + } +} + +void Path::Transform(const Geom::Affine &trans) +{ + for (auto & i : descr_cmd) { + i->transform(trans); + } +} + +void Path::FastBBox(double &l,double &t,double &r,double &b) +{ + l = t = r = b = 0; + bool empty = true; + Geom::Point lastP(0, 0); + + for (auto & i : descr_cmd) { + int const typ = i->getType(); + switch ( typ ) { + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + lastP = nData->p; + } + break; + + case descr_moveto: + { + PathDescrMoveTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + lastP = nData->p; + } + break; + + case descr_arcto: + { + PathDescrArcTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + lastP = nData->p; + } + break; + + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + +/* bug 249665: "...the calculation of the bounding-box for cubic-paths +has some extra steps to make it work correctly in Win32 that unfortunately +are unnecessary in Linux, generating wrong results. This only shows in +Type1 fonts because they use cubic-paths instead of the +bezier-paths used by True-Type fonts." +*/ + +#ifdef _WIN32 + Geom::Point np = nData->p - nData->end; + if ( np[Geom::X] < l ) { + l = np[Geom::X]; + } + if ( np[Geom::X] > r ) { + r = np[Geom::X]; + } + if ( np[Geom::Y] < t ) { + t = np[Geom::Y]; + } + if ( np[Geom::Y] > b ) { + b = np[Geom::Y]; + } + + np = lastP + nData->start; + if ( np[Geom::X] < l ) { + l = np[Geom::X]; + } + if ( np[Geom::X] > r ) { + r = np[Geom::X]; + } + if ( np[Geom::Y] < t ) { + t = np[Geom::Y]; + } + if ( np[Geom::Y] > b ) { + b = np[Geom::Y]; + } +#endif + + lastP = nData->p; + } + break; + + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + lastP = nData->p; + } + break; + + case descr_interm_bezier: + { + PathDescrIntermBezierTo *nData = dynamic_cast(i); + if ( empty ) { + l = r = nData->p[Geom::X]; + t = b = nData->p[Geom::Y]; + empty = false; + } else { + if ( nData->p[Geom::X] < l ) { + l = nData->p[Geom::X]; + } + if ( nData->p[Geom::X] > r ) { + r = nData->p[Geom::X]; + } + if ( nData->p[Geom::Y] < t ) { + t = nData->p[Geom::Y]; + } + if ( nData->p[Geom::Y] > b ) { + b = nData->p[Geom::Y]; + } + } + } + break; + } + } +} + +char *Path::svg_dump_path() const +{ + Inkscape::SVGOStringStream os; + + for (int i = 0; i < int(descr_cmd.size()); i++) { + Geom::Point const p = (i == 0) ? Geom::Point(0, 0) : PrevPoint(i - 1); + descr_cmd[i]->dumpSVG(os, p); + } + + return g_strdup (os.str().c_str()); +} + +// Find out if the segment that corresponds to 'piece' is a straight line +bool Path::IsLineSegment(int piece) +{ + if (piece < 0 || piece >= int(descr_cmd.size())) { + return false; + } + + PathDescr const *theD = descr_cmd[piece]; + int const typ = theD->getType(); + + return (typ == descr_lineto); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/Path.h b/src/livarot/Path.h new file mode 100644 index 0000000..c965e36 --- /dev/null +++ b/src/livarot/Path.h @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Path.h + * nlivarot + * + * Created by fred on Tue Jun 17 2003. + * + */ + +#ifndef my_path +#define my_path + +#include +#include "LivarotDefs.h" +#include <2geom/point.h> + +struct PathDescr; +struct PathDescrLineTo; +struct PathDescrArcTo; +struct PathDescrCubicTo; +struct PathDescrBezierTo; +struct PathDescrIntermBezierTo; + +class SPStyle; + +/* + * the Path class: a structure to hold path description and their polyline approximation (not kept in sync) + * the path description is built with regular commands like MoveTo() LineTo(), etc + * the polyline approximation is built by a call to Convert() or its variants + * another possibility would be to call directly the AddPoint() functions, but that is not encouraged + * the conversion to polyline can salvage data as to where on the path each polyline's point lies; use + * ConvertWithBackData() for this. after this call, it's easy to rewind the polyline: sequences of points + * of the same path command can be reassembled in a command + */ + +// polyline description commands +enum +{ + polyline_lineto = 0, // a lineto + polyline_moveto = 1, // a moveto + polyline_forced = 2 // a forced point, ie a point that was an angle or an intersection in a previous life + // or more realistically a control point in the path description that created the polyline + // forced points are used as "breakable" points for the polyline -> cubic bezier patch operations + // each time the bezier fitter encounters such a point in the polyline, it decreases its treshhold, + // so that it is more likely to cut the polyline at that position and produce a bezier patch +}; + +class Shape; + +// path creation: 2 phases: first the path is given as a succession of commands (MoveTo, LineTo, CurveTo...); then it +// is converted in a polyline +// a polylone can be stroked or filled to make a polygon +class Path +{ + friend class Shape; + +public: + + // flags for the path construction + enum + { + descr_ready = 0, + descr_adding_bezier = 1, // we're making a bezier spline, so you can expect pending_bezier_* to have a value + descr_doing_subpath = 2, // we're doing a path, so there is a moveto somewhere + descr_delayed_bezier = 4,// the bezier spline we're doing was initiated by a TempBezierTo(), so we'll need an endpoint + descr_dirty = 16 // the path description was modified + }; + + // some data for the construction: what's pending, and some flags + int descr_flags; + int pending_bezier_cmd; + int pending_bezier_data; + int pending_moveto_cmd; + int pending_moveto_data; + // the path description + std::vector descr_cmd; + + // polyline storage: a series of coordinates (and maybe weights) + // also back data: info on where this polyline's segment comes from, ie which command in the path description: "piece" + // and what abcissis on the chunk of path for this command: "t" + // t=0 means it's at the start of the command's chunk, t=1 it's at the end + struct path_lineto + { + path_lineto(bool m, Geom::Point pp) : isMoveTo(m), p(pp), piece(-1), t(0), closed(false) {} + path_lineto(bool m, Geom::Point pp, int pie, double tt) : isMoveTo(m), p(pp), piece(pie), t(tt), closed(false) {} + + int isMoveTo; + Geom::Point p; + int piece; + double t; + bool closed; // true if subpath is closed (this point is the last point of a closed subpath) + }; + + std::vector pts; + + bool back; + + Path(); + virtual ~Path(); + + // creation of the path description + void Reset(); // reset to the empty description + void Copy (Path * who); + + // the commands... + int ForcePoint(); + int Close(); + int MoveTo ( Geom::Point const &ip); + int LineTo ( Geom::Point const &ip); + int CubicTo ( Geom::Point const &ip, Geom::Point const &iStD, Geom::Point const &iEnD); + int ArcTo ( Geom::Point const &ip, double iRx, double iRy, double angle, bool iLargeArc, bool iClockwise); + int IntermBezierTo ( Geom::Point const &ip); // add a quadratic bezier spline control point + int BezierTo ( Geom::Point const &ip); // quadratic bezier spline to this point (control points can be added after this) + int TempBezierTo(); // start a quadratic bezier spline (control points can be added after this) + int EndBezierTo(); + int EndBezierTo ( Geom::Point const &ip); // ends a quadratic bezier spline (for curves started with TempBezierTo) + + // transforms a description in a polyline (for stroking and filling) + // treshhold is the max length^2 (sort of) + void Convert (double treshhold); + void ConvertEvenLines (double treshhold); // decomposes line segments too, for later recomposition + // same function for use when you want to later recompose the curves from the polyline + void ConvertWithBackData (double treshhold); + + // creation of the polyline (you can tinker with these function if you want) + void SetBackData (bool nVal); // has back data? + void ResetPoints(); // resets to the empty polyline + int AddPoint ( Geom::Point const &iPt, bool mvto = false); // add point + int AddPoint ( Geom::Point const &iPt, int ip, double it, bool mvto = false); + int AddForcedPoint ( Geom::Point const &iPt); // add point + int AddForcedPoint ( Geom::Point const &iPt, int ip, double it); + int ReplacePoint(Geom::Point const &iPt); // replace point + + // transform in a polygon (in a graph, in fact; a subsequent call to ConvertToShape is needed) + // - fills the polyline; justAdd=true doesn't reset the Shape dest, but simply adds the polyline into it + // closeIfNeeded=false prevent the function from closing the path (resulting in a non-eulerian graph + // pathID is a identification number for the path, and is used for recomposing curves from polylines + // give each different Path a different ID, and feed the appropriate orig[] to the ConvertToForme() function + void Fill(Shape *dest, int pathID = -1, bool justAdd = false, + bool closeIfNeeded = true, bool invert = false); + + // - stroke the path; usual parameters: type of cap=butt, type of join=join and miter (see LivarotDefs.h) + // doClose treat the path as closed (ie a loop) + void Stroke(Shape *dest, bool doClose, double width, JoinType join, + ButtType butt, double miter, bool justAdd = false); + + // build a Path that is the outline of the Path instance's description (the result is stored in dest) + // it doesn't compute the exact offset (it's way too complicated, but an approximation made of cubic bezier patches + // and segments. the algorithm was found in a plugin for Impress (by Chris Cox), but i can't find it back... + void Outline(Path *dest, double width, JoinType join, ButtType butt, + double miter); + + // half outline with edges having the same direction as the original + void OutsideOutline(Path *dest, double width, JoinType join, ButtType butt, + double miter); + + // half outline with edges having the opposite direction as the original + void InsideOutline (Path * dest, double width, JoinType join, ButtType butt, + double miter); + + // polyline to cubic bezier patches + void Simplify (double treshhold); + + // description simplification + void Coalesce (double tresh); + + // utilities + // piece is a command no in the command list + // "at" is an abcissis on the path portion associated with this command + // 0=beginning of portion, 1=end of portion. + void PointAt (int piece, double at, Geom::Point & pos); + void PointAndTangentAt (int piece, double at, Geom::Point & pos, Geom::Point & tgt); + + // last control point before the command i (i included) + // used when dealing with quadratic bezier spline, cause these can contain arbitrarily many commands + const Geom::Point PrevPoint (const int i) const; + + // dash the polyline + // the result is stored in the polyline, so you lose the original. make a copy before if needed + void DashPolyline(float head,float tail,float body,int nbD,float *dashs,bool stPlain,float stOffset); + + void DashPolylineFromStyle(SPStyle *style, float scale, float min_len); + + //utilitaire pour inkscape + void LoadPath(Geom::Path const &path, Geom::Affine const &tr, bool doTransformation, bool append = false); + void LoadPathVector(Geom::PathVector const &pv, Geom::Affine const &tr, bool doTransformation); + void LoadPathVector(Geom::PathVector const &pv); + Geom::PathVector* MakePathVector(); + + void Transform(const Geom::Affine &trans); + + // decompose le chemin en ses sous-chemin + // killNoSurf=true -> oublie les chemins de surface nulle + Path** SubPaths(int &outNb,bool killNoSurf); + // pour recuperer les trous + // nbNest= nombre de contours + // conts= debut de chaque contour + // nesting= parent de chaque contour + Path** SubPathsWithNesting(int &outNb,bool killNoSurf,int nbNest,int* nesting,int* conts); + // surface du chemin (considere comme ferme) + double Surface(); + void PolylineBoundingBox(double &l,double &t,double &r,double &b); + void FastBBox(double &l,double &t,double &r,double &b); + // longueur (totale des sous-chemins) + double Length(); + + void ConvertForcedToMoveTo(); + void ConvertForcedToVoid(); + struct cut_position { + int piece; + double t; + }; + cut_position* CurvilignToPosition(int nbCv,double* cvAbs,int &nbCut); + cut_position PointToCurvilignPosition(Geom::Point const &pos, unsigned seg = 0) const; + //Should this take a cut_position as a param? + double PositionToLength(int piece, double t); + + // caution: not tested on quadratic b-splines, most certainly buggy + void ConvertPositionsToMoveTo(int nbPos,cut_position* poss); + void ConvertPositionsToForced(int nbPos,cut_position* poss); + + void Affiche(); + char *svg_dump_path() const; + + bool IsLineSegment(int piece); + + private: + // utilitary functions for the path construction + void CancelBezier (); + void CloseSubpath(); + void InsertMoveTo (Geom::Point const &iPt,int at); + void InsertForcePoint (int at); + void InsertLineTo (Geom::Point const &iPt,int at); + void InsertArcTo (Geom::Point const &ip, double iRx, double iRy, double angle, bool iLargeArc, bool iClockwise,int at); + void InsertCubicTo (Geom::Point const &ip, Geom::Point const &iStD, Geom::Point const &iEnD,int at); + void InsertBezierTo (Geom::Point const &iPt,int iNb,int at); + void InsertIntermBezierTo (Geom::Point const &iPt,int at); + + // creation of dashes: take the polyline given by spP (length spL) and dash it according to head, body, etc. put the result in + // the polyline of this instance + void DashSubPath(int spL, int spP, std::vector const &orig_pts, float head,float tail,float body,int nbD,float *dashs,bool stPlain,float stOffset); + + // Functions used by the conversion. + // they append points to the polyline + void DoArc ( Geom::Point const &iS, Geom::Point const &iE, double rx, double ry, + double angle, bool large, bool wise, double tresh); + void RecCubicTo ( Geom::Point const &iS, Geom::Point const &iSd, Geom::Point const &iE, Geom::Point const &iEd, double tresh, int lev, + double maxL = -1.0); + void RecBezierTo ( Geom::Point const &iPt, Geom::Point const &iS, Geom::Point const &iE, double treshhold, int lev, double maxL = -1.0); + + void DoArc ( Geom::Point const &iS, Geom::Point const &iE, double rx, double ry, + double angle, bool large, bool wise, double tresh, int piece); + void RecCubicTo ( Geom::Point const &iS, Geom::Point const &iSd, Geom::Point const &iE, Geom::Point const &iEd, double tresh, int lev, + double st, double et, int piece); + void RecBezierTo ( Geom::Point const &iPt, Geom::Point const &iS, const Geom::Point &iE, double treshhold, int lev, double st, double et, + int piece); + + // don't pay attention + struct offset_orig + { + Path *orig; + int piece; + double tSt, tEn; + double off_dec; + }; + void DoArc ( Geom::Point const &iS, Geom::Point const &iE, double rx, double ry, + double angle, bool large, bool wise, double tresh, int piece, + offset_orig & orig); + void RecCubicTo ( Geom::Point const &iS, Geom::Point const &iSd, Geom::Point const &iE, Geom::Point const &iEd, double tresh, int lev, + double st, double et, int piece, offset_orig & orig); + void RecBezierTo ( Geom::Point const &iPt, Geom::Point const &iS, Geom::Point const &iE, double treshhold, int lev, double st, double et, + int piece, offset_orig & orig); + + static void ArcAngles ( Geom::Point const &iS, Geom::Point const &iE, double rx, + double ry, double angle, bool large, bool wise, + double &sang, double &eang); + static void QuadraticPoint (double t, Geom::Point &oPt, Geom::Point const &iS, Geom::Point const &iM, Geom::Point const &iE); + static void CubicTangent (double t, Geom::Point &oPt, Geom::Point const &iS, + Geom::Point const &iSd, Geom::Point const &iE, + Geom::Point const &iEd); + + struct outline_callback_data + { + Path *orig; + int piece; + double tSt, tEn; + Path *dest; + double x1, y1, x2, y2; + union + { + struct + { + double dx1, dy1, dx2, dy2; + } + c; + struct + { + double mx, my; + } + b; + struct + { + double rx, ry, angle; + bool clock, large; + double stA, enA; + } + a; + } + d; + }; + + typedef void (outlineCallback) (outline_callback_data * data, double tol, double width); + struct outline_callbacks + { + outlineCallback *cubicto; + outlineCallback *bezierto; + outlineCallback *arcto; + }; + + void SubContractOutline (int off, int num_pd, + Path * dest, outline_callbacks & calls, + double tolerance, double width, JoinType join, + ButtType butt, double miter, bool closeIfNeeded, + bool skipMoveto, Geom::Point & lastP, Geom::Point & lastT); + void DoStroke(int off, int N, Shape *dest, bool doClose, double width, JoinType join, + ButtType butt, double miter, bool justAdd = false); + + static void TangentOnSegAt(double at, Geom::Point const &iS, PathDescrLineTo const &fin, + Geom::Point &pos, Geom::Point &tgt, double &len); + static void TangentOnArcAt(double at, Geom::Point const &iS, PathDescrArcTo const &fin, + Geom::Point &pos, Geom::Point &tgt, double &len, double &rad); + static void TangentOnCubAt (double at, Geom::Point const &iS, PathDescrCubicTo const &fin, bool before, + Geom::Point &pos, Geom::Point &tgt, double &len, double &rad); + static void TangentOnBezAt (double at, Geom::Point const &iS, + PathDescrIntermBezierTo & mid, + PathDescrBezierTo & fin, bool before, + Geom::Point & pos, Geom::Point & tgt, double &len, double &rad); + static void OutlineJoin (Path * dest, Geom::Point pos, Geom::Point stNor, Geom::Point enNor, + double width, JoinType join, double miter, int nType); + + static bool IsNulCurve (std::vector const &cmd, int curD, Geom::Point const &curX); + + static void RecStdCubicTo (outline_callback_data * data, double tol, + double width, int lev); + static void StdCubicTo (outline_callback_data * data, double tol, + double width); + static void StdBezierTo (outline_callback_data * data, double tol, + double width); + static void RecStdArcTo (outline_callback_data * data, double tol, + double width, int lev); + static void StdArcTo (outline_callback_data * data, double tol, double width); + + + // fonctions annexes pour le stroke + static void DoButt (Shape * dest, double width, ButtType butt, Geom::Point pos, + Geom::Point dir, int &leftNo, int &rightNo); + static void DoJoin (Shape * dest, double width, JoinType join, Geom::Point pos, + Geom::Point prev, Geom::Point next, double miter, double prevL, + double nextL, int *stNo, int *enNo); + static void DoLeftJoin (Shape * dest, double width, JoinType join, Geom::Point pos, + Geom::Point prev, Geom::Point next, double miter, double prevL, + double nextL, int &leftStNo, int &leftEnNo,int pathID=-1,int pieceID=0,double tID=0.0); + static void DoRightJoin (Shape * dest, double width, JoinType join, Geom::Point pos, + Geom::Point prev, Geom::Point next, double miter, double prevL, + double nextL, int &rightStNo, int &rightEnNo,int pathID=-1,int pieceID=0,double tID=0.0); + static void RecRound (Shape * dest, int sNo, int eNo, + Geom::Point const &iS, Geom::Point const &iE, + Geom::Point const &nS, Geom::Point const &nE, + Geom::Point &origine,float width); + + + void DoSimplify(int off, int N, double treshhold); + bool AttemptSimplify(int off, int N, double treshhold, PathDescrCubicTo &res, int &worstP); + static bool FitCubic(Geom::Point const &start, + PathDescrCubicTo &res, + double *Xk, double *Yk, double *Qk, double *tk, int nbPt); + + struct fitting_tables { + int nbPt,maxPt,inPt; + double *Xk; + double *Yk; + double *Qk; + double *tk; + double *lk; + char *fk; + double totLen; + }; + bool AttemptSimplify (fitting_tables &data,double treshhold, PathDescrCubicTo & res,int &worstP); + bool ExtendFit(int off, int N, fitting_tables &data,double treshhold, PathDescrCubicTo & res,int &worstP); + double RaffineTk (Geom::Point pt, Geom::Point p0, Geom::Point p1, Geom::Point p2, Geom::Point p3, double it); + void FlushPendingAddition(Path* dest,PathDescr *lastAddition,PathDescrCubicTo &lastCubic,int lastAD); + +private: + void AddCurve(Geom::Curve const &c); + +}; +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/PathConversion.cpp b/src/livarot/PathConversion.cpp new file mode 100644 index 0000000..737ecbe --- /dev/null +++ b/src/livarot/PathConversion.cpp @@ -0,0 +1,1579 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/transforms.h> +#include "Path.h" +#include "Shape.h" +#include "livarot/path-description.h" + +/* + * path description -> polyline + * and Path -> Shape (the Fill() function at the bottom) + * nathing fancy here: take each command and append an approximation of it to the polyline + */ + +void Path::ConvertWithBackData(double treshhold) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + + SetBackData(true); + ResetPoints(); + if ( descr_cmd.empty() ) { + return; + } + + Geom::Point curX; + int curP = 1; + int lastMoveTo = -1; + + // The initial moveto. + { + int const firstTyp = descr_cmd[0]->getType(); + if ( firstTyp == descr_moveto ) { + curX = dynamic_cast(descr_cmd[0])->p; + } else { + curP = 0; + curX[Geom::X] = curX[Geom::Y] = 0; + } + lastMoveTo = AddPoint(curX, 0, 0.0, true); + } + + // And the rest, one by one. + while ( curP < int(descr_cmd.size()) ) { + + int const nType = descr_cmd[curP]->getType(); + Geom::Point nextX; + + switch (nType) { + case descr_forced: { + AddForcedPoint(curX, curP, 1.0); + curP++; + break; + } + + case descr_moveto: { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + lastMoveTo = AddPoint(nextX, curP, 0.0, true); + // et on avance + curP++; + break; + } + + case descr_close: { + nextX = pts[lastMoveTo].p; + int n = AddPoint(nextX, curP, 1.0, false); + if (n > 0) pts[n].closed = true; + curP++; + break; + } + + case descr_lineto: { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + AddPoint(nextX,curP,1.0,false); + // et on avance + curP++; + break; + } + + case descr_cubicto: { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + RecCubicTo(curX, nData->start, nextX, nData->end, treshhold, 8, 0.0, 1.0, curP); + AddPoint(nextX, curP, 1.0, false); + // et on avance + curP++; + break; + } + + case descr_arcto: { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + DoArc(curX, nextX, nData->rx, nData->ry, nData->angle, nData->large, nData->clockwise, treshhold, curP); + AddPoint(nextX, curP, 1.0, false); + // et on avance + curP++; + break; + } + + case descr_bezierto: { + PathDescrBezierTo *nBData = dynamic_cast(descr_cmd[curP]); + int nbInterm = nBData->nb; + nextX = nBData->p; + + int ip = curP + 1; + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[ip]); + + if ( nbInterm >= 1 ) { + Geom::Point bx = curX; + Geom::Point dx = nData->p; + Geom::Point cx = 2 * bx - dx; + + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + for (int k = 0; k < nbInterm - 1; k++) { + bx = cx; + cx = dx; + + dx = nData->p; + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + Geom::Point stx; + stx = (bx + cx) / 2; + if ( k > 0 ) { + AddPoint(stx,curP - 1+k,1.0,false); + } + + { + Geom::Point mx; + mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8, 0.0, 1.0, curP + k); + } + } + { + bx = cx; + cx = dx; + + dx = nextX; + dx = 2 * dx - cx; + + Geom::Point stx; + stx = (bx + cx) / 2; + + if ( nbInterm > 1 ) { + AddPoint(stx, curP + nbInterm - 2, 1.0, false); + } + + { + Geom::Point mx; + mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8, 0.0, 1.0, curP + nbInterm - 1); + } + } + + } + + + AddPoint(nextX, curP - 1 + nbInterm, 1.0, false); + + // et on avance + curP += 1 + nbInterm; + break; + } + } + curX = nextX; + } +} + + +void Path::Convert(double treshhold) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + + SetBackData(false); + ResetPoints(); + if ( descr_cmd.empty() ) { + return; + } + + Geom::Point curX; + int curP = 1; + int lastMoveTo = 0; + + // le moveto + { + int const firstTyp = descr_cmd[0]->getType(); + if ( firstTyp == descr_moveto ) { + curX = dynamic_cast(descr_cmd[0])->p; + } else { + curP = 0; + curX[0] = curX[1] = 0; + } + lastMoveTo = AddPoint(curX, true); + } + descr_cmd[0]->associated = lastMoveTo; + + // et le reste, 1 par 1 + while ( curP < int(descr_cmd.size()) ) { + + int const nType = descr_cmd[curP]->getType(); + Geom::Point nextX; + + switch (nType) { + case descr_forced: { + descr_cmd[curP]->associated = AddForcedPoint(curX); + curP++; + break; + } + + case descr_moveto: { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + lastMoveTo = AddPoint(nextX, true); + descr_cmd[curP]->associated = lastMoveTo; + + // et on avance + curP++; + break; + } + + case descr_close: { + nextX = pts[lastMoveTo].p; + descr_cmd[curP]->associated = AddPoint(nextX, false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + if ( descr_cmd[curP]->associated > 0 ) { + pts[descr_cmd[curP]->associated].closed = true; + } + curP++; + break; + } + + case descr_lineto: { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + descr_cmd[curP]->associated = AddPoint(nextX, false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + // et on avance + curP++; + break; + } + + case descr_cubicto: { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + RecCubicTo(curX, nData->start, nextX, nData->end, treshhold, 8); + descr_cmd[curP]->associated = AddPoint(nextX,false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + // et on avance + curP++; + break; + } + + case descr_arcto: { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + DoArc(curX, nextX, nData->rx, nData->ry, nData->angle, nData->large, nData->clockwise, treshhold); + descr_cmd[curP]->associated = AddPoint(nextX, false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + // et on avance + curP++; + break; + } + + case descr_bezierto: { + PathDescrBezierTo *nBData = dynamic_cast(descr_cmd[curP]); + int nbInterm = nBData->nb; + nextX = nBData->p; + int curBD = curP; + + curP++; + int ip = curP; + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[ip]); + + if ( nbInterm == 1 ) { + Geom::Point const midX = nData->p; + RecBezierTo(midX, curX, nextX, treshhold, 8); + } else if ( nbInterm > 1 ) { + Geom::Point bx = curX; + Geom::Point dx = nData->p; + Geom::Point cx = 2 * bx - dx; + + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + for (int k = 0; k < nbInterm - 1; k++) { + bx = cx; + cx = dx; + + dx = nData->p; + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + Geom::Point stx = (bx + cx) / 2; + if ( k > 0 ) { + descr_cmd[ip - 2]->associated = AddPoint(stx, false); + if ( descr_cmd[ip - 2]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[ip - 2]->associated = 0; + } else { + descr_cmd[ip - 2]->associated = descr_cmd[ip - 3]->associated; + } + } + } + + { + Geom::Point const mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8); + } + } + + { + bx = cx; + cx = dx; + + dx = nextX; + dx = 2 * dx - cx; + + Geom::Point stx = (bx + cx) / 2; + + descr_cmd[ip - 1]->associated = AddPoint(stx, false); + if ( descr_cmd[ip - 1]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[ip - 1]->associated = 0; + } else { + descr_cmd[ip - 1]->associated = descr_cmd[ip - 2]->associated; + } + } + + { + Geom::Point mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8); + } + } + } + + descr_cmd[curBD]->associated = AddPoint(nextX, false); + if ( descr_cmd[curBD]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curBD]->associated = 0; + } else { + descr_cmd[curBD]->associated = descr_cmd[curBD - 1]->associated; + } + } + + // et on avance + curP += nbInterm; + break; + } + } + + curX = nextX; + } +} + +void Path::ConvertEvenLines(double treshhold) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + + SetBackData(false); + ResetPoints(); + if ( descr_cmd.empty() ) { + return; + } + + Geom::Point curX; + int curP = 1; + int lastMoveTo = 0; + + // le moveto + { + int const firstTyp = descr_cmd[0]->getType(); + if ( firstTyp == descr_moveto ) { + curX = dynamic_cast(descr_cmd[0])->p; + } else { + curP = 0; + curX[0] = curX[1] = 0; + } + lastMoveTo = AddPoint(curX, true); + } + descr_cmd[0]->associated = lastMoveTo; + + // et le reste, 1 par 1 + while ( curP < int(descr_cmd.size()) ) { + + int const nType = descr_cmd[curP]->getType(); + Geom::Point nextX; + + switch (nType) { + case descr_forced: { + descr_cmd[curP]->associated = AddForcedPoint(curX); + curP++; + break; + } + + case descr_moveto: { + PathDescrMoveTo* nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + lastMoveTo = AddPoint(nextX,true); + descr_cmd[curP]->associated = lastMoveTo; + + // et on avance + curP++; + break; + } + + case descr_close: { + nextX = pts[lastMoveTo].p; + { + Geom::Point nexcur; + nexcur = nextX - curX; + const double segL = Geom::L2(nexcur); + if ( (segL > treshhold) && (treshhold > 0) ) { + for (double i = treshhold; i < segL; i += treshhold) { + Geom::Point nX; + nX = (segL - i) * curX + i * nextX; + nX /= segL; + AddPoint(nX); + } + } + } + + descr_cmd[curP]->associated = AddPoint(nextX,false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + if ( descr_cmd[curP]->associated > 0 ) { + pts[descr_cmd[curP]->associated].closed = true; + } + curP++; + break; + } + + case descr_lineto: { + PathDescrLineTo* nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + Geom::Point nexcur = nextX - curX; + const double segL = L2(nexcur); + if ( (segL > treshhold) && (treshhold > 0)) { + for (double i = treshhold; i < segL; i += treshhold) { + Geom::Point nX = ((segL - i) * curX + i * nextX) / segL; + AddPoint(nX); + } + } + + descr_cmd[curP]->associated = AddPoint(nextX,false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + // et on avance + curP++; + break; + } + + case descr_cubicto: { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + RecCubicTo(curX, nData->start, nextX, nData->end, treshhold, 8, 4 * treshhold); + descr_cmd[curP]->associated = AddPoint(nextX, false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + // et on avance + curP++; + break; + } + + case descr_arcto: { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[curP]); + nextX = nData->p; + DoArc(curX, nextX, nData->rx, nData->ry, nData->angle, nData->large, nData->clockwise, treshhold); + descr_cmd[curP]->associated =AddPoint(nextX, false); + if ( descr_cmd[curP]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curP]->associated = 0; + } else { + descr_cmd[curP]->associated = descr_cmd[curP - 1]->associated; + } + } + + // et on avance + curP++; + break; + } + + case descr_bezierto: { + PathDescrBezierTo *nBData = dynamic_cast(descr_cmd[curP]); + int nbInterm = nBData->nb; + nextX = nBData->p; + int curBD = curP; + + curP++; + int ip = curP; + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[ip]); + + if ( nbInterm == 1 ) { + Geom::Point const midX = nData->p; + RecBezierTo(midX, curX, nextX, treshhold, 8, 4 * treshhold); + } else if ( nbInterm > 1 ) { + Geom::Point bx = curX; + Geom::Point dx = nData->p; + Geom::Point cx = 2 * bx - dx; + + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + for (int k = 0; k < nbInterm - 1; k++) { + bx = cx; + cx = dx; + dx = nData->p; + + ip++; + nData = dynamic_cast(descr_cmd[ip]); + + Geom::Point stx = (bx+cx) / 2; + if ( k > 0 ) { + descr_cmd[ip - 2]->associated = AddPoint(stx, false); + if ( descr_cmd[ip - 2]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[ip- 2]->associated = 0; + } else { + descr_cmd[ip - 2]->associated = descr_cmd[ip - 3]->associated; + } + } + } + + { + Geom::Point const mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8, 4 * treshhold); + } + } + + { + bx = cx; + cx = dx; + + dx = nextX; + dx = 2 * dx - cx; + + Geom::Point const stx = (bx + cx) / 2; + + descr_cmd[ip - 1]->associated = AddPoint(stx, false); + if ( descr_cmd[ip - 1]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[ip - 1]->associated = 0; + } else { + descr_cmd[ip - 1]->associated = descr_cmd[ip - 2]->associated; + } + } + + { + Geom::Point const mx = (cx + dx) / 2; + RecBezierTo(cx, stx, mx, treshhold, 8, 4 * treshhold); + } + } + } + + descr_cmd[curBD]->associated = AddPoint(nextX, false); + if ( descr_cmd[curBD]->associated < 0 ) { + if ( curP == 0 ) { + descr_cmd[curBD]->associated = 0; + } else { + descr_cmd[curBD]->associated = descr_cmd[curBD - 1]->associated; + } + } + + // et on avance + curP += nbInterm; + break; + } + } + if ( Geom::LInfty(curX - nextX) > 0.00001 ) { + curX = nextX; + } + } +} + +const Geom::Point Path::PrevPoint(int i) const +{ + /* TODO: I suspect this should assert `(unsigned) i < descr_nb'. We can probably change + the argument to unsigned. descr_nb should probably be changed to unsigned too. */ + g_assert( i >= 0 ); + switch ( descr_cmd[i]->getType() ) { + case descr_moveto: { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[i]); + return nData->p; + } + case descr_lineto: { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[i]); + return nData->p; + } + case descr_arcto: { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[i]); + return nData->p; + } + case descr_cubicto: { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[i]); + return nData->p; + } + case descr_bezierto: { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + return nData->p; + } + case descr_interm_bezier: + case descr_close: + case descr_forced: + return PrevPoint(i - 1); + default: + g_assert_not_reached(); + return Geom::Point(0, 0); + } +} + +// utilitaries: given a quadratic bezier curve (start point, control point, end point, ie that's a clamped curve), +// and an abcissis on it, get the point with that abcissis. +// warning: it's NOT a curvilign abcissis (or whatever you call that in english), so "t" is NOT the length of "start point"->"result point" +void Path::QuadraticPoint(double t, Geom::Point &oPt, + const Geom::Point &iS, const Geom::Point &iM, const Geom::Point &iE) +{ + Geom::Point const ax = iE - 2 * iM + iS; + Geom::Point const bx = 2 * iM - 2 * iS; + Geom::Point const cx = iS; + + oPt = t * t * ax + t * bx + cx; +} +// idem for cubic bezier patch +void Path::CubicTangent(double t, Geom::Point &oPt, const Geom::Point &iS, const Geom::Point &isD, + const Geom::Point &iE, const Geom::Point &ieD) +{ + Geom::Point const ax = ieD - 2 * iE + 2 * iS + isD; + Geom::Point const bx = 3 * iE - ieD - 2 * isD - 3 * iS; + Geom::Point const cx = isD; + + oPt = 3 * t * t * ax + 2 * t * bx + cx; +} + +// extract interesting info of a SVG arc description +static void ArcAnglesAndCenter(Geom::Point const &iS, Geom::Point const &iE, + double rx, double ry, double angle, + bool large, bool wise, + double &sang, double &eang, Geom::Point &dr); + +void Path::ArcAngles(const Geom::Point &iS, const Geom::Point &iE, + double rx, double ry, double angle, bool large, bool wise, double &sang, double &eang) +{ + Geom::Point dr; + ArcAnglesAndCenter(iS, iE, rx, ry, angle, large, wise, sang, eang, dr); +} + +/* N.B. If iS == iE then sang,eang,dr each become NaN. Probably a bug. */ +static void ArcAnglesAndCenter(Geom::Point const &iS, Geom::Point const &iE, + double rx, double ry, double angle, + bool large, bool wise, + double &sang, double &eang, Geom::Point &dr) +{ + Geom::Point se = iE - iS; + Geom::Point ca(cos(angle), sin(angle)); + Geom::Point cse(dot(ca, se), cross(ca, se)); + cse[0] /= rx; + cse[1] /= ry; + double const lensq = dot(cse,cse); + Geom::Point csd = ( ( lensq < 4 + ? sqrt( 1/lensq - .25 ) + : 0.0 ) + * cse.ccw() ); + + Geom::Point ra = -csd - 0.5 * cse; + if ( ra[0] <= -1 ) { + sang = M_PI; + } else if ( ra[0] >= 1 ) { + sang = 0; + } else { + sang = acos(ra[0]); + if ( ra[1] < 0 ) { + sang = 2 * M_PI - sang; + } + } + + ra = -csd + 0.5 * cse; + if ( ra[0] <= -1 ) { + eang = M_PI; + } else if ( ra[0] >= 1 ) { + eang = 0; + } else { + eang = acos(ra[0]); + if ( ra[1] < 0 ) { + eang = 2 * M_PI - eang; + } + } + + csd[0] *= rx; + csd[1] *= ry; + ca[1] = -ca[1]; // because it's the inverse rotation + + dr[0] = dot(ca, csd); + dr[1] = cross(ca, csd); + + ca[1] = -ca[1]; + + if ( wise ) { + + if (large) { + dr = -dr; + double swap = eang; + eang = sang; + sang = swap; + eang += M_PI; + sang += M_PI; + if ( eang >= 2*M_PI ) { + eang -= 2*M_PI; + } + if ( sang >= 2*M_PI ) { + sang -= 2*M_PI; + } + } + + } else { + if (!large) { + dr = -dr; + double swap = eang; + eang = sang; + sang = swap; + eang += M_PI; + sang += M_PI; + if ( eang >= 2*M_PI ) { + eang -= 2 * M_PI; + } + if ( sang >= 2*M_PI ) { + sang -= 2 * M_PI; + } + } + } + + dr += 0.5 * (iS + iE); +} + + + +void Path::DoArc(Geom::Point const &iS, Geom::Point const &iE, + double const rx, double const ry, double const angle, + bool const large, bool const wise, double const /*tresh*/) +{ + /* TODO: Check that our behaviour is standards-conformant if iS and iE are (much) further + apart than the diameter. Also check that we do the right thing for negative radius. + (Same for the other DoArc functions in this file.) */ + if ( rx <= 0.0001 || ry <= 0.0001 ) { + return; + // We always add a lineto afterwards, so this is fine. + // [on ajoute toujours un lineto apres, donc c bon] + } + + double sang; + double eang; + Geom::Point dr_temp; + ArcAnglesAndCenter(iS, iE, rx, ry, angle*M_PI/180.0, large, wise, sang, eang, dr_temp); + Geom::Point dr = dr_temp; + /* TODO: This isn't as good numerically as treating iS and iE as primary. E.g. consider + the case of low curvature (i.e. very large radius). */ + + Geom::Scale const ar(rx, ry); + Geom::Rotate cb(sang); + Geom::Rotate cbangle(angle*M_PI/180.0); + + if (wise) { + + double const incr = -0.1/sqrt(ar.vector().length()); + if ( sang < eang ) { + sang += 2*M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr ; b > eang ; b += incr) { + cb = omega * cb; + AddPoint( cb.vector() * ar * cbangle + dr ); + } + + } else { + + double const incr = 0.1/sqrt(ar.vector().length()); + if ( sang > eang ) { + sang -= 2*M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr ; b < eang ; b += incr) { + cb = omega * cb; + AddPoint( cb.vector() * ar * cbangle + dr); + } + } +} + + +void Path::RecCubicTo( Geom::Point const &iS, Geom::Point const &isD, + Geom::Point const &iE, Geom::Point const &ieD, + double tresh, int lev, double maxL) +{ + Geom::Point se = iE - iS; + const double dC = Geom::L2(se); + if ( dC < 0.01 ) { + + const double sC = dot(isD,isD); + const double eC = dot(ieD,ieD); + if ( sC < tresh && eC < tresh ) { + return; + } + + } else { + const double sC = fabs(cross(se, isD)) / dC; + const double eC = fabs(cross(se, ieD)) / dC; + if ( sC < tresh && eC < tresh ) { + // presque tt droit -> attention si on nous demande de bien subdiviser les petits segments + if ( maxL > 0 && dC > maxL ) { + if ( lev <= 0 ) { + return; + } + Geom::Point m = 0.5 * (iS + iE) + 0.125 * (isD - ieD); + Geom::Point md = 0.75 * (iE - iS) - 0.125 * (isD + ieD); + + Geom::Point hisD = 0.5 * isD; + Geom::Point hieD = 0.5 * ieD; + + RecCubicTo(iS, hisD, m, md, tresh, lev - 1, maxL); + AddPoint(m); + RecCubicTo(m, md, iE, hieD, tresh, lev - 1,maxL); + } + return; + } + } + + if ( lev <= 0 ) { + return; + } + + { + Geom::Point m = 0.5 * (iS + iE) + 0.125 * (isD - ieD); + Geom::Point md = 0.75 * (iE - iS) - 0.125 * (isD + ieD); + + Geom::Point hisD = 0.5 * isD; + Geom::Point hieD = 0.5 * ieD; + + RecCubicTo(iS, hisD, m, md, tresh, lev - 1, maxL); + AddPoint(m); + RecCubicTo(m, md, iE, hieD, tresh, lev - 1,maxL); + } +} + + + +void Path::RecBezierTo(const Geom::Point &iP, + const Geom::Point &iS, + const Geom::Point &iE, + double tresh, int lev, double maxL) +{ + if ( lev <= 0 ) { + return; + } + + Geom::Point ps = iS - iP; + Geom::Point pe = iE - iP; + Geom::Point se = iE - iS; + double s = fabs(cross(pe, ps)); + if ( s < tresh ) { + const double l = L2(se); + if ( maxL > 0 && l > maxL ) { + const Geom::Point m = 0.25 * (iS + iE + 2 * iP); + Geom::Point md = 0.5 * (iS + iP); + RecBezierTo(md, iS, m, tresh, lev - 1, maxL); + AddPoint(m); + md = 0.5 * (iP + iE); + RecBezierTo(md, m, iE, tresh, lev - 1, maxL); + } + return; + } + + { + const Geom::Point m = 0.25 * (iS + iE + 2 * iP); + Geom::Point md = 0.5 * (iS + iP); + RecBezierTo(md, iS, m, tresh, lev - 1, maxL); + AddPoint(m); + md = 0.5 * (iP + iE); + RecBezierTo(md, m, iE, tresh, lev - 1, maxL); + } +} + + +void Path::DoArc(Geom::Point const &iS, Geom::Point const &iE, + double const rx, double const ry, double const angle, + bool const large, bool const wise, double const /*tresh*/, int const piece) +{ + /* TODO: Check that our behaviour is standards-conformant if iS and iE are (much) further + apart than the diameter. Also check that we do the right thing for negative radius. + (Same for the other DoArc functions in this file.) */ + if ( rx <= 0.0001 || ry <= 0.0001 ) { + return; + // We always add a lineto afterwards, so this is fine. + // [on ajoute toujours un lineto apres, donc c bon] + } + + double sang; + double eang; + Geom::Point dr_temp; + ArcAnglesAndCenter(iS, iE, rx, ry, angle*M_PI/180.0, large, wise, sang, eang, dr_temp); + Geom::Point dr = dr_temp; + /* TODO: This isn't as good numerically as treating iS and iE as primary. E.g. consider + the case of low curvature (i.e. very large radius). */ + + Geom::Scale const ar(rx, ry); + Geom::Rotate cb(sang); + Geom::Rotate cbangle(angle*M_PI/180.0); + + if (wise) { + double const incr = -0.1/sqrt(ar.vector().length()); + if ( sang < eang ) { + sang += 2*M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr; b > eang; b += incr) { + cb = omega * cb; + AddPoint(cb.vector() * ar * cbangle + dr, piece, (sang - b) / (sang - eang)); + } + + } else { + + double const incr = 0.1/sqrt(ar.vector().length()); + if ( sang > eang ) { + sang -= 2 * M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr ; b < eang ; b += incr) { + cb = omega * cb; + AddPoint(cb.vector() * ar * cbangle + dr, piece, (b - sang) / (eang - sang)); + } + } +} + +void Path::RecCubicTo(Geom::Point const &iS, Geom::Point const &isD, + Geom::Point const &iE, Geom::Point const &ieD, + double tresh, int lev, double st, double et, int piece) +{ + const Geom::Point se = iE - iS; + const double dC = Geom::L2(se); + if ( dC < 0.01 ) { + const double sC = dot(isD, isD); + const double eC = dot(ieD, ieD); + if ( sC < tresh && eC < tresh ) { + return; + } + } else { + const double sC = fabs(cross(se, isD)) / dC; + const double eC = fabs(cross(se, ieD)) / dC; + if ( sC < tresh && eC < tresh ) { + return; + } + } + + if ( lev <= 0 ) { + return; + } + + Geom::Point m = 0.5 * (iS + iE) + 0.125 * (isD - ieD); + Geom::Point md = 0.75 * (iE - iS) - 0.125 * (isD + ieD); + double mt = (st + et) / 2; + + Geom::Point hisD = 0.5 * isD; + Geom::Point hieD = 0.5 * ieD; + + RecCubicTo(iS, hisD, m, md, tresh, lev - 1, st, mt, piece); + AddPoint(m, piece, mt); + RecCubicTo(m, md, iE, hieD, tresh, lev - 1, mt, et, piece); + +} + + + +void Path::RecBezierTo(Geom::Point const &iP, + Geom::Point const &iS, + Geom::Point const &iE, + double tresh, int lev, double st, double et, int piece) +{ + if ( lev <= 0 ) { + return; + } + + Geom::Point ps = iS - iP; + Geom::Point pe = iE - iP; + const double s = fabs(cross(pe, ps)); + if ( s < tresh ) { + return; + } + + { + const double mt = (st + et) / 2; + const Geom::Point m = 0.25 * (iS + iE + 2 * iP); + RecBezierTo(0.5 * (iS + iP), iS, m, tresh, lev - 1, st, mt, piece); + AddPoint(m, piece, mt); + RecBezierTo(0.5 * (iP + iE), m, iE, tresh, lev - 1, mt, et, piece); + } +} + + + +void Path::DoArc(Geom::Point const &iS, Geom::Point const &iE, + double const rx, double const ry, double const angle, + bool const large, bool const wise, double const /*tresh*/, + int const piece, offset_orig &/*orig*/) +{ + // Will never arrive here, as offsets are made of cubics. + // [on n'arrivera jamais ici, puisque les offsets sont fait de cubiques] + /* TODO: Check that our behaviour is standards-conformant if iS and iE are (much) further + apart than the diameter. Also check that we do the right thing for negative radius. + (Same for the other DoArc functions in this file.) */ + if ( rx <= 0.0001 || ry <= 0.0001 ) { + return; + // We always add a lineto afterwards, so this is fine. + // [on ajoute toujours un lineto apres, donc c bon] + } + + double sang; + double eang; + Geom::Point dr_temp; + ArcAnglesAndCenter(iS, iE, rx, ry, angle*M_PI/180.0, large, wise, sang, eang, dr_temp); + Geom::Point dr = dr_temp; + /* TODO: This isn't as good numerically as treating iS and iE as primary. E.g. consider + the case of low curvature (i.e. very large radius). */ + + Geom::Scale const ar(rx, ry); + Geom::Rotate cb(sang); + Geom::Rotate cbangle(angle*M_PI/180.0); + if (wise) { + + double const incr = -0.1/sqrt(ar.vector().length()); + if ( sang < eang ) { + sang += 2*M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr; b > eang ;b += incr) { + cb = omega * cb; + AddPoint(cb.vector() * ar * cbangle + dr, piece, (sang - b) / (sang - eang)); + } + + } else { + double const incr = 0.1/sqrt(ar.vector().length()); + if ( sang > eang ) { + sang -= 2*M_PI; + } + Geom::Rotate const omega(incr); + for (double b = sang + incr ; b < eang ; b += incr) { + cb = omega * cb; + AddPoint(cb.vector() * ar * cbangle + dr, piece, (b - sang) / (eang - sang)); + } + } +} + + +void Path::RecCubicTo(Geom::Point const &iS, Geom::Point const &isD, + Geom::Point const &iE, Geom::Point const &ieD, + double tresh, int lev, double st, double et, + int piece, offset_orig &orig) +{ + const Geom::Point se = iE - iS; + const double dC = Geom::L2(se); + bool doneSub = false; + if ( dC < 0.01 ) { + const double sC = dot(isD, isD); + const double eC = dot(ieD, ieD); + if ( sC < tresh && eC < tresh ) { + return; + } + } else { + const double sC = fabs(cross(se, isD)) / dC; + const double eC = fabs(cross(se, ieD)) / dC; + if ( sC < tresh && eC < tresh ) { + doneSub = true; + } + } + + if ( lev <= 0 ) { + doneSub = true; + } + + // test des inversions + bool stInv = false; + bool enInv = false; + { + Geom::Point os_pos; + Geom::Point os_tgt; + Geom::Point oe_pos; + Geom::Point oe_tgt; + + orig.orig->PointAndTangentAt(orig.piece, orig.tSt * (1 - st) + orig.tEn * st, os_pos, os_tgt); + orig.orig->PointAndTangentAt(orig.piece, orig.tSt * (1 - et) + orig.tEn * et, oe_pos, oe_tgt); + + + Geom::Point n_tgt = isD; + double si = dot(n_tgt, os_tgt); + if ( si < 0 ) { + stInv = true; + } + n_tgt = ieD; + si = dot(n_tgt, oe_tgt); + if ( si < 0 ) { + enInv = true; + } + if ( stInv && enInv ) { + + AddPoint(os_pos, -1, 0.0); + AddPoint(iE, piece, et); + AddPoint(iS, piece, st); + AddPoint(oe_pos, -1, 0.0); + return; + + } else if ( ( stInv && !enInv ) || ( !stInv && enInv ) ) { + return; + } + + } + + if ( ( !stInv && !enInv && doneSub ) || lev <= 0 ) { + return; + } + + { + const Geom::Point m = 0.5 * (iS+iE) + 0.125 * (isD - ieD); + const Geom::Point md = 0.75 * (iE - iS) - 0.125 * (isD + ieD); + const double mt = (st + et) / 2; + const Geom::Point hisD = 0.5 * isD; + const Geom::Point hieD = 0.5 * ieD; + + RecCubicTo(iS, hisD, m, md, tresh, lev - 1, st, mt, piece, orig); + AddPoint(m, piece, mt); + RecCubicTo(m, md, iE, hieD, tresh, lev - 1, mt, et, piece, orig); + } +} + + + +void Path::RecBezierTo(Geom::Point const &iP, Geom::Point const &iS,Geom::Point const &iE, + double tresh, int lev, double st, double et, + int piece, offset_orig& orig) +{ + bool doneSub = false; + if ( lev <= 0 ) { + return; + } + + const Geom::Point ps = iS - iP; + const Geom::Point pe = iE - iP; + const double s = fabs(cross(pe, ps)); + if ( s < tresh ) { + doneSub = true ; + } + + // test des inversions + bool stInv = false; + bool enInv = false; + { + Geom::Point os_pos; + Geom::Point os_tgt; + Geom::Point oe_pos; + Geom::Point oe_tgt; + Geom::Point n_tgt; + Geom::Point n_pos; + + double n_len; + double n_rad; + PathDescrIntermBezierTo mid(iP); + PathDescrBezierTo fin(iE, 1); + + TangentOnBezAt(0.0, iS, mid, fin, false, n_pos, n_tgt, n_len, n_rad); + orig.orig->PointAndTangentAt(orig.piece, orig.tSt * (1 - st) + orig.tEn * st, os_pos, os_tgt); + double si = dot(n_tgt, os_tgt); + if ( si < 0 ) { + stInv = true; + } + + TangentOnBezAt(1.0, iS, mid, fin, false, n_pos, n_tgt, n_len, n_rad); + orig.orig->PointAndTangentAt(orig.piece, orig.tSt * (1 - et) + orig.tEn * et, oe_pos, oe_tgt); + si = dot(n_tgt, oe_tgt); + if ( si < 0 ) { + enInv = true; + } + + if ( stInv && enInv ) { + AddPoint(os_pos, -1, 0.0); + AddPoint(iE, piece, et); + AddPoint(iS, piece, st); + AddPoint(oe_pos, -1, 0.0); + return; + } + } + + if ( !stInv && !enInv && doneSub ) { + return; + } + + { + double mt = (st + et) / 2; + Geom::Point m = 0.25 * (iS + iE + 2 * iP); + Geom::Point md = 0.5 * (iS + iP); + RecBezierTo(md, iS, m, tresh, lev - 1, st, mt, piece, orig); + AddPoint(m, piece, mt); + md = 0.5 * (iP + iE); + RecBezierTo(md, m, iE, tresh, lev - 1, mt, et, piece, orig); + } +} + + +/* + * put a polyline in a Shape instance, for further fun + * pathID is the ID you want this Path instance to be associated with, for when you're going to recompose the polyline + * in a path description ( you need to have prepared the back data for that, of course) + */ + +void Path::Fill(Shape* dest, int pathID, bool justAdd, bool closeIfNeeded, bool invert) +{ + if ( dest == nullptr ) { + return; + } + + if ( justAdd == false ) { + dest->Reset(pts.size(), pts.size()); + } + + if ( pts.size() <= 1 ) { + return; + } + + int first = dest->numberOfPoints(); + + if ( back ) { + dest->MakeBackData(true); + } + + if ( invert ) { + if ( back ) { + { + // invert && back && !weighted + for (auto & pt : pts) { + dest->AddPoint(pt.p); + } + int lastM = 0; + int curP = 1; + int pathEnd = 0; + bool closed = false; + int lEdge = -1; + + while ( curP < int(pts.size()) ) { + int sbp = curP; + int lm = lastM; + int prp = pathEnd; + + if ( pts[sbp].isMoveTo == polyline_moveto ) { + + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectStart(lEdge); + dest->ConnectStart(first + lastM, lEdge); + } else { + lEdge = dest->AddEdge(first + lastM, first+pathEnd); + if ( lEdge >= 0 ) { + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[lm].piece; + dest->ebData[lEdge].tSt = 1.0; + dest->ebData[lEdge].tEn = 0.0; + } + } + } + + lastM = curP; + pathEnd = curP; + closed = false; + lEdge = -1; + + } else { + + if ( Geom::LInfty(pts[sbp].p - pts[prp].p) >= 0.00001 ) { + lEdge = dest->AddEdge(first + curP, first + pathEnd); + if ( lEdge >= 0 ) { + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[sbp].piece; + if ( pts[sbp].piece == pts[prp].piece ) { + dest->ebData[lEdge].tSt = pts[sbp].t; + dest->ebData[lEdge].tEn = pts[prp].t; + } else { + dest->ebData[lEdge].tSt = pts[sbp].t; + dest->ebData[lEdge].tEn = 0.0; + } + } + pathEnd = curP; + if ( Geom::LInfty(pts[sbp].p - pts[lm].p) < 0.00001 ) { + closed = true; + } else { + closed = false; + } + } + } + + curP++; + } + + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectStart(lEdge); + dest->ConnectStart(first + lastM, lEdge); + } else { + int lm = lastM; + lEdge = dest->AddEdge(first + lastM, first + pathEnd); + if ( lEdge >= 0 ) { + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[lm].piece; + dest->ebData[lEdge].tSt = 1.0; + dest->ebData[lEdge].tEn = 0.0; + } + } + } + } + + } else { + + { + // invert && !back && !weighted + for (auto & pt : pts) { + dest->AddPoint(pt.p); + } + int lastM = 0; + int curP = 1; + int pathEnd = 0; + bool closed = false; + int lEdge = -1; + while ( curP < int(pts.size()) ) { + int sbp = curP; + int lm = lastM; + int prp = pathEnd; + if ( pts[sbp].isMoveTo == polyline_moveto ) { + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectStart(lEdge); + dest->ConnectStart(first + lastM, lEdge); + } else { + dest->AddEdge(first + lastM, first + pathEnd); + } + } + lastM = curP; + pathEnd = curP; + closed = false; + lEdge = -1; + } else { + if ( Geom::LInfty(pts[sbp].p - pts[prp].p) >= 0.00001 ) { + lEdge = dest->AddEdge(first+curP, first+pathEnd); + pathEnd = curP; + if ( Geom::LInfty(pts[sbp].p - pts[lm].p) < 0.00001 ) { + closed = true; + } else { + closed = false; + } + } + } + curP++; + } + + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectStart(lEdge); + dest->ConnectStart(first + lastM, lEdge); + } else { + dest->AddEdge(first + lastM, first + pathEnd); + } + } + + } + } + + } else { + + if ( back ) { + { + // !invert && back && !weighted + for (auto & pt : pts) { + dest->AddPoint(pt.p); + } + + int lastM = 0; + int curP = 1; + int pathEnd = 0; + bool closed = false; + int lEdge = -1; + while ( curP < int(pts.size()) ) { + int sbp = curP; + int lm = lastM; + int prp = pathEnd; + if ( pts[sbp].isMoveTo == polyline_moveto ) { + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectEnd(lEdge); + dest->ConnectEnd(first + lastM, lEdge); + } else { + lEdge = dest->AddEdge(first + pathEnd, first+lastM); + if ( lEdge >= 0 ) { + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[lm].piece; + dest->ebData[lEdge].tSt = 0.0; + dest->ebData[lEdge].tEn = 1.0; + } + } + } + lastM = curP; + pathEnd = curP; + closed = false; + lEdge = -1; + } else { + if ( Geom::LInfty(pts[sbp].p - pts[prp].p) >= 0.00001 ) { + lEdge = dest->AddEdge(first + pathEnd, first + curP); + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[sbp].piece; + if ( pts[sbp].piece == pts[prp].piece ) { + dest->ebData[lEdge].tSt = pts[prp].t; + dest->ebData[lEdge].tEn = pts[sbp].t; + } else { + dest->ebData[lEdge].tSt = 0.0; + dest->ebData[lEdge].tEn = pts[sbp].t; + } + pathEnd = curP; + if ( Geom::LInfty(pts[sbp].p - pts[lm].p) < 0.00001 ) { + closed = true; + } else { + closed = false; + } + } + } + curP++; + } + + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectEnd(lEdge); + dest->ConnectEnd(first + lastM, lEdge); + } else { + int lm = lastM; + lEdge = dest->AddEdge(first + pathEnd, first + lastM); + if ( lEdge >= 0 ) { + dest->ebData[lEdge].pathID = pathID; + dest->ebData[lEdge].pieceID = pts[lm].piece; + dest->ebData[lEdge].tSt = 0.0; + dest->ebData[lEdge].tEn = 1.0; + } + } + } + } + + } else { + { + // !invert && !back && !weighted + for (auto & pt : pts) { + dest->AddPoint(pt.p); + } + + int lastM = 0; + int curP = 1; + int pathEnd = 0; + bool closed = false; + int lEdge = -1; + while ( curP < int(pts.size()) ) { + int sbp = curP; + int lm = lastM; + int prp = pathEnd; + if ( pts[sbp].isMoveTo == polyline_moveto ) { + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectEnd(lEdge); + dest->ConnectEnd(first + lastM, lEdge); + } else { + dest->AddEdge(first + pathEnd, first + lastM); + } + } + lastM = curP; + pathEnd = curP; + closed = false; + lEdge = -1; + } else { + if ( Geom::LInfty(pts[sbp].p - pts[prp].p) >= 0.00001 ) { + lEdge = dest->AddEdge(first+pathEnd, first+curP); + pathEnd = curP; + if ( Geom::LInfty(pts[sbp].p - pts[lm].p) < 0.00001 ) { + closed = true; + } else { + closed = false; + } + } + } + curP++; + } + + if ( closeIfNeeded ) { + if ( closed && lEdge >= 0 ) { + dest->DisconnectEnd(lEdge); + dest->ConnectEnd(first + lastM, lEdge); + } else { + dest->AddEdge(first + pathEnd, first + lastM); + } + } + + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/PathCutting.cpp b/src/livarot/PathCutting.cpp new file mode 100644 index 0000000..ae164c4 --- /dev/null +++ b/src/livarot/PathCutting.cpp @@ -0,0 +1,1534 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PathCutting.cpp + * nlivarot + * + * Created by fred on someday in 2004. + * public domain + * + * Additional Code by Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include "Path.h" +#include "style.h" +#include "livarot/path-description.h" +#include <2geom/pathvector.h> +#include <2geom/point.h> +#include <2geom/affine.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/curves.h> +#include "helper/geom-curves.h" +#include "helper/geom.h" + +#include "svg/svg.h" + +void Path::DashPolyline(float head,float tail,float body,int nbD,float *dashs,bool stPlain,float stOffset) +{ + if ( nbD <= 0 || body <= 0.0001 ) return; // pas de tirets, en fait + + std::vector orig_pts = pts; + pts.clear(); + + int lastMI=-1; + int curP = 0; + int lastMP = -1; + + for (int i = 0; i < int(orig_pts.size()); i++) { + if ( orig_pts[curP].isMoveTo == polyline_moveto ) { + if ( lastMI >= 0 && lastMI < i-1 ) { // au moins 2 points + DashSubPath(i-lastMI,lastMP, orig_pts, head,tail,body,nbD,dashs,stPlain,stOffset); + } + lastMI=i; + lastMP=curP; + } + curP++; + } + if ( lastMI >= 0 && lastMI < int(orig_pts.size()) - 1 ) { + DashSubPath(orig_pts.size() - lastMI, lastMP, orig_pts, head, tail, body, nbD, dashs, stPlain, stOffset); + } +} + +void Path::DashPolylineFromStyle(SPStyle *style, float scale, float min_len) +{ + if (!style->stroke_dasharray.values.empty()) { + + double dlen = 0.0; + // Find total length + for (auto & value : style->stroke_dasharray.values) { + dlen += value.value * scale; + } + if (dlen >= min_len) { + // Extract out dash pattern (relative positions) + double dash_offset = style->stroke_dashoffset.value * scale; + size_t n_dash = style->stroke_dasharray.values.size(); + double *dash = g_new(double, n_dash); + for (unsigned i = 0; i < n_dash; i++) { + dash[i] = style->stroke_dasharray.values[i].value * scale; + } + + // Convert relative positions to absolute positions + int nbD = n_dash; + float *dashs=(float*)malloc((nbD+1)*sizeof(float)); + while ( dash_offset >= dlen ) dash_offset-=dlen; + dashs[0]=dash[0]; + for (int i=1; iDashPolyline(0.0, 0.0, dlen, nbD, dashs, true, dash_offset); + + free(dashs); + g_free(dash); + } + } +} + + +void Path::DashSubPath(int spL, int spP, std::vector const &orig_pts, float head,float tail,float body,int nbD,float *dashs,bool stPlain,float stOffset) +{ + if ( spL <= 0 || spP == -1 ) return; + + double totLength=0; + Geom::Point lastP; + lastP = orig_pts[spP].p; + for (int i=1;i 0.0001 ) { + totLength+=nl; + lastP=n; + } + } + + if ( totLength <= head+tail ) return; // tout mange par la tete et la queue + + double curLength=0; + double dashPos=0; + int dashInd=0; + bool dashPlain=false; + double lastT=0; + int lastPiece=-1; + lastP = orig_pts[spP].p; + for (int i=1;i 0.0001 ) { + double stLength=curLength; + double enLength=curLength+nl; + // couper les bouts en trop + if ( curLength <= head && curLength+nl > head ) { + nl-=head-curLength; + curLength=head; + dashInd=0; + dashPos=stOffset; + bool nPlain=stPlain; + while ( dashs[dashInd] < stOffset ) { + dashInd++; + nPlain=!(nPlain); + if ( dashInd >= nbD ) { + dashPos=0; + dashInd=0; + break; + } + } + if ( nPlain == true && dashPlain == false ) { + Geom::Point p=(enLength-curLength)*lastP+(curLength-stLength)*n; + p/=(enLength-stLength); + if ( back ) { + double pT=0; + if ( nPiece == lastPiece ) { + pT=(lastT*(enLength-curLength)+nT*(curLength-stLength))/(enLength-stLength); + } else { + pT=(nPiece*(curLength-stLength))/(enLength-stLength); + } + AddPoint(p,nPiece,pT,true); + } else { + AddPoint(p,true); + } + } else if ( nPlain == false && dashPlain == true ) { + } + dashPlain=nPlain; + } + // faire les tirets + if ( curLength >= head /*&& curLength+nl <= totLength-tail*/ ) { + while ( curLength <= totLength-tail && nl > 0 ) { + if ( enLength <= totLength-tail ) nl=enLength-curLength; else nl=totLength-tail-curLength; + double leftInDash=body-dashPos; + if ( dashInd < nbD ) { + leftInDash=dashs[dashInd]-dashPos; + } + if ( leftInDash <= nl ) { + bool nPlain=false; + if ( dashInd < nbD ) { + dashPos=dashs[dashInd]; + dashInd++; + if ( dashPlain ) nPlain=false; else nPlain=true; + } else { + dashInd=0; + dashPos=0; + //nPlain=stPlain; + nPlain=dashPlain; + } + if ( nPlain == true && dashPlain == false ) { + Geom::Point p=(enLength-curLength-leftInDash)*lastP+(curLength+leftInDash-stLength)*n; + p/=(enLength-stLength); + if ( back ) { + double pT=0; + if ( nPiece == lastPiece ) { + pT=(lastT*(enLength-curLength-leftInDash)+nT*(curLength+leftInDash-stLength))/(enLength-stLength); + } else { + pT=(nPiece*(curLength+leftInDash-stLength))/(enLength-stLength); + } + AddPoint(p,nPiece,pT,true); + } else { + AddPoint(p,true); + } + } else if ( nPlain == false && dashPlain == true ) { + Geom::Point p=(enLength-curLength-leftInDash)*lastP+(curLength+leftInDash-stLength)*n; + p/=(enLength-stLength); + if ( back ) { + double pT=0; + if ( nPiece == lastPiece ) { + pT=(lastT*(enLength-curLength-leftInDash)+nT*(curLength+leftInDash-stLength))/(enLength-stLength); + } else { + pT=(nPiece*(curLength+leftInDash-stLength))/(enLength-stLength); + } + AddPoint(p,nPiece,pT,false); + } else { + AddPoint(p,false); + } + } + dashPlain=nPlain; + + curLength+=leftInDash; + nl-=leftInDash; + } else { + dashPos+=nl; + curLength+=nl; + nl=0; + } + } + if ( dashPlain ) { + if ( back ) { + AddPoint(n,nPiece,nT,false); + } else { + AddPoint(n,false); + } + } + nl=enLength-curLength; + } + if ( curLength <= totLength-tail && curLength+nl > totLength-tail ) { + nl=totLength-tail-curLength; + dashInd=0; + dashPos=0; + bool nPlain=false; + if ( nPlain == true && dashPlain == false ) { + } else if ( nPlain == false && dashPlain == true ) { + Geom::Point p=(enLength-curLength)*lastP+(curLength-stLength)*n; + p/=(enLength-stLength); + if ( back ) { + double pT=0; + if ( nPiece == lastPiece ) { + pT=(lastT*(enLength-curLength)+nT*(curLength-stLength))/(enLength-stLength); + } else { + pT=(nPiece*(curLength-stLength))/(enLength-stLength); + } + AddPoint(p,nPiece,pT,false); + } else { + AddPoint(p,false); + } + } + dashPlain=nPlain; + } + // continuer + curLength=enLength; + lastP=n; + lastPiece=nPiece; + lastT=nT; + } + } +} + +Geom::PathVector * +Path::MakePathVector() +{ + Geom::PathVector *pv = new Geom::PathVector(); + Geom::Path * currentpath = nullptr; + + Geom::Point lastP,bezSt,bezEn; + int bezNb=0; + for (int i=0;igetType(); + switch ( typ ) { + case descr_close: + { + currentpath->close(true); + } + break; + + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[i]); + currentpath->appendNew(Geom::Point(nData->p[0], nData->p[1])); + lastP = nData->p; + } + break; + + case descr_moveto: + { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[i]); + pv->push_back(Geom::Path()); + currentpath = &pv->back(); + currentpath->start(Geom::Point(nData->p[0], nData->p[1])); + lastP = nData->p; + } + break; + + case descr_arcto: + { + /* TODO: add testcase for this descr_arcto case */ + PathDescrArcTo *nData = dynamic_cast(descr_cmd[i]); + currentpath->appendNew( nData->rx, nData->ry, nData->angle*M_PI/180.0, nData->large, !nData->clockwise, nData->p ); + lastP = nData->p; + } + break; + + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[i]); + gdouble x1=lastP[0]+0.333333*nData->start[0]; + gdouble y1=lastP[1]+0.333333*nData->start[1]; + gdouble x2=nData->p[0]-0.333333*nData->end[0]; + gdouble y2=nData->p[1]-0.333333*nData->end[1]; + gdouble x3=nData->p[0]; + gdouble y3=nData->p[1]; + currentpath->appendNew( Geom::Point(x1,y1) , Geom::Point(x2,y2) , Geom::Point(x3,y3) ); + lastP = nData->p; + } + break; + + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + if ( nData->nb <= 0 ) { + currentpath->appendNew( Geom::Point(nData->p[0], nData->p[1]) ); + bezNb=0; + } else if ( nData->nb == 1 ){ + PathDescrIntermBezierTo *iData = dynamic_cast(descr_cmd[i+1]); + gdouble x1=0.333333*(lastP[0]+2*iData->p[0]); + gdouble y1=0.333333*(lastP[1]+2*iData->p[1]); + gdouble x2=0.333333*(nData->p[0]+2*iData->p[0]); + gdouble y2=0.333333*(nData->p[1]+2*iData->p[1]); + gdouble x3=nData->p[0]; + gdouble y3=nData->p[1]; + currentpath->appendNew( Geom::Point(x1,y1) , Geom::Point(x2,y2) , Geom::Point(x3,y3) ); + bezNb=0; + } else { + bezSt = 2*lastP-nData->p; + bezEn = nData->p; + bezNb = nData->nb; + } + lastP = nData->p; + } + break; + + case descr_interm_bezier: + { + if ( bezNb > 0 ) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[i]); + Geom::Point p_m=nData->p,p_s=0.5*(bezSt+p_m),p_e; + if ( bezNb > 1 ) { + PathDescrIntermBezierTo *iData = dynamic_cast(descr_cmd[i+1]); + p_e=0.5*(p_m+iData->p); + } else { + p_e=bezEn; + } + + Geom::Point cp1=0.333333*(p_s+2*p_m),cp2=0.333333*(2*p_m+p_e); + gdouble x1=cp1[0]; + gdouble y1=cp1[1]; + gdouble x2=cp2[0]; + gdouble y2=cp2[1]; + gdouble x3=p_e[0]; + gdouble y3=p_e[1]; + currentpath->appendNew( Geom::Point(x1,y1) , Geom::Point(x2,y2) , Geom::Point(x3,y3) ); + + bezNb--; + } + } + break; + } + } + + return pv; +} + +void Path::AddCurve(Geom::Curve const &c) +{ + if( is_straight_curve(c) ) + { + LineTo( c.finalPoint() ); + } + /* + else if(Geom::QuadraticBezier const *quadratic_bezier = dynamic_cast(c)) { + ... + } + */ + else if(Geom::CubicBezier const *cubic_bezier = dynamic_cast(&c)) { + Geom::Point tmp = (*cubic_bezier)[3]; + Geom::Point tms = 3 * ((*cubic_bezier)[1] - (*cubic_bezier)[0]); + Geom::Point tme = 3 * ((*cubic_bezier)[3] - (*cubic_bezier)[2]); + CubicTo (tmp, tms, tme); + } + else if(Geom::EllipticalArc const *elliptical_arc = dynamic_cast(&c)) { + ArcTo( elliptical_arc->finalPoint(), + elliptical_arc->ray(Geom::X), elliptical_arc->ray(Geom::Y), + elliptical_arc->rotationAngle()*180.0/M_PI, // convert from radians to degrees + elliptical_arc->largeArc(), !elliptical_arc->sweep() ); + } else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + //recurse to convert the new path resulting from the sbasis to svgd + for(const auto & iter : sbasis_path) { + AddCurve(iter); + } + } +} + +/** append is false by default: it means that the path should be resetted. If it is true, the path is not resetted and Geom::Path will be appended as a new path + */ +void Path::LoadPath(Geom::Path const &path, Geom::Affine const &tr, bool doTransformation, bool append) +{ + if (!append) { + SetBackData (false); + Reset(); + } + if (path.empty()) + return; + + // TODO: this can be optimized by not generating a new path here, but doing the transform in AddCurve + // directly on the curve parameters + + Geom::Path const pathtr = doTransformation ? path * tr : path; + + MoveTo( pathtr.initialPoint() ); + + for(const auto & cit : pathtr) { + AddCurve(cit); + } + + if (pathtr.closed()) { + Close(); + } +} + +void Path::LoadPathVector(Geom::PathVector const &pv) +{ + LoadPathVector(pv, Geom::Affine(), false); +} + +void Path::LoadPathVector(Geom::PathVector const &pv, Geom::Affine const &tr, bool doTransformation) +{ + SetBackData (false); + Reset(); + + // FIXME: 2geom is currently unable to maintain SVGElliptical arcs through transformation, and + // sometimes it crashes on a continuity error during conversions, therefore convert to beziers here. + // (the fix is of course to fix 2geom and then remove this if-statement, and just execute the 'else'-clause) + if (doTransformation) { + Geom::PathVector pvbezier = pathv_to_linear_and_cubic_beziers(pv); + for(const auto & it : pvbezier) { + LoadPath(it, tr, doTransformation, true); + } + } else { + for(const auto & it : pv) { + LoadPath(it, tr, doTransformation, true); + } + } +} + +/** + * \return Length of the lines in the pts vector. + */ + +double Path::Length() +{ + if ( pts.empty() ) { + return 0; + } + + Geom::Point lastP = pts[0].p; + + double len = 0; + for (const auto & pt : pts) { + + if ( pt.isMoveTo != polyline_moveto ) { + len += Geom::L2(pt.p - lastP); + } + + lastP = pt.p; + } + + return len; +} + + +double Path::Surface() +{ + if ( pts.empty() ) { + return 0; + } + + Geom::Point lastM = pts[0].p; + Geom::Point lastP = lastM; + + double surf = 0; + for (const auto & pt : pts) { + + if ( pt.isMoveTo == polyline_moveto ) { + surf += Geom::cross(lastM, lastM - lastP); + lastP = lastM = pt.p; + } else { + surf += Geom::cross(pt.p, pt.p - lastP); + lastP = pt.p; + } + + } + + return surf; +} + + +Path** Path::SubPaths(int &outNb,bool killNoSurf) +{ + int nbRes=0; + Path** res=nullptr; + Path* curAdd=nullptr; + + for (auto & i : descr_cmd) { + int const typ = i->getType(); + switch ( typ ) { + case descr_moveto: + if ( curAdd ) { + if ( curAdd->descr_cmd.size() > 1 ) { + curAdd->Convert(1.0); + double addSurf=curAdd->Surface(); + if ( fabs(addSurf) > 0.0001 || killNoSurf == false ) { + res=(Path**)g_realloc(res,(nbRes+1)*sizeof(Path*)); + res[nbRes++]=curAdd; + } else { + delete curAdd; + } + } else { + delete curAdd; + } + curAdd=nullptr; + } + curAdd=new Path; + curAdd->SetBackData(false); + { + PathDescrMoveTo *nData = dynamic_cast(i); + curAdd->MoveTo(nData->p); + } + break; + case descr_close: + { + curAdd->Close(); + } + break; + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(i); + curAdd->LineTo(nData->p); + } + break; + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(i); + curAdd->CubicTo(nData->p,nData->start,nData->end); + } + break; + case descr_arcto: + { + PathDescrArcTo *nData = dynamic_cast(i); + curAdd->ArcTo(nData->p,nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + } + break; + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(i); + curAdd->BezierTo(nData->p); + } + break; + case descr_interm_bezier: + { + PathDescrIntermBezierTo *nData = dynamic_cast(i); + curAdd->IntermBezierTo(nData->p); + } + break; + default: + break; + } + } + if ( curAdd ) { + if ( curAdd->descr_cmd.size() > 1 ) { + curAdd->Convert(1.0); + double addSurf=curAdd->Surface(); + if ( fabs(addSurf) > 0.0001 || killNoSurf == false ) { + res=(Path**)g_realloc(res,(nbRes+1)*sizeof(Path*)); + res[nbRes++]=curAdd; + } else { + delete curAdd; + } + } else { + delete curAdd; + } + } + curAdd=nullptr; + + outNb=nbRes; + return res; +} +Path** Path::SubPathsWithNesting(int &outNb,bool killNoSurf,int nbNest,int* nesting,int* conts) +{ + int nbRes=0; + Path** res=nullptr; + Path* curAdd=nullptr; + bool increment=false; + + for (int i=0;igetType(); + switch ( typ ) { + case descr_moveto: + { + if ( curAdd && increment == false ) { + if ( curAdd->descr_cmd.size() > 1 ) { + // sauvegarder descr_cmd[0]->associated + int savA=curAdd->descr_cmd[0]->associated; + curAdd->Convert(1.0); + curAdd->descr_cmd[0]->associated=savA; // associated n'est pas utilise apres + double addSurf=curAdd->Surface(); + if ( fabs(addSurf) > 0.0001 || killNoSurf == false ) { + res=(Path**)g_realloc(res,(nbRes+1)*sizeof(Path*)); + res[nbRes++]=curAdd; + } else { + delete curAdd; + } + } else { + delete curAdd; + } + curAdd=nullptr; + } + Path* hasParent=nullptr; + for (int j=0;j= 0 ) { + int parentMvt=conts[nesting[j]]; + for (int k=0;kdescr_cmd.empty() == false && res[k]->descr_cmd[0]->associated == parentMvt ) { + hasParent=res[k]; + break; + } + } + } + if ( conts[j] > i ) break; + } + if ( hasParent ) { + curAdd=hasParent; + increment=true; + } else { + curAdd=new Path; + curAdd->SetBackData(false); + increment=false; + } + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[i]); + int mNo=curAdd->MoveTo(nData->p); + curAdd->descr_cmd[mNo]->associated=i; + } + break; + case descr_close: + { + curAdd->Close(); + } + break; + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[i]); + curAdd->LineTo(nData->p); + } + break; + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[i]); + curAdd->CubicTo(nData->p,nData->start,nData->end); + } + break; + case descr_arcto: + { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[i]); + curAdd->ArcTo(nData->p,nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + } + break; + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + curAdd->BezierTo(nData->p); + } + break; + case descr_interm_bezier: + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[i]); + curAdd->IntermBezierTo(nData->p); + } + break; + default: + break; + } + } + if ( curAdd && increment == false ) { + if ( curAdd->descr_cmd.size() > 1 ) { + curAdd->Convert(1.0); + double addSurf=curAdd->Surface(); + if ( fabs(addSurf) > 0.0001 || killNoSurf == false ) { + res=(Path**)g_realloc(res,(nbRes+1)*sizeof(Path*)); + res[nbRes++]=curAdd; + } else { + delete curAdd; + } + } else { + delete curAdd; + } + } + curAdd=nullptr; + + outNb=nbRes; + return res; +} + + +void Path::ConvertForcedToVoid() +{ + for (int i=0; i < int(descr_cmd.size()); i++) { + if ( descr_cmd[i]->getType() == descr_forced) { + delete descr_cmd[i]; + descr_cmd.erase(descr_cmd.begin() + i); + } + } +} + + +void Path::ConvertForcedToMoveTo() +{ + Geom::Point lastSeen(0, 0); + Geom::Point lastMove(0, 0); + + { + Geom::Point lastPos(0, 0); + for (int i = int(descr_cmd.size()) - 1; i >= 0; i--) { + int const typ = descr_cmd[i]->getType(); + switch ( typ ) { + case descr_forced: + { + PathDescrForced *d = dynamic_cast(descr_cmd[i]); + d->p = lastPos; + break; + } + case descr_close: + { + PathDescrClose *d = dynamic_cast(descr_cmd[i]); + d->p = lastPos; + break; + } + case descr_moveto: + { + PathDescrMoveTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_lineto: + { + PathDescrLineTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_arcto: + { + PathDescrArcTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_cubicto: + { + PathDescrCubicTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_bezierto: + { + PathDescrBezierTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_interm_bezier: + { + PathDescrIntermBezierTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + default: + break; + } + } + } + + bool hasMoved = false; + for (int i = 0; i < int(descr_cmd.size()); i++) { + int const typ = descr_cmd[i]->getType(); + switch ( typ ) { + case descr_forced: + if ( i < int(descr_cmd.size()) - 1 && hasMoved ) { // sinon il termine le chemin + + delete descr_cmd[i]; + descr_cmd[i] = new PathDescrMoveTo(lastSeen); + lastMove = lastSeen; + hasMoved = true; + } + break; + + case descr_moveto: + { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[i]); + lastMove = lastSeen = nData->p; + hasMoved = true; + } + break; + case descr_close: + { + lastSeen=lastMove; + } + break; + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[i]); + lastSeen=nData->p; + } + break; + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[i]); + lastSeen=nData->p; + } + break; + case descr_arcto: + { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[i]); + lastSeen=nData->p; + } + break; + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + lastSeen=nData->p; + } + break; + case descr_interm_bezier: + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[i]); + lastSeen=nData->p; + } + break; + default: + break; + } + } +} +static int CmpPosition(const void * p1, const void * p2) { + Path::cut_position *cp1=(Path::cut_position*)p1; + Path::cut_position *cp2=(Path::cut_position*)p2; + if ( cp1->piece < cp2->piece ) return -1; + if ( cp1->piece > cp2->piece ) return 1; + if ( cp1->t < cp2->t ) return -1; + if ( cp1->t > cp2->t ) return 1; + return 0; +} +static int CmpCurv(const void * p1, const void * p2) { + double *cp1=(double*)p1; + double *cp2=(double*)p2; + if ( *cp1 < *cp2 ) return -1; + if ( *cp1 > *cp2 ) return 1; + return 0; +} + + +Path::cut_position* Path::CurvilignToPosition(int nbCv, double *cvAbs, int &nbCut) +{ + if ( nbCv <= 0 || pts.empty() || back == false ) { + return nullptr; + } + + qsort(cvAbs, nbCv, sizeof(double), CmpCurv); + + cut_position *res = nullptr; + nbCut = 0; + int curCv = 0; + + double len = 0; + double lastT = 0; + int lastPiece = -1; + + Geom::Point lastM = pts[0].p; + Geom::Point lastP = lastM; + + for (const auto & pt : pts) { + + if ( pt.isMoveTo == polyline_moveto ) { + + lastP = lastM = pt.p; + lastT = pt.t; + lastPiece = pt.piece; + + } else { + + double const add = Geom::L2(pt.p - lastP); + double curPos = len; + double curAdd = add; + + while ( curAdd > 0.0001 && curCv < nbCv && curPos + curAdd >= cvAbs[curCv] ) { + double const theta = (cvAbs[curCv] - len) / add; + res = (cut_position*) g_realloc(res, (nbCut + 1) * sizeof(cut_position)); + res[nbCut].piece = pt.piece; + res[nbCut].t = theta * pt.t + (1 - theta) * ( (lastPiece != pt.piece) ? 0 : lastT); + nbCut++; + curAdd -= cvAbs[curCv] - curPos; + curPos = cvAbs[curCv]; + curCv++; + } + + len += add; + lastPiece = pt.piece; + lastP = pt.p; + lastT = pt.t; + } + } + + return res; +} + +/* +Moved from Layout-TNG-OutIter.cpp +TODO: clean up uses of the original function and remove + +Original Comment: +"this function really belongs to Path. I'll probably move it there eventually, +hence the Path-esque coding style" + +*/ +template inline static T square(T x) {return x*x;} +Path::cut_position Path::PointToCurvilignPosition(Geom::Point const &pos, unsigned seg) const +{ + // if the parameter "seg" == 0, then all segments will be considered + // In however e.g. "seg" == 6 , then only the 6th segment will be considered + + unsigned bestSeg = 0; + double bestRangeSquared = DBL_MAX; + double bestT = 0.0; // you need a sentinel, or make sure that you prime with correct values. + + for (unsigned i = 1 ; i < pts.size() ; i++) { + if (pts[i].isMoveTo == polyline_moveto || (seg > 0 && i != seg)) continue; + Geom::Point p1, p2, localPos; + double thisRangeSquared; + double t; + + if (pts[i - 1].p == pts[i].p) { + thisRangeSquared = square(pts[i].p[Geom::X] - pos[Geom::X]) + square(pts[i].p[Geom::Y] - pos[Geom::Y]); + t = 0.0; + } else { + // we rotate all our coordinates so we're always looking at a mostly vertical line. + if (fabs(pts[i - 1].p[Geom::X] - pts[i].p[Geom::X]) < fabs(pts[i - 1].p[Geom::Y] - pts[i].p[Geom::Y])) { + p1 = pts[i - 1].p; + p2 = pts[i].p; + localPos = pos; + } else { + p1 = pts[i - 1].p.cw(); + p2 = pts[i].p.cw(); + localPos = pos.cw(); + } + double gradient = (p2[Geom::X] - p1[Geom::X]) / (p2[Geom::Y] - p1[Geom::Y]); + double intersection = p1[Geom::X] - gradient * p1[Geom::Y]; + /* + orthogonalGradient = -1.0 / gradient; // you are going to have numerical problems here. + orthogonalIntersection = localPos[Geom::X] - orthogonalGradient * localPos[Geom::Y]; + nearestY = (orthogonalIntersection - intersection) / (gradient - orthogonalGradient); + + expand out nearestY fully : + nearestY = (localPos[Geom::X] - (-1.0 / gradient) * localPos[Geom::Y] - intersection) / (gradient - (-1.0 / gradient)); + + multiply top and bottom by gradient: + nearestY = (localPos[Geom::X] * gradient - (-1.0) * localPos[Geom::Y] - intersection * gradient) / (gradient * gradient - (-1.0)); + + and simplify to get: + */ + double nearestY = (localPos[Geom::X] * gradient + localPos[Geom::Y] - intersection * gradient) + / (gradient * gradient + 1.0); + t = (nearestY - p1[Geom::Y]) / (p2[Geom::Y] - p1[Geom::Y]); + if (t <= 0.0) { + thisRangeSquared = square(p1[Geom::X] - localPos[Geom::X]) + square(p1[Geom::Y] - localPos[Geom::Y]); + t = 0.0; + } else if (t >= 1.0) { + thisRangeSquared = square(p2[Geom::X] - localPos[Geom::X]) + square(p2[Geom::Y] - localPos[Geom::Y]); + t = 1.0; + } else { + thisRangeSquared = square(nearestY * gradient + intersection - localPos[Geom::X]) + square(nearestY - localPos[Geom::Y]); + } + } + + if (thisRangeSquared < bestRangeSquared) { + bestSeg = i; + bestRangeSquared = thisRangeSquared; + bestT = t; + } + } + Path::cut_position result; + if (bestSeg == 0) { + result.piece = 0; + result.t = 0.0; + } else { + result.piece = pts[bestSeg].piece; + if (result.piece == pts[bestSeg - 1].piece) { + result.t = pts[bestSeg - 1].t * (1.0 - bestT) + pts[bestSeg].t * bestT; + } else { + result.t = pts[bestSeg].t * bestT; + } + } + return result; +} +/* + this one also belongs to Path + returns the length of the path up to the position indicated by t (0..1) + + TODO: clean up uses of the original function and remove + + should this take a cut_position as a parameter? +*/ +double Path::PositionToLength(int piece, double t) +{ + double length = 0.0; + for (unsigned i = 1 ; i < pts.size() ; i++) { + if (pts[i].isMoveTo == polyline_moveto) continue; + if (pts[i].piece == piece && t < pts[i].t) { + length += Geom::L2((t - pts[i - 1].t) / (pts[i].t - pts[i - 1].t) * (pts[i].p - pts[i - 1].p)); + break; + } + length += Geom::L2(pts[i].p - pts[i - 1].p); + } + return length; +} + +void Path::ConvertPositionsToForced(int nbPos, cut_position *poss) +{ + if ( nbPos <= 0 ) { + return; + } + + { + Geom::Point lastPos(0, 0); + for (int i = int(descr_cmd.size()) - 1; i >= 0; i--) { + int const typ = descr_cmd[i]->getType(); + switch ( typ ) { + + case descr_forced: + { + PathDescrForced *d = dynamic_cast(descr_cmd[i]); + d->p = lastPos; + break; + } + + case descr_close: + { + delete descr_cmd[i]; + descr_cmd[i] = new PathDescrLineTo(Geom::Point(0, 0)); + + int fp = i - 1; + while ( fp >= 0 && (descr_cmd[fp]->getType()) != descr_moveto ) { + fp--; + } + + if ( fp >= 0 ) { + PathDescrMoveTo *oData = dynamic_cast(descr_cmd[fp]); + dynamic_cast(descr_cmd[i])->p = oData->p; + } + } + break; + + case descr_bezierto: + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + Geom::Point theP = nData->p; + if ( nData->nb == 0 ) { + lastPos = theP; + } + } + break; + + case descr_moveto: + { + PathDescrMoveTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_lineto: + { + PathDescrLineTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_arcto: + { + PathDescrArcTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_cubicto: + { + PathDescrCubicTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + case descr_interm_bezier: + { + PathDescrIntermBezierTo *d = dynamic_cast(descr_cmd[i]); + lastPos = d->p; + break; + } + default: + break; + } + } + } + if (descr_cmd[0]->getType() == descr_moveto) + descr_flags |= descr_doing_subpath; // see LP Bug 166302 + + qsort(poss, nbPos, sizeof(cut_position), CmpPosition); + + for (int curP=0;curP= int(descr_cmd.size()) ) break; + float ct=poss[curP].t; + if ( ct < 0 ) continue; + if ( ct > 1 ) continue; + + int const typ = descr_cmd[cp]->getType(); + if ( typ == descr_moveto || typ == descr_forced || typ == descr_close ) { + // ponctuel= rien a faire + } else if ( typ == descr_lineto || typ == descr_arcto || typ == descr_cubicto ) { + // facile: creation d'un morceau et d'un forced -> 2 commandes + Geom::Point theP; + Geom::Point theT; + Geom::Point startP; + startP=PrevPoint(cp-1); + if ( typ == descr_cubicto ) { + double len,rad; + Geom::Point stD,enD,endP; + { + PathDescrCubicTo *oData = dynamic_cast(descr_cmd[cp]); + stD=oData->start; + enD=oData->end; + endP=oData->p; + TangentOnCubAt (ct, startP, *oData,true, theP,theT,len,rad); + } + + theT*=len; + + InsertCubicTo(endP,(1-ct)*theT,(1-ct)*enD,cp+1); + InsertForcePoint(cp+1); + { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[cp]); + nData->start=ct*stD; + nData->end=ct*theT; + nData->p=theP; + } + // decalages dans le tableau des positions de coupe + for (int j=curP+1;j(descr_cmd[cp]); + endP=oData->p; + } + + theP=ct*endP+(1-ct)*startP; + + InsertLineTo(endP,cp+1); + InsertForcePoint(cp+1); + { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[cp]); + nData->p=theP; + } + // decalages dans le tableau des positions de coupe + for (int j=curP+1;j(descr_cmd[cp]); + endP=oData->p; + rx=oData->rx; + ry=oData->ry; + angle=oData->angle; + clockw=oData->clockwise; + large=oData->large; + } + { + double sang,eang; + ArcAngles(startP,endP,rx,ry,angle*M_PI/180.0,large,clockw,sang,eang); + + if (clockw) { + if ( sang < eang ) sang += 2*M_PI; + delta=eang-sang; + } else { + if ( sang > eang ) sang -= 2*M_PI; + delta=eang-sang; + } + if ( delta < 0 ) delta=-delta; + } + + PointAt (cp,ct, theP); + + if ( delta*(1-ct) > M_PI ) { + InsertArcTo(endP,rx,ry,angle,true,clockw,cp+1); + } else { + InsertArcTo(endP,rx,ry,angle,false,clockw,cp+1); + } + InsertForcePoint(cp+1); + { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[cp]); + nData->p=theP; + if ( delta*ct > M_PI ) { + nData->large=true; + } else { + nData->large=false; + } + } + // decalages dans le tableau des positions de coupe + for (int j=curP+1;j= 0 && (descr_cmd[theBDI]->getType()) != descr_bezierto ) theBDI--; + if ( (descr_cmd[theBDI]->getType()) == descr_bezierto ) { + PathDescrBezierTo theBD=*(dynamic_cast(descr_cmd[theBDI])); + if ( cp >= theBDI && cp < theBDI+theBD.nb ) { + if ( theBD.nb == 1 ) { + Geom::Point endP=theBD.p; + Geom::Point midP; + Geom::Point startP; + startP=PrevPoint(theBDI-1); + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[theBDI+1]); + midP=nData->p; + } + Geom::Point aP=ct*midP+(1-ct)*startP; + Geom::Point bP=ct*endP+(1-ct)*midP; + Geom::Point knotP=ct*bP+(1-ct)*aP; + + InsertIntermBezierTo(bP,theBDI+2); + InsertBezierTo(knotP,1,theBDI+2); + InsertForcePoint(theBDI+2); + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[theBDI+1]); + nData->p=aP; + } + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[theBDI]); + nData->p=knotP; + } + // decalages dans le tableau des positions de coupe + for (int j=curP+1;j theBDI ) { + Geom::Point pcP,ncP; + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[cp]); + pcP=nData->p; + } + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[cp+1]); + ncP=nData->p; + } + Geom::Point knotP=0.5*(pcP+ncP); + + InsertBezierTo(knotP,theBD.nb-(cp-theBDI),cp+1); + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[theBDI]); + nData->nb=cp-theBDI; + } + + // decalages dans le tableau des positions de coupe + for (int j=curP;j(descr_cmd[cp+1]); + pcP=nData->p; + } + { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[cp+2]); + ncP=nData->p; + } + Geom::Point knotP=0.5*(pcP+ncP); + + InsertBezierTo(knotP,theBD.nb-1,cp+2); + { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[theBDI]); + nData->nb=1; + } + + // decalages dans le tableau des positions de coupe + for (int j=curP;jgetType(); + if ( typ == descr_moveto ) { + Geom::Point np; + { + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[i]); + np=nData->p; + } + Geom::Point endP; + bool hasClose=false; + int hasForced=-1; + bool doesClose=false; + int j=i+1; + for (;jgetType(); + if ( ntyp == descr_moveto ) { + j--; + break; + } else if ( ntyp == descr_forced ) { + if ( hasForced < 0 ) hasForced=j; + } else if ( ntyp == descr_close ) { + hasClose=true; + break; + } else if ( ntyp == descr_lineto ) { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[j]); + endP=nData->p; + } else if ( ntyp == descr_arcto ) { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[j]); + endP=nData->p; + } else if ( ntyp == descr_cubicto ) { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[j]); + endP=nData->p; + } else if ( ntyp == descr_bezierto ) { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[j]); + endP=nData->p; + } else { + } + } + if ( Geom::LInfty(endP-np) < 0.00001 ) { + doesClose=true; + } + if ( ( doesClose || hasClose ) && hasForced >= 0 ) { + // printf("nasty i=%i j=%i frc=%i\n",i,j,hasForced); + // aghhh. + Geom::Point nMvtP=PrevPoint(hasForced); + res->MoveTo(nMvtP); + Geom::Point nLastP=nMvtP; + for (int k = hasForced + 1; k < j; k++) { + int ntyp=descr_cmd[k]->getType(); + if ( ntyp == descr_moveto ) { + // ne doit pas arriver + } else if ( ntyp == descr_forced ) { + res->MoveTo(nLastP); + } else if ( ntyp == descr_close ) { + // rien a faire ici; de plus il ne peut y en avoir qu'un + } else if ( ntyp == descr_lineto ) { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[k]); + res->LineTo(nData->p); + nLastP=nData->p; + } else if ( ntyp == descr_arcto ) { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[k]); + res->ArcTo(nData->p,nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + nLastP=nData->p; + } else if ( ntyp == descr_cubicto ) { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[k]); + res->CubicTo(nData->p,nData->start,nData->end); + nLastP=nData->p; + } else if ( ntyp == descr_bezierto ) { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[k]); + res->BezierTo(nData->p); + nLastP=nData->p; + } else if ( ntyp == descr_interm_bezier ) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[k]); + res->IntermBezierTo(nData->p); + } else { + } + } + if ( doesClose == false ) res->LineTo(np); + nLastP=np; + for (int k=i+1;kgetType(); + if ( ntyp == descr_moveto ) { + // ne doit pas arriver + } else if ( ntyp == descr_forced ) { + res->MoveTo(nLastP); + } else if ( ntyp == descr_close ) { + // rien a faire ici; de plus il ne peut y en avoir qu'un + } else if ( ntyp == descr_lineto ) { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[k]); + res->LineTo(nData->p); + nLastP=nData->p; + } else if ( ntyp == descr_arcto ) { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[k]); + res->ArcTo(nData->p,nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + nLastP=nData->p; + } else if ( ntyp == descr_cubicto ) { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[k]); + res->CubicTo(nData->p,nData->start,nData->end); + nLastP=nData->p; + } else if ( ntyp == descr_bezierto ) { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[k]); + res->BezierTo(nData->p); + nLastP=nData->p; + } else if ( ntyp == descr_interm_bezier ) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[k]); + res->IntermBezierTo(nData->p); + } else { + } + } + lastP=nMvtP; + i=j; + } else { + // regular, just move on + res->MoveTo(np); + lastP=np; + } + } else if ( typ == descr_close ) { + res->Close(); + } else if ( typ == descr_forced ) { + res->MoveTo(lastP); + } else if ( typ == descr_lineto ) { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[i]); + res->LineTo(nData->p); + lastP=nData->p; + } else if ( typ == descr_arcto ) { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[i]); + res->ArcTo(nData->p,nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + lastP=nData->p; + } else if ( typ == descr_cubicto ) { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[i]); + res->CubicTo(nData->p,nData->start,nData->end); + lastP=nData->p; + } else if ( typ == descr_bezierto ) { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[i]); + res->BezierTo(nData->p); + lastP=nData->p; + } else if ( typ == descr_interm_bezier ) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[i]); + res->IntermBezierTo(nData->p); + } else { + } + } + + Copy(res); + delete res; + return; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/PathOutline.cpp b/src/livarot/PathOutline.cpp new file mode 100644 index 0000000..c7fb226 --- /dev/null +++ b/src/livarot/PathOutline.cpp @@ -0,0 +1,1526 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livarot/Path.h" +#include "livarot/path-description.h" + +/* + * the "outliner" + * takes a sequence of path commands and produces a set of commands that approximates the offset + * result is stored in dest (that paremeter is handed to all the subfunctions) + * not that the result is in general not mathematically correct; you can end up with unwanted holes in your + * beautiful offset. a better way is to do path->polyline->polygon->offset of polygon->polyline(=contours of the polygon)->path + * but computing offsets of the path is faster... + */ + +// outline of a path. +// computed by making 2 offsets, one of the "left" side of the path, one of the right side, and then glueing the two +// the left side has to be reversed to make a contour +void Path::Outline(Path *dest, double width, JoinType join, ButtType butt, double miter) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + if ( descr_cmd.size() <= 1 ) { + return; + } + if ( dest == nullptr ) { + return; + } + + dest->Reset(); + dest->SetBackData(false); + + outline_callbacks calls; + Geom::Point endButt; + Geom::Point endPos; + calls.cubicto = StdCubicTo; + calls.bezierto = StdBezierTo; + calls.arcto = StdArcTo; + + Path *rev = new Path; + + // we repeat the offset contour creation for each subpath + int curP = 0; + do { + int lastM = curP; + do { + curP++; + if (curP >= int(descr_cmd.size())) { + break; + } + int typ = descr_cmd[curP]->getType(); + if (typ == descr_moveto) { + break; + } + } while (curP < int(descr_cmd.size())); + + if (curP >= int(descr_cmd.size())) { + curP = descr_cmd.size(); + } + + if (curP > lastM + 1) { + // we have isolated a subpath, now we make a reversed version of it + // we do so by taking the subpath in the reverse and constructing a path as appropriate + // the construct is stored in "rev" + int curD = curP - 1; + Geom::Point curX; + Geom::Point nextX; + int firstTyp = descr_cmd[curD]->getType(); + bool const needClose = (firstTyp == descr_close); + while (curD > lastM && descr_cmd[curD]->getType() == descr_close) { + curD--; + } + + int realP = curD + 1; + if (curD > lastM) { + curX = PrevPoint(curD); + rev->Reset (); + rev->MoveTo(curX); + while (curD > lastM) { + int const typ = descr_cmd[curD]->getType(); + if (typ == descr_moveto) { + // rev->Close(); + curD--; + } else if (typ == descr_forced) { + // rev->Close(); + curD--; + } else if (typ == descr_lineto) { + nextX = PrevPoint (curD - 1); + rev->LineTo (nextX); + curX = nextX; + curD--; + } else if (typ == descr_cubicto) { + PathDescrCubicTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = PrevPoint (curD - 1); + Geom::Point isD=-nData->start; + Geom::Point ieD=-nData->end; + rev->CubicTo (nextX, ieD,isD); + curX = nextX; + curD--; + } else if (typ == descr_arcto) { + PathDescrArcTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = PrevPoint (curD - 1); + rev->ArcTo (nextX, nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + curX = nextX; + curD--; + } else if (typ == descr_bezierto) { + nextX = PrevPoint (curD - 1); + rev->LineTo (nextX); + curX = nextX; + curD--; + } else if (typ == descr_interm_bezier) { + int nD = curD - 1; + while (nD > lastM && descr_cmd[nD]->getType() != descr_bezierto) nD--; + if ((descr_cmd[nD]->getType()) != descr_bezierto) { + // pas trouve le debut!? + // Not find the start?! + nextX = PrevPoint (nD); + rev->LineTo (nextX); + curX = nextX; + } else { + nextX = PrevPoint (nD - 1); + rev->BezierTo (nextX); + for (int i = curD; i > nD; i--) { + PathDescrIntermBezierTo* nData = dynamic_cast(descr_cmd[i]); + rev->IntermBezierTo (nData->p); + } + rev->EndBezierTo (); + curX = nextX; + } + curD = nD - 1; + } else { + curD--; + } + } + + // offset the paths and glue everything + // actual offseting is done in SubContractOutline() + if (needClose) { + rev->Close (); + rev->SubContractOutline (0, rev->descr_cmd.size(), + dest, calls, 0.0025 * width * width, width, + join, butt, miter, true, false, endPos, endButt); + SubContractOutline (lastM, realP + 1 - lastM, + dest, calls, 0.0025 * width * width, + width, join, butt, miter, true, false, endPos, endButt); + } else { + rev->SubContractOutline (0, rev->descr_cmd.size(), + dest, calls, 0.0025 * width * width, width, + join, butt, miter, false, false, endPos, endButt); + Geom::Point endNor=endButt.ccw(); + if (butt == butt_round) { + dest->ArcTo (endPos+width*endButt, width, width, 0.0, false, true); + dest->ArcTo (endPos+width*endNor, width, width, 0.0, false, true); + } else if (butt == butt_square) { + dest->LineTo (endPos-width*endNor+width*endButt); + dest->LineTo (endPos+width*endNor+width*endButt); + dest->LineTo (endPos+width*endNor); + } else if (butt == butt_pointy) { + dest->LineTo (endPos+width*endButt); + dest->LineTo (endPos+width*endNor); + } else { + dest->LineTo (endPos+width*endNor); + } + SubContractOutline (lastM, realP - lastM, + dest, calls, 0.0025 * width * width, width, join, butt, + miter, false, true, endPos, endButt); + + endNor=endButt.ccw(); + if (butt == butt_round) { + dest->ArcTo (endPos+width*endButt, width, width, 0.0, false, true); + dest->ArcTo (endPos+width*endNor, width, width, 0.0, false, true); + } else if (butt == butt_square) { + dest->LineTo (endPos-width*endNor+width*endButt); + dest->LineTo (endPos+width*endNor+width*endButt); + dest->LineTo (endPos+width*endNor); + } else if (butt == butt_pointy) { + dest->LineTo (endPos+width*endButt); + dest->LineTo (endPos+width*endNor); + } else { + dest->LineTo (endPos+width*endNor); + } + dest->Close (); + } + } // if (curD > lastM) + } // if (curP > lastM + 1) + + } while (curP < int(descr_cmd.size())); + + delete rev; +} + +// versions for outlining closed path: they only make one side of the offset contour +void +Path::OutsideOutline (Path * dest, double width, JoinType join, ButtType butt, + double miter) +{ + if (descr_flags & descr_adding_bezier) { + CancelBezier(); + } + if (descr_flags & descr_doing_subpath) { + CloseSubpath(); + } + if (int(descr_cmd.size()) <= 1) return; + if (dest == nullptr) return; + dest->Reset (); + dest->SetBackData (false); + + outline_callbacks calls; + Geom::Point endButt, endPos; + calls.cubicto = StdCubicTo; + calls.bezierto = StdBezierTo; + calls.arcto = StdArcTo; + SubContractOutline (0, descr_cmd.size(), + dest, calls, 0.0025 * width * width, width, join, butt, + miter, true, false, endPos, endButt); +} + +void +Path::InsideOutline (Path * dest, double width, JoinType join, ButtType butt, + double miter) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + if (int(descr_cmd.size()) <= 1) return; + if (dest == nullptr) return; + dest->Reset (); + dest->SetBackData (false); + + outline_callbacks calls; + Geom::Point endButt, endPos; + calls.cubicto = StdCubicTo; + calls.bezierto = StdBezierTo; + calls.arcto = StdArcTo; + + Path *rev = new Path; + + int curP = 0; + do { + int lastM = curP; + do { + curP++; + if (curP >= int(descr_cmd.size())) break; + int typ = descr_cmd[curP]->getType(); + if (typ == descr_moveto) break; + } while (curP < int(descr_cmd.size())); + if (curP >= int(descr_cmd.size())) curP = descr_cmd.size(); + if (curP > lastM + 1) { + // Otherwise there's only one point. (tr: or "only a point") + // [sinon il n'y a qu'un point] + int curD = curP - 1; + Geom::Point curX; + Geom::Point nextX; + while (curD > lastM && (descr_cmd[curD]->getType()) == descr_close) curD--; + if (curD > lastM) { + curX = PrevPoint (curD); + rev->Reset (); + rev->MoveTo (curX); + while (curD > lastM) { + int typ = descr_cmd[curD]->getType(); + if (typ == descr_moveto) { + rev->Close (); + curD--; + } else if (typ == descr_forced) { + curD--; + } else if (typ == descr_lineto) { + nextX = PrevPoint (curD - 1); + rev->LineTo (nextX); + curX = nextX; + curD--; + } else if (typ == descr_cubicto) { + PathDescrCubicTo *nData = dynamic_cast(descr_cmd[curD]); + nextX = PrevPoint (curD - 1); + Geom::Point isD=-nData->start; + Geom::Point ieD=-nData->end; + rev->CubicTo (nextX, ieD,isD); + curX = nextX; + curD--; + } else if (typ == descr_arcto) { + PathDescrArcTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = PrevPoint (curD - 1); + rev->ArcTo (nextX, nData->rx,nData->ry,nData->angle,nData->large,nData->clockwise); + curX = nextX; + curD--; + } else if (typ == descr_bezierto) { + nextX = PrevPoint (curD - 1); + rev->LineTo (nextX); + curX = nextX; + curD--; + } else if (typ == descr_interm_bezier) { + int nD = curD - 1; + while (nD > lastM && (descr_cmd[nD]->getType()) != descr_bezierto) nD--; + if (descr_cmd[nD]->getType() != descr_bezierto) { + // pas trouve le debut!? + nextX = PrevPoint (nD); + rev->LineTo (nextX); + curX = nextX; + } else { + nextX = PrevPoint (nD - 1); + rev->BezierTo (nextX); + for (int i = curD; i > nD; i--) { + PathDescrIntermBezierTo* nData = dynamic_cast(descr_cmd[i]); + rev->IntermBezierTo (nData->p); + } + rev->EndBezierTo (); + curX = nextX; + } + curD = nD - 1; + } else { + curD--; + } + } + rev->Close (); + rev->SubContractOutline (0, rev->descr_cmd.size(), + dest, calls, 0.0025 * width * width, + width, join, butt, miter, true, false, + endPos, endButt); + } + } + } while (curP < int(descr_cmd.size())); + + delete rev; +} + + +// the offset +// take each command and offset it. +// the bezier spline is split in a sequence of bezier curves, and these are transformed in cubic bezier (which is +// not hard since they are quadratic bezier) +// joins are put where needed +void Path::SubContractOutline(int off, int num_pd, + Path *dest, outline_callbacks & calls, + double tolerance, double width, JoinType join, + ButtType /*butt*/, double miter, bool closeIfNeeded, + bool skipMoveto, Geom::Point &lastP, Geom::Point &lastT) +{ + outline_callback_data callsData; + + callsData.orig = this; + callsData.dest = dest; + int curP = 1; + + // le moveto + Geom::Point curX; + { + int firstTyp = descr_cmd[off]->getType(); + if ( firstTyp != descr_moveto ) { + curX[0] = curX[1] = 0; + curP = 0; + } else { + PathDescrMoveTo* nData = dynamic_cast(descr_cmd[off]); + curX = nData->p; + } + } + Geom::Point curT(0, 0); + + bool doFirst = true; + Geom::Point firstP(0, 0); + Geom::Point firstT(0, 0); + + // et le reste, 1 par 1 + while (curP < num_pd) + { + int curD = off + curP; + int nType = descr_cmd[curD]->getType(); + Geom::Point nextX; + Geom::Point stPos, enPos, stTgt, enTgt, stNor, enNor; + double stRad, enRad, stTle, enTle; + if (nType == descr_forced) { + curP++; + } else if (nType == descr_moveto) { + PathDescrMoveTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = nData->p; + // et on avance + if (doFirst) { + } else { + if (closeIfNeeded) { + if ( Geom::LInfty (curX- firstP) < 0.0001 ) { + OutlineJoin (dest, firstP, curT, firstT, width, join, + miter, nType); + dest->Close (); + } else { + PathDescrLineTo temp(firstP); + + TangentOnSegAt (0.0, curX, temp, stPos, stTgt, + stTle); + TangentOnSegAt (1.0, curX, temp, enPos, enTgt, + enTle); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + // jointure + { + Geom::Point pos; + pos = curX; + OutlineJoin (dest, pos, curT, stNor, width, join, + miter, nType); + } + dest->LineTo (enPos+width*enNor); + + // jointure + { + Geom::Point pos; + pos = firstP; + OutlineJoin (dest, enPos, enNor, firstT, width, join, + miter, nType); + dest->Close (); + } + } + } + } + firstP = nextX; + curP++; + } + else if (nType == descr_close) + { + if (! doFirst) + { + if (Geom::LInfty (curX - firstP) < 0.0001) + { + OutlineJoin (dest, firstP, curT, firstT, width, join, + miter, nType); + dest->Close (); + } + else + { + PathDescrLineTo temp(firstP); + nextX = firstP; + + TangentOnSegAt (0.0, curX, temp, stPos, stTgt, stTle); + TangentOnSegAt (1.0, curX, temp, enPos, enTgt, enTle); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + // jointure + { + OutlineJoin (dest, stPos, curT, stNor, width, join, + miter, nType); + } + + dest->LineTo (enPos+width*enNor); + + // jointure + { + OutlineJoin (dest, enPos, enNor, firstT, width, join, + miter, nType); + dest->Close (); + } + } + } + doFirst = true; + curP++; + } + else if (nType == descr_lineto) + { + PathDescrLineTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = nData->p; + // et on avance + TangentOnSegAt (0.0, curX, *nData, stPos, stTgt, stTle); + TangentOnSegAt (1.0, curX, *nData, enPos, enTgt, enTle); + // test de nullité du segment + if (IsNulCurve (descr_cmd, curD, curX)) + { + if (descr_cmd.size() == 2) { // single point, see LP Bug 1006666 + stTgt = dest->descr_cmd.size() ? Geom::Point(1, 0) : Geom::Point(-1, 0); // reverse direction + enTgt = stTgt; + } else { + curP++; + continue; + } + } + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + if (doFirst) + { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) + { + skipMoveto = false; + } + else + dest->MoveTo (curX+width*stNor); + } + else + { + // jointure + Geom::Point pos; + pos = curX; + OutlineJoin (dest, pos, curT, stNor, width, join, miter, nType); + } + + int n_d = dest->LineTo (nextX+width*enNor); + if (n_d >= 0) + { + dest->descr_cmd[n_d]->associated = curP; + dest->descr_cmd[n_d]->tSt = 0.0; + dest->descr_cmd[n_d]->tEn = 1.0; + } + curP++; + } + else if (nType == descr_cubicto) + { + PathDescrCubicTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = nData->p; + // test de nullite du segment + if (IsNulCurve (descr_cmd, curD, curX)) + { + curP++; + continue; + } + // et on avance + TangentOnCubAt (0.0, curX, *nData, false, stPos, stTgt, + stTle, stRad); + TangentOnCubAt (1.0, curX, *nData, true, enPos, enTgt, + enTle, enRad); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + if (doFirst) + { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) + { + skipMoveto = false; + } + else + dest->MoveTo (curX+width*stNor); + } + else + { + // jointure + Geom::Point pos; + pos = curX; + OutlineJoin (dest, pos, curT, stNor, width, join, miter, nType); + } + + callsData.piece = curP; + callsData.tSt = 0.0; + callsData.tEn = 1.0; + callsData.x1 = curX[0]; + callsData.y1 = curX[1]; + callsData.x2 = nextX[0]; + callsData.y2 = nextX[1]; + callsData.d.c.dx1 = nData->start[0]; + callsData.d.c.dy1 = nData->start[1]; + callsData.d.c.dx2 = nData->end[0]; + callsData.d.c.dy2 = nData->end[1]; + (calls.cubicto) (&callsData, tolerance, width); + + curP++; + } + else if (nType == descr_arcto) + { + PathDescrArcTo* nData = dynamic_cast(descr_cmd[curD]); + nextX = nData->p; + // test de nullité du segment + if (IsNulCurve (descr_cmd, curD, curX)) + { + curP++; + continue; + } + // et on avance + TangentOnArcAt (0.0, curX, *nData, stPos, stTgt, stTle, + stRad); + TangentOnArcAt (1.0, curX, *nData, enPos, enTgt, enTle, + enRad); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; // tjs definie + + if (doFirst) + { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) + { + skipMoveto = false; + } + else + dest->MoveTo (curX+width*stNor); + } + else + { + // jointure + Geom::Point pos; + pos = curX; + OutlineJoin (dest, pos, curT, stNor, width, join, miter, nType); + } + + callsData.piece = curP; + callsData.tSt = 0.0; + callsData.tEn = 1.0; + callsData.x1 = curX[0]; + callsData.y1 = curX[1]; + callsData.x2 = nextX[0]; + callsData.y2 = nextX[1]; + callsData.d.a.rx = nData->rx; + callsData.d.a.ry = nData->ry; + callsData.d.a.angle = nData->angle; + callsData.d.a.clock = nData->clockwise; + callsData.d.a.large = nData->large; + (calls.arcto) (&callsData, tolerance, width); + + curP++; + } + else if (nType == descr_bezierto) + { + PathDescrBezierTo* nBData = dynamic_cast(descr_cmd[curD]); + int nbInterm = nBData->nb; + nextX = nBData->p; + + if (IsNulCurve (descr_cmd, curD, curX)) { + curP += nbInterm + 1; + continue; + } + + curP++; + + curD = off + curP; + int ip = curD; + PathDescrIntermBezierTo* nData = dynamic_cast(descr_cmd[ip]); + + if (nbInterm <= 0) { + // et on avance + PathDescrLineTo temp(nextX); + TangentOnSegAt (0.0, curX, temp, stPos, stTgt, stTle); + TangentOnSegAt (1.0, curX, temp, enPos, enTgt, enTle); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + if (doFirst) { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) { + skipMoveto = false; + } else dest->MoveTo (curX+width*stNor); + } else { + // jointure + Geom::Point pos; + pos = curX; + if (stTle > 0) OutlineJoin (dest, pos, curT, stNor, width, join, miter, nType); + } + int n_d = dest->LineTo (nextX+width*enNor); + if (n_d >= 0) { + dest->descr_cmd[n_d]->associated = curP - 1; + dest->descr_cmd[n_d]->tSt = 0.0; + dest->descr_cmd[n_d]->tEn = 1.0; + } + } else if (nbInterm == 1) { + Geom::Point midX; + midX = nData->p; + // et on avance + TangentOnBezAt (0.0, curX, *nData, *nBData, false, stPos, stTgt, stTle, stRad); + TangentOnBezAt (1.0, curX, *nData, *nBData, true, enPos, enTgt, enTle, enRad); + stNor=stTgt.cw(); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + if (doFirst) { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) { + skipMoveto = false; + } else dest->MoveTo (curX+width*stNor); + } else { + // jointure + Geom::Point pos; + pos = curX; + OutlineJoin (dest, pos, curT, stNor, width, join, miter, nType); + } + + callsData.piece = curP; + callsData.tSt = 0.0; + callsData.tEn = 1.0; + callsData.x1 = curX[0]; + callsData.y1 = curX[1]; + callsData.x2 = nextX[0]; + callsData.y2 = nextX[1]; + callsData.d.b.mx = midX[0]; + callsData.d.b.my = midX[1]; + (calls.bezierto) (&callsData, tolerance, width); + + } else if (nbInterm > 1) { + Geom::Point bx=curX; + Geom::Point cx=curX; + Geom::Point dx=nData->p; + + TangentOnBezAt (0.0, curX, *nData, *nBData, false, stPos, stTgt, stTle, stRad); + stNor=stTgt.cw(); + + ip++; + nData = dynamic_cast(descr_cmd[ip]); + // et on avance + if (stTle > 0) { + if (doFirst) { + doFirst = false; + firstP = stPos; + firstT = stNor; + if (skipMoveto) { + skipMoveto = false; + } else dest->MoveTo (curX+width*stNor); + } else { + // jointure + Geom::Point pos=curX; + OutlineJoin (dest, pos, stTgt, stNor, width, join, miter, nType); + // dest->LineTo(curX+width*stNor.x,curY+width*stNor.y); + } + } + + cx = 2 * bx - dx; + + for (int k = 0; k < nbInterm - 1; k++) { + bx = cx; + cx = dx; + + dx = nData->p; + ip++; + nData = dynamic_cast(descr_cmd[ip]); + Geom::Point stx = (bx + cx) / 2; + // double stw=(bw+cw)/2; + + PathDescrBezierTo tempb((cx + dx) / 2, 1); + PathDescrIntermBezierTo tempi(cx); + TangentOnBezAt (1.0, stx, tempi, tempb, true, enPos, enTgt, enTle, enRad); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + callsData.piece = curP + k; + callsData.tSt = 0.0; + callsData.tEn = 1.0; + callsData.x1 = stx[0]; + callsData.y1 = stx[1]; + callsData.x2 = (cx[0] + dx[0]) / 2; + callsData.y2 = (cx[1] + dx[1]) / 2; + callsData.d.b.mx = cx[0]; + callsData.d.b.my = cx[1]; + (calls.bezierto) (&callsData, tolerance, width); + } + { + bx = cx; + cx = dx; + + dx = nextX; + dx = 2 * dx - cx; + + Geom::Point stx = (bx + cx) / 2; + // double stw=(bw+cw)/2; + + PathDescrBezierTo tempb((cx + dx) / 2, 1); + PathDescrIntermBezierTo tempi(cx); + TangentOnBezAt (1.0, stx, tempi, tempb, true, enPos, + enTgt, enTle, enRad); + enNor=enTgt.cw(); + + lastP = enPos; + lastT = enTgt; + + callsData.piece = curP + nbInterm - 1; + callsData.tSt = 0.0; + callsData.tEn = 1.0; + callsData.x1 = stx[0]; + callsData.y1 = stx[1]; + callsData.x2 = (cx[0] + dx[0]) / 2; + callsData.y2 = (cx[1] + dx[1]) / 2; + callsData.d.b.mx = cx[0]; + callsData.d.b.my = cx[1]; + (calls.bezierto) (&callsData, tolerance, width); + + } + } + + // et on avance + curP += nbInterm; + } + curX = nextX; + curT = enNor; // sera tjs bien definie + } + if (closeIfNeeded) + { + if (! doFirst) + { + } + } + +} + +/* + * + * utilitaires pour l'outline + * + */ + +// like the name says: check whether the path command is actually more than a dumb point. +bool +Path::IsNulCurve (std::vector const &cmd, int curD, Geom::Point const &curX) +{ + switch(cmd[curD]->getType()) { + case descr_lineto: + { + PathDescrLineTo *nData = dynamic_cast(cmd[curD]); + if (Geom::LInfty(nData->p - curX) < 0.00001) { + return true; + } + return false; + } + case descr_cubicto: + { + PathDescrCubicTo *nData = dynamic_cast(cmd[curD]); + Geom::Point A = nData->start + nData->end + 2*(curX - nData->p); + Geom::Point B = 3*(nData->p - curX) - 2*nData->start - nData->end; + Geom::Point C = nData->start; + if (Geom::LInfty(A) < 0.0001 + && Geom::LInfty(B) < 0.0001 + && Geom::LInfty (C) < 0.0001) { + return true; + } + return false; + } + case descr_arcto: + { + PathDescrArcTo* nData = dynamic_cast(cmd[curD]); + if ( Geom::LInfty(nData->p - curX) < 0.00001) { + if ((! nData->large) + || (fabs (nData->rx) < 0.00001 + || fabs (nData->ry) < 0.00001)) { + return true; + } + } + return false; + } + case descr_bezierto: + { + PathDescrBezierTo* nBData = dynamic_cast(cmd[curD]); + if (nBData->nb <= 0) + { + if (Geom::LInfty(nBData->p - curX) < 0.00001) { + return true; + } + return false; + } + else if (nBData->nb == 1) + { + if (Geom::LInfty(nBData->p - curX) < 0.00001) { + int ip = curD + 1; + PathDescrIntermBezierTo* nData = dynamic_cast(cmd[ip]); + if (Geom::LInfty(nData->p - curX) < 0.00001) { + return true; + } + } + return false; + } else if (Geom::LInfty(nBData->p - curX) < 0.00001) { + for (int i = 1; i <= nBData->nb; i++) { + int ip = curD + i; + PathDescrIntermBezierTo* nData = dynamic_cast(cmd[ip]); + if (Geom::LInfty(nData->p - curX) > 0.00001) { + return false; + } + } + return true; + } + } + default: + return true; + } +} + +// tangents and curvarture computing, for the different path command types. +// the need for tangent is obvious: it gives the normal, along which we offset points +// curvature is used to do strength correction on the length of the tangents to the offset (see +// cubic offset) + +/** + * \param at Distance along a tangent (0 <= at <= 1). + * \param iS Start point. + * \param fin LineTo description containing end point. + * \param pos Filled in with the position of `at' on the segment. + * \param tgt Filled in with the normalised tangent vector. + * \param len Filled in with the length of the segment. + */ + +void Path::TangentOnSegAt(double at, Geom::Point const &iS, PathDescrLineTo const &fin, + Geom::Point &pos, Geom::Point &tgt, double &len) +{ + Geom::Point const iE = fin.p; + Geom::Point const seg = iE - iS; + double const l = L2(seg); + if (l <= 0.000001) { + pos = iS; + tgt = Geom::Point(0, 0); + len = 0; + } else { + tgt = seg / l; + pos = (1 - at) * iS + at * iE; // in other words, pos = iS + at * seg + len = l; + } +} + +// barf +void Path::TangentOnArcAt(double at, const Geom::Point &iS, PathDescrArcTo const &fin, + Geom::Point &pos, Geom::Point &tgt, double &len, double &rad) +{ + Geom::Point const iE = fin.p; + double const rx = fin.rx; + double const ry = fin.ry; + double const angle = fin.angle*M_PI/180.0; + bool const large = fin.large; + bool const wise = fin.clockwise; + + pos = iS; + tgt[0] = tgt[1] = 0; + if (rx <= 0.0001 || ry <= 0.0001) + return; + + double const sex = iE[0] - iS[0], sey = iE[1] - iS[1]; + double const ca = cos (angle), sa = sin (angle); + double csex = ca * sex + sa * sey; + double csey = -sa * sex + ca * sey; + csex /= rx; + csey /= ry; + double l = csex * csex + csey * csey; + double const d = sqrt(std::max(1 - l / 4, 0.0)); + double csdx = csey; + double csdy = -csex; + l = sqrt(l); + csdx /= l; + csdy /= l; + csdx *= d; + csdy *= d; + + double sang; + double eang; + double rax = -csdx - csex / 2; + double ray = -csdy - csey / 2; + if (rax < -1) + { + sang = M_PI; + } + else if (rax > 1) + { + sang = 0; + } + else + { + sang = acos (rax); + if (ray < 0) + sang = 2 * M_PI - sang; + } + rax = -csdx + csex / 2; + ray = -csdy + csey / 2; + if (rax < -1) + { + eang = M_PI; + } + else if (rax > 1) + { + eang = 0; + } + else + { + eang = acos (rax); + if (ray < 0) + eang = 2 * M_PI - eang; + } + + csdx *= rx; + csdy *= ry; + double drx = ca * csdx - sa * csdy; + double dry = sa * csdx + ca * csdy; + + if (wise) + { + if (large) + { + drx = -drx; + dry = -dry; + double swap = eang; + eang = sang; + sang = swap; + eang += M_PI; + sang += M_PI; + if (eang >= 2 * M_PI) + eang -= 2 * M_PI; + if (sang >= 2 * M_PI) + sang -= 2 * M_PI; + } + } + else + { + if (! large) + { + drx = -drx; + dry = -dry; + double swap = eang; + eang = sang; + sang = swap; + eang += M_PI; + sang += M_PI; + if (eang >= 2 * M_PI) + eang -= 2 * M_PI; + if (sang >= 2 * M_PI) + sang -= 2 * M_PI; + } + } + drx += (iS[0] + iE[0]) / 2; + dry += (iS[1] + iE[1]) / 2; + + if (wise) { + if (sang < eang) + sang += 2 * M_PI; + double b = sang * (1 - at) + eang * at; + double cb = cos (b), sb = sin (b); + pos[0] = drx + ca * rx * cb - sa * ry * sb; + pos[1] = dry + sa * rx * cb + ca * ry * sb; + tgt[0] = ca * rx * sb + sa * ry * cb; + tgt[1] = sa * rx * sb - ca * ry * cb; + Geom::Point dtgt; + dtgt[0] = -ca * rx * cb + sa * ry * sb; + dtgt[1] = -sa * rx * cb - ca * ry * sb; + len = L2(tgt); + rad = -len * dot(tgt, tgt) / (tgt[0] * dtgt[1] - tgt[1] * dtgt[0]); + tgt /= len; + } + else + { + if (sang > eang) + sang -= 2 * M_PI; + double b = sang * (1 - at) + eang * at; + double cb = cos (b), sb = sin (b); + pos[0] = drx + ca * rx * cb - sa * ry * sb; + pos[1] = dry + sa * rx * cb + ca * ry * sb; + tgt[0] = ca * rx * sb + sa * ry * cb; + tgt[1] = sa * rx * sb - ca * ry * cb; + Geom::Point dtgt; + dtgt[0] = -ca * rx * cb + sa * ry * sb; + dtgt[1] = -sa * rx * cb - ca * ry * sb; + len = L2(tgt); + rad = len * dot(tgt, tgt) / (tgt[0] * dtgt[1] - tgt[1] * dtgt[0]); + tgt /= len; + } +} +void +Path::TangentOnCubAt (double at, Geom::Point const &iS, PathDescrCubicTo const &fin, bool before, + Geom::Point &pos, Geom::Point &tgt, double &len, double &rad) +{ + const Geom::Point E = fin.p; + const Geom::Point Sd = fin.start; + const Geom::Point Ed = fin.end; + + pos = iS; + tgt = Geom::Point(0,0); + len = rad = 0; + + const Geom::Point A = Sd + Ed - 2*E + 2*iS; + const Geom::Point B = 0.5*(Ed - Sd); + const Geom::Point C = 0.25*(6*E - 6*iS - Sd - Ed); + const Geom::Point D = 0.125*(4*iS + 4*E - Ed + Sd); + const double atb = at - 0.5; + pos = (atb * atb * atb)*A + (atb * atb)*B + atb*C + D; + const Geom::Point der = (3 * atb * atb)*A + (2 * atb)*B + C; + const Geom::Point dder = (6 * atb)*A + 2*B; + const Geom::Point ddder = 6 * A; + + double l = Geom::L2 (der); + // lots of nasty cases. inversion points are sadly too common... + if (l <= 0.0001) { + len = 0; + l = L2(dder); + if (l <= 0.0001) { + l = L2(ddder); + if (l <= 0.0001) { + // pas de segment.... + return; + } + rad = 100000000; + tgt = ddder / l; + if (before) { + tgt = -tgt; + } + return; + } + rad = -l * (dot(dder,dder)) / (cross(dder, ddder)); + tgt = dder / l; + if (before) { + tgt = -tgt; + } + return; + } + len = l; + + rad = -l * (dot(der,der)) / (cross(der, dder)); + + tgt = der / l; +} + +void +Path::TangentOnBezAt (double at, Geom::Point const &iS, + PathDescrIntermBezierTo & mid, + PathDescrBezierTo & fin, bool before, Geom::Point & pos, + Geom::Point & tgt, double &len, double &rad) +{ + pos = iS; + tgt = Geom::Point(0,0); + len = rad = 0; + + const Geom::Point A = fin.p + iS - 2*mid.p; + const Geom::Point B = 2*mid.p - 2 * iS; + const Geom::Point C = iS; + + pos = at * at * A + at * B + C; + const Geom::Point der = 2 * at * A + B; + const Geom::Point dder = 2 * A; + double l = Geom::L2(der); + + if (l <= 0.0001) { + l = Geom::L2(dder); + if (l <= 0.0001) { + // pas de segment.... + // Not a segment. + return; + } + rad = 100000000; // Why this number? + tgt = dder / l; + if (before) { + tgt = -tgt; + } + return; + } + len = l; + rad = -l * (dot(der,der)) / (cross(der, dder)); + + tgt = der / l; +} + +void +Path::OutlineJoin (Path * dest, Geom::Point pos, Geom::Point stNor, Geom::Point enNor, double width, + JoinType join, double miter, int nType) +{ + /* + Arbitrarily decide if we're on the inside or outside of a half turn. + A turn of 180 degrees (line path leaves the node in the same direction as it arrived) + is symmetric and has no real inside and outside. However when outlining we shall handle + one path as inside and the reverse path as outside. Handling both as inside joins (as + was done previously) will cut off round joins. Handling both as outside joins could + ideally work because both should fall together, but it seems that this causes many + extra nodes (due to rounding errors). Solution: for the 'half turn'-case toggle + inside/outside each time the same node is processed 2 consecutive times. + */ + static bool TurnInside = true; + static Geom::Point PrevPos(0, 0); + TurnInside ^= PrevPos == pos; + PrevPos = pos; + + const double angSi = cross (stNor, enNor); + const double angCo = dot (stNor, enNor); + + if ((fabs(angSi) < .0000001) && angCo > 0) { // The join is straight -> nothing to do. + } else { + if ((angSi > 0 && width >= 0) + || (angSi < 0 && width < 0)) { // This is an inside join -> join is independent of chosen JoinType. + if ((dest->descr_cmd[dest->descr_cmd.size() - 1]->getType() == descr_lineto) && (nType == descr_lineto)) { + Geom::Point const biss = unit_vector(Geom::rot90( stNor - enNor )); + double c2 = Geom::dot (biss, enNor); + if (fabs(c2) > M_SQRT1_2) { // apply only to obtuse angles + double l = width / c2; + PathDescrLineTo* nLine = dynamic_cast(dest->descr_cmd[dest->descr_cmd.size() - 1]); + nLine->p = pos + l*biss; // relocate to bisector + } else { + dest->LineTo (pos + width*enNor); + } + } else { +// dest->LineTo (pos); // redundant + dest->LineTo (pos + width*enNor); + } + } else if (angSi == 0 && TurnInside) { // Half turn (180 degrees) ... inside (see above). + dest->LineTo (pos + width*enNor); + } else { // This is an outside join -> chosen JoinType should be applied. + if (join == join_round) { + // Use the ends of the cubic: approximate the arc at the + // point where .., and support better the rounding of + // coordinates of the end points. + + // utiliser des bouts de cubique: approximation de l'arc (au point ou on en est...), et supporte mieux + // l'arrondi des coordonnees des extremites + /* double angle=acos(angCo); + if ( angCo >= 0 ) { + Geom::Point stTgt,enTgt; + RotCCWTo(stNor,stTgt); + RotCCWTo(enNor,enTgt); + dest->CubicTo(pos.x+width*enNor.x,pos.y+width*enNor.y, + angle*width*stTgt.x,angle*width*stTgt.y, + angle*width*enTgt.x,angle*width*enTgt.y); + } else { + Geom::Point biNor; + Geom::Point stTgt,enTgt,biTgt; + biNor.x=stNor.x+enNor.x; + biNor.y=stNor.y+enNor.y; + double biL=sqrt(biNor.x*biNor.x+biNor.y*biNor.y); + biNor.x/=biL; + biNor.y/=biL; + RotCCWTo(stNor,stTgt); + RotCCWTo(enNor,enTgt); + RotCCWTo(biNor,biTgt); + dest->CubicTo(pos.x+width*biNor.x,pos.y+width*biNor.y, + angle*width*stTgt.x,angle*width*stTgt.y, + angle*width*biTgt.x,angle*width*biTgt.y); + dest->CubicTo(pos.x+width*enNor.x,pos.y+width*enNor.y, + angle*width*biTgt.x,angle*width*biTgt.y, + angle*width*enTgt.x,angle*width*enTgt.y); + }*/ + if (width > 0) { + dest->ArcTo (pos + width*enNor, + 1.0001 * width, 1.0001 * width, 0.0, false, true); + } else { + dest->ArcTo (pos + width*enNor, + -1.0001 * width, -1.0001 * width, 0.0, false, + false); + } + } else if (join == join_pointy) { + Geom::Point const biss = unit_vector(Geom::rot90( stNor - enNor )); + double c2 = Geom::dot (biss, enNor); + double l = width / c2; + if ( fabs(l) > miter) { + dest->LineTo (pos + width*enNor); + } else { + if (dest->descr_cmd[dest->descr_cmd.size() - 1]->getType() == descr_lineto) { + PathDescrLineTo* nLine = dynamic_cast(dest->descr_cmd[dest->descr_cmd.size() - 1]); + nLine->p = pos+l*biss; // relocate to bisector + } else { + dest->LineTo (pos+l*biss); + } + if (nType != descr_lineto) + dest->LineTo (pos+width*enNor); + } + } else { // Bevel join + dest->LineTo (pos + width*enNor); + } + } + } +} + +// les callbacks + +// see http://www.home.unix-ag.org/simon/sketch/pathstroke.py to understand what's happening here + +void +Path::RecStdCubicTo (outline_callback_data * data, double tol, double width, + int lev) +{ + Geom::Point stPos, miPos, enPos; + Geom::Point stTgt, enTgt, miTgt, stNor, enNor, miNor; + double stRad, miRad, enRad; + double stTle, miTle, enTle; + // un cubic + { + PathDescrCubicTo temp(Geom::Point(data->x2, data->y2), + Geom::Point(data->d.c.dx1, data->d.c.dy1), + Geom::Point(data->d.c.dx2, data->d.c.dy2)); + + Geom::Point initial_point(data->x1, data->y1); + TangentOnCubAt (0.0, initial_point, temp, false, stPos, stTgt, stTle, + stRad); + TangentOnCubAt (0.5, initial_point, temp, false, miPos, miTgt, miTle, + miRad); + TangentOnCubAt (1.0, initial_point, temp, true, enPos, enTgt, enTle, + enRad); + stNor=stTgt.cw(); + miNor=miTgt.cw(); + enNor=enTgt.cw(); + } + + double stGue = 1, miGue = 1, enGue = 1; + // correction of the lengths of the tangent to the offset + // if you don't see why i wrote that, draw a little figure and everything will be clear + if (fabs (stRad) > 0.01) + stGue += width / stRad; + if (fabs (miRad) > 0.01) + miGue += width / miRad; + if (fabs (enRad) > 0.01) + enGue += width / enRad; + stGue *= stTle; + miGue *= miTle; + enGue *= enTle; + + + if (lev <= 0) { + int n_d = data->dest->CubicTo (enPos + width*enNor, + stGue*stTgt, + enGue*enTgt); + if (n_d >= 0) { + data->dest->descr_cmd[n_d]->associated = data->piece; + data->dest->descr_cmd[n_d]->tSt = data->tSt; + data->dest->descr_cmd[n_d]->tEn = data->tEn; + } + return; + } + + Geom::Point chk; + const Geom::Point req = miPos + width * miNor; + { + PathDescrCubicTo temp(enPos + width * enNor, + stGue * stTgt, + enGue * enTgt); + double chTle, chRad; + Geom::Point chTgt; + TangentOnCubAt (0.5, stPos+width*stNor, + temp, false, chk, chTgt, chTle, chRad); + } + const Geom::Point diff = req - chk; + const double err = dot(diff,diff); + if (err <= tol ) { // tolerance is given as a quadratic value, no need to use tol*tol here +// printf("%f <= %f %i\n",err,tol,lev); + int n_d = data->dest->CubicTo (enPos + width*enNor, + stGue*stTgt, + enGue*enTgt); + if (n_d >= 0) { + data->dest->descr_cmd[n_d]->associated = data->piece; + data->dest->descr_cmd[n_d]->tSt = data->tSt; + data->dest->descr_cmd[n_d]->tEn = data->tEn; + } + } else { + outline_callback_data desc = *data; + + desc.tSt = data->tSt; + desc.tEn = (data->tSt + data->tEn) / 2; + desc.x1 = data->x1; + desc.y1 = data->y1; + desc.x2 = miPos[0]; + desc.y2 = miPos[1]; + desc.d.c.dx1 = 0.5 * stTle * stTgt[0]; + desc.d.c.dy1 = 0.5 * stTle * stTgt[1]; + desc.d.c.dx2 = 0.5 * miTle * miTgt[0]; + desc.d.c.dy2 = 0.5 * miTle * miTgt[1]; + RecStdCubicTo (&desc, tol, width, lev - 1); + + desc.tSt = (data->tSt + data->tEn) / 2; + desc.tEn = data->tEn; + desc.x1 = miPos[0]; + desc.y1 = miPos[1]; + desc.x2 = data->x2; + desc.y2 = data->y2; + desc.d.c.dx1 = 0.5 * miTle * miTgt[0]; + desc.d.c.dy1 = 0.5 * miTle * miTgt[1]; + desc.d.c.dx2 = 0.5 * enTle * enTgt[0]; + desc.d.c.dy2 = 0.5 * enTle * enTgt[1]; + RecStdCubicTo (&desc, tol, width, lev - 1); + } +} + +void +Path::StdCubicTo (Path::outline_callback_data * data, double tol, double width) +{ +// fflush (stdout); + RecStdCubicTo (data, tol, width, 8); +} + +void +Path::StdBezierTo (Path::outline_callback_data * data, double tol, double width) +{ + PathDescrBezierTo tempb(Geom::Point(data->x2, data->y2), 1); + PathDescrIntermBezierTo tempi(Geom::Point(data->d.b.mx, data->d.b.my)); + Geom::Point stPos, enPos, stTgt, enTgt; + double stRad, enRad, stTle, enTle; + Geom::Point tmp(data->x1,data->y1); + TangentOnBezAt (0.0, tmp, tempi, tempb, false, stPos, stTgt, + stTle, stRad); + TangentOnBezAt (1.0, tmp, tempi, tempb, true, enPos, enTgt, + enTle, enRad); + data->d.c.dx1 = stTle * stTgt[0]; + data->d.c.dy1 = stTle * stTgt[1]; + data->d.c.dx2 = enTle * enTgt[0]; + data->d.c.dy2 = enTle * enTgt[1]; + RecStdCubicTo (data, tol, width, 8); +} + +void +Path::RecStdArcTo (outline_callback_data * data, double tol, double width, + int lev) +{ + Geom::Point stPos, miPos, enPos; + Geom::Point stTgt, enTgt, miTgt, stNor, enNor, miNor; + double stRad, miRad, enRad; + double stTle, miTle, enTle; + // un cubic + { + PathDescrArcTo temp(Geom::Point(data->x2, data->y2), + data->d.a.rx, data->d.a.ry, + data->d.a.angle, data->d.a.large, data->d.a.clock); + + Geom::Point tmp(data->x1,data->y1); + TangentOnArcAt (data->d.a.stA, tmp, temp, stPos, stTgt, + stTle, stRad); + TangentOnArcAt ((data->d.a.stA + data->d.a.enA) / 2, tmp, + temp, miPos, miTgt, miTle, miRad); + TangentOnArcAt (data->d.a.enA, tmp, temp, enPos, enTgt, + enTle, enRad); + stNor=stTgt.cw(); + miNor=miTgt.cw(); + enNor=enTgt.cw(); + } + + double stGue = 1, miGue = 1, enGue = 1; + if (fabs (stRad) > 0.01) + stGue += width / stRad; + if (fabs (miRad) > 0.01) + miGue += width / miRad; + if (fabs (enRad) > 0.01) + enGue += width / enRad; + stGue *= stTle; + miGue *= miTle; + enGue *= enTle; + double sang, eang; + { + Geom::Point tms(data->x1,data->y1),tme(data->x2,data->y2); + ArcAngles (tms,tme, data->d.a.rx, + data->d.a.ry, data->d.a.angle*M_PI/180.0, data->d.a.large, !data->d.a.clock, + sang, eang); + } + double scal = eang - sang; + if (scal < 0) + scal += 2 * M_PI; + if (scal > 2 * M_PI) + scal -= 2 * M_PI; + scal *= data->d.a.enA - data->d.a.stA; + + if (lev <= 0) + { + int n_d = data->dest->CubicTo (enPos + width*enNor, + stGue*scal*stTgt, + enGue*scal*enTgt); + if (n_d >= 0) { + data->dest->descr_cmd[n_d]->associated = data->piece; + data->dest->descr_cmd[n_d]->tSt = data->d.a.stA; + data->dest->descr_cmd[n_d]->tEn = data->d.a.enA; + } + return; + } + + Geom::Point chk; + const Geom::Point req = miPos + width*miNor; + { + PathDescrCubicTo temp(enPos + width * enNor, stGue * scal * stTgt, enGue * scal * enTgt); + double chTle, chRad; + Geom::Point chTgt; + TangentOnCubAt (0.5, stPos+width*stNor, + temp, false, chk, chTgt, chTle, chRad); + } + const Geom::Point diff = req - chk; + const double err = (dot(diff,diff)); + if (err <= tol) // tolerance is given as a quadratic value, no need to use tol*tol here + { + int n_d = data->dest->CubicTo (enPos + width*enNor, + stGue*scal*stTgt, + enGue*scal*enTgt); + if (n_d >= 0) { + data->dest->descr_cmd[n_d]->associated = data->piece; + data->dest->descr_cmd[n_d]->tSt = data->d.a.stA; + data->dest->descr_cmd[n_d]->tEn = data->d.a.enA; + } + } else { + outline_callback_data desc = *data; + + desc.d.a.stA = data->d.a.stA; + desc.d.a.enA = (data->d.a.stA + data->d.a.enA) / 2; + RecStdArcTo (&desc, tol, width, lev - 1); + + desc.d.a.stA = (data->d.a.stA + data->d.a.enA) / 2; + desc.d.a.enA = data->d.a.enA; + RecStdArcTo (&desc, tol, width, lev - 1); + } +} + +void +Path::StdArcTo (Path::outline_callback_data * data, double tol, double width) +{ + data->d.a.stA = 0.0; + data->d.a.enA = 1.0; + RecStdArcTo (data, tol, width, 8); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/PathSimplify.cpp b/src/livarot/PathSimplify.cpp new file mode 100644 index 0000000..bf3e200 --- /dev/null +++ b/src/livarot/PathSimplify.cpp @@ -0,0 +1,1404 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include <2geom/affine.h> +#include "livarot/Path.h" +#include "livarot/path-description.h" + +/* + * Reassembling polyline segments into cubic bezier patches + * thes functions do not need the back data. but they are slower than recomposing + * path descriptions when you have said back data (it's always easier with a model) + * there's a bezier fitter in bezier-utils.cpp too. the main difference is the way bezier patch are split + * here: walk on the polyline, trying to extend the portion you can fit by respecting the treshhold, split when + * treshhold is exceeded. when encountering a "forced" point, lower the treshhold to favor splitting at that point + * in bezier-utils: fit the whole polyline, get the position with the higher deviation to the fitted curve, split + * there and recurse + */ + + +// algo d'origine: http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/INT-APP/CURVE-APP-global.html + +// need the b-spline basis for cubic splines +// pas oublier que c'est une b-spline clampee +// et que ca correspond a une courbe de bezier normale +#define N03(t) ((1.0-t)*(1.0-t)*(1.0-t)) +#define N13(t) (3*t*(1.0-t)*(1.0-t)) +#define N23(t) (3*t*t*(1.0-t)) +#define N33(t) (t*t*t) +// quadratic b-splines (jsut in case) +#define N02(t) ((1.0-t)*(1.0-t)) +#define N12(t) (2*t*(1.0-t)) +#define N22(t) (t*t) +// linear interpolation b-splines +#define N01(t) ((1.0-t)) +#define N11(t) (t) + + + +void Path::Simplify(double treshhold) +{ + if (pts.size() <= 1) { + return; + } + + Reset(); + + int lastM = 0; + while (lastM < int(pts.size())) { + int lastP = lastM + 1; + while (lastP < int(pts.size()) + && (pts[lastP].isMoveTo == polyline_lineto + || pts[lastP].isMoveTo == polyline_forced)) + { + lastP++; + } + + DoSimplify(lastM, lastP - lastM, treshhold); + + lastM = lastP; + } +} + + +#if 0 +// dichomtomic method to get distance to curve approximation +// a real polynomial solver would get the minimum more efficiently, but since the polynom +// would likely be of degree >= 5, that would imply using some generic solver, liek using the sturm method +static double RecDistanceToCubic(Geom::Point const &iS, Geom::Point const &isD, + Geom::Point const &iE, Geom::Point const &ieD, + Geom::Point &pt, double current, int lev, double st, double et) +{ + if ( lev <= 0 ) { + return current; + } + + Geom::Point const m = 0.5 * (iS + iE) + 0.125 * (isD - ieD); + Geom::Point const md = 0.75 * (iE - iS) - 0.125 * (isD + ieD); + double const mt = (st + et) / 2; + + Geom::Point const hisD = 0.5 * isD; + Geom::Point const hieD = 0.5 * ieD; + + Geom::Point const mp = pt - m; + double nle = Geom::dot(mp, mp); + + if ( nle < current ) { + + current = nle; + nle = RecDistanceToCubic(iS, hisD, m, md, pt, current, lev - 1, st, mt); + if ( nle < current ) { + current = nle; + } + nle = RecDistanceToCubic(m, md, iE, hieD, pt, current, lev - 1, mt, et); + if ( nle < current ) { + current = nle; + } + + } else if ( nle < 2 * current ) { + + nle = RecDistanceToCubic(iS, hisD, m, md, pt, current, lev - 1, st, mt); + if ( nle < current ) { + current = nle; + } + nle = RecDistanceToCubic(m, md, iE, hieD, pt, current, lev - 1, mt, et); + if ( nle < current ) { + current = nle; + } + } + + return current; +} +#endif + +static double DistanceToCubic(Geom::Point const &start, PathDescrCubicTo res, Geom::Point &pt) +{ + Geom::Point const sp = pt - start; + Geom::Point const ep = pt - res.p; + double nle = Geom::dot(sp, sp); + double nnle = Geom::dot(ep, ep); + if ( nnle < nle ) { + nle = nnle; + } + + Geom::Point seg = res.p - start; + nnle = Geom::cross(sp, seg); + nnle *= nnle; + nnle /= Geom::dot(seg, seg); + if ( nnle < nle ) { + if ( Geom::dot(sp,seg) >= 0 ) { + seg = start - res.p; + if ( Geom::dot(ep,seg) >= 0 ) { + nle = nnle; + } + } + } + + return nle; +} + + +/** + * Simplification on a subpath. + */ + +void Path::DoSimplify(int off, int N, double treshhold) +{ + // non-dichotomic method: grow an interval of points approximated by a curve, until you reach the treshhold, and repeat + if (N <= 1) { + return; + } + + int curP = 0; + + fitting_tables data; + data.Xk = data.Yk = data.Qk = nullptr; + data.tk = data.lk = nullptr; + data.fk = nullptr; + data.totLen = 0; + data.nbPt = data.maxPt = data.inPt = 0; + + Geom::Point const moveToPt = pts[off].p; + MoveTo(moveToPt); + Geom::Point endToPt = moveToPt; + + while (curP < N - 1) { + + int lastP = curP + 1; + int M = 2; + + // remettre a zero + data.inPt = data.nbPt = 0; + + PathDescrCubicTo res(Geom::Point(0, 0), Geom::Point(0, 0), Geom::Point(0, 0)); + bool contains_forced = false; + int step = 64; + + while ( step > 0 ) { + int forced_pt = -1; + int worstP = -1; + + do { + if (pts[off + lastP].isMoveTo == polyline_forced) { + contains_forced = true; + } + forced_pt = lastP; + lastP += step; + M += step; + } while (lastP < N && ExtendFit(off + curP, M, data, + (contains_forced) ? 0.05 * treshhold : treshhold, + res, worstP) ); + if (lastP >= N) { + + lastP -= step; + M -= step; + + } else { + // le dernier a echoue + lastP -= step; + M -= step; + + if ( contains_forced ) { + lastP = forced_pt; + M = lastP - curP + 1; + } + + AttemptSimplify(off + curP, M, treshhold, res, worstP); // ca passe forcement + } + step /= 2; + } + + endToPt = pts[off + lastP].p; + if (M <= 2) { + LineTo(endToPt); + } else { + CubicTo(endToPt, res.start, res.end); + } + + curP = lastP; + } + + if (Geom::LInfty(endToPt - moveToPt) < 0.00001) { + Close(); + } + + g_free(data.Xk); + g_free(data.Yk); + g_free(data.Qk); + g_free(data.tk); + g_free(data.lk); + g_free(data.fk); +} + + +// warning: slow +// idea behind this feature: splotches appear when trying to fit a small number of points: you can +// get a cubic bezier that fits the points very well but doesn't fit the polyline itself +// so we add a bit of the error at the middle of each segment of the polyline +// also we restrict this to <=20 points, to avoid unnecessary computations +#define with_splotch_killer + +// primitive= calc the cubic bezier patche that fits Xk and Yk best +// Qk est deja alloue +// retourne false si probleme (matrice non-inversible) +bool Path::FitCubic(Geom::Point const &start, PathDescrCubicTo &res, + double *Xk, double *Yk, double *Qk, double *tk, int nbPt) +{ + Geom::Point const end = res.p; + + // la matrice tNN + Geom::Affine M(0, 0, 0, 0, 0, 0); + for (int i = 1; i < nbPt - 1; i++) { + M[0] += N13(tk[i]) * N13(tk[i]); + M[1] += N23(tk[i]) * N13(tk[i]); + M[2] += N13(tk[i]) * N23(tk[i]); + M[3] += N23(tk[i]) * N23(tk[i]); + } + + double const det = M.det(); + if (fabs(det) < 0.000001) { + res.start[0]=res.start[1]=0.0; + res.end[0]=res.end[1]=0.0; + return false; + } + + Geom::Affine const iM = M.inverse(); + M = iM; + + // phase 1: abcisses + // calcul des Qk + Xk[0] = start[0]; + Yk[0] = start[1]; + Xk[nbPt - 1] = end[0]; + Yk[nbPt - 1] = end[1]; + + for (int i = 1; i < nbPt - 1; i++) { + Qk[i] = Xk[i] - N03 (tk[i]) * Xk[0] - N33 (tk[i]) * Xk[nbPt - 1]; + } + + // le vecteur Q + Geom::Point Q(0, 0); + for (int i = 1; i < nbPt - 1; i++) { + Q[0] += N13 (tk[i]) * Qk[i]; + Q[1] += N23 (tk[i]) * Qk[i]; + } + + Geom::Point P = Q * M; + Geom::Point cp1; + Geom::Point cp2; + cp1[Geom::X] = P[Geom::X]; + cp2[Geom::X] = P[Geom::Y]; + + // phase 2: les ordonnees + for (int i = 1; i < nbPt - 1; i++) { + Qk[i] = Yk[i] - N03 (tk[i]) * Yk[0] - N33 (tk[i]) * Yk[nbPt - 1]; + } + + // le vecteur Q + Q = Geom::Point(0, 0); + for (int i = 1; i < nbPt - 1; i++) { + Q[0] += N13 (tk[i]) * Qk[i]; + Q[1] += N23 (tk[i]) * Qk[i]; + } + + P = Q * M; + cp1[Geom::Y] = P[Geom::X]; + cp2[Geom::Y] = P[Geom::Y]; + + res.start = 3.0 * (cp1 - start); + res.end = 3.0 * (end - cp2 ); + + return true; +} + + +bool Path::ExtendFit(int off, int N, fitting_tables &data, double treshhold, PathDescrCubicTo &res, int &worstP) +{ + if ( N >= data.maxPt ) { + data.maxPt = 2 * N + 1; + data.Xk = (double *) g_realloc(data.Xk, data.maxPt * sizeof(double)); + data.Yk = (double *) g_realloc(data.Yk, data.maxPt * sizeof(double)); + data.Qk = (double *) g_realloc(data.Qk, data.maxPt * sizeof(double)); + data.tk = (double *) g_realloc(data.tk, data.maxPt * sizeof(double)); + data.lk = (double *) g_realloc(data.lk, data.maxPt * sizeof(double)); + data.fk = (char *) g_realloc(data.fk, data.maxPt * sizeof(char)); + } + + if ( N > data.inPt ) { + for (int i = data.inPt; i < N; i++) { + data.Xk[i] = pts[off + i].p[Geom::X]; + data.Yk[i] = pts[off + i].p[Geom::Y]; + data.fk[i] = ( pts[off + i].isMoveTo == polyline_forced ) ? 0x01 : 0x00; + } + data.lk[0] = 0; + data.tk[0] = 0; + + double prevLen = 0; + for (int i = 0; i < data.inPt; i++) { + prevLen += data.lk[i]; + } + data.totLen = prevLen; + + for (int i = ( (data.inPt > 0) ? data.inPt : 1); i < N; i++) { + Geom::Point diff; + diff[Geom::X] = data.Xk[i] - data.Xk[i - 1]; + diff[Geom::Y] = data.Yk[i] - data.Yk[i - 1]; + data.lk[i] = Geom::L2(diff); + data.totLen += data.lk[i]; + data.tk[i] = data.totLen; + } + + for (int i = 0; i < data.inPt; i++) { + data.tk[i] *= prevLen; + data.tk[i] /= data.totLen; + } + + for (int i = data.inPt; i < N; i++) { + data.tk[i] /= data.totLen; + } + data.inPt = N; + } + + if ( N < data.nbPt ) { + // We've gone too far; we'll have to recalulate the .tk. + data.totLen = 0; + data.tk[0] = 0; + data.lk[0] = 0; + for (int i = 1; i < N; i++) { + data.totLen += data.lk[i]; + data.tk[i] = data.totLen; + } + + for (int i = 1; i < N; i++) { + data.tk[i] /= data.totLen; + } + } + + data.nbPt = N; + + if ( data.nbPt <= 0 ) { + return false; + } + + res.p[0] = data.Xk[data.nbPt - 1]; + res.p[1] = data.Yk[data.nbPt - 1]; + res.start[0] = res.start[1] = 0; + res.end[0] = res.end[1] = 0; + worstP = 1; + if ( N <= 2 ) { + return true; + } + + if ( data.totLen < 0.0001 ) { + double worstD = 0; + Geom::Point start; + worstP = -1; + start[0] = data.Xk[0]; + start[1] = data.Yk[0]; + for (int i = 1; i < N; i++) { + Geom::Point nPt; + bool isForced = data.fk[i]; + nPt[0] = data.Xk[i]; + nPt[1] = data.Yk[i]; + + double nle = DistanceToCubic(start, res, nPt); + if ( isForced ) { + // forced points are favored for splitting the recursion; we do this by increasing their distance + if ( worstP < 0 || 2*nle > worstD ) { + worstP = i; + worstD = 2*nle; + } + } else { + if ( worstP < 0 || nle > worstD ) { + worstP = i; + worstD = nle; + } + } + } + + return true; + } + + return AttemptSimplify(data, treshhold, res, worstP); +} + + +// fit a polyline to a bezier patch, return true is treshhold not exceeded (ie: you can continue) +// version that uses tables from the previous iteration, to minimize amount of work done +bool Path::AttemptSimplify (fitting_tables &data,double treshhold, PathDescrCubicTo & res,int &worstP) +{ + Geom::Point start,end; + // pour une coordonnee + Geom::Point cp1, cp2; + + worstP = 1; + if (pts.size() == 2) { + return true; + } + + start[0] = data.Xk[0]; + start[1] = data.Yk[0]; + cp1[0] = data.Xk[1]; + cp1[1] = data.Yk[1]; + end[0] = data.Xk[data.nbPt - 1]; + end[1] = data.Yk[data.nbPt - 1]; + cp2 = cp1; + + if (pts.size() == 3) { + // start -> cp1 -> end + res.start = cp1 - start; + res.end = end - cp1; + worstP = 1; + return true; + } + + if ( FitCubic(start, res, data.Xk, data.Yk, data.Qk, data.tk, data.nbPt) ) { + cp1 = start + res.start / 3; + cp2 = end - res.end / 3; + } else { + // aie, non-inversible + double worstD = 0; + worstP = -1; + for (int i = 1; i < data.nbPt; i++) { + Geom::Point nPt; + nPt[Geom::X] = data.Xk[i]; + nPt[Geom::Y] = data.Yk[i]; + double nle = DistanceToCubic(start, res, nPt); + if ( data.fk[i] ) { + // forced points are favored for splitting the recursion; we do this by increasing their distance + if ( worstP < 0 || 2 * nle > worstD ) { + worstP = i; + worstD = 2 * nle; + } + } else { + if ( worstP < 0 || nle > worstD ) { + worstP = i; + worstD = nle; + } + } + } + return false; + } + + // calcul du delta= pondere par les longueurs des segments + double delta = 0; + { + double worstD = 0; + worstP = -1; + Geom::Point prevAppP; + Geom::Point prevP; + double prevDist; + prevP[Geom::X] = data.Xk[0]; + prevP[Geom::Y] = data.Yk[0]; + prevAppP = prevP; // le premier seulement + prevDist = 0; +#ifdef with_splotch_killer + if ( data.nbPt <= 20 ) { + for (int i = 1; i < data.nbPt - 1; i++) { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + Geom::Point midAppP; + Geom::Point midP; + double midDist; + + curAppP[Geom::X] = N13(data.tk[i]) * cp1[Geom::X] + + N23(data.tk[i]) * cp2[Geom::X] + + N03(data.tk[i]) * data.Xk[0] + + N33(data.tk[i]) * data.Xk[data.nbPt - 1]; + + curAppP[Geom::Y] = N13(data.tk[i]) * cp1[Geom::Y] + + N23(data.tk[i]) * cp2[Geom::Y] + + N03(data.tk[i]) * data.Yk[0] + + N33(data.tk[i]) * data.Yk[data.nbPt - 1]; + + curP[Geom::X] = data.Xk[i]; + curP[Geom::Y] = data.Yk[i]; + double mtk = 0.5 * (data.tk[i] + data.tk[i - 1]); + + midAppP[Geom::X] = N13(mtk) * cp1[Geom::X] + + N23(mtk) * cp2[Geom::X] + + N03(mtk) * data.Xk[0] + + N33(mtk) * data.Xk[data.nbPt - 1]; + + midAppP[Geom::Y] = N13(mtk) * cp1[Geom::Y] + + N23(mtk) * cp2[Geom::Y] + + N03(mtk) * data.Yk[0] + + N33(mtk) * data.Yk[data.nbPt - 1]; + + midP = 0.5 * (curP + prevP); + + Geom::Point diff = curAppP - curP; + curDist = dot(diff, diff); + diff = midAppP - midP; + midDist = dot(diff, diff); + + delta += 0.3333 * (curDist + prevDist + midDist) * data.lk[i]; + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( data.fk[i] && 2 * curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } + delta /= data.totLen; + + } else { +#endif + for (int i = 1; i < data.nbPt - 1; i++) { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + + curAppP[Geom::X] = N13(data.tk[i]) * cp1[Geom::X] + + N23(data.tk[i]) * cp2[Geom::X] + + N03(data.tk[i]) * data.Xk[0] + + N33(data.tk[i]) * data.Xk[data.nbPt - 1]; + + curAppP[Geom::Y] = N13(data.tk[i]) * cp1[Geom::Y] + + N23(data.tk[i]) * cp2[Geom::Y] + + N03(data.tk[i]) * data.Yk[0] + + N33(data.tk[i]) * data.Yk[data.nbPt - 1]; + + curP[Geom::X] = data.Xk[i]; + curP[Geom::Y] = data.Yk[i]; + + Geom::Point diff = curAppP-curP; + curDist = dot(diff, diff); + delta += curDist; + + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( data.fk[i] && 2 * curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } +#ifdef with_splotch_killer + } +#endif + } + + if (delta < treshhold * treshhold) { + // premier jet + + // Refine a little. + for (int i = 1; i < data.nbPt - 1; i++) { + Geom::Point pt(data.Xk[i], data.Yk[i]); + data.tk[i] = RaffineTk(pt, start, cp1, cp2, end, data.tk[i]); + if (data.tk[i] < data.tk[i - 1]) { + // Force tk to be monotonic non-decreasing. + data.tk[i] = data.tk[i - 1]; + } + } + + if ( FitCubic(start, res, data.Xk, data.Yk, data.Qk, data.tk, data.nbPt) == false) { + // ca devrait jamais arriver, mais bon + res.start = 3.0 * (cp1 - start); + res.end = 3.0 * (end - cp2 ); + return true; + } + + double ndelta = 0; + { + double worstD = 0; + worstP = -1; + Geom::Point prevAppP; + Geom::Point prevP(data.Xk[0], data.Yk[0]); + double prevDist = 0; + prevAppP = prevP; // le premier seulement +#ifdef with_splotch_killer + if ( data.nbPt <= 20 ) { + for (int i = 1; i < data.nbPt - 1; i++) { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + Geom::Point midAppP; + Geom::Point midP; + double midDist; + + curAppP[Geom::X] = N13(data.tk[i]) * cp1[Geom::X] + + N23(data.tk[i]) * cp2[Geom::X] + + N03(data.tk[i]) * data.Xk[0] + + N33(data.tk[i]) * data.Xk[data.nbPt - 1]; + + curAppP[Geom::Y] = N13(data.tk[i]) * cp1[Geom::Y] + + N23(data.tk[i]) * cp2[Geom::Y] + + N03(data.tk[i]) * data.Yk[0] + + N33(data.tk[i]) * data.Yk[data.nbPt - 1]; + + curP[Geom::X] = data.Xk[i]; + curP[Geom::Y] = data.Yk[i]; + double mtk = 0.5 * (data.tk[i] + data.tk[i - 1]); + + midAppP[Geom::X] = N13(mtk) * cp1[Geom::X] + + N23(mtk) * cp2[Geom::X] + + N03(mtk) * data.Xk[0] + + N33(mtk) * data.Xk[data.nbPt - 1]; + + midAppP[Geom::Y] = N13(mtk) * cp1[Geom::Y] + + N23(mtk) * cp2[Geom::Y] + + N03(mtk) * data.Yk[0] + + N33(mtk) * data.Yk[data.nbPt - 1]; + + midP = 0.5 * (curP + prevP); + + Geom::Point diff = curAppP - curP; + curDist = dot(diff, diff); + + diff = midAppP - midP; + midDist = dot(diff, diff); + + ndelta += 0.3333 * (curDist + prevDist + midDist) * data.lk[i]; + + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( data.fk[i] && 2 * curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } + ndelta /= data.totLen; + } else { +#endif + for (int i = 1; i < data.nbPt - 1; i++) { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + + curAppP[Geom::X] = N13(data.tk[i]) * cp1[Geom::X] + + N23(data.tk[i]) * cp2[Geom::X] + + N03(data.tk[i]) * data.Xk[0] + + N33(data.tk[i]) * data.Xk[data.nbPt - 1]; + + curAppP[Geom::Y] = N13(data.tk[i]) * cp1[Geom::Y] + + N23(data.tk[i]) * cp2[1] + + N03(data.tk[i]) * data.Yk[0] + + N33(data.tk[i]) * data.Yk[data.nbPt - 1]; + + curP[Geom::X] = data.Xk[i]; + curP[Geom::Y] = data.Yk[i]; + + Geom::Point diff = curAppP - curP; + curDist = dot(diff, diff); + + ndelta += curDist; + + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( data.fk[i] && 2 * curDist > worstD ) { + worstD = 2 * curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } +#ifdef with_splotch_killer + } +#endif + } + + if (ndelta < delta + 0.00001) { + return true; + } else { + // nothing better to do + res.start = 3.0 * (cp1 - start); + res.end = 3.0 * (end - cp2 ); + } + + return true; + } + + return false; +} + + +bool Path::AttemptSimplify(int off, int N, double treshhold, PathDescrCubicTo &res,int &worstP) +{ + Geom::Point start; + Geom::Point end; + + // pour une coordonnee + double *Xk; // la coordonnee traitee (x puis y) + double *Yk; // la coordonnee traitee (x puis y) + double *lk; // les longueurs de chaque segment + double *tk; // les tk + double *Qk; // les Qk + char *fk; // si point force + + Geom::Point cp1; + Geom::Point cp2; + + if (N == 2) { + worstP = 1; + return true; + } + + start = pts[off].p; + cp1 = pts[off + 1].p; + end = pts[off + N - 1].p; + + res.p = end; + res.start[0] = res.start[1] = 0; + res.end[0] = res.end[1] = 0; + if (N == 3) { + // start -> cp1 -> end + res.start = cp1 - start; + res.end = end - cp1; + worstP = 1; + return true; + } + + // Totally inefficient, allocates & deallocates all the time. + tk = (double *) g_malloc(N * sizeof(double)); + Qk = (double *) g_malloc(N * sizeof(double)); + Xk = (double *) g_malloc(N * sizeof(double)); + Yk = (double *) g_malloc(N * sizeof(double)); + lk = (double *) g_malloc(N * sizeof(double)); + fk = (char *) g_malloc(N * sizeof(char)); + + // chord length method + tk[0] = 0.0; + lk[0] = 0.0; + { + Geom::Point prevP = start; + for (int i = 1; i < N; i++) { + Xk[i] = pts[off + i].p[Geom::X]; + Yk[i] = pts[off + i].p[Geom::Y]; + + if ( pts[off + i].isMoveTo == polyline_forced ) { + fk[i] = 0x01; + } else { + fk[i] = 0; + } + + Geom::Point diff(Xk[i] - prevP[Geom::X], Yk[i] - prevP[1]); + prevP[0] = Xk[i]; + prevP[1] = Yk[i]; + lk[i] = Geom::L2(diff); + tk[i] = tk[i - 1] + lk[i]; + } + } + + if (tk[N - 1] < 0.00001) { + // longueur nulle + res.start[0] = res.start[1] = 0; + res.end[0] = res.end[1] = 0; + double worstD = 0; + worstP = -1; + for (int i = 1; i < N; i++) { + Geom::Point nPt; + bool isForced = fk[i]; + nPt[0] = Xk[i]; + nPt[1] = Yk[i]; + + double nle = DistanceToCubic(start, res, nPt); + if ( isForced ) { + // forced points are favored for splitting the recursion; we do this by increasing their distance + if ( worstP < 0 || 2 * nle > worstD ) { + worstP = i; + worstD = 2 * nle; + } + } else { + if ( worstP < 0 || nle > worstD ) { + worstP = i; + worstD = nle; + } + } + } + + g_free(tk); + g_free(Qk); + g_free(Xk); + g_free(Yk); + g_free(fk); + g_free(lk); + + return false; + } + + double totLen = tk[N - 1]; + for (int i = 1; i < N - 1; i++) { + tk[i] /= totLen; + } + + res.p = end; + if ( FitCubic(start, res, Xk, Yk, Qk, tk, N) ) { + cp1 = start + res.start / 3; + cp2 = end + res.end / 3; + } else { + // aie, non-inversible + res.start[0] = res.start[1] = 0; + res.end[0] = res.end[1] = 0; + double worstD = 0; + worstP = -1; + for (int i = 1; i < N; i++) { + Geom::Point nPt(Xk[i], Yk[i]); + bool isForced = fk[i]; + double nle = DistanceToCubic(start, res, nPt); + if ( isForced ) { + // forced points are favored for splitting the recursion; we do this by increasing their distance + if ( worstP < 0 || 2 * nle > worstD ) { + worstP = i; + worstD = 2 * nle; + } + } else { + if ( worstP < 0 || nle > worstD ) { + worstP = i; + worstD = nle; + } + } + } + + g_free(tk); + g_free(Qk); + g_free(Xk); + g_free(Yk); + g_free(fk); + g_free(lk); + return false; + } + + // calcul du delta= pondere par les longueurs des segments + double delta = 0; + { + double worstD = 0; + worstP = -1; + Geom::Point prevAppP; + Geom::Point prevP; + double prevDist; + prevP[0] = Xk[0]; + prevP[1] = Yk[0]; + prevAppP = prevP; // le premier seulement + prevDist = 0; +#ifdef with_splotch_killer + if ( N <= 20 ) { + for (int i = 1; i < N - 1; i++) + { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + Geom::Point midAppP; + Geom::Point midP; + double midDist; + + curAppP[0] = N13 (tk[i]) * cp1[0] + N23 (tk[i]) * cp2[0] + N03 (tk[i]) * Xk[0] + N33 (tk[i]) * Xk[N - 1]; + curAppP[1] = N13 (tk[i]) * cp1[1] + N23 (tk[i]) * cp2[1] + N03 (tk[i]) * Yk[0] + N33 (tk[i]) * Yk[N - 1]; + curP[0] = Xk[i]; + curP[1] = Yk[i]; + midAppP[0] = N13 (0.5*(tk[i]+tk[i-1])) * cp1[0] + N23 (0.5*(tk[i]+tk[i-1])) * cp2[0] + N03 (0.5*(tk[i]+tk[i-1])) * Xk[0] + N33 (0.5*(tk[i]+tk[i-1])) * Xk[N - 1]; + midAppP[1] = N13 (0.5*(tk[i]+tk[i-1])) * cp1[1] + N23 (0.5*(tk[i]+tk[i-1])) * cp2[1] + N03 (0.5*(tk[i]+tk[i-1])) * Yk[0] + N33 (0.5*(tk[i]+tk[i-1])) * Yk[N - 1]; + midP=0.5*(curP+prevP); + + Geom::Point diff; + diff = curAppP-curP; + curDist = dot(diff,diff); + + diff = midAppP-midP; + midDist = dot(diff,diff); + + delta+=0.3333*(curDist+prevDist+midDist)/**lk[i]*/; + + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( fk[i] && 2*curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } + delta/=totLen; + } else { +#endif + for (int i = 1; i < N - 1; i++) + { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + + curAppP[0] = N13 (tk[i]) * cp1[0] + N23 (tk[i]) * cp2[0] + N03 (tk[i]) * Xk[0] + N33 (tk[i]) * Xk[N - 1]; + curAppP[1] = N13 (tk[i]) * cp1[1] + N23 (tk[i]) * cp2[1] + N03 (tk[i]) * Yk[0] + N33 (tk[i]) * Yk[N - 1]; + curP[0] = Xk[i]; + curP[1] = Yk[i]; + + Geom::Point diff; + diff = curAppP-curP; + curDist = dot(diff,diff); + delta += curDist; + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( fk[i] && 2*curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } +#ifdef with_splotch_killer + } +#endif + } + + if (delta < treshhold * treshhold) + { + // premier jet + res.start = 3.0 * (cp1 - start); + res.end = -3.0 * (cp2 - end); + res.p = end; + + // Refine a little. + for (int i = 1; i < N - 1; i++) + { + Geom::Point + pt; + pt[0] = Xk[i]; + pt[1] = Yk[i]; + tk[i] = RaffineTk (pt, start, cp1, cp2, end, tk[i]); + if (tk[i] < tk[i - 1]) + { + // Force tk to be monotonic non-decreasing. + tk[i] = tk[i - 1]; + } + } + + if ( FitCubic(start,res,Xk,Yk,Qk,tk,N) ) { + } else { + // ca devrait jamais arriver, mais bon + res.start = 3.0 * (cp1 - start); + res.end = -3.0 * (cp2 - end); + g_free(tk); + g_free(Qk); + g_free(Xk); + g_free(Yk); + g_free(fk); + g_free(lk); + return true; + } + double ndelta = 0; + { + double worstD = 0; + worstP = -1; + Geom::Point prevAppP; + Geom::Point prevP; + double prevDist; + prevP[0] = Xk[0]; + prevP[1] = Yk[0]; + prevAppP = prevP; // le premier seulement + prevDist = 0; +#ifdef with_splotch_killer + if ( N <= 20 ) { + for (int i = 1; i < N - 1; i++) + { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + Geom::Point midAppP; + Geom::Point midP; + double midDist; + + curAppP[0] = N13 (tk[i]) * cp1[0] + N23 (tk[i]) * cp2[0] + N03 (tk[i]) * Xk[0] + N33 (tk[i]) * Xk[N - 1]; + curAppP[1] = N13 (tk[i]) * cp1[1] + N23 (tk[i]) * cp2[1] + N03 (tk[i]) * Yk[0] + N33 (tk[i]) * Yk[N - 1]; + curP[0] = Xk[i]; + curP[1] = Yk[i]; + midAppP[0] = N13 (0.5*(tk[i]+tk[i-1])) * cp1[0] + N23 (0.5*(tk[i]+tk[i-1])) * cp2[0] + N03 (0.5*(tk[i]+tk[i-1])) * Xk[0] + N33 (0.5*(tk[i]+tk[i-1])) * Xk[N - 1]; + midAppP[1] = N13 (0.5*(tk[i]+tk[i-1])) * cp1[1] + N23 (0.5*(tk[i]+tk[i-1])) * cp2[1] + N03 (0.5*(tk[i]+tk[i-1])) * Yk[0] + N33 (0.5*(tk[i]+tk[i-1])) * Yk[N - 1]; + midP = 0.5*(curP+prevP); + + Geom::Point diff; + diff = curAppP-curP; + curDist = dot(diff,diff); + diff = midAppP-midP; + midDist = dot(diff,diff); + + ndelta+=0.3333*(curDist+prevDist+midDist)/**lk[i]*/; + + if ( curDist > worstD ) { + worstD = curDist; + worstP = i; + } else if ( fk[i] && 2*curDist > worstD ) { + worstD = 2*curDist; + worstP = i; + } + prevP = curP; + prevAppP = curAppP; + prevDist = curDist; + } + ndelta /= totLen; + } else { +#endif + for (int i = 1; i < N - 1; i++) + { + Geom::Point curAppP; + Geom::Point curP; + double curDist; + + curAppP[0] = N13 (tk[i]) * cp1[0] + N23 (tk[i]) * cp2[0] + N03 (tk[i]) * Xk[0] + N33 (tk[i]) * Xk[N - 1]; + curAppP[1] = N13 (tk[i]) * cp1[1] + N23 (tk[i]) * cp2[1] + N03 (tk[i]) * Yk[0] + N33 (tk[i]) * Yk[N - 1]; + curP[0]=Xk[i]; + curP[1]=Yk[i]; + + Geom::Point diff; + diff=curAppP-curP; + curDist=dot(diff,diff); + ndelta+=curDist; + + if ( curDist > worstD ) { + worstD=curDist; + worstP=i; + } else if ( fk[i] && 2*curDist > worstD ) { + worstD=2*curDist; + worstP=i; + } + prevP=curP; + prevAppP=curAppP; + prevDist=curDist; + } +#ifdef with_splotch_killer + } +#endif + } + + g_free(tk); + g_free(Qk); + g_free(Xk); + g_free(Yk); + g_free(fk); + g_free(lk); + + if (ndelta < delta + 0.00001) + { + return true; + } else { + // nothing better to do + res.start = 3.0 * (cp1 - start); + res.end = -3.0 * (cp2 - end); + } + return true; + } else { + // nothing better to do + } + + g_free(tk); + g_free(Qk); + g_free(Xk); + g_free(Yk); + g_free(fk); + g_free(lk); + return false; +} + +double Path::RaffineTk (Geom::Point pt, Geom::Point p0, Geom::Point p1, Geom::Point p2, Geom::Point p3, double it) +{ + // Refinement of the tk values. + // Just one iteration of Newtow Raphson, given that we're approaching the curve anyway. + // [fr: vu que de toute facon la courbe est approchC)e] + double const Ax = pt[Geom::X] - + p0[Geom::X] * N03(it) - + p1[Geom::X] * N13(it) - + p2[Geom::X] * N23(it) - + p3[Geom::X] * N33(it); + + double const Bx = (p1[Geom::X] - p0[Geom::X]) * N02(it) + + (p2[Geom::X] - p1[Geom::X]) * N12(it) + + (p3[Geom::X] - p2[Geom::X]) * N22(it); + + double const Cx = (p0[Geom::X] - 2 * p1[Geom::X] + p2[Geom::X]) * N01(it) + + (p3[Geom::X] - 2 * p2[Geom::X] + p1[Geom::X]) * N11(it); + + double const Ay = pt[Geom::Y] - + p0[Geom::Y] * N03(it) - + p1[Geom::Y] * N13(it) - + p2[Geom::Y] * N23(it) - + p3[Geom::Y] * N33(it); + + double const By = (p1[Geom::Y] - p0[Geom::Y]) * N02(it) + + (p2[Geom::Y] - p1[Geom::Y]) * N12(it) + + (p3[Geom::Y] - p2[Geom::Y]) * N22(it); + + double const Cy = (p0[Geom::Y] - 2 * p1[Geom::Y] + p2[Geom::Y]) * N01(it) + + (p3[Geom::Y] - 2 * p2[Geom::Y] + p1[Geom::Y]) * N11(it); + + double const dF = -6 * (Ax * Bx + Ay * By); + double const ddF = 18 * (Bx * Bx + By * By) - 12 * (Ax * Cx + Ay * Cy); + if (fabs (ddF) > 0.0000001) { + return it - dF / ddF; + } + + return it; +} + +// Variation on the fitting theme: try to merge path commands into cubic bezier patches. +// The goal is to reduce the number of path commands, especially when operations on path produce +// lots of small path elements; ideally you could get rid of very small segments at reduced visual cost. +void Path::Coalesce(double tresh) +{ + if ( descr_flags & descr_adding_bezier ) { + CancelBezier(); + } + + if ( descr_flags & descr_doing_subpath ) { + CloseSubpath(); + } + + if (descr_cmd.size() <= 2) { + return; + } + + SetBackData(false); + Path* tempDest = new Path(); + tempDest->SetBackData(false); + + ConvertEvenLines(0.25*tresh); + + int lastP = 0; + int lastAP = -1; + // As the elements are stored in a separate array, it's no longer worth optimizing + // the rewriting in the same array. + // [[comme les elements sont stockes dans un tableau a part, plus la peine d'optimiser + // la réécriture dans la meme tableau]] + + int lastA = descr_cmd[0]->associated; + int prevA = lastA; + Geom::Point firstP; + + /* FIXME: the use of this variable probably causes a leak or two. + ** It's a hack anyway, and probably only needs to be a type rather than + ** a full PathDescr. + */ + std::unique_ptr lastAddition(new PathDescrMoveTo(Geom::Point(0, 0))); + bool containsForced = false; + PathDescrCubicTo pending_cubic(Geom::Point(0, 0), Geom::Point(0, 0), Geom::Point(0, 0)); + + for (int curP = 0; curP < int(descr_cmd.size()); curP++) { + int typ = descr_cmd[curP]->getType(); + int nextA = lastA; + + if (typ == descr_moveto) { + + if (lastAddition->flags != descr_moveto) { + FlushPendingAddition(tempDest,lastAddition.get(),pending_cubic,lastAP); + } + lastAddition.reset(descr_cmd[curP]->clone()); + lastAP = curP; + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + // Added automatically (too bad about multiple moveto's). + // [fr: (tant pis pour les moveto multiples)] + containsForced = false; + + PathDescrMoveTo *nData = dynamic_cast(descr_cmd[curP]); + firstP = nData->p; + lastA = descr_cmd[curP]->associated; + prevA = lastA; + lastP = curP; + + } else if (typ == descr_close) { + nextA = descr_cmd[curP]->associated; + if (lastAddition->flags != descr_moveto) { + + PathDescrCubicTo res(Geom::Point(0, 0), Geom::Point(0, 0), Geom::Point(0, 0)); + int worstP = -1; + if (AttemptSimplify(lastA, nextA - lastA + 1, (containsForced) ? 0.05 * tresh : tresh, res, worstP)) { + lastAddition.reset(new PathDescrCubicTo(Geom::Point(0, 0), + Geom::Point(0, 0), + Geom::Point(0, 0))); + pending_cubic = res; + lastAP = -1; + } + + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + FlushPendingAddition(tempDest, descr_cmd[curP], pending_cubic, curP); + + } else { + FlushPendingAddition(tempDest,descr_cmd[curP],pending_cubic,curP); + } + + containsForced = false; + lastAddition.reset(new PathDescrMoveTo(Geom::Point(0, 0))); + prevA = lastA = nextA; + lastP = curP; + lastAP = curP; + + } else if (typ == descr_forced) { + + nextA = descr_cmd[curP]->associated; + if (lastAddition->flags != descr_moveto) { + + PathDescrCubicTo res(Geom::Point(0, 0), Geom::Point(0, 0), Geom::Point(0, 0)); + int worstP = -1; + if (AttemptSimplify(lastA, nextA - lastA + 1, 0.05 * tresh, res, worstP)) { + // plus sensible parce que point force + // ca passe + /* (Possible translation: More sensitive because contains a forced point.) */ + containsForced = true; + } else { + // Force the addition. + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + lastAddition.reset(new PathDescrMoveTo(Geom::Point(0, 0))); + prevA = lastA = nextA; + lastP = curP; + lastAP = curP; + containsForced = false; + } + } + + } else if (typ == descr_lineto || typ == descr_cubicto || typ == descr_arcto) { + + nextA = descr_cmd[curP]->associated; + if (lastAddition->flags != descr_moveto) { + + PathDescrCubicTo res(Geom::Point(0, 0), Geom::Point(0, 0), Geom::Point(0, 0)); + int worstP = -1; + if (AttemptSimplify(lastA, nextA - lastA + 1, tresh, res, worstP)) { + lastAddition.reset(new PathDescrCubicTo(Geom::Point(0, 0), + Geom::Point(0, 0), + Geom::Point(0, 0))); + pending_cubic = res; + lastAddition->associated = lastA; + lastP = curP; + lastAP = -1; + } else { + lastA = descr_cmd[lastP]->associated; // pourrait etre surecrit par la ligne suivante + /* (possible translation: Could be overwritten by the next line.) */ + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + lastAddition.reset(descr_cmd[curP]->clone()); + if ( typ == descr_cubicto ) { + pending_cubic = *(dynamic_cast(descr_cmd[curP])); + } + lastAP = curP; + containsForced = false; + } + + } else { + lastA = prevA /*descr_cmd[curP-1]->associated */ ; + lastAddition.reset(descr_cmd[curP]->clone()); + if ( typ == descr_cubicto ) { + pending_cubic = *(dynamic_cast(descr_cmd[curP])); + } + lastAP = curP; + containsForced = false; + } + prevA = nextA; + + } else if (typ == descr_bezierto) { + + if (lastAddition->flags != descr_moveto) { + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + lastAddition.reset(new PathDescrMoveTo(Geom::Point(0, 0))); + } + lastAP = -1; + lastA = descr_cmd[curP]->associated; + lastP = curP; + PathDescrBezierTo *nBData = dynamic_cast(descr_cmd[curP]); + for (int i = 1; i <= nBData->nb; i++) { + FlushPendingAddition(tempDest, descr_cmd[curP + i], pending_cubic, curP + i); + } + curP += nBData->nb; + prevA = nextA; + + } else if (typ == descr_interm_bezier) { + continue; + } else { + continue; + } + } + + if (lastAddition->flags != descr_moveto) { + FlushPendingAddition(tempDest, lastAddition.get(), pending_cubic, lastAP); + } + + Copy(tempDest); + delete tempDest; +} + + +void Path::FlushPendingAddition(Path *dest, PathDescr *lastAddition, + PathDescrCubicTo &lastCubic, int lastAP) +{ + switch (lastAddition->getType()) { + + case descr_moveto: + if ( lastAP >= 0 ) { + PathDescrMoveTo* nData = dynamic_cast(descr_cmd[lastAP]); + dest->MoveTo(nData->p); + } + break; + + case descr_close: + dest->Close(); + break; + + case descr_cubicto: + dest->CubicTo(lastCubic.p, lastCubic.start, lastCubic.end); + break; + + case descr_lineto: + if ( lastAP >= 0 ) { + PathDescrLineTo *nData = dynamic_cast(descr_cmd[lastAP]); + dest->LineTo(nData->p); + } + break; + + case descr_arcto: + if ( lastAP >= 0 ) { + PathDescrArcTo *nData = dynamic_cast(descr_cmd[lastAP]); + dest->ArcTo(nData->p, nData->rx, nData->ry, nData->angle, nData->large, nData->clockwise); + } + break; + + case descr_bezierto: + if ( lastAP >= 0 ) { + PathDescrBezierTo *nData = dynamic_cast(descr_cmd[lastAP]); + dest->BezierTo(nData->p); + } + break; + + case descr_interm_bezier: + if ( lastAP >= 0 ) { + PathDescrIntermBezierTo *nData = dynamic_cast(descr_cmd[lastAP]); + dest->IntermBezierTo(nData->p); + } + break; + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/PathStroke.cpp b/src/livarot/PathStroke.cpp new file mode 100644 index 0000000..e70ce36 --- /dev/null +++ b/src/livarot/PathStroke.cpp @@ -0,0 +1,763 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "Path.h" +#include "Shape.h" +#include <2geom/transforms.h> + +/* + * stroking polylines into a Shape instance + * grunt work. + * if the goal is to raster the stroke, polyline stroke->polygon->uncrossed polygon->raster is grossly + * inefficient (but reuse the intersector, so that's what a lazy programmer like me does). the correct way would be + * to set up a supersampled buffer, raster each polyline stroke's part (one part per segment in the polyline, plus + * each join) because these are all convex polygons, then transform in alpha values + */ + +// until i find something better +static Geom::Point StrokeNormalize(const Geom::Point value) { + double length = L2(value); + if ( length < 0.0000001 ) { + return Geom::Point(0, 0); + } else { + return value/length; + } +} + +// faster version if length is known +static Geom::Point StrokeNormalize(const Geom::Point value, double length) { + if ( length < 0.0000001 ) { + return Geom::Point(0, 0); + } else { + return value/length; + } +} + +void Path::Stroke(Shape *dest, bool doClose, double width, JoinType join, + ButtType butt, double miter, bool justAdd) +{ + if (dest == nullptr) { + return; + } + + if (justAdd == false) { + dest->Reset(3 * pts.size(), 3 * pts.size()); + } + + dest->MakeBackData(false); + + int lastM = 0; + while (lastM < int(pts.size())) { + + int lastP = lastM + 1; + while (lastP < int(pts.size()) // select one subpath + && (pts[lastP].isMoveTo == polyline_lineto + || pts[lastP].isMoveTo == polyline_forced)) + { + lastP++; + } + + if ( lastP > lastM+1 ) { + Geom::Point sbStart = pts[lastM].p; + Geom::Point sbEnd = pts[lastP - 1].p; + // if ( pts[lastP - 1].closed ) { // this is correct, but this bugs text rendering (doesn't close text stroke)... + if ( Geom::LInfty(sbEnd-sbStart) < 0.00001 ) { // why close lines that shouldn't be closed? + // ah I see, because close is defined here for + // a whole path and should be defined per subpath. + // debut==fin => ferme (on devrait garder un element pour les close(), mais tant pis) + DoStroke(lastM, lastP - lastM, dest, true, width, join, butt, miter, true); + } else { + DoStroke(lastM, lastP - lastM, dest, doClose, width, join, butt, miter, true); + } + } else if (butt == butt_round) { // special case: zero length round butt is a circle + int last[2] = { -1, -1 }; + Geom::Point dir; + dir[0] = 1; + dir[1] = 0; + Geom::Point pos = pts[lastM].p; + DoButt(dest, width, butt, pos, dir, last[RIGHT], last[LEFT]); + int end[2]; + dir = -dir; + DoButt(dest, width, butt, pos, dir, end[LEFT], end[RIGHT]); + dest->AddEdge (end[LEFT], last[LEFT]); + dest->AddEdge (last[RIGHT], end[RIGHT]); + } + lastM = lastP; + } +} + +void Path::DoStroke(int off, int N, Shape *dest, bool doClose, double width, JoinType join, + ButtType butt, double miter, bool /*justAdd*/) +{ + if (N <= 1) { + return; + } + + Geom::Point prevP, nextP; + int prevI, nextI; + int upTo; + + int curI = 0; + Geom::Point curP = pts[off].p; + + if (doClose) { + + prevI = N - 1; + while (prevI > 0) { + prevP = pts[off + prevI].p; + Geom::Point diff = curP - prevP; + double dist = dot(diff, diff); + if (dist > 0.001) { + break; + } + prevI--; + } + if (prevI <= 0) { + return; + } + upTo = prevI; + + } else { + + prevP = curP; + prevI = curI; + upTo = N - 1; + } + + { + nextI = 1; + while (nextI <= upTo) { + nextP = pts[off + nextI].p; + Geom::Point diff = curP - nextP; + double dist = dot(diff, diff); + if (dist > 0.0) { // more tolerance for the first distance, to give the cap the right direction + break; + } + nextI++; + } + if (nextI > upTo) { + if (butt == butt_round) { // special case: zero length round butt is a circle + int last[2] = { -1, -1 }; + Geom::Point dir; + dir[0] = 1; + dir[1] = 0; + DoButt(dest, width, butt, curP, dir, last[RIGHT], last[LEFT]); + int end[2]; + dir = -dir; + DoButt(dest, width, butt, curP, dir, end[LEFT], end[RIGHT]); + dest->AddEdge (end[LEFT], last[LEFT]); + dest->AddEdge (last[RIGHT], end[RIGHT]); + } + return; + } + } + + int start[2] = { -1, -1 }; + int last[2] = { -1, -1 }; + Geom::Point prevD = curP - prevP; + Geom::Point nextD = nextP - curP; + double prevLe = Geom::L2(prevD); + double nextLe = Geom::L2(nextD); + prevD = StrokeNormalize(prevD, prevLe); + nextD = StrokeNormalize(nextD, nextLe); + + if (doClose) { + DoJoin(dest, width, join, curP, prevD, nextD, miter, prevLe, nextLe, start, last); + } else { + nextD = -nextD; + DoButt(dest, width, butt, curP, nextD, last[RIGHT], last[LEFT]); + nextD = -nextD; + } + + do { + prevP = curP; + prevI = curI; + curP = nextP; + curI = nextI; + prevD = nextD; + prevLe = nextLe; + nextI++; + while (nextI <= upTo) { + nextP = pts[off + nextI].p; + Geom::Point diff = curP - nextP; + double dist = dot(diff, diff); + if (dist > 0.001 || (nextI == upTo && dist > 0.0)) { // more tolerance for the last distance too, for the right cap direction + break; + } + nextI++; + } + if (nextI > upTo) { + break; + } + + nextD = nextP - curP; + nextLe = Geom::L2(nextD); + nextD = StrokeNormalize(nextD, nextLe); + int nSt[2] = { -1, -1 }; + int nEn[2] = { -1, -1 }; + DoJoin(dest, width, join, curP, prevD, nextD, miter, prevLe, nextLe, nSt, nEn); + dest->AddEdge(nSt[LEFT], last[LEFT]); + last[LEFT] = nEn[LEFT]; + dest->AddEdge(last[RIGHT], nSt[RIGHT]); + last[RIGHT] = nEn[RIGHT]; + } while (nextI <= upTo); + + if (doClose) { + /* prevP=curP; + prevI=curI; + curP=nextP; + curI=nextI; + prevD=nextD;*/ + nextP = pts[off].p; + + nextD = nextP - curP; + nextLe = Geom::L2(nextD); + nextD = StrokeNormalize(nextD, nextLe); + int nSt[2] = { -1, -1 }; + int nEn[2] = { -1, -1 }; + DoJoin(dest, width, join, curP, prevD, nextD, miter, prevLe, nextLe, nSt, nEn); + dest->AddEdge (nSt[LEFT], last[LEFT]); + last[LEFT] = nEn[LEFT]; + dest->AddEdge (last[RIGHT], nSt[RIGHT]); + last[RIGHT] = nEn[RIGHT]; + + dest->AddEdge (start[LEFT], last[LEFT]); + dest->AddEdge (last[RIGHT], start[RIGHT]); + + } else { + + int end[2]; + DoButt (dest, width, butt, curP, prevD, end[LEFT], end[RIGHT]); + dest->AddEdge (end[LEFT], last[LEFT]); + dest->AddEdge (last[RIGHT], end[RIGHT]); + } +} + + +void Path::DoButt(Shape *dest, double width, ButtType butt, Geom::Point pos, Geom::Point dir, + int &leftNo, int &rightNo) +{ + Geom::Point nor; + nor = dir.ccw(); + + if (butt == butt_square) + { + Geom::Point x; + x = pos + width * dir + width * nor; + int bleftNo = dest->AddPoint (x); + x = pos + width * dir - width * nor; + int brightNo = dest->AddPoint (x); + x = pos + width * nor; + leftNo = dest->AddPoint (x); + x = pos - width * nor; + rightNo = dest->AddPoint (x); + dest->AddEdge (rightNo, brightNo); + dest->AddEdge (brightNo, bleftNo); + dest->AddEdge (bleftNo, leftNo); + } + else if (butt == butt_pointy) + { + leftNo = dest->AddPoint (pos + width * nor); + rightNo = dest->AddPoint (pos - width * nor); + int mid = dest->AddPoint (pos + width * dir); + dest->AddEdge (rightNo, mid); + dest->AddEdge (mid, leftNo); + } + else if (butt == butt_round) + { + const Geom::Point sx = pos + width * nor; + const Geom::Point ex = pos - width * nor; + leftNo = dest->AddPoint (sx); + rightNo = dest->AddPoint (ex); + + RecRound (dest, rightNo, leftNo, ex, sx, -nor, nor, pos, width); + } + else + { + leftNo = dest->AddPoint (pos + width * nor); + rightNo = dest->AddPoint (pos - width * nor); + dest->AddEdge (rightNo, leftNo); + } +} + + +void Path::DoJoin (Shape *dest, double width, JoinType join, Geom::Point pos, Geom::Point prev, + Geom::Point next, double miter, double /*prevL*/, double /*nextL*/, + int *stNo, int *enNo) +{ + Geom::Point pnor = prev.ccw(); + Geom::Point nnor = next.ccw(); + double angSi = cross(prev, next); + + /* FIXED: this special case caused bug 1028953 */ + if (angSi > -0.0001 && angSi < 0.0001) { + double angCo = dot (prev, next); + if (angCo > 0.9999) { + // tout droit + stNo[LEFT] = enNo[LEFT] = dest->AddPoint(pos + width * pnor); + stNo[RIGHT] = enNo[RIGHT] = dest->AddPoint(pos - width * pnor); + } else { + // demi-tour + const Geom::Point sx = pos + width * pnor; + const Geom::Point ex = pos - width * pnor; + stNo[LEFT] = enNo[RIGHT] = dest->AddPoint (sx); + stNo[RIGHT] = enNo[LEFT] = dest->AddPoint (ex); + if (join == join_round) { + RecRound (dest, enNo[LEFT], stNo[LEFT], ex, sx, -pnor, pnor, pos, width); + dest->AddEdge(stNo[RIGHT], enNo[RIGHT]); + } else { + dest->AddEdge(enNo[LEFT], stNo[LEFT]); + dest->AddEdge(stNo[RIGHT], enNo[RIGHT]); // two times because both are crossing each other + } + } + return; + } + + if (angSi < 0) { + int midNo = dest->AddPoint(pos); + stNo[LEFT] = dest->AddPoint(pos + width * pnor); + enNo[LEFT] = dest->AddPoint(pos + width * nnor); + dest->AddEdge(enNo[LEFT], midNo); + dest->AddEdge(midNo, stNo[LEFT]); + + if (join == join_pointy) { + + stNo[RIGHT] = dest->AddPoint(pos - width * pnor); + enNo[RIGHT] = dest->AddPoint(pos - width * nnor); + + const Geom::Point biss = StrokeNormalize(prev - next); + double c2 = dot(biss, nnor); + double l = width / c2; + double emiter = width * c2; + if (emiter < miter) { + emiter = miter; + } + + if (fabs(l) < miter) { + int const n = dest->AddPoint(pos - l * biss); + dest->AddEdge(stNo[RIGHT], n); + dest->AddEdge(n, enNo[RIGHT]); + } else { + dest->AddEdge(stNo[RIGHT], enNo[RIGHT]); + } + + } else if (join == join_round) { + Geom::Point sx = pos - width * pnor; + stNo[RIGHT] = dest->AddPoint(sx); + Geom::Point ex = pos - width * nnor; + enNo[RIGHT] = dest->AddPoint(ex); + + RecRound(dest, stNo[RIGHT], enNo[RIGHT], + sx, ex, -pnor, -nnor, pos, width); + + } else { + stNo[RIGHT] = dest->AddPoint(pos - width * pnor); + enNo[RIGHT] = dest->AddPoint(pos - width * nnor); + dest->AddEdge(stNo[RIGHT], enNo[RIGHT]); + } + + } else { + + int midNo = dest->AddPoint(pos); + stNo[RIGHT] = dest->AddPoint(pos - width * pnor); + enNo[RIGHT] = dest->AddPoint(pos - width * nnor); + dest->AddEdge(stNo[RIGHT], midNo); + dest->AddEdge(midNo, enNo[RIGHT]); + + if (join == join_pointy) { + + stNo[LEFT] = dest->AddPoint(pos + width * pnor); + enNo[LEFT] = dest->AddPoint(pos + width * nnor); + + const Geom::Point biss = StrokeNormalize(next - prev); + double c2 = dot(biss, nnor); + double l = width / c2; + double emiter = width * c2; + if (emiter < miter) { + emiter = miter; + } + if ( fabs(l) < miter) { + int const n = dest->AddPoint (pos + l * biss); + dest->AddEdge (enNo[LEFT], n); + dest->AddEdge (n, stNo[LEFT]); + } + else + { + dest->AddEdge (enNo[LEFT], stNo[LEFT]); + } + + } else if (join == join_round) { + + Geom::Point sx = pos + width * pnor; + stNo[LEFT] = dest->AddPoint(sx); + Geom::Point ex = pos + width * nnor; + enNo[LEFT] = dest->AddPoint(ex); + + RecRound(dest, enNo[LEFT], stNo[LEFT], + ex, sx, nnor, pnor, pos, width); + + } else { + stNo[LEFT] = dest->AddPoint(pos + width * pnor); + enNo[LEFT] = dest->AddPoint(pos + width * nnor); + dest->AddEdge(enNo[LEFT], stNo[LEFT]); + } + } +} + + void +Path::DoLeftJoin (Shape * dest, double width, JoinType join, Geom::Point pos, + Geom::Point prev, Geom::Point next, double miter, double /*prevL*/, double /*nextL*/, + int &leftStNo, int &leftEnNo,int pathID,int pieceID,double tID) +{ + Geom::Point pnor=prev.ccw(); + Geom::Point nnor=next.ccw(); + double angSi = cross(prev, next); + if (angSi > -0.0001 && angSi < 0.0001) + { + double angCo = dot (prev, next); + if (angCo > 0.9999) + { + // tout droit + leftEnNo = leftStNo = dest->AddPoint (pos + width * pnor); + } + else + { + // demi-tour + leftStNo = dest->AddPoint (pos + width * pnor); + leftEnNo = dest->AddPoint (pos - width * pnor); + int nEdge=dest->AddEdge (leftEnNo, leftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + return; + } + if (angSi < 0) + { + /* Geom::Point biss; + biss.x=next.x-prev.x; + biss.y=next.y-prev.y; + double c2=cross(next, biss); + double l=width/c2; + double projn=l*(dot(biss,next)); + double projp=-l*(dot(biss,prev)); + if ( projp <= 0.5*prevL && projn <= 0.5*nextL ) { + double x,y; + x=pos.x+l*biss.x; + y=pos.y+l*biss.y; + leftEnNo=leftStNo=dest->AddPoint(x,y); + } else {*/ + leftStNo = dest->AddPoint (pos + width * pnor); + leftEnNo = dest->AddPoint (pos + width * nnor); +// int midNo = dest->AddPoint (pos); +// int nEdge=dest->AddEdge (leftEnNo, midNo); + int nEdge=dest->AddEdge (leftEnNo, leftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } +// nEdge=dest->AddEdge (midNo, leftStNo); +// if ( dest->hasBackData() ) { +// dest->ebData[nEdge].pathID=pathID; +// dest->ebData[nEdge].pieceID=pieceID; +// dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; +// } + // } + } + else + { + if (join == join_pointy) + { + leftStNo = dest->AddPoint (pos + width * pnor); + leftEnNo = dest->AddPoint (pos + width * nnor); + + const Geom::Point biss = StrokeNormalize (pnor + nnor); + double c2 = dot (biss, nnor); + double l = width / c2; + double emiter = width * c2; + if (emiter < miter) + emiter = miter; + if (l <= emiter) + { + int nleftStNo = dest->AddPoint (pos + l * biss); + int nEdge=dest->AddEdge (leftEnNo, nleftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (nleftStNo, leftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + else + { + double s2 = cross(nnor, biss); + double dec = (l - emiter) * c2 / s2; + const Geom::Point tbiss=biss.ccw(); + + int nleftStNo = dest->AddPoint (pos + emiter * biss + dec * tbiss); + int nleftEnNo = dest->AddPoint (pos + emiter * biss - dec * tbiss); + int nEdge=dest->AddEdge (nleftEnNo, nleftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (leftEnNo, nleftEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (nleftStNo, leftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + } + else if (join == join_round) + { + const Geom::Point sx = pos + width * pnor; + leftStNo = dest->AddPoint (sx); + const Geom::Point ex = pos + width * nnor; + leftEnNo = dest->AddPoint (ex); + + RecRound(dest, leftEnNo, leftStNo, + sx, ex, pnor, nnor ,pos, width); + + } + else + { + leftStNo = dest->AddPoint (pos + width * pnor); + leftEnNo = dest->AddPoint (pos + width * nnor); + int nEdge=dest->AddEdge (leftEnNo, leftStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + } +} + void +Path::DoRightJoin (Shape * dest, double width, JoinType join, Geom::Point pos, + Geom::Point prev, Geom::Point next, double miter, double /*prevL*/, + double /*nextL*/, int &rightStNo, int &rightEnNo,int pathID,int pieceID,double tID) +{ + const Geom::Point pnor=prev.ccw(); + const Geom::Point nnor=next.ccw(); + double angSi = cross(prev, next); + if (angSi > -0.0001 && angSi < 0.0001) + { + double angCo = dot (prev, next); + if (angCo > 0.9999) + { + // tout droit + rightEnNo = rightStNo = dest->AddPoint (pos - width*pnor); + } + else + { + // demi-tour + rightEnNo = dest->AddPoint (pos + width*pnor); + rightStNo = dest->AddPoint (pos - width*pnor); + int nEdge=dest->AddEdge (rightStNo, rightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + return; + } + if (angSi < 0) + { + if (join == join_pointy) + { + rightStNo = dest->AddPoint (pos - width*pnor); + rightEnNo = dest->AddPoint (pos - width*nnor); + + const Geom::Point biss = StrokeNormalize (pnor + nnor); + double c2 = dot (biss, nnor); + double l = width / c2; + double emiter = width * c2; + if (emiter < miter) + emiter = miter; + if (l <= emiter) + { + int nrightStNo = dest->AddPoint (pos - l * biss); + int nEdge=dest->AddEdge (rightStNo, nrightStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (nrightStNo, rightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + else + { + double s2 = cross(nnor, biss); + double dec = (l - emiter) * c2 / s2; + const Geom::Point tbiss=biss.ccw(); + + int nrightStNo = dest->AddPoint (pos - emiter*biss - dec*tbiss); + int nrightEnNo = dest->AddPoint (pos - emiter*biss + dec*tbiss); + int nEdge=dest->AddEdge (rightStNo, nrightStNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (nrightStNo, nrightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + nEdge=dest->AddEdge (nrightEnNo, rightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + } + else if (join == join_round) + { + const Geom::Point sx = pos - width * pnor; + rightStNo = dest->AddPoint (sx); + const Geom::Point ex = pos - width * nnor; + rightEnNo = dest->AddPoint (ex); + + RecRound(dest, rightStNo, rightEnNo, + sx, ex, -pnor, -nnor ,pos, width); + } + else + { + rightStNo = dest->AddPoint (pos - width * pnor); + rightEnNo = dest->AddPoint (pos - width * nnor); + int nEdge=dest->AddEdge (rightStNo, rightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } + } + } + else + { + /* Geom::Point biss; + biss=next.x-prev.x; + biss.y=next.y-prev.y; + double c2=cross(biss, next); + double l=width/c2; + double projn=l*(dot(biss,next)); + double projp=-l*(dot(biss,prev)); + if ( projp <= 0.5*prevL && projn <= 0.5*nextL ) { + double x,y; + x=pos.x+l*biss.x; + y=pos.y+l*biss.y; + rightEnNo=rightStNo=dest->AddPoint(x,y); + } else {*/ + rightStNo = dest->AddPoint (pos - width*pnor); + rightEnNo = dest->AddPoint (pos - width*nnor); +// int midNo = dest->AddPoint (pos); +// int nEdge=dest->AddEdge (rightStNo, midNo); + int nEdge=dest->AddEdge (rightStNo, rightEnNo); + if ( dest->hasBackData() ) { + dest->ebData[nEdge].pathID=pathID; + dest->ebData[nEdge].pieceID=pieceID; + dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; + } +// nEdge=dest->AddEdge (midNo, rightEnNo); +// if ( dest->hasBackData() ) { +// dest->ebData[nEdge].pathID=pathID; +// dest->ebData[nEdge].pieceID=pieceID; +// dest->ebData[nEdge].tSt=dest->ebData[nEdge].tEn=tID; +// } + // } + } +} + + +// a very ugly way to produce round joins: doing one (or two, depend on the angle of the join) quadratic bezier curves +// but since most joins are going to be small, nobody will notice -- but somebody noticed and now the ugly stuff is gone! so: + +// a very nice way to produce round joins, caps or dots +void Path::RecRound(Shape *dest, int sNo, int eNo, // start and end index + Geom::Point const &iS, Geom::Point const &iE, // start and end point + Geom::Point const &nS, Geom::Point const &nE, // start and end normal vector + Geom::Point &origine, float width) // center and radius of round +{ + //Geom::Point diff = iS - iE; + //double dist = dot(diff, diff); + if (width < 0.5 || dot(iS - iE, iS - iE)/width < 2.0) { + dest->AddEdge(sNo, eNo); + return; + } + double ang, sia, lod; + if (nS == -nE) { + ang = M_PI; + sia = 1; + } else { + double coa = dot(nS, nE); + sia = cross(nE, nS); + ang = acos(coa); + if ( coa >= 1 ) { + ang = 0; + } + if ( coa <= -1 ) { + ang = M_PI; + } + } + lod = 0.02 + 10 / (10 + width); // limit detail to about 2 degrees (180 * 0.02/Pi degrees) + ang /= lod; + + int nbS = (int) floor(ang); + Geom::Rotate omega(((sia > 0) ? -lod : lod)); + Geom::Point cur = iS - origine; + // StrokeNormalize(cur); + // cur*=width; + int lastNo = sNo; + for (int i = 0; i < nbS; i++) { + cur = cur * omega; + Geom::Point m = origine + cur; + int mNo = dest->AddPoint(m); + dest->AddEdge(lastNo, mNo); + lastNo = mNo; + } + dest->AddEdge(lastNo, eNo); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/README b/src/livarot/README new file mode 100644 index 0000000..3e6f138 --- /dev/null +++ b/src/livarot/README @@ -0,0 +1,17 @@ + + +This directory contains path and shape related code. This code is +partially replaced by lib2geom. We still rely on this code for: + +* Binary path operations. +* Path offsetting. +* Finding start and end positions for lines text inside a shape. + +To do: + +* Move needed functionality to lib2geom or independent functions. +* Delete directory. + +(Livarot is a pungent French cow milk cheese.) + + diff --git a/src/livarot/Shape.cpp b/src/livarot/Shape.cpp new file mode 100644 index 0000000..4b287ca --- /dev/null +++ b/src/livarot/Shape.cpp @@ -0,0 +1,2317 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include "Shape.h" +#include "livarot/sweep-event-queue.h" +#include "livarot/sweep-tree-list.h" + +/* + * Shape instances handling. + * never (i repeat: never) modify edges and points links; use Connect() and Disconnect() instead + * the graph is stored as a set of points and edges, with edges in a doubly-linked list for each point. + */ + +Shape::Shape() + : nbQRas(0), + firstQRas(-1), + lastQRas(-1), + qrsData(nullptr), + nbInc(0), + maxInc(0), + iData(nullptr), + sTree(nullptr), + sEvts(nullptr), + _need_points_sorting(false), + _need_edges_sorting(false), + _has_points_data(false), + _point_data_initialised(false), + _has_edges_data(false), + _has_sweep_src_data(false), + _has_sweep_dest_data(false), + _has_raster_data(false), + _has_quick_raster_data(false), + _has_back_data(false), + _has_voronoi_data(false), + _bbox_up_to_date(false) +{ + leftX = topY = rightX = bottomY = 0; + maxPt = 0; + maxAr = 0; + + type = shape_polygon; +} +Shape::~Shape () +{ + maxPt = 0; + maxAr = 0; + free(qrsData); +} + +void Shape::Affiche() +{ + printf("sh=%p nbPt=%i nbAr=%i\n", this, static_cast(_pts.size()), static_cast(_aretes.size())); // localizing ok + for (unsigned int i=0; i<_pts.size(); i++) { + printf("pt %u : x=(%f %f) dI=%i dO=%i\n",i, _pts[i].x[0], _pts[i].x[1], _pts[i].dI, _pts[i].dO); // localizing ok + } + for (unsigned int i=0; i<_aretes.size(); i++) { + printf("ar %u : dx=(%f %f) st=%i en=%i\n",i, _aretes[i].dx[0], _aretes[i].dx[1], _aretes[i].st, _aretes[i].en); // localizing ok + } +} + +/** + * Allocates space for point cache or clears the cache + \param nVal Allocate a cache (true) or clear it (false) + */ +void +Shape::MakePointData (bool nVal) +{ + if (nVal) + { + if (_has_points_data == false) + { + _has_points_data = true; + _point_data_initialised = false; + _bbox_up_to_date = false; + pData.resize(maxPt); + } + } + /* no need to clean point data - keep it cached*/ +} + +void +Shape::MakeEdgeData (bool nVal) +{ + if (nVal) + { + if (_has_edges_data == false) + { + _has_edges_data = true; + eData.resize(maxAr); + } + } + else + { + if (_has_edges_data) + { + _has_edges_data = false; + eData.clear(); + } + } +} + +void +Shape::MakeRasterData (bool nVal) +{ + if (nVal) + { + if (_has_raster_data == false) + { + _has_raster_data = true; + swrData.resize(maxAr); + } + } + else + { + if (_has_raster_data) + { + _has_raster_data = false; + swrData.clear(); + } + } +} +void +Shape::MakeQuickRasterData (bool nVal) +{ + if (nVal) + { + if (_has_quick_raster_data == false) + { + _has_quick_raster_data = true; + quick_raster_data* new_qrsData = static_cast(realloc(qrsData, maxAr * sizeof(quick_raster_data))); + if (!new_qrsData) { + g_error("Not enough memory available for reallocating Shape::qrsData"); + } else { + qrsData = new_qrsData; + } + } + } + else + { + if (_has_quick_raster_data) + { + _has_quick_raster_data = false; + } + } +} +void +Shape::MakeSweepSrcData (bool nVal) +{ + if (nVal) + { + if (_has_sweep_src_data == false) + { + _has_sweep_src_data = true; + swsData.resize(maxAr); + } + } + else + { + if (_has_sweep_src_data) + { + _has_sweep_src_data = false; + swsData.clear(); + } + } +} +void +Shape::MakeSweepDestData (bool nVal) +{ + if (nVal) + { + if (_has_sweep_dest_data == false) + { + _has_sweep_dest_data = true; + swdData.resize(maxAr); + } + } + else + { + if (_has_sweep_dest_data) + { + _has_sweep_dest_data = false; + swdData.clear(); + } + } +} +void +Shape::MakeBackData (bool nVal) +{ + if (nVal) + { + if (_has_back_data == false) + { + _has_back_data = true; + ebData.resize(maxAr); + } + } + else + { + if (_has_back_data) + { + _has_back_data = false; + ebData.clear(); + } + } +} +void +Shape::MakeVoronoiData (bool nVal) +{ + if (nVal) + { + if (_has_voronoi_data == false) + { + _has_voronoi_data = true; + vorpData.resize(maxPt); + voreData.resize(maxAr); + } + } + else + { + if (_has_voronoi_data) + { + _has_voronoi_data = false; + vorpData.clear(); + voreData.clear(); + } + } +} + + +/** + * Copy point and edge data from `who' into this object, discarding + * any cached data that we have. + */ +void +Shape::Copy (Shape * who) +{ + if (who == nullptr) + { + Reset (0, 0); + return; + } + MakePointData (false); + MakeEdgeData (false); + MakeSweepSrcData (false); + MakeSweepDestData (false); + MakeRasterData (false); + MakeQuickRasterData (false); + MakeBackData (false); + + delete sTree; + sTree = nullptr; + delete sEvts; + sEvts = nullptr; + + Reset (who->numberOfPoints(), who->numberOfEdges()); + type = who->type; + _need_points_sorting = who->_need_points_sorting; + _need_edges_sorting = who->_need_edges_sorting; + _has_points_data = false; + _point_data_initialised = false; + _has_edges_data = false; + _has_sweep_src_data = false; + _has_sweep_dest_data = false; + _has_raster_data = false; + _has_quick_raster_data = false; + _has_back_data = false; + _has_voronoi_data = false; + _bbox_up_to_date = false; + + _pts = who->_pts; + _aretes = who->_aretes; +} + +/** + * Clear points and edges and prepare internal data using new size. + */ +void +Shape::Reset (int pointCount, int edgeCount) +{ + _pts.clear(); + _aretes.clear(); + + type = shape_polygon; + if (pointCount > maxPt) + { + maxPt = pointCount; + if (_has_points_data) + pData.resize(maxPt); + if (_has_voronoi_data) + vorpData.resize(maxPt); + } + if (edgeCount > maxAr) + { + maxAr = edgeCount; + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_back_data) + ebData.resize(maxAr); + if (_has_voronoi_data) + voreData.resize(maxAr); + } + _need_points_sorting = false; + _need_edges_sorting = false; + _point_data_initialised = false; + _bbox_up_to_date = false; +} + +int +Shape::AddPoint (const Geom::Point x) +{ + if (numberOfPoints() >= maxPt) + { + maxPt = 2 * numberOfPoints() + 1; + if (_has_points_data) + pData.resize(maxPt); + if (_has_voronoi_data) + vorpData.resize(maxPt); + } + + dg_point p; + p.x = x; + p.dI = p.dO = 0; + p.incidentEdge[FIRST] = p.incidentEdge[LAST] = -1; + p.oldDegree = -1; + _pts.push_back(p); + int const n = _pts.size() - 1; + + if (_has_points_data) + { + pData[n].pending = 0; + pData[n].edgeOnLeft = -1; + pData[n].nextLinkedPoint = -1; + pData[n].askForWindingS = nullptr; + pData[n].askForWindingB = -1; + pData[n].rx[0] = Round(p.x[0]); + pData[n].rx[1] = Round(p.x[1]); + } + if (_has_voronoi_data) + { + vorpData[n].value = 0.0; + vorpData[n].winding = -2; + } + _need_points_sorting = true; + + return n; +} + +void +Shape::SubPoint (int p) +{ + if (p < 0 || p >= numberOfPoints()) + return; + _need_points_sorting = true; + int cb; + cb = getPoint(p).incidentEdge[FIRST]; + while (cb >= 0 && cb < numberOfEdges()) + { + if (getEdge(cb).st == p) + { + int ncb = getEdge(cb).nextS; + _aretes[cb].nextS = _aretes[cb].prevS = -1; + _aretes[cb].st = -1; + cb = ncb; + } + else if (getEdge(cb).en == p) + { + int ncb = getEdge(cb).nextE; + _aretes[cb].nextE = _aretes[cb].prevE = -1; + _aretes[cb].en = -1; + cb = ncb; + } + else + { + break; + } + } + _pts[p].incidentEdge[FIRST] = _pts[p].incidentEdge[LAST] = -1; + if (p < numberOfPoints() - 1) + SwapPoints (p, numberOfPoints() - 1); + _pts.pop_back(); +} + +void +Shape::SwapPoints (int a, int b) +{ + if (a == b) + return; + if (getPoint(a).totalDegree() == 2 && getPoint(b).totalDegree() == 2) + { + int cb = getPoint(a).incidentEdge[FIRST]; + if (getEdge(cb).st == a) + { + _aretes[cb].st = numberOfPoints(); + } + else if (getEdge(cb).en == a) + { + _aretes[cb].en = numberOfPoints(); + } + cb = getPoint(a).incidentEdge[LAST]; + if (getEdge(cb).st == a) + { + _aretes[cb].st = numberOfPoints(); + } + else if (getEdge(cb).en == a) + { + _aretes[cb].en = numberOfPoints(); + } + + cb = getPoint(b).incidentEdge[FIRST]; + if (getEdge(cb).st == b) + { + _aretes[cb].st = a; + } + else if (getEdge(cb).en == b) + { + _aretes[cb].en = a; + } + cb = getPoint(b).incidentEdge[LAST]; + if (getEdge(cb).st == b) + { + _aretes[cb].st = a; + } + else if (getEdge(cb).en == b) + { + _aretes[cb].en = a; + } + + cb = getPoint(a).incidentEdge[FIRST]; + if (getEdge(cb).st == numberOfPoints()) + { + _aretes[cb].st = b; + } + else if (getEdge(cb).en == numberOfPoints()) + { + _aretes[cb].en = b; + } + cb = getPoint(a).incidentEdge[LAST]; + if (getEdge(cb).st == numberOfPoints()) + { + _aretes[cb].st = b; + } + else if (getEdge(cb).en == numberOfPoints()) + { + _aretes[cb].en = b; + } + + } + else + { + int cb; + cb = getPoint(a).incidentEdge[FIRST]; + while (cb >= 0) + { + int ncb = NextAt (a, cb); + if (getEdge(cb).st == a) + { + _aretes[cb].st = numberOfPoints(); + } + else if (getEdge(cb).en == a) + { + _aretes[cb].en = numberOfPoints(); + } + cb = ncb; + } + cb = getPoint(b).incidentEdge[FIRST]; + while (cb >= 0) + { + int ncb = NextAt (b, cb); + if (getEdge(cb).st == b) + { + _aretes[cb].st = a; + } + else if (getEdge(cb).en == b) + { + _aretes[cb].en = a; + } + cb = ncb; + } + cb = getPoint(a).incidentEdge[FIRST]; + while (cb >= 0) + { + int ncb = NextAt (numberOfPoints(), cb); + if (getEdge(cb).st == numberOfPoints()) + { + _aretes[cb].st = b; + } + else if (getEdge(cb).en == numberOfPoints()) + { + _aretes[cb].en = b; + } + cb = ncb; + } + } + { + dg_point swap = getPoint(a); + _pts[a] = getPoint(b); + _pts[b] = swap; + } + if (_has_points_data) + { + point_data swad = pData[a]; + pData[a] = pData[b]; + pData[b] = swad; + // pData[pData[a].oldInd].newInd=a; + // pData[pData[b].oldInd].newInd=b; + } + if (_has_voronoi_data) + { + voronoi_point swav = vorpData[a]; + vorpData[a] = vorpData[b]; + vorpData[b] = swav; + } +} +void +Shape::SwapPoints (int a, int b, int c) +{ + if (a == b || b == c || a == c) + return; + SwapPoints (a, b); + SwapPoints (b, c); +} + +void +Shape::SortPoints () +{ + if (_need_points_sorting && hasPoints()) + SortPoints (0, numberOfPoints() - 1); + _need_points_sorting = false; +} + +void +Shape::SortPointsRounded () +{ + if (hasPoints()) + SortPointsRounded (0, numberOfPoints() - 1); +} + +void +Shape::SortPoints (int s, int e) +{ + if (s >= e) + return; + if (e == s + 1) + { + if (getPoint(s).x[1] > getPoint(e).x[1] + || (getPoint(s).x[1] == getPoint(e).x[1] && getPoint(s).x[0] > getPoint(e).x[0])) + SwapPoints (s, e); + return; + } + + int ppos = (s + e) / 2; + int plast = ppos; + double pvalx = getPoint(ppos).x[0]; + double pvaly = getPoint(ppos).x[1]; + + int le = s, ri = e; + while (le < ppos || ri > plast) + { + if (le < ppos) + { + do + { + int test = 0; + if (getPoint(le).x[1] > pvaly) + { + test = 1; + } + else if (getPoint(le).x[1] == pvaly) + { + if (getPoint(le).x[0] > pvalx) + { + test = 1; + } + else if (getPoint(le).x[0] == pvalx) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (le < ppos - 1) + { + SwapPoints (le, ppos - 1, ppos); + ppos--; + continue; // sans changer le + } + else if (le == ppos - 1) + { + ppos--; + break; + } + else + { + // oupsie + break; + } + } + if (test > 0) + { + break; + } + le++; + } + while (le < ppos); + } + if (ri > plast) + { + do + { + int test = 0; + if (getPoint(ri).x[1] > pvaly) + { + test = 1; + } + else if (getPoint(ri).x[1] == pvaly) + { + if (getPoint(ri).x[0] > pvalx) + { + test = 1; + } + else if (getPoint(ri).x[0] == pvalx) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (ri > plast + 1) + { + SwapPoints (ri, plast + 1, plast); + plast++; + continue; // sans changer ri + } + else if (ri == plast + 1) + { + plast++; + break; + } + else + { + // oupsie + break; + } + } + if (test < 0) + { + break; + } + ri--; + } + while (ri > plast); + } + if (le < ppos) + { + if (ri > plast) + { + SwapPoints (le, ri); + le++; + ri--; + } + else + { + if (le < ppos - 1) + { + SwapPoints (ppos - 1, plast, le); + ppos--; + plast--; + } + else if (le == ppos - 1) + { + SwapPoints (plast, le); + ppos--; + plast--; + } + } + } + else + { + if (ri > plast + 1) + { + SwapPoints (plast + 1, ppos, ri); + ppos++; + plast++; + } + else if (ri == plast + 1) + { + SwapPoints (ppos, ri); + ppos++; + plast++; + } + else + { + break; + } + } + } + SortPoints (s, ppos - 1); + SortPoints (plast + 1, e); +} + +void +Shape::SortPointsByOldInd (int s, int e) +{ + if (s >= e) + return; + if (e == s + 1) + { + if (getPoint(s).x[1] > getPoint(e).x[1] || (getPoint(s).x[1] == getPoint(e).x[1] && getPoint(s).x[0] > getPoint(e).x[0]) + || (getPoint(s).x[1] == getPoint(e).x[1] && getPoint(s).x[0] == getPoint(e).x[0] + && pData[s].oldInd > pData[e].oldInd)) + SwapPoints (s, e); + return; + } + + int ppos = (s + e) / 2; + int plast = ppos; + double pvalx = getPoint(ppos).x[0]; + double pvaly = getPoint(ppos).x[1]; + int pvali = pData[ppos].oldInd; + + int le = s, ri = e; + while (le < ppos || ri > plast) + { + if (le < ppos) + { + do + { + int test = 0; + if (getPoint(le).x[1] > pvaly) + { + test = 1; + } + else if (getPoint(le).x[1] == pvaly) + { + if (getPoint(le).x[0] > pvalx) + { + test = 1; + } + else if (getPoint(le).x[0] == pvalx) + { + if (pData[le].oldInd > pvali) + { + test = 1; + } + else if (pData[le].oldInd == pvali) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (le < ppos - 1) + { + SwapPoints (le, ppos - 1, ppos); + ppos--; + continue; // sans changer le + } + else if (le == ppos - 1) + { + ppos--; + break; + } + else + { + // oupsie + break; + } + } + if (test > 0) + { + break; + } + le++; + } + while (le < ppos); + } + if (ri > plast) + { + do + { + int test = 0; + if (getPoint(ri).x[1] > pvaly) + { + test = 1; + } + else if (getPoint(ri).x[1] == pvaly) + { + if (getPoint(ri).x[0] > pvalx) + { + test = 1; + } + else if (getPoint(ri).x[0] == pvalx) + { + if (pData[ri].oldInd > pvali) + { + test = 1; + } + else if (pData[ri].oldInd == pvali) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (ri > plast + 1) + { + SwapPoints (ri, plast + 1, plast); + plast++; + continue; // sans changer ri + } + else if (ri == plast + 1) + { + plast++; + break; + } + else + { + // oupsie + break; + } + } + if (test < 0) + { + break; + } + ri--; + } + while (ri > plast); + } + if (le < ppos) + { + if (ri > plast) + { + SwapPoints (le, ri); + le++; + ri--; + } + else + { + if (le < ppos - 1) + { + SwapPoints (ppos - 1, plast, le); + ppos--; + plast--; + } + else if (le == ppos - 1) + { + SwapPoints (plast, le); + ppos--; + plast--; + } + } + } + else + { + if (ri > plast + 1) + { + SwapPoints (plast + 1, ppos, ri); + ppos++; + plast++; + } + else if (ri == plast + 1) + { + SwapPoints (ppos, ri); + ppos++; + plast++; + } + else + { + break; + } + } + } + SortPointsByOldInd (s, ppos - 1); + SortPointsByOldInd (plast + 1, e); +} + +void +Shape::SortPointsRounded (int s, int e) +{ + if (s >= e) + return; + if (e == s + 1) + { + if (pData[s].rx[1] > pData[e].rx[1] + || (pData[s].rx[1] == pData[e].rx[1] && pData[s].rx[0] > pData[e].rx[0])) + SwapPoints (s, e); + return; + } + + int ppos = (s + e) / 2; + int plast = ppos; + double pvalx = pData[ppos].rx[0]; + double pvaly = pData[ppos].rx[1]; + + int le = s, ri = e; + while (le < ppos || ri > plast) + { + if (le < ppos) + { + do + { + int test = 0; + if (pData[le].rx[1] > pvaly) + { + test = 1; + } + else if (pData[le].rx[1] == pvaly) + { + if (pData[le].rx[0] > pvalx) + { + test = 1; + } + else if (pData[le].rx[0] == pvalx) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (le < ppos - 1) + { + SwapPoints (le, ppos - 1, ppos); + ppos--; + continue; // sans changer le + } + else if (le == ppos - 1) + { + ppos--; + break; + } + else + { + // oupsie + break; + } + } + if (test > 0) + { + break; + } + le++; + } + while (le < ppos); + } + if (ri > plast) + { + do + { + int test = 0; + if (pData[ri].rx[1] > pvaly) + { + test = 1; + } + else if (pData[ri].rx[1] == pvaly) + { + if (pData[ri].rx[0] > pvalx) + { + test = 1; + } + else if (pData[ri].rx[0] == pvalx) + { + test = 0; + } + else + { + test = -1; + } + } + else + { + test = -1; + } + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (ri > plast + 1) + { + SwapPoints (ri, plast + 1, plast); + plast++; + continue; // sans changer ri + } + else if (ri == plast + 1) + { + plast++; + break; + } + else + { + // oupsie + break; + } + } + if (test < 0) + { + break; + } + ri--; + } + while (ri > plast); + } + if (le < ppos) + { + if (ri > plast) + { + SwapPoints (le, ri); + le++; + ri--; + } + else + { + if (le < ppos - 1) + { + SwapPoints (ppos - 1, plast, le); + ppos--; + plast--; + } + else if (le == ppos - 1) + { + SwapPoints (plast, le); + ppos--; + plast--; + } + } + } + else + { + if (ri > plast + 1) + { + SwapPoints (plast + 1, ppos, ri); + ppos++; + plast++; + } + else if (ri == plast + 1) + { + SwapPoints (ppos, ri); + ppos++; + plast++; + } + else + { + break; + } + } + } + SortPointsRounded (s, ppos - 1); + SortPointsRounded (plast + 1, e); +} + +/* + * + */ +int +Shape::AddEdge (int st, int en) +{ + if (st == en) + return -1; + if (st < 0 || en < 0) + return -1; + type = shape_graph; + if (numberOfEdges() >= maxAr) + { + maxAr = 2 * numberOfEdges() + 1; + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_raster_data) + swrData.resize(maxAr); + if (_has_back_data) + ebData.resize(maxAr); + if (_has_voronoi_data) + voreData.resize(maxAr); + } + + dg_arete a; + a.dx = Geom::Point(0, 0); + a.st = a.en = -1; + a.prevS = a.nextS = -1; + a.prevE = a.nextE = -1; + if (st >= 0 && en >= 0) { + a.dx = getPoint(en).x - getPoint(st).x; + } + + _aretes.push_back(a); + int const n = numberOfEdges() - 1; + + ConnectStart (st, n); + ConnectEnd (en, n); + if (_has_edges_data) + { + eData[n].weight = 1; + eData[n].rdx = getEdge(n).dx; + } + if (_has_sweep_src_data) + { + swsData[n].misc = nullptr; + swsData[n].firstLinkedPoint = -1; + } + if (_has_back_data) + { + ebData[n].pathID = -1; + ebData[n].pieceID = -1; + ebData[n].tSt = ebData[n].tEn = 0; + } + if (_has_voronoi_data) + { + voreData[n].leF = -1; + voreData[n].riF = -1; + } + _need_edges_sorting = true; + return n; +} + +int +Shape::AddEdge (int st, int en, int leF, int riF) +{ + if (st == en) + return -1; + if (st < 0 || en < 0) + return -1; + { + int cb = getPoint(st).incidentEdge[FIRST]; + while (cb >= 0) + { + if (getEdge(cb).st == st && getEdge(cb).en == en) + return -1; // doublon + if (getEdge(cb).st == en && getEdge(cb).en == st) + return -1; // doublon + cb = NextAt (st, cb); + } + } + type = shape_graph; + if (numberOfEdges() >= maxAr) + { + maxAr = 2 * numberOfEdges() + 1; + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_raster_data) + swrData.resize(maxAr); + if (_has_back_data) + ebData.resize(maxAr); + if (_has_voronoi_data) + voreData.resize(maxAr); + } + + dg_arete a; + a.dx = Geom::Point(0, 0); + a.st = a.en = -1; + a.prevS = a.nextS = -1; + a.prevE = a.nextE = -1; + if (st >= 0 && en >= 0) { + a.dx = getPoint(en).x - getPoint(st).x; + } + + _aretes.push_back(a); + int const n = numberOfEdges() - 1; + + ConnectStart (st, n); + ConnectEnd (en, n); + if (_has_edges_data) + { + eData[n].weight = 1; + eData[n].rdx = getEdge(n).dx; + } + if (_has_sweep_src_data) + { + swsData[n].misc = nullptr; + swsData[n].firstLinkedPoint = -1; + } + if (_has_back_data) + { + ebData[n].pathID = -1; + ebData[n].pieceID = -1; + ebData[n].tSt = ebData[n].tEn = 0; + } + if (_has_voronoi_data) + { + voreData[n].leF = leF; + voreData[n].riF = riF; + } + _need_edges_sorting = true; + return n; +} + +void +Shape::SubEdge (int e) +{ + if (e < 0 || e >= numberOfEdges()) + return; + type = shape_graph; + DisconnectStart (e); + DisconnectEnd (e); + if (e < numberOfEdges() - 1) + SwapEdges (e, numberOfEdges() - 1); + _aretes.pop_back(); + _need_edges_sorting = true; +} + +void +Shape::SwapEdges (int a, int b) +{ + if (a == b) + return; + if (getEdge(a).prevS >= 0 && getEdge(a).prevS != b) + { + if (getEdge(getEdge(a).prevS).st == getEdge(a).st) + { + _aretes[getEdge(a).prevS].nextS = b; + } + else if (getEdge(getEdge(a).prevS).en == getEdge(a).st) + { + _aretes[getEdge(a).prevS].nextE = b; + } + } + if (getEdge(a).nextS >= 0 && getEdge(a).nextS != b) + { + if (getEdge(getEdge(a).nextS).st == getEdge(a).st) + { + _aretes[getEdge(a).nextS].prevS = b; + } + else if (getEdge(getEdge(a).nextS).en == getEdge(a).st) + { + _aretes[getEdge(a).nextS].prevE = b; + } + } + if (getEdge(a).prevE >= 0 && getEdge(a).prevE != b) + { + if (getEdge(getEdge(a).prevE).st == getEdge(a).en) + { + _aretes[getEdge(a).prevE].nextS = b; + } + else if (getEdge(getEdge(a).prevE).en == getEdge(a).en) + { + _aretes[getEdge(a).prevE].nextE = b; + } + } + if (getEdge(a).nextE >= 0 && getEdge(a).nextE != b) + { + if (getEdge(getEdge(a).nextE).st == getEdge(a).en) + { + _aretes[getEdge(a).nextE].prevS = b; + } + else if (getEdge(getEdge(a).nextE).en == getEdge(a).en) + { + _aretes[getEdge(a).nextE].prevE = b; + } + } + if (getEdge(a).st >= 0) + { + if (getPoint(getEdge(a).st).incidentEdge[FIRST] == a) + _pts[getEdge(a).st].incidentEdge[FIRST] = numberOfEdges(); + if (getPoint(getEdge(a).st).incidentEdge[LAST] == a) + _pts[getEdge(a).st].incidentEdge[LAST] = numberOfEdges(); + } + if (getEdge(a).en >= 0) + { + if (getPoint(getEdge(a).en).incidentEdge[FIRST] == a) + _pts[getEdge(a).en].incidentEdge[FIRST] = numberOfEdges(); + if (getPoint(getEdge(a).en).incidentEdge[LAST] == a) + _pts[getEdge(a).en].incidentEdge[LAST] = numberOfEdges(); + } + + + if (getEdge(b).prevS >= 0 && getEdge(b).prevS != a) + { + if (getEdge(getEdge(b).prevS).st == getEdge(b).st) + { + _aretes[getEdge(b).prevS].nextS = a; + } + else if (getEdge(getEdge(b).prevS).en == getEdge(b).st) + { + _aretes[getEdge(b).prevS].nextE = a; + } + } + if (getEdge(b).nextS >= 0 && getEdge(b).nextS != a) + { + if (getEdge(getEdge(b).nextS).st == getEdge(b).st) + { + _aretes[getEdge(b).nextS].prevS = a; + } + else if (getEdge(getEdge(b).nextS).en == getEdge(b).st) + { + _aretes[getEdge(b).nextS].prevE = a; + } + } + if (getEdge(b).prevE >= 0 && getEdge(b).prevE != a) + { + if (getEdge(getEdge(b).prevE).st == getEdge(b).en) + { + _aretes[getEdge(b).prevE].nextS = a; + } + else if (getEdge(getEdge(b).prevE).en == getEdge(b).en) + { + _aretes[getEdge(b).prevE].nextE = a; + } + } + if (getEdge(b).nextE >= 0 && getEdge(b).nextE != a) + { + if (getEdge(getEdge(b).nextE).st == getEdge(b).en) + { + _aretes[getEdge(b).nextE].prevS = a; + } + else if (getEdge(getEdge(b).nextE).en == getEdge(b).en) + { + _aretes[getEdge(b).nextE].prevE = a; + } + } + + + for (int i = 0; i < 2; i++) { + int p = getEdge(b).st; + if (p >= 0) { + if (getPoint(p).incidentEdge[i] == b) { + _pts[p].incidentEdge[i] = a; + } + } + + p = getEdge(b).en; + if (p >= 0) { + if (getPoint(p).incidentEdge[i] == b) { + _pts[p].incidentEdge[i] = a; + } + } + + p = getEdge(a).st; + if (p >= 0) { + if (getPoint(p).incidentEdge[i] == numberOfEdges()) { + _pts[p].incidentEdge[i] = b; + } + } + + p = getEdge(a).en; + if (p >= 0) { + if (getPoint(p).incidentEdge[i] == numberOfEdges()) { + _pts[p].incidentEdge[i] = b; + } + } + + } + + if (getEdge(a).prevS == b) + _aretes[a].prevS = a; + if (getEdge(a).prevE == b) + _aretes[a].prevE = a; + if (getEdge(a).nextS == b) + _aretes[a].nextS = a; + if (getEdge(a).nextE == b) + _aretes[a].nextE = a; + if (getEdge(b).prevS == a) + _aretes[a].prevS = b; + if (getEdge(b).prevE == a) + _aretes[a].prevE = b; + if (getEdge(b).nextS == a) + _aretes[a].nextS = b; + if (getEdge(b).nextE == a) + _aretes[a].nextE = b; + + dg_arete swap = getEdge(a); + _aretes[a] = getEdge(b); + _aretes[b] = swap; + if (_has_edges_data) + { + edge_data swae = eData[a]; + eData[a] = eData[b]; + eData[b] = swae; + } + if (_has_sweep_src_data) + { + sweep_src_data swae = swsData[a]; + swsData[a] = swsData[b]; + swsData[b] = swae; + } + if (_has_sweep_dest_data) + { + sweep_dest_data swae = swdData[a]; + swdData[a] = swdData[b]; + swdData[b] = swae; + } + if (_has_raster_data) + { + raster_data swae = swrData[a]; + swrData[a] = swrData[b]; + swrData[b] = swae; + } + if (_has_back_data) + { + back_data swae = ebData[a]; + ebData[a] = ebData[b]; + ebData[b] = swae; + } + if (_has_voronoi_data) + { + voronoi_edge swav = voreData[a]; + voreData[a] = voreData[b]; + voreData[b] = swav; + } +} +void +Shape::SwapEdges (int a, int b, int c) +{ + if (a == b || b == c || a == c) + return; + SwapEdges (a, b); + SwapEdges (b, c); +} + +void +Shape::SortEdges () +{ + if (_need_edges_sorting == false) { + return; + } + _need_edges_sorting = false; + + edge_list *list = (edge_list *) g_malloc(numberOfEdges() * sizeof (edge_list)); + for (int p = 0; p < numberOfPoints(); p++) + { + int const d = getPoint(p).totalDegree(); + if (d > 1) + { + int cb; + cb = getPoint(p).incidentEdge[FIRST]; + int nb = 0; + while (cb >= 0) + { + int n = nb++; + list[n].no = cb; + if (getEdge(cb).st == p) + { + list[n].x = getEdge(cb).dx; + list[n].starting = true; + } + else + { + list[n].x = -getEdge(cb).dx; + list[n].starting = false; + } + cb = NextAt (p, cb); + } + SortEdgesList (list, 0, nb - 1); + _pts[p].incidentEdge[FIRST] = list[0].no; + _pts[p].incidentEdge[LAST] = list[nb - 1].no; + for (int i = 0; i < nb; i++) + { + if (list[i].starting) + { + if (i > 0) + { + _aretes[list[i].no].prevS = list[i - 1].no; + } + else + { + _aretes[list[i].no].prevS = -1; + } + if (i < nb - 1) + { + _aretes[list[i].no].nextS = list[i + 1].no; + } + else + { + _aretes[list[i].no].nextS = -1; + } + } + else + { + if (i > 0) + { + _aretes[list[i].no].prevE = list[i - 1].no; + } + else + { + _aretes[list[i].no].prevE = -1; + } + if (i < nb - 1) + { + _aretes[list[i].no].nextE = list[i + 1].no; + } + else + { + _aretes[list[i].no].nextE = -1; + } + } + } + } + } + g_free(list); +} + +int +Shape::CmpToVert (Geom::Point ax, Geom::Point bx,bool as,bool bs) +{ + int tstAX = 0; + int tstAY = 0; + int tstBX = 0; + int tstBY = 0; + if (ax[0] > 0) + tstAX = 1; + if (ax[0] < 0) + tstAX = -1; + if (ax[1] > 0) + tstAY = 1; + if (ax[1] < 0) + tstAY = -1; + if (bx[0] > 0) + tstBX = 1; + if (bx[0] < 0) + tstBX = -1; + if (bx[1] > 0) + tstBY = 1; + if (bx[1] < 0) + tstBY = -1; + + int quadA = 0, quadB = 0; + if (tstAX < 0) + { + if (tstAY < 0) + { + quadA = 7; + } + else if (tstAY == 0) + { + quadA = 6; + } + else if (tstAY > 0) + { + quadA = 5; + } + } + else if (tstAX == 0) + { + if (tstAY < 0) + { + quadA = 0; + } + else if (tstAY == 0) + { + quadA = -1; + } + else if (tstAY > 0) + { + quadA = 4; + } + } + else if (tstAX > 0) + { + if (tstAY < 0) + { + quadA = 1; + } + else if (tstAY == 0) + { + quadA = 2; + } + else if (tstAY > 0) + { + quadA = 3; + } + } + if (tstBX < 0) + { + if (tstBY < 0) + { + quadB = 7; + } + else if (tstBY == 0) + { + quadB = 6; + } + else if (tstBY > 0) + { + quadB = 5; + } + } + else if (tstBX == 0) + { + if (tstBY < 0) + { + quadB = 0; + } + else if (tstBY == 0) + { + quadB = -1; + } + else if (tstBY > 0) + { + quadB = 4; + } + } + else if (tstBX > 0) + { + if (tstBY < 0) + { + quadB = 1; + } + else if (tstBY == 0) + { + quadB = 2; + } + else if (tstBY > 0) + { + quadB = 3; + } + } + if (quadA < quadB) + return 1; + if (quadA > quadB) + return -1; + + Geom::Point av, bv; + av = ax; + bv = bx; + double si = cross(av, bv); + int tstSi = 0; + if (si > 0.000001) tstSi = 1; + if (si < -0.000001) tstSi = -1; + if ( tstSi == 0 ) { + if ( as && !bs ) return -1; + if ( !as && bs ) return 1; + } + return tstSi; +} + +void +Shape::SortEdgesList (edge_list * list, int s, int e) +{ + if (s >= e) + return; + if (e == s + 1) { + int cmpval=CmpToVert (list[e].x, list[s].x,list[e].starting,list[s].starting); + if ( cmpval > 0 ) { // priorite aux sortants + edge_list swap = list[s]; + list[s] = list[e]; + list[e] = swap; + } + return; + } + + int ppos = (s + e) / 2; + int plast = ppos; + Geom::Point pvalx = list[ppos].x; + bool pvals = list[ppos].starting; + + int le = s, ri = e; + while (le < ppos || ri > plast) + { + if (le < ppos) + { + do + { + int test = CmpToVert (pvalx, list[le].x,pvals,list[le].starting); + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (le < ppos - 1) + { + edge_list swap = list[le]; + list[le] = list[ppos - 1]; + list[ppos - 1] = list[ppos]; + list[ppos] = swap; + ppos--; + continue; // sans changer le + } + else if (le == ppos - 1) + { + ppos--; + break; + } + else + { + // oupsie + break; + } + } + if (test > 0) + { + break; + } + le++; + } + while (le < ppos); + } + if (ri > plast) + { + do + { + int test = CmpToVert (pvalx, list[ri].x,pvals,list[ri].starting); + if (test == 0) + { + // on colle les valeurs egales au pivot ensemble + if (ri > plast + 1) + { + edge_list swap = list[ri]; + list[ri] = list[plast + 1]; + list[plast + 1] = list[plast]; + list[plast] = swap; + plast++; + continue; // sans changer ri + } + else if (ri == plast + 1) + { + plast++; + break; + } + else + { + // oupsie + break; + } + } + if (test < 0) + { + break; + } + ri--; + } + while (ri > plast); + } + + if (le < ppos) + { + if (ri > plast) + { + edge_list swap = list[le]; + list[le] = list[ri]; + list[ri] = swap; + le++; + ri--; + } + else if (le < ppos - 1) + { + edge_list swap = list[ppos - 1]; + list[ppos - 1] = list[plast]; + list[plast] = list[le]; + list[le] = swap; + ppos--; + plast--; + } + else if (le == ppos - 1) + { + edge_list swap = list[plast]; + list[plast] = list[le]; + list[le] = swap; + ppos--; + plast--; + } + else + { + break; + } + } + else + { + if (ri > plast + 1) + { + edge_list swap = list[plast + 1]; + list[plast + 1] = list[ppos]; + list[ppos] = list[ri]; + list[ri] = swap; + ppos++; + plast++; + } + else if (ri == plast + 1) + { + edge_list swap = list[ppos]; + list[ppos] = list[ri]; + list[ri] = swap; + ppos++; + plast++; + } + else + { + break; + } + } + } + SortEdgesList (list, s, ppos - 1); + SortEdgesList (list, plast + 1, e); + +} + + + +/* + * + */ +void +Shape::ConnectStart (int p, int b) +{ + if (getEdge(b).st >= 0) + DisconnectStart (b); + + _aretes[b].st = p; + _pts[p].dO++; + _aretes[b].nextS = -1; + _aretes[b].prevS = getPoint(p).incidentEdge[LAST]; + if (getPoint(p).incidentEdge[LAST] >= 0) + { + if (getEdge(getPoint(p).incidentEdge[LAST]).st == p) + { + _aretes[getPoint(p).incidentEdge[LAST]].nextS = b; + } + else if (getEdge(getPoint(p).incidentEdge[LAST]).en == p) + { + _aretes[getPoint(p).incidentEdge[LAST]].nextE = b; + } + } + _pts[p].incidentEdge[LAST] = b; + if (getPoint(p).incidentEdge[FIRST] < 0) + _pts[p].incidentEdge[FIRST] = b; +} + +void +Shape::ConnectEnd (int p, int b) +{ + if (getEdge(b).en >= 0) + DisconnectEnd (b); + _aretes[b].en = p; + _pts[p].dI++; + _aretes[b].nextE = -1; + _aretes[b].prevE = getPoint(p).incidentEdge[LAST]; + if (getPoint(p).incidentEdge[LAST] >= 0) + { + if (getEdge(getPoint(p).incidentEdge[LAST]).st == p) + { + _aretes[getPoint(p).incidentEdge[LAST]].nextS = b; + } + else if (getEdge(getPoint(p).incidentEdge[LAST]).en == p) + { + _aretes[getPoint(p).incidentEdge[LAST]].nextE = b; + } + } + _pts[p].incidentEdge[LAST] = b; + if (getPoint(p).incidentEdge[FIRST] < 0) + _pts[p].incidentEdge[FIRST] = b; +} + +void +Shape::DisconnectStart (int b) +{ + if (getEdge(b).st < 0) + return; + _pts[getEdge(b).st].dO--; + if (getEdge(b).prevS >= 0) + { + if (getEdge(getEdge(b).prevS).st == getEdge(b).st) + { + _aretes[getEdge(b).prevS].nextS = getEdge(b).nextS; + } + else if (getEdge(getEdge(b).prevS).en == getEdge(b).st) + { + _aretes[getEdge(b).prevS].nextE = getEdge(b).nextS; + } + } + if (getEdge(b).nextS >= 0) + { + if (getEdge(getEdge(b).nextS).st == getEdge(b).st) + { + _aretes[getEdge(b).nextS].prevS = getEdge(b).prevS; + } + else if (getEdge(getEdge(b).nextS).en == getEdge(b).st) + { + _aretes[getEdge(b).nextS].prevE = getEdge(b).prevS; + } + } + if (getPoint(getEdge(b).st).incidentEdge[FIRST] == b) + _pts[getEdge(b).st].incidentEdge[FIRST] = getEdge(b).nextS; + if (getPoint(getEdge(b).st).incidentEdge[LAST] == b) + _pts[getEdge(b).st].incidentEdge[LAST] = getEdge(b).prevS; + _aretes[b].st = -1; +} + +void +Shape::DisconnectEnd (int b) +{ + if (getEdge(b).en < 0) + return; + _pts[getEdge(b).en].dI--; + if (getEdge(b).prevE >= 0) + { + if (getEdge(getEdge(b).prevE).st == getEdge(b).en) + { + _aretes[getEdge(b).prevE].nextS = getEdge(b).nextE; + } + else if (getEdge(getEdge(b).prevE).en == getEdge(b).en) + { + _aretes[getEdge(b).prevE].nextE = getEdge(b).nextE; + } + } + if (getEdge(b).nextE >= 0) + { + if (getEdge(getEdge(b).nextE).st == getEdge(b).en) + { + _aretes[getEdge(b).nextE].prevS = getEdge(b).prevE; + } + else if (getEdge(getEdge(b).nextE).en == getEdge(b).en) + { + _aretes[getEdge(b).nextE].prevE = getEdge(b).prevE; + } + } + if (getPoint(getEdge(b).en).incidentEdge[FIRST] == b) + _pts[getEdge(b).en].incidentEdge[FIRST] = getEdge(b).nextE; + if (getPoint(getEdge(b).en).incidentEdge[LAST] == b) + _pts[getEdge(b).en].incidentEdge[LAST] = getEdge(b).prevE; + _aretes[b].en = -1; +} + + +void +Shape::Inverse (int b) +{ + int swap; + swap = getEdge(b).st; + _aretes[b].st = getEdge(b).en; + _aretes[b].en = swap; + swap = getEdge(b).prevE; + _aretes[b].prevE = getEdge(b).prevS; + _aretes[b].prevS = swap; + swap = getEdge(b).nextE; + _aretes[b].nextE = getEdge(b).nextS; + _aretes[b].nextS = swap; + _aretes[b].dx = -getEdge(b).dx; + if (getEdge(b).st >= 0) + { + _pts[getEdge(b).st].dO++; + _pts[getEdge(b).st].dI--; + } + if (getEdge(b).en >= 0) + { + _pts[getEdge(b).en].dO--; + _pts[getEdge(b).en].dI++; + } + if (_has_edges_data) + eData[b].weight = -eData[b].weight; + if (_has_sweep_dest_data) + { + int swap = swdData[b].leW; + swdData[b].leW = swdData[b].riW; + swdData[b].riW = swap; + } + if (_has_back_data) + { + double swat = ebData[b].tSt; + ebData[b].tSt = ebData[b].tEn; + ebData[b].tEn = swat; + } + if (_has_voronoi_data) + { + int swai = voreData[b].leF; + voreData[b].leF = voreData[b].riF; + voreData[b].riF = swai; + } +} +void +Shape::CalcBBox (bool strict_degree) +{ + if (_bbox_up_to_date) + return; + if (hasPoints() == false) + { + leftX = rightX = topY = bottomY = 0; + _bbox_up_to_date = true; + return; + } + leftX = rightX = getPoint(0).x[0]; + topY = bottomY = getPoint(0).x[1]; + bool not_set=true; + for (int i = 0; i < numberOfPoints(); i++) + { + if ( strict_degree == false || getPoint(i).dI > 0 || getPoint(i).dO > 0 ) { + if ( not_set ) { + leftX = rightX = getPoint(i).x[0]; + topY = bottomY = getPoint(i).x[1]; + not_set=false; + } else { + if ( getPoint(i).x[0] < leftX) leftX = getPoint(i).x[0]; + if ( getPoint(i).x[0] > rightX) rightX = getPoint(i).x[0]; + if ( getPoint(i).x[1] < topY) topY = getPoint(i).x[1]; + if ( getPoint(i).x[1] > bottomY) bottomY = getPoint(i).x[1]; + } + } + } + + _bbox_up_to_date = true; +} + +// winding of a point with respect to the Shape +// 0= outside +// 1= inside (or -1, that usually the same) +// other=depends on your fill rule +// if the polygon is uncrossed, it's all the same, usually +int +Shape::PtWinding (const Geom::Point px) const +{ + int lr = 0, ll = 0, rr = 0; + + for (int i = 0; i < numberOfEdges(); i++) + { + Geom::Point const adir = getEdge(i).dx; + + Geom::Point const ast = getPoint(getEdge(i).st).x; + Geom::Point const aen = getPoint(getEdge(i).en).x; + + //int const nWeight = eData[i].weight; + int const nWeight = 1; + + if (ast[0] < aen[0]) { + if (ast[0] > px[0]) continue; + if (aen[0] < px[0]) continue; + } else { + if (ast[0] < px[0]) continue; + if (aen[0] > px[0]) continue; + } + if (ast[0] == px[0]) { + if (ast[1] >= px[1]) continue; + if (aen[0] == px[0]) continue; + if (aen[0] < px[0]) ll += nWeight; else rr -= nWeight; + continue; + } + if (aen[0] == px[0]) { + if (aen[1] >= px[1]) continue; + if (ast[0] == px[0]) continue; + if (ast[0] < px[0]) ll -= nWeight; else rr += nWeight; + continue; + } + + if (ast[1] < aen[1]) { + if (ast[1] >= px[1]) continue; + } else { + if (aen[1] >= px[1]) continue; + } + + Geom::Point const diff = px - ast; + double const cote = cross(adir, diff); + if (cote == 0) continue; + if (cote < 0) { + if (ast[0] > px[0]) lr += nWeight; + } else { + if (ast[0] < px[0]) lr -= nWeight; + } + } + return lr + (ll + rr) / 2; +} + + +void Shape::initialisePointData() +{ + if (_point_data_initialised) + return; + int const N = numberOfPoints(); + + for (int i = 0; i < N; i++) { + pData[i].pending = 0; + pData[i].edgeOnLeft = -1; + pData[i].nextLinkedPoint = -1; + pData[i].rx[0] = Round(getPoint(i).x[0]); + pData[i].rx[1] = Round(getPoint(i).x[1]); + } + + _point_data_initialised = true; +} + +void Shape::initialiseEdgeData() +{ + int const N = numberOfEdges(); + + for (int i = 0; i < N; i++) { + eData[i].rdx = pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + eData[i].length = dot(eData[i].rdx, eData[i].rdx); + eData[i].ilength = 1 / eData[i].length; + eData[i].sqlength = sqrt(eData[i].length); + eData[i].isqlength = 1 / eData[i].sqlength; + eData[i].siEd = eData[i].rdx[1] * eData[i].isqlength; + eData[i].coEd = eData[i].rdx[0] * eData[i].isqlength; + + if (eData[i].siEd < 0) { + eData[i].siEd = -eData[i].siEd; + eData[i].coEd = -eData[i].coEd; + } + + swsData[i].misc = nullptr; + swsData[i].firstLinkedPoint = -1; + swsData[i].stPt = swsData[i].enPt = -1; + swsData[i].leftRnd = swsData[i].rightRnd = -1; + swsData[i].nextSh = nullptr; + swsData[i].nextBo = -1; + swsData[i].curPoint = -1; + swsData[i].doneTo = -1; + } +} + + +void Shape::clearIncidenceData() +{ + g_free(iData); + iData = nullptr; + nbInc = maxInc = 0; +} + + + +/** + * A directed graph is Eulerian iff every vertex has equal indegree and outdegree. + * http://mathworld.wolfram.com/EulerianGraph.html + * + * \param s Directed shape. + * \return true if s is Eulerian. + */ + +bool directedEulerian(Shape const *s) +{ + for (int i = 0; i < s->numberOfPoints(); i++) { + if (s->getPoint(i).dI != s->getPoint(i).dO) { + return false; + } + } + + return true; +} + + + +/** + * \param s Shape. + * \param p Point. + * \return Minimum distance from p to any of the points or edges of s. + */ + +double distance(Shape const *s, Geom::Point const &p) +{ + if ( s->hasPoints() == false) { + return 0.0; + } + + /* Find the minimum distance from p to one of the points on s. + ** Computing the dot product of the difference vector gives + ** us the distance squared; we can leave the square root + ** until the end. + */ + double bdot = Geom::dot(p - s->getPoint(0).x, p - s->getPoint(0).x); + + for (int i = 0; i < s->numberOfPoints(); i++) { + Geom::Point const offset( p - s->getPoint(i).x ); + double ndot = Geom::dot(offset, offset); + if ( ndot < bdot ) { + bdot = ndot; + } + } + + for (int i = 0; i < s->numberOfEdges(); i++) { + if ( s->getEdge(i).st >= 0 && s->getEdge(i).en >= 0 ) { + /* The edge has start and end points */ + Geom::Point const st(s->getPoint(s->getEdge(i).st).x); // edge start + Geom::Point const en(s->getPoint(s->getEdge(i).en).x); // edge end + + Geom::Point const d(p - st); // vector between p and edge start + Geom::Point const e(en - st); // vector of the edge + double const el = Geom::dot(e, e); // edge length + + /* Update bdot if appropriate */ + if ( el > 0.001 ) { + double const npr = Geom::dot(d, e); + if ( npr > 0 && npr < el ) { + double const nl = fabs( Geom::cross(d, e) ); + double ndot = nl * nl / el; + if ( ndot < bdot ) { + bdot = ndot; + } + } + } + } + } + + return sqrt(bdot); +} + + + +/** + * Returns true iff the L2 distance from \a thePt to this shape is <= \a max_l2. + * Distance = the min of distance to its points and distance to its edges. + * Points without edges are considered, which is maybe unwanted... + * + * This is largely similar to distance(). + * + * \param s Shape. + * \param p Point. + * \param max_l2 L2 distance. + */ + +bool distanceLessThanOrEqual(Shape const *s, Geom::Point const &p, double const max_l2) +{ + if ( s->hasPoints() == false ) { + return false; + } + + /* TODO: Consider using bbox to return early, perhaps conditional on nbPt or nbAr. */ + + /* TODO: Efficiency: In one test case (scribbling with the freehand tool to create a small number of long + ** path elements), changing from a Distance method to a DistanceLE method reduced this + ** function's CPU time from about 21% of total inkscape CPU time to 14-15% of total inkscape + ** CPU time, due to allowing early termination. I don't know how much the L1 test helps, it + ** may well be a case of premature optimization. Consider testing dot(offset, offset) + ** instead. + */ + + double const max_l1 = max_l2 * M_SQRT2; + for (int i = 0; i < s->numberOfPoints(); i++) { + Geom::Point const offset( p - s->getPoint(i).x ); + double const l1 = Geom::L1(offset); + if ( (l1 <= max_l2) || ((l1 <= max_l1) && (Geom::L2(offset) <= max_l2)) ) { + return true; + } + } + + for (int i = 0; i < s->numberOfEdges(); i++) { + if ( s->getEdge(i).st >= 0 && s->getEdge(i).en >= 0 ) { + Geom::Point const st(s->getPoint(s->getEdge(i).st).x); + Geom::Point const en(s->getPoint(s->getEdge(i).en).x); + Geom::Point const d(p - st); + Geom::Point const e(en - st); + double const el = Geom::L2(e); + if ( el > 0.001 ) { + Geom::Point const e_unit(e / el); + double const npr = Geom::dot(d, e_unit); + if ( npr > 0 && npr < el ) { + double const nl = fabs(Geom::cross(d, e_unit)); + if ( nl <= max_l2 ) { + return true; + } + } + } + } + } + + return false; +} + +//}; + diff --git a/src/livarot/Shape.h b/src/livarot/Shape.h new file mode 100644 index 0000000..2764b90 --- /dev/null +++ b/src/livarot/Shape.h @@ -0,0 +1,577 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef my_shape +#define my_shape + +#include +#include +#include +#include +#include +#include <2geom/point.h> + +#include "livarot/LivarotDefs.h" +#include "object/object-set.h" + +class Path; +class FloatLigne; + +class SweepTree; +class SweepTreeList; +class SweepEventQueue; + +enum { + tweak_mode_grow, + tweak_mode_push, + tweak_mode_repel, + tweak_mode_roughen +}; + +/* + * the Shape class (was the Digraph class, as the header says) stores digraphs (no kidding!) of which + * a very interesting kind are polygons. + * the main use of this class is the ConvertToShape() (or Booleen(), quite the same) function, which + * removes all problems a polygon can present: duplicate points or edges, self-intersection. you end up with a + * full-fledged polygon + */ + +// possible values for the "type" field in the Shape class: +enum +{ + shape_graph = 0, // it's just a graph; a bunch of edges, maybe intersections + shape_polygon = 1, // a polygon: intersection-free, edges oriented so that the inside is on their left + shape_polypatch = 2 // a graph without intersection; each face is a polygon (not yet used) +}; + +class IntLigne; +class BitLigne; +class AlphaLigne; + +class Shape +{ +public: + + struct back_data + { + int pathID, pieceID; + double tSt, tEn; + }; + + struct voronoi_point + { // info for points treated as points of a voronoi diagram (obtained by MakeShape()) + double value; // distance to source + int winding; // winding relatively to source + }; + + struct voronoi_edge + { // info for edges, treated as approximation of edges of the voronoi diagram + int leF, riF; // left and right site + double leStX, leStY, riStX, riStY; // on the left side: (leStX,leStY) is the smallest vector from the source to st + // etc... + double leEnX, leEnY, riEnX, riEnY; + }; + + struct quick_raster_data + { + double x; // x-position on the sweepline + int bord; // index of the edge + int ind; // index of qrsData elem for edge (ie inverse of the bord) + int next,prev; // dbl linkage + }; + + enum sTreeChangeType + { + EDGE_INSERTED = 0, + EDGE_REMOVED = 1, + INTERSECTION = 2 + }; + + struct sTreeChange + { + sTreeChangeType type; // type of modification to the sweepline: + int ptNo; // point at which the modification takes place + + Shape *src; // left edge (or unique edge if not an intersection) involved in the event + int bord; + Shape *osrc; // right edge (if intersection) + int obord; + Shape *lSrc; // edge directly on the left in the sweepline at the moment of the event + int lBrd; + Shape *rSrc; // edge directly on the right + int rBrd; + }; + + struct incidenceData + { + int nextInc; // next incidence in the linked list + int pt; // point incident to the edge (there is one list per edge) + double theta; // coordinate of the incidence on the edge + }; + + Shape(); + virtual ~Shape(); + + void MakeBackData(bool nVal); + void MakeVoronoiData(bool nVal); + + void Affiche(); + + // insertion/deletion/movement of elements in the graph + void Copy(Shape *a); + // -reset the graph, and ensure there's room for n points and m edges + void Reset(int n = 0, int m = 0); + // -points: + int AddPoint(const Geom::Point x); // as the function name says + // returns the index at which the point has been added in the array + void SubPoint(int p); // removes the point at index p + // nota: this function relocates the last point to the index p + // so don't trust point indices if you use SubPoint + void SwapPoints(int a, int b); // swaps 2 points at indices a and b + void SwapPoints(int a, int b, int c); // swaps 3 points: c <- a <- b <- c + void SortPoints(); // sorts the points if needed (checks the need_points_sorting flag) + + // -edges: + // add an edge between points of indices st and en + int AddEdge(int st, int en); + // return the edge index in the array + + // add an edge between points of indices st and en + int AddEdge(int st, int en, int leF, int riF); + // return the edge index in the array + + // version for the voronoi (with faces IDs) + void SubEdge(int e); // removes the edge at index e (same remarks as for SubPoint) + void SwapEdges(int a, int b); // swaps 2 edges + void SwapEdges(int a, int b, int c); // swaps 3 edges + void SortEdges(); // sort the edges if needed (checks the need_edges_sorting falg) + + // primitives for topological manipulations + + // endpoint of edge at index b that is different from the point p + inline int Other(int p, int b) const + { + if (getEdge(b).st == p) { + return getEdge(b).en; + } + return getEdge(b).st; + } + + // next edge (after edge b) in the double-linked list at point p + inline int NextAt(int p, int b) const + { + if (p == getEdge(b).st) { + return getEdge(b).nextS; + } + else if (p == getEdge(b).en) { + return getEdge(b).nextE; + } + + return -1; + } + + // previous edge + inline int PrevAt(int p, int b) const + { + if (p == getEdge(b).st) { + return getEdge(b).prevS; + } + else if (p == getEdge(b).en) { + return getEdge(b).prevE; + } + + return -1; + } + + // same as NextAt, but the list is considered circular + inline int CycleNextAt(int p, int b) const + { + if (p == getEdge(b).st) { + if (getEdge(b).nextS < 0) { + return getPoint(p).incidentEdge[FIRST]; + } + return getEdge(b).nextS; + } else if (p == getEdge(b).en) { + if (getEdge(b).nextE < 0) { + return getPoint(p).incidentEdge[FIRST]; + } + + return getEdge(b).nextE; + } + + return -1; + } + + // same as PrevAt, but the list is considered circular + inline int CyclePrevAt(int p, int b) const + { + if (p == getEdge(b).st) { + if (getEdge(b).prevS < 0) { + return getPoint(p).incidentEdge[LAST]; + } + return getEdge(b).prevS; + } else if (p == getEdge(b).en) { + if (getEdge(b).prevE < 0) { + return getPoint(p).incidentEdge[LAST]; + } + return getEdge(b).prevE; + } + + return -1; + } + + void ConnectStart(int p, int b); // set the point p as the start of edge b + void ConnectEnd(int p, int b); // set the point p as the end of edge b + void DisconnectStart(int b); // disconnect edge b from its start point + void DisconnectEnd(int b); // disconnect edge b from its end point + + // reverses edge b (start <-> end) + void Inverse(int b); + // calc bounding box and sets leftX,rightX,topY and bottomY to their values + void CalcBBox(bool strict_degree = false); + + // debug function: plots the graph (mac only) + void Plot(double ix, double iy, double ir, double mx, double my, bool doPoint, + bool edgesNo, bool pointNo, bool doDir, char *fileName); + + // transforms a polygon in a "forme" structure, ie a set of contours, which can be holes (see ShapeUtils.h) + // return NULL in case it's not possible + void ConvertToForme(Path *dest); + + // version to use when conversion was done with ConvertWithBackData(): will attempt to merge segment belonging to + // the same curve + // nota: apparently the function doesn't like very small segments of arc + void ConvertToForme(Path *dest, int nbP, Path **orig, bool splitWhenForced = false); + // version trying to recover the nesting of subpaths (ie: holes) + void ConvertToFormeNested(Path *dest, int nbP, Path **orig, int wildPath, int &nbNest, + int *&nesting, int *&contStart, bool splitWhenForced = false); + + // sweeping a digraph to produce a intersection-free polygon + // return 0 if everything is ok and a return code otherwise (see LivarotDefs.h) + // the input is the Shape "a" + // directed=true <=> non-zero fill rule + int ConvertToShape(Shape *a, FillRule directed = fill_nonZero, bool invert = false); + // directed=false <=> even-odd fill rule + // invert=true: make as if you inverted all edges in the source + int Reoriente(Shape *a); // subcase of ConvertToShape: the input a is already intersection-free + // all that's missing are the correct directions of the edges + // Reoriented is equivalent to ConvertToShape(a,false,false) , but faster sicne + // it doesn't computes interections nor adjacencies + void ForceToPolygon(); // force the Shape to believe it's a polygon (eulerian+intersection-free+no + // duplicate edges+no duplicate points) + // be careful when using this function + + // the coordinate rounding function + inline static double Round(double x) + { + return ldexp(rint(ldexp(x, 9)), -9); + } + + // 2 miscannellous variations on it, to scale to and back the rounding grid + inline static double HalfRound(double x) + { + return ldexp(x, -9); + } + + inline static double IHalfRound(double x) + { + return ldexp(x, 9); + } + + // boolean operations on polygons (requests intersection-free poylygons) + // boolean operation types are defined in LivarotDefs.h + // same return code as ConvertToShape + int Booleen(Shape *a, Shape *b, BooleanOp mod, int cutPathID = -1); + + // create a graph that is an offseted version of the graph "of" + // the offset is dec, with joins between edges of type "join" (see LivarotDefs.h) + // the result is NOT a polygon; you need a subsequent call to ConvertToShape to get a real polygon + int MakeOffset(Shape *of, double dec, JoinType join, double miter, bool do_profile=false, double cx = 0, double cy = 0, double radius = 0, Geom::Affine *i2doc = nullptr); + + int MakeTweak (int mode, Shape *a, double dec, JoinType join, double miter, bool do_profile, Geom::Point c, Geom::Point vector, double radius, Geom::Affine *i2doc); + + int PtWinding(const Geom::Point px) const; // plus rapide + int Winding(const Geom::Point px) const; + + // rasterization + void BeginRaster(float &pos, int &curPt); + void EndRaster(); + void BeginQuickRaster(float &pos, int &curPt); + void EndQuickRaster(); + + void Scan(float &pos, int &curP, float to, float step); + void QuickScan(float &pos, int &curP, float to, bool doSort, float step); + void DirectScan(float &pos, int &curP, float to, float step); + void DirectQuickScan(float &pos, int &curP, float to, bool doSort, float step); + + void Scan(float &pos, int &curP, float to, FloatLigne *line, bool exact, float step); + void Scan(float &pos, int &curP, float to, FillRule directed, BitLigne *line, bool exact, float step); + void Scan(float &pos, int &curP, float to, AlphaLigne *line, bool exact, float step); + + void QuickScan(float &pos, int &curP, float to, FloatLigne* line, float step); + void QuickScan(float &pos, int &curP, float to, FillRule directed, BitLigne* line, float step); + void QuickScan(float &pos, int &curP, float to, AlphaLigne* line, float step); + + void Transform(Geom::Affine const &tr) + {for(auto & _pt : _pts) _pt.x*=tr;} + + std::vector ebData; + std::vector vorpData; + std::vector voreData; + + int nbQRas; + int firstQRas; + int lastQRas; + quick_raster_data *qrsData; + + std::vector chgts; + int nbInc; + int maxInc; + + incidenceData *iData; + // these ones are allocated at the beginning of each sweep and freed at the end of the sweep + SweepTreeList *sTree; + SweepEventQueue *sEvts; + + // bounding box stuff + double leftX, topY, rightX, bottomY; + + // topological information: who links who? + struct dg_point + { + Geom::Point x; // position + int dI, dO; // indegree and outdegree + int incidentEdge[2]; // first and last incident edge + int oldDegree; + + int totalDegree() const { return dI + dO; } + }; + + struct dg_arete + { + Geom::Point dx; // edge vector + int st, en; // start and end points of the edge + int nextS, prevS; // next and previous edge in the double-linked list at the start point + int nextE, prevE; // next and previous edge in the double-linked list at the end point + }; + + // lists of the nodes and edges + int maxPt; // [FIXME: remove this] + int maxAr; // [FIXME: remove this] + + // flags + int type; + + inline int numberOfPoints() const { return _pts.size(); } + inline bool hasPoints() const { return (_pts.empty() == false); } + inline int numberOfEdges() const { return _aretes.size(); } + inline bool hasEdges() const { return (_aretes.empty() == false); } + + inline void needPointsSorting() { _need_points_sorting = true; } + inline void needEdgesSorting() { _need_edges_sorting = true; } + + inline bool hasBackData() const { return _has_back_data; } + + inline dg_point const &getPoint(int n) const { return _pts[n]; } + inline dg_arete const &getEdge(int n) const { return _aretes[n]; } + +private: + + friend class SweepTree; + friend class SweepEvent; + friend class SweepEventQueue; + + // temporary data for the various algorithms + struct edge_data + { + int weight; // weight of the edge (to handle multiple edges) + Geom::Point rdx; // rounded edge vector + double length, sqlength, ilength, isqlength; // length^2, length, 1/length^2, 1/length + double siEd, coEd; // siEd=abs(rdy/length) and coEd=rdx/length + edge_data() : weight(0), length(0.0), sqlength(0.0), ilength(0.0), isqlength(0.0), siEd(0.0), coEd(0.0) {} + // used to determine the "most horizontal" edge between 2 edges + }; + + struct sweep_src_data + { + void *misc; // pointer to the SweepTree* in the sweepline + int firstLinkedPoint; // not used + int stPt, enPt; // start- end end- points for this edge in the resulting polygon + int ind; // for the GetAdjacencies function: index in the sliceSegs array (for quick deletions) + int leftRnd, rightRnd; // leftmost and rightmost points (in the result polygon) that are incident to + // the edge, for the current sweep position + // not set if the edge doesn't start/end or intersect at the current sweep position + Shape *nextSh; // nextSh and nextBo identify the next edge in the list + int nextBo; // they are used to maintain a linked list of edge that start/end or intersect at + // the current sweep position + int curPoint, doneTo; + double curT; + }; + + struct sweep_dest_data + { + void *misc; // used to check if an edge has already been seen during the depth-first search + int suivParc, precParc; // previous and current next edge in the depth-first search + int leW, riW; // left and right winding numbers for this edge + int ind; // order of the edges during the depth-first search + }; + + struct raster_data + { + SweepTree *misc; // pointer to the associated SweepTree* in the sweepline + double lastX, lastY, curX, curY; // curX;curY is the current intersection of the edge with the sweepline + // lastX;lastY is the intersection with the previous sweepline + bool sens; // true if the edge goes down, false otherwise + double calcX; // horizontal position of the intersection of the edge with the + // previous sweepline + double dxdy, dydx; // horizontal change per unit vertical move of the intersection with the sweepline + int guess; + }; + + struct point_data + { + int oldInd, newInd; // back and forth indices used when sorting the points, to know where they have + // been relocated in the array + int pending; // number of intersection attached to this edge, and also used when sorting arrays + int edgeOnLeft; // not used (should help speeding up winding calculations) + int nextLinkedPoint; // not used + Shape *askForWindingS; + int askForWindingB; + Geom::Point rx; // rounded coordinates of the point + }; + + + struct edge_list + { // temporary array of edges for easier sorting + int no; + bool starting; + Geom::Point x; + }; + + void initialisePointData(); + void initialiseEdgeData(); + void clearIncidenceData(); + + void _countUpDown(int P, int *numberUp, int *numberDown, int *upEdge, int *downEdge) const; + void _countUpDownTotalDegree2(int P, int *numberUp, int *numberDown, int *upEdge, int *downEdge) const; + void _updateIntersection(int e, int p); + + // activation/deactivation of the temporary data arrays + void MakePointData(bool nVal); + void MakeEdgeData(bool nVal); + void MakeSweepSrcData(bool nVal); + void MakeSweepDestData(bool nVal); + void MakeRasterData(bool nVal); + void MakeQuickRasterData(bool nVal); + + void SortPoints(int s, int e); + void SortPointsByOldInd(int s, int e); + + // fonctions annexes pour ConvertToShape et Booleen + void ResetSweep(); // allocates sweep structures + void CleanupSweep(); // deallocates them + + // edge sorting function + void SortEdgesList(edge_list *edges, int s, int e); + + void TesteIntersection(SweepTree *t, Side s, bool onlyDiff); // test if there is an intersection + bool TesteIntersection(SweepTree *iL, SweepTree *iR, Geom::Point &atx, double &atL, double &atR, bool onlyDiff); + bool TesteIntersection(Shape *iL, Shape *iR, int ilb, int irb, + Geom::Point &atx, double &atL, double &atR, + bool onlyDiff); + bool TesteAdjacency(Shape *iL, int ilb, const Geom::Point atx, int nPt, + bool push); + int PushIncidence(Shape *a, int cb, int pt, double theta); + int CreateIncidence(Shape *a, int cb, int pt); + void AssemblePoints(Shape *a); + int AssemblePoints(int st, int en); + void AssembleAretes(FillRule directed = fill_nonZero); + void AddChgt(int lastPointNo, int lastChgtPt, Shape *&shapeHead, + int &edgeHead, sTreeChangeType type, Shape *lS, int lB, Shape *rS, + int rB); + void CheckAdjacencies(int lastPointNo, int lastChgtPt, Shape *shapeHead, int edgeHead); + void CheckEdges(int lastPointNo, int lastChgtPt, Shape *a, Shape *b, BooleanOp mod); + void Avance(int lastPointNo, int lastChgtPt, Shape *iS, int iB, Shape *a, Shape *b, BooleanOp mod); + void DoEdgeTo(Shape *iS, int iB, int iTo, bool direct, bool sens); + void GetWindings(Shape *a, Shape *b = nullptr, BooleanOp mod = bool_op_union, bool brutal = false); + + void Validate(); + + int Winding(int nPt) const; + void SortPointsRounded(); + void SortPointsRounded(int s, int e); + + void CreateEdge(int no, float to, float step); + void AvanceEdge(int no, float to, bool exact, float step); + void DestroyEdge(int no, float to, FloatLigne *line); + void AvanceEdge(int no, float to, FloatLigne *line, bool exact, float step); + void DestroyEdge(int no, BitLigne *line); + void AvanceEdge(int no, float to, BitLigne *line, bool exact, float step); + void DestroyEdge(int no, AlphaLigne *line); + void AvanceEdge(int no, float to, AlphaLigne *line, bool exact, float step); + + void AddContour(Path * dest, int nbP, Path **orig, int startBord, + int curBord, bool splitWhenForced); + int ReFormeLineTo(int bord, int curBord, Path *dest, Path *orig); + int ReFormeArcTo(int bord, int curBord, Path *dest, Path *orig); + int ReFormeCubicTo(int bord, int curBord, Path *dest, Path *orig); + int ReFormeBezierTo(int bord, int curBord, Path *dest, Path *orig); + void ReFormeBezierChunk(const Geom::Point px, const Geom::Point nx, + Path *dest, int inBezier, int nbInterm, + Path *from, int p, double ts, double te); + + int QuickRasterChgEdge(int oBord, int nbord, double x); + int QuickRasterAddEdge(int bord, double x, int guess); + void QuickRasterSubEdge(int bord); + void QuickRasterSwapEdge(int a, int b); + void QuickRasterSort(); + + bool _need_points_sorting; ///< points have been added or removed: we need to sort the points again + bool _need_edges_sorting; ///< edges have been added: maybe they are not ordered clockwise + ///< nota: if you remove an edge, the clockwise order still holds + bool _has_points_data; ///< the pData array is allocated + bool _point_data_initialised;///< the pData array is up to date + bool _has_edges_data; ///< the eData array is allocated + bool _has_sweep_src_data; ///< the swsData array is allocated + bool _has_sweep_dest_data; ///< the swdData array is allocated + bool _has_raster_data; ///< the swrData array is allocated + bool _has_quick_raster_data;///< the swrData array is allocated + bool _has_back_data; //< the ebData array is allocated + bool _has_voronoi_data; + bool _bbox_up_to_date; ///< the leftX/rightX/topY/bottomY are up to date + + std::vector _pts; + std::vector _aretes; + + // the arrays of temporary data + // these ones are dynamically kept at a length of maxPt or maxAr + std::vector eData; + std::vector swsData; + std::vector swdData; + std::vector swrData; + std::vector pData; + + static int CmpQRs(const quick_raster_data &p1, const quick_raster_data &p2) { + if ( fabs(p1.x - p2.x) < 0.00001 ) { + return 0; + } + + return ( ( p1.x < p2.x ) ? -1 : 1 ); + }; + + // edge direction comparison function + static int CmpToVert(const Geom::Point ax, const Geom::Point bx, bool as, bool bs); +}; + +bool directedEulerian(Shape const *s); +double distance(Shape const *s, Geom::Point const &p); +bool distanceLessThanOrEqual(Shape const *s, Geom::Point const &p, double const max_l2); + +#endif diff --git a/src/livarot/ShapeDraw.cpp b/src/livarot/ShapeDraw.cpp new file mode 100644 index 0000000..79d070b --- /dev/null +++ b/src/livarot/ShapeDraw.cpp @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * ShapeDraw.cpp + * nlivarot + * + * Created by fred on Mon Jun 16 2003. + * + */ + +#include +#include +#include +#include "Shape.h" +//#include + +// debug routine for vizualizing the polygons +void +Shape::Plot (double ix, double iy, double ir, double mx, double my, bool doPoint, + bool edgesNo, bool pointsNo, bool doDir,char* fileName) +{ + FILE* outFile=fopen(fileName,"w+"); +// fprintf(outFile,"\n\n\n"); + fprintf(outFile,"\n"); + fprintf(outFile,"\n"); + fprintf(outFile,"\n"); + fprintf(outFile," \n"); + fprintf(outFile," \n"); + + if ( doPoint ) { + for (int i=0;i\n",ph,pv); // localizing ok + } + } + if ( pointsNo ) { + for (int i=0;i\n",ph-2,pv+1); // localizing ok + fprintf(outFile,"%i\n",i); + fprintf(outFile," \n"); + } + } + { + for (int i=0;i\n",sh,sv,endh,endv); // localizing ok + } else { + fprintf(outFile," \n",sh,sv,eh,ev); // localizing ok + } + } + } + if ( edgesNo ) { + for (int i=0;i\n",(sh+eh)/2+2,(sv+ev)/2); // localizing ok + fprintf(outFile,"%i\n",i); + fprintf(outFile," \n"); + } + } + + fprintf(outFile,"\n"); + fclose(outFile); + +} diff --git a/src/livarot/ShapeMisc.cpp b/src/livarot/ShapeMisc.cpp new file mode 100644 index 0000000..b76ea58 --- /dev/null +++ b/src/livarot/ShapeMisc.cpp @@ -0,0 +1,1457 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livarot/Shape.h" +#include "livarot/Path.h" +#include "livarot/path-description.h" +#include +#include +#include +#include +#include <2geom/point.h> +#include <2geom/affine.h> + +/* + * polygon offset and polyline to path reassembling (when using back data) + */ + +// until i find something better +#define MiscNormalize(v) {\ + double _l=sqrt(dot(v,v)); \ + if ( _l < 0.0000001 ) { \ + v[0]=v[1]=0; \ + } else { \ + v/=_l; \ + }\ +} + +// extracting the contour of an uncrossed polygon: a mere depth first search +// more precisely that's extracting an eulerian path from a graph, but here we want to split +// the polygon into contours and avoid holes. so we take a "next counter-clockwise edge first" approach +// (make a checkboard and extract its contours to see the difference) +void +Shape::ConvertToForme (Path * dest) +{ + if (numberOfPoints() <= 1 || numberOfEdges() <= 1) + return; + + // prepare + dest->Reset (); + + MakePointData (true); + MakeEdgeData (true); + MakeSweepDestData (true); + + for (int i = 0; i < numberOfPoints(); i++) + { + pData[i].rx[0] = Round (getPoint(i).x[0]); + pData[i].rx[1] = Round (getPoint(i).x[1]); + } + for (int i = 0; i < numberOfEdges(); i++) + { + eData[i].rdx = pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + } + + // sort edge clockwise, with the closest after midnight being first in the doubly-linked list + // that's vital to the algorithm... + SortEdges (); + + // depth-first search implies: we make a stack of edges traversed. + // precParc: previous in the stack + // suivParc: next in the stack + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].misc = nullptr; + swdData[i].precParc = swdData[i].suivParc = -1; + } + + int searchInd = 0; + + int lastPtUsed = 0; + do + { + // first get a starting point, and a starting edge + // -> take the upper left point, and take its first edge + // points traversed have swdData[].misc != 0, so it's easy + int startBord = -1; + { + int fi = 0; + for (fi = lastPtUsed; fi < numberOfPoints(); fi++) + { + if (getPoint(fi).incidentEdge[FIRST] >= 0 && swdData[getPoint(fi).incidentEdge[FIRST]].misc == nullptr) + break; + } + lastPtUsed = fi + 1; + if (fi < numberOfPoints()) + { + int bestB = getPoint(fi).incidentEdge[FIRST]; + while (bestB >= 0 && getEdge(bestB).st != fi) + bestB = NextAt (fi, bestB); + if (bestB >= 0) + { + startBord = bestB; + dest->MoveTo (getPoint(getEdge(startBord).en).x); + } + } + } + // and walk the graph, doing contours when needed + if (startBord >= 0) + { + // parcours en profondeur pour mettre les leF et riF a leurs valeurs + swdData[startBord].misc = (void *) 1; + // printf("part de %d\n",startBord); + int curBord = startBord; + bool back = false; + swdData[curBord].precParc = -1; + swdData[curBord].suivParc = -1; + do + { + int cPt = getEdge(curBord).en; + int nb = curBord; + // printf("de curBord= %d au point %i -> ",curBord,cPt); + // get next edge + do + { + int nnb = CycleNextAt (cPt, nb); + if (nnb == nb) + { + // cul-de-sac + nb = -1; + break; + } + nb = nnb; + if (nb < 0 || nb == curBord) + break; + } + while (swdData[nb].misc != nullptr || getEdge(nb).st != cPt); + + if (nb < 0 || nb == curBord) + { + // no next edge: end of this contour, we get back + if (back == false) + dest->Close (); + back = true; + // retour en arriere + curBord = swdData[curBord].precParc; + // printf("retour vers %d\n",curBord); + if (curBord < 0) + break; + } + else + { + // new edge, maybe for a new contour + if (back) + { + // we were backtracking, so if we have a new edge, that means we're creating a new contour + dest->MoveTo (getPoint(cPt).x); + back = false; + } + swdData[nb].misc = (void *) 1; + swdData[nb].ind = searchInd++; + swdData[nb].precParc = curBord; + swdData[curBord].suivParc = nb; + curBord = nb; + // printf("suite %d\n",curBord); + { + // add that edge + dest->LineTo (getPoint(getEdge(nb).en).x); + } + } + } + while (true /*swdData[curBord].precParc >= 0 */ ); + // fin du cas non-oriente + } + } + while (lastPtUsed < numberOfPoints()); + + MakePointData (false); + MakeEdgeData (false); + MakeSweepDestData (false); +} + +// same as before, but each time we have a contour, try to reassemble the segments on it to make chunks of +// the original(s) path(s) +// originals are in the orig array, whose size is nbP +void +Shape::ConvertToForme (Path * dest, int nbP, Path * *orig, bool splitWhenForced) +{ + if (numberOfPoints() <= 1 || numberOfEdges() <= 1) + return; +// if (Eulerian (true) == false) +// return; + + if (_has_back_data == false) + { + ConvertToForme (dest); + return; + } + + dest->Reset (); + + MakePointData (true); + MakeEdgeData (true); + MakeSweepDestData (true); + + for (int i = 0; i < numberOfPoints(); i++) + { + pData[i].rx[0] = Round (getPoint(i).x[0]); + pData[i].rx[1] = Round (getPoint(i).x[1]); + } + for (int i = 0; i < numberOfEdges(); i++) + { + eData[i].rdx = pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + } + + SortEdges (); + + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].misc = nullptr; + swdData[i].precParc = swdData[i].suivParc = -1; + } + + int searchInd = 0; + + int lastPtUsed = 0; + do + { + int startBord = -1; + { + int fi = 0; + for (fi = lastPtUsed; fi < numberOfPoints(); fi++) + { + if (getPoint(fi).incidentEdge[FIRST] >= 0 && swdData[getPoint(fi).incidentEdge[FIRST]].misc == nullptr) + break; + } + lastPtUsed = fi + 1; + if (fi < numberOfPoints()) + { + int bestB = getPoint(fi).incidentEdge[FIRST]; + while (bestB >= 0 && getEdge(bestB).st != fi) + bestB = NextAt (fi, bestB); + if (bestB >= 0) + { + startBord = bestB; + } + } + } + if (startBord >= 0) + { + // parcours en profondeur pour mettre les leF et riF a leurs valeurs + swdData[startBord].misc = (void *) 1; + //printf("part de %d\n",startBord); + int curBord = startBord; + bool back = false; + swdData[curBord].precParc = -1; + swdData[curBord].suivParc = -1; + int curStartPt=getEdge(curBord).st; + do + { + int cPt = getEdge(curBord).en; + int nb = curBord; + //printf("de curBord= %d au point %i -> ",curBord,cPt); + do + { + int nnb = CycleNextAt (cPt, nb); + if (nnb == nb) + { + // cul-de-sac + nb = -1; + break; + } + nb = nnb; + if (nb < 0 || nb == curBord) + break; + } + while (swdData[nb].misc != nullptr || getEdge(nb).st != cPt); + + if (nb < 0 || nb == curBord) + { + if (back == false) + { + if (curBord == startBord || curBord < 0) + { + // probleme -> on vire le moveto + // dest->descr_nb--; + } + else + { + swdData[curBord].suivParc = -1; + AddContour (dest, nbP, orig, startBord, curBord,splitWhenForced); + } + // dest->Close(); + } + back = true; + // retour en arriere + curBord = swdData[curBord].precParc; + //printf("retour vers %d\n",curBord); + if (curBord < 0) + break; + } + else + { + if (back) + { + back = false; + startBord = nb; + curStartPt=getEdge(nb).st; + } else { + if ( getEdge(curBord).en == curStartPt ) { + //printf("contour %i ",curStartPt); + swdData[curBord].suivParc = -1; + AddContour (dest, nbP, orig, startBord, curBord,splitWhenForced); + startBord=nb; + } + } + swdData[nb].misc = (void *) 1; + swdData[nb].ind = searchInd++; + swdData[nb].precParc = curBord; + swdData[curBord].suivParc = nb; + curBord = nb; + //printf("suite %d\n",curBord); + } + } + while (true /*swdData[curBord].precParc >= 0 */ ); + // fin du cas non-oriente + } + } + while (lastPtUsed < numberOfPoints()); + + MakePointData (false); + MakeEdgeData (false); + MakeSweepDestData (false); +} +void +Shape::ConvertToFormeNested (Path * dest, int nbP, Path * *orig, int /*wildPath*/,int &nbNest,int *&nesting,int *&contStart,bool splitWhenForced) +{ + nesting=nullptr; + contStart=nullptr; + nbNest=0; + + if (numberOfPoints() <= 1 || numberOfEdges() <= 1) + return; + // if (Eulerian (true) == false) + // return; + + if (_has_back_data == false) + { + ConvertToForme (dest); + return; + } + + dest->Reset (); + +// MakePointData (true); + MakeEdgeData (true); + MakeSweepDestData (true); + + for (int i = 0; i < numberOfPoints(); i++) + { + pData[i].rx[0] = Round (getPoint(i).x[0]); + pData[i].rx[1] = Round (getPoint(i).x[1]); + } + for (int i = 0; i < numberOfEdges(); i++) + { + eData[i].rdx = pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + } + + SortEdges (); + + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].misc = nullptr; + swdData[i].precParc = swdData[i].suivParc = -1; + } + + int searchInd = 0; + + int lastPtUsed = 0; + int parentContour=-1; + do + { + int childEdge = -1; + bool foundChild = false; + int startBord = -1; + { + int fi = 0; + for (fi = lastPtUsed; fi < numberOfPoints(); fi++) + { + if (getPoint(fi).incidentEdge[FIRST] >= 0 && swdData[getPoint(fi).incidentEdge[FIRST]].misc == nullptr) + break; + } + { + int askTo = pData[fi].askForWindingB; + if (askTo < 0 || askTo >= numberOfEdges() ) { + parentContour=-1; + } else { + if (getEdge(askTo).prevS >= 0) { + parentContour = GPOINTER_TO_INT(swdData[askTo].misc); + parentContour-=1; // pour compenser le decalage + } + childEdge = getPoint(fi % numberOfPoints()).incidentEdge[FIRST]; + } + } + lastPtUsed = fi + 1; + if (fi < numberOfPoints()) + { + int bestB = getPoint(fi).incidentEdge[FIRST]; + while (bestB >= 0 && getEdge(bestB).st != fi) + bestB = NextAt (fi, bestB); + if (bestB >= 0) + { + startBord = bestB; + } + } + } + if (startBord >= 0) + { + // parcours en profondeur pour mettre les leF et riF a leurs valeurs + swdData[startBord].misc = (void *)(intptr_t)(1 + nbNest); + if (startBord == childEdge) { + foundChild = true; + } + //printf("part de %d\n",startBord); + int curBord = startBord; + bool back = false; + swdData[curBord].precParc = -1; + swdData[curBord].suivParc = -1; + int curStartPt=getEdge(curBord).st; + do + { + int cPt = getEdge(curBord).en; + int nb = curBord; + //printf("de curBord= %d au point %i -> ",curBord,cPt); + do + { + int nnb = CycleNextAt (cPt, nb); + if (nnb == nb) + { + // cul-de-sac + nb = -1; + break; + } + nb = nnb; + if (nb < 0 || nb == curBord) + break; + } + while (swdData[nb].misc != nullptr || getEdge(nb).st != cPt); + + if (nb < 0 || nb == curBord) + { + if (back == false) + { + if (curBord == startBord || curBord < 0) + { + // probleme -> on vire le moveto + // dest->descr_nb--; + } + else + { +// bool escapePath=false; +// int tb=curBord; +// while ( tb >= 0 && tb < numberOfEdges() ) { +// if ( ebData[tb].pathID == wildPath ) { +// escapePath=true; +// break; +// } +// tb=swdData[tb].precParc; +// } + nesting=(int*)g_realloc(nesting,(nbNest+1)*sizeof(int)); + contStart=(int*)g_realloc(contStart,(nbNest+1)*sizeof(int)); + contStart[nbNest]=dest->descr_cmd.size(); + if (foundChild) { + nesting[nbNest++]=parentContour; + foundChild = false; + } else { + nesting[nbNest++]=-1; // contient des bouts de coupure -> a part + } + swdData[curBord].suivParc = -1; + AddContour (dest, nbP, orig, startBord, curBord,splitWhenForced); + } + // dest->Close(); + } + back = true; + // retour en arriere + curBord = swdData[curBord].precParc; + //printf("retour vers %d\n",curBord); + if (curBord < 0) + break; + } + else + { + if (back) + { + back = false; + startBord = nb; + curStartPt=getEdge(nb).st; + } else { + if ( getEdge(curBord).en == curStartPt ) { + //printf("contour %i ",curStartPt); + +// bool escapePath=false; +// int tb=curBord; +// while ( tb >= 0 && tb < numberOfEdges() ) { +// if ( ebData[tb].pathID == wildPath ) { +// escapePath=true; +// break; +// } +// tb=swdData[tb].precParc; +// } + nesting=(int*)g_realloc(nesting,(nbNest+1)*sizeof(int)); + contStart=(int*)g_realloc(contStart,(nbNest+1)*sizeof(int)); + contStart[nbNest]=dest->descr_cmd.size(); + if (foundChild) { + nesting[nbNest++]=parentContour; + foundChild = false; + } else { + nesting[nbNest++]=-1; // contient des bouts de coupure -> a part + } + swdData[curBord].suivParc = -1; + AddContour (dest, nbP, orig, startBord, curBord,splitWhenForced); + startBord=nb; + } + } + swdData[nb].misc = (void *)(intptr_t)(1 + nbNest); + swdData[nb].ind = searchInd++; + swdData[nb].precParc = curBord; + swdData[curBord].suivParc = nb; + curBord = nb; + if (nb == childEdge) { + foundChild = true; + } + //printf("suite %d\n",curBord); + } + } + while (true /*swdData[curBord].precParc >= 0 */ ); + // fin du cas non-oriente + } + } + while (lastPtUsed < numberOfPoints()); + + MakePointData (false); + MakeEdgeData (false); + MakeSweepDestData (false); +} + + +int +Shape::MakeTweak (int mode, Shape *a, double power, JoinType join, double miter, bool do_profile, Geom::Point c, Geom::Point vector, double radius, Geom::Affine *i2doc) +{ + Reset (0, 0); + MakeBackData(a->_has_back_data); + + bool done_something = false; + + if (power == 0) + { + _pts = a->_pts; + if (numberOfPoints() > maxPt) + { + maxPt = numberOfPoints(); + if (_has_points_data) { + pData.resize(maxPt); + _point_data_initialised = false; + _bbox_up_to_date = false; + } + } + + _aretes = a->_aretes; + if (numberOfEdges() > maxAr) + { + maxAr = numberOfEdges(); + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_raster_data) + swrData.resize(maxAr); + if (_has_back_data) + ebData.resize(maxAr); + } + return 0; + } + if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1 || a->type != shape_polygon) + return shape_input_err; + + a->SortEdges (); + + a->MakeSweepDestData (true); + a->MakeSweepSrcData (true); + + for (int i = 0; i < a->numberOfEdges(); i++) + { + int stB = -1, enB = -1; + if (power <= 0 || mode == tweak_mode_push || mode == tweak_mode_repel || mode == tweak_mode_roughen) { + stB = a->CyclePrevAt (a->getEdge(i).st, i); + enB = a->CycleNextAt (a->getEdge(i).en, i); + } else { + stB = a->CycleNextAt (a->getEdge(i).st, i); + enB = a->CyclePrevAt (a->getEdge(i).en, i); + } + + Geom::Point stD = a->getEdge(stB).dx; + Geom::Point seD = a->getEdge(i).dx; + Geom::Point enD = a->getEdge(enB).dx; + + double stL = sqrt (dot(stD,stD)); + double seL = sqrt (dot(seD,seD)); + //double enL = sqrt (dot(enD,enD)); + MiscNormalize (stD); + MiscNormalize (enD); + MiscNormalize (seD); + + Geom::Point ptP; + int stNo, enNo; + ptP = a->getPoint(a->getEdge(i).st).x; + + Geom::Point to_center = ptP * (*i2doc) - c; + Geom::Point to_center_normalized = (1/Geom::L2(to_center)) * to_center; + + double this_power; + if (do_profile && i2doc) { + double alpha = 1; + double x; + if (mode == tweak_mode_repel) { + x = (Geom::L2(to_center)/radius); + } else { + x = (Geom::L2(ptP * (*i2doc) - c)/radius); + } + if (x > 1) { + this_power = 0; + } else if (x <= 0) { + if (mode == tweak_mode_repel) { + this_power = 0; + } else { + this_power = power; + } + } else { + this_power = power * (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5); + } + } else { + if (mode == tweak_mode_repel) { + this_power = 0; + } else { + this_power = power; + } + } + + if (this_power != 0) + done_something = true; + + double scaler = 1 / (*i2doc).descrim(); + + Geom::Point this_vec(0,0); + if (mode == tweak_mode_push) { + Geom::Affine tovec (*i2doc); + tovec[4] = tovec[5] = 0; + tovec = tovec.inverse(); + this_vec = this_power * (vector * tovec) ; + } else if (mode == tweak_mode_repel) { + this_vec = this_power * scaler * to_center_normalized; + } else if (mode == tweak_mode_roughen) { + double angle = g_random_double_range(0, 2*M_PI); + this_vec = g_random_double_range(0, 1) * this_power * scaler * Geom::Point(sin(angle), cos(angle)); + } + + int usePathID=-1; + int usePieceID=0; + double useT=0.0; + if ( a->_has_back_data ) { + if ( a->ebData[i].pathID >= 0 && a->ebData[stB].pathID == a->ebData[i].pathID && a->ebData[stB].pieceID == a->ebData[i].pieceID + && a->ebData[stB].tEn == a->ebData[i].tSt ) { + usePathID=a->ebData[i].pathID; + usePieceID=a->ebData[i].pieceID; + useT=a->ebData[i].tSt; + } else { + usePathID=a->ebData[i].pathID; + usePieceID=0; + useT=0; + } + } + + if (mode == tweak_mode_push || mode == tweak_mode_repel || mode == tweak_mode_roughen) { + Path::DoLeftJoin (this, 0, join, ptP+this_vec, stD+this_vec, seD+this_vec, miter, stL, seL, + stNo, enNo,usePathID,usePieceID,useT); + a->swsData[i].stPt = enNo; + a->swsData[stB].enPt = stNo; + } else { + if (power > 0) { + Path::DoRightJoin (this, this_power * scaler, join, ptP, stD, seD, miter, stL, seL, + stNo, enNo,usePathID,usePieceID,useT); + a->swsData[i].stPt = enNo; + a->swsData[stB].enPt = stNo; + } else { + Path::DoLeftJoin (this, -this_power * scaler, join, ptP, stD, seD, miter, stL, seL, + stNo, enNo,usePathID,usePieceID,useT); + a->swsData[i].stPt = enNo; + a->swsData[stB].enPt = stNo; + } + } + } + + if (power < 0 || mode == tweak_mode_push || mode == tweak_mode_repel || mode == tweak_mode_roughen) + { + for (int i = 0; i < numberOfEdges(); i++) + Inverse (i); + } + + if ( _has_back_data ) { + for (int i = 0; i < a->numberOfEdges(); i++) + { + int nEd=AddEdge (a->swsData[i].stPt, a->swsData[i].enPt); + ebData[nEd]=a->ebData[i]; + } + } else { + for (int i = 0; i < a->numberOfEdges(); i++) + { + AddEdge (a->swsData[i].stPt, a->swsData[i].enPt); + } + } + + a->MakeSweepSrcData (false); + a->MakeSweepDestData (false); + + return (done_something? 0 : shape_nothing_to_do); +} + + +// offsets +// take each edge, offset it, and make joins with previous at edge start and next at edge end (previous and +// next being with respect to the clockwise order) +// you gotta be very careful with the join, as anything but the right one will fuck everything up +// see PathStroke.cpp for the "right" joins +int +Shape::MakeOffset (Shape * a, double dec, JoinType join, double miter, bool do_profile, double cx, double cy, double radius, Geom::Affine *i2doc) +{ + Reset (0, 0); + MakeBackData(a->_has_back_data); + + bool done_something = false; + + if (dec == 0) + { + _pts = a->_pts; + if (numberOfPoints() > maxPt) + { + maxPt = numberOfPoints(); + if (_has_points_data) { + pData.resize(maxPt); + _point_data_initialised = false; + _bbox_up_to_date = false; + } + } + + _aretes = a->_aretes; + if (numberOfEdges() > maxAr) + { + maxAr = numberOfEdges(); + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_raster_data) + swrData.resize(maxAr); + if (_has_back_data) + ebData.resize(maxAr); + } + return 0; + } + if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1 || a->type != shape_polygon) + return shape_input_err; + + a->SortEdges (); + + a->MakeSweepDestData (true); + a->MakeSweepSrcData (true); + + for (int i = 0; i < a->numberOfEdges(); i++) + { + // int stP=a->swsData[i].stPt/*,enP=a->swsData[i].enPt*/; + int stB = -1, enB = -1; + if (dec > 0) + { + stB = a->CycleNextAt (a->getEdge(i).st, i); + enB = a->CyclePrevAt (a->getEdge(i).en, i); + } + else + { + stB = a->CyclePrevAt (a->getEdge(i).st, i); + enB = a->CycleNextAt (a->getEdge(i).en, i); + } + + Geom::Point stD = a->getEdge(stB).dx; + Geom::Point seD = a->getEdge(i).dx; + Geom::Point enD = a->getEdge(enB).dx; + + double stL = sqrt (dot(stD,stD)); + double seL = sqrt (dot(seD,seD)); + //double enL = sqrt (dot(enD,enD)); + MiscNormalize (stD); + MiscNormalize (enD); + MiscNormalize (seD); + + Geom::Point ptP; + int stNo, enNo; + ptP = a->getPoint(a->getEdge(i).st).x; + + double this_dec; + if (do_profile && i2doc) { + double alpha = 1; + double x = (Geom::L2(ptP * (*i2doc) - Geom::Point(cx,cy))/radius); + if (x > 1) { + this_dec = 0; + } else if (x <= 0) { + this_dec = dec; + } else { + this_dec = dec * (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5); + } + } else { + this_dec = dec; + } + + if (this_dec != 0) + done_something = true; + + int usePathID=-1; + int usePieceID=0; + double useT=0.0; + if ( a->_has_back_data ) { + if ( a->ebData[i].pathID >= 0 && a->ebData[stB].pathID == a->ebData[i].pathID && a->ebData[stB].pieceID == a->ebData[i].pieceID + && a->ebData[stB].tEn == a->ebData[i].tSt ) { + usePathID=a->ebData[i].pathID; + usePieceID=a->ebData[i].pieceID; + useT=a->ebData[i].tSt; + } else { + usePathID=a->ebData[i].pathID; + usePieceID=0; + useT=0; + } + } + if (dec > 0) + { + Path::DoRightJoin (this, this_dec, join, ptP, stD, seD, miter, stL, seL, + stNo, enNo,usePathID,usePieceID,useT); + a->swsData[i].stPt = enNo; + a->swsData[stB].enPt = stNo; + } + else + { + Path::DoLeftJoin (this, -this_dec, join, ptP, stD, seD, miter, stL, seL, + stNo, enNo,usePathID,usePieceID,useT); + a->swsData[i].stPt = enNo; + a->swsData[stB].enPt = stNo; + } + } + + if (dec < 0) + { + for (int i = 0; i < numberOfEdges(); i++) + Inverse (i); + } + + if ( _has_back_data ) { + for (int i = 0; i < a->numberOfEdges(); i++) + { + int nEd=AddEdge (a->swsData[i].stPt, a->swsData[i].enPt); + ebData[nEd]=a->ebData[i]; + } + } else { + for (int i = 0; i < a->numberOfEdges(); i++) + { + AddEdge (a->swsData[i].stPt, a->swsData[i].enPt); + } + } + + a->MakeSweepSrcData (false); + a->MakeSweepDestData (false); + + return (done_something? 0 : shape_nothing_to_do); +} + + + +// we found a contour, now reassemble the edges on it, instead of dumping them in the Path "dest" as a +// polyline. since it was a DFS, the precParc and suivParc make a nice doubly-linked list of the edges in +// the contour. the first and last edges of the contour are startBord and curBord +void +Shape::AddContour (Path * dest, int nbP, Path * *orig, int startBord, int curBord, bool splitWhenForced) +{ + int bord = startBord; + + { + dest->MoveTo (getPoint(getEdge(bord).st).x); + } + + while (bord >= 0) + { + int nPiece = ebData[bord].pieceID; + int nPath = ebData[bord].pathID; + + if (nPath < 0 || nPath >= nbP || orig[nPath] == nullptr) + { + // segment batard + dest->LineTo (getPoint(getEdge(bord).en).x); + bord = swdData[bord].suivParc; + } + else + { + Path *from = orig[nPath]; + if (nPiece < 0 || nPiece >= int(from->descr_cmd.size())) + { + // segment batard + dest->LineTo (getPoint(getEdge(bord).en).x); + bord = swdData[bord].suivParc; + } + else + { + int nType = from->descr_cmd[nPiece]->getType(); + if (nType == descr_close || nType == descr_moveto + || nType == descr_forced) + { + // devrait pas arriver + dest->LineTo (getPoint(getEdge(bord).en).x); + bord = swdData[bord].suivParc; + } + else if (nType == descr_lineto) + { + bord = ReFormeLineTo (bord, curBord, dest, from); + } + else if (nType == descr_arcto) + { + bord = ReFormeArcTo (bord, curBord, dest, from); + } + else if (nType == descr_cubicto) + { + bord = ReFormeCubicTo (bord, curBord, dest, from); + } + else if (nType == descr_bezierto) + { + PathDescrBezierTo* nBData = + dynamic_cast(from->descr_cmd[nPiece]); + + if (nBData->nb == 0) + { + bord = ReFormeLineTo (bord, curBord, dest, from); + } + else + { + bord = ReFormeBezierTo (bord, curBord, dest, from); + } + } + else if (nType == descr_interm_bezier) + { + bord = ReFormeBezierTo (bord, curBord, dest, from); + } + else + { + // devrait pas arriver non plus + dest->LineTo (getPoint(getEdge(bord).en).x); + bord = swdData[bord].suivParc; + } + if (bord >= 0 && getPoint(getEdge(bord).st).totalDegree() > 2 ) { + dest->ForcePoint (); + } else if ( bord >= 0 && getPoint(getEdge(bord).st).oldDegree > 2 && getPoint(getEdge(bord).st).totalDegree() == 2) { + if ( splitWhenForced ) { + // pour les coupures + dest->ForcePoint (); + } else { + if ( _has_back_data ) { + int prevEdge=getPoint(getEdge(bord).st).incidentEdge[FIRST]; + int nextEdge=getPoint(getEdge(bord).st).incidentEdge[LAST]; + if ( getEdge(prevEdge).en != getEdge(bord).st ) { + int swai=prevEdge;prevEdge=nextEdge;nextEdge=swai; + } + if ( ebData[prevEdge].pieceID == ebData[nextEdge].pieceID && ebData[prevEdge].pathID == ebData[nextEdge].pathID ) { + if ( fabs(ebData[prevEdge].tEn-ebData[nextEdge].tSt) < 0.05 ) { + } else { + dest->ForcePoint (); + } + } else { + dest->ForcePoint (); + } + } else { + dest->ForcePoint (); + } + } + } + } + } + } + dest->Close (); +} + +int +Shape::ReFormeLineTo (int bord, int /*curBord*/, Path * dest, Path * /*orig*/) +{ + int nPiece = ebData[bord].pieceID; + int nPath = ebData[bord].pathID; + double /*ts=ebData[bord].tSt, */ te = ebData[bord].tEn; + Geom::Point nx = getPoint(getEdge(bord).en).x; + bord = swdData[bord].suivParc; + while (bord >= 0) + { + if (getPoint(getEdge(bord).st).totalDegree() > 2 + || getPoint(getEdge(bord).st).oldDegree > 2) + { + break; + } + if (ebData[bord].pieceID == nPiece && ebData[bord].pathID == nPath) + { + if (fabs (te - ebData[bord].tSt) > 0.0001) + break; + nx = getPoint(getEdge(bord).en).x; + te = ebData[bord].tEn; + } + else + { + break; + } + bord = swdData[bord].suivParc; + } + { + dest->LineTo (nx); + } + return bord; +} + +int +Shape::ReFormeArcTo (int bord, int /*curBord*/, Path * dest, Path * from) +{ + int nPiece = ebData[bord].pieceID; + int nPath = ebData[bord].pathID; + double ts = ebData[bord].tSt, te = ebData[bord].tEn; + // double px=pts[getEdge(bord).st].x,py=pts[getEdge(bord).st].y; + Geom::Point nx = getPoint(getEdge(bord).en).x; + bord = swdData[bord].suivParc; + while (bord >= 0) + { + if (getPoint(getEdge(bord).st).totalDegree() > 2 + || getPoint(getEdge(bord).st).oldDegree > 2) + { + break; + } + if (ebData[bord].pieceID == nPiece && ebData[bord].pathID == nPath) + { + if (fabs (te - ebData[bord].tSt) > 0.0001) + { + break; + } + nx = getPoint(getEdge(bord).en).x; + te = ebData[bord].tEn; + } + else + { + break; + } + bord = swdData[bord].suivParc; + } + double sang, eang; + PathDescrArcTo* nData = dynamic_cast(from->descr_cmd[nPiece]); + bool nLarge = nData->large; + bool nClockwise = nData->clockwise; + Path::ArcAngles (from->PrevPoint (nPiece - 1), nData->p,nData->rx,nData->ry,nData->angle*M_PI/180.0, nLarge, nClockwise, sang, eang); + if (nClockwise) + { + if (sang < eang) + sang += 2 * M_PI; + } + else + { + if (sang > eang) + sang -= 2 * M_PI; + } + double delta = eang - sang; + double ndelta = delta * (te - ts); + if (ts > te) + nClockwise = !nClockwise; + if (ndelta < 0) + ndelta = -ndelta; + if (ndelta > M_PI) + nLarge = true; + else + nLarge = false; + /* if ( delta < 0 ) delta=-delta; + if ( ndelta < 0 ) ndelta=-ndelta; + if ( ( delta < M_PI && ndelta < M_PI ) || ( delta >= M_PI && ndelta >= M_PI ) ) { + if ( ts < te ) { + } else { + nClockwise=!(nClockwise); + } + } else { + // nLarge=!(nLarge); + nLarge=false; // c'est un sous-segment -> l'arc ne peut que etre plus petit + if ( ts < te ) { + } else { + nClockwise=!(nClockwise); + } + }*/ + { + PathDescrArcTo *nData = dynamic_cast(from->descr_cmd[nPiece]); + dest->ArcTo (nx, nData->rx,nData->ry,nData->angle, nLarge, nClockwise); + } + return bord; +} + +int +Shape::ReFormeCubicTo (int bord, int /*curBord*/, Path * dest, Path * from) +{ + int nPiece = ebData[bord].pieceID; + int nPath = ebData[bord].pathID; + double ts = ebData[bord].tSt, te = ebData[bord].tEn; + Geom::Point nx = getPoint(getEdge(bord).en).x; + bord = swdData[bord].suivParc; + while (bord >= 0) + { + if (getPoint(getEdge(bord).st).totalDegree() > 2 + || getPoint(getEdge(bord).st).oldDegree > 2) + { + break; + } + if (ebData[bord].pieceID == nPiece && ebData[bord].pathID == nPath) + { + if (fabs (te - ebData[bord].tSt) > 0.0001) + { + break; + } + nx = getPoint(getEdge(bord).en).x; + te = ebData[bord].tEn; + } + else + { + break; + } + bord = swdData[bord].suivParc; + } + Geom::Point prevx = from->PrevPoint (nPiece - 1); + + Geom::Point sDx, eDx; + { + PathDescrCubicTo *nData = dynamic_cast(from->descr_cmd[nPiece]); + Path::CubicTangent (ts, sDx, prevx,nData->start,nData->p,nData->end); + Path::CubicTangent (te, eDx, prevx,nData->start,nData->p,nData->end); + } + sDx *= (te - ts); + eDx *= (te - ts); + { + dest->CubicTo (nx,sDx,eDx); + } + return bord; +} + +int +Shape::ReFormeBezierTo (int bord, int /*curBord*/, Path * dest, Path * from) +{ + int nPiece = ebData[bord].pieceID; + int nPath = ebData[bord].pathID; + double ts = ebData[bord].tSt, te = ebData[bord].tEn; + int ps = nPiece, pe = nPiece; + Geom::Point px = getPoint(getEdge(bord).st).x; + Geom::Point nx = getPoint(getEdge(bord).en).x; + int inBezier = -1, nbInterm = -1; + int typ; + typ = from->descr_cmd[nPiece]->getType(); + PathDescrBezierTo *nBData = nullptr; + if (typ == descr_bezierto) + { + nBData = dynamic_cast(from->descr_cmd[nPiece]); + inBezier = nPiece; + nbInterm = nBData->nb; + } + else + { + int n = nPiece - 1; + while (n > 0) + { + typ = from->descr_cmd[n]->getType(); + if (typ == descr_bezierto) + { + inBezier = n; + nBData = dynamic_cast(from->descr_cmd[n]); + nbInterm = nBData->nb; + break; + } + n--; + } + if (inBezier < 0) + { + bord = swdData[bord].suivParc; + dest->LineTo (nx); + return bord; + } + } + bord = swdData[bord].suivParc; + while (bord >= 0) + { + if (getPoint(getEdge(bord).st).totalDegree() > 2 + || getPoint(getEdge(bord).st).oldDegree > 2) + { + break; + } + if (ebData[bord].pathID == nPath) + { + if (ebData[bord].pieceID < inBezier + || ebData[bord].pieceID >= inBezier + nbInterm) + break; + if (ebData[bord].pieceID == pe + && fabs (te - ebData[bord].tSt) > 0.0001) + break; + if (ebData[bord].pieceID != pe + && (ebData[bord].tSt > 0.0001 && ebData[bord].tSt < 0.9999)) + break; + if (ebData[bord].pieceID != pe && (te > 0.0001 && te < 0.9999)) + break; + nx = getPoint(getEdge(bord).en).x; + te = ebData[bord].tEn; + pe = ebData[bord].pieceID; + } + else + { + break; + } + bord = swdData[bord].suivParc; + } + + g_return_val_if_fail(nBData != nullptr, 0); + + if (pe == ps) + { + ReFormeBezierChunk (px, nx, dest, inBezier, nbInterm, from, ps, + ts, te); + } + else if (ps < pe) + { + if (ts < 0.0001) + { + if (te > 0.9999) + { + dest->BezierTo (nx); + for (int i = ps; i <= pe; i++) + { + PathDescrIntermBezierTo *nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + } + else + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[pe]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[pe+1]); + tx = (pnData->p + psData->p) / 2; + } + dest->BezierTo (tx); + for (int i = ps; i < pe; i++) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + ReFormeBezierChunk (tx, nx, dest, inBezier, nbInterm, + from, pe, 0.0, te); + } + } + else + { + if (te > 0.9999) + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[ps+1]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[ps+2]); + tx = (psData->p + pnData->p) / 2; + } + ReFormeBezierChunk (px, tx, dest, inBezier, nbInterm, + from, ps, ts, 1.0); + dest->BezierTo (nx); + for (int i = ps + 1; i <= pe; i++) + { + PathDescrIntermBezierTo *nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + } + else + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[ps+1]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[ps+2]); + tx = (pnData->p + psData->p) / 2; + } + ReFormeBezierChunk (px, tx, dest, inBezier, nbInterm, + from, ps, ts, 1.0); + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[pe]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[pe+1]); + tx = (pnData->p + psData->p) / 2; + } + dest->BezierTo (tx); + for (int i = ps + 1; i <= pe; i++) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + ReFormeBezierChunk (tx, nx, dest, inBezier, nbInterm, + from, pe, 0.0, te); + } + } + } + else + { + if (ts > 0.9999) + { + if (te < 0.0001) + { + dest->BezierTo (nx); + for (int i = ps; i >= pe; i--) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + } + else + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[pe+1]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[pe+2]); + tx = (pnData->p + psData->p) / 2; + } + dest->BezierTo (tx); + for (int i = ps; i > pe; i--) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i+1]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + ReFormeBezierChunk (tx, nx, dest, inBezier, nbInterm, + from, pe, 1.0, te); + } + } + else + { + if (te < 0.0001) + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[ps]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[ps+1]); + tx = (pnData->p + psData->p) / 2; + } + ReFormeBezierChunk (px, tx, dest, inBezier, nbInterm, + from, ps, ts, 0.0); + dest->BezierTo (nx); + for (int i = ps + 1; i >= pe; i--) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + } + else + { + Geom::Point tx; + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[ps]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[ps+1]); + tx = (pnData->p + psData->p) / 2; + } + ReFormeBezierChunk (px, tx, dest, inBezier, nbInterm, + from, ps, ts, 0.0); + { + PathDescrIntermBezierTo* psData = dynamic_cast(from->descr_cmd[pe+1]); + PathDescrIntermBezierTo* pnData = dynamic_cast(from->descr_cmd[pe+2]); + tx = (pnData->p + psData->p) / 2; + } + dest->BezierTo (tx); + for (int i = ps + 1; i > pe; i--) + { + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[i]); + dest->IntermBezierTo (nData->p); + } + dest->EndBezierTo (); + ReFormeBezierChunk (tx, nx, dest, inBezier, nbInterm, + from, pe, 1.0, te); + } + } + } + return bord; +} + +void +Shape::ReFormeBezierChunk (Geom::Point px, Geom::Point nx, + Path * dest, int inBezier, int nbInterm, + Path * from, int p, double ts, double te) +{ + PathDescrBezierTo* nBData = dynamic_cast(from->descr_cmd[inBezier]); + Geom::Point bstx = from->PrevPoint (inBezier - 1); + Geom::Point benx = nBData->p; + + Geom::Point mx; + if (p == inBezier) + { + // premier bout + if (nbInterm <= 1) + { + // seul bout de la spline + PathDescrIntermBezierTo *nData = dynamic_cast(from->descr_cmd[inBezier+1]); + mx = nData->p; + } + else + { + // premier bout d'une spline qui en contient plusieurs + PathDescrIntermBezierTo *nData = dynamic_cast(from->descr_cmd[inBezier+1]); + mx = nData->p; + nData = dynamic_cast(from->descr_cmd[inBezier+2]); + benx = (nData->p + mx) / 2; + } + } + else if (p == inBezier + nbInterm - 1) + { + // dernier bout + // si nbInterm == 1, le cas a deja ete traite + // donc dernier bout d'une spline qui en contient plusieurs + PathDescrIntermBezierTo* nData = dynamic_cast(from->descr_cmd[inBezier+nbInterm]); + mx = nData->p; + nData = dynamic_cast(from->descr_cmd[inBezier+nbInterm-1]); + bstx = (nData->p + mx) / 2; + } + else + { + // la spline contient forcément plusieurs bouts, et ce n'est ni le premier ni le dernier + PathDescrIntermBezierTo *nData = dynamic_cast(from->descr_cmd[p+1]); + mx = nData->p; + nData = dynamic_cast(from->descr_cmd[p]); + bstx = (nData->p + mx) / 2; + nData = dynamic_cast(from->descr_cmd[p+2]); + benx = (nData->p + mx) / 2; + } + Geom::Point cx; + { + Path::QuadraticPoint ((ts + te) / 2, cx, bstx, mx, benx); + } + cx = 2 * cx - (px + nx) / 2; + { + dest->BezierTo (nx); + dest->IntermBezierTo (cx); + dest->EndBezierTo (); + } +} + +#undef MiscNormalize diff --git a/src/livarot/ShapeRaster.cpp b/src/livarot/ShapeRaster.cpp new file mode 100644 index 0000000..7aa300e --- /dev/null +++ b/src/livarot/ShapeRaster.cpp @@ -0,0 +1,2014 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * ShapeRaster.cpp + * nlivarot + * + * Created by fred on Sat Jul 19 2003. + * + */ + +#include "Shape.h" + +#include "livarot/float-line.h" +#include "AlphaLigne.h" +#include "BitLigne.h" + +#include "livarot/sweep-event-queue.h" +#include "livarot/sweep-tree-list.h" +#include "livarot/sweep-tree.h" + +/* + * polygon rasterization: the sweepline algorithm in all its glory + * nothing unusual in this implementation, so nothing special to say + * the *Quick*() functions are not useful. forget about them + */ + +void Shape::BeginRaster(float &pos, int &curPt) +{ + if ( numberOfPoints() <= 1 || numberOfEdges() <= 1 ) { + curPt = 0; + pos = 0; + return; + } + + MakeRasterData(true); + MakePointData(true); + MakeEdgeData(true); + + if (sTree == nullptr) { + sTree = new SweepTreeList(numberOfEdges()); + } + if (sEvts == nullptr) { + sEvts = new SweepEventQueue(numberOfEdges()); + } + + SortPoints(); + + curPt = 0; + pos = getPoint(0).x[1] - 1.0; + + for (int i = 0; i < numberOfPoints(); i++) { + pData[i].pending = 0; + pData[i].edgeOnLeft = -1; + pData[i].nextLinkedPoint = -1; + pData[i].rx[0] = /*Round(*/getPoint(i).x[0]/*)*/; + pData[i].rx[1] = /*Round(*/getPoint(i).x[1]/*)*/; + } + + for (int i = 0;i < numberOfEdges(); i++) { + swrData[i].misc = nullptr; + eData[i].rdx=pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + } +} + + +void Shape::EndRaster() +{ + delete sTree; + sTree = nullptr; + delete sEvts; + sEvts = nullptr; + + MakePointData(false); + MakeEdgeData(false); + MakeRasterData(false); +} + + +void Shape::BeginQuickRaster(float &pos, int &curPt) +{ + if ( numberOfPoints() <= 1 || numberOfEdges() <= 1 ) { + curPt = 0; + pos = 0; + return; + } + + MakeRasterData(true); + MakeQuickRasterData(true); + nbQRas = 0; + firstQRas = lastQRas = -1; + MakePointData(true); + MakeEdgeData(true); + + curPt = 0; + pos = getPoint(0).x[1] - 1.0; + + initialisePointData(); + + for (int i=0;i 0 && getPoint(curPt - 1).x[1] >= to) ) + { + int nPt = (d == DOWNWARDS) ? curPt++ : --curPt; + + // treat a new point: remove and add edges incident to it + int nbUp; + int nbDn; + int upNo; + int dnNo; + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + + if ( d == DOWNWARDS ) { + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + } else { + if ( nbUp <= 0 ) { + dnNo = -1; + } + if ( dnNo >= 0 && swrData[dnNo].misc == nullptr ) { + dnNo = -1; + } + } + + if ( ( d == DOWNWARDS && nbUp > 0 ) || ( d == UPWARDS && nbDn > 0 ) ) { + // first remove edges coming from above or below, as appropriate + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + + Shape::dg_arete const &e = getEdge(cb); + if ( (d == DOWNWARDS && nPt == std::max(e.st, e.en)) || + (d == UPWARDS && nPt == std::min(e.st, e.en)) ) + { + if ( ( d == DOWNWARDS && cb != upNo ) || ( d == UPWARDS && cb != dnNo ) ) { + // we salvage the edge upNo to plug the edges we'll be addingat its place + // but the other edge don't have this chance + SweepTree *node = swrData[cb].misc; + if ( node ) { + swrData[cb].misc = nullptr; + node->Remove(*sTree, *sEvts, true); + } + } + } + cb = NextAt(nPt, cb); + } + } + + // if there is one edge going down and one edge coming from above, we don't Insert() the new edge, + // but replace the upNo edge by the new one (faster) + SweepTree* insertionNode = nullptr; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + int rmNo=(d == DOWNWARDS) ? upNo:dnNo; + int neNo=(d == DOWNWARDS) ? dnNo:upNo; + SweepTree* node = swrData[rmNo].misc; + swrData[rmNo].misc = nullptr; + + int const P = (d == DOWNWARDS) ? nPt : Other(nPt, neNo); + node->ConvertTo(this, neNo, 1, P); + + swrData[neNo].misc = node; + insertionNode = node; + CreateEdge(neNo, to, step); + } else { + // always DOWNWARDS + SweepTree* node = sTree->add(this, dnNo, 1, nPt, this); + swrData[dnNo].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + //if (d == UPWARDS) { + // node->startPoint = Other(nPt, dnNo); + //} + insertionNode = node; + CreateEdge(dnNo,to,step); + } + } else { + if ( upNo >= 0 ) { + // always UPWARDS + SweepTree* node = sTree->add(this, upNo, 1, nPt, this); + swrData[upNo].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + //if (d == UPWARDS) { + node->startPoint = Other(nPt, upNo); + //} + insertionNode = node; + CreateEdge(upNo,to,step); + } + } + + // add the remaining edges + if ( ( d == DOWNWARDS && nbDn > 1 ) || ( d == UPWARDS && nbUp > 1 ) ) { + // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo && cb != upNo ) { + SweepTree *node = sTree->add(this, cb, 1, nPt, this); + swrData[cb].misc = node; + node->InsertAt(*sTree, *sEvts, this, insertionNode, nPt, true); + if (d == UPWARDS) { + node->startPoint = Other(nPt, cb); + } + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + // the final touch: edges intersecting the sweepline must be update so that their intersection with + // said sweepline is correct. + pos = to; + if ( sTree->racine ) { + SweepTree* curS = static_cast(sTree->racine->Leftmost()); + while ( curS ) { + int cb = curS->bord; + AvanceEdge(cb, to, true, step); + curS = static_cast(curS->elem[RIGHT]); + } + } +} + + + +void Shape::QuickScan(float &pos,int &curP, float to, bool /*doSort*/, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos == to ) { + return; + } + + enum Direction { + DOWNWARDS, + UPWARDS + }; + + Direction const d = (pos < to) ? DOWNWARDS : UPWARDS; + + int curPt = curP; + while ( (d == DOWNWARDS && curPt < numberOfPoints() && getPoint(curPt ).x[1] <= to) || + (d == UPWARDS && curPt > 0 && getPoint(curPt - 1).x[1] >= to) ) + { + int nPt = (d == DOWNWARDS) ? curPt++ : --curPt; + + int nbUp; + int nbDn; + int upNo; + int dnNo; + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 0 ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( (d == DOWNWARDS && nPt == std::max(e.st, e.en)) || + (d == UPWARDS && nPt == std::min(e.st, e.en)) ) + { + if ( cb != upNo ) { + QuickRasterSubEdge(cb); + } + } + cb = NextAt(nPt,cb); + } + } + + // traitement du "upNo devient dnNo" + int ins_guess = -1; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + ins_guess = QuickRasterChgEdge(upNo, dnNo, getPoint(nPt).x[0]); + } else { + ins_guess = QuickRasterAddEdge(dnNo, getPoint(nPt).x[0], ins_guess); + } + CreateEdge(dnNo, to, step); + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( (d == DOWNWARDS && nPt == std::min(e.st, e.en)) || + (d == UPWARDS && nPt == std::max(e.st, e.en)) ) + { + if ( cb != dnNo ) { + ins_guess = QuickRasterAddEdge(cb, getPoint(nPt).x[0], ins_guess); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt-1).x[1]; + } else { + pos = to; + } + } + + pos = to; + + for (int i=0; i < nbQRas; i++) { + int cb = qrsData[i].bord; + AvanceEdge(cb, to, true, step); + qrsData[i].x=swrData[cb].curX; + } + + QuickRasterSort(); +} + + + +int Shape::QuickRasterChgEdge(int oBord, int nBord, double x) +{ + if ( oBord == nBord ) { + return -1; + } + + int no = qrsData[oBord].ind; + if ( no >= 0 ) { + qrsData[no].bord = nBord; + qrsData[no].x = x; + qrsData[oBord].ind = -1; + qrsData[nBord].ind = no; + } + + return no; +} + + + +int Shape::QuickRasterAddEdge(int bord, double x, int guess) +{ + int no = nbQRas++; + qrsData[no].bord = bord; + qrsData[no].x = x; + qrsData[bord].ind = no; + qrsData[no].prev = -1; + qrsData[no].next = -1; + + if ( no < 0 || no >= nbQRas ) { + return -1; + } + + if ( firstQRas < 0 ) { + firstQRas = lastQRas = no; + qrsData[no].prev = -1; + qrsData[no].next = -1; + return no; + } + + if ( guess < 0 || guess >= nbQRas ) { + + int c = firstQRas; + while ( c >= 0 && c < nbQRas && CmpQRs(qrsData[c],qrsData[no]) < 0 ) { + c = qrsData[c].next; + } + + if ( c < 0 || c >= nbQRas ) { + qrsData[no].prev = lastQRas; + qrsData[lastQRas].next = no; + lastQRas = no; + } else { + qrsData[no].prev = qrsData[c].prev; + if ( qrsData[no].prev >= 0 ) { + qrsData[qrsData[no].prev].next=no; + } else { + firstQRas = no; + } + + qrsData[no].next = c; + qrsData[c].prev = no; + } + + } else { + int c = guess; + int stTst = CmpQRs(qrsData[c],qrsData[no]); + if ( stTst == 0 ) { + + qrsData[no].prev = qrsData[c].prev; + if ( qrsData[no].prev >= 0 ) { + qrsData[qrsData[no].prev].next = no; + } else { + firstQRas = no; + } + + qrsData[no].next = c; + qrsData[c].prev = no; + + } else if ( stTst > 0 ) { + + while ( c >= 0 && c < nbQRas && CmpQRs(qrsData[c],qrsData[no]) > 0 ) { + c = qrsData[c].prev; + } + + if ( c < 0 || c >= nbQRas ) { + qrsData[no].next = firstQRas; + qrsData[firstQRas].prev = no; // firstQRas != -1 + firstQRas = no; + } else { + qrsData[no].next = qrsData[c].next; + if ( qrsData[no].next >= 0 ) { + qrsData[qrsData[no].next].prev = no; + } else { + lastQRas = no; + } + qrsData[no].prev = c; + qrsData[c].next = no; + } + + } else { + + while ( c >= 0 && c < nbQRas && CmpQRs(qrsData[c],qrsData[no]) < 0 ) { + c = qrsData[c].next; + } + + if ( c < 0 || c >= nbQRas ) { + qrsData[no].prev = lastQRas; + qrsData[lastQRas].next = no; + lastQRas = no; + } else { + qrsData[no].prev = qrsData[c].prev; + if ( qrsData[no].prev >= 0 ) { + qrsData[qrsData[no].prev].next = no; + } else { + firstQRas = no; + } + + qrsData[no].next = c; + qrsData[c].prev = no; + } + } + } + + return no; +} + + + +void Shape::QuickRasterSubEdge(int bord) +{ + int no = qrsData[bord].ind; + if ( no < 0 || no >= nbQRas ) { + return; // euuhHHH + } + + if ( qrsData[no].prev >= 0 ) { + qrsData[qrsData[no].prev].next=qrsData[no].next; + } + + if ( qrsData[no].next >= 0 ) { + qrsData[qrsData[no].next].prev = qrsData[no].prev; + } + + if ( no == firstQRas ) { + firstQRas = qrsData[no].next; + } + + if ( no == lastQRas ) { + lastQRas = qrsData[no].prev; + } + + qrsData[no].prev = qrsData[no].next = -1; + + int savInd = qrsData[no].ind; + qrsData[no] = qrsData[--nbQRas]; + qrsData[no].ind = savInd; + qrsData[qrsData[no].bord].ind = no; + qrsData[bord].ind = -1; + + if ( nbQRas > 0 ) { + if ( firstQRas == nbQRas ) { + firstQRas = no; + } + if ( lastQRas == nbQRas ) { + lastQRas = no; + } + if ( qrsData[no].prev >= 0 ) { + qrsData[qrsData[no].prev].next = no; + } + if ( qrsData[no].next >= 0 ) { + qrsData[qrsData[no].next].prev = no; + } + } +} + + + +void Shape::QuickRasterSwapEdge(int a, int b) +{ + if ( a == b ) { + return; + } + + int na = qrsData[a].ind; + int nb = qrsData[b].ind; + if ( na < 0 || na >= nbQRas || nb < 0 || nb >= nbQRas ) { + return; // errrm + } + + qrsData[na].bord = b; + qrsData[nb].bord = a; + qrsData[a].ind = nb; + qrsData[b].ind = na; + + double swd = qrsData[na].x; + qrsData[na].x = qrsData[nb].x; + qrsData[nb].x = swd; +} + + +void Shape::QuickRasterSort() +{ + if ( nbQRas <= 1 ) { + return; + } + + int cb = qrsData[firstQRas].bord; + + while ( cb >= 0 ) { + int bI = qrsData[cb].ind; + int nI = qrsData[bI].next; + + if ( nI < 0 ) { + break; + } + + int ncb = qrsData[nI].bord; + if ( CmpQRs(qrsData[nI], qrsData[bI]) < 0 ) { + QuickRasterSwapEdge(cb, ncb); + int pI = qrsData[bI].prev; // ca reste bI, puisqu'on a juste echange les contenus + if ( pI < 0 ) { + cb = ncb; // en fait inutile; mais bon... + } else { + int pcb = qrsData[pI].bord; + cb = pcb; + } + } else { + cb = ncb; + } + } +} + + +// direct scan to a given position. goes through the edge list to keep only the ones intersecting the target sweepline +// good for initial setup of scanline algo, bad for incremental changes +void Shape::DirectScan(float &pos, int &curP, float to, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos == to ) { + return; + } + + if ( pos < to ) { + // we're moving downwards + // points of the polygon are sorted top-down, so we take them in order, starting with the one at index curP, + // until we reach the wanted position to. + // don't forget to update curP and pos when we're done + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + curPt++; + } + + for (int i=0;iRemove(*sTree, *sEvts, true); + } + } + + for (int i=0; i < numberOfEdges(); i++) { + Shape::dg_arete const &e = getEdge(i); + if ( ( e.st < curPt && e.en >= curPt ) || ( e.en < curPt && e.st >= curPt )) { + // crosses sweepline + int nPt = (e.st < curPt) ? e.st : e.en; + SweepTree* node = sTree->add(this, i, 1, nPt, this); + swrData[i].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + CreateEdge(i, to, step); + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + } else { + + // same thing, but going up. so the sweepSens is inverted for the Find() function + int curPt=curP; + while ( curPt > 0 && getPoint(curPt-1).x[1] >= to ) { + curPt--; + } + + for (int i = 0; i < numberOfEdges(); i++) { + if ( swrData[i].misc ) { + SweepTree* node = swrData[i].misc; + swrData[i].misc = nullptr; + node->Remove(*sTree, *sEvts, true); + } + } + + for (int i=0;i curPt - 1 && e.en <= curPt - 1 ) || ( e.en > curPt - 1 && e.st <= curPt - 1 )) { + // crosses sweepline + int nPt = (e.st > curPt) ? e.st : e.en; + SweepTree* node = sTree->add(this, i, 1, nPt, this); + swrData[i].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, false); + node->startPoint = Other(nPt, i); + CreateEdge(i, to, step); + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + } + + // the final touch: edges intersecting the sweepline must be update so that their intersection with + // said sweepline is correct. + pos = to; + if ( sTree->racine ) { + SweepTree* curS=static_cast(sTree->racine->Leftmost()); + while ( curS ) { + int cb = curS->bord; + AvanceEdge(cb, to, true, step); + curS = static_cast(curS->elem[RIGHT]); + } + } +} + + + +void Shape::DirectQuickScan(float &pos, int &curP, float to, bool /*doSort*/, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos == to ) { + return; + } + + if ( pos < to ) { + // we're moving downwards + // points of the polygon are sorted top-down, so we take them in order, starting with the one at index curP, + // until we reach the wanted position to. + // don't forget to update curP and pos when we're done + int curPt=curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + curPt++; + } + + for (int i = 0; i < numberOfEdges(); i++) { + if ( qrsData[i].ind < 0 ) { + QuickRasterSubEdge(i); + } + } + + for (int i = 0; i < numberOfEdges(); i++) { + Shape::dg_arete const &e = getEdge(i); + if ( ( e.st < curPt && e.en >= curPt ) || ( e.en < curPt && e.st >= curPt )) { + // crosses sweepline + int nPt = (e.st < e.en) ? e.st : e.en; + QuickRasterAddEdge(i, getPoint(nPt).x[0], -1); + CreateEdge(i, to, step); + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos=getPoint(curPt-1).x[1]; + } else { + pos = to; + } + + } else { + + // same thing, but going up. so the sweepSens is inverted for the Find() function + int curPt=curP; + while ( curPt > 0 && getPoint(curPt-1).x[1] >= to ) { + curPt--; + } + + for (int i = 0; i < numberOfEdges(); i++) { + if ( qrsData[i].ind < 0 ) { + QuickRasterSubEdge(i); + } + } + + for (int i=0;i= curPt-1 ) || ( e.en < curPt-1 && e.st >= curPt-1 )) { + // crosses sweepline + int nPt = (e.st > e.en) ? e.st : e.en; + QuickRasterAddEdge(i, getPoint(nPt).x[0], -1); + CreateEdge(i, to, step); + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt-1).x[1]; + } else { + pos = to; + } + + } + + pos = to; + for (int i = 0; i < nbQRas; i++) { + int cb = qrsData[i].bord; + AvanceEdge(cb, to, true, step); + qrsData[i].x = swrData[cb].curX; + } + + QuickRasterSort(); +} + + +// scan and compute coverage, FloatLigne version coverage of the line is bult in 2 parts: first a +// set of rectangles of height the height of the line (here: "step") one rectangle for each portion +// of the sweepline that is in the polygon at the beginning of the scan. then a set ot trapezoids +// are added or removed to these rectangles, one trapezoid for each edge destroyed or edge crossing +// the entire line. think of it as a refinement of the coverage by rectangles + +void Shape::Scan(float &pos, int &curP, float to, FloatLigne *line, bool exact, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos >= to ) { + return; + } + + // first step: the rectangles since we read the sweepline left to right, we know the + // boundaries of the rectangles are appended in a list, hence the AppendBord(). we salvage + // the guess value for the trapezoids the edges will induce + + if ( sTree->racine ) { + SweepTree *curS = static_cast(sTree->racine->Leftmost()); + while ( curS ) { + + int lastGuess = -1; + int cb = curS->bord; + + if ( swrData[cb].sens == false && curS->elem[LEFT] ) { + + int lb = (static_cast(curS->elem[LEFT]))->bord; + + lastGuess = line->AppendBord(swrData[lb].curX, + to - swrData[lb].curY, + swrData[cb].curX, + to - swrData[cb].curY,0.0); + + swrData[lb].guess = lastGuess - 1; + swrData[cb].guess = lastGuess; + } else { + int lb = curS->bord; + swrData[lb].guess = -1; + } + + curS=static_cast (curS->elem[RIGHT]); + } + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + + int nPt = curPt++; + + // same thing as the usual Scan(), just with a hardcoded "indegree+outdegree=2" case, since + // it's the most common one + + int nbUp; + int nbDn; + int upNo; + int dnNo; + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + SweepTree* node = swrData[cb].misc; + if ( node ) { + _updateIntersection(cb, nPt); + // create trapezoid for the chunk of edge intersecting with the line + DestroyEdge(cb, to, line); + node->Remove(*sTree, *sEvts, true); + } + } + } + + cb = NextAt(nPt,cb); + } + } + + // traitement du "upNo devient dnNo" + SweepTree *insertionNode = nullptr; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + SweepTree* node = swrData[upNo].misc; + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, to, line); + + node->ConvertTo(this, dnNo, 1, nPt); + + swrData[dnNo].misc = node; + insertionNode = node; + CreateEdge(dnNo, to, step); + swrData[dnNo].guess = swrData[upNo].guess; + } else { + SweepTree *node = sTree->add(this, dnNo, 1, nPt, this); + swrData[dnNo].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + insertionNode = node; + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + SweepTree *node = sTree->add(this, cb, 1, nPt, this); + swrData[cb].misc = node; + node->InsertAt(*sTree, *sEvts, this, insertionNode, nPt, true); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + // update intersections with the sweepline, and add trapezoids for edges crossing the line + pos = to; + if ( sTree->racine ) { + SweepTree* curS = static_cast(sTree->racine->Leftmost()); + while ( curS ) { + int cb = curS->bord; + AvanceEdge(cb, to, line, exact, step); + curS = static_cast(curS->elem[RIGHT]); + } + } +} + + + + +void Shape::Scan(float &pos, int &curP, float to, FillRule directed, BitLigne *line, bool exact, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos >= to ) { + return; + } + + if ( sTree->racine ) { + int curW = 0; + float lastX = 0; + SweepTree* curS = static_cast(sTree->racine->Leftmost()); + + if ( directed == fill_oddEven ) { + + while ( curS ) { + int cb = curS->bord; + curW++; + curW &= 0x00000001; + if ( curW == 0 ) { + line->AddBord(lastX,swrData[cb].curX,true); + } else { + lastX = swrData[cb].curX; + } + curS = static_cast(curS->elem[RIGHT]); + } + + } else if ( directed == fill_positive ) { + + // doesn't behave correctly; no way i know to do this without a ConvertToShape() + while ( curS ) { + int cb = curS->bord; + int oW = curW; + if ( swrData[cb].sens ) { + curW++; + } else { + curW--; + } + + if ( curW <= 0 && oW > 0) { + line->AddBord(lastX, swrData[cb].curX, true); + } else if ( curW > 0 && oW <= 0 ) { + lastX = swrData[cb].curX; + } + + curS = static_cast(curS->elem[RIGHT]); + } + + } else if ( directed == fill_nonZero ) { + + while ( curS ) { + int cb = curS->bord; + int oW = curW; + if ( swrData[cb].sens ) { + curW++; + } else { + curW--; + } + + if ( curW == 0 && oW != 0) { + line->AddBord(lastX,swrData[cb].curX,true); + } else if ( curW != 0 && oW == 0 ) { + lastX=swrData[cb].curX; + } + curS = static_cast(curS->elem[RIGHT]); + } + } + + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + int nPt = curPt++; + + int cb; + int nbUp; + int nbDn; + int upNo; + int dnNo; + + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + SweepTree* node=swrData[cb].misc; + if ( node ) { + _updateIntersection(cb, nPt); + DestroyEdge(cb, line); + node->Remove(*sTree,*sEvts,true); + } + } + } + cb = NextAt(nPt,cb); + } + } + + // traitement du "upNo devient dnNo" + SweepTree* insertionNode = nullptr; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + SweepTree* node = swrData[upNo].misc; + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, line); + + node->ConvertTo(this, dnNo, 1, nPt); + + swrData[dnNo].misc = node; + insertionNode = node; + CreateEdge(dnNo, to, step); + + } else { + + SweepTree* node = sTree->add(this,dnNo,1,nPt,this); + swrData[dnNo].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + insertionNode = node; + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + SweepTree* node = sTree->add(this, cb, 1, nPt, this); + swrData[cb].misc = node; + node->InsertAt(*sTree, *sEvts, this, insertionNode, nPt, true); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt, cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + pos = to; + if ( sTree->racine ) { + SweepTree* curS = static_cast(sTree->racine->Leftmost()); + while ( curS ) { + int cb = curS->bord; + AvanceEdge(cb, to, line, exact, step); + curS = static_cast(curS->elem[RIGHT]); + } + } +} + + +void Shape::Scan(float &pos, int &curP, float to, AlphaLigne *line, bool exact, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos >= to ) { + return; + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + int nPt = curPt++; + + int nbUp; + int nbDn; + int upNo; + int dnNo; + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo=-1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo=-1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + SweepTree* node = swrData[cb].misc; + if ( node ) { + _updateIntersection(cb, nPt); + DestroyEdge(cb, line); + node->Remove(*sTree, *sEvts, true); + } + } + } + + cb = NextAt(nPt,cb); + } + } + + // traitement du "upNo devient dnNo" + SweepTree* insertionNode = nullptr; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + SweepTree* node = swrData[upNo].misc; + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, line); + + node->ConvertTo(this, dnNo, 1, nPt); + + swrData[dnNo].misc = node; + insertionNode = node; + CreateEdge(dnNo, to, step); + swrData[dnNo].guess = swrData[upNo].guess; + } else { + SweepTree* node = sTree->add(this, dnNo, 1, nPt, this); + swrData[dnNo].misc = node; + node->Insert(*sTree, *sEvts, this, nPt, true); + insertionNode = node; + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + SweepTree* node = sTree->add(this, cb, 1, nPt, this); + swrData[cb].misc = node; + node->InsertAt(*sTree, *sEvts, this, insertionNode, nPt, true); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + pos = to; + if ( sTree->racine ) { + SweepTree* curS = static_cast(sTree->racine->Leftmost()); + while ( curS ) { + int cb = curS->bord; + AvanceEdge(cb, to, line, exact, step); + curS = static_cast(curS->elem[RIGHT]); + } + } +} + + + +void Shape::QuickScan(float &pos, int &curP, float to, FloatLigne* line, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos >= to ) { + return; + } + + if ( nbQRas > 1 ) { + int curW = 0; + // float lastX = 0; + // float lastY = 0; + int lastGuess = -1; + int lastB = -1; + + for (int i = firstQRas; i >= 0 && i < nbQRas; i = qrsData[i].next) { + int cb = qrsData[i].bord; + int oW = curW; + if ( swrData[cb].sens ) { + curW++; + } else { + curW--; + } + + if ( curW % 2 == 0 && oW % 2 != 0) { + + lastGuess = line->AppendBord(swrData[lastB].curX, + to - swrData[lastB].curY, + swrData[cb].curX, + to - swrData[cb].curY, + 0.0); + + swrData[cb].guess = lastGuess; + if ( lastB >= 0 ) { + swrData[lastB].guess = lastGuess - 1; + } + + } else if ( curW%2 != 0 && oW%2 == 0 ) { + + // lastX = swrData[cb].curX; + // lastY = swrData[cb].curY; + lastB = cb; + swrData[cb].guess = -1; + + } else { + swrData[cb].guess = -1; + } + } + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + int nPt = curPt++; + + int nbUp; + int nbDn; + int upNo; + int dnNo; + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + QuickRasterSubEdge(cb); + _updateIntersection(cb, nPt); + DestroyEdge(cb, to, line); + } + } + cb = NextAt(nPt, cb); + } + } + + // traitement du "upNo devient dnNo" + int ins_guess=-1; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + ins_guess = QuickRasterChgEdge(upNo ,dnNo, getPoint(nPt).x[0]); + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, to, line); + + CreateEdge(dnNo, to, step); + swrData[dnNo].guess = swrData[upNo].guess; + } else { + ins_guess = QuickRasterAddEdge(dnNo, getPoint(nPt).x[0], ins_guess); + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + ins_guess = QuickRasterAddEdge(cb, getPoint(nPt).x[0], ins_guess); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt, cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt-1).x[1]; + } else { + pos=to; + } + + pos = to; + for (int i=0; i < nbQRas; i++) { + int cb = qrsData[i].bord; + AvanceEdge(cb, to, line, true, step); + qrsData[i].x = swrData[cb].curX; + } + + QuickRasterSort(); +} + + + + +void Shape::QuickScan(float &pos, int &curP, float to, FillRule directed, BitLigne* line, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + + if ( pos >= to ) { + return; + } + + if ( nbQRas > 1 ) { + int curW = 0; + float lastX = 0; + + if ( directed == fill_oddEven ) { + + for (int i = firstQRas; i >= 0 && i < nbQRas; i = qrsData[i].next) { + int cb = qrsData[i].bord; + curW++; + curW &= 1; + if ( curW == 0 ) { + line->AddBord(lastX, swrData[cb].curX, true); + } else { + lastX = swrData[cb].curX; + } + } + + } else if ( directed == fill_positive ) { + // doesn't behave correctly; no way i know to do this without a ConvertToShape() + for (int i = firstQRas; i >= 0 && i < nbQRas; i = qrsData[i].next) { + int cb = qrsData[i].bord; + int oW = curW; + if ( swrData[cb].sens ) { + curW++; + } else { + curW--; + } + + if ( curW <= 0 && oW > 0) { + line->AddBord(lastX, swrData[cb].curX, true); + } else if ( curW > 0 && oW <= 0 ) { + lastX = swrData[cb].curX; + } + } + + } else if ( directed == fill_nonZero ) { + for (int i = firstQRas; i >= 0 && i < nbQRas; i = qrsData[i].next) { + int cb = qrsData[i].bord; + int oW = curW; + if ( swrData[cb].sens ) { + curW++; + } else { + curW--; + } + + if ( curW == 0 && oW != 0) { + line->AddBord(lastX, swrData[cb].curX, true); + } else if ( curW != 0 && oW == 0 ) { + lastX = swrData[cb].curX; + } + } + } + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + int nPt = curPt++; + + int nbUp; + int nbDn; + int upNo; + int dnNo; + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo = -1; + } + + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + QuickRasterSubEdge(cb); + _updateIntersection(cb, nPt); + DestroyEdge(cb, line); + } + } + cb = NextAt(nPt, cb); + } + } + + // traitement du "upNo devient dnNo" + int ins_guess = -1; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + ins_guess = QuickRasterChgEdge(upNo, dnNo, getPoint(nPt).x[0]); + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, line); + + CreateEdge(dnNo, to, step); + } else { + ins_guess = QuickRasterAddEdge(dnNo, getPoint(nPt).x[0], ins_guess); + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + ins_guess = QuickRasterAddEdge(cb, getPoint(nPt).x[0], ins_guess); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos=getPoint(curPt - 1).x[1]; + } else { + pos = to; + } + + pos = to; + for (int i = 0; i < nbQRas; i++) { + int cb = qrsData[i].bord; + AvanceEdge(cb, to, line, true, step); + qrsData[i].x = swrData[cb].curX; + } + + QuickRasterSort(); +} + + + +void Shape::QuickScan(float &pos, int &curP, float to, AlphaLigne* line, float step) +{ + if ( numberOfEdges() <= 1 ) { + return; + } + if ( pos >= to ) { + return; + } + + int curPt = curP; + while ( curPt < numberOfPoints() && getPoint(curPt).x[1] <= to ) { + int nPt = curPt++; + + int nbUp; + int nbDn; + int upNo; + int dnNo; + if ( getPoint(nPt).totalDegree() == 2 ) { + _countUpDownTotalDegree2(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } else { + _countUpDown(nPt, &nbUp, &nbDn, &upNo, &dnNo); + } + + if ( nbDn <= 0 ) { + upNo = -1; + } + if ( upNo >= 0 && swrData[upNo].misc == nullptr ) { + upNo = -1; + } + + if ( nbUp > 1 || ( nbUp == 1 && upNo < 0 ) ) { + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::max(e.st, e.en) ) { + if ( cb != upNo ) { + QuickRasterSubEdge(cb); + _updateIntersection(cb, nPt); + DestroyEdge(cb, line); + } + } + cb = NextAt(nPt,cb); + } + } + + // traitement du "upNo devient dnNo" + int ins_guess = -1; + if ( dnNo >= 0 ) { + if ( upNo >= 0 ) { + ins_guess = QuickRasterChgEdge(upNo, dnNo, getPoint(nPt).x[0]); + _updateIntersection(upNo, nPt); + DestroyEdge(upNo, line); + + CreateEdge(dnNo, to, step); + swrData[dnNo].guess = swrData[upNo].guess; + } else { + ins_guess = QuickRasterAddEdge(dnNo, getPoint(nPt).x[0], ins_guess); + CreateEdge(dnNo, to, step); + } + } + + if ( nbDn > 1 ) { // si nbDn == 1 , alors dnNo a deja ete traite + int cb = getPoint(nPt).incidentEdge[FIRST]; + while ( cb >= 0 && cb < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(cb); + if ( nPt == std::min(e.st, e.en) ) { + if ( cb != dnNo ) { + ins_guess = QuickRasterAddEdge(cb,getPoint(nPt).x[0], ins_guess); + CreateEdge(cb, to, step); + } + } + cb = NextAt(nPt,cb); + } + } + } + + curP = curPt; + if ( curPt > 0 ) { + pos = getPoint(curPt-1).x[1]; + } else { + pos = to; + } + + pos = to; + for (int i = 0; i < nbQRas; i++) { + int cb = qrsData[i].bord; + AvanceEdge(cb, to, line, true, step); + qrsData[i].x = swrData[cb].curX; + } + + QuickRasterSort(); +} + + +/* + * operations de bases pour la rasterization + * + */ +void Shape::CreateEdge(int no, float to, float step) +{ + int cPt; + Geom::Point dir; + if ( getEdge(no).st < getEdge(no).en ) { + cPt = getEdge(no).st; + swrData[no].sens = true; + dir = getEdge(no).dx; + } else { + cPt = getEdge(no).en; + swrData[no].sens = false; + dir = -getEdge(no).dx; + } + + swrData[no].lastX = swrData[no].curX = getPoint(cPt).x[0]; + swrData[no].lastY = swrData[no].curY = getPoint(cPt).x[1]; + + if ( fabs(dir[1]) < 0.000001 ) { + swrData[no].dxdy = 0; + } else { + swrData[no].dxdy = dir[0]/dir[1]; + } + + if ( fabs(dir[0]) < 0.000001 ) { + swrData[no].dydx = 0; + } else { + swrData[no].dydx = dir[1]/dir[0]; + } + + swrData[no].calcX = swrData[no].curX + (to - step - swrData[no].curY) * swrData[no].dxdy; + swrData[no].guess = -1; +} + + +void Shape::AvanceEdge(int no, float to, bool exact, float step) +{ + if ( exact ) { + Geom::Point dir; + Geom::Point stp; + if ( swrData[no].sens ) { + stp = getPoint(getEdge(no).st).x; + dir = getEdge(no).dx; + } else { + stp = getPoint(getEdge(no).en).x; + dir = -getEdge(no).dx; + } + + if ( fabs(dir[1]) < 0.000001 ) { + swrData[no].calcX = stp[0] + dir[0]; + } else { + swrData[no].calcX = stp[0] + ((to - stp[1]) * dir[0]) / dir[1]; + } + } else { + swrData[no].calcX += step * swrData[no].dxdy; + } + + swrData[no].lastX = swrData[no].curX; + swrData[no].lastY = swrData[no].curY; + swrData[no].curX = swrData[no].calcX; + swrData[no].curY = to; +} + +/* + * specialisation par type de structure utilise + */ + +void Shape::DestroyEdge(int no, float to, FloatLigne* line) +{ + if ( swrData[no].sens ) { + + if ( swrData[no].curX < swrData[no].lastX ) { + + swrData[no].guess = line->AddBordR(swrData[no].curX, + to - swrData[no].curY, + swrData[no].lastX, + to - swrData[no].lastY, + -swrData[no].dydx, + swrData[no].guess); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + swrData[no].guess = line->AddBord(swrData[no].lastX, + -(to - swrData[no].lastY), + swrData[no].curX, + -(to - swrData[no].curY), + swrData[no].dydx, + swrData[no].guess); + } + + } else { + + if ( swrData[no].curX < swrData[no].lastX ) { + + swrData[no].guess = line->AddBordR(swrData[no].curX, + -(to - swrData[no].curY), + swrData[no].lastX, + -(to - swrData[no].lastY), + swrData[no].dydx, + swrData[no].guess); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + swrData[no].guess = line->AddBord(swrData[no].lastX, + to - swrData[no].lastY, + swrData[no].curX, + to - swrData[no].curY, + -swrData[no].dydx, + swrData[no].guess); + } + } +} + + + +void Shape::AvanceEdge(int no, float to, FloatLigne *line, bool exact, float step) +{ + AvanceEdge(no,to,exact,step); + + if ( swrData[no].sens ) { + + if ( swrData[no].curX < swrData[no].lastX ) { + + swrData[no].guess = line->AddBordR(swrData[no].curX, + to - swrData[no].curY, + swrData[no].lastX, + to - swrData[no].lastY, + -swrData[no].dydx, + swrData[no].guess); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + swrData[no].guess = line->AddBord(swrData[no].lastX, + -(to - swrData[no].lastY), + swrData[no].curX, + -(to - swrData[no].curY), + swrData[no].dydx, + swrData[no].guess); + } + + } else { + + if ( swrData[no].curX < swrData[no].lastX ) { + + swrData[no].guess = line->AddBordR(swrData[no].curX, + -(to - swrData[no].curY), + swrData[no].lastX, + -(to - swrData[no].lastY), + swrData[no].dydx, + swrData[no].guess); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + swrData[no].guess = line->AddBord(swrData[no].lastX, + to - swrData[no].lastY, + swrData[no].curX, + to - swrData[no].curY, + -swrData[no].dydx, + swrData[no].guess); + } + } +} + + +void Shape::DestroyEdge(int no, BitLigne *line) +{ + if ( swrData[no].sens ) { + + if ( swrData[no].curX < swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, swrData[no].lastX, false); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX,swrData[no].curX,false); + } + + } else { + + if ( swrData[no].curX < swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, swrData[no].lastX, false); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, swrData[no].curX, false); + + } + } +} + + +void Shape::AvanceEdge(int no, float to, BitLigne *line, bool exact, float step) +{ + AvanceEdge(no, to, exact, step); + + if ( swrData[no].sens ) { + + if ( swrData[no].curX < swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, swrData[no].lastX, false); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, swrData[no].curX, false); + } + + } else { + + if ( swrData[no].curX < swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, swrData[no].lastX, false); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, swrData[no].curX, false); + } + } +} + + +void Shape::DestroyEdge(int no, AlphaLigne* line) +{ + if ( swrData[no].sens ) { + + if ( swrData[no].curX <= swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, + 0, + swrData[no].lastX, + swrData[no].curY - swrData[no].lastY, + -swrData[no].dydx); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, + 0, + swrData[no].curX, + swrData[no].curY - swrData[no].lastY, + swrData[no].dydx); + } + + } else { + + if ( swrData[no].curX <= swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, + 0, + swrData[no].lastX, + swrData[no].lastY - swrData[no].curY, + swrData[no].dydx); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, + 0, + swrData[no].curX, + swrData[no].lastY - swrData[no].curY, + -swrData[no].dydx); + } + } +} + + +void Shape::AvanceEdge(int no, float to, AlphaLigne *line, bool exact, float step) +{ + AvanceEdge(no,to,exact,step); + + if ( swrData[no].sens ) { + + if ( swrData[no].curX <= swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, + 0, + swrData[no].lastX, + swrData[no].curY - swrData[no].lastY, + -swrData[no].dydx); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, + 0, + swrData[no].curX, + swrData[no].curY - swrData[no].lastY, + swrData[no].dydx); + } + + } else { + + if ( swrData[no].curX <= swrData[no].lastX ) { + + line->AddBord(swrData[no].curX, + 0, + swrData[no].lastX, + swrData[no].lastY - swrData[no].curY, + swrData[no].dydx); + + } else if ( swrData[no].curX > swrData[no].lastX ) { + + line->AddBord(swrData[no].lastX, + 0, + swrData[no].curX, + swrData[no].lastY - swrData[no].curY, + -swrData[no].dydx); + } + } +} + +/** + * \param P point index. + * \param numberUp Filled in with the number of edges coming into P from above. + * \param numberDown Filled in with the number of edges coming exiting P to go below. + * \param upEdge One of the numberUp edges, or -1. + * \param downEdge One of the numberDown edges, or -1. + */ + +void Shape::_countUpDown(int P, int *numberUp, int *numberDown, int *upEdge, int *downEdge) const +{ + *numberUp = 0; + *numberDown = 0; + *upEdge = -1; + *downEdge = -1; + + int i = getPoint(P).incidentEdge[FIRST]; + + while ( i >= 0 && i < numberOfEdges() ) { + Shape::dg_arete const &e = getEdge(i); + if ( P == std::max(e.st, e.en) ) { + *upEdge = i; + (*numberUp)++; + } + if ( P == std::min(e.st, e.en) ) { + *downEdge = i; + (*numberDown)++; + } + i = NextAt(P, i); + } + +} + + + +/** + * Version of Shape::_countUpDown optimised for the case when getPoint(P).totalDegree() == 2. + */ + +void Shape::_countUpDownTotalDegree2(int P, + int *numberUp, int *numberDown, int *upEdge, int *downEdge) const +{ + *numberUp = 0; + *numberDown = 0; + *upEdge = -1; + *downEdge = -1; + + for (int j : getPoint(P).incidentEdge) { + Shape::dg_arete const &e = getEdge(j); + if ( P == std::max(e.st, e.en) ) { + *upEdge = j; + (*numberUp)++; + } + if ( P == std::min(e.st, e.en) ) { + *downEdge = j; + (*numberDown)++; + } + } +} + + +void Shape::_updateIntersection(int e, int p) +{ + swrData[e].lastX = swrData[e].curX; + swrData[e].lastY = swrData[e].curY; + swrData[e].curX = getPoint(p).x[0]; + swrData[e].curY = getPoint(p).x[1]; + swrData[e].misc = nullptr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/ShapeSweep.cpp b/src/livarot/ShapeSweep.cpp new file mode 100644 index 0000000..3b5ed72 --- /dev/null +++ b/src/livarot/ShapeSweep.cpp @@ -0,0 +1,3319 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include <2geom/affine.h> +#include "Shape.h" +#include "livarot/sweep-event-queue.h" +#include "livarot/sweep-tree-list.h" +#include "livarot/sweep-tree.h" + +//int doDebug=0; + +/* + * El Intersector. + * algorithm: 1) benley ottman to get intersections of all the polygon's edges + * 2) rounding of the points of the polygon, Hooby's algorithm + * 3) DFS with clockwise choice of the edge to compute the windings + * 4) choose edges according to winding numbers and fill rule + * some additional nastyness: step 2 needs a seed winding number for the upper-left point of each + * connex subgraph of the graph. computing these brutally is O(n^3): baaaad. so during the sweeping in 1) + * we keep for each point the edge of the resulting graph (not the original) that lies just on its left; + * when the time comes for the point to get its winding number computed, that edge must have been treated, + * because its upper end lies above the aforementioned point, meaning we know the winding number of the point. + * only, there is a catch: since we're sweeping the polygon, the edge we want to link the point to has not yet been + * added (that would be too easy...). so the points are put on a linked list on the original shape's edge, and the list + * is flushed when the edge is added. + * rounding: to do the rounding, we need to find which edges cross the surrounding of the rounded points (at + * each sweepline position). grunt method tries all combination of "rounded points in the sweepline"x"edges crossing + * the sweepline". That's bad (and that's what polyboolean does, if i am not mistaken). so for each point + * rounded in a given sweepline, keep immediate left and right edges at the time the point is treated. + * when edges/points crossing are searched, walk the edge list (in the sweepline at the end of the batch) starting + * from the rounded points' left and right from that time. may sound strange, but it works because edges that + * end or start in the batch have at least one end in the batch. + * all these are the cause of the numerous linked lists of points and edges maintained in the sweeping + */ + +void +Shape::ResetSweep () +{ + MakePointData (true); + MakeEdgeData (true); + MakeSweepSrcData (true); +} + +void +Shape::CleanupSweep () +{ + MakePointData (false); + MakeEdgeData (false); + MakeSweepSrcData (false); +} + +void +Shape::ForceToPolygon () +{ + type = shape_polygon; +} + +int +Shape::Reoriente (Shape * a) +{ + Reset (0, 0); + if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1) + return 0; + if (directedEulerian(a) == false) + return shape_input_err; + + _pts = a->_pts; + if (numberOfPoints() > maxPt) + { + maxPt = numberOfPoints(); + if (_has_points_data) { + pData.resize(maxPt); + _point_data_initialised = false; + _bbox_up_to_date = false; + } + } + + _aretes = a->_aretes; + if (numberOfEdges() > maxAr) + { + maxAr = numberOfEdges(); + if (_has_edges_data) + eData.resize(maxAr); + if (_has_sweep_src_data) + swsData.resize(maxAr); + if (_has_sweep_dest_data) + swdData.resize(maxAr); + if (_has_raster_data) + swrData.resize(maxAr); + } + + MakePointData (true); + MakeEdgeData (true); + MakeSweepDestData (true); + + initialisePointData(); + + for (int i = 0; i < numberOfPoints(); i++) { + _pts[i].x = pData[i].rx; + _pts[i].oldDegree = getPoint(i).totalDegree(); + } + + for (int i = 0; i < a->numberOfEdges(); i++) + { + eData[i].rdx = pData[getEdge(i).en].rx - pData[getEdge(i).st].rx; + eData[i].weight = 1; + _aretes[i].dx = eData[i].rdx; + } + + SortPointsRounded (); + + _need_edges_sorting = true; + GetWindings (this, nullptr, bool_op_union, true); + +// Plot(341,56,8,400,400,true,true,false,true); + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].leW %= 2; + swdData[i].riW %= 2; + if (swdData[i].leW < 0) + swdData[i].leW = -swdData[i].leW; + if (swdData[i].riW < 0) + swdData[i].riW = -swdData[i].riW; + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + + MakePointData (false); + MakeEdgeData (false); + MakeSweepDestData (false); + + if (directedEulerian(this) == false) + { +// printf( "pas euclidian2"); + _pts.clear(); + _aretes.clear(); + return shape_euler_err; + } + + type = shape_polygon; + return 0; +} + +int +Shape::ConvertToShape (Shape * a, FillRule directed, bool invert) +{ + Reset (0, 0); + + if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1) { + return 0; + } + + if ( directed != fill_justDont && directedEulerian(a) == false ) { + g_warning ("Shape error in ConvertToShape: directedEulerian(a) == false\n"); + return shape_input_err; + } + + a->ResetSweep(); + + if (sTree == nullptr) { + sTree = new SweepTreeList(a->numberOfEdges()); + } + if (sEvts == nullptr) { + sEvts = new SweepEventQueue(a->numberOfEdges()); + } + + MakePointData(true); + MakeEdgeData(true); + MakeSweepSrcData(true); + MakeSweepDestData(true); + MakeBackData(a->_has_back_data); + + a->initialisePointData(); + a->initialiseEdgeData(); + + a->SortPointsRounded(); + + chgts.clear(); + + double lastChange = a->pData[0].rx[1] - 1.0; + int lastChgtPt = 0; + int edgeHead = -1; + Shape *shapeHead = nullptr; + + clearIncidenceData(); + + int curAPt = 0; + + while (curAPt < a->numberOfPoints() || sEvts->size() > 0) { + Geom::Point ptX; + double ptL, ptR; + SweepTree *intersL = nullptr; + SweepTree *intersR = nullptr; + int nPt = -1; + Shape *ptSh = nullptr; + bool isIntersection = false; + if (sEvts->peek(intersL, intersR, ptX, ptL, ptR)) + { + if (a->pData[curAPt].pending > 0 + || (a->pData[curAPt].rx[1] > ptX[1] + || (a->pData[curAPt].rx[1] == ptX[1] + && a->pData[curAPt].rx[0] > ptX[0]))) + { + /* FIXME: could just be pop? */ + sEvts->extract(intersL, intersR, ptX, ptL, ptR); + isIntersection = true; + } + else + { + nPt = curAPt++; + ptSh = a; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + } + else + { + nPt = curAPt++; + ptSh = a; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + + if (isIntersection == false) + { + if (ptSh->getPoint(nPt).dI == 0 && ptSh->getPoint(nPt).dO == 0) + continue; + } + + Geom::Point rPtX; + rPtX[0]= Round (ptX[0]); + rPtX[1]= Round (ptX[1]); + int lastPointNo = AddPoint (rPtX); + pData[lastPointNo].rx = rPtX; + + if (rPtX[1] > lastChange) + { + int lastI = AssemblePoints (lastChgtPt, lastPointNo); + + Shape *curSh = shapeHead; + int curBo = edgeHead; + while (curSh) + { + curSh->swsData[curBo].leftRnd = + pData[curSh->swsData[curBo].leftRnd].newInd; + curSh->swsData[curBo].rightRnd = + pData[curSh->swsData[curBo].rightRnd].newInd; + + Shape *neSh = curSh->swsData[curBo].nextSh; + curBo = curSh->swsData[curBo].nextBo; + curSh = neSh; + } + + for (auto & chgt : chgts) + { + chgt.ptNo = pData[chgt.ptNo].newInd; + if (chgt.type == 0) + { + if (chgt.src->getEdge(chgt.bord).st < + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = + chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = + chgt.ptNo; + } + } + else if (chgt.type == 1) + { + if (chgt.src->getEdge(chgt.bord).st > + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = + chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = + chgt.ptNo; + } + } + } + + CheckAdjacencies (lastI, lastChgtPt, shapeHead, edgeHead); + + CheckEdges (lastI, lastChgtPt, a, nullptr, bool_op_union); + + for (int i = lastChgtPt; i < lastI; i++) { + if (pData[i].askForWindingS) { + Shape *windS = pData[i].askForWindingS; + int windB = pData[i].askForWindingB; + pData[i].nextLinkedPoint = windS->swsData[windB].firstLinkedPoint; + windS->swsData[windB].firstLinkedPoint = i; + } + } + + if (lastI < lastPointNo) { + _pts[lastI] = getPoint(lastPointNo); + pData[lastI] = pData[lastPointNo]; + } + lastPointNo = lastI; + _pts.resize(lastI + 1); + + lastChgtPt = lastPointNo; + lastChange = rPtX[1]; + chgts.clear(); + edgeHead = -1; + shapeHead = nullptr; + } + + + if (isIntersection) + { +// printf("(%i %i [%i %i]) ",intersL->bord,intersR->bord,intersL->startPoint,intersR->startPoint); + intersL->RemoveEvent (*sEvts, LEFT); + intersR->RemoveEvent (*sEvts, RIGHT); + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, INTERSECTION, + intersL->src, intersL->bord, intersR->src, intersR->bord); + + intersL->SwapWithRight (*sTree, *sEvts); + + TesteIntersection (intersL, LEFT, false); + TesteIntersection (intersR, RIGHT, false); + } + else + { + int cb; + + int nbUp = 0, nbDn = 0; + int upNo = -1, dnNo = -1; + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + upNo = cb; + nbUp++; + } + if ((ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + dnNo = cb; + nbDn++; + } + cb = ptSh->NextAt (nPt, cb); + } + + if (nbDn <= 0) + { + upNo = -1; + } + if (upNo >= 0 && (SweepTree *) ptSh->swsData[upNo].misc == nullptr) + { + upNo = -1; + } + + bool doWinding = true; + + if (nbUp > 0) + { + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + if (cb != upNo) + { + SweepTree *node = + (SweepTree *) ptSh->swsData[cb].misc; + if (node == nullptr) + { + } + else + { + AddChgt (lastPointNo, lastChgtPt, shapeHead, + edgeHead, EDGE_REMOVED, node->src, node->bord, + nullptr, -1); + ptSh->swsData[cb].misc = nullptr; + + int onLeftB = -1, onRightB = -1; + Shape *onLeftS = nullptr; + Shape *onRightS = nullptr; + if (node->elem[LEFT]) + { + onLeftB = + (static_cast < + SweepTree * >(node->elem[LEFT]))->bord; + onLeftS = + (static_cast < + SweepTree * >(node->elem[LEFT]))->src; + } + if (node->elem[RIGHT]) + { + onRightB = + (static_cast < + SweepTree * >(node->elem[RIGHT]))->bord; + onRightS = + (static_cast < + SweepTree * >(node->elem[RIGHT]))->src; + } + + node->Remove (*sTree, *sEvts, true); + if (onLeftS && onRightS) + { + SweepTree *onLeft = + (SweepTree *) onLeftS->swsData[onLeftB]. + misc; + if (onLeftS == ptSh + && (onLeftS->getEdge(onLeftB).en == nPt + || onLeftS->getEdge(onLeftB).st == + nPt)) + { + } + else + { + if (onRightS == ptSh + && (onRightS->getEdge(onRightB).en == + nPt + || onRightS->getEdge(onRightB). + st == nPt)) + { + } + else + { + TesteIntersection (onLeft, RIGHT, false); + } + } + } + } + } + } + cb = ptSh->NextAt (nPt, cb); + } + } + + // traitement du "upNo devient dnNo" + SweepTree *insertionNode = nullptr; + if (dnNo >= 0) + { + if (upNo >= 0) + { + SweepTree *node = (SweepTree *) ptSh->swsData[upNo].misc; + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_REMOVED, + node->src, node->bord, nullptr, -1); + + ptSh->swsData[upNo].misc = nullptr; + + node->RemoveEvents (*sEvts); + node->ConvertTo (ptSh, dnNo, 1, lastPointNo); + ptSh->swsData[dnNo].misc = node; + TesteIntersection (node, RIGHT, false); + TesteIntersection (node, LEFT, false); + insertionNode = node; + + ptSh->swsData[dnNo].curPoint = lastPointNo; + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_INSERTED, + node->src, node->bord, nullptr, -1); + } + else + { + SweepTree *node = sTree->add(ptSh, dnNo, 1, lastPointNo, this); + ptSh->swsData[dnNo].misc = node; + node->Insert (*sTree, *sEvts, this, lastPointNo, true); + if (doWinding) + { + SweepTree *myLeft = + static_cast < SweepTree * >(node->elem[LEFT]); + if (myLeft) + { + pData[lastPointNo].askForWindingS = myLeft->src; + pData[lastPointNo].askForWindingB = myLeft->bord; + } + else + { + pData[lastPointNo].askForWindingB = -1; + } + doWinding = false; + } + TesteIntersection (node, RIGHT, false); + TesteIntersection (node, LEFT, false); + insertionNode = node; + + ptSh->swsData[dnNo].curPoint = lastPointNo; + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_INSERTED, + node->src, node->bord, nullptr, -1); + } + } + + if (nbDn > 1) + { // si nbDn == 1 , alors dnNo a deja ete traite + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + if (cb != dnNo) + { + SweepTree *node = sTree->add(ptSh, cb, 1, lastPointNo, this); + ptSh->swsData[cb].misc = node; + node->InsertAt (*sTree, *sEvts, this, insertionNode, + nPt, true); + if (doWinding) + { + SweepTree *myLeft = + static_cast < SweepTree * >(node->elem[LEFT]); + if (myLeft) + { + pData[lastPointNo].askForWindingS = + myLeft->src; + pData[lastPointNo].askForWindingB = + myLeft->bord; + } + else + { + pData[lastPointNo].askForWindingB = -1; + } + doWinding = false; + } + TesteIntersection (node, RIGHT, false); + TesteIntersection (node, LEFT, false); + + ptSh->swsData[cb].curPoint = lastPointNo; + AddChgt (lastPointNo, lastChgtPt, shapeHead, + edgeHead, EDGE_INSERTED, node->src, node->bord, nullptr, + -1); + } + } + cb = ptSh->NextAt (nPt, cb); + } + } + } + } + { + int lastI = AssemblePoints (lastChgtPt, numberOfPoints()); + + + Shape *curSh = shapeHead; + int curBo = edgeHead; + while (curSh) + { + curSh->swsData[curBo].leftRnd = + pData[curSh->swsData[curBo].leftRnd].newInd; + curSh->swsData[curBo].rightRnd = + pData[curSh->swsData[curBo].rightRnd].newInd; + + Shape *neSh = curSh->swsData[curBo].nextSh; + curBo = curSh->swsData[curBo].nextBo; + curSh = neSh; + } + + for (auto & chgt : chgts) + { + chgt.ptNo = pData[chgt.ptNo].newInd; + if (chgt.type == 0) + { + if (chgt.src->getEdge(chgt.bord).st < + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = chgt.ptNo; + } + } + else if (chgt.type == 1) + { + if (chgt.src->getEdge(chgt.bord).st > + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = chgt.ptNo; + } + } + } + + CheckAdjacencies (lastI, lastChgtPt, shapeHead, edgeHead); + + CheckEdges (lastI, lastChgtPt, a, nullptr, bool_op_union); + + for (int i = lastChgtPt; i < lastI; i++) + { + if (pData[i].askForWindingS) + { + Shape *windS = pData[i].askForWindingS; + int windB = pData[i].askForWindingB; + pData[i].nextLinkedPoint = windS->swsData[windB].firstLinkedPoint; + windS->swsData[windB].firstLinkedPoint = i; + } + } + + _pts.resize(lastI); + + edgeHead = -1; + shapeHead = nullptr; + } + + chgts.clear(); + +// Plot (98.0, 112.0, 8.0, 400.0, 400.0, true, true, true, true); +// Plot(200.0,200.0,2.0,400.0,400.0,true,true,true,true); + + // AssemblePoints(a); + +// GetAdjacencies(a); + +// MakeAretes(a); + clearIncidenceData(); + + AssembleAretes (directed); + +// Plot (98.0, 112.0, 8.0, 400.0, 400.0, true, true, true, true); + + for (int i = 0; i < numberOfPoints(); i++) + { + _pts[i].oldDegree = getPoint(i).totalDegree(); + } +// Validate(); + + _need_edges_sorting = true; + if ( directed == fill_justDont ) { + SortEdges(); + } else { + GetWindings (a); + } +// Plot (98.0, 112.0, 8.0, 400.0, 400.0, true, true, true, true); +// if ( doDebug ) { +// a->CalcBBox(); +// a->Plot(a->leftX,a->topY,32.0,0.0,0.0,true,true,true,true,"orig.svg"); +// Plot(a->leftX,a->topY,32.0,0.0,0.0,true,true,true,true,"winded.svg"); +// } + if (directed == fill_positive) + { + if (invert) + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW < 0 && swdData[i].riW >= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW >= 0 && swdData[i].riW < 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + else + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + } + else if (directed == fill_nonZero) + { + if (invert) + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW < 0 && swdData[i].riW == 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW > 0 && swdData[i].riW == 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW == 0 && swdData[i].riW < 0) + { + Inverse (i); + eData[i].weight = 1; + } + else if (swdData[i].leW == 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + else + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW > 0 && swdData[i].riW == 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW < 0 && swdData[i].riW == 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW == 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else if (swdData[i].leW == 0 && swdData[i].riW < 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + } + else if (directed == fill_oddEven) + { + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].leW %= 2; + swdData[i].riW %= 2; + if (swdData[i].leW < 0) + swdData[i].leW = -swdData[i].leW; + if (swdData[i].riW < 0) + swdData[i].riW = -swdData[i].riW; + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } else if ( directed == fill_justDont ) { + for (int i=0;iCleanupSweep (); + type = shape_polygon; + return 0; +} + +// technically it's just a ConvertToShape() on 2 polygons at the same time, and different rules +// for choosing the edges according to their winding numbers. +// probably one of the biggest function i ever wrote. +int +Shape::Booleen (Shape * a, Shape * b, BooleanOp mod,int cutPathID) +{ + if (a == b || a == nullptr || b == nullptr) + return shape_input_err; + Reset (0, 0); + if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1) + return 0; + if (b->numberOfPoints() <= 1 || b->numberOfEdges() <= 1) + return 0; + if ( mod == bool_op_cut ) { + } else if ( mod == bool_op_slice ) { + } else { + if (a->type != shape_polygon) + return shape_input_err; + if (b->type != shape_polygon) + return shape_input_err; + } + + a->ResetSweep (); + b->ResetSweep (); + + if (sTree == nullptr) { + sTree = new SweepTreeList(a->numberOfEdges() + b->numberOfEdges()); + } + if (sEvts == nullptr) { + sEvts = new SweepEventQueue(a->numberOfEdges() + b->numberOfEdges()); + } + + MakePointData (true); + MakeEdgeData (true); + MakeSweepSrcData (true); + MakeSweepDestData (true); + if (a->hasBackData () && b->hasBackData ()) + { + MakeBackData (true); + } + else + { + MakeBackData (false); + } + + a->initialisePointData(); + b->initialisePointData(); + + a->initialiseEdgeData(); + b->initialiseEdgeData(); + + a->SortPointsRounded (); + b->SortPointsRounded (); + + chgts.clear(); + + double lastChange = + (a->pData[0].rx[1] < + b->pData[0].rx[1]) ? a->pData[0].rx[1] - 1.0 : b->pData[0].rx[1] - 1.0; + int lastChgtPt = 0; + int edgeHead = -1; + Shape *shapeHead = nullptr; + + clearIncidenceData(); + + int curAPt = 0; + int curBPt = 0; + + while (curAPt < a->numberOfPoints() || curBPt < b->numberOfPoints() || sEvts->size() > 0) + { +/* for (int i=0;ibord,sEvts.events[i].rightSweep->bord); // localizing ok + } + // cout << endl; + if ( sTree.racine ) { + SweepTree* ct=static_cast (sTree.racine->Leftmost()); + while ( ct ) { + printf("%i %i [%i\n",ct->bord,ct->startPoint,(ct->src==a)?1:0); + ct=static_cast (ct->elem[RIGHT]); + } + } + printf("\n");*/ + + Geom::Point ptX; + double ptL, ptR; + SweepTree *intersL = nullptr; + SweepTree *intersR = nullptr; + int nPt = -1; + Shape *ptSh = nullptr; + bool isIntersection = false; + + if (sEvts->peek(intersL, intersR, ptX, ptL, ptR)) + { + if (curAPt < a->numberOfPoints()) + { + if (curBPt < b->numberOfPoints()) + { + if (a->pData[curAPt].rx[1] < b->pData[curBPt].rx[1] + || (a->pData[curAPt].rx[1] == b->pData[curBPt].rx[1] + && a->pData[curAPt].rx[0] < b->pData[curBPt].rx[0])) + { + if (a->pData[curAPt].pending > 0 + || (a->pData[curAPt].rx[1] > ptX[1] + || (a->pData[curAPt].rx[1] == ptX[1] + && a->pData[curAPt].rx[0] > ptX[0]))) + { + /* FIXME: could be pop? */ + sEvts->extract(intersL, intersR, ptX, ptL, ptR); + isIntersection = true; + } + else + { + nPt = curAPt++; + ptSh = a; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + } + else + { + if (b->pData[curBPt].pending > 0 + || (b->pData[curBPt].rx[1] > ptX[1] + || (b->pData[curBPt].rx[1] == ptX[1] + && b->pData[curBPt].rx[0] > ptX[0]))) + { + /* FIXME: could be pop? */ + sEvts->extract(intersL, intersR, ptX, ptL, ptR); + isIntersection = true; + } + else + { + nPt = curBPt++; + ptSh = b; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + } + } + else + { + if (a->pData[curAPt].pending > 0 + || (a->pData[curAPt].rx[1] > ptX[1] + || (a->pData[curAPt].rx[1] == ptX[1] + && a->pData[curAPt].rx[0] > ptX[0]))) + { + /* FIXME: could be pop? */ + sEvts->extract(intersL, intersR, ptX, ptL, ptR); + isIntersection = true; + } + else + { + nPt = curAPt++; + ptSh = a; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + } + } + else + { + if (b->pData[curBPt].pending > 0 + || (b->pData[curBPt].rx[1] > ptX[1] + || (b->pData[curBPt].rx[1] == ptX[1] + && b->pData[curBPt].rx[0] > ptX[0]))) + { + /* FIXME: could be pop? */ + sEvts->extract(intersL, intersR, ptX, ptL, ptR); + isIntersection = true; + } + else + { + nPt = curBPt++; + ptSh = b; + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + } + } + else + { + if (curAPt < a->numberOfPoints()) + { + if (curBPt < b->numberOfPoints()) + { + if (a->pData[curAPt].rx[1] < b->pData[curBPt].rx[1] + || (a->pData[curAPt].rx[1] == b->pData[curBPt].rx[1] + && a->pData[curAPt].rx[0] < b->pData[curBPt].rx[0])) + { + nPt = curAPt++; + ptSh = a; + } + else + { + nPt = curBPt++; + ptSh = b; + } + } + else + { + nPt = curAPt++; + ptSh = a; + } + } + else + { + nPt = curBPt++; + ptSh = b; + } + ptX = ptSh->pData[nPt].rx; + isIntersection = false; + } + + if (isIntersection == false) + { + if (ptSh->getPoint(nPt).dI == 0 && ptSh->getPoint(nPt).dO == 0) + continue; + } + + Geom::Point rPtX; + rPtX[0]= Round (ptX[0]); + rPtX[1]= Round (ptX[1]); + int lastPointNo = AddPoint (rPtX); + pData[lastPointNo].rx = rPtX; + + if (rPtX[1] > lastChange) + { + int lastI = AssemblePoints (lastChgtPt, lastPointNo); + + + Shape *curSh = shapeHead; + int curBo = edgeHead; + while (curSh) + { + curSh->swsData[curBo].leftRnd = + pData[curSh->swsData[curBo].leftRnd].newInd; + curSh->swsData[curBo].rightRnd = + pData[curSh->swsData[curBo].rightRnd].newInd; + + Shape *neSh = curSh->swsData[curBo].nextSh; + curBo = curSh->swsData[curBo].nextBo; + curSh = neSh; + } + + for (auto & chgt : chgts) + { + chgt.ptNo = pData[chgt.ptNo].newInd; + if (chgt.type == 0) + { + if (chgt.src->getEdge(chgt.bord).st < + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = + chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = + chgt.ptNo; + } + } + else if (chgt.type == 1) + { + if (chgt.src->getEdge(chgt.bord).st > + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = + chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = + chgt.ptNo; + } + } + } + + CheckAdjacencies (lastI, lastChgtPt, shapeHead, edgeHead); + + CheckEdges (lastI, lastChgtPt, a, b, mod); + + for (int i = lastChgtPt; i < lastI; i++) + { + if (pData[i].askForWindingS) + { + Shape *windS = pData[i].askForWindingS; + int windB = pData[i].askForWindingB; + pData[i].nextLinkedPoint = + windS->swsData[windB].firstLinkedPoint; + windS->swsData[windB].firstLinkedPoint = i; + } + } + + if (lastI < lastPointNo) + { + _pts[lastI] = getPoint(lastPointNo); + pData[lastI] = pData[lastPointNo]; + } + lastPointNo = lastI; + _pts.resize(lastI + 1); + + lastChgtPt = lastPointNo; + lastChange = rPtX[1]; + chgts.clear(); + edgeHead = -1; + shapeHead = nullptr; + } + + + if (isIntersection) + { + // les 2 events de part et d'autre de l'intersection + // (celui de l'intersection a deja ete depile) + intersL->RemoveEvent (*sEvts, LEFT); + intersR->RemoveEvent (*sEvts, RIGHT); + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, INTERSECTION, + intersL->src, intersL->bord, intersR->src, intersR->bord); + + intersL->SwapWithRight (*sTree, *sEvts); + + TesteIntersection (intersL, LEFT, true); + TesteIntersection (intersR, RIGHT, true); + } + else + { + int cb; + + int nbUp = 0, nbDn = 0; + int upNo = -1, dnNo = -1; + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + upNo = cb; + nbUp++; + } + if ((ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + dnNo = cb; + nbDn++; + } + cb = ptSh->NextAt (nPt, cb); + } + + if (nbDn <= 0) + { + upNo = -1; + } + if (upNo >= 0 && (SweepTree *) ptSh->swsData[upNo].misc == nullptr) + { + upNo = -1; + } + +// upNo=-1; + + bool doWinding = true; + + if (nbUp > 0) + { + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + if (cb != upNo) + { + SweepTree *node = + (SweepTree *) ptSh->swsData[cb].misc; + if (node == nullptr) + { + } + else + { + AddChgt (lastPointNo, lastChgtPt, shapeHead, + edgeHead, EDGE_REMOVED, node->src, node->bord, + nullptr, -1); + ptSh->swsData[cb].misc = nullptr; + + int onLeftB = -1, onRightB = -1; + Shape *onLeftS = nullptr; + Shape *onRightS = nullptr; + if (node->elem[LEFT]) + { + onLeftB = + (static_cast < + SweepTree * >(node->elem[LEFT]))->bord; + onLeftS = + (static_cast < + SweepTree * >(node->elem[LEFT]))->src; + } + if (node->elem[RIGHT]) + { + onRightB = + (static_cast < + SweepTree * >(node->elem[RIGHT]))->bord; + onRightS = + (static_cast < + SweepTree * >(node->elem[RIGHT]))->src; + } + + node->Remove (*sTree, *sEvts, true); + if (onLeftS && onRightS) + { + SweepTree *onLeft = + (SweepTree *) onLeftS->swsData[onLeftB]. + misc; +// SweepTree* onRight=(SweepTree*)onRightS->swsData[onRightB].misc; + if (onLeftS == ptSh + && (onLeftS->getEdge(onLeftB).en == nPt + || onLeftS->getEdge(onLeftB).st == + nPt)) + { + } + else + { + if (onRightS == ptSh + && (onRightS->getEdge(onRightB).en == + nPt + || onRightS->getEdge(onRightB). + st == nPt)) + { + } + else + { + TesteIntersection (onLeft, RIGHT, true); + } + } + } + } + } + } + cb = ptSh->NextAt (nPt, cb); + } + } + + // traitement du "upNo devient dnNo" + SweepTree *insertionNode = nullptr; + if (dnNo >= 0) + { + if (upNo >= 0) + { + SweepTree *node = (SweepTree *) ptSh->swsData[upNo].misc; + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_REMOVED, + node->src, node->bord, nullptr, -1); + + ptSh->swsData[upNo].misc = nullptr; + + node->RemoveEvents (*sEvts); + node->ConvertTo (ptSh, dnNo, 1, lastPointNo); + ptSh->swsData[dnNo].misc = node; + TesteIntersection (node, RIGHT, true); + TesteIntersection (node, LEFT, true); + insertionNode = node; + + ptSh->swsData[dnNo].curPoint = lastPointNo; + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_INSERTED, + node->src, node->bord, nullptr, -1); + } + else + { + SweepTree *node = sTree->add(ptSh, dnNo, 1, lastPointNo, this); + ptSh->swsData[dnNo].misc = node; + node->Insert (*sTree, *sEvts, this, lastPointNo, true); + + if (doWinding) + { + SweepTree *myLeft = + static_cast < SweepTree * >(node->elem[LEFT]); + if (myLeft) + { + pData[lastPointNo].askForWindingS = myLeft->src; + pData[lastPointNo].askForWindingB = myLeft->bord; + } + else + { + pData[lastPointNo].askForWindingB = -1; + } + doWinding = false; + } + + TesteIntersection (node, RIGHT, true); + TesteIntersection (node, LEFT, true); + insertionNode = node; + + ptSh->swsData[dnNo].curPoint = lastPointNo; + + AddChgt (lastPointNo, lastChgtPt, shapeHead, edgeHead, EDGE_INSERTED, + node->src, node->bord, nullptr, -1); + } + } + + if (nbDn > 1) + { // si nbDn == 1 , alors dnNo a deja ete traite + cb = ptSh->getPoint(nPt).incidentEdge[FIRST]; + while (cb >= 0 && cb < ptSh->numberOfEdges()) + { + if ((ptSh->getEdge(cb).st > ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).en) + || (ptSh->getEdge(cb).st < ptSh->getEdge(cb).en + && nPt == ptSh->getEdge(cb).st)) + { + if (cb != dnNo) + { + SweepTree *node = sTree->add(ptSh, cb, 1, lastPointNo, this); + ptSh->swsData[cb].misc = node; +// node->Insert(sTree,*sEvts,this,lastPointNo,true); + node->InsertAt (*sTree, *sEvts, this, insertionNode, + nPt, true); + + if (doWinding) + { + SweepTree *myLeft = + static_cast < SweepTree * >(node->elem[LEFT]); + if (myLeft) + { + pData[lastPointNo].askForWindingS = + myLeft->src; + pData[lastPointNo].askForWindingB = + myLeft->bord; + } + else + { + pData[lastPointNo].askForWindingB = -1; + } + doWinding = false; + } + + TesteIntersection (node, RIGHT, true); + TesteIntersection (node, LEFT, true); + + ptSh->swsData[cb].curPoint = lastPointNo; + + AddChgt (lastPointNo, lastChgtPt, shapeHead, + edgeHead, EDGE_INSERTED, node->src, node->bord, nullptr, + -1); + } + } + cb = ptSh->NextAt (nPt, cb); + } + } + } + } + { + int lastI = AssemblePoints (lastChgtPt, numberOfPoints()); + + + Shape *curSh = shapeHead; + int curBo = edgeHead; + while (curSh) + { + curSh->swsData[curBo].leftRnd = + pData[curSh->swsData[curBo].leftRnd].newInd; + curSh->swsData[curBo].rightRnd = + pData[curSh->swsData[curBo].rightRnd].newInd; + + Shape *neSh = curSh->swsData[curBo].nextSh; + curBo = curSh->swsData[curBo].nextBo; + curSh = neSh; + } + + /* FIXME: this kind of code seems to appear frequently */ + for (auto & chgt : chgts) + { + chgt.ptNo = pData[chgt.ptNo].newInd; + if (chgt.type == 0) + { + if (chgt.src->getEdge(chgt.bord).st < + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = chgt.ptNo; + } + } + else if (chgt.type == 1) + { + if (chgt.src->getEdge(chgt.bord).st > + chgt.src->getEdge(chgt.bord).en) + { + chgt.src->swsData[chgt.bord].stPt = chgt.ptNo; + } + else + { + chgt.src->swsData[chgt.bord].enPt = chgt.ptNo; + } + } + } + + CheckAdjacencies (lastI, lastChgtPt, shapeHead, edgeHead); + + CheckEdges (lastI, lastChgtPt, a, b, mod); + + for (int i = lastChgtPt; i < lastI; i++) + { + if (pData[i].askForWindingS) + { + Shape *windS = pData[i].askForWindingS; + int windB = pData[i].askForWindingB; + pData[i].nextLinkedPoint = windS->swsData[windB].firstLinkedPoint; + windS->swsData[windB].firstLinkedPoint = i; + } + } + + _pts.resize(lastI); + + edgeHead = -1; + shapeHead = nullptr; + } + + chgts.clear(); + clearIncidenceData(); + +// Plot(190,70,6,400,400,true,false,true,true); + + if ( mod == bool_op_cut ) { + AssembleAretes (fill_justDont); + // dupliquer les aretes de la coupure + int i=numberOfEdges()-1; + for (;i>=0;i--) { + if ( ebData[i].pathID == cutPathID ) { + // on duplique + int nEd=AddEdge(getEdge(i).en,getEdge(i).st); + ebData[nEd].pathID=cutPathID; + ebData[nEd].pieceID=ebData[i].pieceID; + ebData[nEd].tSt=ebData[i].tEn; + ebData[nEd].tEn=ebData[i].tSt; + eData[nEd].weight=eData[i].weight; + // lui donner les firstlinkedpoitn si besoin + if ( getEdge(i).en >= getEdge(i).st ) { + int cp = swsData[i].firstLinkedPoint; + while (cp >= 0) { + pData[cp].askForWindingB = nEd; + cp = pData[cp].nextLinkedPoint; + } + swsData[nEd].firstLinkedPoint = swsData[i].firstLinkedPoint; + swsData[i].firstLinkedPoint=-1; + } + } + } + } else if ( mod == bool_op_slice ) { + } else { + AssembleAretes (); + } + + for (int i = 0; i < numberOfPoints(); i++) + { + _pts[i].oldDegree = getPoint(i).totalDegree(); + } + + _need_edges_sorting = true; + if ( mod == bool_op_slice ) { + } else { + GetWindings (a, b, mod, false); + } +// Plot(190,70,6,400,400,true,true,true,true); + + if (mod == bool_op_symdiff) + { + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].leW = swdData[i].leW % 2; + if (swdData[i].leW < 0) + swdData[i].leW = -swdData[i].leW; + swdData[i].riW = swdData[i].riW; + if (swdData[i].riW < 0) + swdData[i].riW = -swdData[i].riW; + + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + else if (mod == bool_op_union || mod == bool_op_diff) + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + else if (mod == bool_op_inters) + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW > 1 && swdData[i].riW <= 1) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 1 && swdData[i].riW > 1) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } else if ( mod == bool_op_cut ) { + // inverser les aretes de la coupe au besoin + for (int i=0;i= 0) { + pData[cp].askForWindingB = i; + cp = pData[cp].nextLinkedPoint; + } + } + SwapEdges(i,numberOfEdges()-1); + SubEdge(numberOfEdges()-1); +// SubEdge(i); + i--; + } else if ( ebData[i].pathID == cutPathID ) { + swdData[i].leW=swdData[i].leW%2; + swdData[i].riW=swdData[i].riW%2; + if ( swdData[i].leW < swdData[i].riW ) { + Inverse(i); + } + } + } + } else if ( mod == bool_op_slice ) { + // supprimer les aretes de la coupe + int i=numberOfEdges()-1; + for (;i>=0;i--) { + if ( ebData[i].pathID == cutPathID || getEdge(i).st < 0 || getEdge(i).en < 0 ) { + SubEdge(i); + } + } + } + else + { + for (int i = 0; i < numberOfEdges(); i++) + { + if (swdData[i].leW > 0 && swdData[i].riW <= 0) + { + eData[i].weight = 1; + } + else if (swdData[i].leW <= 0 && swdData[i].riW > 0) + { + Inverse (i); + eData[i].weight = 1; + } + else + { + eData[i].weight = 0; + SubEdge (i); + i--; + } + } + } + + delete sTree; + sTree = nullptr; + delete sEvts; + sEvts = nullptr; + + if ( mod == bool_op_cut ) { + // on garde le askForWinding + } else { + MakePointData (false); + } + MakeEdgeData (false); + MakeSweepSrcData (false); + MakeSweepDestData (false); + a->CleanupSweep (); + b->CleanupSweep (); + + if (directedEulerian(this) == false) + { +// printf( "pas euclidian2"); + _pts.clear(); + _aretes.clear(); + return shape_euler_err; + } + type = shape_polygon; + return 0; +} + +// frontend to the TesteIntersection() below +void Shape::TesteIntersection(SweepTree *t, Side s, bool onlyDiff) +{ + SweepTree *tt = static_cast(t->elem[s]); + if (tt == nullptr) { + return; + } + + SweepTree *a = (s == LEFT) ? tt : t; + SweepTree *b = (s == LEFT) ? t : tt; + + Geom::Point atx; + double atl; + double atr; + if (TesteIntersection(a, b, atx, atl, atr, onlyDiff)) { + sEvts->add(a, b, atx, atl, atr); + } +} + +// a crucial piece of code: computing intersections between segments +bool +Shape::TesteIntersection (SweepTree * iL, SweepTree * iR, Geom::Point &atx, double &atL, double &atR, bool onlyDiff) +{ + int lSt = iL->src->getEdge(iL->bord).st, lEn = iL->src->getEdge(iL->bord).en; + int rSt = iR->src->getEdge(iR->bord).st, rEn = iR->src->getEdge(iR->bord).en; + Geom::Point ldir, rdir; + ldir = iL->src->eData[iL->bord].rdx; + rdir = iR->src->eData[iR->bord].rdx; + // first, a round of checks to quickly dismiss edge which obviously dont intersect, + // such as having disjoint bounding boxes + if (lSt < lEn) + { + } + else + { + int swap = lSt; + lSt = lEn; + lEn = swap; + ldir = -ldir; + } + if (rSt < rEn) + { + } + else + { + int swap = rSt; + rSt = rEn; + rEn = swap; + rdir = -rdir; + } + + if (iL->src->pData[lSt].rx[0] < iL->src->pData[lEn].rx[0]) + { + if (iR->src->pData[rSt].rx[0] < iR->src->pData[rEn].rx[0]) + { + if (iL->src->pData[lSt].rx[0] > iR->src->pData[rEn].rx[0]) + return false; + if (iL->src->pData[lEn].rx[0] < iR->src->pData[rSt].rx[0]) + return false; + } + else + { + if (iL->src->pData[lSt].rx[0] > iR->src->pData[rSt].rx[0]) + return false; + if (iL->src->pData[lEn].rx[0] < iR->src->pData[rEn].rx[0]) + return false; + } + } + else + { + if (iR->src->pData[rSt].rx[0] < iR->src->pData[rEn].rx[0]) + { + if (iL->src->pData[lEn].rx[0] > iR->src->pData[rEn].rx[0]) + return false; + if (iL->src->pData[lSt].rx[0] < iR->src->pData[rSt].rx[0]) + return false; + } + else + { + if (iL->src->pData[lEn].rx[0] > iR->src->pData[rSt].rx[0]) + return false; + if (iL->src->pData[lSt].rx[0] < iR->src->pData[rEn].rx[0]) + return false; + } + } + + double ang = cross (ldir, rdir); +// ang*=iL->src->eData[iL->bord].isqlength; +// ang*=iR->src->eData[iR->bord].isqlength; + if (ang <= 0) return false; // edges in opposite directions: <-left ... right -> + // they can't intersect + + // d'abord tester les bords qui partent d'un meme point + if (iL->src == iR->src && lSt == rSt) + { + if (iL->src == iR->src && lEn == rEn) + return false; // c'est juste un doublon + atx = iL->src->pData[lSt].rx; + atR = atL = -1; + return true; // l'ordre est mauvais + } + if (iL->src == iR->src && lEn == rEn) + return false; // rien a faire=ils vont terminer au meme endroit + + // tester si on est dans une intersection multiple + + if (onlyDiff && iL->src == iR->src) + return false; + + // on reprend les vrais points + lSt = iL->src->getEdge(iL->bord).st; + lEn = iL->src->getEdge(iL->bord).en; + rSt = iR->src->getEdge(iR->bord).st; + rEn = iR->src->getEdge(iR->bord).en; + + // compute intersection (if there is one) + // Boissonat anr Preparata said in one paper that double precision floats were sufficient for get single precision + // coordinates for the intersection, if the endpoints are single precision. i hope they're right... + { + Geom::Point sDiff, eDiff; + double slDot, elDot; + double srDot, erDot; + sDiff = iL->src->pData[lSt].rx - iR->src->pData[rSt].rx; + eDiff = iL->src->pData[lEn].rx - iR->src->pData[rSt].rx; + srDot = cross(rdir, sDiff); + erDot = cross(rdir, eDiff); + sDiff = iR->src->pData[rSt].rx - iL->src->pData[lSt].rx; + eDiff = iR->src->pData[rEn].rx - iL->src->pData[lSt].rx; + slDot = cross(ldir, sDiff); + elDot = cross(ldir, eDiff); + + if ((srDot >= 0 && erDot >= 0) || (srDot <= 0 && erDot <= 0)) + { + if (srDot == 0) + { + if (lSt < lEn) + { + atx = iL->src->pData[lSt].rx; + atL = 0; + atR = slDot / (slDot - elDot); + return true; + } + else + { + return false; + } + } + else if (erDot == 0) + { + if (lSt > lEn) + { + atx = iL->src->pData[lEn].rx; + atL = 1; + atR = slDot / (slDot - elDot); + return true; + } + else + { + return false; + } + } + if (srDot > 0 && erDot > 0) + { + if (rEn < rSt) + { + if (srDot < erDot) + { + if (lSt < lEn) + { + atx = iL->src->pData[lSt].rx; + atL = 0; + atR = slDot / (slDot - elDot); + return true; + } + } + else + { + if (lEn < lSt) + { + atx = iL->src->pData[lEn].rx; + atL = 1; + atR = slDot / (slDot - elDot); + return true; + } + } + } + } + if (srDot < 0 && erDot < 0) + { + if (rEn > rSt) + { + if (srDot > erDot) + { + if (lSt < lEn) + { + atx = iL->src->pData[lSt].rx; + atL = 0; + atR = slDot / (slDot - elDot); + return true; + } + } + else + { + if (lEn < lSt) + { + atx = iL->src->pData[lEn].rx; + atL = 1; + atR = slDot / (slDot - elDot); + return true; + } + } + } + } + return false; + } + if ((slDot >= 0 && elDot >= 0) || (slDot <= 0 && elDot <= 0)) + { + if (slDot == 0) + { + if (rSt < rEn) + { + atx = iR->src->pData[rSt].rx; + atR = 0; + atL = srDot / (srDot - erDot); + return true; + } + else + { + return false; + } + } + else if (elDot == 0) + { + if (rSt > rEn) + { + atx = iR->src->pData[rEn].rx; + atR = 1; + atL = srDot / (srDot - erDot); + return true; + } + else + { + return false; + } + } + if (slDot > 0 && elDot > 0) + { + if (lEn > lSt) + { + if (slDot < elDot) + { + if (rSt < rEn) + { + atx = iR->src->pData[rSt].rx; + atR = 0; + atL = srDot / (srDot - erDot); + return true; + } + } + else + { + if (rEn < rSt) + { + atx = iR->src->pData[rEn].rx; + atR = 1; + atL = srDot / (srDot - erDot); + return true; + } + } + } + } + if (slDot < 0 && elDot < 0) + { + if (lEn < lSt) + { + if (slDot > elDot) + { + if (rSt < rEn) + { + atx = iR->src->pData[rSt].rx; + atR = 0; + atL = srDot / (srDot - erDot); + return true; + } + } + else + { + if (rEn < rSt) + { + atx = iR->src->pData[rEn].rx; + atR = 1; + atL = srDot / (srDot - erDot); + return true; + } + } + } + } + return false; + } + +/* double slb=slDot-elDot,srb=srDot-erDot; + if ( slb < 0 ) slb=-slb; + if ( srb < 0 ) srb=-srb;*/ + if (iL->src->eData[iL->bord].siEd > iR->src->eData[iR->bord].siEd) + { + atx = + (slDot * iR->src->pData[rEn].rx - + elDot * iR->src->pData[rSt].rx) / (slDot - elDot); + } + else + { + atx = + (srDot * iL->src->pData[lEn].rx - + erDot * iL->src->pData[lSt].rx) / (srDot - erDot); + } + atL = srDot / (srDot - erDot); + atR = slDot / (slDot - elDot); + return true; + } + + return true; +} + +int +Shape::PushIncidence (Shape * a, int cb, int pt, double theta) +{ + if (theta < 0 || theta > 1) + return -1; + + if (nbInc >= maxInc) + { + maxInc = 2 * nbInc + 1; + iData = + (incidenceData *) g_realloc(iData, maxInc * sizeof (incidenceData)); + } + int n = nbInc++; + iData[n].nextInc = a->swsData[cb].firstLinkedPoint; + iData[n].pt = pt; + iData[n].theta = theta; + a->swsData[cb].firstLinkedPoint = n; + return n; +} + +int +Shape::CreateIncidence (Shape * a, int no, int nPt) +{ + Geom::Point adir, diff; + adir = a->eData[no].rdx; + diff = getPoint(nPt).x - a->pData[a->getEdge(no).st].rx; + double t = dot (diff, adir); + t *= a->eData[no].ilength; + return PushIncidence (a, no, nPt, t); +} + +int +Shape::Winding (int nPt) const +{ + int askTo = pData[nPt].askForWindingB; + if (askTo < 0 || askTo >= numberOfEdges()) + return 0; + if (getEdge(askTo).st < getEdge(askTo).en) + { + return swdData[askTo].leW; + } + else + { + return swdData[askTo].riW; + } + return 0; +} + +int +Shape::Winding (const Geom::Point px) const +{ + int lr = 0, ll = 0, rr = 0; + + for (int i = 0; i < numberOfEdges(); i++) + { + Geom::Point adir, diff, ast, aen; + adir = eData[i].rdx; + + ast = pData[getEdge(i).st].rx; + aen = pData[getEdge(i).en].rx; + + int nWeight = eData[i].weight; + + if (ast[0] < aen[0]) + { + if (ast[0] > px[0]) + continue; + if (aen[0] < px[0]) + continue; + } + else + { + if (ast[0] < px[0]) + continue; + if (aen[0] > px[0]) + continue; + } + if (ast[0] == px[0]) + { + if (ast[1] >= px[1]) + continue; + if (aen[0] == px[0]) + continue; + if (aen[0] < px[0]) + ll += nWeight; + else + rr -= nWeight; + continue; + } + if (aen[0] == px[0]) + { + if (aen[1] >= px[1]) + continue; + if (ast[0] == px[0]) + continue; + if (ast[0] < px[0]) + ll -= nWeight; + else + rr += nWeight; + continue; + } + + if (ast[1] < aen[1]) + { + if (ast[1] >= px[1]) + continue; + } + else + { + if (aen[1] >= px[1]) + continue; + } + + diff = px - ast; + double cote = cross(adir, diff); + if (cote == 0) + continue; + if (cote < 0) + { + if (ast[0] > px[0]) + lr += nWeight; + } + else + { + if (ast[0] < px[0]) + lr -= nWeight; + } + } + return lr + (ll + rr) / 2; +} + +// merging duplicate points and edges +int +Shape::AssemblePoints (int st, int en) +{ + if (en > st) { + for (int i = st; i < en; i++) pData[i].oldInd = i; +// SortPoints(st,en-1); + SortPointsByOldInd (st, en - 1); // SortPointsByOldInd() is required here, because of the edges we have + // associated with the point for later computation of winding numbers. + // specifically, we need the first point we treated, it's the only one with a valid + // associated edge (man, that was a nice bug). + for (int i = st; i < en; i++) pData[pData[i].oldInd].newInd = i; + + int lastI = st; + for (int i = st; i < en; i++) { + pData[i].pending = lastI++; + if (i > st && getPoint(i - 1).x[0] == getPoint(i).x[0] && getPoint(i - 1).x[1] == getPoint(i).x[1]) { + pData[i].pending = pData[i - 1].pending; + if (pData[pData[i].pending].askForWindingS == nullptr) { + pData[pData[i].pending].askForWindingS = pData[i].askForWindingS; + pData[pData[i].pending].askForWindingB = pData[i].askForWindingB; + } else { + if (pData[pData[i].pending].askForWindingS == pData[i].askForWindingS + && pData[pData[i].pending].askForWindingB == pData[i].askForWindingB) { + // meme bord, c bon + } else { + // meme point, mais pas le meme bord: ouille! + // il faut prendre le bord le plus a gauche + // en pratique, n'arrive que si 2 maxima sont dans la meme case -> le mauvais choix prend une arete incidente + // au bon choix +// printf("doh"); + } + } + lastI--; + } else { + if (i > pData[i].pending) { + _pts[pData[i].pending].x = getPoint(i).x; + pData[pData[i].pending].rx = getPoint(i).x; + pData[pData[i].pending].askForWindingS = pData[i].askForWindingS; + pData[pData[i].pending].askForWindingB = pData[i].askForWindingB; + } + } + } + for (int i = st; i < en; i++) pData[i].newInd = pData[pData[i].newInd].pending; + return lastI; + } + return en; +} + +void +Shape::AssemblePoints (Shape * a) +{ + if (hasPoints()) + { + int lastI = AssemblePoints (0, numberOfPoints()); + + for (int i = 0; i < a->numberOfEdges(); i++) + { + a->swsData[i].stPt = pData[a->swsData[i].stPt].newInd; + a->swsData[i].enPt = pData[a->swsData[i].enPt].newInd; + } + for (int i = 0; i < nbInc; i++) + iData[i].pt = pData[iData[i].pt].newInd; + + _pts.resize(lastI); + } +} +void +Shape::AssembleAretes (FillRule directed) +{ + if ( directed == fill_justDont && _has_back_data == false ) { + directed=fill_nonZero; + } + + for (int i = 0; i < numberOfPoints(); i++) { + if (getPoint(i).totalDegree() == 2) { + int cb, cc; + cb = getPoint(i).incidentEdge[FIRST]; + cc = getPoint(i).incidentEdge[LAST]; + bool doublon=false; + if ((getEdge(cb).st == getEdge(cc).st && getEdge(cb).en == getEdge(cc).en) + || (getEdge(cb).st == getEdge(cc).en && getEdge(cb).en == getEdge(cc).en)) doublon=true; + if ( directed == fill_justDont ) { + if ( doublon ) { + if ( ebData[cb].pathID > ebData[cc].pathID ) { + cc = getPoint(i).incidentEdge[FIRST]; // on swappe pour enlever cc + cb = getPoint(i).incidentEdge[LAST]; + } else if ( ebData[cb].pathID == ebData[cc].pathID ) { + if ( ebData[cb].pieceID > ebData[cc].pieceID ) { + cc = getPoint(i).incidentEdge[FIRST]; // on swappe pour enlever cc + cb = getPoint(i).incidentEdge[LAST]; + } else if ( ebData[cb].pieceID == ebData[cc].pieceID ) { + if ( ebData[cb].tSt > ebData[cc].tSt ) { + cc = getPoint(i).incidentEdge[FIRST]; // on swappe pour enlever cc + cb = getPoint(i).incidentEdge[LAST]; + } + } + } + } + if ( doublon ) eData[cc].weight = 0; + } else { + } + if ( doublon ) { + if (getEdge(cb).st == getEdge(cc).st) { + eData[cb].weight += eData[cc].weight; + } else { + eData[cb].weight -= eData[cc].weight; + } + eData[cc].weight = 0; + + if (swsData[cc].firstLinkedPoint >= 0) { + int cp = swsData[cc].firstLinkedPoint; + while (cp >= 0) { + pData[cp].askForWindingB = cb; + cp = pData[cp].nextLinkedPoint; + } + if (swsData[cb].firstLinkedPoint < 0) { + swsData[cb].firstLinkedPoint = swsData[cc].firstLinkedPoint; + } else { + int ncp = swsData[cb].firstLinkedPoint; + while (pData[ncp].nextLinkedPoint >= 0) { + ncp = pData[ncp].nextLinkedPoint; + } + pData[ncp].nextLinkedPoint = swsData[cc].firstLinkedPoint; + } + } + + DisconnectStart (cc); + DisconnectEnd (cc); + if (numberOfEdges() > 1) { + int cp = swsData[numberOfEdges() - 1].firstLinkedPoint; + while (cp >= 0) { + pData[cp].askForWindingB = cc; + cp = pData[cp].nextLinkedPoint; + } + } + SwapEdges (cc, numberOfEdges() - 1); + if (cb == numberOfEdges() - 1) { + cb = cc; + } + _aretes.pop_back(); + } + } else { + int cb; + cb = getPoint(i).incidentEdge[FIRST]; + while (cb >= 0 && cb < numberOfEdges()) { + int other = Other (i, cb); + int cc; + cc = getPoint(i).incidentEdge[FIRST]; + while (cc >= 0 && cc < numberOfEdges()) { + int ncc = NextAt (i, cc); + bool doublon=false; + if (cc != cb && Other (i, cc) == other ) doublon=true; + if ( directed == fill_justDont ) { + if ( doublon ) { + if ( ebData[cb].pathID > ebData[cc].pathID ) { + doublon=false; + } else if ( ebData[cb].pathID == ebData[cc].pathID ) { + if ( ebData[cb].pieceID > ebData[cc].pieceID ) { + doublon=false; + } else if ( ebData[cb].pieceID == ebData[cc].pieceID ) { + if ( ebData[cb].tSt > ebData[cc].tSt ) { + doublon=false; + } + } + } + } + if ( doublon ) eData[cc].weight = 0; + } else { + } + if ( doublon ) { +// if (cc != cb && Other (i, cc) == other) { + // doublon + if (getEdge(cb).st == getEdge(cc).st) { + eData[cb].weight += eData[cc].weight; + } else { + eData[cb].weight -= eData[cc].weight; + } + eData[cc].weight = 0; + + if (swsData[cc].firstLinkedPoint >= 0) { + int cp = swsData[cc].firstLinkedPoint; + while (cp >= 0) { + pData[cp].askForWindingB = cb; + cp = pData[cp].nextLinkedPoint; + } + if (swsData[cb].firstLinkedPoint < 0) { + swsData[cb].firstLinkedPoint = swsData[cc].firstLinkedPoint; + } else { + int ncp = swsData[cb].firstLinkedPoint; + while (pData[ncp].nextLinkedPoint >= 0) { + ncp = pData[ncp].nextLinkedPoint; + } + pData[ncp].nextLinkedPoint = swsData[cc].firstLinkedPoint; + } + } + + DisconnectStart (cc); + DisconnectEnd (cc); + if (numberOfEdges() > 1) { + int cp = swsData[numberOfEdges() - 1].firstLinkedPoint; + while (cp >= 0) { + pData[cp].askForWindingB = cc; + cp = pData[cp].nextLinkedPoint; + } + } + SwapEdges (cc, numberOfEdges() - 1); + if (cb == numberOfEdges() - 1) { + cb = cc; + } + if (ncc == numberOfEdges() - 1) { + ncc = cc; + } + _aretes.pop_back(); + } + cc = ncc; + } + cb = NextAt (i, cb); + } + } + } + + if ( directed == fill_justDont ) { + for (int i = 0; i < numberOfEdges(); i++) { + if (eData[i].weight == 0) { +// SubEdge(i); + // i--; + } else { + if (eData[i].weight < 0) Inverse (i); + } + } + } else { + for (int i = 0; i < numberOfEdges(); i++) { + if (eData[i].weight == 0) { + // SubEdge(i); + // i--; + } else { + if (eData[i].weight < 0) Inverse (i); + } + } + } +} +void +Shape::GetWindings (Shape * /*a*/, Shape * /*b*/, BooleanOp /*mod*/, bool brutal) +{ + // preparation du parcours + for (int i = 0; i < numberOfEdges(); i++) + { + swdData[i].misc = nullptr; + swdData[i].precParc = swdData[i].suivParc = -1; + } + + // chainage + SortEdges (); + + int searchInd = 0; + + int lastPtUsed = 0; + do + { + int startBord = -1; + int outsideW = 0; + { + int fi = 0; + for (fi = lastPtUsed; fi < numberOfPoints(); fi++) + { + if (getPoint(fi).incidentEdge[FIRST] >= 0 && swdData[getPoint(fi).incidentEdge[FIRST]].misc == nullptr) + break; + } + lastPtUsed = fi + 1; + if (fi < numberOfPoints()) + { + int bestB = getPoint(fi).incidentEdge[FIRST]; + if (bestB >= 0) + { + startBord = bestB; + if (fi == 0) + { + outsideW = 0; + } + else + { + if (brutal) + { + outsideW = Winding (getPoint(fi).x); + } + else + { + outsideW = Winding (fi); + } + } + if ( getPoint(fi).totalDegree() == 1 ) { + if ( fi == getEdge(startBord).en ) { + if ( eData[startBord].weight == 0 ) { + // on se contente d'inverser + Inverse(startBord); + } else { + // on passe le askForWinding (sinon ca va rester startBord) + pData[getEdge(startBord).st].askForWindingB=pData[getEdge(startBord).en].askForWindingB; + } + } + } + if (getEdge(startBord).en == fi) + outsideW += eData[startBord].weight; + } + } + } + if (startBord >= 0) + { + // parcours en profondeur pour mettre les leF et riF a leurs valeurs + swdData[startBord].misc = (void *) 1; + swdData[startBord].leW = outsideW; + swdData[startBord].riW = outsideW - eData[startBord].weight; +// if ( doDebug ) printf("part de %d\n",startBord); + int curBord = startBord; + bool curDir = true; + swdData[curBord].precParc = -1; + swdData[curBord].suivParc = -1; + do + { + int cPt; + if (curDir) + cPt = getEdge(curBord).en; + else + cPt = getEdge(curBord).st; + int nb = curBord; +// if ( doDebug ) printf("de curBord= %d avec leF= %d et riF= %d -> ",curBord,swdData[curBord].leW,swdData[curBord].riW); + do + { + int nnb = -1; + if (getEdge(nb).en == cPt) + { + outsideW = swdData[nb].riW; + nnb = CyclePrevAt (cPt, nb); + } + else + { + outsideW = swdData[nb].leW; + nnb = CyclePrevAt (cPt, nb); + } + if (nnb == nb) + { + // cul-de-sac + nb = -1; + break; + } + nb = nnb; + } + while (nb >= 0 && nb != curBord && swdData[nb].misc != nullptr); + if (nb < 0 || nb == curBord) + { + // retour en arriere + int oPt; + if (curDir) + oPt = getEdge(curBord).st; + else + oPt = getEdge(curBord).en; + curBord = swdData[curBord].precParc; +// if ( doDebug ) printf("retour vers %d\n",curBord); + if (curBord < 0) + break; + if (oPt == getEdge(curBord).en) + curDir = true; + else + curDir = false; + } + else + { + swdData[nb].misc = (void *) 1; + swdData[nb].ind = searchInd++; + if (cPt == getEdge(nb).st) + { + swdData[nb].riW = outsideW; + swdData[nb].leW = outsideW + eData[nb].weight; + } + else + { + swdData[nb].leW = outsideW; + swdData[nb].riW = outsideW - eData[nb].weight; + } + swdData[nb].precParc = curBord; + swdData[curBord].suivParc = nb; + curBord = nb; +// if ( doDebug ) printf("suite %d\n",curBord); + if (cPt == getEdge(nb).en) + curDir = false; + else + curDir = true; + } + } + while (true /*swdData[curBord].precParc >= 0 */ ); + // fin du cas non-oriente + } + } + while (lastPtUsed < numberOfPoints()); +// fflush(stdout); +} + +bool +Shape::TesteIntersection (Shape * ils, Shape * irs, int ilb, int irb, + Geom::Point &atx, double &atL, double &atR, + bool /*onlyDiff*/) +{ + int lSt = ils->getEdge(ilb).st, lEn = ils->getEdge(ilb).en; + int rSt = irs->getEdge(irb).st, rEn = irs->getEdge(irb).en; + if (lSt == rSt || lSt == rEn) + { + return false; + } + if (lEn == rSt || lEn == rEn) + { + return false; + } + + Geom::Point ldir, rdir; + ldir = ils->eData[ilb].rdx; + rdir = irs->eData[irb].rdx; + + double il = ils->pData[lSt].rx[0], it = ils->pData[lSt].rx[1], ir = + ils->pData[lEn].rx[0], ib = ils->pData[lEn].rx[1]; + if (il > ir) + { + double swf = il; + il = ir; + ir = swf; + } + if (it > ib) + { + double swf = it; + it = ib; + ib = swf; + } + double jl = irs->pData[rSt].rx[0], jt = irs->pData[rSt].rx[1], jr = + irs->pData[rEn].rx[0], jb = irs->pData[rEn].rx[1]; + if (jl > jr) + { + double swf = jl; + jl = jr; + jr = swf; + } + if (jt > jb) + { + double swf = jt; + jt = jb; + jb = swf; + } + + if (il > jr || it > jb || ir < jl || ib < jt) + return false; + + // pre-test + { + Geom::Point sDiff, eDiff; + double slDot, elDot; + double srDot, erDot; + sDiff = ils->pData[lSt].rx - irs->pData[rSt].rx; + eDiff = ils->pData[lEn].rx - irs->pData[rSt].rx; + srDot = cross(rdir, sDiff); + erDot = cross(rdir, eDiff); + if ((srDot >= 0 && erDot >= 0) || (srDot <= 0 && erDot <= 0)) + return false; + + sDiff = irs->pData[rSt].rx - ils->pData[lSt].rx; + eDiff = irs->pData[rEn].rx - ils->pData[lSt].rx; + slDot = cross(ldir, sDiff); + elDot = cross(ldir, eDiff); + if ((slDot >= 0 && elDot >= 0) || (slDot <= 0 && elDot <= 0)) + return false; + + double slb = slDot - elDot, srb = srDot - erDot; + if (slb < 0) + slb = -slb; + if (srb < 0) + srb = -srb; + if (slb > srb) + { + atx = + (slDot * irs->pData[rEn].rx - elDot * irs->pData[rSt].rx) / (slDot - + elDot); + } + else + { + atx = + (srDot * ils->pData[lEn].rx - erDot * ils->pData[lSt].rx) / (srDot - + erDot); + } + atL = srDot / (srDot - erDot); + atR = slDot / (slDot - elDot); + return true; + } + + // a mettre en double precision pour des resultats exacts + Geom::Point usvs; + usvs = irs->pData[rSt].rx - ils->pData[lSt].rx; + + // pas sur de l'ordre des coefs de m + Geom::Affine m(ldir[0], ldir[1], + rdir[0], rdir[1], + 0, 0); + double det = m.det(); + + double tdet = det * ils->eData[ilb].isqlength * irs->eData[irb].isqlength; + + if (tdet > -0.0001 && tdet < 0.0001) + { // ces couillons de vecteurs sont colineaires + Geom::Point sDiff, eDiff; + double sDot, eDot; + sDiff = ils->pData[lSt].rx - irs->pData[rSt].rx; + eDiff = ils->pData[lEn].rx - irs->pData[rSt].rx; + sDot = cross(rdir, sDiff); + eDot = cross(rdir, eDiff); + + atx = + (sDot * irs->pData[lEn].rx - eDot * irs->pData[lSt].rx) / (sDot - + eDot); + atL = sDot / (sDot - eDot); + + sDiff = irs->pData[rSt].rx - ils->pData[lSt].rx; + eDiff = irs->pData[rEn].rx - ils->pData[lSt].rx; + sDot = cross(ldir, sDiff); + eDot = cross(ldir, eDiff); + + atR = sDot / (sDot - eDot); + + return true; + } + + // plus de colinearite ni d'extremites en commun + m[1] = -m[1]; + m[2] = -m[2]; + { + double swap = m[0]; + m[0] = m[3]; + m[3] = swap; + } + + atL = (m[0]* usvs[0] + m[1] * usvs[1]) / det; + atR = -(m[2] * usvs[0] + m[3] * usvs[1]) / det; + atx = ils->pData[lSt].rx + atL * ldir; + + + return true; +} + +bool +Shape::TesteAdjacency (Shape * a, int no, const Geom::Point atx, int nPt, + bool push) +{ + if (nPt == a->swsData[no].stPt || nPt == a->swsData[no].enPt) + return false; + + Geom::Point adir, diff, ast, aen, diff1, diff2, diff3, diff4; + + ast = a->pData[a->getEdge(no).st].rx; + aen = a->pData[a->getEdge(no).en].rx; + + adir = a->eData[no].rdx; + + double sle = a->eData[no].length; + double ile = a->eData[no].ilength; + + diff = atx - ast; + + double e = IHalfRound(cross(adir, diff) * a->eData[no].isqlength); + if (-3 < e && e < 3) + { + double rad = HalfRound (0.501); // when using single precision, 0.505 is better (0.5 would be the correct value, + // but it produces lots of bugs) + diff1[0] = diff[0] - rad; + diff1[1] = diff[1] - rad; + diff2[0] = diff[0] + rad; + diff2[1] = diff[1] - rad; + diff3[0] = diff[0] + rad; + diff3[1] = diff[1] + rad; + diff4[0] = diff[0] - rad; + diff4[1] = diff[1] + rad; + double di1, di2; + bool adjacent = false; + di1 = cross(adir, diff1); + di2 = cross(adir, diff3); + if ((di1 < 0 && di2 > 0) || (di1 > 0 && di2 < 0)) + { + adjacent = true; + } + else + { + di1 = cross(adir, diff2); + di2 = cross(adir, diff4); + if ((di1 < 0 && di2 > 0) || (di1 > 0 && di2 < 0)) + { + adjacent = true; + } + } + if (adjacent) + { + double t = dot (diff, adir); + if (t > 0 && t < sle) + { + if (push) + { + t *= ile; + PushIncidence (a, no, nPt, t); + } + return true; + } + } + } + return false; +} + +void +Shape::CheckAdjacencies (int lastPointNo, int lastChgtPt, Shape * /*shapeHead*/, + int /*edgeHead*/) +{ + for (auto & chgt : chgts) + { + int chLeN = chgt.ptNo; + int chRiN = chgt.ptNo; + if (chgt.src) + { + Shape *lS = chgt.src; + int lB = chgt.bord; + int lftN = lS->swsData[lB].leftRnd; + int rgtN = lS->swsData[lB].rightRnd; + if (lftN < chLeN) + chLeN = lftN; + if (rgtN > chRiN) + chRiN = rgtN; +// for (int n=lftN;n<=rgtN;n++) CreateIncidence(lS,lB,n); + for (int n = lftN - 1; n >= lastChgtPt; n--) + { + if (TesteAdjacency (lS, lB, getPoint(n).x, n, false) == + false) + break; + lS->swsData[lB].leftRnd = n; + } + for (int n = rgtN + 1; n < lastPointNo; n++) + { + if (TesteAdjacency (lS, lB, getPoint(n).x, n, false) == + false) + break; + lS->swsData[lB].rightRnd = n; + } + } + if (chgt.osrc) + { + Shape *rS = chgt.osrc; + int rB = chgt.obord; + int lftN = rS->swsData[rB].leftRnd; + int rgtN = rS->swsData[rB].rightRnd; + if (lftN < chLeN) + chLeN = lftN; + if (rgtN > chRiN) + chRiN = rgtN; +// for (int n=lftN;n<=rgtN;n++) CreateIncidence(rS,rB,n); + for (int n = lftN - 1; n >= lastChgtPt; n--) + { + if (TesteAdjacency (rS, rB, getPoint(n).x, n, false) == + false) + break; + rS->swsData[rB].leftRnd = n; + } + for (int n = rgtN + 1; n < lastPointNo; n++) + { + if (TesteAdjacency (rS, rB, getPoint(n).x, n, false) == + false) + break; + rS->swsData[rB].rightRnd = n; + } + } + if (chgt.lSrc) + { + if (chgt.lSrc->swsData[chgt.lBrd].leftRnd < lastChgtPt) + { + Shape *nSrc = chgt.lSrc; + int nBrd = chgt.lBrd /*,nNo=chgts[cCh].ptNo */ ; + bool hit; + + do + { + hit = false; + for (int n = chRiN; n >= chLeN; n--) + { + if (TesteAdjacency + (nSrc, nBrd, getPoint(n).x, n, false)) + { + if (nSrc->swsData[nBrd].leftRnd < lastChgtPt) + { + nSrc->swsData[nBrd].leftRnd = n; + nSrc->swsData[nBrd].rightRnd = n; + } + else + { + if (n < nSrc->swsData[nBrd].leftRnd) + nSrc->swsData[nBrd].leftRnd = n; + if (n > nSrc->swsData[nBrd].rightRnd) + nSrc->swsData[nBrd].rightRnd = n; + } + hit = true; + } + } + for (int n = chLeN - 1; n >= lastChgtPt; n--) + { + if (TesteAdjacency + (nSrc, nBrd, getPoint(n).x, n, false) == false) + break; + if (nSrc->swsData[nBrd].leftRnd < lastChgtPt) + { + nSrc->swsData[nBrd].leftRnd = n; + nSrc->swsData[nBrd].rightRnd = n; + } + else + { + if (n < nSrc->swsData[nBrd].leftRnd) + nSrc->swsData[nBrd].leftRnd = n; + if (n > nSrc->swsData[nBrd].rightRnd) + nSrc->swsData[nBrd].rightRnd = n; + } + hit = true; + } + if (hit) + { + SweepTree *node = + static_cast < SweepTree * >(nSrc->swsData[nBrd].misc); + if (node == nullptr) + break; + node = static_cast < SweepTree * >(node->elem[LEFT]); + if (node == nullptr) + break; + nSrc = node->src; + nBrd = node->bord; + if (nSrc->swsData[nBrd].leftRnd >= lastChgtPt) + break; + } + } + while (hit); + + } + } + if (chgt.rSrc) + { + if (chgt.rSrc->swsData[chgt.rBrd].leftRnd < lastChgtPt) + { + Shape *nSrc = chgt.rSrc; + int nBrd = chgt.rBrd /*,nNo=chgts[cCh].ptNo */ ; + bool hit; + do + { + hit = false; + for (int n = chLeN; n <= chRiN; n++) + { + if (TesteAdjacency + (nSrc, nBrd, getPoint(n).x, n, false)) + { + if (nSrc->swsData[nBrd].leftRnd < lastChgtPt) + { + nSrc->swsData[nBrd].leftRnd = n; + nSrc->swsData[nBrd].rightRnd = n; + } + else + { + if (n < nSrc->swsData[nBrd].leftRnd) + nSrc->swsData[nBrd].leftRnd = n; + if (n > nSrc->swsData[nBrd].rightRnd) + nSrc->swsData[nBrd].rightRnd = n; + } + hit = true; + } + } + for (int n = chRiN + 1; n < lastPointNo; n++) + { + if (TesteAdjacency + (nSrc, nBrd, getPoint(n).x, n, false) == false) + break; + if (nSrc->swsData[nBrd].leftRnd < lastChgtPt) + { + nSrc->swsData[nBrd].leftRnd = n; + nSrc->swsData[nBrd].rightRnd = n; + } + else + { + if (n < nSrc->swsData[nBrd].leftRnd) + nSrc->swsData[nBrd].leftRnd = n; + if (n > nSrc->swsData[nBrd].rightRnd) + nSrc->swsData[nBrd].rightRnd = n; + } + hit = true; + } + if (hit) + { + SweepTree *node = + static_cast < SweepTree * >(nSrc->swsData[nBrd].misc); + if (node == nullptr) + break; + node = static_cast < SweepTree * >(node->elem[RIGHT]); + if (node == nullptr) + break; + nSrc = node->src; + nBrd = node->bord; + if (nSrc->swsData[nBrd].leftRnd >= lastChgtPt) + break; + } + } + while (hit); + } + } + } +} + + +void Shape::AddChgt(int lastPointNo, int lastChgtPt, Shape * &shapeHead, + int &edgeHead, sTreeChangeType type, Shape * lS, int lB, Shape * rS, + int rB) +{ + sTreeChange c; + c.ptNo = lastPointNo; + c.type = type; + c.src = lS; + c.bord = lB; + c.osrc = rS; + c.obord = rB; + chgts.push_back(c); + const int nCh = chgts.size() - 1; + + /* FIXME: this looks like a cut and paste job */ + + if (lS) { + SweepTree *lE = static_cast < SweepTree * >(lS->swsData[lB].misc); + if (lE && lE->elem[LEFT]) { + SweepTree *llE = static_cast < SweepTree * >(lE->elem[LEFT]); + chgts[nCh].lSrc = llE->src; + chgts[nCh].lBrd = llE->bord; + } else { + chgts[nCh].lSrc = nullptr; + chgts[nCh].lBrd = -1; + } + + if (lS->swsData[lB].leftRnd < lastChgtPt) { + lS->swsData[lB].leftRnd = lastPointNo; + lS->swsData[lB].nextSh = shapeHead; + lS->swsData[lB].nextBo = edgeHead; + edgeHead = lB; + shapeHead = lS; + } else { + int old = lS->swsData[lB].leftRnd; + if (getPoint(old).x[0] > getPoint(lastPointNo).x[0]) { + lS->swsData[lB].leftRnd = lastPointNo; + } + } + if (lS->swsData[lB].rightRnd < lastChgtPt) { + lS->swsData[lB].rightRnd = lastPointNo; + } else { + int old = lS->swsData[lB].rightRnd; + if (getPoint(old).x[0] < getPoint(lastPointNo).x[0]) + lS->swsData[lB].rightRnd = lastPointNo; + } + } + + if (rS) { + SweepTree *rE = static_cast < SweepTree * >(rS->swsData[rB].misc); + if (rE->elem[RIGHT]) { + SweepTree *rrE = static_cast < SweepTree * >(rE->elem[RIGHT]); + chgts[nCh].rSrc = rrE->src; + chgts[nCh].rBrd = rrE->bord; + } else { + chgts[nCh].rSrc = nullptr; + chgts[nCh].rBrd = -1; + } + + if (rS->swsData[rB].leftRnd < lastChgtPt) { + rS->swsData[rB].leftRnd = lastPointNo; + rS->swsData[rB].nextSh = shapeHead; + rS->swsData[rB].nextBo = edgeHead; + edgeHead = rB; + shapeHead = rS; + } else { + int old = rS->swsData[rB].leftRnd; + if (getPoint(old).x[0] > getPoint(lastPointNo).x[0]) { + rS->swsData[rB].leftRnd = lastPointNo; + } + } + if (rS->swsData[rB].rightRnd < lastChgtPt) { + rS->swsData[rB].rightRnd = lastPointNo; + } else { + int old = rS->swsData[rB].rightRnd; + if (getPoint(old).x[0] < getPoint(lastPointNo).x[0]) + rS->swsData[rB].rightRnd = lastPointNo; + } + } else { + SweepTree *lE = static_cast < SweepTree * >(lS->swsData[lB].misc); + if (lE && lE->elem[RIGHT]) { + SweepTree *rlE = static_cast < SweepTree * >(lE->elem[RIGHT]); + chgts[nCh].rSrc = rlE->src; + chgts[nCh].rBrd = rlE->bord; + } else { + chgts[nCh].rSrc = nullptr; + chgts[nCh].rBrd = -1; + } + } +} + +// is this a debug function? It's calling localized "printf" ... +void +Shape::Validate () +{ + for (int i = 0; i < numberOfPoints(); i++) + { + pData[i].rx = getPoint(i).x; + } + for (int i = 0; i < numberOfEdges(); i++) + { + eData[i].rdx = getEdge(i).dx; + } + for (int i = 0; i < numberOfEdges(); i++) + { + for (int j = i + 1; j < numberOfEdges(); j++) + { + Geom::Point atx; + double atL, atR; + if (TesteIntersection (this, this, i, j, atx, atL, atR, false)) + { + printf ("%i %i %f %f di=%f %f dj=%f %f\n", i, j, atx[0],atx[1],getEdge(i).dx[0],getEdge(i).dx[1],getEdge(j).dx[0],getEdge(j).dx[1]); + } + } + } + fflush (stdout); +} + +void +Shape::CheckEdges (int lastPointNo, int lastChgtPt, Shape * a, Shape * b, + BooleanOp mod) +{ + + for (auto & chgt : chgts) + { + if (chgt.type == 0) + { + Shape *lS = chgt.src; + int lB = chgt.bord; + lS->swsData[lB].curPoint = chgt.ptNo; + } + } + for (auto & chgt : chgts) + { +// int chLeN=chgts[cCh].ptNo; +// int chRiN=chgts[cCh].ptNo; + if (chgt.src) + { + Shape *lS = chgt.src; + int lB = chgt.bord; + Avance (lastPointNo, lastChgtPt, lS, lB, a, b, mod); + } + if (chgt.osrc) + { + Shape *rS = chgt.osrc; + int rB = chgt.obord; + Avance (lastPointNo, lastChgtPt, rS, rB, a, b, mod); + } + if (chgt.lSrc) + { + Shape *nSrc = chgt.lSrc; + int nBrd = chgt.lBrd; + while (nSrc->swsData[nBrd].leftRnd >= + lastChgtPt /*&& nSrc->swsData[nBrd].doneTo < lastChgtPt */ ) + { + Avance (lastPointNo, lastChgtPt, nSrc, nBrd, a, b, mod); + + SweepTree *node = + static_cast < SweepTree * >(nSrc->swsData[nBrd].misc); + if (node == nullptr) + break; + node = static_cast < SweepTree * >(node->elem[LEFT]); + if (node == nullptr) + break; + nSrc = node->src; + nBrd = node->bord; + } + } + if (chgt.rSrc) + { + Shape *nSrc = chgt.rSrc; + int nBrd = chgt.rBrd; + while (nSrc->swsData[nBrd].rightRnd >= + lastChgtPt /*&& nSrc->swsData[nBrd].doneTo < lastChgtPt */ ) + { + Avance (lastPointNo, lastChgtPt, nSrc, nBrd, a, b, mod); + + SweepTree *node = + static_cast < SweepTree * >(nSrc->swsData[nBrd].misc); + if (node == nullptr) + break; + node = static_cast < SweepTree * >(node->elem[RIGHT]); + if (node == nullptr) + break; + nSrc = node->src; + nBrd = node->bord; + } + } + } +} + +void +Shape::Avance (int lastPointNo, int lastChgtPt, Shape * lS, int lB, Shape * /*a*/, + Shape * b, BooleanOp mod) +{ + double dd = HalfRound (1); + bool avoidDiag = false; +// if ( lastChgtPt > 0 && pts[lastChgtPt-1].y+dd == pts[lastChgtPt].y ) avoidDiag=true; + + bool direct = true; + if (lS == b && (mod == bool_op_diff || mod == bool_op_symdiff)) + direct = false; + int lftN = lS->swsData[lB].leftRnd; + int rgtN = lS->swsData[lB].rightRnd; + if (lS->swsData[lB].doneTo < lastChgtPt) + { + int lp = lS->swsData[lB].curPoint; + if (lp >= 0 && getPoint(lp).x[1] + dd == getPoint(lastChgtPt).x[1]) + avoidDiag = true; + if (lS->eData[lB].rdx[1] == 0) + { + // tjs de gauche a droite et pas de diagonale + if (lS->eData[lB].rdx[0] >= 0) + { + for (int p = lftN; p <= rgtN; p++) + { + DoEdgeTo (lS, lB, p, direct, true); + lp = p; + } + } + else + { + for (int p = lftN; p <= rgtN; p++) + { + DoEdgeTo (lS, lB, p, direct, false); + lp = p; + } + } + } + else if (lS->eData[lB].rdx[1] > 0) + { + if (lS->eData[lB].rdx[0] >= 0) + { + + for (int p = lftN; p <= rgtN; p++) + { + if (avoidDiag && p == lftN && getPoint(lftN).x[0] == getPoint(lp).x[0] + dd) + { + if (lftN > 0 && lftN - 1 >= lastChgtPt + && getPoint(lftN - 1).x[0] == getPoint(lp).x[0]) + { + DoEdgeTo (lS, lB, lftN - 1, direct, true); + DoEdgeTo (lS, lB, lftN, direct, true); + } + else + { + DoEdgeTo (lS, lB, lftN, direct, true); + } + } + else + { + DoEdgeTo (lS, lB, p, direct, true); + } + lp = p; + } + } + else + { + + for (int p = rgtN; p >= lftN; p--) + { + if (avoidDiag && p == rgtN && getPoint(rgtN).x[0] == getPoint(lp).x[0] - dd) + { + if (rgtN < numberOfPoints() && rgtN + 1 < lastPointNo + && getPoint(rgtN + 1).x[0] == getPoint(lp).x[0]) + { + DoEdgeTo (lS, lB, rgtN + 1, direct, true); + DoEdgeTo (lS, lB, rgtN, direct, true); + } + else + { + DoEdgeTo (lS, lB, rgtN, direct, true); + } + } + else + { + DoEdgeTo (lS, lB, p, direct, true); + } + lp = p; + } + } + } + else + { + if (lS->eData[lB].rdx[0] >= 0) + { + + for (int p = rgtN; p >= lftN; p--) + { + if (avoidDiag && p == rgtN && getPoint(rgtN).x[0] == getPoint(lp).x[0] - dd) + { + if (rgtN < numberOfPoints() && rgtN + 1 < lastPointNo + && getPoint(rgtN + 1).x[0] == getPoint(lp).x[0]) + { + DoEdgeTo (lS, lB, rgtN + 1, direct, false); + DoEdgeTo (lS, lB, rgtN, direct, false); + } + else + { + DoEdgeTo (lS, lB, rgtN, direct, false); + } + } + else + { + DoEdgeTo (lS, lB, p, direct, false); + } + lp = p; + } + } + else + { + + for (int p = lftN; p <= rgtN; p++) + { + if (avoidDiag && p == lftN && getPoint(lftN).x[0] == getPoint(lp).x[0] + dd) + { + if (lftN > 0 && lftN - 1 >= lastChgtPt + && getPoint(lftN - 1).x[0] == getPoint(lp).x[0]) + { + DoEdgeTo (lS, lB, lftN - 1, direct, false); + DoEdgeTo (lS, lB, lftN, direct, false); + } + else + { + DoEdgeTo (lS, lB, lftN, direct, false); + } + } + else + { + DoEdgeTo (lS, lB, p, direct, false); + } + lp = p; + } + } + } + lS->swsData[lB].curPoint = lp; + } + lS->swsData[lB].doneTo = lastPointNo - 1; +} + +void +Shape::DoEdgeTo (Shape * iS, int iB, int iTo, bool direct, bool sens) +{ + int lp = iS->swsData[iB].curPoint; + int ne = -1; + if (sens) + { + if (direct) + ne = AddEdge (lp, iTo); + else + ne = AddEdge (iTo, lp); + } + else + { + if (direct) + ne = AddEdge (iTo, lp); + else + ne = AddEdge (lp, iTo); + } + if (ne >= 0 && _has_back_data) + { + ebData[ne].pathID = iS->ebData[iB].pathID; + ebData[ne].pieceID = iS->ebData[iB].pieceID; + if (iS->eData[iB].length < 0.00001) + { + ebData[ne].tSt = ebData[ne].tEn = iS->ebData[iB].tSt; + } + else + { + double bdl = iS->eData[iB].ilength; + Geom::Point bpx = iS->pData[iS->getEdge(iB).st].rx; + Geom::Point bdx = iS->eData[iB].rdx; + Geom::Point psx = getPoint(getEdge(ne).st).x; + Geom::Point pex = getPoint(getEdge(ne).en).x; + Geom::Point psbx=psx-bpx; + Geom::Point pebx=pex-bpx; + double pst = dot(psbx,bdx) * bdl; + double pet = dot(pebx,bdx) * bdl; + pst = iS->ebData[iB].tSt * (1 - pst) + iS->ebData[iB].tEn * pst; + pet = iS->ebData[iB].tSt * (1 - pet) + iS->ebData[iB].tEn * pet; + ebData[ne].tEn = pet; + ebData[ne].tSt = pst; + } + } + iS->swsData[iB].curPoint = iTo; + if (ne >= 0) + { + int cp = iS->swsData[iB].firstLinkedPoint; + swsData[ne].firstLinkedPoint = iS->swsData[iB].firstLinkedPoint; + while (cp >= 0) + { + pData[cp].askForWindingB = ne; + cp = pData[cp].nextLinkedPoint; + } + iS->swsData[iB].firstLinkedPoint = -1; + } +} diff --git a/src/livarot/float-line.cpp b/src/livarot/float-line.cpp new file mode 100644 index 0000000..9a19729 --- /dev/null +++ b/src/livarot/float-line.cpp @@ -0,0 +1,916 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Implementation of coverage with floating-point values. + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef faster_flatten +# include // std::abs(float) +#endif +#include +#include "livarot/float-line.h" +#include "livarot/int-line.h" +#include + +FloatLigne::FloatLigne() +{ + s_first = s_last = -1; +} + + +FloatLigne::~FloatLigne() += default; + +/// Reset the line to empty (boundaries and runs). +void FloatLigne::Reset() +{ + bords.clear(); + runs.clear(); + s_first = s_last = -1; +} + +/** + * Add a coverage portion. + * + * \param guess Position from where we should try to insert the first + * boundary, or -1 if we don't have a clue. + */ +int FloatLigne::AddBord(float spos, float sval, float epos, float eval, int guess) +{ +// if ( showCopy ) printf("b= %f %f -> %f %f \n",spos,sval,epos,eval); + if ( spos >= epos ) { + return -1; + } + + float pente = (eval - sval) / (epos - spos); + +#ifdef faster_flatten + if ( std::abs(epos - spos) < 0.001 || std::abs(pente) > 1000 ) { + return -1; + epos = spos; + pente = 0; + } +#endif + + if ( guess >= int(bords.size()) ) { + guess = -1; + } + + // add the left boundary + float_ligne_bord b; + int n = bords.size(); + b.pos = spos; + b.val = sval; + b.start = true; + b.other = n + 1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + // insert it in the doubly-linked list + InsertBord(n, spos, guess); + + // add the right boundary + n = bords.size(); + b.pos = epos; + b.val = eval; + b.start = false; + b.other = n-1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + // insert it in the doubly-linked list, knowing that boundary at index n-1 is not too far before me + InsertBord(n, epos, n - 1); + + return n; +} + +/** + * Add a coverage portion. + * + * \param guess Position from where we should try to insert the first + * boundary, or -1 if we don't have a clue. + */ +int FloatLigne::AddBord(float spos, float sval, float epos, float eval, float pente, int guess) +{ +// if ( showCopy ) printf("b= %f %f -> %f %f \n",spos,sval,epos,eval); + if ( spos >= epos ) { + return -1; + } + +#ifdef faster_flatten + if ( std::abs(epos - spos) < 0.001 || std::abs(pente) > 1000 ) { + return -1; + epos = spos; + pente = 0; + } +#endif + + if ( guess >= int(bords.size()) ) { + guess=-1; + } + + float_ligne_bord b; + int n = bords.size(); + b.pos = spos; + b.val = sval; + b.start = true; + b.other = n + 1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + n = bords.size(); + b.pos = epos; + b.val = eval; + b.start = false; + b.other = n - 1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + InsertBord(n - 1, spos, guess); + InsertBord(n, epos, n - 1); +/* if ( bords[n-1].s_next < 0 ) { + bords[n].s_next=-1; + s_last=n; + + bords[n].s_prev=n-1; + bords[n-1].s_next=n; + } else if ( bords[bords[n-1].s_next].pos >= epos ) { + bords[n].s_next=bords[n-1].s_next; + bords[bords[n].s_next].s_prev=n; + + bords[n].s_prev=n-1; + bords[n-1].s_next=n; + } else { + int c=bords[bords[n-1].s_next].s_next; + while ( c >= 0 && bords[c].pos < epos ) c=bords[c].s_next; + if ( c < 0 ) { + bords[n].s_prev=s_last; + bords[s_last].s_next=n; + s_last=n; + } else { + bords[n].s_prev=bords[c].s_prev; + bords[bords[n].s_prev].s_next=n; + + bords[n].s_next=c; + bords[c].s_prev=n; + } + + }*/ + return n; +} + +/** + * Add a coverage portion. + * + * \param guess Position from where we should try to insert the last + * boundary, or -1 if we don't have a clue. + */ +int FloatLigne::AddBordR(float spos, float sval, float epos, float eval, float pente, int guess) +{ +// if ( showCopy ) printf("br= %f %f -> %f %f \n",spos,sval,epos,eval); +// return AddBord(spos,sval,epos,eval,pente,guess); + if ( spos >= epos ){ + return -1; + } + +#ifdef faster_flatten + if ( std::abs(epos - spos) < 0.001 || std::abs(pente) > 1000 ) { + return -1; + epos = spos; + pente = 0; + } +#endif + + if ( guess >= int(bords.size()) ) { + guess=-1; + } + + float_ligne_bord b; + int n = bords.size(); + b.pos = spos; + b.val = sval; + b.start = true; + b.other = n + 1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + n = bords.size(); + b.pos = epos; + b.val = eval; + b.start = false; + b.other = n - 1; + b.pente = pente; + b.s_prev = b.s_next = -1; + bords.push_back(b); + + InsertBord(n, epos, guess); + InsertBord(n - 1, spos, n); + +/* if ( bords[n].s_prev < 0 ) { + bords[n-1].s_prev=-1; + s_first=n-1; + + bords[n-1].s_next=n; + bords[n].s_prev=n-1; + } else if ( bords[bords[n].s_prev].pos <= spos ) { + bords[n-1].s_prev=bords[n].s_prev; + bords[bords[n-1].s_prev].s_next=n-1; + + bords[n-1].s_next=n; + bords[n].s_prev=n-1; + } else { + int c=bords[bords[n].s_prev].s_prev; + while ( c >= 0 && bords[c].pos > spos ) c=bords[c].s_prev; + if ( c < 0 ) { + bords[n-1].s_next=s_first; + bords[s_first].s_prev=n-1; + s_first=n-1; + } else { + bords[n-1].s_next=bords[c].s_next; + bords[bords[n-1].s_next].s_prev=n-1; + + bords[n-1].s_prev=c; + bords[c].s_next=n-1; + } + + }*/ + return n - 1; +} + +/** + * Add a coverage portion by appending boundaries at the end of the list. + * + * This works because we know they are on the right. + */ +int FloatLigne::AppendBord(float spos, float sval, float epos, float eval, float pente) +{ +// if ( showCopy ) printf("b= %f %f -> %f %f \n",spos,sval,epos,eval); +// return AddBord(spos,sval,epos,eval,pente,s_last); + if ( spos >= epos ) { + return -1; + } + +#ifdef faster_flatten + if ( std::abs(epos - spos) < 0.001 || std::abs(pente) > 1000 ) { + return -1; + epos = spos; + pente = 0; + } +#endif + + int n = bords.size(); + float_ligne_bord b; + b.pos = spos; + b.val = sval; + b.start = true; + b.other = n + 1; + b.pente = pente; + b.s_prev = s_last; + b.s_next = n + 1; + bords.push_back(b); + + if ( s_last >= 0 ) { + bords[s_last].s_next = n; + } + + if ( s_first < 0 ) { + s_first = n; + } + + n = bords.size(); + b.pos = epos; + b.val = eval; + b.start = false; + b.other = n - 1; + b.pente = pente; + b.s_prev = n - 1; + b.s_next = -1; + bords.push_back(b); + + s_last = n; + + return n; +} + + + +// insertion in a boubly-linked list. nothing interesting here +void FloatLigne::InsertBord(int no, float /*p*/, int guess) +{ +// TODO check if ignoring p is bad + if ( no < 0 || no >= int(bords.size()) ) { + return; + } + + if ( s_first < 0 ) { + s_first = s_last = no; + bords[no].s_prev = -1; + bords[no].s_next = -1; + return; + } + + if ( guess < 0 || guess >= int(bords.size()) ) { + int c = s_first; + while ( c >= 0 && c < int(bords.size()) && CmpBord(bords[c], bords[no]) < 0 ) { + c = bords[c].s_next; + } + + if ( c < 0 || c >= int(bords.size()) ) { + bords[no].s_prev = s_last; + bords[s_last].s_next = no; + s_last = no; + } else { + bords[no].s_prev = bords[c].s_prev; + if ( bords[no].s_prev >= 0 ) { + bords[bords[no].s_prev].s_next = no; + } else { + s_first = no; + } + bords[no].s_next = c; + bords[c].s_prev = no; + } + } else { + int c = guess; + int stTst = CmpBord(bords[c], bords[no]); + + if ( stTst == 0 ) { + + bords[no].s_prev = bords[c].s_prev; + if ( bords[no].s_prev >= 0 ) { + bords[bords[no].s_prev].s_next = no; + } else { + s_first = no; + } + bords[no].s_next = c; + bords[c].s_prev = no; + + } else if ( stTst > 0 ) { + + while ( c >= 0 && c < int(bords.size()) && CmpBord(bords[c], bords[no]) > 0 ) { + c = bords[c].s_prev; + } + + if ( c < 0 || c >= int(bords.size()) ) { + bords[no].s_next = s_first; + bords[s_first].s_prev =no; // s_first != -1 + s_first = no; + } else { + bords[no].s_next = bords[c].s_next; + if ( bords[no].s_next >= 0 ) { + bords[bords[no].s_next].s_prev = no; + } else { + s_last = no; + } + bords[no].s_prev = c; + bords[c].s_next = no; + } + + } else { + + while ( c >= 0 && c < int(bords.size()) && CmpBord(bords[c],bords[no]) < 0 ) { + c = bords[c].s_next; + } + + if ( c < 0 || c >= int(bords.size()) ) { + bords[no].s_prev = s_last; + bords[s_last].s_next = no; + s_last = no; + } else { + bords[no].s_prev = bords[c].s_prev; + if ( bords[no].s_prev >= 0 ) { + bords[bords[no].s_prev].s_next = no; + } else { + s_first = no; + } + bords[no].s_next = c; + bords[c].s_prev = no; + } + } + } +} + +/** + * Computes the sum of the coverages of the runs currently being scanned, + * of which there are "pending". + */ +float FloatLigne::RemainingValAt(float at, int pending) +{ + float sum = 0; +/* int no=firstAc; + while ( no >= 0 && no < bords.size() ) { + int nn=bords[no].other; + sum+=bords[nn].val+(at-bords[nn].pos)*bords[nn].pente; +// sum+=((at-bords[nn].pos)*bords[no].val+(bords[no].pos-at)*bords[nn].val)/(bords[no].pos-bords[nn].pos); +// sum+=ValAt(at,bords[nn].pos,bords[no].pos,bords[nn].val,bords[no].val); + no=bords[no].next; + }*/ + // for each portion being scanned, compute coverage at position "at" and sum. + // we could simply compute the sum of portion coverages as a "f(x)=ux+y" and evaluate it at "x=at", + // but there are numerical problems with this approach, and it produces ugly lines of incorrectly + // computed alpha values, so i reverted to this "safe but slow" version + + for (int i=0; i < pending; i++) { + int const nn = bords[i].pend_ind; + sum += bords[nn].val + (at - bords[nn].pos) * bords[nn].pente; + } + + return sum; +} + + +/** + * Extract a set of non-overlapping runs from the boundaries. + * + * We scan the boundaries left to right, maintaining a set of coverage + * portions currently being scanned. For each such portion, the function + * will add the index of its first boundary in an array; but instead of + * allocating another array, it uses a field in float_ligne_bord: pend_ind. + * The outcome is that an array of float_ligne_run is produced. + */ +void FloatLigne::Flatten() +{ + if ( int(bords.size()) <= 1 ) { + Reset(); + return; + } + + runs.clear(); + +// qsort(bords,bords.size(),sizeof(float_ligne_bord),FloatLigne::CmpBord); +// SortBords(0,bords.size()-1); + + float totPente = 0; + float totStart = 0; + float totX = bords[0].pos; + + bool startExists = false; + float lastStart = 0; + float lastVal = 0; + int pending = 0; + +// for (int i=0;i=0 && i < int(bords.size()) ;) { + + float cur = bords[i].pos; // position of the current boundary (there may be several boundaries at this position) + float leftV = 0; // deltas in coverage value at this position + float rightV = 0; + float leftP = 0; // deltas in coverage increase per unit length at this position + float rightP = 0; + + // more precisely, leftV is the sum of decreases of coverage value, + // while rightV is the sum of increases, so that leftV+rightV is the delta. + // idem for leftP and rightP + + // start by scanning all boundaries that end a portion at this position + while ( i >= 0 && i < int(bords.size()) && bords[i].pos == cur && bords[i].start == false ) { + leftV += bords[i].val; + leftP += bords[i].pente; + +#ifndef faster_flatten + // we need to remove the boundary that started this coverage portion for the pending list + if ( bords[i].other >= 0 && bords[i].other < int(bords.size()) ) { + // so we use the pend_inv "array" + int const k = bords[bords[i].other].pend_inv; + if ( k >= 0 && k < pending ) { + // and update the pend_ind array and its inverse pend_inv + bords[k].pend_ind = bords[pending - 1].pend_ind; + bords[bords[k].pend_ind].pend_inv = k; + } + } +#endif + + // one less portion pending + pending--; + // and we move to the next boundary in the doubly linked list + i=bords[i].s_next; + //i++; + } + + // then scan all boundaries that start a portion at this position + while ( i >= 0 && i < int(bords.size()) && bords[i].pos == cur && bords[i].start ) { + rightV += bords[i].val; + rightP += bords[i].pente; +#ifndef faster_flatten + bords[pending].pend_ind=i; + bords[i].pend_inv=pending; +#endif + pending++; + i = bords[i].s_next; + //i++; + } + + // coverage value at end of the run will be "start coverage"+"delta per unit length"*"length" + totStart = totStart + totPente * (cur - totX); + + if ( startExists ) { + // add that run + AddRun(lastStart, cur, lastVal, totStart, totPente); + } + // update "delta coverage per unit length" + totPente += rightP - leftP; + // not really needed here + totStart += rightV - leftV; + // update position + totX = cur; + if ( pending > 0 ) { + startExists = true; + +#ifndef faster_flatten + // to avoid accumulation of numerical errors, we compute an accurate coverage for this position "cur" + totStart = RemainingValAt(cur, pending); +#endif + lastVal = totStart; + lastStart = cur; + } else { + startExists = false; + totStart = 0; + totPente = 0; + } + } +} + + +/// Debug dump of the instance. +void FloatLigne::Affiche() +{ + printf("%lu : \n", (long unsigned int) bords.size()); + for (auto & bord : bords) { + printf("(%f %f %f %i) ",bord.pos,bord.val,bord.pente,(bord.start?1:0)); // localization ok + } + + printf("\n"); + printf("%lu : \n", (long unsigned int) runs.size()); + + for (auto & run : runs) { + printf("(%f %f -> %f %f / %f)", + run.st, run.vst, run.en, run.ven, run.pente); // localization ok + } + + printf("\n"); +} + + +int FloatLigne::AddRun(float st, float en, float vst, float ven) +{ + return AddRun(st, en, vst, ven, (ven - vst) / (en - st)); +} + + +int FloatLigne::AddRun(float st, float en, float vst, float ven, float pente) +{ + if ( st >= en ) { + return -1; + } + + int const n = runs.size(); + float_ligne_run r; + r.st = st; + r.en = en; + r.vst = vst; + r.ven = ven; + r.pente = pente; + runs.push_back(r); + + return n; +} + +void FloatLigne::Copy(FloatLigne *a) +{ + if ( a->runs.empty() ) { + Reset(); + return; + } + + bords.clear(); + runs = a->runs; +} + +void FloatLigne::Copy(IntLigne *a) +{ + if ( a->nbRun ) { + Reset(); + return; + } + + bords.clear(); + runs.resize(a->nbRun); + + for (int i = 0; i < int(runs.size()); i++) { + runs[i].st = a->runs[i].st; + runs[i].en = a->runs[i].en; + runs[i].vst = a->runs[i].vst; + runs[i].ven = a->runs[i].ven; + } +} + +/// Cuts the parts having less than tresh coverage. +void FloatLigne::Min(FloatLigne *a, float tresh, bool addIt) +{ + Reset(); + if ( a->runs.empty() ) { + return; + } + + bool startExists = false; + float lastStart=0; + float lastEnd = 0; + + for (auto runA : a->runs) { + if ( runA.vst <= tresh ) { + if ( runA.ven <= tresh ) { + if ( startExists ) { + if ( lastEnd >= runA.st - 0.00001 ) { + lastEnd = runA.en; + } else { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + lastStart = runA.st; + lastEnd = runA.en; + } + } else { + lastStart = runA.st; + lastEnd = runA.en; + } + startExists = true; + } else { + float cutPos = (runA.st * (tresh - runA.ven) + runA.en * (runA.vst - tresh)) / (runA.vst - runA.ven); + if ( startExists ) { + if ( lastEnd >= runA.st - 0.00001 ) { + if ( addIt ) { + AddRun(lastStart, cutPos, tresh, tresh); + } + AddRun(cutPos,runA.en, tresh, runA.ven); + } else { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + if ( addIt ) { + AddRun(runA.st, cutPos, tresh, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } + } else { + if ( addIt ) { + AddRun(runA.st, cutPos, tresh, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } + startExists = false; + } + + } else { + + if ( runA.ven <= tresh ) { + float cutPos = (runA.st * (runA.ven - tresh) + runA.en * (tresh - runA.vst)) / (runA.ven - runA.vst); + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + } + AddRun(runA.st, cutPos, runA.vst, tresh); + startExists = true; + lastStart = cutPos; + lastEnd = runA.en; + } else { + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + } + startExists = false; + AddRun(runA.st, runA.en, runA.vst, runA.ven); + } + } + } + + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + } +} + +/** + * Cuts the coverage a in 2 parts. + * + * over will receive the parts where coverage > tresh, while the present + * FloatLigne will receive the parts where coverage <= tresh. + */ +void FloatLigne::Split(FloatLigne *a, float tresh, FloatLigne *over) +{ + Reset(); + if ( a->runs.empty() ) { + return; + } + + for (auto runA : a->runs) { + if ( runA.vst >= tresh ) { + if ( runA.ven >= tresh ) { + if ( over ) { + over->AddRun(runA.st, runA.en, runA.vst, runA.ven); + } + } else { + float cutPos = (runA.st * (tresh - runA.ven) + runA.en * (runA.vst - tresh)) / (runA.vst - runA.ven); + if ( over ) { + over->AddRun(runA.st, cutPos, runA.vst, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } + } else { + if ( runA.ven >= tresh ) { + float cutPos = (runA.st * (runA.ven - tresh) + runA.en * (tresh-runA.vst)) / (runA.ven - runA.vst); + AddRun(runA.st, cutPos, runA.vst, tresh); + if ( over ) { + over->AddRun(cutPos, runA.en, tresh, runA.ven); + } + } else { + AddRun(runA.st, runA.en, runA.vst, runA.ven); + } + } + } +} + +/** + * Clips the coverage runs to tresh. + * + * If addIt is false, it only leaves the parts that are not entirely under + * tresh. If addIt is true, it's the coverage clamped to tresh. + */ +void FloatLigne::Max(FloatLigne *a, float tresh, bool addIt) +{ + Reset(); + if ( a->runs.empty() <= 0 ) { + return; + } + + bool startExists = false; + float lastStart = 0; + float lastEnd = 0; + for (auto runA : a->runs) { + if ( runA.vst >= tresh ) { + if ( runA.ven >= tresh ) { + if ( startExists ) { + if ( lastEnd >= runA.st-0.00001 ) { + lastEnd = runA.en; + } else { + if ( addIt ) { + AddRun(lastStart,lastEnd,tresh,tresh); + } + lastStart = runA.st; + lastEnd = runA.en; + } + } else { + lastStart = runA.st; + lastEnd = runA.en; + } + startExists = true; + } else { + float cutPos = (runA.st * (tresh - runA.ven) + runA.en * (runA.vst - tresh)) / (runA.vst - runA.ven); + if ( startExists ) { + if ( lastEnd >= runA.st-0.00001 ) { + if ( addIt ) { + AddRun(lastStart, cutPos, tresh, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } else { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + if ( addIt ) { + AddRun(runA.st, cutPos, tresh, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } + } else { + if ( addIt ) { + AddRun(runA.st, cutPos, tresh, tresh); + } + AddRun(cutPos, runA.en, tresh, runA.ven); + } + startExists = false; + } + + } else { + + if ( runA.ven >= tresh ) { + float cutPos = (runA.st * (runA.ven - tresh) + runA.en * (tresh - runA.vst)) / (runA.ven - runA.vst); + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart,lastEnd,tresh,tresh); + } + } + AddRun(runA.st, cutPos, runA.vst, tresh); + startExists = true; + lastStart = cutPos; + lastEnd = runA.en; + } else { + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart,lastEnd,tresh,tresh); + } + } + startExists = false; + AddRun(runA.st, runA.en, runA.vst, runA.ven); + } + } + } + + if ( startExists ) { + if ( addIt ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + } +} + +/// Extract the parts where coverage > tresh. +void FloatLigne::Over(FloatLigne *a, float tresh) +{ + Reset(); + if ( a->runs.empty() ) { + return; + } + + bool startExists = false; + float lastStart = 0; + float lastEnd = 0; + + for (auto runA : a->runs) { + if ( runA.vst >= tresh ) { + if ( runA.ven >= tresh ) { + if ( startExists ) { + if ( lastEnd >= runA.st - 0.00001 ) { + lastEnd = runA.en; + } else { + AddRun(lastStart, lastEnd, tresh, tresh); + lastStart = runA.st; + lastEnd = runA.en; + } + } else { + lastStart = runA.st; + lastEnd = runA.en; + } + startExists = true; + + } else { + + float cutPos = (runA.st * (tresh - runA.ven) + runA.en * (runA.vst - tresh)) / (runA.vst - runA.ven); + if ( startExists ) { + if ( lastEnd >= runA.st - 0.00001 ) { + AddRun(lastStart, cutPos, tresh, tresh); + } else { + AddRun(lastStart, lastEnd, tresh, tresh); + AddRun(runA.st, cutPos, tresh, tresh); + } + } else { + AddRun(runA.st, cutPos, tresh, tresh); + } + startExists = false; + } + + } else { + if ( runA.ven >= tresh ) { + float cutPos = (runA.st * (runA.ven - tresh) + runA.en * (tresh - runA.vst)) / (runA.ven - runA.vst); + if ( startExists ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + startExists = true; + lastStart = cutPos; + lastEnd = runA.en; + } else { + if ( startExists ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } + startExists = false; + } + } + } + + if ( startExists ) { + AddRun(lastStart, lastEnd, tresh, tresh); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/float-line.h b/src/livarot/float-line.h new file mode 100644 index 0000000..8d7faf1 --- /dev/null +++ b/src/livarot/float-line.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_LIVAROT_FLOAT_LINE_H +#define INKSCAPE_LIVAROT_FLOAT_LINE_H + +/** \file + * Coverage with floating-point boundaries. + */ + +#include +#include "livarot/LivarotDefs.h" + +class IntLigne; +class BitLigne; + +/// A coverage portion ("run") with floating point boundaries. +struct float_ligne_run { + float st; + float en; + float vst; + float ven; + float pente; ///< (ven-vst)/(en-st) +}; + +/** + * A floating-point boundary. + * + * Each float_ligne_bord is a boundary of some coverage. + * The Flatten() function will extract non-overlapping runs and produce an + * array of float_ligne_run. The float_ligne_bord are stored in an array, but + * linked like a doubly-linked list. + * + * The idea behind that is that a given edge produces one float_ligne_bord at + * the beginning of Scan() and possibly another in AvanceEdge() and + * DestroyEdge(); but that second float_ligne_bord will not be far away in + * the list from the first, so it's faster to salvage the index of the first + * float_ligne_bord and try to insert the second from that salvaged position. + */ +struct float_ligne_bord { + float pos; ///< position of the boundary + bool start; ///< is the beginning of the coverage portion? + float val; ///< amount of coverage (ie vst if start==true, and ven if start==false) + float pente; ///< (ven-vst)/(en-st) + int other; ///< index, in the array of float_ligne_bord, of the other boundary associated to this one + int s_prev; ///< index of the previous bord in the doubly-linked list + int s_next; ///< index of the next bord in the doubly-linked list + int pend_ind; ///< bords[i].pend_ind is the index of the float_ligne_bord that is the start of the + ///< coverage portion being scanned (in the Flatten() ) + int pend_inv; ///< inverse of pend_ind, for faster handling of insertion/removal in the "pending" array +}; + +/** + * Coverage with floating-point boundaries. + * + * The goal is to salvage exact coverage info in the sweepline performed by + * Scan() or QuickScan(), then clean up a bit, convert floating point bounds + * to integer bounds, because pixel have integer bounds, and then raster runs + * of the type: + * \verbatim + position on the (pixel) line: st en + | | + coverage value (0=empty, 1=full) vst -> ven \endverbatim + */ +class FloatLigne { +public: + std::vector bords; ///< vector of coverage boundaries + std::vector runs; ///< vector of runs + + /// first boundary in the doubly-linked list + int s_first; + /// last boundary in the doubly-linked list + int s_last; + + FloatLigne(); + virtual ~FloatLigne(); + + void Reset(); + + int AddBord(float spos, float sval, float epos, float eval, int guess = -1); + int AddBord(float spos, float sval, float epos, float eval, float pente, int guess = -1); + int AddBordR(float spos, float sval, float epos, float eval, float pente, int guess = -1); + int AppendBord(float spos, float sval, float epos, float eval, float pente); + + void Flatten(); + + void Affiche(); + + void Max(FloatLigne *a, float tresh, bool addIt); + + void Min(FloatLigne *a, float tresh, bool addIt); + + void Split(FloatLigne *a, float tresh, FloatLigne *over); + + void Over(FloatLigne *a, float tresh); + + void Copy(IntLigne *a); + void Copy(FloatLigne *a); + + float RemainingValAt(float at, int pending); + + static int CmpBord(float_ligne_bord const &d1, float_ligne_bord const &d2) { + if ( d1.pos == d2.pos ) { + if ( d1.start && !(d2.start) ) { + return 1; + } + if ( !(d1.start) && d2.start ) { + return -1; + } + return 0; + } + + return (( d1.pos < d2.pos ) ? -1 : 1); + }; + + int AddRun(float st, float en, float vst, float ven, float pente); + +private: + void InsertBord(int no, float p, int guess); + int AddRun(float st, float en, float vst, float ven); + + inline float ValAt(float at, float ps, float pe, float vs, float ve) { + return ((at - ps) * ve + (pe - at) * vs) / (pe - ps); + }; +}; + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/int-line.cpp b/src/livarot/int-line.cpp new file mode 100644 index 0000000..ff87475 --- /dev/null +++ b/src/livarot/int-line.cpp @@ -0,0 +1,1071 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Implementation of coverage with integer boundaries. + *//* + * Authors: + * see git history + * Fred + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include +#include +#include +#include "livarot/int-line.h" +#include "livarot/float-line.h" +#include "livarot/BitLigne.h" + +IntLigne::IntLigne() +{ + nbBord = maxBord = 0; + bords = nullptr; + + nbRun = maxRun = 0; + runs = nullptr; + + firstAc = lastAc = -1; +} + + +IntLigne::~IntLigne() +{ + if ( maxBord > 0 ) { + g_free(bords); + nbBord = maxBord = 0; + bords = nullptr; + } + if ( maxRun > 0 ) { + g_free(runs); + nbRun = maxRun = 0; + runs = nullptr; + } +} + +void IntLigne::Reset() +{ + nbBord = 0; + nbRun = 0; + firstAc = lastAc = -1; +} + +int IntLigne::AddBord(int spos, float sval, int epos, float eval) +{ + if ( nbBord + 1 >= maxBord ) { + maxBord = 2 * nbBord + 2; + bords = (int_ligne_bord *) g_realloc(bords, maxBord * sizeof(int_ligne_bord)); + + } + + int n = nbBord++; + bords[n].pos = spos; + bords[n].val = sval; + bords[n].start = true; + bords[n].other = n+1; + bords[n].prev = bords[n].next = -1; + + n = nbBord++; + bords[n].pos = epos; + bords[n].val = eval; + bords[n].start = false; + bords[n].other = n-1; + bords[n].prev = bords[n].next = -1; + + return n - 1; +} + + +float IntLigne::RemainingValAt(int at) +{ + int no = firstAc; + float sum = 0; + while ( no >= 0 ) { + int nn = bords[no].other; + sum += ValAt(at, bords[nn].pos, bords[no].pos, bords[nn].val, bords[no].val); + no = bords[no].next; + } + return sum; +} + +void IntLigne::Flatten() +{ + if ( nbBord <= 1 ) { + Reset(); + return; + } + + nbRun = 0; + firstAc = lastAc = -1; + + for (int i = 0; i < nbBord; i++) { + bords[i].prev = i; + } + + qsort(bords, nbBord, sizeof(int_ligne_bord), IntLigne::CmpBord); + for (int i = 0; i < nbBord; i++) { + bords[bords[i].prev].next = i; + } + + for (int i = 0; i < nbBord; i++) { + bords[i].other = bords[bords[i].other].next; + } + + int lastStart = 0; + float lastVal = 0; + bool startExists = false; + + for (int i = 0; i < nbBord; ) { + int cur = bords[i].pos; + float leftV = 0; + float rightV = 0; + float midV = 0; + while ( i < nbBord && bords[i].pos == cur && bords[i].start == false ) { + Dequeue(i); + leftV += bords[i].val; + i++; + } + midV = RemainingValAt(cur); + while ( i < nbBord && bords[i].pos == cur && bords[i].start ) { + rightV += bords[i].val; + Enqueue(bords[i].other); + i++; + } + + if ( startExists ) { + AddRun(lastStart, cur, lastVal, leftV + midV); + } + if ( firstAc >= 0 ) { + startExists = true; + lastVal = midV + rightV; + lastStart = cur; + } else { + startExists = false; + } + } +} + + +void IntLigne::Affiche() +{ + printf("%i : \n", nbRun); + for (int i = 0; i < nbRun;i++) { + printf("(%i %f -> %i %f) ", runs[i].st, runs[i].vst, runs[i].en, runs[i].ven); // localization ok + } + printf("\n"); +} + +int IntLigne::AddRun(int st, int en, float vst, float ven) +{ + if ( st >= en ) { + return -1; + } + + if ( nbRun >= maxRun ) { + maxRun = 2 * nbRun + 1; + runs = (int_ligne_run *) g_realloc(runs, maxRun * sizeof(int_ligne_run)); + } + + int n = nbRun++; + runs[n].st = st; + runs[n].en = en; + runs[n].vst = vst; + runs[n].ven = ven; + return n; +} + +void IntLigne::Booleen(IntLigne *a, IntLigne *b, BooleanOp mod) +{ + Reset(); + if ( a->nbRun <= 0 && b->nbRun <= 0 ) { + return; + } + + if ( a->nbRun <= 0 ) { + if ( mod == bool_op_union || mod == bool_op_symdiff ) { + Copy(b); + } + return; + } + + if ( b->nbRun <= 0 ) { + if ( mod == bool_op_union || mod == bool_op_diff || mod == bool_op_symdiff ) { + Copy(a); + } + return; + } + + int curA = 0; + int curB = 0; + int curPos = (a->runs[0].st < b->runs[0].st) ? a->runs[0].st : b->runs[0].st; + int nextPos = curPos; + float valA = 0; + float valB = 0; + if ( curPos == a->runs[0].st ) { + valA = a->runs[0].vst; + } + if ( curPos == b->runs[0].st ) { + valB = b->runs[0].vst; + } + + while ( curA < a->nbRun && curB < b->nbRun ) { + int_ligne_run runA = a->runs[curA]; + int_ligne_run runB = b->runs[curB]; + const bool inA = ( curPos >= runA.st && curPos < runA.en ); + const bool inB = ( curPos >= runB.st && curPos < runB.en ); + + bool startA = false; + bool startB = false; + bool endA = false; + bool endB = false; + + if ( curPos < runA.st ) { + if ( curPos < runB.st ) { + startA = runA.st <= runB.st; + startB = runA.st >= runB.st; + nextPos = startA ? runA.st : runB.st; + } else if ( curPos >= runB.st ) { + startA = runA.st <= runB.en; + endB = runA.st >= runB.en; + nextPos = startA ? runA.st : runB.en; + } + } else if ( curPos == runA.st ) { + if ( curPos < runB.st ) { + endA = runA.en <= runB.st; + startB = runA.en >= runB.st; + nextPos = startB ? runB.en : runA.st; + } else if ( curPos == runB.st ) { + endA = runA.en <= runB.en; + endB = runA.en >= runB.en; + nextPos = endA? runA.en : runB.en; + } else { + endA = runA.en <= runB.en; + endB = runA.en >= runB.en; + nextPos = endA ? runA.en : runB.en; + } + } else { + if ( curPos < runB.st ) { + endA = runA.en <= runB.st; + startB = runA.en >= runB.st; + nextPos = startB ? runB.st : runA.en; + } else if ( curPos == runB.st ) { + endA = runA.en <= runB.en; + endB = runA.en >= runB.en; + nextPos = endA ? runA.en : runB.en; + } else { + endA = runA.en <= runB.en; + endB = runA.en >= runB.en; + nextPos = endA ? runA.en : runB.en; + } + } + + float oValA = valA; + float oValB = valB; + valA = inA ? ValAt(nextPos, runA.st, runA.en, runA.vst, runA.ven) : 0; + valB = inB ? ValAt(nextPos, runB.st, runB.en, runB.vst, runB.ven) : 0; + + if ( mod == bool_op_union ) { + + if ( inA || inB ) { + AddRun(curPos, nextPos, oValA + oValB, valA + valB); + } + + } else if ( mod == bool_op_inters ) { + + if ( inA && inB ) { + AddRun(curPos, nextPos, oValA * oValB, valA * valB); + } + + } else if ( mod == bool_op_diff ) { + + if ( inA ) { + AddRun(curPos, nextPos, oValA - oValB, valA - valB); + } + + } else if ( mod == bool_op_symdiff ) { + if ( inA && !(inB) ) { + AddRun(curPos, nextPos, oValA - oValB, valA - valB); + } + if ( !(inA) && inB ) { + AddRun(curPos, nextPos, oValB - oValA, valB - valA); + } + } + + curPos = nextPos; + if ( startA ) { + // inA=true; these are never used + valA = runA.vst; + } + if ( startB ) { + //inB=true; + valB = runB.vst; + } + if ( endA ) { + //inA=false; + valA = 0; + curA++; + if ( curA < a->nbRun && a->runs[curA].st == curPos ) { + valA = a->runs[curA].vst; + } + } + if ( endB ) { + //inB=false; + valB = 0; + curB++; + if ( curB < b->nbRun && b->runs[curB].st == curPos ) { + valB = b->runs[curB].vst; + } + } + } + + while ( curA < a->nbRun ) { + int_ligne_run runA = a->runs[curA]; + const bool inA = ( curPos >= runA.st && curPos < runA.en ); + const bool inB = false; + + bool startA = false; + bool endA = false; + if ( curPos < runA.st ) { + nextPos = runA.st; + startA = true; + } else if ( curPos >= runA.st ) { + nextPos = runA.en; + endA = true; + } + + float oValA = valA; + float oValB = valB; + valA = inA ? ValAt(nextPos,runA.st, runA.en, runA.vst, runA.ven) : 0; + valB = 0; + + if ( mod == bool_op_union ) { + if ( inA || inB ) { + AddRun(curPos, nextPos, oValA + oValB, valA + valB); + } + } else if ( mod == bool_op_inters ) { + if ( inA && inB ) { + AddRun(curPos, nextPos, oValA * oValB, valA * valB); + } + } else if ( mod == bool_op_diff ) { + if ( inA ) { + AddRun(curPos, nextPos, oValA - oValB, valA - valB); + } + } else if ( mod == bool_op_symdiff ) { + if ( inA && !(inB) ) { + AddRun(curPos, nextPos, oValA - oValB, valA - valB); + } + if ( !(inA) && inB ) { + AddRun(curPos,nextPos,oValB-oValA,valB-valA); + } + } + + curPos = nextPos; + if ( startA ) { + //inA=true; + valA = runA.vst; + } + if ( endA ) { + //inA=false; + valA = 0; + curA++; + if ( curA < a->nbRun && a->runs[curA].st == curPos ) { + valA = a->runs[curA].vst; + } + } + } + + while ( curB < b->nbRun ) { + int_ligne_run runB = b->runs[curB]; + const bool inB = ( curPos >= runB.st && curPos < runB.en ); + const bool inA = false; + + bool startB = false; + bool endB = false; + if ( curPos < runB.st ) { + nextPos = runB.st; + startB = true; + } else if ( curPos >= runB.st ) { + nextPos = runB.en; + endB = true; + } + + float oValA = valA; + float oValB = valB; + valB = inB ? ValAt(nextPos, runB.st, runB.en, runB.vst, runB.ven) : 0; + valA = 0; + + if ( mod == bool_op_union ) { + if ( inA || inB ) { + AddRun(curPos, nextPos, oValA + oValB,valA + valB); + } + } else if ( mod == bool_op_inters ) { + if ( inA && inB ) { + AddRun(curPos, nextPos, oValA * oValB, valA * valB); + } + } else if ( mod == bool_op_diff ) { + if ( inA ) { + AddRun(curPos, nextPos, oValA - oValB, valA - valB); + } + } else if ( mod == bool_op_symdiff ) { + if ( inA && !(inB) ) { + AddRun(curPos, nextPos, oValA - oValB,valA - valB); + } + if ( !(inA) && inB ) { + AddRun(curPos, nextPos, oValB - oValA, valB - valA); + } + } + + curPos = nextPos; + if ( startB ) { + //inB=true; + valB = runB.vst; + } + if ( endB ) { + //inB=false; + valB = 0; + curB++; + if ( curB < b->nbRun && b->runs[curB].st == curPos ) { + valB = b->runs[curB].vst; + } + } + } +} + +/** + * Transform a line of bits into pixel coverage values. + * + * This is where you go from supersampled data to alpha values. + * \see IntLigne::Copy(int nbSub,BitLigne* *a). + */ +void IntLigne::Copy(BitLigne* a) +{ + if ( a->curMax <= a->curMin ) { + Reset(); + return; + } + + if ( a->curMin < a->st ) { + a->curMin = a->st; + } + + if ( a->curMax < a->st ) { + Reset(); + return; + } + + if ( a->curMin > a->en ) { + Reset(); + return; + } + + if ( a->curMax > a->en ) { + a->curMax=a->en; + } + + nbBord = 0; + nbRun = 0; + + int lastVal = 0; + int lastStart = 0; + bool startExists = false; + + int masks[] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; + + uint32_t c_full = a->fullB[(a->curMin-a->st) >> 3]; + uint32_t c_part = a->partB[(a->curMin-a->st) >> 3]; + c_full <<= 4 * ((a->curMin - a->st) & 0x00000007); + c_part <<= 4 * ((a->curMin - a->st) & 0x00000007); + for (int i = a->curMin; i <= a->curMax; i++) { + int nbBit = masks[c_full >> 28] + masks[c_part >> 28]; + + if ( nbBit > 0 ) { + if ( startExists ) { + if ( lastVal == nbBit ) { + // on continue le run + } else { + AddRun(lastStart, i, ((float) lastVal) / 4, ((float) lastVal) / 4); + lastStart = i; + lastVal = nbBit; + } + } else { + lastStart = i; + lastVal = nbBit; + startExists = true; + } + } else { + if ( startExists ) { + AddRun(lastStart, i, ((float) lastVal) / 4, ((float) lastVal) / 4); + } + startExists = false; + } + int chg = (i + 1 - a->st) & 0x00000007; + if ( chg == 0 ) { + c_full = a->fullB[(i + 1 - a->st) >> 3]; + c_part = a->partB[(i + 1 - a->st) >> 3]; + } else { + c_full <<= 4; + c_part <<= 4; + } + } + if ( startExists ) { + AddRun(lastStart, a->curMax + 1, ((float) lastVal) / 4, ((float) lastVal) / 4); + } +} + +/** + * Transform a line of bits into pixel coverage values. + * + * Alpha values are computed from supersampled data, so we have to scan the + * BitLigne left to right, summing the bits in each pixel. The alpha value + * is then "number of bits"/(nbSub*nbSub)". Full bits and partial bits are + * treated as equals because the method produces ugly results otherwise. + * + * \param nbSub Number of BitLigne in the array "a". + */ +void IntLigne::Copy(int nbSub, BitLigne **as) +{ + if ( nbSub <= 0 ) { + Reset(); + return; + } + + if ( nbSub == 1 ) { + Copy(as[0]); + return; + } + + // compute the min-max of the pixels to be rasterized from the min-max of the inpur bitlignes + int curMin = as[0]->curMin; + int curMax = as[0]->curMax; + for (int i = 1; i < nbSub; i++) { + if ( as[i]->curMin < curMin ) { + curMin = as[i]->curMin; + } + if ( as[i]->curMax > curMax ) { + curMax = as[i]->curMax; + } + } + + if ( curMin < as[0]->st ) { + curMin = as[0]->st; + } + + if ( curMax > as[0]->en ) { + curMax = as[0]->en; + } + + if ( curMax <= curMin ) { + Reset(); + return; + } + + nbBord = 0; + nbRun = 0; + + int lastVal = 0; + int lastStart = 0; + bool startExists = false; + float spA; + int masks[16] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; + + int theSt = as[0]->st; + if ( nbSub == 4 ) { + // special case for 4*4 supersampling, to avoid a few loops + uint32_t c_full[4]; + c_full[0] = as[0]->fullB[(curMin - theSt) >> 3] | as[0]->partB[(curMin - theSt) >> 3]; + c_full[0] <<= 4 * ((curMin - theSt) & 7); + c_full[1] = as[1]->fullB[(curMin - theSt) >> 3] | as[1]->partB[(curMin - theSt) >> 3]; + c_full[1] <<= 4 * ((curMin - theSt) & 7); + c_full[2] = as[2]->fullB[(curMin - theSt) >> 3] | as[2]->partB[(curMin - theSt) >> 3]; + c_full[2] <<= 4* ((curMin - theSt) & 7); + c_full[3] = as[3]->fullB[(curMin - theSt) >> 3] | as[3]->partB[(curMin - theSt) >> 3]; + c_full[3] <<= 4* ((curMin - theSt) & 7); + + spA = 1.0 / (4 * 4); + for (int i = curMin; i <= curMax; i++) { + int nbBit = 0; + + if ( c_full[0] == 0 && c_full[1] == 0 && c_full[2] == 0 && c_full[3] == 0 ) { + + if ( startExists ) { + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + } + startExists = false; + i = theSt + (((i - theSt) & (~7) ) + 7); + + } else if ( c_full[0] == 0xFFFFFFFF && c_full[1] == 0xFFFFFFFF && + c_full[2] == 0xFFFFFFFF && c_full[3] == 0xFFFFFFFF ) { + + if ( startExists ) { + if ( lastVal == 4*4) { + } else { + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + lastStart = i; + } + } else { + lastStart = i; + } + lastVal = 4*4; + startExists = true; + i = theSt + (((i - theSt) & (~7) ) + 7); + + } else { + nbBit += masks[c_full[0] >> 28]; + nbBit += masks[c_full[1] >> 28]; + nbBit += masks[c_full[2] >> 28]; + nbBit += masks[c_full[3] >> 28]; + + if ( nbBit > 0 ) { + if ( startExists ) { + if ( lastVal == nbBit ) { + // on continue le run + } else { + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + lastStart = i; + lastVal = nbBit; + } + } else { + lastStart = i; + lastVal = nbBit; + startExists = true; + } + } else { + if ( startExists ) { + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + } + startExists = false; + } + } + int chg = (i + 1 - theSt) & 7; + if ( chg == 0 ) { + if ( i < curMax ) { + c_full[0] = as[0]->fullB[(i + 1 - theSt) >> 3] | as[0]->partB[(i + 1 - theSt) >> 3]; + c_full[1] = as[1]->fullB[(i + 1 - theSt) >> 3] | as[1]->partB[(i + 1 - theSt) >> 3]; + c_full[2] = as[2]->fullB[(i + 1 - theSt) >> 3] | as[2]->partB[(i + 1 - theSt) >> 3]; + c_full[3] = as[3]->fullB[(i + 1 - theSt) >> 3] | as[3]->partB[(i + 1 - theSt) >> 3]; + } else { + // end of line. byebye + } + } else { + c_full[0] <<= 4; + c_full[1] <<= 4; + c_full[2] <<= 4; + c_full[3] <<= 4; + } + } + + } else { + + uint32_t c_full[16]; // we take nbSub < 16, since 16*16 supersampling makes a 1/256 precision in alpha values + // and that's the max of what 32bit argb can represent + // in fact, we'll treat it as 4*nbSub supersampling, so that's a half truth and a full lazyness from me + // uint32_t c_part[16]; + // start by putting the bits of the nbSub BitLignes in as[] in their respective c_full + + for (int i = 0; i < nbSub; i++) { + // fullB and partB treated equally + c_full[i] = as[i]->fullB[(curMin - theSt) >> 3] | as[i]->partB[(curMin - theSt) >> 3]; + c_full[i] <<= 4 * ((curMin - theSt) & 7); + /* c_part[i]=as[i]->partB[(curMin-theSt)>>3]; + c_part[i]<<=4*((curMin-theSt)&7);*/ + } + + spA = 1.0 / (4 * nbSub); // contribution to the alpha value of a single bit of the supersampled data + for (int i = curMin; i <= curMax;i++) { + int nbBit = 0; + // int nbPartBit=0; + // a little acceleration: if the lines only contain full or empty bits, we can flush + // what's remaining in the c_full at best we flush an entire c_full, ie 32 bits, or 32/4=8 pixels + bool allEmpty = true; + bool allFull = true; + for (int j = 0; j < nbSub; j++) { + if ( c_full[j] != 0 /*|| c_part[j] != 0*/ ) { + allEmpty=false; + break; + } + } + + if ( allEmpty ) { + // the remaining bits in c_full[] are empty: flush + if ( startExists ) { + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + } + startExists = false; + i = theSt + (((i - theSt) & (~7) ) + 7); + } else { + for (int j = 0; j < nbSub; j++) { + if ( c_full[j] != 0xFFFFFFFF ) { + allFull=false; + break; + } + } + + if ( allFull ) { + // the remaining bits in c_full[] are empty: flush + if ( startExists ) { + if ( lastVal == 4 * nbSub) { + } else { + AddRun(lastStart, i, ((float) lastVal) * spA,((float) lastVal) * spA); + lastStart = i; + } + } else { + lastStart = i; + } + lastVal = 4 * nbSub; + startExists = true; + i = theSt + (((i - theSt) & (~7) ) + 7); + } else { + // alpha values will be between 0 and 1, so we have more work to do + // compute how many bit this pixel holds + for (int j = 0; j < nbSub; j++) { + nbBit += masks[c_full[j] >> 28]; +// nbPartBit+=masks[c_part[j]>>28]; + } + // and add a single-pixel run if needed, or extend the current run if the alpha value hasn't changed + if ( nbBit > 0 ) { + if ( startExists ) { + if ( lastVal == nbBit ) { + // alpha value hasn't changed: we continue + } else { + // alpha value did change: put the run that was being done,... + AddRun(lastStart, i, ((float) lastVal) * spA, ((float) lastVal) * spA); + // ... and start a new one + lastStart = i; + lastVal = nbBit; + } + } else { + // alpha value was 0, so we "create" a new run with alpha nbBit + lastStart = i; + lastVal = nbBit; + startExists = true; + } + } else { + if ( startExists ) { + AddRun(lastStart, i, ((float) lastVal) * spA,((float) lastVal) * spA); + } + startExists = false; + } + } + } + // move to the right: shift bits in the c_full[], and if we shifted everything, load the next c_full[] + int chg = (i + 1 - theSt) & 7; + if ( chg == 0 ) { + if ( i < curMax ) { + for (int j = 0; j < nbSub; j++) { + c_full[j] = as[j]->fullB[(i + 1 - theSt) >> 3] | as[j]->partB[(i + 1 - theSt) >> 3]; + // c_part[j]=as[j]->partB[(i+1-theSt)>>3]; + } + } else { + // end of line. byebye + } + } else { + for (int j = 0; j < nbSub; j++) { + c_full[j]<<=4; + // c_part[j]<<=4; + } + } + } + } + + if ( startExists ) { + AddRun(lastStart, curMax + 1, ((float) lastVal) * spA,((float) lastVal) * spA); + } +} + +/// Copy another IntLigne +void IntLigne::Copy(IntLigne *a) +{ + if ( a->nbRun <= 0 ) { + Reset(); + return; + } + + nbBord = 0; + nbRun = a->nbRun; + if ( nbRun > maxRun ) { + maxRun = nbRun; + runs = (int_ligne_run*) g_realloc(runs, maxRun * sizeof(int_ligne_run)); + } + memcpy(runs, a->runs, nbRun * sizeof(int_ligne_run)); +} + + +/** + * Copy a FloatLigne's runs. + * + * Compute non-overlapping runs with integer boundaries from a set of runs + * with floating-point boundaries. This involves replacing floating-point + * boundaries that are not integer by single-pixel runs, so this function + * contains plenty of rounding and float->integer conversion (read: + * time-consuming). + * + * \todo + * Optimization Questions: Why is this called so often compared with the + * other Copy() routines? How does AddRun() look for optimization potential? + */ +void IntLigne::Copy(FloatLigne* a) +{ + if ( a->runs.empty() ) { + Reset(); + return; + } + + /* if ( showCopy ) { + printf("\nfloatligne:\n"); + a->Affiche(); + }*/ + + nbBord = 0; + nbRun = 0; + firstAc = lastAc = -1; + bool pixExists = false; + int curPos = (int) floor(a->runs[0].st) - 1; + float lastSurf = 0; + float tolerance = 0.00001; + + // we take each run of the FloatLigne in sequence and make single-pixel runs of its boundaries as needed + // since the float_ligne_runs are non-overlapping, when a single-pixel run intersects with another runs, + // it must intersect with the single-pixel run created for the end of that run. so instead of creating a new + // int_ligne_run, we just add the coverage to that run. + for (auto & run : a->runs) { + float_ligne_run runA = run; + float curStF = floor(runA.st); + float curEnF = floor(runA.en); + int curSt = (int) curStF; + int curEn = (int) curEnF; + + // stEx: start boundary is not integer -> create single-pixel run for it + // enEx: end boundary is not integer -> create single-pixel run for it + // miEx: the runs minus the eventual single-pixel runs is not empty + bool stEx = true; + bool miEx = true; + bool enEx = true; + int miSt = curSt; + float miStF = curStF; + float msv; + float mev; + if ( runA.en - curEnF < tolerance ) { + enEx = false; + } + + // msv and mev are the start and end value of the middle section of the run, that is the run minus the + // single-pixel runs creaed for its boundaries + if ( runA.st-curStF < tolerance /*miSt == runA.st*/ ) { + stEx = false; + msv = runA.vst; + } else { + miSt += 1; + miStF += 1.0; + if ( enEx == false && miSt == curEn ) { + msv = runA.ven; + } else { + // msv=a->ValAt(miSt,runA.st,runA.en,runA.vst,runA.ven); + msv = runA.vst + (miStF-runA.st) * runA.pente; + } + } + + if ( miSt >= curEn ) { + miEx = false; + } + if ( stEx == false && miEx == false /*curEn == runA.st*/ ) { + mev = runA.vst; + } else if ( enEx == false /*curEn == runA.en*/ ) { + mev = runA.ven; + } else { + // mev=a->ValAt(curEn,runA.st,runA.en,runA.vst,runA.ven); + mev = runA.vst + (curEnF-runA.st) * runA.pente; + } + + // check the different cases + if ( stEx && enEx ) { + // stEx && enEx + if ( curEn > curSt ) { + if ( pixExists ) { + if ( curPos < curSt ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + lastSurf=0.5*(msv+run.vst)*(miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } else { + lastSurf+=0.5*(msv+run.vst)*(miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } + pixExists=false; + } else { + lastSurf=0.5*(msv+run.vst)*(miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } + } else if ( pixExists ) { + if ( curPos < curSt ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + lastSurf=0.5*(run.ven+run.vst)*(run.en-run.st); + curPos=curSt; + } else { + lastSurf += 0.5 * (run.ven+run.vst)*(run.en-run.st); + } + } else { + lastSurf=0.5*(run.ven+run.vst)*(run.en-run.st); + curPos=curSt; + pixExists=true; + } + } else if ( pixExists ) { + if ( curPos < curSt ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + lastSurf = 0.5 * (msv+run.vst) * (miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } else { + lastSurf += 0.5 * (msv+run.vst) * (miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } + pixExists=false; + } else { + lastSurf = 0.5 * (msv+run.vst) * (miStF-run.st); + AddRun(curSt,curSt+1,lastSurf,lastSurf); + } + if ( miEx ) { + if ( pixExists && curPos < miSt ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + } + pixExists=false; + AddRun(miSt,curEn,msv,mev); + } + if ( enEx ) { + if ( curEn > curSt ) { + lastSurf=0.5*(mev+run.ven)*(run.en-curEnF); + pixExists=true; + curPos=curEn; + } else if ( ! stEx ) { + if ( pixExists ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + } + lastSurf=0.5*(mev+run.ven)*(run.en-curEnF); + pixExists=true; + curPos=curEn; + } + } + } + if ( pixExists ) { + AddRun(curPos,curPos+1,lastSurf,lastSurf); + } + /* if ( showCopy ) { + printf("-> intligne:\n"); + Affiche(); + }*/ +} + + +void IntLigne::Enqueue(int no) +{ + if ( firstAc < 0 ) { + firstAc = lastAc = no; + bords[no].prev = bords[no].next = -1; + } else { + bords[no].next = -1; + bords[no].prev = lastAc; + bords[lastAc].next = no; + lastAc = no; + } +} + + +void IntLigne::Dequeue(int no) +{ + if ( no == firstAc ) { + if ( no == lastAc ) { + firstAc = lastAc = -1; + } else { + firstAc = bords[no].next; + } + } else if ( no == lastAc ) { + lastAc = bords[no].prev; + } else { + } + if ( bords[no].prev >= 0 ) { + bords[bords[no].prev].next = bords[no].next; + } + if ( bords[no].next >= 0 ) { + bords[bords[no].next].prev = bords[no].prev; + } + + bords[no].prev = bords[no].next = -1; +} + +/** + * Rasterization. + * + * The parameters have the same meaning as in the AlphaLigne class. + */ +void IntLigne::Raster(raster_info &dest, void *color, RasterInRunFunc worker) +{ + if ( nbRun <= 0 ) { + return; + } + + int min = runs[0].st; + int max = runs[nbRun-1].en; + if ( dest.endPix <= min || dest.startPix >= max ) { + return; + } + + int curRun = -1; + for (curRun = 0; curRun < nbRun; curRun++) { + if ( runs[curRun].en > dest.startPix ) { + break; + } + } + + if ( curRun >= nbRun ) { + return; + } + + if ( runs[curRun].st < dest.startPix ) { + int nst = runs[curRun].st; + int nen = runs[curRun].en; + float vst = runs[curRun].vst; + float ven = runs[curRun].ven; + float nvst = (vst * (nen - dest.startPix) + ven * (dest.startPix - nst)) / ((float) (nen - nst)); + if ( runs[curRun].en <= dest.endPix ) { + (worker)(dest, color, dest.startPix, nvst, runs[curRun].en, runs[curRun].ven); + } else { + float nven = (vst * (nen - dest.endPix) + ven * (dest.endPix - nst)) / ((float)(nen - nst)); + (worker)(dest, color, dest.startPix, nvst, dest.endPix, nven); + return; + } + curRun++; + } + + for (; (curRun < nbRun && runs[curRun].en <= dest.endPix); curRun++) { + (worker)(dest, color, runs[curRun].st, runs[curRun].vst, runs[curRun].en, runs[curRun].ven); +//Buffer::RasterRun(*dest,color,runs[curRun].st,runs[curRun].vst,runs[curRun].en,runs[curRun].ven); + } + + if ( curRun >= nbRun ) { + return; + } + + if ( runs[curRun].st < dest.endPix && runs[curRun].en > dest.endPix ) { + int const nst = runs[curRun].st; + int const nen = runs[curRun].en; + float const vst = runs[curRun].vst; + float const ven = runs[curRun].ven; + float const nven = (vst * (nen - dest.endPix) + ven * (dest.endPix - nst)) / ((float)(nen - nst)); + + (worker)(dest,color,runs[curRun].st,runs[curRun].vst,dest.endPix,nven); + curRun++; + } +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/int-line.h b/src/livarot/int-line.h new file mode 100644 index 0000000..576cfcf --- /dev/null +++ b/src/livarot/int-line.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_LIVAROT_INT_LINE_H +#define INKSCAPE_LIVAROT_INT_LINE_H + +#include "livarot/LivarotDefs.h" +#include "object/object-set.h" // For BooleanOp + +/** \file + * Coverage with integer boundaries. + */ + +class BitLigne; +class FloatLigne; + +/// A run with integer boundaries. +struct int_ligne_run { + int st; + int en; + float vst; + float ven; +}; + +/// Integer boundary. +struct int_ligne_bord { + int pos; + bool start; + float val; + int other; + int prev; + int next; +}; + +/** + * Coverage with integer boundaries. + * + * This is what we want for actual rasterization. It contains the same + * stuff as FloatLigne, but technically only the Copy() functions are used. + */ +class IntLigne { +public: + + int nbBord; + int maxBord; + int_ligne_bord* bords; + + int nbRun; + int maxRun; + int_ligne_run* runs; + + int firstAc; + int lastAc; + + IntLigne(); + virtual ~IntLigne(); + + void Reset(); + int AddBord(int spos, float sval, int epos, float eval); + + void Flatten(); + + void Affiche(); + + int AddRun(int st, int en, float vst, float ven); + + void Booleen(IntLigne* a, IntLigne* b, BooleanOp mod); + + void Copy(IntLigne* a); + void Copy(FloatLigne* a); + void Copy(BitLigne* a); + void Copy(int nbSub,BitLigne **a); + + void Enqueue(int no); + void Dequeue(int no); + float RemainingValAt(int at); + + static int CmpBord(void const *p1, void const *p2) { + int_ligne_bord const *d1 = reinterpret_cast(p1); + int_ligne_bord const *d2 = reinterpret_cast(p2); + + if ( d1->pos == d2->pos ) { + if ( d1->start && !(d2->start) ) { + return 1; + } + if ( !(d1->start) && d2->start ) { + return -1; + } + return 0; + } + return (( d1->pos < d2->pos ) ? -1 : 1); + }; + + inline float ValAt(int at, int ps, int pe, float vs, float ve) { + return ((at - ps) * ve + (pe - at) * vs) / (pe - ps); + }; + + void Raster(raster_info &dest, void *color, RasterInRunFunc worker); +}; + + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/path-description.cpp b/src/livarot/path-description.cpp new file mode 100644 index 0000000..ecf50ca --- /dev/null +++ b/src/livarot/path-description.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "livarot/path-description.h" +#include <2geom/affine.h> + +PathDescr *PathDescrMoveTo::clone() const +{ + return new PathDescrMoveTo(*this); +} + +void PathDescrMoveTo::dumpSVG(Inkscape::SVGOStringStream& s, Geom::Point const &/*last*/) const +{ + s << "M " << p[Geom::X] << " " << p[Geom::Y] << " "; +} + +void PathDescrMoveTo::transform(Geom::Affine const& t) +{ + p = p * t; +} + +void PathDescrMoveTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " m " << p[Geom::X] << " " << p[Geom::Y]; +} + +void PathDescrLineTo::dumpSVG(Inkscape::SVGOStringStream& s, Geom::Point const &/*last*/) const +{ + s << "L " << p[Geom::X] << " " << p[Geom::Y] << " "; +} + +PathDescr *PathDescrLineTo::clone() const +{ + return new PathDescrLineTo(*this); +} + +void PathDescrLineTo::transform(Geom::Affine const& t) +{ + p = p * t; +} + +void PathDescrLineTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " l " << p[Geom::X] << " " << p[Geom::Y]; +} + +PathDescr *PathDescrBezierTo::clone() const +{ + return new PathDescrBezierTo(*this); +} + +void PathDescrBezierTo::transform(Geom::Affine const& t) +{ + p = p * t; +} + +void PathDescrBezierTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " b " << p[Geom::X] << " " << p[Geom::Y] << " " << nb; +} + +PathDescr *PathDescrIntermBezierTo::clone() const +{ + return new PathDescrIntermBezierTo(*this); +} + +void PathDescrIntermBezierTo::transform(Geom::Affine const& t) +{ + p = p * t; +} + +void PathDescrIntermBezierTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " i " << p[Geom::X] << " " << p[Geom::Y]; +} + +void PathDescrCubicTo::dumpSVG(Inkscape::SVGOStringStream& s, Geom::Point const &last) const +{ + s << "C " + << last[Geom::X] + start[0] / 3 << " " + << last[Geom::Y] + start[1] / 3 << " " + << p[Geom::X] - end[0] / 3 << " " + << p[Geom::Y] - end[1] / 3 << " " + << p[Geom::X] << " " + << p[Geom::Y] << " "; +} + +PathDescr *PathDescrCubicTo::clone() const +{ + return new PathDescrCubicTo(*this); +} + +void PathDescrCubicTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " c " + << p[Geom::X] << " " << p[Geom::Y] << " " + << start[Geom::X] << " " << start[Geom::Y] << " " + << end[Geom::X] << " " << end[Geom::Y] << " "; +} + +void PathDescrCubicTo::transform(Geom::Affine const& t) +{ + Geom::Affine tr = t; + tr[4] = tr[5] = 0; + start = start * tr; + end = end * tr; + + p = p * t; +} + +void PathDescrArcTo::dumpSVG(Inkscape::SVGOStringStream& s, Geom::Point const &/*last*/) const +{ + s << "A " + << rx << " " + << ry << " " + << angle << " " + << (large ? "1" : "0") << " " + << (clockwise ? "0" : "1") << " " + << p[Geom::X] << " " + << p[Geom::Y] << " "; +} + +PathDescr *PathDescrArcTo::clone() const +{ + return new PathDescrArcTo(*this); +} + +void PathDescrArcTo::transform(Geom::Affine const& t) +{ + p = p * t; +} + +void PathDescrArcTo::dump(std::ostream &s) const +{ + /* localizing ok */ + s << " a " + << p[Geom::X] << " " << p[Geom::Y] << " " + << rx << " " << ry << " " + << angle << " " + << (clockwise ? 1 : 0) << " " + << (large ? 1 : 0); +} + +PathDescr *PathDescrForced::clone() const +{ + return new PathDescrForced(*this); +} + +void PathDescrClose::dumpSVG(Inkscape::SVGOStringStream& s, Geom::Point const &/*last*/) const +{ + s << "z "; +} + +PathDescr *PathDescrClose::clone() const +{ + return new PathDescrClose(*this); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/livarot/path-description.h b/src/livarot/path-description.h new file mode 100644 index 0000000..dd957ec --- /dev/null +++ b/src/livarot/path-description.h @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_LIVAROT_PATH_DESCRIPTION_H +#define SEEN_INKSCAPE_LIVAROT_PATH_DESCRIPTION_H + +#include <2geom/point.h> +#include "svg/stringstream.h" + +// path description commands +/* FIXME: these should be unnecessary once the refactoring of the path +** description stuff is finished. +*/ +enum +{ + descr_moveto = 0, // a moveto + descr_lineto = 1, // a (guess what) lineto + descr_cubicto = 2, + descr_bezierto = 3, // "beginning" of a quadratic bezier spline, will contain its endpoint (i know, it's bad...) + descr_arcto = 4, + descr_close = 5, + descr_interm_bezier = 6, // control point of the bezier spline + descr_forced = 7, + + descr_type_mask = 15 // the command no will be stored in a "flags" field, potentially with other info, so we need + // a mask to AND the field and extract the command +}; + +struct PathDescr +{ + PathDescr() : flags(0), associated(-1), tSt(0), tEn(1) {} + PathDescr(int f) : flags(f), associated(-1), tSt(0), tEn(1) {} + virtual ~PathDescr() = default; + + int getType() const { return flags & descr_type_mask; } + void setType(int t) { + flags &= ~descr_type_mask; + flags |= t; + } + + virtual void dumpSVG(Inkscape::SVGOStringStream &/*s*/, Geom::Point const &/*last*/) const {} + virtual PathDescr *clone() const = 0; + virtual void transform(Geom::Affine const &/*t*/) {} + virtual void dump(std::ostream &/*s*/) const {} + + int flags; // most notably contains the path command no + int associated; // index in the polyline of the point that ends the path portion of this command + double tSt; + double tEn; +}; + +struct PathDescrMoveTo : public PathDescr +{ + PathDescrMoveTo(Geom::Point const &pp) + : PathDescr(descr_moveto), p(pp) {} + + void dumpSVG(Inkscape::SVGOStringStream &s, Geom::Point const &last) const override; + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; +}; + +struct PathDescrLineTo : public PathDescr +{ + PathDescrLineTo(Geom::Point const &pp) + : PathDescr(descr_lineto), p(pp) {} + + void dumpSVG(Inkscape::SVGOStringStream &s, Geom::Point const &last) const override; + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; +}; + +// quadratic bezier curves: a set of control points, and an endpoint +struct PathDescrBezierTo : public PathDescr +{ + PathDescrBezierTo(Geom::Point const &pp, int n) + : PathDescr(descr_bezierto), p(pp), nb(n) {} + + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; // the endpoint's coordinates + int nb; // number of control points, stored in the next path description commands +}; + +/* FIXME: I don't think this should be necessary */ +struct PathDescrIntermBezierTo : public PathDescr +{ + PathDescrIntermBezierTo() + : PathDescr(descr_interm_bezier) , p(0, 0) {} + PathDescrIntermBezierTo(Geom::Point const &pp) + : PathDescr(descr_interm_bezier), p(pp) {} + + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; // control point coordinates +}; + +// cubic spline curve: 2 tangents and one endpoint +struct PathDescrCubicTo : public PathDescr +{ + PathDescrCubicTo(Geom::Point const &pp, Geom::Point const &s, Geom::Point const& e) + : PathDescr(descr_cubicto), p(pp), start(s), end(e) {} + + void dumpSVG(Inkscape::SVGOStringStream &s, Geom::Point const &last) const override; + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; + Geom::Point start; + Geom::Point end; +}; + +// arc: endpoint, 2 radii and one angle, plus 2 booleans to choose the arc (svg style) +struct PathDescrArcTo : public PathDescr +{ + PathDescrArcTo(Geom::Point const &pp, double x, double y, double a, bool l, bool c) + : PathDescr(descr_arcto), p(pp), rx(x), ry(y), angle(a), large(l), clockwise(c) {} + + void dumpSVG(Inkscape::SVGOStringStream &s, Geom::Point const &last) const override; + PathDescr *clone() const override; + void transform(Geom::Affine const &t) override; + void dump(std::ostream &s) const override; + + Geom::Point p; + double rx; + double ry; + double angle; + bool large; + bool clockwise; +}; + +struct PathDescrForced : public PathDescr +{ + PathDescrForced() : PathDescr(descr_forced), p(0, 0) {} + + PathDescr *clone() const override; + + /* FIXME: not sure whether _forced should have a point associated with it; + ** Path::ConvertForcedToMoveTo suggests that maybe it should. + */ + Geom::Point p; +}; + +struct PathDescrClose : public PathDescr +{ + PathDescrClose() : PathDescr(descr_close) {} + + void dumpSVG(Inkscape::SVGOStringStream &s, Geom::Point const &last) const override; + PathDescr *clone() const override; + + /* FIXME: not sure whether _forced should have a point associated with it; + ** Path::ConvertForcedToMoveTo suggests that maybe it should. + */ + Geom::Point p; +}; + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-event-queue.h b/src/livarot/sweep-event-queue.h new file mode 100644 index 0000000..759727a --- /dev/null +++ b/src/livarot/sweep-event-queue.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A container of intersection events. + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIVAROT_SWEEP_EVENT_QUEUE_H +#define SEEN_LIVAROT_SWEEP_EVENT_QUEUE_H + +#include <2geom/forward.h> +class SweepEvent; +class SweepTree; + + +/** + * The structure to hold the intersections events encountered during the sweep. It's an array of + * SweepEvent (not allocated with "new SweepEvent[n]" but with a malloc). There's a list of + * indices because it's a binary heap: inds[i] tell that events[inds[i]] has position i in the + * heap. Each SweepEvent has a field to store its index in the heap, too. + */ +class SweepEventQueue +{ +public: + SweepEventQueue(int s); + virtual ~SweepEventQueue(); + + int size() const { return nbEvt; } + + /// Look for the topmost intersection in the heap + bool peek(SweepTree * &iLeft, SweepTree * &iRight, Geom::Point &oPt, double &itl, double &itr); + /// Extract the topmost intersection from the heap + bool extract(SweepTree * &iLeft, SweepTree * &iRight, Geom::Point &oPt, double &itl, double &itr); + /// Add one intersection in the binary heap + SweepEvent *add(SweepTree *iLeft, SweepTree *iRight, Geom::Point &iPt, double itl, double itr); + + void remove(SweepEvent *e); + void relocate(SweepEvent *e, int to); + +private: + int nbEvt; ///< Number of events currently in the heap. + int maxEvt; ///< Allocated size of the heap. + int *inds; ///< Indices. + SweepEvent *events; ///< Sweep events. +}; + +#endif /* !SEEN_LIVAROT_SWEEP_EVENT_QUEUE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-event.cpp b/src/livarot/sweep-event.cpp new file mode 100644 index 0000000..bb9e4e7 --- /dev/null +++ b/src/livarot/sweep-event.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "livarot/sweep-event-queue.h" +#include "livarot/sweep-tree.h" +#include "livarot/sweep-event.h" +#include "livarot/Shape.h" + +SweepEventQueue::SweepEventQueue(int s) : nbEvt(0), maxEvt(s) +{ + /* FIXME: use new[] for this, but this causes problems when delete[] + ** calls the SweepEvent destructors. + */ + events = (SweepEvent *) g_malloc(maxEvt * sizeof(SweepEvent)); + inds = new int[maxEvt]; +} + +SweepEventQueue::~SweepEventQueue() +{ + g_free(events); + delete []inds; +} + +SweepEvent *SweepEventQueue::add(SweepTree *iLeft, SweepTree *iRight, Geom::Point &px, double itl, double itr) +{ + if (nbEvt > maxEvt) { + return nullptr; + } + + int const n = nbEvt++; + events[n].MakeNew (iLeft, iRight, px, itl, itr); + + SweepTree *t[2] = { iLeft, iRight }; + for (auto & i : t) { + Shape *s = i->src; + Shape::dg_arete const &e = s->getEdge(i->bord); + int const n = std::max(e.st, e.en); + s->pData[n].pending++;; + } + + events[n].ind = n; + inds[n] = n; + + int curInd = n; + while (curInd > 0) { + int const half = (curInd - 1) / 2; + int const no = inds[half]; + if (px[1] < events[no].posx[1] + || (px[1] == events[no].posx[1] && px[0] < events[no].posx[0])) + { + events[n].ind = half; + events[no].ind = curInd; + inds[half] = n; + inds[curInd] = no; + } else { + break; + } + + curInd = half; + } + + return events + n; +} + + + +bool SweepEventQueue::peek(SweepTree * &iLeft, SweepTree * &iRight, Geom::Point &px, double &itl, double &itr) +{ + if (nbEvt <= 0) { + return false; + } + + SweepEvent const &e = events[inds[0]]; + + iLeft = e.sweep[LEFT]; + iRight = e.sweep[RIGHT]; + px = e.posx; + itl = e.tl; + itr = e.tr; + + return true; +} + +bool SweepEventQueue::extract(SweepTree * &iLeft, SweepTree * &iRight, Geom::Point &px, double &itl, double &itr) +{ + if (nbEvt <= 0) { + return false; + } + + SweepEvent &e = events[inds[0]]; + + iLeft = e.sweep[LEFT]; + iRight = e.sweep[RIGHT]; + px = e.posx; + itl = e.tl; + itr = e.tr; + remove(&e); + + return true; +} + + +void SweepEventQueue::remove(SweepEvent *e) +{ + if (nbEvt <= 1) { + e->MakeDelete (); + nbEvt = 0; + return; + } + + int const n = e->ind; + int to = inds[n]; + e->MakeDelete(); + relocate(&events[--nbEvt], to); + + int const moveInd = nbEvt; + if (moveInd == n) { + return; + } + + to = inds[moveInd]; + + events[to].ind = n; + inds[n] = to; + + int curInd = n; + Geom::Point const px = events[to].posx; + bool didClimb = false; + while (curInd > 0) { + int const half = (curInd - 1) / 2; + int const no = inds[half]; + if (px[1] < events[no].posx[1] + || (px[1] == events[no].posx[1] && px[0] < events[no].posx[0])) + { + events[to].ind = half; + events[no].ind = curInd; + inds[half] = to; + inds[curInd] = no; + didClimb = true; + } else { + break; + } + curInd = half; + } + + if (didClimb) { + return; + } + + while (2 * curInd + 1 < nbEvt) { + int const child1 = 2 * curInd + 1; + int const child2 = child1 + 1; + int const no1 = inds[child1]; + int const no2 = inds[child2]; + if (child2 < nbEvt) { + if (px[1] > events[no1].posx[1] + || (px[1] == events[no1].posx[1] + && px[0] > events[no1].posx[0])) + { + if (events[no2].posx[1] > events[no1].posx[1] + || (events[no2].posx[1] == events[no1].posx[1] + && events[no2].posx[0] > events[no1].posx[0])) + { + events[to].ind = child1; + events[no1].ind = curInd; + inds[child1] = to; + inds[curInd] = no1; + curInd = child1; + } else { + events[to].ind = child2; + events[no2].ind = curInd; + inds[child2] = to; + inds[curInd] = no2; + curInd = child2; + } + } else { + if (px[1] > events[no2].posx[1] + || (px[1] == events[no2].posx[1] + && px[0] > events[no2].posx[0])) + { + events[to].ind = child2; + events[no2].ind = curInd; + inds[child2] = to; + inds[curInd] = no2; + curInd = child2; + } else { + break; + } + } + } else { + if (px[1] > events[no1].posx[1] + || (px[1] == events[no1].posx[1] + && px[0] > events[no1].posx[0])) + { + events[to].ind = child1; + events[no1].ind = curInd; + inds[child1] = to; + inds[curInd] = no1; + } + + break; + } + } +} + + + + +void SweepEventQueue::relocate(SweepEvent *e, int to) +{ + if (inds[e->ind] == to) { + return; // j'y suis deja + } + + events[to] = *e; + + e->sweep[LEFT]->evt[RIGHT] = events + to; + e->sweep[RIGHT]->evt[LEFT] = events + to; + inds[e->ind] = to; +} + + +/* + * a simple binary heap + * it only contains intersection events + * the regular benley-ottman stuffs the segment ends in it too, but that not needed here since theses points + * are already sorted. and the binary heap is much faster with only intersections... + * the code sample on which this code is based comes from purists.org + */ +SweepEvent::SweepEvent() +{ + MakeNew (nullptr, nullptr, Geom::Point(0, 0), 0, 0); +} + +SweepEvent::~SweepEvent() +{ + MakeDelete(); +} + +void SweepEvent::MakeNew(SweepTree *iLeft, SweepTree *iRight, Geom::Point const &px, double itl, double itr) +{ + ind = -1; + posx = px; + tl = itl; + tr = itr; + sweep[LEFT] = iLeft; + sweep[RIGHT] = iRight; + sweep[LEFT]->evt[RIGHT] = this; + sweep[RIGHT]->evt[LEFT] = this; +} + +void SweepEvent::MakeDelete() +{ + for (int i = 0; i < 2; i++) { + if (sweep[i]) { + Shape *s = sweep[i]->src; + Shape::dg_arete const &e = s->getEdge(sweep[i]->bord); + int const n = std::max(e.st, e.en); + s->pData[n].pending--; + } + + sweep[i]->evt[1 - i] = nullptr; + sweep[i] = nullptr; + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-event.h b/src/livarot/sweep-event.h new file mode 100644 index 0000000..492c4e8 --- /dev/null +++ b/src/livarot/sweep-event.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_LIVAROT_SWEEP_EVENT_H +#define INKSCAPE_LIVAROT_SWEEP_EVENT_H +/** \file + * Intersection events. + */ + +#include <2geom/point.h> +class SweepTree; + + +/** One intersection event. */ +class SweepEvent +{ +public: + SweepTree *sweep[2]; ///< Sweep element associated with the left and right edge of the intersection. + + Geom::Point posx; ///< Coordinates of the intersection. + double tl, tr; ///< Coordinates of the intersection on the left edge (tl) and on the right edge (tr). + + int ind; ///< Index in the binary heap. + + SweepEvent(); // not used. + virtual ~SweepEvent(); // not used. + + /// Initialize a SweepEvent structure. + void MakeNew (SweepTree * iLeft, SweepTree * iRight, Geom::Point const &iPt, + double itl, double itr); + + /// Void a SweepEvent structure. + void MakeDelete (); +}; + + +#endif /* !INKSCAPE_LIVAROT_SWEEP_EVENT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-tree-list.cpp b/src/livarot/sweep-tree-list.cpp new file mode 100644 index 0000000..97640fd --- /dev/null +++ b/src/livarot/sweep-tree-list.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "livarot/sweep-tree.h" +#include "livarot/sweep-tree-list.h" + + +SweepTreeList::SweepTreeList(int s) : + nbTree(0), + maxTree(s), + trees((SweepTree *) g_malloc(s * sizeof(SweepTree))), + racine(nullptr) +{ + /* FIXME: Use new[] for trees initializer above, but watch out for bad things happening when + * SweepTree::~SweepTree is called. + */ +} + + +SweepTreeList::~SweepTreeList() +{ + g_free(trees); + trees = nullptr; +} + + +SweepTree *SweepTreeList::add(Shape *iSrc, int iBord, int iWeight, int iStartPoint, Shape */*iDst*/) +{ + if (nbTree >= maxTree) { + return nullptr; + } + + int const n = nbTree++; + trees[n].MakeNew(iSrc, iBord, iWeight, iStartPoint); + + return trees + n; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-tree-list.h b/src/livarot/sweep-tree-list.h new file mode 100644 index 0000000..84939c4 --- /dev/null +++ b/src/livarot/sweep-tree-list.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** @file + * @brief SweepTreeList definition + */ + +#ifndef INKSCAPE_LIVAROT_SWEEP_TREE_LIST_H +#define INKSCAPE_LIVAROT_SWEEP_TREE_LIST_H + +class Shape; +class SweepTree; + +/** + * The sweepline: a set of edges intersecting the current sweepline + * stored as an AVL tree. + */ +class SweepTreeList { +public: + int nbTree; ///< Number of nodes in the tree. + int const maxTree; ///< Max number of nodes in the tree. + SweepTree *trees; ///< The array of nodes. + SweepTree *racine; ///< Root of the tree. + + SweepTreeList(int s); + virtual ~SweepTreeList(); + + SweepTree *add(Shape *iSrc, int iBord, int iWeight, int iStartPoint, Shape *iDst); +}; + + +#endif /* !INKSCAPE_LIVAROT_SWEEP_TREE_LIST_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-tree.cpp b/src/livarot/sweep-tree.cpp new file mode 100644 index 0000000..dc350c4 --- /dev/null +++ b/src/livarot/sweep-tree.cpp @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "livarot/sweep-event-queue.h" +#include "livarot/sweep-tree-list.h" +#include "livarot/sweep-tree.h" +#include "livarot/sweep-event.h" +#include "livarot/Shape.h" + + +/* + * the AVL tree holding the edges intersecting the sweepline + * that structure is very sensitive to anything + * you have edges stored in nodes, the nodes are sorted in increasing x-order of intersection + * with the sweepline, you have the 2 potential intersections of the edge in the node with its + * neighbours, plus the fact that it's stored in an array that's realloc'd + */ + +SweepTree::SweepTree() +{ + src = nullptr; + bord = -1; + startPoint = -1; + evt[LEFT] = evt[RIGHT] = nullptr; + sens = true; + //invDirLength=1; +} + +SweepTree::~SweepTree() +{ + MakeDelete(); +} + +void +SweepTree::MakeNew(Shape *iSrc, int iBord, int iWeight, int iStartPoint) +{ + AVLTree::MakeNew(); + ConvertTo(iSrc, iBord, iWeight, iStartPoint); +} + +void +SweepTree::ConvertTo(Shape *iSrc, int iBord, int iWeight, int iStartPoint) +{ + src = iSrc; + bord = iBord; + evt[LEFT] = evt[RIGHT] = nullptr; + startPoint = iStartPoint; + if (src->getEdge(bord).st < src->getEdge(bord).en) { + if (iWeight >= 0) + sens = true; + else + sens = false; + } else { + if (iWeight >= 0) + sens = false; + else + sens = true; + } + //invDirLength=src->eData[bord].isqlength; + //invDirLength=1/sqrt(src->getEdge(bord).dx*src->getEdge(bord).dx+src->getEdge(bord).dy*src->getEdge(bord).dy); +} + + +void SweepTree::MakeDelete() +{ + for (int i = 0; i < 2; i++) { + if (evt[i]) { + evt[i]->sweep[1 - i] = nullptr; + } + evt[i] = nullptr; + } + + AVLTree::MakeDelete(); +} + + +// find the position at which node "newOne" should be inserted in the subtree rooted here +// we want to order with respect to the order of intersections with the sweepline, currently +// lying at y=px[1]. +// px is the upper endpoint of newOne +int +SweepTree::Find(Geom::Point const &px, SweepTree *newOne, SweepTree *&insertL, + SweepTree *&insertR, bool sweepSens) +{ + // get the edge associated with this node: one point+one direction + // since we're dealing with line, the direction (bNorm) is taken downwards + Geom::Point bOrig, bNorm; + bOrig = src->pData[src->getEdge(bord).st].rx; + bNorm = src->eData[bord].rdx; + if (src->getEdge(bord).st > src->getEdge(bord).en) { + bNorm = -bNorm; + } + // rotate to get the normal to the edge + bNorm=bNorm.ccw(); + + Geom::Point diff; + diff = px - bOrig; + + // compute (px-orig)^dir to know on which side of this edge the point px lies + double y = 0; + //if ( startPoint == newOne->startPoint ) { + // y=0; + //} else { + y = dot(bNorm, diff); + //} + //y*=invDirLength; + if (fabs(y) < 0.000001) { + // that damn point px lies on me, so i need to consider to direction of the edge in + // newOne to know if it goes toward my left side or my right side + // sweepSens is needed (actually only used by the Scan() functions) because if the sweepline goes upward, + // signs change + // prendre en compte les directions + Geom::Point nNorm; + nNorm = newOne->src->eData[newOne->bord].rdx; + if (newOne->src->getEdge(newOne->bord).st > + newOne->src->getEdge(newOne->bord).en) + { + nNorm = -nNorm; + } + nNorm=nNorm.ccw(); + + if (sweepSens) { + y = cross(bNorm, nNorm); + } else { + y = cross(nNorm, bNorm); + } + if (y == 0) { + y = dot(bNorm, nNorm); + if (y == 0) { + insertL = this; + insertR = static_cast(elem[RIGHT]); + return found_exact; + } + } + } + if (y < 0) { + if (child[LEFT]) { + return (static_cast(child[LEFT]))->Find(px, newOne, + insertL, insertR, + sweepSens); + } else { + insertR = this; + insertL = static_cast(elem[LEFT]); + if (insertL) { + return found_between; + } else { + return found_on_left; + } + } + } else { + if (child[RIGHT]) { + return (static_cast(child[RIGHT]))->Find(px, newOne, + insertL, insertR, + sweepSens); + } else { + insertL = this; + insertR = static_cast(elem[RIGHT]); + if (insertR) { + return found_between; + } else { + return found_on_right; + } + } + } + return not_found; +} + +// only find a point's position +int +SweepTree::Find(Geom::Point const &px, SweepTree * &insertL, + SweepTree * &insertR) +{ + Geom::Point bOrig, bNorm; + bOrig = src->pData[src->getEdge(bord).st].rx; + bNorm = src->eData[bord].rdx; + if (src->getEdge(bord).st > src->getEdge(bord).en) + { + bNorm = -bNorm; + } + bNorm=bNorm.ccw(); + + Geom::Point diff; + diff = px - bOrig; + + double y = 0; + y = dot(bNorm, diff); + if (y == 0) + { + insertL = this; + insertR = static_cast(elem[RIGHT]); + return found_exact; + } + if (y < 0) + { + if (child[LEFT]) + { + return (static_cast(child[LEFT]))->Find(px, insertL, + insertR); + } + else + { + insertR = this; + insertL = static_cast(elem[LEFT]); + if (insertL) + { + return found_between; + } + else + { + return found_on_left; + } + } + } + else + { + if (child[RIGHT]) + { + return (static_cast(child[RIGHT]))->Find(px, insertL, + insertR); + } + else + { + insertL = this; + insertR = static_cast(elem[RIGHT]); + if (insertR) + { + return found_between; + } + else + { + return found_on_right; + } + } + } + return not_found; +} + +void +SweepTree::RemoveEvents(SweepEventQueue & queue) +{ + RemoveEvent(queue, LEFT); + RemoveEvent(queue, RIGHT); +} + +void SweepTree::RemoveEvent(SweepEventQueue &queue, Side s) +{ + if (evt[s]) { + queue.remove(evt[s]); + evt[s] = nullptr; + } +} + +int +SweepTree::Remove(SweepTreeList &list, SweepEventQueue &queue, + bool rebalance) +{ + RemoveEvents(queue); + AVLTree *tempR = static_cast(list.racine); + int err = AVLTree::Remove(tempR, rebalance); + list.racine = static_cast(tempR); + MakeDelete(); + if (list.nbTree <= 1) + { + list.nbTree = 0; + list.racine = nullptr; + } + else + { + if (list.racine == list.trees + (list.nbTree - 1)) + list.racine = this; + list.trees[--list.nbTree].Relocate(this); + } + return err; +} + +int +SweepTree::Insert(SweepTreeList &list, SweepEventQueue &queue, + Shape *iDst, int iAtPoint, bool rebalance, bool sweepSens) +{ + if (list.racine == nullptr) + { + list.racine = this; + return avl_no_err; + } + SweepTree *insertL = nullptr; + SweepTree *insertR = nullptr; + int insertion = + list.racine->Find(iDst->getPoint(iAtPoint).x, this, + insertL, insertR, sweepSens); + + if (insertion == found_exact) { + if (insertR) { + insertR->RemoveEvent(queue, LEFT); + } + if (insertL) { + insertL->RemoveEvent(queue, RIGHT); + } + + } else if (insertion == found_between) { + insertR->RemoveEvent(queue, LEFT); + insertL->RemoveEvent(queue, RIGHT); + } + + AVLTree *tempR = static_cast(list.racine); + int err = + AVLTree::Insert(tempR, insertion, static_cast(insertL), + static_cast(insertR), rebalance); + list.racine = static_cast(tempR); + return err; +} + +// insertAt() is a speedup on the regular sweepline: if the polygon contains a point of high degree, you +// get a set of edge that are to be added in the same position. thus you insert one edge with a regular insert(), +// and then insert all the other in a doubly-linked list fashion. this avoids the Find() call, but is O(d^2) worst-case +// where d is the number of edge to add in this fashion. hopefully d remains small + +int +SweepTree::InsertAt(SweepTreeList &list, SweepEventQueue &queue, + Shape */*iDst*/, SweepTree *insNode, int fromPt, + bool rebalance, bool sweepSens) +{ + if (list.racine == nullptr) + { + list.racine = this; + return avl_no_err; + } + + Geom::Point fromP; + fromP = src->pData[fromPt].rx; + Geom::Point nNorm; + nNorm = src->getEdge(bord).dx; + if (src->getEdge(bord).st > src->getEdge(bord).en) + { + nNorm = -nNorm; + } + if (sweepSens == false) + { + nNorm = -nNorm; + } + + Geom::Point bNorm; + bNorm = insNode->src->getEdge(insNode->bord).dx; + if (insNode->src->getEdge(insNode->bord).st > + insNode->src->getEdge(insNode->bord).en) + { + bNorm = -bNorm; + } + + SweepTree *insertL = nullptr; + SweepTree *insertR = nullptr; + double ang = cross(bNorm, nNorm); + if (ang == 0) + { + insertL = insNode; + insertR = static_cast(insNode->elem[RIGHT]); + } + else if (ang > 0) + { + insertL = insNode; + insertR = static_cast(insNode->elem[RIGHT]); + + while (insertL) + { + if (insertL->src == src) + { + if (insertL->src->getEdge(insertL->bord).st != fromPt + && insertL->src->getEdge(insertL->bord).en != fromPt) + { + break; + } + } + else + { + int ils = insertL->src->getEdge(insertL->bord).st; + int ile = insertL->src->getEdge(insertL->bord).en; + if ((insertL->src->pData[ils].rx[0] != fromP[0] + || insertL->src->pData[ils].rx[1] != fromP[1]) + && (insertL->src->pData[ile].rx[0] != fromP[0] + || insertL->src->pData[ile].rx[1] != fromP[1])) + { + break; + } + } + bNorm = insertL->src->getEdge(insertL->bord).dx; + if (insertL->src->getEdge(insertL->bord).st > + insertL->src->getEdge(insertL->bord).en) + { + bNorm = -bNorm; + } + ang = cross(bNorm, nNorm); + if (ang <= 0) + { + break; + } + insertR = insertL; + insertL = static_cast(insertR->elem[LEFT]); + } + } + else if (ang < 0) + { + insertL = insNode; + insertR = static_cast(insNode->elem[RIGHT]); + + while (insertR) + { + if (insertR->src == src) + { + if (insertR->src->getEdge(insertR->bord).st != fromPt + && insertR->src->getEdge(insertR->bord).en != fromPt) + { + break; + } + } + else + { + int ils = insertR->src->getEdge(insertR->bord).st; + int ile = insertR->src->getEdge(insertR->bord).en; + if ((insertR->src->pData[ils].rx[0] != fromP[0] + || insertR->src->pData[ils].rx[1] != fromP[1]) + && (insertR->src->pData[ile].rx[0] != fromP[0] + || insertR->src->pData[ile].rx[1] != fromP[1])) + { + break; + } + } + bNorm = insertR->src->getEdge(insertR->bord).dx; + if (insertR->src->getEdge(insertR->bord).st > + insertR->src->getEdge(insertR->bord).en) + { + bNorm = -bNorm; + } + ang = cross(bNorm, nNorm); + if (ang > 0) + { + break; + } + insertL = insertR; + insertR = static_cast(insertL->elem[RIGHT]); + } + } + + int insertion = found_between; + + if (insertL == nullptr) { + insertion = found_on_left; + } + if (insertR == nullptr) { + insertion = found_on_right; + } + + if (insertion == found_exact) { + /* FIXME: surely this can never be called? */ + if (insertR) { + insertR->RemoveEvent(queue, LEFT); + } + if (insertL) { + insertL->RemoveEvent(queue, RIGHT); + } + } else if (insertion == found_between) { + insertR->RemoveEvent(queue, LEFT); + insertL->RemoveEvent(queue, RIGHT); + } + + AVLTree *tempR = static_cast(list.racine); + int err = + AVLTree::Insert(tempR, insertion, static_cast(insertL), + static_cast(insertR), rebalance); + list.racine = static_cast(tempR); + return err; +} + +void +SweepTree::Relocate(SweepTree * to) +{ + if (this == to) + return; + AVLTree::Relocate(to); + to->src = src; + to->bord = bord; + to->sens = sens; + to->evt[LEFT] = evt[LEFT]; + to->evt[RIGHT] = evt[RIGHT]; + to->startPoint = startPoint; + if (unsigned(bord) < src->swsData.size()) + src->swsData[bord].misc = to; + if (unsigned(bord) < src->swrData.size()) + src->swrData[bord].misc = to; + if (evt[LEFT]) + evt[LEFT]->sweep[RIGHT] = to; + if (evt[RIGHT]) + evt[RIGHT]->sweep[LEFT] = to; +} + +// TODO check if ignoring these parameters is bad +void +SweepTree::SwapWithRight(SweepTreeList &/*list*/, SweepEventQueue &/*queue*/) +{ + SweepTree *tL = this; + SweepTree *tR = static_cast(elem[RIGHT]); + + tL->src->swsData[tL->bord].misc = tR; + tR->src->swsData[tR->bord].misc = tL; + + { + Shape *swap = tL->src; + tL->src = tR->src; + tR->src = swap; + } + { + int swap = tL->bord; + tL->bord = tR->bord; + tR->bord = swap; + } + { + int swap = tL->startPoint; + tL->startPoint = tR->startPoint; + tR->startPoint = swap; + } + //{double swap=tL->invDirLength;tL->invDirLength=tR->invDirLength;tR->invDirLength=swap;} + { + bool swap = tL->sens; + tL->sens = tR->sens; + tR->sens = swap; + } +} + +void +SweepTree::Avance(Shape */*dstPts*/, int /*curPoint*/, Shape */*a*/, Shape */*b*/) +{ + return; +/* if ( curPoint != startPoint ) { + int nb=-1; + if ( sens ) { +// nb=dstPts->AddEdge(startPoint,curPoint); + } else { +// nb=dstPts->AddEdge(curPoint,startPoint); + } + if ( nb >= 0 ) { + dstPts->swsData[nb].misc=(void*)((src==b)?1:0); + int wp=waitingPoint; + dstPts->eData[nb].firstLinkedPoint=waitingPoint; + waitingPoint=-1; + while ( wp >= 0 ) { + dstPts->pData[wp].edgeOnLeft=nb; + wp=dstPts->pData[wp].nextLinkedPoint; + } + } + startPoint=curPoint; + }*/ +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/livarot/sweep-tree.h b/src/livarot/sweep-tree.h new file mode 100644 index 0000000..13e1f76 --- /dev/null +++ b/src/livarot/sweep-tree.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_LIVAROT_SWEEP_TREE_H +#define INKSCAPE_LIVAROT_SWEEP_TREE_H + +#include "livarot/AVL.h" +#include <2geom/point.h> + +class Shape; +class SweepEvent; +class SweepEventQueue; +class SweepTreeList; + + +/** + * One node in the AVL tree of edges. + * Note that these nodes will be stored in a dynamically allocated array, hence the Relocate() function. + */ +class SweepTree:public AVLTree +{ +public: + SweepEvent *evt[2]; ///< Intersection with the edge on the left and right (if any). + + Shape *src; /**< Shape from which the edge comes. (When doing boolean operation on polygons, + * edges can come from 2 different polygons.) + */ + int bord; ///< Edge index in the Shape. + bool sens; ///< true= top->bottom; false= bottom->top. + int startPoint; ///< point index in the result Shape associated with the upper end of the edge + + SweepTree(); + ~SweepTree() override; + + // Inits a brand new node. + void MakeNew(Shape *iSrc, int iBord, int iWeight, int iStartPoint); + // changes the edge associated with this node + // goal: reuse the node when an edge follows another, which is the most common case + void ConvertTo(Shape *iSrc, int iBord, int iWeight, int iStartPoint); + + // Delete the contents of node. + void MakeDelete(); + + // utilites + + // the find function that was missing in the AVLTrree class + // the return values are defined in LivarotDefs.h + int Find(Geom::Point const &iPt, SweepTree *newOne, SweepTree *&insertL, + SweepTree *&insertR, bool sweepSens = true); + int Find(Geom::Point const &iPt, SweepTree *&insertL, SweepTree *&insertR); + + /// Remove sweepevents attached to this node. + void RemoveEvents(SweepEventQueue &queue); + + void RemoveEvent(SweepEventQueue &queue, Side s); + + // overrides of the AVLTree functions, to account for the sorting in the tree + // and some other stuff + int Remove(SweepTreeList &list, SweepEventQueue &queue, bool rebalance = true); + int Insert(SweepTreeList &list, SweepEventQueue &queue, Shape *iDst, + int iAtPoint, bool rebalance = true, bool sweepSens = true); + int InsertAt(SweepTreeList &list, SweepEventQueue &queue, Shape *iDst, + SweepTree *insNode, int fromPt, bool rebalance = true, bool sweepSens = true); + + /// Swap nodes, or more exactly, swap the edges in them. + void SwapWithRight(SweepTreeList &list, SweepEventQueue &queue); + + void Avance(Shape *dst, int nPt, Shape *a, Shape *b); + + void Relocate(SweepTree *to); +}; + + +#endif /* !INKSCAPE_LIVAROT_SWEEP_TREE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/CMakeLists.txt b/src/live_effects/CMakeLists.txt new file mode 100644 index 0000000..17a9d46 --- /dev/null +++ b/src/live_effects/CMakeLists.txt @@ -0,0 +1,189 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(live_effects_SRC + effect.cpp + lpe-angle_bisector.cpp + lpe-attach-path.cpp + lpe-bendpath.cpp + lpe-bool.cpp + lpe-bounding-box.cpp + lpe-bspline.cpp + lpe-circle_3pts.cpp + lpe-transform_2pts.cpp + lpe-circle_with_radius.cpp + lpe-clone-original.cpp + lpe-constructgrid.cpp + lpe-copy_rotate.cpp + lpe-curvestitch.cpp + lpe-dashed-stroke.cpp + lpe-dynastroke.cpp + lpe-ellipse_5pts.cpp + lpe-embrodery-stitch.cpp + lpe-embrodery-stitch-ordering.cpp + lpe-envelope.cpp + lpe-extrude.cpp + lpe-fill-between-many.cpp + lpe-fill-between-strokes.cpp + lpe-fillet-chamfer.cpp + lpe-gears.cpp + lpe-interpolate.cpp + lpe-interpolate_points.cpp + lpe-jointype.cpp + lpe-knot.cpp + lpe-lattice.cpp + lpe-lattice2.cpp + lpe-line_segment.cpp + lpe-measure-segments.cpp + lpe-mirror_symmetry.cpp + lpe-offset.cpp + lpe-parallel.cpp + lpe-path_length.cpp + lpe-patternalongpath.cpp + lpe-perp_bisector.cpp + lpe-perspective-envelope.cpp + lpe-powerclip.cpp + lpe-powermask.cpp + lpe-powerstroke.cpp + lpe-recursiveskeleton.cpp + lpe-rough-hatches.cpp + lpe-roughen.cpp + lpe-ruler.cpp + lpe-show_handles.cpp + lpe-simplify.cpp + lpe-skeleton.cpp + lpe-sketch.cpp + lpe-spiro.cpp + lpe-tangent_to_curve.cpp + lpe-taperstroke.cpp + lpe-test-doEffect-stack.cpp + lpe-text_label.cpp + lpegroupbbox.cpp + lpeobject-reference.cpp + lpe-vonkoch.cpp + lpeobject.cpp + spiro-converters.cpp + spiro.cpp + lpe-pts2ellipse.cpp + + parameter/array.cpp + parameter/bool.cpp + parameter/colorpicker.cpp + parameter/hidden.cpp + parameter/item-reference.cpp + parameter/item.cpp + parameter/message.cpp + parameter/originalitemarray.cpp + parameter/originalitem.cpp + parameter/originalpath.cpp + parameter/originalpatharray.cpp + parameter/parameter.cpp + parameter/path-reference.cpp + parameter/path.cpp + parameter/point.cpp + parameter/powerstrokepointarray.cpp + parameter/random.cpp + parameter/satellitesarray.cpp + parameter/text.cpp + parameter/fontbutton.cpp + parameter/togglebutton.cpp + parameter/transformedpoint.cpp + parameter/unit.cpp + parameter/vector.cpp + + + # ------- + # Headers + effect-enum.h + effect.h + lpe-angle_bisector.h + lpe-attach-path.h + lpe-bendpath.h + lpe-bool.h + lpe-bounding-box.h + lpe-bspline.h + lpe-circle_3pts.h + lpe-transform_2pts.h + lpe-circle_with_radius.h + lpe-clone-original.h + lpe-constructgrid.h + lpe-copy_rotate.h + lpe-curvestitch.h + lpe-dashed-stroke.h + lpe-dynastroke.h + lpe-ellipse_5pts.h + lpe-embrodery-stitch.h + lpe-embrodery-stitch-ordering.h + lpe-envelope.h + lpe-extrude.h + lpe-fill-between-many.h + lpe-fill-between-strokes.h + lpe-fillet-chamfer.h + lpe-gears.h + lpe-interpolate.h + lpe-interpolate_points.h + lpe-jointype.h + lpe-knot.h + lpe-lattice.h + lpe-lattice2.h + lpe-line_segment.h + lpe-measure-segments.h + lpe-mirror_symmetry.h + lpe-offset.h + lpe-parallel.h + lpe-path_length.h + lpe-patternalongpath.h + lpe-perp_bisector.h + lpe-perspective-envelope.h + lpe-powerstroke-interpolators.h + lpe-powerclip.h + lpe-powermask.h + lpe-powerstroke.h + lpe-recursiveskeleton.h + lpe-rough-hatches.h + lpe-roughen.h + lpe-ruler.h + lpe-show_handles.h + lpe-simplify.h + lpe-skeleton.h + lpe-sketch.h + lpe-spiro.h + lpe-tangent_to_curve.h + lpe-taperstroke.h + lpe-test-doEffect-stack.h + lpe-text_label.h + lpe-vonkoch.h + lpegroupbbox.h + lpeobject-reference.h + lpeobject.h + spiro-converters.h + spiro.h + lpe-pts2ellipse.h + + parameter/array.h + parameter/bool.h + parameter/colorpicker.h + parameter/hidden.h + parameter/enum.h + parameter/item.h + parameter/message.h + parameter/originalitemarray.cpp + parameter/item-reference.h + parameter/originalitem.h + parameter/originalpath.h + parameter/originalpatharray.h + parameter/parameter.h + parameter/path-reference.h + parameter/path.h + parameter/point.h + parameter/powerstrokepointarray.h + parameter/random.h + parameter/satellitesarray.h + parameter/text.h + parameter/fontbutton.h + parameter/togglebutton.h + parameter/transformedpoint.h + parameter/unit.h + parameter/vector.h +) + +# add_inkscape_lib(live_effects_LIB "${live_effects_SRC}") +add_inkscape_source("${live_effects_SRC}") diff --git a/src/live_effects/README b/src/live_effects/README new file mode 100644 index 0000000..827a62e --- /dev/null +++ b/src/live_effects/README @@ -0,0 +1,8 @@ + + +This directory contains our "Live Path Effects" which create new paths from an existing path, storing the original path for reuse. + +To do: + +* Move to a suitable subdirectory. + diff --git a/src/live_effects/effect-enum.h b/src/live_effects/effect-enum.h new file mode 100644 index 0000000..5fd5614 --- /dev/null +++ b/src/live_effects/effect-enum.h @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_ENUM_H +#define INKSCAPE_LIVEPATHEFFECT_ENUM_H + +/* + * Inkscape::LivePathEffect::EffectType + * + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "util/enums.h" + +namespace Inkscape { +namespace LivePathEffect { + +//Please fill in the same order than in effect.cpp:98 +enum EffectType { + BEND_PATH = 0, + GEARS, + PATTERN_ALONG_PATH, + CURVE_STITCH, + VONKOCH, + KNOT, + CONSTRUCT_GRID, + SPIRO, + ENVELOPE, + INTERPOLATE, + ROUGH_HATCHES, + SKETCH, + RULER, + POWERSTROKE, + CLONE_ORIGINAL, + SIMPLIFY, + LATTICE2, + PERSPECTIVE_ENVELOPE, + INTERPOLATE_POINTS, + TRANSFORM_2PTS, + SHOW_HANDLES, + ROUGHEN, + BSPLINE, + JOIN_TYPE, + TAPER_STROKE, + MIRROR_SYMMETRY, + COPY_ROTATE, + ATTACH_PATH, + FILL_BETWEEN_STROKES, + FILL_BETWEEN_MANY, + ELLIPSE_5PTS, + BOUNDING_BOX, + MEASURE_SEGMENTS, + FILLET_CHAMFER, + BOOL_OP, + POWERCLIP, + POWERMASK, + PTS2ELLIPSE, + OFFSET, + DASHED_STROKE, + ANGLE_BISECTOR, + CIRCLE_WITH_RADIUS, + CIRCLE_3PTS, + EXTRUDE, + LINE_SEGMENT, + PARALLEL, + PERP_BISECTOR, + TANGENT_TO_CURVE, + DOEFFECTSTACK_TEST, + DYNASTROKE, + LATTICE, + PATH_LENGTH, + RECURSIVE_SKELETON, + TEXT_LABEL, + EMBRODERY_STITCH, + INVALID_LPE // This must be last (I made it such that it is not needed anymore I think..., Don't trust on it being + // last. - johan) +}; + +template +struct EnumEffectData { + E id; + const Glib::ustring label; + const Glib::ustring key; + const Glib::ustring icon; + const Glib::ustring untranslated_label; + const Glib::ustring description; + const bool on_path; + const bool on_shape; + const bool on_group; + const bool on_image; + const bool on_text; + const bool experimental; +}; + +const Glib::ustring empty_string(""); + +/** + * Simplified management of enumerations of LPE items with UI labels. + * + * @note that get_id_from_key and get_id_from_label return 0 if it cannot find an entry for that key string. + * @note that get_label and get_key return an empty string when the requested id is not in the list. + */ +template +class EnumEffectDataConverter { + public: + typedef EnumEffectData Data; + + EnumEffectDataConverter(const EnumEffectData *cd, const unsigned int length) + : _length(length) + , _data(cd) + { + } + + E get_id_from_label(const Glib::ustring &label) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].label == label) + return _data[i].id; + } + + return (E)0; + } + + E get_id_from_key(const Glib::ustring &key) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].key == key) + return _data[i].id; + } + + return (E)0; + } + + bool is_valid_key(const Glib::ustring &key) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].key == key) + return true; + } + + return false; + } + + bool is_valid_id(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return true; + } + return false; + } + + const Glib::ustring &get_label(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].label; + } + + return empty_string; + } + + const Glib::ustring &get_key(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].key; + } + + return empty_string; + } + + const Glib::ustring &get_icon(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].icon; + } + + return empty_string; + } + + const Glib::ustring &get_untranslated_label(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].untranslated_label; + } + + return empty_string; + } + + const Glib::ustring &get_description(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].description; + } + + return empty_string; + } + + const bool get_on_path(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_path; + } + + return false; + } + + const bool get_on_shape(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_shape; + } + + return false; + } + + const bool get_on_group(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_group; + } + + return false; + } + + const bool get_on_image(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_image; + } + + return false; + } + + const bool get_on_text(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_text; + } + + return false; + } + + const bool get_experimental(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].experimental; + } + + return false; + } + + const EnumEffectData &data(const unsigned int i) const { return _data[i]; } + + const unsigned int _length; + + private: + const EnumEffectData *_data; +}; + +extern const EnumEffectData LPETypeData[]; /// defined in effect.cpp +extern const EnumEffectDataConverter LPETypeConverter; /// defined in effect.cpp + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/effect.cpp b/src/live_effects/effect.cpp new file mode 100644 index 0000000..783408a --- /dev/null +++ b/src/live_effects/effect.cpp @@ -0,0 +1,1791 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +//#define LPE_ENABLE_TEST_EFFECTS //uncomment for toy effects + +// include effects: +#include "live_effects/lpe-angle_bisector.h" +#include "live_effects/lpe-attach-path.h" +#include "live_effects/lpe-bendpath.h" +#include "live_effects/lpe-bool.h" +#include "live_effects/lpe-bounding-box.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-circle_3pts.h" +#include "live_effects/lpe-circle_with_radius.h" +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpe-constructgrid.h" +#include "live_effects/lpe-copy_rotate.h" +#include "live_effects/lpe-curvestitch.h" +#include "live_effects/lpe-dashed-stroke.h" +#include "live_effects/lpe-dynastroke.h" +#include "live_effects/lpe-ellipse_5pts.h" +#include "live_effects/lpe-embrodery-stitch.h" +#include "live_effects/lpe-envelope.h" +#include "live_effects/lpe-extrude.h" +#include "live_effects/lpe-fill-between-many.h" +#include "live_effects/lpe-fill-between-strokes.h" +#include "live_effects/lpe-fillet-chamfer.h" +#include "live_effects/lpe-gears.h" +#include "live_effects/lpe-interpolate.h" +#include "live_effects/lpe-interpolate_points.h" +#include "live_effects/lpe-jointype.h" +#include "live_effects/lpe-knot.h" +#include "live_effects/lpe-lattice.h" +#include "live_effects/lpe-lattice2.h" +#include "live_effects/lpe-line_segment.h" +#include "live_effects/lpe-measure-segments.h" +#include "live_effects/lpe-mirror_symmetry.h" +#include "live_effects/lpe-offset.h" +#include "live_effects/lpe-parallel.h" +#include "live_effects/lpe-path_length.h" +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpe-perp_bisector.h" +#include "live_effects/lpe-perspective-envelope.h" +#include "live_effects/lpe-powerclip.h" +#include "live_effects/lpe-powermask.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-pts2ellipse.h" +#include "live_effects/lpe-recursiveskeleton.h" +#include "live_effects/lpe-rough-hatches.h" +#include "live_effects/lpe-roughen.h" +#include "live_effects/lpe-ruler.h" +#include "live_effects/lpe-show_handles.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-sketch.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpe-tangent_to_curve.h" +#include "live_effects/lpe-taperstroke.h" +#include "live_effects/lpe-test-doEffect-stack.h" +#include "live_effects/lpe-text_label.h" +#include "live_effects/lpe-transform_2pts.h" +#include "live_effects/lpe-vonkoch.h" + +#include "live_effects/lpeobject.h" + +#include "xml/node-event-vector.h" +#include "xml/sp-css-attr.h" + +#include "display/curve.h" +#include "knotholder.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "ui/icon-loader.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/pen-tool.h" + +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" + +#include +#include +#include +#include + +namespace Inkscape { + +namespace LivePathEffect { + +const EnumEffectData LPETypeData[] = { + // {constant defined in effect-enum.h, N_("name of your effect"), "name of your effect in SVG"} +/* 0.46 */ + { + BEND_PATH + , N_("Bend") //label + , "bend_path" //key + , "bend-path" //icon + , "Bend" //untranslated name + , N_("Bend an object along the curvature of another path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + GEARS + , N_("Gears") //label + , "gears" //key + , "gears" //icon + , "Gears" //untranslated name + , N_("Create interlocking, configurable gears based on the nodes of a path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + PATTERN_ALONG_PATH + , N_("Pattern Along Path") //label + , "skeletal" //key + , "skeletal" //icon + , "Pattern Along Path" //untranslated name + , N_("Place one or more copies of another path along the path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, // for historic reasons, this effect is called skeletal(strokes) in Inkscape:SVG + { + CURVE_STITCH + , N_("Stitch Sub-Paths") //label + , "curvestitching" //key + , "curvestitching" //icon + , "Stitch Sub-Paths" //untranslated name + , N_("Draw perpendicular lines between subpaths of a path, like rungs of a ladder") //description + , true //on_path + , false //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +/* 0.47 */ + { + VONKOCH + , N_("VonKoch") //label + , "vonkoch" //key + , "vonkoch" //icon + , "VonKoch" //untranslated name + , N_("Create VonKoch fractal") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + KNOT + , N_("Knot") //label + , "knot" //key + , "knot" //icon + , "Knot" //untranslated name + , N_("Create gaps in self-intersections, as in Celtic knots") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + CONSTRUCT_GRID + , N_("Construct grid") //label + , "construct_grid" //key + , "construct-grid" //icon + , "Construct grid" //untranslated name + , N_("Create a (perspective) grid from a 3-node path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + SPIRO + , N_("Spiro spline") //label + , "spiro" //key + , "spiro" //icon + , "Spiro spline" //untranslated name + , N_("Make the path curl like wire, using Spiro B-Splines. This effect is usually used directly on the canvas with the Spiro mode of the drawing tools.") //description + , true //on_path + , false //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + ENVELOPE + , N_("Envelope Deformation") //label + , "envelope" //key + , "envelope" //icon + , "Envelope Deformation" //untranslated name + , N_("Adjust the shape of an object by transforming paths on its four sides") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + INTERPOLATE + , N_("Interpolate Sub-Paths") //label + , "interpolate" //key + , "interpolate" //icon + , "Interpolate Sub-Paths" //untranslated name + , N_("Create a stepwise transition between the 2 subpaths of a path") //description + , true //on_path + , false //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + ROUGH_HATCHES + , N_("Hatches (rough)") //label + , "rough_hatches" //key + , "rough-hatches" //icon + , "Hatches (rough)" //untranslated name + , N_("Fill the object with adjustable hatching") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + SKETCH + , N_("Sketch") //label + , "sketch" //key + , "sketch" //icon + , "Sketch" //untranslated name + , N_("Draw multiple short strokes along the path, as in a pencil sketch") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + RULER + , N_("Ruler") //label + , "ruler" //key + , "ruler" //icon + , "Ruler" //untranslated name + , N_("Add ruler marks to the object in adjustable intervals, using the object's stroke style.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +/* 0.91 */ + { + POWERSTROKE + , N_("Power stroke") //label + , "powerstroke" //key + , "powerstroke" //icon + , "Power stroke" //untranslated name + , N_("Create calligraphic strokes and control their variable width and curvature. This effect can also be used directly on the canvas with a pressure sensitive stylus and the Pencil tool.") //description + , true //on_path + , true //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + CLONE_ORIGINAL + , N_("Clone original") //label + , "clone_original" //key + , "clone-original" //icon + , "Clone original" //untranslated name + , N_("Let an object take on the shape, fill, stroke and/or other attributes of another object.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +/* 0.92 */ + { + SIMPLIFY + , N_("Simplify") //label + , "simplify" //key + , "simplify" //icon + , "Simplify" //untranslated name + , N_("Smoothen and simplify a object. This effect is also available in the Pencil tool's tool controls.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + LATTICE2 + , N_("Lattice Deformation 2") //label + , "lattice2" //key + , "lattice2" //icon + , "Lattice Deformation 2" //untranslated name + , N_("Warp an object's shape based on a 5x5 grid") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + PERSPECTIVE_ENVELOPE + , N_("Perspective/Envelope") //label + , "perspective-envelope" //key wrong key with "-" retain because historic + , "perspective-envelope" //icon + , "Perspective/Envelope" //untranslated name + , N_("Transform the object to fit into a shape with four corners, either by stretching it or creating the illusion of a 3D-perspective") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + INTERPOLATE_POINTS + , N_("Interpolate points") //label + , "interpolate_points" //key + , "interpolate-points" //icon + , "Interpolate points" //untranslated name + , N_("Connect the nodes of the object (e.g. corresponding to data points) by different types of lines.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + TRANSFORM_2PTS + , N_("Transform by 2 points") //label + , "transform_2pts" //key + , "transform-2pts" //icon + , "Transform by 2 points" //untranslated name + , N_("Scale, stretch and rotate an object by two handles") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + SHOW_HANDLES + , N_("Show handles") //label + , "show_handles" //key + , "show-handles" //icon + , "Show handles" //untranslated name + , N_("Draw the handles and nodes of objects (replaces the original styling with a black stroke)") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + ROUGHEN + , N_("Roughen") //label + , "roughen" //key + , "roughen" //icon + , "Roughen" //untranslated name + , N_("Roughen an object by adding and randomly shifting new nodes") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + BSPLINE + , N_("BSpline") //label + , "bspline" //key + , "bspline" //icon + , "BSpline" //untranslated name + , N_("Create a BSpline that molds into the path's corners. This effect is usually used directly on the canvas with the BSpline mode of the drawing tools.") //description + , true //on_path + , false //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + JOIN_TYPE + , N_("Join type") //label + , "join_type" //key + , "join-type" //icon + , "Join type" //untranslated name + , N_("Select among various join types for a object's corner nodes (mitre, rounded, extrapolated arc, ...)") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + TAPER_STROKE + , N_("Taper stroke") //label + , "taper_stroke" //key + , "taper-stroke" //icon + , "Taper stroke" //untranslated name + , N_("Let the path's ends narrow down to a tip") //description + , true //on_path + , true //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + MIRROR_SYMMETRY + , N_("Mirror symmetry") //label + , "mirror_symmetry" //key + , "mirror-symmetry" //icon + , "Mirror symmetry" //untranslated name + , N_("Mirror an object along a movable axis, or around the page center. The mirrored copy can be styled independently.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + COPY_ROTATE + , N_("Rotate copies") //label + , "copy_rotate" //key + , "copy-rotate" //icon + , "Rotate copies" //untranslated name + , N_("Create multiple rotated copies of an object, as in a kaleidoscope. The copies can be styled independently.") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +/* Ponyscape -> Inkscape 0.92*/ + { + ATTACH_PATH + , N_("Attach path") //label + , "attach_path" //key + , "attach-path" //icon + , "Attach path" //untranslated name + , N_("Glue the current path's ends to a specific position on one or two other paths") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + FILL_BETWEEN_STROKES + , N_("Fill between strokes") //label + , "fill_between_strokes" //key + , "fill-between-strokes" //icon + , "Fill between strokes" //untranslated name + , N_("Turn the path into a fill between two other open paths (e.g. between two paths with PowerStroke applied to them)") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + FILL_BETWEEN_MANY + , N_("Fill between many") //label + , "fill_between_many" //key + , "fill-between-many" //icon + , "Fill between many" //untranslated name + , N_("Turn the path into a fill between multiple other open paths (e.g. between paths with PowerStroke applied to them)") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + ELLIPSE_5PTS + , N_("Ellipse by 5 points") //label + , "ellipse_5pts" //key + , "ellipse-5pts" //icon + , "Ellipse by 5 points" //untranslated name + , N_("Create an ellipse from 5 nodes on its circumference") //description + , true //on_path + , true //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + BOUNDING_BOX + , N_("Bounding Box") //label + , "bounding_box" //key + , "bounding-box" //icon + , "Bounding Box" //untranslated name + , N_("Turn the path into a bounding box that entirely encompasses another path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +/* 1.0 */ + { + MEASURE_SEGMENTS + , N_("Measure Segments") //label + , "measure_segments" //key + , "measure-segments" //icon + , "Measure Segments" //untranslated name + , N_("Add dimensioning for distances between nodes, optionally with projection and many other configuration options") //description + , true //on_path + , true //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + FILLET_CHAMFER + , N_("Corners (Fillet/Chamfer)") //label + , "fillet_chamfer" //key + , "fillet-chamfer" //icon + , "Corners (Fillet/Chamfer)" //untranslated name + , N_("Adjust the shape of a path's corners, rounding them to a specified radius, or cutting them off") //description + , true //on_path + , true //on_shape + , false //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + BOOL_OP + , N_("Boolean operation") //label + , "bool_op" //key + , "experimental" //icon + , "Boolean operation" //untranslated name + , N_("Cut, union, subtract, intersect and divide a path non-destructively with another path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + POWERCLIP + , N_("Power clip") //label + , "powerclip" //key + , "powerclip" //icon + , "Power clip" //untranslated name + , N_("Invert, hide or flatten a clip (apply like a Boolean operation)") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + POWERMASK + , N_("Power mask") //label + , "powermask" //key + , "powermask" //icon + , "Power mask" //untranslated name + , N_("Invert or hide a mask, or use its negative") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + PTS2ELLIPSE + , N_("Ellipse from points") //label + , "pts2ellipse" //key + , "pts2ellipse" //icon + , "Ellipse from points" //untranslated name + , N_("Draw a circle, ellipse, arc or slice based on the nodes of a path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + OFFSET + , N_("Offset") //label + , "offset" //key + , "offset" //icon + , "Offset" //untranslated name + , N_("Offset the path, optionally keeping cusp corners cusp") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + DASHED_STROKE + , N_("Dashed Stroke") //label + , "dashed_stroke" //key + , "dashed-stroke" //icon + , "Dashed Stroke" //untranslated name + , N_("Add a dashed stroke whose dashes end exactly on a node, optionally with the same number of dashes per path segment") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, + { + ANGLE_BISECTOR + , N_("Angle bisector") //label + , "angle_bisector" //key + , "experimental" //icon + , "Angle bisector" //untranslated name + , N_("Draw a line that halves the angle between the first three nodes of the path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + CIRCLE_WITH_RADIUS + , N_("Circle (by center and radius)") //label + , "circle_with_radius" //key + , "experimental" //icon + , "Circle (by center and radius)" //untranslated name + , N_("Draw a circle, where the first node of the path is the center, and the last determines its radius") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + CIRCLE_3PTS + , N_("Circle by 3 points") //label + , "circle_3pts" //key + , "experimental" //icon + , "Circle by 3 points" //untranslated name + , N_("Draw a circle whose circumference passes through the first three nodes of the path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + EXTRUDE + , N_("Extrude") //label + , "extrude" //key + , "experimental" //icon + , "Extrude" //untranslated name + , N_("Extrude the path, creating a face for each path segment") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + LINE_SEGMENT + , N_("Line Segment") //label + , "line_segment" //key + , "experimental" //icon + , "Line Segment" //untranslated name + , N_("Draw a straight line that connects the first and last node of a path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + PARALLEL + , N_("Parallel") //label + , "parallel" //key + , "experimental" //icon + , "Parallel" //untranslated name + , N_("Create a draggable line that will always be parallel to a two-node path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + PERP_BISECTOR + , N_("Perpendicular bisector") //label + , "perp_bisector" //key + , "experimental" //icon + , "Perpendicular bisector" //untranslated name + , N_("Draw a perpendicular line in the middle of the (imaginary) line that connects the start and end nodes") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + TANGENT_TO_CURVE + , N_("Tangent to curve") //label + , "tangent_to_curve" //key + , "experimental" //icon + , "Tangent to curve" //untranslated name + , N_("Draw a tangent with variable length and additional angle that can be moved along the path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, +#ifdef LPE_ENABLE_TEST_EFFECTS + { + DOEFFECTSTACK_TEST + , N_("doEffect stack test") //label + , "doeffectstacktest" //key + , "experimental" //icon + , "doEffect stack test" //untranslated name + , N_("Test LPE") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + DYNASTROKE + , N_("Dynamic stroke") //label + , "dynastroke" //key + , "experimental" //icon + , "Dynamic stroke" //untranslated name + , N_("Create calligraphic strokes with variably shaped ends, making use of a parameter for the brush angle") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + LATTICE + , N_("Lattice Deformation") //label + , "lattice" //key + , "experimental" //icon + , "Lattice Deformation" //untranslated name + , N_("Deform an object using a 4x4 grid") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + PATH_LENGTH + , N_("Path length") //label + , "path_length" //key + , "experimental" //icon + , "Path length" //untranslated name + , N_("Display the total length of a (curved) path") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + RECURSIVE_SKELETON + , N_("Recursive skeleton") //label + , "recursive_skeleton" //key + , "experimental" //icon + , "Recursive skeleton" //untranslated name + , N_("Draw a path recursively") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + TEXT_LABEL + , N_("Text label") //label + , "text_label" //key + , "experimental" //icon + , "Text label" //untranslated name + , N_("Add a label for the object") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , true //experimental + }, + { + EMBRODERY_STITCH + , N_("Embroidery stitch") //label + , "embrodery_stitch" //key + , "embrodery-stitch" //icon + , "Embroidery stitch" //untranslated name + , N_("Embroidery stitch") //description + , true //on_path + , true //on_shape + , true //on_group + , false //on_image + , false //on_text + , false //experimental + }, +#endif + +}; + +const EnumEffectDataConverter LPETypeConverter(LPETypeData, sizeof(LPETypeData) / sizeof(*LPETypeData)); + +int +Effect::acceptsNumClicks(EffectType type) { + switch (type) { + case INVALID_LPE: return -1; // in case we want to distinguish between invalid LPE and valid ones that expect zero clicks + case ANGLE_BISECTOR: return 3; + case CIRCLE_3PTS: return 3; + case CIRCLE_WITH_RADIUS: return 2; + case LINE_SEGMENT: return 2; + case PERP_BISECTOR: return 2; + default: return 0; + } +} + +Effect* +Effect::New(EffectType lpenr, LivePathEffectObject *lpeobj) +{ + Effect* neweffect = nullptr; + switch (lpenr) { + case EMBRODERY_STITCH: + neweffect = static_cast ( new LPEEmbroderyStitch(lpeobj) ); + break; + case BOOL_OP: + neweffect = static_cast ( new LPEBool(lpeobj) ); + break; + case PATTERN_ALONG_PATH: + neweffect = static_cast ( new LPEPatternAlongPath(lpeobj) ); + break; + case BEND_PATH: + neweffect = static_cast ( new LPEBendPath(lpeobj) ); + break; + case SKETCH: + neweffect = static_cast ( new LPESketch(lpeobj) ); + break; + case ROUGH_HATCHES: + neweffect = static_cast ( new LPERoughHatches(lpeobj) ); + break; + case VONKOCH: + neweffect = static_cast ( new LPEVonKoch(lpeobj) ); + break; + case KNOT: + neweffect = static_cast ( new LPEKnot(lpeobj) ); + break; + case GEARS: + neweffect = static_cast ( new LPEGears(lpeobj) ); + break; + case CURVE_STITCH: + neweffect = static_cast ( new LPECurveStitch(lpeobj) ); + break; + case LATTICE: + neweffect = static_cast ( new LPELattice(lpeobj) ); + break; + case ENVELOPE: + neweffect = static_cast ( new LPEEnvelope(lpeobj) ); + break; + case CIRCLE_WITH_RADIUS: + neweffect = static_cast ( new LPECircleWithRadius(lpeobj) ); + break; + case SPIRO: + neweffect = static_cast ( new LPESpiro(lpeobj) ); + break; + case CONSTRUCT_GRID: + neweffect = static_cast ( new LPEConstructGrid(lpeobj) ); + break; + case PERP_BISECTOR: + neweffect = static_cast ( new LPEPerpBisector(lpeobj) ); + break; + case TANGENT_TO_CURVE: + neweffect = static_cast ( new LPETangentToCurve(lpeobj) ); + break; + case MIRROR_SYMMETRY: + neweffect = static_cast ( new LPEMirrorSymmetry(lpeobj) ); + break; + case CIRCLE_3PTS: + neweffect = static_cast ( new LPECircle3Pts(lpeobj) ); + break; + case ANGLE_BISECTOR: + neweffect = static_cast ( new LPEAngleBisector(lpeobj) ); + break; + case PARALLEL: + neweffect = static_cast ( new LPEParallel(lpeobj) ); + break; + case COPY_ROTATE: + neweffect = static_cast ( new LPECopyRotate(lpeobj) ); + break; + case OFFSET: + neweffect = static_cast ( new LPEOffset(lpeobj) ); + break; + case RULER: + neweffect = static_cast ( new LPERuler(lpeobj) ); + break; + case INTERPOLATE: + neweffect = static_cast ( new LPEInterpolate(lpeobj) ); + break; + case INTERPOLATE_POINTS: + neweffect = static_cast ( new LPEInterpolatePoints(lpeobj) ); + break; + case TEXT_LABEL: + neweffect = static_cast ( new LPETextLabel(lpeobj) ); + break; + case PATH_LENGTH: + neweffect = static_cast ( new LPEPathLength(lpeobj) ); + break; + case LINE_SEGMENT: + neweffect = static_cast ( new LPELineSegment(lpeobj) ); + break; + case DOEFFECTSTACK_TEST: + neweffect = static_cast ( new LPEdoEffectStackTest(lpeobj) ); + break; + case BSPLINE: + neweffect = static_cast ( new LPEBSpline(lpeobj) ); + break; + case DYNASTROKE: + neweffect = static_cast ( new LPEDynastroke(lpeobj) ); + break; + case RECURSIVE_SKELETON: + neweffect = static_cast ( new LPERecursiveSkeleton(lpeobj) ); + break; + case EXTRUDE: + neweffect = static_cast ( new LPEExtrude(lpeobj) ); + break; + case POWERSTROKE: + neweffect = static_cast ( new LPEPowerStroke(lpeobj) ); + break; + case CLONE_ORIGINAL: + neweffect = static_cast ( new LPECloneOriginal(lpeobj) ); + break; + case ATTACH_PATH: + neweffect = static_cast ( new LPEAttachPath(lpeobj) ); + break; + case FILL_BETWEEN_STROKES: + neweffect = static_cast ( new LPEFillBetweenStrokes(lpeobj) ); + break; + case FILL_BETWEEN_MANY: + neweffect = static_cast ( new LPEFillBetweenMany(lpeobj) ); + break; + case ELLIPSE_5PTS: + neweffect = static_cast ( new LPEEllipse5Pts(lpeobj) ); + break; + case BOUNDING_BOX: + neweffect = static_cast ( new LPEBoundingBox(lpeobj) ); + break; + case JOIN_TYPE: + neweffect = static_cast ( new LPEJoinType(lpeobj) ); + break; + case TAPER_STROKE: + neweffect = static_cast ( new LPETaperStroke(lpeobj) ); + break; + case SIMPLIFY: + neweffect = static_cast ( new LPESimplify(lpeobj) ); + break; + case LATTICE2: + neweffect = static_cast ( new LPELattice2(lpeobj) ); + break; + case PERSPECTIVE_ENVELOPE: + neweffect = static_cast ( new LPEPerspectiveEnvelope(lpeobj) ); + break; + case FILLET_CHAMFER: + neweffect = static_cast ( new LPEFilletChamfer(lpeobj) ); + break; + case POWERCLIP: + neweffect = static_cast ( new LPEPowerClip(lpeobj) ); + break; + case POWERMASK: + neweffect = static_cast ( new LPEPowerMask(lpeobj) ); + break; + case ROUGHEN: + neweffect = static_cast ( new LPERoughen(lpeobj) ); + break; + case SHOW_HANDLES: + neweffect = static_cast ( new LPEShowHandles(lpeobj) ); + break; + case TRANSFORM_2PTS: + neweffect = static_cast ( new LPETransform2Pts(lpeobj) ); + break; + case MEASURE_SEGMENTS: + neweffect = static_cast ( new LPEMeasureSegments(lpeobj) ); + break; + case PTS2ELLIPSE: + neweffect = static_cast ( new LPEPts2Ellipse(lpeobj) ); + break; + case DASHED_STROKE: + neweffect = static_cast(new LPEDashedStroke(lpeobj)); + break; + default: + g_warning("LivePathEffect::Effect::New called with invalid patheffect type (%d)", lpenr); + neweffect = nullptr; + break; + } + + if (neweffect) { + neweffect->readallParameters(lpeobj->getRepr()); + } + + return neweffect; +} + +void Effect::createAndApply(const char* name, SPDocument *doc, SPItem *item) +{ + // Path effect definition + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("inkscape:path-effect"); + repr->setAttribute("effect", name); + + doc->getDefs()->getRepr()->addChild(repr, nullptr); // adds to and assigns the 'id' attribute + const gchar * repr_id = repr->attribute("id"); + Inkscape::GC::release(repr); + + gchar *href = g_strdup_printf("#%s", repr_id); + SP_LPE_ITEM(item)->addPathEffect(href, true); + g_free(href); +} + +void +Effect::createAndApply(EffectType type, SPDocument *doc, SPItem *item) +{ + createAndApply(LPETypeConverter.get_key(type).c_str(), doc, item); +} + +Effect::Effect(LivePathEffectObject *lpeobject) + : apply_to_clippath_and_mask(false), + _provides_knotholder_entities(false), + oncanvasedit_it(0), + is_visible(_("Is visible?"), _("If unchecked, the effect remains applied to the object but is temporarily disabled on canvas"), "is_visible", &wr, this, true), + lpeversion(_("Version"), _("LPE version"), "lpeversion", &wr, this, "0", true), + show_orig_path(false), + keep_paths(false), + is_load(true), + lpeobj(lpeobject), + concatenate_before_pwd2(false), + sp_lpe_item(nullptr), + current_zoom(1), + refresh_widgets(false), + current_shape(nullptr), + provides_own_flash_paths(true), // is automatically set to false if providesOwnFlashPaths() is not overridden + defaultsopen(false), + is_ready(false), + is_applied(false) +{ + registerParameter( dynamic_cast(&is_visible) ); + registerParameter( dynamic_cast(&lpeversion) ); + is_visible.widget_is_visible = false; + current_zoom = 0.0; +} + +Effect::~Effect() = default; + +Glib::ustring +Effect::getName() const +{ + if (lpeobj->effecttype_set && LPETypeConverter.is_valid_id(lpeobj->effecttype) ) + return Glib::ustring( _(LPETypeConverter.get_label(lpeobj->effecttype).c_str()) ); + else + return Glib::ustring( _("No effect") ); +} + +EffectType +Effect::effectType() const { + return lpeobj->effecttype; +} + +/** + * Is performed a single time when the effect is freshly applied to a path + */ +void +Effect::doOnApply (SPLPEItem const*/*lpeitem*/) +{ +} + +void +Effect::setCurrentZoom(double cZ) +{ + current_zoom = cZ; +} + +/** + * Overrided function to apply transforms for example to powerstrole, jointtype or tapperstroke + */ +void Effect::transform_multiply(Geom::Affine const &postmul, bool /*set*/) {} + +void +Effect::setSelectedNodePoints(std::vector sNP) +{ + selectedNodesPoints = sNP; +} + +bool +Effect::isNodePointSelected(Geom::Point const &nodePoint) const +{ + if (selectedNodesPoints.size() > 0) { + using Geom::X; + using Geom::Y; + for (auto p : selectedNodesPoints) { + Geom::Affine transformCoordinate = sp_lpe_item->i2dt_affine(); + Geom::Point p2(nodePoint[X],nodePoint[Y]); + p2 *= transformCoordinate; + if (Geom::are_near(p, p2, 0.01)) { + return true; + } + } + } + return false; +} + +void +Effect::processObjects(LPEAction lpe_action) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + for (auto id : items) { + if (id.empty()) { + return; + } + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById(id.c_str()))) { + Inkscape::XML::Node * elemnode = elemref->getRepr(); + std::vector item_list; + item_list.push_back(SP_ITEM(elemref)); + std::vector item_to_select; + std::vector item_selected; + SPCSSAttr *css; + Glib::ustring css_str; + SPItem *item = SP_ITEM(elemref); + switch (lpe_action){ + case LPE_TO_OBJECTS: + if (item->isHidden()) { + item->deleteObject(true); + } else { + if (elemnode->attribute("inkscape:path-effect")) { + sp_item_list_to_curves(item_list, item_selected, item_to_select); + } + elemnode->removeAttribute("sodipodi:insensitive"); + if (!SP_IS_DEFS(SP_ITEM(elemref)->parent)) { + SP_ITEM(elemref)->moveTo(SP_ITEM(sp_lpe_item), false); + } + } + break; + + case LPE_ERASE: + item->deleteObject(true); + break; + + case LPE_VISIBILITY: + css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, elemref->getRepr()->attribute("style")); + if (!this->isVisible()/* && std::strcmp(elemref->getId(),sp_lpe_item->getId()) != 0*/) { + css->setAttribute("display", "none"); + } else { + css->removeAttribute("display"); + } + sp_repr_css_write_string(css,css_str); + elemnode->setAttributeOrRemoveIfEmpty("style", css_str); + break; + + default: + break; + } + } + } + if (lpe_action == LPE_ERASE || lpe_action == LPE_TO_OBJECTS) { + items.clear(); + } +} + +/** + * Is performed each time before the effect is updated. + */ +void +Effect::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + //Do nothing for simple effects +} + +void Effect::doAfterEffect (SPLPEItem const* /*lpeitem*/) +{ + is_load = false; +} + +void Effect::doOnException(SPLPEItem const * /*lpeitem*/) +{ + has_exception = true; + pathvector_after_effect = pathvector_before_effect; +} + + +void Effect::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ +} +void Effect::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ +} +//secret impl methods (shhhh!) +void Effect::doAfterEffect_impl(SPLPEItem const *lpeitem) +{ + doAfterEffect(lpeitem); + is_load = false; + is_applied = false; +} +void Effect::doOnApply_impl(SPLPEItem const* lpeitem) +{ + sp_lpe_item = const_cast(lpeitem); + is_applied = true; + doOnApply(lpeitem); + setReady(); + has_exception = false; + lpeversion.param_setValue("1", true); // we can override this value in each LPE to major versions I dont want to + // repeat inkscape versioning +} + +void Effect::doBeforeEffect_impl(SPLPEItem const* lpeitem) +{ + sp_lpe_item = const_cast(lpeitem); + doBeforeEffect(lpeitem); + update_helperpath(); +} + +/** + * Effects can have a parameter path set before they are applied by accepting a nonzero number of + * mouse clicks. This method activates the pen context, which waits for the specified number of + * clicks. Override Effect::acceptsNumClicks() to return the number of expected mouse clicks. + */ +void +Effect::doAcceptPathPreparations(SPLPEItem *lpeitem) +{ + // switch to pen context + SPDesktop *desktop = SP_ACTIVE_DESKTOP; // TODO: Is there a better method to find the item's desktop? + if (!tools_isactive(desktop, TOOLS_FREEHAND_PEN)) { + tools_switch(desktop, TOOLS_FREEHAND_PEN); + } + + Inkscape::UI::Tools::ToolBase *ec = desktop->event_context; + Inkscape::UI::Tools::PenTool *pc = SP_PEN_CONTEXT(ec); + pc->expecting_clicks_for_LPE = this->acceptsNumClicks(); + pc->waiting_LPE = this; + pc->waiting_item = lpeitem; + pc->polylines_only = true; + + ec->desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, + g_strdup_printf(_("Please specify a parameter path for the LPE '%s' with %d mouse clicks"), + getName().c_str(), acceptsNumClicks())); +} + +void +Effect::writeParamsToSVG() { + std::vector::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + (*p)->write_to_SVG(); + } +} + +/** + * If the effect expects a path parameter (specified by a number of mouse clicks) before it is + * applied, this is the method that processes the resulting path. Override it to customize it for + * your LPE. But don't forget to call the parent method so that is_ready is set to true! + */ +void +Effect::acceptParamPath (SPPath const*/*param_path*/) { + setReady(); +} + +/* + * Here be the doEffect function chain: + */ +void +Effect::doEffect (SPCurve * curve) +{ + Geom::PathVector orig_pathv = curve->get_pathvector(); + + Geom::PathVector result_pathv = doEffect_path(orig_pathv); + + curve->set_pathvector(result_pathv); +} + +Geom::PathVector +Effect::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + + if ( !concatenate_before_pwd2 ) { + // default behavior + for (const auto & i : path_in) { + Geom::Piecewise > pwd2_in = i.toPwSb(); + Geom::Piecewise > pwd2_out = doEffect_pwd2(pwd2_in); + Geom::PathVector path = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE); + // add the output path vector to the already accumulated vector: + for (const auto & j : path) { + path_out.push_back(j); + } + } + } else { + // concatenate the path into possibly discontinuous pwd2 + Geom::Piecewise > pwd2_in; + for (const auto & i : path_in) { + pwd2_in.concat( i.toPwSb() ); + } + Geom::Piecewise > pwd2_out = doEffect_pwd2(pwd2_in); + path_out = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE); + } + + return path_out; +} + +Geom::Piecewise > +Effect::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + g_warning("Effect has no doEffect implementation"); + return pwd2_in; +} + +void +Effect::readallParameters(Inkscape::XML::Node const* repr) +{ + std::vector::iterator it = param_vector.begin(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + while (it != param_vector.end()) { + Parameter * param = *it; + const gchar * key = param->param_key.c_str(); + const gchar * value = repr->attribute(key); + if (value) { + bool accepted = param->param_readSVGValue(value); + if (!accepted) { + g_warning("Effect::readallParameters - '%s' not accepted for %s", value, key); + } + } else { + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)key; + bool valid = prefs->getEntry(pref_path).isValid(); + if(valid){ + param->param_update_default(prefs->getString(pref_path).c_str()); + } else { + param->param_set_default(); + } + } + ++it; + } +} + +/* This function does not and SHOULD NOT write to XML */ +void +Effect::setParameter(const gchar * key, const gchar * new_value) +{ + Parameter * param = getParameter(key); + if (param) { + if (new_value) { + bool accepted = param->param_readSVGValue(new_value); + if (!accepted) { + g_warning("Effect::setParameter - '%s' not accepted for %s", new_value, key); + } + } else { + // set default value + param->param_set_default(); + } + } +} + +void +Effect::registerParameter(Parameter * param) +{ + param_vector.push_back(param); +} + + +/** + * Add all registered LPE knotholder handles to the knotholder + */ +void +Effect::addHandles(KnotHolder *knotholder, SPItem *item) { + using namespace Inkscape::LivePathEffect; + + // add handles provided by the effect itself + addKnotHolderEntities(knotholder, item); + + // add handles provided by the effect's parameters (if any) + for (auto & p : param_vector) { + p->addKnotHolderEntities(knotholder, item); + } +} + +/** + * Return a vector of PathVectors which contain all canvas indicators for this effect. + * This is the function called by external code to get all canvas indicators (effect and its parameters) + * lpeitem = the item onto which this effect is applied + * @todo change return type to one pathvector, add all paths to one pathvector instead of maintaining a vector of pathvectors + */ +std::vector +Effect::getCanvasIndicators(SPLPEItem const* lpeitem) +{ + std::vector hp_vec; + + // add indicators provided by the effect itself + addCanvasIndicators(lpeitem, hp_vec); + + // add indicators provided by the effect's parameters + for (auto & p : param_vector) { + p->addCanvasIndicators(lpeitem, hp_vec); + } + + return hp_vec; +} + +/** + * Add possible canvas indicators (i.e., helperpaths other than the original path) to \a hp_vec + * This function should be overwritten by derived effects if they want to provide their own helperpaths. + */ +void +Effect::addCanvasIndicators(SPLPEItem const*/*lpeitem*/, std::vector &/*hp_vec*/) +{ +} + +/** + * Call to a method on nodetool to update the helper path from the effect + */ +void +Effect::update_helperpath() { + Inkscape::UI::Tools::sp_update_helperpath(); +} + +/** + * This *creates* a new widget, management of deletion should be done by the caller + */ +Gtk::Widget * +Effect::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox() ); + + vbox->set_border_width(5); + + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = param->param_newWidget(); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if (param->widget_is_enabled) { + widg->set_sensitive(true); + } else { + widg->set_sensitive(false); + } + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +bool sp_enter_tooltip(GdkEventCrossing *evt, Gtk::Widget *widg) +{ + widg->trigger_tooltip_query(); + return true; +} + +/** + * This *creates* a new widget, with default values setter + */ +Gtk::Widget * +Effect::defaultParamSet() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::VBox * vbox_expander = Gtk::manage( new Gtk::VBox() ); + Glib::ustring effectname = (Glib::ustring)Inkscape::LivePathEffect::LPETypeConverter.get_label(effectType()); + Glib::ustring effectkey = (Glib::ustring)Inkscape::LivePathEffect::LPETypeConverter.get_key(effectType()); + std::vector::iterator it = param_vector.begin(); + bool has_params = false; + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + has_params = true; + Parameter * param = *it; + const gchar * key = param->param_key.c_str(); + const gchar * label = param->param_label.c_str(); + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Glib::ustring pref_path = "/live_effects/"; + pref_path += effectkey; + pref_path +="/"; + pref_path += key; + bool valid = prefs->getEntry(pref_path).isValid(); + const gchar * set_or_upd; + Glib::ustring def = Glib::ustring(_("Default value: ")) + defvalue + Glib::ustring("\n"); + Glib::ustring ove = Glib::ustring(_("Default value overridden: ")) + Glib::ustring(prefs->getString(pref_path)) + Glib::ustring("\n"); + if (valid) { + set_or_upd = _("Update"); + def = Glib::ustring(_("Default value: ")) + defvalue + Glib::ustring("\n"); + } else { + set_or_upd = _("Set"); + ove = Glib::ustring(_("Default value overridden: None\n")); + } + Gtk::HBox * vbox_param = Gtk::manage( new Gtk::HBox(true) ); + Gtk::HBox *namedicon = Gtk::manage(new Gtk::HBox(true)); + Gtk::Label *parameter_label = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START)); + parameter_label->set_use_markup(true); + parameter_label->set_use_underline(true); + parameter_label->set_ellipsize(Pango::ELLIPSIZE_END); + Glib::ustring tooltip = Glib::ustring("") + parameter_label->get_text() + Glib::ustring("\n") + + param->param_tooltip + Glib::ustring("\n\n"); + Gtk::Image *info = sp_get_icon_image("info", 20); + Gtk::EventBox *infoeventbox = Gtk::manage(new Gtk::EventBox()); + infoeventbox->add(*info); + infoeventbox->set_tooltip_markup((tooltip + def + ove).c_str()); + namedicon->pack_start(*infoeventbox, false, false, 2); + namedicon->pack_start(*parameter_label, true, true, 2); + namedicon->set_homogeneous(false); + vbox_param->pack_start(*namedicon, true, true, 2); + Gtk::Button *set = Gtk::manage(new Gtk::Button((Glib::ustring)set_or_upd)); + Gtk::Button *unset = Gtk::manage(new Gtk::Button(Glib::ustring(_("Unset")))); + unset->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Effect::unsetDefaultParam), pref_path, + tooltip, param, info, set, unset)); + set->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Effect::setDefaultParam), pref_path, tooltip, + param, info, set, unset)); + if (!valid) { + unset->set_sensitive(false); + } + vbox_param->pack_start(*set, true, true, 2); + vbox_param->pack_start(*unset, true, true, 2); + + vbox_expander->pack_start(*vbox_param, true, true, 2); + } + ++it; + } + Glib::ustring tip = "" + effectname + (Glib::ustring)_(": Set default parameters"); + Gtk::Expander * expander = Gtk::manage(new Gtk::Expander(tip)); + expander->set_use_markup(true); + expander->add(*vbox_expander); + expander->set_expanded(defaultsopen); + expander->property_expanded().signal_changed().connect(sigc::bind<0>(sigc::mem_fun(*this, &Effect::onDefaultsExpanderChanged), expander )); + if (has_params) { + Gtk::Widget *vboxwidg = dynamic_cast(expander); + vboxwidg->set_margin_bottom(5); + vboxwidg->set_margin_top(5); + return vboxwidg; + } else { + return nullptr; + } +} + +void +Effect::onDefaultsExpanderChanged(Gtk::Expander * expander) +{ + defaultsopen = expander->get_expanded(); +} + +void Effect::setDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset) +{ + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_path, value); + gchar * label = _("Update"); + set->set_label((Glib::ustring)label); + unset->set_sensitive(true); + Glib::ustring def = Glib::ustring(_("Default value: ")) + defvalue + Glib::ustring("\n"); + Glib::ustring ove = Glib::ustring(_("Default value overridden: ")) + value + Glib::ustring("\n"); + info->set_tooltip_markup((tooltip + def + ove).c_str()); +} + +void Effect::unsetDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset) +{ + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->remove(pref_path); + gchar * label = _("Set"); + set->set_label((Glib::ustring)label); + unset->set_sensitive(false); + Glib::ustring def = Glib::ustring(_("Default value: ")) + defvalue + Glib::ustring("\n"); + Glib::ustring ove = Glib::ustring(_("Default value overridden: None\n")); + info->set_tooltip_markup((tooltip + def + ove).c_str()); +} + +Inkscape::XML::Node *Effect::getRepr() +{ + return lpeobj->getRepr(); +} + +SPDocument *Effect::getSPDoc() +{ + if (lpeobj->document == nullptr) { + g_message("Effect::getSPDoc() returns NULL"); + } + return lpeobj->document; +} + +Parameter * +Effect::getParameter(const char * key) +{ + Glib::ustring stringkey(key); + + if (param_vector.empty()) return nullptr; + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + Parameter * param = *it; + if ( param->param_key == key) { + return param; + } + + ++it; + } + + return nullptr; +} + +Parameter * +Effect::getNextOncanvasEditableParam() +{ + if (param_vector.size() == 0) // no parameters + return nullptr; + + oncanvasedit_it++; + if (oncanvasedit_it >= static_cast(param_vector.size())) { + oncanvasedit_it = 0; + } + int old_it = oncanvasedit_it; + + do { + Parameter * param = param_vector[oncanvasedit_it]; + if(param && param->oncanvas_editable) { + return param; + } else { + oncanvasedit_it++; + if (oncanvasedit_it == static_cast(param_vector.size())) { // loop round the map + oncanvasedit_it = 0; + } + } + } while (oncanvasedit_it != old_it); // iterate until complete loop through map has been made + + return nullptr; +} + +void +Effect::editNextParamOncanvas(SPItem * item, SPDesktop * desktop) +{ + if (!desktop) return; + + Parameter * param = getNextOncanvasEditableParam(); + if (param) { + param->param_editOncanvas(item, desktop); + gchar *message = g_strdup_printf(_("Editing parameter %s."), param->param_label.c_str()); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, message); + g_free(message); + } else { + desktop->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("None of the applied path effect's parameters can be edited on-canvas.") ); + } +} + +/* This function should reset the defaults and is used for example to initialize an effect right after it has been applied to a path +* The nice thing about this is that this function can use knowledge of the original path and set things accordingly for example to the size or origin of the original path! +*/ +void +Effect::resetDefaults(SPItem const* /*item*/) +{ + std::vector::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + (*p)->param_set_default(); + (*p)->write_to_SVG(); + } +} + +bool +Effect::providesKnotholder() const +{ + // does the effect actively provide any knotholder entities of its own? + if (_provides_knotholder_entities) { + return true; + } + + // otherwise: are there any parameters that have knotholderentities? + for (auto p : param_vector) { + if (p->providesKnotHolderEntities()) { + return true; + } + } + + return false; +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/effect.h b/src/live_effects/effect.h new file mode 100644 index 0000000..b216334 --- /dev/null +++ b/src/live_effects/effect.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_H +#define INKSCAPE_LIVEPATHEFFECT_H + +/* + * Copyright (C) Johan Engelen 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "effect-enum.h" +#include "parameter/bool.h" +#include "parameter/hidden.h" +#include "ui/widget/registry.h" +#include <2geom/forward.h> +#include +#include +#include + + +#define LPE_CONVERSION_TOLERANCE 0.01 // FIXME: find good solution for this. + +class SPDocument; +class SPDesktop; +class SPItem; +class LivePathEffectObject; +class SPLPEItem; +class KnotHolder; +class KnotHolderEntity; +class SPPath; +class SPCurve; + +namespace Gtk { + class Widget; +} + +namespace Inkscape { + +namespace XML { + class Node; +} + +namespace LivePathEffect { + +enum LPEPathFlashType { + SUPPRESS_FLASH, +// PERMANENT_FLASH, + DEFAULT +}; + +enum LPEAction { + LPE_ERASE = 0, + LPE_TO_OBJECTS, + LPE_VISIBILITY +}; + +class Effect { +public: + static Effect* New(EffectType lpenr, LivePathEffectObject *lpeobj); + static void createAndApply(const char* name, SPDocument *doc, SPItem *item); + static void createAndApply(EffectType type, SPDocument *doc, SPItem *item); + + virtual ~Effect(); + Effect(const Effect&) = delete; + Effect& operator=(const Effect&) = delete; + + EffectType effectType() const; + + //basically, to get this method called before the derived classes, a bit + //of indirection is needed. We first call these methods, then the below. + void doAfterEffect_impl(SPLPEItem const *lpeitem); + void doOnApply_impl(SPLPEItem const* lpeitem); + void doBeforeEffect_impl(SPLPEItem const* lpeitem); + void setCurrentZoom(double cZ); + void setSelectedNodePoints(std::vector sNP); + bool isNodePointSelected(Geom::Point const &nodePoint) const; + virtual void doOnApply (SPLPEItem const* lpeitem); + virtual void doBeforeEffect (SPLPEItem const* lpeitem); + virtual void transform_multiply(Geom::Affine const &postmul, bool set); + virtual void doAfterEffect (SPLPEItem const* lpeitem); + virtual void doOnException(SPLPEItem const *lpeitem); + virtual void doOnRemove (SPLPEItem const* lpeitem); + virtual void doOnVisibilityToggled(SPLPEItem const* lpeitem); + void writeParamsToSVG(); + + virtual void acceptParamPath (SPPath const* param_path); + static int acceptsNumClicks(EffectType type); + int acceptsNumClicks() const { return acceptsNumClicks(effectType()); } + void doAcceptPathPreparations(SPLPEItem *lpeitem); + SPShape * getCurrentShape() const { return current_shape; }; + void setCurrentShape(SPShape * shape) { current_shape = shape; } + void processObjects(LPEAction lpe_action); + + /* + * isReady() indicates whether all preparations which are necessary to apply the LPE are done, + * e.g., waiting for a parameter path either before the effect is created or when it needs a + * path as argument. This is set in SPLPEItem::addPathEffect(). + */ + inline bool isReady() const { return is_ready; } + inline void setReady(bool ready = true) { is_ready = ready; } + + virtual void doEffect (SPCurve * curve); + + virtual Gtk::Widget * newWidget(); + virtual Gtk::Widget * defaultParamSet(); + /** + * Sets all parameters to their default values and writes them to SVG. + */ + virtual void resetDefaults(SPItem const* item); + + // /TODO: providesKnotholder() is currently used as an indicator of whether a nodepath is + // created for an item or not. When we allow both at the same time, this needs rethinking! + bool providesKnotholder() const; + // /TODO: in view of providesOwnFlashPaths() below, this is somewhat redundant + // (but spiro lpe still needs it!) + virtual LPEPathFlashType pathFlashType() const { return DEFAULT; } + void addHandles(KnotHolder *knotholder, SPItem *item); + std::vector getCanvasIndicators(SPLPEItem const* lpeitem); + void update_helperpath(); + bool has_exception; + + inline bool providesOwnFlashPaths() const { + return provides_own_flash_paths || show_orig_path; + } + inline bool showOrigPath() const { return show_orig_path; } + + Glib::ustring getName() const; + Inkscape::XML::Node * getRepr(); + SPDocument * getSPDoc(); + LivePathEffectObject * getLPEObj() {return lpeobj;}; + LivePathEffectObject const * getLPEObj() const {return lpeobj;}; + Parameter * getParameter(const char * key); + + void readallParameters(Inkscape::XML::Node const* repr); + void setParameter(const gchar * key, const gchar * new_value); + + inline bool isVisible() const { return is_visible; } + + void editNextParamOncanvas(SPItem * item, SPDesktop * desktop); + bool apply_to_clippath_and_mask; + bool keep_paths; // set this to false allow retain extra generated objects, see measure line LPE + bool is_load; + bool refresh_widgets; + BoolParam is_visible; + HiddenParam lpeversion; + Geom::PathVector pathvector_before_effect; + Geom::PathVector pathvector_after_effect; + SPLPEItem *sp_lpe_item; // these get stored in doBeforeEffect_impl, and derived classes may do as they please with + // them. + SPShape *current_shape; // these get stored in performPathEffects. + protected: + Effect(LivePathEffectObject *lpeobject); + + // provide a set of doEffect functions so the developer has a choice + // of what kind of input/output parameters he desires. + // the order in which they appear is the order in which they are + // called by this base class. (i.e. doEffect(SPCurve * curve) defaults to calling + // doEffect(Geom::PathVector ) + virtual Geom::PathVector + doEffect_path (Geom::PathVector const & path_in); + virtual Geom::Piecewise > + doEffect_pwd2 (Geom::Piecewise > const & pwd2_in); + + void registerParameter(Parameter * param); + Parameter * getNextOncanvasEditableParam(); + + virtual void addKnotHolderEntities(KnotHolder * /*knotholder*/, SPItem * /*item*/) {}; + + virtual void addCanvasIndicators(SPLPEItem const* lpeitem, std::vector &hp_vec); + + std::vector param_vector; + bool _provides_knotholder_entities; + + int oncanvasedit_it; + bool is_applied; + bool show_orig_path; // set this to true in derived effects to automatically have the original + // path displayed as helperpath + + Inkscape::UI::Widget::Registry wr; + + LivePathEffectObject *lpeobj; + + // this boolean defaults to false, it concatenates the input path to one pwd2, + // instead of normally 'splitting' the path into continuous pwd2 paths and calling doEffect_pwd2 for each. + bool concatenate_before_pwd2; + std::vector items; + double current_zoom; + std::vector selectedNodesPoints; + +private: + void onDefaultsExpanderChanged(Gtk::Expander * expander); + void setDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset); + void unsetDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset); + bool provides_own_flash_paths; // if true, the standard flash path is suppressed + + bool is_ready; + bool defaultsopen; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-angle_bisector.cpp b/src/live_effects/lpe-angle_bisector.cpp new file mode 100644 index 0000000..c1e7bef --- /dev/null +++ b/src/live_effects/lpe-angle_bisector.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-angle_bisector.h" +#include "2geom/sbasis-to-bezier.h" + +#include "knot-holder-entity.h" +#include "knotholder.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +namespace AB { + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPEAngleBisector* effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityRightEnd(LPEAngleBisector* effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace AB + +LPEAngleBisector::LPEAngleBisector(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + length_left(_("Length left:"), _("Specifies the left end of the bisector"), "length-left", &wr, this, 0), + length_right(_("Length right:"), _("Specifies the right end of the bisector"), "length-right", &wr, this, 250) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter( dynamic_cast(&length_left) ); + registerParameter( dynamic_cast(&length_right) ); +} + +LPEAngleBisector::~LPEAngleBisector() += default; + +Geom::PathVector +LPEAngleBisector::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + + // we assume that the path has >= 3 nodes + ptA = path_in[0].pointAt(1); + Point B = path_in[0].initialPoint(); + Point C = path_in[0].pointAt(2); + + double angle = angle_between(B - ptA, C - ptA); + + dir = unit_vector(B - ptA) * Rotate(angle/2); + + Geom::Point D = ptA - dir * length_left; + Geom::Point E = ptA + dir * length_right; + + Piecewise > output = Piecewise >(D2(SBasis(D[X], E[X]), SBasis(D[Y], E[Y]))); + + return path_from_piecewise(output, LPE_CONVERSION_TOLERANCE); +} + +void +LPEAngleBisector::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new AB::KnotHolderEntityLeftEnd(this); +e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"left\" end of the bisector")); +knotholder->add(e); + } + { + KnotHolderEntity *e = new AB::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"right\" end of the bisector")); + knotholder->add(e); + } +}; + +namespace AB { + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPEAngleBisector *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->dir); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPEAngleBisector *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->dir); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPEAngleBisector const* lpe = dynamic_cast(_effect); + return lpe->ptA - lpe->dir * lpe->length_left; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPEAngleBisector const* lpe = dynamic_cast(_effect); + return lpe->ptA + lpe->dir * lpe->length_right; +} + +} // namespace AB + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-angle_bisector.h b/src/live_effects/lpe-angle_bisector.h new file mode 100644 index 0000000..b780fce --- /dev/null +++ b/src/live_effects/lpe-angle_bisector.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ANGLE_BISECTOR_H +#define INKSCAPE_LPE_ANGLE_BISECTOR_H + +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace AB { + // we use a separate namespace to avoid clashes with other LPEs + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; +} + +class LPEAngleBisector : public Effect { +public: + LPEAngleBisector(LivePathEffectObject *lpeobject); + ~LPEAngleBisector() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + friend class AB::KnotHolderEntityLeftEnd; + friend class AB::KnotHolderEntityRightEnd; + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +//private: + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point ptA; + Geom::Point dir; + + LPEAngleBisector(const LPEAngleBisector&); + LPEAngleBisector& operator=(const LPEAngleBisector&); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-attach-path.cpp b/src/live_effects/lpe-attach-path.cpp new file mode 100644 index 0000000..e266b0a --- /dev/null +++ b/src/live_effects/lpe-attach-path.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-attach-path.h" +#include "display/curve.h" +#include "2geom/path-sink.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEAttachPath::LPEAttachPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + start_path(_("Start path:"), _("Path to attach to the start of this path"), "startpath", &wr, this), + start_path_position(_("Start path position:"), _("Position to attach path start to"), "startposition", &wr, this, 0.0), + start_path_curve_start(_("Start path curve start:"), _("Starting curve"), "startcurvestart", &wr, this, Geom::Point(20,0)/*, true*/), + start_path_curve_end(_("Start path curve end:"), _("Ending curve"), "startcurveend", &wr, this, Geom::Point(20,0)/*, true*/), + end_path(_("End path:"), _("Path to attach to the end of this path"), "endpath", &wr, this), + end_path_position(_("End path position:"), _("Position to attach path end to"), "endposition", &wr, this, 0.0), + end_path_curve_start(_("End path curve start:"), _("Starting curve"), "endcurvestart", &wr, this, Geom::Point(20,0)/*, true*/), + end_path_curve_end(_("End path curve end:"), _("Ending curve"), "endcurveend", &wr, this, Geom::Point(20,0)/*, true*/) +{ + registerParameter(&start_path); + registerParameter(&start_path_position); + registerParameter(&start_path_curve_start); + registerParameter(&start_path_curve_end); + + registerParameter(&end_path); + registerParameter(&end_path_position); + registerParameter(&end_path_curve_start); + registerParameter(&end_path_curve_end); + + //perceived_path = true; + show_orig_path = true; + curve_start_previous_origin = start_path_curve_end.getOrigin(); + curve_end_previous_origin = end_path_curve_end.getOrigin(); +} + +LPEAttachPath::~LPEAttachPath() += default; + +void LPEAttachPath::resetDefaults(SPItem const * /*item*/) +{ + curve_start_previous_origin = start_path_curve_end.getOrigin(); + curve_end_previous_origin = end_path_curve_end.getOrigin(); +} + +void LPEAttachPath::doEffect (SPCurve * curve) +{ + Geom::PathVector this_pathv = curve->get_pathvector(); + if (sp_lpe_item && !this_pathv.empty()) { + Geom::Path p = Geom::Path(this_pathv.front().initialPoint()); + + bool set_start_end = start_path_curve_end.getOrigin() != curve_start_previous_origin; + bool set_end_end = end_path_curve_end.getOrigin() != curve_end_previous_origin; + + if (start_path.linksToPath()) { + + Geom::PathVector linked_pathv = start_path.get_pathvector(); + Geom::Affine linkedtransform = start_path.getObject()->getRelativeTransform(sp_lpe_item); + + if ( !linked_pathv.empty() ) + { + Geom::Path transformedpath = linked_pathv.front() * linkedtransform; + start_path_curve_start.setOrigin(this_pathv.front().initialPoint()); + + std::vector derivs = this_pathv.front().front().pointAndDerivatives(0, 3); + + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Geom::Coord length = derivs[deriv_n].length(); + if ( ! Geom::are_near(length, 0) ) { + if (set_start_end) { + start_path_position.param_set_value(transformedpath.nearestTime(start_path_curve_end.getOrigin()).asFlatTime()); + } + + if (start_path_position > transformedpath.size()) { + start_path_position.param_set_value(transformedpath.size()); + } else if (start_path_position < 0) { + start_path_position.param_set_value(0); + } + Geom::Curve const *c = start_path_position >= transformedpath.size() ? + &transformedpath.back() : &transformedpath.at((int)start_path_position); + + std::vector derivs_2 = c->pointAndDerivatives(start_path_position >= transformedpath.size() ? 1 : (start_path_position - (int)start_path_position), 3); + for (unsigned deriv_n_2 = 1; deriv_n_2 < derivs_2.size(); deriv_n_2++) { + Geom::Coord length_2 = derivs[deriv_n_2].length(); + if ( ! Geom::are_near(length_2, 0) ) { + start_path_curve_end.setOrigin(derivs_2[0]); + curve_start_previous_origin = start_path_curve_end.getOrigin(); + + double startangle = atan2(start_path_curve_start.getVector().y(), start_path_curve_start.getVector().x()); + double endangle = atan2(start_path_curve_end.getVector().y(), start_path_curve_end.getVector().x()); + double startderiv = atan2(derivs[deriv_n].y(), derivs[deriv_n].x()); + double endderiv = atan2(derivs_2[deriv_n_2].y(), derivs_2[deriv_n_2].x()); + Geom::Point pt1 = Geom::Point(start_path_curve_start.getVector().length() * cos(startangle + startderiv), start_path_curve_start.getVector().length() * sin(startangle + startderiv)); + Geom::Point pt2 = Geom::Point(start_path_curve_end.getVector().length() * cos(endangle + endderiv), start_path_curve_end.getVector().length() * sin(endangle + endderiv)); + p = Geom::Path(derivs_2[0]); + p.appendNew(-pt2 + derivs_2[0], -pt1 + this_pathv.front().initialPoint(), this_pathv.front().initialPoint()); + break; + + } + } + break; + } + } + } + } + + p.append(this_pathv.front()); + + if (end_path.linksToPath()) { + + Geom::PathVector linked_pathv = end_path.get_pathvector(); + Geom::Affine linkedtransform = end_path.getObject()->getRelativeTransform(sp_lpe_item); + + if ( !linked_pathv.empty() ) + { + Geom::Path transformedpath = linked_pathv.front() * linkedtransform; + Geom::Curve * last_seg_reverse = this_pathv.front().back().reverse(); + + end_path_curve_start.setOrigin(last_seg_reverse->initialPoint()); + + std::vector derivs = last_seg_reverse->pointAndDerivatives(0, 3); + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Geom::Coord length = derivs[deriv_n].length(); + if ( ! Geom::are_near(length, 0) ) { + if (set_end_end) { + end_path_position.param_set_value(transformedpath.nearestTime(end_path_curve_end.getOrigin()).asFlatTime()); + } + + if (end_path_position > transformedpath.size()) { + end_path_position.param_set_value(transformedpath.size()); + } else if (end_path_position < 0) { + end_path_position.param_set_value(0); + } + const Geom::Curve *c = end_path_position >= transformedpath.size() ? + &transformedpath.back() : &transformedpath.at((int)end_path_position); + + std::vector derivs_2 = c->pointAndDerivatives(end_path_position >= transformedpath.size() ? 1 : (end_path_position - (int)end_path_position), 3); + for (unsigned deriv_n_2 = 1; deriv_n_2 < derivs_2.size(); deriv_n_2++) { + Geom::Coord length_2 = derivs[deriv_n_2].length(); + if ( ! Geom::are_near(length_2, 0) ) { + + end_path_curve_end.setOrigin(derivs_2[0]); + curve_end_previous_origin = end_path_curve_end.getOrigin(); + + double startangle = atan2(end_path_curve_start.getVector().y(), end_path_curve_start.getVector().x()); + double endangle = atan2(end_path_curve_end.getVector().y(), end_path_curve_end.getVector().x()); + double startderiv = atan2(derivs[deriv_n].y(), derivs[deriv_n].x()); + double endderiv = atan2(derivs_2[deriv_n_2].y(), derivs_2[deriv_n_2].x()); + Geom::Point pt1 = Geom::Point(end_path_curve_start.getVector().length() * cos(startangle + startderiv), end_path_curve_start.getVector().length() * sin(startangle + startderiv)); + Geom::Point pt2 = Geom::Point(end_path_curve_end.getVector().length() * cos(endangle + endderiv), end_path_curve_end.getVector().length() * sin(endangle + endderiv)); + p.appendNew(-pt1 + this_pathv.front().finalPoint(), -pt2 + derivs_2[0], derivs_2[0]); + + break; + + } + } + break; + } + } + delete last_seg_reverse; + } + } + Geom::PathVector outvector; + outvector.push_back(p); + curve->set_pathvector(outvector); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-attach-path.h b/src/live_effects/lpe-attach-path.h new file mode 100644 index 0000000..fc8d95c --- /dev/null +++ b/src/live_effects/lpe-attach-path.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ATTACH_PATH_H +#define INKSCAPE_LPE_ATTACH_PATH_H + +/* + * Inkscape::LPEAttachPath + * + * Copyright (C) Ted Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/originalpath.h" +#include "live_effects/parameter/vector.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/transformedpoint.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEAttachPath : public Effect { +public: + LPEAttachPath(LivePathEffectObject *lpeobject); + ~LPEAttachPath() override; + + void doEffect (SPCurve * curve) override; + void resetDefaults(SPItem const * item) override; + +private: + LPEAttachPath(const LPEAttachPath&) = delete; + LPEAttachPath& operator=(const LPEAttachPath&) = delete; + + Geom::Point curve_start_previous_origin; + Geom::Point curve_end_previous_origin; + + OriginalPathParam start_path; + ScalarParam start_path_position; + TransformedPointParam start_path_curve_start; + VectorParam start_path_curve_end; + + OriginalPathParam end_path; + ScalarParam end_path_position; + TransformedPointParam end_path_curve_start; + VectorParam end_path_curve_end; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bendpath.cpp b/src/live_effects/lpe-bendpath.cpp new file mode 100644 index 0000000..8b9aab8 --- /dev/null +++ b/src/live_effects/lpe-bendpath.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Steren Giannini 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-bendpath.h" +#include "knot-holder-entity.h" +#include "knotholder.h" +#include "display/curve.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +/* Theory in e-mail from J.F. Barraud +Let B be the skeleton path, and P the pattern (the path to be deformed). + +P is a map t --> P(t) = ( x(t), y(t) ). +B is a map t --> B(t) = ( a(t), b(t) ). + +The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to +U(s) is s itself. + +We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s). + +The basic deformation associated to B is then given by: + + (x,y) --> U(x)+y*N(x) + +(i.e. we go for distance x along the path, and then for distance y along the normal) + +Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed +first) but I think we can first forget about them. +*/ + +namespace Inkscape { +namespace LivePathEffect { + +namespace BeP { +class KnotHolderEntityWidthBendPath : public LPEKnotHolderEntity { + public: + KnotHolderEntityWidthBendPath(LPEBendPath * effect) : LPEKnotHolderEntity(effect) {} + ~KnotHolderEntityWidthBendPath() override + { + LPEBendPath *lpe = dynamic_cast (_effect); + lpe->_knot_entity = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; +} // BeP + +LPEBendPath::LPEBendPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + bend_path(_("Bend path:"), _("Path along which to bend the original path"), "bendpath", &wr, this, "M0,0 L1,0"), + original_height(0.0), + prop_scale(_("_Width:"), _("Width of the path"), "prop_scale", &wr, this, 1.0), + scale_y_rel(_("W_idth in units of length"), _("Scale the width of the path in units of its length"), "scale_y_rel", &wr, this, false), + vertical_pattern(_("_Original path is vertical"), _("Rotates the original 90 degrees, before bending it along the bend path"), "vertical", &wr, this, false), + hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false) +{ + registerParameter( &bend_path ); + registerParameter( &prop_scale); + registerParameter( &scale_y_rel); + registerParameter( &vertical_pattern); + registerParameter(&hide_knot); + + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + + _knot_entity = nullptr; + _provides_knotholder_entities = true; + apply_to_clippath_and_mask = true; + concatenate_before_pwd2 = true; +} + +LPEBendPath::~LPEBendPath() += default; + +void +LPEBendPath::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the item bounding box + original_bbox(lpeitem, false, true); + original_height = boundingbox_Y.max() - boundingbox_Y.min(); + if (_knot_entity) { + if (hide_knot) { + helper_path.clear(); + _knot_entity->knot->hide(); + } else { + _knot_entity->knot->show(); + } + _knot_entity->update_knot(); + } +} + +void LPEBendPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + bend_path.param_transform_multiply(postmul, false); +} + +Geom::Piecewise > +LPEBendPath::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + +/* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + + if (bend_path.changed) { + uskeleton = arc_length_parametrization(Piecewise >(bend_path.get_pwd2()),2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.01)); + + bend_path.changed = false; + } + + if (uskeleton.empty()) { + return pwd2_in; /// \todo or throw an exception instead? might be better to throw an exception so that the UI can display an error message or smth + } + + D2 > patternd2 = make_cuts_independent(pwd2_in); + Piecewise x = vertical_pattern.get_value() ? Piecewise(patternd2[1]) : Piecewise(patternd2[0]); + Piecewise y = vertical_pattern.get_value() ? Piecewise(patternd2[0]) : Piecewise(patternd2[1]); + + Interval bboxHorizontal = vertical_pattern.get_value() ? boundingbox_Y : boundingbox_X; + Interval bboxVertical = vertical_pattern.get_value() ? boundingbox_X : boundingbox_Y; + + //+0.1 in x fix bug #1658855 + //We use the group bounding box size or the path bbox size to translate well x and y + x-= bboxHorizontal.min() + 0.1; + y-= bboxVertical.middle(); + + double scaling = uskeleton.cuts.back()/bboxHorizontal.extent(); + + if (scaling != 1.0) { + x*=scaling; + } + + if ( scale_y_rel.get_value() ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + + Piecewise > output = compose(uskeleton,x) + y*compose(n,x); + return output; +} + +void +LPEBendPath::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + + Geom::Point start(boundingbox_X.min(), (boundingbox_Y.max()+boundingbox_Y.min())/2); + Geom::Point end(boundingbox_X.max(), (boundingbox_Y.max()+boundingbox_Y.min())/2); + + if ( Geom::are_near(start,end) ) { + end += Geom::Point(1.,0.); + } + + Geom::Path path; + path.start( start ); + path.appendNew( end ); + bend_path.set_new_value( path.toPwSb(), true ); +} + +void +LPEBendPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(helper_path); +} + +void +LPEBendPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new BeP::KnotHolderEntityWidthBendPath(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Change the width"), + SP_KNOT_SHAPE_CIRCLE); + knotholder->add(_knot_entity); + if (hide_knot) { + _knot_entity->knot->hide(); + _knot_entity->update_knot(); + } +} + +namespace BeP { + +void +KnotHolderEntityWidthBendPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + LPEBendPath *lpe = dynamic_cast (_effect); + + Geom::Point const s = snap_knot_position(p, state); + Geom::Path path_in = lpe->bend_path.get_pathvector().pathAt(Geom::PathVectorTime(0, 0, 0.0)); + Geom::Point ptA = path_in.pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in.pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in.curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse(); + Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos); + if(nearest_to_ray == 0){ + lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } else { + lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/bend/width", lpe->prop_scale); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityWidthBendPath::knot_get() const +{ + LPEBendPath *lpe = dynamic_cast (_effect); + Geom::Path path_in = lpe->bend_path.get_pathvector().pathAt(Geom::PathVectorTime(0, 0, 0.0)); + Geom::Point ptA = path_in.pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in.pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in.curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA; + lpe->helper_path.clear(); + if (!lpe->hide_knot) { + Geom::Path hp(result_point); + hp.appendNew(ptA); + lpe->helper_path.push_back(hp); + hp.clear(); + } + return result_point; +} +} // namespace BeP +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-bendpath.h b/src/live_effects/lpe-bendpath.h new file mode 100644 index 0000000..b34f288 --- /dev/null +++ b/src/live_effects/lpe-bendpath.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BENDPATH_H +#define INKSCAPE_LPE_BENDPATH_H + +/* + * Inkscape::LPEPathAlongPath + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Steren Giannini 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/d2.h> +#include <2geom/piecewise.h> + +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace BeP { +class KnotHolderEntityWidthBendPath; +} + +//for Bend path on group : we need information concerning the group Bounding box +class LPEBendPath : public Effect, GroupBBoxEffect { +public: + LPEBendPath(LivePathEffectObject *lpeobject); + ~LPEBendPath() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; + + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + PathParam bend_path; + + friend class BeP::KnotHolderEntityWidthBendPath; +protected: + double original_height; + ScalarParam prop_scale; +private: + BoolParam scale_y_rel; + BoolParam vertical_pattern; + BoolParam hide_knot; + KnotHolderEntity * _knot_entity; + Geom::PathVector helper_path; + Geom::Piecewise > uskeleton; + Geom::Piecewise > n; + + void on_pattern_pasted(); + + LPEBendPath(const LPEBendPath&); + LPEBendPath& operator=(const LPEBendPath&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bool.cpp b/src/live_effects/lpe-bool.cpp new file mode 100644 index 0000000..440a408 --- /dev/null +++ b/src/live_effects/lpe-bool.cpp @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Boolean operation live path effect + * + * Copyright (C) 2016-2017 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include + +#include "live_effects/lpe-bool.h" + +#include "2geom/affine.h" +#include "2geom/bezier-curve.h" +#include "2geom/path-sink.h" +#include "2geom/path.h" +#include "2geom/svg-path-parser.h" +#include "display/curve.h" +#include "object/sp-shape.h" +#include "seltrans.h" +#include "svg/svg.h" +#include "ui/tools/select-tool.h" + +#include "helper/geom.h" + +#include "splivarot.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" +#include "livarot/path-description.h" + +namespace Inkscape { +namespace LivePathEffect { + +// Define an extended boolean operation type + +static const Util::EnumData BoolOpData[LPEBool::bool_op_ex_count] = { + { LPEBool::bool_op_ex_union, N_("union"), "union" }, + { LPEBool::bool_op_ex_inters, N_("intersection"), "inters" }, + { LPEBool::bool_op_ex_diff, N_("difference"), "diff" }, + { LPEBool::bool_op_ex_symdiff, N_("symmetric difference"), "symdiff" }, + { LPEBool::bool_op_ex_cut, N_("division"), "cut" }, + // Note on naming of operations: + // bool_op_cut is called "Division" in the manu, see sp_selected_path_cut + // bool_op_slice is called "Cut path" in the menu, see sp_selected_path_slice + { LPEBool::bool_op_ex_slice, N_("cut"), "slice" }, + { LPEBool::bool_op_ex_slice_inside, N_("cut inside"), "slice-inside" }, + { LPEBool::bool_op_ex_slice_outside, N_("cut outside"), "slice-outside" }, +}; + +static const Util::EnumDataConverter BoolOpConverter(BoolOpData, sizeof(BoolOpData) / sizeof(*BoolOpData)); + +static const Util::EnumData FillTypeData[] = { + { fill_oddEven, N_("even-odd"), "oddeven" }, + { fill_nonZero, N_("non-zero"), "nonzero" }, + { fill_positive, N_("positive"), "positive" }, + { fill_justDont, N_("take from object"), "from-curve" } +}; + +static const Util::EnumDataConverter FillTypeConverter(FillTypeData, sizeof(FillTypeData) / sizeof(*FillTypeData)); + +static const Util::EnumData FillTypeDataThis[] = { + { fill_oddEven, N_("even-odd"), "oddeven" }, + { fill_nonZero, N_("non-zero"), "nonzero" }, + { fill_positive, N_("positive"), "positive" } +}; + +static const Util::EnumDataConverter FillTypeConverterThis(FillTypeDataThis, sizeof(FillTypeDataThis) / sizeof(*FillTypeDataThis)); + +LPEBool::LPEBool(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , operand_path(_("Operand path:"), _("Operand for the boolean operation"), "operand-path", &wr, this) + , bool_operation(_("Operation:"), _("Boolean Operation"), "operation", BoolOpConverter, &wr, this, bool_op_ex_union) + , swap_operands(_("Swap operands"), _("Swap operands (useful e.g. for difference)"), "swap-operands", &wr, this) + , hide_linked(_("Hide Linked"), _("Hide linked path"), "hide-linked", &wr, this, true) + , rmv_inner( + _("Remove inner"), + _("For cut operations: remove inner (non-contour) lines of cutting path to avoid invisible extra points"), + "rmv-inner", &wr, this) + , fill_type_this(_("Fill type this:"), _("Fill type (winding mode) for this path"), "filltype-this", + FillTypeConverterThis, &wr, this, fill_oddEven) + , fill_type_operand(_("Fill type operand:"), _("Fill type (winding mode) for operand path"), "filltype-operand", + FillTypeConverter, &wr, this, fill_justDont) +{ + registerParameter(&operand_path); + registerParameter(&bool_operation); + registerParameter(&swap_operands); + registerParameter(&hide_linked); + registerParameter(&rmv_inner); + registerParameter(&fill_type_this); + registerParameter(&fill_type_operand); + show_orig_path = true; + operand = dynamic_cast(operand_path.getObject()); + contdown = 0; +} + +LPEBool::~LPEBool() += default; + +void LPEBool::resetDefaults(SPItem const * /*item*/) +{ +} + +bool cmp_cut_position(const Path::cut_position &a, const Path::cut_position &b) +{ + return a.piece == b.piece ? a.t < b.t : a.piece < b.piece; +} + +Geom::PathVector +sp_pathvector_boolop_slice_intersect(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool inside, fill_typ fra, fill_typ frb) +{ + // This is similar to sp_pathvector_boolop/bool_op_slice, but keeps only edges inside the cutter area. + // The code is also based on sp_pathvector_boolop_slice. + // + // We have two paths on input + // - a closed area which is used to cut out pieces from a contour (called area below) + // - a contour which is cut into pieces by the border of thr area (called contour below) + // + // The code below works in the following steps + // (a) Convert the area to a shape, so that we can ask the winding number for any point + // (b) Add both, the contour and the area to a single shape and intersect them + // (c) Find the intersection points between area border and contour (vector toCut) + // (d) Split the original contour at the intersection points + // (e) check for each contour edge in combined shape if its center is inside the area - if not discard it + // (f) create a vector of all inside edges + // (g) convert the piece numbers to the piece numbers after applying the cuts + // (h) fill a bool vector with information which pieces are in + // (i) filter the descr_cmd of the result path with this bool vector + // + // The main inefficiency here is step (e) because I use a winding function of the area-shape which goes + // through the complete edge list for each point I ask for, so effort is n-edges-contour * n-edges-area. + // It is tricky to improve this without building into the livarot code. + // One way might be to decide at the intersection points which edges touching the intersection points are + // in by making a loop through all edges on the intersection vertex. Since this is a directed non intersecting + // graph, this should provide sufficient information. + // But since I anyway will change this to the new mechanism some time speed is fairly ok, I didn't look into this. + + + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + Path *contour_path = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathva)); + Path *area_path = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathvb)); + + // Shapes from above paths + Shape *area_shape = new Shape; + Shape *combined_shape = new Shape; + Shape *combined_inters = new Shape; + + // Add the area (process to intersection free shape) + area_path->ConvertWithBackData(1.0); + area_path->Fill(combined_shape, 1); + + // Convert this to a shape with full winding information + area_shape->ConvertToShape(combined_shape, frb); + + // Add the contour to the combined path (just add, no winding processing) + contour_path->ConvertWithBackData(1.0); + contour_path->Fill(combined_shape, 0, true, false, false); + + // Intersect the area and the contour - no fill processing + combined_inters->ConvertToShape(combined_shape, fill_justDont); + + // Result path + Path *result_path = new Path; + result_path->SetBackData(false); + + // Cutting positions for contour + std::vector toCut; + + if (combined_inters->hasBackData()) { + // should always be the case, but ya never know + { + for (int i = 0; i < combined_inters->numberOfPoints(); i++) { + if (combined_inters->getPoint(i).totalDegree() > 2) { + // possibly an intersection + // we need to check that at least one edge from the source path is incident to it + // before we declare it's an intersection + int cb = combined_inters->getPoint(i).incidentEdge[FIRST]; + int nbOrig = 0; + int nbOther = 0; + int piece = -1; + float t = 0.0; + while (cb >= 0 && cb < combined_inters->numberOfEdges()) { + if (combined_inters->ebData[cb].pathID == 0) { + // the source has an edge incident to the point, get its position on the path + piece = combined_inters->ebData[cb].pieceID; + if (combined_inters->getEdge(cb).st == i) { + t = combined_inters->ebData[cb].tSt; + } else { + t = combined_inters->ebData[cb].tEn; + } + nbOrig++; + } + if (combined_inters->ebData[cb].pathID == 1) { + nbOther++; // the cut is incident to this point + } + cb = combined_inters->NextAt(i, cb); + } + if (nbOrig > 0 && nbOther > 0) { + // point incident to both path and cut: an intersection + // note that you only keep one position on the source; you could have degenerate + // cases where the source crosses itself at this point, and you wouyld miss an intersection + Path::cut_position cutpos; + cutpos.piece = piece; + cutpos.t = t; + toCut.push_back(cutpos); + } + } + } + } + { + // remove the edges from the intersection polygon + int i = combined_inters->numberOfEdges() - 1; + for (; i >= 0; i--) { + if (combined_inters->ebData[i].pathID == 1) { + combined_inters->SubEdge(i); + } else { + const Shape::dg_arete &edge = combined_inters->getEdge(i); + const Shape::dg_point &start = combined_inters->getPoint(edge.st); + const Shape::dg_point &end = combined_inters->getPoint(edge.en); + Geom::Point mid = 0.5 * (start.x + end.x); + int wind = area_shape->PtWinding(mid); + if (wind == 0) { + combined_inters->SubEdge(i); + } + } + } + } + } + + // create a vector of pieces, which are in the intersection + std::vector inside_pieces(combined_inters->numberOfEdges()); + for (int i = 0; i < combined_inters->numberOfEdges(); i++) { + inside_pieces[i].piece = combined_inters->ebData[i].pieceID; + // Use the t middle point, this is safe to compare with values from toCut in the presence of roundoff errors + inside_pieces[i].t = 0.5 * (combined_inters->ebData[i].tSt + combined_inters->ebData[i].tEn); + } + std::sort(inside_pieces.begin(), inside_pieces.end(), cmp_cut_position); + + // sort cut positions + std::sort(toCut.begin(), toCut.end(), cmp_cut_position); + + // Compute piece ids after ConvertPositionsToMoveTo + { + int idIncr = 0; + std::vector::iterator itPiece = inside_pieces.begin(); + std::vector::iterator itCut = toCut.begin(); + while (itPiece != inside_pieces.end()) { + while (itCut != toCut.end() && cmp_cut_position(*itCut, *itPiece)) { + ++itCut; + idIncr += 2; + } + itPiece->piece += idIncr; + ++itPiece; + } + } + + // Copy the original path to result and cut at the intersection points + result_path->Copy(contour_path); + result_path->ConvertPositionsToMoveTo(toCut.size(), toCut.data()); // cut where you found intersections + + // Create an array of bools which states which pieces are in + std::vector inside_flags(result_path->descr_cmd.size(), false); + for (auto & inside_piece : inside_pieces) { + inside_flags[ inside_piece.piece ] = true; + // also enable the element -1 to get the MoveTo + if (inside_piece.piece >= 1) { + inside_flags[ inside_piece.piece - 1 ] = true; + } + } + +#if 0 // CONCEPT TESTING + //Check if the inside/outside verdict is consistent - just for testing the concept + // Retrieve the pieces + int nParts = 0; + Path **parts = result_path->SubPaths(nParts, false); + + // Each piece should be either fully in or fully out + int iPiece = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + bool andsum = true; + bool orsum = false; + for (int iCmd = 0; iCmd < parts[iPart]->descr_cmd.size(); iCmd++, iPiece++) { + andsum = andsum && inside_flags[ iPiece ]; + orsum = andsum || inside_flags[ iPiece ]; + } + + if (andsum != orsum) { + g_warning("Inconsistent inside/outside verdict for part=%d", iPart); + } + } + g_free(parts); +#endif + + // iterate over the commands of a path and keep those which are inside + int iDest = 0; + for (int iSrc = 0; iSrc < result_path->descr_cmd.size(); iSrc++) { + if (inside_flags[iSrc] == inside) { + result_path->descr_cmd[iDest++] = result_path->descr_cmd[iSrc]; + } else { + delete result_path->descr_cmd[iSrc]; + } + } + result_path->descr_cmd.resize(iDest); + + delete combined_inters; + delete combined_shape; + delete area_shape; + delete contour_path; + delete area_path; + + gchar *result_str = result_path->svg_dump_path(); + Geom::PathVector outres = Geom::parse_svg_path(result_str); + // CONCEPT TESTING g_warning( "%s", result_str ); + g_free(result_str); + delete result_path; + + return outres; +} + +// remove inner contours +Geom::PathVector +sp_pathvector_boolop_remove_inner(Geom::PathVector const &pathva, fill_typ fra) +{ + Geom::PathVector patht; + Path *patha = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathva)); + + Shape *shape = new Shape; + Shape *shapeshape = new Shape; + Path *resultp = new Path; + resultp->SetBackData(false); + + patha->ConvertWithBackData(0.1); + patha->Fill(shape, 0); + shapeshape->ConvertToShape(shape, fra); + shapeshape->ConvertToForme(resultp, 1, &patha); + + delete shape; + delete shapeshape; + delete patha; + + gchar *result_str = resultp->svg_dump_path(); + Geom::PathVector resultpv = Geom::parse_svg_path(result_str); + g_free(result_str); + + delete resultp; + return resultpv; +} + +static fill_typ GetFillTyp(SPItem *item) +{ + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) { + return fill_nonZero; + } else if (val && strcmp(val, "evenodd") == 0) { + return fill_oddEven; + } else { + return fill_nonZero; + } +} + +void LPEBool::doBeforeEffect(SPLPEItem const *lpeitem) +{ + // operand->set_transform(i2anc_affine(sp_lpe_item, sp_lpe_item->parent)); + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPItem *current_operand = dynamic_cast(operand_path.getObject()); + if (!current_operand) { + if (operand) { + operand->setHidden(false); + } + operand = nullptr; + } + if (current_operand && operand != current_operand) { + if (operand) { + operand->setHidden(false); + } + operand = current_operand; + } + if (operand && operand->parent && sp_lpe_item && sp_lpe_item->parent != operand->parent) { + // TODO: reposition new operand on doOnRemove if keep_paths is false + Inkscape::XML::Node *copy = operand->getRepr()->duplicate(xml_doc); + SPItem *relocated_operand = dynamic_cast(sp_lpe_item->parent->appendChildRepr(copy)); + Inkscape::GC::release(copy); + operand->deleteObject(); + operand = relocated_operand; + Glib::ustring itemid = operand->getId(); + operand_path.linkitem(itemid); + } + // TODO: make 2 methods to globaly inform to a LPE item when is grabbed + // and when the transform is applyed both callers can be in Inkscape::SelTrans in grab and ungrab functions + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && operand) { + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::UI::Tools::SelectTool *selectool = + dynamic_cast(desktop->event_context); + gint cdown = 2; + if (selectool && selectool->_seltrans && selectool->_seltrans->isGrabbed()) { + cdown = 3; + } + if (!is_load && desktop && selection && operand && operand->isHidden() && hide_linked && contdown != 1) { + selection->add(operand); + contdown = cdown; + } + if (contdown == 1 && desktop && selection && operand && operand->isHidden() && hide_linked) { + selection->remove(operand); + } + if (contdown > 0) { + --contdown; + } + if (is_load) { + contdown = 1; + } + if (operand_path.linksToPath() && operand) { + SPItem *itemsel = selection->singleItem(); + if (operand->isHidden() && hide_linked && itemsel && itemsel == operand) { + hide_linked.param_setValue(false); + hide_linked.write_to_SVG(); + } + } + } +} + +void LPEBool::doEffect(SPCurve *curve) +{ + Geom::PathVector path_in = curve->get_pathvector(); + if (operand == SP_ITEM(current_shape)) { + g_warning("operand and current shape are the same"); + operand_path.param_set_default(); + return; + } + + if (operand_path.linksToPath() && operand) { + if (!operand->isHidden() && hide_linked) { + operand->setHidden(true); + } + if (operand->isHidden() && !hide_linked) { + operand->setHidden(false); + } + bool_op_ex op = bool_operation.get_value(); + bool swap = !(swap_operands.get_value()); + + Geom::Affine current_affine = sp_item_transform_repr(sp_lpe_item); + Geom::Affine operand_affine = sp_item_transform_repr(operand); + + Geom::PathVector operand_pv = operand_path.get_pathvector(); + path_in *= current_affine; + operand_pv *= operand_affine; + + Geom::PathVector path_a = swap ? operand_pv : path_in; + Geom::PathVector path_b = swap ? path_in : operand_pv; + + // TODO: I would like to use the original objects fill rule if the UI selected rule is fill_justDont. + // But it doesn't seem possible to access them from here, because SPCurve is not derived from SPItem. + // The nearest function in the call stack, where this is available is SPLPEItem::performPathEffect (this is then an SPItem) + // For the parameter curve, this is possible. + // fill_typ fill_this = fill_type_this. get_value()!=fill_justDont ? fill_type_this.get_value() : GetFillTyp( curve ) ; + fill_typ fill_this = fill_type_this.get_value(); + fill_typ fill_operand = fill_type_operand.get_value() != fill_justDont ? fill_type_operand.get_value() : GetFillTyp(operand_path.getObject()); + + fill_typ fill_a = swap ? fill_operand : fill_this; + fill_typ fill_b = swap ? fill_this : fill_operand; + + if (rmv_inner.get_value()) { + path_b = sp_pathvector_boolop_remove_inner(path_b, fill_b); + } + + Geom::PathVector path_out; + + if (op == bool_op_ex_slice) { + // For slicing, the bool op is added to the line group which is sliced, not the cut path. This swapped order is correct. + path_out = sp_pathvector_boolop(path_b, path_a, to_bool_op(op), fill_b, fill_a); + } else if (op == bool_op_ex_slice_inside) { + path_out = sp_pathvector_boolop_slice_intersect(path_a, path_b, true, fill_a, fill_b); + } else if (op == bool_op_ex_slice_outside) { + path_out = sp_pathvector_boolop_slice_intersect(path_a, path_b, false, fill_a, fill_b); + } else { + path_out = sp_pathvector_boolop(path_a, path_b, to_bool_op(op), fill_a, fill_b); + } + curve->set_pathvector(path_out * current_affine.inverse()); + } +} + +void LPEBool::doOnRemove(SPLPEItem const * /*lpeitem*/) +{ + // set "keep paths" hook on sp-lpe-item.cpp + SPItem *operand = dynamic_cast(operand_path.getObject()); + if (operand_path.linksToPath() && operand) { + if (keep_paths) { + if (operand->isHidden()) { + operand->deleteObject(true); + } + } else { + if (operand->isHidden()) { + operand->setHidden(false); + } + } + } +} + +// TODO: Migrate the tree next function to effect.cpp/h to avoid duplication +void LPEBool::doOnVisibilityToggled(SPLPEItem const * /*lpeitem*/) +{ + SPItem *operand = dynamic_cast(operand_path.getObject()); + if (operand_path.linksToPath() && operand) { + if (!is_visible) { + operand->setHidden(false); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-bool.h b/src/live_effects/lpe-bool.h new file mode 100644 index 0000000..4fd0018 --- /dev/null +++ b/src/live_effects/lpe-bool.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Boolean operation live path effect + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_BOOL_H +#define INKSCAPE_LPE_BOOL_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/originalpath.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "livarot/LivarotDefs.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBool : public Effect { +public: + LPEBool(LivePathEffectObject *lpeobject); + ~LPEBool() override; + + void doEffect(SPCurve *curve) override; + void doBeforeEffect(SPLPEItem const *lpeitem) override; + void resetDefaults(SPItem const *item) override; + void doOnVisibilityToggled(SPLPEItem const * /*lpeitem*/) override; + void doOnRemove(SPLPEItem const * /*lpeitem*/) override; + + enum bool_op_ex { + bool_op_ex_union = bool_op_union, + bool_op_ex_inters = bool_op_inters, + bool_op_ex_diff = bool_op_diff, + bool_op_ex_symdiff = bool_op_symdiff, + bool_op_ex_cut = bool_op_cut, + bool_op_ex_slice = bool_op_slice, + bool_op_ex_slice_inside, // like bool_op_slice, but leaves only the contour pieces inside of the cut path + bool_op_ex_slice_outside, // like bool_op_slice, but leaves only the contour pieces outside of the cut path + bool_op_ex_count + }; + + inline friend bool_op to_bool_op(bool_op_ex val) + { + assert(val <= bool_op_ex_slice); + return (bool_op) val; + } + +private: + LPEBool(const LPEBool &) = delete; + LPEBool &operator=(const LPEBool &) = delete; + + OriginalPathParam operand_path; + EnumParam bool_operation; + EnumParam fill_type_this; + EnumParam fill_type_operand; + BoolParam hide_linked; + BoolParam swap_operands; + BoolParam rmv_inner; + SPItem *operand; + size_t contdown; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bounding-box.cpp b/src/live_effects/lpe-bounding-box.cpp new file mode 100644 index 0000000..106ba24 --- /dev/null +++ b/src/live_effects/lpe-bounding-box.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-bounding-box.h" + +#include "display/curve.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +namespace Inkscape { +namespace LivePathEffect { + +LPEBoundingBox::LPEBoundingBox(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + linked_path(_("Linked path:"), _("Path from which to take the original path data"), "linkedpath", &wr, this), + visual_bounds(_("Visual Bounds"), _("Uses the visual bounding box"), "visualbounds", &wr, this) +{ + registerParameter(&linked_path); + registerParameter(&visual_bounds); + //perceived_path = true; +} + +LPEBoundingBox::~LPEBoundingBox() += default; + +void LPEBoundingBox::doEffect (SPCurve * curve) +{ + if (curve) { + if ( linked_path.linksToPath() && linked_path.getObject() ) { + SPItem * item = linked_path.getObject(); + Geom::OptRect bbox = visual_bounds.get_value() ? item->visualBounds() : item->geometricBounds(); + Geom::Path p(Geom::Point(bbox->left(), bbox->top())); + p.appendNew(Geom::Point(bbox->right(), bbox->top())); + p.appendNew(Geom::Point(bbox->right(), bbox->bottom())); + p.appendNew(Geom::Point(bbox->left(), bbox->bottom())); + p.appendNew(Geom::Point(bbox->left(), bbox->top())); + p.close(); + Geom::PathVector out; + out.push_back(p); + curve->set_pathvector(out); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-bounding-box.h b/src/live_effects/lpe-bounding-box.h new file mode 100644 index 0000000..c5230b4 --- /dev/null +++ b/src/live_effects/lpe-bounding-box.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BOUNDING_BOX_H +#define INKSCAPE_LPE_BOUNDING_BOX_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/originalpath.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBoundingBox : public Effect { +public: + LPEBoundingBox(LivePathEffectObject *lpeobject); + ~LPEBoundingBox() override; + + void doEffect (SPCurve * curve) override; + +private: + OriginalPathParam linked_path; + BoolParam visual_bounds; + +private: + LPEBoundingBox(const LPEBoundingBox&) = delete; + LPEBoundingBox& operator=(const LPEBoundingBox&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bspline.cpp b/src/live_effects/lpe-bspline.cpp new file mode 100644 index 0000000..b6ee330 --- /dev/null +++ b/src/live_effects/lpe-bspline.cpp @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "live_effects/lpe-bspline.h" +#include "ui/widget/scalar.h" +#include "display/curve.h" +#include "helper/geom-curves.h" +#include "object/sp-path.h" +#include "svg/svg.h" +#include "xml/repr.h" +#include "preferences.h" +#include "document-undo.h" +#include "verbs.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +const double HANDLE_CUBIC_GAP = 0.001; +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; +const double DEFAULT_END_POWER = 2.0/3.0; +Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size); + +LPEBSpline::LPEBSpline(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + steps(_("Steps with CTRL:"), _("Change number of steps with CTRL pressed"), "steps", &wr, this, 2), + helper_size(_("Helper size:"), _("Helper size"), "helper_size", &wr, this, 0), + apply_no_weight(_("Apply changes if weight = 0%"), _("Apply changes if weight = 0%"), "apply_no_weight", &wr, this, true), + apply_with_weight(_("Apply changes if weight > 0%"), _("Apply changes if weight > 0%"), "apply_with_weight", &wr, this, true), + only_selected(_("Change only selected nodes"), _("Change only selected nodes"), "only_selected", &wr, this, false), + weight(_("Change weight %:"), _("Change weight percent of the effect"), "weight", &wr, this, DEFAULT_START_POWER * 100) +{ + registerParameter(&weight); + registerParameter(&steps); + registerParameter(&helper_size); + registerParameter(&apply_no_weight); + registerParameter(&apply_with_weight); + registerParameter(&only_selected); + + weight.param_set_range(NO_POWER, 100.0); + weight.param_set_increments(0.1, 0.1); + weight.param_set_digits(4); + weight.param_set_undo(false); + + steps.param_set_range(1, 10); + steps.param_set_increments(1, 1); + steps.param_set_digits(0); + steps.param_set_undo(false); + + helper_size.param_set_range(0.0, 999.0); + helper_size.param_set_increments(1, 1); + helper_size.param_set_digits(2); +} + +LPEBSpline::~LPEBSpline() = default; + +void LPEBSpline::doBeforeEffect (SPLPEItem const* /*lpeitem*/) +{ + if(!hp.empty()) { + hp.clear(); + } +} + + +void LPEBSpline::doOnApply(SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE BSpline can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast(lpeitem); + item->removeCurrentPathEffect(false); + } +} + +void +LPEBSpline::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(hp); +} + +Gtk::Widget *LPEBSpline::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + vbox->set_homogeneous(false); + vbox->set_border_width(5); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast(param->param_newWidget()); + if (param->param_key == "weight") { + Gtk::HBox * buttons = Gtk::manage(new Gtk::HBox(true,0)); + Gtk::Button *default_weight = + Gtk::manage(new Gtk::Button(Glib::ustring(_("Default weight")))); + default_weight->signal_clicked() + .connect(sigc::mem_fun(*this, &LPEBSpline::toDefaultWeight)); + buttons->pack_start(*default_weight, true, true, 2); + Gtk::Button *make_cusp = + Gtk::manage(new Gtk::Button(Glib::ustring(_("Make cusp")))); + make_cusp->signal_clicked() + .connect(sigc::mem_fun(*this, &LPEBSpline::toMakeCusp)); + buttons->pack_start(*make_cusp, true, true, 2); + vbox->pack_start(*buttons, true, true, 2); + } + if (param->param_key == "weight" || param->param_key == "steps") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast(widg)); + widg_registered->signal_value_changed() + .connect(sigc::mem_fun(*this, &LPEBSpline::toWeight)); + widg = dynamic_cast(widg_registered); + if (widg) { + Gtk::HBox * hbox_weight_steps = dynamic_cast(widg); + std::vector< Gtk::Widget* > childList = hbox_weight_steps->get_children(); + Gtk::Entry* entry_widget = dynamic_cast(childList[1]); + entry_widget->set_width_chars(9); + } + } + if (param->param_key == "only_selected" || param->param_key == "apply_no_weight" || param->param_key == "apply_with_weight") { + Gtk::CheckButton *widg_registered = + Gtk::manage(dynamic_cast(widg)); + widg = dynamic_cast(widg_registered); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void LPEBSpline::toDefaultWeight() +{ + changeWeight(DEFAULT_START_POWER * 100); + DocumentUndo::done(getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change to default weight")); +} + +void LPEBSpline::toMakeCusp() +{ + changeWeight(NO_POWER); + DocumentUndo::done(getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change to 0 weight")); +} + +void LPEBSpline::toWeight() +{ + changeWeight(weight); + DocumentUndo::done(getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change scalar parameter")); +} + +void LPEBSpline::changeWeight(double weight_ammount) +{ + SPPath *path = dynamic_cast(sp_lpe_item); + if(path) { + SPCurve *curve = path->getCurveForEdit(); + doBSplineFromWidget(curve, weight_ammount/100.0); + gchar *str = sp_svg_write_path(curve->get_pathvector()); + path->setAttribute("inkscape:original-d", str); + g_free(str); + } +} + +void LPEBSpline::doEffect(SPCurve *curve) +{ + sp_bspline_do_effect(curve, helper_size, hp); +} + +void sp_bspline_do_effect(SPCurve *curve, double helper_size, Geom::PathVector &hp) +{ + if (curve->get_segment_count() < 1) { + return; + } + Geom::PathVector const original_pathv = curve->get_pathvector(); + curve->reset(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for (const auto & path_it : original_pathv) { + if (path_it.empty()) { + continue; + } + if (!prefs->getBool("/tools/nodes/show_outline", true)){ + hp.push_back(path_it); + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + SPCurve *curve_n = new SPCurve(); + Geom::Point previousNode(0, 0); + Geom::Point node(0, 0); + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point next_point_at1(0, 0); + Geom::D2 sbasis_in; + Geom::D2 sbasis_out; + Geom::D2 sbasis_helper; + Geom::CubicBezier const *cubic = nullptr; + curve_n->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + SPCurve *in = new SPCurve(); + in->moveto(curve_it1->initialPoint()); + in->lineto(curve_it1->finalPoint()); + cubic = dynamic_cast(&*curve_it1); + if (cubic) { + sbasis_in = in->first_segment()->toSBasis(); + if(are_near((*cubic)[1],(*cubic)[0]) && !are_near((*cubic)[2],(*cubic)[3])) { + point_at1 = sbasis_in.valueAt(DEFAULT_START_POWER); + } else { + point_at1 = sbasis_in.valueAt(Geom::nearest_time((*cubic)[1], *in->first_segment())); + } + if(are_near((*cubic)[2],(*cubic)[3]) && !are_near((*cubic)[1],(*cubic)[0])) { + point_at2 = sbasis_in.valueAt(DEFAULT_END_POWER); + } else { + point_at2 = sbasis_in.valueAt(Geom::nearest_time((*cubic)[2], *in->first_segment())); + } + } else { + point_at1 = in->first_segment()->initialPoint(); + point_at2 = in->first_segment()->finalPoint(); + } + in->reset(); + delete in; + if ( curve_it2 != curve_endit ) { + SPCurve *out = new SPCurve(); + out->moveto(curve_it2->initialPoint()); + out->lineto(curve_it2->finalPoint()); + cubic = dynamic_cast(&*curve_it2); + if (cubic) { + sbasis_out = out->first_segment()->toSBasis(); + if(are_near((*cubic)[1],(*cubic)[0]) && !are_near((*cubic)[2],(*cubic)[3])) { + next_point_at1 = sbasis_in.valueAt(DEFAULT_START_POWER); + } else { + next_point_at1 = sbasis_out.valueAt(Geom::nearest_time((*cubic)[1], *out->first_segment())); + } + } else { + next_point_at1 = out->first_segment()->initialPoint(); + } + out->reset(); + delete out; + } + if (path_it.closed() && curve_it2 == curve_endit) { + SPCurve *start = new SPCurve(); + start->moveto(path_it.begin()->initialPoint()); + start->lineto(path_it.begin()->finalPoint()); + Geom::D2 sbasis_start = start->first_segment()->toSBasis(); + SPCurve *line_helper = new SPCurve(); + cubic = dynamic_cast(&*path_it.begin()); + if (cubic) { + line_helper->moveto(sbasis_start.valueAt( + Geom::nearest_time((*cubic)[1], *start->first_segment()))); + } else { + line_helper->moveto(start->first_segment()->initialPoint()); + } + start->reset(); + delete start; + + SPCurve *end = new SPCurve(); + end->moveto(curve_it1->initialPoint()); + end->lineto(curve_it1->finalPoint()); + Geom::D2 sbasis_end = end->first_segment()->toSBasis(); + cubic = dynamic_cast(&*curve_it1); + if (cubic) { + line_helper->lineto(sbasis_end.valueAt( + Geom::nearest_time((*cubic)[2], *end->first_segment()))); + } else { + line_helper->lineto(end->first_segment()->finalPoint()); + } + end->reset(); + delete end; + sbasis_helper = line_helper->first_segment()->toSBasis(); + line_helper->reset(); + delete line_helper; + node = sbasis_helper.valueAt(0.5); + curve_n->curveto(point_at1, point_at2, node); + curve_n->move_endpoints(node, node); + } else if ( curve_it2 == curve_endit) { + curve_n->curveto(point_at1, point_at2, curve_it1->finalPoint()); + curve_n->move_endpoints(path_it.begin()->initialPoint(), curve_it1->finalPoint()); + } else { + SPCurve *line_helper = new SPCurve(); + line_helper->moveto(point_at2); + line_helper->lineto(next_point_at1); + sbasis_helper = line_helper->first_segment()->toSBasis(); + line_helper->reset(); + delete line_helper; + previousNode = node; + node = sbasis_helper.valueAt(0.5); + Geom::CubicBezier const *cubic2 = dynamic_cast(&*curve_it1); + if((cubic && are_near((*cubic)[0],(*cubic)[1])) || (cubic2 && are_near((*cubic2)[2],(*cubic2)[3]))) { + node = curve_it1->finalPoint(); + } + curve_n->curveto(point_at1, point_at2, node); + } + if(!are_near(node,curve_it1->finalPoint()) && helper_size > 0.0) { + hp.push_back(sp_bspline_drawHandle(node, helper_size)); + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + curve_n->closepath_current(); + } + curve->append(curve_n, false); + curve_n->reset(); + delete curve_n; + } + if(helper_size > 0.0) { + Geom::PathVector const pathv = curve->get_pathvector(); + hp.push_back(pathv[0]); + } +} + +Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size) +{ + char const * svgd = "M 1,0.5 A 0.5,0.5 0 0 1 0.5,1 0.5,0.5 0 0 1 0,0.5 0.5,0.5 0 0 1 0.5,0 0.5,0.5 0 0 1 1,0.5 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + Geom::Affine aff = Geom::Affine(); + aff *= Geom::Scale(helper_size); + pathv *= aff; + pathv *= Geom::Translate(p - Geom::Point(0.5*helper_size, 0.5*helper_size)); + return pathv[0]; +} + +void LPEBSpline::doBSplineFromWidget(SPCurve *curve, double weight_ammount) +{ + using Geom::X; + using Geom::Y; + + if (curve->get_segment_count() < 1) + return; + // Make copy of old path as it is changed during processing + Geom::PathVector const original_pathv = curve->get_pathvector(); + curve->reset(); + + for (const auto & path_it : original_pathv) { + + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + + SPCurve *curve_n = new SPCurve(); + Geom::Point point_at0(0, 0); + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point point_at3(0, 0); + Geom::D2 sbasis_in; + Geom::D2 sbasis_out; + Geom::CubicBezier const *cubic = nullptr; + curve_n->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + SPCurve *in = new SPCurve(); + in->moveto(curve_it1->initialPoint()); + in->lineto(curve_it1->finalPoint()); + cubic = dynamic_cast(&*curve_it1); + point_at0 = in->first_segment()->initialPoint(); + point_at3 = in->first_segment()->finalPoint(); + sbasis_in = in->first_segment()->toSBasis(); + if (cubic) { + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && Geom::are_near((*cubic)[1], point_at0)) || + (apply_with_weight && !Geom::are_near((*cubic)[1], point_at0))) + { + if (isNodePointSelected(point_at0) || !only_selected) { + point_at1 = sbasis_in.valueAt(weight_ammount); + if (weight_ammount != NO_POWER) { + point_at1 = + Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + } + } else { + point_at1 = (*cubic)[1]; + } + } else { + point_at1 = (*cubic)[1]; + } + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && Geom::are_near((*cubic)[2], point_at3)) || + (apply_with_weight && !Geom::are_near((*cubic)[2], point_at3))) + { + if (isNodePointSelected(point_at3) || !only_selected) { + point_at2 = sbasis_in.valueAt(1 - weight_ammount); + if (weight_ammount != NO_POWER) { + point_at2 = + Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + } + } else { + point_at2 = (*cubic)[2]; + } + } else { + point_at2 = (*cubic)[2]; + } + } else { + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && weight_ammount == NO_POWER) || + (apply_with_weight && weight_ammount != NO_POWER)) + { + if (isNodePointSelected(point_at0) || !only_selected) { + point_at1 = sbasis_in.valueAt(weight_ammount); + point_at1 = + Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + } else { + point_at1 = in->first_segment()->initialPoint(); + } + if (isNodePointSelected(point_at3) || !only_selected) { + point_at2 = sbasis_in.valueAt(1 - weight_ammount); + point_at2 = + Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + } else { + point_at2 = in->first_segment()->finalPoint(); + } + } else { + point_at1 = in->first_segment()->initialPoint(); + point_at2 = in->first_segment()->finalPoint(); + } + } + in->reset(); + delete in; + curve_n->curveto(point_at1, point_at2, point_at3); + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + curve_n->move_endpoints(path_it.begin()->initialPoint(), + path_it.begin()->initialPoint()); + } else { + curve_n->move_endpoints(path_it.begin()->initialPoint(), point_at3); + } + if (path_it.closed()) { + curve_n->closepath_current(); + } + curve->append(curve_n, false); + curve_n->reset(); + delete curve_n; + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-bspline.h b/src/live_effects/lpe-bspline.h new file mode 100644 index 0000000..0c3a065 --- /dev/null +++ b/src/live_effects/lpe-bspline.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BSPLINE_H +#define INKSCAPE_LPE_BSPLINE_H + +/* + * Inkscape::LPEBSpline + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBSpline : public Effect { +public: + LPEBSpline(LivePathEffectObject *lpeobject); + ~LPEBSpline() override; + LPEBSpline(const LPEBSpline &) = delete; + LPEBSpline &operator=(const LPEBSpline &) = delete; + + LPEPathFlashType pathFlashType() const override + { + return SUPPRESS_FLASH; + } + void doOnApply(SPLPEItem const* lpeitem) override; + void doEffect(SPCurve *curve) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doBSplineFromWidget(SPCurve *curve, double value); + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; + Gtk::Widget *newWidget() override; + void changeWeight(double weightValue); + void toDefaultWeight(); + void toMakeCusp(); + void toWeight(); + + // TODO make this private + ScalarParam steps; + +private: + ScalarParam helper_size; + BoolParam apply_no_weight; + BoolParam apply_with_weight; + BoolParam only_selected; + ScalarParam weight; + Geom::PathVector hp; +}; +void sp_bspline_do_effect(SPCurve *curve, double helper_size, Geom::PathVector &hp); + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-circle_3pts.cpp b/src/live_effects/lpe-circle_3pts.cpp new file mode 100644 index 0000000..7982abd --- /dev/null +++ b/src/live_effects/lpe-circle_3pts.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Circle through 3 points" implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-circle_3pts.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/circle.h> +#include <2geom/path-sink.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPECircle3Pts::LPECircle3Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ +} + +LPECircle3Pts::~LPECircle3Pts() += default; + +static void _circle3(Geom::Point const &A, Geom::Point const &B, Geom::Point const &C, Geom::PathVector &path_out) { + using namespace Geom; + + Point D = (A + B)/2; + Point E = (B + C)/2; + + Point v = (B - A).ccw(); + Point w = (C - B).ccw(); + + double det = -v[0] * w[1] + v[1] * w[0]; + + Point M; + if (!v.isZero()) { + Point F = E - D; + double lambda = det == 0 ? 0 : (-w[1] * F[0] + w[0] * F[1]) / det; + M = D + v * lambda; + } else { + M = E; + } + + double radius = L2(M - A); + + Geom::Circle c(M, radius); + path_out = Geom::Path(c); +} + +Geom::PathVector +LPECircle3Pts::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + // we assume that the path has >= 3 nodes + Geom::Point A = path_in[0].initialPoint(); + Geom::Point B = path_in[0].pointAt(1); + Geom::Point C = path_in[0].pointAt(2); + + _circle3(A, B, C, path_out); + + return path_out; +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-circle_3pts.h b/src/live_effects/lpe-circle_3pts.h new file mode 100644 index 0000000..bf89b9f --- /dev/null +++ b/src/live_effects/lpe-circle_3pts.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CIRCLE_3PTS_H +#define INKSCAPE_LPE_CIRCLE_3PTS_H + +/** \file + * LPE "Circle through 3 points" implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECircle3Pts : public Effect { +public: + LPECircle3Pts(LivePathEffectObject *lpeobject); + ~LPECircle3Pts() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPECircle3Pts(const LPECircle3Pts&) = delete; + LPECircle3Pts& operator=(const LPECircle3Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-circle_with_radius.cpp b/src/live_effects/lpe-circle_with_radius.cpp new file mode 100644 index 0000000..1a6b3a8 --- /dev/null +++ b/src/live_effects/lpe-circle_with_radius.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE effect that draws a circle based on two points and a radius. + * - implementation + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-circle_with_radius.h" +#include "display/curve.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/circle.h> +#include <2geom/path-sink.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPECircleWithRadius::LPECircleWithRadius(LivePathEffectObject *lpeobject) : + Effect(lpeobject)//, + // initialise your parameters here: + //radius(_("Float parameter"), _("just a real number like 1.4!"), "svgname", &wr, this, 50) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + //registerParameter( dynamic_cast(&radius) ); +} + +LPECircleWithRadius::~LPECircleWithRadius() += default; + +Geom::PathVector +LPECircleWithRadius::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + Geom::Point center = path_in[0].initialPoint(); + Geom::Point pt = path_in[0].finalPoint(); + + double radius = Geom::L2(pt - center); + + Geom::Circle c(center, radius); + return Geom::Path(c); +} + +/* + +Geom::Piecewise > +LPECircleWithRadius::doEffect_pwd2 (Geom::Piecewise > & pwd2_in) +{ + Geom::Piecewise > output; + + output = pwd2_in; // spice this up to make the effect actually *do* something! + + return output; +} + +*/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-circle_with_radius.h b/src/live_effects/lpe-circle_with_radius.h new file mode 100644 index 0000000..dc9a8b9 --- /dev/null +++ b/src/live_effects/lpe-circle_with_radius.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief LPE effect that draws a circle based on two points and a radius + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_CIRCLE_WITH_RADIUS_H +#define INKSCAPE_LPE_CIRCLE_WITH_RADIUS_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECircleWithRadius : public Effect { +public: + LPECircleWithRadius(LivePathEffectObject *lpeobject); + ~LPECircleWithRadius() override; + +// Choose to implement one of the doEffect functions. You can delete or comment out the others. + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + // add the parameters for your effect here: + //ScalarParam radius; + // there are all kinds of parameters. Check the /live_effects/parameter directory which types exist! + + LPECircleWithRadius(const LPECircleWithRadius&) = delete; + LPECircleWithRadius& operator=(const LPECircleWithRadius&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-clone-original.cpp b/src/live_effects/lpe-clone-original.cpp new file mode 100644 index 0000000..0a7cd18 --- /dev/null +++ b/src/live_effects/lpe-clone-original.cpp @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "display/curve.h" +#include "svg/path-string.h" +#include "svg/svg.h" + +#include "ui/tools-switch.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "display/curve.h" + +#include "xml/sp-css-attr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData ClonelpemethodData[] = { + { CLM_NONE, N_("No Shape"), "none" }, + { CLM_D, N_("With LPE's"), "d" }, + { CLM_ORIGINALD, N_("Without LPE's"), "originald" }, + { CLM_BSPLINESPIRO, N_("Spiro or BSpline Only"), "bsplinespiro" }, +}; +static const Util::EnumDataConverter CLMConverter(ClonelpemethodData, CLM_END); + +LPECloneOriginal::LPECloneOriginal(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , linkeditem(_("Linked Item:"), _("Item from which to take the original data"), "linkeditem", &wr, this) + , method(_("Shape"), _("Shape linked"), "method", CLMConverter, &wr, this, CLM_D) + , attributes("Attributes", "Attributes linked, comma separated attributes like trasform, X, Y...", + "attributes", &wr, this, "") + , css_properties("CSS Properties", + "CSS properties linked, comma separated attributes like fill, filter, opacity...", + "css_properties", &wr, this, "") + , allow_transforms(_("Allow Transforms"), _("Allow transforms"), "allow_transforms", &wr, this, true) +{ + //0.92 compatibility + const gchar * linkedpath = this->getRepr()->attribute("linkedpath"); + if (linkedpath && strcmp(linkedpath, "") != 0){ + this->getRepr()->setAttribute("linkeditem", linkedpath); + this->getRepr()->removeAttribute("linkedpath"); + this->getRepr()->setAttribute("method", "bsplinespiro"); + this->getRepr()->setAttribute("allow_transforms", "false"); + }; + listening = false; + sync = false; + linked = ""; + if (this->getRepr()->attribute("linkeditem")) { + linked = this->getRepr()->attribute("linkeditem"); + } + registerParameter(&linkeditem); + registerParameter(&method); + registerParameter(&attributes); + registerParameter(&css_properties); + registerParameter(&allow_transforms); + attributes.param_hide_canvas_text(); + css_properties.param_hide_canvas_text(); +} + +void +LPECloneOriginal::syncOriginal() +{ + if (method != CLM_NONE) { + sync = true; + // TODO remove the tools_switch atrocity. + sp_lpe_item_update_patheffect (sp_lpe_item, false, true); + method.param_set_value(CLM_NONE); + refresh_widgets = true; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + sp_lpe_item_update_patheffect (sp_lpe_item, false, true); + if (desktop && tools_isactive(desktop, TOOLS_NODES)) { + tools_switch(desktop, TOOLS_SELECT); + tools_switch(desktop, TOOLS_NODES); + } + } +} + +Gtk::Widget * +LPECloneOriginal::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox(Effect::newWidget()) ); + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast(param->param_newWidget()); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + Gtk::Button * sync_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("No Shape Sync to Current")))); + sync_button->signal_clicked().connect(sigc::mem_fun (*this,&LPECloneOriginal::syncOriginal)); + vbox->pack_start(*sync_button, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPECloneOriginal::cloneAttrbutes(SPObject *origin, SPObject *dest, const gchar * attributes, const gchar * css_properties, bool init) +{ + SPDocument *document = getSPDoc(); + if (!document || !origin || !dest) { + return; + } + SPGroup * group_origin = dynamic_cast(origin); + SPGroup * group_dest = dynamic_cast(dest); + if (group_origin && group_dest && group_origin->getItemCount() != group_dest->getItemCount()) { + sp_lpe_item_enable_path_effects(sp_lpe_item, false); + std::vector< SPObject * > childs = group_origin->childList(true); + std::vector< SPObject * > childsdest = group_dest->childList(true); + for (auto & child : childsdest) { + child->deleteObject(true); + } + for (auto & child : childs) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node* dup = child->getRepr()->duplicate(xml_doc); + dest->getRepr()->appendChild(dup); + Inkscape::GC::release(dup); + } + sp_lpe_item_enable_path_effects(sp_lpe_item, true); + } + if (group_origin && group_dest) { + std::vector< SPObject * > childs = group_origin->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = group_dest->nthChild(index); + cloneAttrbutes(child, dest_child, attributes, css_properties, init); + index++; + } + } + //Attributes + SPShape * shape_origin = dynamic_cast(origin); + SPShape * shape_dest = dynamic_cast(dest); + SPItem * item_origin = dynamic_cast(origin); + SPItem * item_dest = dynamic_cast(dest); + SPMask * mask_origin = dynamic_cast(item_origin->getMaskObject()); + SPMask * mask_dest = dynamic_cast(item_dest->getMaskObject()); + if(mask_origin && mask_dest) { + std::vector mask_list = mask_origin->childList(true); + std::vector mask_list_dest = mask_dest->childList(true); + if (mask_list.size() == mask_list_dest.size()) { + size_t i = 0; + for (auto mask_data : mask_list) { + SPObject * mask_dest_data = mask_list_dest[i]; + cloneAttrbutes(mask_data, mask_dest_data, attributes, css_properties, init); + i++; + } + } + } + + SPClipPath *clippath_origin = SP_ITEM(origin)->getClipObject(); + SPClipPath *clippath_dest = SP_ITEM(dest)->getClipObject(); + if(clippath_origin && clippath_dest) { + std::vector clippath_list = clippath_origin->childList(true); + std::vector clippath_list_dest = clippath_dest->childList(true); + if (clippath_list.size() == clippath_list_dest.size()) { + size_t i = 0; + for (auto clippath_data : clippath_list) { + SPObject * clippath_dest_data = clippath_list_dest[i]; + cloneAttrbutes(clippath_data, clippath_dest_data, attributes, css_properties, init); + i++; + } + } + } + gchar ** attarray = g_strsplit(old_attributes.c_str(), ",", 0); + gchar ** iter = attarray; + while (*iter != nullptr) { + const char* attribute = (*iter); + if (strlen(attribute)) { + dest->getRepr()->removeAttribute(attribute); + } + iter++; + } + attarray = g_strsplit(attributes, ",", 0); + iter = attarray; + while (*iter != nullptr) { + const char* attribute = (*iter); + if (strlen(attribute) && shape_dest && shape_origin) { + if (std::strcmp(attribute, "d") == 0) { + SPCurve *c = nullptr; + if (method == CLM_BSPLINESPIRO) { + c = shape_origin->getCurveForEdit(); + SPLPEItem * lpe_item = SP_LPE_ITEM(origin); + if (lpe_item) { + PathEffectList lpelist = lpe_item->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast(lpe)) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(c, 0, hp); + } else if (dynamic_cast(lpe)) { + LivePathEffect::sp_spiro_do_effect(c); + } + } + } + } + } else if (method == CLM_ORIGINALD) { + c = shape_origin->getCurveForEdit(); + } else if(method == CLM_D){ + c = shape_origin->getCurve(); + } + if (c && method != CLM_NONE) { + Geom::PathVector c_pv = c->get_pathvector(); + c->set_pathvector(c_pv); + gchar *str = sp_svg_write_path(c_pv); + if (sync){ + dest->getRepr()->setAttribute("inkscape:original-d", str); + } + shape_dest->setCurveInsync(c); + dest->getRepr()->setAttribute("d", str); + g_free(str); + c->unref(); + } else if (method != CLM_NONE) { + dest->getRepr()->removeAttribute(attribute); + } + } else { + dest->getRepr()->setAttribute(attribute, origin->getRepr()->attribute(attribute)); + } + } + iter++; + } + if (!allow_transforms) { + dest->getRepr()->setAttribute("transform", origin->getRepr()->attribute("transform")); + } + g_strfreev (attarray); + SPCSSAttr *css_origin = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css_origin, origin->getRepr()->attribute("style")); + SPCSSAttr *css_dest = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css_dest, dest->getRepr()->attribute("style")); + if (init) { + css_dest = css_origin; + } + gchar ** styleattarray = g_strsplit(old_css_properties.c_str(), ",", 0); + gchar ** styleiter = styleattarray; + while (*styleiter != nullptr) { + const char* attribute = (*styleiter); + if (strlen(attribute)) { + sp_repr_css_set_property (css_dest, attribute, nullptr); + } + styleiter++; + } + styleattarray = g_strsplit(css_properties, ",", 0); + styleiter = styleattarray; + while (*styleiter != nullptr) { + const char* attribute = (*styleiter); + if (strlen(attribute)) { + const char* origin_attribute = sp_repr_css_property(css_origin, attribute, ""); + if (!strlen(origin_attribute)) { //==0 + sp_repr_css_set_property (css_dest, attribute, nullptr); + } else { + sp_repr_css_set_property (css_dest, attribute, origin_attribute); + } + } + styleiter++; + } + g_strfreev (styleattarray); + Glib::ustring css_str; + sp_repr_css_write_string(css_dest,css_str); + dest->getRepr()->setAttributeOrRemoveIfEmpty("style", css_str); +} + +void +LPECloneOriginal::doBeforeEffect (SPLPEItem const* lpeitem){ + start_listening(); + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (!deleted_connection) { + deleted_connection = sp_lpe_item->connectDelete(sigc::mem_fun(*this, &LPECloneOriginal::lpeitem_deleted)); + } + + if (linkeditem.linksToItem()) { + SPItem *orig = dynamic_cast(linkeditem.getObject()); + if(!orig) { + return; + } + SPText *text_origin = dynamic_cast(orig); + SPGroup *group_origin = dynamic_cast(orig); + SPItem *dest = dynamic_cast(sp_lpe_item); + const gchar * id = orig->getId(); + bool init = !is_load && g_strcmp0(id, linked.c_str()) != 0; + /* if (sp_lpe_item->getRepr()->attribute("style")) { + init = false; + } */ + Glib::ustring attr = "d,"; + if (text_origin) { + SPCurve * curve = text_origin->getNormalizedBpath(); + gchar *str = sp_svg_write_path(curve->get_pathvector()); + dest->getRepr()->setAttribute("inkscape:original-d", str); + g_free(str); + curve->unref(); + attr = ""; + } + if (!allow_transforms) { + attr += Glib::ustring("transform") + Glib::ustring(","); + } + original_bbox(lpeitem, false, true); + auto attributes_str = attributes.param_getSVGValue(); + attr += attributes_str + ","; + if (attr.size() && attributes_str.empty()) { + attr.erase (attr.size()-1, 1); + } + auto css_properties_str = css_properties.param_getSVGValue(); + Glib::ustring style_attr = ""; + if (style_attr.size() && css_properties_str.empty()) { + style_attr.erase (style_attr.size()-1, 1); + } + style_attr += css_properties_str + ","; + cloneAttrbutes(orig, dest, attr.c_str(), style_attr.c_str(), init); + if (!group_origin && linkeditem.last_transform.isTranslation()) { + Geom::Affine orig = sp_lpe_item->transform; + sp_lpe_item->transform *= orig.inverse() * linkeditem.last_transform.inverse() * orig; + linkeditem.last_transform = Geom::identity(); + } + old_css_properties = css_properties.param_getSVGValue(); + old_attributes = attributes.param_getSVGValue(); + sync = false; + linked = id; + } else { + linked = ""; + } +} + +void +LPECloneOriginal::start_listening() +{ + if ( !sp_lpe_item || listening ) { + return; + } + quit_listening(); + listening = true; +} + +void +LPECloneOriginal::quit_listening() +{ + listening = false; +} + +void +LPECloneOriginal::lpeitem_deleted(SPObject */*deleted*/) +{ + quit_listening(); + deleted_connection.disconnect(); +} + +LPECloneOriginal::~LPECloneOriginal() +{ + quit_listening(); +} + +void +LPECloneOriginal::doEffect (SPCurve * curve) +{ + if (method != CLM_NONE) { + SPCurve *current_curve = current_shape->getCurve(); + if (current_curve != nullptr) { + curve->set_pathvector(current_curve->get_pathvector()); + current_curve->unref(); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-clone-original.h b/src/live_effects/lpe-clone-original.h new file mode 100644 index 0000000..7266f06 --- /dev/null +++ b/src/live_effects/lpe-clone-original.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CLONE_ORIGINAL_H +#define INKSCAPE_LPE_CLONE_ORIGINAL_H + +/* + * Inkscape::LPECloneOriginal + * + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/originalitem.h" +#include "live_effects/parameter/text.h" +#include "live_effects/lpegroupbbox.h" + +#include + +namespace Inkscape { +namespace LivePathEffect { + +enum Clonelpemethod { CLM_NONE, CLM_D, CLM_ORIGINALD, CLM_BSPLINESPIRO, CLM_END }; + +class LPECloneOriginal : public Effect, GroupBBoxEffect { +public: + LPECloneOriginal(LivePathEffectObject *lpeobject); + ~LPECloneOriginal() override; + void doEffect (SPCurve * curve) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void cloneAttrbutes(SPObject *origin, SPObject *dest, const gchar * attributes, const gchar * css_properties, bool init); + void modified(SPObject */*obj*/, guint /*flags*/); + Gtk::Widget *newWidget() override; + void syncOriginal(); + void start_listening(); + void quit_listening(); + void lpeitem_deleted(SPObject */*deleted*/); + +private: + OriginalItemParam linkeditem; + EnumParam method; + TextParam attributes; + TextParam css_properties; + BoolParam allow_transforms; + Glib::ustring old_attributes; + Glib::ustring old_css_properties; + Glib::ustring linked; + bool listening; + bool sync; + sigc::connection deleted_connection; + LPECloneOriginal(const LPECloneOriginal&) = delete; + LPECloneOriginal& operator=(const LPECloneOriginal&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-constructgrid.cpp b/src/live_effects/lpe-constructgrid.cpp new file mode 100644 index 0000000..e758619 --- /dev/null +++ b/src/live_effects/lpe-constructgrid.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Construct Grid implementation + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-constructgrid.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +LPEConstructGrid::LPEConstructGrid(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + nr_x(_("Size _X:"), _("The size of the grid in X direction."), "nr_x", &wr, this, 5), + nr_y(_("Size _Y:"), _("The size of the grid in Y direction."), "nr_y", &wr, this, 5) +{ + registerParameter(&nr_x); + registerParameter(&nr_y); + + nr_x.param_make_integer(); + nr_y.param_make_integer(); + nr_x.param_set_range(1, 1e10); + nr_y.param_set_range(1, 1e10); +} + +LPEConstructGrid::~LPEConstructGrid() += default; + +Geom::PathVector +LPEConstructGrid::doEffect_path (Geom::PathVector const & path_in) +{ + // Check that the path has at least 3 nodes (i.e. 2 segments), more nodes are ignored + if (path_in[0].size() >= 2) { + // read the first 3 nodes: + Geom::Path::const_iterator it ( path_in[0].begin() ); + Geom::Point first_p = (*it++).initialPoint(); + Geom::Point origin = (*it++).initialPoint(); + Geom::Point second_p = (*it++).initialPoint(); + // make first_p and second_p be the construction *vectors* of the grid: + first_p -= origin; + second_p -= origin; + Geom::Translate first_translation( first_p ); + Geom::Translate second_translation( second_p ); + + // create the gridpaths of the two directions + Geom::Path first_path( origin ); + first_path.appendNew( origin + first_p*nr_y ); + Geom::Path second_path( origin ); + second_path.appendNew( origin + second_p*nr_x ); + + // use the gridpaths and set them in the correct grid + Geom::PathVector path_out; + path_out.push_back(first_path); + for (int ix = 0; ix < nr_x; ix++) { + path_out.push_back(path_out.back() * second_translation ); + } + path_out.push_back(second_path); + for (int iy = 0; iy < nr_y; iy++) { + path_out.push_back(path_out.back() * first_translation ); + } + + return path_out; + } else { + return path_in; + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-constructgrid.h b/src/live_effects/lpe-constructgrid.h new file mode 100644 index 0000000..5865402 --- /dev/null +++ b/src/live_effects/lpe-constructgrid.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CONSTRUCTGRID_H +#define INKSCAPE_LPE_CONSTRUCTGRID_H + +/** \file + * Implementation of the construct grid LPE, see lpe-constructgrid.cpp + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEConstructGrid : public Effect { +public: + LPEConstructGrid(LivePathEffectObject *lpeobject); + ~LPEConstructGrid() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + ScalarParam nr_x; + ScalarParam nr_y; + + LPEConstructGrid(const LPEConstructGrid&) = delete; + LPEConstructGrid& operator=(const LPEConstructGrid&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif // INKSCAPE_LPE_CONSTRUCTGRID_H diff --git a/src/live_effects/lpe-copy_rotate.cpp b/src/live_effects/lpe-copy_rotate.cpp new file mode 100644 index 0000000..073dfdc --- /dev/null +++ b/src/live_effects/lpe-copy_rotate.cpp @@ -0,0 +1,694 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Jabiertxo Arraiza Cenoz + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-copy_rotate.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/lpeobject.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "xml/sp-css-attr.h" +#include <2geom/intersection-graph.h> +#include <2geom/path-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include +#include + +#include "splivarot.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData RotateMethodData[RM_END] = { + { RM_NORMAL, N_("Normal"), "normal" }, + { RM_KALEIDOSCOPE, N_("Kaleidoscope"), "kaleidoskope" }, + { RM_FUSE, N_("Fuse paths"), "fuse_paths" } +}; +static const Util::EnumDataConverter +RMConverter(RotateMethodData, RM_END); + +bool +pointInTriangle(Geom::Point const &p, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) +{ + //http://totologic.blogspot.com.es/2014/01/accurate-point-in-triangle-test.html + using Geom::X; + using Geom::Y; + double denominator = (p1[X]*(p2[Y] - p3[Y]) + p1[Y]*(p3[X] - p2[X]) + p2[X]*p3[Y] - p2[Y]*p3[X]); + double t1 = (p[X]*(p3[Y] - p1[Y]) + p[Y]*(p1[X] - p3[X]) - p1[X]*p3[Y] + p1[Y]*p3[X]) / denominator; + double t2 = (p[X]*(p2[Y] - p1[Y]) + p[Y]*(p1[X] - p2[X]) - p1[X]*p2[Y] + p1[Y]*p2[X]) / -denominator; + double s = t1 + t2; + + return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && s <= 1; +} + +LPECopyRotate::LPECopyRotate(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + method(_("Method:"), _("Rotate methods"), "method", RMConverter, &wr, this, RM_NORMAL), + origin(_("Origin"), _("Adjust origin of the rotation"), "origin", &wr, this, _("Adjust origin of the rotation")), + starting_point(_("Start point"), _("Starting point to define start angle"), "starting_point", &wr, this, _("Adjust starting point to define start angle")), + starting_angle(_("Starting angle"), _("Angle of the first copy"), "starting_angle", &wr, this, 0.0), + rotation_angle(_("Rotation angle"), _("Angle between two successive copies"), "rotation_angle", &wr, this, 60.0), + num_copies(_("Number of copies"), _("Number of copies of the original path"), "num_copies", &wr, this, 6), + gap(_("Gap"), _("Gap space between copies, use small negative gaps to fix some joins"), "gap", &wr, this, -0.01), + copies_to_360(_("Distribute evenly"), _("Angle between copies is 360°/number of copies (ignores rotation angle setting)"), "copies_to_360", &wr, this, true), + mirror_copies(_("Mirror copies"), _("Mirror between copies"), "mirror_copies", &wr, this, false), + split_items(_("Split elements"), _("Split elements, so each can have its own style"), "split_items", &wr, this, false), + dist_angle_handle(100.0) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + //0.92 compatibility + if (this->getRepr()->attribute("fuse_paths") && strcmp(this->getRepr()->attribute("fuse_paths"), "true") == 0){ + this->getRepr()->removeAttribute("fuse_paths"); + this->getRepr()->setAttribute("method", "kaleidoskope"); + this->getRepr()->setAttribute("mirror_copies", "true"); + }; + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&method); + registerParameter(&num_copies); + registerParameter(&starting_angle); + registerParameter(&starting_point); + registerParameter(&rotation_angle); + registerParameter(&gap); + registerParameter(&origin); + registerParameter(&copies_to_360); + registerParameter(&mirror_copies); + registerParameter(&split_items); + + gap.param_set_range(-99999.0, 99999.0); + gap.param_set_increments(0.01, 0.01); + gap.param_set_digits(5); + num_copies.param_set_range(1, 999999); + num_copies.param_make_integer(true); + apply_to_clippath_and_mask = true; + previous_num_copies = num_copies; + previous_origin = Geom::Point(0,0); + previous_start_point = Geom::Point(0,0); + starting_point.param_widget_is_visible(false); + reset = false; +} + +LPECopyRotate::~LPECopyRotate() += default; + +void +LPECopyRotate::doAfterEffect (SPLPEItem const* lpeitem) +{ + if (split_items) { + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + items.clear(); + container = dynamic_cast(sp_lpe_item->parent); + if (previous_num_copies != num_copies) { + gint numcopies_gap = previous_num_copies - num_copies; + if (numcopies_gap > 0 && num_copies != 0) { + guint counter = num_copies - 1; + while (numcopies_gap > 0) { + Glib::ustring id = Glib::ustring("rotated-"); + id += std::to_string(counter); + id += "-"; + id += this->lpeobj->getId(); + if (id.empty()) { + return; + } + SPObject *elemref = document->getObjectById(id.c_str()); + if (elemref) { + SP_ITEM(elemref)->setHidden(true); + } + counter++; + numcopies_gap--; + } + } + previous_num_copies = num_copies; + } + SPObject *elemref = nullptr; + guint counter = 0; + Glib::ustring id = "rotated-0-"; + id += this->lpeobj->getId(); + while((elemref = document->getObjectById(id.c_str()))) { + id = Glib::ustring("rotated-"); + id += std::to_string(counter); + id += "-"; + id += this->lpeobj->getId(); + if (SP_ITEM(elemref)->isHidden()) { + items.push_back(id); + } + counter++; + } + Geom::Affine m = Geom::Translate(-origin) * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))); + for (size_t i = 1; i < num_copies; ++i) { + Geom::Affine r = Geom::identity(); + if(mirror_copies && i%2 != 0) { + r *= Geom::Rotate(Geom::Angle(half_dir)).inverse(); + r *= Geom::Scale(1, -1); + r *= Geom::Rotate(Geom::Angle(half_dir)); + } + + Geom::Rotate rot(-(Geom::rad_from_deg(rotation_angle * i))); + Geom::Affine t = m * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if (method != RM_NORMAL) { + if(mirror_copies && i%2 != 0) { + t = m * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + } else { + if(mirror_copies && i%2 != 0) { + t = m * Geom::Rotate(Geom::rad_from_deg(-rotation_angle)) * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + } + t *= sp_lpe_item->transform; + toItem(t, i-1, reset); + } + reset = false; + } else { + processObjects(LPE_ERASE); + items.clear(); + } +} + +void LPECopyRotate::cloneStyle(SPObject *orig, SPObject *dest) +{ + dest->getRepr()->setAttribute("style", orig->getRepr()->attribute("style")); + for (auto iter : orig->style->properties()) { + if (iter->style_src != SP_STYLE_SRC_UNSET) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) { + const gchar *attr = orig->getRepr()->attribute(iter->name().c_str()); + if (attr) { + dest->getRepr()->setAttribute(iter->name(), attr); + } + } + } + } +} + +void +LPECopyRotate::cloneD(SPObject *orig, SPObject *dest, Geom::Affine transform, bool reset) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child, transform, reset); + index++; + } + return; + } + + if ( SP_IS_TEXT(orig) && SP_IS_TEXT(dest) && SP_TEXT(orig)->children.size() == SP_TEXT(dest)->children.size()) { + if (reset) { + cloneStyle(orig, dest); + } + size_t index = 0; + for (auto & child : SP_TEXT(orig)->children) { + SPObject *dest_child = dest->nthChild(index); + cloneD(&child, dest_child, transform, reset); + index++; + } + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + if (shape) { + SPCurve *c = shape->getCurve(); + if (c) { + gchar *str = sp_svg_write_path(c->get_pathvector()); + if (shape && !path) { + const char * id = dest->getRepr()->attribute("id"); + const char * style = dest->getRepr()->attribute("style"); + Inkscape::XML::Document *xml_doc = dest->document->getReprDoc(); + Inkscape::XML::Node *dest_node = xml_doc->createElement("svg:path");; + dest_node->setAttribute("id", id); + dest_node->setAttribute("inkscape:connector-curvature", "0"); + dest_node->setAttribute("style", style); + dest->updateRepr(xml_doc, dest_node, SP_OBJECT_WRITE_ALL); + path = SP_PATH(dest); + } + path->getRepr()->setAttribute("d", str); + g_free(str); + c->unref(); + } else { + path->getRepr()->removeAttribute("d"); + } + + } + if (reset) { + cloneStyle(orig, dest); + } +} + +Inkscape::XML::Node * +LPECopyRotate::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + std::vector const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + resultnode->setAttribute("style", prev->attribute("style")); + return resultnode; +} + +void +LPECopyRotate::toItem(Geom::Affine transform, size_t i, bool reset) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Glib::ustring elemref_id = Glib::ustring("rotated-"); + elemref_id += std::to_string(i); + elemref_id += "-"; + elemref_id += this->lpeobj->getId(); + items.push_back(elemref_id); + SPObject *elemref = document->getObjectById(elemref_id.c_str()); + Inkscape::XML::Node *phantom = nullptr; + if (elemref) { + phantom = elemref->getRepr(); + } else { + phantom = createPathBase(sp_lpe_item); + phantom->setAttribute("id", elemref_id); + reset = true; + elemref = container->appendChildRepr(phantom); + Inkscape::GC::release(phantom); + } + cloneD(SP_OBJECT(sp_lpe_item), elemref, transform, reset); + gchar *str = sp_svg_transform_write(transform); + elemref->getRepr()->setAttribute("transform" , str); + g_free(str); + SP_ITEM(elemref)->setHidden(false); + if (elemref->parent != container) { + Inkscape::XML::Node *copy = phantom->duplicate(xml_doc); + copy->setAttribute("id", elemref_id); + container->appendChildRepr(copy); + Inkscape::GC::release(copy); + elemref->deleteObject(); + } +} + +void +LPECopyRotate::resetStyles(){ + reset = true; + doAfterEffect(sp_lpe_item); +} + +Gtk::Widget * LPECopyRotate::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + Gtk::HBox * hbox = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::Button * reset_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset styles")))); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPECopyRotate::resetStyles)); + reset_button->set_size_request(110, 20); + vbox->pack_start(*hbox, true, true, 2); + hbox->pack_start(*reset_button, false, false, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + + +void +LPECopyRotate::doOnApply(SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.middle(), boundingbox_Y.middle()); + origin.param_setValue(A, true); + origin.param_update_default(A); + dist_angle_handle = L2(B - A); + dir = unit_vector(B - A); +} + +void +LPECopyRotate::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + if (copies_to_360 && num_copies > 2) { + rotation_angle.param_set_value(360.0/(double)num_copies); + } + if (method != RM_NORMAL && rotation_angle * num_copies > 360 && rotation_angle > 0 && copies_to_360) { + num_copies.param_set_value(floor(360/rotation_angle)); + } + if (method != RM_NORMAL && mirror_copies && copies_to_360) { + num_copies.param_set_increments(2.0,10.0); + if ((int)num_copies%2 !=0) { + num_copies.param_set_value(num_copies+1); + rotation_angle.param_set_value(360.0/(double)num_copies); + } + } else { + num_copies.param_set_increments(1.0, 10.0); + } + + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.middle(), boundingbox_Y.middle()); + if (Geom::are_near(A, B, 0.01)) { + B += Geom::Point(1.0, 0.0); + } + dir = unit_vector(B - A); + // I first suspected the minus sign to be a bug in 2geom but it is + // likely due to SVG's choice of coordinate system orientation (max) + bool near_start_point = Geom::are_near(previous_start_point, (Geom::Point)starting_point, 0.01); + bool near_origin = Geom::are_near(previous_origin, (Geom::Point)origin, 0.01); + if (!near_start_point) { + starting_angle.param_set_value(deg_from_rad(-angle_between(dir, starting_point - origin))); + if (GDK_SHIFT_MASK) { + dist_angle_handle = L2(B - A); + } else { + dist_angle_handle = L2(starting_point - origin); + } + } + if (dist_angle_handle < 1.0) { + dist_angle_handle = 1.0; + } + double distance = dist_angle_handle; + if (previous_start_point != Geom::Point(0,0) || previous_origin != Geom::Point(0,0)) { + distance = Geom::distance(previous_origin, starting_point); + } + start_pos = origin + dir * Rotate(-rad_from_deg(starting_angle)) * distance; + if (!near_start_point || !near_origin || split_items) { + starting_point.param_setValue(start_pos); + } + + previous_origin = (Geom::Point)origin; + previous_start_point = (Geom::Point)starting_point; +} + +void +LPECopyRotate::split(Geom::PathVector &path_on, Geom::Path const ÷r) +{ + Geom::PathVector tmp_path; + double time_start = 0.0; + Geom::Path original = path_on[0]; + int position = 0; + Geom::Crossings cs = crossings(original,divider); + std::vector crossed; + for(auto & c : cs) { + crossed.push_back(c.ta); + } + std::sort(crossed.begin(), crossed.end()); + for (double time_end : crossed) { + if (time_start == time_end || time_end - time_start < Geom::EPSILON) { + continue; + } + Geom::Path portion_original = original.portion(time_start,time_end); + if (!portion_original.empty()) { + Geom::Point side_checker = portion_original.pointAt(0.0001); + position = Geom::sgn(Geom::cross(divider[1].finalPoint() - divider[0].finalPoint(), side_checker - divider[0].finalPoint())); + if (rotation_angle != 180) { + position = pointInTriangle(side_checker, divider.initialPoint(), divider[0].finalPoint(), divider[1].finalPoint()); + } + if (position == 1) { + tmp_path.push_back(portion_original); + } + portion_original.clear(); + time_start = time_end; + } + } + position = Geom::sgn(Geom::cross(divider[1].finalPoint() - divider[0].finalPoint(), original.finalPoint() - divider[0].finalPoint())); + if (rotation_angle != 180) { + position = pointInTriangle(original.finalPoint(), divider.initialPoint(), divider[0].finalPoint(), divider[1].finalPoint()); + } + if (cs.size() > 0 && position == 1) { + Geom::Path portion_original = original.portion(time_start, original.size()); + if(!portion_original.empty()){ + if (!original.closed()) { + tmp_path.push_back(portion_original); + } else { + if (tmp_path.size() > 0 && tmp_path[0].size() > 0 ) { + portion_original.setFinal(tmp_path[0].initialPoint()); + portion_original.append(tmp_path[0]); + tmp_path[0] = portion_original; + } else { + tmp_path.push_back(portion_original); + } + } + portion_original.clear(); + } + } + if (cs.size()==0 && position == 1) { + tmp_path.push_back(original); + } + path_on = tmp_path; +} + +Geom::PathVector +LPECopyRotate::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + double diagonal = Geom::distance(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + Geom::OptRect bbox = sp_lpe_item->geometricBounds(); + size_divider = Geom::distance(origin,bbox) + (diagonal * 6); + Geom::Point line_start = origin + dir * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))) * size_divider; + Geom::Point line_end = origin + dir * Geom::Rotate(-(Geom::rad_from_deg(rotation_angle + starting_angle))) * size_divider; + divider = Geom::Path(line_start); + divider.appendNew((Geom::Point)origin); + divider.appendNew(line_end); + Geom::OptRect trianglebounds = divider.boundsFast(); + divider.close(); + half_dir = unit_vector(Geom::middle_point(line_start,line_end) - (Geom::Point)origin); + FillRuleBool fillrule = fill_nonZero; + if (current_shape->style && + current_shape->style->fill_rule.set && + current_shape->style->fill_rule.computed == SP_WIND_RULE_EVENODD) + { + fillrule = (FillRuleBool)fill_oddEven; + } + if (method != RM_NORMAL) { + if (method != RM_KALEIDOSCOPE) { + path_out = doEffect_path_post(path_in, fillrule); + } else { + path_out = pathv_to_linear_and_cubic_beziers(path_in); + } + if (num_copies == 0) { + return path_out; + } + Geom::PathVector triangle; + triangle.push_back(divider); + path_out = sp_pathvector_boolop(path_out, triangle, bool_op_inters, fillrule, fillrule); + if ( !split_items ) { + path_out = doEffect_path_post(path_out, fillrule); + } else { + path_out *= Geom::Translate(half_dir * gap); + } + } else { + path_out = doEffect_path_post(path_in, fillrule); + } + if (!split_items && method != RM_NORMAL) { + Geom::PathVector path_out_tmp; + for (const auto & path_it : path_out) { + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + Geom::Path res; + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + if (!Geom::are_near(curve_it1->initialPoint(), curve_it1->pointAt(0.5), 0.05)) { + if (!res.empty()) { + res.setFinal(curve_it1->initialPoint()); + } + Geom::Curve *c = curve_it1->duplicate(); + res.append(c); + } + ++curve_it1; + } + if (path_it.closed()) { + res.close(); + } + path_out_tmp.push_back(res); + } + path_out = path_out_tmp; + } + return pathv_to_linear_and_cubic_beziers(path_out); +} + +Geom::PathVector +LPECopyRotate::doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule) +{ + if ((split_items || num_copies == 1) && method == RM_NORMAL) { + if (split_items) { + Geom::PathVector path_out = pathv_to_linear_and_cubic_beziers(path_in); + Geom::Affine m = Geom::Translate(-origin) * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))); + Geom::Affine t = m * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + return path_out * t; + } + return path_in; + } + + Geom::Affine pre = Geom::Translate(-origin) * Geom::Rotate(-Geom::rad_from_deg(starting_angle)); + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector output_pv; + Geom::PathVector output; + for (int i = 0; i < num_copies; ++i) { + Geom::Rotate rot(-Geom::rad_from_deg(rotation_angle * i)); + Geom::Affine r = Geom::identity(); + if( i%2 != 0 && mirror_copies) { + r *= Geom::Rotate(Geom::Angle(half_dir)).inverse(); + r *= Geom::Scale(1, -1); + r *= Geom::Rotate(Geom::Angle(half_dir)); + } + Geom::Affine t = pre * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if(mirror_copies && i%2 != 0) { + t = pre * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)).inverse() * Geom::Translate(origin); + } + if (method != RM_NORMAL) { + //we use safest way to union + Geom::PathVector join_pv = original_pathv * t; + join_pv *= Geom::Translate(half_dir * rot * gap); + if (!output_pv.empty()) { + output_pv = sp_pathvector_boolop(output_pv, join_pv, bool_op_union, fillrule, fillrule); + } else { + output_pv = join_pv; + } + } else { + t = pre * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if(mirror_copies && i%2 != 0) { + t = pre * Geom::Rotate(Geom::rad_from_deg(-starting_angle-rotation_angle)) * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + output_pv = path_in * t; + output.insert(output.end(), output_pv.begin(), output_pv.end()); + } + } + if (method != RM_NORMAL) { + output = output_pv; + } + return output; +} + +void +LPECopyRotate::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path hp; + hp.start(start_pos); + hp.appendNew((Geom::Point)origin); + hp.appendNew(origin + dir * Rotate(-rad_from_deg(rotation_angle+starting_angle)) * Geom::distance(origin,starting_point)); + Geom::PathVector pathv; + pathv.push_back(hp); + hp_vec.push_back(pathv); +} + +void +LPECopyRotate::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); +} + +void +LPECopyRotate::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPECopyRotate::doOnRemove (SPLPEItem const* lpeitem) +{ + //set "keep paths" hook on sp-lpe-item.cpp + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + items.clear(); + return; + } + processObjects(LPE_ERASE); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-copy_rotate.h b/src/live_effects/lpe-copy_rotate.h new file mode 100644 index 0000000..8469237 --- /dev/null +++ b/src/live_effects/lpe-copy_rotate.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_COPY_ROTATE_H +#define INKSCAPE_LPE_COPY_ROTATE_H + +/** \file + * LPE implementation, see lpe-copy_rotate.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" +// this is only to fillrule +#include "livarot/Shape.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum RotateMethod { + RM_NORMAL, + RM_KALEIDOSCOPE, + RM_FUSE, + RM_END +}; + +typedef FillRule FillRuleBool; + +class LPECopyRotate : public Effect, GroupBBoxEffect { +public: + LPECopyRotate(LivePathEffectObject *lpeobject); + ~LPECopyRotate() override; + void doOnApply (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem) override; + void split(Geom::PathVector &path_in, Geom::Path const ÷r); + void resetDefaults(SPItem const* item) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void cloneStyle(SPObject *orig, SPObject *dest); + Geom::PathVector doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule); + void toItem(Geom::Affine transform, size_t i, bool reset); + void cloneD(SPObject *orig, SPObject *dest, Geom::Affine transform, bool reset); + Inkscape::XML::Node * createPathBase(SPObject *elemref); + void resetStyles(); + //virtual void setFusion(Geom::PathVector &path_in, Geom::Path divider, double sizeDivider); +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + +private: + EnumParam method; + PointParam origin; + PointParam starting_point; + ScalarParam starting_angle; + ScalarParam rotation_angle; + ScalarParam num_copies; + ScalarParam gap; + BoolParam copies_to_360; + BoolParam mirror_copies; + BoolParam split_items; + Geom::Point A; + Geom::Point B; + Geom::Point dir; + Geom::Point half_dir; + Geom::Point start_pos; + Geom::Point previous_origin; + Geom::Point previous_start_point; + double dist_angle_handle; + double size_divider; + Geom::Path divider; + double previous_num_copies; + bool reset; + SPObject * container; + LPECopyRotate(const LPECopyRotate&) = delete; + LPECopyRotate& operator=(const LPECopyRotate&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-curvestitch.cpp b/src/live_effects/lpe-curvestitch.cpp new file mode 100644 index 0000000..aa7a39b --- /dev/null +++ b/src/live_effects/lpe-curvestitch.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Curve Stitching implementation, used as an example for a base starting class + * when implementing new LivePathEffects. + * + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include "live_effects/lpe-curvestitch.h" + +#include "object/sp-path.h" + +#include "svg/svg.h" +#include "xml/repr.h" + +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +LPECurveStitch::LPECurveStitch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + strokepath(_("Stitch path:"), _("The path that will be used as stitch."), "strokepath", &wr, this, "M0,0 L1,0"), + nrofpaths(_("N_umber of paths:"), _("The number of paths that will be generated."), "count", &wr, this, 5), + startpoint_edge_variation(_("Sta_rt edge variance:"), _("The amount of random jitter to move the start points of the stitches inside & outside the guide path"), "startpoint_edge_variation", &wr, this, 0), + startpoint_spacing_variation(_("Sta_rt spacing variance:"), _("The amount of random shifting to move the start points of the stitches back & forth along the guide path"), "startpoint_spacing_variation", &wr, this, 0), + endpoint_edge_variation(_("End ed_ge variance:"), _("The amount of randomness that moves the end points of the stitches inside & outside the guide path"), "endpoint_edge_variation", &wr, this, 0), + endpoint_spacing_variation(_("End spa_cing variance:"), _("The amount of random shifting to move the end points of the stitches back & forth along the guide path"), "endpoint_spacing_variation", &wr, this, 0), + prop_scale(_("Scale _width:"), _("Scale the width of the stitch path"), "prop_scale", &wr, this, 1), + scale_y_rel(_("Scale _width relative to length"), _("Scale the width of the stitch path relative to its length"), "scale_y_rel", &wr, this, false) +{ + registerParameter(&nrofpaths); + registerParameter(&startpoint_edge_variation); + registerParameter(&startpoint_spacing_variation); + registerParameter(&endpoint_edge_variation); + registerParameter(&endpoint_spacing_variation); + registerParameter(&strokepath ); + registerParameter(&prop_scale); + registerParameter(&scale_y_rel); + + nrofpaths.param_make_integer(); + nrofpaths.param_set_range(2, Geom::infinity()); + + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + transformed = false; +} + +LPECurveStitch::~LPECurveStitch() += default; + +Geom::PathVector +LPECurveStitch::doEffect_path (Geom::PathVector const & path_in) +{ + if (path_in.size() >= 2) { + startpoint_edge_variation.resetRandomizer(); + endpoint_edge_variation.resetRandomizer(); + startpoint_spacing_variation.resetRandomizer(); + endpoint_spacing_variation.resetRandomizer(); + + D2 > stroke = make_cuts_independent(strokepath.get_pwd2()); + OptInterval bndsStroke = bounds_exact(stroke[0]); + OptInterval bndsStrokeY = bounds_exact(stroke[1]); + if (!bndsStroke && !bndsStrokeY) { + return path_in; + } + gdouble scaling = bndsStroke->max() - bndsStroke->min(); + Point stroke_origin(bndsStroke->min(), (bndsStrokeY->max()+bndsStrokeY->min())/2); + + Geom::PathVector path_out; + + // do this for all permutations (ii,jj) if there are more than 2 paths? realllly cool! + for (unsigned ii = 0 ; ii < path_in.size() - 1; ii++) + for (unsigned jj = ii+1; jj < path_in.size(); jj++) + { + Piecewise > A = arc_length_parametrization(Piecewise >(path_in[ii].toPwSb()),2,.1); + Piecewise > B = arc_length_parametrization(Piecewise >(path_in[jj].toPwSb()),2,.1); + Interval bndsA = A.domain(); + Interval bndsB = B.domain(); + gdouble incrementA = (bndsA.max()-bndsA.min()) / (nrofpaths-1); + gdouble incrementB = (bndsB.max()-bndsB.min()) / (nrofpaths-1); + gdouble tA = bndsA.min(); + gdouble tB = bndsB.min(); + gdouble tAclean = tA; // the tA without spacing_variation + gdouble tBclean = tB; // the tB without spacing_variation + + for (int i = 0; i < nrofpaths; i++) { + Point start = A(tA); + Point end = B(tB); + if (startpoint_edge_variation.get_value() != 0) + start = start + (startpoint_edge_variation - startpoint_edge_variation.get_value()/2) * (end - start); + if (endpoint_edge_variation.get_value() != 0) + end = end + (endpoint_edge_variation - endpoint_edge_variation.get_value()/2)* (end - start); + + if (!Geom::are_near(start,end)) { + gdouble scaling_y = 1.0; + if (scale_y_rel.get_value() || transformed) { + scaling_y = (L2(end-start)/scaling)*prop_scale; + transformed = false; + } else { + scaling_y = prop_scale; + } + + Affine transform; + transform.setXAxis( (end-start) / scaling ); + transform.setYAxis( rot90(unit_vector(end-start)) * scaling_y); + transform.setTranslation( start ); + Piecewise > pwd2_out = (strokepath.get_pwd2()-stroke_origin) * transform; + + // add stuff to one big pw > and then outside the loop convert to path? + // No: this way, the separate result paths are kept separate which might come in handy some time! + Geom::PathVector result = Geom::path_from_piecewise(pwd2_out, LPE_CONVERSION_TOLERANCE); + path_out.push_back(result[0]); + } + gdouble svA = startpoint_spacing_variation - startpoint_spacing_variation.get_value()/2; + gdouble svB = endpoint_spacing_variation - endpoint_spacing_variation.get_value()/2; + tAclean += incrementA; + tBclean += incrementB; + tA = tAclean + incrementA * svA; + tB = tBclean + incrementB * svB; + if (tA > bndsA.max()) + tA = bndsA.max(); + if (tB > bndsB.max()) + tB = bndsB.max(); + } + } + + return path_out; + } else { + return path_in; + } +} + +void +LPECurveStitch::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + if (!SP_IS_PATH(item)) return; + + using namespace Geom; + + // set the stroke path to run horizontally in the middle of the bounding box of the original path + + // calculate bounding box: (isn't there a simpler way?) + Piecewise > pwd2; + Geom::PathVector temppath = sp_svg_read_pathv( item->getRepr()->attribute("inkscape:original-d")); + for (const auto & i : temppath) { + pwd2.concat( i.toPwSb() ); + } + D2 > d2pw = make_cuts_independent(pwd2); + OptInterval bndsX = bounds_exact(d2pw[0]); + OptInterval bndsY = bounds_exact(d2pw[1]); + if (bndsX && bndsY) { + Point start(bndsX->min(), (bndsY->max()+bndsY->min())/2); + Point end(bndsX->max(), (bndsY->max()+bndsY->min())/2); + if ( !Geom::are_near(start,end) ) { + Geom::Path path; + path.start( start ); + path.appendNew( end ); + strokepath.set_new_value( path.toPwSb(), true ); + } else { + // bounding box is too small to make decent path. set to default default. :-) + strokepath.param_set_and_write_default(); + } + } else { + // bounding box is non-existent. set to default default. :-) + strokepath.param_set_and_write_default(); + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-curvestitch.h b/src/live_effects/lpe-curvestitch.h new file mode 100644 index 0000000..7faef72 --- /dev/null +++ b/src/live_effects/lpe-curvestitch.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CURVESTITCH_H +#define INKSCAPE_LPE_CURVESTITCH_H + +/** \file + * Implementation of the curve stitch effect, see lpe-curvestitch.cpp + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECurveStitch : public Effect { +public: + LPECurveStitch(LivePathEffectObject *lpeobject); + ~LPECurveStitch() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + PathParam strokepath; + ScalarParam nrofpaths; + RandomParam startpoint_edge_variation; + RandomParam startpoint_spacing_variation; + RandomParam endpoint_edge_variation; + RandomParam endpoint_spacing_variation; + ScalarParam prop_scale; + BoolParam scale_y_rel; + bool transformed; + + LPECurveStitch(const LPECurveStitch&) = delete; + LPECurveStitch& operator=(const LPECurveStitch&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-dashed-stroke.cpp b/src/live_effects/lpe-dashed-stroke.cpp new file mode 100644 index 0000000..13d1e94 --- /dev/null +++ b/src/live_effects/lpe-dashed-stroke.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-dashed-stroke.h" +#include "2geom/path.h" +#include "2geom/pathvector.h" +#include "helper/geom.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEDashedStroke::LPEDashedStroke(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , numberdashes(_("Number of dashes"), _("Number of dashes"), "numberdashes", &wr, this, 3) + , holefactor(_("Hole factor"), _("Hole factor"), "holefactor", &wr, this, 0.0) + , splitsegments(_("Use segments"), _("Use segments"), "splitsegments", &wr, this, true) + , halfextreme(_("Half start/end"), _("Start and end of each segment has half size"), "halfextreme", &wr, this, true) + , unifysegment(_("Unify dashes"), _("Approximately unify the dashes length using the minimal length segment"), + "unifysegment", &wr, this, true) + , message(_("Info Box"), _("Important messages"), "message", &wr, this, + _("Add \"Fill Between Many LPE\" to add fill.")) +{ + registerParameter(&numberdashes); + registerParameter(&holefactor); + registerParameter(&splitsegments); + registerParameter(&halfextreme); + registerParameter(&unifysegment); + registerParameter(&message); + numberdashes.param_set_range(2, 999999999); + numberdashes.param_set_increments(1, 1); + numberdashes.param_set_digits(0); + holefactor.param_set_range(-0.99999, 0.99999); + holefactor.param_set_increments(0.01, 0.01); + holefactor.param_set_digits(5); + message.param_set_min_height(30); +} + +LPEDashedStroke::~LPEDashedStroke() = default; + +void LPEDashedStroke::doBeforeEffect(SPLPEItem const *lpeitem) {} + +///Calculate the time in curve_in with a real time of A +//TODO: find a better place to it +double LPEDashedStroke::timeAtLength(double const A, Geom::Path const &segment) +{ + if ( A == 0 || segment[0].isDegenerate()) { + return 0; + } + double t = 1; + t = timeAtLength(A, segment.toPwSb()); + return t; +} + +///Calculate the time in curve_in with a real time of A +//TODO: find a better place to it +double LPEDashedStroke::timeAtLength(double const A, Geom::Piecewise> pwd2) +{ + if ( A == 0 || pwd2.size() == 0) { + return 0; + } + + double t = pwd2.size(); + std::vector t_roots = roots(Geom::arcLengthSb(pwd2) - A); + if (!t_roots.empty()) { + t = t_roots[0]; + } + return t; +} + +Geom::PathVector LPEDashedStroke::doEffect_path(Geom::PathVector const &path_in) +{ + Geom::PathVector const pv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector result; + for (const auto & path_it : pv) { + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + size_t numberdashes_fixed = numberdashes; + if(!splitsegments) { + numberdashes_fixed++; + } + size_t numberholes = numberdashes_fixed - 1; + size_t ammount = numberdashes_fixed + numberholes; + if (halfextreme) { + ammount--; + } + double base = 1/(double)ammount; + double globaldash = base * numberdashes_fixed * (1 + holefactor); + if (halfextreme) { + globaldash = base * (numberdashes_fixed - 1) * (1 + holefactor); + } + double globalhole = 1-globaldash; + double dashpercent = globaldash/numberdashes_fixed; + if (halfextreme) { + dashpercent = globaldash/(numberdashes_fixed -1); + } + double holepercent = globalhole/numberholes; + double dashsize_fixed = 0; + double holesize_fixed = 0; + Geom::Piecewise > pwd2 = path_it.toPwSb(); + double length_pwd2 = length (pwd2); + double minlength = length_pwd2; + if(unifysegment) { + while (curve_it1 != curve_endit) { + double length_segment = (*curve_it1).length(); + if (length_segment < minlength) { + minlength = length_segment; + dashsize_fixed = (*curve_it1).length() * dashpercent; + holesize_fixed = (*curve_it1).length() * holepercent; + } + ++curve_it1; + ++curve_it2; + } + curve_it1 = path_it.begin(); + curve_it2 = ++(path_it.begin()); + curve_endit = path_it.end_default(); + } + size_t p_index = 0; + size_t start_index = result.size(); + if(splitsegments) { + while (curve_it1 != curve_endit) { + Geom::Path segment = path_it.portion(p_index, p_index + 1); + if(unifysegment) { + double integral; + double fractional = modf((*curve_it1).length()/(dashsize_fixed + holesize_fixed), &integral); + numberdashes_fixed = (size_t)integral + 1; + numberholes = numberdashes_fixed - 1; + ammount = numberdashes_fixed + numberholes; + if (halfextreme) { + ammount--; + } + base = 1/(double)ammount; + globaldash = base * numberdashes_fixed * (1 + holefactor); + if (halfextreme) { + globaldash = base * (numberdashes_fixed - 1) * (1 + holefactor); + } + globalhole = 1-globaldash; + dashpercent = globaldash/numberdashes_fixed; + if (halfextreme) { + dashpercent = globaldash/(numberdashes_fixed -1); + } + holepercent = globalhole/numberholes; + } + double dashsize = (*curve_it1).length() * dashpercent; + double holesize = (*curve_it1).length() * holepercent; + if ((*curve_it1).isLineSegment()) { + if (result.size() && Geom::are_near(segment.initialPoint(),result[result.size()-1].finalPoint())) { + result[result.size()-1].setFinal(segment.initialPoint()); + if (halfextreme) { + result[result.size()-1].append(segment.portion(0.0, dashpercent/2.0)); + } else { + result[result.size()-1].append(segment.portion(0.0, dashpercent)); + } + } else { + if (halfextreme) { + result.push_back(segment.portion(0.0, dashpercent/2.0)); + } else { + result.push_back(segment.portion(0.0, dashpercent)); + } + } + + double start = dashpercent + holepercent; + if (halfextreme) { + start = (dashpercent/2.0) + holepercent; + } + while (start < 1) { + if (start + dashpercent > 1) { + result.push_back(segment.portion(start, 1)); + } else { + result.push_back(segment.portion(start, start + dashpercent)); + } + start += dashpercent + holepercent; + } + } else if (!(*curve_it1).isLineSegment()) { + double start = 0.0; + double end = 0.0; + if (halfextreme) { + end = timeAtLength(dashsize/2.0,segment); + } else { + end = timeAtLength(dashsize,segment); + } + if (result.size() && Geom::are_near(segment.initialPoint(),result[result.size()-1].finalPoint())) { + result[result.size()-1].setFinal(segment.initialPoint()); + result[result.size()-1].append(segment.portion(start, end)); + } else { + result.push_back(segment.portion(start, end)); + } + double startsize = dashsize + holesize; + if (halfextreme) { + startsize = (dashsize/2.0) + holesize; + } + double endsize = startsize + dashsize; + start = timeAtLength(startsize,segment); + end = timeAtLength(endsize,segment); + while (start < 1 && start > 0) { + result.push_back(segment.portion(start, end)); + startsize = endsize + holesize; + endsize = startsize + dashsize; + start = timeAtLength(startsize,segment); + end = timeAtLength(endsize,segment); + } + } + if (curve_it2 == curve_endit) { + if (path_it.closed()) { + Geom::Path end = result[result.size()-1]; + end.setFinal(result[start_index].initialPoint()); + end.append(result[start_index]); + result[start_index] = end; + } + } + p_index ++; + ++curve_it1; + ++curve_it2; + } + } else { + double start = 0.0; + double end = 0.0; + double dashsize = length_pwd2 * dashpercent; + double holesize = length_pwd2 * holepercent; + if (halfextreme) { + end = timeAtLength(dashsize/2.0,pwd2); + } else { + end = timeAtLength(dashsize,pwd2); + } + result.push_back(path_it.portion(start, end)); + double startsize = dashsize + holesize; + if (halfextreme) { + startsize = (dashsize/2.0) + holesize; + } + double endsize = startsize + dashsize; + start = timeAtLength(startsize,pwd2); + end = timeAtLength(endsize,pwd2); + while (start < path_it.size() && start > 0) { + result.push_back(path_it.portion(start, end)); + startsize = endsize + holesize; + endsize = startsize + dashsize; + start = timeAtLength(startsize,pwd2); + end = timeAtLength(endsize,pwd2); + } + if (path_it.closed()) { + Geom::Path end = result[result.size()-1]; + end.setFinal(result[start_index].initialPoint()); + end.append(result[start_index]); + result[start_index] = end; + } + } + } + return result; +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-dashed-stroke.h b/src/live_effects/lpe-dashed-stroke.h new file mode 100644 index 0000000..d001eaf --- /dev/null +++ b/src/live_effects/lpe-dashed-stroke.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DASHED_STROKE_H +#define INKSCAPE_LPE_DASHED_STROKE_H + +/* + * Inkscape::LPEDashedStroke + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/message.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEDashedStroke : public Effect { + public: + LPEDashedStroke(LivePathEffectObject *lpeobject); + ~LPEDashedStroke() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + double timeAtLength(double const A, Geom::Path const &segment); + double timeAtLength(double const A, Geom::Piecewise > pwd2); +private: + ScalarParam numberdashes; + ScalarParam holefactor; + BoolParam splitsegments; + BoolParam halfextreme; + BoolParam unifysegment; + MessageParam message; +}; + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-dynastroke.cpp b/src/live_effects/lpe-dynastroke.cpp new file mode 100644 index 0000000..52b09fe --- /dev/null +++ b/src/live_effects/lpe-dynastroke.cpp @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ +/* + * Authors: + * JF Barraud + * + * Copyright (C) JF Barraud 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-dynastroke.h" +#include "display/curve.h" +//# include + +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-math.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { +//TODO: growfor/fadefor can be expressed in unit of width. +//TODO: make round/sharp end choices independent for start and end. +//TODO: define more styles like in calligtool. +//TODO: allow fancy ends. + +static const Util::EnumData DynastrokeMethodData[DSM_END] = { + {DSM_ELLIPTIC_PEN, N_("Elliptic Pen"), "elliptic_pen"}, + {DSM_THICKTHIN_FAST, N_("Thick-Thin strokes (fast)"), "thickthin_fast"}, + {DSM_THICKTHIN_SLOW, N_("Thick-Thin strokes (slow)"), "thickthin_slow"} +}; +static const Util::EnumDataConverter DSMethodConverter(DynastrokeMethodData, DSM_END); + +static const Util::EnumData DynastrokeCappingTypeData[DSCT_END] = { + {DSCT_SHARP, N_("Sharp"), "sharp"}, + {DSCT_ROUND, N_("Round"), "round"}, +}; +static const Util::EnumDataConverter DSCTConverter(DynastrokeCappingTypeData, DSCT_END); + +LPEDynastroke::LPEDynastroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + method(_("Method:"), _("Choose pen type"), "method", DSMethodConverter, &wr, this, DSM_THICKTHIN_FAST), + width(_("Pen width:"), _("Maximal stroke width"), "width", &wr, this, 25), + roundness(_("Pen roundness:"), _("Min/Max width ratio"), "roundness", &wr, this, .2), + angle(_("Angle:"), _("direction of thickest strokes (opposite = thinnest)"), "angle", &wr, this, 45), +// modulo_pi(_("modulo pi"), _("Give forward and backward moves in one direction the same thickness "), "modulo_pi", &wr, this, false), + start_cap(_("Start:"), _("Choose start capping type"), "start_cap", DSCTConverter, &wr, this, DSCT_SHARP), + end_cap(_("End:"), _("Choose end capping type"), "end_cap", DSCTConverter, &wr, this, DSCT_SHARP), + growfor(_("Grow for:"), _("Make the stroke thinner near it's start"), "growfor", &wr, this, 100), + fadefor(_("Fade for:"), _("Make the stroke thinner near it's end"), "fadefor", &wr, this, 100), + round_ends(_("Round ends"), _("Strokes end with a round end"), "round_ends", &wr, this, false), + capping(_("Capping:"), _("left capping"), "capping", &wr, this, "M 100,5 C 50,5 0,0 0,0 0,0 50,-5 100,-5") +{ + + registerParameter(&method); + registerParameter(&width); + registerParameter(&roundness); + registerParameter(&angle); + //registerParameter(&modulo_pi) ); + registerParameter(&start_cap); + registerParameter(&growfor); + registerParameter(&end_cap); + registerParameter(&fadefor); + registerParameter(&round_ends); + registerParameter(&capping); + + width.param_set_range(0, Geom::infinity()); + roundness.param_set_range(0.01, 1); + angle.param_set_range(-360, 360); + growfor.param_set_range(0, Geom::infinity()); + fadefor.param_set_range(0, Geom::infinity()); + + show_orig_path = true; +} + +LPEDynastroke::~LPEDynastroke() += default; + +Geom::Piecewise > +LPEDynastroke::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + +// std::cout<<"do effect: debut\n"; + + Piecewise > output; + Piecewise > m = pwd2_in; + Piecewise > v = derivative(m);; + Piecewise > n = unitVector(v); + n = rot90(n); + Piecewise > n1,n2; + +// for (unsigned i=0; i k = curvature(m); + OptInterval mag = bounds_exact(k); + //TODO test if mag is non empty... + k = (k-mag->min())*width/mag->extent() + (roundness*width); + Piecewise > left = m + k*n; + Piecewise > right = m - k*n; + right = compose(right,Linear(right.cuts.back(),right.cuts.front())); + D2 line; + line[X] = Linear(left.lastValue()[X],right.firstValue()[X]); + line[Y] = Linear(left.lastValue()[Y],right.firstValue()[Y]); + output = left; + output.concat(Piecewise >(line)); + output.concat(right); + line[X] = Linear(right.lastValue()[X],left.firstValue()[X]); + line[Y] = Linear(right.lastValue()[Y],left.firstValue()[Y]); + output.concat(Piecewise >(line)); + return output; +#else + + double angle_rad = angle*M_PI/180.;//TODO: revert orientation?... + Piecewise w; + + // std::vector corners = find_corners(m); + + DynastrokeMethod stroke_method = method.get_value(); + if (roundness==1.) { +// std::cout<<"round pen.\n"; + n1 = n*double(width); + n2 =-n1; + }else{ + switch(stroke_method) { + case DSM_ELLIPTIC_PEN:{ +// std::cout<<"ellptic pen\n"; + //FIXME: roundness=0??? + double c = cos(angle_rad), s = sin(angle_rad); + Affine rot,slant; + rot = Affine(c, -s, s, c, 0, 0 ); + slant = Affine(double(width)*roundness, 0, 0, double(width), 0, 0 ); + Piecewise > nn = unitVector(v * ( rot * slant ) ); + slant = Affine( 0,-roundness, 1, 0, 0, 0 ); + rot = Affine(-s, -c, c, -s, 0, 0 ); + nn = nn * (slant * rot ); + + n1 = nn*double(width); + n2 =-n1; + break; + } + case DSM_THICKTHIN_FAST:{ +// std::cout<<"fast thick thin pen\n"; + D2 > n_xy = make_cuts_independent(n); + w = n_xy[X]*sin(angle_rad) - n_xy[Y]*cos(angle_rad); + w = w * ((1 - roundness)*width/2.) + ((1 + roundness)*width/2.); + n1 = w*n; + n2 = -n1; + break; + } + case DSM_THICKTHIN_SLOW:{ +// std::cout<<"slow thick thin pen\n"; + D2 > n_xy = make_cuts_independent(n); + w = n_xy[X]*cos(angle_rad)+ n_xy[Y]*sin(angle_rad); + w = w * ((1 - roundness)*width/2.) + ((1 + roundness)*width/2.); + //->Slower and less stable, but more accurate . + // General formula: n1 = w*u with ||u||=1 and u.v = -dw/dt + Piecewise dw = derivative(w); + Piecewise ncomp = sqrt(dot(v,v)-dw*dw,.1,3); + //FIXME: is force continuity useful? compatible with corners? +// std::cout<<"ici\n"; + n1 = -dw*v + ncomp*rot90(v); + n1 = w*force_continuity(unitVector(n1),.1); + n2 = -dw*v - ncomp*rot90(v); + n2 = w*force_continuity(unitVector(n2),.1); +// std::cout<<"ici2\n"; + break; + } + default:{ + n1 = n*double(width); + n2 = n1*(-.5); + break; + } + }//case + }//if/else + + // + //TODO: insert relevant stitch at each corner!! + // + + Piecewise > left, right; + if ( m.segs.front().at0() == m.segs.back().at1()){ + // if closed: +// std::cout<<"closed input.\n"; + left = m + n1;//+ n; + right = m + n2;//- n; + } else { + //if not closed, shape the ends: + //TODO: allow fancy ends... +// std::cout<<"shaping the ends\n"; + double grow_length = growfor;// * width; + double fade_length = fadefor;// * width; + Piecewise s = arcLengthSb(m); + double totlength = s.segs.back().at1(); + + //scale factor for a sharp start + SBasis join = SBasis(2,Linear(0,1)); + join[1] = Linear(1,1); + Piecewise factor_in = Piecewise(join); + factor_in.cuts[1]=grow_length; + if (grow_length < totlength){ + factor_in.concat(Piecewise(Linear(1))); + factor_in.cuts[2]=totlength; + } +// std::cout<<"shaping the ends ici\n"; + //scale factor for a sharp end + join[0] = Linear(1,0); + join[1] = Linear(1,1); + Piecewise factor_out; + if (fade_length < totlength){ + factor_out = Piecewise(Linear(1)); + factor_out.cuts[1] = totlength-fade_length; + factor_out.concat(Piecewise(join)); + factor_out.cuts[2] = totlength; + }else{ + factor_out = Piecewise(join); + factor_out.setDomain(Interval(totlength-fade_length,totlength)); + } +// std::cout<<"shaping the ends ici ici\n"; + + Piecewise factor = factor_in*factor_out; + n1 = compose(factor,s)*n1; + n2 = compose(factor,s)*n2; + + left = m + n1; + right = m + n2; +// std::cout<<"shaping the ends ici ici ici\n"; + + if (start_cap.get_value() == DSCT_ROUND){ +// std::cout<<"shaping round start\n"; + SBasis tau(2,Linear(0)); + tau[1] = Linear(-1,0); + Piecewise hbump; + hbump.concat(Piecewise(tau*grow_length)); + hbump.concat(Piecewise(Linear(0))); + hbump.cuts[0]=0; + hbump.cuts[1]=fmin(grow_length,totlength*grow_length/(grow_length+fade_length)); + hbump.cuts[2]=totlength; + hbump = compose(hbump,s); + + left += - hbump * rot90(n); + right += - hbump * rot90(n); + } + if (end_cap.get_value() == DSCT_ROUND){ +// std::cout<<"shaping round end\n"; + SBasis tau(2,Linear(0)); + tau[1] = Linear(0,1); + Piecewise hbump; + hbump.concat(Piecewise(Linear(0))); + hbump.concat(Piecewise(tau*fade_length)); + hbump.cuts[0]=0; + hbump.cuts[1]=fmax(totlength-fade_length, totlength*grow_length/(grow_length+fade_length)); + hbump.cuts[2]=totlength; + hbump = compose(hbump,s); + + left += - hbump * rot90(n); + right += - hbump * rot90(n); + } + } + + left = force_continuity(left); + right = force_continuity(right); + +// std::cout<<"gathering result: left"; + output = left; +// std::cout<<" + reverse(right)"; + output.concat(reverse(right)); +// std::cout<<". done\n"; + +//----------- + return output; +#endif +} + + +/* ######################## */ + +} //namespace LivePathEffect (setq default-directory "c:/Documents And Settings/jf/Mes Documents/InkscapeSVN") +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-dynastroke.h b/src/live_effects/lpe-dynastroke.h new file mode 100644 index 0000000..ae1c511 --- /dev/null +++ b/src/live_effects/lpe-dynastroke.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DYNASTROKE_H +#define INKSCAPE_LPE_DYNASTROKE_H + +/** \file + * LPE implementation, see lpe-dynastroke.cpp. + */ + +/* + * Authors: + * JFB, but derived from Johan Engelen! + * + * Copyright (C) JF Barraud 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum DynastrokeMethod { + DSM_ELLIPTIC_PEN = 0, + DSM_THICKTHIN_FAST, + DSM_THICKTHIN_SLOW, + DSM_END // This must be last +}; +enum DynastrokeCappingType { + DSCT_SHARP = 0, + DSCT_ROUND, + //DSCT_CUSTOM, + DSCT_END // This must be last +}; + + +class LPEDynastroke : public Effect { +public: + LPEDynastroke(LivePathEffectObject *lpeobject); + ~LPEDynastroke() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + EnumParam method; + ScalarParam width; + ScalarParam roundness; + ScalarParam angle; + //BoolParam modulo_pi; + EnumParam start_cap; + EnumParam end_cap; + ScalarParam growfor; + ScalarParam fadefor; + BoolParam round_ends; + PathParam capping; + + LPEDynastroke(const LPEDynastroke&) = delete; + LPEDynastroke& operator=(const LPEDynastroke&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-ellipse_5pts.cpp b/src/live_effects/lpe-ellipse_5pts.cpp new file mode 100644 index 0000000..29288f8 --- /dev/null +++ b/src/live_effects/lpe-ellipse_5pts.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Ellipse through 5 points" implementation + */ + +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-ellipse_5pts.h" +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/path-sink.h> +#include "inkscape.h" +#include "desktop.h" +#include "message-stack.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEEllipse5Pts::LPEEllipse5Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ + //perceived_path = true; +} + +LPEEllipse5Pts::~LPEEllipse5Pts() += default; + +static double _det3(double (*mat)[3]) +{ + for (int i = 0; i < 2; i++) + { + for (int j = i + 1; j < 3; j++) + { + for (int k = i + 1; k < 3; k++) + { + mat[j][k] = (mat[j][k] * mat[i][i] - mat[j][i] * mat[i][k]); + if (i) mat[j][k] /= mat[i-1][i-1]; + } + } + } + return mat[2][2]; +} +static double _det5(double (*mat)[5]) +{ + for (int i = 0; i < 4; i++) + { + for (int j = i + 1; j < 5; j++) + { + for (int k = i + 1; k < 5; k++) + { + mat[j][k] = (mat[j][k] * mat[i][i] - mat[j][i] * mat[i][k]); + if (i) mat[j][k] /= mat[i-1][i-1]; + } + } + } + return mat[4][4]; +} + +Geom::PathVector +LPEEllipse5Pts::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + if (path_in[0].size() < 4) { + + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Five points required for constructing an ellipse")); + return path_in; + } + // we assume that the path has >= 3 nodes + Geom::Point A = path_in[0].initialPoint(); + Geom::Point B = path_in[0].pointAt(1); + Geom::Point C = path_in[0].pointAt(2); + Geom::Point D = path_in[0].pointAt(3); + Geom::Point E = path_in[0].pointAt(4); + + using namespace Geom; + + double rowmajor_matrix[5][6] = + { + {A.x()*A.x(), A.x()*A.y(), A.y()*A.y(), A.x(), A.y(), 1}, + {B.x()*B.x(), B.x()*B.y(), B.y()*B.y(), B.x(), B.y(), 1}, + {C.x()*C.x(), C.x()*C.y(), C.y()*C.y(), C.x(), C.y(), 1}, + {D.x()*D.x(), D.x()*D.y(), D.y()*D.y(), D.x(), D.y(), 1}, + {E.x()*E.x(), E.x()*E.y(), E.y()*E.y(), E.x(), E.y(), 1} + }; + + double mat_a[5][5] = + { + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_b[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_c[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_d[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_e[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_f[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]} + }; + + double a1 = _det5(mat_a); + double b1 = -_det5(mat_b); + double c1 = _det5(mat_c); + double d1 = -_det5(mat_d); + double e1 = _det5(mat_e); + double f1 = -_det5(mat_f); + + double mat_check[][3] = + { + {a1, b1/2, d1/2}, + {b1/2, c1, e1/2}, + {d1/2, e1/2, f1} + }; + + if (_det3(mat_check) == 0 || a1*c1 - b1*b1/4 <= 0) { + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No ellipse found for specified points")); + return path_in; + } + + Geom::Ellipse el(a1, b1, c1, d1, e1, f1); + + double s, e; + double x0, y0, x1, y1, x2, y2, x3, y3; + double len; + + // figure out if we have a slice, guarding against rounding errors + + Geom::Path p(Geom::Point(cos(0), sin(0))); + + double end = 2 * M_PI; + for (s = 0; s < end; s += M_PI_2) { + e = s + M_PI_2; + if (e > end) + e = end; + len = 4*tan((e - s)/4)/3; + x0 = cos(s); + y0 = sin(s); + x1 = x0 + len * cos(s + M_PI_2); + y1 = y0 + len * sin(s + M_PI_2); + x3 = cos(e); + y3 = sin(e); + x2 = x3 + len * cos(e - M_PI_2); + y2 = y3 + len * sin(e - M_PI_2); + p.appendNew(Geom::Point(x1,y1), Geom::Point(x2,y2), Geom::Point(x3,y3)); + } + + Geom::Affine aff = Geom::Scale(el.rays()) * Geom::Rotate(el.rotationAngle()) * Geom::Translate(el.center()); + + path_out.push_back(p * aff); + + return path_out; +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-ellipse_5pts.h b/src/live_effects/lpe-ellipse_5pts.h new file mode 100644 index 0000000..843a9c4 --- /dev/null +++ b/src/live_effects/lpe-ellipse_5pts.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ELLIPSE_5PTS_H +#define INKSCAPE_LPE_ELLIPSE_5PTS_H + +/** \file + * LPE "Ellipse through 5 points" implementation + */ + +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEEllipse5Pts : public Effect { +public: + LPEEllipse5Pts(LivePathEffectObject *lpeobject); + ~LPEEllipse5Pts() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPEEllipse5Pts(const LPEEllipse5Pts&) = delete; + LPEEllipse5Pts& operator=(const LPEEllipse5Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-embrodery-stitch-ordering.cpp b/src/live_effects/lpe-embrodery-stitch-ordering.cpp new file mode 100644 index 0000000..ae22edf --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch-ordering.cpp @@ -0,0 +1,1141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Sub-path Ordering functions for embroidery stitch LPE (Implementation) + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +#include + +namespace Inkscape { +namespace LivePathEffect { +namespace LPEEmbroderyStitchOrdering { + +using namespace Geom; + +// ==================== Debug Trace Macros ==================== + +// ATTENTION: both level and area macros must be enabled for tracing + +// These macros are for enabling certain levels of tracing +#define DebugTrace1(list) // g_warning list +#define DebugTrace2(list) // g_warning list + +// These macros are for enabling certain areas of tracing +#define DebugTraceGrouping(list) // list +#define DebugTraceTSP(list) // list + +// Combinations of above +#define DebugTrace1TSP(list) DebugTraceTSP( DebugTrace1(list) ) +#define DebugTrace2TSP(list) DebugTraceTSP( DebugTrace2(list) ) + +// ==================== Template Utilities ==================== + +// Delete all objects pointed to by a vector and clear the vector + +template< typename T > void delete_and_clear(std::vector &vector) +{ + for (typename std::vector::iterator it = vector.begin(); it != vector.end(); ++it) { + delete *it; + } + vector.clear(); +} + +// Assert that there are no duplicates in a vector + +template< typename T > void assert_unique(std::vector &vector) +{ + typename std::vector copy = vector; + std::sort(copy.begin(), copy.end()); + assert(std::unique(copy.begin(), copy.end()) == copy.end()); +} + +// remove element(s) by value + +template< typename T > void remove_by_value(std::vector *vector, const T &value) +{ + vector->erase(std::remove(vector->begin(), vector->end(), value), vector->end()); +} + +// fill a vector with increasing elements (similar to C++11 iota) +// iota is included in some C++ libraries, not in other (it is always included for C++11) +// To avoid issues, use our own name (not iota) + +template +void fill_increasing(OutputIterator begin, OutputIterator end, Counter counter) +{ + while (begin != end) { + *begin++ = counter++; + } +} + +// check if an iteratable sequence contains an element + +template +bool contains(InputIterator begin, InputIterator end, const Element &elem) +{ + while (begin != end) { + if (*begin == elem) { + return true; + } + ++begin; + } + return false; +} + +// Check if a vector contains an element + +template +bool contains(std::vector const &vector, const Element &elem) +{ + return contains(vector.begin(), vector.end(), elem); +} + +// ==================== Multi-dimensional iterator functions ==================== + +// Below are 3 simple template functions to do triangle/pyramid iteration (without diagonal). +// Here is a sample of iterating over 5 elements in 3 dimensions: +// +// 0 1 2 +// 0 1 3 +// 0 1 4 +// 0 2 3 +// 0 2 4 +// 1 2 4 +// 1 3 4 +// 2 3 4 +// end end end +// +// If the number of elements is less then the number of dimensions, the number of dimensions is reduced automatically. +// +// I thought about creating an iterator class for this, but it doesn't match that well, so I used functions on iterator vectors. + +// Initialize a vector of iterators + +template +void triangleit_begin(std::vector &iterators, Iterator const &begin, Iterator const &end, size_t n) +{ + iterators.clear(); + // limit number of dimensions to number of elements + size_t n1 = end - begin; + n = std::min(n, n1); + if (n) { + iterators.push_back(begin); + for (int i = 1; i < n; i++) { + iterators.push_back(iterators.back() + 1); + } + } +} + +// Increment a vector of iterators + +template +void triangleit_incr(std::vector &iterators, Iterator const &end) +{ + size_t n = iterators.size(); + + for (int i = 0; i < n; i++) { + iterators[n - 1 - i]++; + // Each dimension ends at end-i, so that there are elements left for the i higher dimensions + if (iterators[n - 1 - i] != end - i) { + // Assign increasing numbers to the higher dimension + for (int j = n - i; j < n; j++) { + iterators[j] = iterators[j - 1] + 1; + } + return; + } + } +} + +// Check if a vector of iterators is at the end + +template +bool triangleit_test(std::vector &iterators, Iterator const &end) +{ + if (iterators.empty()) { + return false; + } else { + return iterators.back() != end; + } +} + +// ==================== Trivial Ordering Functions ==================== + +// Sub-path reordering: do nothing - keep original order + +void OrderingOriginal(std::vector &infos) +{ +} + +// Sub-path reordering: reverse every other sub path + +void OrderingZigZag(std::vector &infos, bool revfirst) +{ + for (auto & info : infos) { + info.reverse = (info.index & 1) == (revfirst ? 0 : 1); + } +} + +// Sub-path reordering: continue with the neartest start or end point of yet unused sub paths + +void OrderingClosest(std::vector &infos, bool revfirst) +{ + std::vector result; + result.reserve(infos.size()); + + result.push_back(infos[0]); + result.back().reverse = revfirst; + Point p = result.back().GetEndRev(); + + infos[0].used = true; + + + for (unsigned int iRnd = 1; iRnd < infos.size(); iRnd++) { + // find closest point to p + unsigned iBest = 0; + bool revBest = false; + Coord distBest = infinity(); + + for (std::vector::iterator it = infos.begin(); it != infos.end(); ++it) { + it->index = it - infos.begin(); + it->reverse = (it->index & 1) != 0; + + if (!it->used) { + Coord dist = distance(p, it->GetBegOrig()); + if (dist < distBest) { + distBest = dist; + iBest = it - infos.begin(); + revBest = false; + } + + dist = distance(p, it->GetEndOrig()); + if (dist < distBest) { + distBest = dist; + iBest = it - infos.begin(); + revBest = true; + } + } + } + + result.push_back(infos[iBest]); + result.back().reverse = revBest; + p = result.back().GetEndRev(); + infos[iBest].used = true; + } + + infos = result; +} + +// ==================== Traveling Salesman k-opt Ordering Function and Utilities ==================== + +// A Few notes on this: +// - This is a relatively simple Lin-type k-opt algorithm, but the grouping optimizations done make it already quite complex. +// - The main Ordering Function is OrderingAdvanced +// - Lines which start at the end of another line are connected and treated as one (struct OrderingInfoEx) +// - Groups of zig-zag OrderingInfoEx are grouped (struct OrderingGroup) if both ends of the segment mutually agree with a next neighbor. +// These groups are treated as a unit in the TSP algorithm. +// The only option is to reverse the first segment, so that a group has 4 end points, 2 of which are used externally. +// - Run a k-opt (k=2..5) Lin like Traveling Salesman Problem algorithm on the groups as a unit and the remaining edges. +// See https://en.wikipedia.org/wiki/Travelling_salesman_problem#Iterative_improvement +// The algorithm uses a greedy nearest neighbor as start configuration and does not use repeated random starts. +// - The algorithm searches an open tour (rather than a closed one), so the longest segment in the closed path is ignored. +// - TODO: it might be faster to use k=3 with a few random starting patterns instead of k=5 +// - TODO: it is surely wiser to implement e.g. Lin-Kenrighan TSP, but the simple k-opt works ok. +// - TODO(EASY): add a jump distance, above which threads are removed and make the length of this jump distance constant and large, +// so that mostly the number of jumps is optimized + +// Find 2 nearest points to given point + +void OrderingPoint::FindNearest2(const std::vector &infos) +{ + // This implementation is not terribly elegant (unSTLish). + // But for the first 2 elements using e.g. partial_sort is not simpler. + + Coord dist0 = infinity(); + Coord dist1 = infinity(); + nearest[0] = nullptr; + nearest[1] = nullptr; + + for (auto info : infos) { + Coord dist = distance(point, info->beg.point); + if (dist < dist1) { + if (&info->beg != this && &info->end != this) { + if (dist < dist0) { + nearest[1] = nearest[0]; + nearest[0] = &info->beg; + dist1 = dist0; + dist0 = dist; + } else { + nearest[1] = &info->beg; + dist1 = dist; + } + } + } + + dist = distance(point, info->end.point); + if (dist < dist1) { + if (&info->beg != this && &info->end != this) { + if (dist < dist0) { + nearest[1] = nearest[0]; + nearest[0] = &info->end; + dist1 = dist0; + dist0 = dist; + } else { + nearest[1] = &info->end; + dist1 = dist; + } + } + } + } +} + +// Check if "this" is among the nearest of its nearest + +void OrderingPoint::EnforceMutual() +{ + if (nearest[0] && !(this == nearest[0]->nearest[0] || this == nearest[0]->nearest[1])) { + nearest[0] = nullptr; + } + + if (nearest[1] && !(this == nearest[1]->nearest[0] || this == nearest[1]->nearest[1])) { + nearest[1] = nullptr; + } + + if (nearest[1] && !nearest[0]) { + nearest[0] = nearest[1]; + nearest[1] = nullptr; + } +} + +// Check if the subpath indices of this and other are the same, otherwise zero both nearest + +void OrderingPoint::EnforceSymmetric(const OrderingPoint &other) +{ + if (nearest[0] && !( + (other.nearest[0] && nearest[0]->infoex == other.nearest[0]->infoex) || + (other.nearest[1] && nearest[0]->infoex == other.nearest[1]->infoex) + )) { + nearest[0] = nullptr; + } + + if (nearest[1] && !( + (other.nearest[0] && nearest[1]->infoex == other.nearest[0]->infoex) || + (other.nearest[1] && nearest[1]->infoex == other.nearest[1]->infoex) + )) { + nearest[1] = nullptr; + } + + if (nearest[1] && !nearest[0]) { + nearest[0] = nearest[1]; + nearest[1] = nullptr; + } +} + +void OrderingPoint::Dump() +{ + // COMENTED TO SUPRESS WARNING UNUSED AUTOR TAKE IT UNCOMENTED + // Coord dist0 = nearest[0] ? distance(point, nearest[0]->point) : -1.0; + // Coord dist1 = nearest[1] ? distance(point, nearest[1]->point) : -1.0; + // int idx0 = nearest[0] ? nearest[0]->infoex->idx : -1; + // int idx1 = nearest[1] ? nearest[1]->infoex->idx : -1; + DebugTrace2(("I=%d X=%.5lf Y=%.5lf d1=%.3lf d2=%.3lf i1=%d i2=%d", infoex->idx, point.x(), 297.0 - point.y(), dist0, dist1, idx0, idx1)); +} + + +// If this element can be grouped (has neighbours) but is not yet grouped, create a new group + +void OrderingInfoEx::MakeGroup(std::vector &infos, std::vector *groups) +{ + if (grouped || !beg.HasNearest() || !end.HasNearest()) { + return; + } + + groups->push_back(new OrderingGroup(groups->size())); + + // Add neighbors recursively + AddToGroup(infos, groups->back()); +} + +// Add this and all connected elements to the group + +void OrderingInfoEx::AddToGroup(std::vector &infos, OrderingGroup *group) +{ + if (grouped || !beg.HasNearest() || !end.HasNearest()) { + return; + } + + group->items.push_back(this); + grouped = true; + // Note: beg and end neighbors have been checked to be symmetric + if (beg.nearest[0]) { + beg.nearest[0]->infoex->AddToGroup(infos, group); + } + if (beg.nearest[1]) { + beg.nearest[1]->infoex->AddToGroup(infos, group); + } + if (end.nearest[0]) { + end.nearest[0]->infoex->AddToGroup(infos, group); + } + if (end.nearest[1]) { + end.nearest[1]->infoex->AddToGroup(infos, group); + } +} + +// Constructor + +OrderingGroupNeighbor::OrderingGroupNeighbor(OrderingGroupPoint *me, OrderingGroupPoint *other) : + point(other), + distance(Geom::distance(me->point, other->point)) +{ +} + +// Comparison function for sorting by distance + +bool OrderingGroupNeighbor::Compare(const OrderingGroupNeighbor &a, const OrderingGroupNeighbor &b) +{ + return a.distance < b.distance; +} + +// Find the nearest unused neighbor point + +OrderingGroupNeighbor *OrderingGroupPoint::FindNearestUnused() +{ + for (auto & it : nearest) { + if (!it.point->used) { + DebugTrace1TSP(("Nearest: group %d, size %d, point %d, nghb %d, xFrom %.4lf, yFrom %.4lf, xTo %.4lf, yTo %.4lf, dist %.4lf", + it->point->group->index, it->point->group->items.size(), it->point->indexInGroup, it - nearest.begin(), + point.x(), 297 - point.y(), + it->point->point.x(), 297 - it->point->point.y(), + it->distance)); + return ⁢ + } + } + + // it shouldn't happen that we can't find any point at all + assert(0); + return nullptr; +} + +// Return the other end in the group of the point + +OrderingGroupPoint *OrderingGroupPoint::GetOtherEndGroup() +{ + return group->endpoints[ indexInGroup ^ 1 ]; +} + +// Return the alternate point (if one exists), 0 otherwise + +OrderingGroupPoint *OrderingGroupPoint::GetAltPointGroup() +{ + if (group->nEndPoints < 4) { + return nullptr; + } + + OrderingGroupPoint *alt = group->endpoints[ indexInGroup ^ 2 ]; + return alt->used ? nullptr : alt; +} + + +// Sets the rev flags in the group assuming that the group starts with this point + +void OrderingGroupPoint::SetRevInGroup() +{ + // If this is not a front point, the item list needs to be reversed + group->revItemList = !front; + + // If this is not a begin point, the items need to be reversed + group->revItems = !begin; +} + +// Mark an end point as used and also mark the two other alternating points as used +// Returns the used point + +void OrderingGroupPoint::UsePoint() +{ + group->UsePoint(indexInGroup); +} + +// Mark an end point as unused and possibly also mark the two other alternating points as unused +// Returns the used point + +void OrderingGroupPoint::UnusePoint() +{ + group->UnusePoint(indexInGroup); +} + +// Return the other end in the connection +OrderingGroupPoint *OrderingGroupPoint::GetOtherEndConnection() +{ + assert(connection); + assert(connection->points[ indexInConnection ] == this); + assert(connection->points[ indexInConnection ^ 1 ]); + + return connection->points[ indexInConnection ^ 1 ]; +} + + +// Set the end points of a group from the items + +void OrderingGroup::SetEndpoints() +{ + assert(items.size() >= 1); + + if (items.size() == 1) { + // A simple line: + // + // b0-front--e1 + + nEndPoints = 2; + endpoints[0] = new OrderingGroupPoint(items.front()->beg.point, this, 0, true, true); + endpoints[1] = new OrderingGroupPoint(items.front()->end.point, this, 1, false, true); + } else { + // If the number of elements is even, the group is + // either from items.front().beg to items.back().beg + // or from items.front().end to items.back().end: + // Below: b=beg, e=end, numbers are end point indices + // + // b0-front--e b0-front--e2 + // | | + // b---------e b---------e + // | | + // b---------e b---------e + // | | + // b1-back---e b1-back---e3 + // + // + // if the number of elements is odd, it is crossed: + // + // b0-front--e b--front--e2 + // | | + // b---------e b---------e + // | | + // b--back---e1 b3-back---e + // + // TODO: this is not true with the following kind of pattern + // + // b--front--e + // b---------e + // b--------e + // b--back--e + // + // Here only one connection is possible, from front.end to back.beg + // + // TODO: also this is not true if segment direction is alternating + // + // TOTO: => Just see where you end up from front().begin and front().end + // + // the numbering is such that either end points 0 and 1 are used or 2 and 3. + int cross = items.size() & 1 ? 2 : 0; + nEndPoints = 4; + + endpoints[0 ] = new OrderingGroupPoint(items.front()->beg.point, this, 0, true, true); + endpoints[1 ^ cross] = new OrderingGroupPoint(items.back() ->beg.point, this, 1 ^ cross, true, false); + endpoints[2 ] = new OrderingGroupPoint(items.front()->end.point, this, 2, false, true); + endpoints[3 ^ cross] = new OrderingGroupPoint(items.back() ->end.point, this, 3 ^ cross, false, false); + } +} + +// Add all points from another group as neighbors + +void OrderingGroup::AddNeighbors(OrderingGroup *nghb) +{ + for (int iThis = 0; iThis < nEndPoints; iThis++) { + for (int iNghb = 0; iNghb < nghb->nEndPoints; iNghb++) { + endpoints[iThis]->nearest.emplace_back(endpoints[iThis], nghb->endpoints[iNghb]); + } + } +} + +// Mark an end point as used and also mark the two other alternating points as used +// Returns the used point + +OrderingGroupPoint *OrderingGroup::UsePoint(int index) +{ + assert(index < nEndPoints); + assert(!endpoints[index]->used); + endpoints[index]->used = true; + if (nEndPoints == 4) { + int offs = index < 2 ? 2 : 0; + endpoints[0 + offs]->used = true; + endpoints[1 + offs]->used = true; + } + + return endpoints[index]; +} + +// Mark an end point as unused and possibly also mark the two other alternating points as unused +// Returns the used point + +void OrderingGroup::UnusePoint(int index) +{ + assert(index < nEndPoints); + assert(endpoints[index]->used); + endpoints[index]->used = false; + + if (nEndPoints == 4 && !endpoints[index ^ 1]->used) { + int offs = index < 2 ? 2 : 0; + endpoints[0 + offs]->used = false; + endpoints[1 + offs]->used = false; + } +} + +// Add an end point +void OrderingSegment::AddPoint(OrderingGroupPoint *point) +{ + assert(point); + assert(nEndPoints < 4); + endpoints[ nEndPoints++ ] = point; + + // If both ends of a group are added and the group has 4 points, add the other two as well + if (nEndPoints == 2 && endpoints[0]->group == endpoints[1]->group) { + OrderingGroup *group = endpoints[0]->group; + if (group->nEndPoints == 4) { + for (int i = 0; i < 4; i++) { + endpoints[i] = group->endpoints[i]; + } + nEndPoints = 4; + } + } +} + +// Get begin point (taking swap and end bit into account +OrderingGroupPoint *OrderingSegment::GetBeginPoint(unsigned int iSwap, unsigned int iEnd) +{ + int iPoint = ((iEnd >> endbit) & 1) + (((iSwap >> swapbit) & 1) << 1); + assert(iPoint < nEndPoints); + return endpoints[iPoint]; +} + +// Get end point (taking swap and end bit into account +OrderingGroupPoint *OrderingSegment::GetEndPoint(unsigned int iSwap, unsigned int iEnd) +{ + int iPoint = (((iEnd >> endbit) & 1) ^ 1) + (((iSwap >> swapbit) & 1) << 1); + assert(iPoint < nEndPoints); + return endpoints[iPoint]; +} + + +// Find the next unused point in list +std::vector::iterator FindUnusedAndUse(std::vector *unusedPoints, std::vector::iterator const from) +{ + for (std::vector::iterator it = from; it != unusedPoints->end(); ++it) { + if (!(*it)->used) { + (*it)->UsePoint(); + return it; + } + } + return unusedPoints->end(); +} + +// Find the shortest reconnect between the given points + +bool FindShortestReconnect(std::vector &segments, std::vector &connections, std::vector &allconnections, OrderingGroupConnection **longestConnect, Coord *total, Coord olddist) +{ + // Find the longest connection outside of the active set + // The longest segment is then the longest of this longest outside segment and all inside segments + OrderingGroupConnection *longestOutside = nullptr; + + if (contains(connections, *longestConnect)) { + // The longest connection is inside the active set, so we need to search for the longest outside + Coord length = 0.0; + for (auto & allconnection : allconnections) { + if (allconnection->Distance() > length) { + if (!contains(connections, allconnection)) { + longestOutside = allconnection; + length = allconnection->Distance(); + } + } + } + } else { + longestOutside = *longestConnect; + } + + // length of longestConnect outside + Coord longestOutsideLength = longestOutside ? longestOutside->Distance() : 0.0; + + // We measure length without the longest, so subtract the longest length from the old distance + olddist -= (*longestConnect)->Distance(); + + // Assign a swap bit and end bit to each active connection + int nEndBits = 0; + int nSwapBits = 0; + for (auto & segment : segments) { + segment.endbit = nEndBits++; + if (segment.nEndPoints == 4) { + segment.swapbit = nSwapBits++; + } else { + // bit 32 should always be 0 + segment.swapbit = 31; + } + } + + unsigned int swapMask = (1U << nSwapBits) - 1; + unsigned int endMask = (1U << nEndBits) - 1; + + // Create a permutation vector + std::vector permutation(segments.size()); + fill_increasing(permutation.begin(), permutation.end(), 0); + + // best improvement + bool improved = false; + Coord distBest = olddist; + std::vector permutationBest; + unsigned int iSwapBest; + unsigned int iEndBest; + int nTrials = 0; + + // Loop over the permutations + do { + // Loop over the swap bits + unsigned int iSwap = 0; + do { + // Loop over the end bits + unsigned int iEnd = 0; + do { + // Length of all active connections + Coord lengthTotal = 0; + // Length of longest connection (active or inactive) + Coord lengthLongest = longestOutsideLength; + + // Close the loop with the end point of the last segment + OrderingGroupPoint *prevend = segments[permutation.back()].GetEndPoint(iSwap, iEnd); + for (int & it : permutation) { + OrderingGroupPoint *thisbeg = segments[it].GetBeginPoint(iSwap, iEnd); + Coord length = Geom::distance(thisbeg->point, prevend->point); + lengthTotal += length; + if (length > lengthLongest) { + lengthLongest = length; + } + prevend = segments[it].GetEndPoint(iSwap, iEnd); + } + lengthTotal -= lengthLongest; + + // If there is an improvement, remember the best selection + if (lengthTotal + 1e-6 < distBest) { + improved = true; + distBest = lengthTotal; + permutationBest = permutation; + iSwapBest = iSwap; + iEndBest = iEnd; + + // Just debug printing + OrderingGroupPoint *prevend = segments[permutation.back()].GetEndPoint(iSwap, iEnd); + for (int & it : permutation) { + // COMENTED TO SUPRESS WARNING UNUSED AUTOR TAKE IT UNCOMENTED + //OrderingGroupPoint *thisbeg = segments[it].GetBeginPoint(iSwap, iEnd); + DebugTrace2TSP(("IMP 0F=%d %d %.6lf", thisbeg->group->index, thisbeg->indexInGroup, Geom::distance(thisbeg->point, prevend->point))); + DebugTrace2TSP(("IMP 0T=%d %d %.6lf", prevend->group->index, prevend->indexInGroup, Geom::distance(thisbeg->point, prevend->point))); + prevend = segments[it].GetEndPoint(iSwap, iEnd); + } + } + + nTrials++; + + // bit 0 is always 0, because the first segment is kept fixed + iEnd += 2; + } while (iEnd & endMask); + iSwap++; + } while (iSwap & swapMask); + // first segment is kept fixed + } while (std::next_permutation(permutation.begin() + 1, permutation.end())); + + if (improved) { + DebugTrace2TSP(("Improvement %lf->%lf in %d", olddist, distBest, nTrials)); + // change the connections + + for (std::vector::iterator it = connections.begin(); it != connections.end(); ++it) { + DebugTrace2TSP(("WAS 0F=%d %d %.6lf", (*it)->points[0]->group->index, (*it)->points[0]->indexInGroup, (*it)->Distance())); + DebugTrace2TSP(("WAS 0T=%d %d %.6lf", (*it)->points[1]->group->index, (*it)->points[1]->indexInGroup, (*it)->Distance())); + } + DebugTrace2TSP(("OLDDIST %.6lf delta %.6lf", olddist, olddist - (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[0]->group->index, (*longestConnect)->points[0]->indexInGroup, (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[1]->group->index, (*longestConnect)->points[1]->indexInGroup, (*longestConnect)->Distance())); + + int perm = permutationBest.back(); + + for (std::vector::iterator it = connections.begin(); it != connections.end(); ++it) { + (*it)->Connect(1, segments[ perm ].GetEndPoint(iSwapBest, iEndBest)); + perm = permutationBest[ it - connections.begin() ]; + (*it)->Connect(0, segments[ perm ].GetBeginPoint(iSwapBest, iEndBest)); + + } + + for (std::vector::iterator it = connections.begin(); it != connections.end(); ++it) { + DebugTrace2TSP(("IS 0F=%d %d %.6lf", (*it)->points[0]->group->index, (*it)->points[0]->indexInGroup, (*it)->Distance())); + DebugTrace2TSP(("IS 0T=%d %d %.6lf", (*it)->points[1]->group->index, (*it)->points[1]->indexInGroup, (*it)->Distance())); + } + + (*longestConnect) = longestOutside; + for (auto & connection : connections) { + if (connection->Distance() > (*longestConnect)->Distance()) { + *longestConnect = connection; + } + } + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[0]->group->index, (*longestConnect)->points[0]->indexInGroup, (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[1]->group->index, (*longestConnect)->points[1]->indexInGroup, (*longestConnect)->Distance())); + } + + return improved; +} + +// Check if connections form a tour +void AssertIsTour(std::vector &groups, std::vector &connections, OrderingGroupConnection *longestConnection) +{ + for (auto & connection : connections) { + for (auto pnt : connection->points) { + assert(pnt->connection == connection); + assert(pnt->connection->points[pnt->indexInConnection] == pnt); + assert(pnt->group->endpoints[pnt->indexInGroup] == pnt); + } + } + + Coord length1 = 0; + Coord longest1 = 0; + OrderingGroupPoint *current = connections.front()->points[0]; + + for (unsigned int n = 0; n < connections.size(); n++) { + DebugTrace2TSP(("Tour test 1 %p g=%d/%d c=%d/%d %p %p %.6lf %.3lf %.3lf %d %d %d", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1], current->connection->Distance(), current->point.x(), 297 - current->point.y(), current->begin, current->front, current->group->items.size())); + Coord length = current->connection->Distance(); + length1 += length; + longest1 = std::max(length, longest1); + current = current->GetOtherEndConnection(); + + DebugTrace2TSP(("Tour test 2 %p g=%d/%d c=%d/%d %p %p %.6lf %.3lf %.3lf %d %d %d", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1], current->connection->Distance(), current->point.x(), 297 - current->point.y(), current->begin, current->front, current->group->items.size())); + current = current->GetOtherEndGroup(); + } + DebugTrace2TSP(("Tour test 3 %p g=%d/%d c=%d/%d %p %p", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1])); + assert(current == connections.front()->points[0]); + + // The other direction + Coord length2 = 0; + Coord longest2 = 0; + current = connections.front()->points[0]; + for (unsigned int n = 0; n < connections.size(); n++) { + current = current->GetOtherEndGroup(); + Coord length = current->connection->Distance(); + length2 += length; + longest2 = std::max(length, longest2); + current = current->GetOtherEndConnection(); + } + assert(current == connections.front()->points[0]); + + DebugTrace1TSP(("Tour length %.6lf(%.6lf) longest %.6lf(%.6lf) remaining %.6lf(%.6lf)", length1, length2, longest1, longest2, length1 - longest1, length2 - longest2)); +} + +// Bring a tour into linear order after a modification + +/* I would like to avoid this. + * It is no problem to travel a tour with changing directions using the GetOtherEnd functions, + * but it is difficult to know the segments, that is which endpoint of a connection is connected to which by the unmodified pieces of the tour. + * In the end it is probably better to implement the Lin-Kernighan algorithm which avoids this problem by creating connected changes. */ + +void LinearizeTour(std::vector &connections) +{ + OrderingGroupPoint *current = connections.front()->points[0]; + + for (unsigned int iNew = 0; iNew < connections.size(); iNew++) { + // swap the connection at location n with the current connection + OrderingGroupConnection *connection = current->connection; + unsigned int iOld = connection->index; + assert(connections[iOld] == connection); + + connections[iOld] = connections[iNew]; + connections[iNew] = connection; + connections[iOld]->index = iOld; + connections[iNew]->index = iNew; + + // swap the points of a connection + assert(current == connection->points[0] || current == connection->points[1]); + if (current != connection->points[0]) { + connection->points[1] = connection->points[0]; + connection->points[0] = current; + connection->points[1]->indexInConnection = 1; + connection->points[0]->indexInConnection = 0; + } + + current = current->GetOtherEndConnection(); + current = current->GetOtherEndGroup(); + } +} + +// Use some Traveling Salesman Problem (TSP) like heuristics to bring several groups into a +// order with as short as possible interconnection paths + +void OrderGroups(std::vector *groups, const int nDims) +{ + // There is no point in ordering just one group + if (groups->size() <= 1) { + return; + } + + // Initialize the endpoints for all groups + for (auto & group : *groups) { + group->SetEndpoints(); + } + + // Find the neighboring points for all end points of all groups and sort by distance + for (std::vector::iterator itThis = groups->begin(); itThis != groups->end(); ++itThis) { + for (int i = 0; i < (*itThis)->nEndPoints; i++) { + // This can be up to 2x too large, but still better than incrementing the size + (*itThis)->endpoints[i]->nearest.reserve(4 * groups->size()); + } + + for (std::vector::iterator itNghb = groups->begin(); itNghb != groups->end(); ++itNghb) { + if (itThis != itNghb) { + (*itThis)->AddNeighbors(*itNghb); + } + } + + for (int i = 0; i < (*itThis)->nEndPoints; i++) { + std::sort((*itThis)->endpoints[i]->nearest.begin(), (*itThis)->endpoints[i]->nearest.end(), OrderingGroupNeighbor::Compare); + } + } + + // =========== Step 1: Create a simple nearest neighbor chain =========== + + // Vector of connection points + std::vector connections; + connections.reserve(groups->size()); + // Total Jump Distance + Coord total = 0.0; + + // Start with the first group and connect always with nearest unused point + OrderingGroupPoint *crnt = groups->front()->endpoints[0]; + + // The longest connection is ignored (we don't want cycles) + OrderingGroupConnection *longestConnect = nullptr; + + for (unsigned int nConnected = 0; nConnected < groups->size(); nConnected++) { + // Mark both end points of the current segment as used + crnt->UsePoint(); + crnt = crnt->GetOtherEndGroup(); + crnt->UsePoint(); + + // if this is the last segment, Mark start point of first segment as unused, + // so that the end can connect to it + if (nConnected == groups->size() - 1) { + groups->front()->endpoints[0]->UnusePoint(); + } + + // connect to next segment + OrderingGroupNeighbor *nghb = crnt->FindNearestUnused(); + connections.push_back(new OrderingGroupConnection(crnt, nghb->point, connections.size())); + total += nghb->distance; + crnt = nghb->point; + + if (!longestConnect || nghb->distance > longestConnect->Distance()) { + longestConnect = connections.back(); + } + } + + DebugTrace1TSP(("Total jump distance %.3lf (closed)", total)); + DebugTrace1TSP(("Total jump distance %.3lf (open)", total - longestConnect->Distance())); + + AssertIsTour(*groups, connections, longestConnect); + + // =========== Step 2: Choose nDims segments to clear and reconnect =========== + + bool improvement; + int nRuns = 0; + int nTrials = 0; + int nImprovements = 0; + + do { + improvement = false; + nRuns ++; + std::vector< std::vector::iterator > iterators; + + for ( + triangleit_begin(iterators, connections.begin(), connections.end(), nDims); + triangleit_test(iterators, connections.end()); + triangleit_incr(iterators, connections.end()) + ) { + nTrials ++; + + Coord dist = 0; + + std::vector segments(iterators.size()); + std::vector changedconnections; + changedconnections.reserve(3); + OrderingGroupConnection *prev = *iterators.back(); + + + for (size_t i = 0; i < iterators.size(); i++) { + dist += (*iterators[i])->Distance(); + segments[i].AddPoint(prev->points[1]); + segments[i].AddPoint((*iterators[i])->points[0]); + prev = *iterators[i]; + changedconnections.push_back(*iterators[i]); + } + + if (FindShortestReconnect(segments, changedconnections, connections, &longestConnect, &total, dist)) { + nImprovements ++; + + AssertIsTour(*groups, connections, longestConnect); + LinearizeTour(connections); + AssertIsTour(*groups, connections, longestConnect); + improvement = true; + } + } + } while (improvement && nRuns < 10); + + DebugTrace1TSP(("Finished after %d rounds, %d trials, %d improvements", nRuns, nTrials, nImprovements)); + + // =========== Step N: Create vector of groups from vector of connection points =========== + + std::vector result; + result.reserve(groups->size()); + + // Go through the groups starting with the longest connection (which is this way left out) + { + OrderingGroupPoint *current = longestConnect->points[1]; + + for (unsigned int n = 0; n < connections.size(); n++) { + result.push_back(current->group); + current->SetRevInGroup(); + current = current->GetOtherEndGroup(); + current = current->GetOtherEndConnection(); + } + } + + assert(result.size() == groups->size()); + assert_unique(result); + + delete_and_clear(connections); + + *groups = result; +} + +// Global optimization of path length + +void OrderingAdvanced(std::vector &infos, int nDims) +{ + if (infos.size() < 3) { + return; + } + + // Create extended ordering info vector and copy data from normal ordering info + std::vector infoex; + infoex.reserve(infos.size()); + + for (std::vector::const_iterator it = infos.begin(); it != infos.end();) { + // Note: This assumes that the index in the OrderingInfo matches the vector index! + infoex.push_back(new OrderingInfoEx(*it, infoex.size())); + ++it; + while (it != infos.end() && it->begOrig == infoex.back()->end.point) { + infoex.back()->end.point = it->endOrig; + infoex.back()->origIndices.push_back(it->index); + ++it; + } + } + + // Find closest 2 points for each point and enforce that 2nd nearest is not further away than 1.8xthe nearest + // If this is not the case, clear nearest and 2nd nearest point + for (std::vector::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.FindNearest2(infoex); + (*it)->end.FindNearest2(infoex); + } + + DebugTraceGrouping( + DebugTrace2(("STEP1")); + for (std::vector::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // Make sure the nearest points are mutual + for (auto & it : infoex) { + it->beg.EnforceMutual(); + it->end.EnforceMutual(); + } + + DebugTraceGrouping( + DebugTrace2(("STEP2")); + for (std::vector::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // Make sure the nearest points for begin and end lead to the same sub-path (same index) + for (auto & it : infoex) { + it->beg.EnforceSymmetric(it->end); + it->end.EnforceSymmetric(it->beg); + } + + DebugTraceGrouping( + DebugTrace2(("STEP3")); + for (std::vector::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // The remaining nearest neighbors should be 100% non ambiguous, so group them + std::vector groups; + for (std::vector::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->MakeGroup(infoex, &groups); + } + + // Create single groups for ungrouped lines + std::vector result; + result.reserve(infos.size()); + int nUngrouped = 0; + for (auto & it : infoex) { + if (!it->grouped) { + groups.push_back(new OrderingGroup(groups.size())); + groups.back()->items.push_back(it); + nUngrouped++; + } + } + + DebugTraceGrouping( + DebugTrace2(("Ungrouped lines = %d", nUngrouped)); + DebugTrace2(("%d Groups found", groups.size())); + for (std::vector::iterator it = groups.begin(); it != groups.end(); ++it) { + DebugTrace2(("Group size %d", (*it)->items.size())); + } + ) + + // Order groups, so that the connection path gets shortest + OrderGroups(&groups, nDims); + + // Copy grouped lines to output + for (auto & group : groups) { + for (unsigned int iItem = 0; iItem < group->items.size(); iItem++) { + unsigned int iItemRev = group->revItemList ? group->items.size() - 1 - iItem : iItem; + OrderingInfoEx *item = group->items[iItemRev]; + + // If revItems is false, even items shall have reverse=false + // In this case ( ( iItem & 1 ) == 0 )== true, revItems=false, (true==false) == false + bool reverse = ((iItem & 1) == 0) == group->revItems; + if (!reverse) { + for (int & origIndice : item->origIndices) { + result.push_back(infos[origIndice]); + result.back().reverse = false; + } + } else { + for (std::vector::reverse_iterator itOrig = item->origIndices.rbegin(); itOrig != item->origIndices.rend(); ++itOrig) { + result.push_back(infos[*itOrig]); + result.back().reverse = true; + } + } + } + result.back().connect = true; + } + + + delete_and_clear(groups); + delete_and_clear(infoex); + + infos = result; +} + +} // namespace LPEEmbroderyStitchOrdering +} // namespace LivePathEffect +} // namespace Inkscape diff --git a/src/live_effects/lpe-embrodery-stitch-ordering.h b/src/live_effects/lpe-embrodery-stitch-ordering.h new file mode 100644 index 0000000..9899f66 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch-ordering.h @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Sub-path Ordering functions for embroidery stitch LPE + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EMBRODERY_STITCH_ORDERING_H +#define INKSCAPE_LPE_EMBRODERY_STITCH_ORDERING_H + +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { +namespace LPEEmbroderyStitchOrdering { + +// Structure keeping information on the ordering and reversing of sub paths +// Used for simple ordering functions like zig-zag + +struct OrderingInfo { + int index; + bool reverse; + bool used; + bool connect; + Geom::Point begOrig; // begin point in original orientation + Geom::Point endOrig; // end point in original orientation + + Geom::Point GetBegOrig() const + { + return begOrig; + } + Geom::Point GetEndOrig() const + { + return endOrig; + } + Geom::Point GetBegRev() const + { + return reverse ? endOrig : begOrig; + } + Geom::Point GetEndRev() const + { + return reverse ? begOrig : endOrig; + } +}; + +// Structure for a path end-point in OrderingInfoEx. +// This keeps information about the two nearest neighbor points. + +struct OrderingInfoEx; + +struct OrderingPoint { + OrderingPoint(const Geom::Point &pointIn, OrderingInfoEx *infoexIn, bool beginIn) : + point(pointIn), + infoex(infoexIn), + begin(beginIn) + { + nearest[0] = nearest[1] = nullptr; + } + + // Check if both nearest values are valid + bool IsNearestValid() const + { + return nearest[0] && nearest[1]; + } + // Check if at least one nearest values are valid + bool HasNearest() const + { + return nearest[0] || nearest[1]; + } + // Find 2 nearest points to given point + void FindNearest2(const std::vector &infos); + // Check if "this" is among the nearest of its nearest + void EnforceMutual(); + // Check if the subpath indices of this and other are the same, otherwise zero both nearest + void EnforceSymmetric(const OrderingPoint &other); + // Dump point information + void Dump(); + + Geom::Point point; + OrderingInfoEx *infoex; + bool begin; + const OrderingPoint *nearest[2]; +}; + +// Structure keeping information on the ordering and reversing of sub paths +// Used for advanced ordering functions with block creation and Traveling Salesman Problem Optimization +// A OrderingInfoEx may contain several original sub-paths in case sub-paths are directly connected. +// Directly connected sub-paths happen e.g. after a slice boolean operation. + +struct OrderingGroup; + +struct OrderingInfoEx { + OrderingInfoEx(const OrderingInfo &infoIn, int idxIn) : + beg(infoIn.begOrig, this, true), + end(infoIn.endOrig, this, false), + idx(idxIn), + grouped(false) + { + origIndices.push_back(infoIn.index); + } + + // If this element can be grouped (has neighbours) but is not yet grouped, create a new group + void MakeGroup(std::vector &infos, std::vector *groups); + // Add this and all connected elements to the group + void AddToGroup(std::vector &infos, OrderingGroup *group); + + int idx; + bool grouped; // true if this element has been grouped + OrderingPoint beg; // begin point in original orientation + OrderingPoint end; // end point in original orientation + std::vector origIndices; // Indices of the original OrderInfos (more than 1 if directly connected +}; + +// Neighbor information for OrderingGroupPoint - a distance and a OrderingGroupPoint + +struct OrderingGroupPoint; + +struct OrderingGroupNeighbor { + OrderingGroupNeighbor(OrderingGroupPoint *me, OrderingGroupPoint *other); + + // Distance from owner of this neighbor info + Geom::Coord distance; + // Neighbor point (which in turn contains a pointer to the neighbor group + OrderingGroupPoint *point; + + // Comparison function for sorting by distance + static bool Compare(const OrderingGroupNeighbor &a, const OrderingGroupNeighbor &b); +}; + +// An end point in an OrderingGroup, with nearest neighbor information + +struct OrderingGroupConnection; + +struct OrderingGroupPoint { + OrderingGroupPoint(const Geom::Point &pointIn, OrderingGroup *groupIn, int indexIn, bool beginIn, bool frontIn) : + point(pointIn), + group(groupIn), + indexInGroup(indexIn), + connection(nullptr), + indexInConnection(0), + begin(beginIn), + front(frontIn), + used(false) + { + } + + // Find the nearest unused neighbor point + OrderingGroupNeighbor *FindNearestUnused(); + // Return the other end in the group of the point + OrderingGroupPoint *GetOtherEndGroup(); + // Return the alternate point (if one exists), 0 otherwise + OrderingGroupPoint *GetAltPointGroup(); + // Sets the rev flags in the group assuming that the group starts with this point + void SetRevInGroup(); + // Mark an end point as used and also mark the two other alternating points as used + // Returns the used point + void UsePoint(); + // Mark an end point as unused and possibly also mark the two other alternating points as unused + // Returns the used point + void UnusePoint(); + // Return the other end in the connection + OrderingGroupPoint *GetOtherEndConnection(); + + // The coordinates of the point + Geom::Point point; + // The group to which the point belongs + OrderingGroup *group; + // The end-point index within the group + int indexInGroup; + // The connection, which connects this point + OrderingGroupConnection *connection; + // The end point index in the connection + int indexInConnection; + // True if this is a begin point (rather than an end point) + bool begin; + // True if this is a front point (rather than a back point) + bool front; + // True if the point is used/connected to another point + bool used; + // The nearest neighbors, to which this group end point may connect + std::vector nearest; +}; + +// A connection between two points/groups +struct OrderingGroupConnection { + OrderingGroupConnection(OrderingGroupPoint *fromIn, OrderingGroupPoint *toIn, int indexIn) : + index(indexIn) + { + assert(fromIn->connection == 0); + assert(toIn->connection == 0); + points[0] = nullptr; + points[1] = nullptr; + Connect(0, fromIn); + Connect(1, toIn); + } + + // Connect one of the connection endpoints to the given point + void Connect(int index, OrderingGroupPoint *point) + { + assert(point); + points[index] = point; + point->connection = this; + point->indexInConnection = index; + } + + // Get length of connection + Geom::Coord Distance() + { + return Geom::distance(points[0]->point, points[1]->point); + } + + OrderingGroupPoint *points[2]; + // index of connection in the connections vector (just for debugging) + int index; +}; + +// A group of OrderingInfoEx, which build a block in path interconnect length optimization. +// A block can have two sets of endpoints. +// If a block has 2 sets of endpoints, one can swap between the two sets. + +struct OrderingGroup { + OrderingGroup(int indexIn) : + nEndPoints(0), + revItemList(false), + revItems(false), + index(indexIn) + { + for (auto & endpoint : endpoints) { + endpoint = nullptr; + } + } + + ~OrderingGroup() + { + for (int i = 0; i < nEndPoints; i++) { + delete endpoints[i]; + } + } + + // Set the endpoints of a group from the items + void SetEndpoints(); + // Add all points from another group as neighbors + void AddNeighbors(OrderingGroup *nghb); + // Mark an end point as used and also mark the two other alternating points as used + // Returns the used point + OrderingGroupPoint *UsePoint(int index); + // Mark an end point as unused and possibly also mark the two other alternating points as unused + // Returns the used point + void UnusePoint(int index); + + // Items on the group + std::vector items; + // End points of the group + OrderingGroupPoint *endpoints[4]; + // Number of endpoints used (either 2 or 4) + int nEndPoints; + // Index of the group (just for debugging purposes) + int index; + // If true, the items in the group shall be output from back to front. + bool revItemList; + // If false, the individual items are output alternatingly normal-reversed + // If true, the individual items are output alternatingly reversed-normal + bool revItems; +}; + +// A segment is either a OrderingGroup or a series of groups and connections. +// Usually a segment has just 2 end points. +// If a segment is just one ordering group, it has the same number of end points as the ordering group +// A main difference between a segment and a group is that the segment does not own the end points. + +struct OrderingSegment { + OrderingSegment() : + nEndPoints(0), + endbit(0), + swapbit(0) + {} + + // Add an end point + void AddPoint(OrderingGroupPoint *point); + // Get begin point (taking swap and end bit into account + OrderingGroupPoint *GetBeginPoint(unsigned int iSwap, unsigned int iEnd); + // Get end point (taking swap and end bit into account + OrderingGroupPoint *GetEndPoint(unsigned int iSwap, unsigned int iEnd); + + // End points of the group + OrderingGroupPoint *endpoints[4]; + // Number of endpoints used (either 2 or 4) + int nEndPoints; + // bit index in the end counter + int endbit; + // bit index in the swap counter + int swapbit; +}; + + +// Sub-path reordering: do nothing - keep original order +void OrderingOriginal(std::vector &infos); + +// Sub-path reordering: reverse every other sub path +void OrderingZigZag(std::vector &infos, bool revfirst); + +// Sub-path reordering: continue with the neartest start or end point of yet unused sub paths +void OrderingClosest(std::vector &infos, bool revfirst); + +// Global optimization of path length +void OrderingAdvanced(std::vector &infos, int nDims); + +} //LPEEmbroderyStitchOrdering +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-embrodery-stitch.cpp b/src/live_effects/lpe-embrodery-stitch.cpp new file mode 100644 index 0000000..4435c34 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Embroidery stitch live path effect (Implementation) + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include + +#include "live_effects/lpe-embrodery-stitch.h" +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +#include <2geom/path.h> +#include <2geom/piecewise.h> +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; +using namespace LPEEmbroderyStitchOrdering; + +static const Util::EnumData OrderMethodData[LPEEmbroderyStitch::order_method_count] = { + { LPEEmbroderyStitch::order_method_no_reorder, N_("no reordering"), "no-reorder" }, + { LPEEmbroderyStitch::order_method_zigzag, N_("zig-zag"), "zig-zag" }, + { LPEEmbroderyStitch::order_method_zigzag_rev_first, N_("zig-zag, reverse first"), "zig-zag-rev-first" }, + { LPEEmbroderyStitch::order_method_closest, N_("closest"), "closest" }, + { LPEEmbroderyStitch::order_method_closest_rev_first, N_("closest, reverse first"), "closest-rev-first" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_2, N_("traveling salesman 2-opt (fast, bad)"), "tsp-2opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_3, N_("traveling salesman 3-opt (fast, ok)"), "tsp-3opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_4, N_("traveling salesman 4-opt (seconds)"), "tsp-4opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_5, N_("traveling salesman 5-opt (minutes)"), "tsp-5opt" } +}; + +static const Util::EnumDataConverter +OrderMethodConverter(OrderMethodData, sizeof(OrderMethodData) / sizeof(*OrderMethodData)); + +static const Util::EnumData ConnectMethodData[LPEEmbroderyStitch::connect_method_count] = { + { LPEEmbroderyStitch::connect_method_line, N_("straight line"), "line" }, + { LPEEmbroderyStitch::connect_method_move_point_from, N_("move to begin"), "move-begin" }, + { LPEEmbroderyStitch::connect_method_move_point_mid, N_("move to middle"), "move-middle" }, + { LPEEmbroderyStitch::connect_method_move_point_to, N_("move to end"), "move-end" } +}; + +static const Util::EnumDataConverter +ConnectMethodConverter(ConnectMethodData, sizeof(ConnectMethodData) / sizeof(*ConnectMethodData)); + +LPEEmbroderyStitch::LPEEmbroderyStitch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + ordering(_("Ordering method"), _("Method used to order sub paths"), "ordering", OrderMethodConverter, &wr, this, order_method_no_reorder), + connection(_("Connection method"), _("Method to connect end points of sub paths"), "connection", ConnectMethodConverter, &wr, this, connect_method_line), + stitch_length(_("Stitch length"), _("If not 0, linearize path with given step length"), "stitch-length", &wr, this, 10.0), + stitch_min_length(_("Minimum stitch length [%]"), _("Combine steps shorter than this [%]"), "stitch-min-length", &wr, this, 25.0), + stitch_pattern(_("Stitch pattern"), _("Select between different stitch patterns"), "stitch_pattern", &wr, this, 0), + show_stitches(_("Show stitches"), _("Show stitches as small gaps (just for inspection - don't use for output)"), "show-stitches", &wr, this, false), + show_stitch_gap(_("Show stitch gap"), _("Gap between stitches when showing stitches"), "show-stitch-gap", &wr, this, 0.5), + jump_if_longer(_("Jump if longer"), _("Jump connection if longer than"), "jump-if-longer", &wr, this, 100) +{ + registerParameter(dynamic_cast(&ordering)); + registerParameter(dynamic_cast(&connection)); + registerParameter(dynamic_cast(&stitch_length)); + registerParameter(dynamic_cast(&stitch_min_length)); + registerParameter(dynamic_cast(&stitch_pattern)); + registerParameter(dynamic_cast(&show_stitches)); + registerParameter(dynamic_cast(&show_stitch_gap)); + registerParameter(dynamic_cast(&jump_if_longer)); + + stitch_length.param_set_digits(1); + stitch_length.param_set_range(1, 10000); + stitch_min_length.param_set_digits(1); + stitch_min_length.param_set_range(0, 100); + stitch_pattern.param_make_integer(); + stitch_pattern.param_set_range(0, 2); + show_stitch_gap.param_set_range(0.001, 10); + jump_if_longer.param_set_range(0.0, 1000000); +} + +LPEEmbroderyStitch::~LPEEmbroderyStitch() += default; + +double LPEEmbroderyStitch::GetPatternInitialStep(int pattern, int line) +{ + switch (pattern) { + case 0: + return 0.0; + + case 1: + switch (line % 4) { + case 0: + return 0.0; + case 1: + return 0.25; + case 2: + return 0.50; + case 3: + return 0.75; + } + return 0.0; + + case 2: + switch (line % 4) { + case 0: + return 0.0; + case 1: + return 0.5; + case 2: + return 0.75; + case 3: + return 0.25; + } + return 0.0; + + default: + return 0.0; + } + +} + +Point LPEEmbroderyStitch::GetStartPointInterpolAfterRev(std::vector const &info, unsigned i) +{ + Point start_this = info[i].GetBegRev(); + + if (i == 0) { + return start_this; + } + + if (!info[i - 1].connect) { + return start_this; + } + + Point end_prev = info[i - 1].GetEndRev(); + + switch (connection.get_value()) { + case connect_method_line: + return start_this; + case connect_method_move_point_from: + return end_prev; + case connect_method_move_point_mid: + return 0.5 * start_this + 0.5 * end_prev; + case connect_method_move_point_to: + return start_this; + default: + return start_this; + } +} +Point LPEEmbroderyStitch::GetEndPointInterpolAfterRev(std::vector const &info, unsigned i) +{ + Point end_this = info[i].GetEndRev(); + + if (i + 1 == info.size()) { + return end_this; + } + + if (!info[i].connect) { + return end_this; + } + + Point start_next = info[i + 1].GetBegRev(); + + switch (connection.get_value()) { + case connect_method_line: + return end_this; + case connect_method_move_point_from: + return end_this; + case connect_method_move_point_mid: + return 0.5 * start_next + 0.5 * end_this; + case connect_method_move_point_to: + return start_next; + default: + return end_this; + } +} + +Point LPEEmbroderyStitch::GetStartPointInterpolBeforeRev(std::vector const &info, unsigned i) +{ + if (info[i].reverse) { + return GetEndPointInterpolAfterRev(info, i); + } else { + return GetStartPointInterpolAfterRev(info, i); + } +} + +Point LPEEmbroderyStitch::GetEndPointInterpolBeforeRev(std::vector const &info, unsigned i) +{ + if (info[i].reverse) { + return GetStartPointInterpolAfterRev(info, i); + } else { + return GetEndPointInterpolAfterRev(info, i); + } +} + +PathVector LPEEmbroderyStitch::doEffect_path(PathVector const &path_in) +{ + if (path_in.size() >= 2) { + PathVector path_out; + + // Create vectors with start and end points + std::vector orderinginfos(path_in.size()); + // connect next path to this one + bool connect_with_previous = false; + + for (PathVector::const_iterator it = path_in.begin(); it != path_in.end(); ++it) { + OrderingInfo &info = orderinginfos[ it - path_in.begin() ]; + info.index = it - path_in.begin(); + info.reverse = false; + info.used = false; + info.connect = true; + info.begOrig = it->front().initialPoint(); + info.endOrig = it->back().finalPoint(); + } + + // Compute sub-path ordering + switch (ordering.get_value()) { + case order_method_no_reorder: + OrderingOriginal(orderinginfos); + break; + + case order_method_zigzag: + OrderingZigZag(orderinginfos, false); + break; + + case order_method_zigzag_rev_first: + OrderingZigZag(orderinginfos, true); + break; + + case order_method_closest: + OrderingClosest(orderinginfos, false); + break; + + case order_method_closest_rev_first: + OrderingClosest(orderinginfos, true); + break; + + case order_method_tsp_kopt_2: + OrderingAdvanced(orderinginfos, 2); + break; + + case order_method_tsp_kopt_3: + OrderingAdvanced(orderinginfos, 3); + break; + + case order_method_tsp_kopt_4: + OrderingAdvanced(orderinginfos, 4); + break; + + case order_method_tsp_kopt_5: + OrderingAdvanced(orderinginfos, 5); + break; + + } + + // Iterate over sub-paths in order found above + // Divide paths into stitches (currently always equidistant) + // Interpolate between neighboring paths, so that their ends coincide + for (std::vector::const_iterator it = orderinginfos.begin(); it != orderinginfos.end(); ++it) { + // info index + unsigned iInfo = it - orderinginfos.begin(); + // subpath index + unsigned iPath = it->index; + // decide of path shall be reversed + bool reverse = it->reverse; + // minimum stitch length in absolute measure + double stitch_min_length_abs = stitch_min_length * 0.01 * stitch_length; + + // convert path to piecewise + Piecewise > pwOrig = path_in[iPath].toPwSb(); + // make piecewise equidistant in time + Piecewise > pwEqdist = arc_length_parametrization(pwOrig); + Piecewise > pwStitch; + + // cut into stitches + double cutpos = 0.0; + Interval pwdomain = pwEqdist.domain(); + + // step length of first stitch + double step = GetPatternInitialStep(stitch_pattern, iInfo) * stitch_length; + if (step < stitch_min_length_abs) { + step += stitch_length; + } + + bool last = false; + bool first = true; + double posnext; + for (double pos = pwdomain.min(); !last; pos = posnext, cutpos += 1.0) { + // start point + Point p1; + if (first) { + p1 = GetStartPointInterpolBeforeRev(orderinginfos, iInfo); + first = false; + } else { + p1 = pwEqdist.valueAt(pos); + } + + // end point of this stitch + Point p2; + posnext = pos + step; + // last stitch is to end + if (posnext >= pwdomain.max() - stitch_min_length_abs) { + p2 = GetEndPointInterpolBeforeRev(orderinginfos, iInfo); + last = true; + } else { + p2 = pwEqdist.valueAt(posnext); + } + + pwStitch.push_cut(cutpos); + pwStitch.push_seg(D2(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y])))); + + // stitch length for all except first step + step = stitch_length; + } + pwStitch.push_cut(cutpos); + + if (reverse) { + pwStitch = Geom::reverse(pwStitch); + } + + if (it->connect && iInfo != orderinginfos.size() - 1) { + // Connect this segment with the previous segment by a straight line + Point end = pwStitch.lastValue(); + Point start_next = GetStartPointInterpolAfterRev(orderinginfos, iInfo + 1); + // connect end and start point + if (end != start_next && distance(end, start_next) <= jump_if_longer) { + cutpos += 1.0; + pwStitch.push_seg(D2(SBasis(Linear(end[X], start_next[X])), SBasis(Linear(end[Y], start_next[Y])))); + pwStitch.push_cut(cutpos); + } + } + + if (show_stitches) { + for (auto & seg : pwStitch.segs) { + // Create anew piecewise with just one segment + Piecewise > pwOne; + pwOne.push_cut(0); + pwOne.push_seg(seg); + pwOne.push_cut(1); + + // make piecewise equidistant in time + Piecewise > pwOneEqdist = arc_length_parametrization(pwOne); + Interval pwdomain = pwOneEqdist.domain(); + + // Compute the points of the shortened piece + Coord len = pwdomain.max() - pwdomain.min(); + Coord offs = 0.5 * (show_stitch_gap < 0.5 * len ? show_stitch_gap : 0.5 * len); + Point p1 = pwOneEqdist.valueAt(pwdomain.min() + offs); + Point p2 = pwOneEqdist.valueAt(pwdomain.max() - offs); + Piecewise > pwOneGap; + + // Create Linear SBasis + D2 sbasis = D2(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y]))); + + // Convert to path and add to path list + Geom::Path path = path_from_sbasis(sbasis , LPE_CONVERSION_TOLERANCE, false); + path_out.push_back(path); + } + } else { + PathVector pathv = path_from_piecewise(pwStitch, LPE_CONVERSION_TOLERANCE); + for (const auto & ipv : pathv) { + if (connect_with_previous) { + path_out.back().append(ipv); + } else { + path_out.push_back(ipv); + } + } + } + + connect_with_previous = it->connect; + } + + return path_out; + } else { + return path_in; + } +} + +void +LPEEmbroderyStitch::resetDefaults(SPItem const *item) +{ + Effect::resetDefaults(item); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-embrodery-stitch.h b/src/live_effects/lpe-embrodery-stitch.h new file mode 100644 index 0000000..6bbd931 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Embroidery stitch live path effect + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EMBRODERY_STITCH_H +#define INKSCAPE_LPE_EMBRODERY_STITCH_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +namespace Inkscape { +namespace LivePathEffect { + +using namespace LPEEmbroderyStitchOrdering; + +class LPEEmbroderyStitch : public Effect { +public: + + LPEEmbroderyStitch(LivePathEffectObject *lpeobject); + ~LPEEmbroderyStitch() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + + void resetDefaults(SPItem const *item) override; + + enum order_method { + order_method_no_reorder, + order_method_zigzag, + order_method_zigzag_rev_first, + order_method_closest, + order_method_closest_rev_first, + order_method_tsp_kopt_2, + order_method_tsp_kopt_3, + order_method_tsp_kopt_4, + order_method_tsp_kopt_5, + order_method_count + }; + enum connect_method { + connect_method_line, + connect_method_move_point_from, + connect_method_move_point_mid, + connect_method_move_point_to, + connect_method_count + }; + +private: + EnumParam ordering; + EnumParam connection; + ScalarParam stitch_length; + ScalarParam stitch_min_length; + ScalarParam stitch_pattern; + BoolParam show_stitches; + ScalarParam show_stitch_gap; + ScalarParam jump_if_longer; + + LPEEmbroderyStitch(const LPEEmbroderyStitch &) = delete; + LPEEmbroderyStitch &operator=(const LPEEmbroderyStitch &) = delete; + + double GetPatternInitialStep(int pattern, int line); + Geom::Point GetStartPointInterpolAfterRev(std::vector const &info, unsigned i); + Geom::Point GetEndPointInterpolAfterRev(std::vector const &info, unsigned i); + Geom::Point GetStartPointInterpolBeforeRev(std::vector const &info, unsigned i); + Geom::Point GetEndPointInterpolBeforeRev(std::vector const &info, unsigned i); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-envelope.cpp b/src/live_effects/lpe-envelope.cpp new file mode 100644 index 0000000..cac8d7b --- /dev/null +++ b/src/live_effects/lpe-envelope.cpp @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Steren Giannini 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-envelope.h" +#include "display/curve.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEEnvelope::LPEEnvelope(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + bend_path1(_("Top bend path:"), _("Top path along which to bend the original path"), "bendpath1", &wr, this, "M0,0 L1,0"), + bend_path2(_("Right bend path:"), _("Right path along which to bend the original path"), "bendpath2", &wr, this, "M0,0 L1,0"), + bend_path3(_("Bottom bend path:"), _("Bottom path along which to bend the original path"), "bendpath3", &wr, this, "M0,0 L1,0"), + bend_path4(_("Left bend path:"), _("Left path along which to bend the original path"), "bendpath4", &wr, this, "M0,0 L1,0"), + xx(_("_Enable left & right paths"), _("Enable the left and right deformation paths"), "xx", &wr, this, true), + yy(_("_Enable top & bottom paths"), _("Enable the top and bottom deformation paths"), "yy", &wr, this, true) +{ + registerParameter(&yy); + registerParameter(&xx); + registerParameter(&bend_path1); + registerParameter(&bend_path2); + registerParameter(&bend_path3); + registerParameter(&bend_path4); + concatenate_before_pwd2 = true; + apply_to_clippath_and_mask = true; +} + +LPEEnvelope::~LPEEnvelope() += default; + +void LPEEnvelope::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + bend_path1.param_transform_multiply(postmul, false); + bend_path2.param_transform_multiply(postmul, false); + bend_path3.param_transform_multiply(postmul, false); + bend_path4.param_transform_multiply(postmul, false); +} + +void +LPEEnvelope::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the item bounding box + original_bbox(lpeitem, false, true); +} + +Geom::Piecewise > +LPEEnvelope::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + + if(!xx.get_value() && !yy.get_value()) + { + return pwd2_in; + } + + using namespace Geom; + + // Don't allow empty path parameters: + if ( bend_path1.get_pathvector().empty() + || bend_path2.get_pathvector().empty() + || bend_path3.get_pathvector().empty() + || bend_path4.get_pathvector().empty() ) + { + return pwd2_in; + } + + /* + The code below is inspired from the Bend Path code developed by jfb and mgsloan + Please, read it before trying to understand this one + */ + + Piecewise > uskeleton1 = arc_length_parametrization(bend_path1.get_pwd2(),2,.1); + uskeleton1 = remove_short_cuts(uskeleton1,.01); + Piecewise > n1 = rot90(derivative(uskeleton1)); + n1 = force_continuity(remove_short_cuts(n1,.1)); + + Piecewise > uskeleton2 = arc_length_parametrization(bend_path2.get_pwd2(),2,.1); + uskeleton2 = remove_short_cuts(uskeleton2,.01); + Piecewise > n2 = rot90(derivative(uskeleton2)); + n2 = force_continuity(remove_short_cuts(n2,.1)); + + Piecewise > uskeleton3 = arc_length_parametrization(bend_path3.get_pwd2(),2,.1); + uskeleton3 = remove_short_cuts(uskeleton3,.01); + Piecewise > n3 = rot90(derivative(uskeleton3)); + n3 = force_continuity(remove_short_cuts(n3,.1)); + + Piecewise > uskeleton4 = arc_length_parametrization(bend_path4.get_pwd2(),2,.1); + uskeleton4 = remove_short_cuts(uskeleton4,.01); + Piecewise > n4 = rot90(derivative(uskeleton4)); + n4 = force_continuity(remove_short_cuts(n4,.1)); + + + D2 > patternd2 = make_cuts_independent(pwd2_in); + Piecewise x = Piecewise(patternd2[0]); + Piecewise y = Piecewise(patternd2[1]); + + /*The *1.001 is a hack to avoid a small bug : path at x=0 and y=0 don't work well. */ + x-= boundingbox_X.min()*1.001; + y-= boundingbox_Y.min()*1.001; + + Piecewise x1 = x ; + Piecewise y1 = y ; + + Piecewise x2 = x ; + Piecewise y2 = y ; + x2 -= boundingbox_X.extent(); + + Piecewise x3 = x ; + Piecewise y3 = y ; + y3 -= boundingbox_Y.extent(); + + Piecewise x4 = x ; + Piecewise y4 = y ; + + + /*Scaling to the Bend Path length*/ + double scaling1 = uskeleton1.cuts.back()/boundingbox_X.extent(); + if (scaling1 != 1.0) { + x1*=scaling1; + } + + double scaling2 = uskeleton2.cuts.back()/boundingbox_Y.extent(); + if (scaling2 != 1.0) { + y2*=scaling2; + } + + double scaling3 = uskeleton3.cuts.back()/boundingbox_X.extent(); + if (scaling3 != 1.0) { + x3*=scaling3; + } + + double scaling4 = uskeleton4.cuts.back()/boundingbox_Y.extent(); + if (scaling4 != 1.0) { + y4*=scaling4; + } + + + + Piecewise xbis = x; + Piecewise ybis = y; + xbis *= -1.0; + xbis += boundingbox_X.extent(); + ybis *= -1.0; + ybis += boundingbox_Y.extent(); + /* This is important : y + ybis = constant and x +xbis = constant */ + + Piecewise > output; + Piecewise > output1; + Piecewise > output2; + Piecewise > output_x; + Piecewise > output_y; + + /* + output_y : Deformation by Up and Down Bend Paths + We use weighting : The closer a point is to a Band Path, the more it will be affected by this Bend Path. + This is done by the line "ybis*Derformation1 + y*Deformation2" + The result is a mix between the 2 deformed paths + */ + output_y = ybis*(compose((uskeleton1),x1) + y1*compose(n1,x1) ) + + y*(compose((uskeleton3),x3) + y3*compose(n3,x3) ); + output_y /= (boundingbox_Y.extent()); + if(!xx.get_value() && yy.get_value()) + { + return output_y; + } + + /*output_x : Deformation by Left and Right Bend Paths*/ + output_x = x*(compose((uskeleton2),y2) + -x2*compose(n2,y2) ) + + xbis*(compose((uskeleton4),y4) + -x4*compose(n4,y4) ); + output_x /= (boundingbox_X.extent()); + if(xx.get_value() && !yy.get_value()) + { + return output_x; + } + + /*output : Deformation by Up, Left, Right and Down Bend Paths*/ + if(xx.get_value() && yy.get_value()) + { + Piecewise xsqr = x*xbis; /* xsqr = x * (BBox_X - x) */ + Piecewise ysqr = y*ybis; /* xsqr = y * (BBox_Y - y) */ + Piecewise xsqrbis = xsqr; + Piecewise ysqrbis = ysqr; + xsqrbis *= -1; + xsqrbis += boundingbox_X.extent()*boundingbox_X.extent()/4.; + ysqrbis *= -1; + ysqrbis += boundingbox_Y.extent()*boundingbox_Y.extent()/4.; + /*This is important : xsqr + xsqrbis = constant*/ + + + /* + Here we mix the last two results : output_x and output_y + output1 : The more a point is close to Up and Down, the less it will be affected by output_x. + (This is done with the polynomial function) + output2 : The more a point is close to Left and Right, the less it will be affected by output_y. + output : we do the mean between output1 and output2 for all points. + */ + output1 = (ysqrbis*output_y) + (ysqr*output_x); + output1 /= (boundingbox_Y.extent()*boundingbox_Y.extent()/4.); + + output2 = (xsqrbis*output_x) + (xsqr*output_y); + output2 /= (boundingbox_X.extent()*boundingbox_X.extent()/4.); + + output = output1 + output2; + output /= 2.; + + return output; + /*Of course, the result is not perfect, but on a graphical point of view, this is sufficient.*/ + + } + + // do nothing when xx and yy are both false + return pwd2_in; +} + +void +LPEEnvelope::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + original_bbox(SP_LPE_ITEM(item), false, true); + + Geom::Point Up_Left(boundingbox_X.min(), boundingbox_Y.min()); + Geom::Point Up_Right(boundingbox_X.max(), boundingbox_Y.min()); + Geom::Point Down_Left(boundingbox_X.min(), boundingbox_Y.max()); + Geom::Point Down_Right(boundingbox_X.max(), boundingbox_Y.max()); + + Geom::Path path1; + path1.start( Up_Left ); + path1.appendNew( Up_Right ); + bend_path1.set_new_value( path1.toPwSb(), true ); + + Geom::Path path2; + path2.start( Up_Right ); + path2.appendNew( Down_Right ); + bend_path2.set_new_value( path2.toPwSb(), true ); + + Geom::Path path3; + path3.start( Down_Left ); + path3.appendNew( Down_Right ); + bend_path3.set_new_value( path3.toPwSb(), true ); + + Geom::Path path4; + path4.start( Up_Left ); + path4.appendNew( Down_Left ); + bend_path4.set_new_value( path4.toPwSb(), true ); +} + + +} // namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-envelope.h b/src/live_effects/lpe-envelope.h new file mode 100644 index 0000000..0cf2bfd --- /dev/null +++ b/src/live_effects/lpe-envelope.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ENVELOPE_H +#define INKSCAPE_LPE_ENVELOPE_H + +/* + * Inkscape::LPEEnvelope + * + * Copyright (C) Steren Giannini 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/d2.h> +#include <2geom/piecewise.h> + +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEEnvelope : public Effect, GroupBBoxEffect { +public: + LPEEnvelope(LivePathEffectObject *lpeobject); + ~LPEEnvelope() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + PathParam bend_path1; + PathParam bend_path2; + PathParam bend_path3; + PathParam bend_path4; + BoolParam xx; + BoolParam yy; + + void on_pattern_pasted(); + + LPEEnvelope(const LPEEnvelope&); + LPEEnvelope& operator=(const LPEEnvelope&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-extrude.cpp b/src/live_effects/lpe-extrude.cpp new file mode 100644 index 0000000..57e5ca2 --- /dev/null +++ b/src/live_effects/lpe-extrude.cpp @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE effect for extruding paths (making them "3D"). + * + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-extrude.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +namespace Inkscape { +namespace LivePathEffect { + +LPEExtrude::LPEExtrude(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + extrude_vector(_("Direction"), _("Defines the direction and magnitude of the extrusion"), "extrude_vector", &wr, this, Geom::Point(-10,10)) +{ + show_orig_path = true; + concatenate_before_pwd2 = false; + + registerParameter(&extrude_vector); +} + +LPEExtrude::~LPEExtrude() += default; + +static bool are_colinear(Geom::Point a, Geom::Point b) { + return Geom::are_near(cross(a,b), 0., 0.5); +} + +// find cusps, except at start/end for closed paths. +// this should be factored out later. +static std::vector find_cusps( Geom::Piecewise > const & pwd2_in ) { + using namespace Geom; + Piecewise > deriv = derivative(pwd2_in); + std::vector cusps; + // cusps are spots where the derivative jumps. + for (unsigned i = 1 ; i < deriv.size() ; ++i) { + if ( ! are_colinear(deriv[i-1].at1(), deriv[i].at0()) ) { + // there is a jump in the derivative, so add it to the cusps list + cusps.push_back(deriv.cuts[i]); + } + } + return cusps; +} + +Geom::Piecewise > +LPEExtrude::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + // generate connecting lines (the 'sides' of the extrusion) + Geom::Path path(Point(0.,0.)); + path.appendNew( extrude_vector.getVector() ); + Piecewise > connector = path.toPwSb(); + + switch( 1 ) { + case 0: { + /* This one results in the following subpaths: the original, a displaced copy, and connector lines between the two + */ + + Piecewise > pwd2_out = pwd2_in; + // generate extrusion bottom: (just a copy of original path, displaced a bit) + pwd2_out.concat( pwd2_in + extrude_vector.getVector() ); + + // connecting lines should be put at start and end of path if it is not closed + // it is not possible to check whether a piecewise path is closed, + // so we check whether start and end are close + if ( ! are_near(pwd2_in.firstValue(), pwd2_in.lastValue()) ) { + pwd2_out.concat( connector + pwd2_in.firstValue() ); + pwd2_out.concat( connector + pwd2_in.lastValue() ); + } + // connecting lines should be put at cusps + Piecewise > deriv = derivative(pwd2_in); + std::vector cusps; // = roots(deriv); + for (double cusp : cusps) { + pwd2_out.concat( connector + pwd2_in.valueAt(cusp) ); + } + // connecting lines should be put where the tangent of the path equals the extrude_vector in direction + std::vector rts = roots(dot(deriv, rot90(extrude_vector.getVector()))); + for (double rt : rts) { + pwd2_out.concat( connector + pwd2_in.valueAt(rt) ); + } + return pwd2_out; + } + + default: + case 1: { + /* This one creates separate closed subpaths that correspond to the faces of the extruded shape. + * When the LPE is complete, one can convert the shape to a normal path, then break subpaths apart and start coloring them. + */ + + Piecewise > pwd2_out; + // split input path in pieces between points where deriv == vector + Piecewise > deriv = derivative(pwd2_in); + std::vector rts = roots(dot(deriv, rot90(extrude_vector.getVector()))); + + std::vector cusps = find_cusps(pwd2_in); + + // see if we should treat the path as being closed. + bool closed_path = false; + if ( are_near(pwd2_in.firstValue(), pwd2_in.lastValue()) ) { + // the path is closed, however if there is a cusp at the closing point, we should treat it as being an open path. + if ( are_colinear(deriv.firstValue(), deriv.lastValue()) ) { + // there is no jump in the derivative, so treat path as being closed + closed_path = true; + } + } + + std::vector connector_pts; + if (rts.empty()) { + connector_pts = cusps; + } else if (cusps.empty()) { + connector_pts = rts; + } else { + connector_pts = rts; + connector_pts.insert(connector_pts.begin(), cusps.begin(), cusps.end()); + sort(connector_pts.begin(), connector_pts.end()); + } + + double portion_t = 0.; + for (unsigned i = 0; i < connector_pts.size() ; ++i) { + Piecewise > cut = portion(pwd2_in, portion_t, connector_pts[i] ); + portion_t = connector_pts[i]; + if (closed_path && i == 0) { + // if the path is closed, skip the first cut and add it to the last cut later + continue; + } + Piecewise > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } + if (closed_path) { + Piecewise > cut = portion(pwd2_in, portion_t, pwd2_in.domain().max() ); + cut.continuousConcat(portion(pwd2_in, pwd2_in.domain().min(), connector_pts[0] )); + Piecewise > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } else if (!are_near(portion_t, pwd2_in.domain().max())) { + Piecewise > cut = portion(pwd2_in, portion_t, pwd2_in.domain().max() ); + Piecewise > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } + return pwd2_out; + } + } +} + +void +LPEExtrude::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + using namespace Geom; + + Geom::OptRect bbox = item->geometricBounds(); + if (bbox) { + Interval const &boundingbox_X = (*bbox)[Geom::X]; + Interval const &boundingbox_Y = (*bbox)[Geom::Y]; + extrude_vector.set_and_write_new_values( Geom::Point(boundingbox_X.middle(), boundingbox_Y.middle()), + (boundingbox_X.extent() + boundingbox_Y.extent())*Geom::Point(-0.05,0.2) ); + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-extrude.h b/src/live_effects/lpe-extrude.h new file mode 100644 index 0000000..d666138 --- /dev/null +++ b/src/live_effects/lpe-extrude.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief LPE effect for extruding paths (making them "3D"). + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EXTRUDE_H +#define INKSCAPE_LPE_EXTRUDE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEExtrude : public Effect { +public: + LPEExtrude(LivePathEffectObject *lpeobject); + ~LPEExtrude() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + VectorParam extrude_vector; + + LPEExtrude(const LPEExtrude&) = delete; + LPEExtrude& operator=(const LPEExtrude&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-fill-between-many.cpp b/src/live_effects/lpe-fill-between-many.cpp new file mode 100644 index 0000000..cf5e387 --- /dev/null +++ b/src/live_effects/lpe-fill-between-many.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "live_effects/lpe-fill-between-many.h" +#include "live_effects/lpeobject.h" +#include "xml/node.h" +#include "display/curve.h" +#include "inkscape.h" +#include "selection.h" + +#include "object/sp-defs.h" +#include "object/sp-shape.h" +#include "svg/svg.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData FilllpemethodData[] = { + { FLM_ORIGINALD, N_("Without LPE's"), "originald" }, + { FLM_BSPLINESPIRO, N_("With Spiro or BSpline"), "bsplinespiro" }, + { FLM_D, N_("With LPE's"), "d" } +}; +static const Util::EnumDataConverter FLMConverter(FilllpemethodData, FLM_END); + +LPEFillBetweenMany::LPEFillBetweenMany(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , linked_paths(_("Linked path:"), _("Paths from which to take the original path data"), "linkedpaths", &wr, this) + , method(_("LPE's on linked:"), _("LPE's on linked"), "method", FLMConverter, &wr, this, FLM_BSPLINESPIRO) + , join(_("Join subpaths"), _("Join subpaths"), "join", &wr, this, true) + , close(_("Close"), _("Close path"), "close", &wr, this, true) + , autoreverse(_("Autoreverse"), _("Autoreverse"), "autoreverse", &wr, this, true) + , applied("Store the first apply", "", "applied", &wr, this, "false", false) +{ + registerParameter(&linked_paths); + registerParameter(&method); + registerParameter(&join); + registerParameter(&close); + registerParameter(&autoreverse); + registerParameter(&applied); + previous_method = FLM_END; +} + +LPEFillBetweenMany::~LPEFillBetweenMany() += default; + +void LPEFillBetweenMany::doEffect (SPCurve * curve) +{ + if (previous_method != method) { + if (method == FLM_BSPLINESPIRO) { + linked_paths.allowOnlyBsplineSpiro(true); + linked_paths.setFromOriginalD(false); + } else if(method == FLM_ORIGINALD) { + linked_paths.allowOnlyBsplineSpiro(false); + linked_paths.setFromOriginalD(true); + } else { + linked_paths.allowOnlyBsplineSpiro(false); + linked_paths.setFromOriginalD(false); + } + previous_method = method; + } + Geom::PathVector res_pathv; + Geom::Affine transf = sp_item_transform_repr(sp_lpe_item); + if (transf != Geom::identity()) { + sp_lpe_item->doWriteTransform(Geom::identity()); + } + bool closedlink = false; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = nullptr; + if (desktop) { + selection = desktop->selection; + } + if (!autoreverse) { + for (auto & iter : linked_paths._vector) { + SPObject *obj; + if (iter->ref.isAttached() && (obj = iter->ref.getObject()) && SP_IS_ITEM(obj) && + !iter->_pathvector.empty() && iter->visibled) { + Geom::Path linked_path; + if (iter->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + continue; + } + if (obj && transf != Geom::identity() && selection && !selection->includes(obj->getRepr())) { + SP_ITEM(obj)->doWriteTransform(transf); + } + if (iter->reversed) { + linked_path = iter->_pathvector.front().reversed(); + } else { + linked_path = iter->_pathvector.front(); + } + if (!res_pathv.empty() && join) { + if (!are_near(res_pathv.front().finalPoint(), linked_path.initialPoint(), 0.1)) { + res_pathv.front().appendNew(linked_path.initialPoint()); + } else { + linked_path.setInitial(res_pathv.front().finalPoint()); + } + res_pathv.front().append(linked_path); + } else { + if (close && !join) { + linked_path.close(); + } + res_pathv.push_back(linked_path); + } + } + } + } else { + unsigned int counter = 0; + Geom::Point current = Geom::Point(); + std::vector done; + for (auto & iter : linked_paths._vector) { + SPObject *obj; + if (iter->ref.isAttached() && (obj = iter->ref.getObject()) && SP_IS_ITEM(obj) && + !iter->_pathvector.empty() && iter->visibled) { + Geom::Path linked_path; + if (iter->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + counter++; + continue; + } + if (obj && transf != Geom::identity() && selection && !selection->includes(obj->getRepr())) { + SP_ITEM(obj)->doWriteTransform(transf); + } + if (counter == 0) { + current = iter->_pathvector.front().finalPoint(); + Geom::Path initial_path = iter->_pathvector.front(); + done.push_back(0); + if (close && !join) { + initial_path.close(); + } + res_pathv.push_back(initial_path); + } + Geom::Coord distance = Geom::infinity(); + unsigned int counter2 = 0; + unsigned int added = 0; + PathAndDirectionAndVisible *nearest = nullptr; + for (auto & iter2 : linked_paths._vector) { + SPObject *obj2; + if (iter2->ref.isAttached() && (obj2 = iter2->ref.getObject()) && SP_IS_ITEM(obj2) && + !iter2->_pathvector.empty() && iter2->visibled) { + if (obj == obj2 || std::find(done.begin(), done.end(), counter2) != done.end()) { + counter2++; + continue; + } + if (iter2->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + counter2++; + continue; + } + Geom::Point start = iter2->_pathvector.front().initialPoint(); + Geom::Point end = iter2->_pathvector.front().finalPoint(); + Geom::Coord distance_iter = + std::min(Geom::distance(current, end), Geom::distance(current, start)); + if (distance > distance_iter) { + distance = distance_iter; + nearest = iter2; + added = counter2; + } + counter2++; + } + } + if (nearest != nullptr) { + done.push_back(added); + Geom::Point start = nearest->_pathvector.front().initialPoint(); + Geom::Point end = nearest->_pathvector.front().finalPoint(); + if (Geom::distance(current, end) > Geom::distance(current, start)) { + linked_path = nearest->_pathvector.front(); + } else { + linked_path = nearest->_pathvector.front().reversed(); + } + current = end; + if (!res_pathv.empty() && join) { + if (!are_near(res_pathv.front().finalPoint(), linked_path.initialPoint(), 0.1)) { + res_pathv.front().appendNew(linked_path.initialPoint()); + } else { + linked_path.setInitial(res_pathv.front().finalPoint()); + } + res_pathv.front().append(linked_path); + } else { + if (close && !join) { + linked_path.close(); + } + res_pathv.push_back(linked_path); + } + } + counter++; + } + } + } + if (!res_pathv.empty() && close) { + res_pathv.front().close(); + } + if (res_pathv.empty()) { + res_pathv = curve->get_pathvector(); + } + + curve->set_pathvector(res_pathv); +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-fill-between-many.h b/src/live_effects/lpe-fill-between-many.h new file mode 100644 index 0000000..6ddfb61 --- /dev/null +++ b/src/live_effects/lpe-fill-between-many.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILL_BETWEEN_MANY_H +#define INKSCAPE_LPE_FILL_BETWEEN_MANY_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/originalpatharray.h" +#include "live_effects/parameter/hidden.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum Filllpemethod { + FLM_ORIGINALD, + FLM_BSPLINESPIRO, + FLM_D, + FLM_END +}; + +class LPEFillBetweenMany : public Effect { +public: + LPEFillBetweenMany(LivePathEffectObject *lpeobject); + ~LPEFillBetweenMany() override; + void doEffect (SPCurve * curve) override; +private: + OriginalPathArrayParam linked_paths; + EnumParam method; + BoolParam join; + BoolParam close; + BoolParam autoreverse; + HiddenParam applied; + Filllpemethod previous_method; + LPEFillBetweenMany(const LPEFillBetweenMany&) = delete; + LPEFillBetweenMany& operator=(const LPEFillBetweenMany&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-fill-between-strokes.cpp b/src/live_effects/lpe-fill-between-strokes.cpp new file mode 100644 index 0000000..56457a3 --- /dev/null +++ b/src/live_effects/lpe-fill-between-strokes.cpp @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-fill-between-strokes.h" + +#include "display/curve.h" +#include "svg/svg.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEFillBetweenStrokes::LPEFillBetweenStrokes(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + linked_path(_("Linked path:"), _("Path from which to take the original path data"), "linkedpath", &wr, this), + second_path(_("Second path:"), _("Second path from which to take the original path data"), "secondpath", &wr, this), + reverse_second(_("Reverse Second"), _("Reverses the second path order"), "reversesecond", &wr, this), + join(_("Join subpaths"), _("Join subpaths"), "join", &wr, this, true), + close(_("Close"), _("Close path"), "close", &wr, this, true) +{ + registerParameter(&linked_path); + registerParameter(&second_path); + registerParameter(&reverse_second); + registerParameter(&join); + registerParameter(&close); +} + +LPEFillBetweenStrokes::~LPEFillBetweenStrokes() += default; + +void LPEFillBetweenStrokes::doEffect (SPCurve * curve) +{ + if (curve) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = nullptr; + if (desktop) { + selection = desktop->selection; + } + Geom::Affine transf = sp_item_transform_repr(sp_lpe_item); + if (transf != Geom::identity()) { + sp_lpe_item->doWriteTransform(Geom::identity()); + } + if ( linked_path.linksToPath() && second_path.linksToPath() && linked_path.getObject() && second_path.getObject() ) { + SPItem * linked1 = linked_path.getObject(); + if (linked1 && transf != Geom::identity() && selection && !selection->includes(linked1->getRepr())) { + SP_ITEM(linked1)->doWriteTransform(transf); + } + Geom::PathVector linked_pathv = linked_path.get_pathvector(); + SPItem * linked2 = second_path.getObject(); + if (linked2 && transf != Geom::identity() && selection && !selection->includes(linked2->getRepr())) { + SP_ITEM(linked2)->doWriteTransform(transf); + } + Geom::PathVector second_pathv = second_path.get_pathvector(); + Geom::PathVector result_linked_pathv; + Geom::PathVector result_second_pathv; + for (auto & iter : linked_pathv) + { + result_linked_pathv.push_back(iter); + } + for (auto & iter : second_pathv) + { + result_second_pathv.push_back(iter); + } + + if ( !result_linked_pathv.empty() && !result_second_pathv.empty() && !result_linked_pathv.front().closed() ) { + if (reverse_second.get_value()) { + result_second_pathv.front() = result_second_pathv.front().reversed(); + } + if (join) { + if (!are_near(result_linked_pathv.front().finalPoint(), result_second_pathv.front().initialPoint(), 0.1)) { + result_linked_pathv.front().appendNew(result_second_pathv.front().initialPoint()); + } else { + result_second_pathv.front().setInitial(result_linked_pathv.front().finalPoint()); + } + result_linked_pathv.front().append(result_second_pathv.front()); + if (close) { + result_linked_pathv.front().close(); + } + } else { + if (close) { + result_linked_pathv.front().close(); + result_second_pathv.front().close(); + } + result_linked_pathv.push_back(result_second_pathv.front()); + } + curve->set_pathvector(result_linked_pathv); + } else if ( !result_linked_pathv.empty() ) { + curve->set_pathvector(result_linked_pathv); + } else if ( !result_second_pathv.empty() ) { + curve->set_pathvector(result_second_pathv); + } + } + else if ( linked_path.linksToPath() && linked_path.getObject() ) { + SPItem *linked1 = linked_path.getObject(); + if (linked1 && transf != Geom::identity() && selection && !selection->includes(linked1->getRepr())) { + SP_ITEM(linked1)->doWriteTransform(transf); + } + Geom::PathVector linked_pathv = linked_path.get_pathvector(); + Geom::PathVector result_pathv; + for (auto & iter : linked_pathv) + { + result_pathv.push_back(iter); + } + if ( !result_pathv.empty() ) { + if (close) { + result_pathv.front().close(); + } + curve->set_pathvector(result_pathv); + } + } + else if ( second_path.linksToPath() && second_path.getObject() ) { + SPItem *linked2 = second_path.getObject(); + if (linked2 && transf != Geom::identity() && selection && !selection->includes(linked2->getRepr())) { + SP_ITEM(linked2)->doWriteTransform(transf); + } + Geom::PathVector second_pathv = second_path.get_pathvector(); + Geom::PathVector result_pathv; + for (auto & iter : second_pathv) + { + result_pathv.push_back(iter); + } + if ( !result_pathv.empty() ) { + if (close) { + result_pathv.front().close(); + } + curve->set_pathvector(result_pathv); + } + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-fill-between-strokes.h b/src/live_effects/lpe-fill-between-strokes.h new file mode 100644 index 0000000..028535f --- /dev/null +++ b/src/live_effects/lpe-fill-between-strokes.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILL_BETWEEN_STROKES_H +#define INKSCAPE_LPE_FILL_BETWEEN_STROKES_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/originalpath.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEFillBetweenStrokes : public Effect { +public: + LPEFillBetweenStrokes(LivePathEffectObject *lpeobject); + ~LPEFillBetweenStrokes() override; + void doEffect (SPCurve * curve) override; + +private: + OriginalPathParam linked_path; + OriginalPathParam second_path; + BoolParam reverse_second; + BoolParam join; + BoolParam close; + +private: + LPEFillBetweenStrokes(const LPEFillBetweenStrokes&) = delete; + LPEFillBetweenStrokes& operator=(const LPEFillBetweenStrokes&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-fillet-chamfer.cpp b/src/live_effects/lpe-fillet-chamfer.cpp new file mode 100644 index 0000000..30fe3a6 --- /dev/null +++ b/src/live_effects/lpe-fillet-chamfer.cpp @@ -0,0 +1,699 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz + * + * Copyright (C) 2014 Author(s) + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-fillet-chamfer.h" + +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include "helper/geom-satellite.h" + +#include "display/curve.h" +#include "knotholder.h" +#include "ui/tools/tool-base.h" +#include <2geom/elliptical-arc.h> +#include + +#include "object/sp-shape.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData FilletmethodData[] = { + { FM_AUTO, N_("Auto"), "auto" }, + { FM_ARC, N_("Force arc"), "arc" }, + { FM_BEZIER, N_("Force bezier"), "bezier" } +}; +static const Util::EnumDataConverter FMConverter(FilletmethodData, FM_END); + +LPEFilletChamfer::LPEFilletChamfer(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + unit(_("Unit"), _("Unit"), "unit", &wr, this, "px"), + satellites_param("Satellites_param", "Satellites_param", + "satellites_param", &wr, this), + method(_("Method:"), _("Methods to calculate the fillet or chamfer"), + "method", FMConverter, &wr, this, FM_AUTO), + mode(_("Mode:"), _("Mode, fillet or chamfer"), + "mode", &wr, this, "F", true), + radius(_("Radius:"), _("Radius, in unit or %"), "radius", &wr, + this, 0.0), + chamfer_steps(_("Chamfer steps:"), _("Chamfer steps"), "chamfer_steps", + &wr, this, 1), + flexible(_("Radius in %"), _("Flexible radius size (%)"), + "flexible", &wr, this, false), + only_selected(_("Change only selected nodes"), + _("Change only selected nodes"), "only_selected", &wr, this, + false), + use_knot_distance(_("Use knots distance instead radius"), + _("Use knots distance instead radius"), + "use_knot_distance", &wr, this, true), + hide_knots(_("Hide knots"), _("Hide knots"), "hide_knots", &wr, this, + false), + apply_no_radius(_("Apply changes if radius = 0"), _("Apply changes if radius = 0"), "apply_no_radius", &wr, this, true), + apply_with_radius(_("Apply changes if radius > 0"), _("Apply changes if radius > 0"), "apply_with_radius", &wr, this, true), + _pathvector_satellites(nullptr), + _degenerate_hide(false) +{ + registerParameter(&satellites_param); + registerParameter(&unit); + registerParameter(&method); + registerParameter(&mode); + registerParameter(&radius); + registerParameter(&chamfer_steps); + registerParameter(&flexible); + registerParameter(&use_knot_distance); + registerParameter(&apply_no_radius); + registerParameter(&apply_with_radius); + registerParameter(&only_selected); + registerParameter(&hide_knots); + + radius.param_set_range(0.0, Geom::infinity()); + radius.param_set_increments(1, 1); + radius.param_set_digits(4); + radius.param_set_undo(false); + chamfer_steps.param_set_range(1, 999); + chamfer_steps.param_set_increments(1, 1); + chamfer_steps.param_set_digits(0); + _provides_knotholder_entities = true; + helperpath = false; + previous_unit = Glib::ustring(""); +} + +void LPEFilletChamfer::doOnApply(SPLPEItem const *lpeItem) +{ + SPLPEItem *splpeitem = const_cast(lpeItem); + SPShape *shape = dynamic_cast(splpeitem); + if (shape) { + Geom::PathVector const pathv = pathv_to_linear_and_cubic_beziers(shape->getCurve(true)->get_pathvector()); + Satellites satellites; + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + SatelliteType satellite_type = FILLET; + std::map gchar_map_to_satellite_type = + boost::assign::map_list_of("F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + auto mode_str = mode.param_getSVGValue(); + std::map::iterator it = gchar_map_to_satellite_type.find(mode_str.raw()); + if (it != gchar_map_to_satellite_type.end()) { + satellite_type = it->second; + } + Geom::PathVector pathvres; + for (const auto & path_it : pathv) { + if (path_it.empty() || count_path_nodes(path_it) < 2) { + continue; + } + std::vector subpath_satellites; + Geom::Path::const_iterator curve_it = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + Geom::Path pathresult(curve_it->initialPoint()); + while (curve_it != curve_endit) { + if (pathresult.size()) { + pathresult.setFinal(curve_it->initialPoint()); + } + pathresult.append(*curve_it); + ++curve_it; + Satellite satellite(satellite_type); + satellite.setSteps(chamfer_steps); + satellite.setAmount(power); + satellite.setIsTime(flexible); + satellite.setHasMirror(true); + satellite.setHidden(hide_knots); + subpath_satellites.push_back(satellite); + } + + //we add the last satellite on open path because _pathvector_satellites is related to nodes, not curves + //so maybe in the future we can need this last satellite in other effects + //don't remove for this effect because _pathvector_satellites class has methods when the path is modified + //and we want one method for all uses + if (!path_it.closed()) { + Satellite satellite(satellite_type); + satellite.setSteps(chamfer_steps); + satellite.setAmount(power); + satellite.setIsTime(flexible); + satellite.setHasMirror(true); + satellite.setHidden(hide_knots); + subpath_satellites.push_back(satellite); + } + pathresult.close(path_it.closed()); + pathvres.push_back(pathresult); + pathresult.clear(); + satellites.push_back(subpath_satellites); + } + _pathvector_satellites = new PathVectorSatellites(); + _pathvector_satellites->setPathVector(pathvres); + _pathvector_satellites->setSatellites(satellites); + satellites_param.setPathVectorSatellites(_pathvector_satellites); + } else { + g_warning("LPE Fillet/Chamfer can only be applied to shapes (not groups)."); + SPLPEItem *item = const_cast(lpeItem); + item->removeCurrentPathEffect(false); + } +} + +Gtk::Widget *LPEFilletChamfer::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = param->param_newWidget(); + if (param->param_key == "radius") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast(widg)); + widg_registered->signal_value_changed().connect( + sigc::mem_fun(*this, &LPEFilletChamfer::updateAmount)); + widg = widg_registered; + if (widg) { + Gtk::HBox *scalar_parameter = dynamic_cast(widg); + std::vector childList = scalar_parameter->get_children(); + Gtk::Entry *entry_widget = dynamic_cast(childList[1]); + entry_widget->set_width_chars(6); + } + } else if (param->param_key == "chamfer_steps") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast(widg)); + widg_registered->signal_value_changed().connect( + sigc::mem_fun(*this, &LPEFilletChamfer::updateChamferSteps)); + widg = widg_registered; + if (widg) { + Gtk::HBox *scalar_parameter = dynamic_cast(widg); + std::vector childList = scalar_parameter->get_children(); + Gtk::Entry *entry_widget = dynamic_cast(childList[1]); + entry_widget->set_width_chars(3); + } + } else if (param->param_key == "only_selected") { + Gtk::manage(widg); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + + Gtk::HBox *fillet_container = Gtk::manage(new Gtk::HBox(true, 0)); + Gtk::Button *fillet = Gtk::manage(new Gtk::Button(Glib::ustring(_("Fillet")))); + fillet->signal_clicked() + .connect(sigc::bind(sigc::mem_fun(*this, &LPEFilletChamfer::updateSatelliteType),FILLET)); + + fillet_container->pack_start(*fillet, true, true, 2); + Gtk::Button *inverse_fillet = Gtk::manage(new Gtk::Button(Glib::ustring(_("Inverse fillet")))); + inverse_fillet->signal_clicked() + .connect(sigc::bind(sigc::mem_fun(*this, &LPEFilletChamfer::updateSatelliteType),INVERSE_FILLET)); + fillet_container->pack_start(*inverse_fillet, true, true, 2); + + Gtk::HBox *chamfer_container = Gtk::manage(new Gtk::HBox(true, 0)); + Gtk::Button *chamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Chamfer")))); + chamfer->signal_clicked() + .connect(sigc::bind(sigc::mem_fun(*this, &LPEFilletChamfer::updateSatelliteType),CHAMFER)); + + chamfer_container->pack_start(*chamfer, true, true, 2); + Gtk::Button *inverse_chamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Inverse chamfer")))); + inverse_chamfer->signal_clicked() + .connect(sigc::bind(sigc::mem_fun(*this, &LPEFilletChamfer::updateSatelliteType),INVERSE_CHAMFER)); + chamfer_container->pack_start(*inverse_chamfer, true, true, 2); + + vbox->pack_start(*fillet_container, true, true, 2); + vbox->pack_start(*chamfer_container, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return vbox; +} + +void LPEFilletChamfer::refreshKnots() +{ + if (satellites_param._knoth) { + satellites_param._knoth->update_knots(); + } +} + +void LPEFilletChamfer::updateAmount() +{ + setSelected(_pathvector_satellites); + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + _pathvector_satellites->updateAmount(power, apply_no_radius, apply_with_radius, only_selected, + use_knot_distance, flexible); + satellites_param.setPathVectorSatellites(_pathvector_satellites); +} + +void LPEFilletChamfer::updateChamferSteps() +{ + setSelected(_pathvector_satellites); + _pathvector_satellites->updateSteps(chamfer_steps, apply_no_radius, apply_with_radius, only_selected); + satellites_param.setPathVectorSatellites(_pathvector_satellites); +} + +void LPEFilletChamfer::updateSatelliteType(SatelliteType satellitetype) +{ + std::map satellite_type_to_gchar_map = + boost::assign::map_list_of(FILLET, "F")(INVERSE_FILLET, "IF")(CHAMFER, "C")(INVERSE_CHAMFER, "IC")(INVALID_SATELLITE, "KO"); + mode.param_setValue((Glib::ustring)satellite_type_to_gchar_map.at(satellitetype)); + setSelected(_pathvector_satellites); + _pathvector_satellites->updateSatelliteType(satellitetype, apply_no_radius, apply_with_radius, only_selected); + satellites_param.setPathVectorSatellites(_pathvector_satellites); +} + +void LPEFilletChamfer::setSelected(PathVectorSatellites *_pathvector_satellites){ + Geom::PathVector const pathv = _pathvector_satellites->getPathVector(); + Satellites satellites = _pathvector_satellites->getSatellites(); + for (size_t i = 0; i < satellites.size(); ++i) { + for (size_t j = 0; j < satellites[i].size(); ++j) { + Geom::Curve const &curve_in = pathv[i][j]; + if (only_selected && isNodePointSelected(curve_in.initialPoint()) ){ + satellites[i][j].setSelected(true); + } else { + satellites[i][j].setSelected(false); + } + } + } + _pathvector_satellites->setSatellites(satellites); +} + +void LPEFilletChamfer::doBeforeEffect(SPLPEItem const *lpeItem) +{ + if (!pathvector_before_effect.empty()) { + //fillet chamfer specific calls + satellites_param.setUseDistance(use_knot_distance); + satellites_param.setCurrentZoom(current_zoom); + //mandatory call + satellites_param.setEffectType(effectType()); + Geom::PathVector const pathv = pathv_to_linear_and_cubic_beziers(pathvector_before_effect); + Geom::PathVector pathvres; + for (const auto &path_it : pathv) { + if (path_it.empty() || count_path_nodes(path_it) < 2) { + continue; + } + Geom::Path::const_iterator curve_it = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + Geom::Path pathresult(curve_it->initialPoint()); + while (curve_it != curve_endit) { + if (pathresult.size()) { + pathresult.setFinal(curve_it->initialPoint()); + } + if (Geom::are_near((*curve_it).initialPoint(), (*curve_it).finalPoint())) { + return; + } + pathresult.append(*curve_it); + ++curve_it; + } + pathresult.close(path_it.closed()); + pathvres.push_back(pathresult); + pathresult.clear(); + } // if are different sizes call to recalculate + Satellites satellites = satellites_param.data(); + if (satellites.empty()) { + doOnApply(lpeItem); // dont want _impl to not update versioning + satellites = satellites_param.data(); + } + bool write = false; + if (_pathvector_satellites) { + size_t number_nodes = count_pathvector_nodes(pathvres); + size_t previous_number_nodes = _pathvector_satellites->getTotalSatellites(); + if (number_nodes != previous_number_nodes) { + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + SatelliteType satellite_type = FILLET; + std::map gchar_map_to_satellite_type = + boost::assign::map_list_of("F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + auto mode_str = mode.param_getSVGValue(); + std::map::iterator it = gchar_map_to_satellite_type.find(mode_str.raw()); + if (it != gchar_map_to_satellite_type.end()) { + satellite_type = it->second; + } + Satellite satellite(satellite_type); + satellite.setSteps(chamfer_steps); + satellite.setAmount(power); + satellite.setIsTime(flexible); + satellite.setHasMirror(true); + satellite.setHidden(hide_knots); + _pathvector_satellites->recalculateForNewPathVector(pathvres, satellite); + satellites = _pathvector_satellites->getSatellites(); + write = true; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + } + } + + if (_degenerate_hide) { + satellites_param.setGlobalKnotHide(true); + } else { + satellites_param.setGlobalKnotHide(false); + } + for (size_t i = 0; i < satellites.size(); ++i) { + for (size_t j = 0; j < satellites[i].size(); ++j) { + if (j >= count_path_nodes(pathvres[i])) { + // we are on the end of a open path + // for the moment we dont want to use + // this satellite so simplest do nothing with it + continue; + } + Geom::Curve const &curve_in = pathvres[i][j]; + if (satellites[i][j].is_time != flexible) { + satellites[i][j].is_time = flexible; + double amount = satellites[i][j].amount; + if (satellites[i][j].is_time) { + double time = timeAtArcLength(amount, curve_in); + satellites[i][j].amount = time; + } else { + double size = arcLengthAt(amount, curve_in); + satellites[i][j].amount = size; + } + } + satellites[i][j].hidden = hide_knots; + if (only_selected && isNodePointSelected(curve_in.initialPoint()) ){ + satellites[i][j].setSelected(true); + } + } + if (!pathvres[i].closed()) { + satellites[i][0].amount = 0; + satellites[i][count_path_nodes(pathvres[i]) - 1].amount = 0; + } + } + if (!_pathvector_satellites) { + _pathvector_satellites = new PathVectorSatellites(); + } + _pathvector_satellites->setPathVector(pathvres); + _pathvector_satellites->setSatellites(satellites); + satellites_param.setPathVectorSatellites(_pathvector_satellites, write); + size_t number_nodes = count_pathvector_nodes(pathvres); + size_t previous_number_nodes = _pathvector_satellites->getTotalSatellites(); + if (number_nodes != previous_number_nodes) { + doOnApply(lpeItem); // dont want _impl to not update versioning + satellites = satellites_param.data(); + satellites_param.setPathVectorSatellites(_pathvector_satellites, write); + } + Glib::ustring current_unit = Glib::ustring(unit.get_abbreviation()); + if (previous_unit != current_unit && previous_unit != "") { + updateAmount(); + } + if (write) { + satellites_param.reloadKnots(); + } else { + refreshKnots(); + } + previous_unit = current_unit; + } else { + g_warning("LPE Fillet can only be applied to shapes (not groups)."); + } +} + +void +LPEFilletChamfer::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(_hp); +} + +void +LPEFilletChamfer::addChamferSteps(Geom::Path &tmp_path, Geom::Path path_chamfer, Geom::Point end_arc_point, size_t steps) +{ + setSelected(_pathvector_satellites); + double path_subdivision = 1.0 / steps; + for (size_t i = 1; i < steps; i++) { + Geom::Point chamfer_step = path_chamfer.pointAt(path_subdivision * i); + tmp_path.appendNew(chamfer_step); + } + tmp_path.appendNew(end_arc_point); +} + +Geom::PathVector +LPEFilletChamfer::doEffect_path(Geom::PathVector const &path_in) +{ + const double GAP_HELPER = 0.00001; + Geom::PathVector path_out; + size_t path = 0; + const double K = (4.0 / 3.0) * (sqrt(2.0) - 1.0); + _degenerate_hide = false; + Geom::PathVector const pathv = _pathvector_satellites->getPathVector(); + Satellites satellites = _pathvector_satellites->getSatellites(); + for (const auto &path_it : pathv) { + Geom::Path tmp_path; + + double time0 = 0; + size_t curve = 0; + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + size_t next_index = curve + 1; + if (curve == count_path_nodes(pathv[path]) - 1 && pathv[path].closed()) { + next_index = 0; + } + //append last extreme of paths on open paths + if (curve == count_path_nodes(pathv[path]) - 1 && !pathv[path].closed()) { // the path is open and we are at + // end of path + if (time0 != 1) { //Previous satellite not at 100% amount + Geom::Curve *last_curve = curve_it1->portion(time0, 1); + last_curve->setInitial(tmp_path.finalPoint()); + tmp_path.append(*last_curve); + } + ++curve_it1; + continue; + } + Geom::Curve const &curve_it2 = pathv[path][next_index]; + Satellite satellite = satellites[path][next_index]; + if (Geom::are_near((*curve_it1).initialPoint(), (*curve_it1).finalPoint())) { + _degenerate_hide = true; + g_warning("Knots hidden if consecutive nodes has the same position."); + return path_in; + } + if (!curve) { //curve == 0 + if (!path_it.closed()) { + time0 = 0; + } else { + time0 = satellites[path][0].time(*curve_it1); + } + } + double s = satellite.arcDistance(curve_it2); + double time1 = satellite.time(s, true, (*curve_it1)); + double time2 = satellite.time(curve_it2); + if (time1 <= time0) { + time1 = time0; + } + if (time2 > 1) { + time2 = 1; + } + Geom::Curve *knot_curve_1 = curve_it1->portion(time0, time1); + Geom::Curve *knot_curve_2 = curve_it2.portion(time2, 1); + if (curve > 0) { + knot_curve_1->setInitial(tmp_path.finalPoint()); + } else { + tmp_path.start((*curve_it1).pointAt(time0)); + } + + Geom::Point start_arc_point = knot_curve_1->finalPoint(); + Geom::Point end_arc_point = curve_it2.pointAt(time2); + //add a gap helper + if (time2 == 1) { + end_arc_point = curve_it2.pointAt(time2 - GAP_HELPER); + } + if (time1 == time0) { + start_arc_point = curve_it1->pointAt(time1 + GAP_HELPER); + } + + double k1 = distance(start_arc_point, curve_it1->finalPoint()) * K; + double k2 = distance(curve_it2.initialPoint(), end_arc_point) * K; + Geom::CubicBezier const *cubic_1 = dynamic_cast(&*knot_curve_1); + Geom::CubicBezier const *cubic_2 = dynamic_cast(&*knot_curve_2); + Geom::Ray ray_1(start_arc_point, curve_it1->finalPoint()); + Geom::Ray ray_2(curve_it2.initialPoint(), end_arc_point); + if (cubic_1) { + ray_1.setPoints((*cubic_1)[2], start_arc_point); + } + if (cubic_2) { + ray_2.setPoints(end_arc_point, (*cubic_2)[1]); + } + bool ccw_toggle = cross(curve_it1->finalPoint() - start_arc_point, end_arc_point - start_arc_point) < 0; + double angle = angle_between(ray_1, ray_2, ccw_toggle); + double handle_angle_1 = ray_1.angle() - angle; + double handle_angle_2 = ray_2.angle() + angle; + if (ccw_toggle) { + handle_angle_1 = ray_1.angle() + angle; + handle_angle_2 = ray_2.angle() - angle; + } + Geom::Point handle_1 = Geom::Point::polar(ray_1.angle(), k1) + start_arc_point; + Geom::Point handle_2 = end_arc_point - Geom::Point::polar(ray_2.angle(), k2); + Geom::Point inverse_handle_1 = Geom::Point::polar(handle_angle_1, k1) + start_arc_point; + Geom::Point inverse_handle_2 = end_arc_point - Geom::Point::polar(handle_angle_2, k2); + if (time0 == 1) { + handle_1 = start_arc_point; + inverse_handle_1 = start_arc_point; + } + //remove gap helper + if (time2 == 1) { + end_arc_point = curve_it2.pointAt(time2); + } + if (time1 == time0) { + start_arc_point = curve_it1->pointAt(time0); + } + if (time1 != 1 && !Geom::are_near(angle,Geom::rad_from_deg(360))) { + if (time1 != time0 || (time1 == 1 && time0 == 1)) { + if (!knot_curve_1->isDegenerate()) { + tmp_path.append(*knot_curve_1); + } + } + SatelliteType type = satellite.satellite_type; + size_t steps = satellite.steps; + if (!steps) steps = 1; + Geom::Line const x_line(Geom::Point(0, 0), Geom::Point(1, 0)); + Geom::Line const angled_line(start_arc_point, end_arc_point); + double arc_angle = Geom::angle_between(x_line, angled_line); + double radius = Geom::distance(start_arc_point, middle_point(start_arc_point, end_arc_point)) / + sin(angle / 2.0); + Geom::Coord rx = radius; + Geom::Coord ry = rx; + bool eliptical = (is_straight_curve(*curve_it1) && + is_straight_curve(curve_it2) && method != FM_BEZIER) || + method == FM_ARC; + switch (type) { + case CHAMFER: + { + Geom::Path path_chamfer; + path_chamfer.start(tmp_path.finalPoint()); + if (eliptical) { + ccw_toggle = ccw_toggle ? false : true; + path_chamfer.appendNew(rx, ry, arc_angle, 0, ccw_toggle, end_arc_point); + } else { + path_chamfer.appendNew(handle_1, handle_2, end_arc_point); + } + addChamferSteps(tmp_path, path_chamfer, end_arc_point, steps); + } + break; + case INVERSE_CHAMFER: + { + Geom::Path path_chamfer; + path_chamfer.start(tmp_path.finalPoint()); + if (eliptical) { + path_chamfer.appendNew(rx, ry, arc_angle, 0, ccw_toggle, end_arc_point); + } else { + path_chamfer.appendNew(inverse_handle_1, inverse_handle_2, end_arc_point); + } + addChamferSteps(tmp_path, path_chamfer, end_arc_point, steps); + } + break; + case INVERSE_FILLET: + { + if (eliptical) { + tmp_path.appendNew(rx, ry, arc_angle, 0, ccw_toggle, end_arc_point); + } else { + tmp_path.appendNew(inverse_handle_1, inverse_handle_2, end_arc_point); + } + } + break; + default: //fillet + { + if (eliptical) { + ccw_toggle = ccw_toggle ? false : true; + tmp_path.appendNew(rx, ry, arc_angle, 0, ccw_toggle, end_arc_point); + } else { + tmp_path.appendNew(handle_1, handle_2, end_arc_point); + } + } + break; + } + } else { + if (!knot_curve_1->isDegenerate()) { + tmp_path.append(*knot_curve_1); + } + } + curve++; + ++curve_it1; + time0 = time2; + } + if (path_it.closed()) { + tmp_path.close(); + } + path++; + path_out.push_back(tmp_path); + } + if (helperpath) { + _hp = path_out; + return pathvector_after_effect; + } + _hp.clear(); + return path_out; +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offset:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-fillet-chamfer.h b/src/live_effects/lpe-fillet-chamfer.h new file mode 100644 index 0000000..71f0f5e --- /dev/null +++ b/src/live_effects/lpe-fillet-chamfer.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILLET_CHAMFER_H +#define INKSCAPE_LPE_FILLET_CHAMFER_H + +/* + * Author(s): + * Jabiertxo Arraiza Cenoz + * + * Copyright (C) 2014 Author(s) + * + * Jabiertxof:Thanks to all people help me + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/satellitesarray.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/unit.h" +#include "live_effects/parameter/hidden.h" +#include "helper/geom-pathvectorsatellites.h" +#include "helper/geom-satellite.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum Filletmethod { + FM_AUTO, + FM_ARC, + FM_BEZIER, + FM_END +}; + +class LPEFilletChamfer : public Effect { +public: + LPEFilletChamfer(LivePathEffectObject *lpeobject); + void doBeforeEffect(SPLPEItem const *lpeItem) override; + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + void doOnApply(SPLPEItem const *lpeItem) override; + Gtk::Widget *newWidget() override; + Geom::Ray getRay(Geom::Point start, Geom::Point end, Geom::Curve *curve, bool reverse); + void addChamferSteps(Geom::Path &tmp_path, Geom::Path path_chamfer, Geom::Point end_arc_point, size_t steps); + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; + void updateSatelliteType(SatelliteType satellitetype); + void setSelected(PathVectorSatellites *_pathvector_satellites); + //void convertUnit(); + void updateChamferSteps(); + void updateAmount(); + void refreshKnots(); + bool helperpath; + SatellitesArrayParam satellites_param; + +private: + UnitParam unit; + EnumParam method; + ScalarParam radius; + ScalarParam chamfer_steps; + BoolParam flexible; + HiddenParam mode; + BoolParam only_selected; + BoolParam use_knot_distance; + BoolParam hide_knots; + BoolParam apply_no_radius; + BoolParam apply_with_radius; + bool _degenerate_hide; + PathVectorSatellites *_pathvector_satellites; + Geom::PathVector _hp; + Glib::ustring previous_unit; + LPEFilletChamfer(const LPEFilletChamfer &); + LPEFilletChamfer &operator=(const LPEFilletChamfer &); + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-gears.cpp b/src/live_effects/lpe-gears.cpp new file mode 100644 index 0000000..a7bbfd5 --- /dev/null +++ b/src/live_effects/lpe-gears.cpp @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Copyright 2006 Michael G. Sloan + * Copyright 2006 Aaron Spike + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-gears.h" +#include <2geom/bezier-to-sbasis.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using namespace Geom; + +class Gear { +public: + // pitch circles touch on two properly meshed gears + // all measurements are taken from the pitch circle + double pitch_diameter() {return (_number_of_teeth * _module) / M_PI;} + double pitch_radius() {return pitch_diameter() / 2.0;} + void pitch_radius(double R) {_module = (2 * M_PI * R) / _number_of_teeth;} + + // base circle serves as the basis for the involute toothe profile + double base_diameter() {return pitch_diameter() * cos(_pressure_angle);} + double base_radius() {return base_diameter() / 2.0;} + + // diametrical pitch + double diametrical_pitch() {return _number_of_teeth / pitch_diameter();} + + // height of the tooth above the pitch circle + double addendum() {return 1.0 / diametrical_pitch();} + // depth of the tooth below the pitch circle + double dedendum() {return addendum() + _clearance;} + + // root circle specifies the bottom of the fillet between teeth + double root_radius() {return pitch_radius() - dedendum();} + double root_diameter() {return root_radius() * 2.0;} + + // outer circle is the outside diameter of the gear + double outer_radius() {return pitch_radius() + addendum();} + double outer_diameter() {return outer_radius() * 2.0;} + + // angle covered by the tooth on the pitch circle + double tooth_thickness_angle() {return M_PI / _number_of_teeth;} + + Geom::Point centre() {return _centre;} + void centre(Geom::Point c) {_centre = c;} + + double angle() {return _angle;} + void angle(double a) {_angle = a;} + + int number_of_teeth() {return _number_of_teeth;} + + Geom::Path path(); + Gear spawn(Geom::Point p); + + Gear(int n, double m, double phi) { + _number_of_teeth = n; + _module = m; + _pressure_angle = phi; + _clearance = 0.0; + _angle = 0.0; + _centre = Geom::Point(0.0,0.0); + } +private: + int _number_of_teeth; + double _pressure_angle; + double _module; + double _clearance; + double _angle; + Geom::Point _centre; + D2 _involute(double start, double stop) { + D2 B; + D2 I; + Linear bo = Linear(start,stop); + + B[0] = cos(bo,2); + B[1] = sin(bo,2); + + I = B - Linear(0,1) * derivative(B); + I = I*base_radius() + _centre; + return I; + } + D2 _arc(double start, double stop, double R) { + D2 B; + Linear bo = Linear(start,stop); + + B[0] = cos(bo,2); + B[1] = sin(bo,2); + + B = B*R + _centre; + return B; + } + // angle of the base circle used to create the involute to a certain radius + double involute_swath_angle(double R) { + if (R <= base_radius()) return 0.0; + return sqrt(R*R - base_radius()*base_radius())/base_radius(); + } + + // angle of the base circle between the origin of the involute and the intersection on another radius + double involute_intersect_angle(double R) { + if (R <= base_radius()) return 0.0; + return (sqrt(R*R - base_radius()*base_radius())/base_radius()) - acos(base_radius()/R); + } +}; + +static void +makeContinuous(D2 &a, Point const b) { + for(unsigned d=0;d<2;d++) + a[d][0][0] = b[d]; +} + +Geom::Path Gear::path() { + Geom::Path pb; + + // angle covered by a full tooth and fillet + double tooth_rotation = 2.0 * tooth_thickness_angle(); + // angle covered by an involute + double involute_advance = involute_intersect_angle(outer_radius()) - involute_intersect_angle(root_radius()); + // angle covered by the tooth tip + double tip_advance = tooth_thickness_angle() - (2 * (involute_intersect_angle(outer_radius()) - involute_intersect_angle(pitch_radius()))); + // angle covered by the toothe root + double root_advance = (tooth_rotation - tip_advance) - (2.0 * involute_advance); + // begin drawing the involute at t if the root circle is larger than the base circle + double involute_t = involute_swath_angle(root_radius())/involute_swath_angle(outer_radius()); + + //rewind angle to start drawing from the leading edge of the tooth + double first_tooth_angle = _angle - ((0.5 * tip_advance) + involute_advance); + + Geom::Point prev; + for (int i=0; i < _number_of_teeth; i++) + { + double cursor = first_tooth_angle + (i * tooth_rotation); + + D2 leading_I = compose(_involute(cursor, cursor + involute_swath_angle(outer_radius())), Linear(involute_t,1)); + if(i != 0) makeContinuous(leading_I, prev); + pb.append(SBasisCurve(leading_I)); + cursor += involute_advance; + prev = leading_I.at1(); + + D2 tip = _arc(cursor, cursor+tip_advance, outer_radius()); + makeContinuous(tip, prev); + pb.append(SBasisCurve(tip)); + cursor += tip_advance; + prev = tip.at1(); + + cursor += involute_advance; + D2 trailing_I = compose(_involute(cursor, cursor - involute_swath_angle(outer_radius())), Linear(1,involute_t)); + makeContinuous(trailing_I, prev); + pb.append(SBasisCurve(trailing_I)); + prev = trailing_I.at1(); + + if (base_radius() > root_radius()) { + Geom::Point leading_start = trailing_I.at1(); + Geom::Point leading_end = (root_radius() * unit_vector(leading_start - _centre)) + _centre; + prev = leading_end; + pb.appendNew(leading_end); + } + + D2 root = _arc(cursor, cursor+root_advance, root_radius()); + makeContinuous(root, prev); + pb.append(SBasisCurve(root)); + //cursor += root_advance; + prev = root.at1(); + + if (base_radius() > root_radius()) { + Geom::Point trailing_start = root.at1(); + Geom::Point trailing_end = (base_radius() * unit_vector(trailing_start - _centre)) + _centre; + pb.appendNew(trailing_end); + prev = trailing_end; + } + } + + return pb; +} + +Gear Gear::spawn(Geom::Point p) { + double radius = Geom::distance(this->centre(), p) - this->pitch_radius(); + int N = (int) floor( (radius / this->pitch_radius()) * this->number_of_teeth() ); + + Gear gear(N, _module, _pressure_angle); + gear.centre(p); + + double a = atan2(p - this->centre()); + double new_angle = 0.0; + if (gear.number_of_teeth() % 2 == 0) + new_angle -= gear.tooth_thickness_angle(); + new_angle -= (_angle) * (pitch_radius() / gear.pitch_radius()); + new_angle += (a) * (pitch_radius() / gear.pitch_radius()); + gear.angle(new_angle + a); + return gear; +} + + + +// ################################################################# + + + +namespace Inkscape { +namespace LivePathEffect { + + +LPEGears::LPEGears(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + teeth(_("_Teeth:"), _("The number of teeth"), "teeth", &wr, this, 10), + phi(_("_Phi:"), _("Tooth pressure angle (typically 20-25 deg). The ratio of teeth not in contact."), "phi", &wr, this, 5), + min_radius(_("Min Radius:"), _("Minimum radius, low values can be slow"), "min_radius", &wr, this, 5.0) +{ + /* Tooth pressure angle: The angle between the tooth profile and a perpendicular to the pitch + * circle, usually at the point where the pitch circle meets the tooth profile. Standard angles + * are 20 and 25 degrees. The pressure angle affects the force that tends to separate mating + * gears. A high pressure angle means that higher ratio of teeth not in contact. However, this + * allows the teeth to have higher capacity and also allows fewer teeth without undercutting. + */ + + teeth.param_make_integer(); + teeth.param_set_range(3, 1e10); + min_radius.param_set_range(0.01, 9999.0); + registerParameter(&teeth); + registerParameter(&phi); + registerParameter(&min_radius); +} + +LPEGears::~LPEGears() += default; + +Geom::PathVector +LPEGears::doEffect_path (Geom::PathVector const &path_in) +{ + Geom::PathVector path_out; + Geom::Path gearpath = path_in[0]; + + Geom::Path::iterator it(gearpath.begin()); + if ( it == gearpath.end() ) return path_out; + + Gear * gear = new Gear(teeth, 200.0, phi * M_PI / 180); + Geom::Point gear_centre = (*it).finalPoint(); + gear->centre(gear_centre); + gear->angle(atan2((*it).initialPoint() - gear_centre)); + + ++it; + if ( it == gearpath.end() ) return path_out; + double radius = Geom::distance(gear_centre, (*it).finalPoint()); + radius = radius < min_radius?min_radius:radius; + gear->pitch_radius(radius); + + path_out.push_back( gear->path()); + + for (++it; it != gearpath.end() ; ++it) { + if (are_near((*it).initialPoint(), (*it).finalPoint())) { + continue; + } + // iterate through Geom::Curve in path_in + Gear* gearnew = new Gear(gear->spawn( (*it).finalPoint() )); + path_out.push_back( gearnew->path() ); + delete gear; + gear = gearnew; + } + delete gear; + + return path_out; +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-gears.h b/src/live_effects/lpe-gears.h new file mode 100644 index 0000000..eb5ec8a --- /dev/null +++ b/src/live_effects/lpe-gears.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_GEARS_H +#define INKSCAPE_LPE_GEARS_H + +/* + * Inkscape::LPEGears + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEGears : public Effect { +public: + LPEGears(LivePathEffectObject *lpeobject); + ~LPEGears() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + +private: + ScalarParam teeth; + ScalarParam phi; + ScalarParam min_radius; + + LPEGears(const LPEGears&) = delete; + LPEGears& operator=(const LPEGears&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-interpolate.cpp b/src/live_effects/lpe-interpolate.cpp new file mode 100644 index 0000000..7012aa5 --- /dev/null +++ b/src/live_effects/lpe-interpolate.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE interpolate implementation + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007-2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-interpolate.h" + +#include <2geom/sbasis-to-bezier.h> + +#include "display/curve.h" + +#include "object/sp-path.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +namespace Inkscape { +namespace LivePathEffect { + +LPEInterpolate::LPEInterpolate(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , trajectory_path(_("Trajectory:"), _("Path along which intermediate steps are created."), "trajectory", &wr, this, + "M0,0 L0,0") + , number_of_steps(_("Steps_:"), _("Determines the number of steps from start to end path."), "steps", &wr, this, 5) + , equidistant_spacing(_("E_quidistant spacing"), + _("If true, the spacing between intermediates is constant along the length of the path. If " + "false, the distance depends on the location of the nodes of the trajectory path."), + "equidistant_spacing", &wr, this, true) +{ + show_orig_path = true; + + registerParameter(&trajectory_path); + registerParameter(&equidistant_spacing); + registerParameter(&number_of_steps); + + number_of_steps.param_make_integer(); + number_of_steps.param_set_range(2, Geom::infinity()); +} + +LPEInterpolate::~LPEInterpolate() = default; + +void LPEInterpolate::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + trajectory_path.param_transform_multiply(postmul, false); +} + +/* + * interpolate path_in[0] to path_in[1] + */ +Geom::PathVector LPEInterpolate::doEffect_path(Geom::PathVector const &path_in) +{ + if ((path_in.size() < 2) || (number_of_steps < 2)) { + return path_in; + } + // Don't allow empty path parameter: + if (trajectory_path.get_pathvector().empty()) { + return path_in; + } + + Geom::PathVector path_out; + + Geom::Piecewise > pwd2_A = path_in[0].toPwSb(); + Geom::Piecewise > pwd2_B = path_in[1].toPwSb(); + + // Transform both paths to (0,0) midpoint, so they can easily be positioned along interpolate_path + Geom::OptRect bounds_A = Geom::bounds_exact(pwd2_A); + if (bounds_A) { + pwd2_A -= bounds_A->midpoint(); + } + Geom::OptRect bounds_B = Geom::bounds_exact(pwd2_B); + if (bounds_B) { + pwd2_B -= bounds_B->midpoint(); + } + + // Make sure both paths have the same number of segments and cuts at the same locations + pwd2_B.setDomain(pwd2_A.domain()); + Geom::Piecewise > pA = Geom::partition(pwd2_A, pwd2_B.cuts); + Geom::Piecewise > pB = Geom::partition(pwd2_B, pwd2_A.cuts); + + auto trajectory = calculate_trajectory(bounds_A, bounds_B); + + Geom::Interval trajectory_domain = trajectory.domain(); + + for (int i = 0; i < number_of_steps; ++i) { + double fraction = i / (number_of_steps - 1); + + Geom::Piecewise > pResult = pA * (1 - fraction) + pB * fraction; + pResult += trajectory.valueAt(trajectory_domain.min() + fraction * trajectory_domain.extent()); + + Geom::PathVector pathv = Geom::path_from_piecewise(pResult, LPE_CONVERSION_TOLERANCE); + path_out.push_back(pathv[0]); + } + + return path_out; +} + + +// returns the lpe parameter trajectory_path, transformed so that it starts at the +// bounding box center of the first path and ends at the bounding box center of the +// second path +Geom::Piecewise > LPEInterpolate::calculate_trajectory(Geom::OptRect bounds_A, + Geom::OptRect bounds_B) +{ + Geom::Piecewise > trajectory = trajectory_path.get_pathvector()[0].toPwSb(); + + if (equidistant_spacing) { + trajectory = Geom::arc_length_parametrization(trajectory); + } + + if (!bounds_A || !bounds_B) { + return trajectory; + } + + auto trajectory_start = trajectory.firstValue(); + auto trajectory_end = trajectory.lastValue(); + + auto midpoint_A = bounds_A->midpoint(); + auto midpoint_B = bounds_B->midpoint(); + + Geom::Ray original(trajectory_start, trajectory_end); + Geom::Ray transformed(midpoint_A, midpoint_B); + + double rotation = transformed.angle() - original.angle(); + double scale = Geom::distance(midpoint_A, midpoint_B) / Geom::distance(trajectory_start, trajectory_end); + + Geom::Affine transformation; + + transformation *= Geom::Translate(-trajectory_start); + transformation *= Geom::Scale(scale, scale); + transformation *= Geom::Rotate(rotation); + + transformation *= Geom::Translate(midpoint_A); + + return trajectory * transformation; +} + +void LPEInterpolate::resetDefaults(SPItem const *item) +{ + Effect::resetDefaults(item); + + if (!SP_IS_PATH(item)) + return; + + SPCurve const *crv = SP_PATH(item)->getCurveForEdit(true); + Geom::PathVector const &pathv = crv->get_pathvector(); + if ((pathv.size() < 2)) + return; + + Geom::OptRect bounds_A = pathv[0].boundsExact(); + Geom::OptRect bounds_B = pathv[1].boundsExact(); + + if (bounds_A && bounds_B) { + Geom::PathVector traj_pathv; + traj_pathv.push_back(Geom::Path()); + traj_pathv[0].start(bounds_A->midpoint()); + traj_pathv[0].appendNew(bounds_B->midpoint()); + trajectory_path.set_new_value(traj_pathv, true); + } + else { + trajectory_path.param_set_and_write_default(); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-interpolate.h b/src/live_effects/lpe-interpolate.h new file mode 100644 index 0000000..1c94396 --- /dev/null +++ b/src/live_effects/lpe-interpolate.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_INTERPOLATE_H +#define INKSCAPE_LPE_INTERPOLATE_H + +/** \file + * LPE interpolate implementation, see lpe-interpolate.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007-2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEInterpolate : public Effect { + public: + LPEInterpolate(LivePathEffectObject *lpeobject); + ~LPEInterpolate() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void resetDefaults(SPItem const *item) override; + + private: + PathParam trajectory_path; + ScalarParam number_of_steps; + BoolParam equidistant_spacing; + + Geom::Piecewise > calculate_trajectory(Geom::OptRect bounds_A, Geom::OptRect bounds_B); + + LPEInterpolate(const LPEInterpolate &) = delete; + LPEInterpolate &operator=(const LPEInterpolate &) = delete; +}; + +} // namespace LivePathEffect +} // namespace Inkscape + +#endif // INKSCAPE_LPE_INTERPOLATE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-interpolate_points.cpp b/src/live_effects/lpe-interpolate_points.cpp new file mode 100644 index 0000000..1d84811 --- /dev/null +++ b/src/live_effects/lpe-interpolate_points.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE interpolate_points implementation + * Interpolates between knots of the input path. + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-interpolate_points.h" +#include "live_effects/lpe-powerstroke-interpolators.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + + +static const Util::EnumData InterpolatorTypeData[] = { + {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"}, + {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"}, + {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"}, + {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"}, + {Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM, N_("Centripetal Catmull-Rom"), "CentripetalCatmullRom"} +}; +static const Util::EnumDataConverter InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData)); + +LPEInterpolatePoints::LPEInterpolatePoints(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , interpolator_type( + _("Interpolator type:"), + _("Determines which kind of interpolator will be used to interpolate between stroke width along the path"), + "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM) +{ + show_orig_path = false; + + registerParameter( &interpolator_type ); +} + +LPEInterpolatePoints::~LPEInterpolatePoints() += default; + +Geom::PathVector +LPEInterpolatePoints::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + std::unique_ptr interpolator( Geom::Interpolate::Interpolator::create(static_cast(interpolator_type.get_value())) ); + + for(const auto & path_it : path_in) { + if (path_it.empty()) + continue; + + if (path_it.closed()) { + g_warning("Interpolate points LPE currently ignores whether path is closed or not."); + } + + std::vector pts; + pts.push_back(path_it.initialPoint()); + + for (Geom::Path::const_iterator it = path_it.begin(), e = path_it.end_default(); it != e; ++it) { + pts.push_back((*it).finalPoint()); + } + + Geom::Path path = interpolator->interpolateToPath(pts); + + path_out.push_back(path); + } + + return path_out; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-interpolate_points.h b/src/live_effects/lpe-interpolate_points.h new file mode 100644 index 0000000..9b563cc --- /dev/null +++ b/src/live_effects/lpe-interpolate_points.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_INTERPOLATEPOINTS_H +#define INKSCAPE_LPE_INTERPOLATEPOINTS_H + +/** \file + * LPE interpolate_points implementation, see lpe-interpolate_points.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEInterpolatePoints : public Effect { +public: + LPEInterpolatePoints(LivePathEffectObject *lpeobject); + ~LPEInterpolatePoints() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + EnumParam interpolator_type; + + LPEInterpolatePoints(const LPEInterpolatePoints&) = delete; + LPEInterpolatePoints& operator=(const LPEInterpolatePoints&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif // INKSCAPE_LPE_INTERPOLATEPOINTS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-jointype.cpp b/src/live_effects/lpe-jointype.cpp new file mode 100644 index 0000000..7617f54 --- /dev/null +++ b/src/live_effects/lpe-jointype.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * + * Liam P White + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "helper/geom-pathstroke.h" + +#include "desktop-style.h" + +#include "display/curve.h" + +#include "object/sp-item-group.h" +#include "object/sp-shape.h" +#include "style.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include <2geom/elliptical-arc.h> + +#include "lpe-jointype.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData JoinTypeData[] = { + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_MITER_CLIP, N_("Miter Clip"), "miter-clip"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"}, + {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"}, + {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"}, + {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"}, +}; + +static const Util::EnumData CapTypeData[] = { + {BUTT_FLAT, N_("Butt"), "butt"}, + {BUTT_ROUND, N_("Rounded"), "round"}, + {BUTT_SQUARE, N_("Square"), "square"}, + {BUTT_PEAK, N_("Peak"), "peak"}, + //{BUTT_LEANED, N_("Leaned"), "leaned"} +}; + +static const Util::EnumDataConverter CapTypeConverter(CapTypeData, sizeof(CapTypeData)/sizeof(*CapTypeData)); +static const Util::EnumDataConverter JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData)); + +LPEJoinType::LPEJoinType(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + line_width(_("Line width"), _("Thickness of the stroke"), "line_width", &wr, this, 1.), + linecap_type(_("Line cap"), _("The end shape of the stroke"), "linecap_type", CapTypeConverter, &wr, this, BUTT_FLAT), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE), + //start_lean(_("Start path lean"), _("Start path lean"), "start_lean", &wr, this, 0.), + //end_lean(_("End path lean"), _("End path lean"), "end_lean", &wr, this, 0.), + miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 100.), + attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, true) +{ + show_orig_path = true; + registerParameter(&linecap_type); + registerParameter(&line_width); + registerParameter(&linejoin_type); + //registerParameter(&start_lean); + //registerParameter(&end_lean); + registerParameter(&miter_limit); + registerParameter(&attempt_force_join); + //start_lean.param_set_range(-1,1); + //start_lean.param_set_increments(0.1, 0.1); + //start_lean.param_set_digits(4); + //end_lean.param_set_range(-1,1); + //end_lean.param_set_increments(0.1, 0.1); + //end_lean.param_set_digits(4); +} + +LPEJoinType::~LPEJoinType() += default; + +//from LPEPowerStroke -- sets fill if stroke color because we will +//be converting to a fill to make the new join. + +void LPEJoinType::doOnApply(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPLPEItem* item = const_cast(lpeitem); + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (lpeitem->style->stroke.isPaintserver()) { + SPPaintServer * server = lpeitem->style->getStrokePaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property (css, "fill", str.c_str()); + } + } else if (lpeitem->style->stroke.isColor()) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), lpeitem->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->stroke_opacity.value))); + sp_repr_css_set_property (css, "fill", c); + } else { + sp_repr_css_set_property (css, "fill", "none"); + } + + sp_repr_css_set_property(css, "fill-rule", "nonzero"); + sp_repr_css_set_property(css, "stroke", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)"line_width"; + bool valid = prefs->getEntry(pref_path).isValid(); + if(!valid){ + line_width.param_set_value(width); + } + line_width.write_to_SVG(); + } +} + +void LPEJoinType::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + line_width.param_transform_multiply(postmul, false); + } +} + +//from LPEPowerStroke -- sets stroke color from existing fill color + +void LPEJoinType::doOnRemove(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem)) { + SPLPEItem *item = const_cast(lpeitem); + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (lpeitem->style->fill.isPaintserver()) { + SPPaintServer * server = lpeitem->style->getFillPaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property (css, "stroke", str.c_str()); + } + } else if (lpeitem->style->fill.isColor()) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), lpeitem->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->fill_opacity.value))); + sp_repr_css_set_property (css, "stroke", c); + } else { + sp_repr_css_set_property (css, "stroke", "none"); + } + + Inkscape::CSSOStringStream os; + os << fabs(line_width); + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + + sp_repr_css_set_property(css, "fill", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + item->updateRepr(); + } +} + +Geom::PathVector LPEJoinType::doEffect_path(Geom::PathVector const & path_in) +{ + Geom::PathVector ret; + for (const auto & i : path_in) { + Geom::PathVector tmp = Inkscape::outline(i, line_width, + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), + static_cast(linecap_type.get_value())); + ret.insert(ret.begin(), tmp.begin(), tmp.end()); + } + + return ret; +} + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/live_effects/lpe-jointype.h b/src/live_effects/lpe-jointype.h new file mode 100644 index 0000000..5d5e20c --- /dev/null +++ b/src/live_effects/lpe-jointype.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Liam P White + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file COPYING for more information + */ + +#ifndef INKSCAPE_LPE_JOINTYPE_H +#define INKSCAPE_LPE_JOINTYPE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/enum.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEJoinType : public Effect { +public: + LPEJoinType(LivePathEffectObject *lpeobject); + ~LPEJoinType() override; + + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPEJoinType(const LPEJoinType&) = delete; + LPEJoinType& operator=(const LPEJoinType&) = delete; + + ScalarParam line_width; + EnumParam linecap_type; + EnumParam linejoin_type; + //ScalarParam start_lean; + //ScalarParam end_lean; + ScalarParam miter_limit; + BoolParam attempt_force_join; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/live_effects/lpe-knot.cpp b/src/live_effects/lpe-knot.cpp new file mode 100644 index 0000000..c40eaea --- /dev/null +++ b/src/live_effects/lpe-knot.cpp @@ -0,0 +1,735 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE knot effect implementation. + */ +/* Authors: + * Jean-Francois Barraud + * Abhishek Sharma + * Johan Engelen + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" +#include "live_effects/lpe-knot.h" +#include "knot-holder-entity.h" +#include "knotholder.h" + +#include + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/basic-intersection.h> +#include "helper/geom.h" + +#include "object/sp-shape.h" +#include "object/sp-path.h" +#include "style.h" + +// for change crossing undo +#include "verbs.h" +#include "document.h" +#include "document-undo.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +class KnotHolderEntityCrossingSwitcher : public LPEKnotHolderEntity { +public: + KnotHolderEntityCrossingSwitcher(LPEKnot *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + void knot_click(guint state) override; +}; + + +static Geom::Path::size_type size_nondegenerate(Geom::Path const &path) { + Geom::Path::size_type retval = path.size_default(); + const Geom::Curve &closingline = path.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + retval = path.size_open(); + } + return retval; +} + +//--------------------------------------------------------------------------- +//LPEKnot specific Interval manipulation. +//--------------------------------------------------------------------------- + +//remove an interval from an union of intervals. +//TODO: is it worth moving it to 2Geom? +static +std::vector complementOf(Geom::Interval I, std::vector domain){ + std::vector ret; + if (!domain.empty()) { + double min = domain.front().min(); + double max = domain.back().max(); + Geom::Interval I1 = Geom::Interval(min,I.min()); + Geom::Interval I2 = Geom::Interval(I.max(),max); + + for (auto i : domain){ + boost::optional I1i = intersect(i,I1); + if (I1i && !I1i->isSingular()) ret.push_back(I1i.get()); + boost::optional I2i = intersect(i,I2); + if (I2i && !I2i->isSingular()) ret.push_back(I2i.get()); + } + } + return ret; +} + +//find the time interval during which patha is hidden by pathb near a given crossing. +// Warning: not accurate! +static +Geom::Interval +findShadowedTime(Geom::Path const &patha, std::vector const &pt_and_dir, + double const ta, double const width){ + using namespace Geom; + Point T = unit_vector(pt_and_dir[1]); + Point N = T.cw(); + //Point A = pt_and_dir[0] - 3 * width * T; + //Point B = A+6*width*T; + + Affine mat = from_basis( T, N, pt_and_dir[0] ); + mat = mat.inverse(); + Geom::Path p = patha * mat; + + std::vector times; + + //TODO: explore the path fwd/backward from ta (worth?) + for (unsigned i = 0; i < size_nondegenerate(patha); i++){ + D2 f = p[i].toSBasis(); + std::vector times_i, temptimes; + temptimes = roots(f[Y]-width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[Y]+width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[X]-3*width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[X]+3*width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + for (double & k : times_i){ + k+=i; + } + times.insert(times.end(), times_i.begin(), times_i.end() ); + } + std::sort( times.begin(), times.end() ); + std::vector::iterator new_end = std::unique( times.begin(), times.end() ); + times.resize( new_end - times.begin() ); + + double tmin = 0, tmax = size_nondegenerate(patha); + double period = size_nondegenerate(patha); + if (!times.empty()){ + unsigned rk = upper_bound( times.begin(), times.end(), ta ) - times.begin(); + if ( rk < times.size() ) + tmax = times[rk]; + else if ( patha.closed() ) + tmax = times[0]+period; + + if ( rk > 0 ) + tmin = times[rk-1]; + else if ( patha.closed() ) + tmin = times.back()-period; + } + return Interval(tmin,tmax); +} + +//--------------------------------------------------------------------------- +//LPEKnot specific Crossing Data manipulation. +//--------------------------------------------------------------------------- + +//Yet another crossing data representation. +// an CrossingPoint stores +// -an intersection point +// -the involved path components +// -for each component, the time at which this crossing occurs + the order of this crossing along the component (when starting from 0). + +namespace LPEKnotNS {//just in case... +CrossingPoints::CrossingPoints(Geom::PathVector const &paths) : std::vector(){ +// std::cout<<"\nCrossingPoints creation from path vector\n"; + for( unsigned i=0; i > times; + if ( (i==j) && (ii==jj) ) { + +// std::cout<<"--(self int)\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::Y] <<"\n"; + + find_self_intersections( times, paths[i][ii].toSBasis() ); + } else { +// std::cout<<"--(pair int)\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::Y] <<"\n"; +// std::cout<<"with\n"; +// std::cout << paths[j][jj].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[j][jj].toSBasis()[Geom::Y] <<"\n"; + + find_intersections( times, paths[i][ii].toSBasis(), paths[j][jj].toSBasis() ); + } + for (auto & time : times){ + //std::cout<<"intersection "< cuts; + for( unsigned k=0; k const &input) : std::vector() +{ + if ( (input.size() > 0) && (input.size()%9 == 0) ){ + using namespace Geom; + for( unsigned n=0; n +CrossingPoints::to_vector() +{ + using namespace Geom; + std::vector result; + for( unsigned n=0; n dist_k) ) { + result = k; + dist = dist_k; + } + } + return result; +} + +//TODO: Find a way to warn the user when the topology changes. +//TODO: be smarter at guessing the signs when the topology changed? +void +CrossingPoints::inherit_signs(CrossingPoints const &other, int default_value) +{ + bool topo_changed = false; + for (unsigned n=0; n < size(); n++){ + if ( (n < other.size()) + && (other[n].i == (*this)[n].i) + && (other[n].j == (*this)[n].j) + && (other[n].ni == (*this)[n].ni) + && (other[n].nj == (*this)[n].nj) ) + { + (*this)[n].sign = other[n].sign; + } else { + topo_changed = true; + break; + } + } + if (topo_changed) { + //TODO: Find a way to warn the user!! +// std::cout<<"knot topolgy changed!\n"; + for (unsigned n=0; n < size(); n++){ + Geom::Point p = (*this)[n].pt; + unsigned idx = idx_of_nearest(other,p); + if (idx < other.size()) { + (*this)[n].sign = other[idx].sign; + } else { + (*this)[n].sign = default_value; + } + } + } +} + +} + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +//LPEKnot effect. +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- + + +LPEKnot::LPEKnot(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , + // initialise your parameters here: + interruption_width(_("_Gap length:"), _("Size of hidden region of lower string"), "interruption_width", &wr, this, + 3) + , prop_to_stroke_width( + _("_In units of stroke width"), + _("Gap width is given in multiples of stroke width. When unchecked, document units are used."), + "prop_to_stroke_width", &wr, this, true) + , both(_("_Both gaps"), _("Use gap in both intersection elements"), "both", &wr, this, false) + , inverse_width(_("_Groups: Inverse"), _("Use other stroke width, useful in groups with different stroke widths"), + "inverse_width", &wr, this, false) + , add_stroke_width("St_roke width", "Add the stroke width to the gap size", "add_stroke_width", &wr, this, + "inkscape_1.0_and_up", true) + , add_other_stroke_width("_Crossing path stroke width", "Add crossed stroke width to the gap size", + "add_other_stroke_width", &wr, this, "inkscape_1.0_and_up", true) + , switcher_size(_("S_witcher size:"), _("Orientation indicator/switcher size"), "switcher_size", &wr, this, 15) + , crossing_points_vector(_("Crossing Signs"), _("Crossings signs"), "crossing_points_vector", &wr, this) + , crossing_points() + , gpaths() + , gstroke_widths() + , selectedCrossing(0) + , switcher(0., 0.) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&switcher_size); + registerParameter(&interruption_width); + registerParameter(&prop_to_stroke_width); + registerParameter(&add_stroke_width); + registerParameter(&both); + registerParameter(&inverse_width); + registerParameter(&add_other_stroke_width); + registerParameter(&crossing_points_vector); + + _provides_knotholder_entities = true; +} + +LPEKnot::~LPEKnot() += default; + +void +LPEKnot::updateSwitcher(){ + if (selectedCrossing < crossing_points.size()){ + switcher = crossing_points[selectedCrossing].pt; + //std::cout<<"placing switcher at "<0){ + selectedCrossing = 0; + switcher = crossing_points[selectedCrossing].pt; + //std::cout<<"placing switcher at "<getInt("/options/svgoutput/numericprecision"); + prefs->setInt("/options/svgoutput/numericprecision", 4); // I think this is enough for minor differences + for (i0=0; i0setInt("/options/svgoutput/numericprecision", precision); + if (i0 == gpaths.size() ) {THROW_EXCEPTION("lpe-knot error: group member not recognized");}// this should not happen... + + std::vector dom; + dom.emplace_back(0., size_nondegenerate(gpaths[i0])); + for (unsigned p = 0; p < crossing_points.size(); p++){ + if ( (crossing_points[p].i == i0) || (crossing_points[p].j == i0) ) { + unsigned i = crossing_points[p].i; + unsigned j = crossing_points[p].j; + double ti = crossing_points[p].ti; + double tj = crossing_points[p].tj; + + double curveidx, t; + + t = modf(ti, &curveidx); + if(curveidx == size_nondegenerate(gpaths[i]) ) { curveidx--; t = 1.;} + assert(curveidx >= 0 && curveidx < size_nondegenerate(gpaths[i])); + std::vector flag_i = gpaths[i][curveidx].pointAndDerivatives(t,1); + + t = modf(tj, &curveidx); + if(curveidx == size_nondegenerate(gpaths[j]) ) { curveidx--; t = 1.;} + assert(curveidx >= 0 && curveidx < size_nondegenerate(gpaths[j])); + std::vector flag_j = gpaths[j][curveidx].pointAndDerivatives(t,1); + + + int geom_sign = ( cross(flag_i[1], flag_j[1]) < 0 ? 1 : -1); + bool i0_is_under = false; + double width = interruption_width; + if ( crossing_points[p].sign * geom_sign > 0 ){ + i0_is_under = ( i == i0 ); + } + else if (crossing_points[p].sign * geom_sign < 0) { + if (j == i0){ + i0_is_under = true; + } + } + i0_is_under = crossing_points[p].sign != 0 && both ? true : i0_is_under; + if (i0_is_under && j == i0) { + // last check of sign makes sure we get different outputs when + // path components are part of the same subpath (i == j) + if (!(i == j && !both && crossing_points[p].sign * geom_sign > 0)) { + std::swap(i, j); + std::swap(ti, tj); + std::swap(flag_i, flag_j); + } + } + if (i0_is_under){ + if ( prop_to_stroke_width.get_value() ) { + if (inverse_width) { + width *= gstroke_widths[j]; + } + else { + width *= gstroke_widths[i]; + } + } + if (add_stroke_width.get_value() == "true") { + width += gstroke_widths[i]; + } + if (add_other_stroke_width.get_value() == "true") { + width += gstroke_widths[j]; + } + Interval hidden = findShadowedTime(gpaths[i0], flag_j, ti, width/2); + double period = size_nondegenerate(gpaths[i0]); + if (hidden.max() > period ) hidden -= period; + if (hidden.min()<0){ + dom = complementOf( Interval(0,hidden.max()) ,dom); + dom = complementOf( Interval(hidden.min()+period, period) ,dom); + }else{ + dom = complementOf(hidden,dom); + } + if (crossing_points[p].i == i0 && crossing_points[p].j == i0 && crossing_points[p].sign != 0 && + both) { + hidden = findShadowedTime(gpaths[i0], flag_i, tj, width / 2); + period = size_nondegenerate(gpaths[i0]); + if (hidden.max() > period) + hidden -= period; + if (hidden.min() < 0) { + dom = complementOf(Interval(0, hidden.max()), dom); + dom = complementOf(Interval(hidden.min() + period, period), dom); + } + else { + dom = complementOf(hidden, dom); + } + } + } + } + } + + //If the all component is hidden, continue. + if (dom.empty()){ + continue; + } + + //If the current path is closed and the last/first point is still there, glue first and last piece. + unsigned beg_comp = 0, end_comp = dom.size(); + if ( gpaths[i0].closed() && (dom.front().min() == 0) && (dom.back().max() == size_nondegenerate(gpaths[i0])) ) { + if ( dom.size() == 1){ + path_out.push_back(gpaths[i0]); + continue; + }else{ + // std::cout<<"fusing first and last component\n"; + ++beg_comp; + --end_comp; + Geom::Path first = gpaths[i0].portion(dom.back()); + //FIXME: stitching should not be necessary (?!?) + first.setStitching(true); + first.append(gpaths[i0].portion(dom.front())); + path_out.push_back(first); + } + } + for (unsigned comp = beg_comp; comp < end_comp; comp++){ + assert(dom.at(comp).min() >=0 && dom.at(comp).max() <= size_nondegenerate(gpaths.at(i0))); + path_out.push_back(gpaths[i0].portion(dom.at(comp))); + } + } + return path_out; +} + + + +//recursively collect gpaths and stroke widths (stolen from "sp-lpe_item.cpp"). +static void +collectPathsAndWidths (SPLPEItem const *lpeitem, Geom::PathVector &paths, std::vector &stroke_widths){ + if (SP_IS_GROUP(lpeitem)) { + std::vector item_list = sp_item_group_item_list(SP_GROUP(lpeitem)); + for (auto subitem : item_list) { + if (SP_IS_LPE_ITEM(subitem)) { + collectPathsAndWidths(SP_LPE_ITEM(subitem), paths, stroke_widths); + } + } + } + else if (SP_IS_SHAPE(lpeitem)) { + SPCurve * c = SP_SHAPE(lpeitem)->getCurve(); + if (c) { + Geom::PathVector subpaths = pathv_to_linear_and_cubic_beziers(c->get_pathvector()); + for (const auto & subpath : subpaths){ + paths.push_back(subpath); + //FIXME: do we have to be more careful when trying to access stroke width? + stroke_widths.push_back(lpeitem->style->stroke_width.computed); + } + } + c->unref(); + } +} + + +void +LPEKnot::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem); + + if (SP_IS_PATH(lpeitem)) { + supplied_path = SP_PATH(lpeitem)->getCurve(true)->get_pathvector(); + } + + gpaths.clear(); + gstroke_widths.clear(); + + collectPathsAndWidths(lpeitem, gpaths, gstroke_widths); + +// std::cout<<"\nPaths on input:\n"; +// for (unsigned i=0; i 0 ) std::cout<<"first crossing sign = "< &hp_vec) +{ + using namespace Geom; + double r = switcher_size*.1; + char const * svgd; + //TODO: use a nice path! + if ( (selectedCrossing >= crossing_points.size()) || (crossing_points[selectedCrossing].sign > 0) ) { + //svgd = "M -10,0 A 10 10 0 1 0 0,-10 l 5,-1 -1,2"; + svgd = "m -7.07,7.07 c 3.9,3.91 10.24,3.91 14.14,0 3.91,-3.9 3.91,-10.24 0,-14.14 -3.9,-3.91 -10.24,-3.91 -14.14,0 l 2.83,-4.24 0.7,2.12"; + } else if (crossing_points[selectedCrossing].sign < 0) { + //svgd = "M 10,0 A 10 10 0 1 1 0,-10 l -5,-1 1,2"; + svgd = "m 7.07,7.07 c -3.9,3.91 -10.24,3.91 -14.14,0 -3.91,-3.9 -3.91,-10.24 0,-14.14 3.9,-3.91 10.24,-3.91 14.14,0 l -2.83,-4.24 -0.7,2.12"; + } else { + //svgd = "M 10,0 A 10 10 0 1 0 -10,0 A 10 10 0 1 0 10,0 "; + svgd = "M 10,0 C 10,5.52 5.52,10 0,10 -5.52,10 -10,5.52 -10,0 c 0,-5.52 4.48,-10 10,-10 5.52,0 10,4.48 10,10 z"; + } + PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Affine(r,0,0,r,0,0) * Translate(switcher); + hp_vec.push_back(pathv); +} + +void LPEKnot::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + KnotHolderEntity *e = new KnotHolderEntityCrossingSwitcher(this); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, + _("Drag to select a crossing, click to flip it, Shift + click to change all crossings, Ctrl + click to " + "reset and change all crossings")); + knotholder->add(e); +}; + + +void +KnotHolderEntityCrossingSwitcher::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) +{ + LPEKnot* lpe = dynamic_cast(_effect); + + lpe->selectedCrossing = idx_of_nearest(lpe->crossing_points,p); + lpe->updateSwitcher(); + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityCrossingSwitcher::knot_get() const +{ + LPEKnot const *lpe = dynamic_cast(_effect); + return lpe->switcher; +} + +void +KnotHolderEntityCrossingSwitcher::knot_click(guint state) +{ + LPEKnot* lpe = dynamic_cast(_effect); + unsigned s = lpe->selectedCrossing; + if (s < lpe->crossing_points.size()){ + if (state & GDK_SHIFT_MASK){ + int sign = lpe->crossing_points[s].sign; + for (unsigned p = 0; p < lpe->crossing_points.size(); p++) { + lpe->crossing_points[p].sign = ((lpe->crossing_points[p].sign + 2) % 3) - 1; + } + } + else if (state & GDK_CONTROL_MASK) { + int sign = lpe->crossing_points[s].sign; + for (unsigned p = 0; p < lpe->crossing_points.size(); p++) { + lpe->crossing_points[p].sign = ((sign + 2) % 3) - 1; + } + }else{ + int sign = lpe->crossing_points[s].sign; + lpe->crossing_points[s].sign = ((sign+2)%3)-1; + //std::cout<<"crossing set to"<crossing_points[s].sign<<".\n"; + } + lpe->crossing_points_vector.param_set_and_write_new_value(lpe->crossing_points.to_vector()); + DocumentUndo::done(lpe->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, /// @todo Is this the right verb? + _("Change knot crossing")); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. +// sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); + } +} + + +/* ######################## */ + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/live_effects/lpe-knot.h b/src/live_effects/lpe-knot.h new file mode 100644 index 0000000..6768d79 --- /dev/null +++ b/src/live_effects/lpe-knot.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE knot effect implementation, see lpe-knot.cpp. + */ +/* Authors: + * Jean-Francois Barraud + * Johan Engelen + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_KNOT_H +#define INKSCAPE_LPE_KNOT_H + + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/array.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/parameter.h" +//#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "2geom/crossing.h" + +class SPLPEItem; + +namespace Inkscape { +namespace LivePathEffect { + +class KnotHolderEntityCrossingSwitcher; + +// CrossingPoint, CrossingPoints: +// "point oriented" storage of crossing data (needed to find crossing nearest to a click, etc...) +//TODO: evaluate how lpeknot-specific that is? Should something like this exist in 2geom? +namespace LPEKnotNS {//just in case... +struct CrossingPoint { + Geom::Point pt; + int sign; //+/-1 = positive or neg crossing, 0 = flat. + unsigned i, j; //paths components meeting in this point. + unsigned ni, nj; //this crossing is the ni-th along i, nj-th along j. + double ti, tj; //time along paths. +}; + +class CrossingPoints : public std::vector{ +public: + CrossingPoints() : std::vector() {} + CrossingPoints(Geom::CrossingSet const &cs, Geom::PathVector const &path);//for self crossings only! + CrossingPoints(Geom::PathVector const &paths); + CrossingPoints(std::vector const &input); + std::vector to_vector(); + CrossingPoint get(unsigned const i, unsigned const ni); + void inherit_signs(CrossingPoints const &from_other, int default_value = 1); +}; +} + +class LPEKnot : public Effect, GroupBBoxEffect { +public: + LPEKnot(LivePathEffectObject *lpeobject); + ~LPEKnot() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & input_path) override; + + /* the knotholder entity classes must be declared friends */ + friend class KnotHolderEntityCrossingSwitcher; + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + Geom::PathVector supplied_path; //for knotholder business + +private: + void updateSwitcher(); + + ScalarParam interruption_width; + BoolParam prop_to_stroke_width; + BoolParam both; + BoolParam inverse_width; + // "add_stroke_width" and "add_other_stroke_width" parameters are not used since Inkscape 1.0, + // but changed from bool to hidden parameter to retain backward compatibility and dont show in the UI + HiddenParam add_stroke_width; + HiddenParam add_other_stroke_width; + ScalarParam switcher_size; + ArrayParam crossing_points_vector;//svg storage of crossing_points + + LPEKnotNS::CrossingPoints crossing_points;//topology representation of the knot. + + Geom::PathVector gpaths;//the collection of all the paths in the object or group. + std::vector gstroke_widths;//the collection of all the stroke widths in the object or group. + + //UI: please, someone, help me to improve this!! + unsigned selectedCrossing;//the selected crossing + Geom::Point switcher;//where to put the "switcher" helper + + LPEKnot(const LPEKnot&) = delete; + LPEKnot& operator=(const LPEKnot&) = delete; + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-lattice.cpp b/src/live_effects/lpe-lattice.cpp new file mode 100644 index 0000000..3dcc703 --- /dev/null +++ b/src/live_effects/lpe-lattice.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + + */ +/* + * Authors: + * Johan Engelen + * Steren Giannini + * No� Falzon + * Victor Navez + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-lattice.h" + +#include "display/curve.h" + +#include <2geom/sbasis-2d.h> +#include <2geom/bezier-to-sbasis.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPELattice::LPELattice(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + + // initialise your parameters here: + grid_point0(_("Control handle 0:"), _("Control handle 0"), "gridpoint0", &wr, this), + grid_point1(_("Control handle 1:"), _("Control handle 1"), "gridpoint1", &wr, this), + grid_point2(_("Control handle 2:"), _("Control handle 2"), "gridpoint2", &wr, this), + grid_point3(_("Control handle 3:"), _("Control handle 3"), "gridpoint3", &wr, this), + grid_point4(_("Control handle 4:"), _("Control handle 4"), "gridpoint4", &wr, this), + grid_point5(_("Control handle 5:"), _("Control handle 5"), "gridpoint5", &wr, this), + grid_point6(_("Control handle 6:"), _("Control handle 6"), "gridpoint6", &wr, this), + grid_point7(_("Control handle 7:"), _("Control handle 7"), "gridpoint7", &wr, this), + grid_point8(_("Control handle 8:"), _("Control handle 8"), "gridpoint8", &wr, this), + grid_point9(_("Control handle 9:"), _("Control handle 9"), "gridpoint9", &wr, this), + grid_point10(_("Control handle 10:"), _("Control handle 10"), "gridpoint10", &wr, this), + grid_point11(_("Control handle 11:"), _("Control handle 11"), "gridpoint11", &wr, this), + grid_point12(_("Control handle 12:"), _("Control handle 12"), "gridpoint12", &wr, this), + grid_point13(_("Control handle 13:"), _("Control handle 13"), "gridpoint13", &wr, this), + grid_point14(_("Control handle 14:"), _("Control handle 14"), "gridpoint14", &wr, this), + grid_point15(_("Control handle 15:"), _("Control handle 15"), "gridpoint15", &wr, this) + +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&grid_point0); + registerParameter(&grid_point1); + registerParameter(&grid_point2); + registerParameter(&grid_point3); + registerParameter(&grid_point4); + registerParameter(&grid_point5); + registerParameter(&grid_point6); + registerParameter(&grid_point7); + registerParameter(&grid_point8); + registerParameter(&grid_point9); + registerParameter(&grid_point10); + registerParameter(&grid_point11); + registerParameter(&grid_point12); + registerParameter(&grid_point13); + registerParameter(&grid_point14); + registerParameter(&grid_point15); + + apply_to_clippath_and_mask = true; +} + +LPELattice::~LPELattice() += default; + + +Geom::Piecewise > +LPELattice::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + D2 sb2; + + //Initialisation of the sb2 + for(unsigned dim = 0; dim < 2; dim++) { + sb2[dim].us = 2; + sb2[dim].vs = 2; + const int depth = sb2[dim].us*sb2[dim].vs; + sb2[dim].resize(depth, Linear2d(0)); + } + + //Grouping the point params in a convenient vector + std::vector handles(16); + + handles[0] = &grid_point0; + handles[1] = &grid_point1; + handles[2] = &grid_point2; + handles[3] = &grid_point3; + handles[4] = &grid_point4; + handles[5] = &grid_point5; + handles[6] = &grid_point6; + handles[7] = &grid_point7; + handles[8] = &grid_point8; + handles[9] = &grid_point9; + handles[10] = &grid_point10; + handles[11] = &grid_point11; + handles[12] = &grid_point12; + handles[13] = &grid_point13; + handles[14] = &grid_point14; + handles[15] = &grid_point15; + + Geom::Point origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); + + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + + //numbering is based on 4 rectangles. + for(unsigned dim = 0; dim < 2; dim++) { + Geom::Point dir(0,0); + dir[dim] = 1; + for(unsigned vi = 0; vi < sb2[dim].vs; vi++) { + for(unsigned ui = 0; ui < sb2[dim].us; ui++) { + for(unsigned iv = 0; iv < 2; iv++) { + for(unsigned iu = 0; iu < 2; iu++) { + unsigned corner = iu + 2*iv; + unsigned i = ui + vi*sb2[dim].us; + + //This is the offset from the Upperleft point + Geom::Point base( (ui + iu*(3-2*ui))*width/3., + (vi + iv*(3-2*vi))*height/3.); + + //Special action for corners + if(vi == 0 && ui == 0) { + base = Geom::Point(0,0); + } + + // i = Upperleft corner of the considerated rectangle + // corner = actual corner of the rectangle + // origin = Upperleft point + double dl = dot((*handles[corner+4*i] - (base + origin)), dir)/dot(dir,dir); + sb2[dim][i][corner] = dl/( dim ? height : width )*pow(4.0,ui+vi); + } + } + } + } + } + + Piecewise > output; + output.push_cut(0.); + for(unsigned i = 0; i < pwd2_in.size(); i++) { + D2 B = pwd2_in[i]; + B -= origin; + B*= 1/width; + //Here comes the magic + D2 tB = compose_each(sb2,B); + tB = tB * width + origin; + + output.push(tB,i+1); + } + + return output; +} + +void +LPELattice::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); +} + +void +LPELattice::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + original_bbox(SP_LPE_ITEM(item), false, true); + + // place the 16 control points + grid_point0[Geom::X] = boundingbox_X.min(); + grid_point0[Geom::Y] = boundingbox_Y.min(); + + grid_point1[Geom::X] = boundingbox_X.max(); + grid_point1[Geom::Y] = boundingbox_Y.min(); + + grid_point2[Geom::X] = boundingbox_X.min(); + grid_point2[Geom::Y] = boundingbox_Y.max(); + + grid_point3[Geom::X] = boundingbox_X.max(); + grid_point3[Geom::Y] = boundingbox_Y.max(); + + grid_point4[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point4[Geom::Y] = boundingbox_Y.min(); + + grid_point5[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point5[Geom::Y] = boundingbox_Y.min(); + + grid_point6[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point6[Geom::Y] = boundingbox_Y.max(); + + grid_point7[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point7[Geom::Y] = boundingbox_Y.max(); + + grid_point8[Geom::X] = boundingbox_X.min(); + grid_point8[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point9[Geom::X] = boundingbox_X.max(); + grid_point9[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point10[Geom::X] = boundingbox_X.min(); + grid_point10[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point11[Geom::X] = boundingbox_X.max(); + grid_point11[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point12[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point12[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point13[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point13[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point14[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point14[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point15[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point15[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + grid_point1.param_update_default(grid_point1); + grid_point2.param_update_default(grid_point2); + grid_point3.param_update_default(grid_point3); + grid_point4.param_update_default(grid_point4); + grid_point5.param_update_default(grid_point5); + grid_point6.param_update_default(grid_point6); + grid_point7.param_update_default(grid_point7); + grid_point8.param_update_default(grid_point8); + grid_point9.param_update_default(grid_point9); + grid_point10.param_update_default(grid_point10); + grid_point11.param_update_default(grid_point11); + grid_point12.param_update_default(grid_point12); + grid_point13.param_update_default(grid_point13); + grid_point14.param_update_default(grid_point14); + grid_point15.param_update_default(grid_point15); +} + +/** +void +LPELattice::addHelperPathsImpl(SPLPEItem *lpeitem, SPDesktop *desktop) +{ + SPCurve *c = new SPCurve (); + c->moveto(grid_point0); + c->lineto(grid_point4); + c->lineto(grid_point5); + c->lineto(grid_point1); + + c->moveto(grid_point8); + c->lineto(grid_point12); + c->lineto(grid_point13); + c->lineto(grid_point9); + + c->moveto(grid_point10); + c->lineto(grid_point14); + c->lineto(grid_point15); + c->lineto(grid_point11); + + c->moveto(grid_point2); + c->lineto(grid_point6); + c->lineto(grid_point7); + c->lineto(grid_point3); + + + c->moveto(grid_point0); + c->lineto(grid_point8); + c->lineto(grid_point10); + c->lineto(grid_point2); + + c->moveto(grid_point4); + c->lineto(grid_point12); + c->lineto(grid_point14); + c->lineto(grid_point6); + + c->moveto(grid_point5); + c->lineto(grid_point13); + c->lineto(grid_point15); + c->lineto(grid_point7); + + c->moveto(grid_point1); + c->lineto(grid_point9); + c->lineto(grid_point11); + c->lineto(grid_point3); + + // TODO: factor this out (and remove the #include of desktop.h above) + SPCanvasItem *canvasitem = sp_nodepath_generate_helperpath(desktop, c, lpeitem, 0x009000ff); + Inkscape::Display::TemporaryItem* tmpitem = desktop->add_temporary_canvasitem (canvasitem, 0); + lpeitem->lpe_helperpaths.push_back(tmpitem); + + c->unref(); +} +**/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-lattice.h b/src/live_effects/lpe-lattice.h new file mode 100644 index 0000000..1d81355 --- /dev/null +++ b/src/live_effects/lpe-lattice.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LATTICE_H +#define INKSCAPE_LPE_LATTICE_H + +/** \file + * LPE implementation, see lpe-lattice.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Steren Giannini + * Noé Falzon + * Victor Navez + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPELattice : public Effect, GroupBBoxEffect { +public: + + LPELattice(LivePathEffectObject *lpeobject); + ~LPELattice() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +protected: + //virtual void addHelperPathsImpl(SPLPEItem *lpeitem, SPDesktop *desktop); + + +private: + PointParam grid_point0; + PointParam grid_point1; + PointParam grid_point2; + PointParam grid_point3; + PointParam grid_point4; + PointParam grid_point5; + PointParam grid_point6; + PointParam grid_point7; + PointParam grid_point8; + PointParam grid_point9; + PointParam grid_point10; + PointParam grid_point11; + PointParam grid_point12; + PointParam grid_point13; + PointParam grid_point14; + PointParam grid_point15; + LPELattice(const LPELattice&) = delete; + LPELattice& operator=(const LPELattice&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-lattice2.cpp b/src/live_effects/lpe-lattice2.cpp new file mode 100644 index 0000000..0f0a442 --- /dev/null +++ b/src/live_effects/lpe-lattice2.cpp @@ -0,0 +1,679 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + + */ +/* + * Authors: + * Johan Engelen + * Steren Giannini + * No� Falzon + * Victor Navez + * ~suv + * Jabiertxo Arraiza + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-lattice2.h" +#include "display/curve.h" +#include "helper/geom.h" +#include <2geom/sbasis-2d.h> +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPELattice2::LPELattice2(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + horizontal_mirror(_("Mirror movements in horizontal"), _("Mirror movements in horizontal"), "horizontal_mirror", &wr, this, false), + vertical_mirror(_("Mirror movements in vertical"), _("Mirror movements in vertical"), "vertical_mirror", &wr, this, false), + perimetral(_("Use only perimeter"), _("Use only perimeter"), "perimetral", &wr, this, false), + live_update(_("Update while moving knots (maybe slow)"), _("Update while moving knots (maybe slow)"), "live_update", &wr, this, true), + grid_point_0(_("Control 0:"), _("Control 0 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint0", &wr, this), + grid_point_1(_("Control 1:"), _("Control 1 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint1", &wr, this), + grid_point_2(_("Control 2:"), _("Control 2 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint2", &wr, this), + grid_point_3(_("Control 3:"), _("Control 3 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint3", &wr, this), + grid_point_4(_("Control 4:"), _("Control 4 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint4", &wr, this), + grid_point_5(_("Control 5:"), _("Control 5 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint5", &wr, this), + grid_point_6(_("Control 6:"), _("Control 6 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint6", &wr, this), + grid_point_7(_("Control 7:"), _("Control 7 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint7", &wr, this), + grid_point_8x9(_("Control 8x9:"), _("Control 8x9 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint8x9", &wr, this), + grid_point_10x11(_("Control 10x11:"), _("Control 10x11 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint10x11", &wr, this), + grid_point_12(_("Control 12:"), _("Control 12 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint12", &wr, this), + grid_point_13(_("Control 13:"), _("Control 13 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint13", &wr, this), + grid_point_14(_("Control 14:"), _("Control 14 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint14", &wr, this), + grid_point_15(_("Control 15:"), _("Control 15 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint15", &wr, this), + grid_point_16(_("Control 16:"), _("Control 16 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint16", &wr, this), + grid_point_17(_("Control 17:"), _("Control 17 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint17", &wr, this), + grid_point_18(_("Control 18:"), _("Control 18 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint18", &wr, this), + grid_point_19(_("Control 19:"), _("Control 19 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint19", &wr, this), + grid_point_20x21(_("Control 20x21:"), _("Control 20x21 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint20x21", &wr, this), + grid_point_22x23(_("Control 22x23:"), _("Control 22x23 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint22x23", &wr, this), + grid_point_24x26(_("Control 24x26:"), _("Control 24x26 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint24x26", &wr, this), + grid_point_25x27(_("Control 25x27:"), _("Control 25x27 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint25x27", &wr, this), + grid_point_28x30(_("Control 28x30:"), _("Control 28x30 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint28x30", &wr, this), + grid_point_29x31(_("Control 29x31:"), _("Control 29x31 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint29x31", &wr, this), + grid_point_32x33x34x35(_("Control 32x33x34x35:"), _("Control 32x33x34x35 - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "gridpoint32x33x34x35", &wr, this), + expanded(false) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&horizontal_mirror); + registerParameter(&vertical_mirror); + registerParameter(&perimetral); + registerParameter(&live_update); + registerParameter(&grid_point_0); + registerParameter(&grid_point_1); + registerParameter(&grid_point_2); + registerParameter(&grid_point_3); + registerParameter(&grid_point_4); + registerParameter(&grid_point_5); + registerParameter(&grid_point_6); + registerParameter(&grid_point_7); + registerParameter(&grid_point_8x9); + registerParameter(&grid_point_10x11); + registerParameter(&grid_point_12); + registerParameter(&grid_point_13); + registerParameter(&grid_point_14); + registerParameter(&grid_point_15); + registerParameter(&grid_point_16); + registerParameter(&grid_point_17); + registerParameter(&grid_point_18); + registerParameter(&grid_point_19); + registerParameter(&grid_point_20x21); + registerParameter(&grid_point_22x23); + registerParameter(&grid_point_24x26); + registerParameter(&grid_point_25x27); + registerParameter(&grid_point_28x30); + registerParameter(&grid_point_29x31); + registerParameter(&grid_point_32x33x34x35); + apply_to_clippath_and_mask = true; +} + +LPELattice2::~LPELattice2() += default; + +Geom::Piecewise > +LPELattice2::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + PathVector pathv = path_from_piecewise(pwd2_in,0.001); + //this is because strange problems with sb2 and LineSegment + PathVector cubic = pathv_to_cubicbezier(pathv); + if (cubic.empty()) { + return pwd2_in; + } + Geom::Piecewise > const &pwd2_in_linear_and_cubic = paths_to_pw(cubic); + D2 sb2; + + //Initialisation of the sb2 + for(unsigned dim = 0; dim < 2; dim++) { + sb2[dim].us = 3; + sb2[dim].vs = 3; + const int depth = sb2[dim].us*sb2[dim].vs; + sb2[dim].resize(depth, Linear2d(0)); + } + + //Grouping the point params in a convenient vector + + std::vector handles(36); + + handles[0] = grid_point_0; + handles[1] = grid_point_1; + handles[2] = grid_point_2; + handles[3] = grid_point_3; + handles[4] = grid_point_4; + handles[5] = grid_point_5; + handles[6] = grid_point_6; + handles[7] = grid_point_7; + handles[8] = grid_point_8x9; + handles[9] = grid_point_8x9; + handles[10] = grid_point_10x11; + handles[11] = grid_point_10x11; + handles[12] = grid_point_12; + handles[13] = grid_point_13; + handles[14] = grid_point_14; + handles[15] = grid_point_15; + handles[16] = grid_point_16; + handles[17] = grid_point_17; + handles[18] = grid_point_18; + handles[19] = grid_point_19; + handles[20] = grid_point_20x21; + handles[21] = grid_point_20x21; + handles[22] = grid_point_22x23; + handles[23] = grid_point_22x23; + handles[24] = grid_point_24x26; + handles[25] = grid_point_25x27; + handles[26] = grid_point_24x26; + handles[27] = grid_point_25x27; + handles[28] = grid_point_28x30; + handles[29] = grid_point_29x31; + handles[30] = grid_point_28x30; + handles[31] = grid_point_29x31; + handles[32] = grid_point_32x33x34x35; + handles[33] = grid_point_32x33x34x35; + handles[34] = grid_point_32x33x34x35; + handles[35] = grid_point_32x33x34x35; + + Geom::Point origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); + + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + + //numbering is based on 4 rectangles.16 + for(unsigned dim = 0; dim < 2; dim++) { + Geom::Point dir(0,0); + dir[dim] = 1; + for(unsigned vi = 0; vi < sb2[dim].vs; vi++) { + for(unsigned ui = 0; ui < sb2[dim].us; ui++) { + for(unsigned iv = 0; iv < 2; iv++) { + for(unsigned iu = 0; iu < 2; iu++) { + unsigned corner = iu + 2*iv; + unsigned i = ui + vi*sb2[dim].us; + + //This is the offset from the Upperleft point + Geom::Point base( (ui + iu*(4-2*ui))*width/4., + (vi + iv*(4-2*vi))*height/4.); + + //Special action for corners + if(vi == 0 && ui == 0) { + base = Geom::Point(0,0); + } + + // i = Upperleft corner of the considerated rectangle + // corner = actual corner of the rectangle + // origin = Upperleft point + double dl = dot((handles[corner+4*i] - (base + origin)), dir)/dot(dir,dir); + sb2[dim][i][corner] = dl/( dim ? height : width )*pow(4.0,ui+vi); + } + } + } + } + } + + Piecewise > output; + output.push_cut(0.); + for(unsigned i = 0; i < pwd2_in_linear_and_cubic.size(); i++) { + D2 B = pwd2_in_linear_and_cubic[i]; + B[Geom::X] -= origin[Geom::X]; + B[Geom::X]*= 1/width; + B[Geom::Y] -= origin[Geom::Y]; + B[Geom::Y]*= 1/height; + //Here comes the magic + D2 tB = compose_each(sb2,B); + tB[Geom::X] = tB[Geom::X] * width + origin[Geom::X]; + tB[Geom::Y] = tB[Geom::Y] * height + origin[Geom::Y]; + + output.push(tB,i+1); + } + return output; +} + + +Gtk::Widget * +LPELattice2::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox(Effect::newWidget()) ); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + Gtk::HBox * hbox = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::VBox * vbox_expander = Gtk::manage( new Gtk::VBox(Effect::newWidget()) ); + vbox_expander->set_border_width(0); + vbox_expander->set_spacing(2); + Gtk::Button * reset_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset grid")))); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPELattice2::resetGrid)); + reset_button->set_size_request(140,30); + vbox->pack_start(*hbox, true,true,2); + hbox->pack_start(*reset_button, false, false,2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast(param->param_newWidget()); + if(param->param_key == "grid") { + widg = nullptr; + } + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if (param->param_key == "horizontal_mirror" || + param->param_key == "vertical_mirror" || + param->param_key == "live_update" || + param->param_key == "perimetral") + { + vbox->pack_start(*widg, true, true, 2); + } else { + vbox_expander->pack_start(*widg, true, true, 2); + } + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + + expander = Gtk::manage(new Gtk::Expander(Glib::ustring(_("Show Points")))); + expander->add(*vbox_expander); + expander->set_expanded(expanded); + vbox->pack_start(*expander, true, true, 2); + expander->property_expanded().signal_changed().connect(sigc::mem_fun(*this, &LPELattice2::onExpanderChanged) ); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPELattice2::onExpanderChanged() +{ + expanded = expander->get_expanded(); + if(expanded) { + expander->set_label (Glib::ustring(_("Hide Points"))); + } else { + expander->set_label (Glib::ustring(_("Show Points"))); + } +} +void +LPELattice2::vertical(PointParam ¶m_one, PointParam ¶m_two, Geom::Line vert) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double Y = (A[Geom::Y] + B[Geom::Y])/2; + A[Geom::Y] = Y; + B[Geom::Y] = Y; + Geom::Point nearest = vert.pointAt(vert.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::X] > B[Geom::X]) { + distance_middle *= -1; + } + A[Geom::X] = nearest[Geom::X] - distance_middle; + B[Geom::X] = nearest[Geom::X] + distance_middle; + param_one.param_setValue(A, live_update); + param_two.param_setValue(B, live_update); +} + +void +LPELattice2::horizontal(PointParam ¶m_one, PointParam ¶m_two, Geom::Line horiz) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double X = (A[Geom::X] + B[Geom::X])/2; + A[Geom::X] = X; + B[Geom::X] = X; + Geom::Point nearest = horiz.pointAt(horiz.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::Y] > B[Geom::Y]) { + distance_middle *= -1; + } + A[Geom::Y] = nearest[Geom::Y] - distance_middle; + B[Geom::Y] = nearest[Geom::Y] + distance_middle; + param_one.param_setValue(A, live_update); + param_two.param_setValue(B, live_update); +} + + +void +LPELattice2::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); + setDefaults(); + if (is_applied) { + resetGrid(); + } + Geom::Line vert(grid_point_8x9.param_get_default(),grid_point_10x11.param_get_default()); + Geom::Line horiz(grid_point_24x26.param_get_default(),grid_point_25x27.param_get_default()); + if(vertical_mirror) { + vertical(grid_point_0, grid_point_1,vert); + vertical(grid_point_2, grid_point_3,vert); + vertical(grid_point_4, grid_point_5,vert); + vertical(grid_point_6, grid_point_7,vert); + vertical(grid_point_12, grid_point_13,vert); + vertical(grid_point_14, grid_point_15,vert); + vertical(grid_point_16, grid_point_17,vert); + vertical(grid_point_18, grid_point_19,vert); + vertical(grid_point_24x26, grid_point_25x27,vert); + vertical(grid_point_28x30, grid_point_29x31,vert); + } + if(horizontal_mirror) { + horizontal(grid_point_0, grid_point_2,horiz); + horizontal(grid_point_1, grid_point_3,horiz); + horizontal(grid_point_4, grid_point_6,horiz); + horizontal(grid_point_5, grid_point_7,horiz); + horizontal(grid_point_8x9, grid_point_10x11,horiz); + horizontal(grid_point_12, grid_point_14,horiz); + horizontal(grid_point_13, grid_point_15,horiz); + horizontal(grid_point_16, grid_point_18,horiz); + horizontal(grid_point_17, grid_point_19,horiz); + horizontal(grid_point_20x21, grid_point_22x23,horiz); + } + if (perimetral) { + grid_point_16.param_hide_knot(true); + grid_point_20x21.param_hide_knot(true); + grid_point_17.param_hide_knot(true); + grid_point_28x30.param_hide_knot(true); + grid_point_32x33x34x35.param_hide_knot(true); + grid_point_29x31.param_hide_knot(true); + grid_point_18.param_hide_knot(true); + grid_point_22x23.param_hide_knot(true); + grid_point_19.param_hide_knot(true); + grid_point_16.param_set_default(); + grid_point_20x21.param_set_default(); + grid_point_17.param_set_default(); + grid_point_28x30.param_set_default(); + grid_point_32x33x34x35.param_set_default(); + grid_point_29x31.param_set_default(); + grid_point_18.param_set_default(); + grid_point_22x23.param_set_default(); + grid_point_19.param_set_default(); + } else { + grid_point_16.param_hide_knot(false); + grid_point_20x21.param_hide_knot(false); + grid_point_17.param_hide_knot(false); + grid_point_28x30.param_hide_knot(false); + grid_point_32x33x34x35.param_hide_knot(false); + grid_point_29x31.param_hide_knot(false); + grid_point_18.param_hide_knot(false); + grid_point_22x23.param_hide_knot(false); + grid_point_19.param_hide_knot(false); + } +} + +void +LPELattice2::setDefaults() +{ + Geom::Point gp0((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp1((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp2((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp3((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp4((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp5((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp6((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp7((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp8x9((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp10x11((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp12((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp13((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp14((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp15((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp16((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp17((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp18((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp19((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp20x21((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp22x23((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp24x26((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp25x27((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp28x30((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp29x31((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp32x33x34x35((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + grid_point_0.param_update_default(gp0); + grid_point_1.param_update_default(gp1); + grid_point_2.param_update_default(gp2); + grid_point_3.param_update_default(gp3); + grid_point_4.param_update_default(gp4); + grid_point_5.param_update_default(gp5); + grid_point_6.param_update_default(gp6); + grid_point_7.param_update_default(gp7); + grid_point_8x9.param_update_default(gp8x9); + grid_point_10x11.param_update_default(gp10x11); + grid_point_12.param_update_default(gp12); + grid_point_13.param_update_default(gp13); + grid_point_14.param_update_default(gp14); + grid_point_15.param_update_default(gp15); + grid_point_16.param_update_default(gp16); + grid_point_17.param_update_default(gp17); + grid_point_18.param_update_default(gp18); + grid_point_19.param_update_default(gp19); + grid_point_20x21.param_update_default(gp20x21); + grid_point_22x23.param_update_default(gp22x23); + grid_point_24x26.param_update_default(gp24x26); + grid_point_25x27.param_update_default(gp25x27); + grid_point_28x30.param_update_default(gp28x30); + grid_point_29x31.param_update_default(gp29x31); + grid_point_32x33x34x35.param_update_default(gp32x33x34x35); + grid_point_0.param_set_liveupdate(live_update); + grid_point_1.param_set_liveupdate(live_update); + grid_point_2.param_set_liveupdate(live_update); + grid_point_3.param_set_liveupdate(live_update); + grid_point_4.param_set_liveupdate(live_update); + grid_point_5.param_set_liveupdate(live_update); + grid_point_6.param_set_liveupdate(live_update); + grid_point_7.param_set_liveupdate(live_update); + grid_point_8x9.param_set_liveupdate(live_update); + grid_point_10x11.param_set_liveupdate(live_update); + grid_point_12.param_set_liveupdate(live_update); + grid_point_13.param_set_liveupdate(live_update); + grid_point_14.param_set_liveupdate(live_update); + grid_point_15.param_set_liveupdate(live_update); + grid_point_16.param_set_liveupdate(live_update); + grid_point_17.param_set_liveupdate(live_update); + grid_point_18.param_set_liveupdate(live_update); + grid_point_19.param_set_liveupdate(live_update); + grid_point_20x21.param_set_liveupdate(live_update); + grid_point_22x23.param_set_liveupdate(live_update); + grid_point_24x26.param_set_liveupdate(live_update); + grid_point_25x27.param_set_liveupdate(live_update); + grid_point_28x30.param_set_liveupdate(live_update); + grid_point_29x31.param_set_liveupdate(live_update); + grid_point_32x33x34x35.param_set_liveupdate(live_update); +} + +void +LPELattice2::resetGrid() +{ + grid_point_0.param_set_default(); + grid_point_1.param_set_default(); + grid_point_2.param_set_default(); + grid_point_3.param_set_default(); + grid_point_4.param_set_default(); + grid_point_5.param_set_default(); + grid_point_6.param_set_default(); + grid_point_7.param_set_default(); + grid_point_8x9.param_set_default(); + grid_point_10x11.param_set_default(); + grid_point_12.param_set_default(); + grid_point_13.param_set_default(); + grid_point_14.param_set_default(); + grid_point_15.param_set_default(); + grid_point_16.param_set_default(); + grid_point_17.param_set_default(); + grid_point_18.param_set_default(); + grid_point_19.param_set_default(); + grid_point_20x21.param_set_default(); + grid_point_22x23.param_set_default(); + grid_point_24x26.param_set_default(); + grid_point_25x27.param_set_default(); + grid_point_28x30.param_set_default(); + grid_point_29x31.param_set_default(); + grid_point_32x33x34x35.param_set_default(); +} + +void +LPELattice2::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + setDefaults(); + resetGrid(); +} + +void +LPELattice2::calculateCurve(Geom::Point a,Geom::Point b, SPCurve* c, bool horizontal, bool move) +{ + using Geom::X; + using Geom::Y; + if(move) c->moveto(a); + Geom::Point cubic1 = a + (1./3)* (b - a); + Geom::Point cubic2 = b + (1./3)* (a - b); + if(horizontal) c->curveto(Geom::Point(cubic1[X],a[Y]),Geom::Point(cubic2[X],b[Y]),b); + else c->curveto(Geom::Point(a[X],cubic1[Y]),Geom::Point(b[X],cubic2[Y]),b); +} + +void +LPELattice2::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.clear(); + + SPCurve *c = new SPCurve(); + if (perimetral) { + calculateCurve(grid_point_0,grid_point_4, c,true, true); + calculateCurve(grid_point_4,grid_point_8x9, c,true, false); + calculateCurve(grid_point_8x9,grid_point_5, c,true, false); + calculateCurve(grid_point_5,grid_point_1, c,true, false); + + calculateCurve(grid_point_1,grid_point_13, c, false, true); + calculateCurve(grid_point_13,grid_point_25x27, c,false, false); + calculateCurve(grid_point_25x27,grid_point_15, c,false, false); + calculateCurve(grid_point_15,grid_point_3, c, false, false); + + calculateCurve(grid_point_2,grid_point_6, c,true, true); + calculateCurve(grid_point_6,grid_point_10x11, c,true, false); + calculateCurve(grid_point_10x11,grid_point_7, c,true, false); + calculateCurve(grid_point_7,grid_point_3, c,true, false); + + calculateCurve(grid_point_0,grid_point_12, c,false, true); + calculateCurve(grid_point_12,grid_point_24x26, c,false, false); + calculateCurve(grid_point_24x26,grid_point_14, c,false, false); + calculateCurve(grid_point_14,grid_point_2, c,false, false); + + } else { + calculateCurve(grid_point_0,grid_point_4, c,true, true); + calculateCurve(grid_point_4,grid_point_8x9, c,true, false); + calculateCurve(grid_point_8x9,grid_point_5, c,true, false); + calculateCurve(grid_point_5,grid_point_1, c,true, false); + + calculateCurve(grid_point_12,grid_point_16, c,true, true); + calculateCurve(grid_point_16,grid_point_20x21, c,true, false); + calculateCurve(grid_point_20x21,grid_point_17, c,true, false); + calculateCurve(grid_point_17,grid_point_13, c,true, false); + + calculateCurve(grid_point_24x26,grid_point_28x30, c,true, true); + calculateCurve(grid_point_28x30,grid_point_32x33x34x35, c,true, false); + calculateCurve(grid_point_32x33x34x35,grid_point_29x31, c,true, false); + calculateCurve(grid_point_29x31,grid_point_25x27, c,true, false); + + calculateCurve(grid_point_14,grid_point_18, c,true, true); + calculateCurve(grid_point_18,grid_point_22x23, c,true, false); + calculateCurve(grid_point_22x23,grid_point_19, c,true, false); + calculateCurve(grid_point_19,grid_point_15, c,true, false); + + calculateCurve(grid_point_2,grid_point_6, c,true, true); + calculateCurve(grid_point_6,grid_point_10x11, c,true, false); + calculateCurve(grid_point_10x11,grid_point_7, c,true, false); + calculateCurve(grid_point_7,grid_point_3, c,true, false); + + calculateCurve(grid_point_0,grid_point_12, c,false, true); + calculateCurve(grid_point_12,grid_point_24x26, c,false, false); + calculateCurve(grid_point_24x26,grid_point_14, c,false, false); + calculateCurve(grid_point_14,grid_point_2, c,false, false); + + calculateCurve(grid_point_4,grid_point_16, c,false, true); + calculateCurve(grid_point_16,grid_point_28x30, c,false, false); + calculateCurve(grid_point_28x30,grid_point_18, c,false, false); + calculateCurve(grid_point_18,grid_point_6, c,false, false); + + calculateCurve(grid_point_8x9,grid_point_20x21, c,false, true); + calculateCurve(grid_point_20x21,grid_point_32x33x34x35, c,false, false); + calculateCurve(grid_point_32x33x34x35,grid_point_22x23, c,false, false); + calculateCurve(grid_point_22x23,grid_point_10x11, c,false, false); + + calculateCurve(grid_point_5,grid_point_17, c, false, true); + calculateCurve(grid_point_17,grid_point_29x31, c,false, false); + calculateCurve(grid_point_29x31,grid_point_19, c,false, false); + calculateCurve(grid_point_19,grid_point_7, c,false, false); + + calculateCurve(grid_point_1,grid_point_13, c, false, true); + calculateCurve(grid_point_13,grid_point_25x27, c,false, false); + calculateCurve(grid_point_25x27,grid_point_15, c,false, false); + calculateCurve(grid_point_15,grid_point_3, c, false, false); + } + hp_vec.push_back(c->get_pathvector()); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-lattice2.h b/src/live_effects/lpe-lattice2.h new file mode 100644 index 0000000..319a0dc --- /dev/null +++ b/src/live_effects/lpe-lattice2.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LATTICE2_H +#define INKSCAPE_LPE_LATTICE2_H + +/** \file + * LPE implementation, see lpe-lattice2.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Steren Giannini + * Noé Falzon + * Victor Navez + * ~suv + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Gtk { +class Expander; +} + +namespace Inkscape { +namespace LivePathEffect { + +class LPELattice2 : public Effect, GroupBBoxEffect { +public: + + LPELattice2(LivePathEffectObject *lpeobject); + ~LPELattice2() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* lpeitem) override; + + Gtk::Widget * newWidget() override; + + void calculateCurve(Geom::Point a,Geom::Point b, SPCurve *c, bool horizontal, bool move); + + void vertical(PointParam ¶mA,PointParam ¶mB, Geom::Line vert); + + void horizontal(PointParam ¶mA,PointParam ¶mB,Geom::Line horiz); + + void setDefaults(); + + void onExpanderChanged(); + + void resetGrid(); + +protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; +private: + + BoolParam horizontal_mirror; + BoolParam vertical_mirror; + BoolParam perimetral; + BoolParam live_update; + PointParam grid_point_0; + PointParam grid_point_1; + PointParam grid_point_2; + PointParam grid_point_3; + PointParam grid_point_4; + PointParam grid_point_5; + PointParam grid_point_6; + PointParam grid_point_7; + PointParam grid_point_8x9; + PointParam grid_point_10x11; + PointParam grid_point_12; + PointParam grid_point_13; + PointParam grid_point_14; + PointParam grid_point_15; + PointParam grid_point_16; + PointParam grid_point_17; + PointParam grid_point_18; + PointParam grid_point_19; + PointParam grid_point_20x21; + PointParam grid_point_22x23; + PointParam grid_point_24x26; + PointParam grid_point_25x27; + PointParam grid_point_28x30; + PointParam grid_point_29x31; + PointParam grid_point_32x33x34x35; + + bool expanded; + Gtk::Expander * expander; + + LPELattice2(const LPELattice2&) = delete; + LPELattice2& operator=(const LPELattice2&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-line_segment.cpp b/src/live_effects/lpe-line_segment.cpp new file mode 100644 index 0000000..5c24aa9 --- /dev/null +++ b/src/live_effects/lpe-line_segment.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-line_segment.h" +#include "ui/tools/lpe-tool.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData EndTypeData[] = { + {END_CLOSED , N_("Closed"), "closed"}, + {END_OPEN_INITIAL , N_("Open start"), "open_start"}, + {END_OPEN_FINAL , N_("Open end"), "open_end"}, + {END_OPEN_BOTH , N_("Open both"), "open_both"}, +}; +static const Util::EnumDataConverter EndTypeConverter(EndTypeData, sizeof(EndTypeData)/sizeof(*EndTypeData)); + +LPELineSegment::LPELineSegment(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + end_type(_("End type:"), _("Determines on which side the line or line segment is infinite."), "end_type", EndTypeConverter, &wr, this, END_OPEN_BOTH) +{ + /* register all your parameters here, so Inkscape knows which parameters this effect has: */ + registerParameter(&end_type); +} + +LPELineSegment::~LPELineSegment() += default; + +void +LPELineSegment::doBeforeEffect (SPLPEItem const* lpeitem) +{ + Inkscape::UI::Tools::lpetool_get_limiting_bbox_corners(lpeitem->document, bboxA, bboxB); +} + +Geom::PathVector +LPELineSegment::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector output; + + A = path_in.initialPoint(); + B = path_in.finalPoint(); + + Geom::Rect dummyRect(bboxA, bboxB); + boost::optional intersection_segment = Geom::Line(A, B).clip(dummyRect); + + if (!intersection_segment) { + g_print ("Possible error - no intersection with limiting bounding box.\n"); + return path_in; + } + + if (end_type == END_OPEN_INITIAL || end_type == END_OPEN_BOTH) { + A = intersection_segment->initialPoint(); + } + + if (end_type == END_OPEN_FINAL || end_type == END_OPEN_BOTH) { + B = intersection_segment->finalPoint(); + } + + Geom::Path path(A); + path.appendNew(B); + + output.push_back(path); + + return output; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-line_segment.h b/src/live_effects/lpe-line_segment.h new file mode 100644 index 0000000..c8d3080 --- /dev/null +++ b/src/live_effects/lpe-line_segment.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LINE_SEGMENT_H +#define INKSCAPE_LPE_LINE_SEGMENT_H + +/** \file + * LPE implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum EndType { + END_CLOSED, + END_OPEN_INITIAL, + END_OPEN_FINAL, + END_OPEN_BOTH +}; + +class LPELineSegment : public Effect { +public: + LPELineSegment(LivePathEffectObject *lpeobject); + ~LPELineSegment() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +//private: + EnumParam end_type; + +private: + Geom::Point A, B; // intersections of the line segment with the limiting bounding box + Geom::Point bboxA, bboxB; // upper left and lower right corner of limiting bounding box + + LPELineSegment(const LPELineSegment&) = delete; + LPELineSegment& operator=(const LPELineSegment&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-measure-segments.cpp b/src/live_effects/lpe-measure-segments.cpp new file mode 100644 index 0000000..b480b3c --- /dev/null +++ b/src/live_effects/lpe-measure-segments.cpp @@ -0,0 +1,1285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz + * Some code and ideas migrated from dimensioning.py by + * Johannes B. Rutzmoser, johannes.rutzmoser (at) googlemail (dot) com + * https://github.com/Rutzmoser/inkscape_dimensioning + * Copyright (C) 2014 Author(s) + + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpe-measure-segments.h" +#include "2geom/affine.h" +#include "2geom/angle.h" +#include "2geom/point.h" +#include "2geom/ray.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "text-editing.h" +#include "object/sp-defs.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/svg-length.h" +#include "util/units.h" +#include "xml/node.h" +#include "xml/sp-css-attr.h" +#include "libnrtype/Layout-TNG.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" +#include "preferences.h" +#include "path-chemistry.h" +#include "style.h" + +#include +#include +#include +#include + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using namespace Geom; +namespace Inkscape { +namespace LivePathEffect { + + +static const Util::EnumData OrientationMethodData[] = { + { OM_HORIZONTAL , N_("Horizontal"), "horizontal" }, + { OM_VERTICAL , N_("Vertical") , "vertical" }, + { OM_PARALLEL , N_("Parallel") , "parallel" } +}; +static const Util::EnumDataConverter OMConverter(OrientationMethodData, OM_END); + +LPEMeasureSegments::LPEMeasureSegments(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), + orientation(_("Orientation"), _("Orientation of the line and labels"), "orientation", OMConverter, &wr, this, OM_PARALLEL, false), + coloropacity(_("Color and opacity"), _("Set color and opacity of the dimensions"), "coloropacity", &wr, this, 0x000000ff), + fontbutton(_("Font"), _("Select font for labels"), "fontbutton", &wr, this), + precision(_("Precision"), _("Number of digits after the decimal point"), "precision", &wr, this, 2), + fix_overlaps(_("Merge overlaps °"), _("Minimum angle at which overlapping dimension lines are merged into one, use 180° to disable merging"), "fix_overlaps", &wr, this, 0), + position(_("Position"), _("Distance of dimension line from the path"), "position", &wr, this, 5), + text_top_bottom(_("Label position"), _("Distance of the labels from the dimension line"), "text_top_bottom", &wr, this, 0), + helpline_distance(_("Help line distance"), _("Distance of the perpendicular lines from the path"), "helpline_distance", &wr, this, 0.0), + helpline_overlap(_("Help line elongation"), _("Distance of the perpendicular lines' ends from the dimension line"), "helpline_overlap", &wr, this, 2.0), + line_width(_("Line width"), _("Dimension line width. DIN standard: 0.25 or 0.35 mm"), "line_width", &wr, this, 0.25), + scale(_("Scale"), _("Scaling factor"), "scale", &wr, this, 1.0), + + // TRANSLATORS: Don't translate "{measure}" and "{unit}" variables. + format(_("Label format"), _("Label text format, available variables: {measure}, {unit}"), "format", &wr, this,"{measure}{unit}"), + blacklist(_("Blacklist segments"), _("Comma-separated list of indices of segments that should not be measured. You can use another LPE with different parameters to measure these."), "blacklist", &wr, this,""), + whitelist(_("Invert blacklist"), _("Use the blacklist as whitelist"), "whitelist", &wr, this, false), + showindex(_("Show segment index"), _("Display the index of the segments in the text label for easier blacklisting"), "showindex", &wr, this, false), + arrows_outside(_("Arrows outside"), _("Draw arrows pointing in the opposite direction outside the dimension line"), "arrows_outside", &wr, this, false), + flip_side(_("Flip side"), _("Draw dimension lines and labels on the other side of the path"), "flip_side", &wr, this, false), + scale_sensitive(_("Scale sensitive"), _("When the path is grouped and the group is then scaled, adjust the dimensions."), "scale_sensitive", &wr, this, true), + local_locale(_("Localize number format"), _("Use localized number formatting, e.g. '1,0' instead of '1.0' with German locale"), "local_locale", &wr, this, true), + rotate_anotation(_("Rotate labels"), _("Labels are parallel to the dimension line"), "rotate_anotation", &wr, this, true), + hide_back(_("Hide line under label"), _("Hide the dimension line where the label overlaps it"), "hide_back", &wr, this, true), + hide_arrows(_("Hide arrows"), _("Don't show any arrows"), "hide_arrows", &wr, this, false), + // active for 1.1 + smallx100(_("Multiply values < 1"), _("Multiply values smaller than 1 by 100 and leave out the unit"), "smallx100", &wr, this, false), + linked_items(_("Linked objects:"), _("Objects whose nodes are projected onto the path and generate new measurements"), "linked_items", &wr, this), + distance_projection(_("Distance"), _("Distance of the dimension lines from the outermost node"), "distance_projection", &wr, this, 20.0), + angle_projection(_("Angle of projection"), _("Angle of projection in 90° steps"), "angle_projection", &wr, this, 0.0), + active_projection(_("Activate projection"), _("Activate projection mode"), "active_projection", &wr, this, false), + avoid_overlapping(_("Avoid label overlap"), _("Rotate labels if the segment is shorter than the label"), "avoid_overlapping", &wr, this, true), + onbbox(_("Measure bounding box"), _("Add measurements for the geometrical bounding box"), "onbbox", &wr, this, false), + bboxonly(_("Only bounding box"), _("Measure only the geometrical bounding box"), "bboxonly", &wr, this, false), + centers(_("Add object center"), _("Add the projected object center"), "centers", &wr, this, false), + maxmin(_("Only max and min"), _("Compute only max/min projection values"), "maxmin", &wr, this, false), + helpdata(_("Help"), _("Measure segments help"), "helpdata", &wr, this, "", "") +{ + //set to true the parameters you want to be changed his default values + registerParameter(&unit); + registerParameter(&orientation); + registerParameter(&coloropacity); + registerParameter(&fontbutton); + registerParameter(&precision); + registerParameter(&fix_overlaps); + registerParameter(&position); + registerParameter(&text_top_bottom); + registerParameter(&helpline_distance); + registerParameter(&helpline_overlap); + registerParameter(&line_width); + registerParameter(&scale); + registerParameter(&format); + registerParameter(&blacklist); + registerParameter(&active_projection); + registerParameter(&whitelist); + registerParameter(&showindex); + registerParameter(&arrows_outside); + registerParameter(&flip_side); + registerParameter(&scale_sensitive); + registerParameter(&local_locale); + registerParameter(&rotate_anotation); + registerParameter(&hide_back); + registerParameter(&hide_arrows); + // active for 1.1 + registerParameter(&smallx100); + registerParameter(&linked_items); + registerParameter(&distance_projection); + registerParameter(&angle_projection); + registerParameter(&avoid_overlapping); + registerParameter(&onbbox); + registerParameter(&bboxonly); + registerParameter(¢ers); + registerParameter(&maxmin); + registerParameter(&helpdata); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring format_value = prefs->getString("/live_effects/measure-line/format"); + if(format_value.empty()){ + format_value = "{measure}{unit}"; + } + format.param_update_default(format_value.c_str()); + + format.param_hide_canvas_text(); + blacklist.param_hide_canvas_text(); + precision.param_set_range(0, 100); + precision.param_set_increments(1, 1); + precision.param_set_digits(0); + precision.param_make_integer(true); + fix_overlaps.param_set_range(0, 180); + fix_overlaps.param_set_increments(1, 1); + fix_overlaps.param_set_digits(0); + fix_overlaps.param_make_integer(true); + position.param_set_range(-999999.0, 999999.0); + position.param_set_increments(1, 1); + position.param_set_digits(2); + scale.param_set_range(-999999.0, 999999.0); + scale.param_set_increments(1, 1); + scale.param_set_digits(4); + text_top_bottom.param_set_range(-999999.0, 999999.0); + text_top_bottom.param_set_increments(1, 1); + text_top_bottom.param_set_digits(2); + line_width.param_set_range(0, 999999.0); + line_width.param_set_increments(0.1, 0.1); + line_width.param_set_digits(2); + helpline_distance.param_set_range(-999999.0, 999999.0); + helpline_distance.param_set_increments(1, 1); + helpline_distance.param_set_digits(2); + helpline_overlap.param_set_range(-999999.0, 999999.0); + helpline_overlap.param_set_increments(1, 1); + helpline_overlap.param_set_digits(2); + distance_projection.param_set_range(-999999.0, 999999.0); + distance_projection.param_set_increments(1, 1); + distance_projection.param_set_digits(5); + angle_projection.param_set_range(0.0, 360.0); + angle_projection.param_set_increments(90.0, 90.0); + angle_projection.param_set_digits(2); + locale_base = strdup(setlocale(LC_NUMERIC, nullptr)); + previous_size = 0; + pagenumber = 0; + anotation_width = 0; + fontsize = 0; + rgb32 = 0; + arrow_gap = 0; + //TODO: add newlines for 1.1 (not easy) + helpdata.param_update_default(_("General\n" + "Display and position dimension lines and labels\n\n" + "Projection\n" + "Show a line with measurements based on the selected items\n\n" + "Options\n" + "Options for color, precision, label formatting and display\n\n" + "Tips\n" + "Custom styling: To further customize the styles, " + "use the XML editor to find out the class or ID, then use the " + "Style dialog to apply a new style.\n" + "Blacklists: allow to hide some segments or projection steps.\n" + "Multiple Measure LPEs: In the same object, in conjunction with blacklists," + "this allows for labels and measurements with different orientations or additional projections.\n" + "Set Defaults: For every LPE, default values can be set at the bottom.")); +} + +LPEMeasureSegments::~LPEMeasureSegments() { + doOnRemove(nullptr); +} + +Gtk::Widget * +LPEMeasureSegments::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox() ); + vbox->set_border_width(0); + vbox->set_homogeneous(false); + vbox->set_spacing(0); + Gtk::VBox *vbox0 = Gtk::manage(new Gtk::VBox()); + vbox0->set_border_width(5); + vbox0->set_homogeneous(false); + vbox0->set_spacing(2); + Gtk::VBox *vbox1 = Gtk::manage(new Gtk::VBox()); + vbox1->set_border_width(5); + vbox1->set_homogeneous(false); + vbox1->set_spacing(2); + Gtk::VBox *vbox2 = Gtk::manage(new Gtk::VBox()); + vbox2->set_border_width(5); + vbox2->set_homogeneous(false); + vbox2->set_spacing(2); + //Help page + Gtk::VBox *vbox3 = Gtk::manage(new Gtk::VBox()); + vbox3->set_border_width(5); + vbox3->set_homogeneous(false); + vbox3->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = param->param_newWidget(); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if ( param->param_key == "linked_items") { + vbox1->pack_start(*widg, true, true, 2); + } else if (param->param_key == "active_projection" || + param->param_key == "distance_projection" || + param->param_key == "angle_projection" || + param->param_key == "maxmin" || + param->param_key == "centers" || + param->param_key == "bboxonly" || + param->param_key == "onbbox" ) + { + vbox1->pack_start(*widg, false, true, 2); + } else if (param->param_key == "precision" || + param->param_key == "coloropacity" || + param->param_key == "font" || + param->param_key == "format" || + param->param_key == "blacklist" || + param->param_key == "whitelist" || + param->param_key == "showindex" || + param->param_key == "local_locale" || + param->param_key == "hide_arrows" ) + { + vbox2->pack_start(*widg, false, true, 2); + } else if (//TOD: unhack for 1.1 + param->param_key == "smallx100" ) + { + Glib::ustring widgl = param->param_label; + size_t pos = widgl.find("<"); + if (pos != std::string::npos ) { + widgl.erase(pos, 1); + widgl.insert(pos, "<"); + } + param->param_label = widgl.c_str(); + vbox2->pack_start(*widg, false, true, 2); + } else if (param->param_key == "helpdata") + { + vbox3->pack_start(*widg, false, true, 2); + } else { + vbox0->pack_start(*widg, false, true, 2); + } + + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + + Gtk::Notebook * notebook = Gtk::manage(new Gtk::Notebook()); + notebook->append_page (*vbox0, Glib::ustring(_("General"))); + notebook->append_page (*vbox1, Glib::ustring(_("Projection"))); + notebook->append_page (*vbox2, Glib::ustring(_("Options"))); + notebook->append_page (*vbox3, Glib::ustring(_("Help"))); + vbox0->show_all(); + vbox1->show_all(); + vbox2->show_all(); + vbox3->show_all(); + vbox->pack_start(*notebook, true, true, 2); + notebook->set_current_page(pagenumber); + notebook->signal_switch_page().connect(sigc::mem_fun(*this, &LPEMeasureSegments::on_my_switch_page)); + if(Gtk::Widget* widg = defaultParamSet()) { + //Wrap to make it more omogenious + Gtk::VBox *vbox4 = Gtk::manage(new Gtk::VBox()); + vbox4->set_border_width(5); + vbox4->set_homogeneous(false); + vbox4->set_spacing(2); + vbox4->pack_start(*widg, true, true, 2); + vbox->pack_start(*vbox4, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPEMeasureSegments::on_my_switch_page(Gtk::Widget* page, guint page_number) +{ + if(!page->get_parent()->in_destruction()) { + pagenumber = page_number; + } +} + +void +LPEMeasureSegments::createArrowMarker(Glib::ustring mode) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item|| !sp_lpe_item->getId()) { + return; + } + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring style; + style = Glib::ustring("fill:context-stroke;"); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); + style = style + Glib::ustring(";stroke:none"); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = nullptr; + Inkscape::XML::Node *arrow = nullptr; + if ((elemref = document->getObjectById(mode.c_str()))) { + Inkscape::XML::Node *arrow= elemref->getRepr(); + if (arrow) { + arrow->setAttribute("sodipodi:insensitive", "true"); + arrow->removeAttribute("transform"); + Inkscape::XML::Node *arrow_data = arrow->firstChild(); + if (arrow_data) { + arrow_data->removeAttribute("transform"); + arrow_data->setAttribute("style", style); + } + } + } else { + arrow = xml_doc->createElement("svg:marker"); + arrow->setAttribute("id", mode); + Glib::ustring classarrow = itemid; + classarrow += " "; + classarrow += lpobjid; + classarrow += " measure-arrow-marker"; + arrow->setAttribute("class", classarrow); + arrow->setAttributeOrRemoveIfEmpty("inkscape:stockid", mode); + arrow->setAttribute("orient", "auto"); + arrow->setAttribute("refX", "0.0"); + arrow->setAttribute("refY", "0.0"); + + arrow->setAttribute("sodipodi:insensitive", "true"); + /* Create */ + Inkscape::XML::Node *arrow_path = xml_doc->createElement("svg:path"); + if (std::strcmp(mode.c_str(), "ArrowDIN-start") == 0) { + arrow_path->setAttribute("d", "M -8,0 8,-2.11 8,2.11 z"); + } else if (std::strcmp(mode.c_str(), "ArrowDIN-end") == 0) { + arrow_path->setAttribute("d", "M 8,0 -8,2.11 -8,-2.11 z"); + } else if (std::strcmp(mode.c_str(), "ArrowDINout-start") == 0) { + arrow_path->setAttribute("d", "M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z"); + } else { + arrow_path->setAttribute("d", "M 0,0 16,-2.11 16,-0.5 26,-0.5 26,0.5 16,0.5 16,2.11 z"); + } + Glib::ustring classarrowpath = itemid; + classarrowpath += " "; + classarrowpath += lpobjid; + classarrowpath += " measure-arrow"; + arrow_path->setAttributeOrRemoveIfEmpty("class", classarrowpath); + Glib::ustring arrowpath = mode + Glib::ustring("_path"); + arrow_path->setAttribute("id", arrowpath); + arrow_path->setAttribute("style", style); + arrow->addChild(arrow_path, nullptr); + Inkscape::GC::release(arrow_path); + elemref = SP_OBJECT(document->getDefs()->appendChildRepr(arrow)); + Inkscape::GC::release(arrow); + } + items.push_back(mode); +} + +void +LPEMeasureSegments::createTextLabel(Geom::Point pos, size_t counter, double length, Geom::Coord angle, bool remove, bool valid) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item || !sp_lpe_item->getId()) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring id = Glib::ustring("text-on-"); + id += Glib::ustring::format(counter); + id += "-"; + id += lpobjid; + SPObject *elemref = nullptr; + Inkscape::XML::Node *rtext = nullptr; + Inkscape::XML::Node *rtspan = nullptr; + Inkscape::XML::Node *rstring = nullptr; + elemref = document->getObjectById(id.c_str()); + if (elemref) { + rtext = elemref->getRepr(); + sp_repr_set_svg_double(rtext, "x", pos[Geom::X]); + sp_repr_set_svg_double(rtext, "y", pos[Geom::Y]); + rtext->setAttribute("sodipodi:insensitive", "true"); + rtext->removeAttribute("transform"); + rtspan = rtext->firstChild(); + rstring = rtspan->firstChild(); + rtspan->removeAttribute("x"); + rtspan->removeAttribute("y"); + Glib::ustring classlabel = itemid; + classlabel += " "; + classlabel += lpobjid; + classlabel += " measure-label"; + rtext->setAttribute("class", classlabel); + } else { + rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); + rtext->setAttribute("id", id); + Glib::ustring classlabel = itemid; + classlabel += " "; + classlabel += lpobjid; + classlabel += " measure-label"; + rtext->setAttribute("class", classlabel); + rtext->setAttribute("sodipodi:insensitive", "true"); + rtext->removeAttribute("transform"); + sp_repr_set_svg_double(rtext, "x", pos[Geom::X]); + sp_repr_set_svg_double(rtext, "y", pos[Geom::Y]); + rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); + rtspan->removeAttribute("x"); + rtspan->removeAttribute("y"); + elemref = document->getRoot()->appendChildRepr(rtext); + Inkscape::GC::release(rtext); + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + rstring = xml_doc->createTextNode(""); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + } + SPCSSAttr *css = sp_repr_css_attr_new(); + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + auto fontbutton_str = fontbutton.param_getSVGValue(); + fontlister->fill_css(css, fontbutton_str); + std::stringstream font_size; + setlocale (LC_NUMERIC, "C"); + font_size << fontsize << "px"; + setlocale (LC_NUMERIC, locale_base); + gchar c[32]; + sprintf(c, "#%06x", rgb32 >> 8); + sp_repr_css_set_property (css, "fill",c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + sp_repr_css_set_property (css, "fill-opacity",os.str().c_str()); + sp_repr_css_set_property (css, "font-size",font_size.str().c_str()); + sp_repr_css_unset_property (css, "-inkscape-font-specification"); + if (remove) { + sp_repr_css_set_property (css, "display","none"); + } + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rtext->setAttributeOrRemoveIfEmpty("style", css_str); + rtspan->setAttributeOrRemoveIfEmpty("style", css_str); + rtspan->removeAttribute("transform"); + sp_repr_css_attr_unref (css); + length = Inkscape::Util::Quantity::convert(length, display_unit.c_str(), unit.get_abbreviation()); + if (local_locale) { + setlocale (LC_NUMERIC, ""); + } else { + setlocale (LC_NUMERIC, "C"); + } + gchar length_str[64]; + bool x100 = false; + // active for 1.1 + if (smallx100 && length < 1 ) { + length *=100; + x100 = true; + g_snprintf(length_str, 64, "%.*f", (int)precision - 2, length); + } else { + g_snprintf(length_str, 64, "%.*f", (int)precision, length); + } + setlocale (LC_NUMERIC, locale_base); + auto label_value = format.param_getSVGValue(); + size_t s = label_value.find(Glib::ustring("{measure}"),0); + if(s < label_value.length()) { + label_value.replace(s, 9, length_str); + } + + s = label_value.find(Glib::ustring("{unit}"),0); + if(s < label_value.length()) { + if (x100) { + label_value.replace(s, 6, ""); + } else { + label_value.replace(s, 6, unit.get_abbreviation()); + } + } + + if (showindex) { + label_value = Glib::ustring("[") + Glib::ustring::format(counter) + Glib::ustring("] ") + label_value; + } + if (!valid) { + label_value = Glib::ustring(_("Non Uniform Scale")); + } + rstring->setContent(label_value.c_str()); + // this boring hack is to update the text with document scale inituialy loaded without root transform + Geom::OptRect bounds = SP_ITEM(elemref)->geometricBounds(); + if (bounds) { + anotation_width = bounds->width(); + sp_repr_set_svg_double(rtext, "x", pos[Geom::X] - (anotation_width / 2.0)); + rtspan->removeAttribute("style"); + } + + gchar * transform; + if (rotate_anotation) { + Geom::Affine affine = Geom::Affine(Geom::Translate(pos).inverse()); + angle = std::fmod(angle, 2*M_PI); + if (angle < 0) angle += 2*M_PI; + if (angle >= rad_from_deg(90) && angle < rad_from_deg(270)) { + angle = std::fmod(angle + rad_from_deg(180), 2*M_PI); + if (angle < 0) angle += 2*M_PI; + } + affine *= Geom::Rotate(angle); + affine *= Geom::Translate(pos); + transform = sp_svg_transform_write(affine); + } else { + transform = nullptr; + } + rtext->setAttribute("transform", transform); + g_free(transform); +} + +void +LPEMeasureSegments::createLine(Geom::Point start,Geom::Point end, Glib::ustring name, size_t counter, bool main, bool remove, bool arrows) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item || !sp_lpe_item->getId()) { + return; + } + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring id = name; + id += Glib::ustring::format(counter); + id += "-"; + id += lpobjid; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = document->getObjectById(id.c_str()); + Inkscape::XML::Node *line = nullptr; + if (!main) { + Geom::Ray ray(start, end); + Geom::Coord angle = ray.angle(); + start = start + Point::polar(angle, helpline_distance ); + end = end + Point::polar(angle, helpline_overlap ); + } + Geom::PathVector line_pathv; + + double k = (Geom::distance(start,end)/2.0) - (anotation_width/1.7); + if (main && + std::abs(text_top_bottom) < fontsize/1.5 && + hide_back && + k > 0) + { + //k = std::max(k , arrow_gap -1); + Geom::Ray ray(end, start); + Geom::Coord angle = ray.angle(); + Geom::Path line_path(start); + line_path.appendNew(start - Point::polar(angle, k)); + line_pathv.push_back(line_path); + line_path.clear(); + line_path.start(end + Point::polar(angle, k)); + line_path.appendNew(end); + line_pathv.push_back(line_path); + } else { + Geom::Path line_path(start); + line_path.appendNew(end); + line_pathv.push_back(line_path); + } + if (elemref) { + line = elemref->getRepr(); + gchar * line_str = sp_svg_write_path( line_pathv ); + line->setAttribute("d" , line_str); + line->removeAttribute("transform"); + g_free(line_str); + } else { + line = xml_doc->createElement("svg:path"); + line->setAttributeOrRemoveIfEmpty("id", id); + if (main) { + Glib::ustring classlinedim = itemid; + classlinedim += " "; + classlinedim += lpobjid; + classlinedim += " measure-DIM-line measure-line"; + line->setAttribute("class", classlinedim); + } else { + Glib::ustring classlinehelper = itemid; + classlinehelper += " "; + classlinehelper += lpobjid; + classlinehelper += " measure-helper-line measure-line"; + line->setAttribute("class", classlinehelper); + } + gchar * line_str = sp_svg_write_path( line_pathv ); + line->setAttribute("d" , line_str); + g_free(line_str); + } + + line->setAttribute("sodipodi:insensitive", "true"); + line_pathv.clear(); + + Glib::ustring style; + if (remove) { + style ="display:none;"; + } + if (main) { + line->setAttribute("inkscape:label", "dinline"); + if (!hide_arrows) { + if (arrows_outside) { + style += "marker-start:url(#ArrowDINout-start);marker-end:url(#ArrowDINout-end);"; + } else { + style += "marker-start:url(#ArrowDIN-start);marker-end:url(#ArrowDIN-end);"; + } + } + } else { + line->setAttribute("inkscape:label", "dinhelpline"); + } + std::stringstream stroke_w; + setlocale (LC_NUMERIC, "C"); + + double stroke_width = Inkscape::Util::Quantity::convert(line_width, unit.get_abbreviation(), display_unit.c_str()); + stroke_w << stroke_width; + setlocale (LC_NUMERIC, locale_base); + style += "stroke-width:"; + style += stroke_w.str(); + gchar c[32]; + sprintf(c, "#%06x", rgb32 >> 8); + style += ";stroke:"; + style += Glib::ustring(c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + style = style + Glib::ustring(";stroke-opacity:") + Glib::ustring(os.str()); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, style.c_str()); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + line->setAttributeOrRemoveIfEmpty("style", css_str); + if (!elemref) { + elemref = document->getRoot()->appendChildRepr(line); + Inkscape::GC::release(line); + } +} + +void +LPEMeasureSegments::doOnApply(SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE measure line can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + SPDocument *document = getSPDoc(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + Inkscape::XML::Node *styleNode = nullptr; + Inkscape::XML::Node* textNode = nullptr; + Inkscape::XML::Node *root = document->getReprRoot(); + for (unsigned i = 0; i < root->childCount(); ++i) { + if (Glib::ustring(root->nthChild(i)->name()) == "svg:style") { + styleNode = root->nthChild(i); + for (unsigned j = 0; j < styleNode->childCount(); ++j) { + if (styleNode->nthChild(j)->type() == Inkscape::XML::TEXT_NODE) { + textNode = styleNode->nthChild(j); + } + } + if (textNode == nullptr) { + // Style element found but does not contain text node! + std::cerr << "StyleDialog::_getStyleTextNode(): No text node!" << std::endl; + textNode = document->getReprDoc()->createTextNode(""); + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + } + } + + if (styleNode == nullptr) { + // Style element not found, create one + styleNode = document->getReprDoc()->createElement("svg:style"); + textNode = document->getReprDoc()->createTextNode(""); + root->addChild(styleNode, nullptr); + Inkscape::GC::release(styleNode); + + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + // To fix old meassuring files pre 1.0 + Glib::ustring styleContent = Glib::ustring(textNode->content()); + if (styleContent.find(".measure-arrow\n{\n") == std::string::npos) { + styleContent = styleContent + Glib::ustring("\n.measure-arrow") + Glib::ustring("\n{\n}"); + styleContent = styleContent + Glib::ustring("\n.measure-label") + Glib::ustring("\n{\n\n}"); + styleContent = styleContent + Glib::ustring("\n.measure-line") + Glib::ustring("\n{\n}"); + textNode->setContent(styleContent.c_str()); + } + DocumentUndo::setUndoSensitive(document, saved); +} + +bool +LPEMeasureSegments::isWhitelist (size_t i, std::string listsegments, bool whitelist) +{ + size_t s = listsegments.find(std::to_string(i) + std::string(","), 0); + if (s != std::string::npos) { + if (whitelist) { + return true; + } else { + return false; + } + } else { + if (whitelist) { + return false; + } else { + return true; + } + } + return false; +} + +double getAngle(Geom::Point p1, Geom::Point p2, Geom::Point p3, bool flip_side, double fix_overlaps) +{ + Geom::Ray ray_1(p2,p1); + Geom::Ray ray_2(p3,p1); + bool ccw_toggle = cross(p1 - p2, p3 - p2) < 0; + double angle = angle_between(ray_1, ray_2, ccw_toggle); + if (Geom::deg_from_rad(angle) < fix_overlaps || + Geom::deg_from_rad(angle) > 180 || + ((ccw_toggle && flip_side) || (!ccw_toggle && !flip_side))) + { + angle = 0; + } + return angle; +} + +std::vector< Point > +transformNodes(std::vector< Point > nodes, Geom::Affine transform) +{ + std::vector< Point > result; + for (auto & node : nodes) { + Geom::Point point = node; + result.push_back(point * transform); + } + return result; +} + +std::vector< Point > +getNodes(SPItem * item, Geom::Affine transform, bool onbbox, bool centers, bool bboxonly, double angle_projection) +{ + std::vector< Point > current_nodes; + SPShape * shape = dynamic_cast (item); + SPText * text = dynamic_cast (item); + SPGroup * group = dynamic_cast (item); + SPFlowtext * flowtext = dynamic_cast (item); + //TODO handle clones/use + + if (group) { + std::vector const item_list = sp_item_group_item_list(group); + for (auto sub_item : item_list) { + std::vector< Point > nodes = transformNodes(getNodes(sub_item, sub_item->transform, onbbox, centers, bboxonly, angle_projection), transform); + current_nodes.insert(current_nodes.end(), nodes.begin(), nodes.end()); + } + } else if (shape && !bboxonly) { + SPCurve * c = shape->getCurve(); + current_nodes = transformNodes(c->get_pathvector().nodes(), transform); + c->unref(); + } else if ((text || flowtext) && !bboxonly) { + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) { + break; + } + // get path from iter to iter_next: + SPCurve *curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { + continue; // error converting this glyph + } + if (curve->is_empty()) { // whitespace glyph? + curve->unref(); + continue; + } + std::vector< Point > letter_nodes = transformNodes(curve->get_pathvector().nodes(), transform); + current_nodes.insert(current_nodes.end(),letter_nodes.begin(),letter_nodes.end()); + if (iter == te_get_layout(item)->end()) { + break; + } + } while (true); + } else { + onbbox = true; + } + if (onbbox || centers) { + Geom::OptRect bbox = item->geometricBounds(); + if (bbox && onbbox) { + current_nodes.push_back((*bbox).corner(0) * transform); + current_nodes.push_back((*bbox).corner(2) * transform); + if (!Geom::are_near(angle_projection, 0.0) && + !Geom::are_near(angle_projection, 90.0) && + !Geom::are_near(angle_projection, 180.0) && + !Geom::are_near(angle_projection, 360.0)) + { + current_nodes.push_back((*bbox).corner(1) * transform); + current_nodes.push_back((*bbox).corner(3) * transform); + } + + } + if (bbox && centers) { + current_nodes.push_back((*bbox).midpoint() * transform); + } + } + return current_nodes; +} + + +void +LPEMeasureSegments::doBeforeEffect (SPLPEItem const* lpeitem) +{ + SPLPEItem * splpeitem = const_cast(lpeitem); + Glib::ustring lpobjid = this->lpeobj->getId(); + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + //Avoid crashes on previews + Inkscape::XML::Node *root = document->getReprRoot(); + Geom::Affine parentaffinetransform = i2anc_affine(SP_OBJECT(lpeitem->parent), SP_OBJECT(document->getRoot())); + Geom::Affine affinetransform = i2anc_affine(SP_OBJECT(lpeitem), SP_OBJECT(document->getRoot())); + Geom::Affine itemtransform = affinetransform * parentaffinetransform.inverse(); + //Projection prepare + Geom::PathVector pathvector; + std::vector< Point > nodes; + if (active_projection) { + Geom::OptRect bbox = sp_lpe_item->geometricBounds(); + if (bbox) { + Geom::Point mid = bbox->midpoint(); + double angle = Geom::rad_from_deg(angle_projection); + Geom::Affine transform = itemtransform; + transform *= Geom::Translate(mid).inverse(); + transform *= Geom::Rotate(angle).inverse(); + transform *= Geom::Translate(mid); + std::vector< Point > current_nodes = getNodes(splpeitem, transform, onbbox, centers, bboxonly, angle_projection); + nodes.insert(nodes.end(),current_nodes.begin(), current_nodes.end()); + for (auto & iter : linked_items._vector) { + SPObject *obj; + if (iter->ref.isAttached() && iter->actived && (obj = iter->ref.getObject()) && SP_IS_ITEM(obj)) { + SPItem * item = dynamic_cast(obj); + if (item) { + Geom::Affine affinetransform_sub = i2anc_affine(SP_OBJECT(item), SP_OBJECT(document->getRoot())); + Geom::Affine transform = affinetransform_sub ; + transform *= Geom::Translate(-mid); + transform *= Geom::Rotate(angle).inverse(); + transform *= Geom::Translate(mid); + std::vector< Point > current_nodes = getNodes(item, transform, onbbox, centers, bboxonly, angle_projection); + nodes.insert(nodes.end(),current_nodes.begin(), current_nodes.end()); + } + } + } + double maxdistance = -std::numeric_limits::max(); + std::vector result; + for (auto & node : nodes) { + Geom::Point point = node; + if (point[Geom::X] > maxdistance) { + maxdistance = point[Geom::X]; + } + result.push_back(point[Geom::Y]); + } + double dproj = Inkscape::Util::Quantity::convert(distance_projection, display_unit.c_str(), unit.get_abbreviation()); + Geom::Coord xpos = maxdistance + dproj; + std::sort (result.begin(), result.end()); + Geom::Path path; + Geom::Point prevpoint(Geom::infinity(),Geom::infinity()); + size_t counter = 0; + bool started = false; + Geom::Point point = Geom::Point(); + for (auto & iter : result) { + point = Geom::Point(xpos, iter); + if (Geom::are_near(prevpoint, point)){ + continue; + } + if (!started) { + path.setInitial(point); + started = true; + } else { + if (!maxmin) { + path.appendNew(point); + } + } + prevpoint = point; + } + if (maxmin) { + path.appendNew(point); + } + pathvector.push_back(path); + pathvector *= Geom::Translate(-mid); + pathvector *= Geom::Rotate(angle); + pathvector *= Geom::Translate(mid); + } + } + + //end projection prepare + SPShape *shape = dynamic_cast(splpeitem); + if (shape) { + //only check constrain viewbox on X + display_unit = document->getDisplayUnit()->abbr.c_str(); + guint32 color32 = coloropacity.get_value(); + bool colorchanged = false; + if (color32 != rgb32) { + colorchanged = true; + } + rgb32 = color32; + auto fontdesc_ustring = fontbutton.param_getSVGValue(); + Pango::FontDescription fontdesc(fontdesc_ustring); + double newfontsize = fontdesc.get_size() / (double)Pango::SCALE; + bool fontsizechanged = false; + if (newfontsize != fontsize) { + fontsize = Inkscape::Util::Quantity::convert(newfontsize, "pt", display_unit.c_str()); + fontsizechanged = true; + } + SPCurve *c = shape->getCurve(); + Geom::Point prev_stored = Geom::Point(0,0); + Geom::Point start_stored = Geom::Point(0,0); + Geom::Point end_stored = Geom::Point(0,0); + Geom::Point next_stored = Geom::Point(0,0); + if (!active_projection) { + pathvector = pathv_to_linear_and_cubic_beziers(c->get_pathvector()); + pathvector *= affinetransform; + } + c->unref(); + auto format_str = format.param_getSVGValue(); + if (format_str.empty()) { + format.param_setValue(Glib::ustring("{measure}{unit}")); + } + size_t ncurves = pathvector.curveCount(); + items.clear(); + double start_angle_cross = 0; + double end_angle_cross = 0; + gint counter = -1; + bool previous_fix_overlaps = true; + for (size_t i = 0; i < pathvector.size(); i++) { + size_t count = pathvector[i].size_default(); + if ( pathvector[i].closed()) { + const Geom::Curve &closingline = pathvector[i].back_closed(); + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + count = pathvector[i].size_open(); + } + } + for (size_t j = 0; j < count; j++) { + counter++; + gint fix_overlaps_degree = fix_overlaps; + Geom::Point prev = Geom::Point(0,0); + if (j == 0 && pathvector[i].closed()) { + prev = pathvector.pointAt(pathvector[i].size() - 1); + } else if (j != 0) { + prev = pathvector[i].pointAt(j - 1); + } + Geom::Point start = pathvector[i].pointAt(j); + Geom::Point end = pathvector[i].pointAt(j + 1); + Geom::Point next = Geom::Point(0,0); + if (pathvector[i].closed() && pathvector[i].size() == j+1){ + end = pathvector[i].pointAt(0); + next = pathvector[i].pointAt(1); + } else if (pathvector[i].size() > j + 1) { + next = pathvector[i].pointAt(j+2); + } + auto blacklist_str = blacklist.param_getSVGValue(); + std::string listsegments(blacklist_str.raw() + ","); + listsegments.erase(std::remove(listsegments.begin(), listsegments.end(), ' '), listsegments.end()); + if (isWhitelist(counter, listsegments, (bool)whitelist) && !Geom::are_near(start, end)) { + Glib::ustring idprev = Glib::ustring("infoline-on-start-"); + idprev += Glib::ustring::format(counter-1); + idprev += "-"; + idprev += lpobjid; + SPObject *elemref = document->getObjectById(idprev.c_str()); + if (elemref){ + SPPath* path = dynamic_cast(elemref); + if (path) { + SPCurve* prevcurve = path->getCurve(); + if (prevcurve) { + prev_stored = *prevcurve->first_point(); + } + prevcurve->unref(); + } + } + Glib::ustring idstart = Glib::ustring("infoline-on-start-"); + idstart += Glib::ustring::format(counter); + idstart += "-"; + idstart += lpobjid; + elemref = document->getObjectById(idstart.c_str()); + if (elemref) { + SPPath* path = dynamic_cast(elemref); + if (path) { + SPCurve* startcurve = path->getCurve(); + if (startcurve) { + start_stored = *startcurve->first_point(); + } + startcurve->unref(); + } + } + Glib::ustring idend = Glib::ustring("infoline-on-end-"); + idend += Glib::ustring::format(counter); + idend += "-"; + idend += lpobjid; + elemref = document->getObjectById(idend.c_str()); + if (elemref) { + SPPath* path = dynamic_cast(elemref); + if (path) { + SPCurve* endcurve = path->getCurve(); + if (endcurve) { + end_stored = *endcurve->first_point(); + } + endcurve->unref(); + } + } + Glib::ustring idnext = Glib::ustring("infoline-on-start-"); + idnext += Glib::ustring::format(counter+1); + idnext += "-"; + idnext += lpobjid; + elemref = document->getObjectById(idnext.c_str()); + if (elemref) { + SPPath* path = dynamic_cast(elemref); + if (path) { + SPCurve* nextcurve = path->getCurve(); + if (nextcurve) { + next_stored = *nextcurve->first_point(); + } + nextcurve->unref(); + } + } + Glib::ustring infoline_on_start = "infoline-on-start-"; + infoline_on_start += Glib::ustring::format(counter); + infoline_on_start += "-"; + infoline_on_start += lpobjid; + + Glib::ustring infoline_on_end = "infoline-on-end-"; + infoline_on_end += Glib::ustring::format(counter); + infoline_on_end += "-"; + infoline_on_end += lpobjid; + + Glib::ustring infoline = "infoline-"; + infoline += Glib::ustring::format(counter); + infoline += "-"; + infoline += lpobjid; + + Glib::ustring texton = "text-on-"; + texton += Glib::ustring::format(counter); + texton += "-"; + texton += lpobjid; + items.push_back(infoline_on_start); + items.push_back(infoline_on_end); + items.push_back(infoline); + items.push_back(texton); + if (!hide_arrows) { + if (arrows_outside) { + items.emplace_back("ArrowDINout-start"); + items.emplace_back("ArrowDINout-end"); + } else { + items.emplace_back("ArrowDIN-start"); + items.emplace_back("ArrowDIN-end"); + } + } + if (((Geom::are_near(prev, prev_stored, 0.01) && Geom::are_near(next, next_stored, 0.01)) || + fix_overlaps_degree == 180) && + Geom::are_near(start, start_stored, 0.01) && Geom::are_near(end, end_stored, 0.01) && + !this->refresh_widgets && !colorchanged && !fontsizechanged && !is_load && anotation_width) + { + continue; + } + Geom::Point hstart = start; + Geom::Point hend = end; + bool remove = false; + if (orientation == OM_VERTICAL) { + Coord xpos = std::max(hstart[Geom::X],hend[Geom::X]); + if (flip_side) { + xpos = std::min(hstart[Geom::X],hend[Geom::X]); + } + hstart[Geom::X] = xpos; + hend[Geom::X] = xpos; + if (hstart[Geom::Y] > hend[Geom::Y]) { + swap(hstart,hend); + swap(start,end); + } + if (Geom::are_near(hstart[Geom::Y], hend[Geom::Y])) { + remove = true; + } + } else if (orientation == OM_HORIZONTAL) { + Coord ypos = std::max(hstart[Geom::Y],hend[Geom::Y]); + if (flip_side) { + ypos = std::min(hstart[Geom::Y],hend[Geom::Y]); + } + hstart[Geom::Y] = ypos; + hend[Geom::Y] = ypos; + if (hstart[Geom::X] < hend[Geom::X]) { + swap(hstart,hend); + swap(start,end); + } + if (Geom::are_near(hstart[Geom::X], hend[Geom::X])) { + remove = true; + } + } else if (fix_overlaps_degree != 180) { + start_angle_cross = getAngle( start, prev, end, flip_side, fix_overlaps_degree); + if (prev == Geom::Point(0,0)) { + start_angle_cross = 0; + } + end_angle_cross = getAngle(end, start, next, flip_side, fix_overlaps_degree); + if (next == Geom::Point(0,0)) { + end_angle_cross = 0; + } + } + if (remove) { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + continue; + } + Geom::Ray ray(hstart,hend); + Geom::Coord angle = ray.angle(); + if (flip_side) { + angle = std::fmod(angle + rad_from_deg(180), 2*M_PI); + if (angle < 0) angle += 2*M_PI; + } + Geom::Coord angle_cross = std::fmod(angle + rad_from_deg(90), 2*M_PI); + if (angle_cross < 0) angle_cross += 2*M_PI; + angle = std::fmod(angle, 2*M_PI); + if (angle < 0) angle += 2*M_PI; + double turn = Geom::rad_from_deg(-90); + if (flip_side) { + end_angle_cross *= -1; + start_angle_cross *= -1; + //turn *= -1; + } + double position_turned_start = position / sin(start_angle_cross/2.0); + double length = Geom::distance(start,end); + if (fix_overlaps_degree != 180 && + start_angle_cross != 0 && + position_turned_start < length && + previous_fix_overlaps) + { + hstart = hstart - Point::polar(angle_cross - (start_angle_cross/2.0) - turn, position_turned_start); + } else { + hstart = hstart - Point::polar(angle_cross, position); + } + createLine(start, hstart, Glib::ustring("infoline-on-start-"), counter, false, false); + double position_turned_end = position / sin(end_angle_cross/2.0); + double endlength = Geom::distance(end,next); + if (fix_overlaps_degree != 180 && + end_angle_cross != 0 && + position_turned_end < length && + position_turned_end < endlength) + { + hend = hend - Point::polar(angle_cross + (end_angle_cross/2.0) + turn, position_turned_end); + previous_fix_overlaps = true; + } else { + hend = hend - Point::polar(angle_cross, position); + previous_fix_overlaps = false; + } + length = Geom::distance(start,end) * scale; + Geom::Point pos = Geom::middle_point(hstart, hend); + if (!hide_arrows) { + if (arrows_outside) { + createArrowMarker(Glib::ustring("ArrowDINout-start")); + createArrowMarker(Glib::ustring("ArrowDINout-end")); + } else { + createArrowMarker(Glib::ustring("ArrowDIN-start")); + createArrowMarker(Glib::ustring("ArrowDIN-end")); + } + } + if (angle >= rad_from_deg(90) && angle < rad_from_deg(270)) { + pos = pos - Point::polar(angle_cross, text_top_bottom + (fontsize/2.5)); + } else { + pos = pos + Point::polar(angle_cross, text_top_bottom + (fontsize/2.5)); + } + double parents_scale = (parentaffinetransform.expansionX() + parentaffinetransform.expansionY()) / 2.0; + if (!scale_sensitive) { + length /= parents_scale; + } + if ((anotation_width/2) > Geom::distance(hstart,hend)/2.0) { + if (avoid_overlapping) { + pos = pos - Point::polar(angle_cross, position + (anotation_width/2.0)); + angle += Geom::rad_from_deg(90); + } else { + pos = pos - Point::polar(angle_cross, position); + } + } + if (!scale_sensitive && !parentaffinetransform.preservesAngles()) { + createTextLabel(pos, counter, length, angle, remove, false); + } else { + createTextLabel(pos, counter, length, angle, remove, true); + } + arrow_gap = 8 * Inkscape::Util::Quantity::convert(line_width, unit.get_abbreviation(), display_unit.c_str()); + SPCSSAttr *css = sp_repr_css_attr_new(); + + setlocale (LC_NUMERIC, "C"); + double width_line = atof(sp_repr_css_property(css,"stroke-width","-1")); + setlocale (LC_NUMERIC, locale_base); + if (width_line > -0.0001) { + arrow_gap = 8 * Inkscape::Util::Quantity::convert(width_line, unit.get_abbreviation(), display_unit.c_str()); + } + if(flip_side) { + arrow_gap *= -1; + } + if(hide_arrows) { + arrow_gap *= 0; + } + createLine(end, hend, Glib::ustring("infoline-on-end-"), counter, false, false); + if (!arrows_outside) { + hstart = hstart + Point::polar(angle, arrow_gap); + hend = hend - Point::polar(angle, arrow_gap ); + } + if ((Geom::distance(hstart, hend) / 2.0) > (anotation_width / 1.9) + arrow_gap) { + createLine(hstart, hend, Glib::ustring("infoline-"), counter, true, false, true); + } else { + createLine(hstart, hend, Glib::ustring("infoline-"), counter, true, true, true); + } + } else { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + } + } + } + if (previous_size) { + for (size_t counter = ncurves; counter < previous_size; counter++) { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + } + } + previous_size = ncurves; + } +} + +void +LPEMeasureSegments::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPEMeasureSegments::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + //set "keep paths" hook on sp-lpe-item.cpp + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + items.clear(); + return; + } + processObjects(LPE_ERASE); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offset:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-measure-segments.h b/src/live_effects/lpe-measure-segments.h new file mode 100644 index 0000000..ea7c8e7 --- /dev/null +++ b/src/live_effects/lpe-measure-segments.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_MEASURE_SEGMENTS_H +#define INKSCAPE_LPE_MEASURE_SEGMENTS_H + +/* + * Author(s): + * Jabiertxo Arraiza Cenoz + * + * Copyright (C) 2014 Author(s) + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/originalitemarray.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/colorpicker.h" +#include "live_effects/parameter/fontbutton.h" +#include "live_effects/parameter/message.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/unit.h" + + +namespace Inkscape { +namespace LivePathEffect { + +enum OrientationMethod { + OM_HORIZONTAL, + OM_VERTICAL, + OM_PARALLEL, + OM_END +}; + +class LPEMeasureSegments : public Effect { +public: + LPEMeasureSegments(LivePathEffectObject *lpeobject); + ~LPEMeasureSegments() override; + void doOnApply(SPLPEItem const* lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* /*lpeitem*/) override; + void doEffect (SPCurve * curve) override {}; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void createLine(Geom::Point start,Geom::Point end, Glib::ustring name, size_t counter, bool main, bool remove, bool arrows = false); + void createTextLabel(Geom::Point pos, size_t counter, double length, Geom::Coord angle, bool remove, bool valid); + void createArrowMarker(Glib::ustring mode); + bool isWhitelist(size_t i, std::string listsegments, bool whitelist); + void on_my_switch_page(Gtk::Widget* page, guint page_number); +private: + UnitParam unit; + EnumParam orientation; + ColorPickerParam coloropacity; + FontButtonParam fontbutton; + ScalarParam precision; + ScalarParam fix_overlaps; + ScalarParam position; + ScalarParam text_top_bottom; + ScalarParam helpline_distance; + ScalarParam helpline_overlap; + ScalarParam line_width; + ScalarParam scale; + TextParam format; + TextParam blacklist; + BoolParam active_projection; + BoolParam whitelist; + BoolParam showindex; + BoolParam arrows_outside; + BoolParam flip_side; + BoolParam scale_sensitive; + BoolParam local_locale; + BoolParam rotate_anotation; + BoolParam hide_back; + BoolParam hide_arrows; + BoolParam onbbox; + BoolParam bboxonly; + BoolParam centers; + BoolParam maxmin; + BoolParam smallx100; + OriginalItemArrayParam linked_items; + ScalarParam distance_projection; + ScalarParam angle_projection; + BoolParam avoid_overlapping; + MessageParam helpdata; + Glib::ustring display_unit; + double fontsize; + double anotation_width; + double previous_size; + guint32 rgb32; + double arrow_gap; + guint pagenumber; + gchar const* locale_base; + LPEMeasureSegments(const LPEMeasureSegments &) = delete; + LPEMeasureSegments &operator=(const LPEMeasureSegments &) = delete; + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-mirror_symmetry.cpp b/src/live_effects/lpe-mirror_symmetry.cpp new file mode 100644 index 0000000..d72eaf9 --- /dev/null +++ b/src/live_effects/lpe-mirror_symmetry.cpp @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation: mirrors a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Abhishek Sharma + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilin Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-mirror_symmetry.h" +#include "2geom/affine.h" +#include "2geom/path-intersection.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "path-chemistry.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include + +#include "object/sp-defs.h" +#include "object/sp-lpe-item.h" +#include "object/sp-path.h" +#include "object/sp-text.h" + +#include "xml/sp-css-attr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData ModeTypeData[] = { + { MT_V, N_("Vertical page center"), "vertical" }, + { MT_H, N_("Horizontal page center"), "horizontal" }, + { MT_FREE, N_("Freely defined mirror line"), "free" }, + { MT_X, N_("X coordinate of mirror line midpoint"), "X" }, + { MT_Y, N_("Y coordinate of mirror line midpoint"), "Y" } +}; +static const Util::EnumDataConverter +MTConverter(ModeTypeData, MT_END); + + +LPEMirrorSymmetry::LPEMirrorSymmetry(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + mode(_("Mode"), _("Set mode of transformation. Either freely defined by mirror line or constrained to certain symmetry points."), "mode", MTConverter, &wr, this, MT_FREE), + discard_orig_path(_("Discard original path"), _("Only keep mirrored part of the path, remove the original."), "discard_orig_path", &wr, this, false), + fuse_paths(_("Fuse paths"), _("Fuse original path and mirror image into a single path"), "fuse_paths", &wr, this, false), + oposite_fuse(_("Fuse opposite sides"), _("Picks the part on the other side of the mirror line as the original."), "oposite_fuse", &wr, this, false), + split_items(_("Split elements"), _("Split original and mirror image into separate paths, so each can have its own style."), "split_items", &wr, this, false), + start_point(_("Mirror line start"), _("Start point of mirror line"), "start_point", &wr, this, _("Adjust start point of of mirror line")), + end_point(_("Mirror line end"), _("End point of mirror line"), "end_point", &wr, this, _("Adjust end point of mirror line")), + center_point(_("Mirror line mid"), _("Center point of mirror line"), "center_point", &wr, this, _("Adjust center point of mirror line")) +{ + show_orig_path = true; + registerParameter(&mode); + registerParameter(&discard_orig_path); + registerParameter(&fuse_paths); + registerParameter(&oposite_fuse); + registerParameter(&split_items); + registerParameter(&start_point); + registerParameter(&end_point); + registerParameter(¢er_point); + apply_to_clippath_and_mask = true; + previous_center = Geom::Point(0,0); + center_point.param_widget_is_visible(false); + reset = false; + center_horiz = false; + center_vert = false; +} + +LPEMirrorSymmetry::~LPEMirrorSymmetry() += default; + +void +LPEMirrorSymmetry::doAfterEffect (SPLPEItem const* lpeitem) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + container = dynamic_cast(sp_lpe_item->parent); + Inkscape::XML::Node *root = document->getReprRoot(); + + if (split_items && !discard_orig_path) { + Geom::Line ls((Geom::Point)start_point, (Geom::Point)end_point); + Geom::Affine m = Geom::reflection (ls.vector(), (Geom::Point)start_point); + m *= sp_lpe_item->transform; + toMirror(m, reset); + reset = false; + } else { + processObjects(LPE_ERASE); + items.clear(); + } +} + +Gtk::Widget * +LPEMirrorSymmetry::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + Gtk::HBox * hbox = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::HBox * hbox2 = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::Button * center_vert_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Vertical center")))); + center_vert_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEMirrorSymmetry::centerVert)); + center_vert_button->set_size_request(110,20); + Gtk::Button * center_horiz_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Horizontal center")))); + center_horiz_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEMirrorSymmetry::centerHoriz)); + center_horiz_button->set_size_request(110,20); + Gtk::Button * reset_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset styles")))); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEMirrorSymmetry::resetStyles)); + reset_button->set_size_request(110,20); + vbox->pack_start(*hbox, true,true,2); + vbox->pack_start(*hbox2, true,true,2); + hbox->pack_start(*reset_button, false, false,2); + hbox2->pack_start(*center_vert_button, false, false,2); + hbox2->pack_start(*center_horiz_button, false, false,2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPEMirrorSymmetry::centerVert(){ + center_vert = true; + refresh_widgets = true; + writeParamsToSVG(); +} + +void +LPEMirrorSymmetry::centerHoriz(){ + center_horiz = true; + refresh_widgets = true; + writeParamsToSVG(); +} + +void +LPEMirrorSymmetry::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + Point point_a(boundingbox_X.max(), boundingbox_Y.min()); + Point point_b(boundingbox_X.max(), boundingbox_Y.max()); + Point point_c(boundingbox_X.middle(), boundingbox_Y.middle()); + if (center_vert) { + center_point.param_setValue(point_c); + end_point.param_setValue(Geom::Point(boundingbox_X.middle(), boundingbox_Y.min())); + //force update + start_point.param_setValue(Geom::Point(boundingbox_X.middle(), boundingbox_Y.max()),true); + center_vert = false; + } else if (center_horiz) { + center_point.param_setValue(point_c); + end_point.param_setValue(Geom::Point(boundingbox_X.max(), boundingbox_Y.middle())); + start_point.param_setValue(Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()),true); + //force update + center_horiz = false; + } else { + + if (mode == MT_Y) { + point_a = Geom::Point(boundingbox_X.min(),center_point[Y]); + point_b = Geom::Point(boundingbox_X.max(),center_point[Y]); + } + if (mode == MT_X) { + point_a = Geom::Point(center_point[X],boundingbox_Y.min()); + point_b = Geom::Point(center_point[X],boundingbox_Y.max()); + } + if ((Geom::Point)start_point == (Geom::Point)end_point) { + start_point.param_setValue(point_a); + end_point.param_setValue(point_b); + previous_center = Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + center_point.param_setValue(previous_center); + return; + } + if ( mode == MT_X || mode == MT_Y ) { + if (!are_near(previous_center, (Geom::Point)center_point, 0.01)) { + center_point.param_setValue(Geom::middle_point(point_a, point_b)); + end_point.param_setValue(point_b); + start_point.param_setValue(point_a); + } else { + if ( mode == MT_X ) { + if (!are_near(start_point[X], point_a[X], 0.01)) { + start_point.param_setValue(point_a); + } + if (!are_near(end_point[X], point_b[X], 0.01)) { + end_point.param_setValue(point_b); + } + } else { //MT_Y + if (!are_near(start_point[Y], point_a[Y], 0.01)) { + start_point.param_setValue(point_a); + } + if (!are_near(end_point[Y], point_b[Y], 0.01)) { + end_point.param_setValue(point_b); + } + } + } + } else if ( mode == MT_FREE) { + if (are_near(previous_center, (Geom::Point)center_point, 0.01)) { + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + + } else { + Geom::Point trans = center_point - Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + start_point.param_setValue(start_point * trans); + end_point.param_setValue(end_point * trans); + } + } else if ( mode == MT_V){ + SPDocument *document = getSPDoc(); + if (document) { + Geom::Affine transform = i2anc_affine(SP_OBJECT(lpeitem), nullptr).inverse(); + Geom::Point sp = Geom::Point(document->getWidth().value("px")/2.0, 0) * transform; + start_point.param_setValue(sp); + Geom::Point ep = Geom::Point(document->getWidth().value("px")/2.0, document->getHeight().value("px")) * transform; + end_point.param_setValue(ep); + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + } + } else { //horizontal page + SPDocument *document = getSPDoc(); + if (document) { + Geom::Affine transform = i2anc_affine(SP_OBJECT(lpeitem), nullptr).inverse(); + Geom::Point sp = Geom::Point(0, document->getHeight().value("px")/2.0) * transform; + start_point.param_setValue(sp); + Geom::Point ep = Geom::Point(document->getWidth().value("px"), document->getHeight().value("px")/2.0) * transform; + end_point.param_setValue(ep); + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + } + } + } + previous_center = center_point; +} + +void LPEMirrorSymmetry::cloneStyle(SPObject *orig, SPObject *dest) +{ + dest->getRepr()->setAttribute("style", orig->getRepr()->attribute("style")); + for (auto iter : orig->style->properties()) { + if (iter->style_src != SP_STYLE_SRC_UNSET) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) { + const gchar *attr = orig->getRepr()->attribute(iter->name().c_str()); + if (attr) { + dest->getRepr()->setAttribute(iter->name(), attr); + } + } + } + } +} + +void +LPEMirrorSymmetry::cloneD(SPObject *orig, SPObject *dest, bool reset) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child, reset); + index++; + } + return; + } + + if (SP_IS_TEXT(orig) && SP_IS_TEXT(dest) && SP_TEXT(orig)->children.size() == SP_TEXT(dest)->children.size()) { + if (reset) { + cloneStyle(orig, dest); + } + size_t index = 0; + for (auto &child : SP_TEXT(orig)->children) { + SPObject *dest_child = dest->nthChild(index); + cloneD(&child, dest_child, reset); + index++; + } + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + if (path && shape) { + SPCurve *c = shape->getCurve(); + if (c) { + gchar *str = sp_svg_write_path(c->get_pathvector()); + dest->getRepr()->setAttribute("d", str); + g_free(str); + c->unref(); + } else { + dest->getRepr()->removeAttribute("d"); + } + } + if (reset) { + cloneStyle(orig, dest); + } +} + +Inkscape::XML::Node * +LPEMirrorSymmetry::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + std::vector const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + return resultnode; +} + +void +LPEMirrorSymmetry::toMirror(Geom::Affine transform, bool reset) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Glib::ustring elemref_id = Glib::ustring("mirror-"); + elemref_id += this->lpeobj->getId(); + items.clear(); + items.push_back(elemref_id); + SPObject *elemref = document->getObjectById(elemref_id.c_str()); + Inkscape::XML::Node *phantom = nullptr; + if (elemref) { + phantom = elemref->getRepr(); + } else { + phantom = createPathBase(sp_lpe_item); + phantom->setAttribute("id", elemref_id); + reset = true; + elemref = container->appendChildRepr(phantom); + Inkscape::GC::release(phantom); + } + cloneD(SP_OBJECT(sp_lpe_item), elemref, reset); + gchar *str = sp_svg_transform_write(transform); + elemref->getRepr()->setAttribute("transform" , str); + g_free(str); + if (elemref->parent != container) { + Inkscape::XML::Node *copy = phantom->duplicate(xml_doc); + copy->setAttribute("id", elemref_id); + container->appendChildRepr(copy); + Inkscape::GC::release(copy); + elemref->deleteObject(); + } +} + + +void +LPEMirrorSymmetry::resetStyles(){ + reset = true; + doAfterEffect_impl(sp_lpe_item); +} + + +//TODO: Migrate the tree next function to effect.cpp/h to avoid duplication +void +LPEMirrorSymmetry::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPEMirrorSymmetry::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + //set "keep paths" hook on sp-lpe-item.cpp + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + items.clear(); + return; + } + processObjects(LPE_ERASE); +} + +void +LPEMirrorSymmetry::doOnApply (SPLPEItem const* lpeitem) +{ + using namespace Geom; + + original_bbox(lpeitem, false, true); + + Point point_a(boundingbox_X.max(), boundingbox_Y.min()); + Point point_b(boundingbox_X.max(), boundingbox_Y.max()); + Point point_c(boundingbox_X.max(), boundingbox_Y.middle()); + start_point.param_setValue(point_a, true); + start_point.param_update_default(point_a); + end_point.param_setValue(point_b, true); + end_point.param_update_default(point_b); + center_point.param_setValue(point_c, true); + previous_center = center_point; + SPLPEItem * splpeitem = const_cast(lpeitem); +} + + +Geom::PathVector +LPEMirrorSymmetry::doEffect_path (Geom::PathVector const & path_in) +{ + if (split_items && !fuse_paths) { + return path_in; + } + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector path_out; + + if (!discard_orig_path && !fuse_paths) { + path_out = pathv_to_linear_and_cubic_beziers(path_in); + } + + Geom::Line line_separation((Geom::Point)start_point, (Geom::Point)end_point); + Geom::Affine m = Geom::reflection (line_separation.vector(), (Geom::Point)start_point); + if (fuse_paths && !discard_orig_path) { + for (const auto & path_it : original_pathv) + { + if (path_it.empty()) { + continue; + } + Geom::PathVector tmp_pathvector; + double time_start = 0.0; + int position = 0; + bool end_open = false; + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + if (!are_near(closingline.initialPoint(), closingline.finalPoint())) { + end_open = true; + } + } + Geom::Path original = path_it; + if (end_open && path_it.closed()) { + original.close(false); + original.appendNew( original.initialPoint() ); + original.close(true); + } + Geom::Point s = start_point; + Geom::Point e = end_point; + double dir = line_separation.angle(); + double diagonal = Geom::distance(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + Geom::Rect bbox(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + double size_divider = Geom::distance(center_point, bbox) + diagonal; + s = Geom::Point::polar(dir,size_divider) + center_point; + e = Geom::Point::polar(dir + Geom::rad_from_deg(180),size_divider) + center_point; + Geom::Path divider = Geom::Path(s); + divider.appendNew(e); + Geom::Crossings cs = crossings(original, divider); + std::vector crossed; + for(auto & c : cs) { + crossed.push_back(c.ta); + } + std::sort(crossed.begin(), crossed.end()); + bool swamped = false; + if (crossed.size()) { + swamped = crossed[0] > crossed[crossed.size() - 1]; + } + for (unsigned int i = 0; i < crossed.size(); i++) { + double time_end = crossed[i]; + if (time_start != time_end && time_end - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, time_end); + if (!portion.empty()) { + Geom::Point middle = portion.pointAt((double)portion.size()/2.0); + position = Geom::sgn(Geom::cross(e - s, middle - s)); + if (!oposite_fuse) { + position *= -1; + } + if (position == 1) { + if (!split_items) { + Geom::Path mirror = portion.reversed() * m; + mirror.setInitial(portion.finalPoint()); + portion.append(mirror); + if(i != 0) { + portion.setFinal(portion.initialPoint()); + portion.close(); + } + } else if (path_it.closed() && swamped) { + portion.close(); + } + tmp_pathvector.push_back(portion); + } + portion.clear(); + } + } + time_start = time_end; + } + position = Geom::sgn(Geom::cross(e - s, original.finalPoint() - s)); + if (!oposite_fuse) { + position *= -1; + } + if (cs.size()!=0 && (position == 1)) { + if (time_start != original.size() && original.size() - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, original.size()); + if (!portion.empty()) { + portion = portion.reversed(); + if (!split_items) { + Geom::Path mirror = portion.reversed() * m; + mirror.setInitial(portion.finalPoint()); + portion.append(mirror); + } + portion = portion.reversed(); + if (!original.closed()) { + tmp_pathvector.push_back(portion); + } else { + if (cs.size() > 1 && tmp_pathvector.size() > 0 && tmp_pathvector[0].size() > 0 ) { + if (swamped || !split_items) { + portion.setFinal(tmp_pathvector[0].initialPoint()); + portion.setInitial(tmp_pathvector[0].finalPoint()); + } else { + tmp_pathvector[0] = tmp_pathvector[0].reversed(); + portion = portion.reversed(); + portion.setInitial(tmp_pathvector[0].finalPoint()); + } + tmp_pathvector[0].append(portion); + } else { + tmp_pathvector.push_back(portion); + } + tmp_pathvector[0].close(); + } + portion.clear(); + } + } + } + if (cs.size() == 0 && position == 1) { + tmp_pathvector.push_back(original); + tmp_pathvector.push_back(original * m); + } + path_out.insert(path_out.end(), tmp_pathvector.begin(), tmp_pathvector.end()); + tmp_pathvector.clear(); + } + } else if (!fuse_paths || discard_orig_path) { + for (const auto & i : original_pathv) { + path_out.push_back(i * m); + } + } + return path_out; +} + +void +LPEMirrorSymmetry::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path path; + Geom::Point s = start_point; + Geom::Point e = end_point; + path.start( s ); + path.appendNew( e ); + Geom::PathVector helper; + helper.push_back(path); + hp_vec.push_back(helper); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-mirror_symmetry.h b/src/live_effects/lpe-mirror_symmetry.h new file mode 100644 index 0000000..aaddb38 --- /dev/null +++ b/src/live_effects/lpe-mirror_symmetry.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_MIRROR_SYMMETRY_H +#define INKSCAPE_LPE_MIRROR_SYMMETRY_H + +/** \file + * LPE implementation: mirrors a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilin Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum ModeType { + MT_V, + MT_H, + MT_FREE, + MT_X, + MT_Y, + MT_END +}; + +class LPEMirrorSymmetry : public Effect, GroupBBoxEffect { +public: + LPEMirrorSymmetry(LivePathEffectObject *lpeobject); + ~LPEMirrorSymmetry() override; + void doOnApply (SPLPEItem const* lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void cloneStyle(SPObject *orig, SPObject *dest); + void toMirror(Geom::Affine transform, bool reset); + void cloneD(SPObject *orig, SPObject *dest, bool reset); + Inkscape::XML::Node * createPathBase(SPObject *elemref); + void resetStyles(); + void centerVert(); + void centerHoriz(); + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + +private: + EnumParam mode; + BoolParam discard_orig_path; + BoolParam fuse_paths; + BoolParam oposite_fuse; + BoolParam split_items; + PointParam start_point; + PointParam end_point; + PointParam center_point; + Geom::Point previous_center; + SPObject * container; + bool reset; + bool center_vert; + bool center_horiz; + LPEMirrorSymmetry(const LPEMirrorSymmetry&) = delete; + LPEMirrorSymmetry& operator=(const LPEMirrorSymmetry&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-offset.cpp b/src/live_effects/lpe-offset.cpp new file mode 100644 index 0000000..804c677 --- /dev/null +++ b/src/live_effects/lpe-offset.cpp @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ +/* + * Authors: + * Maximilian Albert + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * Copyright (C) Jabierto Arraiza 2015 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/lpe-offset.h" +#include "display/curve.h" +#include "inkscape.h" +#include "helper/geom.h" +#include "helper/geom-pathstroke.h" +#include <2geom/sbasis-to-bezier.h> +#include <2geom/piecewise.h> +#include <2geom/path-intersection.h> +#include <2geom/intersection-graph.h> +#include <2geom/elliptical-arc.h> +#include <2geom/angle.h> +#include <2geom/curve.h> +#include "object/sp-shape.h" +#include "knot-holder-entity.h" +#include "knotholder.h" +#include "util/units.h" +#include "knot.h" +#include + +#include "splivarot.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "svg/svg.h" + +#include <2geom/elliptical-arc.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +namespace OfS { + class KnotHolderEntityOffsetPoint : public LPEKnotHolderEntity { + public: + KnotHolderEntityOffsetPoint(LPEOffset * effect) : LPEKnotHolderEntity(effect) {} + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + private: + }; +} // OfS + + +static const Util::EnumData JoinTypeData[] = { + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_MITER_CLIP, N_("Miter Clip"), "miter-clip"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"}, + {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"}, + {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"}, + {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"}, +}; + +static const Util::EnumDataConverter JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData)); + + +LPEOffset::LPEOffset(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), + offset(_("Offset:"), _("Offset"), "offset", &wr, this, 0.0), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", JoinTypeConverter, &wr, this, JOIN_MITER), + miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 4.0), + attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, true), + update_on_knot_move(_("Live update"), _("Update while moving handle"), "update_on_knot_move", &wr, this, true) +{ + show_orig_path = true; + registerParameter(&linejoin_type); + registerParameter(&unit); + registerParameter(&offset); + registerParameter(&miter_limit); + registerParameter(&attempt_force_join); + registerParameter(&update_on_knot_move); + offset.param_set_increments(0.1, 0.1); + offset.param_set_digits(4); + offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); + _knot_entity = nullptr; + _provides_knotholder_entities = true; + apply_to_clippath_and_mask = true; + prev_unit = unit.get_abbreviation(); +} + +LPEOffset::~LPEOffset() += default; + +typedef FillRule FillRuleFlatten; + +static void +sp_flatten(Geom::PathVector &pathvector, FillRuleFlatten fillkind) +{ + Path *orig = new Path; + orig->LoadPathVector(pathvector); + Shape *theShape = new Shape; + Shape *theRes = new Shape; + orig->ConvertWithBackData (1.0); + orig->Fill (theShape, 0); + theRes->ConvertToShape (theShape, FillRule(fillkind)); + Path *originaux[1]; + originaux[0] = orig; + Path *res = new Path; + theRes->ConvertToForme (res, 1, originaux, true); + + delete theShape; + delete theRes; + char *res_d = res->svg_dump_path (); + delete res; + delete orig; + pathvector = sp_svg_read_pathv(res_d); +} + +Geom::Point +LPEOffset::get_nearest_point(Geom::PathVector pathv, Geom::Point point) const +{ + Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); + boost::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(point); + if (pathvectortime) { + Geom::PathTime pathtime = pathvectortime->asPathTime(); + res = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); + } + return res; +} + +void LPEOffset::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + offset.param_transform_multiply(postmul, true); + offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); +} + +Geom::Point +LPEOffset::get_default_point(Geom::PathVector pathv) const +{ + Geom::Point origin = Geom::Point(Geom::infinity(), Geom::infinity()); + Geom::OptRect bbox = pathv.boundsFast(); + if (bbox) { + if (SP_IS_GROUP(sp_lpe_item)) { + origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); + } else { + origin = Geom::Point((*bbox).midpoint()[Geom::X],(*bbox).top()); + origin = get_nearest_point(pathv, origin); + } + + } + return origin; +} +double +sp_get_distance_point(Geom::PathVector pathv, Geom::Point origin) { + boost::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(origin); + Geom::Point nearest = origin; + if (pathvectortime) { + Geom::PathTime pathtime = pathvectortime->asPathTime(); + nearest = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); + // we are not on simplet case on a parallel offset + if (Geom::are_near(pathtime.t, 0) || Geom::are_near(pathtime.t, 1)) { + bool start = Geom::are_near(nearest, pathv[(*pathvectortime).path_index].initialPoint()); + bool end = Geom::are_near(nearest, pathv[(*pathvectortime).path_index].finalPoint()); + bool closed = Geom::are_near(pathv[(*pathvectortime).path_index].initialPoint(), + pathv[(*pathvectortime).path_index].finalPoint()); + double dist_corner = 0; + Geom::Point pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); + Geom::Point pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); + if (closed && start) { + pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); + pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); + } + if (!closed && (start || end)) { + if (start) { + pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index); + pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); + } + } + Geom::Line line; + line.setPoints(pa, pb); + line.setAngle(line.angle() + Geom::rad_from_deg(90)); + Geom::Point nearestline = line.pointAt(line.nearestTime(origin)); + boost::optional pathvectortime_line = pathv.nearestTime(nearestline); + if (pathvectortime_line) { + Geom::PathTime pathtime_line = pathvectortime_line->asPathTime(); + nearest = pathv[(*pathvectortime_line).path_index].pointAt(pathtime_line.curve_index + pathtime_line.t); + dist_corner = Geom::distance(nearestline, nearest); + } + if (closed || !(start || end)) { + if (closed && start) { + size_t sizepath = pathv[(*pathvectortime).path_index].size(); + pa = pathv[(*pathvectortime).path_index].pointAt(sizepath - 1); + pb = pathv[(*pathvectortime).path_index].pointAt(sizepath - 2); + } else { + pa = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 1); + pb = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + 2); + } + line; + line.setPoints(pa, pb); + line.setAngle(line.angle() + Geom::rad_from_deg(90)); + Geom::Point nearestline = line.pointAt(line.nearestTime(origin)); + boost::optional pathvectortime_line = pathv.nearestTime(nearestline); + if (pathvectortime_line) { + Geom::PathTime pathtime_line = pathvectortime_line->asPathTime(); + nearest = + pathv[(*pathvectortime_line).path_index].pointAt(pathtime_line.curve_index + pathtime_line.t); + return std::max(dist_corner, Geom::distance(nearestline, nearest)); + } + } + } + } + return Geom::distance(origin, nearest); +} + +double +LPEOffset::sp_get_offset(Geom::Point origin) +{ + SPGroup * group = dynamic_cast(sp_lpe_item); + double ret_offset = 0; + if (group) { + Geom::Point initial = get_default_point(filled_rule_pathv); + ret_offset = Geom::distance(origin, initial); + if (origin[Geom::Y] < initial[Geom::Y]) { + ret_offset *= -1; + } + return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale; + } + int winding_value = filled_rule_pathv.winding(origin); + bool inset = false; + if (winding_value % 2 != 0) { + inset = true; + } + + ret_offset = sp_get_distance_point(filled_rule_pathv, origin); + if (inset) { + ret_offset *= -1; + } + return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale; +} + +void +LPEOffset::addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) +{ + hp_vec.push_back(helper_path); +} + +void +LPEOffset::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem); + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (prev_unit != unit.get_abbreviation()) { + offset.param_set_value(Inkscape::Util::Quantity::convert(offset, prev_unit, unit.get_abbreviation())); + } + prev_unit = unit.get_abbreviation(); + SPGroup const *group = dynamic_cast(lpeitem); + this->scale = lpeitem->i2doc_affine().descrim(); + if (group) { + helper_path.clear(); + Geom::Point origin = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); + Geom::Point endpont = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); + endpont[Geom::Y] = endpont[Geom::Y] + Inkscape::Util::Quantity::convert(offset, unit.get_abbreviation(), "px")/ this->scale; + Geom::Path hp(origin); + hp.appendNew(endpont); + helper_path.push_back(hp); + } +} + +int offset_winding(Geom::PathVector pathvector, Geom::Path path) +{ + int wind = 0; + Geom::Point p = path.initialPoint(); + for (auto i:pathvector) { + if (i == path) continue; + if (!i.boundsFast().contains(p)) continue; + wind += i.winding(p); + } + return wind; +} + +/* Geom::Path +sp_get_outer(Geom::PathVector pathv) { + sp_flatten(pathv, fill_nonZero); + Geom::Path out_bounds; + Geom::Path out_size; + Geom::OptRect bounds; + double size = 0; + bool get_bounds = false; + for (auto path_child:pathv) { + Geom::OptRect path_bounds = path_child.boundsFast(); + if (path_bounds) { + double path_size = (*path_bounds).width() + (*path_bounds).height(); + if (path_bounds.contains(bounds)) { + bounds = path_bounds; + out_bounds = path_child; + get_bounds = true; + } + if (size < path_size) { + size = path_size; + out_size = path_child; + } + } + } + pathv.clear(); + if (get_bounds) { + return out_bounds; + } + return out_size; +} + +Geom::PathVector +sp_get_inner(Geom::PathVector pathv, Geom::Path outer) { + Geom::PathVector out; + for (auto path_child:pathv) { + if (path_child != outer) { + out.push_back(path_child); + } + } + return out; +} */ + +Geom::PathVector +LPEOffset::doEffect_path(Geom::PathVector const & path_in) +{ + SPItem * item = SP_ITEM(current_shape); + if (!item) { + return path_in; + } + SPCSSAttr *css; + const gchar *val; + css = sp_repr_css_attr (item->getRepr() , "style"); + val = sp_repr_css_property (css, "fill-rule", nullptr); + FillRuleFlatten fillrule = fill_nonZero; + if (val && strcmp (val, "evenodd") == 0) + { + fillrule = fill_oddEven; + } + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + filled_rule_pathv = original_pathv; + sp_flatten(filled_rule_pathv, fillrule); + if (offset == 0.0) { + return path_in; + } + Geom::PathVector ret; + Geom::PathVector open_ret; + Geom::PathVector ret_outline; + for (const auto & path_it : filled_rule_pathv) { + Geom::Path original = path_it; + if (original.empty()) { + continue; + } + int wdg = offset_winding(filled_rule_pathv, original); + bool path_inside = wdg % 2 != 0; + double gap_size = -0.01; + bool closed = original.closed(); + double to_offset = + Inkscape::Util::Quantity::convert(std::abs(offset), unit.get_abbreviation(), "px") / this->scale; + if (to_offset <= 0.01) { + return path_in; + } + Geom::OptRect original_bounds = original.boundsFast(); + double original_height = 0; + double original_width = 0; + if (original_bounds) { + original_height = (*original_bounds).height(); + original_width = (*original_bounds).width(); + } + if (path_inside && (offset * 2 > (original_height + original_width) / 2.0)) { + continue; + } + Geom::Path with_dir = half_outline(original, + to_offset, + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), + static_cast(BUTT_FLAT)); + Geom::Path against_dir = half_outline(original.reversed(), + to_offset, + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), + static_cast(BUTT_FLAT)); + Geom::Path with_dir_gap = + half_outline(original, std::abs(to_offset + gap_size), + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); + Geom::Path against_dir_gap = + half_outline(original.reversed(), std::abs(to_offset + gap_size), + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), static_cast(BUTT_FLAT)); + bool reversed = false; + Geom::OptRect against_dir_bounds = against_dir.boundsFast(); + Geom::OptRect with_dir_bounds = with_dir.boundsFast(); + double with_dir_height = 0; + double against_dir_height = 0; + double with_dir_width = 0; + double against_dir_width = 0; + if (with_dir_bounds) { + with_dir_height = (*with_dir_bounds).height(); + with_dir_width = (*with_dir_bounds).width(); + } + if (against_dir_bounds) { + against_dir_height = (*against_dir_bounds).height(); + against_dir_width = (*against_dir_bounds).width(); + } + reversed = against_dir_bounds.contains(with_dir_bounds) == false; + // We can have a strange result for the bounding box container + // Gives a wrong result, in theory, it happens sometimes on expand offset + if (offset > 0 && + ((original_width < against_dir_width && + original_width < with_dir_width) || + (original_height < against_dir_height && + original_height < with_dir_height))) + + { + Geom::Path with_dir_size = half_outline(original, + 2, + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), + static_cast(BUTT_FLAT)); + Geom::Path against_dir_size = half_outline(original.reversed(), + 2, + (attempt_force_join ? std::numeric_limits::max() : miter_limit), + static_cast(linejoin_type.get_value()), + static_cast(BUTT_FLAT)); + + Geom::OptRect against_dir_size_bounds = against_dir_size.boundsFast(); + Geom::OptRect with_dir_size_bounds = with_dir_size.boundsFast(); + reversed = against_dir_size_bounds.contains(with_dir_size_bounds) == false; + } + Geom::PathVector tmp; + Geom::PathVector outline; + Geom::Path big; + Geom::Path gap; + Geom::Path small; + if (offset < 0) { + outline.push_back(with_dir); + outline.push_back(against_dir); + sp_flatten(outline, fill_nonZero); + } + if (reversed || !closed) { + big = with_dir; + gap = with_dir_gap; + small = against_dir; + } else { + big = against_dir; + gap = against_dir_gap; + small = with_dir; + } + //big = sp_get_outer(big); + //gap = sp_get_outer(gap); + + if (!closed) { + tmp.push_back(small); + double smalldist = sp_get_distance_point(tmp, offset_pt); + tmp.clear(); + tmp.push_back(big); + double bigdist = sp_get_distance_point(tmp, offset_pt); + tmp.clear(); + if (bigdist > smalldist) { + open_ret.push_back(small); + } else { + open_ret.push_back(big); + } + continue; + } + bool fix_reverse = (original_width + original_height) / 2.0 > to_offset * 2; + if (offset < 0) { + tmp.push_back(gap); + } else { + if (path_inside) { + if (fix_reverse) { + tmp.push_back(small); + } + } else { + tmp.push_back(big); + } + } + ret.insert(ret.end(), tmp.begin(), tmp.end()); + if (offset < 0) { + ret_outline.insert(ret_outline.end(), outline.begin(), outline.end()); + } + } + + if (offset < 0) { + sp_flatten(ret_outline, fill_nonZero); + if (!ret_outline.empty() && !ret.empty()) { + ret = sp_pathvector_boolop(ret_outline, ret, bool_op_diff, fill_nonZero, fill_oddEven); + } + } + + sp_flatten(ret, fill_nonZero); + ret.insert(ret.end(), open_ret.begin(), open_ret.end()); + + if (offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { + offset_pt = get_default_point(ret); + } + return ret; +} + +void LPEOffset::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new OfS::KnotHolderEntityOffsetPoint(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Offset point"), SP_KNOT_SHAPE_CIRCLE); + knotholder->add(_knot_entity); +} + +namespace OfS { +void KnotHolderEntityOffsetPoint::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + using namespace Geom; + SPGroup * group = dynamic_cast(item); + LPEOffset* lpe = dynamic_cast(_effect); + Geom::Point s = snap_knot_position(p, state); + if (group) { + s[Geom::X] = lpe->boundingbox_X.min(); + } + double offset = lpe->sp_get_offset(s); + lpe->offset_pt = s; + lpe->offset.param_set_value(offset); + if (lpe->update_on_knot_move) { + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, false); + } +} + +Geom::Point KnotHolderEntityOffsetPoint::knot_get() const +{ + SPGroup * group = dynamic_cast(item); + LPEOffset * lpe = dynamic_cast (_effect); + if (!lpe->update_on_knot_move) { + return lpe->offset_pt; + } + Geom::Point nearest = lpe->offset_pt; + + if (lpe->offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { + if (group) { + nearest = Geom::Point(lpe->boundingbox_X.min(), lpe->boundingbox_Y.min()); + } else { + Geom::PathVector out = SP_SHAPE(item)->getCurve(true)->get_pathvector(); + nearest = lpe->get_default_point(out); + boost::optional pathvectortime = out.nearestTime(nearest); + if (pathvectortime) { + Geom::PathTime pathtime = pathvectortime->asPathTime(); + nearest = out[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); + } + } + } + + return nearest; +} + +} // namespace OfS +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-offset.h b/src/live_effects/lpe-offset.h new file mode 100644 index 0000000..83d9f2e --- /dev/null +++ b/src/live_effects/lpe-offset.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_OFFSET_H +#define INKSCAPE_LPE_OFFSET_H + +/** \file + * LPE implementation, see lpe-offset.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/unit.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace OfS { +// we need a separate namespace to avoid clashes with other LPEs +class KnotHolderEntityOffsetPoint; +} + +class LPEOffset : public Effect, GroupBBoxEffect { +public: + LPEOffset(LivePathEffectObject *lpeobject); + ~LPEOffset() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + void calculateOffset (Geom::PathVector const & path_in); + Geom::Point get_default_point(Geom::PathVector pathv) const; + Geom::Point get_nearest_point(Geom::PathVector pathv, Geom::Point point) const; + double sp_get_offset(Geom::Point origin); + friend class OfS::KnotHolderEntityOffsetPoint; + +private: + UnitParam unit; + ScalarParam offset; + EnumParam linejoin_type; + ScalarParam miter_limit; + BoolParam attempt_force_join; + BoolParam update_on_knot_move; + Geom::Point offset_pt; + Glib::ustring prev_unit; + double scale = 1; //take document scale and additional parent transformations into account + KnotHolderEntity * _knot_entity; + Geom::PathVector filled_rule_pathv; + Geom::PathVector helper_path; + Inkscape::UI::Widget::Scalar *offset_widget; + + LPEOffset(const LPEOffset&); + LPEOffset& operator=(const LPEOffset&); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-parallel.cpp b/src/live_effects/lpe-parallel.cpp new file mode 100644 index 0000000..b63d65d --- /dev/null +++ b/src/live_effects/lpe-parallel.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007-2012 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-parallel.h" +#include "object/sp-shape.h" +#include "display/curve.h" + +#include "knotholder.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +namespace Pl { + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPEParallel *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityRightEnd(LPEParallel *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace Pl + +LPEParallel::LPEParallel(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + offset_pt(_("Offset"), _("Adjust the offset"), "offset_pt", &wr, this), + length_left(_("Length left:"), _("Specifies the left end of the parallel"), "length-left", &wr, this, 150), + length_right(_("Length right:"), _("Specifies the right end of the parallel"), "length-right", &wr, this, 150) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter(&offset_pt); + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPEParallel::~LPEParallel() += default; + +void +LPEParallel::doOnApply (SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE parallel can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + SPCurve const *curve = SP_SHAPE(lpeitem)->_curve; + + A = *(curve->first_point()); + B = *(curve->last_point()); + dir = unit_vector(B - A); + Geom::Point offset = (A + B)/2 + dir.ccw() * 100; + offset_pt.param_update_default(offset); + offset_pt.param_setValue(offset, true); +} + +Geom::Piecewise > +LPEParallel::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + Piecewise > output; + + A = pwd2_in.firstValue(); + B = pwd2_in.lastValue(); + dir = unit_vector(B - A); + + C = offset_pt - dir * length_left; + D = offset_pt + dir * length_right; + + output = Piecewise >(D2(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output + dir; +} + +void LPEParallel::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new Pl::KnotHolderEntityLeftEnd(this); +e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"left\" end of the parallel")); +knotholder->add(e); + } + { + KnotHolderEntity *e = new Pl::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"right\" end of the parallel")); + knotholder->add(e); + } +}; + +namespace Pl { + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPEParallel *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = L2(s - lpe->offset_pt) * sgn(dot(s - lpe->offset_pt, lpe->dir)); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPEParallel *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = L2(s - lpe->offset_pt) * sgn(dot(s - lpe->offset_pt, lpe->dir)); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPEParallel const *lpe = dynamic_cast(_effect); + return lpe->C; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPEParallel const *lpe = dynamic_cast(_effect); + return lpe->D; +} + +} // namespace Pl + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-parallel.h b/src/live_effects/lpe-parallel.h new file mode 100644 index 0000000..0a2f65e --- /dev/null +++ b/src/live_effects/lpe-parallel.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PARALLEL_H +#define INKSCAPE_LPE_PARALLEL_H + +/** \file + * LPE implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace Pl { + // we need a separate namespace to avoid clashes with LPEPerpBisector + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; +} + +class LPEParallel : public Effect { +public: + LPEParallel(LivePathEffectObject *lpeobject); + ~LPEParallel() override; + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + /* the knotholder entity classes must be declared friends */ + friend class Pl::KnotHolderEntityLeftEnd; + friend class Pl::KnotHolderEntityRightEnd; + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +private: + PointParam offset_pt; + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point A; + Geom::Point B; + Geom::Point C; + Geom::Point D; + Geom::Point M; + Geom::Point N; + Geom::Point dir; + + LPEParallel(const LPEParallel&) = delete; + LPEParallel& operator=(const LPEParallel&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-path_length.cpp b/src/live_effects/lpe-path_length.cpp new file mode 100644 index 0000000..3a4ca88 --- /dev/null +++ b/src/live_effects/lpe-path_length.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-path_length.h" +#include "util/units.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEPathLength::LPEPathLength(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + scale(_("Scale:"), _("Scaling factor"), "scale", &wr, this, 1.0), + info_text(this), + unit(_("Unit:"), _("Unit"), "unit", &wr, this), + display_unit(_("Display unit"), _("Print unit after path length"), "display_unit", &wr, this, true) +{ + registerParameter(&scale); + registerParameter(&info_text); + registerParameter(&unit); + registerParameter(&display_unit); +} + +LPEPathLength::~LPEPathLength() += default; + +Geom::Piecewise > +LPEPathLength::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + /* convert the measured length to the correct unit ... */ + double lengthval = Geom::length(pwd2_in) * scale; + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit.get_abbreviation()); + + /* ... set it as the canvas text ... */ + gchar *arc_length = g_strdup_printf("%.2f %s", lengthval, + display_unit ? unit.get_abbreviation() : ""); + info_text.param_setValue(arc_length); + g_free(arc_length); + + info_text.setPosAndAnchor(pwd2_in, 0.5, 10); + + // TODO: how can we compute the area (such that cw turns don't count negative)? + // should we display the area here, too, or write a new LPE for this? + Piecewise > A = integral(pwd2_in); + Point c; + double area; + if (centroid(pwd2_in, c, area)) { + //g_print ("Area is zero\n"); + } + //g_print ("Area: %f\n", area); + if (!this->isVisible()) { + info_text.param_setValue(""); + } + return pwd2_in; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-path_length.h b/src/live_effects/lpe-path_length.h new file mode 100644 index 0000000..115bf5c --- /dev/null +++ b/src/live_effects/lpe-path_length.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PATH_LENGTH_H +#define INKSCAPE_LPE_PATH_LENGTH_H + +/** \file + * LPE implementation. + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/unit.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPathLength : public Effect { +public: + LPEPathLength(LivePathEffectObject *lpeobject); + ~LPEPathLength() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + LPEPathLength(const LPEPathLength&) = delete; + LPEPathLength& operator=(const LPEPathLength&) = delete; + ScalarParam scale; + TextParamInternal info_text; + UnitParam unit; + BoolParam display_unit; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-patternalongpath.cpp b/src/live_effects/lpe-patternalongpath.cpp new file mode 100644 index 0000000..5fd2f37 --- /dev/null +++ b/src/live_effects/lpe-patternalongpath.cpp @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include <2geom/bezier-to-sbasis.h> + +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpeobject.h" +#include "display/curve.h" + +#include "object/sp-shape.h" + +#include "knotholder.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +/* Theory in e-mail from J.F. Barraud +Let B be the skeleton path, and P the pattern (the path to be deformed). + +P is a map t --> P(t) = ( x(t), y(t) ). +B is a map t --> B(t) = ( a(t), b(t) ). + +The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to +U(s) is s itself. + +We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s). + +The basic deformation associated to B is then given by: + + (x,y) --> U(x)+y*N(x) + +(i.e. we go for distance x along the path, and then for distance y along the normal) + +Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed +first) but I think we can first forget about them. +*/ + +namespace Inkscape { +namespace LivePathEffect { + +namespace WPAP { + class KnotHolderEntityWidthPatternAlongPath : public LPEKnotHolderEntity { + public: + KnotHolderEntityWidthPatternAlongPath(LPEPatternAlongPath * effect) : LPEKnotHolderEntity(effect) {} + ~KnotHolderEntityWidthPatternAlongPath() override + { + LPEPatternAlongPath *lpe = dynamic_cast (_effect); + lpe->_knot_entity = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; +} // WPAP + +static const Util::EnumData PAPCopyTypeData[PAPCT_END] = { + {PAPCT_SINGLE, N_("Single"), "single"}, + {PAPCT_SINGLE_STRETCHED, N_("Single, stretched"), "single_stretched"}, + {PAPCT_REPEATED, N_("Repeated"), "repeated"}, + {PAPCT_REPEATED_STRETCHED, N_("Repeated, stretched"), "repeated_stretched"} +}; +static const Util::EnumDataConverter PAPCopyTypeConverter(PAPCopyTypeData, PAPCT_END); + +LPEPatternAlongPath::LPEPatternAlongPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + pattern(_("Pattern source:"), _("Path to put along the skeleton path"), "pattern", &wr, this, "M0,0 L1,0"), + original_height(0.0), + prop_scale(_("_Width:"), _("Width of the pattern"), "prop_scale", &wr, this, 1.0), + copytype(_("Pattern copies:"), _("How many pattern copies to place along the skeleton path"), + "copytype", PAPCopyTypeConverter, &wr, this, PAPCT_SINGLE_STRETCHED), + scale_y_rel(_("Wid_th in units of length"), + _("Scale the width of the pattern in units of its length"), + "scale_y_rel", &wr, this, false), + spacing(_("Spa_cing:"), + // xgettext:no-c-format + _("Space between copies of the pattern. Negative values allowed, but are limited to -90% of pattern width."), + "spacing", &wr, this, 0), + normal_offset(_("No_rmal offset:"), "", "normal_offset", &wr, this, 0), + tang_offset(_("Tan_gential offset:"), "", "tang_offset", &wr, this, 0), + prop_units(_("Offsets in _unit of pattern size"), + _("Spacing, tangential and normal offset are expressed as a ratio of width/height"), + "prop_units", &wr, this, false), + vertical_pattern(_("Pattern is _vertical"), _("Rotate pattern 90 deg before applying"), + "vertical_pattern", &wr, this, false), + hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false), + fuse_tolerance(_("_Fuse nearby ends:"), _("Fuse ends closer than this number. 0 means don't fuse."), + "fuse_tolerance", &wr, this, 0) +{ + registerParameter(&pattern); + registerParameter(©type); + registerParameter(&prop_scale); + registerParameter(&scale_y_rel); + registerParameter(&spacing); + registerParameter(&normal_offset); + registerParameter(&tang_offset); + registerParameter(&prop_units); + registerParameter(&vertical_pattern); + registerParameter(&hide_knot); + registerParameter(&fuse_tolerance); + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + _knot_entity = nullptr; + _provides_knotholder_entities = true; + +} + +LPEPatternAlongPath::~LPEPatternAlongPath() += default; + +void LPEPatternAlongPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + pattern.param_transform_multiply(postmul, false); +} + +void +LPEPatternAlongPath::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the pattern bounding box + Geom::OptRect bbox = pattern.get_pathvector().boundsFast(); + if (bbox) { + original_height = (*bbox)[Geom::Y].max() - (*bbox)[Geom::Y].min(); + } + if (_knot_entity) { + if (hide_knot) { + helper_path.clear(); + _knot_entity->knot->hide(); + } else { + _knot_entity->knot->show(); + } + _knot_entity->update_knot(); + } +} + +Geom::Piecewise > +LPEPatternAlongPath::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + // Don't allow empty path parameter: + if ( pattern.get_pathvector().empty() ) { + return pwd2_in; + } + +/* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + Piecewise > output; + std::vector > > pre_output; + + PAPCopyType type = copytype.get_value(); + + D2 > patternd2 = make_cuts_independent(pattern.get_pwd2()); + Piecewise x0 = vertical_pattern.get_value() ? Piecewise(patternd2[1]) : Piecewise(patternd2[0]); + Piecewise y0 = vertical_pattern.get_value() ? Piecewise(patternd2[0]) : Piecewise(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + if (pattBndsX && pattBndsY) { + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double xspace = spacing; + double noffset = normal_offset; + double toffset = tang_offset; + if (prop_units.get_value()){ + xspace *= pattBndsX->extent(); + noffset *= pattBndsY->extent(); + toffset *= pattBndsX->extent(); + } + + //Prevent more than 90% overlap... + if (xspace < -pattBndsX->extent() * 0.9) { + xspace = -pattBndsX->extent() * 0.9; + } + //TODO: dynamical update of parameter ranges? + //if (prop_units.get_value()){ + // spacing.param_set_range(-.9, Geom::infinity()); + // }else{ + // spacing.param_set_range(-pattBndsX.extent()*.9, Geom::infinity()); + // } + + y0 += noffset; + + std::vector > > paths_in; + paths_in = split_at_discontinuities(pwd2_in); + + for (auto path_i : paths_in){ + Piecewise x = x0; + Piecewise y = y0; + Piecewise > uskeleton = arc_length_parametrization(path_i,2, 0.1); + uskeleton = remove_short_cuts(uskeleton, 0.01); + Piecewise > n = rot90(derivative(uskeleton)); + if (Geom::are_near(pwd2_in[0].at0(),pwd2_in[pwd2_in.size()-1].at1(), 0.01)) { + n = force_continuity(remove_short_cuts(n, 0.1), 0.01); + } else { + n = force_continuity(remove_short_cuts(n, 0.1)); + } + int nbCopies = 0; + double scaling = 1; + switch(type) { + case PAPCT_REPEATED: + nbCopies = static_cast(floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + break; + + case PAPCT_SINGLE: + nbCopies = (toffset + pattBndsX->extent() < uskeleton.domain().extent()) ? 1 : 0; + break; + + case PAPCT_SINGLE_STRETCHED: + nbCopies = 1; + scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + break; + + case PAPCT_REPEATED_STRETCHED: + // if uskeleton is closed: + if (are_near(path_i.segs.front().at0(), path_i.segs.back().at1())){ + nbCopies = std::max(1, static_cast(std::floor((uskeleton.domain().extent() - toffset)/(pattBndsX->extent()+xspace)))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent()); + // if not closed: no space at the end + }else{ + nbCopies = std::max(1, static_cast(std::floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace)))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent() - xspace); + } + break; + + default: + return pwd2_in; + }; + + //Ceil to 6 decimals + scaling = ceil(scaling * 1000000) / 1000000; + double pattWidth = pattBndsX->extent() * scaling; + + x *= scaling; + if ( scale_y_rel.get_value() ) { + y *= prop_scale * scaling; + } else { + y *= prop_scale; + } + x += toffset; + + double offs = 0; + for (int i=0; i 0){ + Geom::Piecewise > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); + std::vector > > splited_output_piece = split_at_discontinuities(output_piece); + pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() ); + }else{ + output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs)); + } + offs+=pattWidth; + } + } + if (fuse_tolerance > 0){ + pre_output = fuse_nearby_ends(pre_output, fuse_tolerance); + for (const auto & i : pre_output){ + output.concat(i); + } + } + return output; + } else { + return pwd2_in; + } +} + +void +LPEPatternAlongPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(helper_path); +} + + +void +LPEPatternAlongPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new WPAP::KnotHolderEntityWidthPatternAlongPath(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Change the width"), + SP_KNOT_SHAPE_CIRCLE); + knotholder->add(_knot_entity); + if (hide_knot) { + _knot_entity->knot->hide(); + _knot_entity->update_knot(); + } +} + +namespace WPAP { + +void +KnotHolderEntityWidthPatternAlongPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + LPEPatternAlongPath *lpe = dynamic_cast (_effect); + + Geom::Point const s = snap_knot_position(p, state); + SPShape const *sp_shape = dynamic_cast(SP_LPE_ITEM(item)); + if (sp_shape) { + SPCurve *curve_before = sp_shape->getCurveForEdit(); + if (curve_before) { + Geom::Path const *path_in = curve_before->first_path(); + Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse(); + Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos); + if(nearest_to_ray == 0){ + lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } else { + lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/pap/width", lpe->prop_scale); + curve_before->unref(); + } + } + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityWidthPatternAlongPath::knot_get() const +{ + LPEPatternAlongPath *lpe = dynamic_cast (_effect); + SPShape const *sp_shape = dynamic_cast(SP_LPE_ITEM(item)); + if (sp_shape) { + SPCurve *curve_before = sp_shape->getCurveForEdit(); + if (curve_before) { + Geom::Path const *path_in = curve_before->first_path(); + Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA; + lpe->helper_path.clear(); + if (!lpe->hide_knot) { + Geom::Path hp(result_point); + hp.appendNew(ptA); + lpe->helper_path.push_back(hp); + hp.clear(); + } + curve_before->unref(); + return result_point; + } + } + return Geom::Point(); +} +} // namespace WPAP +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-patternalongpath.h b/src/live_effects/lpe-patternalongpath.h new file mode 100644 index 0000000..690aaa9 --- /dev/null +++ b/src/live_effects/lpe-patternalongpath.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PATTERN_ALONG_PATH_H +#define INKSCAPE_LPE_PATTERN_ALONG_PATH_H + +/* + * Inkscape::LPEPatternAlongPath + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace WPAP { +class KnotHolderEntityWidthPatternAlongPath; +} + +enum PAPCopyType { + PAPCT_SINGLE = 0, + PAPCT_SINGLE_STRETCHED, + PAPCT_REPEATED, + PAPCT_REPEATED_STRETCHED, + PAPCT_END // This must be last +}; + +class LPEPatternAlongPath : public Effect { +public: + LPEPatternAlongPath(LivePathEffectObject *lpeobject); + ~LPEPatternAlongPath() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; + + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + PathParam pattern; + + friend class WPAP::KnotHolderEntityWidthPatternAlongPath; +protected: + double original_height; + ScalarParam prop_scale; + +private: + EnumParam copytype; + BoolParam scale_y_rel; + ScalarParam spacing; + ScalarParam normal_offset; + ScalarParam tang_offset; + BoolParam prop_units; + BoolParam vertical_pattern; + BoolParam hide_knot; + ScalarParam fuse_tolerance; + KnotHolderEntity * _knot_entity; + Geom::PathVector helper_path; + void on_pattern_pasted(); + + LPEPatternAlongPath(const LPEPatternAlongPath&); + LPEPatternAlongPath& operator=(const LPEPatternAlongPath&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-perp_bisector.cpp b/src/live_effects/lpe-perp_bisector.cpp new file mode 100644 index 0000000..b77b882 --- /dev/null +++ b/src/live_effects/lpe-perp_bisector.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilin Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-perp_bisector.h" +#include "display/curve.h" +#include "line-geometry.h" + +#include "object/sp-path.h" + +#include "knotholder.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { +namespace PB { + +class KnotHolderEntityEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityEnd(LPEPerpBisector *effect) : LPEKnotHolderEntity(effect) {}; + void bisector_end_set(Geom::Point const &p, guint state, bool left = true); +}; + +class KnotHolderEntityLeftEnd : public KnotHolderEntityEnd { +public: + KnotHolderEntityLeftEnd(LPEPerpBisector *effect) : KnotHolderEntityEnd(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public KnotHolderEntityEnd { +public: + KnotHolderEntityRightEnd(LPEPerpBisector *effect) : KnotHolderEntityEnd(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const { + LPEPerpBisector const* lpe = dynamic_cast(_effect); + return Geom::Point(lpe->C); +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const { + LPEPerpBisector const* lpe = dynamic_cast(_effect); + return Geom::Point(lpe->D); +} + +void +KnotHolderEntityEnd::bisector_end_set(Geom::Point const &p, guint state, bool left) { + LPEPerpBisector *lpe = dynamic_cast(_effect); + if (!lpe) return; + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->M, lpe->perp_dir); + if (left) { + lpe->C = lpe->M + lpe->perp_dir * lambda; + lpe->length_left.param_set_value(lambda); + } else { + lpe->D = lpe->M + lpe->perp_dir * lambda; + lpe->length_right.param_set_value(-lambda); + } + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), true, true); +} + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) { + bisector_end_set(p, state); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) { + bisector_end_set(p, state, false); +} + +} //namescape PB + +LPEPerpBisector::LPEPerpBisector(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + length_left(_("Length left:"), _("Specifies the left end of the bisector"), "length-left", &wr, this, 200), + length_right(_("Length right:"), _("Specifies the right end of the bisector"), "length-right", &wr, this, 200), + A(0,0), B(0,0), M(0,0), C(0,0), D(0,0), perp_dir(0,0) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPEPerpBisector::~LPEPerpBisector() += default; + +void +LPEPerpBisector::doOnApply (SPLPEItem const*/*lpeitem*/) +{ + /* make the path a straight line */ + /** + SPCurve* curve = sp_path_getCurveForEdit (SP_PATH(lpeitem)); // TODO: Should we use sp_shape_get_curve()? + + Geom::Point A(curve->first_point()); + Geom::Point B(curve->last_point()); + + SPCurve *c = new SPCurve(); + c->moveto(A); + c->lineto(B); + // TODO: Why doesn't sp_path_set_curve_before_LPE(SP_PATH(lpeitem), c, TRUE, true) work? + SP_PATH(lpeitem)->original_curve = c->ref(); + c->unref(); + **/ +} + + +Geom::Piecewise > +LPEPerpBisector::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + Piecewise > output; + + A = pwd2_in.firstValue(); + B = pwd2_in.lastValue(); + M = (A + B)/2; + + perp_dir = unit_vector((B - A).ccw()); + + C = M + perp_dir * length_left; + D = M - perp_dir * length_right; + + output = Piecewise >(D2(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output; +} + +void +LPEPerpBisector::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new PB::KnotHolderEntityLeftEnd(this); +e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"left\" end of the bisector")); +knotholder->add(e); + } + { + KnotHolderEntity *e = new PB::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the \"right\" end of the bisector")); + knotholder->add(e); + } +}; + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-perp_bisector.h b/src/live_effects/lpe-perp_bisector.h new file mode 100644 index 0000000..4d09f88 --- /dev/null +++ b/src/live_effects/lpe-perp_bisector.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PERP_BISECTOR_H +#define INKSCAPE_LPE_PERP_BISECTOR_H + +/** \file + * LPE implementation, see lpe-perp_bisector.cpp. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilin Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace PB { + // we need a separate namespace to avoid clashes with LPETangentToCurve + class KnotHolderEntityEnd; + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; + void bisector_end_set(SPItem *item, Geom::Point const &p, guint state, bool left = true); +} + +class LPEPerpBisector : public Effect { +public: + LPEPerpBisector(LivePathEffectObject *lpeobject); + ~LPEPerpBisector() override; + + virtual EffectType effectType () { return PERP_BISECTOR; } + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > + doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + /* the knotholder entity functions must be declared friends */ + friend class PB::KnotHolderEntityEnd; + friend class PB::KnotHolderEntityLeftEnd; + friend class PB::KnotHolderEntityRightEnd; + friend void PB::bisector_end_set(SPItem *item, Geom::Point const &p, guint state, bool left); + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +private: + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point A; // start of path + Geom::Point B; // end of path + Geom::Point M; // midpoint + Geom::Point C; // left end of bisector + Geom::Point D; // right end of bisector + Geom::Point perp_dir; + + LPEPerpBisector(const LPEPerpBisector&) = delete; + LPEPerpBisector& operator=(const LPEPerpBisector&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-perspective-envelope.cpp b/src/live_effects/lpe-perspective-envelope.cpp new file mode 100644 index 0000000..0d80b68 --- /dev/null +++ b/src/live_effects/lpe-perspective-envelope.cpp @@ -0,0 +1,576 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + + */ +/* + * Authors: + * Jabiertxof Code migration from python extensions envelope and perspective + * Aaron Spike, aaron@ekips.org from envelope and perspective python code + * Dmitry Platonov, shadowjack@mail.ru, 2006 perspective approach & math + * Jose Hevia (freon) Transform algorithm from envelope + * + * Copyright (C) 2007-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-perspective-envelope.h" +#include "helper/geom.h" +#include "display/curve.h" +#include + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +enum DeformationType { + DEFORMATION_PERSPECTIVE, + DEFORMATION_ENVELOPE +}; + +static const Util::EnumData DeformationTypeData[] = { + {DEFORMATION_PERSPECTIVE , N_("Perspective"), "perspective"}, + {DEFORMATION_ENVELOPE , N_("Envelope deformation"), "envelope_deformation"} +}; + +static const Util::EnumDataConverter DeformationTypeConverter(DeformationTypeData, sizeof(DeformationTypeData)/sizeof(*DeformationTypeData)); + +LPEPerspectiveEnvelope::LPEPerspectiveEnvelope(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + horizontal_mirror(_("Mirror movements in horizontal"), _("Mirror movements in horizontal"), "horizontal_mirror", &wr, this, false), + vertical_mirror(_("Mirror movements in vertical"), _("Mirror movements in vertical"), "vertical_mirror", &wr, this, false), + overflow_perspective(_("Overflow perspective"), _("Overflow perspective"), "overflow_perspective", &wr, this, false), + deform_type(_("Type"), _("Select the type of deformation"), "deform_type", DeformationTypeConverter, &wr, this, DEFORMATION_PERSPECTIVE), + up_left_point(_("Top Left"), _("Top Left - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "up_left_point", &wr, this), + up_right_point(_("Top Right"), _("Top Right - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "up_right_point", &wr, this), + down_left_point(_("Down Left"), _("Down Left - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "down_left_point", &wr, this), + down_right_point(_("Down Right"), _("Down Right - Ctrl+Alt+Click: reset, Ctrl: move along axes"), "down_right_point", &wr, this) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&deform_type); + registerParameter(&horizontal_mirror); + registerParameter(&vertical_mirror); + registerParameter(&overflow_perspective); + registerParameter(&up_left_point); + registerParameter(&up_right_point); + registerParameter(&down_left_point); + registerParameter(&down_right_point); + apply_to_clippath_and_mask = true; +} + +LPEPerspectiveEnvelope::~LPEPerspectiveEnvelope() += default; + +void LPEPerspectiveEnvelope::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + up_left_point.param_transform_multiply(postmul, false); + up_right_point.param_transform_multiply(postmul, false); + down_left_point.param_transform_multiply(postmul, false); + down_right_point.param_transform_multiply(postmul, false); +} + +bool pointInTriangle(Geom::Point const &p, std::vector points) +{ + if (points.size() != 3) { + g_warning("Incorrect number of points in pointInTriangle\n"); + return false; + } + Geom::Point p1 = points[0]; + Geom::Point p2 = points[1]; + Geom::Point p3 = points[2]; + // http://totologic.blogspot.com.es/2014/01/accurate-point-in-triangle-test.html + using Geom::X; + using Geom::Y; + double denominator = (p1[X] * (p2[Y] - p3[Y]) + p1[Y] * (p3[X] - p2[X]) + p2[X] * p3[Y] - p2[Y] * p3[X]); + double t1 = (p[X] * (p3[Y] - p1[Y]) + p[Y] * (p1[X] - p3[X]) - p1[X] * p3[Y] + p1[Y] * p3[X]) / denominator; + double t2 = (p[X] * (p2[Y] - p1[Y]) + p[Y] * (p1[X] - p2[X]) - p1[X] * p2[Y] + p1[Y] * p2[X]) / -denominator; + double s = t1 + t2; + + return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && s <= 1; +} + +void LPEPerspectiveEnvelope::doEffect(SPCurve *curve) +{ + double projmatrix[3][3]; + if(deform_type == DEFORMATION_PERSPECTIVE) { + using Geom::X; + using Geom::Y; + std::vector source_handles(4); + source_handles[0] = Geom::Point(boundingbox_X.min(), boundingbox_Y.max()); + source_handles[1] = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); + source_handles[2] = Geom::Point(boundingbox_X.max(), boundingbox_Y.min()); + source_handles[3] = Geom::Point(boundingbox_X.max(), boundingbox_Y.max()); + double solmatrix[8][8] = {{0}}; + double free_term[8] = {0}; + double gslSolmatrix[64]; + for(unsigned int i = 0; i < 4; ++i) { + solmatrix[i][0] = source_handles[i][X]; + solmatrix[i][1] = source_handles[i][Y]; + solmatrix[i][2] = 1; + solmatrix[i][6] = -handles[i][X] * source_handles[i][X]; + solmatrix[i][7] = -handles[i][X] * source_handles[i][Y]; + solmatrix[i+4][3] = source_handles[i][X]; + solmatrix[i+4][4] = source_handles[i][Y]; + solmatrix[i+4][5] = 1; + solmatrix[i+4][6] = -handles[i][Y] * source_handles[i][X]; + solmatrix[i+4][7] = -handles[i][Y] * source_handles[i][Y]; + free_term[i] = handles[i][X]; + free_term[i+4] = handles[i][Y]; + } + int h = 0; + for(auto & i : solmatrix) { + for(double j : i) { + gslSolmatrix[h] = j; + h++; + } + } + //this is get by this page: + //http://www.gnu.org/software/gsl/manual/html_node/Linear-Algebra-Examples.html#Linear-Algebra-Examples + gsl_matrix_view m = gsl_matrix_view_array (gslSolmatrix, 8, 8); + gsl_vector_view b = gsl_vector_view_array (free_term, 8); + gsl_vector *x = gsl_vector_alloc (8); + int s; + gsl_permutation * p = gsl_permutation_alloc (8); + gsl_linalg_LU_decomp (&m.matrix, p, &s); + gsl_linalg_LU_solve (&m.matrix, p, &b.vector, x); + h = 0; + for(auto & i : projmatrix) { + for(double & j : i) { + if(h==8) { + projmatrix[2][2] = 1.0; + continue; + } + j = gsl_vector_get(x, h); + h++; + } + } + gsl_permutation_free (p); + gsl_vector_free (x); + } + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + curve->reset(); + Geom::CubicBezier const *cubic = nullptr; + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point point_at3(0, 0); + for (const auto & path_it : original_pathv) { + //Si está vacío... + if (path_it.empty()) + continue; + //Itreadores + SPCurve *nCurve = new SPCurve(); + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + curve_endit = path_it.end_open(); + } + } + if(deform_type == DEFORMATION_PERSPECTIVE) { + nCurve->moveto(projectPoint(curve_it1->initialPoint(), projmatrix)); + } else { + nCurve->moveto(projectPoint(curve_it1->initialPoint())); + } + while (curve_it1 != curve_endit) { + cubic = dynamic_cast(&*curve_it1); + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + } else { + point_at1 = curve_it1->initialPoint(); + point_at2 = curve_it1->finalPoint(); + } + point_at3 = curve_it1->finalPoint(); + if(deform_type == DEFORMATION_PERSPECTIVE) { + point_at1 = projectPoint(point_at1, projmatrix); + point_at2 = projectPoint(point_at2, projmatrix); + point_at3 = projectPoint(point_at3, projmatrix); + } else { + point_at1 = projectPoint(point_at1); + point_at2 = projectPoint(point_at2); + point_at3 = projectPoint(point_at3); + } + if (cubic) { + nCurve->curveto(point_at1, point_at2, point_at3); + } else { + nCurve->lineto(point_at3); + } + ++curve_it1; + } + //y cerramos la curva + if (path_it.closed()) { + nCurve->move_endpoints(point_at3, point_at3); + nCurve->closepath_current(); + } + curve->append(nCurve, false); + nCurve->reset(); + delete nCurve; + } +} + +Geom::Point +LPEPerspectiveEnvelope::projectPoint(Geom::Point p) +{ + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + double delta_x = boundingbox_X.min() - p[X]; + double delta_y = boundingbox_Y.max() - p[Y]; + Geom::Coord x_ratio = (delta_x * -1) / width; + Geom::Coord y_ratio = delta_y / height; + Geom::Line horiz; + Geom::Line vert; + vert.setPoints (pointAtRatio(y_ratio,down_left_point,up_left_point),pointAtRatio(y_ratio,down_right_point,up_right_point)); + horiz.setPoints (pointAtRatio(x_ratio,down_left_point,down_right_point),pointAtRatio(x_ratio,up_left_point,up_right_point)); + + OptCrossing crossPoint = intersection(horiz,vert); + if(crossPoint) { + return horiz.pointAt(Geom::Coord(crossPoint->ta)); + } else { + return p; + } +} + +Geom::Point +LPEPerspectiveEnvelope::projectPoint(Geom::Point p, double m[][3]) +{ + Geom::Coord x = p[0]; + Geom::Coord y = p[1]; + return Geom::Point( + Geom::Coord((x*m[0][0] + y*m[0][1] + m[0][2])/(x*m[2][0]+y*m[2][1]+m[2][2])), + Geom::Coord((x*m[1][0] + y*m[1][1] + m[1][2])/(x*m[2][0]+y*m[2][1]+m[2][2]))); +} + +Geom::Point +LPEPerspectiveEnvelope::pointAtRatio(Geom::Coord ratio,Geom::Point A, Geom::Point B) +{ + Geom::Coord x = A[X] + (ratio * (B[X]-A[X])); + Geom::Coord y = A[Y]+ (ratio * (B[Y]-A[Y])); + return Point(x, y); +} + + +Gtk::Widget * +LPEPerspectiveEnvelope::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox(Effect::newWidget()) ); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + std::vector::iterator it = param_vector.begin(); + Gtk::HBox * hbox_up_handles = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::HBox * hbox_down_handles = Gtk::manage(new Gtk::HBox(false,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast(param->param_newWidget()); + if (param->param_key == "up_left_point" || + param->param_key == "up_right_point" || + param->param_key == "down_left_point" || + param->param_key == "down_right_point") { + Gtk::HBox * point_hbox = dynamic_cast(widg); + std::vector< Gtk::Widget* > child_list = point_hbox->get_children(); + Gtk::HBox * point_hboxHBox = dynamic_cast(child_list[0]); + std::vector< Gtk::Widget* > child_list2 = point_hboxHBox->get_children(); + point_hboxHBox->remove(child_list2[0][0]); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if(param->param_key == "up_left_point") { + Gtk::Label* handles = Gtk::manage(new Gtk::Label(Glib::ustring(_("Handles:")),Gtk::ALIGN_START)); + vbox->pack_start(*handles, false, false, 2); + hbox_up_handles->pack_start(*widg, true, true, 2); + hbox_up_handles->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), Gtk::PACK_EXPAND_WIDGET); + } else if(param->param_key == "up_right_point") { + hbox_up_handles->pack_start(*widg, true, true, 2); + } else if(param->param_key == "down_left_point") { + hbox_down_handles->pack_start(*widg, true, true, 2); + hbox_down_handles->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), Gtk::PACK_EXPAND_WIDGET); + } else { + hbox_down_handles->pack_start(*widg, true, true, 2); + } + if (tip) { + widg->set_tooltip_markup(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + } + + ++it; + } + vbox->pack_start(*hbox_up_handles,true, true, 2); + Gtk::HBox * hbox_middle = Gtk::manage(new Gtk::HBox(true,2)); + hbox_middle->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_EXPAND_WIDGET); + hbox_middle->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_EXPAND_WIDGET); + vbox->pack_start(*hbox_middle, false, true, 2); + vbox->pack_start(*hbox_down_handles, true, true, 2); + Gtk::HBox * hbox = Gtk::manage(new Gtk::HBox(false,0)); + Gtk::Button* reset_button = Gtk::manage(new Gtk::Button(_("_Clear"), true)); + reset_button->set_image_from_icon_name("edit-clear"); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEPerspectiveEnvelope::resetGrid)); + reset_button->set_size_request(140,30); + vbox->pack_start(*hbox, true,true,2); + hbox->pack_start(*reset_button, false, false,2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPEPerspectiveEnvelope::vertical(PointParam ¶m_one, PointParam ¶m_two, Geom::Line vert) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double Y = (A[Geom::Y] + B[Geom::Y])/2; + A[Geom::Y] = Y; + B[Geom::Y] = Y; + Geom::Point nearest = vert.pointAt(vert.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::X] > B[Geom::X]) { + distance_middle *= -1; + } + A[Geom::X] = nearest[Geom::X] - distance_middle; + B[Geom::X] = nearest[Geom::X] + distance_middle; + param_one.param_setValue(A); + param_two.param_setValue(B); +} + +void +LPEPerspectiveEnvelope::horizontal(PointParam ¶m_one, PointParam ¶m_two, Geom::Line horiz) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double X = (A[Geom::X] + B[Geom::X])/2; + A[Geom::X] = X; + B[Geom::X] = X; + Geom::Point nearest = horiz.pointAt(horiz.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::Y] > B[Geom::Y]) { + distance_middle *= -1; + } + A[Geom::Y] = nearest[Geom::Y] - distance_middle; + B[Geom::Y] = nearest[Geom::Y] + distance_middle; + param_one.param_setValue(A); + param_two.param_setValue(B); +} + +void +LPEPerspectiveEnvelope::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); + Geom::Line vert(Geom::Point(boundingbox_X.middle(),boundingbox_Y.max()), Geom::Point(boundingbox_X.middle(), boundingbox_Y.min())); + Geom::Line horiz(Geom::Point(boundingbox_X.min(),boundingbox_Y.middle()), Geom::Point(boundingbox_X.max(), boundingbox_Y.middle())); + if(vertical_mirror) { + vertical(up_left_point, up_right_point,vert); + vertical(down_left_point, down_right_point,vert); + } + if(horizontal_mirror) { + horizontal(up_left_point, down_left_point,horiz); + horizontal(up_right_point, down_right_point,horiz); + } + setDefaults(); + if (are_near(up_left_point, up_right_point) && are_near(up_right_point, down_left_point) && + are_near(down_left_point, down_right_point)) { + g_warning( + "Perspective/Envelope LPE::doBeforeEffect - lpeobj with invalid parameter, the same value in 4 handles!"); + resetGrid(); + return; + } + if (deform_type == DEFORMATION_PERSPECTIVE) { + if (!overflow_perspective && handles.size() == 4) { + bool move0 = false; + if (handles[0] != down_left_point) { + move0 = true; + } + bool move1 = false; + if (handles[1] != up_left_point) { + move1 = true; + } + bool move2 = false; + if (handles[2] != up_right_point) { + move2 = true; + } + bool move3 = false; + if (handles[3] != down_right_point) { + move3 = true; + } + handles.resize(4); + handles[0] = down_left_point; + handles[1] = up_left_point; + handles[2] = up_right_point; + handles[3] = down_right_point; + Geom::Line line_a(handles[3], handles[1]); + Geom::Line line_b(handles[1], handles[2]); + Geom::Line line_c(handles[2], handles[3]); + int position_a = Geom::sgn(Geom::cross(handles[3] - handles[1], handles[0] - handles[1])); + int position_b = Geom::sgn(Geom::cross(handles[1] - handles[2], handles[0] - handles[2])); + int position_c = Geom::sgn(Geom::cross(handles[2] - handles[3], handles[0] - handles[3])); + if (position_a != 1 && move0) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[0])); + down_left_point.param_setValue(point_a, true); + } + if (position_b == 1 && move0) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[0])); + down_left_point.param_setValue(point_b, true); + } + if (position_c == 1 && move0) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[0])); + down_left_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[0], handles[2]); + line_b.setPoints(handles[2], handles[3]); + line_c.setPoints(handles[3], handles[0]); + position_a = Geom::sgn(Geom::cross(handles[0] - handles[2], handles[1] - handles[2])); + position_b = Geom::sgn(Geom::cross(handles[2] - handles[3], handles[1] - handles[3])); + position_c = Geom::sgn(Geom::cross(handles[3] - handles[0], handles[1] - handles[0])); + if (position_a != 1 && move1) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[1])); + up_left_point.param_setValue(point_a, true); + } + if (position_b == 1 && move1) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[1])); + up_left_point.param_setValue(point_b, true); + } + if (position_c == 1 && move1) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[1])); + up_left_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[1], handles[3]); + line_b.setPoints(handles[3], handles[0]); + line_c.setPoints(handles[0], handles[1]); + position_a = Geom::sgn(Geom::cross(handles[1] - handles[3], handles[2] - handles[3])); + position_b = Geom::sgn(Geom::cross(handles[3] - handles[0], handles[2] - handles[0])); + position_c = Geom::sgn(Geom::cross(handles[0] - handles[1], handles[2] - handles[1])); + if (position_a != 1 && move2) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[2])); + up_right_point.param_setValue(point_a, true); + } + if (position_b == 1 && move2) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[2])); + up_right_point.param_setValue(point_b, true); + } + if (position_c == 1 && move2) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[2])); + up_right_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[2], handles[0]); + line_b.setPoints(handles[0], handles[1]); + line_c.setPoints(handles[1], handles[2]); + position_a = Geom::sgn(Geom::cross(handles[2] - handles[0], handles[3] - handles[0])); + position_b = Geom::sgn(Geom::cross(handles[0] - handles[1], handles[3] - handles[1])); + position_c = Geom::sgn(Geom::cross(handles[1] - handles[2], handles[3] - handles[2])); + if (position_a != 1 && move3) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[3])); + down_right_point.param_setValue(point_a, true); + } + if (position_b == 1 && move3) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[3])); + down_right_point.param_setValue(point_b, true); + } + if (position_c == 1 && move3) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[3])); + down_right_point.param_setValue(point_c, true); + } + } else { + handles.resize(4); + handles[0] = down_left_point; + handles[1] = up_left_point; + handles[2] = up_right_point; + handles[3] = down_right_point; + } + } +} + +void +LPEPerspectiveEnvelope::setDefaults() +{ + Geom::Point up_left(boundingbox_X.min(), boundingbox_Y.min()); + Geom::Point up_right(boundingbox_X.max(), boundingbox_Y.min()); + Geom::Point down_left(boundingbox_X.min(), boundingbox_Y.max()); + Geom::Point down_right(boundingbox_X.max(), boundingbox_Y.max()); + + up_left_point.param_update_default(up_left); + up_right_point.param_update_default(up_right); + down_right_point.param_update_default(down_right); + down_left_point.param_update_default(down_left); +} + +void +LPEPerspectiveEnvelope::resetGrid() +{ + up_left_point.param_set_default(); + up_right_point.param_set_default(); + down_right_point.param_set_default(); + down_left_point.param_set_default(); +} + +void +LPEPerspectiveEnvelope::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + setDefaults(); + resetGrid(); +} + +void +LPEPerspectiveEnvelope::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.clear(); + + SPCurve *c = new SPCurve(); + c->reset(); + c->moveto(up_left_point); + c->lineto(up_right_point); + c->lineto(down_right_point); + c->lineto(down_left_point); + c->lineto(up_left_point); + hp_vec.push_back(c->get_pathvector()); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-perspective-envelope.h b/src/live_effects/lpe-perspective-envelope.h new file mode 100644 index 0000000..7aa32ec --- /dev/null +++ b/src/live_effects/lpe-perspective-envelope.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PERSPECTIVE_ENVELOPE_H +#define INKSCAPE_LPE_PERSPECTIVE_ENVELOPE_H + +/** \file + * LPE implementation , see lpe-perspective-envelope.cpp. + + */ +/* + * Authors: + * Jabiertxof Code migration from python extensions envelope and perspective + * Aaron Spike, aaron@ekips.org from envelope and perspective python code + * Dmitry Platonov, shadowjack@mail.ru, 2006 perspective approach & math + * Jose Hevia (freon) Transform algorithm from envelope + * + * Copyright (C) 2007-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPerspectiveEnvelope : public Effect, GroupBBoxEffect { +public: + + LPEPerspectiveEnvelope(LivePathEffectObject *lpeobject); + + ~LPEPerspectiveEnvelope() override; + + void doEffect(SPCurve *curve) override; + + virtual Geom::Point projectPoint(Geom::Point p); + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + virtual Geom::Point projectPoint(Geom::Point p, double m[][3]); + + virtual Geom::Point pointAtRatio(Geom::Coord ratio,Geom::Point A, Geom::Point B); + + void resetDefaults(SPItem const* item) override; + + virtual void vertical(PointParam ¶mA,PointParam ¶mB, Geom::Line vert); + + virtual void horizontal(PointParam ¶mA,PointParam ¶mB,Geom::Line horiz); + + void doBeforeEffect(SPLPEItem const* lpeitem) override; + + Gtk::Widget * newWidget() override; + + virtual void setDefaults(); + + virtual void resetGrid(); + +protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; +private: + + BoolParam horizontal_mirror; + BoolParam vertical_mirror; + BoolParam overflow_perspective; + EnumParam deform_type; + PointParam up_left_point; + PointParam up_right_point; + PointParam down_left_point; + PointParam down_right_point; + std::vector handles; + LPEPerspectiveEnvelope(const LPEPerspectiveEnvelope&) = delete; + LPEPerspectiveEnvelope& operator=(const LPEPerspectiveEnvelope&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-powerclip.cpp b/src/live_effects/lpe-powerclip.cpp new file mode 100644 index 0000000..ee92678 --- /dev/null +++ b/src/live_effects/lpe-powerclip.cpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-powerclip.h" +#include "display/curve.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-use.h" +#include "style.h" +#include "svg/svg.h" + +#include <2geom/intersection-graph.h> +#include <2geom/path-intersection.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEPowerClip::LPEPowerClip(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , hide_clip(_("Hide clip"), _("Hide clip"), "hide_clip", &wr, this, false) + , inverse(_("Inverse clip"), _("Inverse clip"), "inverse", &wr, this, true) + , flatten(_("Flatten clip"), _("Flatten clip, see fill rule once convert to paths"), "flatten", &wr, this, false) + , message( + _("Info Box"), _("Important messages"), "message", &wr, this, + _("Use fill-rule evenodd on fill and stroke dialog if no flatten result after convert clip to paths.")) +{ + registerParameter(&inverse); + registerParameter(&flatten); + registerParameter(&hide_clip); + registerParameter(&message); + message.param_set_min_height(55); + _updating = false; + _legacy = false; + // legazy fix between 0.92.4 launch and 1.0beta1 + if (this->getRepr()->attribute("is_inverse")) { + this->getRepr()->removeAttribute("is_inverse"); + _legacy = true; + } +} + +LPEPowerClip::~LPEPowerClip() = default; + +Geom::Path sp_bbox_without_clip(SPLPEItem *lpeitem) +{ + Geom::OptRect bbox = lpeitem->visualBounds(Geom::identity(), true, false, true); + if (bbox) { + (*bbox).expandBy(5); + return Geom::Path(*bbox); + } + return Geom::Path(); +} + +Geom::PathVector sp_get_recursive_pathvector(SPLPEItem *item, Geom::PathVector res, bool dir, bool inverse) +{ + SPGroup *group = dynamic_cast(item); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto child : item_list) { + if (child) { + SPLPEItem *childitem = dynamic_cast(child); + if (childitem) { + res = sp_get_recursive_pathvector(childitem, res, dir, inverse); + } + } + } + } + SPShape *shape = dynamic_cast(item); + if (shape && shape->getCurve()) { + for (auto path : shape->getCurve(true)->get_pathvector()) { + if (!path.empty()) { + bool pathdir = Geom::path_direction(path); + if (pathdir == dir && inverse) { + path = path.reversed(); + } + res.push_back(path); + } + } + } + return res; +} + +Geom::PathVector LPEPowerClip::getClipPathvector() +{ + Geom::PathVector res; + Geom::PathVector res_hlp; + if (!sp_lpe_item) { + return res; + } + + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + std::vector clip_path_list = clip_path->childList(true); + clip_path_list.pop_back(); + if (clip_path_list.size()) { + for (auto clip : clip_path_list) { + SPLPEItem *childitem = dynamic_cast(clip); + if (childitem) { + res_hlp = sp_get_recursive_pathvector(childitem, res_hlp, false, inverse); + if (is_load && _legacy) { + childitem->doWriteTransform(Geom::Translate(0, -999999)); + } + if (!childitem->style || !childitem->style->display.set || + childitem->style->display.value != SP_CSS_DISPLAY_NONE) { + childitem->style->display.set = TRUE; + childitem->style->display.value = SP_CSS_DISPLAY_NONE; + childitem->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + } + } + if (is_load && _legacy) { + res_hlp *= Geom::Translate(0, -999999); + _legacy = false; + } + } + } + Geom::Path bbox = sp_bbox_without_clip(sp_lpe_item); + if (hide_clip) { + return bbox; + } + if (inverse && isVisible()) { + res.push_back(bbox); + } + for (auto path : res_hlp) { + res.push_back(path); + } + return res; +} + +void LPEPowerClip::add() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *clip_path = sp_lpe_item->getClipObject(); + SPObject *elemref = NULL; + if (clip_path) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *parent = clip_path->getRepr(); + SPLPEItem *childitem = dynamic_cast(clip_path->childList(true).back()); + if (childitem) { + if (const gchar *powerclip = childitem->getRepr()->attribute("class")) { + if (!strcmp(powerclip, "powerclip")) { + Glib::ustring newclip = Glib::ustring("clipath_") + getId(); + Glib::ustring uri = Glib::ustring("url(#") + newclip + Glib::ustring(")"); + parent = clip_path->getRepr()->duplicate(xml_doc); + parent->setAttribute("id", newclip); + Inkscape::XML::Node *defs = clip_path->getRepr()->parent(); + clip_path = SP_OBJECT(document->getDefs()->appendChildRepr(parent)); + Inkscape::GC::release(parent); + sp_lpe_item->setAttribute("clip-path", uri); + SPLPEItem *childitemdel = dynamic_cast(clip_path->childList(true).back()); + if (childitemdel) { + childitemdel->setAttribute("id", getId()); + return; + } + } + } + } + Inkscape::XML::Node *clip_path_node = xml_doc->createElement("svg:path"); + parent->appendChild(clip_path_node); + Inkscape::GC::release(clip_path_node); + elemref = document->getObjectByRepr(clip_path_node); + if (elemref) { + if (childitem) { + elemref->setAttribute("style", childitem->getAttribute("style")); + } else { + elemref->setAttribute("style", "fill-rule:evenodd"); + } + elemref->setAttribute("class", "powerclip"); + elemref->setAttribute("id", getId()); + gchar *str = sp_svg_write_path(getClipPathvector()); + elemref->setAttribute("d", str); + g_free(str); + } else { + sp_lpe_item->removeCurrentPathEffect(false); + } + } else { + sp_lpe_item->removeCurrentPathEffect(false); + } +} + +Glib::ustring LPEPowerClip::getId() { return Glib::ustring("lpe_") + Glib::ustring(getLPEObj()->getId()); } + +void LPEPowerClip::upd() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *elemref = document->getObjectById(getId().c_str()); + if (elemref && sp_lpe_item) { + gchar *str = sp_svg_write_path(getClipPathvector()); + elemref->setAttribute("d", str); + g_free(str); + elemref->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } else { + add(); + } +} + + +void LPEPowerClip::doBeforeEffect(SPLPEItem const *lpeitem) +{ + if (!_updating) { + upd(); + } +} + +void +LPEPowerClip::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (keep_paths) { + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + SPLPEItem *childitem = dynamic_cast(clip_path->childList(true).front()); + childitem->deleteObject(); + } + return; + } + _updating = true; + SPObject *elemref = document->getObjectById(getId().c_str()); + if (elemref) { + elemref->deleteObject(); + } + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto clip : clip_path_list) { + SPLPEItem *childitem = dynamic_cast(clip); + if (childitem) { + if (!childitem->style || childitem->style->display.set || + childitem->style->display.value == SP_CSS_DISPLAY_NONE) { + childitem->style->display.set = TRUE; + childitem->style->display.value = SP_CSS_DISPLAY_BLOCK; + childitem->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + } + } + } +} + +Geom::PathVector +LPEPowerClip::doEffect_path(Geom::PathVector const & path_in){ + Geom::PathVector path_out = path_in; + if (flatten) { + Geom::PathVector c_pv = getClipPathvector(); + std::unique_ptr pig(new Geom::PathIntersectionGraph(c_pv, path_out)); + if (pig && !c_pv.empty() && !path_out.empty()) { + path_out = pig->getIntersection(); + } + } + return path_out; +} + +void LPEPowerClip::doOnVisibilityToggled(SPLPEItem const *lpeitem) { upd(); } + +void sp_remove_powerclip(Inkscape::Selection *sel) +{ + if (!sel->isEmpty()) { + auto selList = sel->items(); + for (auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem *lpeitem = dynamic_cast(*i); + if (lpeitem) { + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because + * the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powerclip") { + lpeitem->setCurrentPathEffect(lperef); + lpeitem->removeCurrentPathEffect(false); + break; + } + } + } + } + } + } +} + +void sp_inverse_powerclip(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + auto selList = sel->items(); + for(auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem* lpeitem = dynamic_cast(*i); + if (lpeitem) { + SPClipPath *clip_path = SP_ITEM(lpeitem)->getClipObject(); + if(clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + SPUse *use = dynamic_cast(iter); + if (use) { + g_warning("We can`t add inverse clip on clones"); + return; + } + } + Effect::createAndApply(POWERCLIP, SP_ACTIVE_DOCUMENT, lpeitem); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe) { + lpe->getRepr()->setAttribute("inverse", "true"); + } + } + } + } + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-powerclip.h b/src/live_effects/lpe-powerclip.h new file mode 100644 index 0000000..fab59ed --- /dev/null +++ b/src/live_effects/lpe-powerclip.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_POWERCLIP_H +#define INKSCAPE_LPE_POWERCLIP_H + +/* + * Inkscape::LPEPowerClip + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/message.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPowerClip : public Effect { +public: + LPEPowerClip(LivePathEffectObject *lpeobject); + ~LPEPowerClip() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* lpeitem) override; + Glib::ustring getId(); + void add(); + void upd(); + void del(); + Geom::PathVector getClipPathvector(); + + private: + BoolParam inverse; + BoolParam flatten; + BoolParam hide_clip; + MessageParam message; + bool _updating; + bool _legacy; +}; + +void sp_remove_powerclip(Inkscape::Selection *sel); +void sp_inverse_powerclip(Inkscape::Selection *sel); + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-powermask.cpp b/src/live_effects/lpe-powermask.cpp new file mode 100644 index 0000000..19c766a --- /dev/null +++ b/src/live_effects/lpe-powermask.cpp @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-powermask.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include <2geom/path-intersection.h> +#include <2geom/intersection-graph.h> +#include "display/curve.h" +#include "helper/geom.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/stringstream.h" +#include "ui/tools-switch.h" +#include "path-chemistry.h" +#include "extract-uri.h" +#include + +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-defs.h" +#include "object/sp-item-group.h" +#include "object/uri.h" + + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEPowerMask::LPEPowerMask(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + uri("Store the uri of mask", "", "uri", &wr, this, "false", false), + invert(_("Invert mask"), _("Invert mask"), "invert", &wr, this, false), + //wrap(_("Wrap mask data"), _("Wrap mask data allowing previous filters"), "wrap", &wr, this, false), + hide_mask(_("Hide mask"), _("Hide mask"), "hide_mask", &wr, this, false), + background(_("Add background to mask"), _("Add background to mask"), "background", &wr, this, false), + background_color(_("Background color and opacity"), _("Set color and opacity of the background"), "background_color", &wr, this, 0xffffffff) +{ + registerParameter(&uri); + registerParameter(&invert); + registerParameter(&hide_mask); + registerParameter(&background); + registerParameter(&background_color); + previous_color = background_color.get_value(); +} + +LPEPowerMask::~LPEPowerMask() = default; + +Glib::ustring LPEPowerMask::getId() { return Glib::ustring("mask-powermask-") + Glib::ustring(getLPEObj()->getId()); } + +void +LPEPowerMask::doOnApply (SPLPEItem const * lpeitem) +{ + SPLPEItem *item = const_cast(lpeitem); + SPObject * mask = item->getMaskObject(); + bool hasit = false; + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powermask") { + hasit = true; + break; + } + } + } + if (!mask || hasit) { + item->removeCurrentPathEffect(false); + } else { + Glib::ustring newmask = getId(); + Glib::ustring uri = Glib::ustring("url(#") + newmask + Glib::ustring(")"); + mask->setAttribute("id", newmask); + item->setAttribute("mask", uri); + } +} + +void LPEPowerMask::tryForkMask() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *mask = sp_lpe_item->getMaskObject(); + SPObject *elemref = document->getObjectById(getId().c_str()); + if (!elemref && sp_lpe_item && mask) { + Glib::ustring newmask = getId(); + Glib::ustring uri = Glib::ustring("url(#") + newmask + Glib::ustring(")"); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *fork = mask->getRepr()->duplicate(xml_doc); + mask = SP_OBJECT(document->getDefs()->appendChildRepr(fork)); + fork->setAttribute("id", newmask); + Inkscape::GC::release(fork); + sp_lpe_item->setAttribute("mask", uri); + } +} + +void +LPEPowerMask::doBeforeEffect (SPLPEItem const* lpeitem){ + //To avoid close of color dialog and better performance on change color + tryForkMask(); + SPObject * mask = SP_ITEM(sp_lpe_item)->getMaskObject(); + auto uri_str = uri.param_getSVGValue(); + if (hide_mask && mask) { + SP_ITEM(sp_lpe_item)->getMaskRef().detach(); + } else if (!hide_mask && !mask && !uri_str.empty()) { + SP_ITEM(sp_lpe_item)->getMaskRef().try_attach(uri_str.c_str()); + } + mask = SP_ITEM(sp_lpe_item)->getMaskObject(); + if (mask) { + if (previous_color != background_color.get_value()) { + previous_color = background_color.get_value(); + setMask(); + } else { + uri.param_setValue(Glib::ustring(extract_uri(sp_lpe_item->getRepr()->attribute("mask"))), true); + SP_ITEM(sp_lpe_item)->getMaskRef().detach(); + Geom::OptRect bbox = lpeitem->visualBounds(); + if(!bbox) { + return; + } + uri_str = uri.param_getSVGValue(); + SP_ITEM(sp_lpe_item)->getMaskRef().try_attach(uri_str.c_str()); + + Geom::Rect bboxrect = (*bbox); + bboxrect.expandBy(1); + mask_box.clear(); + mask_box = Geom::Path(bboxrect); + setMask(); + } + } else if(!hide_mask) { + SPLPEItem * item = const_cast(lpeitem); + item->removeCurrentPathEffect(false); + } +} + +void +LPEPowerMask::setMask(){ + SPMask *mask = SP_ITEM(sp_lpe_item)->getMaskObject(); + SPObject *elemref = nullptr; + SPDocument *document = getSPDoc(); + if (!document || !mask) { + return; + } + Inkscape::XML::Node *root = document->getReprRoot(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *box = nullptr; + Inkscape::XML::Node *filter = nullptr; + SPDefs * defs = document->getDefs(); + Glib::ustring mask_id = getId(); + Glib::ustring box_id = mask_id + (Glib::ustring)"_box"; + Glib::ustring filter_id = mask_id + (Glib::ustring)"_inverse"; + Glib::ustring filter_label = (Glib::ustring)"filter" + mask_id; + Glib::ustring filter_uri = (Glib::ustring)"url(#" + filter_id + (Glib::ustring)")"; + if (!(elemref = document->getObjectById(filter_id))) { + filter = xml_doc->createElement("svg:filter"); + filter->setAttribute("id", filter_id); + filter->setAttribute("inkscape:label", filter_label); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "color-interpolation-filters", "sRGB"); + sp_repr_css_change(filter, css, "style"); + sp_repr_css_attr_unref(css); + filter->setAttribute("height", "100"); + filter->setAttribute("width", "100"); + filter->setAttribute("x", "-50"); + filter->setAttribute("y", "-50"); + Inkscape::XML::Node *primitive1 = xml_doc->createElement("svg:feColorMatrix"); + Glib::ustring primitive1_id = (mask_id + (Glib::ustring)"_primitive1").c_str(); + primitive1->setAttribute("id", primitive1_id); + primitive1->setAttribute("values", "1"); + primitive1->setAttribute("type", "saturate"); + primitive1->setAttribute("result", "fbSourceGraphic"); + Inkscape::XML::Node *primitive2 = xml_doc->createElement("svg:feColorMatrix"); + Glib::ustring primitive2_id = (mask_id + (Glib::ustring)"_primitive2").c_str(); + primitive2->setAttribute("id", primitive2_id); + primitive2->setAttribute("values", "-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "); + primitive2->setAttribute("in", "fbSourceGraphic"); + elemref = defs->appendChildRepr(filter); + Inkscape::GC::release(filter); + filter->appendChild(primitive1); + Inkscape::GC::release(primitive1); + filter->appendChild(primitive2); + Inkscape::GC::release(primitive2); + } + Glib::ustring g_data_id = mask_id + (Glib::ustring)"_container"; + if((elemref = document->getObjectById(g_data_id))){ + std::vector item_list = sp_item_group_item_list(SP_GROUP(elemref)); + for (auto iter : item_list) { + Inkscape::XML::Node *mask_node = iter->getRepr(); + elemref->getRepr()->removeChild(mask_node); + mask->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + } + elemref->deleteObject(true); + } + std::vector mask_list = mask->childList(true); + for (auto iter : mask_list) { + SPItem * mask_data = SP_ITEM(iter); + Inkscape::XML::Node *mask_node = mask_data->getRepr(); + if (! strcmp(mask_data->getId(), box_id.c_str())){ + continue; + } + Glib::ustring mask_data_id = (Glib::ustring)mask_data->getId(); + SPCSSAttr *css = sp_repr_css_attr_new(); + if(mask_node->attribute("style")) { + sp_repr_css_attr_add_from_string(css, mask_node->attribute("style")); + } + char const* filter = sp_repr_css_property (css, "filter", nullptr); + if(!filter || !strcmp(filter, filter_uri.c_str())) { + if (invert && is_visible) { + sp_repr_css_set_property (css, "filter", filter_uri.c_str()); + } else { + sp_repr_css_set_property (css, "filter", nullptr); + } + Glib::ustring css_str; + sp_repr_css_write_string(css, css_str); + mask_node->setAttribute("style", css_str); + } + } + if ((elemref = document->getObjectById(box_id))) { + elemref->deleteObject(true); + } + if (background && is_visible) { + bool exist = true; + if (!(elemref = document->getObjectById(box_id))) { + box = xml_doc->createElement("svg:path"); + box->setAttribute("id", box_id); + exist = false; + } + Glib::ustring style; + gchar c[32]; + unsigned const rgb24 = background_color.get_value() >> 8; + sprintf(c, "#%06x", rgb24); + style = Glib::ustring("fill:") + Glib::ustring(c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(background_color.get_value()); + style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, style.c_str()); + char const* filter = sp_repr_css_property (css, "filter", nullptr); + if(!filter || !strcmp(filter, filter_uri.c_str())) { + if (invert && is_visible) { + sp_repr_css_set_property (css, "filter", filter_uri.c_str()); + } else { + sp_repr_css_set_property (css, "filter", nullptr); + } + + } + Glib::ustring css_str; + sp_repr_css_write_string(css, css_str); + box->setAttributeOrRemoveIfEmpty("style", css_str); + gchar * box_str = sp_svg_write_path( mask_box ); + box->setAttribute("d" , box_str); + g_free(box_str); + if (!exist) { + elemref = mask->appendChildRepr(box); + Inkscape::GC::release(box); + } + box->setPosition(0); + } else if ((elemref = document->getObjectById(box_id))) { + elemref->deleteObject(true); + } + mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +LPEPowerMask::doOnVisibilityToggled(SPLPEItem const* lpeitem) +{ + doBeforeEffect(lpeitem); +} + +void +LPEPowerMask::doEffect (SPCurve * curve) +{ +} + +void +LPEPowerMask::doOnRemove (SPLPEItem const* lpeitem) +{ + SPMask *mask = lpeitem->getMaskObject(); + if (mask) { + if (keep_paths) { + return; + } + invert.param_setValue(false); + //wrap.param_setValue(false); + background.param_setValue(false); + setMask(); + SPObject *elemref = nullptr; + SPDocument *document = getSPDoc(); + Glib::ustring mask_id = getId(); + Glib::ustring filter_id = mask_id + (Glib::ustring)"_inverse"; + if ((elemref = document->getObjectById(filter_id))) { + elemref->deleteObject(true); + } + } +} + +void sp_inverse_powermask(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + SPDocument *document = SP_ACTIVE_DOCUMENT; + if (!document) { + return; + } + auto selList = sel->items(); + for(auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem* lpeitem = dynamic_cast(*i); + if (lpeitem) { + SPMask *mask = lpeitem->getMaskObject(); + if (mask) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *parent = mask->getRepr(); + Effect::createAndApply(POWERMASK, SP_ACTIVE_DOCUMENT, lpeitem); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe) { + lpe->getRepr()->setAttribute("invert", "false"); + lpe->getRepr()->setAttribute("is_visible", "true"); + lpe->getRepr()->setAttribute("hide_mask", "false"); + lpe->getRepr()->setAttribute("background", "true"); + lpe->getRepr()->setAttribute("background_color", "#ffffffff"); + } + } + } + } + } +} + +void sp_remove_powermask(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + auto selList = sel->items(); + for (auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem *lpeitem = dynamic_cast(*i); + if (lpeitem) { + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because + * the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powermask") { + lpeitem->setCurrentPathEffect(lperef); + lpeitem->removeCurrentPathEffect(false); + break; + } + } + } + } + } + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-powermask.h b/src/live_effects/lpe-powermask.h new file mode 100644 index 0000000..16b74d5 --- /dev/null +++ b/src/live_effects/lpe-powermask.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_POWERMASK_H +#define INKSCAPE_LPE_POWERMASK_H + +/* + * Inkscape::LPEPowerMask + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/colorpicker.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPowerMask : public Effect { +public: + LPEPowerMask(LivePathEffectObject *lpeobject); + ~LPEPowerMask() override; + void doOnApply (SPLPEItem const * lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doEffect (SPCurve * curve) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* lpeitem) override; + void toggleMaskVisibility(); + void tryForkMask(); + Glib::ustring getId(); + void setMask(); +private: + HiddenParam uri; + BoolParam invert; + //BoolParam wrap; + BoolParam hide_mask; + BoolParam background; + ColorPickerParam background_color; + Geom::Path mask_box; + guint32 previous_color; +}; + +void sp_remove_powermask(Inkscape::Selection *sel); +void sp_inverse_powermask(Inkscape::Selection *sel); + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-powerstroke-interpolators.h b/src/live_effects/lpe-powerstroke-interpolators.h new file mode 100644 index 0000000..940d97e --- /dev/null +++ b/src/live_effects/lpe-powerstroke-interpolators.h @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Interpolators for lists of points. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2010-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H +#define INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H + +#include <2geom/path.h> +#include <2geom/bezier-utils.h> +#include <2geom/sbasis-to-bezier.h> + +#include "live_effects/spiro.h" + + +/// @TODO Move this to 2geom? +namespace Geom { +namespace Interpolate { + +enum InterpolatorType { + INTERP_LINEAR, + INTERP_CUBICBEZIER, + INTERP_CUBICBEZIER_JOHAN, + INTERP_SPIRO, + INTERP_CUBICBEZIER_SMOOTH, + INTERP_CENTRIPETAL_CATMULLROM +}; + +class Interpolator { +public: + Interpolator() = default;; + virtual ~Interpolator() = default;; + + static Interpolator* create(InterpolatorType type); + + virtual Geom::Path interpolateToPath(std::vector const &points) const = 0; + +private: + Interpolator(const Interpolator&) = delete; + Interpolator& operator=(const Interpolator&) = delete; +}; + +class Linear : public Interpolator { +public: + Linear() = default;; + ~Linear() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + Path path; + path.start( points.at(0) ); + for (unsigned int i = 1 ; i < points.size(); ++i) { + path.appendNew(points.at(i)); + } + return path; + }; + +private: + Linear(const Linear&) = delete; + Linear& operator=(const Linear&) = delete; +}; + +// this class is terrible +class CubicBezierFit : public Interpolator { +public: + CubicBezierFit() = default;; + ~CubicBezierFit() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + unsigned int n_points = points.size(); + // worst case gives us 2 segment per point + int max_segs = 8*n_points; + Geom::Point * b = g_new(Geom::Point, max_segs); + Geom::Point * points_array = g_new(Geom::Point, 4*n_points); + for (unsigned i = 0; i < n_points; ++i) { + points_array[i] = points.at(i); + } + + double tolerance_sq = 0; // this value is just a random guess + + int const n_segs = Geom::bezier_fit_cubic_r(b, points_array, n_points, + tolerance_sq, max_segs); + + Geom::Path fit; + if ( n_segs > 0) + { + fit.start(b[0]); + for (int c = 0; c < n_segs; c++) { + fit.appendNew(b[4*c+1], b[4*c+2], b[4*c+3]); + } + } + g_free(b); + g_free(points_array); + return fit; + }; + +private: + CubicBezierFit(const CubicBezierFit&) = delete; + CubicBezierFit& operator=(const CubicBezierFit&) = delete; +}; + +/// @todo invent name for this class +class CubicBezierJohan : public Interpolator { +public: + CubicBezierJohan(double beta = 0.2) { + _beta = beta; + }; + ~CubicBezierJohan() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + Path fit; + fit.start(points.at(0)); + for (unsigned int i = 1; i < points.size(); ++i) { + Point p0 = points.at(i-1); + Point p1 = points.at(i); + Point dx = Point(p1[X] - p0[X], 0); + fit.appendNew(p0+_beta*dx, p1-_beta*dx, p1); + } + return fit; + }; + + void setBeta(double beta) { + _beta = beta; + } + + double _beta; + +private: + CubicBezierJohan(const CubicBezierJohan&) = delete; + CubicBezierJohan& operator=(const CubicBezierJohan&) = delete; +}; + +/// @todo invent name for this class +class CubicBezierSmooth : public Interpolator { +public: + CubicBezierSmooth(double beta = 0.2) { + _beta = beta; + }; + ~CubicBezierSmooth() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + Path fit; + fit.start(points.at(0)); + unsigned int num_points = points.size(); + for (unsigned int i = 1; i < num_points; ++i) { + Point p0 = points.at(i-1); + Point p1 = points.at(i); + Point dx = Point(p1[X] - p0[X], 0); + if (i == 1) { + fit.appendNew(p0, p1-0.75*dx, p1); + } else if (i == points.size() - 1) { + fit.appendNew(p0+0.75*dx, p1, p1); + } else { + fit.appendNew(p0+_beta*dx, p1-_beta*dx, p1); + } + } + return fit; + }; + + void setBeta(double beta) { + _beta = beta; + } + + double _beta; + +private: + CubicBezierSmooth(const CubicBezierSmooth&) = delete; + CubicBezierSmooth& operator=(const CubicBezierSmooth&) = delete; +}; + +class SpiroInterpolator : public Interpolator { +public: + SpiroInterpolator() = default;; + ~SpiroInterpolator() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + Path fit; + + Coord scale_y = 100.; + + guint len = points.size(); + Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, len); + for (unsigned int i = 0; i < len; ++i) { + controlpoints[i].x = points[i][X]; + controlpoints[i].y = points[i][Y] / scale_y; + controlpoints[i].ty = 'c'; + } + controlpoints[0].ty = '{'; + controlpoints[1].ty = 'v'; + controlpoints[len-2].ty = 'v'; + controlpoints[len-1].ty = '}'; + + Spiro::spiro_run(controlpoints, len, fit); + + fit *= Scale(1,scale_y); + g_free(controlpoints); + return fit; + }; + +private: + SpiroInterpolator(const SpiroInterpolator&) = delete; + SpiroInterpolator& operator=(const SpiroInterpolator&) = delete; +}; + +// Quick mockup for testing the behavior for powerstroke controlpoint interpolation +class CentripetalCatmullRomInterpolator : public Interpolator { +public: + CentripetalCatmullRomInterpolator() = default;; + ~CentripetalCatmullRomInterpolator() override = default;; + + Path interpolateToPath(std::vector const &points) const override { + unsigned int n_points = points.size(); + + Geom::Path fit(points.front()); + + if (n_points < 3) return fit; // TODO special cases for 0,1 and 2 input points + + // return n_points-1 cubic segments + + // duplicate first point + fit.append(calc_bezier(points[0],points[0],points[1],points[2])); + + for (std::size_t i = 0; i < n_points-2; ++i) { + Point p0 = points[i]; + Point p1 = points[i+1]; + Point p2 = points[i+2]; + Point p3 = (i < n_points-3) ? points[i+3] : points[i+2]; + + fit.append(calc_bezier(p0, p1, p2, p3)); + } + + return fit; + }; + +private: + CubicBezier calc_bezier(Point p0, Point p1, Point p2, Point p3) const { + // create interpolating bezier between p1 and p2 + + // Part of the code comes from StackOverflow user eriatarka84 + // http://stackoverflow.com/a/23980479/2929337 + + // calculate time coords (deltas) of points + // the factor 0.25 can be generalized for other Catmull-Rom interpolation types + // see alpha in Yuksel et al. "On the Parameterization of Catmull-Rom Curves", + // --> http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf + double dt0 = powf(distanceSq(p0, p1), 0.25); + double dt1 = powf(distanceSq(p1, p2), 0.25); + double dt2 = powf(distanceSq(p2, p3), 0.25); + + + // safety check for repeated points + double eps = Geom::EPSILON; + if (dt1 < eps) + dt1 = 1.0; + if (dt0 < eps) + dt0 = dt1; + if (dt2 < eps) + dt2 = dt1; + + // compute tangents when parameterized in [t1,t2] + Point tan1 = (p1 - p0) / dt0 - (p2 - p0) / (dt0 + dt1) + (p2 - p1) / dt1; + Point tan2 = (p2 - p1) / dt1 - (p3 - p1) / (dt1 + dt2) + (p3 - p2) / dt2; + // rescale tangents for parametrization in [0,1] + tan1 *= dt1; + tan2 *= dt1; + + // create bezier from tangents (this is already in 2geom somewhere, or should be moved to it) + // the tangent of a bezier curve is: B'(t) = 3(1-t)^2 (b1 - b0) + 6(1-t)t(b2-b1) + 3t^2(b3-b2) + // So we have to make sure that B'(0) = tan1 and B'(1) = tan2, and we already know that b0=p1 and b3=p2 + // tan1 = B'(0) = 3 (b1 - p1) --> p1 + (tan1)/3 = b1 + // tan2 = B'(1) = 3 (p2 - b2) --> p2 - (tan2)/3 = b2 + + Point b0 = p1; + Point b1 = p1 + tan1 / 3; + Point b2 = p2 - tan2 / 3; + Point b3 = p2; + + return CubicBezier(b0, b1, b2, b3); + } + + CentripetalCatmullRomInterpolator(const CentripetalCatmullRomInterpolator&) = delete; + CentripetalCatmullRomInterpolator& operator=(const CentripetalCatmullRomInterpolator&) = delete; +}; + + +inline Interpolator* +Interpolator::create(InterpolatorType type) { + switch (type) { + case INTERP_LINEAR: + return new Geom::Interpolate::Linear(); + case INTERP_CUBICBEZIER: + return new Geom::Interpolate::CubicBezierFit(); + case INTERP_CUBICBEZIER_JOHAN: + return new Geom::Interpolate::CubicBezierJohan(); + case INTERP_SPIRO: + return new Geom::Interpolate::SpiroInterpolator(); + case INTERP_CUBICBEZIER_SMOOTH: + return new Geom::Interpolate::CubicBezierSmooth(); + case INTERP_CENTRIPETAL_CATMULLROM: + return new Geom::Interpolate::CentripetalCatmullRomInterpolator(); + default: + return new Geom::Interpolate::Linear(); + } +} + +} //namespace Interpolate +} //namespace Geom + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-powerstroke.cpp b/src/live_effects/lpe-powerstroke.cpp new file mode 100644 index 0000000..ee0ccf7 --- /dev/null +++ b/src/live_effects/lpe-powerstroke.cpp @@ -0,0 +1,869 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * PowerStroke LPE implementation. Creates curves with modifiable stroke width. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2010-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-powerstroke-interpolators.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpeobject.h" + +#include "svg/svg-color.h" +#include "desktop-style.h" +#include "svg/css-ostringstream.h" +#include "display/curve.h" + +#include <2geom/elliptical-arc.h> +#include <2geom/path-sink.h> +#include <2geom/path-intersection.h> +#include <2geom/circle.h> +#include "helper/geom.h" + +#include "object/sp-shape.h" +#include "style.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Geom { +// should all be moved to 2geom at some point + +/** Find the point where two straight lines cross. +*/ +static boost::optional intersection_point( Point const & origin_a, Point const & vector_a, + Point const & origin_b, Point const & vector_b) +{ + Coord denom = cross(vector_a, vector_b); + if (!are_near(denom,0.)){ + Coord t = (cross(vector_b, origin_a) + cross(origin_b, vector_b)) / denom; + return origin_a + t * vector_a; + } + return boost::none; +} + +static Geom::CubicBezier sbasis_to_cubicbezier(Geom::D2 const & sbasis_in) +{ + std::vector temp; + sbasis_to_bezier(temp, sbasis_in, 4); + return Geom::CubicBezier( temp ); +} + +/** + * document this! + * very quick: this finds the ellipse with minimum eccentricity + passing through point P and Q, with tangent PO at P and QO at Q + http://mathforum.org/kb/message.jspa?messageID=7471596&tstart=0 + */ +static Ellipse find_ellipse(Point P, Point Q, Point O) +{ + Point p = P - O; + Point q = Q - O; + Coord K = 4 * dot(p,q) / (L2sq(p) + L2sq(q)); + + double cross = p[Y]*q[X] - p[X]*q[Y]; + double a = -q[Y]/cross; + double b = q[X]/cross; + double c = (O[X]*q[Y] - O[Y]*q[X])/cross; + + double d = p[Y]/cross; + double e = -p[X]/cross; + double f = (-O[X]*p[Y] + O[Y]*p[X])/cross; + + // Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0 + double A = (a*d*K+d*d+a*a); + double B = (a*e*K+b*d*K+2*d*e+2*a*b); + double C = (b*e*K+e*e+b*b); + double D = (a*f*K+c*d*K+2*d*f-2*d+2*a*c-2*a); + double E = (b*f*K+c*e*K+2*e*f-2*e+2*b*c-2*b); + double F = c*f*K+f*f-2*f+c*c-2*c+1; + + return Ellipse(A, B, C, D, E, F); +} + +/** + * Find circle that touches inside of the curve, with radius matching the curvature, at time value \c t. + * Because this method internally uses unitTangentAt, t should be smaller than 1.0 (see unitTangentAt). + */ +static Circle touching_circle( D2 const &curve, double t, double tol=0.01 ) +{ + //Piecewise k = curvature(curve, tol); + D2 dM=derivative(curve); + if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) { + dM=derivative(dM); + } + if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) { // try second time + dM=derivative(dM); + } + if ( dM.isZero(tol) || (are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) )) { // admit defeat + return Geom::Circle(Geom::Point(0., 0.), 0.); + } + Piecewise > unitv = unitVector(dM,tol); + if (unitv.empty()) { // admit defeat + return Geom::Circle(Geom::Point(0., 0.), 0.); + } + Piecewise dMlength = dot(Piecewise >(dM),unitv); + Piecewise k = cross(derivative(unitv),unitv); + k = divide(k,dMlength,tol,3); + double curv = k(t); // note that this value is signed + + Geom::Point normal = unitTangentAt(curve, t).cw(); + double radius = 1/curv; + Geom::Point center = curve(t) + radius*normal; + return Geom::Circle(center, fabs(radius)); +} + +} // namespace Geom + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData InterpolatorTypeData[] = { + {Geom::Interpolate::INTERP_CUBICBEZIER_SMOOTH, N_("CubicBezierSmooth"), "CubicBezierSmooth"}, + {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"}, + {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"}, + {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"}, + {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"}, + {Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM, N_("Centripetal Catmull-Rom"), "CentripetalCatmullRom"} +}; +static const Util::EnumDataConverter InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData)); + +enum LineJoinType { + LINEJOIN_BEVEL, + LINEJOIN_ROUND, + LINEJOIN_EXTRP_MITER, + LINEJOIN_MITER, + LINEJOIN_SPIRO, + LINEJOIN_EXTRP_MITER_ARC +}; +static const Util::EnumData LineJoinTypeData[] = { + {LINEJOIN_BEVEL, N_("Beveled"), "bevel"}, + {LINEJOIN_ROUND, N_("Rounded"), "round"}, +// {LINEJOIN_EXTRP_MITER, N_("Extrapolated"), "extrapolated"}, // disabled because doesn't work well + {LINEJOIN_EXTRP_MITER_ARC, N_("Extrapolated arc"), "extrp_arc"}, + {LINEJOIN_MITER, N_("Miter"), "miter"}, + {LINEJOIN_SPIRO, N_("Spiro"), "spiro"}, +}; +static const Util::EnumDataConverter LineJoinTypeConverter(LineJoinTypeData, sizeof(LineJoinTypeData)/sizeof(*LineJoinTypeData)); + +LPEPowerStroke::LPEPowerStroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + offset_points(_("Offset points"), _("Offset points"), "offset_points", &wr, this), + sort_points(_("Sort points"), _("Sort offset points according to their time value along the curve"), "sort_points", &wr, this, true), + interpolator_type(_("Interpolator type:"), _("Determines which kind of interpolator will be used to interpolate between stroke width along the path"), "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM), + interpolator_beta(_("Smoothness:"), _("Sets the smoothness for the CubicBezierJohan interpolator; 0 = linear interpolation, 1 = smooth"), "interpolator_beta", &wr, this, 0.2), + scale_width(_("Width scale:"), _("Width scale all points"), "scale_width", &wr, this, 1.0), + start_linecap_type(_("Start cap:"), _("Determines the shape of the path's start"), "start_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", LineJoinTypeConverter, &wr, this, LINEJOIN_ROUND), + miter_limit(_("Miter limit:"), _("Maximum length of the miter (in units of stroke width)"), "miter_limit", &wr, this, 4.), + end_linecap_type(_("End cap:"), _("Determines the shape of the path's end"), "end_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH) +{ + show_orig_path = true; + + /// @todo offset_points are initialized with empty path, is that bug-save? + + interpolator_beta.addSlider(true); + interpolator_beta.param_set_range(0.,1.); + + registerParameter(&offset_points); + registerParameter(&sort_points); + registerParameter(&interpolator_type); + registerParameter(&interpolator_beta); + registerParameter(&start_linecap_type); + registerParameter(&linejoin_type); + registerParameter(&miter_limit); + registerParameter(&scale_width); + registerParameter(&end_linecap_type); + scale_width.param_set_range(0.0, Geom::infinity()); + scale_width.param_set_increments(0.1, 0.1); + scale_width.param_set_digits(4); + recusion_limit = 0; + has_recursion = false; +} + +LPEPowerStroke::~LPEPowerStroke() = default; + +void +LPEPowerStroke::doBeforeEffect(SPLPEItem const *lpeItem) +{ + offset_points.set_scale_width(scale_width); + if (has_recursion) { + has_recursion = false; + adjustForNewPath(pathvector_before_effect); + } +} + +void LPEPowerStroke::applyStyle(SPLPEItem *lpeitem) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + if (lpeitem->style) { + if (lpeitem->style->stroke.isPaintserver()) { + SPPaintServer *server = lpeitem->style->getStrokePaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "fill", str.c_str()); + } + } else if (lpeitem->style->stroke.isColor()) { + gchar c[64]; + sp_svg_write_color( + c, sizeof(c), + lpeitem->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->stroke_opacity.value))); + sp_repr_css_set_property(css, "fill", c); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + } else { + sp_repr_css_unset_property(css, "fill"); + } + + sp_repr_css_set_property(css, "fill-rule", "nonzero"); + sp_repr_css_set_property(css, "stroke", "none"); + + sp_desktop_apply_css_recursive(lpeitem, css, true); + sp_repr_css_attr_unref(css); +} + +void +LPEPowerStroke::doOnApply(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem)) { + SPLPEItem* item = const_cast(lpeitem); + std::vector points; + Geom::PathVector const &pathv = pathv_to_linear_and_cubic_beziers(SP_SHAPE(lpeitem)->_curve->get_pathvector()); + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed / 2 : 1.; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil"; + bool powerpencil = prefs->getBool(pref_path_pp, false); + bool clipboard = offset_points.data().size() > 0; + if (!powerpencil) { + applyStyle(item); + } + if (!clipboard && !powerpencil) { + item->updateRepr(); + if (pathv.empty()) { + points.emplace_back(0.2,width ); + points.emplace_back(0.5, width); + points.emplace_back(0.8, width); + } else { + Geom::Path const &path = pathv.front(); + Geom::Path::size_type const size = path.size_default(); + if (!path.closed()) { + points.emplace_back(0.2, width); + } + points.emplace_back(0.5 * size, width); + if (!path.closed()) { + points.emplace_back(size - 0.2, width); + } + } + offset_points.param_set_and_write_new_value(points); + } + offset_points.set_scale_width(scale_width); + } else { + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE Powerstroke can only be applied to shapes (not groups)."); + } + } +} + +void LPEPowerStroke::doOnRemove(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem) && !keep_paths) { + SPLPEItem *item = const_cast(lpeitem); + SPCSSAttr *css = sp_repr_css_attr_new (); + if (lpeitem->style->fill.isPaintserver()) { + SPPaintServer * server = lpeitem->style->getFillPaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property (css, "stroke", str.c_str()); + } + } else if (lpeitem->style->fill.isColor()) { + char c[64] = {0}; + sp_svg_write_color (c, sizeof(c), lpeitem->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->fill_opacity.value))); + sp_repr_css_set_property (css, "stroke", c); + } else { + sp_repr_css_set_property (css, "stroke", "none"); + } + + Inkscape::CSSOStringStream os; + os << std::abs(offset_points.median_width()) * 2; + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + + sp_repr_css_set_property(css, "fill", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + + item->updateRepr(); + } +} + +void +LPEPowerStroke::adjustForNewPath(Geom::PathVector const & path_in) +{ + if (!path_in.empty()) { + offset_points.recalculate_controlpoints_for_new_pwd2(path_in[0].toPwSb()); + } +} + +static bool compare_offsets (Geom::Point first, Geom::Point second) +{ + return first[Geom::X] < second[Geom::X]; +} + +static Geom::Path path_from_piecewise_fix_cusps( Geom::Piecewise > const & B, + Geom::Piecewise const & y, // width path + LineJoinType jointype, + double miter_limit, + double tol=Geom::EPSILON) +{ +/* per definition, each discontinuity should be fixed with a join-ending, as defined by linejoin_type +*/ + Geom::PathBuilder pb; + Geom::OptRect bbox = bounds_fast(B); + if (B.empty() || !bbox) { + return pb.peek().front(); + } + + pb.setStitching(true); + + Geom::Point start = B[0].at0(); + pb.moveTo(start); + build_from_sbasis(pb, B[0], tol, false); + unsigned prev_i = 0; + for (unsigned i=1; i < B.size(); i++) { + // Skip degenerate segments. The number below was determined, after examining + // very many paths with powerstrokes of all shapes and sizes, to allow filtering out most + // degenerate segments without losing significant quality; it is close to 1/256. + if (B[i].isConstant(4e-3)) { + continue; + } + if (!are_near(B[prev_i].at1(), B[i].at0(), tol) ) + { // discontinuity found, so fix it :-) + double width = y( B.cuts[i] ); + + Geom::Point tang1 = -unitTangentAt(reverse(B[prev_i]),0.); // = unitTangentAt(B[prev_i],1); + Geom::Point tang2 = unitTangentAt(B[i],0); + Geom::Point discontinuity_vec = B[i].at0() - B[prev_i].at1(); + bool on_outside = ( dot(tang1, discontinuity_vec) >= 0. ); + + if (on_outside) { + // we are on the outside: add some type of join! + switch (jointype) { + case LINEJOIN_ROUND: { + /* for constant width paths, the rounding is a circular arc (rx == ry), + for non-constant width paths, the rounding can be done with an ellipse but is hard and ambiguous. + The elliptical arc should go through the discontinuity's start and end points (of course!) + and also should match the discontinuity tangents at those start and end points. + To resolve the ambiguity, the elliptical arc with minimal eccentricity should be chosen. + A 2Geom method was created to do exactly this :) + */ + + boost::optional O = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (!O) { + // no center found, i.e. 180 degrees round + pb.lineTo(B[i].at0()); // default to bevel for too shallow cusp angles + break; + } + + Geom::Ellipse ellipse; + try { + ellipse = find_ellipse(B[prev_i].at1(), B[i].at0(), *O); + } + catch (Geom::LogicalError &e) { + // 2geom did not find a fitting ellipse, this happens for weird thick paths :) + // do bevel, and break + pb.lineTo(B[i].at0()); + break; + } + + // check if ellipse.ray is within 'sane' range. + if ( ( fabs(ellipse.ray(Geom::X)) > 1e6 ) || + ( fabs(ellipse.ray(Geom::Y)) > 1e6 ) ) + { + // do bevel, and break + pb.lineTo(B[i].at0()); + break; + } + + pb.arcTo( ellipse.ray(Geom::X), ellipse.ray(Geom::Y), ellipse.rotationAngle(), + false, width < 0, B[i].at0() ); + + break; + } + case LINEJOIN_EXTRP_MITER: { + Geom::D2 newcurve1 = B[prev_i] * Geom::reflection(rot90(tang1), B[prev_i].at1()); + Geom::CubicBezier bzr1 = sbasis_to_cubicbezier( reverse(newcurve1) ); + + Geom::D2 newcurve2 = B[i] * Geom::reflection(rot90(tang2), B[i].at0()); + Geom::CubicBezier bzr2 = sbasis_to_cubicbezier( reverse(newcurve2) ); + + Geom::Crossings cross = crossings(bzr1, bzr2); + if (cross.empty()) { + // empty crossing: default to bevel + pb.lineTo(B[i].at0()); + } else { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(bzr1.pointAt(cross[0].ta), point_on_path); + if (len > fabs(width) * miter_limit) { + // miter too big: default to bevel + pb.lineTo(B[i].at0()); + } else { + std::pair sub1 = bzr1.subdivide(cross[0].ta); + std::pair sub2 = bzr2.subdivide(cross[0].tb); + pb.curveTo(sub1.first[1], sub1.first[2], sub1.first[3]); + pb.curveTo(sub2.second[1], sub2.second[2], sub2.second[3]); + } + } + break; + } + case LINEJOIN_EXTRP_MITER_ARC: { + // Extrapolate using the curvature at the end of the path segments to join + Geom::Circle circle1 = Geom::touching_circle(reverse(B[prev_i]), 0.0); + Geom::Circle circle2 = Geom::touching_circle(B[i], 0.0); + std::vector solutions; + solutions = circle1.intersect(circle2); + if (solutions.size() == 2) { + Geom::Point sol(0.,0.); + bool solok = true; + bool point0bad = false; + bool point1bad = false; + if ( dot(tang2, solutions[0].point() - B[i].at0()) > 0) + { + // points[0] is bad, choose points[1] + point0bad = true; + } + if ( dot(tang2, solutions[1].point() - B[i].at0()) > 0) + { + // points[1] is bad, choose points[0] + point1bad = true; + } + if (!point0bad && !point1bad ) { + // both points are good, choose nearest + sol = ( distanceSq(B[i].at0(), solutions[0].point()) < distanceSq(B[i].at0(), solutions[1].point()) ) ? + solutions[0].point() : solutions[1].point(); + } else if (!point0bad) { + sol = solutions[0].point(); + } else if (!point1bad) { + sol = solutions[1].point(); + } else { + solok = false; + } + (*bbox).expandBy (bbox->width()/4); + + if (!(*bbox).contains(sol)) { + solok = false; + } + Geom::EllipticalArc *arc0 = nullptr; + Geom::EllipticalArc *arc1 = nullptr; + bool build = false; + if (solok) { + arc0 = circle1.arc(B[prev_i].at1(), 0.5*(B[prev_i].at1()+sol), sol); + arc1 = circle2.arc(sol, 0.5*(sol+B[i].at0()), B[i].at0()); + if (arc0) { + // FIX: Some assertions errors here + build_from_sbasis(pb,arc0->toSBasis(), tol, false); + build = true; + } else if (arc1) { + boost::optional p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + build = true; + } + } + } + if (build) { + build_from_sbasis(pb,arc1->toSBasis(), tol, false); + } else if (arc0) { + pb.lineTo(B[i].at0()); + } + } + if (!solok || !(arc0 && build)) { + // fall back to miter + boost::optional p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + } + if (arc0) { + delete arc0; + arc0 = nullptr; + } + if (arc1) { + delete arc1; + arc1 = nullptr; + } + } else { + // fall back to miter + boost::optional p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + } + /*else if (solutions == 1) { // one circle is inside the other + // don't know what to do: default to bevel + pb.lineTo(B[i].at0()); + } else { // no intersections + // don't know what to do: default to bevel + pb.lineTo(B[i].at0()); + } */ + + break; + } + case LINEJOIN_MITER: { + boost::optional p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + break; + } + case LINEJOIN_SPIRO: { + Geom::Point direction = B[i].at0() - B[prev_i].at1(); + double tang1_sign = dot(direction,tang1); + double tang2_sign = dot(direction,tang2); + + Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, 4); + controlpoints[0].x = (B[prev_i].at1() - tang1_sign*tang1)[Geom::X]; + controlpoints[0].y = (B[prev_i].at1() - tang1_sign*tang1)[Geom::Y]; + controlpoints[0].ty = '{'; + controlpoints[1].x = B[prev_i].at1()[Geom::X]; + controlpoints[1].y = B[prev_i].at1()[Geom::Y]; + controlpoints[1].ty = ']'; + controlpoints[2].x = B[i].at0()[Geom::X]; + controlpoints[2].y = B[i].at0()[Geom::Y]; + controlpoints[2].ty = '['; + controlpoints[3].x = (B[i].at0() + tang2_sign*tang2)[Geom::X]; + controlpoints[3].y = (B[i].at0() + tang2_sign*tang2)[Geom::Y]; + controlpoints[3].ty = '}'; + + Geom::Path spiro; + Spiro::spiro_run(controlpoints, 4, spiro); + pb.append(spiro.portion(1, spiro.size_open() - 1)); + break; + } + case LINEJOIN_BEVEL: + default: + pb.lineTo(B[i].at0()); + break; + } + + build_from_sbasis(pb, B[i], tol, false); + + } else { + // we are on inside of corner! + Geom::Path bzr1 = path_from_sbasis( B[prev_i], tol ); + Geom::Path bzr2 = path_from_sbasis( B[i], tol ); + Geom::Crossings cross = crossings(bzr1, bzr2); + if (cross.size() != 1) { + // empty crossing or too many crossings: default to bevel + pb.lineTo(B[i].at0()); + pb.append(bzr2); + } else { + // :-) quick hack: + for (unsigned i=0; i < bzr1.size_open(); ++i) { + pb.backspace(); + } + + pb.append( bzr1.portion(0, cross[0].ta) ); + pb.append( bzr2.portion(cross[0].tb, bzr2.size_open()) ); + } + } + } else { + build_from_sbasis(pb, B[i], tol, false); + } + + prev_i = i; + } + pb.flush(); + return pb.peek().front(); +} + +Geom::PathVector +LPEPowerStroke::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + + Geom::PathVector path_out; + if (path_in.empty()) { + return path_in; + } + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::Piecewise > pwd2_in = pathv[0].toPwSb(); + if (pwd2_in.empty()) { + return path_in; + } + Piecewise > der = derivative(pwd2_in); + if (der.empty()) { + return path_in; + } + Piecewise > n = unitVector(der,0.00001); + if (n.empty()) { + return path_in; + } + + n = rot90(n); + offset_points.set_pwd2(pwd2_in, n); + + LineCapType end_linecap = static_cast(end_linecap_type.get_value()); + LineCapType start_linecap = static_cast(start_linecap_type.get_value()); + + std::vector ts_no_scale = offset_points.data(); + if (ts_no_scale.empty()) { + return path_in; + } + std::vector ts; + for (auto & tsp : ts_no_scale) { + Geom::Point p = Geom::Point(tsp[Geom::X], tsp[Geom::Y] * scale_width); + ts.push_back(p); + } + if (sort_points) { + sort(ts.begin(), ts.end(), compare_offsets); + } + // create stroke path where points (x,y) := (t, offset) + Geom::Interpolate::Interpolator *interpolator = Geom::Interpolate::Interpolator::create(static_cast(interpolator_type.get_value())); + if (Geom::Interpolate::CubicBezierJohan *johan = dynamic_cast(interpolator)) { + johan->setBeta(interpolator_beta); + } + if (Geom::Interpolate::CubicBezierSmooth *smooth = dynamic_cast(interpolator)) { + smooth->setBeta(interpolator_beta); + } + if (pathv[0].closed()) { + std::vector ts_close; + //we have only one knot or overwrite before + Geom::Point start = Geom::Point( pwd2_in.domain().min(), ts.front()[Geom::Y]); + Geom::Point end = Geom::Point( pwd2_in.domain().max(), ts.front()[Geom::Y]); + if (ts.size() > 1) { + end = Geom::Point(pwd2_in.domain().max(), 0); + Geom::Point tmpstart(0, 0); + tmpstart[Geom::X] = end[Geom::X] + ts.front()[Geom::X]; + tmpstart[Geom::Y] = ts.front()[Geom::Y]; + ts_close.push_back(ts.back()); + ts_close.push_back(middle_point(tmpstart, ts.back())); + ts_close.push_back(tmpstart); + Geom::Path closepath = interpolator->interpolateToPath(ts_close); + end = closepath.pointAt(Geom::nearest_time(end, closepath)); + end[Geom::X] = pwd2_in.domain().max(); + start = end; + start[Geom::X] = pwd2_in.domain().min(); + } + ts.insert(ts.begin(), start ); + ts.push_back( end ); + ts_close.clear(); + } else { + // add width data for first and last point on the path + // depending on cap type, these first and last points have width zero or take the width from the closest width point. + ts.insert(ts.begin(), Point( pwd2_in.domain().min(), + (start_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.front()[Geom::Y]) ); + ts.emplace_back( pwd2_in.domain().max(), + (end_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.back()[Geom::Y] ); + } + + // do the interpolation in a coordinate system that is more alike to the on-canvas knots, + // instead of the heavily compressed coordinate system of (segment_no offset, Y) in which the knots are stored + double pwd2_in_arclength = length(pwd2_in); + double xcoord_scaling = pwd2_in_arclength / ts.back()[Geom::X]; + for (auto & t : ts) { + t[Geom::X] *= xcoord_scaling; + } + + Geom::Path strokepath = interpolator->interpolateToPath(ts); + delete interpolator; + + // apply the inverse knot-xcoord scaling that was applied before the interpolation + strokepath *= Scale(1/xcoord_scaling, 1); + + D2 > patternd2 = make_cuts_independent(strokepath.toPwSb()); + Piecewise x = Piecewise(patternd2[0]); + Piecewise y = Piecewise(patternd2[1]); + // find time values for which x lies outside path domain + // and only take portion of x and y that lies within those time values + std::vector< double > rtsmin = roots (x - pwd2_in.domain().min()); + std::vector< double > rtsmax = roots (x + pwd2_in.domain().max()); + if ( !rtsmin.empty() && !rtsmax.empty() ) { + x = portion(x, rtsmin.at(0), rtsmax.at(0)); + y = portion(y, rtsmin.at(0), rtsmax.at(0)); + } + + LineJoinType jointype = static_cast(linejoin_type.get_value()); + if (x.empty() || y.empty()) { + return path_in; + } + Piecewise > pwd2_out = compose(pwd2_in,x) + y*compose(n,x); + Piecewise > mirrorpath = reverse( compose(pwd2_in,x) - y*compose(n,x)); + + Geom::Path fixed_path = path_from_piecewise_fix_cusps( pwd2_out, y, jointype, miter_limit, LPE_CONVERSION_TOLERANCE); + Geom::Path fixed_mirrorpath = path_from_piecewise_fix_cusps( mirrorpath, reverse(y), jointype, miter_limit, LPE_CONVERSION_TOLERANCE); + if (pathv[0].closed()) { + fixed_path.close(true); + path_out.push_back(fixed_path); + fixed_mirrorpath.close(true); + path_out.push_back(fixed_mirrorpath); + } else { + // add linecaps... + switch (end_linecap) { + case LINECAP_ZERO_WIDTH: + // do nothing + break; + case LINECAP_PEAK: + { + Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.); + double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + Geom::Point midpoint = 0.5*(fixed_path.finalPoint() + fixed_mirrorpath.initialPoint()) + radius*end_deriv; + fixed_path.appendNew(midpoint); + fixed_path.appendNew(fixed_mirrorpath.initialPoint()); + break; + } + case LINECAP_SQUARE: + { + Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.); + double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + fixed_path.appendNew( fixed_path.finalPoint() + radius*end_deriv ); + fixed_path.appendNew( fixed_mirrorpath.initialPoint() + radius*end_deriv ); + fixed_path.appendNew( fixed_mirrorpath.initialPoint() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew( fixed_mirrorpath.initialPoint() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius1 = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + fixed_path.appendNew( radius1, radius1, M_PI/2., false, y.lastValue() < 0, fixed_mirrorpath.initialPoint() ); + break; + } + } + + fixed_path.append(fixed_mirrorpath); + switch (start_linecap) { + case LINECAP_ZERO_WIDTH: + // do nothing + break; + case LINECAP_PEAK: + { + Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.); + double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + Geom::Point midpoint = 0.5*(fixed_mirrorpath.finalPoint() + fixed_path.initialPoint()) - radius*start_deriv; + fixed_path.appendNew( midpoint ); + fixed_path.appendNew( fixed_path.initialPoint() ); + break; + } + case LINECAP_SQUARE: + { + Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.); + double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + fixed_path.appendNew( fixed_mirrorpath.finalPoint() - radius*start_deriv ); + fixed_path.appendNew( fixed_path.initialPoint() - radius*start_deriv ); + fixed_path.appendNew( fixed_path.initialPoint() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew( fixed_path.initialPoint() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius2 = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + fixed_path.appendNew( radius2, radius2, M_PI/2., false, y.firstValue() < 0, fixed_path.initialPoint() ); + break; + } + } + fixed_path.close(true); + path_out.push_back(fixed_path); + } + if (path_out.empty()) { + return path_in; + // doEffect_path (path_in); + } + return path_out; +} + +void LPEPowerStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + offset_points.param_transform_multiply(postmul, false); +} + +void LPEPowerStroke::doAfterEffect(SPLPEItem const *lpeitem) +{ + if (pathvector_before_effect[0].size() == pathvector_after_effect[0].size()) { + if (recusion_limit < 6) { + Inkscape::LivePathEffect::Effect *effect = + sp_lpe_item->getPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY); + if (effect) { + LivePathEffect::LPESimplify *simplify = + dynamic_cast(effect->getLPEObj()->get_lpe()); + double threshold = simplify->threshold * 1.2; + simplify->threshold.param_set_value(threshold); + simplify->threshold.write_to_SVG(); + has_recursion = true; + } + } + ++recusion_limit; + } else { + recusion_limit = 0; + } +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-powerstroke.h b/src/live_effects/lpe-powerstroke.h new file mode 100644 index 0000000..6ac1496 --- /dev/null +++ b/src/live_effects/lpe-powerstroke.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief PowerStroke LPE effect, see lpe-powerstroke.cpp. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2010-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_POWERSTROKE_H +#define INKSCAPE_LPE_POWERSTROKE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/powerstrokepointarray.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum LineCapType { LINECAP_BUTT, LINECAP_SQUARE, LINECAP_ROUND, LINECAP_PEAK, LINECAP_ZERO_WIDTH }; + +static const Util::EnumData LineCapTypeData[] = { { LINECAP_BUTT, N_("Butt"), "butt" }, + { LINECAP_SQUARE, N_("Square"), "square" }, + { LINECAP_ROUND, N_("Round"), "round" }, + { LINECAP_PEAK, N_("Peak"), "peak" }, + { LINECAP_ZERO_WIDTH, N_("Zero width"), "zerowidth" } }; +static const Util::EnumDataConverter LineCapTypeConverter(LineCapTypeData, + sizeof(LineCapTypeData) / sizeof(*LineCapTypeData)); + +class LPEPowerStroke : public Effect { +public: + LPEPowerStroke(LivePathEffectObject *lpeobject); + ~LPEPowerStroke() override; + LPEPowerStroke(const LPEPowerStroke&) = delete; + LPEPowerStroke& operator=(const LPEPowerStroke&) = delete; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doBeforeEffect(SPLPEItem const *lpeItem) override; + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + void doAfterEffect(SPLPEItem const *lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void applyStyle(SPLPEItem *lpeitem); + // methods called by path-manipulator upon edits + void adjustForNewPath(Geom::PathVector const & path_in); + + PowerStrokePointArrayParam offset_points; +private: + BoolParam sort_points; + EnumParam interpolator_type; + ScalarParam interpolator_beta; + ScalarParam scale_width; + EnumParam start_linecap_type; + EnumParam linejoin_type; + ScalarParam miter_limit; + EnumParam end_linecap_type; + size_t recusion_limit; + bool has_recursion; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-pts2ellipse.cpp b/src/live_effects/lpe-pts2ellipse.cpp new file mode 100644 index 0000000..c657c18 --- /dev/null +++ b/src/live_effects/lpe-pts2ellipse.cpp @@ -0,0 +1,772 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Points to Ellipse" implementation + */ + +/* + * Authors: + * Markus Schwienbacher + * + * Copyright (C) Markus Schwienbacher 2013 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-pts2ellipse.h" + + +#include +#include +#include +#include +#include + +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> + +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData EllipseMethodData[] = { + { EM_AUTO, N_("Auto ellipse"), "auto" }, //!< (2..4 points: circle, from 5 points: ellipse) + { EM_CIRCLE, N_("Force circle"), "circle" }, //!< always fit a circle + { EM_ISOMETRIC_CIRCLE, N_("Isometric circle"), "iso_circle" }, //!< use first two edges to generate a sheared + //!< ellipse + { EM_PERSPECTIVE_CIRCLE, N_("Perspective circle"), "perspective_circle" }, //!< use first three edges to generate an + //!< ellipse representing a distorted + //!< circle in perspective + { EM_STEINER_ELLIPSE, N_("Steiner ellipse"), "steiner_ellipse" }, //!< generate a steiner ellipse from the first + //!< three points + { EM_STEINER_INELLIPSE, N_("Steiner inellipse"), "steiner_inellipse" } //!< generate a steiner inellipse from the + //!< first three points +}; +static const Util::EnumDataConverter EMConverter(EllipseMethodData, EM_END); + +LPEPts2Ellipse::LPEPts2Ellipse(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , method( + _("Method:"), + _("Methods to generate the ellipse\n- Auto ellipse: fits a circle (2..4 points) or an ellipse (at least 5 " + "points)\n- Force circle: (at least 2 points) always fit to a circle\n- Isometric circle: (3 points) use " + "first two edges\n- Perspective circle: (4 points) circle in a square in perspective view\n- Steiner " + "ellipse: (3 points) ellipse on a triangle\n- Steiner inellipse: (3 points) ellipse inside a triangle"), + "method", EMConverter, &wr, this, EM_AUTO) + , gen_isometric_frame(_("_Frame (isometric rectangle)"), _("Draw parallelogram around the ellipse"), + "gen_isometric_frame", &wr, this, false) + , gen_perspective_frame( + _("_Perspective square"), + _("Draw square surrounding the circle in perspective view\n(only in method \"Perspective circle\")"), + "gen_perspective_frame", &wr, this, false) + , gen_arc(_("_Arc"), + _("Generate open arc (open ellipse) based on first and last point\n(only for methods \"Auto ellipse\" " + "and \"Force circle\")"), + "gen_arc", &wr, this, false) + , other_arc(_("_Other arc side"), _("Switch sides of the arc"), "arc_other", &wr, this, false) + , slice_arc(_("_Slice arc"), _("Slice the arc"), "slice_arc", &wr, this, false) + , draw_axes(_("A_xes"), _("Draw both semi-major and semi-minor axes"), "draw_axes", &wr, this, false) + , draw_perspective_axes(_("Perspective axes"), + _("Draw the axes in perspective view\n(only in method \"Perspective circle\")"), + "draw_perspective_axes", &wr, this, false) + , rot_axes(_("Axes rotation"), _("Axes rotation angle [deg]"), "rot_axes", &wr, this, 0) + , draw_ori_path(_("Source _path"), _("Show the original source path"), "draw_ori_path", &wr, this, false) +{ + registerParameter(&method); + registerParameter(&gen_arc); + registerParameter(&other_arc); + registerParameter(&slice_arc); + registerParameter(&gen_isometric_frame); + registerParameter(&draw_axes); + registerParameter(&gen_perspective_frame); + registerParameter(&draw_perspective_axes); + registerParameter(&rot_axes); + registerParameter(&draw_ori_path); + + rot_axes.param_set_range(-360, 360); + rot_axes.param_set_increments(1, 10); + + show_orig_path = true; + + gsl_x = gsl_vector_alloc(8); + gsl_p = gsl_permutation_alloc(8); +} + +LPEPts2Ellipse::~LPEPts2Ellipse() +{ + gsl_permutation_free(gsl_p); + gsl_vector_free(gsl_x); +} + +// helper function, transforms a given value into range [0, 2pi] +inline double range2pi(double a) +{ + a = fmod(a, 2 * M_PI); + if (a < 0) { + a += 2 * M_PI; + } + return a; +} + +inline double deg2rad(double a) { return a * M_PI / 180.0; } + +inline double rad2deg(double a) { return a * 180.0 / M_PI; } + +// helper function, calculates the angle between a0 and a1 in ccw sense +// examples: 0..1->1, -1..1->2, pi/4..-pi/4->1.5pi +// full rotations: 0..2pi->2pi, -pi..pi->2pi, pi..-pi->0, 2pi..0->0 +inline double calc_delta_angle(const double a0, const double a1) +{ + double da = range2pi(a1 - a0); + if ((fabs(da) < 1e-9) && (a0 < a1)) { + da = 2 * M_PI; + } + return da; +} + +int LPEPts2Ellipse::unit_arc_path(Geom::Path &path_in, Geom::Affine &affine, double start, double end, bool slice) +{ + double arc_angle = calc_delta_angle(start, end); + if (fabs(arc_angle) < 1e-9) { + g_warning("angle was 0"); + return -1; + } + + // the delta angle + double da = M_PI_2; + // number of segments with da length + int nda = (int)ceil(arc_angle / M_PI_2); + // recalculate da + da = arc_angle / (double)nda; + + bool closed = false; + if (fabs(arc_angle - 2 * M_PI) < 1e-8) { + closed = true; + da = M_PI_2; + nda = 4; + } + + double s = range2pi(start); + end = s + arc_angle; + + double x0 = cos(s); + double y0 = sin(s); + // construct the path + Geom::Path path(Geom::Point(x0, y0)); + path.setStitching(true); + for (int i = 0; i < nda;) { + double e = s + da; + if (e > end) { + e = end; + } + const double len = 4 * tan((e - s) / 4) / 3; + const double x1 = x0 + len * cos(s + M_PI_2); + const double y1 = y0 + len * sin(s + M_PI_2); + const double x3 = cos(e); + const double y3 = sin(e); + const double x2 = x3 + len * cos(e - M_PI_2); + const double y2 = y3 + len * sin(e - M_PI_2); + path.appendNew(Geom::Point(x1, y1), Geom::Point(x2, y2), Geom::Point(x3, y3)); + s = (++i) * da + start; + x0 = cos(s); + y0 = sin(s); + } + + if (slice && !closed) { + path.appendNew(Geom::Point(0.0, 0.0)); + } + path *= affine; + + path_in.append(path); + if ((slice && !closed) || closed) { + path_in.close(true); + } + return 0; +} + +void LPEPts2Ellipse::gen_iso_frame_paths(Geom::PathVector &path_out, const Geom::Affine &affine) +{ + Geom::Path rect(Geom::Point(-1, -1)); + rect.setStitching(true); + rect.appendNew(Geom::Point(+1, -1)); + rect.appendNew(Geom::Point(+1, +1)); + rect.appendNew(Geom::Point(-1, +1)); + rect *= affine; + rect.close(true); + path_out.push_back(rect); +} + +void LPEPts2Ellipse::gen_perspective_frame_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]) +{ + Geom::Point pts0[4] = { { -1.0, -1.0 }, { +1.0, -1.0 }, { +1.0, +1.0 }, { -1.0, +1.0 } }; + // five_pts.resize(4); + int h = 0; + Geom::Affine affine2; + // const double rot_angle = deg2rad(rot_axes); // negative for ccw rotation + affine2 *= Geom::Rotate(-rot_angle); + for (auto &i : pts0) { + Geom::Point point = i; + point *= affine2; + i = projectPoint(point, projmatrix); + } + + Geom::Path rect(pts0[0]); + rect.setStitching(true); + for (int i = 1; i < 4; i++) + rect.appendNew(pts0[i]); + rect.close(true); + path_out.push_back(rect); +} + +void LPEPts2Ellipse::gen_axes_paths(Geom::PathVector &path_out, const Geom::Affine &affine) +{ + Geom::LineSegment clx(Geom::Point(-1, 0), Geom::Point(1, 0)); + Geom::LineSegment cly(Geom::Point(0, -1), Geom::Point(0, 1)); + + Geom::Path plx, ply; + plx.append(clx); + ply.append(cly); + plx *= affine; + ply *= affine; + + path_out.push_back(plx); + path_out.push_back(ply); +} + +void LPEPts2Ellipse::gen_perspective_axes_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]) +{ + Geom::Point pts[4]; + int h = 0; + double dA = 2.0 * M_PI / 4.0; // delta Angle + for (auto &i : pts) { + const double angle = rot_angle + dA * h++; + const Geom::Point circle_point(sin(angle), cos(angle)); + i = projectPoint(circle_point, projmatrix); + } + { + Geom::LineSegment clx(pts[0], pts[2]); + Geom::LineSegment cly(pts[1], pts[3]); + + Geom::Path plx, ply; + plx.append(clx); + ply.append(cly); + + path_out.push_back(plx); + path_out.push_back(ply); + } +} + +bool LPEPts2Ellipse::is_ccw(const std::vector &pts) +{ + // method: sum up the angles between edges + size_t n = pts.size(); + // edges about vertex 0 + Geom::Point e0(pts.front() - pts.back()); + Geom::Point e1(pts[1] - pts[0]); + Geom::Coord sum = cross(e0, e1); + // the rest + for (size_t i = 1; i < n; i++) { + e0 = e1; + e1 = pts[i] - pts[i - 1]; + sum += cross(e0, e1); + } + // edges about last vertex (closing) + e0 = e1; + e1 = pts.front() - pts.back(); + sum += cross(e0, e1); + + // close the + if (sum < 0) { + return true; + } else { + return false; + } +} + +void endpoints2angles(const bool ccw_wind, const bool use_other_arc, const Geom::Point &p0, const Geom::Point &p1, + Geom::Coord &a0, Geom::Coord &a1) +{ + if (!p0.isZero() && !p1.isZero()) { + a0 = atan2(p0); + a1 = atan2(p1); + if (!ccw_wind) { + std::swap(a0, a1); + } + if (!use_other_arc) { + std::swap(a0, a1); + } + } +} + +/** + * Generates an ellipse (or circle) from the vertices of a given path. Thereby, using fitting + * algorithms from 2geom. Depending on the settings made by the user regarding things like arc, + * slice, circle etc. the final result will be different + */ +Geom::PathVector LPEPts2Ellipse::doEffect_path(Geom::PathVector const &path_in) +{ + Geom::PathVector path_out; + + // 1) draw original path? + if (draw_ori_path.get_value()) { + path_out.insert(path_out.end(), path_in.begin(), path_in.end()); + } + + + // 2) get all points + // (from: extension/internal/odf.cpp) + points.resize(0); + for (const auto &pit : path_in) { + // extract first point of this path + points.push_back(pit.initialPoint()); + // iterate over all curves + for (const auto &cit : pit) { + points.push_back(cit.finalPoint()); + } + } + // avoid identical start-point and end-point + if (points.front() == points.back()) { + points.pop_back(); + } + + // 3) modify GUI based on selected method + // 3.1) arc options + switch (method) { + case EM_AUTO: + case EM_CIRCLE: + gen_arc.param_widget_is_enabled(true); + if (gen_arc.get_value()) { + slice_arc.param_widget_is_enabled(true); + other_arc.param_widget_is_enabled(true); + } else { + other_arc.param_widget_is_enabled(false); + slice_arc.param_widget_is_enabled(false); + } + break; + default: + gen_arc.param_widget_is_enabled(false); + other_arc.param_widget_is_enabled(false); + slice_arc.param_widget_is_enabled(false); + } + // 3.2) perspective options + switch (method) { + case EM_PERSPECTIVE_CIRCLE: + gen_perspective_frame.param_widget_is_enabled(true); + draw_perspective_axes.param_widget_is_enabled(true); + break; + default: + gen_perspective_frame.param_widget_is_enabled(false); + draw_perspective_axes.param_widget_is_enabled(false); + } + + // 4) call method specific code + switch (method) { + case EM_ISOMETRIC_CIRCLE: + // special mode: Use first two edges, interpret them as two sides of a parallelogram and + // generate an ellipse residing inside the parallelogram. This effect is quite useful when + // generating isometric views. Hence, the name. + if (0 != genIsometricEllipse(points, path_out)) { + return path_in; + } + break; + case EM_PERSPECTIVE_CIRCLE: + // special mode: Use first four points, interpret them as the perspective representation of a square and + // draw the ellipse as it was a circle inside that square. + if (0 != genPerspectiveEllipse(points, path_out)) { + return path_in; + } + break; + case EM_STEINER_ELLIPSE: + if (0 != genSteinerEllipse(points, false, path_out)) { + return path_in; + } + break; + case EM_STEINER_INELLIPSE: + if (0 != genSteinerEllipse(points, true, path_out)) { + return path_in; + } + break; + default: + if (0 != genFitEllipse(points, path_out)) { + return path_in; + } + } + return path_out; +} + +/** + * Generates an ellipse (or circle) from the vertices of a given path. Thereby, using fitting + * algorithms from 2geom. Depending on the settings made by the user regarding things like arc, + * slice, circle etc. the final result will be different. We need at least 5 points to fit an + * ellipse. With 5 points each point is on the ellipse. For less points we get a circle. + */ +int LPEPts2Ellipse::genFitEllipse(std::vector const &pts, Geom::PathVector &path_out) +{ + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + Geom::Coord a0 = 0; + Geom::Coord a1 = 2 * M_PI; + + if (pts.size() < 2) { + return -1; + } else if (pts.size() == 2) { + // simple line: circle in the middle of the line to the vertices + Geom::Point line = pts.front() - pts.back(); + double radius = line.length() * 0.5; + if (radius < 1e-9) { + return -1; + } + Geom::Point center = middle_point(pts.front(), pts.back()); + Geom::Circle circle(center[0], center[1], radius); + affine *= Geom::Scale(circle.radius()); + affine *= Geom::Translate(circle.center()); + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + } else if (pts.size() >= 5 && EM_AUTO == method) { + // do ellipse + try { + Geom::Ellipse ellipse; + ellipse.fit(pts); + affine *= Geom::Scale(ellipse.ray(Geom::X), ellipse.ray(Geom::Y)); + affine *= Geom::Rotate(ellipse.rotationAngle()); + affine *= Geom::Translate(ellipse.center()); + if (gen_arc.get_value()) { + Geom::Affine inv_affine = affine.inverse(); + Geom::Point p0 = pts.front() * inv_affine; + Geom::Point p1 = pts.back() * inv_affine; + const bool ccw_wind = is_ccw(pts); + endpoints2angles(ccw_wind, other_arc.get_value(), p0, p1, a0, a1); + } + Geom::Path path; + unit_arc_path(path, affine, a0, a1, slice_arc.get_value()); + path_out.push_back(path); + } catch (...) { + return -1; + } + } else { + // do a circle (3,4 points, or only_circle set) + try { + Geom::Circle circle; + circle.fit(pts); + affine *= Geom::Scale(circle.radius()); + affine *= Geom::Translate(circle.center()); + if (gen_arc.get_value()) { + Geom::Point p0 = pts.front() - circle.center(); + Geom::Point p1 = pts.back() - circle.center(); + const bool ccw_wind = is_ccw(pts); + endpoints2angles(ccw_wind, other_arc.get_value(), p0, p1, a0, a1); + } + Geom::Path path; + unit_arc_path(path, affine, a0, a1, slice_arc.get_value()); + path_out.push_back(path); + } catch (...) { + return -1; + } + } + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +int LPEPts2Ellipse::genIsometricEllipse(std::vector const &pts, Geom::PathVector &path_out) + +{ + // take the first 3 vertices for the edges + if (pts.size() < 3) { + return -1; + } + // calc edges + Geom::Point e0 = pts[0] - pts[1]; + Geom::Point e1 = pts[2] - pts[1]; + + Geom::Coord ce = cross(e0, e1); + // parallel or one is zero? + if (fabs(ce) < 1e-9) { + return -1; + } + // unit vectors along edges + Geom::Point u0 = unit_vector(e0); + Geom::Point u1 = unit_vector(e1); + // calc angles + Geom::Coord a0 = atan2(e0); + // Coord a1=M_PI_2-atan2(e1)-a0; + Geom::Coord a1 = acos(dot(u0, u1)) - M_PI_2; + // if(fabs(a1)<1e-9) return -1; + if (ce < 0) { + a1 = -a1; + } + // lengths: l0= length of edge 0; l1= height of parallelogram + Geom::Coord l0 = e0.length() * 0.5; + Geom::Point e0n = e1 - dot(u0, e1) * u0; + Geom::Coord l1 = e0n.length() * 0.5; + + // center of the ellipse + Geom::Point pos = pts[1] + 0.5 * (e0 + e1); + + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + + // build up the affine transformation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + affine *= Geom::Scale(l0, l1); + affine *= Geom::HShear(-tan(a1)); + affine *= Geom::Rotate(a0); + affine *= Geom::Translate(pos); + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +void evalSteinerEllipse(Geom::Point const &pCenter, Geom::Point const &pCenter_Pt2, Geom::Point const &pPt0_Pt1, + const double &angle, Geom::Point &pRes) +{ + // formula for the evaluation of points on the steiner ellipse using parameter angle + pRes = pCenter + pCenter_Pt2 * cos(angle) + pPt0_Pt1 * sin(angle) / sqrt(3); +} + +int LPEPts2Ellipse::genSteinerEllipse(std::vector const &pts, bool gen_inellipse, + Geom::PathVector &path_out) +{ + // take the first 3 vertices for the edges + if (pts.size() < 3) { + return -1; + } + // calc center + Geom::Point pCenter = (pts[0] + pts[1] + pts[2]) / 3; + // calc main directions of affine triangle + Geom::Point f1 = pts[2] - pCenter; + Geom::Point f2 = (pts[1] - pts[0]) / sqrt(3); + + // calc zero angle t0 + const double denominator = dot(f1, f1) - dot(f2, f2); + double t0 = 0; + if (fabs(denominator) > 1e-12) { + const double cot2t0 = 2.0 * dot(f1, f2) / denominator; + t0 = atan(cot2t0) / 2.0; + } + + // calc relative points of main axes (for axis directions) + Geom::Point p0(0, 0), pRel0, pRel1; + evalSteinerEllipse(p0, pts[2] - pCenter, pts[1] - pts[0], t0, pRel0); + evalSteinerEllipse(p0, pts[2] - pCenter, pts[1] - pts[0], t0 + M_PI_2, pRel1); + Geom::Coord l0 = pRel0.length(); + Geom::Coord l1 = pRel1.length(); + + // basic rotation + double a0 = atan2(pRel0); + + bool swapped = false; + + if (l1 > l0) { + std::swap(l0, l1); + a0 += M_PI_2; + swapped = true; + } + + // the Steiner inellipse is just scaled down by 2 + if (gen_inellipse) { + l0 /= 2; + l1 /= 2; + } + + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + + // build up the affine transformation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + affine *= Geom::Scale(l0, l1); + affine *= Geom::Rotate(a0); + affine *= Geom::Translate(pCenter); + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +// identical to lpe-perspective-envelope.cpp +Geom::Point LPEPts2Ellipse::projectPoint(Geom::Point p, double m[][3]) +{ + Geom::Coord x = p[0]; + Geom::Coord y = p[1]; + return Geom::Point(Geom::Coord((x * m[0][0] + y * m[0][1] + m[0][2]) / (x * m[2][0] + y * m[2][1] + m[2][2])), + Geom::Coord((x * m[1][0] + y * m[1][1] + m[1][2]) / (x * m[2][0] + y * m[2][1] + m[2][2]))); +} + +int LPEPts2Ellipse::genPerspectiveEllipse(std::vector const &pts, Geom::PathVector &path_out) +{ + using Geom::X; + using Geom::Y; + // we need at least four points! + if (pts.size() < 4) + return -1; + + // 1) check if the first three edges are a valid perspective + // calc edge + Geom::Point e[] = { pts[0] - pts[1], pts[1] - pts[2], pts[2] - pts[3], pts[3] - pts[0] }; + // calc directions + Geom::Coord c[] = { cross(e[0], e[1]), cross(e[1], e[2]), cross(e[2], e[3]), cross(e[3], e[0]) }; + // is this quad not convex? + if (!((c[0] > 0 && c[1] > 0 && c[2] > 0 && c[3] > 0) || (c[0] < 0 && c[1] < 0 && c[2] < 0 && c[3] < 0))) + return -1; + + // 2) solve the direct linear transformation (see e.g. lpe-perspective-envelope.cpp or + // https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/) + + // the square points in the initial configuration (about the unit circle): + Geom::Point pts0[4] = { { -1.0, -1.0 }, { +1.0, -1.0 }, { +1.0, +1.0 }, { -1.0, +1.0 } }; + + // build equation in matrix form + double eqnVec[8] = { 0 }; + double eqnMat[64] = { 0 }; + for (unsigned int i = 0; i < 4; ++i) { + eqnMat[8 * (i + 0) + 0] = pts0[i][X]; + eqnMat[8 * (i + 0) + 1] = pts0[i][Y]; + eqnMat[8 * (i + 0) + 2] = 1; + eqnMat[8 * (i + 0) + 6] = -pts[i][X] * pts0[i][X]; + eqnMat[8 * (i + 0) + 7] = -pts[i][X] * pts0[i][Y]; + eqnMat[8 * (i + 4) + 3] = pts0[i][X]; + eqnMat[8 * (i + 4) + 4] = pts0[i][Y]; + eqnMat[8 * (i + 4) + 5] = 1; + eqnMat[8 * (i + 4) + 6] = -pts[i][Y] * pts0[i][X]; + eqnMat[8 * (i + 4) + 7] = -pts[i][Y] * pts0[i][Y]; + eqnVec[i] = pts[i][X]; + eqnVec[i + 4] = pts[i][Y]; + } + // solve using gsl library + gsl_matrix_view m = gsl_matrix_view_array(eqnMat, 8, 8); + gsl_vector_view b = gsl_vector_view_array(eqnVec, 8); + int s = 0; + gsl_linalg_LU_decomp(&m.matrix, gsl_p, &s); + gsl_linalg_LU_solve(&m.matrix, gsl_p, &b.vector, gsl_x); + // transfer the solution to the projection matrix for further use + size_t h = 0; + double projmatrix[3][3]; + for (auto &matRow : projmatrix) { + for (double &matElement : matRow) { + if (h == 8) { + projmatrix[2][2] = 1.0; + } else { + matElement = gsl_vector_get(gsl_x, h++); + } + } + } + + // 3) generate five points on a unit circle and project them + five_pts.resize(5); // reuse and avoid new/delete + h = 0; + double dA = 2.0 * M_PI / 5.0; // delta Angle + for (auto &i : five_pts) { + const double angle = dA * h++; + const Geom::Point circle_point(sin(angle), cos(angle)); + i = projectPoint(circle_point, projmatrix); + } + + // 4) fit the five points to an ellipse with the already known function inside genFitEllipse() function + // build up the affine transformation + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + + try { + Geom::Ellipse ellipse; + ellipse.fit(five_pts); + affine *= Geom::Scale(ellipse.ray(Geom::X), ellipse.ray(Geom::Y)); + affine *= Geom::Rotate(ellipse.rotationAngle()); + affine *= Geom::Translate(ellipse.center()); + } catch (...) { + return -1; + } + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // 5) frames and axes + bool ccw_wind = false; + if (gen_perspective_frame.get_value() || draw_perspective_axes.get_value()) + ccw_wind = is_ccw(pts); + const double ra = ccw_wind ? rot_angle : -rot_angle; + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw perspective frame? + if (gen_perspective_frame.get_value()) { + gen_perspective_frame_paths(path_out, ra, projmatrix); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + // draw perspective axes? + if (draw_perspective_axes.get_value()) { + gen_perspective_axes_paths(path_out, ra, projmatrix); + } + + return 0; +} + + +/* ######################## */ + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-pts2ellipse.h b/src/live_effects/lpe-pts2ellipse.h new file mode 100644 index 0000000..3ccf0c4 --- /dev/null +++ b/src/live_effects/lpe-pts2ellipse.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PTS_TO_ELLIPSE_H +#define INKSCAPE_LPE_PTS_TO_ELLIPSE_H + +/** \file + * LPE "Points to Ellipse" implementation + */ + +/* + * Authors: + * Markus Schwienbacher + * + * Copyright (C) Markus Schwienbacher 2013 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" + +#include + + +// struct gsl_vector; +// struct gsl_permutation; + +namespace Inkscape { +namespace LivePathEffect { + +enum EllipseMethod { + EM_AUTO, + EM_CIRCLE, + EM_ISOMETRIC_CIRCLE, + EM_PERSPECTIVE_CIRCLE, + EM_STEINER_ELLIPSE, + EM_STEINER_INELLIPSE, + EM_END +}; + +class LPEPts2Ellipse : public Effect { + public: + LPEPts2Ellipse(LivePathEffectObject *lpeobject); + ~LPEPts2Ellipse() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + + private: + LPEPts2Ellipse(const LPEPts2Ellipse &) = delete; + LPEPts2Ellipse &operator=(const LPEPts2Ellipse &) = delete; + + + int genIsometricEllipse(std::vector const &points_in, Geom::PathVector &path_out); + + int genFitEllipse(std::vector const &points_in, Geom::PathVector &path_out); + + int genSteinerEllipse(std::vector const &points_in, bool gen_inellipse, Geom::PathVector &path_out); + + int genPerspectiveEllipse(std::vector const &points_in, Geom::PathVector &path_out); + + // utility functions + static int unit_arc_path(Geom::Path &path_in, Geom::Affine &affine, double start = 0.0, + double end = 2.0 * M_PI, // angles + bool slice = false); + static void gen_iso_frame_paths(Geom::PathVector &path_out, const Geom::Affine &affine); + static void gen_perspective_frame_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]); + static void gen_axes_paths(Geom::PathVector &path_out, const Geom::Affine &affine); + static void gen_perspective_axes_paths(Geom::PathVector &path_out, const double rot_angle, double projmatrix[3][3]); + static bool is_ccw(const std::vector &pts); + static Geom::Point projectPoint(Geom::Point p, double m[][3]); + + // GUI parameters + EnumParam method; + BoolParam gen_isometric_frame; + BoolParam gen_perspective_frame; + BoolParam gen_arc; + BoolParam other_arc; + BoolParam slice_arc; + BoolParam draw_axes; + BoolParam draw_perspective_axes; + ScalarParam rot_axes; + BoolParam draw_ori_path; + + // collect the points from the input paths + std::vector points; + + // used for solving perspective circle + gsl_vector *gsl_x; + gsl_permutation *gsl_p; + std::vector five_pts; +}; + +} // namespace LivePathEffect +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-recursiveskeleton.cpp b/src/live_effects/lpe-recursiveskeleton.cpp new file mode 100644 index 0000000..0a67cdb --- /dev/null +++ b/src/live_effects/lpe-recursiveskeleton.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inspired by Hofstadter's 'Goedel Escher Bach', chapter V. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-recursiveskeleton.h" + +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPERecursiveSkeleton::LPERecursiveSkeleton(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + iterations(_("Iterations:"), _("recursivity"), "iterations", &wr, this, 2) +{ + show_orig_path = true; + concatenate_before_pwd2 = true; + iterations.param_make_integer(true); + iterations.param_set_range(1, 15); + registerParameter(&iterations); + +} + +LPERecursiveSkeleton::~LPERecursiveSkeleton() += default; + + +Geom::Piecewise > +LPERecursiveSkeleton::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + Piecewise > output; + double prop_scale = 1.0; + + D2 > patternd2 = make_cuts_independent(pwd2_in); + Piecewise x0 = false /*vertical_pattern.get_value()*/ ? Piecewise(patternd2[1]) : Piecewise(patternd2[0]); + Piecewise y0 = false /*vertical_pattern.get_value()*/ ? Piecewise(patternd2[0]) : Piecewise(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + + if ( !pattBndsX || !pattBndsY) { + return pwd2_in; + } + + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double noffset = 0;//normal_offset; + double toffset = 0;//tang_offset; + if (false /*prop_units.get_value()*/){ + noffset *= pattBndsY->extent(); + toffset *= pattBndsX->extent(); + } + + y0+=noffset; + + output = pwd2_in; + + for (int i = 0; i < iterations; ++i) { + std::vector > > skeleton = split_at_discontinuities(output); + + output.clear(); + for (auto path_i : skeleton){ + Piecewise x = x0; + Piecewise y = y0; + Piecewise > uskeleton = arc_length_parametrization(path_i,2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + Piecewise > n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.1)); + + double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + + // TODO investigate why pattWidth is not being used: + // - Doesn't appear to have been used anywhere in bzr history (Alex V: 2013-03-16) + // double pattWidth = pattBndsX->extent() * scaling; + + if (scaling != 1.0) { + x*=scaling; + } + + if ( true /*scale_y_rel.get_value()*/ ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + x += toffset; + + output.concat(compose(uskeleton,x)+y*compose(n,x)); + } + } + + return output; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-recursiveskeleton.h b/src/live_effects/lpe-recursiveskeleton.h new file mode 100644 index 0000000..1347bef --- /dev/null +++ b/src/live_effects/lpe-recursiveskeleton.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief see lpe-recursiveskeleton.cpp. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_RECURSIVESKELETON_H +#define INKSCAPE_LPE_RECURSIVESKELETON_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + + +class LPERecursiveSkeleton : public Effect { +public: + LPERecursiveSkeleton(LivePathEffectObject *lpeobject); + ~LPERecursiveSkeleton() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + ScalarParam iterations; + + LPERecursiveSkeleton(const LPERecursiveSkeleton&) = delete; + LPERecursiveSkeleton& operator=(const LPERecursiveSkeleton&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-rough-hatches.cpp b/src/live_effects/lpe-rough-hatches.cpp new file mode 100644 index 0000000..acda1f4 --- /dev/null +++ b/src/live_effects/lpe-rough-hatches.cpp @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Curve Stitching implementation, used as an example for a base starting class + * when implementing new LivePathEffects. + * + */ +/* + * Authors: + * JF Barraud. + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include "live_effects/lpe-rough-hatches.h" + +#include "object/sp-item.h" + +#include "xml/repr.h" + +#include <2geom/sbasis-math.h> +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +//------------------------------------------------ +// Some goodies to navigate through curve's levels. +//------------------------------------------------ +struct LevelCrossing{ + Point pt; + double t; + bool sign; + bool used; + std::pair next_on_curve; + std::pair prev_on_curve; +}; +struct LevelCrossingOrder { + bool operator()(LevelCrossing a, LevelCrossing b) { + return ( a.pt[Y] < b.pt[Y] );// a.pt[X] == b.pt[X] since we are supposed to be on the same level... + //return ( a.pt[X] < b.pt[X] || ( a.pt[X] == b.pt[X] && a.pt[Y] < b.pt[Y] ) ); + } +}; +struct LevelCrossingInfo{ + double t; + unsigned level; + unsigned idx; +}; +struct LevelCrossingInfoOrder { + bool operator()(LevelCrossingInfo a, LevelCrossingInfo b) { + return a.t < b.t; + } +}; + +typedef std::vector LevelCrossings; + +static std::vector +discontinuities(Piecewise > const &f){ + std::vector result; + if (f.size()==0) return result; + result.push_back(f.cuts[0]); + Point prev_pt = f.segs[0].at1(); + //double old_t = f.cuts[0]; + for(unsigned i=1; i{ +public: + LevelsCrossings():std::vector(){}; + LevelsCrossings(std::vector > const ×, + Piecewise > const &f, + Piecewise const &dx){ + + for (const auto & time : times){ + LevelCrossings lcs; + for (double j : time){ + LevelCrossing lc; + lc.pt = f.valueAt(j); + lc.t = j; + lc.sign = ( dx.valueAt(j)>0 ); + lc.used = false; + lcs.push_back(lc); + } + std::sort(lcs.begin(), lcs.end(), LevelCrossingOrder()); + push_back(lcs); + } + //Now create time ordering. + std::vectortemp; + for (unsigned i=0; i jumps = discontinuities(f); + unsigned jump_idx = 0; + unsigned first_in_comp = 0; + for (unsigned i=0; i jumps[jump_idx+1]){ + std::pairnext_data(temp[first_in_comp].level,temp[first_in_comp].idx); + (*this)[lvl][idx].next_on_curve = next_data; + first_in_comp = i+1; + jump_idx += 1; + }else{ + std::pair next_data(temp[i+1].level,temp[i+1].idx); + (*this)[lvl][idx].next_on_curve = next_data; + } + } + + for (unsigned i=0; i next = (*this)[i][j].next_on_curve; + (*this)[next.first][next.second].prev_on_curve = std::pair(i,j); + } + } + } + + void findFirstUnused(unsigned &level, unsigned &idx){ + level = size(); + idx = 0; + for (unsigned i=0; i= (*this)[level].size()-1 || (*this)[level][idx+1].used ) { + level = size(); + return; + } + idx += 1; + }else{ + if ( idx <= 0 || (*this)[level][idx-1].used ) { + level = size(); + return; + } + idx -= 1; + } + direction += 1; + return; + } + //double t = (*this)[level][idx].t; + double sign = ((*this)[level][idx].sign ? 1 : -1); + //---double next_t = t; + //level += 1; + direction = (direction + 1)%4; + if (level == size()){ + return; + } + + std::pair next; + if ( sign > 0 ){ + next = (*this)[level][idx].next_on_curve; + }else{ + next = (*this)[level][idx].prev_on_curve; + } + + if ( level+1 != next.first || (*this)[next.first][next.second].used ) { + level = size(); + return; + } + level = next.first; + idx = next.second; + return; + } +}; + +//------------------------------------------------------- +// Bend a path... +//------------------------------------------------------- + +static Piecewise > bend(Piecewise > const &f, Piecewise bending){ + D2 > ff = make_cuts_independent(f); + ff[X] += compose(bending, ff[Y]); + return sectionize(ff); +} + +//-------------------------------------------------------- +// The RoughHatches lpe. +//-------------------------------------------------------- +LPERoughHatches::LPERoughHatches(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + hatch_dist(0), + dist_rdm(_("Frequency randomness:"), _("Variation of distance between hatches, in %."), "dist_rdm", &wr, this, 75), + growth(_("Growth:"), _("Growth of distance between hatches."), "growth", &wr, this, 0.), +//FIXME: top/bottom names are inverted in the UI/svg and in the code!! + scale_tf(_("Half-turns smoothness: 1st side, in:"), _("Set smoothness/sharpness of path when reaching a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bf", &wr, this, 1.), + scale_tb(_("1st side, out:"), _("Set smoothness/sharpness of path when leaving a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bb", &wr, this, 1.), + scale_bf(_("2nd side, in:"), _("Set smoothness/sharpness of path when reaching a 'top' half-turn. 0=sharp, 1=default"), "scale_tf", &wr, this, 1.), + scale_bb(_("2nd side, out:"), _("Set smoothness/sharpness of path when leaving a 'top' half-turn. 0=sharp, 1=default"), "scale_tb", &wr, this, 1.), + top_edge_variation(_("Magnitude jitter: 1st side:"), _("Randomly moves 'bottom' half-turns to produce magnitude variations."), "bottom_edge_variation", &wr, this, 0), + bot_edge_variation(_("2nd side:"), _("Randomly moves 'top' half-turns to produce magnitude variations."), "top_edge_variation", &wr, this, 0), + top_tgt_variation(_("Parallelism jitter: 1st side:"), _("Add direction randomness by moving 'bottom' half-turns tangentially to the boundary."), "bottom_tgt_variation", &wr, this, 0), + bot_tgt_variation(_("2nd side:"), _("Add direction randomness by randomly moving 'top' half-turns tangentially to the boundary."), "top_tgt_variation", &wr, this, 0), + top_smth_variation(_("Variance: 1st side:"), _("Randomness of 'bottom' half-turns smoothness"), "top_smth_variation", &wr, this, 0), + bot_smth_variation(_("2nd side:"), _("Randomness of 'top' half-turns smoothness"), "bottom_smth_variation", &wr, this, 0), +// + fat_output(_("Generate thick/thin path"), _("Simulate a stroke of varying width"), "fat_output", &wr, this, true), + do_bend(_("Bend hatches"), _("Add a global bend to the hatches (slower)"), "do_bend", &wr, this, true), + stroke_width_top(_("Thickness: at 1st side:"), _("Width at 'bottom' half-turns"), "stroke_width_top", &wr, this, 1.), + stroke_width_bot(_("At 2nd side:"), _("Width at 'top' half-turns"), "stroke_width_bottom", &wr, this, 1.), +// + front_thickness(_("From 2nd to 1st side:"), _("Width from 'top' to 'bottom'"), "front_thickness", &wr, this, 1.), + back_thickness(_("From 1st to 2nd side:"), _("Width from 'bottom' to 'top'"), "back_thickness", &wr, this, .25), + + direction(_("Hatches width and dir"), _("Defines hatches frequency and direction"), "direction", &wr, this, Geom::Point(50,0)), +// + bender(_("Global bending"), _("Relative position to a reference point defines global bending direction and amount"), "bender", &wr, this, Geom::Point(-5,0)) +{ + registerParameter(&direction); + registerParameter(&dist_rdm); + registerParameter(&growth); + registerParameter(&do_bend); + registerParameter(&bender); + registerParameter(&top_edge_variation); + registerParameter(&bot_edge_variation); + registerParameter(&top_tgt_variation); + registerParameter(&bot_tgt_variation); + registerParameter(&scale_tf); + registerParameter(&scale_tb); + registerParameter(&scale_bf); + registerParameter(&scale_bb); + registerParameter(&top_smth_variation); + registerParameter(&bot_smth_variation); + registerParameter(&fat_output); + registerParameter(&stroke_width_top); + registerParameter(&stroke_width_bot); + registerParameter(&front_thickness); + registerParameter(&back_thickness); + + //hatch_dist.param_set_range(0.1, Geom::infinity()); + growth.param_set_range(0, Geom::infinity()); + dist_rdm.param_set_range(0, 99.); + stroke_width_top.param_set_range(0, Geom::infinity()); + stroke_width_bot.param_set_range(0, Geom::infinity()); + front_thickness.param_set_range(0, Geom::infinity()); + back_thickness.param_set_range(0, Geom::infinity()); + + // hide the widgets for direction and bender vectorparams + direction.widget_is_visible = false; + bender.widget_is_visible = false; + // give distinguishing colors to direction and bender on-canvas params + direction.set_oncanvas_color(0x00ff7d00); + bender.set_oncanvas_color(0xffffb500); + + concatenate_before_pwd2 = false; + show_orig_path = true; +} + +LPERoughHatches::~LPERoughHatches() += default; + +Geom::Piecewise > +LPERoughHatches::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in){ + + //std::cout<<"doEffect_pwd2:\n"; + + Piecewise > result; + + Piecewise > transformed_pwd2_in = pwd2_in; + Point start = pwd2_in.segs.front().at0(); + Point end = pwd2_in.segs.back().at1(); + if (end != start ){ + transformed_pwd2_in.push_cut( transformed_pwd2_in.cuts.back() + 1 ); + D2 stitch( SBasis( 1, Linear(end[X],start[X]) ), SBasis( 1, Linear(end[Y],start[Y]) ) ); + transformed_pwd2_in.push_seg( stitch ); + } + Point transformed_org = direction.getOrigin(); + Piecewise tilter;//used to bend the hatches + Affine bend_mat;//used to bend the hatches + + if (do_bend.get_value()){ + Point bend_dir = -rot90(unit_vector(bender.getVector())); + double bend_amount = L2(bender.getVector()); + bend_mat = Affine(-bend_dir[Y], bend_dir[X], bend_dir[X], bend_dir[Y],0,0); + transformed_pwd2_in = transformed_pwd2_in * bend_mat; + tilter = Piecewise(shift(Linear(-bend_amount),1)); + OptRect bbox = bounds_exact( transformed_pwd2_in ); + if (!(bbox)) return pwd2_in; + tilter.setDomain((*bbox)[Y]); + transformed_pwd2_in = bend(transformed_pwd2_in, tilter); + transformed_pwd2_in = transformed_pwd2_in * bend_mat.inverse(); + } + hatch_dist = Geom::L2(direction.getVector())/5; + Point hatches_dir = rot90(unit_vector(direction.getVector())); + Affine mat(-hatches_dir[Y], hatches_dir[X], hatches_dir[X], hatches_dir[Y],0,0); + transformed_pwd2_in = transformed_pwd2_in * mat; + transformed_org *= mat; + + std::vector > snakePoints; + snakePoints = linearSnake(transformed_pwd2_in, transformed_org); + if (!snakePoints.empty()){ + Piecewise >smthSnake = smoothSnake(snakePoints); + smthSnake = smthSnake*mat.inverse(); + if (do_bend.get_value()){ + smthSnake = smthSnake*bend_mat; + smthSnake = bend(smthSnake, -tilter); + smthSnake = smthSnake*bend_mat.inverse(); + } + return (smthSnake); + } + return pwd2_in; +} + +//------------------------------------------------ +// Generate the levels with random, growth... +//------------------------------------------------ +std::vector +LPERoughHatches::generateLevels(Interval const &domain, double x_org){ + std::vector result; + int n = int((domain.min()-x_org)/hatch_dist); + double x = x_org + n * hatch_dist; + //double x = domain.min() + double(hatch_dist)/2.; + double step = double(hatch_dist); + double scale = 1+(hatch_dist*growth/domain.extent()); + while (x < domain.max()){ + result.push_back(x); + double rdm = 1; + if (dist_rdm.get_value() != 0) + rdm = 1.+ double((2*dist_rdm - dist_rdm.get_value()))/100.; + x+= step*rdm; + step*=scale;//(1.+double(growth)); + } + return result; +} + + +//------------------------------------------------------- +// Walk through the intersections to create linear hatches +//------------------------------------------------------- +std::vector > +LPERoughHatches::linearSnake(Piecewise > const &f, Point const &org){ + + //std::cout<<"linearSnake:\n"; + std::vector > result; + Piecewise x = make_cuts_independent(f)[X]; + //Remark: derivative is computed twice in the 2 lines below!! + Piecewise dx = derivative(x); + OptInterval range = bounds_exact(x); + + if (!range) return result; + std::vector levels = generateLevels(*range, org[X]); + std::vector > times; + times = multi_roots(x,levels); +//TODO: fix multi_roots!!!***************************************** +//remove doubles :-( + std::vector > cleaned_times(levels.size(),std::vector()); + for (unsigned i=0; i0 ){ + double last_t = times[i][0]-1;//ugly hack!! + for (unsigned j=0; j0.000001){ + last_t = times[i][j]; + cleaned_times[i].push_back(last_t); + } + } + } + } + times = cleaned_times; +//******************************************************************* + + LevelsCrossings lscs(times,f,dx); + + unsigned i,j; + lscs.findFirstUnused(i,j); + + std::vector result_component; + int n = int((range->min()-org[X])/hatch_dist); + + while ( i < lscs.size() ){ + int dir = 0; + //switch orientation of first segment according to starting point. + if ((static_cast(i) % 2 == n % 2) && ((j + 1) < lscs[i].size()) && !lscs[i][j].used){ + j += 1; + dir = 2; + } + + while ( i < lscs.size() ){ + result_component.push_back(lscs[i][j].pt); + lscs[i][j].used = true; + lscs.step(i,j, dir); + } + result.push_back(result_component); + result_component = std::vector(); + lscs.findFirstUnused(i,j); + } + return result; +} + +//------------------------------------------------------- +// Smooth the linear hatches according to params... +//------------------------------------------------------- +Piecewise > +LPERoughHatches::smoothSnake(std::vector > const &linearSnake){ + + Piecewise > result; + for (const auto & comp : linearSnake){ + if (comp.size()>=2){ + Point last_pt = comp[0]; + //Point last_top = linearSnake[comp][0]; + //Point last_bot = linearSnake[comp][0]; + Point last_hdle = comp[0]; + Point last_top_hdle = comp[0]; + Point last_bot_hdle = comp[0]; + Geom::Path res_comp(last_pt); + Geom::Path res_comp_top(last_pt); + Geom::Path res_comp_bot(last_pt); + unsigned i=1; + //bool is_top = true;//Inversion here; due to downward y? + bool is_top = ( comp[0][Y] < comp[1][Y] ); + + while( i+1 inside[X]) inside_hdle_in = inside; + //if (inside_hdle_out[X] < inside[X]) inside_hdle_out = inside; + + if (is_top){ + res_comp_top.appendNew(last_top_hdle,new_hdle_in,new_pt); + res_comp_bot.appendNew(last_bot_hdle,inside_hdle_in,inside); + last_top_hdle = new_hdle_out; + last_bot_hdle = inside_hdle_out; + }else{ + res_comp_top.appendNew(last_top_hdle,inside_hdle_in,inside); + res_comp_bot.appendNew(last_bot_hdle,new_hdle_in,new_pt); + last_top_hdle = inside_hdle_out; + last_bot_hdle = new_hdle_out; + } + }else{ + res_comp.appendNew(last_hdle,new_hdle_in,new_pt); + } + + last_hdle = new_hdle_out; + i+=2; + is_top = !is_top; + } + if ( i(last_top_hdle,comp[i],comp[i]); + res_comp_bot.appendNew(last_bot_hdle,comp[i],comp[i]); + }else{ + res_comp.appendNew(last_hdle,comp[i],comp[i]); + } + } + if ( fat_output.get_value() ){ + res_comp = res_comp_bot; + res_comp.setStitching(true); + res_comp.append(res_comp_top.reversed()); + } + result.concat(res_comp.toPwSb()); + } + } + return result; +} + +void +LPERoughHatches::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + using namespace Geom; + top_edge_variation.resetRandomizer(); + bot_edge_variation.resetRandomizer(); + top_tgt_variation.resetRandomizer(); + bot_tgt_variation.resetRandomizer(); + top_smth_variation.resetRandomizer(); + bot_smth_variation.resetRandomizer(); + dist_rdm.resetRandomizer(); + + //original_bbox(lpeitem); +} + + +void +LPERoughHatches::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + Geom::OptRect bbox = item->geometricBounds(); + Geom::Point origin(0.,0.); + Geom::Point vector(50.,0.); + if (bbox) { + origin = bbox->midpoint(); + vector = Geom::Point((*bbox)[X].extent()/4, 0.); + top_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); + bot_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); + top_edge_variation.write_to_SVG(); + bot_edge_variation.write_to_SVG(); + } + //direction.set_and_write_new_values(origin, vector); + //bender.param_set_and_write_new_value( origin + Geom::Point(5,0) ); + direction.set_and_write_new_values(origin + Geom::Point(0,-5), vector); + bender.set_and_write_new_values( origin, Geom::Point(5,0) ); + hatch_dist = Geom::L2(vector)/2; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-rough-hatches.h b/src/live_effects/lpe-rough-hatches.h new file mode 100644 index 0000000..ea4b0c3 --- /dev/null +++ b/src/live_effects/lpe-rough-hatches.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ROUGH_HATCHES_H +#define INKSCAPE_LPE_ROUGH_HATCHES_H + +/** \file + * Fills an area with rough hatches. + */ + +/* + * Authors: + * JFBarraud + * + * Copyright (C) JF Barraud 2008. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPERoughHatches : public Effect { +public: + LPERoughHatches(LivePathEffectObject *lpeobject); + ~LPERoughHatches() override; + + Geom::Piecewise > + doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* item) override; + + std::vector + generateLevels(Geom::Interval const &domain, double x_org); + + std::vector > + linearSnake(Geom::Piecewise > const &f, Geom::Point const &org); + + Geom::Piecewise > + smoothSnake(std::vector > const &linearSnake); + +private: + double hatch_dist; + RandomParam dist_rdm; + ScalarParam growth; + //topfront,topback,bottomfront,bottomback handle scales. + ScalarParam scale_tf, scale_tb, scale_bf, scale_bb; + + RandomParam top_edge_variation; + RandomParam bot_edge_variation; + RandomParam top_tgt_variation; + RandomParam bot_tgt_variation; + RandomParam top_smth_variation; + RandomParam bot_smth_variation; + + BoolParam fat_output, do_bend; + ScalarParam stroke_width_top; + ScalarParam stroke_width_bot; + ScalarParam front_thickness, back_thickness; + + VectorParam direction; + VectorParam bender; + + LPERoughHatches(const LPERoughHatches&) = delete; + LPERoughHatches& operator=(const LPERoughHatches&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-roughen.cpp b/src/live_effects/lpe-roughen.cpp new file mode 100644 index 0000000..0a48d00 --- /dev/null +++ b/src/live_effects/lpe-roughen.cpp @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Roughen LPE implementation. Creates roughen paths. + */ +/* Authors: + * Jabier Arraiza Cenoz + * + * Thanks to all people involved specially to Josh Andler for the idea and to the + * original extensions authors. + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-roughen.h" +#include "display/curve.h" +#include "helper/geom.h" +#include +#include + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData DivisionMethodData[DM_END] = { { DM_SEGMENTS, N_("By number of segments"), "segments" }, + { DM_SIZE, N_("By max. segment size"), "size" } }; +static const Util::EnumDataConverter DMConverter(DivisionMethodData, DM_END); + +static const Util::EnumData HandlesMethodData[HM_END] = { { HM_ALONG_NODES, N_("Along nodes"), "along" }, + { HM_RAND, N_("Rand"), "rand" }, + { HM_RETRACT, N_("Retract"), "retract" }, + { HM_SMOOTH, N_("Smooth"), "smooth" } }; +static const Util::EnumDataConverter HMConverter(HandlesMethodData, HM_END); + +LPERoughen::LPERoughen(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , method(_("Method"), _("Division method"), "method", DMConverter, &wr, this, DM_SIZE) + , max_segment_size(_("Max. segment size"), _("Max. segment size"), "max_segment_size", &wr, this, 10) + , segments(_("Number of segments"), _("Number of segments"), "segments", &wr, this, 2) + , displace_x(_("Max. displacement in X"), _("Max. displacement in X"), "displace_x", &wr, this, 10.) + , displace_y(_("Max. displacement in Y"), _("Max. displacement in Y"), "displace_y", &wr, this, 10.) + , global_randomize(_("Global randomize"), _("Global randomize"), "global_randomize", &wr, this, 1.) + , handles(_("Handles"), _("Handles options"), "handles", HMConverter, &wr, this, HM_ALONG_NODES) + , shift_nodes(_("Shift nodes"), _("Shift nodes"), "shift_nodes", &wr, this, true) + , fixed_displacement(_("Fixed displacement"), _("Fixed displacement, 1/3 of segment length"), "fixed_displacement", + &wr, this, false) + , spray_tool_friendly(_("Spray Tool friendly"), _("For use with spray tool in copy mode"), "spray_tool_friendly", + &wr, this, false) +{ + registerParameter(&method); + registerParameter(&max_segment_size); + registerParameter(&segments); + registerParameter(&displace_x); + registerParameter(&displace_y); + registerParameter(&global_randomize); + registerParameter(&handles); + registerParameter(&shift_nodes); + registerParameter(&fixed_displacement); + registerParameter(&spray_tool_friendly); + displace_x.param_set_range(0., Geom::infinity()); + displace_y.param_set_range(0., Geom::infinity()); + global_randomize.param_set_range(0., Geom::infinity()); + max_segment_size.param_set_range(0., Geom::infinity()); + max_segment_size.param_set_increments(1, 1); + max_segment_size.param_set_digits(3); + segments.param_set_range(1, Geom::infinity()); + segments.param_set_increments(1, 1); + segments.param_set_digits(0); + seed = 0; + apply_to_clippath_and_mask = true; +} + +LPERoughen::~LPERoughen() = default; + +void LPERoughen::doOnApply(SPLPEItem const *lpeitem) +{ + Geom::OptRect bbox = lpeitem->bounds(SPItem::GEOMETRIC_BBOX); + if (bbox) { + std::vector::iterator it = param_vector.begin(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + while (it != param_vector.end()) { + Parameter *param = *it; + const gchar *key = param->param_key.c_str(); + Glib::ustring pref_path = (Glib::ustring) "/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring) "/" + (Glib::ustring)key; + + + bool valid = prefs->getEntry(pref_path).isValid(); + Glib::ustring displace_x_str = Glib::ustring::format((*bbox).width() / 100.0); + Glib::ustring displace_y_str = Glib::ustring::format((*bbox).height() / 100.0); + Glib::ustring max_segment_size_str = Glib::ustring::format(std::min((*bbox).height(), (*bbox).width()) / 100.0); + if (!valid) { + if (strcmp(key, "method") == 0) { + param->param_readSVGValue("size"); + } else if (strcmp(key, "max_segment_size") == 0) { + param->param_readSVGValue(max_segment_size_str.c_str()); + } else if (strcmp(key, "displace_x") == 0) { + param->param_readSVGValue(displace_x_str.c_str()); + } else if (strcmp(key, "displace_y") == 0) { + param->param_readSVGValue(displace_y_str.c_str()); + } else if (strcmp(key, "handles") == 0) { + param->param_readSVGValue("along"); + } else if (strcmp(key, "shift_nodes") == 0) { + param->param_readSVGValue("true"); + } else if (strcmp(key, "fixed_displacement") == 0) { + param->param_readSVGValue("true"); + } else if (strcmp(key, "spray_tool_friendly") == 0) { + param->param_readSVGValue("true"); + } + } + ++it; + } + } +} + +void LPERoughen::doBeforeEffect(SPLPEItem const *lpeitem) +{ + if (spray_tool_friendly && seed == 0 && SP_OBJECT(lpeitem)->getId()) { + std::string id_item(SP_OBJECT(lpeitem)->getId()); + long seed = static_cast(boost::hash_value(id_item)); + global_randomize.param_set_value(global_randomize.get_value(), seed); + } + displace_x.resetRandomizer(); + displace_y.resetRandomizer(); + global_randomize.resetRandomizer(); + srand(1); +} + +Gtk::Widget *LPERoughen::newWidget() +{ + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast(param->param_newWidget()); + if (param->param_key == "method") { + Gtk::Label *method_label = Gtk::manage( + new Gtk::Label(Glib::ustring(_("Add nodes Subdivide each segment")), Gtk::ALIGN_START)); + method_label->set_use_markup(true); + vbox->pack_start(*method_label, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "displace_x") { + Gtk::Label *displace_x_label = Gtk::manage( + new Gtk::Label(Glib::ustring(_("Jitter nodes Move nodes/handles")), Gtk::ALIGN_START)); + displace_x_label->set_use_markup(true); + vbox->pack_start(*displace_x_label, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "global_randomize") { + Gtk::Label *global_rand = Gtk::manage(new Gtk::Label( + Glib::ustring(_("Extra roughen Add an extra layer of rough")), Gtk::ALIGN_START)); + global_rand->set_use_markup(true); + vbox->pack_start(*global_rand, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "handles") { + Gtk::Label *options = Gtk::manage( + new Gtk::Label(Glib::ustring(_("Options Modify options to rough")), Gtk::ALIGN_START)); + options->set_use_markup(true); + vbox->pack_start(*options, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + if (Gtk::Widget *widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +double LPERoughen::sign(double random_number) +{ + if (rand() % 100 < 49) { + random_number *= -1.; + } + return random_number; +} + +Geom::Point LPERoughen::randomize(double max_length, bool is_node) +{ + double factor = 1.0 / 3.0; + if (is_node) { + factor = 1.0; + } + double displace_x_parsed = displace_x * global_randomize * factor; + double displace_y_parsed = displace_y * global_randomize * factor; + Geom::Point output = Geom::Point(sign(displace_x_parsed), sign(displace_y_parsed)); + if (fixed_displacement) { + Geom::Ray ray(Geom::Point(0, 0), output); + output = Geom::Point::polar(ray.angle(), max_length); + } + return output; +} + +void LPERoughen::doEffect(SPCurve *curve) +{ + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + curve->reset(); + for (const auto &path_it : original_pathv) { + if (path_it.empty()) + continue; + + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + SPCurve *nCurve = new SPCurve(); + Geom::Point prev(0, 0); + Geom::Point last_move(0, 0); + nCurve->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + Geom::CubicBezier const *cubic = nullptr; + cubic = dynamic_cast(&*curve_it1); + if (cubic) { + nCurve->curveto((*cubic)[1] + last_move, (*cubic)[2], curve_it1->finalPoint()); + } else { + nCurve->lineto(curve_it1->finalPoint()); + } + last_move = Geom::Point(0, 0); + double length = curve_it1->length(0.01); + std::size_t splits = 0; + if (method == DM_SEGMENTS) { + splits = segments; + } else { + splits = ceil(length / max_segment_size); + } + Geom::Curve const * original = nCurve->last_segment()->duplicate() ; + for (unsigned int t = 1; t <= splits; t++) { + if (t == splits && splits != 1) { + continue; + } + SPCurve const * tmp; + if (splits == 1) { + tmp = jitter(nCurve->last_segment(), prev, last_move); + } else { + bool last = false; + if (t == splits - 1) { + last = true; + } + double time = Geom::nearest_time(original->pointAt((1. / (double)splits) * t), *nCurve->last_segment()); + tmp = addNodesAndJitter(nCurve->last_segment(), prev, last_move, time, last); + } + if (nCurve->get_segment_count() > 1) { + nCurve->backspace(); + nCurve->append_continuous(tmp, 0.001); + } else { + nCurve = tmp->copy(); + } + delete tmp; + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + if (handles == HM_SMOOTH && curve_it1 == curve_endit) { + SPCurve *out = new SPCurve(); + nCurve = nCurve->create_reverse(); + Geom::CubicBezier const *cubic_start = dynamic_cast(nCurve->first_segment()); + Geom::CubicBezier const *cubic = dynamic_cast(nCurve->last_segment()); + Geom::Point oposite = nCurve->first_segment()->pointAt(1.0 / 3.0); + if (cubic_start) { + Geom::Ray ray((*cubic_start)[1], (*cubic_start)[0]); + double dist = Geom::distance((*cubic_start)[1], (*cubic_start)[0]); + oposite = Geom::Point::polar(ray.angle(), dist) + (*cubic_start)[0]; + } + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1], oposite, (*cubic)[3]); + } else { + out->moveto(nCurve->last_segment()->initialPoint()); + out->curveto(nCurve->last_segment()->initialPoint(), oposite, nCurve->last_segment()->finalPoint()); + } + nCurve->backspace(); + nCurve->append_continuous(out, 0.001); + nCurve = nCurve->create_reverse(); + } + if (handles == HM_ALONG_NODES && curve_it1 == curve_endit) { + SPCurve *out = new SPCurve(); + nCurve = nCurve->create_reverse(); + Geom::CubicBezier const *cubic = dynamic_cast(nCurve->last_segment()); + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1], (*cubic)[2] - ((*cubic)[3] - nCurve->first_segment()->initialPoint()), + (*cubic)[3]); + nCurve->backspace(); + nCurve->append_continuous(out, 0.001); + } + nCurve = nCurve->create_reverse(); + } + nCurve->move_endpoints(nCurve->last_segment()->finalPoint(), nCurve->last_segment()->finalPoint()); + nCurve->closepath_current(); + } + curve->append(nCurve, false); + nCurve->reset(); + delete nCurve; + } +} + +SPCurve const * LPERoughen::addNodesAndJitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move, double t, + bool last) +{ + SPCurve *out = new SPCurve(); + Geom::CubicBezier const *cubic = dynamic_cast(&*A); + double max_length = Geom::distance(A->initialPoint(), A->pointAt(t)) / 3.0; + Geom::Point point_a1(0, 0); + Geom::Point point_a2(0, 0); + Geom::Point point_a3(0, 0); + Geom::Point point_b1(0, 0); + Geom::Point point_b2(0, 0); + Geom::Point point_b3(0, 0); + if (shift_nodes) { + point_a3 = randomize(max_length, true); + if (last) { + point_b3 = randomize(max_length, true); + } + } + if (handles == HM_RAND || handles == HM_SMOOTH) { + point_a1 = randomize(max_length); + point_a2 = randomize(max_length); + point_b1 = randomize(max_length); + if (last) { + point_b2 = randomize(max_length); + } + } else { + point_a2 = point_a3; + point_b1 = point_a3; + if (last) { + point_b2 = point_b3; + } + } + if (handles == HM_SMOOTH) { + if (cubic) { + std::pair div = cubic->subdivide(t); + std::vector seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + Geom::Ray ray(seg1[3] + point_a3, seg2[1] + point_a3); + double length = max_length; + if (!fixed_displacement) { + length = Geom::distance(seg1[3] + point_a3, seg2[1] + point_a3); + } + point_b1 = seg1[3] + point_a3 + Geom::Point::polar(ray.angle(), length); + point_b2 = seg2[2]; + point_b3 = seg2[3] + point_b3; + point_a3 = seg1[3] + point_a3; + ray.setPoints(prev, A->initialPoint()); + point_a1 = A->initialPoint() + Geom::Point::polar(ray.angle(), max_length); + if (last) { + Geom::Path b2(point_b3); + b2.appendNew(point_a3); + length = max_length; + ray.setPoints(point_b3, point_b2); + if (!fixed_displacement) { + length = Geom::distance(b2.pointAt(1.0 / 3.0), point_b3); + } + point_b2 = point_b3 + Geom::Point::polar(ray.angle(), length); + } + ray.setPoints(point_b1, point_a3); + point_a2 = point_a3 + Geom::Point::polar(ray.angle(), max_length); + if (last) { + prev = point_b2; + } else { + prev = point_a2; + } + out->moveto(seg1[0]); + out->curveto(point_a1, point_a2, point_a3); + out->curveto(point_b1, point_b2, point_b3); + } else { + Geom::Ray ray(A->pointAt(t) + point_a3, A->pointAt(t + (t / 3))); + double length = max_length; + if (!fixed_displacement) { + length = Geom::distance(A->pointAt(t) + point_a3, A->pointAt(t + (t / 3))); + } + point_b1 = A->pointAt(t) + point_a3 + Geom::Point::polar(ray.angle(), length); + point_b2 = A->pointAt(t + ((t / 3) * 2)); + point_b3 = A->finalPoint() + point_b3; + point_a3 = A->pointAt(t) + point_a3; + ray.setPoints(prev, A->initialPoint()); + point_a1 = A->initialPoint() + Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = randomize(max_length); + } + if (last) { + Geom::Path b2(point_b3); + b2.appendNew(point_a3); + length = max_length; + ray.setPoints(point_b3, point_b2); + if (!fixed_displacement) { + length = Geom::distance(b2.pointAt(1.0 / 3.0), point_b3); + } + point_b2 = point_b3 + Geom::Point::polar(ray.angle(), length); + } + ray.setPoints(point_b1, point_a3); + point_a2 = point_a3 + Geom::Point::polar(ray.angle(), max_length); + if (last) { + prev = point_b2; + } else { + prev = point_a2; + } + out->moveto(A->initialPoint()); + out->curveto(point_a1, point_a2, point_a3); + out->curveto(point_b1, point_b2, point_b3); + } + } else if (handles == HM_RETRACT) { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + if (cubic && !last) { + std::pair div = cubic->subdivide(t); + std::vector seg2 = div.second.controlPoints(); + out->curveto(seg2[1], seg2[2], seg2[3]); + } else { + out->lineto(A->finalPoint() + point_b3); + } + } else if (handles == HM_ALONG_NODES) { + if (cubic) { + std::pair div = cubic->subdivide(t); + std::vector seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + out->moveto(seg1[0]); + out->curveto(seg1[1] + last_move, seg1[2] + point_a3, seg1[3] + point_a3); + last_move = point_a3; + if (last) { + last_move = point_b3; + } + out->curveto(seg2[1] + point_a3, seg2[2] + point_b3, seg2[3] + point_b3); + } else { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + out->lineto(A->finalPoint() + point_b3); + } + } else if (handles == HM_RAND) { + if (cubic) { + std::pair div = cubic->subdivide(t); + std::vector seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + out->moveto(seg1[0]); + out->curveto(seg1[1] + point_a1, seg1[2] + point_a2 + point_a3, seg1[3] + point_a3); + out->curveto(seg2[1] + point_a3 + point_b1, seg2[2] + point_b2 + point_b3, seg2[3] + point_b3); + } else { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + out->lineto(A->finalPoint() + point_b3); + } + } + return out; +} + +SPCurve *LPERoughen::jitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move) +{ + SPCurve *out = new SPCurve(); + Geom::CubicBezier const *cubic = dynamic_cast(&*A); + double max_length = Geom::distance(A->initialPoint(), A->finalPoint()) / 3.0; + Geom::Point point_a1(0, 0); + Geom::Point point_a2(0, 0); + Geom::Point point_a3(0, 0); + if (shift_nodes) { + point_a3 = randomize(max_length, true); + } + if (handles == HM_RAND || handles == HM_SMOOTH) { + point_a1 = randomize(max_length); + point_a2 = randomize(max_length); + } + if (handles == HM_SMOOTH) { + if (cubic) { + Geom::Ray ray(prev, A->initialPoint()); + point_a1 = Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = A->pointAt(1.0 / 3.0) + randomize(max_length); + } + ray.setPoints((*cubic)[3] + point_a3, (*cubic)[2] + point_a3); + point_a2 = randomize(max_length, ray.angle()); + prev = (*cubic)[2] + point_a2; + out->moveto((*cubic)[0]); + out->curveto((*cubic)[0] + point_a1, (*cubic)[2] + point_a2 + point_a3, (*cubic)[3] + point_a3); + } else { + Geom::Ray ray(prev, A->initialPoint()); + point_a1 = Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = A->pointAt(1.0 / 3.0) + randomize(max_length); + } + ray.setPoints(A->finalPoint() + point_a3, A->pointAt((1.0 / 3.0) * 2) + point_a3); + point_a2 = randomize(max_length, ray.angle()); + prev = A->pointAt((1.0 / 3.0) * 2) + point_a2 + point_a3; + out->moveto(A->initialPoint()); + out->curveto(A->initialPoint() + point_a1, A->pointAt((1.0 / 3.0) * 2) + point_a2 + point_a3, + A->finalPoint() + point_a3); + } + } else if (handles == HM_RETRACT) { + out->moveto(A->initialPoint()); + out->lineto(A->finalPoint() + point_a3); + } else if (handles == HM_ALONG_NODES) { + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1] + last_move, (*cubic)[2] + point_a3, (*cubic)[3] + point_a3); + last_move = point_a3; + } else { + out->moveto(A->initialPoint()); + out->lineto(A->finalPoint() + point_a3); + } + } else if (handles == HM_RAND) { + out->moveto(A->initialPoint()); + out->curveto(A->pointAt(0.3333) + point_a1, A->pointAt(0.6666) + point_a2 + point_a3, + A->finalPoint() + point_a3); + } + return out; +} + +Geom::Point LPERoughen::tPoint(Geom::Point A, Geom::Point B, double t) +{ + using Geom::X; + using Geom::Y; + return Geom::Point(A[X] + t * (B[X] - A[X]), A[Y] + t * (B[Y] - A[Y])); +} + +}; // namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-roughen.h b/src/live_effects/lpe-roughen.h new file mode 100644 index 0000000..76cd84e --- /dev/null +++ b/src/live_effects/lpe-roughen.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Roughen LPE effect, see lpe-roughen.cpp. + */ +/* Authors: + * Jabier Arraiza Cenoz + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_ROUGHEN_H +#define INKSCAPE_LPE_ROUGHEN_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum DivisionMethod { + DM_SEGMENTS, + DM_SIZE, + DM_END +}; + +enum HandlesMethod { + HM_ALONG_NODES, + HM_RAND, + HM_RETRACT, + HM_SMOOTH, + HM_END +}; + + +class LPERoughen : public Effect { + +public: + LPERoughen(LivePathEffectObject *lpeobject); + ~LPERoughen() override; + + void doOnApply(SPLPEItem const *lpeitem) override; + void doEffect(SPCurve *curve) override; + virtual double sign(double randNumber); + virtual Geom::Point randomize(double max_length, bool is_node = false); + void doBeforeEffect(SPLPEItem const * lpeitem) override; + virtual SPCurve const * addNodesAndJitter(Geom::Curve const * A, Geom::Point &prev, Geom::Point &last_move, double t, bool last); + virtual SPCurve *jitter(Geom::Curve const * A, Geom::Point &prev, Geom::Point &last_move); + virtual Geom::Point tPoint(Geom::Point A, Geom::Point B, double t = 0.5); + Gtk::Widget *newWidget() override; + +private: + EnumParam method; + ScalarParam max_segment_size; + ScalarParam segments; + RandomParam displace_x; + RandomParam displace_y; + RandomParam global_randomize; + EnumParam handles; + BoolParam shift_nodes; + BoolParam fixed_displacement; + BoolParam spray_tool_friendly; + long seed; + LPERoughen(const LPERoughen &) = delete; + LPERoughen &operator=(const LPERoughen &) = delete; + +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-ruler.cpp b/src/live_effects/lpe-ruler.cpp new file mode 100644 index 0000000..6005083 --- /dev/null +++ b/src/live_effects/lpe-ruler.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation, see lpe-ruler.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-ruler.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData MarkDirData[] = { + {MARKDIR_LEFT , N_("Left"), "left"}, + {MARKDIR_RIGHT , N_("Right"), "right"}, + {MARKDIR_BOTH , N_("Both"), "both"}, +}; +static const Util::EnumDataConverter MarkDirTypeConverter(MarkDirData, sizeof(MarkDirData)/sizeof(*MarkDirData)); + +static const Util::EnumData BorderMarkData[] = { + {BORDERMARK_NONE , NC_("Border mark", "None"), "none"}, + {BORDERMARK_START , N_("Start"), "start"}, + {BORDERMARK_END , N_("End"), "end"}, + {BORDERMARK_BOTH , N_("Both"), "both"}, +}; +static const Util::EnumDataConverter BorderMarkTypeConverter(BorderMarkData, sizeof(BorderMarkData)/sizeof(*BorderMarkData)); + +LPERuler::LPERuler(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + mark_distance(_("_Mark distance:"), _("Distance between successive ruler marks"), "mark_distance", &wr, this, 20.0), + unit(_("Unit:"), _("Unit"), "unit", &wr, this), + mark_length(_("Ma_jor length:"), _("Length of major ruler marks"), "mark_length", &wr, this, 14.0), + minor_mark_length(_("Mino_r length:"), _("Length of minor ruler marks"), "minor_mark_length", &wr, this, 7.0), + major_mark_steps(_("Major steps_:"), _("Draw a major mark every ... steps"), "major_mark_steps", &wr, this, 5), + shift(_("Shift marks _by:"), _("Shift marks by this many steps"), "shift", &wr, this, 0), + mark_dir(_("Mark direction:"), _("Direction of marks (when viewing along the path from start to end)"), "mark_dir", MarkDirTypeConverter, &wr, this, MARKDIR_LEFT), + offset(_("_Offset:"), _("Offset of first mark"), "offset", &wr, this, 0.0), + border_marks(_("Border marks:"), _("Choose whether to draw marks at the beginning and end of the path"), "border_marks", BorderMarkTypeConverter, &wr, this, BORDERMARK_BOTH) +{ + registerParameter(&unit); + registerParameter(&mark_distance); + registerParameter(&mark_length); + registerParameter(&minor_mark_length); + registerParameter(&major_mark_steps); + registerParameter(&shift); + registerParameter(&offset); + registerParameter(&mark_dir); + registerParameter(&border_marks); + + major_mark_steps.param_make_integer(); + major_mark_steps.param_set_range(1, 1000); + shift.param_make_integer(); + + mark_length.param_set_increments(1.0, 10.0); + minor_mark_length.param_set_increments(1.0, 10.0); + offset.param_set_increments(1.0, 10.0); +} + +LPERuler::~LPERuler() += default; + +Geom::Point LPERuler::n_major; +Geom::Point LPERuler::n_minor; + +Geom::Piecewise > +LPERuler::ruler_mark(Geom::Point const &A, Geom::Point const &n, MarkType const &marktype) +{ + using namespace Geom; + + double real_mark_length = mark_length; + SPDocument *document = getSPDoc(); + if (document) { + real_mark_length = Inkscape::Util::Quantity::convert(real_mark_length, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + double real_minor_mark_length = minor_mark_length; + if (document) { + real_minor_mark_length = Inkscape::Util::Quantity::convert(real_minor_mark_length, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + n_major = real_mark_length * n; + n_minor = real_minor_mark_length * n; + if (mark_dir == MARKDIR_BOTH) { + n_major = n_major * 0.5; + n_minor = n_minor * 0.5; + } + + Point C, D; + switch (marktype) { + case MARK_MAJOR: + C = A; + D = A + n_major; + if (mark_dir == MARKDIR_BOTH) + C -= n_major; + break; + case MARK_MINOR: + C = A; + D = A + n_minor; + if (mark_dir == MARKDIR_BOTH) + C -= n_minor; + break; + default: + // do nothing + break; + } + + Piecewise > seg(D2(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + return seg; +} + +Geom::Piecewise > +LPERuler::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + const int mminterval = static_cast(major_mark_steps); + const int i_shift = static_cast(shift) % mminterval; + int sign = (mark_dir == MARKDIR_RIGHT ? 1 : -1 ); + + Piecewise >output(pwd2_in); + Piecewise >speed = derivative(pwd2_in); + Piecewise arclength = arcLengthSb(pwd2_in); + double totlength = arclength.lastValue(); + + //find at which times to draw a mark: + std::vector s_cuts; + + double real_mark_distance = mark_distance; + SPDocument *document = getSPDoc(); + if (document) { + real_mark_distance = Inkscape::Util::Quantity::convert(real_mark_distance, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + double real_offset = offset; + if (document) { + real_offset = Inkscape::Util::Quantity::convert(real_offset, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + for (double s = real_offset; s > roots = multi_roots(arclength, s_cuts); + std::vector t_cuts; + for (auto & root : roots){ + //FIXME: 2geom multi_roots solver seem to sometimes "repeat" solutions. + //Here, we are supposed to have one and only one solution for each s. + if(root.size()>0) + t_cuts.push_back(root[0]); + } + //draw the marks + for (size_t i = 0; i < t_cuts.size(); i++) { + Point A = pwd2_in(t_cuts[i]); + Point n = rot90(unit_vector(speed(t_cuts[i])))*sign; + if (static_cast(i % mminterval) == i_shift) { + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } else { + output.concat (ruler_mark(A, n, MARK_MINOR)); + } + } + //eventually draw a mark at start + if ((border_marks == BORDERMARK_START || border_marks == BORDERMARK_BOTH) && (offset != 0.0 || i_shift != 0)){ + Point A = pwd2_in.firstValue(); + Point n = rot90(unit_vector(speed.firstValue()))*sign; + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } + //eventually draw a mark at end + if (border_marks == BORDERMARK_END || border_marks == BORDERMARK_BOTH){ + Point A = pwd2_in.lastValue(); + Point n = rot90(unit_vector(speed.lastValue()))*sign; + //speed.lastValue() is sometimes wrong when the path is closed: a tiny line seg might added at the end to fix rounding errors... + //TODO: Find a better fix!! (How do we know if the path was closed?) + if ( A == pwd2_in.firstValue() && + speed.segs.size() > 1 && + speed.segs.back()[X].size() <= 1 && + speed.segs.back()[Y].size() <= 1 && + speed.segs.back()[X].tailError(0) <= 1e-10 && + speed.segs.back()[Y].tailError(0) <= 1e-10 + ){ + n = rot90(unit_vector(speed.segs[speed.segs.size()-2].at1()))*sign; + } + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } + + return output; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-ruler.h b/src/live_effects/lpe-ruler.h new file mode 100644 index 0000000..3766717 --- /dev/null +++ b/src/live_effects/lpe-ruler.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_RULER_H +#define INKSCAPE_LPE_RULER_H + +/** \file + * LPE implementation, see lpe-ruler.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/unit.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum MarkType { + MARK_MAJOR, + MARK_MINOR +}; + +enum MarkDirType { + MARKDIR_LEFT, + MARKDIR_RIGHT, + MARKDIR_BOTH, +}; + +enum BorderMarkType { + BORDERMARK_NONE, + BORDERMARK_START, + BORDERMARK_END, + BORDERMARK_BOTH, +}; + +class LPERuler : public Effect { +public: + LPERuler(LivePathEffectObject *lpeobject); + ~LPERuler() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + Geom::Piecewise > ruler_mark(Geom::Point const &A, Geom::Point const &n, MarkType const &marktype); + + ScalarParam mark_distance; + UnitParam unit; + ScalarParam mark_length; + ScalarParam minor_mark_length; + ScalarParam major_mark_steps; + ScalarParam shift; + EnumParam mark_dir; + ScalarParam offset; + EnumParam border_marks; + + static Geom::Point n_major, n_minor; // used for internal computations + + LPERuler(const LPERuler&) = delete; + LPERuler& operator=(const LPERuler&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-show_handles.cpp b/src/live_effects/lpe-show_handles.cpp new file mode 100644 index 0000000..c40f075 --- /dev/null +++ b/src/live_effects/lpe-show_handles.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jabier Arraiza Cenoz + * + * Copyright (C) Jabier Arraiza Cenoz 2014 + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-show_handles.h" +#include <2geom/sbasis-to-bezier.h> +#include <2geom/svg-path-parser.h> +#include "helper/geom.h" +#include "desktop-style.h" +#include "display/curve.h" +#include "svg/svg.h" + +#include "object/sp-shape.h" +#include "style.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPEShowHandles::LPEShowHandles(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + nodes(_("Show nodes"), _("Show nodes"), "nodes", &wr, this, true), + handles(_("Show handles"), _("Show handles"), "handles", &wr, this, true), + original_path(_("Show path"), _("Show path"), "original_path", &wr, this, true), + show_center_node(_("Show center of node"), _("Show center of node"), "show_center_node", &wr, this, false), + original_d(_("Show original"), _("Show original"), "original_d", &wr, this, false), + scale_nodes_and_handles(_("Scale nodes and handles"), _("Scale nodes and handles"), "scale_nodes_and_handles", &wr, this, 10) +{ + registerParameter(&nodes); + registerParameter(&handles); + registerParameter(&original_path); + registerParameter(&show_center_node); + registerParameter(&original_d); + registerParameter(&scale_nodes_and_handles); + scale_nodes_and_handles.param_set_range(0, 500.); + scale_nodes_and_handles.param_set_increments(1, 1); + scale_nodes_and_handles.param_set_digits(2); + stroke_width = 1.0; +} + +bool LPEShowHandles::alerts_off = false; + +/** + * Sets default styles to element + * this permanently remove.some styles of the element + */ + +void LPEShowHandles::doOnApply(SPLPEItem const* lpeitem) +{ + if(!alerts_off) { + char *msg = _("The \"show handles\" path effect will remove any custom style on the object you are applying it to. If this is not what you want, click Cancel."); + Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_OK_CANCEL, true); + gint response = dialog.run(); + alerts_off = true; + if(response == GTK_RESPONSE_CANCEL) { + SPLPEItem* item = const_cast(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + } + SPLPEItem* item = const_cast(lpeitem); + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", "black"); + sp_repr_css_set_property (css, "stroke-width", "1"); + sp_repr_css_set_property (css, "stroke-linecap", "butt"); + sp_repr_css_set_property(css, "fill", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); +} + +void LPEShowHandles::doBeforeEffect (SPLPEItem const* lpeitem) +{ + SPItem const* item = SP_ITEM(lpeitem); + stroke_width = item->style->stroke_width.computed; +} + +Geom::PathVector LPEShowHandles::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + if(original_path) { + for (const auto & i : path_in) { + path_out.push_back(i); + } + } + if(!outline_path.empty()) { + outline_path.clear(); + } + if (original_d) { + SPCurve * shape_curve = current_shape->getCurveForEdit(); + if (shape_curve) { + Geom::PathVector original_curve = shape_curve->get_pathvector(); + if(original_path) { + for (const auto & i : original_curve) { + path_out.push_back(i); + } + } + original_pathv.insert(original_pathv.end(), original_curve.begin(), original_curve.end()); + } + generateHelperPath(original_pathv); + shape_curve->unref(); + } else { + generateHelperPath(original_pathv); + } + for (const auto & i : outline_path) { + path_out.push_back(i); + } + + return path_out; +} + +void +LPEShowHandles::generateHelperPath(Geom::PathVector result) +{ + if(!handles && !nodes) { + return; + } + + Geom::CubicBezier const *cubic = nullptr; + for (auto & path_it : result) { + //Si está vacío... + if (path_it.empty()) { + continue; + } + //Itreadores + Geom::Path::iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::iterator curve_it2 = ++(path_it.begin()); // outgoing curve + Geom::Path::iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + + if (path_it.closed()) { + // if the path is closed, maybe we have to stop a bit earlier because the + // closing line segment has zerolength. + Geom::Curve const &closingline = path_it.back_closed(); // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + if(nodes) { + Geom::NodeType nodetype = Geom::NODE_CUSP; + if(path_it.closed()) { + nodetype = Geom::get_nodetype(path_it.finalCurve(), *curve_it1); + } + drawNode(curve_it1->initialPoint(), nodetype); + } + while (curve_it1 != curve_endit) { + cubic = dynamic_cast(&*curve_it1); + if (cubic) { + if(handles) { + if(!are_near((*cubic)[0],(*cubic)[1])) { + drawHandle((*cubic)[1]); + drawHandleLine((*cubic)[0],(*cubic)[1]); + } + if(!are_near((*cubic)[3],(*cubic)[2])) { + drawHandle((*cubic)[2]); + drawHandleLine((*cubic)[3],(*cubic)[2]); + } + } + } + if(nodes && (curve_it2 != curve_endit || !path_it.closed())) { + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, *curve_it2); + drawNode(curve_it1->finalPoint(), nodetype); + } + ++curve_it1; + if(curve_it2 != curve_endit) { + ++curve_it2; + } + } + } +} + +void +LPEShowHandles::drawNode(Geom::Point p, Geom::NodeType nodetype) +{ + if(stroke_width * scale_nodes_and_handles > 0.0) { + Geom::Rotate rotate = Geom::Rotate(0); + if ( nodetype == Geom::NODE_CUSP) { + rotate = Geom::Rotate::from_degrees(45); + } + double diameter = stroke_width * scale_nodes_and_handles; + char const * svgd; + if (show_center_node) { + svgd = "M 0.05,0 A 0.05,0.05 0 0 1 0,0.05 0.05,0.05 0 0 1 -0.05,0 0.05,0.05 0 0 1 0,-0.05 0.05,0.05 0 0 1 0.05,0 Z M -0.5,-0.5 0.5,-0.5 0.5,0.5 -0.5,0.5 Z"; + } else { + svgd = "M -0.5,-0.5 0.5,-0.5 0.5,0.5 -0.5,0.5 Z"; + } + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= rotate * Geom::Scale(diameter) * Geom::Translate(p); + outline_path.push_back(pathv[0]); + if (show_center_node) { + outline_path.push_back(pathv[1]); + } + } +} + +void +LPEShowHandles::drawHandle(Geom::Point p) +{ + if(stroke_width * scale_nodes_and_handles > 0.0) { + double diameter = stroke_width * scale_nodes_and_handles; + char const * svgd; + svgd = "M 0.7,0.35 A 0.35,0.35 0 0 1 0.35,0.7 0.35,0.35 0 0 1 0,0.35 0.35,0.35 0 0 1 0.35,0 0.35,0.35 0 0 1 0.7,0.35 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale (diameter) * Geom::Translate(p - Geom::Point(diameter * 0.35,diameter * 0.35)); + outline_path.push_back(pathv[0]); + } +} + + +void +LPEShowHandles::drawHandleLine(Geom::Point p,Geom::Point p2) +{ + Geom::Path path; + double diameter = stroke_width * scale_nodes_and_handles; + if(diameter > 0.0 && Geom::distance(p,p2) > (diameter * 0.35)) { + Geom::Ray ray2(p, p2); + p2 = p2 - Geom::Point::polar(ray2.angle(),(diameter * 0.35)); + } + path.start( p ); + path.appendNew( p2 ); + outline_path.push_back(path); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-show_handles.h b/src/live_effects/lpe-show_handles.h new file mode 100644 index 0000000..865034e --- /dev/null +++ b/src/live_effects/lpe-show_handles.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SHOW_HANDLES_H +#define INKSCAPE_LPE_SHOW_HANDLES_H + +/* + * Authors: + * Jabier Arraiza Cenoz + * + * Copyright (C) Jabier Arraiza Cenoz 2014 + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "helper/geom-nodetype.h" +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEShowHandles : public Effect , GroupBBoxEffect { + +public: + LPEShowHandles(LivePathEffectObject *lpeobject); + ~LPEShowHandles() override = default; + + void doOnApply(SPLPEItem const* lpeitem) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + virtual void generateHelperPath(Geom::PathVector result); + + virtual void drawNode(Geom::Point p, Geom::NodeType nodetype); + + virtual void drawHandle(Geom::Point p); + + virtual void drawHandleLine(Geom::Point p,Geom::Point p2); + +protected: + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + + BoolParam nodes; + BoolParam handles; + BoolParam original_path; + BoolParam original_d; + BoolParam show_center_node; + ScalarParam scale_nodes_and_handles; + double stroke_width; + static bool alerts_off; + + Geom::PathVector outline_path; + + LPEShowHandles(const LPEShowHandles &) = delete; + LPEShowHandles &operator=(const LPEShowHandles &) = delete; + +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif + +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-simplify.cpp b/src/live_effects/lpe-simplify.cpp new file mode 100644 index 0000000..a1821d2 --- /dev/null +++ b/src/live_effects/lpe-simplify.cpp @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-simplify.h" +#include "display/curve.h" +#include "helper/geom.h" +#include <2geom/svg-path-parser.h> +#include "svg/svg.h" +#include "ui/tools/node-tool.h" +#include "ui/icon-names.h" + +#include "splivarot.h" // Path_for_pathvector, simplify paths + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPESimplify::LPESimplify(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , steps(_("Steps:"), _("Change number of simplify steps "), "steps", &wr, this, 1) + , threshold(_("Roughly threshold:"), _("Roughly threshold:"), "threshold", &wr, this, 0.002) + , smooth_angles(_("Smooth angles:"), _("Max degree difference on handles to perform a smooth"), "smooth_angles", + &wr, this, 0.) + , helper_size(_("Helper size:"), _("Helper size"), "helper_size", &wr, this, 5) + , simplify_individual_paths(_("Paths separately"), _("Simplifying paths (separately)"), "simplify_individual_paths", + &wr, this, false, "", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")) + , simplify_just_coalesce(_("Just coalesce"), _("Simplify just coalesce"), "simplify_just_coalesce", &wr, this, + false, "", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")) +{ + registerParameter(&steps); + registerParameter(&threshold); + registerParameter(&smooth_angles); + registerParameter(&helper_size); + registerParameter(&simplify_individual_paths); + registerParameter(&simplify_just_coalesce); + + threshold.param_set_range(0.0001, Geom::infinity()); + threshold.param_set_increments(0.0001, 0.0001); + threshold.param_set_digits(6); + + steps.param_set_range(0, 100); + steps.param_set_increments(1, 1); + steps.param_set_digits(0); + + smooth_angles.param_set_range(0.0, 360.0); + smooth_angles.param_set_increments(10, 10); + smooth_angles.param_set_digits(2); + + helper_size.param_set_range(0.0, 999.0); + helper_size.param_set_increments(5, 5); + helper_size.param_set_digits(2); + + radius_helper_nodes = 6.0; + apply_to_clippath_and_mask = true; +} + +LPESimplify::~LPESimplify() = default; + +void +LPESimplify::doBeforeEffect (SPLPEItem const* lpeitem) +{ + if(!hp.empty()) { + hp.clear(); + } + bbox = SP_ITEM(lpeitem)->visualBounds(); + radius_helper_nodes = helper_size; +} + +Gtk::Widget * +LPESimplify::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::VBox * vbox = Gtk::manage( new Gtk::VBox(Effect::newWidget()) ); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector::iterator it = param_vector.begin(); + Gtk::HBox * buttons = Gtk::manage(new Gtk::HBox(true,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast(param->param_newWidget()); + if (param->param_key == "simplify_individual_paths" || + param->param_key == "simplify_just_coalesce") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + buttons->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + Gtk::HBox * horizontal_box = dynamic_cast(widg); + std::vector< Gtk::Widget* > child_list = horizontal_box->get_children(); + Gtk::Entry* entry_widg = dynamic_cast(child_list[1]); + entry_widg->set_width_chars(8); + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + } + + ++it; + } + vbox->pack_start(*buttons,true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +void +LPESimplify::doEffect(SPCurve *curve) +{ + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + gdouble size = Geom::L2(bbox->dimensions()); + //size /= Geom::Affine(0,0,0,0,0,0).descrim(); + Path* pathliv = Path_for_pathvector(original_pathv); + if(simplify_individual_paths) { + size = Geom::L2(Geom::bounds_fast(original_pathv)->dimensions()); + } + size /= sp_lpe_item->i2doc_affine().descrim(); + for (int unsigned i = 0; i < steps; i++) { + if ( simplify_just_coalesce ) { + pathliv->Coalesce(threshold * size); + } else { + pathliv->ConvertEvenLines(threshold * size); + pathliv->Simplify(threshold * size); + } + } + Geom::PathVector result = Geom::parse_svg_path(pathliv->svg_dump_path()); + generateHelperPathAndSmooth(result); + curve->set_pathvector(result); + Inkscape::UI::Tools::sp_update_helperpath(); +} + +void +LPESimplify::generateHelperPathAndSmooth(Geom::PathVector &result) +{ + if(steps < 1) { + return; + } + Geom::PathVector tmp_path; + Geom::CubicBezier const *cubic = nullptr; + for (auto & path_it : result) { + if (path_it.empty()) { + continue; + } + + Geom::Path::iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::iterator curve_it2 = ++(path_it.begin());// outgoing curve + Geom::Path::iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + SPCurve *nCurve = new SPCurve(); + if (path_it.closed()) { + // if the path is closed, maybe we have to stop a bit earlier because the + // closing line segment has zerolength. + const Geom::Curve &closingline = + path_it.back_closed(); // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + curve_endit = path_it.end_open(); + } + } + if(helper_size > 0) { + drawNode(curve_it1->initialPoint()); + } + nCurve->moveto(curve_it1->initialPoint()); + Geom::Point start = Geom::Point(0,0); + while (curve_it1 != curve_endit) { + cubic = dynamic_cast(&*curve_it1); + Geom::Point point_at1 = curve_it1->initialPoint(); + Geom::Point point_at2 = curve_it1->finalPoint(); + Geom::Point point_at3 = curve_it1->finalPoint(); + Geom::Point point_at4 = curve_it1->finalPoint(); + + if(start == Geom::Point(0,0)) { + start = point_at1; + } + + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + } + + if(path_it.closed() && curve_it2 == curve_endit) { + point_at4 = start; + } + if(curve_it2 != curve_endit) { + cubic = dynamic_cast(&*curve_it2); + if (cubic) { + point_at4 = (*cubic)[1]; + } + } + Geom::Ray ray1(point_at2, point_at3); + Geom::Ray ray2(point_at3, point_at4); + double angle1 = Geom::deg_from_rad(ray1.angle()); + double angle2 = Geom::deg_from_rad(ray2.angle()); + if((smooth_angles >= std::abs(angle2 - angle1)) && !are_near(point_at4,point_at3) && !are_near(point_at2,point_at3)) { + double dist = Geom::distance(point_at2,point_at3); + Geom::Angle angleFixed = ray2.angle(); + angleFixed -= Geom::Angle::from_degrees(180.0); + point_at2 = Geom::Point::polar(angleFixed, dist) + point_at3; + } + nCurve->curveto(point_at1, point_at2, curve_it1->finalPoint()); + cubic = dynamic_cast(nCurve->last_segment()); + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + if(helper_size > 0) { + if(!are_near((*cubic)[0],(*cubic)[1])) { + drawHandle((*cubic)[1]); + drawHandleLine((*cubic)[0],(*cubic)[1]); + } + if(!are_near((*cubic)[3],(*cubic)[2])) { + drawHandle((*cubic)[2]); + drawHandleLine((*cubic)[3],(*cubic)[2]); + } + } + } + if(helper_size > 0) { + drawNode(curve_it1->finalPoint()); + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + nCurve->closepath_current(); + } + tmp_path.push_back(nCurve->get_pathvector()[0]); + nCurve->reset(); + delete nCurve; + } + result = tmp_path; +} + +void +LPESimplify::drawNode(Geom::Point p) +{ + double r = radius_helper_nodes; + char const * svgd; + svgd = "M 0.55,0.5 A 0.05,0.05 0 0 1 0.5,0.55 0.05,0.05 0 0 1 0.45,0.5 0.05,0.05 0 0 1 0.5,0.45 0.05,0.05 0 0 1 0.55,0.5 Z M 0,0 1,0 1,1 0,1 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale(r) * Geom::Translate(p - Geom::Point(0.5*r,0.5*r)); + hp.push_back(pathv[0]); + hp.push_back(pathv[1]); +} + +void +LPESimplify::drawHandle(Geom::Point p) +{ + double r = radius_helper_nodes; + char const * svgd; + svgd = "M 0.7,0.35 A 0.35,0.35 0 0 1 0.35,0.7 0.35,0.35 0 0 1 0,0.35 0.35,0.35 0 0 1 0.35,0 0.35,0.35 0 0 1 0.7,0.35 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale(r) * Geom::Translate(p - Geom::Point(0.35*r,0.35*r)); + hp.push_back(pathv[0]); +} + + +void +LPESimplify::drawHandleLine(Geom::Point p,Geom::Point p2) +{ + Geom::Path path; + path.start( p ); + double diameter = radius_helper_nodes; + if(helper_size > 0 && Geom::distance(p,p2) > (diameter * 0.35)) { + Geom::Ray ray2(p, p2); + p2 = p2 - Geom::Point::polar(ray2.angle(),(diameter * 0.35)); + } + path.appendNew( p2 ); + hp.push_back(path); +} + +void +LPESimplify::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(hp); +} + + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-simplify.h b/src/live_effects/lpe-simplify.h new file mode 100644 index 0000000..9eb3d15 --- /dev/null +++ b/src/live_effects/lpe-simplify.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SIMPLIFY_H +#define INKSCAPE_LPE_SIMPLIFY_H + +/* + * Inkscape::LPESimplify + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/parameter/togglebutton.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPESimplify : public Effect , GroupBBoxEffect { + +public: + LPESimplify(LivePathEffectObject *lpeobject); + ~LPESimplify() override; + LPESimplify(const LPESimplify &) = delete; + LPESimplify &operator=(const LPESimplify &) = delete; + + void doEffect(SPCurve *curve) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + virtual void generateHelperPathAndSmooth(Geom::PathVector &result); + + Gtk::Widget * newWidget() override; + + virtual void drawNode(Geom::Point p); + + virtual void drawHandle(Geom::Point p); + + virtual void drawHandleLine(Geom::Point p,Geom::Point p2); + ScalarParam threshold; + + protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) override; + +private: + ScalarParam steps; + ScalarParam smooth_angles; + ScalarParam helper_size; + ToggleButtonParam simplify_individual_paths; + ToggleButtonParam simplify_just_coalesce; + + double radius_helper_nodes; + Geom::PathVector hp; + Geom::OptRect bbox; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-skeleton.cpp b/src/live_effects/lpe-skeleton.cpp new file mode 100644 index 0000000..302a6a2 --- /dev/null +++ b/src/live_effects/lpe-skeleton.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Minimal dummy LPE effect implementation, used as an example for a base + * starting class when implementing new LivePathEffects. + * + * In vi, three global search-and-replaces will let you rename everything + * in this and the .h file: + * + * :%s/SKELETON/YOURNAME/g + * :%s/Skeleton/Yourname/g + * :%s/skeleton/yourname/g + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-skeleton.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPESkeleton::LPESkeleton(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + number(_("Float parameter"), _("just a real number like 1.4!"), "svgname", &wr, this, 1.2) +{ + /* uncomment the following line to have the original path displayed while the item is selected */ + //show_orig_path = true; + /* uncomment the following line to enable display of the effect-specific on-canvas handles (knotholder entities) */ + //_provides_knotholder_entities + + /* register all your parameters here, so Inkscape knows which parameters this effect has: */ + registerParameter(&number); +} + +LPESkeleton::~LPESkeleton() += default; + + +/* ######################## + * Choose to implement one of the doEffect functions. You can delete or comment out the others. + */ + +/* +void +LPESkeleton::doEffect (SPCurve * curve) +{ + // spice this up to make the effect actually *do* something! +} + +Geom::PathVector +LPESkeleton::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + + path_out = path_in; // spice this up to make the effect actually *do* something! + + return path_out; +} +*/ + +Geom::Piecewise > +LPESkeleton::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + Geom::Piecewise > output; + + output = pwd2_in; // spice this up to make the effect actually *do* something! + + return output; +} + +/* ######################## + * If you want to provide effect-specific on-canvas handles (knotholder entities), define them here: + */ + +/* +namespace Skeleton { + +class KnotHolderEntityMyHandle : public LPEKnotHolderEntity +{ +public: + // the set() and get() methods must be implemented, click() is optional + virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); + virtual Geom::Point knot_get() const; + //virtual void knot_click(guint state); +}; + +} // namespace Skeleton + +void +LPESkeleton::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { + { + KnotHolderEntityMyHandle *e = new KnotHolderEntityMyHandle(this); + e->create( NULL, item, knotholder, + _("Text describing what this handle does"), + //optional: knot_shape, knot_mode, knot_color); + knotholder->add(e); + } +}; +*/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-skeleton.h b/src/live_effects/lpe-skeleton.h new file mode 100644 index 0000000..57d6d73 --- /dev/null +++ b/src/live_effects/lpe-skeleton.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Minimal LPE effect, see lpe-skeleton.cpp. + */ +/* Authors: + * Johan Engelen + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_SKELETON_H +#define INKSCAPE_LPE_SKELETON_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +// each knotholder handle for your LPE requires a separate class derived from LPEKnotHolderEntity; +// define it in lpe-skeleton.cpp and add code to create it in addKnotHolderEntities +// note that the LPE parameter classes implement their own handles! So in most cases, you will +// not have to do anything like this. +/** +namespace Skeleton { + // we need a separate namespace to avoid clashes with other LPEs + class KnotHolderEntityMyHandle; +} +**/ + +class LPESkeleton : public Effect { +public: + LPESkeleton(LivePathEffectObject *lpeobject); + ~LPESkeleton() override; + +// Choose to implement one of the doEffect functions. You can delete or comment out the others. +// virtual void doEffect (SPCurve * curve); +// virtual Geom::PathVector doEffect_path (Geom::PathVector const &path_in); + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const &pwd2_in) override; + + /* the knotholder entity classes (if any) can be declared friends */ + //friend class Skeleton::KnotHolderEntityMyHandle; + //virtual void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item); + +private: + // add the parameters for your effect here: + ScalarParam number; + // there are all kinds of parameters. Check the /live_effects/parameter directory which types exist! + + LPESkeleton(const LPESkeleton&) = delete; + LPESkeleton& operator=(const LPESkeleton&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-sketch.cpp b/src/live_effects/lpe-sketch.cpp new file mode 100644 index 0000000..614d711 --- /dev/null +++ b/src/live_effects/lpe-sketch.cpp @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE sketch effect implementation. + */ +/* Authors: + * Jean-Francois Barraud + * Johan Engelen + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-sketch.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/sbasis-math.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/path-intersection.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPESketch::LPESketch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + //testpointA(_("Test Point A"), _("Test A"), "ptA", &wr, this, Geom::Point(100,100)), + nbiter_approxstrokes(_("Strokes:"), _("Draw that many approximating strokes"), "nbiter_approxstrokes", &wr, this, 5), + strokelength(_("Max stroke length:"), + _("Maximum length of approximating strokes"), "strokelength", &wr, this, 100.), + strokelength_rdm(_("Stroke length variation:"), + _("Random variation of stroke length (relative to maximum length)"), "strokelength_rdm", &wr, this, .3), + strokeoverlap(_("Max. overlap:"), + _("How much successive strokes should overlap (relative to maximum length)"), "strokeoverlap", &wr, this, .3), + strokeoverlap_rdm(_("Overlap variation:"), + _("Random variation of overlap (relative to maximum overlap)"), "strokeoverlap_rdm", &wr, this, .3), + ends_tolerance(_("Max. end tolerance:"), + _("Maximum distance between ends of original and approximating paths (relative to maximum length)"), "ends_tolerance", &wr, this, .1), + parallel_offset(_("Average offset:"), + _("Average distance each stroke is away from the original path"), "parallel_offset", &wr, this, 5.), + tremble_size(_("Max. tremble:"), + _("Maximum tremble magnitude"), "tremble_size", &wr, this, 5.), + tremble_frequency(_("Tremble frequency:"), + _("Average number of tremble periods in a stroke"), "tremble_frequency", &wr, this, 1.) +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + ,nbtangents(_("Construction lines:"), + _("How many construction lines (tangents) to draw"), "nbtangents", &wr, this, 5), + tgtscale(_("Scale:"), + _("Scale factor relating curvature and length of construction lines (try 5*offset)"), "tgtscale", &wr, this, 10.0), + tgtlength(_("Max. length:"), _("Maximum length of construction lines"), "tgtlength", &wr, this, 100.0), + tgtlength_rdm(_("Length variation:"), _("Random variation of the length of construction lines"), "tgtlength_rdm", &wr, this, .3), + tgt_places_rdmness(_("Placement randomness:"), _("0: evenly distributed construction lines, 1: purely random placement"), "tgt_places_rdmness", &wr, this, 1.) +#ifdef LPE_SKETCH_USE_CURVATURE + ,min_curvature(_("k_min:"), _("min curvature"), "k_min", &wr, this, 4.0) + ,max_curvature(_("k_max:"), _("max curvature"), "k_max", &wr, this, 1000.0) +#endif +#endif +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + //Add some comment in the UI: *warning* the precise output of this effect might change in future releases! + //convert to path if you want to keep exact output unchanged in future releases... + //registerParameter(&testpointA) ); + registerParameter(&nbiter_approxstrokes); + registerParameter(&strokelength); + registerParameter(&strokelength_rdm); + registerParameter(&strokeoverlap); + registerParameter(&strokeoverlap_rdm); + registerParameter(&ends_tolerance); + registerParameter(¶llel_offset); + registerParameter(&tremble_size); + registerParameter(&tremble_frequency); +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + registerParameter(&nbtangents); + registerParameter(&tgt_places_rdmness); + registerParameter(&tgtscale); + registerParameter(&tgtlength); + registerParameter(&tgtlength_rdm); +#ifdef LPE_SKETCH_USE_CURVATURE + registerParameter(&min_curvature); + registerParameter(&max_curvature); +#endif +#endif + + nbiter_approxstrokes.param_make_integer(); + nbiter_approxstrokes.param_set_range(0, Geom::infinity()); + strokelength.param_set_range(1, Geom::infinity()); + strokelength.param_set_increments(1., 5.); + strokelength_rdm.param_set_range(0, 1.); + strokeoverlap.param_set_range(0, 1.); + strokeoverlap.param_set_increments(0.1, 0.30); + ends_tolerance.param_set_range(0., 1.); + parallel_offset.param_set_range(0, Geom::infinity()); + tremble_frequency.param_set_range(0.01, 100.); + tremble_frequency.param_set_increments(.5, 1.5); + strokeoverlap_rdm.param_set_range(0, 1.); + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + nbtangents.param_make_integer(); + nbtangents.param_set_range(0, Geom::infinity()); + tgtscale.param_set_range(0, Geom::infinity()); + tgtscale.param_set_increments(.1, .5); + tgtlength.param_set_range(0, Geom::infinity()); + tgtlength.param_set_increments(1., 5.); + tgtlength_rdm.param_set_range(0, 1.); + tgt_places_rdmness.param_set_range(0, 1.); + //this is not very smart, but required to avoid having lot of tangents stacked on short components. + //Note: we could specify a density instead of an absolute number, but this would be scale dependent. + concatenate_before_pwd2 = true; +#endif +} + +LPESketch::~LPESketch() += default; + +/* +Geom::Piecewise > +addLinearEnds (Geom::Piecewise > & m){ + using namespace Geom; + Piecewise > output; + Piecewise > start; + Piecewise > end; + double x,y,vx,vy; + + x = m.segs.front()[0].at0(); + y = m.segs.front()[1].at0(); + vx = m.segs.front()[0][1][0]+Tri(m.segs.front()[0][0]); + vy = m.segs.front()[1][1][0]+Tri(m.segs.front()[1][0]); + start = Piecewise >(D2(Linear (x-vx,x),Linear (y-vy,y))); + start.offsetDomain(m.cuts.front()-1.); + + x = m.segs.back()[0].at1(); + y = m.segs.back()[1].at1(); + vx = -m.segs.back()[0][1][1]+Tri(m.segs.back()[0][0]);; + vy = -m.segs.back()[1][1][1]+Tri(m.segs.back()[1][0]);; + end = Piecewise >(D2(Linear (x,x+vx),Linear (y,y+vy))); + //end.offsetDomain(m.cuts.back()); + + output = start; + output.concat(m); + output.concat(end); + return output; +} +*/ + + + +//This returns a random perturbation. Notice the domain is [s0,s0+first multiple of period>s1]... +Geom::Piecewise > +LPESketch::computePerturbation (double s0, double s1){ + using namespace Geom; + Piecewise >res; + + //global offset for this stroke. + double offsetX = 2*parallel_offset-parallel_offset.get_value(); + double offsetY = 2*parallel_offset-parallel_offset.get_value(); + Point A,dA,B,dB,offset = Point(offsetX,offsetY); + //start point A + for (unsigned dim=0; dim<2; dim++){ + A[dim] = offset[dim] + 2*tremble_size-tremble_size.get_value(); + dA[dim] = 2*tremble_size-tremble_size.get_value(); + } + //compute howmany deg 3 sbasis to concat according to frequency. + + unsigned count = unsigned((s1-s0)/strokelength*tremble_frequency)+1; + //unsigned count = unsigned((s1-s0)/tremble_frequency)+1; + + for (unsigned i=0; i perturb = D2(SBasis(2, Linear()), SBasis(2, Linear())); + for (unsigned dim=0; dim<2; dim++){ + B[dim] = offset[dim] + 2*tremble_size-tremble_size.get_value(); + perturb[dim][0] = Linear(A[dim],B[dim]); + dA[dim] = dA[dim]-B[dim]+A[dim]; + //avoid dividing by 0. Very short strokes will have ends parallel to the curve... + if ( s1-s0 > 1e-2) + dB[dim] = -(2*tremble_size-tremble_size.get_value())/(s0-s1)-B[dim]+A[dim]; + else + dB[dim] = -(2*tremble_size-tremble_size.get_value())-B[dim]+A[dim]; + perturb[dim][1] = Linear(dA[dim],dB[dim]); + } + dA = B-A-dB; + A = B; + //dA = B-A-dB; + res.concat(Piecewise >(perturb)); + } + res.setDomain(Interval(s0,s0+count*strokelength/tremble_frequency)); + //res.setDomain(Interval(s0,s0+count*tremble_frequency)); + return res; +} + + +// Main effect body... +Geom::Piecewise > +LPESketch::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + //If the input path is empty, do nothing. + //Note: this happens when duplicating a 3d box... dunno why. + if (pwd2_in.size()==0) return pwd2_in; + + Piecewise > output; + + // some variables for futur use (for construction lines; compute arclength only once...) + // notations will be : t = path time, s = distance from start along the path. + Piecewise pathlength; + double total_length = 0; + + //TODO: split Construction Lines/Approximated Strokes into two separate effects? + + //----- Approximated Strokes. + std::vector > > pieces_in = split_at_discontinuities (pwd2_in); + + //work separately on each component. + for (auto piece : pieces_in){ + + Piecewise piecelength = arcLengthSb(piece,.1); + double piece_total_length = piecelength.segs.back().at1()-piecelength.segs.front().at0(); + pathlength.concat(piecelength + total_length); + total_length += piece_total_length; + + + //TODO: better check this on the Geom::Path. + bool closed = piece.segs.front().at0() == piece.segs.back().at1(); + if (closed){ + piece.concat(piece); + piecelength.concat(piecelength+piece_total_length); + } + + for (unsigned i = 0; ipiece_total_length - ends_tolerance.get_value()*strokelength) break; + if ( closed && s0>piece_total_length + s0_initial) break; + + std::vector times; + times = roots(piecelength-s0); + t0 = times.at(0);//there should be one and only one solution!! + + // pick a new end point (s1 = s0 + strokelength). + s1 = s0 + strokelength*(1-strokelength_rdm); + // don't let it go beyond the end of the original path. + // TODO/FIXME: this might result in short strokes near the end... + if (!closed && s1>piece_total_length-ends_tolerance.get_value()*strokelength){ + done = true; + //!!the root solver might miss s1==piece_total_length... + if (s1>piece_total_length){s1 = piece_total_length - ends_tolerance*strokelength-0.0001;} + } + if (closed && s1>piece_total_length + s0_initial){ + done = true; + if (closed && s1>2*piece_total_length){ + s1 = 2*piece_total_length - strokeoverlap*(1-strokeoverlap_rdm)*strokelength-0.0001; + } + } + times = roots(piecelength-s1); + if (times.empty()) break;//we should not be there. + t1 = times[0]; + + //pick a rdm perturbation, and collect the perturbed piece into output. + Piecewise > pwperturb = computePerturbation(s0-0.01,s1+0.01); + pwperturb = compose(pwperturb,portion(piecelength,t0,t1)); + + output.concat(portion(piece,t0,t1)+pwperturb); + + //step points: s0 = s1 - overlap. + //TODO: make sure this has to end? + s0 = s1 - strokeoverlap*(1-strokeoverlap_rdm)*(s1-s0); + } + } + } + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + + //----- Construction lines. + //TODO: choose places according to curvature?. + + //at this point we should have: + //pathlength = arcLengthSb(pwd2_in,.1); + //total_length = pathlength.segs.back().at1()-pathlength.segs.front().at0(); + Piecewise > m = pwd2_in; + Piecewise > v = derivative(pwd2_in); + Piecewise > a = derivative(v); + +#ifdef LPE_SKETCH_USE_CURVATURE + //---- curvature experiment...(enable + Piecewise k = curvature(pwd2_in); + OptInterval k_bnds = bounds_exact(abs(k)); + double k_min = k_bnds->min() + k_bnds->extent() * min_curvature; + double k_max = k_bnds->min() + k_bnds->extent() * max_curvature; + + Piecewise bump; + //SBasis bump_seg = SBasis( 2, Linear(0) ); + //bump_seg[1] = Linear( 4. ); + SBasis bump_seg = SBasis( 1, Linear(1) ); + bump.push_cut( k_bnds->min() - 1 ); + bump.push( Linear(0), k_min ); + bump.push(bump_seg,k_max); + bump.push( Linear(0), k_bnds->max()+1 ); + + Piecewise repartition = compose( bump, k ); + repartition = integral(repartition); + //------------------------------- +#endif + + for (unsigned i=0; i times; + times = roots(repartition - proba); + double t = times.at(0);//there should be one and only one solution! +#else + //double s = total_length * ( i + tgtlength_rdm ) / (nbtangents+1.); + double reg_place = total_length * ( i + .5) / ( nbtangents ); + double rdm_place = total_length * tgt_places_rdmness; + double s = ( 1.- tgt_places_rdmness.get_value() ) * reg_place + rdm_place ; + std::vector times; + times = roots(pathlength-s); + double t = times.at(0);//there should be one and only one solution! +#endif + Point m_t = m(t), v_t = v(t), a_t = a(t); + //Compute tgt length according to curvature (not exceeding tgtlength) so that + // dist to original curve ~ 4 * (parallel_offset+tremble_size). + //TODO: put this 4 as a parameter in the UI... + //TODO: what if with v=0? + double l = tgtlength*(1-tgtlength_rdm)/v_t.length(); + double r = std::pow(v_t.length(), 3) / cross(v_t, a_t); + r = sqrt((2*fabs(r)-tgtscale)*tgtscale)/v_t.length(); + l=(r tgt = D2(); + for (unsigned dim=0; dim<2; dim++){ + tgt[dim] = SBasis(Linear(m_t[dim]-v_t[dim]*l, m_t[dim]+v_t[dim]*l)); + } + output.concat(Piecewise >(tgt)); + } +#endif + + return output; +} + +void +LPESketch::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + //init random parameters. + parallel_offset.resetRandomizer(); + strokelength_rdm.resetRandomizer(); + strokeoverlap_rdm.resetRandomizer(); + ends_tolerance.resetRandomizer(); + tremble_size.resetRandomizer(); +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + tgtlength_rdm.resetRandomizer(); + tgt_places_rdmness.resetRandomizer(); +#endif +} + +/* ######################## */ + +} //namespace LivePathEffect (setq default-directory "c:/Documents And Settings/jf/Mes Documents/InkscapeSVN") +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-sketch.h b/src/live_effects/lpe-sketch.h new file mode 100644 index 0000000..4d34088 --- /dev/null +++ b/src/live_effects/lpe-sketch.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * @brief LPE sketch effect implementation, see lpe-sketch.cpp. + */ +/* Authors: + * Jean-Francois Barraud + * Johan Engelen + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_SKETCH_H +#define INKSCAPE_LPE_SKETCH_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/random.h" +#include "live_effects/parameter/point.h" + +#define LPE_SKETCH_USE_CONSTRUCTION_LINES +//#define LPE_SKETCH_USE_CURVATURE + +namespace Inkscape { +namespace LivePathEffect { + +class LPESketch : public Effect { +public: + LPESketch(LivePathEffectObject *lpeobject); + ~LPESketch() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + +private: + // add the parameters for your effect here: + //PointParam testpointA; + ScalarParam nbiter_approxstrokes; + ScalarParam strokelength; + RandomParam strokelength_rdm; + ScalarParam strokeoverlap; + RandomParam strokeoverlap_rdm; + RandomParam ends_tolerance; + RandomParam parallel_offset; + RandomParam tremble_size; + ScalarParam tremble_frequency; + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + ScalarParam nbtangents; + ScalarParam tgtscale; + ScalarParam tgtlength; + RandomParam tgtlength_rdm; + RandomParam tgt_places_rdmness; +#ifdef LPE_SKETCH_USE_CURVATURE + ScalarParam min_curvature; + ScalarParam max_curvature; +#endif +#endif + LPESketch(const LPESketch&) = delete; + LPESketch& operator=(const LPESketch&) = delete; + + Geom::Piecewise > computePerturbation (double s0, double s1); + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/lpe-spiro.cpp b/src/live_effects/lpe-spiro.cpp new file mode 100644 index 0000000..e0464a2 --- /dev/null +++ b/src/live_effects/lpe-spiro.cpp @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#define INKSCAPE_LPE_SPIRO_C + +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-spiro.h" + +#include "display/curve.h" +#include <2geom/curves.h> +#include "helper/geom-nodetype.h" +#include "helper/geom-curves.h" + +#include "live_effects/spiro.h" + +// For handling un-continuous paths: +#include "message-stack.h" +#include "inkscape.h" + +namespace Inkscape { +namespace LivePathEffect { + +LPESpiro::LPESpiro(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ +} + +LPESpiro::~LPESpiro() += default; + +void +LPESpiro::doEffect(SPCurve * curve) +{ + sp_spiro_do_effect(curve); +} + +void sp_spiro_do_effect(SPCurve *curve){ + using Geom::X; + using Geom::Y; + + // Make copy of old path as it is changed during processing + Geom::PathVector const original_pathv = curve->get_pathvector(); + guint len = curve->get_segment_count() + 2; + + curve->reset(); + Spiro::spiro_cp *path = g_new (Spiro::spiro_cp, len); + int ip = 0; + + for(const auto & path_it : original_pathv) { + if (path_it.empty()) + continue; + + // start of path + { + Geom::Point p = path_it.initialPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + path[ip].ty = '{' ; // for closed paths, this is overwritten + ip++; + } + + // midpoints + Geom::Path::const_iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); // outgoing curve + Geom::Path::const_iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + + while ( curve_it2 != curve_endit ) + { + /* This deals with the node between curve_it1 and curve_it2. + * Loop to end_default (so without last segment), loop ends when curve_it2 hits the end + * and then curve_it1 points to end or closing segment */ + Geom::Point p = curve_it1->finalPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + + // Determine type of spiro node this is, determined by the tangents (angles) of the curves + // TODO: see if this can be simplified by using /helpers/geom-nodetype.cpp:get_nodetype + bool this_is_line = is_straight_curve(*curve_it1); + bool next_is_line = is_straight_curve(*curve_it2); + + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, *curve_it2); + + if ( nodetype == Geom::NODE_SMOOTH || nodetype == Geom::NODE_SYMM ) + { + if (this_is_line && !next_is_line) { + path[ip].ty = ']'; + } else if (next_is_line && !this_is_line) { + path[ip].ty = '['; + } else { + path[ip].ty = 'c'; + } + } else { + path[ip].ty = 'v'; + } + + ++curve_it1; + ++curve_it2; + ip++; + } + + // add last point to the spiropath + Geom::Point p = curve_it1->finalPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + if (path_it.closed()) { + // curve_it1 points to the (visually) closing segment. determine the match between first and this last segment (the closing node) + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, path_it.front()); + switch (nodetype) { + case Geom::NODE_NONE: // can't happen! but if it does, it means the path isn't closed :-) + path[ip].ty = '}'; + ip++; + break; + case Geom::NODE_CUSP: + path[0].ty = path[ip].ty = 'v'; + break; + case Geom::NODE_SMOOTH: + case Geom::NODE_SYMM: + path[0].ty = path[ip].ty = 'c'; + break; + } + } else { + // set type to path closer + path[ip].ty = '}'; + ip++; + } + + // run subpath through spiro + int sp_len = ip; + Spiro::spiro_run(path, sp_len, *curve); + ip = 0; + } + + g_free (path); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-spiro.h b/src/live_effects/lpe-spiro.h new file mode 100644 index 0000000..ce07de8 --- /dev/null +++ b/src/live_effects/lpe-spiro.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SPIRO_H +#define INKSCAPE_LPE_SPIRO_H + +/* + * Inkscape::LPESpiro + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" + + +namespace Inkscape { +namespace LivePathEffect { + +class LPESpiro : public Effect { +public: + LPESpiro(LivePathEffectObject *lpeobject); + ~LPESpiro() override; + LPESpiro(const LPESpiro&) = delete; + LPESpiro& operator=(const LPESpiro&) = delete; + + LPEPathFlashType pathFlashType() const override { return SUPPRESS_FLASH; } + + void doEffect(SPCurve * curve) override; +}; + +void sp_spiro_do_effect(SPCurve *curve); + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-tangent_to_curve.cpp b/src/live_effects/lpe-tangent_to_curve.cpp new file mode 100644 index 0000000..827ee46 --- /dev/null +++ b/src/live_effects/lpe-tangent_to_curve.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Implementation of tangent-to-curve LPE. + */ + +/* + * Authors: + * Johan Engelen + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-tangent_to_curve.h" +#include "display/curve.h" +#include "knotholder.h" + +#include "object/sp-shape.h" +#include "object/sp-object-group.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +namespace TtC { + +class KnotHolderEntityAttachPt : public LPEKnotHolderEntity { +public: + KnotHolderEntityAttachPt(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity +{ +public: + KnotHolderEntityRightEnd(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace TtC + +LPETangentToCurve::LPETangentToCurve(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + angle(_("Angle:"), _("Additional angle between tangent and curve"), "angle", &wr, this, 0.0), + t_attach(_("Location along curve:"), _("Location of the point of attachment along the curve (between 0.0 and number-of-segments)"), "t_attach", &wr, this, 0.5), + length_left(_("Length left:"), _("Specifies the left end of the tangent"), "length-left", &wr, this, 150), + length_right(_("Length right:"), _("Specifies the right end of the tangent"), "length-right", &wr, this, 150) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter(&angle); + registerParameter(&t_attach); + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPETangentToCurve::~LPETangentToCurve() += default; + +Geom::Piecewise > +LPETangentToCurve::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + Piecewise > output; + + ptA = pwd2_in.valueAt(t_attach); + derivA = unit_vector(derivative(pwd2_in).valueAt(t_attach)); + + // TODO: Why are positive angles measured clockwise, not counterclockwise? + Geom::Rotate rot(Geom::Rotate::from_degrees(-angle)); + derivA = derivA * rot; + + C = ptA - derivA * length_left; + D = ptA + derivA * length_right; + + output = Piecewise >(D2(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output; +} + +void +LPETangentToCurve::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { + { + KnotHolderEntity *e = new TtC::KnotHolderEntityAttachPt(this); +e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the point of attachment of the tangent")); +knotholder->add(e); + } + { + KnotHolderEntity *e = new TtC::KnotHolderEntityLeftEnd(this); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the left end of the tangent")); + knotholder->add(e); + } + { + KnotHolderEntity *e = new TtC::KnotHolderEntityRightEnd(this); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Adjust the right end of the tangent")); + knotholder->add(e); + } +}; + +namespace TtC { + +void +KnotHolderEntityAttachPt::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPETangentToCurve* lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if ( !SP_IS_SHAPE(lpe->sp_lpe_item) ) { + //lpe->t_attach.param_set_value(0); + g_warning("LPEItem is not a path! %s:%d\n", __FILE__, __LINE__); + return; + } + Piecewise > pwd2 = paths_to_pw( lpe->pathvector_before_effect ); + + double t0 = nearest_time(s, pwd2); + lpe->t_attach.param_set_value(t0); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPETangentToCurve *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->derivA); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPETangentToCurve *lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->derivA); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityAttachPt::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast(_effect); + return lpe->ptA; +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast(_effect); + return lpe->C; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast(_effect); + return lpe->D; +} + +} // namespace TtC + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-tangent_to_curve.h b/src/live_effects/lpe-tangent_to_curve.h new file mode 100644 index 0000000..32cacd1 --- /dev/null +++ b/src/live_effects/lpe-tangent_to_curve.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TANGENT_TO_CURVE_H +#define INKSCAPE_LPE_TANGENT_TO_CURVE_H + +/** \file + * LPE implementation, see lpe-tangent_to_curve.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace TtC { + // we need a separate namespace to avoid clashes with LPEPerpBisector + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; + class KnotHolderEntityAttachPt; +} + +class LPETangentToCurve : public Effect { +public: + LPETangentToCurve(LivePathEffectObject *lpeobject); + ~LPETangentToCurve() override; + Geom::Piecewise > + doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + /* the knotholder entity classes must be declared friends */ + friend class TtC::KnotHolderEntityLeftEnd; + friend class TtC::KnotHolderEntityRightEnd; + friend class TtC::KnotHolderEntityAttachPt; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + +private: + ScalarParam angle; + + ScalarParam t_attach; + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point ptA; // point of attachment to the curve + Geom::Point derivA; + + Geom::Point C; // left end of tangent + Geom::Point D; // right end of tangent + + LPETangentToCurve(const LPETangentToCurve&) = delete; + LPETangentToCurve& operator=(const LPETangentToCurve&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-taperstroke.cpp b/src/live_effects/lpe-taperstroke.cpp new file mode 100644 index 0000000..2db2e01 --- /dev/null +++ b/src/live_effects/lpe-taperstroke.cpp @@ -0,0 +1,557 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Taper Stroke path effect, provided as an alternative to Power Strokes + * for otherwise constant-width paths. + * + * Authors: + * Liam P White + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-taperstroke.h" + +#include <2geom/circle.h> +#include <2geom/sbasis-to-bezier.h> + +#include "desktop-style.h" + +#include "helper/geom-nodetype.h" +#include "helper/geom-pathstroke.h" +#include "display/curve.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" + +#include "knotholder.h" + +#include "object/sp-shape.h" +#include "object/sp-object-group.h" +#include "style.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +template +inline bool withinRange(T value, T low, T high) { + return (value > low && value < high); +} + +namespace Inkscape { +namespace LivePathEffect { + +namespace TpS { + class KnotHolderEntityAttachBegin : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachBegin(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; + + class KnotHolderEntityAttachEnd : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachEnd(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; +} // TpS + +static const Util::EnumData JoinType[] = { + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated"), "extrapolated"}, +}; + +static const Util::EnumDataConverter JoinTypeConverter(JoinType, sizeof (JoinType)/sizeof(*JoinType)); + +LPETaperStroke::LPETaperStroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + line_width(_("Stroke width:"), _("The (non-tapered) width of the path"), "stroke_width", &wr, this, 1.), + attach_start(_("Start offset:"), _("Taper distance from path start"), "attach_start", &wr, this, 0.2), + attach_end(_("End offset:"), _("The ending position of the taper"), "end_offset", &wr, this, 0.2), + smoothing(_("Taper smoothing:"), _("Amount of smoothing to apply to the tapers"), "smoothing", &wr, this, 0.5), + join_type(_("Join type:"), _("Join type for non-smooth nodes"), "jointype", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE), + miter_limit(_("Miter limit:"), _("Limit for miter joins"), "miter_limit", &wr, this, 100.) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + attach_start.param_set_digits(3); + attach_end.param_set_digits(3); + + registerParameter(&line_width); + registerParameter(&attach_start); + registerParameter(&attach_end); + registerParameter(&smoothing); + registerParameter(&join_type); + registerParameter(&miter_limit); +} + +// from LPEPowerStroke -- sets fill if stroke color because we will +// be converting to a fill to make the new join. + +void LPETaperStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + line_width.param_transform_multiply(postmul, false); + } +} + +void LPETaperStroke::doOnApply(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPLPEItem* item = const_cast(lpeitem); + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (lpeitem->style->stroke.isPaintserver()) { + SPPaintServer * server = lpeitem->style->getStrokePaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property (css, "fill", str.c_str()); + } + } else if (lpeitem->style->stroke.isColor()) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), lpeitem->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->stroke_opacity.value))); + sp_repr_css_set_property (css, "fill", c); + } else { + sp_repr_css_set_property (css, "fill", "none"); + } + + sp_repr_css_set_property(css, "fill-rule", "nonzero"); + sp_repr_css_set_property(css, "stroke", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)"stroke_width"; + bool valid = prefs->getEntry(pref_path).isValid(); + if(!valid){ + line_width.param_set_value(width); + } + line_width.write_to_SVG(); + } else { + printf("WARNING: It only makes sense to apply Taper stroke to paths (not groups).\n"); + } +} + +// from LPEPowerStroke -- sets stroke color from existing fill color + +void LPETaperStroke::doOnRemove(SPLPEItem const* lpeitem) +{ + if (SP_IS_SHAPE(lpeitem)) { + SPLPEItem *item = const_cast(lpeitem); + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (lpeitem->style->fill.isPaintserver()) { + SPPaintServer * server = lpeitem->style->getFillPaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property (css, "stroke", str.c_str()); + } + } else if (lpeitem->style->fill.isColor()) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), lpeitem->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(lpeitem->style->fill_opacity.value))); + sp_repr_css_set_property (css, "stroke", c); + } else { + sp_repr_css_set_property (css, "stroke", "none"); + } + + Inkscape::CSSOStringStream os; + os << fabs(line_width); + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + + sp_repr_css_set_property(css, "fill", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + } +} + +using Geom::Piecewise; +using Geom::D2; +using Geom::SBasis; +// leave Geom::Path + +static Geom::Path return_at_first_cusp(Geom::Path const & path_in, double /*smooth_tolerance*/ = 0.05) +{ + Geom::Path temp; + + for (unsigned i = 0; i < path_in.size(); i++) { + temp.append(path_in[i]); + if (Geom::get_nodetype(path_in[i], path_in[i + 1]) != Geom::NODE_SMOOTH ) { + break; + } + } + + return temp; +} + +Piecewise > stretch_along(Piecewise > pwd2_in, Geom::Path pattern, double width); + +// actual effect + +Geom::PathVector LPETaperStroke::doEffect_path(Geom::PathVector const& path_in) +{ + Geom::Path first_cusp = return_at_first_cusp(path_in[0]); + Geom::Path last_cusp = return_at_first_cusp(path_in[0].reversed()); + + bool zeroStart = false; // [distance from start taper knot -> start of path] == 0 + bool zeroEnd = false; // [distance from end taper knot -> end of path] == 0 + bool metInMiddle = false; // knots are touching + + // there is a pretty good chance that people will try to drag the knots + // on top of each other, so block it + + unsigned size = path_in[0].size(); + if (size == first_cusp.size()) { + // check to see if the knots were dragged over each other + // if so, reset the end offset, but still allow the start offset. + if ( attach_start >= (size - attach_end) ) { + attach_end.param_set_value( size - attach_start ); + metInMiddle = true; + } + } + + if (attach_start == size - attach_end) { + metInMiddle = true; + } + if (attach_end == size - attach_start) { + metInMiddle = true; + } + + // don't let it be integer (TODO this is stupid!) + { + if (double(unsigned(attach_start)) == attach_start) { + attach_start.param_set_value(attach_start - 0.00001); + } + if (double(unsigned(attach_end)) == attach_end) { + attach_end.param_set_value(attach_end - 0.00001); + } + } + + unsigned allowed_start = first_cusp.size(); + unsigned allowed_end = last_cusp.size(); + + // don't let the knots be farther than they are allowed to be + { + if ((unsigned)attach_start >= allowed_start) { + attach_start.param_set_value((double)allowed_start - 0.00001); + } + if ((unsigned)attach_end >= allowed_end) { + attach_end.param_set_value((double)allowed_end - 0.00001); + } + } + + // don't let it be zero (this is stupid too!) + if (attach_start < 0.0000001 || withinRange(double(attach_start), 0.00000001, 0.000001)) { + attach_start.param_set_value( 0.0000001 ); + zeroStart = true; + } + if (attach_end < 0.0000001 || withinRange(double(attach_end), 0.00000001, 0.000001)) { + attach_end.param_set_value( 0.0000001 ); + zeroEnd = true; + } + + // Path::operator () means get point at time t + start_attach_point = first_cusp(attach_start); + end_attach_point = last_cusp(attach_end); + Geom::PathVector pathv_out; + + // the following function just splits it up into three pieces. + pathv_out = doEffect_simplePath(path_in); + + // now for the actual tapering. the stretch_along method (stolen from PaP) is used to accomplish this + + Geom::PathVector real_pathv; + Geom::Path real_path; + Geom::PathVector pat_vec; + Piecewise > pwd2; + Geom::Path throwaway_path; + + if (!zeroStart) { + // Construct the pattern + std::stringstream pat_str; + pat_str.imbue(std::locale::classic()); + pat_str << "M 1,0 C " << 1 - (double)smoothing << ",0 0,0.5 0,0.5 0,0.5 " << 1 - (double)smoothing << ",1 1,1"; + + pat_vec = sp_svg_read_pathv(pat_str.str().c_str()); + pwd2.concat(stretch_along(pathv_out[0].toPwSb(), pat_vec[0], fabs(line_width))); + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + + real_path.append(throwaway_path); + } + + // if this condition happens to evaluate false, i.e. there was no space for a path to be drawn, it is simply skipped. + // although this seems obvious, it can probably lead to bugs. + if (!metInMiddle) { + // append the outside outline of the path (goes with the direction of the path) + throwaway_path = half_outline(pathv_out[1], fabs(line_width)/2., miter_limit, static_cast(join_type.get_value())); + if (!zeroStart && real_path.size() >= 1 && throwaway_path.size() >= 1) { + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint())) { + real_path.appendNew(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + } + real_path.append(throwaway_path); + } + + if (!zeroEnd) { + // append the ending taper + std::stringstream pat_str_1; + pat_str_1.imbue(std::locale::classic()); + pat_str_1 << "M 0,1 C " << (double)smoothing << ",1 1,0.5 1,0.5 1,0.5 " << double(smoothing) << ",0 0,0"; + pat_vec = sp_svg_read_pathv(pat_str_1.str().c_str()); + + pwd2 = Piecewise >(); + pwd2.concat(stretch_along(pathv_out[2].toPwSb(), pat_vec[0], fabs(line_width))); + + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!metInMiddle) { + // append the inside outline of the path (against direction) + throwaway_path = half_outline(pathv_out[1].reversed(), fabs(line_width)/2., miter_limit, static_cast(join_type.get_value())); + + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!Geom::are_near(real_path.finalPoint(), real_path.initialPoint())) { + real_path.appendNew(real_path.initialPoint()); + } else { + real_path.setFinal(real_path.initialPoint()); + } + real_path.close(); + + real_pathv.push_back(real_path); + + return real_pathv; +} + +/** + * @return Always returns a PathVector with three elements. + * + * The positions of the effect knots are accessed to determine + * where exactly the input path should be split. + */ +Geom::PathVector LPETaperStroke::doEffect_simplePath(Geom::PathVector const & path_in) +{ + Geom::Coord endTime = path_in[0].size() - attach_end; + + Geom::Path p1 = path_in[0].portion(0., attach_start); + Geom::Path p2 = path_in[0].portion(attach_start, endTime); + Geom::Path p3 = path_in[0].portion(endTime, path_in[0].size()); + + Geom::PathVector out; + out.push_back(p1); + out.push_back(p2); + out.push_back(p3); + + return out; +} + + +/** + * Most of the below function is verbatim from Pattern Along Path. However, it needed a little + * tweaking to get it to work right in this case. Also, large portions of the effect have been + * stripped out as I deemed them unnecessary for the relative simplicity of this effect. + */ +Piecewise > stretch_along(Piecewise > pwd2_in, Geom::Path pattern, double prop_scale) +{ + using namespace Geom; + + // Don't allow empty path parameter: + if ( pattern.empty() ) { + return pwd2_in; + } + + /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + Piecewise > output; + std::vector > > pre_output; + + D2 > patternd2 = make_cuts_independent(pattern.toPwSb()); + Piecewise x0 = Piecewise(patternd2[0]); + Piecewise y0 = Piecewise(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + if (pattBndsX && pattBndsY) { + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double noffset = 0; + double toffset = 0; + // Prevent more than 90% overlap... + + y0+=noffset; + + std::vector > > paths_in; + paths_in = split_at_discontinuities(pwd2_in); + + for (auto path_i : paths_in) { + Piecewise x = x0; + Piecewise y = y0; + Piecewise > uskeleton = arc_length_parametrization(path_i,2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + Piecewise > n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.1)); + + int nbCopies = 0; + double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + nbCopies = 1; + + double pattWidth = pattBndsX->extent() * scaling; + + if (scaling != 1.0) { + x*=scaling; + } + if ( false ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + x += toffset; + + double offs = 0; + for (int i=0; i > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); + std::vector > > splited_output_piece = split_at_discontinuities(output_piece); + pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() ); + } else { + output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs)); + } + offs+=pattWidth; + } + } + return output; + } else { + return pwd2_in; + } +} + +void LPETaperStroke::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + KnotHolderEntity *e = new TpS::KnotHolderEntityAttachBegin(this); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("Start point of the taper"), SP_KNOT_SHAPE_CIRCLE); + knotholder->add(e); + + KnotHolderEntity *f = new TpS::KnotHolderEntityAttachEnd(this); + f->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _("End point of the taper"), SP_KNOT_SHAPE_CIRCLE); + knotholder->add(f); +} + +namespace TpS { + +void KnotHolderEntityAttachBegin::knot_set(Geom::Point const &p, Geom::Point const&/*origin*/, guint state) +{ + using namespace Geom; + + LPETaperStroke* lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if (!SP_IS_SHAPE(lpe->sp_lpe_item)) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + SPCurve* curve; + if (!(curve = SP_SHAPE(lpe->sp_lpe_item)->getCurve())) { + // oops + return; + } + // in case you are wondering, the above are simply sanity checks. we never want to actually + // use that object. + + Geom::PathVector pathv = lpe->pathvector_before_effect; + Piecewise > pwd2; + Geom::Path p_in = return_at_first_cusp(pathv[0]); + pwd2.concat(p_in.toPwSb()); + + double t0 = nearest_time(s, pwd2); + lpe->attach_start.param_set_value(t0); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, true); +} +void KnotHolderEntityAttachEnd::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + using namespace Geom; + + LPETaperStroke* lpe = dynamic_cast(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if (!SP_IS_SHAPE(lpe->sp_lpe_item) ) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + SPCurve* curve; + if ( !(curve = SP_SHAPE(lpe->sp_lpe_item)->getCurve()) ) { + // oops + return; + } + Geom::PathVector pathv = lpe->pathvector_before_effect; + Geom::Path p_in = return_at_first_cusp(pathv[0].reversed()); + Piecewise > pwd2 = p_in.toPwSb(); + + double t0 = nearest_time(s, pwd2); + lpe->attach_end.param_set_value(t0); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point KnotHolderEntityAttachBegin::knot_get() const +{ + LPETaperStroke const * lpe = dynamic_cast (_effect); + return lpe->start_attach_point; +} + +Geom::Point KnotHolderEntityAttachEnd::knot_get() const +{ + LPETaperStroke const * lpe = dynamic_cast (_effect); + return lpe->end_attach_point; +} + +} // namespace TpS +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/live_effects/lpe-taperstroke.h b/src/live_effects/lpe-taperstroke.h new file mode 100644 index 0000000..6b4900c --- /dev/null +++ b/src/live_effects/lpe-taperstroke.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Taper Stroke path effect (meant as a replacement for using Power Strokes for tapering) + */ +/* Authors: + * Liam P White + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_TAPERSTROKE_H +#define INKSCAPE_LPE_TAPERSTROKE_H + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace TpS { +// we need a separate namespace to avoid clashes with other LPEs +class KnotHolderEntityAttachBegin; +class KnotHolderEntityAttachEnd; +} + +class LPETaperStroke : public Effect { +public: + LPETaperStroke(LivePathEffectObject *lpeobject); + ~LPETaperStroke() override = default; + + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + + Geom::PathVector doEffect_path (Geom::PathVector const& path_in) override; + Geom::PathVector doEffect_simplePath(Geom::PathVector const& path_in); + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + friend class TpS::KnotHolderEntityAttachBegin; + friend class TpS::KnotHolderEntityAttachEnd; +private: + ScalarParam line_width; + ScalarParam attach_start; + ScalarParam attach_end; + ScalarParam smoothing; + EnumParam join_type; + ScalarParam miter_limit; + + Geom::Point start_attach_point; + Geom::Point end_attach_point; + + LPETaperStroke(const LPETaperStroke&) = delete; + LPETaperStroke& operator=(const LPETaperStroke&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/live_effects/lpe-test-doEffect-stack.cpp b/src/live_effects/lpe-test-doEffect-stack.cpp new file mode 100644 index 0000000..55d707a --- /dev/null +++ b/src/live_effects/lpe-test-doEffect-stack.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-test-doEffect-stack.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + + +LPEdoEffectStackTest::LPEdoEffectStackTest(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + step(_("Stack step:"), ("How deep we should go into the stack"), "step", &wr, this), + point(_("Point param:"), "tooltip of point parameter", "point_param", &wr, this), + path(_("Path param:"), "tooltip of path parameter", "path_param", &wr, this,"M 0,100 100,0") +{ + registerParameter(&step); + registerParameter(&point); + registerParameter(&path); + + point.set_oncanvas_looks(SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR, 0x00ff0000); + point.param_setValue(point); +} + +LPEdoEffectStackTest::~LPEdoEffectStackTest() += default; + +void +LPEdoEffectStackTest::doEffect (SPCurve * curve) +{ + if (step >= 1) { + Effect::doEffect(curve); + } else { + // return here + return; + } +} + +Geom::PathVector +LPEdoEffectStackTest::doEffect_path (Geom::PathVector const &path_in) +{ + if (step >= 2) { + return Effect::doEffect_path(path_in); + } else { + // return here + Geom::PathVector path_out = path_in; + return path_out; + } +} + +Geom::Piecewise > +LPEdoEffectStackTest::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + Geom::Piecewise > output = pwd2_in; + + return output; +} + + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-test-doEffect-stack.h b/src/live_effects/lpe-test-doEffect-stack.h new file mode 100644 index 0000000..1103e89 --- /dev/null +++ b/src/live_effects/lpe-test-doEffect-stack.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DOEFFECT_STACK_H +#define INKSCAPE_LPE_DOEFFECT_STACK_H + +/* + * Inkscape::LPEdoEffectStackTest + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * This effect is to test whether running up and down the doEffect stack does not change the original-d too much. + * i.e. for this effect, the output should match more or less exactly with the input. + * + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/path.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEdoEffectStackTest : public Effect { +public: + LPEdoEffectStackTest(LivePathEffectObject *lpeobject); + ~LPEdoEffectStackTest() override; + + void doEffect (SPCurve * curve) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + ScalarParam step; + PointParam point; + PathParam path; + + LPEdoEffectStackTest(const LPEdoEffectStackTest&) = delete; + LPEdoEffectStackTest& operator=(const LPEdoEffectStackTest&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-text_label.cpp b/src/live_effects/lpe-text_label.cpp new file mode 100644 index 0000000..aa50296 --- /dev/null +++ b/src/live_effects/lpe-text_label.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE implementation + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-text_label.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +LPETextLabel::LPETextLabel(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + label(_("Label:"), _("Text label attached to the path"), "label", &wr, this, "This is a label") +{ + registerParameter(&label); +} + +LPETextLabel::~LPETextLabel() += default; + +Geom::Piecewise > +LPETextLabel::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + using namespace Geom; + + double t = (pwd2_in.cuts.front() + pwd2_in.cuts.back()) / 2; + Point pos(pwd2_in.valueAt(t)); + Point dir(unit_vector(derivative(pwd2_in).valueAt(t))); + Point n(-rot90(dir) * 30); + + double angle = angle_between(dir, Point(1,0)); + label.setPos(pos + n); + label.setAnchor(std::sin(angle), -std::cos(angle)); + + return pwd2_in; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-text_label.h b/src/live_effects/lpe-text_label.h new file mode 100644 index 0000000..61fff9a --- /dev/null +++ b/src/live_effects/lpe-text_label.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TEXT_LABEL_H +#define INKSCAPE_LPE_TEXT_LABEL_H + +/** \file + * LPE implementation + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/text.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPETextLabel : public Effect { +public: + LPETextLabel(LivePathEffectObject *lpeobject); + ~LPETextLabel() override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + +private: + TextParam label; + + LPETextLabel(const LPETextLabel&) = delete; + LPETextLabel& operator=(const LPETextLabel&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-transform_2pts.cpp b/src/live_effects/lpe-transform_2pts.cpp new file mode 100644 index 0000000..a426018 --- /dev/null +++ b/src/live_effects/lpe-transform_2pts.cpp @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Transform through 2 points" implementation + */ + +/* + * Authors: + * Jabier Arraiza Cenoz + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/lpe-transform_2pts.h" +#include "object/sp-path.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "verbs.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include + + +namespace Inkscape { +namespace LivePathEffect { + +LPETransform2Pts::LPETransform2Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + elastic(_("Elastic"), _("Elastic transform mode"), "elastic", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + from_original_width(_("From original width"), _("From original width"), "from_original_width", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + lock_length(_("Lock length"), _("Lock length to current distance"), "lock_length", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + lock_angle(_("Lock angle"), _("Lock angle"), "lock_angle", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + flip_horizontal(_("Flip horizontal"), _("Flip horizontal"), "flip_horizontal", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + flip_vertical(_("Flip vertical"), _("Flip vertical"), "flip_vertical", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + start(_("Start"), _("Start point"), "start", &wr, this, "Start point"), + end(_("End"), _("End point"), "end", &wr, this, "End point"), + stretch(_("Stretch"), _("Stretch the result"), "stretch", &wr, this, 1), + offset(_("Offset"), _("Offset from knots"), "offset", &wr, this, 0), + first_knot(_("First Knot"), _("First Knot"), "first_knot", &wr, this, 1), + last_knot(_("Last Knot"), _("Last Knot"), "last_knot", &wr, this, 1), + helper_size(_("Helper size:"), _("Rotation helper size"), "helper_size", &wr, this, 3), + from_original_width_toggler(false), + point_a(Geom::Point()), + point_b(Geom::Point()), + pathvector(), + append_path(false), + previous_angle(Geom::rad_from_deg(0)), + previous_start(Geom::Point()), + previous_length(-1) +{ + + registerParameter(&first_knot); + registerParameter(&last_knot); + registerParameter(&helper_size); + registerParameter(&stretch); + registerParameter(&offset); + registerParameter(&start); + registerParameter(&end); + registerParameter(&elastic); + registerParameter(&from_original_width); + registerParameter(&flip_vertical); + registerParameter(&flip_horizontal); + registerParameter(&lock_length); + registerParameter(&lock_angle); + + first_knot.param_make_integer(true); + first_knot.param_set_undo(false); + last_knot.param_make_integer(true); + last_knot.param_set_undo(false); + helper_size.param_set_range(0, 999); + helper_size.param_set_increments(1, 1); + helper_size.param_set_digits(0); + offset.param_set_range(-999999.0, 999999.0); + offset.param_set_increments(1, 1); + offset.param_set_digits(2); + stretch.param_set_range(0, 999.0); + stretch.param_set_increments(0.01, 0.01); + stretch.param_set_digits(4); + apply_to_clippath_and_mask = true; +} + +LPETransform2Pts::~LPETransform2Pts() += default; + +void +LPETransform2Pts::doOnApply(SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + point_a = Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Point(boundingbox_X.max(), boundingbox_Y.middle()); + SPLPEItem * splpeitem = const_cast(lpeitem); + SPPath *sp_path = dynamic_cast(splpeitem); + if (sp_path) { + pathvector = sp_path->getCurveForEdit(true)->get_pathvector(); + } + if(!pathvector.empty()) { + point_a = pathvector.initialPoint(); + point_b = pathvector.finalPoint(); + if(are_near(point_a,point_b)) { + point_b = pathvector.back().finalCurve().initialPoint(); + } + size_t nnodes = nodeCount(pathvector); + last_knot.param_set_value(nnodes); + } + + previous_length = Geom::distance(point_a,point_b); + Geom::Ray transformed(point_a,point_b); + previous_angle = transformed.angle(); + start.param_update_default(point_a); + start.param_set_default(); + end.param_update_default(point_b); + end.param_set_default(); +} + +void LPETransform2Pts::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + start.param_transform_multiply(postmul, false); + end.param_transform_multiply(postmul, false); +} + +void +LPETransform2Pts::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + point_a = Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Point(boundingbox_X.max(), boundingbox_Y.middle()); + + SPLPEItem * splpeitem = const_cast(lpeitem); + SPPath *sp_path = dynamic_cast(splpeitem); + if (sp_path) { + pathvector = sp_path->getCurveForEdit(true)->get_pathvector(); + } + if(from_original_width_toggler != from_original_width) { + from_original_width_toggler = from_original_width; + reset(); + } + if(!pathvector.empty() && !from_original_width) { + append_path = false; + point_a = pointAtNodeIndex(pathvector,(size_t)first_knot-1); + point_b = pointAtNodeIndex(pathvector,(size_t)last_knot-1); + size_t nnodes = nodeCount(pathvector); + first_knot.param_set_range(1, last_knot-1); + last_knot.param_set_range(first_knot+1, nnodes); + if (from_original_width){ + from_original_width.param_setValue(false); + } + } else { + if (first_knot != 1){ + first_knot.param_set_value(1); + } + if (last_knot != 2){ + last_knot.param_set_value(2); + } + first_knot.param_set_range(1,1); + last_knot.param_set_range(2,2); + append_path = false; + if (!from_original_width){ + from_original_width.param_setValue(true); + } + } + if(lock_length && !lock_angle && previous_length != -1) { + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + if(previous_start == start || previous_angle == Geom::rad_from_deg(0)) { + previous_angle = transformed.angle(); + } + } else if(lock_angle && !lock_length && previous_angle != Geom::rad_from_deg(0)) { + if(previous_start == start){ + previous_length = Geom::distance((Geom::Point)start, (Geom::Point)end); + } + } + if(lock_length || lock_angle ) { + Geom::Point end_point = Geom::Point::polar(previous_angle, previous_length) + (Geom::Point)start; + end.param_setValue(end_point); + } + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + previous_angle = transformed.angle(); + previous_length = Geom::distance((Geom::Point)start, (Geom::Point)end); + previous_start = start; +} + +void +LPETransform2Pts::updateIndex() +{ + SPLPEItem * splpeitem = const_cast(sp_lpe_item); + SPPath *sp_path = dynamic_cast(splpeitem); + if (sp_path) { + pathvector = sp_path->getCurveForEdit(true)->get_pathvector(); + } + if(pathvector.empty()) { + return; + } + if(!from_original_width) { + point_a = pointAtNodeIndex(pathvector,(size_t)first_knot-1); + point_b = pointAtNodeIndex(pathvector,(size_t)last_knot-1); + start.param_update_default(point_a); + start.param_set_default(); + end.param_update_default(point_b); + end.param_set_default(); + start.param_update_default(point_a); + end.param_update_default(point_b); + start.param_set_default(); + end.param_set_default(); + } + DocumentUndo::done(getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change index of knot")); +} +//todo migrate to PathVector class? +size_t +LPETransform2Pts::nodeCount(Geom::PathVector pathvector) const +{ + size_t n = 0; + for (auto & it : pathvector) { + n += count_path_nodes(it); + } + return n; +} +//todo migrate to PathVector class? +Geom::Point +LPETransform2Pts::pointAtNodeIndex(Geom::PathVector pathvector, size_t index) const +{ + size_t n = 0; + for (auto & pv_it : pathvector) { + for (Geom::Path::iterator curve_it = pv_it.begin(); curve_it != pv_it.end_closed(); ++curve_it) { + if(index == n) { + return curve_it->initialPoint(); + } + n++; + } + } + return Geom::Point(); +} +//todo migrate to PathVector class? Not used +Geom::Path +LPETransform2Pts::pathAtNodeIndex(Geom::PathVector pathvector, size_t index) const +{ + size_t n = 0; + for (auto & pv_it : pathvector) { + for (Geom::Path::iterator curve_it = pv_it.begin(); curve_it != pv_it.end_closed(); ++curve_it) { + if(index == n) { + return pv_it; + } + n++; + } + } + return Geom::Path(); +} + + +void +LPETransform2Pts::reset() +{ + point_a = Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Geom::Point(boundingbox_X.max(), boundingbox_Y.middle()); + if(!pathvector.empty() && !from_original_width) { + size_t nnodes = nodeCount(pathvector); + first_knot.param_set_range(1, last_knot-1); + last_knot.param_set_range(first_knot+1, nnodes); + first_knot.param_set_value(1); + last_knot.param_set_value(nnodes); + point_a = pathvector.initialPoint(); + point_b = pathvector.finalPoint(); + } else { + first_knot.param_set_value(1); + last_knot.param_set_value(2); + } + offset.param_set_value(0.0); + stretch.param_set_value(1.0); + Geom::Ray transformed(point_a, point_b); + previous_angle = transformed.angle(); + previous_length = Geom::distance(point_a, point_b); + start.param_update_default(point_a); + end.param_update_default(point_b); + start.param_set_default(); + end.param_set_default(); +} + +Gtk::Widget *LPETransform2Pts::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + + std::vector::iterator it = param_vector.begin(); + Gtk::HBox * button1 = Gtk::manage(new Gtk::HBox(true,0)); + Gtk::HBox * button2 = Gtk::manage(new Gtk::HBox(true,0)); + Gtk::HBox * button3 = Gtk::manage(new Gtk::HBox(true,0)); + Gtk::HBox * button4 = Gtk::manage(new Gtk::HBox(true,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (param->param_key == "first_knot" || param->param_key == "last_knot") { + Inkscape::UI::Widget::Scalar *registered_widget = Gtk::manage(dynamic_cast(widg)); + registered_widget->signal_value_changed().connect(sigc::mem_fun(*this, &LPETransform2Pts::updateIndex)); + widg = registered_widget; + if (widg) { + Gtk::HBox *hbox_scalar = dynamic_cast(widg); + std::vector child_list = hbox_scalar->get_children(); + Gtk::Entry *entry_widget = dynamic_cast(child_list[1]); + entry_widget->set_width_chars(3); + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "from_original_width" || param->param_key == "elastic") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button1->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "flip_horizontal" || param->param_key == "flip_vertical") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button2->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "lock_angle" || param->param_key == "lock_length") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button3->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + Gtk::Button *reset = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset")))); + reset->signal_clicked().connect(sigc::mem_fun(*this, &LPETransform2Pts::reset)); + button4->pack_start(*reset, true, true, 2); + vbox->pack_start(*button1, true, true, 2); + vbox->pack_start(*button2, true, true, 2); + vbox->pack_start(*button3, true, true, 2); + vbox->pack_start(*button4, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast(vbox); +} + +Geom::Piecewise > +LPETransform2Pts::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) +{ + Geom::Piecewise > output; + double sca = Geom::distance((Geom::Point)start,(Geom::Point)end)/Geom::distance(point_a,point_b); + Geom::Ray original(point_a,point_b); + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + double rot = transformed.angle() - original.angle(); + Geom::Path helper; + helper.start(point_a); + helper.appendNew(point_b); + Geom::Affine m; + Geom::Angle original_angle = original.angle(); + if(flip_horizontal && flip_vertical){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(-1,-1); + m *= Geom::Rotate(original_angle); + } else if(flip_vertical){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(1,-1); + m *= Geom::Rotate(original_angle); + } else if(flip_horizontal){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(-1,1); + m *= Geom::Rotate(original_angle); + } + if(stretch != 1){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(1,stretch); + m *= Geom::Rotate(original_angle); + } + if(elastic) { + m *= Geom::Rotate(-original_angle); + if(sca > 1){ + m *= Geom::Scale(sca, 1.0); + } else { + m *= Geom::Scale(sca, 1.0-((1.0-sca)/2.0)); + } + m *= Geom::Rotate(transformed.angle()); + } else { + m *= Geom::Scale(sca); + m *= Geom::Rotate(rot); + } + helper *= m; + Geom::Point trans = (Geom::Point)start - helper.initialPoint(); + if(flip_horizontal){ + trans = (Geom::Point)end - helper.initialPoint(); + } + if(offset != 0){ + trans = Geom::Point::polar(transformed.angle() + Geom::rad_from_deg(-90),offset) + trans; + } + m *= Geom::Translate(trans); + + output.concat(pwd2_in * m); + + return output; +} + +void +LPETransform2Pts::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path hp; + hp.start((Geom::Point)start); + hp.appendNew((Geom::Point)end); + Geom::PathVector pathv; + pathv.push_back(hp); + double r = helper_size*.1; + if(lock_length || lock_angle ) { + char const * svgd; + svgd = "M -5.39,8.78 -9.13,5.29 -10.38,10.28 Z M -7.22,7.07 -3.43,3.37 m -1.95,-12.16 -3.74,3.5 -1.26,-5 z m -1.83,1.71 3.78,3.7 M 5.24,8.78 8.98,5.29 10.24,10.28 Z M 7.07,7.07 3.29,3.37 M 5.24,-8.78 l 3.74,3.5 1.26,-5 z M 7.07,-7.07 3.29,-3.37"; + PathVector pathv_move = sp_svg_read_pathv(svgd); + pathv_move *= Affine(r,0,0,r,0,0) * Translate(Geom::Point(start)); + hp_vec.push_back(pathv_move); + } + if(!lock_angle && lock_length) { + char const * svgd; + svgd = "M 0,9.94 C -2.56,9.91 -5.17,8.98 -7.07,7.07 c -3.91,-3.9 -3.91,-10.24 0,-14.14 1.97,-1.97 4.51,-3.02 7.07,-3.04 2.56,0.02 5.1,1.07 7.07,3.04 3.91,3.9 3.91,10.24 0,14.14 C 5.17,8.98 2.56,9.91 0,9.94 Z"; + PathVector pathv_turn = sp_svg_read_pathv(svgd); + pathv_turn *= Geom::Rotate(previous_angle); + pathv_turn *= Affine(r,0,0,r,0,0) * Translate(Geom::Point(end)); + hp_vec.push_back(pathv_turn); + } + hp_vec.push_back(pathv); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-transform_2pts.h b/src/live_effects/lpe-transform_2pts.h new file mode 100644 index 0000000..d72e69a --- /dev/null +++ b/src/live_effects/lpe-transform_2pts.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TRANSFORM_2PTS_H +#define INKSCAPE_LPE_TRANSFORM_2PTS_H + +/** \file + * LPE "Transform through 2 points" implementation + */ + +/* + * Authors: + * + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/togglebutton.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPETransform2Pts : public Effect, GroupBBoxEffect { +public: + LPETransform2Pts(LivePathEffectObject *lpeobject); + ~LPETransform2Pts() override; + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise > doEffect_pwd2 (Geom::Piecewise > const & pwd2_in) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + Gtk::Widget *newWidget() override; + + void updateIndex(); + + size_t nodeCount(Geom::PathVector pathvector) const; + + Geom::Point pointAtNodeIndex(Geom::PathVector pathvector, size_t index) const; + + Geom::Path pathAtNodeIndex(Geom::PathVector pathvector, size_t index) const; + + void reset(); + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + +private: + ToggleButtonParam elastic; + ToggleButtonParam from_original_width; + ToggleButtonParam lock_length; + ToggleButtonParam lock_angle; + ToggleButtonParam flip_horizontal; + ToggleButtonParam flip_vertical; + PointParam start; + PointParam end; + ScalarParam stretch; + ScalarParam offset; + ScalarParam first_knot; + ScalarParam last_knot; + ScalarParam helper_size; + bool from_original_width_toggler; + Geom::Point point_a; + Geom::Point point_b; + Geom::PathVector pathvector; + bool append_path; + Geom::Angle previous_angle; + Geom::Point previous_start; + double previous_length; + LPETransform2Pts(const LPETransform2Pts&) = delete; + LPETransform2Pts& operator=(const LPETransform2Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-vonkoch.cpp b/src/live_effects/lpe-vonkoch.cpp new file mode 100644 index 0000000..8936bd8 --- /dev/null +++ b/src/live_effects/lpe-vonkoch.cpp @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) JF Barraud 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/lpe-vonkoch.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include + +namespace Inkscape { +namespace LivePathEffect { + +void +VonKochPathParam::param_setup_nodepath(Inkscape::NodePath::Path *np) +{ + PathParam::param_setup_nodepath(np); + //sp_nodepath_make_straight_path(np); +} + +//FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. +void +VonKochRefPathParam::param_setup_nodepath(Inkscape::NodePath::Path *np) +{ + PathParam::param_setup_nodepath(np); + //sp_nodepath_make_straight_path(np); +} +bool +VonKochRefPathParam::param_readSVGValue(const gchar * strvalue) +{ + Geom::PathVector old = _pathvector; + bool res = PathParam::param_readSVGValue(strvalue); + if (res && _pathvector.size()==1 && _pathvector.front().size()==1){ + return true; + }else{ + _pathvector = old; + return false; + } +} + +LPEVonKoch::LPEVonKoch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + nbgenerations(_("N_r of generations:"), _("Depth of the recursion --- keep low!!"), "nbgenerations", &wr, this, 1), + generator(_("Generating path:"), _("Path whose segments define the iterated transforms"), "generator", &wr, this, "M0,0 L30,0 M0,10 L10,10 M 20,10 L30,10"), + similar_only(_("_Use uniform transforms only"), _("2 consecutive segments are used to reverse/preserve orientation only (otherwise, they define a general transform)."), "similar_only", &wr, this, false), + drawall(_("Dra_w all generations"), _("If unchecked, draw only the last generation"), "drawall", &wr, this, true), + //,draw_boxes(_("Display boxes"), _("Display boxes instead of paths only"), "draw_boxes", &wr, this, true) + ref_path(_("Reference segment:"), _("The reference segment. Defaults to the horizontal midline of the bbox."), "ref_path", &wr, this, "M0,0 L10,0"), + //refA(_("Ref Start"), _("Left side middle of the reference box"), "refA", &wr, this), + //refB(_("Ref End"), _("Right side middle of the reference box"), "refB", &wr, this), + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + maxComplexity(_("_Max complexity:"), _("Disable effect if the output is too complex"), "maxComplexity", &wr, this, 1000) +{ + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + registerParameter(&ref_path); + //registerParameter(&refA) ); + //registerParameter(&refB) ); + registerParameter(&generator); + registerParameter(&similar_only); + registerParameter(&nbgenerations); + registerParameter(&drawall); + registerParameter(&maxComplexity); + //registerParameter(&draw_boxes) ); + apply_to_clippath_and_mask = true; + nbgenerations.param_make_integer(); + nbgenerations.param_set_range(0, Geom::infinity()); + maxComplexity.param_make_integer(); + maxComplexity.param_set_range(0, Geom::infinity()); +} + +LPEVonKoch::~LPEVonKoch() += default; + +Geom::PathVector +LPEVonKoch::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + + Geom::PathVector generating_path = generator.get_pathvector(); + + if (generating_path.empty()) { + return path_in; + } + + //Collect transform matrices. + Affine m0; + Geom::Path refpath = ref_path.get_pathvector().front(); + Point A = refpath.pointAt(0); + Point B = refpath.pointAt(refpath.size()); + Point u = B-A; + m0 = Affine(u[X], u[Y],-u[Y], u[X], A[X], A[Y]); + + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //Point u = refB-refA; + //m0 = Affine(u[X], u[Y],-u[Y], u[X], refA[X], refA[Y]); + m0 = m0.inverse(); + + std::vector transforms; + for (const auto & i : generating_path){ + Affine m; + if(i.size()==1){ + Point p = i.pointAt(0); + Point u = i.pointAt(1)-p; + m = Affine(u[X], u[Y],-u[Y], u[X], p[X], p[Y]); + m = m0*m; + transforms.push_back(m); + }else if(i.size()>=2){ + Point p = i.pointAt(1); + Point u = i.pointAt(2)-p; + Point v = p-i.pointAt(0); + if (similar_only.get_value()){ + int sign = (u[X]*v[Y]-u[Y]*v[X]>=0?1:-1); + v[X] = -u[Y]*sign; + v[Y] = u[X]*sign; + } + m = Affine(u[X], u[Y],v[X], v[Y], p[X], p[Y]); + m = m0*m; + transforms.push_back(m); + } + } + + if (transforms.empty()){ + return path_in; + } + + //Do nothing if the output is too complex... + int path_in_complexity = 0; + for (const auto & k : path_in){ + path_in_complexity+=k.size(); + } + double complexity = std::pow(transforms.size(), nbgenerations) * path_in_complexity; + if (drawall.get_value()){ + int k = transforms.size(); + if(k>1){ + complexity = (std::pow(k,nbgenerations+1)-1)/(k-1)*path_in_complexity; + }else{ + complexity = nbgenerations*k*path_in_complexity; + } + } + if (complexity > double(maxComplexity)){ + g_warning("VonKoch lpe's output too complex. Effect bypassed."); + return path_in; + } + + //Generate path: + Geom::PathVector pathi = path_in; + Geom::PathVector path_out = path_in; + + for (unsigned i = 0; i &hp_vec) +/*{ + using namespace Geom; + if (draw_boxes.get_value()){ + double ratio = .5; + if (similar_only.get_value()) ratio = boundingbox_Y.extent()/boundingbox_X.extent()/2; + + Point BB1,BB2,BB3,BB4,v; + + //Draw the reference box (ref_path is supposed to consist in one line segment) + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + Geom::Path refpath = ref_path.get_pathvector().front(); + if (refpath.size()==1){ + BB1 = BB4 = refpath.front().pointAt(0); + BB2 = BB3 = refpath.front().pointAt(1); + v = rot90(BB2 - BB1)*ratio; + BB1 -= v; + BB2 -= v; + BB3 += v; + BB4 += v; + Geom::Path refbox(BB1); + refbox.appendNew(BB2); + refbox.appendNew(BB3); + refbox.appendNew(BB4); + refbox.close(); + PathVector refbox_as_vect; + refbox_as_vect.push_back(refbox); + hp_vec.push_back(refbox_as_vect); + } + //Draw the transformed boxes + Geom::PathVector generating_path = generator.get_pathvector(); + for (unsigned i=0;i(BB2); + path.appendNew(BB3); + path.appendNew(BB4); + path.close(); + PathVector pathv; + pathv.push_back(path); + hp_vec.push_back(pathv); + } + } +} +*/ + +void +LPEVonKoch::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + + Geom::PathVector paths = ref_path.get_pathvector(); + Geom::Point A,B; + if (paths.empty()||paths.front().size()==0){ + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //refA.param_setValue( Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()) ); + //refB.param_setValue( Geom::Point(boundingbox_X.max(), boundingbox_Y.middle()) ); + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.max(), boundingbox_Y.middle()); + }else{ + A = paths.front().pointAt(0); + B = paths.front().pointAt(paths.front().size()); + } + if (paths.size()!=1||paths.front().size()!=1){ + Geom::Path tmp_path(A); + tmp_path.appendNew(B); + Geom::PathVector tmp_pathv; + tmp_pathv.push_back(tmp_path); + ref_path.set_new_value(tmp_pathv,true); + } +} + + +void +LPEVonKoch::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + using namespace Geom; + original_bbox(SP_LPE_ITEM(item), false, true); + + Point A,B; + A[Geom::X] = boundingbox_X.min(); + A[Geom::Y] = boundingbox_Y.middle(); + B[Geom::X] = boundingbox_X.max(); + B[Geom::Y] = boundingbox_Y.middle(); + + Geom::PathVector paths,refpaths; + Geom::Path path = Geom::Path(A); + path.appendNew(B); + + refpaths.push_back(path); + ref_path.set_new_value(refpaths, true); + + paths.push_back(path * Affine(1./3,0,0,1./3, A[X]*2./3, A[Y]*2./3 + boundingbox_Y.extent()/2)); + paths.push_back(path * Affine(1./3,0,0,1./3, B[X]*2./3, B[Y]*2./3 + boundingbox_Y.extent()/2)); + generator.set_new_value(paths, true); + + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //refA[Geom::X] = boundingbox_X.min(); + //refA[Geom::Y] = boundingbox_Y.middle(); + //refB[Geom::X] = boundingbox_X.max(); + //refB[Geom::Y] = boundingbox_Y.middle(); + //Geom::PathVector paths; + //Geom::Path path = Geom::Path( (Point) refA); + //path.appendNew( (Point) refB ); + //paths.push_back(path * Affine(1./3,0,0,1./3, refA[X]*2./3, refA[Y]*2./3 + boundingbox_Y.extent()/2)); + //paths.push_back(path * Affine(1./3,0,0,1./3, refB[X]*2./3, refB[Y]*2./3 + boundingbox_Y.extent()/2)); + //paths.push_back(path); + //generator.set_new_value(paths, true); +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-vonkoch.h b/src/live_effects/lpe-vonkoch.h new file mode 100644 index 0000000..7244253 --- /dev/null +++ b/src/live_effects/lpe-vonkoch.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_VONKOCH_H +#define INKSCAPE_LPE_VONKOCH_H + +/* + * Inkscape::LPEVonKoch + * + * Copyright (C) JF Barraud 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class VonKochPathParam : public PathParam{ +public: + VonKochPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"):PathParam(label,tip,key,wr,effect,default_value){} + ~VonKochPathParam() override= default; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + }; + + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. +class VonKochRefPathParam : public PathParam{ +public: + VonKochRefPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"):PathParam(label,tip,key,wr,effect,default_value){} + ~VonKochRefPathParam() override= default; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + bool param_readSVGValue(const gchar * strvalue) override; + }; + +class LPEVonKoch : public Effect, GroupBBoxEffect { +public: + LPEVonKoch(LivePathEffectObject *lpeobject); + ~LPEVonKoch() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* item) override; + + //Useful?? + // protected: + //virtual void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec); + +private: + ScalarParam nbgenerations; + VonKochPathParam generator; + BoolParam similar_only; + BoolParam drawall; + //BoolParam draw_boxes; + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + VonKochRefPathParam ref_path; + // PointParam refA; + // PointParam refB; + ScalarParam maxComplexity; + + LPEVonKoch(const LPEVonKoch&) = delete; + LPEVonKoch& operator=(const LPEVonKoch&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpegroupbbox.cpp b/src/live_effects/lpegroupbbox.cpp new file mode 100644 index 0000000..863b08c --- /dev/null +++ b/src/live_effects/lpegroupbbox.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Steren Giannini 2008 + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document.h" +#include "live_effects/lpegroupbbox.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-item-group.h" +#include "object/sp-lpe-item.h" + +namespace Inkscape { +namespace LivePathEffect { + +/** + * Updates the \c boundingbox_X and \c boundingbox_Y values from the geometric bounding box of \c lpeitem. + * + * @pre lpeitem must have an existing geometric boundingbox (usually this is guaranteed when: \code SP_SHAPE(lpeitem)->curve != NULL \endcode ) + * It's not possible to run LPEs on items without their original-d having a bbox. + * @param lpeitem This is not allowed to be NULL. + * @param absolute Determines whether the bbox should be calculated of the untransformed lpeitem (\c absolute = \c false) + * or of the transformed lpeitem (\c absolute = \c true) using sp_item_i2doc_affine. + * @post Updated values of boundingbox_X and boundingbox_Y. These intervals are set to empty intervals when the precondition is not met. + */ + +Geom::OptRect +GroupBBoxEffect::clip_mask_bbox(SPLPEItem *item, Geom::Affine transform) +{ + Geom::OptRect bbox; + Geom::Affine affine = transform * item->transform; + SPClipPath *clip_path = item->getClipObject(); + if(clip_path) { + bbox.unionWith(clip_path->geometricBounds(affine)); + } + SPMask * mask_path = item->getMaskObject(); + if(mask_path) { + bbox.unionWith(mask_path->visualBounds(affine)); + } + SPGroup * group = dynamic_cast(item); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto iter : item_list) { + SPLPEItem * subitem = dynamic_cast(iter); + if (subitem) { + bbox.unionWith(clip_mask_bbox(subitem, affine)); + } + } + } + return bbox; +} + +void GroupBBoxEffect::original_bbox(SPLPEItem const* lpeitem, bool absolute, bool clip_mask) +{ + // Get item bounding box + Geom::Affine transform; + if (absolute) { + transform = lpeitem->i2doc_affine(); + } + else { + transform = Geom::identity(); + } + + Geom::OptRect bbox = lpeitem->geometricBounds(transform); + if (clip_mask) { + SPLPEItem * item = const_cast(lpeitem); + bbox.unionWith(clip_mask_bbox(item, transform * item->transform.inverse())); + } + if (bbox) { + boundingbox_X = (*bbox)[Geom::X]; + boundingbox_Y = (*bbox)[Geom::Y]; + } else { + boundingbox_X = Geom::Interval(); + boundingbox_Y = Geom::Interval(); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpegroupbbox.h b/src/live_effects/lpegroupbbox.h new file mode 100644 index 0000000..8c71151 --- /dev/null +++ b/src/live_effects/lpegroupbbox.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPEGROUPBBOX_H +#define INKSCAPE_LPEGROUPBBOX_H + +/* + * Inkscape::LivePathEffect_group_bbox + * + * Copyright (C) Steren Giannini 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +class SPLPEItem; + +#include <2geom/interval.h> + +namespace Inkscape { +namespace LivePathEffect { + +class GroupBBoxEffect { +protected: + // Bounding box of the item the path effect is applied on + Geom::Interval boundingbox_X; + Geom::Interval boundingbox_Y; + + //This sets boundingbox_X and boundingbox_Y + Geom::OptRect clip_mask_bbox(SPLPEItem * item, Geom::Affine transform); + void original_bbox(SPLPEItem const* lpeitem, bool absolute = false, bool clip_mask = false); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpeobject-reference.cpp b/src/live_effects/lpeobject-reference.cpp new file mode 100644 index 0000000..3f1eadf --- /dev/null +++ b/src/live_effects/lpeobject-reference.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to the inkscape:live-effect attribute + * + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpeobject-reference.h" + +#include + +#include "bad-uri-exception.h" +#include "live_effects/lpeobject.h" +#include "object/uri.h" + +namespace Inkscape { + +namespace LivePathEffect { + +static void lpeobjectreference_href_changed(SPObject *old_ref, SPObject *ref, LPEObjectReference *lpeobjref); +static void lpeobjectreference_delete_self(SPObject *deleted, LPEObjectReference *lpeobjref); +static void lpeobjectreference_source_modified(SPObject *iSource, guint flags, LPEObjectReference *lpeobjref); + +LPEObjectReference::LPEObjectReference(SPObject* i_owner) : URIReference(i_owner) +{ + owner=i_owner; + lpeobject_href = nullptr; + lpeobject_repr = nullptr; + lpeobject = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(lpeobjectreference_href_changed), this)); // listening to myself, this should be virtual instead + + user_unlink = nullptr; +} + +LPEObjectReference::~LPEObjectReference() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +bool LPEObjectReference::_acceptObject(SPObject * const obj) const +{ + LivePathEffectObject *lpobj = dynamic_cast(obj); + if (lpobj) { + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +void +LPEObjectReference::link(const char *to) +{ + if ( to && !to[0] ) { + quit_listening(); + unlink(); + } else { + if ( !lpeobject_href || ( strcmp(to, lpeobject_href) != 0 ) ) { + if (lpeobject_href) { + g_free(lpeobject_href); + } + lpeobject_href = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +LPEObjectReference::unlink() +{ + if (lpeobject_href) { + g_free(lpeobject_href); + lpeobject_href = nullptr; + } + detach(); +} + +void +LPEObjectReference::start_listening(LivePathEffectObject* to) +{ + if ( to == nullptr ) { + return; + } + lpeobject = to; + lpeobject_repr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&lpeobjectreference_delete_self), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&lpeobjectreference_source_modified), this)); +} + +void +LPEObjectReference::quit_listening() +{ + _modified_connection.disconnect(); + _delete_connection.disconnect(); + lpeobject_repr = nullptr; + lpeobject = nullptr; +} + +static void +lpeobjectreference_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, LPEObjectReference *lpeobjref) +{ + lpeobjref->quit_listening(); + LivePathEffectObject *refobj = dynamic_cast( lpeobjref->getObject() ); + if ( refobj ) { + lpeobjref->start_listening(refobj); + } + if (lpeobjref->owner) { + lpeobjref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void +lpeobjectreference_delete_self(SPObject */*deleted*/, LPEObjectReference *lpeobjref) +{ + lpeobjref->quit_listening(); + lpeobjref->unlink(); + if (lpeobjref->user_unlink) { + lpeobjref->user_unlink(lpeobjref, lpeobjref->owner); + } +} + +static void +lpeobjectreference_source_modified(SPObject */*iSource*/, guint /*flags*/, LPEObjectReference *lpeobjref) +{ +// We dont need to request update when LPE XML is updated +// Retain it temporary, drop if no regression +// SPObject *owner_obj = lpeobjref->owner; +// if (owner_obj) { +// lpeobjref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +// } +} + +} //namespace LivePathEffect + +} // namespace inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpeobject-reference.h b/src/live_effects/lpeobject-reference.h new file mode 100644 index 0000000..a57d01a --- /dev/null +++ b/src/live_effects/lpeobject-reference.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPEOBJECT_REFERENCE_H +#define SEEN_LPEOBJECT_REFERENCE_H + +/* + * The reference corresponding to the inkscape:live-effect attribute + * + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "object/uri-references.h" + +namespace Inkscape { + +namespace XML { +class Node; +} +} + +class LivePathEffectObject; + +namespace Inkscape { + +namespace LivePathEffect { + +class LPEObjectReference : public Inkscape::URIReference { +public: + LPEObjectReference(SPObject *owner); + ~LPEObjectReference() override; + + SPObject *owner; + + // concerning the LPEObject that is referred to: + char *lpeobject_href; + Inkscape::XML::Node *lpeobject_repr; + LivePathEffectObject *lpeobject; + + sigc::connection _modified_connection; + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + void link(const char* to); + void unlink(); + void start_listening(LivePathEffectObject* to); + void quit_listening(); + + void (*user_unlink) (LPEObjectReference *me, SPObject *user); + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + +} //namespace LivePathEffect + +} // namespace inkscape + +#endif /* !SEEN_LPEOBJECT_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpeobject.cpp b/src/live_effects/lpeobject.cpp new file mode 100644 index 0000000..9616731 --- /dev/null +++ b/src/live_effects/lpeobject.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007-2008 + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpeobject.h" + +#include "live_effects/effect.h" + +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "attributes.h" +#include "document.h" + +#include "object/sp-defs.h" + +//#define LIVEPATHEFFECT_VERBOSE + +static void livepatheffect_on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data); + +static Inkscape::XML::NodeEventVector const livepatheffect_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + livepatheffect_on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +LivePathEffectObject::LivePathEffectObject() + : SPObject(), effecttype(Inkscape::LivePathEffect::INVALID_LPE), effecttype_set(false), + lpe(nullptr) +{ +#ifdef LIVEPATHEFFECT_VERBOSE + g_message("Init livepatheffectobject"); +#endif +} + +LivePathEffectObject::~LivePathEffectObject() = default; + +/** + * Virtual build: set livepatheffect attributes from its associated XML node. + */ +void LivePathEffectObject::build(SPDocument *document, Inkscape::XML::Node *repr) { + g_assert(SP_IS_OBJECT(this)); + + SPObject::build(document, repr); + + this->readAttr( "effect" ); + + if (repr) { + repr->addListener (&livepatheffect_repr_events, this); + } + + /* Register ourselves, is this necessary? */ +// document->addResource("path-effect", object); +} + +/** + * Virtual release of livepatheffect members before destruction. + */ +void LivePathEffectObject::release() { + this->getRepr()->removeListenerByData(this); + +/* + if (object->document) { + // Unregister ourselves + sp_document_removeResource(object->document, "livepatheffect", object); + } + + if (gradient->ref) { + gradient->modified_connection.disconnect(); + gradient->ref->detach(); + delete gradient->ref; + gradient->ref = NULL; + } + + gradient->modified_connection.~connection(); +*/ + + if (this->lpe) { + delete this->lpe; + this->lpe = nullptr; + } + + this->effecttype = Inkscape::LivePathEffect::INVALID_LPE; + + SPObject::release(); +} + +/** + * Virtual set: set attribute to value. + */ +void LivePathEffectObject::set(SPAttributeEnum key, gchar const *value) { +#ifdef LIVEPATHEFFECT_VERBOSE + g_print("Set livepatheffect"); +#endif + + switch (key) { + case SP_PROP_PATH_EFFECT: + if (this->lpe) { + delete this->lpe; + this->lpe = nullptr; + } + + if ( value && Inkscape::LivePathEffect::LPETypeConverter.is_valid_key(value) ) { + this->effecttype = Inkscape::LivePathEffect::LPETypeConverter.get_id_from_key(value); + this->lpe = Inkscape::LivePathEffect::Effect::New(this->effecttype, this); + this->effecttype_set = true; + } else { + this->effecttype = Inkscape::LivePathEffect::INVALID_LPE; + this->lpe = nullptr; + this->effecttype_set = false; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + SPObject::set(key, value); +} + +/** + * Virtual write: write object attributes to repr. + */ +Inkscape::XML::Node* LivePathEffectObject::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("inkscape:path-effect"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->lpe) { + repr->setAttributeOrRemoveIfEmpty("effect", Inkscape::LivePathEffect::LPETypeConverter.get_key(this->effecttype)); + + this->lpe->writeParamsToSVG(); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +static void +livepatheffect_on_repr_attr_changed ( Inkscape::XML::Node * /*repr*/, + const gchar *key, + const gchar */*oldval*/, + const gchar *newval, + bool /*is_interactive*/, + void * data ) +{ +#ifdef LIVEPATHEFFECT_VERBOSE + g_print("livepatheffect_on_repr_attr_changed"); +#endif + + if (!data) + return; + + LivePathEffectObject *lpeobj = (LivePathEffectObject*) data; + if (!lpeobj->get_lpe()) + return; + + lpeobj->get_lpe()->setParameter(key, newval); + + lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +// Caution using this function, just compare id and same type of +// effect, we use on clipboard to do not fork in same doc on pastepatheffect +bool LivePathEffectObject::is_similar(LivePathEffectObject *that) +{ + if (this && that) { + const char *thisid = this->getId(); + const char *thatid = that->getId(); + if (!thisid || !thatid || strcmp(thisid, thatid) != 0) { + return false; + } + Inkscape::LivePathEffect::Effect *thislpe = this->get_lpe(); + Inkscape::LivePathEffect::Effect *thatlpe = that->get_lpe(); + if (thatlpe && thislpe && thislpe->getName() != thatlpe->getName()) { + return false; + } + } + return true; +} + +/** + * If this has other users, create a new private duplicate and return it + * returns 'this' when no forking was necessary (and therefore no duplicate was made) + * Check out SPLPEItem::forkPathEffectsIfNecessary ! + */ +LivePathEffectObject *LivePathEffectObject::fork_private_if_necessary(unsigned int nr_of_allowed_users) +{ + if (hrefcount > nr_of_allowed_users) { + SPDocument *doc = this->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *dup_repr = this->getRepr()->duplicate(xml_doc); + + doc->getDefs()->getRepr()->addChild(dup_repr, nullptr); + LivePathEffectObject *lpeobj_new = dynamic_cast(doc->getObjectByRepr(dup_repr)); + + Inkscape::GC::release(dup_repr); + return lpeobj_new; + } + return this; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpeobject.h b/src/live_effects/lpeobject.h new file mode 100644 index 0000000..0787a2e --- /dev/null +++ b/src/live_effects/lpeobject.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_OBJECT_H +#define INKSCAPE_LIVEPATHEFFECT_OBJECT_H + +/* + * Inkscape::LivePathEffect + * + * Copyright (C) Johan Engelen 2007-2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "effect-enum.h" + +#include "object/sp-object.h" + +namespace Inkscape { + namespace XML { + class Node; + struct Document; + } + namespace LivePathEffect { + class Effect; + } +} + +#define LIVEPATHEFFECT(obj) ((LivePathEffectObject*)obj) +#define IS_LIVEPATHEFFECT(obj) (dynamic_cast((SPObject*)obj)) + +class LivePathEffectObject : public SPObject { +public: + LivePathEffectObject(); + ~LivePathEffectObject() override; + + Inkscape::LivePathEffect::EffectType effecttype; + + bool effecttype_set; + // dont check values only structure and ID + bool is_similar(LivePathEffectObject *that); + + LivePathEffectObject * fork_private_if_necessary(unsigned int nr_of_allowed_users = 1); + + /* Note that the returned pointer can be NULL in a valid LivePathEffectObject contained in a valid list of lpeobjects in an lpeitem! + * So one should always check whether the returned value is NULL or not */ + Inkscape::LivePathEffect::Effect * get_lpe() { + return lpe; + } + Inkscape::LivePathEffect::Effect const * get_lpe() const { + return lpe; + }; + + Inkscape::LivePathEffect::Effect *lpe; // this can be NULL in a valid LivePathEffectObject + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/parameter/array.cpp b/src/live_effects/parameter/array.cpp new file mode 100644 index 0000000..376252f --- /dev/null +++ b/src/live_effects/parameter/array.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/array.h" +#include "helper-fns.h" +#include <2geom/coord.h> +#include <2geom/point.h> + +namespace Inkscape { + +namespace LivePathEffect { + +template <> +double +ArrayParam::readsvg(const gchar * str) +{ + double newx = Geom::infinity(); + sp_svg_number_read_d(str, &newx); + return newx; +} + +template <> +float +ArrayParam::readsvg(const gchar * str) +{ + float newx = Geom::infinity(); + sp_svg_number_read_f(str, &newx); + return newx; +} + +template <> +Geom::Point +ArrayParam::readsvg(const gchar * str) +{ + gchar ** strarray = g_strsplit(str, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + return Geom::Point(newx, newy); + } + return Geom::Point(Geom::infinity(),Geom::infinity()); +} + + +template <> +std::vector +ArrayParam >::readsvg(const gchar * str) +{ + std::vector subpath_satellites; + if (!str) { + return subpath_satellites; + } + gchar ** strarray = g_strsplit(str, "@", 0); + gchar ** iter = strarray; + while (*iter != nullptr) { + gchar ** strsubarray = g_strsplit(*iter, ",", 8); + if (*strsubarray[7]) {//steps always > 0 + Satellite *satellite = new Satellite(); + satellite->setSatelliteType(g_strstrip(strsubarray[0])); + satellite->is_time = strncmp(strsubarray[1],"1",1) == 0; + satellite->selected = strncmp(strsubarray[2],"1",1) == 0; + satellite->has_mirror = strncmp(strsubarray[3],"1",1) == 0; + satellite->hidden = strncmp(strsubarray[4],"1",1) == 0; + double amount,angle; + float stepsTmp; + sp_svg_number_read_d(strsubarray[5], &amount); + sp_svg_number_read_d(strsubarray[6], &angle); + sp_svg_number_read_f(g_strstrip(strsubarray[7]), &stepsTmp); + unsigned int steps = (unsigned int)stepsTmp; + satellite->amount = amount; + satellite->angle = angle; + satellite->steps = steps; + subpath_satellites.push_back(*satellite); + } + g_strfreev (strsubarray); + iter++; + } + g_strfreev (strarray); + return subpath_satellites; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/array.h b/src/live_effects/parameter/array.h new file mode 100644 index 0000000..1c9ffc4 --- /dev/null +++ b/src/live_effects/parameter/array.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include + +#include "live_effects/parameter/parameter.h" + +#include "helper/geom-satellite.h" +#include "svg/svg.h" +#include "svg/stringstream.h" + +namespace Inkscape { + +namespace LivePathEffect { + +template +class ArrayParam : public Parameter { +public: + ArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + size_t n = 0 ) + : Parameter(label, tip, key, wr, effect), _vector(n), _default_size(n) + { + + } + + ~ArrayParam() override = default;; + + std::vector const & data() const { + return _vector; + } + + Gtk::Widget * param_newWidget() override { + return nullptr; + } + + bool param_readSVGValue(const gchar * strvalue) override { + _vector.clear(); + gchar ** strarray = g_strsplit(strvalue, "|", 0); + gchar ** iter = strarray; + while (*iter != nullptr) { + _vector.push_back( readsvg(*iter) ); + iter++; + } + g_strfreev (strarray); + return true; + } + void param_update_default(const gchar * default_value) override{}; + Glib::ustring param_getSVGValue() const override { + Inkscape::SVGOStringStream os; + writesvg(os, _vector); + return os.str(); + } + + Glib::ustring param_getDefaultSVGValue() const override { + return ""; + } + + void param_setValue(std::vector const &new_vector) { + _vector = new_vector; + } + + void param_set_default() override { + param_setValue( std::vector(_default_size) ); + } + + void param_set_and_write_new_value(std::vector const &new_vector) { + Inkscape::SVGOStringStream os; + writesvg(os, new_vector); + gchar * str = g_strdup(os.str().c_str()); + param_write_to_repr(str); + g_free(str); + } + +protected: + std::vector _vector; + size_t _default_size; + + void writesvg(SVGOStringStream &str, std::vector const &vector) const { + for (unsigned int i = 0; i < vector.size(); ++i) { + if (i != 0) { + // separate items with pipe symbol + str << " | "; + } + writesvgData(str,vector[i]); + } + } + + void writesvgData(SVGOStringStream &str, float const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, double const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, Geom::Point const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, std::vector const &vector_data) const { + for (size_t i = 0; i < vector_data.size(); ++i) { + if (i != 0) { + // separate items with @ symbol ¿Any other? + str << " @ "; + } + str << vector_data[i].getSatelliteTypeGchar(); + str << ","; + str << vector_data[i].is_time; + str << ","; + str << vector_data[i].selected; + str << ","; + str << vector_data[i].has_mirror; + str << ","; + str << vector_data[i].hidden; + str << ","; + str << vector_data[i].amount; + str << ","; + str << vector_data[i].angle; + str << ","; + str << static_cast(vector_data[i].steps); + } + } + + StorageType readsvg(const gchar * str); + +private: + ArrayParam(const ArrayParam&); + ArrayParam& operator=(const ArrayParam&); +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/bool.cpp b/src/live_effects/parameter/bool.cpp new file mode 100644 index 0000000..d5e77ca --- /dev/null +++ b/src/live_effects/parameter/bool.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "inkscape.h" +#include "verbs.h" +#include "helper-fns.h" +#include + +namespace Inkscape { + +namespace LivePathEffect { + +BoolParam::BoolParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, bool default_value) + : Parameter(label, tip, key, wr, effect), value(default_value), defvalue(default_value) +{ +} + +BoolParam::~BoolParam() += default; + +void +BoolParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +BoolParam::param_update_default(bool const default_value) +{ + defvalue = default_value; +} + +void +BoolParam::param_update_default(const gchar * default_value) +{ + param_update_default(helperfns_read_bool(default_value, defvalue)); +} + +bool +BoolParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(helperfns_read_bool(strvalue, defvalue)); + return true; // not correct: if value is unacceptable, should return false! +} + +Glib::ustring +BoolParam::param_getSVGValue() const +{ + return value ? "true" : "false"; +} + +Glib::ustring +BoolParam::param_getDefaultSVGValue() const +{ + return defvalue ? "true" : "false"; +} + +Gtk::Widget * +BoolParam::param_newWidget() +{ + if(widget_is_visible){ + Inkscape::UI::Widget::RegisteredCheckButton * checkwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredCheckButton( param_label, + param_tooltip, + param_key, + *param_wr, + false, + param_effect->getRepr(), + param_effect->getSPDoc()) ); + + checkwdg->setActive(value); + checkwdg->setProgrammatically = false; + checkwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change bool parameter")); + return dynamic_cast (checkwdg); + } else { + return nullptr; + } +} + +void +BoolParam::param_setValue(bool newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/bool.h b/src/live_effects/parameter/bool.h new file mode 100644 index 0000000..72af921 --- /dev/null +++ b/src/live_effects/parameter/bool.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_BOOL_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_BOOL_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class BoolParam : public Parameter { +public: + BoolParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + bool default_value = false); + ~BoolParam() override; + BoolParam(const BoolParam&) = delete; + BoolParam& operator=(const BoolParam&) = delete; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(bool newvalue); + void param_set_default() override; + void param_update_default(bool const default_value); + void param_update_default(const gchar * default_value) override; + bool get_value() const { return value; }; + inline operator bool() const { return value; }; + +private: + bool value; + bool defvalue; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/colorpicker.cpp b/src/live_effects/parameter/colorpicker.cpp new file mode 100644 index 0000000..d430069 --- /dev/null +++ b/src/live_effects/parameter/colorpicker.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/colorpicker.h" +#include "live_effects/effect.h" +#include "ui/widget/color-picker.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "color.h" +#include "inkscape.h" +#include "svg/stringstream.h" +#include "verbs.h" +#include "document.h" +#include "document-undo.h" + +#include + +namespace Inkscape { + +namespace LivePathEffect { + +ColorPickerParam::ColorPickerParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const guint32 default_color ) + : Parameter(label, tip, key, wr, effect), + value(default_color), + defvalue(default_color) +{ + +} + +void +ColorPickerParam::param_set_default() +{ + param_setValue(defvalue); +} + +static guint32 sp_read_color_alpha(gchar const *str, guint32 def) +{ + guint32 val = 0; + if (str == nullptr) return def; + while ((*str <= ' ') && *str) str++; + if (!*str) return def; + + if (str[0] == '#') { + gint i; + for (i = 1; str[i]; i++) { + int hexval; + if (str[i] >= '0' && str[i] <= '9') + hexval = str[i] - '0'; + else if (str[i] >= 'A' && str[i] <= 'F') + hexval = str[i] - 'A' + 10; + else if (str[i] >= 'a' && str[i] <= 'f') + hexval = str[i] - 'a' + 10; + else + break; + val = (val << 4) + hexval; + } + if (i != 1 + 8) { + return def; + } + } + return val; +} + +void +ColorPickerParam::param_update_default(const gchar * default_value) +{ + defvalue = sp_read_color_alpha(default_value, 0x000000ff); +} + +bool +ColorPickerParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(sp_read_color_alpha(strvalue, 0x000000ff)); + return true; +} + +Glib::ustring +ColorPickerParam::param_getSVGValue() const +{ + gchar c[32]; + sprintf(c, "#%08x", value); + return c; +} + +Glib::ustring +ColorPickerParam::param_getDefaultSVGValue() const +{ + gchar c[32]; + sprintf(c, "#%08x", defvalue); + return c; +} + +Gtk::Widget * +ColorPickerParam::param_newWidget() +{ + Gtk::HBox *hbox = Gtk::manage(new Gtk::HBox()); + + hbox->set_border_width(5); + hbox->set_homogeneous(false); + hbox->set_spacing(2); + Inkscape::UI::Widget::RegisteredColorPicker * colorpickerwdg = + new Inkscape::UI::Widget::RegisteredColorPicker( param_label, + param_label, + param_tooltip, + param_key, + param_key + "_opacity_LPE", + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ); + SPDocument *document = param_effect->getSPDoc(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + colorpickerwdg->setRgba32(value); + DocumentUndo::setUndoSensitive(document, saved); + colorpickerwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change color button parameter")); + hbox->pack_start(*dynamic_cast (colorpickerwdg), true, true); + return dynamic_cast (hbox); +} + +void +ColorPickerParam::param_setValue(const guint32 newvalue) +{ + value = newvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/colorpicker.h b/src/live_effects/parameter/colorpicker.h new file mode 100644 index 0000000..ef917b2 --- /dev/null +++ b/src/live_effects/parameter/colorpicker.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_COLOR_BUTTON_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_COLOR_BUTTON_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class ColorPickerParam : public Parameter { +public: + ColorPickerParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const guint32 default_color = 0x000000ff); + ~ColorPickerParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(guint32 newvalue); + + void param_set_default() override; + + const guint32 get_value() const { return value; }; + +private: + ColorPickerParam(const ColorPickerParam&) = delete; + ColorPickerParam& operator=(const ColorPickerParam&) = delete; + guint32 value; + guint32 defvalue; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/enum.h b/src/live_effects/parameter/enum.h new file mode 100644 index 0000000..2519593 --- /dev/null +++ b/src/live_effects/parameter/enum.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ENUM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ENUM_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-enums.h" +#include +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "verbs.h" + +namespace Inkscape { + +namespace LivePathEffect { + +template class EnumParam : public Parameter { +public: + EnumParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const Util::EnumDataConverter& c, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + E default_value, + bool sort = true) + : Parameter(label, tip, key, wr, effect) + { + enumdataconv = &c; + defvalue = default_value; + value = defvalue; + sorted = sort; + }; + + ~EnumParam() override = default;; + EnumParam(const EnumParam&) = delete; + EnumParam& operator=(const EnumParam&) = delete; + + Gtk::Widget * param_newWidget() override { + Inkscape::UI::Widget::RegisteredEnum *regenum = Gtk::manage ( + new Inkscape::UI::Widget::RegisteredEnum( param_label, param_tooltip, + param_key, *enumdataconv, *param_wr, param_effect->getRepr(), param_effect->getSPDoc(), sorted ) ); + + regenum->set_active_by_id(value); + regenum->combobox()->setProgrammatically = false; + regenum->combobox()->signal_changed().connect(sigc::mem_fun (*this, &EnumParam::_on_change_combo)); + regenum->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change enumeration parameter")); + + return dynamic_cast (regenum); + }; + void _on_change_combo() { param_effect->refresh_widgets = true; } + bool param_readSVGValue(const gchar * strvalue) override { + if (!strvalue) { + param_set_default(); + return true; + } + + param_set_value( enumdataconv->get_id_from_key(Glib::ustring(strvalue)) ); + + return true; + }; + Glib::ustring param_getSVGValue() const override { + return enumdataconv->get_key(value); + }; + + Glib::ustring param_getDefaultSVGValue() const override { + return enumdataconv->get_key(defvalue).c_str(); + }; + + E get_value() const { + return value; + } + + inline operator E() const { + return value; + }; + + void param_set_default() override { + param_set_value(defvalue); + } + + void param_update_default(E default_value) { + defvalue = default_value; + } + + void param_update_default(const gchar * default_value) override { + param_update_default(enumdataconv->get_id_from_key(Glib::ustring(default_value))); + } + + void param_set_value(E val) { + value = val; + } + +private: + E value; + E defvalue; + bool sorted; + + const Util::EnumDataConverter * enumdataconv; +}; + + +}; //namespace LivePathEffect + +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/fontbutton.cpp b/src/live_effects/parameter/fontbutton.cpp new file mode 100644 index 0000000..523c7d8 --- /dev/null +++ b/src/live_effects/parameter/fontbutton.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/fontbutton.h" +#include "live_effects/effect.h" +#include "ui/widget/font-button.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "verbs.h" + +#include + +namespace Inkscape { + +namespace LivePathEffect { + +FontButtonParam::FontButtonParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value ) + : Parameter(label, tip, key, wr, effect), + value(default_value), + defvalue(default_value) +{ +} + +void +FontButtonParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +FontButtonParam::param_update_default(const gchar * default_value) +{ + defvalue = Glib::ustring(default_value); +} + +bool +FontButtonParam::param_readSVGValue(const gchar * strvalue) +{ + Inkscape::SVGOStringStream os; + os << strvalue; + param_setValue((Glib::ustring)os.str()); + return true; +} + +Glib::ustring +FontButtonParam::param_getSVGValue() const +{ + return value.c_str(); +} + +Glib::ustring +FontButtonParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + + + +Gtk::Widget * +FontButtonParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredFontButton * fontbuttonwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredFontButton( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + Glib::ustring fontspec = param_getSVGValue(); + fontbuttonwdg->setValue( fontspec); + fontbuttonwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change font button parameter")); + return dynamic_cast (fontbuttonwdg); +} + +void +FontButtonParam::param_setValue(const Glib::ustring newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/fontbutton.h b/src/live_effects/parameter/fontbutton.h new file mode 100644 index 0000000..4f29509 --- /dev/null +++ b/src/live_effects/parameter/fontbutton.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_FONT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_FONT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class FontButtonParam : public Parameter { +public: + FontButtonParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = "Sans 10"); + ~FontButtonParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue); + + void param_set_default() override; + + const Glib::ustring get_value() const { return defvalue; }; + +private: + FontButtonParam(const FontButtonParam&) = delete; + FontButtonParam& operator=(const FontButtonParam&) = delete; + Glib::ustring value; + Glib::ustring defvalue; + +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/hidden.cpp b/src/live_effects/parameter/hidden.cpp new file mode 100644 index 0000000..dcb468e --- /dev/null +++ b/src/live_effects/parameter/hidden.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) jabiertxof 2017 + * Copyright (C) Maximilian Albert 2008 + * + * Authors: + * Jabiertxof + * Maximilian Albert + * Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "live_effects/parameter/hidden.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" + +namespace Inkscape { + +namespace LivePathEffect { + +HiddenParam::HiddenParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value, bool is_visible) + : Parameter(label, tip, key, wr, effect), + value(default_value), + defvalue(default_value) +{ + param_widget_is_visible(is_visible); +} + +void +HiddenParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +HiddenParam::param_update_default(const gchar * default_value) +{ + defvalue = (Glib::ustring)default_value; +} + + +bool +HiddenParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +HiddenParam::param_getSVGValue() const +{ + return value; +} + +Glib::ustring +HiddenParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +Gtk::Widget * +HiddenParam::param_newWidget() +{ + return nullptr; +} + +void +HiddenParam::param_setValue(const Glib::ustring newvalue, bool write) +{ + value = newvalue; + if (write) { + param_write_to_repr(value.c_str()); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/hidden.h b/src/live_effects/parameter/hidden.h new file mode 100644 index 0000000..0a0df17 --- /dev/null +++ b/src/live_effects/parameter/hidden.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_HIDDEN_H +#define INKSCAPE_LIVEPATHEFFECT_HIDDEN_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Jabiertxof + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) jabiertxof 2017 + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +class HiddenParam : public Parameter { +public: + HiddenParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = "", + bool widget_is_visible = false); + ~HiddenParam() override = default; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue, bool write = false); + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + + const Glib::ustring get_value() const { return value; }; + +private: + HiddenParam(const HiddenParam&) = delete; + HiddenParam& operator=(const HiddenParam&) = delete; + Glib::ustring value; + Glib::ustring defvalue; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/item-reference.cpp b/src/live_effects/parameter/item-reference.cpp new file mode 100644 index 0000000..cea4901 --- /dev/null +++ b/src/live_effects/parameter/item-reference.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of LPE Item parameter. + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/item-reference.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-item-group.h" + +namespace Inkscape { +namespace LivePathEffect { + +bool ItemReference::_acceptObject(SPObject * const obj) const +{ + if (SP_IS_SHAPE(obj) || SP_IS_TEXT(obj) || SP_IS_GROUP(obj)) { + /* Refuse references to lpeobject */ + if (obj == getOwner()) { + return false; + } + // TODO: check whether the referred item has this LPE applied, if so: deny deny deny! + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/item-reference.h b/src/live_effects/parameter/item-reference.h new file mode 100644 index 0000000..64aaac0 --- /dev/null +++ b/src/live_effects/parameter/item-reference.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPE_ITEM_REFERENCE_H +#define SEEN_LPE_ITEM_REFERENCE_H + +/* + * Copyright (C) 2008-2012 Authors + * Authors: Johan Engelen + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/uri-references.h" + +class SPItem; +namespace Inkscape { +namespace XML { class Node; } + +namespace LivePathEffect { + +/** + * The reference corresponding to href of LPE ItemParam. + */ +class ItemReference : public Inkscape::URIReference { +public: + ItemReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return (SPItem *)URIReference::getObject(); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +private: + ItemReference(const ItemReference&) = delete; + ItemReference& operator=(const ItemReference&) = delete; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + + + +#endif /* !SEEN_LPE_PATH_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/item.cpp b/src/live_effects/parameter/item.cpp new file mode 100644 index 0000000..e54cb08 --- /dev/null +++ b/src/live_effects/parameter/item.cpp @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/item.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpe-clone-original.h" +#include + +#include +#include + +#include "bad-uri-exception.h" +#include "ui/widget/point.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpe-clone-original.h" +#include "svg/svg.h" + +#include "desktop.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "ui/icon-loader.h" +#include "xml/repr.h" +// clipboard support +#include "ui/clipboard.h" +// required for linking to other paths +#include "object/uri.h" + +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +ItemParam::ItemParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar * default_value) + : Parameter(label, tip, key, wr, effect), + changed(true), + href(nullptr), + ref( (SPObject*)effect->getLPEObj() ) +{ + last_transform = Geom::identity(); + defvalue = g_strdup(default_value); + ref_changed_connection = ref.changedSignal().connect(sigc::mem_fun(*this, &ItemParam::ref_changed)); +} + +ItemParam::~ItemParam() +{ + remove_link(); + g_free(defvalue); +} + +void +ItemParam::param_set_default() +{ + param_readSVGValue(defvalue); +} + +void +ItemParam::param_update_default(const gchar * default_value){ + defvalue = strdup(default_value); +} + +void +ItemParam::param_set_and_write_default() +{ + param_write_to_repr(defvalue); +} + +bool +ItemParam::param_readSVGValue(const gchar * strvalue) +{ + if (strvalue) { + remove_link(); + if (strvalue[0] == '#') { + if (href) + g_free(href); + href = g_strdup(strvalue); + try { + ref.attach(Inkscape::URI(href)); + //lp:1299948 + SPItem* i = ref.getObject(); + if (i) { + linked_modified_callback(i, SP_OBJECT_MODIFIED_FLAG); + } // else: document still processing new events. Repr of the linked object not created yet. + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref.detach(); + } + } + emit_changed(); + return true; + } + + return false; +} + +Glib::ustring +ItemParam::param_getSVGValue() const +{ + if (href) { + return href; + } + return ""; +} + +Glib::ustring +ItemParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +Gtk::Widget * +ItemParam::param_newWidget() +{ + Gtk::HBox * _widget = Gtk::manage(new Gtk::HBox()); + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); + Gtk::Label* pLabel = Gtk::manage(new Gtk::Label(param_label)); + static_cast(_widget)->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &ItemParam::on_link_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to item on clipboard")); + + static_cast(_widget)->show_all_children(); + + return dynamic_cast (_widget); +} + +void +ItemParam::emit_changed() +{ + changed = true; + signal_item_changed.emit(); +} + + +void +ItemParam::addCanvasIndicators(SPLPEItem const*/*lpeitem*/, std::vector &hp_vec) +{ +} + + +void +ItemParam::start_listening(SPObject * to) +{ + if ( to == nullptr ) { + return; + } + linked_delete_connection = to->connectDelete(sigc::mem_fun(*this, &ItemParam::linked_delete)); + linked_modified_connection = to->connectModified(sigc::mem_fun(*this, &ItemParam::linked_modified)); + if (SP_IS_ITEM(to)) { + linked_transformed_connection = SP_ITEM(to)->connectTransformed(sigc::mem_fun(*this, &ItemParam::linked_transformed)); + } + linked_modified(to, SP_OBJECT_MODIFIED_FLAG); // simulate linked_modified signal, so that path data is updated +} + +void +ItemParam::quit_listening() +{ + linked_modified_connection.disconnect(); + linked_delete_connection.disconnect(); + linked_transformed_connection.disconnect(); +} + +void +ItemParam::ref_changed(SPObject */*old_ref*/, SPObject *new_ref) +{ + quit_listening(); + if ( new_ref ) { + start_listening(new_ref); + } +} + +void +ItemParam::remove_link() +{ + if (href) { + ref.detach(); + g_free(href); + href = nullptr; + } +} + +void +ItemParam::linked_delete(SPObject */*deleted*/) +{ + quit_listening(); + remove_link(); +} + +void ItemParam::linked_modified(SPObject *linked_obj, guint flags) +{ + linked_modified_callback(linked_obj, flags); +} + +void ItemParam::linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item) +{ + linked_transformed_callback(rel_transf, moved_item); +} + +void +ItemParam::linked_modified_callback(SPObject *linked_obj, guint /*flags*/) +{ + emit_changed(); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); + last_transform = Geom::identity(); +} + +void +ItemParam::linked_transformed_callback(Geom::Affine const *rel_transf, SPItem *moved_item) +{ + last_transform = *rel_transf; + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); + if (dynamic_cast(param_effect->getLPEObj()->get_lpe())) { + auto hreflist = param_effect->getLPEObj()->hrefList; + SPDesktop * desktop = SP_ACTIVE_DESKTOP; + if (desktop && hreflist.size()) { + Inkscape::Selection *selection = desktop->getSelection(); + SPLPEItem *sp_lpe_item = dynamic_cast(*hreflist.begin()); + SPLPEItem *moved_lpeitem = dynamic_cast(moved_item); + // here use moved item because sp_lpe_item never has optimized transforms because clone LPE + if (sp_lpe_item && !selection->includes(sp_lpe_item) && moved_lpeitem && !last_transform.isTranslation()) { + if (!moved_lpeitem->optimizeTransforms()) { + sp_lpe_item->transform *= last_transform.withoutTranslation(); + } + sp_lpe_item->doWriteTransform(sp_lpe_item->transform); + } + } + } +} + + +void +ItemParam::linkitem(Glib::ustring itemid) +{ + if (itemid.empty()) { + return; + } + + // add '#' at start to make it an uri. + itemid.insert(itemid.begin(), '#'); + if ( href && strcmp(itemid.c_str(), href) == 0 ) { + // no change, do nothing + return; + } else { + // TODO: + // check if id really exists in document, or only in clipboard document: if only in clipboard then invalid + // check if linking to object to which LPE is applied (maybe delegated to PathReference + + param_write_to_repr(itemid.c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link item parameter to path")); + } +} + +void +ItemParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + const gchar * iid = cm->getFirstObjectID(); + if (!iid) { + return; + } + + Glib::ustring itemid(iid); + linkitem(itemid); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/item.h b/src/live_effects/parameter/item.h new file mode 100644 index 0000000..a6a0e36 --- /dev/null +++ b/src/live_effects/parameter/item.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ITEM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ITEM_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/item-reference.h" +#include +#include + +namespace Inkscape { + +namespace LivePathEffect { + +class ItemParam : public Parameter { +public: + ItemParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = ""); + ~ItemParam() override; + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + void param_set_and_write_default(); + void addCanvasIndicators(SPLPEItem const* lpeitem, std::vector &hp_vec) override; + sigc::signal signal_item_pasted; + sigc::signal signal_item_changed; + void linkitem(Glib::ustring itemid); + Geom::Affine last_transform; + bool changed; /* this gets set whenever the path is changed (this is set to true, and then the signal_item_changed signal is emitted). + * the user must set it back to false if she wants to use it sensibly */ +protected: + + gchar * href; // contains link to other object, e.g. "#path2428", NULL if ItemParam contains pathdata itself + ItemReference ref; + sigc::connection ref_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; + void ref_changed(SPObject *old_ref, SPObject *new_ref); + void remove_link(); + void start_listening(SPObject * to); + void quit_listening(); + void linked_delete(SPObject *deleted); + void linked_modified(SPObject *linked_obj, guint flags); + void linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item); + virtual void linked_modified_callback(SPObject *linked_obj, guint flags); + virtual void linked_transformed_callback(Geom::Affine const *rel_transf, SPItem */*moved_item*/); + void on_link_button_click(); + void emit_changed(); + + gchar * defvalue; + +private: + ItemParam(const ItemParam&) = delete; + ItemParam& operator=(const ItemParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/message.cpp b/src/live_effects/parameter/message.cpp new file mode 100644 index 0000000..341fc58 --- /dev/null +++ b/src/live_effects/parameter/message.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +#include "include/gtkmm_version.h" +#include "live_effects/parameter/message.h" +#include "live_effects/effect.h" + +namespace Inkscape { + +namespace LivePathEffect { + +MessageParam::MessageParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar * default_message, Glib::ustring legend, + Gtk::Align halign, Gtk::Align valign, double marginstart, double marginend) + : Parameter(label, tip, key, wr, effect), + message(default_message), + defmessage(default_message), + _legend(std::move(legend)), + _halign(halign), + _valign(valign), + _marginstart(marginstart), + _marginend(marginend) +{ + if (_legend == Glib::ustring("Use Label")) { + _legend = label; + } + _label = nullptr; + _min_height = -1; +} + +void +MessageParam::param_set_default() +{ + param_setValue(defmessage); +} + +void +MessageParam::param_update_default(const gchar * default_message) +{ + defmessage = default_message; +} + +bool +MessageParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +MessageParam::param_getSVGValue() const +{ + return message; +} + +Glib::ustring +MessageParam::param_getDefaultSVGValue() const +{ + return defmessage; +} + +void +MessageParam::param_set_min_height(int height) +{ + _min_height = height; + if (_label) { + _label->set_size_request(-1, _min_height); + } +} + + +Gtk::Widget * +MessageParam::param_newWidget() +{ + Gtk::Frame * frame = new Gtk::Frame (_legend); + Gtk::Widget * widg_frame = frame->get_label_widget(); + + widg_frame->set_margin_end(_marginend); + widg_frame->set_margin_start(_marginstart); + _label = new Gtk::Label (message, Gtk::ALIGN_END); + _label->set_use_underline (true); + _label->set_use_markup(); + _label->set_line_wrap(true); + _label->set_size_request(-1, _min_height); + Gtk::Widget* widg_label = dynamic_cast (_label); + widg_label->set_halign(_halign); + widg_label->set_valign(_valign); + widg_label->set_margin_end(_marginend); + widg_label->set_margin_start(_marginstart); + frame->add(*widg_label); + return dynamic_cast (frame); +} + +void +MessageParam::param_setValue(const gchar * strvalue) +{ + if (strcmp(strvalue, message) != 0) { + param_effect->refresh_widgets = true; + } + message = strvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/message.h b/src/live_effects/parameter/message.h new file mode 100644 index 0000000..a08068c --- /dev/null +++ b/src/live_effects/parameter/message.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_MESSAGE_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_MESSAGE_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class MessageParam : public Parameter { +public: + MessageParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_message = "Default message", + Glib::ustring legend = "Use Label", + Gtk::Align halign = Gtk::ALIGN_START, + Gtk::Align valign = Gtk::ALIGN_CENTER, + double marginstart = 6, + double marginend = 6); + ~MessageParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(const gchar * message); + + void param_set_default() override; + void param_set_min_height(int height); + const gchar * get_value() const { return message; }; + +private: + Gtk::Label * _label; + int _min_height; + MessageParam(const MessageParam&) = delete; + MessageParam& operator=(const MessageParam&) = delete; + const gchar * message; + const gchar * defmessage; + Glib::ustring _legend; + Gtk::Align _halign; + Gtk::Align _valign; + double _marginstart; + double _marginend; + double _marginleft; + double _marginright; + + +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalitem.cpp b/src/live_effects/parameter/originalitem.cpp new file mode 100644 index 0000000..7ad029a --- /dev/null +++ b/src/live_effects/parameter/originalitem.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/parameter/originalitem.h" + +#include +#include +#include + +#include "display/curve.h" +#include "live_effects/effect.h" + +#include "inkscape.h" +#include "desktop.h" +#include "selection.h" + +#include "object/uri.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +OriginalItemParam::OriginalItemParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect) + : ItemParam(label, tip, key, wr, effect, "") +{ +} + +OriginalItemParam::~OriginalItemParam() += default; + +Gtk::Widget * +OriginalItemParam::param_newWidget() +{ + Gtk::HBox *_widget = Gtk::manage(new Gtk::HBox()); + + { // Label + Gtk::Label *pLabel = Gtk::manage(new Gtk::Label(param_label)); + static_cast(_widget)->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + } + + { // Paste item to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-paste", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemParam::on_link_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to item")); + } + + { // Select original button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-select-original", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemParam::on_select_original_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Select original")); + } + + static_cast(_widget)->show_all_children(); + + return dynamic_cast (_widget); +} + +void +OriginalItemParam::on_select_original_button_click() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPItem *original = ref.getObject(); + if (desktop == nullptr || original == nullptr) { + return; + } + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->set(original); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalitem.h b/src/live_effects/parameter/originalitem.h new file mode 100644 index 0000000..783e5b8 --- /dev/null +++ b/src/live_effects/parameter/originalitem.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_ITEM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_ITEM_H + +/* + * Inkscape::LiveItemEffectParameters + * + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/item.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalItemParam: public ItemParam { +public: + OriginalItemParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + ~OriginalItemParam() override; + bool linksToItem() const { return (href != nullptr); } + SPItem * getObject() const { return ref.getObject(); } + + Gtk::Widget * param_newWidget() override; + +protected: + + void on_select_original_button_click(); + +private: + OriginalItemParam(const OriginalItemParam&) = delete; + OriginalItemParam& operator=(const OriginalItemParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/originalitemarray.cpp b/src/live_effects/parameter/originalitemarray.cpp new file mode 100644 index 0000000..30b25dc --- /dev/null +++ b/src/live_effects/parameter/originalitemarray.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/originalitemarray.h" + +#include +#include +#include +#include +#include + +#include + +#include "inkscape.h" +#include "originalitem.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/clipboard.h" +#include "ui/icon-loader.h" + +#include "object/uri.h" + +#include "live_effects/effect.h" + +#include "verbs.h" +#include "document-undo.h" +#include "document.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalItemArrayParam::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colLabel); + add(_colActive); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colActive; +}; + +OriginalItemArrayParam::OriginalItemArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect ) +: Parameter(label, tip, key, wr, effect), + _vector(), + _tree(), + _text_renderer(), + _toggle_active(), + _scroller() +{ + _model = new ModelColumns(); + _store = Gtk::TreeStore::create(*_model); + _tree.set_model(_store); + + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + Gtk::CellRendererToggle * _toggle_active = manage(new Gtk::CellRendererToggle()); + int activeColNum = _tree.append_column(_("Active"), *_toggle_active) - 1; + Gtk::TreeViewColumn* col_active = _tree.get_column(activeColNum); + _toggle_active->set_activatable(true); + _toggle_active->signal_toggled().connect(sigc::mem_fun(*this, &OriginalItemArrayParam::on_active_toggled)); + col_active->add_attribute(_toggle_active->property_active(), _model->_colActive); + + _text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column(_("Name"), *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(_model->_colLabel); + + //quick little hack -- newer versions of gtk gave the item zero space allotment + _scroller.set_size_request(-1, 120); + + _scroller.add(_tree); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + //_scroller.set_shadow_type(Gtk::SHADOW_IN); + + oncanvas_editable = true; +} + +OriginalItemArrayParam::~OriginalItemArrayParam() +{ + while (!_vector.empty()) { + ItemAndActive *w = _vector.back(); + _vector.pop_back(); + unlink(w); + delete w; + } + delete _model; +} + +void OriginalItemArrayParam::on_active_toggled(const Glib::ustring& item) +{ + Gtk::TreeModel::iterator iter = _store->get_iter(item); + Gtk::TreeModel::Row row = *iter; + ItemAndActive *w = row[_model->_colObject]; + row[_model->_colActive] = !row[_model->_colActive]; + w->actived = row[_model->_colActive]; + + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link item parameter to item")); +} + +void OriginalItemArrayParam::param_set_default() +{ + +} + +Gtk::Widget* OriginalItemArrayParam::param_newWidget() +{ + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + + vbox->pack_start(_scroller, Gtk::PACK_EXPAND_WIDGET); + + + { // Paste item to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemArrayParam::on_link_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Link to item")); + } + + { // Remove linked item + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("list-remove", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemArrayParam::on_remove_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Remove Item")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemArrayParam::on_down_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Down")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalItemArrayParam::on_up_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Up")); + } + + vbox->pack_end(*hbox, Gtk::PACK_SHRINK); + + vbox->show_all_children(true); + + return vbox; +} + +bool OriginalItemArrayParam::_selectIndex(const Gtk::TreeIter& iter, int* i) +{ + if ((*i)-- <= 0) { + _tree.get_selection()->select(iter); + return true; + } + return false; +} + +void OriginalItemArrayParam::on_up_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = -1; + std::vector::iterator piter = _vector.begin(); + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); piter = iter, i++, ++iter) { + if (*iter == row[_model->_colObject]) { + _vector.erase(iter); + _vector.insert(piter, row[_model->_colObject]); + break; + } + } + + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move item up")); + + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::_selectIndex), &i)); + } +} + +void OriginalItemArrayParam::on_down_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = 0; + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); i++, ++iter) { + if (*iter == row[_model->_colObject]) { + std::vector::iterator niter = _vector.erase(iter); + if (niter != _vector.end()) { + ++niter; + i++; + } + _vector.insert(niter, row[_model->_colObject]); + break; + } + } + + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move item down")); + + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::_selectIndex), &i)); + } +} + +void OriginalItemArrayParam::on_remove_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + remove_link(row[_model->_colObject]); + + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Remove item")); + } + +} + +void +OriginalItemArrayParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + //without second parameter populate all elements filled inside the called function + std::vector itemsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "*", 1); + + if (itemsid.empty()) { + return; + } + + bool foundOne = false; + Inkscape::SVGOStringStream os; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->actived ? "1" : "0"); + } + for (auto itemid : itemsid) { + // add '#' at start to make it an uri. + itemid.insert(itemid.begin(), '#'); + + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << itemid.c_str() << ",1"; + } + param_write_to_repr(os.str().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link itemarray parameter to item")); +} + +void OriginalItemArrayParam::unlink(ItemAndActive* to) +{ + to->linked_modified_connection.disconnect(); + to->linked_delete_connection.disconnect(); + to->ref.detach(); + if (to->href) { + g_free(to->href); + to->href = nullptr; + } +} + +void OriginalItemArrayParam::remove_link(ItemAndActive* to) +{ + unlink(to); + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); ++iter) { + if (*iter == to) { + ItemAndActive *w = *iter; + _vector.erase(iter); + delete w; + return; + } + } +} + +void OriginalItemArrayParam::linked_delete(SPObject */*deleted*/, ItemAndActive* to) +{ + remove_link(to); + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); +} + +bool OriginalItemArrayParam::_updateLink(const Gtk::TreeIter& iter, ItemAndActive* pd) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_model->_colObject] == pd) { + SPObject *obj = pd->ref.getObject(); + row[_model->_colLabel] = obj && obj->getId() ? ( obj->label() ? obj->label() : obj->getId() ) : pd->href; + return true; + } + return false; +} + +void OriginalItemArrayParam::linked_changed(SPObject */*old_obj*/, SPObject *new_obj, ItemAndActive* to) +{ + to->linked_delete_connection.disconnect(); + to->linked_modified_connection.disconnect(); + to->linked_transformed_connection.disconnect(); + + if (new_obj && SP_IS_ITEM(new_obj)) { + to->linked_delete_connection = new_obj->connectDelete(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::linked_delete), to)); + to->linked_modified_connection = new_obj->connectModified(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::linked_modified), to)); + to->linked_transformed_connection = SP_ITEM(new_obj)->connectTransformed(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::linked_transformed), to)); + + linked_modified(new_obj, SP_OBJECT_MODIFIED_FLAG, to); + } else { + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::_updateLink), to)); + } +} + +void OriginalItemArrayParam::linked_modified(SPObject *linked_obj, guint flags, ItemAndActive* to) +{ + if (!to) { + return; + } + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::_updateLink), to)); +} + +bool OriginalItemArrayParam::param_readSVGValue(const gchar* strvalue) +{ + if (strvalue) { + while (!_vector.empty()) { + ItemAndActive *w = _vector.back(); + unlink(w); + _vector.pop_back(); + delete w; + } + _store->clear(); + + gchar ** strarray = g_strsplit(strvalue, "|", 0); + for (gchar ** iter = strarray; *iter != nullptr; iter++) { + if ((*iter)[0] == '#') { + gchar ** substrarray = g_strsplit(*iter, ",", 0); + ItemAndActive* w = new ItemAndActive((SPObject *)param_effect->getLPEObj()); + w->href = g_strdup(*substrarray); + w->actived = *(substrarray+1) != nullptr && (*(substrarray+1))[0] == '1'; + w->linked_changed_connection = w->ref.changedSignal().connect(sigc::bind(sigc::mem_fun(*this, &OriginalItemArrayParam::linked_changed), w)); + w->ref.attach(URI(w->href)); + + _vector.push_back(w); + + Gtk::TreeModel::iterator iter = _store->append(); + Gtk::TreeModel::Row row = *iter; + SPObject *obj = w->ref.getObject(); + + row[_model->_colObject] = w; + row[_model->_colLabel] = obj ? ( obj->label() ? obj->label() : obj->getId() ) : w->href; + row[_model->_colActive] = w->actived; + g_strfreev (substrarray); + } + } + g_strfreev (strarray); + return true; + } + return false; +} + +Glib::ustring +OriginalItemArrayParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + bool foundOne = false; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->actived ? "1" : "0"); + } + return os.str(); +} + +Glib::ustring +OriginalItemArrayParam::param_getDefaultSVGValue() const +{ + return ""; +} + +void OriginalItemArrayParam::update() +{ + for (auto & iter : _vector) { + SPObject *linked_obj = iter->ref.getObject(); + linked_modified(linked_obj, SP_OBJECT_MODIFIED_FLAG, iter); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalitemarray.h b/src/live_effects/parameter/originalitemarray.h new file mode 100644 index 0000000..baa7646 --- /dev/null +++ b/src/live_effects/parameter/originalitemarray.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALITEMARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALITEMARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include +#include + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/item-reference.h" + +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "item-reference.h" + +class SPObject; + +namespace Inkscape { + +namespace LivePathEffect { + +class ItemAndActive { +public: + ItemAndActive(SPObject *owner) + : href(nullptr), + ref(owner), + actived(true) + { + + } + gchar *href; + URIReference ref; + bool actived; + + sigc::connection linked_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; +}; + +class OriginalItemArrayParam : public Parameter { +public: + class ModelColumns; + + OriginalItemArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + + ~OriginalItemArrayParam() override; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_update_default(const gchar * default_value) override{}; + /** Disable the canvas indicators of parent class by overriding this method */ + void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/) override {}; + /** Disable the canvas indicators of parent class by overriding this method */ + void addCanvasIndicators(SPLPEItem const* /*lpeitem*/, std::vector & /*hp_vec*/) override {}; + + std::vector _vector; + +protected: + bool _updateLink(const Gtk::TreeIter& iter, ItemAndActive* pd); + bool _selectIndex(const Gtk::TreeIter& iter, int* i); + void unlink(ItemAndActive* to); + void remove_link(ItemAndActive* to); + void setItem(SPObject *linked_obj, guint flags, ItemAndActive* to); + + void linked_changed(SPObject *old_obj, SPObject *new_obj, ItemAndActive* to); + void linked_modified(SPObject *linked_obj, guint flags, ItemAndActive* to); + void linked_transformed(Geom::Affine const *, SPItem *, ItemAndActive*) {} + void linked_delete(SPObject *deleted, ItemAndActive* to); + + ModelColumns *_model; + Glib::RefPtr _store; + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::CellRendererToggle *_toggle_active; + Gtk::TreeView::Column *_name_column; + Gtk::ScrolledWindow _scroller; + + void on_link_button_click(); + void on_remove_button_click(); + void on_up_button_click(); + void on_down_button_click(); + void on_active_toggled(const Glib::ustring& item); + +private: + void update(); + OriginalItemArrayParam(const OriginalItemArrayParam&); + OriginalItemArrayParam& operator=(const OriginalItemArrayParam&); +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalpath.cpp b/src/live_effects/parameter/originalpath.cpp new file mode 100644 index 0000000..69b62d0 --- /dev/null +++ b/src/live_effects/parameter/originalpath.cpp @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "live_effects/parameter/originalpath.h" + +#include +#include +#include + +#include "display/curve.h" +#include "live_effects/effect.h" + +#include "object/uri.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "inkscape.h" +#include "desktop.h" +#include "selection.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +OriginalPathParam::OriginalPathParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect) + : PathParam(label, tip, key, wr, effect, "") +{ + oncanvas_editable = false; + _from_original_d = false; +} + +OriginalPathParam::~OriginalPathParam() += default; + +Gtk::Widget * +OriginalPathParam::param_newWidget() +{ + Gtk::HBox *_widget = Gtk::manage(new Gtk::HBox()); + + { // Label + Gtk::Label *pLabel = Gtk::manage(new Gtk::Label(param_label)); + static_cast(_widget)->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + } + + { // Paste path to link button + Gtk::Image *pIcon = Gtk::manage(new Gtk::Image()); + pIcon->set_from_icon_name("edit-clone", Gtk::ICON_SIZE_BUTTON); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathParam::on_link_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + { // Select original button + Gtk::Image *pIcon = Gtk::manage(new Gtk::Image()); + pIcon->set_from_icon_name("edit-select-original", Gtk::ICON_SIZE_BUTTON); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathParam::on_select_original_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Select original")); + } + + static_cast(_widget)->show_all_children(); + + return dynamic_cast (_widget); +} + +void +OriginalPathParam::linked_modified_callback(SPObject *linked_obj, guint /*flags*/) +{ + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(linked_obj)) { + if (_from_original_d) { + curve = SP_SHAPE(linked_obj)->getCurveForEdit(); + } else { + curve = SP_SHAPE(linked_obj)->getCurve(); + } + } + if (SP_IS_TEXT(linked_obj)) { + curve = SP_TEXT(linked_obj)->getNormalizedBpath(); + } + + if (curve == nullptr) { + // curve invalid, set empty pathvector + _pathvector = Geom::PathVector(); + } else { + _pathvector = curve->get_pathvector(); + curve->unref(); + } + + must_recalculate_pwd2 = true; + emit_changed(); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void +OriginalPathParam::linked_transformed_callback(Geom::Affine const * /*rel_transf*/, SPItem * /*moved_item*/) +{ +/** \todo find good way to compensate for referenced path transform, like done for normal clones. + * See sp-use.cpp: sp_use_move_compensate */ +} + + +void +OriginalPathParam::on_select_original_button_click() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPItem *original = ref.getObject(); + if (desktop == nullptr || original == nullptr) { + return; + } + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->set(original); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalpath.h b/src/live_effects/parameter/originalpath.h new file mode 100644 index 0000000..7bdc23c --- /dev/null +++ b/src/live_effects/parameter/originalpath.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_PATH_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_PATH_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/path.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalPathParam: public PathParam { +public: + OriginalPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + ~OriginalPathParam() override; + + bool linksToPath() const { return (href != nullptr); } + SPItem * getObject() const { return ref.getObject(); } + + Gtk::Widget * param_newWidget() override; + /** Disable the canvas indicators of parent class by overriding this method */ + void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/) override {}; + /** Disable the canvas indicators of parent class by overriding this method */ + void addCanvasIndicators(SPLPEItem const* /*lpeitem*/, std::vector & /*hp_vec*/) override {}; + void setFromOriginalD(bool from_original_d){ _from_original_d = from_original_d; }; + +protected: + void linked_modified_callback(SPObject *linked_obj, guint flags) override; + void linked_transformed_callback(Geom::Affine const *rel_transf, SPItem *moved_item) override; + + void on_select_original_button_click(); + +private: + bool _from_original_d; + OriginalPathParam(const OriginalPathParam&) = delete; + OriginalPathParam& operator=(const OriginalPathParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/originalpatharray.cpp b/src/live_effects/parameter/originalpatharray.cpp new file mode 100644 index 0000000..bee6651 --- /dev/null +++ b/src/live_effects/parameter/originalpatharray.cpp @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/originalpatharray.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" + +#include +#include +#include +#include +#include + +#include + +#include "inkscape.h" +#include "ui/clipboard.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "originalpath.h" +#include "display/curve.h" + +#include <2geom/coord.h> +#include <2geom/point.h> + +#include "ui/icon-loader.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/uri.h" + +#include "live_effects/effect.h" + +#include "verbs.h" +#include "document-undo.h" +#include "document.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalPathArrayParam::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colLabel); + add(_colReverse); + add(_colVisible); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colReverse; + Gtk::TreeModelColumn _colVisible; +}; + +OriginalPathArrayParam::OriginalPathArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect ) +: Parameter(label, tip, key, wr, effect), + _vector(), + _tree(), + _text_renderer(), + _toggle_reverse(), + _toggle_visible(), + _scroller() +{ + _model = new ModelColumns(); + _store = Gtk::TreeStore::create(*_model); + _tree.set_model(_store); + + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + + Gtk::CellRendererToggle * _toggle_reverse = manage(new Gtk::CellRendererToggle()); + int reverseColNum = _tree.append_column(_("Reverse"), *_toggle_reverse) - 1; + Gtk::TreeViewColumn* col_reverse = _tree.get_column(reverseColNum); + _toggle_reverse->set_activatable(true); + _toggle_reverse->signal_toggled().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_reverse_toggled)); + col_reverse->add_attribute(_toggle_reverse->property_active(), _model->_colReverse); + + + Gtk::CellRendererToggle * _toggle_visible = manage(new Gtk::CellRendererToggle()); + int visibleColNum = _tree.append_column(_("Visible"), *_toggle_visible) - 1; + Gtk::TreeViewColumn* col_visible = _tree.get_column(visibleColNum); + _toggle_visible->set_activatable(true); + _toggle_visible->signal_toggled().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_visible_toggled)); + col_visible->add_attribute(_toggle_visible->property_active(), _model->_colVisible); + + _text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column(_("Name"), *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(_model->_colLabel); + + //quick little hack -- newer versions of gtk gave the item zero space allotment + _scroller.set_size_request(-1, 120); + + _scroller.add(_tree); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + //_scroller.set_shadow_type(Gtk::SHADOW_IN); + + oncanvas_editable = true; + _from_original_d = false; + _allow_only_bspline_spiro = false; + +} + +OriginalPathArrayParam::~OriginalPathArrayParam() +{ + while (!_vector.empty()) { + PathAndDirectionAndVisible *w = _vector.back(); + _vector.pop_back(); + unlink(w); + delete w; + } + delete _model; +} + +void OriginalPathArrayParam::on_reverse_toggled(const Glib::ustring& path) +{ + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + PathAndDirectionAndVisible *w = row[_model->_colObject]; + row[_model->_colReverse] = !row[_model->_colReverse]; + w->reversed = row[_model->_colReverse]; + + param_write_to_repr(param_getSVGValue().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link path parameter to path")); +} + +void OriginalPathArrayParam::on_visible_toggled(const Glib::ustring& path) +{ + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + PathAndDirectionAndVisible *w = row[_model->_colObject]; + row[_model->_colVisible] = !row[_model->_colVisible]; + w->visibled = row[_model->_colVisible]; + + param_write_to_repr(param_getSVGValue().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Toggle path parameter visibility")); +} + +void OriginalPathArrayParam::param_set_default() +{ + +} + +Gtk::Widget* OriginalPathArrayParam::param_newWidget() +{ + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + + vbox->pack_start(_scroller, Gtk::PACK_EXPAND_WIDGET); + + + { // Paste path to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_link_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + { // Remove linked path + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("list-remove", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_remove_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Remove Path")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_down_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Down")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathArrayParam::on_up_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Up")); + } + + vbox->pack_end(*hbox, Gtk::PACK_SHRINK); + + vbox->show_all_children(true); + + return vbox; +} + +bool OriginalPathArrayParam::_selectIndex(const Gtk::TreeIter& iter, int* i) +{ + if ((*i)-- <= 0) { + _tree.get_selection()->select(iter); + return true; + } + return false; +} + +void OriginalPathArrayParam::on_up_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = -1; + std::vector::iterator piter = _vector.begin(); + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); piter = iter, i++, ++iter) { + if (*iter == row[_model->_colObject]) { + _vector.erase(iter); + _vector.insert(piter, row[_model->_colObject]); + break; + } + } + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path up")); + + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::_selectIndex), &i)); + } +} + +void OriginalPathArrayParam::on_down_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = 0; + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); i++, ++iter) { + if (*iter == row[_model->_colObject]) { + std::vector::iterator niter = _vector.erase(iter); + if (niter != _vector.end()) { + ++niter; + i++; + } + _vector.insert(niter, row[_model->_colObject]); + break; + } + } + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path down")); + + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::_selectIndex), &i)); + } +} + +void OriginalPathArrayParam::on_remove_button_click() +{ + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + remove_link(row[_model->_colObject]); + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Remove path")); + } + +} + +void +OriginalPathArrayParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + std::vector pathsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:path"); + std::vector textsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:text"); + pathsid.insert(pathsid.end(), textsid.begin(), textsid.end()); + if (pathsid.empty()) { + return; + } + bool foundOne = false; + Inkscape::SVGOStringStream os; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); + } + for (auto pathid : pathsid) { + // add '#' at start to make it an uri. + pathid.insert(pathid.begin(), '#'); + + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << pathid.c_str() << ",0,1"; + } + param_write_to_repr(os.str().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link patharray parameter to path")); +} + +void OriginalPathArrayParam::unlink(PathAndDirectionAndVisible* to) +{ + to->linked_modified_connection.disconnect(); + to->linked_delete_connection.disconnect(); + to->ref.detach(); + to->_pathvector = Geom::PathVector(); + if (to->href) { + g_free(to->href); + to->href = nullptr; + } +} + +void OriginalPathArrayParam::remove_link(PathAndDirectionAndVisible* to) +{ + unlink(to); + for (std::vector::iterator iter = _vector.begin(); iter != _vector.end(); ++iter) { + if (*iter == to) { + PathAndDirectionAndVisible *w = *iter; + _vector.erase(iter); + delete w; + return; + } + } +} + +void OriginalPathArrayParam::linked_delete(SPObject */*deleted*/, PathAndDirectionAndVisible* /*to*/) +{ + //remove_link(to); + + param_write_to_repr(param_getSVGValue().c_str()); +} + +bool OriginalPathArrayParam::_updateLink(const Gtk::TreeIter& iter, PathAndDirectionAndVisible* pd) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_model->_colObject] == pd) { + SPObject *obj = pd->ref.getObject(); + row[_model->_colLabel] = obj && obj->getId() ? ( obj->label() ? obj->label() : obj->getId() ) : pd->href; + return true; + } + return false; +} + +void OriginalPathArrayParam::linked_changed(SPObject */*old_obj*/, SPObject *new_obj, PathAndDirectionAndVisible* to) +{ + to->linked_delete_connection.disconnect(); + to->linked_modified_connection.disconnect(); + to->linked_transformed_connection.disconnect(); + + if (new_obj && SP_IS_ITEM(new_obj)) { + to->linked_delete_connection = new_obj->connectDelete(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::linked_delete), to)); + to->linked_modified_connection = new_obj->connectModified(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::linked_modified), to)); + to->linked_transformed_connection = SP_ITEM(new_obj)->connectTransformed(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::linked_transformed), to)); + + linked_modified(new_obj, SP_OBJECT_MODIFIED_FLAG, to); + } else { + to->_pathvector = Geom::PathVector(); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::_updateLink), to)); + } +} + +void OriginalPathArrayParam::setPathVector(SPObject *linked_obj, guint /*flags*/, PathAndDirectionAndVisible* to) +{ + if (!to) { + return; + } + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(linked_obj)) { + SPLPEItem * lpe_item = SP_LPE_ITEM(linked_obj); + if (_from_original_d) { + curve = SP_SHAPE(linked_obj)->getCurveForEdit(); + } else if (_allow_only_bspline_spiro && lpe_item && lpe_item->hasPathEffect()){ + curve = SP_SHAPE(linked_obj)->getCurveForEdit(); + PathEffectList lpelist = lpe_item->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast(lpe)) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(curve, 0, hp); + } else if (dynamic_cast(lpe)) { + LivePathEffect::sp_spiro_do_effect(curve); + } + } + } + } else { + curve = SP_SHAPE(linked_obj)->getCurve(); + } + } else if (SP_IS_TEXT(linked_obj)) { + curve = SP_TEXT(linked_obj)->getNormalizedBpath(); + } + + if (curve == nullptr) { + // curve invalid, set empty pathvector + to->_pathvector = Geom::PathVector(); + } else { + to->_pathvector = curve->get_pathvector(); + curve->unref(); + } + +} + +void OriginalPathArrayParam::linked_modified(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible* to) +{ + if (!to) { + return; + } + setPathVector(linked_obj, flags, to); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); + _store->foreach_iter(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::_updateLink), to)); +} + +bool OriginalPathArrayParam::param_readSVGValue(const gchar* strvalue) +{ + if (strvalue) { + while (!_vector.empty()) { + PathAndDirectionAndVisible *w = _vector.back(); + unlink(w); + _vector.pop_back(); + delete w; + } + _store->clear(); + + gchar ** strarray = g_strsplit(strvalue, "|", 0); + for (gchar ** iter = strarray; *iter != nullptr; iter++) { + if ((*iter)[0] == '#') { + gchar ** substrarray = g_strsplit(*iter, ",", 0); + PathAndDirectionAndVisible* w = new PathAndDirectionAndVisible((SPObject *)param_effect->getLPEObj()); + w->href = g_strdup(*substrarray); + w->reversed = *(substrarray+1) != nullptr && (*(substrarray+1))[0] == '1'; + //Like this to make backwards compatible, new value added in 0.93 + w->visibled = *(substrarray+2) == nullptr || (*(substrarray+2))[0] == '1'; + w->linked_changed_connection = w->ref.changedSignal().connect(sigc::bind(sigc::mem_fun(*this, &OriginalPathArrayParam::linked_changed), w)); + w->ref.attach(URI(w->href)); + + _vector.push_back(w); + + Gtk::TreeModel::iterator iter = _store->append(); + Gtk::TreeModel::Row row = *iter; + SPObject *obj = w->ref.getObject(); + + row[_model->_colObject] = w; + row[_model->_colLabel] = obj ? ( obj->label() ? obj->label() : obj->getId() ) : w->href; + row[_model->_colReverse] = w->reversed; + row[_model->_colVisible] = w->visibled; + g_strfreev (substrarray); + } + } + g_strfreev (strarray); + return true; + } + return false; +} + +Glib::ustring +OriginalPathArrayParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + bool foundOne = false; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); + } + return os.str(); +} + +Glib::ustring +OriginalPathArrayParam::param_getDefaultSVGValue() const +{ + return ""; +} + +void OriginalPathArrayParam::update() +{ + for (auto & iter : _vector) { + SPObject *linked_obj = iter->ref.getObject(); + linked_modified(linked_obj, SP_OBJECT_MODIFIED_FLAG, iter); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/originalpatharray.h b/src/live_effects/parameter/originalpatharray.h new file mode 100644 index 0000000..6f58627 --- /dev/null +++ b/src/live_effects/parameter/originalpatharray.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALPATHARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALPATHARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include +#include +#include +#include + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path-reference.h" + +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "path-reference.h" + +class SPObject; + +namespace Inkscape { + +namespace LivePathEffect { + +class PathAndDirectionAndVisible { +public: + PathAndDirectionAndVisible(SPObject *owner) + : href(nullptr), + ref(owner), + _pathvector(Geom::PathVector()), + reversed(false), + visibled(true) + { + + } + gchar *href; + URIReference ref; + Geom::PathVector _pathvector; + bool reversed; + bool visibled; + + sigc::connection linked_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; +}; + +class OriginalPathArrayParam : public Parameter { +public: + class ModelColumns; + + OriginalPathArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + + ~OriginalPathArrayParam() override; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_update_default(const gchar * default_value) override{}; + /** Disable the canvas indicators of parent class by overriding this method */ + void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/) override {}; + /** Disable the canvas indicators of parent class by overriding this method */ + void addCanvasIndicators(SPLPEItem const* /*lpeitem*/, std::vector & /*hp_vec*/) override {}; + void setFromOriginalD(bool from_original_d){ _from_original_d = from_original_d; update();}; + void allowOnlyBsplineSpiro(bool allow_only_bspline_spiro){ _allow_only_bspline_spiro = allow_only_bspline_spiro; update();}; + + std::vector _vector; + +protected: + bool _updateLink(const Gtk::TreeIter& iter, PathAndDirectionAndVisible* pd); + bool _selectIndex(const Gtk::TreeIter& iter, int* i); + void unlink(PathAndDirectionAndVisible* to); + void remove_link(PathAndDirectionAndVisible* to); + void setPathVector(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible* to); + + void linked_changed(SPObject *old_obj, SPObject *new_obj, PathAndDirectionAndVisible* to); + void linked_modified(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible* to); + void linked_transformed(Geom::Affine const *, SPItem *, PathAndDirectionAndVisible*) {} + void linked_delete(SPObject *deleted, PathAndDirectionAndVisible* to); + + ModelColumns *_model; + Glib::RefPtr _store; + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::CellRendererToggle *_toggle_reverse; + Gtk::CellRendererToggle *_toggle_visible; + Gtk::TreeView::Column *_name_column; + Gtk::ScrolledWindow _scroller; + + void on_link_button_click(); + void on_remove_button_click(); + void on_up_button_click(); + void on_down_button_click(); + void on_reverse_toggled(const Glib::ustring& path); + void on_visible_toggled(const Glib::ustring& path); + +private: + bool _from_original_d; + bool _allow_only_bspline_spiro; + void update(); + OriginalPathArrayParam(const OriginalPathArrayParam&) = delete; + OriginalPathArrayParam& operator=(const OriginalPathArrayParam&) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/parameter.cpp b/src/live_effects/parameter/parameter.cpp new file mode 100644 index 0000000..355e7dd --- /dev/null +++ b/src/live_effects/parameter/parameter.cpp @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "live_effects/parameter/parameter.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "xml/repr.h" + +#include "svg/stringstream.h" + +#include "verbs.h" + +#include + +#include + +#define noLPEREALPARAM_DEBUG + +namespace Inkscape { + +namespace LivePathEffect { + + +Parameter::Parameter(Glib::ustring label, Glib::ustring tip, Glib::ustring key, Inkscape::UI::Widget::Registry *wr, + Effect *effect) + : param_key(std::move(key)) + , param_wr(wr) + , param_label(std::move(label)) + , oncanvas_editable(false) + , widget_is_visible(true) + , widget_is_enabled(true) + , param_tooltip(std::move(tip)) + , param_effect(effect) +{ +} + +void Parameter::param_write_to_repr(const char *svgd) +{ + param_effect->getRepr()->setAttribute(param_key, svgd); +} + +void Parameter::write_to_SVG() +{ + param_write_to_repr(param_getSVGValue().c_str()); +} + +/*########################################### + * REAL PARAM + */ +ScalarParam::ScalarParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, gdouble default_value) + : Parameter(label, tip, key, wr, effect) + , value(default_value) + , min(-SCALARPARAM_G_MAXDOUBLE) + , max(SCALARPARAM_G_MAXDOUBLE) + , integer(false) + , defvalue(default_value) + , digits(2) + , inc_step(0.1) + , inc_page(1) + , add_slider(false) + , _set_undo(true) +{ +} + +ScalarParam::~ScalarParam() = default; + +bool ScalarParam::param_readSVGValue(const gchar *strvalue) +{ + double newval; + unsigned int success = sp_svg_number_read_d(strvalue, &newval); + if (success == 1) { + param_set_value(newval); + return true; + } + return false; +} + +Glib::ustring ScalarParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << value; + return os.str(); +} + +Glib::ustring ScalarParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void ScalarParam::param_set_default() { param_set_value(defvalue); } + +void ScalarParam::param_update_default(gdouble default_value) { defvalue = default_value; } + +void ScalarParam::param_update_default(const gchar *default_value) +{ + double newval; + unsigned int success = sp_svg_number_read_d(default_value, &newval); + if (success == 1) { + param_update_default(newval); + } +} + +void ScalarParam::param_transform_multiply(Geom::Affine const &postmul, bool set) +{ + // Check if proportional stroke-width scaling is on + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke || set) { + param_set_value(value * postmul.descrim()); + write_to_SVG(); + } +} + +void ScalarParam::param_set_value(gdouble val) +{ + value = val; + if (integer) + value = round(value); + if (value > max) + value = max; + if (value < min) + value = min; +} + +void ScalarParam::param_set_range(gdouble min, gdouble max) +{ + // if you look at client code, you'll see that many effects + // has a tendency to set an upper range of Geom::infinity(). + // Once again, in gtk2, this is not a problem. But in gtk3, + // widgets get allocated the amount of size they ask for, + // leading to excessively long widgets. + if (min >= -SCALARPARAM_G_MAXDOUBLE) { + this->min = min; + } else { + this->min = -SCALARPARAM_G_MAXDOUBLE; + } + if (max <= SCALARPARAM_G_MAXDOUBLE) { + this->max = max; + } else { + this->max = SCALARPARAM_G_MAXDOUBLE; + } + param_set_value(value); // reset value to see whether it is in ranges +} + +void ScalarParam::param_make_integer(bool yes) +{ + integer = yes; + digits = 0; + inc_step = 1; + inc_page = 10; +} + +void ScalarParam::param_set_undo(bool set_undo) { _set_undo = set_undo; } + +Gtk::Widget *ScalarParam::param_newWidget() +{ + if (widget_is_visible) { + Inkscape::UI::Widget::RegisteredScalar *rsu = Gtk::manage(new Inkscape::UI::Widget::RegisteredScalar( + param_label, param_tooltip, param_key, *param_wr, param_effect->getRepr(), param_effect->getSPDoc())); + + rsu->setValue(value); + rsu->setDigits(digits); + rsu->setIncrements(inc_step, inc_page); + rsu->setRange(min, max); + rsu->setProgrammatically = false; + if (add_slider) { + rsu->addSlider(); + } + if (_set_undo) { + rsu->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change scalar parameter")); + } + return dynamic_cast(rsu); + } else { + return nullptr; + } +} + +void ScalarParam::param_set_digits(unsigned digits) { this->digits = digits; } + +void ScalarParam::param_set_increments(double step, double page) +{ + inc_step = step; + inc_page = page; +} + + + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/parameter.h b/src/live_effects/parameter/parameter.h new file mode 100644 index 0000000..b73a5fe --- /dev/null +++ b/src/live_effects/parameter/parameter.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include <2geom/forward.h> +#include <2geom/pathvector.h> +#include + +// In gtk2, this wasn't an issue; we could toss around +// G_MAXDOUBLE and not worry about size allocations. But +// in gtk3, it is an issue: it allocates widget size for the maxmium +// value you pass to it, leading to some insane lengths. +// If you need this to be more, please be conservative about it. +const double SCALARPARAM_G_MAXDOUBLE = + 10000000000.0; // TODO fixme: using an arbitrary large number as a magic value seems fragile. + +class KnotHolder; +class SPLPEItem; +class SPDesktop; +class SPItem; + +namespace Gtk { +class Widget; +} + +namespace Inkscape { + +namespace NodePath { +class Path; +} + +namespace UI { +namespace Widget { +class Registry; +} +} // namespace UI + +namespace LivePathEffect { + +class Effect; + +class Parameter { + public: + Parameter(Glib::ustring label, Glib::ustring tip, Glib::ustring key, Inkscape::UI::Widget::Registry *wr, + Effect *effect); + virtual ~Parameter() = default; + ; + + Parameter(const Parameter &) = delete; + Parameter &operator=(const Parameter &) = delete; + + virtual bool param_readSVGValue(const gchar *strvalue) = 0; // returns true if new value is valid / accepted. + virtual Glib::ustring param_getSVGValue() const = 0; + virtual Glib::ustring param_getDefaultSVGValue() const = 0; + virtual void param_widget_is_visible(bool is_visible) { widget_is_visible = is_visible; } + virtual void param_widget_is_enabled(bool is_enabled) { widget_is_enabled = is_enabled; } + void write_to_SVG(); + + virtual void param_set_default() = 0; + virtual void param_update_default(const gchar *default_value) = 0; + // This creates a new widget (newed with Gtk::manage(new ...);) + virtual Gtk::Widget *param_newWidget() = 0; + + virtual Glib::ustring *param_getTooltip() { return ¶m_tooltip; }; + + // overload these for your particular parameter to make it provide knotholder handles or canvas helperpaths + virtual bool providesKnotHolderEntities() const { return false; } + virtual void addKnotHolderEntities(KnotHolder * /*knotholder*/, SPItem * /*item*/){}; + virtual void addCanvasIndicators(SPLPEItem const * /*lpeitem*/, std::vector & /*hp_vec*/){}; + + virtual void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/){}; + virtual void param_setup_nodepath(Inkscape::NodePath::Path * /*np*/){}; + + virtual void param_transform_multiply(Geom::Affine const & /*postmul*/, bool set){}; + + Glib::ustring param_key; + Glib::ustring param_tooltip; + Inkscape::UI::Widget::Registry *param_wr; + Glib::ustring param_label; + + bool oncanvas_editable; + bool widget_is_visible; + bool widget_is_enabled; + + protected: + Effect *param_effect; + + void param_write_to_repr(const char *svgd); +}; + + +class ScalarParam : public Parameter { + public: + ScalarParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, gdouble default_value = 1.0); + ~ScalarParam() override; + ScalarParam(const ScalarParam &) = delete; + ScalarParam &operator=(const ScalarParam &) = delete; + + bool param_readSVGValue(const gchar *strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void param_set_default() override; + void param_update_default(gdouble default_value); + void param_update_default(const gchar *default_value) override; + void param_set_value(gdouble val); + void param_make_integer(bool yes = true); + void param_set_range(gdouble min, gdouble max); + void param_set_digits(unsigned digits); + void param_set_increments(double step, double page); + void addSlider(bool add_slider_widget) { add_slider = add_slider_widget; }; + double param_get_max() { return max; }; + double param_get_min() { return min; }; + void param_set_undo(bool set_undo); + Gtk::Widget *param_newWidget() override; + + inline operator gdouble() const { return value; }; + + protected: + gdouble value; + gdouble min; + gdouble max; + bool integer; + gdouble defvalue; + unsigned digits; + double inc_step; + double inc_page; + bool add_slider; + bool _set_undo; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/live_effects/parameter/path-reference.cpp b/src/live_effects/parameter/path-reference.cpp new file mode 100644 index 0000000..c3ce3d5 --- /dev/null +++ b/src/live_effects/parameter/path-reference.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of LPE Path parameter. + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/path-reference.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace LivePathEffect { + +bool PathReference::_acceptObject(SPObject * const obj) const +{ + if (SP_IS_SHAPE(obj) || SP_IS_TEXT(obj)) { + /* Refuse references to lpeobject */ + if (obj == getOwner()) { + return false; + } + // TODO: check whether the referred path has this LPE applied, if so: deny deny deny! + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/path-reference.h b/src/live_effects/parameter/path-reference.h new file mode 100644 index 0000000..0b33194 --- /dev/null +++ b/src/live_effects/parameter/path-reference.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPE_PATH_REFERENCE_H +#define SEEN_LPE_PATH_REFERENCE_H + +/* + * Copyright (C) 2008-2012 Authors + * Authors: Johan Engelen + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/uri-references.h" + +class SPItem; +namespace Inkscape { +namespace XML { class Node; } + +namespace LivePathEffect { + +/** + * The reference corresponding to href of LPE PathParam. + */ +class PathReference : public Inkscape::URIReference { +public: + PathReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return (SPItem *)URIReference::getObject(); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +private: + PathReference(const PathReference&) = delete; + PathReference& operator=(const PathReference&) = delete; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + + + +#endif /* !SEEN_LPE_PATH_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/path.cpp b/src/live_effects/parameter/path.cpp new file mode 100644 index 0000000..9b51c2d --- /dev/null +++ b/src/live_effects/parameter/path.cpp @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/path.h" +#include "live_effects/lpeobject.h" + +#include +#include + +#include +#include + +#include "bad-uri-exception.h" +#include "ui/widget/point.h" + +#include "live_effects/effect.h" +#include "svg/svg.h" +#include <2geom/svg-path-parser.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/pathvector.h> +#include <2geom/d2.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "ui/icon-loader.h" +#include "verbs.h" +#include "xml/repr.h" +// needed for on-canvas editing: +#include "ui/tools-switch.h" +#include "ui/shape-editor.h" + +#include "selection.h" +// clipboard support +#include "ui/clipboard.h" +// required for linking to other paths + +#include "object/uri.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "display/curve.h" + +#include "ui/tools/node-tool.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/shape-record.h" + +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +PathParam::PathParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar * default_value) + : Parameter(label, tip, key, wr, effect), + changed(true), + _pathvector(), + _pwd2(), + must_recalculate_pwd2(false), + href(nullptr), + ref( (SPObject*)effect->getLPEObj() ) +{ + defvalue = g_strdup(default_value); + param_readSVGValue(defvalue); + oncanvas_editable = true; + _from_original_d = false; + _edit_button = true; + _copy_button = true; + _paste_button = true; + _link_button = true; + ref_changed_connection = ref.changedSignal().connect(sigc::mem_fun(*this, &PathParam::ref_changed)); +} + +PathParam::~PathParam() +{ + remove_link(); +//TODO: Removed to fix a bug https://bugs.launchpad.net/inkscape/+bug/1716926 +// Maybe wee need to resurrect, not know when this code is added, but seems also not working now in a few test I do. +// in the future and do a deeper fix in multi-path-manipulator +// using namespace Inkscape::UI; +// SPDesktop *desktop = SP_ACTIVE_DESKTOP; +// if (desktop) { +// if (tools_isactive(desktop, TOOLS_NODES)) { +// SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); +// if (item) { +// Inkscape::UI::Tools::NodeTool *nt = static_cast(desktop->event_context); +// std::set shapes; +// ShapeRecord r; +// r.item = item; +// shapes.insert(r); +// nt->_multipath->setItems(shapes); +// } +// } +// } + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + // TODO remove the tools_switch atrocity. + if (tools_isactive(desktop, TOOLS_NODES)) { + tools_switch(desktop, TOOLS_SELECT); + tools_switch(desktop, TOOLS_NODES); + } + } + g_free(defvalue); +} + +Geom::PathVector const & +PathParam::get_pathvector() const +{ + return _pathvector; +} + +Geom::Piecewise > const & +PathParam::get_pwd2() +{ + ensure_pwd2(); + return _pwd2; +} + +void +PathParam::param_set_default() +{ + param_readSVGValue(defvalue); +} + +void +PathParam::param_set_and_write_default() +{ + param_write_to_repr(defvalue); +} + +bool +PathParam::param_readSVGValue(const gchar * strvalue) +{ + if (strvalue) { + _pathvector.clear(); + remove_link(); + must_recalculate_pwd2 = true; + + if (strvalue[0] == '#') { + if (href) + g_free(href); + href = g_strdup(strvalue); + + // Now do the attaching, which emits the changed signal. + try { + ref.attach(Inkscape::URI(href)); + //lp:1299948 + SPItem* i = ref.getObject(); + if (i) { + linked_modified_callback(i, SP_OBJECT_MODIFIED_FLAG); + } // else: document still processing new events. Repr of the linked object not created yet. + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref.detach(); + _pathvector = sp_svg_read_pathv(defvalue); + } + } else { + _pathvector = sp_svg_read_pathv(strvalue); + } + + emit_changed(); + return true; + } + + return false; +} + +Glib::ustring +PathParam::param_getSVGValue() const +{ + if (href) { + return href; + } else { + gchar * svgd = sp_svg_write_path( _pathvector ); + return Glib::convert_return_gchar_ptr_to_ustring(svgd); + } +} + +Glib::ustring +PathParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +void +PathParam::set_buttons(bool edit_button, bool copy_button, bool paste_button, bool link_button) +{ + _edit_button = edit_button; + _copy_button = copy_button; + _paste_button = paste_button; + _link_button = link_button; +} + +Gtk::Widget * +PathParam::param_newWidget() +{ + Gtk::HBox * _widget = Gtk::manage(new Gtk::HBox()); + + Gtk::Label* pLabel = Gtk::manage(new Gtk::Label(param_label)); + static_cast(_widget)->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + Gtk::Image * pIcon = nullptr; + Gtk::Button * pButton = nullptr; + if (_edit_button) { + pIcon = Gtk::manage(sp_get_icon_image("tool-node-editor", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_edit_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Edit on-canvas")); + } + + if (_copy_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-copy", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_copy_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Copy path")); + } + + if (_paste_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-paste", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_paste_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Paste path")); + } + if (_link_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_link_button_click)); + static_cast(_widget)->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + static_cast(_widget)->show_all_children(); + + return dynamic_cast (_widget); +} + +void +PathParam::param_editOncanvas(SPItem *item, SPDesktop * dt) +{ + SPDocument *document = dt->getDocument(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + using namespace Inkscape::UI; + + // TODO remove the tools_switch atrocity. + if (!tools_isactive(dt, TOOLS_NODES)) { + tools_switch(dt, TOOLS_NODES); + } + + Inkscape::UI::Tools::NodeTool *nt = static_cast(dt->event_context); + std::set shapes; + ShapeRecord r; + + r.role = SHAPE_ROLE_LPE_PARAM; + r.edit_transform = item->i2dt_affine(); // TODO is it right? + if (!href) { + r.object = dynamic_cast(param_effect->getLPEObj()); + r.lpe_key = param_key; + Geom::PathVector stored_pv = _pathvector; + if (_pathvector.empty()) { + param_write_to_repr("M0,0 L1,0"); + } else { + gchar *svgd = sp_svg_write_path(stored_pv); + param_write_to_repr(svgd); + g_free(svgd); + } + } else { + r.object = ref.getObject(); + } + shapes.insert(r); + nt->_multipath->setItems(shapes); + DocumentUndo::setUndoSensitive(document, saved); +} + +void +PathParam::param_setup_nodepath(Inkscape::NodePath::Path *) +{ + // TODO this method should not exist at all! +} + +void +PathParam::addCanvasIndicators(SPLPEItem const*/*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(_pathvector); +} + +/* + * Only applies transform when not referring to other path! + */ +void +PathParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + // only apply transform when not referring to other path + if (!href) { + set_new_value( _pathvector * postmul, true ); + } +} + +/* + * See comments for set_new_value(Geom::PathVector). + */ +void +PathParam::set_new_value (Geom::Piecewise > const & newpath, bool write_to_svg) +{ + remove_link(); + _pathvector = Geom::path_from_piecewise(newpath, LPE_CONVERSION_TOLERANCE); + + if (write_to_svg) { + gchar * svgd = sp_svg_write_path( _pathvector ); + param_write_to_repr(svgd); + g_free(svgd); + + // After the whole "writing to svg avalanche of function calling": force value upon pwd2 and don't recalculate. + _pwd2 = newpath; + must_recalculate_pwd2 = false; + } else { + _pwd2 = newpath; + must_recalculate_pwd2 = false; + emit_changed(); + } +} + +/* + * This method sets new path data. + * If this PathParam refers to another path, this link is removed (and replaced with explicit path data). + * + * If write_to_svg = true : + * The new path data is written to SVG. In this case the signal_path_changed signal + * is not directly emitted in this method, because writing to SVG + * triggers the LPEObject to which this belongs to call Effect::setParameter which calls + * PathParam::readSVGValue, which finally emits the signal_path_changed signal. + * If write_to_svg = false : + * The new path data is not written to SVG. This method will emit the signal_path_changed signal. + */ +void +PathParam::set_new_value (Geom::PathVector const &newpath, bool write_to_svg) +{ + remove_link(); + if (newpath.empty()) { + param_set_and_write_default(); + return; + } else { + _pathvector = newpath; + } + must_recalculate_pwd2 = true; + + if (write_to_svg) { + gchar * svgd = sp_svg_write_path( _pathvector ); + param_write_to_repr(svgd); + g_free(svgd); + } else { + emit_changed(); + } +} + +void +PathParam::ensure_pwd2() +{ + if (must_recalculate_pwd2) { + _pwd2.clear(); + for (const auto & i : _pathvector) { + _pwd2.concat( i.toPwSb() ); + } + + must_recalculate_pwd2 = false; + } +} + +void +PathParam::emit_changed() +{ + changed = true; + signal_path_changed.emit(); +} + +void +PathParam::start_listening(SPObject * to) +{ + if ( to == nullptr ) { + return; + } + linked_delete_connection = to->connectDelete(sigc::mem_fun(*this, &PathParam::linked_delete)); + linked_modified_connection = to->connectModified(sigc::mem_fun(*this, &PathParam::linked_modified)); + if (SP_IS_ITEM(to)) { + linked_transformed_connection = SP_ITEM(to)->connectTransformed(sigc::mem_fun(*this, &PathParam::linked_transformed)); + } + linked_modified(to, SP_OBJECT_MODIFIED_FLAG); // simulate linked_modified signal, so that path data is updated +} + +void +PathParam::quit_listening() +{ + linked_modified_connection.disconnect(); + linked_delete_connection.disconnect(); + linked_transformed_connection.disconnect(); +} + +void +PathParam::ref_changed(SPObject */*old_ref*/, SPObject *new_ref) +{ + quit_listening(); + if ( new_ref ) { + start_listening(new_ref); + } +} + +void +PathParam::remove_link() +{ + if (href) { + ref.detach(); + g_free(href); + href = nullptr; + } +} + +void +PathParam::linked_delete(SPObject */*deleted*/) +{ + quit_listening(); + remove_link(); + set_new_value (_pathvector, true); +} + +void PathParam::linked_modified(SPObject *linked_obj, guint flags) +{ + linked_modified_callback(linked_obj, flags); +} + +void PathParam::linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item) +{ + linked_transformed_callback(rel_transf, moved_item); +} + +void +PathParam::linked_modified_callback(SPObject *linked_obj, guint /*flags*/) +{ + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(linked_obj)) { + if (_from_original_d) { + curve = SP_SHAPE(linked_obj)->getCurveForEdit(); + } else { + curve = SP_SHAPE(linked_obj)->getCurve(); + } + } + if (SP_IS_TEXT(linked_obj)) { + curve = SP_TEXT(linked_obj)->getNormalizedBpath(); + } + + if (curve == nullptr) { + // curve invalid, set default value + _pathvector = sp_svg_read_pathv(defvalue); + } else { + _pathvector = curve->get_pathvector(); + curve->unref(); + } + + must_recalculate_pwd2 = true; + emit_changed(); + SP_OBJECT(param_effect->getLPEObj())->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void +PathParam::param_update_default(const gchar * default_value){ + defvalue = strdup(default_value); +} + +/* CALLBACK FUNCTIONS FOR THE BUTTONS */ +void +PathParam::on_edit_button_click() +{ + SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + if (item != nullptr) { + param_editOncanvas(item, SP_ACTIVE_DESKTOP); + } +} + +void +PathParam::paste_param_path(const char *svgd) +{ + // only recognize a non-null, non-empty string + if (svgd && *svgd) { + // remove possible link to path + remove_link(); + SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + char *svgd_new = nullptr; + if (item != nullptr) { + Geom::PathVector path_clipboard = sp_svg_read_pathv(svgd); + path_clipboard *= item->i2doc_affine().inverse(); + svgd_new = sp_svg_write_path(path_clipboard); + svgd = svgd_new; + } + + param_write_to_repr(svgd); + g_free(svgd_new); + signal_path_pasted.emit(); + } +} + +void +PathParam::on_paste_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + Glib::ustring svgd = cm->getPathParameter(SP_ACTIVE_DESKTOP); + paste_param_path(svgd.data()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Paste path parameter")); +} + +void +PathParam::on_copy_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + cm->copyPathParameter(this); +} + +void +PathParam::linkitem(Glib::ustring pathid) +{ + if (pathid.empty()) { + return; + } + + // add '#' at start to make it an uri. + pathid.insert(pathid.begin(), '#'); + if ( href && strcmp(pathid.c_str(), href) == 0 ) { + // no change, do nothing + return; + } else { + // TODO: + // check if id really exists in document, or only in clipboard document: if only in clipboard then invalid + // check if linking to object to which LPE is applied (maybe delegated to PathReference + + param_write_to_repr(pathid.c_str()); + DocumentUndo::done(param_effect->getSPDoc(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Link path parameter to path")); + } +} + +void +PathParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + Glib::ustring pathid = cm->getShapeOrTextObjectId(SP_ACTIVE_DESKTOP); + + linkitem(pathid); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/path.h b/src/live_effects/parameter/path.h new file mode 100644 index 0000000..76a6584 --- /dev/null +++ b/src/live_effects/parameter/path.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_PATH_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_PATH_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/path.h> + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path-reference.h" +#include +#include + +namespace Inkscape { + +namespace LivePathEffect { + +class PathParam : public Parameter { +public: + PathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"); + ~PathParam() override; + + Geom::PathVector const & get_pathvector() const; + Geom::Piecewise > const & get_pwd2(); + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + void param_set_and_write_default(); + void set_new_value (Geom::PathVector const &newpath, bool write_to_svg); + void set_new_value (Geom::Piecewise > const &newpath, bool write_to_svg); + void set_buttons(bool edit_button, bool copy_button, bool paste_button, bool link_button); + void param_editOncanvas(SPItem * item, SPDesktop * dt) override; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + void addCanvasIndicators(SPLPEItem const* lpeitem, std::vector &hp_vec) override; + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + void setFromOriginalD(bool from_original_d){ _from_original_d = from_original_d; }; + + sigc::signal signal_path_pasted; + sigc::signal signal_path_changed; + bool changed; /* this gets set whenever the path is changed (this is set to true, and then the signal_path_changed signal is emitted). + * the user must set it back to false if she wants to use it sensibly */ + + void paste_param_path(const char *svgd); + void on_paste_button_click(); + void linkitem(Glib::ustring pathid); + +protected: + Geom::PathVector _pathvector; // this is primary data storage, since it is closest to SVG. + + Geom::Piecewise > _pwd2; // secondary, hence the bool must_recalculate_pwd2 + bool must_recalculate_pwd2; // set when _pathvector was updated, but _pwd2 not + void ensure_pwd2(); // ensures _pwd2 is up to date + + gchar * href; // contains link to other object, e.g. "#path2428", NULL if PathParam contains pathdata itself + PathReference ref; + sigc::connection ref_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; + void ref_changed(SPObject *old_ref, SPObject *new_ref); + void remove_link(); + void start_listening(SPObject * to); + void quit_listening(); + void linked_delete(SPObject *deleted); + void linked_modified(SPObject *linked_obj, guint flags); + void linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item); + virtual void linked_modified_callback(SPObject *linked_obj, guint flags); + virtual void linked_transformed_callback(Geom::Affine const * /*rel_transf*/, SPItem * /*moved_item*/) {}; + + void on_edit_button_click(); + void on_copy_button_click(); + void on_link_button_click(); + + void emit_changed(); + + gchar * defvalue; + +private: + bool _from_original_d; + bool _edit_button; + bool _copy_button; + bool _paste_button; + bool _link_button; + PathParam(const PathParam&) = delete; + PathParam& operator=(const PathParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/point.cpp b/src/live_effects/parameter/point.cpp new file mode 100644 index 0000000..8f627a9 --- /dev/null +++ b/src/live_effects/parameter/point.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/point.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "ui/widget/point.h" +#include "inkscape.h" +#include "verbs.h" +#include "knotholder.h" +#include + +namespace Inkscape { + +namespace LivePathEffect { + +PointParam::PointParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar *htip, Geom::Point default_value, + bool live_update ) + : Parameter(label, tip, key, wr, effect), + defvalue(default_value), + liveupdate(live_update), + _knot_entity(nullptr) +{ + knot_shape = SP_KNOT_SHAPE_DIAMOND; + knot_mode = SP_KNOT_MODE_XOR; + knot_color = 0xffffff00; + handle_tip = g_strdup(htip); +} + +PointParam::~PointParam() +{ + if (handle_tip) + g_free(handle_tip); +} + +void +PointParam::param_set_default() +{ + param_setValue(defvalue,true); +} + +void +PointParam::param_set_liveupdate( bool live_update) +{ + liveupdate = live_update; +} + +Geom::Point +PointParam::param_get_default() const{ + return defvalue; +} + +void +PointParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +PointParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +void +PointParam::param_hide_knot(bool hide) { + if (_knot_entity) { + bool update = false; + if (hide && _knot_entity->knot->flags & SP_KNOT_VISIBLE) { + update = true; + _knot_entity->knot->hide(); + } else if(!hide && !(_knot_entity->knot->flags & SP_KNOT_VISIBLE)) { + update = true; + _knot_entity->knot->show(); + } + if (update) { + _knot_entity->update_knot(); + } + } +} + +void +PointParam::param_setValue(Geom::Point newpoint, bool write) +{ + *dynamic_cast( this ) = newpoint; + if(write){ + Inkscape::SVGOStringStream os; + os << newpoint; + gchar * str = g_strdup(os.str().c_str()); + param_write_to_repr(str); + g_free(str); + } + if(_knot_entity && liveupdate){ + _knot_entity->update_knot(); + } +} + +bool +PointParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_setValue( Geom::Point(newx, newy) ); + return true; + } + return false; +} + +Glib::ustring +PointParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << *dynamic_cast( this ); + return os.str(); +} + +Glib::ustring +PointParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void +PointParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + param_setValue( (*this) * postmul, true); +} + +Gtk::Widget * +PointParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredTransformedPoint * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredTransformedPoint( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + Geom::Affine transf = SP_ACTIVE_DESKTOP->doc2dt(); + pointwdg->setTransform(transf); + pointwdg->setValue( *this ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change point parameter")); + pointwdg->signal_button_release_event().connect(sigc::mem_fun (*this, &PointParam::on_button_release)); + + Gtk::HBox * hbox = Gtk::manage( new Gtk::HBox() ); + static_cast(hbox)->pack_start(*pointwdg, true, true); + static_cast(hbox)->show_all_children(); + return dynamic_cast (hbox); +} + +bool PointParam::on_button_release(GdkEventButton* button_event) { + param_effect->refresh_widgets = true; + return false; +} + +void +PointParam::set_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + knot_shape = shape; + knot_mode = mode; + knot_color = color; +} + +class PointParamKnotHolderEntity : public KnotHolderEntity { +public: + PointParamKnotHolderEntity(PointParam *p) { this->pparam = p; } + ~PointParamKnotHolderEntity() override { this->pparam->_knot_entity = nullptr;} + + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + +private: + PointParam *pparam; +}; + +void +PointParamKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + Geom::Point s = snap_knot_position(p, state); + if (state & GDK_CONTROL_MASK) { + Geom::Point A(origin[Geom::X],p[Geom::Y]); + Geom::Point B(p[Geom::X],origin[Geom::Y]); + double distanceA = Geom::distance(A,p); + double distanceB = Geom::distance(B,p); + if(distanceA > distanceB){ + s = B; + } else { + s = A; + } + } + if(this->pparam->liveupdate){ + pparam->param_setValue(s, true); + } else { + pparam->param_setValue(s); + } +} + +void +PointParamKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + pparam->param_effect->refresh_widgets = true; + pparam->write_to_SVG(); +} + +Geom::Point +PointParamKnotHolderEntity::knot_get() const +{ + return *pparam; +} + +void +PointParamKnotHolderEntity::knot_click(guint state) +{ + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + this->pparam->param_set_default(); + pparam->param_setValue(*pparam,true); + } + } +} + +void +PointParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new PointParamKnotHolderEntity(this); + // TODO: can we ditch handleTip() etc. because we have access to handle_tip etc. itself??? + _knot_entity->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, handleTip(), knot_shape, knot_mode, + knot_color); + knotholder->add(_knot_entity); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/point.h b/src/live_effects/parameter/point.h new file mode 100644 index 0000000..26e0e32 --- /dev/null +++ b/src/live_effects/parameter/point.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_POINT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_POINT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/point.h> +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/parameter.h" + +#include "knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class PointParamKnotHolderEntity; + +class PointParam : public Geom::Point, public Parameter { +public: + PointParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar *handle_tip = nullptr,// tip for automatically associated on-canvas handle + Geom::Point default_value = Geom::Point(0,0), + bool live_update = true ); + ~PointParam() override; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + inline const gchar *handleTip() const { return handle_tip ? handle_tip : param_tooltip.c_str(); } + void param_setValue(Geom::Point newpoint, bool write = false); + void param_set_default() override; + void param_hide_knot(bool hide); + Geom::Point param_get_default() const; + void param_set_liveupdate(bool live_update); + void param_update_default(Geom::Point default_point); + + void param_update_default(const gchar * default_point) override; + void param_transform_multiply(Geom::Affine const & /*postmul*/, bool set) override; + + void set_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + friend class PointParamKnotHolderEntity; +private: + PointParam(const PointParam&) = delete; + PointParam& operator=(const PointParam&) = delete; + bool on_button_release(GdkEventButton* button_event); + Geom::Point defvalue; + bool liveupdate; + KnotHolderEntity * _knot_entity; + SPKnotShapeType knot_shape; + SPKnotModeType knot_mode; + guint32 knot_color; + gchar *handle_tip; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/powerstrokepointarray.cpp b/src/live_effects/parameter/powerstrokepointarray.cpp new file mode 100644 index 0000000..e33166f --- /dev/null +++ b/src/live_effects/parameter/powerstrokepointarray.cpp @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/lpe-powerstroke-properties.h" +#include "live_effects/parameter/powerstrokepointarray.h" + +#include "knotholder.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-powerstroke.h" + + +#include <2geom/piecewise.h> +#include <2geom/sbasis-geometric.h> + +#include "preferences.h" // for proportional stroke/path scaling behavior + +#include + +namespace Inkscape { + +namespace LivePathEffect { + +PowerStrokePointArrayParam::PowerStrokePointArrayParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect) + : ArrayParam(label, tip, key, wr, effect, 0) +{ + knot_shape = SP_KNOT_SHAPE_DIAMOND; + knot_mode = SP_KNOT_MODE_XOR; + knot_color = 0xff88ff00; +} + +PowerStrokePointArrayParam::~PowerStrokePointArrayParam() += default; + +Gtk::Widget * +PowerStrokePointArrayParam::param_newWidget() +{ + return nullptr; +} + +void PowerStrokePointArrayParam::param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + // Check if proportional stroke-width scaling is on + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + std::vector result; + result.reserve(_vector.size()); // reserve space for the points that will be added in the for loop + for (auto point_it : _vector) + { + // scale each width knot with the average scaling in X and Y + Geom::Coord const A = point_it[Geom::Y] * postmul.descrim(); + result.emplace_back(point_it[Geom::X], A); + } + param_set_and_write_new_value(result); + } +} + +/** call this method to recalculate the controlpoints such that they stay at the same location relative to the new path. Useful after adding/deleting nodes to the path.*/ +void +PowerStrokePointArrayParam::recalculate_controlpoints_for_new_pwd2(Geom::Piecewise > const & pwd2_in) +{ + Inkscape::LivePathEffect::LPEPowerStroke *lpe = dynamic_cast(param_effect); + if (lpe) { + if (last_pwd2.size() > pwd2_in.size()) { + double factor = (double)pwd2_in.size() / (double)last_pwd2.size(); + for (auto & i : _vector) { + i[Geom::X] *= factor; + } + } else if (last_pwd2.size() < pwd2_in.size()) { + // Path has become longer: probably node added, maintain position of knots + Geom::Piecewise > normal = rot90(unitVector(derivative(pwd2_in))); + for (auto & i : _vector) { + Geom::Point pt = i; + Geom::Point position = last_pwd2.valueAt(pt[Geom::X]) + pt[Geom::Y] * last_pwd2_normal.valueAt(pt[Geom::X]); + double t = nearest_time(position, pwd2_in); + i[Geom::X] = t; + } + } + write_to_SVG(); + } +} + +/** call this method to recalculate the controlpoints when path is reversed.*/ +std::vector +PowerStrokePointArrayParam::reverse_controlpoints(bool write) +{ + std::vector controlpoints; + if (!last_pwd2.empty()) { + Geom::Piecewise > const & pwd2_in_reverse = reverse(last_pwd2); + for (auto & i : _vector) { + Geom::Point control_pos = last_pwd2.valueAt(i[Geom::X]); + double new_pos = Geom::nearest_time(control_pos, pwd2_in_reverse); + controlpoints.emplace_back(new_pos,i[Geom::Y]); + i[Geom::X] = new_pos; + } + if (write) { + write_to_SVG(); + _vector.clear(); + _vector = controlpoints; + controlpoints.clear(); + write_to_SVG(); + return _vector; + } + } + return controlpoints; +} + +float PowerStrokePointArrayParam::median_width() +{ + size_t size = _vector.size(); + if (size > 0) + { + if (size % 2 == 0) + { + return (_vector[size / 2 - 1].y() + _vector[size / 2].y()) / 2; + } + else + { + return _vector[size / 2].y(); + } + } + return 1; +} + +void +PowerStrokePointArrayParam::set_pwd2(Geom::Piecewise > const & pwd2_in, Geom::Piecewise > const & pwd2_normal_in) +{ + last_pwd2 = pwd2_in; + last_pwd2_normal = pwd2_normal_in; +} + + +void +PowerStrokePointArrayParam::set_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + knot_shape = shape; + knot_mode = mode; + knot_color = color; +} +/* +class PowerStrokePointArrayParamKnotHolderEntity : public KnotHolderEntity { +public: + PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index); + virtual ~PowerStrokePointArrayParamKnotHolderEntity() {} + + virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); + virtual Geom::Point knot_get() const; + virtual void knot_click(guint state); + + // Checks whether the index falls within the size of the parameter's vector + bool valid_index(unsigned int index) const { + return (_pparam->_vector.size() > index); + }; + +private: + PowerStrokePointArrayParam *_pparam; + unsigned int _index; +};*/ + +PowerStrokePointArrayParamKnotHolderEntity::PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index) + : _pparam(p), + _index(index) +{ +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + if (!valid_index(_index)) { + return; + } + /// @todo how about item transforms??? + Piecewise > const & pwd2 = _pparam->get_pwd2(); + Piecewise > const & n = _pparam->get_pwd2_normal(); + + Geom::Point const s = snap_knot_position(p, state); + double t = nearest_time(s, pwd2); + double offset = dot(s - pwd2.valueAt(t), n.valueAt(t)); + _pparam->_vector.at(_index) = Geom::Point(t, offset/_pparam->_scale_width); + if (_pparam->_vector.size() == 1 ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/powerstroke/width", offset); + } + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +Geom::Point +PowerStrokePointArrayParamKnotHolderEntity::knot_get() const +{ + using namespace Geom; + + if (!valid_index(_index)) { + return Geom::Point(infinity(), infinity()); + } + + Piecewise > const & pwd2 = _pparam->get_pwd2(); + Piecewise > const & n = _pparam->get_pwd2_normal(); + + Point offset_point = _pparam->_vector.at(_index); + if (offset_point[X] > pwd2.size() || offset_point[X] < 0) { + g_warning("Broken powerstroke point at %f, I won't try to add that", offset_point[X]); + return Geom::Point(infinity(), infinity()); + } + Point canvas_point = pwd2.valueAt(offset_point[X]) + (offset_point[Y] * _pparam->_scale_width) * n.valueAt(offset_point[X]); + return canvas_point; +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + _pparam->param_effect->refresh_widgets = true; + _pparam->write_to_SVG(); +} + +void PowerStrokePointArrayParamKnotHolderEntity::knot_set_offset(Geom::Point offset) +{ + _pparam->_vector.at(_index) = Geom::Point(offset.x(), offset.y() / 2); + this->parent_holder->knot_ungrabbed_handler(this->knot, 0); +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_click(guint state) +{ + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + // delete the clicked knot + std::vector & vec = _pparam->_vector; + if (vec.size() > 1) { //Force don't remove last knot + vec.erase(vec.begin() + _index); + _pparam->param_set_and_write_new_value(vec); + // shift knots down one index + for(auto & ent : parent_holder->entity) { + PowerStrokePointArrayParamKnotHolderEntity *pspa_ent = dynamic_cast(ent); + if ( pspa_ent && pspa_ent->_pparam == this->_pparam ) { // check if the knotentity belongs to this powerstrokepointarray parameter + if (pspa_ent->_index > this->_index) { + --pspa_ent->_index; + } + } + }; + // temporary hide, when knotholder were recreated it finally drop + this->knot->hide(); + } + return; + } else { + // add a knot to XML + std::vector & vec = _pparam->_vector; + vec.insert(vec.begin() + _index, 1, vec.at(_index)); // this clicked knot is duplicated + _pparam->param_set_and_write_new_value(vec); + + // shift knots up one index + for(auto & ent : parent_holder->entity) { + PowerStrokePointArrayParamKnotHolderEntity *pspa_ent = dynamic_cast(ent); + if ( pspa_ent && pspa_ent->_pparam == this->_pparam ) { // check if the knotentity belongs to this powerstrokepointarray parameter + if (pspa_ent->_index > this->_index) { + ++pspa_ent->_index; + } + } + }; + // add knot to knotholder + PowerStrokePointArrayParamKnotHolderEntity *e = new PowerStrokePointArrayParamKnotHolderEntity(_pparam, _index+1); + e->create(this->desktop, this->item, parent_holder, Inkscape::CTRL_TYPE_LPE, + _("Stroke width control point: drag to alter the stroke width. Ctrl+click adds a " + "control point, Ctrl+Alt+click deletes it, Shift+click launches width dialog."), + _pparam->knot_shape, _pparam->knot_mode, _pparam->knot_color); + parent_holder->add(e); + } + } + else if ((state & GDK_MOD1_MASK) || (state & GDK_SHIFT_MASK)) + { + Geom::Point offset = Geom::Point(_pparam->_vector.at(_index).x(), _pparam->_vector.at(_index).y() * 2); + Inkscape::UI::Dialogs::PowerstrokePropertiesDialog::showDialog(this->desktop, offset, this); + } +} + +void PowerStrokePointArrayParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + for (unsigned int i = 0; i < _vector.size(); ++i) { + PowerStrokePointArrayParamKnotHolderEntity *e = new PowerStrokePointArrayParamKnotHolderEntity(this, i); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, + _("Stroke width control point: drag to alter the stroke width. Ctrl+click adds a " + "control point, Ctrl+Alt+click deletes it, Shift+click launches width dialog."), + knot_shape, knot_mode, knot_color); + knotholder->add(e); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/powerstrokepointarray.h b/src/live_effects/parameter/powerstrokepointarray.h new file mode 100644 index 0000000..36c4432 --- /dev/null +++ b/src/live_effects/parameter/powerstrokepointarray.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_POWERSTROKE_POINT_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_POWERSTROKE_POINT_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/point.h> + +#include "live_effects/parameter/array.h" + +#include "knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class PowerStrokePointArrayParam : public ArrayParam { +public: + PowerStrokePointArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + ~PowerStrokePointArrayParam() override; + + PowerStrokePointArrayParam(const PowerStrokePointArrayParam&) = delete; + PowerStrokePointArrayParam& operator=(const PowerStrokePointArrayParam&) = delete; + + Gtk::Widget * param_newWidget() override; + + void param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) override; + + void set_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + + + float median_width(); + + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + void param_update_default(const gchar * default_value) override{}; + + void set_pwd2(Geom::Piecewise > const & pwd2_in, Geom::Piecewise > const & pwd2_normal_in); + Geom::Piecewise > const & get_pwd2() const { return last_pwd2; } + Geom::Piecewise > const & get_pwd2_normal() const { return last_pwd2_normal; } + + void recalculate_controlpoints_for_new_pwd2(Geom::Piecewise > const & pwd2_in); + std::vector reverse_controlpoints(bool write); + void set_scale_width(double scale_width){_scale_width = scale_width;}; + double _scale_width; + friend class PowerStrokePointArrayParamKnotHolderEntity; + +private: + SPKnotShapeType knot_shape; + SPKnotModeType knot_mode; + guint32 knot_color; + + Geom::Piecewise > last_pwd2; + Geom::Piecewise > last_pwd2_normal; +}; + +class PowerStrokePointArrayParamKnotHolderEntity : public KnotHolderEntity { +public: + PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index); + ~PowerStrokePointArrayParamKnotHolderEntity() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + virtual void knot_set_offset(Geom::Point offset); + void knot_click(guint state) override; + + /** Checks whether the index falls within the size of the parameter's vector */ + bool valid_index(unsigned int index) const { + return (_pparam->_vector.size() > index); + }; + +private: + PowerStrokePointArrayParam *_pparam; + unsigned int _index; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/random.cpp b/src/live_effects/parameter/random.cpp new file mode 100644 index 0000000..c5346d1 --- /dev/null +++ b/src/live_effects/parameter/random.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/random.h" +#include "live_effects/effect.h" +#include +#include "svg/svg.h" +#include "ui/widget/random.h" + +#include "svg/stringstream.h" + +#include "verbs.h" + +#define noLPERANDOMPARAM_DEBUG + +namespace Inkscape { + +namespace LivePathEffect { + + +RandomParam::RandomParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, gdouble default_value, long default_seed) + : Parameter(label, tip, key, wr, effect) +{ + defvalue = default_value; + value = defvalue; + min = -Geom::infinity(); + max = Geom::infinity(); + integer = false; + + defseed = default_seed; + startseed = defseed; + seed = startseed; +} + +RandomParam::~RandomParam() += default; + +bool +RandomParam::param_readSVGValue(const gchar * strvalue) +{ + double newval, newstartseed; + gchar** stringarray = g_strsplit (strvalue, ";", 2); + unsigned int success = sp_svg_number_read_d(stringarray[0], &newval); + if (success == 1) { + success += sp_svg_number_read_d(stringarray[1], &newstartseed); + if (success == 2) { + param_set_value(newval, static_cast(newstartseed)); + } else { + param_set_value(newval, defseed); + } + g_strfreev(stringarray); + return true; + } + g_strfreev(stringarray); + return false; +} + +Glib::ustring +RandomParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << value << ';' << startseed; + return os.str(); +} + +Glib::ustring +RandomParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue << ';' << defseed; + return os.str(); +} + +void +RandomParam::param_set_default() +{ + param_set_value(defvalue, defseed); +} + +void +RandomParam::param_update_default(gdouble default_value){ + defvalue = default_value; +} + +void +RandomParam::param_update_default(const gchar * default_value){ + double newval; + unsigned int success = sp_svg_number_read_d(default_value, &newval); + if (success == 1) { + param_update_default(newval); + } +} + +void +RandomParam::param_set_value(gdouble val, long newseed) +{ + value = val; + if (integer) + value = round(value); + if (value > max) + value = max; + if (value < min) + value = min; + + startseed = setup_seed(newseed); + seed = startseed; +} + +void +RandomParam::param_set_range(gdouble min, gdouble max) +{ + this->min = min; + this->max = max; +} + +void +RandomParam::param_make_integer(bool yes) +{ + integer = yes; +} + +void +RandomParam::resetRandomizer() +{ + seed = startseed; +} + + +Gtk::Widget * +RandomParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredRandom* regrandom = Gtk::manage( + new Inkscape::UI::Widget::RegisteredRandom( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + + regrandom->setValue(value, startseed); + if (integer) { + regrandom->setDigits(0); + regrandom->setIncrements(1, 10); + } + regrandom->setRange(min, max); + regrandom->setProgrammatically = false; + regrandom->signal_button_release_event().connect(sigc::mem_fun (*this, &RandomParam::on_button_release)); + + regrandom->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change random parameter")); + + return dynamic_cast (regrandom); +} + +bool RandomParam::on_button_release(GdkEventButton* button_event) { + param_effect->refresh_widgets = true; + return false; +} + +RandomParam::operator gdouble() +{ + return rand() * value; +}; + +/* RNG stolen from /display/nr-filter-turbulence.cpp */ +#define RAND_m 2147483647 /* 2**31 - 1 */ +#define RAND_a 16807 /* 7**5; primitive root of m */ +#define RAND_q 127773 /* m / a */ +#define RAND_r 2836 /* m % a */ +#define BSize 0x100 + +long +RandomParam::setup_seed(long lSeed) +{ + if (lSeed <= 0) lSeed = -(lSeed % (RAND_m - 1)) + 1; + if (lSeed > RAND_m - 1) lSeed = RAND_m - 1; + return lSeed; +} + +// generates random number between 0 and 1 +gdouble +RandomParam::rand() +{ + long result; + result = RAND_a * (seed % RAND_q) - RAND_r * (seed / RAND_q); + if (result <= 0) result += RAND_m; + seed = result; + + gdouble dresult = (gdouble)(result % BSize) / BSize; + return dresult; +} + + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/random.h b/src/live_effects/parameter/random.h new file mode 100644 index 0000000..e0bb07a --- /dev/null +++ b/src/live_effects/parameter/random.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_RANDOM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_RANDOM_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" +#include +#include <2geom/point.h> +#include <2geom/path.h> + +namespace Inkscape { + +namespace LivePathEffect { + +class RandomParam : public Parameter { +public: + RandomParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + gdouble default_value = 1.0, + long default_seed = 0); + ~RandomParam() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + + Gtk::Widget * param_newWidget() override; + + void param_set_value(gdouble val, long newseed); + void param_make_integer(bool yes = true); + void param_set_range(gdouble min, gdouble max); + void param_update_default(gdouble default_value); + void param_update_default(const gchar * default_value) override; + void resetRandomizer(); + operator gdouble(); + inline gdouble get_value() { return value; } ; + +protected: + long startseed; + long seed; + long defseed; + + gdouble value; + gdouble min; + gdouble max; + bool integer; + gdouble defvalue; + +private: + bool on_button_release(GdkEventButton* button_event); + long setup_seed(long); + gdouble rand(); + + RandomParam(const RandomParam&) = delete; + RandomParam& operator=(const RandomParam&) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/satellitesarray.cpp b/src/live_effects/parameter/satellitesarray.cpp new file mode 100644 index 0000000..7f15298 --- /dev/null +++ b/src/live_effects/parameter/satellitesarray.cpp @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz + * + * Copyright (C) 2014 Author(s) + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/satellitesarray.h" +#include "helper/geom.h" +#include "knotholder.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-fillet-chamfer.h" +#include "ui/dialog/lpe-fillet-chamfer-properties.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" + +#include "inkscape.h" +#include +// TODO due to internal breakage in glibmm headers, +// this has to be included last. +#include + +namespace Inkscape { + +namespace LivePathEffect { + +SatellitesArrayParam::SatellitesArrayParam(const Glib::ustring &label, + const Glib::ustring &tip, + const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, + Effect *effect) + : ArrayParam >(label, tip, key, wr, effect, 0), _knoth(nullptr) +{ + _knot_shape = SP_KNOT_SHAPE_DIAMOND; + _knot_mode = SP_KNOT_MODE_XOR; + _knot_color = 0xAAFF8800; + _use_distance = false; + _global_knot_hide = false; + _current_zoom = 0; + _effectType = FILLET_CHAMFER; + _last_pathvector_satellites = nullptr; + param_widget_is_visible(false); +} + + +void SatellitesArrayParam::set_oncanvas_looks(SPKnotShapeType shape, + SPKnotModeType mode, + guint32 color) +{ + _knot_shape = shape; + _knot_mode = mode; + _knot_color = color; +} + +void SatellitesArrayParam::setPathVectorSatellites(PathVectorSatellites *pathVectorSatellites, bool write) +{ + _last_pathvector_satellites = pathVectorSatellites; + if (write) { + param_set_and_write_new_value(_last_pathvector_satellites->getSatellites()); + } else { + param_setValue(_last_pathvector_satellites->getSatellites()); + } +} + +void SatellitesArrayParam::reloadKnots() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && tools_isactive(desktop, TOOLS_NODES)) { + Inkscape::UI::Tools::NodeTool *nt = static_cast(desktop->event_context); + if (nt) { + for (auto i = nt->_shape_editors.begin(); i != nt->_shape_editors.end(); ++i) { + Inkscape::UI::ShapeEditor *shape_editor = i->second; + if (shape_editor && shape_editor->lpeknotholder) { + SPItem *item = shape_editor->knotholder->item; + shape_editor->unset_item(true); + shape_editor->set_item(item); + } + } + } + } +} +void SatellitesArrayParam::setUseDistance(bool use_knot_distance) +{ + _use_distance = use_knot_distance; +} + +void SatellitesArrayParam::setCurrentZoom(double current_zoom) +{ + _current_zoom = current_zoom; +} + +void SatellitesArrayParam::setGlobalKnotHide(bool global_knot_hide) +{ + _global_knot_hide = global_knot_hide; +} + +void SatellitesArrayParam::setEffectType(EffectType et) +{ + _effectType = et; +} + +void SatellitesArrayParam::updateCanvasIndicators(bool mirror) +{ + if (!_last_pathvector_satellites) { + return; + } + + if (!_hp.empty()) { + _hp.clear(); + } + Geom::PathVector pathv = _last_pathvector_satellites->getPathVector(); + if (pathv.empty()) { + return; + } + if (mirror == true) { + _hp.clear(); + } + if (_effectType == FILLET_CHAMFER) { + for (size_t i = 0; i < _vector.size(); ++i) { + for (size_t j = 0; j < _vector[i].size(); ++j) { + if (_vector[i][j].hidden || //Ignore if hidden + (!_vector[i][j].has_mirror && mirror == true) || //Ignore if not have mirror and we are in mirror loop + _vector[i][j].amount == 0 || //no helper in 0 value + j >= count_path_nodes(pathv[i]) || //ignore last satellite in open paths with fillet chamfer effect + (!pathv[i].closed() && j == 0) || //ignore first satellites on open paths + count_path_nodes(pathv[i]) == 2) + { + continue; + } + Geom::Curve *curve_in = pathv[i][j].duplicate(); + double pos = 0; + bool overflow = false; + double size_out = _vector[i][j].arcDistance(*curve_in); + double length_out = curve_in->length(); + gint previous_index = j - 1; //Always are previous index because we skip first satellite on open paths + if (j == 0 && pathv[i].closed()) { + previous_index = count_path_nodes(pathv[i]) - 1; + } + if ( previous_index < 0 ) { + return; + } + double length_in = pathv.curveAt(previous_index).length(); + if (mirror) { + curve_in = const_cast(&pathv.curveAt(previous_index)); + pos = _vector[i][j].time(size_out, true, *curve_in); + if (length_out < size_out) { + overflow = true; + } + } else { + pos = _vector[i][j].time(*curve_in); + if (length_in < size_out) { + overflow = true; + } + } + if (pos <= 0 || pos >= 1) { + continue; + } + } + } + } + if (mirror) { + updateCanvasIndicators(false); + } +} +void SatellitesArrayParam::updateCanvasIndicators() +{ + updateCanvasIndicators(true); +} + +void SatellitesArrayParam::addCanvasIndicators( + SPLPEItem const */*lpeitem*/, std::vector &hp_vec) +{ + hp_vec.push_back(_hp); +} + +void SatellitesArrayParam::param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/options/transform/rectcorners", true)) { + for (auto & i : _vector) { + for (auto & j : i) { + if (!j.is_time && j.amount > 0) { + j.amount = j.amount * ((postmul.expansionX() + postmul.expansionY()) / 2); + } + } + } + param_set_and_write_new_value(_vector); + } +} + +void SatellitesArrayParam::addKnotHolderEntities(KnotHolder *knotholder, + SPItem *item, + bool mirror) +{ + if (!_last_pathvector_satellites) { + return; + } + size_t index = 0; + for (size_t i = 0; i < _vector.size(); ++i) { + for (size_t j = 0; j < _vector[i].size(); ++j) { + if (!_vector[i][j].has_mirror && mirror) { + continue; + } + SatelliteType type = _vector[i][j].satellite_type; + if (mirror && i == 0 && j == 0) { + index += _last_pathvector_satellites->getTotalSatellites(); + } + using namespace Geom; + //If is for filletChamfer effect... + if (_effectType == FILLET_CHAMFER) { + const gchar *tip; + if (type == CHAMFER) { + tip = _("Chamfer: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click reset"); + } else if (type == INVERSE_CHAMFER) { + tip = _("Inverse Chamfer: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click reset"); + } else if (type == INVERSE_FILLET) { + tip = _("Inverse Fillet: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click reset"); + } else { + tip = _("Fillet: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click reset"); + } + FilletChamferKnotHolderEntity *e = new FilletChamferKnotHolderEntity(this, index); + e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, _(tip), _knot_shape, _knot_mode, + _knot_color); + knotholder->add(e); + } + index++; + } + } + if (mirror) { + addKnotHolderEntities(knotholder, item, false); + } +} + +void SatellitesArrayParam::updateAmmount(double amount) +{ + Geom::PathVector const pathv = _last_pathvector_satellites->getPathVector(); + Satellites satellites = _last_pathvector_satellites->getSatellites(); + for (size_t i = 0; i < satellites.size(); ++i) { + for (size_t j = 0; j < satellites[i].size(); ++j) { + Geom::Curve const &curve_in = pathv[i][j]; + if (param_effect->isNodePointSelected(curve_in.initialPoint()) ){ + _vector[i][j].amount = amount; + _vector[i][j].setSelected(true); + } else { + _vector[i][j].setSelected(false); + } + } + } +} + +void SatellitesArrayParam::addKnotHolderEntities(KnotHolder *knotholder, + SPItem *item) +{ + _knoth = knotholder; + addKnotHolderEntities(knotholder, item, true); +} + +FilletChamferKnotHolderEntity::FilletChamferKnotHolderEntity( + SatellitesArrayParam *p, size_t index) + : _pparam(p), _index(index) {} + + +void FilletChamferKnotHolderEntity::knot_set(Geom::Point const &p, + Geom::Point const &/*origin*/, + guint state) +{ + if (!_pparam->_last_pathvector_satellites) { + return; + } + size_t total_satellites = _pparam->_last_pathvector_satellites->getTotalSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_satellites) { + index = _index - total_satellites; + is_mirror = true; + } + std::pair index_data = _pparam->_last_pathvector_satellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + + Geom::Point s = snap_knot_position(p, state); + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + Satellite satellite = _pparam->_vector[satelite_index][subsatelite_index]; + Geom::PathVector pathv = _pparam->_last_pathvector_satellites->getPathVector(); + if (satellite.hidden || + (!pathv[satelite_index].closed() && + (subsatelite_index == 0||//ignore first satellites on open paths + count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index))) //ignore last satellite in open paths with fillet chamfer effect + { + return; + } + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + Geom::Curve const &curve_in = pathv[satelite_index][previous_index]; + double mirror_time = Geom::nearest_time(s, curve_in); + Geom::Point mirror = curve_in.pointAt(mirror_time); + double normal_time = Geom::nearest_time(s, pathv[satelite_index][subsatelite_index]); + Geom::Point normal = pathv[satelite_index][subsatelite_index].pointAt(normal_time); + double distance_mirror = Geom::distance(mirror,s); + double distance_normal = Geom::distance(normal,s); + if (Geom::are_near(s, pathv[satelite_index][subsatelite_index].initialPoint(), 1.5 / _pparam->_current_zoom)) { + satellite.amount = 0; + } else if (distance_mirror < distance_normal) { + double time_start = 0; + Satellites satellites = _pparam->_last_pathvector_satellites->getSatellites(); + time_start = satellites[satelite_index][previous_index].time(curve_in); + if (time_start > mirror_time) { + mirror_time = time_start; + } + double size = arcLengthAt(mirror_time, curve_in); + double amount = curve_in.length() - size; + if (satellite.is_time) { + amount = timeAtArcLength(amount, pathv[satelite_index][subsatelite_index]); + } + satellite.amount = amount; + } else { + satellite.setPosition(s, pathv[satelite_index][subsatelite_index]); + } + Inkscape::LivePathEffect::LPEFilletChamfer *filletchamfer = dynamic_cast(_pparam->param_effect); + filletchamfer->helperpath = true; + _pparam->updateAmmount(satellite.amount); + _pparam->_vector[satelite_index][subsatelite_index] = satellite; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void +FilletChamferKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + Inkscape::LivePathEffect::LPEFilletChamfer *filletchamfer = dynamic_cast(_pparam->param_effect); + if (filletchamfer) { + filletchamfer->refresh_widgets = true; + filletchamfer->helperpath = false; + filletchamfer->writeParamsToSVG(); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + } +} + +Geom::Point FilletChamferKnotHolderEntity::knot_get() const +{ + if (!_pparam->_last_pathvector_satellites || _pparam->_global_knot_hide) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Geom::Point tmp_point; + size_t total_satellites = _pparam->_last_pathvector_satellites->getTotalSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_satellites) { + index = _index - total_satellites; + is_mirror = true; + } + std::pair index_data = _pparam->_last_pathvector_satellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Satellite satellite = _pparam->_vector[satelite_index][subsatelite_index]; + Geom::PathVector pathv = _pparam->_last_pathvector_satellites->getPathVector(); + if (satellite.hidden || + (!pathv[satelite_index].closed() && (subsatelite_index == 0 || + subsatelite_index == count_path_nodes(pathv[satelite_index]) - 1))) //ignore first and last satellites on open paths + { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + this->knot->show(); + if (is_mirror) { + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Geom::Curve const &curve_in = pathv[satelite_index][previous_index]; + double s = satellite.arcDistance(pathv[satelite_index][subsatelite_index]); + double t = satellite.time(s, true, curve_in); + if (t > 1) { + t = 1; + } + if (t < 0) { + t = 0; + } + double time_start = 0; + time_start = _pparam->_last_pathvector_satellites->getSatellites()[satelite_index][previous_index].time(curve_in); + if (time_start > t) { + t = time_start; + } + tmp_point = (curve_in).pointAt(t); + } else { + tmp_point = satellite.getPosition(pathv[satelite_index][subsatelite_index]); + } + Geom::Point const canvas_point = tmp_point; + return canvas_point; +} + +void FilletChamferKnotHolderEntity::knot_click(guint state) +{ + if (!_pparam->_last_pathvector_satellites) { + return; + } + size_t total_satellites = _pparam->_last_pathvector_satellites->getTotalSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_satellites) { + index = _index - total_satellites; + is_mirror = true; + } + std::pair index_data = _pparam->_last_pathvector_satellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + Geom::PathVector pathv = _pparam->_last_pathvector_satellites->getPathVector(); + if (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || + count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index)) //ignore last satellite in open paths with fillet chamfer effect + { + return; + } + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + _pparam->_vector[satelite_index][subsatelite_index].amount = 0.0; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + } else { + using namespace Geom; + SatelliteType type = _pparam->_vector[satelite_index][subsatelite_index].satellite_type; + switch (type) { + case FILLET: + type = INVERSE_FILLET; + break; + case INVERSE_FILLET: + type = CHAMFER; + break; + case CHAMFER: + type = INVERSE_CHAMFER; + break; + default: + type = FILLET; + break; + } + _pparam->_vector[satelite_index][subsatelite_index].satellite_type = type; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + const gchar *tip; + if (type == CHAMFER) { + tip = _("Chamfer: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click resets"); + } else if (type == INVERSE_CHAMFER) { + tip = _("Inverse Chamfer: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click resets"); + } else if (type == INVERSE_FILLET) { + tip = _("Inverse Fillet: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click resets"); + } else { + tip = _("Fillet: Ctrl+Click toggles type, " + "Shift+Click open dialog, " + "Ctrl+Alt+Click resets"); + } + this->knot->tip = g_strdup(tip); + this->knot->show(); + } + } else if (state & GDK_SHIFT_MASK) { + double amount = _pparam->_vector[satelite_index][subsatelite_index].amount; + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + if (!_pparam->_use_distance && !_pparam->_vector[satelite_index][subsatelite_index].is_time) { + amount = _pparam->_vector[satelite_index][subsatelite_index].lenToRad( + amount, pathv[satelite_index][previous_index], pathv[satelite_index][subsatelite_index], + _pparam->_vector[satelite_index][previous_index]); + } + bool aprox = false; + Geom::D2 d2_out = pathv[satelite_index][subsatelite_index].toSBasis(); + Geom::D2 d2_in = pathv[satelite_index][previous_index].toSBasis(); + aprox = ((d2_in)[0].degreesOfFreedom() != 2 || + d2_out[0].degreesOfFreedom() != 2) && + !_pparam->_use_distance + ? true + : false; + Inkscape::UI::Dialogs::FilletChamferPropertiesDialog::showDialog( + this->desktop, amount, this, _pparam->_use_distance, aprox, + _pparam->_vector[satelite_index][subsatelite_index]); + } +} + +void FilletChamferKnotHolderEntity::knot_set_offset(Satellite satellite) +{ + if (!_pparam->_last_pathvector_satellites) { + return; + } + size_t total_satellites = _pparam->_last_pathvector_satellites->getTotalSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_satellites) { + index = _index - total_satellites; + is_mirror = true; + } + std::pair index_data = _pparam->_last_pathvector_satellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + Geom::PathVector pathv = _pparam->_last_pathvector_satellites->getPathVector(); + if (satellite.hidden || + (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index))) //ignore last satellite in open paths with fillet chamfer effect + { + return; + } + double amount = satellite.amount; + double max_amount = amount; + if (!_pparam->_use_distance && !satellite.is_time) { + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + amount = _pparam->_vector[satelite_index][subsatelite_index].radToLen( + amount, pathv[satelite_index][previous_index], pathv[satelite_index][subsatelite_index]); + if (max_amount > 0 && amount == 0) { + amount = _pparam->_vector[satelite_index][subsatelite_index].amount; + } + } + satellite.amount = amount; + _pparam->_vector[satelite_index][subsatelite_index] = satellite; + this->parent_holder->knot_ungrabbed_handler(this->knot, 0); + SPLPEItem *splpeitem = dynamic_cast(item); + if (splpeitem) { + sp_lpe_item_update_patheffect(splpeitem, false, false); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/satellitesarray.h b/src/live_effects/parameter/satellitesarray.h new file mode 100644 index 0000000..c15b710 --- /dev/null +++ b/src/live_effects/parameter/satellitesarray.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_SATELLITES_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_SATELLITES_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * Copyright (C) Jabiertxo Arraiza Cenoz + * Special thanks to Johan Engelen for the base of the effect -powerstroke- + * Also to ScislaC for pointing me to the idea + * Also su_v for his constructive feedback and time + * To Nathan Hurst for his review and help on refactor + * and finally to Liam P. White for his big help on coding, + * that saved me a lot of hours + * + * + * This parameter acts as a bridge from pathVectorSatellites class to serialize it as a LPE + * parameter + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/array.h" +#include "live_effects/effect-enum.h" +#include "helper/geom-pathvectorsatellites.h" +#include "knot-holder-entity.h" +#include + +namespace Inkscape { + +namespace LivePathEffect { + +class FilletChamferKnotHolderEntity; + +class SatellitesArrayParam : public ArrayParam > { +public: + SatellitesArrayParam(const Glib::ustring &label, const Glib::ustring &tip, + const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect); + + Gtk::Widget *param_newWidget() override + { + return nullptr; + } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + virtual void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item, bool mirror); + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector &hp_vec) override; + virtual void updateCanvasIndicators(); + virtual void updateCanvasIndicators(bool mirror); + bool providesKnotHolderEntities() const override + { + return true; + } + void param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) override; + void setUseDistance(bool use_knot_distance); + void setCurrentZoom(double current_zoom); + void setGlobalKnotHide(bool global_knot_hide); + void setEffectType(EffectType et); + void reloadKnots(); + void updateAmmount(double amount); + void setPathVectorSatellites(PathVectorSatellites *pathVectorSatellites, bool write = true); + void set_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + + friend class FilletChamferKnotHolderEntity; + friend class LPEFilletChamfer; + +protected: + KnotHolder *_knoth; + +private: + SatellitesArrayParam(const SatellitesArrayParam &) = delete; + SatellitesArrayParam &operator=(const SatellitesArrayParam &) = delete; + + SPKnotShapeType _knot_shape; + SPKnotModeType _knot_mode; + guint32 _knot_color; + Geom::PathVector _hp; + bool _use_distance; + bool _global_knot_hide; + double _current_zoom; + EffectType _effectType; + PathVectorSatellites *_last_pathvector_satellites; + +}; + +class FilletChamferKnotHolderEntity : public KnotHolderEntity { +public: + FilletChamferKnotHolderEntity(SatellitesArrayParam *p, size_t index); + ~FilletChamferKnotHolderEntity() override + { + _pparam->_knoth = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, + guint state) override; + Geom::Point knot_get() const override; + void knot_click(guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_set_offset(Satellite); + /** Checks whether the index falls within the size of the parameter's vector + */ + bool valid_index(size_t index, size_t subindex) const + { + return (_pparam->_vector.size() > index && _pparam->_vector[index].size() > subindex); + }; + +private: + SatellitesArrayParam *_pparam; + size_t _index; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/text.cpp b/src/live_effects/parameter/text.cpp new file mode 100644 index 0000000..0a32bf7 --- /dev/null +++ b/src/live_effects/parameter/text.cpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Maximilian Albert 2008 + * + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include + +#include "live_effects/parameter/text.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "inkscape.h" +#include "verbs.h" +#include "display/canvas-text.h" +#include <2geom/sbasis-geometric.h> + +#include + +namespace Inkscape { + +namespace LivePathEffect { + +TextParam::TextParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value ) + : Parameter(label, tip, key, wr, effect), + value(default_value), + defvalue(default_value), + _hide_canvas_text(false) +{ + if (SPDesktop *desktop = SP_ACTIVE_DESKTOP) { // FIXME: we shouldn't use this! + canvas_text = (SPCanvasText *) sp_canvastext_new(desktop->getTempGroup(), desktop, Geom::Point(0,0), ""); + sp_canvastext_set_text (canvas_text, default_value.c_str()); + sp_canvastext_set_coords (canvas_text, 0, 0); + } else { + _hide_canvas_text = true; + } +} + +void +TextParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +TextParam::param_update_default(const gchar * default_value) +{ + defvalue = (Glib::ustring)default_value; +} + +void +TextParam::param_hide_canvas_text() +{ + if (!_hide_canvas_text) { + sp_canvastext_set_text(canvas_text, " "); + _hide_canvas_text = true; + } +} + +void +TextParam::setPos(Geom::Point pos) +{ + if (!_hide_canvas_text) { + sp_canvastext_set_coords (canvas_text, pos); + } +} + +void +TextParam::setPosAndAnchor(const Geom::Piecewise > &pwd2, + const double t, const double length, bool /*use_curvature*/) +{ + using namespace Geom; + + Piecewise > pwd2_reparam = arc_length_parametrization(pwd2, 2 , 0.1); + double t_reparam = pwd2_reparam.cuts.back() * t; + Point pos = pwd2_reparam.valueAt(t_reparam); + Point dir = unit_vector(derivative(pwd2_reparam).valueAt(t_reparam)); + Point n = -rot90(dir); + double angle = Geom::angle_between(dir, Point(1,0)); + if (!_hide_canvas_text) { + sp_canvastext_set_coords(canvas_text, pos + n * length); + sp_canvastext_set_anchor_manually(canvas_text, std::sin(angle), -std::cos(angle)); + } +} + +void +TextParam::setAnchor(double x_value, double y_value) +{ + anchor_x = x_value; + anchor_y = y_value; + if (!_hide_canvas_text) { + sp_canvastext_set_anchor_manually (canvas_text, anchor_x, anchor_y); + } +} + +bool +TextParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +TextParam::param_getSVGValue() const +{ + return value; +} + +Glib::ustring +TextParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +void +TextParam::setTextParam(Inkscape::UI::Widget::RegisteredText *rsu) +{ + Glib::ustring str(rsu->getText()); + param_setValue(str); + write_to_SVG(); +} + +Gtk::Widget * +TextParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredText *rsu = Gtk::manage(new Inkscape::UI::Widget::RegisteredText( + param_label, param_tooltip, param_key, *param_wr, param_effect->getRepr(), param_effect->getSPDoc())); + rsu->setText(value); + rsu->setProgrammatically = false; + rsu->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change text parameter")); + Gtk::Box *text_container = Gtk::manage(new Gtk::Box()); + Gtk::Button *set = Gtk::manage(new Gtk::Button(Glib::ustring("✔"))); + set->signal_clicked() + .connect(sigc::bind(sigc::mem_fun(*this, &TextParam::setTextParam),rsu)); + text_container->pack_start(*rsu, false, false, 2); + text_container->pack_start(*set, false, false, 2); + Gtk::Widget *return_widg = dynamic_cast (text_container); + return_widg->set_halign(Gtk::ALIGN_END); + return return_widg; +} + +void +TextParam::param_setValue(const Glib::ustring newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; + if (!_hide_canvas_text) { + sp_canvastext_set_text (canvas_text, newvalue.c_str()); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/text.h b/src/live_effects/parameter/text.h new file mode 100644 index 0000000..2d25c61 --- /dev/null +++ b/src/live_effects/parameter/text.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TEXT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TEXT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "display/canvas-bpath.h" +#include "live_effects/parameter/parameter.h" + +struct SPCanvasText; + +namespace Inkscape { + +namespace LivePathEffect { + +class TextParam : public Parameter { +public: + TextParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = ""); + ~TextParam() override = default; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue); + void param_hide_canvas_text(); + void setTextParam(Inkscape::UI::Widget::RegisteredText *rsu); + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + void setPos(Geom::Point pos); + void setPosAndAnchor(const Geom::Piecewise > &pwd2, + const double t, const double length, bool use_curvature = false); + void setAnchor(double x_value, double y_value); + + const Glib::ustring get_value() const { return value; }; + +private: + TextParam(const TextParam&) = delete; + TextParam& operator=(const TextParam&) = delete; + double anchor_x; + double anchor_y; + bool _hide_canvas_text; + Glib::ustring value; + Glib::ustring defvalue; + + SPCanvasText *canvas_text; +}; + +/* + * This parameter does not display a widget in the LPE dialog; LPEs can use it to display on-canvas + * text that should not be settable by the user. Note that since no widget is provided, the + * parameter must be initialized differently than usual (only with a pointer to the parent effect; + * no label, no tooltip, etc.). + */ +class TextParamInternal : public TextParam { +public: + TextParamInternal(Effect* effect) : + TextParam("", "", "", nullptr, effect) {} + + Gtk::Widget * param_newWidget() override { return nullptr; } +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/togglebutton.cpp b/src/live_effects/parameter/togglebutton.cpp new file mode 100644 index 0000000..afabefc --- /dev/null +++ b/src/live_effects/parameter/togglebutton.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 + * Copyright (C) Jabiertxo Arraiza Cenoz 2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include + +#include + +#include "helper-fns.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/togglebutton.h" +#include "selection.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-loader.h" +#include "verbs.h" + +namespace Inkscape { + +namespace LivePathEffect { + +ToggleButtonParam::ToggleButtonParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool default_value, + Glib::ustring inactive_label, char const *_icon_active, char const *_icon_inactive, + Gtk::BuiltinIconSize _icon_size) + : Parameter(label, tip, key, wr, effect) + , value(default_value) + , defvalue(default_value) + , inactive_label(std::move(inactive_label)) + , _icon_active(_icon_active) + , _icon_inactive(_icon_inactive) + , _icon_size(_icon_size) +{ + checkwdg = nullptr; +} + +ToggleButtonParam::~ToggleButtonParam() +{ + if (_toggled_connection.connected()) { + _toggled_connection.disconnect(); + } +} + +void +ToggleButtonParam::param_set_default() +{ + param_setValue(defvalue); +} + +bool +ToggleButtonParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(helperfns_read_bool(strvalue, defvalue)); + return true; // not correct: if value is unacceptable, should return false! +} + +Glib::ustring +ToggleButtonParam::param_getSVGValue() const +{ + return value ? "true" : "false"; +} + +Glib::ustring +ToggleButtonParam::param_getDefaultSVGValue() const +{ + return defvalue ? "true" : "false"; +} + +void +ToggleButtonParam::param_update_default(bool default_value) +{ + defvalue = default_value; +} + +void +ToggleButtonParam::param_update_default(const gchar * default_value) +{ + param_update_default(helperfns_read_bool(default_value, defvalue)); +} + +Gtk::Widget * +ToggleButtonParam::param_newWidget() +{ + if (_toggled_connection.connected()) { + _toggled_connection.disconnect(); + } + + checkwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredToggleButton(param_label, + param_tooltip, + param_key, + *param_wr, + false, + param_effect->getRepr(), + param_effect->getSPDoc()) ); + auto box_button = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); + box_button->set_homogeneous(false); + Gtk::Label *label = new Gtk::Label(""); + if (!param_label.empty()) { + if (value || inactive_label.empty()) { + label->set_text(param_label.c_str()); + } else { + label->set_text(inactive_label.c_str()); + } + } + label->show(); + if (_icon_active) { + if (!_icon_inactive) { + _icon_inactive = _icon_active; + } + box_button->show(); + Gtk::Widget *icon_button = nullptr; + if (!value) { + icon_button = sp_get_icon_image(_icon_inactive, _icon_size); + } else { + icon_button = sp_get_icon_image(_icon_active, _icon_size); + } + icon_button->show(); + box_button->pack_start(*icon_button, false, false, 1); + if (!param_label.empty()) { + box_button->pack_start(*label, false, false, 1); + } + } else { + box_button->pack_start(*label, false, false, 1); + } + + checkwdg->add(*Gtk::manage(box_button)); + checkwdg->setActive(value); + checkwdg->setProgrammatically = false; + checkwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change togglebutton parameter")); + + _toggled_connection = checkwdg->signal_toggled().connect(sigc::mem_fun(*this, &ToggleButtonParam::toggled)); + return checkwdg; +} + +void +ToggleButtonParam::refresh_button() +{ + if (!_toggled_connection.connected()) { + return; + } + + if(!checkwdg){ + return; + } + Gtk::Container *box_button = dynamic_cast(checkwdg->get_child()); + if(!box_button){ + return; + } + std::vector children = box_button->get_children(); + if (!param_label.empty()) { + Gtk::Label *lab = dynamic_cast(children[children.size()-1]); + if (!lab) return; + if(value || inactive_label.empty()){ + lab->set_text(param_label.c_str()); + }else{ + lab->set_text(inactive_label.c_str()); + } + } + if ( _icon_active ) { + Gdk::Pixbuf *icon_pixbuf = nullptr; + Gtk::Widget *im = dynamic_cast(children[0]); + if (!im) return; + if (!value) { + im = sp_get_icon_image(_icon_inactive, _icon_size); + } else { + im = sp_get_icon_image(_icon_active, _icon_size); + } + } +} + +void +ToggleButtonParam::param_setValue(bool newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; + refresh_button(); +} + +void +ToggleButtonParam::toggled() { + if (SP_ACTIVE_DESKTOP) { + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + selection->emitModified(); + } + _signal_toggled.emit(); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/togglebutton.h b/src/live_effects/parameter/togglebutton.h new file mode 100644 index 0000000..1c44ffb --- /dev/null +++ b/src/live_effects/parameter/togglebutton.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TOGGLEBUTTON_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TOGGLEBUTTON_H + +/* + * Copyright (C) Jabiertxo Arraiza Cenoz 2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "live_effects/parameter/parameter.h" +#include "ui/widget/registered-widget.h" + +namespace Inkscape { + +namespace LivePathEffect { + +/** + * class ToggleButtonParam: + * represents a Gtk::ToggleButton as a Live Path Effect parameter + */ +class ToggleButtonParam : public Parameter { +public: + ToggleButtonParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool default_value = false, + Glib::ustring inactive_label = "", char const *icon_active = nullptr, + char const *icon_inactive = nullptr, Gtk::BuiltinIconSize icon_size = Gtk::ICON_SIZE_SMALL_TOOLBAR); + ~ToggleButtonParam() override; + ToggleButtonParam(const ToggleButtonParam &) = delete; + ToggleButtonParam &operator=(const ToggleButtonParam &) = delete; + + Gtk::Widget *param_newWidget() override; + + bool param_readSVGValue(const gchar *strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(bool newvalue); + void param_set_default() override; + + bool get_value() const { return value; }; + + inline operator bool() const { return value; }; + + sigc::signal &signal_toggled() { return _signal_toggled; } + virtual void toggled(); + void param_update_default(bool default_value); + void param_update_default(const gchar *default_value) override; + +private: + void refresh_button(); + bool value; + bool defvalue; + const Glib::ustring inactive_label; + const char * _icon_active; + const char * _icon_inactive; + Gtk::BuiltinIconSize _icon_size; + Inkscape::UI::Widget::RegisteredToggleButton * checkwdg; + + sigc::signal _signal_toggled; + sigc::connection _toggled_connection; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/transformedpoint.cpp b/src/live_effects/parameter/transformedpoint.cpp new file mode 100644 index 0000000..694f946 --- /dev/null +++ b/src/live_effects/parameter/transformedpoint.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include "live_effects/parameter/transformedpoint.h" + +#include "knotholder.h" +#include "svg/svg.h" +#include "svg/stringstream.h" + +#include "live_effects/effect.h" +#include "desktop.h" +#include "verbs.h" + +#include + +namespace Inkscape { + +namespace LivePathEffect { + +TransformedPointParam::TransformedPointParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Geom::Point default_vector, + bool dontTransform) + : Parameter(label, tip, key, wr, effect), + defvalue(default_vector), + origin(0.,0.), + vector(default_vector), + noTransform(dontTransform) +{ + vec_knot_shape = SP_KNOT_SHAPE_DIAMOND; + vec_knot_mode = SP_KNOT_MODE_XOR; + vec_knot_color = 0xffffb500; +} + +TransformedPointParam::~TransformedPointParam() += default; + +void +TransformedPointParam::param_set_default() +{ + setOrigin(Geom::Point(0.,0.)); + setVector(defvalue); +} + +bool +TransformedPointParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 4); + if (!strarray) { + return false; + } + double val[4]; + unsigned int i = 0; + while (i < 4 && strarray[i]) { + if (sp_svg_number_read_d(strarray[i], &val[i]) != 0) { + i++; + } else { + break; + } + } + g_strfreev (strarray); + if (i == 4) { + setOrigin( Geom::Point(val[0], val[1]) ); + setVector( Geom::Point(val[2], val[3]) ); + return true; + } + return false; +} + +Glib::ustring +TransformedPointParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + return os.str(); +} + +Glib::ustring +TransformedPointParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void +TransformedPointParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +TransformedPointParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +Gtk::Widget * +TransformedPointParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredVector * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredVector( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + pointwdg->setPolarCoords(); + pointwdg->setValue( vector, origin ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change vector parameter")); + + Gtk::HBox * hbox = Gtk::manage( new Gtk::HBox() ); + static_cast(hbox)->pack_start(*pointwdg, true, true); + static_cast(hbox)->show_all_children(); + + return dynamic_cast (hbox); +} + +void +TransformedPointParam::set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector) +{ + setValues(new_origin, new_vector); + param_write_to_repr(param_getSVGValue().c_str()); +} + +void +TransformedPointParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + if (!noTransform) { + set_and_write_new_values( origin * postmul, vector * postmul.withoutTranslation() ); + } +} + + +void +TransformedPointParam::set_vector_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + vec_knot_shape = shape; + vec_knot_mode = mode; + vec_knot_color = color; +} + +void +TransformedPointParam::set_oncanvas_color(guint32 color) +{ + vec_knot_color = color; +} + +class TransformedPointParamKnotHolderEntity_Vector : public KnotHolderEntity { +public: + TransformedPointParamKnotHolderEntity_Vector(TransformedPointParam *p) : param(p) { } + ~TransformedPointParamKnotHolderEntity_Vector() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) override { + Geom::Point const s = p - param->origin; + /// @todo implement angle snapping when holding CTRL + param->setVector(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + Geom::Point knot_get() const override{ + return param->origin + param->vector; + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the vector handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + TransformedPointParam *param; +}; + +void +TransformedPointParam::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) +{ + TransformedPointParamKnotHolderEntity_Vector *vector_e = new TransformedPointParamKnotHolderEntity_Vector(this); + vector_e->create(desktop, item, knotholder, Inkscape::CTRL_TYPE_LPE, handleTip(), vec_knot_shape, vec_knot_mode, + vec_knot_color); + knotholder->add(vector_e); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/transformedpoint.h b/src/live_effects/parameter/transformedpoint.h new file mode 100644 index 0000000..3fd6989 --- /dev/null +++ b/src/live_effects/parameter/transformedpoint.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TRANSFORMED_POINT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TRANSFORMED_POINT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/point.h> + +#include "live_effects/parameter/parameter.h" + +#include "knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class TransformedPointParam : public Parameter { +public: + TransformedPointParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Geom::Point default_vector = Geom::Point(1,0), + bool dontTransform = false); + ~TransformedPointParam() override; + + Gtk::Widget * param_newWidget() override; + inline const gchar *handleTip() const { return param_tooltip.c_str(); } + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + Geom::Point getVector() const { return vector; }; + Geom::Point getOrigin() const { return origin; }; + void setValues(Geom::Point const &new_origin, Geom::Point const &new_vector) { setVector(new_vector); setOrigin(new_origin); }; + void setVector(Geom::Point const &new_vector) { vector = new_vector; }; + void setOrigin(Geom::Point const &new_origin) { origin = new_origin; }; + void param_set_default() override; + + void set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector); + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void set_vector_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + void set_oncanvas_color(guint32 color); + Geom::Point param_get_default() { return defvalue; } + void param_update_default(Geom::Point default_point); + void param_update_default(const gchar * default_point) override; + bool providesKnotHolderEntities() const override { return true; } + virtual void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +private: + TransformedPointParam(const TransformedPointParam&) = delete; + TransformedPointParam& operator=(const TransformedPointParam&) = delete; + + Geom::Point defvalue; + + Geom::Point origin; + Geom::Point vector; + + bool noTransform; + + /// The looks of the vector and origin knots oncanvas + SPKnotShapeType vec_knot_shape; + SPKnotModeType vec_knot_mode; + guint32 vec_knot_color; + + friend class TransformedPointParamKnotHolderEntity_Vector; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/unit.cpp b/src/live_effects/parameter/unit.cpp new file mode 100644 index 0000000..65a939b --- /dev/null +++ b/src/live_effects/parameter/unit.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include + +#include "live_effects/parameter/unit.h" +#include "live_effects/effect.h" +#include "verbs.h" +#include "util/units.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { + +namespace LivePathEffect { + + +UnitParam::UnitParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Glib::ustring default_unit) + : Parameter(label, tip, key, wr, effect) +{ + defunit = unit_table.getUnit(default_unit); + unit = defunit; +} + +UnitParam::~UnitParam() += default; + +bool +UnitParam::param_readSVGValue(const gchar * strvalue) +{ + if (strvalue) { + param_set_value(*unit_table.getUnit(strvalue)); + return true; + } + return false; +} + +Glib::ustring +UnitParam::param_getSVGValue() const +{ + return unit->abbr; +} + +Glib::ustring +UnitParam::param_getDefaultSVGValue() const +{ + return defunit->abbr; +} + +void +UnitParam::param_set_default() +{ + param_set_value(*defunit); +} + +void +UnitParam::param_update_default(const gchar * default_unit) +{ + defunit = unit_table.getUnit((Glib::ustring)default_unit); +} + +void +UnitParam::param_set_value(Inkscape::Util::Unit const &val) +{ + param_effect->refresh_widgets = true; + unit = new Inkscape::Util::Unit(val); +} + +const gchar * +UnitParam::get_abbreviation() const +{ + return unit->abbr.c_str(); +} + +Gtk::Widget * +UnitParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredUnitMenu* unit_menu = Gtk::manage( + new Inkscape::UI::Widget::RegisteredUnitMenu(param_label, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc())); + + unit_menu->setUnit(unit->abbr); + unit_menu->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change unit parameter")); + + return dynamic_cast (unit_menu); +} + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/unit.h b/src/live_effects/parameter/unit.h new file mode 100644 index 0000000..b87d29a --- /dev/null +++ b/src/live_effects/parameter/unit.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_UNIT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_UNIT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Maximilian Albert 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace LivePathEffect { + +class UnitParam : public Parameter { +public: + UnitParam(const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Glib::ustring default_unit = "px"); + ~UnitParam() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_set_value(Inkscape::Util::Unit const &val); + void param_update_default(const gchar * default_unit) override; + const gchar *get_abbreviation() const; + Gtk::Widget * param_newWidget() override; + + operator Inkscape::Util::Unit const *() const { return unit; } + +private: + Inkscape::Util::Unit const *unit; + Inkscape::Util::Unit const *defunit; + + UnitParam(const UnitParam&) = delete; + UnitParam& operator=(const UnitParam&) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/vector.cpp b/src/live_effects/parameter/vector.cpp new file mode 100644 index 0000000..ab0a9ae --- /dev/null +++ b/src/live_effects/parameter/vector.cpp @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/registered-widget.h" +#include + +#include "live_effects/parameter/vector.h" + +#include "knotholder.h" +#include "svg/svg.h" +#include "svg/stringstream.h" + +#include "live_effects/effect.h" +#include "verbs.h" + +namespace Inkscape { + +namespace LivePathEffect { + +VectorParam::VectorParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Geom::Point default_vector) + : Parameter(label, tip, key, wr, effect), + defvalue(default_vector), + origin(0.,0.), + vector(default_vector) +{ + vec_knot_shape = SP_KNOT_SHAPE_DIAMOND; + vec_knot_mode = SP_KNOT_MODE_XOR; + vec_knot_color = 0xffffb500; + ori_knot_shape = SP_KNOT_SHAPE_CIRCLE; + ori_knot_mode = SP_KNOT_MODE_XOR; + ori_knot_color = 0xffffb500; +} + +VectorParam::~VectorParam() += default; + +void +VectorParam::param_set_default() +{ + setOrigin(Geom::Point(0.,0.)); + setVector(defvalue); +} + +void +VectorParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +VectorParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +bool +VectorParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 4); + if (!strarray) { + return false; + } + double val[4]; + unsigned int i = 0; + while (i < 4 && strarray[i]) { + if (sp_svg_number_read_d(strarray[i], &val[i]) != 0) { + i++; + } else { + break; + } + } + g_strfreev (strarray); + if (i == 4) { + setOrigin( Geom::Point(val[0], val[1]) ); + setVector( Geom::Point(val[2], val[3]) ); + return true; + } + return false; +} + +Glib::ustring +VectorParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + return os.str(); +} + +Glib::ustring +VectorParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +Gtk::Widget * +VectorParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredVector * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredVector( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + pointwdg->setPolarCoords(); + pointwdg->setValue( vector, origin ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(SP_VERB_DIALOG_LIVE_PATH_EFFECT, _("Change vector parameter")); + + Gtk::HBox * hbox = Gtk::manage( new Gtk::HBox() ); + static_cast(hbox)->pack_start(*pointwdg, true, true); + static_cast(hbox)->show_all_children(); + + return dynamic_cast (hbox); +} + +void +VectorParam::set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector) +{ + setValues(new_origin, new_vector); + param_write_to_repr(param_getSVGValue().c_str()); +} + +void +VectorParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + set_and_write_new_values( origin * postmul, vector * postmul.withoutTranslation() ); +} + + +void +VectorParam::set_vector_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + vec_knot_shape = shape; + vec_knot_mode = mode; + vec_knot_color = color; +} + +void +VectorParam::set_origin_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color) +{ + ori_knot_shape = shape; + ori_knot_mode = mode; + ori_knot_color = color; +} + +void +VectorParam::set_oncanvas_color(guint32 color) +{ + vec_knot_color = color; + ori_knot_color = color; +} + +class VectorParamKnotHolderEntity_Origin : public KnotHolderEntity { +public: + VectorParamKnotHolderEntity_Origin(VectorParam *p) : param(p) { } + ~VectorParamKnotHolderEntity_Origin() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) override { + Geom::Point const s = snap_knot_position(p, state); + param->setOrigin(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + Geom::Point knot_get() const override { + return param->origin; + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the origin handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + VectorParam *param; +}; + +class VectorParamKnotHolderEntity_Vector : public KnotHolderEntity { +public: + VectorParamKnotHolderEntity_Vector(VectorParam *p) : param(p) { } + ~VectorParamKnotHolderEntity_Vector() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) override { + Geom::Point const s = p - param->origin; + /// @todo implement angle snapping when holding CTRL + param->setVector(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + Geom::Point knot_get() const override { + return param->origin + param->vector; + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the vector handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + VectorParam *param; +}; + +void +VectorParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + VectorParamKnotHolderEntity_Origin *origin_e = new VectorParamKnotHolderEntity_Origin(this); + origin_e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, handleTip(), ori_knot_shape, ori_knot_mode, + ori_knot_color); + knotholder->add(origin_e); + + VectorParamKnotHolderEntity_Vector *vector_e = new VectorParamKnotHolderEntity_Vector(this); + vector_e->create(nullptr, item, knotholder, Inkscape::CTRL_TYPE_LPE, handleTip(), vec_knot_shape, vec_knot_mode, + vec_knot_color); + knotholder->add(vector_e); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/parameter/vector.h b/src/live_effects/parameter/vector.h new file mode 100644 index 0000000..d9894e9 --- /dev/null +++ b/src/live_effects/parameter/vector.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_VECTOR_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_VECTOR_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2008 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/point.h> + +#include "live_effects/parameter/parameter.h" + +#include "knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class VectorParam : public Parameter { +public: + VectorParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Geom::Point default_vector = Geom::Point(1,0) ); + ~VectorParam() override; + + Gtk::Widget * param_newWidget() override; + inline const gchar *handleTip() const { return param_tooltip.c_str(); } + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + Geom::Point getVector() const { return vector; }; + Geom::Point getOrigin() const { return origin; }; + void setValues(Geom::Point const &new_origin, Geom::Point const &new_vector) { setVector(new_vector); setOrigin(new_origin); }; + void setVector(Geom::Point const &new_vector) { vector = new_vector; }; + void setOrigin(Geom::Point const &new_origin) { origin = new_origin; }; + void param_set_default() override; + + void set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector); + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void set_vector_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + void set_origin_oncanvas_looks(SPKnotShapeType shape, SPKnotModeType mode, guint32 color); + void set_oncanvas_color(guint32 color); + void param_update_default(Geom::Point default_point); + void param_update_default(const gchar * default_point) override; + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + +private: + VectorParam(const VectorParam&) = delete; + VectorParam& operator=(const VectorParam&) = delete; + + Geom::Point defvalue; + + Geom::Point origin; + Geom::Point vector; + + /// The looks of the vector and origin knots oncanvas + SPKnotShapeType vec_knot_shape; + SPKnotModeType vec_knot_mode; + guint32 vec_knot_color; + SPKnotShapeType ori_knot_shape; + SPKnotModeType ori_knot_mode; + guint32 ori_knot_color; + + friend class VectorParamKnotHolderEntity_Origin; + friend class VectorParamKnotHolderEntity_Vector; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/spiro-converters.cpp b/src/live_effects/spiro-converters.cpp new file mode 100644 index 0000000..411fb65 --- /dev/null +++ b/src/live_effects/spiro-converters.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Johan Engelen + * + * Copyright (C) 2010-2012 Authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "spiro-converters.h" +#include <2geom/path.h> +#include "display/curve.h" +#include + +#define SPIRO_SHOW_INFINITE_COORDINATE_CALLS +#ifdef SPIRO_SHOW_INFINITE_COORDINATE_CALLS +# define SPIRO_G_MESSAGE(x) g_message(x) +#else +# define SPIRO_G_MESSAGE(x) +#endif + +namespace Spiro { + +void +ConverterSPCurve::moveto(double x, double y) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _curve.moveto(x, y); + } else { + SPIRO_G_MESSAGE("Spiro: moveto not finite"); + } +} + +void +ConverterSPCurve::lineto(double x, double y, bool close_last) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _curve.lineto(x, y); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: lineto not finite"); + } +} + +void +ConverterSPCurve::quadto(double xm, double ym, double x3, double y3, bool close_last) +{ + if ( std::isfinite(xm) && std::isfinite(ym) && std::isfinite(x3) && std::isfinite(y3) ) { + _curve.quadto(xm, ym, x3, y3); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: quadto not finite"); + } +} + +void +ConverterSPCurve::curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) +{ + if ( std::isfinite(x1) && std::isfinite(y1) && std::isfinite(x2) && std::isfinite(y2) ) { + _curve.curveto(x1, y1, x2, y2, x3, y3); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: curveto not finite"); + } +} + + +ConverterPath::ConverterPath(Geom::Path &path) + : _path(path) +{ + _path.setStitching(true); +} + +void +ConverterPath::moveto(double x, double y) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _path.start(Geom::Point(x, y)); + } else { + SPIRO_G_MESSAGE("spiro moveto not finite"); + } +} + +void +ConverterPath::lineto(double x, double y, bool close_last) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _path.appendNew( Geom::Point(x, y) ); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro lineto not finite"); + } +} + +void +ConverterPath::quadto(double xm, double ym, double x3, double y3, bool close_last) +{ + if ( std::isfinite(xm) && std::isfinite(ym) && std::isfinite(x3) && std::isfinite(y3) ) { + _path.appendNew(Geom::Point(xm, ym), Geom::Point(x3, y3)); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro quadto not finite"); + } +} + +void +ConverterPath::curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) +{ + if ( std::isfinite(x1) && std::isfinite(y1) && std::isfinite(x2) && std::isfinite(y2) ) { + _path.appendNew(Geom::Point(x1, y1), Geom::Point(x2, y2), Geom::Point(x3, y3)); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro curveto not finite"); + } +} + +} // namespace Spiro + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/spiro-converters.h b/src/live_effects/spiro-converters.h new file mode 100644 index 0000000..98041a2 --- /dev/null +++ b/src/live_effects/spiro-converters.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SPIRO_CONVERTERS_H +#define INKSCAPE_SPIRO_CONVERTERS_H + +#include <2geom/forward.h> +class SPCurve; + +namespace Spiro { + +class ConverterBase { +public: + ConverterBase() = default;; + virtual ~ConverterBase() = default;; + + virtual void moveto(double x, double y) = 0; + virtual void lineto(double x, double y, bool close_last) = 0; + virtual void quadto(double x1, double y1, double x2, double y2, bool close_last) = 0; + virtual void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) = 0; +}; + + +/** + * Converts Spiro to Inkscape's SPCurve + */ +class ConverterSPCurve : public ConverterBase { +public: + ConverterSPCurve(SPCurve &curve) + : _curve(curve) + {} + + void moveto(double x, double y) override; + void lineto(double x, double y, bool close_last) override; + void quadto(double x1, double y1, double x2, double y2, bool close_last) override; + void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) override; + +private: + SPCurve &_curve; + + ConverterSPCurve(const ConverterSPCurve&) = delete; + ConverterSPCurve& operator=(const ConverterSPCurve&) = delete; +}; + + +/** + * Converts Spiro to 2Geom's Path + */ +class ConverterPath : public ConverterBase { +public: + ConverterPath(Geom::Path &path); + + void moveto(double x, double y) override; + void lineto(double x, double y, bool close_last) override; + void quadto(double x1, double y1, double x2, double y2, bool close_last) override; + void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) override; + +private: + Geom::Path &_path; + + ConverterPath(const ConverterPath&) = delete; + ConverterPath& operator=(const ConverterPath&) = delete; +}; + + +} // namespace Spiro + +#endif diff --git a/src/live_effects/spiro.cpp b/src/live_effects/spiro.cpp new file mode 100644 index 0000000..4c129ba --- /dev/null +++ b/src/live_effects/spiro.cpp @@ -0,0 +1,1122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * C implementation of third-order polynomial spirals. + *//* + * Authors: see git history + * Raph Levien + * Johan Engelen + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spiro.h" + +#include +#include +#include + +#include "display/curve.h" + +#define SPIRO_SHOW_INFINITE_COORDINATE_CALLS + +namespace Spiro { + +void spiro_run(const spiro_cp *src, int src_len, SPCurve &curve) +{ + spiro_seg *s = Spiro::run_spiro(src, src_len); + Spiro::ConverterSPCurve bc(curve); + Spiro::spiro_to_otherpath(s, src_len, bc); + free(s); +} + +void spiro_run(const spiro_cp *src, int src_len, Geom::Path &path) +{ + spiro_seg *s = Spiro::run_spiro(src, src_len); + Spiro::ConverterPath bc(path); + Spiro::spiro_to_otherpath(s, src_len, bc); + free(s); +} + + +/************************************ + * Spiro math + */ + +struct spiro_seg_s { + double x; + double y; + char ty; + double bend_th; + double ks[4]; + double seg_ch; + double seg_th; + double l; +}; + +struct bandmat { + double a[11]; /* band-diagonal matrix */ + double al[5]; /* lower part of band-diagonal decomposition */ +}; + +#ifndef M_PI +#define M_PI 3.14159265358979323846 /* pi */ +#endif + +int n = 4; + +#ifndef ORDER +#define ORDER 12 +#endif + +/* Integrate polynomial spiral curve over range -.5 .. .5. */ +static void +integrate_spiro(const double ks[4], double xy[2]) +{ +#if 0 + int n = 1024; +#endif + double th1 = ks[0]; + double th2 = .5 * ks[1]; + double th3 = (1./6) * ks[2]; + double th4 = (1./24) * ks[3]; + double x, y; + double ds = 1. / n; + double ds2 = ds * ds; + double ds3 = ds2 * ds; + double k0 = ks[0] * ds; + double k1 = ks[1] * ds; + double k2 = ks[2] * ds; + double k3 = ks[3] * ds; + int i; + double s = .5 * ds - .5; + + x = 0; + y = 0; + + for (i = 0; i < n; i++) { + +#if ORDER > 2 + double u, v; + double km0, km1, km2, km3; + + if (n == 1) { + km0 = k0; + km1 = k1 * ds; + km2 = k2 * ds2; + } else { + km0 = (((1./6) * k3 * s + .5 * k2) * s + k1) * s + k0; + km1 = ((.5 * k3 * s + k2) * s + k1) * ds; + km2 = (k3 * s + k2) * ds2; + } + km3 = k3 * ds3; +#endif + + { + +#if ORDER == 4 + double km0_2 = km0 * km0; + u = 24 - km0_2; + v = km1; +#endif + +#if ORDER == 6 + double km0_2 = km0 * km0; + double km0_4 = km0_2 * km0_2; + u = 24 - km0_2 + (km0_4 - 4 * km0 * km2 - 3 * km1 * km1) * (1./80); + v = km1 + (km3 - 6 * km0_2 * km1) * (1./80); +#endif + +#if ORDER == 8 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t6_6 = t4_4 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6; + v -= (1./480) * t3_4 + (1./2688) * t3_6; + u += (1./1920) * t4_4 + (1./10752) * t4_6; + v += (1./53760) * t5_6; + u -= (1./322560) * t6_6; +#endif + +#if ORDER == 10 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t8_8 = t6_6 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8; + v += (1./53760) * t5_6 + (1./276480) * t5_8; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8; + v -= (1./1.16122e+07) * t7_8; + u += (1./9.28973e+07) * t8_8; +#endif + +#if ORDER == 12 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t10_10 = t8_8 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8 + (1./67584) * t3_10; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8 + (1./270336) * t4_10; + v += (1./53760) * t5_6 + (1./276480) * t5_8 + (1./1.35168e+06) * t5_10; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8 + (1./8.11008e+06) * t6_10; + v -= (1./1.16122e+07) * t7_8 + (1./5.67706e+07) * t7_10; + u += (1./9.28973e+07) * t8_8 + (1./4.54164e+08) * t8_10; + v += (1./4.08748e+09) * t9_10; + u -= (1./4.08748e+10) * t10_10; +#endif + +#if ORDER == 14 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t3_12 = t2_8 * t1_4; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t4_11 = 2 * (t2_3 * t2_8 + t2_4 * t2_7 + t2_5 * t2_6); + double t4_12 = 2 * (t2_4 * t2_8 + t2_5 * t2_7) + t2_6 * t2_6; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t5_12 = t4_8 * t1_4 + t4_9 * t1_3 + t4_10 * t1_2 + t4_11 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t6_11 = t4_4 * t2_7 + t4_5 * t2_6 + t4_6 * t2_5 + t4_7 * t2_4 + t4_8 * t2_3 + t4_9 * t2_2; + double t6_12 = t4_4 * t2_8 + t4_5 * t2_7 + t4_6 * t2_6 + t4_7 * t2_5 + t4_8 * t2_4 + t4_9 * t2_3 + t4_10 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t7_12 = t6_8 * t1_4 + t6_9 * t1_3 + t6_10 * t1_2 + t6_11 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t8_11 = t6_6 * t2_5 + t6_7 * t2_4 + t6_8 * t2_3 + t6_9 * t2_2; + double t8_12 = t6_6 * t2_6 + t6_7 * t2_5 + t6_8 * t2_4 + t6_9 * t2_3 + t6_10 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t9_12 = t8_8 * t1_4 + t8_9 * t1_3 + t8_10 * t1_2 + t8_11 * t1_1; + double t10_10 = t8_8 * t2_2; + double t10_11 = t8_8 * t2_3 + t8_9 * t2_2; + double t10_12 = t8_8 * t2_4 + t8_9 * t2_3 + t8_10 * t2_2; + double t11_12 = t10_10 * t1_2 + t10_11 * t1_1; + double t12_12 = t10_10 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8 + (1./67584) * t3_10 + (1./319488) * t3_12; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8 + (1./270336) * t4_10 + (1./1.27795e+06) * t4_12; + v += (1./53760) * t5_6 + (1./276480) * t5_8 + (1./1.35168e+06) * t5_10 + (1./6.38976e+06) * t5_12; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8 + (1./8.11008e+06) * t6_10 + (1./3.83386e+07) * t6_12; + v -= (1./1.16122e+07) * t7_8 + (1./5.67706e+07) * t7_10 + (1./2.6837e+08) * t7_12; + u += (1./9.28973e+07) * t8_8 + (1./4.54164e+08) * t8_10 + (1./2.14696e+09) * t8_12; + v += (1./4.08748e+09) * t9_10 + (1./1.93226e+10) * t9_12; + u -= (1./4.08748e+10) * t10_10 + (1./1.93226e+11) * t10_12; + v -= (1./2.12549e+12) * t11_12; + u += (1./2.55059e+13) * t12_12; +#endif + +#if ORDER == 16 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t3_12 = t2_8 * t1_4; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t4_11 = 2 * (t2_3 * t2_8 + t2_4 * t2_7 + t2_5 * t2_6); + double t4_12 = 2 * (t2_4 * t2_8 + t2_5 * t2_7) + t2_6 * t2_6; + double t4_13 = 2 * (t2_5 * t2_8 + t2_6 * t2_7); + double t4_14 = 2 * (t2_6 * t2_8) + t2_7 * t2_7; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t5_12 = t4_8 * t1_4 + t4_9 * t1_3 + t4_10 * t1_2 + t4_11 * t1_1; + double t5_14 = t4_10 * t1_4 + t4_11 * t1_3 + t4_12 * t1_2 + t4_13 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t6_11 = t4_4 * t2_7 + t4_5 * t2_6 + t4_6 * t2_5 + t4_7 * t2_4 + t4_8 * t2_3 + t4_9 * t2_2; + double t6_12 = t4_4 * t2_8 + t4_5 * t2_7 + t4_6 * t2_6 + t4_7 * t2_5 + t4_8 * t2_4 + t4_9 * t2_3 + t4_10 * t2_2; + double t6_13 = t4_5 * t2_8 + t4_6 * t2_7 + t4_7 * t2_6 + t4_8 * t2_5 + t4_9 * t2_4 + t4_10 * t2_3 + t4_11 * t2_2; + double t6_14 = t4_6 * t2_8 + t4_7 * t2_7 + t4_8 * t2_6 + t4_9 * t2_5 + t4_10 * t2_4 + t4_11 * t2_3 + t4_12 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t7_12 = t6_8 * t1_4 + t6_9 * t1_3 + t6_10 * t1_2 + t6_11 * t1_1; + double t7_14 = t6_10 * t1_4 + t6_11 * t1_3 + t6_12 * t1_2 + t6_13 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t8_11 = t6_6 * t2_5 + t6_7 * t2_4 + t6_8 * t2_3 + t6_9 * t2_2; + double t8_12 = t6_6 * t2_6 + t6_7 * t2_5 + t6_8 * t2_4 + t6_9 * t2_3 + t6_10 * t2_2; + double t8_13 = t6_6 * t2_7 + t6_7 * t2_6 + t6_8 * t2_5 + t6_9 * t2_4 + t6_10 * t2_3 + t6_11 * t2_2; + double t8_14 = t6_6 * t2_8 + t6_7 * t2_7 + t6_8 * t2_6 + t6_9 * t2_5 + t6_10 * t2_4 + t6_11 * t2_3 + t6_12 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t9_12 = t8_8 * t1_4 + t8_9 * t1_3 + t8_10 * t1_2 + t8_11 * t1_1; + double t9_14 = t8_10 * t1_4 + t8_11 * t1_3 + t8_12 * t1_2 + t8_13 * t1_1; + double t10_10 = t8_8 * t2_2; + double t10_11 = t8_8 * t2_3 + t8_9 * t2_2; + double t10_12 = t8_8 * t2_4 + t8_9 * t2_3 + t8_10 * t2_2; + double t10_13 = t8_8 * t2_5 + t8_9 * t2_4 + t8_10 * t2_3 + t8_11 * t2_2; + double t10_14 = t8_8 * t2_6 + t8_9 * t2_5 + t8_10 * t2_4 + t8_11 * t2_3 + t8_12 * t2_2; + double t11_12 = t10_10 * t1_2 + t10_11 * t1_1; + double t11_14 = t10_10 * t1_4 + t10_11 * t1_3 + t10_12 * t1_2 + t10_13 * t1_1; + double t12_12 = t10_10 * t2_2; + double t12_13 = t10_10 * t2_3 + t10_11 * t2_2; + double t12_14 = t10_10 * t2_4 + t10_11 * t2_3 + t10_12 * t2_2; + double t13_14 = t12_12 * t1_2 + t12_13 * t1_1; + double t14_14 = t12_12 * t2_2; + u = 1; + u -= 1./24 * t2_2 + 1./160 * t2_4 + 1./896 * t2_6 + 1./4608 * t2_8; + u += 1./1920 * t4_4 + 1./10752 * t4_6 + 1./55296 * t4_8 + 1./270336 * t4_10 + 1./1277952 * t4_12 + 1./5898240 * t4_14; + u -= 1./322560 * t6_6 + 1./1658880 * t6_8 + 1./8110080 * t6_10 + 1./38338560 * t6_12 + 1./176947200 * t6_14; + u += 1./92897280 * t8_8 + 1./454164480 * t8_10 + 4.6577500191e-10 * t8_12 + 1.0091791708e-10 * t8_14; + u -= 2.4464949595e-11 * t10_10 + 5.1752777990e-12 * t10_12 + 1.1213101898e-12 * t10_14; + u += 3.9206649992e-14 * t12_12 + 8.4947741650e-15 * t12_14; + u -= 4.6674583324e-17 * t14_14; + v = 0; + v += 1./12 * t1_2 + 1./80 * t1_4; + v -= 1./480 * t3_4 + 1./2688 * t3_6 + 1./13824 * t3_8 + 1./67584 * t3_10 + 1./319488 * t3_12; + v += 1./53760 * t5_6 + 1./276480 * t5_8 + 1./1351680 * t5_10 + 1./6389760 * t5_12 + 1./29491200 * t5_14; + v -= 1./11612160 * t7_8 + 1./56770560 * t7_10 + 1./268369920 * t7_12 + 8.0734333664e-10 * t7_14; + v += 2.4464949595e-10 * t9_10 + 5.1752777990e-11 * t9_12 + 1.1213101898e-11 * t9_14; + v -= 4.7047979991e-13 * t11_12 + 1.0193728998e-13 * t11_14; + v += 6.5344416654e-16 * t13_14; +#endif + + } + + if (n == 1) { +#if ORDER == 2 + x = 1; + y = 0; +#else + x = u; + y = v; +#endif + } else { + double th = (((th4 * s + th3) * s + th2) * s + th1) * s; + double cth = cos(th); + double sth = sin(th); + +#if ORDER == 2 + x += cth; + y += sth; +#else + x += cth * u - sth * v; + y += cth * v + sth * u; +#endif + s += ds; + } + } + +#if ORDER == 4 || ORDER == 6 + xy[0] = x * (1./24 * ds); + xy[1] = y * (1./24 * ds); +#else + xy[0] = x * ds; + xy[1] = y * ds; +#endif +} + +static double +compute_ends(const double ks[4], double ends[2][4], double seg_ch) +{ + double xy[2]; + double ch, th; + double l, l2, l3; + double th_even, th_odd; + double k0_even, k0_odd; + double k1_even, k1_odd; + double k2_even, k2_odd; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + l = ch / seg_ch; + + th_even = .5 * ks[0] + (1./48) * ks[2]; + th_odd = .125 * ks[1] + (1./384) * ks[3] - th; + ends[0][0] = th_even - th_odd; + ends[1][0] = th_even + th_odd; + k0_even = l * (ks[0] + .125 * ks[2]); + k0_odd = l * (.5 * ks[1] + (1./48) * ks[3]); + ends[0][1] = k0_even - k0_odd; + ends[1][1] = k0_even + k0_odd; + l2 = l * l; + k1_even = l2 * (ks[1] + .125 * ks[3]); + k1_odd = l2 * .5 * ks[2]; + ends[0][2] = k1_even - k1_odd; + ends[1][2] = k1_even + k1_odd; + l3 = l2 * l; + k2_even = l3 * ks[2]; + k2_odd = l3 * .5 * ks[3]; + ends[0][3] = k2_even - k2_odd; + ends[1][3] = k2_even + k2_odd; + + return l; +} + +static void +compute_pderivs(const spiro_seg *s, double ends[2][4], double derivs[4][2][4], + int jinc) +{ + double recip_d = 2e6; + double delta = 1./ recip_d; + double try_ks[4]; + double try_ends[2][4]; + int i, j, k; + + compute_ends(s->ks, ends, s->seg_ch); + for (i = 0; i < jinc; i++) { + for (j = 0; j < 4; j++) + try_ks[j] = s->ks[j]; + try_ks[i] += delta; + compute_ends(try_ks, try_ends, s->seg_ch); + for (k = 0; k < 2; k++) + for (j = 0; j < 4; j++) + derivs[j][k][i] = recip_d * (try_ends[k][j] - ends[k][j]); + } +} + +static double +mod_2pi(double th) +{ + double u = th / (2 * M_PI); + return 2 * M_PI * (u - floor(u + 0.5)); +} + +static spiro_seg * +setup_path(const spiro_cp *src, int n) +{ + int n_seg = src[0].ty == '{' ? n - 1 : n; + spiro_seg *r = (spiro_seg *)malloc((n_seg + 1) * sizeof(spiro_seg)); + int i; + int ilast; + + for (i = 0; i < n_seg; i++) { + r[i].x = src[i].x; + r[i].y = src[i].y; + r[i].ty = src[i].ty; + r[i].ks[0] = 0.; + r[i].ks[1] = 0.; + r[i].ks[2] = 0.; + r[i].ks[3] = 0.; + } + r[n_seg].x = src[n_seg % n].x; + r[n_seg].y = src[n_seg % n].y; + r[n_seg].ty = src[n_seg % n].ty; + + for (i = 0; i < n_seg; i++) { + double dx = r[i + 1].x - r[i].x; + double dy = r[i + 1].y - r[i].y; + r[i].seg_ch = hypot(dx, dy); + r[i].seg_th = atan2(dy, dx); + } + + ilast = n_seg - 1; + for (i = 0; i < n_seg; i++) { + if (r[i].ty == '{' || r[i].ty == '}' || r[i].ty == 'v') + r[i].bend_th = 0.; + else + r[i].bend_th = mod_2pi(r[i].seg_th - r[ilast].seg_th); + ilast = i; + } + return r; +} + +static void +bandec11(bandmat *m, int *perm, int n) +{ + int i, j, k; + int l; + + /* pack top triangle to the left. */ + for (i = 0; i < 5; i++) { + for (j = 0; j < i + 6; j++) + m[i].a[j] = m[i].a[j + 5 - i]; + for (; j < 11; j++) + m[i].a[j] = 0.; + } + l = 5; + for (k = 0; k < n; k++) { + int pivot = k; + double pivot_val = m[k].a[0]; + double pivot_scale; + + l = l < n ? l + 1 : n; + + for (j = k + 1; j < l; j++) + if (fabs(m[j].a[0]) > fabs(pivot_val)) { + pivot_val = m[j].a[0]; + pivot = j; + } + + perm[k] = pivot; + if (pivot != k) { + for (j = 0; j < 11; j++) { + double tmp = m[k].a[j]; + m[k].a[j] = m[pivot].a[j]; + m[pivot].a[j] = tmp; + } + } + + if (fabs(pivot_val) < 1e-12) pivot_val = 1e-12; + pivot_scale = 1. / pivot_val; + for (i = k + 1; i < l; i++) { + double x = m[i].a[0] * pivot_scale; + m[k].al[i - k - 1] = x; + for (j = 1; j < 11; j++) + m[i].a[j - 1] = m[i].a[j] - x * m[k].a[j]; + m[i].a[10] = 0.; + } + } +} + +static void +banbks11(const bandmat *m, const int *perm, double *v, int n) +{ + int i, k, l; + + /* forward substitution */ + l = 5; + for (k = 0; k < n; k++) { + i = perm[k]; + if (i != k) { + double tmp = v[k]; + v[k] = v[i]; + v[i] = tmp; + } + if (l < n) l++; + for (i = k + 1; i < l; i++) + v[i] -= m[k].al[i - k - 1] * v[k]; + } + + /* back substitution */ + l = 1; + for (i = n - 1; i >= 0; i--) { + double x = v[i]; + for (k = 1; k < l; k++) + x -= m[i].a[k] * v[k + i]; + v[i] = x / m[i].a[0]; + if (l < 11) l++; + } +} + +static int compute_jinc(char ty0, char ty1) +{ + if (ty0 == 'o' || ty1 == 'o' || + ty0 == ']' || ty1 == '[') + return 4; + else if (ty0 == 'c' && ty1 == 'c') + return 2; + else if (((ty0 == '{' || ty0 == 'v' || ty0 == '[') && ty1 == 'c') || + (ty0 == 'c' && (ty1 == '}' || ty1 == 'v' || ty1 == ']'))) + return 1; + else + return 0; +} + +static int count_vec(const spiro_seg *s, int nseg) +{ + int i; + int n = 0; + + for (i = 0; i < nseg; i++) + n += compute_jinc(s[i].ty, s[i + 1].ty); + return n; +} + +static void +add_mat_line(bandmat *m, double *v, + double derivs[4], double x, double y, int j, int jj, int jinc, + int nmat) +{ + if (jj >= 0) { + int joff = (j + 5 - jj + nmat) % nmat; + if (nmat < 6) { + joff = j + 5 - jj; + } else if (nmat == 6) { + joff = 2 + (j + 3 - jj + nmat) % nmat; + } +#ifdef VERBOSE + printf("add_mat_line j=%d jj=%d jinc=%d nmat=%d joff=%d\n", j, jj, jinc, nmat, joff); +#endif + v[jj] += x; + for (int k = 0; k < jinc; k++) + m[jj].a[joff + k] += y * derivs[k]; + } +} + +static double +spiro_iter(spiro_seg *s, bandmat *m, int *perm, double *v, const int n) +{ + int cyclic = s[0].ty != '{' && s[0].ty != 'v'; + int nmat = count_vec(s, n); + int n_invert; + + for (int i = 0; i < nmat; i++) { + v[i] = 0.; + for (double & j : m[i].a) { + j = 0.; + } + for (double & j : m[i].al) { + j = 0.; + } + } + + int j = 0; + int jj; + if (s[0].ty == 'o') { + jj = nmat - 2; + } else if (s[0].ty == 'c') { + jj = nmat - 1; + } else { + jj = 0; + } + for (int i = 0; i < n; i++) { + char ty0 = s[i].ty; + char ty1 = s[i + 1].ty; + int jinc = compute_jinc(ty0, ty1); + double th = s[i].bend_th; + double ends[2][4]; + double derivs[4][2][4]; + int jthl = -1, jk0l = -1, jk1l = -1, jk2l = -1; + int jthr = -1, jk0r = -1, jk1r = -1, jk2r = -1; + + compute_pderivs(&s[i], ends, derivs, jinc); + + /* constraints crossing left */ + if (ty0 == 'o' || ty0 == 'c' || ty0 == '[' || ty0 == ']') { + jthl = jj++; + jj %= nmat; + jk0l = jj++; + } + if (ty0 == 'o') { + jj %= nmat; + jk1l = jj++; + jk2l = jj++; + } + + /* constraints on left */ + if ((ty0 == '[' || ty0 == 'v' || ty0 == '{' || ty0 == 'c') && + jinc == 4) { + if (ty0 != 'c') + jk1l = jj++; + jk2l = jj++; + } + + /* constraints on right */ + if ((ty1 == ']' || ty1 == 'v' || ty1 == '}' || ty1 == 'c') && + jinc == 4) { + if (ty1 != 'c') + jk1r = jj++; + jk2r = jj++; + } + + /* constraints crossing right */ + if (ty1 == 'o' || ty1 == 'c' || ty1 == '[' || ty1 == ']') { + jthr = jj; + jk0r = (jj + 1) % nmat; + } + if (ty1 == 'o') { + jk1r = (jj + 2) % nmat; + jk2r = (jj + 3) % nmat; + } + + add_mat_line(m, v, derivs[0][0], th - ends[0][0], 1, j, jthl, jinc, nmat); + add_mat_line(m, v, derivs[1][0], ends[0][1], -1, j, jk0l, jinc, nmat); + add_mat_line(m, v, derivs[2][0], ends[0][2], -1, j, jk1l, jinc, nmat); + add_mat_line(m, v, derivs[3][0], ends[0][3], -1, j, jk2l, jinc, nmat); + add_mat_line(m, v, derivs[0][1], -ends[1][0], 1, j, jthr, jinc, nmat); + add_mat_line(m, v, derivs[1][1], -ends[1][1], 1, j, jk0r, jinc, nmat); + add_mat_line(m, v, derivs[2][1], -ends[1][2], 1, j, jk1r, jinc, nmat); + add_mat_line(m, v, derivs[3][1], -ends[1][3], 1, j, jk2r, jinc, nmat); + if (jthl >= 0) + v[jthl] = mod_2pi(v[jthl]); + if (jthr >= 0) + v[jthr] = mod_2pi(v[jthr]); + j += jinc; + } + if (cyclic) { + memcpy(m + nmat, m, sizeof(bandmat) * nmat); + memcpy(m + 2 * nmat, m, sizeof(bandmat) * nmat); + memcpy(v + nmat, v, sizeof(double) * nmat); + memcpy(v + 2 * nmat, v, sizeof(double) * nmat); + n_invert = 3 * nmat; + j = nmat; + } else { + n_invert = nmat; + j = 0; + } +#ifdef VERBOSE + for (int i = 0; i < n; i++) { + for (int k = 0; k < 11; k++) { + printf(" %2.4f", m[i].a[k]); + } + printf(": %2.4f\n", v[i]); + } + printf("---\n"); +#endif + bandec11(m, perm, n_invert); + banbks11(m, perm, v, n_invert); + + double norm = 0.; + for (int i = 0; i < n; i++) { + char ty0 = s[i].ty; + char ty1 = s[i + 1].ty; + int jinc = compute_jinc(ty0, ty1); + int k; + + for (k = 0; k < jinc; k++) { + double dk = v[j++]; + +#ifdef VERBOSE + printf("s[%d].ks[%d] += %f\n", i, k, dk); +#endif + s[i].ks[k] += dk; + norm += dk * dk; + } + s[i].ks[0] = 2.0*mod_2pi(s[i].ks[0]/2.0); + } + return norm; +} + +static int +solve_spiro(spiro_seg *s, const int nseg) +{ + int nmat = count_vec(s, nseg); + int n_alloc = nmat; + + if (nmat == 0) { + return 0; + } + if (s[0].ty != '{' && s[0].ty != 'v') { + n_alloc *= 3; + } + if (n_alloc < 5) { + n_alloc = 5; + } + + bandmat *m = (bandmat *)malloc(sizeof(bandmat) * n_alloc); + double *v = (double *)malloc(sizeof(double) * n_alloc); + int *perm = (int *)malloc(sizeof(int) * n_alloc); + + for (unsigned i = 0; i < 10; i++) { + double norm = spiro_iter(s, m, perm, v, nseg); +#ifdef VERBOSE + printf("%% norm = %g\n", norm); +#endif + if (norm < 1e-12) break; + } + + free(m); + free(v); + free(perm); + return 0; +} + +static void +spiro_seg_to_otherpath(const double ks[4], + double x0, double y0, double x1, double y1, + ConverterBase &bc, int depth, bool close_last) +{ + double bend = fabs(ks[0]) + fabs(.5 * ks[1]) + fabs(.125 * ks[2]) + + fabs((1./48) * ks[3]); + + if (!(bend > 1e-8)) { + bc.lineto(x1, y1, close_last); + } else { + double seg_ch = hypot(x1 - x0, y1 - y0); + double seg_th = atan2(y1 - y0, x1 - x0); + double xy[2]; + double ch, th; + double scale, rot; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + scale = seg_ch / ch; + rot = seg_th - th; + if (depth > 5 || bend < 1.) { + double ul, vl; + double ur, vr; + double th_even, th_odd; + th_even = (1./384) * ks[3] + (1./8) * ks[1] + rot; + th_odd = (1./48) * ks[2] + .5 * ks[0]; + ul = (scale * (1./3)) * cos(th_even - th_odd); + vl = (scale * (1./3)) * sin(th_even - th_odd); + ur = (scale * (1./3)) * cos(th_even + th_odd); + vr = (scale * (1./3)) * sin(th_even + th_odd); + bc.curveto(x0 + ul, y0 + vl, x1 - ur, y1 - vr, x1, y1, close_last); + } else { + /* subdivide */ + double ksub[4]; + double thsub; + double xysub[2]; + double xmid, ymid; + double cth, sth; + + ksub[0] = .5 * ks[0] - .125 * ks[1] + (1./64) * ks[2] - (1./768) * ks[3]; + ksub[1] = .25 * ks[1] - (1./16) * ks[2] + (1./128) * ks[3]; + ksub[2] = .125 * ks[2] - (1./32) * ks[3]; + ksub[3] = (1./16) * ks[3]; + thsub = rot - .25 * ks[0] + (1./32) * ks[1] - (1./384) * ks[2] + (1./6144) * ks[3]; + cth = .5 * scale * cos(thsub); + sth = .5 * scale * sin(thsub); + integrate_spiro(ksub, xysub); + xmid = x0 + cth * xysub[0] - sth * xysub[1]; + ymid = y0 + cth * xysub[1] + sth * xysub[0]; + spiro_seg_to_otherpath(ksub, x0, y0, xmid, ymid, bc, depth + 1, false); + ksub[0] += .25 * ks[1] + (1./384) * ks[3]; + ksub[1] += .125 * ks[2]; + ksub[2] += (1./16) * ks[3]; + spiro_seg_to_otherpath(ksub, xmid, ymid, x1, y1, bc, depth + 1, close_last); + } + } +} + +spiro_seg * +run_spiro(const spiro_cp *src, int n) +{ + int nseg = src[0].ty == '{' ? n - 1 : n; + spiro_seg *s = setup_path(src, n); + if (nseg > 1) + solve_spiro(s, nseg); + return s; +} + +void +free_spiro(spiro_seg *s) +{ + free(s); +} + +void +spiro_to_otherpath(const spiro_seg *s, int n, ConverterBase &bc) +{ + int i; + int nsegs = s[n - 1].ty == '}' ? n - 1 : n; + + for (i = 0; i < nsegs; i++) { + double x0 = s[i].x; + double y0 = s[i].y; + double x1 = s[i + 1].x; + double y1 = s[i + 1].y; + + if (i == 0) { + bc.moveto(x0, y0); + } + // on the last segment, set the 'close_last' flag if path is closed + spiro_seg_to_otherpath(s[i].ks, x0, y0, x1, y1, bc, 0, (nsegs == n) && (i == n - 1)); + } +} + +double +get_knot_th(const spiro_seg *s, int i) +{ + double ends[2][4]; + + if (i == 0) { + compute_ends(s[i].ks, ends, s[i].seg_ch); + return s[i].seg_th - ends[0][0]; + } else { + compute_ends(s[i - 1].ks, ends, s[i - 1].seg_ch); + return s[i - 1].seg_th + ends[1][0]; + } +} + + +} // namespace Spiro + +/************************************ + * Unit_test code + */ + + +#ifdef UNIT_TEST +#include +#include /* for gettimeofday */ + +using namespace Spiro; + +static double +get_time (void) +{ + struct timeval tv; + struct timezone tz; + + gettimeofday (&tv, &tz); + + return tv.tv_sec + 1e-6 * tv.tv_usec; +} + +int +test_integ(void) { + double ks[] = {1, 2, 3, 4}; + double xy[2]; + double xynom[2]; + int i, j; + int nsubdiv; + + n = ORDER < 6 ? 4096 : 1024; + integrate_spiro(ks, xynom); + nsubdiv = ORDER < 12 ? 8 : 7; + for (i = 0; i < nsubdiv; i++) { + double st, en; + double err; + int n_iter = (1 << (20 - i)); + + n = 1 << i; + st = get_time(); + for (j = 0; j < n_iter; j++) + integrate_spiro(ks, xy); + en = get_time(); + err = hypot(xy[0] - xynom[0], xy[1] - xynom[1]); + printf("%d %d %g %g\n", ORDER, n, (en - st) / n_iter, err); +#if 0 + double ch, th; + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + printf("n = %d: integ(%g %g %g %g) = %g %g, ch = %g, th = %g\n", n, + ks[0], ks[1], ks[2], ks[3], xy[0], xy[1], ch, th); + printf("%d: %g %g\n", n, xy[0] - xynom[0], xy[1] - xynom[1]); +#endif + } + return 0; +} + +void +print_seg(const double ks[4], double x0, double y0, double x1, double y1) +{ + double bend = fabs(ks[0]) + fabs(.5 * ks[1]) + fabs(.125 * ks[2]) + + fabs((1./48) * ks[3]); + + if (bend < 1e-8) { + printf("%g %g lineto\n", x1, y1); + } else { + double seg_ch = hypot(x1 - x0, y1 - y0); + double seg_th = atan2(y1 - y0, x1 - x0); + double xy[2]; + double ch, th; + double scale, rot; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + scale = seg_ch / ch; + rot = seg_th - th; + if (bend < 1.) { + double th_even, th_odd; + double ul, vl; + double ur, vr; + th_even = (1./384) * ks[3] + (1./8) * ks[1] + rot; + th_odd = (1./48) * ks[2] + .5 * ks[0]; + ul = (scale * (1./3)) * cos(th_even - th_odd); + vl = (scale * (1./3)) * sin(th_even - th_odd); + ur = (scale * (1./3)) * cos(th_even + th_odd); + vr = (scale * (1./3)) * sin(th_even + th_odd); + printf("%g %g %g %g %g %g curveto\n", + x0 + ul, y0 + vl, x1 - ur, y1 - vr, x1, y1); + + } else { + /* subdivide */ + double ksub[4]; + double thsub; + double xysub[2]; + double xmid, ymid; + double cth, sth; + + ksub[0] = .5 * ks[0] - .125 * ks[1] + (1./64) * ks[2] - (1./768) * ks[3]; + ksub[1] = .25 * ks[1] - (1./16) * ks[2] + (1./128) * ks[3]; + ksub[2] = .125 * ks[2] - (1./32) * ks[3]; + ksub[3] = (1./16) * ks[3]; + thsub = rot - .25 * ks[0] + (1./32) * ks[1] - (1./384) * ks[2] + (1./6144) * ks[3]; + cth = .5 * scale * cos(thsub); + sth = .5 * scale * sin(thsub); + integrate_spiro(ksub, xysub); + xmid = x0 + cth * xysub[0] - sth * xysub[1]; + ymid = y0 + cth * xysub[1] + sth * xysub[0]; + print_seg(ksub, x0, y0, xmid, ymid); + ksub[0] += .25 * ks[1] + (1./384) * ks[3]; + ksub[1] += .125 * ks[2]; + ksub[2] += (1./16) * ks[3]; + print_seg(ksub, xmid, ymid, x1, y1); + } + } +} + +void +print_segs(const spiro_seg *segs, int nsegs) +{ + int i; + + for (i = 0; i < nsegs; i++) { + double x0 = segs[i].x; + double y0 = segs[i].y; + double x1 = segs[i + 1].x; + double y1 = segs[i + 1].y; + + if (i == 0) + printf("%g %g moveto\n", x0, y0); + printf("%% ks = [ %g %g %g %g ]\n", + segs[i].ks[0], segs[i].ks[1], segs[i].ks[2], segs[i].ks[3]); + print_seg(segs[i].ks, x0, y0, x1, y1); + } + printf("stroke\n"); +} + +int +test_curve(void) +{ + spiro_cp path[] = { + {334, 117, 'v'}, + {305, 176, 'v'}, + {212, 142, 'c'}, + {159, 171, 'c'}, + {224, 237, 'c'}, + {347, 335, 'c'}, + {202, 467, 'c'}, + {81, 429, 'v'}, + {114, 368, 'v'}, + {201, 402, 'c'}, + {276, 369, 'c'}, + {218, 308, 'c'}, + {91, 211, 'c'}, + {124, 111, 'c'}, + {229, 82, 'c'} + }; + spiro_seg *segs; + int i; + + n = 1; + for (i = 0; i < 1000; i++) { + segs = setup_path(path, 15); + solve_spiro(segs, 15); + } + printf("100 800 translate 1 -1 scale 1 setlinewidth\n"); + print_segs(segs, 15); + printf("showpage\n"); + return 0; +} + +int main(int argc, char **argv) +{ + return test_curve(); +} +#endif diff --git a/src/live_effects/spiro.h b/src/live_effects/spiro.h new file mode 100644 index 0000000..3ca1fb8 --- /dev/null +++ b/src/live_effects/spiro.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * C implementation of third-order polynomial spirals. + *//* + * Authors: see git history + * Raph Levien + * Johan Engelen + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SPIRO_H +#define INKSCAPE_SPIRO_H + +#include "live_effects/spiro-converters.h" +#include <2geom/forward.h> + +class SPCurve; + +namespace Spiro { + +struct spiro_cp { + double x; + double y; + char ty; +}; + + +void spiro_run(const spiro_cp *src, int src_len, SPCurve &curve); +void spiro_run(const spiro_cp *src, int src_len, Geom::Path &path); + +/* the following methods are only for expert use: */ +typedef struct spiro_seg_s spiro_seg; +spiro_seg * run_spiro(const spiro_cp *src, int n); +void free_spiro(spiro_seg *s); +void spiro_to_otherpath(const spiro_seg *s, int n, ConverterBase &bc); +double get_knot_th(const spiro_seg *s, int i); + + +} // namespace Spiro + +#endif // INKSCAPE_SPIRO_H diff --git a/src/live_effects/todo.txt b/src/live_effects/todo.txt new file mode 100644 index 0000000..a208867 --- /dev/null +++ b/src/live_effects/todo.txt @@ -0,0 +1,11 @@ +reminder list + + +cleanup nodepath code that draws helper path + +ARCS !!! see sp_arc_set_elliptical_path_attribute(SPArc *arc, Inkscape::XML::Node *repr) + +make sp_nodepath_is_over_stroke perhaps + + +find dir "fixme" and fix'em! \ No newline at end of file diff --git a/src/manipulation/README b/src/manipulation/README new file mode 100644 index 0000000..461fc45 --- /dev/null +++ b/src/manipulation/README @@ -0,0 +1,16 @@ + +This directory contains code that allows manipulation of the SVG +tree. The manipulations can be to: + +* Elements: creating, deleting, reordering, selection. +* Attributes: adding, removing, changing the value. +* Properties: adding, removing, changing the value. + +This code in this directory should be kept GUI independent. + +To do: + +* Move relevant code here. +* Remove GUI dependency if necessary. + + diff --git a/src/media.cpp b/src/media.cpp new file mode 100644 index 0000000..75a67f5 --- /dev/null +++ b/src/media.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "media.h" + +void +media_clear_all(Media &media) +{ + media.print = false; + media.screen = false; +} + +void +media_set_all(Media &media) +{ + media.print = true; + media.screen = true; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/media.h b/src/media.h new file mode 100644 index 0000000..5a9353a --- /dev/null +++ b/src/media.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_MEDIA_H +#define INKSCAPE_MEDIA_H + +class Media { +public: + bool print; + bool screen; +}; + +void media_clear_all(Media &); +void media_set_all(Media &); + +#endif /* !INKSCAPE_MEDIA_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/menus-skeleton.h b/src/menus-skeleton.h new file mode 100644 index 0000000..43ed8ef --- /dev/null +++ b/src/menus-skeleton.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_MENUS_SKELETON_H +#define SEEN_MENUS_SKELETON_H + +#ifdef __cplusplus +#undef N_ +#define N_(x) x +#endif + +static char const menus_skeleton[] = +"\n" +"\n" +" \n" +" \n" +" \n" +"\n"; + +#define MENUS_SKELETON_SIZE (sizeof(menus_skeleton) - 1) + + +#endif /* !SEEN_MENUS_SKELETON_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/message-context.cpp b/src/message-context.cpp new file mode 100644 index 0000000..4ff92a9 --- /dev/null +++ b/src/message-context.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MessageContext - context for posting status messages + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "message-context.h" +#include "message-stack.h" + +namespace Inkscape { + +MessageContext::MessageContext(std::shared_ptr stack) +: _stack(std::move(stack)), _message_id(0), _flash_message_id(0) +{} + +MessageContext::~MessageContext() { + clear(); + _stack = nullptr; +} + +void MessageContext::set(MessageType type, gchar const *message) { + if (_message_id) { + _stack->cancel(_message_id); + } + _message_id = _stack->push(type, message); +} + +void MessageContext::setF(MessageType type, gchar const *format, ...) +{ + va_list args; + va_start(args, format); + setVF(type, format, args); + va_end(args); +} + +void MessageContext::setVF(MessageType type, gchar const *format, va_list args) +{ + gchar *message=g_strdup_vprintf(format, args); + set(type, message); + g_free(message); +} + +void MessageContext::flash(MessageType type, gchar const *message) { + if (_flash_message_id) { + _stack->cancel(_flash_message_id); + } + _flash_message_id = _stack->flash(type, message); +} + +void MessageContext::flashF(MessageType type, gchar const *format, ...) { + va_list args; + va_start(args, format); + flashVF(type, format, args); + va_end(args); +} + +void MessageContext::flashVF(MessageType type, gchar const *format, va_list args) { + gchar *message=g_strdup_vprintf(format, args); + flash(type, message); + g_free(message); +} + +void MessageContext::clear() { + if (_message_id) { + _stack->cancel(_message_id); + _message_id = 0; + } + if (_flash_message_id) { + _stack->cancel(_flash_message_id); + _flash_message_id = 0; + } +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/message-context.h b/src/message-context.h new file mode 100644 index 0000000..cfe155a --- /dev/null +++ b/src/message-context.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Interface for locally managing a current status message + */ + +/* + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_MESSAGE_CONTEXT_H +#define SEEN_INKSCAPE_MESSAGE_CONTEXT_H + +#include +#include +#include + +#include "message.h" + +namespace Inkscape { + +class MessageStack; + +/** A convenience class for working with MessageStacks. + * + * In general, a particular piece of code will only want to display + * one status message at a time. This class takes care of tracking + * a "current" message id in a particular stack for us, and provides + * a convenient means to remove or replace it. + * + * @see Inkscape::MessageStack + */ +class MessageContext { +public: + /** Constructs an Inkscape::MessageContext referencing a particular + * Inkscape::MessageStack, which will be used for our messages + * + * MessageContexts retain references to the MessageStacks they use. + * + * @param stack the Inkscape::MessageStack to use for our messages + */ + MessageContext(std::shared_ptr stack); + ~MessageContext(); + + /** @brief pushes a message on the stack, replacing our old message + * + * @param type the message type + * @param message the message text + */ + void set(MessageType type, char const *message); + + /** @brief pushes a message on the stack using prinf-style formatting, + * and replacing our old message + * + * @param type the message type + * @param format a printf-style formatting string + */ + void setF(MessageType type, char const *format, ...) G_GNUC_PRINTF(3,4); + + /** @brief pushes a message on the stack using printf-style formatting, + * and a stdarg argument list + * + * @param type the message type + * @param format a printf-style formatting string + * @param args printf-style arguments + */ + void setVF(MessageType type, char const *format, va_list args); + + /** @brief pushes a message onto the stack for a brief period of time + * without disturbing our "current" message + * + * @param type the message type + * @param message the message text + */ + void flash(MessageType type, char const *message); + + /** @brief pushes a message onto the stack for a brief period of time + * using printf-style formatting, without disturbing our current + * message + * + * @param type the message type + * @param format a printf-style formatting string + */ + void flashF(MessageType type, char const *format, ...) G_GNUC_PRINTF(3,4); + + /** @brief pushes a message onto the stack for a brief period of time + * using printf-style formatting and a stdarg argument list; + * it does not disturb our "current" message + * + * @param type the message type + * @param format a printf-style formatting string + * @param args printf-style arguments + */ + void flashVF(MessageType type, char const *format, va_list args); + + /** @brief removes our current message from the stack */ + void clear(); + +private: + std::shared_ptr _stack; ///< the message stack to use + MessageId _message_id; ///< our current message id, or 0 + MessageId _flash_message_id; ///< current flashed message id, or 0 +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/message-stack.cpp b/src/message-stack.cpp new file mode 100644 index 0000000..cebd5f2 --- /dev/null +++ b/src/message-stack.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MessageStack - manages stack of active status messages + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include "message-stack.h" + +namespace Inkscape { + +MessageStack::MessageStack() +: _messages(nullptr), _next_id(1) +{ +} + +MessageStack::~MessageStack() +{ + while (_messages) { + _messages = _discard(_messages); + } +} + +MessageId MessageStack::push(MessageType type, gchar const *message) { + return _push(type, 0, message); +} + +MessageId MessageStack::pushF(MessageType type, gchar const *format, ...) +{ + va_list args; + va_start(args, format); + MessageId id=pushVF(type, format, args); + va_end(args); + return id; +} + +MessageId MessageStack::pushVF(MessageType type, gchar const *format, va_list args) +{ + MessageId id; + gchar *message=g_strdup_vprintf(format, args); + id = push(type, message); + g_free(message); + return id; +} + +void MessageStack::cancel(MessageId id) { + Message **ref; + for ( ref = &_messages ; *ref ; ref = &(*ref)->next ) { + if ( (*ref)->id == id ) { + *ref = _discard(*ref); + _emitChanged(); + break; + } + } +} + +MessageId MessageStack::flash(MessageType type, Glib::ustring const &message) +{ + MessageId id = flash( type, message.c_str() ); + return id; +} + +MessageId MessageStack::flash(MessageType type, gchar const *message) { + switch (type) { + case INFORMATION_MESSAGE: // stay rather long so as to seem permanent, but eventually disappear + return _push(type, 6000 + 80*strlen(message), message); + break; + case ERROR_MESSAGE: // pretty important stuff, but temporary + return _push(type, 4000 + 60*strlen(message), message); + break; + case WARNING_MESSAGE: // a bit less important than error + return _push(type, 2000 + 40*strlen(message), message); + break; + case IMMEDIATE_MESSAGE: // same length as normal, higher priority + return _push(type, 1000 + 20*strlen(message), message); + break; + case NORMAL_MESSAGE: // something ephemeral + default: + return _push(type, 1000 + 20*strlen(message), message); + break; + } +} + +MessageId MessageStack::flashF(MessageType type, gchar const *format, ...) { + va_list args; + va_start(args, format); + MessageId id = flashVF(type, format, args); + va_end(args); + return id; +} + +MessageId MessageStack::flashVF(MessageType type, gchar const *format, va_list args) +{ + gchar *message=g_strdup_vprintf(format, args); + MessageId id = flash(type, message); + g_free(message); + return id; +} + +MessageId MessageStack::_push(MessageType type, guint lifetime, gchar const *message) +{ + Message *m=new Message; + MessageId id=_next_id++; + + m->stack = this; + m->id = id; + m->type = type; + m->message = g_strdup(message); + + if (lifetime) { + m->timeout_id = g_timeout_add(lifetime, &MessageStack::_timeout, m); + } else { + m->timeout_id = 0; + } + + m->next = _messages; + _messages = m; + + _emitChanged(); + + return id; +} + +MessageStack::Message *MessageStack::_discard(MessageStack::Message *m) +{ + Message *next=m->next; + if (m->timeout_id) { + g_source_remove(m->timeout_id); + m->timeout_id = 0; + } + g_free(m->message); + m->message = nullptr; + m->stack = nullptr; + delete m; + return next; +} + +void MessageStack::_emitChanged() { + if (_messages) { + _changed_signal.emit(_messages->type, _messages->message); + } else { + _changed_signal.emit(NORMAL_MESSAGE, nullptr); + } +} + +gboolean MessageStack::_timeout(gpointer data) { + Message *m=reinterpret_cast(data); + m->timeout_id = 0; + m->stack->cancel(m->id); + return FALSE; +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/message-stack.h b/src/message-stack.h new file mode 100644 index 0000000..f86ae7b --- /dev/null +++ b/src/message-stack.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Raw stack of active status messages + */ + +/* + * Authors: + * MenTaLguY + * Jon A. Cruz + * + * Copyright (C) 2004 MenTaLguY + * Copyright (C) 2011 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_MESSAGE_STACK_H +#define SEEN_INKSCAPE_MESSAGE_STACK_H + +#include +#include +#include // G_GNUC_PRINTF is the only thing worth having from here +#include +#include + +#include "message.h" + +namespace Inkscape { + +/** + * A class which holds a stack of displayed messages. + * + * Messages can be pushed onto the top of the stack, and removed + * from any point in the stack by their id. + * + * Messages may also be "flashed", meaning that they will be + * automatically removed from the stack a fixed period of time + * after they are pushed. + * + * "Flashed" warnings and errors will persist longer than normal + * messages. + * + * There is no simple "pop" operation provided, since these + * stacks are intended to be shared by many different clients; + * assuming that the message you pushed is still on top is an + * invalid and unsafe assumption. + */ +class MessageStack final +{ +public: + MessageStack(); + ~MessageStack(); + + MessageStack(MessageStack const &) = delete; // no copy + void operator=(MessageStack const &) = delete; // no assign + + /** @brief returns the type of message currently at the top of the stack */ + MessageType currentMessageType() { + return _messages ? _messages->type : NORMAL_MESSAGE; + } + /** @brief returns the text of the message currently at the top of + * the stack + */ + char const *currentMessage() { + return _messages ? _messages->message : nullptr; + } + + /** @brief connects to the "changed" signal which is emitted whenever + * the topmost message on the stack changes. + */ + sigc::connection connectChanged(sigc::slot slot) + { + return _changed_signal.connect(slot); + } + + /** @brief pushes a message onto the stack + * + * @param type the message type + * @param message the message text + * + * @return the id of the pushed message + */ + MessageId push(MessageType type, char const *message); + + /** @brief pushes a message onto the stack using printf-like formatting + * + * @param type the message type + * @param format a printf-style format string + * + * @return the id of the pushed message + */ + MessageId pushF(MessageType type, char const *format, ...) G_GNUC_PRINTF(3,4); + + /** @brief pushes a message onto the stack using printf-like formatting, + * using a stdarg argument list + * + * @param type the message type + * @param format a printf-style format string + * @param args the subsequent printf-style arguments + * + * @return the id of the pushed message + */ + MessageId pushVF(MessageType type, char const *format, va_list args); + + /** @brief removes a message from the stack, given its id + * + * This method will remove a message from the stack if it has not + * already been removed. It may be removed from any part of the stack. + * + * @param id the message id to remove + */ + void cancel(MessageId id); + + /** + * Temporarily pushes a message onto the stack. + * + * @param type the message type + * @param message the message text + * + * @return the id of the pushed message + */ + MessageId flash(MessageType type, char const *message); + + /** + * Temporarily pushes a message onto the stack. + * + * @param type the message type + * @param message the message text + * + * @return the id of the pushed message + */ + MessageId flash(MessageType type, Glib::ustring const &message); + + + /** @brief temporarily pushes a message onto the stack using + * printf-like formatting + * + * @param type the message type + * @param format a printf-style format string + * + * @return the id of the pushed message + */ + MessageId flashF(MessageType type, char const *format, ...) G_GNUC_PRINTF(3,4); + + /** @brief temporarily pushes a message onto the stack using + * printf-like formatting, using a stdarg argument list + * + * @param type the message type + * @param format a printf-style format string + * @param args the printf-style arguments + * + * @return the id of the pushed message + */ + MessageId flashVF(MessageType type, char const *format, va_list args); + +private: + struct Message { + Message *next; + MessageStack *stack; + MessageId id; + MessageType type; + gchar *message; + guint timeout_id; + }; + + /// pushes a message onto the stack with an optional timeout + MessageId _push(MessageType type, unsigned int lifetime, char const *message); + + Message *_discard(Message *m); ///< frees a message struct and returns the next such struct in the list + void _emitChanged(); ///< emits the "changed" signal + static int _timeout(void* data); ///< callback to expire flashed messages + + sigc::signal _changed_signal; + Message *_messages; ///< the stack of messages as a linked list + MessageId _next_id; ///< the next message id to assign +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/message.h b/src/message.h new file mode 100644 index 0000000..fe16bbd --- /dev/null +++ b/src/message.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * status-message-related types + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_MESSAGE_H +#define SEEN_INKSCAPE_MESSAGE_H + +namespace Inkscape { + +/** + * A hint about the meaning of a message; is it an ordinary message, + * a message advising the user of some significant or unexpected condition, + * or a message indicating an unambiguous error. + */ +enum MessageType { + NORMAL_MESSAGE, + IMMEDIATE_MESSAGE, + WARNING_MESSAGE, + ERROR_MESSAGE, + INFORMATION_MESSAGE +}; + +/** + * An integer ID which identifies a displayed message in a particular + * Inkscape::MessageStack + * + * @see Inkscape::MessageStack + */ +typedef unsigned long MessageId; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/mod360.cpp b/src/mod360.cpp new file mode 100644 index 0000000..182022d --- /dev/null +++ b/src/mod360.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include +#include + +#include "mod360.h" + +/** Returns \a x wrapped around to between 0 and less than 360, + or 0 if \a x isn't finite. +**/ +double mod360(double const x) +{ + double const m = fmod(x, 360.0); + double const ret = ( std::isnan(m) + ? 0.0 + : ( m < 0 + ? m + 360 + : m ) ); + g_return_val_if_fail(0.0 <= ret && ret < 360.0, + 0.0); + return ret; +} + +/** Returns \a x wrapped around to between -180 and less than 180, + or 0 if \a x isn't finite. +**/ +double mod360symm(double const x) +{ + double m = mod360(x); + + return m < 180.0 ? m : m - 360.0; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/mod360.h b/src/mod360.h new file mode 100644 index 0000000..5dd1401 --- /dev/null +++ b/src/mod360.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_MOD360_H +#define SEEN_MOD360_H + +double mod360(double const x); +double mod360symm (double const x); + +#endif /* !SEEN_MOD360_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/number-opt-number.h b/src/number-opt-number.h new file mode 100644 index 0000000..6bb97f6 --- /dev/null +++ b/src/number-opt-number.h @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NUMBER_OPT_NUMBER_H +#define SEEN_NUMBER_OPT_NUMBER_H + +/** \file + * implementation. + */ +/* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "svg/stringstream.h" + +class NumberOptNumber { + +public: + + float number; + + float optNumber; + + unsigned int _set : 1; + + unsigned int optNumber_set : 1; + + NumberOptNumber() + { + number = 0.0; + optNumber = 0.0; + + _set = FALSE; + optNumber_set = FALSE; + } + + float getNumber() + { + if(_set) + return number; + return -1; + } + + float getOptNumber() + { + if(optNumber_set) + return optNumber; + return -1; + } + + void setOptNumber(float num) + { + optNumber_set = true; + optNumber = num; + } + + void setNumber(float num) + { + _set = true; + number = num; + } + + bool optNumIsSet(){ + return optNumber_set; + } + + bool numIsSet(){ + return _set; + } + + char *getValueString() + { + Inkscape::SVGOStringStream os; + + if( _set ) + { + + if( optNumber_set ) + { + os << number << " " << optNumber; + } + else { + os << number; + } + } + return g_strdup(os.str().c_str()); + } + + void set(char const *str) + { + if(!str) + return; + + char **values = g_strsplit(str, " ", 2); + + if( values[0] != nullptr ) + { + number = g_ascii_strtod(values[0], nullptr); + _set = TRUE; + + if( values[1] != nullptr ) + { + optNumber = g_ascii_strtod(values[1], nullptr); + optNumber_set = TRUE; + } + else + optNumber_set = FALSE; + } + else { + _set = FALSE; + optNumber_set = FALSE; + } + + g_strfreev(values); + } + +}; + +#endif /* !SEEN_NUMBER_OPT_NUMBER_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object-hierarchy.cpp b/src/object-hierarchy.cpp new file mode 100644 index 0000000..c05bb6f --- /dev/null +++ b/src/object-hierarchy.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Object hierarchy implementation. + * + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "object-hierarchy.h" + +#include "object/sp-object.h" + +namespace Inkscape { + +ObjectHierarchy::ObjectHierarchy(SPObject *top) { + if (top) { + _addBottom(top); + } +} + +ObjectHierarchy::~ObjectHierarchy() { + _clear(); +} + +void ObjectHierarchy::clear() { + _clear(); + _changed_signal.emit(nullptr, nullptr); +} + +void ObjectHierarchy::setTop(SPObject *object) { + if (object == nullptr) { printf("Assertion object != NULL failed\n"); return; } + + if ( top() == object ) { + return; + } + + if (!top()) { + _addTop(object); + } else if (object->isAncestorOf(top())) { + _addTop(object, top()); + } else if ( object == bottom() || object->isAncestorOf(bottom()) ) { + _trimAbove(object); + } else { + _clear(); + _addTop(object); + } + + _changed_signal.emit(top(), bottom()); +} + +void ObjectHierarchy::_addTop(SPObject *senior, SPObject *junior) { + assert(junior != NULL); + assert(senior != NULL); + + SPObject *object = junior->parent; + do { + _addTop(object); + object = object->parent; + } while ( object != senior ); +} + +void ObjectHierarchy::_addTop(SPObject *object) { + assert(object != NULL); + _hierarchy.push_back(_attach(object)); + _added_signal.emit(object); +} + +void ObjectHierarchy::_trimAbove(SPObject *limit) { + while ( !_hierarchy.empty() && _hierarchy.back().object != limit ) { + SPObject *object=_hierarchy.back().object; + + sp_object_ref(object, nullptr); + _detach(_hierarchy.back()); + _hierarchy.pop_back(); + _removed_signal.emit(object); + sp_object_unref(object, nullptr); + } +} + +void ObjectHierarchy::setBottom(SPObject *object) { + if (object == nullptr) { printf("assertion object != NULL failed\n"); return; } + + if ( bottom() == object ) { + return; + } + + if (!top()) { + _addBottom(object); + } else if (bottom()->isAncestorOf(object)) { + _addBottom(bottom(), object); + } else if ( top() == object ) { + _trimBelow(top()); + } else if (top()->isAncestorOf(object)) { + if (object->isAncestorOf(bottom())) { + _trimBelow(object); + } else { // object is a sibling or cousin of bottom() + SPObject *saved_top=top(); + sp_object_ref(saved_top, nullptr); + _clear(); + _addBottom(saved_top); + _addBottom(saved_top, object); + sp_object_unref(saved_top, nullptr); + } + } else { + _clear(); + _addBottom(object); + } + + _changed_signal.emit(top(), bottom()); +} + +void ObjectHierarchy::_trimBelow(SPObject *limit) { + while ( !_hierarchy.empty() && _hierarchy.front().object != limit ) { + SPObject *object=_hierarchy.front().object; + sp_object_ref(object, nullptr); + _detach(_hierarchy.front()); + _hierarchy.pop_front(); + _removed_signal.emit(object); + sp_object_unref(object, nullptr); + } +} + +void ObjectHierarchy::_addBottom(SPObject *senior, SPObject *junior) { + assert(junior != NULL); + assert(senior != NULL); + + if ( junior != senior ) { + _addBottom(senior, junior->parent); + _addBottom(junior); + } +} + +void ObjectHierarchy::_addBottom(SPObject *object) { + assert(object != NULL); + _hierarchy.push_front(_attach(object)); + _added_signal.emit(object); +} + +void ObjectHierarchy::_trim_for_release(SPObject *object) { + this->_trimBelow(object); + assert(!this->_hierarchy.empty()); + assert(this->_hierarchy.front().object == object); + + sp_object_ref(object, nullptr); + this->_detach(this->_hierarchy.front()); + this->_hierarchy.pop_front(); + this->_removed_signal.emit(object); + sp_object_unref(object, nullptr); + + this->_changed_signal.emit(this->top(), this->bottom()); +} + +ObjectHierarchy::Record ObjectHierarchy::_attach(SPObject *object) { + sp_object_ref(object, nullptr); + sigc::connection connection + = object->connectRelease( + sigc::mem_fun(*this, &ObjectHierarchy::_trim_for_release) + ); + return Record(object, connection); +} + +void ObjectHierarchy::_detach(ObjectHierarchy::Record &rec) { + rec.connection.disconnect(); + sp_object_unref(rec.object, nullptr); +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object-hierarchy.h b/src/object-hierarchy.h new file mode 100644 index 0000000..e515a3d --- /dev/null +++ b/src/object-hierarchy.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_OBJECT_HIERARCHY_H +#define SEEN_INKSCAPE_OBJECT_HIERARCHY_H + +#include +#include +#include +#include +#include + +class SPObject; + +namespace Inkscape { + +/** + * An Inkscape::ObjectHierarchy is useful for situations where one wishes + * to keep a reference to an SPObject, but fall back on one of its ancestors + * when that object is removed. + * + * That cannot be accomplished simply by hooking the "release" signal of the + * SPObject, as by the time that signal is emitted, the object's parent + * field has already been cleared. + * + * There are also some subtle refcounting issues to take into account. + * + * @see SPObject + */ +class ObjectHierarchy { +public: + + /** + * Create new object hierarchy. + * @param top The first entry if non-NULL. + */ + ObjectHierarchy(SPObject *top=nullptr); + + ~ObjectHierarchy(); + + bool contains(SPObject *object); + + sigc::connection connectAdded(const sigc::slot &slot) { + return _added_signal.connect(slot); + } + sigc::connection connectRemoved(const sigc::slot &slot) { + return _removed_signal.connect(slot); + } + sigc::connection connectChanged(const sigc::slot &slot) + { + return _changed_signal.connect(slot); + } + + /** + * Remove all entries. + */ + void clear(); + + SPObject *top() { + return !_hierarchy.empty() ? _hierarchy.back().object : nullptr; + } + + /** + * Trim or expand hierarchy on top such that object becomes top entry. + */ + void setTop(SPObject *object); + + SPObject *bottom() { + return !_hierarchy.empty() ? _hierarchy.front().object : nullptr; + } + + /** + * Trim or expand hierarchy at bottom such that object becomes bottom entry. + */ + void setBottom(SPObject *object); + +private: + struct Record { + Record(SPObject *o, sigc::connection c) + : object(o), connection(c) {} + + SPObject *object; + sigc::connection connection; + }; + + ObjectHierarchy(ObjectHierarchy const &); // no copy + + void operator=(ObjectHierarchy const &); // no assign + + /** + * Add hierarchy from junior's parent to senior to this + * hierarchy's top. + */ + void _addTop(SPObject *senior, SPObject *junior); + + /** + * Add object to top of hierarchy. + * \pre object!=NULL. + */ + void _addTop(SPObject *object); + + /** + * Remove all objects above limit from hierarchy. + */ + void _trimAbove(SPObject *limit); + + /** + * Add hierarchy from senior to junior, in range (senior, junior], to this hierarchy's bottom. + */ + void _addBottom(SPObject *senior, SPObject *junior); + + /** + * Add object at bottom of hierarchy. + * \pre object!=NULL + */ + void _addBottom(SPObject *object); + + /** + * Remove all objects under given object. + * @param limit If NULL, remove all. + */ + void _trimBelow(SPObject *limit); + + Record _attach(SPObject *object); + + void _detach(Record &record); + + void _clear() { _trimBelow(nullptr); } + + void _trim_for_release(SPObject *released); + + std::list _hierarchy; + sigc::signal _added_signal; + sigc::signal _removed_signal; + sigc::signal _changed_signal; +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object-snapper.cpp b/src/object-snapper.cpp new file mode 100644 index 0000000..61c440f --- /dev/null +++ b/src/object-snapper.cpp @@ -0,0 +1,879 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Snapping things to objects. + * + * Authors: + * Carl Hetherington + * Diederik van Lierop + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2005 - 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/circle.h> +#include <2geom/line.h> +#include <2geom/path-intersection.h> +#include <2geom/path-sink.h> + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "text-editing.h" + +#include "splivarot.h" // curve_for_item, pathvector_for_curves +#include "object/sp-clippath.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" + +#include "svg/svg.h" + +Inkscape::ObjectSnapper::ObjectSnapper(SnapManager *sm, Geom::Coord const d) + : Snapper(sm, d) +{ + _candidates = new std::vector; + _points_to_snap_to = new std::vector; + _paths_to_snap_to = new std::vector; +} + +Inkscape::ObjectSnapper::~ObjectSnapper() +{ + _candidates->clear(); + delete _candidates; + + _points_to_snap_to->clear(); + delete _points_to_snap_to; + + _clear_paths(); + delete _paths_to_snap_to; +} + +Geom::Coord Inkscape::ObjectSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getObjectTolerance() / zoom; +} + +bool Inkscape::ObjectSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getObjectTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +void Inkscape::ObjectSnapper::_findCandidates(SPObject* parent, + std::vector const *it, + bool const &first_point, + Geom::Rect const &bbox_to_snap, + bool const clip_or_mask, + Geom::Affine const additional_affine) const // transformation of the item being clipped / masked +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + if (dt == nullptr) { + g_warning("desktop == NULL, so we cannot snap; please inform the developers of this bug"); + // Apparently the setup() method from the SnapManager class hasn't been called before trying to snap. + } + + if (first_point) { + _candidates->clear(); + } + + Geom::Rect bbox_to_snap_incl = bbox_to_snap; // _incl means: will include the snapper tolerance + bbox_to_snap_incl.expandBy(getSnapperTolerance()); // see? + + for (auto& o: parent->children) { + g_assert(dt != nullptr); + SPItem *item = dynamic_cast(&o); + if (item && !(dt->itemIsHidden(item) && !clip_or_mask)) { + // Snapping to items in a locked layer is allowed + // Don't snap to hidden objects, unless they're a clipped path or a mask + /* See if this item is on the ignore list */ + std::vector::const_iterator i; + if (it != nullptr) { + i = it->begin(); + while (i != it->end() && *i != &o) { + ++i; + } + } + + if (it == nullptr || i == it->end()) { + if (item) { + if (!clip_or_mask) { // cannot clip or mask more than once + // The current item is not a clipping path or a mask, but might + // still be the subject of clipping or masking itself ; if so, then + // we should also consider that path or mask for snapping to + SPObject *obj = item->getClipObject(); + if (obj && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_CLIP)) { + _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); + } + obj = item->getMaskObject(); + if (obj && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_MASK)) { + _findCandidates(obj, it, false, bbox_to_snap, true, item->i2doc_affine()); + } + } + + if (dynamic_cast(item)) { + _findCandidates(&o, it, false, bbox_to_snap, clip_or_mask, additional_affine); + } else { + Geom::OptRect bbox_of_item; + Preferences *prefs = Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box", false); + // We'll only need to obtain the visual bounding box if the user preferences tell + // us to, AND if we are snapping to the bounding box itself. If we're snapping to + // paths only, then we can just as well use the geometric bounding box (which is faster) + SPItem::BBoxType bbox_type = (!prefs_bbox && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + if (clip_or_mask) { + // Oh oh, this will get ugly. We cannot use sp_item_i2d_affine directly because we need to + // insert an additional transformation in document coordinates (code copied from sp_item_i2d_affine) + bbox_of_item = item->bounds(bbox_type, item->i2doc_affine() * additional_affine * dt->doc2dt()); + } else { + bbox_of_item = item->desktopBounds(bbox_type); + } + if (bbox_of_item) { + // See if the item is within range + if (bbox_to_snap_incl.intersects(*bbox_of_item) + || (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ROTATION_CENTER) && bbox_to_snap_incl.contains(item->getCenter()))) { // rotation center might be outside of the bounding box + // This item is within snapping range, so record it as a candidate + _candidates->push_back(SnapCandidateItem(item, clip_or_mask, additional_affine)); + // For debugging: print the id of the candidate to the console + // SPObject *obj = (SPObject*)item; + // std::cout << "Snap candidate added: " << obj->getId() << std::endl; + if (_candidates->size() > 200) { // This makes Inkscape crawl already + std::cout << "Warning: limit of 200 snap target paths reached, some will be ignored" << std::endl; + break; + } + } + } + } + } + } + } + } +} + + +void Inkscape::ObjectSnapper::_collectNodes(SnapSourceType const &t, + bool const &first_point) const +{ + // Now, let's first collect all points to snap to. If we have a whole bunch of points to snap, + // e.g. when translating an item using the selector tool, then we will only do this for the + // first point and store the collection for later use. This significantly improves the performance + if (first_point) { + _points_to_snap_to->clear(); + + // Determine the type of bounding box we should snap to + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + bool p_is_a_node = t & SNAPSOURCE_NODE_CATEGORY; + bool p_is_a_bbox = t & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_other = (t & SNAPSOURCE_OTHERS_CATEGORY) || (t & SNAPSOURCE_DATUMS_CATEGORY); + + // A point considered for snapping should be either a node, a bbox corner or a guide/other. Pick only ONE! + if (((p_is_a_node && p_is_a_bbox) || (p_is_a_bbox && p_is_other) || (p_is_a_node && p_is_other))) { + g_warning("Snap warning: node type is ambiguous"); + } + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER, SNAPTARGET_BBOX_EDGE_MIDPOINT, SNAPTARGET_BBOX_MIDPOINT)) { + Preferences *prefs = Preferences::get(); + bool prefs_bbox = prefs->getBool("/tools/bounding_box"); + bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + } + + // Consider the page border for snapping to + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_CORNER)) { + _getBorderNodes(_points_to_snap_to); + } + + for (const auto & _candidate : *_candidates) { + //Geom::Affine i2doc(Geom::identity()); + SPItem *root_item = _candidate.item; + + SPUse *use = dynamic_cast(_candidate.item); + if (use) { + root_item = use->root(); + } + g_return_if_fail(root_item); + + //Collect all nodes so we can snap to them + if (p_is_a_node || p_is_other || (p_is_a_bbox && !_snapmanager->snapprefs.getStrictSnapping())) { + // Note: there are two ways in which intersections are considered: + // Method 1: Intersections are calculated for each shape individually, for both the + // snap source and snap target (see sp_shape_snappoints) + // Method 2: Intersections are calculated for each curve or line that we've snapped to, i.e. only for + // the target (see the intersect() method in the SnappedCurve and SnappedLine classes) + // Some differences: + // - Method 1 doesn't find intersections within a set of multiple objects + // - Method 2 only works for targets + // When considering intersections as snap targets: + // - Method 1 only works when snapping to nodes, whereas + // - Method 2 only works when snapping to paths + // - There will be performance differences too! + // If both methods are being used simultaneously, then this might lead to duplicate targets! + + // Well, here we will be looking for snap TARGETS. Both methods can therefore be used. + // When snapping to paths, we will get a collection of snapped lines and snapped curves. findBestSnap() will + // go hunting for intersections (but only when asked to in the prefs of course). In that case we can just + // temporarily block the intersections in sp_item_snappoints, we don't need duplicates. If we're not snapping to + // paths though but only to item nodes then we should still look for the intersections in sp_item_snappoints() + bool old_pref = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH_INTERSECTION); + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH)) { + // So if we snap to paths, then findBestSnap will find the intersections + // and therefore we temporarily disable SNAPTARGET_PATH_INTERSECTION, which will + // avoid root_item->getSnappoints() below from returning intersections + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_PATH_INTERSECTION, false); + } + + // We should not snap a transformation center to any of the centers of the items in the + // current selection (see the comment in SelTrans::centerRequest()) + bool old_pref2 = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_ROTATION_CENTER); + if (old_pref2) { + std::vector rotationSource=_snapmanager->getRotationCenterSource(); + for (auto itemlist : rotationSource) { + if (_candidate.item == itemlist) { + // don't snap to this item's rotation center + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_ROTATION_CENTER, false); + break; + } + } + } + + root_item->getSnappoints(*_points_to_snap_to, &_snapmanager->snapprefs); + + // restore the original snap preferences + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_PATH_INTERSECTION, old_pref); + _snapmanager->snapprefs.setTargetSnappable(SNAPTARGET_ROTATION_CENTER, old_pref2); + } + + //Collect the bounding box's corners so we can snap to them + if (p_is_a_bbox || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_node) || p_is_other) { + // Discard the bbox of a clipped path / mask, because we don't want to snap to both the bbox + // of the item AND the bbox of the clipping path at the same time + if (!_candidate.clip_or_mask) { + Geom::OptRect b = root_item->desktopBounds(bbox_type); + getBBoxPoints(b, _points_to_snap_to, true, + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER), + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE_MIDPOINT), + _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT)); + } + } + } + } +} + +void Inkscape::ObjectSnapper::_snapNodes(IntermSnapResults &isr, + SnapCandidatePoint const &p, + std::vector *unselected_nodes, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const +{ + // Iterate through all nodes, find out which one is the closest to p, and snap to it! + + _collectNodes(p.getSourceType(), p.getSourceNum() <= 0); + + if (unselected_nodes != nullptr && unselected_nodes->size() > 0) { + g_assert(_points_to_snap_to != nullptr); + _points_to_snap_to->insert(_points_to_snap_to->end(), unselected_nodes->begin(), unselected_nodes->end()); + } + + SnappedPoint s; + bool success = false; + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + + for (const auto & k : *_points_to_snap_to) { + if (_allowSourceToSnapToTarget(p.getSourceType(), k.getTargetType(), strict_snapping)) { + Geom::Point target_pt = k.getPoint(); + Geom::Coord dist = Geom::L2(target_pt - p.getPoint()); // Default: free (unconstrained) snapping + if (!c.isUndefined()) { + // We're snapping to nodes along a constraint only, so find out if this node + // is at the constraint, while allowing for a small margin + if (Geom::L2(target_pt - c.projection(target_pt)) > 1e-9) { + // The distance from the target point to its projection on the constraint + // is too large, so this point is not on the constraint. Skip it! + continue; + } + dist = Geom::L2(target_pt - p_proj_on_constraint); + } + + if (dist < getSnapperTolerance() && dist < s.getSnapDistance()) { + s = SnappedPoint(target_pt, p.getSourceType(), p.getSourceNum(), k.getTargetType(), dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true, k.getTargetBBox()); + success = true; + } + } + } + + if (success) { + isr.points.push_back(s); + } +} + +void Inkscape::ObjectSnapper::_snapTranslatingGuide(IntermSnapResults &isr, + Geom::Point const &p, + Geom::Point const &guide_normal) const +{ + // Iterate through all nodes, find out which one is the closest to this guide, and snap to it! + _collectNodes(SNAPSOURCE_GUIDE, true); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + _collectPaths(p, SNAPSOURCE_GUIDE, true); + _snapPaths(isr, SnapCandidatePoint(p, SNAPSOURCE_GUIDE), nullptr, nullptr); + } + + SnappedPoint s; + + Geom::Coord tol = getSnapperTolerance(); + + for (const auto & k : *_points_to_snap_to) { + Geom::Point target_pt = k.getPoint(); + // Project each node (*k) on the guide line (running through point p) + Geom::Point p_proj = Geom::projection(target_pt, Geom::Line(p, p + Geom::rot90(guide_normal))); + Geom::Coord dist = Geom::L2(target_pt - p_proj); // distance from node to the guide + Geom::Coord dist2 = Geom::L2(p - p_proj); // distance from projection of node on the guide, to the mouse location + if ((dist < tol && dist2 < tol) || getSnapperAlwaysSnap()) { + s = SnappedPoint(target_pt, SNAPSOURCE_GUIDE, 0, k.getTargetType(), dist, tol, getSnapperAlwaysSnap(), false, true, k.getTargetBBox()); + isr.points.push_back(s); + } + } +} + + +/// @todo investigate why Geom::Point p is passed in but ignored. +void Inkscape::ObjectSnapper::_collectPaths(Geom::Point /*p*/, + SnapSourceType const source_type, + bool const &first_point) const +{ + // Now, let's first collect all paths to snap to. If we have a whole bunch of points to snap, + // e.g. when translating an item using the selector tool, then we will only do this for the + // first point and store the collection for later use. This significantly improves the performance + if (first_point) { + _clear_paths(); + + // Determine the type of bounding box we should snap to + SPItem::BBoxType bbox_type = SPItem::GEOMETRIC_BBOX; + + bool p_is_a_node = source_type & SNAPSOURCE_NODE_CATEGORY; + bool p_is_a_bbox = source_type & SNAPSOURCE_BBOX_CATEGORY; + bool p_is_other = (source_type & SNAPSOURCE_OTHERS_CATEGORY) || (source_type & SNAPSOURCE_DATUMS_CATEGORY); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE)) { + Preferences *prefs = Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box", false); + bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + } + + // Consider the page border for snapping + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PAGE_BORDER) && _snapmanager->snapprefs.isAnyCategorySnappable()) { + Geom::PathVector *border_path = _getBorderPathv(); + if (border_path != nullptr) { + _paths_to_snap_to->push_back(SnapCandidatePath(border_path, SNAPTARGET_PAGE_BORDER, Geom::OptRect())); + } + } + + for (const auto & _candidate : *_candidates) { + + /* Transform the requested snap point to this item's coordinates */ + Geom::Affine i2doc(Geom::identity()); + SPItem *root_item = nullptr; + /* We might have a clone at hand, so make sure we get the root item */ + SPUse *use = dynamic_cast(_candidate.item); + if (use) { + i2doc = use->get_root_transform(); + root_item = use->root(); + g_return_if_fail(root_item); + } else { + i2doc = _candidate.item->i2doc_affine(); + root_item = _candidate.item; + } + + //Build a list of all paths considered for snapping to + + //Add the item's path to snap to + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_TEXT_BASELINE)) { + if (p_is_other || p_is_a_node || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_bbox)) { + if (dynamic_cast(root_item) || dynamic_cast(root_item)) { + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_TEXT_BASELINE)) { + // Snap to the text baseline + Text::Layout const *layout = te_get_layout(static_cast(root_item)); + if (layout != nullptr && layout->outputExists()) { + Geom::PathVector *pv = new Geom::PathVector(); + pv->push_back(layout->baseline() * root_item->i2dt_affine() * _candidate.additional_affine * _snapmanager->getDesktop()->doc2dt()); + _paths_to_snap_to->push_back(SnapCandidatePath(pv, SNAPTARGET_TEXT_BASELINE, Geom::OptRect())); + } + } + } else { + // Snapping for example to a traced bitmap is very stressing for + // the CPU, so we'll only snap to paths having no more than 500 nodes + // This also leads to a lag of approx. 500 msec (in my lousy test set-up). + bool very_complex_path = false; + SPPath *path = dynamic_cast(root_item); + if (path) { + very_complex_path = path->nodesInPath() > 500; + } + + if (!very_complex_path && root_item && _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION)) { + SPCurve *curve = nullptr; + SPShape *shape = dynamic_cast(root_item); + if (shape) { + curve = shape->getCurve(); + }/* else if (dynamic_cast(root_item) || dynamic_cast(root_item)) { + curve = te_get_layout(root_item)->convertToCurves(); + }*/ + if (curve) { + // We will get our own copy of the pathvector, which must be freed at some point + + // Geom::PathVector *pv = pathvector_for_curve(root_item, curve, true, true, Geom::identity(), (*i).additional_affine); + + Geom::PathVector *pv = new Geom::PathVector(curve->get_pathvector()); + (*pv) *= root_item->i2dt_affine() * _candidate.additional_affine * _snapmanager->getDesktop()->doc2dt(); // (_edit_transform * _i2d_transform); + + _paths_to_snap_to->push_back(SnapCandidatePath(pv, SNAPTARGET_PATH, Geom::OptRect())); // Perhaps for speed, get a reference to the Geom::pathvector, and store the transformation besides it. + curve->unref(); + } + } + } + } + } + + //Add the item's bounding box to snap to + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE)) { + if (p_is_other || p_is_a_bbox || (!_snapmanager->snapprefs.getStrictSnapping() && p_is_a_node)) { + // Discard the bbox of a clipped path / mask, because we don't want to snap to both the bbox + // of the item AND the bbox of the clipping path at the same time + if (!_candidate.clip_or_mask) { + Geom::OptRect rect = root_item->bounds(bbox_type, i2doc); + if (rect) { + Geom::PathVector *path = _getPathvFromRect(*rect); + rect = root_item->desktopBounds(bbox_type); + _paths_to_snap_to->push_back(SnapCandidatePath(path, SNAPTARGET_BBOX_EDGE, rect)); + } + } + } + } + } + } +} + +void Inkscape::ObjectSnapper::_snapPaths(IntermSnapResults &isr, + SnapCandidatePoint const &p, + std::vector *unselected_nodes, + SPPath const *selected_path) const +{ + _collectPaths(p.getPoint(), p.getSourceType(), p.getSourceNum() <= 0); + // Now we can finally do the real snapping, using the paths collected above + + SPDesktop const *dt = _snapmanager->getDesktop(); + g_assert(dt != nullptr); + Geom::Point const p_doc = dt->dt2doc(p.getPoint()); + + bool const node_tool_active = _snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION) && selected_path != nullptr; + + if (p.getSourceNum() <= 0) { + /* findCandidates() is used for snapping to both paths and nodes. It ignores the path that is + * currently being edited, because that path requires special care: when snapping to nodes + * only the unselected nodes of that path should be considered, and these will be passed on separately. + * This path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable. + * */ + if (node_tool_active) { + // TODO fix the function to be const correct: + SPCurve *curve = curve_for_item(const_cast(selected_path)); + if (curve) { + Geom::PathVector *pathv = pathvector_for_curve(const_cast(selected_path), + curve, + true, + true, + Geom::identity(), + Geom::identity()); // We will get our own copy of the path, which must be freed at some point + _paths_to_snap_to->push_back(SnapCandidatePath(pathv, SNAPTARGET_PATH, Geom::OptRect(), true)); + curve->unref(); + } + } + } + + int num_path = 0; // _paths_to_snap_to contains multiple path_vectors, each containing multiple paths. + // num_path will count the paths, and will not be zeroed for each path_vector. It will + // continue counting + + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + bool snap_perp = _snapmanager->snapprefs.getSnapPerp(); + bool snap_tang = _snapmanager->snapprefs.getSnapTang(); + + //dt->snapindicator->remove_debugging_points(); + for (const auto & it_p : *_paths_to_snap_to) { + if (_allowSourceToSnapToTarget(p.getSourceType(), it_p.target_type, strict_snapping)) { + bool const being_edited = node_tool_active && it_p.currently_being_edited; + //if true then this pathvector it_pv is currently being edited in the node tool + + for(Geom::PathVector::iterator it_pv = (it_p.path_vector)->begin(); it_pv != (it_p.path_vector)->end(); ++it_pv) { + // Find a nearest point for each curve within this path + // n curves will return n time values with 0 <= t <= 1 + std::vector anp = (*it_pv).nearestTimePerCurve(p_doc); + + //std::cout << "#nearest points = " << anp.size() << " | p = " << p.getPoint() << std::endl; + // Now we will examine each of the nearest points, and determine whether it's within snapping range and if we should snap to it + std::vector::const_iterator np = anp.begin(); + unsigned int index = 0; + for (; np != anp.end(); ++np, index++) { + Geom::Curve const *curve = &(it_pv->at(index)); + Geom::Point const sp_doc = curve->pointAt(*np); + //dt->snapindicator->set_new_debugging_point(sp_doc*dt->doc2dt()); + bool c1 = true; + bool c2 = true; + if (being_edited) { + /* If the path is being edited, then we should only snap though to stationary pieces of the path + * and not to the pieces that are being dragged around. This way we avoid + * self-snapping. For this we check whether the nodes at both ends of the current + * piece are unselected; if they are then this piece must be stationary + */ + g_assert(unselected_nodes != nullptr); + Geom::Point start_pt = dt->doc2dt(curve->pointAt(0)); + Geom::Point end_pt = dt->doc2dt(curve->pointAt(1)); + c1 = isUnselectedNode(start_pt, unselected_nodes); + c2 = isUnselectedNode(end_pt, unselected_nodes); + /* Unfortunately, this might yield false positives for coincident nodes. Inkscape might therefore mistakenly + * snap to path segments that are not stationary. There are at least two possible ways to overcome this: + * - Linking the individual nodes of the SPPath we have here, to the nodes of the NodePath::SubPath class as being + * used in sp_nodepath_selected_nodes_move. This class has a member variable called "selected". For this the nodes + * should be in the exact same order for both classes, so we can index them + * - Replacing the SPPath being used here by the NodePath::SubPath class; but how? + */ + } + + Geom::Point const sp_dt = dt->doc2dt(sp_doc); + if (!being_edited || (c1 && c2)) { + Geom::Coord dist = Geom::distance(sp_doc, p_doc); + // std::cout << " dist -> " << dist << std::endl; + if (dist < getSnapperTolerance()) { + // Add the curve we have snapped to + Geom::Point sp_tangent_dt = Geom::Point(0,0); + if (p.getSourceType() == Inkscape::SNAPSOURCE_GUIDE_ORIGIN) { + // We currently only use the tangent when snapping guides, so only in this case we will + // actually calculate the tangent to avoid wasting CPU cycles + Geom::Point sp_tangent_doc = curve->unitTangentAt(*np); + sp_tangent_dt = dt->doc2dt(sp_tangent_doc) - dt->doc2dt(Geom::Point(0,0)); + } + isr.curves.emplace_back(sp_dt, sp_tangent_dt, num_path, index, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, curve, p.getSourceType(), p.getSourceNum(), it_p.target_type, it_p.target_bbox); + if (snap_tang || snap_perp) { + // For each curve that's within snapping range, we will now also search for tangential and perpendicular snaps + _snapPathsTangPerp(snap_tang, snap_perp, isr, p, curve, dt); + } + } + } + } + num_path++; + } // End of: for (Geom::PathVector::iterator ....) + } + } +} + +/* Returns true if point is coincident with one of the unselected nodes */ +bool Inkscape::ObjectSnapper::isUnselectedNode(Geom::Point const &point, std::vector const *unselected_nodes) const +{ + if (unselected_nodes == nullptr) { + return false; + } + + if (unselected_nodes->size() == 0) { + return false; + } + + for (const auto & unselected_node : *unselected_nodes) { + if (Geom::L2(point - unselected_node.getPoint()) < 1e-4) { + return true; + } + } + + return false; +} + +void Inkscape::ObjectSnapper::_snapPathsConstrained(IntermSnapResults &isr, + SnapCandidatePoint const &p, + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const +{ + + _collectPaths(p_proj_on_constraint, p.getSourceType(), p.getSourceNum() <= 0); + + // Now we can finally do the real snapping, using the paths collected above + + SPDesktop const *dt = _snapmanager->getDesktop(); + g_assert(dt != nullptr); + + Geom::Point direction_vector = c.getDirection(); + if (!is_zero(direction_vector)) { + direction_vector = Geom::unit_vector(direction_vector); + } + + // The intersection point of the constraint line with any path, must lie within two points on the + // SnapConstraint: p_min_on_cl and p_max_on_cl. The distance between those points is twice the snapping tolerance + Geom::Point const p_min_on_cl = dt->dt2doc(p_proj_on_constraint - getSnapperTolerance() * direction_vector); + Geom::Point const p_max_on_cl = dt->dt2doc(p_proj_on_constraint + getSnapperTolerance() * direction_vector); + Geom::Coord tolerance = getSnapperTolerance(); + + // PS: Because the paths we're about to snap to are all expressed relative to document coordinate system, we will have + // to convert the snapper coordinates from the desktop coordinates to document coordinates + + Geom::PathVector constraint_path; + if (c.isCircular()) { + Geom::Circle constraint_circle(dt->dt2doc(c.getPoint()), c.getRadius()); + Geom::PathBuilder pb; + pb.feed(constraint_circle); + pb.flush(); + constraint_path = pb.peek(); + } else { + Geom::Path constraint_line; + constraint_line.start(p_min_on_cl); + constraint_line.appendNew(p_max_on_cl); + constraint_path.push_back(constraint_line); + } + + bool strict_snapping = _snapmanager->snapprefs.getStrictSnapping(); + + // Find all intersections of the constrained path with the snap target candidates + for (const auto & k : *_paths_to_snap_to) { + if (k.path_vector && _allowSourceToSnapToTarget(p.getSourceType(), k.target_type, strict_snapping)) { + // Do the intersection math + std::vector inters = constraint_path.intersect(*(k.path_vector)); + + // Convert the collected intersections to snapped points + for (const auto & inter : inters) { + // Convert to desktop coordinates + Geom::Point p_inters = dt->doc2dt(inter.point()); + // Construct a snapped point + Geom::Coord dist = Geom::L2(p.getPoint() - p_inters); + SnappedPoint s = SnappedPoint(p_inters, p.getSourceType(), p.getSourceNum(), k.target_type, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), true, false, k.target_bbox); + // Store the snapped point + if (dist <= tolerance) { // If the intersection is within snapping range, then we might snap to it + isr.points.push_back(s); + } + } + } + } +} + + +void Inkscape::ObjectSnapper::freeSnap(IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const +{ + if (_snap_enabled == false || _snapmanager->snapprefs.isSourceSnappable(p.getSourceType()) == false || ThisSnapperMightSnap() == false) { + return; + } + + /* Get a list of all the SPItems that we will try to snap to */ + if (p.getSourceNum() <= 0) { + Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(p.getPoint(), p.getPoint()); + _findCandidates(_snapmanager->getDocument()->getRoot(), it, p.getSourceNum() <= 0, local_bbox_to_snap, false, Geom::identity()); + } + + _snapNodes(isr, p, unselected_nodes); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + unsigned n = (unselected_nodes == nullptr) ? 0 : unselected_nodes->size(); + if (n > 0) { + /* While editing a path in the node tool, findCandidates must ignore that path because + * of the node snapping requirements (i.e. only unselected nodes must be snapable). + * That path must not be ignored however when snapping to the paths, so we add it here + * manually when applicable + */ + SPPath const *path = nullptr; + if (it != nullptr) { + SPPath const *tmpPath = dynamic_cast(*it->begin()); + if ((it->size() == 1) && tmpPath) { + path = tmpPath; + } // else: *it->begin() might be a SPGroup, e.g. when editing a LPE of text that has been converted to a group of paths + // as reported in bug #356743. In that case we can just ignore it, i.e. not snap to this item + } + _snapPaths(isr, p, unselected_nodes, path); + } else { + _snapPaths(isr, p, nullptr, nullptr); + } + } +} + +void Inkscape::ObjectSnapper::constrainedSnap( IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const +{ + if (_snap_enabled == false || _snapmanager->snapprefs.isSourceSnappable(p.getSourceType()) == false || ThisSnapperMightSnap() == false) { + return; + } + + // project the mouse pointer onto the constraint. Only the projected point will be considered for snapping + Geom::Point pp = c.projection(p.getPoint()); + + /* Get a list of all the SPItems that we will try to snap to */ + if (p.getSourceNum() <= 0) { + Geom::Rect const local_bbox_to_snap = bbox_to_snap ? *bbox_to_snap : Geom::Rect(pp, pp); + _findCandidates(_snapmanager->getDocument()->getRoot(), it, p.getSourceNum() <= 0, local_bbox_to_snap, false, Geom::identity()); + } + + // A constrained snap, is a snap in only one degree of freedom (specified by the constraint line). + // This is useful for example when scaling an object while maintaining a fixed aspect ratio. It's + // nodes are only allowed to move in one direction (i.e. in one degree of freedom). + + _snapNodes(isr, p, unselected_nodes, c, pp); + + if (_snapmanager->snapprefs.isTargetSnappable(SNAPTARGET_PATH, SNAPTARGET_PATH_INTERSECTION, SNAPTARGET_BBOX_EDGE, SNAPTARGET_PAGE_BORDER, SNAPTARGET_TEXT_BASELINE)) { + _snapPathsConstrained(isr, p, c, pp); + } +} + +bool Inkscape::ObjectSnapper::ThisSnapperMightSnap() const +{ + return true; +} + +void Inkscape::ObjectSnapper::_clear_paths() const +{ + for (const auto & k : *_paths_to_snap_to) { + delete k.path_vector; + } + _paths_to_snap_to->clear(); +} + +Geom::PathVector* Inkscape::ObjectSnapper::_getBorderPathv() const +{ + Geom::Rect const border_rect = Geom::Rect(Geom::Point(0,0), Geom::Point((_snapmanager->getDocument())->getWidth().value("px"),(_snapmanager->getDocument())->getHeight().value("px"))); + return _getPathvFromRect(border_rect); +} + +Geom::PathVector* Inkscape::ObjectSnapper::_getPathvFromRect(Geom::Rect const rect) const +{ + SPCurve const *border_curve = SPCurve::new_from_rect(rect, true); + if (border_curve) { + Geom::PathVector *dummy = new Geom::PathVector(border_curve->get_pathvector()); + return dummy; + } else { + return nullptr; + } +} + +void Inkscape::ObjectSnapper::_getBorderNodes(std::vector *points) const +{ + Geom::Coord w = (_snapmanager->getDocument())->getWidth().value("px"); + Geom::Coord h = (_snapmanager->getDocument())->getHeight().value("px"); + points->push_back(SnapCandidatePoint(Geom::Point(0,0), SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_CORNER)); + points->push_back(SnapCandidatePoint(Geom::Point(0,h), SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_CORNER)); + points->push_back(SnapCandidatePoint(Geom::Point(w,h), SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_CORNER)); + points->push_back(SnapCandidatePoint(Geom::Point(w,0), SNAPSOURCE_UNDEFINED, SNAPTARGET_PAGE_CORNER)); +} + +void Inkscape::getBBoxPoints(Geom::OptRect const bbox, + std::vector *points, + bool const /*isTarget*/, + bool const includeCorners, + bool const includeLineMidpoints, + bool const includeObjectMidpoints) +{ + if (bbox) { + // collect the corners of the bounding box + for ( unsigned k = 0 ; k < 4 ; k++ ) { + if (includeCorners) { + points->push_back(SnapCandidatePoint(bbox->corner(k), SNAPSOURCE_BBOX_CORNER, -1, SNAPTARGET_BBOX_CORNER, *bbox)); + } + // optionally, collect the midpoints of the bounding box's edges too + if (includeLineMidpoints) { + points->push_back(SnapCandidatePoint((bbox->corner(k) + bbox->corner((k+1) % 4))/2, SNAPSOURCE_BBOX_EDGE_MIDPOINT, -1, SNAPTARGET_BBOX_EDGE_MIDPOINT, *bbox)); + } + } + if (includeObjectMidpoints) { + points->push_back(SnapCandidatePoint(bbox->midpoint(), SNAPSOURCE_BBOX_MIDPOINT, -1, SNAPTARGET_BBOX_MIDPOINT, *bbox)); + } + } +} + +bool Inkscape::ObjectSnapper::_allowSourceToSnapToTarget(SnapSourceType source, SnapTargetType target, bool strict_snapping) const +{ + bool allow_this_pair_to_snap = true; + + if (strict_snapping) { // bounding boxes will not snap to nodes/paths and vice versa + if (((source & SNAPSOURCE_BBOX_CATEGORY) && (target & SNAPTARGET_NODE_CATEGORY)) || + ((source & SNAPSOURCE_NODE_CATEGORY) && (target & SNAPTARGET_BBOX_CATEGORY))) { + allow_this_pair_to_snap = false; + } + } + + return allow_this_pair_to_snap; +} + +void Inkscape::ObjectSnapper::_snapPathsTangPerp(bool snap_tang, bool snap_perp, IntermSnapResults &isr, SnapCandidatePoint const &p, Geom::Curve const *curve, SPDesktop const *dt) const +{ + // Here we will try to snap either tangentially or perpendicularly to a single path; for this we need to know where the origin is located of the line that is currently being rotated, + // or we need to know the vector of the guide which is currently being translated + std::vector > const origins_and_vectors = p.getOriginsAndVectors(); + // Now we will iterate over all the origins and vectors and see which of these will get use a tangential or perpendicular snap + for (const auto & origins_and_vector : origins_and_vectors) { + Geom::Point origin_or_vector_doc = dt->dt2doc(origins_and_vector.first); // "first" contains a Geom::Point, denoting either a point or vector + if (origins_and_vector.second) { // if "second" is true then "first" is a vector, otherwise it's a point + // So we have a vector, which tells us what tangential or perpendicular direction we're looking for + if (curve->degreesOfFreedom() <= 2) { // A LineSegment has order one, and therefore 2 DOF + // When snapping to a point of a line segment that has a specific tangential or normal vector, then either all point + // along that line will be snapped to or no points at all will be snapped to. This is not very useful, so let's skip + // any line segments and lets only snap to higher order curves + continue; + } + // The vector is being treated as a point (relative to the origin), and has been translated to document coordinates accordingly + // We need however to make it a vector again, because also the origin has been transformed + origin_or_vector_doc -= dt->dt2doc(Geom::Point(0,0)); + } + + Geom::Point point_dt; + Geom::Coord dist; + std::vector ts; + + if (snap_tang) { // Find all points that lead to a tangential snap + if (origins_and_vector.second) { // if "second" is true then "first" is a vector, otherwise it's a point + ts = find_tangents_by_vector(origin_or_vector_doc, curve->toSBasis()); + } else { + ts = find_tangents(origin_or_vector_doc, curve->toSBasis()); + } + for (double t : ts) { + point_dt = dt->doc2dt(curve->pointAt(t)); + dist = Geom::distance(point_dt, p.getPoint()); + isr.points.emplace_back(point_dt, p.getSourceType(), p.getSourceNum(), SNAPTARGET_PATH_TANGENTIAL, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + } + } + + if (snap_perp) { // Find all points that lead to a perpendicular snap + if (origins_and_vector.second) { + ts = find_normals_by_vector(origin_or_vector_doc, curve->toSBasis()); + } else { + ts = find_normals(origin_or_vector_doc, curve->toSBasis()); + } + for (double t : ts) { + point_dt = dt->doc2dt(curve->pointAt(t)); + dist = Geom::distance(point_dt, p.getPoint()); + isr.points.emplace_back(point_dt, p.getSourceType(), p.getSourceNum(), SNAPTARGET_PATH_PERPENDICULAR, dist, getSnapperTolerance(), getSnapperAlwaysSnap(), false, true); + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object-snapper.h b/src/object-snapper.h new file mode 100644 index 0000000..ac40dbc --- /dev/null +++ b/src/object-snapper.h @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_OBJECT_SNAPPER_H +#define SEEN_OBJECT_SNAPPER_H +/* + * Authors: + * Carl Hetherington + * Diederik van Lierop + * + * Copyright (C) 2005 - 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snapper.h" +#include "snap-candidate.h" + +class SPDesktop; +class SPNamedView; +class SPItem; +class SPObject; +class SPPath; + +namespace Inkscape +{ + +/** + * Snapping things to objects. + */ +class ObjectSnapper : public Snapper +{ + +public: + ObjectSnapper(SnapManager *sm, Geom::Coord const d); + ~ObjectSnapper() override; + + /** + * @return true if this Snapper will snap at least one kind of point. + */ + bool ThisSnapperMightSnap() const override; + + /** + * @return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels. + */ + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + + void freeSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + std::vector const *it, + std::vector *unselected_nodes) const override; + + void constrainedSnap(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + SnapConstraint const &c, + std::vector const *it, + std::vector *unselected_nodes) const override; + +private: + //store some lists of candidates, points and paths, so we don't have to rebuild them for each point we want to snap + std::vector *_candidates; + std::vector *_points_to_snap_to; + std::vector *_paths_to_snap_to; + + /** + * Find all items within snapping range. + * @param parent Pointer to the document's root, or to a clipped path or mask object. + * @param it List of items to ignore. + * @param bbox_to_snap Bounding box hulling the whole bunch of points, all from the same selection and having the same transformation. + * @param clip_or_mask The parent object being passed is either a clip or mask. + */ + void _findCandidates(SPObject* parent, + std::vector const *it, + bool const &first_point, + Geom::Rect const &bbox_to_snap, + bool const _clip_or_mask, + Geom::Affine const additional_affine) const; + + void _snapNodes(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + std::vector *unselected_nodes, + SnapConstraint const &c = SnapConstraint(), + Geom::Point const &p_proj_on_constraint = Geom::Point()) const; + + void _snapTranslatingGuide(IntermSnapResults &isr, + Geom::Point const &p, + Geom::Point const &guide_normal) const; + + void _collectNodes(Inkscape::SnapSourceType const &t, + bool const &first_point) const; + + void _snapPaths(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + std::vector *unselected_nodes, // in desktop coordinates + SPPath const *selected_path) const; + + void _snapPathsConstrained(IntermSnapResults &isr, + Inkscape::SnapCandidatePoint const &p, // in desktop coordinates + SnapConstraint const &c, + Geom::Point const &p_proj_on_constraint) const; + + void _snapPathsTangPerp(bool snap_tang, + bool snap_perp, + IntermSnapResults &isr, + SnapCandidatePoint const &p, + Geom::Curve const *curve, + SPDesktop const *dt) const; + + bool isUnselectedNode(Geom::Point const &point, std::vector const *unselected_nodes) const; + + /** + * Returns index of first NR_END bpath in array. + */ + void _collectPaths(Geom::Point p, + Inkscape::SnapSourceType const source_type, + bool const &first_point) const; + + void _clear_paths() const; + Geom::PathVector* _getBorderPathv() const; + Geom::PathVector* _getPathvFromRect(Geom::Rect const rect) const; + void _getBorderNodes(std::vector *points) const; + bool _allowSourceToSnapToTarget(SnapSourceType source, SnapTargetType target, bool strict_snapping) const; + +}; // end of ObjectSnapper class + +void getBBoxPoints(Geom::OptRect const bbox, std::vector *points, bool const isTarget, bool const includeCorners, bool const includeLineMidpoints, bool const includeObjectMidpoints); + +} // end of namespace Inkscape + +#endif diff --git a/src/object/CMakeLists.txt b/src/object/CMakeLists.txt new file mode 100644 index 0000000..e44d56f --- /dev/null +++ b/src/object/CMakeLists.txt @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + + +set(object_SRC + box3d-side.cpp + box3d.cpp + color-profile.cpp + object-set.cpp + persp3d-reference.cpp + persp3d.cpp + sp-anchor.cpp + sp-clippath.cpp + sp-conn-end-pair.cpp + sp-conn-end.cpp + sp-defs.cpp + sp-desc.cpp + sp-dimensions.cpp + sp-ellipse.cpp + sp-factory.cpp + sp-filter-reference.cpp + sp-filter.cpp + sp-flowdiv.cpp + sp-flowregion.cpp + sp-flowtext.cpp + sp-font-face.cpp + sp-font.cpp + sp-glyph-kerning.cpp + sp-glyph.cpp + sp-gradient-reference.cpp + sp-gradient.cpp + sp-guide.cpp + sp-hatch-path.cpp + sp-hatch.cpp + sp-image.cpp + sp-item-group.cpp + sp-item-rm-unsatisfied-cns.cpp + sp-item-transform.cpp + sp-item-update-cns.cpp + sp-item.cpp + sp-line.cpp + sp-linear-gradient.cpp + sp-lpe-item.cpp + sp-marker.cpp + sp-mask.cpp + sp-mesh-array.cpp + sp-mesh-gradient.cpp + sp-mesh-patch.cpp + sp-mesh-row.cpp + sp-metadata.cpp + sp-missing-glyph.cpp + sp-namedview.cpp + sp-object-group.cpp + sp-object.cpp + sp-offset.cpp + sp-paint-server.cpp + sp-path.cpp + sp-pattern.cpp + sp-polygon.cpp + sp-polyline.cpp + sp-radial-gradient.cpp + sp-rect.cpp + sp-root.cpp + sp-script.cpp + sp-shape.cpp + sp-shape-reference.cpp + sp-solid-color.cpp + sp-spiral.cpp + sp-star.cpp + sp-stop.cpp + sp-string.cpp + sp-style-elem.cpp + sp-switch.cpp + sp-symbol.cpp + sp-tag-use-reference.cpp + sp-tag-use.cpp + sp-tag.cpp + sp-text.cpp + sp-title.cpp + sp-tref-reference.cpp + sp-tref.cpp + sp-tspan.cpp + sp-use-reference.cpp + sp-use.cpp + uri-references.cpp + uri.cpp + viewbox.cpp + + # ------- + # Headers + box3d-side.h + box3d.h + color-profile.h + object-set.h + persp3d-reference.h + persp3d.h + sp-anchor.h + sp-clippath.h + sp-conn-end-pair.h + sp-conn-end.h + sp-defs.h + sp-desc.h + sp-dimensions.h + sp-ellipse.h + sp-factory.h + sp-filter-reference.h + sp-filter-units.h + sp-filter.h + sp-flowdiv.h + sp-flowregion.h + sp-flowtext.h + sp-font-face.h + sp-font.h + sp-glyph-kerning.h + sp-glyph.h + sp-gradient-reference.h + sp-gradient-spread.h + sp-gradient-units.h + sp-gradient-vector.h + sp-gradient.h + sp-guide.h + sp-hatch-path.h + sp-hatch.h + sp-image.h + sp-item-group.h + sp-item-rm-unsatisfied-cns.h + sp-item-transform.h + sp-item-update-cns.h + sp-item.h + sp-line.h + sp-linear-gradient.h + sp-lpe-item.h + sp-marker-loc.h + sp-marker.h + sp-mask.h + sp-mesh-array.h + sp-mesh-gradient.h + sp-mesh-patch.h + sp-mesh-row.h + sp-metadata.h + sp-missing-glyph.h + sp-namedview.h + sp-object-group.h + sp-object.h + sp-offset.h + sp-paint-server-reference.h + sp-paint-server.h + sp-path.h + sp-pattern.h + sp-polygon.h + sp-polyline.h + sp-radial-gradient.h + sp-rect.h + sp-root.h + sp-script.h + sp-shape.h + sp-shape-reference.h + sp-solid-color.h + sp-spiral.h + sp-star.h + sp-stop.h + sp-string.h + sp-style-elem.h + sp-switch.h + sp-symbol.h + sp-tag.h + sp-tag-use.h + sp-tag-use-reference.h + sp-text.h + sp-textpath.h + sp-title.h + sp-tref-reference.h + sp-tref.h + sp-tspan.h + sp-use-reference.h + sp-use.h + uri-references.h + uri.h + viewbox.h +) + +add_inkscape_source("${object_SRC}") + +add_subdirectory(filters) diff --git a/src/object/README b/src/object/README new file mode 100644 index 0000000..aded8d6 --- /dev/null +++ b/src/object/README @@ -0,0 +1,116 @@ + +This directory contains classes that are derived from SPObject as well +as closely related code. + +The object tree implements an XML-to-display primitive mapping, and +provides an object hierarchy that can be modified using the +GUI. Changes in the XML tree are automatically propagated to the +object tree via observers, but not the other way around — a function +called updateRepr() must be explicitly called. Relevant nodes of the +object tree contains fully cascaded CSS style information. The object +tree also includes clones of objects that are referenced by the +element in the XML tree (this is needed as clones may have different +styling due to inheritance). + +See: http://wiki.inkscape.org/wiki/index.php/Object_tree + +Object class inheritance: + +SPObject sp-object.h: + ColorProfile color-profile.h: + Persp3D persp3d.h: + SPDefs sp-defs.h: + SPDesc sp-desc.h: + SPFilter sp-filter.h: + SPFlowline sp-flowdiv.h: + SPFlowregionbreak sp-flowdiv.h: + SPFontFace sp-font-face.h: + SPFont sp-font.h: + SPGlyph sp-glyph.h: + SPGlyphKerning sp-glyph-kerning.h: + SPHkern sp-glyph-kerning.h: + SPVkern sp-glyph-kerning.h: + SPGuide sp-guide.h: + SPHatchPath sp-hatch-path.h: + SPItem sp-item.h: + SPFlowdiv sp-flowdiv.h: + SPFlowtspan sp-flowdiv.h: + SPFlowpara sp-flowdiv.h: + SPFlowregion sp-flowregion.h: + SPFlowregionExclude sp-flowregion.h: + SPFlowtext sp-flowtext.h: + SPImage sp-image.h: + SPLPEItem sp-lpe-item.h: + SPGroup sp-item-group.h: + SPBox3D box3d.h: + SPAnchor sp-anchor.h: + SPMarker sp-marker.h: + SPRoot sp-root.h: + SPSwitch sp-switch.h: + SPSymbol sp-symbol.h: + SPShape sp-shape.h: + SPGenericEllipse sp-ellipse.h: + SPLine sp-line.h: + SPOffset sp-offset.h: + SPPath sp-path.h: + SPPolygon sp-polygon.h: + SPStar sp-star.h: + SPPolyLine sp-polyline.h: + Box3DSide box3d-side.h: + SPRect sp-rect.h: + SPSpiral sp-spiral.h: + SPText sp-text.h: + SPTextPath sp-textpath.h: + SPTRef sp-tref.h: + SPTSpan sp-tspan.h: + SPUse sp-use.h: + SPMeshpatch sp-mesh-patch.h: + SPMeshrow sp-mesh-row.h: + SPMetadata sp-metadata.h: + SPMissingGlyph sp-missing-glyph.h: + SPObjectGroup sp-object-group.h: + SPClipPath sp-clippath.h: + SPMask sp-mask.h: + SPNamedView sp-namedview.h: + SPPaintServer sp-paint-server.h: + SPGradient sp-gradient.h: + SPLinearGradient sp-linear-gradient.h: + SPMeshGradient sp-mesh-gradient.h: + SPRadialGradient sp-radial-gradient.h: + SPHatch sp-hatch.h: + SPPattern sp-pattern.h: + SPSolidColor sp-solid-color.h: + SPScript sp-script.h: + SPStop sp-stop.h: + SPString sp-string.h: + SPStyleElem sp-style-elem.h: + SPTag sp-tag.h: + SPTagUse sp-tag-use.h: + SPTitle sp-title.h: + +Other related files: + + object-set.h: + persp3d-reference.h + sp-conn-end-pair.h + sp-conn-end.h + sp-dimensions.h + sp-factory.h + sp-filter-reference.h + sp-filter-units.h + sp-gradient-reference.h + sp-gradient-spread.h + sp-gradient-units.h + sp-gradient-vector.h + sp-item-rm-unsatisfied-cns.h + sp-item-transform.h + sp-item-update-cns.h + sp-marker-loc.h + sp-mesh-array.h + sp-paint-server-reference.h + sp-tag-use-reference.h + sp-tref-reference.h + sp-use-reference.h + uri.h + uri-references.h + viewbox.h diff --git a/src/object/box3d-side.cpp b/src/object/box3d-side.cpp new file mode 100644 index 0000000..53b37f6 --- /dev/null +++ b/src/object/box3d-side.cpp @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3D box face implementation + * + * Authors: + * Maximilian Albert + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d-side.h" +#include "document.h" +#include "xml/document.h" +#include "xml/repr.h" +#include "display/curve.h" +#include "svg/svg.h" +#include "attributes.h" +#include "inkscape.h" +#include "persp3d.h" +#include "persp3d-reference.h" +#include "ui/tools/box3d-tool.h" +#include "desktop-style.h" + +static void box3d_side_compute_corner_ids(Box3DSide *side, unsigned int corners[4]); + +Box3DSide::Box3DSide() : SPPolygon() { + this->dir1 = Box3D::NONE; + this->dir2 = Box3D::NONE; + this->front_or_rear = Box3D::FRONT; +} + +Box3DSide::~Box3DSide() = default; + +void Box3DSide::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPPolygon::build(document, repr); + + this->readAttr( "inkscape:box3dsidetype" ); +} + + +Inkscape::XML::Node* Box3DSide::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?) + // thus we don' set "sodipodi:type" so that the box is only saved as an ordinary svg:path + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + sp_repr_set_int(repr, "inkscape:box3dsidetype", this->dir1 ^ this->dir2 ^ this->front_or_rear); + } + + this->set_shape(); + + /* Duplicate the path */ + SPCurve const *curve = this->_curve; + + //Nulls might be possible if this called iteratively + if ( !curve ) { + return nullptr; + } + + char *d = sp_svg_write_path ( curve->get_pathvector() ); + repr->setAttribute("d", d); + g_free (d); + + SPPolygon::write(xml_doc, repr, flags); + + return repr; +} + +void Box3DSide::set(SPAttributeEnum key, const gchar* value) { + // TODO: In case the box was recreated (by undo, e.g.) we need to recreate the path + // (along with other info?) from the parent box. + + /* fixme: we should really collect updates */ + switch (key) { + case SP_ATTR_INKSCAPE_BOX3D_SIDE_TYPE: + if (value) { + guint desc = atoi (value); + + if (!Box3D::is_face_id(desc)) { + g_print ("desc is not a face id: =%s=\n", value); + } + + g_return_if_fail (Box3D::is_face_id (desc)); + + Box3D::Axis plane = (Box3D::Axis) (desc & 0x7); + plane = (Box3D::is_plane(plane) ? plane : Box3D::orth_plane_or_axis(plane)); + this->dir1 = Box3D::extract_first_axis_direction(plane); + this->dir2 = Box3D::extract_second_axis_direction(plane); + this->front_or_rear = (Box3D::FrontOrRear) (desc & 0x8); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPPolygon::set(key, value); + break; + } +} + +void Box3DSide::update(SPCtx* ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + if (flags & (SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + this->set_shape(); + } + + SPPolygon::update(ctx, flags); +} + +/* Create a new Box3DSide and append it to the parent box */ +Box3DSide * Box3DSide::createBox3DSide(SPBox3D *box) +{ + Box3DSide *box3d_side = nullptr; + Inkscape::XML::Document *xml_doc = box->document->getReprDoc();; + Inkscape::XML::Node *repr_side = xml_doc->createElement("svg:path"); + repr_side->setAttribute("sodipodi:type", "inkscape:box3dside"); + box3d_side = static_cast(box->appendChildRepr(repr_side)); + return box3d_side; +} + +/* + * Function which return the type attribute for Box3D. + * Acts as a replacement for directly accessing the XML Tree directly. + */ +int Box3DSide::getFaceId() +{ + return this->getIntAttribute("inkscape:box3dsidetype", -1); +} + +void +box3d_side_position_set (Box3DSide *side) { + side->set_shape(); + + // This call is responsible for live update of the sides during the initial drag + side->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void Box3DSide::set_shape() { + if (!this->document->getRoot()) { + // avoid a warning caused by sp_document_height() (which is called from sp_item_i2d_affine() below) + // when reading a file containing 3D boxes + return; + } + + SPObject *parent = this->parent; + + SPBox3D *box = dynamic_cast(parent); + if (!box) { + g_warning("Parent of 3D box side is not a 3D box.\n"); + return; + } + + Persp3D *persp = box3d_side_perspective(this); + + if (!persp) { + return; + } + + // TODO: Draw the correct quadrangle here + // To do this, determine the perspective of the box, the orientation of the side (e.g., XY-FRONT) + // compute the coordinates of the corners in P^3, project them onto the canvas, and draw the + // resulting path. + + unsigned int corners[4]; + box3d_side_compute_corner_ids(this, corners); + + SPCurve *c = new SPCurve(); + + if (!box3d_get_corner_screen(box, corners[0]).isFinite() || + !box3d_get_corner_screen(box, corners[1]).isFinite() || + !box3d_get_corner_screen(box, corners[2]).isFinite() || + !box3d_get_corner_screen(box, corners[3]).isFinite() ) + { + g_warning ("Trying to draw a 3D box side with invalid coordinates.\n"); + delete c; + return; + } + + c->moveto(box3d_get_corner_screen(box, corners[0])); + c->lineto(box3d_get_corner_screen(box, corners[1])); + c->lineto(box3d_get_corner_screen(box, corners[2])); + c->lineto(box3d_get_corner_screen(box, corners[3])); + c->closepath(); + + /* Reset the shape's curve to the "original_curve" + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + SPCurve * before = this->getCurveBeforeLPE(); + bool haslpe = this->hasPathEffectOnClipOrMaskRecursive(this); + if (before || haslpe) { + if (c && before && before->get_pathvector() != c->get_pathvector()){ + this->setCurveBeforeLPE(c); + sp_lpe_item_update_patheffect(this, true, false); + } else if(haslpe) { + this->setCurveBeforeLPE(c); + } else { + //This happends on undo, fix bug:#1791784 + this->setCurveInsync(c); + } + } else { + this->setCurveInsync(c); + } + + if (before) { + before->unref(); + } + c->unref(); +} + +Glib::ustring box3d_side_axes_string(Box3DSide *side) +{ + Glib::ustring result(Box3D::string_from_axes((Box3D::Axis) (side->dir1 ^ side->dir2))); + + switch ((Box3D::Axis) (side->dir1 ^ side->dir2)) { + case Box3D::XY: + result += ((side->front_or_rear == Box3D::FRONT) ? "front" : "rear"); + break; + + case Box3D::XZ: + result += ((side->front_or_rear == Box3D::FRONT) ? "top" : "bottom"); + break; + + case Box3D::YZ: + result += ((side->front_or_rear == Box3D::FRONT) ? "right" : "left"); + break; + + default: + break; + } + + return result; +} + +static void +box3d_side_compute_corner_ids(Box3DSide *side, unsigned int corners[4]) { + Box3D::Axis orth = Box3D::third_axis_direction (side->dir1, side->dir2); + + corners[0] = (side->front_or_rear ? orth : 0); + corners[1] = corners[0] ^ side->dir1; + corners[2] = corners[0] ^ side->dir1 ^ side->dir2; + corners[3] = corners[0] ^ side->dir2; +} + +Persp3D * +box3d_side_perspective(Box3DSide *side) { + SPBox3D *box = side ? dynamic_cast(side->parent) : nullptr; + return box ? box->persp_ref->getObject() : nullptr; +} + +Inkscape::XML::Node *box3d_side_convert_to_path(Box3DSide *side) { + // TODO: Copy over all important attributes (see sp_selected_item_to_curved_repr() for an example) + SPDocument *doc = side->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("d", side->getAttribute("d")); + repr->setAttribute("style", side->getAttribute("style")); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/box3d-side.h b/src/object/box3d-side.h new file mode 100644 index 0000000..63420f8 --- /dev/null +++ b/src/object/box3d-side.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_BOX3D_SIDE_H +#define SEEN_BOX3D_SIDE_H + +/* + * 3D box face implementation + * + * Authors: + * Maximilian Albert + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-polygon.h" +#include "axis-manip.h" + + +class SPBox3D; +class Persp3D; + +// FIXME: Would it be better to inherit from SPPath instead? +class Box3DSide : public SPPolygon { +public: + Box3DSide(); + ~Box3DSide() override; + + Box3D::Axis dir1; + Box3D::Axis dir2; + Box3D::FrontOrRear front_or_rear; + int getFaceId(); + static Box3DSide * createBox3DSide(SPBox3D *box); + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void update(SPCtx *ctx, unsigned int flags) override; + + void set_shape() override; +}; + +void box3d_side_position_set (Box3DSide *side); // FIXME: Replace this by box3d_side_set_shape?? + +Glib::ustring box3d_side_axes_string(Box3DSide *side); + +Persp3D *box3d_side_perspective(Box3DSide *side); + + +Inkscape::XML::Node *box3d_side_convert_to_path(Box3DSide *side); + +#endif // SEEN_BOX3D_SIDE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/box3d.cpp b/src/object/box3d.cpp new file mode 100644 index 0000000..ecdcbf4 --- /dev/null +++ b/src/object/box3d.cpp @@ -0,0 +1,1360 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Maximilian Albert + * Lauris Kaplinski + * bulia byak + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2007 Authors + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d.h" + +#include +#include "attributes.h" +#include "xml/document.h" +#include "xml/repr.h" + +#include "bad-uri-exception.h" +#include "box3d-side.h" +#include "ui/tools/box3d-tool.h" +#include "perspective-line.h" +#include "persp3d-reference.h" +#include "uri.h" +#include <2geom/line.h> +#include "sp-guide.h" +#include "sp-namedview.h" + +#include "desktop.h" + +#include "include/macros.h" + +static void box3d_ref_changed(SPObject *old_ref, SPObject *ref, SPBox3D *box); + +static gint counter = 0; + +SPBox3D::SPBox3D() : SPGroup() { + this->my_counter = 0; + this->swapped = Box3D::NONE; + + this->persp_href = nullptr; + this->persp_ref = new Persp3DReference(this); + + /* we initialize the z-orders to zero so that they are updated during dragging */ + for (int & z_order : z_orders) { + z_order = 0; + } +} + +SPBox3D::~SPBox3D() = default; + +void SPBox3D::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGroup::build(document, repr); + + my_counter = counter++; + + /* we initialize the z-orders to zero so that they are updated during dragging */ + for (int & z_order : z_orders) { + z_order = 0; + } + + // TODO: Create/link to the correct perspective + + if ( document ) { + persp_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(box3d_ref_changed), this)); + + readAttr( "inkscape:perspectiveID" ); + readAttr( "inkscape:corner0" ); + readAttr( "inkscape:corner7" ); + } +} + +void SPBox3D::release() { + SPBox3D* object = this; + SPBox3D *box = object; + + if (box->persp_href) { + g_free(box->persp_href); + } + + // We have to store this here because the Persp3DReference gets destroyed below, but we need to + // access it to call persp3d_remove_box(), which cannot be called earlier because the reference + // needs to be destroyed first. + Persp3D *persp = box3d_get_perspective(box); + + if (box->persp_ref) { + box->persp_ref->detach(); + delete box->persp_ref; + box->persp_ref = nullptr; + } + + if (persp) { + persp3d_remove_box (persp, box); + /* + // TODO: This deletes a perspective when the last box referring to it is gone. Eventually, + // it would be nice to have this but currently it crashes when undoing/redoing box deletion + // Reason: When redoing a box deletion, the associated perspective is deleted twice, first + // by the following code and then again by the redo mechanism! Perhaps we should perform + // deletion of the perspective from another location "outside" the undo/redo mechanism? + if (persp->perspective_impl->boxes.empty()) { + SPDocument *doc = box->document; + persp->deleteObject(); + doc->setCurrentPersp3D(persp3d_document_first_persp(doc)); + } + */ + } + + SPGroup::release(); +} + +void SPBox3D::set(SPAttributeEnum key, const gchar* value) { + SPBox3D* object = this; + SPBox3D *box = object; + + switch (key) { + case SP_ATTR_INKSCAPE_BOX3D_PERSPECTIVE_ID: + if ( value && box->persp_href && ( strcmp(value, box->persp_href) == 0 ) ) { + /* No change, do nothing. */ + } else { + if (box->persp_href) { + g_free(box->persp_href); + box->persp_href = nullptr; + } + if (value) { + box->persp_href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + box->persp_ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + box->persp_ref->detach(); + } + } else { + // Detach, which emits the changed signal. + box->persp_ref->detach(); + } + } + + // FIXME: Is the following update doubled by some call in either persp3d.cpp or vanishing_point_new.cpp? + box3d_position_set(box); + break; + case SP_ATTR_INKSCAPE_BOX3D_CORNER0: + if (value && strcmp(value, "0 : 0 : 0 : 0")) { + box->orig_corner0 = Proj::Pt3(value); + box->save_corner0 = box->orig_corner0; + box3d_position_set(box); + } + break; + case SP_ATTR_INKSCAPE_BOX3D_CORNER7: + if (value && strcmp(value, "0 : 0 : 0 : 0")) { + box->orig_corner7 = Proj::Pt3(value); + box->save_corner7 = box->orig_corner7; + box3d_position_set(box); + } + break; + default: + SPGroup::set(key, value); + break; + } +} + +/** + * Gets called when (re)attached to another perspective. + */ +static void +box3d_ref_changed(SPObject *old_ref, SPObject *ref, SPBox3D *box) +{ + if (old_ref) { + sp_signal_disconnect_by_data(old_ref, box); + Persp3D *oldPersp = dynamic_cast(old_ref); + if (oldPersp) { + persp3d_remove_box(oldPersp, box); + } + } + Persp3D *persp = dynamic_cast(ref); + if ( persp && (ref != box) ) // FIXME: Comparisons sane? + { + persp3d_add_box(persp, box); + } +} + +void SPBox3D::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* FIXME?: Perhaps the display updates of box sides should be instantiated from here, but this + causes evil update loops so it's all done from box3d_position_set, which is called from + various other places (like the handlers in shape-editor-knotholders.cpp, vanishing-point.cpp, etc. */ + + } + + // Invoke parent method + SPGroup::update(ctx, flags); +} + +Inkscape::XML::Node* SPBox3D::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPBox3D* object = this; + SPBox3D *box = object; + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?) + // thus we don' set "sodipodi:type" so that the box is only saved as an ordinary svg:g + repr = xml_doc->createElement("svg:g"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + + if (box->persp_href) { + repr->setAttribute("inkscape:perspectiveID", box->persp_href); + } else { + /* box is not yet linked to a perspective; use the document's current perspective */ + SPDocument *doc = object->document; + if (box->persp_ref->getURI()) { + auto uri_string = box->persp_ref->getURI()->str(); + repr->setAttributeOrRemoveIfEmpty("inkscape:perspectiveID", uri_string); + } else { + Glib::ustring href = "#"; + href += doc->getCurrentPersp3D()->getId(); + repr->setAttribute("inkscape:perspectiveID", href); + } + } + + gchar *coordstr0 = box->orig_corner0.coord_string(); + gchar *coordstr7 = box->orig_corner7.coord_string(); + repr->setAttribute("inkscape:corner0", coordstr0); + repr->setAttribute("inkscape:corner7", coordstr7); + g_free(coordstr0); + g_free(coordstr7); + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + box->save_corner0 = box->orig_corner0; + box->save_corner7 = box->orig_corner7; + } + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPBox3D::display_name() { + return _("3D Box"); +} + +void box3d_position_set(SPBox3D *box) +{ + /* This draws the curve and calls requestDisplayUpdate() for each side (the latter is done in + box3d_side_position_set() to avoid update conflicts with the parent box) */ + for (auto& obj: box->children) { + Box3DSide *side = dynamic_cast(&obj); + if (side) { + box3d_side_position_set(side); + } + } +} + +Geom::Affine SPBox3D::set_transform(Geom::Affine const &xform) { + // We don't apply the transform to the box directly but instead to its perspective (which is + // done in sp_selection_apply_affine). Here we only adjust strokes, patterns, etc. + + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + for (auto& child: children) { + SPItem *childitem = dynamic_cast(&child); + if (childitem) { + // Adjust stroke width + childitem->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + childitem->adjust_pattern(xform); + + // Adjust gradient fill + childitem->adjust_gradient(xform); + } + } + + return Geom::identity(); +} + +static Proj::Pt3 +box3d_get_proj_corner (guint id, Proj::Pt3 const &c0, Proj::Pt3 const &c7) { + return Proj::Pt3 ((id & Box3D::X) ? c7[Proj::X] : c0[Proj::X], + (id & Box3D::Y) ? c7[Proj::Y] : c0[Proj::Y], + (id & Box3D::Z) ? c7[Proj::Z] : c0[Proj::Z], + 1.0); +} + +Proj::Pt3 +box3d_get_proj_corner (SPBox3D const *box, guint id) { + return Proj::Pt3 ((id & Box3D::X) ? box->orig_corner7[Proj::X] : box->orig_corner0[Proj::X], + (id & Box3D::Y) ? box->orig_corner7[Proj::Y] : box->orig_corner0[Proj::Y], + (id & Box3D::Z) ? box->orig_corner7[Proj::Z] : box->orig_corner0[Proj::Z], + 1.0); +} + +Geom::Point +box3d_get_corner_screen (SPBox3D const *box, guint id, bool item_coords) { + Proj::Pt3 proj_corner (box3d_get_proj_corner (box, id)); + if (!box3d_get_perspective(box)) { + return Geom::Point (Geom::infinity(), Geom::infinity()); + } + Geom::Affine const i2d(box->i2dt_affine ()); + if (item_coords) { + return box3d_get_perspective(box)->perspective_impl->tmat.image(proj_corner).affine() * i2d.inverse(); + } else { + return box3d_get_perspective(box)->perspective_impl->tmat.image(proj_corner).affine(); + } +} + +Proj::Pt3 +box3d_get_proj_center (SPBox3D *box) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + return Proj::Pt3 ((box->orig_corner0[Proj::X] + box->orig_corner7[Proj::X]) / 2, + (box->orig_corner0[Proj::Y] + box->orig_corner7[Proj::Y]) / 2, + (box->orig_corner0[Proj::Z] + box->orig_corner7[Proj::Z]) / 2, + 1.0); +} + +Geom::Point +box3d_get_center_screen (SPBox3D *box) { + Proj::Pt3 proj_center (box3d_get_proj_center (box)); + if (!box3d_get_perspective(box)) { + return Geom::Point (Geom::infinity(), Geom::infinity()); + } + Geom::Affine const i2d( box->i2dt_affine() ); + return box3d_get_perspective(box)->perspective_impl->tmat.image(proj_center).affine() * i2d.inverse(); +} + +/* + * To keep the snappoint from jumping randomly between the two lines when the mouse pointer is close to + * their intersection, we remember the last snapped line and keep snapping to this specific line as long + * as the distance from the intersection to the mouse pointer is less than remember_snap_threshold. + */ + +// Should we make the threshold settable in the preferences? +static double remember_snap_threshold = 30; +static guint remember_snap_index = 0; + +// constant for sizing the array of points to be considered: +static const int MAX_POINT_COUNT = 4; + +static Proj::Pt3 +box3d_snap (SPBox3D *box, int id, Proj::Pt3 const &pt_proj, Proj::Pt3 const &start_pt) { + double z_coord = start_pt[Proj::Z]; + double diff_x = box->save_corner7[Proj::X] - box->save_corner0[Proj::X]; + double diff_y = box->save_corner7[Proj::Y] - box->save_corner0[Proj::Y]; + double x_coord = start_pt[Proj::X]; + double y_coord = start_pt[Proj::Y]; + Proj::Pt3 A_proj (x_coord, y_coord, z_coord, 1.0); + Proj::Pt3 B_proj (x_coord + diff_x, y_coord, z_coord, 1.0); + Proj::Pt3 C_proj (x_coord + diff_x, y_coord + diff_y, z_coord, 1.0); + Proj::Pt3 D_proj (x_coord, y_coord + diff_y, z_coord, 1.0); + Proj::Pt3 E_proj (x_coord - diff_x, y_coord + diff_y, z_coord, 1.0); + + Persp3DImpl *persp_impl = box3d_get_perspective(box)->perspective_impl; + Geom::Point A = persp_impl->tmat.image(A_proj).affine(); + Geom::Point B = persp_impl->tmat.image(B_proj).affine(); + Geom::Point C = persp_impl->tmat.image(C_proj).affine(); + Geom::Point D = persp_impl->tmat.image(D_proj).affine(); + Geom::Point E = persp_impl->tmat.image(E_proj).affine(); + Geom::Point pt = persp_impl->tmat.image(pt_proj).affine(); + + // TODO: Replace these lines between corners with lines from a corner to a vanishing point + // (this might help to prevent rounding errors if the box is small) + Box3D::Line pl1(A, B); + Box3D::Line pl2(A, D); + Box3D::Line diag1(A, (id == -1 || (!(id & Box3D::X) == !(id & Box3D::Y))) ? C : E); + Box3D::Line diag2(A, E); // diag2 is only taken into account if id equals -1, i.e., if we are snapping the center + + int num_snap_lines = (id != -1) ? 3 : 4; + Geom::Point snap_pts[MAX_POINT_COUNT]; + + snap_pts[0] = pl1.closest_to (pt); + snap_pts[1] = pl2.closest_to (pt); + snap_pts[2] = diag1.closest_to (pt); + if (id == -1) { + snap_pts[3] = diag2.closest_to (pt); + } + + gdouble const zoom = SP_ACTIVE_DESKTOP->current_zoom(); + + // determine the distances to all potential snapping points + double snap_dists[MAX_POINT_COUNT]; + for (int i = 0; i < num_snap_lines; ++i) { + snap_dists[i] = Geom::L2 (snap_pts[i] - pt) * zoom; + } + + // while we are within a given tolerance of the starting point, + // keep snapping to the same point to avoid jumping + bool within_tolerance = true; + for (int i = 0; i < num_snap_lines; ++i) { + if (snap_dists[i] > remember_snap_threshold) { + within_tolerance = false; + break; + } + } + + // find the closest snapping point + int snap_index = -1; + double snap_dist = Geom::infinity(); + for (int i = 0; i < num_snap_lines; ++i) { + if (snap_dists[i] < snap_dist) { + snap_index = i; + snap_dist = snap_dists[i]; + } + } + + // snap to the closest point (or the previously remembered one + // if we are within tolerance of the starting point) + Geom::Point result; + if (within_tolerance) { + result = snap_pts[remember_snap_index]; + } else { + remember_snap_index = snap_index; + result = snap_pts[snap_index]; + } + return box3d_get_perspective(box)->perspective_impl->tmat.preimage (result, z_coord, Proj::Z); +} + +SPBox3D * SPBox3D::createBox3D(SPItem * parent) +{ + SPBox3D *box3d = nullptr; + Inkscape::XML::Document *xml_doc = parent->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:g"); + repr->setAttribute("sodipodi:type", "inkscape:box3d"); + box3d = reinterpret_cast(parent->appendChildRepr(repr)); + return box3d; +} + +void +box3d_set_corner (SPBox3D *box, const guint id, Geom::Point const &new_pos, const Box3D::Axis movement, bool constrained) { + g_return_if_fail ((movement != Box3D::NONE) && (movement != Box3D::XYZ)); + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + /* update corners 0 and 7 according to which handle was moved and to the axes of movement */ + if (!(movement & Box3D::Z)) { + Persp3DImpl *persp_impl = box3d_get_perspective(box)->perspective_impl; + Proj::Pt3 pt_proj (persp_impl->tmat.preimage (new_pos, (id < 4) ? box->orig_corner0[Proj::Z] : + box->orig_corner7[Proj::Z], Proj::Z)); + if (constrained) { + pt_proj = box3d_snap (box, id, pt_proj, box3d_get_proj_corner (id, box->save_corner0, box->save_corner7)); + } + + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + box->orig_corner0 = Proj::Pt3 ((id & Box3D::X) ? box->save_corner0[Proj::X] : pt_proj[Proj::X], + (id & Box3D::Y) ? box->save_corner0[Proj::Y] : pt_proj[Proj::Y], + box->save_corner0[Proj::Z], + 1.0); + box->orig_corner7 = Proj::Pt3 ((id & Box3D::X) ? pt_proj[Proj::X] : box->save_corner7[Proj::X], + (id & Box3D::Y) ? pt_proj[Proj::Y] : box->save_corner7[Proj::Y], + box->save_corner7[Proj::Z], + 1.0); + } else { + Persp3D *persp = box3d_get_perspective(box); + Persp3DImpl *persp_impl = box3d_get_perspective(box)->perspective_impl; + Box3D::PerspectiveLine pl(persp_impl->tmat.image( + box3d_get_proj_corner (id, box->save_corner0, box->save_corner7)).affine(), + Proj::Z, persp); + Geom::Point new_pos_snapped(pl.closest_to(new_pos)); + Proj::Pt3 pt_proj (persp_impl->tmat.preimage (new_pos_snapped, + box3d_get_proj_corner (box, id)[(movement & Box3D::Y) ? Proj::X : Proj::Y], + (movement & Box3D::Y) ? Proj::X : Proj::Y)); + bool corner0_move_x = !(id & Box3D::X) && (movement & Box3D::X); + bool corner0_move_y = !(id & Box3D::Y) && (movement & Box3D::Y); + bool corner7_move_x = (id & Box3D::X) && (movement & Box3D::X); + bool corner7_move_y = (id & Box3D::Y) && (movement & Box3D::Y); + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + box->orig_corner0 = Proj::Pt3 (corner0_move_x ? pt_proj[Proj::X] : box->orig_corner0[Proj::X], + corner0_move_y ? pt_proj[Proj::Y] : box->orig_corner0[Proj::Y], + (id & Box3D::Z) ? box->orig_corner0[Proj::Z] : pt_proj[Proj::Z], + 1.0); + box->orig_corner7 = Proj::Pt3 (corner7_move_x ? pt_proj[Proj::X] : box->orig_corner7[Proj::X], + corner7_move_y ? pt_proj[Proj::Y] : box->orig_corner7[Proj::Y], + (id & Box3D::Z) ? pt_proj[Proj::Z] : box->orig_corner7[Proj::Z], + 1.0); + } + // FIXME: Should we update the box here? If so, how? +} + +void box3d_set_center (SPBox3D *box, Geom::Point const &new_pos, Geom::Point const &old_pos, const Box3D::Axis movement, bool constrained) { + g_return_if_fail ((movement != Box3D::NONE) && (movement != Box3D::XYZ)); + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + Persp3D *persp = box3d_get_perspective(box); + if (!(movement & Box3D::Z)) { + double coord = (box->orig_corner0[Proj::Z] + box->orig_corner7[Proj::Z]) / 2; + double radx = (box->orig_corner7[Proj::X] - box->orig_corner0[Proj::X]) / 2; + double rady = (box->orig_corner7[Proj::Y] - box->orig_corner0[Proj::Y]) / 2; + + Proj::Pt3 pt_proj (persp->perspective_impl->tmat.preimage (new_pos, coord, Proj::Z)); + if (constrained) { + Proj::Pt3 old_pos_proj (persp->perspective_impl->tmat.preimage (old_pos, coord, Proj::Z)); + old_pos_proj.normalize(); + pt_proj = box3d_snap (box, -1, pt_proj, old_pos_proj); + } + // normalizing pt_proj is essential because we want to mingle affine coordinates + pt_proj.normalize(); + box->orig_corner0 = Proj::Pt3 ((movement & Box3D::X) ? pt_proj[Proj::X] - radx : box->orig_corner0[Proj::X], + (movement & Box3D::Y) ? pt_proj[Proj::Y] - rady : box->orig_corner0[Proj::Y], + box->orig_corner0[Proj::Z], + 1.0); + box->orig_corner7 = Proj::Pt3 ((movement & Box3D::X) ? pt_proj[Proj::X] + radx : box->orig_corner7[Proj::X], + (movement & Box3D::Y) ? pt_proj[Proj::Y] + rady : box->orig_corner7[Proj::Y], + box->orig_corner7[Proj::Z], + 1.0); + } else { + double coord = (box->orig_corner0[Proj::X] + box->orig_corner7[Proj::X]) / 2; + double radz = (box->orig_corner7[Proj::Z] - box->orig_corner0[Proj::Z]) / 2; + + Box3D::PerspectiveLine pl(old_pos, Proj::Z, persp); + Geom::Point new_pos_snapped(pl.closest_to(new_pos)); + Proj::Pt3 pt_proj (persp->perspective_impl->tmat.preimage (new_pos_snapped, coord, Proj::X)); + + /* normalizing pt_proj is essential because we want to mingle affine coordinates */ + pt_proj.normalize(); + box->orig_corner0 = Proj::Pt3 (box->orig_corner0[Proj::X], + box->orig_corner0[Proj::Y], + pt_proj[Proj::Z] - radz, + 1.0); + box->orig_corner7 = Proj::Pt3 (box->orig_corner7[Proj::X], + box->orig_corner7[Proj::Y], + pt_proj[Proj::Z] + radz, + 1.0); + } +} + +/* + * Manipulates corner1 through corner4 to contain the indices of the corners + * from which the perspective lines in the direction of 'axis' emerge + */ +void box3d_corners_for_PLs (const SPBox3D * box, Proj::Axis axis, + Geom::Point &corner1, Geom::Point &corner2, Geom::Point &corner3, Geom::Point &corner4) +{ + Persp3D *persp = box3d_get_perspective(box); + g_return_if_fail (persp); + Persp3DImpl *persp_impl = persp->perspective_impl; + //box->orig_corner0.normalize(); + //box->orig_corner7.normalize(); + double coord = (box->orig_corner0[axis] > box->orig_corner7[axis]) ? + box->orig_corner0[axis] : + box->orig_corner7[axis]; + + Proj::Pt3 c1, c2, c3, c4; + // FIXME: This can certainly be done more elegantly/efficiently than by a case-by-case analysis. + switch (axis) { + case Proj::X: + c1 = Proj::Pt3 (coord, box->orig_corner0[Proj::Y], box->orig_corner0[Proj::Z], 1.0); + c2 = Proj::Pt3 (coord, box->orig_corner7[Proj::Y], box->orig_corner0[Proj::Z], 1.0); + c3 = Proj::Pt3 (coord, box->orig_corner7[Proj::Y], box->orig_corner7[Proj::Z], 1.0); + c4 = Proj::Pt3 (coord, box->orig_corner0[Proj::Y], box->orig_corner7[Proj::Z], 1.0); + break; + case Proj::Y: + c1 = Proj::Pt3 (box->orig_corner0[Proj::X], coord, box->orig_corner0[Proj::Z], 1.0); + c2 = Proj::Pt3 (box->orig_corner7[Proj::X], coord, box->orig_corner0[Proj::Z], 1.0); + c3 = Proj::Pt3 (box->orig_corner7[Proj::X], coord, box->orig_corner7[Proj::Z], 1.0); + c4 = Proj::Pt3 (box->orig_corner0[Proj::X], coord, box->orig_corner7[Proj::Z], 1.0); + break; + case Proj::Z: + c1 = Proj::Pt3 (box->orig_corner7[Proj::X], box->orig_corner7[Proj::Y], coord, 1.0); + c2 = Proj::Pt3 (box->orig_corner7[Proj::X], box->orig_corner0[Proj::Y], coord, 1.0); + c3 = Proj::Pt3 (box->orig_corner0[Proj::X], box->orig_corner0[Proj::Y], coord, 1.0); + c4 = Proj::Pt3 (box->orig_corner0[Proj::X], box->orig_corner7[Proj::Y], coord, 1.0); + break; + default: + return; + } + corner1 = persp_impl->tmat.image(c1).affine(); + corner2 = persp_impl->tmat.image(c2).affine(); + corner3 = persp_impl->tmat.image(c3).affine(); + corner4 = persp_impl->tmat.image(c4).affine(); +} + +/* Auxiliary function: Checks whether the half-line from A to B crosses the line segment joining C and D */ +static bool +box3d_half_line_crosses_joining_line (Geom::Point const &A, Geom::Point const &B, + Geom::Point const &C, Geom::Point const &D) { + Geom::Point n0 = (B - A).ccw(); + double d0 = dot(n0,A); + + Geom::Point n1 = (D - C).ccw(); + double d1 = dot(n1,C); + + Geom::Line lineAB(A,B); + Geom::Line lineCD(C,D); + + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(lineAB, lineCD); + } + catch (Geom::InfiniteSolutions& e) + { + // We're probably dealing with parallel lines, so they don't really cross + return false; + } + + if (!inters) { + return false; + } + + Geom::Point E = lineAB.pointAt((*inters).ta); // the point of intersection + + if ((dot(C,n0) < d0) == (dot(D,n0) < d0)) { + // C and D lie on the same side of the line AB + return false; + } + if ((dot(A,n1) < d1) != (dot(B,n1) < d1)) { + // A and B lie on different sides of the line CD + return true; + } else if (Geom::distance(E,A) < Geom::distance(E,B)) { + // The line CD passes on the "wrong" side of A + return false; + } + + // The line CD passes on the "correct" side of A + return true; +} + +static bool +box3d_XY_axes_are_swapped (SPBox3D *box) { + Persp3D *persp = box3d_get_perspective(box); + g_return_val_if_fail(persp, false); + Box3D::PerspectiveLine l1(box3d_get_corner_screen(box, 3, false), Proj::X, persp); + Box3D::PerspectiveLine l2(box3d_get_corner_screen(box, 3, false), Proj::Y, persp); + Geom::Point v1(l1.direction()); + Geom::Point v2(l2.direction()); + v1.normalize(); + v2.normalize(); + + return (v1[Geom::X]*v2[Geom::Y] - v1[Geom::Y]*v2[Geom::X] > 0); +} + +static inline void +box3d_aux_set_z_orders (int z_orders[6], int a, int b, int c, int d, int e, int f) { + // TODO add function argument: SPDocument *doc = box->document + auto doc = SP_ACTIVE_DOCUMENT; + + if (doc->is_yaxisdown()) { + std::swap(a, f); + std::swap(b, e); + std::swap(c, d); + } + + z_orders[0] = a; + z_orders[1] = b; + z_orders[2] = c; + z_orders[3] = d; + z_orders[4] = e; + z_orders[5] = f; +} + + +/* + * In standard perspective we have: + * 2 = front face + * 1 = top face + * 0 = left face + * 3 = right face + * 4 = bottom face + * 5 = rear face + */ + +/* All VPs infinite */ +static void +box3d_set_new_z_orders_case0 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis) { + bool swapped = box3d_XY_axes_are_swapped(box); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 4, 1, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 2, 4, 0); + } + break; + case Box3D::Y: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 4, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + break; + case Box3D::Z: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 1, 0, 2); + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 4, 3, 2); + } + break; + default: + g_assert_not_reached(); + break; + } +} + +/* Precisely one finite VP */ +static void +box3d_set_new_z_orders_case1 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis, Box3D::Axis fin_axis) { + Persp3D *persp = box3d_get_perspective(box); + Geom::Point vp(persp3d_get_VP(persp, Box3D::toProj(fin_axis)).affine()); + + // note: in some of the case distinctions below we rely upon the fact that oaxis1 and oaxis2 are ordered + Box3D::Axis oaxis1 = Box3D::get_remaining_axes(fin_axis).first; + Box3D::Axis oaxis2 = Box3D::get_remaining_axes(fin_axis).second; + int inside1 = 0; + int inside2 = 0; + inside1 = box3d_pt_lies_in_PL_sector (box, vp, 3, 3 ^ oaxis2, oaxis1); + inside2 = box3d_pt_lies_in_PL_sector (box, vp, 3, 3 ^ oaxis1, oaxis2); + + bool swapped = box3d_XY_axes_are_swapped(box); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 1, 0, 2, 4); + } + break; + case Box3D::Y: + if (inside2 > 0) { + box3d_aux_set_z_orders (z_orders, 1, 2, 3, 0, 5, 4); + } else if (inside2 < 0) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 4, 0, 5); + } else { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + } + break; + case Box3D::Z: + if (inside2) { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 1, 3, 0, 4, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 0, 1, 2); + } + } else if (inside1) { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 1, 0, 2); + } + } else { + // "regular" case + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 0, 1, 2, 5, 4, 3); + } else { + box3d_aux_set_z_orders (z_orders, 5, 3, 4, 0, 2, 1); + } + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 5, 0, 1); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 3, 2, 4); + } + break; + default: + g_assert_not_reached(); + } +} + +/* Precisely 2 finite VPs */ +static void +box3d_set_new_z_orders_case2 (SPBox3D *box, int z_orders[6], Box3D::Axis central_axis, Box3D::Axis /*infinite_axis*/) { + bool swapped = box3d_XY_axes_are_swapped(box); + + int insidexy = box3d_VP_lies_in_PL_sector (box, Proj::X, 3, 3 ^ Box3D::Z, Box3D::Y); + //int insidexz = box3d_VP_lies_in_PL_sector (box, Proj::X, 3, 3 ^ Box3D::Y, Box3D::Z); + + int insideyx = box3d_VP_lies_in_PL_sector (box, Proj::Y, 3, 3 ^ Box3D::Z, Box3D::X); + int insideyz = box3d_VP_lies_in_PL_sector (box, Proj::Y, 3, 3 ^ Box3D::X, Box3D::Z); + + //int insidezx = box3d_VP_lies_in_PL_sector (box, Proj::Z, 3, 3 ^ Box3D::Y, Box3D::X); + int insidezy = box3d_VP_lies_in_PL_sector (box, Proj::Z, 3, 3 ^ Box3D::X, Box3D::Y); + + switch(central_axis) { + case Box3D::X: + if (!swapped) { + if (insidezy == -1) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } else if (insidexy == 1) { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 5, 1, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 1, 3, 5); + } + } else { + if (insideyz == -1) { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 0, 2, 4); + } else { + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 2, 4, 0); + } else { + if (insidexy == 0) { + box3d_aux_set_z_orders (z_orders, 3, 5, 1, 0, 2, 4); + } else { + box3d_aux_set_z_orders (z_orders, 3, 1, 5, 0, 2, 4); + } + } + } + } + break; + case Box3D::Y: + if (!swapped) { + if (insideyz == 1) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 0, 5, 4); + } else { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } + } else { + if (insideyx == 1) { + box3d_aux_set_z_orders (z_orders, 4, 0, 5, 1, 3, 2); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 4, 1, 3, 2); + } + } + break; + case Box3D::Z: + if (!swapped) { + if (insidezy == 1) { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 4, 3, 5); + } else if (insidexy == -1) { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 5, 4, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 5, 3, 4); + } + } else { + box3d_aux_set_z_orders (z_orders, 3, 4, 5, 1, 0, 2); + } + break; + case Box3D::NONE: + if (!swapped) { + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + } else { + box3d_aux_set_z_orders (z_orders, 5, 0, 1, 4, 3, 2); + } + break; + default: + g_assert_not_reached(); + break; + } +} + +/* + * It can happen that during dragging the box is everted. + * In this case the opposite sides in this direction need to be swapped + */ +static Box3D::Axis +box3d_everted_directions (SPBox3D *box) { + Box3D::Axis ev = Box3D::NONE; + + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + if (box->orig_corner0[Proj::X] < box->orig_corner7[Proj::X]) + ev = (Box3D::Axis) (ev ^ Box3D::X); + if (box->orig_corner0[Proj::Y] < box->orig_corner7[Proj::Y]) + ev = (Box3D::Axis) (ev ^ Box3D::Y); + if (box->orig_corner0[Proj::Z] > box->orig_corner7[Proj::Z]) // FIXME: Remove the need to distinguish signs among the cases + ev = (Box3D::Axis) (ev ^ Box3D::Z); + + return ev; +} + +static void +box3d_swap_sides(int z_orders[6], Box3D::Axis axis) { + int pos1 = -1; + int pos2 = -1; + + for (int i = 0; i < 6; ++i) { + if (!(Box3D::int_to_face(z_orders[i]) & axis)) { + if (pos1 == -1) { + pos1 = i; + } else { + pos2 = i; + break; + } + } + } + + if ((pos1 != -1) && (pos2 != -1)){ + int tmp = z_orders[pos1]; + z_orders[pos1] = z_orders[pos2]; + z_orders[pos2] = tmp; + } +} + + +bool +box3d_recompute_z_orders (SPBox3D *box) { + Persp3D *persp = box3d_get_perspective(box); + + if (!persp) + return false; + + int z_orders[6]; + + Geom::Point c3(box3d_get_corner_screen(box, 3, false)); + + // determine directions from corner3 to the VPs + int num_finite = 0; + Box3D::Axis axis_finite = Box3D::NONE; + Box3D::Axis axis_infinite = Box3D::NONE; + Geom::Point dirs[3]; + for (int i = 0; i < 3; ++i) { + dirs[i] = persp3d_get_PL_dir_from_pt(persp, c3, Box3D::toProj(Box3D::axes[i])); + if (persp3d_VP_is_finite(persp->perspective_impl, Proj::axes[i])) { + num_finite++; + axis_finite = Box3D::axes[i]; + } else { + axis_infinite = Box3D::axes[i]; + } + } + + // determine the "central" axis (if there is one) + Box3D::Axis central_axis = Box3D::NONE; + if(Box3D::lies_in_sector(dirs[0], dirs[1], dirs[2])) { + central_axis = Box3D::Z; + } else if(Box3D::lies_in_sector(dirs[1], dirs[2], dirs[0])) { + central_axis = Box3D::X; + } else if(Box3D::lies_in_sector(dirs[2], dirs[0], dirs[1])) { + central_axis = Box3D::Y; + } + + switch (num_finite) { + case 0: + // TODO: Remark: In this case (and maybe one of the others, too) the z-orders for all boxes + // coincide, hence only need to be computed once in a more central location. + box3d_set_new_z_orders_case0(box, z_orders, central_axis); + break; + case 1: + box3d_set_new_z_orders_case1(box, z_orders, central_axis, axis_finite); + break; + case 2: + case 3: + box3d_set_new_z_orders_case2(box, z_orders, central_axis, axis_infinite); + break; + default: + /* + * For each VP F, check whether the half-line from the corner3 to F crosses the line segment + * joining the other two VPs. If this is the case, it determines the "central" corner from + * which the visible sides can be deduced. Otherwise, corner3 is the central corner. + */ + // FIXME: We should eliminate the use of Geom::Point altogether + Box3D::Axis central_axis = Box3D::NONE; + Geom::Point vp_x = persp3d_get_VP(persp, Proj::X).affine(); + Geom::Point vp_y = persp3d_get_VP(persp, Proj::Y).affine(); + Geom::Point vp_z = persp3d_get_VP(persp, Proj::Z).affine(); + Geom::Point vpx(vp_x[Geom::X], vp_x[Geom::Y]); + Geom::Point vpy(vp_y[Geom::X], vp_y[Geom::Y]); + Geom::Point vpz(vp_z[Geom::X], vp_z[Geom::Y]); + + Geom::Point c3 = box3d_get_corner_screen(box, 3, false); + Geom::Point corner3(c3[Geom::X], c3[Geom::Y]); + + if (box3d_half_line_crosses_joining_line (corner3, vpx, vpy, vpz)) { + central_axis = Box3D::X; + } else if (box3d_half_line_crosses_joining_line (corner3, vpy, vpz, vpx)) { + central_axis = Box3D::Y; + } else if (box3d_half_line_crosses_joining_line (corner3, vpz, vpx, vpy)) { + central_axis = Box3D::Z; + } + + // FIXME: At present, this is not used. Why is it calculated? + /* + unsigned int central_corner = 3 ^ central_axis; + if (central_axis == Box3D::Z) { + central_corner = central_corner ^ Box3D::XYZ; + } + if (box3d_XY_axes_are_swapped(box)) { + central_corner = central_corner ^ Box3D::XYZ; + } + */ + + Geom::Point c1(box3d_get_corner_screen(box, 1, false)); + Geom::Point c2(box3d_get_corner_screen(box, 2, false)); + Geom::Point c7(box3d_get_corner_screen(box, 7, false)); + + Geom::Point corner1(c1[Geom::X], c1[Geom::Y]); + Geom::Point corner2(c2[Geom::X], c2[Geom::Y]); + Geom::Point corner7(c7[Geom::X], c7[Geom::Y]); + // FIXME: At present we don't use the information about central_corner computed above. + switch (central_axis) { + case Box3D::Y: + if (!box3d_half_line_crosses_joining_line(vpz, vpy, corner3, corner2)) { + box3d_aux_set_z_orders (z_orders, 2, 3, 1, 5, 0, 4); + } else { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 3, 0, 5, 4); + } + break; + + case Box3D::Z: + if (box3d_half_line_crosses_joining_line(vpx, vpz, corner3, corner1)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 0, 1, 4, 3, 5); + } else if (box3d_half_line_crosses_joining_line(vpx, vpy, corner3, corner7)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 5, 3, 4); + } else { + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 3, 4, 5); + } + break; + + case Box3D::X: + if (box3d_half_line_crosses_joining_line(vpz, vpx, corner3, corner1)) { + // degenerate case + box3d_aux_set_z_orders (z_orders, 2, 1, 0, 4, 5, 3); + } else { + box3d_aux_set_z_orders (z_orders, 2, 4, 0, 5, 1, 3); + } + break; + + case Box3D::NONE: + box3d_aux_set_z_orders (z_orders, 2, 3, 4, 1, 0, 5); + break; + + default: + g_assert_not_reached(); + break; + } // end default case + } + + // TODO: If there are still errors in z-orders of everted boxes, we need to choose a variable corner + // instead of the hard-coded corner #3 in the computations above + Box3D::Axis ev = box3d_everted_directions(box); + for (auto & axe : Box3D::axes) { + if (ev & axe) { + box3d_swap_sides(z_orders, axe); + } + } + + // Check whether anything actually changed + for (int i = 0; i < 6; ++i) { + if (box->z_orders[i] != z_orders[i]) { + for (int j = i; j < 6; ++j) { + box->z_orders[j] = z_orders[j]; + } + return true; + } + } + return false; +} + +static std::map box3d_get_sides(SPBox3D *box) +{ + std::map sides; + for (auto& obj: box->children) { + Box3DSide *side = dynamic_cast(&obj); + if (side) { + sides[Box3D::face_to_int(side->getFaceId())] = side; + } + } + sides.erase(-1); + return sides; +} + + +// TODO: Check whether the box is everted in any direction and swap the sides opposite to this direction +void +box3d_set_z_orders (SPBox3D *box) { + // For efficiency reasons, we only set the new z-orders if something really changed + if (box3d_recompute_z_orders (box)) { + std::map sides = box3d_get_sides(box); + std::map::iterator side; + for (int z_order : box->z_orders) { + side = sides.find(z_order); + if (side != sides.end()) { + ((*side).second)->lowerToBottom(); + } + } + } +} + +/* + * Auxiliary function for z-order recomputing: + * Determines whether \a pt lies in the sector formed by the two PLs from the corners with IDs + * \a i21 and \a id2 to the VP in direction \a axis. If the VP is infinite, we say that \a pt + * lies in the sector if it lies between the two (parallel) PLs. + * \ret * 0 if \a pt doesn't lie in the sector + * * 1 if \a pt lies in the sector and either VP is finite of VP is infinite and the direction + * from the edge between the two corners to \a pt points towards the VP + * * -1 otherwise + */ +// TODO: Maybe it would be useful to have a similar method for projective points pt because then we +// can use it for VPs and perhaps merge the case distinctions during z-order recomputation. +int +box3d_pt_lies_in_PL_sector (SPBox3D const *box, Geom::Point const &pt, int id1, int id2, Box3D::Axis axis) { + Persp3D *persp = box3d_get_perspective(box); + + // the two corners + Geom::Point c1(box3d_get_corner_screen(box, id1, false)); + Geom::Point c2(box3d_get_corner_screen(box, id2, false)); + + int ret = 0; + if (persp3d_VP_is_finite(persp->perspective_impl, Box3D::toProj(axis))) { + Geom::Point vp(persp3d_get_VP(persp, Box3D::toProj(axis)).affine()); + Geom::Point v1(c1 - vp); + Geom::Point v2(c2 - vp); + Geom::Point w(pt - vp); + ret = static_cast(Box3D::lies_in_sector(v1, v2, w)); + } else { + Box3D::PerspectiveLine pl1(c1, Box3D::toProj(axis), persp); + Box3D::PerspectiveLine pl2(c2, Box3D::toProj(axis), persp); + if (pl1.lie_on_same_side(pt, c2) && pl2.lie_on_same_side(pt, c1)) { + // test whether pt lies "towards" or "away from" the VP + Box3D::Line edge(c1,c2); + Geom::Point c3(box3d_get_corner_screen(box, id1 ^ axis, false)); + if (edge.lie_on_same_side(pt, c3)) { + ret = 1; + } else { + ret = -1; + } + } + } + return ret; +} + +int +box3d_VP_lies_in_PL_sector (SPBox3D const *box, Proj::Axis vpdir, int id1, int id2, Box3D::Axis axis) { + Persp3D *persp = box3d_get_perspective(box); + + if (!persp3d_VP_is_finite(persp->perspective_impl, vpdir)) { + return 0; + } else { + return box3d_pt_lies_in_PL_sector(box, persp3d_get_VP(persp, vpdir).affine(), id1, id2, axis); + } +} + +/* swap the coordinates of corner0 and corner7 along the specified axis */ +static void +box3d_swap_coords(SPBox3D *box, Proj::Axis axis, bool smaller = true) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + if ((box->orig_corner0[axis] < box->orig_corner7[axis]) != smaller) { + double tmp = box->orig_corner0[axis]; + box->orig_corner0[axis] = box->orig_corner7[axis]; + box->orig_corner7[axis] = tmp; + } + // Should we also swap the coordinates of save_corner0 and save_corner7? +} + +/* ensure that the coordinates of corner0 and corner7 are in the correct order (to prevent everted boxes) */ +void +box3d_relabel_corners(SPBox3D *box) { + box3d_swap_coords(box, Proj::X, false); + box3d_swap_coords(box, Proj::Y, false); + box3d_swap_coords(box, Proj::Z, true); +} + +static void +box3d_check_for_swapped_coords(SPBox3D *box, Proj::Axis axis, bool smaller) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + if ((box->orig_corner0[axis] < box->orig_corner7[axis]) != smaller) { + box->swapped = (Box3D::Axis) (box->swapped | Proj::toAffine(axis)); + } else { + box->swapped = (Box3D::Axis) (box->swapped & ~Proj::toAffine(axis)); + } +} + +static void +box3d_exchange_coords(SPBox3D *box) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + + for (int i = 0; i < 3; ++i) { + if (box->swapped & Box3D::axes[i]) { + double tmp = box->orig_corner0[i]; + box->orig_corner0[i] = box->orig_corner7[i]; + box->orig_corner7[i] = tmp; + } + } +} + +void +box3d_check_for_swapped_coords(SPBox3D *box) { + box3d_check_for_swapped_coords(box, Proj::X, false); + box3d_check_for_swapped_coords(box, Proj::Y, false); + box3d_check_for_swapped_coords(box, Proj::Z, true); + + box3d_exchange_coords(box); +} + +static void box3d_extract_boxes_rec(SPObject *obj, std::list &boxes) { + SPBox3D *box = dynamic_cast(obj); + if (box) { + boxes.push_back(box); + } else if (dynamic_cast(obj)) { + for (auto& child: obj->children) { + box3d_extract_boxes_rec(&child, boxes); + } + } +} + +std::list +box3d_extract_boxes(SPObject *obj) { + std::list boxes; + box3d_extract_boxes_rec(obj, boxes); + return boxes; +} + +Persp3D * +box3d_get_perspective(SPBox3D const *box) { + return box->persp_ref->getObject(); +} + +void +box3d_switch_perspectives(SPBox3D *box, Persp3D *old_persp, Persp3D *new_persp, bool recompute_corners) { + if (recompute_corners) { + box->orig_corner0.normalize(); + box->orig_corner7.normalize(); + double z0 = box->orig_corner0[Proj::Z]; + double z7 = box->orig_corner7[Proj::Z]; + Geom::Point corner0_screen = box3d_get_corner_screen(box, 0, false); + Geom::Point corner7_screen = box3d_get_corner_screen(box, 7, false); + + box->orig_corner0 = new_persp->perspective_impl->tmat.preimage(corner0_screen, z0, Proj::Z); + box->orig_corner7 = new_persp->perspective_impl->tmat.preimage(corner7_screen, z7, Proj::Z); + } + + persp3d_remove_box (old_persp, box); + persp3d_add_box (new_persp, box); + + Glib::ustring href = "#"; + href += new_persp->getId(); + box->setAttribute("inkscape:perspectiveID", href); +} + +/* Converts the 3D box to an ordinary SPGroup, adds it to the XML tree at the same position as + the original box and deletes the latter */ +SPGroup *box3d_convert_to_group(SPBox3D *box) +{ + SPDocument *doc = box->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // remember position of the box + int pos = box->getPosition(); + + // remember important attributes + gchar const *id = box->getAttribute("id"); + gchar const *style = box->getAttribute("style"); + gchar const *mask = box->getAttribute("mask"); + gchar const *clip_path = box->getAttribute("clip-path"); + + // create a new group and add the sides (converted to ordinary paths) as its children + Inkscape::XML::Node *grepr = xml_doc->createElement("svg:g"); + + for (auto& obj: box->children) { + Box3DSide *side = dynamic_cast(&obj); + if (side) { + Inkscape::XML::Node *repr = box3d_side_convert_to_path(side); + grepr->appendChild(repr); + } else { + g_warning("Non-side item encountered as child of a 3D box."); + } + } + + // add the new group to the box's parent and set remembered position + SPObject *parent = box->parent; + parent->appendChild(grepr); + grepr->setPosition(pos); + grepr->setAttributeOrRemoveIfEmpty("style", style); + grepr->setAttributeOrRemoveIfEmpty("mask", mask); + grepr->setAttributeOrRemoveIfEmpty("clip-path", clip_path); + + box->deleteObject(true); + + grepr->setAttribute("id", id); + + SPGroup *group = dynamic_cast(doc->getObjectByRepr(grepr)); + g_assert(group != nullptr); + return group; +} + +const char *SPBox3D::displayName() const { + return _("3D Box"); +} + +gchar *SPBox3D::description() const { + // We could put more details about the 3d box here + return g_strdup(""); +} + +static inline void +box3d_push_back_corner_pair(SPBox3D const *box, std::list > &pts, int c1, int c2) { + pts.emplace_back(box3d_get_corner_screen(box, c1, false), + box3d_get_corner_screen(box, c2, false)); +} + +void SPBox3D::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!prefs->getBool("/tools/shapes/3dbox/convertguides", true)) { + this->convert_to_guides(); + return; + } + + std::list > pts; + + /* perspective lines in X direction */ + box3d_push_back_corner_pair(this, pts, 0, 1); + box3d_push_back_corner_pair(this, pts, 2, 3); + box3d_push_back_corner_pair(this, pts, 4, 5); + box3d_push_back_corner_pair(this, pts, 6, 7); + + /* perspective lines in Y direction */ + box3d_push_back_corner_pair(this, pts, 0, 2); + box3d_push_back_corner_pair(this, pts, 1, 3); + box3d_push_back_corner_pair(this, pts, 4, 6); + box3d_push_back_corner_pair(this, pts, 5, 7); + + /* perspective lines in Z direction */ + box3d_push_back_corner_pair(this, pts, 0, 4); + box3d_push_back_corner_pair(this, pts, 1, 5); + box3d_push_back_corner_pair(this, pts, 2, 6); + box3d_push_back_corner_pair(this, pts, 3, 7); + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/box3d.h b/src/object/box3d.h new file mode 100644 index 0000000..96f1b35 --- /dev/null +++ b/src/object/box3d.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_BOX3D_H +#define SEEN_SP_BOX3D_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Maximilian Albert + * Abhishek Sharma + * Jon A. Cruz box3d_extract_boxes(SPObject *obj); + +Persp3D *box3d_get_perspective(SPBox3D const *box); +void box3d_switch_perspectives(SPBox3D *box, Persp3D *old_persp, Persp3D *new_persp, bool recompute_corners = false); + +SPGroup *box3d_convert_to_group(SPBox3D *box); + + +#endif // SEEN_SP_BOX3D_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/color-profile.cpp b/src/object/color-profile.cpp new file mode 100644 index 0000000..cdfea4b --- /dev/null +++ b/src/object/color-profile.cpp @@ -0,0 +1,1334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#define noDEBUG_LCMS + +#include + +#include +#include +#include + +#ifdef DEBUG_LCMS +#include +#endif // DEBUG_LCMS + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#if HAVE_LIBLCMS2 +# include +#endif // HAVE_LIBLCMS2 + +#include "xml/repr.h" +#include "color.h" +#include "color-profile.h" +#include "cms-system.h" +#include "color-profile-cms-fns.h" +#include "attributes.h" +#include "inkscape.h" +#include "document.h" +#include "preferences.h" +#include +#include +#include "uri.h" + +#ifdef _WIN32 +#include +#endif // _WIN32 + +using Inkscape::ColorProfile; +using Inkscape::ColorProfileImpl; + +namespace +{ +#if defined(HAVE_LIBLCMS2) +cmsHPROFILE getSystemProfileHandle(); +cmsHPROFILE getProofProfileHandle(); +void loadProfiles(); +Glib::ustring getNameFromProfile(cmsHPROFILE profile); +#endif // defined(HAVE_LIBLCMS2) +} + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE_SCISLAC(key, ...) \ +{\ + Inkscape::Preferences *prefs = Inkscape::Preferences::get();\ + bool dump = prefs->getBool(Glib::ustring("/options/scislac/") + #key);\ + bool dumpD = prefs->getBool(Glib::ustring("/options/scislac/") + #key"D");\ + bool dumpD2 = prefs->getBool(Glib::ustring("/options/scislac/") + #key"D2");\ + dumpD &= ( (update_in_progress == 0) || dumpD2 );\ + if ( dump )\ + {\ + g_message( __VA_ARGS__ );\ +\ + }\ + if ( dumpD )\ + {\ + GtkWidget *dialog = gtk_message_dialog_new(NULL,\ + GTK_DIALOG_DESTROY_WITH_PARENT, \ + GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, \ + __VA_ARGS__ \ + );\ + g_signal_connect_swapped(dialog, "response",\ + G_CALLBACK(gtk_widget_destroy), \ + dialog); \ + gtk_widget_show_all( dialog );\ + }\ +} + + +#define DEBUG_MESSAGE(key, ...)\ +{\ + g_message( __VA_ARGS__ );\ +} + +#else +#define DEBUG_MESSAGE_SCISLAC(key, ...) +#define DEBUG_MESSAGE(key, ...) +#endif // DEBUG_LCMS + +namespace Inkscape { + +class ColorProfileImpl { +public: +#if defined(HAVE_LIBLCMS2) + static cmsHPROFILE _sRGBProf; + static cmsHPROFILE _NullProf; +#endif // defined(HAVE_LIBLCMS2) + + ColorProfileImpl(); + +#if defined(HAVE_LIBLCMS2) + static cmsUInt32Number _getInputFormat( cmsColorSpaceSignature space ); + + static cmsHPROFILE getNULLProfile(); + static cmsHPROFILE getSRGBProfile(); + + void _clearProfile(); + + cmsHPROFILE _profHandle; + cmsProfileClassSignature _profileClass; + cmsColorSpaceSignature _profileSpace; + cmsHTRANSFORM _transf; + cmsHTRANSFORM _revTransf; + cmsHTRANSFORM _gamutTransf; +#endif // defined(HAVE_LIBLCMS2) +}; + +#if defined(HAVE_LIBLCMS2) +cmsColorSpaceSignature asICColorSpaceSig(ColorSpaceSig const & sig) +{ + return ColorSpaceSigWrapper(sig); +} + +cmsProfileClassSignature asICColorProfileClassSig(ColorProfileClassSig const & sig) +{ + return ColorProfileClassSigWrapper(sig); +} +#endif // defined(HAVE_LIBLCMS2) + +} // namespace Inkscape + +ColorProfileImpl::ColorProfileImpl() +#if defined(HAVE_LIBLCMS2) + : + _profHandle(nullptr), + _profileClass(cmsSigInputClass), + _profileSpace(cmsSigRgbData), + _transf(nullptr), + _revTransf(nullptr), + _gamutTransf(nullptr) +#endif // defined(HAVE_LIBLCMS2) +{ +} + +#if defined(HAVE_LIBLCMS2) + +cmsHPROFILE ColorProfileImpl::_sRGBProf = nullptr; + +cmsHPROFILE ColorProfileImpl::getSRGBProfile() { + if ( !_sRGBProf ) { + _sRGBProf = cmsCreate_sRGBProfile(); + } + return ColorProfileImpl::_sRGBProf; +} + +cmsHPROFILE ColorProfileImpl::_NullProf = nullptr; + +cmsHPROFILE ColorProfileImpl::getNULLProfile() { + if ( !_NullProf ) { + _NullProf = cmsCreateNULLProfile(); + } + return _NullProf; +} + +#endif // defined(HAVE_LIBLCMS2) + +ColorProfile::FilePlusHome::FilePlusHome(Glib::ustring filename, bool isInHome) : filename(std::move(filename)), isInHome(isInHome) { +} + +ColorProfile::FilePlusHome::FilePlusHome(const ColorProfile::FilePlusHome &filePlusHome) : FilePlusHome(filePlusHome.filename, filePlusHome.isInHome) { +} + +bool ColorProfile::FilePlusHome::operator<(FilePlusHome const &other) const { + // if one is from home folder, other from global folder, sort home folder first. cf bug 1457126 + bool result; + if (this->isInHome != other.isInHome) result = this->isInHome; + else result = this->filename < other.filename; + return result; +} + +ColorProfile::FilePlusHomeAndName::FilePlusHomeAndName(ColorProfile::FilePlusHome filePlusHome, Glib::ustring name) + : FilePlusHome(filePlusHome), name(std::move(name)) { +} + +bool ColorProfile::FilePlusHomeAndName::operator<(ColorProfile::FilePlusHomeAndName const &other) const { + bool result; + if (this->isInHome != other.isInHome) result = this->isInHome; + else result = this->name < other.name; + return result; +} + + +ColorProfile::ColorProfile() : SPObject() { + this->impl = new ColorProfileImpl(); + + this->href = nullptr; + this->local = nullptr; + this->name = nullptr; + this->intentStr = nullptr; + this->rendering_intent = Inkscape::RENDERING_INTENT_UNKNOWN; +} + +ColorProfile::~ColorProfile() = default; + +bool ColorProfile::operator<(ColorProfile const &other) const { + gchar *a_name_casefold = g_utf8_casefold(this->name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(other.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; +} + +/** + * Callback: free object + */ +void ColorProfile::release() { + // Unregister ourselves + if ( this->document ) { + this->document->removeResource("iccprofile", this); + } + + if ( this->href ) { + g_free( this->href ); + this->href = nullptr; + } + + if ( this->local ) { + g_free( this->local ); + this->local = nullptr; + } + + if ( this->name ) { + g_free( this->name ); + this->name = nullptr; + } + + if ( this->intentStr ) { + g_free( this->intentStr ); + this->intentStr = nullptr; + } + +#if defined(HAVE_LIBLCMS2) + this->impl->_clearProfile(); +#endif // defined(HAVE_LIBLCMS2) + + delete this->impl; + this->impl = nullptr; +} + +#if defined(HAVE_LIBLCMS2) +void ColorProfileImpl::_clearProfile() +{ + _profileSpace = cmsSigRgbData; + + if ( _transf ) { + cmsDeleteTransform( _transf ); + _transf = nullptr; + } + if ( _revTransf ) { + cmsDeleteTransform( _revTransf ); + _revTransf = nullptr; + } + if ( _gamutTransf ) { + cmsDeleteTransform( _gamutTransf ); + _gamutTransf = nullptr; + } + if ( _profHandle ) { + cmsCloseProfile( _profHandle ); + _profHandle = nullptr; + } +} +#endif // defined(HAVE_LIBLCMS2) + +/** + * Callback: set attributes from associated repr. + */ +void ColorProfile::build(SPDocument *document, Inkscape::XML::Node *repr) { + g_assert(this->href == nullptr); + g_assert(this->local == nullptr); + g_assert(this->name == nullptr); + g_assert(this->intentStr == nullptr); + + SPObject::build(document, repr); + + this->readAttr( "xlink:href" ); + this->readAttr( "id" ); + this->readAttr( "local" ); + this->readAttr( "name" ); + this->readAttr( "rendering-intent" ); + + // Register + if ( document ) { + document->addResource( "iccprofile", this ); + } +} + + +/** + * Callback: set attribute. + */ +void ColorProfile::set(SPAttributeEnum key, gchar const *value) { + switch (key) { + case SP_ATTR_XLINK_HREF: + if ( this->href ) { + g_free( this->href ); + this->href = nullptr; + } + if ( value ) { + this->href = g_strdup( value ); + if ( *this->href ) { +#if defined(HAVE_LIBLCMS2) + + // TODO open filename and URIs properly + //FILE* fp = fopen_utf8name( filename, "r" ); + //LCMSAPI cmsHPROFILE LCMSEXPORT cmsOpenProfileFromMem(LPVOID MemPtr, cmsUInt32Number dwSize); + + // Try to open relative + SPDocument *doc = this->document; + if (!doc) { + doc = SP_ACTIVE_DOCUMENT; + g_warning("this has no document. using active"); + } + //# 1. Get complete URI of document + gchar const *docbase = doc->getDocumentURI(); + + Inkscape::URI docUri(""); + if (docbase) { // The file has already been saved + docUri = Inkscape::URI::from_native_filename(docbase); + } + + this->impl->_clearProfile(); + + try { + auto hrefUri = Inkscape::URI(this->href, docUri); + auto contents = hrefUri.getContents(); + this->impl->_profHandle = cmsOpenProfileFromMem(contents.data(), contents.size()); + } catch (...) { + g_warning("Failed to open CMS profile URI '%.100s'", this->href); + } + + if ( this->impl->_profHandle ) { + this->impl->_profileSpace = cmsGetColorSpace( this->impl->_profHandle ); + this->impl->_profileClass = cmsGetDeviceClass( this->impl->_profHandle ); + } + DEBUG_MESSAGE( lcmsOne, "cmsOpenProfileFromFile( '%s'...) = %p", fullname, (void*)this->impl->_profHandle ); +#endif // defined(HAVE_LIBLCMS2) + } + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_LOCAL: + if ( this->local ) { + g_free( this->local ); + this->local = nullptr; + } + this->local = g_strdup( value ); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_NAME: + if ( this->name ) { + g_free( this->name ); + this->name = nullptr; + } + this->name = g_strdup( value ); + DEBUG_MESSAGE( lcmsTwo, " name set to '%s'", this->name ); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_RENDERING_INTENT: + if ( this->intentStr ) { + g_free( this->intentStr ); + this->intentStr = nullptr; + } + this->intentStr = g_strdup( value ); + + if ( value ) { + if ( strcmp( value, "auto" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_AUTO; + } else if ( strcmp( value, "perceptual" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_PERCEPTUAL; + } else if ( strcmp( value, "relative-colorimetric" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_RELATIVE_COLORIMETRIC; + } else if ( strcmp( value, "saturation" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_SATURATION; + } else if ( strcmp( value, "absolute-colorimetric" ) == 0 ) { + this->rendering_intent = RENDERING_INTENT_ABSOLUTE_COLORIMETRIC; + } else { + this->rendering_intent = RENDERING_INTENT_UNKNOWN; + } + } else { + this->rendering_intent = RENDERING_INTENT_UNKNOWN; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPObject::set(key, value); + break; + } +} + +/** + * Callback: write attributes to associated repr. + */ +Inkscape::XML::Node* ColorProfile::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:color-profile"); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->href ) { + repr->setAttribute( "xlink:href", this->href ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->local ) { + repr->setAttribute( "local", this->local ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->name ) { + repr->setAttribute( "name", this->name ); + } + + if ( (flags & SP_OBJECT_WRITE_ALL) || this->intentStr ) { + repr->setAttribute( "rendering-intent", this->intentStr ); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +#if defined(HAVE_LIBLCMS2) + +struct MapMap { + cmsColorSpaceSignature space; + cmsUInt32Number inForm; +}; + +cmsUInt32Number ColorProfileImpl::_getInputFormat( cmsColorSpaceSignature space ) +{ + MapMap possible[] = { + {cmsSigXYZData, TYPE_XYZ_16}, + {cmsSigLabData, TYPE_Lab_16}, + //cmsSigLuvData + {cmsSigYCbCrData, TYPE_YCbCr_16}, + {cmsSigYxyData, TYPE_Yxy_16}, + {cmsSigRgbData, TYPE_RGB_16}, + {cmsSigGrayData, TYPE_GRAY_16}, + {cmsSigHsvData, TYPE_HSV_16}, + {cmsSigHlsData, TYPE_HLS_16}, + {cmsSigCmykData, TYPE_CMYK_16}, + {cmsSigCmyData, TYPE_CMY_16}, + }; + + int index = 0; + for ( guint i = 0; i < G_N_ELEMENTS(possible); i++ ) { + if ( possible[i].space == space ) { + index = i; + break; + } + } + + return possible[index].inForm; +} + +static int getLcmsIntent( guint svgIntent ) +{ + int intent = INTENT_PERCEPTUAL; + switch ( svgIntent ) { + case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: + intent = INTENT_RELATIVE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_SATURATION: + intent = INTENT_SATURATION; + break; + case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: + intent = INTENT_ABSOLUTE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_PERCEPTUAL: + case Inkscape::RENDERING_INTENT_UNKNOWN: + case Inkscape::RENDERING_INTENT_AUTO: + default: + intent = INTENT_PERCEPTUAL; + } + return intent; +} + +static SPObject* bruteFind( SPDocument* document, gchar const* name ) +{ + SPObject* result = nullptr; + std::vector current = document->getResourceList("iccprofile"); + for (std::vector::const_iterator it = current.begin(); (!result) && (it != current.end()); ++it) { + if ( IS_COLORPROFILE(*it) ) { + ColorProfile* prof = COLORPROFILE(*it); + if ( prof ) { + if ( prof->name && (strcmp(prof->name, name) == 0) ) { + result = SP_OBJECT(*it); + break; + } + } + } + } + + return result; +} + +cmsHPROFILE Inkscape::CMSSystem::getHandle( SPDocument* document, guint* intent, gchar const* name ) +{ + cmsHPROFILE prof = nullptr; + + SPObject* thing = bruteFind( document, name ); + if ( thing ) { + prof = COLORPROFILE(thing)->impl->_profHandle; + } + + if ( intent ) { + *intent = thing ? COLORPROFILE(thing)->rendering_intent : (guint)RENDERING_INTENT_UNKNOWN; + } + + DEBUG_MESSAGE( lcmsThree, " queried for profile of '%s'. Returning %p with intent of %d", name, prof, (intent? *intent:0) ); + + return prof; +} + +Inkscape::ColorSpaceSig ColorProfile::getColorSpace() const { + return ColorSpaceSigWrapper(impl->_profileSpace); +} + +Inkscape::ColorProfileClassSig ColorProfile::getProfileClass() const { + return ColorProfileClassSigWrapper(impl->_profileClass); +} + +cmsHTRANSFORM ColorProfile::getTransfToSRGB8() +{ + if ( !impl->_transf && impl->_profHandle ) { + int intent = getLcmsIntent(rendering_intent); + impl->_transf = cmsCreateTransform( impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, intent, 0 ); + } + return impl->_transf; +} + +cmsHTRANSFORM ColorProfile::getTransfFromSRGB8() +{ + if ( !impl->_revTransf && impl->_profHandle ) { + int intent = getLcmsIntent(rendering_intent); + impl->_revTransf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_RGBA_8, impl->_profHandle, ColorProfileImpl::_getInputFormat(impl->_profileSpace), intent, 0 ); + } + return impl->_revTransf; +} + +cmsHTRANSFORM ColorProfile::getTransfGamutCheck() +{ + if ( !impl->_gamutTransf ) { + impl->_gamutTransf = cmsCreateProofingTransform(ColorProfileImpl::getSRGBProfile(), + TYPE_BGRA_8, + ColorProfileImpl::getNULLProfile(), + TYPE_GRAY_8, + impl->_profHandle, + INTENT_RELATIVE_COLORIMETRIC, + INTENT_RELATIVE_COLORIMETRIC, + (cmsFLAGS_GAMUTCHECK | cmsFLAGS_SOFTPROOFING)); + } + return impl->_gamutTransf; +} + +bool ColorProfile::GamutCheck(SPColor color) +{ + guint32 val = color.toRGBA32(0); + +#if HAVE_LIBLCMS2 + cmsUInt16Number oldAlarmCodes[cmsMAXCHANNELS] = {0}; + cmsGetAlarmCodes(oldAlarmCodes); + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = ~0; + cmsSetAlarmCodes(newAlarmCodes); +#endif + + cmsUInt8Number outofgamut = 0; + guchar check_color[4] = { + static_cast(SP_RGBA32_R_U(val)), + static_cast(SP_RGBA32_G_U(val)), + static_cast(SP_RGBA32_B_U(val)), + 255}; + + cmsHTRANSFORM gamutCheck = ColorProfile::getTransfGamutCheck(); + if (gamutCheck) { + cmsDoTransform(gamutCheck, &check_color, &outofgamut, 1); + } + +#if HAVE_LIBLCMS2 + cmsSetAlarmCodes(oldAlarmCodes); +#endif + + return (outofgamut != 0); +} + +class ProfileInfo +{ +public: + ProfileInfo( cmsHPROFILE prof, Glib::ustring path ); + + Glib::ustring const& getName() {return _name;} + Glib::ustring const& getPath() {return _path;} + cmsColorSpaceSignature getSpace() {return _profileSpace;} + cmsProfileClassSignature getClass() {return _profileClass;} + +private: + Glib::ustring _path; + Glib::ustring _name; + cmsColorSpaceSignature _profileSpace; + cmsProfileClassSignature _profileClass; +}; + +ProfileInfo::ProfileInfo( cmsHPROFILE prof, Glib::ustring path ) : + _path(std::move( path )), + _name( getNameFromProfile(prof) ), + _profileSpace( cmsGetColorSpace( prof ) ), + _profileClass( cmsGetDeviceClass( prof ) ) +{ +} + + + +static std::vector knownProfiles; + +std::vector Inkscape::CMSSystem::getDisplayNames() +{ + loadProfiles(); + std::vector result; + + for (auto & knownProfile : knownProfiles) { + if ( knownProfile.getClass() == cmsSigDisplayClass && knownProfile.getSpace() == cmsSigRgbData ) { + result.push_back( knownProfile.getName() ); + } + } + std::sort(result.begin(), result.end()); + + return result; +} + +std::vector Inkscape::CMSSystem::getSoftproofNames() +{ + loadProfiles(); + std::vector result; + + for (auto & knownProfile : knownProfiles) { + if ( knownProfile.getClass() == cmsSigOutputClass ) { + result.push_back( knownProfile.getName() ); + } + } + std::sort(result.begin(), result.end()); + + return result; +} + +Glib::ustring Inkscape::CMSSystem::getPathForProfile(Glib::ustring const& name) +{ + loadProfiles(); + Glib::ustring result; + + for (auto & knownProfile : knownProfiles) { + if ( name == knownProfile.getName() ) { + result = knownProfile.getPath(); + break; + } + } + + return result; +} + +void Inkscape::CMSSystem::doTransform(cmsHTRANSFORM transform, void *inBuf, void *outBuf, unsigned int size) +{ + cmsDoTransform(transform, inBuf, outBuf, size); +} + +bool Inkscape::CMSSystem::isPrintColorSpace(ColorProfile const *profile) +{ + bool isPrint = false; + if ( profile ) { + ColorSpaceSigWrapper colorspace = profile->getColorSpace(); + isPrint = (colorspace == cmsSigCmykData) || (colorspace == cmsSigCmyData); + } + return isPrint; +} + +gint Inkscape::CMSSystem::getChannelCount(ColorProfile const *profile) +{ + gint count = 0; + if ( profile ) { +#if HAVE_LIBLCMS2 + count = cmsChannelsOf( asICColorSpaceSig(profile->getColorSpace()) ); +#endif + } + return count; +} + +#endif // defined(HAVE_LIBLCMS2) + +// the bool return value tells if it's a user's directory or a system location +// note that this will treat places under $HOME as system directories when they are found via $XDG_DATA_DIRS +std::set ColorProfile::getBaseProfileDirs() { +#if defined(HAVE_LIBLCMS2) + static bool warnSet = false; + if (!warnSet) { + warnSet = true; + } +#endif // defined(HAVE_LIBLCMS2) + std::set sources; + + // first try user's local dir + gchar* path = g_build_filename(g_get_user_data_dir(), "color", "icc", NULL); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + // search colord ICC store paths + // (see https://github.com/hughsie/colord/blob/fe10f76536bb27614ced04e0ff944dc6fb4625c0/lib/colord/cd-icc-store.c#L590) + + // user store + path = g_build_filename(g_get_user_data_dir(), "icc", NULL); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + path = g_build_filename(g_get_home_dir(), ".color", "icc", NULL); + sources.insert(FilePlusHome(path, true)); + g_free(path); + + // machine store + sources.insert(FilePlusHome("/var/lib/color/icc", false)); + sources.insert(FilePlusHome("/var/lib/colord/icc", false)); + + const gchar* const * dataDirs = g_get_system_data_dirs(); + for ( int i = 0; dataDirs[i]; i++ ) { + gchar* path = g_build_filename(dataDirs[i], "color", "icc", NULL); + sources.insert(FilePlusHome(path, false)); + g_free(path); + } + + // On OS X: + { + sources.insert(FilePlusHome("/System/Library/ColorSync/Profiles", false)); + sources.insert(FilePlusHome("/Library/ColorSync/Profiles", false)); + + gchar *path = g_build_filename(g_get_home_dir(), "Library", "ColorSync", "Profiles", NULL); + sources.insert(FilePlusHome(path, true)); + g_free(path); + } + +#ifdef _WIN32 + wchar_t pathBuf[MAX_PATH + 1]; + pathBuf[0] = 0; + DWORD pathSize = sizeof(pathBuf); + g_assert(sizeof(wchar_t) == sizeof(gunichar2)); + if ( GetColorDirectoryW( NULL, pathBuf, &pathSize ) ) { + gchar * utf8Path = g_utf16_to_utf8( (gunichar2*)(&pathBuf[0]), -1, NULL, NULL, NULL ); + if ( !g_utf8_validate(utf8Path, -1, NULL) ) { + g_warning( "GetColorDirectoryW() resulted in invalid UTF-8" ); + } else { + sources.insert(FilePlusHome(utf8Path, false)); + } + g_free( utf8Path ); + } +#endif // _WIN32 + + return sources; +} + +static bool isIccFile( gchar const *filepath ) +{ + bool isIccFile = false; + GStatBuf st; + if ( g_stat(filepath, &st) == 0 && (st.st_size > 128) ) { + //0-3 == size + //36-39 == 'acsp' 0x61637370 + int fd = g_open( filepath, O_RDONLY, S_IRWXU); + if ( fd != -1 ) { + guchar scratch[40] = {0}; + size_t len = sizeof(scratch); + + //size_t left = 40; + ssize_t got = read(fd, scratch, len); + if ( got != -1 ) { + size_t calcSize = (scratch[0] << 24) | (scratch[1] << 16) | (scratch[2] << 8) | scratch[3]; + if ( calcSize > 128 && calcSize <= static_cast(st.st_size) ) { + isIccFile = (scratch[36] == 'a') && (scratch[37] == 'c') && (scratch[38] == 's') && (scratch[39] == 'p'); + } + } + + close(fd); +#if defined(HAVE_LIBLCMS2) + if (isIccFile) { + cmsHPROFILE prof = cmsOpenProfileFromFile( filepath, "r" ); + if ( prof ) { + cmsProfileClassSignature profClass = cmsGetDeviceClass(prof); + if ( profClass == cmsSigNamedColorClass ) { + isIccFile = false; // Ignore named color profiles for now. + } + cmsCloseProfile( prof ); + } + } +#endif // defined(HAVE_LIBLCMS2) + } + } + return isIccFile; +} + +std::set ColorProfile::getProfileFiles() +{ + std::set files; + using Inkscape::IO::Resource::get_filenames; + + for (auto &path: ColorProfile::getBaseProfileDirs()) { + for(auto &filename: get_filenames(path.filename, {".icc", ".icm"})) { + if ( isIccFile(filename.c_str()) ) { + files.insert(FilePlusHome(filename, path.isInHome)); + } + } + } + + return files; +} + +std::set ColorProfile::getProfileFilesWithNames() +{ + std::set result; + +#if defined(HAVE_LIBLCMS2) + for (auto &profile: getProfileFiles()) { + cmsHPROFILE hProfile = cmsOpenProfileFromFile(profile.filename.c_str(), "r"); + if ( hProfile ) { + Glib::ustring name = getNameFromProfile(hProfile); + result.insert( FilePlusHomeAndName(profile, name) ); + cmsCloseProfile(hProfile); + } + } +#endif // defined(HAVE_LIBLCMS2) + + return result; +} + +#if defined(HAVE_LIBLCMS2) +#if HAVE_LIBLCMS2 +void errorHandlerCB(cmsContext /*contextID*/, cmsUInt32Number errorCode, char const *errorText) +{ + g_message("lcms: Error %d", errorCode); + g_message(" %p", errorText); + //g_message("lcms: Error %d; %s", errorCode, errorText); +} +#endif + +namespace +{ + +Glib::ustring getNameFromProfile(cmsHPROFILE profile) +{ + Glib::ustring nameStr; + if ( profile ) { +#if HAVE_LIBLCMS2 + cmsUInt32Number byteLen = cmsGetProfileInfo(profile, cmsInfoDescription, "en", "US", nullptr, 0); + if (byteLen > 0) { + // TODO investigate wchar_t and cmsGetProfileInfo() + std::vector data(byteLen); + cmsUInt32Number readLen = cmsGetProfileInfoASCII(profile, cmsInfoDescription, + "en", "US", + data.data(), data.size()); + if (readLen < data.size()) { + data.resize(readLen); + } + nameStr = Glib::ustring(data.begin(), data.end()); + } + if (nameStr.empty() || !g_utf8_validate(nameStr.c_str(), -1, nullptr)) { + nameStr = _("(invalid UTF-8 string)"); + } +#endif + } + return nameStr; +} + +/** + * This function loads or refreshes data in knownProfiles. + * Call it at the start of every call that requires this data. + */ +void loadProfiles() +{ + static bool error_handler_set = false; + if (!error_handler_set) { +#if HAVE_LIBLCMS2 + //cmsSetLogErrorHandler(errorHandlerCB); + //g_message("LCMS error handler set"); +#endif + error_handler_set = true; + } + + static bool profiles_searched = false; + if ( !profiles_searched ) { + knownProfiles.clear(); + + for (auto &profile: ColorProfile::getProfileFiles()) { + cmsHPROFILE prof = cmsOpenProfileFromFile( profile.filename.c_str(), "r" ); + if ( prof ) { + ProfileInfo info( prof, Glib::filename_to_utf8( profile.filename.c_str() ) ); + cmsCloseProfile( prof ); + prof = nullptr; + + bool sameName = false; + for(auto &knownProfile: knownProfiles) { + if ( knownProfile.getName() == info.getName() ) { + sameName = true; + break; + } + } + + if ( !sameName ) { + knownProfiles.push_back(info); + } + } + } + profiles_searched = true; + } +} +} // namespace + +static bool gamutWarn = false; + +static Gdk::RGBA lastGamutColor("#808080"); + +static bool lastBPC = false; +#if defined(cmsFLAGS_PRESERVEBLACK) +static bool lastPreserveBlack = false; +#endif // defined(cmsFLAGS_PRESERVEBLACK) +static int lastIntent = INTENT_PERCEPTUAL; +static int lastProofIntent = INTENT_PERCEPTUAL; +static cmsHTRANSFORM transf = nullptr; + +namespace { +cmsHPROFILE getSystemProfileHandle() +{ + static cmsHPROFILE theOne = nullptr; + static Glib::ustring lastURI; + + loadProfiles(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring uri = prefs->getString("/options/displayprofile/uri"); + + if ( !uri.empty() ) { + if ( uri != lastURI ) { + lastURI.clear(); + if ( theOne ) { + cmsCloseProfile( theOne ); + } + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + theOne = cmsOpenProfileFromFile( uri.data(), "r" ); + if ( theOne ) { + // a display profile must have the proper stuff + cmsColorSpaceSignature space = cmsGetColorSpace(theOne); + cmsProfileClassSignature profClass = cmsGetDeviceClass(theOne); + + if ( profClass != cmsSigDisplayClass ) { + g_warning("Not a display profile"); + cmsCloseProfile( theOne ); + theOne = nullptr; + } else if ( space != cmsSigRgbData ) { + g_warning("Not an RGB profile"); + cmsCloseProfile( theOne ); + theOne = nullptr; + } else { + lastURI = uri; + } + } + } + } else if ( theOne ) { + cmsCloseProfile( theOne ); + theOne = nullptr; + lastURI.clear(); + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + } + + return theOne; +} + + +cmsHPROFILE getProofProfileHandle() +{ + static cmsHPROFILE theOne = nullptr; + static Glib::ustring lastURI; + + loadProfiles(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool which = prefs->getBool( "/options/softproof/enable"); + Glib::ustring uri = prefs->getString("/options/softproof/uri"); + + if ( which && !uri.empty() ) { + if ( lastURI != uri ) { + lastURI.clear(); + if ( theOne ) { + cmsCloseProfile( theOne ); + } + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + theOne = cmsOpenProfileFromFile( uri.data(), "r" ); + if ( theOne ) { + // a display profile must have the proper stuff + cmsColorSpaceSignature space = cmsGetColorSpace(theOne); + cmsProfileClassSignature profClass = cmsGetDeviceClass(theOne); + + (void)space; + (void)profClass; +/* + if ( profClass != cmsSigDisplayClass ) { + g_warning("Not a display profile"); + cmsCloseProfile( theOne ); + theOne = 0; + } else if ( space != cmsSigRgbData ) { + g_warning("Not an RGB profile"); + cmsCloseProfile( theOne ); + theOne = 0; + } else { +*/ + lastURI = uri; +/* + } +*/ + } + } + } else if ( theOne ) { + cmsCloseProfile( theOne ); + theOne = nullptr; + lastURI.clear(); + if ( transf ) { + cmsDeleteTransform( transf ); + transf = nullptr; + } + } + + return theOne; +} +} // namespace + +static void free_transforms(); + +cmsHTRANSFORM Inkscape::CMSSystem::getDisplayTransform() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + if ( transf ) { + cmsDeleteTransform(transf); + transf = nullptr; + } + return nullptr; + } + + bool warn = prefs->getBool( "/options/softproof/gamutwarn"); + int intent = prefs->getIntLimited( "/options/displayprofile/intent", 0, 0, 3 ); + int proofIntent = prefs->getIntLimited( "/options/softproof/intent", 0, 0, 3 ); + bool bpc = prefs->getBool( "/options/softproof/bpc"); +#if defined(cmsFLAGS_PRESERVEBLACK) + bool preserveBlack = prefs->getBool( "/options/softproof/preserveblack"); +#endif //defined(cmsFLAGS_PRESERVEBLACK) + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + Gdk::RGBA gamutColor( colorStr.empty() ? "#808080" : colorStr ); + + if ( (warn != gamutWarn) + || (lastIntent != intent) + || (lastProofIntent != proofIntent) + || (bpc != lastBPC) +#if defined(cmsFLAGS_PRESERVEBLACK) + || (preserveBlack != lastPreserveBlack) +#endif // defined(cmsFLAGS_PRESERVEBLACK) + || (gamutColor != lastGamutColor) + ) { + gamutWarn = warn; + free_transforms(); + lastIntent = intent; + lastProofIntent = proofIntent; + lastBPC = bpc; +#if defined(cmsFLAGS_PRESERVEBLACK) + lastPreserveBlack = preserveBlack; +#endif // defined(cmsFLAGS_PRESERVEBLACK) + lastGamutColor = gamutColor; + } + + // Fetch these now, as they might clear the transform as a side effect. + cmsHPROFILE hprof = getSystemProfileHandle(); + cmsHPROFILE proofProf = hprof ? getProofProfileHandle() : nullptr; + + if ( !transf ) { + if ( hprof && proofProf ) { + cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; + if ( gamutWarn ) { + dwFlags |= cmsFLAGS_GAMUTCHECK; + + auto gamutColor_r = gamutColor.get_red_u(); + auto gamutColor_g = gamutColor.get_green_u(); + auto gamutColor_b = gamutColor.get_blue_u(); + +#if HAVE_LIBLCMS2 + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = gamutColor_r; + newAlarmCodes[1] = gamutColor_g; + newAlarmCodes[2] = gamutColor_b; + newAlarmCodes[3] = ~0; + cmsSetAlarmCodes(newAlarmCodes); +#endif + } + if ( bpc ) { + dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + } +#if defined(cmsFLAGS_PRESERVEBLACK) + if ( preserveBlack ) { + dwFlags |= cmsFLAGS_PRESERVEBLACK; + } +#endif // defined(cmsFLAGS_PRESERVEBLACK) + transf = cmsCreateProofingTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, hprof, TYPE_BGRA_8, proofProf, intent, proofIntent, dwFlags ); + } else if ( hprof ) { + transf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, hprof, TYPE_BGRA_8, intent, 0 ); + } + } + + return transf; +} + + +class MemProfile { +public: + MemProfile(); + ~MemProfile(); + + std::string id; + cmsHPROFILE hprof; + cmsHTRANSFORM transf; +}; + +MemProfile::MemProfile() : + id(), + hprof(nullptr), + transf(nullptr) +{ +} + +MemProfile::~MemProfile() += default; + +static std::vector perMonitorProfiles; + +void free_transforms() +{ + if ( transf ) { + cmsDeleteTransform(transf); + transf = nullptr; + } + + for ( auto profile : perMonitorProfiles ) { + if ( profile.transf ) { + cmsDeleteTransform(profile.transf); + profile.transf = nullptr; + } + } +} + +Glib::ustring Inkscape::CMSSystem::getDisplayId( int monitor ) +{ + Glib::ustring id; + + if ( monitor >= 0 && monitor < static_cast(perMonitorProfiles.size()) ) { + MemProfile& item = perMonitorProfiles[monitor]; + id = item.id; + } + + return id; +} + +Glib::ustring Inkscape::CMSSystem::setDisplayPer( gpointer buf, guint bufLen, int monitor ) +{ + while ( static_cast(perMonitorProfiles.size()) <= monitor ) { + MemProfile tmp; + perMonitorProfiles.push_back(tmp); + } + MemProfile& item = perMonitorProfiles[monitor]; + + if ( item.hprof ) { + cmsCloseProfile( item.hprof ); + item.hprof = nullptr; + } + + Glib::ustring id; + + if ( buf && bufLen ) { + gsize len = bufLen; // len is an inout parameter + id = Glib::Checksum::compute_checksum(Glib::Checksum::CHECKSUM_MD5, + reinterpret_cast(buf), len); + + // Note: if this is not a valid profile, item.hprof will be set to null. + item.hprof = cmsOpenProfileFromMem(buf, bufLen); + } + item.id = id; + + return id; +} + +cmsHTRANSFORM Inkscape::CMSSystem::getDisplayPer( Glib::ustring const& id ) +{ + cmsHTRANSFORM result = nullptr; + if ( id.empty() ) { + return nullptr; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool found = false; + + for ( auto it2 = perMonitorProfiles.begin(); it2 != perMonitorProfiles.end() && !found; ++it2 ) { + if ( id == it2->id ) { + MemProfile& item = *it2; + + bool warn = prefs->getBool( "/options/softproof/gamutwarn"); + int intent = prefs->getIntLimited( "/options/displayprofile/intent", 0, 0, 3 ); + int proofIntent = prefs->getIntLimited( "/options/softproof/intent", 0, 0, 3 ); + bool bpc = prefs->getBool( "/options/softproof/bpc"); +#if defined(cmsFLAGS_PRESERVEBLACK) + bool preserveBlack = prefs->getBool( "/options/softproof/preserveblack"); +#endif //defined(cmsFLAGS_PRESERVEBLACK) + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + Gdk::RGBA gamutColor( colorStr.empty() ? "#808080" : colorStr ); + + if ( (warn != gamutWarn) + || (lastIntent != intent) + || (lastProofIntent != proofIntent) + || (bpc != lastBPC) +#if defined(cmsFLAGS_PRESERVEBLACK) + || (preserveBlack != lastPreserveBlack) +#endif // defined(cmsFLAGS_PRESERVEBLACK) + || (gamutColor != lastGamutColor) + ) { + gamutWarn = warn; + free_transforms(); + lastIntent = intent; + lastProofIntent = proofIntent; + lastBPC = bpc; +#if defined(cmsFLAGS_PRESERVEBLACK) + lastPreserveBlack = preserveBlack; +#endif // defined(cmsFLAGS_PRESERVEBLACK) + lastGamutColor = gamutColor; + } + + // Fetch these now, as they might clear the transform as a side effect. + cmsHPROFILE proofProf = item.hprof ? getProofProfileHandle() : nullptr; + + if ( !item.transf ) { + if ( item.hprof && proofProf ) { + cmsUInt32Number dwFlags = cmsFLAGS_SOFTPROOFING; + if ( gamutWarn ) { + dwFlags |= cmsFLAGS_GAMUTCHECK; + auto gamutColor_r = gamutColor.get_red_u(); + auto gamutColor_g = gamutColor.get_green_u(); + auto gamutColor_b = gamutColor.get_blue_u(); + +#if HAVE_LIBLCMS2 + cmsUInt16Number newAlarmCodes[cmsMAXCHANNELS] = {0}; + newAlarmCodes[0] = gamutColor_r; + newAlarmCodes[1] = gamutColor_g; + newAlarmCodes[2] = gamutColor_b; + newAlarmCodes[3] = ~0; + cmsSetAlarmCodes(newAlarmCodes); +#endif + } + if ( bpc ) { + dwFlags |= cmsFLAGS_BLACKPOINTCOMPENSATION; + } +#if defined(cmsFLAGS_PRESERVEBLACK) + if ( preserveBlack ) { + dwFlags |= cmsFLAGS_PRESERVEBLACK; + } +#endif // defined(cmsFLAGS_PRESERVEBLACK) + item.transf = cmsCreateProofingTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, item.hprof, TYPE_BGRA_8, proofProf, intent, proofIntent, dwFlags ); + } else if ( item.hprof ) { + item.transf = cmsCreateTransform( ColorProfileImpl::getSRGBProfile(), TYPE_BGRA_8, item.hprof, TYPE_BGRA_8, intent, 0 ); + } + } + + result = item.transf; + found = true; + } + } + + return result; +} + + + +#endif // defined(HAVE_LIBLCMS2) + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/color-profile.h b/src/object/color-profile.h new file mode 100644 index 0000000..5120b92 --- /dev/null +++ b/src/object/color-profile.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_COLOR_PROFILE_H +#define SEEN_COLOR_PROFILE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include +#include + +#include +#include "cms-color-types.h" + +#include "sp-object.h" + +struct SPColor; + +namespace Inkscape { + +enum { + RENDERING_INTENT_UNKNOWN = 0, + RENDERING_INTENT_AUTO = 1, + RENDERING_INTENT_PERCEPTUAL = 2, + RENDERING_INTENT_RELATIVE_COLORIMETRIC = 3, + RENDERING_INTENT_SATURATION = 4, + RENDERING_INTENT_ABSOLUTE_COLORIMETRIC = 5 +}; + +class ColorProfileImpl; + + +/** + * Color Profile. + */ +class ColorProfile : public SPObject { +public: + ColorProfile(); + ~ColorProfile() override; + + bool operator<(ColorProfile const &other) const; + + // we use std::set with pointers to ColorProfile, just having operator< isn't enough to sort these + struct pointerComparator { + bool operator()(const ColorProfile * const & a, const ColorProfile * const & b) { return (*a) < (*b); }; + }; + + friend cmsHPROFILE colorprofile_get_handle( SPDocument*, unsigned int*, char const* ); + friend class CMSSystem; + + class FilePlusHome { + public: + FilePlusHome(Glib::ustring filename, bool isInHome); + FilePlusHome(const FilePlusHome &filePlusHome); + bool operator<(FilePlusHome const &other) const; + Glib::ustring filename; + bool isInHome; + }; + class FilePlusHomeAndName: public FilePlusHome { + public: + FilePlusHomeAndName(FilePlusHome filePlusHome, Glib::ustring name); + bool operator<(FilePlusHomeAndName const &other) const; + Glib::ustring name; + }; + + static std::set getBaseProfileDirs(); + static std::set getProfileFiles(); + static std::set getProfileFilesWithNames(); +#if defined(HAVE_LIBLCMS2) + //icColorSpaceSignature getColorSpace() const; + ColorSpaceSig getColorSpace() const; + //icProfileClassSignature getProfileClass() const; + ColorProfileClassSig getProfileClass() const; + cmsHTRANSFORM getTransfToSRGB8(); + cmsHTRANSFORM getTransfFromSRGB8(); + cmsHTRANSFORM getTransfGamutCheck(); + bool GamutCheck(SPColor color); + +#endif // defined(HAVE_LIBLCMS2) + + char* href; + char* local; + char* name; + char* intentStr; + unsigned int rendering_intent; // FIXME: type the enum and hold that instead + +protected: + ColorProfileImpl *impl; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +} // namespace Inkscape + +//#define COLORPROFILE_TYPE (Inkscape::colorprofile_get_type()) +#define COLORPROFILE(obj) ((Inkscape::ColorProfile*)obj) +#define IS_COLORPROFILE(obj) (dynamic_cast((SPObject*)obj)) + +#endif // !SEEN_COLOR_PROFILE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/CMakeLists.txt b/src/object/filters/CMakeLists.txt new file mode 100644 index 0000000..14ea76b --- /dev/null +++ b/src/object/filters/CMakeLists.txt @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(filters_SRC + sp-filter-primitive.cpp + blend.cpp + colormatrix.cpp + componenttransfer-funcnode.cpp + componenttransfer.cpp + composite.cpp + convolvematrix.cpp + diffuselighting.cpp + displacementmap.cpp + distantlight.cpp + flood.cpp + gaussian-blur.cpp + image.cpp + merge.cpp + mergenode.cpp + morphology.cpp + offset.cpp + pointlight.cpp + specularlighting.cpp + spotlight.cpp + tile.cpp + turbulence.cpp + + + # ------- + # Headers + sp-filter-primitive.h + blend.h + colormatrix.h + componenttransfer-funcnode.h + componenttransfer.h + composite.h + convolvematrix.h + diffuselighting.h + displacementmap.h + distantlight.h + flood.h + gaussian-blur.h + image.h + merge.h + mergenode.h + morphology.h + offset.h + pointlight.h + specularlighting.h + spotlight.h + tile.h + turbulence.h +) + +add_inkscape_source("${filters_SRC}") diff --git a/src/object/filters/blend.cpp b/src/object/filters/blend.cpp new file mode 100644 index 0000000..2d0c053 --- /dev/null +++ b/src/object/filters/blend.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Abhishek Sharma + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "blend.h" + +#include "attributes.h" + +#include "display/nr-filter.h" + +#include "object/sp-filter.h" + +#include "xml/repr.h" + + +SPFeBlend::SPFeBlend() + : SPFilterPrimitive(), blend_mode(SP_CSS_BLEND_NORMAL), + in2(Inkscape::Filters::NR_FILTER_SLOT_NOT_SET) +{ +} + +SPFeBlend::~SPFeBlend() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeBlend variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeBlend::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "mode" ); + this->readAttr( "in2" ); + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + repr->setAttribute("in2", parent->name_for_image(this->in2)); + } +} + +/** + * Drops any allocated memory. + */ +void SPFeBlend::release() { + SPFilterPrimitive::release(); +} + +static SPBlendMode sp_feBlend_readmode(gchar const *value) { + if (!value) { + return SP_CSS_BLEND_NORMAL; + } + + switch (value[0]) { + case 'n': + if (strncmp(value, "normal", 6) == 0) + return SP_CSS_BLEND_NORMAL; + break; + case 'm': + if (strncmp(value, "multiply", 8) == 0) + return SP_CSS_BLEND_MULTIPLY; + break; + case 's': + if (strncmp(value, "screen", 6) == 0) + return SP_CSS_BLEND_SCREEN; + if (strncmp(value, "saturation", 10) == 0) + return SP_CSS_BLEND_SATURATION; + break; + case 'd': + if (strncmp(value, "darken", 6) == 0) + return SP_CSS_BLEND_DARKEN; + if (strncmp(value, "difference", 10) == 0) + return SP_CSS_BLEND_DIFFERENCE; + break; + case 'l': + if (strncmp(value, "lighten", 7) == 0) + return SP_CSS_BLEND_LIGHTEN; + if (strncmp(value, "luminosity", 10) == 0) + return SP_CSS_BLEND_LUMINOSITY; + break; + case 'o': + if (strncmp(value, "overlay", 7) == 0) + return SP_CSS_BLEND_OVERLAY; + break; + case 'c': + if (strncmp(value, "color-dodge", 11) == 0) + return SP_CSS_BLEND_COLORDODGE; + if (strncmp(value, "color-burn", 10) == 0) + return SP_CSS_BLEND_COLORBURN; + if (strncmp(value, "color", 5) == 0) + return SP_CSS_BLEND_COLOR; + break; + case 'h': + if (strncmp(value, "hard-light", 10) == 0) + return SP_CSS_BLEND_HARDLIGHT; + if (strncmp(value, "hue", 3) == 0) + return SP_CSS_BLEND_HUE; + break; + case 'e': + if (strncmp(value, "exclusion", 10) == 0) + return SP_CSS_BLEND_EXCLUSION; + default: + std::cout << "SPBlendMode: Unimplemented mode: " << value << std::endl; + // do nothing by default + break; + } + + return SP_CSS_BLEND_NORMAL; +} + +/** + * Sets a specific value in the SPFeBlend. + */ +void SPFeBlend::set(SPAttributeEnum key, gchar const *value) { + SPBlendMode mode; + int input; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_MODE: + mode = sp_feBlend_readmode(value); + + if (mode != this->blend_mode) { + this->blend_mode = mode; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_IN2: + input = this->read_in(value); + + if (input != this->in2) { + this->in2 = input; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeBlend::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->readAttr( "mode" ); + this->readAttr( "in2" ); + } + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + /* This may not be true.... see issue at + * http://www.w3.org/TR/filter-effects/#feBlendElement (but it doesn't hurt). */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + + // TODO: XML Tree being used directly here while it shouldn't be. + this->setAttribute("in2", parent->name_for_image(this->in2)); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeBlend::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + SPFilter *parent = SP_FILTER(this->parent); + + if (!repr) { + repr = doc->createElement("svg:feBlend"); + } + + gchar const *in2_name = parent->name_for_image(this->in2); + + if( !in2_name ) { + + // This code is very similar to name_previous_out() + SPObject *i = parent->firstChild(); + + // Find previous filter primitive + while (i && i->getNext() != this) { + i = i->getNext(); + } + + if( i ) { + SPFilterPrimitive *i_prim = SP_FILTER_PRIMITIVE(i); + in2_name = parent->name_for_image(i_prim->image_out); + } + } + + if (in2_name) { + repr->setAttribute("in2", in2_name); + } else { + g_warning("Unable to set in2 for feBlend"); + } + + char const *mode; + switch(this->blend_mode) { + case SP_CSS_BLEND_NORMAL: + mode = "normal"; break; + case SP_CSS_BLEND_MULTIPLY: + mode = "multiply"; break; + case SP_CSS_BLEND_SCREEN: + mode = "screen"; break; + case SP_CSS_BLEND_DARKEN: + mode = "darken"; break; + case SP_CSS_BLEND_LIGHTEN: + mode = "lighten"; break; + // New + case SP_CSS_BLEND_OVERLAY: + mode = "overlay"; break; + case SP_CSS_BLEND_COLORDODGE: + mode = "color-dodge"; break; + case SP_CSS_BLEND_COLORBURN: + mode = "color-burn"; break; + case SP_CSS_BLEND_HARDLIGHT: + mode = "hard-light"; break; + case SP_CSS_BLEND_SOFTLIGHT: + mode = "soft-light"; break; + case SP_CSS_BLEND_DIFFERENCE: + mode = "difference"; break; + case SP_CSS_BLEND_EXCLUSION: + mode = "exclusion"; break; + case SP_CSS_BLEND_HUE: + mode = "hue"; break; + case SP_CSS_BLEND_SATURATION: + mode = "saturation"; break; + case SP_CSS_BLEND_COLOR: + mode = "color"; break; + case SP_CSS_BLEND_LUMINOSITY: + mode = "luminosity"; break; + default: + mode = nullptr; + } + + repr->setAttribute("mode", mode); + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeBlend::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_BLEND); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterBlend *nr_blend = dynamic_cast(nr_primitive); + g_assert(nr_blend != nullptr); + + this->renderer_common(nr_primitive); + + nr_blend->set_mode(this->blend_mode); + nr_blend->set_input(1, this->in2); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/blend.h b/src/object/filters/blend.h new file mode 100644 index 0000000..094d0cf --- /dev/null +++ b/src/object/filters/blend.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG blend filter effect + *//* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEBLEND_H_SEEN +#define SP_FEBLEND_H_SEEN + +#include "sp-filter-primitive.h" +#include "display/nr-filter-blend.h" + +#define SP_FEBLEND(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEBLEND(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeBlend : public SPFilterPrimitive { +public: + SPFeBlend(); + ~SPFeBlend() override; + + SPBlendMode blend_mode; + int in2; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEBLEND_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/colormatrix.cpp b/src/object/filters/colormatrix.cpp new file mode 100644 index 0000000..e35bcb2 --- /dev/null +++ b/src/object/filters/colormatrix.cpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Felipe Sanches + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe C. da S. Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "attributes.h" +#include "svg/svg.h" +#include "colormatrix.h" +#include "xml/repr.h" +#include "helper-fns.h" + +#include "display/nr-filter.h" + +SPFeColorMatrix::SPFeColorMatrix() + : SPFilterPrimitive(), type(Inkscape::Filters::COLORMATRIX_MATRIX), value(0) +{ +} + +SPFeColorMatrix::~SPFeColorMatrix() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeColorMatrix variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeColorMatrix::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "type" ); + this->readAttr( "values" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeColorMatrix::release() { + SPFilterPrimitive::release(); +} + +static Inkscape::Filters::FilterColorMatrixType sp_feColorMatrix_read_type(gchar const *value){ + if (!value) { + return Inkscape::Filters::COLORMATRIX_MATRIX; //matrix is default + } + + switch(value[0]){ + case 'm': + if (strcmp(value, "matrix") == 0) return Inkscape::Filters::COLORMATRIX_MATRIX; + break; + case 's': + if (strcmp(value, "saturate") == 0) return Inkscape::Filters::COLORMATRIX_SATURATE; + break; + case 'h': + if (strcmp(value, "hueRotate") == 0) return Inkscape::Filters::COLORMATRIX_HUEROTATE; + break; + case 'l': + if (strcmp(value, "luminanceToAlpha") == 0) return Inkscape::Filters::COLORMATRIX_LUMINANCETOALPHA; + break; + } + + return Inkscape::Filters::COLORMATRIX_MATRIX; //matrix is default +} + +/** + * Sets a specific value in the SPFeColorMatrix. + */ +void SPFeColorMatrix::set(SPAttributeEnum key, gchar const *str) { + Inkscape::Filters::FilterColorMatrixType read_type; + + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + switch(key) { + case SP_ATTR_TYPE: + read_type = sp_feColorMatrix_read_type(str); + + if (this->type != read_type){ + this->type = read_type; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_VALUES: + if (str){ + this->values = helperfns_read_vector(str); + this->value = helperfns_read_number(str, HELPERFNS_NO_WARNING); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, str); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeColorMatrix::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeColorMatrix::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeColorMatrix::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_COLORMATRIX); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterColorMatrix *nr_colormatrix = dynamic_cast(nr_primitive); + g_assert(nr_colormatrix != nullptr); + + this->renderer_common(nr_primitive); + nr_colormatrix->set_type(this->type); + nr_colormatrix->set_value(this->value); + nr_colormatrix->set_values(this->values); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/colormatrix.h b/src/object/filters/colormatrix.h new file mode 100644 index 0000000..a44ea8a --- /dev/null +++ b/src/object/filters/colormatrix.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG color matrix filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOLORMATRIX_H_SEEN +#define SP_FECOLORMATRIX_H_SEEN + +#include +#include "sp-filter-primitive.h" +#include "display/nr-filter-colormatrix.h" + +#define SP_FECOLORMATRIX(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FECOLORMATRIX(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeColorMatrix : public SPFilterPrimitive { +public: + SPFeColorMatrix(); + ~SPFeColorMatrix() override; + + Inkscape::Filters::FilterColorMatrixType type; + gdouble value; + std::vector values; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FECOLORMATRIX_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/componenttransfer-funcnode.cpp b/src/object/filters/componenttransfer-funcnode.cpp new file mode 100644 index 0000000..ed6ff88 --- /dev/null +++ b/src/object/filters/componenttransfer-funcnode.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG , , and implementations. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Felipe Corrêa da Silva Sanches + * Abhishek Sharma + * + * Copyright (C) 2006, 2007, 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "attributes.h" +#include "document.h" +#include "componenttransfer.h" +#include "componenttransfer-funcnode.h" +#include "xml/repr.h" +#include "helper-fns.h" + +/* FeFuncNode class */ +SPFeFuncNode::SPFeFuncNode(SPFeFuncNode::Channel channel) + : SPObject(), type(Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY), + slope(1), intercept(0), amplitude(1), exponent(1), offset(0), channel(channel) { +} + +SPFeFuncNode::~SPFeFuncNode() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPDistantLight variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeFuncNode::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + //Read values of key attributes from XML nodes into object. + this->readAttr( "type" ); + this->readAttr( "tableValues" ); + this->readAttr( "slope" ); + this->readAttr( "intercept" ); + this->readAttr( "amplitude" ); + this->readAttr( "exponent" ); + this->readAttr( "offset" ); + + +//is this necessary? + document->addResource("fefuncnode", this); //maybe feFuncR, fefuncG, feFuncB and fefuncA ? +} + +/** + * Drops any allocated memory. + */ +void SPFeFuncNode::release() { + if ( this->document ) { + // Unregister ourselves + this->document->removeResource("fefuncnode", this); + } + +//TODO: release resources here +} + +static Inkscape::Filters::FilterComponentTransferType sp_feComponenttransfer_read_type(gchar const *value){ + if (!value) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR; //type attribute is REQUIRED. + } + + switch(value[0]){ + case 'i': + if (strncmp(value, "identity", 8) == 0) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY; + } + break; + case 't': + if (strncmp(value, "table", 5) == 0) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_TABLE; + } + break; + case 'd': + if (strncmp(value, "discrete", 8) == 0) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_DISCRETE; + } + break; + case 'l': + if (strncmp(value, "linear", 6) == 0) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_LINEAR; + } + break; + case 'g': + if (strncmp(value, "gamma", 5) == 0) { + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_GAMMA; + } + break; + } + + return Inkscape::Filters::COMPONENTTRANSFER_TYPE_ERROR; //type attribute is REQUIRED. +} + +/** + * Sets a specific value in the SPFeFuncNode. + */ +void SPFeFuncNode::set(SPAttributeEnum key, gchar const *value) { + Inkscape::Filters::FilterComponentTransferType type; + double read_num; + + switch(key) { + case SP_ATTR_TYPE: + type = sp_feComponenttransfer_read_type(value); + + if(type != this->type) { + this->type = type; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_TABLEVALUES: + if (value){ + this->tableValues = helperfns_read_vector(value); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_SLOPE: + read_num = value ? helperfns_read_number(value) : 1; + + if (read_num != this->slope) { + this->slope = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_INTERCEPT: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->intercept) { + this->intercept = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_AMPLITUDE: + read_num = value ? helperfns_read_number(value) : 1; + + if (read_num != this->amplitude) { + this->amplitude = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_EXPONENT: + read_num = value ? helperfns_read_number(value) : 1; + + if (read_num != this->exponent) { + this->exponent = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_OFFSET: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->offset) { + this->offset = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeFuncNode::update(SPCtx *ctx, guint flags) { + std::cout << "SPFeFuncNode::update" << std::endl; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->readAttr( "type" ); + this->readAttr( "tableValues" ); + this->readAttr( "slope" ); + this->readAttr( "intercept" ); + this->readAttr( "amplitude" ); + this->readAttr( "exponent" ); + this->readAttr( "offset" ); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeFuncNode::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + std::cout << "SPFeFuncNode::write" << std::endl; + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/componenttransfer-funcnode.h b/src/object/filters/componenttransfer-funcnode.h new file mode 100644 index 0000000..8022035 --- /dev/null +++ b/src/object/filters/componenttransfer-funcnode.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN +#define SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN + +/** \file + * SVG implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Felipe Corrêa da Silva Sanches + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "../sp-object.h" +#include "display/nr-filter-component-transfer.h" + +#define SP_FEFUNCNODE(obj) (dynamic_cast((SPObject*)obj)) + +class SPFeFuncNode : public SPObject { +public: + enum Channel { + R, G, B, A + }; + + SPFeFuncNode(Channel channel); + ~SPFeFuncNode() override; + + Inkscape::Filters::FilterComponentTransferType type; + std::vector tableValues; + double slope; + double intercept; + double amplitude; + double exponent; + double offset; + Channel channel; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; +}; + +#endif /* !SP_FECOMPONENTTRANSFER_FUNCNODE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/componenttransfer.cpp b/src/object/filters/componenttransfer.cpp new file mode 100644 index 0000000..3a921c2 --- /dev/null +++ b/src/object/filters/componenttransfer.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "document.h" + +// In same directory +#include "componenttransfer.h" +#include "componenttransfer-funcnode.h" + +#include "display/nr-filter.h" + +#include "xml/repr.h" + +SPFeComponentTransfer::SPFeComponentTransfer() + : SPFilterPrimitive(), renderer(nullptr) +{ +} + +SPFeComponentTransfer::~SPFeComponentTransfer() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeComponentTransfer variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeComponentTransfer::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + + //do we need this? + //document->addResource("feComponentTransfer", object); +} + +static void sp_feComponentTransfer_children_modified(SPFeComponentTransfer *sp_componenttransfer) +{ + if (sp_componenttransfer->renderer) { + bool set[4] = {false, false, false, false}; + for(auto& node: sp_componenttransfer->children) { + int i = 4; + + SPFeFuncNode *funcNode = SP_FEFUNCNODE(&node); + if(!funcNode) { + continue; + } + + switch (funcNode->channel) { + case SPFeFuncNode::R: + i = 0; + break; + case SPFeFuncNode::G: + i = 1; + break; + case SPFeFuncNode::B: + i = 2; + break; + case SPFeFuncNode::A: + i = 3; + break; + } + + if (i == 4) { + g_warning("Unrecognized channel for component transfer."); + break; + } + sp_componenttransfer->renderer->type[i] = ((SPFeFuncNode *) &node)->type; + sp_componenttransfer->renderer->tableValues[i] = ((SPFeFuncNode *) &node)->tableValues; + sp_componenttransfer->renderer->slope[i] = ((SPFeFuncNode *) &node)->slope; + sp_componenttransfer->renderer->intercept[i] = ((SPFeFuncNode *) &node)->intercept; + sp_componenttransfer->renderer->amplitude[i] = ((SPFeFuncNode *) &node)->amplitude; + sp_componenttransfer->renderer->exponent[i] = ((SPFeFuncNode *) &node)->exponent; + sp_componenttransfer->renderer->offset[i] = ((SPFeFuncNode *) &node)->offset; + set[i] = true; + } + // Set any types not explicitly set to the identity transform + for(int i=0;i<4;i++) { + if (!set[i]) { + sp_componenttransfer->renderer->type[i] = Inkscape::Filters::COMPONENTTRANSFER_TYPE_IDENTITY; + } + } + } +} + +/** + * Callback for child_added event. + */ +void SPFeComponentTransfer::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPFilterPrimitive::child_added(child, ref); + + sp_feComponentTransfer_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPFeComponentTransfer::remove_child(Inkscape::XML::Node *child) { + SPFilterPrimitive::remove_child(child); + + sp_feComponentTransfer_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Drops any allocated memory. + */ +void SPFeComponentTransfer::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeComponentTransfer. + */ +void SPFeComponentTransfer::set(SPAttributeEnum key, gchar const *value) { + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeComponentTransfer::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeComponentTransfer::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeComponentTransfer::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_COMPONENTTRANSFER); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterComponentTransfer *nr_componenttransfer = dynamic_cast(nr_primitive); + g_assert(nr_componenttransfer != nullptr); + + this->renderer = nr_componenttransfer; + this->renderer_common(nr_primitive); + + + sp_feComponentTransfer_children_modified(this); //do we need it?! +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/componenttransfer.h b/src/object/filters/componenttransfer.h new file mode 100644 index 0000000..b1ca47c --- /dev/null +++ b/src/object/filters/componenttransfer.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG component transferfilter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOMPONENTTRANSFER_H_SEEN +#define SP_FECOMPONENTTRANSFER_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FECOMPONENTTRANSFER(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FECOMPONENTTRANSFER(obj) (dynamic_cast((SPObject*)obj) != NULL) + +namespace Inkscape { +namespace Filters { +class FilterComponentTransfer; +} } + +class SPFeComponentTransfer : public SPFilterPrimitive { +public: + SPFeComponentTransfer(); + ~SPFeComponentTransfer() override; + + Inkscape::Filters::FilterComponentTransfer *renderer; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FECOMPONENTTRANSFER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/composite.cpp b/src/object/filters/composite.cpp new file mode 100644 index 0000000..716aeaa --- /dev/null +++ b/src/object/filters/composite.cpp @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "composite.h" + +#include "attributes.h" +#include "helper-fns.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-composite.h" + +#include "object/sp-filter.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +SPFeComposite::SPFeComposite() + : SPFilterPrimitive(), composite_operator(COMPOSITE_DEFAULT), + k1(0), k2(0), k3(0), k4(0), in2(Inkscape::Filters::NR_FILTER_SLOT_NOT_SET) +{ +} + +SPFeComposite::~SPFeComposite() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeComposite variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeComposite::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + this->readAttr( "operator" ); + + if (this->composite_operator == COMPOSITE_ARITHMETIC) { + this->readAttr( "k1" ); + this->readAttr( "k2" ); + this->readAttr( "k3" ); + this->readAttr( "k4" ); + } + + this->readAttr( "in2" ); + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + repr->setAttribute("in2", parent->name_for_image(this->in2)); + } +} + +/** + * Drops any allocated memory. + */ +void SPFeComposite::release() { + SPFilterPrimitive::release(); +} + +static FeCompositeOperator +sp_feComposite_read_operator(gchar const *value) { + if (!value) { + return COMPOSITE_DEFAULT; + } + + if (strcmp(value, "over") == 0) { + return COMPOSITE_OVER; + } else if (strcmp(value, "in") == 0) { + return COMPOSITE_IN; + } else if (strcmp(value, "out") == 0) { + return COMPOSITE_OUT; + } else if (strcmp(value, "atop") == 0) { + return COMPOSITE_ATOP; + } else if (strcmp(value, "xor") == 0) { + return COMPOSITE_XOR; + } else if (strcmp(value, "arithmetic") == 0) { + return COMPOSITE_ARITHMETIC; + } +#ifdef WITH_CSSCOMPOSITE + else if (strcmp(value, "clear") == 0) { + return COMPOSITE_CLEAR; + } else if (strcmp(value, "copy") == 0) { + return COMPOSITE_COPY; + } else if (strcmp(value, "destination") == 0) { + return COMPOSITE_DESTINATION; + } else if (strcmp(value, "destination-over") == 0) { + return COMPOSITE_DESTINATION_OVER; + } else if (strcmp(value, "destination-in") == 0) { + return COMPOSITE_DESTINATION_IN; + } else if (strcmp(value, "destination-out") == 0) { + return COMPOSITE_DESTINATION_OUT; + } else if (strcmp(value, "destination-atop") == 0) { + return COMPOSITE_DESTINATION_ATOP; + } else if (strcmp(value, "lighter") == 0) { + return COMPOSITE_LIGHTER; + } +#endif + std::cout << "Inkscape::Filters::FilterCompositeOperator: Unimplemented operator: " << value << std::endl; + + return COMPOSITE_DEFAULT; +} + +/** + * Sets a specific value in the SPFeComposite. + */ +void SPFeComposite::set(SPAttributeEnum key, gchar const *value) { + int input; + FeCompositeOperator op; + double k_n; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_OPERATOR: + op = sp_feComposite_read_operator(value); + if (op != this->composite_operator) { + this->composite_operator = op; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_K1: + k_n = value ? helperfns_read_number(value) : 0; + if (k_n != this->k1) { + this->k1 = k_n; + if (this->composite_operator == COMPOSITE_ARITHMETIC) + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_K2: + k_n = value ? helperfns_read_number(value) : 0; + if (k_n != this->k2) { + this->k2 = k_n; + if (this->composite_operator == COMPOSITE_ARITHMETIC) + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_K3: + k_n = value ? helperfns_read_number(value) : 0; + if (k_n != this->k3) { + this->k3 = k_n; + if (this->composite_operator == COMPOSITE_ARITHMETIC) + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_K4: + k_n = value ? helperfns_read_number(value) : 0; + if (k_n != this->k4) { + this->k4 = k_n; + if (this->composite_operator == COMPOSITE_ARITHMETIC) + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_IN2: + input = this->read_in(value); + if (input != this->in2) { + this->in2 = input; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeComposite::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + /* This may not be true.... see issue at + * http://www.w3.org/TR/filter-effects/#feBlendElement (but it doesn't hurt). */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + + //XML Tree being used directly here while it shouldn't be. + this->setAttribute("in2", parent->name_for_image(this->in2)); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeComposite::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + SPFilter *parent = SP_FILTER(this->parent); + + if (!repr) { + repr = doc->createElement("svg:feComposite"); + } + + gchar const *in2_name = parent->name_for_image(this->in2); + + if( !in2_name ) { + + // This code is very similar to name_previous_out() + SPObject *i = parent->firstChild(); + + // Find previous filter primitive + while (i && i->getNext() != this) { + i = i->getNext(); + } + + if( i ) { + SPFilterPrimitive *i_prim = SP_FILTER_PRIMITIVE(i); + in2_name = parent->name_for_image(i_prim->image_out); + } + } + + if (in2_name) { + repr->setAttribute("in2", in2_name); + } else { + g_warning("Unable to set in2 for feComposite"); + } + + char const *comp_op; + + switch (this->composite_operator) { + case COMPOSITE_OVER: + comp_op = "over"; break; + case COMPOSITE_IN: + comp_op = "in"; break; + case COMPOSITE_OUT: + comp_op = "out"; break; + case COMPOSITE_ATOP: + comp_op = "atop"; break; + case COMPOSITE_XOR: + comp_op = "xor"; break; + case COMPOSITE_ARITHMETIC: + comp_op = "arithmetic"; break; +#ifdef WITH_CSSCOMPOSITE + // New CSS operators + case COMPOSITE_CLEAR: + comp_op = "clear"; break; + case COMPOSITE_COPY: + comp_op = "copy"; break; + case COMPOSITE_DESTINATION: + comp_op = "destination"; break; + case COMPOSITE_DESTINATION_OVER: + comp_op = "destination-over"; break; + case COMPOSITE_DESTINATION_IN: + comp_op = "destination-in"; break; + case COMPOSITE_DESTINATION_OUT: + comp_op = "destination-out"; break; + case COMPOSITE_DESTINATION_ATOP: + comp_op = "destination-atop"; break; + case COMPOSITE_LIGHTER: + comp_op = "lighter"; break; +#endif + default: + comp_op = nullptr; + } + + repr->setAttribute("operator", comp_op); + + if (this->composite_operator == COMPOSITE_ARITHMETIC) { + sp_repr_set_svg_double(repr, "k1", this->k1); + sp_repr_set_svg_double(repr, "k2", this->k2); + sp_repr_set_svg_double(repr, "k3", this->k3); + sp_repr_set_svg_double(repr, "k4", this->k4); + } else { + repr->removeAttribute("k1"); + repr->removeAttribute("k2"); + repr->removeAttribute("k3"); + repr->removeAttribute("k4"); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeComposite::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_COMPOSITE); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterComposite *nr_composite = dynamic_cast(nr_primitive); + g_assert(nr_composite != nullptr); + + this->renderer_common(nr_primitive); + + nr_composite->set_operator(this->composite_operator); + nr_composite->set_input(1, this->in2); + + if (this->composite_operator == COMPOSITE_ARITHMETIC) { + nr_composite->set_arithmetic(this->k1, this->k2, + this->k3, this->k4); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/composite.h b/src/object/filters/composite.h new file mode 100644 index 0000000..1205b31 --- /dev/null +++ b/src/object/filters/composite.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG composite filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECOMPOSITE_H_SEEN +#define SP_FECOMPOSITE_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FECOMPOSITE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FECOMPOSITE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum FeCompositeOperator { + // Default value is 'over', but let's distinquish specifying the + // default and implicitly using the default + COMPOSITE_DEFAULT, + COMPOSITE_OVER, /* Source Over */ + COMPOSITE_IN, /* Source In */ + COMPOSITE_OUT, /* Source Out */ + COMPOSITE_ATOP, /* Source Atop */ + COMPOSITE_XOR, + COMPOSITE_ARITHMETIC, /* Not a fundamental PorterDuff operator, nor Cairo */ +#ifdef WITH_CSSCOMPOSITE + // New in CSS + COMPOSITE_CLEAR, + COMPOSITE_COPY, /* Source */ + COMPOSITE_DESTINATION, + COMPOSITE_DESTINATION_OVER, + COMPOSITE_DESTINATION_IN, + COMPOSITE_DESTINATION_OUT, + COMPOSITE_DESTINATION_ATOP, + COMPOSITE_LIGHTER, /* Plus, Add (Not a fundamental PorterDuff operator */ +#endif + COMPOSITE_ENDOPERATOR /* Cairo Saturate is not included in CSS */ +}; + +class SPFeComposite : public SPFilterPrimitive { +public: + SPFeComposite(); + ~SPFeComposite() override; + + FeCompositeOperator composite_operator; + double k1, k2, k3, k4; + int in2; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FECOMPOSITE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/convolvematrix.cpp b/src/object/filters/convolvematrix.cpp new file mode 100644 index 0000000..658a383 --- /dev/null +++ b/src/object/filters/convolvematrix.cpp @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "convolvematrix.h" + +#include "attributes.h" +#include "helper-fns.h" + +#include "display/nr-filter.h" + +#include "xml/repr.h" + +SPFeConvolveMatrix::SPFeConvolveMatrix() : SPFilterPrimitive() { + this->bias = 0; + this->divisorIsSet = false; + this->divisor = 0; + + //Setting default values: + this->order.set("3 3"); + this->targetX = 1; + this->targetY = 1; + this->edgeMode = Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; + this->preserveAlpha = false; + + //some helper variables: + this->targetXIsSet = false; + this->targetYIsSet = false; + this->kernelMatrixIsSet = false; +} + +SPFeConvolveMatrix::~SPFeConvolveMatrix() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeConvolveMatrix variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeConvolveMatrix::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "order" ); + this->readAttr( "kernelMatrix" ); + this->readAttr( "divisor" ); + this->readAttr( "bias" ); + this->readAttr( "targetX" ); + this->readAttr( "targetY" ); + this->readAttr( "edgeMode" ); + this->readAttr( "kernelUnitLength" ); + this->readAttr( "preserveAlpha" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeConvolveMatrix::release() { + SPFilterPrimitive::release(); +} + +static Inkscape::Filters::FilterConvolveMatrixEdgeMode sp_feConvolveMatrix_read_edgeMode(gchar const *value){ + if (!value) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; //duplicate is default + } + + switch (value[0]) { + case 'd': + if (strncmp(value, "duplicate", 9) == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; + } + break; + case 'w': + if (strncmp(value, "wrap", 4) == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_WRAP; + } + break; + case 'n': + if (strncmp(value, "none", 4) == 0) { + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_NONE; + } + break; + } + + return Inkscape::Filters::CONVOLVEMATRIX_EDGEMODE_DUPLICATE; //duplicate is default +} + +/** + * Sets a specific value in the SPFeConvolveMatrix. + */ +void SPFeConvolveMatrix::set(SPAttributeEnum key, gchar const *value) { + double read_num; + int read_int; + bool read_bool; + Inkscape::Filters::FilterConvolveMatrixEdgeMode read_mode; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_ORDER: + this->order.set(value); + + //From SVG spec: If is not provided, it defaults to . + if (this->order.optNumIsSet() == false) { + this->order.setOptNumber(this->order.getNumber()); + } + + if (this->targetXIsSet == false) { + this->targetX = (int) floor(this->order.getNumber()/2); + } + + if (this->targetYIsSet == false) { + this->targetY = (int) floor(this->order.getOptNumber()/2); + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_KERNELMATRIX: + if (value){ + this->kernelMatrixIsSet = true; + this->kernelMatrix = helperfns_read_vector(value); + + if (! this->divisorIsSet) { + this->divisor = 0; + + for (double i : this->kernelMatrix) { + this->divisor += i; + } + + if (this->divisor == 0) { + this->divisor = 1; + } + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + g_warning("For feConvolveMatrix you MUST pass a kernelMatrix parameter!"); + } + break; + case SP_ATTR_DIVISOR: + if (value) { + read_num = helperfns_read_number(value); + + if (read_num == 0) { + // This should actually be an error, but given our UI it is more useful to simply set divisor to the default. + if (this->kernelMatrixIsSet) { + for (double i : this->kernelMatrix) { + read_num += i; + } + } + + if (read_num == 0) { + read_num = 1; + } + + if (this->divisorIsSet || this->divisor!=read_num) { + this->divisorIsSet = false; + this->divisor = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else if (!this->divisorIsSet || this->divisor!=read_num) { + this->divisorIsSet = true; + this->divisor = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + case SP_ATTR_BIAS: + read_num = 0; + if (value) { + read_num = helperfns_read_number(value); + } + + if (read_num != this->bias){ + this->bias = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_TARGETX: + if (value) { + read_int = (int) helperfns_read_number(value); + + if (read_int < 0 || read_int > this->order.getNumber()){ + g_warning("targetX must be a value between 0 and orderX! Assuming floor(orderX/2) as default value."); + read_int = (int) floor(this->order.getNumber()/2.0); + } + + this->targetXIsSet = true; + + if (read_int != this->targetX){ + this->targetX = read_int; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + case SP_ATTR_TARGETY: + if (value) { + read_int = (int) helperfns_read_number(value); + + if (read_int < 0 || read_int > this->order.getOptNumber()){ + g_warning("targetY must be a value between 0 and orderY! Assuming floor(orderY/2) as default value."); + read_int = (int) floor(this->order.getOptNumber()/2.0); + } + + this->targetYIsSet = true; + + if (read_int != this->targetY){ + this->targetY = read_int; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + case SP_ATTR_EDGEMODE: + read_mode = sp_feConvolveMatrix_read_edgeMode(value); + + if (read_mode != this->edgeMode){ + this->edgeMode = read_mode; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_KERNELUNITLENGTH: + this->kernelUnitLength.set(value); + + //From SVG spec: If the value is not specified, it defaults to the same value as . + if (this->kernelUnitLength.optNumIsSet() == false) { + this->kernelUnitLength.setOptNumber(this->kernelUnitLength.getNumber()); + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_PRESERVEALPHA: + read_bool = helperfns_read_bool(value, false); + + if (read_bool != this->preserveAlpha){ + this->preserveAlpha = read_bool; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, value); + break; + } + +} + +/** + * Receives update notifications. + */ +void SPFeConvolveMatrix::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeConvolveMatrix::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeConvolveMatrix::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_CONVOLVEMATRIX); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterConvolveMatrix *nr_convolve = dynamic_cast(nr_primitive); + g_assert(nr_convolve != nullptr); + + this->renderer_common(nr_primitive); + + nr_convolve->set_targetX(this->targetX); + nr_convolve->set_targetY(this->targetY); + nr_convolve->set_orderX( (int)this->order.getNumber() ); + nr_convolve->set_orderY( (int)this->order.getOptNumber() ); + nr_convolve->set_kernelMatrix(this->kernelMatrix); + nr_convolve->set_divisor(this->divisor); + nr_convolve->set_bias(this->bias); + nr_convolve->set_preserveAlpha(this->preserveAlpha); +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/convolvematrix.h b/src/object/filters/convolvematrix.h new file mode 100644 index 0000000..e19608a --- /dev/null +++ b/src/object/filters/convolvematrix.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG matrix convolution filter effect + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FECONVOLVEMATRIX_H_SEEN +#define SP_FECONVOLVEMATRIX_H_SEEN + +#include +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-convolve-matrix.h" + +#define SP_FECONVOLVEMATRIX(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FECONVOLVEMATRIX(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeConvolveMatrix : public SPFilterPrimitive { +public: + SPFeConvolveMatrix(); + ~SPFeConvolveMatrix() override; + + NumberOptNumber order; + std::vector kernelMatrix; + double divisor, bias; + int targetX, targetY; + Inkscape::Filters::FilterConvolveMatrixEdgeMode edgeMode; + NumberOptNumber kernelUnitLength; + bool preserveAlpha; + + bool targetXIsSet; + bool targetYIsSet; + bool divisorIsSet; + bool kernelMatrixIsSet; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FECONVOLVEMATRIX_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/diffuselighting.cpp b/src/object/filters/diffuselighting.cpp new file mode 100644 index 0000000..a9ee1cf --- /dev/null +++ b/src/object/filters/diffuselighting.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Jean-Rene Reinhard + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// Same directory +#include "diffuselighting.h" +#include "distantlight.h" +#include "pointlight.h" +#include "spotlight.h" + +#include "strneq.h" +#include "attributes.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-diffuselighting.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/svg-icc-color.h" + +#include "xml/repr.h" + +/* FeDiffuseLighting base class */ +static void sp_feDiffuseLighting_children_modified(SPFeDiffuseLighting *sp_diffuselighting); + +SPFeDiffuseLighting::SPFeDiffuseLighting() : SPFilterPrimitive() { + this->surfaceScale = 1; + this->diffuseConstant = 1; + this->lighting_color = 0xffffffff; + this->icc = nullptr; + + //TODO kernelUnit + this->renderer = nullptr; + + this->surfaceScale_set = FALSE; + this->diffuseConstant_set = FALSE; + this->lighting_color_set = FALSE; +} + +SPFeDiffuseLighting::~SPFeDiffuseLighting() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeDiffuseLighting variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeDiffuseLighting::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "surfaceScale" ); + this->readAttr( "diffuseConstant" ); + this->readAttr( "kernelUnitLength" ); + this->readAttr( "lighting-color" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeDiffuseLighting::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeDiffuseLighting. + */ +void SPFeDiffuseLighting::set(SPAttributeEnum key, gchar const *value) { + gchar const *cend_ptr = nullptr; + gchar *end_ptr = nullptr; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + //TODO test forbidden values + case SP_ATTR_SURFACESCALE: + end_ptr = nullptr; + + if (value) { + this->surfaceScale = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->surfaceScale_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->surfaceScale = 1; + this->surfaceScale_set = FALSE; + } + + if (this->renderer) { + this->renderer->surfaceScale = this->surfaceScale; + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_DIFFUSECONSTANT: + end_ptr = nullptr; + + if (value) { + this->diffuseConstant = g_ascii_strtod(value, &end_ptr); + + if (end_ptr && this->diffuseConstant >= 0) { + this->diffuseConstant_set = TRUE; + } else { + end_ptr = nullptr; + g_warning("this: diffuseConstant should be a positive number ... defaulting to 1"); + } + } + + if (!value || !end_ptr) { + this->diffuseConstant = 1; + this->diffuseConstant_set = FALSE; + } + + if (this->renderer) { + this->renderer->diffuseConstant = this->diffuseConstant; + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_KERNELUNITLENGTH: + //TODO kernelUnit + //this->kernelUnitLength.set(value); + /*TODOif (feDiffuseLighting->renderer) { + feDiffuseLighting->renderer->surfaceScale = feDiffuseLighting->renderer; + } + */ + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_PROP_LIGHTING_COLOR: + cend_ptr = nullptr; + this->lighting_color = sp_svg_read_color(value, &cend_ptr, 0xffffffff); + + //if a value was read + if (cend_ptr) { + while (g_ascii_isspace(*cend_ptr)) { + ++cend_ptr; + } + + if (strneq(cend_ptr, "icc-color(", 10)) { + if (!this->icc) { + this->icc = new SVGICCColor(); + } + + if ( ! sp_svg_read_icc_color( cend_ptr, this->icc ) ) { + delete this->icc; + this->icc = nullptr; + } + } + + this->lighting_color_set = TRUE; + } else { + //lighting_color already contains the default value + this->lighting_color_set = FALSE; + } + + if (this->renderer) { + this->renderer->lighting_color = this->lighting_color; + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeDiffuseLighting::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr( "surfaceScale" ); + this->readAttr( "diffuseConstant" ); + this->readAttr( "kernelUnit" ); + this->readAttr( "lighting-color" ); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeDiffuseLighting::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values _and children_ into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + //repr = doc->createElement("svg:feDiffuseLighting"); + } + + if (this->surfaceScale_set) { + sp_repr_set_css_double(repr, "surfaceScale", this->surfaceScale); + } else { + repr->removeAttribute("surfaceScale"); + } + + if (this->diffuseConstant_set) { + sp_repr_set_css_double(repr, "diffuseConstant", this->diffuseConstant); + } else { + repr->removeAttribute("diffuseConstant"); + } + + /*TODO kernelUnits */ + if (this->lighting_color_set) { + gchar c[64]; + sp_svg_write_color(c, sizeof(c), this->lighting_color); + repr->setAttribute("lighting-color", c); + } else { + repr->removeAttribute("lighting-color"); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +/** + * Callback for child_added event. + */ +void SPFeDiffuseLighting::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPFilterPrimitive::child_added(child, ref); + + sp_feDiffuseLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPFeDiffuseLighting::remove_child(Inkscape::XML::Node *child) { + SPFilterPrimitive::remove_child(child); + + sp_feDiffuseLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeDiffuseLighting::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) { + SPFilterPrimitive::order_changed(child, old_ref, new_ref); + + sp_feDiffuseLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +static void sp_feDiffuseLighting_children_modified(SPFeDiffuseLighting *sp_diffuselighting) +{ + if (sp_diffuselighting->renderer) { + sp_diffuselighting->renderer->light_type = Inkscape::Filters::NO_LIGHT; + if (SP_IS_FEDISTANTLIGHT(sp_diffuselighting->firstChild())) { + sp_diffuselighting->renderer->light_type = Inkscape::Filters::DISTANT_LIGHT; + sp_diffuselighting->renderer->light.distant = SP_FEDISTANTLIGHT(sp_diffuselighting->firstChild()); + } + if (SP_IS_FEPOINTLIGHT(sp_diffuselighting->firstChild())) { + sp_diffuselighting->renderer->light_type = Inkscape::Filters::POINT_LIGHT; + sp_diffuselighting->renderer->light.point = SP_FEPOINTLIGHT(sp_diffuselighting->firstChild()); + } + if (SP_IS_FESPOTLIGHT(sp_diffuselighting->firstChild())) { + sp_diffuselighting->renderer->light_type = Inkscape::Filters::SPOT_LIGHT; + sp_diffuselighting->renderer->light.spot = SP_FESPOTLIGHT(sp_diffuselighting->firstChild()); + } + } +} + +void SPFeDiffuseLighting::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_DIFFUSELIGHTING); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterDiffuseLighting *nr_diffuselighting = dynamic_cast(nr_primitive); + g_assert(nr_diffuselighting != nullptr); + + this->renderer = nr_diffuselighting; + this->renderer_common(nr_primitive); + + nr_diffuselighting->diffuseConstant = this->diffuseConstant; + nr_diffuselighting->surfaceScale = this->surfaceScale; + nr_diffuselighting->lighting_color = this->lighting_color; + nr_diffuselighting->set_icc(this->icc); + + //We assume there is at most one child + nr_diffuselighting->light_type = Inkscape::Filters::NO_LIGHT; + + if (SP_IS_FEDISTANTLIGHT(this->firstChild())) { + nr_diffuselighting->light_type = Inkscape::Filters::DISTANT_LIGHT; + nr_diffuselighting->light.distant = SP_FEDISTANTLIGHT(this->firstChild()); + } + + if (SP_IS_FEPOINTLIGHT(this->firstChild())) { + nr_diffuselighting->light_type = Inkscape::Filters::POINT_LIGHT; + nr_diffuselighting->light.point = SP_FEPOINTLIGHT(this->firstChild()); + } + + if (SP_IS_FESPOTLIGHT(this->firstChild())) { + nr_diffuselighting->light_type = Inkscape::Filters::SPOT_LIGHT; + nr_diffuselighting->light.spot = SP_FESPOTLIGHT(this->firstChild()); + } + + //nr_offset->set_dx(sp_offset->dx); + //nr_offset->set_dy(sp_offset->dy); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/diffuselighting.h b/src/object/filters/diffuselighting.h new file mode 100644 index 0000000..7905f64 --- /dev/null +++ b/src/object/filters/diffuselighting.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG diffuse lighting filter effect + *//* + * Authors: + * Hugo Rodrigues + * Jean-Rene Reinhard + * + * Copyright (C) 2006-2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEDIFFUSELIGHTING_H_SEEN +#define SP_FEDIFFUSELIGHTING_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" + +#define SP_FEDIFFUSELIGHTING(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEDIFFUSELIGHTING(obj) (dynamic_cast((SPObject*)obj) != NULL) + +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { +class FilterDiffuseLighting; +} } + +class SPFeDiffuseLighting : public SPFilterPrimitive { +public: + SPFeDiffuseLighting(); + ~SPFeDiffuseLighting() override; + + gfloat surfaceScale; + guint surfaceScale_set : 1; + gfloat diffuseConstant; + guint diffuseConstant_set : 1; + NumberOptNumber kernelUnitLength; + guint32 lighting_color; + guint lighting_color_set : 1; + Inkscape::Filters::FilterDiffuseLighting *renderer; + SVGICCColor *icc; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old_repr, Inkscape::XML::Node* new_repr) override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEDIFFUSELIGHTING_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/displacementmap.cpp b/src/object/filters/displacementmap.cpp new file mode 100644 index 0000000..80b9638 --- /dev/null +++ b/src/object/filters/displacementmap.cpp @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "displacementmap.h" + +#include "attributes.h" +#include "helper-fns.h" + +#include "display/nr-filter-displacement-map.h" +#include "display/nr-filter.h" + +#include "object/sp-filter.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +SPFeDisplacementMap::SPFeDisplacementMap() : SPFilterPrimitive() { + this->scale=0; + this->xChannelSelector = DISPLACEMENTMAP_CHANNEL_ALPHA; + this->yChannelSelector = DISPLACEMENTMAP_CHANNEL_ALPHA; + this->in2 = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +SPFeDisplacementMap::~SPFeDisplacementMap() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeDisplacementMap variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeDisplacementMap::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "scale" ); + this->readAttr( "in2" ); + this->readAttr( "xChannelSelector" ); + this->readAttr( "yChannelSelector" ); + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + repr->setAttribute("in2", parent->name_for_image(this->in2)); + } +} + +/** + * Drops any allocated memory. + */ +void SPFeDisplacementMap::release() { + SPFilterPrimitive::release(); +} + +static FilterDisplacementMapChannelSelector sp_feDisplacementMap_readChannelSelector(gchar const *value) +{ + if (!value) return DISPLACEMENTMAP_CHANNEL_ALPHA; + + switch (value[0]) { + case 'R': + return DISPLACEMENTMAP_CHANNEL_RED; + break; + case 'G': + return DISPLACEMENTMAP_CHANNEL_GREEN; + break; + case 'B': + return DISPLACEMENTMAP_CHANNEL_BLUE; + break; + case 'A': + return DISPLACEMENTMAP_CHANNEL_ALPHA; + break; + default: + // error + g_warning("Invalid attribute for Channel Selector. Valid modes are 'R', 'G', 'B' or 'A'"); + break; + } + + return DISPLACEMENTMAP_CHANNEL_ALPHA; //default is Alpha Channel +} + +/** + * Sets a specific value in the SPFeDisplacementMap. + */ +void SPFeDisplacementMap::set(SPAttributeEnum key, gchar const *value) { + int input; + double read_num; + FilterDisplacementMapChannelSelector read_selector; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_XCHANNELSELECTOR: + read_selector = sp_feDisplacementMap_readChannelSelector(value); + + if (read_selector != this->xChannelSelector){ + this->xChannelSelector = read_selector; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_YCHANNELSELECTOR: + read_selector = sp_feDisplacementMap_readChannelSelector(value); + + if (read_selector != this->yChannelSelector){ + this->yChannelSelector = read_selector; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_SCALE: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->scale) { + this->scale = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_IN2: + input = this->read_in(value); + + if (input != this->in2) { + this->in2 = input; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeDisplacementMap::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + /* Unlike normal in, in2 is required attribute. Make sure, we can call + * it by some name. */ + if (this->in2 == Inkscape::Filters::NR_FILTER_SLOT_NOT_SET || + this->in2 == Inkscape::Filters::NR_FILTER_UNNAMED_SLOT) + { + SPFilter *parent = SP_FILTER(this->parent); + this->in2 = this->name_previous_out(); + + //XML Tree being used directly here while it shouldn't be. + this->setAttribute("in2", parent->name_for_image(this->in2)); + } + + SPFilterPrimitive::update(ctx, flags); +} + +static char const * get_channelselector_name(FilterDisplacementMapChannelSelector selector) { + switch(selector) { + case DISPLACEMENTMAP_CHANNEL_RED: + return "R"; + case DISPLACEMENTMAP_CHANNEL_GREEN: + return "G"; + case DISPLACEMENTMAP_CHANNEL_BLUE: + return "B"; + case DISPLACEMENTMAP_CHANNEL_ALPHA: + return "A"; + default: + return nullptr; + } +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeDisplacementMap::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + SPFilter *parent = SP_FILTER(this->parent); + + if (!repr) { + repr = doc->createElement("svg:feDisplacementMap"); + } + + gchar const *in2_name = parent->name_for_image(this->in2); + + if( !in2_name ) { + + // This code is very similar to name_previous_out() + SPObject *i = parent->firstChild(); + + // Find previous filter primitive + while (i && i->getNext() != this) { + i = i->getNext(); + } + + if( i ) { + SPFilterPrimitive *i_prim = SP_FILTER_PRIMITIVE(i); + in2_name = parent->name_for_image(i_prim->image_out); + } + } + + if (in2_name) { + repr->setAttribute("in2", in2_name); + } else { + g_warning("Unable to set in2 for feDisplacementMap"); + } + + sp_repr_set_svg_double(repr, "scale", this->scale); + repr->setAttribute("xChannelSelector", + get_channelselector_name(this->xChannelSelector)); + repr->setAttribute("yChannelSelector", + get_channelselector_name(this->yChannelSelector)); + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeDisplacementMap::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_DISPLACEMENTMAP); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterDisplacementMap *nr_displacement_map = dynamic_cast(nr_primitive); + g_assert(nr_displacement_map != nullptr); + + this->renderer_common(nr_primitive); + + nr_displacement_map->set_input(1, this->in2); + nr_displacement_map->set_scale(this->scale); + nr_displacement_map->set_channel_selector(0, this->xChannelSelector); + nr_displacement_map->set_channel_selector(1, this->yChannelSelector); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/displacementmap.h b/src/object/filters/displacementmap.h new file mode 100644 index 0000000..e9e0731 --- /dev/null +++ b/src/object/filters/displacementmap.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG displacement map filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEDISPLACEMENTMAP_H_SEEN +#define SP_FEDISPLACEMENTMAP_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FEDISPLACEMENTMAP(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEDISPLACEMENTMAP(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum FilterDisplacementMapChannelSelector { + DISPLACEMENTMAP_CHANNEL_RED, + DISPLACEMENTMAP_CHANNEL_GREEN, + DISPLACEMENTMAP_CHANNEL_BLUE, + DISPLACEMENTMAP_CHANNEL_ALPHA, + DISPLACEMENTMAP_CHANNEL_ENDTYPE +}; + +class SPFeDisplacementMap : public SPFilterPrimitive { +public: + SPFeDisplacementMap(); + ~SPFeDisplacementMap() override; + + int in2; + double scale; + FilterDisplacementMapChannelSelector xChannelSelector; + FilterDisplacementMapChannelSelector yChannelSelector; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEDISPLACEMENTMAP_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/distantlight.cpp b/src/object/filters/distantlight.cpp new file mode 100644 index 0000000..7862906 --- /dev/null +++ b/src/object/filters/distantlight.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +// In same directory +#include "distantlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include "attributes.h" +#include "document.h" + +#include "xml/repr.h" + +SPFeDistantLight::SPFeDistantLight() + : SPObject(), azimuth(0), azimuth_set(FALSE), elevation(0), elevation_set(FALSE) { +} + +SPFeDistantLight::~SPFeDistantLight() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPDistantLight variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeDistantLight::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + //Read values of key attributes from XML nodes into object. + this->readAttr( "azimuth" ); + this->readAttr( "elevation" ); + +//is this necessary? + document->addResource("fedistantlight", this); +} + +/** + * Drops any allocated memory. + */ +void SPFeDistantLight::release() { + if ( this->document ) { + // Unregister ourselves + this->document->removeResource("fedistantlight", this); + } + +//TODO: release resources here +} + +/** + * Sets a specific value in the SPFeDistantLight. + */ +void SPFeDistantLight::set(SPAttributeEnum key, gchar const *value) { + gchar *end_ptr; + + switch (key) { + case SP_ATTR_AZIMUTH: + end_ptr =nullptr; + + if (value) { + this->azimuth = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->azimuth_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->azimuth_set = FALSE; + this->azimuth = 0; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_ELEVATION: + end_ptr =nullptr; + + if (value) { + this->elevation = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->elevation_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->elevation_set = FALSE; + this->elevation = 0; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + // See if any parents need this value. + SPObject::set(key, value); + break; + } +} + +/** + * * Receives update notifications. + * */ +void SPFeDistantLight::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr( "azimuth" ); + this->readAttr( "elevation" ); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeDistantLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + if (this->azimuth_set) { + sp_repr_set_css_double(repr, "azimuth", this->azimuth); + } + + if (this->elevation_set) { + sp_repr_set_css_double(repr, "elevation", this->elevation); + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/distantlight.h b/src/object/filters/distantlight.h new file mode 100644 index 0000000..dfc193d --- /dev/null +++ b/src/object/filters/distantlight.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FEDISTANTLIGHT_H_SEEN +#define SP_FEDISTANTLIGHT_H_SEEN + +/** \file + * SVG implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "../sp-object.h" + +#define SP_FEDISTANTLIGHT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEDISTANTLIGHT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/* Distant light class */ +class SPFeDistantLight : public SPObject { +public: + SPFeDistantLight(); + ~SPFeDistantLight() override; + + /** azimuth attribute */ + float azimuth; + unsigned int azimuth_set : 1; + /** elevation attribute */ + float elevation; + unsigned int elevation_set : 1; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SP_FEDISTANTLIGHT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/flood.cpp b/src/object/filters/flood.cpp new file mode 100644 index 0000000..ed3b94a --- /dev/null +++ b/src/object/filters/flood.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "flood.h" + +#include "strneq.h" +#include "attributes.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-flood.h" + +#include "xml/repr.h" + +SPFeFlood::SPFeFlood() : SPFilterPrimitive() { + this->color = 0; + + this->opacity = 1; + this->icc = nullptr; +} + +SPFeFlood::~SPFeFlood() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeFlood variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeFlood::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "flood-opacity" ); + this->readAttr( "flood-color" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeFlood::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeFlood. + */ +void SPFeFlood::set(SPAttributeEnum key, gchar const *value) { + gchar const *cend_ptr = nullptr; + gchar *end_ptr = nullptr; + guint32 read_color; + double read_num; + bool dirty = false; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_PROP_FLOOD_COLOR: + cend_ptr = nullptr; + read_color = sp_svg_read_color(value, &cend_ptr, 0xffffffff); + + if (cend_ptr && read_color != this->color){ + this->color = read_color; + dirty=true; + } + + if (cend_ptr){ + while (g_ascii_isspace(*cend_ptr)) { + ++cend_ptr; + } + + if (strneq(cend_ptr, "icc-color(", 10)) { + if (!this->icc) { + this->icc = new SVGICCColor(); + } + + if ( ! sp_svg_read_icc_color( cend_ptr, this->icc ) ) { + delete this->icc; + this->icc = nullptr; + } + + dirty = true; + } + } + + if (dirty) { + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_PROP_FLOOD_OPACITY: + if (value) { + read_num = g_ascii_strtod(value, &end_ptr); + + if (end_ptr != nullptr) { + if (*end_ptr) { + g_warning("Unable to convert \"%s\" to number", value); + read_num = 1; + } + } + } else { + read_num = 1; + } + + if (read_num != this->opacity) { + this->opacity = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeFlood::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeFlood::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeFlood::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_FLOOD); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterFlood *nr_flood = dynamic_cast(nr_primitive); + g_assert(nr_flood != nullptr); + + this->renderer_common(nr_primitive); + + nr_flood->set_opacity(this->opacity); + nr_flood->set_color(this->color); + nr_flood->set_icc(this->icc); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/flood.h b/src/object/filters/flood.h new file mode 100644 index 0000000..f36e6db --- /dev/null +++ b/src/object/filters/flood.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG flood filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEFLOOD_H_SEEN +#define SP_FEFLOOD_H_SEEN + +#include "sp-filter-primitive.h" +#include "svg/svg-icc-color.h" + +#define SP_FEFLOOD(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEFLOOD(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeFlood : public SPFilterPrimitive { +public: + SPFeFlood(); + ~SPFeFlood() override; + + guint32 color; + SVGICCColor *icc; + double opacity; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEFLOOD_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/gaussian-blur.cpp b/src/object/filters/gaussian-blur.cpp new file mode 100644 index 0000000..2cbbaa7 --- /dev/null +++ b/src/object/filters/gaussian-blur.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "gaussian-blur.h" + +#include "attributes.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-gaussian.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +SPGaussianBlur::SPGaussianBlur() : SPFilterPrimitive() { +} + +SPGaussianBlur::~SPGaussianBlur() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPGaussianBlur variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPGaussianBlur::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + this->readAttr( "stdDeviation" ); +} + +/** + * Drops any allocated memory. + */ +void SPGaussianBlur::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPGaussianBlur. + */ +void SPGaussianBlur::set(SPAttributeEnum key, gchar const *value) { + switch(key) { + case SP_ATTR_STDDEVIATION: + this->stdDeviation.set(value); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPGaussianBlur::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->readAttr( "stdDeviation" ); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPGaussianBlur::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void sp_gaussianBlur_setDeviation(SPGaussianBlur *blur, float num) +{ + blur->stdDeviation.setNumber(num); +} + +void sp_gaussianBlur_setDeviation(SPGaussianBlur *blur, float num, float optnum) +{ + blur->stdDeviation.setNumber(num); + blur->stdDeviation.setOptNumber(optnum); +} + +void SPGaussianBlur::build_renderer(Inkscape::Filters::Filter* filter) { + int handle = filter->add_primitive(Inkscape::Filters::NR_FILTER_GAUSSIANBLUR); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(handle); + Inkscape::Filters::FilterGaussian *nr_blur = dynamic_cast(nr_primitive); + + this->renderer_common(nr_primitive); + + gfloat num = this->stdDeviation.getNumber(); + + if (num >= 0.0) { + gfloat optnum = this->stdDeviation.getOptNumber(); + + if(optnum >= 0.0) { + nr_blur->set_deviation((double) num, (double) optnum); + } else { + nr_blur->set_deviation((double) num); + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/gaussian-blur.h b/src/object/filters/gaussian-blur.h new file mode 100644 index 0000000..a2ba281 --- /dev/null +++ b/src/object/filters/gaussian-blur.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG Gaussian blur filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_GAUSSIANBLUR_H_SEEN +#define SP_GAUSSIANBLUR_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" + +#define SP_GAUSSIANBLUR(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GAUSSIANBLUR(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPGaussianBlur : public SPFilterPrimitive { +public: + SPGaussianBlur(); + ~SPGaussianBlur() override; + + /** stdDeviation attribute */ + NumberOptNumber stdDeviation; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +void sp_gaussianBlur_setDeviation(SPGaussianBlur *blur, float num); +void sp_gaussianBlur_setDeviation(SPGaussianBlur *blur, float num, float optnum); + +#endif /* !SP_GAUSSIANBLUR_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/image.cpp b/src/object/filters/image.cpp new file mode 100644 index 0000000..ea88d81 --- /dev/null +++ b/src/object/filters/image.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "image.h" + +#include + +#include "attributes.h" +#include "enums.h" + +#include "bad-uri-exception.h" + +#include "object/sp-image.h" +#include "object/uri.h" +#include "object/uri-references.h" + +#include "display/nr-filter-image.h" +#include "display/nr-filter.h" + +#include "xml/repr.h" + + +SPFeImage::SPFeImage() : SPFilterPrimitive() { + this->href = nullptr; + this->from_element = false; + this->SVGElemRef = nullptr; + this->SVGElem = nullptr; + + this->aspect_align = SP_ASPECT_XMID_YMID; // Default + this->aspect_clip = SP_ASPECT_MEET; // Default +} + +SPFeImage::~SPFeImage() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeImage variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeImage::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + + this->readAttr( "preserveAspectRatio" ); + this->readAttr( "xlink:href" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeImage::release() { + this->_image_modified_connection.disconnect(); + this->_href_modified_connection.disconnect(); + + if (this->SVGElemRef) { + delete this->SVGElemRef; + } + + SPFilterPrimitive::release(); +} + +static void sp_feImage_elem_modified(SPObject* /*href*/, guint /*flags*/, SPObject* obj) +{ + obj->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +static void sp_feImage_href_modified(SPObject* /*old_elem*/, SPObject* new_elem, SPObject* obj) +{ + SPFeImage *feImage = SP_FEIMAGE(obj); + feImage->_image_modified_connection.disconnect(); + if (new_elem) { + feImage->SVGElem = SP_ITEM(new_elem); + feImage->_image_modified_connection = ((SPObject*) feImage->SVGElem)->connectModified(sigc::bind(sigc::ptr_fun(&sp_feImage_elem_modified), obj)); + } else { + feImage->SVGElem = nullptr; + } + + obj->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Sets a specific value in the SPFeImage. + */ +void SPFeImage::set(SPAttributeEnum key, gchar const *value) { + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_XLINK_HREF: + if (this->href) { + g_free(this->href); + } + this->href = (value) ? g_strdup (value) : nullptr; + if (!this->href) return; + delete this->SVGElemRef; + this->SVGElemRef = nullptr; + this->SVGElem = nullptr; + this->_image_modified_connection.disconnect(); + this->_href_modified_connection.disconnect(); + try{ + Inkscape::URI SVGElem_uri(this->href); + this->SVGElemRef = new Inkscape::URIReference(this->document); + this->SVGElemRef->attach(SVGElem_uri); + this->from_element = true; + this->_href_modified_connection = this->SVGElemRef->changedSignal().connect(sigc::bind(sigc::ptr_fun(&sp_feImage_href_modified), this)); + if (SPObject *elemref = this->SVGElemRef->getObject()) { + this->SVGElem = SP_ITEM(elemref); + this->_image_modified_connection = ((SPObject*) this->SVGElem)->connectModified(sigc::bind(sigc::ptr_fun(&sp_feImage_elem_modified), this)); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } else { + g_warning("SVG element URI was not found in the document while loading this: %s", value); + } + } + // catches either MalformedURIException or UnsupportedURIException + catch(const Inkscape::BadURIException & e) + { + this->from_element = false; + /* This occurs when using external image as the source */ + //g_warning("caught Inkscape::BadURIException in sp_feImage_set"); + break; + } + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + /* Copied from sp-image.cpp */ + /* Do setup before, so we can use break to escape */ + this->aspect_align = SP_ASPECT_XMID_YMID; // Default + this->aspect_clip = SP_ASPECT_MEET; // Default + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + if (value) { + int len; + gchar c[256]; + const gchar *p, *e; + unsigned int align, clip; + p = value; + while (*p && *p == 32) p += 1; + if (!*p) break; + e = p; + while (*e && *e != 32) e += 1; + len = e - p; + if (len > 8) break; + memcpy (c, value, len); + c[len] = 0; + /* Now the actual part */ + if (!strcmp (c, "none")) { + align = SP_ASPECT_NONE; + } else if (!strcmp (c, "xMinYMin")) { + align = SP_ASPECT_XMIN_YMIN; + } else if (!strcmp (c, "xMidYMin")) { + align = SP_ASPECT_XMID_YMIN; + } else if (!strcmp (c, "xMaxYMin")) { + align = SP_ASPECT_XMAX_YMIN; + } else if (!strcmp (c, "xMinYMid")) { + align = SP_ASPECT_XMIN_YMID; + } else if (!strcmp (c, "xMidYMid")) { + align = SP_ASPECT_XMID_YMID; + } else if (!strcmp (c, "xMaxYMid")) { + align = SP_ASPECT_XMAX_YMID; + } else if (!strcmp (c, "xMinYMax")) { + align = SP_ASPECT_XMIN_YMAX; + } else if (!strcmp (c, "xMidYMax")) { + align = SP_ASPECT_XMID_YMAX; + } else if (!strcmp (c, "xMaxYMax")) { + align = SP_ASPECT_XMAX_YMAX; + } else { + g_warning("Illegal preserveAspectRatio: %s", c); + break; + } + clip = SP_ASPECT_MEET; + while (*e && *e == 32) e += 1; + if (*e) { + if (!strcmp (e, "meet")) { + clip = SP_ASPECT_MEET; + } else if (!strcmp (e, "slice")) { + clip = SP_ASPECT_SLICE; + } else { + break; + } + } + this->aspect_align = align; + this->aspect_clip = clip; + } + break; + + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeImage::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeImage::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeImage::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_IMAGE); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterImage *nr_image = dynamic_cast(nr_primitive); + g_assert(nr_image != nullptr); + + this->renderer_common(nr_primitive); + + nr_image->from_element = this->from_element; + nr_image->SVGElem = this->SVGElem; + nr_image->set_align( this->aspect_align ); + nr_image->set_clip( this->aspect_clip ); + nr_image->set_href(this->href); + nr_image->set_document(this->document); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/image.h b/src/object/filters/image.h new file mode 100644 index 0000000..dcd78f3 --- /dev/null +++ b/src/object/filters/image.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG image filter effect + *//* + * Authors: + * Felipe Corrêa da Silva Sanches + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEIMAGE_H_SEEN +#define SP_FEIMAGE_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FEIMAGE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEIMAGE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPItem; + +namespace Inkscape { +class URIReference; +} + +class SPFeImage : public SPFilterPrimitive { +public: + SPFeImage(); + ~SPFeImage() override; + + gchar *href; + + /* preserveAspectRatio */ + unsigned int aspect_align : 4; + unsigned int aspect_clip : 1; + + bool from_element; + SPItem* SVGElem; + Inkscape::URIReference* SVGElemRef; + sigc::connection _image_modified_connection; + sigc::connection _href_modified_connection; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEIMAGE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/merge.cpp b/src/object/filters/merge.cpp new file mode 100644 index 0000000..5fd77c5 --- /dev/null +++ b/src/object/filters/merge.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "svg/svg.h" +#include "xml/repr.h" + +#include "merge.h" +#include "mergenode.h" +#include "display/nr-filter.h" +#include "display/nr-filter-merge.h" + +SPFeMerge::SPFeMerge() : SPFilterPrimitive() { +} + +SPFeMerge::~SPFeMerge() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeMerge variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeMerge::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); +} + +/** + * Drops any allocated memory. + */ +void SPFeMerge::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeMerge. + */ +void SPFeMerge::set(SPAttributeEnum key, gchar const *value) { + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeMerge::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeMerge::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it. And child nodes, too! */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeMerge::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_MERGE); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterMerge *nr_merge = dynamic_cast(nr_primitive); + g_assert(nr_merge != nullptr); + + this->renderer_common(nr_primitive); + + int in_nr = 0; + + for(auto& input: children) { + if (SP_IS_FEMERGENODE(&input)) { + SPFeMergeNode *node = SP_FEMERGENODE(&input); + nr_merge->set_input(in_nr, node->input); + in_nr++; + } + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/merge.h b/src/object/filters/merge.h new file mode 100644 index 0000000..a2c87af --- /dev/null +++ b/src/object/filters/merge.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG merge filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FEMERGE_H_SEEN +#define SP_FEMERGE_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FEMERGE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEMERGE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeMerge : public SPFilterPrimitive { +public: + SPFeMerge(); + ~SPFeMerge() override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEMERGE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/mergenode.cpp b/src/object/filters/mergenode.cpp new file mode 100644 index 0000000..e5e2efc --- /dev/null +++ b/src/object/filters/mergenode.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * feMergeNode implementation. A feMergeNode contains the name of one + * input image for feMerge. + */ +/* + * Authors: + * Kees Cook + * Niko Kiirala + * Abhishek Sharma + * + * Copyright (C) 2004,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "mergenode.h" +#include "merge.h" + +#include "attributes.h" + +#include "display/nr-filter-types.h" + +#include "xml/repr.h" + +SPFeMergeNode::SPFeMergeNode() + : SPObject(), input(Inkscape::Filters::NR_FILTER_SLOT_NOT_SET) { +} + +SPFeMergeNode::~SPFeMergeNode() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeMergeNode variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeMergeNode::build(SPDocument */*document*/, Inkscape::XML::Node */*repr*/) { + this->readAttr( "in" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeMergeNode::release() { + SPObject::release(); +} + +/** + * Sets a specific value in the SPFeMergeNode. + */ +void SPFeMergeNode::set(SPAttributeEnum key, gchar const *value) { + SPFeMerge *parent = SP_FEMERGE(this->parent); + + if (key == SP_ATTR_IN) { + int input = parent->read_in(value); + if (input != this->input) { + this->input = input; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + + /* See if any parents need this value. */ + SPObject::set(key, value); +} + +/** + * Receives update notifications. + */ +void SPFeMergeNode::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeMergeNode::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + // Inkscape-only this, not copied during an "plain SVG" dump: + if (flags & SP_OBJECT_WRITE_EXT) { + if (repr) { + // is this sane? + //repr->mergeFrom(object->getRepr(), "id"); + } else { + repr = this->getRepr()->duplicate(doc); + } + } + + SPObject::write(doc, repr, flags); + + return repr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/mergenode.h b/src/object/filters/mergenode.h new file mode 100644 index 0000000..36a4cc1 --- /dev/null +++ b/src/object/filters/mergenode.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FEMERGENODE_H_SEEN +#define SP_FEMERGENODE_H_SEEN + +/** \file + * feMergeNode implementation. A feMergeNode stores information about one + * input image for feMerge filter primitive. + */ +/* + * Authors: + * Kees Cook + * Niko Kiirala + * + * Copyright (C) 2004,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +#define SP_FEMERGENODE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEMERGENODE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeMergeNode : public SPObject { +public: + SPFeMergeNode(); + ~SPFeMergeNode() override; + + int input; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; +}; + +#endif /* !SP_FEMERGENODE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/morphology.cpp b/src/object/filters/morphology.cpp new file mode 100644 index 0000000..02ae005 --- /dev/null +++ b/src/object/filters/morphology.cpp @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Felipe Sanches + * Hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "attributes.h" +#include "svg/svg.h" +#include "morphology.h" +#include "xml/repr.h" +#include "display/nr-filter.h" + +SPFeMorphology::SPFeMorphology() : SPFilterPrimitive() { + this->Operator = Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; + + //Setting default values: + this->radius.set("0"); +} + +SPFeMorphology::~SPFeMorphology() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeMorphology variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeMorphology::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "operator" ); + this->readAttr( "radius" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeMorphology::release() { + SPFilterPrimitive::release(); +} + +static Inkscape::Filters::FilterMorphologyOperator sp_feMorphology_read_operator(gchar const *value){ + if (!value) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; //erode is default + } + + switch(value[0]){ + case 'e': + if (strncmp(value, "erode", 5) == 0) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; + } + break; + case 'd': + if (strncmp(value, "dilate", 6) == 0) { + return Inkscape::Filters::MORPHOLOGY_OPERATOR_DILATE; + } + break; + } + + return Inkscape::Filters::MORPHOLOGY_OPERATOR_ERODE; //erode is default +} + +/** + * Sets a specific value in the SPFeMorphology. + */ +void SPFeMorphology::set(SPAttributeEnum key, gchar const *value) { + Inkscape::Filters::FilterMorphologyOperator read_operator; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_OPERATOR: + read_operator = sp_feMorphology_read_operator(value); + + if (read_operator != this->Operator){ + this->Operator = read_operator; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_RADIUS: + this->radius.set(value); + + //From SVG spec: If is not provided, it defaults to . + if (this->radius.optNumIsSet() == false) { + this->radius.setOptNumber(this->radius.getNumber()); + } + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } + +} + +/** + * Receives update notifications. + */ +void SPFeMorphology::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeMorphology::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeMorphology::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_MORPHOLOGY); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterMorphology *nr_morphology = dynamic_cast(nr_primitive); + g_assert(nr_morphology != nullptr); + + this->renderer_common(nr_primitive); + + nr_morphology->set_operator(this->Operator); + nr_morphology->set_xradius( this->radius.getNumber() ); + nr_morphology->set_yradius( this->radius.getOptNumber() ); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/morphology.h b/src/object/filters/morphology.h new file mode 100644 index 0000000..94842c4 --- /dev/null +++ b/src/object/filters/morphology.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * @brief SVG morphology filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEMORPHOLOGY_H_SEEN +#define SP_FEMORPHOLOGY_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-morphology.h" + +#define SP_FEMORPHOLOGY(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEMORPHOLOGY(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeMorphology : public SPFilterPrimitive { +public: + SPFeMorphology(); + ~SPFeMorphology() override; + + Inkscape::Filters::FilterMorphologyOperator Operator; + NumberOptNumber radius; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEMORPHOLOGY_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/offset.cpp b/src/object/filters/offset.cpp new file mode 100644 index 0000000..5370348 --- /dev/null +++ b/src/object/filters/offset.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Niko Kiirala + * Abhishek Sharma + * + * Copyright (C) 2006,2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "offset.h" + +#include "attributes.h" +#include "helper-fns.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-offset.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +SPFeOffset::SPFeOffset() : SPFilterPrimitive() { + this->dx = 0; + this->dy = 0; +} + +SPFeOffset::~SPFeOffset() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeOffset variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeOffset::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + this->readAttr( "dx" ); + this->readAttr( "dy" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeOffset::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeOffset. + */ +void SPFeOffset::set(SPAttributeEnum key, gchar const *value) { + double read_num; + + switch(key) { + case SP_ATTR_DX: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->dx) { + this->dx = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_DY: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->dy) { + this->dy = read_num; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeOffset::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + this->readAttr( "dx" ); + this->readAttr( "dy" ); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeOffset::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeOffset::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_OFFSET); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterOffset *nr_offset = dynamic_cast(nr_primitive); + g_assert(nr_offset != nullptr); + + this->renderer_common(nr_primitive); + + nr_offset->set_dx(this->dx); + nr_offset->set_dy(this->dy); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/offset.h b/src/object/filters/offset.h new file mode 100644 index 0000000..7b9febb --- /dev/null +++ b/src/object/filters/offset.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG offset filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FEOFFSET_H_SEEN +#define SP_FEOFFSET_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FEOFFSET(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEOFFSET(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeOffset : public SPFilterPrimitive { +public: + SPFeOffset(); + ~SPFeOffset() override; + + double dx, dy; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FEOFFSET_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/pointlight.cpp b/src/object/filters/pointlight.cpp new file mode 100644 index 0000000..1be7711 --- /dev/null +++ b/src/object/filters/pointlight.cpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// Same directory +#include "pointlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include + +#include "attributes.h" +#include "document.h" + + +#include "xml/node.h" +#include "xml/repr.h" + +SPFePointLight::SPFePointLight() + : SPObject(), x(0), x_set(FALSE), y(0), y_set(FALSE), z(0), z_set(FALSE) { +} + +SPFePointLight::~SPFePointLight() = default; + + +/** + * Reads the Inkscape::XML::Node, and initializes SPPointLight variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFePointLight::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + //Read values of key attributes from XML nodes into object. + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "z" ); + +//is this necessary? + document->addResource("fepointlight", this); +} + +/** + * Drops any allocated memory. + */ +void SPFePointLight::release() { + if ( this->document ) { + // Unregister ourselves + this->document->removeResource("fepointlight", this); + } + +//TODO: release resources here +} + +/** + * Sets a specific value in the SPFePointLight. + */ +void SPFePointLight::set(SPAttributeEnum key, gchar const *value) { + gchar *end_ptr; + + switch (key) { + case SP_ATTR_X: + end_ptr = nullptr; + + if (value) { + this->x = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->x_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->x = 0; + this->x_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_Y: + end_ptr = nullptr; + + if (value) { + this->y = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->y_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->y = 0; + this->y_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_Z: + end_ptr = nullptr; + + if (value) { + this->z = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->z_set = TRUE; + } + } + + if (!value || !end_ptr) { + this->z = 0; + this->z_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + // See if any parents need this value. + SPObject::set(key, value); + break; + } +} + +/** + * * Receives update notifications. + * */ +void SPFePointLight::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "z" ); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFePointLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + if (this->x_set) + sp_repr_set_css_double(repr, "x", this->x); + if (this->y_set) + sp_repr_set_css_double(repr, "y", this->y); + if (this->z_set) + sp_repr_set_css_double(repr, "z", this->z); + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/pointlight.h b/src/object/filters/pointlight.h new file mode 100644 index 0000000..6eb374b --- /dev/null +++ b/src/object/filters/pointlight.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation, see sp-filter.cpp. + */ +#ifndef SP_FEPOINTLIGHT_H_SEEN +#define SP_FEPOINTLIGHT_H_SEEN + +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +#define SP_FEPOINTLIGHT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FEPOINTLIGHT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFePointLight : public SPObject { +public: + SPFePointLight(); + ~SPFePointLight() override; + + /** x coordinate of the light source */ + float x; + unsigned int x_set : 1; + /** y coordinate of the light source */ + float y; + unsigned int y_set : 1; + /** z coordinate of the light source */ + float z; + unsigned int z_set : 1; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SP_FEPOINTLIGHT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/sp-filter-primitive.cpp b/src/object/filters/sp-filter-primitive.cpp new file mode 100644 index 0000000..5d4c78b --- /dev/null +++ b/src/object/filters/sp-filter-primitive.cpp @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Superclass for all the filter primitives + * + */ +/* + * Authors: + * Kees Cook + * Niko Kiirala + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "sp-filter-primitive.h" + +#include "attributes.h" + +#include "display/nr-filter-primitive.h" + +#include "style.h" + + +// CPPIFY: Make pure virtual. +//void SPFilterPrimitive::build_renderer(Inkscape::Filters::Filter* filter) { +// throw; +//} + +SPFilterPrimitive::SPFilterPrimitive() : SPObject() { + this->image_in = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + this->image_out = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + + // We must keep track if a value is set or not, if not set then the region defaults to 0%, 0%, + // 100%, 100% ("x", "y", "width", "height") of the -> filter <- region. If set then + // percentages are in terms of bounding box or viewbox, depending on value of "primitiveUnits" + + // NB: SVGLength.set takes prescaled percent values: 1 means 100% + this->x.unset(SVGLength::PERCENT, 0, 0); + this->y.unset(SVGLength::PERCENT, 0, 0); + this->width.unset(SVGLength::PERCENT, 1, 0); + this->height.unset(SVGLength::PERCENT, 1, 0); +} + +SPFilterPrimitive::~SPFilterPrimitive() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFilterPrimitive variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFilterPrimitive::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive* object = this; + + object->readAttr( "style" ); // struct not derived from SPItem, we need to do this ourselves. + object->readAttr( "in" ); + object->readAttr( "result" ); + object->readAttr( "x" ); + object->readAttr( "y" ); + object->readAttr( "width" ); + object->readAttr( "height" ); + + SPObject::build(document, repr); +} + +/** + * Drops any allocated memory. + */ +void SPFilterPrimitive::release() { + SPObject::release(); +} + +/** + * Sets a specific value in the SPFilterPrimitive. + */ +void SPFilterPrimitive::set(SPAttributeEnum key, gchar const *value) { + + int image_nr; + switch (key) { + case SP_ATTR_IN: + if (value) { + image_nr = this->read_in(value); + } else { + image_nr = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + } + if (image_nr != this->image_in) { + this->image_in = image_nr; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_RESULT: + if (value) { + image_nr = this->read_result(value); + } else { + image_nr = Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + } + if (image_nr != this->image_out) { + this->image_out = image_nr; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + + /* Filter primitive sub-region */ + case SP_ATTR_X: + this->x.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_Y: + this->y.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_WIDTH: + this->width.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_HEIGHT: + this->height.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + /* See if any parents need this value. */ + SPObject::set(key, value); +} + +/** + * Receives update notifications. + */ +void SPFilterPrimitive::update(SPCtx *ctx, guint flags) { + + SPItemCtx *ictx = (SPItemCtx *) ctx; + + // Do here since we know viewport (Bounding box case handled during rendering) + SPFilter *parent = SP_FILTER(this->parent); + + if( parent->primitiveUnits == SP_FILTER_UNITS_USERSPACEONUSE ) { + this->calcDimsFromParentViewport(ictx, true); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFilterPrimitive::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + SPFilterPrimitive* object = this; + + SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(object); + SPFilter *parent = SP_FILTER(object->parent); + + if (!repr) { + repr = object->getRepr()->duplicate(doc); + } + + gchar const *in_name = parent->name_for_image(prim->image_in); + repr->setAttribute("in", in_name); + + gchar const *out_name = parent->name_for_image(prim->image_out); + repr->setAttribute("result", out_name); + + /* Do we need to add x,y,width,height? */ + SPObject::write(doc, repr, flags); + + return repr; +} + +int SPFilterPrimitive::read_in(gchar const *name) +{ + if (!name){ + return Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; + } + // TODO: are these case sensitive or not? (assumed yes) + switch (name[0]) { + case 'S': + if (strcmp(name, "SourceGraphic") == 0) + return Inkscape::Filters::NR_FILTER_SOURCEGRAPHIC; + if (strcmp(name, "SourceAlpha") == 0) + return Inkscape::Filters::NR_FILTER_SOURCEALPHA; + if (strcmp(name, "StrokePaint") == 0) + return Inkscape::Filters::NR_FILTER_STROKEPAINT; + break; + case 'B': + if (strcmp(name, "BackgroundImage") == 0) + return Inkscape::Filters::NR_FILTER_BACKGROUNDIMAGE; + if (strcmp(name, "BackgroundAlpha") == 0) + return Inkscape::Filters::NR_FILTER_BACKGROUNDALPHA; + break; + case 'F': + if (strcmp(name, "FillPaint") == 0) + return Inkscape::Filters::NR_FILTER_FILLPAINT; + break; + } + + SPFilter *parent = SP_FILTER(this->parent); + int ret = parent->get_image_name(name); + if (ret >= 0) return ret; + + return Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +int SPFilterPrimitive::read_result(gchar const *name) +{ + SPFilter *parent = SP_FILTER(this->parent); + int ret = parent->get_image_name(name); + if (ret >= 0) return ret; + + ret = parent->set_image_name(name); + if (ret >= 0) return ret; + + return Inkscape::Filters::NR_FILTER_SLOT_NOT_SET; +} + +/** + * Gives name for output of previous filter. Makes things clearer when 'this' + * is a filter with two or more inputs. Returns the slot number of result + * of previous primitive, or NR_FILTER_SOURCEGRAPHIC if this is the first + * primitive. + */ +int SPFilterPrimitive::name_previous_out() { + SPFilter *parent = SP_FILTER(this->parent); + SPObject *i = parent->firstChild(); + while (i && i->getNext() != this) { + i = i->getNext(); + } + if (i) { + SPFilterPrimitive *i_prim = SP_FILTER_PRIMITIVE(i); + if (i_prim->image_out < 0) { + Glib::ustring name = parent->get_new_result_name(); + int slot = parent->set_image_name(name.c_str()); + i_prim->image_out = slot; + //XML Tree is being directly used while it shouldn't be. + i_prim->setAttributeOrRemoveIfEmpty("result", name); + return slot; + } else { + return i_prim->image_out; + } + } + return Inkscape::Filters::NR_FILTER_SOURCEGRAPHIC; +} + +/* Common initialization for filter primitives */ +void SPFilterPrimitive::renderer_common(Inkscape::Filters::FilterPrimitive *nr_prim) +{ + g_assert(nr_prim != nullptr); + + + nr_prim->set_input(this->image_in); + nr_prim->set_output(this->image_out); + + /* TODO: place here code to handle input images, filter area etc. */ + // We don't know current viewport or bounding box, this is wrong approach. + nr_prim->set_subregion( this->x, this->y, this->width, this->height ); + + // Give renderer access to filter properties + nr_prim->setStyle( this->style ); +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/sp-filter-primitive.h b/src/object/filters/sp-filter-primitive.h new file mode 100644 index 0000000..ae33a02 --- /dev/null +++ b/src/object/filters/sp-filter-primitive.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_FILTER_PRIMITIVE_H +#define SEEN_SP_FILTER_PRIMITIVE_H + +/** \file + * Document level base class for all SVG filter primitives. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "../sp-object.h" +#include "../sp-dimensions.h" + +#define SP_FILTER_PRIMITIVE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FILTER_PRIMITIVE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +namespace Inkscape { +namespace Filters { +class Filter; +class FilterPrimitive; +} } + +class SPFilterPrimitive : public SPObject, public SPDimensions { +public: + SPFilterPrimitive(); + ~SPFilterPrimitive() override; + + int image_in, image_out; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +public: + virtual void build_renderer(Inkscape::Filters::Filter* filter) = 0; + + /* Common initialization for filter primitives */ + void renderer_common(Inkscape::Filters::FilterPrimitive *nr_prim); + + int name_previous_out(); + int read_in(char const *name); + int read_result(char const *name); +}; + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/specularlighting.cpp b/src/object/filters/specularlighting.cpp new file mode 100644 index 0000000..52af6ee --- /dev/null +++ b/src/object/filters/specularlighting.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * Jean-Rene Reinhard + * Abhishek Sharma + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// Same directory +#include "specularlighting.h" +#include "distantlight.h" +#include "pointlight.h" +#include "spotlight.h" + +#include "attributes.h" +#include "strneq.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-specularlighting.h" + +#include "object/sp-object.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/svg-icc-color.h" + +#include "xml/repr.h" + +/* FeSpecularLighting base class */ +static void sp_feSpecularLighting_children_modified(SPFeSpecularLighting *sp_specularlighting); + +SPFeSpecularLighting::SPFeSpecularLighting() : SPFilterPrimitive() { + this->surfaceScale = 1; + this->specularConstant = 1; + this->specularExponent = 1; + this->lighting_color = 0xffffffff; + this->icc = nullptr; + + //TODO kernelUnit + this->renderer = nullptr; + + this->surfaceScale_set = FALSE; + this->specularConstant_set = FALSE; + this->specularExponent_set = FALSE; + this->lighting_color_set = FALSE; +} + +SPFeSpecularLighting::~SPFeSpecularLighting() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeSpecularLighting variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeSpecularLighting::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "surfaceScale" ); + this->readAttr( "specularConstant" ); + this->readAttr( "specularExponent" ); + this->readAttr( "kernelUnitLength" ); + this->readAttr( "lighting-color" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeSpecularLighting::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeSpecularLighting. + */ +void SPFeSpecularLighting::set(SPAttributeEnum key, gchar const *value) { + gchar const *cend_ptr = nullptr; + gchar *end_ptr = nullptr; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ +//TODO test forbidden values + case SP_ATTR_SURFACESCALE: + end_ptr = nullptr; + if (value) { + this->surfaceScale = g_ascii_strtod(value, &end_ptr); + if (end_ptr) { + this->surfaceScale_set = TRUE; + } else { + g_warning("this: surfaceScale should be a number ... defaulting to 1"); + } + + } + //if the attribute is not set or has an unreadable value + if (!value || !end_ptr) { + this->surfaceScale = 1; + this->surfaceScale_set = FALSE; + } + if (this->renderer) { + this->renderer->surfaceScale = this->surfaceScale; + } + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SPECULARCONSTANT: + end_ptr = nullptr; + if (value) { + this->specularConstant = g_ascii_strtod(value, &end_ptr); + if (end_ptr && this->specularConstant >= 0) { + this->specularConstant_set = TRUE; + } else { + end_ptr = nullptr; + g_warning("this: specularConstant should be a positive number ... defaulting to 1"); + } + } + if (!value || !end_ptr) { + this->specularConstant = 1; + this->specularConstant_set = FALSE; + } + if (this->renderer) { + this->renderer->specularConstant = this->specularConstant; + } + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SPECULAREXPONENT: + end_ptr = nullptr; + if (value) { + this->specularExponent = g_ascii_strtod(value, &end_ptr); + if (this->specularExponent >= 1 && this->specularExponent <= 128) { + this->specularExponent_set = TRUE; + } else { + end_ptr = nullptr; + g_warning("this: specularExponent should be a number in range [1, 128] ... defaulting to 1"); + } + } + if (!value || !end_ptr) { + this->specularExponent = 1; + this->specularExponent_set = FALSE; + } + if (this->renderer) { + this->renderer->specularExponent = this->specularExponent; + } + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_KERNELUNITLENGTH: + //TODO kernelUnit + //this->kernelUnitLength.set(value); + /*TODOif (feSpecularLighting->renderer) { + feSpecularLighting->renderer->surfaceScale = feSpecularLighting->renderer; + } + */ + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_PROP_LIGHTING_COLOR: + cend_ptr = nullptr; + this->lighting_color = sp_svg_read_color(value, &cend_ptr, 0xffffffff); + //if a value was read + if (cend_ptr) { + while (g_ascii_isspace(*cend_ptr)) { + ++cend_ptr; + } + if (strneq(cend_ptr, "icc-color(", 10)) { + if (!this->icc) this->icc = new SVGICCColor(); + if ( ! sp_svg_read_icc_color( cend_ptr, this->icc ) ) { + delete this->icc; + this->icc = nullptr; + } + } + this->lighting_color_set = TRUE; + } else { + //lighting_color already contains the default value + this->lighting_color_set = FALSE; + } + if (this->renderer) { + this->renderer->lighting_color = this->lighting_color; + } + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeSpecularLighting::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr( "surfaceScale" ); + this->readAttr( "specularConstant" ); + this->readAttr( "specularExponent" ); + this->readAttr( "kernelUnitLength" ); + this->readAttr( "lighting-color" ); + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeSpecularLighting::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values _and children_ into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + //repr = doc->createElement("svg:feSpecularLighting"); + } + + if (this->surfaceScale_set) { + sp_repr_set_css_double(repr, "surfaceScale", this->surfaceScale); + } + + if (this->specularConstant_set) { + sp_repr_set_css_double(repr, "specularConstant", this->specularConstant); + } + + if (this->specularExponent_set) { + sp_repr_set_css_double(repr, "specularExponent", this->specularExponent); + } + + /*TODO kernelUnits */ + if (this->lighting_color_set) { + gchar c[64]; + sp_svg_write_color(c, sizeof(c), this->lighting_color); + repr->setAttribute("lighting-color", c); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +/** + * Callback for child_added event. + */ +void SPFeSpecularLighting::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPFilterPrimitive::child_added(child, ref); + + sp_feSpecularLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPFeSpecularLighting::remove_child(Inkscape::XML::Node *child) { + SPFilterPrimitive::remove_child(child); + + sp_feSpecularLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFeSpecularLighting::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) { + SPFilterPrimitive::order_changed(child, old_ref, new_ref); + + sp_feSpecularLighting_children_modified(this); + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +static void sp_feSpecularLighting_children_modified(SPFeSpecularLighting *sp_specularlighting) { + if (sp_specularlighting->renderer) { + sp_specularlighting->renderer->light_type = Inkscape::Filters::NO_LIGHT; + + if (SP_IS_FEDISTANTLIGHT(sp_specularlighting->firstChild())) { + sp_specularlighting->renderer->light_type = Inkscape::Filters::DISTANT_LIGHT; + sp_specularlighting->renderer->light.distant = SP_FEDISTANTLIGHT(sp_specularlighting->firstChild()); + } + + if (SP_IS_FEPOINTLIGHT(sp_specularlighting->firstChild())) { + sp_specularlighting->renderer->light_type = Inkscape::Filters::POINT_LIGHT; + sp_specularlighting->renderer->light.point = SP_FEPOINTLIGHT(sp_specularlighting->firstChild()); + } + + if (SP_IS_FESPOTLIGHT(sp_specularlighting->firstChild())) { + sp_specularlighting->renderer->light_type = Inkscape::Filters::SPOT_LIGHT; + sp_specularlighting->renderer->light.spot = SP_FESPOTLIGHT(sp_specularlighting->firstChild()); + } + } +} + +void SPFeSpecularLighting::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_SPECULARLIGHTING); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterSpecularLighting *nr_specularlighting = dynamic_cast(nr_primitive); + g_assert(nr_specularlighting != nullptr); + + this->renderer = nr_specularlighting; + this->renderer_common(nr_primitive); + + nr_specularlighting->specularConstant = this->specularConstant; + nr_specularlighting->specularExponent = this->specularExponent; + nr_specularlighting->surfaceScale = this->surfaceScale; + nr_specularlighting->lighting_color = this->lighting_color; + nr_specularlighting->set_icc(this->icc); + + //We assume there is at most one child + nr_specularlighting->light_type = Inkscape::Filters::NO_LIGHT; + + if (SP_IS_FEDISTANTLIGHT(this->firstChild())) { + nr_specularlighting->light_type = Inkscape::Filters::DISTANT_LIGHT; + nr_specularlighting->light.distant = SP_FEDISTANTLIGHT(this->firstChild()); + } + + if (SP_IS_FEPOINTLIGHT(this->firstChild())) { + nr_specularlighting->light_type = Inkscape::Filters::POINT_LIGHT; + nr_specularlighting->light.point = SP_FEPOINTLIGHT(this->firstChild()); + } + + if (SP_IS_FESPOTLIGHT(this->firstChild())) { + nr_specularlighting->light_type = Inkscape::Filters::SPOT_LIGHT; + nr_specularlighting->light.spot = SP_FESPOTLIGHT(this->firstChild()); + } + + //nr_offset->set_dx(sp_offset->dx); + //nr_offset->set_dy(sp_offset->dy); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/specularlighting.h b/src/object/filters/specularlighting.h new file mode 100644 index 0000000..427cbce --- /dev/null +++ b/src/object/filters/specularlighting.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG specular lighting filter effect + *//* + * Authors: + * Hugo Rodrigues + * Jean-Rene Reinhard + * + * Copyright (C) 2006 Hugo Rodrigues + * 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FESPECULARLIGHTING_H_SEEN +#define SP_FESPECULARLIGHTING_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" + +#define SP_FESPECULARLIGHTING(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FESPECULARLIGHTING(obj) (dynamic_cast((SPObject*)obj) != NULL) + +struct SVGICCColor; + +namespace Inkscape { +namespace Filters { +class FilterSpecularLighting; +} +} + +class SPFeSpecularLighting : public SPFilterPrimitive { +public: + SPFeSpecularLighting(); + ~SPFeSpecularLighting() override; + + gfloat surfaceScale; + guint surfaceScale_set : 1; + gfloat specularConstant; + guint specularConstant_set : 1; + gfloat specularExponent; + guint specularExponent_set : 1; + NumberOptNumber kernelUnitLength; + guint32 lighting_color; + guint lighting_color_set : 1; + SVGICCColor *icc; + + Inkscape::Filters::FilterSpecularLighting *renderer; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old_repr, Inkscape::XML::Node* new_repr) override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FESPECULARLIGHTING_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/spotlight.cpp b/src/object/filters/spotlight.cpp new file mode 100644 index 0000000..dcc71b7 --- /dev/null +++ b/src/object/filters/spotlight.cpp @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// Same directory +#include "spotlight.h" +#include "diffuselighting.h" +#include "specularlighting.h" + +#include "attributes.h" +#include "document.h" + +#include "xml/repr.h" + +SPFeSpotLight::SPFeSpotLight() + : SPObject(), x(0), x_set(FALSE), y(0), y_set(FALSE), z(0), z_set(FALSE), pointsAtX(0), pointsAtX_set(FALSE), + pointsAtY(0), pointsAtY_set(FALSE), pointsAtZ(0), pointsAtZ_set(FALSE), + specularExponent(1), specularExponent_set(FALSE), limitingConeAngle(90), + limitingConeAngle_set(FALSE) +{ +} + +SPFeSpotLight::~SPFeSpotLight() = default; + + +/** + * Reads the Inkscape::XML::Node, and initializes SPPointLight variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeSpotLight::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + //Read values of key attributes from XML nodes into object. + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "z" ); + this->readAttr( "pointsAtX" ); + this->readAttr( "pointsAtY" ); + this->readAttr( "pointsAtZ" ); + this->readAttr( "specularExponent" ); + this->readAttr( "limitingConeAngle" ); + +//is this necessary? + document->addResource("fespotlight", this); +} + +/** + * Drops any allocated memory. + */ +void SPFeSpotLight::release() { + if ( this->document ) { + // Unregister ourselves + this->document->removeResource("fespotlight", this); + } + +//TODO: release resources here +} + +/** + * Sets a specific value in the SPFeSpotLight. + */ +void SPFeSpotLight::set(SPAttributeEnum key, gchar const *value) { + gchar *end_ptr; + + switch (key) { + case SP_ATTR_X: + end_ptr = nullptr; + + if (value) { + this->x = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->x_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->x = 0; + this->x_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_Y: + end_ptr = nullptr; + + if (value) { + this->y = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->y_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->y = 0; + this->y_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_Z: + end_ptr = nullptr; + + if (value) { + this->z = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->z_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->z = 0; + this->z_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_POINTSATX: + end_ptr = nullptr; + + if (value) { + this->pointsAtX = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->pointsAtX_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->pointsAtX = 0; + this->pointsAtX_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_POINTSATY: + end_ptr = nullptr; + + if (value) { + this->pointsAtY = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->pointsAtY_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->pointsAtY = 0; + this->pointsAtY_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_POINTSATZ: + end_ptr = nullptr; + + if (value) { + this->pointsAtZ = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->pointsAtZ_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->pointsAtZ = 0; + this->pointsAtZ_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_SPECULAREXPONENT: + end_ptr = nullptr; + + if (value) { + this->specularExponent = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->specularExponent_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->specularExponent = 1; + this->specularExponent_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_LIMITINGCONEANGLE: + end_ptr = nullptr; + + if (value) { + this->limitingConeAngle = g_ascii_strtod(value, &end_ptr); + + if (end_ptr) { + this->limitingConeAngle_set = TRUE; + } + } + + if(!value || !end_ptr) { + this->limitingConeAngle = 90; + this->limitingConeAngle_set = FALSE; + } + + if (this->parent && + (SP_IS_FEDIFFUSELIGHTING(this->parent) || + SP_IS_FESPECULARLIGHTING(this->parent))) { + this->parent->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + // See if any parents need this value. + SPObject::set(key, value); + break; + } +} + +/** + * * Receives update notifications. + * */ +void SPFeSpotLight::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "z" ); + this->readAttr( "pointsAtX" ); + this->readAttr( "pointsAtY" ); + this->readAttr( "pointsAtZ" ); + this->readAttr( "specularExponent" ); + this->readAttr( "limitingConeAngle" ); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeSpotLight::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + if (this->x_set) + sp_repr_set_css_double(repr, "x", this->x); + if (this->y_set) + sp_repr_set_css_double(repr, "y", this->y); + if (this->z_set) + sp_repr_set_css_double(repr, "z", this->z); + if (this->pointsAtX_set) + sp_repr_set_css_double(repr, "pointsAtX", this->pointsAtX); + if (this->pointsAtY_set) + sp_repr_set_css_double(repr, "pointsAtY", this->pointsAtY); + if (this->pointsAtZ_set) + sp_repr_set_css_double(repr, "pointsAtZ", this->pointsAtZ); + if (this->specularExponent_set) + sp_repr_set_css_double(repr, "specularExponent", this->specularExponent); + if (this->limitingConeAngle_set) + sp_repr_set_css_double(repr, "limitingConeAngle", this->limitingConeAngle); + + SPObject::write(doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/spotlight.h b/src/object/filters/spotlight.h new file mode 100644 index 0000000..2169124 --- /dev/null +++ b/src/object/filters/spotlight.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FESPOTLIGHT_H_SEEN +#define SP_FESPOTLIGHT_H_SEEN + +/** \file + * SVG implementation, see sp-filter.cpp. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jean-Rene Reinhard + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-object.h" + +#define SP_FESPOTLIGHT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FESPOTLIGHT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFeSpotLight : public SPObject { +public: + SPFeSpotLight(); + ~SPFeSpotLight() override; + + /** x coordinate of the light source */ + float x; + unsigned int x_set : 1; + /** y coordinate of the light source */ + float y; + unsigned int y_set : 1; + /** z coordinate of the light source */ + float z; + unsigned int z_set : 1; + /** x coordinate of the point the source is pointing at */ + float pointsAtX; + unsigned int pointsAtX_set : 1; + /** y coordinate of the point the source is pointing at */ + float pointsAtY; + unsigned int pointsAtY_set : 1; + /** z coordinate of the point the source is pointing at */ + float pointsAtZ; + unsigned int pointsAtZ_set : 1; + /** specular exponent (focus of the light) */ + float specularExponent; + unsigned int specularExponent_set : 1; + /** limiting cone angle */ + float limitingConeAngle; + unsigned int limitingConeAngle_set : 1; + //other fields + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SP_FESPOTLIGHT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/tile.cpp b/src/object/filters/tile.cpp new file mode 100644 index 0000000..b8a22b8 --- /dev/null +++ b/src/object/filters/tile.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tile.h" + +#include "attributes.h" + +#include "display/nr-filter.h" +#include "display/nr-filter-tile.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +SPFeTile::SPFeTile() : SPFilterPrimitive() { +} + +SPFeTile::~SPFeTile() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeTile variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeTile::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); +} + +/** + * Drops any allocated memory. + */ +void SPFeTile::release() { + SPFilterPrimitive::release(); +} + +/** + * Sets a specific value in the SPFeTile. + */ +void SPFeTile::set(SPAttributeEnum key, gchar const *value) { + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeTile::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeTile::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + return repr; +} + +void SPFeTile::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_TILE); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterTile *nr_tile = dynamic_cast(nr_primitive); + g_assert(nr_tile != nullptr); + + this->renderer_common(nr_primitive); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/tile.h b/src/object/filters/tile.h new file mode 100644 index 0000000..1f18c61 --- /dev/null +++ b/src/object/filters/tile.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG tile filter effect + *//* + * Authors: + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FETILE_H_SEEN +#define SP_FETILE_H_SEEN + +#include "sp-filter-primitive.h" + +#define SP_FETILE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FETILE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/* FeTile base class */ +class SPFeTile : public SPFilterPrimitive { +public: + SPFeTile(); + ~SPFeTile() override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FETILE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/turbulence.cpp b/src/object/filters/turbulence.cpp new file mode 100644 index 0000000..d1221f4 --- /dev/null +++ b/src/object/filters/turbulence.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + * + */ +/* + * Authors: + * Felipe Corrêa da Silva Sanches + * hugo Rodrigues + * Abhishek Sharma + * + * Copyright (C) 2007 Felipe Sanches + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "svg/svg.h" +#include "turbulence.h" +#include "helper-fns.h" +#include "xml/repr.h" + +#include "display/nr-filter.h" + +SPFeTurbulence::SPFeTurbulence() : SPFilterPrimitive() { + this->stitchTiles = false; + this->seed = 0; + this->numOctaves = 0; + this->type = Inkscape::Filters::TURBULENCE_FRACTALNOISE; + + this->updated=false; +} + +SPFeTurbulence::~SPFeTurbulence() = default; + +/** + * Reads the Inkscape::XML::Node, and initializes SPFeTurbulence variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFeTurbulence::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPFilterPrimitive::build(document, repr); + + /*LOAD ATTRIBUTES FROM REPR HERE*/ + this->readAttr( "baseFrequency" ); + this->readAttr( "numOctaves" ); + this->readAttr( "seed" ); + this->readAttr( "stitchTiles" ); + this->readAttr( "type" ); +} + +/** + * Drops any allocated memory. + */ +void SPFeTurbulence::release() { + SPFilterPrimitive::release(); +} + +static bool sp_feTurbulence_read_stitchTiles(gchar const *value){ + if (!value) { + return false; // 'noStitch' is default + } + + switch(value[0]){ + case 's': + if (strncmp(value, "stitch", 6) == 0) { + return true; + } + break; + case 'n': + if (strncmp(value, "noStitch", 8) == 0) { + return false; + } + break; + } + + return false; // 'noStitch' is default +} + +static Inkscape::Filters::FilterTurbulenceType sp_feTurbulence_read_type(gchar const *value){ + if (!value) { + return Inkscape::Filters::TURBULENCE_TURBULENCE; // 'turbulence' is default + } + + switch(value[0]){ + case 'f': + if (strncmp(value, "fractalNoise", 12) == 0) { + return Inkscape::Filters::TURBULENCE_FRACTALNOISE; + } + break; + case 't': + if (strncmp(value, "turbulence", 10) == 0) { + return Inkscape::Filters::TURBULENCE_TURBULENCE; + } + break; + } + + return Inkscape::Filters::TURBULENCE_TURBULENCE; // 'turbulence' is default +} + +/** + * Sets a specific value in the SPFeTurbulence. + */ +void SPFeTurbulence::set(SPAttributeEnum key, gchar const *value) { + int read_int; + double read_num; + bool read_bool; + Inkscape::Filters::FilterTurbulenceType read_type; + + switch(key) { + /*DEAL WITH SETTING ATTRIBUTES HERE*/ + case SP_ATTR_BASEFREQUENCY: + this->baseFrequency.set(value); + + // From SVG spec: If two s are provided, the first number represents + // a base frequency in the X direction and the second value represents a base + // frequency in the Y direction. If one number is provided, then that value is + // used for both X and Y. + if (this->baseFrequency.optNumIsSet() == false) { + this->baseFrequency.setOptNumber(this->baseFrequency.getNumber()); + } + + this->updated = false; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_NUMOCTAVES: + read_int = value ? (int)floor(helperfns_read_number(value)) : 1; + + if (read_int != this->numOctaves){ + this->numOctaves = read_int; + this->updated = false; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_SEED: + read_num = value ? helperfns_read_number(value) : 0; + + if (read_num != this->seed){ + this->seed = read_num; + this->updated = false; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_STITCHTILES: + read_bool = sp_feTurbulence_read_stitchTiles(value); + + if (read_bool != this->stitchTiles){ + this->stitchTiles = read_bool; + this->updated = false; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + case SP_ATTR_TYPE: + read_type = sp_feTurbulence_read_type(value); + + if (read_type != this->type){ + this->type = read_type; + this->updated = false; + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + default: + SPFilterPrimitive::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFeTurbulence::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + + SPFilterPrimitive::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFeTurbulence::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + /* TODO: Don't just clone, but create a new repr node and write all + * relevant values into it */ + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPFilterPrimitive::write(doc, repr, flags); + + /* turbulence doesn't take input */ + repr->removeAttribute("in"); + + return repr; +} + +void SPFeTurbulence::build_renderer(Inkscape::Filters::Filter* filter) { + g_assert(filter != nullptr); + + int primitive_n = filter->add_primitive(Inkscape::Filters::NR_FILTER_TURBULENCE); + Inkscape::Filters::FilterPrimitive *nr_primitive = filter->get_primitive(primitive_n); + Inkscape::Filters::FilterTurbulence *nr_turbulence = dynamic_cast(nr_primitive); + g_assert(nr_turbulence != nullptr); + + this->renderer_common(nr_primitive); + + nr_turbulence->set_baseFrequency(0, this->baseFrequency.getNumber()); + nr_turbulence->set_baseFrequency(1, this->baseFrequency.getOptNumber()); + nr_turbulence->set_numOctaves(this->numOctaves); + nr_turbulence->set_seed(this->seed); + nr_turbulence->set_stitchTiles(this->stitchTiles); + nr_turbulence->set_type(this->type); + nr_turbulence->set_updated(this->updated); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/filters/turbulence.h b/src/object/filters/turbulence.h new file mode 100644 index 0000000..c43e322 --- /dev/null +++ b/src/object/filters/turbulence.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG turbulence filter effect + *//* + * Authors: + * Felipe Corrêa da Silva Sanches + * Hugo Rodrigues + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FETURBULENCE_H_SEEN +#define SP_FETURBULENCE_H_SEEN + +#include "sp-filter-primitive.h" +#include "number-opt-number.h" +#include "display/nr-filter-turbulence.h" + +#define SP_FETURBULENCE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FETURBULENCE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/* FeTurbulence base class */ + +class SPFeTurbulence : public SPFilterPrimitive { +public: + SPFeTurbulence(); + ~SPFeTurbulence() override; + + /** TURBULENCE ATTRIBUTES HERE */ + NumberOptNumber baseFrequency; + int numOctaves; + double seed; + bool stitchTiles; + Inkscape::Filters::FilterTurbulenceType type; + SVGLength x, y, height, width; + bool updated; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const gchar* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + void build_renderer(Inkscape::Filters::Filter* filter) override; +}; + +#endif /* !SP_FETURBULENCE_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/object-set.cpp b/src/object/object-set.cpp new file mode 100644 index 0000000..bf41bb0 --- /dev/null +++ b/src/object/object-set.cpp @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Multiindex container for selection + * + * Authors: + * Adrian Boguszewski + * + * Copyright (C) 2016 Adrian Boguszewski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "object-set.h" +#include "box3d.h" +#include "persp3d.h" +#include "preferences.h" +#include +#include + +namespace Inkscape { + +bool ObjectSet::add(SPObject* object, bool nosignal) { + g_return_val_if_fail(object != nullptr, false); + g_return_val_if_fail(SP_IS_OBJECT(object), false); + + // any ancestor is in the set - do nothing + if (_anyAncestorIsInSet(object)) { + return false; + } + + // very nice function, but changes selection behavior (probably needs new selection option to deal with it) + // check if there is mutual ancestor for some elements, which can replace all of them in the set +// object = _getMutualAncestor(object); + + // remove all descendants from the set + _removeDescendantsFromSet(object); + + _add(object); + if (!nosignal) + _emitSignals(); + return true; +} + +bool ObjectSet::remove(SPObject* object) { + g_return_val_if_fail(object != nullptr, false); + g_return_val_if_fail(SP_IS_OBJECT(object), false); + + // object is the top of subtree + if (includes(object)) { + _remove(object); + _emitSignals(); + return true; + } + + // any ancestor of object is in the set + if (_anyAncestorIsInSet(object)) { + _removeAncestorsFromSet(object); + _emitSignals(); + return true; + } + + // no object nor any parent in the set + return false; +} + +bool ObjectSet::includes(SPObject *object) { + g_return_val_if_fail(object != nullptr, false); + g_return_val_if_fail(SP_IS_OBJECT(object), false); + + return _container.get().find(object) != _container.get().end(); +} + +void ObjectSet::clear() { + _clear(); + _emitSignals(); +} + +int ObjectSet::size() { + return _container.size(); +} + +bool ObjectSet::_anyAncestorIsInSet(SPObject *object) { + SPObject* o = object; + while (o != nullptr) { + if (includes(o)) { + return true; + } + o = o->parent; + } + + return false; +} + +void ObjectSet::_removeDescendantsFromSet(SPObject *object) { + for (auto& child: object->children) { + if (includes(&child)) { + _remove(&child); + // there is certainly no children of this child in the set + continue; + } + + _removeDescendantsFromSet(&child); + } +} + +void ObjectSet::_disconnect(SPObject *object) { + _releaseConnections[object].disconnect(); + _releaseConnections.erase(object); + _remove3DBoxesRecursively(object); + _releaseSignals(object); +} + +void ObjectSet::_remove(SPObject *object) { + _disconnect(object); + _container.get().erase(object); +} + +void ObjectSet::_add(SPObject *object) { + _releaseConnections[object] = object->connectRelease(sigc::hide_return(sigc::mem_fun(*this, &ObjectSet::remove))); + _container.push_back(object); + _add3DBoxesRecursively(object); + _connectSignals(object); +} + +void ObjectSet::_clear() { + for (auto object: _container) + _disconnect(object); + _container.clear(); +} + +SPObject *ObjectSet::_getMutualAncestor(SPObject *object) { + SPObject *o = object; + + bool flag = true; + while (o->parent != nullptr) { + for (auto &child: o->parent->children) { + if(&child != o && !includes(&child)) { + flag = false; + break; + } + } + if (!flag) { + break; + } + o = o->parent; + } + return o; +} + +void ObjectSet::_removeAncestorsFromSet(SPObject *object) { + SPObject* o = object; + while (o->parent != nullptr) { + for (auto &child: o->parent->children) { + if (&child != o) { + _add(&child); + } + } + if (includes(o->parent)) { + _remove(o->parent); + break; + } + o = o->parent; + } +} + +ObjectSet::~ObjectSet() { + _clear(); +} + +void ObjectSet::toggle(SPObject *obj) { + if (includes(obj)) { + remove(obj); + } else { + add(obj); + } +} + +bool ObjectSet::isEmpty() { + return _container.size() == 0; +} + +SPObject *ObjectSet::single() { + return _container.size() == 1 ? *_container.begin() : nullptr; +} + +SPItem *ObjectSet::singleItem() { + if (_container.size() == 1) { + SPObject* obj = *_container.begin(); + if (SP_IS_ITEM(obj)) { + return SP_ITEM(obj); + } + } + + return nullptr; +} + +SPItem *ObjectSet::smallestItem(CompareSize compare) { + return _sizeistItem(true, compare); +} + +SPItem *ObjectSet::largestItem(CompareSize compare) { + return _sizeistItem(false, compare); +} + +SPItem *ObjectSet::_sizeistItem(bool sml, CompareSize compare) { + auto items = this->items(); + gdouble max = sml ? 1e18 : 0; + SPItem *ist = nullptr; + + for (auto i = items.begin(); i != items.end(); ++i) { + Geom::OptRect obox = SP_ITEM(*i)->documentPreferredBounds(); + if (!obox || obox.empty()) { + continue; + } + + Geom::Rect bbox = *obox; + + gdouble size = compare == AREA ? bbox.area() : + (compare == VERTICAL ? bbox.height() : bbox.width()); + size = sml ? size : size * -1; + if (size < max) { + max = size; + ist = SP_ITEM(*i); + } + } + + return ist; +} + +SPObjectRange ObjectSet::objects() { + return SPObjectRange(_container.get().begin(), _container.get().end()); +} + +Inkscape::XML::Node *ObjectSet::singleRepr() { + SPObject *obj = single(); + return obj ? obj->getRepr() : nullptr; +} + +void ObjectSet::set(SPObject *object, bool persist_selection_context) { + _clear(); + _add(object); + if(dynamic_cast(this)) + return dynamic_cast(this)->_emitChanged(persist_selection_context); +} + +void ObjectSet::setReprList(std::vector const &list) { + if(!document()) + return; + clear(); + for (auto iter = list.rbegin(); iter != list.rend(); ++iter) { + SPObject *obj = document()->getObjectById((*iter)->attribute("id")); + if (obj) { + add(obj, true); + } + } + _emitSignals(); + if(dynamic_cast(this)) + return dynamic_cast(this)->_emitChanged();// +} + + + +Geom::OptRect ObjectSet::bounds(SPItem::BBoxType type) const +{ + return (type == SPItem::GEOMETRIC_BBOX) ? + geometricBounds() : visualBounds(); +} + +Geom::OptRect ObjectSet::geometricBounds() const +{ + auto items = const_cast(this)->items(); + + Geom::OptRect bbox; + for (auto iter = items.begin(); iter != items.end(); ++iter) { + bbox.unionWith(SP_ITEM(*iter)->desktopGeometricBounds()); + } + return bbox; +} + +Geom::OptRect ObjectSet::visualBounds() const +{ + auto items = const_cast(this)->items(); + + Geom::OptRect bbox; + for (auto iter = items.begin(); iter != items.end(); ++iter) { + bbox.unionWith(SP_ITEM(*iter)->desktopVisualBounds()); + } + return bbox; +} + +Geom::OptRect ObjectSet::preferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return bounds(SPItem::VISUAL_BBOX); + } else { + return bounds(SPItem::GEOMETRIC_BBOX); + } +} + +Geom::OptRect ObjectSet::documentBounds(SPItem::BBoxType type) const +{ + Geom::OptRect bbox; + auto items = const_cast(this)->items(); + if (items.empty()) return bbox; + + for (auto iter = items.begin(); iter != items.end(); ++iter) { + SPItem *item = SP_ITEM(*iter); + bbox |= item->documentBounds(type); + } + + return bbox; +} + +// If we have a selection of multiple items, then the center of the first item +// will be returned; this is also the case in SelTrans::centerRequest() +boost::optional ObjectSet::center() const { + auto items = const_cast(this)->items(); + if (!items.empty()) { + SPItem *first = items.back(); // from the first item in selection + if (first->isCenterSet()) { // only if set explicitly + return first->getCenter(); + } + } + Geom::OptRect bbox = preferredBounds(); + if (bbox) { + return bbox->midpoint(); + } else { + return boost::optional(); + } +} + +std::list const ObjectSet::perspList() { + std::list pl; + for (auto & _3dboxe : _3dboxes) { + Persp3D *persp = box3d_get_perspective(_3dboxe); + if (std::find(pl.begin(), pl.end(), persp) == pl.end()) + pl.push_back(persp); + } + return pl; +} + +std::list const ObjectSet::box3DList(Persp3D *persp) { + std::list boxes; + if (persp) { + for (auto box : _3dboxes) { + if (persp == box3d_get_perspective(box)) { + boxes.push_back(box); + } + } + } else { + boxes = _3dboxes; + } + return boxes; +} + +void ObjectSet::_add3DBoxesRecursively(SPObject *obj) { + std::list boxes = box3d_extract_boxes(obj); + + for (auto box : boxes) { + _3dboxes.push_back(box); + } +} + +void ObjectSet::_remove3DBoxesRecursively(SPObject *obj) { + std::list boxes = box3d_extract_boxes(obj); + + for (auto box : boxes) { + std::list::iterator b = std::find(_3dboxes.begin(), _3dboxes.end(), box); + if (b == _3dboxes.end()) { + g_print ("Warning! Trying to remove unselected box from selection.\n"); + return; + } + _3dboxes.erase(b); + } +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/object-set.h b/src/object/object-set.h new file mode 100644 index 0000000..66a3b76 --- /dev/null +++ b/src/object/object-set.h @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Multiindex container for selection + * + * Authors: + * Adrian Boguszewski + * Marc Jeanmougin + * + * Copyright (C) 2016 Adrian Boguszewski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PROTOTYPE_OBJECTSET_H +#define INKSCAPE_PROTOTYPE_OBJECTSET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sp-object.h" +#include "sp-item.h" +#include "sp-item-group.h" +#include "desktop.h" +#include "document.h" +#include "verbs.h" + +enum BoolOpErrors { + DONE, + DONE_NO_PATH, + DONE_NO_ACTION, + ERR_TOO_LESS_PATHS_1, + ERR_TOO_LESS_PATHS_2, + ERR_NO_PATHS, + ERR_Z_ORDER +}; + +// boolean operation +enum bool_op +{ + bool_op_union, // A OR B + bool_op_inters, // A AND B + bool_op_diff, // A \ B + bool_op_symdiff, // A XOR B + bool_op_cut, // coupure (pleines) + bool_op_slice // coupure (contour) +}; +typedef enum bool_op BooleanOp; + +class SPBox3D; +class Persp3D; + +namespace Inkscape { + +namespace XML { +class Node; +} + +struct hashed{}; +struct random_access{}; + +struct is_item { + bool operator()(SPObject* obj) { + return SP_IS_ITEM(obj); + } +}; + +struct is_group { + bool operator()(SPObject* obj) { + return SP_IS_GROUP(obj); + } +}; + +struct object_to_item { + typedef SPItem* result_type; + SPItem* operator()(SPObject* obj) const { + return SP_ITEM(obj); + } +}; + +struct object_to_node { + typedef XML::Node* result_type; + XML::Node* operator()(SPObject* obj) const { + return obj->getRepr(); + } +}; + +struct object_to_group { + typedef SPGroup* result_type; + SPGroup* operator()(SPObject* obj) const { + return SP_GROUP(obj); + } +}; + +typedef boost::multi_index_container< + SPObject*, + boost::multi_index::indexed_by< + boost::multi_index::sequenced<>, + boost::multi_index::random_access< + boost::multi_index::tag>, + boost::multi_index::hashed_unique< + boost::multi_index::tag, + boost::multi_index::identity> + >> MultiIndexContainer; + +typedef boost::any_range< + SPObject*, + boost::random_access_traversal_tag, + SPObject* const&, + std::ptrdiff_t> SPObjectRange; + +class ObjectSet { +public: + enum CompareSize {HORIZONTAL, VERTICAL, AREA}; + typedef decltype(MultiIndexContainer().get() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_item())) SPItemRange; + typedef decltype(MultiIndexContainer().get() | boost::adaptors::filtered(is_group()) | boost::adaptors::transformed(object_to_group())) SPGroupRange; + typedef decltype(MultiIndexContainer().get() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_node())) XMLNodeRange; + + ObjectSet(SPDesktop* desktop): _desktop(desktop) { + if (desktop) + _document = desktop->getDocument(); + }; + ObjectSet(SPDocument* doc): _desktop(nullptr), _document(doc) {}; + ObjectSet(): _desktop(nullptr), _document(nullptr) {}; + virtual ~ObjectSet(); + + void setDocument(SPDocument* doc){ + _document = doc; + } + + + /** + * Add an SPObject to the set of selected objects. + * + * @param obj the SPObject to add + * @param nosignal true if no signals should be sent + */ + bool add(SPObject* object, bool nosignal = false); + + /** + * Add an XML node's SPObject to the set of selected objects. + * + * @param the xml node of the item to add + */ + void add(XML::Node *repr) { + if(document() && repr) + add(document()->getObjectById(repr->attribute("id"))); + } + + /** Add items from an STL iterator range to the selection. + * \param from the begin iterator + * \param to the end iterator + */ + template + void add(InputIterator from, InputIterator to) { + for(auto it = from; it != to; ++it) { + _add(*it); + } + _emitSignals(); + } + + /** + * Removes an item from the set of selected objects. + * + * It is ok to call this method for an unselected item. + * + * @param item the item to unselect + * + * @return is success + */ + bool remove(SPObject* object); + + /** + * Returns true if the given object is selected. + */ + bool includes(SPObject *object); + + /** + * Set the selection to a single specific object. + * + * @param obj the object to select + */ + void set(SPObject *object, bool persist_selection_context = false); + void set(XML::Node *repr) { + if(document() && repr) + set(document()->getObjectById(repr->attribute("id"))); + } + /** + * Unselects all selected objects. + */ + void clear(); + + /** + * Returns size of the selection. + */ + int size(); + + /** + * Returns true if no items are selected. + */ + bool isEmpty(); + + /** + * Removes an item if selected, adds otherwise. + * + * @param item the item to unselect + */ + void toggle(SPObject *obj); + + /** + * Returns a single selected object. + * + * @return NULL unless exactly one object is selected + */ + SPObject *single(); + + /** + * Returns a single selected item. + * + * @return NULL unless exactly one object is selected + */ + SPItem *singleItem(); + + /** + * Returns the smallest item from this selection. + */ + SPItem *smallestItem(CompareSize compare); + + /** + * Returns the largest item from this selection. + */ + SPItem *largestItem(CompareSize compare); + + /** Returns the list of selected objects. */ + SPObjectRange objects(); + + /** Returns a range of selected SPItems. */ + SPItemRange items() { + return SPItemRange(_container.get() + | boost::adaptors::filtered(is_item()) + | boost::adaptors::transformed(object_to_item())); + }; + + /** Returns a range of selected groups. */ + SPGroupRange groups() { + return SPGroupRange (_container.get() + | boost::adaptors::filtered(is_group()) + | boost::adaptors::transformed(object_to_group())); + } + + /** Returns a range of the xml nodes of all selected objects. */ + XMLNodeRange xmlNodes() { + return XMLNodeRange(_container.get() + | boost::adaptors::filtered(is_item()) + | boost::adaptors::transformed(object_to_node())); + } + + /** + * Returns a single selected object's xml node. + * + * @return NULL unless exactly one object is selected + */ + XML::Node *singleRepr(); + + /** + * Selects exactly the specified objects. + * + * @param objs the objects to select + */ + template + typename boost::enable_if, void>::type + setList(const std::vector &objs) { + _clear(); + addList(objs); + } + + /** + * Selects exactly the specified objects. + * + * @param list the repr list to add + */ + void setReprList(std::vector const &list); + + /** + * Adds the specified objects to selection, without deselecting first. + * + * @param objs the objects to select + */ + template + typename boost::enable_if, void>::type + addList(const std::vector &objs) { + for (auto obj: objs) { + if (!includes(obj)) { + add(obj, true); + } + } + _emitSignals(); + } + + /** Returns the bounding rectangle of the selection. */ + Geom::OptRect bounds(SPItem::BBoxType type) const; + Geom::OptRect visualBounds() const; + Geom::OptRect geometricBounds() const; + + /** + * Returns either the visual or geometric bounding rectangle of the selection, based on the + * preferences specified for the selector tool + */ + Geom::OptRect preferredBounds() const; + + /* Returns the bounding rectangle of the selectionin document coordinates.*/ + Geom::OptRect documentBounds(SPItem::BBoxType type) const; + + /** + * Returns the rotation/skew center of the selection. + */ + boost::optional center() const; + + /** Returns a list of all perspectives which have a 3D box in the current selection. + (these may also be nested in groups) */ + std::list const perspList(); + + /** + * Returns a list of all 3D boxes in the current selection which are associated to @c + * persp. If @c pers is @c NULL, return all selected boxes. + */ + std::list const box3DList(Persp3D *persp = nullptr); + + /** + * Returns the desktop the selection is bound to + * + * @return the desktop the selection is bound to, or NULL if in console mode + */ + SPDesktop *desktop() { return _desktop; } + + /** + * Returns the document the selection is bound to + * + * @return the document the selection is bound to, or NULL if in console mode + */ + SPDocument *document() { return _document; } + + //item groups operations + //in selection-chemistry.cpp + void deleteItems(); + void duplicate(bool suppressDone = false, bool duplicateLayer = false); + void clone(); + + /** + * @brief Unlink all directly selected clones. + * @param skip_undo If this is set to true the call to DocumentUndo::done is omitted. + * @return True if anything was unlinked, otherwise false. + */ + bool unlink(const bool skip_undo = false); + /** + * @brief Recursively unlink any clones present in the current selection, + * including clones which are used to clip other objects, groups of clones etc. + * @return true if anything was unlinked, otherwise false. + */ + bool unlinkRecursive(const bool skip_undo = false, const bool force = false); + void removeLPESRecursive(bool keep_paths); + void relink(); + void cloneOriginal(); + void cloneOriginalPathLPE(bool allow_transforms = false); + Inkscape::XML::Node* group(); + void popFromGroup(); + void ungroup(); + + //z-order management + //in selection-chemistry.cpp + void stackUp(bool skip_undo = false); + void raise(bool skip_undo = false); + void raiseToTop(bool skip_undo = false); + void stackDown(bool skip_undo = false); + void lower(bool skip_undo = false); + void lowerToBottom(bool skip_undo = false); + void toNextLayer(bool skip_undo = false); + void toPrevLayer(bool skip_undo = false); + void toLayer(SPObject *layer, bool skip_undo = false); + + //clipboard management + //in selection-chemistry.cpp + void copy(); + void cut(); + void pasteStyle(); + void pasteSize(bool apply_x, bool apply_y); + void pasteSizeSeparately(bool apply_x, bool apply_y); + void pastePathEffect(); + + //path operations + //in path-chemistry.cpp + void combine(bool skip_undo = false); + void breakApart(bool skip_undo = false); + void toCurves(bool skip_undo = false); + void toLPEItems(); + void pathReverse(); + + // Boolean operations + // in splivarot.cpp + bool pathUnion(const bool skip_undo = false); + bool pathIntersect(const bool skip_undo = false); + bool pathDiff(const bool skip_undo = false); + bool pathSymDiff(const bool skip_undo = false); + bool pathCut(const bool skip_undo = false); + bool pathSlice(const bool skip_undo = false); + + //Other path operations + //in selection-chemistry.cpp + void toMarker(bool apply = true); + void toGuides(); + void toSymbol(); + void unSymbol(); + void tile(bool apply = true); //"Object to Pattern" + void untile(); + void createBitmapCopy(); + void setMask(bool apply_clip_path, bool apply_to_layer = false, bool skip_undo = false); + void editMask(bool clip); + void unsetMask(const bool apply_clip_path, const bool skip_undo = false); + void setClipGroup(); + + // moves + // in selection-chemistry.cpp + void removeLPE(); + void removeFilter(); + void applyAffine(Geom::Affine const &affine, bool set_i2d=true,bool compensate=true, bool adjust_transf_center=true); + void removeTransform(); + void setScaleAbsolute(double, double, double, double); + void setScaleRelative(const Geom::Point&, const Geom::Scale&); + void rotateRelative(const Geom::Point&, double); + void skewRelative(const Geom::Point&, double, double); + void moveRelative(const Geom::Point &move, bool compensate = true); + void moveRelative(double dx, double dy); + void rotate90(bool ccw); + void rotate(double); + void rotateScreen(double); + void scale(double); + void scaleScreen(double); + void scaleTimes(double); + void move(double dx, double dy); + void moveScreen(double dx, double dy); + + // various + void getExportHints(Glib::ustring &filename, float *xdpi, float *ydpi); + bool fitCanvas(bool with_margins, bool skip_undo = false); + void swapFillStroke(); + +protected: + virtual void _connectSignals(SPObject* object) {}; + virtual void _releaseSignals(SPObject* object) {}; + virtual void _emitSignals() {}; + void _add(SPObject* object); + void _clear(); + void _remove(SPObject* object); + bool _anyAncestorIsInSet(SPObject *object); + void _removeDescendantsFromSet(SPObject *object); + void _removeAncestorsFromSet(SPObject *object); + SPItem *_sizeistItem(bool sml, CompareSize compare); + SPObject *_getMutualAncestor(SPObject *object); + virtual void _add3DBoxesRecursively(SPObject *obj); + virtual void _remove3DBoxesRecursively(SPObject *obj); + + MultiIndexContainer _container; + GC::soft_ptr _desktop; + GC::soft_ptr _document; + std::list _3dboxes; + std::unordered_map _releaseConnections; + +private: + BoolOpErrors pathBoolOp(bool_op bop, const bool skip_undo, const bool checked = false, const unsigned int verb = SP_VERB_NONE, const Glib::ustring description = ""); + void _disconnect(SPObject* object); + +}; + +typedef ObjectSet::SPItemRange SPItemRange; +typedef ObjectSet::SPGroupRange SPGroupRange; +typedef ObjectSet::XMLNodeRange XMLNodeRange; + +} // namespace Inkscape + +#endif //INKSCAPE_PROTOTYPE_OBJECTSET_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/persp3d-reference.cpp b/src/object/persp3d-reference.cpp new file mode 100644 index 0000000..294c62d --- /dev/null +++ b/src/object/persp3d-reference.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to the inkscape:perspectiveID attribute + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2007 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "persp3d-reference.h" +#include "uri.h" + +static void persp3dreference_href_changed(SPObject *old_ref, SPObject *ref, Persp3DReference *persp3dref); +static void persp3dreference_delete_self(SPObject *deleted, Persp3DReference *persp3dref); +static void persp3dreference_source_modified(SPObject *iSource, guint flags, Persp3DReference *persp3dref); + +Persp3DReference::Persp3DReference(SPObject* i_owner) : URIReference(i_owner) +{ + owner=i_owner; + persp_href = nullptr; + persp_repr = nullptr; + persp = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(persp3dreference_href_changed), this)); // listening to myself, this should be virtual instead +} + +Persp3DReference::~Persp3DReference() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +bool +Persp3DReference::_acceptObject(SPObject *obj) const +{ + return SP_IS_PERSP3D(obj) && URIReference::_acceptObject(obj); +; + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + +void +Persp3DReference::unlink() +{ + g_free(persp_href); + persp_href = nullptr; + detach(); +} + +void +Persp3DReference::start_listening(Persp3D* to) +{ + if ( to == nullptr ) { + return; + } + persp = to; + persp_repr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&persp3dreference_delete_self), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&persp3dreference_source_modified), this)); +} + +void +Persp3DReference::quit_listening() +{ + if ( persp == nullptr ) { + return; + } + _modified_connection.disconnect(); + _delete_connection.disconnect(); + persp_repr = nullptr; + persp = nullptr; +} + +static void +persp3dreference_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, Persp3DReference *persp3dref) +{ + persp3dref->quit_listening(); + Persp3D *refobj = SP_PERSP3D(persp3dref->getObject()); + if ( refobj ) { + persp3dref->start_listening(refobj); + } + + persp3dref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +persp3dreference_delete_self(SPObject */*deleted*/, Persp3DReference *persp3dref) +{ + g_return_if_fail(persp3dref->owner); + persp3dref->owner->deleteObject(); +} + +static void +persp3dreference_source_modified(SPObject */*iSource*/, guint /*flags*/, Persp3DReference *persp3dref) +{ + persp3dref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/persp3d-reference.h b/src/object/persp3d-reference.h new file mode 100644 index 0000000..8d254dd --- /dev/null +++ b/src/object/persp3d-reference.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PERSP3D_REFERENCE_H +#define SEEN_PERSP3D_REFERENCE_H + +/* + * The reference corresponding to the inkscape:perspectiveID attribute + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2007 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "uri-references.h" +#include "persp3d.h" + +class SPObject; + +namespace Inkscape { +namespace XML { +class Node; +} +} + +class Persp3DReference : public Inkscape::URIReference { +public: + Persp3DReference(SPObject *obj); + ~Persp3DReference() override; + + Persp3D *getObject() const { + return SP_PERSP3D(URIReference::getObject()); + } + + SPObject *owner; + + // concerning the Persp3D (we only use SPBox3D) that is referred to: + char *persp_href; + Inkscape::XML::Node *persp_repr; + Persp3D *persp; + + sigc::connection _changed_connection; + sigc::connection _modified_connection; + sigc::connection _delete_connection; + + void link(char* to); + void unlink(); + void start_listening(Persp3D* to); + void quit_listening(); + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + + +#endif /* !SEEN_PERSP3D_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/persp3d.cpp b/src/object/persp3d.cpp new file mode 100644 index 0000000..e13b6d0 --- /dev/null +++ b/src/object/persp3d.cpp @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Class modelling a 3D perspective as an SPObject + * + * Authors: + * Maximilian Albert + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "persp3d.h" +#include "perspective-line.h" +#include "sp-root.h" +#include "sp-defs.h" + +#include "attributes.h" +#include "document-undo.h" +#include "vanishing-point.h" +#include "ui/tools/box3d-tool.h" +#include "svg/stringstream.h" +#include "xml/node-event-vector.h" +#include "desktop.h" + +#include +#include "verbs.h" +#include "util/units.h" + +using Inkscape::DocumentUndo; + +static void persp3d_on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data); + +static int global_counter = 0; + +/* Constructor/destructor for the internal class */ + +Persp3DImpl::Persp3DImpl() : + tmat (Proj::TransfMat3x4 ()), + document (nullptr) +{ + my_counter = global_counter++; +} + +static Inkscape::XML::NodeEventVector const persp3d_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + persp3d_on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +Persp3D::Persp3D() : SPObject() { + this->perspective_impl = new Persp3DImpl(); +} + +Persp3D::~Persp3D() = default; + + +/** + * Virtual build: set persp3d attributes from its associated XML node. + */ +void Persp3D::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr( "inkscape:vp_x" ); + this->readAttr( "inkscape:vp_y" ); + this->readAttr( "inkscape:vp_z" ); + this->readAttr( "inkscape:persp3d-origin" ); + + if (repr) { + repr->addListener (&persp3d_repr_events, this); + } +} + +/** + * Virtual release of Persp3D members before destruction. + */ +void Persp3D::release() { + delete this->perspective_impl; + this->getRepr()->removeListenerByData(this); +} + +/** + * Apply viewBox and legacy desktop transformation to point loaded from SVG + */ +static Proj::Pt2 legacy_transform_forward(Proj::Pt2 pt, SPDocument const *doc) { + // Read values are in 'user units'. + auto root = doc->getRoot(); + if (root->viewBox_set) { + pt[0] *= root->width.computed / root->viewBox.width(); + pt[1] *= root->height.computed / root->viewBox.height(); + } + + // stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + pt[1] *= -1; + if (pt[2]) { + pt[1] += doc->getHeight().value("px"); + } + } + + return pt; +} + +/** + * Apply viewBox and legacy desktop transformation to point to be written to SVG + */ +static Proj::Pt2 legacy_transform_backward(Proj::Pt2 pt, SPDocument const *doc) { + // stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + pt[1] *= -1; + if (pt[2]) { + pt[1] += doc->getHeight().value("px"); + } + } + + // Written values are in 'user units'. + auto root = doc->getRoot(); + if (root->viewBox_set) { + pt[0] *= root->viewBox.width() / root->width.computed; + pt[1] *= root->viewBox.height() / root->height.computed; + } + + return pt; +} + +/** + * Virtual set: set attribute to value. + */ +// FIXME: Currently we only read the finite positions of vanishing points; +// should we move VPs into their own repr (as it's done for SPStop, e.g.)? +void Persp3D::set(SPAttributeEnum key, gchar const *value) { + + switch (key) { + case SP_ATTR_INKSCAPE_PERSP3D_VP_X: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::X, ptn ); + } + break; + } + case SP_ATTR_INKSCAPE_PERSP3D_VP_Y: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::Y, ptn ); + } + break; + } + case SP_ATTR_INKSCAPE_PERSP3D_VP_Z: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::Z, ptn ); + } + break; + } + case SP_ATTR_INKSCAPE_PERSP3D_ORIGIN: { + if (value) { + Proj::Pt2 pt (value); + Proj::Pt2 ptn = legacy_transform_forward(pt, document); + perspective_impl->tmat.set_image_pt( Proj::W, ptn ); + } + break; + } + default: { + SPObject::set(key, value); + break; + } + } + + // FIXME: Is this the right place for resetting the draggers? + Inkscape::UI::Tools::ToolBase *ec = INKSCAPE.active_event_context(); + if (SP_IS_BOX3D_CONTEXT(ec)) { + Inkscape::UI::Tools::Box3dTool *bc = SP_BOX3D_CONTEXT(ec); + bc->_vpdrag->updateDraggers(); + bc->_vpdrag->updateLines(); + bc->_vpdrag->updateBoxHandles(); + bc->_vpdrag->updateBoxReprs(); + } +} + +void Persp3D::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* TODO: Should we update anything here? */ + + } + + SPObject::update(ctx, flags); +} + +Persp3D *persp3d_create_xml_element(SPDocument *document, Persp3DImpl *dup) {// if dup is given, copy the attributes over + SPDefs *defs = document->getDefs(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr; + + /* if no perspective is given, create a default one */ + repr = xml_doc->createElement("inkscape:perspective"); + repr->setAttribute("sodipodi:type", "inkscape:persp3d"); + + // Use 'user-units' + double width = document->getWidth().value("px"); + double height = document->getHeight().value("px"); + if( document->getRoot()->viewBox_set ) { + Geom::Rect vb = document->getRoot()->viewBox; + width = vb.width(); + height = vb.height(); + } + + Proj::Pt2 proj_vp_x = Proj::Pt2 (0.0, height/2.0, 1.0); + Proj::Pt2 proj_vp_y = Proj::Pt2 (0.0, 1000.0, 0.0); + Proj::Pt2 proj_vp_z = Proj::Pt2 (width, height/2.0, 1.0); + Proj::Pt2 proj_origin = Proj::Pt2 (width/2.0, height/3.0, 1.0 ); + + if (dup) { + proj_vp_x = dup->tmat.column (Proj::X); + proj_vp_y = dup->tmat.column (Proj::Y); + proj_vp_z = dup->tmat.column (Proj::Z); + proj_origin = dup->tmat.column (Proj::W); + } + + gchar *str = nullptr; + str = proj_vp_x.coord_string(); + repr->setAttribute("inkscape:vp_x", str); + g_free (str); + str = proj_vp_y.coord_string(); + repr->setAttribute("inkscape:vp_y", str); + g_free (str); + str = proj_vp_z.coord_string(); + repr->setAttribute("inkscape:vp_z", str); + g_free (str); + str = proj_origin.coord_string(); + repr->setAttribute("inkscape:persp3d-origin", str); + g_free (str); + + /* Append the new persp3d to defs */ + defs->getRepr()->addChild(repr, nullptr); + Inkscape::GC::release(repr); + + return reinterpret_cast( defs->get_child_by_repr(repr) ); +} + +Persp3D *persp3d_document_first_persp(SPDocument *document) +{ + Persp3D *first = nullptr; + for (auto& child: document->getDefs()->children) { + if (SP_IS_PERSP3D(&child)) { + first = SP_PERSP3D(&child); + break; + } + } + return first; +} + +/** + * Virtual write: write object attributes to repr. + */ +Inkscape::XML::Node* Persp3D::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + + if ((flags & SP_OBJECT_WRITE_BUILD & SP_OBJECT_WRITE_EXT) && !repr) { + // this is where we end up when saving as plain SVG (also in other circumstances?); + // hence we don't set the sodipodi:type attribute + repr = xml_doc->createElement("inkscape:perspective"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::X ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_x", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Y ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_y", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::Z ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:vp_z", os.str()); + } + { + Proj::Pt2 pt = perspective_impl->tmat.column( Proj::W ); + Inkscape::SVGOStringStream os; + pt = legacy_transform_backward(pt, document); + os << pt[0] << " : " << pt[1] << " : " << pt[2]; + repr->setAttribute("inkscape:persp3d-origin", os.str()); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* convenience wrapper around persp3d_get_finite_dir() and persp3d_get_infinite_dir() */ +Geom::Point persp3d_get_PL_dir_from_pt (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis) { + if (persp3d_VP_is_finite(persp->perspective_impl, axis)) { + return persp3d_get_finite_dir(persp, pt, axis); + } else { + return persp3d_get_infinite_dir(persp, axis); + } +} + +Geom::Point +persp3d_get_finite_dir (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis) { + Box3D::PerspectiveLine pl(pt, axis, persp); + return pl.direction(); +} + +Geom::Point +persp3d_get_infinite_dir (Persp3D *persp, Proj::Axis axis) { + Proj::Pt2 vp(persp3d_get_VP(persp, axis)); + if (vp[2] != 0.0) { + g_print ("VP should be infinite but is (%f : %f : %f)\n", vp[0], vp[1], vp[2]); + g_return_val_if_fail(vp[2] != 0.0, Geom::Point(0.0, 0.0)); + } + return Geom::Point(vp[0], vp[1]); +} + +double +persp3d_get_infinite_angle (Persp3D *persp, Proj::Axis axis) { + return persp->perspective_impl->tmat.get_infinite_angle(axis); +} + +bool +persp3d_VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis) { + return persp_impl->tmat.has_finite_image(axis); +} + +void +persp3d_toggle_VP (Persp3D *persp, Proj::Axis axis, bool set_undo) { + persp->perspective_impl->tmat.toggle_finite(axis); + // FIXME: Remove this repr update and rely on vp_drag_sel_modified() to do this for us + // On the other hand, vp_drag_sel_modified() would update all boxes; + // here we can confine ourselves to the boxes of this particular perspective. + persp3d_update_box_reprs (persp); + persp->updateRepr(SP_OBJECT_WRITE_EXT); + if (set_undo) { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_3DBOX, + _("Toggle vanishing point")); + } +} + +/* toggle VPs for the same axis in all perspectives of a given list */ +void +persp3d_toggle_VPs (std::list p, Proj::Axis axis) { + for (auto & i : p) { + persp3d_toggle_VP(i, axis, false); + } + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_3DBOX, + _("Toggle multiple vanishing points")); +} + +void +persp3d_set_VP_state (Persp3D *persp, Proj::Axis axis, Proj::VPState state) { + if (persp3d_VP_is_finite(persp->perspective_impl, axis) != (state == Proj::VP_FINITE)) { + persp3d_toggle_VP(persp, axis); + } +} + +void +persp3d_rotate_VP (Persp3D *persp, Proj::Axis axis, double angle, bool alt_pressed) { // angle is in degrees + // FIXME: Most of this functionality should be moved to trans_mat_3x4.(h|cpp) + if (persp->perspective_impl->tmat.has_finite_image(axis)) { + // don't rotate anything for finite VPs + return; + } + Proj::Pt2 v_dir_proj (persp->perspective_impl->tmat.column(axis)); + Geom::Point v_dir (v_dir_proj[0], v_dir_proj[1]); + double a = Geom::atan2 (v_dir) * 180/M_PI; + a += alt_pressed ? 0.5 * ((angle > 0 ) - (angle < 0)) : angle; // the r.h.s. yields +/-0.5 or angle + persp->perspective_impl->tmat.set_infinite_direction (axis, a); + + persp3d_update_box_reprs (persp); + persp->updateRepr(SP_OBJECT_WRITE_EXT); +} + +void +persp3d_apply_affine_transformation (Persp3D *persp, Geom::Affine const &xform) { + persp->perspective_impl->tmat *= xform; + persp3d_update_box_reprs(persp); + persp->updateRepr(SP_OBJECT_WRITE_EXT); +} + +void +persp3d_add_box (Persp3D *persp, SPBox3D *box) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + if (!box) { + return; + } + if (std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box) != persp_impl->boxes.end()) { + return; + } + persp_impl->boxes.push_back(box); +} + +void +persp3d_remove_box (Persp3D *persp, SPBox3D *box) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + std::vector::iterator i = std::find (persp_impl->boxes.begin(), persp_impl->boxes.end(), box); + if (i != persp_impl->boxes.end()) + persp_impl->boxes.erase(i); +} + +bool +persp3d_has_box (Persp3D *persp, SPBox3D *box) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + // FIXME: For some reason, std::find() does not seem to compare pointers "correctly" (or do we need to + // provide a proper comparison function?), so we manually traverse the list. + for (auto & boxe : persp_impl->boxes) { + if (boxe == box) { + return true; + } + } + return false; +} + +void +persp3d_update_box_displays (Persp3D *persp) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + if (persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + box3d_position_set(boxe); + } +} + +void +persp3d_update_box_reprs (Persp3D *persp) { + if (!persp) { + // Hmm, is it an error if this happens? + return; + } + Persp3DImpl *persp_impl = persp->perspective_impl; + + if (persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + boxe->updateRepr(SP_OBJECT_WRITE_EXT); + box3d_set_z_orders(boxe); + } +} + +void +persp3d_update_z_orders (Persp3D *persp) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + if (persp_impl->boxes.empty()) + return; + for (auto & boxe : persp_impl->boxes) { + box3d_set_z_orders(boxe); + } +} + +// FIXME: For some reason we seem to require a vector instead of a list in Persp3D, but in vp_knot_moved_handler() +// we need a list of boxes. If we can store a list in Persp3D right from the start, this function becomes +// obsolete. We should do this. +std::list +persp3d_list_of_boxes(Persp3D *persp) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + std::list bx_lst; + for (auto & boxe : persp_impl->boxes) { + bx_lst.push_back(boxe); + } + return bx_lst; +} + +bool +persp3d_perspectives_coincide(const Persp3D *lhs, const Persp3D *rhs) +{ + return lhs->perspective_impl->tmat == rhs->perspective_impl->tmat; +} + +void +persp3d_absorb(Persp3D *persp1, Persp3D *persp2) { + /* double check if we are called in sane situations */ + g_return_if_fail (persp3d_perspectives_coincide(persp1, persp2) && persp1 != persp2); + + // Note: We first need to copy the boxes of persp2 into a separate list; + // otherwise the loop below gets confused when perspectives are reattached. + std::list boxes_of_persp2 = persp3d_list_of_boxes(persp2); + + for (auto & i : boxes_of_persp2) { + box3d_switch_perspectives(i, persp2, persp1, true); + i->updateRepr(SP_OBJECT_WRITE_EXT); // so that undo/redo can do its job properly + } +} + +static void +persp3d_on_repr_attr_changed ( Inkscape::XML::Node * /*repr*/, + const gchar */*key*/, + const gchar */*oldval*/, + const gchar */*newval*/, + bool /*is_interactive*/, + void * data ) +{ + if (!data) + return; + + Persp3D *persp = (Persp3D*) data; + persp3d_update_box_displays (persp); +} + +/* checks whether all boxes linked to this perspective are currently selected */ +bool +persp3d_has_all_boxes_in_selection (Persp3D *persp, Inkscape::ObjectSet *set) { + Persp3DImpl *persp_impl = persp->perspective_impl; + + std::list selboxes = set->box3DList(); + + for (auto & boxe : persp_impl->boxes) { + if (std::find(selboxes.begin(), selboxes.end(), boxe) == selboxes.end()) { + // we have an unselected box in the perspective + return false; + } + } + return true; +} + +/* some debugging stuff follows */ + +void +persp3d_print_debugging_info (Persp3D *persp) { + Persp3DImpl *persp_impl = persp->perspective_impl; + g_print ("=== Info for Persp3D %d ===\n", persp_impl->my_counter); + gchar * cstr; + for (auto & axe : Proj::axes) { + cstr = persp3d_get_VP(persp, axe).coord_string(); + g_print (" VP %s: %s\n", Proj::string_from_axis(axe), cstr); + g_free(cstr); + } + cstr = persp3d_get_VP(persp, Proj::W).coord_string(); + g_print (" Origin: %s\n", cstr); + g_free(cstr); + + g_print (" Boxes: "); + for (auto & boxe : persp_impl->boxes) { + g_print ("%d (%d) ", boxe->my_counter, box3d_get_perspective(boxe)->perspective_impl->my_counter); + } + g_print ("\n"); + g_print ("========================\n"); +} + +void persp3d_print_debugging_info_all(SPDocument *document) +{ + for (auto& child: document->getDefs()->children) { + if (SP_IS_PERSP3D(&child)) { + persp3d_print_debugging_info(SP_PERSP3D(&child)); + } + } + persp3d_print_all_selected(); +} + +void +persp3d_print_all_selected() { + g_print ("\n======================================\n"); + g_print ("Selected perspectives and their boxes:\n"); + + std::list sel_persps = SP_ACTIVE_DESKTOP->getSelection()->perspList(); + + for (auto & sel_persp : sel_persps) { + Persp3D *persp = SP_PERSP3D(sel_persp); + Persp3DImpl *persp_impl = persp->perspective_impl; + g_print (" %s (%d): ", persp->getRepr()->attribute("id"), persp->perspective_impl->my_counter); + for (auto & boxe : persp_impl->boxes) { + g_print ("%d ", boxe->my_counter); + } + g_print ("\n"); + } + g_print ("======================================\n\n"); + } + +void print_current_persp3d(gchar *func_name, Persp3D *persp) { + g_print ("%s: current_persp3d is now %s\n", + func_name, + persp ? persp->getRepr()->attribute("id") : "NULL"); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/persp3d.h b/src/object/persp3d.h new file mode 100644 index 0000000..6da182c --- /dev/null +++ b/src/object/persp3d.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PERSP3D_H +#define SEEN_PERSP3D_H + +/* + * Implementation of 3D perspectives as SPObjects + * + * Authors: + * Maximilian Albert + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define SP_PERSP3D(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_PERSP3D(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#include +#include +#include + +#include "transf_mat_3x4.h" +#include "document.h" +#include "inkscape.h" // for SP_ACTIVE_DOCUMENT + +#include "sp-object.h" + +class SPBox3D; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class Box3dTool; + +} +} +} + + +class Persp3DImpl { +public: + Persp3DImpl(); + +//private: + Proj::TransfMat3x4 tmat; + + // Also write the list of boxes into the xml repr and vice versa link boxes to their persp3d? + std::vector boxes; + SPDocument *document; + + // for debugging only + int my_counter; + +// friend class Persp3D; +}; + +class Persp3D : public SPObject { +public: + Persp3D(); + ~Persp3D() override; + + Persp3DImpl *perspective_impl; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + + +// FIXME: Make more of these inline! +inline Persp3D * persp3d_get_from_repr (Inkscape::XML::Node *repr) { + return SP_PERSP3D(SP_ACTIVE_DOCUMENT->getObjectByRepr(repr)); +} +inline Proj::Pt2 persp3d_get_VP (Persp3D *persp, Proj::Axis axis) { + return persp->perspective_impl->tmat.column(axis); +} +Geom::Point persp3d_get_PL_dir_from_pt (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis); // convenience wrapper around the following two +Geom::Point persp3d_get_finite_dir (Persp3D *persp, Geom::Point const &pt, Proj::Axis axis); +Geom::Point persp3d_get_infinite_dir (Persp3D *persp, Proj::Axis axis); +double persp3d_get_infinite_angle (Persp3D *persp, Proj::Axis axis); +bool persp3d_VP_is_finite (Persp3DImpl *persp_impl, Proj::Axis axis); +void persp3d_toggle_VP (Persp3D *persp, Proj::Axis axis, bool set_undo = true); +void persp3d_toggle_VPs (std::list, Proj::Axis axis); +void persp3d_set_VP_state (Persp3D *persp, Proj::Axis axis, Proj::VPState state); +void persp3d_rotate_VP (Persp3D *persp, Proj::Axis axis, double angle, bool alt_pressed); // angle is in degrees +void persp3d_apply_affine_transformation (Persp3D *persp, Geom::Affine const &xform); + +void persp3d_add_box (Persp3D *persp, SPBox3D *box); +void persp3d_remove_box (Persp3D *persp, SPBox3D *box); +bool persp3d_has_box (Persp3D *persp, SPBox3D *box); + +void persp3d_update_box_displays (Persp3D *persp); +void persp3d_update_box_reprs (Persp3D *persp); +void persp3d_update_z_orders (Persp3D *persp); +inline unsigned int persp3d_num_boxes (Persp3D *persp) { return persp->perspective_impl->boxes.size(); } +std::list persp3d_list_of_boxes(Persp3D *persp); + +bool persp3d_perspectives_coincide(Persp3D const *lhs, Persp3D const *rhs); +void persp3d_absorb(Persp3D *persp1, Persp3D *persp2); + +Persp3D * persp3d_create_xml_element (SPDocument *document, Persp3DImpl *dup = nullptr); +Persp3D * persp3d_document_first_persp (SPDocument *document); + +bool persp3d_has_all_boxes_in_selection (Persp3D *persp, Inkscape::ObjectSet *set); + +void persp3d_print_debugging_info (Persp3D *persp); +void persp3d_print_debugging_info_all(SPDocument *doc); +void persp3d_print_all_selected(); + +void print_current_persp3d(char *func_name, Persp3D *persp); + +#endif /* __PERSP3D_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-anchor.cpp b/src/object/sp-anchor.cpp new file mode 100644 index 0000000..544e2d3 --- /dev/null +++ b/src/object/sp-anchor.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG element implementation + * + * Author: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 2017 Martin Owens + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_ANCHOR_VERBOSE + +#include +#include "xml/quote.h" +#include "xml/repr.h" +#include "attributes.h" +#include "sp-anchor.h" +#include "ui/view/svg-view-widget.h" +#include "document.h" + +SPAnchor::SPAnchor() : SPGroup() { + this->href = nullptr; + this->type = nullptr; + this->title = nullptr; + this->page = nullptr; +} + +SPAnchor::~SPAnchor() = default; + +void SPAnchor::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGroup::build(document, repr); + + this->readAttr( "xlink:type" ); + this->readAttr( "xlink:role" ); + this->readAttr( "xlink:arcrole" ); + this->readAttr( "xlink:title" ); + this->readAttr( "xlink:show" ); + this->readAttr( "xlink:actuate" ); + this->readAttr( "xlink:href" ); + this->readAttr( "target" ); +} + +void SPAnchor::release() { + if (this->href) { + g_free(this->href); + this->href = nullptr; + } + if (this->type) { + g_free(this->type); + this->type = nullptr; + } + if (this->title) { + g_free(this->title); + this->title = nullptr; + } + if (this->page) { + g_free(this->page); + this->page = nullptr; + } + + SPGroup::release(); +} + +void SPAnchor::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_XLINK_HREF: + g_free(this->href); + this->href = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + this->updatePageAnchor(); + break; + case SP_ATTR_XLINK_TYPE: + g_free(this->type); + this->type = g_strdup(value); + this->updatePageAnchor(); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_XLINK_ROLE: + case SP_ATTR_XLINK_ARCROLE: + case SP_ATTR_XLINK_TITLE: + g_free(this->title); + this->title = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_XLINK_SHOW: + case SP_ATTR_XLINK_ACTUATE: + case SP_ATTR_TARGET: + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +/* + * Detect if this anchor qualifies as a page link and append + * the new page document to this document. + */ +void SPAnchor::updatePageAnchor() { + if (this->type && !strcmp(this->type, "page")) { + if (this->href && !this->page) { + this->page = this->document->createChildDoc(this->href); + } + } +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPAnchor::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:a"); + } + + repr->setAttribute("xlink:href", this->href); + if (this->type) repr->setAttribute("xlink:type", this->type); + if (this->title) repr->setAttribute("xlink:title", this->title); + + if (repr != this->getRepr()) { + // XML Tree being directly used while it shouldn't be in the + // below COPY_ATTR lines + COPY_ATTR(repr, this->getRepr(), "xlink:role"); + COPY_ATTR(repr, this->getRepr(), "xlink:arcrole"); + COPY_ATTR(repr, this->getRepr(), "xlink:show"); + COPY_ATTR(repr, this->getRepr(), "xlink:actuate"); + COPY_ATTR(repr, this->getRepr(), "target"); + } + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPAnchor::displayName() const { + return _("Link"); +} + +gchar* SPAnchor::description() const { + if (this->href) { + char *quoted_href = xml_quote_strdup(this->href); + char *ret = g_strdup_printf(_("to %s"), quoted_href); + g_free(quoted_href); + return ret; + } else { + return g_strdup (_("without URI")); + } +} + +/* fixme: We should forward event to appropriate container/view */ +/* The only use of SPEvent appears to be here, to change the cursor in Inkview when over a link (and + * which hasn't worked since at least 0.48). GUI code should not be here. */ +int SPAnchor::event(SPEvent* event) { + + switch (event->type) { + case SPEvent::ACTIVATE: + if (this->href) { + // If this actually worked, it could be useful to open a webpage with the link. + g_print("Activated xlink:href=\"%s\"\n", this->href); + return TRUE; + } + break; + + case SPEvent::MOUSEOVER: + { + if (event->view) { + event->view->mouseover(); + } + break; + } + + case SPEvent::MOUSEOUT: + { + if (event->view) { + event->view->mouseout(); + } + break; + } + + default: + break; + } + + return FALSE; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-anchor.h b/src/object/sp-anchor.h new file mode 100644 index 0000000..0e88155 --- /dev/null +++ b/src/object/sp-anchor.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ANCHOR_H +#define SEEN_SP_ANCHOR_H + +/* + * SVG element implementation + * + * Author: + * Lauris Kaplinski + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item-group.h" + +#define SP_ANCHOR(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_ANCHOR(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPAnchor : public SPGroup { +public: + SPAnchor(); + ~SPAnchor() override; + + char *href; + char *type; + char *title; + SPDocument *page; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + virtual void updatePageAnchor(); + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + const char* displayName() const override; + char* description() const override; + int event(SPEvent *event) override; +}; + +#endif diff --git a/src/object/sp-clippath.cpp b/src/object/sp-clippath.cpp new file mode 100644 index 0000000..f29c020 --- /dev/null +++ b/src/object/sp-clippath.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2001-2002 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "xml/repr.h" + +#include "enums.h" +#include "attributes.h" +#include "document.h" +#include "style.h" + +#include <2geom/transforms.h> + +#include "sp-clippath.h" +#include "sp-item.h" +#include "sp-defs.h" + +static SPClipPathView* sp_clippath_view_new_prepend(SPClipPathView *list, unsigned int key, Inkscape::DrawingItem *arenaitem); +static SPClipPathView* sp_clippath_view_list_remove(SPClipPathView *list, SPClipPathView *view); + +SPClipPath::SPClipPath() : SPObjectGroup() { + this->clipPathUnits_set = FALSE; + this->clipPathUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + + this->display = nullptr; +} + +SPClipPath::~SPClipPath() = default; + +void SPClipPath::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObjectGroup::build(doc, repr); + + this->readAttr( "style" ); + this->readAttr( "clipPathUnits" ); + + /* Register ourselves */ + doc->addResource("clipPath", this); +} + +void SPClipPath::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("clipPath", this); + } + + while (this->display) { + /* We simply unref and let item manage this in handler */ + this->display = sp_clippath_view_list_remove(this->display, this->display); + } + + SPObjectGroup::release(); +} + +void SPClipPath::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_CLIPPATHUNITS: + this->clipPathUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + this->clipPathUnits_set = FALSE; + + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->clipPathUnits_set = TRUE; + } else if (!strcmp(value, "objectBoundingBox")) { + this->clipPathUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + this->clipPathUnits_set = TRUE; + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + if (SP_ATTRIBUTE_IS_CSS(key)) { + this->style->clear(key); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPObjectGroup::set(key, value); + } + break; + } +} + +void SPClipPath::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + /* Invoke SPObjectGroup implementation */ + SPObjectGroup::child_added(child, ref); + + /* Show new object */ + SPObject *ochild = this->document->getObjectByRepr(child); + + if (SP_IS_ITEM(ochild)) { + for (SPClipPathView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = SP_ITEM(ochild)->invoke_show(v->arenaitem->drawing(), v->key, SP_ITEM_REFERENCE_FLAGS); + + if (ac) { + v->arenaitem->prependChild(ac); + } + } + } +} + +void SPClipPath::update(SPCtx* ctx, unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + + sp_object_unref(child); + } + + for (SPClipPathView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + + if (this->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && v->bbox) { + Geom::Affine t = Geom::Scale(v->bbox->dimensions()); + t.setTranslation(v->bbox->min()); + g->setChildTransform(t); + } else { + g->setChildTransform(Geom::identity()); + } + } +} + +void SPClipPath::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPClipPath::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:clipPath"); + } + + SPObjectGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem *SPClipPath::show(Inkscape::Drawing &drawing, unsigned int key) { + Inkscape::DrawingGroup *ai = new Inkscape::DrawingGroup(drawing); + display = sp_clippath_view_new_prepend(display, key, ai); + + for (auto& child: children) { + if (SP_IS_ITEM(&child)) { + Inkscape::DrawingItem *ac = SP_ITEM(&child)->invoke_show(drawing, key, SP_ITEM_REFERENCE_FLAGS); + + if (ac) { + /* The order is not important in clippath */ + ai->appendChild(ac); + } + } + } + + if (clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && display->bbox) { + Geom::Affine t = Geom::Scale(display->bbox->dimensions()); + t.setTranslation(display->bbox->min()); + ai->setChildTransform(t); + } + + ai->setStyle(this->style); + + return ai; +} + +void SPClipPath::hide(unsigned int key) { + for (auto& child: children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_hide(key); + } + } + for (SPClipPathView *v = display; v != nullptr; v = v->next) { + if (v->key == key) { + /* We simply unref and let item to manage this in handler */ + display = sp_clippath_view_list_remove(display, v); + return; + } + } +} + +void SPClipPath::setBBox(unsigned int key, Geom::OptRect const &bbox) { + for (SPClipPathView *v = display; v != nullptr; v = v->next) { + if (v->key == key) { + v->bbox = bbox; + break; + } + } +} + +Geom::OptRect SPClipPath::geometricBounds(Geom::Affine const &transform) { + Geom::OptRect bbox; + for (auto& i: children) { + if (SP_IS_ITEM(&i)) { + Geom::OptRect tmp = SP_ITEM(&i)->geometricBounds(SP_ITEM(&i)->transform * transform); + bbox.unionWith(tmp); + } + } + return bbox; +} + +/* ClipPath views */ + +SPClipPathView * +sp_clippath_view_new_prepend(SPClipPathView *list, unsigned int key, Inkscape::DrawingItem *arenaitem) +{ + SPClipPathView *new_path_view = g_new(SPClipPathView, 1); + + new_path_view->next = list; + new_path_view->key = key; + new_path_view->arenaitem = arenaitem; + new_path_view->bbox = Geom::OptRect(); + + return new_path_view; +} + +SPClipPathView * +sp_clippath_view_list_remove(SPClipPathView *list, SPClipPathView *view) +{ + if (view == list) { + list = list->next; + } else { + SPClipPathView *prev; + prev = list; + while (prev->next != view) prev = prev->next; + prev->next = view->next; + } + + delete view->arenaitem; + g_free(view); + + return list; +} + +// Create a mask element (using passed elements), add it to +const gchar *SPClipPath::create (std::vector &reprs, SPDocument *document) +{ + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:clipPath"); + repr->setAttribute("clipPathUnits", "userSpaceOnUse"); + + defsrepr->appendChild(repr); + const gchar *id = repr->attribute("id"); + SPObject *clip_path_object = document->getObjectById(id); + + for (auto node : reprs) { + clip_path_object->appendChildRepr(node); + } + + Inkscape::GC::release(repr); + return id; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-clippath.h b/src/object/sp-clippath.h new file mode 100644 index 0000000..87c9b7a --- /dev/null +++ b/src/object/sp-clippath.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CLIPPATH_H +#define SEEN_SP_CLIPPATH_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2001-2002 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "sp-object-group.h" +#include "display/drawing.h" +#include "display/drawing-group.h" +#include "uri-references.h" +#include "xml/node.h" + +#define SP_CLIPPATH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_CLIPPATH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + + +struct SPClipPathView { + SPClipPathView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +}; + +class SPClipPath : public SPObjectGroup { +public: + SPClipPath(); + ~SPClipPath() override; + + class Reference; + + unsigned int clipPathUnits_set : 1; + unsigned int clipPathUnits : 1; + + SPClipPathView *display; + static char const *create(std::vector &reprs, SPDocument *document); + //static GType sp_clippath_get_type(void); + + Inkscape::DrawingItem *show(Inkscape::Drawing &drawing, unsigned int key); + void hide(unsigned int key); + + void setBBox(unsigned int key, Geom::OptRect const &bbox); + Geom::OptRect geometricBounds(Geom::Affine const &transform); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + + +class SPClipPathReference : public Inkscape::URIReference { +public: + SPClipPathReference(SPObject *obj) : URIReference(obj) {} + SPClipPath *getObject() const { + return static_cast(URIReference::getObject()); + } + +protected: + /** + * If the owner element of this reference (the element with <... clippath="...">) + * is a child of the clippath it refers to, return false. + * \return false if obj is not a clippath or if obj is a parent of this + * reference's owner element. True otherwise. + */ + bool _acceptObject(SPObject *obj) const override { + if (!SP_IS_CLIPPATH(obj)) { + return false; + } + SPObject * const owner = this->getOwner(); + if (!URIReference::_acceptObject(obj)) { + //XML Tree being used directly here while it shouldn't be... + Inkscape::XML::Node * const owner_repr = owner->getRepr(); + //XML Tree being used directly here while it shouldn't be... + Inkscape::XML::Node * const obj_repr = obj->getRepr(); + char const * owner_name = ""; + char const * owner_clippath = ""; + char const * obj_name = ""; + char const * obj_id = ""; + if (owner_repr != nullptr) { + owner_name = owner_repr->name(); + owner_clippath = owner_repr->attribute("clippath"); + } + if (obj_repr != nullptr) { + obj_name = obj_repr->name(); + obj_id = obj_repr->attribute("id"); + } + printf("WARNING: Ignoring recursive clippath reference " + "<%s clippath=\"%s\"> in <%s id=\"%s\">", + owner_name, owner_clippath, + obj_name, obj_id); + return false; + } + return true; + } +}; + +#endif // SEEN_SP_CLIPPATH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-conn-end-pair.cpp b/src/object/sp-conn-end-pair.cpp new file mode 100644 index 0000000..5919017 --- /dev/null +++ b/src/object/sp-conn-end-pair.cpp @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class for handling connector endpoint movement and libavoid interaction. + * + * Authors: + * Peter Moulder + * Michael Wybrow + * Abhishek Sharma + * + * * Copyright (C) 2004-2005 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "attributes.h" +#include "sp-conn-end.h" +#include "uri.h" +#include "display/curve.h" +#include "xml/repr.h" +#include "sp-path.h" +#include "3rdparty/adaptagrams/libavoid/router.h" +#include "document.h" +#include "sp-item-group.h" + + +SPConnEndPair::SPConnEndPair(SPPath *const owner) + : _path(owner) + , _connRef(nullptr) + , _connType(SP_CONNECTOR_NOAVOID) + , _connCurvature(0.0) + , _transformed_connection() +{ + for (unsigned handle_ix = 0; handle_ix <= 1; ++handle_ix) { + this->_connEnd[handle_ix] = new SPConnEnd(SP_OBJECT(owner)); + this->_connEnd[handle_ix]->_changed_connection + = this->_connEnd[handle_ix]->ref.changedSignal() + .connect(sigc::bind(sigc::ptr_fun(sp_conn_end_href_changed), + this->_connEnd[handle_ix], owner, handle_ix)); + } +} + +SPConnEndPair::~SPConnEndPair() +{ + for (auto & handle_ix : this->_connEnd) { + delete handle_ix; + handle_ix = nullptr; + } +} + +void SPConnEndPair::release() +{ + for (auto & handle_ix : this->_connEnd) { + handle_ix->_changed_connection.disconnect(); + handle_ix->_delete_connection.disconnect(); + handle_ix->_transformed_connection.disconnect(); + handle_ix->_group_connection.disconnect(); + g_free(handle_ix->href); + handle_ix->href = nullptr; + handle_ix->ref.detach(); + } + + // If the document is being destroyed then the router instance + // and the ConnRefs will have been destroyed with it. + const bool routerInstanceExists = (_path->document->getRouter() != nullptr); + + if (_connRef && routerInstanceExists) { + _connRef->router()->deleteConnector(_connRef); + } + _connRef = nullptr; + + _transformed_connection.disconnect(); +} + +void sp_conn_end_pair_build(SPObject *object) +{ + object->readAttr( "inkscape:connector-type" ); + object->readAttr( "inkscape:connection-start" ); + object->readAttr( "inkscape:connection-end" ); + object->readAttr( "inkscape:connector-curvature" ); +} + + +static void avoid_conn_transformed(Geom::Affine const */*mp*/, SPItem *moved_item) +{ + SPPath *path = SP_PATH(moved_item); + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + + +void SPConnEndPair::setAttr(unsigned const key, gchar const *const value) +{ + switch (key) { + case SP_ATTR_CONNECTOR_TYPE: + if (value && (strcmp(value, "polyline") == 0 || strcmp(value, "orthogonal") == 0)) { + int new_conn_type = strcmp(value, "polyline") ? SP_CONNECTOR_ORTHOGONAL : SP_CONNECTOR_POLYLINE; + + if (!_connRef) { + _connType = new_conn_type; + Avoid::Router *router = _path->document->getRouter(); + _connRef = new Avoid::ConnRef(router); + _connRef->setRoutingType(new_conn_type == SP_CONNECTOR_POLYLINE ? + Avoid::ConnType_PolyLine : Avoid::ConnType_Orthogonal); + _transformed_connection = _path->connectTransformed(sigc::ptr_fun(&avoid_conn_transformed)); + } else if (new_conn_type != _connType) { + _connType = new_conn_type; + _connRef->setRoutingType(new_conn_type == SP_CONNECTOR_POLYLINE ? + Avoid::ConnType_PolyLine : Avoid::ConnType_Orthogonal); + sp_conn_reroute_path(_path); + } + } else { + _connType = SP_CONNECTOR_NOAVOID; + + if (_connRef) { + _connRef->router()->deleteConnector(_connRef); + _connRef = nullptr; + _transformed_connection.disconnect(); + } + } + break; + case SP_ATTR_CONNECTOR_CURVATURE: + if (value) { + _connCurvature = g_strtod(value, nullptr); + if (_connRef && _connRef->isInitialised()) { + // Redraw the connector, but only if it has been initialised. + sp_conn_reroute_path(_path); + } + } + break; + case SP_ATTR_CONNECTION_START: + case SP_ATTR_CONNECTION_END: + this->_connEnd[(key == SP_ATTR_CONNECTION_START ? 0 : 1)]->setAttacherHref(value, _path); + break; + } +} + +void SPConnEndPair::writeRepr(Inkscape::XML::Node *const repr) const +{ + char const * const attr_strs[] = {"inkscape:connection-start", "inkscape:connection-end"}; + for (unsigned handle_ix = 0; handle_ix < 2; ++handle_ix) { + const Inkscape::URI* U = this->_connEnd[handle_ix]->ref.getURI(); + if (U) { + auto str = U->str(); + repr->setAttribute(attr_strs[handle_ix], str); + } + } + if (_connType == SP_CONNECTOR_POLYLINE || _connType == SP_CONNECTOR_ORTHOGONAL) { + repr->setAttribute("inkscape:connector-curvature", Glib::Ascii::dtostr(_connCurvature)); + repr->setAttribute("inkscape:connector-type", _connType == SP_CONNECTOR_POLYLINE ? "polyline" : "orthogonal" ); + } +} + +void SPConnEndPair::getAttachedItems(SPItem *h2attItem[2]) const { + for (unsigned h = 0; h < 2; ++h) { + h2attItem[h] = this->_connEnd[h]->ref.getObject(); + + // Deal with the case of the attached object being an empty group. + // A group containing no items does not have a valid bbox, so + // causes problems for the auto-routing code. Also, since such a + // group no longer has an onscreen representation and can only be + // selected through the XML editor, it makes sense just to detach + // connectors from them. + if (SP_IS_GROUP(h2attItem[h])) { + if (SP_GROUP(h2attItem[h])->getItemCount() == 0) { + // This group is empty, so detach. + sp_conn_end_detach(_path, h); + h2attItem[h] = nullptr; + } + } + } +} + +void SPConnEndPair::getEndpoints(Geom::Point endPts[]) const +{ + SPCurve const *curve = _path->getCurveForEdit(true); + SPItem *h2attItem[2] = {nullptr}; + getAttachedItems(h2attItem); + Geom::Affine i2d = _path->i2doc_affine(); + + for (unsigned h = 0; h < 2; ++h) { + if (h2attItem[h]) { + endPts[h] = h2attItem[h]->getAvoidRef().getConnectionPointPos(); + } else if (!curve->is_empty()) { + if (h == 0) { + endPts[h] = *(curve->first_point()) * i2d; + } else { + endPts[h] = *(curve->last_point()) * i2d; + } + } + } +} + +gdouble SPConnEndPair::getCurvature() const +{ + return _connCurvature; +} + +SPConnEnd** SPConnEndPair::getConnEnds() +{ + return _connEnd; +} + +bool SPConnEndPair::isOrthogonal() const +{ + return _connType == SP_CONNECTOR_ORTHOGONAL; +} + + +static void redrawConnectorCallback(void *ptr) +{ + SPPath *path = SP_PATH(ptr); + if (path->document == nullptr) { + // This can happen when the document is being destroyed. + return; + } + sp_conn_redraw_path(path); +} + +void SPConnEndPair::rerouteFromManipulation() +{ + sp_conn_reroute_path_immediate(_path); +} + + +// Called from SPPath::update to initialise the endpoints. +void SPConnEndPair::update() +{ + if (_connType != SP_CONNECTOR_NOAVOID) { + g_assert(_connRef != nullptr); + if (!_connRef->isInitialised()) { + _updateEndPoints(); + _connRef->setCallback(&redrawConnectorCallback, _path); + } + } +} + +void SPConnEndPair::_updateEndPoints() +{ + Geom::Point endPt[2]; + getEndpoints(endPt); + + Avoid::Point src(endPt[0][Geom::X], endPt[0][Geom::Y]); + Avoid::Point dst(endPt[1][Geom::X], endPt[1][Geom::Y]); + + _connRef->setEndpoints(src, dst); +} + + +bool SPConnEndPair::isAutoRoutingConn() +{ + return _connType != SP_CONNECTOR_NOAVOID; +} + +void SPConnEndPair::makePathInvalid() +{ + g_assert(_connRef != nullptr); + + _connRef->makePathInvalid(); +} + + +// Redraws the curve along the recalculated route +// Straight or curved +void recreateCurve(SPCurve *curve, Avoid::ConnRef *connRef, const gdouble curvature) +{ + g_assert(connRef != nullptr); + + bool straight = curvature<1e-3; + + Avoid::PolyLine route = connRef->displayRoute(); + if (!straight) route = route.curvedPolyline(curvature); + connRef->calcRouteDist(); + + curve->reset(); + + curve->moveto( Geom::Point(route.ps[0].x, route.ps[0].y) ); + int pn = route.size(); + for (int i = 1; i < pn; ++i) { + Geom::Point p(route.ps[i].x, route.ps[i].y); + if (straight) { + curve->lineto( p ); + } else { + switch (route.ts[i]) { + case 'M': + curve->moveto( p ); + break; + case 'L': + curve->lineto( p ); + break; + case 'C': + g_assert( i+2curveto( p, Geom::Point(route.ps[i+1].x, route.ps[i+1].y), + Geom::Point(route.ps[i+2].x, route.ps[i+2].y) ); + i+=2; + break; + } + } + } +} + + +void SPConnEndPair::tellLibavoidNewEndpoints(bool const processTransaction) +{ + if (_connRef == nullptr || !isAutoRoutingConn()) { + // Do nothing + return; + } + makePathInvalid(); + + _updateEndPoints(); + if (processTransaction) { + _connRef->router()->processTransaction(); + } + return; +} + + +bool SPConnEndPair::reroutePathFromLibavoid() +{ + if (_connRef == nullptr || !isAutoRoutingConn()) { + // Do nothing + return false; + } + + SPCurve *curve = _path->getCurve(true); + + recreateCurve(curve, _connRef, _connCurvature); + + Geom::Affine doc2item = _path->i2doc_affine().inverse(); + curve->transform(doc2item); + + return true; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-conn-end-pair.h b/src/object/sp-conn-end-pair.h new file mode 100644 index 0000000..8fc3259 --- /dev/null +++ b/src/object/sp-conn-end-pair.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_CONN_END_PAIR +#define SEEN_SP_CONN_END_PAIR + +/* + * A class for handling connector endpoint movement and libavoid interaction. + * + * Authors: + * Peter Moulder + * + * * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "3rdparty/adaptagrams/libavoid/connector.h" + + +class SPConnEnd; +class SPCurve; +class SPPath; +class SPItem; +class SPObject; + +namespace Geom { class Point; } +namespace Inkscape { +namespace XML { +class Node; +} +} + +extern void recreateCurve(SPCurve *curve, Avoid::ConnRef *connRef, double curvature); + +class SPConnEndPair { +public: + SPConnEndPair(SPPath *); + ~SPConnEndPair(); + void release(); + void setAttr(unsigned const key, char const *const value); + void writeRepr(Inkscape::XML::Node *const repr) const; + void getAttachedItems(SPItem *[2]) const; + void getEndpoints(Geom::Point endPts[]) const; + double getCurvature() const; + SPConnEnd** getConnEnds(); + bool isOrthogonal() const; + friend void recreateCurve(SPCurve *curve, Avoid::ConnRef *connRef, double curvature); + void tellLibavoidNewEndpoints(bool const processTransaction = false); + bool reroutePathFromLibavoid(); + void makePathInvalid(); + void update(); + bool isAutoRoutingConn(); + void rerouteFromManipulation(); + +private: + void _updateEndPoints(); + + SPConnEnd *_connEnd[2]; + + SPPath *_path; + + // libavoid's internal representation of the item. + Avoid::ConnRef *_connRef; + + int _connType; + double _connCurvature; + + // A sigc connection for transformed signal. + sigc::connection _transformed_connection; +}; + + +void sp_conn_end_pair_build(SPObject *object); + + +// _connType options: +enum { + SP_CONNECTOR_NOAVOID, // Basic connector - a straight line. + SP_CONNECTOR_POLYLINE, // Object avoiding polyline. + SP_CONNECTOR_ORTHOGONAL // Object avoiding orthogonal polyline (only horizontal and vertical segments). +}; + + +#endif /* !SEEN_SP_CONN_END_PAIR */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-conn-end.cpp b/src/object/sp-conn-end.cpp new file mode 100644 index 0000000..a3039e8 --- /dev/null +++ b/src/object/sp-conn-end.cpp @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-conn-end.h" + +#include +#include +#include + +#include "bad-uri-exception.h" +#include "display/curve.h" +#include "xml/repr.h" +#include "sp-path.h" +#include "uri.h" +#include "document.h" +#include "sp-item-group.h" +#include "2geom/path-intersection.h" + + +static void change_endpts(SPCurve *const curve, double const endPos[2]); + +SPConnEnd::SPConnEnd(SPObject *const owner) + : ref(owner) + , href(nullptr) + // Default to center connection endpoint + , _changed_connection() + , _delete_connection() + , _transformed_connection() + , _group_connection() +{ +} + +static SPObject const *get_nearest_common_ancestor(SPObject const *const obj, SPItem const *const objs[2]) +{ + SPObject const *anc_sofar = obj; + for (unsigned i = 0; i < 2; ++i) { + if ( objs[i] != nullptr ) { + anc_sofar = anc_sofar->nearestCommonAncestor(objs[i]); + } + } + return anc_sofar; +} + + +static bool try_get_intersect_point_with_item_recursive(Geom::PathVector& conn_pv, SPItem* item, + const Geom::Affine& item_transform, double& intersect_pos) +{ + double initial_pos = intersect_pos; + // if this is a group... + if (SP_IS_GROUP(item)) { + SPGroup* group = SP_GROUP(item); + + // consider all first-order children + double child_pos = 0.0; + std::vector g = sp_item_group_item_list(group); + for (auto child_item : g) { + try_get_intersect_point_with_item_recursive(conn_pv, child_item, + item_transform * child_item->transform, child_pos); + if (intersect_pos < child_pos) + intersect_pos = child_pos; + } + return intersect_pos != initial_pos; + } + + // if this is not a shape, nothing to be done + if (!SP_IS_SHAPE(item)) return false; + + // make sure it has an associated curve + SPCurve* item_curve = SP_SHAPE(item)->getCurve(); + if (!item_curve) return false; + + // apply transformations (up to common ancestor) + item_curve->transform(item_transform); + + const Geom::PathVector& curve_pv = item_curve->get_pathvector(); + Geom::CrossingSet cross = crossings(conn_pv, curve_pv); + // iterate over all Crossings + //TODO: check correctness of the following code: inner loop uses loop variable + // with a name identical to the loop variable of the outer loop. Then rename. + for (const auto & cr : cross) { + for (const auto & cr_pt : cr) { + if ( intersect_pos < cr_pt.ta) + intersect_pos = cr_pt.ta; + } + } + + item_curve->unref(); + + return intersect_pos != initial_pos; +} + + +// This function returns the outermost intersection point between the path (a connector) +// and the item given. If the item is a group, then the component items are considered. +// The transforms given should be to a common ancestor of both the path and item. +// +static bool try_get_intersect_point_with_item(SPPath* conn, SPItem* item, + const Geom::Affine& item_transform, const Geom::Affine& conn_transform, + const bool at_start, double& intersect_pos) +{ + // Copy the curve and apply transformations up to common ancestor. + SPCurve* conn_curve = conn->_curve->copy(); + conn_curve->transform(conn_transform); + + Geom::PathVector conn_pv = conn_curve->get_pathvector(); + + // If this is not the starting point, use Geom::Path::reverse() to reverse the path + if (!at_start) { + // connectors are actually a single path, so consider the first element from a Geom::PathVector + conn_pv[0] = conn_pv[0].reversed(); + } + + // We start with the intersection point at the beginning of the path + intersect_pos = 0.0; + + // Find the intersection. + bool result = try_get_intersect_point_with_item_recursive(conn_pv, item, item_transform, intersect_pos); + + if (!result) { + // No intersection point has been found (why?) + // just default to connector end + intersect_pos = 0; + } + // If not at the starting point, recompute position with respect to original path + if (!at_start) { + intersect_pos = conn_pv[0].size() - intersect_pos; + } + // Free the curve copy. + conn_curve->unref(); + + return result; +} + + +static void sp_conn_get_route_and_redraw(SPPath *const path, const bool updatePathRepr = true) +{ + // Get the new route around obstacles. + bool rerouted = path->connEndPair.reroutePathFromLibavoid(); + if (!rerouted) { + return; + } + + SPItem *h2attItem[2] = {nullptr}; + path->connEndPair.getAttachedItems(h2attItem); + + SPObject const *const ancestor = get_nearest_common_ancestor(path, h2attItem); + Geom::Affine const path2anc(i2anc_affine(path, ancestor)); + + // Set sensible values in case there the connector ends are not + // attached to any shapes. + Geom::PathVector conn_pv = path->_curve->get_pathvector(); + double endPos[2] = { 0.0, static_cast(conn_pv[0].size()) }; + + for (unsigned h = 0; h < 2; ++h) { + // Assume center point for all + if (h2attItem[h]) { + Geom::Affine h2i2anc = i2anc_affine(h2attItem[h], ancestor); + try_get_intersect_point_with_item(path, h2attItem[h], h2i2anc, path2anc, + (h == 0), endPos[h]); + } + } + change_endpts(path->_curve, endPos); + if (updatePathRepr) { + path->updateRepr(); + path->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + + +static void sp_conn_end_shape_move(Geom::Affine const */*mp*/, SPItem */*moved_item*/, SPPath *const path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + + +void sp_conn_reroute_path(SPPath *const path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + path->connEndPair.tellLibavoidNewEndpoints(); + } +} + + +void sp_conn_reroute_path_immediate(SPPath *const path) +{ + if (path->connEndPair.isAutoRoutingConn()) { + bool processTransaction = true; + path->connEndPair.tellLibavoidNewEndpoints(processTransaction); + } + // Don't update the path repr or else connector dragging is slowed by + // constant update of values to the xml editor, and each step is also + // needlessly remembered by undo/redo. + bool const updatePathRepr = false; + sp_conn_get_route_and_redraw(path, updatePathRepr); +} + +void sp_conn_redraw_path(SPPath *const path) +{ + sp_conn_get_route_and_redraw(path); +} + + +static void change_endpts(SPCurve *const curve, double const endPos[2]) +{ + // Use Geom::Path::portion to cut the curve at the end positions + if (endPos[0] > endPos[1]) { + // Path is "negative", reset the curve and return + curve->reset(); + return; + } + const Geom::Path& old_path = curve->get_pathvector()[0]; + Geom::PathVector new_path_vector; + new_path_vector.push_back(old_path.portion(endPos[0], endPos[1])); + curve->set_pathvector(new_path_vector); +} + +static void sp_conn_end_deleted(SPObject *, SPObject *const owner, unsigned const handle_ix) +{ + char const * const attrs[] = { + "inkscape:connection-start", "inkscape:connection-end"}; + owner->removeAttribute(attrs[handle_ix]); + /* I believe this will trigger sp_conn_end_href_changed. */ +} + +void sp_conn_end_detach(SPObject *const owner, unsigned const handle_ix) +{ + sp_conn_end_deleted(nullptr, owner, handle_ix); +} + +void SPConnEnd::setAttacherHref(gchar const *value, SPPath* /*path*/) +{ + bool validRef = true; + + if (value && href && strcmp(value, href) == 0) { + /* No change, do nothing. */ + } else if (!value) { + validRef = false; + } else { + href = g_strdup(value); + // Now do the attaching, which emits the changed signal. + try { + ref.attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. (Also needed for + * sp-use.) */ + g_warning("%s", e.what()); + validRef = false; + } + } + + if (!validRef) { + ref.detach(); + g_free(href); + href = nullptr; + } +} + + +void sp_conn_end_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, + SPConnEnd *connEndPtr, SPPath *const path, unsigned const handle_ix) +{ + g_return_if_fail(connEndPtr != nullptr); + SPConnEnd &connEnd = *connEndPtr; + connEnd._delete_connection.disconnect(); + connEnd._transformed_connection.disconnect(); + connEnd._group_connection.disconnect(); + + if (connEnd.href) { + SPObject *refobj = connEnd.ref.getObject(); + if (refobj) { + connEnd._delete_connection + = refobj->connectDelete(sigc::bind(sigc::ptr_fun(&sp_conn_end_deleted), + path, handle_ix)); + // This allows the connector tool to dive into a group's children + // And connect to their children's centers. + SPObject *parent = refobj->parent; + if (SP_IS_GROUP(parent) && ! SP_IS_LAYER(parent)) { + connEnd._group_connection + = SP_ITEM(parent)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_conn_end_shape_move), + path)); + } + connEnd._transformed_connection + = SP_ITEM(refobj)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_conn_end_shape_move), + path)); + } + } +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-conn-end.h b/src/object/sp-conn-end.h new file mode 100644 index 0000000..039ae5a --- /dev/null +++ b/src/object/sp-conn-end.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_CONN_END +#define SEEN_SP_CONN_END + +#include +#include + +#include "sp-use-reference.h" +#include "conn-avoid-ref.h" + +class SPPath; + +class SPConnEnd { +public: + SPConnEnd(SPObject *owner); + + SPUseReference ref; + char *href; + + /** Change of href string (not a modification of the attributes of the referrent). */ + sigc::connection _changed_connection; + + /** Called when the attached object gets deleted. */ + sigc::connection _delete_connection; + + /** A sigc connection for transformed signal, used to do move compensation. */ + sigc::connection _transformed_connection; + + /** A sigc connection for owning group transformed, used to do move compensation. */ + sigc::connection _group_connection; + + void setAttacherHref(char const * value, SPPath * unused); + //void setAttacherEndpoint(char const *, SPPath *); // not defined + + +private: + SPConnEnd(SPConnEnd const &) = delete; // no copy + SPConnEnd &operator=(SPConnEnd const &) = delete; // no assign +}; + +void sp_conn_end_href_changed(SPObject *old_ref, SPObject *ref, + SPConnEnd *connEnd, SPPath *path, unsigned const handle_ix); +void sp_conn_reroute_path(SPPath *const path); +void sp_conn_reroute_path_immediate(SPPath *const path); +void sp_conn_redraw_path(SPPath *const path); +void sp_conn_end_detach(SPObject *const owner, unsigned const handle_ix); + + +#endif /* !SEEN_SP_CONN_END */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-defs.cpp b/src/object/sp-defs.cpp new file mode 100644 index 0000000..6d67a26 --- /dev/null +++ b/src/object/sp-defs.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: We should really check childrens validity - currently everything + * flips in + */ + +#include "sp-defs.h" +#include "xml/repr.h" +#include "document.h" + +SPDefs::SPDefs() : SPObject() { +} + +SPDefs::~SPDefs() = default; + +void SPDefs::release() { + SPObject::release(); +} + +void SPDefs::update(SPCtx *ctx, guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l(this->childList(true)); + for(auto child : l){ + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + sp_object_unref(child); + } +} + +void SPDefs::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPDefs::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + + if (!repr) { + repr = xml_doc->createElement("svg:defs"); + } + + std::vector l; + for (auto& child: children) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + if (crepr) { + l.push_back(crepr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + child.updateRepr(flags); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-defs.h b/src/object/sp-defs.h new file mode 100644 index 0000000..54025bf --- /dev/null +++ b/src/object/sp-defs.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DEFS_H +#define SEEN_SP_DEFS_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_DEFS(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_DEFS(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPDefs : public SPObject { +public: + SPDefs(); + ~SPDefs() override; + +protected: + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif // !SEEN_SP_DEFS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-desc.cpp b/src/object/sp-desc.cpp new file mode 100644 index 0000000..3b739c4 --- /dev/null +++ b/src/object/sp-desc.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Jeff Schiller + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-desc.h" +#include "xml/repr.h" + +SPDesc::SPDesc() : SPObject() { +} + +SPDesc::~SPDesc() = default; + +/** + * Writes it's settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPDesc::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + if (!repr) { + repr = this->getRepr()->duplicate(doc); + } + + SPObject::write(doc, repr, flags); + + return repr; +} diff --git a/src/object/sp-desc.h b/src/object/sp-desc.h new file mode 100644 index 0000000..8db0055 --- /dev/null +++ b/src/object/sp-desc.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DESC_H +#define SEEN_SP_DESC_H + +/* + * SVG implementation + * + * Authors: + * Jeff Schiller + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_DESC(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_DESC(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPDesc : public SPObject { +public: + SPDesc(); + ~SPDesc() override; + +protected: + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-dimensions.cpp b/src/object/sp-dimensions.cpp new file mode 100644 index 0000000..a4d9585 --- /dev/null +++ b/src/object/sp-dimensions.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG dimensions implementation + * + * Authors: + * Lauris Kaplinski + * Edward Flick (EAF) + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-dimensions.h" +#include "sp-item.h" + +void SPDimensions::calcDimsFromParentViewport(const SPItemCtx *ictx, bool assign_to_set) +{ +#define ASSIGN(field) { if (assign_to_set) { field._set = true; } } + if (this->x.unit == SVGLength::PERCENT) { + ASSIGN(x); + this->x.computed = this->x.value * ictx->viewport.width(); + } + + if (this->y.unit == SVGLength::PERCENT) { + ASSIGN(y); + this->y.computed = this->y.value * ictx->viewport.height(); + } + + if (this->width.unit == SVGLength::PERCENT) { + ASSIGN(width); + this->width.computed = this->width.value * ictx->viewport.width(); + } + + if (this->height.unit == SVGLength::PERCENT) { + ASSIGN(height); + this->height.computed = this->height.value * ictx->viewport.height(); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-dimensions.h b/src/object/sp-dimensions.h new file mode 100644 index 0000000..2f2538c --- /dev/null +++ b/src/object/sp-dimensions.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_DIMENSIONS_H__ +#define SP_DIMENSIONS_H__ + +/* + * dimensions helper class, common code used by root, image and others + * + * Authors: + * Shlomi Fish + * Copyright (C) 2017 Shlomi Fish, authors + * + * Released under dual Expat and GNU GPL, read the file 'COPYING' for more information + * + */ + +#include "svg/svg-length.h" + +class SPItemCtx; + +class SPDimensions { + +public: + SVGLength x; + SVGLength y; + SVGLength width; + SVGLength height; + void calcDimsFromParentViewport(const SPItemCtx *ictx, bool assign_to_set = false); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-ellipse.cpp b/src/object/sp-ellipse.cpp new file mode 100644 index 0000000..a3f04d8 --- /dev/null +++ b/src/object/sp-ellipse.cpp @@ -0,0 +1,767 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG and related implementations + * + * Authors: + * Lauris Kaplinski + * Mitsuru Oka + * bulia byak + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" + +#include <2geom/angle.h> +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/path-sink.h> + +#include "attributes.h" +#include "display/curve.h" +#include "document.h" +#include "preferences.h" +#include "snap-candidate.h" +#include "sp-ellipse.h" +#include "style.h" +#include "svg/svg.h" +#include "svg/path-string.h" + +#define SP_2PI (2 * M_PI) + +SPGenericEllipse::SPGenericEllipse() + : SPShape() + , start(0) + , end(SP_2PI) + , type(SP_GENERIC_ELLIPSE_UNDEFINED) + , arc_type(SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) +{ +} + +SPGenericEllipse::~SPGenericEllipse() += default; + +void SPGenericEllipse::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + // std::cout << "SPGenericEllipse::build: Entrance: " << this->type + // << " (" << g_quark_to_string(repr->code()) << ")" << std::endl; + + switch ( type ) { + case SP_GENERIC_ELLIPSE_ARC: + this->readAttr("sodipodi:cx"); + this->readAttr("sodipodi:cy"); + this->readAttr("sodipodi:rx"); + this->readAttr("sodipodi:ry"); + this->readAttr("sodipodi:start"); + this->readAttr("sodipodi:end"); + this->readAttr("sodipodi:open"); + this->readAttr("sodipodi:arc-type"); + break; + + case SP_GENERIC_ELLIPSE_CIRCLE: + this->readAttr("cx"); + this->readAttr("cy"); + this->readAttr("r"); + break; + + case SP_GENERIC_ELLIPSE_ELLIPSE: + this->readAttr("cx"); + this->readAttr("cy"); + this->readAttr("rx"); + this->readAttr("ry"); + break; + + default: + std::cerr << "SPGenericEllipse::build() unknown defined type." << std::endl; + } + + // std::cout << " cx: " << cx.write() << std::endl; + // std::cout << " cy: " << cy.write() << std::endl; + // std::cout << " rx: " << rx.write() << std::endl; + // std::cout << " ry: " << ry.write() << std::endl; + SPShape::build(document, repr); +} + +void SPGenericEllipse::set(SPAttributeEnum key, gchar const *value) +{ + // There are multiple ways to set internal cx, cy, rx, and ry (via SVG attributes or Sodipodi + // attributes) thus we don't want to unset them if a read fails (e.g., when we explicitly clear + // an attribute by setting it to NULL). + + // We must update the SVGLengths immediately or nodes may be misplaced after they are moved. + double const w = viewport.width(); + double const h = viewport.height(); + double const d = hypot(w, h) / sqrt(2); // diagonal + double const em = style->font_size.computed; + double const ex = em * 0.5; + + SVGLength t; + switch (key) { + case SP_ATTR_CX: + case SP_ATTR_SODIPODI_CX: + if( t.read(value) ) cx = t; + cx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_CY: + case SP_ATTR_SODIPODI_CY: + if( t.read(value) ) cy = t; + cy.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_RX: + case SP_ATTR_SODIPODI_RX: + if( t.read(value) && t.value > 0.0 ) rx = t; + rx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_RY: + case SP_ATTR_SODIPODI_RY: + if( t.read(value) && t.value > 0.0 ) ry = t; + ry.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_R: + if( t.read(value) && t.value > 0.0 ) { + this->ry = this->rx = t; + } + rx.update( em, ex, d ); + ry.update( em, ex, d ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_SODIPODI_START: + if (value) { + sp_svg_number_read_d(value, &this->start); + } else { + this->start = 0; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_SODIPODI_END: + if (value) { + sp_svg_number_read_d(value, &this->end); + } else { + this->end = 2 * M_PI; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_SODIPODI_OPEN: + // This is for reading in old files. + if ((!value) || strcmp(value,"true")) { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE; + } else { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_ARC; + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_SODIPODI_ARC_TYPE: + // To read in old files that use 'open', we need to not set if value is null. + // We could also check inkscape version. + if (value) { + if (!strcmp(value,"arc")) { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_ARC; + } else if (!strcmp(value,"chord")) { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD; + } else { + this->arc_type = SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE; + } + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPGenericEllipse::update(SPCtx *ctx, guint flags) +{ + // std::cout << "\nSPGenericEllipse::update: Entrance" << std::endl; + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + Geom::Rect const &viewbox = ((SPItemCtx const *) ctx)->viewport; + + double const dx = viewbox.width(); + double const dy = viewbox.height(); + double const dr = hypot(dx, dy) / sqrt(2); + double const em = this->style->font_size.computed; + double const ex = em * 0.5; // fixme: get from pango or libnrtype + + this->cx.update(em, ex, dx); + this->cy.update(em, ex, dy); + this->rx.update(em, ex, dr); + this->ry.update(em, ex, dr); + + this->set_shape(); + } + + SPShape::update(ctx, flags); + // std::cout << "SPGenericEllipse::update: Exit\n" << std::endl; +} + +Inkscape::XML::Node *SPGenericEllipse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + // std::cout << "\nSPGenericEllipse::write: Entrance (" + // << (repr == NULL ? " NULL" : g_quark_to_string(repr->code())) + // << ")" << std::endl; + + GenericEllipseType new_type = SP_GENERIC_ELLIPSE_UNDEFINED; + if (_isSlice() || hasPathEffect() ) { + new_type = SP_GENERIC_ELLIPSE_ARC; + } else if ( rx.computed == ry.computed ) { + new_type = SP_GENERIC_ELLIPSE_CIRCLE; + } else { + new_type = SP_GENERIC_ELLIPSE_ELLIPSE; + } + // std::cout << " new_type: " << new_type << std::endl; + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + + switch ( new_type ) { + + case SP_GENERIC_ELLIPSE_ARC: + repr = xml_doc->createElement("svg:path"); + break; + case SP_GENERIC_ELLIPSE_CIRCLE: + repr = xml_doc->createElement("svg:circle"); + break; + case SP_GENERIC_ELLIPSE_ELLIPSE: + repr = xml_doc->createElement("svg:ellipse"); + break; + case SP_GENERIC_ELLIPSE_UNDEFINED: + default: + std::cerr << "SPGenericEllipse::write(): unknown type." << std::endl; + } + } + + if (type != new_type) { + switch (new_type) { + case SP_GENERIC_ELLIPSE_ARC: + repr->setCodeUnsafe(g_quark_from_string("svg:path")); + break; + case SP_GENERIC_ELLIPSE_CIRCLE: + repr->setCodeUnsafe(g_quark_from_string("svg:circle")); + break; + case SP_GENERIC_ELLIPSE_ELLIPSE: + repr->setCodeUnsafe(g_quark_from_string("svg:ellipse")); + break; + default: + std::cerr << "SPGenericEllipse::write(): unknown type." << std::endl; + } + type = new_type; + } + + // std::cout << " type: " << g_quark_to_string( repr->code() ) << std::endl; + // std::cout << " cx: " << cx.write() << " " << cx.computed + // << " cy: " << cy.write() << " " << cy.computed + // << " rx: " << rx.write() << " " << rx.computed + // << " ry: " << ry.write() << " " << ry.computed << std::endl; + + switch ( type ) { + case SP_GENERIC_ELLIPSE_UNDEFINED: + case SP_GENERIC_ELLIPSE_ARC: + + repr->removeAttribute("cx"); + repr->removeAttribute("cy"); + repr->removeAttribute("rx"); + repr->removeAttribute("ry"); + repr->removeAttribute("r"); + + if (flags & SP_OBJECT_WRITE_EXT) { + + repr->setAttribute("sodipodi:type", "arc"); + sp_repr_set_svg_length(repr, "sodipodi:cx", cx); + sp_repr_set_svg_length(repr, "sodipodi:cy", cy); + sp_repr_set_svg_length(repr, "sodipodi:rx", rx); + sp_repr_set_svg_length(repr, "sodipodi:ry", ry); + + // write start and end only if they are non-trivial; otherwise remove + if (_isSlice()) { + sp_repr_set_svg_double(repr, "sodipodi:start", start); + sp_repr_set_svg_double(repr, "sodipodi:end", end); + + switch ( arc_type ) { + case SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE: + repr->removeAttribute("sodipodi:open"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "slice"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD: + // A chord's path isn't "open" but its fill most closely resembles an arc. + repr->setAttribute("sodipodi:open", "true"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "chord"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_ARC: + repr->setAttribute("sodipodi:open", "true"); // For backwards compat. + repr->setAttribute("sodipodi:arc-type", "arc"); + break; + default: + std::cerr << "SPGenericEllipse::write: unknown arc-type." << std::endl; + } + } else { + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + } + } + + // write d= + set_elliptical_path_attribute(repr); + break; + + case SP_GENERIC_ELLIPSE_CIRCLE: + sp_repr_set_svg_length(repr, "cx", cx); + sp_repr_set_svg_length(repr, "cy", cy); + sp_repr_set_svg_length(repr, "r", rx); + repr->removeAttribute("rx"); + repr->removeAttribute("ry"); + repr->removeAttribute("sodipodi:cx"); + repr->removeAttribute("sodipodi:cy"); + repr->removeAttribute("sodipodi:rx"); + repr->removeAttribute("sodipodi:ry"); + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + repr->removeAttribute("sodipodi:type"); + repr->removeAttribute("d"); + break; + + case SP_GENERIC_ELLIPSE_ELLIPSE: + sp_repr_set_svg_length(repr, "cx", cx); + sp_repr_set_svg_length(repr, "cy", cy); + sp_repr_set_svg_length(repr, "rx", rx); + sp_repr_set_svg_length(repr, "ry", ry); + repr->removeAttribute("r"); + repr->removeAttribute("sodipodi:cx"); + repr->removeAttribute("sodipodi:cy"); + repr->removeAttribute("sodipodi:rx"); + repr->removeAttribute("sodipodi:ry"); + repr->removeAttribute("sodipodi:end"); + repr->removeAttribute("sodipodi:start"); + repr->removeAttribute("sodipodi:open"); + repr->removeAttribute("sodipodi:arc-type"); + repr->removeAttribute("sodipodi:type"); + repr->removeAttribute("d"); + break; + + default: + std::cerr << "SPGenericEllipse::write: unknown type." << std::endl; + } + + set_shape(); // evaluate SPCurve + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +const char *SPGenericEllipse::displayName() const +{ + + switch ( type ) { + case SP_GENERIC_ELLIPSE_UNDEFINED: + case SP_GENERIC_ELLIPSE_ARC: + + if (_isSlice()) { + switch ( arc_type ) { + case SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE: + return _("Slice"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD: + return _("Chord"); + break; + case SP_GENERIC_ELLIPSE_ARC_TYPE_ARC: + return _("Arc"); + break; + } + } else { + return _("Ellipse"); + } + + case SP_GENERIC_ELLIPSE_CIRCLE: + return _("Circle"); + + case SP_GENERIC_ELLIPSE_ELLIPSE: + return _("Ellipse"); + + default: + return "Unknown ellipse: ERROR"; + } + return ("Shouldn't be here"); +} + +// Create path for rendering shape on screen +void SPGenericEllipse::set_shape() +{ + // std::cout << "SPGenericEllipse::set_shape: Entrance" << std::endl; + if (hasBrokenPathEffect()) { + g_warning("The ellipse shape has unknown LPE on it! Convert to path to make it editable preserving the appearance; editing it as ellipse will remove the bad LPE"); + + if (this->getRepr()->attribute("d")) { + // unconditionally read the curve from d, if any, to preserve appearance + Geom::PathVector pv = sp_svg_read_pathv(this->getRepr()->attribute("d")); + SPCurve *cold = new SPCurve(pv); + this->setCurveInsync(cold); + cold->unref(); + } + + return; + } + if (Geom::are_near(this->rx.computed, 0) || Geom::are_near(this->ry.computed, 0)) { + return; + } + + this->normalize(); + + SPCurve *c = nullptr; + + // For simplicity, we use a circle with center (0, 0) and radius 1 for our calculations. + Geom::Circle circle(0, 0, 1); + + if (!this->_isSlice()) { + start = 0.0; + end = 2.0*M_PI; + } + double incr = end - start; // arc angle + if (incr < 0.0) incr += 2.0*M_PI; + + int numsegs = 1 + int(incr*2.0/M_PI); // number of arc segments + if (numsegs > 4) numsegs = 4; + + incr = incr/numsegs; // limit arc angle to less than 90 degrees + Geom::Path path(Geom::Point::polar(start)); + Geom::EllipticalArc* arc; + for (int seg = 0; seg < numsegs; seg++) { + arc = circle.arc(Geom::Point::polar(start + seg*incr), Geom::Point::polar(start + (seg + 0.5)*incr), Geom::Point::polar(start + (seg + 1.0)*incr)); + path.append(*arc); + delete arc; + } + Geom::PathBuilder pb; + pb.append(path); + if (this->_isSlice() && this->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) { + pb.lineTo(Geom::Point(0, 0)); + } + + if ((this->arc_type != SP_GENERIC_ELLIPSE_ARC_TYPE_ARC) || (this->type != SP_GENERIC_ELLIPSE_ARC)) { + pb.closePath(); + } else { + pb.flush(); + } + c = new SPCurve(pb.peek()); + + // gchar *str = sp_svg_write_path(curve->get_pathvector()); + // std::cout << " path: " << str << std::endl; + // g_free(str); + + // Stretching / moving the calculated shape to fit the actual dimensions. + Geom::Affine aff = Geom::Scale(rx.computed, ry.computed) * Geom::Translate(cx.computed, cy.computed); + c->transform(aff); + + /* Reset the shape's curve to the "original_curve" + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + SPCurve * before = this->getCurveBeforeLPE(); + bool haslpe = this->hasPathEffectOnClipOrMaskRecursive(this); + if (before || haslpe) { + if (c && before && before->get_pathvector() != c->get_pathvector()){ + this->setCurveBeforeLPE(c); + sp_lpe_item_update_patheffect(this, true, false); + } else if(haslpe) { + this->setCurveBeforeLPE(c); + } else { + //This happends on undo, fix bug:#1791784 + this->setCurveInsync(c); + } + } else { + this->setCurveInsync(c); + } + + if (before) { + before->unref(); + } + c->unref(); +} + +Geom::Affine SPGenericEllipse::set_transform(Geom::Affine const &xform) +{ + notifyTransform(xform); + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + + /* Calculate ellipse start in parent coords. */ + Geom::Point pos(Geom::Point(this->cx.computed, this->cy.computed) * xform); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + if (sw > 1e-9) { + ret[0] /= sw; + ret[1] /= sw; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if (sh > 1e-9) { + ret[2] /= sh; + ret[3] /= sh; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + if (this->rx._set) { + this->rx.scale( sw ); + } + + if (this->ry._set) { + this->ry.scale( sh ); + } + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->cx = pos[Geom::X]; + this->cy = pos[Geom::Y]; + + this->set_shape(); + + // Adjust stroke width + this->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPGenericEllipse::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const +{ + // CPPIFY: is this call necessary? + const_cast(this)->normalize(); + + Geom::Affine const i2dt = this->i2dt_affine(); + + // Snap to the 4 quadrant points of the ellipse, but only if the arc + // spans far enough to include them + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ELLIPSE_QUADRANT_POINT)) { + for (double angle = 0; angle < SP_2PI; angle += M_PI_2) { + if (Geom::AngleInterval(this->start, this->end, true).contains(angle)) { + Geom::Point pt = this->getPointAtAngle(angle) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_ELLIPSE_QUADRANT_POINT, Inkscape::SNAPTARGET_ELLIPSE_QUADRANT_POINT); + } + } + } + + double cx = this->cx.computed; + double cy = this->cy.computed; + + + bool slice = this->_isSlice(); + + // Add the centre, if we have a closed slice or when explicitly asked for + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && slice && + this->arc_type == SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE) { + Geom::Point pt = Geom::Point(cx, cy) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + Geom::Point pt = Geom::Point(cx, cy) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } + + // And if we have a slice, also snap to the endpoints + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP) && slice) { + // Add the start point, if it's not coincident with a quadrant point + if (!Geom::are_near(std::fmod(this->start, M_PI_2), 0)) { + Geom::Point pt = this->getPointAtAngle(this->start) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + + // Add the end point, if it's not coincident with a quadrant point + if (!Geom::are_near(std::fmod(this->end, M_PI_2), 0)) { + Geom::Point pt = this->getPointAtAngle(this->end) * i2dt; + p.emplace_back(pt, Inkscape::SNAPSOURCE_NODE_CUSP, Inkscape::SNAPTARGET_NODE_CUSP); + } + } +} + +void SPGenericEllipse::modified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->set_shape(); + } + + SPShape::modified(flags); +} + +void SPGenericEllipse::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +void SPGenericEllipse::normalize() +{ + Geom::AngleInterval a(this->start, this->end, true); + + this->start = a.initialAngle().radians0(); + this->end = a.finalAngle().radians0(); +} + +Geom::Point SPGenericEllipse::getPointAtAngle(double arg) const +{ + return Geom::Point::polar(arg) * Geom::Scale(rx.computed, ry.computed) * Geom::Translate(cx.computed, cy.computed); +} + +/* + * set_elliptical_path_attribute: + * + * Convert center to endpoint parameterization and set it to repr. + * + * See SVG 1.0 Specification W3C Recommendation + * ``F.6 Elliptical arc implementation notes'' for more detail. + */ +bool SPGenericEllipse::set_elliptical_path_attribute(Inkscape::XML::Node *repr) +{ + // Make sure our pathvector is up to date. + this->set_shape(); + + if (_curve) { + gchar* d = sp_svg_write_path(_curve->get_pathvector()); + + repr->setAttribute("d", d); + + g_free(d); + } else { + repr->removeAttribute("d"); + } + + return true; +} + +void SPGenericEllipse::position_set(gdouble x, gdouble y, gdouble rx, gdouble ry) +{ + this->cx = x; + this->cy = y; + this->rx = rx; + this->ry = ry; + + Inkscape::Preferences * prefs = Inkscape::Preferences::get(); + + // those pref values are in degrees, while we want radians + if (prefs->getDouble("/tools/shapes/arc/start", 0.0) != 0) { + this->start = Geom::Angle::from_degrees(prefs->getDouble("/tools/shapes/arc/start", 0.0)).radians0(); + } + + if (prefs->getDouble("/tools/shapes/arc/end", 0.0) != 0) { + this->end = Geom::Angle::from_degrees(prefs->getDouble("/tools/shapes/arc/end", 0.0)).radians0(); + } + + this->arc_type = (GenericEllipseArcType)prefs->getInt("/tools/shapes/arc/arc_type", 0); + if (_isSlice()) { + this->type = SP_GENERIC_ELLIPSE_ARC; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +bool SPGenericEllipse::_isSlice() const +{ + Geom::AngleInterval a(this->start, this->end, true); + + return !(Geom::are_near(a.extent(), 0) || Geom::are_near(a.extent(), SP_2PI)); +} + +/** +Returns the ratio in which the vector from p0 to p1 is stretched by transform + */ +gdouble SPGenericEllipse::vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform) { + if (p0 == p1) { + return 0; + } + + return (Geom::distance(p0 * xform, p1 * xform) / Geom::distance(p0, p1)); +} + +void SPGenericEllipse::setVisibleRx(gdouble rx) { + if (rx == 0) { + this->rx.unset(); + } else { + this->rx = rx / SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed + 1, this->cy.computed), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +void SPGenericEllipse::setVisibleRy(gdouble ry) { + if (ry == 0) { + this->ry.unset(); + } else { + this->ry = ry / SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed, this->cy.computed + 1), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +gdouble SPGenericEllipse::getVisibleRx() const { + if (!this->rx._set) { + return 0; + } + + return this->rx.computed * SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed + 1, this->cy.computed), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); +} + +gdouble SPGenericEllipse::getVisibleRy() const { + if (!this->ry._set) { + return 0; + } + + return this->ry.computed * SPGenericEllipse::vectorStretch( + Geom::Point(this->cx.computed, this->cy.computed + 1), + Geom::Point(this->cx.computed, this->cy.computed), + this->i2doc_affine()); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/sp-ellipse.h b/src/object/sp-ellipse.h new file mode 100644 index 0000000..5af1819 --- /dev/null +++ b/src/object/sp-ellipse.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * SVG and related implementations + * + * Authors: + * Lauris Kaplinski + * Mitsuru Oka + * Tavmjong Bah + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_ELLIPSE_H +#define SEEN_SP_ELLIPSE_H + +#include "svg/svg-length.h" +#include "sp-shape.h" + +/* Common parent class */ +#define SP_GENERICELLIPSE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GENERICELLIPSE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum GenericEllipseType { + SP_GENERIC_ELLIPSE_UNDEFINED, // FIXME shouldn't exist + SP_GENERIC_ELLIPSE_ARC, + SP_GENERIC_ELLIPSE_CIRCLE, + SP_GENERIC_ELLIPSE_ELLIPSE +}; + +enum GenericEllipseArcType { + SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE, // Default + SP_GENERIC_ELLIPSE_ARC_TYPE_ARC, + SP_GENERIC_ELLIPSE_ARC_TYPE_CHORD +}; + +class SPGenericEllipse : public SPShape { +public: + SPGenericEllipse(); + ~SPGenericEllipse() override; + + // Regardless of type, the ellipse/circle/arc is stored + // internally with these variables. (Circle radius is rx). + SVGLength cx; + SVGLength cy; + SVGLength rx; + SVGLength ry; + + // Return slice, chord, or arc. + GenericEllipseArcType arcType() { return arc_type; }; + void setArcType(GenericEllipseArcType type) { arc_type = type; }; + + double start, end; + GenericEllipseType type; + GenericEllipseArcType arc_type; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + + void set(SPAttributeEnum key, char const *value) override; + void update(SPCtx *ctx, unsigned int flags) override; + + Inkscape::XML::Node *write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char *displayName() const override; + + void set_shape() override; + void update_patheffect(bool write) override; + Geom::Affine set_transform(Geom::Affine const &xform) override; + + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; + + void modified(unsigned int flags) override; + + /** + * @brief Makes sure that start and end lie between 0 and 2 * PI. + */ + void normalize(); + + Geom::Point getPointAtAngle(double arg) const; + + bool set_elliptical_path_attribute(Inkscape::XML::Node *repr); + void position_set(double x, double y, double rx, double ry); + + double getVisibleRx() const; + void setVisibleRx(double rx); + + double getVisibleRy() const; + void setVisibleRy(double ry); + +protected: + /** + * @brief Determines whether the shape is a part of an ellipse. + */ + bool _isSlice() const; + +private: + static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/sp-factory.cpp b/src/object/sp-factory.cpp new file mode 100644 index 0000000..f0d84af --- /dev/null +++ b/src/object/sp-factory.cpp @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for SPObject tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-factory.h" + +// primary +#include "box3d.h" +#include "box3d-side.h" +#include "color-profile.h" +#include "persp3d.h" +#include "sp-anchor.h" +#include "sp-clippath.h" +#include "sp-defs.h" +#include "sp-desc.h" +#include "sp-ellipse.h" +#include "sp-filter.h" +#include "sp-flowdiv.h" +#include "sp-flowregion.h" +#include "sp-flowtext.h" +#include "sp-font.h" +#include "sp-font-face.h" +#include "sp-glyph.h" +#include "sp-glyph-kerning.h" +#include "sp-guide.h" +#include "sp-hatch.h" +#include "sp-hatch-path.h" +#include "sp-image.h" +#include "sp-line.h" +#include "sp-linear-gradient.h" +#include "sp-marker.h" +#include "sp-mask.h" +#include "sp-mesh-gradient.h" +#include "sp-mesh-patch.h" +#include "sp-mesh-row.h" +#include "sp-metadata.h" +#include "sp-missing-glyph.h" +#include "sp-namedview.h" +#include "sp-offset.h" +#include "sp-path.h" +#include "sp-pattern.h" +#include "sp-polyline.h" +#include "sp-radial-gradient.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-script.h" +#include "sp-solid-color.h" +#include "sp-spiral.h" +#include "sp-star.h" +#include "sp-stop.h" +#include "sp-string.h" +#include "sp-style-elem.h" +#include "sp-switch.h" +#include "sp-symbol.h" +#include "sp-tag.h" +#include "sp-tag-use.h" +#include "sp-text.h" +#include "sp-textpath.h" +#include "sp-title.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-use.h" +#include "live_effects/lpeobject.h" + +// filters +#include "filters/blend.h" +#include "filters/colormatrix.h" +#include "filters/componenttransfer.h" +#include "filters/componenttransfer-funcnode.h" +#include "filters/composite.h" +#include "filters/convolvematrix.h" +#include "filters/diffuselighting.h" +#include "filters/displacementmap.h" +#include "filters/distantlight.h" +#include "filters/flood.h" +#include "filters/gaussian-blur.h" +#include "filters/image.h" +#include "filters/merge.h" +#include "filters/mergenode.h" +#include "filters/morphology.h" +#include "filters/offset.h" +#include "filters/pointlight.h" +#include "filters/specularlighting.h" +#include "filters/spotlight.h" +#include "filters/tile.h" +#include "filters/turbulence.h" + +SPObject *SPFactory::createObject(std::string const& id) +{ + SPObject *ret = nullptr; + + if (id == "inkscape:box3d") + ret = new SPBox3D; + else if (id == "inkscape:box3dside") + ret = new Box3DSide; + else if (id == "svg:color-profile") + ret = new Inkscape::ColorProfile; + else if (id == "inkscape:persp3d") + ret = new Persp3D; + else if (id == "svg:a") + ret = new SPAnchor; + else if (id == "svg:clipPath") + ret = new SPClipPath; + else if (id == "svg:defs") + ret = new SPDefs; + else if (id == "svg:desc") + ret = new SPDesc; + else if (id == "svg:ellipse") { + SPGenericEllipse *e = new SPGenericEllipse; + e->type = SP_GENERIC_ELLIPSE_ELLIPSE; + ret = e; + } else if (id == "svg:circle") { + SPGenericEllipse *c = new SPGenericEllipse; + c->type = SP_GENERIC_ELLIPSE_CIRCLE; + ret = c; + } else if (id == "arc") { + SPGenericEllipse *a = new SPGenericEllipse; + a->type = SP_GENERIC_ELLIPSE_ARC; + ret = a; + } + else if (id == "svg:filter") + ret = new SPFilter; + else if (id == "svg:flowDiv") + ret = new SPFlowdiv; + else if (id == "svg:flowSpan") + ret = new SPFlowtspan; + else if (id == "svg:flowPara") + ret = new SPFlowpara; + else if (id == "svg:flowLine") + ret = new SPFlowline; + else if (id == "svg:flowRegionBreak") + ret = new SPFlowregionbreak; + else if (id == "svg:flowRegion") + ret = new SPFlowregion; + else if (id == "svg:flowRegionExclude") + ret = new SPFlowregionExclude; + else if (id == "svg:flowRoot") + ret = new SPFlowtext; + else if (id == "svg:font") + ret = new SPFont; + else if (id == "svg:font-face") + ret = new SPFontFace; + else if (id == "svg:glyph") + ret = new SPGlyph; + else if (id == "svg:hkern") + ret = new SPHkern; + else if (id == "svg:vkern") + ret = new SPVkern; + else if (id == "sodipodi:guide") + ret = new SPGuide; + else if (id == "svg:hatch") + ret = new SPHatch; + else if (id == "svg:hatchpath") + ret = new SPHatchPath; + else if (id == "svg:hatchPath") { + std::cerr << "Warning: has been renamed " << std::endl; + ret = new SPHatchPath; + } + else if (id == "svg:image") + ret = new SPImage; + else if (id == "svg:g") + ret = new SPGroup; + else if (id == "svg:line") + ret = new SPLine; + else if (id == "svg:linearGradient") + ret = new SPLinearGradient; + else if (id == "svg:marker") + ret = new SPMarker; + else if (id == "svg:mask") + ret = new SPMask; + else if (id == "svg:mesh") { // SVG 2 old + ret = new SPMeshGradient; + std::cerr << "Warning: has been renamed ." << std::endl; + std::cerr << "Warning: has been repurposed as a shape that tightly wraps a ." << std::endl; + } + else if (id == "svg:meshGradient") { // SVG 2 old + ret = new SPMeshGradient; + std::cerr << "Warning: has been renamed " << std::endl; + } + else if (id == "svg:meshgradient") // SVG 2 + ret = new SPMeshGradient; + else if (id == "svg:meshPatch") { + ret = new SPMeshpatch; + std::cerr << "Warning: and have been renamed and " << std::endl; + } + else if (id == "svg:meshpatch") + ret = new SPMeshpatch; + else if (id == "svg:meshRow") + ret = new SPMeshrow; + else if (id == "svg:meshrow") + ret = new SPMeshrow; + else if (id == "svg:metadata") + ret = new SPMetadata; + else if (id == "svg:missing-glyph") + ret = new SPMissingGlyph; + else if (id == "sodipodi:namedview") + ret = new SPNamedView; + else if (id == "inkscape:offset") + ret = new SPOffset; + else if (id == "svg:path") + ret = new SPPath; + else if (id == "svg:pattern") + ret = new SPPattern; + else if (id == "svg:polygon") + ret = new SPPolygon; + else if (id == "svg:polyline") + ret = new SPPolyLine; + else if (id == "svg:radialGradient") + ret = new SPRadialGradient; + else if (id == "svg:rect") + ret = new SPRect; + else if (id == "rect") // LPE rect + ret = new SPRect; + else if (id == "svg:svg") + ret = new SPRoot; + else if (id == "svg:script") + ret = new SPScript; + else if (id == "svg:solidColor") { + ret = new SPSolidColor; + std::cerr << "Warning: has been renamed " << std::endl; + } + else if (id == "svg:solidcolor") + ret = new SPSolidColor; + else if (id == "spiral") + ret = new SPSpiral; + else if (id == "star") + ret = new SPStar; + else if (id == "svg:stop") + ret = new SPStop; + else if (id == "string") + ret = new SPString; + else if (id == "svg:style") + ret = new SPStyleElem; + else if (id == "svg:switch") + ret = new SPSwitch; + else if (id == "svg:symbol") + ret = new SPSymbol; + else if (id == "inkscape:tag") + ret = new SPTag; + else if (id == "inkscape:tagref") + ret = new SPTagUse; + else if (id == "svg:text") + ret = new SPText; + else if (id == "svg:title") + ret = new SPTitle; + else if (id == "svg:tref") + ret = new SPTRef; + else if (id == "svg:tspan") + ret = new SPTSpan; + else if (id == "svg:textPath") + ret = new SPTextPath; + else if (id == "svg:use") + ret = new SPUse; + else if (id == "inkscape:path-effect") + ret = new LivePathEffectObject; + + + // filters + else if (id == "svg:feBlend") + ret = new SPFeBlend; + else if (id == "svg:feColorMatrix") + ret = new SPFeColorMatrix; + else if (id == "svg:feComponentTransfer") + ret = new SPFeComponentTransfer; + else if (id == "svg:feFuncR") + ret = new SPFeFuncNode(SPFeFuncNode::R); + else if (id == "svg:feFuncG") + ret = new SPFeFuncNode(SPFeFuncNode::G); + else if (id == "svg:feFuncB") + ret = new SPFeFuncNode(SPFeFuncNode::B); + else if (id == "svg:feFuncA") + ret = new SPFeFuncNode(SPFeFuncNode::A); + else if (id == "svg:feComposite") + ret = new SPFeComposite; + else if (id == "svg:feConvolveMatrix") + ret = new SPFeConvolveMatrix; + else if (id == "svg:feDiffuseLighting") + ret = new SPFeDiffuseLighting; + else if (id == "svg:feDisplacementMap") + ret = new SPFeDisplacementMap; + else if (id == "svg:feDistantLight") + ret = new SPFeDistantLight; + else if (id == "svg:feFlood") + ret = new SPFeFlood; + else if (id == "svg:feGaussianBlur") + ret = new SPGaussianBlur; + else if (id == "svg:feImage") + ret = new SPFeImage; + else if (id == "svg:feMerge") + ret = new SPFeMerge; + else if (id == "svg:feMergeNode") + ret = new SPFeMergeNode; + else if (id == "svg:feMorphology") + ret = new SPFeMorphology; + else if (id == "svg:feOffset") + ret = new SPFeOffset; + else if (id == "svg:fePointLight") + ret = new SPFePointLight; + else if (id == "svg:feSpecularLighting") + ret = new SPFeSpecularLighting; + else if (id == "svg:feSpotLight") + ret = new SPFeSpotLight; + else if (id == "svg:feTile") + ret = new SPFeTile; + else if (id == "svg:feTurbulence") + ret = new SPFeTurbulence; + else if (id == "inkscape:grid") + ret = new SPObject; // TODO wtf + else if (id == "rdf:RDF") // no SP node yet + {} + else if (id == "inkscape:clipboard") // SP node not necessary + {} + else if (id == "inkscape:templateinfo" || id == "inkscape:_templateinfo") // metadata for templates + {} + else if (id.empty()) // comments + {} + else { + fprintf(stderr, "WARNING: unknown type: %s\n", id.c_str()); + } + + return ret; +} + +std::string NodeTraits::get_type_string(Inkscape::XML::Node const &node) +{ + std::string name; + + switch (node.type()) { + case Inkscape::XML::TEXT_NODE: + name = "string"; + break; + + case Inkscape::XML::ELEMENT_NODE: { + char const *const sptype = node.attribute("sodipodi:type"); + + if (sptype) { + name = sptype; + } else { + name = node.name(); + } + break; + } + default: + name = ""; + break; + } + + return name; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-factory.h b/src/object/sp-factory.h new file mode 100644 index 0000000..f87d84a --- /dev/null +++ b/src/object/sp-factory.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for SPObject tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_FACTORY_SEEN +#define SP_FACTORY_SEEN + +#include + +class SPObject; + +namespace Inkscape { +namespace XML { +class Node; +} +} + +struct SPFactory { + static SPObject *createObject(std::string const& id); +}; + +struct NodeTraits { + static std::string get_type_string(Inkscape::XML::Node const &node); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-filter-reference.cpp b/src/object/sp-filter-reference.cpp new file mode 100644 index 0000000..66e5e12 --- /dev/null +++ b/src/object/sp-filter-reference.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-filter.h" +#include "sp-filter-reference.h" + +bool +SPFilterReference::_acceptObject(SPObject *obj) const +{ + return SP_IS_FILTER(obj) && URIReference::_acceptObject(obj); + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-filter-reference.h b/src/object/sp-filter-reference.h new file mode 100644 index 0000000..8e5805f --- /dev/null +++ b/src/object/sp-filter-reference.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_FILTER_REFERENCE_H +#define SEEN_SP_FILTER_REFERENCE_H + +#include "uri-references.h" + +class SPObject; +class SPDocument; +class SPFilter; + +class SPFilterReference : public Inkscape::URIReference { +public: + SPFilterReference(SPObject *obj) : URIReference(obj) {} + SPFilterReference(SPDocument *doc) : URIReference(doc) {} + + SPFilter *getObject() const { + return static_cast(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif /* !SEEN_SP_FILTER_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-filter-units.h b/src/object/sp-filter-units.h new file mode 100644 index 0000000..7bdd1db --- /dev/null +++ b/src/object/sp-filter-units.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_FILTER_UNITS_H +#define SEEN_SP_FILTER_UNITS_H + +enum SPFilterUnits { + SP_FILTER_UNITS_OBJECTBOUNDINGBOX, + SP_FILTER_UNITS_USERSPACEONUSE +}; + + +#endif /* !SEEN_SP_FILTER_UNITS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-filter.cpp b/src/object/sp-filter.cpp new file mode 100644 index 0000000..4c434b9 --- /dev/null +++ b/src/object/sp-filter.cpp @@ -0,0 +1,518 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation. + */ +/* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-filter.h" + +#include +#include +#include +#include + +#include + +#include "bad-uri-exception.h" +#include "attributes.h" +#include "display/nr-filter.h" +#include "document.h" +#include "sp-filter-reference.h" +#include "filters/sp-filter-primitive.h" +#include "uri.h" +#include "xml/repr.h" + +static void filter_ref_changed(SPObject *old_ref, SPObject *ref, SPFilter *filter); +static void filter_ref_modified(SPObject *href, guint flags, SPFilter *filter); + + +SPFilter::SPFilter() + : SPObject(), filterUnits(SP_FILTER_UNITS_OBJECTBOUNDINGBOX), filterUnits_set(FALSE), + primitiveUnits(SP_FILTER_UNITS_USERSPACEONUSE), primitiveUnits_set(FALSE), + filterRes(NumberOptNumber()), + _renderer(nullptr), _image_name(new std::map), _image_number_next(0) +{ + this->href = new SPFilterReference(this); + this->href->changedSignal().connect(sigc::bind(sigc::ptr_fun(filter_ref_changed), this)); + + this->x = 0; + this->y = 0; + this->width = 0; + this->height = 0; + + this->_image_name->clear(); +} + +SPFilter::~SPFilter() = default; + + +/** + * Reads the Inkscape::XML::Node, and initializes SPFilter variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void SPFilter::build(SPDocument *document, Inkscape::XML::Node *repr) { + //Read values of key attributes from XML nodes into object. + this->readAttr( "style" ); // struct not derived from SPItem, we need to do this ourselves. + this->readAttr( "filterUnits" ); + this->readAttr( "primitiveUnits" ); + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "width" ); + this->readAttr( "height" ); + this->readAttr( "filterRes" ); + this->readAttr( "xlink:href" ); + this->_refcount = 0; + + SPObject::build(document, repr); + +//is this necessary? + document->addResource("filter", this); +} + +/** + * Drops any allocated memory. + */ +void SPFilter::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("filter", this); + } + +//TODO: release resources here + + //release href + if (this->href) { + this->modified_connection.disconnect(); + this->href->detach(); + delete this->href; + this->href = nullptr; + } + + for (std::map::const_iterator i = this->_image_name->begin() ; i != this->_image_name->end() ; ++i) { + g_free(i->first); + } + + delete this->_image_name; + + SPObject::release(); +} + +/** + * Sets a specific value in the SPFilter. + */ +void SPFilter::set(SPAttributeEnum key, gchar const *value) { + switch (key) { + case SP_ATTR_FILTERUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->filterUnits = SP_FILTER_UNITS_USERSPACEONUSE; + } else { + this->filterUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + } + + this->filterUnits_set = TRUE; + } else { + this->filterUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + this->filterUnits_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_PRIMITIVEUNITS: + if (value) { + if (!strcmp(value, "objectBoundingBox")) { + this->primitiveUnits = SP_FILTER_UNITS_OBJECTBOUNDINGBOX; + } else { + this->primitiveUnits = SP_FILTER_UNITS_USERSPACEONUSE; + } + + this->primitiveUnits_set = TRUE; + } else { + this->primitiveUnits = SP_FILTER_UNITS_USERSPACEONUSE; + this->primitiveUnits_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_X: + this->x.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_Y: + this->y.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_WIDTH: + this->width.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_HEIGHT: + this->height.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_FILTERRES: + this->filterRes.set(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_XLINK_HREF: + if (value) { + try { + this->href->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->href->detach(); + } + } else { + this->href->detach(); + } + break; + default: + // See if any parents need this value. + SPObject::set(key, value); + break; + } +} + + +/** + * Returns the number of references to the filter. + */ +guint SPFilter::getRefCount() { + // NOTE: this is currently updated by sp_style_filter_ref_changed() in style.cpp + return _refcount; +} + +/** + * Receives update notifications. + */ +void SPFilter::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx *ictx = (SPItemCtx *) ctx; + + // Do here since we know viewport (Bounding box case handled during rendering) + // Note: This only works for root viewport since this routine is not called after + // setting a new viewport. A true fix requires a strategy like SPItemView or SPMarkerView. + if(this->filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) { + this->calcDimsFromParentViewport(ictx, true); + } + /* do something to trigger redisplay, updates? */ + + } + + // Update filter primitives in order to update filter primitive area + // (SPObject::ActionUpdate is not actually used) + unsigned childflags = flags; + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l(this->childList(true, SPObject::ActionUpdate)); + for(SPObject* child: l){ + if( SP_IS_FILTER_PRIMITIVE( child ) ) { + child->updateDisplay(ctx, childflags); + } + sp_object_unref(child); + } + + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node* SPFilter::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { + // Original from sp-item-group.cpp + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = doc->createElement("svg:filter"); + } + + std::vector l; + for (auto& child: children) { + Inkscape::XML::Node *crepr = child.updateRepr(doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + child.updateRepr(flags); + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->filterUnits_set) { + switch (this->filterUnits) { + case SP_FILTER_UNITS_USERSPACEONUSE: + repr->setAttribute("filterUnits", "userSpaceOnUse"); + break; + default: + repr->setAttribute("filterUnits", "objectBoundingBox"); + break; + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->primitiveUnits_set) { + switch (this->primitiveUnits) { + case SP_FILTER_UNITS_OBJECTBOUNDINGBOX: + repr->setAttribute("primitiveUnits", "objectBoundingBox"); + break; + default: + repr->setAttribute("primitiveUnits", "userSpaceOnUse"); + break; + } + } + + if (this->x._set) { + sp_repr_set_svg_double(repr, "x", this->x.computed); + } else { + repr->removeAttribute("x"); + } + + if (this->y._set) { + sp_repr_set_svg_double(repr, "y", this->y.computed); + } else { + repr->removeAttribute("y"); + } + + if (this->width._set) { + sp_repr_set_svg_double(repr, "width", this->width.computed); + } else { + repr->removeAttribute("width"); + } + + if (this->height._set) { + sp_repr_set_svg_double(repr, "height", this->height.computed); + } else { + repr->removeAttribute("height"); + } + + if (this->filterRes.getNumber()>=0) { + gchar *tmp = this->filterRes.getValueString(); + repr->setAttribute("filterRes", tmp); + g_free(tmp); + } else { + repr->removeAttribute("filterRes"); + } + + if (this->href->getURI()) { + auto uri_string = this->href->getURI()->str(); + repr->setAttributeOrRemoveIfEmpty("xlink:href", uri_string); + } + + SPObject::write(doc, repr, flags); + + return repr; +} + + +/** + * Gets called when the filter is (re)attached to another filter. + */ +static void +filter_ref_changed(SPObject *old_ref, SPObject *ref, SPFilter *filter) +{ + if (old_ref) { + filter->modified_connection.disconnect(); + } + + if ( SP_IS_FILTER(ref) + && ref != filter ) + { + filter->modified_connection = + ref->connectModified(sigc::bind(sigc::ptr_fun(&filter_ref_modified), filter)); + } + + filter_ref_modified(ref, 0, filter); +} + +static void filter_ref_modified(SPObject */*href*/, guint /*flags*/, SPFilter *filter) +{ + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for child_added event. + */ +void SPFilter::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPFilter::remove_child(Inkscape::XML::Node *child) { + SPObject::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFilter::build_renderer(Inkscape::Filters::Filter *nr_filter) +{ + g_assert(nr_filter != nullptr); + + this->_renderer = nr_filter; + + nr_filter->set_filter_units(this->filterUnits); + nr_filter->set_primitive_units(this->primitiveUnits); + nr_filter->set_x(this->x); + nr_filter->set_y(this->y); + nr_filter->set_width(this->width); + nr_filter->set_height(this->height); + + if (this->filterRes.getNumber() >= 0) { + if (this->filterRes.getOptNumber() >= 0) { + nr_filter->set_resolution(this->filterRes.getNumber(), + this->filterRes.getOptNumber()); + } else { + nr_filter->set_resolution(this->filterRes.getNumber()); + } + } + + nr_filter->clear_primitives(); + for(auto& primitive_obj: this->children) { + if (SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + SPFilterPrimitive *primitive = SP_FILTER_PRIMITIVE(&primitive_obj); + g_assert(primitive != nullptr); + +// if (((SPFilterPrimitiveClass*) G_OBJECT_GET_CLASS(primitive))->build_renderer) { +// ((SPFilterPrimitiveClass *) G_OBJECT_GET_CLASS(primitive))->build_renderer(primitive, nr_filter); +// } else { +// g_warning("Cannot build filter renderer: missing builder"); +// } // CPPIFY: => FilterPrimitive should be abstract. + primitive->build_renderer(nr_filter); + } + } +} + +int SPFilter::primitive_count() const { + int count = 0; + + for(const auto& primitive_obj: this->children) { + if (SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + count++; + } + } + + return count; +} + +int SPFilter::get_image_name(gchar const *name) const { + std::map::iterator result = this->_image_name->find(const_cast(name)); + if (result == this->_image_name->end()) return -1; + else return (*result).second; +} + +int SPFilter::set_image_name(gchar const *name) { + int value = this->_image_number_next; + this->_image_number_next++; + gchar *name_copy = strdup(name); + std::pair new_pair(name_copy, value); + const std::pair::iterator,bool> ret = this->_image_name->insert(new_pair); + if (ret.second == false) { + // The element is not inserted (because an element with the same key was already in the map) + // Therefore, free the memory allocated for the new entry: + free(name_copy); + + return (*ret.first).second; + } + return value; +} + +gchar const *SPFilter::name_for_image(int const image) const { + switch (image) { + case Inkscape::Filters::NR_FILTER_SOURCEGRAPHIC: + return "SourceGraphic"; + break; + case Inkscape::Filters::NR_FILTER_SOURCEALPHA: + return "SourceAlpha"; + break; + case Inkscape::Filters::NR_FILTER_BACKGROUNDIMAGE: + return "BackgroundImage"; + break; + case Inkscape::Filters::NR_FILTER_BACKGROUNDALPHA: + return "BackgroundAlpha"; + break; + case Inkscape::Filters::NR_FILTER_STROKEPAINT: + return "StrokePaint"; + break; + case Inkscape::Filters::NR_FILTER_FILLPAINT: + return "FillPaint"; + break; + case Inkscape::Filters::NR_FILTER_SLOT_NOT_SET: + case Inkscape::Filters::NR_FILTER_UNNAMED_SLOT: + return nullptr; + break; + default: + for (std::map::const_iterator i + = this->_image_name->begin() ; + i != this->_image_name->end() ; ++i) { + if (i->second == image) { + return i->first; + } + } + } + return nullptr; +} + +Glib::ustring SPFilter::get_new_result_name() const { + int largest = 0; + + for(const auto& primitive_obj: this->children) { + if (SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + const Inkscape::XML::Node *repr = primitive_obj.getRepr(); + char const *result = repr->attribute("result"); + int index; + if (result) + { + if (sscanf(result, "result%5d", &index) == 1) + { + if (index > largest) + { + largest = index; + } + } + } + } + } + + return "result" + Glib::Ascii::dtostr(largest + 1); +} + +bool ltstr::operator()(const char* s1, const char* s2) const +{ + return strcmp(s1, s2) < 0; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-filter.h b/src/object/sp-filter.h new file mode 100644 index 0000000..c7af8bf --- /dev/null +++ b/src/object/sp-filter.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG element + *//* + * Authors: + * Hugo Rodrigues + * Niko Kiirala + * + * Copyright (C) 2006,2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_FILTER_H_SEEN +#define SP_FILTER_H_SEEN + +#include +#include + +#include "number-opt-number.h" +#include "sp-dimensions.h" +#include "sp-object.h" +#include "sp-filter-units.h" +#include "svg/svg-length.h" + +#define SP_FILTER(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FILTER(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FILTER_FILTER_UNITS(f) (SP_FILTER(f)->filterUnits) +#define SP_FILTER_PRIMITIVE_UNITS(f) (SP_FILTER(f)->primitiveUnits) + +namespace Inkscape { +namespace Filters { +class Filter; +} } + +class SPFilterReference; +class SPFilterPrimitive; + +struct ltstr { + bool operator()(const char* s1, const char* s2) const; +}; + +class SPFilter : public SPObject, public SPDimensions { +public: + SPFilter(); + ~SPFilter() override; + + /* Initializes the given Inkscape::Filters::Filter object as a renderer for this + * SPFilter object. */ + void build_renderer(Inkscape::Filters::Filter *nr_filter); + + /// Returns the number of filter primitives in this SPFilter object. + int primitive_count() const; + + /// Returns a slot number for given image name, or -1 for unknown name. + int get_image_name(char const *name) const; + + /// Returns slot number for given image name, even if it's unknown. + int set_image_name(char const *name); + + /** Finds image name based on it's slot number. Returns 0 for unknown slot + * numbers. */ + char const *name_for_image(int const image) const; + + /// Returns a result image name that is not in use inside this filter. + Glib::ustring get_new_result_name() const; + + SPFilterUnits filterUnits; + unsigned int filterUnits_set : 1; + SPFilterUnits primitiveUnits; + unsigned int primitiveUnits_set : 1; + NumberOptNumber filterRes; + SPFilterReference *href; + sigc::connection modified_connection; + + guint getRefCount(); + guint _refcount; + + Inkscape::Filters::Filter *_renderer; + + std::map* _image_name; + int _image_number_next; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttributeEnum key, const char* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SP_FILTER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-flowdiv.cpp b/src/object/sp-flowdiv.cpp new file mode 100644 index 0000000..366cdb0 --- /dev/null +++ b/src/object/sp-flowdiv.cpp @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include "xml/repr.h" +#include "sp-flowdiv.h" +#include "sp-string.h" +#include "document.h" + +SPFlowdiv::SPFlowdiv() : SPItem() { +} + +SPFlowdiv::~SPFlowdiv() = default; + +void SPFlowdiv::release() { + SPItem::release(); +} + +void SPFlowdiv::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast(ctx); + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (SP_IS_ITEM(child)) { + SPItem const &chi = *SP_ITEM(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); +} + +void SPFlowdiv::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowdiv::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->_requireSVGVersion(Inkscape::Version(1, 2)); + + SPItem::build(doc, repr); +} + +void SPFlowdiv::set(SPAttributeEnum key, const gchar* value) { + SPItem::set(key, value); +} + + +Inkscape::XML::Node* SPFlowdiv::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowDiv"); + } + + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( SP_IS_FLOWTSPAN (&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_STRING(&child) ) { + c_repr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( SP_IS_FLOWTSPAN (&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_STRING(&child) ) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowtspan::SPFlowtspan() : SPItem() { +} + +SPFlowtspan::~SPFlowtspan() = default; + +void SPFlowtspan::release() { + SPItem::release(); +} + +void SPFlowtspan::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast(ctx); + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (SP_IS_ITEM(child)) { + SPItem const &chi = *SP_ITEM(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); +} + +void SPFlowtspan::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowtspan::build(SPDocument *doc, Inkscape::XML::Node *repr) { + SPItem::build(doc, repr); +} + +void SPFlowtspan::set(SPAttributeEnum key, const gchar* value) { + SPItem::set(key, value); +} + +Inkscape::XML::Node *SPFlowtspan::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags&SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowSpan"); + } + + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( SP_IS_FLOWTSPAN(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_STRING(&child) ) { + c_repr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( SP_IS_FLOWTSPAN(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_STRING(&child) ) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ +SPFlowpara::SPFlowpara() : SPItem() { +} + +SPFlowpara::~SPFlowpara() = default; + +void SPFlowpara::release() { + SPItem::release(); +} + +void SPFlowpara::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast(ctx); + SPItemCtx cctx = *ictx; + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (SP_IS_ITEM(child)) { + SPItem const &chi = *SP_ITEM(child); + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, flags); + } else { + child->updateDisplay(ctx, flags); + } + } + sp_object_unref(child); + } +} + +void SPFlowpara::modified(unsigned int flags) { + SPItem::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +void SPFlowpara::build(SPDocument *doc, Inkscape::XML::Node *repr) { + SPItem::build(doc, repr); +} + +void SPFlowpara::set(SPAttributeEnum key, const gchar* value) { + SPItem::set(key, value); +} + +Inkscape::XML::Node *SPFlowpara::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags&SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowPara"); + } + + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr = nullptr; + + if ( SP_IS_FLOWTSPAN(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_STRING(&child) ) { + c_repr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( SP_IS_FLOWTSPAN(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_FLOWPARA(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_STRING(&child) ) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowline::SPFlowline() : SPObject() { +} + +SPFlowline::~SPFlowline() = default; + +void SPFlowline::release() { + SPObject::release(); +} + +void SPFlowline::modified(unsigned int flags) { + SPObject::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; +} + +Inkscape::XML::Node *SPFlowline::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowLine"); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/* + * + */ + +SPFlowregionbreak::SPFlowregionbreak() : SPObject() { +} + +SPFlowregionbreak::~SPFlowregionbreak() = default; + +void SPFlowregionbreak::release() { + SPObject::release(); +} + +void SPFlowregionbreak::modified(unsigned int flags) { + SPObject::modified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; +} + +Inkscape::XML::Node *SPFlowregionbreak::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowLine"); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-flowdiv.h b/src/object/sp-flowdiv.h new file mode 100644 index 0000000..20c2bd1 --- /dev/null +++ b/src/object/sp-flowdiv.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWDIV_H +#define SEEN_SP_ITEM_FLOWDIV_H + +/* + */ + +#include "sp-object.h" +#include "sp-item.h" + +#define SP_FLOWDIV(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWDIV(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FLOWTSPAN(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWTSPAN(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FLOWPARA(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWPARA(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FLOWLINE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWLINE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FLOWREGIONBREAK(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWREGIONBREAK(obj) (dynamic_cast((SPObject*)obj) != NULL) + +// these 3 are derivatives of SPItem to get the automatic style handling +class SPFlowdiv : public SPItem { +public: + SPFlowdiv(); + ~SPFlowdiv() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowtspan : public SPItem { +public: + SPFlowtspan(); + ~SPFlowtspan() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowpara : public SPItem { +public: + SPFlowpara(); + ~SPFlowpara() override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +// these do not need any style +class SPFlowline : public SPObject { +public: + SPFlowline(); + ~SPFlowline() override; + +protected: + void release() override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +class SPFlowregionbreak : public SPObject { +public: + SPFlowregionbreak(); + ~SPFlowregionbreak() override; + +protected: + void release() override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-flowregion.cpp b/src/object/sp-flowregion.cpp new file mode 100644 index 0000000..f1accf2 --- /dev/null +++ b/src/object/sp-flowregion.cpp @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include + +#include +#include "display/curve.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "sp-use.h" +#include "style.h" +#include "document.h" +#include "sp-title.h" +#include "sp-desc.h" + +#include "sp-flowregion.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + + +static void GetDest(SPObject* child,Shape **computed); + + +SPFlowregion::SPFlowregion() : SPItem() { +} + +SPFlowregion::~SPFlowregion() { + for (auto & it : this->computed) { + delete it; + } +} + +void SPFlowregion::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPFlowregion::remove_child(Inkscape::XML::Node * child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPFlowregion::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast(ctx); + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vectorl; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + SPItem *item = dynamic_cast(child); + + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); + + this->UpdateComputed(); +} + +void SPFlowregion::UpdateComputed() +{ + for (auto & it : computed) { + delete it; + } + computed.clear(); + + for (auto& child: children) { + Shape *shape = nullptr; + GetDest(&child, &shape); + computed.push_back(shape); + } +} + +void SPFlowregion::modified(guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vectorl; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node *SPFlowregion::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowRegion"); + } + + std::vector l; + for (auto& child: children) { + if ( !dynamic_cast(&child) && !dynamic_cast(&child) ) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + } + + for (auto i = l.rbegin(); i != l.rend(); ++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + + for (auto& child: children) { + if ( !dynamic_cast(&child) && !dynamic_cast(&child) ) { + child.updateRepr(flags); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + this->UpdateComputed(); // copied from update(), see LP Bug 1339305 + + return repr; +} + +const char* SPFlowregion::displayName() const { + // TRANSLATORS: "Flow region" is an area where text is allowed to flow + return _("Flow Region"); +} + +SPFlowregionExclude::SPFlowregionExclude() : SPItem() { + this->computed = nullptr; +} + +SPFlowregionExclude::~SPFlowregionExclude() { + if (this->computed) { + delete this->computed; + this->computed = nullptr; + } +} + +void SPFlowregionExclude::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPFlowregionExclude::remove_child(Inkscape::XML::Node * child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPFlowregionExclude::update(SPCtx *ctx, unsigned int flags) { + SPItemCtx *ictx = reinterpret_cast(ctx); + SPItemCtx cctx = *ictx; + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for(auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + SPItem *item = dynamic_cast(child); + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, flags); + } else { + child->updateDisplay(ctx, flags); + } + } + + sp_object_unref(child); + } + + this->UpdateComputed(); +} + + +void SPFlowregionExclude::UpdateComputed() +{ + if (computed) { + delete computed; + computed = nullptr; + } + + for (auto& child: children) { + GetDest(&child, &computed); + } +} + +void SPFlowregionExclude::modified(guint flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node *SPFlowregionExclude::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if ( repr == nullptr ) { + repr = xml_doc->createElement("svg:flowRegionExclude"); + } + + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + + for (auto i = l.rbegin(); i != l.rend(); ++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + + } else { + for (auto& child: children) { + child.updateRepr(flags); + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPFlowregionExclude::displayName() const { + /* TRANSLATORS: A region "cut out of" a flow region; text is not allowed to flow inside the + * flow excluded region. flowRegionExclude in SVG 1.2: see + * http://www.w3.org/TR/2004/WD-SVG12-20041027/flow.html#flowRegion-elem and + * http://www.w3.org/TR/2004/WD-SVG12-20041027/flow.html#flowRegionExclude-elem. */ + return _("Flow Excluded Region"); +} + +static void UnionShape(Shape **base_shape, Shape const *add_shape) +{ + if (*base_shape == nullptr) + *base_shape = new Shape; + if ( (*base_shape)->hasEdges() == false ) { + (*base_shape)->Copy(const_cast(add_shape)); + } else if ( add_shape->hasEdges() ) { + Shape* temp=new Shape; + temp->Booleen(const_cast(add_shape), *base_shape, bool_op_union); + delete *base_shape; + *base_shape = temp; + } +} + +static void GetDest(SPObject* child,Shape **computed) +{ + if ( child == nullptr || dynamic_cast(child) == nullptr ) return; + + SPCurve *curve=nullptr; + Geom::Affine tr_mat; + + SPObject* u_child = child; + SPItem *item = dynamic_cast(u_child); + g_assert(item != nullptr); + SPUse *use = dynamic_cast(item); + if ( use ) { + u_child = use->child; + tr_mat = use->getRelativeTransform(child->parent); + } else { + tr_mat = item->transform; + } + SPShape *shape = dynamic_cast(u_child); + if ( shape ) { + if (!(shape->_curve)) { + shape->set_shape(); + } + curve = shape->getCurve(); + } else { + SPText *text = dynamic_cast(u_child); + if ( text ) { + curve = text->getNormalizedBpath(); + } + } + + if ( curve ) { + Path* temp=new Path; + temp->LoadPathVector(curve->get_pathvector(), tr_mat, true); + Shape* n_shp=new Shape; + temp->Convert(0.25); + temp->Fill(n_shp,0); + Shape* uncross=new Shape; + SPStyle* style = u_child->style; + if ( style && style->fill_rule.computed == SP_WIND_RULE_EVENODD ) { + uncross->ConvertToShape(n_shp,fill_oddEven); + } else { + uncross->ConvertToShape(n_shp,fill_nonZero); + } + UnionShape(computed, uncross); + delete uncross; + delete n_shp; + delete temp; + curve->unref(); + } else { +// printf("no curve\n"); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-flowregion.h b/src/object/sp-flowregion.h new file mode 100644 index 0000000..83464b1 --- /dev/null +++ b/src/object/sp-flowregion.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWREGION_H +#define SEEN_SP_ITEM_FLOWREGION_H + +/* + */ + +#include "sp-item.h" + +#define SP_FLOWREGION(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWREGION(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_FLOWREGIONEXCLUDE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWREGIONEXCLUDE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class Path; +class Shape; +class flow_dest; +class FloatLigne; + +class SPFlowregion : public SPItem { +public: + SPFlowregion(); + ~SPFlowregion() override; + + std::vector computed; + + void UpdateComputed(); + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* displayName() const override; +}; + +class SPFlowregionExclude : public SPItem { +public: + SPFlowregionExclude(); + ~SPFlowregionExclude() override; + + Shape *computed; + + void UpdateComputed(); + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* displayName() const override; +}; + +#endif diff --git a/src/object/sp-flowtext.cpp b/src/object/sp-flowtext.cpp new file mode 100644 index 0000000..75f9beb --- /dev/null +++ b/src/object/sp-flowtext.cpp @@ -0,0 +1,767 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + */ + +#include +#include +#include + +#include "attributes.h" +#include "xml/repr.h" +#include "style.h" +#include "inkscape.h" +#include "document.h" + +#include "desktop.h" + +#include "text-tag-attributes.h" +#include "text-editing.h" + +#include "sp-flowdiv.h" +#include "sp-flowregion.h" +#include "sp-flowtext.h" +#include "sp-rect.h" +#include "sp-string.h" +#include "sp-text.h" +#include "sp-use.h" + +#include "libnrtype/font-instance.h" + +#include "livarot/Shape.h" + +#include "display/drawing-text.h" + +SPFlowtext::SPFlowtext() : SPItem(), + par_indent(0), + _optimizeScaledText(false) +{ +} + +SPFlowtext::~SPFlowtext() = default; + +void SPFlowtext::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPItem::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/* fixme: hide (Lauris) */ + +void SPFlowtext::remove_child(Inkscape::XML::Node* child) { + SPItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFlowtext::update(SPCtx* ctx, unsigned int flags) { + SPItemCtx *ictx = (SPItemCtx *) ctx; + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + g_assert(child != nullptr); + + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + SPItem *item = dynamic_cast(child); + if (item) { + SPItem const &chi = *item; + cctx.i2doc = chi.transform * ictx->i2doc; + cctx.i2vp = chi.transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + SPItem::update(ctx, flags); + + this->rebuildLayout(); + + Geom::OptRect pbox = this->geometricBounds(); + + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + this->_clearFlow(g); + g->setStyle(this->style); + // pass the bbox of the flowtext object as paintbox (used for paintserver fills) + this->layout.show(g, pbox); + } +} + +void SPFlowtext::modified(unsigned int flags) { + SPObject *region = nullptr; + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + // FIXME: the below stanza is copied over from sp_text_modified, consider factoring it out + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) { + Geom::OptRect pbox = geometricBounds(); + + for (SPItemView* v = display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + _clearFlow(g); + g->setStyle(style); + layout.show(g, pbox); + } + } + + for (auto& o: children) { + if (dynamic_cast(&o)) { + region = &o; + break; + } + } + + if (region) { + if (flags || (region->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + region->emitModified(flags); // pass down to the region only + } + } +} + +void SPFlowtext::build(SPDocument* doc, Inkscape::XML::Node* repr) { + this->_requireSVGVersion(Inkscape::Version(1, 2)); + + SPItem::build(doc, repr); + + this->readAttr( "inkscape:layoutOptions" ); // must happen after css has been read +} + +void SPFlowtext::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_LAYOUT_OPTIONS: { + // deprecated attribute, read for backward compatibility only + //XML Tree being directly used while it shouldn't be. + SPCSSAttr *opts = sp_repr_css_attr(this->getRepr(), "inkscape:layoutOptions"); + { + gchar const *val = sp_repr_css_property(opts, "justification", nullptr); + + if (val != nullptr && !this->style->text_align.set) { + if ( strcmp(val, "0") == 0 || strcmp(val, "false") == 0 ) { + this->style->text_align.value = SP_CSS_TEXT_ALIGN_LEFT; + } else { + this->style->text_align.value = SP_CSS_TEXT_ALIGN_JUSTIFY; + } + + this->style->text_align.set = TRUE; + this->style->text_align.inherit = FALSE; + this->style->text_align.computed = this->style->text_align.value; + } + } + /* no equivalent css attribute for these two (yet) + { + gchar const *val = sp_repr_css_property(opts, "layoutAlgo", NULL); + if ( val == NULL ) { + group->algo = 0; + } else { + if ( strcmp(val, "better") == 0 ) { // knuth-plass, never worked for general cases + group->algo = 2; + } else if ( strcmp(val, "simple") == 0 ) { // greedy, but allowed lines to be compressed by up to 20% if it would make them fit + group->algo = 1; + } else if ( strcmp(val, "default") == 0 ) { // the same one we use, a standard greedy + group->algo = 0; + } + } + } + */ + { // This would probably translate to padding-left, if SPStyle had it. + gchar const *val = sp_repr_css_property(opts, "par-indent", nullptr); + + if ( val == nullptr ) { + this->par_indent = 0.0; + } else { + this->par_indent = g_ascii_strtod(val, nullptr); + } + } + + sp_repr_css_attr_unref(opts); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + default: + SPItem::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPFlowtext::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + if ( flags & SP_OBJECT_WRITE_BUILD ) { + if ( repr == nullptr ) { + repr = doc->createElement("svg:flowRoot"); + } + + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node *c_repr = nullptr; + + if ( dynamic_cast(&child) || dynamic_cast(&child) || dynamic_cast(&child) || dynamic_cast(&child)) { + c_repr = child.updateRepr(doc, nullptr, flags); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( dynamic_cast(&child) || dynamic_cast(&child) || dynamic_cast(&child) || dynamic_cast(&child)) { + child.updateRepr(flags); + } + } + } + + this->rebuildLayout(); // copied from update(), see LP Bug 1339305 + + SPItem::write(doc, repr, flags); + + return repr; +} + +Geom::OptRect SPFlowtext::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox = this->layout.bounds(transform); + + // Add stroke width + // FIXME this code is incorrect + if (bbox && type == SPItem::VISUAL_BBOX && !this->style->stroke.isNone()) { + double scale = transform.descrim(); + bbox->expandBy(0.5 * this->style->stroke_width.computed * scale); + } + + return bbox; +} + +void SPFlowtext::print(SPPrintContext *ctx) { + Geom::OptRect pbox, bbox, dbox; + pbox = this->geometricBounds(); + bbox = this->desktopVisualBounds(); + dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions()); + + Geom::Affine const ctm (this->i2dt_affine()); + + this->layout.print(ctx, pbox, dbox, bbox, ctm); +} + +const char* SPFlowtext::displayName() const { + if (has_internal_frame()) { + return _("Flowed Text"); + } else { + return _("Linked Flowed Text"); + } +} + +gchar* SPFlowtext::description() const { + int const nChars = layout.iteratorToCharIndex(layout.end()); + char const *trunc = (layout.inputTruncated()) ? _(" [truncated]") : ""; + + return g_strdup_printf(ngettext("(%d character%s)", "(%d characters%s)", nChars), nChars, trunc); +} + +void SPFlowtext::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) { + // Choose a point on the baseline for snapping from or to, with the horizontal position + // of this point depending on the text alignment (left vs. right) + Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this); + + if (layout != nullptr && layout->outputExists()) { + boost::optional pt = layout->baselineAnchorPoint(); + + if (pt) { + p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR); + } + } + } +} + +Inkscape::DrawingItem* SPFlowtext::show(Inkscape::Drawing &drawing, unsigned int /*key*/, unsigned int /*flags*/) { + Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing); + flowed->setPickChildren(false); + flowed->setStyle(this->style); + + // pass the bbox of the flowtext object as paintbox (used for paintserver fills) + Geom::OptRect bbox = this->geometricBounds(); + this->layout.show(flowed, bbox); + + return flowed; +} + +void SPFlowtext::hide(unsigned int key) { + for (SPItemView* v = this->display; v != nullptr; v = v->next) { + if (v->key == key) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + this->_clearFlow(g); + } + } +} + + +/* + * + */ +void SPFlowtext::_buildLayoutInput(SPObject *root, Shape const *exclusion_shape, std::list *shapes, SPObject **pending_line_break_object) +{ + Inkscape::Text::Layout::OptionalTextTagAttrs pi; + bool with_indent = false; + + if (dynamic_cast(root) || dynamic_cast(root)) { + + layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE; + + layout.strut.reset(); + if (style) { + font_instance *font = font_factory::Default()->FaceFromStyle( style ); + if (font) { + font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight); + font->Unref(); + } + layout.strut *= style->font_size.computed; + if (style->line_height.normal ) { + layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL ); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + layout.strut.computeEffective( style->line_height.computed ); + } else { + if( style->font_size.computed > 0.0 ) { + layout.strut.computeEffective( style->line_height.computed/style->font_size.computed ); + } + } + } + + // emulate par-indent with the first char's kern + SPObject *t = root; + SPFlowtext *ft = nullptr; + while (t && !ft) { + ft = dynamic_cast(t); + t = t->parent; + } + + if (ft) { + double indent = ft->par_indent; + if (indent != 0) { + with_indent = true; + SVGLength sl; + sl.value = sl.computed = indent; + sl._set = true; + pi.dx.push_back(sl); + } + } + } + + if (*pending_line_break_object) { + if (dynamic_cast(*pending_line_break_object)) { + layout.appendControlCode(Inkscape::Text::Layout::SHAPE_BREAK, *pending_line_break_object); + } else { + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, *pending_line_break_object); + } + *pending_line_break_object = nullptr; + } + + for (auto& child: root->children) { + SPString *str = dynamic_cast(&child); + if (str) { + if (*pending_line_break_object) { + if (dynamic_cast(*pending_line_break_object)) + layout.appendControlCode(Inkscape::Text::Layout::SHAPE_BREAK, *pending_line_break_object); + else { + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, *pending_line_break_object); + } + *pending_line_break_object = nullptr; + } + if (with_indent) { + layout.appendText(str->string, root->style, &child, &pi); + } else { + layout.appendText(str->string, root->style, &child); + } + } else { + SPFlowregion *region = dynamic_cast(&child); + if (region) { + std::vector const &computed = region->computed; + for (auto it : computed) { + shapes->push_back(Shape()); + if (exclusion_shape->hasEdges()) { + shapes->back().Booleen(it, const_cast(exclusion_shape), bool_op_diff); + } else { + shapes->back().Copy(it); + } + layout.appendWrapShape(&shapes->back()); + } + } + //Xml Tree is being directly used while it shouldn't be. + else if (!dynamic_cast(&child) && !sp_repr_is_meta_element(child.getRepr())) { + _buildLayoutInput(&child, exclusion_shape, shapes, pending_line_break_object); + } + } + } + + if (dynamic_cast(root) || dynamic_cast(root) || dynamic_cast(root) || dynamic_cast(root)) { + if (!root->hasChildren()) { + layout.appendText("", root->style, root); + } + *pending_line_break_object = root; + } +} + +Shape* SPFlowtext::_buildExclusionShape() const +{ + Shape *shape = new Shape(); + Shape *shape_temp = new Shape(); + + for (auto& child: children) { + // RH: is it right that this shouldn't be recursive? + SPFlowregionExclude *c_child = dynamic_cast(const_cast(&child)); + if ( c_child && c_child->computed && c_child->computed->hasEdges() ) { + if (shape->hasEdges()) { + shape_temp->Booleen(shape, c_child->computed, bool_op_union); + std::swap(shape, shape_temp); + } else { + shape->Copy(c_child->computed); + } + } + } + + delete shape_temp; + + return shape; +} + +void SPFlowtext::rebuildLayout() +{ + std::list shapes; + + layout.clear(); + Shape *exclusion_shape = _buildExclusionShape(); + SPObject *pending_line_break_object = nullptr; + _buildLayoutInput(this, exclusion_shape, &shapes, &pending_line_break_object); + delete exclusion_shape; + layout.calculateFlow(); +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif +} + +void SPFlowtext::_clearFlow(Inkscape::DrawingGroup *in_arena) +{ + in_arena->clearChildren(); +} + +Inkscape::XML::Node *SPFlowtext::getAsText() +{ + if (!this->layout.outputExists()) { + return nullptr; + } + + Inkscape::XML::Document *xml_doc = this->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:text"); + repr->setAttribute("xml:space", "preserve"); + repr->setAttribute("style", this->getRepr()->attribute("style")); + Geom::Point anchor_point = this->layout.characterAnchorPoint(this->layout.begin()); + sp_repr_set_svg_double(repr, "x", anchor_point[Geom::X]); + sp_repr_set_svg_double(repr, "y", anchor_point[Geom::Y]); + + for (Inkscape::Text::Layout::iterator it = this->layout.begin() ; it != this->layout.end() ; ) { + Inkscape::XML::Node *line_tspan = xml_doc->createElement("svg:tspan"); + line_tspan->setAttribute("sodipodi:role", "line"); + + Inkscape::Text::Layout::iterator it_line_end = it; + it_line_end.nextStartOfLine(); + + while (it != it_line_end) { + + Inkscape::XML::Node *span_tspan = xml_doc->createElement("svg:tspan"); + Geom::Point anchor_point = this->layout.characterAnchorPoint(it); + // use kerning to simulate justification and whatnot + Inkscape::Text::Layout::iterator it_span_end = it; + it_span_end.nextStartOfSpan(); + Inkscape::Text::Layout::OptionalTextTagAttrs attrs; + this->layout.simulateLayoutUsingKerning(it, it_span_end, &attrs); + // set x,y attributes only when we need to + bool set_x = false; + bool set_y = false; + if (!this->transform.isIdentity()) { + set_x = set_y = true; + } else { + Inkscape::Text::Layout::iterator it_chunk_start = it; + it_chunk_start.thisStartOfChunk(); + if (it == it_chunk_start) { + set_x = true; + // don't set y so linespacing adjustments and things will still work + } + Inkscape::Text::Layout::iterator it_shape_start = it; + it_shape_start.thisStartOfShape(); + if (it == it_shape_start) + set_y = true; + } + if (set_x && !attrs.dx.empty()) + attrs.dx[0] = 0.0; + TextTagAttributes(attrs).writeTo(span_tspan); + if (set_x) + sp_repr_set_svg_double(span_tspan, "x", anchor_point[Geom::X]); // FIXME: this will pick up the wrong end of counter-directional runs + if (set_y) + sp_repr_set_svg_double(span_tspan, "y", anchor_point[Geom::Y]); + if (line_tspan->childCount() == 0) { + sp_repr_set_svg_double(line_tspan, "x", anchor_point[Geom::X]); // FIXME: this will pick up the wrong end of counter-directional runs + sp_repr_set_svg_double(line_tspan, "y", anchor_point[Geom::Y]); + } + + SPObject *source_obj = nullptr; + Glib::ustring::iterator span_text_start_iter; + this->layout.getSourceOfCharacter(it, &source_obj, &span_text_start_iter); + + Glib::ustring style_text = (dynamic_cast(source_obj) ? source_obj->parent : source_obj)->style->write( SP_STYLE_FLAG_IFDIFF, SP_STYLE_SRC_UNSET, this->style); + span_tspan->setAttributeOrRemoveIfEmpty("style", style_text); + + SPString *str = dynamic_cast(source_obj); + if (str) { + Glib::ustring *string = &(str->string); // TODO fixme: dangerous, unsafe premature-optimization + SPObject *span_end_obj = nullptr; + Glib::ustring::iterator span_text_end_iter; + this->layout.getSourceOfCharacter(it_span_end, &span_end_obj, &span_text_end_iter); + if (span_end_obj != source_obj) { + if (it_span_end == this->layout.end()) { + span_text_end_iter = span_text_start_iter; + for (int i = this->layout.iteratorToCharIndex(it_span_end) - this->layout.iteratorToCharIndex(it) ; i ; --i) + ++span_text_end_iter; + } else + span_text_end_iter = string->end(); // spans will never straddle a source boundary + } + + if (span_text_start_iter != span_text_end_iter) { + Glib::ustring new_string; + while (span_text_start_iter != span_text_end_iter) + new_string += *span_text_start_iter++; // grr. no substr() with iterators + Inkscape::XML::Node *new_text = xml_doc->createTextNode(new_string.c_str()); + span_tspan->appendChild(new_text); + Inkscape::GC::release(new_text); + } + } + it = it_span_end; + + line_tspan->appendChild(span_tspan); + Inkscape::GC::release(span_tspan); + } + repr->appendChild(line_tspan); + Inkscape::GC::release(line_tspan); + } + + return repr; +} + +SPItem const *SPFlowtext::get_frame(SPItem const *after) const +{ + SPItem *item = const_cast(this)->get_frame(after); + return item; +} + +SPItem *SPFlowtext::get_frame(SPItem const *after) +{ + SPItem *frame = nullptr; + + SPObject *region = nullptr; + for (auto& o: children) { + if (dynamic_cast(&o)) { + region = &o; + break; + } + } + + if (region) { + bool past = false; + + for (auto& o: region->children) { + SPItem *item = dynamic_cast(&o); + if (item) { + if ( (after == nullptr) || past ) { + frame = item; + } else { + if (item == after) { + past = true; + } + } + } + } + + SPUse *use = dynamic_cast(frame); + if ( use ) { + frame = use->get_original(); + } + } + return frame; +} + +bool SPFlowtext::has_internal_frame() const +{ + SPItem const *frame = get_frame(nullptr); + + return (frame && isAncestorOf(frame) && dynamic_cast(frame)); +} + + +SPItem *create_flowtext_with_internal_frame (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *root_repr = xml_doc->createElement("svg:flowRoot"); + root_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + SPItem *ft_item = dynamic_cast(desktop->currentLayer()->appendChildRepr(root_repr)); + g_assert(ft_item != nullptr); + SPObject *root_object = doc->getObjectByRepr(root_repr); + g_assert(dynamic_cast(root_object) != nullptr); + + Inkscape::XML::Node *region_repr = xml_doc->createElement("svg:flowRegion"); + root_repr->appendChild(region_repr); + SPObject *region_object = doc->getObjectByRepr(region_repr); + g_assert(dynamic_cast(region_object) != nullptr); + + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); // FIXME: use path!!! after rects are converted to use path + region_repr->appendChild(rect_repr); + + SPRect *rect = dynamic_cast(doc->getObjectByRepr(rect_repr)); + g_assert(rect != nullptr); + + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + using Geom::X; + using Geom::Y; + Geom::Coord const x0 = MIN(p0[X], p1[X]); + Geom::Coord const y0 = MIN(p0[Y], p1[Y]); + Geom::Coord const x1 = MAX(p0[X], p1[X]); + Geom::Coord const y1 = MAX(p0[Y], p1[Y]); + Geom::Coord const w = x1 - x0; + Geom::Coord const h = y1 - y0; + + SPItem *item = dynamic_cast(desktop->currentLayer()); + g_assert(item != nullptr); + rect->setPosition(x0, y0, w, h); + rect->doWriteTransform(item->i2doc_affine().inverse(), nullptr, true); + rect->updateRepr(); + + Inkscape::XML::Node *para_repr = xml_doc->createElement("svg:flowPara"); + root_repr->appendChild(para_repr); + SPObject *para_object = doc->getObjectByRepr(para_repr); + g_assert(dynamic_cast(para_object) != nullptr); + + Inkscape::XML::Node *text = xml_doc->createTextNode(""); + para_repr->appendChild(text); + + Inkscape::GC::release(root_repr); + Inkscape::GC::release(region_repr); + Inkscape::GC::release(para_repr); + Inkscape::GC::release(rect_repr); + + return ft_item; +} + +void SPFlowtext::fix_overflow_flowregion(bool inverse) +{ + SPObject *object = dynamic_cast(this); + for (auto child : object->childList(false)) { + SPFlowregion *flowregion = dynamic_cast(child); + if (flowregion) { + object = dynamic_cast(flowregion); + for (auto childshapes : object->childList(false)) { + Geom::Scale scale = Geom::Scale(1000); //200? maybe find better way to fix overglow issue removing new lines... + if (inverse) { + scale = scale.inverse(); + } + SP_ITEM(childshapes)->doWriteTransform(scale, nullptr, true); + } + break; + } + } +} + +Geom::Affine SPFlowtext::set_transform (Geom::Affine const &xform) +{ + if ((this->_optimizeScaledText && !xform.withoutTranslation().isNonzeroUniformScale()) + || (!this->_optimizeScaledText && !xform.isNonzeroUniformScale())) { + this->_optimizeScaledText = false; + return xform; + } + this->_optimizeScaledText = false; + + SPText *text = reinterpret_cast(this); + + double const ex = xform.descrim(); + if (ex == 0) { + return xform; + } + + SPObject *region = nullptr; + for (auto& o: children) { + if (dynamic_cast(&o)) { + region = &o; + break; + } + } + if (region) { + SPRect *rect = dynamic_cast(region->firstChild()); + if (rect) { + rect->set_i2d_affine(xform * rect->i2dt_affine()); + rect->doWriteTransform(rect->transform, nullptr, true); + } + } + + Geom::Affine ret(xform); + ret[0] /= ex; + ret[1] /= ex; + ret[2] /= ex; + ret[3] /= ex; + + // Adjust font size + text->_adjustFontsizeRecursive (this, ex); + + // Adjust stroke width + this->adjust_stroke_width_recursive (ex); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return Geom::Affine(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-flowtext.h b/src/object/sp-flowtext.h new file mode 100644 index 0000000..9baf640 --- /dev/null +++ b/src/object/sp-flowtext.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_FLOWTEXT_H +#define SEEN_SP_ITEM_FLOWTEXT_H + +/* + */ + +#include <2geom/forward.h> + +#include "libnrtype/Layout-TNG.h" +#include "sp-item.h" +#include "desktop.h" + +#define SP_FLOWTEXT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FLOWTEXT(obj) (dynamic_cast((SPObject*)obj) != NULL) + + +namespace Inkscape { + +class DrawingGroup; + +} // namespace Inkscape + +class SPFlowtext : public SPItem { +public: + SPFlowtext(); + ~SPFlowtext() override; + + /** Completely recalculates the layout. */ + void rebuildLayout(); + + /** Converts the flowroot in into a \ tree, keeping all the formatting and positioning, + but losing the automatic wrapping ability. */ + Inkscape::XML::Node *getAsText(); + + // TODO check if these should return SPRect instead of SPItem + + SPItem *get_frame(SPItem const *after); + + SPItem const *get_frame(SPItem const *after) const; + + bool has_internal_frame() const; + +//semiprivate: (need to be accessed by the C-style functions still) + Inkscape::Text::Layout layout; + + /** discards the drawing objects representing this text. */ + void _clearFlow(Inkscape::DrawingGroup* in_arena); + + double par_indent; + + bool _optimizeScaledText; + + /** Converts the text object to its component curves */ + SPCurve *getNormalizedBpath() const { + return layout.convertToCurves(); + } + + /** Optimize scaled flow text on next set_transform. */ + void optimizeScaledText() + {_optimizeScaledText = true;} + +private: + /** Recursively walks the xml tree adding tags and their contents. */ + void _buildLayoutInput(SPObject *root, Shape const *exclusion_shape, std::list *shapes, SPObject **pending_line_break_object); + + /** calculates the union of all the \ children + of this flowroot. */ + Shape* _buildExclusionShape() const; + +public: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttributeEnum key, const char* value) override; + Geom::Affine set_transform(Geom::Affine const& xform) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + void fix_overflow_flowregion(bool inverse); + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; +}; + +SPItem *create_flowtext_with_internal_frame (SPDesktop *desktop, Geom::Point p1, Geom::Point p2); + +#endif // SEEN_SP_ITEM_FLOWTEXT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-font-face.cpp b/src/object/sp-font-face.cpp new file mode 100644 index 0000000..18215cb --- /dev/null +++ b/src/object/sp-font-face.cpp @@ -0,0 +1,825 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG element implementation + * + * Section 20.8.3 of the W3C SVG 1.1 spec + * available at: + * http://www.w3.org/TR/SVG/fonts.html#FontFaceElement + * + * Author: + * Felipe C. da S. Sanches + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-font-face.h" +#include "document.h" + +#include + +static std::vector sp_read_fontFaceStyleType(gchar const *value){ + std::vector v; + + if (!value){ + v.push_back(SP_FONTFACE_STYLE_ALL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_STYLE_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_STYLE_NORMAL); + value += 6; + } + break; + case 'i': + if (strncmp(value, "italic", 6) == 0){ + v.push_back(SP_FONTFACE_STYLE_ITALIC); + value += 6; + } + break; + case 'o': + if (strncmp(value, "oblique", 7) == 0){ + v.push_back(SP_FONTFACE_STYLE_OBLIQUE); + value += 7; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector sp_read_fontFaceVariantType(gchar const *value){ + std::vector v; + + if (!value){ + v.push_back(SP_FONTFACE_VARIANT_NORMAL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_VARIANT_NORMAL); + value += 6; + } + break; + case 's': + if (strncmp(value, "small-caps", 10) == 0){ + v.push_back(SP_FONTFACE_VARIANT_SMALL_CAPS); + value += 10; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector sp_read_fontFaceWeightType(gchar const *value){ + std::vector v; + + if (!value){ + v.push_back(SP_FONTFACE_WEIGHT_ALL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_WEIGHT_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_NORMAL); + value += 6; + } + break; + case 'b': + if (strncmp(value, "bold", 4) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_BOLD); + value += 4; + } + break; + case '1': + if (strncmp(value, "100", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_100); + value += 3; + } + break; + case '2': + if (strncmp(value, "200", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_200); + value += 3; + } + break; + case '3': + if (strncmp(value, "300", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_300); + value += 3; + } + break; + case '4': + if (strncmp(value, "400", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_400); + value += 3; + } + break; + case '5': + if (strncmp(value, "500", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_500); + value += 3; + } + break; + case '6': + if (strncmp(value, "600", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_600); + value += 3; + } + break; + case '7': + if (strncmp(value, "700", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_700); + value += 3; + } + break; + case '8': + if (strncmp(value, "800", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_800); + value += 3; + } + break; + case '9': + if (strncmp(value, "900", 3) == 0){ + v.push_back(SP_FONTFACE_WEIGHT_900); + value += 3; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +static std::vector sp_read_fontFaceStretchType(gchar const *value){ + std::vector v; + + if (!value){ + v.push_back(SP_FONTFACE_STRETCH_NORMAL); + return v; + } + + if (strncmp(value, "all", 3) == 0){ + value += 3; + while(value[0]==',' || value[0]==' ') + value++; + v.push_back(SP_FONTFACE_STRETCH_ALL); + return v; + } + + while(value[0]!='\0'){ + switch(value[0]){ + case 'n': + if (strncmp(value, "normal", 6) == 0){ + v.push_back(SP_FONTFACE_STRETCH_NORMAL); + value += 6; + } + break; + case 'u': + if (strncmp(value, "ultra-condensed", 15) == 0){ + v.push_back(SP_FONTFACE_STRETCH_ULTRA_CONDENSED); + value += 15; + } + if (strncmp(value, "ultra-expanded", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_ULTRA_EXPANDED); + value += 14; + } + break; + case 'e': + if (strncmp(value, "expanded", 8) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXPANDED); + value += 8; + } + if (strncmp(value, "extra-condensed", 15) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXTRA_CONDENSED); + value += 15; + } + if (strncmp(value, "extra-expanded", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_EXTRA_EXPANDED); + value += 14; + } + break; + case 'c': + if (strncmp(value, "condensed", 9) == 0){ + v.push_back(SP_FONTFACE_STRETCH_CONDENSED); + value += 9; + } + break; + case 's': + if (strncmp(value, "semi-condensed", 14) == 0){ + v.push_back(SP_FONTFACE_STRETCH_SEMI_CONDENSED); + value += 14; + } + if (strncmp(value, "semi-expanded", 13) == 0){ + v.push_back(SP_FONTFACE_STRETCH_SEMI_EXPANDED); + value += 13; + } + break; + } + while(value[0]==',' || value[0]==' ') + value++; + } + return v; +} + +SPFontFace::SPFontFace() : SPObject() { + std::vector style; + style.push_back(SP_FONTFACE_STYLE_ALL); + this->font_style = style; + + std::vector variant; + variant.push_back(SP_FONTFACE_VARIANT_NORMAL); + this->font_variant = variant; + + std::vector weight; + weight.push_back(SP_FONTFACE_WEIGHT_ALL); + this->font_weight = weight; + + std::vector stretch; + stretch.push_back(SP_FONTFACE_STRETCH_NORMAL); + this->font_stretch = stretch; + this->font_family = nullptr; + + //this->font_style = ; + //this->font_variant = ; + //this->font_weight = ; + //this->font_stretch = ; + this->font_size = nullptr; + //this->unicode_range = ; + this->units_per_em = 1000; + //this->panose_1 = ; + this->stemv = 0; + this->stemh = 0; + this->slope = 0; + this->cap_height = 0; + this->x_height = 0; + this->accent_height = 0; + this->ascent = 0; + this->descent = 0; + this->widths = nullptr; + this->bbox = nullptr; + this->ideographic = 0; + this->alphabetic = 0; + this->mathematical = 0; + this->hanging = 0; + this->v_ideographic = 0; + this->v_alphabetic = 0; + this->v_mathematical = 0; + this->v_hanging = 0; + this->underline_position = 0; + this->underline_thickness = 0; + this->strikethrough_position = 0; + this->strikethrough_thickness = 0; + this->overline_position = 0; + this->overline_thickness = 0; +} + +SPFontFace::~SPFontFace() = default; + +void SPFontFace::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr( "font-family" ); + this->readAttr( "font-style" ); + this->readAttr( "font-variant" ); + this->readAttr( "font-weight" ); + this->readAttr( "font-stretch" ); + this->readAttr( "font-size" ); + this->readAttr( "unicode-range" ); + this->readAttr( "units-per-em" ); + this->readAttr( "panose-1" ); + this->readAttr( "stem-v" ); + this->readAttr( "stem-h" ); + this->readAttr( "slope" ); + this->readAttr( "cap-height" ); + this->readAttr( "x-height" ); + this->readAttr( "accent-height" ); + this->readAttr( "ascent" ); + this->readAttr( "descent" ); + this->readAttr( "widths" ); + this->readAttr( "bbox" ); + this->readAttr( "ideographic" ); + this->readAttr( "alphabetic" ); + this->readAttr( "mathematical" ); + this->readAttr( "ranging" ); + this->readAttr( "v-ideogaphic" ); + this->readAttr( "v-alphabetic" ); + this->readAttr( "v-mathematical" ); + this->readAttr( "v-hanging" ); + this->readAttr( "underline-position" ); + this->readAttr( "underline-thickness" ); + this->readAttr( "strikethrough-position" ); + this->readAttr( "strikethrough-thickness" ); + this->readAttr( "overline-position" ); + this->readAttr( "overline-thickness" ); +} + +/** + * Callback for child_added event. + */ +void SPFontFace::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/** + * Callback for remove_child event. + */ +void SPFontFace::remove_child(Inkscape::XML::Node *child) { + SPObject::remove_child(child); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFontFace::release() { + SPObject::release(); +} + +void SPFontFace::set(SPAttributeEnum key, const gchar *value) { + std::vector style; + std::vector variant; + std::vector weight; + std::vector stretch; + + switch (key) { + case SP_PROP_FONT_FAMILY: + if (this->font_family) { + g_free(this->font_family); + } + + this->font_family = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_PROP_FONT_STYLE: + style = sp_read_fontFaceStyleType(value); + + if (this->font_style.size() != style.size()){ + this->font_style = style; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;ifont_style[i]){ + this->font_style = style; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SP_PROP_FONT_VARIANT: + variant = sp_read_fontFaceVariantType(value); + + if (this->font_variant.size() != variant.size()){ + this->font_variant = variant; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;ifont_variant[i]){ + this->font_variant = variant; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SP_PROP_FONT_WEIGHT: + weight = sp_read_fontFaceWeightType(value); + + if (this->font_weight.size() != weight.size()){ + this->font_weight = weight; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;ifont_weight[i]){ + this->font_weight = weight; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SP_PROP_FONT_STRETCH: + stretch = sp_read_fontFaceStretchType(value); + + if (this->font_stretch.size() != stretch.size()){ + this->font_stretch = stretch; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + for (unsigned int i=0;ifont_stretch[i]){ + this->font_stretch = stretch; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + } + } + break; + case SP_ATTR_UNITS_PER_EM: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->units_per_em){ + this->units_per_em = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_STEMV: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->stemv){ + this->stemv = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_STEMH: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->stemh){ + this->stemh = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_SLOPE: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->slope){ + this->slope = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_CAP_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->cap_height){ + this->cap_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_X_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->x_height){ + this->x_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_ACCENT_HEIGHT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->accent_height){ + this->accent_height = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_ASCENT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->ascent){ + this->ascent = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_DESCENT: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->descent){ + this->descent = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_IDEOGRAPHIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->ideographic){ + this->ideographic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_ALPHABETIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->alphabetic){ + this->alphabetic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_MATHEMATICAL: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->mathematical){ + this->mathematical = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_HANGING: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->hanging){ + this->hanging = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_V_IDEOGRAPHIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_ideographic){ + this->v_ideographic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_V_ALPHABETIC: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_alphabetic){ + this->v_alphabetic = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_V_MATHEMATICAL: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_mathematical){ + this->v_mathematical = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_V_HANGING: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->v_hanging){ + this->v_hanging = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_UNDERLINE_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->underline_position){ + this->underline_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_UNDERLINE_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->underline_thickness){ + this->underline_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_STRIKETHROUGH_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->strikethrough_position){ + this->strikethrough_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_STRIKETHROUGH_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->strikethrough_thickness){ + this->strikethrough_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_OVERLINE_POSITION: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->overline_position){ + this->overline_position = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_OVERLINE_THICKNESS: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->overline_thickness){ + this->overline_thickness = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPObject::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFontFace::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr( "font-family" ); + this->readAttr( "font-style" ); + this->readAttr( "font-variant" ); + this->readAttr( "font-weight" ); + this->readAttr( "font-stretch" ); + this->readAttr( "font-size" ); + this->readAttr( "unicode-range" ); + this->readAttr( "units-per-em" ); + this->readAttr( "panose-1" ); + this->readAttr( "stemv" ); + this->readAttr( "stemh" ); + this->readAttr( "slope" ); + this->readAttr( "cap-height" ); + this->readAttr( "x-height" ); + this->readAttr( "accent-height" ); + this->readAttr( "ascent" ); + this->readAttr( "descent" ); + this->readAttr( "widths" ); + this->readAttr( "bbox" ); + this->readAttr( "ideographic" ); + this->readAttr( "alphabetic" ); + this->readAttr( "mathematical" ); + this->readAttr( "hanging" ); + this->readAttr( "v-ideographic" ); + this->readAttr( "v-alphabetic" ); + this->readAttr( "v-mathematical" ); + this->readAttr( "v-hanging" ); + this->readAttr( "underline-position" ); + this->readAttr( "underline-thickness" ); + this->readAttr( "strikethrough-position" ); + this->readAttr( "strikethrough-thickness" ); + this->readAttr( "overline-position" ); + this->readAttr( "overline-thickness" ); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPFontFace::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:font-face"); + } + + //TODO: + //sp_repr_set_svg_double(repr, "font-family", face->font_family); + //sp_repr_set_svg_double(repr, "font-style", face->font_style); + //sp_repr_set_svg_double(repr, "font-variant", face->font_variant); + //sp_repr_set_svg_double(repr, "font-weight", face->font_weight); + //sp_repr_set_svg_double(repr, "font-stretch", face->font_stretch); + //sp_repr_set_svg_double(repr, "font-size", face->font_size); + //sp_repr_set_svg_double(repr, "unicode-range", face->unicode_range); + sp_repr_set_svg_double(repr, "units-per-em", this->units_per_em); + //sp_repr_set_svg_double(repr, "panose-1", face->panose_1); + sp_repr_set_svg_double(repr, "stemv", this->stemv); + sp_repr_set_svg_double(repr, "stemh", this->stemh); + sp_repr_set_svg_double(repr, "slope", this->slope); + sp_repr_set_svg_double(repr, "cap-height", this->cap_height); + sp_repr_set_svg_double(repr, "x-height", this->x_height); + sp_repr_set_svg_double(repr, "accent-height", this->accent_height); + sp_repr_set_svg_double(repr, "ascent", this->ascent); + sp_repr_set_svg_double(repr, "descent", this->descent); + //sp_repr_set_svg_double(repr, "widths", face->widths); + //sp_repr_set_svg_double(repr, "bbox", face->bbox); + sp_repr_set_svg_double(repr, "ideographic", this->ideographic); + sp_repr_set_svg_double(repr, "alphabetic", this->alphabetic); + sp_repr_set_svg_double(repr, "mathematical", this->mathematical); + sp_repr_set_svg_double(repr, "hanging", this->hanging); + sp_repr_set_svg_double(repr, "v-ideographic", this->v_ideographic); + sp_repr_set_svg_double(repr, "v-alphabetic", this->v_alphabetic); + sp_repr_set_svg_double(repr, "v-mathematical", this->v_mathematical); + sp_repr_set_svg_double(repr, "v-hanging", this->v_hanging); + sp_repr_set_svg_double(repr, "underline-position", this->underline_position); + sp_repr_set_svg_double(repr, "underline-thickness", this->underline_thickness); + sp_repr_set_svg_double(repr, "strikethrough-position", this->strikethrough_position); + sp_repr_set_svg_double(repr, "strikethrough-thickness", this->strikethrough_thickness); + sp_repr_set_svg_double(repr, "overline-position", this->overline_position); + sp_repr_set_svg_double(repr, "overline-thickness", this->overline_thickness); + + if (repr != this->getRepr()) { + // In all COPY_ATTR given below the XML tree is + // being used directly while it shouldn't be. + COPY_ATTR(repr, this->getRepr(), "font-family"); + COPY_ATTR(repr, this->getRepr(), "font-style"); + COPY_ATTR(repr, this->getRepr(), "font-variant"); + COPY_ATTR(repr, this->getRepr(), "font-weight"); + COPY_ATTR(repr, this->getRepr(), "font-stretch"); + COPY_ATTR(repr, this->getRepr(), "font-size"); + COPY_ATTR(repr, this->getRepr(), "unicode-range"); + COPY_ATTR(repr, this->getRepr(), "units-per-em"); + COPY_ATTR(repr, this->getRepr(), "panose-1"); + COPY_ATTR(repr, this->getRepr(), "stemv"); + COPY_ATTR(repr, this->getRepr(), "stemh"); + COPY_ATTR(repr, this->getRepr(), "slope"); + COPY_ATTR(repr, this->getRepr(), "cap-height"); + COPY_ATTR(repr, this->getRepr(), "x-height"); + COPY_ATTR(repr, this->getRepr(), "accent-height"); + COPY_ATTR(repr, this->getRepr(), "ascent"); + COPY_ATTR(repr, this->getRepr(), "descent"); + COPY_ATTR(repr, this->getRepr(), "widths"); + COPY_ATTR(repr, this->getRepr(), "bbox"); + COPY_ATTR(repr, this->getRepr(), "ideographic"); + COPY_ATTR(repr, this->getRepr(), "alphabetic"); + COPY_ATTR(repr, this->getRepr(), "mathematical"); + COPY_ATTR(repr, this->getRepr(), "hanging"); + COPY_ATTR(repr, this->getRepr(), "v-ideographic"); + COPY_ATTR(repr, this->getRepr(), "v-alphabetic"); + COPY_ATTR(repr, this->getRepr(), "v-mathematical"); + COPY_ATTR(repr, this->getRepr(), "v-hanging"); + COPY_ATTR(repr, this->getRepr(), "underline-position"); + COPY_ATTR(repr, this->getRepr(), "underline-thickness"); + COPY_ATTR(repr, this->getRepr(), "strikethrough-position"); + COPY_ATTR(repr, this->getRepr(), "strikethrough-thickness"); + COPY_ATTR(repr, this->getRepr(), "overline-position"); + COPY_ATTR(repr, this->getRepr(), "overline-thickness"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-font-face.h b/src/object/sp-font-face.h new file mode 100644 index 0000000..e946176 --- /dev/null +++ b/src/object/sp-font-face.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_FONTFACE_H +#define SEEN_SP_FONTFACE_H + +#include + +/* + * SVG element implementation + * + * Section 20.8.3 of the W3C SVG 1.1 spec + * available at: + * http://www.w3.org/TR/SVG/fonts.html#FontFaceElement + * + * Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_FONTFACE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FONTFACE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum FontFaceStyleType{ + SP_FONTFACE_STYLE_ALL, + SP_FONTFACE_STYLE_NORMAL, + SP_FONTFACE_STYLE_ITALIC, + SP_FONTFACE_STYLE_OBLIQUE +}; + +enum FontFaceVariantType{ + SP_FONTFACE_VARIANT_NORMAL, + SP_FONTFACE_VARIANT_SMALL_CAPS +}; + +enum FontFaceWeightType{ + SP_FONTFACE_WEIGHT_ALL, + SP_FONTFACE_WEIGHT_NORMAL, + SP_FONTFACE_WEIGHT_BOLD, + SP_FONTFACE_WEIGHT_100, + SP_FONTFACE_WEIGHT_200, + SP_FONTFACE_WEIGHT_300, + SP_FONTFACE_WEIGHT_400, + SP_FONTFACE_WEIGHT_500, + SP_FONTFACE_WEIGHT_600, + SP_FONTFACE_WEIGHT_700, + SP_FONTFACE_WEIGHT_800, + SP_FONTFACE_WEIGHT_900 +}; + +enum FontFaceStretchType{ + SP_FONTFACE_STRETCH_ALL, + SP_FONTFACE_STRETCH_NORMAL, + SP_FONTFACE_STRETCH_ULTRA_CONDENSED, + SP_FONTFACE_STRETCH_EXTRA_CONDENSED, + SP_FONTFACE_STRETCH_CONDENSED, + SP_FONTFACE_STRETCH_SEMI_CONDENSED, + SP_FONTFACE_STRETCH_SEMI_EXPANDED, + SP_FONTFACE_STRETCH_EXPANDED, + SP_FONTFACE_STRETCH_EXTRA_EXPANDED, + SP_FONTFACE_STRETCH_ULTRA_EXPANDED +}; + +enum FontFaceUnicodeRangeType{ + FONTFACE_UNICODERANGE_FIXME_HERE, +}; + +class SPFontFace : public SPObject { +public: + SPFontFace(); + ~SPFontFace() override; + + char* font_family; + std::vector font_style; + std::vector font_variant; + std::vector font_weight; + std::vector font_stretch; + char* font_size; + std::vector unicode_range; + double units_per_em; + std::vector panose_1; + double stemv; + double stemh; + double slope; + double cap_height; + double x_height; + double accent_height; + double ascent; + double descent; + char* widths; + char* bbox; + double ideographic; + double alphabetic; + double mathematical; + double hanging; + double v_ideographic; + double v_alphabetic; + double v_mathematical; + double v_hanging; + double underline_position; + double underline_thickness; + double strikethrough_position; + double strikethrough_thickness; + double overline_position; + double overline_thickness; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttributeEnum key, const char* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif //#ifndef __SP_FONTFACE_H__ diff --git a/src/object/sp-font.cpp b/src/object/sp-font.cpp new file mode 100644 index 0000000..9164bd3 --- /dev/null +++ b/src/object/sp-font.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG element implementation + * + * Author: + * Felipe C. da S. Sanches + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-font.h" +#include "document.h" + +#include "display/nr-svgfonts.h" + + +//I think we should have extra stuff here and in the set method in order to set default value as specified at http://www.w3.org/TR/SVG/fonts.html + +// TODO determine better values and/or make these dynamic: +double FNT_DEFAULT_ADV = 1024; // TODO determine proper default +double FNT_DEFAULT_ASCENT = 768; // TODO determine proper default +double FNT_UNITS_PER_EM = 1024; // TODO determine proper default + +SPFont::SPFont() : SPObject() { + this->horiz_origin_x = 0; + this->horiz_origin_y = 0; + this->horiz_adv_x = FNT_DEFAULT_ADV; + this->vert_origin_x = FNT_DEFAULT_ADV / 2.0; + this->vert_origin_y = FNT_DEFAULT_ASCENT; + this->vert_adv_y = FNT_UNITS_PER_EM; +} + +SPFont::~SPFont() = default; + +void SPFont::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObject::build(document, repr); + + this->readAttr( "horiz-origin-x" ); + this->readAttr( "horiz-origin-y" ); + this->readAttr( "horiz-adv-x" ); + this->readAttr( "vert-origin-x" ); + this->readAttr( "vert-origin-y" ); + this->readAttr( "vert-adv-y" ); + + document->addResource("font", this); +} + +/** + * Callback for child_added event. + */ +void SPFont::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +/** + * Callback for remove_child event. + */ +void SPFont::remove_child(Inkscape::XML::Node* child) { + SPObject::remove_child(child); + + this->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPFont::release() { + this->document->removeResource("font", this); + + SPObject::release(); +} + +void SPFont::set(SPAttributeEnum key, const gchar *value) { + // TODO these are floating point, so some epsilon comparison would be good + switch (key) { + case SP_ATTR_HORIZ_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_origin_x){ + this->horiz_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_HORIZ_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_origin_y){ + this->horiz_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ADV; + + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ADV / 2.0; + + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_DEFAULT_ASCENT; + + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : FNT_UNITS_PER_EM; + + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + SPObject::set(key, value); + break; + } +} + +/** + * Receives update notifications. + */ +void SPFont::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG)) { + this->readAttr( "horiz-origin-x" ); + this->readAttr( "horiz-origin-y" ); + this->readAttr( "horiz-adv-x" ); + this->readAttr( "vert-origin-x" ); + this->readAttr( "vert-origin-y" ); + this->readAttr( "vert-adv-y" ); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPFont::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:font"); + } + + sp_repr_set_svg_double(repr, "horiz-origin-x", this->horiz_origin_x); + sp_repr_set_svg_double(repr, "horiz-origin-y", this->horiz_origin_y); + sp_repr_set_svg_double(repr, "horiz-adv-x", this->horiz_adv_x); + sp_repr_set_svg_double(repr, "vert-origin-x", this->vert_origin_x); + sp_repr_set_svg_double(repr, "vert-origin-y", this->vert_origin_y); + sp_repr_set_svg_double(repr, "vert-adv-y", this->vert_adv_y); + + if (repr != this->getRepr()) { + // All the below COPY_ATTR functions are directly using + // the XML Tree while they shouldn't + COPY_ATTR(repr, this->getRepr(), "horiz-origin-x"); + COPY_ATTR(repr, this->getRepr(), "horiz-origin-y"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-font.h b/src/object/sp-font.h new file mode 100644 index 0000000..55176f1 --- /dev/null +++ b/src/object/sp-font.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_FONT_H_SEEN +#define SP_FONT_H_SEEN + +/* + * SVG element implementation + * + * Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_FONT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_FONT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPFont : public SPObject { +public: + SPFont(); + ~SPFont() override; + + double horiz_origin_x; + double horiz_origin_y; + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif //#ifndef SP_FONT_H_SEEN diff --git a/src/object/sp-glyph-kerning.cpp b/src/object/sp-glyph-kerning.cpp new file mode 100644 index 0000000..a812396 --- /dev/null +++ b/src/object/sp-glyph-kerning.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * SVG and elements implementation + * W3C SVG 1.1 spec, page 476, section 20.7 + * + * Authors: + * Felipe C. da S. Sanches + * Abhishek Sharma + * + * Copyright (C) 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-glyph-kerning.h" + +#include "document.h" +#include + + +SPGlyphKerning::SPGlyphKerning() + : SPObject() +//TODO: correct these values: + , u1(nullptr) + , g1(nullptr) + , u2(nullptr) + , g2(nullptr) + , k(0) +{ +} + +void SPGlyphKerning::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr( "u1" ); + this->readAttr( "g1" ); + this->readAttr( "u2" ); + this->readAttr( "g2" ); + this->readAttr( "k" ); +} + +void SPGlyphKerning::release() +{ + SPObject::release(); +} + +GlyphNames::GlyphNames(const gchar* value) +{ + if (value) { + names = g_strdup(value); + } +} + +GlyphNames::~GlyphNames() +{ + if (names) { + g_free(names); + } +} + +bool GlyphNames::contains(const char* name) +{ + if (!(this->names) || !name) { + return false; + } + + std::istringstream is(this->names); + std::string str; + std::string s(name); + + while (is >> str) { + if (str == s) { + return true; + } + } + + return false; +} + +void SPGlyphKerning::set(SPAttributeEnum key, const gchar *value) +{ + switch (key) { + case SP_ATTR_U1: + { + if (this->u1) { + delete this->u1; + } + + this->u1 = new UnicodeRange(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_U2: + { + if (this->u2) { + delete this->u2; + } + + this->u2 = new UnicodeRange(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_G1: + { + if (this->g1) { + delete this->g1; + } + + this->g1 = new GlyphNames(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_G2: + { + if (this->g2) { + delete this->g2; + } + + this->g2 = new GlyphNames(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_K: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->k){ + this->k = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +/** + * Receives update notifications. + */ +void SPGlyphKerning::update(SPCtx *ctx, guint flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr( "u1" ); + this->readAttr( "u2" ); + this->readAttr( "g2" ); + this->readAttr( "k" ); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPGlyphKerning::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyphkerning"); // fix this! + } + + if (repr != this->getRepr()) { + // All the COPY_ATTR functions below use + // XML Tree directly, while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "u1"); + COPY_ATTR(repr, this->getRepr(), "g1"); + COPY_ATTR(repr, this->getRepr(), "u2"); + COPY_ATTR(repr, this->getRepr(), "g2"); + COPY_ATTR(repr, this->getRepr(), "k"); + } + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-glyph-kerning.h b/src/object/sp-glyph-kerning.h new file mode 100644 index 0000000..4e5df20 --- /dev/null +++ b/src/object/sp-glyph-kerning.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG and elements implementation + * + * Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_GLYPH_KERNING_H +#define SEEN_SP_GLYPH_KERNING_H + +#include "sp-object.h" +#include "unicoderange.h" + +#define SP_HKERN(obj) (dynamic_cast(obj)) +#define SP_IS_HKERN(obj) (dynamic_cast(obj) != NULL) + +#define SP_VKERN(obj) (dynamic_cast(obj)) +#define SP_IS_VKERN(obj) (dynamic_cast(obj) != NULL) + +// CPPIFY: These casting macros are buggy, as Vkern and Hkern aren't "real" classes. + +class GlyphNames { +public: + GlyphNames(char const* value); + ~GlyphNames(); + bool contains(char const* name); +private: + char* names; +}; + +class SPGlyphKerning : public SPObject { +public: + SPGlyphKerning(); + ~SPGlyphKerning() override = default; + + // FIXME encapsulation + UnicodeRange* u1; + GlyphNames* g1; + UnicodeRange* u2; + GlyphNames* g2; + double k; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +class SPHkern : public SPGlyphKerning { + ~SPHkern() override = default; +}; + +class SPVkern : public SPGlyphKerning { + ~SPVkern() override = default; +}; + +#endif // !SEEN_SP_GLYPH_KERNING_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/sp-glyph.cpp b/src/object/sp-glyph.cpp new file mode 100644 index 0000000..472a2bd --- /dev/null +++ b/src/object/sp-glyph.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifdef HAVE_CONFIG_H +#endif + +/* + * SVG element implementation + * + * Author: + * Felipe C. da S. Sanches + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-glyph.h" +#include "document.h" + +SPGlyph::SPGlyph() + : SPObject() +//TODO: correct these values: + , d(nullptr) + , orientation(GLYPH_ORIENTATION_BOTH) + , arabic_form(GLYPH_ARABIC_FORM_INITIAL) + , lang(nullptr) + , horiz_adv_x(0) + , vert_origin_x(0) + , vert_origin_y(0) + , vert_adv_y(0) +{ +} + +void SPGlyph::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr( "unicode" ); + this->readAttr( "glyph-name" ); + this->readAttr( "d" ); + this->readAttr( "orientation" ); + this->readAttr( "arabic-form" ); + this->readAttr( "lang" ); + this->readAttr( "horiz-adv-x" ); + this->readAttr( "vert-origin-x" ); + this->readAttr( "vert-origin-y" ); + this->readAttr( "vert-adv-y" ); +} + +void SPGlyph::release() { + SPObject::release(); +} + +static glyphArabicForm sp_glyph_read_arabic_form(gchar const *value){ + if (!value) { + return GLYPH_ARABIC_FORM_INITIAL; //TODO: verify which is the default default (for me, the spec is not clear) + } + + switch(value[0]){ + case 'i': + if (strncmp(value, "initial", 7) == 0) { + return GLYPH_ARABIC_FORM_INITIAL; + } + + if (strncmp(value, "isolated", 8) == 0) { + return GLYPH_ARABIC_FORM_ISOLATED; + } + break; + case 'm': + if (strncmp(value, "medial", 6) == 0) { + return GLYPH_ARABIC_FORM_MEDIAL; + } + break; + case 't': + if (strncmp(value, "terminal", 8) == 0) { + return GLYPH_ARABIC_FORM_TERMINAL; + } + break; + } + + return GLYPH_ARABIC_FORM_INITIAL; //TODO: VERIFY DEFAULT! +} + +static glyphOrientation sp_glyph_read_orientation(gchar const *value) +{ + if (!value) { + return GLYPH_ORIENTATION_BOTH; + } + + switch(value[0]){ + case 'h': + return GLYPH_ORIENTATION_HORIZONTAL; + break; + case 'v': + return GLYPH_ORIENTATION_VERTICAL; + break; + } + +//ERROR? TODO: VERIFY PROPER ERROR HANDLING + return GLYPH_ORIENTATION_BOTH; +} + +void SPGlyph::set(SPAttributeEnum key, const gchar *value) +{ + switch (key) { + case SP_ATTR_UNICODE: + { + this->unicode.clear(); + + if (value) { + this->unicode.append(value); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_GLYPH_NAME: + { + this->glyph_name.clear(); + + if (value) { + this->glyph_name.append(value); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_D: + { + if (this->d) { + g_free(this->d); + } + + this->d = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_ORIENTATION: + { + glyphOrientation orient = sp_glyph_read_orientation(value); + + if (this->orientation != orient){ + this->orientation = orient; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_ARABIC_FORM: + { + glyphArabicForm form = sp_glyph_read_arabic_form(value); + + if (this->arabic_form != form){ + this->arabic_form = form; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_LANG: + { + if (this->lang) { + g_free(this->lang); + } + + this->lang = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +/** + * Receives update notifications. + */ +void SPGlyph::update(SPCtx *ctx, guint flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + /* do something to trigger redisplay, updates? */ + this->readAttr( "unicode" ); + this->readAttr( "glyph-name" ); + this->readAttr( "d" ); + this->readAttr( "orientation" ); + this->readAttr( "arabic-form" ); + this->readAttr( "lang" ); + this->readAttr( "horiz-adv-x" ); + this->readAttr( "vert-origin-x" ); + this->readAttr( "vert-origin-y" ); + this->readAttr( "vert-adv-y" ); + } + + SPObject::update(ctx, flags); +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPGlyph::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyph"); + } + + /* I am commenting out this part because I am not certain how does it work. I will have to study it later. Juca + repr->setAttribute("unicode", glyph->unicode); + repr->setAttribute("glyph-name", glyph->glyph_name); + repr->setAttribute("d", glyph->d); + sp_repr_set_svg_double(repr, "orientation", (double) glyph->orientation); + sp_repr_set_svg_double(repr, "arabic-form", (double) glyph->arabic_form); + repr->setAttribute("lang", glyph->lang); + sp_repr_set_svg_double(repr, "horiz-adv-x", glyph->horiz_adv_x); + sp_repr_set_svg_double(repr, "vert-origin-x", glyph->vert_origin_x); + sp_repr_set_svg_double(repr, "vert-origin-y", glyph->vert_origin_y); + sp_repr_set_svg_double(repr, "vert-adv-y", glyph->vert_adv_y); + */ + + if (repr != this->getRepr()) { + // All the COPY_ATTR functions below use + // XML Tree directly while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "unicode"); + COPY_ATTR(repr, this->getRepr(), "glyph-name"); + COPY_ATTR(repr, this->getRepr(), "d"); + COPY_ATTR(repr, this->getRepr(), "orientation"); + COPY_ATTR(repr, this->getRepr(), "arabic-form"); + COPY_ATTR(repr, this->getRepr(), "lang"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-glyph.h b/src/object/sp-glyph.h new file mode 100644 index 0000000..6a6ce66 --- /dev/null +++ b/src/object/sp-glyph.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_GLYPH_H +#define SEEN_SP_GLYPH_H + +#include "sp-object.h" + +#define SP_GLYPH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GLYPH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum glyphArabicForm { + GLYPH_ARABIC_FORM_INITIAL, + GLYPH_ARABIC_FORM_MEDIAL, + GLYPH_ARABIC_FORM_TERMINAL, + GLYPH_ARABIC_FORM_ISOLATED, +}; + +enum glyphOrientation { + GLYPH_ORIENTATION_HORIZONTAL, + GLYPH_ORIENTATION_VERTICAL, + GLYPH_ORIENTATION_BOTH +}; + +/* + * SVG element + */ + +class SPGlyph : public SPObject { +public: + SPGlyph(); + ~SPGlyph() override = default; + + // FIXME encapsulation + Glib::ustring unicode; + Glib::ustring glyph_name; + char* d; + glyphOrientation orientation; + glyphArabicForm arabic_form; + char* lang; + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +}; + +#endif // !SEEN_SP_GLYPH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/sp-gradient-reference.cpp b/src/object/sp-gradient-reference.cpp new file mode 100644 index 0000000..95bd594 --- /dev/null +++ b/src/object/sp-gradient-reference.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-gradient-reference.h" +#include "sp-gradient.h" + +bool +SPGradientReference::_acceptObject(SPObject *obj) const +{ + return SP_IS_GRADIENT(obj) && URIReference::_acceptObject(obj); + /* effic: Don't bother making this an inline function: _acceptObject is a virtual function, + typically called from a context where the runtime type is not known at compile time. */ +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient-reference.h b/src/object/sp-gradient-reference.h new file mode 100644 index 0000000..925d559 --- /dev/null +++ b/src/object/sp-gradient-reference.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_REFERENCE_H +#define SEEN_SP_GRADIENT_REFERENCE_H + +#include "uri-references.h" + +class SPGradient; +class SPObject; + +class SPGradientReference : public Inkscape::URIReference { +public: + SPGradientReference(SPObject *obj) : URIReference(obj) {} + + SPGradient *getObject() const { + return reinterpret_cast(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + + +#endif /* !SEEN_SP_GRADIENT_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient-spread.h b/src/object/sp-gradient-spread.h new file mode 100644 index 0000000..47ceee5 --- /dev/null +++ b/src/object/sp-gradient-spread.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_SPREAD_H +#define SEEN_SP_GRADIENT_SPREAD_H + +enum SPGradientSpread { + SP_GRADIENT_SPREAD_PAD, + SP_GRADIENT_SPREAD_REFLECT, + SP_GRADIENT_SPREAD_REPEAT, + SP_GRADIENT_SPREAD_UNDEFINED = INT_MAX +}; + + +#endif /* !SEEN_SP_GRADIENT_SPREAD_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient-units.h b/src/object/sp-gradient-units.h new file mode 100644 index 0000000..1a4335d --- /dev/null +++ b/src/object/sp-gradient-units.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_UNITS_H +#define SEEN_SP_GRADIENT_UNITS_H + +enum SPGradientUnits { + SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX, + SP_GRADIENT_UNITS_USERSPACEONUSE +}; + + +#endif /* !SEEN_SP_GRADIENT_UNITS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient-vector.h b/src/object/sp-gradient-vector.h new file mode 100644 index 0000000..82721aa --- /dev/null +++ b/src/object/sp-gradient-vector.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GRADIENT_VECTOR_H +#define SEEN_SP_GRADIENT_VECTOR_H + +#include +#include "color.h" + +/** + * Differs from SPStop in that SPStop mirrors the \ element in the document, whereas + * SPGradientStop shows more the effective stop color. + * + * For example, SPGradientStop has no currentColor option: currentColor refers to the color + * property value of the gradient where currentColor appears, so we interpret currentColor before + * copying from SPStop to SPGradientStop. + */ +struct SPGradientStop { + double offset; + SPColor color; + float opacity; +}; + +/** + * The effective gradient vector, after copying stops from the referenced gradient if necessary. + */ +struct SPGradientVector { + bool built; + std::vector stops; +}; + + +#endif /* !SEEN_SP_GRADIENT_VECTOR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient.cpp b/src/object/sp-gradient.cpp new file mode 100644 index 0000000..c6f8682 --- /dev/null +++ b/src/object/sp-gradient.cpp @@ -0,0 +1,1197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SPGradient, SPStop, SPLinearGradient, SPRadialGradient, + * SPMeshGradient, SPMeshRow, SPMeshPatch + */ +/* + * Authors: + * Lauris Kaplinski + * bulia byak + * Jasper van de Gronde + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2004 David Turner + * Copyright (C) 2009 Jasper van de Gronde + * Copyright (C) 2011 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#define noSP_GRADIENT_VERBOSE +//#define OBJECT_TRACE + +#include "sp-gradient.h" + +#include +#include + +#include <2geom/transforms.h> + +#include + +#include +#include + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" +#include "gradient-chemistry.h" + +#include "sp-gradient-reference.h" +#include "sp-linear-gradient.h" +#include "sp-radial-gradient.h" +#include "sp-mesh-gradient.h" +#include "sp-mesh-row.h" +#include "sp-mesh-patch.h" +#include "sp-stop.h" + +#include "display/cairo-utils.h" + +#include "svg/svg.h" +#include "svg/css-ostringstream.h" + +bool SPGradient::hasStops() const +{ + return has_stops; +} + +bool SPGradient::hasPatches() const +{ + return has_patches; +} + +bool SPGradient::isUnitsSet() const +{ + return units_set; +} + +SPGradientUnits SPGradient::getUnits() const +{ + return units; +} + +bool SPGradient::isSpreadSet() const +{ + return spread_set; +} + +SPGradientSpread SPGradient::getSpread() const +{ + return spread; +} + +void SPGradient::setSwatch( bool swatch ) +{ + if ( swatch != isSwatch() ) { + this->swatch = swatch; // to make isSolid() work, this happens first + gchar const* paintVal = swatch ? (isSolid() ? "solid" : "gradient") : nullptr; + setAttribute( "osb:paint", paintVal, nullptr ); + + requestModified( SP_OBJECT_MODIFIED_FLAG ); + } +} + + +/** + * return true if this gradient is "equivalent" to that gradient. + * Equivalent meaning they have the same stop count, same stop colors and same stop opacity + * @param that - A gradient to compare this to + */ +bool SPGradient::isEquivalent(SPGradient *that) +{ + //TODO Make this work for mesh gradients + + bool status = false; + + while(true){ // not really a loop, used to avoid deep nesting or multiple exit points from function + if (this->getStopCount() != that->getStopCount()) { break; } + if (this->hasStops() != that->hasStops()) { break; } + if (!this->getVector() || !that->getVector()) { break; } + if (this->isSwatch() != that->isSwatch()) { break; } + if ( this->isSwatch() ){ + // drop down to check stops. + } + else if ( + (SP_IS_LINEARGRADIENT(this) && SP_IS_LINEARGRADIENT(that)) || + (SP_IS_RADIALGRADIENT(this) && SP_IS_RADIALGRADIENT(that)) || + (SP_IS_MESHGRADIENT(this) && SP_IS_MESHGRADIENT(that))) { + if(!this->isAligned(that))break; + } + else { break; } // this should never happen, some unhandled type of gradient + + SPStop *as = this->getVector()->getFirstStop(); + SPStop *bs = that->getVector()->getFirstStop(); + + bool effective = true; + while (effective && (as && bs)) { + if (!as->getColor().isClose(bs->getColor(), 0.001) || + as->offset != bs->offset || as->getOpacity() != bs->getOpacity() ) { + effective = false; + break; + } + else { + as = as->getNextStop(); + bs = bs->getNextStop(); + } + } + if (!effective) break; + + status = true; + break; + } + return status; +} + +/** + * return true if this gradient is "aligned" to that gradient. + * Aligned means that they have exactly the same coordinates and transform. + * @param that - A gradient to compare this to + */ +bool SPGradient::isAligned(SPGradient *that) +{ + bool status = false; + + /* Some gradients have coordinates/other values specified, some don't. + yes/yes check the coordinates/other values + no/no aligned (because both have all default values) + yes/no not aligned + no/yes not aligned + It is NOT safe to just compare the computed values because if that field has + not been set the computed value could be full of garbage. + + In theory the yes/no and no/yes cases could be aligned if the specified value + matches the default value. + */ + + while(true){ // not really a loop, used to avoid deep nesting or multiple exit points from function + if(this->gradientTransform_set != that->gradientTransform_set) { break; } + if(this->gradientTransform_set && + (this->gradientTransform != that->gradientTransform)) { break; } + if (SP_IS_LINEARGRADIENT(this) && SP_IS_LINEARGRADIENT(that)) { + SPLinearGradient *sg=SP_LINEARGRADIENT(this); + SPLinearGradient *tg=SP_LINEARGRADIENT(that); + + if( sg->x1._set != tg->x1._set) { break; } + if( sg->y1._set != tg->y1._set) { break; } + if( sg->x2._set != tg->x2._set) { break; } + if( sg->y2._set != tg->y2._set) { break; } + if( sg->x1._set && sg->y1._set && sg->x2._set && sg->y2._set) { + if( (sg->x1.computed != tg->x1.computed) || + (sg->y1.computed != tg->y1.computed) || + (sg->x2.computed != tg->x2.computed) || + (sg->y2.computed != tg->y2.computed) ) { break; } + } else if( sg->x1._set || sg->y1._set || sg->x2._set || sg->y2._set) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else if (SP_IS_RADIALGRADIENT(this) && SP_IS_LINEARGRADIENT(that)) { + SPRadialGradient *sg=SP_RADIALGRADIENT(this); + SPRadialGradient *tg=SP_RADIALGRADIENT(that); + + if( sg->cx._set != tg->cx._set) { break; } + if( sg->cy._set != tg->cy._set) { break; } + if( sg->r._set != tg->r._set) { break; } + if( sg->fx._set != tg->fx._set) { break; } + if( sg->fy._set != tg->fy._set) { break; } + if( sg->cx._set && sg->cy._set && sg->fx._set && sg->fy._set && sg->r._set) { + if( (sg->cx.computed != tg->cx.computed) || + (sg->cy.computed != tg->cy.computed) || + (sg->r.computed != tg->r.computed ) || + (sg->fx.computed != tg->fx.computed) || + (sg->fy.computed != tg->fy.computed) ) { break; } + } else if( sg->cx._set || sg->cy._set || sg->fx._set || sg->fy._set || sg->r._set ) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else if (SP_IS_MESHGRADIENT(this) && SP_IS_MESHGRADIENT(that)) { + SPMeshGradient *sg=SP_MESHGRADIENT(this); + SPMeshGradient *tg=SP_MESHGRADIENT(that); + + if( sg->x._set != !tg->x._set) { break; } + if( sg->y._set != !tg->y._set) { break; } + if( sg->x._set && sg->y._set) { + if( (sg->x.computed != tg->x.computed) || + (sg->y.computed != tg->y.computed) ) { break; } + } else if( sg->x._set || sg->y._set) { break; } // some mix of set and not set + // none set? assume aligned and fall through + } else { + break; + } + status = true; + break; + } + return status; +} + +/* + * Gradient + */ +SPGradient::SPGradient() : SPPaintServer(), units(), + spread(), + ref(nullptr), + state(2), + vector() { + + this->ref = new SPGradientReference(this); + this->ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(SPGradient::gradientRefChanged), this)); + + /** \todo + * Fixme: reprs being rearranged (e.g. via the XML editor) + * may require us to clear the state. + */ + this->state = SP_GRADIENT_STATE_UNKNOWN; + + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + this->units_set = FALSE; + + this->gradientTransform = Geom::identity(); + this->gradientTransform_set = FALSE; + + this->spread = SP_GRADIENT_SPREAD_PAD; + this->spread_set = FALSE; + + this->has_stops = FALSE; + this->has_patches = FALSE; + + this->vector.built = false; + this->vector.stops.clear(); +} + +SPGradient::~SPGradient() = default; + +/** + * Virtual build: set gradient attributes from its associated repr. + */ +void SPGradient::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + // Work-around in case a swatch had been marked for immediate collection: + if ( repr->attribute("osb:paint") && repr->attribute("inkscape:collect") ) { + repr->removeAttribute("inkscape:collect"); + } + + SPPaintServer::build(document, repr); + + for (auto& ochild: children) { + if (SP_IS_STOP(&ochild)) { + this->has_stops = TRUE; + break; + } + if (SP_IS_MESHROW(&ochild)) { + for (auto& ochild2: ochild.children) { + if (SP_IS_MESHPATCH(&ochild2)) { + this->has_patches = TRUE; + break; + } + } + if (this->has_patches == TRUE) { + break; + } + } + } + + this->readAttr( "gradientUnits" ); + this->readAttr( "gradientTransform" ); + this->readAttr( "spreadMethod" ); + this->readAttr( "xlink:href" ); + this->readAttr( "osb:paint" ); + + // Register ourselves + document->addResource("gradient", this); +} + +/** + * Virtual release of SPGradient members before destruction. + */ +void SPGradient::release() +{ + +#ifdef SP_GRADIENT_VERBOSE + g_print("Releasing this %s\n", this->getId()); +#endif + + if (this->document) { + // Unregister ourselves + this->document->removeResource("gradient", this); + } + + if (this->ref) { + this->modified_connection.disconnect(); + this->ref->detach(); + delete this->ref; + this->ref = nullptr; + } + + //this->modified_connection.~connection(); + + SPPaintServer::release(); +} + +/** + * Set gradient attribute to value. + */ +void SPGradient::set(SPAttributeEnum key, gchar const *value) +{ +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPGradient::set: " << key << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + switch (key) { + case SP_ATTR_GRADIENTUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->units = SP_GRADIENT_UNITS_USERSPACEONUSE; + } else { + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + } + + this->units_set = TRUE; + } else { + this->units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + this->units_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_GRADIENTTRANSFORM: { + Geom::Affine t; + if (value && sp_svg_transform_read(value, &t)) { + this->gradientTransform = t; + this->gradientTransform_set = TRUE; + } else { + this->gradientTransform = Geom::identity(); + this->gradientTransform_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_SPREADMETHOD: + if (value) { + if (!strcmp(value, "reflect")) { + this->spread = SP_GRADIENT_SPREAD_REFLECT; + } else if (!strcmp(value, "repeat")) { + this->spread = SP_GRADIENT_SPREAD_REPEAT; + } else { + this->spread = SP_GRADIENT_SPREAD_PAD; + } + + this->spread_set = TRUE; + } else { + this->spread_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_XLINK_HREF: + if (value) { + try { + this->ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->ref->detach(); + } + } else { + this->ref->detach(); + } + break; + + case SP_ATTR_OSB_SWATCH: + { + bool newVal = (value != nullptr); + bool modified = false; + + if (newVal != this->swatch) { + this->swatch = newVal; + modified = true; + } + + if (newVal) { + // Might need to flip solid/gradient + Glib::ustring paintVal = ( this->hasStops() && (this->getStopCount() == 0) ) ? "solid" : "gradient"; + + if ( paintVal != value ) { + this->setAttribute( "osb:paint", paintVal, nullptr ); + modified = true; + } + } + + if (modified) { + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + default: + SPPaintServer::set(key, value); + break; + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::set", false ); +#endif +} + +/** + * Gets called when the gradient is (re)attached to another gradient. + */ +void SPGradient::gradientRefChanged(SPObject *old_ref, SPObject *ref, SPGradient *gr) +{ + if (old_ref) { + gr->modified_connection.disconnect(); + } + if ( SP_IS_GRADIENT(ref) + && ref != gr ) + { + gr->modified_connection = ref->connectModified(sigc::bind<2>(sigc::ptr_fun(&SPGradient::gradientRefModified), gr)); + } + + // Per SVG, all unset attributes must be inherited from linked gradient. + // So, as we're now (re)linked, we assign linkee's values to this gradient if they are not yet set - + // but without setting the _set flags. + // FIXME: do the same for gradientTransform too + if (!gr->units_set) { + gr->units = gr->fetchUnits(); + } + if (!gr->spread_set) { + gr->spread = gr->fetchSpread(); + } + + /// \todo Fixme: what should the flags (second) argument be? */ + gradientRefModified(ref, 0, gr); +} + +/** + * Callback for child_added event. + */ +void SPGradient::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + this->invalidateVector(); + + SPPaintServer::child_added(child, ref); + + SPObject *ochild = this->get_child_by_repr(child); + if ( ochild && SP_IS_STOP(ochild) ) { + this->has_stops = TRUE; + if ( this->getStopCount() > 0 ) { + gchar const * attr = this->getAttribute("osb:paint"); + if ( attr && strcmp(attr, "gradient") ) { + this->setAttribute( "osb:paint", "gradient", nullptr ); + } + } + } + if ( ochild && SP_IS_MESHROW(ochild) ) { + this->has_patches = TRUE; + } + + /// \todo Fixme: should we schedule "modified" here? + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for remove_child event. + */ +void SPGradient::remove_child(Inkscape::XML::Node *child) +{ + this->invalidateVector(); + + SPPaintServer::remove_child(child); + + this->has_stops = FALSE; + this->has_patches = FALSE; + for (auto& ochild: children) { + if (SP_IS_STOP(&ochild)) { + this->has_stops = TRUE; + break; + } + if (SP_IS_MESHROW(&ochild)) { + for (auto& ochild2: ochild.children) { + if (SP_IS_MESHPATCH(&ochild2)) { + this->has_patches = TRUE; + break; + } + } + if (this->has_patches == TRUE) { + break; + } + } + } + + if ( this->getStopCount() == 0 ) { + gchar const * attr = this->getAttribute("osb:paint"); + + if ( attr && strcmp(attr, "solid") ) { + this->setAttribute( "osb:paint", "solid", nullptr ); + } + } + + /* Fixme: should we schedule "modified" here? */ + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/** + * Callback for modified event. + */ +void SPGradient::modified(guint flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::modified" ); +#endif + if (flags & SP_OBJECT_CHILD_MODIFIED_FLAG) { + if (SP_IS_MESHGRADIENT(this)) { + this->invalidateArray(); + } else { + this->invalidateVector(); + } + } + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + if (SP_IS_MESHGRADIENT(this)) { + this->ensureArray(); + } else { + this->ensureVector(); + } + } + + if (flags & SP_OBJECT_MODIFIED_FLAG) flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + flags &= SP_OBJECT_MODIFIED_CASCADE; + + // FIXME: climb up the ladder of hrefs + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::modified", false ); +#endif +} + +SPStop* SPGradient::getFirstStop() +{ + SPStop* first = nullptr; + for (auto& ochild: children) { + if (SP_IS_STOP(&ochild)) { + first = SP_STOP(&ochild); + break; + } + } + return first; +} + +int SPGradient::getStopCount() const +{ + int count = 0; + + for (SPStop *stop = const_cast(this)->getFirstStop(); stop && stop->getNextStop(); stop = stop->getNextStop()) { + count++; + } + + return count; +} + +/** + * Write gradient attributes to repr. + */ +Inkscape::XML::Node *SPGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::write" ); +#endif + + SPPaintServer::write(xml_doc, repr, flags); + + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vector l; + + for (auto& child: children) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } + + if (this->ref->getURI()) { + auto uri_string = this->ref->getURI()->str(); + repr->setAttributeOrRemoveIfEmpty("xlink:href", uri_string); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->units_set) { + switch (this->units) { + case SP_GRADIENT_UNITS_USERSPACEONUSE: + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + break; + default: + repr->setAttribute("gradientUnits", "objectBoundingBox"); + break; + } + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->gradientTransform_set) { + gchar *c=sp_svg_transform_write(this->gradientTransform); + repr->setAttribute("gradientTransform", c); + g_free(c); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->spread_set) { + /* FIXME: Ensure that this->spread is the inherited value + * if !this->spread_set. Not currently happening: see SPGradient::modified. + */ + switch (this->spread) { + case SP_GRADIENT_SPREAD_REFLECT: + repr->setAttribute("spreadMethod", "reflect"); + break; + case SP_GRADIENT_SPREAD_REPEAT: + repr->setAttribute("spreadMethod", "repeat"); + break; + default: + repr->setAttribute("spreadMethod", "pad"); + break; + } + } + + if ( (flags & SP_OBJECT_WRITE_EXT) && this->isSwatch() ) { + if ( this->isSolid() ) { + repr->setAttribute( "osb:paint", "solid" ); + } else { + repr->setAttribute( "osb:paint", "gradient" ); + } + } else { + repr->removeAttribute("osb:paint"); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPGradient::write", false ); +#endif + return repr; +} + +/** + * Forces the vector to be built, if not present (i.e., changed). + * + * \pre SP_IS_GRADIENT(gradient). + */ +void SPGradient::ensureVector() +{ + if ( !vector.built ) { + rebuildVector(); + } +} + +/** + * Forces the array to be built, if not present (i.e., changed). + * + * \pre SP_IS_GRADIENT(gradient). + */ +void SPGradient::ensureArray() +{ + //std::cout << "SPGradient::ensureArray()" << std::endl; + if ( !array.built ) { + rebuildArray(); + } +} + +/** + * Set units property of gradient and emit modified. + */ +void SPGradient::setUnits(SPGradientUnits units) +{ + if (units != this->units) { + this->units = units; + units_set = TRUE; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +/** + * Set spread property of gradient and emit modified. + */ +void SPGradient::setSpread(SPGradientSpread spread) +{ + if (spread != this->spread) { + this->spread = spread; + spread_set = TRUE; + requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +/** + * Returns the first of {src, src-\>ref-\>getObject(), + * src-\>ref-\>getObject()-\>ref-\>getObject(),...} + * for which \a match is true, or NULL if none found. + * + * The raison d'être of this routine is that it correctly handles cycles in the href chain (e.g., if + * a gradient gives itself as its href, or if each of two gradients gives the other as its href). + * + * \pre SP_IS_GRADIENT(src). + */ +static SPGradient * +chase_hrefs(SPGradient *const src, bool (*match)(SPGradient const *)) +{ + g_return_val_if_fail(SP_IS_GRADIENT(src), NULL); + + /* Use a pair of pointers for detecting loops: p1 advances half as fast as p2. If there is a + loop, then once p1 has entered the loop, we'll detect it the next time the distance between + p1 and p2 is a multiple of the loop size. */ + SPGradient *p1 = src, *p2 = src; + bool do1 = false; + for (;;) { + if (match(p2)) { + return p2; + } + + p2 = p2->ref->getObject(); + if (!p2) { + return p2; + } + if (do1) { + p1 = p1->ref->getObject(); + } + do1 = !do1; + + if ( p2 == p1 ) { + /* We've been here before, so return NULL to indicate that no matching gradient found + * in the chain. */ + return nullptr; + } + } +} + +/** + * True if gradient has stops. + */ +static bool has_stopsFN(SPGradient const *gr) +{ + return gr->hasStops(); +} + +/** + * True if gradient has patches (i.e. a mesh). + */ +static bool has_patchesFN(SPGradient const *gr) +{ + return gr->hasPatches(); +} + +/** + * True if gradient has spread set. + */ +static bool has_spread_set(SPGradient const *gr) +{ + return gr->isSpreadSet(); +} + +/** + * True if gradient has units set. + */ +static bool +has_units_set(SPGradient const *gr) +{ + return gr->isUnitsSet(); +} + + +SPGradient *SPGradient::getVector(bool force_vector) +{ + SPGradient * src = chase_hrefs(this, has_stopsFN); + if (src == nullptr) { + src = this; + } + + if (force_vector) { + src = sp_gradient_ensure_vector_normalized(src); + } + return src; +} + +SPGradient *SPGradient::getArray(bool force_vector) +{ + SPGradient * src = chase_hrefs(this, has_patchesFN); + if (src == nullptr) { + src = this; + } + return src; +} + +/** + * Returns the effective spread of given gradient (climbing up the refs chain if needed). + * + * \pre SP_IS_GRADIENT(gradient). + */ +SPGradientSpread SPGradient::fetchSpread() +{ + SPGradient const *src = chase_hrefs(this, has_spread_set); + return ( src + ? src->spread + : SP_GRADIENT_SPREAD_PAD ); // pad is the default +} + +/** + * Returns the effective units of given gradient (climbing up the refs chain if needed). + * + * \pre SP_IS_GRADIENT(gradient). + */ +SPGradientUnits SPGradient::fetchUnits() +{ + SPGradient const *src = chase_hrefs(this, has_units_set); + return ( src + ? src->units + : SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX ); // bbox is the default +} + + +/** + * Clears the gradient's svg:stop children from its repr. + */ +void +SPGradient::repr_clear_vector() +{ + Inkscape::XML::Node *repr = getRepr(); + + /* Collect stops from original repr */ + std::vector l; + for (Inkscape::XML::Node *child = repr->firstChild() ; child != nullptr; child = child->next() ) { + if (!strcmp(child->name(), "svg:stop")) { + l.push_back(child); + } + } + /* Remove all stops */ + for (auto i=l.rbegin();i!=l.rend();++i) { + /** \todo + * fixme: This should work, unless we make gradient + * into generic group. + */ + sp_repr_unparent(*i); + } +} + +/** + * Writes the gradient's internal vector (whether from its own stops, or + * inherited from refs) into the gradient repr as svg:stop elements. + */ +void +SPGradient::repr_write_vector() +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = getRepr(); + + /* We have to be careful, as vector may be our own, so construct repr list at first */ + std::vector l; + + for (auto & stop : vector.stops) { + Inkscape::CSSOStringStream os; + Inkscape::XML::Node *child = xml_doc->createElement("svg:stop"); + sp_repr_set_css_double(child, "offset", stop.offset); + /* strictly speaking, offset an SVG rather than a CSS one, but exponents make no + * sense for offset proportions. */ + os << "stop-color:" << stop.color.toString() << ";stop-opacity:" << stop.opacity; + child->setAttribute("style", os.str()); + /* Order will be reversed here */ + l.push_back(child); + } + + repr_clear_vector(); + + /* And insert new children from list */ + for (auto i=l.rbegin();i!=l.rend();++i) { + Inkscape::XML::Node *child = *i; + repr->addChild(child, nullptr); + Inkscape::GC::release(child); + } +} + + +void SPGradient::gradientRefModified(SPObject */*href*/, guint /*flags*/, SPGradient *gradient) +{ + if ( gradient->invalidateVector() ) { + gradient->requestModified(SP_OBJECT_MODIFIED_FLAG); + // Conditional to avoid causing infinite loop if there's a cycle in the href chain. + } +} + +/** Return true if change made. */ +bool SPGradient::invalidateVector() +{ + bool ret = false; + + if (vector.built) { + vector.built = false; + vector.stops.clear(); + ret = true; + } + + return ret; +} + +/** Return true if change made. */ +bool SPGradient::invalidateArray() +{ + bool ret = false; + + if (array.built) { + array.built = false; + // array.clear(); + ret = true; + } + + return ret; +} + +/** Creates normalized color vector */ +void SPGradient::rebuildVector() +{ + gint len = 0; + for (auto& child: children) { + if (SP_IS_STOP(&child)) { + len ++; + } + } + + has_stops = (len != 0); + + vector.stops.clear(); + + SPGradient *reffed = ref ? ref->getObject() : nullptr; + if ( !hasStops() && reffed ) { + /* Copy vector from referenced gradient */ + vector.built = true; // Prevent infinite recursion. + reffed->ensureVector(); + if (!reffed->vector.stops.empty()) { + vector.built = reffed->vector.built; + vector.stops.assign(reffed->vector.stops.begin(), reffed->vector.stops.end()); + return; + } + } + + for (auto& child: children) { + if (SP_IS_STOP(&child)) { + SPStop *stop = SP_STOP(&child); + + SPGradientStop gstop; + if (!vector.stops.empty()) { + // "Each gradient offset value is required to be equal to or greater than the + // previous gradient stop's offset value. If a given gradient stop's offset + // value is not equal to or greater than all previous offset values, then the + // offset value is adjusted to be equal to the largest of all previous offset + // values." + gstop.offset = MAX(stop->offset, vector.stops.back().offset); + } else { + gstop.offset = stop->offset; + } + + // "Gradient offset values less than 0 (or less than 0%) are rounded up to + // 0%. Gradient offset values greater than 1 (or greater than 100%) are rounded + // down to 100%." + gstop.offset = CLAMP(gstop.offset, 0, 1); + + gstop.color = stop->getColor(); + gstop.opacity = stop->getOpacity(); + + vector.stops.push_back(gstop); + } + } + + // Normalize per section 13.2.4 of SVG 1.1. + if (vector.stops.empty()) { + /* "If no stops are defined, then painting shall occur as if 'none' were specified as the + * paint style." + */ + { + SPGradientStop gstop; + gstop.offset = 0.0; + gstop.color.set( 0x00000000 ); + gstop.opacity = 0.0; + vector.stops.push_back(gstop); + } + { + SPGradientStop gstop; + gstop.offset = 1.0; + gstop.color.set( 0x00000000 ); + gstop.opacity = 0.0; + vector.stops.push_back(gstop); + } + } else { + /* "If one stop is defined, then paint with the solid color fill using the color defined + * for that gradient stop." + */ + if (vector.stops.front().offset > 0.0) { + // If the first one is not at 0, then insert a copy of the first at 0. + SPGradientStop gstop; + gstop.offset = 0.0; + gstop.color = vector.stops.front().color; + gstop.opacity = vector.stops.front().opacity; + vector.stops.insert(vector.stops.begin(), gstop); + } + if (vector.stops.back().offset < 1.0) { + // If the last one is not at 1, then insert a copy of the last at 1. + SPGradientStop gstop; + gstop.offset = 1.0; + gstop.color = vector.stops.back().color; + gstop.opacity = vector.stops.back().opacity; + vector.stops.push_back(gstop); + } + } + + vector.built = true; +} + +/** Creates normalized color mesh patch array */ +void SPGradient::rebuildArray() +{ + // std::cout << "SPGradient::rebuildArray()" << std::endl; + + if( !SP_IS_MESHGRADIENT(this) ) { + g_warning( "SPGradient::rebuildArray() called for non-mesh gradient" ); + return; + } + + array.read( SP_MESHGRADIENT( this ) ); + has_patches = array.patch_columns() > 0; +} + +Geom::Affine +SPGradient::get_g2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox) const +{ + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + return ( Geom::Scale(bbox.dimensions()) + * Geom::Translate(bbox.min()) + * Geom::Affine(ctm) ); + } else { + return ctm; + } +} + +Geom::Affine +SPGradient::get_gs2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox) const +{ + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + return ( gradientTransform + * Geom::Scale(bbox.dimensions()) + * Geom::Translate(bbox.min()) + * Geom::Affine(ctm) ); + } else { + return gradientTransform * ctm; + } +} + +void +SPGradient::set_gs2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox, Geom::Affine const &gs2d) +{ + gradientTransform = gs2d * ctm.inverse(); + if (getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX ) { + gradientTransform = ( gradientTransform + * Geom::Translate(-bbox.min()) + * Geom::Scale(bbox.dimensions()).inverse() ); + } + gradientTransform_set = TRUE; + + requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + + + +/* CAIRO RENDERING STUFF */ + +void +sp_gradient_pattern_common_setup(cairo_pattern_t *cp, + SPGradient *gr, + Geom::OptRect const &bbox, + double opacity) +{ + // set spread type + switch (gr->getSpread()) { + case SP_GRADIENT_SPREAD_REFLECT: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REFLECT); + break; + case SP_GRADIENT_SPREAD_REPEAT: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT); + break; + case SP_GRADIENT_SPREAD_PAD: + default: + cairo_pattern_set_extend(cp, CAIRO_EXTEND_PAD); + break; + } + + // add stops + if (!SP_IS_MESHGRADIENT(gr)) { + for (auto & stop : gr->vector.stops) + { + // multiply stop opacity by paint opacity + cairo_pattern_add_color_stop_rgba(cp, stop.offset, + stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity * opacity); + } + } + + // set pattern transform matrix + Geom::Affine gs2user = gr->gradientTransform; + if (gr->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + ink_cairo_pattern_set_matrix(cp, gs2user.inverse()); +} + +cairo_pattern_t * +SPGradient::create_preview_pattern(double width) +{ + cairo_pattern_t *pat = nullptr; + + if (!SP_IS_MESHGRADIENT(this)) { + ensureVector(); + + pat = cairo_pattern_create_linear(0, 0, width, 0); + + for (auto & stop : vector.stops) + { + cairo_pattern_add_color_stop_rgba(pat, stop.offset, + stop.color.v.c[0], stop.color.v.c[1], stop.color.v.c[2], stop.opacity); + } + } else { + + // For the moment, use the top row of nodes for preview. + unsigned columns = array.patch_columns(); + + double offset = 1.0/double(columns); + + pat = cairo_pattern_create_linear(0, 0, width, 0); + + for (unsigned i = 0; i < columns+1; ++i) { + SPMeshNode* node = array.node( 0, i*3 ); + cairo_pattern_add_color_stop_rgba(pat, i*offset, + node->color.v.c[0], node->color.v.c[1], node->color.v.c[2], node->opacity); + } + } + + return pat; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-gradient.h b/src/object/sp-gradient.h new file mode 100644 index 0000000..3de9cbe --- /dev/null +++ b/src/object/sp-gradient.h @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_H +#define SEEN_SP_GRADIENT_H +/* + * Authors: + * Lauris Kaplinski + * Johan Engelen + * Jon A. Cruz + * + * Copyrigt (C) 2010 Jon A. Cruz + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/affine.h> +#include +#include +#include +#include + +#include "sp-paint-server.h" +#include "sp-gradient-spread.h" +#include "sp-gradient-units.h" +#include "sp-gradient-vector.h" +#include "sp-mesh-array.h" + +class SPGradientReference; +class SPStop; + +#define SP_GRADIENT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GRADIENT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +enum SPGradientType { + SP_GRADIENT_TYPE_UNKNOWN, + SP_GRADIENT_TYPE_LINEAR, + SP_GRADIENT_TYPE_RADIAL, + SP_GRADIENT_TYPE_MESH +}; + +enum SPGradientState { + SP_GRADIENT_STATE_UNKNOWN, + SP_GRADIENT_STATE_VECTOR, + SP_GRADIENT_STATE_PRIVATE +}; + +enum GrPointType { + POINT_LG_BEGIN = 0, //start enum at 0 (for indexing into gr_knot_shapes array for example) + POINT_LG_END, + POINT_LG_MID, + POINT_RG_CENTER, + POINT_RG_R1, + POINT_RG_R2, + POINT_RG_FOCUS, + POINT_RG_MID1, + POINT_RG_MID2, + POINT_MG_CORNER, + POINT_MG_HANDLE, + POINT_MG_TENSOR, + // insert new point types here. + + POINT_G_INVALID +}; + +namespace Inkscape { + +enum PaintTarget { + FOR_FILL, + FOR_STROKE +}; + +/** + * Convenience function to access a common vector of all enum values. + */ +std::vector const &allPaintTargets(); + +} // namespace Inkscape + +/** + * Gradient + * + * Implement spread, stops list + * \todo fixme: Implement more here (Lauris) + */ +class SPGradient : public SPPaintServer { +public: + SPGradient(); + ~SPGradient() override; + +private: + /** gradientUnits attribute */ + SPGradientUnits units; + unsigned int units_set : 1; +public: + + /** gradientTransform attribute */ + Geom::Affine gradientTransform; + unsigned int gradientTransform_set : 1; + +private: + /** spreadMethod attribute */ + SPGradientSpread spread; + unsigned int spread_set : 1; + + /** Gradient stops */ + unsigned int has_stops : 1; + + /** Gradient patches */ + unsigned int has_patches : 1; + +public: + /** Reference (href) */ + SPGradientReference *ref; + + /** State in Inkscape gradient system */ + unsigned int state; + + /** Linear and Radial Gradients */ + + /** Composed vector */ + SPGradientVector vector; + + sigc::connection modified_connection; + + bool hasStops() const; + + SPStop* getFirstStop(); + int getStopCount() const; + + bool isEquivalent(SPGradient *b); + bool isAligned(SPGradient *b); + + /** Mesh Gradients **************/ + + /** Composed array (for mesh gradients) */ + SPMeshNodeArray array; + SPMeshNodeArray array_smoothed; // Smoothed version of array + + bool hasPatches() const; + + + /** All Gradients **************/ + bool isUnitsSet() const; + SPGradientUnits getUnits() const; + void setUnits(SPGradientUnits units); + + + bool isSpreadSet() const; + SPGradientSpread getSpread() const; + +/** + * Returns private vector of given gradient (the gradient at the end of the href chain which has + * stops), optionally normalizing it. + * + * \pre SP_IS_GRADIENT(gradient). + * \pre There exists a gradient in the chain that has stops. + */ + SPGradient *getVector(bool force_private = false); + + /** + * Returns private mesh of given gradient (the gradient at the end of the href chain which has + * patches), optionally normalizing it. + */ + SPGradient *getArray(bool force_private = false); + + //static GType getType(); + + /** Forces vector to be built, if not present (i.e. changed) */ + void ensureVector(); + + /** Forces array (mesh) to be built, if not present (i.e. changed) */ + void ensureArray(); + + /** + * Set spread property of gradient and emit modified. + */ + void setSpread(SPGradientSpread spread); + + SPGradientSpread fetchSpread(); + SPGradientUnits fetchUnits(); + + void setSwatch(bool swatch = true); + + static void gradientRefModified(SPObject *href, unsigned int flags, SPGradient *gradient); + static void gradientRefChanged(SPObject *old_ref, SPObject *ref, SPGradient *gr); + + /* Gradient repr methods */ + void repr_write_vector(); + void repr_clear_vector(); + + cairo_pattern_t *create_preview_pattern(double width); + + /** Transforms to/from gradient position space in given environment */ + Geom::Affine get_g2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox) const; + Geom::Affine get_gs2d_matrix(Geom::Affine const &ctm, + Geom::Rect const &bbox) const; + void set_gs2d_matrix(Geom::Affine const &ctm, Geom::Rect const &bbox, + Geom::Affine const &gs2d); + +private: + bool invalidateVector(); + bool invalidateArray(); + void rebuildVector(); + void rebuildArray(); + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) override; + void remove_child(Inkscape::XML::Node *child) override; + + void set(SPAttributeEnum key, char const *value) override; +}; + +void +sp_gradient_pattern_common_setup(cairo_pattern_t *cp, + SPGradient *gr, + Geom::OptRect const &bbox, + double opacity); + + +#endif // SEEN_SP_GRADIENT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-guide.cpp b/src/object/sp-guide.cpp new file mode 100644 index 0000000..8b36f4f --- /dev/null +++ b/src/object/sp-guide.cpp @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape guideline implementation + * + * Authors: + * Lauris Kaplinski + * Peter Moulder + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2000-2002 authors + * Copyright (C) 2004 Monash University + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include + +#include "display/sp-canvas.h" +#include "display/guideline.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/stringstream.h" +#include "attributes.h" +#include "sp-guide.h" +#include "sp-item-notify-moveto.h" +#include +#include +#include +#include "inkscape.h" +#include "desktop.h" +#include "sp-root.h" +#include "sp-namedview.h" +#include "document-undo.h" +#include "helper-fns.h" +#include "verbs.h" + +using Inkscape::DocumentUndo; + +SPGuide::SPGuide() + : SPObject() + , label(nullptr) + , locked(false) + , normal_to_line(Geom::Point(0.,1.)) + , point_on_line(Geom::Point(0.,0.)) + , color(0x0000ff7f) + , hicolor(0xff00007f) +{} + +void SPGuide::setColor(guint32 c) +{ + color = c; + + for(auto view : this->views) { + sp_guideline_set_color(view, this->color); + } +} + +void SPGuide::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + + this->readAttr( "inkscape:color" ); + this->readAttr( "inkscape:label" ); + this->readAttr( "inkscape:locked" ); + this->readAttr( "orientation" ); + this->readAttr( "position" ); + + /* Register */ + document->addResource("guide", this); +} + +void SPGuide::release() +{ + for(auto view : this->views) { + sp_guideline_delete(view); + } + this->views.clear(); + + if (this->document) { + // Unregister ourselves + this->document->removeResource("guide", this); + } + + SPObject::release(); +} + +void SPGuide::set(SPAttributeEnum key, const gchar *value) { + switch (key) { + case SP_ATTR_INKSCAPE_COLOR: + if (value) { + this->setColor(sp_svg_read_color(value, 0x0000ff00) | 0x7f); + } + break; + case SP_ATTR_INKSCAPE_LABEL: + // this->label already freed in sp_guideline_set_label (src/display/guideline.cpp) + // see bug #1498444, bug #1469514 + if (value) { + this->label = g_strdup(value); + } else { + this->label = nullptr; + } + + this->set_label(this->label, false); + break; + case SP_ATTR_INKSCAPE_LOCKED: + if (value) { + this->set_locked(helperfns_read_bool(value, false), false); + } + break; + case SP_ATTR_ORIENTATION: + { + if (value && !strcmp(value, "horizontal")) { + /* Visual representation of a horizontal line, constrain vertically (y coordinate). */ + this->normal_to_line = Geom::Point(0., 1.); + } else if (value && !strcmp(value, "vertical")) { + this->normal_to_line = Geom::Point(1., 0.); + } else if (value) { + gchar ** strarray = g_strsplit(value, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2 && (fabs(newx) > 1e-6 || fabs(newy) > 1e-6)) { + Geom::Point direction(newx, newy); + + // stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + direction[Geom::X] *= -1.0; + } + + direction.normalize(); + this->normal_to_line = direction; + } else { + // default to vertical line for bad arguments + this->normal_to_line = Geom::Point(1., 0.); + } + } else { + // default to vertical line for bad arguments + this->normal_to_line = Geom::Point(1., 0.); + } + this->set_normal(this->normal_to_line, false); + } + break; + case SP_ATTR_POSITION: + { + if (value) { + gchar ** strarray = g_strsplit(value, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + // If root viewBox set, interpret guides in terms of viewBox (90/96) + SPRoot *root = document->getRoot(); + if( root->viewBox_set ) { + if(Geom::are_near((root->width.computed * root->viewBox.height()) / (root->viewBox.width() * root->height.computed), 1.0, Geom::EPSILON)) { + // for uniform scaling, try to reduce numerical error + double vbunit2px = (root->width.computed / root->viewBox.width() + root->height.computed / root->viewBox.height())/2.0; + newx = newx * vbunit2px; + newy = newy * vbunit2px; + } else { + newx = newx * root->width.computed / root->viewBox.width(); + newy = newy * root->height.computed / root->viewBox.height(); + } + } + this->point_on_line = Geom::Point(newx, newy); + } else if (success == 1) { + // before 0.46 style guideline definition. + const gchar *attr = this->getRepr()->attribute("orientation"); + if (attr && !strcmp(attr, "horizontal")) { + this->point_on_line = Geom::Point(0, newx); + } else { + this->point_on_line = Geom::Point(newx, 0); + } + } + + // stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + this->point_on_line[Geom::Y] = document->getHeight().value("px") - this->point_on_line[Geom::Y]; + } + } else { + // default to (0,0) for bad arguments + this->point_on_line = Geom::Point(0,0); + } + // update position in non-committing way + // fixme: perhaps we need to add an update method instead, and request_update here + this->moveto(this->point_on_line, false); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +/* Only used internally and in sp-line.cpp */ +SPGuide *SPGuide::createSPGuide(SPDocument *doc, Geom::Point const &pt1, Geom::Point const &pt2) +{ + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide"); + + Geom::Point n = Geom::rot90(pt2 - pt1); + + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = pt1.x(); + double newy = pt1.y(); + + SPRoot *root = doc->getRoot(); + + // stores inverted y-axis coordinates + if (doc->is_yaxisdown()) { + newy = doc->getHeight().value("px") - newy; + n[Geom::X] *= -1.0; + } + + if( root->viewBox_set ) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0; + newx = newx * px2vbunit; + newy = newy * px2vbunit; + } else { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + } + + sp_repr_set_point(repr, "position", Geom::Point( newx, newy )); + sp_repr_set_point(repr, "orientation", n); + + SPNamedView *namedview = sp_document_namedview(doc, nullptr); + if (namedview) { + if (namedview->lockguides) { + repr->setAttribute("inkscape:locked", "true"); + } + namedview->appendChild(repr); + } + Inkscape::GC::release(repr); + + SPGuide *guide= SP_GUIDE(doc->getObjectByRepr(repr)); + return guide; +} + +SPGuide *SPGuide::duplicate(){ + return SPGuide::createSPGuide(document, point_on_line, Geom::Point(point_on_line[Geom::X] + normal_to_line[Geom::Y],point_on_line[Geom::Y] - normal_to_line[Geom::X])); +} + +void sp_guide_pt_pairs_to_guides(SPDocument *doc, std::list > &pts) +{ + for (auto & pt : pts) { + SPGuide::createSPGuide(doc, pt.first, pt.second); + } +} + +void sp_guide_create_guides_around_page(SPDesktop *dt) +{ + SPDocument *doc=dt->getDocument(); + std::list > pts; + + Geom::Point A(0, 0); + Geom::Point C(doc->getWidth().value("px"), doc->getHeight().value("px")); + Geom::Point B(C[Geom::X], 0); + Geom::Point D(0, C[Geom::Y]); + + pts.emplace_back(A, B); + pts.emplace_back(B, C); + pts.emplace_back(C, D); + pts.emplace_back(D, A); + + sp_guide_pt_pairs_to_guides(doc, pts); + + DocumentUndo::done(doc, SP_VERB_NONE, _("Create Guides Around the Page")); +} + +void sp_guide_delete_all_guides(SPDesktop *dt) +{ + SPDocument *doc=dt->getDocument(); + std::vector current = doc->getResourceList("guide"); + while (!current.empty()){ + SPGuide* guide = SP_GUIDE(*(current.begin())); + sp_guide_remove(guide); + current = doc->getResourceList("guide"); + } + + DocumentUndo::done(doc, SP_VERB_NONE, _("Delete All Guides")); +} + +void SPGuide::showSPGuide(SPCanvasGroup *group, GCallback handler) +{ + SPCanvasItem *item = sp_guideline_new(group, label, point_on_line, normal_to_line); + sp_guideline_set_color(SP_GUIDELINE(item), color); + sp_guideline_set_locked(SP_GUIDELINE(item), locked); + + g_signal_connect(G_OBJECT(item), "event", G_CALLBACK(handler), this); + + views.push_back(SP_GUIDELINE(item)); +} + +void SPGuide::showSPGuide() +{ + for(std::vector::const_iterator it = this->views.begin(); it != this->views.end(); ++it) { + sp_canvas_item_show(SP_CANVAS_ITEM(*it)); + if((*it)->origin) { + sp_canvas_item_show(SP_CANVAS_ITEM((*it)->origin)); + } else { + //reposition to same place to show knots + sp_guideline_set_position(*it, point_on_line); + } + } +} + +void SPGuide::hideSPGuide(SPCanvas *canvas) +{ + g_assert(canvas != nullptr); + g_assert(SP_IS_CANVAS(canvas)); + for(std::vector::iterator it = this->views.begin(); it != this->views.end(); ++it) { + if (canvas == SP_CANVAS_ITEM(*it)->canvas) { + sp_guideline_delete(*it); + views.erase(it); + return; + } + } + + assert(false); +} + +void SPGuide::hideSPGuide() +{ + for(std::vector::const_iterator it = this->views.begin(); it != this->views.end(); ++it) { + sp_canvas_item_hide(SP_CANVAS_ITEM(*it)); + if ((*it)->origin) { + sp_canvas_item_hide(SP_CANVAS_ITEM((*it)->origin)); + } + } +} + +void SPGuide::sensitize(SPCanvas *canvas, bool sensitive) +{ + g_assert(canvas != nullptr); + g_assert(SP_IS_CANVAS(canvas)); + + for(std::vector::const_iterator it = this->views.begin(); it != this->views.end(); ++it) { + if (canvas == SP_CANVAS_ITEM(*it)->canvas) { + sp_guideline_set_sensitive(*it, sensitive); + return; + } + } + + assert(false); +} + +Geom::Point SPGuide::getPositionFrom(Geom::Point const &pt) const +{ + return -(pt - point_on_line); +} + +double SPGuide::getDistanceFrom(Geom::Point const &pt) const +{ + return Geom::dot(pt - point_on_line, normal_to_line); +} + +/** + * \arg commit False indicates temporary moveto in response to motion event while dragging, + * true indicates a "committing" version: in response to button release event after + * dragging a guideline, or clicking OK in guide editing dialog. + */ +void SPGuide::moveto(Geom::Point const point_on_line, bool const commit) +{ + if(this->locked) { + return; + } + for(auto view : this->views) { + sp_guideline_set_position(view, point_on_line); + } + + /* Calling sp_repr_set_point must precede calling sp_item_notify_moveto in the commit + case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */ + if (commit) { + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = point_on_line.x(); + double newy = point_on_line.y(); + + // stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + newy = document->getHeight().value("px") - newy; + } + + SPRoot *root = document->getRoot(); + if( root->viewBox_set ) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + double px2vbunit = (root->viewBox.width()/root->width.computed + root->viewBox.height()/root->height.computed)/2.0; + newx = newx * px2vbunit; + newy = newy * px2vbunit; + } else { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + } + + //XML Tree being used here directly while it shouldn't be. + sp_repr_set_point(getRepr(), "position", Geom::Point(newx, newy) ); + } + +/* DISABLED CODE BECAUSE SPGuideAttachment IS NOT USE AT THE MOMENT (johan) + for (std::vector::const_iterator i(attached_items.begin()), + iEnd(attached_items.end()); + i != iEnd; ++i) + { + SPGuideAttachment const &att = *i; + sp_item_notify_moveto(*att.item, this, att.snappoint_ix, position, commit); + } +*/ +} + +/** + * \arg commit False indicates temporary moveto in response to motion event while dragging, + * true indicates a "committing" version: in response to button release event after + * dragging a guideline, or clicking OK in guide editing dialog. + */ +void SPGuide::set_normal(Geom::Point const normal_to_line, bool const commit) +{ + if(this->locked) { + return; + } + for(auto view : this->views) { + sp_guideline_set_normal(view, normal_to_line); + } + + /* Calling sp_repr_set_svg_point must precede calling sp_item_notify_moveto in the commit + case, so that the guide's new position is available for sp_item_rm_unsatisfied_cns. */ + if (commit) { + //XML Tree being used directly while it shouldn't be + auto normal = normal_to_line; + + // stores inverted y-axis coordinates + if (document->is_yaxisdown()) { + normal[Geom::X] *= -1.0; + } + + sp_repr_set_point(getRepr(), "orientation", normal); + } + +/* DISABLED CODE BECAUSE SPGuideAttachment IS NOT USE AT THE MOMENT (johan) + for (std::vector::const_iterator i(attached_items.begin()), + iEnd(attached_items.end()); + i != iEnd; ++i) + { + SPGuideAttachment const &att = *i; + sp_item_notify_moveto(*att.item, this, att.snappoint_ix, position, commit); + } +*/ +} + +void SPGuide::set_color(const unsigned r, const unsigned g, const unsigned b, bool const commit) +{ + this->color = (r << 24) | (g << 16) | (b << 8) | 0x7f; + + if (! views.empty()) { + sp_guideline_set_color(views[0], this->color); + } + + if (commit) { + std::ostringstream os; + os << "rgb(" << r << "," << g << "," << b << ")"; + //XML Tree being used directly while it shouldn't be + setAttribute("inkscape:color", os.str()); + } +} + +void SPGuide::set_locked(const bool locked, bool const commit) +{ + this->locked = locked; + if ( !views.empty() ) { + sp_guideline_set_locked(views[0], locked); + } + + if (commit) { + setAttribute("inkscape:locked", locked ? "true" : "false"); + } +} + +void SPGuide::set_label(const char* label, bool const commit) +{ + if (!views.empty()) { + sp_guideline_set_label(views[0], label); + } + + if (commit) { + //XML Tree being used directly while it shouldn't be + setAttribute("inkscape:label", label); + } +} + +/** + * Returns a human-readable description of the guideline for use in dialog boxes and status bar. + * If verbose is false, only positioning information is included (useful for dialogs). + * + * The caller is responsible for freeing the string. + */ +char* SPGuide::description(bool const verbose) const +{ + using Geom::X; + using Geom::Y; + + char *descr = nullptr; + if ( !this->document ) { + // Guide has probably been deleted and no longer has an attached namedview. + descr = g_strdup(_("Deleted")); + } else { + SPNamedView *namedview = sp_document_namedview(this->document, nullptr); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(this->point_on_line[X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(this->point_on_line[Y], "px"); + Glib::ustring position_string_x = x_q.string(namedview->display_units); + Glib::ustring position_string_y = y_q.string(namedview->display_units); + + gchar *shortcuts = g_strdup_printf("; %s", _("Shift+drag to rotate, Ctrl+drag to move origin, Del to delete")); + + if ( are_near(this->normal_to_line, Geom::Point(1., 0.)) || + are_near(this->normal_to_line, -Geom::Point(1., 0.)) ) { + descr = g_strdup_printf(_("vertical, at %s"), position_string_x.c_str()); + } else if ( are_near(this->normal_to_line, Geom::Point(0., 1.)) || + are_near(this->normal_to_line, -Geom::Point(0., 1.)) ) { + descr = g_strdup_printf(_("horizontal, at %s"), position_string_y.c_str()); + } else { + double const radians = this->angle(); + double const degrees = Geom::deg_from_rad(radians); + int const degrees_int = (int) round(degrees); + descr = g_strdup_printf(_("at %d degrees, through (%s,%s)"), + degrees_int, position_string_x.c_str(), position_string_y.c_str()); + } + + if (verbose) { + gchar *oldDescr = descr; + descr = g_strconcat(oldDescr, shortcuts, NULL); + g_free(oldDescr); + } + + g_free(shortcuts); + } + + return descr; +} + +void sp_guide_remove(SPGuide *guide) +{ + g_assert(SP_IS_GUIDE(guide)); + + for (std::vector::const_iterator i(guide->attached_items.begin()), + iEnd(guide->attached_items.end()); + i != iEnd; ++i) + { + SPGuideAttachment const &att = *i; + remove_last(att.item->constraints, SPGuideConstraint(guide, att.snappoint_ix)); + } + guide->attached_items.clear(); + + //XML Tree being used directly while it shouldn't be. + sp_repr_unparent(guide->getRepr()); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-guide.h b/src/object/sp-guide.h new file mode 100644 index 0000000..4ecd62d --- /dev/null +++ b/src/object/sp-guide.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPGuide -- a guideline + *//* + * Authors: + * Lauris Kaplinski 2000 + * Johan Engelen 2007 + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GUIDE_H +#define SEEN_SP_GUIDE_H + +#include <2geom/point.h> +#include + +#include "sp-object.h" +#include "sp-guide-attachment.h" + +typedef unsigned int guint32; +extern "C" { + typedef void (*GCallback) (); +} + +class SPDesktop; +struct SPCanvas; +struct SPCanvasGroup; +struct SPGuideLine; +#define SP_GUIDE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GUIDE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/* Represents the constraint on p that dot(g.direction, p) == g.position. */ +class SPGuide : public SPObject { +public: + SPGuide(); + ~SPGuide() override = default; + + void set_color(const unsigned r, const unsigned g, const unsigned b, bool const commit); + void setColor(guint32 c); + void setHiColor(guint32 h) { hicolor = h; } + + guint32 getColor() const { return color; } + guint32 getHiColor() const { return hicolor; } + Geom::Point getPoint() const { return point_on_line; } + Geom::Point getNormal() const { return normal_to_line; } + + void moveto(Geom::Point const point_on_line, bool const commit); + void set_normal(Geom::Point const normal_to_line, bool const commit); + + void set_label(const char* label, bool const commit); + char const* getLabel() const { return label; } + + void set_locked(const bool locked, bool const commit); + bool getLocked() const { return locked; } + + static SPGuide *createSPGuide(SPDocument *doc, Geom::Point const &pt1, Geom::Point const &pt2); + SPGuide *duplicate(); + + void showSPGuide(SPCanvasGroup *group, GCallback handler); + void hideSPGuide(SPCanvas *canvas); + void showSPGuide(); // argument-free versions + void hideSPGuide(); + + void sensitize(SPCanvas *canvas, bool sensitive); + + bool isHorizontal() const { return (normal_to_line[Geom::X] == 0.); }; + bool isVertical() const { return (normal_to_line[Geom::Y] == 0.); }; + + char* description(bool const verbose = true) const; + + double angle() const { return std::atan2( - normal_to_line[Geom::X], normal_to_line[Geom::Y] ); } + double getDistanceFrom(Geom::Point const &pt) const; + Geom::Point getPositionFrom(Geom::Point const &pt) const; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, const char* value) override; + + char* label; + std::vector views; // contains an object of type SPGuideline (see display/guideline.cpp for definition) + bool locked; + Geom::Point normal_to_line; + Geom::Point point_on_line; + + guint32 color; + guint32 hicolor; +public: + std::vector attached_items; // unused +}; + +// These functions rightfully belong to SPDesktop. What gives?! +void sp_guide_pt_pairs_to_guides(SPDocument *doc, std::list > &pts); +void sp_guide_create_guides_around_page(SPDesktop *dt); +void sp_guide_delete_all_guides(SPDesktop *dt); + +void sp_guide_remove(SPGuide *guide); + +#endif // SEEN_SP_GUIDE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-hatch-path.cpp b/src/object/sp-hatch-path.cpp new file mode 100644 index 0000000..2b04ef7 --- /dev/null +++ b/src/object/sp-hatch-path.cpp @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG implementation + */ +/* + * Author: + * Tomasz Boczkowski + * Jon A. Cruz + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/path.h> + +#include "svg/svg.h" +#include "display/cairo-utils.h" +#include "display/curve.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/drawing.h" +#include "display/drawing-shape.h" +#include "helper/geom.h" +#include "attributes.h" +#include "sp-item.h" +#include "sp-hatch-path.h" +#include "svg/css-ostringstream.h" + +SPHatchPath::SPHatchPath() + : offset(), + _display(), + _curve(nullptr), + _continuous(false) +{ + offset.unset(); +} + +SPHatchPath::~SPHatchPath() += default; + +void SPHatchPath::setCurve(SPCurve *new_curve, bool owner) +{ + if (_curve) { + _curve = _curve->unref(); + } + + if (new_curve) { + if (owner) { + _curve = new_curve->ref(); + } else { + _curve = new_curve->copy(); + } + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPHatchPath::build(SPDocument* doc, Inkscape::XML::Node* repr) +{ + SPObject::build(doc, repr); + + readAttr("d"); + readAttr("offset"); + readAttr( "style" ); + + style->fill.setNone(); +} + +void SPHatchPath::release() +{ + for (auto & iter : _display) { + delete iter.arenaitem; + iter.arenaitem = nullptr; + } + + SPObject::release(); +} + +void SPHatchPath::set(SPAttributeEnum key, const gchar* value) +{ + switch (key) { + case SP_ATTR_D: + if (value) { + Geom::PathVector pv; + _readHatchPathVector(value, pv, _continuous); + SPCurve *curve = new SPCurve(pv); + + if (curve) { + setCurve(curve, true); + curve->unref(); + } + } else { + setCurve(nullptr, true); + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_OFFSET: + offset.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + if (SP_ATTRIBUTE_IS_CSS(key)) { + style->clear(key); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPObject::set(key, value); + } + break; + } +} + + +void SPHatchPath::update(SPCtx* ctx, unsigned int flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; + } + + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + if (style->stroke_width.unit == SP_CSS_UNIT_PERCENT) { + //TODO: Check specification + + SPItemCtx *ictx = static_cast(ctx); + double const aw = (ictx) ? 1.0 / ictx->i2vp.descrim() : 1.0; + style->stroke_width.computed = style->stroke_width.value * aw; + + for (auto & iter : _display) { + iter.arenaitem->setStyle(style); + } + } + } + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG)) { + for (auto & iter : _display) { + _updateView(iter); + } + } +} + +bool SPHatchPath::isValid() const +{ + if (_curve && (_repeatLength() <= 0)) { + return false; + } else { + return true; + } +} + +Inkscape::DrawingItem *SPHatchPath::show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptInterval extents) +{ + Inkscape::DrawingShape *s = new Inkscape::DrawingShape(drawing); + _display.push_front(View(s, key)); + _display.front().extents = extents; + + _updateView(_display.front()); + + return s; +} + +void SPHatchPath::hide(unsigned int key) +{ + for (ViewIterator iter = _display.begin(); iter != _display.end(); ++iter) { + if (iter->key == key) { + delete iter->arenaitem; + _display.erase(iter); + return; + } + } + + g_assert_not_reached(); +} + +void SPHatchPath::setStripExtents(unsigned int key, Geom::OptInterval const &extents) +{ + for (auto & iter : _display) { + if (iter.key == key) { + iter.extents = extents; + break; + } + } +} + +Geom::Interval SPHatchPath::bounds() const +{ + Geom::OptRect bbox; + Geom::Interval result; + + Geom::Affine transform = Geom::Translate(offset.computed, 0); + if (!_curve) { + SPCurve test_curve; + test_curve.moveto(Geom::Point(0, 0)); + test_curve.moveto(Geom::Point(0, 1)); + bbox = bounds_exact_transformed(test_curve.get_pathvector(), transform); + } else { + bbox = bounds_exact_transformed(_curve->get_pathvector(), transform); + } + + gdouble stroke_width = style->stroke_width.computed; + result.setMin(bbox->left() - stroke_width / 2); + result.setMax(bbox->right() + stroke_width / 2); + return result; +} + +SPCurve *SPHatchPath::calculateRenderCurve(unsigned key) const +{ + for (const auto & iter : _display) { + if (iter.key == key) { + return _calculateRenderCurve(iter); + } + } + g_assert_not_reached(); + return nullptr; +} + +gdouble SPHatchPath::_repeatLength() const +{ + gdouble val = 0; + + if (_curve && _curve->last_point()) { + val = _curve->last_point()->y(); + } + + return val; +} + +void SPHatchPath::_updateView(View &view) +{ + SPCurve *calculated_curve = _calculateRenderCurve(view); + + Geom::Affine offset_transform = Geom::Translate(offset.computed, 0); + view.arenaitem->setTransform(offset_transform); + style->fill.setNone(); + view.arenaitem->setStyle(style); + view.arenaitem->setPath(calculated_curve); + + calculated_curve->unref(); +} + +SPCurve *SPHatchPath::_calculateRenderCurve(View const &view) const +{ + SPCurve *calculated_curve = new SPCurve; + + if (!view.extents) { + return calculated_curve; + } + + if (!_curve) { + calculated_curve->moveto(0, view.extents->min()); + calculated_curve->lineto(0, view.extents->max()); + //TODO: if hatch has a dasharray defined, adjust line ends + } else { + gdouble repeatLength = _repeatLength(); + if (repeatLength > 0) { + gdouble initial_y = floor(view.extents->min() / repeatLength) * repeatLength; + int segment_cnt = ceil((view.extents->extent()) / repeatLength) + 1; + + SPCurve *segment =_curve->copy(); + segment->transform(Geom::Translate(0, initial_y)); + + Geom::Affine step_transform = Geom::Translate(0, repeatLength); + for (int i = 0; i < segment_cnt; ++i) { + if (_continuous) { + calculated_curve->append_continuous(segment, 0.0625); + } else { + calculated_curve->append(segment, false); + } + segment->transform(step_transform); + } + + segment->unref(); + } + } + return calculated_curve; +} + + +void SPHatchPath::_readHatchPathVector(char const *str, Geom::PathVector &pathv, bool &continous_join) +{ + if (!str) { + return; + } + + pathv = sp_svg_read_pathv(str); + + if (!pathv.empty()) { + continous_join = false; + } else { + Glib::ustring str2 = Glib::ustring::compose("M0,0 %1", str); + pathv = sp_svg_read_pathv(str2.c_str()); + if (pathv.empty()) { + return; + } + + gdouble last_point_x = pathv.back().finalPoint().x(); + Inkscape::CSSOStringStream stream; + stream << last_point_x; + Glib::ustring str3 = Glib::ustring::compose("M%1,0 %2", stream.str(), str); + Geom::PathVector pathv3 = sp_svg_read_pathv(str3.c_str()); + + //Path can be composed of relative commands only. In this case final point + //coordinates would depend on first point position. If this happens, fall + //back to using 0,0 as first path point + if (pathv3.back().finalPoint().y() == pathv.back().finalPoint().y()) { + pathv = pathv3; + } + continous_join = true; + } +} + +SPHatchPath::View::View(Inkscape::DrawingShape *arenaitem, int key) + : arenaitem(arenaitem), + extents(), + key(key) +{ +} + +SPHatchPath::View::~View() +{ + // remember, do not delete arenaitem here + arenaitem = nullptr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-hatch-path.h b/src/object/sp-hatch-path.h new file mode 100644 index 0000000..1052590 --- /dev/null +++ b/src/object/sp-hatch-path.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG implementation + */ +/* + * Author: + * Tomasz Boczkowski + * Jon A. Cruz + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_HATCH_PATH_H +#define SEEN_SP_HATCH_PATH_H + +#include +#include +#include +#include + +#include "svg/svg-length.h" + +namespace Inkscape { + +class Drawing; +class DrawingShape; + +} + +class SPHatchPath : public SPObject { +public: + SPHatchPath(); + ~SPHatchPath() override; + + SVGLength offset; + + void setCurve(SPCurve *curve, bool owner); + + bool isValid() const; + + Inkscape::DrawingItem *show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptInterval extents); + void hide(unsigned int key); + + void setStripExtents(unsigned int key, Geom::OptInterval const &extents); + Geom::Interval bounds() const; + + SPCurve *calculateRenderCurve(unsigned key) const; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, const gchar* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + +private: + class View { + public: + View(Inkscape::DrawingShape *arenaitem, int key); + //Do not delete arenaitem in destructor. + + ~View(); + + Inkscape::DrawingShape *arenaitem; + Geom::OptInterval extents; + unsigned int key; + }; + + typedef std::list::iterator ViewIterator; + typedef std::list::const_iterator ConstViewIterator; + std::list _display; + + gdouble _repeatLength() const; + void _updateView(View &view); + SPCurve *_calculateRenderCurve(View const &view) const; + + void _readHatchPathVector(char const *str, Geom::PathVector &pathv, bool &continous_join); + + SPCurve *_curve; + bool _continuous; +}; + +#endif // SEEN_SP_HATCH_PATH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-hatch.cpp b/src/object/sp-hatch.cpp new file mode 100644 index 0000000..ef9bae6 --- /dev/null +++ b/src/object/sp-hatch.cpp @@ -0,0 +1,809 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG implementation + */ +/* + * Authors: + * Tomasz Boczkowski + * Jon A. Cruz + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-hatch.h" + +#include +#include + +#include <2geom/transforms.h> +#include + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/drawing.h" +#include "display/drawing-pattern.h" + +#include "sp-defs.h" +#include "sp-hatch-path.h" +#include "sp-item.h" + +#include "svg/svg.h" + +SPHatch::SPHatch() + : SPPaintServer(), + href(), + ref(nullptr), // avoiding 'this' in initializer list + _hatchUnits(UNITS_OBJECTBOUNDINGBOX), + _hatchUnits_set(false), + _hatchContentUnits(UNITS_USERSPACEONUSE), + _hatchContentUnits_set(false), + _hatchTransform(Geom::identity()), + _hatchTransform_set(false), + _x(), + _y(), + _pitch(), + _rotate(), + _modified_connection(), + _display() +{ + ref = new SPHatchReference(this); + ref->changedSignal().connect(sigc::mem_fun(this, &SPHatch::_onRefChanged)); + + // TODO check that these should start already as unset: + _x.unset(); + _y.unset(); + _pitch.unset(); + _rotate.unset(); +} + +SPHatch::~SPHatch() = default; + +void SPHatch::build(SPDocument* doc, Inkscape::XML::Node* repr) +{ + SPPaintServer::build(doc, repr); + + readAttr("hatchUnits"); + readAttr("hatchContentUnits"); + readAttr("hatchTransform"); + readAttr("x"); + readAttr("y"); + readAttr("pitch"); + readAttr("rotate"); + readAttr("xlink:href"); + readAttr( "style" ); + + // Register ourselves + doc->addResource("hatch", this); +} + +void SPHatch::release() +{ + if (document) { + // Unregister ourselves + document->removeResource("hatch", this); + } + + std::vector children(hatchPaths()); + for (auto & view_iter : _display) { + for (auto child : children) { + child->hide(view_iter.key); + } + delete view_iter.arenaitem; + view_iter.arenaitem = nullptr; + } + + if (ref) { + _modified_connection.disconnect(); + ref->detach(); + delete ref; + ref = nullptr; + } + + SPPaintServer::release(); +} + +void SPHatch::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) +{ + SPObject::child_added(child, ref); + + SPHatchPath *path_child = dynamic_cast(document->getObjectByRepr(child)); + + if (path_child) { + for (auto & iter : _display) { + Geom::OptInterval extents = _calculateStripExtents(iter.bbox); + Inkscape::DrawingItem *ac = path_child->show(iter.arenaitem->drawing(), iter.key, extents); + + path_child->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + if (ac) { + iter.arenaitem->prependChild(ac); + } + } + } + //FIXME: notify all hatches that refer to this child set +} + +void SPHatch::set(SPAttributeEnum key, const gchar* value) +{ + switch (key) { + case SP_ATTR_HATCHUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + _hatchUnits = UNITS_USERSPACEONUSE; + } else { + _hatchUnits = UNITS_OBJECTBOUNDINGBOX; + } + + _hatchUnits_set = true; + } else { + _hatchUnits_set = false; + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HATCHCONTENTUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + _hatchContentUnits = UNITS_USERSPACEONUSE; + } else { + _hatchContentUnits = UNITS_OBJECTBOUNDINGBOX; + } + + _hatchContentUnits_set = true; + } else { + _hatchContentUnits_set = false; + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HATCHTRANSFORM: { + Geom::Affine t; + + if (value && sp_svg_transform_read(value, &t)) { + _hatchTransform = t; + _hatchTransform_set = true; + } else { + _hatchTransform = Geom::identity(); + _hatchTransform_set = false; + } + + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_X: + _x.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + _y.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_PITCH: + _pitch.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_ROTATE: + _rotate.readOrUnset(value); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_XLINK_HREF: + if (value && href == value) { + // Href unchanged, do nothing. + } else { + href.clear(); + + if (value) { + // First, set the href field; it's only used in the "unchanged" check above. + href = value; + // Now do the attaching, which emits the changed signal. + if (value) { + try { + ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref->detach(); + } + } else { + ref->detach(); + } + } + } + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + if (SP_ATTRIBUTE_IS_CSS(key)) { + style->clear(key); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPPaintServer::set(key, value); + } + break; + } +} + +bool SPHatch::_hasHatchPatchChildren(SPHatch const *hatch) +{ + for (auto& child: hatch->children) { + SPHatchPath const *hatchPath = dynamic_cast(&child); + if (hatchPath) { + return true; + } + } + return false; +} + +std::vector SPHatch::hatchPaths() +{ + std::vector list; + SPHatch *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); + + if (src) { + for (auto& child: src->children) { + SPHatchPath *hatchPath = dynamic_cast(&child); + if (hatchPath) { + list.push_back(hatchPath); + } + } + } + return list; +} + +std::vector SPHatch::hatchPaths() const +{ + std::vector list; + SPHatch const *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); + + if (src) { + for (auto& child: src->children) { + SPHatchPath const *hatchPath = dynamic_cast(&child); + if (hatchPath) { + list.push_back(hatchPath); + } + } + } + return list; +} + +// TODO: ::remove_child and ::order_changed handles - see SPPattern + + +void SPHatch::update(SPCtx* ctx, unsigned int flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector children(hatchPaths()); + + for (auto child : children) { + sp_object_ref(child, nullptr); + + for (auto & view_iter : _display) { + Geom::OptInterval strip_extents = _calculateStripExtents(view_iter.bbox); + child->setStripExtents(view_iter.key, strip_extents); + } + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + + child->updateDisplay(ctx, flags); + } + + sp_object_unref(child, nullptr); + } + + for (auto & iter : _display) { + _updateView(iter); + } +} + +void SPHatch::modified(unsigned int flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector children(hatchPaths()); + + for (auto child : children) { + sp_object_ref(child, nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child, nullptr); + } +} + +void SPHatch::_onRefChanged(SPObject *old_ref, SPObject *ref) +{ + if (old_ref) { + _modified_connection.disconnect(); + } + + SPHatch *hatch = dynamic_cast(ref); + if (hatch) { + _modified_connection = ref->connectModified(sigc::mem_fun(this, &SPHatch::_onRefModified)); + } + + if (!_hasHatchPatchChildren(this)) { + SPHatch *old_shown = nullptr; + SPHatch *new_shown = nullptr; + std::vector oldhatchPaths; + std::vector newhatchPaths; + + SPHatch *old_hatch = dynamic_cast(old_ref); + if (old_hatch) { + old_shown = old_hatch->rootHatch(); + oldhatchPaths = old_shown->hatchPaths(); + } + if (hatch) { + new_shown = hatch->rootHatch(); + newhatchPaths = new_shown->hatchPaths(); + } + if (old_shown != new_shown) { + + for (auto & iter : _display) { + Geom::OptInterval extents = _calculateStripExtents(iter.bbox); + + for (auto child : oldhatchPaths) { + child->hide(iter.key); + } + for (auto child : newhatchPaths) { + Inkscape::DrawingItem *cai = child->show(iter.arenaitem->drawing(), iter.key, extents); + child->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + if (cai) { + iter.arenaitem->appendChild(cai); + } + + } + } + } + } + + _onRefModified(ref, 0); +} + +void SPHatch::_onRefModified(SPObject */*ref*/, guint /*flags*/) +{ + requestModified(SP_OBJECT_MODIFIED_FLAG); + // Conditional to avoid causing infinite loop if there's a cycle in the href chain. +} + + +SPHatch *SPHatch::rootHatch() +{ + SPHatch *src = chase_hrefs(this, sigc::ptr_fun(&_hasHatchPatchChildren)); + return src ? src : this; // document is broken, we can't get to root; but at least we can return pat which is supposedly a valid hatch +} + +// Access functions that look up fields up the chain of referenced hatchs and return the first one which is set +// FIXME: all of them must use chase_hrefs as children() and rootHatch() + +SPHatch::HatchUnits SPHatch::hatchUnits() const +{ + HatchUnits units = _hatchUnits; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_hatchUnits_set) { + units = pat_i->_hatchUnits; + break; + } + } + return units; +} + +SPHatch::HatchUnits SPHatch::hatchContentUnits() const +{ + HatchUnits units = _hatchContentUnits; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_hatchContentUnits_set) { + units = pat_i->_hatchContentUnits; + break; + } + } + return units; +} + +Geom::Affine const &SPHatch::hatchTransform() const +{ + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_hatchTransform_set) { + return pat_i->_hatchTransform; + } + } + return _hatchTransform; +} + +gdouble SPHatch::x() const +{ + gdouble val = 0; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_x._set) { + val = pat_i->_x.computed; + break; + } + } + return val; +} + +gdouble SPHatch::y() const +{ + gdouble val = 0; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_y._set) { + val = pat_i->_y.computed; + break; + } + } + return val; +} + +gdouble SPHatch::pitch() const +{ + gdouble val = 0; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_pitch._set) { + val = pat_i->_pitch.computed; + break; + } + } + return val; +} + +gdouble SPHatch::rotate() const +{ + gdouble val = 0; + for (SPHatch const *pat_i = this; pat_i; pat_i = (pat_i->ref) ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_rotate._set) { + val = pat_i->_rotate.computed; + break; + } + } + return val; +} + +guint SPHatch::_countHrefs(SPObject *o) const +{ + if (!o) + return 1; + + guint i = 0; + + SPStyle *style = o->style; + if (style && style->fill.isPaintserver() && SP_IS_HATCH(SP_STYLE_FILL_SERVER(style)) && + SP_HATCH(SP_STYLE_FILL_SERVER(style)) == this) { + i++; + } + if (style && style->stroke.isPaintserver() && SP_IS_HATCH(SP_STYLE_STROKE_SERVER(style)) && + SP_HATCH(SP_STYLE_STROKE_SERVER(style)) == this) { + i++; + } + + for (auto &child : o->children) { + i += _countHrefs(&child); + } + + return i; +} + +SPHatch *SPHatch::clone_if_necessary(SPItem *item, const gchar *property) +{ + SPHatch *hatch = this; + if (hatch->href.empty() || hatch->hrefcount > _countHrefs(item)) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:hatch"); + repr->setAttribute("inkscape:collect", "always"); + Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id")); + repr->setAttribute("xlink:href", parent_ref); + + defsrepr->addChild(repr, nullptr); + const gchar *child_id = repr->attribute("id"); + SPObject *child = document->getObjectById(child_id); + g_assert(SP_IS_HATCH(child)); + + hatch = SP_HATCH(child); + + Glib::ustring href = Glib::ustring::compose("url(#%1)", hatch->getRepr()->attribute("id")); + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, property, href.c_str()); + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + return hatch; +} + +void SPHatch::transform_multiply(Geom::Affine postmul, bool set) +{ + if (set) { + _hatchTransform = postmul; + } else { + _hatchTransform = hatchTransform() * postmul; + } + + _hatchTransform_set = true; + + gchar *c = sp_svg_transform_write(_hatchTransform); + setAttribute("transform", c); + g_free(c); +} + +bool SPHatch::isValid() const +{ + bool valid = false; + + if (pitch() > 0) { + std::vector children(hatchPaths()); + if (!children.empty()) { + valid = true; + for (ConstChildIterator iter = children.begin(); (iter != children.end()) && valid; ++iter) { + SPHatchPath const *child = *iter; + valid = child->isValid(); + } + } + } + + return valid; +} + +Inkscape::DrawingPattern *SPHatch::show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptRect bbox) +{ + Inkscape::DrawingPattern *ai = new Inkscape::DrawingPattern(drawing); + //TODO: set some debug flag to see DrawingPattern + _display.push_front(View(ai, key)); + _display.front().bbox = bbox; + + std::vector children(hatchPaths()); + + Geom::OptInterval extents = _calculateStripExtents(bbox); + for (auto child : children) { + Inkscape::DrawingItem *cai = child->show(drawing, key, extents); + if (cai) { + ai->appendChild(cai); + } + } + + View& view = _display.front(); + _updateView(view); + + return ai; +} + +void SPHatch::hide(unsigned int key) +{ + std::vector children(hatchPaths()); + + for (auto child : children) { + child->hide(key); + } + + for (ViewIterator iter = _display.begin(); iter != _display.end(); ++iter) { + if (iter->key == key) { + delete iter->arenaitem; + _display.erase(iter); + return; + } + } + + g_assert_not_reached(); +} + + +Geom::Interval SPHatch::bounds() const +{ + Geom::Interval result; + std::vector children(hatchPaths()); + + for (auto child : children) { + if (result.extent() == 0) { + result = child->bounds(); + } else { + result |= child->bounds(); + } + } + return result; +} + +SPHatch::RenderInfo SPHatch::calculateRenderInfo(unsigned key) const +{ + RenderInfo info; + for (const auto & iter : _display) { + if (iter.key == key) { + return _calculateRenderInfo(iter); + } + } + g_assert_not_reached(); + return info; +} + +void SPHatch::_updateView(View &view) +{ + RenderInfo info = _calculateRenderInfo(view); + //The rendering of hatch overflow is implemented by repeated drawing + //of hatch paths over one strip. Within each iteration paths are moved by pitch value. + //The movement progresses from right to left. This gives the same result + //as drawing whole strips in left-to-right order. + + + view.arenaitem->setChildTransform(info.child_transform); + view.arenaitem->setPatternToUserTransform(info.pattern_to_user_transform); + view.arenaitem->setTileRect(info.tile_rect); + view.arenaitem->setStyle(style); + view.arenaitem->setOverflow(info.overflow_initial_transform, info.overflow_steps, + info.overflow_step_transform); +} + +SPHatch::RenderInfo SPHatch::_calculateRenderInfo(View const &view) const +{ + RenderInfo info; + + Geom::OptInterval extents = _calculateStripExtents(view.bbox); + if (extents) { + double tile_x = x(); + double tile_y = y(); + double tile_width = pitch(); + double tile_height = extents->max() - extents->min(); + double tile_rotate = rotate(); + double tile_render_y = extents->min(); + + if (view.bbox && (hatchUnits() == UNITS_OBJECTBOUNDINGBOX)) { + tile_x *= view.bbox->width(); + tile_y *= view.bbox->height(); + tile_width *= view.bbox->width(); + } + + // Extent calculated using content units, need to correct. + if (view.bbox && (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX)) { + tile_height *= view.bbox->height(); + tile_render_y *= view.bbox->height(); + } + + // Pattern size in hatch space + Geom::Rect hatch_tile = Geom::Rect::from_xywh(0, tile_render_y, tile_width, tile_height); + + // Content to bbox + Geom::Affine content2ps; + if (view.bbox && (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX)) { + content2ps = Geom::Affine(view.bbox->width(), 0.0, 0.0, view.bbox->height(), 0, 0); + } + + // Tile (hatch space) to user. + Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * Geom::Rotate::from_degrees(tile_rotate) * hatchTransform(); + + info.child_transform = content2ps; + info.pattern_to_user_transform = ps2user; + info.tile_rect = hatch_tile; + + if (style->overflow.computed == SP_CSS_OVERFLOW_VISIBLE) { + Geom::Interval bounds = this->bounds(); + gdouble pitch = this->pitch(); + if (view.bbox) { + if (hatchUnits() == UNITS_OBJECTBOUNDINGBOX) { + pitch *= view.bbox->width(); + } + if (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX) { + bounds *= view.bbox->width(); + } + } + gdouble overflow_right_strip = floor(bounds.max() / pitch) * pitch; + info.overflow_steps = ceil((overflow_right_strip - bounds.min()) / pitch) + 1; + info.overflow_step_transform = Geom::Translate(pitch, 0.0); + info.overflow_initial_transform = Geom::Translate(-overflow_right_strip, 0.0); + } else { + info.overflow_steps = 1; + } + } + + return info; +} + +//calculates strip extents in content space +Geom::OptInterval SPHatch::_calculateStripExtents(Geom::OptRect const &bbox) const +{ + if (!bbox || (bbox->area() == 0)) { + return Geom::OptInterval(); + } else { + double tile_x = x(); + double tile_y = y(); + double tile_rotate = rotate(); + + Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * Geom::Rotate::from_degrees(tile_rotate) * hatchTransform(); + Geom::Affine user2ps = ps2user.inverse(); + + Geom::Interval extents; + for (int i = 0; i < 4; ++i) { + Geom::Point corner = bbox->corner(i); + Geom::Point corner_ps = corner * user2ps; + if (i == 0 || corner_ps.y() < extents.min()) { + extents.setMin(corner_ps.y()); + } + if (i == 0 || corner_ps.y() > extents.max()) { + extents.setMax(corner_ps.y()); + } + } + + if (hatchContentUnits() == UNITS_OBJECTBOUNDINGBOX) { + extents /= bbox->height(); + } + + return extents; + } +} + +cairo_pattern_t* SPHatch::pattern_new(cairo_t * /*base_ct*/, Geom::OptRect const &/*bbox*/, double /*opacity*/) +{ + //this code should not be used + //it is however required by the fact that SPPaintServer::hatch_new is pure virtual + return cairo_pattern_create_rgb(0.5, 0.5, 1.0); +} + +void SPHatch::setBBox(unsigned int key, Geom::OptRect const &bbox) +{ + for (auto & iter : _display) { + if (iter.key == key) { + iter.bbox = bbox; + break; + } + } +} + +// + +SPHatch::RenderInfo::RenderInfo() + : child_transform(), + pattern_to_user_transform(), + tile_rect(), + overflow_steps(0), + overflow_step_transform(), + overflow_initial_transform() +{ +} + +SPHatch::RenderInfo::~RenderInfo() += default; + +// + +SPHatch::View::View(Inkscape::DrawingPattern *arenaitem, int key) + : arenaitem(arenaitem), + bbox(), + key(key) +{ +} + +SPHatch::View::~View() +{ + // remember, do not delete arenaitem here + arenaitem = nullptr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-hatch.h b/src/object/sp-hatch.h new file mode 100644 index 0000000..f548163 --- /dev/null +++ b/src/object/sp-hatch.h @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG implementation + */ +/* + * Authors: + * Tomasz Boczkowski + * Jon A. Cruz + * + * Copyright (C) 2014 Tomasz Boczkowski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_HATCH_H +#define SEEN_SP_HATCH_H + +#include +#include +#include +#include + +#include "svg/svg-length.h" +#include "svg/svg-angle.h" +#include "sp-paint-server.h" +#include "uri-references.h" + +class SPHatchReference; +class SPHatchPath; +class SPItem; + +namespace Inkscape { + +class Drawing; +class DrawingPattern; + +namespace XML { + +class Node; + +} +} + +#define SP_HATCH(obj) (dynamic_cast((SPObject *)obj)) +#define SP_IS_HATCH(obj) (dynamic_cast((SPObject *)obj) != NULL) + +class SPHatch : public SPPaintServer { +public: + enum HatchUnits { + UNITS_USERSPACEONUSE, + UNITS_OBJECTBOUNDINGBOX + }; + + class RenderInfo { + public: + RenderInfo(); + ~RenderInfo(); + + Geom::Affine child_transform; + Geom::Affine pattern_to_user_transform; + Geom::Rect tile_rect; + + int overflow_steps; + Geom::Affine overflow_step_transform; + Geom::Affine overflow_initial_transform; + }; + + SPHatch(); + ~SPHatch() override; + + // Reference (href) + Glib::ustring href; + SPHatchReference *ref; + + gdouble x() const; + gdouble y() const; + gdouble pitch() const; + gdouble rotate() const; + HatchUnits hatchUnits() const; + HatchUnits hatchContentUnits() const; + Geom::Affine const &hatchTransform() const; + SPHatch *rootHatch(); //TODO: const + + std::vector hatchPaths(); + std::vector hatchPaths() const; + + SPHatch *clone_if_necessary(SPItem *item, const gchar *property); + void transform_multiply(Geom::Affine postmul, bool set); + + bool isValid() const override; + + Inkscape::DrawingPattern *show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptRect bbox) override; + void hide(unsigned int key) override; + cairo_pattern_t* pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) override; + + RenderInfo calculateRenderInfo(unsigned key) const; + Geom::Interval bounds() const; + void setBBox(unsigned int key, Geom::OptRect const &bbox) override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void set(SPAttributeEnum key, const gchar* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + +private: + class View { + public: + View(Inkscape::DrawingPattern *arenaitem, int key); + //Do not delete arenaitem in destructor. + + ~View(); + + Inkscape::DrawingPattern *arenaitem; + Geom::OptRect bbox; + unsigned int key; + }; + + typedef std::vector::iterator ChildIterator; + typedef std::vector::const_iterator ConstChildIterator; + typedef std::list::iterator ViewIterator; + typedef std::list::const_iterator ConstViewIterator; + + static bool _hasHatchPatchChildren(SPHatch const* hatch); + + void _updateView(View &view); + RenderInfo _calculateRenderInfo(View const &view) const; + Geom::OptInterval _calculateStripExtents(Geom::OptRect const &bbox) const; + + /** + Count how many times hatch is used by the styles of o and its descendants + */ + guint _countHrefs(SPObject *o) const; + + /** + * Gets called when the hatch is reattached to another + */ + void _onRefChanged(SPObject *old_ref, SPObject *ref); + + /** + * Gets called when the referenced is changed + */ + void _onRefModified(SPObject *ref, guint flags); + + // patternUnits and patternContentUnits attribute + HatchUnits _hatchUnits : 1; + bool _hatchUnits_set : 1; + HatchUnits _hatchContentUnits : 1; + bool _hatchContentUnits_set : 1; + + // hatchTransform attribute + Geom::Affine _hatchTransform; + bool _hatchTransform_set : 1; + + // Strip + SVGLength _x; + SVGLength _y; + SVGLength _pitch; + SVGAngle _rotate; + + sigc::connection _modified_connection; + + std::list _display; +}; + + +class SPHatchReference : public Inkscape::URIReference { +public: + SPHatchReference (SPObject *obj) + : URIReference(obj) + {} + + SPHatch *getObject() const { + return reinterpret_cast(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override { + return dynamic_cast(obj) != nullptr && URIReference::_acceptObject(obj); + } +}; + +#endif // SEEN_SP_HATCH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-image.cpp b/src/object/sp-image.cpp new file mode 100644 index 0000000..cec0c12 --- /dev/null +++ b/src/object/sp-image.cpp @@ -0,0 +1,905 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Edward Flick (EAF) + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include +#include +#include +#include +#include +#include <2geom/rect.h> +#include <2geom/transforms.h> +#include +#include + +#include "display/drawing-image.h" +#include "display/cairo-utils.h" +#include "display/curve.h" +// Added for preserveAspectRatio support -- EAF +#include "attributes.h" +#include "print.h" +#include "document.h" +#include "sp-image.h" +#include "sp-clippath.h" +#include "xml/quote.h" +#include "preferences.h" +#include "io/sys.h" + +#if defined(HAVE_LIBLCMS2) +#include "cms-system.h" +#include "color-profile.h" +#include + +//#define DEBUG_LCMS +#ifdef DEBUG_LCMS +#define DEBUG_MESSAGE(key, ...)\ +{\ + g_message( __VA_ARGS__ );\ +} +#include +#else +#define DEBUG_MESSAGE(key, ...) +#endif // DEBUG_LCMS +#endif // defined(HAVE_LIBLCMS2) +/* + * SPImage + */ + +// TODO: give these constants better names: +#define MAGIC_EPSILON 1e-9 +#define MAGIC_EPSILON_TOO 1e-18 +// TODO: also check if it is correct to be using two different epsilon values + +static void sp_image_set_curve(SPImage *image); + +static Inkscape::Pixbuf *sp_image_repr_read_image(gchar const *href, gchar const *absref, gchar const *base, + double svgdpi = 0); +static void sp_image_update_arenaitem (SPImage *img, Inkscape::DrawingImage *ai); +static void sp_image_update_canvas_image (SPImage *image); + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE_SCISLAC(key, ...) \ +{\ + Inkscape::Preferences *prefs = Inkscape::Preferences::get();\ + bool dump = prefs->getBool("/options/scislac/" #key);\ + bool dumpD = prefs->getBool("/options/scislac/" #key "D");\ + bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2");\ + dumpD &&= ( (update_in_progress == 0) || dumpD2 );\ + if ( dump )\ + {\ + g_message( __VA_ARGS__ );\ +\ + }\ + if ( dumpD )\ + {\ + GtkWidget *dialog = gtk_message_dialog_new(NULL,\ + GTK_DIALOG_DESTROY_WITH_PARENT, \ + GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, \ + __VA_ARGS__ \ + );\ + g_signal_connect_swapped(dialog, "response",\ + G_CALLBACK(gtk_widget_destroy), \ + dialog); \ + gtk_widget_show_all( dialog );\ + }\ +} +#else // DEBUG_LCMS +#define DEBUG_MESSAGE_SCISLAC(key, ...) +#endif // DEBUG_LCMS + +SPImage::SPImage() : SPItem(), SPViewBox() { + + this->x.unset(); + this->y.unset(); + this->width.unset(); + this->height.unset(); + this->clipbox = Geom::Rect(); + this->sx = this->sy = 1.0; + this->ox = this->oy = 0.0; + this->dpi = 96.00; + this->prev_width = 0.0; + this->prev_height = 0.0; + this->curve = nullptr; + + this->href = nullptr; +#if defined(HAVE_LIBLCMS2) + this->color_profile = nullptr; +#endif // defined(HAVE_LIBLCMS2) + this->pixbuf = nullptr; +} + +SPImage::~SPImage() = default; + +void SPImage::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr( "xlink:href" ); + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "width" ); + this->readAttr( "height" ); + this->readAttr("inkscape:svg-dpi"); + this->readAttr( "preserveAspectRatio" ); + this->readAttr( "color-profile" ); + + /* Register */ + document->addResource("image", this); +} + +void SPImage::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("image", this); + } + + if (this->href) { + g_free (this->href); + this->href = nullptr; + } + + delete this->pixbuf; + this->pixbuf = nullptr; + +#if defined(HAVE_LIBLCMS2) + if (this->color_profile) { + g_free (this->color_profile); + this->color_profile = nullptr; + } +#endif // defined(HAVE_LIBLCMS2) + + if (this->curve) { + this->curve = this->curve->unref(); + } + + SPItem::release(); +} + +void SPImage::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_XLINK_HREF: + g_free (this->href); + this->href = (value) ? g_strdup (value) : nullptr; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + + case SP_ATTR_X: + /* ex, em not handled correctly. */ + if (!this->x.read(value)) { + this->x.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + /* ex, em not handled correctly. */ + if (!this->y.read(value)) { + this->y.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_WIDTH: + /* ex, em not handled correctly. */ + if (!this->width.read(value)) { + this->width.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HEIGHT: + /* ex, em not handled correctly. */ + if (!this->height.read(value)) { + this->height.unset(); + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_SVG_DPI: + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + +#if defined(HAVE_LIBLCMS2) + case SP_PROP_COLOR_PROFILE: + if ( this->color_profile ) { + g_free (this->color_profile); + } + + this->color_profile = (value) ? g_strdup (value) : nullptr; + + if ( value ) { + DEBUG_MESSAGE( lcmsFour, " color-profile set to '%s'", value ); + } else { + DEBUG_MESSAGE( lcmsFour, " color-profile cleared" ); + } + + // TODO check on this HREF_MODIFIED flag + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + break; + +#endif // defined(HAVE_LIBLCMS2) + + default: + SPItem::set(key, value); + break; + } + + sp_image_set_curve(this); //creates a curve at the image's boundary for snapping +} + +// BLIP +#if defined(HAVE_LIBLCMS2) +void SPImage::apply_profile(Inkscape::Pixbuf *pixbuf) { + + // TODO: this will prevent using MIME data when exporting. + // Integrate color correction into loading. + pixbuf->ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); + int imagewidth = pixbuf->width(); + int imageheight = pixbuf->height(); + int rowstride = pixbuf->rowstride();; + guchar* px = pixbuf->pixels(); + + if ( px ) { + DEBUG_MESSAGE( lcmsFive, "in 's sp_image_update. About to call colorprofile_get_handle()" ); + + guint profIntent = Inkscape::RENDERING_INTENT_UNKNOWN; + cmsHPROFILE prof = Inkscape::CMSSystem::getHandle( this->document, + &profIntent, + this->color_profile ); + if ( prof ) { + cmsProfileClassSignature profileClass = cmsGetDeviceClass( prof ); + if ( profileClass != cmsSigNamedColorClass ) { + int intent = INTENT_PERCEPTUAL; + + switch ( profIntent ) { + case Inkscape::RENDERING_INTENT_RELATIVE_COLORIMETRIC: + intent = INTENT_RELATIVE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_SATURATION: + intent = INTENT_SATURATION; + break; + case Inkscape::RENDERING_INTENT_ABSOLUTE_COLORIMETRIC: + intent = INTENT_ABSOLUTE_COLORIMETRIC; + break; + case Inkscape::RENDERING_INTENT_PERCEPTUAL: + case Inkscape::RENDERING_INTENT_UNKNOWN: + case Inkscape::RENDERING_INTENT_AUTO: + default: + intent = INTENT_PERCEPTUAL; + } + + cmsHPROFILE destProf = cmsCreate_sRGBProfile(); + cmsHTRANSFORM transf = cmsCreateTransform( prof, + TYPE_RGBA_8, + destProf, + TYPE_RGBA_8, + intent, 0 ); + if ( transf ) { + guchar* currLine = px; + for ( int y = 0; y < imageheight; y++ ) { + // Since the types are the same size, we can do the transformation in-place + cmsDoTransform( transf, currLine, currLine, imagewidth ); + currLine += rowstride; + } + + cmsDeleteTransform( transf ); + } else { + DEBUG_MESSAGE( lcmsSix, "in 's sp_image_update. Unable to create LCMS transform." ); + } + + cmsCloseProfile( destProf ); + } else { + DEBUG_MESSAGE( lcmsSeven, "in 's sp_image_update. Profile type is named color. Can't transform." ); + } + } else { + DEBUG_MESSAGE( lcmsEight, "in 's sp_image_update. No profile found." ); + } + } +} +#endif // defined(HAVE_LIBLCMS2) + +void SPImage::update(SPCtx *ctx, unsigned int flags) { + + SPDocument *doc = this->document; + + SPItem::update(ctx, flags); + if (flags & SP_IMAGE_HREF_MODIFIED_FLAG) { + delete this->pixbuf; + this->pixbuf = nullptr; + if (this->href) { + Inkscape::Pixbuf *pixbuf = nullptr; + double svgdpi = 96; + if (this->getRepr()->attribute("inkscape:svg-dpi")) { + svgdpi = atof(this->getRepr()->attribute("inkscape:svg-dpi")); + } + this->dpi = svgdpi; + pixbuf = sp_image_repr_read_image(this->getRepr()->attribute("xlink:href"), + this->getRepr()->attribute("sodipodi:absref"), doc->getDocumentBase(), svgdpi); + + if (pixbuf) { +#if defined(HAVE_LIBLCMS2) + if ( this->color_profile ) apply_profile( pixbuf ); +#endif + this->pixbuf = pixbuf; + } + } + } + + SPItemCtx *ictx = (SPItemCtx *) ctx; + + // Why continue without a pixbuf? So we can display "Missing Image" png. + // Eventually, we should properly support SVG image type (i.e. render it ourselves). + if (this->pixbuf) { + if (!this->x._set) { + this->x.unit = SVGLength::PX; + this->x.computed = 0; + } + + if (!this->y._set) { + this->y.unit = SVGLength::PX; + this->y.computed = 0; + } + + if (!this->width._set) { + this->width.unit = SVGLength::PX; + this->width.computed = this->pixbuf->width(); + } + + if (!this->height._set) { + this->height.unit = SVGLength::PX; + this->height.computed = this->pixbuf->height(); + } + } + + // Calculate x, y, width, height from parent/initial viewport, see sp-root.cpp + this->calcDimsFromParentViewport(ictx); + + // Image creates a new viewport + ictx->viewport= Geom::Rect::from_xywh( this->x.computed, this->y.computed, + this->width.computed, this->height.computed); + + this->clipbox = ictx->viewport; + + this->ox = this->x.computed; + this->oy = this->y.computed; + + if (this->pixbuf) { + + // Viewbox is either from SVG (not supported) or dimensions of pixbuf (PNG, JPG) + this->viewBox = Geom::Rect::from_xywh(0, 0, this->pixbuf->width(), this->pixbuf->height()); + this->viewBox_set = true; + + // SPItemCtx rctx = + get_rctx( ictx ); + + this->ox = c2p[4]; + this->oy = c2p[5]; + this->sx = c2p[0]; + this->sy = c2p[3]; + } + + + + // TODO: eliminate ox, oy, sx, sy + + sp_image_update_canvas_image ((SPImage *) this); + + // don't crash with missing xlink:href attribute + if (!this->pixbuf) { + return; + } + + double proportion_pixbuf = this->pixbuf->height() / (double)this->pixbuf->width(); + double proportion_image = this->height.computed / (double)this->width.computed; + if (this->prev_width && + (this->prev_width != this->pixbuf->width() || this->prev_height != this->pixbuf->height())) { + if (std::abs(this->prev_width - this->pixbuf->width()) > std::abs(this->prev_height - this->pixbuf->height())) { + proportion_pixbuf = this->pixbuf->width() / (double)this->pixbuf->height(); + proportion_image = this->width.computed / (double)this->height.computed; + if (proportion_pixbuf != proportion_image) { + double new_height = this->height.computed * proportion_pixbuf; + sp_repr_set_svg_double(this->getRepr(), "width", new_height); + } + } + else { + if (proportion_pixbuf != proportion_image) { + double new_width = this->width.computed * proportion_pixbuf; + sp_repr_set_svg_double(this->getRepr(), "height", new_width); + } + } + } + this->prev_width = this->pixbuf->width(); + this->prev_height = this->pixbuf->height(); +} + +void SPImage::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingImage *img = dynamic_cast(v->arenaitem); + img->setStyle(this->style); + } + } +} + + +Inkscape::XML::Node *SPImage::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags ) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:image"); + } + + repr->setAttribute("xlink:href", this->href); + + /* fixme: Reset attribute if needed (Lauris) */ + if (this->x._set) { + sp_repr_set_svg_double(repr, "x", this->x.computed); + } + + if (this->y._set) { + sp_repr_set_svg_double(repr, "y", this->y.computed); + } + + if (this->width._set) { + sp_repr_set_svg_double(repr, "width", this->width.computed); + } + + if (this->height._set) { + sp_repr_set_svg_double(repr, "height", this->height.computed); + } + repr->setAttribute("inkscape:svg-dpi", this->getRepr()->attribute("inkscape:svg-dpi")); + //XML Tree being used directly here while it shouldn't be... + repr->setAttribute("preserveAspectRatio", this->getRepr()->attribute("preserveAspectRatio")); +#if defined(HAVE_LIBLCMS2) + if (this->color_profile) { + repr->setAttribute("color-profile", this->color_profile); + } +#endif // defined(HAVE_LIBLCMS2) + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPImage::bbox(Geom::Affine const &transform, SPItem::BBoxType /*type*/) const { + Geom::OptRect bbox; + + if ((this->width.computed > 0.0) && (this->height.computed > 0.0)) { + bbox = Geom::Rect::from_xywh(this->x.computed, this->y.computed, this->width.computed, this->height.computed); + *bbox *= transform; + } + + return bbox; +} + +void SPImage::print(SPPrintContext *ctx) { + if (this->pixbuf && (this->width.computed > 0.0) && (this->height.computed > 0.0) ) { + Inkscape::Pixbuf *pb = new Inkscape::Pixbuf(*this->pixbuf); + pb->ensurePixelFormat(Inkscape::Pixbuf::PF_GDK); + + guchar *px = pb->pixels(); + int w = pb->width(); + int h = pb->height(); + int rs = pb->rowstride(); + + double vx = this->ox; + double vy = this->oy; + + Geom::Affine t; + Geom::Translate tp(vx, vy); + Geom::Scale s(this->sx, this->sy); + t = s * tp; + ctx->image_R8G8B8A8_N(px, w, h, rs, t, this->style); + delete pb; + } +} + +const char* SPImage::displayName() const { + return _("Image"); +} + +gchar* SPImage::description() const { + char *href_desc; + + if (this->href) { + href_desc = (strncmp(this->href, "data:", 5) == 0) + ? g_strdup(_("embedded")) + : xml_quote_strdup(this->href); + } else { + g_warning("Attempting to call strncmp() with a null pointer."); + href_desc = g_strdup("(null_pointer)"); // we call g_free() on href_desc + } + + char *ret = ( this->pixbuf == nullptr + ? g_strdup_printf(_("[bad reference]: %s"), href_desc) + : g_strdup_printf(_("%d × %d: %s"), + this->pixbuf->width(), + this->pixbuf->height(), + href_desc) ); + + if (this->pixbuf == nullptr && + this->document) + { + Inkscape::Pixbuf * pb = nullptr; + double svgdpi = 96; + if (this->getRepr()->attribute("inkscape:svg-dpi")) { + svgdpi = atof(this->getRepr()->attribute("inkscape:svg-dpi")); + } + pb = sp_image_repr_read_image(this->getRepr()->attribute("xlink:href"), + this->getRepr()->attribute("sodipodi:absref"), this->document->getDocumentBase(), svgdpi); + + if (pb) { + ret = g_strdup_printf(_("%d × %d: %s"), + pb->width(), + pb->height(), + href_desc); + delete pb; + } + } + + g_free(href_desc); + return ret; +} + +Inkscape::DrawingItem* SPImage::show(Inkscape::Drawing &drawing, unsigned int /*key*/, unsigned int /*flags*/) { + Inkscape::DrawingImage *ai = new Inkscape::DrawingImage(drawing); + + sp_image_update_arenaitem(this, ai); + + return ai; +} + +static std::string broken_image_svg = R"A( + + + + + + + Linked + Image + Not Found + + +)A"; + +Inkscape::Pixbuf *sp_image_repr_read_image(gchar const *href, gchar const *absref, gchar const *base, double svgdpi) +{ + Inkscape::Pixbuf *inkpb = nullptr; + + gchar const *filename = href; + + if (filename != nullptr) { + if (g_ascii_strncasecmp(filename, "data:", 5) == 0) { + /* data URI - embedded image */ + filename += 5; + inkpb = Inkscape::Pixbuf::create_from_data_uri(filename, svgdpi); + } else { + auto url = Inkscape::URI::from_href_and_basedir(href, base); + + if (url.hasScheme("file")) { + auto native = url.toNativeFilename(); + inkpb = Inkscape::Pixbuf::create_from_file(native.c_str(), svgdpi); + } else { + try { + auto contents = url.getContents(); + inkpb = Inkscape::Pixbuf::create_from_buffer(contents, svgdpi); + } catch (const Gio::Error &e) { + g_warning("URI::getContents failed for '%.100s'", href); + } + } + } + + if (inkpb != nullptr) { + return inkpb; + } + } + + /* at last try to load from sp absolute path name */ + filename = absref; + if (filename != nullptr) { + // using absref is outside of SVG rules, so we must at least warn the user + if ( base != nullptr && href != nullptr ) { + g_warning (" did not resolve to a valid image file (base dir is %s), now trying sodipodi:absref=\"%s\"", href, base, absref); + } else { + g_warning ("xlink:href did not resolve to a valid image file, now trying sodipodi:absref=\"%s\"", absref); + } + + inkpb = Inkscape::Pixbuf::create_from_file(filename, svgdpi); + if (inkpb != nullptr) { + return inkpb; + } + } + + /* Nope: We do not find any valid pixmap file :-( */ + // Need a "fake" filename to trigger svg mode. + inkpb = Inkscape::Pixbuf::create_from_buffer(broken_image_svg, 0, "brokenimage.svg"); + + /* It's included here so if it still does not does load, */ + /* our libraries are broken! */ + g_assert (inkpb != nullptr); + + return inkpb; +} + +/* We assert that realpixbuf is either NULL or identical size to pixbuf */ +static void +sp_image_update_arenaitem (SPImage *image, Inkscape::DrawingImage *ai) +{ + ai->setStyle(SP_OBJECT(image)->style); + ai->setPixbuf(image->pixbuf); + ai->setOrigin(Geom::Point(image->ox, image->oy)); + ai->setScale(image->sx, image->sy); + ai->setClipbox(image->clipbox); +} + +static void sp_image_update_canvas_image(SPImage *image) +{ + SPItem *item = SP_ITEM(image); + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + sp_image_update_arenaitem(image, dynamic_cast(v->arenaitem)); + } +} + +void SPImage::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + /* An image doesn't have any nodes to snap, but still we want to be able snap one image + to another. Therefore we will create some snappoints at the corner, similar to a rect. If + the image is rotated, then the snappoints will rotate with it. Again, just like a rect. + */ + + if (this->getClipObject()) { + //We are looking at a clipped image: do not return any snappoints, as these might be + //far far away from the visible part from the clipped image + //TODO Do return snappoints, but only when within visual bounding box + } else { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_IMG_CORNER)) { + // The image has not been clipped: return its corners, which might be rotated for example + double const x0 = this->x.computed; + double const y0 = this->y.computed; + double const x1 = x0 + this->width.computed; + double const y1 = y0 + this->height.computed; + + Geom::Affine const i2d (this->i2dt_affine ()); + + p.emplace_back(Geom::Point(x0, y0) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x0, y1) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x1, y1) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + p.emplace_back(Geom::Point(x1, y0) * i2d, Inkscape::SNAPSOURCE_IMG_CORNER, Inkscape::SNAPTARGET_IMG_CORNER); + } + } +} + +/* + * Initially we'll do: + * Transform x, y, set x, y, clear translation + */ + +Geom::Affine SPImage::set_transform(Geom::Affine const &xform) { + /* Calculate position in parent coords. */ + Geom::Point pos( Geom::Point(this->x.computed, this->y.computed) * xform ); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + Geom::Point const scale(hypot(ret[0], ret[1]), + hypot(ret[2], ret[3])); + + if ( scale[Geom::X] > MAGIC_EPSILON ) { + ret[0] /= scale[Geom::X]; + ret[1] /= scale[Geom::X]; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if ( scale[Geom::Y] > MAGIC_EPSILON ) { + ret[2] /= scale[Geom::Y]; + ret[3] /= scale[Geom::Y]; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + this->width = this->width.computed * scale[Geom::X]; + this->height = this->height.computed * scale[Geom::Y]; + + /* Find position in item coords */ + pos = pos * ret.inverse(); + this->x = pos[Geom::X]; + this->y = pos[Geom::Y]; + + return ret; +} + +static void sp_image_set_curve( SPImage *image ) +{ + //create a curve at the image's boundary for snapping + if ((image->height.computed < MAGIC_EPSILON_TOO) || (image->width.computed < MAGIC_EPSILON_TOO) || (image->getClipObject())) { + if (image->curve) { + image->curve = image->curve->unref(); + } + } else { + Geom::OptRect rect = image->bbox(Geom::identity(), SPItem::VISUAL_BBOX); + SPCurve *c = nullptr; + + if (rect->isFinite()) { + c = SPCurve::new_from_rect(*rect, true); + } + + if (image->curve) { + image->curve = image->curve->unref(); + } + + if (c) { + image->curve = c->ref(); + + c->unref(); + } + } +} + +/** + * Return duplicate of curve (if any exists) or NULL if there is no curve + */ +SPCurve *SPImage::get_curve() const +{ + SPCurve *result = nullptr; + if (curve) { + result = curve->copy(); + } + return result; +} + +void sp_embed_image(Inkscape::XML::Node *image_node, Inkscape::Pixbuf *pb) +{ + bool free_data = false; + + // check whether the pixbuf has MIME data + guchar *data = nullptr; + gsize len = 0; + std::string data_mimetype; + + data = const_cast(pb->getMimeData(len, data_mimetype)); + + if (data == nullptr) { + // if there is no supported MIME data, embed as PNG + data_mimetype = "image/png"; + gdk_pixbuf_save_to_buffer(pb->getPixbufRaw(), reinterpret_cast(&data), &len, "png", nullptr, NULL); + free_data = true; + } + + // Save base64 encoded data in image node + // this formula taken from Glib docs + gsize needed_size = len * 4 / 3 + len * 4 / (3 * 72) + 7; + needed_size += 5 + 8 + data_mimetype.size(); // 5 bytes for data: + 8 for ;base64, + + gchar *buffer = (gchar *) g_malloc(needed_size); + gchar *buf_work = buffer; + buf_work += g_sprintf(buffer, "data:%s;base64,", data_mimetype.c_str()); + + gint state = 0; + gint save = 0; + gsize written = 0; + written += g_base64_encode_step(data, len, TRUE, buf_work, &state, &save); + written += g_base64_encode_close(TRUE, buf_work + written, &state, &save); + buf_work[written] = 0; // null terminate + + // TODO: this is very wasteful memory-wise. + // It would be better to only keep the binary data around, + // and base64 encode on the fly when saving the XML. + image_node->setAttribute("xlink:href", buffer); + + g_free(buffer); + if (free_data) g_free(data); +} + +void sp_embed_svg(Inkscape::XML::Node *image_node, std::string const &fn) +{ + if (!g_file_test(fn.c_str(), G_FILE_TEST_EXISTS)) { + return; + } + GStatBuf stdir; + int val = g_stat(fn.c_str(), &stdir); + if (val == 0 && stdir.st_mode & S_IFDIR){ + return; + } + + // we need to load the entire file into memory, + // since we'll store it as MIME data + gchar *data = nullptr; + gsize len = 0; + GError *error = nullptr; + + if (g_file_get_contents(fn.c_str(), &data, &len, &error)) { + + if (error != nullptr) { + std::cerr << "Pixbuf::create_from_file: " << error->message << std::endl; + std::cerr << " (" << fn << ")" << std::endl; + return; + } + + std::string data_mimetype = "image/svg+xml"; + + + // Save base64 encoded data in image node + // this formula taken from Glib docs + gsize needed_size = len * 4 / 3 + len * 4 / (3 * 72) + 7; + needed_size += 5 + 8 + data_mimetype.size(); // 5 bytes for data: + 8 for ;base64, + + gchar *buffer = (gchar *) g_malloc(needed_size); + gchar *buf_work = buffer; + buf_work += g_sprintf(buffer, "data:%s;base64,", data_mimetype.c_str()); + + gint state = 0; + gint save = 0; + gsize written = 0; + written += g_base64_encode_step(reinterpret_cast(data), len, TRUE, buf_work, &state, &save); + written += g_base64_encode_close(TRUE, buf_work + written, &state, &save); + buf_work[written] = 0; // null terminate + + // TODO: this is very wasteful memory-wise. + // It would be better to only keep the binary data around, + // and base64 encode on the fly when saving the XML. + image_node->setAttribute("xlink:href", buffer); + + g_free(buffer); + g_free(data); + } +} + +void SPImage::refresh_if_outdated() +{ + if ( href && pixbuf && pixbuf->modificationTime()) { + // It *might* change + + GStatBuf st; + memset(&st, 0, sizeof(st)); + int val = 0; + if (g_file_test (pixbuf->originalPath().c_str(), G_FILE_TEST_EXISTS)){ + val = g_stat(pixbuf->originalPath().c_str(), &st); + } + if ( !val ) { + // stat call worked. Check time now + if ( st.st_mtime != pixbuf->modificationTime() ) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_IMAGE_HREF_MODIFIED_FLAG); + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-image.h b/src/object/sp-image.h new file mode 100644 index 0000000..d591a49 --- /dev/null +++ b/src/object/sp-image.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG implementation + *//* + * Authors: + * Lauris Kaplinski + * Edward Flick (EAF) + * + * Copyright (C) 1999-2005 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SP_IMAGE_H +#define SEEN_INKSCAPE_SP_IMAGE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include +#include "svg/svg-length.h" +#include "display/curve.h" +#include "sp-item.h" +#include "viewbox.h" +#include "sp-dimensions.h" + +#define SP_IMAGE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_IMAGE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_IMAGE_HREF_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A + +namespace Inkscape { class Pixbuf; } +class SPImage : public SPItem, public SPViewBox, public SPDimensions { +public: + SPImage(); + ~SPImage() override; + + Geom::Rect clipbox; + double sx, sy; + double ox, oy; + double dpi; + double prev_width, prev_height; + + SPCurve *curve; // This curve is at the image's boundary for snapping + + char *href; +#if defined(HAVE_LIBLCMS2) + char *color_profile; +#endif // defined(HAVE_LIBLCMS2) + + Inkscape::Pixbuf *pixbuf; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void modified(unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + +#if defined(HAVE_LIBLCMS2) + void apply_profile(Inkscape::Pixbuf *pixbuf); +#endif // defined(HAVE_LIBLCMS2) + + SPCurve *get_curve () const; + void refresh_if_outdated(); +}; + +/* Return duplicate of curve or NULL */ +void sp_embed_image(Inkscape::XML::Node *imgnode, Inkscape::Pixbuf *pb); +void sp_embed_svg(Inkscape::XML::Node *image_node, std::string const &fn); + +#endif diff --git a/src/object/sp-item-group.cpp b/src/object/sp-item-group.cpp new file mode 100644 index 0000000..e64503c --- /dev/null +++ b/src/object/sp-item-group.cpp @@ -0,0 +1,990 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2006 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "attributes.h" +#include "document.h" +#include "document-undo.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "display/drawing-group.h" +#include "display/curve.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "svg/svg.h" +#include "svg/css-ostringstream.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" + +#include "box3d.h" +#include "persp3d.h" +#include "sp-defs.h" +#include "sp-item-transform.h" +#include "sp-root.h" +#include "sp-rect.h" +#include "sp-offset.h" +#include "sp-clippath.h" +#include "sp-mask.h" +#include "sp-path.h" +#include "sp-use.h" +#include "sp-title.h" +#include "sp-desc.h" +#include "sp-switch.h" +#include "sp-textpath.h" +#include "sp-flowtext.h" +#include "style.h" + +using Inkscape::DocumentUndo; + +static void sp_group_perform_patheffect(SPGroup *group, SPGroup *top_group, Inkscape::LivePathEffect::Effect *lpe, bool write); + +SPGroup::SPGroup() : SPLPEItem(), + _expanded(false), + _insert_bottom(false), + _layer_mode(SPGroup::GROUP) +{ +} + +SPGroup::~SPGroup() = default; + +void SPGroup::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr( "inkscape:groupmode" ); + + SPLPEItem::build(document, repr); +} + +void SPGroup::release() { + if (this->_layer_mode == SPGroup::LAYER) { + this->document->removeResource("layer", this); + } + + SPLPEItem::release(); +} + +void SPGroup::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPLPEItem::child_added(child, ref); + + SPObject *last_child = this->lastChild(); + if (last_child && last_child->getRepr() == child) { + // optimization for the common special case where the child is being added at the end + SPItem *item = dynamic_cast(last_child); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + + for (v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = item->invoke_show (v->arenaitem->drawing(), v->key, v->flags); + + if (ac) { + v->arenaitem->appendChild(ac); + } + } + } + } else { // general case + SPItem *item = dynamic_cast(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + unsigned position = item->pos_in_parent(); + + for (v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = item->invoke_show (v->arenaitem->drawing(), v->key, v->flags); + + if (ac) { + v->arenaitem->prependChild(ac); + ac->setZOrder(position); + } + } + } + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +/* fixme: hide (Lauris) */ + +void SPGroup::remove_child(Inkscape::XML::Node *child) { + SPLPEItem::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGroup::order_changed (Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPLPEItem::order_changed(child, old_ref, new_ref); + + SPItem *item = dynamic_cast(get_child_by_repr(child)); + if ( item ) { + /* TODO: this should be moved into SPItem somehow */ + SPItemView *v; + unsigned position = item->pos_in_parent(); + for ( v = item->display ; v != nullptr ; v = v->next ) { + v->arenaitem->setZOrder(position); + } + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +void SPGroup::update(SPCtx *ctx, unsigned int flags) { + // std::cout << "SPGroup::update(): " << (getId()?getId():"null") << std::endl; + SPItemCtx *ictx, cctx; + + ictx = (SPItemCtx *) ctx; + cctx = *ictx; + + unsigned childflags = flags; + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l=this->childList(true, SPObject::ActionUpdate); + for(auto child : l){ + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + SPItem *item = dynamic_cast(child); + if (item) { + cctx.i2doc = item->transform * ictx->i2doc; + cctx.i2vp = item->transform * ictx->i2vp; + child->updateDisplay((SPCtx *)&cctx, childflags); + } else { + child->updateDisplay(ctx, childflags); + } + } + + sp_object_unref(child); + } + + // For a group, we need to update ourselves *after* updating children. + // this is because the group might contain shapes such as rect or ellipse, + // which recompute their equivalent path (a.k.a curve) in the update callback, + // and this is in turn used when computing bbox. + SPLPEItem::update(ctx, flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *group = dynamic_cast(v->arenaitem); + if( this->parent ) { + this->context_style = this->parent->context_style; + } + group->setStyle(this->style, this->context_style); + } + } +} + +void SPGroup::modified(guint flags) { + //std::cout << "SPGroup::modified(): " << (getId()?getId():"null") << std::endl; + SPLPEItem::modified(flags); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *group = dynamic_cast(v->arenaitem); + group->setStyle(this->style); + } + } + + std::vector l=this->childList(true); + for(auto child : l){ + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPGroup::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vector l; + + if (!repr) { + if (dynamic_cast(this)) { + repr = xml_doc->createElement("svg:switch"); + } else { + repr = xml_doc->createElement("svg:g"); + } + } + + for (auto& child: children) { + if ( !dynamic_cast(&child) && !dynamic_cast(&child) ) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( !dynamic_cast(&child) && !dynamic_cast(&child) ) { + child.updateRepr(flags); + } + } + } + + if ( flags & SP_OBJECT_WRITE_EXT ) { + const char *value; + if ( _layer_mode == SPGroup::LAYER ) { + value = "layer"; + } else if ( _layer_mode == SPGroup::MASK_HELPER ) { + value = "maskhelper"; + } else if ( flags & SP_OBJECT_WRITE_ALL ) { + value = "group"; + } else { + value = nullptr; + } + + repr->setAttribute("inkscape:groupmode", value); + } + + SPLPEItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPGroup::bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const +{ + Geom::OptRect bbox; + + // TODO CPPIFY: replace this const_cast later + std::vector l = const_cast(this)->childList(false, SPObject::ActionBBox); + for(auto o : l){ + SPItem *item = dynamic_cast(o); + if (item && !item->isHidden()) { + Geom::Affine const ct(item->transform * transform); + bbox |= item->bounds(bboxtype, ct); + } + } + + return bbox; +} + +void SPGroup::print(SPPrintContext *ctx) { + for(auto& child: children){ + SPObject *o = &child; + SPItem *item = dynamic_cast(o); + if (item) { + item->invoke_print(ctx); + } + } +} + +const char *SPGroup::displayName() const { + return _("Group"); +} + +gchar *SPGroup::description() const { + gint len = this->getItemCount(); + return g_strdup_printf( + ngettext(_("of %d object"), _("of %d objects"), len), len); +} + +void SPGroup::set(SPAttributeEnum key, gchar const* value) { + switch (key) { + case SP_ATTR_INKSCAPE_GROUPMODE: + if ( value && !strcmp(value, "layer") ) { + this->setLayerMode(SPGroup::LAYER); + } else if ( value && !strcmp(value, "maskhelper") ) { + this->setLayerMode(SPGroup::MASK_HELPER); + } else { + this->setLayerMode(SPGroup::GROUP); + } + break; + + default: + SPLPEItem::set(key, value); + break; + } +} + +Inkscape::DrawingItem *SPGroup::show (Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + // std::cout << "SPGroup::show(): " << (getId()?getId():"null") << std::endl; + Inkscape::DrawingGroup *ai; + + ai = new Inkscape::DrawingGroup(drawing); + ai->setPickChildren(this->effectiveLayerMode(key) == SPGroup::LAYER); + if( this->parent ) { + this->context_style = this->parent->context_style; + } + ai->setStyle(this->style, this->context_style); + + this->_showChildren(drawing, ai, key, flags); + return ai; +} + +void SPGroup::hide (unsigned int key) { + std::vector l=this->childList(false, SPObject::ActionShow); + for(auto o : l){ + SPItem *item = dynamic_cast(o); + if (item) { + item->invoke_hide(key); + } + } + +// SPLPEItem::onHide(key); +} + + +void SPGroup::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + for (auto& o: children) + { + SPItem const *item = dynamic_cast(&o); + if (item) { + item->getSnappoints(p, snapprefs); + } + } +} + +void sp_item_group_ungroup_handle_clones(SPItem *parent, Geom::Affine const g) +{ + for(std::list::const_iterator refd=parent->hrefList.begin();refd!=parent->hrefList.end();++refd){ + SPItem *citem = dynamic_cast(*refd); + if (citem && !citem->cloned) { + SPUse *useitem = dynamic_cast(citem); + if (useitem && useitem->get_original() == parent) { + Geom::Affine ctrans; + ctrans = g.inverse() * citem->transform; + gchar *affinestr = sp_svg_transform_write(ctrans); + citem->setAttribute("transform", affinestr); + g_free(affinestr); + } + } + } +} + +void +sp_recursive_scale_text_size(Inkscape::XML::Node *repr, double scale){ + for (Inkscape::XML::Node *child = repr->firstChild() ; child; child = child->next() ){ + if ( child) { + sp_recursive_scale_text_size(child, scale); + } + } + SPCSSAttr * css = sp_repr_css_attr(repr,"style"); + Glib::ustring element = g_quark_to_string(repr->code()); + if ((css && element == "svg:text") || element == "svg:tspan") { + gchar const *w = sp_repr_css_property(css, "font-size", nullptr); + if (w) { + gchar *units = nullptr; + double wd = g_ascii_strtod(w, &units); + wd *= scale; + if (w != units) { + Inkscape::CSSOStringStream os; + os << wd << units; // reattach units + sp_repr_css_set_property(css, "font-size", os.str().c_str()); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + repr->setAttributeOrRemoveIfEmpty("style", css_str); + } + } + w = nullptr; + w = sp_repr_css_property(css, "letter-spacing", nullptr); + if (w) { + gchar *units = nullptr; + double wd = g_ascii_strtod(w, &units); + wd *= scale; + if (w != units) { + Inkscape::CSSOStringStream os; + os << wd << units; // reattach units + sp_repr_css_set_property(css, "letter-spacing", os.str().c_str()); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + repr->setAttributeOrRemoveIfEmpty("style", css_str); + } + } + w = nullptr; + w = sp_repr_css_property(css, "word-spacing", nullptr); + if (w) { + gchar *units = nullptr; + double wd = g_ascii_strtod(w, &units); + wd *= scale; + if (w != units) { + Inkscape::CSSOStringStream os; + os << wd << units; // reattach units + sp_repr_css_set_property(css, "word-spacing", os.str().c_str()); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + repr->setAttributeOrRemoveIfEmpty("style", css_str); + } + } + gchar const *dx = repr->attribute("dx"); + if (dx) { + gchar ** dxarray = g_strsplit(dx, " ", 0); + Inkscape::SVGOStringStream dx_data; + while (*dxarray != nullptr) { + double pos; + sp_svg_number_read_d(*dxarray, &pos); + pos *= scale; + dx_data << pos << " "; + dxarray++; + } + repr->setAttribute("dx", dx_data.str()); + } + gchar const *dy = repr->attribute("dy"); + if (dy) { + gchar ** dyarray = g_strsplit(dy, " ", 0); + Inkscape::SVGOStringStream dy_data; + while (*dyarray != nullptr) { + double pos; + sp_svg_number_read_d(*dyarray, &pos); + pos *= scale; + dy_data << pos << " "; + dyarray++; + } + repr->setAttribute("dy", dy_data.str()); + } + } +} + +void +sp_item_group_ungroup (SPGroup *group, std::vector &children, bool do_done) +{ + g_return_if_fail (group != nullptr); + + SPDocument *doc = group->document; + SPRoot *root = doc->getRoot(); + SPObject *defs = root->defs; + + Inkscape::XML::Node *grepr = group->getRepr(); + + g_return_if_fail (!strcmp (grepr->name(), "svg:g") + || !strcmp (grepr->name(), "svg:a") + || !strcmp (grepr->name(), "svg:switch") + || !strcmp (grepr->name(), "svg:svg")); + + // this converts the gradient/pattern fill/stroke on the group, if any, to userSpaceOnUse + group->adjust_paint_recursive(Geom::identity(), Geom::identity()); + + SPItem *pitem = dynamic_cast(group->parent); + g_assert(pitem); + Inkscape::XML::Node *prepr = pitem->getRepr(); + + { + SPBox3D *box = dynamic_cast(group); + if (box) { + group = box3d_convert_to_group(box); + } + } + + group->removeAllPathEffects(false); + + /* Step 1 - generate lists of children objects */ + std::vector items; + std::vector objects; + Geom::Affine const g(group->transform); + + for (auto& child: group->children) { + if (SPItem *citem = dynamic_cast(&child)) { + sp_item_group_ungroup_handle_clones(citem, g); + } + } + + for (auto& child: group->children) { + SPItem *citem = dynamic_cast(&child); + if (citem) { + /* Merging of style */ + // this converts the gradient/pattern fill/stroke, if any, to userSpaceOnUse; we need to do + // it here _before_ the new transform is set, so as to use the pre-transform bbox + citem->adjust_paint_recursive(Geom::identity(), Geom::identity()); + + child.style->merge( group->style ); + /* + * fixme: We currently make no allowance for the case where child is cloned + * and the group has any style settings. + * + * (This should never occur with documents created solely with the current + * version of inkscape without using the XML editor: we usually apply group + * style changes to children rather than to the group itself.) + * + * If the group has no style settings, then style->merge() should be a no-op. Otherwise + * (i.e. if we change the child's style to compensate for its parent going away) + * then those changes will typically be reflected in any clones of child, + * whereas we'd prefer for Ungroup not to affect the visual appearance. + * + * The only way of preserving styling appearance in general is for child to + * be put into a new group -- a somewhat surprising response to an Ungroup + * command. We could add a new groupmode:transparent that would mostly + * hide the existence of such groups from the user (i.e. editing behaves as + * if the transparent group's children weren't in a group), though that's + * extra complication & maintenance burden and this case is rare. + */ + + child.updateRepr(); + + Inkscape::XML::Node *nrepr = child.getRepr()->duplicate(prepr->document()); + + // Merging transform + Geom::Affine ctrans = citem->transform * g; + // We should not apply the group's transformation to both a linked offset AND to its source + if (dynamic_cast(citem)) { // Do we have an offset at hand (whether it's dynamic or linked)? + SPItem *source = sp_offset_get_source(dynamic_cast(citem)); + // When dealing with a chain of linked offsets, the transformation of an offset will be + // tied to the transformation of the top-most source, not to any of the intermediate + // offsets. So let's find the top-most source + while (source != nullptr && dynamic_cast(source)) { + source = sp_offset_get_source(dynamic_cast(source)); + } + if (source != nullptr && // If true then we must be dealing with a linked offset ... + group->isAncestorOf(source) ) { // ... of which the source is in the same group + ctrans = citem->transform; // then we should apply the transformation of the group to the offset + } + } + + // FIXME: constructing a transform that would fully preserve the appearance of a + // textpath if it is ungrouped with its path seems to be impossible in general + // case. E.g. if the group was squeezed, to keep the ungrouped textpath squeezed + // as well, we'll need to relink it to some "virtual" path which is inversely + // stretched relative to the actual path, and then squeeze the textpath back so it + // would both fit the actual path _and_ be squeezed as before. It's a bummer. + + // This is just a way to temporarily remember the transform in repr. When repr is + // reattached outside of the group, the transform will be written more properly + // (i.e. optimized into the object if the corresponding preference is set) + gchar *affinestr=sp_svg_transform_write(ctrans); + SPText * text = dynamic_cast(citem); + if (text) { + //this causes a change in text-on-path appearance when there is a non-conformal transform, see bug #1594565 + SPTextPath * text_path = dynamic_cast(text->firstChild()); + if (!text_path) { + nrepr->setAttribute("transform", affinestr); + } else { + // The following breaks roundtripping group -> ungroup + // double scale = (ctrans.expansionX() + ctrans.expansionY()) / 2.0; + // sp_recursive_scale_text_size(nrepr, scale); + Geom::Affine ttrans = ctrans.inverse() * SP_ITEM(text)->transform * ctrans; + gchar *affinestr = sp_svg_transform_write(ttrans); + nrepr->setAttribute("transform", affinestr); + g_free(affinestr); + } + } else { + nrepr->setAttribute("transform", affinestr); + } + g_free(affinestr); + + items.push_back(nrepr); + + } else { + Inkscape::XML::Node *nrepr = child.getRepr()->duplicate(prepr->document()); + objects.push_back(nrepr); + } + } + + /* Step 2 - clear group */ + // remember the position of the group + gint pos = group->getRepr()->position(); + + // the group is leaving forever, no heir, clones should take note; its children however are going to reemerge + group->deleteObject(true, false); + + /* Step 3 - add nonitems */ + if (!objects.empty()) { + Inkscape::XML::Node *last_def = defs->getRepr()->lastChild(); + for (auto i=objects.rbegin();i!=objects.rend();++i) { + Inkscape::XML::Node *repr = *i; + if (!sp_repr_is_meta_element(repr)) { + defs->getRepr()->addChild(repr, last_def); + } + Inkscape::GC::release(repr); + } + } + + /* Step 4 - add items */ + for (auto i=items.rbegin();i!=items.rend();++i) { + Inkscape::XML::Node *repr = *i; + // add item + // restore position; since the items list was prepended (i.e. reverse), we now add + // all children at the same pos, which inverts the order once again + prepr->addChildAtPos(repr, pos); + + // fill in the children list if non-null + SPItem *item = static_cast(doc->getObjectByRepr(repr)); + + if (item) { + item->doWriteTransform(item->transform, nullptr, false); + children.insert(children.begin(),item); + item->requestModified(SP_OBJECT_MODIFIED_FLAG); + } else { + g_assert_not_reached(); + } + + Inkscape::GC::release(repr); + } + if (do_done) { + DocumentUndo::done(doc, SP_VERB_NONE, _("Ungroup")); + } +} + +/* + * some API for list aspect of SPGroup + */ + +std::vector sp_item_group_item_list(SPGroup * group) +{ + std::vector s; + g_return_val_if_fail(group != nullptr, s); + + for (auto& o: group->children) { + if ( dynamic_cast(&o) ) { + s.push_back((SPItem*)&o); + } + } + return s; +} + +SPObject *sp_item_group_get_child_by_name(SPGroup *group, SPObject *ref, const gchar *name) +{ + SPObject *child = (ref) ? ref->getNext() : group->firstChild(); + while ( child && strcmp(child->getRepr()->name(), name) ) { + child = child->getNext(); + } + return child; +} + +void SPGroup::setLayerMode(LayerMode mode) { + if ( _layer_mode != mode ) { + if ( mode == LAYER ) { + this->document->addResource("layer", this); + } else if ( _layer_mode == LAYER ) { + this->document->removeResource("layer", this); + } + _layer_mode = mode; + _updateLayerMode(); + } +} + +SPGroup::LayerMode SPGroup::layerDisplayMode(unsigned int dkey) const { + std::map::const_iterator iter; + iter = _display_modes.find(dkey); + if ( iter != _display_modes.end() ) { + return (*iter).second; + } else { + return GROUP; + } +} + +void SPGroup::setExpanded(bool isexpanded) { + if ( _expanded != isexpanded ){ + _expanded = isexpanded; + } +} + +void SPGroup::setInsertBottom(bool insertbottom) { + if ( _insert_bottom != insertbottom) { + _insert_bottom = insertbottom; + } +} + +void SPGroup::setLayerDisplayMode(unsigned int dkey, SPGroup::LayerMode mode) { + if ( layerDisplayMode(dkey) != mode ) { + _display_modes[dkey] = mode; + _updateLayerMode(dkey); + } +} + +void SPGroup::_updateLayerMode(unsigned int display_key) { + SPItemView *view; + for ( view = this->display ; view ; view = view->next ) { + if ( !display_key || view->key == display_key ) { + Inkscape::DrawingGroup *g = dynamic_cast(view->arenaitem); + if (g) { + g->setPickChildren(effectiveLayerMode(view->key) == SPGroup::LAYER); + } + } + } +} + +void SPGroup::translateChildItems(Geom::Translate const &tr) +{ + if ( hasChildren() ) { + for (auto& o: children) { + SPItem *item = dynamic_cast(&o); + if ( item ) { + item->move_rel(tr); + } + } + } +} + +// Recursively (or not) scale child items around a point +void SPGroup::scaleChildItemsRec(Geom::Scale const &sc, Geom::Point const &p, bool noRecurse) +{ + if ( hasChildren() ) { + for (auto& o: children) { + if ( SPDefs *defs = dynamic_cast(&o) ) { // select symbols from defs, ignore clips, masks, patterns + for (auto& defschild: defs->children) { + SPGroup *defsgroup = dynamic_cast(&defschild); + if (defsgroup) + defsgroup->scaleChildItemsRec(sc, p, false); + } + } else if ( SPItem *item = dynamic_cast(&o) ) { + SPGroup *group = dynamic_cast(item); + if (group && !dynamic_cast(item)) { + /* Using recursion breaks clipping because transforms are applied + in coordinates for draws but nothing in defs is changed + instead change the transform on the entire group, and the transform + is applied after any references to clipping paths. However NOT using + recursion apparently breaks as of r13544 other parts of Inkscape + involved with showing/modifying units. So offer both for use + in different contexts. + */ + if(noRecurse) { + // used for EMF import + Geom::Translate const s(p); + Geom::Affine final = s.inverse() * sc * s; + Geom::Affine tAff = item->i2dt_affine() * final; + item->set_i2d_affine(tAff); + tAff = item->transform; + // Eliminate common rounding error affecting EMF/WMF input. + // When the rounding error persists it converts the simple + // transform=scale() to transform=matrix(). + if(std::abs(tAff[4]) < 1.0e-5 && std::abs(tAff[5]) < 1.0e-5){ + tAff[4] = 0.0; + tAff[5] = 0.0; + } + item->doWriteTransform(tAff, nullptr, true); + } else { + // used for other import + SPItem *sub_item = nullptr; + if (item->getClipObject()) { + sub_item = dynamic_cast(item->getClipObject()->firstChild()); + } + if (sub_item != nullptr) { + sub_item->doWriteTransform(sub_item->transform*sc, nullptr, true); + } + sub_item = nullptr; + if (item->getMaskObject()) { + sub_item = dynamic_cast(item->getMaskObject()->firstChild()); + } + if (sub_item != nullptr) { + sub_item->doWriteTransform(sub_item->transform*sc, nullptr, true); + } + item->doWriteTransform(sc.inverse()*item->transform*sc, nullptr, true); + group->scaleChildItemsRec(sc, p, false); + } + } else { +// Geom::OptRect bbox = item->desktopVisualBounds(); +// if (bbox) { // test not needed, this was causing a failure to scale and in the clipboard, see LP Bug 1365451 + // Scale item + Geom::Translate const s(p); + Geom::Affine final = s.inverse() * sc * s; + + gchar const *conn_type = nullptr; + SPText *text_item = dynamic_cast(item); + bool is_text_path = text_item && text_item->firstChild() && dynamic_cast(text_item->firstChild()); + if (is_text_path) { + text_item->optimizeTextpathText(); + } else { + SPFlowtext *flowText = dynamic_cast(item); + if (flowText) { + flowText->optimizeScaledText(); + } else { + SPBox3D *box = dynamic_cast(item); + if (box) { + // Force recalculation from perspective + box3d_position_set(box); + } else if (item->getAttribute("inkscape:connector-type") != nullptr + && (item->getAttribute("inkscape:connection-start") == nullptr + || item->getAttribute("inkscape:connection-end") == nullptr)) { + // Remove and store connector type for transform if disconnected + conn_type = item->getAttribute("inkscape:connector-type"); + item->removeAttribute("inkscape:connector-type"); + } + } + } + + Persp3D *persp = dynamic_cast(item); + if (persp) { + persp3d_apply_affine_transformation(persp, final); + } else if (is_text_path && !item->transform.isIdentity()) { + // Save and reset current transform + Geom::Affine tmp(item->transform); + item->transform = Geom::Affine(); + // Apply scale + item->set_i2d_affine(item->i2dt_affine() * sc); + item->doWriteTransform(item->transform, nullptr, true); + // Scale translation and restore original transform + tmp[4] *= sc[0]; + tmp[5] *= sc[1]; + item->doWriteTransform(tmp, nullptr, true); + } else if (dynamic_cast(item)) { + // calculate the matrix we need to apply to the clone + // to cancel its induced transform from its original + Geom::Affine move = final.inverse() * item->transform * final; + item->doWriteTransform(move, &move, true); + } else { + item->doWriteTransform(item->transform*sc, nullptr, true); + } + + if (conn_type != nullptr) { + item->setAttribute("inkscape:connector-type", conn_type); + } + + if (item->isCenterSet() && !(final.isTranslation() || final.isIdentity())) { + item->scaleCenter(sc); // All coordinates have been scaled, so also the center must be scaled + item->updateRepr(); + } +// } + } + } + } + } +} + +gint SPGroup::getItemCount() const { + gint len = 0; + for (auto& child: children) { + if (dynamic_cast(&child)) { + len++; + } + } + + return len; +} + +void SPGroup::_showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) { + Inkscape::DrawingItem *ac = nullptr; + std::vector l=this->childList(false, SPObject::ActionShow); + for(auto o : l){ + SPItem * child = dynamic_cast(o); + if (child) { + ac = child->invoke_show (drawing, key, flags); + if (ac) { + ai->appendChild(ac); + } + } + } +} + +void SPGroup::update_patheffect(bool write) { +#ifdef GROUP_VERBOSE + g_message("sp_group_update_patheffect: %p\n", lpeitem); +#endif + std::vector const item_list = sp_item_group_item_list(this); + + for (auto sub_item : item_list) { + if (sub_item) { + SPLPEItem *lpe_item = dynamic_cast(sub_item); + if (lpe_item) { + lpe_item->update_patheffect(write); + } + } + } + + this->resetClipPathAndMaskLPE(); + if (hasPathEffect() && pathEffectsEnabled()) { + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + lpeobj->get_lpe()->doBeforeEffect_impl(this); + sp_group_perform_patheffect(this, this, lpe, write); + lpeobj->get_lpe()->doAfterEffect_impl(this); + } + } + } + } +} + +static void +sp_group_perform_patheffect(SPGroup *group, SPGroup *top_group, Inkscape::LivePathEffect::Effect *lpe, bool write) +{ + std::vector const item_list = sp_item_group_item_list(group); + for (auto sub_item : item_list) { + SPGroup *sub_group = dynamic_cast(sub_item); + if (sub_group) { + sp_group_perform_patheffect(sub_group, top_group, lpe, write); + } else { + SPShape* sub_shape = dynamic_cast(sub_item); + //SPPath* sub_path = dynamic_cast(sub_item); + SPItem* clipmaskto = dynamic_cast(sub_item); + if (clipmaskto) { + top_group->applyToClipPath(clipmaskto, lpe); + top_group->applyToMask(clipmaskto, lpe); + } + if (sub_shape) { + SPCurve * c = sub_shape->getCurve(); + bool success = false; + // only run LPEs when the shape has a curve defined + if (c) { + lpe->pathvector_before_effect = c->get_pathvector(); + c->transform(i2anc_affine(sub_shape, top_group)); + sub_shape->setCurveInsync(c); + if (lpe->lpeversion.param_getSVGValue() != "0") { // we are on 1 or up + sub_shape->bbox_vis_cache_is_valid = false; + sub_shape->bbox_geom_cache_is_valid = false; + } + success = top_group->performOnePathEffect(c, sub_shape, lpe); + c->transform(i2anc_affine(sub_shape, top_group).inverse()); + Inkscape::XML::Node *repr = sub_item->getRepr(); + if (c && success) { + sub_shape->setCurveInsync(c); + lpe->pathvector_after_effect = c->get_pathvector(); + if (write) { + gchar *str = sp_svg_write_path(lpe->pathvector_after_effect); + repr->setAttribute("d", str); +#ifdef GROUP_VERBOSE + g_message("sp_group_perform_patheffect writes 'd' attribute"); +#endif + g_free(str); + } + c->unref(); + } else { + // LPE was unsuccessful or doeffect stack return null. Read the old 'd'-attribute. + if (gchar const * value = repr->attribute("d")) { + Geom::PathVector pv = sp_svg_read_pathv(value); + SPCurve *oldcurve = new (std::nothrow) SPCurve(pv); + if (oldcurve) { + sub_shape->setCurve(oldcurve); + oldcurve->unref(); + } + } + } + } + } + } + } + SPItem* clipmaskto = dynamic_cast(group); + if (clipmaskto) { + top_group->applyToClipPath(clipmaskto, lpe); + top_group->applyToMask(clipmaskto, lpe); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-item-group.h b/src/object/sp-item-group.h new file mode 100644 index 0000000..5f9b187 --- /dev/null +++ b/src/object/sp-item-group.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ITEM_GROUP_H +#define SEEN_SP_ITEM_GROUP_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "sp-lpe-item.h" + +#define SP_GROUP(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_GROUP(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#define SP_IS_LAYER(obj) (SP_IS_GROUP(obj) && SP_GROUP(obj)->layerMode() == SPGroup::LAYER) + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + +class SPGroup : public SPLPEItem { +public: + SPGroup(); + ~SPGroup() override; + + enum LayerMode { GROUP, LAYER, MASK_HELPER }; + + bool _expanded; + bool _insert_bottom; + LayerMode _layer_mode; + std::map _display_modes; + + LayerMode layerMode() const { return _layer_mode; } + void setLayerMode(LayerMode mode); + + bool expanded() const { return _expanded; } + void setExpanded(bool isexpanded); + + bool insertBottom() const { return _insert_bottom; } + void setInsertBottom(bool insertbottom); + + LayerMode effectiveLayerMode(unsigned int display_key) const { + if ( _layer_mode == LAYER ) { + return LAYER; + } else { + return layerDisplayMode(display_key); + } + } + + LayerMode layerDisplayMode(unsigned int display_key) const; + void setLayerDisplayMode(unsigned int display_key, LayerMode mode); + void translateChildItems(Geom::Translate const &tr); + void scaleChildItemsRec(Geom::Scale const &sc, Geom::Point const &p, bool noRecurse); + + int getItemCount() const; + virtual void _showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags); + +private: + void _updateLayerMode(unsigned int display_key=0); + +public: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + + void update(SPCtx *ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + void set(SPAttributeEnum key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const override; + void print(SPPrintContext *ctx) override; + const char* displayName() const override; + char *description() const override; + Inkscape::DrawingItem *show (Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide (unsigned int key) override; + + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; + + void update_patheffect(bool write) override; +}; + + +/** + * finds clones of a child of the group going out of the group; and inverse the group transform on its clones + * Also called when moving objects between different layers + * @param group current group + * @param parent original parent + * @param g transform + */ +void sp_item_group_ungroup_handle_clones(SPItem *parent, Geom::Affine const g); + +void sp_item_group_ungroup (SPGroup *group, std::vector &children, bool do_done = true); + + +std::vector sp_item_group_item_list (SPGroup *group); + +SPObject *sp_item_group_get_child_by_name (SPGroup *group, SPObject *ref, const char *name); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-item-rm-unsatisfied-cns.cpp b/src/object/sp-item-rm-unsatisfied-cns.cpp new file mode 100644 index 0000000..3b476e2 --- /dev/null +++ b/src/object/sp-item-rm-unsatisfied-cns.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/coord.h> +#include + +#include "remove-last.h" +#include "sp-guide.h" +#include "sp-item-rm-unsatisfied-cns.h" + +void sp_item_rm_unsatisfied_cns(SPItem &item) +{ + if (item.constraints.empty()) { + return; + } + std::vector snappoints; + item.getSnappoints(snappoints, nullptr); + for (unsigned i = item.constraints.size(); i--;) { + g_assert( i < item.constraints.size() ); + SPGuideConstraint const &cn = item.constraints[i]; + int const snappoint_ix = cn.snappoint_ix; + g_assert( snappoint_ix < int(snappoints.size()) ); + + if (!Geom::are_near(cn.g->getDistanceFrom(snappoints[snappoint_ix].getPoint()), 0, 1e-2)) { + + remove_last(cn.g->attached_items, SPGuideAttachment(&item, cn.snappoint_ix)); + + g_assert( i < item.constraints.size() ); + + item.constraints.erase(item.constraints.begin() + i); + } + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item-rm-unsatisfied-cns.h b/src/object/sp-item-rm-unsatisfied-cns.h new file mode 100644 index 0000000..ac03b74 --- /dev/null +++ b/src/object/sp-item-rm-unsatisfied-cns.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_RM_UNSATISFIED_CNS_H +#define SEEN_SP_ITEM_RM_UNSATISFIED_CNS_H + +class SPItem; + +void sp_item_rm_unsatisfied_cns(SPItem &item); + + +#endif // SEEN_SP_ITEM_RM_UNSATISFIED_CNS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item-transform.cpp b/src/object/sp-item-transform.cpp new file mode 100644 index 0000000..e01571e --- /dev/null +++ b/src/object/sp-item-transform.cpp @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Transforming single items + * + * Authors: + * Lauris Kaplinski + * Frank Felfe + * bulia byak + * Johan Engelen + * Abhishek Sharma + * Diederik van Lierop + * + * Copyright (C) 1999-2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> +#include "sp-item.h" +#include "sp-item-transform.h" + +#include + +/** + * Calculate the affine transformation required to transform one visual bounding box into another, accounting for a uniform strokewidth. + * + * PS: This function will only return accurate results for the visual bounding box of a selection of one or more objects, all having + * the same strokewidth. If the stroke width varies from object to object in this selection, then the function + * get_scale_transform_for_variable_stroke() should be called instead + * + * When scaling or stretching an object using the selector, e.g. by dragging the handles or by entering a value, we will + * need to calculate the affine transformation for the old dimensions to the new dimensions. When using a geometric bounding + * box this is very straightforward, but when using a visual bounding box this become more tricky as we need to account for + * the strokewidth, which is either constant or scales width the area of the object. This function takes care of the calculation + * of the affine transformation: + * @param bbox_visual Current visual bounding box + * @param stroke_x Apparent strokewidth in horizontal direction + * @param stroke_y Apparent strokewidth in vertical direction + * @param transform_stroke If true then the stroke will be scaled proportional to the square root of the area of the geometric bounding box + * @param preserve If true then the transform element will be preserved in XML, and evaluated after stroke is applied + * @param x0 Coordinate of the target visual bounding box + * @param y0 Coordinate of the target visual bounding box + * @param x1 Coordinate of the target visual bounding box + * @param y1 Coordinate of the target visual bounding box + * PS: we have to pass each coordinate individually, to find out if we are mirroring the object; Using a Geom::Rect() instead is + * not possible here because it will only allow for a positive width and height, and therefore cannot mirror + * @return + */ +Geom::Affine get_scale_transform_for_uniform_stroke(Geom::Rect const &bbox_visual, gdouble stroke_x, gdouble stroke_y, bool transform_stroke, bool preserve, gdouble x0, gdouble y0, gdouble x1, gdouble y1) +{ + Geom::Affine p2o = Geom::Translate (-bbox_visual.min()); + Geom::Affine o2n = Geom::Translate (x0, y0); + + Geom::Affine scale = Geom::Scale (1, 1); + Geom::Affine unbudge = Geom::Translate (0, 0); // moves the object(s) to compensate for the drift caused by stroke width change + + // 1) We start with a visual bounding box (w0, h0) which we want to transfer into another visual bounding box (w1, h1) + // 2) The stroke is r0, equal for all edges, if preserve transforms is false + // 3) Given this visual bounding box we can calculate the geometric bounding box by subtracting half the stroke from each side; + // -> The width and height of the geometric bounding box will therefore be (w0 - 2*0.5*r0) and (h0 - 2*0.5*r0) + // 4) If preserve transforms is true, then stroke_x != stroke_y, since these are the apparent stroke widths, after transforming + + if ((stroke_x == Geom::infinity()) || (fabs(stroke_x) < 1e-6)) stroke_x = 0; + if ((stroke_y == Geom::infinity()) || (fabs(stroke_y) < 1e-6)) stroke_y = 0; + + gdouble w0 = bbox_visual.width(); // will return a value >= 0, as required further down the road + gdouble h0 = bbox_visual.height(); + + // We also know the width and height of the new visual bounding box + gdouble w1 = x1 - x0; // can have any sign + gdouble h1 = y1 - y0; + // The new visual bounding box will have a stroke r1 + + // Here starts the calculation you've been waiting for; first do some preparation + int flip_x = (w1 > 0) ? 1 : -1; + int flip_y = (h1 > 0) ? 1 : -1; + + // w1 and h1 will be negative when mirroring, but if so then e.g. w1-r0 won't make sense + // Therefore we will use the absolute values from this point on + w1 = fabs(w1); + h1 = fabs(h1); + // w0 and h0 will always be positive due to the definition of the width() and height() methods. + + // Check whether the stroke is negative; i.e. the geometric bounding box is larger than the visual bounding box, which + // occurs for example for clipped objects (see launchpad bug #811819) + if (stroke_x < 0 || stroke_y < 0) { + Geom::Affine direct = Geom::Scale(flip_x * w1 / w0, flip_y* h1 / h0); // Scaling of the visual bounding box + // How should we handle the stroke width scaling of clipped object? I don't know if we can/should handle this, + // so for now we simply return the direct scaling + return (p2o * direct * o2n); + } + gdouble r0 = sqrt(stroke_x*stroke_y); // r0 is redundant, used only for those cases where stroke_x = stroke_y + + // We will now try to calculate the affine transformation required to transform the first visual bounding box into + // the second one, while accounting for strokewidth + + if ((fabs(w0 - stroke_x) < 1e-6) && (fabs(h0 - stroke_y) < 1e-6)) { + return Geom::Affine(); + } + + gdouble scale_x = 1; + gdouble scale_y = 1; + gdouble r1; + + if ((fabs(w0 - stroke_x) < 1e-6) || w1 == 0) { // We have a vertical line at hand + scale_y = h1/h0; + scale_x = transform_stroke ? 1 : scale_y; + unbudge *= Geom::Translate (-flip_x * 0.5 * (scale_x - 1.0) * w0, 0); + unbudge *= Geom::Translate ( flip_x * 0.5 * (w1 - w0), 0); // compensate for the fact that this operation cannot be performed + } else if ((fabs(h0 - stroke_y) < 1e-6) || h1 == 0) { // We have a horizontal line at hand + scale_x = w1/w0; + scale_y = transform_stroke ? 1 : scale_x; + unbudge *= Geom::Translate (0, -flip_y * 0.5 * (scale_y - 1.0) * h0); + unbudge *= Geom::Translate (0, flip_y * 0.5 * (h1 - h0)); // compensate for the fact that this operation cannot be performed + } else { // We have a true 2D object at hand + if (transform_stroke && !preserve) { + /* Initial area of the geometric bounding box: A0 = (w0-r0)*(h0-r0) + * Desired area of the geometric bounding box: A1 = (w1-r1)*(h1-r1) + * This is how the stroke should scale: r1^2 / A1 = r0^2 / A0 + * So therefore we will need to solve this equation: + * + * r1^2 * (w0-r0) * (h0-r0) = r0^2 * (w1-r1) * (h1-r1) + * + * This is a quadratic equation in r1, of which the roots can be found using the ABC formula + * */ + gdouble A = -w0*h0 + r0*(w0 + h0); + gdouble B = -(w1 + h1) * r0*r0; + gdouble C = w1 * h1 * r0*r0; + if (B*B - 4*A*C < 0) { + g_message("stroke scaling error : %d, %f, %f, %f, %f, %f", preserve, r0, w0, h0, w1, h1); + } else { + r1 = -C/B; + if (!Geom::are_near(A*C/B/B, 0.0, Geom::EPSILON)) + r1 = fabs((-B - sqrt(B*B - 4*A*C))/(2*A)); + // If w1 < 0 then the scale will be wrong if we just assume that scale_x = (w1 - r1)/(w0 - r0); + // Therefore we here need the absolute values of w0, w1, h0, h1, and r0, as taken care of earlier + scale_x = (w1 - r1)/(w0 - r0); + scale_y = (h1 - r1)/(h0 - r0); + // Make sure that the lower-left corner of the visual bounding box stays where it is, even though the stroke width has changed + unbudge *= Geom::Translate (-flip_x * 0.5 * (r0 * scale_x - r1), -flip_y * 0.5 * (r0 * scale_y - r1)); + } + } else if (!transform_stroke && !preserve) { // scale the geometric bbox with constant stroke + scale_x = (w1 - r0) / (w0 - r0); + scale_y = (h1 - r0) / (h0 - r0); + unbudge *= Geom::Translate (-flip_x * 0.5 * r0 * (scale_x - 1), -flip_y * 0.5 * r0 * (scale_y - 1)); + } else if (!transform_stroke) { // 'Preserve Transforms' was chosen. + // geometric mean of stroke_x and stroke_y will be preserved + // new_stroke_x = stroke_x*sqrt(scale_x/scale_y) + // new_stroke_y = stroke_y*sqrt(scale_y/scale_x) + // scale_x = (w1 - new_stroke_x)/(w0 - stroke_x) + // scale_y = (h1 - new_stroke_y)/(h0 - stroke_y) + gdouble A = h1*(w0 - stroke_x); + gdouble B = (h0*stroke_x - w0*stroke_y); + gdouble C = -w1*(h0 - stroke_y); + gdouble Sx_div_Sy; // Sx_div_Sy = sqrt(scale_x/scale_y) + if (B*B - 4*A*C < 0) { + g_message("stroke scaling error : %d, %f, %f, %f, %f, %f, %f", preserve, stroke_x, stroke_y, w0, h0, w1, h1); + } else { + Sx_div_Sy = (-B + sqrt(B*B - 4*A*C))/2/A; + scale_x = (w1 - stroke_x*Sx_div_Sy)/(w0 - stroke_x); + scale_y = (h1 - stroke_y/Sx_div_Sy)/(h0 - stroke_y); + unbudge *= Geom::Translate (-flip_x * 0.5 * stroke_x * scale_x * (1.0 - sqrt(1.0/scale_x/scale_y)), -flip_y * 0.5 * stroke_y * scale_y * (1.0 - sqrt(1.0/scale_x/scale_y))); + } + } else { // 'Preserve Transforms' was chosen, and stroke is scaled + scale_x = w1 / w0; + scale_y = h1 / h0; + } + } + + // Now we account for mirroring by flipping if needed + scale *= Geom::Scale(flip_x * scale_x, flip_y * scale_y); + + return (p2o * scale * unbudge * o2n); +} + +/** + * Calculate the affine transformation required to transform one visual bounding box into another, accounting for a VARIABLE strokewidth. + * + * Note: Please try to understand get_scale_transform_for_uniform_stroke() first, and read all it's comments carefully. This function + * (get_scale_transform_for_variable_stroke) is a bit different because it will allow for a strokewidth that's different for each + * side of the visual bounding box. Such a situation will arise when transforming the visual bounding box of a selection of objects, + * each having a different stroke width. In fact this function is a generalized version of get_scale_transform_for_uniform_stroke(), but + * will not (yet) replace it because it has not been tested as carefully, and because the old function is can serve as an introduction to + * understand the new one. + * + * When scaling or stretching an object using the selector, e.g. by dragging the handles or by entering a value, we will + * need to calculate the affine transformation for the old dimensions to the new dimensions. When using a geometric bounding + * box this is very straightforward, but when using a visual bounding box this become more tricky as we need to account for + * the strokewidth, which is either constant or scales width the area of the object. This function takes care of the calculation + * of the affine transformation: + * + * @param bbox_visual Current visual bounding box + * @param bbox_geometric Current geometric bounding box (allows for calculating the strokewidth of each edge) + * @param transform_stroke If true then the stroke will be scaled proportional to the square root of the area of the geometric bounding box + * @param preserve If true then the transform element will be preserved in XML, and evaluated after stroke is applied + * @param x0 Coordinate of the target visual bounding box + * @param y0 Coordinate of the target visual bounding box + * @param x1 Coordinate of the target visual bounding box + * @param y1 Coordinate of the target visual bounding box + * PS: we have to pass each coordinate individually, to find out if we are mirroring the object; Using a Geom::Rect() instead is + * not possible here because it will only allow for a positive width and height, and therefore cannot mirror + * @return + */ +Geom::Affine get_scale_transform_for_variable_stroke(Geom::Rect const &bbox_visual, Geom::Rect const &bbox_geom, bool transform_stroke, bool preserve, gdouble x0, gdouble y0, gdouble x1, gdouble y1) +{ + Geom::Affine p2o = Geom::Translate (-bbox_visual.min()); + Geom::Affine o2n = Geom::Translate (x0, y0); + + Geom::Affine scale = Geom::Scale (1, 1); + Geom::Affine unbudge = Geom::Translate (0, 0); // moves the object(s) to compensate for the drift caused by stroke width change + + // 1) We start with a visual bounding box (w0, h0) which we want to transfer into another visual bounding box (w1, h1) + // 2) We will also know the geometric bounding box, which can be used to calculate the strokewidth. The strokewidth will however + // be different for each of the four sides (left/right/top/bottom: r0l, r0r, r0t, r0b) + + gdouble w0 = bbox_visual.width(); // will return a value >= 0, as required further down the road + gdouble h0 = bbox_visual.height(); + + // We also know the width and height of the new visual bounding box + gdouble w1 = x1 - x0; // can have any sign + gdouble h1 = y1 - y0; + // The new visual bounding box will have strokes r1l, r1r, r1t, and r1b + + // We will now try to calculate the affine transformation required to transform the first visual bounding box into + // the second one, while accounting for strokewidth + gdouble r0w = w0 - bbox_geom.width(); // r0w is the average strokewidth of the left and right edges, i.e. 0.5*(r0l + r0r) + gdouble r0h = h0 - bbox_geom.height(); // r0h is the average strokewidth of the top and bottom edges, i.e. 0.5*(r0t + r0b) + if ((r0w == Geom::infinity()) || (fabs(r0w) < 1e-6)) r0w = 0; + if ((r0h == Geom::infinity()) || (fabs(r0h) < 1e-6)) r0h = 0; + + int flip_x = (w1 > 0) ? 1 : -1; + int flip_y = (h1 > 0) ? 1 : -1; + + // w1 and h1 will be negative when mirroring, but if so then e.g. w1-r0 won't make sense + // Therefore we will use the absolute values from this point on + w1 = fabs(w1); + h1 = fabs(h1); + // w0 and h0 will always be positive due to the definition of the width() and height() methods. + + if ((fabs(w0 - r0w) < 1e-6) && (fabs(h0 - r0h) < 1e-6)) { + return Geom::Affine(); + } + + // Check whether the stroke is negative; i.e. the geometric bounding box is larger than the visual bounding box, which + // occurs for example for clipped objects (see launchpad bug #811819) + if (r0w < 0 || r0h < 0) { + Geom::Affine direct = Geom::Scale(flip_x * w1 / w0, flip_y* h1 / h0); // Scaling of the visual bounding box + // How should we handle the stroke width scaling of clipped object? I don't know if we can/should handle this, + // so for now we simply return the direct scaling + return (p2o * direct * o2n); + } + + // The calculation of the new strokewidth will only use the average stroke for each of the dimensions; To find the new stroke for each + // of the edges individually though, we will use the boundary condition that the ratio of the left/right strokewidth will not change due to the + // scaling. The same holds for the ratio of the top/bottom strokewidth. + gdouble stroke_ratio_w = fabs(r0w) < 1e-6 ? 1 : (bbox_geom[Geom::X].min() - bbox_visual[Geom::X].min())/r0w; + gdouble stroke_ratio_h = fabs(r0h) < 1e-6 ? 1 : (bbox_geom[Geom::Y].min() - bbox_visual[Geom::Y].min())/r0h; + + gdouble scale_x = 1; + gdouble scale_y = 1; + gdouble r1h; + gdouble r1w; + + if ((fabs(w0 - r0w) < 1e-6) || w1 == 0) { // We have a vertical line at hand + scale_y = h1/h0; + scale_x = transform_stroke ? 1 : scale_y; + unbudge *= Geom::Translate (-flip_x * 0.5 * (scale_x - 1.0) * w0, 0); + unbudge *= Geom::Translate ( flip_x * 0.5 * (w1 - w0), 0); // compensate for the fact that this operation cannot be performed + } else if ((fabs(h0 - r0h) < 1e-6) || h1 == 0) { // We have a horizontal line at hand + scale_x = w1/w0; + scale_y = transform_stroke ? 1 : scale_x; + unbudge *= Geom::Translate (0, -flip_y * 0.5 * (scale_y - 1.0) * h0); + unbudge *= Geom::Translate (0, flip_y * 0.5 * (h1 - h0)); // compensate for the fact that this operation cannot be performed + } else { // We have a true 2D object at hand + if (transform_stroke && !preserve) { + /* Initial area of the geometric bounding box: A0 = (w0-r0w)*(h0-r0h) + * Desired area of the geometric bounding box: A1 = (w1-r1w)*(h1-r1h) + * This is how the stroke should scale: r1w^2 = A1/A0 * r0w^2, AND + * r1h^2 = A1/A0 * r0h^2 + * These can be re-expressed as : r1w/r0w = r1h/r0h + * and : r1w*r1w*(w0 - r0w)*(h0 - r0h) = r0w*r0w*(w1 - r1w)*(h1 - r1h) + * This leads to a quadratic equation in r1w, solved as follows: + * */ + + gdouble A = w0*h0 - r0h*w0 - r0w*h0; + gdouble B = r0h*w1 + r0w*h1; + gdouble C = -w1*h1; + + if (B*B - 4*A*C < 0) { + g_message("variable stroke scaling error : %d, %d, %f, %f, %f, %f, %f, %f", transform_stroke, preserve, r0w, r0h, w0, h0, w1, h1); + } else { + gdouble det = -C/B; + if (!Geom::are_near(A*C/B/B, 0.0, Geom::EPSILON)) + det = (-B + sqrt(B*B - 4*A*C))/(2*A); + r1w = r0w*det; + r1h = r0h*det; + // If w1 < 0 then the scale will be wrong if we just assume that scale_x = (w1 - r1)/(w0 - r0); + // Therefore we here need the absolute values of w0, w1, h0, h1, and r0, as taken care of earlier + scale_x = (w1 - r1w)/(w0 - r0w); + scale_y = (h1 - r1h)/(h0 - r0h); + // Make sure that the lower-left corner of the visual bounding box stays where it is, even though the stroke width has changed + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * (r0w * scale_x - r1w), -flip_y * stroke_ratio_h * (r0h * scale_y - r1h)); + } + } else if (!transform_stroke && !preserve) { // scale the geometric bbox with constant stroke + scale_x = (w1 - r0w) / (w0 - r0w); + scale_y = (h1 - r0h) / (h0 - r0h); + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * r0w * (scale_x - 1), -flip_y * stroke_ratio_h * r0h * (scale_y - 1)); + } else if (!transform_stroke) { // 'Preserve Transforms' was chosen. + // geometric mean of r0w and r0h will be preserved + // new_r0w = r0w*sqrt(scale_x/scale_y) + // new_r0h = r0h*sqrt(scale_y/scale_x) + // scale_x = (w1 - new_r0w)/(w0 - r0w) + // scale_y = (h1 - new_r0h)/(h0 - r0h) + gdouble A = h1*(w0 - r0w); + gdouble B = (h0*r0w - w0*r0h); + gdouble C = -w1*(h0 - r0h); + gdouble Sx_div_Sy; // Sx_div_Sy = sqrt(scale_x/scale_y) + if (B*B - 4*A*C < 0) { + g_message("variable stroke scaling error : %d, %d, %f, %f, %f, %f, %f, %f", transform_stroke, preserve, r0w, r0h, w0, h0, w1, h1); + } else { + Sx_div_Sy = (-B + sqrt(B*B - 4*A*C))/2/A; + scale_x = (w1 - r0w*Sx_div_Sy)/(w0 - r0w); + scale_y = (h1 - r0h/Sx_div_Sy)/(h0 - r0h); + unbudge *= Geom::Translate (-flip_x * stroke_ratio_w * r0w * scale_x * (1.0 - sqrt(1.0/scale_x/scale_y)), -flip_y * stroke_ratio_h * r0h * scale_y * (1.0 - sqrt(1.0/scale_x/scale_y))); + } + } else { // 'Preserve Transforms' was chosen, and stroke is scaled + scale_x = w1 / w0; + scale_y = h1 / h0; + } + } + + // Now we account for mirroring by flipping if needed + scale *= Geom::Scale(flip_x * scale_x, flip_y * scale_y); + + return (p2o * scale * unbudge * o2n); +} + +Geom::Rect get_visual_bbox(Geom::OptRect const &initial_geom_bbox, Geom::Affine const &abs_affine, gdouble const initial_strokewidth, bool const transform_stroke) +{ + g_assert(initial_geom_bbox); + + // Find the new geometric bounding box; Do this by transforming each corner of + // the initial geometric bounding box individually and fitting a new boundingbox + // around the transformerd corners + Geom::Point const p0 = Geom::Point(initial_geom_bbox->corner(0)) * abs_affine; + Geom::Rect new_geom_bbox(p0, p0); + for (unsigned i = 1 ; i < 4 ; i++) { + new_geom_bbox.expandTo(Geom::Point(initial_geom_bbox->corner(i)) * abs_affine); + } + + Geom::Rect new_visual_bbox = new_geom_bbox; + if (initial_strokewidth > 0 && initial_strokewidth < Geom::infinity()) { + if (transform_stroke) { + // scale stroke by: sqrt (((w1-r0)/(w0-r0))*((h1-r0)/(h0-r0))) (for visual bboxes, see get_scale_transform_for_stroke) + // equals scaling by: sqrt ((w1/w0)*(h1/h0)) for geometrical bboxes + // equals scaling by: sqrt (area1/area0) for geometrical bboxes + gdouble const new_strokewidth = initial_strokewidth * sqrt (new_geom_bbox.area() / initial_geom_bbox->area()); + new_visual_bbox.expandBy(0.5 * new_strokewidth); + } else { + // Do not transform the stroke + new_visual_bbox.expandBy(0.5 * initial_strokewidth); + } + } + + return new_visual_bbox; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item-transform.h b/src/object/sp-item-transform.h new file mode 100644 index 0000000..4c74014 --- /dev/null +++ b/src/object/sp-item-transform.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_TRANSFORM_H +#define SEEN_SP_ITEM_TRANSFORM_H + +#include <2geom/forward.h> + +class SPItem; + +Geom::Affine get_scale_transform_for_uniform_stroke (Geom::Rect const &bbox_visual, double stroke_x, double stroke_y, bool transform_stroke, bool preserve, double x0, double y0, double x1, double y1); +Geom::Affine get_scale_transform_for_variable_stroke (Geom::Rect const &bbox_visual, Geom::Rect const &bbox_geom, bool transform_stroke, bool preserve, double x0, double y0, double x1, double y1); +Geom::Rect get_visual_bbox (Geom::OptRect const &initial_geom_bbox, Geom::Affine const &abs_affine, double const initial_strokewidth, bool const transform_stroke); + + +#endif // SEEN_SP_ITEM_TRANSFORM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item-update-cns.cpp b/src/object/sp-item-update-cns.cpp new file mode 100644 index 0000000..516fb67 --- /dev/null +++ b/src/object/sp-item-update-cns.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "satisfied-guide-cns.h" + +#include "sp-item-update-cns.h" +#include "sp-guide.h" + +void sp_item_update_cns(SPItem &item, SPDesktop const &desktop) +{ + std::vector snappoints; + item.getSnappoints(snappoints, nullptr); + /* TODO: Implement the ordering. */ + std::vector found_cns; + satisfied_guide_cns(desktop, snappoints, found_cns); + /* effic: It might be nice to avoid an n^2 algorithm, but in practice n will be + small enough that it's still usually more efficient. */ + + for (auto cn : found_cns) + { + if ( std::find(item.constraints.begin(), + item.constraints.end(), + cn) + == item.constraints.end() ) + { + item.constraints.push_back(cn); + cn.g->attached_items.emplace_back(&item, cn.snappoint_ix); + } + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item-update-cns.h b/src/object/sp-item-update-cns.h new file mode 100644 index 0000000..3ff0d62 --- /dev/null +++ b/src/object/sp-item-update-cns.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_UPDATE_CNS_H +#define SEEN_SP_ITEM_UPDATE_CNS_H + +#include <2geom/forward.h> + +class SPDesktop; +class SPItem; + +void sp_item_update_cns(SPItem &item, SPDesktop const &desktop); + + +#endif // SEEN_SP_ITEM_UPDATE_CNS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-item.cpp b/src/object/sp-item.cpp new file mode 100644 index 0000000..8c8720d --- /dev/null +++ b/src/object/sp-item.cpp @@ -0,0 +1,1827 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2001-2006 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item.h" + +#include + +#include "bad-uri-exception.h" +#include "svg/svg.h" +#include "print.h" +#include "display/drawing-item.h" +#include "attributes.h" +#include "document.h" + +#include "inkscape.h" +#include "desktop.h" +#include "gradient-chemistry.h" +#include "conn-avoid-ref.h" +#include "conditions.h" +#include "filter-chemistry.h" + +#include "sp-clippath.h" +#include "sp-desc.h" +#include "sp-guide.h" +#include "sp-hatch.h" +#include "sp-item-rm-unsatisfied-cns.h" +#include "sp-mask.h" +#include "sp-pattern.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "sp-switch.h" +#include "sp-text.h" +#include "sp-textpath.h" +#include "sp-title.h" +#include "sp-use.h" + +#include "style.h" +#include "uri.h" + + +#include "util/find-last-if.h" + +#include "extract-uri.h" + +#include "live_effects/lpeobject.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject-reference.h" + +#include "util/units.h" + +#define noSP_ITEM_DEBUG_IDLE + +//#define OBJECT_TRACE + +static SPItemView* sp_item_view_list_remove(SPItemView *list, + SPItemView *view); + + +SPItem::SPItem() : SPObject() { + + sensitive = TRUE; + bbox_valid = FALSE; + + _highlightColor = nullptr; + + transform_center_x = 0; + transform_center_y = 0; + + freeze_stroke_width = false; + _is_evaluated = true; + _evaluated_status = StatusUnknown; + + transform = Geom::identity(); + // doc_bbox = Geom::OptRect(); + + display = nullptr; + + clip_ref = nullptr; + mask_ref = nullptr; + + style->signal_fill_ps_changed.connect(sigc::bind(sigc::ptr_fun(fill_ps_ref_changed), this)); + style->signal_stroke_ps_changed.connect(sigc::bind(sigc::ptr_fun(stroke_ps_ref_changed), this)); + + avoidRef = nullptr; +} + +SPItem::~SPItem() = default; + +SPClipPath *SPItem::getClipObject() const { return clip_ref ? clip_ref->getObject() : nullptr; } + +SPMask *SPItem::getMaskObject() const { return mask_ref ? mask_ref->getObject() : nullptr; } + +SPMaskReference &SPItem::getMaskRef() +{ + if (!mask_ref) { + mask_ref = new SPMaskReference(this); + mask_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(mask_ref_changed), this)); + } + + return *mask_ref; +} + +SPClipPathReference &SPItem::getClipRef() +{ + if (!clip_ref) { + clip_ref = new SPClipPathReference(this); + clip_ref->changedSignal().connect(sigc::bind(sigc::ptr_fun(clip_ref_changed), this)); + } + + return *clip_ref; +} + +SPAvoidRef &SPItem::getAvoidRef() +{ + if (!avoidRef) { + avoidRef = new SPAvoidRef(this); + } + return *avoidRef; +} + +bool SPItem::isVisibleAndUnlocked() const { + return (!isHidden() && !isLocked()); +} + +bool SPItem::isVisibleAndUnlocked(unsigned display_key) const { + return (!isHidden(display_key) && !isLocked()); +} + +bool SPItem::isLocked() const { + for (SPObject const *o = this; o != nullptr; o = o->parent) { + SPItem const *item = dynamic_cast(o); + if (item && !(item->sensitive)) { + return true; + } + } + return false; +} + +void SPItem::setLocked(bool locked) { + setAttribute("sodipodi:insensitive", + ( locked ? "1" : nullptr )); + updateRepr(); + document->_emitModified(); +} + +bool SPItem::isHidden() const { + if (!isEvaluated()) + return true; + return style->display.computed == SP_CSS_DISPLAY_NONE; +} + +void SPItem::setHidden(bool hide) { + style->display.set = TRUE; + style->display.value = ( hide ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE ); + style->display.computed = style->display.value; + style->display.inherit = FALSE; + updateRepr(); +} + +bool SPItem::isHidden(unsigned display_key) const { + if (!isEvaluated()) + return true; + for ( SPItemView *view(display) ; view ; view = view->next ) { + if ( view->key == display_key ) { + g_assert(view->arenaitem != nullptr); + for ( Inkscape::DrawingItem *arenaitem = view->arenaitem ; + arenaitem ; arenaitem = arenaitem->parent() ) + { + if (!arenaitem->visible()) { + return true; + } + } + return false; + } + } + return true; +} + +bool SPItem::isHighlightSet() const { + return _highlightColor != nullptr; +} + +guint32 SPItem::highlight_color() const { + if (_highlightColor) + { + return atoi(_highlightColor) | 0x00000000; + } + else { + SPItem const *item = dynamic_cast(parent); + if (parent && (parent != this) && item) + { + return item->highlight_color(); + } + else + { + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff); + } + } +} + +void SPItem::setEvaluated(bool evaluated) { + _is_evaluated = evaluated; + _evaluated_status = StatusSet; +} + +void SPItem::resetEvaluated() { + if ( StatusCalculated == _evaluated_status ) { + _evaluated_status = StatusUnknown; + bool oldValue = _is_evaluated; + if ( oldValue != isEvaluated() ) { + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } if ( StatusSet == _evaluated_status ) { + SPSwitch *switchItem = dynamic_cast(parent); + if (switchItem) { + switchItem->resetChildEvaluated(); + } + } +} + +bool SPItem::isEvaluated() const { + if ( StatusUnknown == _evaluated_status ) { + _is_evaluated = sp_item_evaluate(this); + _evaluated_status = StatusCalculated; + } + return _is_evaluated; +} + +bool SPItem::isExplicitlyHidden() const +{ + return (style->display.set + && style->display.value == SP_CSS_DISPLAY_NONE); +} + +void SPItem::setExplicitlyHidden(bool val) { + style->display.set = val; + style->display.value = ( val ? SP_CSS_DISPLAY_NONE : SP_CSS_DISPLAY_INLINE ); + style->display.computed = style->display.value; + updateRepr(); +} + +void SPItem::setCenter(Geom::Point const &object_centre) { + document->ensureUpToDate(); + + // Copied from DocumentProperties::onDocUnitChange() + gdouble viewscale = 1.0; + Geom::Rect vb = this->document->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = this->document->getWidth().value("px") / vb.width(); + gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + + // FIXME this is seriously wrong + Geom::OptRect bbox = desktopGeometricBounds(); + if (bbox) { + // object centre is document coordinates (i.e. in pixels), so we need to consider the viewbox + // to translate to user units; transform_center_x/y is in user units + transform_center_x = (object_centre[Geom::X] - bbox->midpoint()[Geom::X])/viewscale; + if (Geom::are_near(transform_center_x, 0)) // rounding error + transform_center_x = 0; + transform_center_y = (object_centre[Geom::Y] - bbox->midpoint()[Geom::Y])/viewscale; + if (Geom::are_near(transform_center_y, 0)) // rounding error + transform_center_y = 0; + } +} + +void +SPItem::unsetCenter() { + transform_center_x = 0; + transform_center_y = 0; +} + +bool SPItem::isCenterSet() const { + return (transform_center_x != 0 || transform_center_y != 0); +} + +// Get the item's transformation center in desktop coordinates (i.e. in pixels) +Geom::Point SPItem::getCenter() const { + document->ensureUpToDate(); + + // Copied from DocumentProperties::onDocUnitChange() + gdouble viewscale = 1.0; + Geom::Rect vb = this->document->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = this->document->getWidth().value("px") / vb.width(); + gdouble viewscale_h = this->document->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + + // FIXME this is seriously wrong + Geom::OptRect bbox = desktopGeometricBounds(); + if (bbox) { + // transform_center_x/y are stored in user units, so we have to take the viewbox into account to translate to document coordinates + return bbox->midpoint() + Geom::Point (transform_center_x*viewscale, transform_center_y*viewscale); + + } else { + return Geom::Point(0, 0); // something's wrong! + } + +} + +void +SPItem::scaleCenter(Geom::Scale const &sc) { + transform_center_x *= sc[Geom::X]; + transform_center_y *= sc[Geom::Y]; +} + +namespace { + +bool is_item(SPObject const &object) { + return dynamic_cast(&object) != nullptr; +} + +} + +void SPItem::raiseToTop() { + using Inkscape::Algorithms::find_last_if; + + auto topmost = find_last_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item); + if (topmost != parent->children.end()) { + getRepr()->parent()->changeOrder( getRepr(), topmost->getRepr() ); + } +} + +bool SPItem::raiseOne() { + auto next_higher = std::find_if(++parent->children.iterator_to(*this), parent->children.end(), &is_item); + if (next_higher != parent->children.end()) { + Inkscape::XML::Node *ref = next_higher->getRepr(); + getRepr()->parent()->changeOrder(getRepr(), ref); + return true; + } + return false; +} + +bool SPItem::lowerOne() { + using Inkscape::Algorithms::find_last_if; + + auto next_lower = find_last_if(parent->children.begin(), parent->children.iterator_to(*this), &is_item); + if (next_lower != parent->children.iterator_to(*this)) { + Inkscape::XML::Node *ref = nullptr; + if (next_lower != parent->children.begin()) { + next_lower--; + ref = next_lower->getRepr(); + } + getRepr()->parent()->changeOrder(getRepr(), ref); + return true; + } + return false; +} + +void SPItem::lowerToBottom() { + auto bottom = std::find_if(parent->children.begin(), parent->children.iterator_to(*this), &is_item); + if (bottom != parent->children.iterator_to(*this)) { + Inkscape::XML::Node *ref = nullptr; + if (bottom != parent->children.begin()) { + bottom--; + ref = bottom->getRepr(); + } + parent->getRepr()->changeOrder(getRepr(), ref); + } +} + +void SPItem::moveTo(SPItem *target, bool intoafter) { + + Inkscape::XML::Node *target_ref = ( target ? target->getRepr() : nullptr ); + Inkscape::XML::Node *our_ref = getRepr(); + + if (!target_ref) { + // Assume move to the "first" in the top node, find the top node + intoafter = false; + SPObject* bottom = this->document->getObjectByRepr(our_ref->root())->firstChild(); + while(!dynamic_cast(bottom->getNext())){ + bottom = bottom->getNext(); + } + target_ref = bottom->getRepr(); + } + + if (target_ref == our_ref) { + // Move to ourself ignore + return; + } + + if (intoafter) { + // Move this inside of the target at the end + our_ref->parent()->removeChild(our_ref); + target_ref->addChild(our_ref, nullptr); + } else if (target_ref->parent() != our_ref->parent()) { + // Change in parent, need to remove and add + our_ref->parent()->removeChild(our_ref); + target_ref->parent()->addChild(our_ref, target_ref); + } else { + // Same parent, just move + our_ref->parent()->changeOrder(our_ref, target_ref); + } +} + +void SPItem::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem* object = this; + + object->readAttr( "style" ); + object->readAttr( "transform" ); + object->readAttr( "clip-path" ); + object->readAttr( "mask" ); + object->readAttr( "sodipodi:insensitive" ); + object->readAttr( "inkscape:transform-center-x" ); + object->readAttr( "inkscape:transform-center-y" ); + object->readAttr( "inkscape:connector-avoid" ); + object->readAttr( "inkscape:connection-points" ); + object->readAttr( "inkscape:highlight-color" ); + + SPObject::build(document, repr); +} + +void SPItem::release() { + SPItem* item = this; + + // Note: do this here before the clip_ref is deleted, since calling + // ensureUpToDate() for triggered routing may reference + // the deleted clip_ref. + delete item->avoidRef; + + // we do NOT disconnect from the changed signal of those before deletion. + // The destructor will call *_ref_changed with NULL as the new value, + // which will cause the hide() function to be called. + delete item->clip_ref; + delete item->mask_ref; + + SPObject::release(); + + SPPaintServer *fill_ps = style->getFillPaintServer(); + SPPaintServer *stroke_ps = style->getStrokePaintServer(); + while (item->display) { + if (fill_ps) { + fill_ps->hide(item->display->arenaitem->key()); + } + if (stroke_ps) { + stroke_ps->hide(item->display->arenaitem->key()); + } + item->display = sp_item_view_list_remove(item->display, item->display); + } + + //item->_transformed_signal.~signal(); +} + +void SPItem::set(SPAttributeEnum key, gchar const* value) { + SPItem *item = this; + SPItem* object = item; + + switch (key) { + case SP_ATTR_TRANSFORM: { + Geom::Affine t; + if (value && sp_svg_transform_read(value, &t)) { + item->set_item_transform(t); + } else { + item->set_item_transform(Geom::identity()); + } + break; + } + case SP_PROP_CLIP_PATH: { + auto uri = extract_uri(value); + if (!uri.empty() || item->clip_ref) { + item->getClipRef().try_attach(uri.c_str()); + } + break; + } + case SP_PROP_MASK: { + auto uri = extract_uri(value); + if (!uri.empty() || item->mask_ref) { + item->getMaskRef().try_attach(uri.c_str()); + } + break; + } + case SP_ATTR_SODIPODI_INSENSITIVE: + { + item->sensitive = !value; + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + v->arenaitem->setSensitive(item->sensitive); + } + break; + } + case SP_ATTR_INKSCAPE_HIGHLIGHT_COLOR: + { + g_free(item->_highlightColor); + if (value) { + item->_highlightColor = g_strdup(value); + } else { + item->_highlightColor = nullptr; + } + break; + } + case SP_ATTR_CONNECTOR_AVOID: + if (value || item->avoidRef) { + item->getAvoidRef().setAvoid(value); + } + break; + case SP_ATTR_TRANSFORM_CENTER_X: + if (value) { + item->transform_center_x = g_strtod(value, nullptr); + } else { + item->transform_center_x = 0; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_TRANSFORM_CENTER_Y: + if (value) { + item->transform_center_y = g_strtod(value, nullptr); + item->transform_center_y *= -document->yaxisdir(); + } else { + item->transform_center_y = 0; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_PROP_SYSTEM_LANGUAGE: + case SP_PROP_REQUIRED_FEATURES: + case SP_PROP_REQUIRED_EXTENSIONS: + { + item->resetEvaluated(); + // pass to default handler + } + default: + if (SP_ATTRIBUTE_IS_CSS(key)) { + style->clear(key); + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } else { + SPObject::set(key, value); + } + break; + } +} + +void SPItem::clip_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item) +{ + item->bbox_valid = FALSE; // force a re-evaluation + if (old_clip) { + SPItemView *v; + /* Hide clippath */ + for (v = item->display; v != nullptr; v = v->next) { + SPClipPath *oldPath = dynamic_cast(old_clip); + g_assert(oldPath != nullptr); + oldPath->hide(v->arenaitem->key()); + } + } + SPClipPath *clipPath = dynamic_cast(clip); + if (clipPath) { + Geom::OptRect bbox = item->geometricBounds(); + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + if (!v->arenaitem->key()) { + v->arenaitem->setKey(SPItem::display_key_new(3)); + } + Inkscape::DrawingItem *ai = clipPath->show( + v->arenaitem->drawing(), + v->arenaitem->key()); + v->arenaitem->setClip(ai); + clipPath->setBBox(v->arenaitem->key(), bbox); + clip->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPItem::mask_ref_changed(SPObject *old_mask, SPObject *mask, SPItem *item) +{ + item->bbox_valid = FALSE; // force a re-evaluation + if (old_mask) { + /* Hide mask */ + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + SPMask *maskItem = dynamic_cast(old_mask); + g_assert(maskItem != nullptr); + maskItem->sp_mask_hide(v->arenaitem->key()); + } + } + SPMask *maskItem = dynamic_cast(mask); + if (maskItem) { + Geom::OptRect bbox = item->geometricBounds(); + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + if (!v->arenaitem->key()) { + v->arenaitem->setKey(SPItem::display_key_new(3)); + } + Inkscape::DrawingItem *ai = maskItem->sp_mask_show( + v->arenaitem->drawing(), + v->arenaitem->key()); + v->arenaitem->setMask(ai); + maskItem->sp_mask_set_bbox(v->arenaitem->key(), bbox); + mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } +} + +void SPItem::fill_ps_ref_changed(SPObject *old_ps, SPObject *ps, SPItem *item) { + SPPaintServer *old_fill_ps = dynamic_cast(old_ps); + if (old_fill_ps) { + for (SPItemView *v =item->display; v != nullptr; v = v->next) { + old_fill_ps->hide(v->arenaitem->key()); + } + } + + SPPaintServer *new_fill_ps = dynamic_cast(ps); + if (new_fill_ps) { + Geom::OptRect bbox = item->geometricBounds(); + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + if (!v->arenaitem->key()) { + v->arenaitem->setKey(SPItem::display_key_new(3)); + } + Inkscape::DrawingPattern *pi = new_fill_ps->show( + v->arenaitem->drawing(), v->arenaitem->key(), bbox); + v->arenaitem->setFillPattern(pi); + if (pi) { + new_fill_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } +} + +void SPItem::stroke_ps_ref_changed(SPObject *old_ps, SPObject *ps, SPItem *item) { + SPPaintServer *old_stroke_ps = dynamic_cast(old_ps); + if (old_stroke_ps) { + for (SPItemView *v =item->display; v != nullptr; v = v->next) { + old_stroke_ps->hide(v->arenaitem->key()); + } + } + + SPPaintServer *new_stroke_ps = dynamic_cast(ps); + if (new_stroke_ps) { + Geom::OptRect bbox = item->geometricBounds(); + for (SPItemView *v = item->display; v != nullptr; v = v->next) { + if (!v->arenaitem->key()) { + v->arenaitem->setKey(SPItem::display_key_new(3)); + } + Inkscape::DrawingPattern *pi = new_stroke_ps->show( + v->arenaitem->drawing(), v->arenaitem->key(), bbox); + v->arenaitem->setStrokePattern(pi); + if (pi) { + new_stroke_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } +} + +void SPItem::update(SPCtx* ctx, guint flags) { + + SPItemCtx const *ictx = reinterpret_cast(ctx); + + // Any of the modifications defined in sp-object.h might change bbox, + // so we invalidate it unconditionally + bbox_valid = FALSE; + + viewport = ictx->viewport; // Cache viewport + + if (flags & (SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + for (SPItemView *v = display; v != nullptr; v = v->next) { + v->arenaitem->setTransform(transform); + } + } + + SPClipPath *clip_path = clip_ref ? clip_ref->getObject() : nullptr; + SPMask *mask = mask_ref ? mask_ref->getObject() : nullptr; + + if ( clip_path || mask ) { + Geom::OptRect bbox = geometricBounds(); + if (clip_path) { + for (SPItemView *v = display; v != nullptr; v = v->next) { + clip_path->setBBox(v->arenaitem->key(), bbox); + } + } + if (mask) { + for (SPItemView *v = display; v != nullptr; v = v->next) { + mask->sp_mask_set_bbox(v->arenaitem->key(), bbox); + } + } + } + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = display; v != nullptr; v = v->next) { + v->arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value)); + v->arenaitem->setAntialiasing(style->shape_rendering.computed == SP_CSS_SHAPE_RENDERING_CRISPEDGES ? 0 : 2); + v->arenaitem->setIsolation( style->isolation.value ); + v->arenaitem->setBlendMode( style->mix_blend_mode.value ); + v->arenaitem->setVisible(!isHidden()); + } + } + } + /* Update bounding box in user space, used for filter and objectBoundingBox units */ + if (style->filter.set && display) { + Geom::OptRect item_bbox = geometricBounds(); + SPItemView *itemview = display; + do { + if (itemview->arenaitem) + itemview->arenaitem->setItemBounds(item_bbox); + } while ( (itemview = itemview->next) ); + } + + // Update libavoid with item geometry (for connector routing). + if (avoidRef && document) { + avoidRef->handleSettingChange(); + } +} + +void SPItem::modified(unsigned int /*flags*/) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPItem::modified" ); + objectTrace( "SPItem::modified", false ); +#endif +} + +Inkscape::XML::Node* SPItem::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPItem *item = this; + SPItem* object = item; + + // in the case of SP_OBJECT_WRITE_BUILD, the item should always be newly created, + // so we need to add any children from the underlying object to the new repr + if (flags & SP_OBJECT_WRITE_BUILD) { + std::vectorl; + for (auto& child: object->children) { + if (dynamic_cast(&child) || dynamic_cast(&child)) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + if (crepr) { + l.push_back(crepr); + } + } + } + for (auto i = l.rbegin(); i!= l.rend(); ++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: object->children) { + if (dynamic_cast(&child) || dynamic_cast(&child)) { + child.updateRepr(flags); + } + } + } + + gchar *c = sp_svg_transform_write(item->transform); + repr->setAttribute("transform", c); + g_free(c); + + if (flags & SP_OBJECT_WRITE_EXT) { + repr->setAttribute("sodipodi:insensitive", ( item->sensitive ? nullptr : "true" )); + if (item->transform_center_x != 0) + sp_repr_set_svg_double (repr, "inkscape:transform-center-x", item->transform_center_x); + else + repr->removeAttribute("inkscape:transform-center-x"); + if (item->transform_center_y != 0) { + auto y = item->transform_center_y; + y *= -document->yaxisdir(); + sp_repr_set_svg_double (repr, "inkscape:transform-center-y", y); + } else + repr->removeAttribute("inkscape:transform-center-y"); + } + + if (item->clip_ref){ + if (item->clip_ref->getObject()) { + auto value = item->clip_ref->getURI()->cssStr(); + repr->setAttributeOrRemoveIfEmpty("clip-path", value); + } + } + if (item->mask_ref){ + if (item->mask_ref->getObject()) { + auto value = item->mask_ref->getURI()->cssStr(); + repr->setAttributeOrRemoveIfEmpty("mask", value); + } + } + if (item->_highlightColor){ + repr->setAttribute("inkscape:highlight-color", item->_highlightColor); + } else { + repr->removeAttribute("inkscape:highlight-color"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +// CPPIFY: make pure virtual +Geom::OptRect SPItem::bbox(Geom::Affine const & /*transform*/, SPItem::BBoxType /*type*/) const { + //throw; + return Geom::OptRect(); +} + +Geom::OptRect SPItem::geometricBounds(Geom::Affine const &transform) const +{ + Geom::OptRect bbox; + + // call the subclass method + // CPPIFY + //bbox = this->bbox(transform, SPItem::GEOMETRIC_BBOX); + bbox = const_cast(this)->bbox(transform, SPItem::GEOMETRIC_BBOX); + + return bbox; +} + +Geom::OptRect SPItem::visualBounds(Geom::Affine const &transform, bool wfilter, bool wclip, bool wmask) const +{ + using Geom::X; + using Geom::Y; + + Geom::OptRect bbox; + + + SPFilter *filter = (style && style->filter.href) ? dynamic_cast(style->getFilter()) : nullptr; + if (filter && wfilter) { + // call the subclass method + // CPPIFY + //bbox = this->bbox(Geom::identity(), SPItem::VISUAL_BBOX); + bbox = const_cast(this)->bbox(Geom::identity(), SPItem::GEOMETRIC_BBOX); // see LP Bug 1229971 + + // default filer area per the SVG spec: + SVGLength x, y, w, h; + Geom::Point minp, maxp; + x.set(SVGLength::PERCENT, -0.10, 0); + y.set(SVGLength::PERCENT, -0.10, 0); + w.set(SVGLength::PERCENT, 1.20, 0); + h.set(SVGLength::PERCENT, 1.20, 0); + + // if area is explicitly set, override: + if (filter->x._set) + x = filter->x; + if (filter->y._set) + y = filter->y; + if (filter->width._set) + w = filter->width; + if (filter->height._set) + h = filter->height; + + double len_x = bbox ? bbox->width() : 0; + double len_y = bbox ? bbox->height() : 0; + + x.update(12, 6, len_x); + y.update(12, 6, len_y); + w.update(12, 6, len_x); + h.update(12, 6, len_y); + + if (filter->filterUnits == SP_FILTER_UNITS_OBJECTBOUNDINGBOX && bbox) { + minp[X] = bbox->left() + x.computed * (x.unit == SVGLength::PERCENT ? 1.0 : len_x); + maxp[X] = minp[X] + w.computed * (w.unit == SVGLength::PERCENT ? 1.0 : len_x); + minp[Y] = bbox->top() + y.computed * (y.unit == SVGLength::PERCENT ? 1.0 : len_y); + maxp[Y] = minp[Y] + h.computed * (h.unit == SVGLength::PERCENT ? 1.0 : len_y); + } else if (filter->filterUnits == SP_FILTER_UNITS_USERSPACEONUSE) { + minp[X] = x.computed; + maxp[X] = minp[X] + w.computed; + minp[Y] = y.computed; + maxp[Y] = minp[Y] + h.computed; + } + bbox = Geom::OptRect(minp, maxp); + *bbox *= transform; + } else { + // call the subclass method + // CPPIFY + //bbox = this->bbox(transform, SPItem::VISUAL_BBOX); + bbox = const_cast(this)->bbox(transform, SPItem::VISUAL_BBOX); + } + if (clip_ref && clip_ref->getObject() && wclip) { + SPItem *ownerItem = dynamic_cast(clip_ref->getOwner()); + g_assert(ownerItem != nullptr); + ownerItem->bbox_valid = FALSE; // LP Bug 1349018 + bbox.intersectWith(clip_ref->getObject()->geometricBounds(transform)); + } + if (mask_ref && mask_ref->getObject() && wmask) { + bbox_valid = false; // LP Bug 1349018 + bbox.intersectWith(mask_ref->getObject()->visualBounds(transform)); + } + + return bbox; +} + +Geom::OptRect SPItem::bounds(BBoxType type, Geom::Affine const &transform) const +{ + if (type == GEOMETRIC_BBOX) { + return geometricBounds(transform); + } else { + return visualBounds(transform); + } +} + +Geom::OptRect SPItem::documentPreferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return documentBounds(SPItem::VISUAL_BBOX); + } else { + return documentBounds(SPItem::GEOMETRIC_BBOX); + } +} + + + +Geom::OptRect SPItem::documentGeometricBounds() const +{ + return geometricBounds(i2doc_affine()); +} + +Geom::OptRect SPItem::documentVisualBounds() const +{ + if (!bbox_valid) { + doc_bbox = visualBounds(i2doc_affine()); + bbox_valid = true; + } + return doc_bbox; +} +Geom::OptRect SPItem::documentBounds(BBoxType type) const +{ + if (type == GEOMETRIC_BBOX) { + return documentGeometricBounds(); + } else { + return documentVisualBounds(); + } +} + +Geom::OptRect SPItem::desktopGeometricBounds() const +{ + return geometricBounds(i2dt_affine()); +} + +Geom::OptRect SPItem::desktopVisualBounds() const +{ + Geom::OptRect ret = documentVisualBounds(); + if (ret) { + *ret *= document->doc2dt(); + } + return ret; +} + +Geom::OptRect SPItem::desktopPreferredBounds() const +{ + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + return desktopBounds(SPItem::VISUAL_BBOX); + } else { + return desktopBounds(SPItem::GEOMETRIC_BBOX); + } +} + +Geom::OptRect SPItem::desktopBounds(BBoxType type) const +{ + if (type == GEOMETRIC_BBOX) { + return desktopGeometricBounds(); + } else { + return desktopVisualBounds(); + } +} + +unsigned int SPItem::pos_in_parent() const { + g_assert(parent != nullptr); + g_assert(SP_IS_OBJECT(parent)); + + unsigned int pos = 0; + + for (auto& iter: parent->children) { + if (&iter == this) { + return pos; + } + + if (dynamic_cast(&iter)) { + pos++; + } + } + + g_assert_not_reached(); + return 0; +} + +// CPPIFY: make pure virtual, see below! +void SPItem::snappoints(std::vector & /*p*/, Inkscape::SnapPreferences const */*snapprefs*/) const { + //throw; +} + /* This will only be called if the derived class doesn't override this. + * see for example sp_genericellipse_snappoints in sp-ellipse.cpp + * We don't know what shape we could be dealing with here, so we'll just + * do nothing + */ + +void SPItem::getSnappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const +{ + // Get the snappoints of the item + // CPPIFY + //this->snappoints(p, snapprefs); + const_cast(this)->snappoints(p, snapprefs); + + // Get the snappoints at the item's center + if (snapprefs != nullptr && snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER)) { + p.emplace_back(getCenter(), Inkscape::SNAPSOURCE_ROTATION_CENTER, Inkscape::SNAPTARGET_ROTATION_CENTER); + } + + // Get the snappoints of clipping paths and mask, if any + std::list clips_and_masks; + + if (clip_ref) clips_and_masks.push_back(clip_ref->getObject()); + if (mask_ref) clips_and_masks.push_back(mask_ref->getObject()); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + for (std::list::const_iterator o = clips_and_masks.begin(); o != clips_and_masks.end(); ++o) { + if (*o) { + // obj is a group object, the children are the actual clippers + for(auto& child: (*o)->children) { + SPItem *item = dynamic_cast(const_cast(&child)); + if (item) { + std::vector p_clip_or_mask; + // Please note the recursive call here! + item->getSnappoints(p_clip_or_mask, snapprefs); + // Take into account the transformation of the item being clipped or masked + for (const auto & p_orig : p_clip_or_mask) { + // All snappoints are in desktop coordinates, but the item's transformation is + // in document coordinates. Hence the awkward construction below + Geom::Point pt = desktop->dt2doc(p_orig.getPoint()) * i2dt_affine(); + p.emplace_back(pt, p_orig.getSourceType(), p_orig.getTargetType()); + } + } + } + } + } +} + +// CPPIFY: make pure virtual +void SPItem::print(SPPrintContext* /*ctx*/) { + //throw; +} + +void SPItem::invoke_print(SPPrintContext *ctx) +{ + if ( !isHidden() ) { + if (!transform.isIdentity() || style->opacity.value != SP_SCALE24_MAX) { + ctx->bind(transform, SP_SCALE24_TO_FLOAT(style->opacity.value)); + this->print(ctx); + ctx->release(); + } else { + this->print(ctx); + } + } +} + +const char* SPItem::displayName() const { + return _("Object"); +} + +gchar* SPItem::description() const { + return g_strdup(""); +} + +gchar *SPItem::detailedDescription() const { + gchar* s = g_strdup_printf("%s %s", + this->displayName(), this->description()); + + if (s && clip_ref && clip_ref->getObject()) { + gchar *snew = g_strdup_printf (_("%s; clipped"), s); + g_free (s); + s = snew; + } + + if (s && mask_ref && mask_ref->getObject()) { + gchar *snew = g_strdup_printf (_("%s; masked"), s); + g_free (s); + s = snew; + } + + if ( style && style->filter.href && style->filter.href->getObject() ) { + const gchar *label = style->filter.href->getObject()->label(); + gchar *snew = nullptr; + + if (label) { + snew = g_strdup_printf (_("%s; filtered (%s)"), s, _(label)); + } else { + snew = g_strdup_printf (_("%s; filtered"), s); + } + + g_free (s); + s = snew; + } + + return s; +} + +bool SPItem::isFiltered() const { + return (style && style->filter.href && style->filter.href->getObject()); +} + + +SPObject* SPItem::isInMask() const { + SPObject* parent = this->parent; + while (parent && !dynamic_cast(parent)) { + parent = parent->parent; + } + return parent; +} + +SPObject* SPItem::isInClipPath() const { + SPObject* parent = this->parent; + while (parent && !dynamic_cast(parent)) { + parent = parent->parent; + } + return parent; +} + +unsigned SPItem::display_key_new(unsigned numkeys) +{ + static unsigned dkey = 0; + + dkey += numkeys; + + return dkey - numkeys; +} + +// CPPIFY: make pure virtual +Inkscape::DrawingItem* SPItem::show(Inkscape::Drawing& /*drawing*/, unsigned int /*key*/, unsigned int /*flags*/) { + //throw; + return nullptr; +} + +Inkscape::DrawingItem *SPItem::invoke_show(Inkscape::Drawing &drawing, unsigned key, unsigned flags) +{ + Inkscape::DrawingItem *ai = nullptr; + + ai = this->show(drawing, key, flags); + + if (ai != nullptr) { + Geom::OptRect item_bbox = geometricBounds(); + + display = sp_item_view_new_prepend(display, this, flags, key, ai); + ai->setTransform(transform); + ai->setOpacity(SP_SCALE24_TO_FLOAT(style->opacity.value)); + ai->setIsolation( style->isolation.value ); + ai->setBlendMode( style->mix_blend_mode.value ); + //ai->setCompositeOperator( style->composite_op.value ); + ai->setVisible(!isHidden()); + ai->setSensitive(sensitive); + if (clip_ref && clip_ref->getObject()) { + SPClipPath *cp = clip_ref->getObject(); + + if (!display->arenaitem->key()) { + display->arenaitem->setKey(display_key_new(3)); + } + int clip_key = display->arenaitem->key(); + + // Show and set clip + Inkscape::DrawingItem *ac = cp->show(drawing, clip_key); + ai->setClip(ac); + + // Update bbox, in case the clip uses bbox units + cp->setBBox(clip_key, item_bbox); + cp->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + if (mask_ref && mask_ref->getObject()) { + SPMask *mask = mask_ref->getObject(); + + if (!display->arenaitem->key()) { + display->arenaitem->setKey(display_key_new(3)); + } + int mask_key = display->arenaitem->key(); + + // Show and set mask + Inkscape::DrawingItem *ac = mask->sp_mask_show(drawing, mask_key); + ai->setMask(ac); + + // Update bbox, in case the mask uses bbox units + mask->sp_mask_set_bbox(mask_key, item_bbox); + mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + + SPPaintServer *fill_ps = style->getFillPaintServer(); + if (fill_ps) { + if (!display->arenaitem->key()) { + display->arenaitem->setKey(display_key_new(3)); + } + int fill_key = display->arenaitem->key(); + + Inkscape::DrawingPattern *ap = fill_ps->show(drawing, fill_key, item_bbox); + ai->setFillPattern(ap); + if (ap) { + fill_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + SPPaintServer *stroke_ps = style->getStrokePaintServer(); + if (stroke_ps) { + if (!display->arenaitem->key()) { + display->arenaitem->setKey(display_key_new(3)); + } + int stroke_key = display->arenaitem->key(); + + Inkscape::DrawingPattern *ap = stroke_ps->show(drawing, stroke_key, item_bbox); + ai->setStrokePattern(ap); + if (ap) { + stroke_ps->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + ai->setItem(this); + ai->setItemBounds(geometricBounds()); + } + + return ai; +} + +// CPPIFY: make pure virtual +void SPItem::hide(unsigned int /*key*/) { + //throw; +} + +void SPItem::invoke_hide(unsigned key) +{ + this->hide(key); + + SPItemView *ref = nullptr; + SPItemView *v = display; + while (v != nullptr) { + SPItemView *next = v->next; + if (v->key == key) { + if (clip_ref && clip_ref->getObject()) { + (clip_ref->getObject())->hide(v->arenaitem->key()); + v->arenaitem->setClip(nullptr); + } + if (mask_ref && mask_ref->getObject()) { + mask_ref->getObject()->sp_mask_hide(v->arenaitem->key()); + v->arenaitem->setMask(nullptr); + } + SPPaintServer *fill_ps = style->getFillPaintServer(); + if (fill_ps) { + fill_ps->hide(v->arenaitem->key()); + } + SPPaintServer *stroke_ps = style->getStrokePaintServer(); + if (stroke_ps) { + stroke_ps->hide(v->arenaitem->key()); + } + if (!ref) { + display = v->next; + } else { + ref->next = v->next; + } + delete v->arenaitem; + g_free(v); + } else { + ref = v; + } + v = next; + } +} + +// Adjusters + +void SPItem::adjust_pattern(Geom::Affine const &postmul, bool set, PaintServerTransform pt) +{ + bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH); + if (fill && style && (style->fill.isPaintserver())) { + SPObject *server = style->getFillPaintServer(); + SPPattern *serverPatt = dynamic_cast(server); + if ( serverPatt ) { + SPPattern *pattern = serverPatt->clone_if_necessary(this, "fill"); + pattern->transform_multiply(postmul, set); + } + } + + bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH); + if (stroke && style && (style->stroke.isPaintserver())) { + SPObject *server = style->getStrokePaintServer(); + SPPattern *serverPatt = dynamic_cast(server); + if ( serverPatt ) { + SPPattern *pattern = serverPatt->clone_if_necessary(this, "stroke"); + pattern->transform_multiply(postmul, set); + } + } +} + +void SPItem::adjust_hatch(Geom::Affine const &postmul, bool set, PaintServerTransform pt) +{ + bool fill = (pt == TRANSFORM_FILL || pt == TRANSFORM_BOTH); + if (fill && style && (style->fill.isPaintserver())) { + SPObject *server = style->getFillPaintServer(); + SPHatch *serverHatch = dynamic_cast(server); + if (serverHatch) { + SPHatch *hatch = serverHatch->clone_if_necessary(this, "fill"); + hatch->transform_multiply(postmul, set); + } + } + + bool stroke = (pt == TRANSFORM_STROKE || pt == TRANSFORM_BOTH); + if (stroke && style && (style->stroke.isPaintserver())) { + SPObject *server = style->getStrokePaintServer(); + SPHatch *serverHatch = dynamic_cast(server); + if (serverHatch) { + SPHatch *hatch = serverHatch->clone_if_necessary(this, "stroke"); + hatch->transform_multiply(postmul, set); + } + } +} + +void SPItem::adjust_gradient( Geom::Affine const &postmul, bool set ) +{ + if ( style && style->fill.isPaintserver() ) { + SPPaintServer *server = style->getFillPaintServer(); + SPGradient *serverGrad = dynamic_cast(server); + if ( serverGrad ) { + + /** + * \note Bbox units for a gradient are generally a bad idea because + * with them, you cannot preserve the relative position of the + * object and its gradient after rotation or skew. So now we + * convert them to userspace units which are easy to keep in sync + * just by adding the object's transform to gradientTransform. + * \todo FIXME: convert back to bbox units after transforming with + * the item, so as to preserve the original units. + */ + SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "fill" ); + + sp_gradient_transform_multiply( gradient, postmul, set ); + } + } + + if ( style && style->stroke.isPaintserver() ) { + SPPaintServer *server = style->getStrokePaintServer(); + SPGradient *serverGrad = dynamic_cast(server); + if ( serverGrad ) { + SPGradient *gradient = sp_gradient_convert_to_userspace( serverGrad, this, "stroke"); + sp_gradient_transform_multiply( gradient, postmul, set ); + } + } +} + +void SPItem::adjust_stroke( gdouble ex ) +{ + if (freeze_stroke_width) { + return; + } + + SPStyle *style = this->style; + + if (style && !Geom::are_near(ex, 1.0, Geom::EPSILON)) { + style->stroke_width.computed *= ex; + style->stroke_width.set = TRUE; + + if ( !style->stroke_dasharray.values.empty() ) { + for (auto & value : style->stroke_dasharray.values) { + value.value *= ex; + value.computed *= ex; + } + style->stroke_dashoffset.value *= ex; + style->stroke_dashoffset.computed *= ex; + } + + updateRepr(); + } +} + +/** + * Find out the inverse of previous transform of an item (from its repr) + */ +Geom::Affine sp_item_transform_repr (SPItem *item) +{ + Geom::Affine t_old(Geom::identity()); + gchar const *t_attr = item->getRepr()->attribute("transform"); + if (t_attr) { + Geom::Affine t; + if (sp_svg_transform_read(t_attr, &t)) { + t_old = t; + } + } + + return t_old; +} + + +void SPItem::adjust_stroke_width_recursive(double expansion) +{ + adjust_stroke (expansion); + +// A clone's child is the ghost of its original - we must not touch it, skip recursion + if ( !dynamic_cast(this) ) { + for (auto& o: children) { + SPItem *item = dynamic_cast(&o); + if (item) { + item->adjust_stroke_width_recursive(expansion); + } + } + } +} + +void SPItem::freeze_stroke_width_recursive(bool freeze) +{ + freeze_stroke_width = freeze; + +// A clone's child is the ghost of its original - we must not touch it, skip recursion + if ( !dynamic_cast(this) ) { + for (auto& o: children) { + SPItem *item = dynamic_cast(&o); + if (item) { + item->freeze_stroke_width_recursive(freeze); + } + } + } +} + +/** + * Recursively adjust rx and ry of rects. + */ +static void +sp_item_adjust_rects_recursive(SPItem *item, Geom::Affine advertized_transform) +{ + SPRect *rect = dynamic_cast(item); + if (rect) { + rect->compensateRxRy(advertized_transform); + } + + for(auto& o: item->children) { + SPItem *itm = dynamic_cast(&o); + if (itm) { + sp_item_adjust_rects_recursive(itm, advertized_transform); + } + } +} + +void SPItem::adjust_paint_recursive(Geom::Affine advertized_transform, Geom::Affine t_ancestors, PaintServerType type) +{ +// _Before_ full pattern/gradient transform: t_paint * t_item * t_ancestors +// _After_ full pattern/gradient transform: t_paint_new * t_item * t_ancestors * advertised_transform +// By equating these two expressions we get t_paint_new = t_paint * paint_delta, where: + Geom::Affine t_item = sp_item_transform_repr (this); + Geom::Affine paint_delta = t_item * t_ancestors * advertized_transform * t_ancestors.inverse() * t_item.inverse(); + +// Within text, we do not fork gradients, and so must not recurse to avoid double compensation; +// also we do not recurse into clones, because a clone's child is the ghost of its original - +// we must not touch it + if (!(dynamic_cast(this) || dynamic_cast(this))) { + for (auto& o: children) { + SPItem *item = dynamic_cast(&o); + if (item) { + // At the level of the transformed item, t_ancestors is identity; + // below it, it is the accumulated chain of transforms from this level to the top level + item->adjust_paint_recursive(advertized_transform, t_item * t_ancestors, type); + } + } + } + +// We recursed into children first, and are now adjusting this object second; +// this is so that adjustments in a tree are done from leaves up to the root, +// and paintservers on leaves inheriting their values from ancestors could adjust themselves properly +// before ancestors themselves are adjusted, probably differently (bug 1286535) + + switch (type) { + case PATTERN: { + adjust_pattern(paint_delta); + break; + } + case HATCH: { + adjust_hatch(paint_delta); + break; + } + default: { + adjust_gradient(paint_delta); + } + } +} + +// CPPIFY:: make pure virtual? +// Not all SPItems must necessarily have a set transform method! +Geom::Affine SPItem::set_transform(Geom::Affine const &transform) { +// throw; + return transform; +} + +void SPItem::doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv, bool compensate) +{ + // calculate the relative transform, if not given by the adv attribute + Geom::Affine advertized_transform; + if (adv != nullptr) { + advertized_transform = *adv; + } else { + advertized_transform = sp_item_transform_repr (this).inverse() * transform; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (compensate) { + // recursively compensating for stroke scaling will not always work, because it can be scaled to zero or infinite + // from which we cannot ever recover by applying an inverse scale; therefore we temporarily block any changes + // to the strokewidth in such a case instead, and unblock these after the transformation + // (as reported in https://bugs.launchpad.net/inkscape/+bug/825840/comments/4) + if (!prefs->getBool("/options/transform/stroke", true)) { + double const expansion = 1. / advertized_transform.descrim(); + if (expansion < 1e-9 || expansion > 1e9) { + freeze_stroke_width_recursive(true); + // This will only work if the item has a set_transform method (in this method adjust_stroke() will be called) + // We will still have to apply the inverse scaling to other items, not having a set_transform method + // such as ellipses and stars + // PS: We cannot use this freeze_stroke_width_recursive() trick in all circumstances. For example, it will + // break pasting objects within their group (because in such a case the transformation of the group will affect + // the strokewidth, and has to be compensated for. See https://bugs.launchpad.net/inkscape/+bug/959223/comments/10) + } else { + adjust_stroke_width_recursive(expansion); + } + } + + // recursively compensate rx/ry of a rect if requested + if (!prefs->getBool("/options/transform/rectcorners", true)) { + sp_item_adjust_rects_recursive(this, advertized_transform); + } + + // recursively compensate pattern fill if it's not to be transformed + if (!prefs->getBool("/options/transform/pattern", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), PATTERN); + } + if (!prefs->getBool("/options/transform/hatch", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), HATCH); + } + + /// \todo FIXME: add the same else branch as for gradients below, to convert patterns to userSpaceOnUse as well + /// recursively compensate gradient fill if it's not to be transformed + if (!prefs->getBool("/options/transform/gradient", true)) { + adjust_paint_recursive(advertized_transform.inverse(), Geom::identity(), GRADIENT); + } else { + // this converts the gradient/pattern fill/stroke, if any, to userSpaceOnUse; we need to do + // it here _before_ the new transform is set, so as to use the pre-transform bbox + adjust_paint_recursive(Geom::identity(), Geom::identity(), GRADIENT); + } + + } // endif(compensate) + + gint preserve = prefs->getBool("/options/preservetransform/value", false); + Geom::Affine transform_attr (transform); + + // CPPIFY: check this code. + // If onSetTransform is not overridden, CItem::onSetTransform will return the transform it was given as a parameter. + // onSetTransform cannot be pure due to the fact that not all visible Items are transformable. + SPLPEItem * lpeitem = SP_LPE_ITEM(this); + if ( // run the object's set_transform (i.e. embed transform) only if: + (dynamic_cast(this) && firstChild() && dynamic_cast(firstChild())) || + (!preserve && // user did not chose to preserve all transforms + (!clip_ref || !clip_ref->getObject()) && // the object does not have a clippath + (!mask_ref || !mask_ref->getObject()) && // the object does not have a mask + !(!transform.isTranslation() && style && style->getFilter())) // the object does not have a filter, or the transform is translation (which is supposed to not affect filters) + ) + { + transform_attr = this->set_transform(transform); + } + if (freeze_stroke_width) { + freeze_stroke_width_recursive(false); + if (compensate) { + if (!prefs->getBool("/options/transform/stroke", true)) { + // Recursively compensate for stroke scaling, depending on user preference + // (As to why we need to do this, see the comment a few lines above near the freeze_stroke_width_recursive(true) call) + double const expansion = 1. / advertized_transform.descrim(); + adjust_stroke_width_recursive(expansion); + } + } + } + // this avoid temporary scaling issues on display when near identity + // this must be a bit grater than EPSILON * transform.descrim() + double e = 1e-5 * transform.descrim(); + if (transform_attr.isIdentity(e)) { + transform_attr = Geom::Affine(); + } + set_item_transform(transform_attr); + + // Note: updateRepr comes before emitting the transformed signal since + // it causes clone SPUse's copy of the original object to brought up to + // date with the original. Otherwise, sp_use_bbox returns incorrect + // values if called in code handling the transformed signal. + updateRepr(); + + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + sp_lpe_item_update_patheffect(lpeitem, true, false); + } + + // send the relative transform with a _transformed_signal + _transformed_signal.emit(&advertized_transform, this); +} + +// CPPIFY: see below, do not make pure? +gint SPItem::event(SPEvent* /*event*/) { + return FALSE; +} + +gint SPItem::emitEvent(SPEvent &event) +{ + return this->event(&event); +} + +void SPItem::set_item_transform(Geom::Affine const &transform_matrix) +{ + if (!Geom::are_near(transform_matrix, transform, 1e-18)) { + transform = transform_matrix; + /* The SP_OBJECT_USER_MODIFIED_FLAG_B is used to mark the fact that it's only a + transformation. It's apparently not used anywhere else. */ + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_USER_MODIFIED_FLAG_B); + sp_item_rm_unsatisfied_cns(*this); + } +} + +//void SPItem::convert_to_guides() const { +// // CPPIFY: If not overridden, call SPItem::convert_to_guides() const, see below! +// this->convert_to_guides(); +//} + + +Geom::Affine i2anc_affine(SPObject const *object, SPObject const *const ancestor) { + Geom::Affine ret(Geom::identity()); + g_return_val_if_fail(object != nullptr, ret); + + /* stop at first non-renderable ancestor */ + while ( object != ancestor && dynamic_cast(object) ) { + SPRoot const *root = dynamic_cast(object); + if (root) { + ret *= root->c2p; + } else { + SPItem const *item = dynamic_cast(object); + g_assert(item != nullptr); + ret *= item->transform; + } + object = object->parent; + } + return ret; +} + +Geom::Affine +i2i_affine(SPObject const *src, SPObject const *dest) { + g_return_val_if_fail(src != nullptr && dest != nullptr, Geom::identity()); + SPObject const *ancestor = src->nearestCommonAncestor(dest); + return i2anc_affine(src, ancestor) * i2anc_affine(dest, ancestor).inverse(); +} + +Geom::Affine SPItem::getRelativeTransform(SPObject const *dest) const { + return i2i_affine(this, dest); +} + +Geom::Affine SPItem::i2doc_affine() const +{ + return i2anc_affine(this, nullptr); +} + +Geom::Affine SPItem::i2dt_affine() const +{ + Geom::Affine ret(i2doc_affine()); + ret *= document->doc2dt(); + return ret; +} + +// TODO should be named "set_i2dt_affine" +void SPItem::set_i2d_affine(Geom::Affine const &i2dt) +{ + Geom::Affine dt2p; /* desktop to item parent transform */ + if (parent) { + dt2p = static_cast(parent)->i2dt_affine().inverse(); + } else { + dt2p = document->dt2doc(); + } + + Geom::Affine const i2p( i2dt * dt2p ); + set_item_transform(i2p); +} + + +Geom::Affine SPItem::dt2i_affine() const +{ + /* fixme: Implement the right way (Lauris) */ + return i2dt_affine().inverse(); +} + +/* Item views */ + +SPItemView *SPItem::sp_item_view_new_prepend(SPItemView *list, SPItem *item, unsigned flags, unsigned key, Inkscape::DrawingItem *drawing_item) +{ + g_assert(item != nullptr); + g_assert(dynamic_cast(item) != nullptr); + g_assert(drawing_item != nullptr); + + SPItemView *new_view = g_new(SPItemView, 1); + + new_view->next = list; + new_view->flags = flags; + new_view->key = key; + new_view->arenaitem = drawing_item; + + return new_view; +} + +static SPItemView* +sp_item_view_list_remove(SPItemView *list, SPItemView *view) +{ + SPItemView *ret = list; + if (view == list) { + ret = list->next; + } else { + SPItemView *prev; + prev = list; + while (prev->next != view) prev = prev->next; + prev->next = view->next; + } + + delete view->arenaitem; + g_free(view); + + return ret; +} + +Inkscape::DrawingItem *SPItem::get_arenaitem(unsigned key) +{ + for ( SPItemView *iv = display ; iv ; iv = iv->next ) { + if ( iv->key == key ) { + return iv->arenaitem; + } + } + + return nullptr; +} + +int sp_item_repr_compare_position(SPItem const *first, SPItem const *second) +{ + return sp_repr_compare_position(first->getRepr(), + second->getRepr()); +} + +SPItem const *sp_item_first_item_child(SPObject const *obj) +{ + return sp_item_first_item_child( const_cast(obj) ); +} + +SPItem *sp_item_first_item_child(SPObject *obj) +{ + SPItem *child = nullptr; + for (auto& iter: obj->children) { + SPItem *tmp = dynamic_cast(&iter); + if ( tmp ) { + child = tmp; + break; + } + } + return child; +} + +void SPItem::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getInt("/tools/bounding_box", 0); + + Geom::OptRect bbox = (prefs_bbox == 0) ? desktopVisualBounds() : desktopGeometricBounds(); + if (!bbox) { + g_warning ("Cannot determine item's bounding box during conversion to guides.\n"); + return; + } + + std::list > pts; + + Geom::Point A((*bbox).min()); + Geom::Point C((*bbox).max()); + Geom::Point B(A[Geom::X], C[Geom::Y]); + Geom::Point D(C[Geom::X], A[Geom::Y]); + + pts.emplace_back(A, B); + pts.emplace_back(B, C); + pts.emplace_back(C, D); + pts.emplace_back(D, A); + + sp_guide_pt_pairs_to_guides(document, pts); +} + +void SPItem::rotate_rel(Geom::Rotate const &rotation) +{ + Geom::Point center = getCenter(); + Geom::Translate const s(getCenter()); + Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + + // Rotate item. + set_i2d_affine(i2dt_affine() * (Geom::Affine)affine); + // Use each item's own transform writer, consistent with sp_selection_apply_affine() + doWriteTransform(transform); + + // Restore the center position (it's changed because the bbox center changed) + if (isCenterSet()) { + setCenter(center * affine); + updateRepr(); + } +} + +void SPItem::scale_rel(Geom::Scale const &scale) +{ + Geom::OptRect bbox = desktopVisualBounds(); + if (bbox) { + Geom::Translate const s(bbox->midpoint()); // use getCenter? + set_i2d_affine(i2dt_affine() * s.inverse() * scale * s); + doWriteTransform(transform); + } +} + +void SPItem::skew_rel(double skewX, double skewY) +{ + Geom::Point center = getCenter(); + Geom::Translate const s(getCenter()); + + Geom::Affine const skew(1, skewY, skewX, 1, 0, 0); + Geom::Affine affine = Geom::Affine(s).inverse() * skew * Geom::Affine(s); + + set_i2d_affine(i2dt_affine() * affine); + doWriteTransform(transform); + + // Restore the center position (it's changed because the bbox center changed) + if (isCenterSet()) { + setCenter(center * affine); + updateRepr(); + } +} + +void SPItem::move_rel( Geom::Translate const &tr) +{ + set_i2d_affine(i2dt_affine() * tr); + + doWriteTransform(transform); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-item.h b/src/object/sp-item.h new file mode 100644 index 0000000..f33fea7 --- /dev/null +++ b/src/object/sp-item.h @@ -0,0 +1,480 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_ITEM_H +#define SEEN_SP_ITEM_H + +/** + * @file + * Some things pertinent to all visible shapes: SPItem, SPItemView, SPItemCtx, SPItemClass, SPEvent. + */ + +/* + * Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Abhishek Sharma + * + * Copyright (C) 1999-2006 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include + +#include "sp-object.h" +#include "snap-preferences.h" +#include "snap-candidate.h" + +//class SPGuideConstraint; +#include "sp-guide-constraint.h" +#include "xml/repr.h" + +class SPClipPath; +class SPClipPathReference; +class SPMask; +class SPMaskReference; +class SPAvoidRef; +class SPPattern; +struct SPPrintContext; +typedef unsigned int guint32; + +namespace Inkscape { + +class Drawing; +class DrawingItem; +class URIReference; + +namespace UI { +namespace View { +class SVGViewWidget; +} +} +} + +// TODO make a completely new function that transforms either the fill or +// stroke of any SPItem without adding an extra parameter to adjust_pattern. +enum PaintServerTransform { TRANSFORM_BOTH, TRANSFORM_FILL, TRANSFORM_STROKE }; + +/** + * Event structure. + * + * @todo This is just placeholder. Plan: + * We do extensible event structure, that hold applicable (ui, non-ui) + * data pointers. So it is up to given object/arena implementation + * to process correct ones in meaningful way. + * Also, this probably goes to SPObject base class. + * + * GUI Code should not be here! + */ +class SPEvent { + +public: + enum Type { + INVALID, + NONE, + ACTIVATE, + MOUSEOVER, + MOUSEOUT + }; + + Type type; + Inkscape::UI::View::SVGViewWidget* view; +}; + +class SPItemView { +public: + SPItemView *next; + unsigned int flags; + unsigned int key; + Inkscape::DrawingItem *arenaitem; +}; + +/* flags */ + +#define SP_ITEM_BBOX_VISUAL 1 + +#define SP_ITEM_SHOW_DISPLAY (1 << 0) + +/** + * Flag for referenced views (i.e. markers, clippaths, masks and patterns); + * currently unused, does the same as DISPLAY + */ +#define SP_ITEM_REFERENCE_FLAGS (1 << 1) + +/** + * Contains transformations to document/viewport and the viewport size. + */ +class SPItemCtx : public SPCtx { +public: + /** Item to document transformation */ + Geom::Affine i2doc; + + /** Viewport size */ + Geom::Rect viewport; + + /** Item to viewport transformation */ + Geom::Affine i2vp; +}; + +#define SP_ITEM(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_ITEM(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** + * Base class for visual SVG elements. + * SPItem is an abstract base class for all graphic (visible) SVG nodes. It + * is a subclass of SPObject, with great deal of specific functionality. + */ +class SPItem : public SPObject { +public: + enum BBoxType { + // legacy behavior: includes crude stroke, markers; excludes long miters, blur margin; is known to be wrong for caps + APPROXIMATE_BBOX, + // includes only the bare path bbox, no stroke, no nothing + GEOMETRIC_BBOX, + // includes everything: correctly done stroke (with proper miters and caps), markers, filter margins (e.g. blur) + VISUAL_BBOX + }; + + enum PaintServerType { PATTERN, HATCH, GRADIENT }; + + SPItem(); + ~SPItem() override; + + unsigned int sensitive : 1; + unsigned int stop_paint: 1; + mutable unsigned bbox_valid : 1; + double transform_center_x; + double transform_center_y; + bool freeze_stroke_width; + + Geom::Affine transform; + mutable Geom::OptRect doc_bbox; + Geom::Rect viewport; // Cache viewport information + + SPClipPath *getClipObject() const; + SPMask *getMaskObject() const; + + SPClipPathReference &getClipRef(); + SPMaskReference &getMaskRef(); + + SPAvoidRef &getAvoidRef(); + + private: + SPClipPathReference *clip_ref; + SPMaskReference *mask_ref; + + // Used for object-avoiding connectors + SPAvoidRef *avoidRef; + + public: + SPItemView *display; + + std::vector constraints; + + sigc::signal _transformed_signal; + + bool isLocked() const; + void setLocked(bool lock); + + bool isHidden() const; + void setHidden(bool hidden); + + // Objects dialogue + bool isSensitive() const { + return sensitive; + }; + + bool isHighlightSet() const; + guint32 highlight_color() const; + + void setHighlightColor(guint32 color); + + void unsetHighlightColor(); + //==================== + + bool isEvaluated() const; + void setEvaluated(bool visible); + void resetEvaluated(); + + bool isHidden(unsigned display_key) const; + + /** + * Returns something suitable for the `Hide' checkbox in the Object Properties dialog box. + * Corresponds to setExplicitlyHidden. + */ + bool isExplicitlyHidden() const; + + /** + * Sets the display CSS property to `hidden' if \a val is true, + * otherwise makes it unset. + */ + void setExplicitlyHidden(bool val); + + /** + * Sets the transform_center_x and transform_center_y properties to retain the rotation center + */ + void setCenter(Geom::Point const &object_centre); + + void unsetCenter(); + bool isCenterSet() const; + Geom::Point getCenter() const; + void scaleCenter(Geom::Scale const &sc); + + bool isVisibleAndUnlocked() const; + + bool isVisibleAndUnlocked(unsigned display_key) const; + + Geom::Affine getRelativeTransform(SPObject const *obj) const; + + bool raiseOne(); + bool lowerOne(); + void raiseToTop(); + void lowerToBottom(); + + /** + * Move this SPItem into or after another SPItem in the doc. + * + * @param target the SPItem to move into or after. + * @param intoafter move to after the target (false), move inside (sublayer) of the target (true). + */ + void moveTo(SPItem *target, bool intoafter); + + sigc::connection connectTransformed(sigc::slot slot) { + return _transformed_signal.connect(slot); + } + + /** + * Get item's geometric bounding box in this item's coordinate system. + * + * The geometric bounding box includes only the path, disregarding all style attributes. + */ + Geom::OptRect geometricBounds(Geom::Affine const &transform = Geom::identity()) const; + + /** + * Get item's visual bounding box in this item's coordinate system. + * + * The visual bounding box includes the stroke and the filter region. + * @param wfilter use filter expand in bbox calculation + * @param wclip use clip data in bbox calculation + * @param wmask use mask data in bbox calculation + */ + Geom::OptRect visualBounds(Geom::Affine const &transform = Geom::identity(), bool wfilter = true, bool wclip = true, + bool wmask = true) const; + + Geom::OptRect bounds(BBoxType type, Geom::Affine const &transform = Geom::identity()) const; + + /** + * Get item's geometric bbox in document coordinate system. + * Document coordinates are the default coordinates of the root element: + * the origin is at the top left, X grows to the right and Y grows downwards. + */ + Geom::OptRect documentGeometricBounds() const; + + /** + * Get item's visual bbox in document coordinate system. + */ + Geom::OptRect documentVisualBounds() const; + + Geom::OptRect documentBounds(BBoxType type) const; + Geom::OptRect documentPreferredBounds() const; + + /** + * Get item's geometric bbox in desktop coordinate system. + * Desktop coordinates should be user defined. Currently they are hardcoded: + * origin is at bottom left, X grows to the right and Y grows upwards. + */ + Geom::OptRect desktopGeometricBounds() const; + + /** + * Get item's visual bbox in desktop coordinate system. + */ + Geom::OptRect desktopVisualBounds() const; + + Geom::OptRect desktopPreferredBounds() const; + Geom::OptRect desktopBounds(BBoxType type) const; + + unsigned int pos_in_parent() const; + + /** + * Returns a string suitable for status bar, formatted in pango markup language. + * + * Must be freed by caller. + */ + char *detailedDescription() const; + + /** + * Returns true if the item is filtered, false otherwise. + * Used with groups/lists to determine how many, or if any, are filtered. + */ + bool isFiltered() const; + + SPObject* isInMask() const; + + SPObject* isInClipPath() const; + + void invoke_print(SPPrintContext *ctx); + + /** + * Allocates unique integer keys. + * + * @param numkeys Number of keys required. + * @return First allocated key; hence if the returned key is n + * you can use n, n + 1, ..., n + (numkeys - 1) + */ + static unsigned int display_key_new(unsigned int numkeys); + + Inkscape::DrawingItem *invoke_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + + // Removed item from display tree. + void invoke_hide(unsigned int key); + + void getSnappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs=nullptr) const; + void adjust_pattern(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false, + PaintServerTransform = TRANSFORM_BOTH); + void adjust_hatch(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false, + PaintServerTransform = TRANSFORM_BOTH); + void adjust_gradient(/* Geom::Affine const &premul, */ Geom::Affine const &postmul, bool set = false); + void adjust_stroke(double ex); + + /** + * Recursively scale stroke width in \a item and its children by \a expansion. + */ + void adjust_stroke_width_recursive(double ex); + + void freeze_stroke_width_recursive(bool freeze); + + /** + * Recursively compensate pattern or gradient transform. + */ + void adjust_paint_recursive(Geom::Affine advertized_transform, Geom::Affine t_ancestors, + PaintServerType type = GRADIENT); + + /** + * Set a new transform on an object. + * + * Compensate for stroke scaling and gradient/pattern fill transform, if + * necessary. Call the object's set_transform method if transforms are + * stored optimized. Send _transformed_signal. Invoke _write method so that + * the repr is updated with the new transform. + */ + void doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv = nullptr, bool compensate = true); + + /** + * Sets item private transform (not propagated to repr), without compensating stroke widths, + * gradients, patterns as sp_item_write_transform does. + */ + void set_item_transform(Geom::Affine const &transform_matrix); + + int emitEvent (SPEvent &event); + + /** + * Return the arenaitem corresponding to the given item in the display + * with the given key + */ + Inkscape::DrawingItem *get_arenaitem(unsigned int key); + + /** + * Returns the accumulated transformation of the item and all its ancestors, including root's viewport. + * @pre (item != NULL) and SP_IS_ITEM(item). + */ + Geom::Affine i2doc_affine() const; + + /** + * Returns the transformation from item to desktop coords + */ + Geom::Affine i2dt_affine() const; + + void set_i2d_affine(Geom::Affine const &transform); + + /** + * should rather be named "sp_item_d2i_affine" to match "sp_item_i2d_affine" (or vice versa). + */ + Geom::Affine dt2i_affine() const; + + char *_highlightColor; + +private: + enum EvaluatedStatus + { + StatusUnknown, StatusCalculated, StatusSet + }; + + mutable bool _is_evaluated; + mutable EvaluatedStatus _evaluated_status; + + static SPItemView *sp_item_view_new_prepend(SPItemView *list, SPItem *item, unsigned flags, unsigned key, Inkscape::DrawingItem *arenaitem); + static void clip_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item); + static void mask_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item); + static void fill_ps_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item); + static void stroke_ps_ref_changed(SPObject *old_clip, SPObject *clip, SPItem *item); + +public: + void rotate_rel(Geom::Rotate const &rotation); + void scale_rel(Geom::Scale const &scale); + void skew_rel(double skewX, double skewY); + void move_rel( Geom::Translate const &tr); + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + virtual Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const; + virtual void print(SPPrintContext *ctx); + virtual const char* displayName() const; + virtual char* description() const; + virtual Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + virtual void hide(unsigned int key); + virtual void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const; + virtual Geom::Affine set_transform(Geom::Affine const &transform); + + virtual void convert_to_guides() const; + + virtual int event(SPEvent *event); +}; + + +// Utility + +/** + * @pre \a ancestor really is an ancestor (\>=) of \a object, or NULL. + * ("Ancestor (\>=)" here includes as far as \a object itself.) + */ +Geom::Affine i2anc_affine(SPObject const *item, SPObject const *ancestor); + +Geom::Affine i2i_affine(SPObject const *src, SPObject const *dest); + +Geom::Affine sp_item_transform_repr (SPItem *item); + +/* fixme: - these are evil, but OK */ + +int sp_item_repr_compare_position(SPItem const *first, SPItem const *second); + +inline bool sp_item_repr_compare_position_bool(SPObject const *first, SPObject const *second) +{ + return sp_repr_compare_position(((SPItem*)first)->getRepr(), + ((SPItem*)second)->getRepr())<0; +} + + +SPItem *sp_item_first_item_child (SPObject *obj); +SPItem const *sp_item_first_item_child (SPObject const *obj); + +#endif // SEEN_SP_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-line.cpp b/src/object/sp-line.cpp new file mode 100644 index 0000000..a095d85 --- /dev/null +++ b/src/object/sp-line.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "style.h" +#include "sp-line.h" +#include "sp-guide.h" +#include "display/curve.h" +#include +#include "document.h" +#include "inkscape.h" + +SPLine::SPLine() : SPShape() { + this->x1.unset(); + this->y1.unset(); + this->x2.unset(); + this->y2.unset(); +} + +SPLine::~SPLine() = default; + +void SPLine::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPShape::build(document, repr); + + this->readAttr( "x1" ); + this->readAttr( "y1" ); + this->readAttr( "x2" ); + this->readAttr( "y2" ); +} + +void SPLine::set(SPAttributeEnum key, const gchar* value) { + /* fixme: we should really collect updates */ + + switch (key) { + case SP_ATTR_X1: + this->x1.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y1: + this->y1.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_X2: + this->x2.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y2: + this->y2.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPLine::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + SPStyle const *style = this->style; + SPItemCtx const *ictx = (SPItemCtx const *) ctx; + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; // fixme: get from pango or libnrtype. + + this->x1.update(em, ex, w); + this->x2.update(em, ex, w); + this->y1.update(em, ex, h); + this->y2.update(em, ex, h); + + this->set_shape(); + } + + SPShape::update(ctx, flags); +} + +Inkscape::XML::Node* SPLine::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:line"); + } + + if (repr != this->getRepr()) { + repr->mergeFrom(this->getRepr(), "id"); + } + + sp_repr_set_svg_double(repr, "x1", this->x1.computed); + sp_repr_set_svg_double(repr, "y1", this->y1.computed); + sp_repr_set_svg_double(repr, "x2", this->x2.computed); + sp_repr_set_svg_double(repr, "y2", this->y2.computed); + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPLine::displayName() const { + return _("Line"); +} + +void SPLine::convert_to_guides() const { + Geom::Point points[2]; + Geom::Affine const i2dt(this->i2dt_affine()); + + points[0] = Geom::Point(this->x1.computed, this->y1.computed)*i2dt; + points[1] = Geom::Point(this->x2.computed, this->y2.computed)*i2dt; + + SPGuide::createSPGuide(this->document, points[0], points[1]); +} + + +Geom::Affine SPLine::set_transform(Geom::Affine const &transform) { + Geom::Point points[2]; + + points[0] = Geom::Point(this->x1.computed, this->y1.computed); + points[1] = Geom::Point(this->x2.computed, this->y2.computed); + + points[0] *= transform; + points[1] *= transform; + + this->x1.computed = points[0][Geom::X]; + this->y1.computed = points[0][Geom::Y]; + this->x2.computed = points[1][Geom::X]; + this->y2.computed = points[1][Geom::Y]; + + this->adjust_stroke(transform.descrim()); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + + return Geom::identity(); +} + +void SPLine::set_shape() { + SPCurve *c = new SPCurve(); + + c->moveto(this->x1.computed, this->y1.computed); + c->lineto(this->x2.computed, this->y2.computed); + + this->setCurveInsync(c); // *_insync does not call update, avoiding infinite recursion when set_shape is called by update + this->setCurveBeforeLPE(c); + + // LPE's cannot be applied to lines. (the result can (generally) not be represented as SPLine) + + c->unref(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-line.h b/src/object/sp-line.h new file mode 100644 index 0000000..5861d5b --- /dev/null +++ b/src/object/sp-line.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_LINE_H +#define SEEN_SP_LINE_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "svg/svg-length.h" +#include "sp-shape.h" + +#define SP_LINE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_LINE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPLine : public SPShape { +public: + SPLine(); + ~SPLine() override; + + SVGLength x1; + SVGLength y1; + SVGLength x2; + SVGLength y2; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void set(SPAttributeEnum key, char const* value) override; + + const char* displayName() const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + void convert_to_guides() const override; + void update(SPCtx* ctx, unsigned int flags) override; + + void set_shape() override; +}; + +#endif // SEEN_SP_LINE_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-linear-gradient.cpp b/src/object/sp-linear-gradient.cpp new file mode 100644 index 0000000..d0d75af --- /dev/null +++ b/src/object/sp-linear-gradient.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include + +#include "sp-linear-gradient.h" + +#include "attributes.h" +#include "style.h" +#include "xml/repr.h" + +/* + * Linear Gradient + */ +SPLinearGradient::SPLinearGradient() : SPGradient() { + this->x1.unset(SVGLength::PERCENT, 0.0, 0.0); + this->y1.unset(SVGLength::PERCENT, 0.0, 0.0); + this->x2.unset(SVGLength::PERCENT, 1.0, 1.0); + this->y2.unset(SVGLength::PERCENT, 0.0, 0.0); +} + +SPLinearGradient::~SPLinearGradient() = default; + +void SPLinearGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGradient::build(document, repr); + + this->readAttr( "x1" ); + this->readAttr( "y1" ); + this->readAttr( "x2" ); + this->readAttr( "y2" ); +} + +/** + * Callback: set attribute. + */ +void SPLinearGradient::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_X1: + this->x1.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y1: + this->y1.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_X2: + this->x2.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y2: + this->y2.readOrUnset(value, SVGLength::PERCENT, 0.0, 0.0); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } +} + +void +SPLinearGradient::update(SPCtx *ctx, guint flags) +{ + // To do: Verify flags. + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx const *ictx = reinterpret_cast(ctx); + + if (getUnits() == SP_GRADIENT_UNITS_USERSPACEONUSE) { + double w = ictx->viewport.width(); + double h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->x1.update(em, ex, w); + this->y1.update(em, ex, h); + this->x2.update(em, ex, w); + this->y2.update(em, ex, h); + } + } +} + +/** + * Callback: write attributes to associated repr. + */ +Inkscape::XML::Node* SPLinearGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:linearGradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x1._set) { + sp_repr_set_svg_double(repr, "x1", this->x1.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y1._set) { + sp_repr_set_svg_double(repr, "y1", this->y1.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x2._set) { + sp_repr_set_svg_double(repr, "x2", this->x2.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y2._set) { + sp_repr_set_svg_double(repr, "y2", this->y2.computed); + } + + SPGradient::write(xml_doc, repr, flags); + + return repr; +} + +cairo_pattern_t* SPLinearGradient::pattern_new(cairo_t * /*ct*/, Geom::OptRect const &bbox, double opacity) { + this->ensureVector(); + + cairo_pattern_t *cp = cairo_pattern_create_linear( + this->x1.computed, this->y1.computed, + this->x2.computed, this->y2.computed); + + sp_gradient_pattern_common_setup(cp, this, bbox, opacity); + + return cp; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-linear-gradient.h b/src/object/sp-linear-gradient.h new file mode 100644 index 0000000..c54e900 --- /dev/null +++ b/src/object/sp-linear-gradient.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_LINEAR_GRADIENT_H +#define SP_LINEAR_GRADIENT_H + +/** \file + * SPLinearGradient: SVG implementation + */ + +#include "sp-gradient.h" +#include "svg/svg-length.h" + +#define SP_LINEARGRADIENT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_LINEARGRADIENT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** Linear gradient. */ +class SPLinearGradient : public SPGradient { +public: + SPLinearGradient(); + ~SPLinearGradient() override; + + SVGLength x1; + SVGLength y1; + SVGLength x2; + SVGLength y2; + + cairo_pattern_t* pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttributeEnum key, char const *value) override; + void update(SPCtx *ctx, guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_LINEAR_GRADIENT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-lpe-item.cpp b/src/object/sp-lpe-item.cpp new file mode 100755 index 0000000..0e8ca91 --- /dev/null +++ b/src/object/sp-lpe-item.cpp @@ -0,0 +1,1293 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Base class for live path effect items + */ +/* + * Authors: + * Johan Engelen + * Bastien Bouclet + * Abhishek Sharma + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include + +#include "bad-uri-exception.h" + +#include "attributes.h" +#include "desktop.h" +#include "display/curve.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-bool.h" +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpe-copy_rotate.h" +#include "live_effects/lpe-lattice2.h" +#include "live_effects/lpe-measure-segments.h" +#include "live_effects/lpe-mirror_symmetry.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "sp-clippath.h" +#include "sp-ellipse.h" +#include "sp-item-group.h" +#include "sp-mask.h" +#include "sp-path.h" +#include "sp-rect.h" +#include "sp-root.h" +#include "svg/svg.h" +#include "ui/shape-editor.h" +#include "uri.h" + +/* LPEItem base class */ + +static void lpeobject_ref_modified(SPObject *href, guint flags, SPLPEItem *lpeitem); +static void sp_lpe_item_create_original_path_recursive(SPLPEItem *lpeitem); +static void sp_lpe_item_cleanup_original_path_recursive(SPLPEItem *lpeitem, bool keep_paths, bool force = false, bool is_clip_mask = false); + +typedef std::list HRefList; +static std::string patheffectlist_svg_string(PathEffectList const & list); +static std::string hreflist_svg_string(HRefList const & list); + +namespace { + void clear_path_effect_list(PathEffectList* const l) { + PathEffectList::iterator it = l->begin(); + while ( it != l->end()) { + (*it)->unlink(); + delete *it; + it = l->erase(it); + } + } +} + +SPLPEItem::SPLPEItem() + : SPItem() + , path_effects_enabled(1) + , path_effect_list(new PathEffectList()) + , lpe_modified_connection_list(new std::list()) + , current_path_effect(nullptr) + , lpe_helperpaths() +{ +} + +SPLPEItem::~SPLPEItem() = default; + +void SPLPEItem::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr( "inkscape:path-effect" ); + + SPItem::build(document, repr); +} + +void SPLPEItem::release() { + // disconnect all modified listeners: + + for (auto & mod_it : *this->lpe_modified_connection_list) + { + mod_it.disconnect(); + } + + delete this->lpe_modified_connection_list; + this->lpe_modified_connection_list = nullptr; + + clear_path_effect_list(this->path_effect_list); + // delete the list itself + delete this->path_effect_list; + this->path_effect_list = nullptr; + + SPItem::release(); +} + +void SPLPEItem::set(SPAttributeEnum key, gchar const* value) { + switch (key) { + case SP_ATTR_INKSCAPE_PATH_EFFECT: + { + this->current_path_effect = nullptr; + + // Disable the path effects while populating the LPE list + sp_lpe_item_enable_path_effects(this, false); + + // disconnect all modified listeners: + for (auto & mod_it : *this->lpe_modified_connection_list) + { + mod_it.disconnect(); + } + + this->lpe_modified_connection_list->clear(); + clear_path_effect_list(this->path_effect_list); + + // Parse the contents of "value" to rebuild the path effect reference list + if ( value ) { + std::istringstream iss(value); + std::string href; + + while (std::getline(iss, href, ';')) + { + Inkscape::LivePathEffect::LPEObjectReference *path_effect_ref = new Inkscape::LivePathEffect::LPEObjectReference(this); + + try { + path_effect_ref->link(href.c_str()); + } catch (Inkscape::BadURIException &e) { + g_warning("BadURIException when trying to find LPE: %s", e.what()); + path_effect_ref->unlink(); + delete path_effect_ref; + path_effect_ref = nullptr; + } + + this->path_effect_list->push_back(path_effect_ref); + + if ( path_effect_ref->lpeobject && path_effect_ref->lpeobject->get_lpe() ) { + // connect modified-listener + this->lpe_modified_connection_list->push_back( + path_effect_ref->lpeobject->connectModified(sigc::bind(sigc::ptr_fun(&lpeobject_ref_modified), this)) ); + } else { + // something has gone wrong in finding the right patheffect. + g_warning("Unknown LPE type specified, LPE stack effectively disabled"); + // keep the effect in the lpestack, so the whole stack is effectively disabled but maintained + } + } + } + + sp_lpe_item_enable_path_effects(this, true); + } + break; + + default: + SPItem::set(key, value); + break; + } +} + +void SPLPEItem::update(SPCtx* ctx, unsigned int flags) { + SPItem::update(ctx, flags); + + // update the helperpaths of all LPEs applied to the item + // TODO: re-add for the new node tool +} + +void SPLPEItem::modified(unsigned int flags) { + //stop update when modified and make the effect update on the LPE transform method if the effect require it + //if (SP_IS_GROUP(this) && (flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_USER_MODIFIED_FLAG_B)) { + // sp_lpe_item_update_patheffect(this, true, false); + //} +} + +Inkscape::XML::Node* SPLPEItem::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_EXT) { + if ( hasPathEffect() ) { + repr->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(*this->path_effect_list)); + } else { + repr->removeAttribute("inkscape:path-effect"); + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +/** + * returns true when LPE was successful. + */ +bool SPLPEItem::performPathEffect(SPCurve *curve, SPShape *current, bool is_clip_or_mask) { + + if (!curve) { + return false; + } + + if (this->hasPathEffect() && this->pathEffectsEnabled()) { + PathEffectList path_effect_list(*this->path_effect_list); + size_t path_effect_list_size = path_effect_list.size(); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return false; + } + + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (!lpe || !performOnePathEffect(curve, current, lpe, is_clip_or_mask)) { + return false; + } + if (path_effect_list_size != this->path_effect_list->size()) { + break; + } + } + } + return true; +} + +/** + * returns true when LPE was successful. + */ +bool SPLPEItem::performOnePathEffect(SPCurve *curve, SPShape *current, Inkscape::LivePathEffect::Effect *lpe, bool is_clip_or_mask) { + if (!lpe) { + /** \todo Investigate the cause of this. + * Not sure, but I think this can happen when an unknown effect type is specified... + */ + g_warning("SPLPEItem::performPathEffect - lpeobj with invalid lpe in the stack!"); + return false; + } + if (lpe->isVisible()) { + if (lpe->acceptsNumClicks() > 0 && !lpe->isReady()) { + // if the effect expects mouse input before being applied and the input is not finished + // yet, we don't alter the path + return false; + } + //if is not clip or mask or LPE apply to clip and mask + if (!is_clip_or_mask || lpe->apply_to_clippath_and_mask) { + lpe->setCurrentShape(current); + if (!SP_IS_GROUP(this)) { + lpe->pathvector_before_effect = curve->get_pathvector(); + } + // To Calculate BBox on shapes and nested LPE + current->setCurveInsync(curve); + if (lpe->lpeversion.param_getSVGValue() != "0") { // we are on 1 or up + current->bbox_vis_cache_is_valid = false; + current->bbox_geom_cache_is_valid = false; + } + // Groups have their doBeforeEffect called elsewhere + if (!SP_IS_GROUP(this) && !is_clip_or_mask) { + lpe->doBeforeEffect_impl(this); + } + + try { + lpe->doEffect(curve); + lpe->has_exception = false; + } + + catch (std::exception & e) { + g_warning("Exception during LPE %s execution. \n %s", lpe->getName().c_str(), e.what()); + if (SP_ACTIVE_DESKTOP && SP_ACTIVE_DESKTOP->messageStack()) { + SP_ACTIVE_DESKTOP->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("An exception occurred during execution of the Path Effect.") ); + } + lpe->doOnException(this); + return false; + } + + + if (!SP_IS_GROUP(this)) { + // To have processed the shape to doAfterEffect + current->setCurveInsync(curve); + if (curve) { + lpe->pathvector_after_effect = curve->get_pathvector(); + } + lpe->doAfterEffect_impl(this); + } + } + } + return true; +} + +/** + * returns true when LPE write unoptimiced + */ +bool SPLPEItem::optimizeTransforms() +{ + if (dynamic_cast(this)) { + return false; + } + auto* mask_path = this->getMaskObject(); + if(mask_path) { + return false; + } + auto* clip_path = this->getClipObject(); + if(clip_path) { + return false; + } + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + if (dynamic_cast(lpe) || + dynamic_cast(lpe) || + dynamic_cast(lpe) || + dynamic_cast(lpe) || + dynamic_cast(lpe) || + dynamic_cast(lpe)) { + return false; + } + } + } + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return !prefs->getBool("/options/preservetransform/value", false); +} + +/** + * notify tranbsform applied to a LPE + */ +void SPLPEItem::notifyTransform(Geom::Affine const &postmul) +{ + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe && !lpe->is_load) { + lpe->transform_multiply(postmul, false); + } + } + } +} + +// CPPIFY: make pure virtual +void SPLPEItem::update_patheffect(bool /*write*/) { + //throw; +} + +/** + * Calls any registered handlers for the update_patheffect action + */ +void +sp_lpe_item_update_patheffect (SPLPEItem *lpeitem, bool wholetree, bool write) +{ +#ifdef SHAPE_VERBOSE + g_message("sp_lpe_item_update_patheffect: %p\n", lpeitem); +#endif + g_return_if_fail (lpeitem != nullptr); + g_return_if_fail (SP_IS_OBJECT (lpeitem)); + g_return_if_fail (SP_IS_LPE_ITEM (lpeitem)); + + // Do not check for LPE item to allow LPE work on clips/mask + if (!lpeitem->pathEffectsEnabled()) + return; + + SPLPEItem *top = nullptr; + + if (wholetree) { + SPLPEItem *prev_parent = lpeitem; + SPLPEItem *parent = dynamic_cast(prev_parent->parent); + while (parent && parent->hasPathEffectRecursive()) { + prev_parent = parent; + parent = dynamic_cast(prev_parent->parent); + } + top = prev_parent; + } + else { + top = lpeitem; + } + top->update_patheffect(write); +} + +/** + * Gets called when any of the lpestack's lpeobject repr contents change: i.e. parameter change in any of the stacked LPEs + */ +static void +lpeobject_ref_modified(SPObject */*href*/, guint /*flags*/, SPLPEItem *lpeitem) +{ +#ifdef SHAPE_VERBOSE + g_message("lpeobject_ref_modified"); +#endif + sp_lpe_item_update_patheffect (lpeitem, true, true); +} + +static void +sp_lpe_item_create_original_path_recursive(SPLPEItem *lpeitem) +{ + g_return_if_fail(lpeitem != nullptr); + + SPClipPath *clip_path = SP_ITEM(lpeitem)->getClipObject(); + if(clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + SPLPEItem * clip_data = dynamic_cast(iter); + sp_lpe_item_create_original_path_recursive(clip_data); + } + } + + SPMask *mask_path = SP_ITEM(lpeitem)->getMaskObject(); + if(mask_path) { + std::vector mask_path_list = mask_path->childList(true); + for (auto iter : mask_path_list) { + SPLPEItem * mask_data = dynamic_cast(iter); + sp_lpe_item_create_original_path_recursive(mask_data); + } + } + if (SP_IS_GROUP(lpeitem)) { + std::vector item_list = sp_item_group_item_list(SP_GROUP(lpeitem)); + for (auto subitem : item_list) { + if (SP_IS_LPE_ITEM(subitem)) { + sp_lpe_item_create_original_path_recursive(SP_LPE_ITEM(subitem)); + } + } + } else if (SPPath * path = dynamic_cast(lpeitem)) { + Inkscape::XML::Node *pathrepr = path->getRepr(); + if ( !pathrepr->attribute("inkscape:original-d") ) { + if (gchar const * value = pathrepr->attribute("d")) { + Geom::PathVector pv = sp_svg_read_pathv(value); + pathrepr->setAttribute("inkscape:original-d", value); + SPCurve * original = new SPCurve(); + original->set_pathvector(pv); + path->setCurveBeforeLPE(original); + original->unref(); + } + } + } else if (SPShape * shape = dynamic_cast(lpeitem)) { + if (!shape->getCurveBeforeLPE(true)) { + shape->setCurveBeforeLPE(shape->getCurve()); + } + } +} + +static void +sp_lpe_item_cleanup_original_path_recursive(SPLPEItem *lpeitem, bool keep_paths, bool force, bool is_clip_mask) +{ + g_return_if_fail(lpeitem != nullptr); + SPItem *item = dynamic_cast(lpeitem); + if (!item) { + return; + } + SPGroup *group = dynamic_cast(lpeitem); + SPShape *shape = dynamic_cast(lpeitem); + SPPath *path = dynamic_cast(lpeitem); + SPClipPath *clip_path = item->getClipObject(); + if(clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + SPLPEItem* clip_data = dynamic_cast(iter); + if (clip_data) { + sp_lpe_item_cleanup_original_path_recursive(clip_data, keep_paths, lpeitem && !lpeitem->hasPathEffectRecursive(), true); + } + } + } + + SPMask *mask_path = item->getMaskObject(); + if(mask_path) { + std::vector mask_path_list = mask_path->childList(true); + for (auto iter : mask_path_list) { + SPLPEItem* mask_data = dynamic_cast(iter); + if (mask_data) { + sp_lpe_item_cleanup_original_path_recursive(mask_data, keep_paths, lpeitem && !lpeitem->hasPathEffectRecursive(), true); + } + } + } + + if (group) { + std::vector item_list = sp_item_group_item_list(SP_GROUP(lpeitem)); + for (auto iter : item_list) { + SPLPEItem* subitem = dynamic_cast(iter); + if (subitem) { + sp_lpe_item_cleanup_original_path_recursive(subitem, keep_paths); + } + } + } else if (path) { + Inkscape::XML::Node *repr = lpeitem->getRepr(); + if (repr->attribute("inkscape:original-d") && + !lpeitem->hasPathEffectRecursive() && + (!is_clip_mask || + ( is_clip_mask && force))) + { + if (!keep_paths) { + repr->setAttribute("d", repr->attribute("inkscape:original-d")); + } + repr->removeAttribute("inkscape:original-d"); + path->setCurveBeforeLPE(nullptr); + if (!(shape->getCurve(true)->get_segment_count())) { + repr->parent()->removeChild(repr); + } + } else { + if (!keep_paths) { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + } + } else if (shape) { + Inkscape::XML::Node *repr = lpeitem->getRepr(); + SPCurve * c_lpe = shape->getCurve(); + if (c_lpe) { + gchar *d_str = sp_svg_write_path(c_lpe->get_pathvector()); + if (d_str) { + if (!lpeitem->hasPathEffectRecursive() && + (!is_clip_mask || + ( is_clip_mask && force))) + { + if (!keep_paths) { + repr->removeAttribute("d"); + shape->setCurveBeforeLPE(nullptr); + } else { + const char * id = repr->attribute("id"); + const char * style = repr->attribute("style"); + // remember the position of the item + gint pos = shape->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = shape->getRepr()->parent(); + // remember class + char const *class_attr = shape->getRepr()->attribute("class"); + // remember title + gchar *title = shape->title(); + // remember description + gchar *desc = shape->desc(); + // remember transformation + gchar const *transform_str = shape->getRepr()->attribute("transform"); + // Mask + gchar const *mask_str = (gchar *) shape->getRepr()->attribute("mask"); + // Clip path + gchar const *clip_str = (gchar *) shape->getRepr()->attribute("clip-path"); + + /* Rotation center */ + gchar const *transform_center_x = shape->getRepr()->attribute("inkscape:transform-center-x"); + gchar const *transform_center_y = shape->getRepr()->attribute("inkscape:transform-center-y"); + + // remember highlight color + guint32 highlight_color = 0; + if (shape->isHighlightSet()) + highlight_color = shape->highlight_color(); + + // It's going to resurrect, so we delete without notifying listeners. + SPDocument * doc = shape->document; + shape->deleteObject(false); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + // restore id + repr->setAttribute("id", id); + // restore class + repr->setAttribute("class", class_attr); + // restore transform + repr->setAttribute("transform", transform_str); + // restore clip + repr->setAttribute("clip-path", clip_str); + // restore mask + repr->setAttribute("mask", mask_str); + // restore transform_center_x + repr->setAttribute("inkscape:transform-center-x", transform_center_x); + // restore transform_center_y + repr->setAttribute("inkscape:transform-center-y", transform_center_y); + //restore d + repr->setAttribute("d", d_str); + //restore style + repr->setAttribute("style", style); + // add the new repr to the parent + parent->appendChild(repr); + SPObject* newObj = doc->getObjectByRepr(repr); + if (title && newObj) { + newObj->setTitle(title); + g_free(title); + } + if (desc && newObj) { + newObj->setDesc(desc); + g_free(desc); + } + if (highlight_color && newObj) { + SP_ITEM(newObj)->setHighlightColor( highlight_color ); + } + // move to the saved position + repr->setPosition(pos > 0 ? pos : 0); + Inkscape::GC::release(repr); + lpeitem = dynamic_cast(newObj); + } + } else { + if (!keep_paths) { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + } + c_lpe->unref(); + } + } + } +} + +void SPLPEItem::addPathEffect(std::string value, bool reset) +{ + if (!value.empty()) { + // Apply the path effects here because in the casse of a group, lpe->resetDefaults + // needs that all the subitems have their effects applied + sp_lpe_item_update_patheffect(this, false, true); + + // Disable the path effects while preparing the new lpe + sp_lpe_item_enable_path_effects(this, false); + + // Add the new reference to the list of LPE references + HRefList hreflist; + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) + { + hreflist.push_back( std::string((*it)->lpeobject_href) ); + } + hreflist.push_back(value); // C++11: should be emplace_back std::move'd (also the reason why passed by value to addPathEffect) + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); + // Make sure that ellipse is stored as + if( SP_IS_GENERICELLIPSE(this)) { + SP_GENERICELLIPSE(this)->write( this->getRepr()->document(), this->getRepr(), SP_OBJECT_WRITE_EXT ); + } + + + LivePathEffectObject *lpeobj = this->path_effect_list->back()->lpeobject; + if (lpeobj && lpeobj->get_lpe()) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + // Ask the path effect to reset itself if it doesn't have parameters yet + if (reset) { + // has to be called when all the subitems have their lpes applied + lpe->resetDefaults(this); + } + // Moved here to fix #1299461, we can call previous function twice after + // if anyone find necessary + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(this); + // perform this once when the effect is applied + lpe->doOnApply_impl(this); + } + + //Enable the path effects now that everything is ready to apply the new path effect + sp_lpe_item_enable_path_effects(this, true); + + // Apply the path effect + sp_lpe_item_update_patheffect(this, true, true); + } +} + +void SPLPEItem::addPathEffect(LivePathEffectObject * new_lpeobj) +{ + const gchar * repr_id = new_lpeobj->getRepr()->attribute("id"); + gchar *hrefstr = g_strdup_printf("#%s", repr_id); + this->addPathEffect(hrefstr, false); + g_free(hrefstr); +} + +/** + * If keep_path is true, the item should not be updated, effectively 'flattening' the LPE. + */ +void SPLPEItem::removeCurrentPathEffect(bool keep_paths) +{ + Inkscape::LivePathEffect::LPEObjectReference* lperef = this->getCurrentLPEReference(); + if (!lperef) { + return; + } + if (Inkscape::LivePathEffect::Effect* effect_ = this->getCurrentLPE()) { + effect_->keep_paths = keep_paths; + effect_->doOnRemove(this); + this->path_effect_list->remove(lperef); //current lpe ref is always our 'own' pointer from the path_effect_list + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(*this->path_effect_list)); + if (!keep_paths) { + // Make sure that ellipse is stored as or if possible. + if( SP_IS_GENERICELLIPSE(this)) { + SP_GENERICELLIPSE(this)->write( this->getRepr()->document(), this->getRepr(), SP_OBJECT_WRITE_EXT ); + } + } + sp_lpe_item_cleanup_original_path_recursive(this, keep_paths); + } +} + +/** + * If keep_path is true, the item should not be updated, effectively 'flattening' the LPE. + */ +void SPLPEItem::removeAllPathEffects(bool keep_paths) +{ + if (keep_paths) { + if (path_effect_list->empty()) { + return; + } + } + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + if (!lperef) { + continue; + } + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect * lpe = lpeobj->get_lpe(); + if (lpe) { + lpe->keep_paths = keep_paths; + lpe->doOnRemove(this); + } + } + } + this->path_effect_list->clear(); + this->removeAttribute("inkscape:path-effect"); + if (!keep_paths) { + // Make sure that ellipse is stored as or if possible. + if (SP_IS_GENERICELLIPSE(this)) { + SP_GENERICELLIPSE(this)->write(this->getRepr()->document(), this->getRepr(), SP_OBJECT_WRITE_EXT); + } + } + sp_lpe_item_cleanup_original_path_recursive(this, keep_paths); + +} + +void SPLPEItem::downCurrentPathEffect() +{ + Inkscape::LivePathEffect::LPEObjectReference* lperef = getCurrentLPEReference(); + if (!lperef) + return; + + PathEffectList new_list = *this->path_effect_list; + PathEffectList::iterator cur_it = find( new_list.begin(), new_list.end(), lperef ); + if (cur_it != new_list.end()) { + PathEffectList::iterator down_it = cur_it; + ++down_it; + if (down_it != new_list.end()) { // perhaps current effect is already last effect + std::iter_swap(cur_it, down_it); + } + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(new_list)); + + sp_lpe_item_cleanup_original_path_recursive(this, false); +} + +void SPLPEItem::upCurrentPathEffect() +{ + Inkscape::LivePathEffect::LPEObjectReference* lperef = getCurrentLPEReference(); + if (!lperef) + return; + + PathEffectList new_list = *this->path_effect_list; + PathEffectList::iterator cur_it = find( new_list.begin(), new_list.end(), lperef ); + if (cur_it != new_list.end() && cur_it != new_list.begin()) { + PathEffectList::iterator up_it = cur_it; + --up_it; + std::iter_swap(cur_it, up_it); + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", patheffectlist_svg_string(new_list)); + + sp_lpe_item_cleanup_original_path_recursive(this, false); +} + +/** used for shapes so they can see if they should also disable shape calculation and read from d= */ +bool SPLPEItem::hasBrokenPathEffect() const +{ + if (path_effect_list->empty()) { + return false; + } + + // go through the list; if some are unknown or invalid, return true + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj || !lpeobj->get_lpe()) { + return true; + } + } + + return false; +} + + + +bool SPLPEItem::hasPathEffectOfType(int const type, bool is_ready) const +{ + if (path_effect_list->empty()) { + return false; + } + + for (PathEffectList::const_iterator it = path_effect_list->begin(); it != path_effect_list->end(); ++it) + { + LivePathEffectObject const *lpeobj = (*it)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const* lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + if (is_ready || lpe->isReady()) { + return true; + } + } + } + } + + return false; +} + +/** + * returns true when any LPE apply to clip or mask. + */ +bool SPLPEItem::hasPathEffectOnClipOrMask(SPLPEItem * shape) const +{ + if (shape->hasPathEffectRecursive()) { + return true; + } + if (!path_effect_list || path_effect_list->empty()) { + return false; + } + + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + continue; + } + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (lpe->apply_to_clippath_and_mask) { + return true; + } + } + return false; +} + +/** + * returns true when any LPE apply to clip or mask. recursive mode + */ +bool SPLPEItem::hasPathEffectOnClipOrMaskRecursive(SPLPEItem * shape) const +{ + SPLPEItem * parent_lpe_item = dynamic_cast(parent); + if (parent_lpe_item) { + return hasPathEffectOnClipOrMask(shape) || parent_lpe_item->hasPathEffectOnClipOrMaskRecursive(shape); + } + else { + return hasPathEffectOnClipOrMask(shape); + } +} + +bool SPLPEItem::hasPathEffect() const +{ + if (!path_effect_list || path_effect_list->empty()) { + return false; + } + + // go through the list; if some are unknown or invalid, we are not an LPE item! + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj || !lpeobj->get_lpe()) { + return false; + } + } + + return true; +} + +bool SPLPEItem::hasPathEffectRecursive() const +{ + SPLPEItem * parent_lpe_item = dynamic_cast(parent); + if (parent_lpe_item) { + return hasPathEffect() || parent_lpe_item->hasPathEffectRecursive(); + } + else { + return hasPathEffect(); + } +} + +void +SPLPEItem::resetClipPathAndMaskLPE(bool fromrecurse) +{ + if (fromrecurse) { + SPGroup* group = dynamic_cast(this); + SPShape* shape = dynamic_cast(this); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto iter2 : item_list) { + SPLPEItem * subitem = dynamic_cast(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync( shape->getCurveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + return; + } + SPClipPath *clip_path = this->getClipObject(); + if(clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + SPGroup* group = dynamic_cast(iter); + SPShape* shape = dynamic_cast(iter); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto iter2 : item_list) { + SPLPEItem * subitem = dynamic_cast(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync( shape->getCurveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + } + } + SPMask *mask = this->getMaskObject(); + if(mask) { + std::vector mask_list = mask->childList(true); + for (auto iter : mask_list) { + SPGroup* group = dynamic_cast(iter); + SPShape* shape = dynamic_cast(iter); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto iter2 : item_list) { + SPLPEItem * subitem = dynamic_cast(iter2); + if (subitem) { + subitem->resetClipPathAndMaskLPE(true); + } + } + } else if (shape) { + shape->setCurveInsync( shape->getCurveForEdit()); + if (!hasPathEffectOnClipOrMaskRecursive(shape)) { + shape->removeAttribute("inkscape:original-d"); + shape->setCurveBeforeLPE(nullptr); + } else { + // make sure there is an original-d for paths!!! + sp_lpe_item_create_original_path_recursive(shape); + } + } + } + } +} + +void +SPLPEItem::applyToClipPath(SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + if (lpe && !lpe->apply_to_clippath_and_mask) { + return; + } + SPClipPath *clip_path = to->getClipObject(); + if(clip_path) { + std::vector clip_path_list = clip_path->childList(true); + for (auto clip_data : clip_path_list) { + applyToClipPathOrMask(SP_ITEM(clip_data), to, lpe); + } + } +} + +void +SPLPEItem::applyToMask(SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + if (lpe && !lpe->apply_to_clippath_and_mask) { + return; + } + SPMask *mask = to->getMaskObject(); + if(mask) { + std::vector mask_list = mask->childList(true); + for (auto mask_data : mask_list) { + applyToClipPathOrMask(SP_ITEM(mask_data), to, lpe); + } + } +} + +void +SPLPEItem::applyToClipPathOrMask(SPItem *clip_mask, SPItem* to, Inkscape::LivePathEffect::Effect *lpe) +{ + SPGroup* group = dynamic_cast(clip_mask); + SPShape* shape = dynamic_cast(clip_mask); + SPLPEItem* tolpe = dynamic_cast(to); + SPRoot *root = this->document->getRoot(); + if (group) { + std::vector item_list = sp_item_group_item_list(group); + for (auto subitem : item_list) { + applyToClipPathOrMask(subitem, to, lpe); + } + } else if (shape) { + if (sp_version_inside_range(root->version.inkscape, 0, 1, 0, 92)) { + shape->removeAttribute("inkscape:original-d"); + } else { + SPCurve * c = shape->getCurve(); + if (c) { + bool success = false; + try { + if (lpe) { + success = this->performOnePathEffect(c, shape, lpe, true); + } else { + success = this->performPathEffect(c, shape, true); + } + } catch (std::exception & e) { + g_warning("Exception during LPE execution. \n %s", e.what()); + if (SP_ACTIVE_DESKTOP && SP_ACTIVE_DESKTOP->messageStack()) { + SP_ACTIVE_DESKTOP->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("An exception occurred during execution of the Path Effect.") ); + } + success = false; + } + if (success && c) { + shape->setCurveInsync(c); + gchar *str = sp_svg_write_path(c->get_pathvector()); + shape->setAttribute("d", str); + g_free(str); + } else { + // LPE was unsuccessful or doeffect stack return null.. Read the old 'd'-attribute. + if (gchar const * value = shape->getAttribute("d")) { + Geom::PathVector pv = sp_svg_read_pathv(value); + SPCurve *oldcurve = new (std::nothrow) SPCurve(pv); + if (oldcurve) { + SP_SHAPE(clip_mask)->setCurve(oldcurve); + oldcurve->unref(); + } + } + } + if (c) { + c->unref(); + } + shape->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } +} + +Inkscape::LivePathEffect::Effect* +SPLPEItem::getPathEffectOfType(int type) +{ + PathEffectList path_effect_list(*this->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect* lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + return lpe; + } + } + } + return nullptr; +} + +Inkscape::LivePathEffect::Effect const* +SPLPEItem::getPathEffectOfType(int type) const +{ + std::list::const_iterator i; + for (i = path_effect_list->begin(); i != path_effect_list->end(); ++i) { + LivePathEffectObject const *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect const *lpe = lpeobj->get_lpe(); + if (lpe && (lpe->effectType() == type)) { + return lpe; + } + } + } + return nullptr; +} + +void SPLPEItem::editNextParamOncanvas(SPDesktop *dt) +{ + Inkscape::LivePathEffect::LPEObjectReference *lperef = this->getCurrentLPEReference(); + if (lperef && lperef->lpeobject && lperef->lpeobject->get_lpe()) { + lperef->lpeobject->get_lpe()->editNextParamOncanvas(this, dt); + } +} + +void SPLPEItem::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPItem::child_added(child, ref); + + if (this->hasPathEffectRecursive()) { + SPObject *ochild = this->get_child_by_repr(child); + + if ( ochild && SP_IS_LPE_ITEM(ochild) ) { + sp_lpe_item_create_original_path_recursive(SP_LPE_ITEM(ochild)); + } + } +} +void SPLPEItem::remove_child(Inkscape::XML::Node * child) { + if (this->hasPathEffectRecursive()) { + SPObject *ochild = this->get_child_by_repr(child); + + if ( ochild && SP_IS_LPE_ITEM(ochild) ) { + sp_lpe_item_cleanup_original_path_recursive(SP_LPE_ITEM(ochild), false); + } + } + + SPItem::remove_child(child); +} + +static std::string patheffectlist_svg_string(PathEffectList const & list) +{ + HRefList hreflist; + + for (auto it : list) + { + hreflist.push_back( std::string(it->lpeobject_href) ); // C++11: use emplace_back + } + + return hreflist_svg_string(hreflist); +} + +/** + * THE function that should be used to generate any patheffectlist string. + * one of the methods to change the effect list: + * - create temporary href list + * - populate the templist with the effects from the old list that you want to have and their order + * - call this function with temp list as param + */ +static std::string hreflist_svg_string(HRefList const & list) +{ + std::string r; + bool semicolon_first = false; + + for (const auto & it : list) + { + if (semicolon_first) { + r += ';'; + } + + semicolon_first = true; + + r += it; + } + + return r; +} + +// Return a copy of the effect list +PathEffectList SPLPEItem::getEffectList() +{ + return *path_effect_list; +} + +// Return a copy of the effect list +PathEffectList const SPLPEItem::getEffectList() const +{ + return *path_effect_list; +} + +Inkscape::LivePathEffect::LPEObjectReference* SPLPEItem::getCurrentLPEReference() +{ + if (!this->current_path_effect && !this->path_effect_list->empty()) { + setCurrentPathEffect(this->path_effect_list->back()); + } + + return this->current_path_effect; +} + +Inkscape::LivePathEffect::Effect* SPLPEItem::getCurrentLPE() +{ + Inkscape::LivePathEffect::LPEObjectReference* lperef = getCurrentLPEReference(); + + if (lperef && lperef->lpeobject) + return lperef->lpeobject->get_lpe(); + else + return nullptr; +} + +bool SPLPEItem::setCurrentPathEffect(Inkscape::LivePathEffect::LPEObjectReference* lperef) +{ + for (auto & it : *path_effect_list) { + if (it->lpeobject_repr == lperef->lpeobject_repr) { + this->current_path_effect = it; // current_path_effect should always be a pointer from the path_effect_list ! + return true; + } + } + + return false; +} + +/** + * Writes a new "inkscape:path-effect" string to xml, where the old_lpeobjects are substituted by the new ones. + * Note that this method messes up the item's \c PathEffectList. + */ +void SPLPEItem::replacePathEffects( std::vector const &old_lpeobjs, + std::vector const &new_lpeobjs ) +{ + HRefList hreflist; + for (PathEffectList::const_iterator it = this->path_effect_list->begin(); it != this->path_effect_list->end(); ++it) + { + LivePathEffectObject const * current_lpeobj = (*it)->lpeobject; + std::vector::const_iterator found_it(std::find(old_lpeobjs.begin(), old_lpeobjs.end(), current_lpeobj)); + + if ( found_it != old_lpeobjs.end() ) { + std::vector::difference_type found_index = std::distance (old_lpeobjs.begin(), found_it); + const gchar * repr_id = new_lpeobjs[found_index]->getRepr()->attribute("id"); + gchar *hrefstr = g_strdup_printf("#%s", repr_id); + hreflist.push_back( std::string(hrefstr) ); + g_free(hrefstr); + } + else { + hreflist.push_back( std::string((*it)->lpeobject_href) ); + } + } + + this->setAttributeOrRemoveIfEmpty("inkscape:path-effect", hreflist_svg_string(hreflist)); +} + +/** + * Check all effects in the stack if they are used by other items, and fork them if so. + * It is not recommended to fork the effects by yourself calling LivePathEffectObject::fork_private_if_necessary, + * use this method instead. + * Returns true if one or more effects were forked; returns false if nothing was done. + */ +bool SPLPEItem::forkPathEffectsIfNecessary(unsigned int nr_of_allowed_users, bool recursive) +{ + bool forked = false; + SPGroup * group = dynamic_cast(this); + if (group && recursive) { + std::vector item_list = sp_item_group_item_list(group); + for (auto child:item_list) { + SPLPEItem *lpeitem = dynamic_cast(child); + if (lpeitem && lpeitem->forkPathEffectsIfNecessary(nr_of_allowed_users, recursive)) { + forked = true; + } + } + } + + if ( this->hasPathEffect() ) { + // If one of the path effects is used by 2 or more items, fork it + // so that each object has its own independent copy of the effect. + // Note: replacing path effects messes up the path effect list + + // Clones of the LPEItem will increase the refcount of the lpeobjects. + // Therefore, nr_of_allowed_users should be increased with the number of clones (i.e. refs to the lpeitem) + nr_of_allowed_users += this->hrefcount; + + std::vector old_lpeobjs, new_lpeobjs; + PathEffectList effect_list = this->getEffectList(); + for (auto & it : effect_list) + { + LivePathEffectObject *lpeobj = it->lpeobject; + if (lpeobj) { + LivePathEffectObject *forked_lpeobj = lpeobj->fork_private_if_necessary(nr_of_allowed_users); + if (forked_lpeobj && forked_lpeobj != lpeobj) { + forked = true; + old_lpeobjs.push_back(lpeobj); + new_lpeobjs.push_back(forked_lpeobj); + forked_lpeobj->get_lpe()->is_load = true; + } + } + } + + if (forked) { + this->replacePathEffects(old_lpeobjs, new_lpeobjs); + } + } + + return forked; +} + +// Enable or disable the path effects of the item. +void sp_lpe_item_enable_path_effects(SPLPEItem *lpeitem, bool enable) +{ + if (enable) { + lpeitem->path_effects_enabled++; + } + else { + lpeitem->path_effects_enabled--; + } +} + +// Are the path effects enabled on this item ? +bool SPLPEItem::pathEffectsEnabled() const +{ + return path_effects_enabled > 0; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-lpe-item.h b/src/object/sp-lpe-item.h new file mode 100644 index 0000000..907b1f8 --- /dev/null +++ b/src/object/sp-lpe-item.h @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_LPE_ITEM_H_SEEN +#define SP_LPE_ITEM_H_SEEN + +/** \file + * Base class for live path effect items + */ +/* + * Authors: + * Johan Engelen + * Bastien Bouclet + * + * Copyright (C) 2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "sp-item.h" + +#define SP_LPE_ITEM(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_LPE_ITEM(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class LivePathEffectObject; +class SPCurve; +class SPShape; +class SPDesktop; + +namespace Inkscape{ + namespace Display { + class TemporaryItem; + } + namespace LivePathEffect{ + class LPEObjectReference; + class Effect; + } +} + +typedef std::list PathEffectList; + +class SPLPEItem : public SPItem { +public: + SPLPEItem(); + ~SPLPEItem() override; + + int path_effects_enabled; + + PathEffectList* path_effect_list; + std::list *lpe_modified_connection_list; // this list contains the connections for listening to lpeobject parameter changes + + Inkscape::LivePathEffect::LPEObjectReference* current_path_effect; + std::vector lpe_helperpaths; + + void replacePathEffects( std::vector const &old_lpeobjs, + std::vector const &new_lpeobjs ); + + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, char const* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + virtual void update_patheffect(bool write); + bool optimizeTransforms(); + void notifyTransform(Geom::Affine const &postmul); + bool performPathEffect(SPCurve *curve, SPShape *current, bool is_clip_or_mask = false); + bool performOnePathEffect(SPCurve *curve, SPShape *current, Inkscape::LivePathEffect::Effect *lpe, bool is_clip_or_mask = false); + bool pathEffectsEnabled() const; + bool hasPathEffect() const; + bool hasPathEffectOfType(int const type, bool is_ready = true) const; + bool hasPathEffectRecursive() const; + bool hasPathEffectOnClipOrMask(SPLPEItem * shape) const; + bool hasPathEffectOnClipOrMaskRecursive(SPLPEItem * shape) const; + Inkscape::LivePathEffect::Effect* getPathEffectOfType(int type); + Inkscape::LivePathEffect::Effect const* getPathEffectOfType(int type) const; + bool hasBrokenPathEffect() const; + + PathEffectList getEffectList(); + PathEffectList const getEffectList() const; + + void downCurrentPathEffect(); + void upCurrentPathEffect(); + Inkscape::LivePathEffect::LPEObjectReference* getCurrentLPEReference(); + Inkscape::LivePathEffect::Effect* getCurrentLPE(); + bool setCurrentPathEffect(Inkscape::LivePathEffect::LPEObjectReference* lperef); + void removeCurrentPathEffect(bool keep_paths); + void removeAllPathEffects(bool keep_paths); + void addPathEffect(std::string value, bool reset); + void addPathEffect(LivePathEffectObject * new_lpeobj); + void resetClipPathAndMaskLPE(bool fromrecurse = false); + void applyToMask(SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + void applyToClipPath(SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + void applyToClipPathOrMask(SPItem * clip_mask, SPItem* to, Inkscape::LivePathEffect::Effect *lpe = nullptr); + bool forkPathEffectsIfNecessary(unsigned int nr_of_allowed_users = 1, bool recursive = true); + + void editNextParamOncanvas(SPDesktop *dt); +}; +void sp_lpe_item_update_patheffect (SPLPEItem *lpeitem, bool wholetree, bool write); // careful, class already has method with *very* similar name! +void sp_lpe_item_enable_path_effects(SPLPEItem *lpeitem, bool enable); + +#endif /* !SP_LPE_ITEM_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-marker-loc.h b/src/object/sp-marker-loc.h new file mode 100644 index 0000000..6b88d69 --- /dev/null +++ b/src/object/sp-marker-loc.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_MARKER_LOC_H +#define SEEN_SP_MARKER_LOC_H + +/** + * These enums are to allow us to have 4-element arrays that represent a set of marker locations + * (all, start, mid, and end). This allows us to iterate through the array in places where we need + * to do a process across all of the markers, instead of separate code stanzas for each. + * + * IMPORTANT: the code assumes that the locations have the values as written below! so don't change the values!!! + */ +enum SPMarkerLoc { + SP_MARKER_LOC = 0, + SP_MARKER_LOC_START = 1, + SP_MARKER_LOC_MID = 2, + SP_MARKER_LOC_END = 3, + SP_MARKER_LOC_QTY = 4 +}; + + +#endif /* !SEEN_SP_MARKER_LOC_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-marker.cpp b/src/object/sp-marker.cpp new file mode 100644 index 0000000..fb694df --- /dev/null +++ b/src/object/sp-marker.cpp @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Bryce Harrington + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * 2004-2006 Bryce Harrington + * 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include <2geom/affine.h> +#include <2geom/transforms.h> +#include "svg/svg.h" +#include "display/drawing-group.h" +#include "xml/repr.h" +#include "attributes.h" +#include "document.h" +#include "preferences.h" + +#include "sp-marker.h" +#include "sp-defs.h" + +class SPMarkerView { + +public: + + SPMarkerView() = default;; + ~SPMarkerView() { + for (auto & item : items) { + delete item; + } + items.clear(); + } + std::vector items; +}; + +SPMarker::SPMarker() : SPGroup(), SPViewBox(), + markerUnits_set(0), + markerUnits(0), + refX(), + refY(), + markerWidth(), + markerHeight(), + orient_set(0), + orient_mode(MARKER_ORIENT_ANGLE) +{ + // cppcheck-suppress useInitializationList + orient = 0; +} + +/** + * Initializes an SPMarker object. This notes the marker's viewBox is + * not set and initializes the marker's c2p identity matrix. + */ + +SPMarker::~SPMarker() = default; + +/** + * Virtual build callback for SPMarker. + * + * This is to be invoked immediately after creation of an SPMarker. This + * method fills an SPMarker object with its SVG attributes, and calls the + * parent class' build routine to attach the object to its document and + * repr. The result will be creation of the whole document tree. + * + * \see SPObject::build() + */ +void SPMarker::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr( "markerUnits" ); + this->readAttr( "refX" ); + this->readAttr( "refY" ); + this->readAttr( "markerWidth" ); + this->readAttr( "markerHeight" ); + this->readAttr( "orient" ); + this->readAttr( "viewBox" ); + this->readAttr( "preserveAspectRatio" ); + this->readAttr( "style" ); + + SPGroup::build(document, repr); +} + + +/** + * Removes, releases and unrefs all children of object + * + * This is the inverse of sp_marker_build(). It must be invoked as soon + * as the marker is removed from the tree, even if it is still referenced + * by other objects. It hides and removes any views of the marker, then + * calls the parent classes' release function to deregister the object + * and release its SPRepr bindings. The result will be the destruction + * of the entire document tree. + * + * \see SPObject::release() + */ +void SPMarker::release() { + + std::map::iterator it; + for (it = views_map.begin(); it != views_map.end(); ++it) { + SPGroup::hide( it->first ); + } + views_map.clear(); + + SPGroup::release(); +} + + +void SPMarker::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_MARKERUNITS: + this->markerUnits_set = FALSE; + this->markerUnits = SP_MARKER_UNITS_STROKEWIDTH; + + if (value) { + if (!strcmp (value, "strokeWidth")) { + this->markerUnits_set = TRUE; + } else if (!strcmp (value, "userSpaceOnUse")) { + this->markerUnits = SP_MARKER_UNITS_USERSPACEONUSE; + this->markerUnits_set = TRUE; + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_REFX: + this->refX.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_REFY: + this->refY.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_MARKERWIDTH: + this->markerWidth.readOrUnset(value, SVGLength::NONE, 3.0, 3.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_MARKERHEIGHT: + this->markerHeight.readOrUnset(value, SVGLength::NONE, 3.0, 3.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_ORIENT: + this->orient_set = FALSE; + this->orient_mode = MARKER_ORIENT_ANGLE; + this->orient = 0.0; + + if (value) { + if (!strcmp (value, "auto")) { + this->orient_mode = MARKER_ORIENT_AUTO; + this->orient_set = TRUE; + } else if (!strcmp (value, "auto-start-reverse")) { + this->orient_mode = MARKER_ORIENT_AUTO_START_REVERSE; + this->orient_set = TRUE; + } else { + orient.readOrUnset(value); + if (orient._set) { + this->orient_mode = MARKER_ORIENT_ANGLE; + this->orient_set = orient._set; + } + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_VIEWBOX: + set_viewBox( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +void SPMarker::update(SPCtx *ctx, guint flags) { + + SPItemCtx ictx; + + // Copy parent context + ictx.flags = ctx->flags; + + // Initialize transformations + ictx.i2doc = Geom::identity(); + ictx.i2vp = Geom::identity(); + + // Set up viewport + ictx.viewport = Geom::Rect::from_xywh(0, 0, this->markerWidth.computed, this->markerHeight.computed); + + SPItemCtx rctx = get_rctx( &ictx ); + + // Shift according to refX, refY + Geom::Point ref( this->refX.computed, this->refY.computed ); + ref *= c2p; + this->c2p = this->c2p * Geom::Translate( -ref ); + + // And invoke parent method + SPGroup::update((SPCtx *) &rctx, flags); + + // As last step set additional transform of drawing group + std::map::iterator it; + for (it = views_map.begin(); it != views_map.end(); ++it) { + for (auto & item : it->second.items) { + if (item) { + Inkscape::DrawingGroup *g = dynamic_cast(item); + g->setChildTransform(this->c2p); + } + } + } +} + +Inkscape::XML::Node* SPMarker::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:marker"); + } + + if (this->markerUnits_set) { + if (this->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + repr->setAttribute("markerUnits", "strokeWidth"); + } else { + repr->setAttribute("markerUnits", "userSpaceOnUse"); + } + } else { + repr->removeAttribute("markerUnits"); + } + + if (this->refX._set) { + sp_repr_set_svg_double(repr, "refX", this->refX.computed); + } else { + repr->removeAttribute("refX"); + } + + if (this->refY._set) { + sp_repr_set_svg_double (repr, "refY", this->refY.computed); + } else { + repr->removeAttribute("refY"); + } + + if (this->markerWidth._set) { + sp_repr_set_svg_double (repr, "markerWidth", this->markerWidth.computed); + } else { + repr->removeAttribute("markerWidth"); + } + + if (this->markerHeight._set) { + sp_repr_set_svg_double (repr, "markerHeight", this->markerHeight.computed); + } else { + repr->removeAttribute("markerHeight"); + } + + if (this->orient_set) { + if (this->orient_mode == MARKER_ORIENT_AUTO) { + repr->setAttribute("orient", "auto"); + } else if (this->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + repr->setAttribute("orient", "auto-start-reverse"); + } else { + sp_repr_set_css_double(repr, "orient", this->orient.computed); + } + } else { + repr->removeAttribute("orient"); + } + + /* fixme: */ + //XML Tree being used directly here while it shouldn't be.... + repr->setAttribute("viewBox", this->getRepr()->attribute("viewBox")); + //XML Tree being used directly here while it shouldn't be.... + repr->setAttribute("preserveAspectRatio", this->getRepr()->attribute("preserveAspectRatio")); + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem* SPMarker::show(Inkscape::Drawing &/*drawing*/, unsigned int /*key*/, unsigned int /*flags*/) { + // Markers in tree are never shown directly even if outside of . + return nullptr; +} + +Inkscape::DrawingItem* SPMarker::private_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + return SPGroup::show(drawing, key, flags); +} + +void SPMarker::hide(unsigned int key) { + // CPPIFY: correct? + SPGroup::hide(key); +} + +Geom::OptRect SPMarker::bbox(Geom::Affine const &/*transform*/, SPItem::BBoxType /*type*/) const { + return Geom::OptRect(); +} + +void SPMarker::print(SPPrintContext* /*ctx*/) { + +} + +/* fixme: Remove link if zero-sized (Lauris) */ + +/** + * Removes any SPMarkerViews that a marker has with a specific key. + * Set up the DrawingItem array's size in the specified SPMarker's SPMarkerView. + * This is called from sp_shape_update() for shapes that have markers. It + * removes the old view of the marker and establishes a new one, registering + * it with the marker's list of views for future updates. + * + * \param marker Marker to create views in. + * \param key Key to give each SPMarkerView. + * \param size Number of DrawingItems to put in the SPMarkerView. + */ +// If marker views are always created in order, then this function could be eliminated +// by doing the push_back in sp_marker_show_instance. +void +sp_marker_show_dimension (SPMarker *marker, unsigned int key, unsigned int size) +{ + std::map::iterator it = marker->views_map.find(key); + if (it != marker->views_map.end()) { + if (it->second.items.size() != size ) { + // Need to change size of vector! (We should not really need to do this.) + marker->hide(key); + it->second.items.clear(); + for (unsigned int i = 0; i < size; ++i) { + it->second.items.push_back(nullptr); + } + } + } else { + marker->views_map[key] = SPMarkerView(); + for (unsigned int i = 0; i < size; ++i) { + marker->views_map[key].items.push_back(nullptr); + } + } +} + +/** + * Shows an instance of a marker. This is called during sp_shape_update_marker_view() + * show and transform a child item in the drawing for all views with the given key. + */ +Inkscape::DrawingItem * +sp_marker_show_instance ( SPMarker *marker, Inkscape::DrawingItem *parent, + unsigned int key, unsigned int pos, + Geom::Affine const &base, float linewidth) +{ + // Do not show marker if linewidth == 0 and markerUnits == strokeWidth + // otherwise Cairo will fail to render anything on the tile + // that contains the "degenerate" marker. + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH && linewidth == 0) { + return nullptr; + } + + std::map::iterator it = marker->views_map.find(key); + if (it == marker->views_map.end()) { + // Key not found + return nullptr; + } + + SPMarkerView *view = &(it->second); + if (pos >= view->items.size() ) { + // Position index too large, doesn't exist. + return nullptr; + } + + // If not already created + if (view->items[pos] == nullptr) { + + /* Parent class ::show method */ + view->items[pos] = marker->private_show(parent->drawing(), key, SP_ITEM_REFERENCE_FLAGS); + + if (view->items[pos]) { + /* fixme: Position (Lauris) */ + parent->prependChild(view->items[pos]); + Inkscape::DrawingGroup *g = dynamic_cast(view->items[pos]); + if (g) g->setChildTransform(marker->c2p); + } + } + + if (view->items[pos]) { + Geom::Affine m; + if (marker->orient_mode == MARKER_ORIENT_AUTO) { + m = base; + } else if (marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + // m = Geom::Rotate::from_degrees( 180.0 ) * base; + // Rotating is done at rendering time if necessary + m = base; + } else { + /* fixme: Orient units (Lauris) */ + m = Geom::Rotate::from_degrees(marker->orient.computed); + m *= Geom::Translate(base.translation()); + } + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + m = Geom::Scale(linewidth) * m; + } + view->items[pos]->setTransform(m); + } + + return view->items[pos]; +} + +/** + * Hides/removes all views of the given marker that have key 'key'. + * This replaces SPItem implementation because we have our own views + * \param key SPMarkerView key to hide. + */ +void +sp_marker_hide (SPMarker *marker, unsigned int key) +{ + marker->hide(key); + marker->views_map.erase(key); +} + + +const gchar *generate_marker(std::vector &reprs, Geom::Rect bounds, SPDocument *document, Geom::Point center, Geom::Affine move) +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:marker"); + + // Uncommenting this will make the marker fixed-size independent of stroke width. + // Commented out for consistency with standard markers which scale when you change + // stroke width: + //repr->setAttribute("markerUnits", "userSpaceOnUse"); + + sp_repr_set_svg_double(repr, "markerWidth", bounds.dimensions()[Geom::X]); + sp_repr_set_svg_double(repr, "markerHeight", bounds.dimensions()[Geom::Y]); + sp_repr_set_svg_double(repr, "refX", center[Geom::X]); + sp_repr_set_svg_double(repr, "refY", center[Geom::Y]); + + repr->setAttribute("orient", "auto"); + + defsrepr->appendChild(repr); + const gchar *mark_id = repr->attribute("id"); + SPObject *mark_object = document->getObjectById(mark_id); + + for (auto node : reprs){ + SPItem *copy = SP_ITEM(mark_object->appendChildRepr(node)); + + Geom::Affine dup_transform; + if (!sp_svg_transform_read (node->attribute("transform"), &dup_transform)) + dup_transform = Geom::identity(); + dup_transform *= move; + + copy->doWriteTransform(dup_transform); + } + + Inkscape::GC::release(repr); + return mark_id; +} + +SPObject *sp_marker_fork_if_necessary(SPObject *marker) +{ + if (marker->hrefcount < 2) { + return marker; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean colorStock = prefs->getBool("/options/markers/colorStockMarkers", true); + gboolean colorCustom = prefs->getBool("/options/markers/colorCustomMarkers", false); + const gchar *stock = marker->getRepr()->attribute("inkscape:isstock"); + gboolean isStock = (!stock || !strcmp(stock,"true")); + + if (isStock ? !colorStock : !colorCustom) { + return marker; + } + + SPDocument *doc = marker->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + // Turn off garbage-collectable or it might be collected before we can use it + marker->removeAttribute("inkscape:collect"); + Inkscape::XML::Node *mark_repr = marker->getRepr()->duplicate(xml_doc); + doc->getDefs()->getRepr()->addChild(mark_repr, nullptr); + if (!mark_repr->attribute("inkscape:stockid")) { + mark_repr->setAttribute("inkscape:stockid", mark_repr->attribute("id")); + } + marker->setAttribute("inkscape:collect", "always"); + + SPObject *marker_new = static_cast(doc->getObjectByRepr(mark_repr)); + Inkscape::GC::release(mark_repr); + return marker_new; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-marker.h b/src/object/sp-marker.h new file mode 100644 index 0000000..5bd5a9c --- /dev/null +++ b/src/object/sp-marker.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MARKER_H +#define SEEN_SP_MARKER_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * This is quite similar in logic to + * Maybe we should merge them somehow (Lauris) + */ + +#define SP_TYPE_MARKER (sp_marker_get_type ()) +#define SP_MARKER(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MARKER(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPMarkerView; + +#include + +#include <2geom/rect.h> +#include <2geom/affine.h> + +#include "enums.h" +#include "svg/svg-length.h" +#include "svg/svg-angle.h" +#include "sp-item-group.h" +#include "uri-references.h" +#include "viewbox.h" + +enum markerOrient { + MARKER_ORIENT_ANGLE, + MARKER_ORIENT_AUTO, + MARKER_ORIENT_AUTO_START_REVERSE +}; + +class SPMarker : public SPGroup, public SPViewBox { +public: + SPMarker(); + ~SPMarker() override; + + /* units */ + unsigned int markerUnits_set : 1; + unsigned int markerUnits : 1; + + /* reference point */ + SVGLength refX; + SVGLength refY; + + /* dimensions */ + SVGLength markerWidth; + SVGLength markerHeight; + + /* orient */ + unsigned int orient_set : 1; + markerOrient orient_mode : 2; + SVGAngle orient; + + /* Private views indexed by key that corresponds to a + * particular marker type (start, mid, end) on a particular + * path. SPMarkerView is a wrapper for a vector of pointers to + * Inkscape::DrawingItem instances, one pointer for each + * rendered marker. + */ + std::map views_map; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, gchar const* value) override; + void update(SPCtx *ctx, guint flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + virtual Inkscape::DrawingItem* private_show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags); + void hide(unsigned int key) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; +}; + +class SPMarkerReference : public Inkscape::URIReference { + SPMarkerReference(SPObject *obj) : URIReference(obj) {} + SPMarker *getObject() const { + return static_cast(URIReference::getObject()); + } +protected: + bool _acceptObject(SPObject *obj) const override { + return SP_IS_MARKER(obj) && URIReference::_acceptObject(obj); + } +}; + +void sp_marker_show_dimension (SPMarker *marker, unsigned int key, unsigned int size); +Inkscape::DrawingItem *sp_marker_show_instance (SPMarker *marker, Inkscape::DrawingItem *parent, + unsigned int key, unsigned int pos, + Geom::Affine const &base, float linewidth); +void sp_marker_hide (SPMarker *marker, unsigned int key); +const char *generate_marker (std::vector &reprs, Geom::Rect bounds, SPDocument *document, Geom::Point center, Geom::Affine move); +SPObject *sp_marker_fork_if_necessary(SPObject *marker); + +#endif diff --git a/src/object/sp-mask.cpp b/src/object/sp-mask.cpp new file mode 100644 index 0000000..8e8b695 --- /dev/null +++ b/src/object/sp-mask.cpp @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include <2geom/transforms.h> + +#include "display/drawing.h" +#include "display/drawing-group.h" +#include "xml/repr.h" + +#include "enums.h" +#include "attributes.h" +#include "document.h" +#include "style.h" +#include "attributes.h" + +#include "sp-defs.h" +#include "sp-item.h" +#include "sp-mask.h" + +SPMaskView *sp_mask_view_new_prepend (SPMaskView *list, unsigned int key, Inkscape::DrawingItem *arenaitem); +SPMaskView *sp_mask_view_list_remove (SPMaskView *list, SPMaskView *view); + +SPMask::SPMask() : SPObjectGroup() { + this->maskUnits_set = FALSE; + this->maskUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + + this->maskContentUnits_set = FALSE; + this->maskContentUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + + this->display = nullptr; +} + +SPMask::~SPMask() = default; + +void SPMask::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObjectGroup::build(doc, repr); + + this->readAttr( "maskUnits" ); + this->readAttr( "maskContentUnits" ); + this->readAttr( "style" ); + + /* Register ourselves */ + doc->addResource("mask", this); +} + +void SPMask::release() { + if (this->document) { + // Unregister ourselves + this->document->removeResource("mask", this); + } + + while (this->display) { + // We simply unref and let item manage this in handler + this->display = sp_mask_view_list_remove(this->display, this->display); + } + + SPObjectGroup::release(); +} + +void SPMask::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_MASKUNITS: + this->maskUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + this->maskUnits_set = FALSE; + + if (value) { + if (!strcmp (value, "userSpaceOnUse")) { + this->maskUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + this->maskUnits_set = TRUE; + } else if (!strcmp (value, "objectBoundingBox")) { + this->maskUnits_set = TRUE; + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_MASKCONTENTUNITS: + this->maskContentUnits = SP_CONTENT_UNITS_USERSPACEONUSE; + this->maskContentUnits_set = FALSE; + + if (value) { + if (!strcmp (value, "userSpaceOnUse")) { + this->maskContentUnits_set = TRUE; + } else if (!strcmp (value, "objectBoundingBox")) { + this->maskContentUnits = SP_CONTENT_UNITS_OBJECTBOUNDINGBOX; + this->maskContentUnits_set = TRUE; + } + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObjectGroup::set(key, value); + break; + } +} + +Geom::OptRect +SPMask::geometricBounds(Geom::Affine const &transform) { + Geom::OptRect bbox; + + for (auto& i: children) { + if (SP_IS_ITEM(&i)) { + Geom::OptRect tmp = SP_ITEM(&i)->geometricBounds(Geom::Affine(SP_ITEM(&i)->transform) * transform); + bbox.unionWith(tmp); + } + } + + return bbox; +} + +Geom::OptRect +SPMask::visualBounds(Geom::Affine const &transform) { + Geom::OptRect bbox; + for (auto& i: children) { + if (SP_IS_ITEM(&i)) { + Geom::OptRect tmp = SP_ITEM(&i)->visualBounds(SP_ITEM(&i)->transform * transform); + bbox.unionWith(tmp); + } + } + + return bbox; +} + +void SPMask::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + /* Invoke SPObjectGroup implementation */ + SPObjectGroup::child_added(child, ref); + + /* Show new object */ + SPObject *ochild = this->document->getObjectByRepr(child); + + if (SP_IS_ITEM (ochild)) { + for (SPMaskView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ac = SP_ITEM (ochild)->invoke_show(v->arenaitem->drawing(), v->key, SP_ITEM_REFERENCE_FLAGS); + + if (ac) { + // @fixme must take position into account + v->arenaitem->prependChild(ac); + } + } + } +} + + +void SPMask::update(SPCtx* ctx, unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector children = this->childList(true); + + for (auto child : children) { + if (flags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + + sp_object_unref(child); + } + + for (SPMaskView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + + if (this->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && v->bbox) { + Geom::Affine t = Geom::Scale(v->bbox->dimensions()); + t.setTranslation(v->bbox->min()); + g->setChildTransform(t); + } else { + g->setChildTransform(Geom::identity()); + } + } +} + +void SPMask::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::vector children = this->childList(true); + + for (auto child : children) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPMask::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:mask"); + } + + SPObjectGroup::write(xml_doc, repr, flags); + + return repr; +} + +// Create a mask element (using passed elements), add it to +const gchar * +sp_mask_create (std::vector &reprs, SPDocument *document) +{ + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:mask"); + repr->setAttribute("maskUnits", "userSpaceOnUse"); + + defsrepr->appendChild(repr); + const gchar *mask_id = repr->attribute("id"); + SPObject *mask_object = document->getObjectById(mask_id); + + for (auto node : reprs) { + mask_object->appendChildRepr(node); + } + + if (repr != defsrepr->lastChild()) + defsrepr->changeOrder(repr, defsrepr->lastChild()); // workaround for bug 989084 + + Inkscape::GC::release(repr); + return mask_id; +} + +Inkscape::DrawingItem *SPMask::sp_mask_show(Inkscape::Drawing &drawing, unsigned int key) { + g_return_val_if_fail (SP_IS_MASK (this), NULL); + + Inkscape::DrawingGroup *ai = new Inkscape::DrawingGroup(drawing); + this->display = sp_mask_view_new_prepend (this->display, key, ai); + + for (auto& child: children) { + if (SP_IS_ITEM (&child)) { + Inkscape::DrawingItem *ac = SP_ITEM (&child)->invoke_show (drawing, key, SP_ITEM_REFERENCE_FLAGS); + + if (ac) { + ai->appendChild(ac); + } + } + } + + if (this->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && this->display->bbox) { + Geom::Affine t = Geom::Scale(this->display->bbox->dimensions()); + t.setTranslation(this->display->bbox->min()); + ai->setChildTransform(t); + } + + return ai; +} + +void SPMask::sp_mask_hide(unsigned int key) { + g_return_if_fail (SP_IS_MASK (this)); + + for (auto& child: children) { + if (SP_IS_ITEM (&child)) { + SP_ITEM(&child)->invoke_hide (key); + } + } + + for (SPMaskView *v = this->display; v != nullptr; v = v->next) { + if (v->key == key) { + /* We simply unref and let item to manage this in handler */ + this->display = sp_mask_view_list_remove (this->display, v); + return; + } + } + + g_assert_not_reached (); +} + +void SPMask::sp_mask_set_bbox(unsigned int key, Geom::OptRect const &bbox) { + for (SPMaskView *v = this->display; v != nullptr; v = v->next) { + if (v->key == key) { + v->bbox = bbox; + break; + } + } +} + +/* Mask views */ + +SPMaskView * +sp_mask_view_new_prepend (SPMaskView *list, unsigned int key, Inkscape::DrawingItem *arenaitem) +{ + SPMaskView *new_mask_view = g_new (SPMaskView, 1); + + new_mask_view->next = list; + new_mask_view->key = key; + new_mask_view->arenaitem = arenaitem; + new_mask_view->bbox = Geom::OptRect(); + + return new_mask_view; +} + +SPMaskView * +sp_mask_view_list_remove (SPMaskView *list, SPMaskView *view) +{ + if (view == list) { + list = list->next; + } else { + SPMaskView *prev; + prev = list; + while (prev->next != view) prev = prev->next; + prev->next = view->next; + } + + delete view->arenaitem; + g_free (view); + + return list; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mask.h b/src/object/sp-mask.h new file mode 100644 index 0000000..db56035 --- /dev/null +++ b/src/object/sp-mask.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MASK_H +#define SEEN_SP_MASK_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/rect.h> +#include "sp-object-group.h" +#include "uri-references.h" +#include "xml/node.h" + +#define SP_MASK(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MASK(obj) (dynamic_cast((SPObject*)obj) != NULL) + +namespace Inkscape { + +class Drawing; +class DrawingItem; + + +} // namespace Inkscape + +struct SPMaskView { + SPMaskView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +}; + +class SPMask : public SPObjectGroup { +public: + SPMask(); + ~SPMask() override; + + unsigned int maskUnits_set : 1; + unsigned int maskUnits : 1; + + unsigned int maskContentUnits_set : 1; + unsigned int maskContentUnits : 1; + + SPMaskView *display; + + Inkscape::DrawingItem *sp_mask_show(Inkscape::Drawing &drawing, unsigned int key); + void sp_mask_hide(unsigned int key); + + Geom::OptRect geometricBounds(Geom::Affine const &transform); + + Geom::OptRect visualBounds(Geom::Affine const &transform) ; + + void sp_mask_set_bbox(unsigned int key, Geom::OptRect const &bbox); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + + void set(SPAttributeEnum key, const char* value) override; + + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +class SPMaskReference : public Inkscape::URIReference { +public: + SPMaskReference(SPObject *obj) : URIReference(obj) {} + SPMask *getObject() const { + return static_cast(URIReference::getObject()); + } +protected: + /** + * If the owner element of this reference (the element with <... mask="...">) + * is a child of the mask it refers to, return false. + * \return false if obj is not a mask or if obj is a parent of this + * reference's owner element. True otherwise. + */ + bool _acceptObject(SPObject *obj) const override { + if (!SP_IS_MASK(obj)) { + return false; + } + SPObject * const owner = this->getOwner(); + if (!URIReference::_acceptObject(obj)) { + //XML Tree being used directly here while it shouldn't be... + Inkscape::XML::Node * const owner_repr = owner->getRepr(); + //XML Tree being used directly here while it shouldn't be... + Inkscape::XML::Node * const obj_repr = obj->getRepr(); + char const * owner_name = ""; + char const * owner_mask = ""; + char const * obj_name = ""; + char const * obj_id = ""; + if (owner_repr != nullptr) { + owner_name = owner_repr->name(); + owner_mask = owner_repr->attribute("mask"); + } + if (obj_repr != nullptr) { + obj_name = obj_repr->name(); + obj_id = obj_repr->attribute("id"); + } + printf("WARNING: Ignoring recursive mask reference " + "<%s mask=\"%s\"> in <%s id=\"%s\">", + owner_name, owner_mask, + obj_name, obj_id); + return false; + } + return true; + } +}; + +const char *sp_mask_create (std::vector &reprs, SPDocument *document); + +#endif // SEEN_SP_MASK_H diff --git a/src/object/sp-mesh-array.cpp b/src/object/sp-mesh-array.cpp new file mode 100644 index 0000000..b0b80d9 --- /dev/null +++ b/src/object/sp-mesh-array.cpp @@ -0,0 +1,3098 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + A group of classes and functions for manipulating mesh gradients. + + A mesh is made up of an array of patches. Each patch has four sides and four corners. The sides can + be shared between two patches and the corners between up to four. + + The order of the points for each side always goes from left to right or top to bottom. + For sides 2 and 3 the points must be reversed when used (as in calls to cairo functions). + + Two patches: (C=corner, S=side, H=handle, T=tensor) + + C0 H1 H2 C1 C0 H1 H2 C1 + + ---------- + ---------- + + | S0 | S0 | + H1 | T0 T1 |H1 T0 T1 | H1 + |S3 S1|S3 S1| + H2 | T3 T2 |H2 T3 T2 | H2 + | S2 | S2 | + + ---------- + ---------- + + C3 H1 H2 C2 C3 H1 H2 C2 + + The mesh is stored internally as an array of nodes that includes the tensor nodes. + + Note: This code uses tensor points which are not part of the SVG2 plan at the moment. + Including tensor points was motivated by a desire to experiment with their usefulness + in smoothing color transitions. There doesn't seem to be much advantage for that + purpose. However including them internally allows for storing all the points in + an array which simplifies things like inserting new rows or columns. +*/ + +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2012, 2015 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +// For color picking +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/cairo-utils.h" +#include "document.h" +#include "sp-root.h" + +#include "sp-mesh-gradient.h" +#include "sp-mesh-array.h" +#include "sp-mesh-row.h" +#include "sp-mesh-patch.h" +#include "sp-stop.h" +#include "display/curve.h" + +// For new mesh creation +#include "preferences.h" +#include "sp-ellipse.h" +#include "sp-star.h" + +// For writing color/opacity to style +#include "svg/css-ostringstream.h" + +// For default color +#include "style.h" +#include "svg/svg-color.h" + + +// Includes bezier-curve.h, ray.h, crossing.h +#include "2geom/line.h" + +#include "xml/repr.h" +#include +#include + +enum { ROW, COL }; + +SPMeshPatchI::SPMeshPatchI( std::vector > * n, int r, int c ) { + + nodes = n; + row = r*3; // Convert from patch array to node array + col = c*3; + + guint i = 0; + if( row != 0 ) i = 1; + for( ; i < 4; ++i ) { + if( nodes->size() < row+i+1 ) { + std::vector< SPMeshNode* > row; + nodes->push_back( row ); + } + + guint j = 0; + if( col != 0 ) j = 1; + for( ; j < 4; ++j ) { + if( (*nodes)[row+i].size() < col+j+1 ){ + SPMeshNode* node = new SPMeshNode; + // Ensure all nodes know their type. + node->node_type = MG_NODE_TYPE_HANDLE; + if( (i == 0 || i == 3) && (j == 0 || j == 3 ) ) node->node_type = MG_NODE_TYPE_CORNER; + if( (i == 1 || i == 2) && (j == 1 || j == 2 ) ) node->node_type = MG_NODE_TYPE_TENSOR; + (*nodes)[row+i].push_back( node ); + } + } + } +} + +/** + Returns point for side in proper order for patch +*/ +Geom::Point SPMeshPatchI::getPoint( guint s, guint pt ) { + + assert( s < 4 ); + assert( pt < 4 ); + + Geom::Point p; + switch ( s ) { + case 0: + p = (*nodes)[ row ][ col+pt ]->p; + break; + case 1: + p = (*nodes)[ row+pt ][ col+3 ]->p; + break; + case 2: + p = (*nodes)[ row+3 ][ col+3-pt ]->p; + break; + case 3: + p = (*nodes)[ row+3-pt ][ col ]->p; + break; + } + return p; + +}; + +/** + Returns vector of points for a side in proper order for a patch (clockwise order). +*/ +std::vector< Geom::Point > SPMeshPatchI::getPointsForSide( guint i ) { + + assert( i < 4 ); + + std::vector< Geom::Point> points; + points.push_back( getPoint( i, 0 ) ); + points.push_back( getPoint( i, 1 ) ); + points.push_back( getPoint( i, 2 ) ); + points.push_back( getPoint( i, 3 ) ); + return points; +}; + + +/** + Set point for side in proper order for patch +*/ +void SPMeshPatchI::setPoint( guint s, guint pt, Geom::Point p, bool set ) { + + assert( s < 4 ); + assert( pt < 4 ); + + NodeType node_type = MG_NODE_TYPE_CORNER; + if( pt == 1 || pt == 2 ) node_type = MG_NODE_TYPE_HANDLE; + + // std::cout << "SPMeshPatchI::setPoint: s: " << s + // << " pt: " << pt + // << " p: " << p + // << " node_type: " << node_type + // << " set: " << set + // << " row: " << row + // << " col: " << col << std::endl; + switch ( s ) { + case 0: + (*nodes)[ row ][ col+pt ]->p = p; + (*nodes)[ row ][ col+pt ]->set = set; + (*nodes)[ row ][ col+pt ]->node_type = node_type; + break; + case 1: + (*nodes)[ row+pt ][ col+3 ]->p = p; + (*nodes)[ row+pt ][ col+3 ]->set = set; + (*nodes)[ row+pt ][ col+3 ]->node_type = node_type; + break; + case 2: + (*nodes)[ row+3 ][ col+3-pt ]->p = p; + (*nodes)[ row+3 ][ col+3-pt ]->set = set; + (*nodes)[ row+3 ][ col+3-pt ]->node_type = node_type; + break; + case 3: + (*nodes)[ row+3-pt ][ col ]->p = p; + (*nodes)[ row+3-pt ][ col ]->set = set; + (*nodes)[ row+3-pt ][ col ]->node_type = node_type; + break; + } + +}; + +/** + Get path type for side (stored in handle nodes). +*/ +gchar SPMeshPatchI::getPathType( guint s ) { + + assert( s < 4 ); + + gchar type = 'x'; + + switch ( s ) { + case 0: + type = (*nodes)[ row ][ col+1 ]->path_type; + break; + case 1: + type = (*nodes)[ row+1 ][ col+3 ]->path_type; + break; + case 2: + type = (*nodes)[ row+3 ][ col+2 ]->path_type; + break; + case 3: + type = (*nodes)[ row+2 ][ col ]->path_type; + break; + } + + return type; +}; + +/** + Set path type for side (stored in handle nodes). +*/ +void SPMeshPatchI::setPathType( guint s, gchar t ) { + + assert( s < 4 ); + + switch ( s ) { + case 0: + (*nodes)[ row ][ col+1 ]->path_type = t; + (*nodes)[ row ][ col+2 ]->path_type = t; + break; + case 1: + (*nodes)[ row+1 ][ col+3 ]->path_type = t; + (*nodes)[ row+2 ][ col+3 ]->path_type = t; + break; + case 2: + (*nodes)[ row+3 ][ col+1 ]->path_type = t; + (*nodes)[ row+3 ][ col+2 ]->path_type = t; + break; + case 3: + (*nodes)[ row+1 ][ col ]->path_type = t; + (*nodes)[ row+2 ][ col ]->path_type = t; + break; + } + +}; + +/** + Set tensor control point for "corner" i. + */ +void SPMeshPatchI::setTensorPoint( guint i, Geom::Point p ) { + + assert( i < 4 ); + switch ( i ) { + case 0: + (*nodes)[ row + 1 ][ col + 1 ]->p = p; + (*nodes)[ row + 1 ][ col + 1 ]->set = true; + (*nodes)[ row + 1 ][ col + 1 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 1: + (*nodes)[ row + 1 ][ col + 2 ]->p = p; + (*nodes)[ row + 1 ][ col + 2 ]->set = true; + (*nodes)[ row + 1 ][ col + 2 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 2: + (*nodes)[ row + 2 ][ col + 2 ]->p = p; + (*nodes)[ row + 2 ][ col + 2 ]->set = true; + (*nodes)[ row + 2 ][ col + 2 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + case 3: + (*nodes)[ row + 2 ][ col + 1 ]->p = p; + (*nodes)[ row + 2 ][ col + 1 ]->set = true; + (*nodes)[ row + 2 ][ col + 1 ]->node_type = MG_NODE_TYPE_TENSOR; + break; + } +} + +/** + Return if any tensor control point is set. + */ +bool SPMeshPatchI::tensorIsSet() { + for( guint i = 0; i < 4; ++i ) { + if( tensorIsSet( i ) ) { + return true; + } + } + return false; +} + +/** + Return if tensor control point for "corner" i is set. + */ +bool SPMeshPatchI::tensorIsSet( unsigned int i ) { + + assert( i < 4 ); + + bool set = false; + switch ( i ) { + case 0: + set = (*nodes)[ row + 1 ][ col + 1 ]->set; + break; + case 1: + set = (*nodes)[ row + 1 ][ col + 2 ]->set; + break; + case 2: + set = (*nodes)[ row + 2 ][ col + 2 ]->set; + break; + case 3: + set = (*nodes)[ row + 2 ][ col + 1 ]->set; + break; + } + return set; +} + +/** + Return tensor control point for "corner" i. + If not set, returns calculated (Coons) point. + */ +Geom::Point SPMeshPatchI::getTensorPoint( guint k ) { + + assert( k < 4 ); + + guint i = 0; + guint j = 0; + + + switch ( k ) { + case 0: + i = 1; + j = 1; + break; + case 1: + i = 1; + j = 2; + break; + case 2: + i = 2; + j = 2; + break; + case 3: + i = 2; + j = 1; + break; + } + + Geom::Point p; + if( (*nodes)[ row + i ][ col + j ]->set ) { + p = (*nodes)[ row + i ][ col + j ]->p; + } else { + p = coonsTensorPoint( k ); + } + return p; +} + +/** + Find default tensor point (equivalent point to Coons Patch). + Formulas defined in PDF spec. + Equivalent to 1/3 of side length from corner for square patch. + */ +Geom::Point SPMeshPatchI::coonsTensorPoint( guint i ) { + + Geom::Point t; + Geom::Point p[4][4]; // Points in PDF notation + + p[0][0] = getPoint( 0, 0 ); + p[0][1] = getPoint( 0, 1 ); + p[0][2] = getPoint( 0, 2 ); + p[0][3] = getPoint( 0, 3 ); + p[1][0] = getPoint( 3, 2 ); + p[1][3] = getPoint( 1, 1 ); + p[2][0] = getPoint( 3, 1 ); + p[2][3] = getPoint( 1, 2 ); + p[3][0] = getPoint( 2, 3 ); + p[3][1] = getPoint( 2, 2 ); + p[3][2] = getPoint( 2, 1 ); + p[3][3] = getPoint( 2, 0 ); + + switch ( i ) { + case 0: + t = ( -4.0 * p[0][0] + + 6.0 * ( p[0][1] + p[1][0] ) + + -2.0 * ( p[0][3] + p[3][0] ) + + 3.0 * ( p[3][1] + p[1][3] ) + + -1.0 * p[3][3] ) / 9.0; + break; + + case 1: + t = ( -4.0 * p[0][3] + + 6.0 * ( p[0][2] + p[1][3] ) + + -2.0 * ( p[0][0] + p[3][3] ) + + 3.0 * ( p[3][2] + p[1][0] ) + + -1.0 * p[3][0] ) / 9.0; + break; + + case 2: + t = ( -4.0 * p[3][3] + + 6.0 * ( p[3][2] + p[2][3] ) + + -2.0 * ( p[3][0] + p[0][3] ) + + 3.0 * ( p[0][2] + p[2][0] ) + + -1.0 * p[0][0] ) / 9.0; + break; + + case 3: + t = ( -4.0 * p[3][0] + + 6.0 * ( p[3][1] + p[2][0] ) + + -2.0 * ( p[3][3] + p[0][0] ) + + 3.0 * ( p[0][1] + p[2][3] ) + + -1.0 * p[0][3] ) / 9.0; + break; + + default: + + g_warning( "Impossible!" ); + + } + return t; +} + +/** + Update default values for handle and tensor nodes. +*/ +void SPMeshPatchI::updateNodes() { + + // std::cout << "SPMeshPatchI::updateNodes: " << row << "," << col << std::endl; + // Handles first (tensors require update handles). + for( guint i = 0; i < 4; ++i ) { + for( guint j = 0; j < 4; ++j ) { + if( (*nodes)[ row + i ][ col + j ]->set == false ) { + + if( (*nodes)[ row + i ][ col + j ]->node_type == MG_NODE_TYPE_HANDLE ) { + + // If a handle is not set it is because the side is a line. + // Set node points 1/3 of the way between corners. + + if( i == 0 || i == 3 ) { + Geom::Point p0 = ( (*nodes)[ row + i ][ col ]->p ); + Geom::Point p3 = ( (*nodes)[ row + i ][ col + 3 ]->p ); + Geom::Point dp = (p3 - p0)/3.0; + if( j == 2 ) dp *= 2.0; + (*nodes)[ row + i ][ col + j ]->p = p0 + dp; + } + + if( j == 0 || j == 3 ) { + Geom::Point p0 = ( (*nodes)[ row ][ col + j ]->p ); + Geom::Point p3 = ( (*nodes)[ row + 3 ][ col + j ]->p ); + Geom::Point dp = (p3 - p0)/3.0; + if( i == 2 ) dp *= 2.0; + (*nodes)[ row + i ][ col + j ]->p = p0 + dp; + } + } + } + } + } + + // Update tensor nodes + for( guint i = 1; i < 3; ++i ) { + for( guint j = 1; j < 3; ++j ) { + if( (*nodes)[ row + i ][ col + j ]->set == false ) { + + (*nodes)[ row + i ][ col + j ]->node_type = MG_NODE_TYPE_TENSOR; + + guint t = 0; + if( i == 1 && j == 2 ) t = 1; + if( i == 2 && j == 2 ) t = 2; + if( i == 2 && j == 1 ) t = 3; + (*nodes)[ row + i ][ col + j ]->p = coonsTensorPoint( t ); + // std::cout << "Update node: " << i << ", " << j << " " << coonsTensorPoint( t ) << std::endl; + + } + } + } +} + +/** + Return color for corner of patch. +*/ +SPColor SPMeshPatchI::getColor( guint i ) { + + assert( i < 4 ); + + SPColor color; + switch ( i ) { + case 0: + color = (*nodes)[ row ][ col ]->color; + break; + case 1: + color = (*nodes)[ row ][ col+3 ]->color; + break; + case 2: + color = (*nodes)[ row+3 ][ col+3 ]->color; + break; + case 3: + color = (*nodes)[ row+3 ][ col ]->color; + break; + + } + + return color; + +}; + +/** + Set color for corner of patch. +*/ +void SPMeshPatchI::setColor( guint i, SPColor color ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->color = color; + break; + case 1: + (*nodes)[ row ][ col+3 ]->color = color; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->color = color; + break; + case 3: + (*nodes)[ row+3 ][ col ]->color = color; + break; + } +}; + +/** + Return opacity for corner of patch. +*/ +gdouble SPMeshPatchI::getOpacity( guint i ) { + + assert( i < 4 ); + + gdouble opacity = 0.0; + switch ( i ) { + case 0: + opacity = (*nodes)[ row ][ col ]->opacity; + break; + case 1: + opacity = (*nodes)[ row ][ col+3 ]->opacity; + break; + case 2: + opacity = (*nodes)[ row+3 ][ col+3 ]->opacity; + break; + case 3: + opacity = (*nodes)[ row+3 ][ col ]->opacity; + break; + } + + return opacity; +}; + + +/** + Set opacity for corner of patch. +*/ +void SPMeshPatchI::setOpacity( guint i, gdouble opacity ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->opacity = opacity; + break; + case 1: + (*nodes)[ row ][ col+3 ]->opacity = opacity; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->opacity = opacity; + break; + case 3: + (*nodes)[ row+3 ][ col ]->opacity = opacity; + break; + + } + +}; + + +/** + Return stop pointer for corner of patch. +*/ +SPStop* SPMeshPatchI::getStopPtr( guint i ) { + + assert( i < 4 ); + + SPStop* stop = nullptr; + switch ( i ) { + case 0: + stop = (*nodes)[ row ][ col ]->stop; + break; + case 1: + stop = (*nodes)[ row ][ col+3 ]->stop; + break; + case 2: + stop = (*nodes)[ row+3 ][ col+3 ]->stop; + break; + case 3: + stop = (*nodes)[ row+3 ][ col ]->stop; + break; + } + + return stop; +}; + + +/** + Set stop pointer for corner of patch. +*/ +void SPMeshPatchI::setStopPtr( guint i, SPStop* stop ) { + + assert( i < 4 ); + + switch ( i ) { + case 0: + (*nodes)[ row ][ col ]->stop = stop; + break; + case 1: + (*nodes)[ row ][ col+3 ]->stop = stop; + break; + case 2: + (*nodes)[ row+3 ][ col+3 ]->stop = stop; + break; + case 3: + (*nodes)[ row+3 ][ col ]->stop = stop; + break; + + } + +}; + + +SPMeshNodeArray::SPMeshNodeArray( SPMeshGradient *mg ) { + + read( mg ); + +}; + + +// Copy constructor +SPMeshNodeArray::SPMeshNodeArray( const SPMeshNodeArray& rhs ) { + + built = false; + mg = nullptr; + draggers_valid = false; + + nodes = rhs.nodes; // This only copies the pointers but it does size the vector of vectors. + + for( unsigned i=0; i < nodes.size(); ++i ) { + for( unsigned j=0; j < nodes[i].size(); ++j ) { + nodes[i][j] = new SPMeshNode( *rhs.nodes[i][j] ); // Copy data. + } + } +}; + + +// Copy assignment operator +SPMeshNodeArray& SPMeshNodeArray::operator=( const SPMeshNodeArray& rhs ) { + + if( this == &rhs ) return *this; + + clear(); // Clear any existing array. + + built = false; + mg = nullptr; + draggers_valid = false; + + nodes = rhs.nodes; // This only copies the pointers but it does size the vector of vectors. + + for( unsigned i=0; i < nodes.size(); ++i ) { + for( unsigned j=0; j < nodes[i].size(); ++j ) { + nodes[i][j] = new SPMeshNode( *rhs.nodes[i][j] ); // Copy data. + } + } + + return *this; +}; + +// Fill array with data from mesh objects. +// Returns true of array's dimensions unchanged. +bool SPMeshNodeArray::read( SPMeshGradient *mg_in ) { + + mg = mg_in; + SPMeshGradient* mg_array = dynamic_cast(mg->getArray()); + if (!mg_array) { + std::cerr << "SPMeshNodeArray::read: No mesh array!" << std::endl; + return false; + } + // std::cout << "SPMeshNodeArray::read: " << mg_in << " array: " << mg_array << std::endl; + + // Count rows and columns, if unchanged reuse array to keep draggers valid. + unsigned cols = 0; + unsigned rows = 0; + for (auto& ro: mg_array->children) { + if (SP_IS_MESHROW(&ro)) { + ++rows; + if (rows == 1 ) { + for (auto& po: ro.children) { + if (SP_IS_MESHPATCH(&po)) { + ++cols; + } + } + } + } + } + bool same_size = true; + if (cols != patch_columns() || rows != patch_rows() ) { + // Draggers will be invalidated. + same_size = false; + clear(); + draggers_valid = false; + } + + Geom::Point current_p( mg->x.computed, mg->y.computed ); + // std::cout << "SPMeshNodeArray::read: p: " << current_p << std::endl; + + guint max_column = 0; + guint irow = 0; // Corresponds to top of patch being read in. + for (auto& ro: mg_array->children) { + + if (SP_IS_MESHROW(&ro)) { + + guint icolumn = 0; // Corresponds to left of patch being read in. + for (auto& po: ro.children) { + + if (SP_IS_MESHPATCH(&po)) { + + SPMeshpatch *patch = SP_MESHPATCH(&po); + + // std::cout << "SPMeshNodeArray::read: row size: " << nodes.size() << std::endl; + SPMeshPatchI new_patch( &nodes, irow, icolumn ); // Adds new nodes. + // std::cout << " after: " << nodes.size() << std::endl; + + gint istop = 0; + + // Only 'top' side defined for first row. + if( irow != 0 ) ++istop; + + for (auto& so: po.children) { + if (SP_IS_STOP(&so)) { + + if( istop > 3 ) { + // std::cout << " Mesh Gradient: Too many stops: " << istop << std::endl; + break; + } + + SPStop *stop = SP_STOP(&so); + + // Handle top of first row. + if( istop == 0 && icolumn == 0 ) { + // First patch in mesh. + new_patch.setPoint( 0, 0, current_p ); + } + // First point is always already defined by previous side (stop). + current_p = new_patch.getPoint( istop, 0 ); + + // If side closes patch, then we read one less point. + bool closed = false; + if( icolumn == 0 && istop == 3 ) closed = true; + if( icolumn > 0 && istop == 2 ) closed = true; + + + // Copy path and then replace commas by spaces so we can use stringstream to parse + std::string path_string = *(stop->path_string); + std::replace(path_string.begin(),path_string.end(),',',' '); + + // std::cout << " path_string: " << path_string << std::endl; + // std::cout << " current_p: " << current_p << std::endl; + + std::stringstream os( path_string ); + + // Determine type of path + char path_type; + os >> path_type; + new_patch.setPathType( istop, path_type ); + + gdouble x, y; + Geom::Point p, dp; + guint max; + switch ( path_type ) { + case 'l': + if( !closed ) { + os >> x >> y; + if( !os.fail() ) { + dp = Geom::Point( x, y ); + new_patch.setPoint( istop, 3, current_p + dp ); + } else { + std::cerr << "Failed to read l" << std::endl; + } + } + // To facilitate some side operations, set handles to 1/3 and + // 2/3 distance between corner points but flag as unset. + p = new_patch.getPoint( istop, 3 ); + dp = (p - current_p)/3.0; // Calculate since may not be set if closed. + // std::cout << " istop: " << istop + // << " dp: " << dp + // << " p: " << p + // << " current_p: " << current_p + // << std::endl; + new_patch.setPoint( istop, 1, current_p + dp, false ); + new_patch.setPoint( istop, 2, current_p + 2.0 * dp, false ); + break; + case 'L': + if( !closed ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + new_patch.setPoint( istop, 3, p ); + } else { + std::cerr << "Failed to read L" << std::endl; + } + } + // To facilitate some side operations, set handles to 1/3 and + // 2/3 distance between corner points but flag as unset. + p = new_patch.getPoint( istop, 3 ); + dp = (p - current_p)/3.0; + new_patch.setPoint( istop, 1, current_p + dp, false ); + new_patch.setPoint( istop, 2, current_p + 2.0 * dp, false ); + break; + case 'c': + max = 4; + if( closed ) max = 3; + for( guint i = 1; i < max; ++i ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + p += current_p; + new_patch.setPoint( istop, i, p ); + } else { + std::cerr << "Failed to read c: " << i << std::endl; + } + } + break; + case 'C': + max = 4; + if( closed ) max = 3; + for( guint i = 1; i < max; ++i ) { + os >> x >> y; + if( !os.fail() ) { + p = Geom::Point( x, y ); + new_patch.setPoint( istop, i, p ); + } else { + std::cerr << "Failed to read C: " << i << std::endl; + } + } + break; + default: + // should not reach + std::cerr << "Path Error: unhandled path type: " << path_type << std::endl; + } + current_p = new_patch.getPoint( istop, 3 ); + + // Color + if( (istop == 0 && irow == 0 && icolumn > 0) || (istop == 1 && irow > 0 ) ) { + // skip + } else { + SPColor color = stop->getColor(); + double opacity = stop->getOpacity(); + new_patch.setColor( istop, color ); + new_patch.setOpacity( istop, opacity ); + new_patch.setStopPtr( istop, stop ); + } + ++istop; + } + } // Loop over stops + + // Read in tensor string after stops since tensor nodes defined relative to corner nodes. + + // Copy string and then replace commas by spaces so we can use stringstream to parse XXXX + if( patch->tensor_string ) { + std::string tensor_string = *(patch->tensor_string); + std::replace(tensor_string.begin(),tensor_string.end(),',',' '); + + // std::cout << " tensor_string: " << tensor_string << std::endl; + + std::stringstream os( tensor_string ); + for( guint i = 0; i < 4; ++i ) { + double x = 0.0; + double y = 0.0; + os >> x >> y; + if( !os.fail() ) { + new_patch.setTensorPoint( i, new_patch.getPoint( i, 0 ) + Geom::Point( x, y ) ); + } else { + std::cerr << "Failed to read p: " << i << std::endl; + break; + } + } + } + ++icolumn; + if( max_column < icolumn ) max_column = icolumn; + } + } + ++irow; + } + } + + // Insure we have a true array. + for(auto & node : nodes) { + node.resize( max_column * 3 + 1 ); + } + + // Set node edge. + for( guint i = 0; i < nodes.size(); ++i ) { + for( guint j = 0; j < nodes[i].size(); ++j ) { + nodes[i][j]->node_edge = MG_NODE_EDGE_NONE; + if( i == 0 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_TOP; + if( i == nodes.size() - 1 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_BOTTOM; + if( j == 0 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_RIGHT; + if( j == nodes[i].size() - 1 ) nodes[i][j]->node_edge |= MG_NODE_EDGE_LEFT; + } + } + + // std::cout << "SPMeshNodeArray::Read: result:" << std::endl; + // print(); + + built = true; + + return same_size; +}; + +/** + Write repr using our array. +*/ +void SPMeshNodeArray::write( SPMeshGradient *mg ) { + + // std::cout << "SPMeshNodeArray::write: entrance:" << std::endl; + // print(); + using Geom::X; + using Geom::Y; + + SPMeshGradient* mg_array = dynamic_cast(mg->getArray()); + if (!mg_array) { + // std::cerr << "SPMeshNodeArray::write: missing patches!" << std::endl; + mg_array = mg; + } + + // First we must delete reprs for old mesh rows and patches. We only need to call the + // deleteObject() method, which in turn calls sp_repr_unparent. Since iterators do not play + // well with boost::intrusive::list (which ChildrenList derive from) we need to iterate over a + // copy of the pointers to the objects. + std::vector children_pointers; + for (auto& row : mg_array->children) { + children_pointers.push_back(&row); + } + + for (auto i : children_pointers) { + i->deleteObject(); + } + + // Now we build new reprs + Inkscape::XML::Node *mesh = mg->getRepr(); + Inkscape::XML::Node *mesh_array = mg_array->getRepr(); + + SPMeshNodeArray* array = &(mg_array->array); + SPMeshPatchI patch0( &(array->nodes), 0, 0 ); + Geom::Point current_p = patch0.getPoint( 0, 0 ); // Side 0, point 0 + + sp_repr_set_svg_double( mesh, "x", current_p[X] ); + sp_repr_set_svg_double( mesh, "y", current_p[Y] ); + + Geom::Point current_p2( mg->x.computed, mg->y.computed ); + + Inkscape::XML::Document *xml_doc = mesh->document(); + guint rows = array->patch_rows(); + for( guint i = 0; i < rows; ++i ) { + + // Write row + Inkscape::XML::Node *row = xml_doc->createElement("svg:meshrow"); + mesh_array->appendChild( row ); // No attributes + + guint columns = array->patch_columns(); + for( guint j = 0; j < columns; ++j ) { + + // Write patch + Inkscape::XML::Node *patch = xml_doc->createElement("svg:meshpatch"); + + SPMeshPatchI patchi( &(array->nodes), i, j ); + + // Add tensor + if( patchi.tensorIsSet() ) { + + std::stringstream is; + + for( guint k = 0; k < 4; ++k ) { + Geom::Point p = patchi.getTensorPoint( k ) - patchi.getPoint( k, 0 ); + is << p[X] << "," << p[Y]; + if( k < 3 ) is << " "; + } + + patch->setAttribute("tensor", is.str()); + // std::cout << " SPMeshNodeArray::write: tensor: " << is.str() << std::endl; + } + + row->appendChild( patch ); + + // Write sides + for( guint k = 0; k < 4; ++k ) { + + // Only first row has top stop + if( k == 0 && i != 0 ) continue; + + // Only first column has left stop + if( k == 3 && j != 0 ) continue; + + Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop"); + + // Add path + std::stringstream is; + char path_type = patchi.getPathType( k ); + is << path_type; + + std::vector< Geom::Point> p = patchi.getPointsForSide( k ); + current_p = patchi.getPoint( k, 0 ); + + switch ( path_type ) { + case 'l': + is << " " + << ( p[3][X] - current_p[X] ) << "," + << ( p[3][Y] - current_p[Y] ); + break; + case 'L': + is << " " + << p[3][X] << "," + << p[3][Y]; + break; + case 'c': + is << " " + << ( p[1][X] - current_p[X] ) << "," + << ( p[1][Y] - current_p[Y] ) << " " + << ( p[2][X] - current_p[X] ) << "," + << ( p[2][Y] - current_p[Y] ) << " " + << ( p[3][X] - current_p[X] ) << "," + << ( p[3][Y] - current_p[Y] ); + break; + case 'C': + is << " " + << p[1][X] << "," + << p[1][Y] << " " + << p[2][X] << "," + << p[2][Y] << " " + << p[3][X] << "," + << p[3][Y]; + break; + case 'z': + case 'Z': + std::cerr << "SPMeshNodeArray::write(): bad path type" << path_type << std::endl; + break; + default: + std::cerr << "SPMeshNodeArray::write(): unhandled path type" << path_type << std::endl; + } + stop->setAttribute("path", is.str()); + // std::cout << "SPMeshNodeArray::write: path: " << is.str().c_str() << std::endl; + // Add stop-color + if( ( k == 0 && i == 0 && j == 0 ) || + ( k == 1 && i == 0 ) || + ( k == 2 ) || + ( k == 3 && j == 0 ) ) { + + // Why are we setting attribute and not style? + //stop->setAttribute("stop-color", patchi.getColor(k).toString() ); + //stop->setAttribute("stop-opacity", patchi.getOpacity(k) ); + + Inkscape::CSSOStringStream os; + os << "stop-color:" << patchi.getColor(k).toString() << ";stop-opacity:" << patchi.getOpacity(k); + stop->setAttribute("style", os.str()); + } + patch->appendChild( stop ); + } + } + } +} + +/** + * Find default color based on colors in existing fill. + */ +static SPColor default_color( SPItem *item ) { + + SPColor color( 0.5, 0.0, 0.5 ); + + if ( item->style ) { + SPIPaint const &paint = ( item->style->fill ); // Could pick between style.fill/style.stroke + if ( paint.isColor() ) { + color = paint.value.color; + } else if ( paint.isPaintserver() ) { + SPObject const *server = item->style->getFillPaintServer(); + if ( SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector() ) { + SPStop *firstStop = SP_GRADIENT(server)->getVector()->getFirstStop(); + if ( firstStop ) { + color = firstStop->getColor(); + } + } + } + } else { + std::cerr << " SPMeshNodeArray: default_color(): No style" << std::endl; + } + + return color; +} + +/** + Create a default mesh. +*/ +void SPMeshNodeArray::create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ) { + + // std::cout << "SPMeshNodeArray::create: Entrance" << std::endl; + + if( !bbox ) { + // Set default size to bounding box if size not given. + std::cerr << "SPMeshNodeArray::create(): bbox empty" << std::endl; + bbox = item->geometricBounds(); + + if( !bbox ) { + std::cerr << "SPMeshNodeArray::create: ERROR: No bounding box!" << std::endl; + return; + } + } + + Geom::Coord const width = bbox->dimensions()[Geom::X]; + Geom::Coord const height = bbox->dimensions()[Geom::Y]; + Geom::Point center = bbox->midpoint(); + + // Must keep repr and array in sync. We have two choices: + // Build the repr first and then "read" it. + // Construct the array and then "write" it. + // We'll do the second. + + // Remove any existing mesh. We could choose to simply scale an existing mesh... + //clear(); + + // We get called twice when a new mesh is created...WHY? + // return if we've already constructed the mesh. + if( !nodes.empty() ) return; + + // Set 'gradientUnits'. Our calculations assume "userSpaceOnUse". + Inkscape::XML::Node *repr = mg->getRepr(); + repr->setAttribute("gradientUnits", "userSpaceOnUse"); + + // Get default color + SPColor color = default_color( item ); + + // Set some corners to white so we can see the mesh. + SPColor white( 1.0, 1.0, 1.0 ); + if (color == white) { + // If default color is white, set other color to black. + white = SPColor( 0.0, 0.0, 0.0 ); + } + + // Get preferences + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint prows = prefs->getInt("/tools/mesh/mesh_rows", 1); + guint pcols = prefs->getInt("/tools/mesh/mesh_cols", 1); + + SPMeshGeometry mesh_type = + (SPMeshGeometry) prefs->getInt("/tools/mesh/mesh_geometry", SP_MESH_GEOMETRY_NORMAL); + + if( mesh_type == SP_MESH_GEOMETRY_CONICAL ) { + + // Conical gradient.. for any shape/path using geometric bounding box. + + gdouble rx = width/2.0; + gdouble ry = height/2.0; + + // Start and end angles + gdouble start = 0.0; + gdouble end = 2.0 * M_PI; + + if ( SP_IS_STAR( item ) ) { + // But if it is a star... use star parameters! + SPStar* star = SP_STAR( item ); + center = star->center; + rx = star->r[0]; + ry = star->r[0]; + start = star->arg[0]; + end = start + 2.0 * M_PI; + } + + if ( SP_IS_GENERICELLIPSE( item ) ) { + // For arcs use set start/stop + SPGenericEllipse* arc = SP_GENERICELLIPSE( item ); + center[Geom::X] = arc->cx.computed; + center[Geom::Y] = arc->cy.computed; + rx = arc->rx.computed; + ry = arc->ry.computed; + start = arc->start; + end = arc->end; + if( end <= start ) { + end += 2.0 * M_PI; + } + } + + // std::cout << " start: " << start << " end: " << end << std::endl; + + // IS THIS NECESSARY? + sp_repr_set_svg_double( repr, "x", center[Geom::X] + rx * cos(start) ); + sp_repr_set_svg_double( repr, "y", center[Geom::Y] + ry * sin(start) ); + + guint sections = pcols; + + // If less sections, arc approximation error too great. (Check!) + if( sections < 4 ) sections = 4; + + double arc = (end - start) / (double)sections; + + // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve + gdouble kappa = 4.0/3.0 * tan(arc/4.0); + gdouble lenx = rx * kappa; + gdouble leny = ry * kappa; + + gdouble s = start; + for( guint i = 0; i < sections; ++i ) { + + SPMeshPatchI patch( &nodes, 0, i ); + + gdouble x0 = center[Geom::X] + rx * cos(s); + gdouble y0 = center[Geom::Y] + ry * sin(s); + gdouble x1 = x0 - lenx * sin(s); + gdouble y1 = y0 + leny * cos(s); + + s += arc; + gdouble x3 = center[Geom::X] + rx * cos(s); + gdouble y3 = center[Geom::Y] + ry * sin(s); + gdouble x2 = x3 + lenx * sin(s); + gdouble y2 = y3 - leny * cos(s); + + patch.setPoint( 0, 0, Geom::Point( x0, y0 ) ); + patch.setPoint( 0, 1, Geom::Point( x1, y1 ) ); + patch.setPoint( 0, 2, Geom::Point( x2, y2 ) ); + patch.setPoint( 0, 3, Geom::Point( x3, y3 ) ); + + patch.setPoint( 2, 0, center ); + patch.setPoint( 3, 0, center ); + + for( guint k = 0; k < 4; ++k ) { + patch.setPathType( k, 'l' ); + patch.setColor( k, (i+k)%2 ? color : white ); + patch.setOpacity( k, 1.0 ); + } + patch.setPathType( 0, 'c' ); + + // Set handle and tensor nodes. + patch.updateNodes(); + + } + + split_row( 0, prows ); + + } else { + + // Normal grid meshes + + if( SP_IS_GENERICELLIPSE( item ) ) { + + // std::cout << "We've got ourselves an arc!" << std::endl; + + SPGenericEllipse* arc = SP_GENERICELLIPSE( item ); + center[Geom::X] = arc->cx.computed; + center[Geom::Y] = arc->cy.computed; + gdouble rx = arc->rx.computed; + gdouble ry = arc->ry.computed; + + gdouble s = -3.0/2.0 * M_PI_2; + + sp_repr_set_svg_double( repr, "x", center[Geom::X] + rx * cos(s) ); + sp_repr_set_svg_double( repr, "y", center[Geom::Y] + ry * sin(s) ); + + gdouble lenx = rx * 4*tan(M_PI_2/4)/3; + gdouble leny = ry * 4*tan(M_PI_2/4)/3; + + SPMeshPatchI patch( &nodes, 0, 0 ); + for( guint i = 0; i < 4; ++i ) { + + gdouble x0 = center[Geom::X] + rx * cos(s); + gdouble y0 = center[Geom::Y] + ry * sin(s); + gdouble x1 = x0 + lenx * cos(s + M_PI_2); + gdouble y1 = y0 + leny * sin(s + M_PI_2); + + s += M_PI_2; + gdouble x3 = center[Geom::X] + rx * cos(s); + gdouble y3 = center[Geom::Y] + ry * sin(s); + gdouble x2 = x3 + lenx * cos(s - M_PI_2); + gdouble y2 = y3 + leny * sin(s - M_PI_2); + + Geom::Point p1( x1, y1 ); + Geom::Point p2( x2, y2 ); + Geom::Point p3( x3, y3 ); + patch.setPoint( i, 1, p1 ); + patch.setPoint( i, 2, p2 ); + patch.setPoint( i, 3, p3 ); + + patch.setPathType( i, 'c' ); + + patch.setColor( i, i%2 ? color : white ); + patch.setOpacity( i, 1.0 ); + } + + // Fill out tensor points + patch.updateNodes(); + + split_row( 0, prows ); + split_column( 0, pcols ); + + // END Arc + + } else if ( SP_IS_STAR( item ) ) { + + // Do simplest thing... assume star is not rounded or randomized. + // (It should be easy to handle the rounded/randomized cases by making + // the appropriate star class function public.) + SPStar* star = SP_STAR( item ); + guint sides = star->sides; + + // std::cout << "We've got ourselves an star! Sides: " << sides << std::endl; + + Geom::Point p0 = sp_star_get_xy( star, SP_STAR_POINT_KNOT1, 0 ); + sp_repr_set_svg_double( repr, "x", p0[Geom::X] ); + sp_repr_set_svg_double( repr, "y", p0[Geom::Y] ); + + for( guint i = 0; i < sides; ++i ) { + + if( star->flatsided ) { + + SPMeshPatchI patch( &nodes, 0, i ); + + patch.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, i ) ); + guint ii = i+1; + if( ii == sides ) ii = 0; + patch.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, ii ) ); + patch.setPoint( 2, 0, star->center ); + patch.setPoint( 3, 0, star->center ); + + for( guint s = 0; s < 4; ++s ) { + patch.setPathType( s, 'l' ); + patch.setColor( s, (i+s)%2 ? color : white ); + patch.setOpacity( s, 1.0 ); + } + + // Set handle and tensor nodes. + patch.updateNodes(); + + } else { + + SPMeshPatchI patch0( &nodes, 0, 2*i ); + + patch0.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, i ) ); + patch0.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT2, i ) ); + patch0.setPoint( 2, 0, star->center ); + patch0.setPoint( 3, 0, star->center ); + + guint ii = i+1; + if( ii == sides ) ii = 0; + + SPMeshPatchI patch1( &nodes, 0, 2*i+1 ); + + patch1.setPoint( 0, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT2, i ) ); + patch1.setPoint( 1, 0, sp_star_get_xy( star, SP_STAR_POINT_KNOT1, ii ) ); + patch1.setPoint( 2, 0, star->center ); + patch1.setPoint( 3, 0, star->center ); + + for( guint s = 0; s < 4; ++s ) { + patch0.setPathType( s, 'l' ); + patch0.setColor( s, s%2 ? color : white ); + patch0.setOpacity( s, 1.0 ); + patch1.setPathType( s, 'l' ); + patch1.setColor( s, s%2 ? white : color ); + patch1.setOpacity( s, 1.0 ); + } + + // Set handle and tensor nodes. + patch0.updateNodes(); + patch1.updateNodes(); + + } + } + + //print(); + + split_row( 0, prows ); + //split_column( 0, pcols ); + + } else { + + // Generic + + sp_repr_set_svg_double(repr, "x", bbox->min()[Geom::X]); + sp_repr_set_svg_double(repr, "y", bbox->min()[Geom::Y]); + + // Get node array size + guint nrows = prows * 3 + 1; + guint ncols = pcols * 3 + 1; + + gdouble dx = width / (gdouble)(ncols-1.0); + gdouble dy = height / (gdouble)(nrows-1.0); + + Geom::Point p0( mg->x.computed, mg->y.computed ); + + for( guint i = 0; i < nrows; ++i ) { + std::vector< SPMeshNode* > row; + for( guint j = 0; j < ncols; ++j ) { + SPMeshNode* node = new SPMeshNode; + node->p = p0 + Geom::Point( j * dx, i * dy ); + + node->node_edge = MG_NODE_EDGE_NONE; + if( i == 0 ) node->node_edge |= MG_NODE_EDGE_TOP; + if( i == nrows -1 ) node->node_edge |= MG_NODE_EDGE_BOTTOM; + if( j == 0 ) node->node_edge |= MG_NODE_EDGE_LEFT; + if( j == ncols -1 ) node->node_edge |= MG_NODE_EDGE_RIGHT; + + if( i%3 == 0 ) { + + if( j%3 == 0) { + // Corner + node->node_type = MG_NODE_TYPE_CORNER; + node->set = true; + node->color = (i+j)%2 ? color : white; + node->opacity = 1.0; + + } else { + // Side + node->node_type = MG_NODE_TYPE_HANDLE; + node->set = true; + node->path_type = 'c'; + } + + } else { + + if( j%3 == 0) { + // Side + node->node_type = MG_NODE_TYPE_HANDLE; + node->set = true; + node->path_type = 'c'; + } else { + // Tensor + node->node_type = MG_NODE_TYPE_TENSOR; + node->set = false; + } + + } + + row.push_back( node ); + } + nodes.push_back( row ); + } + // End normal + } + + } // If conical + + //print(); + + // Write repr + write( mg ); +} + + +/** + Clear mesh gradient. +*/ +void SPMeshNodeArray::clear() { + + for(auto & node : nodes) { + for(auto & j : node) { + if( j ) { + delete j; + } + } + } + nodes.clear(); +}; + + +/** + Print mesh gradient (for debugging). +*/ +void SPMeshNodeArray::print() { + for( guint i = 0; i < nodes.size(); ++i ) { + std::cout << "New node row:" << std::endl; + for( guint j = 0; j < nodes[i].size(); ++j ) { + if( nodes[i][j] ) { + std::cout.width(4); + std::cout << " Node: " << i << "," << j << ": " + << nodes[i][j]->p + << " Node type: " << nodes[i][j]->node_type + << " Node edge: " << nodes[i][j]->node_edge + << " Set: " << nodes[i][j]->set + << " Path type: " << nodes[i][j]->path_type + << " Stop: " << nodes[i][j]->stop + << std::endl; + } else { + std::cout << "Error: missing mesh node." << std::endl; + } + } // Loop over patches + } // Loop over rows +}; + + + +/* +double hermite( const double p0, const double p1, const double m0, const double m1, const double t ) { + double t2 = t*t; + double t3 = t2*t; + + double result = (2.0*t3 - 3.0*t2 +1.0) * p0 + + (t3 - 2.0*t2 + t) * m0 + + (-2.0*t3 + 3.0*t2) * p1 + + (t3 -t2) * m1; + + return result; +} +*/ + +class SPMeshSmoothCorner { + +public: + SPMeshSmoothCorner() { + for(auto & i : g) { + for( unsigned j = 0; j < 4; ++j ) { + i[j] = 0; + } + } + } + + double g[3][8]; // 3 colors, 8 parameters: see enum. + Geom::Point p; // Location of point +}; + +// Find slope at point 1 given values at previous and next points +// Return value is slope in user space +double find_slope1( const double &p0, const double &p1, const double &p2, + const double &d01, const double &d12 ) { + + double slope = 0; + + if( d01 > 0 && d12 > 0 ) { + slope = 0.5 * ( (p1 - p0)/d01 + (p2 - p1)/d12 ); + + if( ( p0 > p1 && p1 < p2 ) || + ( p0 < p1 && p1 > p2 ) ) { + // At minimum or maximum, use slope of zero + slope = 0; + } else { + // Ensure we don't overshoot + if( fabs(slope) > fabs(3*(p1-p0)/d01) ) { + slope = 3*(p1-p0)/d01; + } + if( fabs(slope) > fabs(3*(p2-p1)/d12) ) { + slope = 3*(p2-p1)/d12; + } + } + } else { + // Do something clever + } + return slope; +}; + + +/* +// Find slope at point 0 given values at previous and next points +// TO DO: TAKE DISTANCE BETWEEN POINTS INTO ACCOUNT +double find_slope2( double pmm, double ppm, double pmp, double ppp, double p0 ) { + + // pmm == d[i-1][j-1], ... 'm' is minus, 'p' is plus + double slope = (ppp - ppm - pmp + pmm)/2.0; + if( (ppp > p0 && ppm > p0 && pmp > p0 && pmm > 0) || + (ppp < p0 && ppm < p0 && pmp < p0 && pmm < 0) ) { + // At minimum or maximum, use slope of zero + slope = 0; + } else { + // Don't really know what to do here + if( fabs(slope) > fabs(3*(ppp-p0)) ) { + slope = 3*(ppp-p0); + } + if( fabs(slope) > fabs(3*(pmp-p0)) ) { + slope = 3*(pmp-p0); + } + if( fabs(slope) > fabs(3*(ppm-p0)) ) { + slope = 3*(ppm-p0); + } + if( fabs(slope) > fabs(3*(pmm-p0)) ) { + slope = 3*(pmm-p0); + } + } + return slope; +} +*/ + +// https://en.wikipedia.org/wiki/Bicubic_interpolation +void invert( const double v[16], double alpha[16] ) { + + const double A[16][16] = { + + { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + {-3, 3, 0, 0, -2,-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 2,-2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2,-1, 0, 0 }, + { 0, 0, 0, 0, 0, 0, 0, 0, 2,-2, 0, 0, 1, 1, 0, 0 }, + {-3, 0, 3, 0, 0, 0, 0, 0, -2, 0,-1, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0,-1, 0 }, + { 9,-9,-9, 9, 6, 3,-6,-3, 6,-6, 3,-3, 4, 2, 2, 1 }, + {-6, 6, 6,-6, -3,-3, 3, 3, -4, 4,-2, 2, -2,-2,-1,-1 }, + { 2, 0,-2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 2, 0,-2, 0, 0, 0, 0, 0, 1, 0, 1, 0 }, + {-6, 6, 6,-6, -4,-2, 4, 2, -3, 3,-3, 3, -2,-1,-2,-1 }, + { 4,-4,-4, 4, 2, 2,-2,-2, 2,-2, 2,-2, 1, 1, 1, 1 } + }; + + for( unsigned i = 0; i < 16; ++i ) { + alpha[i] = 0; + for( unsigned j = 0; j < 16; ++j ) { + alpha[i] += A[i][j]*v[j]; + } + } +} + +double sum( const double alpha[16], const double& x, const double& y ) { + + double result = 0; + + double xx = x*x; + double xxx = xx * x; + double yy = y*y; + double yyy = yy * y; + + result += alpha[ 0 ]; + result += alpha[ 1 ] * x; + result += alpha[ 2 ] * xx; + result += alpha[ 3 ] * xxx; + result += alpha[ 4 ] * y; + result += alpha[ 5 ] * y * x; + result += alpha[ 6 ] * y * xx; + result += alpha[ 7 ] * y * xxx; + result += alpha[ 8 ] * yy; + result += alpha[ 9 ] * yy * x; + result += alpha[ 10 ] * yy * xx; + result += alpha[ 11 ] * yy * xxx; + result += alpha[ 12 ] * yyy; + result += alpha[ 13 ] * yyy * x; + result += alpha[ 14 ] * yyy * xx; + result += alpha[ 15 ] * yyy * xxx; + + return result; +} + +/** + Fill 'smooth' with a smoothed version of the array by subdividing each patch into smaller patches. +*/ +void SPMeshNodeArray::bicubic( SPMeshNodeArray* smooth, SPMeshType type ) { + + + *smooth = *this; // Deep copy via copy assignment constructor, smooth cleared before copy + // std::cout << "SPMeshNodeArray::smooth2(): " << this->patch_rows() << " " << smooth->patch_columns() << std::endl; + // std::cout << " " << smooth << " " << this << std::endl; + + // Find derivatives at corners + + // Create array of corner points + std::vector< std::vector > d; + d.resize( smooth->patch_rows() + 1 ); + for( unsigned i = 0; i < d.size(); ++i ) { + d[i].resize( smooth->patch_columns() + 1 ); + for( unsigned j = 0; j < d[i].size(); ++j ) { + float rgb_color[3]; + this->nodes[ i*3 ][ j*3 ]->color.get_rgb_floatv(rgb_color); + d[i][j].g[0][0] = rgb_color[ 0 ]; + d[i][j].g[1][0] = rgb_color[ 1 ]; + d[i][j].g[2][0] = rgb_color[ 2 ]; + d[i][j].p = this->nodes[ i*3 ][ j*3 ]->p; + } + } + + // Calculate interior derivatives + for( unsigned i = 0; i < d.size(); ++i ) { + for( unsigned j = 0; j < d[i].size(); ++j ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // dx + + if( i != 0 && i != d.size()-1 ) { + double lm = Geom::distance( d[i-1][j].p, d[i][j].p ); + double lp = Geom::distance( d[i+1][j].p, d[i][j].p ); + d[i][j].g[k][1] = find_slope1( d[i-1][j].g[k][0], d[i][j].g[k][0], d[i+1][j].g[k][0], lm, lp ); + } + + // dy + if( j != 0 && j != d[i].size()-1 ) { + double lm = Geom::distance( d[i][j-1].p, d[i][j].p ); + double lp = Geom::distance( d[i][j+1].p, d[i][j].p ); + d[i][j].g[k][2] = find_slope1( d[i][j-1].g[k][0], d[i][j].g[k][0], d[i][j+1].g[k][0], lm, lp ); + } + + // dxdy if needed, need to take lengths into account + // if( i != 0 && i != d.size()-1 && j != 0 && j != d[i].size()-1 ) { + // d[i][j].g[k][3] = find_slope2( d[i-1][j-1].g[k][0], d[i+1][j-1].g[k][0], + // d[i-1][j+1].g[k][0], d[i-1][j-1].g[k][0], + // d[i][j].g[k][0] ); + // } + + } + } + } + + // Calculate exterior derivatives + // We need to do this after calculating interior derivatives as we need to already + // have the non-exterior derivative calculated for finding the parabola. + for( unsigned j = 0; j< d[0].size(); ++j ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // Parabolic + double d0 = Geom::distance( d[1][j].p, d[0 ][j].p ); + if( d0 > 0 ) { + d[0][j].g[k][1] = 2.0*(d[1][j].g[k][0] - d[0 ][j].g[k][0])/d0 - d[1][j].g[k][1]; + } else { + d[0][j].g[k][1] = 0; + } + + unsigned z = d.size()-1; + double dz = Geom::distance( d[z][j].p, d[z-1][j].p ); + if( dz > 0 ) { + d[z][j].g[k][1] = 2.0*(d[z][j].g[k][0] - d[z-1][j].g[k][0])/dz - d[z-1][j].g[k][1]; + } else { + d[z][j].g[k][1] = 0; + } + } + } + + for( unsigned i = 0; i< d.size(); ++i ) { + for( unsigned k = 0; k < 3; ++k ) { // Loop over colors + + // Parabolic + double d0 = Geom::distance( d[i][1].p, d[i][0 ].p ); + if( d0 > 0 ) { + d[i][0].g[k][2] = 2.0*(d[i][1].g[k][0] - d[i][0 ].g[k][0])/d0 - d[i][1].g[k][2]; + } else { + d[i][0].g[k][2] = 0; + } + + unsigned z = d[0].size()-1; + double dz = Geom::distance( d[i][z].p, d[i][z-1].p ); + if( dz > 0 ) { + d[i][z].g[k][2] = 2.0*(d[i][z].g[k][0] - d[i][z-1].g[k][0])/dz - d[i][z-1].g[k][2]; + } else { + d[i][z].g[k][2] = 0; + } + } + } + + // Leave outside corner cross-derivatives at zero. + + // Next split each patch into 8x8 smaller patches. + + // Split each row into eight rows. + // Must do it from end so inserted rows don't mess up indexing + for( int i = smooth->patch_rows() - 1; i >= 0; --i ) { + smooth->split_row( i, unsigned(8) ); + } + + // Split each column into eight columns. + // Must do it from end so inserted columns don't mess up indexing + for( int i = smooth->patch_columns() - 1; i >= 0; --i ) { + smooth->split_column( i, (unsigned)8 ); + } + + // Fill new patches + for( unsigned i = 0; i < this->patch_rows(); ++i ) { + for( unsigned j = 0; j < this->patch_columns(); ++j ) { + + double dx0 = Geom::distance( d[i ][j ].p, d[i+1][j ].p ); + double dx1 = Geom::distance( d[i ][j+1].p, d[i+1][j+1].p ); + double dy0 = Geom::distance( d[i ][j ].p, d[i ][j+1].p ); + double dy1 = Geom::distance( d[i+1][j ].p, d[i+1][j+1].p ); + + // Temp loop over 0..8 to get last column/row edges + float r[3][9][9]; // result + for( unsigned m = 0; m < 3; ++m ) { + + double v[16]; + v[ 0] = d[i ][j ].g[m][0]; + v[ 1] = d[i+1][j ].g[m][0]; + v[ 2] = d[i ][j+1].g[m][0]; + v[ 3] = d[i+1][j+1].g[m][0]; + v[ 4] = d[i ][j ].g[m][1]*dx0; + v[ 5] = d[i+1][j ].g[m][1]*dx0; + v[ 6] = d[i ][j+1].g[m][1]*dx1; + v[ 7] = d[i+1][j+1].g[m][1]*dx1; + v[ 8] = d[i ][j ].g[m][2]*dy0; + v[ 9] = d[i+1][j ].g[m][2]*dy1; + v[10] = d[i ][j+1].g[m][2]*dy0; + v[11] = d[i+1][j+1].g[m][2]*dy1; + v[12] = d[i ][j ].g[m][3]; + v[13] = d[i+1][j ].g[m][3]; + v[14] = d[i ][j+1].g[m][3]; + v[15] = d[i+1][j+1].g[m][3]; + + double alpha[16]; + invert( v, alpha ); + + for( unsigned k = 0; k < 9; ++k ) { + for( unsigned l = 0; l < 9; ++l ) { + double x = k/8.0; + double y = l/8.0; + r[m][k][l] = sum( alpha, x, y ); + // Clamp to allowed values + if( r[m][k][l] > 1.0 ) + r[m][k][l] = 1.0; + if( r[m][k][l] < 0.0 ) + r[m][k][l] = 0.0; + } + } + + } // Loop over colors + + for( unsigned k = 0; k < 9; ++k ) { + for( unsigned l = 0; l < 9; ++l ) { + // Every third node is a corner node + smooth->nodes[ (i*8+k)*3 ][(j*8+l)*3 ]->color.set( r[0][k][l], r[1][k][l], r[2][k][l] ); + } + } + } + } +} + +/** + Number of patch rows. +*/ +guint SPMeshNodeArray::patch_rows() { + + return nodes.size()/3; +} + +/** + Number of patch columns. +*/ +guint SPMeshNodeArray::patch_columns() { + if (nodes.empty()) { + return 0; + } + return nodes[0].size()/3; +} + +/** + Inputs: + i, j: Corner draggable indices. + Returns: + true if corners adjacent. + n[] is array of nodes in top/bottom or left/right order. +*/ +bool SPMeshNodeArray::adjacent_corners( guint i, guint j, SPMeshNode* n[4] ) { + + // This works as all corners have indices and they + // are numbered in order by row and column (and + // the node array is rectangular). + + bool adjacent = false; + + guint c1 = i; + guint c2 = j; + if( j < i ) { + c1 = j; + c2 = i; + } + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + guint crow1 = c1 / ncorners; + guint crow2 = c2 / ncorners; + guint ccol1 = c1 % ncorners; + guint ccol2 = c2 % ncorners; + + guint nrow = crow1 * 3; + guint ncol = ccol1 * 3; + + // std::cout << " i: " << i + // << " j: " << j + // << " ncorners: " << ncorners + // << " c1: " << c1 + // << " crow1: " << crow1 + // << " ccol1: " << ccol1 + // << " c2: " << c2 + // << " crow2: " << crow2 + // << " ccol2: " << ccol2 + // << " nrow: " << nrow + // << " ncol: " << ncol + // << std::endl; + + // Check for horizontal neighbors + if ( crow1 == crow2 && (ccol2 - ccol1) == 1 ) { + adjacent = true; + for( guint k = 0; k < 4; ++k ) { + n[k] = nodes[nrow][ncol+k]; + } + } + + // Check for vertical neighbors + if ( ccol1 == ccol2 && (crow2 - crow1) == 1 ) { + adjacent = true; + for( guint k = 0; k < 4; ++k ) { + n[k] = nodes[nrow+k][ncol]; + } + } + + return adjacent; +} + +/** + Toggle sides between lineto and curve to if both corners selected. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::side_toggle( std::vector corners ) { + + guint toggled = 0; + + if( corners.size() < 2 ) return 0; + + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + SPMeshNode* n[4]; + if( adjacent_corners( corners[i], corners[j], n ) ) { + + gchar path_type = n[1]->path_type; + switch (path_type) + { + case 'L': + n[1]->path_type = 'C'; + n[2]->path_type = 'C'; + n[1]->set = true; + n[2]->set = true; + break; + + case 'l': + n[1]->path_type = 'c'; + n[2]->path_type = 'c'; + n[1]->set = true; + n[2]->set = true; + break; + + case 'C': { + n[1]->path_type = 'L'; + n[2]->path_type = 'L'; + n[1]->set = false; + n[2]->set = false; + // 'L' acts as if handles are 1/3 of path length from corners. + Geom::Point dp = (n[3]->p - n[0]->p)/3.0; + n[1]->p = n[0]->p + dp; + n[2]->p = n[3]->p - dp; + break; + } + case 'c': { + n[1]->path_type = 'l'; + n[2]->path_type = 'l'; + n[1]->set = false; + n[2]->set = false; + // 'l' acts as if handles are 1/3 of path length from corners. + Geom::Point dp = (n[3]->p - n[0]->p)/3.0; + n[1]->p = n[0]->p + dp; + n[2]->p = n[3]->p - dp; + // std::cout << "Toggle sides: " + // << n[0]->p << " " + // << n[1]->p << " " + // << n[2]->p << " " + // << n[3]->p << " " + // << dp << std::endl; + break; + } + default: + std::cout << "Toggle sides: Invalid path type: " << path_type << std::endl; + } + ++toggled; + } + } + } + if( toggled > 0 ) built = false; + return toggled; +} + +/** + * Converts generic Beziers to Beziers approximating elliptical arcs, preserving handle direction. + * There are infinite possible solutions. The solution chosen here is to generate a section of an + * ellipse that is centered on the intersection of the two lines passing through the two nodes but + * parallel to the other node's handle direction. This is the section of an ellipse that + * corresponds to a quarter of a circle squished and then skewed. + */ +guint SPMeshNodeArray::side_arc( std::vector corners ) { + + if( corners.size() < 2 ) return 0; + + guint arced = 0; + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + SPMeshNode* n[4]; + if( adjacent_corners( corners[i], corners[j], n ) ) { + + gchar path_type = n[1]->path_type; + switch (path_type) + { + case 'L': + case 'l': + std::cerr << "SPMeshNodeArray::side_arc: Can't convert straight lines to arcs." << std::endl; + break; + + case 'C': + case 'c': { + + Geom::Ray ray1( n[0]->p, n[1]->p ); + Geom::Ray ray2( n[3]->p, n[2]->p ); + if( !are_parallel( (Geom::Line)ray1, (Geom::Line)ray2 ) ) { + + Geom::OptCrossing crossing = intersection( ray1, ray2 ); + + if( crossing ) { + + Geom::Point intersection = ray1.pointAt( (*crossing).ta ); + + const double f = 4.0/3.0 * tan( M_PI/2.0/4.0 ); + + Geom::Point h1 = intersection - n[0]->p; + Geom::Point h2 = intersection - n[3]->p; + + n[1]->p = n[0]->p + f*h1; + n[2]->p = n[3]->p + f*h2; + ++arced; + + } else { + std::cerr << "SPMeshNodeArray::side_arc: No crossing, can't turn into arc." << std::endl; + } + } else { + std::cerr << "SPMeshNodeArray::side_arc: Handles parallel, can't turn into arc." << std::endl; + } + break; + } + default: + std::cerr << "SPMeshNodeArray::side_arc: Invalid path type: " << n[1]->path_type << std::endl; + } + } + } + } + if( arced > 0 ) built = false; + return arced; +} + +/** + Toggle sides between lineto and curve to if both corners selected. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::tensor_toggle( std::vector corners ) { + + // std::cout << "SPMeshNodeArray::tensor_toggle" << std::endl; + + if( corners.size() < 4 ) return 0; + + guint toggled = 0; + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + for( guint i = 0; i < corners.size()-3; ++i ) { + for( guint j = i+1; j < corners.size()-2; ++j ) { + for( guint k = j+1; k < corners.size()-1; ++k ) { + for( guint l = k+1; l < corners.size(); ++l ) { + + guint c[4]; + c[0] = corners[i]; + c[1] = corners[j]; + c[2] = corners[k]; + c[3] = corners[l]; + std::sort( c, c+4 ); + + // Check we have four corners of one patch selected + if( c[1]-c[0] == 1 && + c[3]-c[2] == 1 && + c[2]-c[0] == ncorners && + c[3]-c[1] == ncorners && + c[0] % ncorners < ncorners - 1 ) { + + // Patch + guint prow = c[0] / ncorners; + guint pcol = c[0] % ncorners; + + // Upper left node of patch + guint irow = prow * 3; + guint jcol = pcol * 3; + + // std::cout << "tensor::toggle: " + // << c[0] << ", " + // << c[1] << ", " + // << c[2] << ", " + // << c[3] << std::endl; + + // std::cout << "tensor::toggle: " + // << " irow: " << irow + // << " jcol: " << jcol + // << " prow: " << prow + // << " pcol: " << pcol + // << std::endl; + + SPMeshPatchI patch( &nodes, prow, pcol ); + patch.updateNodes(); + + if( patch.tensorIsSet() ) { + // Unset tensor points + nodes[irow+1][jcol+1]->set = false; + nodes[irow+1][jcol+2]->set = false; + nodes[irow+2][jcol+1]->set = false; + nodes[irow+2][jcol+2]->set = false; + } else { + // Set tensor points + nodes[irow+1][jcol+1]->set = true; + nodes[irow+1][jcol+2]->set = true; + nodes[irow+2][jcol+1]->set = true; + nodes[irow+2][jcol+2]->set = true; + } + + ++toggled; + } + } + } + } + } + if( toggled > 0 ) built = false; + return toggled; +} + +/** + Attempts to smooth color transitions across corners. + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::color_smooth( std::vector corners ) { + + // std::cout << "SPMeshNodeArray::color_smooth" << std::endl; + + guint smoothed = 0; + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + // Number of node rows and columns + guint ncols = patch_columns() * 3 + 1; + guint nrows = patch_rows() * 3 + 1; + + for(unsigned int corner : corners) { + + // std::cout << "SPMeshNodeArray::color_smooth: " << i << " " << corner << std::endl; + + // Node row & col + guint nrow = (corner / ncorners) * 3; + guint ncol = (corner % ncorners) * 3; + + SPMeshNode* n[7]; + for( guint s = 0; s < 2; ++s ) { + + bool smooth = false; + + // Find neighboring nodes + if( s == 0 ) { + + // Horizontal + if( ncol > 2 && ncol+3 < ncols) { + for( guint j = 0; j < 7; ++j ) { + n[j] = nodes[ nrow ][ ncol - 3 + j ]; + } + smooth = true; + } + + } else { + + // Vertical + if( nrow > 2 && nrow+3 < nrows) { + for( guint j = 0; j < 7; ++j ) { + n[j] = nodes[ nrow - 3 + j ][ ncol ]; + } + smooth = true; + } + } + + if( smooth ) { + + // Let the smoothing begin + // std::cout << " checking: " << ncol << " " << nrow << std::endl; + + // Get initial slopes using closest handles. + double slope[2][3]; + double slope_ave[3]; + double slope_diff[3]; + + // Color of corners + SPColor color0 = n[0]->color; + SPColor color3 = n[3]->color; + SPColor color6 = n[6]->color; + + // Distance nodes from selected corner + Geom::Point d[7]; + for( guint k = 0; k < 7; ++k ) { + d[k]= n[k]->p - n[3]->p; + // std::cout << " d[" << k << "]: " << d[k].length() << std::endl; + } + + double sdm = -1.0; // Slope Diff Max + guint cdm = 0; // Color Diff Max (Which color has the maximum difference in slopes) + for( guint c = 0; c < 3; ++c ) { + if( d[2].length() != 0.0 ) { + slope[0][c] = (color3.v.c[c] - color0.v.c[c]) / d[2].length(); + } + if( d[4].length() != 0.0 ) { + slope[1][c] = (color6.v.c[c] - color3.v.c[c]) / d[4].length(); + } + slope_ave[c] = (slope[0][c]+slope[1][c]) / 2.0; + slope_diff[c] = (slope[0][c]-slope[1][c]); + // std::cout << " color: " << c << " :" + // << color0.v.c[c] << " " + // << color3.v.c[c] << " " + // << color6.v.c[c] + // << " slope: " + // << slope[0][c] << " " + // << slope[1][c] + // << " slope_ave: " << slope_ave[c] + // << " slope_diff: " << slope_diff[c] + // << std::endl; + + // Find color with maximum difference + if( std::abs( slope_diff[c] ) > sdm ) { + sdm = std::abs( slope_diff[c] ); + cdm = c; + } + } + // std::cout << " cdm: " << cdm << std::endl; + + // Find new handle positions: + double length_left = d[0].length(); + double length_right = d[6].length(); + if( slope_ave[ cdm ] != 0.0 ) { + length_left = std::abs( (color3.v.c[cdm] - color0.v.c[cdm]) / slope_ave[ cdm ] ); + length_right = std::abs( (color6.v.c[cdm] - color3.v.c[cdm]) / slope_ave[ cdm ] ); + } + + // Move closest handle a maximum of mid point... but don't shorten + double max = 0.8; + if( length_left > max * d[0].length() && length_left > d[2].length() ) { + std::cout << " Can't smooth left side" << std::endl; + length_left = std::max( max * d[0].length(), d[2].length() ); + } + if( length_right > max * d[6].length() && length_right > d[4].length() ) { + std::cout << " Can't smooth right side" << std::endl; + length_right = std::max( max * d[6].length(), d[4].length() ); + } + + if( d[2].length() != 0.0 ) d[2] *= length_left/d[2].length(); + if( d[4].length() != 0.0 ) d[4] *= length_right/d[4].length(); + + // std::cout << " length_left: " << length_left + // << " d[0]: " << d[0].length() + // << " length_right: " << length_right + // << " d[6]: " << d[6].length() + // << std::endl; + + n[2]->p = n[3]->p + d[2]; + n[4]->p = n[3]->p + d[4]; + + ++smoothed; + } + } + + } + + if( smoothed > 0 ) built = false; + return smoothed; +} + +/** + Pick color from background for selected corners. +*/ +guint SPMeshNodeArray::color_pick( std::vector icorners, SPItem* item ) { + + // std::cout << "SPMeshNodeArray::color_pick" << std::endl; + + guint picked = 0; + + // Code inspired from clone tracing + + // Setup... + + // We need a copy of the drawing so we can hide the mesh. + Inkscape::Drawing *pick_drawing = new Inkscape::Drawing(); + unsigned pick_visionkey = SPItem::display_key_new(1); + + SPDocument *pick_doc = mg->document; + + pick_drawing->setRoot(pick_doc->getRoot()->invoke_show(*pick_drawing, pick_visionkey, SP_ITEM_SHOW_DISPLAY)); + + item->invoke_hide(pick_visionkey); + + pick_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + pick_doc->ensureUpToDate(); + + //gdouble pick_zoom = 1.0; // zoom; + //pick_drawing->root()->setTransform(Geom::Scale(pick_zoom)); + pick_drawing->update(); + + // std::cout << " transform: " << std::endl; + // std::cout << item->transform << std::endl; + // std::cout << " i2doc: " << std::endl; + // std::cout << item->i2doc_affine() << std::endl; + // std::cout << " i2dt: " << std::endl; + // std::cout << item->i2dt_affine() << std::endl; + // std::cout << " dt2i: " << std::endl; + // std::cout << item->dt2i_affine() << std::endl; + SPGradient* gr = SP_GRADIENT( mg ); + // if( gr->gradientTransform_set ) { + // std::cout << " gradient transform set: " << std::endl; + // std::cout << gr->gradientTransform << std::endl; + // } else { + // std::cout << " gradient transform not set! " << std::endl; + // } + + // Do picking + for(unsigned int corner : icorners) { + + SPMeshNode* n = corners[ corner ]; + + // Region to average over + Geom::Point p = n->p; + // std::cout << " before transform: p: " << p << std::endl; + p *= gr->gradientTransform; + // std::cout << " after transform: p: " << p << std::endl; + p *= item->i2doc_affine(); + // std::cout << " after transform: p: " << p << std::endl; + + // If on edge, move inward + guint cols = patch_columns()+1; + guint rows = patch_rows()+1; + guint col = corner % cols; + guint row = corner / cols; + guint ncol = col * 3; + guint nrow = row * 3; + + const double size = 3.0; + + // Top edge + if( row == 0 ) { + Geom::Point dp = nodes[nrow+1][ncol]->p - p; + p += unit_vector( dp ) * size; + } + // Right edge + if( col == cols-1 ) { + Geom::Point dp = nodes[nrow][ncol-1]->p - p; + p += unit_vector( dp ) * size; + } + // Bottom edge + if( row == rows-1 ) { + Geom::Point dp = nodes[nrow-1][ncol]->p - p; + p += unit_vector( dp ) * size; + } + // Left edge + if( col == 0 ) { + Geom::Point dp = nodes[nrow][ncol+1]->p - p; + p += unit_vector( dp ) * size; + } + + Geom::Rect box( p[Geom::X]-size/2.0, p[Geom::Y]-size/2.0, + p[Geom::X]+size/2.0, p[Geom::Y]+size/2.0 ); + + /* Item integer bbox in points */ + Geom::IntRect ibox = box.roundOutwards(); + + /* Find visible area */ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); + Inkscape::DrawingContext dc(s, ibox.min()); + + /* Render copy and pick color */ + pick_drawing->render(dc, ibox); + double R = 0, G = 0, B = 0, A = 0; + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + + // std::cout << " p: " << p + // << " box: " << ibox + // << " R: " << R + // << " G: " << G + // << " B: " << B + // << std::endl; + n->color.set( R, G, B ); + } + + pick_doc->getRoot()->invoke_hide(pick_visionkey); + delete pick_drawing; + + picked = 1; // Picking always happens + if( picked > 0 ) built = false; + return picked; +} + +/** + Splits selected rows and/or columns in half (according to the path 't' parameter). + Input is a list of selected corner draggable indices. +*/ +guint SPMeshNodeArray::insert( std::vector corners ) { + + guint inserted = 0; + + if( corners.size() < 2 ) return 0; + + std::set columns; + std::set rows; + + for( guint i = 0; i < corners.size()-1; ++i ) { + for( guint j = i+1; j < corners.size(); ++j ) { + + // This works as all corners have indices and they + // are numbered in order by row and column (and + // the node array is rectangular). + + guint c1 = corners[i]; + guint c2 = corners[j]; + if (c2 < c1) { + c1 = corners[j]; + c2 = corners[i]; + } + + // Number of corners in a row of patches. + guint ncorners = patch_columns() + 1; + + guint crow1 = c1 / ncorners; + guint crow2 = c2 / ncorners; + guint ccol1 = c1 % ncorners; + guint ccol2 = c2 % ncorners; + + // Check for horizontal neighbors + if ( crow1 == crow2 && (ccol2 - ccol1) == 1 ) { + columns.insert( ccol1 ); + } + + // Check for vertical neighbors + if ( ccol1 == ccol2 && (crow2 - crow1) == 1 ) { + rows.insert( crow1 ); + } + } + } + + // Iterate backwards so column/row numbers are not invalidated. + std::set::reverse_iterator rit; + for (rit=columns.rbegin(); rit != columns.rend(); ++rit) { + split_column( *rit, 0.5); + ++inserted; + } + for (rit=rows.rbegin(); rit != rows.rend(); ++rit) { + split_row( *rit, 0.5); + ++inserted; + } + + if( inserted > 0 ) built = false; + return inserted; +} + +/** + Moves handles in response to a corner node move. + p_old: original position of moved corner node. + corner: the corner node moved (draggable index, i.e. point_i). + selected: list of all corners selected (draggable indices). + op: how other corners should be moved. + Corner node must already have been moved! +*/ +void SPMeshNodeArray::update_handles( guint corner, std::vector< guint > /*selected*/, Geom::Point p_old, MeshNodeOperation /*op*/ ) +{ + if (!draggers_valid) { + std::cerr << "SPMeshNodeArray::update_handles: Draggers not valid!" << std::endl; + return; + } + // assert( draggers_valid ); + + // std::cout << "SPMeshNodeArray::update_handles: " + // << " corner: " << corner + // << " op: " << op + // << std::endl; + + // Find number of patch rows and columns + guint mrow = patch_rows(); + guint mcol = patch_columns(); + + // Number of corners in a row of patches. + guint ncorners = mcol + 1; + + // Find corner row/column + guint crow = corner / ncorners; + guint ccol = corner % ncorners; + + // Find node row/column + guint nrow = crow * 3; + guint ncol = ccol * 3; + + // std::cout << " mrow: " << mrow + // << " mcol: " << mcol + // << " crow: " << crow + // << " ccol: " << ccol + // << " ncorners: " << ncorners + // << " nrow: " << nrow + // << " ncol: " << ncol + // << std::endl; + + // New corner mesh coordinate. + Geom::Point p_new = nodes[nrow][ncol]->p; + + // Corner point move dpg in mesh coordinate system. + Geom::Point dp = p_new - p_old; + + // std::cout << " p_old: " << p_old << std::endl; + // std::cout << " p_new: " << p_new << std::endl; + // std::cout << " dp: " << dp << std::endl; + + // STEP 1: ONLY DO DIRECT MOVE + bool patch[4]; + patch[0] = patch[1] = patch[2] = patch[3] = false; + if( ccol > 0 && crow > 0 ) patch[0] = true; + if( ccol < mcol && crow > 0 ) patch[1] = true; + if( ccol < mcol && crow < mrow ) patch[2] = true; + if( ccol > 0 && crow < mrow ) patch[3] = true; + + // std::cout << patch[0] << " " + // << patch[1] << " " + // << patch[2] << " " + // << patch[3] << std::endl; + + // Move handles + if( patch[0] || patch[1] ) { + if( nodes[nrow-1][ncol]->path_type == 'l' || + nodes[nrow-1][ncol]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow-3][ncol]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow-1][ncol ]->p = nodes[nrow][ncol]->p + s; + nodes[nrow-2][ncol ]->p = nodes[nrow-3][ncol]->p - s; + } else { + nodes[nrow-1][ncol ]->p += dp; + } + } + + if( patch[1] || patch[2] ) { + if( nodes[nrow ][ncol+1]->path_type == 'l' || + nodes[nrow ][ncol+1]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow][ncol+3]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow ][ncol+1]->p = nodes[nrow][ncol]->p + s; + nodes[nrow ][ncol+2]->p = nodes[nrow][ncol+3]->p - s; + } else { + nodes[nrow ][ncol+1]->p += dp; + } + } + + if( patch[2] || patch[3] ) { + if( nodes[nrow+1][ncol ]->path_type == 'l' || + nodes[nrow+1][ncol ]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow+3][ncol]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow+1][ncol ]->p = nodes[nrow][ncol]->p + s; + nodes[nrow+2][ncol ]->p = nodes[nrow+3][ncol]->p - s; + } else { + nodes[nrow+1][ncol ]->p += dp; + } + } + + if( patch[3] || patch[0] ) { + if( nodes[nrow ][ncol-1]->path_type == 'l' || + nodes[nrow ][ncol-1]->path_type == 'L' ) { + Geom::Point s = (nodes[nrow][ncol-3]->p - nodes[nrow][ncol]->p)/3.0; + nodes[nrow ][ncol-1]->p = nodes[nrow][ncol]->p + s; + nodes[nrow ][ncol-2]->p = nodes[nrow][ncol-3]->p - s; + } else { + nodes[nrow ][ncol-1]->p += dp; + } + } + + + // Move tensors + if( patch[0] ) nodes[nrow-1][ncol-1]->p += dp; + if( patch[1] ) nodes[nrow-1][ncol+1]->p += dp; + if( patch[2] ) nodes[nrow+1][ncol+1]->p += dp; + if( patch[3] ) nodes[nrow+1][ncol-1]->p += dp; + + // // Check if neighboring corners are selected. + + // bool do_scale = false; + + // bool do_scale_xp = do_scale; + // bool do_scale_xn = do_scale; + // bool do_scale_yp = do_scale; + // bool do_scale_yn = do_scale; + + // if( ccol < mcol+1 ) { + // if( std::find( sc.begin(), sc.end(), point_i + 1 ) != sc.end() ) { + // do_scale_xp = false; + // std::cout << " Not scaling x+" << std::endl; + // } + // } + + // if( ccol > 0 ) { + // if( std::find( sc.begin(), sc.end(), point_i - 1 ) != sc.end() ) { + // do_scale_xn = false; + // std::cout << " Not scaling x-" << std::endl; + // } + // } + + // if( crow < mrow+1 ) { + // if( std::find( sc.begin(), sc.end(), point_i + ncorners ) != sc.end() ) { + // do_scale_yp = false; + // std::cout << " Not scaling y+" << std::endl; + // } + // } + + // if( crow > 0 ) { + // if( std::find( sc.begin(), sc.end(), point_i - ncorners ) != sc.end() ) { + // do_scale_yn = false; + // std::cout << " Not scaling y-" << std::endl; + // } + // } + + // // We have four patches to adjust... + // for ( guint k = 0; k < 4; ++k ) { + + // bool do_scale_x = do_scale; + // bool do_scale_y = do_scale; + + // SPMeshNode* pnodes[4][4]; + + // // Load up matrix + // switch (k) { + + // case 0: + // if( crow < mrow+1 && ccol < mcol+1 ) { + // // Bottom right patch + + // do_scale_x = do_scale_xp; + // do_scale_y = do_scale_yp; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[i][j] = mg->array.nodes[nrow+i][nrow+j]; + // } + // } + // } + // break; + + // case 1: + // if( crow < mrow+1 && ccol > 0 ) { + // // Bottom left patch (note x, y swapped) + + // do_scale_y = do_scale_xn; + // do_scale_x = do_scale_yp; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[j][i] = mg->array.nodes[nrow+i][nrow-j]; + // } + // } + // } + // break; + + // case 2: + // if( crow > 0 && ccol > 0 ) { + // // Top left patch + + // do_scale_x = do_scale_xn; + // do_scale_y = do_scale_yn; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[i][j] = mg->array.nodes[nrow-i][nrow-j]; + // } + // } + // } + // break; + + // case 3: + // if( crow > 0 && ccol < mcol+1 ) { + // // Top right patch (note x, y swapped) + + // do_scale_y = do_scale_xp; + // do_scale_x = do_scale_yn; + + // for( guint i = 0; i < 4; ++i ) { + // for( guint j = 0; j< 4; ++j ) { + // pnodes[j][i] = mg->array.nodes[nrow-i][nrow+j]; + // } + // } + // } + // break; + // } + + // // Now we must move points in both x and y. + // // There are upto six points to move: P01, P02, P11, P12, P21, P22. + // // (The points P10, P20 will be moved in another branch of the loop. + // // The points P03, P13, P23, P33, P32, P31, P30 are not moved.) + // // + // // P00 P01 P02 P03 + // // P10 P11 P12 P13 + // // P20 P21 P22 P23 + // // P30 P31 P32 P33 + // // + // // The goal is to preserve the direction of the handle! + + + // Geom::Point dsx_new = pnodes[0][3]->p - pnodes[0][0]->p; // New side x + // Geom::Point dsy_new = pnodes[3][0]->p - pnodes[0][0]->p; // New side y + // Geom::Point dsx_old = pnodes[0][3]->p - pcg_old; // Old side x + // Geom::Point dsy_old = pnodes[3][0]->p - pcg_old; // Old side y + + + // double scale_factor_x = 1.0; + // if( dsx_old.length() != 0.0 ) scale_factor_x = dsx_new.length()/dsx_old.length(); + + // double scale_factor_y = 1.0; + // if( dsy_old.length() != 0.0 ) scale_factor_y = dsy_new.length()/dsy_old.length(); + + + // if( do_scalex && do_scaley ) { + + // // We have six point to move. + + // // P01 + // Geom::Point dp01 = pnodes[0][1] - pcg_old; + // dp01 *= scale_factor_x; + // pnodes[0][1] = pnodes[0][0] + dp01; + + // // P02 + // Geom::Point dp02 = pnodes[0][2] - pnodes[0][3]; + // dp02 *= scale_factor_x; + // pnodes[0][2] = pnodes[0][3] + dp02; + + // // P11 + // Geom::Point dp11 = pnodes[1][1] - pcg_old; + // dp11 *= scale_factor_x; + // pnodes[1][1] = pnodes[0][0] + dp11; + + + + // // P21 + // Geom::Point dp21 = pnodes[2][1] - pnodes[3][0]; + // dp21 *= scale_factor_x; + // dp21 *= scale_factor_y; + // pnodes[2][1] = pnodes[3][0] + dp21; + + + // Geom::Point dsx1 = pnodes[0][1]->p - +} + + +SPCurve * SPMeshNodeArray::outline_path() { + + SPCurve *outline = new SPCurve(); + + if (nodes.empty() ) { + std::cerr << "SPMeshNodeArray::outline_path: empty array!" << std::endl; + return outline; + } + + outline->moveto( nodes[0][0]->p ); + + int ncol = nodes[0].size(); + int nrow = nodes.size(); + + // Top + for (int i = 1; i < ncol; i += 3 ) { + outline->curveto( nodes[0][i]->p, nodes[0][i+1]->p, nodes[0][i+2]->p); + } + + // Right + for (int i = 1; i < nrow; i += 3 ) { + outline->curveto( nodes[i][ncol-1]->p, nodes[i+1][ncol-1]->p, nodes[i+2][ncol-1]->p); + } + + // Bottom (right to left) + for (int i = 1; i < ncol; i += 3 ) { + outline->curveto( nodes[nrow-1][ncol-i-1]->p, nodes[nrow-1][ncol-i-2]->p, nodes[nrow-1][ncol-i-3]->p); + } + + // Left (bottom to top) + for (int i = 1; i < nrow; i += 3 ) { + outline->curveto( nodes[nrow-i-1][0]->p, nodes[nrow-i-2][0]->p, nodes[nrow-i-3][0]->p); + } + + outline->closepath(); + + return outline; +} + +void SPMeshNodeArray::transform(Geom::Affine const &m) { + + for (int i = 0; i < nodes[0].size(); ++i) { + for (auto & node : nodes) { + node[i]->p *= m; + } + } +} + +// Transform mesh to fill box. Return true if mesh transformed. +bool SPMeshNodeArray::fill_box(Geom::OptRect &box) { + + // If gradientTransfor is set (as happens when an object is transformed + // with the "optimized" preferences set true), we need to remove it. + if (mg->gradientTransform_set) { + Geom::Affine gt = mg->gradientTransform; + transform( gt ); + mg->gradientTransform_set = false; + mg->gradientTransform.setIdentity(); + } + + SPCurve *outline = outline_path(); + Geom::OptRect mesh_bbox = outline->get_pathvector().boundsExact(); + outline->unref(); + + if ((*mesh_bbox).width() == 0 || (*mesh_bbox).height() == 0) { + return false; + } + + double scale_x = (*box).width() /(*mesh_bbox).width() ; + double scale_y = (*box).height()/(*mesh_bbox).height(); + + Geom::Translate t1(-(*mesh_bbox).min()); + Geom::Scale scale(scale_x,scale_y); + Geom::Translate t2((*box).min()); + Geom::Affine trans = t1 * scale * t2; + if (!trans.isIdentity() ) { + transform(trans); + write( mg ); + mg->requestModified(SP_OBJECT_MODIFIED_FLAG); + return true; + } + + return false; +} + +// Defined in gradient-chemistry.cpp +guint32 average_color(guint32 c1, guint32 c2, gdouble p); + +/** + Split a row into n equal parts. +*/ +void SPMeshNodeArray::split_row( unsigned int row, unsigned int n ) { + + double nn = n; + if( n > 1 ) split_row( row, (nn-1)/nn ); + if( n > 2 ) split_row( row, n-1 ); +} + +/** + Split a column into n equal parts. +*/ +void SPMeshNodeArray::split_column( unsigned int col, unsigned int n ) { + + double nn = n; + if( n > 1 ) split_column( col, (nn-1)/nn ); + if( n > 2 ) split_column( col, n-1 ); +} + +/** + Split a row into two rows at coord (fraction of row height). +*/ +void SPMeshNodeArray::split_row( unsigned int row, double coord ) { + + // std::cout << "Splitting row: " << row << " at " << coord << std::endl; + // print(); + assert( coord >= 0.0 && coord <= 1.0 ); + assert( row < patch_rows() ); + + built = false; + + // First step is to ensure that handle and tensor points are up-to-date if they are not set. + // (We can't do this on the fly as we overwrite the necessary points to do the calculation + // during the update.) + for( guint j = 0; j < patch_columns(); ++ j ) { + SPMeshPatchI patch( &nodes, row, j ); + patch.updateNodes(); + } + + // Add three new rows of empty nodes + for( guint i = 0; i < 3; ++i ) { + std::vector< SPMeshNode* > new_row; + for( guint j = 0; j < nodes[0].size(); ++j ) { + SPMeshNode* new_node = new SPMeshNode; + new_row.push_back( new_node ); + } + nodes.insert( nodes.begin()+3*(row+1), new_row ); + } + + guint i = 3 * row; // Convert from patch row to node row + for( guint j = 0; j < nodes[i].size(); ++j ) { + + // std::cout << "Splitting row: column: " << j << std::endl; + + Geom::Point p[4]; + for( guint k = 0; k < 4; ++k ) { + guint n = k; + if( k == 3 ) n = 6; // Bottom patch row has been shifted by new rows + p[k] = nodes[i+n][j]->p; + // std::cout << p[k] << std::endl; + } + + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + + std::pair, Geom::BezierCurveN<3> > b_new = + b.subdivide( coord ); + + // Update points + for( guint n = 0; n < 4; ++n ) { + nodes[i+n ][j]->p = b_new.first[n]; + nodes[i+n+3][j]->p = b_new.second[n]; + // std::cout << b_new.first[n] << " " << b_new.second[n] << std::endl; + } + + if( nodes[i][j]->node_type == MG_NODE_TYPE_CORNER ) { + // We are splitting a side + + // Path type stored in handles. + gchar path_type = nodes[i+1][j]->path_type; + nodes[i+4][j]->path_type = path_type; + nodes[i+5][j]->path_type = path_type; + bool set = nodes[i+1][j]->set; + nodes[i+4][j]->set = set; + nodes[i+5][j]->set = set; + nodes[i+4][j]->node_type = MG_NODE_TYPE_HANDLE; + nodes[i+5][j]->node_type = MG_NODE_TYPE_HANDLE; + + // Color stored in corners + guint c0 = nodes[i ][j]->color.toRGBA32( 1.0 ); + guint c1 = nodes[i+6][j]->color.toRGBA32( 1.0 ); + gdouble o0 = nodes[i ][j]->opacity; + gdouble o1 = nodes[i+6][j]->opacity; + guint cnew = average_color( c0, c1, coord ); + gdouble onew = o0 * (1.0 - coord) + o1 * coord; + nodes[i+3][j]->color.set( cnew ); + nodes[i+3][j]->opacity = onew; + nodes[i+3][j]->node_type = MG_NODE_TYPE_CORNER; + nodes[i+3][j]->set = true; + + } else { + // We are splitting a middle + + bool set = nodes[i+1][j]->set || nodes[i+2][j]->set; + nodes[i+4][j]->set = set; + nodes[i+5][j]->set = set; + nodes[i+4][j]->node_type = MG_NODE_TYPE_TENSOR; + nodes[i+5][j]->node_type = MG_NODE_TYPE_TENSOR; + + // Path type, if different, choose l -> L -> c -> C. + gchar path_type0 = nodes[i ][j]->path_type; + gchar path_type1 = nodes[i+6][j]->path_type; + gchar path_type = 'l'; + if( path_type0 == 'L' || path_type1 == 'L') path_type = 'L'; + if( path_type0 == 'c' || path_type1 == 'c') path_type = 'c'; + if( path_type0 == 'C' || path_type1 == 'C') path_type = 'C'; + nodes[i+3][j]->path_type = path_type; + nodes[i+3][j]->node_type = MG_NODE_TYPE_HANDLE; + if( path_type == 'c' || path_type == 'C' ) nodes[i+3][j]->set = true; + + } + + nodes[i+3][j]->node_edge = MG_NODE_EDGE_NONE; + nodes[i+4][j]->node_edge = MG_NODE_EDGE_NONE; + nodes[i+5][j]->node_edge = MG_NODE_EDGE_NONE;; + if( j == 0 ) { + nodes[i+3][j]->node_edge |= MG_NODE_EDGE_LEFT; + nodes[i+4][j]->node_edge |= MG_NODE_EDGE_LEFT; + nodes[i+5][j]->node_edge |= MG_NODE_EDGE_LEFT; + } + if( j == nodes[i].size() - 1 ) { + nodes[i+3][j]->node_edge |= MG_NODE_EDGE_RIGHT; + nodes[i+4][j]->node_edge |= MG_NODE_EDGE_RIGHT; + nodes[i+5][j]->node_edge |= MG_NODE_EDGE_RIGHT; + } + } + + // std::cout << "Splitting row: result:" << std::endl; + // print(); +} + + + +/** + Split a column into two columns at coord (fraction of column width). +*/ +void SPMeshNodeArray::split_column( unsigned int col, double coord ) { + + // std::cout << "Splitting column: " << col << " at " << coord << std::endl; + // print(); + assert( coord >= 0.0 && coord <= 1.0 ); + assert( col < patch_columns() ); + + built = false; + + // First step is to ensure that handle and tensor points are up-to-date if they are not set. + // (We can't do this on the fly as we overwrite the necessary points to do the calculation + // during the update.) + for( guint i = 0; i < patch_rows(); ++ i ) { + SPMeshPatchI patch( &nodes, i, col ); + patch.updateNodes(); + } + + guint j = 3 * col; // Convert from patch column to node column + for( guint i = 0; i < nodes.size(); ++i ) { + + // std::cout << "Splitting column: row: " << i << std::endl; + + Geom::Point p[4]; + for( guint k = 0; k < 4; ++k ) { + p[k] = nodes[i][j+k]->p; + } + + Geom::BezierCurveN<3> b( p[0], p[1], p[2], p[3] ); + + std::pair, Geom::BezierCurveN<3> > b_new = + b.subdivide( coord ); + + // Add three new nodes + for( guint n = 0; n < 3; ++n ) { + SPMeshNode* new_node = new SPMeshNode; + nodes[i].insert( nodes[i].begin()+j+3, new_node ); + } + + // Update points + for( guint n = 0; n < 4; ++n ) { + nodes[i][j+n]->p = b_new.first[n]; + nodes[i][j+n+3]->p = b_new.second[n]; + } + + if( nodes[i][j]->node_type == MG_NODE_TYPE_CORNER ) { + // We are splitting a side + + // Path type stored in handles. + gchar path_type = nodes[i][j+1]->path_type; + nodes[i][j+4]->path_type = path_type; + nodes[i][j+5]->path_type = path_type; + bool set = nodes[i][j+1]->set; + nodes[i][j+4]->set = set; + nodes[i][j+5]->set = set; + nodes[i][j+4]->node_type = MG_NODE_TYPE_HANDLE; + nodes[i][j+5]->node_type = MG_NODE_TYPE_HANDLE; + + // Color stored in corners + guint c0 = nodes[i][j ]->color.toRGBA32( 1.0 ); + guint c1 = nodes[i][j+6]->color.toRGBA32( 1.0 ); + gdouble o0 = nodes[i][j ]->opacity; + gdouble o1 = nodes[i][j+6]->opacity; + guint cnew = average_color( c0, c1, coord ); + gdouble onew = o0 * (1.0 - coord) + o1 * coord; + nodes[i][j+3]->color.set( cnew ); + nodes[i][j+3]->opacity = onew; + nodes[i][j+3]->node_type = MG_NODE_TYPE_CORNER; + nodes[i][j+3]->set = true; + + } else { + // We are splitting a middle + + bool set = nodes[i][j+1]->set || nodes[i][j+2]->set; + nodes[i][j+4]->set = set; + nodes[i][j+5]->set = set; + nodes[i][j+4]->node_type = MG_NODE_TYPE_TENSOR; + nodes[i][j+5]->node_type = MG_NODE_TYPE_TENSOR; + + // Path type, if different, choose l -> L -> c -> C. + gchar path_type0 = nodes[i][j ]->path_type; + gchar path_type1 = nodes[i][j+6]->path_type; + gchar path_type = 'l'; + if( path_type0 == 'L' || path_type1 == 'L') path_type = 'L'; + if( path_type0 == 'c' || path_type1 == 'c') path_type = 'c'; + if( path_type0 == 'C' || path_type1 == 'C') path_type = 'C'; + nodes[i][j+3]->path_type = path_type; + nodes[i][j+3]->node_type = MG_NODE_TYPE_HANDLE; + if( path_type == 'c' || path_type == 'C' ) nodes[i][j+3]->set = true; + + } + + nodes[i][j+3]->node_edge = MG_NODE_EDGE_NONE; + nodes[i][j+4]->node_edge = MG_NODE_EDGE_NONE; + nodes[i][j+5]->node_edge = MG_NODE_EDGE_NONE;; + if( i == 0 ) { + nodes[i][j+3]->node_edge |= MG_NODE_EDGE_TOP; + nodes[i][j+4]->node_edge |= MG_NODE_EDGE_TOP; + nodes[i][j+5]->node_edge |= MG_NODE_EDGE_TOP; + } + if( i == nodes.size() - 1 ) { + nodes[i][j+3]->node_edge |= MG_NODE_EDGE_BOTTOM; + nodes[i][j+4]->node_edge |= MG_NODE_EDGE_BOTTOM; + nodes[i][j+5]->node_edge |= MG_NODE_EDGE_BOTTOM; + } + + } + + // std::cout << "Splitting col: result:" << std::endl; + // print(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-array.h b/src/object/sp-mesh-array.h new file mode 100644 index 0000000..d2e3be9 --- /dev/null +++ b/src/object/sp-mesh-array.h @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESH_ARRAY_H +#define SEEN_SP_MESH_ARRAY_H +/* + * Authors: + * Tavmjong Bah + * + * Copyrigt (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A group of classes and functions for manipulating mesh gradients. + + A mesh is made up of an array of patches. Each patch has four sides and four corners. The sides can + be shared between two patches and the corners between up to four. + + The order of the points for each side always goes from left to right or top to bottom. + For sides 2 and 3 the points must be reversed when used (as in calls to cairo functions). + + Two patches: (C=corner, S=side, H=handle, T=tensor) + + C0 H1 H2 C1 C0 H1 H2 C1 + + ---------- + ---------- + + | S0 | S0 | + H1 | T0 T1 |H1 T0 T1 | H1 + |S3 S1|S3 S1| + H2 | T3 T2 |H2 T3 T2 | H2 + | S2 | S2 | + + ---------- + ---------- + + C3 H1 H2 C2 C3 H1 H2 C2 + + The mesh is stored internally as an array of nodes that includes the tensor nodes. + + Note: This code uses tensor points which are not part of the SVG2 plan at the moment. + Including tensor points was motivated by a desire to experiment with their usefulness + in smoothing color transitions. There doesn't seem to be much advantage for that + purpose. However including them internally allows for storing all the points in + an array which simplifies things like inserting new rows or columns. +*/ + +#include <2geom/point.h> +#include "color.h" + +// For color picking +#include "sp-item.h" + +enum SPMeshType { + SP_MESH_TYPE_COONS, + SP_MESH_TYPE_BICUBIC +}; + +enum SPMeshGeometry { + SP_MESH_GEOMETRY_NORMAL, + SP_MESH_GEOMETRY_CONICAL +}; + +enum NodeType { + MG_NODE_TYPE_UNKNOWN, + MG_NODE_TYPE_CORNER, + MG_NODE_TYPE_HANDLE, + MG_NODE_TYPE_TENSOR +}; + +// Is a node along an edge? +enum NodeEdge { + MG_NODE_EDGE_NONE, + MG_NODE_EDGE_TOP = 1, + MG_NODE_EDGE_LEFT = 2, + MG_NODE_EDGE_BOTTOM = 4, + MG_NODE_EDGE_RIGHT = 8 +}; + +enum MeshCornerOperation { + MG_CORNER_SIDE_TOGGLE, + MG_CORNER_SIDE_ARC, + MG_CORNER_TENSOR_TOGGLE, + MG_CORNER_COLOR_SMOOTH, + MG_CORNER_COLOR_PICK, + MG_CORNER_INSERT +}; + +enum MeshNodeOperation { + MG_NODE_NO_SCALE, + MG_NODE_SCALE, + MG_NODE_SCALE_HANDLE +}; + +class SPStop; + +class SPMeshNode { +public: + SPMeshNode() { + node_type = MG_NODE_TYPE_UNKNOWN; + node_edge = MG_NODE_EDGE_NONE; + set = false; + draggable = -1; + path_type = 'u'; + opacity = 0.0; + stop = nullptr; + } + NodeType node_type; + unsigned int node_edge; + bool set; + Geom::Point p; + unsigned int draggable; // index of on-screen node + char path_type; + SPColor color; + double opacity; + SPStop *stop; // Stop corresponding to node. +}; + + +// I for Internal to distinguish it from the Object class +// This is a convenience class... +class SPMeshPatchI { + +private: + std::vector > *nodes; + int row; + int col; + +public: + SPMeshPatchI( std::vector > *n, int r, int c ); + Geom::Point getPoint( unsigned int side, unsigned int point ); + std::vector< Geom::Point > getPointsForSide( unsigned int i ); + void setPoint( unsigned int side, unsigned int point, Geom::Point p, bool set = true ); + char getPathType( unsigned int i ); + void setPathType( unsigned int, char t ); + Geom::Point getTensorPoint( unsigned int i ); + void setTensorPoint( unsigned int i, Geom::Point p ); + bool tensorIsSet(); + bool tensorIsSet( unsigned int i ); + Geom::Point coonsTensorPoint( unsigned int i ); + void updateNodes(); + SPColor getColor( unsigned int i ); + void setColor( unsigned int i, SPColor c ); + double getOpacity( unsigned int i ); + void setOpacity( unsigned int i, double o ); + SPStop* getStopPtr( unsigned int i ); + void setStopPtr( unsigned int i, SPStop* ); +}; + +class SPMeshGradient; +class SPCurve; + +// An array of mesh nodes. +class SPMeshNodeArray { + +// Should be private +public: + SPMeshGradient *mg; + std::vector< std::vector< SPMeshNode* > > nodes; + +public: + // Draggables to nodes + bool draggers_valid; + std::vector< SPMeshNode* > corners; + std::vector< SPMeshNode* > handles; + std::vector< SPMeshNode* > tensors; + +public: + + friend class SPMeshPatchI; + + SPMeshNodeArray() { built = false; mg = nullptr; draggers_valid = false; }; + SPMeshNodeArray( SPMeshGradient *mg ); + SPMeshNodeArray( const SPMeshNodeArray& rhs ); + SPMeshNodeArray& operator=(const SPMeshNodeArray& rhs); + + ~SPMeshNodeArray() { clear(); }; + bool built; + + bool read( SPMeshGradient *mg ); + void write( SPMeshGradient *mg ); + void create( SPMeshGradient *mg, SPItem *item, Geom::OptRect bbox ); + void clear(); + void print(); + + // Fill 'smooth' with a smoothed version by subdividing each patch. + void bicubic( SPMeshNodeArray* smooth, SPMeshType type); + + // Get size of patch + unsigned int patch_rows(); + unsigned int patch_columns(); + + SPMeshNode * node( unsigned int i, unsigned int j ) { return nodes[i][j]; } + + // Operations on corners + bool adjacent_corners( unsigned int i, unsigned int j, SPMeshNode* n[4] ); + unsigned int side_toggle( std::vector< unsigned int > ); + unsigned int side_arc( std::vector< unsigned int > ); + unsigned int tensor_toggle( std::vector< unsigned int > ); + unsigned int color_smooth( std::vector< unsigned int > ); + unsigned int color_pick( std::vector< unsigned int >, SPItem* ); + unsigned int insert( std::vector< unsigned int > ); + + // Update other nodes in response to a node move. + void update_handles( unsigned int corner, std::vector< unsigned int > selected_corners, Geom::Point old_p, MeshNodeOperation op ); + + // Return outline path (don't forget to unref() when done with curve) + SPCurve * outline_path(); + + // Transform array + void transform(Geom::Affine const &m); + + // Transform mesh to fill box. Return true if not identity transform. + bool fill_box(Geom::OptRect &box); + + // Find bounding box + // Geom::OptRect findBoundingBox(); + + void split_row( unsigned int i, unsigned int n ); + void split_column( unsigned int j, unsigned int n ); + void split_row( unsigned int i, double coord ); + void split_column( unsigned int j, double coord ); +}; + +#endif /* !SEEN_SP_MESH_ARRAY_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + c-basic-offset:2 + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-gradient.cpp b/src/object/sp-mesh-gradient.cpp new file mode 100644 index 0000000..89b5d08 --- /dev/null +++ b/src/object/sp-mesh-gradient.cpp @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include + +#include "attributes.h" +#include "display/cairo-utils.h" + +#include "sp-mesh-gradient.h" + +/* + * Mesh Gradient + */ +//#define MESH_DEBUG +//#define OBJECT_TRACE + +SPMeshGradient::SPMeshGradient() : SPGradient(), type( SP_MESH_TYPE_COONS ), type_set(false) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::SPMeshGradient" ); +#endif + + // Start coordinate of mesh + this->x.unset(SVGLength::NONE, 0.0, 0.0); + this->y.unset(SVGLength::NONE, 0.0, 0.0); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::SPMeshGradient", false ); +#endif +} + +SPMeshGradient::~SPMeshGradient() { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::~SPMeshGradient (empty function)" ); + objectTrace( "SPMeshGradient::~SPMeshGradient", false ); +#endif +} + +void SPMeshGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::build" ); +#endif + + SPGradient::build(document, repr); + + // Start coordinate of meshgradient + this->readAttr( "x" ); + this->readAttr( "y" ); + + this->readAttr( "type" ); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::build", false ); +#endif +} + + +void SPMeshGradient::set(SPAttributeEnum key, gchar const *value) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::set" ); +#endif + + switch (key) { + case SP_ATTR_X: + if (!this->x.read(value)) { + this->x.unset(SVGLength::NONE, 0.0, 0.0); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + if (!this->y.read(value)) { + this->y.unset(SVGLength::NONE, 0.0, 0.0); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_TYPE: + if (value) { + if (!strcmp(value, "coons")) { + this->type = SP_MESH_TYPE_COONS; + } else if (!strcmp(value, "bicubic")) { + this->type = SP_MESH_TYPE_BICUBIC; + } else { + std::cerr << "SPMeshGradient::set(): invalid value " << value << std::endl; + } + this->type_set = TRUE; + } else { + // std::cout << "SPMeshGradient::set() No value " << std::endl; + this->type = SP_MESH_TYPE_COONS; + this->type_set = FALSE; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::set", false ); +#endif +} + +/** + * Write mesh gradient attributes to associated repr. + */ +Inkscape::XML::Node* SPMeshGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::write", false ); +#endif + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshgradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->x._set) { + sp_repr_set_svg_double(repr, "x", this->x.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->y._set) { + sp_repr_set_svg_double(repr, "y", this->y.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->type_set) { + switch (this->type) { + case SP_MESH_TYPE_COONS: + repr->setAttribute("type", "coons"); + break; + case SP_MESH_TYPE_BICUBIC: + repr->setAttribute("type", "bicubic"); + break; + default: + // Do nothing + break; + } + } + + SPGradient::write(xml_doc, repr, flags); + +#ifdef OBJECT_TRACE + objectTrace( "SPMeshGradient::write", false ); +#endif + return repr; +} + +cairo_pattern_t* SPMeshGradient::pattern_new(cairo_t * /*ct*/, + Geom::OptRect const &bbox, + double opacity) +{ + using Geom::X; + using Geom::Y; + +#ifdef MESH_DEBUG + std::cout << "sp_meshgradient_create_pattern: " << (*bbox) << " " << opacity << std::endl; +#endif + + this->ensureArray(); + + cairo_pattern_t *cp = nullptr; + + SPMeshNodeArray* my_array = &array; + + if( type_set ) { + switch (type) { + case SP_MESH_TYPE_COONS: + // std::cout << "SPMeshGradient::pattern_new: Coons" << std::endl; + break; + case SP_MESH_TYPE_BICUBIC: + array.bicubic( &array_smoothed, type ); + my_array = &array_smoothed; + break; + } + } + + cp = cairo_pattern_create_mesh(); + + for( unsigned int i = 0; i < my_array->patch_rows(); ++i ) { + for( unsigned int j = 0; j < my_array->patch_columns(); ++j ) { + + SPMeshPatchI patch( &(my_array->nodes), i, j ); + + cairo_mesh_pattern_begin_patch( cp ); + cairo_mesh_pattern_move_to( cp, patch.getPoint( 0, 0 )[X], patch.getPoint( 0, 0 )[Y] ); + + for( unsigned int k = 0; k < 4; ++k ) { +#ifdef DEBUG_MESH + std::cout << i << " " << j << " " + << patch.getPathType( k ) << " ("; + for( int p = 0; p < 4; ++p ) { + std::cout << patch.getPoint( k, p ); + } + std::cout << ") " + << patch.getColor( k ).toString() << std::endl; +#endif + + switch ( patch.getPathType( k ) ) { + case 'l': + case 'L': + case 'z': + case 'Z': + cairo_mesh_pattern_line_to( cp, + patch.getPoint( k, 3 )[X], + patch.getPoint( k, 3 )[Y] ); + break; + case 'c': + case 'C': + { + std::vector< Geom::Point > pts = patch.getPointsForSide( k ); + cairo_mesh_pattern_curve_to( cp, + pts[1][X], pts[1][Y], + pts[2][X], pts[2][Y], + pts[3][X], pts[3][Y] ); + break; + } + default: + // Shouldn't happen + std::cout << "sp_mesh_create_pattern: path error" << std::endl; + } + + if( patch.tensorIsSet(k) ) { + // Tensor point defined relative to corner. + Geom::Point t = patch.getTensorPoint(k); + cairo_mesh_pattern_set_control_point( cp, k, t[X], t[Y] ); + //std::cout << " sp_mesh_create_pattern: tensor " << k + // << " set to " << t << "." << std::endl; + } else { + // Geom::Point t = patch.coonsTensorPoint(k); + //std::cout << " sp_mesh_create_pattern: tensor " << k + // << " calculated as " << t << "." <gradientTransform; + if (this->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + ink_cairo_pattern_set_matrix(cp, gs2user.inverse()); + + /* + cairo_pattern_t *cp = cairo_pattern_create_radial( + rg->fx.computed, rg->fy.computed, 0, + rg->cx.computed, rg->cy.computed, rg->r.computed); + sp_gradient_pattern_common_setup(cp, gr, bbox, opacity); + */ + + return cp; +} diff --git a/src/object/sp-mesh-gradient.h b/src/object/sp-mesh-gradient.h new file mode 100644 index 0000000..48e3ce4 --- /dev/null +++ b/src/object/sp-mesh-gradient.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_MESH_GRADIENT_H +#define SP_MESH_GRADIENT_H + +/** \file + * SPMeshGradient: SVG implementation. + */ + +#include "svg/svg-length.h" +#include "sp-gradient.h" + +#define SP_MESHGRADIENT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MESHGRADIENT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** Mesh gradient. */ +class SPMeshGradient : public SPGradient { +public: + SPMeshGradient(); + ~SPMeshGradient() override; + + SVGLength x; // Upper left corner of meshgradient + SVGLength y; // Upper right corner of mesh + SPMeshType type; + bool type_set; + cairo_pattern_t* pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttributeEnum key, char const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_MESH_GRADIENT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-patch.cpp b/src/object/sp-mesh-patch.cpp new file mode 100644 index 0000000..88d3298 --- /dev/null +++ b/src/object/sp-mesh-patch.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @gradient meshpatch class. + */ +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Tavmjong Bah + * + * Copyright (C) 1999,2005 authors + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-mesh-patch.h" +#include "style.h" + +#include "attributes.h" + +SPMeshpatch* SPMeshpatch::getNextMeshpatch() +{ + SPMeshpatch *result = nullptr; + + for (SPObject* obj = getNext(); obj && !result; obj = obj->getNext()) { + if (SP_IS_MESHPATCH(obj)) { + result = SP_MESHPATCH(obj); + } + } + + return result; +} + +SPMeshpatch* SPMeshpatch::getPrevMeshpatch() +{ + SPMeshpatch *result = nullptr; + + for (SPObject* obj = getPrev(); obj; obj = obj->getPrev()) { + // The closest previous SPObject that is an SPMeshpatch *should* be ourself. + if (SP_IS_MESHPATCH(obj)) { + SPMeshpatch* meshpatch = SP_MESHPATCH(obj); + // Sanity check to ensure we have a proper sibling structure. + if (meshpatch->getNextMeshpatch() == this) { + result = meshpatch; + } else { + g_warning("SPMeshpatch previous/next relationship broken"); + } + break; + } + } + + return result; +} + + +/* + * Mesh Patch + */ +SPMeshpatch::SPMeshpatch() : SPObject() { + this->tensor_string = nullptr; +} + +SPMeshpatch::~SPMeshpatch() = default; + +void SPMeshpatch::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); + + this->readAttr( "tensor" ); +} + +/** + * Virtual build: set meshpatch attributes from its associated XML node. + */ +void SPMeshpatch::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_TENSOR: { + if (value) { + this->tensor_string = new Glib::ustring( value ); + // std::cout << "sp_meshpatch_set: Tensor string: " << patch->tensor_string->c_str() << std::endl; + } + break; + } + default: { + // Do nothing + } + } +} + +/** + * modified + */ +void SPMeshpatch::modified(unsigned int flags) { + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +/** + * Virtual set: set attribute to value. + */ +Inkscape::XML::Node* SPMeshpatch::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshpatch"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/** + * Virtual write: write object attributes to repr. + */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-patch.h b/src/object/sp-mesh-patch.h new file mode 100644 index 0000000..7ff780f --- /dev/null +++ b/src/object/sp-mesh-patch.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESHPATCH_H +#define SEEN_SP_MESHPATCH_H + +/** \file + * SPMeshpatch: SVG implementation. + */ +/* + * Authors: Tavmjong Bah + * + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "sp-object.h" + +#define SP_MESHPATCH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MESHPATCH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** Gradient Meshpatch. */ +class SPMeshpatch : public SPObject { +public: + SPMeshpatch(); + ~SPMeshpatch() override; + + SPMeshpatch* getNextMeshpatch(); + SPMeshpatch* getPrevMeshpatch(); + Glib::ustring * tensor_string; + //SVGLength tx[4]; // Tensor points + //SVGLength ty[4]; // Tensor points + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttributeEnum key, const char* value) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_MESHPATCH_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-row.cpp b/src/object/sp-mesh-row.cpp new file mode 100644 index 0000000..1456e76 --- /dev/null +++ b/src/object/sp-mesh-row.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @gradient meshrow class. + */ +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Jon A. Cruz + * Tavmjong Bah + * + * Copyright (C) 1999,2005 authors + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "sp-mesh-row.h" +#include "style.h" + +SPMeshrow* SPMeshrow::getNextMeshrow() +{ + SPMeshrow *result = nullptr; + + for (SPObject* obj = getNext(); obj && !result; obj = obj->getNext()) { + if (SP_IS_MESHROW(obj)) { + result = SP_MESHROW(obj); + } + } + + return result; +} + +SPMeshrow* SPMeshrow::getPrevMeshrow() +{ + SPMeshrow *result = nullptr; + + for (SPObject* obj = getPrev(); obj; obj = obj->getPrev()) { + // The closest previous SPObject that is an SPMeshrow *should* be ourself. + if (SP_IS_MESHROW(obj)) { + SPMeshrow* meshrow = SP_MESHROW(obj); + // Sanity check to ensure we have a proper sibling structure. + if (meshrow->getNextMeshrow() == this) { + result = meshrow; + } else { + g_warning("SPMeshrow previous/next relationship broken"); + } + break; + } + } + + return result; +} + + +/* + * Mesh Row + */ +SPMeshrow::SPMeshrow() : SPObject() { +} + +SPMeshrow::~SPMeshrow() = default; + +void SPMeshrow::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); +} + + +/** + * Virtual build: set meshrow attributes from its associated XML node. + */ +void SPMeshrow::set(SPAttributeEnum /*key*/, const gchar* /*value*/) { +} + +/** + * modified + */ +void SPMeshrow::modified(unsigned int flags) { + + flags &= SP_OBJECT_MODIFIED_CASCADE; + std::vector l; + for (auto& child: children) { + sp_object_ref(&child); + l.push_back(&child); + } + + for (auto child:l) { + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + sp_object_unref(child); + } +} + + +/** + * Virtual set: set attribute to value. + */ +Inkscape::XML::Node* SPMeshrow::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:meshrow"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/** + * Virtual write: write object attributes to repr. + */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-mesh-row.h b/src/object/sp-mesh-row.h new file mode 100644 index 0000000..89baa5e --- /dev/null +++ b/src/object/sp-mesh-row.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESHROW_H +#define SEEN_SP_MESHROW_H + +/** \file + * SPMeshrow: SVG implementation. + */ +/* + * Authors: Tavmjong Bah + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_MESHROW(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MESHROW(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** Gradient Meshrow. */ +class SPMeshrow : public SPObject { +public: + SPMeshrow(); + ~SPMeshrow() override; + + SPMeshrow* getNextMeshrow(); + SPMeshrow* getPrevMeshrow(); + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttributeEnum key, const char* value) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif /* !SEEN_SP_MESHROW_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-metadata.cpp b/src/object/sp-metadata.cpp new file mode 100644 index 0000000..5376f79 --- /dev/null +++ b/src/object/sp-metadata.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Kees Cook + * + * Copyright (C) 2004 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-metadata.h" +#include "xml/node-iterators.h" +#include "document.h" + +#include "sp-item-group.h" +#include "sp-root.h" + +#define noDEBUG_METADATA +#ifdef DEBUG_METADATA +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /**/ +#endif + +/* Metadata base class */ + +SPMetadata::SPMetadata() : SPObject() { +} + +SPMetadata::~SPMetadata() = default; + +namespace { + +void strip_ids_recursively(Inkscape::XML::Node *node) { + using Inkscape::XML::NodeSiblingIterator; + if ( node->type() == Inkscape::XML::ELEMENT_NODE ) { + node->removeAttribute("id"); + } + for ( NodeSiblingIterator iter=node->firstChild() ; iter ; ++iter ) { + strip_ids_recursively(iter); + } +} + +} + + +void SPMetadata::build(SPDocument* doc, Inkscape::XML::Node* repr) { + using Inkscape::XML::NodeSiblingIterator; + + debug("0x%08x",(unsigned int)this); + + /* clean up our mess from earlier versions; elements under rdf:RDF should not + * have id= attributes... */ + static GQuark const rdf_root_name = g_quark_from_static_string("rdf:RDF"); + + for ( NodeSiblingIterator iter=repr->firstChild() ; iter ; ++iter ) { + if ( (GQuark)iter->code() == rdf_root_name ) { + strip_ids_recursively(iter); + } + } + + SPObject::build(doc, repr); +} + +void SPMetadata::release() { + debug("0x%08x",(unsigned int)this); + + // handle ourself + + SPObject::release(); +} + +void SPMetadata::set(SPAttributeEnum key, const gchar* value) { + debug("0x%08x %s(%u): '%s'",(unsigned int)this, + sp_attribute_name(key),key,value); + + // see if any parents need this value + SPObject::set(key, value); +} + +void SPMetadata::update(SPCtx* /*ctx*/, unsigned int flags) { + debug("0x%08x",(unsigned int)this); + //SPMetadata *metadata = SP_METADATA(object); + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something? */ + + } + +// SPObject::onUpdate(ctx, flags); +} + +Inkscape::XML::Node* SPMetadata::write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) { + debug("0x%08x",(unsigned int)this); + + if ( repr != this->getRepr() ) { + if (repr) { + repr->mergeFrom(this->getRepr(), "id"); + } else { + repr = this->getRepr()->duplicate(doc); + } + } + + SPObject::write(doc, repr, flags); + + return repr; +} + +/** + * Retrieves the metadata object associated with a document. + */ +SPMetadata *sp_document_metadata(SPDocument *document) +{ + SPObject *nv; + + g_return_val_if_fail (document != nullptr, NULL); + + nv = sp_item_group_get_child_by_name( document->getRoot(), nullptr, + "metadata"); + g_assert (nv != nullptr); + + return (SPMetadata *)nv; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-metadata.h b/src/object/sp-metadata.h new file mode 100644 index 0000000..b0b2adc --- /dev/null +++ b/src/object/sp-metadata.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_METADATA_H +#define SEEN_SP_METADATA_H + +/* + * SVG implementation + * + * Authors: + * Kees Cook + * + * Copyright (C) 2004 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +/* Metadata base class */ + +#define SP_METADATA(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_METADATA(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPMetadata : public SPObject { +public: + SPMetadata(); + ~SPMetadata() override; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttributeEnum key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +SPMetadata * sp_document_metadata (SPDocument *document); + +#endif +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-missing-glyph.cpp b/src/object/sp-missing-glyph.cpp new file mode 100644 index 0000000..a69f70f --- /dev/null +++ b/src/object/sp-missing-glyph.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG element implementation + * + * Author: + * Felipe C. da S. Sanches + * Abhishek Sharma + * + * Copyright (C) 2008, Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/repr.h" +#include "attributes.h" +#include "sp-missing-glyph.h" +#include "document.h" + +SPMissingGlyph::SPMissingGlyph() : SPObject() { +//TODO: correct these values: + this->d = nullptr; + this->horiz_adv_x = 0; + this->vert_origin_x = 0; + this->vert_origin_y = 0; + this->vert_adv_y = 0; +} + +SPMissingGlyph::~SPMissingGlyph() = default; + +void SPMissingGlyph::build(SPDocument* doc, Inkscape::XML::Node* repr) { + SPObject::build(doc, repr); + + this->readAttr( "d" ); + this->readAttr( "horiz-adv-x" ); + this->readAttr( "vert-origin-x" ); + this->readAttr( "vert-origin-y" ); + this->readAttr( "vert-adv-y" ); +} + +void SPMissingGlyph::release() { + SPObject::release(); +} + + +void SPMissingGlyph::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_D: + { + if (this->d) { + g_free(this->d); + } + this->d = g_strdup(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_HORIZ_ADV_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->horiz_adv_x){ + this->horiz_adv_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_X: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_origin_x){ + this->vert_origin_x = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ORIGIN_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_origin_y){ + this->vert_origin_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + case SP_ATTR_VERT_ADV_Y: + { + double number = value ? g_ascii_strtod(value, nullptr) : 0; + if (number != this->vert_adv_y){ + this->vert_adv_y = number; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + break; + } + default: + { + SPObject::set(key, value); + break; + } + } +} + +#define COPY_ATTR(rd,rs,key) (rd)->setAttribute((key), rs->attribute(key)); + +Inkscape::XML::Node* SPMissingGlyph::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:glyph"); + } + + /* I am commenting out this part because I am not certain how does it work. I will have to study it later. Juca + repr->setAttribute("d", glyph->d); + sp_repr_set_svg_double(repr, "horiz-adv-x", glyph->horiz_adv_x); + sp_repr_set_svg_double(repr, "vert-origin-x", glyph->vert_origin_x); + sp_repr_set_svg_double(repr, "vert-origin-y", glyph->vert_origin_y); + sp_repr_set_svg_double(repr, "vert-adv-y", glyph->vert_adv_y); + */ + if (repr != this->getRepr()) { + + // TODO + // All the COPY_ATTR functions below use + // XML Tree directly while they shouldn't. + COPY_ATTR(repr, this->getRepr(), "d"); + COPY_ATTR(repr, this->getRepr(), "horiz-adv-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-x"); + COPY_ATTR(repr, this->getRepr(), "vert-origin-y"); + COPY_ATTR(repr, this->getRepr(), "vert-adv-y"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-missing-glyph.h b/src/object/sp-missing-glyph.h new file mode 100644 index 0000000..7f80ffc --- /dev/null +++ b/src/object/sp-missing-glyph.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MISSING_GLYPH_H +#define SEEN_SP_MISSING_GLYPH_H + +/* + * SVG element implementation + * + * Authors: + * Felipe C. da S. Sanches + * + * Copyright (C) 2008 Felipe C. da S. Sanches + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_MISSING_GLYPH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_MISSING_GLYPH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPMissingGlyph : public SPObject { +public: + SPMissingGlyph(); + ~SPMissingGlyph() override; + + char* d; + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + +private: + double horiz_adv_x; + double vert_origin_x; + double vert_origin_y; + double vert_adv_y; +}; + +#endif //#ifndef __SP_MISSING_GLYPH_H__ diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp new file mode 100644 index 0000000..cbc9311 --- /dev/null +++ b/src/object/sp-namedview.cpp @@ -0,0 +1,1227 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * implementation + * + * Authors: + * Lauris Kaplinski + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) 1999-2013 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include "event-log.h" +#include <2geom/transforms.h> + +#include "display/canvas-grid.h" +#include "util/units.h" +#include "svg/svg-color.h" +#include "xml/repr.h" +#include "attributes.h" +#include "document.h" +#include "document-undo.h" +#include "desktop-events.h" +#include "enums.h" +#include "ui/monitor.h" + +#include "sp-guide.h" +#include "sp-item-group.h" +#include "sp-namedview.h" +#include "preferences.h" +#include "desktop.h" +#include "conn-avoid-ref.h" // for defaultConnSpacing. +#include "sp-root.h" +#include + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +#define DEFAULTGRIDCOLOR 0x3f3fff25 +#define DEFAULTGRIDEMPCOLOR 0x3f3fff60 +#define DEFAULTGRIDEMPSPACING 5 +#define DEFAULTGUIDECOLOR 0x0000ff7f +#define DEFAULTGUIDEHICOLOR 0xff00007f +#define DEFAULTBORDERCOLOR 0x000000ff +#define DEFAULTPAGECOLOR 0xffffff00 + +static void sp_namedview_setup_guides(SPNamedView * nv); +static void sp_namedview_lock_guides(SPNamedView * nv); +static void sp_namedview_show_single_guide(SPGuide* guide, bool show); +static void sp_namedview_lock_single_guide(SPGuide* guide, bool show); + +static gboolean sp_str_to_bool(const gchar *str); +static gboolean sp_nv_read_opacity(const gchar *str, guint32 *color); + +SPNamedView::SPNamedView() : SPObjectGroup(), snap_manager(this) { + + this->zoom = 0; + this->guidecolor = 0; + this->guidehicolor = 0; + this->views.clear(); + this->borderlayer = 0; + this->page_size_units = nullptr; + this->window_x = 0; + this->cy = 0; + this->window_y = 0; + this->display_units = nullptr; + this->page_size_units = nullptr; + this->pagecolor = 0; + this->cx = 0; + this->pageshadow = 0; + this->window_width = 0; + this->window_height = 0; + this->window_maximized = 0; + this->bordercolor = 0; + + this->editable = TRUE; + this->showguides = TRUE; + this->lockguides = false; + this->grids_visible = false; + this->showborder = TRUE; + this->pagecheckerboard = FALSE; + this->showpageshadow = TRUE; + + this->guides.clear(); + this->viewcount = 0; + this->grids.clear(); + + this->default_layer_id = 0; + + this->connector_spacing = defaultConnSpacing; +} + +SPNamedView::~SPNamedView() = default; + +static void sp_namedview_generate_old_grid(SPNamedView * /*nv*/, SPDocument *document, Inkscape::XML::Node *repr) { + bool old_grid_settings_present = false; + + // set old settings + const char* gridspacingx = "1px"; + const char* gridspacingy = "1px"; + const char* gridoriginy = "0px"; + const char* gridoriginx = "0px"; + const char* gridempspacing = "5"; + const char* gridcolor = "#3f3fff"; + const char* gridempcolor = "#3f3fff"; + const char* gridopacity = "0.15"; + const char* gridempopacity = "0.38"; + + const char* value = nullptr; + if ((value = repr->attribute("gridoriginx"))) { + gridoriginx = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridoriginy"))) { + gridoriginy = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridspacingx"))) { + gridspacingx = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridspacingy"))) { + gridspacingy = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridcolor"))) { + gridcolor = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridempcolor"))) { + gridempcolor = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridempspacing"))) { + gridempspacing = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridopacity"))) { + gridopacity = value; + old_grid_settings_present = true; + } + if ((value = repr->attribute("gridempopacity"))) { + gridempopacity = value; + old_grid_settings_present = true; + } + + if (old_grid_settings_present) { + // generate new xy grid with the correct settings + // first create the child xml node, then hook it to repr. This order is important, to not set off listeners to repr before the new node is complete. + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *newnode = xml_doc->createElement("inkscape:grid"); + newnode->setAttribute("id", "GridFromPre046Settings"); + newnode->setAttribute("type", Inkscape::CanvasGrid::getSVGName(Inkscape::GRID_RECTANGULAR)); + newnode->setAttribute("originx", gridoriginx); + newnode->setAttribute("originy", gridoriginy); + newnode->setAttribute("spacingx", gridspacingx); + newnode->setAttribute("spacingy", gridspacingy); + newnode->setAttribute("color", gridcolor); + newnode->setAttribute("empcolor", gridempcolor); + newnode->setAttribute("opacity", gridopacity); + newnode->setAttribute("empopacity", gridempopacity); + newnode->setAttribute("empspacing", gridempspacing); + + repr->appendChild(newnode); + Inkscape::GC::release(newnode); + + // remove all old settings + repr->removeAttribute("gridoriginx"); + repr->removeAttribute("gridoriginy"); + repr->removeAttribute("gridspacingx"); + repr->removeAttribute("gridspacingy"); + repr->removeAttribute("gridcolor"); + repr->removeAttribute("gridempcolor"); + repr->removeAttribute("gridopacity"); + repr->removeAttribute("gridempopacity"); + repr->removeAttribute("gridempspacing"); + +// SPDocumentUndo::done(doc, SP_VERB_DIALOG_NAMEDVIEW, _("Create new grid from pre0.46 grid settings")); + } +} + +void SPNamedView::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPObjectGroup::build(document, repr); + + this->readAttr( "inkscape:document-units" ); + this->readAttr( "units" ); + this->readAttr( "viewonly" ); + this->readAttr( "showguides" ); + this->readAttr( "showgrid" ); + this->readAttr( "gridtolerance" ); + this->readAttr( "guidetolerance" ); + this->readAttr( "objecttolerance" ); + this->readAttr( "guidecolor" ); + this->readAttr( "guideopacity" ); + this->readAttr( "guidehicolor" ); + this->readAttr( "guidehiopacity" ); + this->readAttr( "showborder" ); + this->readAttr( "inkscape:showpageshadow" ); + this->readAttr( "borderlayer" ); + this->readAttr( "bordercolor" ); + this->readAttr( "borderopacity" ); + this->readAttr( "pagecolor" ); + this->readAttr( "inkscape:pagecheckerboard" ); + this->readAttr( "inkscape:pageopacity" ); + this->readAttr( "inkscape:pageshadow" ); + this->readAttr( "inkscape:zoom" ); + this->readAttr( "inkscape:cx" ); + this->readAttr( "inkscape:cy" ); + this->readAttr( "inkscape:window-width" ); + this->readAttr( "inkscape:window-height" ); + this->readAttr( "inkscape:window-x" ); + this->readAttr( "inkscape:window-y" ); + this->readAttr( "inkscape:window-maximized" ); + this->readAttr( "inkscape:snap-global" ); + this->readAttr( "inkscape:snap-bbox" ); + this->readAttr( "inkscape:snap-nodes" ); + this->readAttr( "inkscape:snap-others" ); + this->readAttr( "inkscape:snap-from-guide" ); + this->readAttr( "inkscape:snap-center" ); + this->readAttr( "inkscape:snap-smooth-nodes" ); + this->readAttr( "inkscape:snap-midpoints" ); + this->readAttr( "inkscape:snap-object-midpoints" ); + this->readAttr( "inkscape:snap-text-baseline" ); + this->readAttr( "inkscape:snap-bbox-edge-midpoints" ); + this->readAttr( "inkscape:snap-bbox-midpoints" ); + this->readAttr( "inkscape:snap-to-guides" ); + this->readAttr( "inkscape:snap-grids" ); + this->readAttr( "inkscape:snap-intersection-paths" ); + this->readAttr( "inkscape:object-paths" ); + this->readAttr( "inkscape:snap-perpendicular" ); + this->readAttr( "inkscape:snap-tangential" ); + this->readAttr( "inkscape:snap-path-clip" ); + this->readAttr( "inkscape:snap-path-mask" ); + this->readAttr( "inkscape:object-nodes" ); + this->readAttr( "inkscape:bbox-paths" ); + this->readAttr( "inkscape:bbox-nodes" ); + this->readAttr( "inkscape:snap-page" ); + this->readAttr( "inkscape:current-layer" ); + this->readAttr( "inkscape:connector-spacing" ); + this->readAttr( "inkscape:lockguides" ); + + /* Construct guideline list */ + for (auto& o: children) { + if (SP_IS_GUIDE(&o)) { + SPGuide * g = SP_GUIDE(&o); + this->guides.push_back(g); + //g_object_set(G_OBJECT(g), "color", nv->guidecolor, "hicolor", nv->guidehicolor, NULL); + g->setColor(this->guidecolor); + g->setHiColor(this->guidehicolor); + g->readAttr( "inkscape:color" ); + } + } + + // backwards compatibility with grid settings (pre 0.46) + sp_namedview_generate_old_grid(this, document, repr); +} + +void SPNamedView::release() { + this->guides.clear(); + + // delete grids: + for(auto grid : this->grids) + delete grid; + this->grids.clear(); + SPObjectGroup::release(); +} + +void SPNamedView::set(SPAttributeEnum key, const gchar* value) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool global_snapping = prefs->getBool("/options/snapdefault/value", true); + switch (key) { + case SP_ATTR_VIEWONLY: + this->editable = (!value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SHOWGUIDES: + if (!value) { // show guides if not specified, for backwards compatibility + this->showguides = TRUE; + } else { + this->showguides = sp_str_to_bool(value); + } + sp_namedview_setup_guides(this); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SHOWGRIDS: + if (!value) { // don't show grids if not specified, for backwards compatibility + this->grids_visible = false; + } else { + this->grids_visible = sp_str_to_bool(value); + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GRIDTOLERANCE: + this->snap_manager.snapprefs.setGridTolerance(value ? g_ascii_strtod(value, nullptr) : 10000); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GUIDETOLERANCE: + this->snap_manager.snapprefs.setGuideTolerance(value ? g_ascii_strtod(value, nullptr) : 20); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_OBJECTTOLERANCE: + this->snap_manager.snapprefs.setObjectTolerance(value ? g_ascii_strtod(value, nullptr) : 20); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GUIDECOLOR: + this->guidecolor = (this->guidecolor & 0xff) | (DEFAULTGUIDECOLOR & 0xffffff00); + + if (value) { + this->guidecolor = (this->guidecolor & 0xff) | sp_svg_read_color(value, this->guidecolor); + } + + for(auto guide : this->guides) { + guide->setColor(this->guidecolor); + guide->readAttr("inkscape:color"); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GUIDEOPACITY: + this->guidecolor = (this->guidecolor & 0xffffff00) | (DEFAULTGUIDECOLOR & 0xff); + sp_nv_read_opacity(value, &this->guidecolor); + + for(auto guide : this->guides) { + guide->setColor(this->guidecolor); + guide->readAttr("inkscape:color"); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GUIDEHICOLOR: + this->guidehicolor = (this->guidehicolor & 0xff) | (DEFAULTGUIDEHICOLOR & 0xffffff00); + + if (value) { + this->guidehicolor = (this->guidehicolor & 0xff) | sp_svg_read_color(value, this->guidehicolor); + } + for(auto guide : this->guides) { + guide->setHiColor(this->guidehicolor); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_GUIDEHIOPACITY: + this->guidehicolor = (this->guidehicolor & 0xffffff00) | (DEFAULTGUIDEHICOLOR & 0xff); + sp_nv_read_opacity(value, &this->guidehicolor); + for(auto guide : this->guides) { + guide->setHiColor(this->guidehicolor); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SHOWBORDER: + this->showborder = (value) ? sp_str_to_bool (value) : TRUE; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_BORDERLAYER: + this->borderlayer = SP_BORDER_LAYER_BOTTOM; + if (value && !strcasecmp(value, "true")) this->borderlayer = SP_BORDER_LAYER_TOP; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_BORDERCOLOR: + this->bordercolor = (this->bordercolor & 0xff) | (DEFAULTBORDERCOLOR & 0xffffff00); + if (value) { + this->bordercolor = (this->bordercolor & 0xff) | sp_svg_read_color (value, this->bordercolor); + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_BORDEROPACITY: + this->bordercolor = (this->bordercolor & 0xffffff00) | (DEFAULTBORDERCOLOR & 0xff); + sp_nv_read_opacity(value, &this->bordercolor); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_PAGECOLOR: + this->pagecolor = (this->pagecolor & 0xff) | (DEFAULTPAGECOLOR & 0xffffff00); + if (value) { + this->pagecolor = (this->pagecolor & 0xff) | sp_svg_read_color(value, this->pagecolor); + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_PAGECHECKERBOARD: + this->pagecheckerboard = (value) ? sp_str_to_bool (value) : false; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_PAGEOPACITY: + this->pagecolor = (this->pagecolor & 0xffffff00) | (DEFAULTPAGECOLOR & 0xff); + sp_nv_read_opacity(value, &this->pagecolor); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_PAGESHADOW: + this->pageshadow = value? atoi(value) : 2; // 2 is the default + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_SHOWPAGESHADOW: + this->showpageshadow = (value) ? sp_str_to_bool(value) : TRUE; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_ZOOM: + this->zoom = value ? g_ascii_strtod(value, nullptr) : 0; // zero means not set + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_CX: + this->cx = value ? g_ascii_strtod(value, nullptr) : HUGE_VAL; // HUGE_VAL means not set + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_CY: + this->cy = value ? g_ascii_strtod(value, nullptr) : HUGE_VAL; // HUGE_VAL means not set + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_WINDOW_WIDTH: + this->window_width = value? atoi(value) : -1; // -1 means not set + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_WINDOW_HEIGHT: + this->window_height = value ? atoi(value) : -1; // -1 means not set + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_WINDOW_X: + this->window_x = value ? atoi(value) : 0; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_WINDOW_Y: + this->window_y = value ? atoi(value) : 0; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_WINDOW_MAXIMIZED: + this->window_maximized = value ? atoi(value) : 0; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_GLOBAL: + this->snap_manager.snapprefs.setSnapEnabledGlobally(value ? sp_str_to_bool(value) : global_snapping); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_NODE_CATEGORY, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_OTHERS: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_OTHERS_CATEGORY, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_ROTATION_CENTER: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_GRID: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_GRID, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_GUIDE: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_GUIDE, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE_SMOOTH: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_NODE_SMOOTH, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_LINE_MIDPOINT: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_LINE_MIDPOINT, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_OBJECT_MIDPOINT: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_TEXT_BASELINE: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE_MIDPOINT: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_BBOX_EDGE_MIDPOINT, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_MIDPOINT: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_BBOX_MIDPOINT, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_INTERSECTION: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_INTERSECTION, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PERP: + this->snap_manager.snapprefs.setSnapPerp(value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_TANG: + this->snap_manager.snapprefs.setSnapTang(value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_CLIP: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_CLIP, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_MASK: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PATH_MASK, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE_CUSP: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_NODE_CUSP, value ? sp_str_to_bool(value) : TRUE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_BBOX_EDGE, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_CORNER: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_BBOX_CORNER, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_SNAP_PAGE_BORDER: + this->snap_manager.snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_PAGE_BORDER, value ? sp_str_to_bool(value) : FALSE); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_CURRENT_LAYER: + this->default_layer_id = value ? g_quark_from_string(value) : 0; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_CONNECTOR_SPACING: + this->connector_spacing = value ? g_ascii_strtod(value, nullptr) : + defaultConnSpacing; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + case SP_ATTR_INKSCAPE_DOCUMENT_UNITS: { + /* The default display unit if the document doesn't override this: e.g. for files saved as + * `plain SVG', or non-inkscape files, or files created by an inkscape 0.40 & + * earlier. + * + * Note that these units are not the same as the units used for the values in SVG! + * + * We default to `px'. + */ + static Inkscape::Util::Unit const *px = unit_table.getUnit("px"); + Inkscape::Util::Unit const *new_unit = px; + + if (value && document->getRoot()->viewBox_set) { + Inkscape::Util::Unit const *const req_unit = unit_table.getUnit(value); + if ( !unit_table.hasUnit(value) ) { + g_warning("Unrecognized unit `%s'", value); + /* fixme: Document errors should be reported in the status bar or + * the like (e.g. as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing); g_log + * should be only for programmer errors. */ + } else if ( req_unit->isAbsolute() ) { + new_unit = req_unit; + } else { + g_warning("Document units must be absolute like `mm', `pt' or `px', but found `%s'", + value); + /* fixme: Don't use g_log (see above). */ + } + } + this->display_units = new_unit; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_UNITS: { + // Only used in "Custom size" section of Document Properties dialog + Inkscape::Util::Unit const *new_unit = nullptr; + + if (value) { + Inkscape::Util::Unit const *const req_unit = unit_table.getUnit(value); + if ( !unit_table.hasUnit(value) ) { + g_warning("Unrecognized unit `%s'", value); + /* fixme: Document errors should be reported in the status bar or + * the like (e.g. as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing); g_log + * should be only for programmer errors. */ + } else if ( req_unit->isAbsolute() ) { + new_unit = req_unit; + } else { + g_warning("Document units must be absolute like `mm', `pt' or `px', but found `%s'", + value); + /* fixme: Don't use g_log (see above). */ + } + } + this->page_size_units = new_unit; + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_INKSCAPE_LOCKGUIDES: + this->lockguides = value ? sp_str_to_bool(value) : FALSE; + this->lockGuides(); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPObjectGroup::set(key, value); + break; + } +} + +/** +* add a grid item from SVG-repr. Check if this namedview already has a gridobject for this one! If desktop=null, add grid-canvasitem to all desktops of this namedview, +* otherwise only add it to the specified desktop. +*/ +static Inkscape::CanvasGrid* +sp_namedview_add_grid(SPNamedView *nv, Inkscape::XML::Node *repr, SPDesktop *desktop) { + Inkscape::CanvasGrid* grid = nullptr; + //check if namedview already has an object for this grid + for(auto it : nv->grids) { + if (repr == it->repr) { + grid = it; + break; + } + } + + if (!grid) { + //create grid object + Inkscape::GridType gridtype = Inkscape::CanvasGrid::getGridTypeFromSVGName(repr->attribute("type")); + if (!nv->document) { + g_warning("sp_namedview_add_grid - how come doc is null here?!"); + return nullptr; + } + grid = Inkscape::CanvasGrid::NewGrid(nv, repr, nv->document, gridtype); + nv->grids.push_back(grid); + } + + if (!desktop) { + //add canvasitem to all desktops + for(auto view : nv->views) { + grid->createCanvasItem(view); + } + } else { + //add canvasitem only for specified desktop + grid->createCanvasItem(desktop); + } + + return grid; +} + +void SPNamedView::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObjectGroup::child_added(child, ref); + + if (!strcmp(child->name(), "inkscape:grid")) { + sp_namedview_add_grid(this, child, nullptr); + } else { + SPObject *no = this->document->getObjectByRepr(child); + if ( !SP_IS_OBJECT(no) ) { + return; + } + + if (SP_IS_GUIDE(no)) { + SPGuide *g = (SPGuide *) no; + this->guides.push_back(g); + + //g_object_set(G_OBJECT(g), "color", this->guidecolor, "hicolor", this->guidehicolor, NULL); + g->setColor(this->guidecolor); + g->setHiColor(this->guidehicolor); + g->readAttr("inkscape:color"); + + if (this->editable) { + for(auto view : this->views) { + g->SPGuide::showSPGuide(view->guides, (GCallback) sp_dt_guide_event); + + if (view->guides_active) { + g->sensitize(view->getCanvas(), TRUE); + } + + sp_namedview_show_single_guide(SP_GUIDE(g), this->showguides); + } + } + } + } +} + +void SPNamedView::remove_child(Inkscape::XML::Node *child) { + if (!strcmp(child->name(), "inkscape:grid")) { + for(std::vector::iterator it=this->grids.begin();it!=this->grids.end();++it ) { + if ( (*it)->repr == child ) { + delete (*it); + this->grids.erase(it); + break; + } + } + } else { + for(std::vector::iterator it=this->guides.begin();it!=this->guides.end();++it ) { + if ( (*it)->getRepr() == child ) { + this->guides.erase(it); + break; + } + } + } + + SPObjectGroup::remove_child(child); +} + +Inkscape::XML::Node* SPNamedView::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ( ( flags & SP_OBJECT_WRITE_EXT ) && + repr != this->getRepr() ) + { + if (repr) { + repr->mergeFrom(this->getRepr(), "id"); + } else { + repr = this->getRepr()->duplicate(xml_doc); + } + } + + return repr; +} + +void SPNamedView::show(SPDesktop *desktop) +{ + for(auto guide : this->guides) { + guide->showSPGuide( desktop->guides, (GCallback) sp_dt_guide_event); + if (desktop->guides_active) { + guide->sensitize(desktop->getCanvas(), TRUE); + } + sp_namedview_show_single_guide(guide, showguides); + } + + views.push_back(desktop); + + // generate grids specified in SVG: + Inkscape::XML::Node *repr = this->getRepr(); + if (repr) { + for (Inkscape::XML::Node * child = repr->firstChild() ; child != nullptr; child = child->next() ) { + if (!strcmp(child->name(), "inkscape:grid")) { + sp_namedview_add_grid(this, child, desktop); + } + } + } + + desktop->showGrids(grids_visible, false); +} + +/* + * Restores window geometry from the document settings or defaults in prefs + */ +void sp_namedview_window_from_document(SPDesktop *desktop) +{ + SPNamedView *nv = desktop->namedview; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int window_geometry = prefs->getInt("/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE); + int default_size = prefs->getInt("/options/defaultwindowsize/value", PREFS_WINDOW_SIZE_NATURAL); + bool new_document = (nv->window_width <= 0) || (nv->window_height <= 0); + bool show_dialogs = true; + + // restore window size and position stored with the document + Gtk::Window *win = desktop->getToplevel(); + g_assert(win); + + if (window_geometry == PREFS_WINDOW_GEOMETRY_LAST) { + gint pw = prefs->getInt("/desktop/geometry/width", -1); + gint ph = prefs->getInt("/desktop/geometry/height", -1); + gint px = prefs->getInt("/desktop/geometry/x", -1); + gint py = prefs->getInt("/desktop/geometry/y", -1); + gint full = prefs->getBool("/desktop/geometry/fullscreen"); + gint maxed = prefs->getBool("/desktop/geometry/maximized"); + if (pw>0 && ph>0) { + + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_point(px, py); + pw = std::min(pw, monitor_geometry.get_width()); + ph = std::min(ph, monitor_geometry.get_height()); + desktop->setWindowSize(pw, ph); + desktop->setWindowPosition(Geom::Point(px, py)); + } + if (maxed) { + win->maximize(); + } + if (full) { + win->fullscreen(); + } + } else if ((window_geometry == PREFS_WINDOW_GEOMETRY_FILE && nv->window_maximized) || + (new_document && (default_size == PREFS_WINDOW_SIZE_MAXIMIZED))) { + win->maximize(); + } else { + const int MIN_WINDOW_SIZE = 600; + int w = 0; + int h = 0; + bool move_to_screen = false; + if (window_geometry == PREFS_WINDOW_GEOMETRY_FILE && !new_document) { + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_point(nv->window_x, nv->window_y); + w = MIN(monitor_geometry.get_width(), nv->window_width); + h = MIN(monitor_geometry.get_height(), nv->window_height); + move_to_screen = true; + } else if (default_size == PREFS_WINDOW_SIZE_LARGE) { + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_window(win->get_window()); + w = MAX(0.75 * monitor_geometry.get_width(), MIN_WINDOW_SIZE); + h = MAX(0.75 * monitor_geometry.get_height(), MIN_WINDOW_SIZE); + } else if (default_size == PREFS_WINDOW_SIZE_SMALL) { + w = h = MIN_WINDOW_SIZE; + } else if (default_size == PREFS_WINDOW_SIZE_NATURAL) { + // don't set size (i.e. keep the gtk+ default, which will be the natural size) + // unless gtk+ decided it would be a good idea to show a window that is larger than the screen + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_at_window(win->get_window()); + int monitor_width = monitor_geometry.get_width(); + int monitor_height = monitor_geometry.get_height(); + int window_width, window_height; + win->get_size(window_width, window_height); + if (window_width > monitor_width || window_height > monitor_height) { + w = std::min(monitor_width, window_width); + h = std::min(monitor_height, window_height); + } + } + if ((w > 0) && (h > 0)) { +#ifndef _WIN32 + gint dx= 0; + gint dy = 0; + gint dw = 0; + gint dh = 0; + desktop->getWindowGeometry(dx, dy, dw, dh); + if ((w != dw) || (h != dh)) { + // Don't show dialogs when window is initially resized on OSX/Linux due to gdl dock bug + // This will happen on sp_desktop_widget_size_allocate + show_dialogs = FALSE; + } +#endif + desktop->setWindowSize(w, h); + if (move_to_screen) { + desktop->setWindowPosition(Geom::Point(nv->window_x, nv->window_y)); + } + } + } + + // Cancel any history of transforms up to this point (must be before call to zoom). + desktop->clear_transform_history(); + + if (show_dialogs) { + desktop->show_dialogs(); + } +} + +/* + * Restores zoom and view from the document settings + */ +void sp_namedview_zoom_and_view_from_document(SPDesktop *desktop) +{ + SPNamedView *nv = desktop->namedview; + if (nv->zoom != 0 && nv->zoom != HUGE_VAL && !std::isnan(nv->zoom) + && nv->cx != HUGE_VAL && !std::isnan(nv->cx) + && nv->cy != HUGE_VAL && !std::isnan(nv->cy)) { + desktop->zoom_absolute_center_point( Geom::Point(nv->cx, nv->cy), nv->zoom ); + } else if (desktop->getDocument()) { // document without saved zoom, zoom to its page + desktop->zoom_page(); + } +} + +void SPNamedView::writeNewGrid(SPDocument *document,int gridtype) +{ + g_assert(this->getRepr() != nullptr); + Inkscape::CanvasGrid::writeNewGridToRepr(this->getRepr(),document,static_cast(gridtype)); +} + +bool SPNamedView::getSnapGlobal() const +{ + return this->snap_manager.snapprefs.getSnapEnabledGlobally(); +} + +void SPNamedView::setSnapGlobal(bool v) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_set_boolean(this->getRepr(), "inkscape:snap-global", v); +} + +void sp_namedview_update_layers_from_document (SPDesktop *desktop) +{ + SPObject *layer = nullptr; + SPDocument *document = desktop->doc(); + SPNamedView *nv = desktop->namedview; + if ( nv->default_layer_id != 0 ) { + layer = document->getObjectById(g_quark_to_string(nv->default_layer_id)); + } + // don't use that object if it's not at least group + if ( !layer || !SP_IS_GROUP(layer) ) { + layer = nullptr; + } + // if that didn't work out, look for the topmost layer + if (!layer) { + for (auto& iter: document->getRoot()->children) { + if (desktop->isLayer(&iter)) { + layer = &iter; + } + } + } + if (layer) { + desktop->setCurrentLayer(layer); + } + + // FIXME: find a better place to do this + desktop->event_log->updateUndoVerbs(); +} + +void sp_namedview_document_from_window(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int window_geometry = prefs->getInt("/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE); + bool save_geometry_in_file = window_geometry == PREFS_WINDOW_GEOMETRY_FILE; + bool save_viewport_in_file = prefs->getBool("/options/savedocviewport/value", true); + Inkscape::XML::Node *view = desktop->namedview->getRepr(); + Geom::Rect const r = desktop->get_display_area(); + + // saving window geometry is not undoable + bool saved = DocumentUndo::getUndoSensitive(desktop->getDocument()); + DocumentUndo::setUndoSensitive(desktop->getDocument(), false); + + if (save_viewport_in_file) { + sp_repr_set_svg_double(view, "inkscape:zoom", desktop->current_zoom()); + sp_repr_set_svg_double(view, "inkscape:cx", r.midpoint()[Geom::X]); + sp_repr_set_svg_double(view, "inkscape:cy", r.midpoint()[Geom::Y]); + } + + if (save_geometry_in_file) { + gint w, h, x, y; + desktop->getWindowGeometry(x, y, w, h); + sp_repr_set_int(view, "inkscape:window-width", w); + sp_repr_set_int(view, "inkscape:window-height", h); + sp_repr_set_int(view, "inkscape:window-x", x); + sp_repr_set_int(view, "inkscape:window-y", y); + sp_repr_set_int(view, "inkscape:window-maximized", desktop->is_maximized()); + } + + view->setAttribute("inkscape:current-layer", desktop->currentLayer()->getId()); + + // restore undoability + DocumentUndo::setUndoSensitive(desktop->getDocument(), saved); +} + +void SPNamedView::hide(SPDesktop const *desktop) +{ + g_assert(desktop != nullptr); + g_assert(std::find(views.begin(),views.end(),desktop)!=views.end()); + for(auto & guide : this->guides) { + guide->hideSPGuide(desktop->getCanvas()); + } + views.erase(std::remove(views.begin(),views.end(),desktop),views.end()); +} + +void SPNamedView::activateGuides(void* desktop, bool active) +{ + g_assert(desktop != nullptr); + g_assert(std::find(views.begin(),views.end(),desktop)!=views.end()); + + SPDesktop *dt = static_cast(desktop); + for(auto & guide : this->guides) { + guide->sensitize(dt->getCanvas(), active); + } +} + +static void sp_namedview_setup_guides(SPNamedView *nv) +{ + for(std::vector::iterator it=nv->guides.begin();it!=nv->guides.end();++it ) { + sp_namedview_show_single_guide(*it, nv->showguides); + } +} + +static void sp_namedview_lock_guides(SPNamedView *nv) +{ + for(std::vector::iterator it=nv->guides.begin();it!=nv->guides.end();++it ) { + sp_namedview_lock_single_guide(*it, nv->lockguides); + } +} + +static void sp_namedview_show_single_guide(SPGuide* guide, bool show) +{ + if (show) { + guide->showSPGuide(); + } else { + guide->hideSPGuide(); + } +} + +static void sp_namedview_lock_single_guide(SPGuide* guide, bool locked) +{ + guide->set_locked(locked, true); +} + +void sp_namedview_toggle_guides(SPDocument *doc, SPNamedView *namedview) +{ + unsigned int v; + Inkscape::XML::Node *repr = namedview->getRepr(); + unsigned int set = sp_repr_get_boolean(repr, "showguides", &v); + if (!set) { // hide guides if not specified, for backwards compatibility + v = FALSE; + } else { + v = !v; + } + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + sp_repr_set_boolean(repr, "showguides", v); + DocumentUndo::setUndoSensitive(doc, saved); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_TOGGLE_GUIDES); + if (verb) { + desktop->_menu_update.emit(verb->get_code(), namedview->getGuides()); + } + } + doc->setModifiedSinceSave(); +} + +void sp_namedview_guides_toggle_lock(SPDocument *doc, SPNamedView * namedview) +{ + unsigned int v; + Inkscape::XML::Node *repr = namedview->getRepr(); + unsigned int set = sp_repr_get_boolean(repr, "inkscape:lockguides", &v); + if (!set) { // hide guides if not specified, for backwards compatibility + v = true; + } else { + v = !v; + } + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + sp_repr_set_boolean(repr, "inkscape:lockguides", v); + sp_namedview_lock_guides(namedview); + DocumentUndo::setUndoSensitive(doc, saved); + doc->setModifiedSinceSave(); +} + +void sp_namedview_show_grids(SPNamedView * namedview, bool show, bool dirty_document) +{ + namedview->grids_visible = show; + + SPDocument *doc = namedview->document; + Inkscape::XML::Node *repr = namedview->getRepr(); + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + sp_repr_set_boolean(repr, "showgrid", namedview->grids_visible); + DocumentUndo::setUndoSensitive(doc, saved); + + /* we don't want the document to get dirty on startup; that's when + we call this function with dirty_document = false */ + if (dirty_document) { + doc->setModifiedSinceSave(); + } +} + +gchar const *SPNamedView::getName() const +{ + SPException ex; + SP_EXCEPTION_INIT(&ex); + return this->getAttribute("id", &ex); +} + +guint SPNamedView::getViewCount() +{ + return ++viewcount; +} + +std::vector const SPNamedView::getViewList() const +{ + return views; +} + +/* This should be moved somewhere */ + +static gboolean sp_str_to_bool(const gchar *str) +{ + if (str) { + if (!g_ascii_strcasecmp(str, "true") || + !g_ascii_strcasecmp(str, "yes") || + !g_ascii_strcasecmp(str, "y") || + (atoi(str) != 0)) { + return TRUE; + } + } + + return FALSE; +} + +static gboolean sp_nv_read_opacity(const gchar *str, guint32 *color) +{ + if (!str) { + return FALSE; + } + + gchar *u; + gdouble v = g_ascii_strtod(str, &u); + if (!u) { + return FALSE; + } + v = CLAMP(v, 0.0, 1.0); + + *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999); + + return TRUE; +} + +SPNamedView *sp_document_namedview(SPDocument *document, const gchar *id) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPObject *nv = sp_item_group_get_child_by_name(document->getRoot(), nullptr, "sodipodi:namedview"); + g_assert(nv != nullptr); + + if (id == nullptr) { + return (SPNamedView *) nv; + } + + while (nv && strcmp(nv->getId(), id)) { + nv = sp_item_group_get_child_by_name(document->getRoot(), nv, "sodipodi:namedview"); + } + + return (SPNamedView *) nv; +} + +SPNamedView const *sp_document_namedview(SPDocument const *document, const gchar *id) +{ + return sp_document_namedview(const_cast(document), id); // use a const_cast here to avoid duplicating code +} + +void SPNamedView::setGuides(bool v) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_set_boolean(this->getRepr(), "showguides", v); + sp_repr_set_boolean(this->getRepr(), "inkscape:guide-bbox", v); +} + +bool SPNamedView::getGuides() +{ + g_assert(this->getRepr() != nullptr); + unsigned int v; + unsigned int set = sp_repr_get_boolean(this->getRepr(), "showguides", &v); + if (!set) { // show guides if not specified, for backwards compatibility + v = TRUE; + } + + return v; +} + +void SPNamedView::lockGuides() +{ + sp_namedview_lock_guides(this); +} + +/** + * Gets page fitting margin information from the namedview node in the XML. + * \param nv_repr reference to this document's namedview + * \param key the same key used by the RegisteredScalarUnit in + * ui/widget/page-sizer.cpp + * \param margin_units units for the margin + * \param return_units units to return the result in + * \param width width in px (for percentage margins) + * \param height height in px (for percentage margins) + * \param use_width true if the this key is left or right margins, false + * otherwise. Used for percentage margins. + * \return the margin size in px, else 0.0 if anything is invalid. + */ +double SPNamedView::getMarginLength(gchar const * const key, + Inkscape::Util::Unit const * const margin_units, + Inkscape::Util::Unit const * const return_units, + double const width, + double const height, + bool const use_width) +{ + double value; + static Inkscape::Util::Unit const *percent = unit_table.getUnit("%"); + if(!this->storeAsDouble(key,&value)) { + return 0.0; + } + if (*margin_units == *percent) { + return (use_width)? width * value : height * value; + } + if (!margin_units->compatibleWith(return_units)) { + return 0.0; + } + return value; +} + +/** + * Returns namedview's default unit. + * If no default unit is set, "px" is returned + */ +Inkscape::Util::Unit const * SPNamedView::getDisplayUnit() const +{ + return display_units ? display_units : unit_table.getUnit("px"); +} + +/** + * Returns the first grid it could find that isEnabled(). Returns NULL, if none is enabled + */ +Inkscape::CanvasGrid * sp_namedview_get_first_enabled_grid(SPNamedView *namedview) +{ + for(auto grid : namedview->grids) { + if (grid->isEnabled()) + return grid; + } + + return nullptr; +} + +void SPNamedView::translateGuides(Geom::Translate const &tr) { + for(auto & it : this->guides) { + SPGuide &guide = *it; + Geom::Point point_on_line = guide.getPoint(); + point_on_line *= tr; + guide.moveto(point_on_line, true); + } +} + +void SPNamedView::translateGrids(Geom::Translate const &tr) { + for(auto & grid : this->grids) { + grid->setOrigin(grid->origin * tr); + } +} + +void SPNamedView::scrollAllDesktops(double dx, double dy, bool is_scrolling) { + for(auto & view : this->views) { + view->scroll_relative_in_svg_coords(dx, dy, is_scrolling); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-namedview.h b/src/object/sp-namedview.h new file mode 100644 index 0000000..3aabacf --- /dev/null +++ b/src/object/sp-namedview.h @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_SP_NAMEDVIEW_H +#define INKSCAPE_SP_NAMEDVIEW_H + +/* + * implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen + * Copyright (C) Lauris Kaplinski 2000-2002 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define SP_NAMEDVIEW(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_NAMEDVIEW(obj) (dynamic_cast((SPObject*)obj) != NULL) + +#include "sp-object-group.h" +#include "snap.h" +#include "document.h" +#include "util/units.h" +#include + +namespace Inkscape { + class CanvasGrid; + namespace Util { + class Unit; + } +} + +typedef unsigned int guint32; +typedef guint32 GQuark; + +enum { + SP_BORDER_LAYER_BOTTOM, + SP_BORDER_LAYER_TOP +}; + +class SPNamedView : public SPObjectGroup { +public: + SPNamedView(); + ~SPNamedView() override; + + unsigned int editable : 1; + unsigned int showguides : 1; + unsigned int lockguides : 1; + unsigned int pagecheckerboard : 1; + unsigned int showborder : 1; + unsigned int showpageshadow : 1; + unsigned int borderlayer : 2; + + double zoom; + double cx; + double cy; + int window_width; + int window_height; + int window_x; + int window_y; + int window_maximized; + + SnapManager snap_manager; + std::vector grids; + bool grids_visible; + + Inkscape::Util::Unit const *display_units; // Units used for the UI (*not* the same as units of SVG coordinates) + Inkscape::Util::Unit const *page_size_units; // Only used in "Custom size" part of Document Properties dialog + + GQuark default_layer_id; + + double connector_spacing; + + guint32 guidecolor; + guint32 guidehicolor; + guint32 bordercolor; + guint32 pagecolor; + guint32 pageshadow; + + std::vector guides; + std::vector views; + + int viewcount; + + void show(SPDesktop *desktop); + void hide(SPDesktop const *desktop); + void activateGuides(void* desktop, bool active); + char const *getName() const; + unsigned int getViewCount(); + std::vector const getViewList() const; + Inkscape::Util::Unit const * getDisplayUnit() const; + + void translateGuides(Geom::Translate const &translation); + void translateGrids(Geom::Translate const &translation); + void scrollAllDesktops(double dx, double dy, bool is_scrolling); + void writeNewGrid(SPDocument *document,int gridtype); + bool getSnapGlobal() const; + void setSnapGlobal(bool v); + void setGuides(bool v); + bool getGuides(); + void lockGuides(); + +private: + double getMarginLength(gchar const * const key,Inkscape::Util::Unit const * const margin_units,Inkscape::Util::Unit const * const return_units,double const width,double const height,bool const use_width); + friend class SPDocument; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + + +SPNamedView *sp_document_namedview(SPDocument *document, char const *name); +SPNamedView const *sp_document_namedview(SPDocument const *document, char const *name); + +void sp_namedview_window_from_document(SPDesktop *desktop); +void sp_namedview_zoom_and_view_from_document(SPDesktop *desktop); +void sp_namedview_document_from_window(SPDesktop *desktop); +void sp_namedview_update_layers_from_document (SPDesktop *desktop); + +void sp_namedview_toggle_guides(SPDocument *doc, SPNamedView *namedview); +void sp_namedview_guides_toggle_lock(SPDocument *doc, SPNamedView *namedview); +void sp_namedview_show_grids(SPNamedView *namedview, bool show, bool dirty_document); +Inkscape::CanvasGrid * sp_namedview_get_first_enabled_grid(SPNamedView *namedview); + + +#endif /* !INKSCAPE_SP_NAMEDVIEW_H */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-object-group.cpp b/src/object/sp-object-group.cpp new file mode 100644 index 0000000..0977287 --- /dev/null +++ b/src/object/sp-object-group.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Abstract base class for non-item groups + * + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2003 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object-group.h" +#include "xml/repr.h" +#include "document.h" + +SPObjectGroup::SPObjectGroup() : SPObject() { +} + +SPObjectGroup::~SPObjectGroup() = default; + +void SPObjectGroup::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject::child_added(child, ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPObjectGroup::remove_child(Inkscape::XML::Node *child) { + SPObject::remove_child(child); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +void SPObjectGroup::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) { + SPObject::order_changed(child, old_ref, new_ref); + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + + +Inkscape::XML::Node *SPObjectGroup::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = xml_doc->createElement("svg:g"); + } + + std::vector l; + for (auto& child: children) { + Inkscape::XML::Node *crepr = child.updateRepr(xml_doc, nullptr, flags); + + if (crepr) { + l.push_back(crepr); + } + } + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + child.updateRepr(flags); + } + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-object-group.h b/src/object/sp-object-group.h new file mode 100644 index 0000000..604c9ba --- /dev/null +++ b/src/object/sp-object-group.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_OBJECTGROUP_H +#define SEEN_SP_OBJECTGROUP_H + +/* + * Abstract base class for non-item groups + * + * Author: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 1999-2003 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_OBJECTGROUP(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_OBJECTGROUP(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPObjectGroup : public SPObject { +public: + SPObjectGroup(); + ~SPObjectGroup() override; + +protected: + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old, Inkscape::XML::Node* new_repr) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif // SEEN_SP_OBJECTGROUP_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-object.cpp b/src/object/sp-object.cpp new file mode 100644 index 0000000..2ffbee2 --- /dev/null +++ b/src/object/sp-object.cpp @@ -0,0 +1,1689 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SPObject implementation. + * + * Authors: + * Lauris Kaplinski + * bulia byak + * Stephen Silver + * Jon A. Cruz + * Abhishek Sharma + * Adrian Boguszewski + * + * Copyright (C) 1999-2016 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include + +#include "helper/sp-marshal.h" +#include "xml/node-event-vector.h" +#include "attributes.h" +#include "attribute-rel-util.h" +#include "color-profile.h" +#include "document.h" +#include "preferences.h" +#include "style.h" +#include "live_effects/lpeobject.h" +#include "sp-factory.h" +#include "sp-paint-server.h" +#include "sp-root.h" +#include "sp-style-elem.h" +#include "sp-script.h" +#include "streq.h" +#include "strneq.h" +#include "xml/node-fns.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" +#include "debug/demangle.h" +#include "util/format.h" +#include "util/longest-common-suffix.h" + +#define noSP_OBJECT_DEBUG_CASCADE + +#define noSP_OBJECT_DEBUG + +#ifdef SP_OBJECT_DEBUG +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /* */ +#endif + +// Define to enable indented tracing of SPObject. +//#define OBJECT_TRACE +unsigned SPObject::indent_level = 0; + +guint update_in_progress = 0; // guard against update-during-update + +Inkscape::XML::NodeEventVector object_event_vector = { + SPObject::repr_child_added, + SPObject::repr_child_removed, + SPObject::repr_attr_changed, + SPObject::repr_content_changed, + SPObject::repr_order_changed +}; + +/** + * A friend class used to set internal members on SPObject so as to not expose settors in SPObject's public API + */ +class SPObjectImpl +{ +public: + +/** + * Null's the id member of an SPObject without attempting to free prior contents. + * + * @param[inout] obj Pointer to the object which's id shall be nulled. + */ + static void setIdNull( SPObject* obj ) { + if (obj) { + obj->id = nullptr; + } + } + +/** + * Sets the id member of an object, freeing any prior content. + * + * @param[inout] obj Pointer to the object which's id shall be set. + * @param[in] id New id + */ + static void setId( SPObject* obj, gchar const* id ) { + if (obj && (id != obj->id) ) { + if (obj->id) { + g_free(obj->id); + obj->id = nullptr; + } + if (id) { + obj->id = g_strdup(id); + } + } + } +}; + +/** + * Constructor, sets all attributes to default values. + */ +SPObject::SPObject() + : cloned(0), clone_original(nullptr), uflags(0), mflags(0), hrefcount(0), _total_hrefcount(0), + document(nullptr), parent(nullptr), id(nullptr), repr(nullptr), refCount(1), hrefList(std::list()), + _successor(nullptr), _collection_policy(SPObject::COLLECT_WITH_PARENT), + _label(nullptr), _default_label(nullptr) +{ + debug("id=%p, typename=%s",this, g_type_name_from_instance((GTypeInstance*)this)); + + //used XML Tree here. + this->getRepr(); // TODO check why this call is made + + SPObjectImpl::setIdNull(this); + + // FIXME: now we create style for all objects, but per SVG, only the following can have style attribute: + // vg, g, defs, desc, title, symbol, use, image, switch, path, rect, circle, ellipse, line, polyline, + // polygon, text, tspan, tref, textPath, altGlyph, glyphRef, marker, linearGradient, radialGradient, + // stop, pattern, clipPath, mask, filter, feImage, a, font, glyph, missing-glyph, foreignObject + this->style = new SPStyle( nullptr, this ); // Is it necessary to call with "this"? + this->context_style = nullptr; +} + +/** + * Destructor, frees the used memory and unreferences a potential successor of the object. + */ +SPObject::~SPObject() { + g_free(this->_label); + g_free(this->_default_label); + + this->_label = nullptr; + this->_default_label = nullptr; + + if (this->_successor) { + sp_object_unref(this->_successor, nullptr); + this->_successor = nullptr; + } + if (parent) { + parent->children.erase(parent->children.iterator_to(*this)); + } + + if( style == nullptr ) { + // style pointer could be NULL if unreffed too many times. + // Conjecture: style pointer is never NULL. + std::cerr << "SPObject::~SPObject(): style pointer is NULL" << std::endl; + } else if( style->refCount() > 1 ) { + // Conjecture: style pointer should be unreffed by other classes before reaching here. + // Conjecture is false for SPTSpan where ref is held by InputStreamTextSource. + // As an additional note: + // The outer tspan of a nested tspan will result in a ref count of five: one for the + // TSpan itself, one for the InputStreamTextSource instance before the inner tspan and + // one for the one after, along with one for each corresponding DrawingText instance. + // std::cerr << "SPObject::~SPObject(): someone else still holding ref to style" << std::endl; + // + sp_style_unref( this->style ); + } else { + delete this->style; + } +} + +// CPPIFY: make pure virtual +void SPObject::read_content() { + //throw; +} + +void SPObject::update(SPCtx* /*ctx*/, unsigned int /*flags*/) { + //throw; +} + +void SPObject::modified(unsigned int /*flags*/) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::modified (default) (empty function)" ); + objectTrace( "SPObject::modified (default)", false ); +#endif + //throw; +} + +namespace { + +namespace Debug = Inkscape::Debug; +namespace Util = Inkscape::Util; + +typedef Debug::SimpleEvent BaseRefCountEvent; + +class RefCountEvent : public BaseRefCountEvent { +public: + RefCountEvent(SPObject *object, int bias, char const *name) + : BaseRefCountEvent(name) + { + _addProperty("object", Util::format("%p", object).pointer()); + _addProperty("class", Debug::demangle(g_type_name(G_TYPE_FROM_INSTANCE(object)))); + _addProperty("new-refcount", Util::format("%d", G_OBJECT(object)->ref_count + bias).pointer()); + } +}; + +class RefEvent : public RefCountEvent { +public: + RefEvent(SPObject *object) + : RefCountEvent(object, 1, "sp-object-ref") + {} +}; + +class UnrefEvent : public RefCountEvent { +public: + UnrefEvent(SPObject *object) + : RefCountEvent(object, -1, "sp-object-unref") + {} +}; + +} + +gchar const* SPObject::getId() const { + return id; +} + +Inkscape::XML::Node * SPObject::getRepr() { + return repr; +} + +Inkscape::XML::Node const* SPObject::getRepr() const{ + return repr; +} + + +SPObject *sp_object_ref(SPObject *object, SPObject *owner) +{ + g_return_val_if_fail(object != nullptr, NULL); + g_return_val_if_fail(SP_IS_OBJECT(object), NULL); + g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL); + + Inkscape::Debug::EventTracker tracker(object); + + object->refCount++; + + return object; +} + +SPObject *sp_object_unref(SPObject *object, SPObject *owner) +{ + g_return_val_if_fail(object != nullptr, NULL); + g_return_val_if_fail(SP_IS_OBJECT(object), NULL); + g_return_val_if_fail(!owner || SP_IS_OBJECT(owner), NULL); + + Inkscape::Debug::EventTracker tracker(object); + + object->refCount--; + + if (object->refCount <= 0) { + delete object; + } + + return nullptr; +} + +void SPObject::hrefObject(SPObject* owner) +{ + // if (owner) std::cout << " owner: " << *owner << std::endl; + + // If owner is a clone, do not increase hrefcount, it's already href'ed by original. + if (!owner || !owner->cloned) { + hrefcount++; + _updateTotalHRefCount(1); + } + + if(owner) + hrefList.push_front(owner); +} + +void SPObject::unhrefObject(SPObject* owner) +{ + g_return_if_fail(hrefcount > 0); + + if (!owner || !owner->cloned) { + hrefcount--; + } + + _updateTotalHRefCount(-1); + + if(owner) + hrefList.remove(owner); +} + +void SPObject::_updateTotalHRefCount(int increment) { + SPObject *topmost_collectable = nullptr; + for ( SPObject *iter = this ; iter ; iter = iter->parent ) { + iter->_total_hrefcount += increment; + if ( iter->_total_hrefcount < iter->hrefcount ) { + g_critical("HRefs overcounted"); + } + if ( iter->_total_hrefcount == 0 && + iter->_collection_policy != COLLECT_WITH_PARENT ) + { + topmost_collectable = iter; + } + } + if (topmost_collectable) { + topmost_collectable->requestOrphanCollection(); + } +} + +bool SPObject::isAncestorOf(SPObject const *object) const { + g_return_val_if_fail(object != nullptr, false); + object = object->parent; + while (object) { + if ( object == this ) { + return true; + } + object = object->parent; + } + return false; +} + +namespace { + +bool same_objects(SPObject const &a, SPObject const &b) { + return &a == &b; +} + +} + +SPObject const *SPObject::nearestCommonAncestor(SPObject const *object) const { + g_return_val_if_fail(object != nullptr, NULL); + + using Inkscape::Algorithms::longest_common_suffix; + return longest_common_suffix(this, object, nullptr, &same_objects); +} + +static SPObject const *AncestorSon(SPObject const *obj, SPObject const *ancestor) { + SPObject const *result = nullptr; + if ( obj && ancestor ) { + if (obj->parent == ancestor) { + result = obj; + } else { + result = AncestorSon(obj->parent, ancestor); + } + } + return result; +} + +int sp_object_compare_position(SPObject const *first, SPObject const *second) +{ + int result = 0; + if (first != second) { + SPObject const *ancestor = first->nearestCommonAncestor(second); + // Need a common ancestor to be able to compare + if ( ancestor ) { + // we have an object and its ancestor (should not happen when sorting selection) + if (ancestor == first) { + result = 1; + } else if (ancestor == second) { + result = -1; + } else { + SPObject const *to_first = AncestorSon(first, ancestor); + SPObject const *to_second = AncestorSon(second, ancestor); + + g_assert(to_second->parent == to_first->parent); + + result = sp_repr_compare_position(to_first->getRepr(), to_second->getRepr()); + } + } + } + return result; +} + +bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second){ + return sp_object_compare_position(first,second)<0; +} + + +SPObject *SPObject::appendChildRepr(Inkscape::XML::Node *repr) { + if ( !cloned ) { + getRepr()->appendChild(repr); + return document->getObjectByRepr(repr); + } else { + g_critical("Attempt to append repr as child of cloned object"); + return nullptr; + } +} + +void SPObject::setCSS(SPCSSAttr *css, gchar const *attr) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_css_set(this->getRepr(), css, attr); +} + +void SPObject::changeCSS(SPCSSAttr *css, gchar const *attr) +{ + g_assert(this->getRepr() != nullptr); + sp_repr_css_change(this->getRepr(), css, attr); +} + +std::vector SPObject::childList(bool add_ref, Action) { + std::vector l; + for (auto& child: children) { + if (add_ref) { + sp_object_ref(&child); + } + l.push_back(&child); + } + return l; +} + +gchar const *SPObject::label() const { + return _label; +} + +gchar const *SPObject::defaultLabel() const { + if (_label) { + return _label; + } else { + if (!_default_label) { + if (getId()) { + _default_label = g_strdup_printf("#%s", getId()); + } else if (getRepr()) { + _default_label = g_strdup_printf("<%s>", getRepr()->name()); + } else { + _default_label = g_strdup("Default label"); + } + } + return _default_label; + } +} + +void SPObject::setLabel(gchar const *label) +{ + getRepr()->setAttribute("inkscape:label", label); +} + + +void SPObject::requestOrphanCollection() { + g_return_if_fail(document != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // do not remove style or script elements (Bug #276244) + if (dynamic_cast(this)) { + // leave it + } else if (dynamic_cast(this)) { + // leave it + } else if ((! prefs->getBool("/options/cleanupswatches/value", false)) && SP_IS_PAINT_SERVER(this) && static_cast(this)->isSwatch() ) { + // leave it + } else if (IS_COLORPROFILE(this)) { + // leave it + } else if (dynamic_cast(this)) { + document->queueForOrphanCollection(this); + } else { + document->queueForOrphanCollection(this); + + /** \todo + * This is a temporary hack added to make fill&stroke rebuild its + * gradient list when the defs are vacuumed. gradient-vector.cpp + * listens to the modified signal on defs, and now we give it that + * signal. Mental says that this should be made automatic by + * merging SPObjectGroup with SPObject; SPObjectGroup would issue + * this signal automatically. Or maybe just derive SPDefs from + * SPObjectGroup? + */ + + this->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); + } +} + +void SPObject::_sendDeleteSignalRecursive() { + for (auto& child: children) { + child._delete_signal.emit(&child); + child._sendDeleteSignalRecursive(); + } +} + +void SPObject::deleteObject(bool propagate, bool propagate_descendants) +{ + sp_object_ref(this, nullptr); + if ( SP_IS_LPE_ITEM(this) && SP_LPE_ITEM(this)->hasPathEffect()) { + SP_LPE_ITEM(this)->removeAllPathEffects(false); + } + if (propagate) { + _delete_signal.emit(this); + } + if (propagate_descendants) { + this->_sendDeleteSignalRecursive(); + } + + Inkscape::XML::Node *repr = getRepr(); + if (repr && repr->parent()) { + sp_repr_unparent(repr); + } + + if (_successor) { + _successor->deleteObject(propagate, propagate_descendants); + } + sp_object_unref(this, nullptr); +} + +void SPObject::cropToObject(SPObject *except) +{ + std::vector toDelete; + for (auto& child: children) { + if (SP_IS_ITEM(&child)) { + if (child.isAncestorOf(except)) { + child.cropToObject(except); + } else if(&child != except) { + sp_object_ref(&child, nullptr); + toDelete.push_back(&child); + } + } + } + for (auto & i : toDelete) { + i->deleteObject(true, true); + sp_object_unref(i, nullptr); + } +} + +void SPObject::attach(SPObject *object, SPObject *prev) +{ + //g_return_if_fail(parent != NULL); + //g_return_if_fail(SP_IS_OBJECT(parent)); + g_return_if_fail(object != nullptr); + g_return_if_fail(SP_IS_OBJECT(object)); + g_return_if_fail(!prev || SP_IS_OBJECT(prev)); + g_return_if_fail(!prev || prev->parent == this); + g_return_if_fail(!object->parent); + + sp_object_ref(object, this); + object->parent = this; + this->_updateTotalHRefCount(object->_total_hrefcount); + + auto it = children.begin(); + if (prev != nullptr) { + it = ++children.iterator_to(*prev); + } + children.insert(it, *object); + + if (!object->xml_space.set) + object->xml_space.value = this->xml_space.value; +} + +void SPObject::reorder(SPObject* obj, SPObject* prev) { + g_return_if_fail(obj != nullptr); + g_return_if_fail(obj->parent); + g_return_if_fail(obj->parent == this); + g_return_if_fail(obj != prev); + g_return_if_fail(!prev || prev->parent == obj->parent); + + auto it = children.begin(); + if (prev != nullptr) { + it = ++children.iterator_to(*prev); + } + + children.splice(it, children, children.iterator_to(*obj)); +} + +void SPObject::detach(SPObject *object) +{ + //g_return_if_fail(parent != NULL); + //g_return_if_fail(SP_IS_OBJECT(parent)); + g_return_if_fail(object != nullptr); + g_return_if_fail(SP_IS_OBJECT(object)); + g_return_if_fail(object->parent == this); + + children.erase(children.iterator_to(*object)); + object->releaseReferences(); + + object->parent = nullptr; + + this->_updateTotalHRefCount(-object->_total_hrefcount); + sp_object_unref(object, this); +} + +SPObject *SPObject::get_child_by_repr(Inkscape::XML::Node *repr) +{ + g_return_val_if_fail(repr != nullptr, NULL); + SPObject *result = nullptr; + + if (children.size() > 0 && children.back().getRepr() == repr) { + result = &children.back(); // optimization for common scenario + } else { + for (auto& child: children) { + if (child.getRepr() == repr) { + result = &child; + break; + } + } + } + return result; +} + +void SPObject::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPObject* object = this; + + const std::string type_string = NodeTraits::get_type_string(*child); + + SPObject* ochild = SPFactory::createObject(type_string); + if (ochild == nullptr) { + // Currently, there are many node types that do not have + // corresponding classes in the SPObject tree. + // (rdf:RDF, inkscape:clipboard, ...) + // Thus, simply ignore this case for now. + return; + } + + SPObject *prev = ref ? object->get_child_by_repr(ref) : nullptr; + object->attach(ochild, prev); + sp_object_unref(ochild, nullptr); + + ochild->invoke_build(object->document, child, object->cloned); +} + +void SPObject::release() { + SPObject* object = this; + debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); + auto tmp = children | boost::adaptors::transformed([](SPObject& obj){return &obj;}); + std::vector toRelease(tmp.begin(), tmp.end()); + + for (auto& p: toRelease) { + object->detach(p); + } +} + +void SPObject::remove_child(Inkscape::XML::Node* child) { + debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); + + SPObject *ochild = this->get_child_by_repr(child); + + // If the xml node has got a corresponding child in the object tree + if (ochild) { + this->detach(ochild); + } +} + +void SPObject::order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node * /*old_ref*/, Inkscape::XML::Node *new_ref) { + SPObject* object = this; + + SPObject *ochild = object->get_child_by_repr(child); + g_return_if_fail(ochild != nullptr); + SPObject *prev = new_ref ? object->get_child_by_repr(new_ref) : nullptr; + object->reorder(ochild, prev); + ochild->_position_changed_signal.emit(ochild); +} + +void SPObject::build(SPDocument *document, Inkscape::XML::Node *repr) { + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::build" ); +#endif + SPObject* object = this; + + /* Nothing specific here */ + debug("id=%p, typename=%s", object, g_type_name_from_instance((GTypeInstance*)object)); + + object->readAttr("xml:space"); + object->readAttr("lang"); + object->readAttr("xml:lang"); // "xml:lang" overrides "lang" per spec, read it last. + object->readAttr("inkscape:label"); + object->readAttr("inkscape:collect"); + + // Inherit if not set + if (lang.empty() && object->parent) { + lang = object->parent->lang; + } + + if(object->cloned && (repr->attribute("id")) ) // The cases where this happens are when the "original" has no id. This happens + // if it is a SPString (a TextNode, e.g. in a ), or when importing + // stuff externally modified to have no id. + object->clone_original = document->getObjectById(repr->attribute("id")); + + for (Inkscape::XML::Node *rchild = repr->firstChild() ; rchild != nullptr; rchild = rchild->next()) { + const std::string typeString = NodeTraits::get_type_string(*rchild); + + SPObject* child = SPFactory::createObject(typeString); + if (child == nullptr) { + // Currently, there are many node types that do not have + // corresponding classes in the SPObject tree. + // (rdf:RDF, inkscape:clipboard, ...) + // Thus, simply ignore this case for now. + continue; + } + + object->attach(child, object->lastChild()); + sp_object_unref(child, nullptr); + child->invoke_build(document, rchild, object->cloned); + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::build", false ); +#endif +} + +void SPObject::invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::invoke_build" ); +#endif + debug("id=%p, typename=%s", this, g_type_name_from_instance((GTypeInstance*)this)); + + //g_assert(object != NULL); + //g_assert(SP_IS_OBJECT(object)); + g_assert(document != nullptr); + g_assert(repr != nullptr); + + g_assert(this->document == nullptr); + g_assert(this->repr == nullptr); + g_assert(this->getId() == nullptr); + + /* Bookkeeping */ + + this->document = document; + this->repr = repr; + if (!cloned) { + Inkscape::GC::anchor(repr); + } + this->cloned = cloned; + + /* Invoke derived methods, if any */ + this->build(document, repr); + + if ( !cloned ) { + this->document->bindObjectToRepr(this->repr, this); + + if (Inkscape::XML::id_permitted(this->repr)) { + /* If we are not cloned, and not seeking, force unique id */ + gchar const *id = this->repr->attribute("id"); + if (!document->isSeeking()) { + { + gchar *realid = sp_object_get_unique_id(this, id); + g_assert(realid != nullptr); + + this->document->bindObjectToId(realid, this); + SPObjectImpl::setId(this, realid); + g_free(realid); + } + + /* Redefine ID, if required */ + if ((id == nullptr) || (std::strcmp(id, this->getId()) != 0)) { + this->repr->setAttribute("id", this->getId()); + } + } else if (id) { + // bind if id, but no conflict -- otherwise, we can expect + // a subsequent setting of the id attribute + if (!this->document->getObjectById(id)) { + this->document->bindObjectToId(id, this); + SPObjectImpl::setId(this, id); + } + } + } + } else { + g_assert(this->getId() == nullptr); + } + + + /* Signalling (should be connected AFTER processing derived methods */ + sp_repr_add_listener(repr, &object_event_vector, this); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::invoke_build", false ); +#endif +} + +int SPObject::getIntAttribute(char const *key, int def) +{ + sp_repr_get_int(getRepr(),key,&def); + return def; +} + +unsigned SPObject::getPosition(){ + g_assert(this->repr); + + return repr->position(); +} + +void SPObject::appendChild(Inkscape::XML::Node *child) { + g_assert(this->repr); + + repr->appendChild(child); +} + +SPObject* SPObject::nthChild(unsigned index) { + g_assert(this->repr); + if (hasChildren()) { + std::vector l; + unsigned counter = 0; + for (auto& child: children) { + if (counter == index) { + return &child; + } + counter++; + } + } + return nullptr; +} + +void SPObject::addChild(Inkscape::XML::Node *child, Inkscape::XML::Node * prev) +{ + g_assert(this->repr); + + repr->addChild(child,prev); +} + +void SPObject::releaseReferences() { + g_assert(this->document); + g_assert(this->repr); + + sp_repr_remove_listener_by_data(this->repr, this); + + this->_release_signal.emit(this); + + this->release(); + + /* all hrefs should be released by the "release" handlers */ + g_assert(this->hrefcount == 0); + + if (!cloned) { + if (this->id) { + this->document->bindObjectToId(this->id, nullptr); + } + g_free(this->id); + this->id = nullptr; + + g_free(this->_default_label); + this->_default_label = nullptr; + + this->document->bindObjectToRepr(this->repr, nullptr); + + Inkscape::GC::release(this->repr); + } else { + g_assert(!this->id); + } + + // style belongs to SPObject, we should not need to unref here. + // if (this->style) { + // this->style = sp_style_unref(this->style); + // } + + this->document = nullptr; + this->repr = nullptr; +} + + +SPObject *SPObject::getPrev() +{ + SPObject *prev = nullptr; + if (parent && !parent->children.empty() && &parent->children.front() != this) { + prev = &*(--parent->children.iterator_to(*this)); + } + return prev; +} + +SPObject* SPObject::getNext() +{ + SPObject *next = nullptr; + if (parent && !parent->children.empty() && &parent->children.back() != this) { + next = &*(++parent->children.iterator_to(*this)); + } + return next; +} + +void SPObject::repr_child_added(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data) +{ + SPObject *object = SP_OBJECT(data); + + object->child_added(child, ref); +} + +void SPObject::repr_child_removed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/, gpointer data) +{ + SPObject *object = SP_OBJECT(data); + + object->remove_child(child); +} + +void SPObject::repr_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node *child, Inkscape::XML::Node *old, Inkscape::XML::Node *newer, gpointer data) +{ + SPObject *object = SP_OBJECT(data); + + object->order_changed(child, old, newer); +} + +void SPObject::set(SPAttributeEnum key, gchar const* value) { + +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPObject::set: " << key << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + g_assert(key != SP_ATTR_INVALID); + + SPObject* object = this; + + switch (key) { + + case SP_ATTR_ID: + + //XML Tree being used here. + if ( !object->cloned && object->getRepr()->type() == Inkscape::XML::ELEMENT_NODE ) { + SPDocument *document=object->document; + SPObject *conflict=nullptr; + + gchar const *new_id = value; + + if (new_id) { + conflict = document->getObjectById((char const *)new_id); + } + + if ( conflict && conflict != object ) { + if (!document->isSeeking()) { + sp_object_ref(conflict, nullptr); + // give the conflicting object a new ID + gchar *new_conflict_id = sp_object_get_unique_id(conflict, nullptr); + conflict->setAttribute("id", new_conflict_id); + g_free(new_conflict_id); + sp_object_unref(conflict, nullptr); + } else { + new_id = nullptr; + } + } + + if (object->getId()) { + document->bindObjectToId(object->getId(), nullptr); + SPObjectImpl::setId(object, nullptr); + } + + if (new_id) { + SPObjectImpl::setId(object, new_id); + document->bindObjectToId(object->getId(), object); + } + + g_free(object->_default_label); + object->_default_label = nullptr; + } + break; + + case SP_ATTR_INKSCAPE_LABEL: + g_free(object->_label); + if (value) { + object->_label = g_strdup(value); + } else { + object->_label = nullptr; + } + g_free(object->_default_label); + object->_default_label = nullptr; + break; + + case SP_ATTR_INKSCAPE_COLLECT: + if ( value && !std::strcmp(value, "always") ) { + object->setCollectionPolicy(SPObject::ALWAYS_COLLECT); + } else { + object->setCollectionPolicy(SPObject::COLLECT_WITH_PARENT); + } + break; + + case SP_ATTR_XML_SPACE: + if (value && !std::strcmp(value, "preserve")) { + object->xml_space.value = SP_XML_SPACE_PRESERVE; + object->xml_space.set = TRUE; + } else if (value && !std::strcmp(value, "default")) { + object->xml_space.value = SP_XML_SPACE_DEFAULT; + object->xml_space.set = TRUE; + } else if (object->parent) { + SPObject *parent; + parent = object->parent; + object->xml_space.value = parent->xml_space.value; + } + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + + case SP_ATTR_LANG: + if (value) { + lang = value; + // To do: sanity check + } + break; + + case SP_ATTR_XML_LANG: + if (value) { + lang = value; + // To do: sanity check + } + break; + + case SP_ATTR_STYLE: + object->style->readFromObject( object ); + object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + break; + + default: + break; + } +#ifdef OBJECT_TRACE + objectTrace( "SPObject::set", false ); +#endif +} + +void SPObject::setKeyValue(SPAttributeEnum key, gchar const *value) +{ + //g_assert(object != NULL); + //g_assert(SP_IS_OBJECT(object)); + + this->set(key, value); +} + +void SPObject::readAttr(gchar const *key) +{ + //g_assert(object != NULL); + //g_assert(SP_IS_OBJECT(object)); + g_assert(key != nullptr); + + //XML Tree being used here. + g_assert(this->getRepr() != nullptr); + + auto keyid = sp_attribute_lookup(key); + if (keyid != SP_ATTR_INVALID) { + /* Retrieve the 'key' attribute from the object's XML representation */ + gchar const *value = getRepr()->attribute(key); + + setKeyValue(keyid, value); + } +} + +void SPObject::repr_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const *key, gchar const * /*oldval*/, gchar const * /*newval*/, bool is_interactive, gpointer data) +{ + SPObject *object = SP_OBJECT(data); + + object->readAttr(key); + + // manual changes to extension attributes require the normal + // attributes, which depend on them, to be updated immediately + if (is_interactive) { + object->updateRepr(0); + } +} + +void SPObject::repr_content_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*oldcontent*/, gchar const * /*newcontent*/, gpointer data) +{ + SPObject *object = SP_OBJECT(data); + + object->read_content(); +} + +/** + * Return string representation of space value. + */ +static gchar const *sp_xml_get_space_string(unsigned int space) +{ + switch (space) { + case SP_XML_SPACE_DEFAULT: + return "default"; + case SP_XML_SPACE_PRESERVE: + return "preserve"; + default: + return nullptr; + } +} + +Inkscape::XML::Node* SPObject::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::write" ); +#endif + + if (!repr && (flags & SP_OBJECT_WRITE_BUILD)) { + repr = this->getRepr()->duplicate(doc); + if (!( flags & SP_OBJECT_WRITE_EXT )) { + repr->removeAttribute("inkscape:collect"); + } + } else if (repr) { + repr->setAttribute("id", this->getId()); + + if (this->xml_space.set) { + char const *xml_space; + xml_space = sp_xml_get_space_string(this->xml_space.value); + repr->setAttribute("xml:space", xml_space); + } + + if ( flags & SP_OBJECT_WRITE_EXT && + this->collectionPolicy() == SPObject::ALWAYS_COLLECT ) + { + repr->setAttribute("inkscape:collect", "always"); + } else { + repr->removeAttribute("inkscape:collect"); + } + + if (style) { + // Write if property set by style attribute in this object + Glib::ustring s = + style->write(SP_STYLE_FLAG_IFSET | SP_STYLE_FLAG_IFSRC, SP_STYLE_SRC_STYLE_PROP); + + // Write style attributes (SP_STYLE_SRC_ATTRIBUTE) back to xml object + bool any_written = false; + auto properties = style->properties(); + for (auto * prop : properties) { + if(prop->shall_write(SP_STYLE_FLAG_IFSET | SP_STYLE_FLAG_IFSRC, SP_STYLE_SRC_ATTRIBUTE)) { + // WARNING: We don't know for sure if the css names are the same as the attribute names + repr->setAttribute(prop->name().c_str(), prop->get_value().c_str()); + any_written = true; + } + } + if(any_written) { + // We need to ask the object to update the style and keep things in sync + // see `case SP_ATTR_STYLE` above for how the style attr itself does this. + style->readFromObject(this); + requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + + // Check for valid attributes. This may be time consuming. + // It is useful, though, for debugging Inkscape code. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if( prefs->getBool("/options/svgoutput/check_on_editing") ) { + + unsigned int flags = sp_attribute_clean_get_prefs(); + Glib::ustring s_cleaned = sp_attribute_clean_style( repr, s.c_str(), flags ); + } + + repr->setAttributeOrRemoveIfEmpty("style", s); + } else { + /** \todo I'm not sure what to do in this case. Bug #1165868 + * suggests that it can arise, but the submitter doesn't know + * how to do so reliably. The main two options are either + * leave repr's style attribute unchanged, or explicitly clear it. + * Must also consider what to do with property attributes for + * the element; see below. + */ + char const *style_str = repr->attribute("style"); + if (!style_str) { + style_str = "NULL"; + } + g_warning("Item's style is NULL; repr style attribute is %s", style_str); + } + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::write", false ); +#endif + return repr; +} + +Inkscape::XML::Node * SPObject::updateRepr(unsigned int flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1" ); +#endif + + if ( !cloned ) { + Inkscape::XML::Node *repr = getRepr(); + if (repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return updateRepr(repr->document(), repr, flags); + } else { + g_critical("Attempt to update non-existent repr"); +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return nullptr; + } + } else { + /* cloned objects have no repr */ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 1", false ); +#endif + return nullptr; + } +} + +Inkscape::XML::Node * SPObject::updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags) +{ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 2" ); +#endif + + g_assert(doc != nullptr); + + if (cloned) { + /* cloned objects have no repr */ +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateRepr 2", false ); +#endif + return nullptr; + } + + if (!(flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = getRepr(); + } + +#ifdef OBJECT_TRACE + Inkscape::XML::Node *node = write(doc, repr, flags); + objectTrace( "SPObject::updateRepr 2", false ); + return node; +#else + return this->write(doc, repr, flags); +#endif + +} + +/* Modification */ + +void SPObject::requestDisplayUpdate(unsigned int flags) +{ + g_return_if_fail( this->document != nullptr ); + + // update_in_progress is a global variable. It can be come greater than one when reading in a second + // document (as in creating the broken image bitmap). It is still an important warning so we don't + // remove it entirely. We probably shouldn't be calling requestDisplayUpdate in the set() methods. + if (update_in_progress > 2) { + g_print("WARNING: Requested update while update in progress, counter = %d\n", update_in_progress); + } + + /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or + * SP_OBJECT_CHILD_MODIFIED_FLAG */ + g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); + g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); + g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestDisplayUpdate" ); +#endif + + bool already_propagated = (!(this->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); + //https://stackoverflow.com/a/7841333 + if ((this->uflags & flags) != flags ) { + this->uflags |= flags; + } + /* If requestModified has already been called on this object or one of its children, then we + * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. + */ + if (already_propagated) { + if(this->document) { + if (parent) { + parent->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); + } else { + this->document->requestModified(); + } + } + } + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestDisplayUpdate", false ); +#endif + +} + +void SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) +{ + g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateDisplay" ); +#endif + + update_in_progress ++; + +#ifdef SP_OBJECT_DEBUG_CASCADE + g_print("Update %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); +#endif + + /* Get this flags */ + flags |= this->uflags; + /* Copy flags to modified cascade for later processing */ + this->mflags |= this->uflags; + /* We have to clear flags here to allow rescheduling update */ + this->uflags = 0; + + // Merge style if we have good reasons to think that parent style is changed */ + /** \todo + * I am not sure whether we should check only propagated + * flag. We are currently assuming that style parsing is + * done immediately. I think this is correct (Lauris). + */ + if ((flags & SP_OBJECT_STYLE_MODIFIED_FLAG) && (flags & SP_OBJECT_PARENT_MODIFIED_FLAG)) { + if (this->style && this->parent) { + style->cascade( this->parent->style ); + } + } + + try + { + this->update(ctx, flags); + } + catch(...) + { + /** \todo + * in case of catching an exception we need to inform the user somehow that the document is corrupted + * maybe by implementing an document flag documentOk + * or by a modal error dialog + */ + g_warning("SPObject::updateDisplay(SPCtx *ctx, unsigned int flags) : throw in ((SPObjectClass *) G_OBJECT_GET_CLASS(this))->update(this, ctx, flags);"); + } + + update_in_progress --; + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::updateDisplay", false ); +#endif +} + +void SPObject::requestModified(unsigned int flags) +{ + g_return_if_fail( this->document != nullptr ); + + /* requestModified must be used only to set one of SP_OBJECT_MODIFIED_FLAG or + * SP_OBJECT_CHILD_MODIFIED_FLAG */ + g_return_if_fail(!(flags & SP_OBJECT_PARENT_MODIFIED_FLAG)); + g_return_if_fail((flags & SP_OBJECT_MODIFIED_FLAG) || (flags & SP_OBJECT_CHILD_MODIFIED_FLAG)); + g_return_if_fail(!((flags & SP_OBJECT_MODIFIED_FLAG) && (flags & SP_OBJECT_CHILD_MODIFIED_FLAG))); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestModified" ); +#endif + + bool already_propagated = (!(this->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))); + + this->mflags |= flags; + + /* If requestModified has already been called on this object or one of its children, then we + * don't need to set CHILD_MODIFIED on our ancestors because it's already been done. + */ + if (already_propagated) { + if (parent) { + parent->requestModified(SP_OBJECT_CHILD_MODIFIED_FLAG); + } else { + document->requestModified(); + } + } +#ifdef OBJECT_TRACE + objectTrace( "SPObject::requestModified", false ); +#endif +} + +void SPObject::emitModified(unsigned int flags) +{ + /* only the MODIFIED_CASCADE flag is legal here */ + g_return_if_fail(!(flags & ~SP_OBJECT_MODIFIED_CASCADE)); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::emitModified", true, flags ); +#endif + +#ifdef SP_OBJECT_DEBUG_CASCADE + g_print("Modified %s:%s %x %x %x\n", g_type_name_from_instance((GTypeInstance *) this), getId(), flags, this->uflags, this->mflags); +#endif + + flags |= this->mflags; + /* We have to clear mflags beforehand, as signal handlers may + * make changes and therefore queue new modification notifications + * themselves. */ + this->mflags = 0; + + sp_object_ref(this); + + this->modified(flags); + + _modified_signal.emit(this, flags); + sp_object_unref(this); + +#ifdef OBJECT_TRACE + objectTrace( "SPObject::emitModified", false ); +#endif +} + +gchar const *SPObject::getTagName(SPException *ex) const +{ + g_assert(repr != nullptr); + /* If exception is not clear, return */ + if (!SP_EXCEPTION_IS_OK(ex)) { + return nullptr; + } + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + return getRepr()->name(); +} + +gchar const *SPObject::getAttribute(gchar const *key, SPException *ex) const +{ + g_assert(this->repr != nullptr); + /* If exception is not clear, return */ + if (!SP_EXCEPTION_IS_OK(ex)) { + return nullptr; + } + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + return (gchar const *) getRepr()->attribute(key); +} + +void SPObject::setAttribute(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value, SPException *ex) +{ + g_assert(this->repr != nullptr); + /* If exception is not clear, return */ + g_return_if_fail(SP_EXCEPTION_IS_OK(ex)); + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + getRepr()->setAttribute(key, value); +} + + +void SPObject::removeAttribute(gchar const *key, SPException *ex) +{ + /* If exception is not clear, return */ + g_return_if_fail(SP_EXCEPTION_IS_OK(ex)); + + /// \todo fixme: Exception if object is NULL? */ + //XML Tree being used here. + getRepr()->setAttribute(key, nullptr, false); +} + +bool SPObject::storeAsDouble( gchar const *key, double *val ) const +{ + g_assert(this->getRepr()!= nullptr); + return sp_repr_get_double(((Inkscape::XML::Node *)(this->getRepr())),key,val); +} + +/** Helper */ +gchar * +sp_object_get_unique_id(SPObject *object, + gchar const *id) +{ + static unsigned long count = 0; + + g_assert(SP_IS_OBJECT(object)); + + count++; + + //XML Tree being used here. + gchar const *name = object->getRepr()->name(); + g_assert(name != nullptr); + + gchar const *local = std::strchr(name, ':'); + if (local) { + name = local + 1; + } + + if (id != nullptr) { + if (object->document->getObjectById(id) == nullptr) { + return g_strdup(id); + } + } + + size_t const name_len = std::strlen(name); + size_t const buflen = name_len + (sizeof(count) * 10 / 4) + 1; + gchar *const buf = (gchar *) g_malloc(buflen); + std::memcpy(buf, name, name_len); + gchar *const count_buf = buf + name_len; + size_t const count_buflen = buflen - name_len; + do { + ++count; + g_snprintf(count_buf, count_buflen, "%lu", count); + } while ( object->document->getObjectById(buf) != nullptr ); + return buf; +} + +void SPObject::_requireSVGVersion(Inkscape::Version version) { + for ( SPObject::ParentIterator iter=this ; iter ; ++iter ) { + SPObject *object = iter; + if (SP_IS_ROOT(object)) { + SPRoot *root = SP_ROOT(object); + if ( root->version.svg < version ) { + root->version.svg = version; + } + } + } +} + +// Titles and descriptions + +/* Note: + Titles and descriptions are stored in 'title' and 'desc' child elements + (see section 5.4 of the SVG 1.0 and 1.1 specifications). The spec allows + an element to have more than one 'title' child element, but strongly + recommends against this and requires using the first one if a choice must + be made. The same applies to 'desc' elements. Therefore, these functions + ignore all but the first 'title' child element and first 'desc' child + element, except when deleting a title or description. + + This will change in SVG 2, where multiple 'title' and 'desc' elements will + be allowed with different localized strings. +*/ + +gchar * SPObject::title() const +{ + return getTitleOrDesc("svg:title"); +} + +bool SPObject::setTitle(gchar const *title, bool verbatim) +{ + return setTitleOrDesc(title, "svg:title", verbatim); +} + +gchar * SPObject::desc() const +{ + return getTitleOrDesc("svg:desc"); +} + +bool SPObject::setDesc(gchar const *desc, bool verbatim) +{ + return setTitleOrDesc(desc, "svg:desc", verbatim); +} + +char * SPObject::getTitleOrDesc(gchar const *svg_tagname) const +{ + char *result = nullptr; + SPObject *elem = findFirstChild(svg_tagname); + if ( elem ) { + //This string copy could be avoided by changing + //the return type of SPObject::getTitleOrDesc + //to std::unique_ptr + result = g_strdup(elem->textualContent().c_str()); + } + return result; +} + +bool SPObject::setTitleOrDesc(gchar const *value, gchar const *svg_tagname, bool verbatim) +{ + if (!verbatim) { + // If the new title/description is just whitespace, + // treat it as though it were NULL. + if (value) { + bool just_whitespace = true; + for (const gchar *cp = value; *cp; ++cp) { + if (!std::strchr("\r\n \t", *cp)) { + just_whitespace = false; + break; + } + } + if (just_whitespace) { + value = nullptr; + } + } + // Don't stomp on mark-up if there is no real change. + if (value) { + gchar *current_value = getTitleOrDesc(svg_tagname); + if (current_value) { + bool different = std::strcmp(current_value, value); + g_free(current_value); + if (!different) { + return false; + } + } + } + } + + SPObject *elem = findFirstChild(svg_tagname); + + if (value == nullptr) { + if (elem == nullptr) { + return false; + } + // delete the title/description(s) + while (elem) { + elem->deleteObject(); + elem = findFirstChild(svg_tagname); + } + return true; + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + if (elem == nullptr) { + // create a new 'title' or 'desc' element, putting it at the + // beginning (in accordance with the spec's recommendations) + Inkscape::XML::Node *xml_elem = xml_doc->createElement(svg_tagname); + repr->addChild(xml_elem, nullptr); + elem = document->getObjectByRepr(xml_elem); + Inkscape::GC::release(xml_elem); + } + else { + // remove the current content of the 'text' or 'desc' element + auto tmp = elem->children | boost::adaptors::transformed([](SPObject& obj) { return &obj; }); + std::vector vec(tmp.begin(), tmp.end()); + for (auto &child: vec) { + child->deleteObject(); + } + } + + // add the new content + elem->appendChildRepr(xml_doc->createTextNode(value)); + return true; +} + +SPObject* SPObject::findFirstChild(gchar const *tagname) const +{ + for (auto& child: const_cast(this)->children) + { + if (child.repr->type() == Inkscape::XML::ELEMENT_NODE && + !std::strcmp(child.repr->name(), tagname)) { + return &child; + } + } + return nullptr; +} + +Glib::ustring SPObject::textualContent() const +{ + Glib::ustring text; + + for (auto& child: children) + { + Inkscape::XML::NodeType child_type = child.repr->type(); + + if (child_type == Inkscape::XML::ELEMENT_NODE) { + text += child.textualContent(); + } + else if (child_type == Inkscape::XML::TEXT_NODE) { + text += child.repr->content(); + } + } + return text; +} + +// For debugging: Print SP tree structure. +void SPObject::recursivePrintTree( unsigned level ) +{ + if (level == 0) { + std::cout << "SP Object Tree" << std::endl; + } + std::cout << "SP: "; + for (unsigned i = 0; i < level; ++i) { + std::cout << " "; + } + std::cout << (getId()?getId():"No object id") + << " clone: " << std::boolalpha << (bool)cloned + << " hrefcount: " << hrefcount << std::endl; + for (auto& child: children) { + child.recursivePrintTree(level + 1); + } +} + +// Function to allow tracing of program flow through SPObject and derived classes. +// To trace function, add at entrance ('in' = true) and exit of function ('in' = false). +void SPObject::objectTrace( std::string text, bool in, unsigned flags ) { + if( in ) { + for (unsigned i = 0; i < indent_level; ++i) { + std::cout << " "; + } + std::cout << text << ":" + << " entrance: " + << (id?id:"null") + // << " uflags: " << uflags + // << " mflags: " << mflags + // << " flags: " << flags + << std::endl; + ++indent_level; + } else { + --indent_level; + for (unsigned i = 0; i < indent_level; ++i) { + std::cout << " "; + } + std::cout << text << ":" + << " exit: " + << (id?id:"null") + // << " uflags: " << uflags + // << " mflags: " << mflags + // << " flags: " << flags + << std::endl; + } +} + +std::ostream &operator<<(std::ostream &out, const SPObject &o) +{ + out << (o.getId()?o.getId():"No ID") + << " cloned: " << std::boolalpha << (bool)o.cloned + << " ref: " << o.refCount + << " href: " << o.hrefcount + << " total href: " << o._total_hrefcount; + return out; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-object.h b/src/object/sp-object.h new file mode 100644 index 0000000..100583b --- /dev/null +++ b/src/object/sp-object.h @@ -0,0 +1,879 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_OBJECT_H_SEEN +#define SP_OBJECT_H_SEEN + + +/* + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * Adrian Boguszewski + * + * Copyright (C) 1999-2016 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "util/const_char_ptr.h" +/* SPObject flags */ + +class SPObject; + +#define SP_OBJECT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_OBJECT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/* Async modification flags */ +#define SP_OBJECT_MODIFIED_FLAG (1 << 0) +#define SP_OBJECT_CHILD_MODIFIED_FLAG (1 << 1) +#define SP_OBJECT_PARENT_MODIFIED_FLAG (1 << 2) +#define SP_OBJECT_STYLE_MODIFIED_FLAG (1 << 3) +#define SP_OBJECT_VIEWPORT_MODIFIED_FLAG (1 << 4) +#define SP_OBJECT_USER_MODIFIED_FLAG_A (1 << 5) +#define SP_OBJECT_USER_MODIFIED_FLAG_B (1 << 6) +#define SP_OBJECT_USER_MODIFIED_FLAG_C (1 << 7) + +/* Convenience */ +#define SP_OBJECT_FLAGS_ALL 0xff + +/* Flags that mark object as modified */ +/* Object, Child, Style, Viewport, User */ +#define SP_OBJECT_MODIFIED_STATE (SP_OBJECT_FLAGS_ALL & ~(SP_OBJECT_PARENT_MODIFIED_FLAG)) + +/* Flags that will propagate downstreams */ +/* Parent, Style, Viewport, User */ +#define SP_OBJECT_MODIFIED_CASCADE (SP_OBJECT_FLAGS_ALL & ~(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) + +/* Write flags */ +#define SP_OBJECT_WRITE_BUILD (1 << 0) +#define SP_OBJECT_WRITE_EXT (1 << 1) +#define SP_OBJECT_WRITE_ALL (1 << 2) +#define SP_OBJECT_WRITE_NO_CHILDREN (1 << 3) + +#include +#include +#include +#include +#include +#include +#include +#include "version.h" +#include "util/forward-pointer-iterator.h" + +enum SPAttributeEnum : unsigned; + +class SPCSSAttr; +class SPStyle; + +namespace Inkscape { +namespace XML { +class Node; +struct Document; +} +} + +namespace Glib { + class ustring; +} + +enum SPExceptionType { + SP_NO_EXCEPTION, + SP_INDEX_SIZE_ERR, + SP_DOMSTRING_SIZE_ERR, + SP_HIERARCHY_REQUEST_ERR, + SP_WRONG_DOCUMENT_ERR, + SP_INVALID_CHARACTER_ERR, + SP_NO_DATA_ALLOWED_ERR, + SP_NO_MODIFICATION_ALLOWED_ERR, + SP_NOT_FOUND_ERR, + SP_NOT_SUPPORTED_ERR, + SP_INUSE_ATTRIBUTE_ERR, + SP_INVALID_STATE_ERR, + SP_SYNTAX_ERR, + SP_INVALID_MODIFICATION_ERR, + SP_NAMESPACE_ERR, + SP_INVALID_ACCESS_ERR +}; + +/// An attempt to implement exceptions, unused? +struct SPException { + SPExceptionType code; +}; + +#define SP_EXCEPTION_INIT(ex) {(ex)->code = SP_NO_EXCEPTION;} +#define SP_EXCEPTION_IS_OK(ex) (!(ex) || ((ex)->code == SP_NO_EXCEPTION)) + +/// Unused +struct SPCtx { + unsigned int flags; +}; + +enum { + SP_XML_SPACE_DEFAULT, + SP_XML_SPACE_PRESERVE +}; + +class SPDocument; + +/// Internal class consisting of two bits. +class SPIXmlSpace { +public: + SPIXmlSpace(): set(0), value(SP_XML_SPACE_DEFAULT) {}; + unsigned int set : 1; + unsigned int value : 1; +}; + +/* + * Refcounting + * + * Owner is here for debug reasons, you can set it to NULL safely + * Ref should return object, NULL is error, unref return always NULL + */ + +/** + * Increase reference count of object, with possible debugging. + * + * @param owner If non-NULL, make debug log entry. + * @return object, NULL is error. + * \pre object points to real object + * @todo need to move this to be a member of SPObject. + */ +SPObject *sp_object_ref(SPObject *object, SPObject *owner=nullptr); + +/** + * Decrease reference count of object, with possible debugging and + * finalization. + * + * @param owner If non-NULL, make debug log entry. + * @return always NULL + * \pre object points to real object + * @todo need to move this to be a member of SPObject. + */ +SPObject *sp_object_unref(SPObject *object, SPObject *owner=nullptr); + +/** + * SPObject is an abstract base class of all of the document nodes at the + * SVG document level. Each SPObject subclass implements a certain SVG + * element node type, or is an abstract base class for different node + * types. The SPObject layer is bound to the SPRepr layer, closely + * following the SPRepr mutations via callbacks. During creation, + * SPObject parses and interprets all textual attributes and CSS style + * strings of the SPRepr, and later updates the internal state whenever + * it receives a signal about a change. The opposite is not true - there + * are methods manipulating SPObjects directly and such changes do not + * propagate to the SPRepr layer. This is important for implementation of + * the undo stack, animations and other features. + * + * SPObjects are bound to the higher-level container SPDocument, which + * provides document level functionality such as the undo stack, + * dictionary and so on. Source: doc/architecture.txt + */ +class SPObject { +public: + enum CollectionPolicy { + COLLECT_WITH_PARENT, + ALWAYS_COLLECT + }; + + SPObject(); + virtual ~SPObject(); + + unsigned int cloned : 1; + SPObject *clone_original; + unsigned int uflags : 8; + unsigned int mflags : 8; + SPIXmlSpace xml_space; + Glib::ustring lang; + unsigned int hrefcount; /* number of xlink:href references */ + unsigned int _total_hrefcount; /* our hrefcount + total descendants */ + SPDocument *document; /* Document we are part of */ + SPObject *parent; /* Our parent (only one allowed) */ + +private: + SPObject(const SPObject&); + SPObject& operator=(const SPObject&); + + char *id; /* Our very own unique id */ + Inkscape::XML::Node *repr; /* Our xml representation */ + +public: + int refCount; + std::list hrefList; + + /** + * Returns the objects current ID string. + */ + char const* getId() const; + + /** + * Returns the XML representation of tree + */ +//protected: + Inkscape::XML::Node * getRepr(); + + /** + * Returns the XML representation of tree + */ + Inkscape::XML::Node const* getRepr() const; + +public: + + /** + * Cleans up an SPObject, releasing its references and + * requesting that references to it be released + */ + void releaseReferences(); + + /** + * Connects to the release request signal + * + * @param slot the slot to connect + * + * @return the sigc::connection formed + */ + sigc::connection connectRelease(sigc::slot slot) { + return _release_signal.connect(slot); + } + + /** + * Represents the style properties, whether from presentation attributes, the style + * attribute, or inherited. + * + * private_set() doesn't handle SP_ATTR_STYLE or any presentation attributes at the + * time of writing, so this is probably NULL for all SPObject's that aren't an SPItem. + * + * However, this gives rise to the bugs mentioned in sp_object_get_style_property. + * Note that some non-SPItem SPObject's, such as SPStop, do need styling information, + * and need to inherit properties even through other non-SPItem parents like \. + */ + SPStyle *style; + + /** + * Represents the style that should be used to resolve 'context-fill' and 'context-stroke' + */ + SPStyle *context_style; + + /// Switch containing next() method. + struct ParentIteratorStrategy { + static SPObject const *next(SPObject const *object) { + return object->parent; + } + }; + + typedef Inkscape::Util::ForwardPointerIterator ParentIterator; + typedef Inkscape::Util::ForwardPointerIterator ConstParentIterator; + + bool isSiblingOf(SPObject const *object) const { + if (object == nullptr) return false; + return this->parent && this->parent == object->parent; + } + + /** + * True if object is non-NULL and this is some in/direct parent of object. + */ + bool isAncestorOf(SPObject const *object) const; + + /** + * Returns youngest object being parent to this and object. + */ + SPObject const *nearestCommonAncestor(SPObject const *object) const; + + /* Returns next object in sibling list or NULL. */ + SPObject *getNext(); + + /** + * Returns previous object in sibling list or NULL. + */ + SPObject *getPrev(); + + bool hasChildren() const { return ( children.size() > 0 ); } + + SPObject *firstChild() { return children.empty() ? nullptr : &children.front(); } + SPObject const *firstChild() const { return children.empty() ? nullptr : &children.front(); } + + SPObject *lastChild() { return children.empty() ? nullptr : &children.back(); } + SPObject const *lastChild() const { return children.empty() ? nullptr : &children.back(); } + + SPObject *nthChild(unsigned index); + SPObject const *nthChild(unsigned index) const; + + enum Action { ActionGeneral, ActionBBox, ActionUpdate, ActionShow }; + + /** + * Retrieves the children as a std vector object, optionally ref'ing the children + * in the process, if add_ref is specified. + */ + std::vector childList(bool add_ref, Action action = ActionGeneral); + + /** + * Append repr as child of this object. + * \pre this is not a cloned object + */ + SPObject *appendChildRepr(Inkscape::XML::Node *repr); + + /** + * Gets the author-visible label property for the object or a default if + * no label is defined. + */ + char const *label() const; + + /** + * Returns a default label property for this object. + */ + char const *defaultLabel() const; + + /** + * Sets the author-visible label for this object. + * + * @param label the new label. + */ + void setLabel(char const *label); + + /** + * Returns the title of this object, or NULL if there is none. + * The caller must free the returned string using g_free() - see comment + * for getTitleOrDesc() below. + */ + char *title() const; + + /** + * Sets the title of this object. + * A NULL first argument is interpreted as meaning that the existing title + * (if any) should be deleted. + * The second argument is optional - @see setTitleOrDesc() below for details. + */ + bool setTitle(char const *title, bool verbatim = false); + + /** + * Returns the description of this object, or NULL if there is none. + * The caller must free the returned string using g_free() - see comment + * for getTitleOrDesc() below. + */ + char *desc() const; + + /** + * Sets the description of this object. + * A NULL first argument is interpreted as meaning that the existing + * description (if any) should be deleted. + * The second argument is optional - @see setTitleOrDesc() below for details. + */ + bool setDesc(char const *desc, bool verbatim=false); + + /** + * Set the policy under which this object will be orphan-collected. + * + * Orphan-collection is the process of deleting all objects which no longer have + * hyper-references pointing to them. The policy determines when this happens. Many objects + * should not be deleted simply because they are no longer referred to; other objects (like + * "intermediate" gradients) are more or less throw-away and should always be collected when no + * longer in use. + * + * Along these lines, there are currently two orphan-collection policies: + * + * COLLECT_WITH_PARENT - don't worry about the object's hrefcount; + * if its parent is collected, this object + * will be too + * + * COLLECT_ALWAYS - always collect the object as soon as its + * hrefcount reaches zero + * + * @return the current collection policy in effect for this object + */ + CollectionPolicy collectionPolicy() const { return _collection_policy; } + + /** + * Sets the orphan-collection policy in effect for this object. + * + * @param policy the new policy to adopt + * + * @see SPObject::collectionPolicy + */ + void setCollectionPolicy(CollectionPolicy policy) { + _collection_policy = policy; + } + + /** + * Requests a later automatic call to collectOrphan(). + * + * This method requests that collectOrphan() be called during the document update cycle, + * deleting the object if it is no longer used. + * + * If the current collection policy is COLLECT_WITH_PARENT, this function has no effect. + * + * @see SPObject::collectOrphan + */ + void requestOrphanCollection(); + + /** + * Unconditionally delete the object if it is not referenced. + * + * Unconditionally delete the object if there are no outstanding hyper-references to it. + * Observers are not notified of the object's deletion (at the SPObject level; XML tree + * notifications still fire). + * + * @see SPObject::deleteObject + */ + void collectOrphan() { + if ( _total_hrefcount == 0 ) { + deleteObject(false); + } + } + + /** + * Increase weak refcount. + * + * Hrefcount is used for weak references, for example, to + * determine whether any graphical element references a certain gradient + * node. + * It keeps a list of "owners". + * @param owner Used to track who uses this object. + */ + void hrefObject(SPObject* owner = nullptr); + + /** + * Decrease weak refcount. + * + * Hrefcount is used for weak references, for example, to determine whether + * any graphical element references a certain gradient node. + * @param owner Used to track who uses this object. + * \pre hrefcount>0 + */ + void unhrefObject(SPObject* owner = nullptr); + + /** + * Check if object is referenced by any other object. + */ + bool isReferenced() { return ( _total_hrefcount > 0 ); } + + /** + * Deletes an object, unparenting it from its parent. + * + * Detaches the object's repr, and optionally sends notification that the object has been + * deleted. + * + * @param propagate If it is set to true, it emits a delete signal. + * + * @param propagate_descendants If it is true, it recursively sends the delete signal to children. + */ + void deleteObject(bool propagate, bool propagate_descendants); + + /** + * Deletes on object. + * + * @param propagate Notify observers of this object and its children that they have been + * deleted? + */ + void deleteObject(bool propagate = true) + { + deleteObject(propagate, propagate); + } + + /** + * Removes all children except for the given object, it's children and it's ancesstors. + */ + void cropToObject(SPObject *except); + + /** + * Connects a slot to be called when an object is deleted. + * + * This connects a slot to an object's internal delete signal, which is invoked when the object + * is deleted + * + * The signal is mainly useful for e.g. knowing when to break hrefs or dissociate clones. + * + * @param slot the slot to connect + * + * @see SPObject::deleteObject + */ + sigc::connection connectDelete(sigc::slot slot) { + return _delete_signal.connect(slot); + } + + sigc::connection connectPositionChanged(sigc::slot slot) { + return _position_changed_signal.connect(slot); + } + + /** + * Returns the object which supercedes this one (if any). + * + * This is mainly useful for ensuring we can correctly perform a series of moves or deletes, + * even if the objects in question have been replaced in the middle of the sequence. + */ + SPObject *successor() { return _successor; } + + /** + * Indicates that another object supercedes this one. + */ + void setSuccessor(SPObject *successor) { + assert(successor != NULL); + assert(_successor == NULL); + assert(successor->_successor == NULL); + sp_object_ref(successor, nullptr); + _successor = successor; + } + + /* modifications; all three sets of methods should probably ultimately be protected, as they + * are not really part of its public interface. However, other parts of the code to + * occasionally use them at present. */ + + /* the no-argument version of updateRepr() is intended to be a bit more public, however -- it + * essentially just flushes any changes back to the backing store (the repr layer); maybe it + * should be called something else and made public at that point. */ + + /** + * Updates the object's repr based on the object's state. + * + * This method updates the repr attached to the object to reflect the object's current + * state; see the three-argument version for details. + * + * @param flags object write flags that apply to this update + * + * @return the updated repr + */ + Inkscape::XML::Node *updateRepr(unsigned int flags = SP_OBJECT_WRITE_EXT); + + /** + * Updates the given repr based on the object's state. + * + * Used both to create reprs in the original document, and to create reprs + * in another document (e.g. a temporary document used when saving as "Plain SVG". + * + * This method updates the given repr to reflect the object's current state. There are + * several flags that affect this: + * + * SP_OBJECT_WRITE_BUILD - create new reprs + * + * SP_OBJECT_WRITE_EXT - write elements and attributes + * which are not part of pure SVG + * (i.e. the Inkscape and Sodipodi + * namespaces) + * + * SP_OBJECT_WRITE_ALL - create all nodes and attributes, + * even those which might be redundant + * + * @param repr the repr to update + * @param flags object write flags that apply to this update + * + * @return the updated repr + */ + Inkscape::XML::Node *updateRepr(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, unsigned int flags); + + /** + * Queues an deferred update of this object's display. + * + * This method sets flags to indicate updates to be performed later, during the idle loop. + * + * There are several flags permitted here: + * + * SP_OBJECT_MODIFIED_FLAG - the object has been modified + * + * SP_OBJECT_CHILD_MODIFIED_FLAG - a child of the object has been + * modified + * + * SP_OBJECT_STYLE_MODIFIED_FLAG - the object's style has been + * modified + * + * There are also some subclass-specific modified flags which are hardly ever used. + * + * One of either MODIFIED or CHILD_MODIFIED is required. + * + * @param flags flags indicating what to update + */ + void requestDisplayUpdate(unsigned int flags); + + /** + * Updates the object's display immediately + * + * This method is called during the idle loop by SPDocument in order to update the object's + * display. + * + * One additional flag is legal here: + * + * SP_OBJECT_PARENT_MODIFIED_FLAG - the parent has been + * modified + * + * @param ctx an SPCtx which accumulates various state + * during the recursive update -- beware! some + * subclasses try to cast this to an SPItemCtx * + * + * @param flags flags indicating what to update (in addition + * to any already set flags) + */ + void updateDisplay(SPCtx *ctx, unsigned int flags); + + /** + * Requests that a modification notification signal + * be emitted later (e.g. during the idle loop) + * + * Request modified always bubbles *up* the tree, as opposed to + * request display update, which trickles down and relies on the + * flags set during this pass... + * + * @param flags flags indicating what has been modified + */ + void requestModified(unsigned int flags); + + /** + * Emits the MODIFIED signal with the object's flags. + * The object's mflags are the original set aside during the update pass for + * later delivery here. Once emitModified() is called, those flags don't + * need to be stored any longer. + * + * @param flags indicating what has been modified. + */ + void emitModified(unsigned int flags); + + /** + * Connects to the modification notification signal + * + * @param slot the slot to connect + * + * @return the connection formed thereby + */ + sigc::connection connectModified( + sigc::slot slot + ) { + return _modified_signal.connect(slot); + } + + /** Sends the delete signal to all children of this object recursively */ + void _sendDeleteSignalRecursive(); + + /** + * Adds increment to _total_hrefcount of object and its parents. + */ + void _updateTotalHRefCount(int increment); + + void _requireSVGVersion(unsigned major, unsigned minor) { + _requireSVGVersion(Inkscape::Version(major, minor)); + } + + /** + * Lifts SVG version of all root objects to version. + */ + void _requireSVGVersion(Inkscape::Version version); + + sigc::signal _release_signal; + sigc::signal _delete_signal; + sigc::signal _position_changed_signal; + sigc::signal _modified_signal; + SPObject *_successor; + CollectionPolicy _collection_policy; + char *_label; + mutable char *_default_label; + + // WARNING: + // Methods below should not be used outside of the SP tree, + // as they operate directly on the XML representation. + // In future, they will be made protected. + + /** + * Put object into object tree, under parent, and behind prev; + * also update object's XML space. + */ + void attach(SPObject *object, SPObject *prev); + + /** + * In list of object's children, move object behind prev. + */ + void reorder(SPObject* obj, SPObject *prev); + + /** + * Remove object from parent's children, release and unref it. + */ + void detach(SPObject *object); + + /** + * Return object's child whose node pointer equals repr. + */ + SPObject *get_child_by_repr(Inkscape::XML::Node *repr); + + void invoke_build(SPDocument *document, Inkscape::XML::Node *repr, unsigned int cloned); + + int getIntAttribute(char const *key, int def); + + unsigned getPosition(); + + char const * getAttribute(char const *name,SPException *ex=nullptr) const; + + void appendChild(Inkscape::XML::Node *child); + + void addChild(Inkscape::XML::Node *child,Inkscape::XML::Node *prev=nullptr); + + /** + * Call virtual set() function of object. + */ + void setKeyValue(SPAttributeEnum key, char const *value); + + + void setAttribute(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value, + SPException *ex=nullptr); + + void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value, + SPException *ex=nullptr) { + this->setAttribute(key.data(), + (value.data() == nullptr || value.data()[0]=='\0') ? nullptr : value.data(), ex); + } + + /** + * Read value of key attribute from XML node into object. + */ + void readAttr(char const *key); + + char const *getTagName(SPException *ex) const; + + void removeAttribute(char const *key, SPException *ex=nullptr); + + void setCSS(SPCSSAttr *css, char const *attr); + + void changeCSS(SPCSSAttr *css, char const *attr); + + bool storeAsDouble( char const *key, double *val ) const; + +private: + // Private member functions used in the definitions of setTitle(), + // setDesc(), title() and desc(). + + /** + * Sets or deletes the title or description of this object. + * A NULL 'value' argument causes the title or description to be deleted. + * + * 'verbatim' parameter: + * If verbatim==true, then the title or description is set to exactly the + * specified value. If verbatim==false then two exceptions are made: + * (1) If the specified value is just whitespace, then the title/description + * is deleted. + * (2) If the specified value is the same as the current value except for + * mark-up, then the current value is left unchanged. + * This is usually the desired behaviour, so 'verbatim' defaults to false for + * setTitle() and setDesc(). + * + * The return value is true if a change was made to the title/description, + * and usually false otherwise. + */ + bool setTitleOrDesc(char const *value, char const *svg_tagname, bool verbatim); + + /** + * Returns the title or description of this object, or NULL if there is none. + * + * The SVG spec allows 'title' and 'desc' elements to contain text marked up + * using elements from other namespaces. Therefore, this function cannot + * in general just return a pointer to an existing string - it must instead + * construct a string containing the title or description without the mark-up. + * Consequently, the return value is a newly allocated string (or NULL), and + * must be freed (using g_free()) by the caller. + */ + char * getTitleOrDesc(char const *svg_tagname) const; + + /** + * Find the first child of this object with a given tag name, + * and return it. Returns NULL if there is no matching child. + */ + SPObject * findFirstChild(char const *tagname) const; + + /** + * Return the full textual content of an element (typically all the + * content except the tags). + * Must not be used on anything except elements. + */ + Glib::ustring textualContent() const; + + /* Real handlers of repr signals */ + +public: + /** + * Callback for attr_changed node event. + */ + static void repr_attr_changed(Inkscape::XML::Node *repr, char const *key, char const *oldval, char const *newval, bool is_interactive, void* data); + + /** + * Callback for content_changed node event. + */ + static void repr_content_changed(Inkscape::XML::Node *repr, char const *oldcontent, char const *newcontent, void* data); + + /** + * Callback for child_added node event. + */ + static void repr_child_added(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void* data); + + /** + * Callback for remove_child node event. + */ + static void repr_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void *data); + + /** + * Callback for order_changed node event. + * + * \todo fixme: + */ + static void repr_order_changed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *old, Inkscape::XML::Node *newer, void* data); + + + friend class SPObjectImpl; + +protected: + virtual void build(SPDocument* doc, Inkscape::XML::Node* repr); + virtual void release(); + + virtual void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref); + virtual void remove_child(Inkscape::XML::Node* child); + + virtual void order_changed(Inkscape::XML::Node* child, Inkscape::XML::Node* old_repr, Inkscape::XML::Node* new_repr); + + virtual void set(SPAttributeEnum key, const char* value); + + virtual void update(SPCtx* ctx, unsigned int flags); + virtual void modified(unsigned int flags); + + virtual Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags); + + typedef boost::intrusive::list_member_hook<> ListHook; + ListHook _child_hook; +public: + typedef boost::intrusive::list< + SPObject, + boost::intrusive::member_hook< + SPObject, + ListHook, + &SPObject::_child_hook + >> ChildrenList; + ChildrenList children; + virtual void read_content(); + + void recursivePrintTree(unsigned level = 0); // For debugging + static unsigned indent_level; + void objectTrace( std::string, bool in=true, unsigned flags=0 ); +}; + +std::ostream &operator<<(std::ostream &out, const SPObject &o); + +/** + * Compares height of objects in tree. + * + * Works for different-parent objects, so long as they have a common ancestor. + * \return \verbatim + * 0 positions are equivalent + * 1 first object's position is greater than the second + * -1 first object's position is less than the second \endverbatim + */ +int sp_object_compare_position(SPObject const *first, SPObject const *second); +bool sp_object_compare_position_bool(SPObject const *first, SPObject const *second); +gchar * sp_object_get_unique_id(SPObject *object, gchar const *defid); + +#endif // SP_OBJECT_H_SEEN + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-offset.cpp b/src/object/sp-offset.cpp new file mode 100644 index 0000000..2ae1578 --- /dev/null +++ b/src/object/sp-offset.cpp @@ -0,0 +1,1221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Implementation of . + */ + +/* + * Authors: (of the sp-spiral.c upon which this file was constructed): + * Mitsuru Oka + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-offset.h" + +#include +#include + +#include + +#include "bad-uri-exception.h" +#include "svg/svg.h" +#include "attributes.h" +#include "display/curve.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "enums.h" +#include "preferences.h" +#include "sp-text.h" +#include "sp-use-reference.h" +#include "uri.h" + +class SPDocument; + +#define noOFFSET_VERBOSE + +/** \note + * SPOffset is a derivative of SPShape, much like the SPSpiral or SPRect. + * The goal is to have a source shape (= originalPath), an offset (= radius) + * and compute the offset of the source by the radius. To get it to work, + * one needs to know what the source is and what the radius is, and how it's + * stored in the xml representation. The object itself is a "path" element, + * to get lots of shape functionality for free. The source is the easy part: + * it's stored in a "inkscape:original" attribute in the path. In case of + * "linked" offset, as they've been dubbed, there is an additional + * "inkscape:href" that contains the id of an element of the svg. + * When built, the object will attach a listener vector to that object and + * rebuild the "inkscape:original" whenever the href'd object changes. This + * is of course grossly inefficient, and also does not react to changes + * to the href'd during context stuff (like changing the shape of a star by + * dragging control points) unless the path of that object is changed during + * the context (seems to be the case for SPEllipse). The computation of the + * offset is done in sp_offset_set_shape(), a function that is called whenever + * a change occurs to the offset (change of source or change of radius). + * just like the sp-star and other, this path derivative can make control + * points, or more precisely one control point, that's enough to define the + * radius (look in shape-editor-knotholders). + */ + +static void refresh_offset_source(SPOffset* offset); + +static void sp_offset_start_listening(SPOffset *offset,SPObject* to); +static void sp_offset_quit_listening(SPOffset *offset); +static void sp_offset_href_changed(SPObject *old_ref, SPObject *ref, SPOffset *offset); +static void sp_offset_move_compensate(Geom::Affine const *mp, SPItem *original, SPOffset *self); +static void sp_offset_delete_self(SPObject *deleted, SPOffset *self); +static void sp_offset_source_modified (SPObject *iSource, guint flags, SPItem *item); + + +// slow= source path->polygon->offset of polygon->polygon->path +// fast= source path->offset of source path->polygon->path +// fast is not mathematically correct, because computing the offset of a single +// cubic bezier patch is not trivial; in particular, there are problems with holes +// reappearing in offset when the radius becomes too large +//TODO: need fix for bug: #384688 with fix released in r.14156 +//but reverted because bug #1507049 seems has more priority. +static bool use_slow_but_correct_offset_method = false; + +SPOffset::SPOffset() : SPShape() { + this->rad = 1.0; + this->original = nullptr; + this->originalPath = nullptr; + this->knotSet = false; + this->sourceDirty=false; + this->isUpdating=false; + // init various connections + this->sourceHref = nullptr; + this->sourceRepr = nullptr; + this->sourceObject = nullptr; + + // set up the uri reference + this->sourceRef = new SPUseReference(this); + this->_changed_connection = this->sourceRef->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_offset_href_changed), this)); +} + +SPOffset::~SPOffset() { + delete this->sourceRef; + + this->_modified_connection.disconnect(); + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + this->_transformed_connection.disconnect(); +} + +void SPOffset::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPShape::build(document, repr); + + //XML Tree being used directly here while it shouldn't be. + if (this->getRepr()->attribute("inkscape:radius")) { + this->readAttr( "inkscape:radius" ); + } else { + //XML Tree being used directly here (as object->getRepr) + //in all the below lines in the block while it shouldn't be. + gchar const *oldA = this->getRepr()->attribute("sodipodi:radius"); + this->setAttribute("inkscape:radius", oldA); + this->removeAttribute("sodipodi:radius"); + + this->readAttr( "inkscape:radius" ); + } + + if (this->getRepr()->attribute("inkscape:original")) { + this->readAttr( "inkscape:original" ); + } else { + gchar const *oldA = this->getRepr()->attribute("sodipodi:original"); + this->setAttribute("inkscape:original", oldA); + this->removeAttribute("sodipodi:original"); + + this->readAttr( "inkscape:original" ); + } + + if (this->getRepr()->attribute("xlink:href")) { + this->readAttr( "xlink:href" ); + } else { + gchar const *oldA = this->getRepr()->attribute("inkscape:href"); + + if (oldA) { + size_t lA = strlen(oldA); + char *nA=(char*)malloc((1+lA+1)*sizeof(char)); + + memcpy(nA+1,oldA,lA*sizeof(char)); + + nA[0]='#'; + nA[lA+1]=0; + + this->setAttribute("xlink:href", nA); + + free(nA); + + this->removeAttribute("inkscape:href"); + } + + this->readAttr( "xlink:href" ); + } +} + +Inkscape::XML::Node* SPOffset::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + /** \todo + * Fixme: we may replace these attributes by + * inkscape:offset="cx cy exp revo rad arg t0" + */ + repr->setAttribute("sodipodi:type", "inkscape:offset"); + sp_repr_set_svg_double(repr, "inkscape:radius", this->rad); + repr->setAttribute("inkscape:original", this->original); + repr->setAttribute("inkscape:href", this->sourceHref); + } + + + // Make sure the offset has curve + SPCurve *curve = SP_SHAPE (this)->getCurve(); + + if (curve == nullptr) { + this->set_shape(); + } + + // write that curve to "d" + char *d = sp_svg_write_path (this->_curve->get_pathvector()); + repr->setAttribute("d", d); + g_free (d); + + SPShape::write(xml_doc, repr, flags | SP_SHAPE_WRITE_PATH); + + return repr; +} + +void SPOffset::release() { + if (this->original) { + free (this->original); + } + + if (this->originalPath) { + delete ((Path *) this->originalPath); + } + + this->original = nullptr; + this->originalPath = nullptr; + + sp_offset_quit_listening(this); + + this->_changed_connection.disconnect(); + + g_free(this->sourceHref); + + this->sourceHref = nullptr; + this->sourceRef->detach(); + + SPShape::release(); +} + +void SPOffset::set(SPAttributeEnum key, const gchar* value) { + if ( this->sourceDirty ) { + refresh_offset_source(this); + } + + /* fixme: we should really collect updates */ + switch (key) + { + case SP_ATTR_INKSCAPE_ORIGINAL: + case SP_ATTR_SODIPODI_ORIGINAL: + if (value == nullptr) { + } else { + if (this->original) { + free (this->original); + delete ((Path *) this->originalPath); + + this->original = nullptr; + this->originalPath = nullptr; + } + + this->original = strdup (value); + + Geom::PathVector pv = sp_svg_read_pathv(this->original); + + this->originalPath = new Path; + reinterpret_cast(this->originalPath)->LoadPathVector(pv); + + this->knotSet = false; + + if ( this->isUpdating == false ) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + break; + + case SP_ATTR_INKSCAPE_RADIUS: + case SP_ATTR_SODIPODI_RADIUS: + if (!sp_svg_length_read_computed_absolute (value, &this->rad)) { + if (fabs (this->rad) < 0.01) { + this->rad = (this->rad < 0) ? -0.01 : 0.01; + } + + this->knotSet = false; // knotset=false because it's not set from the context + } + + if ( this->isUpdating == false ) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + break; + + case SP_ATTR_INKSCAPE_HREF: + case SP_ATTR_XLINK_HREF: + if ( value == nullptr ) { + sp_offset_quit_listening(this); + if ( this->sourceHref ) { + g_free(this->sourceHref); + } + + this->sourceHref = nullptr; + this->sourceRef->detach(); + } else { + if ( this->sourceHref && ( strcmp(value, this->sourceHref) == 0 ) ) { + } else { + if ( this->sourceHref ) { + g_free(this->sourceHref); + } + + this->sourceHref = g_strdup(value); + + try { + this->sourceRef->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->sourceRef->detach(); + } + } + } + break; + + default: + SPShape::set(key, value); + break; + } +} + +void SPOffset::update(SPCtx *ctx, guint flags) { + this->isUpdating=true; // prevent sp_offset_set from requesting updates + + if ( this->sourceDirty ) { + refresh_offset_source(this); + } + + if (flags & + (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + this->set_shape(); + } + + this->isUpdating=false; + + SPShape::update(ctx, flags); +} + +const char* SPOffset::displayName() const { + if ( this->sourceHref ) { + return _("Linked Offset"); + } else { + return _("Dynamic Offset"); + } +} + +gchar* SPOffset::description() const { + // TRANSLATORS COMMENT: %s is either "outset" or "inset" depending on sign + return g_strdup_printf(_("%s by %f pt"), (this->rad >= 0) ? + _("outset") : _("inset"), fabs (this->rad)); +} + +void SPOffset::set_shape() { + if ( this->originalPath == nullptr ) { + // oops : no path?! (the offset object should do harakiri) + return; + } +#ifdef OFFSET_VERBOSE + g_print ("rad=%g\n", offset->rad); +#endif + // au boulot + + if ( fabs(this->rad) < 0.01 ) { + // grosso modo: 0 + // just put the source of this (almost-non-offsetted) object as being the actual offset, + // no one will notice. it's also useless to compute the offset with a 0 radius + + //XML Tree being used directly here while it shouldn't be. + const char *res_d = this->getRepr()->attribute("inkscape:original"); + + if ( res_d ) { + Geom::PathVector pv = sp_svg_read_pathv(res_d); + SPCurve *c = new SPCurve(pv); + g_assert(c != nullptr); + + this->setCurveInsync (c); + this->setCurveBeforeLPE(c); + + c->unref(); + } + + return; + } + + // extra paranoiac careful check. the preceding if () should take care of this case + if (fabs (this->rad) < 0.01) { + this->rad = (this->rad < 0) ? -0.01 : 0.01; + } + + Path *orig = new Path; + orig->Copy ((Path *)this->originalPath); + + if ( use_slow_but_correct_offset_method == false ) { + // version par outline + Shape *theShape = new Shape; + Shape *theRes = new Shape; + Path *originaux[1]; + Path *res = new Path; + res->SetBackData (false); + + // and now: offset + float o_width; + if (this->rad >= 0) + { + o_width = this->rad; + orig->OutsideOutline (res, o_width, join_round, butt_straight, 20.0); + } + else + { + o_width = -this->rad; + orig->OutsideOutline (res, -o_width, join_round, butt_straight, 20.0); + } + + if (o_width >= 1.0) + { + // res->ConvertForOffset (1.0, orig, offset->rad); + res->ConvertWithBackData (1.0); + } + else + { + // res->ConvertForOffset (o_width, orig, offset->rad); + res->ConvertWithBackData (o_width); + } + res->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_positive); + originaux[0] = res; + + theRes->ConvertToForme (orig, 1, originaux); + + Geom::OptRect bbox = this->documentVisualBounds(); + + if ( bbox ) { + gdouble size = L2(bbox->dimensions()); + gdouble const exp = this->transform.descrim(); + + if (exp != 0) { + size /= exp; + } + + orig->Coalesce (size * 0.001); + //g_print ("coa %g exp %g item %p\n", size * 0.001, exp, item); + } + + + // if (o_width >= 1.0) + // { + // orig->Coalesce (0.1); // small treshhold, since we only want to get rid of small segments + // the curve should already be computed by the Outline() function + // orig->ConvertEvenLines (1.0); + // orig->Simplify (0.5); + // } + // else + // { + // orig->Coalesce (0.1*o_width); + // orig->ConvertEvenLines (o_width); + // orig->Simplify (0.5 * o_width); + // } + + delete theShape; + delete theRes; + delete res; + } else { + // version par makeoffset + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + + // and now: offset + float o_width; + if (this->rad >= 0) + { + o_width = this->rad; + } + else + { + o_width = -this->rad; + } + + // one has to have a measure of the details + if (o_width >= 1.0) + { + orig->ConvertWithBackData (0.5); + } + else + { + orig->ConvertWithBackData (0.5*o_width); + } + + orig->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_positive); + + Path *originaux[1]; + originaux[0]=orig; + + Path *res = new Path; + theRes->ConvertToForme (res, 1, originaux); + + int nbPart=0; + Path** parts=res->SubPaths(nbPart,true); + char *holes=(char*)malloc(nbPart*sizeof(char)); + + // we offset contours separately, because we can. + // this way, we avoid doing a unique big ConvertToShape when dealing with big shapes with lots of holes + { + Shape* onePart=new Shape; + Shape* oneCleanPart=new Shape; + + theShape->Reset(); + + for (int i=0;iSurface(); + parts[i]->Convert(1.0); + + { + // raffiner si besoin + double bL,bT,bR,bB; + parts[i]->PolylineBoundingBox(bL,bT,bR,bB); + double measure=((bR-bL)+(bB-bT))*0.5; + if ( measure < 10.0 ) { + parts[i]->Convert(0.02*measure); + } + } + + if ( partSurf < 0 ) { // inverse par rapport a la realite + // plein + holes[i]=0; + parts[i]->Fill(oneCleanPart,0); + onePart->ConvertToShape(oneCleanPart,fill_positive); // there aren't intersections in that one, but maybe duplicate points and null edges + oneCleanPart->MakeOffset(onePart,this->rad,join_round,20.0); + onePart->ConvertToShape(oneCleanPart,fill_positive); + + onePart->CalcBBox(); + double typicalSize=0.5*((onePart->rightX-onePart->leftX)+(onePart->bottomY-onePart->topY)); + + if ( typicalSize < 0.05 ) { + typicalSize=0.05; + } + + typicalSize*=0.01; + + if ( typicalSize > 1.0 ) { + typicalSize=1.0; + } + + onePart->ConvertToForme (parts[i]); + parts[i]->ConvertEvenLines (typicalSize); + parts[i]->Simplify (typicalSize); + + double nPartSurf=parts[i]->Surface(); + + if ( nPartSurf >= 0 ) { + // inversion de la surface -> disparait + delete parts[i]; + parts[i]=nullptr; + } else { + } + +/* int firstP=theShape->nbPt; + for (int j=0;jnbPt;j++) theShape->AddPoint(onePart->pts[j].x); + for (int j=0;jnbAr;j++) theShape->AddEdge(firstP+onePart->aretes[j].st,firstP+onePart->aretes[j].en);*/ + } else { + // trou + holes[i]=1; + parts[i]->Fill(oneCleanPart,0,false,true,true); + onePart->ConvertToShape(oneCleanPart,fill_positive); + oneCleanPart->MakeOffset(onePart,-this->rad,join_round,20.0); + onePart->ConvertToShape(oneCleanPart,fill_positive); +// for (int j=0;jnbAr;j++) onePart->Inverse(j); // pas oublier de reinverser + + onePart->CalcBBox(); + double typicalSize=0.5*((onePart->rightX-onePart->leftX)+(onePart->bottomY-onePart->topY)); + + if ( typicalSize < 0.05 ) { + typicalSize=0.05; + } + + typicalSize*=0.01; + + if ( typicalSize > 1.0 ) { + typicalSize=1.0; + } + + onePart->ConvertToForme (parts[i]); + parts[i]->ConvertEvenLines (typicalSize); + parts[i]->Simplify (typicalSize); + double nPartSurf=parts[i]->Surface(); + + if ( nPartSurf >= 0 ) { + // inversion de la surface -> disparait + delete parts[i]; + parts[i]=nullptr; + } else { + } + + /* int firstP=theShape->nbPt; + for (int j=0;jnbPt;j++) theShape->AddPoint(onePart->pts[j].x); + for (int j=0;jnbAr;j++) theShape->AddEdge(firstP+onePart->aretes[j].en,firstP+onePart->aretes[j].st);*/ + } +// delete parts[i]; + } +// theShape->MakeOffset(theRes,offset->rad,join_round,20.0); + delete onePart; + delete oneCleanPart; + } + + if ( nbPart > 1 ) { + theShape->Reset(); + + for (int i=0;iConvertWithBackData(1.0); + + if ( holes[i] ) { + parts[i]->Fill(theShape,i,true,true,true); + } else { + parts[i]->Fill(theShape,i,true,true,false); + } + } + } + + theRes->ConvertToShape (theShape, fill_positive); + theRes->ConvertToForme (orig,nbPart,parts); + + for (int i=0;iCopy(parts[0]); + + for (int i=0;iReset(); + } +// theRes->ConvertToShape (theShape, fill_positive); +// theRes->ConvertToForme (orig); + +/* if (o_width >= 1.0) { + orig->ConvertEvenLines (1.0); + orig->Simplify (1.0); + } else { + orig->ConvertEvenLines (1.0*o_width); + orig->Simplify (1.0 * o_width); + }*/ + + if ( parts ) { + free(parts); + } + + if ( holes ) { + free(holes); + } + + delete res; + delete theShape; + delete theRes; + } + { + char *res_d = nullptr; + + if (orig->descr_cmd.size() <= 1) + { + // Aie.... nothing left. + res_d = strdup ("M 0 0 L 0 0 z"); + //printf("%s\n",res_d); + } + else + { + + res_d = orig->svg_dump_path (); + } + + delete orig; + + Geom::PathVector pv = sp_svg_read_pathv(res_d); + SPCurve *c = new SPCurve(pv); + g_assert(c != nullptr); + + this->setCurveInsync (c); + this->setCurveBeforeLPE(c); + c->unref(); + + free (res_d); + } +} + +void SPOffset::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + SPShape::snappoints(p, snapprefs); +} + + +// utilitaires pour les poignees +// used to get the distance to the shape: distance to polygon give the fabs(radius), we still need +// the sign. for edges, it's easy to determine which side the point is on, for points of the polygon +// it's trickier: we need to identify which angle the point is in; to that effect, we take each +// successive clockwise angle (A,C) and check if the vector B given by the point is in the angle or +// outside. +// another method would be to use the Winding() function to test whether the point is inside or outside +// the polygon (it would be wiser to do so, in fact, but i like being stupid) + +/** + * + * \todo + * FIXME: This can be done using linear operations, more stably and + * faster. method: transform A and C into B's space, A should be + * negative and B should be positive in the orthogonal component. I + * think this is equivalent to + * dot(A, rot90(B))*dot(C, rot90(B)) == -1. + * -- njh + */ +static bool +vectors_are_clockwise (Geom::Point A, Geom::Point B, Geom::Point C) +{ + using Geom::rot90; + double ab_s = dot(A, rot90(B)); + double ab_c = dot(A, B); + double bc_s = dot(B, rot90(C)); + double bc_c = dot(B, C); + double ca_s = dot(C, rot90(A)); + double ca_c = dot(C, A); + + double ab_a = acos (ab_c); + + if (ab_c <= -1.0) { + ab_a = M_PI; + } + + if (ab_c >= 1.0) { + ab_a = 0; + } + + if (ab_s < 0) { + ab_a = 2 * M_PI - ab_a; + } + + double bc_a = acos (bc_c); + + if (bc_c <= -1.0) { + bc_a = M_PI; + } + + if (bc_c >= 1.0) { + bc_a = 0; + } + + if (bc_s < 0) { + bc_a = 2 * M_PI - bc_a; + } + + double ca_a = acos (ca_c); + + if (ca_c <= -1.0) { + ca_a = M_PI; + } + + if (ca_c >= 1.0) { + ca_a = 0; + } + + if (ca_s < 0) { + ca_a = 2 * M_PI - ca_a; + } + + double lim = 2 * M_PI - ca_a; + + if (ab_a < lim) { + return true; + } + + return false; +} + +/** + * Distance to the original path; that function is called from shape-editor-knotholders + * to set the radius when the control knot moves. + * + * The sign of the result is the radius we're going to offset the shape with, + * so result > 0 ==outset and result < 0 ==inset. thus result<0 means + * 'px inside source'. + */ +double +sp_offset_distance_to_original (SPOffset * offset, Geom::Point px) +{ + if (offset == nullptr || offset->originalPath == nullptr || ((Path *) offset->originalPath)->descr_cmd.size() <= 1) { + return 1.0; + } + + double dist = 1.0; + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + /** \todo + * Awfully damn stupid method: uncross the source path EACH TIME you + * need to compute the distance. The good way to do this would be to + * store the uncrossed source path somewhere, and delete it when the + * context is finished. Hopefully this part is much faster than actually + * computing the offset (which happen just after), so the time spent in + * this function should end up being negligible with respect to the + * delay of one context. + */ + // move + ((Path *) offset->originalPath)->Convert (1.0); + ((Path *) offset->originalPath)->Fill (theShape, 0); + theRes->ConvertToShape (theShape, fill_oddEven); + + if (theRes->numberOfEdges() <= 1) + { + + } + else + { + double ptDist = -1.0; + bool ptSet = false; + double arDist = -1.0; + bool arSet = false; + + // first get the minimum distance to the points + for (int i = 0; i < theRes->numberOfPoints(); i++) + { + if (theRes->getPoint(i).totalDegree() > 0) + { + Geom::Point nx = theRes->getPoint(i).x; + Geom::Point nxpx = px-nx; + double ndist = sqrt (dot(nxpx,nxpx)); + + if (ptSet == false || fabs (ndist) < fabs (ptDist)) + { + // we have a new minimum distance + // now we need to wheck if px is inside or outside (for the sign) + nx = px - theRes->getPoint(i).x; + double nlen = sqrt (dot(nx , nx)); + nx /= nlen; + int pb, cb, fb; + fb = theRes->getPoint(i).incidentEdge[LAST]; + pb = theRes->getPoint(i).incidentEdge[LAST]; + cb = theRes->getPoint(i).incidentEdge[FIRST]; + + do + { + // one angle + Geom::Point prx, nex; + prx = theRes->getEdge(pb).dx; + nlen = sqrt (dot(prx, prx)); + prx /= nlen; + nex = theRes->getEdge(cb).dx; + nlen = sqrt (dot(nex , nex)); + nex /= nlen; + + if (theRes->getEdge(pb).en == i) + { + prx = -prx; + } + + if (theRes->getEdge(cb).en == i) + { + nex = -nex; + } + + if (vectors_are_clockwise (nex, nx, prx)) + { + // we're in that angle. set the sign, and exit that loop + if (theRes->getEdge(cb).st == i) + { + ptDist = -ndist; + ptSet = true; + } + else + { + ptDist = ndist; + ptSet = true; + } + break; + } + + pb = cb; + cb = theRes->NextAt (i, cb); + } + + while (cb >= 0 && pb >= 0 && pb != fb); + } + } + } + + // loop over the edges to try to improve the distance + for (int i = 0; i < theRes->numberOfEdges(); i++) + { + Geom::Point sx = theRes->getPoint(theRes->getEdge(i).st).x; + Geom::Point ex = theRes->getPoint(theRes->getEdge(i).en).x; + Geom::Point nx = ex - sx; + double len = sqrt (dot(nx,nx)); + + if (len > 0.0001) + { + Geom::Point pxsx=px-sx; + double ab = dot(nx,pxsx); + + if (ab > 0 && ab < len * len) + { + // we're in the zone of influence of the segment + double ndist = (cross(nx, pxsx)) / len; + + if (arSet == false || fabs (ndist) < fabs (arDist)) + { + arDist = ndist; + arSet = true; + } + } + } + } + + if (arSet || ptSet) + { + if (arSet == false) { + arDist = ptDist; + } + + if (ptSet == false) { + ptDist = arDist; + } + + if (fabs (ptDist) < fabs (arDist)) { + dist = ptDist; + } else { + dist = arDist; + } + } + } + + delete theShape; + delete theRes; + + return dist; +} + +/** + * Computes a point on the offset; used to set a "seed" position for + * the control knot. + * + * \return the topmost point on the offset. + */ +void +sp_offset_top_point (SPOffset const * offset, Geom::Point *px) +{ + (*px) = Geom::Point(0, 0); + + if (offset == nullptr) { + return; + } + + if (offset->knotSet) + { + (*px) = offset->knot; + return; + } + + SPCurve *curve = SP_SHAPE (offset)->getCurve(); + + if (curve == nullptr) + { + // CPPIFY + //offset->set_shape(); + const_cast(offset)->set_shape(); + + curve = SP_SHAPE (offset)->getCurve(); + + if (curve == nullptr) + return; + } + + if (curve->is_empty()) + { + curve->unref(); + return; + } + + Path *finalPath = new Path; + finalPath->LoadPathVector(curve->get_pathvector()); + + Shape *theShape = new Shape; + + finalPath->Convert (1.0); + finalPath->Fill (theShape, 0); + + if (theShape->hasPoints()) + { + theShape->SortPoints (); + *px = theShape->getPoint(0).x; + } + + delete theShape; + delete finalPath; + curve->unref(); +} + +// the listening functions +static void sp_offset_start_listening(SPOffset *offset,SPObject* to) +{ + if ( to == nullptr ) { + return; + } + + offset->sourceObject = to; + offset->sourceRepr = to->getRepr(); + + offset->_delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_offset_delete_self), offset)); + offset->_transformed_connection = SP_ITEM(to)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_offset_move_compensate), offset)); + offset->_modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_offset_source_modified), offset)); +} + +static void sp_offset_quit_listening(SPOffset *offset) +{ + if ( offset->sourceObject == nullptr ) { + return; + } + + offset->_modified_connection.disconnect(); + offset->_delete_connection.disconnect(); + offset->_transformed_connection.disconnect(); + + offset->sourceRepr = nullptr; + offset->sourceObject = nullptr; +} + +static void +sp_offset_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPOffset *offset) +{ + sp_offset_quit_listening(offset); + + if (offset->sourceRef) { + SPItem *refobj = offset->sourceRef->getObject(); + + if (refobj) { + sp_offset_start_listening(offset,refobj); + } + + offset->sourceDirty=true; + offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void sp_offset_move_compensate(Geom::Affine const *mp, SPItem */*original*/, SPOffset *self) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + + Geom::Affine m(*mp); + + if (!(m.isTranslation()) || mode == SP_CLONE_COMPENSATION_NONE) { + self->sourceDirty=true; + self->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return; + } + + // calculate the compensation matrix and the advertized movement matrix + self->readAttr("transform"); + + Geom::Affine t = self->transform; + Geom::Affine offset_move = t.inverse() * m * t; + + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + offset_move = offset_move.inverse() * m; + advertized_move = m; + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + offset_move = offset_move.inverse(); + advertized_move.setIdentity(); + } else { + g_assert_not_reached(); + } + + self->sourceDirty=true; + + // commit the compensation + self->transform *= offset_move; + self->doWriteTransform(self->transform, &advertized_move); + self->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_offset_delete_self(SPObject */*deleted*/, SPOffset *offset) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + // leave it be. just forget about the source + sp_offset_quit_listening(offset); + + if ( offset->sourceHref ) { + g_free(offset->sourceHref); + } + + offset->sourceHref = nullptr; + offset->sourceRef->detach(); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + offset->deleteObject(); + } +} + +static void +sp_offset_source_modified (SPObject */*iSource*/, guint flags, SPItem *item) +{ + SPOffset *offset = SP_OFFSET(item); + offset->sourceDirty=true; + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG)) { + offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void +refresh_offset_source(SPOffset* offset) +{ + if ( offset == nullptr ) { + return; + } + + offset->sourceDirty=false; + + // le mauvais cas: pas d'attribut d => il faut verifier que c'est une SPShape puis prendre le contour + // The bad case: no d attribute. Must check that it's an SPShape and then take the outline. + SPObject *refobj=offset->sourceObject; + + if ( refobj == nullptr ) { + return; + } + + SPItem *item = SP_ITEM (refobj); + SPCurve *curve = nullptr; + + if (SP_IS_SHAPE (item)) { + curve = SP_SHAPE (item)->getCurve (); + } + else if (SP_IS_TEXT (item)) { + curve = SP_TEXT (item)->getNormalizedBpath (); + } + else { + return; + } + + if (curve == nullptr) { + return; + } + + Path *orig = new Path; + orig->LoadPathVector(curve->get_pathvector()); + curve->unref(); + + if (!item->transform.isIdentity()) { + gchar const *t_attr = item->getRepr()->attribute("transform"); + + if (t_attr) { + Geom::Affine t; + + if (sp_svg_transform_read(t_attr, &t)) { + orig->Transform(t); + } + } + } + + // Finish up. + { + SPCSSAttr *css; + const gchar *val; + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData (1.0); + orig->Fill (theShape, 0); + + css = sp_repr_css_attr (offset->sourceRepr , "style"); + val = sp_repr_css_property (css, "fill-rule", nullptr); + + if (val && strcmp (val, "nonzero") == 0) + { + theRes->ConvertToShape (theShape, fill_nonZero); + } + else if (val && strcmp (val, "evenodd") == 0) + { + theRes->ConvertToShape (theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape (theShape, fill_nonZero); + } + + Path *originaux[1]; + originaux[0] = orig; + Path *res = new Path; + theRes->ConvertToForme (res, 1, originaux); + + delete theShape; + delete theRes; + + char *res_d = res->svg_dump_path (); + delete res; + delete orig; + + // TODO fix: + //XML Tree being used directly here while it shouldn't be. + offset->setAttribute("inkscape:original", res_d); + + free (res_d); + } +} + +SPItem * +sp_offset_get_source (SPOffset *offset) +{ + if (offset && offset->sourceRef) { + SPItem *refobj = offset->sourceRef->getObject(); + + if (SP_IS_ITEM (refobj)) { + return (SPItem *) refobj; + } + } + + return nullptr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-offset.h b/src/object/sp-offset.h new file mode 100644 index 0000000..f5e9bf1 --- /dev/null +++ b/src/object/sp-offset.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_OFFSET_H +#define SEEN_SP_OFFSET_H +/* + * Authors: + * Mitsuru Oka + * Lauris Kaplinski + * (of the sp-spiral.h upon which this file was created) + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "sp-shape.h" + +#define SP_OFFSET(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_OFFSET(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPUseReference; + +/** + * SPOffset class. + * + * An offset is defined by curve and radius. The original curve is kept as + * a path in a sodipodi:original attribute. It's not possible to change + * the original curve. + * + * SPOffset is a derivative of SPShape, much like the SPSpiral or SPRect. + * The goal is to have a source shape (= originalPath), an offset (= radius) + * and compute the offset of the source by the radius. To get it to work, + * one needs to know what the source is and what the radius is, and how it's + * stored in the xml representation. The object itself is a "path" element, + * to get lots of shape functionality for free. The source is the easy part: + * it's stored in a "inkscape:original" attribute in the path. In case of + * "linked" offset, as they've been dubbed, there is an additional + * "inkscape:href" that contains the id of an element of the svg. + * When built, the object will attach a listener vector to that object and + * rebuild the "inkscape:original" whenever the href'd object changes. This + * is of course grossly inefficient, and also does not react to changes + * to the href'd during context stuff (like changing the shape of a star by + * dragging control points) unless the path of that object is changed during + * the context (seems to be the case for SPEllipse). The computation of the + * offset is done in sp_offset_set_shape(), a function that is called whenever + * a change occurs to the offset (change of source or change of radius). + * just like the sp-star and other, this path derivative can make control + * points, or more precisely one control point, that's enough to define the + * radius (look in shape-editor-knotholders). + */ +class SPOffset : public SPShape { +public: + SPOffset(); + ~SPOffset() override; + + void *originalPath; ///< will be a livarot Path, just don't declare it here to please the gcc linker FIXME what? + char *original; ///< SVG description of the source path + float rad; ///< offset radius + + /// for interactive setting of the radius + bool knotSet; + Geom::Point knot; + + bool sourceDirty; + bool isUpdating; + + char *sourceHref; + SPUseReference *sourceRef; + Inkscape::XML::Node *sourceRepr; ///< the repr associated with that id + SPObject *sourceObject; + + sigc::connection _modified_connection; + sigc::connection _delete_connection; + sigc::connection _changed_connection; + sigc::connection _transformed_connection; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned flags) override; + void release() override; + + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; + const char* displayName() const override; + char* description() const override; + + void set_shape() override; +}; + +double sp_offset_distance_to_original (SPOffset * offset, Geom::Point px); +void sp_offset_top_point (SPOffset const *offset, Geom::Point *px); + +SPItem *sp_offset_get_source (SPOffset *offset); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-paint-server-reference.h b/src/object/sp-paint-server-reference.h new file mode 100644 index 0000000..4f496ba --- /dev/null +++ b/src/object/sp-paint-server-reference.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PAINT_SERVER_REFERENCE_H +#define SEEN_SP_PAINT_SERVER_REFERENCE_H + +/* + * Reference class for gradients and patterns. + * + * Author: + * Lauris Kaplinski + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri-references.h" + +class SPDocument; +class SPObject; +class SPPaintServer; + +class SPPaintServerReference : public Inkscape::URIReference { +public: + SPPaintServerReference (SPObject *obj) : URIReference(obj) {} + SPPaintServerReference (SPDocument *doc) : URIReference(doc) {} + SPPaintServer *getObject() const; + +protected: + bool _acceptObject(SPObject *obj) const override; +}; + +#endif // SEEN_SP_PAINT_SERVER_REFERENCE_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-paint-server.cpp b/src/object/sp-paint-server.cpp new file mode 100644 index 0000000..1ec4db8 --- /dev/null +++ b/src/object/sp-paint-server.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Base class for gradients and patterns + * + * Author: + * Lauris Kaplinski + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-paint-server-reference.h" +#include "sp-paint-server.h" + +#include "sp-gradient.h" +#include "xml/node.h" + +SPPaintServer *SPPaintServerReference::getObject() const +{ + return static_cast(URIReference::getObject()); +} + +bool SPPaintServerReference::_acceptObject(SPObject *obj) const +{ + return SP_IS_PAINT_SERVER(obj) && URIReference::_acceptObject(obj); +} + +SPPaintServer::SPPaintServer() : SPObject() { + this->swatch = false; +} + +SPPaintServer::~SPPaintServer() = default; + +bool SPPaintServer::isSwatch() const +{ + return swatch; +} + + +// TODO: So a solid brush is a gradient with a swatch and zero stops? +// Should we derive a new class for that? Or at least make this method +// virtual and move it out of the way? +bool SPPaintServer::isSolid() const +{ + bool solid = false; + if (swatch && SP_IS_GRADIENT(this)) { + SPGradient *grad = SP_GRADIENT(this); + if ( grad->hasStops() && (grad->getStopCount() == 0) ) { + solid = true; + } + } + return solid; +} + +bool SPPaintServer::isValid() const +{ + return true; +} + +Inkscape::DrawingPattern *SPPaintServer::show(Inkscape::Drawing &/*drawing*/, unsigned int /*key*/, Geom::OptRect /*bbox*/) +{ + return nullptr; +} + +void SPPaintServer::hide(unsigned int /*key*/) +{ +} + +void SPPaintServer::setBBox(unsigned int /*key*/, Geom::OptRect const &/*bbox*/) +{ +} + +cairo_pattern_t* SPPaintServer::pattern_new(cairo_t * /*ct*/, Geom::OptRect const &/*bbox*/, double /*opacity*/) +{ + return nullptr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-paint-server.h b/src/object/sp-paint-server.h new file mode 100644 index 0000000..9e06170 --- /dev/null +++ b/src/object/sp-paint-server.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PAINT_SERVER_H +#define SEEN_SP_PAINT_SERVER_H + +/* + * Base class for gradients and patterns + * + * Author: + * Lauris Kaplinski + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/rect.h> +#include +#include "sp-object.h" + +namespace Inkscape { + +class Drawing; +class DrawingPattern; + +} + +#define SP_PAINT_SERVER(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_PAINT_SERVER(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPPaintServer : public SPObject { +public: + SPPaintServer(); + ~SPPaintServer() override; + + bool isSwatch() const; + bool isSolid() const; + virtual bool isValid() const; + + //There are two ways to render a paint. The simple one is to create cairo_pattern_t structure + //on demand by pattern_new method. It is used for gradients. The other one is to add elements + //representing PaintServer in NR tree. It is used by hatches and patterns. + //Either pattern new or all three methods show, hide, setBBox need to be implemented + virtual Inkscape::DrawingPattern *show(Inkscape::Drawing &drawing, unsigned int key, Geom::OptRect bbox); // TODO check passing bbox by value. Looks suspicious. + virtual void hide(unsigned int key); + virtual void setBBox(unsigned int key, Geom::OptRect const &bbox); + + virtual cairo_pattern_t* pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity); + +protected: + bool swatch; +}; + +/** + * Returns the first of {src, src-\>ref-\>getObject(), + * src-\>ref-\>getObject()-\>ref-\>getObject(),...} + * for which \a match is true, or NULL if none found. + * + * The raison d'être of this routine is that it correctly handles cycles in the href chain (e.g., if + * a gradient gives itself as its href, or if each of two gradients gives the other as its href). + * + * \pre SP_IS_GRADIENT(src). + */ +template +PaintServer *chase_hrefs(PaintServer *src, sigc::slot match) { + /* Use a pair of pointers for detecting loops: p1 advances half as fast as p2. If there is a + loop, then once p1 has entered the loop, we'll detect it the next time the distance between + p1 and p2 is a multiple of the loop size. */ + PaintServer *p1 = src, *p2 = src; + bool do1 = false; + for (;;) { + if (match(p2)) { + return p2; + } + + p2 = p2->ref->getObject(); + if (!p2) { + return p2; + } + if (do1) { + p1 = p1->ref->getObject(); + } + do1 = !do1; + + if ( p2 == p1 ) { + /* We've been here before, so return NULL to indicate that no matching gradient found + * in the chain. */ + return nullptr; + } + } +} + +#endif // SEEN_SP_PAINT_SERVER_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-path.cpp b/src/object/sp-path.cpp new file mode 100644 index 0000000..8e5ead2 --- /dev/null +++ b/src/object/sp-path.cpp @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * David Turner + * Abhishek Sharma + * Johan Engelen + * + * Copyright (C) 2004 David Turner + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "sp-lpe-item.h" + +#include "display/curve.h" +#include <2geom/curves.h> +#include "helper/geom-curves.h" + +#include "svg/svg.h" +#include "xml/repr.h" +#include "attributes.h" + +#include "sp-path.h" +#include "sp-guide.h" + +#include "document.h" +#include "desktop.h" + +#include "desktop-style.h" +#include "ui/tools/tool-base.h" +#include "inkscape.h" +#include "style.h" + +#define noPATH_VERBOSE + +gint SPPath::nodesInPath() const +{ + return _curve ? _curve->nodes_in_path() : 0; +} + +const char* SPPath::displayName() const { + return _("Path"); +} + +gchar* SPPath::description() const { + int count = this->nodesInPath(); + char *lpe_desc = g_strdup(""); + + if (hasPathEffect()) { + Glib::ustring s; + PathEffectList effect_list = this->getEffectList(); + + for (auto & it : effect_list) + { + LivePathEffectObject *lpeobj = it->lpeobject; + + if (!lpeobj || !lpeobj->get_lpe()) { + break; + } + + if (s.empty()) { + s = lpeobj->get_lpe()->getName(); + } else { + s = s + ", " + lpeobj->get_lpe()->getName(); + } + } + lpe_desc = g_strdup_printf(_(", path effect: %s"), s.c_str()); + } + char *ret = g_strdup_printf(ngettext( + _("%i node%s"), _("%i nodes%s"), count), count, lpe_desc); + g_free(lpe_desc); + return ret; +} + +void SPPath::convert_to_guides() const { + if (!this->_curve) { + return; + } + + std::list > pts; + + Geom::Affine const i2dt(this->i2dt_affine()); + Geom::PathVector const & pv = this->_curve->get_pathvector(); + + for(const auto & pit : pv) { + for(Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit) { + // only add curves for straight line segments + if( is_straight_curve(*cit) ) + { + pts.emplace_back(cit->initialPoint() * i2dt, cit->finalPoint() * i2dt); + } + } + } + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +SPPath::SPPath() : SPShape(), connEndPair(this) { +} + +SPPath::~SPPath() = default; + +void SPPath::build(SPDocument *document, Inkscape::XML::Node *repr) { + /* Are these calls actually necessary? */ + this->readAttr( "marker" ); + this->readAttr( "marker-start" ); + this->readAttr( "marker-mid" ); + this->readAttr( "marker-end" ); + + sp_conn_end_pair_build(this); + + SPShape::build(document, repr); + // Our code depends on 'd' being an attribute (LPE's, etc.). To support 'd' as a property, we + // check it here (after the style property has been evaluated, this allows us to properly + // handled precedence of property vs attribute). If we read in a 'd' set by styling, convert it + // to an attribute. We'll convert it back on output. + + d_source = style->d.style_src; + + if (style->d.set && + + (d_source == SP_STYLE_SRC_STYLE_PROP || d_source == SP_STYLE_SRC_STYLE_SHEET) ) { + + if (char const *d_val = style->d.value()) { + // Chrome shipped with a different syntax for property vs attribute. + // The SVG Working group decided to follow the Chrome syntax (which may + // allow future extensions of the 'd' property). The property syntax + // wraps the path data with "path(...)". We must strip that! + + // Must be Glib::ustring or we get conversion errors! + Glib::ustring input = d_val; + Glib::ustring expression = R"A(path\("(.*)"\))A"; + Glib::RefPtr regex = Glib::Regex::create(expression); + Glib::MatchInfo matchInfo; + regex->match(input, matchInfo); + + if (matchInfo.matches()) { + Glib::ustring value = matchInfo.fetch(1); + Geom::PathVector pv = sp_svg_read_pathv(value.c_str()); + + SPCurve *curve = new SPCurve(pv); + if (curve) { + + // Update curve + this->setCurveInsync(curve, TRUE); + curve->unref(); + + // Convert from property to attribute (convert back on write) + setAttributeOrRemoveIfEmpty("d", value); + + SPCSSAttr *css = sp_repr_css_attr( getRepr(), "style"); + sp_repr_css_unset_property ( css, "d"); + sp_repr_css_set ( getRepr(), css, "style" ); + sp_repr_css_attr_unref ( css ); + + style->d.style_src = SP_STYLE_SRC_ATTRIBUTE; + } else { + std::cerr << "SPPath::build: Failed to create curve: " << input << std::endl; + } + } + } + // If any if statement is false, do nothing... don't overwrite 'd' from attribute + } + + + // this->readAttr( "inkscape:original-d" ); // bug #1299948 + // Why we take the long way of doing this probably needs some explaining: + // + // Normally upon being built, reading the inkscape:original-d attribute + // will cause the path to actually _write to its repr_ in response to this. + // This is bad, bad news if the attached effect refers to a path which + // hasn't been constructed yet. + // + // What will happen is the effect parameter will cause the effect to + // recalculate with a completely different value due to the parameter being + // "empty" -- even worse, an undo event might be created with the bad value, + // and undoing the current action could cause it to revert to the "bad" + // state. (After that, the referred object will be constructed and the + // reference will trigger the path effect to update and commit the right + // value to "d".) + // + // This mild nastiness here (don't recalculate effects on build) prevents a + // plethora of issues with effects with linked parameters doing wild and + // stupid things on new documents upon a mere undo. + + if (gchar const* s = this->getRepr()->attribute("inkscape:original-d")) + { + // Write the value to _curve_before_lpe, do not recalculate effects + Geom::PathVector pv = sp_svg_read_pathv(s); + SPCurve *curve = new SPCurve(pv); + + if (_curve_before_lpe) { + _curve_before_lpe = _curve_before_lpe->unref(); + } + + if (curve) { + _curve_before_lpe = curve->ref(); + } + } + this->readAttr( "d" ); + + /* d is a required attribute */ + char const *d = this->getAttribute("d", nullptr); + + if (d == nullptr) { + // First see if calculating the path effect will generate "d": + this->update_patheffect(true); + d = this->getAttribute("d", nullptr); + + // I guess that didn't work, now we have nothing useful to write ("") + if (d == nullptr) { + this->setKeyValue( sp_attribute_lookup("d"), ""); + } + } +} + +void SPPath::release() { + this->connEndPair.release(); + + SPShape::release(); +} + +void SPPath::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_INKSCAPE_ORIGINAL_D: + if (value) { + Geom::PathVector pv = sp_svg_read_pathv(value); + SPCurve *curve = new SPCurve(pv); + + if (curve) { + this->setCurveBeforeLPE(curve); + curve->unref(); + } + } else { + bool haslpe = this->hasPathEffectOnClipOrMaskRecursive(this); + if (!haslpe) { + this->setCurveBeforeLPE(nullptr); + } else { + //This happends on undo, fix bug:#1791784 + this->removeAllPathEffects(false); + } + } + sp_lpe_item_update_patheffect(this, true, true); + break; + + case SP_ATTR_D: + if (value) { + Geom::PathVector pv = sp_svg_read_pathv(value); + SPCurve *curve = new SPCurve(pv); + + if (curve) { + this->setCurve(curve); + curve->unref(); + } + } else { + this->setCurve(nullptr); + } + break; + + case SP_PROP_MARKER: + case SP_PROP_MARKER_START: + case SP_PROP_MARKER_MID: + case SP_PROP_MARKER_END: + sp_shape_set_marker(this, key, value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_CONNECTOR_TYPE: + case SP_ATTR_CONNECTOR_CURVATURE: + case SP_ATTR_CONNECTION_START: + case SP_ATTR_CONNECTION_END: + case SP_ATTR_CONNECTION_START_POINT: + case SP_ATTR_CONNECTION_END_POINT: + this->connEndPair.setAttr(key, value); + break; + + default: + SPShape::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPPath::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:path"); + } + +#ifdef PATH_VERBOSE +g_message("sp_path_write writes 'd' attribute"); +#endif + + if (this->_curve) { + gchar *str = sp_svg_write_path(this->_curve->get_pathvector()); + repr->setAttribute("d", str); + g_free(str); + } else { + repr->removeAttribute("d"); + } + + if (flags & SP_OBJECT_WRITE_EXT) { + if ( this->_curve_before_lpe != nullptr ) { + gchar *str = sp_svg_write_path(this->_curve_before_lpe->get_pathvector()); + repr->setAttribute("inkscape:original-d", str); + g_free(str); + } else { + repr->removeAttribute("inkscape:original-d"); + } + } + + this->connEndPair.writeRepr(repr); + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +void SPPath::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +void SPPath::update(SPCtx *ctx, guint flags) { + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + SPShape::update(ctx, flags); + this->connEndPair.update(); +} + +Geom::Affine SPPath::set_transform(Geom::Affine const &transform) { + if (!_curve) { // 0 nodes, nothing to transform + return Geom::identity(); + } + + if (pathEffectsEnabled() && !optimizeTransforms()) { + return transform; + } + if (hasPathEffectRecursive() && pathEffectsEnabled()) { + if (!_curve_before_lpe) { + // we are inside a LPE group creating a new element + // and the original-d curve is not defined, + // This fix a issue with calligrapic tool that make a transform just when draw + setCurveBeforeLPE(_curve); + } + _curve_before_lpe->transform(transform); + } else { + _curve->transform(transform); + } + notifyTransform(transform); + // Adjust stroke + this->adjust_stroke(transform.descrim()); + + // Adjust pattern fill + this->adjust_pattern(transform); + + // Adjust gradient fill + this->adjust_gradient(transform); + + // nothing remains - we've written all of the transform, so return identity + return Geom::identity(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-path.h b/src/object/sp-path.h new file mode 100644 index 0000000..4ccec67 --- /dev/null +++ b/src/object/sp-path.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PATH_H +#define SEEN_SP_PATH_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Ximian, Inc. + * Johan Engelen + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-shape.h" +#include "sp-conn-end-pair.h" +#include "style-internal.h" // For SPStyleSrc + +class SPCurve; + +#define SP_PATH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_PATH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** + * SVG implementation + */ +class SPPath : public SPShape { +public: + SPPath(); + ~SPPath() override; + + int nodesInPath() const; + friend class SPConnEndPair; + SPConnEndPair connEndPair; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void update(SPCtx* ctx, unsigned int flags) override; + + void set(SPAttributeEnum key, char const* value) override; + void update_patheffect(bool write) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + const char* displayName() const override; + char* description() const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + void convert_to_guides() const override; +private: + SPStyleSrc d_source; // Source of 'd' value, saved for output. +}; + +#endif // SEEN_SP_PATH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-pattern.cpp b/src/object/sp-pattern.cpp new file mode 100644 index 0000000..a41c65d --- /dev/null +++ b/src/object/sp-pattern.cpp @@ -0,0 +1,697 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Author: + * Lauris Kaplinski + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-pattern.h" + +#include +#include + +#include + +#include <2geom/transforms.h> + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" + +#include "sp-defs.h" +#include "sp-factory.h" +#include "sp-item.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-surface.h" +#include "display/drawing.h" +#include "display/drawing-group.h" + +#include "svg/svg.h" + +SPPattern::SPPattern() + : SPPaintServer() + , SPViewBox() +{ + this->ref = new SPPatternReference(this); + this->ref->changedSignal().connect(sigc::mem_fun(this, &SPPattern::_onRefChanged)); + + this->_pattern_units = UNITS_OBJECTBOUNDINGBOX; + this->_pattern_units_set = false; + + this->_pattern_content_units = UNITS_USERSPACEONUSE; + this->_pattern_content_units_set = false; + + this->_pattern_transform = Geom::identity(); + this->_pattern_transform_set = false; + + this->_x.unset(); + this->_y.unset(); + this->_width.unset(); + this->_height.unset(); +} + +SPPattern::~SPPattern() = default; + +void SPPattern::build(SPDocument *doc, Inkscape::XML::Node *repr) +{ + SPPaintServer::build(doc, repr); + + this->readAttr("patternUnits"); + this->readAttr("patternContentUnits"); + this->readAttr("patternTransform"); + this->readAttr("x"); + this->readAttr("y"); + this->readAttr("width"); + this->readAttr("height"); + this->readAttr("viewBox"); + this->readAttr("preserveAspectRatio"); + this->readAttr("xlink:href"); + this->readAttr("style"); + + /* Register ourselves */ + doc->addResource("pattern", this); +} + +void SPPattern::release() +{ + if (this->document) { + // Unregister ourselves + this->document->removeResource("pattern", this); + } + + if (this->ref) { + this->_modified_connection.disconnect(); + this->ref->detach(); + delete this->ref; + this->ref = nullptr; + } + + SPPaintServer::release(); +} + +void SPPattern::set(SPAttributeEnum key, const gchar *value) +{ + switch (key) { + case SP_ATTR_PATTERNUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->_pattern_units = UNITS_USERSPACEONUSE; + } + else { + this->_pattern_units = UNITS_OBJECTBOUNDINGBOX; + } + + this->_pattern_units_set = true; + } + else { + this->_pattern_units_set = false; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_PATTERNCONTENTUNITS: + if (value) { + if (!strcmp(value, "userSpaceOnUse")) { + this->_pattern_content_units = UNITS_USERSPACEONUSE; + } + else { + this->_pattern_content_units = UNITS_OBJECTBOUNDINGBOX; + } + + this->_pattern_content_units_set = true; + } + else { + this->_pattern_content_units_set = false; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_PATTERNTRANSFORM: { + Geom::Affine t; + + if (value && sp_svg_transform_read(value, &t)) { + this->_pattern_transform = t; + this->_pattern_transform_set = true; + } + else { + this->_pattern_transform = Geom::identity(); + this->_pattern_transform_set = false; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + case SP_ATTR_X: + this->_x.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + this->_y.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_WIDTH: + this->_width.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HEIGHT: + this->_height.readOrUnset(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_VIEWBOX: + set_viewBox(value); + this->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + set_preserveAspectRatio(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_XLINK_HREF: + if (value && this->href == value) { + /* Href unchanged, do nothing. */ + } + else { + this->href.clear(); + + if (value) { + // First, set the href field; it's only used in the "unchanged" check above. + this->href = value; + // Now do the attaching, which emits the changed signal. + if (value) { + try { + this->ref->attach(Inkscape::URI(value)); + } + catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->ref->detach(); + } + } + else { + this->ref->detach(); + } + } + } + break; + + default: + SPPaintServer::set(key, value); + break; + } +} + + +/* TODO: do we need a ::remove_child handler? */ + +/* fixme: We need ::order_changed handler too (Lauris) */ + +void SPPattern::_getChildren(std::list &l) +{ + for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->firstChild()) { // find the first one with children + for (auto& child: pat_i->children) { + l.push_back(&child); + } + break; // do not go further up the chain if children are found + } + } +} + +void SPPattern::update(SPCtx *ctx, unsigned int flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::list l; + _getChildren(l); + + for (auto child : l) { + sp_object_ref(child, nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->updateDisplay(ctx, flags); + } + + sp_object_unref(child, nullptr); + } +} + +void SPPattern::modified(unsigned int flags) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + std::list l; + _getChildren(l); + + for (auto child : l) { + sp_object_ref(child, nullptr); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child, nullptr); + } +} + +void SPPattern::_onRefChanged(SPObject *old_ref, SPObject *ref) +{ + if (old_ref) { + _modified_connection.disconnect(); + } + + if (SP_IS_PATTERN(ref)) { + _modified_connection = ref->connectModified(sigc::mem_fun(this, &SPPattern::_onRefModified)); + } + + _onRefModified(ref, 0); +} + +void SPPattern::_onRefModified(SPObject * /*ref*/, guint /*flags*/) +{ + requestModified(SP_OBJECT_MODIFIED_FLAG); + // Conditional to avoid causing infinite loop if there's a cycle in the href chain. +} + +guint SPPattern::_countHrefs(SPObject *o) const +{ + if (!o) + return 1; + + guint i = 0; + + SPStyle *style = o->style; + if (style && style->fill.isPaintserver() && SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style)) && + SP_PATTERN(SP_STYLE_FILL_SERVER(style)) == this) { + i++; + } + if (style && style->stroke.isPaintserver() && SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style)) && + SP_PATTERN(SP_STYLE_STROKE_SERVER(style)) == this) { + i++; + } + + for (auto& child: o->children) { + i += _countHrefs(&child); + } + + return i; +} + +SPPattern *SPPattern::_chain() const +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); + repr->setAttribute("inkscape:collect", "always"); + Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id")); + repr->setAttribute("xlink:href", parent_ref); + + defsrepr->addChild(repr, nullptr); + const gchar *child_id = repr->attribute("id"); + SPObject *child = document->getObjectById(child_id); + g_assert(SP_IS_PATTERN(child)); + + return SP_PATTERN(child); +} + +SPPattern *SPPattern::clone_if_necessary(SPItem *item, const gchar *property) +{ + SPPattern *pattern = this; + if (pattern->href.empty() || pattern->hrefcount > _countHrefs(item)) { + pattern = _chain(); + Glib::ustring href = Glib::ustring::compose("url(#%1)", pattern->getRepr()->attribute("id")); + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, property, href.c_str()); + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + return pattern; +} + +void SPPattern::transform_multiply(Geom::Affine postmul, bool set) +{ + // this formula is for a different interpretation of pattern transforms as described in (*) in sp-pattern.cpp + // for it to work, we also need sp_object_read_attr( item, "transform"); + // pattern->patternTransform = premul * item->transform * pattern->patternTransform * item->transform.inverse() * + // postmul; + + // otherwise the formula is much simpler + if (set) { + _pattern_transform = postmul; + } + else { + _pattern_transform = getTransform() * postmul; + } + _pattern_transform_set = true; + + gchar *c = sp_svg_transform_write(_pattern_transform); + setAttribute("patternTransform", c); + g_free(c); +} + +const gchar *SPPattern::produce(const std::vector &reprs, Geom::Rect bounds, + SPDocument *document, Geom::Affine transform, Geom::Affine move) +{ + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr(); + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern"); + repr->setAttribute("patternUnits", "userSpaceOnUse"); + sp_repr_set_svg_double(repr, "width", bounds.dimensions()[Geom::X]); + sp_repr_set_svg_double(repr, "height", bounds.dimensions()[Geom::Y]); + //TODO: Maybe is better handle it in sp_svg_transform_write + if(transform != Geom::Affine()){ + gchar *t = sp_svg_transform_write(transform); + repr->setAttribute("patternTransform", t); + g_free(t); + } + defsrepr->appendChild(repr); + const gchar *pat_id = repr->attribute("id"); + SPObject *pat_object = document->getObjectById(pat_id); + + for (auto node : reprs) { + SPItem *copy = SP_ITEM(pat_object->appendChildRepr(node)); + + Geom::Affine dup_transform; + if (!sp_svg_transform_read(node->attribute("transform"), &dup_transform)) + dup_transform = Geom::identity(); + dup_transform *= move; + + copy->doWriteTransform(dup_transform, nullptr, false); + } + + Inkscape::GC::release(repr); + return pat_id; +} + +SPPattern *SPPattern::rootPattern() +{ + for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->firstChild()) { // find the first one with children + return pat_i; + } + } + return this; // document is broken, we can't get to root; but at least we can return pat which is supposedly a valid + // pattern +} + + + +// Access functions that look up fields up the chain of referenced patterns and return the first one which is set +// FIXME: all of them must use chase_hrefs the same as in SPGradient, to avoid lockup on circular refs + +SPPattern::PatternUnits SPPattern::patternUnits() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_pattern_units_set) + return pat_i->_pattern_units; + } + return _pattern_units; +} + +SPPattern::PatternUnits SPPattern::patternContentUnits() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_pattern_content_units_set) + return pat_i->_pattern_content_units; + } + return _pattern_content_units; +} + +Geom::Affine const &SPPattern::getTransform() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_pattern_transform_set) + return pat_i->_pattern_transform; + } + return _pattern_transform; +} + +gdouble SPPattern::x() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_x._set) + return pat_i->_x.computed; + } + return 0; +} + +gdouble SPPattern::y() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_y._set) + return pat_i->_y.computed; + } + return 0; +} + +gdouble SPPattern::width() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_width._set) + return pat_i->_width.computed; + } + return 0; +} + +gdouble SPPattern::height() const +{ + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->_height._set) + return pat_i->_height.computed; + } + return 0; +} + +Geom::OptRect SPPattern::viewbox() const +{ + Geom::OptRect viewbox; + for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i->viewBox_set) { + viewbox = pat_i->viewBox; + break; + } + } + return viewbox; +} + +bool SPPattern::_hasItemChildren() const +{ + for (auto& child: children) { + if (SP_IS_ITEM(&child)) { + return true; + } + } + + return false; +} + +bool SPPattern::isValid() const +{ + double tile_width = width(); + double tile_height = height(); + + if (tile_width <= 0 || tile_height <= 0) + return false; + return true; +} + +cairo_pattern_t *SPPattern::pattern_new(cairo_t *base_ct, Geom::OptRect const &bbox, double opacity) +{ + + bool needs_opacity = (1.0 - opacity) >= 1e-3; + bool visible = opacity >= 1e-3; + + if (!visible) { + return nullptr; + } + + /* Show items */ + SPPattern *shown = nullptr; + + for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + // find the first one with item children + if (pat_i && SP_IS_OBJECT(pat_i) && pat_i->_hasItemChildren()) { + shown = pat_i; + break; // do not go further up the chain if children are found + } + } + + if (!shown) { + return cairo_pattern_create_rgba(0, 0, 0, 0); + } + + /* Create drawing for rendering */ + Inkscape::Drawing drawing; + unsigned int dkey = SPItem::display_key_new(1); + Inkscape::DrawingGroup *root = new Inkscape::DrawingGroup(drawing); + drawing.setRoot(root); + + for (auto& child: shown->children) { + if (SP_IS_ITEM(&child)) { + // for each item in pattern, show it on our drawing, add to the group, + // and connect to the release signal in case the item gets deleted + Inkscape::DrawingItem *cai; + cai = SP_ITEM(&child)->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY); + root->appendChild(cai); + } + } + + // ****** Geometry ****** + // + // * "width" and "height" determine tile size. + // * "viewBox" (if defined) or "patternContentUnits" determines placement of content inside + // tile. + // * "x", "y", and "patternTransform" transform tile to user space after tile is generated. + + // These functions recursively search up the tree to find the values. + double tile_x = x(); + double tile_y = y(); + double tile_width = width(); + double tile_height = height(); + if (bbox && (patternUnits() == UNITS_OBJECTBOUNDINGBOX)) { + tile_x *= bbox->width(); + tile_y *= bbox->height(); + tile_width *= bbox->width(); + tile_height *= bbox->height(); + } + + // Pattern size in pattern space + Geom::Rect pattern_tile = Geom::Rect::from_xywh(0, 0, tile_width, tile_height); + + // Content to tile (pattern space) + Geom::Affine content2ps; + Geom::OptRect effective_view_box = viewbox(); + if (effective_view_box) { + // viewBox to pattern server (using SPViewBox) + viewBox = *effective_view_box; + c2p.setIdentity(); + apply_viewbox(pattern_tile); + content2ps = c2p; + } + else { + + // Content to bbox + if (bbox && (patternContentUnits() == UNITS_OBJECTBOUNDINGBOX)) { + content2ps = Geom::Affine(bbox->width(), 0.0, 0.0, bbox->height(), 0, 0); + } + } + + + // Tile (pattern space) to user. + Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * getTransform(); + + + // Transform of object with pattern (includes screen scaling) + cairo_matrix_t cm; + cairo_get_matrix(base_ct, &cm); + Geom::Affine full(cm.xx, cm.yx, cm.xy, cm.yy, 0, 0); + + // The DrawingSurface class handles the mapping from "logical space" + // (coordinates in the rendering) to "physical space" (surface pixels). + // An oversampling is done as the pattern may not pixel align with the final surface. + // The cairo surface is created when the DrawingContext is declared. + + // Oversample the pattern + // TODO: find optimum value + // TODO: this is lame. instead of using descrim(), we should extract + // the scaling component from the complete matrix and use it + // to find the optimum tile size for rendering + // c is number of pixels in buffer x and y. + // Scale factor of 1.1 is too small... see bug #1251039 + Geom::Point c(pattern_tile.dimensions() * ps2user.descrim() * full.descrim() * 2.0); + + // Create drawing surface with size of pattern tile (in pattern space) but with number of pixels + // based on required resolution (c). + Inkscape::DrawingSurface pattern_surface(pattern_tile, c.ceil()); + Inkscape::DrawingContext dc(pattern_surface); + + pattern_tile *= pattern_surface.drawingTransform(); + Geom::IntRect one_tile = pattern_tile.roundOutwards(); + + // Render pattern. + if (needs_opacity) { + dc.pushGroup(); // this group is for pattern + opacity + } + + // TODO: make sure there are no leaks. + dc.transform(pattern_surface.drawingTransform().inverse()); + root->setTransform(content2ps * pattern_surface.drawingTransform()); + drawing.update(); + + // Render drawing to pattern_surface via drawing context, this calls root->render + // which is really DrawingItem->render(). + drawing.render(dc, one_tile); + for (auto& child: shown->children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_hide(dkey); + } + } + + // Uncomment to debug + // cairo_surface_t* raw = pattern_surface.raw(); + // std::cout << " cairo_surface (sp-pattern): " + // << " width: " << cairo_image_surface_get_width( raw ) + // << " height: " << cairo_image_surface_get_height( raw ) + // << std::endl; + // std::string filename = "sp-pattern-" + (std::string)getId() + ".png"; + // cairo_surface_write_to_png( pattern_surface.raw(), filename.c_str() ); + + if (needs_opacity) { + dc.popGroupToSource(); // pop raw pattern + dc.paint(opacity); // apply opacity + } + + // Apply transformation to user space. Also compensate for oversampling. + Geom::Affine raw_transform = ps2user.inverse() * pattern_surface.drawingTransform(); + + // Cairo doesn't like large values of x0 and y0. We can replace x0 and y0 by equivalent + // values close to zero (since one tile on a grid is the same as another it doesn't + // matter which tile is used as the base tile). + int w = one_tile[Geom::X].extent(); + int h = one_tile[Geom::Y].extent(); + int m = raw_transform[4] / w; + int n = raw_transform[5] / h; + raw_transform *= Geom::Translate( -m*w, -n*h ); + + cairo_pattern_t *cp = cairo_pattern_create_for_surface(pattern_surface.raw()); + ink_cairo_pattern_set_matrix(cp, raw_transform); + cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT); + + return cp; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-pattern.h b/src/object/sp-pattern.h new file mode 100644 index 0000000..a5fc3d0 --- /dev/null +++ b/src/object/sp-pattern.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG implementation + *//* + * Author: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_PATTERN_H +#define SEEN_SP_PATTERN_H + +#include +#include +#include +#include + +#include "svg/svg-length.h" +#include "sp-paint-server.h" +#include "uri-references.h" +#include "viewbox.h" + +class SPPatternReference; +class SPItem; + +namespace Inkscape { +namespace XML { + +class Node; +} +} + +#define SP_PATTERN(obj) (dynamic_cast((SPObject *)obj)) +#define SP_IS_PATTERN(obj) (dynamic_cast((SPObject *)obj) != NULL) + +class SPPattern : public SPPaintServer, public SPViewBox { +public: + enum PatternUnits { UNITS_USERSPACEONUSE, UNITS_OBJECTBOUNDINGBOX }; + + SPPattern(); + ~SPPattern() override; + + /* Reference (href) */ + Glib::ustring href; + SPPatternReference *ref; + + gdouble x() const; + gdouble y() const; + gdouble width() const; + gdouble height() const; + Geom::OptRect viewbox() const; + SPPattern::PatternUnits patternUnits() const; + SPPattern::PatternUnits patternContentUnits() const; + Geom::Affine const &getTransform() const; + SPPattern *rootPattern(); // TODO: const + + SPPattern *clone_if_necessary(SPItem *item, const gchar *property); + void transform_multiply(Geom::Affine postmul, bool set); + + /** + * @brief create a new pattern in XML tree + * @return created pattern id + */ + static const gchar *produce(const std::vector &reprs, Geom::Rect bounds, + SPDocument *document, Geom::Affine transform, Geom::Affine move); + + bool isValid() const override; + + cairo_pattern_t *pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) override; + +protected: + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, const gchar *value) override; + void update(SPCtx *ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + +private: + bool _hasItemChildren() const; + void _getChildren(std::list &l); + SPPattern *_chain() const; + + /** + Count how many times pattern is used by the styles of o and its descendants + */ + guint _countHrefs(SPObject *o) const; + + /** + Gets called when the pattern is reattached to another + */ + void _onRefChanged(SPObject *old_ref, SPObject *ref); + + /** + Gets called when the referenced is changed + */ + void _onRefModified(SPObject *ref, guint flags); + + /* patternUnits and patternContentUnits attribute */ + PatternUnits _pattern_units : 1; + bool _pattern_units_set : 1; + PatternUnits _pattern_content_units : 1; + bool _pattern_content_units_set : 1; + /* patternTransform attribute */ + Geom::Affine _pattern_transform; + bool _pattern_transform_set : 1; + /* Tile rectangle */ + SVGLength _x; + SVGLength _y; + SVGLength _width; + SVGLength _height; + + sigc::connection _modified_connection; +}; + + +class SPPatternReference : public Inkscape::URIReference { +public: + SPPatternReference(SPObject *obj) + : URIReference(obj) + { + } + + SPPattern *getObject() const + { + return reinterpret_cast(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject *obj) const override { + return SP_IS_PATTERN (obj)&& URIReference::_acceptObject(obj); + } +}; + +#endif // SEEN_SP_PATTERN_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-polygon.cpp b/src/object/sp-polygon.cpp new file mode 100644 index 0000000..ac47fca --- /dev/null +++ b/src/object/sp-polygon.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-polygon.h" +#include "display/curve.h" +#include +#include <2geom/curves.h> +#include "helper/geom-curves.h" +#include "svg/stringstream.h" +#include "xml/repr.h" +#include "document.h" + +SPPolygon::SPPolygon() : SPShape() { +} + +SPPolygon::~SPPolygon() = default; + +void SPPolygon::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPPolygon* object = this; + + SPShape::build(document, repr); + + object->readAttr( "points" ); +} + +/* + * sp_svg_write_polygon: Write points attribute for polygon tag. + * pathv may only contain paths with only straight line segments + * Return value: points attribute string. + */ +static gchar *sp_svg_write_polygon(Geom::PathVector const & pathv) +{ + Inkscape::SVGOStringStream os; + + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit) { + if ( is_straight_curve(*cit) ) + { + os << cit->finalPoint()[0] << "," << cit->finalPoint()[1] << " "; + } else { + g_error("sp_svg_write_polygon: polygon path contains non-straight line segments"); + } + } + } + + return g_strdup(os.str().c_str()); +} + +Inkscape::XML::Node* SPPolygon::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + // Tolerable workaround: we need to update the object's curve before we set points= + // because it's out of sync when e.g. some extension attrs of the polygon or star are changed in XML editor + this->set_shape(); + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:polygon"); + } + + /* We can safely write points here, because all subclasses require it too (Lauris) */ + /* While saving polygon element without points attribute _curve is NULL (see bug 1202753) */ + if (this->_curve != nullptr) { + gchar *str = sp_svg_write_polygon(this->_curve->get_pathvector()); + repr->setAttribute("points", str); + g_free(str); + } + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + + +static gboolean polygon_get_value(gchar const **p, gdouble *v) +{ + while (**p != '\0' && (**p == ',' || **p == '\x20' || **p == '\x9' || **p == '\xD' || **p == '\xA')) { + (*p)++; + } + + if (**p == '\0') { + return false; + } + + gchar *e = nullptr; + *v = g_ascii_strtod(*p, &e); + + if (e == *p) { + return false; + } + + *p = e; + + return true; +} + +void SPPolygon::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_POINTS: { + if (!value) { + /* fixme: The points attribute is required. We should handle its absence as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */ + break; + } + + SPCurve *curve = new SPCurve(); + gboolean hascpt = FALSE; + + gchar const *cptr = value; + bool has_error = false; + + while (TRUE) { + gdouble x; + + if (!polygon_get_value(&cptr, &x)) { + break; + } + + gdouble y; + + if (!polygon_get_value(&cptr, &y)) { + /* fixme: It is an error for an odd number of points to be specified. We + * should display the points up to now (as we currently do, though perhaps + * without the closepath: the spec isn't quite clear on whether to do a + * closepath or not, though I'd guess it's best not to do a closepath), but + * then flag the document as in error, as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + * + * (Ref: http://www.w3.org/TR/SVG11/shapes.html#PolygonElement.) */ + has_error = true; + break; + } + + if (hascpt) { + curve->lineto(x, y); + } else { + curve->moveto(x, y); + hascpt = TRUE; + } + } + + if (has_error || *cptr != '\0') { + /* TODO: Flag the document as in error, as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */ + } else if (hascpt) { + /* We might have done a moveto but no lineto. I'm not sure how we're supposed to represent + * a single-point polygon in SPCurve. TODO: add a testcase with only one coordinate pair */ + curve->closepath(); + } + + this->setCurve(curve); + curve->unref(); + break; + } + default: + SPShape::set(key, value); + break; + } +} + +gchar* SPPolygon::description() const { + return g_strdup(_("Polygon")); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-polygon.h b/src/object/sp-polygon.h new file mode 100644 index 0000000..afb1aa4 --- /dev/null +++ b/src/object/sp-polygon.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_POLYGON_H +#define SEEN_SP_POLYGON_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-shape.h" + +#define SP_POLYGON(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_POLYGON(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPPolygon : public SPShape { +public: + SPPolygon(); + ~SPPolygon() override; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void set(SPAttributeEnum key, char const* value) override; + char* description() const override; +}; + +// made 'public' so that SPCurve can set it as friend: +void sp_polygon_set(SPObject *object, unsigned int key, char const*value); + +#endif diff --git a/src/object/sp-polyline.cpp b/src/object/sp-polyline.cpp new file mode 100644 index 0000000..c75e4bc --- /dev/null +++ b/src/object/sp-polyline.cpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-polyline.h" +#include "display/curve.h" +#include +#include "xml/repr.h" +#include "document.h" + +SPPolyLine::SPPolyLine() : SPShape() { +} + +SPPolyLine::~SPPolyLine() = default; + +void SPPolyLine::build(SPDocument * document, Inkscape::XML::Node * repr) { + SPShape::build(document, repr); + + this->readAttr("points"); +} + +void SPPolyLine::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_POINTS: { + SPCurve * curve; + const gchar * cptr; + char * eptr; + gboolean hascpt; + + if (!value) { + break; + } + + curve = new SPCurve (); + hascpt = FALSE; + + cptr = value; + eptr = nullptr; + + while (TRUE) { + gdouble x, y; + + while (*cptr != '\0' && (*cptr == ',' || *cptr == '\x20' || *cptr == '\x9' || *cptr == '\xD' || *cptr == '\xA')) { + cptr++; + } + + if (!*cptr) { + break; + } + + x = g_ascii_strtod (cptr, &eptr); + + if (eptr == cptr) { + break; + } + + cptr = eptr; + + while (*cptr != '\0' && (*cptr == ',' || *cptr == '\x20' || *cptr == '\x9' || *cptr == '\xD' || *cptr == '\xA')) { + cptr++; + } + + if (!*cptr) { + break; + } + + y = g_ascii_strtod (cptr, &eptr); + + if (eptr == cptr) { + break; + } + + cptr = eptr; + + if (hascpt) { + curve->lineto(x, y); + } else { + curve->moveto(x, y); + hascpt = TRUE; + } + } + + this->setCurve(curve); + curve->unref(); + break; + } + default: + SPShape::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPPolyLine::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:polyline"); + } + + if (repr != this->getRepr()) { + repr->mergeFrom(this->getRepr(), "id"); + } + + SPShape::write(xml_doc, repr, flags); + + return repr; +} + +gchar* SPPolyLine::description() const { + return g_strdup(_("Polyline")); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-polyline.h b/src/object/sp-polyline.h new file mode 100644 index 0000000..005413b --- /dev/null +++ b/src/object/sp-polyline.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_POLYLINE_H +#define SEEN_SP_POLYLINE_H + +#include "sp-shape.h" + +#define SP_POLYLINE(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_POLYLINE(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPPolyLine : public SPShape { +public: + SPPolyLine(); + ~SPPolyLine() override; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void set(SPAttributeEnum key, char const* value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + char* description() const override; +}; + +#endif // SEEN_SP_POLYLINE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-radial-gradient.cpp b/src/object/sp-radial-gradient.cpp new file mode 100644 index 0000000..7981714 --- /dev/null +++ b/src/object/sp-radial-gradient.cpp @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include + +#include "sp-radial-gradient.h" + +#include "attributes.h" +#include "style.h" +#include "xml/repr.h" + +#include <2geom/transforms.h> + +/* + * Radial Gradient + */ +SPRadialGradient::SPRadialGradient() : SPGradient() { + this->cx.unset(SVGLength::PERCENT, 0.5, 0.5); + this->cy.unset(SVGLength::PERCENT, 0.5, 0.5); + this->r.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fx.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fy.unset(SVGLength::PERCENT, 0.5, 0.5); + this->fr.unset(SVGLength::PERCENT, 0.5, 0.5); +} + +SPRadialGradient::~SPRadialGradient() = default; + +/** + * Set radial gradient attributes from associated repr. + */ +void SPRadialGradient::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPGradient::build(document, repr); + + this->readAttr( "cx" ); + this->readAttr( "cy" ); + this->readAttr( "r" ); + this->readAttr( "fx" ); + this->readAttr( "fy" ); + this->readAttr( "fr" ); +} + +/** + * Set radial gradient attribute. + */ +void SPRadialGradient::set(SPAttributeEnum key, gchar const *value) { + + switch (key) { + case SP_ATTR_CX: + if (!this->cx.read(value)) { + this->cx.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + if (!this->fx._set) { + this->fx.value = this->cx.value; + this->fx.computed = this->cx.computed; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_CY: + if (!this->cy.read(value)) { + this->cy.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + if (!this->fy._set) { + this->fy.value = this->cy.value; + this->fy.computed = this->cy.computed; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_R: + if (!this->r.read(value)) { + this->r.unset(SVGLength::PERCENT, 0.5, 0.5); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_FX: + if (!this->fx.read(value)) { + this->fx.unset(this->cx.unit, this->cx.value, this->cx.computed); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_FY: + if (!this->fy.read(value)) { + this->fy.unset(this->cy.unit, this->cy.value, this->cy.computed); + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_FR: + if (!this->fr.read(value)) { + this->fr.unset(SVGLength::PERCENT, 0.0, 0.0); + } + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPGradient::set(key, value); + break; + } +} + +void +SPRadialGradient::update(SPCtx *ctx, guint flags) +{ + // To do: Verify flags. + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + SPItemCtx const *ictx = reinterpret_cast(ctx); + + if (getUnits() == SP_GRADIENT_UNITS_USERSPACEONUSE) { + double w = ictx->viewport.width(); + double h = ictx->viewport.height(); + double d = sqrt ((w*w + h*h)/2.0); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->cx.update(em, ex, w); + this->cy.update(em, ex, h); + this->r.update(em, ex, d); + this->fx.update(em, ex, w); + this->fy.update(em, ex, h); + this->fr.update(em, ex, d); + } + } +} + +/** + * Write radial gradient attributes to associated repr. + */ +Inkscape::XML::Node* SPRadialGradient::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:radialGradient"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->cx._set) { + sp_repr_set_svg_double(repr, "cx", this->cx.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->cy._set) { + sp_repr_set_svg_double(repr, "cy", this->cy.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->r._set) { + sp_repr_set_svg_double(repr, "r", this->r.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fx._set) { + sp_repr_set_svg_double(repr, "fx", this->fx.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fy._set) { + sp_repr_set_svg_double(repr, "fy", this->fy.computed); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->fr._set) { + sp_repr_set_svg_double(repr, "fr", this->fr.computed); + } + + SPGradient::write(xml_doc, repr, flags); + + return repr; +} + +cairo_pattern_t* SPRadialGradient::pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) { + this->ensureVector(); + + Geom::Point focus(this->fx.computed, this->fy.computed); + Geom::Point center(this->cx.computed, this->cy.computed); + + double radius = this->r.computed; + double focusr = this->fr.computed; + double scale = 1.0; + double tolerance = cairo_get_tolerance(ct); + + // NOTE: SVG2 will allow the use of a focus circle which can + // have its center outside the first circle. + + // code below suggested by Cairo devs to overcome tolerance problems + // more: https://bugs.freedesktop.org/show_bug.cgi?id=40918 + + // Corrected for + // https://bugs.launchpad.net/inkscape/+bug/970355 + + Geom::Affine gs2user = this->gradientTransform; + + if (this->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX && bbox) { + Geom::Affine bbox2user(bbox->width(), 0, 0, bbox->height(), bbox->left(), bbox->top()); + gs2user *= bbox2user; + } + + // we need to use vectors with the same direction to represent the transformed + // radius and the focus-center delta, because gs2user might contain non-uniform scaling + Geom::Point d(focus - center); + Geom::Point d_user(d.length(), 0); + Geom::Point r_user(radius, 0); + Geom::Point fr_user(focusr, 0); + d_user *= gs2user.withoutTranslation(); + r_user *= gs2user.withoutTranslation(); + fr_user *= gs2user.withoutTranslation(); + + double dx = d_user.x(), dy = d_user.y(); + cairo_user_to_device_distance(ct, &dx, &dy); + + // compute the tolerance distance in user space + // create a vector with the same direction as the transformed d, + // with the length equal to tolerance + double dl = hypot(dx, dy); + double tx = tolerance * dx / dl, ty = tolerance * dy / dl; + cairo_device_to_user_distance(ct, &tx, &ty); + double tolerance_user = hypot(tx, ty); + + if (d_user.length() + tolerance_user > r_user.length()) { + scale = r_user.length() / d_user.length(); + + // nudge the focus slightly inside + scale *= 1.0 - 2.0 * tolerance / dl; + } + + cairo_pattern_t *cp = cairo_pattern_create_radial( + scale * d.x() + center.x(), scale * d.y() + center.y(), focusr, + center.x(), center.y(), radius); + + sp_gradient_pattern_common_setup(cp, this, bbox, opacity); + + return cp; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-radial-gradient.h b/src/object/sp-radial-gradient.h new file mode 100644 index 0000000..72c4dec --- /dev/null +++ b/src/object/sp-radial-gradient.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_RADIAL_GRADIENT_H +#define SP_RADIAL_GRADIENT_H + +/** \file + * SPRadialGradient: SVG implementtion. + */ + +#include "sp-gradient.h" +#include "svg/svg-length.h" + +typedef struct _cairo cairo_t; +typedef struct _cairo_pattern cairo_pattern_t; + +#define SP_RADIALGRADIENT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_RADIALGRADIENT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +/** Radial gradient. */ +class SPRadialGradient : public SPGradient { +public: + SPRadialGradient(); + ~SPRadialGradient() override; + + SVGLength cx; + SVGLength cy; + SVGLength r; + SVGLength fx; + SVGLength fy; + SVGLength fr; // Focus radius. Added in SVG 2 + + cairo_pattern_t* pattern_new(cairo_t *ct, Geom::OptRect const &bbox, double opacity) override; + +protected: + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void set(SPAttributeEnum key, char const *value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif /* !SP_RADIAL_GRADIENT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-rect.cpp b/src/object/sp-rect.cpp new file mode 100644 index 0000000..fda6ee4 --- /dev/null +++ b/src/object/sp-rect.cpp @@ -0,0 +1,652 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * bulia byak + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" + +#include "inkscape.h" +#include "document.h" +#include "attributes.h" +#include "style.h" +#include "sp-rect.h" +#include "sp-guide.h" +#include "preferences.h" +#include "svg/svg.h" +#include + +#define noRECT_VERBOSE + +//#define OBJECT_TRACE + +SPRect::SPRect() : SPShape() { +} + +SPRect::~SPRect() = default; + +void SPRect::build(SPDocument* doc, Inkscape::XML::Node* repr) { +#ifdef OBJECT_TRACE + objectTrace( "SPRect::build" ); +#endif + + SPShape::build(doc, repr); + + this->readAttr("x"); + this->readAttr("y"); + this->readAttr("width"); + this->readAttr("height"); + this->readAttr("rx"); + this->readAttr("ry"); + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::build", false ); +#endif +} + +void SPRect::set(SPAttributeEnum key, gchar const *value) { + +#ifdef OBJECT_TRACE + std::stringstream temp; + temp << "SPRect::set: " << key << " " << (value?value:"null"); + objectTrace( temp.str() ); +#endif + + /* fixme: We need real error processing some time */ + + // We must update the SVGLengths immediately or nodes may be misplaced after they are moved. + double const w = viewport.width(); + double const h = viewport.height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; + + switch (key) { + case SP_ATTR_X: + this->x.readOrUnset(value); + this->x.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + this->y.readOrUnset(value); + this->y.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_WIDTH: + if (!this->width.read(value) || this->width.value < 0.0) { + this->width.unset(); + } + this->width.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HEIGHT: + if (!this->height.read(value) || this->height.value < 0.0) { + this->height.unset(); + } + this->height.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_RX: + if (!this->rx.read(value) || this->rx.value <= 0.0) { + this->rx.unset(); + } + this->rx.update( em, ex, w ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_RY: + if (!this->ry.read(value) || this->ry.value <= 0.0) { + this->ry.unset(); + } + this->ry.update( em, ex, h ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + default: + SPShape::set(key, value); + break; + } +#ifdef OBJECT_TRACE + objectTrace( "SPRect::set", false ); +#endif +} + +void SPRect::update(SPCtx* ctx, unsigned int flags) { + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::update", true, flags ); +#endif + + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + SPItemCtx const *ictx = reinterpret_cast(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + this->x.update(em, ex, w); + this->y.update(em, ex, h); + this->width.update(em, ex, w); + this->height.update(em, ex, h); + this->rx.update(em, ex, w); + this->ry.update(em, ex, h); + this->set_shape(); + + flags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; // since we change the description, it's not a "just translation" anymore + } + + SPShape::update(ctx, flags); +#ifdef OBJECT_TRACE + objectTrace( "SPRect::update", false, flags ); +#endif +} + +Inkscape::XML::Node * SPRect::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::write", true, flags ); +#endif + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:rect"); + } + if (this->hasPathEffectOnClipOrMaskRecursive(this) && repr && strcmp(repr->name(), "svg:rect") == 0) { + repr->setCodeUnsafe(g_quark_from_string("svg:path")); + repr->setAttribute("sodipodi:type", "rect"); + } + sp_repr_set_svg_length(repr, "width", this->width); + sp_repr_set_svg_length(repr, "height", this->height); + + if (this->rx._set) { + sp_repr_set_svg_length(repr, "rx", this->rx); + } + + if (this->ry._set) { + sp_repr_set_svg_length(repr, "ry", this->ry); + } + + sp_repr_set_svg_length(repr, "x", this->x); + sp_repr_set_svg_length(repr, "y", this->y); + // write d= + if (strcmp(repr->name(), "svg:rect") != 0) { + set_rect_path_attribute(repr); // include set_shape() + } else { + this->set_shape(); // evaluate SPCurve + } + SPShape::write(xml_doc, repr, flags); + +#ifdef OBJECT_TRACE + objectTrace( "SPRect::write", false, flags ); +#endif + + return repr; +} + +const char* SPRect::displayName() const { + return _("Rectangle"); +} + +#define C1 0.554 + +void SPRect::set_shape() { + if (hasBrokenPathEffect()) { + g_warning("The rect shape has unknown LPE on it!"); + + if (this->getRepr()->attribute("d")) { + // unconditionally read the curve from d, if any, to preserve appearance + Geom::PathVector pv = sp_svg_read_pathv(this->getRepr()->attribute("d")); + SPCurve *cold = new SPCurve(pv); + this->setCurveInsync(cold); + this->setCurveBeforeLPE( cold ); + cold->unref(); + } + + return; + } + if ((this->height.computed < 1e-18) || (this->width.computed < 1e-18)) { + this->setCurveInsync(nullptr); + this->setCurveBeforeLPE(nullptr); + return; + } + + SPCurve *c = new SPCurve(); + + double const x = this->x.computed; + double const y = this->y.computed; + double const w = this->width.computed; + double const h = this->height.computed; + double const w2 = w / 2; + double const h2 = h / 2; + double const rx = std::min(( this->rx._set + ? this->rx.computed + : ( this->ry._set + ? this->ry.computed + : 0.0 ) ), + .5 * this->width.computed); + double const ry = std::min(( this->ry._set + ? this->ry.computed + : ( this->rx._set + ? this->rx.computed + : 0.0 ) ), + .5 * this->height.computed); + /* TODO: Handle negative rx or ry as per + * http://www.w3.org/TR/SVG11/shapes.html#RectElementRXAttribute once Inkscape has proper error + * handling (see http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing). + */ + + /* We don't use proper circular/elliptical arcs, but bezier curves can approximate a 90-degree + * arc fairly well. + */ + if ((rx > 1e-18) && (ry > 1e-18)) { + c->moveto(x + rx, y); + + if (rx < w2) { + c->lineto(x + w - rx, y); + } + + c->curveto(x + w - rx * (1 - C1), y, x + w, y + ry * (1 - C1), x + w, y + ry); + + if (ry < h2) { + c->lineto(x + w, y + h - ry); + } + + c->curveto(x + w, y + h - ry * (1 - C1), x + w - rx * (1 - C1), y + h, x + w - rx, y + h); + + if (rx < w2) { + c->lineto(x + rx, y + h); + } + + c->curveto(x + rx * (1 - C1), y + h, x, y + h - ry * (1 - C1), x, y + h - ry); + + if (ry < h2) { + c->lineto(x, y + ry); + } + + c->curveto(x, y + ry * (1 - C1), x + rx * (1 - C1), y, x + rx, y); + } else { + c->moveto(x + 0.0, y + 0.0); + c->lineto(x + w, y + 0.0); + c->lineto(x + w, y + h); + c->lineto(x + 0.0, y + h); + } + + c->closepath(); + + + /* Reset the shape's curve to the "original_curve" + * This is very important for LPEs to work properly! (the bbox might be recalculated depending on the curve in shape)*/ + SPCurve * before = this->getCurveBeforeLPE(); + bool haslpe = this->hasPathEffectOnClipOrMaskRecursive(this); + if (before || haslpe) { + if (c && before && before->get_pathvector() != c->get_pathvector()){ + this->setCurveBeforeLPE(c); + sp_lpe_item_update_patheffect(this, true, false); + } else if(haslpe) { + this->setCurveBeforeLPE(c); + } else { + //This happends on undo, fix bug:#1791784 + this->setCurveInsync(c); + } + } else { + this->setCurveInsync(c); + } + if (before) { + before->unref(); + } + if (this->hasPathEffectOnClipOrMaskRecursive(this)) { + Inkscape::XML::Node *rectrepr = this->getRepr(); + if (strcmp(rectrepr->name(), "svg:rect") == 0) { + sp_lpe_item_update_patheffect(this, true, false); + this->write(rectrepr->document(), rectrepr, SP_OBJECT_MODIFIED_FLAG); + } + } + c->unref(); +} + +bool SPRect::set_rect_path_attribute(Inkscape::XML::Node *repr) +{ + // Make sure our pathvector is up to date. + this->set_shape(); + + if (_curve) { + gchar *d = sp_svg_write_path(_curve->get_pathvector()); + + repr->setAttribute("d", d); + + g_free(d); + } else { + repr->removeAttribute("d"); + } + + return true; +} + +void SPRect::modified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->set_shape(); + } + + SPShape::modified(flags); +} + +/* fixme: Think (Lauris) */ + +void SPRect::setPosition(gdouble x, gdouble y, gdouble width, gdouble height) { + this->x = x; + this->y = y; + this->width = width; + this->height = height; + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::setRx(bool set, gdouble value) { + this->rx._set = set; + + if (set) { + this->rx = value; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::setRy(bool set, gdouble value) { + this->ry._set = set; + + if (set) { + this->ry = value; + } + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPRect::update_patheffect(bool write) { + SPShape::update_patheffect(write); +} + +Geom::Affine SPRect::set_transform(Geom::Affine const& xform) { + if (pathEffectsEnabled() && !optimizeTransforms()) { + return xform; + } + notifyTransform(xform); + /* Calculate rect start in parent coords. */ + Geom::Point pos(Geom::Point(this->x.computed, this->y.computed) * xform); + + /* This function takes care of translation and scaling, we return whatever parts we can't + handle. */ + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + gdouble const sw = hypot(ret[0], ret[1]); + gdouble const sh = hypot(ret[2], ret[3]); + + if (sw > 1e-9) { + ret[0] /= sw; + ret[1] /= sw; + } else { + ret[0] = 1.0; + ret[1] = 0.0; + } + + if (sh > 1e-9) { + ret[2] /= sh; + ret[3] /= sh; + } else { + ret[2] = 0.0; + ret[3] = 1.0; + } + + /* Preserve units */ + this->width.scale( sw ); + this->height.scale( sh ); + + if (this->rx._set) { + this->rx.scale( sw ); + } + + if (this->ry._set) { + this->ry.scale( sh ); + } + + /* Find start in item coords */ + pos = pos * ret.inverse(); + this->x = pos[Geom::X]; + this->y = pos[Geom::Y]; + + this->set_shape(); + + // Adjust stroke width + this->adjust_stroke(sqrt(fabs(sw * sh))); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + + +/** +Returns the ratio in which the vector from p0 to p1 is stretched by transform + */ +gdouble SPRect::vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform) { + if (p0 == p1) { + return 0; + } + + return (Geom::distance(p0 * xform, p1 * xform) / Geom::distance(p0, p1)); +} + +void SPRect::setVisibleRx(gdouble rx) { + if (rx == 0) { + this->rx.unset(); + } else { + this->rx = rx / SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +void SPRect::setVisibleRy(gdouble ry) { + if (ry == 0) { + this->ry.unset(); + } else { + this->ry = ry / SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + } + + this->updateRepr(); +} + +gdouble SPRect::getVisibleRx() const { + if (!this->rx._set) { + return 0; + } + + return this->rx.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +gdouble SPRect::getVisibleRy() const { + if (!this->ry._set) { + return 0; + } + + return this->ry.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +Geom::Rect SPRect::getRect() const { + Geom::Point p0 = Geom::Point(this->x.computed, this->y.computed); + Geom::Point p2 = Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed); + + return Geom::Rect(p0, p2); +} + +void SPRect::compensateRxRy(Geom::Affine xform) { + if (this->rx.computed == 0 && this->ry.computed == 0) { + return; // nothing to compensate + } + + // test unit vectors to find out compensation: + Geom::Point c(this->x.computed, this->y.computed); + Geom::Point cx = c + Geom::Point(1, 0); + Geom::Point cy = c + Geom::Point(0, 1); + + // apply previous transform if any + c *= this->transform; + cx *= this->transform; + cy *= this->transform; + + // find out stretches that we need to compensate + gdouble eX = SPRect::vectorStretch(cx, c, xform); + gdouble eY = SPRect::vectorStretch(cy, c, xform); + + // If only one of the radii is set, set both radii so they have the same visible length + // This is needed because if we just set them the same length in SVG, they might end up unequal because of transform + if ((this->rx._set && !this->ry._set) || (this->ry._set && !this->rx._set)) { + gdouble r = MAX(this->rx.computed, this->ry.computed); + this->rx = r / eX; + this->ry = r / eY; + } else { + this->rx = this->rx.computed / eX; + this->ry = this->ry.computed / eY; + } + + // Note that a radius may end up larger than half-side if the rect is scaled down; + // that's ok because this preserves the intended radii in case the rect is enlarged again, + // and set_shape will take care of trimming too large radii when generating d= +} + +void SPRect::setVisibleWidth(gdouble width) { + this->width = width / SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + + this->updateRepr(); +} + +void SPRect::setVisibleHeight(gdouble height) { + this->height = height / SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); + + this->updateRepr(); +} + +gdouble SPRect::getVisibleWidth() const { + if (!this->width._set) { + return 0; + } + + return this->width.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed + 1, this->y.computed), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +gdouble SPRect::getVisibleHeight() const { + if (!this->height._set) { + return 0; + } + + return this->height.computed * SPRect::vectorStretch( + Geom::Point(this->x.computed, this->y.computed + 1), + Geom::Point(this->x.computed, this->y.computed), + this->i2doc_affine()); +} + +void SPRect::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + /* This method overrides sp_shape_snappoints, which is the default for any shape. The default method + returns all eight points along the path of a rounded rectangle, but not the real corners. Snapping + the startpoint and endpoint of each rounded corner is not very useful and really confusing. Instead + we could snap either the real corners, or not snap at all. Bulia Byak opted to snap the real corners, + but it should be noted that this might be confusing in some cases with relatively large radii. With + small radii though the user will easily understand which point is snapping. */ + + Geom::Affine const i2dt (this->i2dt_affine ()); + + Geom::Point p0 = Geom::Point(this->x.computed, this->y.computed) * i2dt; + Geom::Point p1 = Geom::Point(this->x.computed, this->y.computed + this->height.computed) * i2dt; + Geom::Point p2 = Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed) * i2dt; + Geom::Point p3 = Geom::Point(this->x.computed + this->width.computed, this->y.computed) * i2dt; + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_RECT_CORNER)) { + p.emplace_back(p0, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p1, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p2, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + p.emplace_back(p3, Inkscape::SNAPSOURCE_RECT_CORNER, Inkscape::SNAPTARGET_RECT_CORNER); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_LINE_MIDPOINT)) { + p.emplace_back((p0 + p1)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p1 + p2)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p2 + p3)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + p.emplace_back((p3 + p0)/2, Inkscape::SNAPSOURCE_LINE_MIDPOINT, Inkscape::SNAPTARGET_LINE_MIDPOINT); + } + + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) { + p.emplace_back((p0 + p2)/2, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + } +} + +void SPRect::convert_to_guides() const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!prefs->getBool("/tools/shapes/rect/convertguides", true)) { + // Use bounding box instead of edges + SPShape::convert_to_guides(); + return; + } + + std::list > pts; + + Geom::Affine const i2dt(this->i2dt_affine()); + + Geom::Point A1(Geom::Point(this->x.computed, this->y.computed) * i2dt); + Geom::Point A2(Geom::Point(this->x.computed, this->y.computed + this->height.computed) * i2dt); + Geom::Point A3(Geom::Point(this->x.computed + this->width.computed, this->y.computed + this->height.computed) * i2dt); + Geom::Point A4(Geom::Point(this->x.computed + this->width.computed, this->y.computed) * i2dt); + + pts.emplace_back(A1, A2); + pts.emplace_back(A2, A3); + pts.emplace_back(A3, A4); + pts.emplace_back(A4, A1); + + sp_guide_pt_pairs_to_guides(this->document, pts); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-rect.h b/src/object/sp-rect.h new file mode 100644 index 0000000..3aeecc9 --- /dev/null +++ b/src/object/sp-rect.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_RECT_H +#define SEEN_SP_RECT_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +#include "svg/svg-length.h" +#include "sp-shape.h" + +#define SP_RECT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_RECT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPRect : public SPShape { +public: + SPRect(); + ~SPRect() override; + + void setPosition(double x, double y, double width, double height); + + /* If SET if FALSE, VALUE is just ignored */ + void setRx(bool set, double value); + void setRy(bool set, double value); + + double getVisibleRx() const; + void setVisibleRx(double rx); + + double getVisibleRy() const; + void setVisibleRy(double ry); + + Geom::Rect getRect() const; + + double getVisibleWidth() const; + void setVisibleWidth(double rx); + + double getVisibleHeight() const; + void setVisibleHeight(double ry); + + void compensateRxRy(Geom::Affine xform); + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + + void set(SPAttributeEnum key, char const *value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + bool set_rect_path_attribute(Inkscape::XML::Node *repr); + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + const char* displayName() const override; + void update_patheffect(bool write) override; + void set_shape() override; + Geom::Affine set_transform(Geom::Affine const& xform) override; + + void snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const override; + void convert_to_guides() const override; + + SVGLength x; + SVGLength y; + SVGLength width; + SVGLength height; + SVGLength rx; + SVGLength ry; + +private: + static double vectorStretch(Geom::Point p0, Geom::Point p1, Geom::Affine xform); +}; + +#endif // SEEN_SP_RECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-root.cpp b/src/object/sp-root.cpp new file mode 100644 index 0000000..7980c0f --- /dev/null +++ b/src/object/sp-root.cpp @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG \ implementation. + */ +/* + * Authors: + * Lauris Kaplinski + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include <2geom/transforms.h> + +#include "attributes.h" +#include "print.h" +#include "document.h" +#include "inkscape-version.h" +#include "sp-defs.h" +#include "sp-root.h" +#include "display/drawing-group.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "xml/repr.h" +#include "util/units.h" + +SPRoot::SPRoot() : SPGroup(), SPViewBox() +{ + this->onload = nullptr; + + static Inkscape::Version const zero_version(0, 0); + + sp_version_from_string(SVG_VERSION, &this->original.svg); + this->version.svg = zero_version; + this->original.svg = zero_version; + this->version.inkscape = zero_version; + this->original.inkscape = zero_version; + + this->unset_x_and_y(); + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + + this->defs = nullptr; +} + +SPRoot::~SPRoot() += default; + +void SPRoot::unset_x_and_y() +{ + this->x.unset(SVGLength::PERCENT, 0.0, 0.0); // Ignored for root SVG element + this->y.unset(SVGLength::PERCENT, 0.0, 0.0); +} + +void SPRoot::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + //XML Tree being used directly here while it shouldn't be. + if (!this->getRepr()->attribute("version")) { + repr->setAttribute("version", SVG_VERSION); + } + + this->readAttr("version"); + this->readAttr("inkscape:version"); + /* It is important to parse these here, so objects will have viewport build-time */ + this->readAttr("x"); + this->readAttr("y"); + this->readAttr("width"); + this->readAttr("height"); + this->readAttr("viewBox"); + this->readAttr("preserveAspectRatio"); + this->readAttr("onload"); + + SPGroup::build(document, repr); + + // Search for first node + for (auto& o: children) { + if (SP_IS_DEFS(&o)) { + this->defs = SP_DEFS(&o); + break; + } + } + + // clear transform, if any was read in - SVG does not allow transform= on + SP_ITEM(this)->transform = Geom::identity(); +} + +void SPRoot::release() +{ + this->defs = nullptr; + + SPGroup::release(); +} + + +void SPRoot::set(SPAttributeEnum key, const gchar *value) +{ + switch (key) { + case SP_ATTR_VERSION: + if (!sp_version_from_string(value, &this->version.svg)) { + this->version.svg = this->original.svg; + } + break; + + case SP_ATTR_INKSCAPE_VERSION: + if (!sp_version_from_string(value, &this->version.inkscape)) { + this->version.inkscape = this->original.inkscape; + } + break; + + case SP_ATTR_X: + /* Valid for non-root SVG elements; ex, em not handled correctly. */ + if (!this->x.read(value)) { + this->x.unset(SVGLength::PERCENT, 0.0, 0.0); + } + + /* fixme: I am almost sure these do not require viewport flag (Lauris) */ + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + /* Valid for non-root SVG elements; ex, em not handled correctly. */ + if (!this->y.read(value)) { + this->y.unset(SVGLength::PERCENT, 0.0, 0.0); + } + + /* fixme: I am almost sure these do not require viewport flag (Lauris) */ + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_WIDTH: + if (!this->width.read(value) || !(this->width.computed > 0.0)) { + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_HEIGHT: + if (!this->height.read(value) || !(this->height.computed > 0.0)) { + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + } + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_VIEWBOX: + set_viewBox( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_ONLOAD: + this->onload = (char *) value; + break; + + default: + /* Pass the set event to the parent */ + SPGroup::set(key, value); + break; + } +} + +void SPRoot::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) +{ + SPGroup::child_added(child, ref); + + SPObject *co = this->document->getObjectByRepr(child); + // NOTE: some XML nodes do not have corresponding SP objects, + // for instance inkscape:clipboard used in the clipboard code. + // See LP bug #1227827 + //g_assert (co != NULL || !strcmp("comment", child->name())); // comment repr node has no object + + if (co && SP_IS_DEFS(co)) { + // We search for first node - it is not beautiful, but works + for (auto& c: children) { + if (SP_IS_DEFS(&c)) { + this->defs = SP_DEFS(&c); + break; + } + } + } +} + +void SPRoot::remove_child(Inkscape::XML::Node *child) +{ + if (this->defs && (this->defs->getRepr() == child)) { + SPObject *iter = nullptr; + + // We search for first remaining node - it is not beautiful, but works + for (auto& child: children) { + iter = &child; + if (SP_IS_DEFS(iter) && (SPDefs *)iter != this->defs) { + this->defs = (SPDefs *)iter; + break; + } + } + + if (!iter) { + /* we should probably create a new here? */ + this->defs = nullptr; + } + } + + SPGroup::remove_child(child); +} + +void SPRoot::setRootDimensions() +{ + /* + * This is the root SVG element: + * + * x, y, width, and height apply to positioning the SVG element inside a parent. + * For the root SVG in Inkscape there is no parent, thus special rules apply: + * If width, height not set, width = 100%, height = 100% (as always). + * If width and height are in percent, they are percent of viewBox width/height. + * If width, height, and viewBox are not set... pick "random" width/height. + * x, y are ignored. + * initial viewport = (0 0 width height) + */ + if( this->viewBox_set ) { + + if( this->width._set ) { + // Check if this is necessary + if (this->width.unit == SVGLength::PERCENT) { + this->width.computed = this->width.value * this->viewBox.width(); + } + } else { + this->width.set( SVGLength::PX, this->viewBox.width(), this->viewBox.width() ); + } + + if( this->height._set ) { + if (this->height.unit == SVGLength::PERCENT) { + this->height.computed = this->height.value * this->viewBox.height(); + } + } else { + this->height.set(SVGLength::PX, this->viewBox.height(), this->viewBox.height() ); + } + + } else { + + if( !this->width._set || this->width.unit == SVGLength::PERCENT) { + this->width.set( SVGLength::PX, 300, 300 ); // CSS/SVG default + } + + if( !this->height._set || this->height.unit == SVGLength::PERCENT) { + this->height.set( SVGLength::PX, 150, 150 ); // CSS/SVG default + } + } + + // Ignore x, y values for root element + this->unset_x_and_y(); +} + +void SPRoot::update(SPCtx *ctx, guint flags) +{ + SPItemCtx const *ictx = (SPItemCtx const *) ctx; + + if( !this->parent ) { + this->setRootDimensions(); + } + + // Calculate x, y, width, height from parent/initial viewport + this->calcDimsFromParentViewport(ictx); + + // std::cout << "SPRoot::update: final:" + // << " x: " << x.computed + // << " y: " << y.computed + // << " width: " << width.computed + // << " height: " << height.computed << std::endl; + + // Calculate new viewport + SPItemCtx rctx = *ictx; + rctx.viewport = Geom::Rect::from_xywh( this->x.computed, this->y.computed, + this->width.computed, this->height.computed ); + rctx = get_rctx( &rctx, Inkscape::Util::Quantity::convert(1, this->document->getDisplayUnit(), "px") ); + + /* And invoke parent method */ + SPGroup::update((SPCtx *) &rctx, flags); + + /* As last step set additional transform of drawing group */ + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + g->setChildTransform(this->c2p); + } +} + +void SPRoot::modified(unsigned int flags) +{ + SPGroup::modified(flags); + + /* fixme: (Lauris) */ + if (!this->parent && (flags & SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + this->document->emitResizedSignal(this->width.computed, this->height.computed); + } +} + + +Inkscape::XML::Node *SPRoot::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:svg"); + } + + /* Only update version string on successful write to file. This is handled by 'file_save()'. + * if (flags & SP_OBJECT_WRITE_EXT) { + * repr->setAttribute("inkscape:version", Inkscape::version_string); + * } + */ + + if (!repr->attribute("version")) { + gchar *myversion = sp_version_to_string(this->version.svg); + repr->setAttribute("version", myversion); + g_free(myversion); + } + + if (fabs(this->x.computed) > 1e-9) { + sp_repr_set_svg_double(repr, "x", this->x.computed); + } + + if (fabs(this->y.computed) > 1e-9) { + sp_repr_set_svg_double(repr, "y", this->y.computed); + } + + /* Unlike all other SPObject, here we want to preserve absolute units too (and only here, + * according to the recommendation in http://www.w3.org/TR/SVG11/coords.html#Units). + */ + repr->setAttribute("width", sp_svg_length_write_with_units(this->width)); + repr->setAttribute("height", sp_svg_length_write_with_units(this->height)); + + if (this->viewBox_set) { + Inkscape::SVGOStringStream os; + os << this->viewBox.left() << " " << this->viewBox.top() << " " + << this->viewBox.width() << " " << this->viewBox.height(); + + repr->setAttribute("viewBox", os.str()); + } + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem *SPRoot::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) +{ + Inkscape::DrawingItem *ai = SPGroup::show(drawing, key, flags); + + if (ai) { + Inkscape::DrawingGroup *g = dynamic_cast(ai); + g->setChildTransform(this->c2p); + } + + // Uncomment to print out XML tree + // getRepr()->recursivePrintTree(0); + + // Uncomment to print out SP Object tree + // recursivePrintTree(0); + + // Uncomment to print out Display Item tree + // ai->recursivePrintTree(0); + + return ai; +} + +void SPRoot::print(SPPrintContext *ctx) +{ + ctx->bind(this->c2p, 1.0); + + SPGroup::print(ctx); + + ctx->release(); +} + +const char *SPRoot::displayName() const { + return "SVG"; // Do not translate +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-root.h b/src/object/sp-root.h new file mode 100644 index 0000000..310f878 --- /dev/null +++ b/src/object/sp-root.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SPRoot: SVG \ implementation. + */ +/* + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_ROOT_H_SEEN +#define SP_ROOT_H_SEEN + +#include "version.h" +#include "svg/svg-length.h" +#include "sp-item-group.h" +#include "viewbox.h" +#include "sp-dimensions.h" + +#define SP_ROOT(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_ROOT(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPDefs; + +/** \ element */ +class SPRoot : public SPGroup, public SPViewBox, public SPDimensions { +public: + SPRoot(); + ~SPRoot() override; + + struct { + Inkscape::Version svg; + Inkscape::Version inkscape; + } version, original; + + char *onload; + + /** + * Primary \ element where we put new defs (patterns, gradients etc.). + * + * At the time of writing, this is chosen as the first \ child of + * this \ element: see writers of this member in sp-root.cpp. + */ + SPDefs *defs; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void modified(unsigned int flags) override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void print(SPPrintContext *ctx) override; + const char* displayName() const override; +private: + void unset_x_and_y(); + void setRootDimensions(); +}; + +#endif /* !SP_ROOT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-script.cpp b/src/object/sp-script.cpp new file mode 100644 index 0000000..d965421 --- /dev/null +++ b/src/object/sp-script.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG %d object"), _("of %d objects"), len), len); +} + +void SPSwitch::child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) { + SPGroup::child_added(child, ref); + + this->_reevaluate(true); +} + +void SPSwitch::remove_child(Inkscape::XML::Node *child) { + SPGroup::remove_child(child); + + this->_reevaluate(); +} + +void SPSwitch::order_changed (Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) +{ + SPGroup::order_changed(child, old_ref, new_ref); + + this->_reevaluate(); +} + +void SPSwitch::_reevaluate(bool /*add_to_drawing*/) { + SPObject *evaluated_child = _evaluateFirst(); + if (!evaluated_child || _cached_item == evaluated_child) { + return; + } + + _releaseLastItem(_cached_item); + + std::vector item_list = _childList(false, SPObject::ActionShow); + for ( std::vector::const_reverse_iterator iter=item_list.rbegin();iter!=item_list.rend();++iter) { + SPObject *o = *iter; + if ( !SP_IS_ITEM (o) ) { + continue; + } + + SPItem * child = SP_ITEM(o); + child->setEvaluated(o == evaluated_child); + } + + _cached_item = evaluated_child; + _release_connection = evaluated_child->connectRelease(sigc::bind(sigc::ptr_fun(&SPSwitch::_releaseItem), this)); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); +} + +void SPSwitch::_releaseItem(SPObject *obj, SPSwitch *selection) +{ + selection->_releaseLastItem(obj); +} + +void SPSwitch::_releaseLastItem(SPObject *obj) +{ + if (nullptr == this->_cached_item || this->_cached_item != obj) + return; + + this->_release_connection.disconnect(); + this->_cached_item = nullptr; +} + +void SPSwitch::_showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) { + SPObject *evaluated_child = this->_evaluateFirst(); + + std::vector l = this->_childList(false, SPObject::ActionShow); + + for ( std::vector::const_reverse_iterator iter=l.rbegin();iter!=l.rend();++iter) { + SPObject *o = *iter; + + if (SP_IS_ITEM (o)) { + SPItem * child = SP_ITEM(o); + child->setEvaluated(o == evaluated_child); + Inkscape::DrawingItem *ac = child->invoke_show (drawing, key, flags); + + if (ac) { + ai->appendChild(ac); + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-switch.h b/src/object/sp-switch.h new file mode 100644 index 0000000..0aecd60 --- /dev/null +++ b/src/object/sp-switch.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SWITCH_H +#define SEEN_SP_SWITCH_H + +/* + * SVG implementation + * + * Authors: + * Andrius R. + * + * Copyright (C) 2006 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "sp-item-group.h" + + +#define SP_SWITCH(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_SWITCH(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPSwitch : public SPGroup { +public: + SPSwitch(); + ~SPSwitch() override; + + void resetChildEvaluated() { _reevaluate(); } + + std::vector _childList(bool add_ref, SPObject::Action action); + void _showChildren (Inkscape::Drawing &drawing, Inkscape::DrawingItem *ai, unsigned int key, unsigned int flags) override; + + SPObject *_evaluateFirst(); + void _reevaluate(bool add_to_arena = false); + static void _releaseItem(SPObject *obj, SPSwitch *selection); + void _releaseLastItem(SPObject *obj); + + SPObject *_cached_item; + sigc::connection _release_connection; + + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node *child) override; + void order_changed(Inkscape::XML::Node *child, Inkscape::XML::Node *old_ref, Inkscape::XML::Node *new_ref) override; + const char* displayName() const override; + gchar *description() const override; +}; + +#endif diff --git a/src/object/sp-symbol.cpp b/src/object/sp-symbol.cpp new file mode 100644 index 0000000..dda078d --- /dev/null +++ b/src/object/sp-symbol.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include <2geom/transforms.h> +#include "display/drawing-group.h" +#include "xml/repr.h" +#include "attributes.h" +#include "print.h" +#include "sp-symbol.h" +#include "document.h" + +SPSymbol::SPSymbol() : SPGroup(), SPViewBox() { +} + +SPSymbol::~SPSymbol() = default; + +void SPSymbol::build(SPDocument *document, Inkscape::XML::Node *repr) { + this->readAttr( "viewBox" ); + this->readAttr( "preserveAspectRatio" ); + + SPGroup::build(document, repr); +} + +void SPSymbol::release() { + SPGroup::release(); +} + +void SPSymbol::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_VIEWBOX: + set_viewBox( value ); + // std::cout << "Symbol: ViewBox: " << viewBox << std::endl; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + case SP_ATTR_PRESERVEASPECTRATIO: + set_preserveAspectRatio( value ); + // std::cout << "Symbol: Preserve aspect ratio: " << aspect_align << ", " << aspect_clip << std::endl; + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG); + break; + + default: + SPGroup::set(key, value); + break; + } +} + +void SPSymbol::child_added(Inkscape::XML::Node *child, Inkscape::XML::Node *ref) { + SPGroup::child_added(child, ref); +} + + +void SPSymbol::update(SPCtx *ctx, guint flags) { + if (this->cloned) { + + SPItemCtx *ictx = (SPItemCtx *) ctx; + SPItemCtx rctx = get_rctx( ictx ); + + // And invoke parent method + SPGroup::update((SPCtx *) &rctx, flags); + + // As last step set additional transform of drawing group + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + g->setChildTransform(this->c2p); + } + } else { + // No-op + SPGroup::update(ctx, flags); + } +} + +void SPSymbol::modified(unsigned int flags) { + SPGroup::modified(flags); +} + + +Inkscape::XML::Node* SPSymbol::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:symbol"); + } + + //XML Tree being used directly here while it shouldn't be. + repr->setAttribute("viewBox", this->getRepr()->attribute("viewBox")); + + //XML Tree being used directly here while it shouldn't be. + repr->setAttribute("preserveAspectRatio", this->getRepr()->attribute("preserveAspectRatio")); + + SPGroup::write(xml_doc, repr, flags); + + return repr; +} + +Inkscape::DrawingItem* SPSymbol::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + Inkscape::DrawingItem *ai = nullptr; + + if (this->cloned) { + // Cloned is actually renderable + ai = SPGroup::show(drawing, key, flags); + Inkscape::DrawingGroup *g = dynamic_cast(ai); + + if (g) { + g->setChildTransform(this->c2p); + } + } + + return ai; +} + +void SPSymbol::hide(unsigned int key) { + if (this->cloned) { + /* Cloned is actually renderable */ + SPGroup::hide(key); + } +} + + +Geom::OptRect SPSymbol::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox; + + // We don't need a bounding box for Symbols dialog when selecting + // symbols. They have no canvas location. But cloned symbols are. + if (this->cloned) { + Geom::Affine const a( this->c2p * transform ); + bbox = SPGroup::bbox(a, type); + } + + return bbox; +} + +void SPSymbol::print(SPPrintContext* ctx) { + if (this->cloned) { + // Cloned is actually renderable + + ctx->bind(this->c2p, 1.0); + + SPGroup::print(ctx); + + ctx->release (); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-symbol.h b/src/object/sp-symbol.h new file mode 100644 index 0000000..19fd882 --- /dev/null +++ b/src/object/sp-symbol.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SYMBOL_H +#define SEEN_SP_SYMBOL_H + +/* + * SVG implementation + * + * Authors: + * Lauris Kaplinski + * + * Copyright (C) 1999-2003 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * This is quite similar in logic to + * Maybe we should merge them somehow (Lauris) + */ + +#include <2geom/affine.h> +#include "sp-item-group.h" +#include "viewbox.h" + +#define SP_TYPE_SYMBOL (sp_symbol_get_type ()) +#define SP_SYMBOL(obj) (dynamic_cast((SPObject*)obj)) +#define SP_IS_SYMBOL(obj) (dynamic_cast((SPObject*)obj) != NULL) + +class SPSymbol : public SPGroup, public SPViewBox { +public: + SPSymbol(); + ~SPSymbol() override; + + void build(SPDocument *document, Inkscape::XML::Node *repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx *ctx, unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + + void modified(unsigned int flags) override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void print(SPPrintContext *ctx) override; + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void hide (unsigned int key) override; +}; + +#endif diff --git a/src/object/sp-tag-use-reference.cpp b/src/object/sp-tag-use-reference.cpp new file mode 100644 index 0000000..e0d45ab --- /dev/null +++ b/src/object/sp-tag-use-reference.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of element. + * + * Copyright (C) Theodore Janeczko 2012-2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tag-use-reference.h" + +#include +#include + +#include "bad-uri-exception.h" +#include "livarot/Path.h" +#include "preferences.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "uri.h" + + +bool SPTagUseReference::_acceptObject(SPObject * const obj) const +{ + if (SP_IS_ITEM(obj)) { + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + + +static void sp_usepath_href_changed(SPObject *old_ref, SPObject *ref, SPTagUsePath *offset); +static void sp_usepath_delete_self(SPObject *deleted, SPTagUsePath *offset); + +SPTagUsePath::SPTagUsePath(SPObject* i_owner):SPTagUseReference(i_owner) +{ + owner=i_owner; + originalPath = nullptr; + sourceDirty=false; + sourceHref = nullptr; + sourceRepr = nullptr; + sourceObject = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_usepath_href_changed), this)); // listening to myself, this should be virtual instead + + user_unlink = nullptr; +} + +SPTagUsePath::~SPTagUsePath() +{ + delete originalPath; + originalPath = nullptr; + + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +void +SPTagUsePath::link(char *to) +{ + if ( to == nullptr ) { + quit_listening(); + unlink(); + } else { + if ( !sourceHref || ( strcmp(to, sourceHref) != 0 ) ) { + g_free(sourceHref); + sourceHref = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +SPTagUsePath::unlink() +{ + g_free(sourceHref); + sourceHref = nullptr; + detach(); +} + +void +SPTagUsePath::start_listening(SPObject* to) +{ + if ( to == nullptr ) { + return; + } + sourceObject = to; + sourceRepr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_usepath_delete_self), this)); +} + +void +SPTagUsePath::quit_listening() +{ + if ( sourceObject == nullptr ) { + return; + } + _delete_connection.disconnect(); + sourceRepr = nullptr; + sourceObject = nullptr; +} + +static void +sp_usepath_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPTagUsePath *offset) +{ + offset->quit_listening(); + SPItem *refobj = offset->getObject(); + if ( refobj ) { + offset->start_listening(refobj); + } +} + +static void +sp_usepath_delete_self(SPObject */*deleted*/, SPTagUsePath *offset) +{ + offset->owner->deleteObject(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-tag-use-reference.h b/src/object/sp-tag-use-reference.h new file mode 100644 index 0000000..13695d9 --- /dev/null +++ b/src/object/sp-tag-use-reference.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TAG_USE_REFERENCE_H +#define SEEN_SP_TAG_USE_REFERENCE_H + +/* + * The reference corresponding to href of element. + * + * Copyright (C) Theodore Janeczko 2012-2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "sp-object.h" +#include "sp-item.h" +#include "uri-references.h" + +class Path; + +namespace Inkscape { +namespace XML { + class Node; +} +} + + +class SPTagUseReference : public Inkscape::URIReference { +public: + SPTagUseReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return static_cast(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + + +class SPTagUsePath : public SPTagUseReference { +public: + Path *originalPath; + bool sourceDirty; + + SPObject *owner; + gchar *sourceHref; + Inkscape::XML::Node *sourceRepr; + SPObject *sourceObject; + + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + SPTagUsePath(SPObject* i_owner); + ~SPTagUsePath() override; + + void link(char* to); + void unlink(); + void start_listening(SPObject* to); + void quit_listening(); + void refresh_source(); + + void (*user_unlink) (SPObject *user); +}; + +#endif /* !SEEN_SP_USE_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-tag-use.cpp b/src/object/sp-tag-use.cpp new file mode 100644 index 0000000..5f8d445 --- /dev/null +++ b/src/object/sp-tag-use.cpp @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG implementation + * + * Authors: + * Theodore Janeczko + * Liam P White + * + * Copyright (C) Theodore Janeczko 2012-2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tag-use.h" + +#include +#include + +#include + +#include "bad-uri-exception.h" +#include "display/drawing-group.h" +#include "attributes.h" +#include "document.h" +#include "uri.h" +#include "xml/repr.h" +#include "preferences.h" +#include "style.h" +#include "sp-factory.h" +#include "sp-symbol.h" +#include "sp-tag-use-reference.h" + +SPTagUse::SPTagUse() +{ + href = nullptr; + //new (_changed_connection) sigc::connection; + ref = new SPTagUseReference(this); + + _changed_connection = ref->changedSignal().connect(sigc::mem_fun(*this, &SPTagUse::href_changed)); +} + +SPTagUse::~SPTagUse() +{ + ref->detach(); + delete ref; + ref = nullptr; +} + +void +SPTagUse::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + SPObject::build(document, repr); + readAttr( "xlink:href" ); + + // We don't need to create child here: + // reading xlink:href will attach ref, and that will cause the changed signal to be emitted, + // which will call sp_tag_use_href_changed, and that will take care of the child +} + +void +SPTagUse::release() +{ + _changed_connection.disconnect(); + + g_free(href); + href = nullptr; + + ref->detach(); + + SPObject::release(); +} + +void +SPTagUse::set(SPAttributeEnum key, gchar const *value) +{ + + switch (key) { + case SP_ATTR_XLINK_HREF: { + if ( value && href && ( strcmp(value, href) == 0 ) ) { + /* No change, do nothing. */ + } else { + g_free(href); + href = nullptr; + if (value) { + // First, set the href field, because sp_tag_use_href_changed will need it. + href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref->detach(); + } + } else { + ref->detach(); + } + } + break; + } + + default: + SPObject::set(key, value); + break; + } +} + +Inkscape::XML::Node * +SPTagUse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("inkscape:tagref"); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +void +SPTagUse::href_changed(SPObject *old_ref, SPObject *new_ref) +{ + if (old_ref && getRepr()) { + auto const id = old_ref->getAttribute("id"); + if (id) { + getRepr()->setAttribute("xlink:href", Glib::ustring("#") + id); + } + } +} + +SPItem * SPTagUse::get_original() +{ + SPItem *ref_ = nullptr; + if (ref) { + ref_ = ref->getObject(); + } + return ref_; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-tag-use.h b/src/object/sp-tag-use.h new file mode 100644 index 0000000..158d8c3 --- /dev/null +++ b/src/object/sp-tag-use.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TAG_USE_H__ +#define __SP_TAG_USE_H__ + +/* + * SVG implementation + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include +#include "svg/svg-length.h" +#include "sp-object.h" + + +#define SP_TAG_USE(obj) (dynamic_cast (obj)) +#define SP_IS_TAG_USE(obj) (dynamic_cast (obj) != NULL) + +class SPItem; +class SPTagUse; +class SPTagUseReference; + +class SPTagUse : public SPObject { + +public: + // item built from the original's repr (the visible clone) + // relative to the SPUse itself, it is treated as a child, similar to a grouped item relative to its group + gchar *href; +public: + SPTagUse(); + ~SPTagUse() override; + + void build(SPDocument *doc, Inkscape::XML::Node *repr) override; + void set(SPAttributeEnum key, gchar const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + void release() override; + + virtual void href_changed(SPObject* old_ref, SPObject* ref); + + //virtual SPItem* unlink(); + virtual SPItem* get_original(); + + // the reference to the original object + SPTagUseReference *ref; + sigc::connection _changed_connection; +}; + +#endif diff --git a/src/object/sp-tag.cpp b/src/object/sp-tag.cpp new file mode 100644 index 0000000..3c98f5b --- /dev/null +++ b/src/object/sp-tag.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG implementation + * + * Authors: + * Theodore Janeczko + * Liam P. White + * + * Copyright (C) Theodore Janeczko 2012-2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "attributes.h" +#include "sp-tag.h" +#include "xml/repr.h" +#include + +/* + * Move this SPItem into or after another SPItem in the doc + * \param target - the SPItem to move into or after + * \param intoafter - move to after the target (false), move inside (sublayer) of the target (true) + */ +void SPTag::moveTo(SPObject *target, gboolean intoafter) { + + Inkscape::XML::Node *target_ref = ( target ? target->getRepr() : nullptr ); + Inkscape::XML::Node *our_ref = getRepr(); + gboolean first = FALSE; + + if (target_ref == our_ref) { + // Move to ourself ignore + return; + } + + if (!target_ref) { + // Assume move to the "first" in the top node, find the top node + target_ref = our_ref; + while (target_ref->parent() != target_ref->root()) { + target_ref = target_ref->parent(); + } + first = TRUE; + } + + if (intoafter) { + // Move this inside of the target at the end + our_ref->parent()->removeChild(our_ref); + target_ref->addChild(our_ref, nullptr); + } else if (target_ref->parent() != our_ref->parent()) { + // Change in parent, need to remove and add + our_ref->parent()->removeChild(our_ref); + target_ref->parent()->addChild(our_ref, target_ref); + } else if (!first) { + // Same parent, just move + our_ref->parent()->changeOrder(our_ref, target_ref); + } +} + +/** + * Reads the Inkscape::XML::Node, and initializes SPTag variables. For this to get called, + * our name must be associated with a repr via "sp_object_type_register". Best done through + * sp-object-repr.cpp's repr_name_entries array. + */ +void +SPTag::build(SPDocument *document, Inkscape::XML::Node *repr) +{ + readAttr( "inkscape:expanded" ); + SPObject::build(document, repr); +} + +/** + * Sets a specific value in the SPTag. + */ +void +SPTag::set(SPAttributeEnum key, gchar const *value) +{ + + switch (key) + { + case SP_ATTR_INKSCAPE_EXPANDED: + if ( value && !strcmp(value, "true") ) { + setExpanded(true); + } + break; + default: + SPObject::set(key, value); + break; + } +} + +void SPTag::setExpanded(bool isexpanded) { + //if ( _expanded != isexpanded ){ + _expanded = isexpanded; + //} +} + +/** + * Receives update notifications. + */ +void +SPTag::update(SPCtx *ctx, guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) { + + /* do something to trigger redisplay, updates? */ + + } + SPObject::update(ctx, flags); +} + +/** + * Writes its settings to an incoming repr object, if any. + */ +Inkscape::XML::Node * +SPTag::write(Inkscape::XML::Document *doc, Inkscape::XML::Node *repr, guint flags) +{ + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = doc->createElement("inkscape:tag"); + } + + // Inkscape-only object, not copied during an "plain SVG" dump: + if (flags & SP_OBJECT_WRITE_EXT) { + if (_expanded) { + repr->setAttribute("inkscape:expanded", "true"); + } else { + repr->removeAttribute("inkscape:expanded"); + } + } + SPObject::write(doc, repr, flags); + return repr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-tag.h b/src/object/sp-tag.h new file mode 100644 index 0000000..09435cd --- /dev/null +++ b/src/object/sp-tag.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_TAG_H_SEEN +#define SP_TAG_H_SEEN + +/** \file + * SVG implementation + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +/* Skeleton base class */ + +#define SP_TAG(o) (dynamic_cast(o)) +#define SP_IS_TAG(o) (dynamic_cast(o) != NULL) + +class SPTag; + +class SPTag : public SPObject { +public: + SPTag() = default; + ~SPTag() override = default; + + void build(SPDocument * doc, Inkscape::XML::Node *repr) override; + //virtual void release(); + void set(SPAttributeEnum key, const gchar* value) override; + void update(SPCtx * ctx, unsigned flags) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + bool expanded() const { return _expanded; } + void setExpanded(bool isexpanded); + + void moveTo(SPObject *target, gboolean intoafter); + +private: + bool _expanded; +}; + + +#endif /* !SP_SKELETON_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-text.cpp b/src/object/sp-text.cpp new file mode 100644 index 0000000..89db9ca --- /dev/null +++ b/src/object/sp-text.cpp @@ -0,0 +1,1761 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG and implementation + * + * Author: + * Lauris Kaplinski + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: + * + * These subcomponents should not be items, or alternately + * we have to invent set of flags to mark, whether standard + * attributes are applicable to given item (I even like this + * idea somewhat - Lauris) + * + */ + +#include <2geom/affine.h> +#include +#include + +#include +#include + +#include "svg/svg.h" +#include "display/drawing-text.h" +#include "attributes.h" +#include "document.h" +#include "preferences.h" +#include "desktop.h" +#include "desktop-style.h" +#include "sp-namedview.h" +#include "inkscape.h" +#include "xml/quote.h" +#include "mod360.h" + +#include "sp-title.h" +#include "sp-desc.h" +#include "sp-text.h" + +#include "sp-shape.h" +#include "sp-textpath.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-flowregion.h" + +#include "text-editing.h" + +// For SVG 2 text flow +#include "livarot/Path.h" +#include "livarot/Shape.h" +#include "display/curve.h" + +/*##################################################### +# SPTEXT +#####################################################*/ +SPText::SPText() : SPItem() { +} + +SPText::~SPText() +{ + if (css) { + sp_repr_css_attr_unref(css); + } +}; + +void SPText::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "dx" ); + this->readAttr( "dy" ); + this->readAttr( "rotate" ); + + // textLength and friends + this->readAttr( "textLength" ); + this->readAttr( "lengthAdjust" ); + SPItem::build(doc, repr); + css = nullptr; + this->readAttr( "sodipodi:linespacing" ); // has to happen after the styles are read +} + +void SPText::release() { + SPItem::release(); +} + +void SPText::set(SPAttributeEnum key, const gchar* value) { + //std::cout << "SPText::set: " << sp_attribute_name( key ) << ": " << (value?value:"Null") << std::endl; + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SP_ATTR_SODIPODI_LINESPACING: + // convert deprecated tag to css... but only if 'line-height' missing. + if (value && !this->style->line_height.set) { + this->style->line_height.set = TRUE; + this->style->line_height.inherit = FALSE; + this->style->line_height.normal = FALSE; + this->style->line_height.unit = SP_CSS_UNIT_PERCENT; + this->style->line_height.value = this->style->line_height.computed = sp_svg_read_percentage (value, 1.0); + } + // Remove deprecated attribute + this->removeAttribute("sodipodi:linespacing"); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); + break; + + default: + SPItem::set(key, value); + break; + } + } +} + +void SPText::child_added(Inkscape::XML::Node *rch, Inkscape::XML::Node *ref) { + SPItem::child_added(rch, ref); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + +void SPText::remove_child(Inkscape::XML::Node *rch) { + SPItem::remove_child(rch); + + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_CONTENT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + + +void SPText::update(SPCtx *ctx, guint flags) { + + unsigned childflags = (flags & SP_OBJECT_MODIFIED_CASCADE); + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + // Create temporary list of children + std::vector l; + for (auto& child: children) { + sp_object_ref(&child, this); + l.push_back(&child); + } + + for (auto child:l) { + if (childflags || (child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + /* fixme: Do we need transform? */ + child->updateDisplay(ctx, childflags); + } + sp_object_unref(child, this); + } + + // update ourselves after updating children + SPItem::update(ctx, flags); + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + + SPItemCtx const *ictx = reinterpret_cast(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + + // Set inline_size computed value if necessary (i.e. if unit is %). + if (has_inline_size()) { + if (style->inline_size.unit == SP_CSS_UNIT_PERCENT) { + if (is_horizontal()) { + style->inline_size.computed = style->inline_size.value * ictx->viewport.width(); + } else { + style->inline_size.computed = style->inline_size.value * ictx->viewport.height(); + } + } + } + + /* fixme: It is not nice to have it here, but otherwise children content changes does not work */ + /* fixme: Even now it may not work, as we are delayed */ + /* fixme: So check modification flag everywhere immediate state is used */ + this->rebuildLayout(); + + Geom::OptRect paintbox = this->geometricBounds(); + + for (SPItemView* v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + this->_clearFlow(g); + g->setStyle(this->style, this->parent->style); + // pass the bbox of this as paintbox (used for paintserver fills) + this->layout.show(g, paintbox); + } + } +} + +void SPText::modified(guint flags) { +// SPItem::onModified(flags); + + guint cflags = (flags & SP_OBJECT_MODIFIED_CASCADE); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + cflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + // FIXME: all that we need to do here is to call setStyle, to set the changed + // style, but there's no easy way to access the drawing glyphs or texts corresponding to a + // text this. Therefore we do here the same as in _update, that is, destroy all items + // and create new ones. This is probably quite wasteful. + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG )) { + Geom::OptRect paintbox = this->geometricBounds(); + + for (SPItemView* v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + this->_clearFlow(g); + g->setStyle(this->style, this->parent->style); + this->layout.show(g, paintbox); + } + } + + // Create temporary list of children + std::vector l; + for (auto& child: children) { + sp_object_ref(&child, this); + l.push_back(&child); + } + + for (auto child:l) { + if (cflags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(cflags); + } + sp_object_unref(child, this); + } +} + +Inkscape::XML::Node *SPText::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if (flags & SP_OBJECT_WRITE_BUILD) { + if (!repr) { + repr = xml_doc->createElement("svg:text"); + } + + std::vector l; + + for (auto& child: children) { + if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) { + continue; + } + + Inkscape::XML::Node *crepr = nullptr; + + if (SP_IS_STRING(&child)) { + crepr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } else { + crepr = child.updateRepr(xml_doc, nullptr, flags); + } + + if (crepr) { + l.push_back(crepr); + } + } + + for (auto i=l.rbegin();i!=l.rend();++i) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if (SP_IS_TITLE(&child) || SP_IS_DESC(&child)) { + continue; + } + + if (SP_IS_STRING(&child)) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } else { + child.updateRepr(flags); + } + } + } + + this->attributes.writeTo(repr); + this->rebuildLayout(); // copied from update(), see LP Bug 1339305 + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +Geom::OptRect SPText::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox = SP_TEXT(this)->layout.bounds(transform); + + // FIXME this code is incorrect + if (bbox && type == SPItem::VISUAL_BBOX && !this->style->stroke.isNone()) { + double scale = transform.descrim(); + bbox->expandBy(0.5 * this->style->stroke_width.computed * scale); + } + + return bbox; +} + +Inkscape::DrawingItem* SPText::show(Inkscape::Drawing &drawing, unsigned /*key*/, unsigned /*flags*/) { + Inkscape::DrawingGroup *flowed = new Inkscape::DrawingGroup(drawing); + flowed->setPickChildren(false); + flowed->setStyle(this->style, this->parent->style); + + // pass the bbox of the text object as paintbox (used for paintserver fills) + this->layout.show(flowed, this->geometricBounds()); + + return flowed; +} + + +void SPText::hide(unsigned int key) { + for (SPItemView* v = this->display; v != nullptr; v = v->next) { + if (v->key == key) { + Inkscape::DrawingGroup *g = dynamic_cast(v->arenaitem); + this->_clearFlow(g); + } + } +} + +const char* SPText::displayName() const { + if (has_inline_size()) { + return _("Auto-wrapped text"); + } else if (has_shape_inside()) { + return _("Text in-a-shape"); + } else { + return _("Text"); + } +} + +gchar* SPText::description() const { + + SPStyle *style = this->style; + + char *n = xml_quote_strdup(style->font_family.value()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(style->font_size.computed, "px"); + q.quantity *= this->i2doc_affine().descrim(); + Glib::ustring xs = q.string(sp_style_get_css_unit_string(unit)); + + char const *trunc = ""; + Inkscape::Text::Layout const *layout = te_get_layout((SPItem *) this); + + if (layout && layout->inputTruncated()) { + trunc = _(" [truncated]"); + } + + char *ret = ( SP_IS_TEXT_TEXTPATH(this) + ? g_strdup_printf(_("on path%s (%s, %s)"), trunc, n, xs.c_str()) + : g_strdup_printf(_("%s (%s, %s)"), trunc, n, xs.c_str()) ); + return ret; +} + +void SPText::snappoints(std::vector &p, Inkscape::SnapPreferences const *snapprefs) const { + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_TEXT_BASELINE)) { + // Choose a point on the baseline for snapping from or to, with the horizontal position + // of this point depending on the text alignment (left vs. right) + Inkscape::Text::Layout const *layout = te_get_layout(this); + + if (layout != nullptr && layout->outputExists()) { + boost::optional pt = layout->baselineAnchorPoint(); + + if (pt) { + p.emplace_back((*pt) * this->i2dt_affine(), Inkscape::SNAPSOURCE_TEXT_ANCHOR, Inkscape::SNAPTARGET_TEXT_ANCHOR); + } + } + } +} + +void SPText::hide_shape_inside() +{ + SPText *text = dynamic_cast(this); + SPStyle *item_style = this->style; + if (item_style && text && item_style->shape_inside.set) { + SPCSSAttr *css_unset = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + sp_repr_css_unset_property(css_unset, "shape-inside"); + sp_repr_css_attr_unref(css_unset); + this->changeCSS(css_unset, "style"); + } else { + css = nullptr; + } +} + +void SPText::show_shape_inside() +{ + SPText *text = dynamic_cast(this); + if (text && css) { + this->changeCSS(css, "style"); + } +} + +Geom::Affine SPText::set_transform(Geom::Affine const &xform) { + // See if 'shape-inside' has rectangle + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/use_svg2", true)) { + Inkscape::XML::Node* rectangle = get_first_rectangle(); + if (rectangle) { + return xform; + } + } + // we cannot optimize textpath because changing its fontsize will break its match to the path + + if (SP_IS_TEXT_TEXTPATH (this)) { + if (!this->_optimizeTextpathText) { + return xform; + } else { + this->_optimizeTextpathText = false; + } + } + + // we cannot optimize text with textLength because it may show different size than specified + if (this->attributes.getTextLength()->_set) + return xform; + + if (this->style && this->style->inline_size.set) + return xform; + + /* This function takes care of scaling & translation only, we return whatever parts we can't + handle. */ + +// TODO: pjrm tried to use fontsize_expansion(xform) here and it works for text in that font size +// is scaled more intuitively when scaling non-uniformly; however this necessitated using +// fontsize_expansion instead of expansion in other places too, where it was not appropriate +// (e.g. it broke stroke width on copy/pasting of style from horizontally stretched to vertically +// stretched shape). Using fontsize_expansion only here broke setting the style via font +// dialog. This needs to be investigated further. + double const ex = xform.descrim(); + if (ex == 0) { + return xform; + } + + Geom::Affine ret(Geom::Affine(xform).withoutTranslation()); + ret[0] /= ex; + ret[1] /= ex; + ret[2] /= ex; + ret[3] /= ex; + + // Adjust x/y, dx/dy + this->_adjustCoordsRecursive (this, xform * ret.inverse(), ex); + + // Adjust font size + this->_adjustFontsizeRecursive (this, ex); + + // Adjust stroke width + this->adjust_stroke_width_recursive (ex); + + // Adjust pattern fill + this->adjust_pattern(xform * ret.inverse()); + + // Adjust gradient fill + this->adjust_gradient(xform * ret.inverse()); + + return ret; +} + +void SPText::print(SPPrintContext *ctx) { + Geom::OptRect pbox, bbox, dbox; + pbox = this->geometricBounds(); + bbox = this->desktopVisualBounds(); + dbox = Geom::Rect::from_xywh(Geom::Point(0,0), this->document->getDimensions()); + + Geom::Affine const ctm (this->i2dt_affine()); + + this->layout.print(ctx,pbox,dbox,bbox,ctm); +} + +/* + * Member functions + */ + +void SPText::_buildLayoutInit() +{ + + layout.strut.reset(); + layout.wrap_mode = Inkscape::Text::Layout::WRAP_NONE; // Default to SVG 1.1 + + if (style) { + + // Strut + font_instance *font = font_factory::Default()->FaceFromStyle( style ); + if (font) { + font->FontMetrics(layout.strut.ascent, layout.strut.descent, layout.strut.xheight); + font->Unref(); + } + layout.strut *= style->font_size.computed; + if (style->line_height.normal ) { + layout.strut.computeEffective( Inkscape::Text::Layout::LINE_HEIGHT_NORMAL ); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + layout.strut.computeEffective( style->line_height.computed ); + } else { + if( style->font_size.computed > 0.0 ) { + layout.strut.computeEffective( style->line_height.computed/style->font_size.computed ); + } + } + + + // To do: follow SPItem clip_ref/mask_ref code + if (style->shape_inside.set ) { + + layout.wrap_mode = Inkscape::Text::Layout::WRAP_SHAPE_INSIDE; + + // Find union of all exclusion shapes + Shape *exclusion_shape = nullptr; + if(style->shape_subtract.set) { + exclusion_shape = _buildExclusionShape(); + } + + // Find inside shape curves + for (auto shape_id : style->shape_inside.shape_ids) { + + SPShape *shape = dynamic_cast(document->getObjectById( shape_id )); + if ( shape ) { + + // This code adapted from sp-flowregion.cpp: GetDest() + if (!(shape->_curve)) { + shape->set_shape(); + } + SPCurve *curve = shape->getCurve(); + + if ( curve ) { + Path *temp = new Path; + Path *padded = new Path; + temp->LoadPathVector( curve->get_pathvector(), shape->transform, true ); + if( style->shape_padding.set ) { + // std::cout << " padding: " << style->shape_padding.computed << std::endl; + temp->OutsideOutline ( padded, style->shape_padding.computed, join_round, butt_straight, 20.0 ); + } else { + // std::cout << " no padding" << std::endl; + padded->Copy( temp ); + } + padded->Convert( 0.25 ); // Convert to polyline + Shape* sh = new Shape; + padded->Fill( sh, 0 ); + // for( unsigned i = 0; i < temp->pts.size(); ++i ) { + // std::cout << " ........ " << temp->pts[i].p << std::endl; + // } + // std::cout << " ...... shape: " << sh->numberOfPoints() << std::endl; + Shape *uncross = new Shape; + uncross->ConvertToShape( sh ); + + // Subtract exclusion shape + if(style->shape_subtract.set) { + Shape *copy = new Shape; + if (exclusion_shape && exclusion_shape->hasEdges()) { + copy->Booleen(uncross, const_cast(exclusion_shape), bool_op_diff); + } else { + copy->Copy(uncross); + } + layout.appendWrapShape( copy ); + continue; + } + + layout.appendWrapShape( uncross ); + + delete temp; + delete padded; + delete sh; + // delete uncross; + } else { + std::cerr << "SPText::_buildLayoutInit(): Failed to get curve." << std::endl; + } + } + } + delete exclusion_shape; + + } else if (has_inline_size()) { + + layout.wrap_mode = Inkscape::Text::Layout::WRAP_INLINE_SIZE; + + // If both shape_inside and inline_size are set, shape_inside wins out. + + // We construct a rectangle with one dimension set by the computed value of 'inline-size' + // and the other dimension set to infinity. Text is laid out starting at the 'x' and 'y' + // attribute values. This is handled elsewhere. + + Geom::OptRect opt_frame = get_frame(); + Geom::Rect frame = *opt_frame; + + Shape *shape = new Shape; + shape->Reset(); + int v0 = shape->AddPoint(frame.corner(0)); + int v1 = shape->AddPoint(frame.corner(1)); + int v2 = shape->AddPoint(frame.corner(2)); + int v3 = shape->AddPoint(frame.corner(3)); + shape->AddEdge(v0, v1); + shape->AddEdge(v1, v2); + shape->AddEdge(v2, v3); + shape->AddEdge(v3, v0); + Shape *uncross = new Shape; + uncross->ConvertToShape( shape ); + + layout.appendWrapShape( uncross ); + + delete shape; + + } else if (style->white_space.value == SP_CSS_WHITE_SPACE_PRE || + style->white_space.value == SP_CSS_WHITE_SPACE_PREWRAP || + style->white_space.value == SP_CSS_WHITE_SPACE_PRELINE ) { + layout.wrap_mode = Inkscape::Text::Layout::WRAP_WHITE_SPACE; + } + + } // if (style) +} + +unsigned SPText::_buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath) +{ + unsigned length = 0; + unsigned child_attrs_offset = 0; + Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs; + + // Per SVG spec, an object with 'display:none' doesn't contribute to text layout. + if (object->style->display.computed == SP_CSS_DISPLAY_NONE) { + return 0; + } + + SPText* text_object = dynamic_cast(object); + SPTSpan* tspan_object = dynamic_cast(object); + SPTRef* tref_object = dynamic_cast(object); + SPTextPath* textpath_object = dynamic_cast(object); + + if (text_object) { + + bool use_xy = true; + bool use_dxdyrotate = true; + + // SVG 2 Text wrapping. + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE || + layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + use_xy = false; + use_dxdyrotate = false; + } + + text_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate); + + // SVG 2 Text wrapping + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + + // For horizontal text: + // 'x' is used to calculate the left/right edges of the rectangle but is not + // needed later. If not deleted here, it will cause an incorrect positioning + // of the first line. + // 'y' is used to determine where the first line box is located and is needed + // during the output stage. + // For vertical text: + // Follow above but exchange 'x' and 'y'. + // The SVG 2 spec currently says use the 'x' and 'y' from the element, + // if not defined in the element, use the 'x' and 'y' from the first child. + // We only look at the element. (Doing otherwise means tracking if + // we've found 'x' and 'y' and then creating the Shape at the end.) + if (is_horizontal()) { + // Horizontal text + SVGLength* y = _getFirstYLength(); + if (y) { + optional_attrs.y.push_back(*y); + } else { + std::cerr << "SPText::_buildLayoutInput: No 'y' attribute value with horizontal 'inline-size'!" << std::endl; + } + } else { + // Vertical text + SVGLength* x = _getFirstXLength(); + if (x) { + optional_attrs.x.push_back(*x); + } else { + std::cerr << "SPText::_buildLayoutInput: No 'x' attribute value with vertical 'inline-size'!" << std::endl; + } + } + } + + // set textLength on the entire layout, see note in TNG-Layout.h + if (text_object->attributes.getTextLength()->_set) { + layout.textLength._set = true; + layout.textLength.value = text_object->attributes.getTextLength()->value; + layout.textLength.computed = text_object->attributes.getTextLength()->computed; + layout.textLength.unit = text_object->attributes.getTextLength()->unit; + layout.lengthAdjust = (Inkscape::Text::Layout::LengthAdjust) text_object->attributes.getLengthAdjust(); + } + } + + else if (tspan_object) { + + // x, y attributes are stripped from some tspans marked with role="line" as we do our own line layout. + // This should be checked carefully, as it can undo line layout in imported SVG files. + bool use_xy = !in_textpath && + (tspan_object->role == SP_TSPAN_ROLE_UNSPECIFIED || !tspan_object->attributes.singleXYCoordinates()); + bool use_dxdyrotate = true; + + // SVG 2 Text wrapping: see comment above. + if (layout.wrap_mode == Inkscape::Text::Layout::WRAP_SHAPE_INSIDE || + layout.wrap_mode == Inkscape::Text::Layout::WRAP_INLINE_SIZE) { + use_xy = false; + use_dxdyrotate = false; + } + + tspan_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, use_xy, use_dxdyrotate); + + if (tspan_object->role != SP_TSPAN_ROLE_UNSPECIFIED) { + // We are doing line wrapping using sodipodi:role="line". New lines have been stripped. + + // Insert paragraph break before text if not first tspan. + SPObject *prev_object = object->getPrev(); + if (prev_object && dynamic_cast(prev_object)) { + if (!layout.inputExists()) { + // Add an object to store style, needed even if there is no text. When does this happen? + layout.appendText("", prev_object->style, prev_object, &optional_attrs); + } + layout.appendControlCode(Inkscape::Text::Layout::PARAGRAPH_BREAK, prev_object); + } + + // Create empty span to store info (any non-empty tspan with sodipodi:role="line" has a child). + if (!object->hasChildren()) { + layout.appendText("", object->style, object, &optional_attrs); + } + + length++; // interpreting line breaks as a character for the purposes of x/y/etc attributes + // is a liberal interpretation of the svg spec, but a strict reading would mean + // that if the first line is empty the second line would take its place at the + // start position. Very confusing. + // SVG 2 clarifies, attributes are matched to unicode input characters so line + // breaks do match to an x/y/etc attribute. + child_attrs_offset--; + } + } + + else if (tref_object) { + tref_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, true, true); + } + + else if (textpath_object) { + in_textpath = true; // This should be made local so we can mix normal text with textpath per SVG 2. + textpath_object->attributes.mergeInto(&optional_attrs, parent_optional_attrs, parent_attrs_offset, false, true); + optional_attrs.x.clear(); // Hmm, you can use x with horizontal text. So this is probably wrong. + optional_attrs.y.clear(); + } + + else { + optional_attrs = parent_optional_attrs; + child_attrs_offset = parent_attrs_offset; + } + + // Recurse + for (auto& child: object->children) { + SPString *str = dynamic_cast(&child); + if (str) { + Glib::ustring const &string = str->string; + // std::cout << " Appending: >" << string << "<" << std::endl; + layout.appendText(string, object->style, &child, &optional_attrs, child_attrs_offset + length); + length += string.length(); + } else if (!sp_repr_is_meta_element(child.getRepr())) { + /* ^^^^ XML Tree being directly used here while it shouldn't be.*/ + length += _buildLayoutInput(&child, optional_attrs, child_attrs_offset + length, in_textpath); + } + } + + return length; +} + +Shape* SPText::_buildExclusionShape() const +{ + std::unique_ptr result(new Shape()); // Union of all exclusion shapes + std::unique_ptr shape_temp(new Shape()); + + for(auto shape_id : style->shape_subtract.shape_ids) { + + SPShape *shape = dynamic_cast(document->getObjectById( shape_id )); + if ( shape ) { + // This code adapted from sp-flowregion.cpp: GetDest() + if (!(shape->_curve)) { + shape->set_shape(); + } + SPCurve *curve = shape->getCurve(); + + if ( curve ) { + Path *temp = new Path; + Path *margin = new Path; + temp->LoadPathVector( curve->get_pathvector(), shape->transform, true ); + + if( shape->style->shape_margin.set ) { + temp->OutsideOutline ( margin, -shape->style->shape_margin.computed, join_round, butt_straight, 20.0 ); + } else { + margin->Copy( temp ); + } + + margin->Convert( 0.25 ); // Convert to polyline + Shape* sh = new Shape; + margin->Fill( sh, 0 ); + + Shape *uncross = new Shape; + uncross->ConvertToShape( sh ); + + if (result->hasEdges()) { + shape_temp->Booleen(result.get(), uncross, bool_op_union); + std::swap(result, shape_temp); + } else { + result->Copy(uncross); + } + } + } + } + return result.release(); +} + + +// SVG requires one to use the first x/y value found on a child element if x/y not given on text +// element. TODO: Recurse. +SVGLength* +SPText::_getFirstXLength() +{ + SVGLength* x = attributes.getFirstXLength(); + + if (!x) { + for (auto& child: children) { + if (SP_IS_TSPAN(&child)) { + SPTSpan *tspan = SP_TSPAN(&child); + x = tspan->attributes.getFirstXLength(); + break; + } + } + } + + return x; +} + + +SVGLength* +SPText::_getFirstYLength() +{ + SVGLength* y = attributes.getFirstYLength(); + + if (!y) { + for (auto& child: children) { + if (SP_IS_TSPAN(&child)) { + SPTSpan *tspan = SP_TSPAN(&child); + y = tspan->attributes.getFirstYLength(); + break; + } + } + } + + return y; +} + + +void SPText::rebuildLayout() +{ + layout.clear(); + _buildLayoutInit(); + + Inkscape::Text::Layout::OptionalTextTagAttrs optional_attrs; + _buildLayoutInput(this, optional_attrs, 0, false); + + layout.calculateFlow(); + + for (auto& child: children) { + if (SP_IS_TEXTPATH(&child)) { + SPTextPath const *textpath = SP_TEXTPATH(&child); + if (textpath->originalPath != nullptr) { +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif + layout.fitToPathAlign(textpath->startOffset, *textpath->originalPath); + } + } + } +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + g_print("%s", layout.dumpAsText().c_str()); +#endif + + // set the x,y attributes on role:line spans + for (auto& child: children) { + if (SP_IS_TSPAN(&child)) { + SPTSpan *tspan = SP_TSPAN(&child); + if ((tspan->role != SP_TSPAN_ROLE_UNSPECIFIED) + && tspan->attributes.singleXYCoordinates() ) { + Inkscape::Text::Layout::iterator iter = layout.sourceToIterator(tspan); + Geom::Point anchor_point = layout.chunkAnchorPoint(iter); + tspan->attributes.setFirstXY(anchor_point); + // repr needs to be updated but if we do it here we get a loop. + } + } + } +} + + +void SPText::_adjustFontsizeRecursive(SPItem *item, double ex, bool is_root) +{ + SPStyle *style = item->style; + + if (style && !Geom::are_near(ex, 1.0)) { + if (!style->font_size.set && is_root) { + style->font_size.set = true; + } + style->font_size.type = SP_FONT_SIZE_LENGTH; + style->font_size.computed *= ex; + style->letter_spacing.computed *= ex; + style->word_spacing.computed *= ex; + if (style->line_height.unit != SP_CSS_UNIT_NONE && + style->line_height.unit != SP_CSS_UNIT_PERCENT && + style->line_height.unit != SP_CSS_UNIT_EM && + style->line_height.unit != SP_CSS_UNIT_EX) { + // No unit on 'line-height' property has special behavior. + style->line_height.computed *= ex; + } + item->updateRepr(); + } + + for(auto& o: item->children) { + if (SP_IS_ITEM(&o)) + _adjustFontsizeRecursive(SP_ITEM(&o), ex, false); + } +} + +void +remove_newlines_recursive(SPObject* object, bool is_svg2) +{ + // Replace '\n' by space. + SPString* string = dynamic_cast(object); + if (string) { + static Glib::RefPtr r = Glib::Regex::create("\n+"); + string->string = r->replace(string->string, 0, " ", (Glib::RegexMatchFlags)0); + string->getRepr()->setContent(string->string.c_str()); + } + + for (auto child : object->childList(false)) { + remove_newlines_recursive(child, is_svg2); + } + + // Add space at end of a line if line is created by sodipodi:role="line". + SPTSpan* tspan = dynamic_cast(object); + if (tspan && + tspan->role == SP_TSPAN_ROLE_LINE && + tspan->getNext() != nullptr && // Don't add space at end of last line. + !is_svg2) { // SVG2 uses newlines, should not have sodipodi:role. + + std::vector children = tspan->childList(false); + + // Find last string (could be more than one if there is tspan in the middle of a tspan). + for (auto it = children.rbegin(); it != children.rend(); ++it) { + SPString* string = dynamic_cast(*it); + if (string) { + string->string += ' '; + string->getRepr()->setContent(string->string.c_str()); + break; + } + } + } +} + +// Prepare multi-line text for putting on path. +void +SPText::remove_newlines() +{ + remove_newlines_recursive(this, has_shape_inside() || has_inline_size()); + style->inline_size.clear(); + style->shape_inside.clear(); + updateRepr(); +} + +void SPText::_adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root) +{ + if (SP_IS_TSPAN(item)) + SP_TSPAN(item)->attributes.transform(m, ex, ex, is_root); + // it doesn't matter if we change the x,y for role=line spans because we'll just overwrite them anyway + else if (SP_IS_TEXT(item)) + SP_TEXT(item)->attributes.transform(m, ex, ex, is_root); + else if (SP_IS_TEXTPATH(item)) + SP_TEXTPATH(item)->attributes.transform(m, ex, ex, is_root); + else if (SP_IS_TREF(item)) { + SP_TREF(item)->attributes.transform(m, ex, ex, is_root); + } else { + g_warning("element is not text"); + return; + } + + for(auto& o: item->children) { + if (SP_IS_ITEM(&o)) + _adjustCoordsRecursive(SP_ITEM(&o), m, ex, false); + } +} + + +void SPText::_clearFlow(Inkscape::DrawingGroup *in_arena) +{ + in_arena->clearChildren(); +} + + +/** Remove 'x' and 'y' values on children (lines) or they will be interpreted as absolute positions + * when 'inline-size' is removed. + */ +void SPText::remove_svg11_fallback() { + for (auto& child: children) { + child.removeAttribute("x"); + child.removeAttribute("y"); + } +} + +/** Convert new lines in 'inline-size' text to tspans with sodipodi:role="tspan". + * Note sodipodi:role="tspan" will be removed in the future! + */ +void SPText::newline_to_sodipodi() { + + // New lines can come anywhere, we must search character-by-character. + auto it = layout.begin(); + while (it != layout.end()) { + if (layout.characterAt(it) == '\n') { + + // Delete newline ('\n'). + iterator_pair pair; + auto it_end = it; + it_end.nextCharacter(); + sp_te_delete (this, it, it_end, pair); + it = pair.first; + + // Insert newline (sodipodi:role="line"). + it = sp_te_insert_line(this, it); + } + + it.nextCharacter(); + layout.validateIterator(&it); + } +} + +/** Convert tspans with sodipodi:role="tspans" to '\n'. + * Note sodipodi:role="tspan" will be removed in the future! + */ +void SPText::sodipodi_to_newline() { + + // tspans with sodipodi:role="line" are only direct children of a element. + for (auto child : childList(false)) { + + auto tspan = dynamic_cast(child); // Could have or . + if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) { + + // Remove sodipodi:role attribute. + tspan->removeAttribute("sodipodi:role"); + tspan->updateRepr(); + + // Insert '/n' if not last line. + // This may screw up dx, dy, rotate but... SVG 2 text cannot have these values. + if (tspan != lastChild()) { + auto last_child = tspan->lastChild(); + auto last_string = dynamic_cast<SPString *>(last_child); + if (last_string) { + // Add '/n' to string. + last_string->string += "\n"; + last_string->updateRepr(); + } else { + // Insert new string with '\n'. + auto tspan_node = tspan->getRepr(); + auto xml_doc = tspan_node->document(); + tspan_node->appendChild(xml_doc->createTextNode("\n")); + } + } + } + } +} + +bool SPText::is_horizontal() const +{ + unsigned mode = style->writing_mode.computed; + return (mode == SP_CSS_WRITING_MODE_LR_TB || mode == SP_CSS_WRITING_MODE_RL_TB); +} + +bool SPText::has_inline_size() const +{ + // If inline size is '0' it is as if it is not set. + return (style->inline_size.set && style->inline_size.value != 0); +} + +bool SPText::has_shape_inside() const +{ + return (style->shape_inside.set); +} + +// Gets rectangle defined by <text> x, y and inline-size ("infinite" in one direction). +Geom::OptRect SPText::get_frame() +{ + Geom::OptRect opt_frame; + Geom::Rect frame; + + if (has_inline_size()) { + double inline_size = style->inline_size.computed; + //unsigned mode = style->writing_mode.computed; + unsigned anchor = style->text_anchor.computed; + unsigned direction = style->direction.computed; + + if (is_horizontal()) { + // horizontal + frame = Geom::Rect::from_xywh(attributes.firstXY()[Geom::X], -100000, inline_size, 200000); + if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + frame *= Geom::Translate (-inline_size/2.0, 0 ); + } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) { + frame *= Geom::Translate (-inline_size, 0); + } + } else { + // vertical + frame = Geom::Rect::from_xywh(-100000, attributes.firstXY()[Geom::Y], 200000, inline_size); + if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + frame *= Geom::Translate (0, -inline_size/2.0); + } else if (anchor == SP_CSS_TEXT_ANCHOR_END) { + frame *= Geom::Translate (0, -inline_size); + } + } + + opt_frame = frame; + + } else { + // See if 'shape-inside' has rectangle + Inkscape::XML::Node* rectangle = get_first_rectangle(); + + if (rectangle) { + double x = 0.0; + double y = 0.0; + double width = 0.0; + double height = 0.0; + sp_repr_get_double (rectangle, "x", &x); + sp_repr_get_double (rectangle, "y", &y); + sp_repr_get_double (rectangle, "width", &width); + sp_repr_get_double (rectangle, "height", &height); + frame = Geom::Rect::from_xywh( x, y, width, height); + opt_frame = frame; + } + } + + return opt_frame; +} + +// Find the node of the first rectangle (if it exists) in 'shape-inside'. +Inkscape::XML::Node* SPText::get_first_rectangle() +{ + Inkscape::XML::Node* first_rectangle = nullptr; + + Inkscape::XML::Node *our_ref = getRepr(); + + if (style->shape_inside.set) { + + std::vector<Glib::ustring> shapes = get_shapes(); + + for (auto shape: shapes) { + + Inkscape::XML::Node *item = + sp_repr_lookup_descendant (our_ref->root(), "id", shape.c_str()); + + if (item && strncmp("svg:rect", item->name(), 8) == 0) { + return item; + break; + } + } + } + + return first_rectangle; +} + +// Get a list of shape in 'shape-inside' as a vector of strings. +std::vector<Glib::ustring> SPText::get_shapes() const +{ + return style->shape_inside.shape_ids; +} + + +SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text"); + text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + SPText *text_object = dynamic_cast<SPText *>(desktop->currentLayer()->appendChildRepr(text_repr)); + g_assert(text_object != nullptr); + + // Invert coordinate system? + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + + // Pixels to user units + p0 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + p1 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + + sp_repr_set_svg_double( text_repr, "x", p0[Geom::X]); + sp_repr_set_svg_double( text_repr, "y", p0[Geom::Y]); + + double inline_size = p1[Geom::X] - p0[Geom::X]; + + text_object->style->inline_size.setDouble( inline_size ); + text_object->style->inline_size.set = true; + + Inkscape::XML::Node *text_node = xml_doc->createTextNode(""); + text_repr->appendChild(text_node); + + SPItem *item = dynamic_cast<SPItem *>(desktop->currentLayer()); + g_assert(item != nullptr); + + // text_object->transform = item->i2doc_affine().inverse(); + + text_object->updateRepr(); + + Inkscape::GC::release(text_repr); + Inkscape::GC::release(text_node); + + return text_object; +} + +SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1) +{ + SPDocument *doc = desktop->getDocument(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *text_repr = xml_doc->createElement("svg:text"); + text_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + SPText *text_object = dynamic_cast<SPText *>(desktop->currentLayer()->appendChildRepr(text_repr)); + g_assert(text_object != nullptr); + + // Invert coordinate system? + p0 *= desktop->dt2doc(); + p1 *= desktop->dt2doc(); + + // Pixels to user units + p0 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + p1 *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + + // Create rectangle + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); + sp_repr_set_svg_double( rect_repr, "x", p0[Geom::X]); + sp_repr_set_svg_double( rect_repr, "y", p0[Geom::Y]); + sp_repr_set_svg_double( rect_repr, "width", abs(p1[Geom::X]-p0[Geom::X])); + sp_repr_set_svg_double( rect_repr, "height", abs(p1[Geom::Y]-p0[Geom::Y])); + + // Find defs, if does not exist, create. + Inkscape::XML::Node *defs_repr = sp_repr_lookup_name (xml_doc->root(), "svg:defs"); + if (defs_repr == nullptr) { + defs_repr = xml_doc->createElement("svg:defs"); + xml_doc->root()->addChild(defs_repr, nullptr); + } + else Inkscape::GC::anchor(defs_repr); + + // Add rectangle to defs. + defs_repr->addChild(rect_repr, nullptr); + + // Apply desktop style (do before adding "shape-inside"). + sp_desktop_apply_style_tool(desktop, text_repr, "/tools/text", true); + SPCSSAttr *css = sp_repr_css_attr(text_repr, "style" ); + Geom::Affine const local(text_object->i2doc_affine()); + double const ex(local.descrim()); + if ( (ex != 0.0) && (ex != 1.0) ) { + sp_css_attr_scale(css, 1/ex); + } + + sp_repr_css_set_property (css, "white-space", "pre"); // Respect new lines. + + // Link rectangle to text + std::string value("url(#"); + value += rect_repr->attribute("id"); + value += ")"; + sp_repr_css_set_property (css, "shape-inside", value.c_str()); + sp_repr_css_set(text_repr, css, "style"); + + sp_repr_css_attr_unref(css); + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? + Inkscape::XML::Node *text_node = xml_doc->createTextNode(""); + rtspan->appendChild(text_node); + text_repr->appendChild(rtspan); + + SPItem *item = dynamic_cast<SPItem *>(desktop->currentLayer()); + g_assert(item != nullptr); + + Inkscape::GC::release(rtspan); + Inkscape::GC::release(text_repr); + Inkscape::GC::release(text_node); + Inkscape::GC::release(defs_repr); + Inkscape::GC::release(rect_repr); + + return text_object; +} + +/* + * TextTagAttributes implementation + */ + +// Not used. +// void TextTagAttributes::readFrom(Inkscape::XML::Node const *node) +// { +// readSingleAttribute(SP_ATTR_X, node->attribute("x")); +// readSingleAttribute(SP_ATTR_Y, node->attribute("y")); +// readSingleAttribute(SP_ATTR_DX, node->attribute("dx")); +// readSingleAttribute(SP_ATTR_DY, node->attribute("dy")); +// readSingleAttribute(SP_ATTR_ROTATE, node->attribute("rotate")); +// readSingleAttribute(SP_ATTR_TEXTLENGTH, node->attribute("textLength")); +// readSingleAttribute(SP_ATTR_LENGTHADJUST, node->attribute("lengthAdjust")); +// } + +bool TextTagAttributes::readSingleAttribute(unsigned key, gchar const *value, SPStyle const *style, Geom::Rect const *viewport) +{ + // std::cout << "TextTagAttributes::readSingleAttribute: key: " << key + // << " value: " << (value?value:"Null") << std::endl; + std::vector<SVGLength> *attr_vector; + bool update_x = false; + bool update_y = false; + switch (key) { + case SP_ATTR_X: attr_vector = &attributes.x; update_x = true; break; + case SP_ATTR_Y: attr_vector = &attributes.y; update_y = true; break; + case SP_ATTR_DX: attr_vector = &attributes.dx; update_x = true; break; + case SP_ATTR_DY: attr_vector = &attributes.dy; update_y = true; break; + case SP_ATTR_ROTATE: attr_vector = &attributes.rotate; break; + case SP_ATTR_TEXTLENGTH: + attributes.textLength.readOrUnset(value); + return true; + break; + case SP_ATTR_LENGTHADJUST: + attributes.lengthAdjust = (value && !strcmp(value, "spacingAndGlyphs")? + Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS : + Inkscape::Text::Layout::LENGTHADJUST_SPACING); // default is "spacing" + return true; + break; + default: return false; + } + + // FIXME: sp_svg_length_list_read() amalgamates repeated separators. This prevents unset values. + *attr_vector = sp_svg_length_list_read(value); + + if( (update_x || update_y) && style != nullptr && viewport != nullptr ) { + double const w = viewport->width(); + double const h = viewport->height(); + double const em = style->font_size.computed; + double const ex = em * 0.5; + for(auto & it : *attr_vector) { + if( update_x ) + it.update( em, ex, w ); + if( update_y ) + it.update( em, ex, h ); + } + } + return true; +} + +void TextTagAttributes::writeTo(Inkscape::XML::Node *node) const +{ + writeSingleAttributeVector(node, "x", attributes.x); + writeSingleAttributeVector(node, "y", attributes.y); + writeSingleAttributeVector(node, "dx", attributes.dx); + writeSingleAttributeVector(node, "dy", attributes.dy); + writeSingleAttributeVector(node, "rotate", attributes.rotate); + + writeSingleAttributeLength(node, "textLength", attributes.textLength); + + if (attributes.textLength._set) { + if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) { + node->setAttribute("lengthAdjust", "spacing"); + } else if (attributes.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) { + node->setAttribute("lengthAdjust", "spacingAndGlyphs"); + } + } +} + +void TextTagAttributes::update( double em, double ex, double w, double h ) +{ + for(auto & it : attributes.x) { + it.update( em, ex, w ); + } + for(auto & it : attributes.y) { + it.update( em, ex, h ); + } + for(auto & it : attributes.dx) { + it.update( em, ex, w ); + } + for(auto & it : attributes.dy) { + it.update( em, ex, h ); + } +} + +void TextTagAttributes::writeSingleAttributeLength(Inkscape::XML::Node *node, gchar const *key, const SVGLength &length) +{ + if (length._set) { + node->setAttribute(key, length.write()); + } else + node->removeAttribute(key); +} + +void TextTagAttributes::writeSingleAttributeVector(Inkscape::XML::Node *node, gchar const *key, std::vector<SVGLength> const &attr_vector) +{ + if (attr_vector.empty()) + node->removeAttribute(key); + else { + Glib::ustring string; + + // FIXME: this has no concept of unset values because sp_svg_length_list_read() can't read them back in + for (auto it : attr_vector) { + if (!string.empty()) string += ' '; + string += it.write(); + } + node->setAttributeOrRemoveIfEmpty(key, string); + } +} + +bool TextTagAttributes::singleXYCoordinates() const +{ + return attributes.x.size() <= 1 && attributes.y.size() <= 1; +} + +bool TextTagAttributes::anyAttributesSet() const +{ + return !attributes.x.empty() || !attributes.y.empty() || !attributes.dx.empty() || !attributes.dy.empty() || !attributes.rotate.empty(); +} + +Geom::Point TextTagAttributes::firstXY() const +{ + Geom::Point point; + if (attributes.x.empty()) point[Geom::X] = 0.0; + else point[Geom::X] = attributes.x[0].computed; + if (attributes.y.empty()) point[Geom::Y] = 0.0; + else point[Geom::Y] = attributes.y[0].computed; + return point; +} + +void TextTagAttributes::setFirstXY(Geom::Point &point) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.x.empty()) + attributes.x.resize(1, zero_length); + if (attributes.y.empty()) + attributes.y.resize(1, zero_length); + attributes.x[0] = point[Geom::X]; + attributes.y[0] = point[Geom::Y]; +} + +SVGLength* TextTagAttributes::getFirstXLength() +{ + if (!attributes.x.empty()) { + return &attributes.x[0]; + } else { + return nullptr; + } +} + +SVGLength* TextTagAttributes::getFirstYLength() +{ + if (!attributes.y.empty()) { + return &attributes.y[0]; + } else { + return nullptr; + } +} + +// Instance of TextTagAttributes contains attributes as defined by text/tspan element. +// output: What will be sent to the rendering engine. +// parent_attrs: Attributes collected from all ancestors. +// parent_attrs_offset: Where this element fits into the parent_attrs. +// copy_xy: Should this elements x, y attributes contribute to output (can preserve set values but not use them... kind of strange). +// copy_dxdxrotate: Should this elements dx, dy, rotate attributes contribute to output. +void TextTagAttributes::mergeInto(Inkscape::Text::Layout::OptionalTextTagAttrs *output, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_attrs, unsigned parent_attrs_offset, bool copy_xy, bool copy_dxdyrotate) const +{ + mergeSingleAttribute(&output->x, parent_attrs.x, parent_attrs_offset, copy_xy ? &attributes.x : nullptr); + mergeSingleAttribute(&output->y, parent_attrs.y, parent_attrs_offset, copy_xy ? &attributes.y : nullptr); + mergeSingleAttribute(&output->dx, parent_attrs.dx, parent_attrs_offset, copy_dxdyrotate ? &attributes.dx : nullptr); + mergeSingleAttribute(&output->dy, parent_attrs.dy, parent_attrs_offset, copy_dxdyrotate ? &attributes.dy : nullptr); + mergeSingleAttribute(&output->rotate, parent_attrs.rotate, parent_attrs_offset, copy_dxdyrotate ? &attributes.rotate : nullptr); + if (attributes.textLength._set) { // only from current node, this is not inherited from parent + output->textLength.value = attributes.textLength.value; + output->textLength.computed = attributes.textLength.computed; + output->textLength.unit = attributes.textLength.unit; + output->textLength._set = attributes.textLength._set; + output->lengthAdjust = attributes.lengthAdjust; + } +} + +void TextTagAttributes::mergeSingleAttribute(std::vector<SVGLength> *output_list, std::vector<SVGLength> const &parent_list, unsigned parent_offset, std::vector<SVGLength> const *overlay_list) +{ + output_list->clear(); + if (overlay_list == nullptr) { + if (parent_list.size() > parent_offset) + { + output_list->reserve(parent_list.size() - parent_offset); + std::copy(parent_list.begin() + parent_offset, parent_list.end(), std::back_inserter(*output_list)); + } + } else { + output_list->reserve(std::max((int)parent_list.size() - (int)parent_offset, (int)overlay_list->size())); + unsigned overlay_offset = 0; + while (parent_offset < parent_list.size() || overlay_offset < overlay_list->size()) { + SVGLength const *this_item; + if (overlay_offset < overlay_list->size()) { + this_item = &(*overlay_list)[overlay_offset]; + overlay_offset++; + parent_offset++; + } else { + this_item = &parent_list[parent_offset]; + parent_offset++; + } + output_list->push_back(*this_item); + } + } +} + +void TextTagAttributes::erase(unsigned start_index, unsigned n) +{ + if (n == 0) return; + if (!singleXYCoordinates()) { + eraseSingleAttribute(&attributes.x, start_index, n); + eraseSingleAttribute(&attributes.y, start_index, n); + } + eraseSingleAttribute(&attributes.dx, start_index, n); + eraseSingleAttribute(&attributes.dy, start_index, n); + eraseSingleAttribute(&attributes.rotate, start_index, n); +} + +void TextTagAttributes::eraseSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n) +{ + if (attr_vector->size() <= start_index) return; + if (attr_vector->size() <= start_index + n) + attr_vector->erase(attr_vector->begin() + start_index, attr_vector->end()); + else + attr_vector->erase(attr_vector->begin() + start_index, attr_vector->begin() + start_index + n); +} + +void TextTagAttributes::insert(unsigned start_index, unsigned n) +{ + if (n == 0) return; + if (!singleXYCoordinates()) { + insertSingleAttribute(&attributes.x, start_index, n, true); + insertSingleAttribute(&attributes.y, start_index, n, true); + } + insertSingleAttribute(&attributes.dx, start_index, n, false); + insertSingleAttribute(&attributes.dy, start_index, n, false); + insertSingleAttribute(&attributes.rotate, start_index, n, false); +} + +void TextTagAttributes::insertSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n, bool is_xy) +{ + if (attr_vector->size() <= start_index) return; + SVGLength zero_length; + zero_length = 0.0; + attr_vector->insert(attr_vector->begin() + start_index, n, zero_length); + if (is_xy) { + double begin = start_index == 0 ? (*attr_vector)[start_index + n].computed : (*attr_vector)[start_index - 1].computed; + double diff = ((*attr_vector)[start_index + n].computed - begin) / n; // n tested for nonzero in insert() + for (unsigned i = 0 ; i < n ; i++) + (*attr_vector)[start_index + i] = begin + diff * i; + } +} + +void TextTagAttributes::split(unsigned index, TextTagAttributes *second) +{ + if (!singleXYCoordinates()) { + splitSingleAttribute(&attributes.x, index, &second->attributes.x, false); + splitSingleAttribute(&attributes.y, index, &second->attributes.y, false); + } + splitSingleAttribute(&attributes.dx, index, &second->attributes.dx, true); + splitSingleAttribute(&attributes.dy, index, &second->attributes.dy, true); + splitSingleAttribute(&attributes.rotate, index, &second->attributes.rotate, true); +} + +void TextTagAttributes::splitSingleAttribute(std::vector<SVGLength> *first_vector, unsigned index, std::vector<SVGLength> *second_vector, bool trimZeros) +{ + second_vector->clear(); + if (first_vector->size() <= index) return; + second_vector->resize(first_vector->size() - index); + std::copy(first_vector->begin() + index, first_vector->end(), second_vector->begin()); + first_vector->resize(index); + if (trimZeros) + while (!first_vector->empty() && (!first_vector->back()._set || first_vector->back().value == 0.0)) + first_vector->resize(first_vector->size() - 1); +} + +void TextTagAttributes::join(TextTagAttributes const &first, TextTagAttributes const &second, unsigned second_index) +{ + if (second.singleXYCoordinates()) { + attributes.x = first.attributes.x; + attributes.y = first.attributes.y; + } else { + joinSingleAttribute(&attributes.x, first.attributes.x, second.attributes.x, second_index); + joinSingleAttribute(&attributes.y, first.attributes.y, second.attributes.y, second_index); + } + joinSingleAttribute(&attributes.dx, first.attributes.dx, second.attributes.dx, second_index); + joinSingleAttribute(&attributes.dy, first.attributes.dy, second.attributes.dy, second_index); + joinSingleAttribute(&attributes.rotate, first.attributes.rotate, second.attributes.rotate, second_index); +} + +void TextTagAttributes::joinSingleAttribute(std::vector<SVGLength> *dest_vector, std::vector<SVGLength> const &first_vector, std::vector<SVGLength> const &second_vector, unsigned second_index) +{ + if (second_vector.empty()) + *dest_vector = first_vector; + else { + dest_vector->resize(second_index + second_vector.size()); + if (first_vector.size() < second_index) { + std::copy(first_vector.begin(), first_vector.end(), dest_vector->begin()); + SVGLength zero_length; + zero_length = 0.0; + std::fill(dest_vector->begin() + first_vector.size(), dest_vector->begin() + second_index, zero_length); + } else + std::copy(first_vector.begin(), first_vector.begin() + second_index, dest_vector->begin()); + std::copy(second_vector.begin(), second_vector.end(), dest_vector->begin() + second_index); + } +} + +void TextTagAttributes::transform(Geom::Affine const &matrix, double scale_x, double scale_y, bool extend_zero_length) +{ + SVGLength zero_length; + zero_length = 0.0; + + /* edge testcases for this code: + 1) moving text elements whose position is done entirely with transform="...", no x,y attributes + 2) unflowing multi-line flowtext then moving it (it has x but not y) + */ + unsigned points_count = std::max(attributes.x.size(), attributes.y.size()); + if (extend_zero_length && points_count < 1) + points_count = 1; + for (unsigned i = 0 ; i < points_count ; i++) { + Geom::Point point; + if (i < attributes.x.size()) point[Geom::X] = attributes.x[i].computed; + else point[Geom::X] = 0.0; + if (i < attributes.y.size()) point[Geom::Y] = attributes.y[i].computed; + else point[Geom::Y] = 0.0; + point *= matrix; + if (i < attributes.x.size()) + attributes.x[i] = point[Geom::X]; + else if (point[Geom::X] != 0.0 && extend_zero_length) { + attributes.x.resize(i + 1, zero_length); + attributes.x[i] = point[Geom::X]; + } + if (i < attributes.y.size()) + attributes.y[i] = point[Geom::Y]; + else if (point[Geom::Y] != 0.0 && extend_zero_length) { + attributes.y.resize(i + 1, zero_length); + attributes.y[i] = point[Geom::Y]; + } + } + for (auto & it : attributes.dx) + it = it.computed * scale_x; + for (auto & it : attributes.dy) + it = it.computed * scale_y; +} + +double TextTagAttributes::getDx(unsigned index) +{ + if( attributes.dx.empty()) { + return 0.0; + } + if( index < attributes.dx.size() ) { + return attributes.dx[index].computed; + } else { + return 0.0; // attributes.dx.back().computed; + } +} + + +double TextTagAttributes::getDy(unsigned index) +{ + if( attributes.dy.empty() ) { + return 0.0; + } + if( index < attributes.dy.size() ) { + return attributes.dy[index].computed; + } else { + return 0.0; // attributes.dy.back().computed; + } +} + + +void TextTagAttributes::addToDx(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length); + attributes.dx[index] = attributes.dx[index].computed + delta; +} + +void TextTagAttributes::addToDy(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length); + attributes.dy[index] = attributes.dy[index].computed + delta; +} + +void TextTagAttributes::addToDxDy(unsigned index, Geom::Point const &adjust) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (adjust[Geom::X] != 0.0) { + if (attributes.dx.size() < index + 1) attributes.dx.resize(index + 1, zero_length); + attributes.dx[index] = attributes.dx[index].computed + adjust[Geom::X]; + } + if (adjust[Geom::Y] != 0.0) { + if (attributes.dy.size() < index + 1) attributes.dy.resize(index + 1, zero_length); + attributes.dy[index] = attributes.dy[index].computed + adjust[Geom::Y]; + } +} + +double TextTagAttributes::getRotate(unsigned index) +{ + if( attributes.rotate.empty() ) { + return 0.0; + } + if( index < attributes.rotate.size() ) { + return attributes.rotate[index].computed; + } else { + return attributes.rotate.back().computed; + } +} + + +void TextTagAttributes::addToRotate(unsigned index, double delta) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.rotate.size() < index + 2) { + if (attributes.rotate.empty()) + attributes.rotate.resize(index + 2, zero_length); + else + attributes.rotate.resize(index + 2, attributes.rotate.back()); + } + attributes.rotate[index] = mod360(attributes.rotate[index].computed + delta); +} + + +void TextTagAttributes::setRotate(unsigned index, double angle) +{ + SVGLength zero_length; + zero_length = 0.0; + + if (attributes.rotate.size() < index + 2) { + if (attributes.rotate.empty()) + attributes.rotate.resize(index + 2, zero_length); + else + attributes.rotate.resize(index + 2, attributes.rotate.back()); + } + attributes.rotate[index] = mod360(angle); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-text.h b/src/object/sp-text.h new file mode 100644 index 0000000..d1cbf0f --- /dev/null +++ b/src/object/sp-text.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TEXT_H +#define SEEN_SP_TEXT_H + +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "desktop.h" +#include "sp-item.h" +#include "sp-string.h" // Provides many other headers with SP_IS_STRING +#include "text-tag-attributes.h" + +#include "libnrtype/Layout-TNG.h" + +#include "xml/node-event-vector.h" + +#define SP_TEXT(obj) (dynamic_cast<SPText*>((SPObject*)obj)) +#define SP_IS_TEXT(obj) (dynamic_cast<const SPText*>((SPObject*)obj) != NULL) + +/* Text specific flags */ +#define SP_TEXT_CONTENT_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A +#define SP_TEXT_LAYOUT_MODIFIED_FLAG SP_OBJECT_USER_MODIFIED_FLAG_A + + +/* SPText */ +class SPText : public SPItem { +public: + SPText(); + ~SPText() override; + + /** Converts the text object to its component curves */ + SPCurve *getNormalizedBpath() const + {return layout.convertToCurves();} + + /** Completely recalculates the layout. */ + void rebuildLayout(); + + //semiprivate: (need to be accessed by the C-style functions still) + TextTagAttributes attributes; + Inkscape::Text::Layout layout; + + /** when the object is transformed it's nicer to change the font size + and coordinates when we can, rather than just applying a matrix + transform. is_root is used to indicate to the function that it should + extend zero-length position vectors to length 1 in order to record the + new position. This is necessary to convert from objects whose position is + completely specified by transformations. */ + static void _adjustCoordsRecursive(SPItem *item, Geom::Affine const &m, double ex, bool is_root = true); + static void _adjustFontsizeRecursive(SPItem *item, double ex, bool is_root = true); + /** + This two functions are useful because layout calculations need text visible for example + Calculating a invisible char position object or pasting text with paragraps that overflow + shape defined. I have doubts abot trransform into a toggle function*/ + void show_shape_inside(); + void hide_shape_inside(); + + /** discards the drawing objects representing this text. */ + void _clearFlow(Inkscape::DrawingGroup *in_arena); + + bool _optimizeTextpathText; + +private: + + /** Initializes layout from <text> (i.e. this node). */ + void _buildLayoutInit(); + + /** Recursively walks the xml tree adding tags and their contents. The + non-trivial code does two things: firstly, it manages the positioning + attributes and their inheritance rules, and secondly it keeps track of line + breaks and makes sure both that they are assigned the correct SPObject and + that we don't get a spurious extra one at the end of the flow. */ + unsigned _buildLayoutInput(SPObject *object, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_optional_attrs, unsigned parent_attrs_offset, bool in_textpath); + + /** Union all exclusion shapes. */ + Shape* _buildExclusionShape() const; + + /** Find first x/y values which may be in a descendent element. */ + SVGLength* _getFirstXLength(); + SVGLength* _getFirstYLength(); + SPCSSAttr *css; + + public: + /** Optimize textpath text on next set_transform. */ + void optimizeTextpathText() {_optimizeTextpathText = true;} + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void child_added(Inkscape::XML::Node* child, Inkscape::XML::Node* ref) override; + void remove_child(Inkscape::XML::Node* child) override; + void set(SPAttributeEnum key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + void print(SPPrintContext *ctx) override; + const char* displayName() const override; + char* description() const override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + Geom::Affine set_transform(Geom::Affine const &transform) override; + + // For 'inline-size', need to also remove any 'x' and 'y' added by SVG 1.1 fallback. + void remove_svg11_fallback(); + + void newline_to_sodipodi(); // 'inline-size' to Inkscape multi-line text. + void sodipodi_to_newline(); // Inkscape mult-line text to SVG 2 text. + + bool is_horizontal() const; + bool has_inline_size() const; + bool has_shape_inside() const; + Geom::OptRect get_frame(); // Gets inline-size or shape-inside frame. + Inkscape::XML::Node* get_first_rectangle(); // Gets first shape-inside rectangle (if it exists). + std::vector<Glib::ustring> get_shapes() const; // Gets list of shapes in shape-inside. + void remove_newlines(); // Removes newlines in text. +}; + +SPItem *create_text_with_inline_size (SPDesktop *desktop, Geom::Point p0, Geom::Point p1); +SPItem *create_text_with_rectangle (SPDesktop *desktop, Geom::Point p0, Geom::Point p1); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-textpath.h b/src/object/sp-textpath.h new file mode 100644 index 0000000..3d8d4d4 --- /dev/null +++ b/src/object/sp-textpath.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_TEXTPATH_H +#define INKSCAPE_SP_TEXTPATH_H + +#include "svg/svg-length.h" +#include "sp-item.h" +#include "sp-text.h" + +class SPUsePath; +class Path; + +#define SP_TEXTPATH(obj) (dynamic_cast<SPTextPath*>((SPObject*)obj)) +#define SP_IS_TEXTPATH(obj) (dynamic_cast<const SPTextPath*>((SPObject*)obj) != NULL) + +enum TextPathSide { + SP_TEXT_PATH_SIDE_LEFT, + SP_TEXT_PATH_SIDE_RIGHT +}; + +class SPTextPath : public SPItem { +public: + SPTextPath(); + ~SPTextPath() override; + + TextTagAttributes attributes; + SVGLength startOffset; + TextPathSide side; + + Path *originalPath; + bool isUpdating; + SPUsePath *sourcePath; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#define SP_IS_TEXT_TEXTPATH(obj) (SP_IS_TEXT(obj) && obj->firstChild() && SP_IS_TEXTPATH(obj->firstChild())) + +SPItem *sp_textpath_get_path_item(SPTextPath *tp); +void sp_textpath_to_text(SPObject *tp); + + +#endif /* !INKSCAPE_SP_TEXTPATH_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-title.cpp b/src/object/sp-title.cpp new file mode 100644 index 0000000..fe295e4 --- /dev/null +++ b/src/object/sp-title.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <title> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-title.h" +#include "xml/repr.h" + +SPTitle::SPTitle() : SPObject() { +} + +SPTitle::~SPTitle() = default; + +Inkscape::XML::Node* SPTitle::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + SPTitle* object = this; + + if (!repr) { + repr = object->getRepr()->duplicate(xml_doc); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + diff --git a/src/object/sp-title.h b/src/object/sp-title.h new file mode 100644 index 0000000..77f4b88 --- /dev/null +++ b/src/object/sp-title.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TITLE_H +#define SEEN_SP_TITLE_H + +/* + * SVG <title> implementation + * + * Authors: + * Jeff Schiller <codedread@gmail.com> + * + * Copyright (C) 2008 Jeff Schiller + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-object.h" + +#define SP_TITLE(obj) (dynamic_cast<SPTitle*>((SPObject*)obj)) +#define SP_IS_TITLE(obj) (dynamic_cast<const SPTitle*>((SPObject*)obj) != NULL) + +class SPTitle : public SPObject { +public: + SPTitle(); + ~SPTitle() override; + + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; +}; + +#endif diff --git a/src/object/sp-tref-reference.cpp b/src/object/sp-tref-reference.cpp new file mode 100644 index 0000000..798e74a --- /dev/null +++ b/src/object/sp-tref-reference.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of <tref> element. + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * This file was created based on sp-use-reference.cpp + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include "enums.h" +#include "sp-tref-reference.h" + +#include "sp-text.h" +#include "sp-tref.h" + + +bool SPTRefReference::_acceptObject(SPObject * const obj) const +{ + SPObject *owner = getOwner(); + if (SP_IS_TREF(owner)) + return URIReference::_acceptObject(obj); + else + return false; +} + + +void SPTRefReference::updateObserver() +{ + SPObject *referred = getObject(); + + if (referred) { + if (subtreeObserved) { + subtreeObserved->removeObserver(*this); + delete subtreeObserved; + } + + subtreeObserved = new Inkscape::XML::Subtree(*referred->getRepr()); + subtreeObserved->addObserver(*this); + } +} + + +void SPTRefReference::notifyChildAdded(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && SP_IS_TREF(owner)) { + sp_tref_update_text(SP_TREF(owner)); + } +} + + +void SPTRefReference::notifyChildRemoved(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && SP_IS_TREF(owner)) { + sp_tref_update_text(SP_TREF(owner)); + } +} + + +void SPTRefReference::notifyChildOrderChanged(Inkscape::XML::Node &/*node*/, Inkscape::XML::Node &/*child*/, + Inkscape::XML::Node */*old_prev*/, Inkscape::XML::Node */*new_prev*/) +{ + SPObject *owner = getOwner(); + + if (owner && SP_IS_TREF(owner)) { + sp_tref_update_text(SP_TREF(owner)); + } +} + + +void SPTRefReference::notifyContentChanged(Inkscape::XML::Node &/*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + SPObject *owner = getOwner(); + + if (owner && SP_IS_TREF(owner)) { + sp_tref_update_text(SP_TREF(owner)); + } +} + + +void SPTRefReference::notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark /*name*/, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared /*new_value*/) +{ + // Do nothing - tref only cares about textual content +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-tref-reference.h b/src/object/sp-tref-reference.h new file mode 100644 index 0000000..f4c1c0f --- /dev/null +++ b/src/object/sp-tref-reference.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TREF_REFERENCE_H +#define SEEN_SP_TREF_REFERENCE_H + +/* + * The reference corresponding to href of <tref> element. + * + * This file was created based on sp-use-reference.h + * + * Copyright (C) 2007 Gail Banaszkiewicz + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "sp-item.h" +#include "uri-references.h" + +#include "util/share.h" +#include "xml/node-observer.h" +#include "xml/subtree.h" + +typedef unsigned int GQuark; + +class SPTRefReference : public Inkscape::URIReference, + public Inkscape::XML::NodeObserver { +public: + SPTRefReference(SPObject *owner) : URIReference(owner), subtreeObserved(nullptr) { + updateObserver(); + } + + ~SPTRefReference() override { + if (subtreeObserved) { + subtreeObserved->removeObserver(*this); + delete subtreeObserved; + } + } + + SPItem *getObject() const { + return static_cast<SPItem *>(URIReference::getObject()); + } + + void updateObserver(); + + ///////////////////////////////////////////////////////////////////// + // Node Observer Functions + // ----------------------- + void notifyChildAdded(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *prev) override; + void notifyChildRemoved(Inkscape::XML::Node &node, Inkscape::XML::Node &child, Inkscape::XML::Node *prev) override; + void notifyChildOrderChanged(Inkscape::XML::Node &node, Inkscape::XML::Node &child, + Inkscape::XML::Node *old_prev, Inkscape::XML::Node *new_prev) override; + void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark name, + Inkscape::Util::ptr_shared old_value, + Inkscape::Util::ptr_shared new_value) override; + ///////////////////////////////////////////////////////////////////// + +protected: + bool _acceptObject(SPObject * obj) const override; + + Inkscape::XML::Subtree *subtreeObserved; +}; + +#endif /* !SEEN_SP_TREF_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-tref.cpp b/src/object/sp-tref.cpp new file mode 100644 index 0000000..a99f868 --- /dev/null +++ b/src/object/sp-tref.cpp @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVG <tref> implementation - All character data within the referenced + * element, including character data enclosed within additional markup, + * will be rendered. + * + * This file was created based on skeleton.cpp + */ +/* + * Authors: + * Gail Banaszkiewicz <Gail.Banaszkiewicz@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-tref.h" + +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "attributes.h" +#include "document.h" +#include "sp-factory.h" +#include "sp-text.h" +#include "style.h" +#include "text-editing.h" + +//#define DEBUG_TREF +#ifdef DEBUG_TREF +# define debug(f, a...) { g_message("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_message(f, ## a); \ + g_message("\n"); \ + } +#else +# define debug(f, a...) /**/ +#endif + + +static void build_string_from_root(Inkscape::XML::Node *root, Glib::ustring *retString); + +/* TRef base class */ +static void sp_tref_href_changed(SPObject *old_ref, SPObject *ref, SPTRef *tref); +static void sp_tref_delete_self(SPObject *deleted, SPTRef *self); + +SPTRef::SPTRef() : SPItem() { + this->stringChild = nullptr; + + this->href = nullptr; + this->uriOriginalRef = new SPTRefReference(this); + + this->_changed_connection = + this->uriOriginalRef->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_tref_href_changed), this)); +} + +SPTRef::~SPTRef() { + delete this->uriOriginalRef; +} + +void SPTRef::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr( "xlink:href" ); + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "dx" ); + this->readAttr( "dy" ); + this->readAttr( "rotate" ); +} + +void SPTRef::release() { + //this->attributes.~TextTagAttributes(); + + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + + g_free(this->href); + this->href = nullptr; + + this->uriOriginalRef->detach(); + + SPItem::release(); +} + +void SPTRef::set(SPAttributeEnum key, const gchar* value) { + debug("0x%p %s(%u): '%s'",this, + sp_attribute_name(key),key,value ? value : "<no value>"); + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { // x, y, dx, dy, rotate + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else if (key == SP_ATTR_XLINK_HREF) { // xlink:href + if ( !value ) { + // No value + g_free(this->href); + this->href = nullptr; + this->uriOriginalRef->detach(); + } else if ((this->href && strcmp(value, this->href) != 0) || (!this->href)) { + // Value has changed + + if ( this->href ) { + g_free(this->href); + this->href = nullptr; + } + + this->href = g_strdup(value); + + try { + this->uriOriginalRef->attach(Inkscape::URI(value)); + this->uriOriginalRef->updateObserver(); + } catch ( Inkscape::BadURIException &e ) { + g_warning("%s", e.what()); + this->uriOriginalRef->detach(); + } + + // No matter what happened, an update should be in order + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } else { // default + SPItem::set(key, value); + } +} + +void SPTRef::update(SPCtx *ctx, guint flags) { + debug("0x%p",this); + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + SPObject *child = this->stringChild; + + if (child) { + if ( childflags || ( child->uflags & SP_OBJECT_MODIFIED_FLAG )) { + child->updateDisplay(ctx, childflags); + } + } + + SPItem::update(ctx, flags); +} + +void SPTRef::modified(unsigned int flags) { + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + SPObject *child = this->stringChild; + + if (child) { + sp_object_ref(child); + + if (flags || (child->mflags & SP_OBJECT_MODIFIED_FLAG)) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +Inkscape::XML::Node* SPTRef::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + debug("0x%p",this); + + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:tref"); + } + + this->attributes.writeTo(repr); + + if (this->uriOriginalRef->getURI()) { + auto uri = this->uriOriginalRef->getURI()->str(); + auto uri_string = uri.c_str(); + debug("uri_string=%s", uri_string); + repr->setAttribute("xlink:href", uri_string); + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +Geom::OptRect SPTRef::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox; + // find out the ancestor text which holds our layout + SPObject const *parent_text = this; + + while ( parent_text && !SP_IS_TEXT(parent_text) ) { + parent_text = parent_text->parent; + } + + if (parent_text == nullptr) { + return bbox; + } + + // get the bbox of our portion of the layout + bbox = SP_TEXT(parent_text)->layout.bounds(transform, + sp_text_get_length_upto(parent_text, this), sp_text_get_length_upto(this, nullptr) - 1); + + // Add stroke width + // FIXME this code is incorrect + if (bbox && type == SPItem::VISUAL_BBOX && !this->style->stroke.isNone()) { + double scale = transform.descrim(); + bbox->expandBy(0.5 * this->style->stroke_width.computed * scale); + } + + return bbox; +} + +const char* SPTRef::displayName() const { + return _("Cloned Character Data"); +} + +gchar* SPTRef::description() const { + SPObject const *referred = this->getObjectReferredTo(); + + if (referred) { + char *child_desc; + + if (SP_IS_ITEM(referred)) { + child_desc = SP_ITEM(referred)->detailedDescription(); + } else { + child_desc = g_strdup(""); + } + + char *ret = g_strdup_printf("%s%s", + (SP_IS_ITEM(referred) ? _(" from ") : ""), child_desc); + g_free(child_desc); + + return ret; + } + + return g_strdup(_("[orphaned]")); +} + + +/* For the sigc::connection changes (i.e. when the object being referred to changes) */ +static void +sp_tref_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPTRef *tref) +{ + if (tref) + { + // Save a pointer to the original object being referred to + SPObject *refRoot = tref->getObjectReferredTo(); + + tref->_delete_connection.disconnect(); + + if (tref->stringChild) { + tref->detach(tref->stringChild); + tref->stringChild = nullptr; + } + + // Ensure that we are referring to a legitimate object + if (tref->href && refRoot && sp_tref_reference_allowed(tref, refRoot)) { + + // Update the text being referred to (will create a new string child) + sp_tref_update_text(tref); + + // Restore the delete connection now that we're done messing with stuff + tref->_delete_connection = refRoot->connectDelete(sigc::bind(sigc::ptr_fun(&sp_tref_delete_self), tref)); + } + + } +} + + +/** + * Delete the tref object + */ +static void +sp_tref_delete_self(SPObject */*deleted*/, SPTRef *self) +{ + self->deleteObject(); +} + +/** + * Return the object referred to via the URI reference + */ +SPObject * SPTRef::getObjectReferredTo() +{ + SPObject *referredObject = nullptr; + + if (uriOriginalRef) { + referredObject = uriOriginalRef->getObject(); + } + + return referredObject; +} + +/** + * Return the object referred to via the URI reference + */ +SPObject const *SPTRef::getObjectReferredTo() const { + SPObject *referredObject = nullptr; + + if (uriOriginalRef) { + referredObject = uriOriginalRef->getObject(); + } + + return referredObject; +} + + +/** + * Returns true when the given tref is allowed to refer to a particular object + */ +bool +sp_tref_reference_allowed(SPTRef *tref, SPObject *possible_ref) +{ + bool allowed = false; + + if (tref && possible_ref) { + if (tref != possible_ref) { + bool ancestor = false; + for (SPObject *obj = tref; obj; obj = obj->parent) { + if (possible_ref == obj) { + ancestor = true; + break; + } + } + allowed = !ancestor; + } + } + + return allowed; +} + + +/** + * Returns true if a tref is fully contained in the confines of the given + * iterators and layout (or if there is no tref). + */ +bool +sp_tref_fully_contained(SPObject *start_item, Glib::ustring::iterator &start, + SPObject *end_item, Glib::ustring::iterator &end) +{ + bool fully_contained = false; + + if (start_item && end_item) { + + // If neither the beginning or the end is a tref then we return true (whether there + // is a tref in the innards or not, because if there is one then it must be totally + // contained) + if (!(SP_IS_STRING(start_item) && SP_IS_TREF(start_item->parent)) + && !(SP_IS_STRING(end_item) && SP_IS_TREF(end_item->parent))) { + fully_contained = true; + } + + // Both the beginning and end are trefs; but in this case, the string iterators + // must be at the right places + else if ((SP_IS_STRING(start_item) && SP_IS_TREF(start_item->parent)) + && (SP_IS_STRING(end_item) && SP_IS_TREF(end_item->parent))) { + if (start == SP_STRING(start_item)->string.begin() + && end == SP_STRING(start_item)->string.end()) { + fully_contained = true; + } + } + + // If the beginning is a string that is a child of a tref, the iterator has to be + // at the beginning of the item + else if ((SP_IS_STRING(start_item) && SP_IS_TREF(start_item->parent)) + && !(SP_IS_STRING(end_item) && SP_IS_TREF(end_item->parent))) { + if (start == SP_STRING(start_item)->string.begin()) { + fully_contained = true; + } + } + + // Same, but the for the end + else if (!(SP_IS_STRING(start_item) && SP_IS_TREF(start_item->parent)) + && (SP_IS_STRING(end_item) && SP_IS_TREF(end_item->parent))) { + if (end == SP_STRING(start_item)->string.end()) { + fully_contained = true; + } + } + } + + return fully_contained; +} + + +void sp_tref_update_text(SPTRef *tref) +{ + if (tref) { + // Get the character data that will be used with this tref + Glib::ustring charData = ""; + build_string_from_root(tref->getObjectReferredTo()->getRepr(), &charData); + + if (tref->stringChild) { + tref->detach(tref->stringChild); + tref->stringChild = nullptr; + } + + // Create the node and SPString to be the tref's child + Inkscape::XML::Document *xml_doc = tref->document->getReprDoc(); + + Inkscape::XML::Node *newStringRepr = xml_doc->createTextNode(charData.c_str()); + tref->stringChild = SPFactory::createObject(NodeTraits::get_type_string(*newStringRepr)); + + // Add this SPString as a child of the tref + tref->attach(tref->stringChild, tref->lastChild()); + sp_object_unref(tref->stringChild, nullptr); + (tref->stringChild)->invoke_build(tref->document, newStringRepr, TRUE); + + Inkscape::GC::release(newStringRepr); + } +} + + + +/** + * Using depth-first search, build up a string by concatenating all SPStrings + * found in the tree starting at the root + */ +static void +build_string_from_root(Inkscape::XML::Node *root, Glib::ustring *retString) +{ + if (root && retString) { + + // Stop and concatenate when a SPString is found + if (root->type() == Inkscape::XML::TEXT_NODE) { + *retString += (root->content()); + + debug("%s", retString->c_str()); + + // Otherwise, continue searching down the tree (with the assumption that no children nodes + // of a SPString are actually legal) + } else { + Inkscape::XML::Node *childNode; + for (childNode = root->firstChild(); childNode; childNode = childNode->next()) { + build_string_from_root(childNode, retString); + } + } + } +} + +/** + * This function will create a new tspan element with the same attributes as + * the tref had and add the same text as a child. The tref is replaced in the + * tree with the new tspan. + * The code is based partially on sp_use_unlink + */ +SPObject * +sp_tref_convert_to_tspan(SPObject *obj) +{ + SPObject * new_tspan = nullptr; + + //////////////////// + // BASE CASE + //////////////////// + if (SP_IS_TREF(obj)) { + + SPTRef *tref = SP_TREF(obj); + + if (tref && tref->stringChild) { + Inkscape::XML::Node *tref_repr = tref->getRepr(); + Inkscape::XML::Node *tref_parent = tref_repr->parent(); + + SPDocument *document = tref->document; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan"); + + // Add the new tspan element just after the current tref + tref_parent->addChild(new_tspan_repr, tref_repr); + Inkscape::GC::release(new_tspan_repr); + + new_tspan = document->getObjectByRepr(new_tspan_repr); + + // Create a new string child for the tspan + Inkscape::XML::Node *new_string_repr = tref->stringChild->getRepr()->duplicate(xml_doc); + new_tspan_repr->addChild(new_string_repr, nullptr); + + //SPObject * new_string_child = document->getObjectByRepr(new_string_repr); + + // Merge style from the tref + new_tspan->style->merge( tref->style ); + new_tspan->style->cascade( new_tspan->parent->style ); + new_tspan->updateRepr(); + + // Hold onto our SPObject and repr for now. + sp_object_ref(tref); + Inkscape::GC::anchor(tref_repr); + + // Remove ourselves, not propagating delete events to avoid a + // chain-reaction with other elements that might reference us. + tref->deleteObject(false); + + // Give the copy our old id and let go of our old repr. + new_tspan_repr->setAttribute("id", tref_repr->attribute("id")); + Inkscape::GC::release(tref_repr); + + // Establish the succession and let go of our object. + tref->setSuccessor(new_tspan); + sp_object_unref(tref); + } + } + //////////////////// + // RECURSIVE CASE + //////////////////// + else { + std::vector<SPObject *> l; + for (auto& child: obj->children) { + sp_object_ref(&child, obj); + l.push_back(&child); + } + for(auto child:l) { + // Note that there may be more than one conversion happening here, so if it's not a + // tref being passed into this function, the returned value can't be specifically known + new_tspan = sp_tref_convert_to_tspan(child); + + sp_object_unref(child, obj); + } + } + + return new_tspan; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-tref.h b/src/object/sp-tref.h new file mode 100644 index 0000000..c27fe37 --- /dev/null +++ b/src/object/sp-tref.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_TREF_H +#define SP_TREF_H + +/** \file + * SVG <tref> implementation, see sp-tref.cpp. + * + * This file was created based on skeleton.h + */ +/* + * Authors: + * Gail Banaszkiewicz <Gail.Banaszkiewicz@gmail.com> + * + * Copyright (C) 2007 Gail Banaszkiewicz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-item.h" +#include "sp-tref-reference.h" +#include "text-tag-attributes.h" + + +/* tref base class */ + +#define SP_TREF(obj) (dynamic_cast<SPTRef*>((SPObject*)obj)) +#define SP_IS_TREF(obj) (dynamic_cast<const SPTRef*>((SPObject*)obj) != NULL) + +class SPTRef : public SPItem { +public: + SPTRef(); + ~SPTRef() override; + + // Attributes that are used in the same way they would be in a tspan + TextTagAttributes attributes; + + // Text stored in the xlink:href attribute + char *href; + + // URI reference to original object + SPTRefReference *uriOriginalRef; + + // Shortcut pointer to the child of the tref (which is a copy + // of the character data stored at and/or below the node + // referenced by uriOriginalRef) + SPObject *stringChild; + + // The sigc connections for various notifications + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + SPObject * getObjectReferredTo(); + SPObject const *getObjectReferredTo() const; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, char const* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, guint flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + const char* displayName() const override; + char* description() const override; +}; + +void sp_tref_update_text(SPTRef *tref); +bool sp_tref_reference_allowed(SPTRef *tref, SPObject *possible_ref); +bool sp_tref_fully_contained(SPObject *start_item, Glib::ustring::iterator &start, + SPObject *end_item, Glib::ustring::iterator &end); +SPObject * sp_tref_convert_to_tspan(SPObject *item); + + +#endif /* !SP_TREF_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-tspan.cpp b/src/object/sp-tspan.cpp new file mode 100644 index 0000000..bfa52c5 --- /dev/null +++ b/src/object/sp-tspan.cpp @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <text> and <tspan> implementation + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * fixme: + * + * These subcomponents should not be items, or alternately + * we have to invent set of flags to mark, whether standard + * attributes are applicable to given item (I even like this + * idea somewhat - Lauris) + * + */ + +#include <cstring> +#include <string> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "attributes.h" +#include "document.h" +#include "text-editing.h" + +#include "sp-textpath.h" +#include "sp-tref.h" +#include "sp-tspan.h" +#include "sp-use-reference.h" +#include "style.h" + +#include "display/curve.h" + +#include "livarot/Path.h" + +#include "svg/stringstream.h" + + +/*##################################################### +# SPTSPAN +#####################################################*/ +SPTSpan::SPTSpan() : SPItem() { + this->role = SP_TSPAN_ROLE_UNSPECIFIED; +} + +SPTSpan::~SPTSpan() = default; + +void SPTSpan::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "dx" ); + this->readAttr( "dy" ); + this->readAttr( "rotate" ); + + // Strip sodipodi:role from SVG 2 flowed text. + // this->role = SP_TSPAN_ROLE_UNSPECIFIED; + SPText* text = dynamic_cast<SPText *>(parent); + if (text && !(text->has_shape_inside()|| text->has_inline_size())) { + this->readAttr( "sodipodi:role" ); + } + + // We'll intercept "style" to strip "visibility" property (SVG 1.1 fallback for SVG 2 text) then pass it on. + this->readAttr( "style" ); + + SPItem::build(doc, repr); +} + +void SPTSpan::release() { + SPItem::release(); +} + +void SPTSpan::set(SPAttributeEnum key, const gchar* value) { + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SP_ATTR_SODIPODI_ROLE: + if (value && (!strcmp(value, "line") || !strcmp(value, "paragraph"))) { + this->role = SP_TSPAN_ROLE_LINE; + } else { + this->role = SP_TSPAN_ROLE_UNSPECIFIED; + } + break; + + case SP_ATTR_STYLE: + if (value) { + Glib::ustring style(value); + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("visibility\\s*:\\s*hidden;*"); + Glib::ustring stripped = regex->replace_literal(style, 0, "", static_cast<Glib::RegexMatchFlags >(0)); + Inkscape::XML::Node *repr = getRepr(); + repr->setAttributeOrRemoveIfEmpty("style", stripped); + } + // Fall through + default: + SPItem::set(key, value); + break; + } + } +} + +void SPTSpan::update(SPCtx *ctx, guint flags) { + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if ( flags || ( ochild.uflags & SP_OBJECT_MODIFIED_FLAG )) { + ochild.updateDisplay(ctx, childflags); + } + } + + SPItem::update(ctx, flags); + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + } +} + +void SPTSpan::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if (flags || (ochild.mflags & SP_OBJECT_MODIFIED_FLAG)) { + ochild.emitModified(flags); + } + } +} + +Geom::OptRect SPTSpan::bbox(Geom::Affine const &transform, SPItem::BBoxType type) const { + Geom::OptRect bbox; + // find out the ancestor text which holds our layout + SPObject const *parent_text = this; + + while (parent_text && !SP_IS_TEXT(parent_text)) { + parent_text = parent_text->parent; + } + + if (parent_text == nullptr) { + return bbox; + } + + // get the bbox of our portion of the layout + bbox = SP_TEXT(parent_text)->layout.bounds(transform, sp_text_get_length_upto(parent_text, this), sp_text_get_length_upto(this, nullptr) - 1); + + if (!bbox) { + return bbox; + } + + // Add stroke width + // FIXME this code is incorrect + if (type == SPItem::VISUAL_BBOX && !this->style->stroke.isNone()) { + double scale = transform.descrim(); + bbox->expandBy(0.5 * this->style->stroke_width.computed * scale); + } + + return bbox; +} + +Inkscape::XML::Node* SPTSpan::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:tspan"); + } + + this->attributes.writeTo(repr); + + if ( flags&SP_OBJECT_WRITE_BUILD ) { + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr=nullptr; + + if ( SP_IS_TSPAN(&child) || SP_IS_TREF(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_TEXTPATH(&child) ) { + //c_repr = child.updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( SP_IS_STRING(&child) ) { + c_repr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for (auto i = l.rbegin(); i!= l.rend(); ++i) { + repr->addChild((*i), nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( SP_IS_TSPAN(&child) || SP_IS_TREF(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_TEXTPATH(&child) ) { + //c_repr = child->updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( SP_IS_STRING(&child) ) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + +const char* SPTSpan::displayName() const { + return _("Text Span"); +} + + +/*##################################################### +# SPTEXTPATH +#####################################################*/ +void refresh_textpath_source(SPTextPath* offset); + +SPTextPath::SPTextPath() : SPItem() { + this->startOffset._set = false; + this->side = SP_TEXT_PATH_SIDE_LEFT; + this->originalPath = nullptr; + this->isUpdating=false; + + // set up the uri reference + this->sourcePath = new SPUsePath(this); + this->sourcePath->user_unlink = sp_textpath_to_text; +} + +SPTextPath::~SPTextPath() { + delete this->sourcePath; +} + +void SPTextPath::build(SPDocument *doc, Inkscape::XML::Node *repr) { + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "dx" ); + this->readAttr( "dy" ); + this->readAttr( "rotate" ); + this->readAttr( "startOffset" ); + this->readAttr( "side" ); + this->readAttr( "xlink:href" ); + + this->readAttr( "style"); + + SPItem::build(doc, repr); +} + +void SPTextPath::release() { + //this->attributes.~TextTagAttributes(); + + if (this->originalPath) { + delete this->originalPath; + } + + this->originalPath = nullptr; + + SPItem::release(); +} + +void SPTextPath::set(SPAttributeEnum key, const gchar* value) { + + if (this->attributes.readSingleAttribute(key, value, style, &viewport)) { + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } else { + switch (key) { + case SP_ATTR_XLINK_HREF: + this->sourcePath->link((char*)value); + break; + case SP_ATTR_SIDE: + if (!value) { + return; + } + + if (strncmp(value, "left", 4) == 0) + side = SP_TEXT_PATH_SIDE_LEFT; + else if (strncmp(value, "right", 5) == 0) + side = SP_TEXT_PATH_SIDE_RIGHT; + else { + std::cerr << "SPTextPath: Bad side value: " << (value?value:"null") << std::endl; + side = SP_TEXT_PATH_SIDE_LEFT; + } + break; + case SP_ATTR_STARTOFFSET: + this->startOffset.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + default: + SPItem::set(key, value); + break; + } + } +} + +void SPTextPath::update(SPCtx *ctx, guint flags) { + this->isUpdating = true; + + if ( this->sourcePath->sourceDirty ) { + refresh_textpath_source(this); + } + + this->isUpdating = false; + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if ( flags || ( ochild.uflags & SP_OBJECT_MODIFIED_FLAG )) { + ochild.updateDisplay(ctx, flags); + } + } + + if (flags & ( SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_LAYOUT_MODIFIED_FLAG ) ) + { + SPItemCtx const *ictx = reinterpret_cast<SPItemCtx const *>(ctx); + + double const w = ictx->viewport.width(); + double const h = ictx->viewport.height(); + double const em = style->font_size.computed; + double const ex = 0.5 * em; // fixme: get x height from pango or libnrtype. + + attributes.update( em, ex, w, h ); + } +} + + +void refresh_textpath_source(SPTextPath* tp) +{ + if ( tp == nullptr ) { + return; + } + + tp->sourcePath->refresh_source(); + tp->sourcePath->sourceDirty=false; + + if ( tp->sourcePath->originalPath ) { + if (tp->originalPath) { + delete tp->originalPath; + } + + SPCurve* curve_copy; + if (tp->side == SP_TEXT_PATH_SIDE_LEFT) { + curve_copy = tp->sourcePath->originalPath->copy(); + } else { + curve_copy = tp->sourcePath->originalPath->create_reverse(); + } + + SPItem *item = SP_ITEM(tp->sourcePath->sourceObject); + tp->originalPath = new Path; + tp->originalPath->LoadPathVector(curve_copy->get_pathvector(), item->transform, true); + tp->originalPath->ConvertWithBackData(0.01); + + curve_copy->unref(); + } +} + +void SPTextPath::modified(unsigned int flags) { +// SPItem::onModified(flags); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + for (auto& ochild: children) { + if (flags || (ochild.mflags & SP_OBJECT_MODIFIED_FLAG)) { + ochild.emitModified(flags); + } + } +} + +Inkscape::XML::Node* SPTextPath::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:textPath"); + } + + this->attributes.writeTo(repr); + + if (this->side == SP_TEXT_PATH_SIDE_RIGHT) { + this->setAttribute("side", "right"); + } + + if (this->startOffset._set) { + if (this->startOffset.unit == SVGLength::PERCENT) { + Inkscape::SVGOStringStream os; + os << (this->startOffset.computed * 100.0) << "%"; + this->setAttribute("startOffset", os.str()); + } else { + /* FIXME: This logic looks rather undesirable if e.g. startOffset is to be + in ems. */ + sp_repr_set_svg_double(repr, "startOffset", this->startOffset.computed); + } + } + + if ( this->sourcePath->sourceHref ) { + repr->setAttribute("xlink:href", this->sourcePath->sourceHref); + } + + if ( flags & SP_OBJECT_WRITE_BUILD ) { + std::vector<Inkscape::XML::Node *> l; + + for (auto& child: children) { + Inkscape::XML::Node* c_repr=nullptr; + + if ( SP_IS_TSPAN(&child) || SP_IS_TREF(&child) ) { + c_repr = child.updateRepr(xml_doc, nullptr, flags); + } else if ( SP_IS_TEXTPATH(&child) ) { + //c_repr = child->updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( SP_IS_STRING(&child) ) { + c_repr = xml_doc->createTextNode(SP_STRING(&child)->string.c_str()); + } + + if ( c_repr ) { + l.push_back(c_repr); + } + } + + for( auto i = l.rbegin(); i != l.rend(); ++i ) { + repr->addChild(*i, nullptr); + Inkscape::GC::release(*i); + } + } else { + for (auto& child: children) { + if ( SP_IS_TSPAN(&child) || SP_IS_TREF(&child) ) { + child.updateRepr(flags); + } else if ( SP_IS_TEXTPATH(&child) ) { + //c_repr = child.updateRepr(xml_doc, NULL, flags); // shouldn't happen + } else if ( SP_IS_STRING(&child) ) { + child.getRepr()->setContent(SP_STRING(&child)->string.c_str()); + } + } + } + + SPItem::write(xml_doc, repr, flags); + + return repr; +} + + +SPItem *sp_textpath_get_path_item(SPTextPath *tp) +{ + if (tp && tp->sourcePath) { + SPItem *refobj = tp->sourcePath->getObject(); + + if (SP_IS_ITEM(refobj)) { + return refobj; + } + } + return nullptr; +} + +void sp_textpath_to_text(SPObject *tp) +{ + SPObject *text = tp->parent; + + // make a list of textpath children + std::vector<Inkscape::XML::Node *> tp_reprs; + + for (auto& o: tp->children) { + tp_reprs.push_back(o.getRepr()); + } + + for (auto i = tp_reprs.rbegin(); i != tp_reprs.rend(); ++i) { + // make a copy of each textpath child + Inkscape::XML::Node *copy = (*i)->duplicate(text->getRepr()->document()); + // remove the old repr from under textpath + tp->getRepr()->removeChild(*i); + // put its copy under text + text->getRepr()->addChild(copy, nullptr); // fixme: copy id + } + + // set x/y on text (to be near where it was when on path) + // Copied from Layout::fitToPathAlign + Path *path = dynamic_cast<SPTextPath*>(tp)->originalPath; + SVGLength const startOffset = dynamic_cast<SPTextPath*>(tp)->startOffset; + double offset = 0.0; + if (startOffset._set) { + if (startOffset.unit == SVGLength::PERCENT) + offset = startOffset.computed * path->Length(); + else + offset = startOffset.computed; + } + int unused = 0; + Path::cut_position *cut_pos = path->CurvilignToPosition(1, &offset, unused); + Geom::Point midpoint; + Geom::Point tangent; + path->PointAndTangentAt(cut_pos[0].piece, cut_pos[0].t, midpoint, tangent); + sp_repr_set_svg_double(text->getRepr(), "x", midpoint[Geom::X]); + sp_repr_set_svg_double(text->getRepr(), "y", midpoint[Geom::Y]); + + //remove textpath + tp->deleteObject(); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-tspan.h b/src/object/sp-tspan.h new file mode 100644 index 0000000..4125efb --- /dev/null +++ b/src/object/sp-tspan.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_TSPAN_H +#define INKSCAPE_SP_TSPAN_H + +/* + * tspan and textpath, based on the flowtext routines + */ + +#include "sp-item.h" +#include "text-tag-attributes.h" + +#define SP_TSPAN(obj) (dynamic_cast<SPTSpan*>((SPObject*)obj)) +#define SP_IS_TSPAN(obj) (dynamic_cast<const SPTSpan*>((SPObject*)obj) != NULL) + +enum { + SP_TSPAN_ROLE_UNSPECIFIED, + SP_TSPAN_ROLE_PARAGRAPH, + SP_TSPAN_ROLE_LINE +}; + +class SPTSpan : public SPItem { +public: + SPTSpan(); + ~SPTSpan() override; + + unsigned int role : 2; + TextTagAttributes attributes; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, const char* value) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType type) const override; + const char* displayName() const override; +}; + +#endif /* !INKSCAPE_SP_TSPAN_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/sp-use-reference.cpp b/src/object/sp-use-reference.cpp new file mode 100644 index 0000000..f2b8575 --- /dev/null +++ b/src/object/sp-use-reference.cpp @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of <use> element. + * + * Copyright (C) 2004 Bulia Byak + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "sp-use-reference.h" + +#include <cstring> +#include <string> + +#include "bad-uri-exception.h" +#include "enums.h" + +#include "display/curve.h" +#include "livarot/Path.h" +#include "preferences.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "uri.h" + +bool SPUseReference::_acceptObject(SPObject * const obj) const +{ + return URIReference::_acceptObject(obj); +} + + +static void sp_usepath_href_changed(SPObject *old_ref, SPObject *ref, SPUsePath *offset); +static void sp_usepath_move_compensate(Geom::Affine const *mp, SPItem *original, SPUsePath *self); +static void sp_usepath_delete_self(SPObject *deleted, SPUsePath *offset); +static void sp_usepath_source_modified(SPObject *iSource, guint flags, SPUsePath *offset); + +SPUsePath::SPUsePath(SPObject* i_owner):SPUseReference(i_owner) +{ + owner=i_owner; + originalPath = nullptr; + sourceDirty=false; + sourceHref = nullptr; + sourceRepr = nullptr; + sourceObject = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_usepath_href_changed), this)); // listening to myself, this should be virtual instead + + user_unlink = nullptr; +} + +SPUsePath::~SPUsePath() +{ + if (originalPath != nullptr) { + originalPath->unref(); + } + + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +void +SPUsePath::link(char *to) +{ + if ( to == nullptr ) { + quit_listening(); + unlink(); + } else { + if ( !sourceHref || ( strcmp(to, sourceHref) != 0 ) ) { + g_free(sourceHref); + sourceHref = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +SPUsePath::unlink() +{ + g_free(sourceHref); + sourceHref = nullptr; + detach(); +} + +void +SPUsePath::start_listening(SPObject* to) +{ + if ( to == nullptr ) { + return; + } + sourceObject = to; + sourceRepr = to->getRepr(); + _delete_connection = to->connectDelete(sigc::bind(sigc::ptr_fun(&sp_usepath_delete_self), this)); + _transformed_connection = SP_ITEM(to)->connectTransformed(sigc::bind(sigc::ptr_fun(&sp_usepath_move_compensate), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_usepath_source_modified), this)); +} + +void +SPUsePath::quit_listening() +{ + if ( sourceObject == nullptr ) { + return; + } + _modified_connection.disconnect(); + _delete_connection.disconnect(); + _transformed_connection.disconnect(); + sourceRepr = nullptr; + sourceObject = nullptr; +} + +static void +sp_usepath_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, SPUsePath *offset) +{ + offset->quit_listening(); + SPItem *refobj = offset->getObject(); + if ( refobj ) { + offset->start_listening(refobj); + } + offset->sourceDirty=true; + offset->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_usepath_move_compensate(Geom::Affine const *mp, SPItem *original, SPUsePath *self) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + if (mode == SP_CLONE_COMPENSATION_NONE) { + return; + } + SPItem *item = SP_ITEM(self->owner); + +// TODO kill naughty naughty #if 0 +#if 0 + Geom::Affine m(*mp); + if (!(m.is_translation())) { + return; + } + Geom::Affine const t(item->transform); + Geom::Affine clone_move = t.inverse() * m * t; + + // Calculate the compensation matrix and the advertized movement matrix. + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + //clone_move = clone_move.inverse(); + advertized_move.set_identity(); + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + clone_move = clone_move.inverse() * m; + advertized_move = m; + } else { + g_assert_not_reached(); + } + + // Commit the compensation. + item->transform *= clone_move; + sp_item_write_transform(item, item->getRepr(), item->transform, &advertized_move); +#else + (void)mp; + (void)original; +#endif + + self->sourceDirty = true; + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +static void +sp_usepath_delete_self(SPObject */*deleted*/, SPUsePath *offset) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + // leave it be. just forget about the source + offset->quit_listening(); + offset->unlink(); + if (offset->user_unlink) + offset->user_unlink(offset->owner); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + offset->owner->deleteObject(); + } +} + +static void +sp_usepath_source_modified(SPObject */*iSource*/, guint /*flags*/, SPUsePath *offset) +{ + offset->sourceDirty = true; + offset->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPUsePath::refresh_source() +{ + sourceDirty = false; + + if (originalPath != nullptr) { + originalPath->unref(); + } + + SPObject *refobj = sourceObject; + if ( refobj == nullptr ) return; + + SPItem *item = SP_ITEM(refobj); + + if (SP_IS_SHAPE(item)) { + SPCurve *originalCurve = SP_SHAPE(item)->getCurve(); + if (originalCurve != nullptr) { + originalPath = originalCurve->copy(); + } else { + sourceDirty = true; + } + } else if (SP_IS_TEXT(item)) { + originalPath = SP_TEXT(item)->getNormalizedBpath()->copy(); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-use-reference.h b/src/object/sp-use-reference.h new file mode 100644 index 0000000..0cb207d --- /dev/null +++ b/src/object/sp-use-reference.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_USE_REFERENCE_H +#define SEEN_SP_USE_REFERENCE_H + +/* + * The reference corresponding to href of <use> element. + * + * Copyright (C) 2004 Bulia Byak + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include "sp-item.h" +#include "uri-references.h" + +class SPCurve; + +namespace Inkscape { +namespace XML { +class Node; +} +} + + +class SPUseReference : public Inkscape::URIReference { +public: + SPUseReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return static_cast<SPItem *>(URIReference::getObject()); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + + +class SPUsePath : public SPUseReference { +public: + SPCurve *originalPath; + bool sourceDirty; + + SPObject *owner; + char *sourceHref; + Inkscape::XML::Node *sourceRepr; + SPObject *sourceObject; + + sigc::connection _modified_connection; + sigc::connection _delete_connection; + sigc::connection _changed_connection; + sigc::connection _transformed_connection; + + SPUsePath(SPObject* i_owner); + ~SPUsePath() override; + + void link(char* to); + void unlink(); + void start_listening(SPObject* to); + void quit_listening(); + void refresh_source(); + + void (*user_unlink) (SPObject *user); +}; + +#endif /* !SEEN_SP_USE_REFERENCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-use.cpp b/src/object/sp-use.cpp new file mode 100644 index 0000000..f1cf6d0 --- /dev/null +++ b/src/object/sp-use.cpp @@ -0,0 +1,767 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG <use> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <2geom/transforms.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> + +#include "bad-uri-exception.h" +#include "display/drawing-group.h" +#include "attributes.h" +#include "document.h" +#include "sp-clippath.h" +#include "sp-mask.h" +#include "sp-factory.h" +#include "sp-flowregion.h" +#include "uri.h" +#include "print.h" +#include "xml/repr.h" +#include "svg/svg.h" +#include "preferences.h" +#include "style.h" + +#include "sp-use.h" +#include "sp-symbol.h" +#include "sp-root.h" +#include "sp-use-reference.h" +#include "sp-shape.h" +#include "sp-text.h" +#include "sp-flowtext.h" + +SPUse::SPUse() + : SPItem(), + SPDimensions(), + child(nullptr), + href(nullptr), + ref(new SPUseReference(this)), + _delete_connection(), + _changed_connection(), + _transformed_connection() +{ + this->x.unset(); + this->y.unset(); + this->width.unset(SVGLength::PERCENT, 1.0, 1.0); + this->height.unset(SVGLength::PERCENT, 1.0, 1.0); + + this->_changed_connection = this->ref->changedSignal().connect( + sigc::hide(sigc::hide(sigc::mem_fun(this, &SPUse::href_changed))) + ); +} + +SPUse::~SPUse() { + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + this->ref->detach(); + delete this->ref; + this->ref = nullptr; +} + +void SPUse::build(SPDocument *document, Inkscape::XML::Node *repr) { + SPItem::build(document, repr); + + this->readAttr( "x" ); + this->readAttr( "y" ); + this->readAttr( "width" ); + this->readAttr( "height" ); + this->readAttr( "xlink:href" ); + + // We don't need to create child here: + // reading xlink:href will attach ref, and that will cause the changed signal to be emitted, + // which will call SPUse::href_changed, and that will take care of the child +} + +void SPUse::release() { + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + this->_delete_connection.disconnect(); + this->_changed_connection.disconnect(); + this->_transformed_connection.disconnect(); + + g_free(this->href); + this->href = nullptr; + + this->ref->detach(); + + SPItem::release(); +} + +void SPUse::set(SPAttributeEnum key, const gchar* value) { + switch (key) { + case SP_ATTR_X: + this->x.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_Y: + this->y.readOrUnset(value); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_WIDTH: + this->width.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_HEIGHT: + this->height.readOrUnset(value, SVGLength::PERCENT, 1.0, 1.0); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + break; + + case SP_ATTR_XLINK_HREF: { + if ( value && this->href && ( strcmp(value, this->href) == 0 ) ) { + /* No change, do nothing. */ + } else { + g_free(this->href); + this->href = nullptr; + + if (value) { + // First, set the href field, because SPUse::href_changed will need it. + this->href = g_strdup(value); + + // Now do the attaching, which emits the changed signal. + try { + this->ref->attach(Inkscape::URI(value)); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + this->ref->detach(); + } + } else { + this->ref->detach(); + } + } + break; + } + + default: + SPItem::set(key, value); + break; + } +} + +Inkscape::XML::Node* SPUse::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:use"); + } + + SPItem::write(xml_doc, repr, flags); + + sp_repr_set_svg_double(repr, "x", this->x.computed); + sp_repr_set_svg_double(repr, "y", this->y.computed); + repr->setAttribute("width", sp_svg_length_write_with_units(this->width)); + repr->setAttribute("height", sp_svg_length_write_with_units(this->height)); + + if (this->ref->getURI()) { + auto uri_string = this->ref->getURI()->str(); + repr->setAttributeOrRemoveIfEmpty("xlink:href", uri_string); + } + + SPShape *shape = dynamic_cast<SPShape *>(child); + if (shape) { + shape->set_shape(); // evaluate SPCurve of child + } else { + SPText *text = dynamic_cast<SPText *>(child); + if (text) { + text->rebuildLayout(); // refresh Layout, LP Bug 1339305 + } else { + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(child); + if (flowtext) { + SPFlowregion *flowregion = dynamic_cast<SPFlowregion *>(flowtext->firstChild()); + if (flowregion) { + flowregion->UpdateComputed(); + } + flowtext->rebuildLayout(); + } + } + } + + return repr; +} + +Geom::OptRect SPUse::bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const { + Geom::OptRect bbox; + + if (this->child) { + Geom::Affine const ct(child->transform * Geom::Translate(this->x.computed, this->y.computed) * transform ); + + bbox = child->bounds(bboxtype, ct); + } + + return bbox; +} + +void SPUse::print(SPPrintContext* ctx) { + bool translated = false; + + if ((this->x._set && this->x.computed != 0) || (this->y._set && this->y.computed != 0)) { + Geom::Affine tp(Geom::Translate(this->x.computed, this->y.computed)); + ctx->bind(tp, 1.0); + translated = true; + } + + if (this->child) { + this->child->invoke_print(ctx); + } + + if (translated) { + ctx->release(); + } +} + +const char* SPUse::displayName() const { + if (dynamic_cast<SPSymbol *>(child)) { + return _("Symbol"); + } else { + return _("Clone"); + } +} + +gchar* SPUse::description() const { + if (child) { + if ( dynamic_cast<SPSymbol *>(child) ) { + if (child->title()) { + return g_strdup_printf(_("called %s"), Glib::Markup::escape_text(Glib::ustring( g_dpgettext2(nullptr, "Symbol", child->title()))).c_str()); + } else if (child->getAttribute("id")) { + return g_strdup_printf(_("called %s"), Glib::Markup::escape_text(Glib::ustring( g_dpgettext2(nullptr, "Symbol", child->getAttribute("id")))).c_str()); + } else { + return g_strdup_printf(_("called %s"), _("Unnamed Symbol")); + } + } + + static unsigned recursion_depth = 0; + + if (recursion_depth >= 4) { + /* TRANSLATORS: Used for statusbar description for long <use> chains: + * "Clone of: Clone of: ... in Layer 1". */ + return g_strdup(_("...")); + /* We could do better, e.g. chasing the href chain until we reach something other than + * a <use>, and giving its description. */ + } + + ++recursion_depth; + char *child_desc = this->child->detailedDescription(); + --recursion_depth; + + char *ret = g_strdup_printf(_("of: %s"), child_desc); + g_free(child_desc); + + return ret; + } else { + return g_strdup(_("[orphaned]")); + } +} + +Inkscape::DrawingItem* SPUse::show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) { + + // std::cout << "SPUse::show: " << (getId()?getId():"null") << std::endl; + Inkscape::DrawingGroup *ai = new Inkscape::DrawingGroup(drawing); + ai->setPickChildren(false); + this->context_style = this->style; + ai->setStyle(this->style, this->context_style); + + if (this->child) { + Inkscape::DrawingItem *ac = this->child->invoke_show(drawing, key, flags); + + if (ac) { + ai->prependChild(ac); + } + + Geom::Translate t(this->x.computed, this->y.computed); + ai->setChildTransform(t); + } + + return ai; +} + +void SPUse::hide(unsigned int key) { + if (this->child) { + this->child->invoke_hide(key); + } + +// SPItem::onHide(key); +} + + +/** + * Returns the ultimate original of a SPUse (i.e. the first object in the chain of its originals + * which is not an SPUse). If no original is found, NULL is returned (it is the responsibility + * of the caller to make sure that this is handled correctly). + * + * Note that the returned is the clone object, i.e. the child of an SPUse (of the argument one for + * the trivial case) and not the "true original". + */ +SPItem *SPUse::root() { + SPItem *orig = this->child; + + SPUse *use = dynamic_cast<SPUse *>(orig); + while (orig && use) { + orig = use->child; + use = dynamic_cast<SPUse *>(orig); + } + + return orig; +} + +SPItem const *SPUse::root() const { + return const_cast<SPUse*>(this)->root(); +} + +/** + * Get the number of dereferences or calls to get_original() needed to get an object + * which is not an svg:use. Returns -1 if there is no original object. + */ +int SPUse::cloneDepth() const { + unsigned depth = 1; + SPItem *orig = this->child; + + while (orig && dynamic_cast<SPUse *>(orig)) { + ++depth; + orig = dynamic_cast<SPUse *>(orig)->child; + } + + if (!orig) { + return -1; + } else { + return depth; + } +} + +/** + * Returns the effective transform that goes from the ultimate original to given SPUse, both ends + * included. + */ +Geom::Affine SPUse::get_root_transform() { + //track the ultimate source of a chain of uses + SPObject *orig = this->child; + + std::vector<SPItem*> chain; + chain.push_back(this); + + while (dynamic_cast<SPUse *>(orig)) { + chain.push_back(dynamic_cast<SPItem *>(orig)); + orig = dynamic_cast<SPUse *>(orig)->child; + } + + chain.push_back(dynamic_cast<SPItem *>(orig)); + + // calculate the accumulated transform, starting from the original + Geom::Affine t(Geom::identity()); + + for (auto i=chain.rbegin(); i!=chain.rend(); ++i) { + SPItem *i_tem = *i; + + // "An additional transformation translate(x,y) is appended to the end (i.e., + // right-side) of the transform attribute on the generated 'g', where x and y + // represent the values of the x and y attributes on the 'use' element." - http://www.w3.org/TR/SVG11/struct.html#UseElement + SPUse *i_use = dynamic_cast<SPUse *>(i_tem); + if (i_use) { + if ((i_use->x._set && i_use->x.computed != 0) || (i_use->y._set && i_use->y.computed != 0)) { + t = t * Geom::Translate(i_use->x._set ? i_use->x.computed : 0, i_use->y._set ? i_use->y.computed : 0); + } + } + + t *= i_tem->transform; + } + return t; +} + +/** + * Returns the transform that leads to the use from its immediate original. + * Does not include the original's transform if any. + */ +Geom::Affine SPUse::get_parent_transform() { + Geom::Affine t(Geom::identity()); + + if ((this->x._set && this->x.computed != 0) || (this->y._set && this->y.computed != 0)) { + t *= Geom::Translate(this->x._set ? this->x.computed : 0, this->y._set ? this->y.computed : 0); + } + + t *= this->transform; + return t; +} + +/** + * Sensing a movement of the original, this function attempts to compensate for it in such a way + * that the clone stays unmoved or moves in parallel (depending on user setting) regardless of the + * clone's transform. + */ +void SPUse::move_compensate(Geom::Affine const *mp) { + // the clone is orphaned; or this is not a real use, but a clone of another use; + // we skip it, otherwise duplicate compensation will occur + if (this->cloned) { + return; + } + + // never compensate uses which are used in flowtext + if (parent && dynamic_cast<SPFlowregion *>(parent)) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_PARALLEL); + // user wants no compensation + if (mode == SP_CLONE_COMPENSATION_NONE) + return; + + Geom::Affine m(*mp); + Geom::Affine t = this->get_parent_transform(); + Geom::Affine clone_move = t.inverse() * m * t; + + // this is not a simple move, do not try to compensate + if (!(m.isTranslation())){ + //BUT move clippaths accordingly. + //if clone has a clippath, move it accordingly + if (getClipObject()) { + for (auto &clip : getClipObject()->children) { + SPItem *item = (SPItem*) &clip; + if(item){ + item->transform *= m; + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + if (getMaskObject()) { + for (auto &mask : getMaskObject()->children) { + SPItem *item = (SPItem*) &mask; + if(item){ + item->transform *= m; + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + return; + } + + // restore item->transform field from the repr, in case it was changed by seltrans + this->readAttr ("transform"); + + + // calculate the compensation matrix and the advertized movement matrix + Geom::Affine advertized_move; + if (mode == SP_CLONE_COMPENSATION_PARALLEL) { + clone_move = clone_move.inverse() * m; + advertized_move = m; + } else if (mode == SP_CLONE_COMPENSATION_UNMOVED) { + clone_move = clone_move.inverse(); + advertized_move.setIdentity(); + } else { + g_assert_not_reached(); + } + + //if clone has a clippath, move it accordingly + if (getClipObject()) { + for (auto &clip : getClipObject()->children) { + SPItem *item = (SPItem*) &clip; + if(item){ + item->transform *= clone_move.inverse(); + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + if (getMaskObject()) { + for (auto &mask : getMaskObject()->children) { + SPItem *item = (SPItem*) &mask; + if(item){ + item->transform *= clone_move.inverse(); + Geom::Affine identity; + item->doWriteTransform(item->transform, &identity); + } + } + } + + + // commit the compensation + this->transform *= clone_move; + this->doWriteTransform(this->transform, &advertized_move); + this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void SPUse::href_changed() { + this->_delete_connection.disconnect(); + this->_transformed_connection.disconnect(); + + if (this->child) { + this->detach(this->child); + this->child = nullptr; + } + + if (this->href) { + SPItem *refobj = this->ref->getObject(); + + if (refobj) { + Inkscape::XML::Node *childrepr = refobj->getRepr(); + + SPObject* obj = SPFactory::createObject(NodeTraits::get_type_string(*childrepr)); + + SPItem *item = dynamic_cast<SPItem *>(obj); + if (item) { + child = item; + + this->attach(this->child, this->lastChild()); + sp_object_unref(this->child, this); + + this->child->invoke_build(this->document, childrepr, TRUE); + + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingItem *ai = this->child->invoke_show(v->arenaitem->drawing(), v->key, v->flags); + + if (ai) { + v->arenaitem->prependChild(ai); + } + } + } else { + delete obj; + g_warning("Tried to create svg:use from invalid object"); + } + + this->_delete_connection = refobj->connectDelete( + sigc::hide(sigc::mem_fun(this, &SPUse::delete_self)) + ); + + this->_transformed_connection = refobj->connectTransformed( + sigc::hide(sigc::mem_fun(this, &SPUse::move_compensate)) + ); + } + } +} + +void SPUse::delete_self() { + // always delete uses which are used in flowtext + if (parent && dynamic_cast<SPFlowregion *>(parent)) { + deleteObject(); + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint const mode = prefs->getInt("/options/cloneorphans/value", + SP_CLONE_ORPHANS_UNLINK); + + if (mode == SP_CLONE_ORPHANS_UNLINK) { + this->unlink(); + } else if (mode == SP_CLONE_ORPHANS_DELETE) { + this->deleteObject(); + } +} + +void SPUse::update(SPCtx *ctx, unsigned flags) { + // std::cout << "SPUse::update: " << (getId()?getId():"null") << std::endl; + SPItemCtx *ictx = (SPItemCtx *) ctx; + SPItemCtx cctx = *ictx; + + unsigned childflags = flags; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + childflags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + childflags &= SP_OBJECT_MODIFIED_CASCADE; + + /* Set up child viewport */ + this->calcDimsFromParentViewport(ictx); + + childflags &= ~SP_OBJECT_USER_MODIFIED_FLAG_B; + + if (this->child) { + sp_object_ref(this->child); + + // viewport is only changed if referencing a symbol or svg element + if( SP_IS_SYMBOL(this->child) || SP_IS_ROOT(this->child) ) { + cctx.viewport = Geom::Rect::from_xywh(0, 0, this->width.computed, this->height.computed); + cctx.i2vp = Geom::identity(); + } + + if (childflags || (this->child->uflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + SPItem const *chi = dynamic_cast<SPItem const *>(child); + g_assert(chi != nullptr); + cctx.i2doc = chi->transform * ictx->i2doc; + cctx.i2vp = chi->transform * ictx->i2vp; + this->child->updateDisplay((SPCtx *)&cctx, childflags); + } + + sp_object_unref(this->child); + } + + SPItem::update(ctx, flags); + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem); + this->context_style = this->style; + g->setStyle(this->style, this->context_style); + } + } + + /* As last step set additional transform of arena group */ + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem); + Geom::Affine t(Geom::Translate(this->x.computed, this->y.computed)); + g->setChildTransform(t); + } +} + +void SPUse::modified(unsigned int flags) { + // std::cout << "SPUse::modified: " << (getId()?getId():"null") << std::endl; + if (flags & SP_OBJECT_MODIFIED_FLAG) { + flags |= SP_OBJECT_PARENT_MODIFIED_FLAG; + } + + flags &= SP_OBJECT_MODIFIED_CASCADE; + + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + for (SPItemView *v = this->display; v != nullptr; v = v->next) { + Inkscape::DrawingGroup *g = dynamic_cast<Inkscape::DrawingGroup *>(v->arenaitem); + this->context_style = this->style; + g->setStyle(this->style, this->context_style); + } + } + + if (child) { + sp_object_ref(child); + + if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) { + child->emitModified(flags); + } + + sp_object_unref(child); + } +} + +SPItem *SPUse::unlink() { + Inkscape::XML::Node *repr = this->getRepr(); + + if (!repr) { + return nullptr; + } + + Inkscape::XML::Node *parent = repr->parent(); + SPDocument *document = this->document; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // Track the ultimate source of a chain of uses. + SPItem *orig = this->root(); + + if (!orig) { + return nullptr; + } + + // Calculate the accumulated transform, starting from the original. + Geom::Affine t = this->get_root_transform(); + + Inkscape::XML::Node *copy = nullptr; + + if (auto symbol = dynamic_cast<SPSymbol *>(orig)) { + // make a group, copy children + copy = xml_doc->createElement("svg:g"); + + for (Inkscape::XML::Node *child = orig->getRepr()->firstChild() ; child != nullptr; child = child->next()) { + Inkscape::XML::Node *newchild = child->duplicate(xml_doc); + copy->appendChild(newchild); + } + + // viewBox transformation + t = symbol->c2p * t; + } else { // just copy + copy = orig->getRepr()->duplicate(xml_doc); + } + + // Add the duplicate repr just after the existing one. + parent->addChild(copy, repr); + + // Retrieve the SPItem of the resulting repr. + SPObject *unlinked = document->getObjectByRepr(copy); + + // Merge style from the use. + unlinked->style->merge( this->style ); + unlinked->style->cascade( unlinked->parent->style ); + unlinked->updateRepr(); + + // Hold onto our SPObject and repr for now. + sp_object_ref(this); + Inkscape::GC::anchor(repr); + + // Remove ourselves, not propagating delete events to avoid a + // chain-reaction with other elements that might reference us. + this->deleteObject(false); + + // Give the copy our old id and let go of our old repr. + copy->setAttribute("id", repr->attribute("id")); + Inkscape::GC::release(repr); + + // Remove tiled clone attrs. + copy->removeAttribute("inkscape:tiled-clone-of"); + copy->removeAttribute("inkscape:tile-w"); + copy->removeAttribute("inkscape:tile-h"); + copy->removeAttribute("inkscape:tile-cx"); + copy->removeAttribute("inkscape:tile-cy"); + + // Establish the succession and let go of our object. + this->setSuccessor(unlinked); + sp_object_unref(this); + + SPItem *item = dynamic_cast<SPItem *>(unlinked); + g_assert(item != nullptr); + + // Set the accummulated transform. + { + Geom::Affine nomove(Geom::identity()); + // Advertise ourselves as not moving. + item->doWriteTransform(t, &nomove); + } + + return item; +} + +SPItem *SPUse::get_original() { + SPItem *ref = nullptr; + + if (this->ref){ + ref = this->ref->getObject(); + } + + return ref; +} + +void SPUse::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const { + SPItem const *root = this->root(); + + if (!root) { + return; + } + + root->snappoints(p, snapprefs); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/object/sp-use.h b/src/object/sp-use.h new file mode 100644 index 0000000..34b99f6 --- /dev/null +++ b/src/object/sp-use.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_USE_H +#define SEEN_SP_USE_H + +/* + * SVG <use> implementation + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2014 Authors + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "svg/svg-length.h" +#include "sp-dimensions.h" +#include "sp-item.h" +#include "enums.h" + +class SPUseReference; + +class SPUse : public SPItem, public SPDimensions { +public: + SPUse(); + ~SPUse() override; + + // item built from the original's repr (the visible clone) + // relative to the SPUse itself, it is treated as a child, similar to a grouped item relative to its group + SPItem *child; + + // SVG attrs + char *href; + + // the reference to the original object + SPUseReference *ref; + + // a sigc connection for delete notifications + sigc::connection _delete_connection; + sigc::connection _changed_connection; + + // a sigc connection for transformed signal, used to do move compensation + sigc::connection _transformed_connection; + + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + void set(SPAttributeEnum key, char const *value) override; + Inkscape::XML::Node* write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, unsigned int flags) override; + void update(SPCtx* ctx, unsigned int flags) override; + void modified(unsigned int flags) override; + + Geom::OptRect bbox(Geom::Affine const &transform, SPItem::BBoxType bboxtype) const override; + const char* displayName() const override; + char* description() const override; + void print(SPPrintContext *ctx) override; + Inkscape::DrawingItem* show(Inkscape::Drawing &drawing, unsigned int key, unsigned int flags) override; + void hide(unsigned int key) override; + void snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const override; + + SPItem *root(); + SPItem const *root() const; + int cloneDepth() const; + + SPItem *unlink(); + SPItem *get_original(); + Geom::Affine get_parent_transform(); + Geom::Affine get_root_transform(); + +private: + void href_changed(); + void move_compensate(Geom::Affine const *mp); + void delete_self(); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/uri-references.cpp b/src/object/uri-references.cpp new file mode 100644 index 0000000..64e4c8b --- /dev/null +++ b/src/object/uri-references.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Helper methods for resolving URI References + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Marc Jeanmougin + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri-references.h" + +#include <iostream> +#include <cstring> + +#include <glibmm/miscutils.h> +#include "live_effects/lpeobject.h" +#include "bad-uri-exception.h" +#include "document.h" +#include "sp-object.h" +#include "uri.h" +#include "extract-uri.h" +#include "sp-tag-use.h" + +namespace Inkscape { + +URIReference::URIReference(SPObject *owner) + : _owner(owner) + , _owner_document(nullptr) + , _obj(nullptr) + , _uri(nullptr) +{ + g_assert(_owner != nullptr); + /* FIXME !!! attach to owner's destroy signal to clean up in case */ +} + +URIReference::URIReference(SPDocument *owner_document) + : _owner(nullptr) + , _owner_document(owner_document) + , _obj(nullptr) + , _uri(nullptr) +{ + g_assert(_owner_document != nullptr); +} + +URIReference::~URIReference() +{ + detach(); +} + +/* + * The main ideas here are: + * (1) "If we are inside a clone, then we can accept if and only if our "original thing" can accept the reference" + * (this caused problems when there are clones because a change in ids triggers signals for the object hrefing this id, + * but also its cloned reprs(descendants of <use> referencing an ancestor of the href'ing object)). + * + * (2) Once we have an (potential owner) object, it can accept a href to obj, iff the graph of objects where directed + * edges are + * either parent->child relations , *** or href'ing to href'ed *** relations, stays acyclic. + * We can go either from owner and up in the tree, or from obj and down, in either case this will be in the worst case + *linear in the number of objects. + * There are no easy objects allowing to do the second proposition, while "hrefList" is a "list of objects href'ing us", + *so we'll take this. + * Then we keep a set of already visited elements, and do a DFS on this graph. if we find obj, then BOOM. + */ + +bool URIReference::_acceptObject(SPObject *obj) const +{ + // we go back following hrefList and parent to find if the object already references ourselves indirectly + std::set<SPObject *> done; + SPObject *owner = getOwner(); + //allow LPE as owner has any URI attached + LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(obj); + if (!owner || lpobj) + return true; + + while (owner->cloned) { + if(!owner->clone_original)//happens when the clone is existing and linking to something, even before the original objects exists. + //for instance, it can happen when you paste a filtered object in a already cloned group: The construction of the + //clone representation of the filtered object will finish before the original object, so the cloned repr will + //have to _accept the filter even though the original does not exist yet. In that case, we'll accept iff the parent of the + //original can accept it: loops caused by other relations than parent-child would be prevented when created on their base object. + //Fixes bug 1636533. + owner = owner->parent; + else + owner = owner->clone_original; + } + // once we have the "original" object (hopefully) we look at who is referencing it + if (obj == owner) + return false; + std::list<SPObject *> todo(owner->hrefList); + todo.push_front(owner->parent); + while (!todo.empty()) { + SPObject *e = todo.front(); + todo.pop_front(); + if (!dynamic_cast<SPObject *>(e)) + continue; + if (done.insert(e).second) { + if (e == obj) { + return false; + } + todo.push_front(e->parent); + todo.insert(todo.begin(), e->hrefList.begin(), e->hrefList.end()); + } + } + return true; +} + +void URIReference::attach(const URI &uri) +{ + SPDocument *document = nullptr; + + // Attempt to get the document that contains the URI + if (_owner) { + document = _owner->document; + } else if (_owner_document) { + document = _owner_document; + } + + // createChildDoc() assumes that the referenced file is an SVG. + // PNG and JPG files are allowed (in the case of feImage). + gchar const *filename = uri.getPath() ? uri.getPath() : ""; + bool skip = false; + if (g_str_has_suffix(filename, ".jpg") || g_str_has_suffix(filename, ".JPG") || + g_str_has_suffix(filename, ".png") || g_str_has_suffix(filename, ".PNG")) { + skip = true; + } + + // The path contains references to separate document files to load. + if (document && uri.getPath() && !skip) { + char const *base = document->getDocumentBase(); + auto absuri = URI::from_href_and_basedir(uri.str().c_str(), base); + std::string path; + + try { + path = absuri.toNativeFilename(); + } catch (const Glib::Error &e) { + g_warning("%s", e.what().c_str()); + } + + if (!path.empty()) { + document = document->createChildDoc(path); + } else { + document = nullptr; + } + } + if (!document) { + g_warning("Can't get document for referenced URI: %s", filename); + return; + } + + gchar const *fragment = uri.getFragment(); + if (!uri.isRelative() || uri.getQuery() || !fragment) { + throw UnsupportedURIException(); + } + + /* FIXME !!! real xpointer support should be delegated to document */ + /* for now this handles the minimal xpointer form that SVG 1.0 + * requires of us + */ + gchar *id = nullptr; + if (!strncmp(fragment, "xpointer(", 9)) { + /* FIXME !!! this is wasteful */ + /* FIXME: It looks as though this is including "))" in the id. I suggest moving + the strlen calculation and validity testing to before strdup, and copying just + the id without the "))". -- pjrm */ + if (!strncmp(fragment, "xpointer(id(", 12)) { + id = g_strdup(fragment + 12); + size_t const len = strlen(id); + if (len < 3 || strcmp(id + len - 2, "))")) { + g_free(id); + throw MalformedURIException(); + } + } else { + throw UnsupportedURIException(); + } + } else { + id = g_strdup(fragment); + } + + /* FIXME !!! validate id as an NCName somewhere */ + + _connection.disconnect(); + delete _uri; + _uri = new URI(uri); + + _setObject(document->getObjectById(id)); + _connection = document->connectIdChanged(id, sigc::mem_fun(*this, &URIReference::_setObject)); + g_free(id); +} + +bool URIReference::try_attach(char const *uri) +{ + if (uri && uri[0]) { + try { + attach(Inkscape::URI(uri)); + return true; + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + } + } + detach(); + return false; +} + +void URIReference::detach() +{ + _connection.disconnect(); + delete _uri; + _uri = nullptr; + _setObject(nullptr); +} + +void URIReference::_setObject(SPObject *obj) +{ + if (obj && !_acceptObject(obj)) { + obj = nullptr; + } + + if (obj == _obj) + return; + + SPObject *old_obj = _obj; + _obj = obj; + + _release_connection.disconnect(); + if (_obj && (!_owner || !_owner->cloned)) { + _obj->hrefObject(_owner); + _release_connection = _obj->connectRelease(sigc::mem_fun(*this, &URIReference::_release)); + } + _changed_signal.emit(old_obj, _obj); + if (old_obj && (!_owner || !_owner->cloned)) { + /* release the old object _after_ the signal emission */ + old_obj->unhrefObject(_owner); + } +} + +/* If an object is deleted, current semantics require that we release + * it on its "release" signal, rather than later, when its ID is actually + * unregistered from the document. + */ +void URIReference::_release(SPObject *obj) +{ + g_assert(_obj == obj); + _setObject(nullptr); +} + +} /* namespace Inkscape */ + + + +SPObject *sp_css_uri_reference_resolve(SPDocument *document, const gchar *uri) +{ + SPObject *ref = nullptr; + + if (document && uri && (strncmp(uri, "url(", 4) == 0)) { + auto trimmed = extract_uri(uri); + if (!trimmed.empty()) { + ref = sp_uri_reference_resolve(document, trimmed.c_str()); + } + } + + return ref; +} + +SPObject *sp_uri_reference_resolve(SPDocument *document, const gchar *uri) +{ + SPObject *ref = nullptr; + + if (uri && (*uri == '#')) { + ref = document->getObjectById(uri + 1); + } + + return ref; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/uri-references.h b/src/object/uri-references.h new file mode 100644 index 0000000..2b89163 --- /dev/null +++ b/src/object/uri-references.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_URI_REFERENCES_H +#define SEEN_SP_URI_REFERENCES_H + +/* + * Helper methods for resolving URI References + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <vector> +#include <set> +#include <sigc++/connection.h> +#include <sigc++/trackable.h> + +class SPObject; +class SPDocument; + +namespace Inkscape { + +class URI; + +/** + * A class encapsulating a reference to a particular URI; observers can + * be notified when the URI comes to reference a different SPObject. + * + * The URIReference increments and decrements the SPObject's hrefcount + * automatically. + * + * @see SPObject + */ +class URIReference : public sigc::trackable { +public: + /** + * Constructor. + * + * @param owner The object on whose behalf this URIReference + * is holding a reference to the target object. + */ + URIReference(SPObject *owner); + URIReference(SPDocument *owner_document); + + /* Definition-less to prevent accidental use. */ + void operator=(URIReference const& ref) = delete; + + /** + * Destructor. Calls shutdown() if the reference has not been + * shut down yet. + */ + virtual ~URIReference(); + + /** + * Attaches to a URI, relative to the specified document. + * + * Throws a BadURIException if the URI is unsupported, + * or the fragment identifier is xpointer and malformed. + * + * @param uri the URI to watch + */ + void attach(URI const& uri); + + /** + * Try to attach to a URI. Return false if URL is malformed and detach any + * previous attachment. + */ + bool try_attach(char const *uri); + + /** + * Detaches from the currently attached URI target, if any; + * the current referrent is signaled as NULL. + */ + void detach(); + + /** + * @brief Returns a pointer to the current referrent of the + * attached URI, or NULL. + * + * @return a pointer to the referenced SPObject or NULL + */ + SPObject *getObject() const { return _obj; } + + /** + * @brief Returns a pointer to the URIReference's owner + * + * @return a pointer to the URIReference's owner + */ + SPObject *getOwner() const { return _owner; } + + /** + * Accessor for the referrent change notification signal; + * this signal is emitted whenever the URIReference's + * referrent changes. + * + * Signal handlers take two parameters: the old and new + * referrents. + * + * @returns a signal + */ + sigc::signal<void, SPObject *, SPObject *> changedSignal() { + return _changed_signal; + } + + /** + * Returns a pointer to a URI containing the currently attached + * URI, or NULL if no URI is currently attached. + * + * @returns the currently attached URI, or NULL + */ + URI const* getURI() const { + return _uri; + } + + /** + * Returns true if there is currently an attached URI + * + * @returns true if there is an attached URI + */ + bool isAttached() const { + return (bool)_uri; + } + + SPDocument *getOwnerDocument() { return _owner_document; } + SPObject *getOwnerObject() { return _owner; } + +protected: + virtual bool _acceptObject(SPObject *obj) const; +private: + SPObject *_owner; + SPDocument *_owner_document; + sigc::connection _connection; + sigc::connection _release_connection; + SPObject *_obj; + URI *_uri; + + sigc::signal<void, SPObject *, SPObject *> _changed_signal; + + void _setObject(SPObject *object); + void _release(SPObject *object); +}; + +} + +/** + * Resolves an item referenced by a URI in CSS form contained in "url(...)" + */ +SPObject* sp_css_uri_reference_resolve( SPDocument *document, const char *uri ); + +SPObject *sp_uri_reference_resolve (SPDocument *document, const char *uri); + +#endif // SEEN_SP_URI_REFERENCES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/object/uri.cpp b/src/object/uri.cpp new file mode 100644 index 0000000..05539a6 --- /dev/null +++ b/src/object/uri.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "uri.h" + +#include <cstring> + +#include <giomm/contenttype.h> +#include <giomm/file.h> +#include <glibmm/base64.h> +#include <glibmm/convert.h> +#include <glibmm/ustring.h> +#include <glibmm/miscutils.h> + +#include "bad-uri-exception.h" + +namespace Inkscape { + +auto const URI_ALLOWED_NON_ALNUM = "!#$%&'()*+,-./:;=?@_~"; + +/** + * Return true if the given URI string contains characters that need escaping. + * + * Note: It does not check if valid characters appear in invalid context (e.g. + * '%' not followed by two hex digits). + */ +static bool uri_needs_escaping(char const *uri) +{ + for (auto *p = uri; *p; ++p) { + if (!g_ascii_isalnum(*p) && !strchr(URI_ALLOWED_NON_ALNUM, *p)) { + return true; + } + } + return false; +} + +URI::URI() { + init(xmlCreateURI()); +} + +URI::URI(gchar const *preformed, char const *baseuri) +{ + xmlURIPtr uri; + if (!preformed) { + throw MalformedURIException(); + } + + // check for invalid characters, escape if needed + xmlChar *escaped = nullptr; + if (uri_needs_escaping(preformed)) { + escaped = xmlURIEscapeStr( // + (xmlChar const *)preformed, // + (xmlChar const *)URI_ALLOWED_NON_ALNUM); + preformed = (decltype(preformed))escaped; + } + + // make absolute + xmlChar *full = nullptr; + if (baseuri) { + full = xmlBuildURI( // + (xmlChar const *)preformed, // + (xmlChar const *)baseuri); +#if LIBXML_VERSION < 20905 + // libxml2 bug: "file:/some/file" instead of "file:///some/file" + auto f = (gchar const *)full; + if (f && g_str_has_prefix(f, "file:/") && f[6] != '/') { + auto fixed = std::string(f, 6) + "//" + std::string(f + 6); + xmlFree(full); + full = (xmlChar *)xmlMemStrdup(fixed.c_str()); + } +#endif + preformed = (decltype(preformed))full; + } + + uri = xmlParseURI(preformed); + + if (full) { + xmlFree(full); + } + if (escaped) { + xmlFree(escaped); + } + if (!uri) { + throw MalformedURIException(); + } + init(uri); +} + +URI::URI(char const *preformed, URI const &baseuri) + : URI::URI(preformed, baseuri.str().c_str()) +{ +} + +// From RFC 2396: +// +// URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] +// absoluteURI = scheme ":" ( hier_part | opaque_part ) +// relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] +// +// hier_part = ( net_path | abs_path ) [ "?" query ] +// opaque_part = uric_no_slash *uric +// +// uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | +// "&" | "=" | "+" | "$" | "," +// +// net_path = "//" authority [ abs_path ] +// abs_path = "/" path_segments +// rel_path = rel_segment [ abs_path ] +// +// rel_segment = 1*( unreserved | escaped | +// ";" | "@" | "&" | "=" | "+" | "$" | "," ) +// +// authority = server | reg_name + +bool URI::isOpaque() const { + return getOpaque() != nullptr; +} + +bool URI::isRelative() const { + return !_xmlURIPtr()->scheme; +} + +bool URI::isNetPath() const { + return isRelative() && _xmlURIPtr()->server; +} + +bool URI::isRelativePath() const { + if (isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + return path && path[0] != '/'; + } + return false; +} + +bool URI::isAbsolutePath() const { + if (isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + return path && path[0] == '/'; + } + return false; +} + +const gchar *URI::getScheme() const { + return (gchar *)_xmlURIPtr()->scheme; +} + +const gchar *URI::getPath() const { + return (gchar *)_xmlURIPtr()->path; +} + +const gchar *URI::getQuery() const { + return (gchar *)_xmlURIPtr()->query; +} + +const gchar *URI::getFragment() const { + return (gchar *)_xmlURIPtr()->fragment; +} + +const gchar *URI::getOpaque() const { + if (!isRelative() && !_xmlURIPtr()->server) { + const gchar *path = getPath(); + if (path && path[0] != '/') { + return path; + } + } + return nullptr; +} + +std::string URI::toNativeFilename() const +{ // + auto uristr = str(); + + // remove fragment identifier + if (getFragment() != nullptr) { + uristr.resize(uristr.find('#')); + } + + return Glib::filename_from_uri(uristr); +} + +/* TODO !!! proper error handling */ +URI URI::from_native_filename(gchar const *path) { + gchar *uri = g_filename_to_uri(path, nullptr, nullptr); + URI result(uri); + g_free( uri ); + return result; +} + +URI URI::from_dirname(gchar const *path) +{ + std::string pathstr = path ? path : "."; + + if (!Glib::path_is_absolute(pathstr)) { + pathstr = Glib::build_filename(Glib::get_current_dir(), pathstr); + } + + auto uristr = Glib::filename_to_uri(pathstr); + + if (uristr[uristr.size() - 1] != '/') { + uristr.push_back('/'); + } + + return URI(uristr.c_str()); +} + +URI URI::from_href_and_basedir(char const *href, char const *basedir) +{ + try { + return URI(href, URI::from_dirname(basedir)); + } catch (...) { + return URI(); + } +} + +/** + * Replacement for buggy xmlBuildRelativeURI + * https://gitlab.gnome.org/GNOME/libxml2/merge_requests/12 + * + * Special case: Don't cross filesystem root, e.g. drive letter on Windows. + * This is an optimization to keep things practical, it's not required for correctness. + * + * @param uri an absolute URI + * @param base an absolute URI without any ".." path segments + * @return relative URI if possible, otherwise @a uri unchanged + */ +static std::string build_relative_uri(char const *uri, char const *base) +{ + size_t n_slash = 0; + size_t i = 0; + + // find longest common prefix + for (; uri[i]; ++i) { + if (uri[i] != base[i]) { + break; + } + + if (uri[i] == '/') { + ++n_slash; + } + } + + // URIs must share protocol://server/ + if (n_slash < 3) { + return uri; + } + + // Don't cross filesystem root + if (n_slash == 3 && g_str_has_prefix(base, "file:///") && base[8]) { + return uri; + } + + std::string relative; + + for (size_t j = i; base[j]; ++j) { + if (base[j] == '/') { + relative += "../"; + } + } + + while (uri[i - 1] != '/') { + --i; + } + + relative += (uri + i); + + if (relative.empty() && base[i]) { + relative = "./"; + } + + return relative; +} + +std::string URI::str(char const *baseuri) const +{ + std::string s; + auto saveuri = xmlSaveUri(_xmlURIPtr()); + if (saveuri) { + auto save = (const char *)saveuri; + if (baseuri && baseuri[0]) { + s = build_relative_uri(save, baseuri); + } else { + s = save; + } + xmlFree(saveuri); + } + return s; +} + +std::string URI::getMimeType() const +{ + const char *path = getPath(); + + if (path) { + if (hasScheme("data")) { + for (const char *p = path; *p; ++p) { + if (*p == ';' || *p == ',') { + return std::string(path, p); + } + } + } else { + bool uncertain; + auto type = Gio::content_type_guess(path, nullptr, 0, uncertain); + return Gio::content_type_get_mime_type(type).raw(); + } + } + + return "unknown/unknown"; +} + +std::string URI::getContents() const +{ + if (hasScheme("data")) { + // handle data URIs + + const char *p = getPath(); + const char *tok = nullptr; + + // scan "[<media type>][;base64]," header + for (; *p && *p != ','; ++p) { + if (*p == ';') { + tok = p + 1; + } + } + + // body follows after comma + if (*p != ',') { + g_critical("data URI misses comma"); + } else if (tok && strncmp("base64", tok, p - tok) == 0) { + // base64 encoded body + return Glib::Base64::decode(p + 1); + } else { + // raw body + return p + 1; + } + } else { + // handle non-data URIs with GVfs + auto file = Gio::File::create_for_uri(str()); + + gsize length = 0; + char *buffer = nullptr; + + if (file->load_contents(buffer, length)) { + auto contents = std::string(buffer, buffer + length); + g_free(buffer); + return contents; + } else { + g_critical("failed to load contents from %.100s", str().c_str()); + } + } + + return ""; +} + +bool URI::hasScheme(const char *scheme) const +{ + const char *s = getScheme(); + return s && g_ascii_strcasecmp(s, scheme) == 0; +} + +/** + * If \c s starts with a "%XX" triplet, return its byte value, 0 otherwise. + */ +static int uri_unescape_triplet(const char *s) +{ + int H1, H2; + + if (s[0] == '%' // + && (H1 = g_ascii_xdigit_value(s[1])) != -1 // + && (H2 = g_ascii_xdigit_value(s[2])) != -1) { + return (H1 << 4) | H2; + } + + return 0; +} + +/** + * If \c s starts with a percent-escaped UTF-8 sequence, unescape one code + * point and store it in \c out variable. Do nothing and return 0 if \c s + * doesn't start with UTF-8. + * + * @param[in] s percent-escaped string + * @param[out] out out-buffer, must have at least size 5 + * @return number of bytes read from \c s + */ +static int uri_unescape_utf8_codepoint(const char *s, char *out) +{ + int n = 0; + int value = uri_unescape_triplet(s); + + if ((value >> 5) == /* 0b110 */ 0x6) { + // 110xxxxx 10xxxxxx + n = 2; + } else if ((value >> 4) == /* 0b1110 */ 0xE) { + // 1110xxxx 10xxxxxx 10xxxxxx + n = 3; + } else if ((value >> 3) == /* 0b11110 */ 0x1E) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + n = 4; + } else { + return 0; + } + + out[0] = value; + out[n] = 0; + + for (int i = 1; i < n; ++i) { + value = uri_unescape_triplet(s + (i * 3)); + + if ((value >> 6) != /* 0b10 */ 0x2) { + return 0; + } + + out[i] = value; + } + + return n * 3; +} + +std::string uri_to_iri(const char *uri) +{ + std::string iri; + + char utf8buf[5]; + + for (const char *p = uri; *p;) { + int n = uri_unescape_utf8_codepoint(p, utf8buf); + if (n) { + iri.append(utf8buf); + p += n; + } else { + iri += *p; + p += 1; + } + } + + return iri; +} + +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/uri.h b/src/object/uri.h new file mode 100644 index 0000000..381adec --- /dev/null +++ b/src/object/uri.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_URI_H +#define INKSCAPE_URI_H + +#include <libxml/uri.h> +#include <memory> +#include <string> + +namespace Inkscape { + +/** + * Represents an URI as per RFC 2396. + * + * Typical use-cases of this class: + * - converting between relative and absolute URIs + * - converting URIs to/from filenames (alternative: Glib functions, but those only handle absolute paths) + * - generic handling of data/file/http URIs (e.g. URI::getContents and URI::getMimeType) + * + * Wraps libxml2's URI functions. Direct usage of libxml2's C-API is discouraged if favor of + * Inkscape::URI. (At the time of writing this, no de-factor standard C++ URI library exists, so + * wrapping libxml2 seems like a good solution) + * + * Implementation detail: Immutable type, copies share a ref-counted data pointer. + */ +class URI { +public: + + /* Blank constructor */ + URI(); + + /** + * Constructor from a C-style ASCII string. + * + * @param preformed Properly quoted C-style string to be represented. + * @param baseuri If @a preformed is a relative URI, use @a baseuri to make it absolute + * + * @throw MalformedURIException + */ + explicit URI(char const *preformed, char const *baseuri = nullptr); + explicit URI(char const *preformed, URI const &baseuri); + + /** + * Determines if the URI represented is an 'opaque' URI. + * + * @return \c true if the URI is opaque, \c false if hierarchial. + */ + bool isOpaque() const; + + /** + * Determines if the URI represented is 'relative' as per RFC 2396. + * + * Relative URI references are distinguished by not beginning with a + * scheme name. + * + * @return \c true if the URI is relative, \c false if it is absolute. + */ + bool isRelative() const; + + /** + * Determines if the relative URI represented is a 'net-path' as per RFC 2396. + * + * A net-path is one that starts with "//". + * + * @return \c true if the URI is relative and a net-path, \c false otherwise. + */ + bool isNetPath() const; + + /** + * Determines if the relative URI represented is a 'relative-path' as per RFC 2396. + * + * A relative-path is one that starts with no slashes. + * + * @return \c true if the URI is relative and a relative-path, \c false otherwise. + */ + bool isRelativePath() const; + + /** + * Determines if the relative URI represented is a 'absolute-path' as per RFC 2396. + * + * An absolute-path is one that starts with a single "/". + * + * @return \c true if the URI is relative and an absolute-path, \c false otherwise. + */ + bool isAbsolutePath() const; + + /** + * Return the scheme, e.g.\ "http", or \c NULL if this is not an absolute URI. + */ + const char *getScheme() const; + + /** + * Return the path. + * + * Example: "http://host/foo/bar?query#frag" -> "/foo/bar" + * + * For an opaque URI, this is identical to getOpaque() + */ + const char *getPath() const; + + /** + * Return the query, which is the part between "?" and the optional fragment hash ("#") + */ + const char *getQuery() const; + + /** + * Return the fragment, which is everything after "#" + */ + const char *getFragment() const; + + /** + * For an opaque URI, return everything between the scheme colon (":") and the optional + * fragment hash ("#"). For non-opaque URIs, return NULL. + */ + const char *getOpaque() const; + + /** + * Construct a "file" URI from an absolute filename. + */ + static URI from_native_filename(char const *path); + + /** + * URI of a local directory. The URI path will end with a slash. + */ + static URI from_dirname(char const *path); + + /** + * Convenience function for the common use case given a xlink:href attribute and a local + * directory as the document base. Returns an empty URI on failure. + */ + static URI from_href_and_basedir(char const *href, char const *basedir); + + /** + * Convert this URI to a native filename. + * + * Discards the fragment identifier. + * + * @throw Glib::ConvertError If this is not a "file" URI + */ + std::string toNativeFilename() const; + + /** + * Return the string representation of this URI + * + * @param baseuri Return a relative path if this URI shares protocol and host with @a baseuri + */ + std::string str(char const *baseuri = nullptr) const; + + /** + * Get the MIME type (e.g.\ "image/png") + */ + std::string getMimeType() const; + + /** + * Return the contents of the file + * + * @throw Glib::Error If the URL can't be read + */ + std::string getContents() const; + + /** + * Return a CSS formatted url value + * + * @param baseuri Return a relative path if this URI shares protocol and host with @a baseuri + */ + std::string cssStr(char const *baseuri = nullptr) const { + return "url(" + str(baseuri) + ")"; + } + + /** + * True if the scheme equals the given string (not case sensitive) + */ + bool hasScheme(const char *scheme) const; + +private: + std::shared_ptr<xmlURI> m_shared; + + void init(xmlURI *ptr) { m_shared.reset(ptr, xmlFreeURI); } + + xmlURI *_xmlURIPtr() const { return m_shared.get(); } +}; + +/** + * Unescape the UTF-8 parts of the given URI. + * + * Does not decode non-UTF-8 escape sequences (e.g. reserved ASCII characters). + * Does not do any IDN (internationalized domain name) decoding. + * + * @param uri URI or part of a URI + * @return IRI equivalent of \c uri + */ +std::string uri_to_iri(const char *uri); + +} /* namespace Inkscape */ + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/viewbox.cpp b/src/object/viewbox.cpp new file mode 100644 index 0000000..06a6725 --- /dev/null +++ b/src/object/viewbox.cpp @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * viewBox helper class, common code used by root, symbol, marker, pattern, image, view + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> (code extracted from symbol.cpp) + * Tavmjong Bah <tavmjong@free.fr> + * Johan Engelen + * + * Copyright (C) 2013-2014 Tavmjong Bah, authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/transforms.h> + +#include "viewbox.h" +#include "enums.h" +#include "sp-item.h" + +SPViewBox::SPViewBox() + : viewBox_set(false) + , viewBox() + , aspect_set(false) + , aspect_align(SP_ASPECT_XMID_YMID) // Default per spec + , aspect_clip(SP_ASPECT_MEET) + , c2p(Geom::identity()) +{ +} + +void SPViewBox::set_viewBox(const gchar* value) { + + if (value) { + gchar *eptr = const_cast<gchar*>(value); // const-cast necessary because of const-incorrect interface definition of g_ascii_strtod + + double x = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double y = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double width = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + double height = g_ascii_strtod (eptr, &eptr); + + while (*eptr && ((*eptr == ',') || (*eptr == ' '))) { + eptr++; + } + + if ((width > 0) && (height > 0)) { + /* Set viewbox */ + this->viewBox = Geom::Rect::from_xywh(x, y, width, height); + this->viewBox_set = true; + } else { + this->viewBox_set = false; + } + } else { + this->viewBox_set = false; + } + + // The C++ way? -- not necessarily using iostreams + // std::string sv( value ); + // std::replace( sv.begin(), sv.end(), ',', ' '); + // std::stringstream ss( sv ); + // double x, y, width, height; + // ss >> x >> y >> width >> height; +} + +void SPViewBox::set_preserveAspectRatio(const gchar* value) { + + /* Do setup before, so we can use break to escape */ + this->aspect_set = false; + this->aspect_align = SP_ASPECT_XMID_YMID; // Default per spec + this->aspect_clip = SP_ASPECT_MEET; + + if (value) { + const gchar *p = value; + + while (*p && (*p == 32)) { + p += 1; + } + + if (!*p) { + return; + } + + const gchar *e = p; + + while (*e && (*e != 32)) { + e += 1; + } + + int len = e - p; + + if (len > 8) { // Can't have buffer overflow as 8 < 256 + return; + } + + gchar c[256]; + memcpy (c, value, len); + + c[len] = 0; + + /* Now the actual part */ + unsigned int align = SP_ASPECT_NONE; + if (!strcmp (c, "none")) { + align = SP_ASPECT_NONE; + } else if (!strcmp (c, "xMinYMin")) { + align = SP_ASPECT_XMIN_YMIN; + } else if (!strcmp (c, "xMidYMin")) { + align = SP_ASPECT_XMID_YMIN; + } else if (!strcmp (c, "xMaxYMin")) { + align = SP_ASPECT_XMAX_YMIN; + } else if (!strcmp (c, "xMinYMid")) { + align = SP_ASPECT_XMIN_YMID; + } else if (!strcmp (c, "xMidYMid")) { + align = SP_ASPECT_XMID_YMID; + } else if (!strcmp (c, "xMaxYMid")) { + align = SP_ASPECT_XMAX_YMID; + } else if (!strcmp (c, "xMinYMax")) { + align = SP_ASPECT_XMIN_YMAX; + } else if (!strcmp (c, "xMidYMax")) { + align = SP_ASPECT_XMID_YMAX; + } else if (!strcmp (c, "xMaxYMax")) { + align = SP_ASPECT_XMAX_YMAX; + } else { + return; + } + + unsigned int clip = SP_ASPECT_MEET; + + while (*e && (*e == 32)) { + e += 1; + } + + if (*e) { + if (!strcmp (e, "meet")) { + clip = SP_ASPECT_MEET; + } else if (!strcmp (e, "slice")) { + clip = SP_ASPECT_SLICE; + } else { + return; + } + } + + this->aspect_set = true; + this->aspect_align = align; + this->aspect_clip = clip; + } +} + +// Apply scaling from viewbox +void SPViewBox::apply_viewbox(const Geom::Rect& in, double scale_none) { + + /* Determine actual viewbox in viewport coordinates */ + // scale_none is the scale that would apply if the viewbox and page size are same size + // it is passed here because it is a double-precision variable, while 'in' is originally float + double x = 0.0; + double y = 0.0; + double scale_x = in.width() / this->viewBox.width(); + double scale_y = in.height() / this->viewBox.height(); + double scale_uniform = 1.0; // used only if scaling is uniform + + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is already uniform, reduce numerical error + scale_uniform = (scale_x + scale_y)/2.0; + if (Geom::are_near(scale_uniform / scale_none, 1.0, Geom::EPSILON)) + scale_uniform = scale_none; // objects are same size, reduce numerical error + scale_x = scale_uniform; + scale_y = scale_uniform; + } else if (this->aspect_align != SP_ASPECT_NONE) { + // scaling is not uniform, but force it to be + scale_uniform = (this->aspect_clip == SP_ASPECT_MEET) ? MIN (scale_x, scale_y) : MAX (scale_x, scale_y); + scale_x = scale_uniform; + scale_y = scale_uniform; + double width = this->viewBox.width() * scale_uniform; + double height = this->viewBox.height() * scale_uniform; + + /* Now place viewbox to requested position */ + switch (this->aspect_align) { + case SP_ASPECT_XMIN_YMIN: + break; + case SP_ASPECT_XMID_YMIN: + x = 0.5 * (in.width() - width); + break; + case SP_ASPECT_XMAX_YMIN: + x = 1.0 * (in.width() - width); + break; + case SP_ASPECT_XMIN_YMID: + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMID_YMID: + x = 0.5 * (in.width() - width); + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMAX_YMID: + x = 1.0 * (in.width() - width); + y = 0.5 * (in.height() - height); + break; + case SP_ASPECT_XMIN_YMAX: + y = 1.0 * (in.height() - height); + break; + case SP_ASPECT_XMID_YMAX: + x = 0.5 * (in.width() - width); + y = 1.0 * (in.height() - height); + break; + case SP_ASPECT_XMAX_YMAX: + x = 1.0 * (in.width() - width); + y = 1.0 * (in.height() - height); + break; + default: + break; + } + } + + /* Viewbox transform from scale and position */ + Geom::Affine q; + q[0] = scale_x; + q[1] = 0.0; + q[2] = 0.0; + q[3] = scale_y; + q[4] = x - scale_x * this->viewBox.left(); + q[5] = y - scale_y * this->viewBox.top(); + + // std::cout << " q\n" << q << std::endl; + + /* Append viewbox transformation */ + this->c2p = q * this->c2p; +} + +SPItemCtx SPViewBox::get_rctx(const SPItemCtx* ictx, double scale_none) { + + /* Create copy of item context */ + SPItemCtx rctx = *ictx; + + /* Calculate child to parent transformation */ + /* Apply parent translation (set up as viewport) */ + this->c2p = Geom::Translate(rctx.viewport.min()); + + if (this->viewBox_set) { + // Adjusts c2p for viewbox + apply_viewbox( rctx.viewport, scale_none ); + } + + rctx.i2doc = this->c2p * rctx.i2doc; + + /* If viewBox is set initialize child viewport */ + /* Otherwise it is already correct */ + if (this->viewBox_set) { + rctx.viewport = this->viewBox; + rctx.i2vp = Geom::identity(); + } + + return rctx; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/object/viewbox.h b/src/object/viewbox.h new file mode 100644 index 0000000..42032d2 --- /dev/null +++ b/src/object/viewbox.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_VIEWBOX_H__ +#define __SP_VIEWBOX_H__ + +/* + * viewBox helper class, common code used by root, symbol, marker, pattern, image, view + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> (code extracted from sp-symbol.h) + * Tavmjong Bah + * Johan Engelen + * + * Copyright (C) 2013-2014 Tavmjong Bah, authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/rect.h> +#include <glib.h> + +class SPItemCtx; + +class SPViewBox { + +public: + SPViewBox(); + + /* viewBox; */ + bool viewBox_set; + Geom::Rect viewBox; // Could use optrect + + /* preserveAspectRatio */ + bool aspect_set; + unsigned int aspect_align; // enum + unsigned int aspect_clip; // enum + + /* Child to parent additional transform */ + Geom::Affine c2p; + + void set_viewBox(const gchar* value); + void set_preserveAspectRatio(const gchar* value); + + /* Adjusts c2p for viewbox */ + void apply_viewbox(const Geom::Rect& in, double scale_none = 1.0); + + SPItemCtx get_rctx( const SPItemCtx* ictx, double scale_none = 1.0); + +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/path-chemistry.cpp b/src/path-chemistry.cpp new file mode 100644 index 0000000..b14b25c --- /dev/null +++ b/src/path-chemistry.cpp @@ -0,0 +1,752 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Here are handlers for modifying selections, specific to paths + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2008 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <glibmm/i18n.h> + + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "text-editing.h" +#include "verbs.h" + +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "object/box3d.h" +#include "object/object-set.h" +#include "object/sp-flowtext.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "style.h" + +#include "svg/svg.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; +using Inkscape::ObjectSet; + + +inline bool less_than_items(SPItem const *first, SPItem const *second) +{ + return sp_repr_compare_position(first->getRepr(), + second->getRepr())<0; +} + +void +ObjectSet::combine(bool skip_undo) +{ + //Inkscape::Selection *selection = desktop->getSelection(); + //Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPDocument *doc = document(); + std::vector<SPItem*> items_copy(items().begin(), items().end()); + + if (items_copy.size() < 1) { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to combine.")); + return; + } + + if(desktop()){ + desktop()->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Combining paths...")); + // set "busy" cursor + desktop()->setWaitingCursor(); + } + + items_copy = sp_degroup_list (items_copy); // descend into any groups in selection + + std::vector<SPItem*> to_paths; + for (std::vector<SPItem*>::const_reverse_iterator i = items_copy.rbegin(); i != items_copy.rend(); ++i) { + if (!dynamic_cast<SPPath *>(*i) && !dynamic_cast<SPGroup *>(*i)) { + to_paths.push_back(*i); + } + } + std::vector<Inkscape::XML::Node*> converted; + bool did = sp_item_list_to_curves(to_paths, items_copy, converted); + for (auto i : converted) + items_copy.push_back((SPItem*)doc->getObjectByRepr(i)); + + items_copy = sp_degroup_list (items_copy); // converting to path may have added more groups, descend again + + sort(items_copy.begin(),items_copy.end(),less_than_items); + assert(!items_copy.empty()); // cannot be NULL because of list length check at top of function + + // remember the position, id, transform and style of the topmost path, they will be assigned to the combined one + gint position = 0; + char const *transform = nullptr; + char const *path_effect = nullptr; + + SPCurve* curve = nullptr; + SPItem *first = nullptr; + Inkscape::XML::Node *parent = nullptr; + + if (did) { + clear(); + } + + for (std::vector<SPItem*>::const_reverse_iterator i = items_copy.rbegin(); i != items_copy.rend(); ++i){ + + SPItem *item = *i; + SPPath *path = dynamic_cast<SPPath *>(item); + if (!path) { + continue; + } + + if (!did) { + clear(); + did = true; + } + + SPCurve *c = path->getCurveForEdit(); + if (first == nullptr) { // this is the topmost path + first = item; + parent = first->getRepr()->parent(); + position = first->getRepr()->position(); + transform = first->getRepr()->attribute("transform"); + // FIXME: merge styles of combined objects instead of using the first one's style + path_effect = first->getRepr()->attribute("inkscape:path-effect"); + //c->transform(item->transform); + curve = c; + } else { + c->transform(item->getRelativeTransform(first)); + curve->append(c, false); + c->unref(); + + // reduce position only if the same parent + if (item->getRepr()->parent() == parent) { + position--; + } + // delete the object for real, so that its clones can take appropriate action + item->deleteObject(); + } + } + + + if (did) { + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, first->getRepr()); + + // delete the topmost. + first->deleteObject(false); + + // restore id, transform, path effect, and style + if (transform) { + repr->setAttribute("transform", transform); + } + + repr->setAttribute("inkscape:path-effect", path_effect); + + // set path data corresponding to new curve + gchar *dstring = sp_svg_write_path(curve->get_pathvector()); + curve->unref(); + if (path_effect) { + repr->setAttribute("inkscape:original-d", dstring); + } else { + repr->setAttribute("d", dstring); + } + g_free(dstring); + + // add the new group to the parent of the topmost + // move to the position of the topmost, reduced by the number of deleted items + parent->addChildAtPos(repr, position > 0 ? position : 0); + + if ( !skip_undo ) { + DocumentUndo::done(doc, SP_VERB_SELECTION_COMBINE, + _("Combine")); + } + set(repr); + + Inkscape::GC::release(repr); + + } else { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No path(s)</b> to combine in the selection.")); + } + + if(desktop()) + desktop()->clearWaitingCursor(); +} + +void +ObjectSet::breakApart(bool skip_undo) +{ + if (isEmpty()) { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>path(s)</b> to break apart.")); + return; + } + if(desktop()){ + desktop()->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Breaking apart paths...")); + // set "busy" cursor + desktop()->setWaitingCursor(); + // disable redrawing during the break-apart operation for remarkable speedup for large paths + desktop()->getCanvas()->_drawing_disabled = true; + } + + bool did = false; + + std::vector<SPItem*> itemlist(items().begin(), items().end()); + for (auto item : itemlist){ + + SPPath *path = dynamic_cast<SPPath *>(item); + if (!path) { + continue; + } + + SPCurve *curve = path->getCurveForEdit(); + if (curve == nullptr) { + continue; + } + did = true; + + Inkscape::XML::Node *parent = item->getRepr()->parent(); + gint pos = item->getRepr()->position(); + char const *id = item->getRepr()->attribute("id"); + + // XML Tree being used directly here while it shouldn't be... + gchar *style = g_strdup(item->getRepr()->attribute("style")); + // XML Tree being used directly here while it shouldn't be... + gchar *path_effect = g_strdup(item->getRepr()->attribute("inkscape:path-effect")); + Geom::Affine transform = path->transform; + // it's going to resurrect as one of the pieces, so we delete without advertisement + item->deleteObject(false); + + + std::list<SPCurve *> list = curve->split(); + + curve->unref(); + + std::vector<Inkscape::XML::Node*> reprs; + for (auto curve:list) { + + Inkscape::XML::Node *repr = parent->document()->createElement("svg:path"); + repr->setAttribute("style", style); + + repr->setAttribute("inkscape:path-effect", path_effect); + + gchar *str = sp_svg_write_path(curve->get_pathvector()); + if (path_effect) + repr->setAttribute("inkscape:original-d", str); + else + repr->setAttribute("d", str); + str = sp_svg_transform_write(transform); + repr->setAttribute("transform", str); + g_free(str); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + // if it's the first one, restore id + if (curve == *(list.begin())) + repr->setAttribute("id", id); + + reprs.push_back(repr); + + Inkscape::GC::release(repr); + } + setReprList(reprs); + + g_free(style); + g_free(path_effect); + } + + if (desktop()) { + desktop()->getCanvas()->_drawing_disabled = false; + desktop()->clearWaitingCursor(); + } + + if (did) { + if ( !skip_undo ) { + DocumentUndo::done(document(), SP_VERB_SELECTION_BREAK_APART, + _("Break apart")); + } + } else { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No path(s)</b> to break apart in the selection.")); + } +} + +void ObjectSet::toCurves(bool skip_undo) +{ + if (isEmpty()) { + if (desktop()) + desktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to convert to path.")); + return; + } + + bool did = false; + if (desktop()) { + desktop()->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Converting objects to paths...")); + // set "busy" cursor + desktop()->setWaitingCursor(); + } + unlinkRecursive(true); + std::vector<SPItem*> selected(items().begin(), items().end()); + std::vector<Inkscape::XML::Node*> to_select; + std::vector<SPItem*> items(selected); + + did = sp_item_list_to_curves(items, selected, to_select); + + if (did) { + setReprList(to_select); + addList(selected); + } + + if (desktop()) { + desktop()->clearWaitingCursor(); + } + if (did&& !skip_undo) { + DocumentUndo::done(document(), SP_VERB_OBJECT_TO_CURVE, + _("Object to path")); + } else { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No objects</b> to convert to path in the selection.")); + return; + } +} + +/** Converts the selected items to LPEItems if they are not already so; e.g. SPRects) */ +void ObjectSet::toLPEItems() +{ + + if (isEmpty()) { + return; + } + unlinkRecursive(true); + std::vector<SPItem*> selected(items().begin(), items().end()); + std::vector<Inkscape::XML::Node*> to_select; + clear(); + std::vector<SPItem*> items(selected); + + + sp_item_list_to_curves(items, selected, to_select, true); + + setReprList(to_select); + addList(selected); +} + +bool +sp_item_list_to_curves(const std::vector<SPItem*> &items, std::vector<SPItem*>& selected, std::vector<Inkscape::XML::Node*> &to_select, bool skip_all_lpeitems) +{ + bool did = false; + for (auto item : items){ + g_assert(item != nullptr); + SPDocument *document = item->document; + + SPGroup *group = dynamic_cast<SPGroup *>(item); + if ( skip_all_lpeitems && + dynamic_cast<SPLPEItem *>(item) && + !group ) // also convert objects in an SPGroup when skip_all_lpeitems is set. + { + continue; + } + + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + // convert 3D box to ordinary group of paths; replace the old element in 'selected' with the new group + Inkscape::XML::Node *repr = box3d_convert_to_group(box)->getRepr(); + + if (repr) { + to_select.insert(to_select.begin(),repr); + did = true; + selected.erase(remove(selected.begin(), selected.end(), item), selected.end()); + } + + continue; + } + // remember id + char const *id = item->getRepr()->attribute("id"); + + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + lpeitem->removeAllPathEffects(true); + SPObject *elemref = document->getObjectById(id); + if (elemref != item) { + selected.erase(remove(selected.begin(), selected.end(), item), selected.end()); + if (elemref) { + //If the LPE item is a shape is converted to a path so we need to reupdate the item + item = dynamic_cast<SPItem *>(elemref); + selected.push_back(item); + did = true; + } + } + } + + SPPath *path = dynamic_cast<SPPath *>(item); + if (path) { + // remove connector attributes + if (item->getAttribute("inkscape:connector-type") != nullptr) { + item->removeAttribute("inkscape:connection-start"); + item->removeAttribute("inkscape:connection-end"); + item->removeAttribute("inkscape:connector-type"); + item->removeAttribute("inkscape:connector-curvature"); + did = true; + } + continue; // already a path, and no path effect + } + + if (group) { + std::vector<SPItem*> item_list = sp_item_group_item_list(group); + + std::vector<Inkscape::XML::Node*> item_to_select; + std::vector<SPItem*> item_selected; + + if (sp_item_list_to_curves(item_list, item_selected, item_to_select)) + did = true; + + + continue; + } + + Inkscape::XML::Node *repr = sp_selected_item_to_curved_repr(item, 0); + if (!repr) + continue; + + did = true; + selected.erase(remove(selected.begin(), selected.end(), item), selected.end()); + + // remember the position of the item + gint pos = item->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + // remember class + char const *class_attr = item->getRepr()->attribute("class"); + + // It's going to resurrect, so we delete without notifying listeners. + item->deleteObject(false); + + // restore id + repr->setAttribute("id", id); + // restore class + repr->setAttribute("class", class_attr); + // add the new repr to the parent + parent->addChildAtPos(repr, pos); + + /* Buglet: We don't re-add the (new version of the) object to the selection of any other + * desktops where it was previously selected. */ + to_select.insert(to_select.begin(),repr); + Inkscape::GC::release(repr); + } + + return did; +} + +Inkscape::XML::Node * +sp_selected_item_to_curved_repr(SPItem *item, guint32 /*text_grouping_policy*/) +{ + if (!item) + return nullptr; + + Inkscape::XML::Document *xml_doc = item->getRepr()->document(); + + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + // Special treatment for text: convert each glyph to separate path, then group the paths + Inkscape::XML::Node *g_repr = xml_doc->createElement("svg:g"); + + // Save original text for accessibility. + Glib::ustring original_text = sp_te_get_string_multiline( item, + te_get_layout(item)->begin(), + te_get_layout(item)->end() ); + if( original_text.size() > 0 ) { + g_repr->setAttributeOrRemoveIfEmpty("aria-label", original_text ); + } + + g_repr->setAttribute("transform", item->getRepr()->attribute("transform")); + + Inkscape::copy_object_properties(g_repr, item->getRepr()); + + /* Whole text's style */ + Glib::ustring style_str = + item->style->write( SP_STYLE_FLAG_IFDIFF, SP_STYLE_SRC_UNSET, item->parent ? item->parent->style : nullptr); // TODO investigate possibility + g_repr->setAttributeOrRemoveIfEmpty("style", style_str); + + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) + break; + + /* This glyph's style */ + SPObject *pos_obj = nullptr; + te_get_layout(item)->getSourceOfCharacter(iter, &pos_obj); + if (!pos_obj) // no source for glyph, abort + break; + while (dynamic_cast<SPString const *>(pos_obj) && pos_obj->parent) { + pos_obj = pos_obj->parent; // SPStrings don't have style + } + Glib::ustring style_str = + pos_obj->style->write( SP_STYLE_FLAG_IFDIFF, SP_STYLE_SRC_UNSET, pos_obj->parent ? pos_obj->parent->style : nullptr); // TODO investigate possibility + + // get path from iter to iter_next: + SPCurve *curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { // error converting this glyph + continue; + } + if (curve->is_empty()) { // whitespace glyph? + curve->unref(); + continue; + } + + Inkscape::XML::Node *p_repr = xml_doc->createElement("svg:path"); + + gchar *def_str = sp_svg_write_path(curve->get_pathvector()); + p_repr->setAttribute("d", def_str); + g_free(def_str); + curve->unref(); + + p_repr->setAttributeOrRemoveIfEmpty("style", style_str); + + g_repr->appendChild(p_repr); + + Inkscape::GC::release(p_repr); + + if (iter == te_get_layout(item)->end()) + break; + + } while (true); + + return g_repr; + } + SPCurve *curve = nullptr; + { + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + curve = shape->getCurveForEdit(); + } + } + + if (!curve) + return nullptr; + + // Prevent empty paths from being added to the document + // otherwise we end up with zomby markup in the SVG file + if(curve->is_empty()) + { + curve->unref(); + return nullptr; + } + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, item->getRepr()); + + /* Transformation */ + repr->setAttribute("transform", item->getRepr()->attribute("transform")); + + /* Style */ + Glib::ustring style_str = + item->style->write( SP_STYLE_FLAG_IFDIFF, SP_STYLE_SRC_UNSET, item->parent ? item->parent->style : nullptr); // TODO investigate possibility + repr->setAttributeOrRemoveIfEmpty("style", style_str); + + /* Definition */ + gchar *def_str = sp_svg_write_path(curve->get_pathvector()); + repr->setAttribute("d", def_str); + g_free(def_str); + curve->unref(); + return repr; +} + + +void +ObjectSet::pathReverse() +{ + if (isEmpty()) { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>path(s)</b> to reverse.")); + return; + } + + + // set "busy" cursor + if(desktop()){ + desktop()->setWaitingCursor(); + desktop()->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Reversing paths...")); + } + + bool did = false; + + for (auto i = items().begin(); i != items().end(); ++i){ + + SPPath *path = dynamic_cast<SPPath *>(*i); + if (!path) { + continue; + } + + did = true; + + SPCurve *rcurve = path->getCurveForEdit(true)->create_reverse(); + + gchar *str = sp_svg_write_path(rcurve->get_pathvector()); + if ( path->hasPathEffectRecursive() ) { + path->setAttribute("inkscape:original-d", str); + } else { + path->setAttribute("d", str); + } + g_free(str); + + rcurve->unref(); + + // reverse nodetypes order (Bug #179866) + gchar *nodetypes = g_strdup(path->getRepr()->attribute("sodipodi:nodetypes")); + if ( nodetypes ) { + path->setAttribute("sodipodi:nodetypes", g_strreverse(nodetypes)); + g_free(nodetypes); + } + } + if(desktop()) + desktop()->clearWaitingCursor(); + + if (did) { + DocumentUndo::done(document(), SP_VERB_SELECTION_REVERSE, + _("Reverse path")); + } else { + if(desktop()) + desktop()->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No paths</b> to reverse in the selection.")); + } +} + + +/** + * Copy generic attributes, like those from the "Object Properties" dialog, + * but also style and transformation center. + * + * @param dest XML node to copy attributes to + * @param src XML node to copy attributes from + */ +static void ink_copy_generic_attributes( // + Inkscape::XML::Node *dest, // + Inkscape::XML::Node const *src) +{ + static char const *const keys[] = { + // core + "id", + + // clip & mask + "clip-path", + "mask", + + // style + "style", + + // inkscape + "inkscape:highlight-color", + "inkscape:label", + "inkscape:transform-center-x", + "inkscape:transform-center-y", + + // interactivity + "onclick", + "onmouseover", + "onmouseout", + "onmousedown", + "onmouseup", + "onmousemove", + "onfocusin", + "onfocusout", + "onload", + }; + + for (auto *key : keys) { + auto *value = src->attribute(key); + if (value) { + dest->setAttribute(key, value); + } + } +} + + +/** + * Copy generic child elements, like those from the "Object Properties" dialog + * (title and description) but also XML comments. + * + * Does not check if children of the same type already exist in dest. + * + * @param dest XML node to copy children to + * @param src XML node to copy children from + */ +static void ink_copy_generic_children( // + Inkscape::XML::Node *dest, // + Inkscape::XML::Node const *src) +{ + static std::set<std::string> const names{ + // descriptive elements + "svg:title", + "svg:desc", + }; + + for (const auto *child = src->firstChild(); child != nullptr; child = child->next()) { + // check if this child should be copied + if (!(child->type() == Inkscape::XML::COMMENT_NODE || // + (child->name() && names.count(child->name())))) { + continue; + } + + auto dchild = child->duplicate(dest->document()); + dest->appendChild(dchild); + dchild->release(); + } +} + + +/** + * Copy generic object properties, like: + * - id + * - label + * - title + * - description + * - style + * - clip + * - mask + * - transformation center + * - highlight color + * - interactivity (event attributes) + * + * @param dest XML node to copy to + * @param src XML node to copy from + */ +void Inkscape::copy_object_properties( // + Inkscape::XML::Node *dest, // + Inkscape::XML::Node const *src) +{ + ink_copy_generic_attributes(dest, src); + ink_copy_generic_children(dest, src); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/path-chemistry.h b/src/path-chemistry.h new file mode 100644 index 0000000..e5e711a --- /dev/null +++ b/src/path-chemistry.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PATH_CHEMISTRY_H +#define SEEN_PATH_CHEMISTRY_H + +/* + * Here are handlers for modifying selections, specific to paths + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +class SPDesktop; +class SPItem; + +namespace Inkscape { +class Selection; +class ObjectSet; +namespace XML { +class Node; +} // namespace XML + +void copy_object_properties(XML::Node *dest, XML::Node const *src); +} // namespace Inkscape + +typedef unsigned int guint32; + +//void sp_selected_path_combine (SPDesktop *desktop, bool skip_undo = false); +//void sp_selected_path_break_apart (SPDesktop *desktop, bool skip_undo = false); + //interactive=true only has an effect if desktop != NULL, i.e. if a GUI is available +//void sp_selected_path_to_curves (Inkscape::Selection *selection, SPDesktop *desktop, bool interactive = true); +//void sp_selected_to_lpeitems(ObjectSet *selection); +Inkscape::XML::Node *sp_selected_item_to_curved_repr(SPItem *item, guint32 text_grouping_policy); +//void sp_selected_path_reverse (SPDesktop *desktop); +bool sp_item_list_to_curves(const std::vector<SPItem*> &items, std::vector<SPItem*> &selected, std::vector<Inkscape::XML::Node*> &to_select, bool skip_all_lpeitems = false); + +#endif // SEEN_PATH_CHEMISTRY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/path-prefix.cpp b/src/path-prefix.cpp new file mode 100644 index 0000000..29bffbd --- /dev/null +++ b/src/path-prefix.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * path-prefix.cpp - Inkscape specific prefix handling *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef _WIN32 +#include <windows.h> // for GetModuleFileNameW +#endif + +#ifdef __APPLE__ +#include <mach-o/dyld.h> // for _NSGetExecutablePath +#endif + +#include <glib.h> + +#include "path-prefix.h" + +/** + * Determine the location of the Inkscape data directory (typically the share/ folder + * from where Inkscape should be loading resources) and append a relative path + * + * - by default use the compile time value of INKSCAPE_DATADIR + * - on Windows inkscape_datadir will be relative to the called executable by default + * (typically inkscape/share but also handles the case where the executable is in a /bin subfolder) + * - if the environment variable INKSCAPE_DATADIR is set it will override all of the above + */ +char *append_inkscape_datadir(const char *relative_path) +{ + static gchar const *inkscape_datadir; + if (!inkscape_datadir) { + gchar *datadir; + gchar const *datadir_env = g_getenv("INKSCAPE_DATADIR"); + if (datadir_env) { + datadir = g_strdup(datadir_env); + } else { +#ifdef _WIN32 + gchar *module_path = g_win32_get_package_installation_directory_of_module(NULL); + datadir = g_build_filename(module_path, "share", NULL); + g_free(module_path); +#elif defined(__APPLE__) + gchar *program_dir = get_program_dir(); + if (g_str_has_suffix(program_dir, "Contents/MacOS")) { + datadir = g_build_filename(program_dir, "../Resources/share", nullptr); + } else { + datadir = g_strdup(INKSCAPE_DATADIR); + } + g_free(program_dir); +#else + datadir = g_strdup(INKSCAPE_DATADIR); +#endif + } + +#if GLIB_CHECK_VERSION(2,58,0) + inkscape_datadir = g_canonicalize_filename(datadir, NULL); + g_free(datadir); +#else + inkscape_datadir = datadir; +#endif + } + + if (!relative_path) { + relative_path = ""; + } + +#if GLIB_CHECK_VERSION(2,58,0) + return g_canonicalize_filename(relative_path, inkscape_datadir); +#else + return g_build_filename(inkscape_datadir, relative_path, NULL); +#endif +} + +/** + * Gets the the currently running program's executable name (including full path) + * + * @return executable name (including full path) encoded as UTF-8 + * or NULL if it can't be determined + */ +gchar *get_program_name() +{ + static gchar *program_name = NULL; + + if (program_name == NULL) { + // There is no portable way to get an executable's name including path, so we need to do it manually. + // TODO: Re-evaluate boost::dll::program_location() once we require Boost >= 1.61 + // + // The following platform-specific code is partially based on GdxPixbuf's get_toplevel() + // See also https://stackoverflow.com/a/1024937 +#if defined(_WIN32) + wchar_t module_file_name[MAX_PATH]; + if (GetModuleFileNameW(NULL, module_file_name, MAX_PATH)) { + program_name = g_utf16_to_utf8((gunichar2 *)module_file_name, -1, NULL, NULL, NULL); + } else { + g_warning("get_program_name() - GetModuleFileNameW failed"); + } +#elif defined(__APPLE__) + char pathbuf[PATH_MAX + 1]; + uint32_t bufsize = sizeof(pathbuf); + if (_NSGetExecutablePath(pathbuf, &bufsize) == 0) { + program_name = realpath(pathbuf, nullptr); + } else { + g_warning("get_program_name() - _NSGetExecutablePath failed"); + } +#elif defined(__linux__) + program_name = g_file_read_link("/proc/self/exe", NULL); + if (!program_name) { + g_warning("get_program_name() - g_file_read_link failed"); + } +#else +#warning get_program_name() - no known way to obtain executable name on this platform + g_info("get_program_name() - no known way to obtain executable name on this platform"); +#endif + } + + return program_name; +} + +/** + * Gets the the full path to the directory containing the currently running program's executable + * + * @return full path to directory encoded as UTF-8 + * or NULL if it can't be determined + */ +gchar *get_program_dir() +{ + return g_path_get_dirname(get_program_name()); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/path-prefix.h b/src/path-prefix.h new file mode 100644 index 0000000..dff011d --- /dev/null +++ b/src/path-prefix.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Separate the inkscape paths from the prefix code, as that is kind of + * a separate package (binreloc) + * http://autopackage.org/downloads.html + * + * Since the directories set up by autoconf end up in config.h, we can't + * _change_ them, since config.h isn't protected by a set of + * one-time-include directives and is repeatedly re-included by some + * chains of .h files. As a result, nothing should refer to those + * define'd directories, and instead should use only the paths defined here. + * + */ +#ifndef SEEN_PATH_PREFIX_H +#define SEEN_PATH_PREFIX_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "prefix.h" + +#ifdef ENABLE_BINRELOC // TODO: Should we drop binreloc in favor of OS-specific relocation code + // in append_inkscape_datadir() like we already do for win32? +/* The way that we're building now is with a shared library between Inkscape + and Inkview, and the code will find the path to the library then. But we + don't really want that. This prefix then pulls things out of the lib directory + and back into the root install dir. */ +# define INKSCAPE_LIBPREFIX "/../.." +# define INKSCAPE_DATADIR_REAL BR_DATADIR( INKSCAPE_LIBPREFIX "/share") +# define INKSCAPE_SYSTEMDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape") +# define INKSCAPE_ATTRRELDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/attributes" ) +# define INKSCAPE_DOCDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/doc" ) +# define INKSCAPE_EXAMPLESDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/examples" ) +# define INKSCAPE_EXTENSIONDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/extensions" ) +# define INKSCAPE_FILTERDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/filters" ) +# define INKSCAPE_FONTSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/fonts" ) +# define INKSCAPE_KEYSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/keys" ) +# define INKSCAPE_ICONSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/icons" ) +# define INKSCAPE_PIXMAPSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/pixmaps" ) +# define INKSCAPE_MARKERSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/markers" ) +# define INKSCAPE_PAINTDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/paint" ) +# define INKSCAPE_PALETTESDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/palettes" ) +# define INKSCAPE_SCREENSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/screens" ) +# define INKSCAPE_SYMBOLSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/symbols" ) +# define INKSCAPE_THEMEDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/themes" ) +# define INKSCAPE_TUTORIALSDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/tutorials" ) +# define INKSCAPE_TEMPLATESDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/templates" ) +# define INKSCAPE_UIDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/inkscape/ui" ) +//CREATE V0.1 support +# define CREATE_PAINTDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/create/paint" ) +# define CREATE_PALETTESDIR BR_DATADIR( INKSCAPE_LIBPREFIX "/share/create/swatches" ) +#elif defined ENABLE_OSX_APP_LOCATIONS // TODO: Is ENABLE_OSX_APP_LOCATIONS still in use? +# define INKSCAPE_DATADIR_REAL "Contents/Resources/share" +# define INKSCAPE_SYSTEMDIR "Contents/Resources/share/inkscape" +# define INKSCAPE_ATTRRELDIR "Contents/Resources/share/inkscape/attributes" +# define INKSCAPE_DOCDIR "Contents/Resources/share/inkscape/doc" +# define INKSCAPE_EXAMPLESDIR "Contents/Resources/share/inkscape/examples" +# define INKSCAPE_EXTENSIONDIR "Contents/Resources/share/inkscape/extensions" +# define INKSCAPE_FILTERDIR "Contents/Resources/share/inkscape/filters" +# define INKSCAPE_FONTSDIR "Contents/Resources/share/inkscape/fonts" +# define INKSCAPE_KEYSDIR "Contents/Resources/share/inkscape/keys" +# define INKSCAPE_ICONSDIR "Contents/Resources/share/inkscape/icons" +# define INKSCAPE_PIXMAPSDIR "Contents/Resources/share/inkscape/pixmaps" +# define INKSCAPE_MARKERSDIR "Contents/Resources/share/inkscape/markers" +# define INKSCAPE_PAINTDIR "Contents/Resources/share/inkscape/paint" +# define INKSCAPE_PALETTESDIR "Contents/Resources/share/inkscape/palettes" +# define INKSCAPE_SCREENSDIR "Contents/Resources/share/inkscape/screens" +# define INKSCAPE_SYMBOLSDIR "Contents/Resources/share/inkscape/symbols" +# define INKSCAPE_THEMEDIR "Contents/Resources/share/inkscape/themes" +# define INKSCAPE_TUTORIALSDIR "Contents/Resources/share/inkscape/tutorials" +# define INKSCAPE_TEMPLATESDIR "Contents/Resources/share/inkscape/templates" +# define INKSCAPE_UIDIR "Contents/Resources/share/inkscape/ui" +//CREATE V0.1 support +# define CREATE_PAINTDIR "/Library/Application Support/create/paint" +# define CREATE_PALETTESDIR "/Library/Application Support/create/swatches" +#else +# define INKSCAPE_DATADIR_REAL append_inkscape_datadir() +# define INKSCAPE_SYSTEMDIR append_inkscape_datadir("inkscape") +# define INKSCAPE_ATTRRELDIR append_inkscape_datadir("inkscape/attributes") +# define INKSCAPE_BINDDIR append_inkscape_datadir("inkscape/bind") +# define INKSCAPE_DOCDIR append_inkscape_datadir("inkscape/doc") +# define INKSCAPE_EXAMPLESDIR append_inkscape_datadir("inkscape/examples") +# define INKSCAPE_EXTENSIONDIR append_inkscape_datadir("inkscape/extensions") +# define INKSCAPE_FILTERDIR append_inkscape_datadir("inkscape/filters") +# define INKSCAPE_FONTSDIR append_inkscape_datadir("inkscape/fonts") +# define INKSCAPE_KEYSDIR append_inkscape_datadir("inkscape/keys") +# define INKSCAPE_ICONSDIR append_inkscape_datadir("inkscape/icons") +# define INKSCAPE_PIXMAPSDIR append_inkscape_datadir("inkscape/pixmaps") +# define INKSCAPE_MARKERSDIR append_inkscape_datadir("inkscape/markers") +# define INKSCAPE_PAINTDIR append_inkscape_datadir("inkscape/paint") +# define INKSCAPE_PALETTESDIR append_inkscape_datadir("inkscape/palettes") +# define INKSCAPE_SCREENSDIR append_inkscape_datadir("inkscape/screens") +# define INKSCAPE_SYMBOLSDIR append_inkscape_datadir("inkscape/symbols") +# define INKSCAPE_THEMEDIR append_inkscape_datadir("inkscape/themes") +# define INKSCAPE_TUTORIALSDIR append_inkscape_datadir("inkscape/tutorials") +# define INKSCAPE_TEMPLATESDIR append_inkscape_datadir("inkscape/templates") +# define INKSCAPE_UIDIR append_inkscape_datadir("inkscape/ui") +//CREATE V0.1 support +# define CREATE_PAINTDIR append_inkscape_datadir("create/paint") +# define CREATE_PALETTESDIR append_inkscape_datadir("create/swatches") +#endif + + +char *append_inkscape_datadir(const char *relative_path=nullptr); +char *get_program_name(); +char *get_program_dir(); + + +#endif /* _PATH_PREFIX_H_ */ diff --git a/src/perspective-line.cpp b/src/perspective-line.cpp new file mode 100644 index 0000000..e530a83 --- /dev/null +++ b/src/perspective-line.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Perspective line for 3D perspectives + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "perspective-line.h" + +namespace Box3D { + +PerspectiveLine::PerspectiveLine (Geom::Point const &pt, Proj::Axis const axis, Persp3D *persp) : + Line (pt, persp3d_get_VP(persp, axis).affine(), true) +{ + g_assert (persp != nullptr); + + if (!persp3d_get_VP(persp, axis).is_finite()) { + Proj::Pt2 vp(persp3d_get_VP(persp, axis)); + this->set_direction(Geom::Point(vp[Proj::X], vp[Proj::Y])); + } + this->vp_dir = axis; + this->persp = persp; +} + +} // namespace Box3D + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/perspective-line.h b/src/perspective-line.h new file mode 100644 index 0000000..c1dcb46 --- /dev/null +++ b/src/perspective-line.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Perspective line for 3D perspectives + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PERSPECTIVE_LINE_H +#define SEEN_PERSPECTIVE_LINE_H + +#include "line-geometry.h" + +class SPDesktop; + +namespace Box3D { + +class PerspectiveLine : public Box3D::Line { +public: + /** + * Create a perspective line starting at 'pt' and pointing in the direction of the + * vanishing point corresponding to 'axis'. If the VP has style VP_FINITE then the + * PL runs through it; otherwise it has the direction specified by the v_dir vector + * of the VP. + */ + PerspectiveLine (Geom::Point const &pt, Proj::Axis const axis, Persp3D *persp); + +private: + Proj::Axis vp_dir; // direction of the associated VP + Persp3D *persp; +}; + + +} // namespace Box3D + + +#endif /* !SEEN_PERSPECTIVE_LINE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/plugin.def b/src/plugin.def new file mode 100644 index 0000000..e5e5643 --- /dev/null +++ b/src/plugin.def @@ -0,0 +1,8 @@ +; SPDX-License-Identifier: GPL-2.0-or-later +;######################################################## +;## File: plugin.def +;## Purpose: Used by dllwrap to make an inkscape plugin +;######################################################## + +EXPORTS + inkscape_plugin_table diff --git a/src/preferences-skeleton.h b/src/preferences-skeleton.h new file mode 100644 index 0000000..14303b1 --- /dev/null +++ b/src/preferences-skeleton.h @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_PREFERENCES_SKELETON_H +#define SEEN_PREFERENCES_SKELETON_H + +#include "inkscape-version.h" + +// FIXME why is this here? +#ifdef N_ +#undef N_ +#endif +#define N_(x) x + +/* The root's "version" attribute describes the preferences file format version. + * It should only increase when a backwards-incompatible change is made, + * and special handling has to be added to the preferences class to update + * obsolete versions the user might have. */ +static char const preferences_skeleton[] = +R"=====( +<inkscape version="1" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"> + <group id="window"> + <group id="task" /> + <group id="menu" state="1"/> + <group id="commands" state="1"/> + <group id="snaptoolbox" state="1"/> + <group id="toppanel" state="1"/> + <group id="toolbox" state="1"/> + <group id="statusbar" state="1"/> + <group id="panels" state="1"/> + <group id="rulers" state="1"/> + <group id="scrollbars" state="1"/> + </group> + <group id="fullscreen"> + <group id="task" /> + <group id="menu" state="1"/> + <group id="commands" state="1"/> + <group id="snaptoolbox" state="1"/> + <group id="toppanel" state="1"/> + <group id="toolbox" state="1"/> + <group id="statusbar" state="1"/> + <group id="panels" state="1"/> + <group id="rulers" state="1"/> + <group id="scrollbars" state="1"/> + </group> + <group id="focus"> + <group id="task" /> + <group id="menu" state="0"/> + <group id="commands" state="0"/> + <group id="snaptoolbox" state="0"/> + <group id="toppanel" state="0"/> + <group id="toolbox" state="0"/> + <group id="statusbar" state="0"/> + <group id="panels" state="0"/> + <group id="rulers" state="0"/> + <group id="scrollbars" state="0"/> + </group> + + <group id="template"> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + objecttolerance="10.0" + gridtolerance="10.0" + guidetolerance="10.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:window-width="640" + inkscape:window-height="480" /> + </group> + + <group id="tools" bounding_box="0"> + + <group id="shapes" style="fill-rule:evenodd;" selcue="1" gradientdrag="1"> + <eventcontext id="rect" style="fill:blue;" usecurrent="1"/> + <eventcontext id="3dbox" style="stroke:none;stroke-linejoin:round;" usecurrent="1"> + <side id="XYfront" style="fill:#8686bf;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + <side id="XYrear" style="fill:#e9e9ff;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + <side id="XZtop" style="fill:#4d4d9f;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + <side id="XZbottom" style="fill:#afafde;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + <side id="YZright" style="fill:#353564;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + <side id="YZleft" style="fill:#d7d7ff;stroke:none;stroke-linejoin:round;" usecurrent="0"/> + </eventcontext> + <eventcontext id="arc" style="fill:red;" end="0" start="0" usecurrent="1"/> + <eventcontext id="star" magnitude="5" style="fill:yellow;" usecurrent="1"/> + <eventcontext id="spiral" style="fill:none;stroke:black" expansion="1" usecurrent="0"/> + </group> + + <group id="freehand" + style="fill:none;stroke:black;stroke-opacity:1;stroke-linejoin:miter;stroke-linecap:butt;"> + <eventcontext id="pencil" tolerance="4.0" selcue="1" style="stroke-width:1px;" usecurrent="0" average_all_sketches="1"/> + <eventcontext id="pen" mode="drag" selcue="1" style="stroke-width:1px;" usecurrent="0"/> + </group> + + <eventcontext id="calligraphic" style="fill:black;fill-opacity:1;fill-rule:nonzero;stroke:none;" + mass="2" angle="30" width="15" thinning="10" flatness="90" cap_rounding="0.0" usecurrent="1" + tracebackground="0" usepressure="1" usetilt="0" keep_selected="1"> + + <group id="preset"> + <group id="cp0" name="Dip pen" mass="2" wiggle="0.0" angle="30.0" thinning="10" tremor="0.0" flatness="90" cap_rounding="0.0" tracebackground="0" usepressure="1" usetilt="1" /> + <group id="cp1" name="Marker" mass="2" wiggle="0.0" angle="90.0" thinning="0.0" tremor="0.0" flatness="0.0" cap_rounding="1.0" tracebackground="0" usepressure="0" usetilt="0" /> + <group id="cp2" name="Brush" mass="2" wiggle="25" angle="45.0" thinning="-40" tremor="0.0" flatness="16" cap_rounding=".1" tracebackground="0" usepressure="1" usetilt="1" /> + <group id="cp3" name="Wiggly" usetilt="1" tracebackground="0" usepressure="1" cap_rounding="0.1" flatness="16" tremor="18" thinning="-30" angle="30" wiggle="50" mass="0" /> + <group id="cp4" name="Splotch" width="100" usetilt="1" tracebackground="0" usepressure="0" cap_rounding="1" flatness="0" tremor="10" thinning="30" angle="30" wiggle="0" mass="0" /> + <group id="cp5" name="Tracing" width="50" mass="0" wiggle="0.0" angle="0.0" thinning="0.0" tremor="0.0" flatness="0" cap_rounding="0.0" tracebackground="1" usepressure="1" usetilt="1"/> + </group> + </eventcontext> + + <eventcontext id="eraser" mode="1" style="fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;" + mass="0.02" drag="1" angle="30" width="10" thinning="0.1" flatness="0.0" cap_rounding="1.4" usecurrent="0" + tracebackground="0" usepressure="1" usetilt="0" selcue="1"> + </eventcontext> + + <eventcontext id="lpetool" mode="drag" style="fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;"> + </eventcontext> + + <eventcontext id="text" usecurrent="0" gradientdrag="1" + font_sample="AaBbCcIiPpQq12369$€¢?.;/()" + show_sample_in_list="1" use_svg2="1" + style="fill:black;fill-opacity:1;line-height:1.25;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;" selcue="1"/> + + <eventcontext id="nodes" selcue="1" gradientdrag="1" + pathflash_enabled="1" pathflash_unselected="0" pathflash_timeout="500" show_handles="1" show_outline="0" + sculpting_profile="1" single_node_transform_handles="0" show_transform_handles="0" live_outline="1" live_objects="1" show_helperpath="0" x="0" y="0" edit_clipping_paths="0" edit_masks="0" /> + <eventcontext id="tweak" selcue="0" gradientdrag="0" show_handles="0" width="0.2" force="0.2" fidelity="0.5" usepressure="1" style="fill:red;stroke:none;" usecurrent="0"/> + <eventcontext id="spray" selcue="1" gradientdrag="0" usepressure="1" width="15" population="70" mode="1" rotation_variation="0" scale_variation="0" standard_deviation="70" mean="0"/> + <eventcontext id="gradient" selcue="1"/> + <eventcontext id="mesh" selcue="1"/> + <eventcontext id="zoom" selcue="1" gradientdrag="0"/> + <eventcontext id="dropper" selcue="1" gradientdrag="1" pick="1" setalpha="1"/> + <eventcontext id="select" selcue="1" gradientdrag="0"/> + <eventcontext id="connector" style="fill:none;fill-rule:evenodd;stroke:black;stroke-opacity:1;stroke-linejoin:miter;stroke-width:1px;stroke-linecap:butt;" selcue="1"/> + <eventcontext id="paintbucket" style="fill:#a0a0a0;stroke:none;" usecurrent="1"/> + </group> + + <group id="palette"> + <group id="dashes"> + <dash id="solid" style="stroke-dasharray:none"/> + <dash id="dash-1-1" style="stroke-dasharray:1,1"/> + <dash id="dash-1-2" style="stroke-dasharray:1,2"/> + <dash id="dash-1-3" style="stroke-dasharray:1,3"/> + <dash id="dash-1-4" style="stroke-dasharray:1,4"/> + <dash id="dash-1-6" style="stroke-dasharray:1,6"/> + <dash id="dash-1-8" style="stroke-dasharray:1,8"/> + <dash id="dash-1-12" style="stroke-dasharray:1,12"/> + <dash id="dash-1-24" style="stroke-dasharray:1,24"/> + <dash id="dash-1-48" style="stroke-dasharray:1,48"/> + <dash id="dash-empty" style="stroke-dasharray:0 11"/> + <dash id="dash-2-1" style="stroke-dasharray:2,1"/> + <dash id="dash-3-1" style="stroke-dasharray:3,1"/> + <dash id="dash-4-1" style="stroke-dasharray:4,1"/> + <dash id="dash-6-1" style="stroke-dasharray:6,1"/> + <dash id="dash-8-1" style="stroke-dasharray:8,1"/> + <dash id="dash-12-1" style="stroke-dasharray:12,1"/> + <dash id="dash-24-1" style="stroke-dasharray:24,1"/> + <dash id="dash-2-2" style="stroke-dasharray:2,2"/> + <dash id="dash-3-3" style="stroke-dasharray:3,3"/> + <dash id="dash-4-4" style="stroke-dasharray:4,4"/> + <dash id="dash-6-6" style="stroke-dasharray:6,6"/> + <dash id="dash-8-8" style="stroke-dasharray:8,8"/> + <dash id="dash-12-12" style="stroke-dasharray:12,12"/> + <dash id="dash-24-24" style="stroke-dasharray:24,24"/> + <dash id="dash-2-4" style="stroke-dasharray:2,4"/> + <dash id="dash-4-2" style="stroke-dasharray:4,2"/> + <dash id="dash-2-6" style="stroke-dasharray:2,6"/> + <dash id="dash-6-2" style="stroke-dasharray:6,2"/> + <dash id="dash-4-8" style="stroke-dasharray:4,8"/> + <dash id="dash-8-4" style="stroke-dasharray:8,4"/> + <dash id="dash-2-1-012-1" style="stroke-dasharray:2,1,0.5,1"/> + <dash id="dash-4-2-1-2" style="stroke-dasharray:4,2,1,2"/> + <dash id="dash-8-2-1-2" style="stroke-dasharray:8,2,1,2"/> + <dash id="dash-012-012" style="stroke-dasharray:0.5,0.5"/> + <dash id="dash-014-014" style="stroke-dasharray:0.25,0.25"/> + <dash id="dash-0110-0110" style="stroke-dasharray:0.1,0.1"/> + </group> + </group> + + <group id="colorselector" page="0"/> + + <group id="embedded"> + <group id="swatches" + panel_size="1" + panel_mode="1" + panel_ratio="100" + panel_wrap="0" + palette="Inkscape default" /> + </group> + + <group id="dialogs"> + <group id="toolbox"/> + <group id="fillstroke"/> + <group id="filtereffects"/> + <group id="textandfont"/> + <group id="transformation" applyseparately="0"/> + <group id="align"/> + <group id="xml"/> + <group id="find"/> + <group id="spellcheck" w="200" h="250" ignorenumbers="1"/> + <group id="documentoptions" state="1"/> + <group id="preferences" state="1"/> + <group id="gradienteditor"/> + <group id="object"/> + <group id="export" default="" append_extension="1" path=""> + <group id="exportarea"/> + <group id="defaultxdpi"/> + </group> + <group id="save_as" default="" append_extension="1" enable_preview="1" path="" use_current_dir="1"/> + <group id="save_copy" default="" append_extension="1" enable_preview="1" path=""/> + <group id="open" enable_preview="1" path=""/> + <group id="import" enable_preview="1" path="" ask="1" ask_svg="1" link="link" scale="optimizeSpeed"/> + <group id="debug" redirect="0"/> + <group id="clonetiler" /> + <group id="gridtiler" /> + <group id="extension-error" show-on-startup="0"/> + <group id="memory" /> + <group id="messages" /> + <group id="swatches" /> + <group id="iconpreview" /> + <group id="aboutextensions" /> + <group id="treeeditor" /> + <group id="layers" maxDepth="20"/> + <group id="extensioneditor" /> + <group id="trace" state="1" /> + <group id="script" /> + <group id="input" /> + <group id="colorpickerwindow" /> + <group id="undo-history" /> + </group> + <group id="printing"> + <settings id="ps"/> + <group id="debug" add-label-comments="0"/> + </group> + + <group id="options"> + <group id="renderingcache" size="512" /> + <group id="useoldpdfexporter" value="0" /> + <group id="highlightoriginal" value="1" /> + <group id="relinkclonesonduplicate" value="0" /> + <group id="mapalt" value="1" /> + <group id="trackalt" value="0" /> + <group id="switchonextinput" value="0" /> + <group id="useextinput" value="1" /> + <group id="nudgedistance" value="2px"/> + <group id="rotationsnapsperpi" value="12"/> + <group id="cursortolerance" value="8.0"/> + <group id="dragtolerance" value="4.0"/> + <group id="grabsize" value="3"/> + <group + id="displayprofile" + enable="0" + from_display="0" + intent="0" + uri="" /> + <group + id="softproof" + enable="0" + intent="0" + gamutcolor="#808080" + gamutwarn="0" + bpc="0" + preserveblack="0" + uri="" /> + <group id="savewindowgeometry" value="1"/> + <group id="defaultoffsetwidth" value="2px"/> + <group id="defaultscale" value="2px"/> + <group id="maxrecentdocuments" value="36"/> + <group id="zoomincrement" value="1.414213562"/> + <group id="zoomcorrection" value="1.0" unit="mm"/> + <group id="keyscroll" value="15"/> + <group id="wheelscroll" value="40"/> + <group id="spacebarpans" value="1"/> + <group id="wheelzooms" value="0"/> + <group id="transientpolicy" value="1"/> + <group id="scrollingacceleration" value="0.4"/> + <group id="snapdelay" value="0"/> + <group id="snapweight" value="0.5"/> + <group id="snapclosestonly" value="0"/> + <group id="snapindicator" value="1"/> + <group id="autoscrollspeed" value="0.7"/> + <group id="autoscrolldistance" value="-10"/> + <group id="simplifythreshold" value="0.002"/> + <group id="bitmapeditor" value="gimp"/> + <group id="svgeditor" value="inkscape"/> + <group id="bitmapautoreload" value="1"/> + <group id="dialogtype" value="1"/> + <group id="dock" + cancenterdock="1" + dockbarstyle="2" + switcherstyle="2"/> + <group id="dialogsskiptaskbar" value="1"/> + <group id="arenatilescachesize" value="8192"/> + <group id="preservetransform" value="0"/> + <group id="clonecompensation" value="1"/> + <group id="cloneorphans" value="0"/> + <group id="stickyzoom" value="0"/> + <group id="selcue" value="2"/> + <group id="transform" stroke="1" rectcorners="1" pattern="1" gradient="1" /> + <group id="dash" scale="1" /> + <group id="kbselection" inlayer="1" onlyvisible="1" onlysensitive="1" /> + <group id="selection" layerdeselect="1" /> + <group id="createbitmap"/> + <group id="compassangledisplay" value="0"/> + <group id="middlemousezoom" value="1"/> + <group id="maskobject" topmost="1" remove="1"/> + <group id="blurquality" value="0"/> + <group id="filterquality" value="1"/> + <group id="showfiltersinfobox" value="1" /> + <group id="startmode" outline="0"/> + <group id="outlinemode" value="0"/> + + <group id="wireframecolors" + onlight="255" + ondark="4294967295" + images="4278190335" + clips="16711935" + masks="65535"/> + <group id="svgoutput" + disable_optimizations="0" + usenamedcolors="0" + numericprecision="8" + minimumexponent="-8" + inlineattrs="0" + indent="2" + pathstring_format="2" + forcerepeatcommands="0" + incorrect_attributes_warn="1" + incorrect_attributes_remove="0" + incorrect_style_properties_warn="1" + incorrect_style_properties_remove="0" + style_defaults_warn="1" + style_defaults_remove="0" + check_on_reading="0" + check_on_editing="0" + check_on_writing="0" + sort_attributes="0"/> + <group id="externalresources"> + <group id="xml" + allow_net_access="0"/> + </group> + <group id="forkgradientvectors" value="1"/> + <group id="iconrender" named_nodelay="0"/> + <group id="autosave" enable="1" interval="10" path="" max="10"/> + <group id="grids" + no_emphasize_when_zoomedout="0"> + <group id="xy" + units="px" + origin_x="0.0" + origin_y="0.0" + spacing_x="1.0" + spacing_y="1.0" + empspacing="5" + dotted="0"/> + <group id="axonom" + units="mm" + origin_x="0.0" + origin_y="0.0" + spacing_y="1.0" + angle_x="30.0" + angle_z="30.0" + empspacing="5"/> + </group> + <group id="workarounds" + colorsontop="0" + partialdynamic="0"/> + </group> + + <group id="extensions"> + </group> + + <group id="desktop" + style=""> + <group + width="640" + height="480" + x="0" + y="0" + fullscreen="0" + id="geometry" /> + <group + id="XYfront" /> + <group + id="XYrear" /> + <group + id="XZtop" /> + <group + id="XZbottom" /> + <group + id="YZleft" /> + <group + id="YZright" /> + </group> + + <group id="devices"> + </group> + + <group + id="toolbox" + icononly="1" + secondary="1" + small="1"> + <group + id="tools" + icononly="1" + small="0" /> + </group> + + <group + id="iconpreview" + autoRefresh="1" + pack="1" + selectionHold="1" + showFrames="1" + selectionOnly="0"> + <group + id="sizes"> + <group + id="default"> + <group + value="16" + show="1" + id="size16" /> + <group + value="22" + show="0" + id="size22" /> + <group + value="24" + show="1" + id="size24" /> + <group + value="32" + show="1" + id="size32" /> + <group + value="48" + show="1" + id="size48" /> + <group + value="50" + show="0" + id="size50" /> + <group + value="64" + show="0" + id="size64" /> + <group + value="72" + show="0" + id="size72" /> + <group + value="80" + show="0" + id="size80" /> + <group + value="96" + show="0" + id="size96" /> + <group + value="128" + show="1" + id="size128" /> + <group + value="256" + show="0" + id="size256" /> + </group> + </group> + </group> + <group id="debug"> + <group id="latency" skew="1"/> + </group> + <group id="ui" + language=""/> + +</inkscape> +)====="; + +#define PREFERENCES_SKELETON_SIZE (sizeof(preferences_skeleton) - 1) + +// Raw string literal cannot contain translatable strings. Fortunately, we only translate +// calligraphy presets. +// Note: actual translation is done in CalligraphyToolbar::build_presets_list(), we just +// mark the strings as translatable here (see GitLab issue 128): +Glib::ustring calligraphy_name_array[] = { + _("Dip pen"), + _("Marker"), + _("Brush"), + _("Wiggly"), + _("Splotchy"), + _("Tracing") +}; + +#endif /* !SEEN_PREFERENCES_SKELETON_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/preferences.cpp b/src/preferences.cpp new file mode 100644 index 0000000..aead814 --- /dev/null +++ b/src/preferences.cpp @@ -0,0 +1,991 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Singleton class to access the preferences file - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2008,2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <ctime> +#include <sstream> +#include <utility> +#include <glibmm/fileutils.h> +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glib/gstdio.h> +#include <gtk/gtk.h> +#include "preferences.h" +#include "preferences-skeleton.h" +#include "inkscape.h" +#include "xml/node-observer.h" +#include "xml/node-iterators.h" +#include "xml/attribute-record.h" +#include "util/units.h" +#include "attribute-rel-util.h" +#include "io/resource.h" + +#define PREFERENCES_FILE_NAME "preferences.xml" + +using Inkscape::Util::unit_table; + +namespace Inkscape { + +static Inkscape::XML::Document *loadImpl( std::string const& prefsFilename, Glib::ustring & errMsg ); +static void migrateDetails( Inkscape::XML::Document *from, Inkscape::XML::Document *to ); + +static Inkscape::XML::Document *migrateFromDoc = nullptr; + +// cachedRawValue prefixes for encoding nullptr +static Glib::ustring const RAWCACHE_CODE_NULL {"N"}; +static Glib::ustring const RAWCACHE_CODE_VALUE {"V"}; + +// private inner class definition + +/** + * XML - prefs observer bridge. + * + * This is an XML node observer that watches for changes in the XML document storing the preferences. + * It is used to implement preference observers. + */ +class Preferences::PrefNodeObserver : public XML::NodeObserver { +public: + PrefNodeObserver(Observer &o, Glib::ustring filter) : + _observer(o), + _filter(std::move(filter)) + {} + ~PrefNodeObserver() override = default; + void notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared, Util::ptr_shared) override; +private: + Observer &_observer; + Glib::ustring const _filter; +}; + +Preferences::Preferences() +{ + char *path = Inkscape::IO::Resource::profile_path(PREFERENCES_FILE_NAME); + _prefs_filename = path; + g_free(path); + + _loadDefaults(); + _load(); + + _initialized = true; +} + +Preferences::~Preferences() +{ + // unref XML document + Inkscape::GC::release(_prefs_doc); +} + +/** + * Load internal defaults. + * + * In the future this will try to load the system-wide file before falling + * back to the internal defaults. + */ +void Preferences::_loadDefaults() +{ + _prefs_doc = sp_repr_read_mem(preferences_skeleton, PREFERENCES_SKELETON_SIZE, nullptr); +#ifdef _WIN32 + setBool("/options/desktopintegration/value", 1); +#endif +#if defined(GDK_WINDOWING_QUARTZ) + // No maximise for Quartz, see lp:1302627 + setInt("/options/defaultwindowsize/value", -1); +#endif + +} + +/** + * Load the user's customized preferences. + * + * Tries to load the user's preferences.xml file. If there is none, creates it. + */ +void Preferences::_load() +{ + Glib::ustring const not_saved = _("Inkscape will run with default settings, " + "and new settings will not be saved. "); + + // NOTE: After we upgrade to Glib 2.16, use Glib::ustring::compose + + // 1. Does the file exist? + if (!g_file_test(_prefs_filename.c_str(), G_FILE_TEST_EXISTS)) { + char *_prefs_dir = Inkscape::IO::Resource::profile_path(nullptr); + // No - we need to create one. + // Does the profile directory exist? + if (!g_file_test(_prefs_dir, G_FILE_TEST_EXISTS)) { + // No - create the profile directory + if (g_mkdir_with_parents(_prefs_dir, 0755)) { + // the creation failed + //_reportError(Glib::ustring::compose(_("Cannot create profile directory %1."), + // Glib::filename_to_utf8(_prefs_dir)), not_saved); + gchar *msg = g_strdup_printf(_("Cannot create profile directory %s."), _prefs_dir); + _reportError(msg, not_saved); + g_free(msg); + return; + } + } else if (!g_file_test(_prefs_dir, G_FILE_TEST_IS_DIR)) { + // The profile dir is not actually a directory + //_reportError(Glib::ustring::compose(_("%1 is not a valid directory."), + // Glib::filename_to_utf8(_prefs_dir)), not_saved); + gchar *msg = g_strdup_printf(_("%s is not a valid directory."), _prefs_dir); + _reportError(msg, not_saved); + g_free(msg); + return; + } + // create some subdirectories for user stuff + char const *user_dirs[] = {"extensions", "fonts", "icons", "keys", "palettes", "templates", nullptr}; + for (int i=0; user_dirs[i]; ++i) { + // XXX Why are we doing this here? shouldn't this be an IO load item? + char *dir = Inkscape::IO::Resource::profile_path(user_dirs[i]); + if (!g_file_test(dir, G_FILE_TEST_EXISTS)) + g_mkdir(dir, 0755); + g_free(dir); + } + // The profile dir exists and is valid. + if (!g_file_set_contents(_prefs_filename.c_str(), preferences_skeleton, PREFERENCES_SKELETON_SIZE, nullptr)) { + // The write failed. + //_reportError(Glib::ustring::compose(_("Failed to create the preferences file %1."), + // Glib::filename_to_utf8(_prefs_filename)), not_saved); + gchar *msg = g_strdup_printf(_("Failed to create the preferences file %s."), + Glib::filename_to_utf8(_prefs_filename).c_str()); + _reportError(msg, not_saved); + g_free(msg); + return; + } + + if ( migrateFromDoc ) { + migrateDetails( migrateFromDoc, _prefs_doc ); + } + + // The prefs file was just created. + // We can return now and skip the rest of the load process. + _writable = true; + return; + } + + // Yes, the pref file exists. + Glib::ustring errMsg; + Inkscape::XML::Document *prefs_read = loadImpl( _prefs_filename, errMsg ); + + if ( prefs_read ) { + // Merge the loaded prefs with defaults. + _prefs_doc->root()->mergeFrom(prefs_read->root(), "id"); + Inkscape::GC::release(prefs_read); + _writable = true; + } else { + _reportError(errMsg, not_saved); + } +} + +//_reportError(msg, not_saved); +static Inkscape::XML::Document *loadImpl( std::string const& prefsFilename, Glib::ustring & errMsg ) +{ + // 2. Is it a regular file? + if (!g_file_test(prefsFilename.c_str(), G_FILE_TEST_IS_REGULAR)) { + gchar *msg = g_strdup_printf(_("The preferences file %s is not a regular file."), + Glib::filename_to_utf8(prefsFilename).c_str()); + errMsg = msg; + g_free(msg); + return nullptr; + } + + // 3. Is the file readable? + gchar *prefs_xml = nullptr; gsize len = 0; + if (!g_file_get_contents(prefsFilename.c_str(), &prefs_xml, &len, nullptr)) { + gchar *msg = g_strdup_printf(_("The preferences file %s could not be read."), + Glib::filename_to_utf8(prefsFilename).c_str()); + errMsg = msg; + g_free(msg); + return nullptr; + } + + // 4. Is it valid XML? + Inkscape::XML::Document *prefs_read = sp_repr_read_mem(prefs_xml, len, nullptr); + g_free(prefs_xml); + if (!prefs_read) { + gchar *msg = g_strdup_printf(_("The preferences file %s is not a valid XML document."), + Glib::filename_to_utf8(prefsFilename).c_str()); + errMsg = msg; + g_free(msg); + return nullptr; + } + + // 5. Basic sanity check: does the root element have a correct name? + if (strcmp(prefs_read->root()->name(), "inkscape")) { + gchar *msg = g_strdup_printf(_("The file %s is not a valid Inkscape preferences file."), + Glib::filename_to_utf8(prefsFilename).c_str()); + errMsg = msg; + g_free(msg); + Inkscape::GC::release(prefs_read); + return nullptr; + } + + return prefs_read; +} + +static void migrateDetails( Inkscape::XML::Document *from, Inkscape::XML::Document *to ) +{ + // TODO pull in additional prefs with more granularity + to->root()->mergeFrom(from->root(), "id"); +} + +/** + * Flush all pref changes to the XML file. + */ +void Preferences::save() +{ + // no-op if the prefs file is not writable + if (_writable) { + // sp_repr_save_file uses utf-8 instead of the glib filename encoding. + // I don't know why filenames are kept in utf-8 in Inkscape and then + // converted to filename encoding when necessary through special functions + // - wouldn't it be easier to keep things in the encoding they are supposed + // to be in? + + // No, it would not. There are many reasons, one key reason being that the + // rest of GTK+ is explicitly UTF-8. From an engineering standpoint, keeping + // the filesystem encoding would change things from a one-to-many problem to + // instead be a many-to-many problem. Also filesystem encoding can change + // from one run of the program to the next, so can not be stored. + // There are many other factors, so ask if you would like to learn them. - JAC + Glib::ustring utf8name = Glib::filename_to_utf8(_prefs_filename); + if (!utf8name.empty()) { + sp_repr_save_file(_prefs_doc, utf8name.c_str()); + } + } +} + +/** + * Deletes the preferences.xml file + */ +void Preferences::reset() +{ + time_t sptime = time (nullptr); + struct tm *sptm = localtime (&sptime); + gchar sptstr[256]; + strftime(sptstr, 256, "%Y_%m_%d_%H_%M_%S", sptm); + + char *new_name = g_strdup_printf("%s_%s.xml", _prefs_filename.c_str(), sptstr); + + + if (g_file_test(_prefs_filename.c_str(), G_FILE_TEST_EXISTS)) { + //int retcode = g_unlink (_prefs_filename.c_str()); + int retcode = g_rename (_prefs_filename.c_str(), new_name ); + if (retcode == 0) g_warning("%s %s.", _("Preferences file was backed up to"), new_name); + else g_warning("%s", _("There was an error trying to reset the preferences file.")); + } + + g_free(new_name); + _observer_map.clear(); + Inkscape::GC::release(_prefs_doc); + _prefs_doc = nullptr; + _loadDefaults(); + _load(); + save(); +} + +bool Preferences::getLastError( Glib::ustring& primary, Glib::ustring& secondary ) +{ + bool result = _hasError; + if ( _hasError ) { + primary = _lastErrPrimary; + secondary = _lastErrSecondary; + _hasError = false; + _lastErrPrimary.clear(); + _lastErrSecondary.clear(); + } else { + primary.clear(); + secondary.clear(); + } + return result; +} + +// Now for the meat. + +/** + * Get names of all entries in the specified path. + * + * @param path Preference path to query. + * @return A vector containing all entries in the given directory. + */ +std::vector<Preferences::Entry> Preferences::getAllEntries(Glib::ustring const &path) +{ + std::vector<Entry> temp; + Inkscape::XML::Node *node = _getNode(path, false); + if (node) { + // argh - purge this Util::List nonsense from XML classes fast + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> alist = node->attributeList(); + for (; alist; ++alist) { + temp.push_back( Entry(path + '/' + g_quark_to_string(alist->key), static_cast<void const*>(alist->value.pointer())) ); + } + } + return temp; +} + +/** + * Get the paths to all subdirectories of the specified path. + * + * @param path Preference path to query. + * @return A vector containing absolute paths to all subdirectories in the given path. + */ +std::vector<Glib::ustring> Preferences::getAllDirs(Glib::ustring const &path) +{ + std::vector<Glib::ustring> temp; + Inkscape::XML::Node *node = _getNode(path, false); + if (node) { + for (Inkscape::XML::NodeSiblingIterator i = node->firstChild(); i; ++i) { + if (i->attribute("id") == nullptr) { + continue; + } + temp.push_back(path + '/' + i->attribute("id")); + } + } + return temp; +} + +// getter methods + +Preferences::Entry const Preferences::getEntry(Glib::ustring const &pref_path) +{ + gchar const *v; + _getRawValue(pref_path, v); + return Entry(pref_path, v); +} + +// setter methods + +/** + * Set a boolean attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setBool(Glib::ustring const &pref_path, bool value) +{ + /// @todo Boolean values should be stored as "true" and "false", + /// but this is not possible due to an interaction with event contexts. + /// Investigate this in depth. + _setRawValue(pref_path, ( value ? "1" : "0" )); +} + +/** + * Set an point attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setPoint(Glib::ustring const &pref_path, Geom::Point value) +{ + _setRawValue(pref_path, Glib::ustring::compose("%1",value[Geom::X]) + "," + Glib::ustring::compose("%1",value[Geom::Y])); +} + +/** + * Set an integer attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setInt(Glib::ustring const &pref_path, int value) +{ + _setRawValue(pref_path, Glib::ustring::compose("%1",value)); +} + +/** + * Set an unsigned integer attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setUInt(Glib::ustring const &pref_path, unsigned int value) +{ + _setRawValue(pref_path, Glib::ustring::compose("%1",value)); +} + +/** + * Set a floating point attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setDouble(Glib::ustring const &pref_path, double value) +{ + _setRawValue(pref_path, Glib::ustring::compose("%1",value)); +} + +/** + * Set a floating point attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + * @param unit_abbr The string of the unit (abbreviated). + */ +void Preferences::setDoubleUnit(Glib::ustring const &pref_path, double value, Glib::ustring const &unit_abbr) +{ + Glib::ustring str = Glib::ustring::compose("%1%2",value,unit_abbr); + _setRawValue(pref_path, str); +} + +void Preferences::setColor(Glib::ustring const &pref_path, guint32 value) +{ + gchar buf[16]; + g_snprintf(buf, 16, "#%08x", value); + _setRawValue(pref_path, buf); +} + +/** + * Set a string attribute of a preference. + * + * @param pref_path Path of the preference to modify. + * @param value The new value of the pref attribute. + */ +void Preferences::setString(Glib::ustring const &pref_path, Glib::ustring const &value) +{ + _setRawValue(pref_path, value); +} + +void Preferences::setStyle(Glib::ustring const &pref_path, SPCSSAttr *style) +{ + Glib::ustring css_str; + sp_repr_css_write_string(style, css_str); + _setRawValue(pref_path, css_str); +} + +void Preferences::mergeStyle(Glib::ustring const &pref_path, SPCSSAttr *style) +{ + SPCSSAttr *current = getStyle(pref_path); + sp_repr_css_merge(current, style); + sp_attribute_purge_default_style(current, SP_ATTR_CLEAN_DEFAULT_REMOVE); + Glib::ustring css_str; + sp_repr_css_write_string(current, css_str); + _setRawValue(pref_path, css_str); + sp_repr_css_attr_unref(current); +} + +/** + * Remove an entry + * Make sure observers have been removed before calling + */ +void Preferences::remove(Glib::ustring const &pref_path) +{ + auto it = cachedRawValue.find(pref_path.c_str()); + if (it != cachedRawValue.end()) cachedRawValue.erase(it); + + Inkscape::XML::Node *node = _getNode(pref_path, false); + if (node && node->parent()) { + node->parent()->removeChild(node); + } else { //Handle to remove also attributes in path not only the container node + // verify path + g_assert( pref_path.at(0) == '/' ); + if (_prefs_doc == nullptr){ + return; + } + node = _prefs_doc->root(); + Inkscape::XML::Node *child = nullptr; + gchar **splits = g_strsplit(pref_path.c_str(), "/", 0); + if ( splits ) { + for (int part_i = 0; splits[part_i]; ++part_i) { + // skip empty path segments + if (!splits[part_i][0]) { + continue; + } + if (!node->firstChild()) { + node->removeAttribute(splits[part_i]); + g_strfreev(splits); + return; + } + for (child = node->firstChild(); child; child = child->next()) { + if (!strcmp(splits[part_i], child->attribute("id"))) { + break; + } + } + node = child; + } + } + g_strfreev(splits); + } +} + +/** + * Class that holds additional information for registered Observers. + */ +class Preferences::_ObserverData +{ +public: + _ObserverData(Inkscape::XML::Node *node, bool isAttr) : _node(node), _is_attr(isAttr) {} + + Inkscape::XML::Node *_node; ///< Node at which the wrapping PrefNodeObserver is registered + bool _is_attr; ///< Whether this Observer watches a single attribute +}; + +Preferences::Observer::Observer(Glib::ustring path) : + observed_path(std::move(path)), + _data(nullptr) +{ +} + +Preferences::Observer::~Observer() +{ + // on destruction remove observer to prevent invalid references + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->removeObserver(*this); +} + +void Preferences::PrefNodeObserver::notifyAttributeChanged(XML::Node &node, GQuark name, Util::ptr_shared, Util::ptr_shared new_value) +{ + // filter out attributes we don't watch + gchar const *attr_name = g_quark_to_string(name); + if ( _filter.empty() || (_filter == attr_name) ) { + _ObserverData *d = Preferences::_get_pref_observer_data(_observer); + Glib::ustring notify_path = _observer.observed_path; + + if (!d->_is_attr) { + std::vector<gchar const *> path_fragments; + notify_path.reserve(256); // this will make appending operations faster + + // walk the XML tree, saving each of the id attributes in a vector + // we terminate when we hit the observer's attachment node, because the path to this node + // is already stored in notify_path + for (XML::NodeParentIterator n = &node; static_cast<XML::Node*>(n) != d->_node; ++n) { + path_fragments.push_back(n->attribute("id")); + } + // assemble the elements into a path + for (std::vector<gchar const *>::reverse_iterator i = path_fragments.rbegin(); i != path_fragments.rend(); ++i) { + notify_path.push_back('/'); + notify_path.append(*i); + } + + // append attribute name + notify_path.push_back('/'); + notify_path.append(attr_name); + } + + Entry const val = Preferences::_create_pref_value(notify_path, static_cast<void const*>(new_value.pointer())); + _observer.notify(val); + } +} + +/** + * Find the XML node to observe. + */ +XML::Node *Preferences::_findObserverNode(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key, bool create) +{ + // first assume that the last path element is an entry. + _keySplit(pref_path, node_key, attr_key); + + // find the node corresponding to the "directory". + Inkscape::XML::Node *node = _getNode(node_key, create), *child; + if (!node) return node; + + for (child = node->firstChild(); child; child = child->next()) { + // If there is a node with id corresponding to the attr key, + // this means that the last part of the path is actually a key (folder). + // Change values accordingly. + if (attr_key == child->attribute("id")) { + node = child; + attr_key = ""; + node_key = pref_path; + break; + } + } + return node; +} + +void Preferences::addObserver(Observer &o) +{ + // prevent adding the same observer twice + if ( _observer_map.find(&o) == _observer_map.end() ) { + Glib::ustring node_key, attr_key; + Inkscape::XML::Node *node; + node = _findObserverNode(o.observed_path, node_key, attr_key, true); + if (node) { + // set additional data + o._data.reset(new _ObserverData(node, !attr_key.empty())); + + _observer_map[&o].reset(new PrefNodeObserver(o, attr_key)); + + // if we watch a single pref, we want to receive notifications only for a single node + if (o._data->_is_attr) { + node->addObserver( *(_observer_map[&o]) ); + } else { + node->addSubtreeObserver( *(_observer_map[&o]) ); + } + } + } +} + +void Preferences::removeObserver(Observer &o) +{ + // prevent removing an observer which was not added + auto it = _observer_map.find(&o); + if (it != _observer_map.end()) { + Inkscape::XML::Node *node = o._data->_node; + _ObserverData *priv_data = o._data.get(); + + if (priv_data->_is_attr) { + node->removeObserver(*it->second); + } else { + node->removeSubtreeObserver(*it->second); + } + + _observer_map.erase(it); + } +} + + +/** + * Get the XML node corresponding to the given pref key. + * + * @param pref_key Preference key (path) to get. + * @param create Whether to create the corresponding node if it doesn't exist. + * @param separator The character used to separate parts of the pref key. + * @return XML node corresponding to the specified key. + * + * Derived from former inkscape_get_repr(). Private because it assumes that the backend is + * a flat XML file, which may not be the case e.g. if we are using GConf (in future). + */ +Inkscape::XML::Node *Preferences::_getNode(Glib::ustring const &pref_key, bool create) +{ + // verify path + g_assert( pref_key.at(0) == '/' ); + // No longer necessary, can cause problems with input devices which have a dot in the name + // g_assert( pref_key.find('.') == Glib::ustring::npos ); + + if (_prefs_doc == nullptr){ + return nullptr; + } + Inkscape::XML::Node *node = _prefs_doc->root(); + Inkscape::XML::Node *child = nullptr; + gchar **splits = g_strsplit(pref_key.c_str(), "/", 0); + + if ( splits ) { + for (int part_i = 0; splits[part_i]; ++part_i) { + // skip empty path segments + if (!splits[part_i][0]) { + continue; + } + + for (child = node->firstChild(); child; child = child->next()) { + if (child->attribute("id") == nullptr) { + continue; + } + if (!strcmp(splits[part_i], child->attribute("id"))) { + break; + } + } + + // If the previous loop found a matching key, child now contains the node + // matching the processed key part. If no node was found then it is NULL. + if (!child) { + if (create) { + // create the rest of the key + while(splits[part_i]) { + child = node->document()->createElement("group"); + child->setAttribute("id", splits[part_i]); + node->appendChild(child); + + ++part_i; + node = child; + } + g_strfreev(splits); + splits = nullptr; + return node; + } else { + g_strfreev(splits); + splits = nullptr; + return nullptr; + } + } + + node = child; + } + g_strfreev(splits); + } + return node; +} + +void Preferences::_getRawValue(Glib::ustring const &path, gchar const *&result) +{ + // will return empty string if `path` was not in the cache yet + auto& cacheref = cachedRawValue[path.c_str()]; + + // check in cache first + if (_initialized && !cacheref.empty()) { + if (cacheref == RAWCACHE_CODE_NULL) { + result = nullptr; + } else { + result = cacheref.c_str() + RAWCACHE_CODE_VALUE.length(); + } + return; + } + + // create node and attribute keys + Glib::ustring node_key, attr_key; + _keySplit(path, node_key, attr_key); + + // retrieve the attribute + Inkscape::XML::Node *node = _getNode(node_key, false); + if ( node == nullptr ) { + result = nullptr; + } else { + gchar const *attr = node->attribute(attr_key.c_str()); + if ( attr == nullptr ) { + result = nullptr; + } else { + result = attr; + } + } + + if (_initialized && result) { + cacheref = RAWCACHE_CODE_VALUE; + cacheref += result; + } else { + cacheref = RAWCACHE_CODE_NULL; + } +} + +void Preferences::_setRawValue(Glib::ustring const &path, Glib::ustring const &value) +{ + // create node and attribute keys + Glib::ustring node_key, attr_key; + _keySplit(path, node_key, attr_key); + + // set the attribute + Inkscape::XML::Node *node = _getNode(node_key, true); + node->setAttributeOrRemoveIfEmpty(attr_key, value); + + if (_initialized) { + cachedRawValue[path.c_str()] = RAWCACHE_CODE_VALUE + value; + } +} + +// The _extract* methods are where the actual work is done - they define how preferences are stored +// in the XML file. + +bool Preferences::_extractBool(Entry const &v) +{ + if (v.cached_bool) return v.value_bool; + v.cached_bool = true; + gchar const *s = static_cast<gchar const *>(v._value); + if ( !s[0] || !strcmp(s, "0") || !strcmp(s, "false") ) { + return false; + } else { + v.value_bool = true; + return true; + } +} + +Geom::Point Preferences::_extractPoint(Entry const &v) +{ + if (v.cached_point) return v.value_point; + v.cached_point = true; + gchar const *s = static_cast<gchar const *>(v._value); + gchar ** strarray = g_strsplit(s, ",", 2); + double newx = atoi(strarray[0]); + double newy = atoi(strarray[1]); + g_strfreev (strarray); + return Geom::Point(newx, newy); +} + +int Preferences::_extractInt(Entry const &v) +{ + if (v.cached_int) return v.value_int; + v.cached_int = true; + gchar const *s = static_cast<gchar const *>(v._value); + if ( !strcmp(s, "true") ) { + v.value_int = 1; + return true; + } else if ( !strcmp(s, "false") ) { + v.value_int = 0; + return false; + } else { + int val = 0; + + // TODO: We happily save unsigned integers (notably RGBA values) as signed integers and overflow as needed. + // We should consider adding an unsigned integer type to preferences or use HTML colors where appropriate + // (the latter would breaks backwards compatibility, though) + errno = 0; + val = (int)strtol(s, nullptr, 0); + if (errno == ERANGE) { + errno = 0; + val = (int)strtoul(s, nullptr, 0); + if (errno == ERANGE) { + g_warning("Integer preference out of range: '%s' (raw value: %s)", v._pref_path.c_str(), s); + val = 0; + } + } + + v.value_int = val; + return v.value_int; + } +} + +unsigned int Preferences::_extractUInt(Entry const &v) +{ + if (v.cached_uint) return v.value_uint; + v.cached_uint = true; + gchar const *s = static_cast<gchar const *>(v._value); + + // Note: 'strtoul' can also read overflowed (i.e. negative) signed int values that we used to save before we + // had the unsigned type, so this is fully backwards compatible and can be replaced seamlessly + unsigned int val = 0; + errno = 0; + val = (unsigned int)strtoul(s, nullptr, 0); + if (errno == ERANGE) { + g_warning("Unsigned integer preference out of range: '%s' (raw value: %s)", v._pref_path.c_str(), s); + val = 0; + } + + v.value_uint = val; + return v.value_uint; +} + +double Preferences::_extractDouble(Entry const &v) +{ + if (v.cached_double) return v.value_double; + v.cached_double = true; + gchar const *s = static_cast<gchar const *>(v._value); + v.value_double = g_ascii_strtod(s, nullptr); + return v.value_double; +} + +double Preferences::_extractDouble(Entry const &v, Glib::ustring const &requested_unit) +{ + double val = _extractDouble(v); + Glib::ustring unit = _extractUnit(v); + + if (unit.length() == 0) { + // no unit specified, don't do conversion + return val; + } + return val * (unit_table.getUnit(unit)->factor / unit_table.getUnit(requested_unit)->factor); /// \todo rewrite using Quantity class, so the standard code handles unit conversion +} + +Glib::ustring Preferences::_extractString(Entry const &v) +{ + return Glib::ustring(static_cast<gchar const *>(v._value)); +} + +Glib::ustring Preferences::_extractUnit(Entry const &v) +{ + if (v.cached_unit) return v.value_unit; + v.cached_unit = true; + v.value_unit = ""; + gchar const *str = static_cast<gchar const *>(v._value); + gchar const *e; + g_ascii_strtod(str, (char **) &e); + if (e == str) { + return ""; + } + + if (e[0] == 0) { + /* Unitless */ + return ""; + } else { + v.value_unit = Glib::ustring(e); + return v.value_unit; + } +} + +guint32 Preferences::_extractColor(Entry const &v) +{ + if (v.cached_color) return v.value_color; + v.cached_color = true; + gchar const *s = static_cast<gchar const *>(v._value); + std::istringstream hr(s); + guint32 color; + if (s[0] == '#') { + hr.ignore(1); + hr >> std::hex >> color; + } else { + hr >> color; + } + v.value_color = color; + return color; +} + +SPCSSAttr *Preferences::_extractStyle(Entry const &v) +{ + if (v.cached_style) return v.value_style; + v.cached_style = true; + SPCSSAttr *style = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(style, static_cast<gchar const*>(v._value)); + v.value_style = style; + return style; +} + +SPCSSAttr *Preferences::_extractInheritedStyle(Entry const &v) +{ + // This is the dirtiest extraction method. Generally we ignore whatever was in v._value + // and just get the style using sp_repr_css_attr_inherited. To implement this in GConf, + // we'll have to walk up the tree and call sp_repr_css_attr_add_from_string + Glib::ustring node_key, attr_key; + _keySplit(v._pref_path, node_key, attr_key); + + Inkscape::XML::Node *node = _getNode(node_key, false); + return sp_repr_css_attr_inherited(node, attr_key.c_str()); +} + +// XML backend helper: Split the path into a node key and an attribute key. +void Preferences::_keySplit(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key) +{ + // everything after the last slash + attr_key = pref_path.substr(pref_path.rfind('/') + 1, Glib::ustring::npos); + // everything before the last slash + node_key = pref_path.substr(0, pref_path.rfind('/')); +} + +void Preferences::_reportError(Glib::ustring const &msg, Glib::ustring const &secondary) +{ + _hasError = true; + _lastErrPrimary = msg; + _lastErrSecondary = secondary; + if (_errorHandler) { + _errorHandler->handleError(msg, secondary); + } +} + +Preferences::Entry const Preferences::_create_pref_value(Glib::ustring const &path, void const *ptr) +{ + return Entry(path, ptr); +} + +void Preferences::setErrorHandler(ErrorReporter* handler) +{ + _errorHandler = handler; +} + +void Preferences::unload(bool save) +{ + if (_instance) + { + if (save) { + _instance->save(); + } + delete _instance; + _instance = nullptr; + } +} + +Preferences *Preferences::_instance = nullptr; + + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/preferences.h b/src/preferences.h new file mode 100644 index 0000000..50133f5 --- /dev/null +++ b/src/preferences.h @@ -0,0 +1,811 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Singleton class to access the preferences file in a convenient way. + */ +/* Authors: + * Krzysztof Kosi_ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2008,2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PREFSTORE_H +#define INKSCAPE_PREFSTORE_H + +#include <climits> +#include <cfloat> +#include <glibmm/ustring.h> +#include <map> +#include <memory> +#include <unordered_map> +#include <utility> +#include <vector> +#include <2geom/point.h> + +#include "xml/repr.h" + +class SPCSSAttr; +typedef unsigned int guint32; + +namespace Inkscape { + +class ErrorReporter { +public: + virtual ~ErrorReporter() = default; + virtual void handleError(Glib::ustring const& primary, Glib::ustring const& secondary ) const = 0; +}; + +/** + * Preference storage class. + * + * This is a singleton that allows one to access the user preferences stored in + * the preferences.xml file. The preferences are stored in a file system-like + * hierarchy. They are generally typeless - it's up to the programmer to ensure + * that a given preference is always accessed as the correct type. The backend + * is not guaranteed to be tolerant to type mismatches. + * + * Preferences are identified by paths similar to file system paths. Components + * of the path are separated by a slash (/). As an additional requirement, + * the path must start with a slash, and not contain a trailing slash. + * An example of a correct path would be "/options/some_group/some_option". + * + * All preferences are loaded when the first singleton pointer is requested. + * To save the preferences, the method save() or the static function unload() + * can be used. + * + * In future, this will be a virtual base from which specific backends + * derive (e.g. GConf, flat XML file...) + */ +class Preferences { + class _ObserverData; + +public: + // ############################# + // ## inner class definitions ## + // ############################# + + class Entry; + class Observer; + + /** + * Base class for preference observers. + * + * If you want to watch for changes in the preferences, you'll have to + * derive a class from this one and override the notify() method. + */ + class Observer { + friend class Preferences; + + public: + + /** + * Constructor. + * + * Since each Observer is assigned to a single path, the base + * constructor takes this path as an argument. This prevents one from + * adding a single observer to multiple paths, but this is intentional + * to simplify the implementation of observers and notifications. + * + * After you add the object with Preferences::addObserver(), you will + * receive notifications for everything below the attachment point. + * You can also specify a single preference as the watch point. + * For example, watching the directory "/foo" will give you notifications + * about "/foo/some_pref" as well as "/foo/some_dir/other_pref". + * Watching the preference "/options/some_group/some_option" will only + * generate notifications when this single preference changes. + * + * @param path Preference path the observer should watch. + */ + Observer(Glib::ustring path); + virtual ~Observer(); + + /** + * Notification about a preference change. + * + * @param new_val Entry object containing information about + * the modified preference. + */ + virtual void notify(Preferences::Entry const &new_val) = 0; + + Glib::ustring const observed_path; ///< Path which the observer watches + private: + std::unique_ptr<_ObserverData> _data; ///< additional data used by the implementation while the observer is active + }; + + + /** + * Data type representing a typeless value of a preference. + * + * This is passed to the observer in the notify() method. + * To retrieve useful data from it, use its member functions. Setting + * any preference using the Preferences class invalidates this object, + * so use its get methods before doing so. + */ + class Entry { + friend class Preferences; // Preferences class has to access _value + public: + ~Entry() = default; + Entry() + : _pref_path("") + , _value(nullptr) + , cached_bool(false) + , cached_point(false) + , cached_int(false) + , cached_uint(false) + , cached_double(false) + , cached_unit(false) + , cached_color(false) + , cached_style(false) {} // needed to enable use in maps + Entry(Entry const &other) = default; + + /** + * Check whether the received entry is valid. + * + * @return If false, the default value will be returned by the getters. + */ + bool isValid() const { return _value != nullptr; } + + /** + * Interpret the preference as a Boolean value. + * + * @param def Default value if the preference is not set. + */ + inline bool getBool(bool def=false) const; + + /** + * Interpret the preference as an point. + * + * @param def Default value if the preference is not set. + */ + inline Geom::Point getPoint(Geom::Point def=Geom::Point()) const; + + /** + * Interpret the preference as an integer. + * + * @param def Default value if the preference is not set. + */ + inline int getInt(int def=0) const; + + /** + * Interpret the preference as a limited integer. + * + * This method will return the default value if the interpreted value is + * larger than @c max or smaller than @c min. Do not use to store + * Boolean values as integers. + * + * @param def Default value if the preference is not set. + * @param min Minimum value allowed to return. + * @param max Maximum value allowed to return. + */ + inline int getIntLimited(int def=0, int min=INT_MIN, int max=INT_MAX) const; + + /** + * Interpret the preference as an unsigned integer. + * + * @param def Default value if the preference is not set. + */ + inline unsigned int getUInt(unsigned int def=0) const; + + /** + * Interpret the preference as a floating point value. + * + * @param def Default value if the preference is not set. + * @param unit Specifies the unit of the returned result. Will be ignored when equal to "". If the preference has no unit set, the default unit will be assumed. + */ + inline double getDouble(double def=0.0, Glib::ustring const &unit = "") const; + + /** + * Interpret the preference as a limited floating point value. + * + * This method will return the default value if the interpreted value is + * larger than @c max or smaller than @c min. + * + * @param def Default value if the preference is not set. + * @param min Minimum value allowed to return. + * @param max Maximum value allowed to return. + * @param unit Specifies the unit of the returned result. Will be ignored when equal to "". If the preference has no unit set, the default unit will be assumed. + */ + inline double getDoubleLimited(double def=0.0, double min=DBL_MIN, double max=DBL_MAX, Glib::ustring const &unit = "") const; + + /** + * Interpret the preference as an UTF-8 string. + * + * To store a filename, convert it using Glib::filename_to_utf8(). + */ + inline Glib::ustring getString() const; + + /** + * Interpret the preference as a number followed by a unit (without space), and return this unit string. + */ + inline Glib::ustring getUnit() const; + + /** + * Interpret the preference as an RGBA color value. + */ + inline guint32 getColor(guint32 def) const; + + /** + * Interpret the preference as a CSS style. + * + * @return A CSS style that has to be unrefed when no longer necessary. Never NULL. + */ + inline SPCSSAttr *getStyle() const; + + /** + * Interpret the preference as a CSS style with directory-based + * inheritance. + * + * This function will look up the preferences with the same entry name + * in ancestor directories and return the inherited CSS style. + * + * @return Inherited CSS style that has to be unrefed after use. Never NULL. + */ + inline SPCSSAttr *getInheritedStyle() const; + + /** + * Get the full path of the preference described by this Entry. + */ + Glib::ustring const &getPath() const { return _pref_path; } + + /** + * Get the last component of the preference's path. + * + * E.g. For "/options/some_group/some_option" it will return "some_option". + */ + Glib::ustring getEntryName() const; + private: + Entry(Glib::ustring path, void const *v) + : _pref_path(std::move(path)) + , _value(v) + , cached_bool(false) + , cached_point(false) + , cached_int(false) + , cached_uint(false) + , cached_double(false) + , cached_unit(false) + , cached_color(false) + , cached_style(false) {} + + Glib::ustring _pref_path; + void const *_value; + + mutable bool value_bool; + mutable Geom::Point value_point; + mutable int value_int; + mutable unsigned int value_uint; + mutable double value_double; + mutable Glib::ustring value_unit; + mutable guint32 value_color; + mutable SPCSSAttr* value_style; + + mutable bool cached_bool; + mutable bool cached_point; + mutable bool cached_int; + mutable bool cached_uint; + mutable bool cached_double; + mutable bool cached_unit; + mutable bool cached_color; + mutable bool cached_style; + }; + + // disable copying + Preferences(Preferences const &) = delete; + Preferences operator=(Preferences const &) = delete; + + // utility methods + + /** + * Save all preferences to the hard disk. + * + * For some backends, the preferences may be saved as they are modified. + * Not calling this method doesn't guarantee the preferences are unmodified + * the next time Inkscape runs. + */ + void save(); + + /** + * Deletes the preferences.xml file. + */ + void reset(); + /** + * Check whether saving the preferences will have any effect. + */ + bool isWritable() { return _writable; } + /*@}*/ + + /** + * Return details of the last encountered error, if any. + * + * This method will return true if an error has been encountered, and fill + * in the primary and secondary error strings of the last error. If an error + * had been encountered, this will reset it. + * + * @param string to set to the primary error message. + * @param string to set to the secondary error message. + * + * @return True if an error has occurred since last checking, false otherwise. + */ + bool getLastError( Glib::ustring& primary, Glib::ustring& secondary ); + + /** + * @name Iterate over directories and entries. + * @{ + */ + + /** + * Get all entries from the specified directory. + * + * This method will return a vector populated with preference entries + * from the specified directory. Subdirectories will not be represented. + */ + std::vector<Entry> getAllEntries(Glib::ustring const &path); + + /** + * Get all subdirectories of the specified directory. + * + * This will return a vector populated with full paths to the subdirectories + * present in the specified @c path. + */ + std::vector<Glib::ustring> getAllDirs(Glib::ustring const &path); + /*@}*/ + + /** + * @name Retrieve data from the preference storage. + * @{ + */ + + /** + * Retrieve a Boolean value. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + */ + bool getBool(Glib::ustring const &pref_path, bool def=false) { + return getEntry(pref_path).getBool(def); + } + + /** + * Retrieve a point. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + */ + Geom::Point getPoint(Glib::ustring const &pref_path, Geom::Point def=Geom::Point()) { + return getEntry(pref_path).getPoint(def); + } + + /** + * Retrieve an integer. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + */ + int getInt(Glib::ustring const &pref_path, int def=0) { + return getEntry(pref_path).getInt(def); + } + + /** + * Retrieve a limited integer. + * + * The default value is returned if the actual value is larger than @c max + * or smaller than @c min. Do not use to store Boolean values. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + * @param min Minimum value to return. + * @param max Maximum value to return. + */ + int getIntLimited(Glib::ustring const &pref_path, int def=0, int min=INT_MIN, int max=INT_MAX) { + return getEntry(pref_path).getIntLimited(def, min, max); + } + + /** + * Retrieve an unsigned integer. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + */ + unsigned int getUInt(Glib::ustring const &pref_path, unsigned int def=0) { + return getEntry(pref_path).getUInt(def); + } + + /** + * Retrieve a floating point value. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + */ + double getDouble(Glib::ustring const &pref_path, double def=0.0, Glib::ustring const &unit = "") { + return getEntry(pref_path).getDouble(def, unit); + } + + /** + * Retrieve a limited floating point value. + * + * The default value is returned if the actual value is larger than @c max + * or smaller than @c min. + * + * @param pref_path Path to the retrieved preference. + * @param def The default value to return if the preference is not set. + * @param min Minimum value to return. + * @param max Maximum value to return. + * @param unit Specifies the unit of the returned result. Will be ignored when equal to "". If the preference has no unit set, the default unit will be assumed. + */ + double getDoubleLimited(Glib::ustring const &pref_path, double def=0.0, double min=DBL_MIN, double max=DBL_MAX, Glib::ustring const &unit = "") { + return getEntry(pref_path).getDoubleLimited(def, min, max, unit); + } + + /** + * Retrieve an UTF-8 string. + * + * @param pref_path Path to the retrieved preference. + */ + Glib::ustring getString(Glib::ustring const &pref_path) { + return getEntry(pref_path).getString(); + } + + /** + * Retrieve the unit string. + * + * @param pref_path Path to the retrieved preference. + */ + Glib::ustring getUnit(Glib::ustring const &pref_path) { + return getEntry(pref_path).getUnit(); + } + + guint32 getColor(Glib::ustring const &pref_path, guint32 def=0x000000ff) { + return getEntry(pref_path).getColor(def); + } + + /** + * Retrieve a CSS style. + * + * @param pref_path Path to the retrieved preference. + * @return A CSS style that has to be unrefed after use. + */ + SPCSSAttr *getStyle(Glib::ustring const &pref_path) { + return getEntry(pref_path).getStyle(); + } + + /** + * Retrieve an inherited CSS style. + * + * This method will look up preferences with the same entry name in ancestor + * directories and return a style obtained by inheriting properties from + * ancestor styles. + * + * @param pref_path Path to the retrieved preference. + * @return An inherited CSS style that has to be unrefed after use. + */ + SPCSSAttr *getInheritedStyle(Glib::ustring const &pref_path) { + return getEntry(pref_path).getInheritedStyle(); + } + + /** + * Retrieve a preference entry without specifying its type. + */ + Entry const getEntry(Glib::ustring const &pref_path); + /*@}*/ + + + Glib::ustring getPrefsFilename() { + return _prefs_filename; + } + + /** + * @name Update preference values. + * @{ + */ + + /** + * Set a Boolean value. + */ + void setBool(Glib::ustring const &pref_path, bool value); + + /** + * Set a point value. + */ + void setPoint(Glib::ustring const &pref_path, Geom::Point value); + + /** + * Set an integer value. + */ + void setInt(Glib::ustring const &pref_path, int value); + + /** + * Set an unsigned integer value. + */ + void setUInt(Glib::ustring const &pref_path, unsigned int value); + + /** + * Set a floating point value. + */ + void setDouble(Glib::ustring const &pref_path, double value); + + /** + * Set a floating point value with unit. + */ + void setDoubleUnit(Glib::ustring const &pref_path, double value, Glib::ustring const &unit_abbr); + + /** + * Set an UTF-8 string value. + */ + void setString(Glib::ustring const &pref_path, Glib::ustring const &value); + + /** + * Set an RGBA color value. + */ + void setColor(Glib::ustring const &pref_path, guint32 value); + + /** + * Set a CSS style. + */ + void setStyle(Glib::ustring const &pref_path, SPCSSAttr *style); + + /** + * Merge a CSS style with the current preference value. + * + * This method is similar to setStyle(), except that it merges the style + * rather than replacing it. This means that if @c style doesn't have + * a property set, it is left unchanged in the style stored in + * the preferences. + */ + void mergeStyle(Glib::ustring const &pref_path, SPCSSAttr *style); + /*@}*/ + + /** + * @name Receive notifications about preference changes. + * @{ + */ + + /** + * Register a preference observer. + */ + void addObserver(Observer &); + + /** + * Remove an observer an prevent further notifications to it. + */ + void removeObserver(Observer &); + /*@}*/ + + /** + * @name Access and manipulate the Preferences object. + * @{ + */ + + + /** + * Access the singleton Preferences object. + */ + static Preferences *get() { + if (!_instance) { + _instance = new Preferences(); + } + return _instance; + } + + void setErrorHandler(ErrorReporter* handler); + + /** + * Unload all preferences. + * + * @param save Whether to save the preferences; defaults to true. + * + * This deletes the singleton object. Calling get() after this function + * will reinstate it, so you shouldn't. Pass false as the parameter + * to suppress automatic saving. + */ + static void unload(bool save=true); + /*@}*/ + + /** + * Remove a node from prefs + * @param pref_path Path to entry + * + */ + void remove(Glib::ustring const &pref_path); + +protected: + /* helper methods used by Entry + * This will enable using the same Entry class with different backends. + * For now, however, those methods are not virtual. These methods assume + * that v._value is not NULL + */ + bool _extractBool(Entry const &v); + Geom::Point _extractPoint(Entry const &v); + int _extractInt(Entry const &v); + unsigned int _extractUInt(Entry const &v); + double _extractDouble(Entry const &v); + double _extractDouble(Entry const &v, Glib::ustring const &requested_unit); + Glib::ustring _extractString(Entry const &v); + Glib::ustring _extractUnit(Entry const &v); + guint32 _extractColor(Entry const &v); + SPCSSAttr *_extractStyle(Entry const &v); + SPCSSAttr *_extractInheritedStyle(Entry const &v); + +private: + Preferences(); + ~Preferences(); + void _loadDefaults(); + void _load(); + void _getRawValue(Glib::ustring const &path, gchar const *&result); + void _setRawValue(Glib::ustring const &path, Glib::ustring const &value); + void _reportError(Glib::ustring const &, Glib::ustring const &); + void _keySplit(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key); + XML::Node *_getNode(Glib::ustring const &pref_path, bool create=false); + XML::Node *_findObserverNode(Glib::ustring const &pref_path, Glib::ustring &node_key, Glib::ustring &attr_key, bool create); + + std::string _prefs_filename; ///< Full filename (with directory) of the prefs file + Glib::ustring _lastErrPrimary; ///< Last primary error message, if any. + Glib::ustring _lastErrSecondary; ///< Last secondary error message, if any. + XML::Document *_prefs_doc = nullptr; ///< XML document storing all the preferences + ErrorReporter *_errorHandler = nullptr; ///< Pointer to object reporting errors. + bool _writable = false; ///< Will the preferences be saved at exit? + bool _hasError = false; ///< Indication that some error has occurred; + bool _initialized = false; ///< Is this instance fully initialized? Caching should be avoided before. + std::unordered_map<std::string, Glib::ustring> cachedRawValue; + + /// Wrapper class for XML node observers + class PrefNodeObserver; + + typedef std::map<Observer *, std::unique_ptr<PrefNodeObserver>> _ObsMap; + /// Map that keeps track of wrappers assigned to PrefObservers + _ObsMap _observer_map; + + // privilege escalation methods for PrefNodeObserver + static Entry const _create_pref_value(Glib::ustring const &, void const *ptr); + static _ObserverData *_get_pref_observer_data(Observer &o) { return o._data.get(); } + + static Preferences *_instance; + +friend class PrefNodeObserver; +friend class Entry; +}; + +/* Trivial inline Preferences::Entry functions. + * In fact only the _extract* methods do something, the rest is delegation + * to avoid duplication of code. There should be no performance hit if + * compiled with -finline-functions. + */ + +inline bool Preferences::Entry::getBool(bool def) const +{ + if (!this->isValid()) { + return def; + } else { + return Inkscape::Preferences::get()->_extractBool(*this); + } +} + +inline Geom::Point Preferences::Entry::getPoint(Geom::Point def) const +{ + if (!this->isValid()) { + return def; + } else { + return Inkscape::Preferences::get()->_extractPoint(*this); + } +} + +inline int Preferences::Entry::getInt(int def) const +{ + if (!this->isValid()) { + return def; + } else { + return Inkscape::Preferences::get()->_extractInt(*this); + } +} + +inline int Preferences::Entry::getIntLimited(int def, int min, int max) const +{ + if (!this->isValid()) { + return def; + } else { + int val = Inkscape::Preferences::get()->_extractInt(*this); + return ( val >= min && val <= max ? val : def ); + } +} + +inline unsigned int Preferences::Entry::getUInt(unsigned int def) const +{ + if (!this->isValid()) { + return def; + } else { + return Inkscape::Preferences::get()->_extractUInt(*this); + } +} + +inline double Preferences::Entry::getDouble(double def, Glib::ustring const &unit) const +{ + if (!this->isValid()) { + return def; + } else if (unit.length() == 0) { + return Inkscape::Preferences::get()->_extractDouble(*this); + } else { + return Inkscape::Preferences::get()->_extractDouble(*this, unit); + } +} + +inline double Preferences::Entry::getDoubleLimited(double def, double min, double max, Glib::ustring const &unit) const +{ + if (!this->isValid()) { + return def; + } else { + double val = def; + if (unit.length() == 0) { + val = Inkscape::Preferences::get()->_extractDouble(*this); + } else { + val = Inkscape::Preferences::get()->_extractDouble(*this, unit); + } + return ( val >= min && val <= max ? val : def ); + } +} + +inline Glib::ustring Preferences::Entry::getString() const +{ + if (!this->isValid()) { + return ""; + } else { + return Inkscape::Preferences::get()->_extractString(*this); + } +} + +inline Glib::ustring Preferences::Entry::getUnit() const +{ + if (!this->isValid()) { + return ""; + } else { + return Inkscape::Preferences::get()->_extractUnit(*this); + } +} + +inline guint32 Preferences::Entry::getColor(guint32 def) const +{ + if (!this->isValid()) { + return def; + } else { + return Inkscape::Preferences::get()->_extractColor(*this); + } +} + +inline SPCSSAttr *Preferences::Entry::getStyle() const +{ + if (!this->isValid()) { + return sp_repr_css_attr_new(); + } else { + return Inkscape::Preferences::get()->_extractStyle(*this); + } +} + +inline SPCSSAttr *Preferences::Entry::getInheritedStyle() const +{ + if (!this->isValid()) { + return sp_repr_css_attr_new(); + } else { + return Inkscape::Preferences::get()->_extractInheritedStyle(*this); + } +} + +inline Glib::ustring Preferences::Entry::getEntryName() const +{ + Glib::ustring path_base = _pref_path; + path_base.erase(0, path_base.rfind('/') + 1); + return path_base; +} + +} // namespace Inkscape + +#endif // INKSCAPE_PREFSTORE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:75 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/prefix.cpp b/src/prefix.cpp new file mode 100644 index 0000000..1712841 --- /dev/null +++ b/src/prefix.cpp @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * BinReloc - a library for creating relocatable executables + * Written by: Mike Hearn <mike@theoretic.com> + * Hongli Lai <h.lai@chello.nl> + * http://autopackage.org/ + * + * This source code is public domain. You can relicense this code + * under whatever license you want. + * + * NOTE: if you're using C++ and are getting "undefined reference + * to br_*", try renaming prefix.c to prefix.cpp + */ + +/* WARNING, BEFORE YOU MODIFY PREFIX.C: + * + * If you make changes to any of the functions in prefix.c, you MUST + * change the BR_NAMESPACE macro (in prefix.h). + * This way you can avoid symbol table conflicts with other libraries + * that also happen to use BinReloc. + * + * Example: + * #define BR_NAMESPACE(funcName) foobar_ ## funcName + * --> expands br_locate to foobar_br_locate + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef _PREFIX_C_ +#define _PREFIX_C_ + +#include <glib.h> +#include <cstdlib> +#include <cstdio> +#include <cstring> +#include "prefix.h" + + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +#ifdef __GNUC__ + #define br_return_val_if_fail(expr,val) if (!(expr)) {fprintf (stderr, "** BinReloc (%s): assertion %s failed\n", __PRETTY_FUNCTION__, #expr); return val;} +#else + #define br_return_val_if_fail(expr,val) if (!(expr)) return val +#endif /* __GNUC__ */ + + +#ifdef ENABLE_BINRELOC + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/param.h> +#include <unistd.h> + +/** + * br_locate: + * symbol: A symbol that belongs to the app/library you want to locate. + * Returns: A newly allocated string containing the full path of the + * app/library that func belongs to, or NULL on error. This + * string should be freed when not when no longer needed. + * + * Finds out to which application or library symbol belongs, then locate + * the full path of that application or library. + * Note that symbol cannot be a pointer to a function. That will not work. + * + * Example: + * --> main.c + * #include "prefix.h" + * #include "libfoo.h" + * + * int main (int argc, char *argv[]) { + * printf ("Full path of this app: %s\n", br_locate (&argc)); + * libfoo_start (); + * return 0; + * } + * + * --> libfoo.c starts here + * #include "prefix.h" + * + * void libfoo_start () { + * --> "" is a symbol that belongs to libfoo (because it's called + * --> from libfoo_start()); that's why this works. + * printf ("libfoo is located in: %s\n", br_locate ("")); + * } + */ +char * +br_locate (void *symbol) +{ + char line[5000]; + FILE *f; + char *path; + + br_return_val_if_fail (symbol != NULL, NULL); + + f = fopen ("/proc/self/maps", "r"); + if (!f) + return NULL; + + while (!feof (f)) + { + unsigned long start, end; + + if (!fgets (line, sizeof (line), f)) + continue; + + int inode = 0; + if (sscanf(line, "%lx-%lx r%*s %*x %*s %d", &start, &end, &inode) != 3 || inode == 0) + continue; + + if (symbol >= (void *) start && symbol < (void *) end) + { + char *tmp; + size_t len; + + /* Extract the filename; it is always an absolute path */ + path = strchr (line, '/'); + if (!path) + continue; + + /* Get rid of the newline */ + tmp = strrchr (path, '\n'); + if (tmp) *tmp = 0; + + /* Get rid of "(deleted)" */ + len = strlen (path); + if (len > 10 && strcmp (path + len - 10, " (deleted)") == 0) + { + tmp = path + len - 10; + *tmp = 0; + } + + fclose(f); + return strdup (path); + } + } + + fclose (f); + return NULL; +} + + +/** + * br_locate_prefix: + * symbol: A symbol that belongs to the app/library you want to locate. + * Returns: A prefix. This string should be freed when no longer needed. + * + * Locates the full path of the app/library that symbol belongs to, and return + * the prefix of that path, or NULL on error. + * Note that symbol cannot be a pointer to a function. That will not work. + * + * Example: + * --> This application is located in /usr/bin/foo + * br_locate_prefix (&argc); --> returns: "/usr" + */ +char * +br_locate_prefix (void *symbol) +{ + char *path, *prefix; + + br_return_val_if_fail (symbol != NULL, NULL); + + path = br_locate (symbol); + if (!path) return NULL; + + prefix = br_extract_prefix (path); + free (path); + return prefix; +} + + +/** + * br_prepend_prefix: + * symbol: A symbol that belongs to the app/library you want to locate. + * path: The path that you want to prepend the prefix to. + * Returns: The new path, or NULL on error. This string should be freed when no + * longer needed. + * + * Gets the prefix of the app/library that symbol belongs to. Prepend that prefix to path. + * Note that symbol cannot be a pointer to a function. That will not work. + * + * Example: + * --> The application is /usr/bin/foo + * br_prepend_prefix (&argc, "/share/foo/data.png"); --> Returns "/usr/share/foo/data.png" + */ +char * +br_prepend_prefix (void *symbol, char const *path) +{ + char *tmp, *newpath; + + br_return_val_if_fail (symbol != NULL, NULL); + br_return_val_if_fail (path != NULL, NULL); + + tmp = br_locate_prefix (symbol); + if (!tmp) return NULL; + + if (strcmp (tmp, "/") == 0) + newpath = strdup (path); + else + newpath = br_strcat (tmp, path); + + /* Get rid of compiler warning ("br_prepend_prefix never used") */ + if (0) br_prepend_prefix (NULL, NULL); + + free (tmp); + return newpath; +} + +#endif /* ENABLE_BINRELOC */ + + +/* Thread stuff for thread safetiness */ +#if BR_THREADS + +GPrivate* br_thread_key = (GPrivate *)NULL; + +/* + We do not need local store init() or fini(), because + g_private_new (g_free) will take care of all of that + for us. Isn't GLib wonderful? +*/ + +#else /* !BR_THREADS */ + +static char *br_last_value = (char*)nullptr; + +static void +br_free_last_value () +{ + if (br_last_value) + free (br_last_value); +} + +#endif /* BR_THREADS */ + + +/** + * br_thread_local_store: + * str: A dynamically allocated string. + * Returns: str. This return value must not be freed. + * + * Store str in a thread-local variable and return str. The next + * you run this function, that variable is freed too. + * This function is created so you don't have to worry about freeing + * strings. + * + * Example: + * char *foo; + * foo = thread_local_store (strdup ("hello")); --> foo == "hello" + * foo = thread_local_store (strdup ("world")); --> foo == "world"; "hello" is now freed. + */ +const char * +br_thread_local_store (char *str) +{ + #if BR_THREADS + if (!g_thread_supported ()) + { + g_thread_init ((GThreadFunctions *)NULL); + br_thread_key = g_private_new (g_free); + } + + char *specific = (char *) g_private_get (br_thread_key); + if (specific) + free (specific); + g_private_set (br_thread_key, str); + + #else /* !BR_THREADS */ + static int initialized = 0; + + if (!initialized) + { + atexit (br_free_last_value); + initialized = 1; + } + + if (br_last_value) + free (br_last_value); + br_last_value = str; + #endif /* BR_THREADS */ + + return (const char *) str; +} + + +/** + * br_strcat: + * str1: A string. + * str2: Another string. + * Returns: A newly-allocated string. This string should be freed when no longer needed. + * + * Concatenate str1 and str2 to a newly allocated string. + */ +char * +br_strcat (const char *str1, const char *str2) +{ + char *result; + size_t len1, len2; + + if (!str1) str1 = ""; + if (!str2) str2 = ""; + + len1 = strlen (str1); + len2 = strlen (str2); + + result = (char *) malloc (len1 + len2 + 1); + memcpy (result, str1, len1); + memcpy (result + len1, str2, len2); + result[len1 + len2] = '\0'; + + return result; +} + + +/* Emulates glibc's strndup() */ +static char * +br_strndup (char *str, size_t size) +{ + char *result = (char*)nullptr; + size_t len; + + br_return_val_if_fail (str != (char*)nullptr, (char*)nullptr); + + len = strlen (str); + if (!len) return strdup (""); + if (size > len) size = len; + + result = (char *) calloc (sizeof (char), len + 1); + memcpy (result, str, size); + return result; +} + + +/** + * br_extract_dir: + * path: A path. + * Returns: A directory name. This string should be freed when no longer needed. + * + * Extracts the directory component of path. Similar to g_path_get_dirname() + * or the dirname commandline application. + * + * Example: + * br_extract_dir ("/usr/local/foobar"); --> Returns: "/usr/local" + */ +char * +br_extract_dir (const char *path) +{ + const char *end; + char *result; + + br_return_val_if_fail (path != (char*)nullptr, (char*)nullptr); + + end = strrchr (path, '/'); + if (!end) return strdup ("."); + + while (end > path && *end == '/') + end--; + result = br_strndup ((char *) path, end - path + 1); + if (!*result) + { + free (result); + return strdup ("/"); + } else + return result; +} + + +/** + * br_extract_prefix: + * path: The full path of an executable or library. + * Returns: The prefix, or NULL on error. This string should be freed when no longer needed. + * + * Extracts the prefix from path. This function assumes that your executable + * or library is installed in an LSB-compatible directory structure. + * + * Example: + * br_extract_prefix ("/usr/bin/gnome-panel"); --> Returns "/usr" + * br_extract_prefix ("/usr/local/lib/libfoo.so"); --> Returns "/usr/local" + * br_extract_prefix ("/usr/local/libfoo.so"); --> Returns "/usr" + */ +char * +br_extract_prefix (const char *path) +{ + const char *end; + char *tmp, *result; + + br_return_val_if_fail (path != (char*)nullptr, (char*)nullptr); + + if (!*path) return strdup ("/"); + end = strrchr (path, '/'); + if (!end) return strdup (path); + + tmp = br_strndup ((char *) path, end - path); + if (!*tmp) + { + free (tmp); + return strdup ("/"); + } + end = strrchr (tmp, '/'); + if (!end) return tmp; + + result = br_strndup (tmp, end - tmp); + free (tmp); + + if (!*result) + { + free (result); + result = strdup ("/"); + } + + return result; +} + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* _PREFIX_C */ diff --git a/src/prefix.h b/src/prefix.h new file mode 100644 index 0000000..6c71b25 --- /dev/null +++ b/src/prefix.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * BinReloc - a library for creating relocatable executables + * Written by: Mike Hearn <mike@theoretic.com> + * Hongli Lai <h.lai@chello.nl> + * http://autopackage.org/ + * + * This source code is public domain. You can relicense this code + * under whatever license you want. + * + * See http://autopackage.org/docs/binreloc/ for + * more information and how to use this. + * + * NOTE: if you're using C++ and are getting "undefined reference + * to br_*", try renaming prefix.c to prefix.cpp + */ + +#ifndef _PREFIX_H_ +#define _PREFIX_H_ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* WARNING, BEFORE YOU MODIFY PREFIX.C: + * + * If you make changes to any of the functions in prefix.c, you MUST + * change the BR_NAMESPACE macro. + * This way you can avoid symbol table conflicts with other libraries + * that also happen to use BinReloc. + * + * Example: + * #define BR_NAMESPACE(funcName) foobar_ ## funcName + * --> expands br_locate to foobar_br_locate + */ +#undef BR_NAMESPACE +#define BR_NAMESPACE(funcName) funcName + + +#ifdef ENABLE_BINRELOC + +#define br_thread_local_store BR_NAMESPACE(br_thread_local_store) +#define br_locate BR_NAMESPACE(br_locate) +#define br_locate_prefix BR_NAMESPACE(br_locate_prefix) +#define br_prepend_prefix BR_NAMESPACE(br_prepend_prefix) + +#ifndef BR_NO_MACROS + /* These are convenience macros that replace the ones usually used + in Autoconf/Automake projects */ + #undef SELFPATH + #undef PREFIX + #undef PREFIXDIR + #undef BINDIR + #undef SBINDIR + #undef DATADIR + #undef LIBDIR + #undef LIBEXECDIR + #undef ETCDIR + #undef SYSCONFDIR + #undef CONFDIR + #undef LOCALEDIR + + #define SELFPATH (br_thread_local_store (br_locate ((void *) ""))) + #define PREFIX (br_thread_local_store (br_locate_prefix ((void *) ""))) + #define PREFIXDIR (br_thread_local_store (br_locate_prefix ((void *) ""))) + #define BINDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/bin"))) + #define SBINDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/sbin"))) + #define DATADIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/share"))) + #define LIBDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/lib"))) + #define LIBEXECDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/libexec"))) + #define ETCDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/etc"))) + #define SYSCONFDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/etc"))) + #define CONFDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/etc"))) + #define LOCALEDIR (br_thread_local_store (br_prepend_prefix ((void *) "", "/share/locale"))) +#endif /* BR_NO_MACROS */ + + +/* The following functions are used internally by BinReloc + and shouldn't be used directly in applications. */ + +const char *br_thread_local_store (char *str); +char *br_locate (void *symbol); +char *br_locate_prefix (void *symbol); +char *br_prepend_prefix (void *symbol, char const *path); + + +#endif /* ENABLE_BINRELOC */ + + +/* These macros and functions are not guarded by the ENABLE_BINRELOC + * macro because they are portable. You can use these functions. + */ + +#define br_strcat BR_NAMESPACE(br_strcat) +#define br_extract_dir BR_NAMESPACE(br_extract_dir) +#define br_extract_prefix BR_NAMESPACE(br_extract_prefix) + +#ifndef BR_NO_MACROS + /* Convenience functions for concatenating paths */ + #define BR_SELFPATH(suffix) (br_thread_local_store (br_strcat (SELFPATH, suffix))) + #define BR_PREFIX(suffix) (br_thread_local_store (br_strcat (PREFIX, suffix))) + #define BR_PREFIXDIR(suffix) (br_thread_local_store (br_strcat (BR_PREFIX, suffix))) + #define BR_BINDIR(suffix) (br_thread_local_store (br_strcat (BINDIR, suffix))) + #define BR_SBINDIR(suffix) (br_thread_local_store (br_strcat (SBINDIR, suffix))) + #define BR_DATADIR(suffix) (br_thread_local_store (br_strcat (DATADIR, suffix))) + #define BR_LIBDIR(suffix) (br_thread_local_store (br_strcat (LIBDIR, suffix))) + #define BR_LIBEXECDIR(suffix) (br_thread_local_store (br_strcat (LIBEXECDIR, suffix))) + #define BR_ETCDIR(suffix) (br_thread_local_store (br_strcat (ETCDIR, suffix))) + #define BR_SYSCONFDIR(suffix) (br_thread_local_store (br_strcat (SYSCONFDIR, suffix))) + #define BR_CONFDIR(suffix) (br_thread_local_store (br_strcat (CONFDIR, suffix))) + #define BR_LOCALEDIR(suffix) (br_thread_local_store (br_strcat (LOCALEDIR, suffix))) +#endif + +char *br_strcat (const char *str1, const char *str2); +char *br_extract_dir (const char *path); +char *br_extract_prefix(const char *path); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* _PREFIX_H_ */ diff --git a/src/print.cpp b/src/print.cpp new file mode 100644 index 0000000..6e9c85f --- /dev/null +++ b/src/print.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** \file + * Frontend to printing + */ +/* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Kees Cook <kees@outflux.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * This code is in public domain + */ + +#include "print.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" + +#include "display/drawing-item.h" +#include "display/drawing.h" + +#include "extension/print.h" +#include "extension/system.h" + +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "ui/dialog/print.h" + + +unsigned int +SPPrintContext::bind(Geom::Affine const &transform, float opacity) +{ + return module->bind(transform, opacity); +} + +unsigned int +SPPrintContext::release() +{ + return module->release(); +} + +unsigned int +SPPrintContext::comment(char const *comment) +{ + return module->comment(comment); +} + +unsigned int +SPPrintContext::fill(Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return module->fill(pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +SPPrintContext::stroke(Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return module->stroke(pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +SPPrintContext::image_R8G8B8A8_N(guchar *px, unsigned int w, unsigned int h, unsigned int rs, + Geom::Affine const &transform, SPStyle const *style) +{ + return module->image(px, w, h, rs, transform, style); +} + +unsigned int SPPrintContext::text(char const *text, Geom::Point p, + SPStyle const *style) +{ + return module->text(text, p, style); +} + +/* UI */ + +void +sp_print_document(Gtk::Window& parentWindow, SPDocument *doc) +{ + doc->ensureUpToDate(); + + // Build arena + SPItem *base = doc->getRoot(); + + // Run print dialog + Inkscape::UI::Dialog::Print printop(doc,base); + Gtk::PrintOperationResult res = printop.run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parentWindow); + (void)res; // TODO handle this +} + +void sp_print_document_to_file(SPDocument *doc, gchar const *filename) +{ + doc->ensureUpToDate(); + + Inkscape::Extension::Print *mod = Inkscape::Extension::get_print(SP_MODULE_KEY_PRINT_PS); + SPPrintContext context; + gchar const *oldconst = mod->get_param_string("destination"); + gchar *oldoutput = g_strdup(oldconst); + + mod->set_param_string("destination", (gchar *)filename); + +/* Start */ + context.module = mod; + /* fixme: This has to go into module constructor somehow */ + /* Create new drawing */ + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = (mod->base)->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + /* Print document */ + mod->begin(doc); + (mod->base)->invoke_print(&context); + mod->finish(); + /* Release drawing items */ + (mod->base)->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // should be deleted by invoke_hide +/* end */ + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/print.h b/src/print.h new file mode 100644 index 0000000..e983469 --- /dev/null +++ b/src/print.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef PRINT_H_INKSCAPE +#define PRINT_H_INKSCAPE + +/** \file + * Frontend to printing + */ +/* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * This code is in public domain + */ + +#include <2geom/forward.h> + +namespace Gtk { +class Window; +} + +class SPDocument; +class SPStyle; + +namespace Inkscape { +namespace Extension { + +class Print; + +} // namespace Extension +} // namespace Inkscape + +struct SPPrintContext { + Inkscape::Extension::Print *module; + + unsigned int bind(Geom::Affine const &transform, float opacity); + unsigned int release(); + unsigned int comment(char const *comment); + unsigned int fill(Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox); + unsigned int stroke(Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox); + + unsigned int image_R8G8B8A8_N(unsigned char *px, unsigned int w, unsigned int h, unsigned int rs, + Geom::Affine const &transform, SPStyle const *style); + + unsigned int text(char const *text, Geom::Point p, + SPStyle const *style); + + void get_param(char *name, bool *value); +}; + + +/* UI */ +void sp_print_document(Gtk::Window& parentWindow, SPDocument *doc); +void sp_print_document_to_file(SPDocument *doc, char const *filename); + + +#endif /* !PRINT_H_INKSCAPE */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/profile-manager.cpp b/src/profile-manager.cpp new file mode 100644 index 0000000..b092980 --- /dev/null +++ b/src/profile-manager.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ProfileManager - a view of a document's color profiles. + * + * Copyright 2007 Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <cstring> + +#include "profile-manager.h" + +#include "document.h" + +#include "object/color-profile.h" + + +namespace Inkscape { + +ProfileManager::ProfileManager(SPDocument *document) : + _doc(document), + _knownProfiles() +{ + _resource_connection = _doc->connectResourcesChanged( "iccprofile", sigc::mem_fun(*this, &ProfileManager::_resourcesChanged) ); +} + +ProfileManager::~ProfileManager() +{ + _resource_connection.disconnect(); + _doc = nullptr; +} + +void ProfileManager::_resourcesChanged() +{ + std::vector<SPObject*> newList; + if (_doc) { + std::vector<SPObject *> current = _doc->getResourceList( "iccprofile" ); + newList = current; + } + sort( newList.begin(), newList.end() ); + + std::vector<SPObject*> diff1; + std::set_difference( _knownProfiles.begin(), _knownProfiles.end(), newList.begin(), newList.end(), + std::insert_iterator<std::vector<SPObject*> >(diff1, diff1.begin()) ); + + std::vector<SPObject*> diff2; + std::set_difference( newList.begin(), newList.end(), _knownProfiles.begin(), _knownProfiles.end(), + std::insert_iterator<std::vector<SPObject*> >(diff2, diff2.begin()) ); + + if ( !diff1.empty() ) { + for ( std::vector<SPObject*>::iterator it = diff1.begin(); it < diff1.end(); ++it ) { + SPObject* tmp = *it; + _knownProfiles.erase( remove(_knownProfiles.begin(), _knownProfiles.end(), tmp), _knownProfiles.end() ); + if ( includes(tmp) ) { + _removeOne(tmp); + } + } + } + + if ( !diff2.empty() ) { + for ( std::vector<SPObject*>::iterator it = diff2.begin(); it < diff2.end(); ++it ) { + SPObject* tmp = *it; + _knownProfiles.push_back(tmp); + _addOne(tmp); + } + sort( _knownProfiles.begin(), _knownProfiles.end() ); + } +} + +ColorProfile* ProfileManager::find(gchar const* name) +{ + ColorProfile* match = nullptr; + if ( name ) { + unsigned int howMany = childCount(nullptr); + for ( unsigned int index = 0; index < howMany; index++ ) { + SPObject *obj = nthChildOf(nullptr, index); + ColorProfile* prof = reinterpret_cast<ColorProfile*>(obj); + if (prof && (prof->name && !strcmp(name, prof->name))) { + match = prof; + break; + } + } + } + return match; +} + +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/profile-manager.h b/src/profile-manager.h new file mode 100644 index 0000000..66655d1 --- /dev/null +++ b/src/profile-manager.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ProfileManager - a view of a document's color profiles. + * + * Copyright 2007 Jon A. Cruz <jon@joncruz.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_PROFILE_MANAGER_H +#define SEEN_INKSCAPE_PROFILE_MANAGER_H + +#include <vector> + +#include "document-subset.h" +#include "gc-finalized.h" + +class SPDocument; + +namespace Inkscape { + +class ColorProfile; + +class ProfileManager : public DocumentSubset, + public GC::Finalized +{ +public: + ProfileManager(SPDocument *document); + ~ProfileManager() override; + + ColorProfile* find(char const* name); + +private: + ProfileManager(ProfileManager const &) = delete; // no copy + void operator=(ProfileManager const &) = delete; // no assign + + void _resourcesChanged(); + + SPDocument* _doc; + sigc::connection _resource_connection; + std::vector<SPObject*> _knownProfiles; +}; + +} + +#endif // SEEN_INKSCAPE_PROFILE_MANAGER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/proj_pt.cpp b/src/proj_pt.cpp new file mode 100644 index 0000000..db9827f --- /dev/null +++ b/src/proj_pt.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * 3x4 transformation matrix to map points from projective 3-space into the projective plane + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "proj_pt.h" +#include "svg/stringstream.h" + +namespace Proj { + +Pt2::Pt2(const char *coord_str) { + if (!coord_str) { + pt[0] = 0.0; + pt[1] = 0.0; + pt[2] = 1.0; + g_warning ("Coordinate string is empty. Creating default Pt2\n"); + return; + } + char **coords = g_strsplit(coord_str, ":", 0); + if (coords[0] == nullptr || coords[1] == nullptr || coords[2] == nullptr) { + g_strfreev (coords); + g_warning ("Malformed coordinate string.\n"); + return; + } + + pt[0] = g_ascii_strtod(coords[0], nullptr); + pt[1] = g_ascii_strtod(coords[1], nullptr); + pt[2] = g_ascii_strtod(coords[2], nullptr); + g_strfreev (coords); +} + +void +Pt2::normalize() { + if (fabs(pt[2]) < 1E-6 || pt[2] == 1.0) + return; + pt[0] /= pt[2]; + pt[1] /= pt[2]; + pt[2] = 1.0; +} + +Geom::Point +Pt2::affine() { + if (fabs(pt[2]) < epsilon) { + return Geom::Point (Geom::infinity(), Geom::infinity()); + } + return Geom::Point (pt[0]/pt[2], pt[1]/pt[2]); +} + +char * +Pt2::coord_string() { + Inkscape::SVGOStringStream os; + os << pt[0] << " : " + << pt[1] << " : " + << pt[2]; + return g_strdup(os.str().c_str()); +} + +Pt3::Pt3(const char *coord_str) { + if (!coord_str) { + pt[0] = 0.0; + pt[1] = 0.0; + pt[2] = 0.0; + pt[3] = 1.0; + g_warning ("Coordinate string is empty. Creating default Pt2\n"); + return; + } + char **coords = g_strsplit(coord_str, ":", 0); + if (coords[0] == nullptr || coords[1] == nullptr || + coords[2] == nullptr || coords[3] == nullptr) { + g_strfreev (coords); + g_warning ("Malformed coordinate string.\n"); + return; + } + + pt[0] = g_ascii_strtod(coords[0], nullptr); + pt[1] = g_ascii_strtod(coords[1], nullptr); + pt[2] = g_ascii_strtod(coords[2], nullptr); + pt[3] = g_ascii_strtod(coords[3], nullptr); +} + +void +Pt3::normalize() { + if (fabs(pt[3]) < 1E-6 || pt[3] == 1.0) + return; + pt[0] /= pt[3]; + pt[1] /= pt[3]; + pt[2] /= pt[3]; + pt[3] = 1.0; +} + +char * +Pt3::coord_string() { + Inkscape::SVGOStringStream os; + os << pt[0] << " : " + << pt[1] << " : " + << pt[2] << " : " + << pt[3]; + return g_strdup(os.str().c_str()); +} + +} // namespace Proj + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/proj_pt.h b/src/proj_pt.h new file mode 100644 index 0000000..1d1ec47 --- /dev/null +++ b/src/proj_pt.h @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * 3x4 transformation matrix to map points from projective 3-space into the projective plane + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PROJ_PT_H +#define SEEN_PROJ_PT_H + +#include <2geom/point.h> +#include <cstdio> + +namespace Proj { + +const double epsilon = 1E-6; + +// TODO: Catch the case when the constructors are called with only zeros +class Pt2 { +public: + Pt2 () { pt[0] = 0; pt[1] = 0; pt[2] = 1.0; } // we default to (0 : 0 : 1) + Pt2 (double x, double y, double w) { pt[0] = x; pt[1] = y; pt[2] = w; } + Pt2 (Geom::Point const &point) { pt[0] = point[Geom::X]; pt[1] = point[Geom::Y]; pt[2] = 1; } + Pt2 (const char *coord_str); + + inline double operator[] (unsigned int index) const { + if (index > 2) { return Geom::infinity(); } + return pt[index]; + } + inline double &operator[] (unsigned int index) { + // FIXME: How should we handle wrong indices? + //if (index > 2) { return Geom::infinity(); } + return pt[index]; + } + inline bool operator== (Pt2 &rhs) { + normalize(); + rhs.normalize(); + return (fabs(pt[0] - rhs.pt[0]) < epsilon && + fabs(pt[1] - rhs.pt[1]) < epsilon && + fabs(pt[2] - rhs.pt[2]) < epsilon); + } + inline bool operator!= (Pt2 &rhs) { + return !((*this) == rhs); + } + + /*** For convenience, we define addition/subtraction etc. as "affine" operators (i.e., + the result for finite points is the same as if the affine points were added ***/ + inline Pt2 operator+(Pt2 &rhs) const { + Pt2 result (*this); + result.normalize(); + rhs.normalize(); + for ( unsigned i = 0 ; i < 2 ; ++i ) { + result.pt[i] += rhs.pt[i]; + } + return result; + } + + inline Pt2 operator-(Pt2 &rhs) const { + Pt2 result (*this); + result.normalize(); + rhs.normalize(); + for ( unsigned i = 0 ; i < 2 ; ++i ) { + result.pt[i] -= rhs.pt[i]; + } + return result; + } + + inline Pt2 operator*(double const s) const { + Pt2 result (*this); + result.normalize(); + for ( unsigned i = 0 ; i < 2 ; ++i ) { + result.pt[i] *= s; + } + return result; + } + + void normalize(); + Geom::Point affine(); + inline bool is_finite() { return pt[2] != 0; } // FIXME: Should we allow for some tolerance? + char *coord_string(); + inline void print(char const *s) const { printf ("%s(%8.2f : %8.2f : %8.2f)\n", s, pt[0], pt[1], pt[2]); } + +private: + double pt[3]; +}; + + +class Pt3 { +public: + Pt3 () { pt[0] = 0; pt[1] = 0; pt[2] = 0; pt[3] = 1.0; } // we default to (0 : 0 : 0 : 1) + Pt3 (double x, double y, double z, double w) { pt[0] = x; pt[1] = y; pt[2] = z; pt[3] = w; } + Pt3 (const char *coord_str); + + inline bool operator== (Pt3 &rhs) { + normalize(); + rhs.normalize(); + return (fabs(pt[0] - rhs.pt[0]) < epsilon && + fabs(pt[1] - rhs.pt[1]) < epsilon && + fabs(pt[2] - rhs.pt[2]) < epsilon && + fabs(pt[3] - rhs.pt[3]) < epsilon); + } + + /*** For convenience, we define addition/subtraction etc. as "affine" operators (i.e., + the result for finite points is the same as if the affine points were added ***/ + inline Pt3 operator+(Pt3 &rhs) const { + Pt3 result(*this); + result.normalize(); + rhs.normalize(); + for ( unsigned i = 0 ; i < 3 ; ++i ) { + result.pt[i] += rhs.pt[i]; + } + return result; + } + + inline Pt3 operator-(Pt3 &rhs) const { + Pt3 result (*this); + result.normalize(); + rhs.normalize(); + for ( unsigned i = 0 ; i < 3 ; ++i ) { + result.pt[i] -= rhs.pt[i]; + } + return result; + } + + inline Pt3 operator*(double const s) const { + Pt3 result (*this); + result.normalize(); + for ( unsigned i = 0 ; i < 3 ; ++i ) { + result.pt[i] *= s; + } + return result; + } + + inline double operator[] (unsigned int index) const { + if (index > 3) { return Geom::infinity(); } + return pt[index]; + } + inline double &operator[] (unsigned int index) { + // FIXME: How should we handle wrong indices? + //if (index > 3) { return Geom::infinity(); } + return pt[index]; + } + void normalize(); + inline bool is_finite() { return pt[3] != 0; } // FIXME: Should we allow for some tolerance? + char *coord_string(); + inline void print(char const *s) const { + printf ("%s(%8.2f : %8.2f : %8.2f : %8.2f)\n", s, pt[0], pt[1], pt[2], pt[3]); + } + +private: + double pt[4]; +}; + +} // namespace Proj + +#endif // !SEEN_PROJ_PT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/proofs b/src/proofs new file mode 100644 index 0000000..7134196 --- /dev/null +++ b/src/proofs @@ -0,0 +1,335 @@ + +# SPDX-License-Identifier: GPL-2.0-or-later + +This file contains some loose proofs of a few properties. It's somewhat +ad-hoc. At least it gives an indication of what assert/g_assert calls have +been checked by a developer. If an assertion does trigger, then this file may +help in debugging that assertion failure. + +It's currently ordered by caller. + +(Re-ordering to avoid forward references in proofs might be a good idea, +though this would in some cases require splitting up the proofs for a routine, +e.g. proving preconditions of g called from f, then proving g's postcondition, +then using that to prove something else in f again. Furthermore it may not +even be possible to avoid forward references for recursive/looping code.) + + + +src/pencil-context.cpp:fit_and_split + +Very loose proof of !sp_curve_empty (pc->red_curve) assertion: +fit_and_split is called successively with its input varying only by appending a point. +For the n_segs > 0 && unsigned(pc->npoints) < G_N_ELEMENTS(pc->p) condition to fail, +we must have at least 3 distinct points, which means that a previous call had 2 distinct points, +in which case we'd have filled in pc->red_curve to a non-empty curve. + +Expansion of the above claim of at least 3 distinct points: We know n_segs <= 0 || +unsigned(dc->npoints) >= G_N_ELEMENTS(pc->p) from the negation of the containing `if' condition. +G_N_ELEMENTS(pc->p) is greater than 3 (in int arithmetic), from PencilTool::p array definition +in pencil-context.h. npoints grows by no more than one per fit_and_split invocation; we should be +able to establish that dc->npoints == G_N_ELEMENTS(pc->p) if unsigned(dc->npoints) >= +G_N_ELEMENTS(pc->p), in which case 3 <= dc->npoints in int arithmetic. We know that dc->npoints >= +2 from assertion at head of fit_and_split; in which case if n_segs <= 0 then fit_and_split has +failed, which implies that dc->npoints > 2 (since the fitter can always exactly fit 2 points, +i.e. it never fails if npoints == 2; TODO: add sp_bezier_fit_cubic postcondition for this). + + +src/pencil-context.cpp:fit_and_split + +Proof of precondition: The only caller is spdc_add_freehand_point (by +textual search in that file, and staticness). See proof for that +function. + + +src/pencil-context.cpp:spdc_add_freehand_point + +Proof of fit_and_split `pc->npoints > 1' requirement: +It initially unconditionally asserts `pc->npoints > 0'. There are no function calls or modifications +of pc or pc->npoints other than incrementing pc->npoints after that assertion. +We assume that integer overflow doesn't occur during that increment, +so we get pc->npoints > 1. + + +src/pencil-context.cpp:spdc_set_endpoint + +Very loose proof of npoints > 0: Should be preceded by spdc_set_startpoint(pc) according to state +transitions. spdc_set_startpoint sets pc->npoints to 0 (handled at beginning of function) or 1. + + +src/display/bezier-utils.cpp:compute_max_error + +Proof of postcondition: *splitPoint is set only from i, which goes from 1 to less than last. +i isn't written to in the loop body: only uses are indexing built-in arrays d and u +(and RHS of assignment). + + +src/display/bezier-utils.cpp:sp_bezier_fit_cubic_full + +Proof of `nsegs1 != 0' assertion: nsegs1 is const. Have already +returned in the (nsegs1 < 0) case, so !(nsegs1 < 0), i.e. nsegs1 >= 0 +(given that nsegs1 is gint). nsegs1 is set to +sp_bezier_fit_cubic_full(_, _, _, splitPoint + 1, ...). We will show +that sp_bezier_fit_cubic_full ensures len < 2 || ret != 0. splitPoint +> 0 from compute_max_error postcondition combined with error >= +precondition and thus having handled the compute_max_error returning 0 +case: if returned 0 for maxError then maxError <= error * 9.0 would be +true, and we recalculate splitPoint; if the renewed maxError is 0 then +the maxError <= error test will succeed and we return. If we don't +return, then the most recent compute_max_error must have returned +non-zero, which implies (through compute_max_error postcondition) that +splitPoint would have been set s.t. 0 < splitPoint. splitPoint is not +subsequently written to. (It is subsequently passed only to +sp_darray_center_tangent 2nd arg, which is a plain unsigned rather +than reference.) 0 < splitPoint < last guarantees splitPoint + 1 >= +2. (We know splitPoint + 1 won't overflow because last = len - 1 and +len is of the same type as splitPoint.) Passing splitPoint + 1 for +len of the call that sets nsegs1 ensures that nsegs1 is non-zero (from +the len < 2 || ret != 0 property that we show below). QED. + +Proof that len < 2 || (failed no-dups precondition) || ret != 0: All +returns are either -1, 0, 1, or nsegs1 + nsegs2. There are two +literal 0 cases: one conditional on len < 2, and the other for failed +precondition (non-uniqued data). For the nsegs1 + nsegs2 case, we've +already ruled out nsegs1 < 0 (through conditional return) and nsegs2 < +0 (same). The nsegs1 + nsegs2 case occurs only when we recurse; we've +already shown the desired property for non-recursive case. In the +nsegs1 non-recursive case, we have that nsegs1 != 0, which combined +with !(nsegs1 < 0) and !(nsegs2 < 0) implies that nsegs1 + nsegs2 +either overflows or is greater than 0. We should be able to show that +nsegs1 + nsegs2 < len even with exact arithmetic. (Very loose proof: +given that len >= 2 (from earlier conditional return), we can fit len +points using len-1 segments even using straight line segments.) +nsegs1 and nsegs2 are the same type as len, and we've shown that +nsegs1 + nsegs2 in exact arithmetic is >= 0 from each operand being +non-negative, so nsegs1 + nsegs2 doesn't overflow. Thus nsegs1 + +nsegs2 > 0. Thus we have shown for each return point that either the +return value is -1 or > 0 or occurs when len < 2 or in failure of +no-dups precondition. (We should also show that no-dups outer +precondition being met implies it being met for inner instances of +sp_bezier_fit_cubic_full, because we pass a subsequence of the data +array, and don't write to that array.) QED. + +We should also show that the recursion is finite for the inductive +proof to hold. The finiteness comes from inner calls having len > 0 +and len less than that of outer calls (from splitPoint calculation and +0 < splitPoint < last for recursive case and last < len and transitive +property of < for gint). If len < 2 then we don't recurse +(conditional return). + +We should go over this proof to make it clear that there are no +"forward references" other than for recursive case. We could also be +more formal in use of inductive proof (e.g. clearly stating what the +base and inductive premises are; namely the non-recursing and +recursing cases of sp_bezier_fit_cubic_full). + +Better proof sketch that nseg1 + nsegs2 < len: ret < len for each +recursive case other than where len > 0 precondition fails. nsegs1 is +calculated for inner len=splitPoint + 1, nsegs2 for inner len=len - +splitPoint. Assuming exact arithmetic we'll transform that to ret <= +len - 1. Implies that in exact arithmetic, nsegs1 + nsegs2 <= +(splitPoint + 1 - 1) + (len - splitPoint - 1). Simplifying RHS (using +exact arithmetic): nsegs1 + nsegs2 <= len - 1, i.e. nsegs1 + nsegs2 < +len. Presumably we can show that machine arithmetic gets the same +results as exact arithmetic from similar arguments used so far for +showing that overflow doesn't occur. For the recursive case the +return values are either nsegs1 + nsegs2 or -1. + +We should also show that inner preconditions hold, especially the len +> 0 precondition. (For nsegs1 call, we use 0 < splitPoint and that +splitPoint + 1 doesn't overflow. For nsegs2 call, we pass len - +splitPoint; combine with splitPoint < last, last = len - 1, and no +overflow.) We've already sketched a proof for no-dups precondition. +The others are fairly simple. + +For error >= 0: error is const, and we pass it to all recursions. + +For inner max_beziers >= 1: recursions are conditional on outer +1 < max_beziers before setting rec_max_beziers1 to max_beziers - 1, +and passing rec_max_beziers1 as inner max_beziers value, +so we have outer max_beziers >= 2 so inner max_beziers >= 1. +max_beziers and rec_max_beziers1 are both const. + + +src/display/bezier-utils.cpp:sp_darray_right_tangent(Point const[], unsigned) + +Proof of unit_vector precondition that a != Point(0, 0): our unequal precondition. + +Loose (incorrect) proof of unit_vector precondition that neither +coordinate is NaN: our in_svg_plane precondition, and fact that +in_svg_plane returns false if either argument is infinite. HOWEVER, +the unchecked in_svg_plane precondition isn't currently guaranteed, so +we're just relying on the input points never being infinity (which +might occur with strange zoom settings). + + +src/display/bezier-utils.cpp:sp_darray_right_tangent(Point const[], unsigned, double) + +Loose proof of unit_vector precondition that a != Point(0, 0) for first call to unit_vector: + +We've asserted that 0 <= tolerance_sq; combine with tolerance_sq < +distsq and transitivity of <=/< show that 0 < distsq. Definition of +dot should give us that t != 0.0, given that 0.0 * 0.0 == +0.0, and 0 +< +0.0 is false. + +Loose proof for the second unit_vector invocation: distsq != 0 from ?: +condition, which should give us that t != Point(0, 0) in the same way +as in the above proof. + +Proof of sp_darray_right_tangent(Point[], unsigned) preconditions: We +have the same preconditions, and pass the same arguments. d, *d and +len are const. + + + +src/extension/internal/ps.cpp:PrintPS::print_fill_style: + +Proof of the + g_return_if_fail( style->fill.type == SP_PAINT_TYPE_COLOR + || ( style->fill.type == SP_PAINT_TYPE_PAINTSERVER + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) ) ) +at beginning of function: + +rgrep print_fill_style reveals no callers in any other files. There are two calls in ps.cpp, both +inside an `if' test of that same condition (with no relevant lines between the test and the call). +Each call uses `style' as its second argument, and `style' in print_fill_style refers to its second +parameter. In both caller & callee, `style' is a const pointer to const, and there is very little +code between the two tests, so the relevant values are very unlikely to change between the two +tests. + + +Proof of + g_assert( style->fill.type == SP_PAINT_TYPE_PAINTSERVER + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) ) : + +The g_return_if_fail(style->fill.type == SP_PAINT_TYPE_COLOR + || ( style->fill.type == SP_PAINT_TYPE_PAINTSERVER + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) ) ) +call at the beginning of the function, and we're in the `else' branch of a test for +style->fill.type == SP_PAINT_TYPE_COLOR, and style is a const pointer to const, so it's likely that +style->fill and the gradient object have the same values throughout. + + + +src/extensions/internal/ps.cpp:PrintPS::fill: + +Proof of the two assertions + g_assert( style->fill.type == SP_PAINT_TYPE_PAINTSERVER + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) ) : + +Each is in the `else' branch of a test for `style->fill.type == SP_PAINT_TYPE_COLOR', +within a test for + ( style->fill.type == SP_PAINT_TYPE_COLOR + || ( style->fill.type == SP_PAINT_TYPE_PAINTSERVER + && SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) ) ). + +`style' is a const pointer to const, so the values are unlikely to have changed between the tests. + + + +src/seltrans.cpp:sp_sel_trans_update_handles + +Proof of requirements of sp_show_handles: + +sp_show_handles requirements: !arg1.empty. + +Before any call to sp_show_handles is a test `if (... || seltrans.empty) { ...; return; }' +(with no `break' etc. call preventing that `return'). +Each subsequent sp_show_handles call uses seltrans as arg1. +seltrans is a reference. There are no calls between that failing seltrans.empty test +and the sp_show_handles calls that pass seltrans. The sole call is sp_remove_handles, +probably doesn't have access to seltrans. + + + +src/seltrans.cpp:sp_show_handles + +Proof of precondition: + +sp_show_handles is static. Searching reveals calls only in sp_sel_trans_update_handles (proof above). + + + +src/sp-spiral.cpp:sp_spiral_fit_and_draw + +Proof of postcondition is_unit_vector(*hat2): + +hat2 is set by sp_spiral_get_tangent unconditionally, which Ensures is_unit_vector(*hat2). +We then negate *hat2, which doesn't affect its length. +We pass it only to sp_bezier_fit_cubic_full, which claims constness of *hat2. + +Proof of unconditionalness: Not inside if/for/while. No previous `return'. + + +src/sp-spiral.cpp:sp_spiral_set_shape + +Loose proof of requirements for sp_spiral_fit_and_draw: + + Proof of dstep > 0: + + SAMPLE_STEP equals .25. + spiral->revo is bounded to [0.05, 20.0] (and non-NaN) by various CLAMP calls. + (TODO: Add precondition, given that those CLAMP calls are outside of this function.) + SAMPLE_SIZE equals 8. + dstep is const and equals SAMPLE_STEP / spiral->revo / (SAMPLE_SIZE - 1), + == 1 / (4 * [.05, 20.0] * 7) + == 1 / [1.4, 560] + dstep in [.0018, .714]. + + Proof of is_unit_vector(hat1): + + Initially guaranteed by sp_spiral_get_tangent Ensures. + For subsequent calls, hat1 is set from negated hat2 as set by sp_spiral_fit_and_draw, + which Ensures is_unit_vector(hat2). + + + +src/style.cpp:sp_css_attr_from_style: + +Proof of sp_style_write_string pre `style != NULL': + +Passes style as style argument. style is const, and has already been checked against NULL. + + +src/style.cpp:sp_css_attr_from_object + +Proof of `flags in {IFSET, ALWAYS} precondition: + + $ grep sp_css_attr_from_object `sed 's,#.*,,' make.files ` + file.cpp: SPCSSAttr *style = sp_css_attr_from_object (SP_DOCUMENT_ROOT (doc)); + selection-chemistry.cpp: SPCSSAttr *css = sp_css_attr_from_object (SP_OBJECT(item), SP_STYLE_FLAG_ALWAYS); + selection-chemistry.cpp: SPCSSAttr *temp = sp_css_attr_from_object (last_element, SP_STYLE_FLAG_IFSET); + style.cpp:sp_css_attr_from_object (SPObject *object, guint flags) + style.h:SPCSSAttr *sp_css_attr_from_object(SPObject *object, guint flags = SP_STYLE_FLAG_IFSET); + + +src/style.cpp:sp_css_attr_from_style + +Proof of precondition `style != NULL': + +Callers are selection-chemistry.cpp and style.cpp: + + $ grep sp_css_attr_from_style `sed 's,#.*,,' make.files ` +selection-chemistry.cpp: SPCSSAttr *css = sp_css_attr_from_style (query, SP_STYLE_FLAG_ALWAYS); + style.cpp:sp_css_attr_from_style (SPStyle const *const style, guint flags) + style.cpp: return sp_css_attr_from_style (style, flags); + style.h:SPCSSAttr *sp_css_attr_from_style (SPStyle const *const style, guint flags); + +selection-chemistry.cpp caller: query is initialized from sp_style_new() +(which guarantees non-NULL), and is const. + +style.cpp caller: preceded by explicit test for NULL: + + $ grep -B2 sp_css_attr_from_style style.cpp|tail -3 + if (style == NULL) + return NULL; + return sp_css_attr_from_style (style, flags); + + + + +# Local Variables: +# mode:indented-text +# fill-column:99 +# End: +# vim: filetype=text:tabstop=8:fileencoding=utf-8:textwidth=99 : diff --git a/src/pure-transform.cpp b/src/pure-transform.cpp new file mode 100644 index 0000000..847bbdb --- /dev/null +++ b/src/pure-transform.cpp @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Class for pure transformations, such as translating, scaling, stretching, skewing, and rotating + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2015 Diederik van Lierop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pure-transform.h" +#include "snap.h" + +namespace Inkscape + +{ + +void PureTransform::snap(::SnapManager *sm, std::vector<Inkscape::SnapCandidatePoint> const &points, Geom::Point const &pointer) { + std::vector<Inkscape::SnapCandidatePoint> transformed_points; + Geom::Rect bbox; + + long source_num = 0; + for (std::vector<Inkscape::SnapCandidatePoint>::const_iterator i = points.begin(); i != points.end(); ++i) { + + /* Work out the transformed version of this point */ + Geom::Point transformed = getTransformedPoint(*i); // _transformPoint(*i, transformation_type, transformation, origin, dim, uniform); + + // add the current transformed point to the box hulling all transformed points + if (i == points.begin()) { + bbox = Geom::Rect(transformed, transformed); + } else { + bbox.expandTo(transformed); + } + + transformed_points.emplace_back(transformed, (*i).getSourceType(), source_num, Inkscape::SNAPTARGET_UNDEFINED, Geom::OptRect()); + source_num++; + } + + /* The current best metric for the best transformation; lower is better, whereas Geom::infinity() + ** means that we haven't snapped anything. + */ + Inkscape::SnapCandidatePoint best_original_point; + g_assert(best_snapped_point.getAlwaysSnap() == false); // Check initialization of snapped point + g_assert(best_snapped_point.getAtIntersection() == false); + g_assert(best_snapped_point.getSnapped() == false); // Check initialization to catch any regression + + std::vector<Inkscape::SnapCandidatePoint>::iterator j = transformed_points.begin(); + + // std::cout << std::endl; + bool first_free_snap = true; + + for (std::vector<Inkscape::SnapCandidatePoint>::const_iterator i = points.begin(); i != points.end(); ++i) { + + // If we have a collection of SnapCandidatePoints, with mixed constrained snapping and free snapping + // requirements (this can happen when scaling, see PureScale::snap()), then freeSnap might never see the + // SnapCandidatePoint with source_num == 0. The freeSnap() method in the object snapper depends on this, + // because only for source-num == 0 the target nodes will be collected. Therefore we enforce that the first + // SnapCandidatePoint that is to be freeSnapped always has source_num == 0; + // TODO: This is a bit ugly so fix this; do we need sourcenum for anything else? if we don't then get rid + // of it and explicitly communicate to the object snapper that this is a first point + if (first_free_snap) { + (*j).setSourceNum(0); + first_free_snap = false; + } + + Inkscape::SnappedPoint snapped_point = snap(sm, *j, (*i).getPoint(), bbox); // Calls the snap() method of the derived classes + + snapped_point.setPointerDistance(Geom::L2(pointer - (*i).getPoint())); + + /*Find the transformation that describes where the snapped point has + ** ended up, and also the metric for this transformation. + */ + + bool store_best_snap = false; + if (snapped_point.getSnapped()) { + // We snapped; keep track of the best snap + if (best_snapped_point.isOtherSnapBetter(snapped_point, true)) { + store_best_snap = true; + } + } else { + // So we didn't snap for this point + if (!best_snapped_point.getSnapped()) { + // ... and none of the points before snapped either + // We might still need to apply a constraint though, if we tried a constrained snap. And + // in case of a free snap we might have use for the transformed point, so let's return that + // point, whether it's constrained or not + + if (best_snapped_point.isOtherSnapBetter(snapped_point, true) ) { + // .. so we must keep track of the best non-snapped constrained point.. but what + // is the best? There is no best, or is there? We cannot compare on snapped distance + // because neither has snapped, and both have their snapped distance set to infinity. + // There might be a difference in "constrainedness" though, 1D vs 2D snapping + store_best_snap = true; + } + } + } + + if (store_best_snap || i == points.begin()) { + best_original_point = (*i); + best_snapped_point = snapped_point; // Can be a point that didn't snap, but then at least we + // return something meaningful; we might have use for the transformation. The default + // snapped_point, as initialized before this loop, is not very meaningful at all. + } + + ++j; + } + + /* The current best transformation */ + //Geom::Point best_transformation = getResult(best_original_point, best_snapped_point); + storeTransform(best_original_point, best_snapped_point); + + Geom::Coord best_metric = best_snapped_point.getSnapDistance(); + + // Using " < 1e6" instead of " < Geom::infinity()" for catching some rounding errors + // These rounding errors might be caused by NRRects, see bug #1584301 + best_snapped_point.setSnapDistance(best_metric < 1e6 ? best_metric : Geom::infinity()); +} + + + + + +Geom::Point PureTranslate::getTransformedPoint(SnapCandidatePoint const &p) const { + return p.getPoint() + _vector; +} + +void PureTranslate::storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) { + /* Consider the case in which a box is almost aligned with a grid in both + * horizontal and vertical directions. The distance to the intersection of + * the grid lines will always be larger then the distance to a single grid + * line. If we prefer snapping to an intersection over to a single + * grid line, then we cannot use "metric = Geom::L2(result)". Therefore the + * snapped distance will be used as a metric. Please note that the snapped + * distance to an intersection is defined as the distance to the nearest line + * of the intersection, and not to the intersection itself! + */ + // Only for translations, the relevant metric will be the real snapped distance, + // so we don't have to do anything special here + _vector_snapped = snapped_point.getPoint() - original_point.getPoint(); +} + +SnappedPoint PureTranslate::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point /*pt_orig*/, Geom::OptRect const &bbox_to_snap) const { + return sm->freeSnap(p, bbox_to_snap); +} + +SnappedPoint PureTranslateConstrained::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + // Calculate a constraint dedicated for this specific point + // When doing a constrained translation, all points will move in the same direction, i.e. + // either horizontally or vertically. The lines along which they move are therefore all + // parallel, but might not be co-linear. Therefore we will have to specify the point through + // which the constraint-line runs here, for each point individually. + Snapper::SnapConstraint dedicated_constraint = Snapper::SnapConstraint(pt_orig, _direction); + return sm->constrainedSnap(p, dedicated_constraint, bbox_to_snap); +} + + + + + +Geom::Point PureScale::getTransformedPoint(SnapCandidatePoint const &p) const { + return (p.getPoint() - _origin) * _scale + _origin; +} + +void PureScale::storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) { + _scale_snapped = Geom::Scale(Geom::infinity(), Geom::infinity()); + // If this point *i is horizontally or vertically aligned with + // the origin of the scaling, then it will scale purely in X or Y + // We can therefore only calculate the scaling in this direction + // and the scaling factor for the other direction should remain + // untouched (unless scaling is uniform of course) + Geom::Point const a = snapped_point.getPoint() - _origin; // vector to snapped point + Geom::Point const b = original_point.getPoint() - _origin; // vector to original point (not the transformed point!) + for (int index = 0; index < 2; index++) { + if (fabs(b[index]) > 1e-4) { // if SCALING CAN occur in this direction + if (fabs(fabs(a[index]/b[index]) - fabs(_scale[index])) > 1e-7) { // if SNAPPING DID occur in this direction + _scale_snapped[index] = a[index] / b[index]; // then calculate it! + // _scale_snapped will be (1,1) if we haven't snapped, because the snapped point equals the original point + } + // we might have left result[1-index] = Geom::infinity() if scaling didn't occur in the other direction + } + } + + if (_scale_snapped == Geom::Scale(Geom::infinity(), Geom::infinity())) { + // This point must have been at the origin, so we cannot possibly snap; it won't scale (i.e. won't move while dragging) + snapped_point.setSnapDistance(Geom::infinity()); + snapped_point.setSecondSnapDistance(Geom::infinity()); + return; + } + + if (_uniform) { + // Lock the scaling the be uniform, but keep the sign such that we don't change which quadrant we have dragged into + if (fabs(_scale_snapped[0]) < fabs(_scale_snapped[1])) { + _scale_snapped[1] = fabs(_scale_snapped[0]) * Geom::sgn(_scale[1]); + } else { + _scale_snapped[0] = fabs(_scale_snapped[1]) * Geom::sgn(_scale[0]); + } + } + + // Don't ever exit with one of scaling components uninitialized + for (int index = 0; index < 2; index++) { + if (_scale_snapped[index] == Geom::infinity()) { + _scale_snapped[index] = _scale[index]; + } + } + + // Compare the resulting scaling with the desired scaling + Geom::Point scale_metric = _scale_snapped.vector() - _scale.vector(); + snapped_point.setSnapDistance(Geom::L2(scale_metric)); + snapped_point.setSecondSnapDistance(Geom::infinity()); +} + +// When scaling, a point aligned either horizontally or vertically with the origin can only +// move in that specific direction; therefore it should only snap in that direction, so this +// then becomes a constrained snap; otherwise we can use a free snap; +SnappedPoint PureScale::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + Geom::Point const b = (pt_orig - _origin); // vector to original point (not the transformed point!) + bool const c1 = fabs(b[Geom::X]) < 1e-6; + bool const c2 = fabs(b[Geom::Y]) < 1e-6; + if ((c1 || c2) && !(c1 && c2)) { + Geom::Point cvec; cvec[c1] = 1.; + Snapper::SnapConstraint dedicated_constraint = Inkscape::Snapper::SnapConstraint(_origin, cvec); + return sm->constrainedSnap(p, dedicated_constraint, bbox_to_snap); + } else { + return sm->freeSnap(p, bbox_to_snap); + } +} + +SnappedPoint PureScaleConstrained::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + // When constrained scaling, only uniform scaling is supported. + // When uniformly scaling, each point will have its own unique constraint line, + // running from the scaling origin to the original untransformed point. We will + // calculate that line here as a dedicated constraint + Geom::Point b = pt_orig - _origin; + Snapper::SnapConstraint dedicated_constraint = Inkscape::Snapper::SnapConstraint(_origin, b); + return sm->constrainedSnap(p, dedicated_constraint, bbox_to_snap); +} + + + + + +Geom::Point PureStretchConstrained::getTransformedPoint(SnapCandidatePoint const &p) const { + Geom::Scale s(1, 1); + if (_uniform) + s[Geom::X] = s[Geom::Y] = _magnitude; + else { + s[_direction] = _magnitude; + s[1 - _direction] = 1; + } + return ((p.getPoint() - _origin) * s) + _origin; +} + +SnappedPoint PureStretchConstrained::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + Snapper::SnapConstraint dedicated_constraint; + if (_uniform) { + // When uniformly stretching, each point will have its own unique constraint line, + // running from the scaling origin to the original untransformed point. We will + // calculate that line here + Geom::Point b = pt_orig - _origin; + dedicated_constraint = Inkscape::Snapper::SnapConstraint(_origin, b); // dedicated constraint + } else { + Geom::Point cvec; cvec[_direction] = 1.; + dedicated_constraint = Inkscape::Snapper::SnapConstraint(pt_orig, cvec); + } + + return sm->constrainedSnap(p, dedicated_constraint, bbox_to_snap); +} + +void PureStretchConstrained::storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) { + Geom::Point const a = snapped_point.getPoint() - _origin; // vector to snapped point + Geom::Point const b = original_point.getPoint() - _origin; // vector to original point (not the transformed point!) + + _stretch_snapped = Geom::Scale(Geom::infinity(), Geom::infinity()); + if (fabs(b[_direction]) > 1e-4) { // if STRETCHING will occur for this point + _stretch_snapped[_direction] = a[_direction] / b[_direction]; + _stretch_snapped[1-_direction] = _uniform ? _stretch_snapped[_direction] : 1; + } else { // STRETCHING might occur for this point, but only when the stretching is uniform + if (_uniform && fabs(b[1-_direction]) > 1e-4) { + _stretch_snapped[1-_direction] = a[1-_direction] / b[1-_direction]; + _stretch_snapped[_direction] = _stretch_snapped[1-_direction]; + } + } + + // _stretch_snapped might have one or both components at infinity! + + // Store the metric for this transformation as a virtual distance + snapped_point.setSnapDistance(std::abs(_stretch_snapped[_direction] - _magnitude)); + snapped_point.setSecondSnapDistance(Geom::infinity()); +} + + + + + +Geom::Point PureSkewConstrained::getTransformedPoint(SnapCandidatePoint const &p) const { + Geom::Point transformed; + // Apply the skew factor + transformed[_direction] = (p.getPoint())[_direction] + _skew * ((p.getPoint())[1 - _direction] - _origin[1 - _direction]); + // While skewing, mirroring and scaling (by integer multiples) in the opposite direction is also allowed. + // Apply that scale factor here + transformed[1-_direction] = (p.getPoint() - _origin)[1 - _direction] * _scale + _origin[1 - _direction]; + return transformed; +} + +SnappedPoint PureSkewConstrained::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + // Snapping the nodes of the bounding box of a selection that is being transformed, will only work if + // the transformation of the bounding box is equal to the transformation of the individual nodes. This is + // NOT the case for example when rotating or skewing. The bounding box itself cannot possibly rotate or skew, + // so it's corners have a different transformation. The snappers cannot handle this, therefore snapping + // of bounding boxes is not allowed here. + g_assert(!(p.getSourceType() & Inkscape::SNAPSOURCE_BBOX_CATEGORY)); + + Geom::Point constraint_vector; + constraint_vector[1-_direction] = 0.0; + constraint_vector[_direction] = 1.0; + + return sm->constrainedSnap(p, Inkscape::Snapper::SnapConstraint(constraint_vector), bbox_to_snap); +} + +void PureSkewConstrained::storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) { + Geom::Point const b = original_point.getPoint() - _origin; // vector to original point (not the transformed point!) + _skew_snapped = (snapped_point.getPoint()[_direction] - (original_point.getPoint())[_direction]) / b[1 - _direction]; // skew factor + + // Store the metric for this transformation as a virtual distance + snapped_point.setSnapDistance(std::abs(_skew_snapped - _skew)); + snapped_point.setSecondSnapDistance(Geom::infinity()); +} + + + + +Geom::Point PureRotateConstrained::getTransformedPoint(SnapCandidatePoint const &p) const { + return (p.getPoint() - _origin) * Geom::Rotate(_angle) + _origin; +} + +SnappedPoint PureRotateConstrained::snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const { + // Snapping the nodes of the bounding box of a selection that is being transformed, will only work if + // the transformation of the bounding box is equal to the transformation of the individual nodes. This is + // NOT the case for example when rotating or skewing. The bounding box itself cannot possibly rotate or skew, + // so it's corners have a different transformation. The snappers cannot handle this, therefore snapping + // of bounding boxes is not allowed here. + g_assert(!(p.getSourceType() & Inkscape::SNAPSOURCE_BBOX_CATEGORY)); + + // Calculate a constraint dedicated for this specific point + Geom::Point b = pt_orig - _origin; + Geom::Coord r = Geom::L2(b); // the radius of the circular constraint + Snapper::SnapConstraint dedicated_constraint = Inkscape::Snapper::SnapConstraint(_origin, b, r); + return sm->constrainedSnap(p, dedicated_constraint, bbox_to_snap); +} + +void PureRotateConstrained::storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) { + Geom::Point const a = snapped_point.getPoint() - _origin; // vector to snapped point + Geom::Point const b = (original_point.getPoint() - _origin); // vector to original point (not the transformed point!) + // a is vector to snapped point; b is vector to original point; now lets calculate angle between a and b + _angle_snapped = atan2(Geom::dot(Geom::rot90(b), a), Geom::dot(b, a)); + if (Geom::L2(b) < 1e-9) { // points too close to the rotation center will not move. Don't try to snap these + // as they will always yield a perfect snap result if they're already snapped beforehand (e.g. + // when the transformation center has been snapped to a grid intersection in the selector tool) + snapped_point.setSnapDistance(Geom::infinity()); + // PS1: Apparently we don't have to do this for skewing, but why? + // PS2: We cannot easily filter these points upstream, e.g. in the grab() method (seltrans.cpp) + // because the rotation center will change when pressing shift, and grab() won't be recalled. + // Filtering could be done in handleRequest() (again in seltrans.cpp), by iterating through + // the snap candidates. But hey, we're iterating here anyway. + } else { + snapped_point.setSnapDistance(fabs(_angle_snapped - _angle)); + } + snapped_point.setSecondSnapDistance(Geom::infinity()); + +} + +} diff --git a/src/pure-transform.h b/src/pure-transform.h new file mode 100644 index 0000000..6c5c7b5 --- /dev/null +++ b/src/pure-transform.h @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Class for pure transformations, such as translating, scaling, stretching, skewing, and rotating. Pure means that they + * cannot be combined. This is what makes them different from affine transformations. Pure transformations are being + * used in the selector tool and node tool + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2015 Diederik van Lierop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PURE_TRANSFORM_H +#define SEEN_PURE_TRANSFORM_H + +#include <glib.h> // for g_warning +#include "snapper.h" // for SnapConstraint + +class SnapManager; + +namespace Inkscape { + +class PureTransform { + +protected: + virtual SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const = 0; + virtual Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const = 0; + virtual void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) = 0; + +public: + //PureTransform(); + virtual ~PureTransform() = default;; +// virtual PureTransform * clone () const = 0; // https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Virtual_Constructor + + // Snap a group of points + SnappedPoint best_snapped_point; + void snap(::SnapManager *sm, std::vector<Inkscape::SnapCandidatePoint> const &points, Geom::Point const &pointer); +}; + +// ************************************************************************************************************** + +class PureTranslate: public PureTransform { + +protected: + Geom::Point _vector; + Geom::Point _vector_snapped; + + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const override; + void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) override; + +public: +// PureTranslate(); // Default constructor +// PureTranslate(PureTranslate const &); // Copy constructor + ~PureTranslate() override = default;; + PureTranslate(Geom::Point vector = Geom::Point()) : _vector(vector), _vector_snapped(vector) {} + + Geom::Point getTranslationSnapped() {return _vector_snapped;} +// PureTranslate * clone () const {return new PureTranslate(*this);} +}; + + +class PureTranslateConstrained: public PureTranslate { + +protected: + Geom::Dim2 _direction; + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + +public: + ~PureTranslateConstrained() override = default;; + PureTranslateConstrained(Geom::Coord displacement, Geom::Dim2 direction): + PureTranslate() { + _vector[direction] = displacement; + _vector[1-direction] = 0.0; + _direction = direction; + } + // PureTranslateConstrained * clone () const {return new PureTranslateConstrained(*this);} +}; + +// ************************************************************************************************************** + +class PureScale: public PureTransform { + +protected: + Geom::Scale _scale; + Geom::Scale _scale_snapped; + Geom::Point _origin; + bool _uniform; + + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const override; + void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) override; + +public: +// PureScale(); // Default constructor +// PureScale(PureScale const &); // Copy constructor + ~PureScale() override = default;; + + PureScale(Geom::Scale scale, Geom::Point origin, bool uniform) : + _scale (scale), + _scale_snapped (scale), + _origin (origin), + _uniform (uniform) + {} + + Geom::Scale getScaleSnapped() {return _scale_snapped;} +// PureScale * clone () const {return new PureScale (*this);} +}; + +class PureScaleConstrained: public PureScale { +//Magnitude of the scale components will be the same, but the sign could still be different () +protected: + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + +public: + ~PureScaleConstrained() override = default;; + PureScaleConstrained(Geom::Scale scale, Geom::Point origin): + PureScale(scale, origin, true) {}; // Non-uniform constrained scaling is not supported + +// PureScaleConstrained * clone () const {return new PureScaleConstrained(*this);} +}; + +// ************************************************************************************************************** + +class PureStretchConstrained: public PureTransform { +// A stretch is always implicitly constrained + +protected: + Geom::Coord _magnitude; + Geom::Scale _stretch_snapped; + Geom::Point _origin; + Geom::Dim2 _direction; + bool _uniform; + + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const override; + void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) override; + +public: + ~PureStretchConstrained() override = default;; + PureStretchConstrained(Geom::Coord magnitude, Geom::Point origin, Geom::Dim2 direction, bool uniform) : + _magnitude (magnitude), + _stretch_snapped (Geom::Scale(magnitude, magnitude)), + _origin (origin), + _direction (direction), + _uniform (uniform) + { + if (not uniform) { + _stretch_snapped[1-direction] = 1.0; + } + } + + Geom::Scale getStretchSnapped() {return _stretch_snapped;} + Geom::Coord getMagnitude() {return _magnitude;} + Geom::Coord getMagnitudeSnapped() {return _stretch_snapped[_direction];} + +// PureStretchConstrained * clone () const {return new PureStretchConstrained(*this);} +}; + +// ************************************************************************************************************** + +class PureSkewConstrained: public PureTransform { +// A skew is always implicitly constrained + +protected: + Geom::Coord _skew; + Geom::Coord _skew_snapped; + Geom::Coord _scale; + Geom::Point _origin; + Geom::Dim2 _direction; + + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const override; + void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) override; + +public: + ~PureSkewConstrained() override = default;; + PureSkewConstrained(Geom::Coord skew, Geom::Coord scale, Geom::Point origin, Geom::Dim2 direction) : + _skew (skew), + _skew_snapped (skew), + _scale (scale), + _origin (origin), + _direction (direction) + {}; + + Geom::Coord getSkewSnapped() {return _skew_snapped;} + +// PureSkewConstrained * clone () const {return new PureSkewConstrained(*this);} +}; + +// ************************************************************************************************************** + +class PureRotateConstrained: public PureTransform { +// A rotation is always implicitly constrained, so we will hide the constructor by making it protected; devs should use PureRotateConstrained instead +// It's _constraint member variable though will be empty + +protected: + double _angle; // in radians + double _angle_snapped; + Geom::Point _origin; + bool _uniform; + + SnappedPoint snap(::SnapManager *sm, SnapCandidatePoint const &p, Geom::Point pt_orig, Geom::OptRect const &bbox_to_snap) const override; + Geom::Point getTransformedPoint(SnapCandidatePoint const &p) const override; + void storeTransform(SnapCandidatePoint const &original_point, SnappedPoint &snapped_point) override; + +public: +// PureRotate(); // Default constructor +// PureRotate(PureRotate const &); // Copy constructor + ~PureRotateConstrained() override = default;; + + PureRotateConstrained(double angle, Geom::Point origin) : + _angle (angle), // in radians! + _angle_snapped (angle), + _origin (origin), + _uniform (true) // We do not yet allow for simultaneous rotation and scaling + {} + + double getAngleSnapped() {return _angle_snapped;} + +// PureRotate * clone () const {return new PureRotate(*this);} +}; + +} + +#endif // !SEEN_PURE_TRANSFORM_H + diff --git a/src/rdf.cpp b/src/rdf.cpp new file mode 100644 index 0000000..86864cb --- /dev/null +++ b/src/rdf.cpp @@ -0,0 +1,1258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * RDF manipulation functions. + * + * @todo move these to xml/ instead of dialogs/ + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * Jon Phillips <jon@rejon.org> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004 Kees Cook <kees@outflux.net> + * Copyright (C) 2006 Jon Phillips <jon@rejon.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "inkscape.h" +#include "preferences.h" +#include "rdf.h" + +#include "object/sp-item-group.h" +#include "object/sp-root.h" + +#include "xml/repr.h" + +/* + Example RDF XML from various places... + +<rdf:RDF xmlns="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<Work rdf:about=""> + <dc:title>title of work</dc:title> + <dc:date>year</dc:date> + <dc:description>description of work</dc:description> + <dc:creator><Agent> + <dc:title>creator</dc:title> + </Agent></dc:creator> + <dc:rights><Agent> + <dc:title>holder</dc:title> + </Agent></dc:rights> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:source rdf:resource="source"/> + <license rdf:resource="http://creativecommons.org/licenses/by/4.0/" +/> +</Work> + + + <rdf:RDF xmlns="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <Work rdf:about=""> + <dc:title>SVG Road Signs</dc:title> + <dc:rights><Agent> + <dc:title>John Cliff</dc:title> + </Agent></dc:rights> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <license rdf:resource="http://creativecommons.org/ns#PublicDomain" /> + </Work> + + <License rdf:about="http://creativecommons.org/ns#PublicDomain"> + <permits rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <permits rdf:resource="http://creativecommons.org/ns#Distribution" /> + <permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + </License> + +</rdf:RDF> + + +Bag example: + +<dc:subject> +<rdf:Bag> +<rdf:li>open clip art logo</rdf:li> +<rdf:li>images</rdf:li> +<rdf:li>logo</rdf:li> +<rdf:li>clip art</rdf:li> +<rdf:li>ocal</rdf:li> +<rdf:li>logotype</rdf:li> +<rdf:li>filetype</rdf:li> +</rdf:Bag> +</dc:subject> +*/ + +struct rdf_double_t rdf_license_empty [] = { + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a_sa [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { "cc:requires", "http://creativecommons.org/ns#ShareAlike", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a_nd [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a_nc [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { "cc:prohibits", "http://creativecommons.org/ns#CommercialUse", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a_nc_sa [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { "cc:prohibits", "http://creativecommons.org/ns#CommercialUse", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { "cc:requires", "http://creativecommons.org/ns#ShareAlike", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_cc_a_nc_nd [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { "cc:prohibits", "http://creativecommons.org/ns#CommercialUse", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_pd [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_freeart [] = { + { "cc:permits", "http://creativecommons.org/ns#Reproduction", }, + { "cc:permits", "http://creativecommons.org/ns#Distribution", }, + { "cc:permits", "http://creativecommons.org/ns#DerivativeWorks", }, + { "cc:requires", "http://creativecommons.org/ns#ShareAlike", }, + { "cc:requires", "http://creativecommons.org/ns#Notice", }, + { "cc:requires", "http://creativecommons.org/ns#Attribution", }, + { nullptr, nullptr } +}; + +struct rdf_double_t rdf_license_ofl [] = { + { "cc:permits", "http://scripts.sil.org/pub/OFL/Reproduction", }, + { "cc:permits", "http://scripts.sil.org/pub/OFL/Distribution", }, + { "cc:permits", "http://scripts.sil.org/pub/OFL/Embedding", }, + { "cc:permits", "http://scripts.sil.org/pub/OFL/DerivativeWorks", }, + { "cc:requires", "http://scripts.sil.org/pub/OFL/Notice", }, + { "cc:requires", "http://scripts.sil.org/pub/OFL/Attribution", }, + { "cc:requires", "http://scripts.sil.org/pub/OFL/ShareAlike", }, + { "cc:requires", "http://scripts.sil.org/pub/OFL/DerivativeRenaming", }, + { "cc:requires", "http://scripts.sil.org/pub/OFL/BundlingWhenSelling", }, + { nullptr, nullptr } +}; + +struct rdf_license_t rdf_licenses [] = { + { N_("CC Attribution"), + "http://creativecommons.org/licenses/by/4.0/", + rdf_license_cc_a, + }, + + { N_("CC Attribution-ShareAlike"), + "http://creativecommons.org/licenses/by-sa/4.0/", + rdf_license_cc_a_sa, + }, + + { N_("CC Attribution-NoDerivs"), + "http://creativecommons.org/licenses/by-nd/4.0/", + rdf_license_cc_a_nd, + }, + + { N_("CC Attribution-NonCommercial"), + "http://creativecommons.org/licenses/by-nc/4.0/", + rdf_license_cc_a_nc, + }, + + { N_("CC Attribution-NonCommercial-ShareAlike"), + "http://creativecommons.org/licenses/by-nc-sa/4.0/", + rdf_license_cc_a_nc_sa, + }, + + { N_("CC Attribution-NonCommercial-NoDerivs"), + "http://creativecommons.org/licenses/by-nc-nd/4.0/", + rdf_license_cc_a_nc_nd, + }, + + { N_("CC0 Public Domain Dedication"), + "http://creativecommons.org/publicdomain/zero/1.0/", + rdf_license_pd, + }, + + { N_("FreeArt"), + "http://artlibre.org/licence/lal", + rdf_license_freeart, + }, + + { N_("Open Font License"), + "http://scripts.sil.org/OFL", + rdf_license_ofl, + }, + + { nullptr, nullptr, rdf_license_empty, } +}; + +#define XML_TAG_NAME_SVG "svg:svg" +#define XML_TAG_NAME_METADATA "svg:metadata" +#define XML_TAG_NAME_RDF "rdf:RDF" +#define XML_TAG_NAME_WORK "cc:Work" +#define XML_TAG_NAME_LICENSE "cc:License" +// Note the lowercase L! +#define XML_TAG_NAME_LICENSE_PROP "cc:license" + + +// Remember when using the "title" and "tip" elements to pass them through +// the localization functions when you use them! +struct rdf_work_entity_t rdf_work_entities [] = { + { "title", N_("Title:"), "dc:title", RDF_CONTENT, + N_("A name given to the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "date", N_("Date:"), "dc:date", RDF_CONTENT, + N_("A point or period of time associated with an event in the lifecycle of the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "format", N_("Format:"), "dc:format", RDF_CONTENT, + N_("The file format, physical medium, or dimensions of the resource"), RDF_FORMAT_LINE, RDF_EDIT_HARDCODED, + }, + { "type", N_("Type:"), "dc:type", RDF_RESOURCE, + N_("The nature or genre of the resource"), RDF_FORMAT_LINE, RDF_EDIT_HARDCODED, + }, + + { "creator", N_("Creator:"), "dc:creator", RDF_AGENT, + N_("An entity primarily responsible for making the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "rights", N_("Rights:"), "dc:rights", RDF_AGENT, + N_("Information about rights held in and over the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "publisher", N_("Publisher:"), "dc:publisher", RDF_AGENT, + N_("An entity responsible for making the resource available"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + + { "identifier", N_("Identifier:"), "dc:identifier", RDF_CONTENT, + N_("An unambiguous reference to the resource within a given context"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "source", N_("Source:"), "dc:source", RDF_CONTENT, + N_("A related resource from which the described resource is derived"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "relation", N_("Relation:"), "dc:relation", RDF_CONTENT, + N_("A related resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "language", N_("Language:"), "dc:language", RDF_CONTENT, + N_("A language of the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + { "subject", N_("Keywords:"), "dc:subject", RDF_BAG, + N_("The topic of the resource"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + // TRANSLATORS: "Coverage": the spatial or temporal characteristics of the content. + // For info, see Appendix D of http://www.w3.org/TR/1998/WD-rdf-schema-19980409/ + { "coverage", N_("Coverage:"), "dc:coverage", RDF_CONTENT, + N_("The spatial or temporal topic of the resource, the spatial applicability of the resource, or the jurisdiction under which the resource is relevant"), RDF_FORMAT_LINE, RDF_EDIT_GENERIC, + }, + + { "description", N_("Description:"), "dc:description", RDF_CONTENT, + N_("An account of the resource"), RDF_FORMAT_MULTILINE, RDF_EDIT_GENERIC, + }, + + // FIXME: need to handle 1 agent per line of input + { "contributor", N_("Contributors:"), "dc:contributor", RDF_AGENT, + N_("An entity responsible for making contributions to the resource"), RDF_FORMAT_MULTILINE, RDF_EDIT_GENERIC, + }, + + // TRANSLATORS: URL to a page that defines the license for the document + { "license_uri", N_("URI:"), "cc:license", RDF_RESOURCE, + // TRANSLATORS: this is where you put a URL to a page that defines the license + N_("URI to this document's license's namespace definition"), RDF_FORMAT_LINE, RDF_EDIT_SPECIAL, + }, + + // TRANSLATORS: fragment of XML representing the license of the document + { "license_fragment", N_("Fragment:"), "License", RDF_XML, + N_("XML fragment for the RDF 'License' section"), RDF_FORMAT_MULTILINE, RDF_EDIT_SPECIAL, + }, + + { nullptr, nullptr, nullptr, RDF_CONTENT, + nullptr, RDF_FORMAT_LINE, RDF_EDIT_HARDCODED, + } +}; + + +// Simple start of C++-ification: +class RDFImpl +{ +public: + /** + * Some implementations do not put RDF stuff inside <metadata>, + * so we need to check for it and add it if we don't see it. + */ + static void ensureParentIsMetadata( SPDocument *doc, Inkscape::XML::Node *node ); + + static Inkscape::XML::Node const *getRdfRootRepr( SPDocument const * doc ); + static Inkscape::XML::Node *ensureRdfRootRepr( SPDocument * doc ); + + static Inkscape::XML::Node const *getXmlRepr( SPDocument const * doc, gchar const * name ); + static Inkscape::XML::Node *getXmlRepr( SPDocument * doc, gchar const * name ); + static Inkscape::XML::Node *ensureXmlRepr( SPDocument * doc, gchar const * name ); + + static Inkscape::XML::Node const *getWorkRepr( SPDocument const * doc, gchar const * name ); + static Inkscape::XML::Node *ensureWorkRepr( SPDocument * doc, gchar const * name ); + + static const gchar *getWorkEntity(SPDocument const * doc, struct rdf_work_entity_t & entity); + static unsigned int setWorkEntity(SPDocument * doc, struct rdf_work_entity_t & entity, gchar const * text); + + static void setDefaults( SPDocument * doc ); + + /** + * Pull the text out of an RDF entity, depends on how it's stored. + * + * @return A pointer to the entity's static contents as a string + * @param repr The XML element to extract from + * @param entity The desired RDF/Work entity + * + */ + static const gchar *getReprText( Inkscape::XML::Node const * repr, struct rdf_work_entity_t const & entity ); + + static unsigned int setReprText( Inkscape::XML::Node * repr, + struct rdf_work_entity_t const & entity, + gchar const * text ); + + static struct rdf_license_t *getLicense(SPDocument *document); + + static void setLicense(SPDocument * doc, struct rdf_license_t const * license); +}; + +/** + * Retrieves a known RDF/Work entity by name. + * + * @return A pointer to an RDF/Work entity + * @param name The desired RDF/Work entity + * + */ +struct rdf_work_entity_t *rdf_find_entity(gchar const * name) +{ + struct rdf_work_entity_t *entity; + for (entity=rdf_work_entities; entity->name; entity++) { + if (strcmp(entity->name,name)==0) break; + } + if (entity->name) return entity; + return nullptr; +} + +/* + * Takes the inkscape rdf struct and spits out a static RDF, which is only + * useful for testing. We must merge the rdf struct into the XML DOM for + * changes to be saved. + */ +/* + + Since g_markup_printf_escaped doesn't exist for most people's glib + right now, this function will remain commented out since it's only + for generic debug anyway. --Kees + +gchar * +rdf_string(struct rdf_t * rdf) +{ + gulong overall=0; + gchar *string=NULL; + + gchar *rdf_head="\ +<rdf:RDF xmlns=\"http://creativecommons.org/ns#\"\ + xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\ + xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\ +"; + gchar *work_head="\ +<Work rdf:about=\"\">\ + <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />\ +"; + gchar *work_title=NULL; + gchar *work_date=NULL; + gchar *work_description=NULL; + gchar *work_creator=NULL; + gchar *work_owner=NULL; + gchar *work_source=NULL; + gchar *work_license=NULL; + gchar *license_head=NULL; + gchar *license=NULL; + gchar *license_end="</License>\n"; + gchar *work_end="</Work>\n"; + gchar *rdf_end="</rdf:RDF>\n"; + + if (rdf && rdf->work_title && rdf->work_title[0]) { + work_title=g_markup_printf_escaped(" <dc:title>%s</dc:title>\n", + rdf->work_title); + overall+=strlen(work_title); + } + if (rdf && rdf->work_date && rdf->work_date[0]) { + work_date=g_markup_printf_escaped(" <dc:date>%s</dc:date>\n", + rdf->work_date); + overall+=strlen(work_date); + } + if (rdf && rdf->work_description && rdf->work_description[0]) { + work_description=g_markup_printf_escaped(" <dc:description>%s</dc:description>\n", + rdf->work_description); + overall+=strlen(work_description); + } + if (rdf && rdf->work_creator && rdf->work_creator[0]) { + work_creator=g_markup_printf_escaped(" <dc:creator><Agent>\ + <dc:title>%s</dc:title>\ + </Agent></dc:creator>\n", + rdf->work_creator); + overall+=strlen(work_creator); + } + if (rdf && rdf->work_owner && rdf->work_owner[0]) { + work_owner=g_markup_printf_escaped(" <dc:rights><Agent>\ + <dc:title>%s</dc:title>\ + </Agent></dc:rights>\n", + rdf->work_owner); + overall+=strlen(work_owner); + } + if (rdf && rdf->work_source && rdf->work_source[0]) { + work_source=g_markup_printf_escaped(" <dc:source rdf:resource=\"%s\" />\n", + rdf->work_source); + overall+=strlen(work_source); + } + if (rdf && rdf->license && rdf->license->work_rdf && rdf->license->work_rdf[0]) { + work_license=g_markup_printf_escaped(" <license rdf:resource=\"%s\" />\n", + rdf->license->work_rdf); + overall+=strlen(work_license); + + license_head=g_markup_printf_escaped("<License rdf:about=\"%s\">\n", + rdf->license->work_rdf); + overall+=strlen(license_head); + overall+=strlen(rdf->license->license_rdf); + overall+=strlen(license_end); + } + + overall+=strlen(rdf_head)+strlen(rdf_end); + overall+=strlen(work_head)+strlen(work_end); + + overall++; // NULL term + + if (!(string=(gchar*)g_malloc(overall))) { + return NULL; + } + + string[0]='\0'; + strcat(string,rdf_head); + strcat(string,work_head); + + if (work_title) strcat(string,work_title); + if (work_date) strcat(string,work_date); + if (work_description) strcat(string,work_description); + if (work_creator) strcat(string,work_creator); + if (work_owner) strcat(string,work_owner); + if (work_source) strcat(string,work_source); + if (work_license) strcat(string,work_license); + + strcat(string,work_end); + if (license_head) { + strcat(string,license_head); + strcat(string,rdf->license->license_rdf); + strcat(string,license_end); + } + strcat(string,rdf_end); + + return string; +} +*/ + + +const gchar *RDFImpl::getReprText( Inkscape::XML::Node const * repr, struct rdf_work_entity_t const & entity ) +{ + g_return_val_if_fail (repr != nullptr, NULL); + static gchar * bag = nullptr; + gchar * holder = nullptr; + + Inkscape::XML::Node const * temp = nullptr; + switch (entity.datatype) { + case RDF_CONTENT: + temp = repr->firstChild(); + if ( temp == nullptr ) return nullptr; + + return temp->content(); + + case RDF_AGENT: + temp = sp_repr_lookup_name ( repr, "cc:Agent", 1 ); + if ( temp == nullptr ) return nullptr; + + temp = sp_repr_lookup_name ( temp, "dc:title", 1 ); + if ( temp == nullptr ) return nullptr; + + temp = temp->firstChild(); + if ( temp == nullptr ) return nullptr; + + return temp->content(); + + case RDF_RESOURCE: + return repr->attribute("rdf:resource"); + + case RDF_XML: + return "xml goes here"; + + case RDF_BAG: + /* clear the static string. yucky. */ + if (bag) g_free(bag); + bag = nullptr; + + temp = sp_repr_lookup_name ( repr, "rdf:Bag", 1 ); + if ( temp == nullptr ) { + /* backwards compatible: read contents */ + temp = repr->firstChild(); + if ( temp == nullptr ) return nullptr; + + return temp->content(); + } + + for ( temp = temp->firstChild() ; + temp ; + temp = temp->next() ) { + if (!strcmp(temp->name(),"rdf:li") && + temp->firstChild()) { + const gchar * str = temp->firstChild()->content(); + if (bag) { + holder = bag; + bag = g_strconcat(holder, ", ", str, NULL); + g_free(holder); + } + else { + bag = g_strdup(str); + } + } + } + return bag; + + default: + break; + } + return nullptr; +} + +unsigned int RDFImpl::setReprText( Inkscape::XML::Node * repr, + struct rdf_work_entity_t const & entity, + gchar const * text ) +{ + g_return_val_if_fail ( repr != nullptr, 0); + g_return_val_if_fail ( text != nullptr, 0); + gchar * str = nullptr; + gchar** strlist = nullptr; + int i; + + Inkscape::XML::Node * temp=nullptr; + Inkscape::XML::Node * parent=repr; + + Inkscape::XML::Document * xmldoc = parent->document(); + g_return_val_if_fail (xmldoc != nullptr, FALSE); + + // set document's title element to the RDF title + if (!strcmp(entity.name, "title")) { + SPDocument *doc = SP_ACTIVE_DOCUMENT; + if (doc && doc->getRoot()) { + doc->getRoot()->setTitle(text); + } + } + + switch (entity.datatype) { + case RDF_CONTENT: + temp = parent->firstChild(); + if ( temp == nullptr ) { + temp = xmldoc->createTextNode( text ); + g_return_val_if_fail (temp != nullptr, FALSE); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + + return TRUE; + } + else { + temp->setContent(text); + return TRUE; + } + + case RDF_AGENT: + temp = sp_repr_lookup_name ( parent, "cc:Agent", 1 ); + if ( temp == nullptr ) { + temp = xmldoc->createElement ( "cc:Agent" ); + g_return_val_if_fail (temp != nullptr, FALSE); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + } + parent = temp; + + temp = sp_repr_lookup_name ( parent, "dc:title", 1 ); + if ( temp == nullptr ) { + temp = xmldoc->createElement ( "dc:title" ); + g_return_val_if_fail (temp != nullptr, FALSE); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + } + parent = temp; + + temp = parent->firstChild(); + if ( temp == nullptr ) { + temp = xmldoc->createTextNode( text ); + g_return_val_if_fail (temp != nullptr, FALSE); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + + return TRUE; + } + else { + temp->setContent(text); + return TRUE; + } + + case RDF_RESOURCE: + parent->setAttribute("rdf:resource", text ); + return true; + + case RDF_XML: + return 1; + + case RDF_BAG: + /* find/create the rdf:Bag item */ + temp = sp_repr_lookup_name ( parent, "rdf:Bag", 1 ); + if ( temp == nullptr ) { + /* backward compatibility: drop the dc:subject contents */ + while ( (temp = parent->firstChild()) ) { + parent->removeChild(temp); + } + + temp = xmldoc->createElement ( "rdf:Bag" ); + g_return_val_if_fail (temp != nullptr, FALSE); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + } + parent = temp; + + /* toss all the old list items */ + while ( (temp = parent->firstChild()) ) { + parent->removeChild(temp); + } + + /* chop our list up on commas */ + strlist = g_strsplit( text, ",", 0); + + for (i = 0; (str = strlist[i]); i++) { + temp = xmldoc->createElement ( "rdf:li" ); + g_return_val_if_fail (temp != nullptr, 0); + + parent->appendChild(temp); + Inkscape::GC::release(temp); + + Inkscape::XML::Node * child = xmldoc->createTextNode( g_strstrip(str) ); + g_return_val_if_fail (child != nullptr, 0); + + temp->appendChild(child); + Inkscape::GC::release(child); + } + g_strfreev( strlist ); + + return 1; + + default: + break; + } + return 0; +} + +void RDFImpl::ensureParentIsMetadata( SPDocument *doc, Inkscape::XML::Node *node ) +{ + if ( !node ) { + g_critical("Null node passed to ensureParentIsMetadata()."); + } else if ( !node->parent() ) { + g_critical( "No parent node when verifying <metadata> placement." ); + } else { + Inkscape::XML::Node * currentParent = node->parent(); + if ( strcmp( currentParent->name(), XML_TAG_NAME_METADATA ) != 0 ) { + Inkscape::XML::Node * metadata = doc->getReprDoc()->createElement( XML_TAG_NAME_METADATA ); + if ( !metadata ) { + g_critical("Unable to create metadata element."); + } else { + // attach the metadata node + currentParent->appendChild( metadata ); + Inkscape::GC::release( metadata ); + + // move the node into it + Inkscape::GC::anchor( node ); + sp_repr_unparent( node ); + metadata->appendChild( node ); + Inkscape::GC::release( node ); + } + } + } +} + +Inkscape::XML::Node const *RDFImpl::getRdfRootRepr( SPDocument const * doc ) +{ + Inkscape::XML::Node const *rdf = nullptr; + if ( !doc ) { + g_critical("Null doc passed to getRdfRootRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else { + rdf = sp_repr_lookup_name( doc->getReprDoc(), XML_TAG_NAME_RDF ); + } + + return rdf; +} + +Inkscape::XML::Node *RDFImpl::ensureRdfRootRepr( SPDocument * doc ) +{ + Inkscape::XML::Node *rdf = nullptr; + if ( !doc ) { + g_critical("Null doc passed to ensureRdfRootRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else { + rdf = sp_repr_lookup_name( doc->getReprDoc(), XML_TAG_NAME_RDF ); + if ( !rdf ) { + Inkscape::XML::Node * svg = sp_repr_lookup_name( doc->getReprRoot(), XML_TAG_NAME_SVG ); + if ( !svg ) { + g_critical("Unable to locate svg element."); + } else { + Inkscape::XML::Node * parent = sp_repr_lookup_name( svg, XML_TAG_NAME_METADATA ); + if ( parent == nullptr ) { + parent = doc->getReprDoc()->createElement( XML_TAG_NAME_METADATA ); + if ( !parent ) { + g_critical("Unable to create metadata element"); + } else { + svg->appendChild(parent); + Inkscape::GC::release(parent); + } + } + + if ( parent && !parent->document() ) { + g_critical("Parent has no document"); + } else if ( parent ) { + rdf = parent->document()->createElement( XML_TAG_NAME_RDF ); + if ( !rdf ) { + g_critical("Unable to create root RDF element."); + } else { + parent->appendChild(rdf); + Inkscape::GC::release(rdf); + } + } + } + } + } + + if ( rdf ) { + ensureParentIsMetadata( doc, rdf ); + } + + return rdf; +} + +Inkscape::XML::Node const *RDFImpl::getXmlRepr( SPDocument const * doc, gchar const * name ) +{ + Inkscape::XML::Node const * xml = nullptr; + if ( !doc ) { + g_critical("Null doc passed to getXmlRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else if (!name) { + g_critical("Null name passed to getXmlRepr()"); + } else { + Inkscape::XML::Node const * rdf = getRdfRootRepr( doc ); + if ( rdf ) { + xml = sp_repr_lookup_name( rdf, name ); + } + } + return xml; +} + +Inkscape::XML::Node *RDFImpl::getXmlRepr( SPDocument * doc, gchar const * name ) +{ + Inkscape::XML::Node const *xml = getXmlRepr( const_cast<SPDocument const *>(doc), name ); + + return const_cast<Inkscape::XML::Node *>(xml); +} + +Inkscape::XML::Node *RDFImpl::ensureXmlRepr( SPDocument * doc, gchar const * name ) +{ + Inkscape::XML::Node * xml = nullptr; + if ( !doc ) { + g_critical("Null doc passed to ensureXmlRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else if (!name) { + g_critical("Null name passed to ensureXmlRepr()"); + } else { + Inkscape::XML::Node * rdf = ensureRdfRootRepr( doc ); + if ( rdf ) { + xml = sp_repr_lookup_name( rdf, name ); + if ( !xml ) { + xml = doc->getReprDoc()->createElement( name ); + if ( !xml ) { + g_critical("Unable to create xml element <%s>.", name); + } else { + xml->setAttribute("rdf:about", "" ); + + rdf->appendChild(xml); + Inkscape::GC::release(xml); + } + } + } + } + return xml; +} + +Inkscape::XML::Node const *RDFImpl::getWorkRepr( SPDocument const * doc, gchar const * name ) +{ + Inkscape::XML::Node const * item = nullptr; + if ( !doc ) { + g_critical("Null doc passed to getWorkRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else if (!name) { + g_critical("Null name passed to getWorkRepr()"); + } else { + Inkscape::XML::Node const* work = getXmlRepr( doc, XML_TAG_NAME_WORK ); + if ( work ) { + item = sp_repr_lookup_name( work, name, 1 ); + } + } + return item; +} + +Inkscape::XML::Node *RDFImpl::ensureWorkRepr( SPDocument * doc, gchar const * name ) +{ + Inkscape::XML::Node * item = nullptr; + if ( !doc ) { + g_critical("Null doc passed to ensureWorkRepr()"); + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else if (!name) { + g_critical("Null name passed to ensureWorkRepr()"); + } else { + Inkscape::XML::Node * work = ensureXmlRepr( doc, XML_TAG_NAME_WORK ); + if ( work ) { + item = sp_repr_lookup_name( work, name, 1 ); + if ( !item ) { + //printf("missing XML '%s'\n",name); + item = doc->getReprDoc()->createElement( name ); + if ( !item ) { + g_critical("Unable to create xml element <%s>", name); + } else { + work->appendChild(item); + Inkscape::GC::release(item); + } + } + } + } + return item; +} + + +// Public API: +const gchar *rdf_get_work_entity(SPDocument const * doc, struct rdf_work_entity_t * entity) +{ + const gchar *result = nullptr; + if ( !doc ) { + g_critical("Null doc passed to rdf_get_work_entity()"); + } else if ( entity ) { + //g_message("want '%s'\n",entity->title); + + result = RDFImpl::getWorkEntity( doc, *entity ); + + //g_message("found '%s' == '%s'\n", entity->title, result ); + } + return result; +} + +const gchar *RDFImpl::getWorkEntity(SPDocument const * doc, struct rdf_work_entity_t & entity) +{ + gchar const *result = nullptr; + + Inkscape::XML::Node const * item = getWorkRepr( doc, entity.tag ); + if ( item ) { + result = getReprText( item, entity ); + // TODO note that this is the location that used to set the title if needed. Ensure code it not required. + } + + return result; +} + +// Public API: +unsigned int rdf_set_work_entity(SPDocument * doc, struct rdf_work_entity_t * entity, + const gchar * text) +{ + unsigned int result = 0; + if ( !doc ) { + g_critical("Null doc passed to rdf_set_work_entity()"); + } else if ( entity ) { + result = RDFImpl::setWorkEntity( doc, *entity, text ); + } + + return result; +} + +unsigned int RDFImpl::setWorkEntity(SPDocument * doc, struct rdf_work_entity_t & entity, const gchar * text) +{ + int result = 0; + if ( !text ) { + // FIXME: on a "NULL" text, delete the entity. For now, blank it. + text = ""; + } + + /* + printf("changing '%s' (%s) to '%s'\n", + entity->title, + entity->tag, + text); + */ + + Inkscape::XML::Node * item = ensureWorkRepr( doc, entity.tag ); + if ( !item ) { + g_critical("Unable to get work element"); + } else { + result = setReprText( item, entity, text ); + } + return result; +} + + +#undef DEBUG_MATCH + +static bool +rdf_match_license(Inkscape::XML::Node const *repr, struct rdf_license_t const *license) +{ + g_assert ( repr != nullptr ); + g_assert ( license != nullptr ); + + bool result=TRUE; +#ifdef DEBUG_MATCH + printf("checking against '%s'\n",license->name); +#endif + + int count = 0; + for (struct rdf_double_t const *details = license->details; + details->name; details++ ) { + count++; + } + bool * matched = (bool*)calloc(count,sizeof(bool)); + + for (Inkscape::XML::Node const *current = repr->firstChild(); + current; + current = current->next() ) { + + gchar const * attr = current->attribute("rdf:resource"); + if ( attr == nullptr ) continue; + +#ifdef DEBUG_MATCH + printf("\texamining '%s' => '%s'\n", current->name(), attr); +#endif + + bool found_match=FALSE; + for (int i=0; i<count; i++) { + // skip already matched items + if (matched[i]) continue; + +#ifdef DEBUG_MATCH + printf("\t\t'%s' vs '%s'\n", current->name(), license->details[i].name); + printf("\t\t'%s' vs '%s'\n", attr, license->details[i].resource); +#endif + + if (!strcmp( current->name(), + license->details[i].name ) && + !strcmp( attr, + license->details[i].resource )) { + matched[i]=TRUE; + found_match=TRUE; +#ifdef DEBUG_MATCH + printf("\t\tgood!\n"); +#endif + break; + } + } + if (!found_match) { + // if we checked each known item of the license + // and didn't find it, we must abort + result=FALSE; +#ifdef DEBUG_MATCH + printf("\t\tno '%s' element matched XML (bong)!\n",license->name); +#endif + break; + } + } +#ifdef DEBUG_MATCH + if (result) printf("\t\tall XML found matching elements!\n"); +#endif + for (int i=0; result && i<count; i++) { + // scan looking for an unmatched item + if (matched[i]==0) { + result=FALSE; +#ifdef DEBUG_MATCH + printf("\t\tnot all '%s' elements used to match (bong)!\n", license->name); +#endif + } + } + +#ifdef DEBUG_MATCH + printf("\t\tall '%s' elements used to match!\n",license->name); +#endif + + free(matched); + +#ifdef DEBUG_MATCH + if (result) printf("matched '%s'\n",license->name); +#endif + return result; +} + +// Public API: +struct rdf_license_t *rdf_get_license(SPDocument *document) +{ + return RDFImpl::getLicense(document); +} + +struct rdf_license_t *RDFImpl::getLicense(SPDocument *document) +{ + // Base license lookup on the URI of cc:license rather than the license + // properties, per instructions from the ccREL gurus. + // (Fixes https://bugs.launchpad.net/inkscape/+bug/372427) + + struct rdf_work_entity_t *entity = rdf_find_entity("license_uri"); + if (entity == nullptr) { + g_critical("Can't find internal entity structure for 'license_uri'"); + return nullptr; + } + + const gchar *uri = getWorkEntity(document, *entity); + struct rdf_license_t * license_by_uri = nullptr; + + if (uri != nullptr) { + for (struct rdf_license_t * license = rdf_licenses; license->name; license++) { + if (g_strcmp0(uri, license->uri) == 0) { + license_by_uri = license; + break; + } + } + } + + // To improve backward compatibility, the old license matching code is + // kept as fallback and to warn about and fix discrepancies. + + // TODO: would it be better to do this code on document load? Is + // sp_metadata_build() then the right place to put the call to sort out + // any RDF mess? + + struct rdf_license_t * license_by_properties = nullptr; + + Inkscape::XML::Node const *repr = getXmlRepr( document, XML_TAG_NAME_LICENSE ); + if (repr) { + for ( struct rdf_license_t * license = rdf_licenses; license->name; license++ ) { + if ( rdf_match_license( repr, license ) ) { + license_by_properties = license; + break; + } + } + } + + if (license_by_uri != nullptr && license_by_properties != nullptr) { + // Both property and structure, use property + if (license_by_uri != license_by_properties) { + // TODO: this should be a user-visible warning, but how? + g_warning("Mismatch between %s and %s metadata:\n" + "%s value URI: %s (using this one!)\n" + "%s derived URI: %s", + XML_TAG_NAME_LICENSE_PROP, + XML_TAG_NAME_LICENSE, + XML_TAG_NAME_LICENSE_PROP, + license_by_uri->uri, + XML_TAG_NAME_LICENSE, + license_by_properties->uri); + } + + // Reset license structure to match so the document is consistent + // (and this will also silence the warning above on repeated calls). + setLicense(document, license_by_uri); + + return license_by_uri; + } + else if (license_by_uri != nullptr) { + // Only cc:license property, set structure for backward compatibility + setLicense(document, license_by_uri); + + return license_by_uri; + } + else if (license_by_properties != nullptr) { + // Only cc:License structure + // TODO: this could be a user-visible warning too + g_warning("No %s metadata found, derived license URI from %s: %s", + XML_TAG_NAME_LICENSE_PROP, XML_TAG_NAME_LICENSE, + license_by_properties->uri); + + // Set license property to match + setWorkEntity(document, *entity, license_by_properties->uri); + + return license_by_properties; + } + + // No license info at all + return nullptr; +} + +// Public API: +void rdf_set_license(SPDocument * doc, struct rdf_license_t const * license) +{ + RDFImpl::setLicense( doc, license ); +} + +void RDFImpl::setLicense(SPDocument * doc, struct rdf_license_t const * license) +{ + // When basing license check on only the license URI (see fix for + // https://bugs.launchpad.net/inkscape/+bug/372427 above) we should + // really drop this license section, but keep writing it for a while for + // compatibility with older versions. + + // drop old license section + Inkscape::XML::Node * repr = getXmlRepr( doc, XML_TAG_NAME_LICENSE ); + if (repr) { + sp_repr_unparent(repr); + } + + if ( !license ) { + // All done + } else if ( !doc->getReprDoc() ) { + g_critical("XML doc is null."); + } else { + // build new license section + repr = ensureXmlRepr( doc, XML_TAG_NAME_LICENSE ); + g_assert( repr ); + + repr->setAttribute("rdf:about", license->uri ); + + for (struct rdf_double_t const * detail = license->details; detail->name; detail++) { + Inkscape::XML::Node * child = doc->getReprDoc()->createElement( detail->name ); + g_assert ( child != nullptr ); + + child->setAttribute("rdf:resource", detail->resource ); + repr->appendChild(child); + Inkscape::GC::release(child); + } + } +} + +struct rdf_entity_default_t { + gchar const * name; + gchar const * text; +}; +struct rdf_entity_default_t rdf_defaults[] = { + { "format", "image/svg+xml", }, + { "type", "http://purl.org/dc/dcmitype/StillImage", }, + { nullptr, nullptr, } +}; + +// Public API: +void rdf_set_defaults( SPDocument * doc ) +{ + RDFImpl::setDefaults( doc ); + +} + +void RDFImpl::setDefaults( SPDocument * doc ) +{ + g_assert( doc != nullptr ); + + // Create metadata node if it doesn't already exist + if (!sp_item_group_get_child_by_name( doc->getRoot(), nullptr, + XML_TAG_NAME_METADATA)) { + if ( !doc->getReprDoc()) { + g_critical("XML doc is null."); + } else { + // create repr + Inkscape::XML::Node * rnew = doc->getReprDoc()->createElement(XML_TAG_NAME_METADATA); + + // insert into the document + doc->getReprRoot()->addChild(rnew, nullptr); + + // clean up + Inkscape::GC::release(rnew); + } + } + + // install defaults + for ( struct rdf_entity_default_t * rdf_default = rdf_defaults; rdf_default->name; rdf_default++) { + struct rdf_work_entity_t * entity = rdf_find_entity( rdf_default->name ); + g_assert( entity != nullptr ); + + if ( getWorkEntity( doc, *entity ) == nullptr ) { + setWorkEntity( doc, *entity, rdf_default->text ); + } + } +} + +/* + * Add the metadata stored in the users preferences to the document if + * a) the preference 'Input/Output->Add default metadata to new documents' is true, and + * b) there is no metadata already in the file (such as from a template) + */ +void rdf_add_from_preferences(SPDocument *doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/metadata/addToNewFile")) { + return; + } + + // If there is already some metadata in the doc (from a template) don't add default metadata + for (struct rdf_work_entity_t *entity = rdf_work_entities; entity && entity->name; entity++) { + if ( entity->editable == RDF_EDIT_GENERIC && + rdf_get_work_entity (doc, entity)) { + return; + } + } + + // Put the metadata from user preferences into the doc + for (struct rdf_work_entity_t *entity = rdf_work_entities; entity && entity->name; entity++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(entity->name)); + if (text.length() > 0) { + rdf_set_work_entity (doc, entity, text.c_str()); + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/rdf.h b/src/rdf.h new file mode 100644 index 0000000..f5abda0 --- /dev/null +++ b/src/rdf.h @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief headers for RDF types + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_RDF_H +#define SEEN_RDF_H + +#include <glibmm/i18n.h> +#include "document.h" + +#define PREFS_METADATA "/metadata/rdf/" + +// yeah, it's not a triple yet... +/** + * \brief Holds license name/resource doubles for rdf_license_t entries + */ +struct rdf_double_t { + char const *name; + char const *resource; +}; + +/** + * \brief Holds license name and RDF information + */ +struct rdf_license_t { + char const *name; /* localized name of this license */ + char const *uri; /* URL for the RDF/Work/license element */ + struct rdf_double_t *details; /* the license details */ +// char const *fragment; /* XML contents for the RDF/License tag */ +}; + +extern rdf_license_t rdf_licenses []; + +/** + * \brief Describes how a given RDF entity is stored in XML + */ +enum RDFType { + RDF_CONTENT, // direct between-XML-tags content + RDF_AGENT, // requires the "Agent" hierarchy before doing content + RDF_RESOURCE, // stored in "rdf:resource" element + RDF_XML, // literal XML + RDF_BAG // rdf:Bag resources +}; + +/** + * \brief Describes how a given RDF entity should be edited + */ +enum RDF_Format { + RDF_FORMAT_LINE, // uses single line data (GtkEntry) + RDF_FORMAT_MULTILINE, // uses multiline data (GtkTextView) + RDF_FORMAT_SPECIAL // uses some other edit methods +}; + +enum RDF_Editable { + RDF_EDIT_GENERIC, // editable via generic widgets + RDF_EDIT_SPECIAL, // special widgets are needed + RDF_EDIT_HARDCODED // isn't editable +}; + +/** + * \brief Holds known RDF/Work tags + */ +struct rdf_work_entity_t { + char const *name; /* unique name of this entity for internal reference */ + char const *title; /* localized title of this entity for data entry */ + char const *tag; /* namespace tag for the RDF/Work element */ + RDFType datatype; /* how to extract/inject the RDF information */ + char const *tip; /* tool tip to explain the meaning of the entity */ + RDF_Format format; /* in what format is this data edited? */ + RDF_Editable editable;/* in what way is the data editable? */ +}; + +extern rdf_work_entity_t rdf_work_entities []; + +/** + * \brief Generic collection of RDF information for the RDF debug function + */ +struct rdf_t { + char* work_title; + char* work_date; + char* work_creator; + char* work_owner; + char* work_publisher; + char* work_type; + char* work_source; + char* work_subject; + char* work_description; + struct rdf_license_t* license; +}; + +struct rdf_work_entity_t * rdf_find_entity(char const * name); + +/** + * \brief Retrieves a known RDF/Work entity's contents from the document XML by name + * \return A pointer to the entity's static contents as a string, or NULL if no entity exists + * \param entity The desired RDF/Work entity + * + */ +const gchar * rdf_get_work_entity(SPDocument const * doc, + struct rdf_work_entity_t * entity); + +/** + * \brief Stores a string into a named RDF/Work entity in the document XML + * \param entity The desired RDF/Work entity to replace + * \param string The string to replace the entity contents with + * + */ +unsigned int rdf_set_work_entity(SPDocument * doc, + struct rdf_work_entity_t * entity, + const char * text); + +/** + * \brief Attempts to match and retrieve a known RDF/License from the document XML + * \return A pointer to the static RDF license structure + * + */ +struct rdf_license_t * rdf_get_license(SPDocument *doc); + +/** + * \brief Stores an RDF/License XML in the document XML + * \param document Which document to update + * \param license The desired RDF/License structure to store; NULL drops old license, so can be used for proprietary license. + * + */ +void rdf_set_license(SPDocument * doc, + struct rdf_license_t const * license); + +void rdf_set_defaults ( SPDocument * doc ); + +void rdf_add_from_preferences ( SPDocument *doc ); + +#endif // SEEN_RDF_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/remove-last.h b/src/remove-last.h new file mode 100644 index 0000000..b67721d --- /dev/null +++ b/src/remove-last.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __REMOVE_LAST_H__ +#define __REMOVE_LAST_H__ + +#include <algorithm> +#include <vector> +#include <glib.h> + +template<class T> +inline void remove_last(std::vector<T> &seq, T const &elem) +{ + typename std::vector<T>::reverse_iterator i(find(seq.rbegin(), seq.rend(), elem)); + g_assert( i != seq.rend() ); + seq.erase(i.base()); +} + + +#endif /* !__REMOVE_LAST_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/removeoverlap.cpp b/src/removeoverlap.cpp new file mode 100644 index 0000000..8b3064b --- /dev/null +++ b/src/removeoverlap.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Interface between Inkscape code (SPItem) and remove-overlaps function. + */ +/* + * Authors: + * Tim Dwyer <tgdwyer@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> + +#include <2geom/transforms.h> + +#include "removeoverlap.h" + +#include "libvpsc/rectangle.h" + +#include "object/sp-item.h" +#include "object/sp-item-transform.h" + + +using vpsc::Rectangle; + +namespace { + +struct Record { + SPItem * item; + Geom::Point midpoint; + Rectangle * vspc_rect; + + Record() : item(nullptr), vspc_rect(nullptr) {} + Record(SPItem * i, Geom::Point m, Rectangle * r) + : item(i), midpoint(m), vspc_rect(r) {} +}; + +} + +/** +* Takes a list of inkscape items and moves them as little as possible +* such that rectangular bounding boxes are separated by at least xGap +* horizontally and yGap vertically +*/ +void removeoverlap(std::vector<SPItem*> const & items, double const xGap, double const yGap) { + std::vector<SPItem*> selected = items; + std::vector<Record> records; + std::vector<Rectangle*> rs; + + Geom::Point const gap(xGap, yGap); + for (SPItem * item: selected) { + using Geom::X; using Geom::Y; + Geom::OptRect item_box(item->desktopVisualBounds()); + if (item_box) { + Geom::Point min(item_box->min() - .5 * gap); + Geom::Point max(item_box->max() + .5 * gap); + // A negative gap is allowed, but will lead to problems when the gap is larger than + // the bounding box (in either X or Y direction, or both); min will have become max + // now, which cannot be handled by Rectangle() which is called below. And how will + // removeRectangleOverlap handle such a case? + // That's why we will enforce some boundaries on min and max here: + if (max[X] < min[X]) { + min[X] = max[X] = (min[X] + max[X]) / 2.; + } + if (max[Y] < min[Y]) { + min[Y] = max[Y] = (min[Y] + max[Y]) / 2.; + } + Rectangle * vspc_rect = new Rectangle(min[X], max[X], min[Y], max[Y]); + records.emplace_back(item, item_box->midpoint(), vspc_rect); + rs.push_back(vspc_rect); + } + } + if (!rs.empty()) { + removeoverlaps(rs); + } + for (Record & rec: records) { + Geom::Point const curr = rec.midpoint; + Geom::Point const dest(rec.vspc_rect->getCentreX(), rec.vspc_rect->getCentreY()); + rec.item->move_rel(Geom::Translate(dest - curr)); + delete rec.vspc_rect; + } +} diff --git a/src/removeoverlap.h b/src/removeoverlap.h new file mode 100644 index 0000000..0c5f6d9 --- /dev/null +++ b/src/removeoverlap.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Remove overlaps function + */ +/* + * Authors: + * Tim Dwyer <tgdwyer@gmail.com> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_REMOVEOVERLAP_H +#define SEEN_REMOVEOVERLAP_H + +class SPItem; + +void removeoverlap(std::vector<SPItem*> const &items, double xGap, double yGap); + +#endif // SEEN_REMOVEOVERLAP_H diff --git a/src/rubberband.cpp b/src/rubberband.cpp new file mode 100644 index 0000000..3519ee8 --- /dev/null +++ b/src/rubberband.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Rubberbanding selector. + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/sodipodi-ctrlrect.h" +#include "desktop.h" + +#include "rubberband.h" +#include "display/sp-canvas.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" + +Inkscape::Rubberband *Inkscape::Rubberband::_instance = nullptr; + +Inkscape::Rubberband::Rubberband(SPDesktop *dt) + : _desktop(dt), _rect(nullptr), _touchpath(nullptr), _started(false) +{ + _points.clear(); + _mode = RUBBERBAND_MODE_RECT; + _touchpath_curve = new SPCurve(); +} + +void Inkscape::Rubberband::delete_canvas_items() +{ + if (_rect) { + SPCanvasItem *temp = _rect; + _rect = nullptr; + sp_canvas_item_destroy(temp); + } + if (_touchpath) { + SPCanvasItem *temp = _touchpath; + _touchpath = nullptr; + sp_canvas_item_destroy(temp); + } +} + + +void Inkscape::Rubberband::start(SPDesktop *d, Geom::Point const &p) +{ + _points.clear(); + _touchpath_curve->reset(); + delete_canvas_items(); + _desktop = d; + _start = p; + _started = true; + _points.push_back(_desktop->d2w(p)); + _touchpath_curve->moveto(p); + + _desktop->canvas->forceFullRedrawAfterInterruptions(5); +} + +void Inkscape::Rubberband::stop() +{ + _started = false; + _mode = RUBBERBAND_MODE_RECT; // restore the default + + _points.clear(); + _touchpath_curve->reset(); + + delete_canvas_items(); + + if (_desktop) { + _desktop->canvas->endForcedFullRedraws(); + } +} + +void Inkscape::Rubberband::move(Geom::Point const &p) +{ + if (!_started) + return; + + _end = p; + _desktop->scroll_to_point(p); + _touchpath_curve->lineto(p); + + Geom::Point next = _desktop->d2w(p); + // we want the points to be at most 0.5 screen pixels apart, + // so that we don't lose anything small; + // if they are farther apart, we interpolate more points + if (!_points.empty() && Geom::L2(next-_points.back()) > 0.5) { + Geom::Point prev = _points.back(); + int subdiv = 2 * (int) round(Geom::L2(next-prev) + 0.5); + for (int i = 1; i <= subdiv; i ++) { + _points.push_back(prev + ((double)i/subdiv) * (next - prev)); + } + } else { + _points.push_back(next); + } + + if (_mode == RUBBERBAND_MODE_RECT) { + if (_rect == nullptr) { + _rect = static_cast<CtrlRect *>(sp_canvas_item_new(_desktop->getControls(), SP_TYPE_CTRLRECT, nullptr)); + _rect->setColor(0x808080ff, false, 0x0); + _rect->setInvert(true); + } + _rect->setRectangle(Geom::Rect(_start, _end)); + + sp_canvas_item_show(_rect); + if (_touchpath) + sp_canvas_item_hide(_touchpath); + + } else if (_mode == RUBBERBAND_MODE_TOUCHPATH) { + if (_touchpath == nullptr) { + _touchpath = sp_canvas_bpath_new(_desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(_touchpath), 0xff0000ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(_touchpath), 0, SP_WIND_RULE_NONZERO); + } + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(_touchpath), _touchpath_curve); + + sp_canvas_item_show(_touchpath); + if (_rect) + sp_canvas_item_hide(_rect); + } +} + +void Inkscape::Rubberband::setMode(int mode) +{ + _mode = mode; +} + +/** + * @return Rectangle in desktop coordinates + */ +Geom::OptRect Inkscape::Rubberband::getRectangle() const +{ + if (!_started) { + return Geom::OptRect(); + } + + return Geom::Rect(_start, _end); +} + +Inkscape::Rubberband *Inkscape::Rubberband::get(SPDesktop *desktop) +{ + if (_instance == nullptr) { + _instance = new Inkscape::Rubberband(desktop); + } + + return _instance; +} + +bool Inkscape::Rubberband::is_started() +{ + return _started; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/rubberband.h b/src/rubberband.h new file mode 100644 index 0000000..3521339 --- /dev/null +++ b/src/rubberband.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_RUBBERBAND_H +#define SEEN_RUBBERBAND_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Carl Hetherington <inkscape@carlh.net> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/rect.h> +#include <boost/optional.hpp> +#include <vector> + +/* fixme: do multidocument safe */ + +class CtrlRect; +class SPCurve; +class SPDesktop; +struct SPCanvasItem; + +enum { + RUBBERBAND_MODE_RECT, + RUBBERBAND_MODE_TOUCHPATH +}; + +namespace Inkscape +{ + +/** + * Rubberbanding selector. + */ +class Rubberband +{ +public: + + void start(SPDesktop *desktop, Geom::Point const &p); + void move(Geom::Point const &p); + Geom::OptRect getRectangle() const; + void stop(); + bool is_started(); + + inline int getMode() {return _mode;} + inline std::vector<Geom::Point> getPoints() {return _points;} + + void setMode(int mode); + + static Rubberband* get(SPDesktop *desktop); + +private: + + Rubberband(SPDesktop *desktop); + static Rubberband* _instance; + + SPDesktop *_desktop; + Geom::Point _start; + Geom::Point _end; + + std::vector<Geom::Point> _points; + + CtrlRect *_rect; + SPCanvasItem *_touchpath; + SPCurve *_touchpath_curve; + + void delete_canvas_items(); + + bool _started; + int _mode; +}; + +} + +#endif // SEEN_RUBBERBAND_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/satisfied-guide-cns.cpp b/src/satisfied-guide-cns.cpp new file mode 100644 index 0000000..5eb2a1d --- /dev/null +++ b/src/satisfied-guide-cns.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/coord.h> + +#include "satisfied-guide-cns.h" + +#include "desktop.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +void satisfied_guide_cns(SPDesktop const &desktop, + std::vector<Inkscape::SnapCandidatePoint> const &snappoints, + std::vector<SPGuideConstraint> &cns) +{ + SPNamedView const &nv = *desktop.getNamedView(); + for(auto guide : nv.guides) { + SPGuide &g = *guide; + for (unsigned int i = 0; i < snappoints.size(); ++i) { + if (Geom::are_near(g.getDistanceFrom(snappoints[i].getPoint()), 0, 1e-2)) { + cns.emplace_back(&g, i); + } + } + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/satisfied-guide-cns.h b/src/satisfied-guide-cns.h new file mode 100644 index 0000000..575009f --- /dev/null +++ b/src/satisfied-guide-cns.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SATISFIED_GUIDE_CNS_H +#define SEEN_SATISFIED_GUIDE_CNS_H + +#include <2geom/forward.h> +#include <vector> + +#include "snap-candidate.h" + +class SPDesktop; +class SPGuideConstraint; + +void satisfied_guide_cns(SPDesktop const &desktop, + std::vector<Inkscape::SnapCandidatePoint> const &snappoints, + std::vector<SPGuideConstraint> &cns); + + +#endif // SEEN_SATISFIED_GUIDE_CNS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/selcue.cpp b/src/selcue.cpp new file mode 100644 index 0000000..b1d30bc --- /dev/null +++ b/src/selcue.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Helper object for showing selected items + * + * Authors: + * bulia byak <bulia@users.sf.net> + * Carl Hetherington <inkscape@carlh.net> + * Abhishek Sharma + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selcue.h" + +#include "desktop.h" +#include "selection.h" +#include "text-editing.h" + +#include "display/sodipodi-ctrl.h" +#include "display/sodipodi-ctrlrect.h" +#include "display/sp-canvas-util.h" + +#include "libnrtype/Layout-TNG.h" + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" + +Inkscape::SelCue::BoundingBoxPrefsObserver::BoundingBoxPrefsObserver(SelCue &sel_cue) : + Observer("/tools/bounding_box"), + _sel_cue(sel_cue) +{ +} + +void Inkscape::SelCue::BoundingBoxPrefsObserver::notify(Preferences::Entry const &val) +{ + _sel_cue._boundingBoxPrefsChanged(static_cast<int>(val.getBool())); +} + +Inkscape::SelCue::SelCue(SPDesktop *desktop) + : _desktop(desktop), + _bounding_box_prefs_observer(*this) +{ + _selection = _desktop->getSelection(); + + _sel_changed_connection = _selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &Inkscape::SelCue::_newItemBboxes)) + ); + + { + void(SelCue::*modifiedSignal)() = &SelCue::_updateItemBboxes; + _sel_modified_connection = _selection->connectModified( + sigc::hide(sigc::hide(sigc::mem_fun(*this, modifiedSignal))) + ); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _updateItemBboxes(prefs); + prefs->addObserver(_bounding_box_prefs_observer); +} + +Inkscape::SelCue::~SelCue() +{ + _sel_changed_connection.disconnect(); + _sel_modified_connection.disconnect(); + + for (auto & _item_bboxe : _item_bboxes) { + sp_canvas_item_destroy(_item_bboxe); + } + _item_bboxes.clear(); + + for (auto & _text_baseline : _text_baselines) { + sp_canvas_item_destroy(_text_baseline); + } + _text_baselines.clear(); +} + +void Inkscape::SelCue::_updateItemBboxes() +{ + _updateItemBboxes(Inkscape::Preferences::get()); +} + +void Inkscape::SelCue::_updateItemBboxes(Inkscape::Preferences *prefs) +{ + gint mode = prefs->getInt("/options/selcue/value", MARK); + if (mode == NONE) { + return; + } + + g_return_if_fail(_selection != nullptr); + + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + + _updateItemBboxes(mode, prefs_bbox); +} + +void Inkscape::SelCue::_updateItemBboxes(gint mode, int prefs_bbox) +{ + auto items = _selection->items(); + if (_item_bboxes.size() != (unsigned int) boost::distance(items)) { + _newItemBboxes(); + return; + } + + int bcount = 0; + auto ll= _selection->items(); + for (auto l = ll.begin(); l != ll.end(); ++l) { + SPItem *item = *l; + SPCanvasItem* box = _item_bboxes[bcount ++]; + + if (box) { + Geom::OptRect const b = (prefs_bbox == 0) ? + item->desktopVisualBounds() : item->desktopGeometricBounds(); + + if (b) { + sp_canvas_item_show(box); + if (mode == MARK) { + SP_CTRL(box)->moveto(Geom::Point(b->min()[Geom::X], b->max()[Geom::Y])); + } else if (mode == BBOX) { + SP_CTRLRECT(box)->setRectangle(*b); + } + } else { // no bbox + sp_canvas_item_hide(box); + } + } + } + + _newTextBaselines(); +} + + +void Inkscape::SelCue::_newItemBboxes() +{ + for (auto & _item_bboxe : _item_bboxes) { + sp_canvas_item_destroy(_item_bboxe); + } + _item_bboxes.clear(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/options/selcue/value", MARK); + if (mode == NONE) { + return; + } + + g_return_if_fail(_selection != nullptr); + + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + + auto ll= _selection->items(); + for (auto l = ll.begin(); l != ll.end(); ++l) { + SPItem *item = *l; + + Geom::OptRect const b = (prefs_bbox == 0) ? + item->desktopVisualBounds() : item->desktopGeometricBounds(); + + SPCanvasItem* box = nullptr; + + if (b) { + if (mode == MARK) { + box = sp_canvas_item_new(_desktop->getControls(), + SP_TYPE_CTRL, + "mode", SP_CTRL_MODE_XOR, + "shape", SP_CTRL_SHAPE_DIAMOND, + "size", 6, + "filled", TRUE, + "fill_color", 0x000000ff, + "stroked", FALSE, + "stroke_color", 0x000000ff, + NULL); + sp_canvas_item_show(box); + SP_CTRL(box)->moveto(Geom::Point(b->min()[Geom::X], b->max()[Geom::Y])); + + sp_canvas_item_move_to_z(box, 0); // just low enough to not get in the way of other draggable knots + + } else if (mode == BBOX) { + box = sp_canvas_item_new(_desktop->getControls(), + SP_TYPE_CTRLRECT, + nullptr); + + SP_CTRLRECT(box)->setRectangle(*b); + SP_CTRLRECT(box)->setColor(0xffffffa0, false, 0); + SP_CTRLRECT(box)->setDashed(true); + SP_CTRLRECT(box)->setInvert(false); + SP_CTRLRECT(box)->setShadow(1, 0x0000c0a0); + + sp_canvas_item_move_to_z(box, 0); + } + } + + if (box) { + _item_bboxes.push_back(box); + } + } + + _newTextBaselines(); +} + +void Inkscape::SelCue::_newTextBaselines() +{ + for (auto & _text_baseline : _text_baselines) { + sp_canvas_item_destroy(_text_baseline); + } + _text_baselines.clear(); + + auto ll = _selection->items(); + for (auto l=ll.begin();l!=ll.end();++l) { + SPItem *item = *l; + + SPCanvasItem* baseline_point = nullptr; + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { // visualize baseline + Inkscape::Text::Layout const *layout = te_get_layout(item); + if (layout != nullptr && layout->outputExists()) { + boost::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + if (pt) { + baseline_point = sp_canvas_item_new(_desktop->getControls(), SP_TYPE_CTRL, + "mode", SP_CTRL_MODE_XOR, + "size", 5, + "filled", 0, + "stroked", 1, + "stroke_color", 0x000000ff, + NULL); + + sp_canvas_item_show(baseline_point); + SP_CTRL(baseline_point)->moveto((*pt) * item->i2dt_affine()); + sp_canvas_item_move_to_z(baseline_point, 0); + } + } + } + + if (baseline_point) { + _text_baselines.push_back(baseline_point); + } + } +} + +void Inkscape::SelCue::_boundingBoxPrefsChanged(int prefs_bbox) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/options/selcue/value", MARK); + if (mode == NONE) { + return; + } + + g_return_if_fail(_selection != nullptr); + + _updateItemBboxes(mode, prefs_bbox); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/selcue.h b/src/selcue.h new file mode 100644 index 0000000..74a7443 --- /dev/null +++ b/src/selcue.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SELCUE_H +#define SEEN_SP_SELCUE_H + +/* + * Helper object for showing selected items + * + * Authors: + * bulia byak <bulia@users.sf.net> + * Carl Hetherington <inkscape@carlh.net> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <list> + +#include <sigc++/sigc++.h> + +#include "preferences.h" + +class SPDesktop; +struct SPCanvasItem; + +namespace Inkscape { + +class Selection; + +class SelCue +{ +public: + SelCue(SPDesktop *desktop); + ~SelCue(); + + enum Type { + NONE, + MARK, + BBOX + }; + +private: + class BoundingBoxPrefsObserver: public Preferences::Observer + { + public: + BoundingBoxPrefsObserver(SelCue &sel_cue); + + void notify(Preferences::Entry const &val) override; + + private: + SelCue &_sel_cue; + }; + + friend class Inkscape::SelCue::BoundingBoxPrefsObserver; + + void _updateItemBboxes(); + void _updateItemBboxes(Inkscape::Preferences *prefs); + void _updateItemBboxes(int mode, int prefs_bbox); + void _newItemBboxes(); + void _newTextBaselines(); + void _boundingBoxPrefsChanged(int prefs_bbox); + + SPDesktop *_desktop; + Selection *_selection; + sigc::connection _sel_changed_connection; + sigc::connection _sel_modified_connection; + std::vector<SPCanvasItem*> _item_bboxes; + std::vector<SPCanvasItem*> _text_baselines; + + BoundingBoxPrefsObserver _bounding_box_prefs_observer; +}; + +} + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/selection-chemistry.cpp b/src/selection-chemistry.cpp new file mode 100644 index 0000000..6a59b8b --- /dev/null +++ b/src/selection-chemistry.cpp @@ -0,0 +1,4480 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Miscellaneous operations on selected items. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Andrius R. <knutux@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Martin Sucha <martin.sucha-inkscape@jts-sro.sk> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> (Symbol additions) + * Adrian Boguszewski + * Marc Jeanmougin + * + * Copyright (C) 1999-2016 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/range/adaptor/reversed.hpp> +#include <cstring> +#include <glibmm/i18n.h> +#include <map> +#include <string> + +#include <gtkmm/clipboard.h> + +#include "selection-chemistry.h" + +#include "file.h" + +// TODO FIXME: This should be moved into preference repr +SPCycleType SP_CYCLING = SP_CYCLE_FOCUS; + + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-drag.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "layer-model.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "selection.h" +#include "text-editing.h" +#include "text-chemistry.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/sp-canvas.h" + +#include "helper/png-write.h" + +#include "io/resource.h" + +#include "live_effects/effect.h" +#include "live_effects/parameter/originalpath.h" + +#include "object/box3d.h" +#include "object/object-set.h" +#include "object/persp3d.h" +#include "object/sp-clippath.h" +#include "object/sp-conn-end.h" +#include "object/sp-defs.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowregion.h" +#include "object/sp-flowtext.h" +#include "object/sp-gradient-reference.h" +#include "object/sp-image.h" +#include "object/sp-item-transform.h" +#include "object/sp-item.h" +#include "object/sp-line.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-pattern.h" +#include "object/sp-polyline.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "object/sp-symbol.h" +#include "object/sp-textpath.h" +#include "object/sp-tref.h" +#include "object/sp-tspan.h" +#include "object/sp-use.h" +#include "style.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "ui/clipboard.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools-switch.h" +#include "ui/tools/connector-tool.h" +#include "ui/tools/dropper-tool.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/text-tool.h" + +#include "xml/rebase-hrefs.h" +#include "xml/simple-document.h" + +using Inkscape::DocumentUndo; +using Geom::X; +using Geom::Y; +using Inkscape::UI::Tools::NodeTool; +using namespace Inkscape; + +/* The clipboard handling is in ui/clipboard.cpp now. There are some legacy functions left here, +because the layer manipulation code uses them. It should be rewritten specifically +for that purpose. */ + + +// helper for printing error messages, regardless of whether we have a GUI or not +// If desktop == NULL, errors will be shown on stderr +static void +selection_display_message(SPDesktop *desktop, Inkscape::MessageType msgType, Glib::ustring const &msg) +{ + if (desktop) { + desktop->messageStack()->flash(msgType, msg); + } else { + if (msgType == Inkscape::IMMEDIATE_MESSAGE || + msgType == Inkscape::WARNING_MESSAGE || + msgType == Inkscape::ERROR_MESSAGE) { + g_printerr("%s\n", msg.c_str()); + } + } +} + +namespace Inkscape { + +void SelectionHelper::selectAll(SPDesktop *dt) +{ + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + if (!nt->_multipath->empty()) { + nt->_multipath->selectSubpaths(); + return; + } + } + sp_edit_select_all(dt); +} + +void SelectionHelper::selectAllInAll(SPDesktop *dt) +{ + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_selected_nodes->selectAll(); + } else { + sp_edit_select_all_in_all_layers(dt); + } +} + +void SelectionHelper::selectNone(SPDesktop *dt) +{ + NodeTool *nt = nullptr; + if (tools_isactive(dt, TOOLS_NODES)) { + nt = static_cast<NodeTool*>(dt->event_context); + } + + if (nt && !nt->_selected_nodes->empty()) { + nt->_selected_nodes->clear(); + } else if (!dt->getSelection()->isEmpty()) { + dt->getSelection()->clear(); + } else { + // If nothing selected switch to selection tool + tools_switch(dt, TOOLS_SELECT); + } +} + +void SelectionHelper::selectSameFillStroke(SPDesktop *dt) +{ + sp_select_same_fill_stroke_style(dt, true, true, true); +} + +void SelectionHelper::selectSameFillColor(SPDesktop *dt) +{ + sp_select_same_fill_stroke_style(dt, true, false, false); +} + +void SelectionHelper::selectSameStrokeColor(SPDesktop *dt) +{ + sp_select_same_fill_stroke_style(dt, false, true, false); +} + +void SelectionHelper::selectSameStrokeStyle(SPDesktop *dt) +{ + sp_select_same_fill_stroke_style(dt, false, false, true); +} + +void SelectionHelper::selectSameObjectType(SPDesktop *dt) +{ + sp_select_same_object_type(dt); +} + +void SelectionHelper::invert(SPDesktop *dt) +{ + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_multipath->invertSelectionInSubpaths(); + } else { + sp_edit_invert(dt); + } +} + +void SelectionHelper::invertAllInAll(SPDesktop *dt) +{ + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_selected_nodes->invertSelection(); + } else { + sp_edit_invert_in_all_layers(dt); + } +} + +void SelectionHelper::reverse(SPDesktop *dt) +{ + // TODO make this a virtual method of event context! + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_multipath->reverseSubpaths(); + } else { + dt->getSelection()->pathReverse(); + } +} + +void SelectionHelper::selectNext(SPDesktop *dt) +{ + Inkscape::UI::Tools::ToolBase *ec = dt->event_context; + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_multipath->shiftSelection(1); + } else if (tools_isactive(dt, TOOLS_GRADIENT) + && ec->_grdrag->isNonEmpty()) { + Inkscape::UI::Tools::sp_gradient_context_select_next(ec); + } else { + sp_selection_item_next(dt); + } +} + +void SelectionHelper::selectPrev(SPDesktop *dt) +{ + Inkscape::UI::Tools::ToolBase *ec = dt->event_context; + if (tools_isactive(dt, TOOLS_NODES)) { + NodeTool *nt = static_cast<NodeTool*>(dt->event_context); + nt->_multipath->shiftSelection(-1); + } else if (tools_isactive(dt, TOOLS_GRADIENT) + && ec->_grdrag->isNonEmpty()) { + Inkscape::UI::Tools::sp_gradient_context_select_prev(ec); + } else { + sp_selection_item_prev(dt); + } +} + +/* + * Fixes the current selection, removing locked objects from it + */ +void SelectionHelper::fixSelection(SPDesktop *dt) +{ + if(!dt) + return; + + Inkscape::Selection *selection = dt->getSelection(); + + std::vector<SPItem*> items ; + + auto selList = selection->items(); + + for(auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPItem *item = *i; + if( item && + !dt->isLayer(item) && + (!item->isLocked())) + { + items.push_back(item); + } + } + + selection->setList(items); +} + +} // namespace Inkscape + + +/** + * Copies repr and its inherited css style elements, along with the accumulated transform 'full_t', + * then prepends the copy to 'clip'. + */ +static void sp_selection_copy_one(Inkscape::XML::Node *repr, Geom::Affine full_t, std::vector<Inkscape::XML::Node*> &clip, Inkscape::XML::Document* xml_doc) +{ + Inkscape::XML::Node *copy = repr->duplicate(xml_doc); + + // copy complete inherited style + SPCSSAttr *css = sp_repr_css_attr_inherited(repr, "style"); + sp_repr_css_set(copy, css, "style"); + sp_repr_css_attr_unref(css); + + // write the complete accumulated transform passed to us + // (we're dealing with unattached repr, so we write to its attr + // instead of using sp_item_set_transform) + gchar *affinestr=sp_svg_transform_write(full_t); + copy->setAttribute("transform", affinestr); + g_free(affinestr); + + clip.insert(clip.begin(),copy); +} + +static void sp_selection_copy_impl(std::vector<SPItem*> const &items, std::vector<Inkscape::XML::Node*> &clip, Inkscape::XML::Document* xml_doc) +{ + // Sort items: + std::vector<SPItem*> sorted_items(items); + sort(sorted_items.begin(),sorted_items.end(),sp_object_compare_position_bool); + + // Copy item reprs: + for (auto item : sorted_items) { + if (item) { + sp_selection_copy_one(item->getRepr(), item->i2doc_affine(), clip, xml_doc); + } else { + g_assert_not_reached(); + } + } + reverse(clip.begin(),clip.end()); +} + +// TODO check if parent parameter should be changed to SPItem, of if the code should handle non-items. +static std::vector<Inkscape::XML::Node*> sp_selection_paste_impl(SPDocument *doc, SPObject *parent, std::vector<Inkscape::XML::Node*> &clip) +{ + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + SPItem *parentItem = dynamic_cast<SPItem *>(parent); + g_assert(parentItem != nullptr); + + std::vector<Inkscape::XML::Node*> copied; + // add objects to document + for (auto repr : clip) { + Inkscape::XML::Node *copy = repr->duplicate(xml_doc); + + // premultiply the item transform by the accumulated parent transform in the paste layer + Geom::Affine local(parentItem->i2doc_affine()); + if (!local.isIdentity()) { + gchar const *t_str = copy->attribute("transform"); + Geom::Affine item_t(Geom::identity()); + if (t_str) + sp_svg_transform_read(t_str, &item_t); + item_t *= local.inverse(); + // (we're dealing with unattached repr, so we write to its attr instead of using sp_item_set_transform) + gchar *affinestr=sp_svg_transform_write(item_t); + copy->setAttribute("transform", affinestr); + g_free(affinestr); + } + + parent->appendChildRepr(copy); + copied.push_back(copy); + Inkscape::GC::release(copy); + } + return copied; +} + +static void sp_selection_delete_impl(std::vector<SPItem*> const &items, bool propagate = true, bool propagate_descendants = true) +{ + for (auto item : items) { + sp_object_ref(item, nullptr); + } + for (auto item : items) { + item->deleteObject(propagate, propagate_descendants); + sp_object_unref(item, nullptr); + } +} + + +void ObjectSet::deleteItems() +{ + if(desktop() && tools_isactive(desktop(), TOOLS_TEXT)){ + if (Inkscape::UI::Tools::sp_text_delete_selection(desktop()->event_context)) { + DocumentUndo::done(desktop()->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Delete text")); + return; + } + } + + if (isEmpty()) { + selection_display_message(desktop(),Inkscape::WARNING_MESSAGE, _("<b>Nothing</b> was deleted.")); + return; + } + std::vector<SPItem*> selected(items().begin(), items().end()); + clear(); + sp_selection_delete_impl(selected); + if(SPDesktop *d = desktop()){ + d->currentLayer()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + /* a tool may have set up private information in it's selection context + * that depends on desktop items. I think the only sane way to deal with + * this currently is to reset the current tool, which will reset it's + * associated selection context. For example: deleting an object + * while moving it around the canvas. + */ + tools_switch( d, tools_active( d ) ); + } + if(document()) + DocumentUndo::done(document(), SP_VERB_EDIT_DELETE, + _("Delete")); + +} + + + +static void add_ids_recursive(std::vector<const gchar *> &ids, SPObject *obj) +{ + if (obj) { + ids.push_back(obj->getId()); + + if (dynamic_cast<SPGroup *>(obj)) { + for (auto& child: obj->children) { + add_ids_recursive(ids, &child); + } + } + } +} + +void ObjectSet::duplicate(bool suppressDone, bool duplicateLayer) +{ + if(duplicateLayer && !desktop() ){ + //TODO: understand why layer management is tied to desktop and not to document. + return; + } + + SPDocument *doc = document(); + + if(!doc) + return; + + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty() && !duplicateLayer) { + selection_display_message(desktop(),Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to duplicate.")); + return; + } + std::vector<Inkscape::XML::Node*> reprs(xmlNodes().begin(), xmlNodes().end()); + + if(duplicateLayer){ + reprs.clear(); + reprs.push_back(desktop()->currentLayer()->getRepr()); + } + + clear(); + + // sorting items from different parents sorts each parent's subset without possibly mixing + // them, just what we need + sort(reprs.begin(),reprs.end(),sp_repr_compare_position_bool); + + std::vector<const gchar *> old_ids; + std::vector<const gchar *> new_ids; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool relink_clones = prefs->getBool("/options/relinkclonesonduplicate/value"); + const bool fork_livepatheffects = prefs->getBool("/options/forklpeonduplicate/value", true); + + // check ref-d shapes, split in defs|internal|external + // add external & defs to reprs + auto text_refs = text_categorize_refs(doc, reprs.begin(), reprs.end(), + static_cast<text_ref_t>(TEXT_REF_DEF | TEXT_REF_EXTERNAL | TEXT_REF_INTERNAL)); + for (auto const &ref : text_refs) { + if (ref.second == TEXT_REF_DEF || ref.second == TEXT_REF_EXTERNAL) { + reprs.push_back(doc->getObjectById(ref.first)->getRepr()); + } + } + + std::vector<Inkscape::XML::Node*> copies; + for(auto old_repr : reprs) { + Inkscape::XML::Node *parent = old_repr->parent(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + + if (!duplicateLayer || sp_repr_is_def(old_repr)) { + parent->appendChild(copy); + } else if (sp_repr_is_layer(old_repr)) { + parent->addChild(copy, old_repr); + } else { + // duplicateLayer, non-layer, non-def + // external nodes -- append to new layer + // text_relink will ignore extra nodes in layer children + copies[0]->appendChild(copy); + } + + if (relink_clones) { + SPObject *old_obj = doc->getObjectByRepr(old_repr); + SPObject *new_obj = doc->getObjectByRepr(copy); + add_ids_recursive(old_ids, old_obj); + add_ids_recursive(new_ids, new_obj); + } + + if (fork_livepatheffects) { + SPObject *new_obj = doc->getObjectByRepr(copy); + SPLPEItem *newLPEObj = dynamic_cast<SPLPEItem *>(new_obj); + if (newLPEObj) { + newLPEObj->forkPathEffectsIfNecessary(1); + } + } + + copies.push_back(copy); + Inkscape::GC::release(copy); + } + + // Relink copied text nodes to copied reference shapes + text_relink_refs(text_refs, reprs.begin(), reprs.end(), copies.begin()); + + // copies contains def nodes, we don't want that in our selection + std::vector<Inkscape::XML::Node*> newsel; + if (!duplicateLayer) { + // compute newsel, by removing def nodes from copies + for (auto node : copies) { + if (!sp_repr_is_def(node)) { + newsel.push_back(node); + } + } + } + + if (relink_clones) { + + g_assert(old_ids.size() == new_ids.size()); + + for (unsigned int i = 0; i < old_ids.size(); i++) { + const gchar *id = old_ids[i]; + SPObject *old_clone = doc->getObjectById(id); + SPUse *use = dynamic_cast<SPUse *>(old_clone); + SPOffset *offset = dynamic_cast<SPOffset *>(old_clone); + SPText *text = dynamic_cast<SPText *>(old_clone); + SPPath *path = dynamic_cast<SPPath *>(old_clone); + if (use) { + SPItem *orig = use->get_original(); + if (!orig) // orphaned + continue; + for (unsigned int j = 0; j < old_ids.size(); j++) { + if (!strcmp(orig->getId(), old_ids[j])) { + // we have both orig and clone in selection, relink + // std::cout << id << " old, its ori: " << orig->getId() << "; will relink:" << new_ids[i] << " to " << new_ids[j] << "\n"; + SPObject *new_clone = doc->getObjectById(new_ids[i]); + new_clone->setAttribute("xlink:href", Glib::ustring("#") + new_ids[j]); + new_clone->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } else if (offset) { + gchar *source_href = offset->sourceHref; + for (guint j = 0; j < old_ids.size(); j++) { + if (source_href && source_href[0]=='#' && !strcmp(source_href+1, old_ids[j])) { + doc->getObjectById(new_ids[i])->setAttribute("xlink:href", Glib::ustring("#") + new_ids[j]); + } + } + } else if (text) { + SPTextPath *textpath = dynamic_cast<SPTextPath *>(text->firstChild()); + if (!textpath) continue; + const gchar *source_href = sp_textpath_get_path_item(textpath)->getId(); + for (guint j = 0; j < old_ids.size(); j++) { + if (!strcmp(source_href, old_ids[j])) { + doc->getObjectById(new_ids[i])->firstChild()->setAttribute("xlink:href", Glib::ustring("#") + new_ids[j]); + } + } + } else if (path) { + if (old_clone->getAttribute("inkscape:connection-start") != nullptr) { + const char *old_start = old_clone->getAttribute("inkscape:connection-start"); + const char *old_end = old_clone->getAttribute("inkscape:connection-end"); + SPObject *new_clone = doc->getObjectById(new_ids[i]); + for (guint j = 0; j < old_ids.size(); j++) { + if(old_start == Glib::ustring("#") + old_ids[j]) { + new_clone->setAttribute("inkscape:connection-start", Glib::ustring("#") + new_ids[j]); + } + if(old_end == Glib::ustring("#") + old_ids[j]) { + new_clone->setAttribute("inkscape:connection-end", Glib::ustring("#") + new_ids[j]); + } + } + } + } + } + } + + + if ( !suppressDone ) { + DocumentUndo::done(document(), SP_VERB_EDIT_DUPLICATE, + _("Duplicate")); + } + if(!duplicateLayer) + setReprList(newsel); + else{ + SPObject* new_layer = doc->getObjectByRepr(copies[0]); + gchar* name = g_strdup_printf(_("%s copy"), new_layer->label()); + desktop()->layer_manager->renameLayer( new_layer, name, TRUE ); + g_free(name); + } +} + +void sp_edit_clear_all(Inkscape::Selection *selection) +{ + if (!selection) + return; + + SPDocument *doc = selection->layers()->getDocument(); + selection->clear(); + + SPGroup *group = dynamic_cast<SPGroup *>(selection->layers()->currentLayer()); + g_return_if_fail(group != nullptr); + std::vector<SPItem*> items = sp_item_group_item_list(group); + + for(auto & item : items){ + item->deleteObject(); + } + + DocumentUndo::done(doc, SP_VERB_EDIT_CLEAR_ALL, + _("Delete all")); +} + +/* + * Return a list of SPItems that are the children of 'list' + * + * list - source list of items to search in + * desktop - desktop associated with the source list + * exclude - list of items to exclude from result + * onlyvisible - TRUE includes only items visible on canvas + * onlysensitive - TRUE includes only non-locked items + * ingroups - TRUE to recursively get grouped items children + */ +std::vector<SPItem*> &get_all_items(std::vector<SPItem*> &list, SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::vector<SPItem*> const &exclude) +{ + for (auto& child: from->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item && + !desktop->isLayer(item) && + (!onlysensitive || !item->isLocked()) && + (!onlyvisible || !desktop->itemIsHidden(item)) && + (exclude.empty() || exclude.end() == std::find(exclude.begin(), exclude.end(), &child)) + ) + { + list.insert(list.begin(),item); + } + + if (ingroups || (item && desktop->isLayer(item))) { + list = get_all_items(list, &child, desktop, onlyvisible, onlysensitive, ingroups, exclude); + } + } + + return list; +} + +static void sp_edit_select_all_full(SPDesktop *dt, bool force_all_layers, bool invert) +{ + if (!dt) + return; + + Inkscape::Selection *selection = dt->getSelection(); + + g_return_if_fail(dynamic_cast<SPGroup *>(dt->currentLayer())); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + PrefsSelectionContext inlayer = (PrefsSelectionContext) prefs->getInt("/options/kbselection/inlayer", PREFS_SELECTION_LAYER); + bool onlyvisible = prefs->getBool("/options/kbselection/onlyvisible", true); + bool onlysensitive = prefs->getBool("/options/kbselection/onlysensitive", true); + + std::vector<SPItem*> items ; + + std::vector<SPItem*> exclude; + if (invert) { + exclude.insert(exclude.end(), selection->items().begin(), selection->items().end()); + } + + if (force_all_layers) + inlayer = PREFS_SELECTION_ALL; + + switch (inlayer) { + case PREFS_SELECTION_LAYER: { + if ( (onlysensitive && dynamic_cast<SPItem *>(dt->currentLayer())->isLocked()) || + (onlyvisible && dt->itemIsHidden(dynamic_cast<SPItem *>(dt->currentLayer()))) ) + return; + + std::vector<SPItem*> all_items = sp_item_group_item_list(dynamic_cast<SPGroup *>(dt->currentLayer())); + + for (std::vector<SPItem*>::const_reverse_iterator i=all_items.rbegin();i!=all_items.rend();++i) { + SPItem *item = *i; + + if (item && (!onlysensitive || !item->isLocked())) { + if (!onlyvisible || !dt->itemIsHidden(item)) { + if (!dt->isLayer(item)) { + if (!invert || exclude.end() == std::find(exclude.begin(),exclude.end(),item)) { + items.push_back(item); // leave it in the list + } + } + } + } + } + + break; + } + case PREFS_SELECTION_LAYER_RECURSIVE: { + std::vector<SPItem*> x; + items = get_all_items(x, dt->currentLayer(), dt, onlyvisible, onlysensitive, FALSE, exclude); + break; + } + default: { + std::vector<SPItem*> x; + items = get_all_items(x, dt->currentRoot(), dt, onlyvisible, onlysensitive, FALSE, exclude); + break; + } + } + + selection->setList(items); + +} + +void sp_edit_select_all(SPDesktop *desktop) +{ + sp_edit_select_all_full(desktop, false, false); +} + +void sp_edit_select_all_in_all_layers(SPDesktop *desktop) +{ + sp_edit_select_all_full(desktop, true, false); +} + +void sp_edit_invert(SPDesktop *desktop) +{ + sp_edit_select_all_full(desktop, false, true); +} + +void sp_edit_invert_in_all_layers(SPDesktop *desktop) +{ + sp_edit_select_all_full(desktop, true, true); +} + +Inkscape::XML::Node* ObjectSet::group() { + SPDocument *doc = document(); + if(!doc) + return nullptr; + if (isEmpty()) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>some objects</b> to group.")); + return nullptr; + } + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *group = xml_doc->createElement("svg:g"); + + std::vector<Inkscape::XML::Node*> p(xmlNodes().begin(), xmlNodes().end()); + std::sort(p.begin(), p.end(), sp_repr_compare_position_bool); + this->clear(); + + // Remember the position and parent of the topmost object. + gint topmost = p.back()->position(); + Inkscape::XML::Node *topmost_parent = p.back()->parent(); + + for(auto current : p){ + if (current->parent() == topmost_parent) { + Inkscape::XML::Node *spnew = current->duplicate(xml_doc); + sp_repr_unparent(current); + group->appendChild(spnew); + Inkscape::GC::release(spnew); + topmost --; // only reduce count for those items deleted from topmost_parent + } else { // move it to topmost_parent first + std::vector<Inkscape::XML::Node*> temp_clip; + + // At this point, current may already have no item, due to its being a clone whose original is already moved away + // So we copy it artificially calculating the transform from its repr->attr("transform") and the parent transform + gchar const *t_str = current->attribute("transform"); + Geom::Affine item_t(Geom::identity()); + if (t_str) + sp_svg_transform_read(t_str, &item_t); + item_t *= dynamic_cast<SPItem *>(doc->getObjectByRepr(current->parent()))->i2doc_affine(); + // FIXME: when moving both clone and original from a transformed group (either by + // grouping into another parent, or by cut/paste) the transform from the original's + // parent becomes embedded into original itself, and this affects its clones. Fix + // this by remembering the transform diffs we write to each item into an array and + // then, if this is clone, looking up its original in that array and pre-multiplying + // it by the inverse of that original's transform diff. + + sp_selection_copy_one(current, item_t, temp_clip, xml_doc); + sp_repr_unparent(current); + + // paste into topmost_parent (temporarily) + std::vector<Inkscape::XML::Node*> copied = sp_selection_paste_impl(doc, doc->getObjectByRepr(topmost_parent), temp_clip); + if (!temp_clip.empty())temp_clip.clear() ; + if (!copied.empty()) { // if success, + // take pasted object (now in topmost_parent) + Inkscape::XML::Node *in_topmost = copied.back(); + // make a copy + Inkscape::XML::Node *spnew = in_topmost->duplicate(xml_doc); + // remove pasted + sp_repr_unparent(in_topmost); + // put its copy into group + group->appendChild(spnew); + Inkscape::GC::release(spnew); + copied.clear(); + } + } + } + + // Add the new group to the topmost members' parent + topmost_parent->addChildAtPos(group, topmost + 1); + + set(doc->getObjectByRepr(group)); + DocumentUndo::done(doc, SP_VERB_SELECTION_GROUP, + C_("Verb", "Group")); + + return group; +} + + +static bool clone_depth_descending(gconstpointer a, gconstpointer b) { + SPUse *use_a = static_cast<SPUse *>(const_cast<gpointer>(a)); + SPUse *use_b = static_cast<SPUse *>(const_cast<gpointer>(b)); + int depth_a = use_a->cloneDepth(); + int depth_b = use_b->cloneDepth(); + return (depth_a==depth_b)?(a<b):(depth_a>depth_b); +} + +void ObjectSet::popFromGroup(){ + if (isEmpty()) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("<b>No objects selected</b> to pop out of group.")); + return; + } + + auto item = items().begin(); // leaving this because it will be useful for + // future implementation of complex pop ungrouping + SPItem *obj = *item; + SPItem *parent_group = static_cast<SPItem*>(obj->parent); + if (!SP_IS_GROUP(parent_group) || SP_IS_LAYER(parent_group)) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Selection <b>not in a group</b>.")); + return; + } + if (parent_group->firstChild()->getNext() == nullptr) { + std::vector<SPItem*> children; + sp_item_group_ungroup(static_cast<SPGroup*>(parent_group), children, false); + } + else { + toNextLayer(true); + parent_group->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + + if(document()) + DocumentUndo::done(document(), SP_VERB_SELECTION_UNGROUP_POP_SELECTION, + _("Pop selection from group")); + +} + +static void ungroup_impl(ObjectSet *set) +{ + std::set<SPObject*> groups(set->groups().begin(),set->groups().end()); + + std::vector<SPItem*> new_select; + auto old_select = set->items(); + std::vector<SPItem*> items(old_select.begin(), old_select.end()); + + // If any of the clones refer to the groups, unlink them and replace them with successors + // in the items list. + std::vector<SPUse*> clones_to_unlink; + for (auto item : items) { + SPUse *use = dynamic_cast<SPUse *>(item); + + SPItem *original = use; + while (dynamic_cast<SPUse *>(original)) { + original = dynamic_cast<SPUse *>(original)->get_original(); + } + + if (groups.find(original) != groups.end()) { + clones_to_unlink.push_back(use); + } + } + + // Unlink clones beginning from those with highest clone depth. + // This way we can be sure than no additional automatic unlinking happens, + // and the items in the list remain valid + std::sort(clones_to_unlink.begin(),clones_to_unlink.end(),clone_depth_descending); + + for (auto use:clones_to_unlink) { + std::vector<SPItem*>::iterator items_node = std::find(items.begin(),items.end(), use); + *items_node = use->unlink(); + } + + // do the actual work + for (auto & item : items) { + SPItem *obj = item; + + // ungroup only the groups marked earlier + if (groups.find(item) != groups.end()) { + std::vector<SPItem*> children; + sp_item_group_ungroup(dynamic_cast<SPGroup *>(obj), children, false); + // add the items resulting from ungrouping to the selection + new_select.insert(new_select.end(),children.begin(),children.end()); + item = NULL; // zero out the original pointer, which is no longer valid + } else { + // if not a group, keep in the selection + new_select.push_back(item); + } + } + + set->setList(new_select); +} + +void ObjectSet::ungroup() +{ + if (isEmpty()) { + if(desktop()) + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select a <b>group</b> to ungroup.")); + return; + } + + if (boost::distance(groups()) == 0) { + if(desktop()) + selection_display_message(desktop(), Inkscape::ERROR_MESSAGE, _("<b>No groups</b> to ungroup in the selection.")); + return; + } + + ungroup_impl(this); + if(document()) + DocumentUndo::done(document(), SP_VERB_SELECTION_UNGROUP, + _("Ungroup")); +} + +// TODO replace it with ObjectSet::degroup_list + +/** Replace all groups in the list with their member objects, recursively; returns a new list, frees old */ +std::vector<SPItem*> +sp_degroup_list(std::vector<SPItem*> &items) +{ + std::vector<SPItem*> out; + bool has_groups = false; + for (auto item : items) { + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (!group) { + out.push_back(item); + } else { + has_groups = true; + std::vector<SPItem*> members = sp_item_group_item_list(group); + for (auto member : members) { + out.push_back(member); + } + members.clear(); + } + } + + if (has_groups) { // recurse if we unwrapped a group - it may have contained others + out = sp_degroup_list(out); + } + + return out; +} + + +/** If items in the list have a common parent, return it, otherwise return NULL */ +static SPGroup * +sp_item_list_common_parent_group(const SPItemRange &items) +{ + if (items.empty()) { + return nullptr; + } + SPObject *parent = items.front()->parent; + // Strictly speaking this CAN happen, if user selects <svg> from Inkscape::XML editor + if (!dynamic_cast<SPGroup *>(parent)) { + return nullptr; + } + for (auto item=items.begin();item!=items.end();++item) { + if((*item)==items.front())continue; + if ((*item)->parent != parent) { + return nullptr; + } + } + + return dynamic_cast<SPGroup *>(parent); +} + +/** Finds out the minimum common bbox of the selected items. */ +static Geom::OptRect +enclose_items(std::vector<SPItem*> const &items) +{ + g_assert(!items.empty()); + + Geom::OptRect r; + for (auto item : items) { + r.unionWith(item->documentVisualBounds()); + } + return r; +} + +// TODO determine if this is intentionally different from SPObject::getPrev() +static SPObject *prev_sibling(SPObject *child) +{ + SPObject *prev = nullptr; + if ( child && dynamic_cast<SPGroup *>(child->parent) ) { + prev = child->getPrev(); + } + return prev; +} + +void ObjectSet::raise(bool skip_undo){ + + if(isEmpty()){ + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to raise.")); + return; + } + + SPGroup const *group = sp_item_list_common_parent_group(items()); + if (!group) { + if(desktop()) + selection_display_message(desktop(), Inkscape::ERROR_MESSAGE, _("You cannot raise/lower objects from <b>different groups</b> or <b>layers</b>.")); + return; + } + + std::vector<SPItem*> items_copy(items().begin(), items().end()); + Inkscape::XML::Node *grepr = const_cast<Inkscape::XML::Node *>(items_copy.front()->parent->getRepr()); + + /* Construct reverse-ordered list of selected children. */ + std::vector<SPItem*> rev(items_copy); + sort(rev.begin(),rev.end(),sp_item_repr_compare_position_bool); + + // Determine the common bbox of the selected items. + Geom::OptRect selected = enclose_items(items_copy); + + // Iterate over all objects in the selection (starting from top). + if (selected) { + for (auto child : rev) { + // for each selected object, find the next sibling + for (SPObject *newref = child->getNext(); newref; newref = newref->getNext()) { + // if the sibling is an item AND overlaps our selection, + SPItem *newItem = dynamic_cast<SPItem *>(newref); + if (newItem) { + Geom::OptRect newref_bbox = newItem->documentVisualBounds(); + if ( newref_bbox && selected->intersects(*newref_bbox) ) { + // AND if it's not one of our selected objects, + if ( std::find(items_copy.begin(),items_copy.end(),newref)==items_copy.end()) { + // move the selected object after that sibling + grepr->changeOrder(child->getRepr(), newref->getRepr()); + } + break; + } + } + } + } + } + if(document() && !skip_undo) + DocumentUndo::done(document(), SP_VERB_SELECTION_RAISE, + //TRANSLATORS: "Raise" means "to raise an object" in the undo history + C_("Undo action", "Raise")); +} + + +void ObjectSet::raiseToTop(bool skip_undo) { + if(isEmpty()){ + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to raise.")); + return; + } + + SPGroup const *group = sp_item_list_common_parent_group(items()); + if (!group) { + selection_display_message(desktop(), Inkscape::ERROR_MESSAGE, _("You cannot raise/lower objects from <b>different groups</b> or <b>layers</b>.")); + return; + } + + + std::vector<Inkscape::XML::Node*> rl(xmlNodes().begin(), xmlNodes().end()); + sort(rl.begin(),rl.end(),sp_repr_compare_position_bool); + + for (auto repr : rl) { + repr->setPosition(-1); + } + if (document() && !skip_undo) { + DocumentUndo::done(document(), SP_VERB_SELECTION_TO_FRONT, + _("Raise to top")); + } +} + +void ObjectSet::lower(bool skip_undo){ + if(isEmpty()){ + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to lower.")); + return; + } + + SPGroup const *group = sp_item_list_common_parent_group(items()); + if (!group) { + selection_display_message(desktop(), Inkscape::ERROR_MESSAGE, _("You cannot raise/lower objects from <b>different groups</b> or <b>layers</b>.")); + return; + } + + std::vector<SPItem*> items_copy(items().begin(), items().end()); + Inkscape::XML::Node *grepr = const_cast<Inkscape::XML::Node *>(items_copy.front()->parent->getRepr()); + + // Determine the common bbox of the selected items. + Geom::OptRect selected = enclose_items(items_copy); + + /* Construct direct-ordered list of selected children. */ + std::vector<SPItem*> rev(items_copy); + sort(rev.begin(),rev.end(),sp_item_repr_compare_position_bool); + + // Iterate over all objects in the selection (starting from top). + if (selected) { + for (std::vector<SPItem*>::const_reverse_iterator item=rev.rbegin();item!=rev.rend();++item) { + SPObject *child = *item; + // for each selected object, find the prev sibling + for (SPObject *newref = prev_sibling(child); newref; newref = prev_sibling(newref)) { + // if the sibling is an item AND overlaps our selection, + SPItem *newItem = dynamic_cast<SPItem *>(newref); + if (newItem) { + Geom::OptRect ref_bbox = newItem->documentVisualBounds(); + if ( ref_bbox && selected->intersects(*ref_bbox) ) { + // AND if it's not one of our selected objects, + if (items_copy.end()==std::find(items_copy.begin(),items_copy.end(),newref)) { + // move the selected object before that sibling + SPObject *put_after = prev_sibling(newref); + if (put_after) + grepr->changeOrder(child->getRepr(), put_after->getRepr()); + else + child->getRepr()->setPosition(0); + } + break; + } + } + } + } + } + if(document() && !skip_undo) + DocumentUndo::done(document(), SP_VERB_SELECTION_LOWER, + //TRANSLATORS: "Lower" means "to lower an object" in the undo history + C_("Undo action", "Lower")); +} + + +void ObjectSet::lowerToBottom(bool skip_undo){ + if(!document()) + return; + if (isEmpty()) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to lower to bottom.")); + return; + } + + SPGroup const *group = sp_item_list_common_parent_group(items()); + if (!group) { + selection_display_message(desktop(), Inkscape::ERROR_MESSAGE, _("You cannot raise/lower objects from <b>different groups</b> or <b>layers</b>.")); + return; + } + + std::vector<Inkscape::XML::Node*> rl(xmlNodes().begin(), xmlNodes().end()); + sort(rl.begin(),rl.end(),sp_repr_compare_position_bool); + + for (std::vector<Inkscape::XML::Node*>::const_reverse_iterator l=rl.rbegin();l!=rl.rend();++l) { + gint minpos; + SPObject *pp; + Inkscape::XML::Node *repr = (*l); + pp = document()->getObjectByRepr(repr->parent()); + minpos = 0; + g_assert(dynamic_cast<SPGroup *>(pp)); + for (auto& pc: pp->children) { + if(dynamic_cast<SPItem *>(&pc)) { + break; + } + minpos += 1; + } + repr->setPosition(minpos); + } + if (document() && !skip_undo) { + DocumentUndo::done(document(), SP_VERB_SELECTION_TO_BACK, + _("Lower to bottom")); + } +} + +void ObjectSet::stackUp(bool skip_undo) { + if (isEmpty()) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to stack up.")); + return; + } + + std::vector<SPItem*> selection(items().begin(), items().end()); + sort(selection.begin(), selection.end(), sp_item_repr_compare_position_bool); + + for (auto item: selection | boost::adaptors::reversed) { + if (!item->raiseOne()) { // stop if top was reached + if(document() && !skip_undo) + DocumentUndo::cancel(document()); + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("We hit top.")); + return; + } + } + + if(document() && !skip_undo) + DocumentUndo::done(document(), SP_VERB_SELECTION_STACK_UP, + //TRANSLATORS: undo history: "stack up" means to raise an object of its ordinal position by 1 + C_("Undo action", "stack up")); +} + +void ObjectSet::stackDown(bool skip_undo) { + if (isEmpty()) { + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to stack down.")); + return; + } + + std::vector<SPItem*> selection(items().begin(), items().end()); + sort(selection.begin(), selection.end(), sp_item_repr_compare_position_bool); + + for (auto item: selection) { + if (!item->lowerOne()) { // stop if bottom was reached + if(document() && !skip_undo) + DocumentUndo::cancel(document()); + selection_display_message(desktop(), Inkscape::WARNING_MESSAGE, _("We hit bottom.")); + return; + } + } + + if(document() && !skip_undo) + DocumentUndo::done(document(), SP_VERB_SELECTION_STACK_DOWN, + //TRANSLATORS: undo history: "stack down" means to lower an object of its ordinal position by 1 + C_("Undo action", "stack down")); +} + +void +sp_undo(SPDesktop *desktop, SPDocument *) +{ + // No re/undo while dragging, too dangerous. + if(desktop->getCanvas()->_is_dragging) return; + + if (!DocumentUndo::undo(desktop->getDocument())) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Nothing to undo.")); + } +} + +void +sp_redo(SPDesktop *desktop, SPDocument *) +{ + // No re/undo while dragging, too dangerous. + if(desktop->getCanvas()->_is_dragging) return; + + if (!DocumentUndo::redo(desktop->getDocument())) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Nothing to redo.")); + } +} + +void ObjectSet::cut() +{ + copy(); + deleteItems(); +} + +/** + * \pre item != NULL + */ +SPCSSAttr * +take_style_from_item(SPObject *object) +{ + // CPPIFY: + // This function should only take SPItems, but currently SPString is not an Item. + + // write the complete cascaded style, context-free + SPCSSAttr *css = sp_css_attr_from_object(object, SP_STYLE_FLAG_ALWAYS); + if (css == nullptr) + return nullptr; + + if ((dynamic_cast<SPGroup *>(object) && object->firstChild()) || + (dynamic_cast<SPText *>(object) && object->firstChild() && object->firstChild()->getNext() == nullptr)) { + // if this is a text with exactly one tspan child, merge the style of that tspan as well + // If this is a group, merge the style of its topmost (last) child with style + auto list = object->children | boost::adaptors::reversed; + for (auto& element: list) { + if (element.style ) { + SPCSSAttr *temp = sp_css_attr_from_object(&element, SP_STYLE_FLAG_IFSET); + if (temp) { + sp_repr_css_merge(css, temp); + sp_repr_css_attr_unref(temp); + } + break; + } + } + } + + // Remove black-listed properties (those that should not be used in a default style) + css = sp_css_attr_unset_blacklist(css); + + if (!(dynamic_cast<SPText *>(object) || dynamic_cast<SPTSpan *>(object) || dynamic_cast<SPTRef *>(object) || dynamic_cast<SPString *>(object))) { + // do not copy text properties from non-text objects, it's confusing + css = sp_css_attr_unset_text(css); + } + + + SPItem *item = dynamic_cast<SPItem *>(object); + if (item) { + // FIXME: also transform gradient/pattern fills, by forking? NO, this must be nondestructive + double ex = item->i2doc_affine().descrim(); + if (ex != 1.0) { + css = sp_css_attr_scale(css, ex); + } + } + + return css; +} + +void ObjectSet::copy() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + cm->copy(this); +} + +void sp_selection_paste(SPDesktop *desktop, bool in_place) +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (cm->paste(desktop, in_place)) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_EDIT_PASTE, _("Paste")); + } +} + +void ObjectSet::pasteStyle() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (cm->pasteStyle(this)) { + DocumentUndo::done(document(), SP_VERB_EDIT_PASTE_STYLE, _("Paste style")); + } +} + +void ObjectSet::pastePathEffect() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (cm->pastePathEffect(this)) { + DocumentUndo::done(document(), SP_VERB_EDIT_PASTE_LIVEPATHEFFECT, + _("Paste live path effect")); + } +} + + +static void sp_selection_remove_livepatheffect_impl(SPItem *item) +{ + if ( SPLPEItem *lpeitem = dynamic_cast<SPLPEItem*>(item) ) { + if ( lpeitem->hasPathEffect() ) { + lpeitem->removeAllPathEffects(false); + } + } +} + +void ObjectSet::removeLPE() +{ + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to remove live path effects from.")); + return; + } + auto list= items(); + for (auto itemlist=list.begin();itemlist!=list.end();++itemlist) { + SPItem *item = *itemlist; + + sp_selection_remove_livepatheffect_impl(item); + + } + + if(document()) + DocumentUndo::done(document(), SP_VERB_EDIT_REMOVE_LIVEPATHEFFECT, + _("Remove live path effect")); +} + +void ObjectSet::removeFilter() +{ + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to remove filters from.")); + return; + } + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_unset_property(css, "filter"); + sp_desktop_set_style(this, desktop(), css); + sp_repr_css_attr_unref(css); + if(document()) + DocumentUndo::done(document(), SP_VERB_EDIT_REMOVE_FILTER, + _("Remove filter")); +} + + +void ObjectSet::pasteSize(bool apply_x, bool apply_y) +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (cm->pasteSize(this, false, apply_x, apply_y)) { + DocumentUndo::done(document(), SP_VERB_EDIT_PASTE_SIZE, + _("Paste size")); + } +} + +void ObjectSet::pasteSizeSeparately(bool apply_x, bool apply_y) +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (cm->pasteSize(this, true, apply_x, apply_y)) { + DocumentUndo::done(document(), SP_VERB_EDIT_PASTE_SIZE_SEPARATELY, + _("Paste size separately")); + } +} + +/** + * Ensures that the clones of objects are not modified when moving objects between layers. + * Calls the same function as ungroup + */ +void sp_selection_change_layer_maintain_clones(std::vector<SPItem*> const &items,SPObject *where) +{ + for (auto item : items) { + if (item) { + SPItem *oldparent = dynamic_cast<SPItem *>(item->parent); + SPItem *newparent = dynamic_cast<SPItem *>(where); + sp_item_group_ungroup_handle_clones(item, + (oldparent->i2doc_affine()) + *((newparent->i2doc_affine()).inverse())); + } + } +} + +void ObjectSet::toNextLayer(bool skip_undo) +{ + if(!desktop()) + return; + SPDesktop *dt=desktop(); //TODO make it desktop-independent + + // check if something is selected + if (isEmpty()) { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to move to the layer above.")); + return; + } + + std::vector<SPItem*> items_copy(items().begin(), items().end()); + + bool no_more = false; // Set to true, if no more layers above + SPObject *next=Inkscape::next_layer(dt->currentRoot(), dt->currentLayer()); + if (next) { + clear(); + sp_selection_change_layer_maintain_clones(items_copy,next); + std::vector<Inkscape::XML::Node*> temp_clip; + sp_selection_copy_impl(items_copy, temp_clip, dt->doc()->getReprDoc()); + sp_selection_delete_impl(items_copy, false, false); + next=Inkscape::next_layer(dt->currentRoot(), dt->currentLayer()); // Fixes bug 1482973: crash while moving layers + std::vector<Inkscape::XML::Node*> copied; + if (next) { + copied = sp_selection_paste_impl(dt->getDocument(), next, temp_clip); + } else { + copied = sp_selection_paste_impl(dt->getDocument(), dt->currentLayer(), temp_clip); + no_more = true; + } + setReprList(copied); + if (next) dt->setCurrentLayer(next); + if ( !skip_undo ) { + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_MOVE_TO_NEXT, + _("Raise to next layer")); + } + } else { + no_more = true; + } + + if (no_more) { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No more layers above.")); + } + +} + +void ObjectSet::toPrevLayer(bool skip_undo) +{ + if(!desktop()) + return; + SPDesktop *dt=desktop(); //TODO make it desktop-independent + + // check if something is selected + if (isEmpty()) { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to move to the layer below.")); + return; + } + + std::vector<SPItem*> items_copy(items().begin(), items().end()); + + bool no_more = false; // Set to true, if no more layers below + SPObject *next=Inkscape::previous_layer(dt->currentRoot(), dt->currentLayer()); + if (next) { + clear(); + sp_selection_change_layer_maintain_clones(items_copy,next); + std::vector<Inkscape::XML::Node*> temp_clip; + sp_selection_copy_impl(items_copy, temp_clip, dt->doc()->getReprDoc()); // we're in the same doc, so no need to copy defs + sp_selection_delete_impl(items_copy, false, false); + next=Inkscape::previous_layer(dt->currentRoot(), dt->currentLayer()); // Fixes bug 1482973: crash while moving layers + std::vector<Inkscape::XML::Node*> copied; + if (next) { + copied = sp_selection_paste_impl(dt->getDocument(), next, temp_clip); + } else { + copied = sp_selection_paste_impl(dt->getDocument(), dt->currentLayer(), temp_clip); + no_more = true; + } + setReprList( copied); + if (next) dt->setCurrentLayer(next); + if ( !skip_undo ) { + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_MOVE_TO_PREV, + _("Lower to previous layer")); + } + } else { + no_more = true; + } + + if (no_more) { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No more layers below.")); + } +} + +void ObjectSet::toLayer(SPObject *moveto, bool skip_undo) +{ + if(!document()) + return; + SPDesktop *dt = desktop(); + + // check if something is selected + if (isEmpty()) { + if(dt) + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to move.")); + return; + } + + std::vector<SPItem*> items_copy(items().begin(), items().end()); + + if (moveto) { + clear(); + sp_selection_change_layer_maintain_clones(items_copy,moveto); + std::vector<Inkscape::XML::Node*> temp_clip; + sp_selection_copy_impl(items_copy, temp_clip, document()->getReprDoc()); // we're in the same doc, so no need to copy defs + sp_selection_delete_impl(items_copy, false, false); + std::vector<Inkscape::XML::Node*> copied = sp_selection_paste_impl(document(), moveto, temp_clip); + setReprList(copied); + if (!temp_clip.empty()) temp_clip.clear(); + if (moveto && dt) dt->setCurrentLayer(moveto); + if ( !skip_undo ) { + DocumentUndo::done(document(), SP_VERB_LAYER_MOVE_TO, + _("Move selection to layer")); + } + } +} + +static bool +object_set_contains_original(SPItem *item, ObjectSet *set) +{ + bool contains_original = false; + + SPItem *item_use = item; + SPItem *item_use_first = item; + SPUse *use = dynamic_cast<SPUse *>(item_use); + while (use && item_use && !contains_original) + { + item_use = use->get_original(); + use = dynamic_cast<SPUse *>(item_use); + contains_original |= set->includes(item_use); + if (item_use == item_use_first) + break; + } + + // If it's a tref, check whether the object containing the character + // data is part of the selection + SPTRef *tref = dynamic_cast<SPTRef *>(item); + if (!contains_original && tref) { + contains_original = set->includes(tref->getObjectReferredTo()); + } + + return contains_original; +} + + +static bool +object_set_contains_both_clone_and_original(ObjectSet *set) +{ + bool clone_with_original = false; + auto items = set->items(); + for (auto l=items.begin();l!=items.end() ;++l) { + SPItem *item = *l; + if (item) { + clone_with_original |= object_set_contains_original(item, set); + if (clone_with_original) + break; + } + } + return clone_with_original; +} + +/** Apply matrix to the selection. \a set_i2d is normally true, which means objects are in the +original transform, synced with their reprs, and need to jump to the new transform in one go. A +value of set_i2d==false is only used by seltrans when it's dragging objects live (not outlines); in +that case, items are already in the new position, but the repr is in the old, and this function +then simply updates the repr from item->transform. + */ + +void ObjectSet::applyAffine(Geom::Affine const &affine, bool set_i2d, bool compensate, + bool adjust_transf_center) +{ + if (isEmpty()) + return; + + // For each perspective with a box in selection, check whether all boxes are selected and + // unlink all non-selected boxes. + Persp3D *persp; + Persp3D *transf_persp; + std::list<Persp3D *> plist = perspList(); + for (auto & i : plist) { + persp = (Persp3D *) i; + + if (!persp3d_has_all_boxes_in_selection (persp, this)) { + std::list<SPBox3D *> selboxes = box3DList(persp); + + // create a new perspective as a copy of the current one and link the selected boxes to it + transf_persp = persp3d_create_xml_element (persp->document, persp->perspective_impl); + + for (auto & selboxe : selboxes) + box3d_switch_perspectives(selboxe, persp, transf_persp); + } else { + transf_persp = persp; + } + + persp3d_apply_affine_transformation(transf_persp, affine); + } + auto items_copy = items(); + for (auto l=items_copy.begin();l!=items_copy.end() ;++l) { + SPItem *item = *l; + + if( dynamic_cast<SPRoot *>(item) ) { + // An SVG element cannot have a transform. We could change 'x' and 'y' in response + // to a translation... but leave that for another day. + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Cannot transform an embedded SVG.")); + break; + } + + Geom::Point old_center(0,0); + if (set_i2d && item->isCenterSet()) + old_center = item->getCenter(); + +#if 0 /* Re-enable this once persistent guides have a graphical indication. + At the time of writing, this is the only place to re-enable. */ + sp_item_update_cns(*item, desktop()); +#endif + + // we're moving both a clone and its original or any ancestor in clone chain? + bool transform_clone_with_original = object_set_contains_original(item, this); + + // ...both a text-on-path and its path? + bool transform_textpath_with_path = ((dynamic_cast<SPText *>(item) && item->firstChild() && dynamic_cast<SPTextPath *>(item->firstChild())) + && includes( sp_textpath_get_path_item(dynamic_cast<SPTextPath *>(item->firstChild())) )); + + // ...both a flowtext and its frame? + bool transform_flowtext_with_frame = (dynamic_cast<SPFlowtext *>(item) && includes( dynamic_cast<SPFlowtext *>(item)->get_frame(nullptr))); // (only the first frame is checked so far) + + // ...both an offset and its source? + bool transform_offset_with_source = (dynamic_cast<SPOffset *>(item) && dynamic_cast<SPOffset *>(item)->sourceHref) && includes( sp_offset_get_source(dynamic_cast<SPOffset *>(item)) ); + + // If we're moving a connector, we want to detach it + // from shapes that aren't part of the selection, but + // leave it attached if they are + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + SPPath *path = dynamic_cast<SPPath *>(item); + if (path) { + SPItem *attItem[2] = {nullptr, nullptr}; + path->connEndPair.getAttachedItems(attItem); + for (int n = 0; n < 2; ++n) { + if (!includes(attItem[n])) { + sp_conn_end_detach(item, n); + } + } + } else { + g_assert_not_reached(); + } + } + + // "clones are unmoved when original is moved" preference + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + bool prefs_unmoved = (compensation == SP_CLONE_COMPENSATION_UNMOVED); + bool prefs_parallel = (compensation == SP_CLONE_COMPENSATION_PARALLEL); + + /* If this is a clone and it's selected along with its original, do not move it; + * it will feel the transform of its original and respond to it itself. + * Without this, a clone is doubly transformed, very unintuitive. + * + * Same for textpath if we are also doing ANY transform to its path: do not touch textpath, + * letters cannot be squeezed or rotated anyway, they only refill the changed path. + * Same for linked offset if we are also moving its source: do not move it. */ + if (transform_textpath_with_path) { + // Restore item->transform field from the repr, in case it was changed by seltrans. + item->readAttr( "transform" ); + } else if (transform_flowtext_with_frame) { + // apply the inverse of the region's transform to the <use> so that the flow remains + // the same (even though the output itself gets transformed) + for (auto& region: item->children) { + if (dynamic_cast<SPFlowregion *>(®ion) || dynamic_cast<SPFlowregionExclude *>(®ion)) { + for (auto& itm: region.children) { + SPUse *use = dynamic_cast<SPUse *>(&itm); + if ( use ) { + use->doWriteTransform(item->transform.inverse(), nullptr, compensate); + } + } + } + } + } else if (transform_clone_with_original || transform_offset_with_source) { + // We are transforming a clone along with its original. The below matrix juggling is + // necessary to ensure that they transform as a whole, i.e. the clone's induced + // transform and its move compensation are both cancelled out. + + // restore item->transform field from the repr, in case it was changed by seltrans + item->readAttr( "transform" ); + + // calculate the matrix we need to apply to the clone to cancel its induced transform from its original + Geom::Affine parent2dt; + { + SPItem *parentItem = dynamic_cast<SPItem *>(item->parent); + if (parentItem) { + parent2dt = parentItem->i2dt_affine(); + } else { + g_assert_not_reached(); + } + } + Geom::Affine t = parent2dt * affine * parent2dt.inverse(); + Geom::Affine t_inv = t.inverse(); + Geom::Affine result = t_inv * item->transform * t; + + if (transform_clone_with_original && (prefs_parallel || prefs_unmoved) && affine.isTranslation()) { + // we need to cancel out the move compensation, too + + // find out the clone move, same as in sp_use_move_compensate + Geom::Affine parent; + { + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + parent = use->get_parent_transform(); + } else { + g_assert_not_reached(); + } + } + Geom::Affine clone_move = parent.inverse() * t * parent; + + if (prefs_parallel) { + Geom::Affine move = result * clone_move * t_inv; + item->doWriteTransform(move, &move, compensate); + + } else if (prefs_unmoved) { + //if (dynamic_cast<SPUse *>(sp_use_get_original(dynamic_cast<SPUse *>(item)))) + // clone_move = Geom::identity(); + Geom::Affine move = result * clone_move; + item->doWriteTransform(move, &t, compensate); + } + + } else if (transform_offset_with_source && (prefs_parallel || prefs_unmoved) && affine.isTranslation()){ + Geom::Affine parent = item->transform; + Geom::Affine offset_move = parent.inverse() * t * parent; + + if (prefs_parallel) { + Geom::Affine move = result * offset_move * t_inv; + item->doWriteTransform(move, &move, compensate); + + } else if (prefs_unmoved) { + Geom::Affine move = result * offset_move; + item->doWriteTransform(move, &t, compensate); + } + + } else { + // just apply the result + item->doWriteTransform(result, &t, compensate); + } + + } else { + if (set_i2d) { + item->set_i2d_affine(item->i2dt_affine() * (Geom::Affine)affine); + } + item->doWriteTransform(item->transform, nullptr, compensate); + } + + if (adjust_transf_center) { // The transformation center should not be touched in case of pasting or importing, which is allowed by this if clause + // if we're moving the actual object, not just updating the repr, we can transform the + // center by the same matrix (only necessary for non-translations) + if (set_i2d && item->isCenterSet() && !(affine.isTranslation() || affine.isIdentity())) { + item->setCenter(old_center * affine); + item->updateRepr(); + } + } + } +} + +void ObjectSet::removeTransform() +{ + auto items = xmlNodes(); + for (auto l=items.begin();l!=items.end() ;++l) { + (*l)->removeAttribute("transform"); + } + + if(document()) + DocumentUndo::done(document(), SP_VERB_OBJECT_FLATTEN, + _("Remove transform")); +} + +void ObjectSet::setScaleAbsolute(double x0, double x1,double y0, double y1) +{ + if (isEmpty()) + return; + + Geom::OptRect bbox = visualBounds(); + if ( !bbox ) { + return; + } + + Geom::Translate const p2o(-bbox->min()); + + Geom::Scale const newSize(x1 - x0, + y1 - y0); + Geom::Scale const scale( newSize * Geom::Scale(bbox->dimensions()).inverse() ); + Geom::Translate const o2n(x0, y0); + Geom::Affine const final( p2o * scale * o2n ); + + applyAffine(final); +} + +void ObjectSet::setScaleRelative(Geom::Point const &align, Geom::Scale const &scale) +{ + if (isEmpty()) + return; + + Geom::OptRect bbox = visualBounds(); + + if ( !bbox ) { + return; + } + + // FIXME: ARBITRARY LIMIT: don't try to scale above 1 Mpx, it won't display properly and will crash sooner or later anyway + if ( bbox->dimensions()[Geom::X] * scale[Geom::X] > 1e6 || + bbox->dimensions()[Geom::Y] * scale[Geom::Y] > 1e6 ) + { + return; + } + + Geom::Translate const n2d(-align); + Geom::Translate const d2n(align); + Geom::Affine const final( n2d * scale * d2n ); + applyAffine(final); +} + +void ObjectSet::rotateRelative(Geom::Point const ¢er, double angle_degrees) +{ + Geom::Translate const d2n(center); + Geom::Translate const n2d(-center); + Geom::Rotate const rotate(Geom::Rotate::from_degrees(angle_degrees)); + Geom::Affine const final( Geom::Affine(n2d) * rotate * d2n ); + applyAffine(final); +} + +void ObjectSet::skewRelative(Geom::Point const &align, double dx, double dy) +{ + Geom::Translate const d2n(align); + Geom::Translate const n2d(-align); + Geom::Affine const skew(1, dy, + dx, 1, + 0, 0); + Geom::Affine const final( n2d * skew * d2n ); + applyAffine(final); +} + +void ObjectSet::moveRelative(Geom::Point const &move, bool compensate) +{ + applyAffine(Geom::Affine(Geom::Translate(move)), true, compensate); +} + +void ObjectSet::moveRelative(double dx, double dy) +{ + applyAffine(Geom::Affine(Geom::Translate(dx, dy))); +} + +/** + * Rotates selected objects 90 degrees, either clock-wise or counter-clockwise, depending on the value of ccw. + */ +void ObjectSet::rotate90(bool ccw) +{ + if (isEmpty()) + return; + + auto items_copy = items(); + double y_dir = document() ? document()->yaxisdir() : 1; + Geom::Rotate const rot_90(Geom::Point(0, ccw ? -y_dir : y_dir)); // pos. or neg. rotation, depending on the value of ccw + for (auto l=items_copy.begin();l!=items_copy.end() ;++l) { + SPItem *item = *l; + if (item) { + item->rotate_rel(rot_90); + } else { + g_assert_not_reached(); + } + } + + if (document()) + DocumentUndo::done(document(), + ccw ? SP_VERB_OBJECT_ROTATE_90_CCW : SP_VERB_OBJECT_ROTATE_90_CW, + ccw ? _("Rotate 90\xc2\xb0 CCW") : _("Rotate 90\xc2\xb0 CW")); +} + +void ObjectSet::rotate(gdouble const angle_degrees) +{ + if (isEmpty()) + return; + + boost::optional<Geom::Point> center_ = center(); + if (!center_) { + return; + } + rotateRelative(*center_, angle_degrees); + + if (document()) + DocumentUndo::maybeDone(document(), + ( ( angle_degrees > 0 ) + ? "selector:rotate:ccw" + : "selector:rotate:cw" ), + SP_VERB_CONTEXT_SELECT, + _("Rotate")); +} + +/* + * Selects all the visible items with the same fill and/or stroke color/style as the items in the current selection + * + * Params: + * desktop - set the selection on this desktop + * fill - select objects matching fill + * stroke - select objects matching stroke + */ +void sp_select_same_fill_stroke_style(SPDesktop *desktop, gboolean fill, gboolean stroke, gboolean style) +{ + if (!desktop) { + return; + } + + if (!fill && !stroke && !style) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool onlyvisible = prefs->getBool("/options/kbselection/onlyvisible", true); + bool onlysensitive = prefs->getBool("/options/kbselection/onlysensitive", true); + bool ingroups = TRUE; + std::vector<SPItem*> x,y; + std::vector<SPItem*> all_list = get_all_items(x, desktop->currentRoot(), desktop, onlyvisible, onlysensitive, ingroups, y); + std::vector<SPItem*> all_matches; + + Inkscape::Selection *selection = desktop->getSelection(); + auto items = selection->items(); + + std::vector<SPItem*> tmp; + for (auto iter : all_list) { + if(!SP_IS_GROUP(iter)){ + tmp.push_back(iter); + } + } + all_list=tmp; + + for (auto sel_iter=items.begin();sel_iter!=items.end();++sel_iter) { + SPItem *sel = *sel_iter; + std::vector<SPItem*> matches = all_list; + if (fill && stroke && style) { + matches = sp_get_same_style(sel, matches); + } + else if (fill) { + matches = sp_get_same_style(sel, matches, SP_FILL_COLOR); + } + else if (stroke) { + matches = sp_get_same_style(sel, matches, SP_STROKE_COLOR); + } + else if (style) { + matches = sp_get_same_style(sel, matches,SP_STROKE_STYLE_ALL); + } + all_matches.insert(all_matches.end(), matches.begin(),matches.end()); + } + + selection->clear(); + selection->setList(all_matches); + +} + + +/* + * Selects all the visible items with the same object type as the items in the current selection + * + * Params: + * desktop - set the selection on this desktop + */ +void sp_select_same_object_type(SPDesktop *desktop) +{ + if (!desktop) { + return; + } + + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool onlyvisible = prefs->getBool("/options/kbselection/onlyvisible", true); + bool onlysensitive = prefs->getBool("/options/kbselection/onlysensitive", true); + bool ingroups = TRUE; + std::vector<SPItem*> x,y; + std::vector<SPItem*> all_list = get_all_items(x, desktop->currentRoot(), desktop, onlyvisible, onlysensitive, ingroups, y); + std::vector<SPItem*> matches = all_list; + + Inkscape::Selection *selection = desktop->getSelection(); + + auto items= selection->items(); + for (auto sel_iter=items.begin();sel_iter!=items.end();++sel_iter) { + SPItem *sel = *sel_iter; + if (sel) { + matches = sp_get_same_object_type(sel, matches); + } else { + g_assert_not_reached(); + } + } + + selection->clear(); + selection->setList(matches); + +} + + + +/* + * Find all items in src list that have the same fill or stroke style as sel + * Return the list of matching items + */ +std::vector<SPItem*> sp_get_same_fill_or_stroke_color(SPItem *sel, std::vector<SPItem*> &src, SPSelectStrokeStyleType type) +{ + std::vector<SPItem*> matches ; + gboolean match = false; + + SPIPaint *sel_paint = sel->style->getFillOrStroke(type == SP_FILL_COLOR); + + for (std::vector<SPItem*>::const_reverse_iterator i=src.rbegin();i!=src.rend();++i) { + SPItem *iter = *i; + if (iter) { + SPIPaint *iter_paint = iter->style->getFillOrStroke(type == SP_FILL_COLOR); + match = false; + if (sel_paint->isColor() && iter_paint->isColor() // color == color comparison doesn't seem to work here. + && (sel_paint->value.color.toRGBA32(1.0) == iter_paint->value.color.toRGBA32(1.0))) { + match = true; + } else if (sel_paint->isPaintserver() && iter_paint->isPaintserver()) { + + SPPaintServer *sel_server = + (type == SP_FILL_COLOR) ? sel->style->getFillPaintServer() : sel->style->getStrokePaintServer(); + SPPaintServer *iter_server = + (type == SP_FILL_COLOR) ? iter->style->getFillPaintServer() : iter->style->getStrokePaintServer(); + + if ((dynamic_cast<SPLinearGradient *>(sel_server) || dynamic_cast<SPRadialGradient *>(sel_server) || + (dynamic_cast<SPGradient *>(sel_server) && dynamic_cast<SPGradient *>(sel_server)->getVector()->isSwatch())) + && + (dynamic_cast<SPLinearGradient *>(iter_server) || dynamic_cast<SPRadialGradient *>(iter_server) || + (dynamic_cast<SPGradient *>(iter_server) && dynamic_cast<SPGradient *>(iter_server)->getVector()->isSwatch()))) { + SPGradient *sel_vector = dynamic_cast<SPGradient *>(sel_server)->getVector(); + SPGradient *iter_vector = dynamic_cast<SPGradient *>(iter_server)->getVector(); + if (sel_vector == iter_vector) { + match = true; + } + + } else if (dynamic_cast<SPPattern *>(sel_server) && dynamic_cast<SPPattern *>(iter_server)) { + SPPattern *sel_pat = dynamic_cast<SPPattern *>(sel_server)->rootPattern(); + SPPattern *iter_pat = dynamic_cast<SPPattern *>(iter_server)->rootPattern(); + if (sel_pat == iter_pat) { + match = true; + } + } + } else if (sel_paint->isNone() && iter_paint->isNone()) { + match = true; + } else if (sel_paint->isNoneSet() && iter_paint->isNoneSet()) { + match = true; + } + + if (match) { + matches.push_back(iter); + } + } else { + g_assert_not_reached(); + } + } + + return matches; +} + +static bool item_type_match (SPItem *i, SPItem *j) +{ + if ( dynamic_cast<SPRect *>(i)) { + return ( dynamic_cast<SPRect *>(j) ); + + } else if (dynamic_cast<SPGenericEllipse *>(i)) { + return (dynamic_cast<SPGenericEllipse *>(j)); + + } else if (dynamic_cast<SPStar *>(i) || dynamic_cast<SPPolygon *>(i)) { + return (dynamic_cast<SPStar *>(j) || dynamic_cast<SPPolygon *>(j)) ; + + } else if (dynamic_cast<SPSpiral *>(i)) { + return (dynamic_cast<SPSpiral *>(j)); + + } else if (dynamic_cast<SPPath *>(i) || dynamic_cast<SPLine *>(i) || dynamic_cast<SPPolyLine *>(i)) { + return (dynamic_cast<SPPath *>(j) || dynamic_cast<SPLine *>(j) || dynamic_cast<SPPolyLine *>(j)); + + } else if (dynamic_cast<SPText *>(i) || dynamic_cast<SPFlowtext *>(i) || dynamic_cast<SPTSpan *>(i) || dynamic_cast<SPTRef *>(i) || dynamic_cast<SPString *>(i)) { + return (dynamic_cast<SPText *>(j) || dynamic_cast<SPFlowtext *>(j) || dynamic_cast<SPTSpan *>(j) || dynamic_cast<SPTRef *>(j) || dynamic_cast<SPString *>(j)); + + } else if (dynamic_cast<SPUse *>(i)) { + return (dynamic_cast<SPUse *>(j)) ; + + } else if (dynamic_cast<SPImage *>(i)) { + return (dynamic_cast<SPImage *>(j)); + + } else if (dynamic_cast<SPOffset *>(i) && dynamic_cast<SPOffset *>(i)->sourceHref) { // Linked offset + return (dynamic_cast<SPOffset *>(j) && dynamic_cast<SPOffset *>(j)->sourceHref); + + } else if (dynamic_cast<SPOffset *>(i) && !dynamic_cast<SPOffset *>(i)->sourceHref) { // Dynamic offset + return (dynamic_cast<SPOffset *>(j) && !dynamic_cast<SPOffset *>(j)->sourceHref); + + } + + return false; +} + +/* + * Find all items in src list that have the same object type as sel by type + * Return the list of matching items + */ +std::vector<SPItem*> sp_get_same_object_type(SPItem *sel, std::vector<SPItem*> &src) +{ + std::vector<SPItem*> matches; + + for (std::vector<SPItem*>::const_reverse_iterator i=src.rbegin();i!=src.rend();++i) { + SPItem *item = *i; + if (item && item_type_match(sel, item) && !item->cloned) { + matches.push_back(item); + } + } + return matches; +} + +/* + * Find all items in src list that have the same stroke style as sel by type + * Return the list of matching items + */ +std::vector<SPItem*> sp_get_same_style(SPItem *sel, std::vector<SPItem*> &src, SPSelectStrokeStyleType type) +{ + std::vector<SPItem*> matches; + bool match = false; + + SPStyle *sel_style = sel->style; + + if (type == SP_FILL_COLOR || type == SP_STYLE_ALL) { + src = sp_get_same_fill_or_stroke_color(sel, src, SP_FILL_COLOR); + } + if (type == SP_STROKE_COLOR || type == SP_STYLE_ALL) { + src = sp_get_same_fill_or_stroke_color(sel, src, SP_STROKE_COLOR); + } + + /* + * Stroke width needs to handle transformations, so call this function + * to get the transformed stroke width + */ + std::vector<SPItem*> objects; + SPStyle *sel_style_for_width = nullptr; + if (type == SP_STROKE_STYLE_WIDTH || type == SP_STROKE_STYLE_ALL || type==SP_STYLE_ALL ) { + objects.push_back(sel); + sel_style_for_width = new SPStyle(SP_ACTIVE_DOCUMENT); + objects_query_strokewidth (objects, sel_style_for_width); + } + bool match_g; + for (auto iter : src) { + if (iter) { + match_g=true; + SPStyle *iter_style = iter->style; + match = true; + + if (type == SP_STROKE_STYLE_WIDTH|| type == SP_STROKE_STYLE_ALL|| type==SP_STYLE_ALL) { + match = (sel_style->stroke_width.set == iter_style->stroke_width.set); + if (sel_style->stroke_width.set && iter_style->stroke_width.set) { + std::vector<SPItem*> objects; + objects.insert(objects.begin(),iter); + SPStyle tmp_style(SP_ACTIVE_DOCUMENT); + objects_query_strokewidth (objects, &tmp_style); + + if (sel_style_for_width) { + match = (sel_style_for_width->stroke_width.computed == tmp_style.stroke_width.computed); + } + } + } + match_g = match_g && match; + if (type == SP_STROKE_STYLE_DASHES|| type == SP_STROKE_STYLE_ALL || type==SP_STYLE_ALL) { + match = (sel_style->stroke_dasharray.set == iter_style->stroke_dasharray.set); + if (sel_style->stroke_dasharray.set && iter_style->stroke_dasharray.set) { + match = (sel_style->stroke_dasharray == iter_style->stroke_dasharray); + } + } + match_g = match_g && match; + if (type == SP_STROKE_STYLE_MARKERS|| type == SP_STROKE_STYLE_ALL|| type==SP_STYLE_ALL) { + match = true; + int len = sizeof(sel_style->marker)/sizeof(SPIString); + for (int i = 0; i < len; i++) { + if (g_strcmp0(sel_style->marker_ptrs[i]->value(), + iter_style->marker_ptrs[i]->value())) { + match = false; + break; + } + } + } + match_g = match_g && match; + if (match_g) { + while (iter->cloned) iter=dynamic_cast<SPItem *>(iter->parent); + matches.insert(matches.begin(),iter); + } + } else { + g_assert_not_reached(); + } + } + + if( sel_style_for_width != nullptr ) delete sel_style_for_width; + return matches; +} + +// helper function: +static +Geom::Point +cornerFarthestFrom(Geom::Rect const &r, Geom::Point const &p){ + Geom::Point m = r.midpoint(); + unsigned i = 0; + if (p[X] < m[X]) { + i = 1; + } + if (p[Y] < m[Y]) { + i = 3 - i; + } + return r.corner(i); +} + +/** +\param angle the angle in "angular pixels", i.e. how many visible pixels must move the outermost point of the rotated object +*/ +void ObjectSet::rotateScreen(double angle) +{ + if (isEmpty()||!desktop()) + return; + + Geom::OptRect bbox = visualBounds(); + boost::optional<Geom::Point> center_ = center(); + + if ( !bbox || !center_ ) { + return; + } + + gdouble const zoom = desktop()->current_zoom(); + gdouble const zmove = angle / zoom; + gdouble const r = Geom::L2(cornerFarthestFrom(*bbox, *center_) - *center_); + + gdouble const zangle = 180 * atan2(zmove, r) / M_PI; + + rotateRelative(*center_, zangle); + + DocumentUndo::maybeDone(document(), + ( (angle > 0) + ? "selector:rotate:ccw" + : "selector:rotate:cw" ), + SP_VERB_CONTEXT_SELECT, + _("Rotate by pixels")); +} + +void ObjectSet::scale(double grow) +{ + if (isEmpty()) + return; + + Geom::OptRect bbox = visualBounds(); + if (!bbox) { + return; + } + + Geom::Point const center_(bbox->midpoint()); + + // you can't scale "do nizhe pola" (below zero) + double const max_len = bbox->maxExtent(); + if ( max_len + grow <= 1e-3 ) { + return; + } + + double const times = 1.0 + grow / max_len; + setScaleRelative(center_, Geom::Scale(times, times)); + + if (document()) { + DocumentUndo::maybeDone(document(), + ( (grow > 0) + ? "selector:scale:larger" + : "selector:scale:smaller" ), + SP_VERB_CONTEXT_SELECT, + _("Scale")); + } +} + +void ObjectSet::scaleScreen(double grow_pixels) +{ + if(!desktop()) + return; + scale(grow_pixels / desktop()->current_zoom()); +} + +void ObjectSet::scaleTimes(double times) +{ + if (isEmpty()) + return; + + Geom::OptRect sel_bbox = visualBounds(); + + if (!sel_bbox) { + return; + } + + Geom::Point const center_(sel_bbox->midpoint()); + setScaleRelative(center_, Geom::Scale(times, times)); + DocumentUndo::done(document(), SP_VERB_CONTEXT_SELECT, + _("Scale by whole factor")); +} + +void ObjectSet::move(double dx, double dy) +{ + if (isEmpty()) { + return; + } + + moveRelative(dx, dy); + + if (document()) { + if (dx == 0) { + DocumentUndo::maybeDone(document(), "selector:move:vertical", SP_VERB_CONTEXT_SELECT, + _("Move vertically")); + } else if (dy == 0) { + DocumentUndo::maybeDone(document(), "selector:move:horizontal", SP_VERB_CONTEXT_SELECT, + _("Move horizontally")); + } else { + DocumentUndo::done(document(), SP_VERB_CONTEXT_SELECT, + _("Move")); + } + } +} + +void ObjectSet::moveScreen(double dx, double dy) +{ + if (isEmpty() || !desktop()) { + return; + } + + // same as sp_selection_move but divide deltas by zoom factor + gdouble const zoom = desktop()->current_zoom(); + gdouble const zdx = dx / zoom; + gdouble const zdy = dy / zoom; + moveRelative(zdx, zdy); + + SPDocument *doc = document(); + if (dx == 0) { + DocumentUndo::maybeDone(doc, "selector:move:vertical", SP_VERB_CONTEXT_SELECT, + _("Move vertically by pixels")); + } else if (dy == 0) { + DocumentUndo::maybeDone(doc, "selector:move:horizontal", SP_VERB_CONTEXT_SELECT, + _("Move horizontally by pixels")); + } else { + DocumentUndo::done(doc, SP_VERB_CONTEXT_SELECT, + _("Move")); + } +} + + + +struct Forward { + typedef SPObject *Iterator; + + static Iterator children(SPObject *o) { return o->firstChild(); } + static Iterator siblings_after(SPObject *o) { return o->getNext(); } + static void dispose(Iterator i) {} + + static SPObject *object(Iterator i) { return i; } + static Iterator next(Iterator i) { return i->getNext(); } + static bool isNull(Iterator i) {return (!i);} +}; + +struct ListReverse { + typedef std::list<SPObject *> *Iterator; + + static Iterator children(SPObject *o) { + return make_list(o, nullptr); + } + static Iterator siblings_after(SPObject *o) { + return make_list(o->parent, o); + } + static void dispose(Iterator i) { + delete i; + } + + static SPObject *object(Iterator i) { + return *(i->begin()); + } + static Iterator next(Iterator i) { i->pop_front(); return i; } + + static bool isNull(Iterator i) {return i->empty();} + +private: + static std::list<SPObject *> *make_list(SPObject *object, SPObject *limit) { + auto list = new std::list<SPObject *>; + for (auto &child: object->children) { + if (&child == limit) { + break; + } + list->push_front(&child); + } + return list; + } +}; + + + +template <typename D> +SPItem *next_item(SPDesktop *desktop, std::vector<SPObject *> &path, SPObject *root, + bool only_in_viewport, PrefsSelectionContext inlayer, bool onlyvisible, bool onlysensitive) +{ + typename D::Iterator children; + typename D::Iterator iter; + + SPItem *found=nullptr; + + if (!path.empty()) { + SPObject *object=path.back(); + path.pop_back(); + g_assert(object->parent == root); + if (desktop->isLayer(object)) { + found = next_item<D>(desktop, path, object, only_in_viewport, inlayer, onlyvisible, onlysensitive); + } + iter = children = D::siblings_after(object); + } else { + iter = children = D::children(root); + } + + while ( !D::isNull(iter) && !found ) { + SPObject *object=D::object(iter); + if (desktop->isLayer(object)) { + if (PREFS_SELECTION_LAYER != inlayer) { // recurse into sublayers + std::vector<SPObject *> empt; + found = next_item<D>(desktop, empt, object, only_in_viewport, inlayer, onlyvisible, onlysensitive); + } + } else { + SPItem *item = dynamic_cast<SPItem *>(object); + if ( item && + ( !only_in_viewport || desktop->isWithinViewport(item) ) && + ( !onlyvisible || !desktop->itemIsHidden(item)) && + ( !onlysensitive || !item->isLocked()) && + !desktop->isLayer(item) ) + { + found = item; + } + } + iter = D::next(iter); + } + + D::dispose(children); + + return found; +} + + +template <typename D> +SPItem *next_item_from_list(SPDesktop *desktop, std::vector<SPItem*> const &items, + SPObject *root, bool only_in_viewport, PrefsSelectionContext inlayer, bool onlyvisible, bool onlysensitive) +{ + SPObject *current=root; + for(auto item : items) { + if ( root->isAncestorOf(item) && + ( !only_in_viewport || desktop->isWithinViewport(item) ) ) + { + current = item; + break; + } + } + + std::vector<SPObject *> path; + while ( current != root ) { + path.push_back(current); + current = current->parent; + } + + SPItem *next; + // first, try from the current object + next = next_item<D>(desktop, path, root, only_in_viewport, inlayer, onlyvisible, onlysensitive); + + if (!next) { // if we ran out of objects, start over at the root + std::vector<SPObject *> empt; + next = next_item<D>(desktop, empt, root, only_in_viewport, inlayer, onlyvisible, onlysensitive); + } + + return next; +} + +void +sp_selection_item_next(SPDesktop *desktop) +{ + g_return_if_fail(desktop != nullptr); + Inkscape::Selection *selection = desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + PrefsSelectionContext inlayer = (PrefsSelectionContext)prefs->getInt("/options/kbselection/inlayer", PREFS_SELECTION_LAYER); + bool onlyvisible = prefs->getBool("/options/kbselection/onlyvisible", true); + bool onlysensitive = prefs->getBool("/options/kbselection/onlysensitive", true); + + SPObject *root; + if (PREFS_SELECTION_ALL != inlayer) { + root = selection->activeContext(); + } else { + root = desktop->currentRoot(); + } + + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + SPItem *item=next_item_from_list<Forward>(desktop, vec, root, SP_CYCLING == SP_CYCLE_VISIBLE, inlayer, onlyvisible, onlysensitive); + + if (item) { + selection->set(item, PREFS_SELECTION_LAYER_RECURSIVE == inlayer); + if ( SP_CYCLING == SP_CYCLE_FOCUS ) { + scroll_to_show_item(desktop, item); + } + } +} + +void +sp_selection_item_prev(SPDesktop *desktop) +{ + SPDocument *document = desktop->getDocument(); + g_return_if_fail(document != nullptr); + g_return_if_fail(desktop != nullptr); + Inkscape::Selection *selection = desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + PrefsSelectionContext inlayer = (PrefsSelectionContext) prefs->getInt("/options/kbselection/inlayer", PREFS_SELECTION_LAYER); + bool onlyvisible = prefs->getBool("/options/kbselection/onlyvisible", true); + bool onlysensitive = prefs->getBool("/options/kbselection/onlysensitive", true); + + SPObject *root; + if (PREFS_SELECTION_ALL != inlayer) { + root = selection->activeContext(); + } else { + root = desktop->currentRoot(); + } + + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + SPItem *item=next_item_from_list<ListReverse>(desktop, vec, root, SP_CYCLING == SP_CYCLE_VISIBLE, inlayer, onlyvisible, onlysensitive); + + if (item) { + selection->set(item, PREFS_SELECTION_LAYER_RECURSIVE == inlayer); + if ( SP_CYCLING == SP_CYCLE_FOCUS ) { + scroll_to_show_item(desktop, item); + } + } +} + +void sp_selection_next_patheffect_param(SPDesktop * dt) +{ + if (!dt) return; + + Inkscape::Selection *selection = dt->getSelection(); + if ( selection && !selection->isEmpty() ) { + SPItem *item = selection->singleItem(); + if ( SPLPEItem *lpeitem = dynamic_cast<SPLPEItem*>(item) ) { + if (lpeitem->hasPathEffect()) { + lpeitem->editNextParamOncanvas(dt); + } else { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("The selection has no applied path effect.")); + } + } + } +} + +/*bool has_path_recursive(SPObject *obj) +{ + if (!obj) return false; + if (SP_IS_PATH(obj)) { + return true; + } + if (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj)) { + for (SPObject *c = obj->children; c; c = c->next) { + if (has_path_recursive(c)) return true; + } + } + return false; +}*/ + +void ObjectSet::editMask(bool /*clip*/) +{ + return; +} + + + + +/** + * If \a item is not entirely visible then adjust visible area to centre on the centre on of + * \a item. + */ +void scroll_to_show_item(SPDesktop *desktop, SPItem *item) +{ + Geom::Rect dbox = desktop->get_display_area(); + Geom::OptRect sbox = item->desktopVisualBounds(); + + if ( sbox && dbox.contains(*sbox) == false ) { + Geom::Point const s_dt = sbox->midpoint(); + Geom::Point const s_w = desktop->d2w(s_dt); + Geom::Point const d_dt = dbox.midpoint(); + Geom::Point const d_w = desktop->d2w(d_dt); + Geom::Point const moved_w( d_w - s_w ); + desktop->scroll_relative(moved_w); + } +} + +void ObjectSet::clone() +{ + if (document() == nullptr) { + return; + } + + Inkscape::XML::Document *xml_doc = document()->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an <b>object</b> to clone.")); + return; + } + + std::vector<Inkscape::XML::Node*> reprs(xmlNodes().begin(), xmlNodes().end()); + + clear(); + + // sorting items from different parents sorts each parent's subset without possibly mixing them, just what we need + sort(reprs.begin(),reprs.end(),sp_repr_compare_position_bool); + + std::vector<Inkscape::XML::Node*> newsel; + + for(auto sel_repr : reprs){ + Inkscape::XML::Node *parent = sel_repr->parent(); + + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + gchar *href_str = g_strdup_printf("#%s", sel_repr->attribute("id")); + clone->setAttribute("xlink:href", href_str); + g_free(href_str); + + clone->setAttribute("inkscape:transform-center-x", sel_repr->attribute("inkscape:transform-center-x")); + clone->setAttribute("inkscape:transform-center-y", sel_repr->attribute("inkscape:transform-center-y")); + + // add the new clone to the top of the original's parent + parent->appendChild(clone); + + newsel.push_back(clone); + Inkscape::GC::release(clone); + } + + DocumentUndo::done(document(), SP_VERB_EDIT_CLONE, + C_("Action", "Clone")); + + setReprList(newsel); +} + +void ObjectSet::relink() +{ + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>clones</b> to relink.")); + return; + } + + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + const gchar *newid = cm->getFirstObjectID(); + if (!newid) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Copy an <b>object</b> to clipboard to relink clones to.")); + return; + } + gchar *newref = g_strdup_printf("#%s", newid); + + // Get a copy of current selection. + bool relinked = false; + auto items_= items(); + for (auto i=items_.begin();i!=items_.end();++i){ + SPItem *item = *i; + + if (dynamic_cast<SPUse *>(item)) { + item->setAttribute("xlink:href", newref); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + relinked = true; + } + } + + g_free(newref); + + if (!relinked) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No clones to relink</b> in the selection.")); + } else { + DocumentUndo::done(document(), SP_VERB_EDIT_UNLINK_CLONE, + _("Relink clone")); + } +} + + +bool ObjectSet::unlink(const bool skip_undo) +{ + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>clones</b> to unlink.")); + return false; + } + + // Get a copy of current selection. + std::vector<SPItem*> new_select; + bool unlinked = false; + std::vector<SPItem *> items_(items().begin(), items().end()); + + for (auto i=items_.rbegin();i!=items_.rend();++i){ + SPItem *item = *i; + + ObjectSet tmp_set(document()); + tmp_set.set(item); + auto *clip_obj = item->getClipObject(); + auto *mask_obj = item->getMaskObject(); + if (clip_obj) { + SPUse *clipuse = dynamic_cast<SPUse *>(clip_obj); + if (clipuse) { + tmp_set.unsetMask(true,true); + unlinked = tmp_set.unlink(true) || unlinked; + tmp_set.setMask(true,false,true); + } + new_select.push_back(tmp_set.singleItem()); + } else if (mask_obj) { + SPUse *maskuse = dynamic_cast<SPUse *>(mask_obj); + if (maskuse) { + tmp_set.unsetMask(false,true); + unlinked = tmp_set.unlink(true) || unlinked; + tmp_set.setMask(false,false,true); + } + new_select.push_back(tmp_set.singleItem()); + } else { + if (dynamic_cast<SPText *>(item)) { + SPObject *tspan = sp_tref_convert_to_tspan(item); + + if (tspan) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + + // Set unlink to true, and fall into the next if which + // will include this text item in the new selection + unlinked = true; + } + + if (!(dynamic_cast<SPUse *>(item) || dynamic_cast<SPTRef *>(item))) { + // keep the non-use item in the new selection + new_select.push_back(item); + continue; + } + + SPItem *unlink = nullptr; + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + unlink = use->unlink(); + // Unable to unlink use (external or invalid href?) + if (!unlink) { + new_select.push_back(item); + continue; + } + } else /*if (SP_IS_TREF(use))*/ { + unlink = dynamic_cast<SPItem *>(sp_tref_convert_to_tspan(item)); + g_assert(unlink != nullptr); + } + + unlinked = true; + // Add ungrouped items to the new selection. + new_select.push_back(unlink); + } + } + + if (!new_select.empty()) { // set new selection + clear(); + setList(new_select); + } + if (!unlinked) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No clones to unlink</b> in the selection.")); + } + + if (!skip_undo) { + DocumentUndo::done(document(), SP_VERB_EDIT_UNLINK_CLONE, + _("Unlink clone")); + } + return unlinked; +} + +bool ObjectSet::unlinkRecursive(const bool skip_undo, const bool force) { + if (isEmpty()){ + if (desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>clones</b> to unlink.")); + return false; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool pathoperationsunlink = prefs->getBool("/options/pathoperationsunlink/value", true); + if (!force && !pathoperationsunlink) { + return false; + } + bool unlinked = false; + ObjectSet tmp_set(document()); + std::vector<SPItem*> items_(items().begin(), items().end()); + for (auto& it:items_) { + tmp_set.set(it); + unlinked = tmp_set.unlink(true) || unlinked; + it = tmp_set.singleItem(); + if (SP_IS_GROUP(it)) { + std::vector<SPObject*> c = it->childList(false); + tmp_set.setList(c); + unlinked = tmp_set.unlinkRecursive(skip_undo, force) || unlinked; + } + } + if (!unlinked) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No clones to unlink</b> in the selection.")); + } + if (!skip_undo) { + DocumentUndo::done(document(), SP_VERB_EDIT_UNLINK_CLONE_RECURSIVE, + _("Unlink clone recursively")); + } + setList(items_); + return unlinked; +} + +void ObjectSet::removeLPESRecursive(bool keep_paths) { + if (isEmpty()){ + return; + } + + ObjectSet tmp_set(document()); + std::vector<SPItem*> items_(items().begin(), items().end()); + for (auto& it:items_) { + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(it); + SPGroup *spgroup = dynamic_cast<SPGroup *>(it); + if (spgroup) { + std::vector<SPObject*> c = spgroup->childList(false); + tmp_set.setList(c); + tmp_set.removeLPESRecursive(keep_paths); + } else if (splpeitem) { + splpeitem->removeAllPathEffects(keep_paths); + } + } + setList(items_); +} + +void ObjectSet::cloneOriginal() +{ + SPItem *item = singleItem(); + + gchar const *error = _("Select a <b>clone</b> to go to its original. Select a <b>linked offset</b> to go to its source. Select a <b>text on path</b> to go to the path. Select a <b>flowed text</b> to go to its frame."); + + // Check if other than two objects are selected + + auto items_= items(); + if (boost::distance(items_) != 1 || !item) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, error); + return; + } + + SPItem *original = nullptr; + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + original = use->get_original(); + } else { + SPOffset *offset = dynamic_cast<SPOffset *>(item); + if (offset && offset->sourceHref) { + original = sp_offset_get_source(offset); + } else { + SPText *text = dynamic_cast<SPText *>(item); + SPTextPath *textpath = (text) ? dynamic_cast<SPTextPath *>(text->firstChild()) : nullptr; + if (text && textpath) { + original = sp_textpath_get_path_item(textpath); + } else { + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item); + if (flowtext) { + original = flowtext->get_frame(nullptr); // first frame only + } + } + } + } + + if (original == nullptr) { // it's an object that we don't know what to do with + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, error); + return; + } + + if (!original) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>Cannot find</b> the object to select (orphaned clone, offset, textpath, flowed text?)")); + return; + } + + for (SPObject *o = original; o && !dynamic_cast<SPRoot *>(o); o = o->parent) { + if (dynamic_cast<SPDefs *>(o)) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The object you're trying to select is <b>not visible</b> (it is in <defs>)")); + return; + } + } + + if (original) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool highlight = prefs->getBool("/options/highlightoriginal/value"); + if (highlight) { + Geom::OptRect a = item->desktopVisualBounds(); + Geom::OptRect b = original->desktopVisualBounds(); + if ( a && b && desktop()) { + // draw a flashing line between the objects + SPCurve *curve = new SPCurve(); + curve->moveto(a->midpoint()); + curve->lineto(b->midpoint()); + + SPCanvasItem * canvasitem = sp_canvas_bpath_new(desktop()->getTempGroup(), curve); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvasitem), 0x0000ddff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT, 5, 3); + sp_canvas_item_show(canvasitem); + curve->unref(); + desktop()->add_temporary_canvasitem(canvasitem, 1000); + } + } + + clear(); + set(original); + if (SP_CYCLING == SP_CYCLE_FOCUS && desktop()) { + scroll_to_show_item(desktop(), original); + } + } +} + +/** +* This applies the Fill Between Many LPE, and has it refer to the selection. +*/ +void ObjectSet::cloneOriginalPathLPE(bool allow_transforms) +{ + + Inkscape::SVGOStringStream os; + SPObject * firstItem = nullptr; + auto items_= items(); + bool multiple = false; + for (auto i=items_.begin();i!=items_.end();++i){ + if (SP_IS_SHAPE(*i) || SP_IS_TEXT(*i) || SP_IS_GROUP(*i)) { + if (firstItem) { + os << "|"; + multiple = true; + } else { + firstItem = SP_ITEM(*i); + } + os << '#' << SP_ITEM(*i)->getId() << ",0,1"; + } + } + if (firstItem) { + Inkscape::XML::Document *xml_doc = document()->getReprDoc(); + SPObject *parent = firstItem->parent; + // create the LPE + Inkscape::XML::Node *lpe_repr = xml_doc->createElement("inkscape:path-effect"); + if (multiple) { + lpe_repr->setAttribute("effect", "fill_between_many"); + lpe_repr->setAttributeOrRemoveIfEmpty("linkedpaths", os.str()); + lpe_repr->setAttribute("applied", "true"); + } else { + lpe_repr->setAttribute("effect", "clone_original"); + lpe_repr->setAttribute("linkeditem", ((Glib::ustring)"#" + (Glib::ustring)firstItem->getId())); + } + gchar const *method_str = allow_transforms ? "d" : "bsplinespiro"; + lpe_repr->setAttribute("method", method_str); + gchar const *allow_transforms_str = allow_transforms ? "true" : "false"; + lpe_repr->setAttribute("allow_transforms", allow_transforms_str); + document()->getDefs()->getRepr()->addChild(lpe_repr, nullptr); // adds to <defs> and assigns the 'id' attribute + std::string lpe_id_href = std::string("#") + lpe_repr->attribute("id"); + Inkscape::GC::release(lpe_repr); + Inkscape::XML::Node* clone = nullptr; + SPGroup *firstgroup = dynamic_cast<SPGroup *>(firstItem); + if (firstgroup) { + if (!multiple) { + clone = firstgroup->getRepr()->duplicate(xml_doc); + } + } else { + // create the new path + clone = xml_doc->createElement("svg:path"); + clone->setAttribute("d", "M 0 0"); + + } + if (clone) { + // add the new clone to the top of the original's parent + parent->appendChildRepr(clone); + // select the new object: + set(clone); + Inkscape::GC::release(clone); + SPObject *clone_obj = document()->getObjectById(clone->attribute("id")); + SPLPEItem *clone_lpeitem = dynamic_cast<SPLPEItem *>(clone_obj); + if (clone_lpeitem) { + clone_lpeitem->addPathEffect(lpe_id_href, false); + } + if (multiple) { + DocumentUndo::done(document(), SP_VERB_EDIT_CLONE_ORIGINAL_PATH_LPE, _("Fill between many")); + } else { + DocumentUndo::done(document(), SP_VERB_EDIT_CLONE_ORIGINAL_PATH_LPE, _("Clone original")); + } + } + } else { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select path(s) to fill.")); + } +} + +void ObjectSet::toMarker(bool apply) +{ + // sp_selection_tile has similar code + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if (desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>object(s)</b> to convert to marker.")); + return; + } + + doc->ensureUpToDate(); + Geom::OptRect r = visualBounds(); + if (!r) { + return; + } + + std::vector<SPItem*> items_(items().begin(), items().end()); + + // bottommost object, after sorting + SPObject *parent = items_.front()->parent; + + Geom::Affine parent_transform; + { + SPItem *parentItem = dynamic_cast<SPItem *>(parent); + if (parent) { + parent_transform = parentItem->i2doc_affine(); + } else { + g_assert_not_reached(); + } + } + + // Create a list of duplicates, to be pasted inside marker element. + std::vector<Inkscape::XML::Node*> repr_copies; + for (std::vector<SPItem*>::const_reverse_iterator i=items_.rbegin();i!=items_.rend();++i){ + Inkscape::XML::Node *dup = (*i)->getRepr()->duplicate(xml_doc); + repr_copies.push_back(dup); + } + + Geom::Rect bbox(r->min() * doc->dt2doc(), r->max() * doc->dt2doc()); + + // calculate the transform to be applied to objects to move them to 0,0 + // (alternative would be to define viewBox or set overflow:visible) + Geom::Affine const move = Geom::Translate(-bbox.min()); + Geom::Point const center = bbox.dimensions() * 0.5; + + if (apply) { + // Delete objects so that their clones don't get alerted; + // the objects will be restored inside the marker element. + for (auto item : items_){ + item->deleteObject(false); + } + } + + // Hack: Temporarily set clone compensation to unmoved, so that we can move clone-originals + // without disturbing clones. + // See ActorAlign::on_button_click() in src/ui/dialog/align-and-distribute.cpp + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + gchar const *mark_id = generate_marker(repr_copies, bbox, doc, center, parent_transform * move); + (void)mark_id; + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + + + DocumentUndo::done(doc, SP_VERB_EDIT_SELECTION_2_MARKER, + _("Objects to marker")); +} + +static void sp_selection_to_guides_recursive(SPItem *item, bool wholegroups) { + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group && !dynamic_cast<SPBox3D *>(item) && !wholegroups) { + std::vector<SPItem*> items=sp_item_group_item_list(group); + for (auto item : items){ + sp_selection_to_guides_recursive(item, wholegroups); + } + } else { + item->convert_to_guides(); + } +} + +void ObjectSet::toGuides() +{ + SPDocument *doc = document(); + // we need to copy the list because it gets reset when objects are deleted + std::vector<SPItem*> items_(items().begin(), items().end()); + + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to convert to guides.")); + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool deleteitems = !prefs->getBool("/tools/cvg_keep_objects", false); + bool wholegroups = prefs->getBool("/tools/cvg_convert_whole_groups", false); + + // If an object is earlier in the selection list than its clone, and it is deleted, then the clone will have changed + // and its entry in the selection list is invalid (crash). + // Therefore: first convert all, then delete all. + + for (auto item : items_){ + sp_selection_to_guides_recursive(item, wholegroups); + } + + if (deleteitems) { + clear(); + sp_selection_delete_impl(items_); + } + + DocumentUndo::done(doc, SP_VERB_EDIT_SELECTION_2_GUIDES, _("Objects to guides")); +} + +/* + * Convert objects to <symbol>. How that happens depends on what is selected: + * + * 1) A random selection of objects will be embedded into a single <symbol> element. + * + * 2) Except, a single <g> will have its content directly embedded into a <symbol>; the 'id' and + * 'style' of the <g> are transferred to the <symbol>. + * + * 3) Except, a single <g> with a transform that isn't a translation will keep the group when + * embedded into a <symbol> (with 'id' and 'style' transferred to <symbol>). This is because a + * <symbol> cannot have a transform. (If the transform is a pure translation, the translation + * is moved to the referencing <use> element that is created.) + * + * Possible improvements: + * + * Move objects inside symbol so bbox corner at 0,0 (see marker/pattern) + * + * For SVG2, set 'refX' 'refY' to object center (with compensating shift in <use> + * transformation). + */ +void ObjectSet::toSymbol() +{ + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + // Check if something is selected. + if (isEmpty()) { + if (desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> to convert to symbol.")); + return; + } + + doc->ensureUpToDate(); + + std::vector<SPObject*> items_(objects().begin(), objects().end()); + sort(items_.begin(),items_.end(),sp_object_compare_position_bool); + + // Keep track of parent, this is where <use> will be inserted. + Inkscape::XML::Node *the_first_repr = items_[0]->getRepr(); + Inkscape::XML::Node *the_parent_repr = the_first_repr->parent(); + + // Find out if we have a single group + bool single_group = false; + SPGroup *the_group = nullptr; + Geom::Affine transform; + if( items_.size() == 1 ) { + SPObject *object = items_[0]; + the_group = dynamic_cast<SPGroup *>(object); + if ( the_group ) { + single_group = true; + + if( !sp_svg_transform_read( object->getAttribute("transform"), &transform )) + transform = Geom::identity(); + + if( transform.isTranslation() ) { + + // Create new list from group children. + items_ = object->childList(false); + + // Hack: Temporarily set clone compensation to unmoved, so that we can move clone-originals + // without disturbing clones. + // See ActorAlign::on_button_click() in src/ui/dialog/align-and-distribute.cpp + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // Remove transform on group, updating clones. + the_group->doWriteTransform(Geom::identity()); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + } + } + } + + // Create new <symbol> + Inkscape::XML::Node *defsrepr = doc->getDefs()->getRepr(); + Inkscape::XML::Node *symbol_repr = xml_doc->createElement("svg:symbol"); + Inkscape::XML::Node *title_repr = xml_doc->createElement("svg:title"); + + defsrepr->appendChild(symbol_repr); + bool settitle = false; + // For a single group, copy relevant attributes. + if( single_group ) { + Glib::ustring id = the_group->getAttribute("id"); + symbol_repr->setAttribute("style", the_group->getAttribute("style")); + + gchar * title = the_group->title(); + if (title) { + symbol_repr->addChildAtPos(title_repr, 0); + title_repr->appendChild(xml_doc->createTextNode(title)); + Inkscape::GC::release(title_repr); + } + g_free(title); + + gchar * desc = the_group->desc(); + if (desc) { + Inkscape::XML::Node *desc_repr = xml_doc->createElement("svg:desc"); + desc_repr->setContent(desc); + desc_repr->appendChild(xml_doc->createTextNode(desc)); + symbol_repr->addChildAtPos(desc_repr, 1); + Inkscape::GC::release(desc_repr); + } + g_free(desc); + symbol_repr->setAttribute("class", the_group->getAttribute("class")); + the_group->setAttribute("id", id + "_transform"); + symbol_repr->setAttribute("id", id); + + // This should eventually be replaced by 'refX' and 'refY' once SVG WG approves it. + // It is done here for round-tripping + symbol_repr->setAttribute("inkscape:transform-center-x", + the_group->getAttribute("inkscape:transform-center-x")); + symbol_repr->setAttribute("inkscape:transform-center-y", + the_group->getAttribute("inkscape:transform-center-y")); + + the_group->removeAttribute("style"); + + } + + // Move selected items to new <symbol> + for (std::vector<SPObject*>::const_reverse_iterator i=items_.rbegin();i!=items_.rend();++i){ + gchar* title = (*i)->title(); + if (!single_group && !settitle && title) { + symbol_repr->addChildAtPos(title_repr, 0); + title_repr->appendChild(xml_doc->createTextNode(title)); + Inkscape::GC::release(title_repr); + gchar * desc = (*i)->desc(); + if (desc) { + Inkscape::XML::Node *desc_repr = xml_doc->createElement("svg:desc"); + desc_repr->appendChild(xml_doc->createTextNode(desc)); + symbol_repr->addChildAtPos(desc_repr, 1); + Inkscape::GC::release(desc_repr); + } + g_free(desc); + settitle = true; + } + g_free(title); + Inkscape::XML::Node *repr = (*i)->getRepr(); + repr->parent()->removeChild(repr); + symbol_repr->addChild(repr, nullptr); + } + + if( single_group && transform.isTranslation() ) { + the_group->deleteObject(true); + } + + // Create <use> pointing to new symbol (to replace the moved objects). + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + + clone->setAttribute("xlink:href", Glib::ustring("#")+symbol_repr->attribute("id")); + + the_parent_repr->appendChild(clone); + + if( single_group && transform.isTranslation() ) { + if( !transform.isIdentity() ) { + gchar *c = sp_svg_transform_write( transform ); + clone->setAttribute("transform", c); + g_free(c); + } + } + + // Change selection to new <use> element. + set(clone); + + // Clean up + Inkscape::GC::release(symbol_repr); + + DocumentUndo::done(doc, SP_VERB_EDIT_SYMBOL, _("Group to symbol")); +} + +/* + * Convert <symbol> to <g>. All <use> elements referencing symbol remain unchanged. + */ +void ObjectSet::unSymbol() +{ + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + // Check if something is selected. + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select a <b>symbol</b> to extract objects from.")); + return; + } + + SPObject* symbol = single(); + + // Make sure we have only one object in selection. + // Require that we really have a <symbol>. + if( symbol == nullptr || !dynamic_cast<SPSymbol *>( symbol )) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select only one <b>symbol</b> in Symbol dialog to convert to group.")); + return; + } + + doc->ensureUpToDate(); + + // Create new <g> and insert in current layer + Inkscape::XML::Node *group = xml_doc->createElement("svg:g"); + //TODO: Better handle if no desktop, currently go to defs without it + if(desktop()) { + desktop()->currentLayer()->getRepr()->appendChild(group); + } else { + symbol->parent->getRepr()->appendChild(group); + } + // Move all children of symbol to group + std::vector<SPObject*> children = symbol->childList(false); + + // Converting a group to a symbol inserts a group for non-translational transform. + // In converting a symbol back to a group we strip out the inserted group (or any other + // group that only adds a transform to the symbol content). + if( children.size() == 1 ) { + SPObject *object = children[0]; + if ( dynamic_cast<SPGroup *>( object ) ) { + if( object->getAttribute("style") == nullptr || + object->getAttribute("class") == nullptr ) { + + group->setAttribute("transform", object->getAttribute("transform")); + children = object->childList(false); + } + } + } + + for (std::vector<SPObject*>::const_reverse_iterator i=children.rbegin();i!=children.rend();++i){ + Inkscape::XML::Node *repr = (*i)->getRepr(); + repr->parent()->removeChild(repr); + group->addChild(repr,nullptr); + } + + // Copy relevant attributes + group->setAttribute("style", symbol->getAttribute("style")); + group->setAttribute("class", symbol->getAttribute("class")); + group->setAttribute("title", symbol->getAttribute("title")); + group->setAttribute("inkscape:transform-center-x", + symbol->getAttribute("inkscape:transform-center-x")); + group->setAttribute("inkscape:transform-center-y", + symbol->getAttribute("inkscape:transform-center-y")); + + + // Need to delete <symbol>; all <use> elements that referenced <symbol> should + // auto-magically reference <g> (if <symbol> deleted after setting <g> 'id'). + Glib::ustring id = symbol->getAttribute("id"); + group->setAttribute("id", id); + symbol->deleteObject(true); + + // Change selection to new <g> element. + SPItem *group_item = static_cast<SPItem *>(document()->getObjectByRepr(group)); + set(group_item); + + // Clean up + Inkscape::GC::release(group); + + DocumentUndo::done(doc, SP_VERB_EDIT_UNSYMBOL, _("Group from symbol")); +} + +void ObjectSet::tile(bool apply) +{ + // toMarker has similar code + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if (desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>object(s)</b> to convert to pattern.")); + return; + } + + doc->ensureUpToDate(); + Geom::OptRect r = visualBounds(); + if ( !r ) { + return; + } + + std::vector<SPItem*> items_(items().begin(), items().end()); + + sort(items_.begin(),items_.end(),sp_object_compare_position_bool); + + // bottommost object, after sorting + SPObject *parent = items_[0]->parent; + + + Geom::Affine parent_transform; + { + SPItem *parentItem = dynamic_cast<SPItem *>(parent); + if (parentItem) { + parent_transform = parentItem->i2doc_affine(); + } else { + g_assert_not_reached(); + } + } + + // remember the position of the first item + gint pos = items_[0]->getRepr()->position(); + + // create a list of duplicates + std::vector<Inkscape::XML::Node*> repr_copies; + for (auto item : items_){ + Inkscape::XML::Node *dup = item->getRepr()->duplicate(xml_doc); + repr_copies.push_back(dup); + } + + Geom::Rect bbox(r->min() * doc->dt2doc(), r->max() * doc->dt2doc()); + + if (apply) { + // delete objects so that their clones don't get alerted; this object will be restored shortly + for (auto item : items_){ + item->deleteObject(false); + } + } + + // Hack: Temporarily set clone compensation to unmoved, so that we can move clone-originals + // without disturbing clones. + // See ActorAlign::on_button_click() in src/ui/dialog/align-and-distribute.cpp + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + Geom::Affine move = Geom::Translate(- bbox.min()); + gchar const *pat_id = SPPattern::produce(repr_copies, bbox, doc, + move.inverse() /* patternTransform */, + parent_transform * move); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + if (apply) { + Inkscape::XML::Node *rect = xml_doc->createElement("svg:rect"); + gchar *style_str = g_strdup_printf("stroke:none;fill:url(#%s)", pat_id); + rect->setAttribute("style", style_str); + g_free(style_str); + + gchar *c = sp_svg_transform_write(parent_transform.inverse()); + rect->setAttribute("transform", c); + g_free(c); + + sp_repr_set_svg_double(rect, "width", bbox.width()); + sp_repr_set_svg_double(rect, "height", bbox.height()); + sp_repr_set_svg_double(rect, "x", bbox.left()); + sp_repr_set_svg_double(rect, "y", bbox.top()); + + // restore parent and position + parent->getRepr()->addChildAtPos(rect, pos); + SPItem *rectangle = static_cast<SPItem *>(document()->getObjectByRepr(rect)); + + Inkscape::GC::release(rect); + + clear(); + set(rectangle); + } + + + DocumentUndo::done(doc, SP_VERB_EDIT_TILE, + _("Objects to pattern")); +} + +void ObjectSet::untile() +{ + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an <b>object with pattern fill</b> to extract objects from.")); + return; + } + + std::vector<SPItem*> new_select; + + bool did = false; + + std::vector<SPItem*> items_(items().begin(), items().end()); + for (std::vector<SPItem*>::const_reverse_iterator i=items_.rbegin();i!=items_.rend();++i){ + SPItem *item = *i; + + SPStyle *style = item->style; + + if (!style || !style->fill.isPaintserver()) + continue; + + SPPaintServer *server = item->style->getFillPaintServer(); + + SPPattern *basePat = dynamic_cast<SPPattern *>(server); + if (!basePat) { + continue; + } + + did = true; + + SPPattern *pattern = basePat->rootPattern(); + + Geom::Affine pat_transform = basePat->getTransform(); + pat_transform *= item->transform; + + for (auto& child: pattern->children) { + if (dynamic_cast<SPItem *>(&child)) { + Inkscape::XML::Node *copy = child.getRepr()->duplicate(xml_doc); + SPItem *i = dynamic_cast<SPItem *>(item->parent->appendChildRepr(copy)); + + // FIXME: relink clones to the new canvas objects + // use SPObject::setid when mental finishes it to steal ids of + + // this is needed to make sure the new item has curve (simply requestDisplayUpdate does not work) + doc->ensureUpToDate(); + + if (i) { + Geom::Affine transform( i->transform * pat_transform ); + i->doWriteTransform(transform); + + new_select.push_back(i); + } else { + g_assert_not_reached(); + } + } + } + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", "none"); + sp_repr_css_change(item->getRepr(), css, "style"); + } + + if (!did) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No pattern fills</b> in the selection.")); + } else { + DocumentUndo::done(document(), SP_VERB_EDIT_UNTILE, + _("Pattern to objects")); + setList(new_select); + } +} + +void ObjectSet::getExportHints(Glib::ustring &filename, float *xdpi, float *ydpi) +{ + if (isEmpty()) { + return; + } + + auto reprlst = xmlNodes(); + bool filename_search = TRUE; + bool xdpi_search = TRUE; + bool ydpi_search = TRUE; + + for (auto i=reprlst.begin();filename_search&&xdpi_search&&ydpi_search&&i!=reprlst.end();++i){ + gchar const *dpi_string; + Inkscape::XML::Node *repr = *i; + + if (filename_search) { + const gchar* tmp = repr->attribute("inkscape:export-filename"); + if (tmp){ + filename = tmp; + filename_search = FALSE; + } + else{ + filename.clear(); + } + } + + if (xdpi_search) { + dpi_string = repr->attribute("inkscape:export-xdpi"); + if (dpi_string != nullptr) { + *xdpi = atof(dpi_string); + xdpi_search = FALSE; + } + } + + if (ydpi_search) { + dpi_string = repr->attribute("inkscape:export-ydpi"); + if (dpi_string != nullptr) { + *ydpi = atof(dpi_string); + ydpi_search = FALSE; + } + } + } +} + +void sp_document_get_export_hints(SPDocument *doc, Glib::ustring &filename, float *xdpi, float *ydpi) +{ + Inkscape::XML::Node * repr = doc->getReprRoot(); + + const gchar* tmp = repr->attribute("inkscape:export-filename"); + if(tmp) + { + filename = tmp; + } + else + { + filename.clear(); + } + gchar const *dpi_string = repr->attribute("inkscape:export-xdpi"); + if (dpi_string != nullptr) { + *xdpi = atof(dpi_string); + } + + dpi_string = repr->attribute("inkscape:export-ydpi"); + if (dpi_string != nullptr) { + *ydpi = atof(dpi_string); + } +} + +void ObjectSet::createBitmapCopy() +{ + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to make a bitmap copy.")); + return; + } + if(desktop()){ + desktop()->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, _("Rendering bitmap...")); + // set "busy" cursor + desktop()->setWaitingCursor(); + } + + // Get the bounding box of the selection + doc->ensureUpToDate(); + Geom::OptRect bbox = documentBounds(SPItem::VISUAL_BBOX); + if (!bbox) { + if(desktop()) + desktop()->clearWaitingCursor(); + return; // exceptional situation, so not bother with a translatable error message, just quit quietly + } + + // List of the items to show; all others will be hidden + std::vector<SPItem*> items_(items().begin(), items().end()); + + // Sort items so that the topmost comes last + sort(items_.begin(),items_.end(),sp_item_repr_compare_position_bool); + + // Generate a random value from the current time (you may create bitmap from the same object(s) + // multiple times, and this is done so that they don't clash) + guint current = guint(g_get_monotonic_time() % 1024); + // Create the filename. + gchar *const basename = g_strdup_printf("%s-%s-%u.png", + doc->getDocumentName(), + items_[0]->getRepr()->attribute("id"), + current); + // Imagemagick is known not to handle spaces in filenames, so we replace anything but letters, + // digits, and a few other chars, with "_" + g_strcanon(basename, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.=+~$#@^&!?", '_'); + + // Build the complete path by adding document base dir, if set, otherwise home dir + gchar *directory = nullptr; + if ( doc->getDocumentURI() ) { + directory = g_path_get_dirname( doc->getDocumentURI() ); + } + if (directory == nullptr) { + directory = Inkscape::IO::Resource::homedir_path(nullptr); + } + gchar *filepath = g_build_filename(directory, basename, NULL); + g_free(directory); + + //g_print("%s\n", filepath); + + // Remember parent and z-order of the topmost one + gint pos = items_.back()->getRepr()->position(); + SPObject *parent_object = items_.back()->parent; + Inkscape::XML::Node *parent = parent_object->getRepr(); + + // Calculate resolution + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double res; + int const prefs_res = prefs->getInt("/options/createbitmap/resolution", 0); + int const prefs_min = prefs->getInt("/options/createbitmap/minsize", 0); + if (0 < prefs_res) { + // If it's given explicitly in prefs, take it + res = prefs_res; + } else if (0 < prefs_min) { + // If minsize is given, look up minimum bitmap size (default 250 pixels) and calculate resolution from it + res = Inkscape::Util::Quantity::convert(prefs_min, "in", "px") / MIN(bbox->width(), bbox->height()); + } else { + float hint_xdpi = 0, hint_ydpi = 0; + Glib::ustring hint_filename; + // take resolution hint from the selected objects + getExportHints(hint_filename, &hint_xdpi, &hint_ydpi); + if (hint_xdpi != 0) { + res = hint_xdpi; + } else { + // take resolution hint from the document + sp_document_get_export_hints(doc, hint_filename, &hint_xdpi, &hint_ydpi); + if (hint_xdpi != 0) { + res = hint_xdpi; + } else { + // if all else fails, take the default 96 dpi + res = Inkscape::Util::Quantity::convert(1, "in", "px"); + } + } + } + + // The width and height of the bitmap in pixels + unsigned width = (unsigned) floor(bbox->width() * Inkscape::Util::Quantity::convert(res, "px", "in")); + unsigned height =(unsigned) floor(bbox->height() * Inkscape::Util::Quantity::convert(res, "px", "in")); + + // Find out if we have to run an external filter + gchar const *run = nullptr; + Glib::ustring filter = prefs->getString("/options/createbitmap/filter"); + if (!filter.empty()) { + // filter command is given; + // see if we have a parameter to pass to it + Glib::ustring param1 = prefs->getString("/options/createbitmap/filter_param1"); + if (!param1.empty()) { + if (param1[param1.length() - 1] == '%') { + // if the param string ends with %, interpret it as a percentage of the image's max dimension + gchar p1[256]; + g_ascii_dtostr(p1, 256, ceil(g_ascii_strtod(param1.data(), nullptr) * MAX(width, height) / 100)); + // the first param is always the image filename, the second is param1 + run = g_strdup_printf("%s \"%s\" %s", filter.data(), filepath, p1); + } else { + // otherwise pass the param1 unchanged + run = g_strdup_printf("%s \"%s\" %s", filter.data(), filepath, param1.data()); + } + } else { + // run without extra parameter + run = g_strdup_printf("%s \"%s\"", filter.data(), filepath); + } + } + + // Calculate the matrix that will be applied to the image so that it exactly overlaps the source objects + Geom::Affine eek; + { + SPItem *parentItem = dynamic_cast<SPItem *>(parent_object); + if (parentItem) { + eek = parentItem->i2doc_affine(); + } else { + g_assert_not_reached(); + } + } + Geom::Affine t; + + double shift_x = bbox->left(); + double shift_y = bbox->top(); + if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) { // for default 96 dpi, snap it to pixel grid + shift_x = round(shift_x); + shift_y = round(shift_y); + } + t = Geom::Translate(shift_x, shift_y) * eek.inverse(); + + // TODO: avoid roundtrip via file + // Do the export + sp_export_png_file(doc, filepath, + bbox->min()[Geom::X], bbox->min()[Geom::Y], + bbox->max()[Geom::X], bbox->max()[Geom::Y], + width, height, res, res, + (guint32) 0xffffff00, + nullptr, nullptr, + true, /*bool force_overwrite,*/ + items_); + + // Run filter, if any + if (run) { + g_print("Running external filter: %s\n", run); + int result = system(run); + + if(result == -1) + g_warning("Could not run external filter: %s\n", run); + } + + // Import the image back + Inkscape::Pixbuf *pb = Inkscape::Pixbuf::create_from_file(filepath); + if (pb) { + // Create the repr for the image + // TODO: avoid unnecessary roundtrip between data URI and decoded pixbuf + Inkscape::XML::Node * repr = xml_doc->createElement("svg:image"); + sp_embed_image(repr, pb); + if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) { // for default 96 dpi, snap it to pixel grid + sp_repr_set_svg_double(repr, "width", width); + sp_repr_set_svg_double(repr, "height", height); + } else { + sp_repr_set_svg_double(repr, "width", bbox->width()); + sp_repr_set_svg_double(repr, "height", bbox->height()); + } + + // Write transform + gchar *c=sp_svg_transform_write(t); + repr->setAttribute("transform", c); + g_free(c); + + // add the new repr to the parent + parent->addChildAtPos(repr, pos + 1); + + // Set selection to the new image + clear(); + add(repr); + + // Clean up + Inkscape::GC::release(repr); + delete pb; + + // Complete undoable transaction + DocumentUndo::done(doc, SP_VERB_SELECTION_CREATE_BITMAP, + _("Create bitmap")); + } + if(desktop()) + desktop()->clearWaitingCursor(); + + g_free(basename); + g_free(filepath); +} + +/* Creates a mask or clipPath from selection. + * What is a clip group? + * A clip group is a tangled mess of XML that allows an object inside a group + * to clip the entire group using a few <use>s and generally irritating me. + */ + +void ObjectSet::setClipGroup() +{ + SPDocument* doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to create clippath or mask from.")); + return; + } + + std::vector<Inkscape::XML::Node*> p(xmlNodes().begin(), xmlNodes().end()); + + sort(p.begin(),p.end(),sp_repr_compare_position_bool); + + clear(); + + int topmost = (p.back())->position(); + Inkscape::XML::Node *topmost_parent = (p.back())->parent(); + + Inkscape::XML::Node *inner = xml_doc->createElement("svg:g"); + inner->setAttribute("inkscape:label", "Clip"); + + for(auto current : p){ + if (current->parent() == topmost_parent) { + Inkscape::XML::Node *spnew = current->duplicate(xml_doc); + sp_repr_unparent(current); + inner->appendChild(spnew); + Inkscape::GC::release(spnew); + topmost --; // only reduce count for those items deleted from topmost_parent + } else { // move it to topmost_parent first + std::vector<Inkscape::XML::Node*> temp_clip; + + // At this point, current may already have no item, due to its being a clone whose original is already moved away + // So we copy it artificially calculating the transform from its repr->attr("transform") and the parent transform + gchar const *t_str = current->attribute("transform"); + Geom::Affine item_t(Geom::identity()); + if (t_str) + sp_svg_transform_read(t_str, &item_t); + item_t *= SP_ITEM(doc->getObjectByRepr(current->parent()))->i2doc_affine(); + // FIXME: when moving both clone and original from a transformed group (either by + // grouping into another parent, or by cut/paste) the transform from the original's + // parent becomes embedded into original itself, and this affects its clones. Fix + // this by remembering the transform diffs we write to each item into an array and + // then, if this is clone, looking up its original in that array and pre-multiplying + // it by the inverse of that original's transform diff. + + sp_selection_copy_one(current, item_t, temp_clip, xml_doc); + sp_repr_unparent(current); + + // paste into topmost_parent (temporarily) + std::vector<Inkscape::XML::Node*> copied = sp_selection_paste_impl(doc, doc->getObjectByRepr(topmost_parent), temp_clip); + if (!copied.empty()) { // if success, + // take pasted object (now in topmost_parent) + Inkscape::XML::Node *in_topmost = copied.back(); + // make a copy + Inkscape::XML::Node *spnew = in_topmost->duplicate(xml_doc); + // remove pasted + sp_repr_unparent(in_topmost); + // put its copy into group + inner->appendChild(spnew); + Inkscape::GC::release(spnew); + } + } + } + + Inkscape::XML::Node *outer = xml_doc->createElement("svg:g"); + outer->appendChild(inner); + topmost_parent->addChildAtPos(outer, topmost + 1); + + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + clone->setAttribute("xlink:href", g_strdup_printf("#%s", inner->attribute("id"))); + + clone->setAttribute("inkscape:transform-center-x", inner->attribute("inkscape:transform-center-x")); + clone->setAttribute("inkscape:transform-center-y", inner->attribute("inkscape:transform-center-y")); + + std::vector<Inkscape::XML::Node*> templist; + templist.push_back(clone); + // add the new clone to the top of the original's parent + gchar const *mask_id = SPClipPath::create(templist, doc); + + char* tmp = g_strdup_printf("url(#%s)", mask_id); + outer->setAttribute("clip-path", tmp); + g_free(tmp); + + Inkscape::GC::release(clone); + + set(outer); + DocumentUndo::done(doc, SP_VERB_OBJECT_SET_CLIPPATH, _("Create Clip Group")); +} + +/** + * Creates a mask or clipPath from selection. + * Two different modes: + * if applyToLayer, all selection is moved to DEFS as mask/clippath + * and is applied to current layer + * otherwise, topmost object is used as mask for other objects + * If \a apply_clip_path parameter is true, clipPath is created, otherwise mask + * + */ + void ObjectSet::setMask(bool apply_clip_path, bool apply_to_layer, bool skip_undo) +{ + if(!desktop() && apply_to_layer) + return; + + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + bool is_empty = isEmpty(); + if ( apply_to_layer && is_empty) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to create clippath or mask from.")); + return; + } else if (!apply_to_layer && ( is_empty || boost::distance(items())==1 )) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select mask object and <b>object(s)</b> to apply clippath or mask to.")); + return; + } + + // FIXME: temporary patch to prevent crash! + // Remove this when bboxes are fixed to not blow up on an item clipped/masked with its own clone + bool clone_with_original = object_set_contains_both_clone_and_original(this); + if (clone_with_original) { + return; // in this version, you cannot clip/mask an object with its own clone + } + // /END FIXME + + doc->ensureUpToDate(); + + // Comment out this section because we don't need it, I think. + // Also updated comment code to work correctly if indeed it's needed. + // To reactivate, remove the next line and uncomment the section. + std::vector<SPItem*> items_(items().begin(), items().end()); + /* + std::vector<SPItem*> items_prerect_(items().begin(), items().end()); + std::vector<SPItem*> items_; + + // convert any rects to paths + for (std::vector<SPItem *>::const_iterator i = items_prerect_.begin(); i != items_prerect_.end(); ++i) { + clear(); + if (dynamic_cast<SPRect *>(*i)) { + add(*i); + toCurves(); + items_.push_back(*items().begin()); + } else { + items_.push_back(*i); + } + } + clear(); + */ + sort(items_.begin(),items_.end(),sp_object_compare_position_bool); + + // See lp bug #542004 + clear(); + + // create a list of duplicates + std::vector<std::pair<Inkscape::XML::Node*, Geom::Affine>> mask_items; + std::vector<SPItem*> apply_to_items; + std::vector<SPItem*> items_to_delete; + std::vector<SPItem*> items_to_select; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool topmost = prefs->getBool("/options/maskobject/topmost", true); + bool remove_original = prefs->getBool("/options/maskobject/remove", true); + int grouping = prefs->getInt("/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE); + + if (apply_to_layer) { + // all selected items are used for mask, which is applied to a layer + apply_to_items.push_back(SP_ITEM(desktop()->currentLayer())); + } + + for (std::vector<SPItem*>::const_iterator i=items_.begin();i!=items_.end();++i) { + if((!topmost && !apply_to_layer && *i == items_.front()) + || (topmost && !apply_to_layer && *i == items_.back()) + || apply_to_layer){ + + Inkscape::XML::Node *dup = (*i)->getRepr()->duplicate(xml_doc); + mask_items.emplace_back(dup, (*i)->i2doc_affine()); + + if (remove_original) { + items_to_delete.push_back(*i); + } + else { + items_to_select.push_back(*i); + } + continue; + }else{ + apply_to_items.push_back(*i); + items_to_select.push_back(*i); + } + } + + items_.clear(); + + if (grouping == PREFS_MASKOBJECT_GROUPING_ALL) { + // group all those objects into one group + // and apply mask to that + ObjectSet* set = new ObjectSet(document()); + set->add(apply_to_items.begin(), apply_to_items.end()); + + items_to_select.clear(); + + Inkscape::XML::Node *group = set->group(); + group->setAttribute("inkscape:groupmode", "maskhelper"); + + // apply clip/mask only to newly created group + apply_to_items.clear(); + apply_to_items.push_back(dynamic_cast<SPItem*>(doc->getObjectByRepr(group))); + + items_to_select.push_back((SPItem*)(doc->getObjectByRepr(group))); + + delete set; + Inkscape::GC::release(group); + } + if (grouping == PREFS_MASKOBJECT_GROUPING_SEPARATE) { + items_to_select.clear(); + } + + + gchar const *attributeName = apply_clip_path ? "clip-path" : "mask"; + for (std::vector<SPItem*>::const_reverse_iterator i = apply_to_items.rbegin(); i != apply_to_items.rend(); ++i) { + SPItem *item = reinterpret_cast<SPItem *>(*i); + std::vector<Inkscape::XML::Node*> mask_items_dup; + std::map<Inkscape::XML::Node*, Geom::Affine> dup_transf; + for (auto & mask_item : mask_items) { + Inkscape::XML::Node *dup = (mask_item.first)->duplicate(xml_doc); + mask_items_dup.push_back(dup); + dup_transf[dup] = mask_item.second; + } + + Inkscape::XML::Node *current = SP_OBJECT(*i)->getRepr(); + // Node to apply mask to + Inkscape::XML::Node *apply_mask_to = current; + + if (grouping == PREFS_MASKOBJECT_GROUPING_SEPARATE) { + // enclose current node in group, and apply crop/mask on that + Inkscape::XML::Node *group = xml_doc->createElement("svg:g"); + // make a note we should ungroup this when unsetting mask + group->setAttribute("inkscape:groupmode", "maskhelper"); + + Inkscape::XML::Node *spnew = current->duplicate(xml_doc); + current->parent()->addChild(group, current); + sp_repr_unparent(current); + group->appendChild(spnew); + + // Apply clip/mask to group instead + apply_mask_to = group; + + items_to_select.push_back((SPItem*)doc->getObjectByRepr(group)); + Inkscape::GC::release(spnew); + Inkscape::GC::release(group); + } + + gchar const *mask_id = nullptr; + if (apply_clip_path) { + mask_id = SPClipPath::create(mask_items_dup, doc); + } else { + mask_id = sp_mask_create(mask_items_dup, doc); + } + + // inverted object transform should be applied to a mask object, + // as mask is calculated in user space (after applying transform) + for (auto & it : mask_items_dup) { + SPItem *clip_item = SP_ITEM(doc->getObjectByRepr(it)); + clip_item->doWriteTransform(dup_transf[it]); + clip_item->doWriteTransform(clip_item->transform * item->i2doc_affine().inverse()); + } + + apply_mask_to->setAttribute(attributeName, Glib::ustring("url(#") + mask_id + ')'); + + } + + for (auto i : items_to_delete) { + SPObject *item = reinterpret_cast<SPObject*>(i); + item->deleteObject(false); + items_to_select.erase(std::remove(items_to_select.begin(), items_to_select.end(), item), items_to_select.end()); + } + + addList(items_to_select); + if (!skip_undo) { + if (apply_clip_path) { + DocumentUndo::done(doc, SP_VERB_OBJECT_SET_CLIPPATH, _("Set clipping path")); + } else { + DocumentUndo::done(doc, SP_VERB_OBJECT_SET_MASK, _("Set mask")); + } + } +} + +void ObjectSet::unsetMask(const bool apply_clip_path, const bool skip_undo) { + SPDocument *doc = document(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // check if something is selected + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to remove clippath or mask from.")); + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool remove_original = prefs->getBool("/options/maskobject/remove", true); + bool ungroup_masked = prefs->getBool("/options/maskobject/ungrouping", true); + doc->ensureUpToDate(); + + gchar const *attributeName = apply_clip_path ? "clip-path" : "mask"; + std::map<SPObject*,SPItem*> referenced_objects; + + std::vector<SPItem*> items_(items().begin(), items().end()); + clear(); + + std::vector<SPGroup *> items_to_ungroup; + std::vector<SPItem*> items_to_select(items_); + + + // SPObject* refers to a group containing the clipped path or mask itself, + // whereas SPItem* refers to the item being clipped or masked + for (auto i : items_){ + if (remove_original) { + // remember referenced mask/clippath, so orphaned masks can be moved back to document + SPItem *item = i; + SPObject *obj_ref = nullptr; + + if (apply_clip_path) { + obj_ref = item->getClipObject(); + } else { + obj_ref = item->getMaskObject(); + } + + // collect distinct mask object (and associate with item to apply transform) + if (obj_ref) { + referenced_objects[obj_ref] = item; + } + } + + i->setAttribute(attributeName, "none"); + + SPGroup *group = dynamic_cast<SPGroup *>(i); + if (ungroup_masked && group) { + // if we had previously enclosed masked object in group, + // add it to list so we can ungroup it later + + // ungroup only groups we created when setting clip/mask + if (group->layerMode() == SPGroup::MASK_HELPER) { + items_to_ungroup.push_back(group); + } + + } + } + + // restore mask objects into a document + for (auto & referenced_object : referenced_objects) { + SPObject *obj = referenced_object.first; // Group containing the clipped paths or masks + std::vector<Inkscape::XML::Node *> items_to_move; + for (auto& child: obj->children) { + // Collect all clipped paths and masks within a single group + Inkscape::XML::Node *copy = child.getRepr()->duplicate(xml_doc); + if (copy->attribute("inkscape:original-d") && copy->attribute("inkscape:path-effect")) { + copy->setAttribute("d", copy->attribute("inkscape:original-d")); + } else if (copy->attribute("inkscape:original-d")) { + copy->setAttribute("d", copy->attribute("inkscape:original-d")); + copy->removeAttribute("inkscape:original-d"); + } else if (!copy->attribute("inkscape:path-effect") && !SP_IS_PATH(&child)) { + copy->removeAttribute("d"); + copy->removeAttribute("inkscape:original-d"); + } + items_to_move.push_back(copy); + } + + if (!obj->isReferenced()) { + // delete from defs if no other object references this mask + obj->deleteObject(false); + } + + // remember parent and position of the item to which the clippath/mask was applied + Inkscape::XML::Node *parent = (referenced_object.second)->getRepr()->parent(); + Inkscape::XML::Node *ref_repr = referenced_object.second->getRepr(); + + // Iterate through all clipped paths / masks + for (auto i=items_to_move.rbegin();i!=items_to_move.rend();++i) { + Inkscape::XML::Node *repr = *i; + + // insert into parent, restore pos + parent->addChild(repr, ref_repr); + + SPItem *mask_item = dynamic_cast<SPItem *>(document()->getObjectByRepr(repr)); + if (!mask_item) { + continue; + } + items_to_select.push_back(mask_item); + + // transform mask, so it is moved the same spot where mask was applied + Geom::Affine transform(mask_item->transform); + transform *= referenced_object.second->transform; + mask_item->doWriteTransform(transform); + } + } + + // ungroup marked groups added when setting mask + for (auto i=items_to_ungroup.rbegin();i!=items_to_ungroup.rend();++i) { + SPGroup *group = *i; + if (group) { + items_to_select.erase(std::remove(items_to_select.begin(), items_to_select.end(), group), items_to_select.end()); + std::vector<SPItem*> children; + sp_item_group_ungroup(group, children, false); + items_to_select.insert(items_to_select.end(),children.rbegin(),children.rend()); + } else { + g_assert_not_reached(); + } + } + + // rebuild selection + addList(items_to_select); + if (!skip_undo) { + if (apply_clip_path) { + DocumentUndo::done(doc, SP_VERB_OBJECT_UNSET_CLIPPATH, _("Release clipping path")); + } else { + DocumentUndo::done(doc, SP_VERB_OBJECT_UNSET_MASK, _("Release mask")); + } + } +} + +/** + * \param with_margins margins defined in the xml under <sodipodi:namedview> + * "fit-margin-..." attributes. See SPDocument::fitToRect. + * \return true if an undoable change should be recorded. + */ +bool ObjectSet::fitCanvas(bool with_margins, bool skip_undo) +{ + g_return_val_if_fail(document() != nullptr, false); + + if (isEmpty()) { + if(desktop()) + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>object(s)</b> to fit canvas to.")); + return false; + } + Geom::OptRect const bbox = documentBounds(SPItem::VISUAL_BBOX); + if (bbox) { + document()->fitToRect(*bbox, with_margins); + if(!skip_undo) + DocumentUndo::done(document(), SP_VERB_FIT_CANVAS_TO_SELECTION, + _("Fit Page to Selection")); + return true; + } else { + return false; + } +} + +void ObjectSet::swapFillStroke() +{ + if (desktop() == nullptr) { + return; + } + + SPIPaint *paint; + SPPaintServer *server; + Glib::ustring _paintserver_id; + + auto list= items(); + for (auto itemlist=list.begin();itemlist!=list.end();++itemlist) { + SPItem *item = *itemlist; + + SPCSSAttr *css = sp_repr_css_attr_new (); + + _paintserver_id.clear(); + paint = &(item->style->fill); + if (paint->set && paint->isNone()) + sp_repr_css_set_property (css, "stroke", "none"); + else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32(SP_SCALE24_TO_FLOAT (item->style->fill_opacity.value)); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "stroke", c); + } + else if (!paint->set) + sp_repr_css_unset_property (css, "stroke"); + else if (paint->set && paint->isPaintserver()) { + server = SP_STYLE_FILL_SERVER(item->style); + if (server) { + Inkscape::XML::Node *srepr = server->getRepr(); + _paintserver_id += "url(#"; + _paintserver_id += srepr->attribute("id"); + _paintserver_id += ")"; + sp_repr_css_set_property (css, "stroke", _paintserver_id.c_str()); + } + } + + _paintserver_id.clear(); + paint = &(item->style->stroke); + if (paint->set && paint->isNone()) + sp_repr_css_set_property (css, "fill", "none"); + else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32(SP_SCALE24_TO_FLOAT (item->style->stroke_opacity.value)); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "fill", c); + } + else if (!paint->set) + sp_repr_css_unset_property (css, "fill"); + else if (paint->set && paint->isPaintserver()) { + server = SP_STYLE_STROKE_SERVER(item->style); + if (server) { + Inkscape::XML::Node *srepr = server->getRepr(); + _paintserver_id += "url(#"; + _paintserver_id += srepr->attribute("id"); + _paintserver_id += ")"; + sp_repr_css_set_property (css, "fill", _paintserver_id.c_str()); + } + } + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); + } + + DocumentUndo::done(document(), SP_VERB_EDIT_SWAP_FILL_STROKE, + _("Swap fill and stroke of an object")); +} + +/** + * \param with_margins margins defined in the xml under <sodipodi:namedview> + * "fit-margin-..." attributes. See SPDocument::fitToRect. + */ +bool +fit_canvas_to_drawing(SPDocument *doc, bool with_margins) +{ + g_return_val_if_fail(doc != nullptr, false); + + doc->ensureUpToDate(); + SPItem const *const root = doc->getRoot(); + Geom::OptRect bbox = root->documentVisualBounds(); + if (bbox) { + doc->fitToRect(*bbox, with_margins); + return true; + } else { + return false; + } +} + +void +verb_fit_canvas_to_drawing(SPDesktop *desktop) +{ + if (fit_canvas_to_drawing(desktop->getDocument())) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_FIT_CANVAS_TO_DRAWING, + _("Fit Page to Drawing")); + } +} + +/** + * Fits canvas to selection or drawing with margins from <sodipodi:namedview> + * "fit-margin-..." attributes. See SPDocument::fitToRect and + * ui/dialog/page-sizer. + */ +void fit_canvas_to_selection_or_drawing(SPDesktop *desktop) { + g_return_if_fail(desktop != nullptr); + SPDocument *doc = desktop->getDocument(); + + g_return_if_fail(doc != nullptr); + g_return_if_fail(desktop->selection != nullptr); + + bool const changed = ( desktop->selection->isEmpty() + ? fit_canvas_to_drawing(doc, true) + : desktop->selection->fitCanvas(true,true)); + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING, + _("Fit Page to Selection or Drawing")); + } +}; + +static void itemtree_map(void (*f)(SPItem *, SPDesktop *), SPObject *root, SPDesktop *desktop) { + // don't operate on layers + { + SPItem *item = dynamic_cast<SPItem *>(root); + if (item && !desktop->isLayer(item)) { + f(item, desktop); + } + } + for (auto& child: root->children) { + //don't recurse into locked layers + SPItem *item = dynamic_cast<SPItem *>(&child); + if (!(item && desktop->isLayer(item) && item->isLocked())) { + itemtree_map(f, &child, desktop); + } + } +} + +static void unlock(SPItem *item, SPDesktop */*desktop*/) { + if (item->isLocked()) { + item->setLocked(FALSE); + } +} + +static void unhide(SPItem *item, SPDesktop *desktop) { + if (desktop->itemIsHidden(item)) { + item->setExplicitlyHidden(FALSE); + } +} + +static void process_all(void (*f)(SPItem *, SPDesktop *), SPDesktop *dt, bool layer_only) { + if (!dt) return; + + SPObject *root; + if (layer_only) { + root = dt->currentLayer(); + } else { + root = dt->currentRoot(); + } + + itemtree_map(f, root, dt); +} + +void unlock_all(SPDesktop *dt) { + process_all(&unlock, dt, true); +} + +void unlock_all_in_all_layers(SPDesktop *dt) { + process_all(&unlock, dt, false); +} + +void unhide_all(SPDesktop *dt) { + process_all(&unhide, dt, true); +} + +void unhide_all_in_all_layers(SPDesktop *dt) { + process_all(&unhide, dt, false); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/selection-chemistry.h b/src/selection-chemistry.h new file mode 100644 index 0000000..4a39cbe --- /dev/null +++ b/src/selection-chemistry.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SELECTION_CHEMISTRY_H +#define SEEN_SELECTION_CHEMISTRY_H + +/* + * Miscellaneous operations on selected items + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2012 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +class SPCSSAttr; +class SPDesktop; +class SPDocument; +class SPItem; +class SPObject; + +namespace Inkscape { + +class Selection; +class ObjectSet; + +namespace LivePathEffect { + class PathParam; +} + + class SelectionHelper { + public: + static void selectAll(SPDesktop *desktop); + static void selectAllInAll(SPDesktop *desktop); + static void selectNone(SPDesktop *desktop); + static void selectSameFillStroke(SPDesktop *dt); + static void selectSameFillColor(SPDesktop *dt); + static void selectSameStrokeColor(SPDesktop *dt); + static void selectSameStrokeStyle(SPDesktop *dt); + static void selectSameObjectType(SPDesktop *dt); + static void invert(SPDesktop *desktop); + static void invertAllInAll(SPDesktop *desktop); + static void reverse(SPDesktop *dt); + static void selectNext(SPDesktop *desktop); + static void selectPrev(SPDesktop *desktop); + static void fixSelection(SPDesktop *desktop); + }; +} // namespace Inkscape + +void sp_edit_clear_all(Inkscape::Selection *selection); + +void sp_edit_select_all(SPDesktop *desktop); +void sp_edit_select_all_in_all_layers (SPDesktop *desktop); +void sp_edit_invert (SPDesktop *desktop); +void sp_edit_invert_in_all_layers (SPDesktop *desktop); + + +SPCSSAttr *take_style_from_item (SPObject *object); + +void sp_selection_paste(SPDesktop *desktop, bool in_place); + +void sp_set_style_clipboard (SPCSSAttr *css); + + +void sp_selection_item_next (SPDesktop *desktop); +void sp_selection_item_prev (SPDesktop *desktop); + +void sp_selection_next_patheffect_param(SPDesktop * dt); + +//void sp_selection_edit_clip_or_mask(SPDesktop * dt, bool clip); + +enum SPSelectStrokeStyleType { + SP_FILL_COLOR = 0, + SP_STROKE_COLOR = 1, + SP_STROKE_STYLE_WIDTH = 2, + SP_STROKE_STYLE_DASHES = 3, + SP_STROKE_STYLE_MARKERS = 4, + SP_STROKE_STYLE_ALL = 5, + SP_STYLE_ALL = 6 +}; + +void sp_select_same_fill_stroke_style(SPDesktop *desktop, gboolean fill, gboolean strok, gboolean style); +void sp_select_same_object_type(SPDesktop *desktop); + +std::vector<SPItem*> sp_get_same_style(SPItem *sel, std::vector<SPItem*> &src, SPSelectStrokeStyleType type=SP_STYLE_ALL); +std::vector<SPItem*> sp_get_same_object_type(SPItem *sel, std::vector<SPItem*> &src); + +void scroll_to_show_item(SPDesktop *desktop, SPItem *item); + +void sp_undo (SPDesktop *desktop, SPDocument *doc); +void sp_redo (SPDesktop *desktop, SPDocument *doc); + +void sp_document_get_export_hints (SPDocument * doc, Glib::ustring &filename, float *xdpi, float *ydpi); + +bool fit_canvas_to_drawing(SPDocument *, bool with_margins = false); +void verb_fit_canvas_to_drawing(SPDesktop *); +void fit_canvas_to_selection_or_drawing(SPDesktop *); + +void unlock_all(SPDesktop *dt); +void unlock_all_in_all_layers(SPDesktop *dt); +void unhide_all(SPDesktop *dt); +void unhide_all_in_all_layers(SPDesktop *dt); + +std::vector<SPItem*> &get_all_items(std::vector<SPItem*> &list, SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::vector<SPItem*> const &exclude); + +std::vector<SPItem*> sp_degroup_list (std::vector<SPItem*> &items); + +/* selection cycling */ +enum SPCycleType +{ + SP_CYCLE_SIMPLE, + SP_CYCLE_VISIBLE, // cycle only visible items + SP_CYCLE_FOCUS // readjust visible area to view selected item +}; + + + +// TODO FIXME: This should be moved into preference repr +extern SPCycleType SP_CYCLING; + +#endif // SEEN_SELECTION_CHEMISTRY_H diff --git a/src/selection-describer.cpp b/src/selection-describer.cpp new file mode 100644 index 0000000..526d526 --- /dev/null +++ b/src/selection-describer.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::SelectionDescriber - shows messages describing selection + * + * Authors: + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <set> +#include <utility> + +#include <glibmm/i18n.h> + +#include "selection-describer.h" + +#include "layer-model.h" +#include "selection.h" +#include "desktop.h" + +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-symbol.h" +#include "object/sp-textpath.h" +#include "object/sp-use.h" + +#include "xml/quote.h" + +// Returns a list of terms for the items to be used in the statusbar +char* collect_terms (const std::vector<SPItem*> &items) +{ + std::set<Glib::ustring> check; + std::stringstream ss; + bool first = true; + + for (auto item : items) { + if (item && item->displayName()) { + Glib::ustring term(item->displayName()); + if (term != "" && (check.insert(term).second)) { + ss << (first ? "" : ", ") << "<b>" << term.raw() << "</b>"; + first = false; + } + } + } + return g_strdup(ss.str().c_str()); +} + +// Returns the number of terms in the list +static int count_terms (const std::vector<SPItem*> &items) +{ + std::set<Glib::ustring> check; + int count=0; + for (auto item : items) { + if (item && item->displayName()) { + Glib::ustring term(item->displayName()); + if (term != "" && (check.insert(term).second)) { + count++; + } + } + } + return count; +} + +// Returns the number of filtered items in the list +static int count_filtered (const std::vector<SPItem*> &items) +{ + int count=0; + for (auto item : items) { + if (item) { + count += item->isFiltered(); + } + } + return count; +} + + +namespace Inkscape { + +SelectionDescriber::SelectionDescriber(Inkscape::Selection *selection, std::shared_ptr<MessageStack> stack, char *when_selected, char *when_nothing) + : _context(std::move(stack)), + _when_selected (when_selected), + _when_nothing (when_nothing) +{ + _selection_changed_connection = new sigc::connection ( + selection->connectChanged( + sigc::mem_fun(*this, &SelectionDescriber::_updateMessageFromSelection))); + _updateMessageFromSelection(selection); +} + +SelectionDescriber::~SelectionDescriber() +{ + _selection_changed_connection->disconnect(); + delete _selection_changed_connection; +} + +void SelectionDescriber::_updateMessageFromSelection(Inkscape::Selection *selection) { + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + + if (items.empty()) { // no items + _context.set(Inkscape::NORMAL_MESSAGE, _when_nothing); + } else { + SPItem *item = items[0]; + g_assert(item != nullptr); + SPObject *layer = selection->layers()->layerForObject(item); + SPObject *root = selection->layers()->currentRoot(); + + // Layer name + gchar *layer_name; + if (layer == root) { + layer_name = g_strdup(_("root")); + } else if(!layer) { + layer_name = g_strdup(_("none")); + } else { + char const *layer_label; + bool is_label = false; + if (layer->label()) { + layer_label = layer->label(); + is_label = true; + } else { + layer_label = layer->defaultLabel(); + } + char *quoted_layer_label = xml_quote_strdup(layer_label); + if (is_label) { + layer_name = g_strdup_printf(_("layer <b>%s</b>"), quoted_layer_label); + } else { + layer_name = g_strdup_printf(_("layer <b><i>%s</i></b>"), quoted_layer_label); + } + g_free(quoted_layer_label); + } + + // Parent name + SPObject *parent = item->parent; + if (!parent) { // fix selector * to "svg:svg" + return; + } + gchar const *parent_label = parent->getId(); + gchar *parent_name = nullptr; + if (parent_label) { + char *quoted_parent_label = xml_quote_strdup(parent_label); + parent_name = g_strdup_printf(_("<i>%s</i>"), quoted_parent_label); + g_free(quoted_parent_label); + } + + gchar *in_phrase; + guint num_layers = selection->numberOfLayers(); + guint num_parents = selection->numberOfParents(); + if (num_layers == 1) { + if (num_parents == 1) { + if (layer == parent) + in_phrase = g_strdup_printf(_(" in %s"), layer_name); + else if (!layer) + in_phrase = g_strdup_printf("%s", _(" hidden in definitions")); + else if (parent_name) + in_phrase = g_strdup_printf(_(" in group %s (%s)"), parent_name, layer_name); + else + in_phrase = g_strdup_printf(_(" in unnamed group (%s)"), layer_name); + } else { + in_phrase = g_strdup_printf(ngettext(" in <b>%i</b> parent (%s)", " in <b>%i</b> parents (%s)", num_parents), num_parents, layer_name); + } + } else { + in_phrase = g_strdup_printf(ngettext(" in <b>%i</b> layer", " in <b>%i</b> layers", num_layers), num_layers); + } + g_free (layer_name); + g_free (parent_name); + + if (items.size()==1) { // one item + char *item_desc = item->detailedDescription(); + + bool isUse = dynamic_cast<SPUse *>(item) != nullptr; + if (isUse && dynamic_cast<SPSymbol *>(item->firstChild())) { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s. %s.", + item_desc, in_phrase, + _("Convert symbol to group to edit"), _when_selected); + } else if (dynamic_cast<SPSymbol *>(item)) { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s.", + item_desc, in_phrase, + _("Remove from symbols tray to edit symbol")); + } else { + SPOffset *offset = (isUse) ? nullptr : dynamic_cast<SPOffset *>(item); + if (isUse || (offset && offset->sourceHref)) { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s. %s.", + item_desc, in_phrase, + _("Use <b>Shift+D</b> to look up original"), _when_selected); + } else { + SPText *text = dynamic_cast<SPText *>(item); + if (text && text->firstChild() && dynamic_cast<SPText *>(text->firstChild())) { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s. %s.", + item_desc, in_phrase, + _("Use <b>Shift+D</b> to look up path"), _when_selected); + } else { + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item); + if (flowtext && !flowtext->has_internal_frame()) { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s. %s.", + item_desc, in_phrase, + _("Use <b>Shift+D</b> to look up frame"), _when_selected); + } else { + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s. %s.", + item_desc, in_phrase, _when_selected); + } + } + } + } + + g_free(item_desc); + } else { // multiple items + int objcount = items.size(); + char *terms = collect_terms (items); + int n_terms = count_terms(items); + + gchar *objects_str = g_strdup_printf(ngettext( + "<b>%1$i</b> objects selected of type %2$s", + "<b>%1$i</b> objects selected of types %2$s", n_terms), + objcount, terms); + + g_free(terms); + + // indicate all, some, or none filtered + gchar *filt_str = nullptr; + int n_filt = count_filtered(items); //all filtered + if (n_filt) { + filt_str = g_strdup_printf(ngettext("; <i>%d filtered object</i> ", + "; <i>%d filtered objects</i> ", n_filt), n_filt); + } else { + filt_str = g_strdup(""); + } + + _context.setF(Inkscape::NORMAL_MESSAGE, "%s%s%s. %s.", objects_str, filt_str, in_phrase, _when_selected); + if (objects_str) { + g_free(objects_str); + objects_str = nullptr; + } + if (filt_str) { + g_free(filt_str); + filt_str = nullptr; + } + } + + g_free(in_phrase); + } +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/selection-describer.h b/src/selection-describer.h new file mode 100644 index 0000000..259f5b2 --- /dev/null +++ b/src/selection-describer.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::SelectionDescriber - shows messages describing selection + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SELECTION_DESCRIPTION_HANDLER_H +#define SEEN_INKSCAPE_SELECTION_DESCRIPTION_HANDLER_H + +#include <cstddef> +#include <memory> +#include <sigc++/sigc++.h> +#include "message-context.h" + +namespace Inkscape { + +class MessageStack; +class Selection; + +class SelectionDescriber : public sigc::trackable { +public: + SelectionDescriber(Inkscape::Selection *selection, std::shared_ptr<MessageStack> stack, char *when_selected, char *when_nothing); + ~SelectionDescriber(); + +private: + void _updateMessageFromSelection(Inkscape::Selection *selection); + sigc::connection *_selection_changed_connection; + + MessageContext _context; + + char *_when_selected; + char *_when_nothing; +}; + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/selection.cpp b/src/selection.cpp new file mode 100644 index 0000000..487758d --- /dev/null +++ b/src/selection.cpp @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Per-desktop selection container + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * Adrian Boguszewski + * + * Copyright (C) 2016 Adrian Boguszewski + * Copyright (C) 2006 Andrius R. + * Copyright (C) 2004-2005 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include "inkscape.h" +#include "preferences.h" +#include "desktop.h" +#include "document.h" +#include "ui/tools/node-tool.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tool/control-point-selection.h" +#include "object/sp-path.h" +#include "object/sp-defs.h" +#include "object/sp-shape.h" +#include "xml/repr.h" + +#define SP_SELECTION_UPDATE_PRIORITY (G_PRIORITY_HIGH_IDLE + 1) + +namespace Inkscape { + +Selection::Selection(LayerModel *layers, SPDesktop *desktop): + ObjectSet(desktop), + _layers(layers), + _selection_context(nullptr), + _flags(0), + _idle(0) +{ +} + +Selection::~Selection() { + _layers = nullptr; + if (_idle) { + g_source_remove(_idle); + _idle = 0; + } +} + +/* Handler for selected objects "modified" signal */ + +void Selection::_schedule_modified(SPObject */*obj*/, guint flags) { + if (!this->_idle) { + /* Request handling to be run in _idle loop */ + this->_idle = g_idle_add_full(SP_SELECTION_UPDATE_PRIORITY, GSourceFunc(&Selection::_emit_modified), this, nullptr); + } + + /* Collect all flags */ + this->_flags |= flags; +} + +gboolean Selection::_emit_modified(Selection *selection) +{ + /* force new handler to be created if requested before we return */ + selection->_idle = 0; + guint flags = selection->_flags; + selection->_flags = 0; + + selection->_emitModified(flags); + + /* drop this handler */ + return FALSE; +} + +void Selection::_emitModified(guint flags) { + INKSCAPE.selection_modified(this, flags); + _modified_signal.emit(this, flags); +} + +void Selection::_emitChanged(bool persist_selection_context/* = false */) { + if (persist_selection_context) { + if (nullptr == _selection_context) { + _selection_context = _layers->currentLayer(); + sp_object_ref(_selection_context, nullptr); + _context_release_connection = _selection_context->connectRelease(sigc::mem_fun(*this, &Selection::_releaseContext)); + } + } else { + _releaseContext(_selection_context); + } + + INKSCAPE.selection_changed(this); + _changed_signal.emit(this); +} + +void Selection::_releaseContext(SPObject *obj) +{ + if (nullptr == _selection_context || _selection_context != obj) + return; + + _context_release_connection.disconnect(); + + sp_object_unref(_selection_context, nullptr); + _selection_context = nullptr; +} + +SPObject *Selection::activeContext() { + if (nullptr != _selection_context) + return _selection_context; + return _layers->currentLayer(); +} + +std::vector<Inkscape::SnapCandidatePoint> Selection::getSnapPoints(SnapPreferences const *snapprefs) const { + std::vector<Inkscape::SnapCandidatePoint> p; + + if (snapprefs != nullptr){ + SnapPreferences snapprefs_dummy = *snapprefs; // create a local copy of the snapping prefs + snapprefs_dummy.setTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER, false); // locally disable snapping to the item center + auto items = const_cast<Selection *>(this)->items(); + for (auto iter = items.begin(); iter != items.end(); ++iter) { + SPItem *this_item = *iter; + this_item->getSnappoints(p, &snapprefs_dummy); + + //Include the transformation origin for snapping + //For a selection or group only the overall center is considered, not for each item individually + if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_ROTATION_CENTER)) { + p.emplace_back(this_item->getCenter(), SNAPSOURCE_ROTATION_CENTER); + } + } + } + + return p; +} + +SPObject *Selection::_objectForXMLNode(Inkscape::XML::Node *repr) const { + g_return_val_if_fail(repr != nullptr, NULL); + gchar const *id = repr->attribute("id"); + g_return_val_if_fail(id != nullptr, NULL); + SPObject *object=_layers->getDocument()->getObjectById(id); + g_return_val_if_fail(object != nullptr, NULL); + return object; +} + +size_t Selection::numberOfLayers() { + auto items = this->items(); + std::set<SPObject*> layers; + for (auto iter = items.begin(); iter != items.end(); ++iter) { + SPObject *layer = _layers->layerForObject(*iter); + layers.insert(layer); + } + + return layers.size(); +} + +size_t Selection::numberOfParents() { + auto items = this->items(); + std::set<SPObject*> parents; + for (auto iter = items.begin(); iter != items.end(); ++iter) { + SPObject *parent = (*iter)->parent; + parents.insert(parent); + } + return parents.size(); +} + +void Selection::_emitSignals() { + _emitChanged(); +} + +void Selection::_connectSignals(SPObject *object) { + _modified_connections[object] = object->connectModified(sigc::mem_fun(*this, &Selection::_schedule_modified)); +} + +void Selection::_releaseSignals(SPObject *object) { + _modified_connections[object].disconnect(); + _modified_connections.erase(object); +} + +void +Selection::emptyBackup(){ + _selected_ids.clear(); + _seldata.clear(); + params.clear(); +} + +void +Selection::setBackup () +{ + SPDesktop *desktop = this->desktop(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + Inkscape::UI::Tools::NodeTool *tool = nullptr; + if (desktop) { + Inkscape::UI::Tools::ToolBase *ec = desktop->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<Inkscape::UI::Tools::NodeTool*>(ec); + } + } + _selected_ids.clear(); + _seldata.clear(); + params.clear(); + auto items = const_cast<Selection *>(this)->items(); + for (auto iter = items.begin(); iter != items.end(); ++iter) { + SPItem *item = *iter; + std::string selected_id; + selected_id += "--id="; + selected_id += item->getId(); + params.push_back(selected_id); + _selected_ids.emplace_back(item->getId()); + } + if(tool){ + Inkscape::UI::ControlPointSelection *cps = tool->_selected_nodes; + std::list<Inkscape::UI::SelectableControlPoint *> points_list = cps->_points_list; + for (auto & i : points_list) { + Inkscape::UI::Node *node = dynamic_cast<Inkscape::UI::Node*>(i); + if (node) { + std::string id = node->nodeList().subpathList().pm().item()->getId(); + + int sp = 0; + bool found_sp = false; + for(Inkscape::UI::SubpathList::iterator i = node->nodeList().subpathList().begin(); i != node->nodeList().subpathList().end(); ++i,++sp){ + if(&**i == &(node->nodeList())){ + found_sp = true; + break; + } + } + int nl=0; + bool found_nl = false; + for (Inkscape::UI::NodeList::iterator j = node->nodeList().begin(); j != node->nodeList().end(); ++j, ++nl){ + if(&*j==node){ + found_nl = true; + break; + } + } + std::ostringstream ss; + ss<< "--selected-nodes=" << id << ":" << sp << ":" << nl; + Glib::ustring selected_nodes = ss.str(); + + if(found_nl && found_sp) { + _seldata.emplace_back(id,std::make_pair(sp,nl)); + params.push_back(selected_nodes); + } else { + g_warning("Something went wrong while trying to pass selected nodes to extension. Please report a bug."); + } + } + } + }//end add selected nodes +} + +void +Selection::restoreBackup() +{ + SPDesktop *desktop = this->desktop(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + Inkscape::UI::Tools::NodeTool *tool = nullptr; + if (desktop) { + Inkscape::UI::Tools::ToolBase *ec = desktop->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<Inkscape::UI::Tools::NodeTool*>(ec); + } + } + clear(); + std::vector<std::string>::iterator it = _selected_ids.begin(); + for (; it!= _selected_ids.end(); ++it){ + SPItem * item = dynamic_cast<SPItem *>(document->getObjectById(it->c_str())); + SPDefs * defs = document->getDefs(); + if (item && !defs->isAncestorOf(item)) { + add(item); + } + } + if (tool) { + Inkscape::UI::ControlPointSelection *cps = tool->_selected_nodes; + cps->selectAll(); + std::list<Inkscape::UI::SelectableControlPoint *> points_list = cps->_points_list; + cps->clear(); + Inkscape::UI::Node * node = dynamic_cast<Inkscape::UI::Node*>(*points_list.begin()); + if (node) { + Inkscape::UI::SubpathList sp = node->nodeList().subpathList(); + for (auto & l : _seldata) { + SPPath * path = dynamic_cast<SPPath *>(document->getObjectById(l.first)); + gint sp_count = 0; + for (Inkscape::UI::SubpathList::iterator j = sp.begin(); j != sp.end(); ++j, ++sp_count) { + if(sp_count == l.second.first) { + gint nt_count = 0; + for (Inkscape::UI::NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k, ++nt_count) { + if(nt_count == l.second.second) { + cps->insert(k.ptr()); + break; + } + } + break; + } + } + } + } + points_list.clear(); + } +} + + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/selection.h b/src/selection.h new file mode 100644 index 0000000..f6d0934 --- /dev/null +++ b/src/selection.h @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_SELECTION_H +#define SEEN_INKSCAPE_SELECTION_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Adrian Boguszewski + * + * Copyright (C) 2016 Adrian Boguszewski + * Copyright (C) 2004-2005 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include <map> +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "inkgc/gc-managed.h" +#include "gc-finalized.h" +#include "gc-anchored.h" +#include "object/object-set.h" + + +namespace Inkscape { +class LayerModel; +namespace XML { +class Node; +} +} + +namespace Inkscape { + +/** + * The set of selected SPObjects for a given document and layer model. + * + * This class represents the set of selected SPItems for a given + * document (referenced in LayerModel). + * + * An SPObject and its parent cannot be simultaneously selected; + * selecting an SPObjects has the side-effect of unselecting any of + * its children which might have been selected. + * + * This is a per-desktop object that keeps the list of selected objects + * at the given desktop. Both SPItem and SPRepr lists can be retrieved + * from the selection. Many actions operate on the selection, so it is + * widely used throughout the code. + * It also implements its own asynchronous notification signals that + * UI elements can listen to. + */ +class Selection : public Inkscape::GC::Managed<>, + public Inkscape::GC::Finalized, + public Inkscape::GC::Anchored, + public ObjectSet +{ +friend class ObjectSet; +public: + /** + * Constructs an selection object, bound to a particular + * layer model + * + * @param layers the layer model (for the SPDesktop, if GUI) + * @param desktop the desktop associated with the layer model, or NULL if in console mode + */ + Selection(LayerModel *layers, SPDesktop *desktop); + ~Selection() override; + + /** no copy. */ + Selection(Selection const &) = delete; + /** no assign. */ + void operator=(Selection const &) = delete; + + /** + * Returns the layer model the selection is bound to (works in console or GUI mode) + * + * @return the layer model the selection is bound to, which is the same as the desktop + * layer model for GUI mode + */ + LayerModel *layers() { return _layers; } + + /** + * Returns active layer for selection (currentLayer or its parent). + * + * @return layer item the selection is bound to + */ + SPObject *activeContext(); + + using ObjectSet::add; + + /** + * Add an XML node's SPObject to the set of selected objects. + * + * @param the xml node of the item to add + */ + void add(XML::Node *repr) { + add(_objectForXMLNode(repr)); + } + + using ObjectSet::set; + + /** + * Set the selection to an XML node's SPObject. + * + * @param repr the xml node of the item to select + */ + void set(XML::Node *repr) { + set(_objectForXMLNode(repr)); + } + + using ObjectSet::remove; + + /** + * Removes an item from the set of selected objects. + * + * It is ok to call this method for an unselected item. + * + * @param repr the xml node of the item to remove + */ + void remove(XML::Node *repr) { + remove(_objectForXMLNode(repr)); + } + + using ObjectSet::includes; + + /** + * Returns true if the given item is selected. + */ + bool includes(XML::Node *repr) { + return includes(_objectForXMLNode(repr)); + } + + /** Returns the number of layers in which there are selected objects. */ + size_t numberOfLayers(); + + /** Returns the number of parents to which the selected objects belong. */ + size_t numberOfParents(); + + /** + * Compute the list of points in the selection that are to be considered for snapping from. + * + * @return Selection's snap points + */ + std::vector<Inkscape::SnapCandidatePoint> getSnapPoints(SnapPreferences const *snapprefs) const; + + /** + * Connects a slot to be notified of selection changes. + * + * This method connects the given slot such that it will + * be called upon any change in the set of selected objects. + * + * @param slot the slot to connect + * + * @return the resulting connection + */ + void emitModified(){ _emitModified(this->_flags); }; + sigc::connection connectChanged(sigc::slot<void, Selection *> const &slot) { + return _changed_signal.connect(slot); + } + sigc::connection connectChangedFirst(sigc::slot<void, Selection *> const &slot) + { + return _changed_signal.slots().insert(_changed_signal.slots().begin(), slot); + } + + /** + * Connects a slot to be notified of selected object modifications. + * + * This method connects the given slot such that it will + * receive notifications whenever any selected item is + * modified. + * + * @param slot the slot to connect + * + * @return the resulting connection + * + */ + sigc::connection connectModified(sigc::slot<void, Selection *, unsigned int> const &slot) + { + return _modified_signal.connect(slot); + } + sigc::connection connectModifiedFirst(sigc::slot<void, Selection *, unsigned int> const &slot) + { + return _modified_signal.slots().insert(_modified_signal.slots().begin(), slot); + } + + /** + * Set a backup of current selection and store it also to be command line readable by extension system + */ + void setBackup(); + /** + * Clear backup of current selection + */ + void emptyBackup(); + /** + * Restore a selection from a existing backup + */ + void restoreBackup(); + /** + * Here store a paramlist when set backup + */ + std::list<std::string> params; + +protected: + void _emitSignals() override; + void _connectSignals(SPObject* object) override; + void _releaseSignals(SPObject* object) override; + +private: + /** Issues modification notification signals. */ + static int _emit_modified(Selection *selection); + /** Schedules an item modification signal to be sent. */ + void _schedule_modified(SPObject *obj, unsigned int flags); + + /** Issues modified selection signal. */ + void _emitModified(unsigned int flags); + /** Issues changed selection signal. */ + void _emitChanged(bool persist_selection_context = false); + /** returns the SPObject corresponding to an xml node (if any). */ + SPObject *_objectForXMLNode(XML::Node *repr) const; + /** Releases an active layer object that is being removed. */ + void _releaseContext(SPObject *obj); + + LayerModel *_layers; + SPObject* _selection_context; + unsigned int _flags; + unsigned int _idle; + std::vector<std::pair<std::string, std::pair<int, int> > > _seldata; + std::vector<std::string> _selected_ids; + std::map<SPObject *, sigc::connection> _modified_connections; + sigc::connection _context_release_connection; + + sigc::signal<void, Selection *> _changed_signal; + sigc::signal<void, Selection *, unsigned int> _modified_signal; +}; + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/seltrans-handles.cpp b/src/seltrans-handles.cpp new file mode 100644 index 0000000..1c5ed73 --- /dev/null +++ b/src/seltrans-handles.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "seltrans-handles.h" + +#ifdef __cplusplus +#undef N_ +#define N_(x) x +#endif + +SPSelTransTypeInfo const handtypes[] = { + { DEF_COLOR, N_("<b>Squeeze or stretch</b> selection; with <b>Ctrl</b> to scale uniformly; with <b>Shift</b> to scale around rotation center") }, + { DEF_COLOR, N_("<b>Scale</b> selection; with <b>Ctrl</b> to scale uniformly; with <b>Shift</b> to scale around rotation center") }, + { DEF_COLOR, N_("<b>Skew</b> selection; with <b>Ctrl</b> to snap angle; with <b>Shift</b> to skew around the opposite side") }, + { DEF_COLOR, N_("<b>Rotate</b> selection; with <b>Ctrl</b> to snap angle; with <b>Shift</b> to rotate around the opposite corner") }, + { CEN_COLOR, N_("<b>Center</b> of rotation and skewing: drag to reposition; scaling with Shift also uses this center") }, + { DEF_COLOR, N_("<b>Align</b> objects to the side clicked; <b>Shift</b> click to invert side; <b>Ctrl</b> to group whole selection.") }, + { DEF_COLOR, N_("<b>Align</b> objects to center; <b>Shift</b> click to center vertically instead of horizontally.") } +}; + +SPSelTransHandle const hands[] = { +//center handle will be 0 so we can reference it quickly. + {HANDLE_CENTER, SP_ANCHOR_CENTER, GDK_CROSSHAIR, 12, 0.5, 0.5}, +//handle-type anchor-nudge cursor image x y + {HANDLE_SCALE, SP_ANCHOR_SE, GDK_TOP_LEFT_CORNER, 0, 0, 1}, + {HANDLE_STRETCH, SP_ANCHOR_S, GDK_TOP_SIDE, 3, 0.5, 1}, + {HANDLE_SCALE, SP_ANCHOR_SW, GDK_TOP_RIGHT_CORNER, 1, 1, 1}, + {HANDLE_STRETCH, SP_ANCHOR_W, GDK_RIGHT_SIDE, 2, 1, 0.5}, + {HANDLE_SCALE, SP_ANCHOR_NW, GDK_BOTTOM_RIGHT_CORNER, 0, 1, 0}, + {HANDLE_STRETCH, SP_ANCHOR_N, GDK_BOTTOM_SIDE, 3, 0.5, 0}, + {HANDLE_SCALE, SP_ANCHOR_NE, GDK_BOTTOM_LEFT_CORNER, 1, 0, 0}, + {HANDLE_STRETCH, SP_ANCHOR_E, GDK_LEFT_SIDE, 2, 0, 0.5}, + {HANDLE_ROTATE, SP_ANCHOR_SE, GDK_EXCHANGE, 4, 0, 1}, + {HANDLE_SKEW, SP_ANCHOR_S, GDK_SB_H_DOUBLE_ARROW, 8, 0.5, 1}, + {HANDLE_ROTATE, SP_ANCHOR_SW, GDK_EXCHANGE, 5, 1, 1}, + {HANDLE_SKEW, SP_ANCHOR_W, GDK_SB_V_DOUBLE_ARROW, 9, 1, 0.5}, + {HANDLE_ROTATE, SP_ANCHOR_NW, GDK_EXCHANGE, 6, 1, 0}, + {HANDLE_SKEW, SP_ANCHOR_N, GDK_SB_H_DOUBLE_ARROW, 10, 0.5, 0}, + {HANDLE_ROTATE, SP_ANCHOR_NE, GDK_EXCHANGE, 7, 0, 0}, + {HANDLE_SKEW, SP_ANCHOR_E, GDK_SB_V_DOUBLE_ARROW, 11, 0, 0.5}, + {HANDLE_ALIGN, SP_ANCHOR_S, GDK_TOP_SIDE, 13, 0.5, 1}, + {HANDLE_ALIGN, SP_ANCHOR_W, GDK_RIGHT_SIDE, 14, 1, 0.5}, + {HANDLE_ALIGN, SP_ANCHOR_N, GDK_BOTTOM_SIDE, 15, 0.5, 0}, + {HANDLE_ALIGN, SP_ANCHOR_E, GDK_LEFT_SIDE, 16, 0, 0.5}, + {HANDLE_CENTER_ALIGN, SP_ANCHOR_CENTER, GDK_CROSSHAIR, 17, 0.5, 0.5}, + {HANDLE_ALIGN, SP_ANCHOR_SE, GDK_TOP_LEFT_CORNER, 18, 0, 1}, + {HANDLE_ALIGN, SP_ANCHOR_SW, GDK_TOP_RIGHT_CORNER, 19, 1, 1}, + {HANDLE_ALIGN, SP_ANCHOR_NW, GDK_BOTTOM_RIGHT_CORNER, 20, 1, 0}, + {HANDLE_ALIGN, SP_ANCHOR_NE, GDK_BOTTOM_LEFT_CORNER, 21, 0, 0}, +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/seltrans-handles.h b/src/seltrans-handles.h new file mode 100644 index 0000000..9811b13 --- /dev/null +++ b/src/seltrans-handles.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SELTRANS_HANDLES_H +#define SEEN_SP_SELTRANS_HANDLES_H + +/* + * Seltrans knots + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <gdk/gdk.h> + +#include "enums.h" +#include "verbs.h" + +typedef unsigned int guint32; + +namespace Inkscape { + class SelTrans; +} + +// Colours are RRGGBBAA: FILL, OVER&DRAG, STROKE, OVER&DRAG +guint32 const DEF_COLOR[] = { 0x000000ff, 0x00ff6600, 0x000000ff, 0x000000ff }; +guint32 const CEN_COLOR[] = { 0x00000000, 0x00000000, 0x000000ff, 0xff0000b0 }; + +enum SPSelTransType { + HANDLE_STRETCH, + HANDLE_SCALE, + HANDLE_SKEW, + HANDLE_ROTATE, + HANDLE_CENTER, + HANDLE_ALIGN, + HANDLE_CENTER_ALIGN +}; + +// Which handle does what in the alignment (clicking) +const int AlignVerb[18] = { + // Left Click + SP_VERB_ALIGN_VERTICAL_TOP, + SP_VERB_ALIGN_HORIZONTAL_RIGHT, + SP_VERB_ALIGN_VERTICAL_BOTTOM, + SP_VERB_ALIGN_HORIZONTAL_LEFT, + SP_VERB_ALIGN_VERTICAL_CENTER, + SP_VERB_ALIGN_BOTH_TOP_LEFT, + SP_VERB_ALIGN_BOTH_TOP_RIGHT, + SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT, + SP_VERB_ALIGN_BOTH_BOTTOM_LEFT, + // Shift Click + SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR, + SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR, + SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR, + SP_VERB_ALIGN_HORIZONTAL_CENTER, + SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR, +}; +// Ofset from the index in the handle list to the index in the verb list. +const int AlignHandleToVerb = -13; +// Offset for moving from Left click to Shift Click +const int AlignShiftVerb = 9; + +struct SPSelTransTypeInfo { + guint32 const *color; + char const *tip; +}; +// One per handle type in order +extern SPSelTransTypeInfo const handtypes[7]; + +struct SPSelTransHandle; + +struct SPSelTransHandle { + SPSelTransType type; + SPAnchorType anchor; + GdkCursorType cursor; + unsigned int control; + gdouble x, y; +}; +// These are 4 * each handle type + 1 for center +int const NUMHANDS = 26; +extern SPSelTransHandle const hands[NUMHANDS]; + +#endif // SEEN_SP_SELTRANS_HANDLES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/seltrans.cpp b/src/seltrans.cpp new file mode 100644 index 0000000..96e6b2c --- /dev/null +++ b/src/seltrans.cpp @@ -0,0 +1,1712 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Helper object for transforming selected items. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 1999-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/transforms.h> + +#include "seltrans.h" + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "knot.h" +#include "message-stack.h" +#include "mod360.h" +#include "pure-transform.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "seltrans-handles.h" +#include "verbs.h" + +#include "display/snap-indicator.h" +#include "display/sodipodi-ctrl.h" +#include "display/sp-ctrlline.h" +#include "display/guideline.h" + +#include "helper/action.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/control-manager.h" +#include "ui/tools/select-tool.h" + +using Inkscape::ControlManager; +using Inkscape::DocumentUndo; + +static void sp_sel_trans_handle_grab(SPKnot *knot, guint state, SPSelTransHandle const* data); +static void sp_sel_trans_handle_ungrab(SPKnot *knot, guint state, SPSelTransHandle const* data); +static void sp_sel_trans_handle_click(SPKnot *knot, guint state, SPSelTransHandle const* data); +static void sp_sel_trans_handle_new_event(SPKnot *knot, Geom::Point const &position, guint32 state, SPSelTransHandle const* data); +static gboolean sp_sel_trans_handle_request(SPKnot *knot, Geom::Point *p, guint state, SPSelTransHandle const *data); + +extern GdkPixbuf *handles[]; + +static gboolean sp_sel_trans_handle_event(SPKnot *knot, GdkEvent *event, SPSelTransHandle const*) +{ + switch (event->type) { + case GDK_MOTION_NOTIFY: + break; + case GDK_KEY_PRESS: + if (Inkscape::UI::Tools::get_latin_keyval (&event->key) == GDK_KEY_space) { + /* stamping mode: both mode(show content and outline) operation with knot */ + if (!SP_KNOT_IS_GRABBED(knot)) { + return FALSE; + } + SPDesktop *desktop = knot->desktop; + Inkscape::SelTrans *seltrans = SP_SELECT_CONTEXT(desktop->event_context)->_seltrans; + seltrans->stamp(); + return TRUE; + } + break; + default: + break; + } + + return FALSE; +} + +Inkscape::SelTrans::BoundingBoxPrefsObserver::BoundingBoxPrefsObserver(SelTrans &sel_trans) : + Observer("/tools/bounding_box"), + _sel_trans(sel_trans) +{ +} + +void Inkscape::SelTrans::BoundingBoxPrefsObserver::notify(Preferences::Entry const &val) +{ + _sel_trans._boundingBoxPrefsChanged(static_cast<int>(val.getBool())); +} + +Inkscape::SelTrans::SelTrans(SPDesktop *desktop) : + _desktop(desktop), + _selcue(desktop), + _state(STATE_SCALE), + _show(SHOW_CONTENT), + _grabbed(false), + _show_handles(true), + _bbox(), + _visual_bbox(), + _absolute_affine(Geom::Scale(1,1)), + _opposite(Geom::Point(0,0)), + _opposite_for_specpoints(Geom::Point(0,0)), + _opposite_for_bboxpoints(Geom::Point(0,0)), + _origin_for_specpoints(Geom::Point(0,0)), + _origin_for_bboxpoints(Geom::Point(0,0)), + _stamp_cache(std::vector<SPItem*>()), + _message_context(desktop->messageStack()), + _bounding_box_prefs_observer(*this) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + _snap_bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + + g_return_if_fail(desktop != nullptr); + + _updateVolatileState(); + _current_relative_affine.setIdentity(); + + _center_is_set = false; // reread _center from items, or set to bbox midpoint + + _makeHandles(); + _updateHandles(); + + _selection = desktop->getSelection(); + + _norm = sp_canvas_item_new(desktop->getControls(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "mode", SP_CTRL_MODE_XOR, + "shape", SP_CTRL_SHAPE_BITMAP, + "size", 13, + "filled", TRUE, + "fill_color", 0x00000000, + "stroked", TRUE, + "stroke_color", 0xff0000b0, + "pixbuf", handles[12], + NULL); + + _grip = sp_canvas_item_new(desktop->getControls(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "mode", SP_CTRL_MODE_XOR, + "shape", SP_CTRL_SHAPE_CROSS, + "size", 7, + "filled", TRUE, + "fill_color", 0xffffff7f, + "stroked", TRUE, + "stroke_color", 0xff0000b0, + "pixbuf", handles[12], + NULL); + + sp_canvas_item_hide(_grip); + sp_canvas_item_hide(_norm); + + for (auto & i : _l) { + i = ControlManager::getManager().createControlLine(desktop->getControls()); + sp_canvas_item_hide(i); + } + + _sel_changed_connection = _selection->connectChanged( + sigc::mem_fun(*this, &Inkscape::SelTrans::_selChanged) + ); + + _sel_modified_connection = _selection->connectModified( + sigc::mem_fun(*this, &Inkscape::SelTrans::_selModified) + ); + + _all_snap_sources_iter = _all_snap_sources_sorted.end(); + + prefs->addObserver(_bounding_box_prefs_observer); +} + +Inkscape::SelTrans::~SelTrans() +{ + _sel_changed_connection.disconnect(); + _sel_modified_connection.disconnect(); + + for (auto & knot : knots) { + knot_unref(knot); + knot = nullptr; + } + + if (_norm) { + sp_canvas_item_destroy(_norm); + _norm = nullptr; + } + if (_grip) { + sp_canvas_item_destroy(_grip); + _grip = nullptr; + } + for (auto & i : _l) { + if (i) { + sp_canvas_item_destroy(i); + i = nullptr; + } + } + + for (auto & _item : _items) { + sp_object_unref(_item, nullptr); + } + + _items.clear(); + _items_const.clear(); + _items_affines.clear(); + _items_centers.clear(); +} + +void Inkscape::SelTrans::resetState() +{ + _state = STATE_SCALE; +} + +void Inkscape::SelTrans::increaseState() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_align = prefs->getBool("/dialogs/align/oncanvas", false); + + if (_state == STATE_SCALE) { + _state = STATE_ROTATE; + } else if (_state == STATE_ROTATE && show_align) { + _state = STATE_ALIGN; + } else { + _state = STATE_SCALE; + } + + _center_is_set = true; // no need to reread center + + _updateHandles(); +} + +void Inkscape::SelTrans::setCenter(Geom::Point const &p) +{ + _center = p; + _center_is_set = true; + + // Write the new center position into all selected items + auto items= _desktop->selection->items(); + for (auto iter=items.begin();iter!=items.end(); ++iter) { + SPItem *it = SP_ITEM(*iter); + it->setCenter(p); + // only set the value; updating repr and document_done will be done once, on ungrab + } + + _updateHandles(); +} + +void Inkscape::SelTrans::grab(Geom::Point const &p, gdouble x, gdouble y, bool show_handles, bool translating) +{ + // While dragging a handle, we will either scale, skew, or rotate and the "translating" parameter will be false + // When dragging the selected item itself however, we will translate the selection and that parameter will be true + Inkscape::Selection *selection = _desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + g_return_if_fail(!_grabbed); + + _grabbed = true; + _show_handles = show_handles; + _updateVolatileState(); + _current_relative_affine.setIdentity(); + + _changed = false; + + if (_empty) { + return; + } + + auto items= _desktop->selection->items(); + for (auto iter=items.begin();iter!=items.end(); ++iter) { + SPItem *it = static_cast<SPItem*>(sp_object_ref(*iter, nullptr)); + _items.push_back(it); + _items_const.push_back(it); + _items_affines.push_back(it->i2dt_affine()); + _items_centers.push_back(it->getCenter()); // for content-dragging, we need to remember original centers + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(it); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + sp_lpe_item_update_patheffect(lpeitem, false, false); + } + } + + if (y != -1 && _desktop->is_yaxisdown()) { + y = 1 - y; + } + + _handle_x = x; + _handle_y = y; + + // The selector tool should snap the bbox, special snappoints, and path nodes + // (The special points are the handles, center, rotation axis, font baseline, ends of spiral, etc.) + + // First, determine the bounding box + _bbox = selection->bounds(_snap_bbox_type); + _visual_bbox = selection->visualBounds(); // Used for correctly scaling the strokewidth + _geometric_bbox = selection->geometricBounds(); + + _point = p; + if (_geometric_bbox) { + _point_geom = _geometric_bbox->min() + _geometric_bbox->dimensions() * Geom::Scale(x, y); + } else { + _point_geom = p; + } + + // Next, get all points to consider for snapping + SnapManager const &m = _desktop->namedview->snap_manager; + _snap_points.clear(); + if (m.someSnapperMightSnap(false)) { // Only search for snap sources when really needed, to avoid unnecessary delays + _snap_points = selection->getSnapPoints(&m.snapprefs); // This might take some time! + } + if (_snap_points.size() > 200 && !(prefs->getBool("/options/snapclosestonly/value", false))) { + /* Snapping a huge number of nodes will take way too long, so limit the number of snappable nodes + A typical user would rarely ever try to snap such a large number of nodes anyway, because + (s)he would hardly be able to discern which node would be snapping */ + std::cout << "Warning: limit of 200 snap sources reached, some will be ignored" << std::endl; + _snap_points.resize(200); + // Unfortunately, by now we will have lost the font-baseline snappoints :-( + } + + // Find bbox hulling all special points, which excludes stroke width. Here we need to include the + // path nodes, for example because a rectangle which has been converted to a path doesn't have + // any other special points + Geom::OptRect snap_points_bbox = selection->bounds(SPItem::GEOMETRIC_BBOX); + + _bbox_points.clear(); + // Collect the bounding box's corners and midpoints for each selected item + if (m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) { + bool c = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CORNER); + bool mp = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_MIDPOINT); + bool emp = m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_EDGE_MIDPOINT); + // Preferably we'd use the bbox of each selected item, but for example 50 items will produce at least 200 bbox points, + // which might make Inkscape crawl(see the comment a few lines above). In that case we will use the bbox of the selection as a whole + bool c1 = (_items.size() > 0) && (_items.size() < 50); + bool c2 = prefs->getBool("/options/snapclosestonly/value", false); + if (translating && (c1 || c2)) { + // Get the bounding box points for each item in the selection + for (auto & _item : _items) { + Geom::OptRect b = _item->desktopBounds(_snap_bbox_type); + getBBoxPoints(b, &_bbox_points, false, c, emp, mp); + } + } else { + // Only get the bounding box points of the selection as a whole + getBBoxPoints(selection->bounds(_snap_bbox_type), &_bbox_points, false, c, emp, mp); + } + } + + if (_bbox) { + // There are two separate "opposites" (i.e. opposite w.r.t. the handle being dragged): + // - one for snapping the boundingbox, which can be either visual or geometric + // - one for snapping the special points + // The "opposite" in case of a geometric boundingbox always coincides with the "opposite" for the special points + // These distinct "opposites" are needed in the snapmanager to avoid bugs such as LP167905 (in which + // a box is caught between two guides) + _opposite_for_bboxpoints = _bbox->min() + _bbox->dimensions() * Geom::Scale(1-x, 1-y); + if (snap_points_bbox) { + _opposite_for_specpoints = (*snap_points_bbox).min() + (*snap_points_bbox).dimensions() * Geom::Scale(1-x, 1-y); + } else { + _opposite_for_specpoints = _opposite_for_bboxpoints; + } + _opposite = _opposite_for_bboxpoints; + } + + // When snapping the node closest to the mouse pointer is absolutely preferred over the closest snap + // (i.e. when weight == 1), then we will not even try to snap to other points and disregard those other points + + if (prefs->getBool("/options/snapclosestonly/value", false)) { + _keepClosestPointOnly(p); + } + + if ((x != -1) && (y != -1)) { + sp_canvas_item_show(_norm); + sp_canvas_item_show(_grip); + } + + if (_show == SHOW_OUTLINE) { + for (auto & i : _l) + sp_canvas_item_show(i); + } + + _updateHandles(); + g_return_if_fail(_stamp_cache.empty()); +} + +void Inkscape::SelTrans::transform(Geom::Affine const &rel_affine, Geom::Point const &norm) +{ + g_return_if_fail(_grabbed); + g_return_if_fail(!_empty); + + Geom::Affine const affine( Geom::Translate(-norm) * rel_affine * Geom::Translate(norm) ); + + if (_show == SHOW_CONTENT) { + // update the content + for (unsigned i = 0; i < _items.size(); i++) { + SPItem &item = *_items[i]; + if( SP_IS_ROOT(&item) ) { + _desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Cannot transform an embedded SVG.")); + break; + } + Geom::Affine const &prev_transform = _items_affines[i]; + item.set_i2d_affine(prev_transform * affine); + // The new affine will only have been applied if the transformation is different from the previous one, see SPItem::set_item_transform + } + } else { + if (_bbox) { + Geom::Point p[4]; + /* update the outline */ + for (unsigned i = 0 ; i < 4 ; i++) { + p[i] = _bbox->corner(i) * affine; + } + for (unsigned i = 0 ; i < 4 ; i++) { + _l[i]->setCoords(p[i], p[(i+1)%4]); + } + } + } + + _current_relative_affine = affine; + _changed = true; + _updateHandles(); +} + +void Inkscape::SelTrans::ungrab() +{ + g_return_if_fail(_grabbed); + _grabbed = false; + _show_handles = true; + + _desktop->snapindicator->remove_snapsource(); + + Inkscape::Selection *selection = _desktop->getSelection(); + _updateVolatileState(); + + for (auto & _item : _items) { + sp_object_unref(_item, nullptr); + } + + sp_canvas_item_hide(_norm); + sp_canvas_item_hide(_grip); + + if (_show == SHOW_OUTLINE) { + for (auto & i : _l) + sp_canvas_item_hide(i); + } + if(!_stamp_cache.empty()){ + _stamp_cache.clear(); + } + + _message_context.clear(); + + if (!_empty && _changed) { + if (!_current_relative_affine.isIdentity()) { // we can have a identity affine + // when trying to stretch a perfectly vertical line in horizontal direction, which will not be allowed by the handles; + + selection->applyAffine(_current_relative_affine, (_show == SHOW_OUTLINE) ? true : false); + if (_center) { + *_center *= _current_relative_affine; + _center_is_set = true; + } + + // If dragging showed content live, sp_selection_apply_affine cannot change the centers + // appropriately - it does not know the original positions of the centers (all objects already have + // the new bboxes). So we need to reset the centers from our saved array. + if (_show != SHOW_OUTLINE && !_current_relative_affine.isTranslation()) { + for (unsigned i = 0; i < _items_centers.size(); i++) { + SPItem *currentItem = _items[i]; + if (currentItem->isCenterSet()) { // only if it's already set + currentItem->setCenter (_items_centers[i] * _current_relative_affine); + currentItem->updateRepr(); + } + } + } + } + + _items.clear(); + _items_const.clear(); + _items_affines.clear(); + _items_centers.clear(); + + if (!_current_relative_affine.isIdentity()) { // we can have a identity affine + // when trying to stretch a perfectly vertical line in horizontal direction, which will not be allowed + // by the handles; this would be identified as a (zero) translation by isTranslation() + if (_current_relative_affine.isTranslation()) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Move")); + } else if (_current_relative_affine.withoutTranslation().isScale()) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Scale")); + } else if (_current_relative_affine.withoutTranslation().isRotation()) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Rotate")); + } else { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Skew")); + } + } else { + _updateHandles(); + } + + } else { + + if (_center_is_set) { + // we were dragging center; update reprs and commit undoable action + auto items= _desktop->selection->items(); + for (auto iter=items.begin();iter!=items.end(); ++iter) { + SPItem *it = *iter; + it->updateRepr(); + } + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Set center")); + } + + _items.clear(); + _items_const.clear(); + _items_affines.clear(); + _items_centers.clear(); + _updateHandles(); + } +} + +/* fixme: This is really bad, as we compare positions for each stamp (Lauris) */ +/* fixme: IMHO the best way to keep sort cache would be to implement timestamping at last */ + +void Inkscape::SelTrans::stamp() +{ + Inkscape::Selection *selection = _desktop->getSelection(); + + bool fixup = !_grabbed; + if ( fixup && !_stamp_cache.empty() ) { + // TODO - give a proper fix. Simple temporary work-around for the grab() issue + _stamp_cache.clear(); + } + + /* stamping mode */ + if (!_empty) { + std::vector<SPItem*> l; + if (!_stamp_cache.empty()) { + l = _stamp_cache; + } else { + /* Build cache */ + l.insert(l.end(), selection->items().begin(), selection->items().end()); + sort(l.begin(), l.end(), sp_object_compare_position_bool); + _stamp_cache = l; + } + + for(auto original_item : l) { + Inkscape::XML::Node *original_repr = original_item->getRepr(); + + // remember parent + Inkscape::XML::Node *parent = original_repr->parent(); + + Inkscape::XML::Node *copy_repr = original_repr->duplicate(parent->document()); + + // add the new repr to the parent + parent->addChild(copy_repr, original_repr->prev()); + + SPItem *copy_item = (SPItem *) _desktop->getDocument()->getObjectByRepr(copy_repr); + + Geom::Affine const *new_affine; + if (_show == SHOW_OUTLINE) { + Geom::Affine const i2d(original_item->i2dt_affine()); + Geom::Affine const i2dnew( i2d * _current_relative_affine ); + copy_item->set_i2d_affine(i2dnew); + new_affine = ©_item->transform; + } else { + new_affine = &original_item->transform; + } + + copy_item->doWriteTransform(*new_affine); + + if ( copy_item->isCenterSet() && _center ) { + copy_item->setCenter(*_center * _current_relative_affine); + } + Inkscape::GC::release(copy_repr); + SPLPEItem * lpeitem = dynamic_cast<SPLPEItem *>(copy_item); + if(lpeitem && lpeitem->hasPathEffectRecursive()) { + lpeitem->forkPathEffectsIfNecessary(1); + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + } + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Stamp")); + } + + if ( fixup && !_stamp_cache.empty() ) { + // TODO - give a proper fix. Simple temporary work-around for the grab() issue + _stamp_cache.clear(); + } +} + +void Inkscape::SelTrans::_updateHandles() +{ + for (auto & knot : knots) + knot->hide(); + + if ( !_show_handles || _empty ) + return; + + if (!_center_is_set) { + _center = _desktop->selection->center(); + _center_is_set = true; + } + + if ( _state == STATE_SCALE ) { + _showHandles(HANDLE_STRETCH); + _showHandles(HANDLE_SCALE); + } else if(_state == STATE_ALIGN) { + _showHandles(HANDLE_ALIGN); + _showHandles(HANDLE_CENTER_ALIGN); + } else { + _showHandles(HANDLE_SKEW); + _showHandles(HANDLE_ROTATE); + _showHandles(HANDLE_CENTER); + } +} + +void Inkscape::SelTrans::_updateVolatileState() +{ + Inkscape::Selection *selection = _desktop->getSelection(); + _empty = selection->isEmpty(); + + if (_empty) { + return; + } + + //Update the bboxes + _bbox = selection->bounds(_snap_bbox_type); + _visual_bbox = selection->visualBounds(); + + if (!_bbox) { + _empty = true; + return; + } + + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + _strokewidth = stroke_average_width(vec); +} + +void Inkscape::SelTrans::_showHandles(SPSelTransType type) +{ + // shouldn't have nullary bbox, but knots + g_assert(_bbox); + + auto const y_dir = _desktop->yaxisdir(); + + for (int i = 0; i < NUMHANDS; i++) { + if (hands[i].type != type) + continue; + + // Position knots to scale the selection bbox + Geom::Point const bpos(hands[i].x, (hands[i].y - 0.5) * (-y_dir) + 0.5); + Geom::Point p(_bbox->min() + (_bbox->dimensions() * Geom::Scale(bpos))); + knots[i]->moveto(p); + knots[i]->show(); + + // This controls the center handle's position, because the default can + // be moved and needs to be remembered. + if( type == HANDLE_CENTER && _center ) + knots[i]->moveto(*_center); + } +} + +void Inkscape::SelTrans::_makeHandles() +{ + for (int i = 0; i < NUMHANDS; i++) { + SPSelTransTypeInfo info = handtypes[hands[i].type]; + knots[i] = new SPKnot(_desktop, _(info.tip)); + + knots[i]->setShape(SP_CTRL_SHAPE_BITMAP); + knots[i]->setSize(13); + knots[i]->setAnchor(hands[i].anchor); + knots[i]->setMode(SP_CTRL_MODE_XOR); + knots[i]->setFill(info.color[0], info.color[1], info.color[1], info.color[1]); + knots[i]->setStroke(info.color[2], info.color[3], info.color[3], info.color[3]); + + knots[i]->setPixbuf(handles[hands[i].control]); + knots[i]->updateCtrl(); + + knots[i]->request_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_request), &hands[i])); + knots[i]->moved_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_new_event), &hands[i])); + knots[i]->grabbed_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_grab), &hands[i])); + knots[i]->ungrabbed_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_ungrab), &hands[i])); + knots[i]->click_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_click), &hands[i])); + knots[i]->event_signal.connect(sigc::bind(sigc::ptr_fun(sp_sel_trans_handle_event), &hands[i])); + } +} + +static void sp_sel_trans_handle_grab(SPKnot *knot, guint state, SPSelTransHandle const* data) +{ + SP_SELECT_CONTEXT(knot->desktop->event_context)->_seltrans->handleGrab( + knot, state, *(SPSelTransHandle const *) data + ); +} + +static void sp_sel_trans_handle_ungrab(SPKnot *knot, guint /*state*/, SPSelTransHandle const* /*data*/) +{ + SP_SELECT_CONTEXT(knot->desktop->event_context)->_seltrans->ungrab(); +} + +static void sp_sel_trans_handle_new_event(SPKnot *knot, Geom::Point const& position, guint state, SPSelTransHandle const *data) +{ + Geom::Point pos = position; + + SP_SELECT_CONTEXT(knot->desktop->event_context)->_seltrans->handleNewEvent( + knot, &pos, state, *(SPSelTransHandle const *) data + ); +} + +static gboolean sp_sel_trans_handle_request(SPKnot *knot, Geom::Point *position, guint state, SPSelTransHandle const *data) +{ + return SP_SELECT_CONTEXT(knot->desktop->event_context)->_seltrans->handleRequest( + knot, position, state, *(SPSelTransHandle const *) data + ); +} + +static void sp_sel_trans_handle_click(SPKnot *knot, guint state, SPSelTransHandle const* data) +{ + SP_SELECT_CONTEXT(knot->desktop->event_context)->_seltrans->handleClick( + knot, state, *(SPSelTransHandle const *) data + ); +} + +void Inkscape::SelTrans::handleClick(SPKnot */*knot*/, guint state, SPSelTransHandle const &handle) +{ + switch (handle.type) { + case HANDLE_CENTER: + if (state & GDK_SHIFT_MASK) { + // Unset the center position for all selected items + auto items = _desktop->selection->items(); + for (auto iter=items.begin();iter!=items.end(); ++iter) { + SPItem *it = *iter; + it->unsetCenter(); + it->updateRepr(); + _center_is_set = false; // center has changed + _updateHandles(); + } + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SELECT, + _("Reset center")); + } + break; + case HANDLE_ALIGN: + case HANDLE_CENTER_ALIGN: + align(state, handle); + default: + break; + } +} + +void Inkscape::SelTrans::handleGrab(SPKnot *knot, guint /*state*/, SPSelTransHandle const &handle) +{ + grab(knot->position(), handle.x, handle.y, FALSE, FALSE); + + // Forcing handles visibility must be done after grab() to be effective + switch (handle.type) { + case HANDLE_CENTER: + g_object_set(G_OBJECT(_grip), + "shape", SP_CTRL_SHAPE_BITMAP, + "size", 13, + NULL); + sp_canvas_item_hide(_norm); + sp_canvas_item_show(_grip); + break; + default: + g_object_set(G_OBJECT(_grip), + "shape", SP_CTRL_SHAPE_CROSS, + "size", 7, + NULL); + sp_canvas_item_show(_norm); + sp_canvas_item_show(_grip); + break; + } +} + + +void Inkscape::SelTrans::handleNewEvent(SPKnot *knot, Geom::Point *position, guint state, SPSelTransHandle const &handle) +{ + if (!SP_KNOT_IS_GRABBED(knot)) { + return; + } + + // in case items have been unhooked from the document, don't + // try to continue processing events for them. + for (auto & _item : _items) { + if ( !_item->document ) { + return; + } + } + switch (handle.type) { + case HANDLE_SCALE: + scale(*position, state); + break; + case HANDLE_STRETCH: + stretch(handle, *position, state); + break; + case HANDLE_SKEW: + skew(handle, *position, state); + break; + case HANDLE_ROTATE: + rotate(*position, state); + break; + case HANDLE_CENTER: + setCenter(*position); + break; + case HANDLE_ALIGN: + case HANDLE_CENTER_ALIGN: + break; + } +} + + +gboolean Inkscape::SelTrans::handleRequest(SPKnot *knot, Geom::Point *position, guint state, SPSelTransHandle const &handle) +{ + if (!SP_KNOT_IS_GRABBED(knot)) + return TRUE; + + // When holding shift while rotating or skewing, the transformation will be + // relative to the point opposite of the handle; otherwise it will be relative + // to the center as set for the selection + if ((!(state & GDK_SHIFT_MASK) == !(_state == STATE_ROTATE)) && (handle.type != HANDLE_CENTER)) { + _origin = _opposite; + _origin_for_bboxpoints = _opposite_for_bboxpoints; + _origin_for_specpoints = _opposite_for_specpoints; + } else if (_center) { + _origin = *_center; + _origin_for_bboxpoints = *_center; + _origin_for_specpoints = *_center; + } else { + // FIXME + return TRUE; + } + if (request(handle, *position, state)) { + knot->setPosition(*position, state); + SP_CTRL(_grip)->moveto(*position); + if (handle.type == HANDLE_CENTER) { + SP_CTRL(_norm)->moveto(*position); + } else { + SP_CTRL(_norm)->moveto(_origin); + } + } + + return TRUE; +} + + +void Inkscape::SelTrans::_selChanged(Inkscape::Selection */*selection*/) +{ + if (!_grabbed) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // reread in case it changed on the fly: + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + _snap_bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + + _updateVolatileState(); + _current_relative_affine.setIdentity(); + _center_is_set = false; // center(s) may have changed + _updateHandles(); + } +} + +void Inkscape::SelTrans::_selModified(Inkscape::Selection */*selection*/, guint /*flags*/) +{ + if (!_grabbed) { + _updateVolatileState(); + _current_relative_affine.setIdentity(); + + // reset internal flag + _changed = false; + + _center_is_set = false; // center(s) may have changed + + _updateHandles(); + } +} + +void Inkscape::SelTrans::_boundingBoxPrefsChanged(int prefs_bbox) +{ + _snap_bbox_type = !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + + _updateVolatileState(); + _updateHandles(); +} + +/* + * handlers for handle move-request + */ + +/** Returns -1 or 1 according to the sign of x. Returns 1 for 0 and NaN. */ +static double sign(double const x) +{ + return ( x < 0 + ? -1 + : 1 ); +} + +gboolean Inkscape::SelTrans::scaleRequest(Geom::Point &pt, guint state) +{ + + // Calculate the scale factors, which can be either visual or geometric + // depending on which type of bbox is currently being used (see preferences -> selector tool) + Geom::Scale default_scale = calcScaleFactors(_point, pt, _origin); + + // Find the scale factors for the geometric bbox + Geom::Point pt_geom = _getGeomHandlePos(pt); + Geom::Scale geom_scale = calcScaleFactors(_point_geom, pt_geom, _origin_for_specpoints); + + _absolute_affine = Geom::identity(); //Initialize the scaler + + if (state & GDK_MOD1_MASK) { // scale by an integer multiplier/divider + // We're scaling either the visual or the geometric bbox here (see the comment above) + for ( unsigned int i = 0 ; i < 2 ; i++ ) { + if (fabs(default_scale[i]) > 1) { + default_scale[i] = round(default_scale[i]); + } else if (default_scale[i] != 0) { + default_scale[i] = 1/round(1/(MIN(default_scale[i], 10))); + } + } + // Update the knot position + pt = _calcAbsAffineDefault(default_scale); + // When scaling by an integer, snapping is not needed + } else { + // In all other cases we should try to snap now + Inkscape::PureScale *bb, *sn; + + if ((state & GDK_CONTROL_MASK) || _desktop->isToolboxButtonActive ("lock")) { + // Scale is locked to a 1:1 aspect ratio, so that s[X] must be made to equal s[Y]. + // + // The aspect-ratio must be locked before snapping + if (fabs(default_scale[Geom::X]) > fabs(default_scale[Geom::Y])) { + default_scale[Geom::X] = fabs(default_scale[Geom::Y]) * sign(default_scale[Geom::X]); + geom_scale[Geom::X] = fabs(geom_scale[Geom::Y]) * sign(geom_scale[Geom::X]); + } else { + default_scale[Geom::Y] = fabs(default_scale[Geom::X]) * sign(default_scale[Geom::Y]); + geom_scale[Geom::Y] = fabs(geom_scale[Geom::X]) * sign(geom_scale[Geom::Y]); + } + + // Snap along a suitable constraint vector from the origin. + + bb = new Inkscape::PureScaleConstrained(default_scale, _origin_for_bboxpoints); + sn = new Inkscape::PureScaleConstrained(geom_scale, _origin_for_specpoints); + } else { + /* Scale aspect ratio is unlocked */ + bb = new Inkscape::PureScale(default_scale, _origin_for_bboxpoints, false); + sn = new Inkscape::PureScale(geom_scale, _origin_for_specpoints, false); + } + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, false, _items_const); + m.snapTransformed(_bbox_points, _point, (*bb)); + m.snapTransformed(_snap_points, _point, (*sn)); + m.unSetup(); + + // These lines below are duplicated in stretchRequest + //TODO: Eliminate this code duplication + if (bb->best_snapped_point.getSnapped() || sn->best_snapped_point.getSnapped()) { + if (bb->best_snapped_point.getSnapped()) { + if (!bb->best_snapped_point.isOtherSnapBetter(sn->best_snapped_point, false)) { + // We snapped the bbox (which is either visual or geometric) + _desktop->snapindicator->set_new_snaptarget(bb->best_snapped_point); + default_scale = bb->getScaleSnapped(); + // Calculate the new transformation and update the handle position + pt = _calcAbsAffineDefault(default_scale); + } + } else if (sn->best_snapped_point.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(sn->best_snapped_point); + // We snapped the special points (e.g. nodes), which are not at the visual bbox + // The handle location however (pt) might however be at the visual bbox, so we + // will have to calculate pt taking the stroke width into account + geom_scale = sn->getScaleSnapped(); + pt = _calcAbsAffineGeom(geom_scale); + } + } else { + // We didn't snap at all! Don't update the handle position, just calculate the new transformation + _calcAbsAffineDefault(default_scale); + _desktop->snapindicator->remove_snaptarget(); + } + + delete bb; + delete sn; + } + + /* Status text */ + _message_context.setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Scale</b>: %0.2f%% x %0.2f%%; with <b>Ctrl</b> to lock ratio"), + 100 * _absolute_affine[0], 100 * _absolute_affine[3]); + + return TRUE; +} + +gboolean Inkscape::SelTrans::stretchRequest(SPSelTransHandle const &handle, Geom::Point &pt, guint state) +{ + Geom::Dim2 axis, perp; + switch (handle.cursor) { + case GDK_TOP_SIDE: + case GDK_BOTTOM_SIDE: + axis = Geom::Y; + perp = Geom::X; + break; + case GDK_LEFT_SIDE: + case GDK_RIGHT_SIDE: + axis = Geom::X; + perp = Geom::Y; + break; + default: + g_assert_not_reached(); + return TRUE; + }; + + // Calculate the scale factors, which can be either visual or geometric + // depending on which type of bbox is currently being used (see preferences -> selector tool) + Geom::Scale default_scale = calcScaleFactors(_point, pt, _origin); + default_scale[perp] = 1; + + // Find the scale factors for the geometric bbox + Geom::Point pt_geom = _getGeomHandlePos(pt); + Geom::Scale geom_scale = calcScaleFactors(_point_geom, pt_geom, _origin_for_specpoints); + geom_scale[perp] = 1; + + _absolute_affine = Geom::identity(); //Initialize the scaler + + if (state & GDK_MOD1_MASK) { // stretch by an integer multiplier/divider + if (fabs(default_scale[axis]) > 1) { + default_scale[axis] = round(default_scale[axis]); + } else if (default_scale[axis] != 0) { + default_scale[axis] = 1/round(1/(MIN(default_scale[axis], 10))); + } + // Calculate the new transformation and update the handle position + pt = _calcAbsAffineDefault(default_scale); + // When stretching by an integer, snapping is not needed + } else { + // In all other cases we should try to snap now + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, false, _items_const); + + bool symmetrical = state & GDK_CONTROL_MASK; + + Inkscape::PureStretchConstrained bb = Inkscape::PureStretchConstrained(Geom::Coord(default_scale[axis]), _origin_for_bboxpoints, Geom::Dim2(axis), symmetrical); + Inkscape::PureStretchConstrained sn = Inkscape::PureStretchConstrained(Geom::Coord(geom_scale[axis]), _origin_for_specpoints, Geom::Dim2(axis), symmetrical); + + m.snapTransformed(_bbox_points, _point, bb); + m.snapTransformed(_snap_points, _point, sn); + m.unSetup(); + + if (bb.best_snapped_point.getSnapped()) { + // We snapped the bbox (which is either visual or geometric) + default_scale[axis] = bb.getMagnitude(); + } + + if (sn.best_snapped_point.getSnapped()) { + geom_scale[axis] = sn.getMagnitude(); + } + + if (symmetrical) { + // on ctrl, apply symmetrical scaling instead of stretching + // Preserve aspect ratio, but never flip in the dimension not being edited (by using fabs()) + default_scale[perp] = fabs(default_scale[axis]); + geom_scale[perp] = fabs(geom_scale[axis]); + } + + // These lines below are duplicated in scaleRequest + if (bb.best_snapped_point.getSnapped() || sn.best_snapped_point.getSnapped()) { + if (bb.best_snapped_point.getSnapped()) { + if (!bb.best_snapped_point.isOtherSnapBetter(sn.best_snapped_point, false)) { + // We snapped the bbox (which is either visual or geometric) + _desktop->snapindicator->set_new_snaptarget(bb.best_snapped_point); + default_scale = bb.getStretchSnapped(); + // Calculate the new transformation and update the handle position + pt = _calcAbsAffineDefault(default_scale); + } + } else if (sn.best_snapped_point.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(sn.best_snapped_point); + // We snapped the special points (e.g. nodes), which are not at the visual bbox + // The handle location however (pt) might however be at the visual bbox, so we + // will have to calculate pt taking the stroke width into account + geom_scale = sn.getStretchSnapped(); + pt = _calcAbsAffineGeom(geom_scale); + } + } else { + // We didn't snap at all! Don't update the handle position, just calculate the new transformation + _calcAbsAffineDefault(default_scale); + _desktop->snapindicator->remove_snaptarget(); + } + } + + // status text + _message_context.setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Scale</b>: %0.2f%% x %0.2f%%; with <b>Ctrl</b> to lock ratio"), + 100 * _absolute_affine[0], 100 * _absolute_affine[3]); + + return TRUE; +} + +gboolean Inkscape::SelTrans::request(SPSelTransHandle const &handle, Geom::Point &pt, guint state) +{ + // These _should_ be in the handstype somewhere instead + switch (handle.type) { + case HANDLE_SCALE: + return scaleRequest(pt, state); + case HANDLE_STRETCH: + return stretchRequest(handle, pt, state); + case HANDLE_SKEW: + return skewRequest(handle, pt, state); + case HANDLE_ROTATE: + return rotateRequest(pt, state); + case HANDLE_CENTER: + return centerRequest(pt, state); + case HANDLE_ALIGN: + case HANDLE_CENTER_ALIGN: + break; // Do nothing, no dragging + } + return FALSE; +} + +gboolean Inkscape::SelTrans::skewRequest(SPSelTransHandle const &handle, Geom::Point &pt, guint state) +{ + /* When skewing (or rotating): + * 1) the stroke width will not change. This makes life much easier because we don't have to + * account for that (like for scaling or stretching). As a consequence, all points will + * have the same origin for the transformation and for the snapping. + * 2) When holding shift, the transformation will be relative to the point opposite of + * the handle; otherwise it will be relative to the center as set for the selection + */ + + Geom::Dim2 dim_a; + Geom::Dim2 dim_b; + + switch (handle.cursor) { + case GDK_SB_H_DOUBLE_ARROW: + dim_a = Geom::Y; + dim_b = Geom::X; + break; + case GDK_SB_V_DOUBLE_ARROW: + dim_a = Geom::X; + dim_b = Geom::Y; + break; + default: + g_assert_not_reached(); + abort(); + break; + } + + // _point and _origin are noisy, ranging from 1 to 1e-9 or even smaller; this is due to the + // limited SVG output precision, which can be arbitrarily set in the preferences + Geom::Point const initial_delta = _point - _origin; + + // The handle and the origin shouldn't be too close to each other; let's check for that! + // Due to the limited resolution though (see above), we'd better use a relative error here + if (_bbox) { + Geom::Coord d = (*_bbox).dimensions()[dim_a]; + if (fabs(initial_delta[dim_a]/d) < 1e-4) { + return false; + } + } + + // Calculate the scale factors, which can be either visual or geometric + // depending on which type of bbox is currently being used (see preferences -> selector tool) + Geom::Scale scale = calcScaleFactors(_point, pt, _origin, false); + Geom::Scale skew = calcScaleFactors(_point, pt, _origin, true); + scale[dim_b] = 1; + skew[dim_b] = 1; + + if (fabs(scale[dim_a]) < 1) { + // Prevent shrinking of the selected object, while allowing mirroring + scale[dim_a] = sign(scale[dim_a]); + } else { + // Allow expanding of the selected object by integer multiples + scale[dim_a] = floor(scale[dim_a] + 0.5); + } + + double radians = atan(skew[dim_a] / scale[dim_a]); + + if (state & GDK_CONTROL_MASK) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Snap to defined angle increments + int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + if (snaps) { + double sections = floor(radians * snaps / M_PI + .5); + if (fabs(sections) >= snaps / 2) { + sections = sign(sections) * (snaps / 2 - 1); + } + radians = (M_PI / snaps) * sections; + } + skew[dim_a] = tan(radians) * scale[dim_a]; + } else { + // Snap to objects, grids, guides + + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, false, _items_const); + + // When skewing, we cannot snap the corners of the bounding box, see the comment in PureSkewConstrained for details + Inkscape::PureSkewConstrained sn = Inkscape::PureSkewConstrained(skew[dim_a], scale[dim_a], _origin, Geom::Dim2(dim_b)); + m.snapTransformed(_snap_points, _point, sn); + + if (sn.best_snapped_point.getSnapped()) { + // We snapped something, so change the skew to reflect it + skew[dim_a] = sn.getSkewSnapped(); + _desktop->snapindicator->set_new_snaptarget(sn.best_snapped_point); + } else { + _desktop->snapindicator->remove_snaptarget(); + } + + m.unSetup(); + } + + // Update the handle position + pt[dim_b] = initial_delta[dim_a] * skew[dim_a] + _point[dim_b]; + pt[dim_a] = initial_delta[dim_a] * scale[dim_a] + _origin[dim_a]; + + // Calculate the relative affine + _relative_affine = Geom::identity(); + _relative_affine[2*dim_a + dim_a] = (pt[dim_a] - _origin[dim_a]) / initial_delta[dim_a]; + _relative_affine[2*dim_a + (dim_b)] = (pt[dim_b] - _point[dim_b]) / initial_delta[dim_a]; + _relative_affine[2*(dim_b) + (dim_a)] = 0; + _relative_affine[2*(dim_b) + (dim_b)] = 1; + + for (int i = 0; i < 2; i++) { + if (fabs(_relative_affine[3*i]) < 1e-15) { + _relative_affine[3*i] = 1e-15; + } + } + + // Update the status text + double degrees = mod360symm(Geom::deg_from_rad(radians)); + _message_context.setF(Inkscape::IMMEDIATE_MESSAGE, + // TRANSLATORS: don't modify the first ";" + // (it will NOT be displayed as ";" - only the second one will be) + _("<b>Skew</b>: %0.2f°; with <b>Ctrl</b> to snap angle"), + degrees); + + return TRUE; +} + +gboolean Inkscape::SelTrans::rotateRequest(Geom::Point &pt, guint state) +{ + /* When rotating (or skewing): + * 1) the stroke width will not change. This makes life much easier because we don't have to + * account for that (like for scaling or stretching). As a consequence, all points will + * have the same origin for the transformation and for the snapping. + * 2) When holding shift, the transformation will be relative to the point opposite of + * the handle; otherwise it will be relative to the center as set for the selection + */ + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + // rotate affine in rotate + Geom::Point const d1 = _point - _origin; + Geom::Point const d2 = pt - _origin; + + Geom::Coord const h1 = Geom::L2(d1); // initial radius + if (h1 < 1e-15) return FALSE; + Geom::Point q1 = d1 / h1; // normalized initial vector to handle + Geom::Coord const h2 = Geom::L2(d2); // new radius + if (fabs(h2) < 1e-15) return FALSE; + Geom::Point q2 = d2 / h2; // normalized new vector to handle + + Geom::Rotate r1(q1); + Geom::Rotate r2(q2); + + double radians = atan2(Geom::dot(Geom::rot90(d1), d2), Geom::dot(d1, d2));; + if (state & GDK_CONTROL_MASK) { + // Snap to defined angle increments + double cos_t = Geom::dot(q1, q2); + double sin_t = Geom::dot(Geom::rot90(q1), q2); + radians = atan2(sin_t, cos_t); + if (snaps) { + radians = ( M_PI / snaps ) * floor( radians * snaps / M_PI + .5 ); + } + r1 = Geom::Rotate(0); //q1 = Geom::Point(1, 0); + r2 = Geom::Rotate(radians); //q2 = Geom::Point(cos(radians), sin(radians)); + } else { + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop, false, _items_const); + // When rotating, we cannot snap the corners of the bounding box, see the comment in "constrainedSnapRotate" for details + Inkscape::PureRotateConstrained sn = Inkscape::PureRotateConstrained(radians, _origin); + m.snapTransformed(_snap_points, _point, sn); + m.unSetup(); + + if (sn.best_snapped_point.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(sn.best_snapped_point); + // We snapped something, so change the rotation to reflect it + radians = sn.getAngleSnapped(); + r1 = Geom::Rotate(0); + r2 = Geom::Rotate(radians); + } else { + _desktop->snapindicator->remove_snaptarget(); + } + + } + + + // Calculate the relative affine + _relative_affine = r2 * r1.inverse(); + + // Update the handle position + pt = _point * Geom::Translate(-_origin) * _relative_affine * Geom::Translate(_origin); + + // Update the status text + double degrees = mod360symm(Geom::deg_from_rad(radians)); + _message_context.setF(Inkscape::IMMEDIATE_MESSAGE, + // TRANSLATORS: don't modify the first ";" + // (it will NOT be displayed as ";" - only the second one will be) + _("<b>Rotate</b>: %0.2f°; with <b>Ctrl</b> to snap angle"), degrees); + + return TRUE; +} + +// Move the item's transformation center +gboolean Inkscape::SelTrans::centerRequest(Geom::Point &pt, guint state) +{ + // When dragging the transformation center while multiple items have been selected, then those + // items will share a single center. While dragging that single center, it should never snap to the + // centers of any of the selected objects. Therefore we will have to pass the list of selected items + // to the snapper, to avoid self-snapping of the rotation center + std::vector<SPItem *> items(_selection->items().begin(), _selection->items().end()); + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.setRotationCenterSource(items); + + if (state & GDK_CONTROL_MASK) { // with Ctrl, constrain to axes + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + constraints.emplace_back(_point, Geom::Point(1, 0)); + constraints.emplace_back(_point, Geom::Point(0, 1)); + Inkscape::SnappedPoint sp = m.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(pt, Inkscape::SNAPSOURCE_ROTATION_CENTER), constraints, state & GDK_SHIFT_MASK); + pt = sp.getPoint(); + } + else { + if (!(state & GDK_SHIFT_MASK)) { // Shift disables snapping + m.freeSnapReturnByRef(pt, Inkscape::SNAPSOURCE_ROTATION_CENTER); + } + } + + m.unSetup(); + + // status text + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(pt[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(pt[Geom::Y], "px"); + Glib::ustring xs(x_q.string(_desktop->namedview->display_units)); + Glib::ustring ys(y_q.string(_desktop->namedview->display_units)); + _message_context.setF(Inkscape::NORMAL_MESSAGE, _("Move <b>center</b> to %s, %s"), + xs.c_str(), ys.c_str()); + return TRUE; +} + +void Inkscape::SelTrans::align(guint state, SPSelTransHandle const &handle) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); + int align_to = prefs->getInt("/dialogs/align/align-to", 6); + + int verb_id = -1; + if (state & GDK_SHIFT_MASK) { + verb_id = AlignVerb[handle.control + AlignHandleToVerb + AlignShiftVerb]; + } else { + verb_id = AlignVerb[handle.control + AlignHandleToVerb]; + } + if(verb_id >= 0) { + prefs->setBool("/dialogs/align/sel-as-groups", (state & GDK_CONTROL_MASK) != 0); + prefs->setInt("/dialogs/align/align-to", 6); + Inkscape::Verb *verb = Inkscape::Verb::get( verb_id ); + g_assert( verb != NULL ); + SPAction *action = verb->get_action((Inkscape::UI::View::View *) this->_desktop); + sp_action_perform (action, NULL); + } + + // Set the special align point and settings back to nothing so we don't interfere + prefs->setBool("/dialogs/align/sel-as-groups", sel_as_group); + prefs->setInt("/dialogs/align/align-to", align_to); +} + +/* + * handlers for handle movement + * + */ + + + +void Inkscape::SelTrans::stretch(SPSelTransHandle const &/*handle*/, Geom::Point &/*pt*/, guint /*state*/) +{ + transform(_absolute_affine, Geom::Point(0, 0)); // we have already accounted for origin, so pass 0,0 +} + +void Inkscape::SelTrans::scale(Geom::Point &/*pt*/, guint /*state*/) +{ + transform(_absolute_affine, Geom::Point(0, 0)); // we have already accounted for origin, so pass 0,0 +} + +void Inkscape::SelTrans::skew(SPSelTransHandle const &/*handle*/, Geom::Point &/*pt*/, guint /*state*/) +{ + transform(_relative_affine, _origin); +} + +void Inkscape::SelTrans::rotate(Geom::Point &/*pt*/, guint /*state*/) +{ + transform(_relative_affine, _origin); +} + +void Inkscape::SelTrans::moveTo(Geom::Point const &xy, guint state) +{ + SnapManager &m = _desktop->namedview->snap_manager; + + /* The amount that we've moved by during this drag */ + Geom::Point dxy = xy - _point; + + bool const alt = (state & GDK_MOD1_MASK); + bool const control = (state & GDK_CONTROL_MASK); + bool const shift = (state & GDK_SHIFT_MASK); + + if (control) { // constrained to the orthogonal axes + if (fabs(dxy[Geom::X]) > fabs(dxy[Geom::Y])) { + dxy[Geom::Y] = 0; + } else { + dxy[Geom::X] = 0; + } + } + + if (alt) {// Alt pressed means: move only by integer multiples of the grid spacing + m.setup(_desktop, true, _items_const); + dxy = m.multipleOfGridPitch(dxy, _point); + m.unSetup(); + } else if (!shift) { //!shift: with snapping + /* We're snapping to things, possibly with a constraint to horizontal or + ** vertical movement. Obtain a list of possible translations and then + ** pick the smallest. + */ + + m.setup(_desktop, false, _items_const); + + /* This will be our list of possible translations */ + std::list<Inkscape::SnappedPoint> s; + + Inkscape::PureTranslate *bb, *sn; + + if (control) { // constrained movement with snapping + + /* Snap to things, and also constrain to horizontal or vertical movement */ + + Geom::Dim2 dim = fabs(dxy[Geom::X]) > fabs(dxy[Geom::Y]) ? Geom::X : Geom::Y; + // When doing a constrained translation, all points will move in the same direction, i.e. + // either horizontally or vertically. Therefore we only have to specify the direction of + // the constraint-line once. The constraint lines are parallel, but might not be colinear. + // Therefore we will have to set the point through which the constraint-line runs + // individually for each point to be snapped; this will be handled however by snapTransformed() + bb = new Inkscape::PureTranslateConstrained(dxy[dim], dim); + sn = new Inkscape::PureTranslateConstrained(dxy[dim], dim); + } else { // !control + /* Snap to things with no constraint */ + bb = new Inkscape::PureTranslate(dxy); + sn = new Inkscape::PureTranslate(dxy); + } + // Let's leave this timer code here for a while. I'll probably need it in the near future (Diederik van Lierop) + /* GTimeVal starttime; + GTimeVal endtime; + g_get_current_time(&starttime); */ + + m.snapTransformed(_bbox_points, _point, (*bb)); + m.snapTransformed(_snap_points, _point, (*sn)); + m.unSetup(); + + /*g_get_current_time(&endtime); + double elapsed = ((((double)endtime.tv_sec - starttime.tv_sec) * G_USEC_PER_SEC + (endtime.tv_usec - starttime.tv_usec))) / 1000.0; + std::cout << "Time spent snapping: " << elapsed << std::endl; */ + + /* Pick one */ + Inkscape::SnappedPoint best_snapped_point; + + bool sn_is_best = sn->best_snapped_point.getSnapped(); + bool bb_is_best = bb->best_snapped_point.getSnapped(); + + if (bb_is_best && sn_is_best) { + sn_is_best = bb->best_snapped_point.isOtherSnapBetter(sn->best_snapped_point, true); + bb_is_best = !sn_is_best; + } + + if (sn_is_best) { + best_snapped_point = sn->best_snapped_point; + dxy = sn->getTranslationSnapped(); + } else if (bb_is_best) { + best_snapped_point = bb->best_snapped_point; + dxy = bb->getTranslationSnapped(); + } + + if (best_snapped_point.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(best_snapped_point); + } else { + // We didn't snap, so remove any previous snap indicator + _desktop->snapindicator->remove_snaptarget(); + if (control) { + // If we didn't snap, then we should still constrain horizontally or vertically + // (When we did snap, then this constraint has already been enforced by + // calling constrainedSnapTranslate() above) + if (fabs(dxy[Geom::X]) > fabs(dxy[Geom::Y])) { + dxy[Geom::Y] = 0; + } else { + dxy[Geom::X] = 0; + } + } + } + delete bb; + delete sn; + } + + Geom::Affine const move((Geom::Translate(dxy))); + Geom::Point const norm(0, 0); + transform(move, norm); + + // status text + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dxy[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dxy[Geom::Y], "px"); + Glib::ustring xs(x_q.string(_desktop->namedview->display_units)); + Glib::ustring ys(y_q.string(_desktop->namedview->display_units)); + _message_context.setF(Inkscape::NORMAL_MESSAGE, + _("<b>Move</b> by %s, %s; with <b>Ctrl</b> to restrict to horizontal/vertical; with <b>Shift</b> to disable snapping"), + xs.c_str(), ys.c_str()); +} + +// Given a location of a handle at the visual bounding box, find the corresponding location at the +// geometrical bounding box +Geom::Point Inkscape::SelTrans::_getGeomHandlePos(Geom::Point const &visual_handle_pos) +{ + if ( _snap_bbox_type == SPItem::GEOMETRIC_BBOX) { + // When the selector tool is using geometric bboxes, then the handle is already + // located at one of the geometric bbox corners + return visual_handle_pos; + } + + if (!_geometric_bbox) { + //_getGeomHandlePos() can only be used after _geometric_bbox has been defined! + return visual_handle_pos; + } + + // Using the Geom::Rect constructor below ensures that "min() < max()", which is important + // because this will also hold for _bbox, and which is required for get_scale_transform_for_stroke() + Geom::Rect new_bbox = Geom::Rect(_origin_for_bboxpoints, visual_handle_pos); // new visual bounding box + // Please note that the new_bbox might in fact be just a single line, for example when stretching (in + // which case the handle and origin will be aligned vertically or horizontally) + Geom::Point normalized_handle_pos = (visual_handle_pos - new_bbox.min()) * Geom::Scale(new_bbox.dimensions()).inverse(); + + // Calculate the absolute affine while taking into account the scaling of the stroke width + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool preserve = prefs->getBool("/options/preservetransform/value", false); + Geom::Affine abs_affine = get_scale_transform_for_uniform_stroke (*_bbox, _strokewidth, _strokewidth, transform_stroke, preserve, + new_bbox.min()[Geom::X], new_bbox.min()[Geom::Y], new_bbox.max()[Geom::X], new_bbox.max()[Geom::Y]); + + // Calculate the scaled geometrical bbox + Geom::Rect new_geom_bbox = Geom::Rect(_geometric_bbox->min() * abs_affine, _geometric_bbox->max() * abs_affine); + // Find the location of the handle on this new geometrical bbox + return normalized_handle_pos * Geom::Scale(new_geom_bbox.dimensions()) + new_geom_bbox.min(); //new position of the geometric handle +} + +Geom::Scale Inkscape::calcScaleFactors(Geom::Point const &initial_point, Geom::Point const &new_point, Geom::Point const &origin, bool const skew) +{ + // Work out the new scale factors for the bbox + + Geom::Point const initial_delta = initial_point - origin; + Geom::Point const new_delta = new_point - origin; + Geom::Point const offset = new_point - initial_point; + Geom::Scale scale(1, 1); + + for ( unsigned int i = 0 ; i < 2 ; i++ ) { + if ( fabs(initial_delta[i]) > 1e-6 ) { + if (skew) { + scale[i] = offset[1-i] / initial_delta[i]; + } else { + scale[i] = new_delta[i] / initial_delta[i]; + } + } + } + + return scale; +} + +// Only for scaling/stretching +Geom::Point Inkscape::SelTrans::_calcAbsAffineDefault(Geom::Scale const default_scale) +{ + Geom::Affine abs_affine = Geom::Translate(-_origin) * Geom::Affine(default_scale) * Geom::Translate(_origin); + Geom::Point new_bbox_min = _visual_bbox->min() * abs_affine; + Geom::Point new_bbox_max = _visual_bbox->max() * abs_affine; + + bool transform_stroke = false; + bool preserve = false; + gdouble stroke_x = 0; + gdouble stroke_y = 0; + + if ( _snap_bbox_type != SPItem::GEOMETRIC_BBOX) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + transform_stroke = prefs->getBool("/options/transform/stroke", true); + preserve = prefs->getBool("/options/preservetransform/value", false); + stroke_x = _visual_bbox->width() - _geometric_bbox->width(); + stroke_y = _visual_bbox->height() - _geometric_bbox->height(); + } + + _absolute_affine = get_scale_transform_for_uniform_stroke (*_visual_bbox, stroke_x, stroke_y, transform_stroke, preserve, + new_bbox_min[Geom::X], new_bbox_min[Geom::Y], new_bbox_max[Geom::X], new_bbox_max[Geom::Y]); + + // return the new handle position + return ( _point - _origin ) * default_scale + _origin; +} + +// Only for scaling/stretching +Geom::Point Inkscape::SelTrans::_calcAbsAffineGeom(Geom::Scale const geom_scale) +{ + _relative_affine = Geom::Affine(geom_scale); + _absolute_affine = Geom::Translate(-_origin_for_specpoints) * _relative_affine * Geom::Translate(_origin_for_specpoints); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool const transform_stroke = prefs->getBool("/options/transform/stroke", true); + if (_geometric_bbox) { + Geom::Rect visual_bbox = get_visual_bbox(_geometric_bbox, _absolute_affine, _strokewidth, transform_stroke); + // return the new handle position + return visual_bbox.min() + visual_bbox.dimensions() * Geom::Scale(_handle_x, _handle_y); + } + + // Fall back scenario, in case we don't have a geometric bounding box at hand; + // (Due to some bugs related to bounding boxes having at least one zero dimension; For more details + // see https://bugs.launchpad.net/inkscape/+bug/318726) + g_warning("No geometric bounding box has been calculated; this is a bug that needs fixing!"); + return _calcAbsAffineDefault(geom_scale); // this is bogus, but we must return _something_ +} + +void Inkscape::SelTrans::_keepClosestPointOnly(Geom::Point const &p) +{ + SnapManager const &m = _desktop->namedview->snap_manager; + + // If we're not going to snap nodes, then we might just as well get rid of their snappoints right away + if (!(m.snapprefs.isTargetSnappable(SNAPTARGET_NODE_CATEGORY, SNAPTARGET_OTHERS_CATEGORY) || m.snapprefs.isAnyDatumSnappable())) { + _snap_points.clear(); + } + + // If we're not going to snap bounding boxes, then we might just as well get rid of their snappoints right away + if (!m.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY)) { + _bbox_points.clear(); + } + + _all_snap_sources_sorted = _snap_points; + _all_snap_sources_sorted.insert(_all_snap_sources_sorted.end(), _bbox_points.begin(), _bbox_points.end()); + + // Calculate and store the distance to the reference point for each snap candidate point + for(auto & i : _all_snap_sources_sorted) { + i.setDistance(Geom::L2(i.getPoint() - p)); + } + + // Sort them ascending, using the distance calculated above as the single criteria + std::sort(_all_snap_sources_sorted.begin(), _all_snap_sources_sorted.end()); + + // Now get the closest snap source + _snap_points.clear(); + _bbox_points.clear(); + if (!_all_snap_sources_sorted.empty()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + if (_all_snap_sources_sorted.front().getSourceType() & SNAPSOURCE_BBOX_CATEGORY) { + _bbox_points.push_back(_all_snap_sources_sorted.front()); + } else { + _snap_points.push_back(_all_snap_sources_sorted.front()); + } + } + +} +// TODO: This code is duplicated in transform-handle-set.cpp; fix this! +void Inkscape::SelTrans::getNextClosestPoint(bool reverse) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + if (!_all_snap_sources_sorted.empty()) { + if (reverse) { // Shift-tab will find a closer point + if (_all_snap_sources_iter == _all_snap_sources_sorted.begin()) { + _all_snap_sources_iter = _all_snap_sources_sorted.end(); + } + --_all_snap_sources_iter; + } else { // Tab will find a point further away + ++_all_snap_sources_iter; + if (_all_snap_sources_iter == _all_snap_sources_sorted.end()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + } + } + + _snap_points.clear(); + _bbox_points.clear(); + + if ((*_all_snap_sources_iter).getSourceType() & SNAPSOURCE_BBOX_CATEGORY) { + _bbox_points.push_back(*_all_snap_sources_iter); + } else { + _snap_points.push_back(*_all_snap_sources_iter); + } + + // Show the updated snap source now; otherwise it won't be shown until the selection is being moved again + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.displaySnapsource(*_all_snap_sources_iter); + m.unSetup(); + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/seltrans.h b/src/seltrans.h new file mode 100644 index 0000000..dadef62 --- /dev/null +++ b/src/seltrans.h @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SELTRANS_H +#define SEEN_SELTRANS_H + +/* + * Helper object for transforming selected items + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/affine.h> +#include <2geom/rect.h> +#include <cstddef> +#include <sigc++/sigc++.h> +#include <vector> + +#include "knot.h" +#include "message-context.h" +#include "seltrans-handles.h" +#include "selcue.h" + +#include "object/sp-item.h" +#include "display/guideline.h" + +class SPKnot; +class SPDesktop; +struct SPCanvasItem; +struct SPCtrlLine; +struct SPSelTransHandle; + +namespace Inkscape { + +Geom::Scale calcScaleFactors(Geom::Point const &initial_point, Geom::Point const &new_point, Geom::Point const &origin, bool const skew = false); + +namespace XML { + class Node; +} + +class SelTrans +{ +public: + SelTrans(SPDesktop *desktop); + ~SelTrans(); + + void increaseState(); + void resetState(); + void setCenter(Geom::Point const &p); + void grab(Geom::Point const &p, double x, double y, bool show_handles, bool translating); + void transform(Geom::Affine const &rel_affine, Geom::Point const &norm); + void ungrab(); + void stamp(); + void moveTo(Geom::Point const &xy, unsigned int state); + void stretch(SPSelTransHandle const &handle, Geom::Point &pt, unsigned int state); + void scale(Geom::Point &pt, unsigned int state); + void skew(SPSelTransHandle const &handle, Geom::Point &pt, unsigned int state); + void rotate(Geom::Point &pt, unsigned int state); + void align(guint state, SPSelTransHandle const &handle); + int request(SPSelTransHandle const &handle, Geom::Point &pt, unsigned int state); + int scaleRequest(Geom::Point &pt, unsigned int state); + int stretchRequest(SPSelTransHandle const &handle, Geom::Point &pt, unsigned int state); + int skewRequest(SPSelTransHandle const &handle, Geom::Point &pt, unsigned int state); + int rotateRequest(Geom::Point &pt, unsigned int state); + int centerRequest(Geom::Point &pt, unsigned int state); + + int handleRequest(SPKnot *knot, Geom::Point *position, unsigned int state, SPSelTransHandle const &handle); + void handleGrab(SPKnot *knot, unsigned int state, SPSelTransHandle const &handle); + void handleClick(SPKnot *knot, unsigned int state, SPSelTransHandle const &handle); + void handleNewEvent(SPKnot *knot, Geom::Point *position, unsigned int state, SPSelTransHandle const &handle); + + enum Show + { + SHOW_CONTENT, + SHOW_OUTLINE + }; + + void setShow(Show s) { + _show = s; + } + bool isEmpty() { + return _empty; + } + bool isGrabbed() { + return _grabbed; + } + bool centerIsVisible() { + return ( SP_KNOT_IS_VISIBLE (knots[0]) ); + } + + void getNextClosestPoint(bool reverse); + +private: + class BoundingBoxPrefsObserver: public Preferences::Observer + { + public: + BoundingBoxPrefsObserver(SelTrans &sel_trans); + + void notify(Preferences::Entry const &val) override; + + private: + SelTrans &_sel_trans; + }; + + friend class Inkscape::SelTrans::BoundingBoxPrefsObserver; + + void _updateHandles(); + void _updateVolatileState(); + void _selChanged(Inkscape::Selection *selection); + void _selModified(Inkscape::Selection *selection, unsigned int flags); + void _boundingBoxPrefsChanged(int prefs_bbox); + void _makeHandles(); + void _showHandles(SPSelTransType type); + Geom::Point _getGeomHandlePos(Geom::Point const &visual_handle_pos); + Geom::Point _calcAbsAffineDefault(Geom::Scale const default_scale); + Geom::Point _calcAbsAffineGeom(Geom::Scale const geom_scale); + void _keepClosestPointOnly(Geom::Point const &p); + + enum State { + STATE_SCALE, //scale or stretch + STATE_ROTATE, //rotate or skew + STATE_ALIGN //on canvas align + }; + + SPDesktop *_desktop; + + std::vector<SPItem *> _items; + std::vector<SPItem const *> _items_const; + std::vector<Geom::Affine> _items_affines; + std::vector<Geom::Point> _items_centers; + + std::vector<Inkscape::SnapCandidatePoint> _snap_points; + std::vector<Inkscape::SnapCandidatePoint> _bbox_points; + std::vector<Inkscape::SnapCandidatePoint> _all_snap_sources_sorted; + std::vector<Inkscape::SnapCandidatePoint>::iterator _all_snap_sources_iter; + Inkscape::SelCue _selcue; + + Inkscape::Selection *_selection; + State _state; + Show _show; + + bool _grabbed; + bool _show_handles; + bool _empty; + bool _changed; + + SPItem::BBoxType _snap_bbox_type; + + Geom::OptRect _bbox; + Geom::OptRect _visual_bbox; + Geom::OptRect _geometric_bbox; + double _strokewidth; + + Geom::Affine _current_relative_affine; + Geom::Affine _absolute_affine; + Geom::Affine _relative_affine; + /* According to Merriam - Webster's online dictionary + * Affine: a transformation (as a translation, a rotation, or a uniform stretching) that carries straight + * lines into straight lines and parallel lines into parallel lines but may alter distance between points + * and angles between lines <affine geometry> + */ + + Geom::Point _opposite; ///< opposite point to where a scale is taking place + Geom::Point _opposite_for_specpoints; + Geom::Point _opposite_for_bboxpoints; + Geom::Point _origin_for_specpoints; + Geom::Point _origin_for_bboxpoints; + + double _handle_x; + double _handle_y; + + boost::optional<Geom::Point> _center; + bool _center_is_set; ///< we've already set _center, no need to reread it from items + int _center_handle; + + SPKnot *knots[NUMHANDS]; + SPCanvasItem *_norm; + SPCanvasItem *_grip; + SPCtrlLine *_l[4]; + unsigned int _sel_changed_id; + unsigned int _sel_modified_id; + std::vector<SPItem*> _stamp_cache; + + Geom::Point _origin; ///< position of origin for transforms + Geom::Point _point; ///< original position of the knot being used for the current transform + Geom::Point _point_geom; ///< original position of the knot being used for the current transform + Inkscape::MessageContext _message_context; + sigc::connection _sel_changed_connection; + sigc::connection _sel_modified_connection; + BoundingBoxPrefsObserver _bounding_box_prefs_observer; +}; + +} + +#endif // SEEN_SELTRANS_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/shortcuts.cpp b/src/shortcuts.cpp new file mode 100644 index 0000000..ae38e89 --- /dev/null +++ b/src/shortcuts.cpp @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * 2005 Monash University + * 2005 MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** \file + * Keyboard shortcut processing. + */ +/* + * Authors: + * + * + * You may redistribute and/or modify this file under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + */ + +#include <vector> +#include <cstring> +#include <string> +#include <map> + +#include "shortcuts.h" +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> +#include <gtk/gtk.h> + +#include <glibmm/i18n.h> +#include <glibmm/convert.h> +#include <glibmm/miscutils.h> + +#include "helper/action.h" +#include "io/dir-util.h" +#include "io/sys.h" +#include "io/resource.h" +#include "verbs.h" +#include "xml/node-iterators.h" +#include "xml/repr.h" +#include "document.h" +#include "preferences.h" +#include "ui/tools/tool-base.h" +#include "inkscape.h" +#include "desktop.h" +#include "path-prefix.h" +#include "ui/dialog/filedialog.h" + +using namespace Inkscape; +using namespace Inkscape::IO::Resource; + +static bool try_shortcuts_file(char const *filename, bool const is_user_set=false); +static void read_shortcuts_file(char const *filename, bool const is_user_set=false); + +unsigned int sp_shortcut_get_key(unsigned int const shortcut); +GdkModifierType sp_shortcut_get_modifiers(unsigned int const shortcut); + +/* Returns true if action was performed */ + +bool +sp_shortcut_invoke(unsigned int shortcut, Inkscape::UI::View::View *view) +{ + Inkscape::Verb *verb = sp_shortcut_get_verb(shortcut); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if (action) { + sp_action_perform(action, nullptr); + return true; + } + } + return false; +} + +static std::map<unsigned int, Inkscape::Verb * > *verbs = nullptr; +static std::map<Inkscape::Verb *, unsigned int> *primary_shortcuts = nullptr; +static std::map<Inkscape::Verb *, unsigned int> *user_shortcuts = nullptr; + +void sp_shortcut_init() +{ + verbs = new std::map<unsigned int, Inkscape::Verb * >(); + primary_shortcuts = new std::map<Inkscape::Verb *, unsigned int>(); + user_shortcuts = new std::map<Inkscape::Verb *, unsigned int>(); + + // try to load shortcut file as set in preferences + // if preference is unset or loading fails fallback to share/keys/default.xml and finally share/keys/inkscape.xml + // make paths relative to share/keys/ if possible (handle parallel installations of Inkscape gracefully) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::string shortcutfile = prefs->getString("/options/kbshortcuts/shortcutfile"); + bool success = false; + gchar const *reason; + if (shortcutfile.empty()) { + reason = "No key file set in preferences"; + } else { + reason = "Unable to read key file set in preferences"; + + bool absolute = Glib::path_is_absolute(shortcutfile); + if (absolute) { + success = try_shortcuts_file(shortcutfile.c_str()); + } else { + success = try_shortcuts_file(get_path(SYSTEM, KEYS, shortcutfile.c_str())); + } + + // store shortcutfile location relative to INKSCAPE_DATADIR + if (absolute && success) { + shortcutfile = sp_relative_path_from_path(shortcutfile, std::string(get_path(SYSTEM, KEYS))); + prefs->setString("/options/kbshortcuts/shortcutfile", shortcutfile.c_str()); + } + } +#ifdef WITH_CARBON_INTEGRATION + if (!success) { + g_info("%s. Falling back to 'carbon.xml' for MacOSX keyboards.", reason); + success = try_shortcuts_file(get_path(SYSTEM, KEYS, "carbon.xml")); + } +#endif + if (!success) { + g_info("%s. Falling back to 'default.xml'.", reason); + success = try_shortcuts_file(get_path(SYSTEM, KEYS, "default.xml")); + } + if (!success) { + g_info("Could not load 'default.xml' either. Falling back to 'inkscape.xml'."); + success = try_shortcuts_file(get_path(SYSTEM, KEYS, "inkscape.xml")); + } + if (!success) { + g_warning("Could not load any keyboard shortcut file (including fallbacks to 'default.xml' and 'inkscape.xml')."); + } + + // load shortcuts adjusted by user + try_shortcuts_file(get_path(USER, KEYS, "default.xml"), true); +} + +static bool try_shortcuts_file(char const *filename, bool const is_user_set) { + using Inkscape::IO::file_test; + + /* ah, if only we had an exception to catch... (permission, forgiveness) */ + if (file_test(filename, G_FILE_TEST_EXISTS)) { + read_shortcuts_file(filename, is_user_set); + return true; + } + + g_info("Unable to read keyboard shortcuts from %s (does not exist)", filename); + return false; +} + +/* + * Return the keyval corresponding to the key event in group 0 and the effective modifiers. + * + * Usage of group 0 (i.e. the main, typically English layout) instead of simply event->keyval + * ensures that shortcuts work regardless of the active keyboard layouts (e.g. Cyrillic). + * + * The effective modifiers are the modifiers that were not "consumed" by the translation and + * can be used by the application to define a shortcut, e.g. + * - when pressing "Shift+9" the resulting character is "(" + * the shift key was "consumed" to make this character and should not be part of the shortcut + * - when pressing "Ctrl+9" the resulting character is also "9" + * the ctrl key was *not* consumed to make this character and must be included in the shortcut + * - Exception: letter keys like [A-Z] always need the shift modifier, + * otherwise lower case and uper case keys are treated as equivalent + * The modifier values are already transformed from the default GDK_*_MASK into the equivalent high-bit masks + * defined by SP_SHORTCUT_*_MASK to allow for subsequent packing of the whole shortcut into a single int + * + * Note: Don't call this function directly but use the wrappers + * - sp_shortcut_get_from_event() - create a new shortcut from a key event + * - sp_shortcut_get_for_event() - get an existing shortcut for a key event + * (they correctly handle the packing of modifier keys into the keyval) + */ +guint sp_shortcut_translate_event(GdkEventKey const *event, guint *effective_modifiers) { + guint keyval = 0; + + guint initial_modifiers = event->state; + guint consumed_modifiers = 0; + guint remaining_modifiers = 0; + guint resulting_modifiers = 0; // remaining modifiers encoded in high-bit mask + + keyval = Inkscape::UI::Tools::get_latin_keyval(event, &consumed_modifiers); + + // Observe case convertible key values always as lower case and don't consume the "Shift" + // modifier for them + bool is_case_convertible = !(gdk_keyval_is_upper(keyval) && gdk_keyval_is_lower(keyval)); + if (is_case_convertible) { + keyval = gdk_keyval_to_lower(keyval); + consumed_modifiers &= ~GDK_SHIFT_MASK; + } + + remaining_modifiers = initial_modifiers & ~consumed_modifiers; + resulting_modifiers = ( remaining_modifiers & GDK_SHIFT_MASK ? SP_SHORTCUT_SHIFT_MASK : 0 ) | + ( remaining_modifiers & GDK_CONTROL_MASK ? SP_SHORTCUT_CONTROL_MASK : 0 ) | + ( remaining_modifiers & GDK_SUPER_MASK ? SP_SHORTCUT_SUPER_MASK : 0 ) | + ( remaining_modifiers & GDK_HYPER_MASK ? SP_SHORTCUT_HYPER_MASK : 0 ) | + ( remaining_modifiers & GDK_META_MASK ? SP_SHORTCUT_META_MASK : 0 ) | + ( remaining_modifiers & GDK_MOD1_MASK ? SP_SHORTCUT_ALT_MASK : 0 ); + + *effective_modifiers = resulting_modifiers; + return keyval; +} + +/* + * Returns a new Inkscape shortcut parsed from a key event. + */ +unsigned int sp_shortcut_get_from_event(GdkEventKey const *event) { + guint effective_modifiers; + + sp_shortcut_translate_event(event, &effective_modifiers); + + // return the actual keyval and the corresponding modifiers for creating the shortcut + // we must not return the translated keyval, otherwise we end up with illegal shortcuts like "Shift+9" instead of "(" + return (event->keyval) | effective_modifiers; +} + +/* + * Returns a new Inkscape shortcut parsed from a key event. + * (equivalent to sp_shortcut_get_from_event() but accepts the arguments of Gtk::CellRendererAccel::signal_accel_edited) + */ +unsigned int sp_shortcut_get_from_gdk_event(guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode) { + GdkEventKey event; + event.keyval = accel_key; + event.state = accel_mods; + event.hardware_keycode = hardware_keycode; + + return sp_shortcut_get_from_event(&event); +} + +/* + * Returns the Inkscape-internal integral shortcut representation for a key event. + * Use this to compare the received key event to known shortcuts. + */ +unsigned int sp_shortcut_get_for_event(GdkEventKey const *event) { + guint keyval; + guint effective_modifiers; + + keyval = sp_shortcut_translate_event(event, &effective_modifiers); + + // return the keyval translated to group 0 (English keyboard layout) and corresponding modifiers + return keyval | effective_modifiers; +} + +Glib::ustring sp_shortcut_to_label(unsigned int const shortcut) { + + Glib::ustring modifiers = ""; + + if (shortcut & SP_SHORTCUT_CONTROL_MASK) + modifiers += "Ctrl,"; + if (shortcut & SP_SHORTCUT_SHIFT_MASK) + modifiers += "Shift,"; + if (shortcut & SP_SHORTCUT_ALT_MASK) + modifiers += "Alt,"; + if (shortcut & SP_SHORTCUT_SUPER_MASK) + modifiers += "Super,"; + if (shortcut & SP_SHORTCUT_HYPER_MASK) + modifiers += "Hyper,"; + if (shortcut & SP_SHORTCUT_META_MASK) + modifiers += "Meta,"; + + if(modifiers.length() > 0 && + modifiers.find(',',modifiers.length()-1)!=modifiers.npos) { + modifiers.erase(modifiers.length()-1, 1); + } + + return modifiers; +} + +/* + * REmove all shortucts from the users file + */ + +void sp_shortcuts_delete_all_from_file() { + + + char const *filename = get_path(USER, KEYS, "default.xml"); + + XML::Document *doc=sp_repr_read_file(filename, nullptr); + if (!doc) { + g_warning("Unable to read keys file %s", filename); + return; + } + + XML::Node *root=doc->root(); + g_return_if_fail(!strcmp(root->name(), "keys")); + + XML::Node *iter=root->firstChild(); + while (iter) { + + if (strcmp(iter->name(), "bind")) { + // some unknown element, do not complain + iter = iter->next(); + continue; + } + + // Delete node + sp_repr_unparent(iter); + iter=root->firstChild(); + } + + + sp_repr_save_file(doc, filename, nullptr); + + GC::release(doc); +} + +Inkscape::XML::Document *sp_shortcut_create_template_file(char const *filename) { + + gchar const *buffer = + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?> " + "<keys name=\"My custom shortcuts\">" + "</keys>"; + + Inkscape::XML::Document *doc = sp_repr_read_mem(buffer, strlen(buffer), nullptr); + sp_repr_save_file(doc, filename, nullptr); + + return sp_repr_read_file(filename, nullptr); +} + +/* + * Get a list of keyboard shortcut files names and paths from the system and users paths + * Don't add the users custom keyboards file + */ +void sp_shortcut_get_file_names(std::vector<Glib::ustring> *names, std::vector<Glib::ustring> *paths) { + using namespace Inkscape::IO::Resource; + + std::vector<Glib::ustring> filenames = get_filenames(SYSTEM, KEYS, {".xml"}); + std::vector<Glib::ustring> filenames_user = get_filenames(USER, KEYS, {".xml"}, {"default.xml"}); // exclude default.xml as it only includes the user's modifications + filenames.insert(filenames.end(), filenames_user.begin(), filenames_user.end()); + + std::vector<std::pair<Glib::ustring, Glib::ustring>> names_and_paths; + for(auto &filename: filenames) { + Glib::ustring label = Glib::path_get_basename(filename); + Glib::ustring filename_relative = sp_relative_path_from_path(filename, std::string(get_path(SYSTEM, KEYS))); + + XML::Document *doc = sp_repr_read_file(filename.c_str(), nullptr); + if (!doc) { + g_warning("Unable to read keyboard shortcut file %s", filename.c_str()); + continue; + } + + // Get the "key name" from the root element of each file + XML::Node *root=doc->root(); + if (!strcmp(root->name(), "keys")) { + gchar const *name=root->attribute("name"); + if (name) { + label = Glib::ustring(name) + " (" + label + ")"; + } + std::pair<Glib::ustring, Glib::ustring> name_and_path; + name_and_path = std::make_pair(label, filename_relative); + names_and_paths.push_back(name_and_path); + } else { + g_warning("Not a shortcut keys file %s", filename.c_str()); + } + Inkscape::GC::release(doc); + } + + // sort by name + std::sort(names_and_paths.begin(), names_and_paths.end(), + [](std::pair<Glib::ustring, Glib::ustring> pair1, std::pair<Glib::ustring, Glib::ustring> pair2) { + return Glib::path_get_basename(pair1.first).compare(Glib::path_get_basename(pair2.first)) < 0; + }); + auto it_default = std::find_if(names_and_paths.begin(), names_and_paths.end(), + [](std::pair<Glib::ustring, Glib::ustring>& pair) { + return !Glib::path_get_basename(pair.second).compare("default.xml"); + }); + if (it_default != names_and_paths.end()) { + std::rotate(names_and_paths.begin(), it_default, it_default+1); + } + + // transform pairs to output vectors + std::transform(names_and_paths.begin(),names_and_paths.end(), std::back_inserter(*names), + [](const std::pair<Glib::ustring, Glib::ustring>& pair) { return pair.first; }); + std::transform(names_and_paths.begin(),names_and_paths.end(), std::back_inserter(*paths), + [](const std::pair<Glib::ustring, Glib::ustring>& pair) { return pair.second; }); +} + +Glib::ustring sp_shortcut_get_file_path() +{ + //# Get the current directory for finding files + Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring attr = prefs->getString("/dialogs/save_export/path"); + if (!attr.empty()) open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + if (open_path.empty()) { + /* Grab document directory */ + const gchar* docURI = SP_ACTIVE_DOCUMENT->getDocumentURI(); + if (docURI) { + open_path = Glib::path_get_dirname(docURI); + open_path.append(G_DIR_SEPARATOR_S); + } + } + + //# If no open path, default to our home directory + if (open_path.empty()) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + return open_path; +} + +//static Inkscape::UI::Dialog::FileSaveDialog * saveDialog = NULL; + +void sp_shortcut_file_export() +{ + Glib::ustring open_path = sp_shortcut_get_file_path(); + open_path.append("shortcut_keys.xml"); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Glib::ustring filename; + + Inkscape::UI::Dialog::FileSaveDialog *saveDialog = + Inkscape::UI::Dialog::FileSaveDialog::create( + *(desktop->getToplevel()), + open_path, + Inkscape::UI::Dialog::CUSTOM_TYPE, + _("Select a filename for exporting"), + "", + "", + Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS + ); + saveDialog->addFileType(_("Inkscape shortcuts (*.xml)"), ".xml"); + + + bool success = saveDialog->show(); + if (!success) { + delete saveDialog; + return; + } + + Glib::ustring fileName = saveDialog->getFilename(); + if (fileName.size() > 0) { + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + sp_shortcut_file_export_do(newFileName.c_str()); + } + + delete saveDialog; +} + +bool sp_shortcut_file_import() { + + Glib::ustring open_path = sp_shortcut_get_file_path(); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::UI::Dialog::FileOpenDialog *importFileDialog = + Inkscape::UI::Dialog::FileOpenDialog::create( + *desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::CUSTOM_TYPE, + _("Select a file to import")); + importFileDialog->addFilterMenu(_("Inkscape shortcuts (*.xml)"), "*.xml"); + + //# Show the dialog + bool const success = importFileDialog->show(); + + if (!success) { + delete importFileDialog; + return false; + } + + Glib::ustring fileName = importFileDialog->getFilename(); + sp_shortcut_file_import_do(fileName.c_str()); + + delete importFileDialog; + + return true; +} + +void sp_shortcut_file_import_do(char const *importname) { + + XML::Document *doc=sp_repr_read_file(importname, nullptr); + if (!doc) { + g_warning("Unable to read keyboard shortcut file %s", importname); + return; + } + + char const *filename = get_path(USER, KEYS, "default.xml"); + sp_repr_save_file(doc, filename, nullptr); + + GC::release(doc); + + sp_shortcut_init(); +} + +void sp_shortcut_file_export_do(char const *exportname) { + + char const *filename = get_path(USER, KEYS, "default.xml"); + + XML::Document *doc=sp_repr_read_file(filename, nullptr); + if (!doc) { + g_warning("Unable to read keyboard shortcut file %s", filename); + return; + } + + sp_repr_save_file(doc, exportname, nullptr); + + GC::release(doc); +} +/* + * Add or delete a shortcut to the users default.xml keys file + * @param addremove - when true add/override a shortcut, when false remove shortcut + * @param addshift - when true addthe Shifg modifier to the non-display element + * + * Shortcut file consists of pairs of bind elements : + * Element (a) is used for shortcut display in menus (display="True") and contains the gdk_keyval_name of the shortcut key + * Element (b) is used in shortcut lookup and contains an uppercase version of the gdk_keyval_name, + * or a gdk_keyval_name name and the "Shift" modifier for Shift altered hardware code keys (see get_latin_keyval() for explanation) + */ +void sp_shortcut_delete_from_file(char const * /*action*/, unsigned int const shortcut) { + + char const *filename = get_path(USER, KEYS, "default.xml"); + + XML::Document *doc=sp_repr_read_file(filename, nullptr); + if (!doc) { + g_warning("Unable to read keyboard shortcut file %s", filename); + return; + } + + gchar *key = gdk_keyval_name (sp_shortcut_get_key(shortcut)); + std::string modifiers = sp_shortcut_to_label(shortcut & (SP_SHORTCUT_MODIFIER_MASK)); + + if (!key) { + g_warning("Unknown key for shortcut %u", shortcut); + return; + } + + //g_message("Removing key %s, mods %s action %s", key, modifiers.c_str(), action); + + XML::Node *root=doc->root(); + g_return_if_fail(!strcmp(root->name(), "keys")); + XML::Node *iter=root->firstChild(); + while (iter) { + + if (strcmp(iter->name(), "bind")) { + // some unknown element, do not complain + iter = iter->next(); + continue; + } + + gchar const *verb_name=iter->attribute("action"); + if (!verb_name) { + iter = iter->next(); + continue; + } + + gchar const *keyval_name = iter->attribute("key"); + if (!keyval_name || !*keyval_name) { + // that's ok, it's just listed for reference without assignment, skip it + iter = iter->next(); + continue; + } + + if (Glib::ustring(key).lowercase() != Glib::ustring(keyval_name).lowercase()) { + // If deleting, then delete both the upper and lower case versions + iter = iter->next(); + continue; + } + + gchar const *modifiers_string = iter->attribute("modifiers"); + if ((modifiers_string && !strcmp(modifiers.c_str(), modifiers_string)) || + (!modifiers_string && modifiers.empty())) { + //Looks like a match + // Delete node + sp_repr_unparent(iter); + iter = root->firstChild(); + continue; + } + iter = iter->next(); + } + + sp_repr_save_file(doc, filename, nullptr); + + GC::release(doc); + +} + +void sp_shortcut_add_to_file(char const *action, unsigned int const shortcut) { + + char const *filename = get_path(USER, KEYS, "default.xml"); + + XML::Document *doc=sp_repr_read_file(filename, nullptr); + if (!doc) { + g_warning("Unable to read keyboard shortcut file %s, creating ....", filename); + doc = sp_shortcut_create_template_file(filename); + if (!doc) { + g_warning("Unable to create keyboard shortcut file %s", filename); + return; + } + } + + gchar *key = gdk_keyval_name (sp_shortcut_get_key(shortcut)); + std::string modifiers = sp_shortcut_to_label(shortcut & (SP_SHORTCUT_MODIFIER_MASK)); + + if (!key) { + g_warning("Unknown key for shortcut %u", shortcut); + return; + } + + //g_message("Adding key %s, mods %s action %s", key, modifiers.c_str(), action); + + // Add node + Inkscape::XML::Node *newnode; + newnode = doc->createElement("bind"); + newnode->setAttribute("key", key); + newnode->setAttributeOrRemoveIfEmpty("modifiers", modifiers); + newnode->setAttribute("action", action); + newnode->setAttribute("display", "true"); + + doc->root()->appendChild(newnode); + + if (strlen(key) == 1) { + // Add another uppercase version if a character + Inkscape::XML::Node *newnode; + newnode = doc->createElement("bind"); + newnode->setAttribute("key", Glib::ustring(key).uppercase()); + newnode->setAttributeOrRemoveIfEmpty("modifiers", modifiers); + newnode->setAttribute("action", action); + doc->root()->appendChild(newnode); + } + + sp_repr_save_file(doc, filename, nullptr); + + GC::release(doc); + +} +static void read_shortcuts_file(char const *filename, bool const is_user_set) { + XML::Document *doc=sp_repr_read_file(filename, nullptr); + if (!doc) { + g_warning("Unable to read keys file %s", filename); + return; + } + + XML::Node const *root=doc->root(); + g_return_if_fail(!strcmp(root->name(), "keys")); + XML::NodeConstSiblingIterator iter=root->firstChild(); + for ( ; iter ; ++iter ) { + bool is_primary; + + if (!strcmp(iter->name(), "bind")) { + is_primary = iter->attribute("display") && strcmp(iter->attribute("display"), "false") && strcmp(iter->attribute("display"), "0"); + } else { + // some unknown element, do not complain + continue; + } + + gchar const *verb_name=iter->attribute("action"); + if (!verb_name) { + g_warning("Missing verb name (action= attribute) for shortcut"); + continue; + } + + Inkscape::Verb *verb=Inkscape::Verb::getbyid(verb_name); + if (!verb +#ifndef HAVE_ASPELL + && strcmp(verb_name, "DialogSpellcheck") != 0 +#endif + ) { + g_warning("Unknown verb name: %s", verb_name); + continue; + } + + gchar const *keyval_name=iter->attribute("key"); + if (!keyval_name || !*keyval_name) { + // that's ok, it's just listed for reference without assignment, skip it + continue; + } + + guint keyval=gdk_keyval_from_name(keyval_name); + if (keyval == GDK_KEY_VoidSymbol || keyval == 0) { + g_warning("Unknown keyval %s for %s", keyval_name, verb_name); + continue; + } + + guint modifiers=0; + + gchar const *modifiers_string=iter->attribute("modifiers"); + if (modifiers_string) { + gchar const *iter=modifiers_string; + while (*iter) { + size_t length=strcspn(iter, ","); + gchar *mod=g_strndup(iter, length); + if (!strcmp(mod, "Control") || !strcmp(mod, "Ctrl")) { + modifiers |= SP_SHORTCUT_CONTROL_MASK; + } else if (!strcmp(mod, "Shift")) { + modifiers |= SP_SHORTCUT_SHIFT_MASK; + } else if (!strcmp(mod, "Alt")) { + modifiers |= SP_SHORTCUT_ALT_MASK; + } else if (!strcmp(mod, "Super")) { + modifiers |= SP_SHORTCUT_SUPER_MASK; + } else if (!strcmp(mod, "Hyper") || !strcmp(mod, "Cmd")) { + modifiers |= SP_SHORTCUT_HYPER_MASK; + } else if (!strcmp(mod, "Meta")) { + modifiers |= SP_SHORTCUT_META_MASK; + } else if (!strcmp(mod, "Primary")) { + auto display = Gdk::Display::get_default(); + if (display) { + GdkKeymap* keymap = display->get_keymap(); + GdkModifierType mod = + gdk_keymap_get_modifier_mask (keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR); + gdk_keymap_add_virtual_modifiers(keymap, &mod); + if (mod & GDK_CONTROL_MASK) + modifiers |= SP_SHORTCUT_CONTROL_MASK; + else if (mod & GDK_META_MASK) + modifiers |= SP_SHORTCUT_META_MASK; + else { + g_warning("unsupported primary accelerator "); + modifiers |= SP_SHORTCUT_CONTROL_MASK; + } + } else { + modifiers |= SP_SHORTCUT_CONTROL_MASK; + } + } else { + g_warning("Unknown modifier %s for %s", mod, verb_name); + } + g_free(mod); + iter += length; + if (*iter) iter++; + } + } + + sp_shortcut_set(keyval | modifiers, verb, is_primary, is_user_set); + } + + GC::release(doc); +} + +/** + * Removes a keyboard shortcut for the given verb. + * (Removes any existing binding for the given shortcut, including appropriately + * adjusting sp_shortcut_get_primary if necessary.)* + */ +void +sp_shortcut_unset(unsigned int const shortcut) +{ + if (!verbs) sp_shortcut_init(); + + Inkscape::Verb *verb = (*verbs)[shortcut]; + + /* Maintain the invariant that sp_shortcut_get_primary(v) returns either 0 or a valid shortcut for v. */ + if (verb) { + + (*verbs)[shortcut] = nullptr; + + unsigned int const old_primary = (*primary_shortcuts)[verb]; + if (old_primary == shortcut) { + (*primary_shortcuts)[verb] = 0; + } + + } +} + +GtkAccelGroup * +sp_shortcut_get_accel_group() +{ + static GtkAccelGroup *accel_group = nullptr; + + if (!accel_group) { + accel_group = gtk_accel_group_new (); + } + + return accel_group; +} + +/** + * Adds a gtk accelerator to a widget + * Used to display the keyboard shortcuts in the main menu items + */ +void +sp_shortcut_add_accelerator(GtkWidget *item, unsigned int const shortcut) +{ + if (shortcut == GDK_KEY_VoidSymbol) { + return; + } + + unsigned int accel_key = sp_shortcut_get_key(shortcut); + if (accel_key > 0) { + gtk_widget_add_accelerator (item, + "activate", + sp_shortcut_get_accel_group(), + accel_key, + sp_shortcut_get_modifiers(shortcut), + GTK_ACCEL_VISIBLE); + } +} + + +unsigned int +sp_shortcut_get_key(unsigned int const shortcut) +{ + return (shortcut & (~SP_SHORTCUT_MODIFIER_MASK)); +} + +GdkModifierType +sp_shortcut_get_modifiers(unsigned int const shortcut) +{ + return static_cast<GdkModifierType>( + ((shortcut & SP_SHORTCUT_SHIFT_MASK) ? GDK_SHIFT_MASK : 0) | + ((shortcut & SP_SHORTCUT_CONTROL_MASK) ? GDK_CONTROL_MASK : 0) | + ((shortcut & SP_SHORTCUT_SUPER_MASK) ? GDK_SUPER_MASK : 0) | + ((shortcut & SP_SHORTCUT_HYPER_MASK) ? GDK_HYPER_MASK : 0) | + ((shortcut & SP_SHORTCUT_META_MASK) ? GDK_META_MASK : 0) | + ((shortcut & SP_SHORTCUT_ALT_MASK) ? GDK_MOD1_MASK : 0) + ); +} + +/** + * Adds a keyboard shortcut for the given verb. + * (Removes any existing binding for the given shortcut, including appropriately + * adjusting sp_shortcut_get_primary if necessary.) + * + * \param is_primary True iff this is the shortcut to be written in menu items or buttons. + * + * \post sp_shortcut_get_verb(shortcut) == verb. + * \post !is_primary or sp_shortcut_get_primary(verb) == shortcut. + */ +void +sp_shortcut_set(unsigned int const shortcut, Inkscape::Verb *const verb, bool const is_primary, bool const is_user_set) +{ + if (!verbs) sp_shortcut_init(); + + Inkscape::Verb *old_verb = (*verbs)[shortcut]; + (*verbs)[shortcut] = verb; + + /* Maintain the invariant that sp_shortcut_get_primary(v) returns either 0 or a valid shortcut for v. */ + if (old_verb && old_verb != verb) { + unsigned int const old_primary = (*primary_shortcuts)[old_verb]; + + if (old_primary == shortcut) { + (*primary_shortcuts)[old_verb] = 0; + (*user_shortcuts)[old_verb] = 0; + } + } + + if (is_primary) { + (*primary_shortcuts)[verb] = shortcut; + (*user_shortcuts)[verb] = is_user_set; + } +} + +Inkscape::Verb * +sp_shortcut_get_verb(unsigned int shortcut) +{ + if (!verbs) sp_shortcut_init(); + return (*verbs)[shortcut]; +} + +unsigned int sp_shortcut_get_primary(Inkscape::Verb *verb) +{ + unsigned int result = GDK_KEY_VoidSymbol; + if (!primary_shortcuts) { + sp_shortcut_init(); + } + + if (primary_shortcuts->count(verb)) { + result = (*primary_shortcuts)[verb]; + } + return result; +} + +bool sp_shortcut_is_user_set(Inkscape::Verb *verb) +{ + unsigned int result = false; + if (!primary_shortcuts) { + sp_shortcut_init(); + } + + if (primary_shortcuts->count(verb)) { + result = (*user_shortcuts)[verb]; + } + return result; +} + +gchar *sp_shortcut_get_label(unsigned int shortcut) +{ + // The comment below was copied from the function sp_ui_shortcut_string in interface.cpp (which was subsequently removed) + /* TODO: This function shouldn't exist. Our callers should use GtkAccelLabel instead of + * a generic GtkLabel containing this string, and should call gtk_widget_add_accelerator. + * Will probably need to change sp_shortcut_invoke callers. + * + * The existing gtk_label_new_with_mnemonic call can be replaced with + * g_object_new(GTK_TYPE_ACCEL_LABEL, NULL) followed by + * gtk_label_set_text_with_mnemonic(lbl, str). + */ + gchar *result = nullptr; + if (shortcut != GDK_KEY_VoidSymbol) { + result = gtk_accelerator_get_label( + sp_shortcut_get_key(shortcut), + sp_shortcut_get_modifiers(shortcut)); + } + return result; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/shortcuts.h b/src/shortcuts.h new file mode 100644 index 0000000..88d3180 --- /dev/null +++ b/src/shortcuts.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_SHORTCUTS_H +#define SEEN_SP_SHORTCUTS_H + +/* + * Keyboard shortcut processing + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * This code is in public domain + */ + +#include <vector> +#include <gdkmm/types.h> +#include <gdk/gdktypes.h> + + +typedef struct _GtkAccelGroup GtkAccelGroup; +typedef struct _GtkWidget GtkWidget; + +namespace Inkscape { + class Verb; + namespace UI { + namespace View { + class View; + } + } +} + +/* We define high-bit mask for packing into single int */ + +#define SP_SHORTCUT_SHIFT_MASK GDK_MODIFIER_RESERVED_20_MASK +#define SP_SHORTCUT_CONTROL_MASK GDK_MODIFIER_RESERVED_21_MASK +#define SP_SHORTCUT_ALT_MASK GDK_MODIFIER_RESERVED_22_MASK +#define SP_SHORTCUT_SUPER_MASK GDK_MODIFIER_RESERVED_23_MASK +#define SP_SHORTCUT_HYPER_MASK GDK_MODIFIER_RESERVED_24_MASK +#define SP_SHORTCUT_META_MASK GDK_MODIFIER_RESERVED_25_MASK +#define SP_SHORTCUT_MODIFIER_MASK (SP_SHORTCUT_SHIFT_MASK|SP_SHORTCUT_CONTROL_MASK|SP_SHORTCUT_ALT_MASK|SP_SHORTCUT_SUPER_MASK|SP_SHORTCUT_HYPER_MASK|SP_SHORTCUT_META_MASK) + + +/* Returns true if action was performed */ +bool sp_shortcut_invoke (unsigned int shortcut, Inkscape::UI::View::View *view); + +void sp_shortcut_init(); +Inkscape::Verb * sp_shortcut_get_verb (unsigned int shortcut); +unsigned int sp_shortcut_get_primary (Inkscape::Verb * verb); // Returns GDK_VoidSymbol if no shortcut is found. +char* sp_shortcut_get_label (unsigned int shortcut); // Returns the human readable form of the shortcut (or NULL), for example Shift+Ctrl+F. Free the returned string with g_free. +void sp_shortcut_set(unsigned int const shortcut, Inkscape::Verb *const verb, bool const is_primary, bool const is_user_set=false); +void sp_shortcut_unset(unsigned int const shortcut); +void sp_shortcut_add_to_file(char const *action, unsigned int const shortcut); +void sp_shortcut_delete_from_file(char const *action, unsigned int const shortcut); +void sp_shortcuts_delete_all_from_file(); +Glib::ustring sp_shortcut_to_label(unsigned int const shortcut); +unsigned int sp_shortcut_get_from_event(GdkEventKey const *event); +unsigned int sp_shortcut_get_from_gdk_event(unsigned int accel_key, Gdk::ModifierType accel_mods, unsigned int hardware_keycode); +unsigned int sp_shortcut_get_for_event(GdkEventKey const *event); +void sp_shortcut_get_file_names(std::vector<Glib::ustring> *names, std::vector<Glib::ustring> *paths); +bool sp_shortcut_is_user_set(Inkscape::Verb *verb); +void sp_shortcut_file_export(); +bool sp_shortcut_file_import(); +void sp_shortcut_file_import_do(char const *importname); +void sp_shortcut_file_export_do(char const *exportname); +GtkAccelGroup *sp_shortcut_get_accel_group(); +void sp_shortcut_add_accelerator(GtkWidget *item, unsigned int const shortcut); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/show-preview.bmp b/src/show-preview.bmp new file mode 100644 index 0000000..a895af0 Binary files /dev/null and b/src/show-preview.bmp differ diff --git a/src/snap-candidate.h b/src/snap-candidate.h new file mode 100644 index 0000000..4854fba --- /dev/null +++ b/src/snap-candidate.h @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAP_CANDIDATE_H +#define SEEN_SNAP_CANDIDATE_H + +/** + * @file + * Some utility classes to store various kinds of snap candidates. + */ +/* + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2010 - 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/rect.h> +#include <cstdio> +#include <utility> + +#include "snap-enums.h" + +class SPItem; // forward declaration + +namespace Inkscape { + +/// Class to store data for points which are snap candidates, either as a source or as a target +class SnapCandidatePoint +{ +public: + SnapCandidatePoint() = default;; // only needed / used for resizing() of a vector in seltrans.cpp; do not use uninitialized instances! + + SnapCandidatePoint(Geom::Point const &point, Inkscape::SnapSourceType const source, long const source_num, Inkscape::SnapTargetType const target, Geom::OptRect bbox) + : _point(point), + _source_type(source), + _target_type(target), + _source_num(source_num), + _target_bbox(std::move(bbox)), + _dist() + { + }; + + SnapCandidatePoint(Geom::Point const &point, Inkscape::SnapSourceType const source, Inkscape::SnapTargetType const target) + : _point(point), + _source_type(source), + _target_type(target), + _target_bbox(Geom::OptRect()), + _dist() + { + _source_num = -1; + } + + SnapCandidatePoint(Geom::Point const &point, Inkscape::SnapSourceType const source) + : _point(point), + _source_type(source), + _target_type(Inkscape::SNAPTARGET_UNDEFINED), + _source_num(-1), + _target_bbox(Geom::OptRect()), + _dist() + { + }; + + inline Geom::Point const & getPoint() const {return _point;} + inline Inkscape::SnapSourceType getSourceType() const {return _source_type;} + inline Inkscape::SnapTargetType getTargetType() const {return _target_type;} + bool isSingleHandle() const {return (_source_type == SNAPSOURCE_NODE_HANDLE || _source_type == SNAPSOURCE_OTHER_HANDLE) && _source_num == -1;} + + inline long getSourceNum() const {return _source_num;} + void setSourceNum(long num) {_source_num = num;} + + void setDistance(Geom::Coord dist) {_dist = dist;} + Geom::Coord getDistance() { return _dist;} + + void addOrigin(Geom::Point pt) { _origins_and_vectors.emplace_back(pt, false); } + void addVector(Geom::Point v) { _origins_and_vectors.emplace_back(v, true); } + std::vector<std::pair<Geom::Point, bool> > const & getOriginsAndVectors() const {return _origins_and_vectors;} + + bool operator <(const SnapCandidatePoint &other) const { return _dist < other._dist; } // Needed for sorting the SnapCandidatePoints + inline Geom::OptRect const getTargetBBox() const {return _target_bbox;} + +private: + // Coordinates of the point + Geom::Point _point; + // For perpendicular or tangential snapping of a ROTATING line we need to know its (stationary) starting point. + // In case of editing with the node tool, a node can be connected to two lines simultaneously, in which case we + // need to consider two starting points; Therefore a vector containing multiple starting points is used here. However, + // for perpendicular or tangential snapping of a TRANSLATING line we need to know its direction vector instead. This + // vector will be stored in the same way as the starting point is, i.e. as a Geom::Point. A boolean is paired to this + // point, which is true for vectors but false for origins + std::vector<std::pair<Geom::Point, bool> > _origins_and_vectors; + + // If this SnapCandidatePoint is a snap source, then _source_type must be defined. If it + // is a snap target, then _target_type must be defined. If it's yet unknown whether it will + // be a source or target, then both may be defined + Inkscape::SnapSourceType _source_type; + Inkscape::SnapTargetType _target_type; + + //Sequence number of the source point within the set of points that is to be snapped. + // - Starts counting at zero, but only if there might be more points following (e.g. in the selector tool) + // - Minus one (-1) if we're sure that we have only a single point + long _source_num; + + // If this is a target and it belongs to a bounding box, e.g. when the target type is + // SNAPTARGET_BBOX_EDGE_MIDPOINT, then _target_bbox stores the relevant bounding box + Geom::OptRect _target_bbox; + + // For finding the snap candidate closest to the mouse pointer + Geom::Coord _dist; +}; + +class SnapCandidateItem +{ +public: + SnapCandidateItem(SPItem* item, bool clip_or_mask, Geom::Affine additional_affine) + : item(item), clip_or_mask(clip_or_mask), additional_affine(additional_affine) {} + ~SnapCandidateItem() = default;; + + SPItem* item; // An item that is to be considered for snapping to + bool clip_or_mask; // If true, then item refers to a clipping path or a mask + + /* To find out the absolute position of a clipping path or mask, we not only need to know + * the transformation of the clipping path or mask itself, but also the transformation of + * the object to which the clip or mask is being applied; that transformation is stored here + */ + Geom::Affine additional_affine; +} +; + +class SnapCandidatePath +{ + +public: + SnapCandidatePath(Geom::PathVector* path, SnapTargetType target, Geom::OptRect bbox, bool edited = false) + : path_vector(path), target_type(target), target_bbox(std::move(bbox)), currently_being_edited(edited) {}; + ~SnapCandidatePath() = default;; + + Geom::PathVector* path_vector; + SnapTargetType target_type; + Geom::OptRect target_bbox; + bool currently_being_edited; // true for the path that's currently being edited in the node tool (if any) + +}; +} // end of namespace Inkscape +#endif /* !SEEN_SNAP_CANDIDATE_H */ diff --git a/src/snap-enums.h b/src/snap-enums.h new file mode 100644 index 0000000..321963b --- /dev/null +++ b/src/snap-enums.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SNAPENUMS_H_ +#define SNAPENUMS_H_ +/* + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2010 - 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +namespace Inkscape { + +/** + * enumerations of snap source types and snap target types. + */ +enum SnapSourceType { // When adding source types here, then also update Inkscape::SnapPreferences::source2target! + SNAPSOURCE_UNDEFINED = 0, + //------------------------------------------------------------------- + // Bbox points can be located at the edge of the stroke (for visual bboxes); they will therefore not snap + // to nodes because these are always located at the center of the stroke + SNAPSOURCE_BBOX_CATEGORY = 16, // will be used as a flag and must therefore be a power of two. Also, + // must be larger than the largest number of targets in a single group + SNAPSOURCE_BBOX_CORNER, + SNAPSOURCE_BBOX_MIDPOINT, + SNAPSOURCE_BBOX_EDGE_MIDPOINT, + //------------------------------------------------------------------- + // For the same reason, nodes will not snap to bbox points + SNAPSOURCE_NODE_CATEGORY = 32, // will be used as a flag and must therefore be a power of two + SNAPSOURCE_NODE_SMOOTH, // Symmetrical nodes are also considered to be smooth; there's no dedicated type for symm. nodes + SNAPSOURCE_NODE_CUSP, + SNAPSOURCE_LINE_MIDPOINT, + SNAPSOURCE_PATH_INTERSECTION, + SNAPSOURCE_RECT_CORNER, // of a rectangle, so at the center of the stroke + SNAPSOURCE_CONVEX_HULL_CORNER, + SNAPSOURCE_ELLIPSE_QUADRANT_POINT, + SNAPSOURCE_NODE_HANDLE, // eg. nodes in the path editor, handles of stars or rectangles, etc. (tied to a stroke) + //------------------------------------------------------------------- + // Other points (e.g. guides) will snap to both bounding boxes and nodes + SNAPSOURCE_DATUMS_CATEGORY = 64, // will be used as a flag and must therefore be a power of two + SNAPSOURCE_GUIDE, + SNAPSOURCE_GUIDE_ORIGIN, + //------------------------------------------------------------------- + // Other points (e.g. gradient knots, image corners) will snap to both bounding boxes and nodes + SNAPSOURCE_OTHERS_CATEGORY = 128, // will be used as a flag and must therefore be a power of two + SNAPSOURCE_ROTATION_CENTER, + SNAPSOURCE_OBJECT_MIDPOINT, // midpoint of rectangles, ellipses, polygon, etc. + SNAPSOURCE_IMG_CORNER, + SNAPSOURCE_TEXT_ANCHOR, + SNAPSOURCE_OTHER_HANDLE, // eg. the handle of a gradient or of a connector (ie not being tied to a stroke) + SNAPSOURCE_GRID_PITCH, // eg. when pasting or alt-dragging in the selector tool; not really a snap source +}; + +enum SnapTargetType { + SNAPTARGET_UNDEFINED = 0, + //------------------------------------------------------------------- + SNAPTARGET_BBOX_CATEGORY = 16, // will be used as a flag and must therefore be a power of two. Also, + // must be larger than the largest number of targets in a single group + // i.e > 15 because that's the number of targets in the "others" group + SNAPTARGET_BBOX_CORNER, + SNAPTARGET_BBOX_EDGE, + SNAPTARGET_BBOX_EDGE_MIDPOINT, + SNAPTARGET_BBOX_MIDPOINT, + //------------------------------------------------------------------- + SNAPTARGET_NODE_CATEGORY = 32, // will be used as a flag and must therefore be a power of two + SNAPTARGET_NODE_SMOOTH, + SNAPTARGET_NODE_CUSP, + SNAPTARGET_LINE_MIDPOINT, + SNAPTARGET_PATH, // If path targets are added here, then also add them to the list in findBestSnap() + SNAPTARGET_PATH_PERPENDICULAR, + SNAPTARGET_PATH_TANGENTIAL, + SNAPTARGET_PATH_INTERSECTION, + SNAPTARGET_PATH_GUIDE_INTERSECTION, + SNAPTARGET_PATH_CLIP, + SNAPTARGET_PATH_MASK, + SNAPTARGET_ELLIPSE_QUADRANT_POINT, // this corner is at the center of the stroke + SNAPTARGET_RECT_CORNER, // of a rectangle, so this corner is at the center of the stroke + //------------------------------------------------------------------- + SNAPTARGET_DATUMS_CATEGORY = 64, // will be used as a flag and must therefore be a power of two + SNAPTARGET_GRID, + SNAPTARGET_GRID_INTERSECTION, + SNAPTARGET_GRID_PERPENDICULAR, + SNAPTARGET_GUIDE, + SNAPTARGET_GUIDE_INTERSECTION, + SNAPTARGET_GUIDE_ORIGIN, + SNAPTARGET_GUIDE_PERPENDICULAR, + SNAPTARGET_GRID_GUIDE_INTERSECTION, + SNAPTARGET_PAGE_BORDER, + SNAPTARGET_PAGE_CORNER, + //------------------------------------------------------------------- + SNAPTARGET_OTHERS_CATEGORY = 128, // will be used as a flag and must therefore be a power of two + SNAPTARGET_OBJECT_MIDPOINT, + SNAPTARGET_IMG_CORNER, + SNAPTARGET_ROTATION_CENTER, + SNAPTARGET_TEXT_ANCHOR, + SNAPTARGET_TEXT_BASELINE, + SNAPTARGET_CONSTRAINED_ANGLE, + SNAPTARGET_CONSTRAINT, + //------------------------------------------------------------------- + SNAPTARGET_MAX_ENUM_VALUE +}; + +} +#endif /* SNAPENUMS_H_ */ diff --git a/src/snap-preferences.cpp b/src/snap-preferences.cpp new file mode 100644 index 0000000..876293a --- /dev/null +++ b/src/snap-preferences.cpp @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Storing of snapping preferences. + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2008 - 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "inkscape.h" + +Inkscape::SnapPreferences::SnapPreferences() : + _snap_enabled_globally(true), + _snap_postponed_globally(false), + _strict_snapping(true), + _snap_perp(false), + _snap_tang(false) +{ + // Check for powers of two; see the comments in snap-enums.h + g_assert((SNAPTARGET_BBOX_CATEGORY != 0) && !(SNAPTARGET_BBOX_CATEGORY & (SNAPTARGET_BBOX_CATEGORY - 1))); + g_assert((SNAPTARGET_NODE_CATEGORY != 0) && !(SNAPTARGET_NODE_CATEGORY & (SNAPTARGET_NODE_CATEGORY - 1))); + g_assert((SNAPTARGET_DATUMS_CATEGORY != 0) && !(SNAPTARGET_DATUMS_CATEGORY & (SNAPTARGET_DATUMS_CATEGORY - 1))); + g_assert((SNAPTARGET_OTHERS_CATEGORY != 0) && !(SNAPTARGET_OTHERS_CATEGORY & (SNAPTARGET_OTHERS_CATEGORY - 1))); + + for (int & _active_snap_target : _active_snap_targets) { + _active_snap_target = -1; + } +} + +bool Inkscape::SnapPreferences::isAnyDatumSnappable() const +{ + return isTargetSnappable(SNAPTARGET_GUIDE, SNAPTARGET_GRID, SNAPTARGET_PAGE_BORDER); +} + +bool Inkscape::SnapPreferences::isAnyCategorySnappable() const +{ + return isTargetSnappable(SNAPTARGET_NODE_CATEGORY, SNAPTARGET_BBOX_CATEGORY, SNAPTARGET_OTHERS_CATEGORY) || isTargetSnappable(SNAPTARGET_GUIDE, SNAPTARGET_GRID, SNAPTARGET_PAGE_BORDER); +} + +void Inkscape::SnapPreferences::_mapTargetToArrayIndex(Inkscape::SnapTargetType &target, bool &always_on, bool &group_on) const +{ + if (target == SNAPTARGET_BBOX_CATEGORY || + target == SNAPTARGET_NODE_CATEGORY || + target == SNAPTARGET_OTHERS_CATEGORY || + target == SNAPTARGET_DATUMS_CATEGORY) { + // These main targets should be handled separately, because otherwise we might call isTargetSnappable() + // for them (to check whether the corresponding group is on) which would lead to an infinite recursive loop + always_on = (target == SNAPTARGET_DATUMS_CATEGORY); + group_on = true; + return; + } + + if (target & SNAPTARGET_BBOX_CATEGORY) { + group_on = isTargetSnappable(SNAPTARGET_BBOX_CATEGORY); // Only if the group with bbox sources/targets has been enabled, then we might snap to any of the bbox targets + return; + } + + if (target & SNAPTARGET_NODE_CATEGORY) { + group_on = isTargetSnappable(SNAPTARGET_NODE_CATEGORY); // Only if the group with path/node sources/targets has been enabled, then we might snap to any of the nodes/paths + switch (target) { + case SNAPTARGET_RECT_CORNER: + target = SNAPTARGET_NODE_CUSP; + break; + case SNAPTARGET_ELLIPSE_QUADRANT_POINT: + target = SNAPTARGET_NODE_SMOOTH; + break; + case SNAPTARGET_PATH_GUIDE_INTERSECTION: + target = SNAPTARGET_PATH_INTERSECTION; + break; + case SNAPTARGET_PATH_PERPENDICULAR: + case SNAPTARGET_PATH_TANGENTIAL: + target = SNAPTARGET_PATH; + break; + default: + break; + } + + return; + } + + if (target & SNAPTARGET_DATUMS_CATEGORY) { + group_on = true; // These snap targets cannot be disabled as part of a disabled group; + switch (target) { + // Some snap targets don't have their own toggle. These targets are called "secondary targets". We will re-map + // them to their cousin which does have a toggle, and which is called a "primary target" + case SNAPTARGET_GRID_INTERSECTION: + case SNAPTARGET_GRID_PERPENDICULAR: + target = SNAPTARGET_GRID; + break; + case SNAPTARGET_GUIDE_INTERSECTION: + case SNAPTARGET_GUIDE_ORIGIN: + case SNAPTARGET_GUIDE_PERPENDICULAR: + target = SNAPTARGET_GUIDE; + break; + case SNAPTARGET_PAGE_CORNER: + target = SNAPTARGET_PAGE_BORDER; + break; + + // Some snap targets cannot be toggled at all, and are therefore always enabled + case SNAPTARGET_GRID_GUIDE_INTERSECTION: + always_on = true; // Doesn't have it's own button + break; + + // These are only listed for completeness + case SNAPTARGET_GRID: + case SNAPTARGET_GUIDE: + case SNAPTARGET_PAGE_BORDER: + case SNAPTARGET_DATUMS_CATEGORY: + break; + default: + g_warning("Snap-preferences warning: Undefined snap target (#%i)", target); + break; + } + return; + } + + if (target & SNAPTARGET_OTHERS_CATEGORY) { + // Only if the group with "other" snap sources/targets has been enabled, then we might snap to any of those targets + // ... but this doesn't hold for the page border, grids, and guides + group_on = isTargetSnappable(SNAPTARGET_OTHERS_CATEGORY); + switch (target) { + // Some snap targets don't have their own toggle. These targets are called "secondary targets". We will re-map + // them to their cousin which does have a toggle, and which is called a "primary target" + case SNAPTARGET_TEXT_ANCHOR: + target = SNAPTARGET_TEXT_BASELINE; + break; + + case SNAPTARGET_IMG_CORNER: // Doesn't have its own button, on if the group is on + target = SNAPTARGET_OTHERS_CATEGORY; + break; + // Some snap targets cannot be toggled at all, and are therefore always enabled + case SNAPTARGET_CONSTRAINED_ANGLE: + case SNAPTARGET_CONSTRAINT: + always_on = true; // Doesn't have it's own button + break; + + // These are only listed for completeness + case SNAPTARGET_OBJECT_MIDPOINT: + case SNAPTARGET_ROTATION_CENTER: + case SNAPTARGET_TEXT_BASELINE: + case SNAPTARGET_OTHERS_CATEGORY: + break; + default: + g_warning("Snap-preferences warning: Undefined snap target (#%i)", target); + break; + } + + return; + } + + if (target == SNAPTARGET_UNDEFINED ) { + g_warning("Snap-preferences warning: Undefined snaptarget (#%i)", target); + } else { + g_warning("Snap-preferences warning: Snaptarget not handled (#%i)", target); + } +} + +void Inkscape::SnapPreferences::setTargetSnappable(Inkscape::SnapTargetType const target, bool enabled) +{ + bool always_on = false; + bool group_on = false; // Only needed as a dummy + Inkscape::SnapTargetType index = target; + + _mapTargetToArrayIndex(index, always_on, group_on); + + if (always_on) { // If true, then this snap target is always active and cannot be toggled + // Catch coding errors + g_warning("Snap-preferences warning: Trying to enable/disable a snap target (#%i) that's always on by definition", index); + } else { + if (index == target) { // I.e. if it has not been re-mapped, then we have a primary target at hand + _active_snap_targets[index] = enabled; + } else { // If it has been re-mapped though, then this target does not have its own toggle button and should therefore not be set + g_warning("Snap-preferences warning: Trying to enable/disable a secondary snap target (#%i); only primary targets can be set", index); + } + } +} + +bool Inkscape::SnapPreferences::isTargetSnappable(Inkscape::SnapTargetType const target) const +{ + bool always_on = false; + bool group_on = false; + Inkscape::SnapTargetType index = target; + + _mapTargetToArrayIndex(index, always_on, group_on); + + if (group_on) { // If true, then this snap target is in a snap group that has been enabled (e.g. bbox group, nodes/paths group, or "others" group + if (always_on) { // If true, then this snap target is always active and cannot be toggled + return true; + } else { + if (_active_snap_targets[index] == -1) { + // Catch coding errors + g_warning("Snap-preferences warning: Using an uninitialized snap target setting (#%i)", index); + // This happens if setTargetSnappable() has not been called for this parameter, e.g. from within sp_namedview_set, + // or if this target index doesn't exist at all + } + return _active_snap_targets[index]; + } + } else { + return false; + } +} + +bool Inkscape::SnapPreferences::isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2) const { + return isTargetSnappable(target1) || isTargetSnappable(target2); +} + +bool Inkscape::SnapPreferences::isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3) const { + return isTargetSnappable(target1) || isTargetSnappable(target2) || isTargetSnappable(target3); +} + +bool Inkscape::SnapPreferences::isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3, Inkscape::SnapTargetType const target4) const { + return isTargetSnappable(target1) || isTargetSnappable(target2) || isTargetSnappable(target3) || isTargetSnappable(target4); +} + +bool Inkscape::SnapPreferences::isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3, Inkscape::SnapTargetType const target4, Inkscape::SnapTargetType const target5) const { + return isTargetSnappable(target1) || isTargetSnappable(target2) || isTargetSnappable(target3) || isTargetSnappable(target4) || isTargetSnappable(target5); +} + +bool Inkscape::SnapPreferences::isSnapButtonEnabled(Inkscape::SnapTargetType const target) const +{ + bool always_on = false; // Only needed as a dummy + bool group_on = false; // Only needed as a dummy + Inkscape::SnapTargetType index = target; + + _mapTargetToArrayIndex(index, always_on, group_on); + + if (_active_snap_targets[index] == -1) { + // Catch coding errors + g_warning("Snap-preferences warning: Using an uninitialized snap target setting (#%i)", index); + // This happens if setTargetSnappable() has not been called for this parameter, e.g. from within sp_namedview_set, + // or if this target index doesn't exist at all + } else { + if (index == target) { // I.e. if it has not been re-mapped, then we have a primary target at hand, which does have its own toggle button + return _active_snap_targets[index]; + } else { // If it has been re-mapped though, then this target does not have its own toggle button and therefore the button status cannot be read + g_warning("Snap-preferences warning: Trying to determine the button status of a secondary snap target (#%i); However, only primary targets have a button", index); + } + } + + return false; +} + +Inkscape::SnapTargetType Inkscape::SnapPreferences::source2target(Inkscape::SnapSourceType source) const +{ + switch (source) + { + case SNAPSOURCE_UNDEFINED: + return SNAPTARGET_UNDEFINED; + case SNAPSOURCE_BBOX_CATEGORY: + return SNAPTARGET_BBOX_CATEGORY; + case SNAPSOURCE_BBOX_CORNER: + return SNAPTARGET_BBOX_CORNER; + case SNAPSOURCE_BBOX_MIDPOINT: + return SNAPTARGET_BBOX_MIDPOINT; + case SNAPSOURCE_BBOX_EDGE_MIDPOINT: + return SNAPTARGET_BBOX_EDGE_MIDPOINT; + case SNAPSOURCE_NODE_CATEGORY: + return SNAPTARGET_NODE_CATEGORY; + case SNAPSOURCE_NODE_SMOOTH: + return SNAPTARGET_NODE_SMOOTH; + case SNAPSOURCE_NODE_CUSP: + return SNAPTARGET_NODE_CUSP; + case SNAPSOURCE_LINE_MIDPOINT: + return SNAPTARGET_LINE_MIDPOINT; + case SNAPSOURCE_PATH_INTERSECTION: + return SNAPTARGET_PATH_INTERSECTION; + case SNAPSOURCE_RECT_CORNER: + return SNAPTARGET_RECT_CORNER; + case SNAPSOURCE_ELLIPSE_QUADRANT_POINT: + return SNAPTARGET_ELLIPSE_QUADRANT_POINT; + case SNAPSOURCE_DATUMS_CATEGORY: + return SNAPTARGET_DATUMS_CATEGORY; + case SNAPSOURCE_GUIDE: + return SNAPTARGET_GUIDE; + case SNAPSOURCE_GUIDE_ORIGIN: + return SNAPTARGET_GUIDE_ORIGIN; + case SNAPSOURCE_OTHERS_CATEGORY: + return SNAPTARGET_OTHERS_CATEGORY; + case SNAPSOURCE_ROTATION_CENTER: + return SNAPTARGET_ROTATION_CENTER; + case SNAPSOURCE_OBJECT_MIDPOINT: + return SNAPTARGET_OBJECT_MIDPOINT; + case SNAPSOURCE_IMG_CORNER: + return SNAPTARGET_IMG_CORNER; + case SNAPSOURCE_TEXT_ANCHOR: + return SNAPTARGET_TEXT_ANCHOR; + + case SNAPSOURCE_NODE_HANDLE: + case SNAPSOURCE_OTHER_HANDLE: + case SNAPSOURCE_CONVEX_HULL_CORNER: + // For these snapsources there doesn't exist an equivalent snap target + return SNAPTARGET_NODE_CATEGORY; + case SNAPSOURCE_GRID_PITCH: + return SNAPTARGET_GRID; + default: + g_warning("Mapping of snap source to snap target undefined"); + return SNAPTARGET_UNDEFINED; + } +} + +bool Inkscape::SnapPreferences::isSourceSnappable(Inkscape::SnapSourceType const source) const +{ + return isTargetSnappable(source2target(source)); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/snap-preferences.h b/src/snap-preferences.h new file mode 100644 index 0000000..4c0bc02 --- /dev/null +++ b/src/snap-preferences.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SNAPPREFERENCES_H_ +#define SNAPPREFERENCES_H_ + +/* + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2008 - 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snap-enums.h" + +namespace Inkscape +{ + +/** + * Storing of snapping preferences. + */ +class SnapPreferences +{ +public: + SnapPreferences(); + void setTargetSnappable(Inkscape::SnapTargetType const target, bool enabled); + bool isTargetSnappable(Inkscape::SnapTargetType const target) const; + bool isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2) const; + bool isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3) const; + bool isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3, Inkscape::SnapTargetType const target4) const; + bool isTargetSnappable(Inkscape::SnapTargetType const target1, Inkscape::SnapTargetType const target2, Inkscape::SnapTargetType const target3, Inkscape::SnapTargetType const target4, Inkscape::SnapTargetType const target5) const; + bool isSnapButtonEnabled(Inkscape::SnapTargetType const target) const; + + SnapTargetType source2target(SnapSourceType source) const; + bool isSourceSnappable(Inkscape::SnapSourceType const source) const; + + bool isAnyDatumSnappable() const; // Needed because we cannot toggle the datum snap targets as a group + bool isAnyCategorySnappable() const; + + void setSnapEnabledGlobally(bool enabled) {_snap_enabled_globally = enabled;} + bool getSnapEnabledGlobally() const {return _snap_enabled_globally;} + + void setSnapPostponedGlobally(bool postponed) {_snap_postponed_globally = postponed;} + bool getSnapPostponedGlobally() const {return _snap_postponed_globally;} + + bool getStrictSnapping() const {return _strict_snapping;} + + bool getSnapPerp() const {return _snap_perp;} + bool getSnapTang() const {return _snap_tang;} + void setSnapPerp(bool enabled) {_snap_perp = enabled;} + void setSnapTang(bool enabled) {_snap_tang = enabled;} + + double getGridTolerance() const {return _grid_tolerance;} + double getGuideTolerance() const {return _guide_tolerance;} + double getObjectTolerance() const {return _object_tolerance;} + + void setGridTolerance(double val) {_grid_tolerance = val;} + void setGuideTolerance(double val) {_guide_tolerance = val;} + void setObjectTolerance(double val) {_object_tolerance = val;} + +private: + + /** + * Map snap target to array index. + * + * The status of each snap toggle (in the snap toolbar) is stored as a boolean value in an array. This method returns the position + * of relevant boolean in that array, for any given type of snap target. For most snap targets, the enumerated value of that targets + * matches the position in the array (primary snap targets). This however does not hold for snap targets which don't have their own + * toggle button (secondary snap targets). + * + * PS: + * - For snap sources, just pass the corresponding snap target instead (each snap source should have a twin snap target, but not vice versa) + * - All parameters are passed by reference, and will be overwritten + * + * @param target Stores the enumerated snap target, which can be modified to correspond to the array index of this snap target. + * @param always_on If true, then this snap target is always active and cannot be toggled. + * @param group_on If true, then this snap target is in a snap group that has been enabled (e.g. bbox group, nodes/paths group, or "others" group. + */ + void _mapTargetToArrayIndex(Inkscape::SnapTargetType &target, bool &always_on, bool &group_on) const; + + int _active_snap_targets[Inkscape::SNAPTARGET_MAX_ENUM_VALUE]; + + bool _snap_enabled_globally; // Toggles ALL snapping + bool _snap_postponed_globally; // Hold all snapping temporarily when the mouse is moving fast + + //If enabled, then bbox corners will only snap to bboxes, + //and nodes will only snap to nodes and paths. We will not + //snap bbox corners to nodes, or nodes to bboxes. + //(snapping to grids and guides is not affected by this) + bool _strict_snapping; + + bool _snap_perp; + bool _snap_tang; + + double _grid_tolerance; + double _guide_tolerance; + double _object_tolerance; +}; + +} +#endif /*SNAPPREFERENCES_H_*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/snap.cpp b/src/snap.cpp new file mode 100644 index 0000000..acc6008 --- /dev/null +++ b/src/snap.cpp @@ -0,0 +1,801 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SnapManager class. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * Nathan Hurst <njh@njhurst.com> + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2006-2007 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2004 Nathan Hurst + * Copyright (C) 1999-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> +#include <vector> + +#include <2geom/transforms.h> + +#include "snap.h" + +#include "desktop.h" +#include "inkscape.h" +#include "pure-transform.h" + +#include "display/canvas-grid.h" +#include "display/snap-indicator.h" + +#include "helper/mathfns.h" + +#include "object/sp-namedview.h" +#include "object/sp-guide.h" + +#include "ui/tools/tool-base.h" + +using Inkscape::Util::round_to_upper_multiple_plus; +using Inkscape::Util::round_to_lower_multiple_plus; + +SnapManager::SnapManager(SPNamedView const *v) : + guide(this, 0), + object(this, 0), + snapprefs(), + _named_view(v), + _rotation_center_source_items(std::vector<SPItem*>()), + _guide_to_ignore(nullptr), + _desktop(nullptr), + _snapindicator(true), + _unselected_nodes(nullptr) +{ +} + +SnapManager::SnapperList SnapManager::getSnappers() const +{ + SnapManager::SnapperList s; + s.push_back(&guide); + s.push_back(&object); + + SnapManager::SnapperList gs = getGridSnappers(); + s.splice(s.begin(), gs); + + return s; +} + +SnapManager::SnapperList SnapManager::getGridSnappers() const +{ + SnapperList s; + + if (_desktop && _desktop->gridsEnabled() && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID)) { + for(auto grid : _named_view->grids) { + s.push_back(grid->snapper); + } + } + + return s; +} + +bool SnapManager::someSnapperMightSnap(bool immediately) const +{ + if ( !snapprefs.getSnapEnabledGlobally() ) { + return false; + } + + // If we're asking if some snapper might snap RIGHT NOW (without the snap being postponed)... + if ( immediately && snapprefs.getSnapPostponedGlobally() ) { + return false; + } + + SnapperList const s = getSnappers(); + SnapperList::const_iterator i = s.begin(); + while (i != s.end() && (*i)->ThisSnapperMightSnap() == false) { + ++i; + } + + return (i != s.end()); +} + +bool SnapManager::gridSnapperMightSnap() const +{ + if ( !snapprefs.getSnapEnabledGlobally() || snapprefs.getSnapPostponedGlobally() ) { + return false; + } + + SnapperList const s = getGridSnappers(); + SnapperList::const_iterator i = s.begin(); + while (i != s.end() && (*i)->ThisSnapperMightSnap() == false) { + ++i; + } + + return (i != s.end()); +} + +void SnapManager::freeSnapReturnByRef(Geom::Point &p, + Inkscape::SnapSourceType const source_type, + Geom::OptRect const &bbox_to_snap) const +{ + Inkscape::SnappedPoint const s = freeSnap(Inkscape::SnapCandidatePoint(p, source_type, Inkscape::SNAPTARGET_PATH), bbox_to_snap); + s.getPointIfSnapped(p); +} + +Inkscape::SnappedPoint SnapManager::freeSnap(Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap, + bool to_paths_only) const +{ + if (!someSnapperMightSnap()) { + return Inkscape::SnappedPoint(p, Inkscape::SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false); + } + + IntermSnapResults isr; + SnapperList const snappers = getSnappers(); + + for (auto snapper : snappers) { + snapper->freeSnap(isr, p, bbox_to_snap, &_items_to_ignore, _unselected_nodes); + } + + return findBestSnap(p, isr, false, false, to_paths_only); +} + +void SnapManager::preSnap(Inkscape::SnapCandidatePoint const &p, bool to_paths_only) +{ + // setup() must have been called before calling this method! + + if (_snapindicator) { + _snapindicator = false; // prevent other methods from drawing a snap indicator; we want to control this here + Inkscape::SnappedPoint s = freeSnap(p, Geom::OptRect(), to_paths_only); + g_assert(_desktop != nullptr); + if (s.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(s, true); + } else { + _desktop->snapindicator->remove_snaptarget(true); + } + _snapindicator = true; // restore the original value + } +} + +Geom::Point SnapManager::multipleOfGridPitch(Geom::Point const &t, Geom::Point const &origin) +{ + if (!snapprefs.getSnapEnabledGlobally() || snapprefs.getSnapPostponedGlobally()) + return t; + + if (_desktop && _desktop->gridsEnabled()) { + bool success = false; + Geom::Point nearest_multiple; + Geom::Coord nearest_distance = Geom::infinity(); + Inkscape::SnappedPoint bestSnappedPoint(t); + + // It will snap to the grid for which we find the closest snap. This might be a different + // grid than to which the objects were initially aligned. I don't see an easy way to fix + // this, so when using multiple grids one can get unexpected results + + // Cannot use getGridSnappers() because we need both the grids AND their snappers + // Therefore we iterate through all grids manually + for (auto grid : _named_view->grids) { + const Inkscape::Snapper* snapper = grid->snapper; + if (snapper && snapper->ThisSnapperMightSnap()) { + // To find the nearest multiple of the grid pitch for a given translation t, we + // will use the grid snapper. Simply snapping the value t to the grid will do, but + // only if the origin of the grid is at (0,0). If it's not then compensate for this + // in the translation t + Geom::Point const t_offset = t + grid->origin; + IntermSnapResults isr; + // Only the first three parameters are being used for grid snappers + snapper->freeSnap(isr, Inkscape::SnapCandidatePoint(t_offset, Inkscape::SNAPSOURCE_GRID_PITCH),Geom::OptRect(), nullptr, nullptr); + // Find the best snap for this grid, including intersections of the grid-lines + bool old_val = _snapindicator; + _snapindicator = false; + Inkscape::SnappedPoint s = findBestSnap(Inkscape::SnapCandidatePoint(t_offset, Inkscape::SNAPSOURCE_GRID_PITCH), isr, false, true); + _snapindicator = old_val; + if (s.getSnapped() && (s.getSnapDistance() < nearest_distance)) { + // use getSnapDistance() instead of getWeightedDistance() here because the pointer's position + // doesn't tell us anything about which node to snap + success = true; + nearest_multiple = s.getPoint() - grid->origin; + nearest_distance = s.getSnapDistance(); + bestSnappedPoint = s; + } + } + } + + if (success) { + bestSnappedPoint.setPoint(origin + nearest_multiple); + _desktop->snapindicator->set_new_snaptarget(bestSnappedPoint); + return nearest_multiple; + } + } + + return t; +} + +void SnapManager::constrainedSnapReturnByRef(Geom::Point &p, + Inkscape::SnapSourceType const source_type, + Inkscape::Snapper::SnapConstraint const &constraint, + Geom::OptRect const &bbox_to_snap) const +{ + Inkscape::SnappedPoint const s = constrainedSnap(Inkscape::SnapCandidatePoint(p, source_type), constraint, bbox_to_snap); + p = s.getPoint(); // If we didn't snap, then we will return the point projected onto the constraint +} + +Inkscape::SnappedPoint SnapManager::constrainedSnap(Inkscape::SnapCandidatePoint const &p, + Inkscape::Snapper::SnapConstraint const &constraint, + Geom::OptRect const &bbox_to_snap) const +{ + // First project the mouse pointer onto the constraint + Geom::Point pp = constraint.projection(p.getPoint()); + + Inkscape::SnappedPoint no_snap = Inkscape::SnappedPoint(pp, p.getSourceType(), p.getSourceNum(), Inkscape::SNAPTARGET_CONSTRAINT, Geom::infinity(), 0, false, true, false); + + if (!someSnapperMightSnap()) { + // Always return point on constraint + return no_snap; + } + + Inkscape::SnappedPoint result = no_snap; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if ((prefs->getBool("/options/snapmousepointer/value", false)) && p.isSingleHandle()) { + // Snapping the mouse pointer instead of the constrained position of the knot allows + // to snap to things which don't intersect with the constraint line; this is basically + // then just a freesnap with the constraint applied afterwards + // We'll only do this if we're dragging a single handle, and for example not when transforming an object in the selector tool + result = freeSnap(p, bbox_to_snap); + if (result.getSnapped()) { + // only change the snap indicator if we really snapped to something + if (_snapindicator && _desktop) { + _desktop->snapindicator->set_new_snaptarget(result); + } + // Apply the constraint + result.setPoint(constraint.projection(result.getPoint())); + return result; + } + return no_snap; + } + + IntermSnapResults isr; + SnapperList const snappers = getSnappers(); + for (auto snapper : snappers) { + snapper->constrainedSnap(isr, p, bbox_to_snap, constraint, &_items_to_ignore, _unselected_nodes); + } + + result = findBestSnap(p, isr, true); + + + if (result.getSnapped()) { + // only change the snap indicator if we really snapped to something + if (_snapindicator && _desktop) { + _desktop->snapindicator->set_new_snaptarget(result); + } + return result; + } + return no_snap; +} + +/* See the documentation for constrainedSnap() directly above for more details. + * The difference is that multipleConstrainedSnaps() will take a list of constraints instead of a single one, + * and will try to snap the SnapCandidatePoint to only the closest constraint + * \param p Source point to be snapped + * \param constraints List of directions or lines along which snapping must occur + * \param dont_snap If true then we will only apply the constraint, without snapping + * \param bbox_to_snap Bounding box hulling the set of points, all from the same selection and having the same transformation + */ + + +Inkscape::SnappedPoint SnapManager::multipleConstrainedSnaps(Inkscape::SnapCandidatePoint const &p, + std::vector<Inkscape::Snapper::SnapConstraint> const &constraints, + bool dont_snap, + Geom::OptRect const &bbox_to_snap) const +{ + + Inkscape::SnappedPoint no_snap = Inkscape::SnappedPoint(p.getPoint(), p.getSourceType(), p.getSourceNum(), Inkscape::SNAPTARGET_CONSTRAINT, Geom::infinity(), 0, false, true, false); + if (constraints.size() == 0) { + return no_snap; + } + + // We haven't tried to snap yet; we will first determine which constraint is closest to where we are now, + // i.e. lets find out which of the constraints yields the closest projection of point p + + // Project the mouse pointer on each of the constraints + std::vector<Geom::Point> projections; + for (const auto & constraint : constraints) { + // Project the mouse pointer onto the constraint; In case we don't snap then we will + // return the projection onto the constraint, such that the constraint is always enforced + Geom::Point pp = constraint.projection(p.getPoint()); + projections.push_back(pp); + } + + // Select the closest constraint + no_snap.setPoint(projections.front()); + Inkscape::Snapper::SnapConstraint cc = constraints.front(); //closest constraint + + std::vector<Inkscape::Snapper::SnapConstraint>::const_iterator c = constraints.begin(); + std::vector<Geom::Point>::iterator pp = projections.begin(); + for (; pp != projections.end(); ++pp) { + if (Geom::L2(*pp - p.getPoint()) < Geom::L2(no_snap.getPoint() - p.getPoint())) { + no_snap.setPoint(*pp); // Remember the projection onto the closest constraint + cc = *c; // Remember the closest constraint itself + } + ++c; + } + + if (!someSnapperMightSnap() || dont_snap) { + return no_snap; + } + + IntermSnapResults isr; + SnapperList const snappers = getSnappers(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool snap_mouse = prefs->getBool("/options/snapmousepointer/value", false); + + Inkscape::SnappedPoint result = no_snap; + if (snap_mouse && p.isSingleHandle()) { + // Snapping the mouse pointer instead of the constrained position of the knot allows + // to snap to things which don't intersect with the constraint line; this is basically + // then just a freesnap with the constraint applied afterwards + // We'll only to this if we're dragging a single handle, and for example not when transforming an object in the selector tool + result = freeSnap(p, bbox_to_snap); + // Now apply the constraint afterwards + result.setPoint(cc.projection(result.getPoint())); + } else { + // Try to snap along the closest constraint + for (auto snapper : snappers) { + snapper->constrainedSnap(isr, p, bbox_to_snap, cc, &_items_to_ignore,_unselected_nodes); + } + result = findBestSnap(p, isr, true); + } + + return result.getSnapped() ? result : no_snap; +} + +Inkscape::SnappedPoint SnapManager::constrainedAngularSnap(Inkscape::SnapCandidatePoint const &p, + boost::optional<Geom::Point> const &p_ref, + Geom::Point const &o, + unsigned const snaps) const +{ + Inkscape::SnappedPoint sp; + if (snaps > 0) { // 0 means no angular snapping + // p is at an arbitrary angle. Now we should snap this angle to specific increments. + // For this we'll calculate the closest two angles, one at each side of the current angle + Geom::Line y_axis(Geom::Point(0, 0), Geom::Point(0, 1)); + Geom::Line p_line(o, p.getPoint()); + double angle = Geom::angle_between(y_axis, p_line); + double angle_incr = M_PI / snaps; + double angle_offset = 0; + if (p_ref) { + Geom::Line p_line_ref(o, *p_ref); + angle_offset = Geom::angle_between(y_axis, p_line_ref); + } + double angle_ceil = round_to_upper_multiple_plus(angle, angle_incr, angle_offset); + double angle_floor = round_to_lower_multiple_plus(angle, angle_incr, angle_offset); + // We have two angles now. The constrained snapper will try each of them and return the closest + + // Now do the snapping... + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + constraints.emplace_back(Geom::Line(o, angle_ceil - M_PI/2)); + constraints.emplace_back(Geom::Line(o, angle_floor - M_PI/2)); + sp = multipleConstrainedSnaps(p, constraints); // Constraints will always be applied, even if we didn't snap + if (!sp.getSnapped()) { // If we haven't snapped then we only had the constraint applied; + sp.setTarget(Inkscape::SNAPTARGET_CONSTRAINED_ANGLE); + } + } else { + sp = freeSnap(p); + } + return sp; +} + +void SnapManager::guideFreeSnap(Geom::Point &p, Geom::Point &origin_or_vector, bool origin, bool freeze_angle) const +{ + if (freeze_angle && origin) { + g_warning("Dear developer, when snapping guides you shouldn't ask me to freeze the guide's vector when you haven't specified one"); + // You've supplied me with an origin instead of a vector + } + + if (!snapprefs.getSnapEnabledGlobally() || snapprefs.getSnapPostponedGlobally() || !snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GUIDE)) { + return; + } + + Inkscape::SnapCandidatePoint candidate(p, Inkscape::SNAPSOURCE_GUIDE); + if (origin) { + candidate.addOrigin(origin_or_vector); + } else { + candidate = Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_GUIDE_ORIGIN); + candidate.addVector(Geom::rot90(origin_or_vector)); + } + + IntermSnapResults isr; + SnapperList snappers = getSnappers(); + for (SnapperList::const_iterator i = snappers.begin(); i != snappers.end(); ++i) { + (*i)->freeSnap(isr, candidate, Geom::OptRect(), nullptr, nullptr); + } + + Inkscape::SnappedPoint const s = findBestSnap(candidate, isr, false); + + s.getPointIfSnapped(p); + + if (!freeze_angle && s.getSnapped()) { + if (!Geom::are_near(s.getTangent(), Geom::Point(0,0))) { // If the tangent has been set ... + origin_or_vector = Geom::rot90(s.getTangent()); // then use it to update the normal of the guide + // PS: The tangent might not have been set if we snapped for example to a node + } + } +} + +void SnapManager::guideConstrainedSnap(Geom::Point &p, SPGuide const &guideline) const +{ + if (!snapprefs.getSnapEnabledGlobally() || snapprefs.getSnapPostponedGlobally() || !snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GUIDE)) { + return; + } + + Inkscape::SnapCandidatePoint candidate(p, Inkscape::SNAPSOURCE_GUIDE_ORIGIN, Inkscape::SNAPTARGET_UNDEFINED); + + IntermSnapResults isr; + Inkscape::Snapper::SnapConstraint cl(guideline.getPoint(), Geom::rot90(guideline.getNormal())); + + SnapperList snappers = getSnappers(); + for (SnapperList::const_iterator i = snappers.begin(); i != snappers.end(); ++i) { + (*i)->constrainedSnap(isr, candidate, Geom::OptRect(), cl, nullptr, nullptr); + } + + Inkscape::SnappedPoint const s = findBestSnap(candidate, isr, false); + s.getPointIfSnapped(p); +} + +void SnapManager::snapTransformed( + std::vector<Inkscape::SnapCandidatePoint> const &points, + Geom::Point const &pointer, + Inkscape::PureTransform &transform + ) +{ + /* We have a list of points, which we are proposing to transform in some way. We need to see + ** if any of these points, when transformed, snap to anything. If they do, we return the + ** appropriate transformation with `true'; otherwise we return the original scale with `false'. + */ + + if (points.size() == 0) { + transform.best_snapped_point = Inkscape::SnappedPoint(pointer); + return; + } + + // We will try to snap a set of points, but we don't want to have a snap indicator displayed + // for each of them. That's why it's temporarily disabled here, and re-enabled again after we + // have finished calling the freeSnap() and constrainedSnap() methods + bool _orig_snapindicator_status = _snapindicator; + _snapindicator = false; + + transform.snap(this, points, pointer); + + // Allow the snapindicator to be displayed again + _snapindicator = _orig_snapindicator_status; + + if (_snapindicator) { + if (transform.best_snapped_point.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(transform.best_snapped_point); + } else { + _desktop->snapindicator->remove_snaptarget(); + } + } + + if (points.size() == 1) { + displaySnapsource(Inkscape::SnapCandidatePoint(transform.best_snapped_point.getPoint(), points.at(0).getSourceType())); + } +} + +Inkscape::SnappedPoint SnapManager::findBestSnap(Inkscape::SnapCandidatePoint const &p, + IntermSnapResults const &isr, + bool constrained, + bool allowOffScreen, + bool to_path_only) const +{ + g_assert(_desktop != nullptr); + + /* + std::cout << "Type and number of snapped constraints: " << std::endl; + std::cout << " Points : " << isr.points.size() << std::endl; + std::cout << " Grid lines : " << isr.grid_lines.size()<< std::endl; + std::cout << " Guide lines : " << isr.guide_lines.size()<< std::endl; + std::cout << " Curves : " << isr.curves.size()<< std::endl; + */ + + /* + // Display all snap candidates on the canvas + _desktop->snapindicator->remove_debugging_points(); + for (std::list<Inkscape::SnappedPoint>::const_iterator i = isr.points.begin(); i != isr.points.end(); i++) { + _desktop->snapindicator->set_new_debugging_point((*i).getPoint()); + } + for (std::list<Inkscape::SnappedCurve>::const_iterator i = isr.curves.begin(); i != isr.curves.end(); i++) { + _desktop->snapindicator->set_new_debugging_point((*i).getPoint()); + } + for (std::list<Inkscape::SnappedLine>::const_iterator i = isr.grid_lines.begin(); i != isr.grid_lines.end(); i++) { + _desktop->snapindicator->set_new_debugging_point((*i).getPoint()); + } + for (std::list<Inkscape::SnappedLine>::const_iterator i = isr.guide_lines.begin(); i != isr.guide_lines.end(); i++) { + _desktop->snapindicator->set_new_debugging_point((*i).getPoint()); + } + */ + + // Store all snappoints + std::list<Inkscape::SnappedPoint> sp_list; + + // search for the closest snapped point + Inkscape::SnappedPoint closestPoint; + if (getClosestSP(isr.points, closestPoint)) { + sp_list.push_back(closestPoint); + } + + // search for the closest snapped curve + Inkscape::SnappedCurve closestCurve; + // We might have collected the paths only to snap to their intersection, without the intention to snap to the paths themselves + // Therefore we explicitly check whether the paths should be considered as snap targets themselves + bool exclude_paths = !snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH); + if (getClosestCurve(isr.curves, closestCurve, exclude_paths)) { + sp_list.emplace_back(closestCurve); + } + + // search for the closest snapped grid line + Inkscape::SnappedLine closestGridLine; + if (getClosestSL(isr.grid_lines, closestGridLine)) { + sp_list.emplace_back(closestGridLine); + } + + // search for the closest snapped guide line + Inkscape::SnappedLine closestGuideLine; + if (getClosestSL(isr.guide_lines, closestGuideLine)) { + sp_list.emplace_back(closestGuideLine); + } + + // When freely snapping to a grid/guide/path, only one degree of freedom is eliminated + // Therefore we will try get fully constrained by finding an intersection with another grid/guide/path + + // When doing a constrained snap however, we're already at an intersection of the constrained line and + // the grid/guide/path we're snapping to. This snappoint is therefore fully constrained, so there's + // no need to look for additional intersections + if (!constrained) { + if (snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_INTERSECTION)) { + // search for the closest snapped intersection of curves + Inkscape::SnappedPoint closestCurvesIntersection; + if (getClosestIntersectionCS(isr.curves, p.getPoint(), closestCurvesIntersection, _desktop->dt2doc())) { + closestCurvesIntersection.setSource(p.getSourceType()); + sp_list.push_back(closestCurvesIntersection); + } + } + + if (snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION)) { + // search for the closest snapped intersection of a guide with a curve + Inkscape::SnappedPoint closestCurveGuideIntersection; + if (getClosestIntersectionCL(isr.curves, isr.guide_lines, p.getPoint(), closestCurveGuideIntersection, _desktop->dt2doc())) { + closestCurveGuideIntersection.setSource(p.getSourceType()); + sp_list.push_back(closestCurveGuideIntersection); + } + } + + // search for the closest snapped intersection of grid lines + Inkscape::SnappedPoint closestGridPoint; + if (getClosestIntersectionSL(isr.grid_lines, closestGridPoint)) { + closestGridPoint.setSource(p.getSourceType()); + closestGridPoint.setTarget(Inkscape::SNAPTARGET_GRID_INTERSECTION); + sp_list.push_back(closestGridPoint); + } + + // search for the closest snapped intersection of guide lines + Inkscape::SnappedPoint closestGuidePoint; + if (getClosestIntersectionSL(isr.guide_lines, closestGuidePoint)) { + closestGuidePoint.setSource(p.getSourceType()); + closestGuidePoint.setTarget(Inkscape::SNAPTARGET_GUIDE_INTERSECTION); + sp_list.push_back(closestGuidePoint); + } + + // search for the closest snapped intersection of grid with guide lines + if (snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID_GUIDE_INTERSECTION)) { + Inkscape::SnappedPoint closestGridGuidePoint; + if (getClosestIntersectionSL(isr.grid_lines, isr.guide_lines, closestGridGuidePoint)) { + closestGridGuidePoint.setSource(p.getSourceType()); + closestGridGuidePoint.setTarget(Inkscape::SNAPTARGET_GRID_GUIDE_INTERSECTION); + sp_list.push_back(closestGridGuidePoint); + } + } + } + + // Filter out all snap targets that do NOT include a path; this is useful when we try to insert + // a node in a path (on doubleclick in the node tool). We don't want to change the shape of the + // path, so the snapped point must be on a path, and not e.g. on a grid intersection + if (to_path_only) { + std::list<Inkscape::SnappedPoint>::iterator i = sp_list.begin(); + + while (i != sp_list.end()) { + Inkscape::SnapTargetType t = (*i).getTarget(); + if (t == Inkscape::SNAPTARGET_LINE_MIDPOINT || + t == Inkscape::SNAPTARGET_PATH || + t == Inkscape::SNAPTARGET_PATH_PERPENDICULAR || + t == Inkscape::SNAPTARGET_PATH_TANGENTIAL || + t == Inkscape::SNAPTARGET_PATH_INTERSECTION || + t == Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION || + t == Inkscape::SNAPTARGET_PATH_CLIP || + t == Inkscape::SNAPTARGET_PATH_MASK || + t == Inkscape::SNAPTARGET_ELLIPSE_QUADRANT_POINT) { + ++i; + } else { + i = sp_list.erase(i); + } + } + } + + // now let's see which snapped point gets a thumbs up + Inkscape::SnappedPoint bestSnappedPoint(p.getPoint()); + // std::cout << "Finding the best snap..." << std::endl; + for (std::list<Inkscape::SnappedPoint>::const_iterator i = sp_list.begin(); i != sp_list.end(); ++i) { + // std::cout << "sp = " << (*i).getPoint() << " | source = " << (*i).getSource() << " | target = " << (*i).getTarget(); + bool onScreen = _desktop->get_display_area().contains((*i).getPoint()); + if (onScreen || allowOffScreen) { // Only snap to points which are not off the screen + if ((*i).getSnapDistance() <= (*i).getTolerance()) { // Only snap to points within snapping range + // if it's the first point, or if it is closer than the best snapped point so far + if (i == sp_list.begin() || bestSnappedPoint.isOtherSnapBetter(*i, false)) { + // then prefer this point over the previous one + bestSnappedPoint = *i; + } + } + } + // std::cout << std::endl; + } + + // Update the snap indicator, if requested + if (_snapindicator) { + if (bestSnappedPoint.getSnapped()) { + _desktop->snapindicator->set_new_snaptarget(bestSnappedPoint); + } else { + _desktop->snapindicator->remove_snaptarget(); + } + } + + // std::cout << "findBestSnap = " << bestSnappedPoint.getPoint() << " | dist = " << bestSnappedPoint.getSnapDistance() << std::endl; + return bestSnappedPoint; +} + +void SnapManager::setup(SPDesktop const *desktop, + bool snapindicator, + SPItem const *item_to_ignore, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes, + SPGuide *guide_to_ignore) +{ + g_assert(desktop != nullptr); + if (_desktop != nullptr) { + g_warning("The snapmanager has been set up before, but unSetup() hasn't been called afterwards. It possibly held invalid pointers"); + } + _items_to_ignore.clear(); + if (item_to_ignore) { + _items_to_ignore.push_back(item_to_ignore); + } + _desktop = desktop; + _snapindicator = snapindicator; + _unselected_nodes = unselected_nodes; + _guide_to_ignore = guide_to_ignore; + _rotation_center_source_items.clear(); +} + +void SnapManager::setup(SPDesktop const *desktop, + bool snapindicator, + std::vector<SPItem const *> &items_to_ignore, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes, + SPGuide *guide_to_ignore) +{ + g_assert(desktop != nullptr); + if (_desktop != nullptr) { + g_warning("The snapmanager has been set up before, but unSetup() hasn't been called afterwards. It possibly held invalid pointers"); + } + _items_to_ignore = items_to_ignore; + _desktop = desktop; + _snapindicator = snapindicator; + _unselected_nodes = unselected_nodes; + _guide_to_ignore = guide_to_ignore; + _rotation_center_source_items.clear(); +} + +/// Setup, taking the list of items to ignore from the desktop's selection. +void SnapManager::setupIgnoreSelection(SPDesktop const *desktop, + bool snapindicator, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes, + SPGuide *guide_to_ignore) +{ + g_assert(desktop != nullptr); + if (_desktop != nullptr) { + // Someone has been naughty here! This is dangerous + g_warning("The snapmanager has been set up before, but unSetup() hasn't been called afterwards. It possibly held invalid pointers"); + } + _desktop = desktop; + _snapindicator = snapindicator; + _unselected_nodes = unselected_nodes; + _guide_to_ignore = guide_to_ignore; + _rotation_center_source_items.clear(); + _items_to_ignore.clear(); + + Inkscape::Selection *sel = _desktop->selection; + auto items = sel->items(); + for (auto i=items.begin();i!=items.end();++i) { + _items_to_ignore.push_back(*i); + } +} + +SPDocument *SnapManager::getDocument() const +{ + return _named_view->document; +} + +//Geom::Point SnapManager::_transformPoint(Inkscape::SnapCandidatePoint const &p, +// Transformation const transformation_type, +// Geom::Point const &transformation, +// Geom::Point const &origin, +// Geom::Dim2 const dim, +// bool const uniform) const +//{ +// /* Work out the transformed version of this point */ +// Geom::Point transformed; +// switch (transformation_type) { +// case TRANSLATE: +// transformed = p.getPoint() + transformation; +// break; +// case SCALE: +// transformed = (p.getPoint() - origin) * Geom::Scale(transformation[Geom::X], transformation[Geom::Y]) + origin; +// break; +// case STRETCH: +// { +// Geom::Scale s(1, 1); +// if (uniform) +// s[Geom::X] = s[Geom::Y] = transformation[dim]; +// else { +// s[dim] = transformation[dim]; +// s[1 - dim] = 1; +// } +// transformed = ((p.getPoint() - origin) * s) + origin; +// break; +// } +// case SKEW: +// // Apply the skew factor +// transformed[dim] = (p.getPoint())[dim] + transformation[0] * ((p.getPoint())[1 - dim] - origin[1 - dim]); +// // While skewing, mirroring and scaling (by integer multiples) in the opposite direction is also allowed. +// // Apply that scale factor here +// transformed[1-dim] = (p.getPoint() - origin)[1 - dim] * transformation[1] + origin[1 - dim]; +// break; +// case ROTATE: +// // for rotations: transformation[0] stores the angle in radians +// transformed = (p.getPoint() - origin) * Geom::Rotate(transformation[0]) + origin; +// break; +// default: +// g_assert_not_reached(); +// } +// +// return transformed; +//} + +/** + * Mark the location of the snap source (not the snap target!) on the canvas by drawing a symbol. + * + * @param point_type Category of points to which the source point belongs: node, guide or bounding box + * @param p The transformed position of the source point, paired with an identifier of the type of the snap source. + */ +void SnapManager::displaySnapsource(Inkscape::SnapCandidatePoint const &p) const { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value")) { + Inkscape::SnapSourceType t = p.getSourceType(); + bool p_is_a_node = t & Inkscape::SNAPSOURCE_NODE_CATEGORY; + bool p_is_a_bbox = t & Inkscape::SNAPSOURCE_BBOX_CATEGORY; + bool p_is_other = (t & Inkscape::SNAPSOURCE_OTHERS_CATEGORY) || (t & Inkscape::SNAPSOURCE_DATUMS_CATEGORY); + + g_assert(_desktop != nullptr); + if (snapprefs.getSnapEnabledGlobally() && (p_is_other || (p_is_a_node && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_NODE_CATEGORY)) || (p_is_a_bbox && snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY)))) { + _desktop->snapindicator->set_new_snapsource(p); + } else { + _desktop->snapindicator->remove_snapsource(); + } + } +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/snap.h b/src/snap.h new file mode 100644 index 0000000..886bfca --- /dev/null +++ b/src/snap.h @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Per-desktop object that handles snapping queries. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Copyright (C) 2006-2007 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2000-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SNAP_H +#define SEEN_SNAP_H + +#include <vector> + +#include "guide-snapper.h" +#include "object-snapper.h" +#include "snap-preferences.h" + + +// Guides +enum SPGuideDragType { // used both here and in desktop-events.cpp + SP_DRAG_TRANSLATE, + SP_DRAG_ROTATE, + SP_DRAG_MOVE_ORIGIN, + SP_DRAG_NONE +}; + +class SPDocument; +class SPGuide; +class SPNamedView; + +namespace Inkscape { + class PureTransform; +} + + +/** + * Class to coordinate snapping operations. + * + * The SnapManager class handles most (if not all) of the interfacing of the snapping mechanisms + * with the other parts of the code base. It stores the references to the various types of snappers + * for grid, guides and objects, and it stores most of the snapping preferences. Besides that + * it provides methods to setup the snapping environment (e.g. keeps a list of the items to ignore + * when looking for snap target candidates, and toggling of the snap indicator), and it provides + * many different methods for snapping queries (free snapping vs. constrained snapping, + * returning the result by reference or through a return statement, etc.) + * + * Each SPNamedView has one of these. It offers methods to snap points to whatever + * snappers are defined (e.g. grid, guides etc.). It also allows callers to snap + * points which have undergone some transformation (e.g. translation, scaling etc.) + * + * \par How snapping is implemented in Inkscape + * \par + * The snapping system consists of two key elements. The first one is the snap manager + * (this class), which keeps some data about objects in the document and answers queries + * of the type "given this point and type of transformation, what is the best place + * to snap to?". + * + * The second is in event-context.cpp and implements the snapping timeout. Whenever a motion + * events happens over the canvas, it stores it for later use and initiates a timeout. + * This timeout is discarded whenever a new motion event occurs. When the timeout expires, + * a global flag in SnapManager, accessed via getSnapPostponedGlobally(), is set to true + * and the stored event is replayed, but this time with snapping enabled. This way you can + * write snapping code directly in your control point's dragged handler as if there was + * no timeout. + */ +class SnapManager +{ +public: + enum Transformation { + TRANSLATE, + SCALE, + STRETCH, + SKEW, + ROTATE + }; + + /** + * Construct a SnapManager for a SPNamedView. + * + * @param v 'Owning' SPNamedView. + */ + SnapManager(SPNamedView const *v); + + typedef std::list<const Inkscape::Snapper*> SnapperList; + + /** + * Return true if any snapping might occur, whether its to grids, guides or objects. + * + * Each snapper instance handles its own snapping target, e.g. grids, guides or + * objects. This method iterates through all these snapper instances and returns + * true if any of the snappers might possible snap, considering only the relevant + * snapping preferences. + * + * @return true if one of the snappers will try to snap to something. + */ + bool someSnapperMightSnap(bool immediately = true) const; + + /** + * @return true if one of the grids might be snapped to. + */ + bool gridSnapperMightSnap() const; + + /** + * Convenience shortcut when there is only one item to ignore. + */ + void setup(SPDesktop const *desktop, + bool snapindicator = true, + SPItem const *item_to_ignore = nullptr, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes = nullptr, + SPGuide *guide_to_ignore = nullptr); + + /** + * Prepare the snap manager for the actual snapping, which includes building a list of snap targets + * to ignore and toggling the snap indicator. + * + * There are two overloaded setup() methods, of which the other one only allows for a single item to be ignored + * whereas this one will take a list of items to ignore + * + * @param desktop Reference to the desktop to which this snap manager is attached. + * @param snapindicator If true then a snap indicator will be displayed automatically (when enabled in the preferences). + * @param items_to_ignore These items will not be snapped to, e.g. the items that are currently being dragged. This avoids "self-snapping". + * @param unselected_nodes Stationary nodes of the path that is currently being edited in the node tool and + * that can be snapped too. Nodes not in this list will not be snapped to, to avoid "self-snapping". Of each + * unselected node both the position (Geom::Point) and the type (Inkscape::SnapTargetType) will be stored. + * @param guide_to_ignore Guide that is currently being dragged and should not be snapped to. + */ + void setup(SPDesktop const *desktop, + bool snapindicator, + std::vector<SPItem const *> &items_to_ignore, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes = nullptr, + SPGuide *guide_to_ignore = nullptr); + + void setupIgnoreSelection(SPDesktop const *desktop, + bool snapindicator = true, + std::vector<Inkscape::SnapCandidatePoint> *unselected_nodes = nullptr, + SPGuide *guide_to_ignore = nullptr); + + void unSetup() {_rotation_center_source_items.clear(); + _guide_to_ignore = nullptr; + _desktop = nullptr; + _unselected_nodes = nullptr;} + + // If we're dragging a rotation center, then setRotationCenterSource() stores the parent item + // of this rotation center; this reference is used to make sure that we do not snap a rotation + // center to itself + // NOTE: Must be called after calling setup(), not before! + void setRotationCenterSource(const std::vector<SPItem*> &items) {_rotation_center_source_items = items;} + const std::vector<SPItem*> &getRotationCenterSource() {return _rotation_center_source_items;} + + // freeSnapReturnByRef() is preferred over freeSnap(), because it only returns a + // point if snapping has occurred (by overwriting p); otherwise p is untouched + + /** + * Try to snap a point to grids, guides or objects. + * + * Try to snap a point to grids, guides or objects, in two degrees-of-freedom, + * i.e. snap in any direction on the two dimensional canvas to the nearest + * snap target. freeSnapReturnByRef() is equal in snapping behavior to + * freeSnap(), but the former returns the snapped point trough the referenced + * parameter p. This parameter p initially contains the position of the snap + * source and will we overwritten by the target position if snapping has occurred. + * This makes snapping transparent to the calling code. If this is not desired + * because either the calling code must know whether snapping has occurred, or + * because the original position should not be touched, then freeSnap() should be + * called instead. + * + * PS: + * 1) SnapManager::setup() must have been called before calling this method, + * although only once for each set of points + * 2) Only to be used when a single source point is to be snapped; it assumes + * that source_num = 0, which is inefficient when snapping sets our source points + * + * @param p Current position of the snap source; will be overwritten by the position of the snap target if snapping has occurred. + * @param source_type Detailed description of the source type, will be used by the snap indicator. + * @param bbox_to_snap Bounding box hulling the set of points, all from the same selection and having the same transformation. + */ + void freeSnapReturnByRef(Geom::Point &p, + Inkscape::SnapSourceType const source_type, + Geom::OptRect const &bbox_to_snap = Geom::OptRect()) const; + + /** + * Try to snap a point to grids, guides or objects. + * + * Try to snap a point to grids, guides or objects, in two degrees-of-freedom, + * i.e. snap in any direction on the two dimensional canvas to the nearest + * snap target. freeSnap() is equal in snapping behavior to + * freeSnapReturnByRef(). Please read the comments of the latter for more details + * + * PS: SnapManager::setup() must have been called before calling this method, + * although only once for each set of points + * + * @param p Source point to be snapped. + * @param bbox_to_snap Bounding box hulling the set of points, all from the same selection and having the same transformation. + * @param to_path_only Only snap to points on a path, such as path intersections with itself or with grids/guides. This is used for + * example when adding nodes to a path. We will not snap for example to grid intersections + * @return An instance of the SnappedPoint class, which holds data on the snap source, snap target, and various metrics. + */ + Inkscape::SnappedPoint freeSnap(Inkscape::SnapCandidatePoint const &p, + Geom::OptRect const &bbox_to_snap = Geom::OptRect(), + bool to_path_only = false) const; + + void preSnap(Inkscape::SnapCandidatePoint const &p, bool to_path_only = false); + + /** + * Snap to the closest multiple of a grid pitch. + * + * When pasting, we would like to snap to the grid. Problem is that we don't know which + * nodes were aligned to the grid at the time of copying, so we don't know which nodes + * to snap. If we'd snap an unaligned node to the grid, previously aligned nodes would + * become unaligned. That's undesirable. Instead we will make sure that the offset + * between the source and its pasted copy is a multiple of the grid pitch. If the source + * was aligned, then the copy will therefore also be aligned. + * + * PS: Whether we really find a multiple also depends on the snapping range! Most users + * will have "always snap" enabled though, in which case a multiple will always be found. + * PS2: When multiple grids are present then the result will become ambiguous. There is no + * way to control to which grid this method will snap. + * + * @param t Vector that represents the offset of the pasted copy with respect to the original. + * @return Offset vector after snapping to the closest multiple of a grid pitch. + */ + Geom::Point multipleOfGridPitch(Geom::Point const &t, Geom::Point const &origin); + + // constrainedSnapReturnByRef() is preferred over constrainedSnap(), because it only returns a + // point, by overwriting p, if snapping has occurred; otherwise p is untouched + + /** + * Try to snap a point along a constraint line to grids, guides or objects. + * + * Try to snap a point to grids, guides or objects, in only one degree-of-freedom, + * i.e. snap in a specific direction on the two dimensional canvas to the nearest + * snap target. + * + * constrainedSnapReturnByRef() is equal in snapping behavior to + * constrainedSnap(), but the former returns the snapped point trough the referenced + * parameter p. This parameter p initially contains the position of the snap + * source and will be overwritten by the target position if snapping has occurred. + * This makes snapping transparent to the calling code. If this is not desired + * because either the calling code must know whether snapping has occurred, or + * because the original position should not be touched, then constrainedSnap() should + * be called instead. If there's nothing to snap to or if snapping has been disabled, + * then this method will still apply the constraint (but without snapping) + * + * PS: + * 1) SnapManager::setup() must have been called before calling this method, + * although only once for each set of points + * 2) Only to be used when a single source point is to be snapped; it assumes + * that source_num = 0, which is inefficient when snapping sets our source points + + * + * @param p Current position of the snap source; will be overwritten by the position of the snap target if snapping has occurred. + * @param source_type Detailed description of the source type, will be used by the snap indicator. + * @param constraint The direction or line along which snapping must occur. + * @param bbox_to_snap Bounding box hulling the set of points, all from the same selection and having the same transformation. + */ + void constrainedSnapReturnByRef(Geom::Point &p, + Inkscape::SnapSourceType const source_type, + Inkscape::Snapper::SnapConstraint const &constraint, + Geom::OptRect const &bbox_to_snap = Geom::OptRect()) const; + + /** + * Try to snap a point along a constraint line to grids, guides or objects. + * + * Try to snap a point to grids, guides or objects, in only one degree-of-freedom, + * i.e. snap in a specific direction on the two dimensional canvas to the nearest + * snap target. constrainedSnap is equal in snapping behavior to + * constrainedSnapReturnByRef(). Please read the comments of the latter for more details. + * + * PS: SnapManager::setup() must have been called before calling this method, + * although only once for each set of points + * PS: If there's nothing to snap to or if snapping has been disabled, then this + * method will still apply the constraint (but without snapping) + * + * @param p Source point to be snapped. + * @param constraint The direction or line along which snapping must occur. + * @param bbox_to_snap Bounding box hulling the set of points, all from the same selection and having the same transformation. + */ + Inkscape::SnappedPoint constrainedSnap(Inkscape::SnapCandidatePoint const &p, + Inkscape::Snapper::SnapConstraint const &constraint, + Geom::OptRect const &bbox_to_snap = Geom::OptRect()) const; + + Inkscape::SnappedPoint multipleConstrainedSnaps(Inkscape::SnapCandidatePoint const &p, + std::vector<Inkscape::Snapper::SnapConstraint> const &constraints, + bool dont_snap = false, + Geom::OptRect const &bbox_to_snap = Geom::OptRect()) const; + + /** + * Try to snap a point to something at a specific angle. + * + * When drawing a straight line or modifying a gradient, it will snap to specific angle increments + * if CTRL is being pressed. This method will enforce this angular constraint (even if there is nothing + * to snap to) + * + * @param p Source point to be snapped. + * @param p_ref Optional original point, relative to which the angle should be calculated. If empty then + * the angle will be calculated relative to the y-axis. + * @param snaps Number of angular increments per PI radians; E.g. if snaps = 2 then we will snap every PI/2 = 90 degrees. + */ + Inkscape::SnappedPoint constrainedAngularSnap(Inkscape::SnapCandidatePoint const &p, + boost::optional<Geom::Point> const &p_ref, + Geom::Point const &o, + unsigned const snaps) const; + + /** + * Wrapper method to make snapping of the guide origin a bit easier (i.e. simplifies the calling code). + * + * PS: SnapManager::setup() must have been called before calling this method, + * + * @param p Current position of the point on the guide that is to be snapped; will be overwritten by the position of the snap target if snapping has occurred. + * @param origin_or_vector Data used for tangential and perpendicular snapping. When rotating a guide the origin of the rotation is specified here, whereas when + * dragging a guide its vector is specified here + * @param origin If true then origin_or_vector contains an origin, other it contains a vector + * @param freeze_angle If true (in which case origin is false), then the vector specified in origin_or_vector will not be touched, i.e. the guide will + * in all circumstances keep its angle. Otherwise the vector in origin_or_vector can be updated, meaning that the guide might take on the angle of a curve that + * has been snapped too tangentially or perpendicularly + */ + void guideFreeSnap(Geom::Point &p, Geom::Point &origin_or_vector, bool origin, bool freeze_angle) const; + + /** + * Wrapper method to make snapping of the guide origin a bit easier (i.e. simplifies the calling code). + * + * PS: SnapManager::setup() must have been called before calling this method, + * + * @param p Current position of the point on the guide that is to be snapped; will be overwritten by the position of the snap target if snapping has occurred. + * @param guideline The guide that is currently being dragged + */ + void guideConstrainedSnap(Geom::Point &p, SPGuide const &guideline) const; + + Inkscape::GuideSnapper guide; ///< guide snapper + Inkscape::ObjectSnapper object; ///< snapper to other objects + Inkscape::SnapPreferences snapprefs; + + /** + * Return a list of snappers. + * + * Inkscape snaps to objects, grids, and guides. For each of these snap targets a + * separate class is used, which has been derived from the base Snapper class. The + * getSnappers() method returns a list of pointers to instances of this class. This + * list contains exactly one instance of the guide snapper and of the object snapper + * class, but any number of grid snappers (because each grid has its own snapper + * instance) + * + * @return List of snappers that we use. + */ + SnapperList getSnappers() const; + + /** + * Return a list of gridsnappers. + * + * Each grid has its own instance of the snapper class. This way snapping can + * be enabled per grid individually. A list will be returned containing the + * pointers to these instances, but only for grids that are being displayed + * and for which snapping is enabled. + * + * @return List of gridsnappers that we use. + */ + SnapperList getGridSnappers() const; + + SPDesktop const *getDesktop() const {return _desktop;} + SPNamedView const *getNamedView() const {return _named_view;} + SPDocument *getDocument() const; + SPGuide const *getGuideToIgnore() const {return _guide_to_ignore;} + + bool getSnapIndicator() const {return _snapindicator;} + + /** + * Given a set of possible snap targets, find the best target (which is not necessarily + * also the nearest target), and show the snap indicator if requested. + * + * @param p Source point to be snapped. + * @param isr A structure holding all snap targets that have been found so far. + * @param constrained True if the snap is constrained, e.g. for stretching or for purely horizontal translation. + * @param allowOffScreen If true, then snapping to points which are off the screen is allowed (needed for example when pasting to the grid). + * @param to_path_only Only snap to points on a path, such as path intersections with itself or with grids/guides. This is used for + * example when adding nodes to a path. We will not snap for example to grid intersections + * @return An instance of the SnappedPoint class, which holds data on the snap source, snap target, and various metrics. + */ + Inkscape::SnappedPoint findBestSnap(Inkscape::SnapCandidatePoint const &p, IntermSnapResults const &isr, bool constrained, bool allowOffScreen = false, bool to_paths_only = false) const; + + /** + * Mark the location of the snap source (not the snap target!) on the canvas by drawing a symbol. + * + * @param point_type Category of points to which the source point belongs: node, guide or bounding box. + * @param p The transformed position of the source point, paired with an identifier of the type of the snap source. + */ + void displaySnapsource(Inkscape::SnapCandidatePoint const &p) const; + + /** + * Method for snapping sets of points while they are being transformed. + * + * Method for snapping sets of points while they are being transformed, when using + * for example the selector tool. This method is for internal use only, and should + * not have to be called directly. Use freeSnapTransalation(), constrainedSnapScale(), + * etc. instead. + * + * This is what is being done in this method: transform each point, find out whether + * a free snap or constrained snap is more appropriate, do the snapping, calculate + * some metrics to quantify the snap "distance", and see if it's better than the + * previous snap. Finally, the best ("nearest") snap from all these points is returned. + * If no snap has occurred and we're asked for a constrained snap then the constraint + * will be applied nevertheless + * + * @param points Collection of points to snap (snap sources), at their untransformed position, all points undergoing the same transformation. Paired with an identifier of the type of the snap source. + * @param pointer Location of the mouse pointer at the time dragging started (i.e. when the selection was still untransformed). + * @param transform Describes the type of transformation, it's parameters, and any additional constraints + */ + void snapTransformed(std::vector<Inkscape::SnapCandidatePoint> const &points, + Geom::Point const &pointer, + Inkscape::PureTransform &transform); + +protected: + SPNamedView const *_named_view; + +private: + std::vector<SPItem const *> _items_to_ignore; ///< Items that should not be snapped to, for example the items that are currently being dragged. Set using the setup() method + std::vector<SPItem*> _rotation_center_source_items; // to avoid snapping a rotation center to itself + SPGuide *_guide_to_ignore; ///< A guide that should not be snapped to, e.g. the guide that is currently being dragged + SPDesktop const *_desktop; + bool _snapindicator; ///< When true, an indicator will be drawn at the position that was being snapped to + std::vector<Inkscape::SnapCandidatePoint> *_unselected_nodes; ///< Nodes of the path that is currently being edited and which have not been selected and which will therefore be stationary. Only these nodes will be considered for snapping to. Of each unselected node both the position (Geom::Point) and the type (Inkscape::SnapTargetType) will be stored + +}; + +#endif // !SEEN_SNAP_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-curve.cpp b/src/snapped-curve.cpp new file mode 100644 index 0000000..70ad72e --- /dev/null +++ b/src/snapped-curve.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * \file src/snapped-curve.cpp + * SnappedCurve class. + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snapped-curve.h" +#include <2geom/path-intersection.h> + +Inkscape::SnappedCurve::SnappedCurve(Geom::Point const &snapped_point, Geom::Point const &tangent, int num_path, int num_segm, Geom::Coord const &snapped_distance, Geom::Coord const &snapped_tolerance, bool const &always_snap, bool const &fully_constrained, Geom::Curve const *curve, SnapSourceType source, long source_num, SnapTargetType target, Geom::OptRect target_bbox) +{ + _num_path = num_path; + _num_segm = num_segm; + _distance = snapped_distance; + _tolerance = std::max(snapped_tolerance, 1.0); + _always_snap = always_snap; + _curve = curve; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; + _point = snapped_point; + _tangent = tangent; + _at_intersection = false; + _fully_constrained = fully_constrained; + _source = source; + _source_num = source_num; + _target = target; + _target_bbox = target_bbox; +} + +Inkscape::SnappedCurve::SnappedCurve() +{ + _num_path = 0; + _num_segm = 0; + _distance = Geom::infinity(); + _tolerance = 1; + _always_snap = false; + _curve = nullptr; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; + _point = Geom::Point(0,0); + _tangent = Geom::Point(0,0); + _at_intersection = false; + _fully_constrained = false; + _source = SNAPSOURCE_UNDEFINED; + _source_num = -1; + _target = SNAPTARGET_UNDEFINED; + _target_bbox = Geom::OptRect(); +} + +Inkscape::SnappedCurve::~SnappedCurve() += default; + +Inkscape::SnappedPoint Inkscape::SnappedCurve::intersect(SnappedCurve const &curve, Geom::Point const &p, Geom::Affine dt2doc) const +{ + // Calculate the intersections of two curves, which are both within snapping range, and + // return only the closest intersection + // The point of intersection should be considered for snapping, but might be outside the snapping range + // PS: We need p (the location of the mouse pointer) to find out which intersection is the + // closest, as there might be multiple intersections of two curves + Geom::Crossings cs = crossings(*(this->_curve), *(curve._curve)); + + if (cs.size() > 0) { + // There might be multiple intersections: find the closest + Geom::Coord best_dist = Geom::infinity(); + Geom::Point best_p = Geom::Point(Geom::infinity(), Geom::infinity()); + for (const auto & c : cs) { + Geom::Point p_ix = this->_curve->pointAt(c.ta); + Geom::Coord dist = Geom::distance(p_ix, p); + + // Test if we have two segments (curves) from the same path.. + if (this->_num_path == curve._num_path) { + // Never try to intersect a segment with itself + if (this->_num_segm == curve._num_segm) continue; + // Two subsequent segments (curves) in a path will have a common node; this node is not considered to be an intersection + if (this->_num_segm == curve._num_segm + 1 && c.ta == 0 && c.tb == 1) continue; + if (this->_num_segm + 1 == curve._num_segm && c.ta == 1 && c.tb == 0) continue; + } + + if (dist < best_dist) { + best_dist = dist; + best_p = p_ix; + } + } + + // Now we've found the closest intersection, return it as a SnappedPoint + bool const use_this_as_primary = _distance < curve.getSnapDistance(); + Inkscape::SnappedCurve const *primaryC = use_this_as_primary ? this : &curve; + Inkscape::SnappedCurve const *secondaryC = use_this_as_primary ? &curve : this; + + // The intersection should in fact be returned in desktop coordinates + best_p = best_p * dt2doc; + + Geom::Coord primaryDist = use_this_as_primary ? Geom::L2(best_p - this->getPoint()) : Geom::L2(best_p - curve.getPoint()); + Geom::Coord secondaryDist = use_this_as_primary ? Geom::L2(best_p - curve.getPoint()) : Geom::L2(best_p - this->getPoint()); + // TODO: Investigate whether it is possible to use document coordinates everywhere + // in the snapper code. Only the mouse position should be in desktop coordinates, I guess. + // All paths are already in document coords and we are certainly not going to change THAT. + return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, primaryC->getSourceNum(), Inkscape::SNAPTARGET_PATH_INTERSECTION, primaryDist, primaryC->getTolerance(), primaryC->getAlwaysSnap(), true, false, true, + secondaryDist, secondaryC->getTolerance(), secondaryC->getAlwaysSnap()); + } + + // No intersection + return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false); +} + +Inkscape::SnappedPoint Inkscape::SnappedCurve::intersect(SnappedLine const &line, Geom::Point const &p, Geom::Affine dt2doc) const +{ + // Calculate the intersections of a curve with a line, which are both within snapping range, and + // return only the closest intersection + // The point of intersection should be considered for snapping, but might be outside the snapping range + // PS: We need p (the location of the mouse pointer) to find out which intersection is the + // closest, as there might be multiple intersections of a single curve with a line + + // 1) get a Geom::Line object from the SnappedLine + // 2) convert to document coordinates (line and p are in desktop coordinates, but the curves are in document coordinate) + // 3) create a Geom::LineSegment (i.e. a curve), because we cannot use a Geom::Line for calculating intersections + // (for this we will create a 2e6 pixels long linesegment, with t running from -1e6 to 1e6; this should be long + // enough for any practical purpose) + Geom::LineSegment line_segm = line.getLine().transformed(dt2doc).segment(-1e6, 1e6); // + const Geom::Curve *line_as_curve = dynamic_cast<Geom::Curve const*>(&line_segm); + Geom::Crossings cs = crossings(*(this->_curve), *line_as_curve); + + if (cs.size() > 0) { + // There might be multiple intersections: find the closest + Geom::Coord best_dist = Geom::infinity(); + Geom::Point best_p = Geom::Point(Geom::infinity(), Geom::infinity()); + for (const auto & c : cs) { + Geom::Point p_ix = this->_curve->pointAt(c.ta); + Geom::Coord dist = Geom::distance(p_ix, p); + + if (dist < best_dist) { + best_dist = dist; + best_p = p_ix; + } + } + + // The intersection should in fact be returned in desktop coordinates + best_p = best_p * dt2doc; + + // Now we've found the closest intersection, return it as a SnappedPoint + if (_distance < line.getSnapDistance()) { + // curve is the closest, so this is our primary snap target + return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, this->getSourceNum(), Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION, + Geom::L2(best_p - this->getPoint()), this->getTolerance(), this->getAlwaysSnap(), true, false, true, + Geom::L2(best_p - line.getPoint()), line.getTolerance(), line.getAlwaysSnap()); + } else { + return SnappedPoint(best_p, Inkscape::SNAPSOURCE_UNDEFINED, line.getSourceNum(), Inkscape::SNAPTARGET_PATH_GUIDE_INTERSECTION, + Geom::L2(best_p - line.getPoint()), line.getTolerance(), line.getAlwaysSnap(), true, false, true, + Geom::L2(best_p - this->getPoint()), this->getTolerance(), this->getAlwaysSnap()); + } + } + + // No intersection + return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false); +} + + +// search for the closest snapped line +bool getClosestCurve(std::list<Inkscape::SnappedCurve> const &list, Inkscape::SnappedCurve &result, bool exclude_paths) +{ + bool success = false; + + for (std::list<Inkscape::SnappedCurve>::const_iterator i = list.begin(); i != list.end(); ++i) { + if (exclude_paths && ((*i).getTarget() == Inkscape::SNAPTARGET_PATH)) { + continue; + } + if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) { + result = *i; + success = true; + } + } + + return success; +} + +// search for the closest intersection of two snapped curves, which are both member of the same collection +bool getClosestIntersectionCS(std::list<Inkscape::SnappedCurve> const &list, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc) +{ + bool success = false; + + for (std::list<Inkscape::SnappedCurve>::const_iterator i = list.begin(); i != list.end(); ++i) { + if ((*i).getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes, + // as this would require two bboxes two be flashed in the snap indicator + std::list<Inkscape::SnappedCurve>::const_iterator j = i; + ++j; + for (; j != list.end(); ++j) { + if ((*j).getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes + Inkscape::SnappedPoint sp = (*i).intersect(*j, p, dt2doc); + if (sp.getAtIntersection()) { + // if it's the first point + bool const c1 = !success; + // or, if it's closer + bool const c2 = sp.getSnapDistance() < result.getSnapDistance(); + // or, if it's just as close then look at the other distance + // (only relevant for snapped points which are at an intersection) + bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance()); + // then prefer this point over the previous one + if (c1 || c2 || c3) { + result = sp; + success = true; + } + } + } + } + } + } + + return success; +} + +// search for the closest intersection of two snapped curves, which are member of two different collections +bool getClosestIntersectionCL(std::list<Inkscape::SnappedCurve> const &curve_list, std::list<Inkscape::SnappedLine> const &line_list, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc) +{ + bool success = false; + + for (const auto & i : curve_list) { + if (i.getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes, + // as this would require two bboxes two be flashed in the snap indicator + for (const auto & j : line_list) { + if (j.getTarget() != Inkscape::SNAPTARGET_BBOX_EDGE) { // We don't support snapping to intersections of bboxes + Inkscape::SnappedPoint sp = i.intersect(j, p, dt2doc); + if (sp.getAtIntersection()) { + // if it's the first point + bool const c1 = !success; + // or, if it's closer + bool const c2 = sp.getSnapDistance() < result.getSnapDistance(); + // or, if it's just as close then look at the other distance + // (only relevant for snapped points which are at an intersection) + bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance()); + // then prefer this point over the previous one + if (c1 || c2 || c3) { + result = sp; + success = true; + } + } + } + } + } + } + + return success; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-curve.h b/src/snapped-curve.h new file mode 100644 index 0000000..bbbfb09 --- /dev/null +++ b/src/snapped-curve.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAPPEDCURVE_H +#define SEEN_SNAPPEDCURVE_H + +/** + * \file src/snapped-curve.h + * \brief SnappedCurve class. + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <vector> +#include <list> + +#include "snapped-point.h" +#include "snapped-line.h" + +namespace Inkscape { + +/// Class describing the result of an attempt to snap to a curve. +class SnappedCurve : public SnappedPoint +{ +public: + SnappedCurve(); + SnappedCurve(Geom::Point const &snapped_point, Geom::Point const &tangent, int num_path, int num_segm, Geom::Coord const &snapped_distance, Geom::Coord const &snapped_tolerance, bool const &always_snap, bool const &fully_constrained, Geom::Curve const *curve, SnapSourceType source, long source_num, SnapTargetType target, Geom::OptRect target_bbox); + ~SnappedCurve(); + Inkscape::SnappedPoint intersect(SnappedCurve const &curve, Geom::Point const &p, Geom::Affine dt2doc) const; //intersect with another SnappedCurve + Inkscape::SnappedPoint intersect(SnappedLine const &line, Geom::Point const &p, Geom::Affine dt2doc) const; //intersect with a SnappedLine + +private: + Geom::Curve const *_curve; + int _num_path; // Unique id of the path to which this segment belongs too + int _num_segm; // Sequence number of this segment in the path +}; + +} + +bool getClosestCurve(std::list<Inkscape::SnappedCurve> const &list, Inkscape::SnappedCurve &result, bool exclude_paths = false); +bool getClosestIntersectionCS(std::list<Inkscape::SnappedCurve> const &list, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc); +bool getClosestIntersectionCL(std::list<Inkscape::SnappedCurve> const &list1, std::list<Inkscape::SnappedLine> const &list2, Geom::Point const &p, Inkscape::SnappedPoint &result, Geom::Affine dt2doc); + + +#endif /* !SEEN_SNAPPEDCURVE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-line.cpp b/src/snapped-line.cpp new file mode 100644 index 0000000..5224117 --- /dev/null +++ b/src/snapped-line.cpp @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * \file src/snapped-line.cpp + * SnappedLine class. + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snapped-line.h" + +Inkscape::SnappedLineSegment::SnappedLineSegment(Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &snapped_tolerance, bool const &always_snap, Geom::Point const &start_point_of_line, Geom::Point const &end_point_of_line) + : _start_point_of_line(start_point_of_line), _end_point_of_line(end_point_of_line) +{ + _point = snapped_point; + _source = source; + _source_num = source_num; + _target = target; + _distance = snapped_distance; + _tolerance = std::max(snapped_tolerance, 1.0); + _always_snap = always_snap; + _at_intersection = false; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; +} + +Inkscape::SnappedLineSegment::SnappedLineSegment() +{ + _start_point_of_line = Geom::Point(0,0); + _end_point_of_line = Geom::Point(0,0); + _point = Geom::Point(0,0); + _source = SNAPSOURCE_UNDEFINED; + _source_num = -1; + _target = SNAPTARGET_UNDEFINED; + _distance = Geom::infinity(); + _tolerance = 1; + _always_snap = false; + _at_intersection = false; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; +} + + +Inkscape::SnappedLineSegment::~SnappedLineSegment() += default; + +Inkscape::SnappedPoint Inkscape::SnappedLineSegment::intersect(SnappedLineSegment const &line) const +{ + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(getLineSegment(), line.getLineSegment()); + } + catch (Geom::InfiniteSolutions &e) + { + // We're probably dealing with parallel lines, so they don't really cross + inters = Geom::OptCrossing(); + } + + if (inters) { + Geom::Point inters_pt = getLineSegment().pointAt((*inters).ta); + /* If a snapper has been told to "always snap", then this one should be preferred + * over the other, if that other one has not been told so. (The preferred snapper + * will be labeled "primary" below) + */ + bool const c1 = this->getAlwaysSnap() && !line.getAlwaysSnap(); //do not use _tolerance directly! + /* If neither or both have been told to "always snap", then cast a vote based on + * the snapped distance. For this we should consider the distance to the snapped + * line, not the distance to the intersection. + * See the comment in Inkscape::SnappedLine::intersect + */ + bool const c2 = _distance < line.getSnapDistance(); + bool const use_this_as_primary = c1 || c2; + Inkscape::SnappedLineSegment const *primarySLS = use_this_as_primary ? this : &line; + Inkscape::SnappedLineSegment const *secondarySLS = use_this_as_primary ? &line : this; + Geom::Coord primaryDist = use_this_as_primary ? Geom::L2(inters_pt - this->getPoint()) : Geom::L2(inters_pt - line.getPoint()); + Geom::Coord secondaryDist = use_this_as_primary ? Geom::L2(inters_pt - line.getPoint()) : Geom::L2(inters_pt - this->getPoint()); + return SnappedPoint(inters_pt, SNAPSOURCE_UNDEFINED, primarySLS->getSourceNum(), SNAPTARGET_PATH_INTERSECTION, primaryDist, primarySLS->getTolerance(), primarySLS->getAlwaysSnap(), true, false, true, + secondaryDist, secondarySLS->getTolerance(), secondarySLS->getAlwaysSnap()); + } + + // No intersection + return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false); +}; + + + +Inkscape::SnappedLine::SnappedLine(Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &snapped_tolerance, bool const &always_snap, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) + : _normal_to_line(normal_to_line), _point_on_line(point_on_line) +{ + _source = source; + _source_num = source_num; + _target = target; + _distance = snapped_distance; + _tolerance = std::max(snapped_tolerance, 1.0); + _always_snap = always_snap; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; + _point = snapped_point; + _at_intersection = false; +} + +Inkscape::SnappedLine::SnappedLine() +{ + _normal_to_line = Geom::Point(0,0); + _point_on_line = Geom::Point(0,0); + _source = SNAPSOURCE_UNDEFINED; + _source_num = -1; + _target = SNAPTARGET_UNDEFINED; + _distance = Geom::infinity(); + _tolerance = 1; + _always_snap = false; + _second_distance = Geom::infinity(); + _second_tolerance = 1; + _second_always_snap = false; + _point = Geom::Point(0,0); + _at_intersection = false; +} + +Inkscape::SnappedLine::~SnappedLine() += default; + +Inkscape::SnappedPoint Inkscape::SnappedLine::intersect(SnappedLine const &line) const +{ + // Calculate the intersection of two lines, which are both within snapping range + // One could be a grid line, whereas the other could be a guide line + // The point of intersection should be considered for snapping, but might be outside the snapping range + + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(getLine(), line.getLine()); + } + catch (Geom::InfiniteSolutions &e) + { + // We're probably dealing with parallel lines, so they don't really cross + inters = Geom::OptCrossing(); + } + + if (inters) { + Geom::Point inters_pt = getLine().pointAt((*inters).ta); + /* If a snapper has been told to "always snap", then this one should be preferred + * over the other, if that other one has not been told so. (The preferred snapper + * will be labelled "primary" below) + */ + bool const c1 = this->getAlwaysSnap() && !line.getAlwaysSnap(); + /* If neither or both have been told to "always snap", then cast a vote based on + * the snapped distance. For this we should consider the distance to the snapped + * line or to the intersection + */ + bool const c2 = _distance < line.getSnapDistance(); + bool const use_this_as_primary = c1 || c2; + Inkscape::SnappedLine const *primarySL = use_this_as_primary ? this : &line; + Inkscape::SnappedLine const *secondarySL = use_this_as_primary ? &line : this; + Geom::Coord primaryDist = use_this_as_primary ? Geom::L2(inters_pt - this->getPoint()) : Geom::L2(inters_pt - line.getPoint()); + Geom::Coord secondaryDist = use_this_as_primary ? Geom::L2(inters_pt - line.getPoint()) : Geom::L2(inters_pt - this->getPoint()); + return SnappedPoint(inters_pt, Inkscape::SNAPSOURCE_UNDEFINED, primarySL->getSourceNum(), Inkscape::SNAPTARGET_UNDEFINED, primaryDist, primarySL->getTolerance(), primarySL->getAlwaysSnap(), true, false, true, + secondaryDist, secondarySL->getTolerance(), secondarySL->getAlwaysSnap()); + // The type of the snap target is yet undefined, as we cannot tell whether + // we're snapping to grid or the guide lines; must be set by on a higher level + } + + // No intersection + return SnappedPoint(Geom::Point(Geom::infinity(), Geom::infinity()), SNAPSOURCE_UNDEFINED, 0, SNAPTARGET_UNDEFINED, Geom::infinity(), 0, false, false, false, false, Geom::infinity(), 0, false); +} + +// search for the closest snapped line segment +bool getClosestSLS(std::list<Inkscape::SnappedLineSegment> const &list, Inkscape::SnappedLineSegment &result) +{ + bool success = false; + + for (std::list<Inkscape::SnappedLineSegment>::const_iterator i = list.begin(); i != list.end(); ++i) { + if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) { + result = *i; + success = true; + } + } + + return success; +} + +// search for the closest intersection of two snapped line segments, which are both member of the same collection +bool getClosestIntersectionSLS(std::list<Inkscape::SnappedLineSegment> const &list, Inkscape::SnappedPoint &result) +{ + bool success = false; + + for (std::list<Inkscape::SnappedLineSegment>::const_iterator i = list.begin(); i != list.end(); ++i) { + std::list<Inkscape::SnappedLineSegment>::const_iterator j = i; + ++j; + for (; j != list.end(); ++j) { + Inkscape::SnappedPoint sp = (*i).intersect(*j); + if (sp.getAtIntersection()) { + // if it's the first point + bool const c1 = !success; + // or, if it's closer + bool const c2 = sp.getSnapDistance() < result.getSnapDistance(); + // or, if it's just then look at the other distance + // (only relevant for snapped points which are at an intersection + bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance()); + // then prefer this point over the previous one + if (c1 || c2 || c3) { + result = sp; + success = true; + } + } + } + } + + return success; +} + +// search for the closest snapped line +bool getClosestSL(std::list<Inkscape::SnappedLine> const &list, Inkscape::SnappedLine &result) +{ + bool success = false; + + for (std::list<Inkscape::SnappedLine>::const_iterator i = list.begin(); i != list.end(); ++i) { + if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) { + result = *i; + success = true; + } + } + + return success; +} + +// search for the closest intersection of two snapped lines, which are both member of the same collection +bool getClosestIntersectionSL(std::list<Inkscape::SnappedLine> const &list, Inkscape::SnappedPoint &result) +{ + bool success = false; + + for (std::list<Inkscape::SnappedLine>::const_iterator i = list.begin(); i != list.end(); ++i) { + std::list<Inkscape::SnappedLine>::const_iterator j = i; + ++j; + for (; j != list.end(); ++j) { + Inkscape::SnappedPoint sp = (*i).intersect(*j); + if (sp.getAtIntersection()) { + // if it's the first point + bool const c1 = !success; + // or, if it's closer + bool const c2 = sp.getSnapDistance() < result.getSnapDistance(); + // or, if it's just then look at the other distance + // (only relevant for snapped points which are at an intersection + bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance()); + // then prefer this point over the previous one + if (c1 || c2 || c3) { + result = sp; + success = true; + } + } + } + } + + return success; +} + +// search for the closest intersection of two snapped lines, which are in two different collections +bool getClosestIntersectionSL(std::list<Inkscape::SnappedLine> const &list1, std::list<Inkscape::SnappedLine> const &list2, Inkscape::SnappedPoint &result) +{ + bool success = false; + + for (const auto & i : list1) { + for (const auto & j : list2) { + Inkscape::SnappedPoint sp = i.intersect(j); + if (sp.getAtIntersection()) { + // if it's the first point + bool const c1 = !success; + // or, if it's closer + bool const c2 = sp.getSnapDistance() < result.getSnapDistance(); + // or, if it's just then look at the other distance + // (only relevant for snapped points which are at an intersection + bool const c3 = (sp.getSnapDistance() == result.getSnapDistance()) && (sp.getSecondSnapDistance() < result.getSecondSnapDistance()); + // then prefer this point over the previous one + if (c1 || c2 || c3) { + result = sp; + success = true; + } + } + } + } + + return success; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-line.h b/src/snapped-line.h new file mode 100644 index 0000000..eda197c --- /dev/null +++ b/src/snapped-line.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAPPEDLINE_H +#define SEEN_SNAPPEDLINE_H + +/** + * \file src/snapped-line.h + * \brief SnappedLine class. + * + * Authors: + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include <list> +#include "snapped-point.h" + +namespace Inkscape +{ + +/// Class describing the result of an attempt to snap to a line segment. +class SnappedLineSegment : public SnappedPoint +{ +public: + SnappedLineSegment(); + SnappedLineSegment(Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &snapped_tolerance,bool const &always_snap, Geom::Point const &start_point_of_line, Geom::Point const &end_point_of_line); + ~SnappedLineSegment(); + Inkscape::SnappedPoint intersect(SnappedLineSegment const &line) const; //intersect with another SnappedLineSegment + Geom::LineSegment getLineSegment() const {return Geom::LineSegment(_start_point_of_line, _end_point_of_line);} + +private: + Geom::Point _start_point_of_line; + Geom::Point _end_point_of_line; +}; + + +/// Class describing the result of an attempt to snap to a line. +class SnappedLine : public SnappedPoint +{ +public: + SnappedLine(); + SnappedLine(Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &snapped_tolerance, bool const &always_snap, Geom::Point const &normal_to_line, Geom::Point const &point_on_line); + ~SnappedLine(); + Inkscape::SnappedPoint intersect(SnappedLine const &line) const; //intersect with another SnappedLine + // This line is described by this equation: + // a*x + b*y = c <-> nx*px + ny+py = c <-> n.p = c + Geom::Point getNormal() const {return _normal_to_line;} // n = (nx, ny) + Geom::Point getPointOnLine() const {return _point_on_line;} // p = (px, py) + Geom::Coord getConstTerm() const {return dot(_normal_to_line, _point_on_line);} // c = n.p = nx*px + ny*py; + Geom::Line getLine() const {return Geom::Line(_point_on_line, _point_on_line + Geom::rot90(_normal_to_line));} + +private: + Geom::Point _normal_to_line; + Geom::Point _point_on_line; +}; + +} + +bool getClosestSLS(std::list<Inkscape::SnappedLineSegment> const &list, Inkscape::SnappedLineSegment &result); +bool getClosestIntersectionSLS(std::list<Inkscape::SnappedLineSegment> const &list, Inkscape::SnappedPoint &result); +bool getClosestSL(std::list<Inkscape::SnappedLine> const &list, Inkscape::SnappedLine &result); +bool getClosestIntersectionSL(std::list<Inkscape::SnappedLine> const &list, Inkscape::SnappedPoint &result); +bool getClosestIntersectionSL(std::list<Inkscape::SnappedLine> const &list1, std::list<Inkscape::SnappedLine> const &list2, Inkscape::SnappedPoint &result); + + +#endif /* !SEEN_SNAPPEDLINE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-point.cpp b/src/snapped-point.cpp new file mode 100644 index 0000000..033713c --- /dev/null +++ b/src/snapped-point.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * \file src/snapped-point.cpp + * SnappedPoint class. + * + * Authors: + * Mathieu Dimanche <mdimanche@free.fr> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> + +#include <utility> +#include "snapped-point.h" +#include "preferences.h" + +// overloaded constructor +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox) : + _point(p), + _tangent(Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)),// tolerance should never be smaller than 1 px, as it is used for normalization in isOtherSnapBetter. We don't want a division by zero. + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox(std::move(target_bbox)), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Inkscape::SnapCandidatePoint const &p, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained) : + _point (p.getPoint()), + _tangent (Geom::Point(0,0)), + _source (p.getSourceType()), + _source_num (p.getSourceNum()), + _target(target), + _at_intersection (false), + _constrained_snap (constrained_snap), + _fully_constrained (fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)), + _always_snap(a), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox (p.getTargetBBox()), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &at_intersection, bool const &constrained_snap, bool const &fully_constrained, Geom::Coord const &d2, Geom::Coord const &t2, bool const &a2) : + _point(p), + _tangent (Geom::Point(0,0)), + _source(source), + _source_num(source_num), + _target(target), + _at_intersection(at_intersection), + _constrained_snap(constrained_snap), + _fully_constrained(fully_constrained), + _distance(d), + _tolerance(std::max(t,1.0)), + _always_snap(a), + _second_distance(d2), + _second_tolerance(std::max(t2,1.0)), + _second_always_snap(a2), + // tolerance should never be smaller than 1 px, as it is used for normalization in + // isOtherSnapBetter. We don't want a division by zero. + _target_bbox (Geom::OptRect()), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(): + _point (Geom::Point(0,0)), + _tangent (Geom::Point(0,0)), + _source (SNAPSOURCE_UNDEFINED), + _source_num (-1), + _target (SNAPTARGET_UNDEFINED), + _at_intersection (false), + _constrained_snap (false), + _fully_constrained (false), + _distance (Geom::infinity()), + _tolerance (1), + _always_snap (false), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox (Geom::OptRect()), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::SnappedPoint(Geom::Point const &p): + _point (p), + _tangent (Geom::Point(0,0)), + _source (SNAPSOURCE_UNDEFINED), + _source_num (-1), + _target (SNAPTARGET_UNDEFINED), + _at_intersection (false), + _constrained_snap (false), + _fully_constrained (false), + _distance (Geom::infinity()), + _tolerance (1), + _always_snap (false), + _second_distance (Geom::infinity()), + _second_tolerance (1), + _second_always_snap (false), + _target_bbox (Geom::OptRect()), + _pointer_distance (Geom::infinity()) +{ +} + +Inkscape::SnappedPoint::~SnappedPoint() += default; + +void Inkscape::SnappedPoint::getPointIfSnapped(Geom::Point &p) const +{ + // When we have snapped + if (getSnapped()) { + // then return the snapped point by overwriting p + p = _point; + } //otherwise p will be left untouched; this way the caller doesn't have to check whether we've snapped +} + +// search for the closest snapped point +bool getClosestSP(std::list<Inkscape::SnappedPoint> const &list, Inkscape::SnappedPoint &result) +{ + bool success = false; + + for (std::list<Inkscape::SnappedPoint>::const_iterator i = list.begin(); i != list.end(); ++i) { + if ((i == list.begin()) || (*i).getSnapDistance() < result.getSnapDistance()) { + result = *i; + success = true; + } + } + + return success; +} + +bool Inkscape::SnappedPoint::isOtherSnapBetter(Inkscape::SnappedPoint const &other_one, bool weighted) const +{ + + if (getSnapped() && !other_one.getSnapped()) { + return false; + } + + if (!getSnapped() && other_one.getSnapped()) { + return true; + } + + double dist_other = other_one.getSnapDistance(); + double dist_this = getSnapDistance(); + + // The distance to the pointer should only be taken into account when finding the best snapped source node (when + // there's more than one). It is not useful when trying to find the best snapped target point. + // (both the snap distance and the pointer distance are measured in document pixels, not in screen pixels) + if (weighted) { + Geom::Coord const dist_pointer_other = other_one.getPointerDistance(); + Geom::Coord const dist_pointer_this = getPointerDistance(); + // Weight factor: controls which node should be preferred for snapping, which is either + // the node with the closest snap (w = 0), or the node closest to the mousepointer (w = 1) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double w = prefs->getDoubleLimited("/options/snapweight/value", 0.5, 0, 1); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + w = 1; + } + if (w > 0) { + if (!(w == 1 && dist_pointer_this == dist_pointer_other)) { + // When accounting for the distance to the mouse pointer, then at least one of the snapped points should + // have that distance set. If not, then this is a bug. Either "weighted" must be set to false, or the + // mouse pointer distance must be set. + g_assert(dist_pointer_this != Geom::infinity() || dist_pointer_other != Geom::infinity()); + // The snap distance will always be smaller than the tolerance set for the snapper. The pointer distance can + // however be very large. To compare these in a fair way, we will have to normalize these metrics first + // The closest pointer distance will be normalized to 1.0; the other one will be > 1.0 + // The snap distance will be normalized to 1.0 if it's equal to the snapper tolerance + double const norm_p = std::min(dist_pointer_this, dist_pointer_other) + 1; + // make sure norm_p is never too close to zero (e.g. when snapping the bbox-corner that was grabbed), by incr. with 1 + double const norm_t_other = std::min(50.0, other_one.getTolerance()); + double const norm_t_this = std::min(50.0, getTolerance()); + dist_other = w * dist_pointer_other / norm_p + (1-w) * dist_other / norm_t_other; + dist_this = w * dist_pointer_this / norm_p + (1-w) * dist_this / norm_t_this; + } + } + } + + // When snapping to a constraint line only, which is not really a snap but merely a projection + // to the constraint line, then give this snap a very low priority. Basically, any other snap will do + if (other_one.getTarget() == SNAPTARGET_CONSTRAINT) { + dist_other += 1e6; + } + if (getTarget() == SNAPTARGET_CONSTRAINT) { + dist_this += 1e6; + } + + // If it's closer + bool c1 = dist_other < dist_this; + // or, if it's for a snapper with "always snap" turned on, and the previous wasn't + bool c2 = other_one.getAlwaysSnap() && !getAlwaysSnap(); + // But in no case fall back from a snapper with "always snap" on to one with "always snap" off + bool c2n = !other_one.getAlwaysSnap() && getAlwaysSnap(); + // or, if we have a fully constrained snappoint (e.g. to a node or an intersection), while the previous one was only partly constrained (e.g. to a line) + bool c3 = (other_one.getFullyConstrained() && !other_one.getConstrainedSnap()) && !getFullyConstrained(); // Do not consider constrained snaps here, because these will always be fully constrained anyway + // But in no case fall back; (has less priority than c3n, so it is allowed to fall back when c3 is true, see below) + bool c3n = !other_one.getFullyConstrained() && (getFullyConstrained() && !getConstrainedSnap()); + + // When both are fully constrained AND coincident, then prefer nodes over intersections + bool d = other_one.getFullyConstrained() && getFullyConstrained() && (Geom::L2(other_one.getPoint() - getPoint()) < 1e-9); + bool c4 = d && !other_one.getAtIntersection() && getAtIntersection(); + // But don't fall back... + bool c4n = d && other_one.getAtIntersection() && !getAtIntersection(); + + // or, if it's just as close then consider the second distance ... + bool c5a = (dist_other == dist_this); + bool c5b = (other_one.getSecondSnapDistance() < getSecondSnapDistance()) && (getSecondSnapDistance() < Geom::infinity()); + // ... or prefer free snaps over constrained snaps + bool c5c = !other_one.getConstrainedSnap() && getConstrainedSnap(); + + bool other_is_better = (c1 || c2 || c3 || c4 || (c5a && (c5b || c5c))) && !c2n && (!c3n || c2) && !c4n; + + /* + std::cout << other_one.getPoint() << " (Other one, dist = " << dist_other << ") vs. " << getPoint() << " (this one, dist = " << dist_this << ") ---> "; + std::cout << "c1 = " << c1 << " | c2 = " << c2 << " | c2n = " << c2n << " | c3 = " << c3 << " | c3n = " << c3n << " | c4 = " << c4 << " | c4n = " << c4n << " | c5a = " << c5a << " | c5b = " << c5b << " | c5c = " << c5c << std::endl; + std::cout << "Other one provides a better snap: " << other_is_better << std::endl; + */ + + return other_is_better; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapped-point.h b/src/snapped-point.h new file mode 100644 index 0000000..75b5472 --- /dev/null +++ b/src/snapped-point.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAPPEDPOINT_H +#define SEEN_SNAPPEDPOINT_H + +/** + * \file src/snapped-point.h + * \brief SnappedPoint class. + * + * Authors: + * Mathieu Dimanche <mdimanche@free.fr> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/geom.h> +#include <list> +#include <vector> + +#include "snap-candidate.h" + +namespace Inkscape +{ + +/// Class describing the result of an attempt to snap. +class SnappedPoint +{ + +public: + SnappedPoint(); + SnappedPoint(Geom::Point const &p); + SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &at_intersection, bool const &constrained_snap, bool const &fully_constrained, Geom::Coord const &d2, Geom::Coord const &t2, bool const &a2); + SnappedPoint(Geom::Point const &p, SnapSourceType const &source, long source_num, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained, Geom::OptRect target_bbox = Geom::OptRect()); + SnappedPoint(SnapCandidatePoint const &p, SnapTargetType const &target, Geom::Coord const &d, Geom::Coord const &t, bool const &a, bool const &constrained_snap, bool const &fully_constrained); + ~SnappedPoint(); + + Geom::Coord getSnapDistance() const {return _distance;} + void setSnapDistance(Geom::Coord const d) {_distance = d;} + Geom::Coord getTolerance() const {return _tolerance;} + bool getAlwaysSnap() const {return _always_snap;} + Geom::Coord getSecondSnapDistance() const {return _second_distance;} + void setSecondSnapDistance(Geom::Coord const d) {_second_distance = d;} + Geom::Coord getSecondTolerance() const {return _second_tolerance;} + bool getSecondAlwaysSnap() const {return _second_always_snap;} + Geom::Coord getPointerDistance() const {return _pointer_distance;} + void setPointerDistance(Geom::Coord const d) {_pointer_distance = d;} + + /* This is the preferred method to find out which point we have snapped + * to, because it only returns a point if snapping has actually occurred + * (by overwriting p) + */ + void getPointIfSnapped(Geom::Point &p) const; + + /* This method however always returns a point, even if no snapping + * has occurred; A check should be implemented in the calling code + * to check for snapping. Use this method only when really needed, e.g. + * when the calling code is trying to snap multiple points and must + * determine itself which point is most appropriate, or when doing a + * constrainedSnap that also enforces projection onto the constraint (in + * which case you need the new point anyway, even if we didn't snap) + */ + Geom::Point getPoint() const {return _point;} + void setPoint(Geom::Point const &p) {_point = p;} + Geom::Point getTangent() const {return _tangent;} + + bool getAtIntersection() const {return _at_intersection;} + bool getFullyConstrained() const {return _fully_constrained;} + bool getConstrainedSnap() const {return _constrained_snap;} + bool getSnapped() const {return _distance < Geom::infinity();} + void setTarget(SnapTargetType const target) {_target = target;} + SnapTargetType getTarget() const {return _target;} + void setTargetBBox(Geom::OptRect const target) {_target_bbox = target;} + Geom::OptRect const getTargetBBox() const {return _target_bbox;} + void setSource(SnapSourceType const source) {_source = source;} + SnapSourceType getSource() const {return _source;} + long getSourceNum() const {return _source_num;} + + bool isOtherSnapBetter(SnappedPoint const &other_one, bool weighted) const; + + /*void dump() const { + std::cout << "_point = " << _point << std::endl; + std::cout << "_source = " << _source << std::endl; + std::cout << "_source_num = " << _source_num << std::endl; + std::cout << "_target = " << _target << std::endl; + std::cout << "_at_intersection = " << _at_intersection << std::endl; + std::cout << "_fully_constrained = " << _fully_constrained << std::endl; + std::cout << "_distance = " << _distance << std::endl; + std::cout << "_tolerance = " << _tolerance << std::endl; + std::cout << "_always_snap = " << _always_snap << std::endl; + std::cout << "_second_distance = " << _second_distance << std::endl; + std::cout << "_second_tolerance = " << _second_tolerance << std::endl; + std::cout << "_second_always_snap = " << _second_always_snap << std::endl; + std::cout << "_transformation = " << _transformation << std::endl; + std::cout << "_pointer_distance = " << _pointer_distance << std::endl; + }*/ + +protected: + Geom::Point _point; // Location of the snapped point + Geom::Point _tangent; // Tangent of the curve we snapped to, at the snapped point + SnapSourceType _source; // Describes what snapped + long _source_num; // Sequence number of the source point that snapped, if that point is part of a set of points. (starting at zero if we might have a set of points; -1 if we only have a single point) + SnapTargetType _target; // Describes to what we've snapped to + bool _at_intersection; // If true, the snapped point is at an intersection + bool _constrained_snap; // If true, then the snapped point was found when looking for a constrained snap + bool _fully_constrained; // When snapping for example to a node, then the snap will be "fully constrained". + // When snapping to a line however, the snap is only partly constrained (i.e. only in one dimension) + + /* Distance from original point to snapped point. If the snapped point is at + an intersection of e.g. two lines, then this is the distance to the closest + line */ + Geom::Coord _distance; + /* The snapping tolerance in screen pixels (depends on zoom)*/ + Geom::Coord _tolerance; + /* If true then "Always snap" is on */ + bool _always_snap; + + /* If the snapped point is at an intersection of e.g. two lines, then this is + the distance to the farthest line */ + Geom::Coord _second_distance; + /* The snapping tolerance in screen pixels (depends on zoom)*/ + Geom::Coord _second_tolerance; + /* If true then "Always snap" is on */ + bool _second_always_snap; + /* The bounding box we've snapped to (when applicable); will be used by the snapindicator */ + Geom::OptRect _target_bbox; + /* Distance from the un-transformed point to the mouse pointer, measured at the point in time when dragging started */ + Geom::Coord _pointer_distance; +}; + +}// end of namespace Inkscape + +bool getClosestSP(std::list<Inkscape::SnappedPoint> const &list, Inkscape::SnappedPoint &result); + +#endif /* !SEEN_SNAPPEDPOINT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapper.cpp b/src/snapper.cpp new file mode 100644 index 0000000..b288608 --- /dev/null +++ b/src/snapper.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file src/snapper.cpp + * Snapper class. + * + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> + +#include "snapper.h" + +/** + * Construct new Snapper for named view. + * @param nv Named view. + * @param d Snap tolerance. + */ +Inkscape::Snapper::Snapper(SnapManager *sm, Geom::Coord const /*t*/) : + _snapmanager(sm), + _snap_enabled(true), + _snap_visible_only(true) +{ + g_assert(_snapmanager != nullptr); +} + +/** + * @param s true to enable this snapper, otherwise false. + */ + +void Inkscape::Snapper::setEnabled(bool s) +{ + _snap_enabled = s; +} + +void Inkscape::Snapper::setSnapVisibleOnly(bool s) +{ + _snap_visible_only = s; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/snapper.h b/src/snapper.h new file mode 100644 index 0000000..662d282 --- /dev/null +++ b/src/snapper.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAPPER_H +#define SEEN_SNAPPER_H + +/** + * \file src/snapper.h + * \brief Snapper class. + * + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Diederik van Lierop <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/optional.hpp> +#include <cstdio> +#include <list> + +#include "snap-candidate.h" +#include "snapped-point.h" +#include "snapped-line.h" +#include "snapped-curve.h" + +struct IntermSnapResults { + std::list<Inkscape::SnappedPoint> points; + std::list<Inkscape::SnappedLine> grid_lines; + std::list<Inkscape::SnappedLine> guide_lines; + std::list<Inkscape::SnappedCurve> curves; +}; + +class SnapManager; +class SPItem; + +namespace Inkscape +{ +/// Parent for classes that can snap points to something +class Snapper +{ +public: + //Snapper() {} //does not seem to be used somewhere + Snapper(SnapManager *sm, ::Geom::Coord const t); + virtual ~Snapper() = default; + + virtual Geom::Coord getSnapperTolerance() const = 0; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + virtual bool getSnapperAlwaysSnap() const = 0; //if true, then the snapper will always snap, regardless of its tolerance + + /** + * \return true if this Snapper will snap at least one kind of point. + */ + virtual bool ThisSnapperMightSnap() const {return _snap_enabled;} // will likely be overridden by derived classes + + // These four methods are only used for grids, for which snapping can be enabled individually + void setEnabled(bool s); + void setSnapVisibleOnly(bool s); + bool getEnabled() const {return _snap_enabled;} + bool getSnapVisibleOnly() const {return _snap_visible_only;} + + virtual void freeSnap(IntermSnapResults &/*isr*/, + Inkscape::SnapCandidatePoint const &/*p*/, + Geom::OptRect const &/*bbox_to_snap*/, + std::vector<SPItem const *> const */*it*/, + std::vector<SnapCandidatePoint> */*unselected_nodes*/) const {}; + + // Class for storing the constraint for constrained snapping; can be + // - a line (infinite line with origin, running through _point pointing in _direction) + // - a direction (infinite line without origin, i.e. only a direction vector, stored in _direction) + // - a circle (_point denotes the center, _radius doesn't need an explanation, _direction contains + // the vector from the origin to the original untransformed point); + class SnapConstraint + { + private: + enum SnapConstraintType {LINE, DIRECTION, CIRCLE, UNDEFINED}; + + public: + // Constructs a direction constraint, e.g. horizontal or vertical but without a specified point + SnapConstraint(Geom::Point const &d) : _point(), _direction(d), _radius(0), _type(DIRECTION) {} + // Constructs a linear constraint + SnapConstraint(Geom::Point const &p, Geom::Point const &d) : _point(p), _direction(d), _radius(0), _type(LINE) {} + // Orthogonal version + SnapConstraint(Geom::Point const &p, Geom::Dim2 const &d) : _point(p), _direction(), _radius(0), _type(LINE) {_direction[d] = 1.;} + SnapConstraint(Geom::Line const &l) : _point(l.origin()), _direction(l.versor()), _radius(0), _type(LINE) {} + // Constructs a circular constraint + SnapConstraint(Geom::Point const &p, Geom::Point const &d, Geom::Coord const &r) : _point(p), _direction(d), _radius(r), _type(CIRCLE) {} + // Undefined, or empty constraint + SnapConstraint() : _point(), _direction(), _radius(0), _type(UNDEFINED) {} + + bool hasPoint() const {return _type != DIRECTION && _type != UNDEFINED;} + + Geom::Point getPoint() const { + assert(_type != DIRECTION && _type != UNDEFINED); + return _point; + } + + Geom::Point getDirection() const { + return _direction; + } + + Geom::Coord getRadius() const { + assert(_type == CIRCLE); + return _radius; + } + + bool isCircular() const { return _type == CIRCLE; } + bool isLinear() const { return _type == LINE; } + bool isDirection() const { return _type == DIRECTION; } + bool isUndefined() const { return _type == UNDEFINED; } + + Geom::Point projection(Geom::Point const &p) const { // returns the projection of p on this constraint + if (_type == CIRCLE) { + // project on to a circular constraint + Geom::Point v_orig = p - _point; + Geom::Coord l = Geom::L2(v_orig); + if (l > 0) { + return _point + _radius * v_orig/l; // Length of _direction is equal to the radius + } else { + // point to be projected is exactly at the center of the circle, so any point on the circle is a projection + return _point + Geom::Point(_radius, 0); + } + } else if (_type != UNDEFINED){ + // project on to a linear constraint + Geom::Point const p1_on_cl = (_type == LINE) ? _point : p; + Geom::Point const p2_on_cl = p1_on_cl + _direction; + return Geom::projection(p, Geom::Line(p1_on_cl, p2_on_cl)); + } else { + printf("WARNING: Bug: trying to find the projection onto an undefined constraint"); + return Geom::Point(); + } + } + + private: + Geom::Point _point; + Geom::Point _direction; + Geom::Coord _radius; + SnapConstraintType _type; + }; + + virtual void constrainedSnap(IntermSnapResults &/*isr*/, + Inkscape::SnapCandidatePoint const &/*p*/, + Geom::OptRect const &/*bbox_to_snap*/, + SnapConstraint const &/*c*/, + std::vector<SPItem const *> const */*it*/, + std::vector<SnapCandidatePoint> */*unselected_nodes*/) const {}; + +protected: + SnapManager *_snapmanager; + + // This is only used for grids, for which snapping can be enabled individually + bool _snap_enabled; ///< true if this snapper is enabled, otherwise false + bool _snap_visible_only; +}; + +} + +#endif /* !SEEN_SNAPPER_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/sp-cursor.cpp b/src/sp-cursor.cpp new file mode 100644 index 0000000..dd8a300 --- /dev/null +++ b/src/sp-cursor.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Some convenience stuff + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2010 Jasper van de Gronde + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2012 Kris De Gussem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <gdk/gdk.h> +#include <map> +#include <sstream> + +#include "color.h" +#include "sp-cursor.h" + +static void free_cursor_data(unsigned char *pixels, void* /*data*/) { + delete [] reinterpret_cast<guint32*>(pixels); +} + +struct RGBA { + guchar v[4]; + + RGBA() { + v[0] = 0; + v[1] = 0; + v[2] = 0; + v[3] = 0; + } + + RGBA(guchar r, guchar g, guchar b, guchar a) { + v[0] = r; + v[1] = g; + v[2] = b; + v[3] = a; + } + + operator guint32() const { + guint32 result = (static_cast<guint32>(v[0]) << 0) + | (static_cast<guint32>(v[1]) << 8) + | (static_cast<guint32>(v[2]) << 16) + | (static_cast<guint32>(v[3]) << 24); + return result; + } +}; + +GdkCursor *sp_cursor_from_xpm(char const *const *xpm, guint32 fill, guint32 stroke) +{ + GdkPixbuf *pixbuf; + GdkCursor *cursor = nullptr; + GdkDisplay *display = gdk_display_get_default(); + + int height = 0; + int width = 0; + int colors = 0; + int pix = 0; + int hot_x = 0; + int hot_y = 0; + std::stringstream ss (std::stringstream::in | std::stringstream::out); + ss << xpm[0]; + ss >> height; + ss >> width; + ss >> colors; + ss >> pix; + ss >> hot_x; + ss >> hot_y; + + if (gdk_display_supports_cursor_alpha(display) && gdk_display_supports_cursor_color(display)) { + std::map<char, RGBA> colorMap; + + for (int i = 0; i < colors; i++) { + + char const *p = xpm[1 + i]; + g_assert(*p >=0); + unsigned char const ccode = (guchar) *p; + + p++; + while (isspace(*p)) { + p++; + } + p++; + while (isspace(*p)) { + p++; + } + + if (strcmp(p, "Fill") == 0) { + colorMap[ccode] = RGBA(SP_RGBA32_R_U(fill), SP_RGBA32_G_U(fill), SP_RGBA32_B_U(fill), SP_RGBA32_A_U(fill)); + } else if (strcmp(p, "Stroke") == 0) { + colorMap[ccode] = RGBA(SP_RGBA32_R_U(stroke), SP_RGBA32_G_U(stroke), SP_RGBA32_B_U(stroke), SP_RGBA32_A_U(stroke)); + } else if (p[0] == '#') { + GdkRGBA color; + if (gdk_rgba_parse(&color, p)) { + colorMap[ccode] = RGBA(color.red * 255, color.green * 255, color.blue * 255, color.alpha * 255); + } else { + colorMap[ccode] = RGBA(); + } + } else { // Catches 'None' + colorMap[ccode] = RGBA(); + } + } + + guint32 *pixmap_buffer = new guint32[width * height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + std::map<char, RGBA>::const_iterator it = colorMap.find(xpm[1 + colors + y][x]); + pixmap_buffer[y * width + x] = (it == colorMap.end()) ? 0u : it->second; + } + } + +#if G_BYTE_ORDER == G_BIG_ENDIAN + for (int i = 0, n = width * height; i < n; i++) { + guint32 v = pixmap_buffer[i]; + pixmap_buffer[i] = ((v & 0xFF) << 24) | (((v >> 8) & 0xFF) << 16) | (((v >> 16) & 0xFF) << 8) | ((v >> 24) & 0xFF); + } +#endif + + pixbuf = gdk_pixbuf_new_from_data(reinterpret_cast<guchar*>(pixmap_buffer), GDK_COLORSPACE_RGB, TRUE, 8, width, height, width * sizeof(guint32), free_cursor_data, nullptr); + } else { + pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)xpm); + } + + if (pixbuf != nullptr) { + cursor = gdk_cursor_new_from_pixbuf(display, pixbuf, hot_x, hot_y); + g_object_unref(pixbuf); + } else { + g_warning("Failed to load cursor from xpm!"); + } + return cursor; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/sp-cursor.h b/src/sp-cursor.h new file mode 100644 index 0000000..73c59e6 --- /dev/null +++ b/src/sp-cursor.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SP_CURSOR_H +#define SP_CURSOR_H + +typedef unsigned int guint32; +typedef struct _GdkPixbuf GdkPixbuf; +typedef struct _GdkCursor GdkCursor; +typedef struct _GdkColor GdkColor; + +GdkCursor* sp_cursor_from_xpm(char const *const *xpm, guint32 fill=0, guint32 stroke=0); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/sp-guide-attachment.h b/src/sp-guide-attachment.h new file mode 100644 index 0000000..05d43a2 --- /dev/null +++ b/src/sp-guide-attachment.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GUIDE_ATTACHMENT_H +#define SEEN_SP_GUIDE_ATTACHMENT_H + +#include "object/sp-item.h" + +class SPGuideAttachment { +public: + SPItem *item; + int snappoint_ix; + +public: + SPGuideAttachment() : + item(static_cast<SPItem *>(nullptr)), + snappoint_ix(0) + { } + + SPGuideAttachment(SPItem *i, int s) : + item(i), + snappoint_ix(s) + { } + + bool operator==(SPGuideAttachment const &o) const { + return ( ( item == o.item ) + && ( snappoint_ix == o.snappoint_ix ) ); + } + + bool operator!=(SPGuideAttachment const &o) const { + return !(*this == o); + } +}; + +#endif // SEEN_SP_GUIDE_ATTACHMENT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/sp-guide-constraint.h b/src/sp-guide-constraint.h new file mode 100644 index 0000000..578f392 --- /dev/null +++ b/src/sp-guide-constraint.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_GUIDE_CONSTRAINT_H +#define SEEN_SP_GUIDE_CONSTRAINT_H + +class SPGuide; + +class SPGuideConstraint { +public: + SPGuide *g; + int snappoint_ix; + +public: + explicit SPGuideConstraint() : + g(static_cast<SPGuide *>(nullptr)), + snappoint_ix(0) + { } + + explicit SPGuideConstraint(SPGuide *g, int snappoint_ix) : + g(g), + snappoint_ix(snappoint_ix) + { } + + bool operator==(SPGuideConstraint const &o) const { + return ( ( g == o.g ) + && ( snappoint_ix == o.snappoint_ix ) ); + } + + bool operator!=(SPGuideConstraint const &o) const { + return !( *this == o ); + } +}; + +#endif // SEEN_SP_GUIDE_CONSTRAINT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/sp-item-notify-moveto.cpp b/src/sp-item-notify-moveto.cpp new file mode 100644 index 0000000..57e44f4 --- /dev/null +++ b/src/sp-item-notify-moveto.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** \file + * Implementation of sp_item_notify_moveto(). + */ + +#include <2geom/transforms.h> +#include <vector> + +#include "sp-item-notify-moveto.h" + +#include "object/sp-guide.h" +#include "object/sp-item.h" +#include "object/sp-item-rm-unsatisfied-cns.h" + +#define return_if_fail(test) if (!(test)) { printf("WARNING: assertion '%s' failed", #test); return; } + +/** + * Called by sp_guide_moveto to indicate that the guide line corresponding to g has been moved, and + * that consequently this item should move with it. + * + * \pre exist [cn in item.constraints] g eq cn.g. + */ +void sp_item_notify_moveto(SPItem &item, SPGuide const &mv_g, int const snappoint_ix, + double const position, bool const commit) +{ + return_if_fail(SP_IS_ITEM(&item)); + return_if_fail( unsigned(snappoint_ix) < 8 ); + Geom::Point const dir( mv_g.getNormal() ); + double const dir_lensq(dot(dir, dir)); + return_if_fail( dir_lensq != 0 ); + + std::vector<Inkscape::SnapCandidatePoint> snappoints; + item.getSnappoints(snappoints, nullptr); + return_if_fail( snappoint_ix < int(snappoints.size()) ); + + double const pos0 = dot(dir, snappoints[snappoint_ix].getPoint()); + /// \todo effic: skip if mv_g is already satisfied. + + /* Translate along dir to make dot(dir, snappoints(item)[snappoint_ix]) == position. */ + + /* Calculation: + dot(dir, snappoints[snappoint_ix] + s * dir) = position. + dot(dir, snappoints[snappoint_ix]) + dot(dir, s * dir) = position. + pos0 + s * dot(dir, dir) = position. + s * lensq(dir) = position - pos0. + s = (position - pos0) / dot(dir, dir). */ + Geom::Translate const tr( ( position - pos0 ) + * ( dir / dir_lensq ) ); + item.set_i2d_affine(item.i2dt_affine() * tr); + /// \todo Reget snappoints, check satisfied. + + if (commit) { + /// \todo Consider maintaining a set of dirty items. + + /* Commit repr. */ + { + item.doWriteTransform(item.transform); + } + + sp_item_rm_unsatisfied_cns(item); +#if 0 /* nyi */ + move_cn_to_front(mv_g, snappoint_ix, item.constraints); + /** \note If the guideline is connected to multiple snappoints of + * this item, then keeping those cns in order requires that the + * guide send notifications in order of increasing importance. + */ +#endif + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/sp-item-notify-moveto.h b/src/sp-item-notify-moveto.h new file mode 100644 index 0000000..d256f39 --- /dev/null +++ b/src/sp-item-notify-moveto.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_ITEM_NOTIFY_MOVETO_H +#define SEEN_SP_ITEM_NOTIFY_MOVETO_H + +class SPItem; +class SPGuide; + +void sp_item_notify_moveto(SPItem &item, SPGuide const &g, int const snappoint_ix, + double position, bool const commit); + + +#endif // SEEN_SP_ITEM_NOTIFY_MOVETO_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/splivarot.cpp b/src/splivarot.cpp new file mode 100644 index 0000000..a6e75ef --- /dev/null +++ b/src/splivarot.cpp @@ -0,0 +1,2509 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * boolean operations and outlines + *//* + * Authors: + * see git history + * Created by fred on Fri Dec 05 2003. + * tweaked endlessly by bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * contains lots of stitched pieces of path-chemistry.c + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include <cstring> +#include <string> +#include <vector> + +#include <glib.h> +#include <glibmm/i18n.h> + +#include <2geom/svg-path-parser.h> // to get from SVG on boolean to Geom::Path +#include <2geom/svg-path-writer.h> + +#include "splivarot.h" + +#include "document-undo.h" +#include "document.h" +#include "layer-model.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "selection.h" +#include "text-editing.h" +#include "verbs.h" + +#include "display/sp-canvas.h" + +#include "helper/geom.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-marker.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "style.h" + +#include "svg/svg.h" + +#include "util/units.h" // to get abbr for document units + +#include "xml/repr-sorting.h" +#include "xml/repr.h" + +using Inkscape::DocumentUndo; + +bool Ancetre(Inkscape::XML::Node *a, Inkscape::XML::Node *who); + +void sp_selected_path_do_offset(SPDesktop *desktop, bool expand, double prefOffset); +void sp_selected_path_create_offset_object(SPDesktop *desktop, int expand, bool updating); + +bool Inkscape::ObjectSet::pathUnion(const bool skip_undo) { + BoolOpErrors result = pathBoolOp(bool_op_union, skip_undo, false, SP_VERB_SELECTION_UNION, _("Union")); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathIntersect(const bool skip_undo) +{ + BoolOpErrors result = pathBoolOp(bool_op_inters, skip_undo, false, SP_VERB_SELECTION_INTERSECT, _("Intersection")); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathDiff(const bool skip_undo) +{ + BoolOpErrors result = pathBoolOp(bool_op_diff, skip_undo, false, SP_VERB_SELECTION_DIFF, _("Difference")); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathSymDiff(const bool skip_undo) +{ + BoolOpErrors result = pathBoolOp(bool_op_symdiff, skip_undo, false, SP_VERB_SELECTION_SYMDIFF, _("Exclusion")); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathCut(const bool skip_undo) +{ + BoolOpErrors result = pathBoolOp(bool_op_cut, skip_undo, false, SP_VERB_SELECTION_CUT, _("Division")); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathSlice(const bool skip_undo) +{ + BoolOpErrors result = pathBoolOp(bool_op_slice, skip_undo, false, SP_VERB_SELECTION_SLICE, _("Cut path")); + return DONE == result; +} + +// helper for printing error messages, regardless of whether we have a GUI or not +// If desktop == NULL, errors will be shown on stderr +static void +boolop_display_error_message(SPDesktop *desktop, Glib::ustring const &msg) +{ + if (desktop) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, msg); + } else { + g_printerr("%s\n", msg.c_str()); + } +} + +// boolean operations PathVectors A,B -> PathVector result. +// This is derived from sp_selected_path_boolop +// take the source paths from the file, do the operation, delete the originals and add the results +// fra,fra are fill_rules for PathVectors a,b +Geom::PathVector +sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, fill_typ fra, fill_typ frb) +{ + + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + int nbOriginaux = 2; + std::vector<Path *> originaux(nbOriginaux); + std::vector<FillRule> origWind(nbOriginaux); + origWind[0]=fra; + origWind[1]=frb; + Geom::PathVector patht; + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + originaux[0] = Path_for_pathvector(pathv_to_linear_and_cubic_beziers( pathva)); + originaux[1] = Path_for_pathvector(pathv_to_linear_and_cubic_beziers( pathvb)); + + // some temporary instances, first + Shape *theShapeA = new Shape; + Shape *theShapeB = new Shape; + Shape *theShape = new Shape; + Path *res = new Path; + res->SetBackData(false); + Path::cut_position *toCut=nullptr; + int nbToCut=0; + + if ( bop == bool_op_inters || bop == bool_op_union || bop == bool_op_diff || bop == bool_op_symdiff ) { + // true boolean op + // get the polygons of each path, with the winding rule specified, and apply the operation iteratively + originaux[0]->ConvertWithBackData(0.1); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(0.1); + + originaux[1]->Fill(theShape, 1); + + theShapeB->ConvertToShape(theShape, origWind[1]); + + theShape->Booleen(theShapeB, theShapeA, bop); + + } else if ( bop == bool_op_cut ) { + // cuts= sort of a bastard boolean operation, thus not the axact same modus operandi + // technically, the cut path is not necessarily a polygon (thus has no winding rule) + // it is just uncrossed, and cleaned from duplicate edges and points + // then it's fed to Booleen() which will uncross it against the other path + // then comes the trick: each edge of the cut path is duplicated (one in each direction), + // thus making a polygon. the weight of the edges of the cut are all 0, but + // the Booleen need to invert the ones inside the source polygon (for the subsequent + // ConvertToForme) + + // the cut path needs to have the highest pathID in the back data + // that's how the Booleen() function knows it's an edge of the cut + + // FIXME: this gives poor results, the final paths are full of extraneous nodes. Decreasing + // ConvertWithBackData parameter below simply increases the number of nodes, so for now I + // left it at 1.0. Investigate replacing this by a combination of difference and + // intersection of the same two paths. -- bb + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(1.0); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(1.0); + + originaux[1]->Fill(theShape, 1,false,false,false); //do not closeIfNeeded + + theShapeB->ConvertToShape(theShape, fill_justDont); // fill_justDont doesn't computes winding numbers + + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bool_op_cut, 1); + + } else if ( bop == bool_op_slice ) { + // slice is not really a boolean operation + // you just put the 2 shapes in a single polygon, uncross it + // the points where the degree is > 2 are intersections + // just check it's an intersection on the path you want to cut, and keep it + // the intersections you have found are then fed to ConvertPositionsToMoveTo() which will + // make new subpath at each one of these positions + // inversion pour l'opÂŽration + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(1.0); + + originaux[0]->Fill(theShapeA, 0,false,false,false); // don't closeIfNeeded + + originaux[1]->ConvertWithBackData(1.0); + + originaux[1]->Fill(theShapeA, 1,true,false,false);// don't closeIfNeeded and just dump in the shape, don't reset it + + theShape->ConvertToShape(theShapeA, fill_justDont); + + if ( theShape->hasBackData() ) { + // should always be the case, but ya never know + { + for (int i = 0; i < theShape->numberOfPoints(); i++) { + if ( theShape->getPoint(i).totalDegree() > 2 ) { + // possibly an intersection + // we need to check that at least one edge from the source path is incident to it + // before we declare it's an intersection + int cb = theShape->getPoint(i).incidentEdge[FIRST]; + int nbOrig=0; + int nbOther=0; + int piece=-1; + float t=0.0; + while ( cb >= 0 && cb < theShape->numberOfEdges() ) { + if ( theShape->ebData[cb].pathID == 0 ) { + // the source has an edge incident to the point, get its position on the path + piece=theShape->ebData[cb].pieceID; + if ( theShape->getEdge(cb).st == i ) { + t=theShape->ebData[cb].tSt; + } else { + t=theShape->ebData[cb].tEn; + } + nbOrig++; + } + if ( theShape->ebData[cb].pathID == 1 ) nbOther++; // the cut is incident to this point + cb=theShape->NextAt(i, cb); + } + if ( nbOrig > 0 && nbOther > 0 ) { + // point incident to both path and cut: an intersection + // note that you only keep one position on the source; you could have degenerate + // cases where the source crosses itself at this point, and you wouyld miss an intersection + toCut=(Path::cut_position*)realloc(toCut, (nbToCut+1)*sizeof(Path::cut_position)); + toCut[nbToCut].piece=piece; + toCut[nbToCut].t=t; + nbToCut++; + } + } + } + } + { + // i think it's useless now + int i = theShape->numberOfEdges() - 1; + for (;i>=0;i--) { + if ( theShape->ebData[i].pathID == 1 ) { + theShape->SubEdge(i); + } + } + } + + } + } + + int* nesting=nullptr; + int* conts=nullptr; + int nbNest=0; + // pour compenser le swap juste avant + if ( bop == bool_op_slice ) { +// theShape->ConvertToForme(res, nbOriginaux, originaux, true); +// res->ConvertForcedToMoveTo(); + res->Copy(originaux[0]); + res->ConvertPositionsToMoveTo(nbToCut, toCut); // cut where you found intersections + free(toCut); + } else if ( bop == bool_op_cut ) { + // il faut appeler pour desallouer PointData (pas vital, mais bon) + // the Booleen() function did not deallocate the point_data array in theShape, because this + // function needs it. + // this function uses the point_data to get the winding number of each path (ie: is a hole or not) + // for later reconstruction in objects, you also need to extract which path is parent of holes (nesting info) + theShape->ConvertToFormeNested(res, nbOriginaux, &originaux[0], 1, nbNest, nesting, conts); + } else { + theShape->ConvertToForme(res, nbOriginaux, &originaux[0]); + } + + delete theShape; + delete theShapeA; + delete theShapeB; + delete originaux[0]; + delete originaux[1]; + + gchar *result_str = res->svg_dump_path(); + Geom::PathVector outres = Geom::parse_svg_path(result_str); + g_free(result_str); + + delete res; + return outres; +} + + +/* Convert from a livarot path to a 2geom PathVector */ +Geom::PathVector pathliv_to_pathvector(Path *pathliv){ + Geom::PathVector outres = Geom::parse_svg_path(pathliv->svg_dump_path()); + return outres; +} + + +// boolean operations on the desktop +// take the source paths from the file, do the operation, delete the originals and add the results +BoolOpErrors Inkscape::ObjectSet::pathBoolOp(bool_op bop, const bool skip_undo, const bool checked, const unsigned int verb, const Glib::ustring description) +{ + if (nullptr != desktop() && !checked) { + SPDocument *doc = desktop()->getDocument(); + // don't redraw the canvas during the operation as that can remarkably slow down the progress + desktop()->getCanvas()->_drawing_disabled = true; + BoolOpErrors returnCode = ObjectSet::pathBoolOp(bop, true, true); + desktop()->getCanvas()->_drawing_disabled = false; + + switch(returnCode) { + case ERR_TOO_LESS_PATHS_1: + boolop_display_error_message(desktop(), _("Select <b>at least 1 path</b> to perform a boolean union.")); + break; + case ERR_TOO_LESS_PATHS_2: + boolop_display_error_message(desktop(), _("Select <b>at least 2 paths</b> to perform a boolean operation.")); + break; + case ERR_NO_PATHS: + boolop_display_error_message(desktop(), _("One of the objects is <b>not a path</b>, cannot perform boolean operation.")); + break; + case ERR_Z_ORDER: + boolop_display_error_message(desktop(), _("Unable to determine the <b>z-order</b> of the objects selected for difference, XOR, division, or path cut.")); + break; + case DONE_NO_PATH: + if (!skip_undo) { + DocumentUndo::done(doc, SP_VERB_NONE, description); + } + break; + case DONE: + if (!skip_undo) { + DocumentUndo::done(doc, verb, description); + } + break; + case DONE_NO_ACTION: + // Do nothing (?) + break; + } + return returnCode; + } + + SPDocument *doc = document(); + std::vector<SPItem*> il(items().begin(), items().end()); + + // allow union on a single object for the purpose of removing self overlapse (svn log, revision 13334) + if (il.size() < 2 && bop != bool_op_union) { + return ERR_TOO_LESS_PATHS_2; + } + else if (il.size() < 1) { + return ERR_TOO_LESS_PATHS_1; + } + + g_assert(!il.empty()); + + // reverseOrderForOp marks whether the order of the list is the top->down order + // it's only used when there are 2 objects, and for operations who need to know the + // topmost object (differences, cuts) + bool reverseOrderForOp = false; + + if (bop == bool_op_diff || bop == bool_op_cut || bop == bool_op_slice) { + // check in the tree to find which element of the selection list is topmost (for 2-operand commands only) + Inkscape::XML::Node *a = il.front()->getRepr(); + Inkscape::XML::Node *b = il.back()->getRepr(); + + if (a == nullptr || b == nullptr) { + return ERR_Z_ORDER; + } + + if (Ancetre(a, b)) { + // a is the parent of b, already in the proper order + } else if (Ancetre(b, a)) { + // reverse order + reverseOrderForOp = true; + } else { + + // objects are not in parent/child relationship; + // find their lowest common ancestor + Inkscape::XML::Node *parent = LCA(a, b); + if (parent == nullptr) { + return ERR_Z_ORDER; + } + + // find the children of the LCA that lead from it to the a and b + Inkscape::XML::Node *as = AncetreFils(a, parent); + Inkscape::XML::Node *bs = AncetreFils(b, parent); + + // find out which comes first + for (Inkscape::XML::Node *child = parent->firstChild(); child; child = child->next()) { + if (child == as) { + /* a first, so reverse. */ + reverseOrderForOp = true; + break; + } + if (child == bs) + break; + } + } + } + + g_assert(!il.empty()); + + // first check if all the input objects have shapes + // otherwise bail out + for (auto item : il) + { + if (!SP_IS_SHAPE(item) && !SP_IS_TEXT(item) && !SP_IS_FLOWTEXT(item)) + { + return ERR_NO_PATHS; + } + } + + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + int nbOriginaux = il.size(); + std::vector<Path *> originaux(nbOriginaux); + std::vector<FillRule> origWind(nbOriginaux); + int curOrig; + { + curOrig = 0; + for (std::vector<SPItem*>::const_iterator l = il.begin(); l != il.end(); l++) + { + // apply live path effects prior to performing boolean operation + if (SP_IS_LPE_ITEM(*l)) { + SP_LPE_ITEM(*l)->removeAllPathEffects(true); + } + + SPCSSAttr *css = sp_repr_css_attr(reinterpret_cast<SPObject *>(il[0])->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) { + origWind[curOrig]= fill_nonZero; + } else if (val && strcmp(val, "evenodd") == 0) { + origWind[curOrig]= fill_oddEven; + } else { + origWind[curOrig]= fill_nonZero; + } + + originaux[curOrig] = Path_for_item(*l, true, true); + if (originaux[curOrig] == nullptr || originaux[curOrig]->descr_cmd.size() <= 1) + { + for (int i = curOrig; i >= 0; i--) delete originaux[i]; + return DONE_NO_ACTION; + } + curOrig++; + } + } + // reverse if needed + // note that the selection list keeps its order + if ( reverseOrderForOp ) { + std::swap(originaux[0], originaux[1]); + std::swap(origWind[0], origWind[1]); + } + + // and work + // some temporary instances, first + Shape *theShapeA = new Shape; + Shape *theShapeB = new Shape; + Shape *theShape = new Shape; + Path *res = new Path; + res->SetBackData(false); + Path::cut_position *toCut=nullptr; + int nbToCut=0; + + if ( bop == bool_op_inters || bop == bool_op_union || bop == bool_op_diff || bop == bool_op_symdiff ) { + // true boolean op + // get the polygons of each path, with the winding rule specified, and apply the operation iteratively + originaux[0]->ConvertWithBackData(0.1); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + curOrig = 1; + for (std::vector<SPItem*>::const_iterator l = il.begin(); l != il.end(); l++){ + if(*l==il[0])continue; + originaux[curOrig]->ConvertWithBackData(0.1); + + originaux[curOrig]->Fill(theShape, curOrig); + + theShapeB->ConvertToShape(theShape, origWind[curOrig]); + + /* Due to quantization of the input shape coordinates, we may end up with A or B being empty. + * If this is a union or symdiff operation, we just use the non-empty shape as the result: + * A=0 => (0 or B) == B + * B=0 => (A or 0) == A + * A=0 => (0 xor B) == B + * B=0 => (A xor 0) == A + * If this is an intersection operation, we just use the empty shape as the result: + * A=0 => (0 and B) == 0 == A + * B=0 => (A and 0) == 0 == B + * If this a difference operation, and the upper shape (A) is empty, we keep B. + * If the lower shape (B) is empty, we still keep B, as it's empty: + * A=0 => (B - 0) == B + * B=0 => (0 - A) == 0 == B + * + * In any case, the output from this operation is stored in shape A, so we may apply + * the above rules simply by judicious use of swapping A and B where necessary. + */ + bool zeroA = theShapeA->numberOfEdges() == 0; + bool zeroB = theShapeB->numberOfEdges() == 0; + if (zeroA || zeroB) { + // We might need to do a swap. Apply the above rules depending on operation type. + bool resultIsB = ((bop == bool_op_union || bop == bool_op_symdiff) && zeroA) + || ((bop == bool_op_inters) && zeroB) + || (bop == bool_op_diff); + if (resultIsB) { + // Swap A and B to use B as the result + Shape *swap = theShapeB; + theShapeB = theShapeA; + theShapeA = swap; + } + } else { + // Just do the Boolean operation as usual + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bop); + Shape *swap = theShape; + theShape = theShapeA; + theShapeA = swap; + } + curOrig++; + } + + { + Shape *swap = theShape; + theShape = theShapeA; + theShapeA = swap; + } + + } else if ( bop == bool_op_cut ) { + // cuts= sort of a bastard boolean operation, thus not the axact same modus operandi + // technically, the cut path is not necessarily a polygon (thus has no winding rule) + // it is just uncrossed, and cleaned from duplicate edges and points + // then it's fed to Booleen() which will uncross it against the other path + // then comes the trick: each edge of the cut path is duplicated (one in each direction), + // thus making a polygon. the weight of the edges of the cut are all 0, but + // the Booleen need to invert the ones inside the source polygon (for the subsequent + // ConvertToForme) + + // the cut path needs to have the highest pathID in the back data + // that's how the Booleen() function knows it's an edge of the cut + + // FIXME: this gives poor results, the final paths are full of extraneous nodes. Decreasing + // ConvertWithBackData parameter below simply increases the number of nodes, so for now I + // left it at 1.0. Investigate replacing this by a combination of difference and + // intersection of the same two paths. -- bb + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(1.0); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(1.0); + + if ((originaux[1]->pts.size() == 2) && originaux[1]->pts[0].isMoveTo && !originaux[1]->pts[1].isMoveTo) + originaux[1]->Fill(theShape, 1,false,true,false); // see LP Bug 177956 + else + originaux[1]->Fill(theShape, 1,false,false,false); //do not closeIfNeeded + + theShapeB->ConvertToShape(theShape, fill_justDont); // fill_justDont doesn't computes winding numbers + + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bool_op_cut, 1); + + } else if ( bop == bool_op_slice ) { + // slice is not really a boolean operation + // you just put the 2 shapes in a single polygon, uncross it + // the points where the degree is > 2 are intersections + // just check it's an intersection on the path you want to cut, and keep it + // the intersections you have found are then fed to ConvertPositionsToMoveTo() which will + // make new subpath at each one of these positions + // inversion pour l'opÂŽration + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(1.0); + + originaux[0]->Fill(theShapeA, 0,false,false,false); // don't closeIfNeeded + + originaux[1]->ConvertWithBackData(1.0); + + originaux[1]->Fill(theShapeA, 1,true,false,false);// don't closeIfNeeded and just dump in the shape, don't reset it + + theShape->ConvertToShape(theShapeA, fill_justDont); + + if ( theShape->hasBackData() ) { + // should always be the case, but ya never know + { + for (int i = 0; i < theShape->numberOfPoints(); i++) { + if ( theShape->getPoint(i).totalDegree() > 2 ) { + // possibly an intersection + // we need to check that at least one edge from the source path is incident to it + // before we declare it's an intersection + int cb = theShape->getPoint(i).incidentEdge[FIRST]; + int nbOrig=0; + int nbOther=0; + int piece=-1; + float t=0.0; + while ( cb >= 0 && cb < theShape->numberOfEdges() ) { + if ( theShape->ebData[cb].pathID == 0 ) { + // the source has an edge incident to the point, get its position on the path + piece=theShape->ebData[cb].pieceID; + if ( theShape->getEdge(cb).st == i ) { + t=theShape->ebData[cb].tSt; + } else { + t=theShape->ebData[cb].tEn; + } + nbOrig++; + } + if ( theShape->ebData[cb].pathID == 1 ) nbOther++; // the cut is incident to this point + cb=theShape->NextAt(i, cb); + } + if ( nbOrig > 0 && nbOther > 0 ) { + // point incident to both path and cut: an intersection + // note that you only keep one position on the source; you could have degenerate + // cases where the source crosses itself at this point, and you wouyld miss an intersection + toCut=(Path::cut_position*)realloc(toCut, (nbToCut+1)*sizeof(Path::cut_position)); + toCut[nbToCut].piece=piece; + toCut[nbToCut].t=t; + nbToCut++; + } + } + } + } + { + // i think it's useless now + int i = theShape->numberOfEdges() - 1; + for (;i>=0;i--) { + if ( theShape->ebData[i].pathID == 1 ) { + theShape->SubEdge(i); + } + } + } + + } + } + + int* nesting=nullptr; + int* conts=nullptr; + int nbNest=0; + // pour compenser le swap juste avant + if ( bop == bool_op_slice ) { +// theShape->ConvertToForme(res, nbOriginaux, originaux, true); +// res->ConvertForcedToMoveTo(); + res->Copy(originaux[0]); + res->ConvertPositionsToMoveTo(nbToCut, toCut); // cut where you found intersections + free(toCut); + } else if ( bop == bool_op_cut ) { + // il faut appeler pour desallouer PointData (pas vital, mais bon) + // the Booleen() function did not deallocate the point_data array in theShape, because this + // function needs it. + // this function uses the point_data to get the winding number of each path (ie: is a hole or not) + // for later reconstruction in objects, you also need to extract which path is parent of holes (nesting info) + theShape->ConvertToFormeNested(res, nbOriginaux, &originaux[0], 1, nbNest, nesting, conts); + } else { + theShape->ConvertToForme(res, nbOriginaux, &originaux[0]); + } + + delete theShape; + delete theShapeA; + delete theShapeB; + for (int i = 0; i < nbOriginaux; i++) delete originaux[i]; + + if (res->descr_cmd.size() <= 1) + { + // only one command, presumably a moveto: it isn't a path + for (auto l : il){ + l->deleteObject(); + } + clear(); + + delete res; + return DONE_NO_PATH; + } + + // get the source path object + SPObject *source; + if ( bop == bool_op_diff || bop == bool_op_cut || bop == bool_op_slice ) { + if (reverseOrderForOp) { + source = il[0]; + } else { + source = il.back(); + } + } else { + // find out the bottom object + std::vector<Inkscape::XML::Node*> sorted(xmlNodes().begin(), xmlNodes().end()); + + sort(sorted.begin(),sorted.end(),sp_repr_compare_position_bool); + + source = doc->getObjectByRepr(sorted.front()); + } + + // adjust style properties that depend on a possible transform in the source object in order + // to get a correct style attribute for the new path + SPItem* item_source = SP_ITEM(source); + Geom::Affine i2doc(item_source->i2doc_affine()); + item_source->adjust_stroke(i2doc.descrim()); + item_source->adjust_pattern(i2doc); + item_source->adjust_gradient(i2doc); + + Inkscape::XML::Node *repr_source = source->getRepr(); + + // remember important aspects of the source path, to be restored + gint pos = repr_source->position(); + Inkscape::XML::Node *parent = repr_source->parent(); + gchar const *id = repr_source->attribute("id"); + // remove source paths + clear(); + for (auto l : il){ + if (l != item_source) { + // delete the object for real, so that its clones can take appropriate action + l->deleteObject(); + } + } + + // premultiply by the inverse of parent's repr + SPItem *parent_item = SP_ITEM(doc->getObjectByRepr(parent)); + Geom::Affine local (parent_item->i2doc_affine()); + gchar *transform = sp_svg_transform_write(local.inverse()); + + // now that we have the result, add it on the canvas + if ( bop == bool_op_cut || bop == bool_op_slice ) { + int nbRP=0; + Path** resPath; + if ( bop == bool_op_slice ) { + // there are moveto's at each intersection, but it's still one unique path + // so break it down and add each subpath independently + // we could call break_apart to do this, but while we have the description... + resPath=res->SubPaths(nbRP, false); + } else { + // cut operation is a bit wicked: you need to keep holes + // that's why you needed the nesting + // ConvertToFormeNested() dumped all the subpath in a single Path "res", so we need + // to get the path for each part of the polygon. that's why you need the nesting info: + // to know in which subpath to add a subpath + resPath=res->SubPathsWithNesting(nbRP, true, nbNest, nesting, conts); + + // cleaning + if ( conts ) free(conts); + if ( nesting ) free(nesting); + } + + // add all the pieces resulting from cut or slice + std::vector <Inkscape::XML::Node*> selection; + for (int i=0;i<nbRP;i++) { + gchar *d = resPath[i]->svg_dump_path(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, repr_source); + + // Delete source on last iteration (after we don't need repr_source anymore). As a consequence, the last + // item will inherit the original's id. + if (i + 1 == nbRP) { + item_source->deleteObject(false); + } + + repr->setAttribute("d", d); + g_free(d); + + // for slice, remove fill + if (bop == bool_op_slice) { + SPCSSAttr *css; + + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", "none"); + + sp_repr_css_change(repr, css, "style"); + + sp_repr_css_attr_unref(css); + } + + repr->setAttribute("transform", transform); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + selection.push_back(repr); + Inkscape::GC::release(repr); + + delete resPath[i]; + } + setReprList(selection); + if ( resPath ) free(resPath); + + } else { + gchar *d = res->svg_dump_path(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, repr_source); + + // delete it so that its clones don't get alerted; this object will be restored shortly, with the same id + item_source->deleteObject(false); + + repr->setAttribute("d", d); + g_free(d); + + repr->setAttribute("transform", transform); + + parent->addChildAtPos(repr, pos); + + set(repr); + Inkscape::GC::release(repr); + } + + g_free(transform); + + delete res; + + return DONE; +} + +static +void sp_selected_path_outline_add_marker( SPItem *context, SPObject *marker_object, Geom::Affine marker_transform, + Geom::Scale stroke_scale, Geom::Affine transform, + Inkscape::XML::Node *g_repr, Inkscape::XML::Document *xml_doc, SPDocument * doc, + SPDesktop *desktop , bool legacy) +{ + SPMarker* marker = SP_MARKER (marker_object); + SPItem* marker_item = sp_item_first_item_child(marker_object); + if (!marker_item) { + return; + } + + Geom::Affine tr(marker_transform); + + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + tr = stroke_scale * tr; + } + + // total marker transform + tr = marker_item->transform * marker->c2p * tr * transform; + + if (marker_item->getRepr()) { + Inkscape::XML::Node *m_repr = marker_item->getRepr()->duplicate(xml_doc); + g_repr->addChildAtPos(m_repr, 0); + SPItem *marker_item = (SPItem *) doc->getObjectByRepr(m_repr); + marker_item->doWriteTransform(tr); + if (!legacy) { + sp_item_path_outline(marker_item, desktop, legacy, context); + } + } +} + +static +void item_outline_add_marker_child( SPItem const *item, Geom::Affine marker_transform, Geom::PathVector* pathv_in ) +{ + Geom::Affine tr(marker_transform); + tr = item->transform * tr; + + // note: a marker child item can be an item group! + if (SP_IS_GROUP(item)) { + // recurse through all childs: + for (auto& o: item->children) { + if ( SP_IS_ITEM(&o) ) { + item_outline_add_marker_child(SP_ITEM(&o), tr, pathv_in); + } + } + } else { + Geom::PathVector* marker_pathv = item_outline(item); + + if (marker_pathv) { + for (const auto & j : *marker_pathv) { + pathv_in->push_back(j * tr); + } + delete marker_pathv; + } + } +} + +static +void item_outline_add_marker( SPObject const *marker_object, Geom::Affine marker_transform, + Geom::Scale stroke_scale, Geom::PathVector* pathv_in ) +{ + SPMarker const * marker = SP_MARKER(marker_object); + + Geom::Affine tr(marker_transform); + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + tr = stroke_scale * tr; + } + // total marker transform + tr = marker->c2p * tr; + + SPItem const * marker_item = sp_item_first_item_child(marker_object); // why only consider the first item? can a marker only consist of a single item (that may be a group)? + if (marker_item) { + item_outline_add_marker_child(marker_item, tr, pathv_in); + } +} + +/** + * Returns a pathvector that is the outline of the stroked item, with markers. + * item must be SPShape or SPText. + */ +Geom::PathVector* item_outline(SPItem const *item, bool bbox_only) +{ + Geom::PathVector *ret_pathv = nullptr; + + if (!SP_IS_SHAPE(item) && !SP_IS_TEXT(item)) { + return ret_pathv; + } + + // no stroke: no outline + if (!item->style || item->style->stroke.noneSet) { + return ret_pathv; + } + + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(item)) { + curve = SP_SHAPE(item)->getCurve(); + } else if (SP_IS_TEXT(item)) { + curve = SP_TEXT(item)->getNormalizedBpath(); + } + if (curve == nullptr) { + return ret_pathv; + } + + if (curve->get_pathvector().empty()) { + return ret_pathv; + } + + // remember old stroke style, to be set on fill + SPStyle *i_style = item->style; + + Geom::Affine const transform(item->transform); + float const scale = transform.descrim(); + + float o_width = i_style->stroke_width.computed; + if (o_width < Geom::EPSILON) { + // This may result in rounding errors for very small stroke widths (happens e.g. when user unit is large). + // See bug lp:1244861 + o_width = Geom::EPSILON; + } + float o_miter = i_style->stroke_miterlimit.value * o_width; + + JoinType o_join; + ButtType o_butt; + { + switch (i_style->stroke_linejoin.computed) { + case SP_STROKE_LINEJOIN_MITER: + o_join = join_pointy; + break; + case SP_STROKE_LINEJOIN_ROUND: + o_join = join_round; + break; + default: + o_join = join_straight; + break; + } + + switch (i_style->stroke_linecap.computed) { + case SP_STROKE_LINECAP_SQUARE: + o_butt = butt_square; + break; + case SP_STROKE_LINECAP_ROUND: + o_butt = butt_round; + break; + default: + o_butt = butt_straight; + break; + } + } + + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers( curve->get_pathvector() ); + + Path *orig = new Path; + orig->LoadPathVector(pathv); + + Path *res = new Path; + res->SetBackData(false); + + if (!i_style->stroke_dasharray.values.empty()) { + double size = Geom::L2(Geom::bounds_fast(pathv)->dimensions()); + orig->ConvertWithBackData(0.005); + + orig->DashPolylineFromStyle(i_style, scale, 0); + orig->Simplify(size * 0.00005); + } + orig->Outline(res, 0.5 * o_width, o_join, o_butt, 0.5 * o_miter); + + if (!bbox_only) { + orig->Coalesce(0.5 * o_width); + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + res->ConvertWithBackData(1.0); + res->Fill(theShape, 0); + theRes->ConvertToShape(theShape, fill_positive); + + Path *originaux[1]; + originaux[0] = res; + theRes->ConvertToForme(orig, 1, originaux); + + delete theShape; + delete theRes; + } + + if (orig->descr_cmd.size() <= 1) { + // ca a merdÂŽ, ou bien le resultat est vide + delete res; + delete orig; + curve->unref(); + return ret_pathv; + } + + + if (res->descr_cmd.size() > 1) { // if there's 0 or 1 node left, drop this path altogether + ret_pathv = bbox_only ? res->MakePathVector() : orig->MakePathVector(); + + if (SP_IS_SHAPE(item) && SP_SHAPE(item)->hasMarkers() && !bbox_only) { + SPShape *shape = SP_SHAPE(item); + + Geom::PathVector const & pathv = curve->get_pathvector(); + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( SPObject *marker_obj = shape->_marker[i] ) { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(pathv.front().front())); + item_outline_add_marker( marker_obj, m, + Geom::Scale(i_style->stroke_width.computed), + ret_pathv ); + } + } + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + SPObject *midmarker_obj = shape->_marker[i]; + if (!midmarker_obj) continue; + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(path_it->front())); + item_outline_add_marker( midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), + ret_pathv ); + } + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment + */ + Geom::Affine const m (sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + item_outline_add_marker( midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), + ret_pathv); + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_outline_add_marker( midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), + ret_pathv ); + } + } + } + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( SPObject *marker_obj = shape->_marker[i] ) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_outline_add_marker( marker_obj, m, + Geom::Scale(i_style->stroke_width.computed), + ret_pathv ); + } + } + } + + curve->unref(); + } + + delete res; + delete orig; + + return ret_pathv; +} + +bool +sp_item_path_outline(SPItem *item, SPDesktop *desktop, bool legacy, SPItem *context) +{ + char const *id = item->getRepr()->attribute("id"); + SPDocument * document = item->document; + bool did = false; + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument * doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + SPLPEItem *lpeitem = SP_LPE_ITEM(item); + if (lpeitem) { + lpeitem->removeAllPathEffects(true); + SPObject *elemref = document->getObjectById(id); + if (elemref && elemref != item) { + // If the LPE item is a shape, it is converted to a path + // so we need to reupdate the item + item = dynamic_cast<SPItem *>(elemref); + } + } + + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + if (legacy) { + return false; + } + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + for (auto subitem : item_list) { + sp_item_path_outline(subitem, desktop, legacy); + } + } else { + if (!SP_IS_SHAPE(item) && !SP_IS_TEXT(item)) + return did; + + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(item)) { + curve = SP_SHAPE(item)->getCurve(); + if (curve == nullptr) + return did; + } + if (SP_IS_TEXT(item)) { + curve = SP_TEXT(item)->getNormalizedBpath(); + if (curve == nullptr) + return did; + } + + g_assert(curve != nullptr); + + if (curve->get_pathvector().empty()) { + return did; + } + + // pas de stroke pas de chocolat + if (!item->style) { + curve->unref(); + return did; + } + + // remember old stroke style, to be set on fill + SPStyle *i_style = item->style; + //Stroke - and markers + // Copying stroke style to fill will fail for properties not defined by style attribute + // (i.e., properties defined in style sheet or by attributes). + + // Stroke + SPCSSAttr *ncss = sp_css_attr_from_style(i_style, SP_STYLE_FLAG_ALWAYS); + SPCSSAttr *ncsf = sp_css_attr_from_style(i_style, SP_STYLE_FLAG_ALWAYS); + if (context) { + SPStyle *context_style = context->style; + SPCSSAttr *ctxt_style = sp_css_attr_from_style(context_style, SP_STYLE_FLAG_ALWAYS); + // TODO: browsers has diferent behabiours with context on markers + // we need to revisit in the future for best matching + // also dont know if opacity is or want to be included in context + gchar const *s_val = sp_repr_css_property(ctxt_style, "stroke", nullptr); + gchar const *f_val = sp_repr_css_property(ctxt_style, "fill", nullptr); + if (i_style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE || + i_style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + { + gchar const *fill_value = (i_style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) ? s_val : f_val; + sp_repr_css_set_property(ncss, "fill", fill_value); + sp_repr_css_set_property(ncsf, "fill", fill_value); + } + if (i_style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE || + i_style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + { + gchar const *stroke_value = (i_style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) ? f_val : s_val; + sp_repr_css_set_property(ncss, "stroke", stroke_value); + sp_repr_css_set_property(ncsf, "stroke", stroke_value); + } + } + gchar const *s_val = sp_repr_css_property(ncss, "stroke", nullptr); + gchar const *s_opac = sp_repr_css_property(ncss, "stroke-opacity", nullptr); + gchar const *f_val = sp_repr_css_property(ncss, "fill", nullptr); + gchar const *opacity = sp_repr_css_property(ncss, "opacity", nullptr); + gchar const *filter = sp_repr_css_property(ncss, "filter", nullptr); + sp_repr_css_set_property(ncss, "stroke", "none"); + sp_repr_css_set_property(ncss, "stroke-width", nullptr); + sp_repr_css_set_property(ncss, "stroke-opacity", "1.0"); + sp_repr_css_set_property(ncss, "filter", nullptr); + sp_repr_css_set_property(ncss, "opacity", nullptr); + sp_repr_css_set_property(ncss, "fill", s_val); + if ( s_opac ) { + sp_repr_css_set_property(ncss, "fill-opacity", s_opac); + } else { + sp_repr_css_set_property(ncss, "fill-opacity", "1.0"); + } + sp_repr_css_unset_property(ncss, "marker-start"); + sp_repr_css_unset_property(ncss, "marker-mid"); + sp_repr_css_unset_property(ncss, "marker-end"); + + // Fill + sp_repr_css_set_property(ncsf, "stroke", "none"); + sp_repr_css_set_property(ncsf, "stroke-opacity", "1.0"); + sp_repr_css_set_property(ncss, "stroke-width", nullptr); + sp_repr_css_set_property(ncsf, "filter", nullptr); + sp_repr_css_set_property(ncsf, "opacity", nullptr); + sp_repr_css_unset_property(ncsf, "marker-start"); + sp_repr_css_unset_property(ncsf, "marker-mid"); + sp_repr_css_unset_property(ncsf, "marker-end"); + + Geom::Affine const transform(item->transform); + float const scale = transform.descrim(); + + Path *orig = new Path; + Path *res = new Path; + SPCurve *curvetemp = curve_for_item(item); + if (curvetemp == nullptr) { + curve->unref(); + return did; + } + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers( curvetemp->get_pathvector() ); + curvetemp->unref(); + if ( !item->style->stroke.noneSet ) { + float o_width, o_miter; + JoinType o_join; + ButtType o_butt; + + { + int jointype, captype; + + jointype = i_style->stroke_linejoin.computed; + captype = i_style->stroke_linecap.computed; + o_width = i_style->stroke_width.computed; + + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + o_join = join_pointy; + break; + case SP_STROKE_LINEJOIN_ROUND: + o_join = join_round; + break; + default: + o_join = join_straight; + break; + } + + switch (captype) { + case SP_STROKE_LINECAP_SQUARE: + o_butt = butt_square; + break; + case SP_STROKE_LINECAP_ROUND: + o_butt = butt_round; + break; + default: + o_butt = butt_straight; + break; + } + + if (o_width < 0.032) + o_width = 0.032; + o_miter = i_style->stroke_miterlimit.value * o_width; + } + + + + orig->LoadPathVector(pathv); + res->SetBackData(false); + + if (!i_style->stroke_dasharray.values.empty()) { + double size = Geom::L2(Geom::bounds_fast(pathv)->dimensions()); + orig->ConvertWithBackData(0.005); + + orig->DashPolylineFromStyle(i_style, scale, 0); + orig->Simplify(size * 0.00005); + } + orig->Outline(res, 0.5 * o_width, o_join, o_butt, 0.5 * o_miter); + orig->Coalesce(0.5 * o_width); + + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + res->ConvertWithBackData(1.0); + res->Fill(theShape, 0); + theRes->ConvertToShape(theShape, fill_positive); + + Path *originaux[1]; + originaux[0] = res; + theRes->ConvertToForme(orig, 1, originaux); + + delete theShape; + delete theRes; + + if (orig->descr_cmd.size() <= 1) { + // ca a merdÂŽ, ou bien le resultat est vide + delete res; + delete orig; + return did; + } + } + // remember the position of the item + gint pos = item->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + if (res->descr_cmd.size() > 1) { // if there's 0 or 1 node left, drop this path altogether + + //The stroke + Inkscape::XML::Node *stroke = nullptr; + if( s_val && !item->style->stroke.noneSet ){ + SPDocument * doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + stroke = xml_doc->createElement("svg:path"); + + // restore old style, but set old stroke style on fill + sp_repr_css_change(stroke, ncss, "style"); + + sp_repr_css_attr_unref(ncss); + + gchar *str = orig->svg_dump_path(); + stroke->setAttribute("d", str); + g_free(str); + } + + if (SP_IS_SHAPE(item)) { + Inkscape::XML::Node *g_repr = xml_doc->createElement("svg:g"); + Inkscape::copy_object_properties(g_repr, item->getRepr()); + // drop copied style, children will be re-styled (stroke becomes fill) + g_repr->removeAttribute("style"); + + // add the group to the parent + // move to the saved position + parent->addChildAtPos(g_repr, pos); + + //The fill + Inkscape::XML::Node *fill = nullptr; + //gchar const *f_val = sp_repr_css_property(ncsf, "fill", NULL); + if(f_val && !item->style->fill.noneSet && !legacy){ + fill = xml_doc->createElement("svg:path"); + sp_repr_css_change(fill, ncsf, "style"); + + sp_repr_css_attr_unref(ncsf); + + gchar *str = sp_svg_write_path( pathv ); + fill->setAttribute("d", str); + g_free(str); + } + // restore transform + SPItem *newitem = (SPItem *) doc->getObjectByRepr(g_repr); + newitem->doWriteTransform(transform); + SPShape *shape = SP_SHAPE(item); + + Geom::PathVector const & pathv = curve->get_pathvector(); + Inkscape::XML::Node *markers = nullptr; + if(SP_SHAPE(item)->hasMarkers ()) { + if (!legacy) { + markers = xml_doc->createElement("svg:g"); + g_repr->addChildAtPos(markers, pos); + } else { + markers = g_repr; + } + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( SPObject *marker_obj = shape->_marker[i] ) { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(pathv.front().front())); + sp_selected_path_outline_add_marker( item, marker_obj, m, + Geom::Scale(i_style->stroke_width.computed), transform, + markers, xml_doc, doc, desktop, legacy); + } + } + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + SPObject *midmarker_obj = shape->_marker[i]; + if (!midmarker_obj) continue; + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(path_it->front())); + sp_selected_path_outline_add_marker( item, midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), transform, + markers, xml_doc, doc, desktop, legacy); + } + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment + */ + Geom::Affine const m (sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + sp_selected_path_outline_add_marker( item, midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), transform, + markers, xml_doc, doc, desktop, legacy); + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + sp_selected_path_outline_add_marker( item, midmarker_obj, m, + Geom::Scale(i_style->stroke_width.computed), transform, + markers, xml_doc, doc, desktop, legacy); + } + } + } + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( SPObject *marker_obj = shape->_marker[i] ) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + sp_selected_path_outline_add_marker( item, marker_obj, m, + Geom::Scale(i_style->stroke_width.computed), transform, + markers, xml_doc, doc, desktop, legacy); + } + } + } + + gchar const *paint_order = sp_repr_css_property(ncss, "paint-order", nullptr); + SPIPaintOrder temp; + temp.read( paint_order ); + bool unique = false; + if ((!fill && !markers) || (!fill && !stroke) || (!markers && !stroke)) { + unique = true; + } + if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL && !legacy && !unique) { + + if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(2); + } + } else { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( markers ) { + markers->setPosition(1); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + } + } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) { + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( fill ) { + g_repr->appendChild(fill); + } + if ( markers ) { + markers->setPosition(2); + } + } else { + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(1); + } + if ( fill ) { + g_repr->appendChild(fill); + } + } + } else { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + if ( markers ) { + markers->setPosition(0); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( fill ) { + g_repr->appendChild(fill); + } + } else { + if ( markers ) { + markers->setPosition(0); + } + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + } + } + + } else if (!unique) { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(2); + } + } + if( fill || stroke || markers ) { + did = true; + } + + Inkscape::XML::Node *out = nullptr; + if (!fill && !markers && did) { + out = stroke; + } else if (!fill && !stroke && did) { + out = markers; + } else if (!markers && !stroke && did) { + out = fill; + } else if(did) { + out = g_repr; + } + + SPCSSAttr *r_style = sp_repr_css_attr_new(); + sp_repr_css_set_property(r_style, "opacity", opacity); + sp_repr_css_set_property(r_style, "filter", filter); + sp_repr_css_change(out, r_style, "style"); + + sp_repr_css_attr_unref(r_style); + if (unique) { + g_assert(out != g_repr); + parent->addChild(out, g_repr); + parent->removeChild(g_repr); + } + out->setAttribute("transform", item->getRepr()->attribute("transform")); + //bug lp:1290573 : completely destroy the old object first + curve->unref(); + //Check for recursive markers to path + if (did) { + if( selection->includes(item) ){ + selection->remove(item); + item->deleteObject(false); + selection->add(out); + } else { + item->deleteObject(false); + } + out->setAttribute("id", id); + Inkscape::GC::release(out); + } + } + } + delete res; + delete orig; + } + return did; +} + +void +sp_selected_path_outline(SPDesktop *desktop, bool legacy) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>stroked path(s)</b> to convert stroke to path.")); + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/pathoperationsunlink/value", true)) { + selection->unlinkRecursive(true); + } + + bool scale_stroke = prefs->getBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/stroke", true); + bool did = false; + std::vector<SPItem*> il(selection->items().begin(), selection->items().end()); + for (auto item : il){ + did = sp_item_path_outline(item, desktop, legacy); + } + + prefs->setBool("/options/transform/stroke", scale_stroke); + if (did) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_SELECTION_OUTLINE, + _("Convert stroke to path")); + } else { + // TRANSLATORS: "to outline" means "to convert stroke to path" + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No stroked paths</b> in the selection.")); + return; + } +} + + +void +sp_selected_path_offset(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double prefOffset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + sp_selected_path_do_offset(desktop, true, prefOffset); +} +void +sp_selected_path_inset(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double prefOffset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + sp_selected_path_do_offset(desktop, false, prefOffset); +} + +void +sp_selected_path_offset_screen(SPDesktop *desktop, double pixels) +{ + sp_selected_path_do_offset(desktop, true, pixels / desktop->current_zoom()); +} + +void +sp_selected_path_inset_screen(SPDesktop *desktop, double pixels) +{ + sp_selected_path_do_offset(desktop, false, pixels / desktop->current_zoom()); +} + + +void sp_selected_path_create_offset_object_zero(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 0, false); +} + +void sp_selected_path_create_offset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 1, false); +} +void sp_selected_path_create_inset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, -1, false); +} + +void sp_selected_path_create_updating_offset_object_zero(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 0, true); +} + +void sp_selected_path_create_updating_offset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 1, true); +} +void sp_selected_path_create_updating_inset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, -1, true); +} + +void sp_selected_path_create_offset_object(SPDesktop *desktop, int expand, bool updating) +{ + SPCurve *curve = nullptr; + Inkscape::Selection *selection = desktop->getSelection(); + SPItem *item = selection->singleItem(); + + if (item == nullptr || ( !SP_IS_SHAPE(item) && !SP_IS_TEXT(item) ) ) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Selected object is <b>not a path</b>, cannot inset/outset.")); + return; + } + else if (SP_IS_SHAPE(item)) + { + curve = SP_SHAPE(item)->getCurve(); + } + else // Item must be SP_TEXT + { + curve = SP_TEXT(item)->getNormalizedBpath(); + } + + if (curve == nullptr) { + return; + } + + Geom::Affine const transform(item->transform); + auto scaling_factor = item->i2doc_affine().descrim(); + + item->doWriteTransform(Geom::identity()); + + // remember the position of the item + gint pos = item->getRepr()->position(); + + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + float o_width = 0; + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + o_width = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + o_width /= scaling_factor; + + if (scaling_factor == 0 || o_width < 0.01) { + o_width = 0.01; + } + } + + Path *orig = Path_for_item(item, true, false); + if (orig == nullptr) + { + curve->unref(); + return; + } + + Path *res = new Path; + res->SetBackData(false); + + { + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData(1.0); + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + else if (val && strcmp(val, "evenodd") == 0) + { + theRes->ConvertToShape(theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + Path *originaux[1]; + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + delete theShape; + delete theRes; + } + + curve->unref(); + + if (res->descr_cmd.size() <= 1) + { + // pas vraiment de points sur le resultat + // donc il ne reste rien + DocumentUndo::done(desktop->getDocument(), + (updating ? SP_VERB_SELECTION_LINKED_OFFSET + : SP_VERB_SELECTION_DYNAMIC_OFFSET), + (updating ? _("Create linked offset") + : _("Create dynamic offset"))); + selection->clear(); + + delete res; + delete orig; + return; + } + + { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + if (!updating) { + Inkscape::copy_object_properties(repr, item->getRepr()); + } else { + gchar const *style = item->getRepr()->attribute("style"); + repr->setAttribute("style", style); + } + + repr->setAttribute("sodipodi:type", "inkscape:offset"); + sp_repr_set_svg_double(repr, "inkscape:radius", ( expand > 0 + ? o_width + : expand < 0 + ? -o_width + : 0 )); + + gchar *str = res->svg_dump_path(); + repr->setAttribute("inkscape:original", str); + g_free(str); + str = nullptr; + + if ( updating ) { + + //XML Tree being used directly here while it shouldn't be + item->doWriteTransform(transform); + char const *id = item->getRepr()->attribute("id"); + char const *uri = g_strdup_printf("#%s", id); + repr->setAttribute("xlink:href", uri); + g_free((void *) uri); + } else { + repr->removeAttribute("inkscape:href"); + // delete original + item->deleteObject(false); + } + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + SPItem *nitem = reinterpret_cast<SPItem *>(desktop->getDocument()->getObjectByRepr(repr)); + + if ( !updating ) { + // apply the transform to the offset + nitem->doWriteTransform(transform); + } + + // The object just created from a temporary repr is only a seed. + // We need to invoke its write which will update its real repr (in particular adding d=) + nitem->updateRepr(); + + Inkscape::GC::release(repr); + + selection->set(nitem); + } + + DocumentUndo::done(desktop->getDocument(), + (updating ? SP_VERB_SELECTION_LINKED_OFFSET + : SP_VERB_SELECTION_DYNAMIC_OFFSET), + (updating ? _("Create linked offset") + : _("Create dynamic offset"))); + + delete res; + delete orig; +} + + + + + + + + + + + +/** + * Apply offset to selected paths + * @param desktop Targetted desktop + * @param expand True if offset expands, False if it shrinks paths + * @param prefOffset Size of offset in pixels + */ +void +sp_selected_path_do_offset(SPDesktop *desktop, bool expand, double prefOffset) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>path(s)</b> to inset/outset.")); + return; + } + + bool did = false; + std::vector<SPItem*> il(selection->items().begin(), selection->items().end()); + for (auto item : il){ + SPCurve *curve = nullptr; + + if (!SP_IS_SHAPE(item) && !SP_IS_TEXT(item) && !SP_IS_FLOWTEXT(item)) + continue; + else if (SP_IS_SHAPE(item)) { + curve = SP_SHAPE(item)->getCurve(); + } + else if (SP_IS_FLOWTEXT(item)) { + curve = SP_FLOWTEXT(item)->getNormalizedBpath(); + } + else { // Item must be SP_TEXT + curve = SP_TEXT(item)->getNormalizedBpath(); + } + + if (curve == nullptr) + continue; + + Geom::Affine const transform(item->transform); + auto scaling_factor = item->i2doc_affine().descrim(); + + item->doWriteTransform(Geom::identity()); + + float o_width = 0; + float o_miter = 0; + JoinType o_join = join_straight; + //ButtType o_butt = butt_straight; + + { + SPStyle *i_style = item->style; + int jointype = i_style->stroke_linejoin.value; + + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + o_join = join_pointy; + break; + case SP_STROKE_LINEJOIN_ROUND: + o_join = join_round; + break; + default: + o_join = join_straight; + break; + } + + // scale to account for transforms and document units + o_width = prefOffset / scaling_factor; + + if (scaling_factor == 0 || o_width < 0.01) { + o_width = 0.01; + } + o_miter = i_style->stroke_miterlimit.value * o_width; + } + + Path *orig = Path_for_item(item, false); + if (orig == nullptr) { + curve->unref(); + continue; + } + + Path *res = new Path; + res->SetBackData(false); + + { + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData(0.03); + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + else if (val && strcmp(val, "evenodd") == 0) + { + theRes->ConvertToShape(theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + // et maintenant: offset + // methode inexacte +/* Path *originaux[1]; + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + if (expand) { + res->OutsideOutline(orig, 0.5 * o_width, o_join, o_butt, o_miter); + } else { + res->OutsideOutline(orig, -0.5 * o_width, o_join, o_butt, o_miter); + } + + orig->ConvertWithBackData(1.0); + orig->Fill(theShape, 0); + theRes->ConvertToShape(theShape, fill_positive); + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + if (o_width >= 0.5) { + // res->Coalesce(1.0); + res->ConvertEvenLines(1.0); + res->Simplify(1.0); + } else { + // res->Coalesce(o_width); + res->ConvertEvenLines(1.0*o_width); + res->Simplify(1.0 * o_width); + } */ + // methode par makeoffset + + if (expand) + { + theShape->MakeOffset(theRes, o_width, o_join, o_miter); + } + else + { + theShape->MakeOffset(theRes, -o_width, o_join, o_miter); + } + theRes->ConvertToShape(theShape, fill_positive); + + res->Reset(); + theRes->ConvertToForme(res); + + // Without this, too many nodes are created. + // This was removed earlier due to distoring small shapes. + // The threshold has been lowered which should reduce distortions. + // See: https://gitlab.com/inkscape/inkscape/-/issues/964 + res->ConvertEvenLines(0.1); + res->Simplify(0.1); + + delete theShape; + delete theRes; + } + + did = true; + + curve->unref(); + // remember the position of the item + gint pos = item->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + selection->remove(item); + + Inkscape::XML::Node *repr = nullptr; + + if (res->descr_cmd.size() > 1) { // if there's 0 or 1 node left, drop this path altogether + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, item->getRepr()); + } + + item->deleteObject(false); + + if (repr) { + gchar *str = res->svg_dump_path(); + repr->setAttribute("d", str); + g_free(str); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + SPItem *newitem = (SPItem *) desktop->getDocument()->getObjectByRepr(repr); + + // reapply the transform + newitem->doWriteTransform(transform); + + selection->add(repr); + + Inkscape::GC::release(repr); + } + + delete orig; + delete res; + } + + if (did) { + DocumentUndo::done(desktop->getDocument(), + (expand ? SP_VERB_SELECTION_OFFSET : SP_VERB_SELECTION_INSET), + (expand ? _("Outset path") : _("Inset path"))); + } else { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No paths</b> to inset/outset in the selection.")); + return; + } +} + + +static bool +sp_selected_path_simplify_items(SPDesktop *desktop, + Inkscape::Selection *selection, std::vector<SPItem*> &items, + float threshold, bool justCoalesce, + float angleLimit, bool breakableAngles, + bool modifySelection); + + +//return true if we changed something, else false +static bool +sp_selected_path_simplify_item(SPDesktop *desktop, + Inkscape::Selection *selection, SPItem *item, + float threshold, bool justCoalesce, + float angleLimit, bool breakableAngles, + gdouble size, bool modifySelection) +{ + if (!(SP_IS_GROUP(item) || SP_IS_SHAPE(item) || SP_IS_TEXT(item))) + return false; + + //If this is a group, do the children instead + if (SP_IS_GROUP(item)) { + std::vector<SPItem*> items = sp_item_group_item_list(SP_GROUP(item)); + + return sp_selected_path_simplify_items(desktop, selection, items, + threshold, justCoalesce, + angleLimit, breakableAngles, + false); + } + + // get path to simplify (note that the path *before* LPE calculation is needed) + Path *orig = Path_for_item_before_LPE(item, false); + if (orig == nullptr) { + return false; + } + + // correct virtual size by full transform (bug #166937) + size /= item->i2doc_affine().descrim(); + + // save the transform, to re-apply it after simplification + Geom::Affine const transform(item->transform); + + /* + reset the transform, effectively transforming the item by transform.inverse(); + this is necessary so that the item is transformed twice back and forth, + allowing all compensations to cancel out regardless of the preferences + */ + item->doWriteTransform(Geom::identity()); + + // remember the position of the item + gint pos = item->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + // remember path effect + char const *patheffect = item->getRepr()->attribute("inkscape:path-effect"); + + //If a group was selected, to not change the selection list + if (modifySelection) { + selection->remove(item); + } + + if ( justCoalesce ) { + orig->Coalesce(threshold * size); + } else { + orig->ConvertEvenLines(threshold * size); + orig->Simplify(threshold * size); + } + + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + // restore attributes + Inkscape::copy_object_properties(repr, item->getRepr()); + + item->deleteObject(false); + + // restore path effect + repr->setAttribute("inkscape:path-effect", patheffect); + + // path + gchar *str = orig->svg_dump_path(); + if (patheffect) + repr->setAttribute("inkscape:original-d", str); + else + repr->setAttribute("d", str); + g_free(str); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + SPItem *newitem = (SPItem *) desktop->getDocument()->getObjectByRepr(repr); + + // reapply the transform + newitem->doWriteTransform(transform); + + //If we are not in a selected group + if (modifySelection) + selection->add(repr); + + Inkscape::GC::release(repr); + + // clean up + if (orig) delete orig; + + return true; +} + + +bool +sp_selected_path_simplify_items(SPDesktop *desktop, + Inkscape::Selection *selection, std::vector<SPItem*> &items, + float threshold, bool justCoalesce, + float angleLimit, bool breakableAngles, + bool modifySelection) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool simplifyIndividualPaths = prefs->getBool("/options/simplifyindividualpaths/value"); + + gchar *simplificationType; + if (simplifyIndividualPaths) { + simplificationType = _("Simplifying paths (separately):"); + } else { + simplificationType = _("Simplifying paths:"); + } + + bool didSomething = false; + + Geom::OptRect selectionBbox = selection->visualBounds(); + if (!selectionBbox) { + return false; + } + gdouble selectionSize = L2(selectionBbox->dimensions()); + + gdouble simplifySize = selectionSize; + + int pathsSimplified = 0; + int totalPathCount = items.size(); + + // set "busy" cursor + desktop->setWaitingCursor(); + + for (auto item : items){ + if (!(SP_IS_GROUP(item) || SP_IS_SHAPE(item) || SP_IS_TEXT(item))) + continue; + + if (simplifyIndividualPaths) { + Geom::OptRect itemBbox = item->documentVisualBounds(); + if (itemBbox) { + simplifySize = L2(itemBbox->dimensions()); + } else { + simplifySize = 0; + } + } + + pathsSimplified++; + + if (pathsSimplified % 20 == 0) { + gchar *message = g_strdup_printf(_("%s <b>%d</b> of <b>%d</b> paths simplified..."), + simplificationType, pathsSimplified, totalPathCount); + desktop->messageStack()->flash(Inkscape::IMMEDIATE_MESSAGE, message); + g_free(message); + } + + didSomething |= sp_selected_path_simplify_item(desktop, selection, item, + threshold, justCoalesce, angleLimit, breakableAngles, simplifySize, modifySelection); + } + + desktop->clearWaitingCursor(); + + if (pathsSimplified > 20) { + desktop->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, _("<b>%d</b> paths simplified."), pathsSimplified); + } + + return didSomething; +} + +static void +sp_selected_path_simplify_selection(SPDesktop *desktop, float threshold, bool justCoalesce, + float angleLimit, bool breakableAngles) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>path(s)</b> to simplify.")); + return; + } + + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + + bool didSomething = sp_selected_path_simplify_items(desktop, selection, + items, threshold, + justCoalesce, + angleLimit, + breakableAngles, true); + + if (didSomething) + DocumentUndo::done(desktop->getDocument(), SP_VERB_SELECTION_SIMPLIFY, + _("Simplify")); + else + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No paths</b> to simplify in the selection.")); + +} + + +// globals for keeping track of accelerated simplify +static gint64 previous_time = 0; +static gdouble simplifyMultiply = 1.0; + +void +sp_selected_path_simplify(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble simplifyThreshold = + prefs->getDouble("/options/simplifythreshold/value", 0.003); + bool simplifyJustCoalesce = prefs->getBool("/options/simplifyjustcoalesce/value", false); + + //Get the current time + gint64 current_time = g_get_monotonic_time(); + //Was the previous call to this function recent? (<0.5 sec) + if (previous_time > 0 && current_time - previous_time < 500000) { + + // add to the threshold 1/2 of its original value + simplifyMultiply += 0.5; + simplifyThreshold *= simplifyMultiply; + + } else { + // reset to the default + simplifyMultiply = 1; + } + + //remember time for next call + previous_time = current_time; + + //g_print("%g\n", simplify_threshold); + + //Make the actual call + sp_selected_path_simplify_selection(desktop, simplifyThreshold, + simplifyJustCoalesce, 0.0, false); +} + + + +// fonctions utilitaires + +bool +Ancetre(Inkscape::XML::Node *a, Inkscape::XML::Node *who) +{ + if (who == nullptr || a == nullptr) + return false; + if (who == a) + return true; + return Ancetre(a->parent(), who); +} + +// derived from Path_for_item +Path * +Path_for_pathvector(Geom::PathVector const &epathv) +{ + /*std::cout << "converting to Livarot path" << std::endl; + + Geom::SVGPathWriter wr; + wr.feed(epathv); + std::cout << wr.str() << std::endl;*/ + + Path *dest = new Path; + dest->LoadPathVector(epathv); + return dest; +} + +Path * +Path_for_item(SPItem *item, bool doTransformation, bool transformFull) +{ + SPCurve *curve = curve_for_item(item); + + if (curve == nullptr) + return nullptr; + + Geom::PathVector *pathv = pathvector_for_curve(item, curve, doTransformation, transformFull, Geom::identity(), Geom::identity()); + curve->unref(); + + /*std::cout << "converting to Livarot path" << std::endl; + + Geom::SVGPathWriter wr; + if (pathv) { + wr.feed(*pathv); + } + std::cout << wr.str() << std::endl;*/ + + Path *dest = new Path; + dest->LoadPathVector(*pathv); + delete pathv; + + /*gchar *str = dest->svg_dump_path(); + std::cout << "After conversion:\n" << str << std::endl; + g_free(str);*/ + + return dest; +} + +/** + * Obtains an item's Path before the LPE stack has been applied. + */ +Path * +Path_for_item_before_LPE(SPItem *item, bool doTransformation, bool transformFull) +{ + SPCurve *curve = curve_for_item_before_LPE(item); + + if (curve == nullptr) + return nullptr; + + Geom::PathVector *pathv = pathvector_for_curve(item, curve, doTransformation, transformFull, Geom::identity(), Geom::identity()); + curve->unref(); + + Path *dest = new Path; + dest->LoadPathVector(*pathv); + delete pathv; + + return dest; +} + +/* + * NOTE: Returns empty pathvector if curve == NULL + * TODO: see if calling this method can be optimized. All the pathvector copying might be slow. + */ +Geom::PathVector* +pathvector_for_curve(SPItem *item, SPCurve *curve, bool doTransformation, bool transformFull, Geom::Affine extraPreAffine, Geom::Affine extraPostAffine) +{ + if (curve == nullptr) + return nullptr; + + Geom::PathVector *dest = new Geom::PathVector; + *dest = curve->get_pathvector(); // Make a copy; must be freed by the caller! + + if (doTransformation) { + if (transformFull) { + *dest *= extraPreAffine * item->i2doc_affine() * extraPostAffine; + } else { + *dest *= extraPreAffine * (Geom::Affine)item->transform * extraPostAffine; + } + } else { + *dest *= extraPreAffine * extraPostAffine; + } + + return dest; +} + +/** + * Obtains an item's curve. For SPPath, it is the path *before* LPE. For SPShapes other than path, it is the path *after* LPE. + * So the result is somewhat ill-defined, and probably this method should not be used... See curve_for_item_before_LPE. + */ +SPCurve* curve_for_item(SPItem *item) +{ + if (!item) + return nullptr; + + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(item)) { + if (SP_IS_PATH(item)) { + curve = SP_PATH(item)->getCurveForEdit(); + } else { + curve = SP_SHAPE(item)->getCurve(); + } + } + else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) + { + curve = te_get_layout(item)->convertToCurves(); + } + else if (SP_IS_IMAGE(item)) + { + curve = SP_IMAGE(item)->get_curve(); + } + + return curve; // do not forget to unref the curve at some point! +} + +/** + * Obtains an item's curve *before* LPE. + * The returned SPCurve should be unreffed by the caller. + */ +SPCurve* curve_for_item_before_LPE(SPItem *item) +{ + if (!item) + return nullptr; + + SPCurve *curve = nullptr; + if (SP_IS_SHAPE(item)) { + curve = SP_SHAPE(item)->getCurveForEdit(); + } + else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) + { + curve = te_get_layout(item)->convertToCurves(); + } + else if (SP_IS_IMAGE(item)) + { + curve = SP_IMAGE(item)->get_curve(); + } + + return curve; // do not forget to unref the curve at some point! +} + +boost::optional<Path::cut_position> get_nearest_position_on_Path(Path *path, Geom::Point p, unsigned seg) +{ + //get nearest position on path + Path::cut_position pos = path->PointToCurvilignPosition(p, seg); + return pos; +} + +Geom::Point get_point_on_Path(Path *path, int piece, double t) +{ + Geom::Point p; + path->PointAt(piece, t, p); + return p; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/splivarot.h b/src/splivarot.h new file mode 100644 index 0000000..ec218ce --- /dev/null +++ b/src/splivarot.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * boolean operations and outlines + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_LIVAROT_H +#define SEEN_SP_LIVAROT_H + +#include <2geom/forward.h> +#include <2geom/path.h> +#include "livarot/Path.h" +#include "object/object-set.h" // bool_op + +class SPCurve; +class SPDesktop; +class SPItem; + +namespace Inkscape { + class Selection; + class ObjectSet; +} + +// offset/inset of a curve +// takes the fill-rule in consideration +// offset amount is the stroke-width of the curve +void sp_selected_path_offset (SPDesktop *desktop); +void sp_selected_path_offset_screen (SPDesktop *desktop, double pixels); +void sp_selected_path_inset (SPDesktop *desktop); +void sp_selected_path_inset_screen (SPDesktop *desktop, double pixels); +void sp_selected_path_create_offset (SPDesktop *desktop); +void sp_selected_path_create_inset (SPDesktop *desktop); +void sp_selected_path_create_updating_offset (SPDesktop *desktop); +void sp_selected_path_create_updating_inset (SPDesktop *desktop); + +void sp_selected_path_create_offset_object_zero (SPDesktop *desktop); +void sp_selected_path_create_updating_offset_object_zero (SPDesktop *desktop); + +// outline of a curve +// uses the stroke-width +void sp_selected_path_outline (SPDesktop *desktop, bool legacy = false); +bool sp_item_path_outline(SPItem *item, SPDesktop *desktop, bool legacy, SPItem *context = nullptr); +Geom::PathVector* item_outline(SPItem const *item, bool bbox_only = false); + +// simplifies a path (removes small segments and the like) +void sp_selected_path_simplify (SPDesktop *desktop); + +Path *Path_for_pathvector(Geom::PathVector const &pathv); +Path *Path_for_item(SPItem *item, bool doTransformation, bool transformFull = true); +Path *Path_for_item_before_LPE(SPItem *item, bool doTransformation, bool transformFull = true); +Geom::PathVector* pathvector_for_curve(SPItem *item, SPCurve *curve, bool doTransformation, bool transformFull, Geom::Affine extraPreAffine, Geom::Affine extraPostAffine); +SPCurve *curve_for_item(SPItem *item); +SPCurve *curve_for_item_before_LPE(SPItem *item); +boost::optional<Path::cut_position> get_nearest_position_on_Path(Path *path, Geom::Point p, unsigned seg = 0); +Geom::Point get_point_on_Path(Path *path, int piece, double t); +Geom::PathVector sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, FillRule fra, FillRule frb); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/streq.h b/src/streq.h new file mode 100644 index 0000000..0ffd83d --- /dev/null +++ b/src/streq.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_STREQ_H +#define INKSCAPE_STREQ_H + +#include <cstring> + +/** Convenience/readability wrapper for strcmp(a,b)==0. */ +inline bool +streq(char const *a, char const *b) +{ + return std::strcmp(a, b) == 0; +} + +struct streq_rel { + bool operator()(char const *a, char const *b) const + { + return (std::strcmp(a, b) == 0); + } +}; + +#endif /* !INKSCAPE_STREQ_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/strneq.h b/src/strneq.h new file mode 100644 index 0000000..1f153d2 --- /dev/null +++ b/src/strneq.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_STRNEQ_H +#define INKSCAPE_STRNEQ_H + +#include <cstring> + +/** Convenience/readability wrapper for strncmp(a,b,n)==0. */ +inline bool +strneq(char const *a, char const *b, size_t n) +{ + return std::strncmp(a, b, n) == 0; +} + + +#endif /* !INKSCAPE_STRNEQ_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/style-enums.h b/src/style-enums.h new file mode 100644 index 0000000..30f715d --- /dev/null +++ b/src/style-enums.h @@ -0,0 +1,704 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_STYLE_ENUMS_H +#define SEEN_SP_STYLE_ENUMS_H + +/** \file + * SPStyle enums: named public enums that correspond to SVG property values. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* SPFontStyle */ + +#include "display/canvas-bpath.h" // FIXME those enums belong here! + +#include <cstdint> + +enum SPCSSFontSize : std::int_least8_t { + SP_CSS_FONT_SIZE_XX_SMALL, + SP_CSS_FONT_SIZE_X_SMALL, + SP_CSS_FONT_SIZE_SMALL, + SP_CSS_FONT_SIZE_MEDIUM, + SP_CSS_FONT_SIZE_LARGE, + SP_CSS_FONT_SIZE_X_LARGE, + SP_CSS_FONT_SIZE_XX_LARGE, + SP_CSS_FONT_SIZE_SMALLER, + SP_CSS_FONT_SIZE_LARGER +}; + +enum SPCSSFontStyle : std::uint_least8_t { + SP_CSS_FONT_STYLE_NORMAL, + SP_CSS_FONT_STYLE_ITALIC, + SP_CSS_FONT_STYLE_OBLIQUE +}; + +enum SPCSSFontVariant : std::uint_least8_t { + SP_CSS_FONT_VARIANT_NORMAL, + SP_CSS_FONT_VARIANT_SMALL_CAPS +}; + +enum SPCSSFontWeight : std::int_least8_t { + SP_CSS_FONT_WEIGHT_100, + SP_CSS_FONT_WEIGHT_200, + SP_CSS_FONT_WEIGHT_300, + SP_CSS_FONT_WEIGHT_400, + SP_CSS_FONT_WEIGHT_500, + SP_CSS_FONT_WEIGHT_600, + SP_CSS_FONT_WEIGHT_700, + SP_CSS_FONT_WEIGHT_800, + SP_CSS_FONT_WEIGHT_900, + SP_CSS_FONT_WEIGHT_NORMAL, + SP_CSS_FONT_WEIGHT_BOLD, + SP_CSS_FONT_WEIGHT_LIGHTER, + SP_CSS_FONT_WEIGHT_BOLDER +}; + +enum SPCSSFontStretch : std::int_least8_t { + SP_CSS_FONT_STRETCH_ULTRA_CONDENSED, + SP_CSS_FONT_STRETCH_EXTRA_CONDENSED, + SP_CSS_FONT_STRETCH_CONDENSED, + SP_CSS_FONT_STRETCH_SEMI_CONDENSED, + SP_CSS_FONT_STRETCH_NORMAL, + SP_CSS_FONT_STRETCH_SEMI_EXPANDED, + SP_CSS_FONT_STRETCH_EXPANDED, + SP_CSS_FONT_STRETCH_EXTRA_EXPANDED, + SP_CSS_FONT_STRETCH_ULTRA_EXPANDED, + SP_CSS_FONT_STRETCH_NARROWER, + SP_CSS_FONT_STRETCH_WIDER +}; + +// Can select more than one +enum SPCSSFontVariantLigatures : std::uint_least8_t { + SP_CSS_FONT_VARIANT_LIGATURES_NONE = 0, + SP_CSS_FONT_VARIANT_LIGATURES_COMMON = 1, + SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 2, + SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL = 4, + SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 8, + SP_CSS_FONT_VARIANT_LIGATURES_NORMAL = 9, // Special case + SP_CSS_FONT_VARIANT_LIGATURES_NOCOMMON = 16, + SP_CSS_FONT_VARIANT_LIGATURES_NODISCRETIONARY = 32, + SP_CSS_FONT_VARIANT_LIGATURES_NOHISTORICAL = 64, + SP_CSS_FONT_VARIANT_LIGATURES_NOCONTEXTUAL = 128 +}; + +enum SPCSSFontVariantPosition : std::uint_least8_t { + SP_CSS_FONT_VARIANT_POSITION_NORMAL = 1, + SP_CSS_FONT_VARIANT_POSITION_SUB = 2, + SP_CSS_FONT_VARIANT_POSITION_SUPER = 4 +}; + +enum SPCSSFontVariantCaps : std::uint_least8_t { + SP_CSS_FONT_VARIANT_CAPS_NORMAL = 1, + SP_CSS_FONT_VARIANT_CAPS_SMALL = 2, + SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL = 4, + SP_CSS_FONT_VARIANT_CAPS_PETITE = 8, + SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE = 16, + SP_CSS_FONT_VARIANT_CAPS_UNICASE = 32, + SP_CSS_FONT_VARIANT_CAPS_TITLING = 64 +}; + +// Can select more than one (see spec) +enum SPCSSFontVariantNumeric : std::uint_least8_t { + SP_CSS_FONT_VARIANT_NUMERIC_NORMAL = 0, + SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS = 1, + SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS = 2, + SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS = 4, + SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS = 8, + SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 16, + SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 32, + SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL = 64, + SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO = 128 +}; + +// Quite complicated... (see spec) +enum SPCSSFontVariantAlternates : std::uint_least8_t { + SP_CSS_FONT_VARIANT_ALTERNATES_NORMAL, + SP_CSS_FONT_VARIANT_ALTERNATES_HISTORICAL_FORMS, + SP_CSS_FONT_VARIANT_ALTERNATES_STYLISTIC, + SP_CSS_FONT_VARIANT_ALTERNATES_STYLESET, + SP_CSS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT, + SP_CSS_FONT_VARIANT_ALTERNATES_SWASH, + SP_CSS_FONT_VARIANT_ALTERNATES_ORNAMENTS, + SP_CSS_FONT_VARIANT_ALTERNATES_ANNOTATION +}; + +// Can select more than one (see spec) +enum SPCSSFontVariantEastAsian : std::uint_least16_t { + SP_CSS_FONT_VARIANT_EAST_ASIAN_NORMAL = 0, + SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78 = 1, + SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83 = 2, + SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90 = 4, + SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04 = 8, + SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 16, + SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 32, + SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 64, + SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH = 128, + SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY = 256 +}; + +enum SPCSSTextAlign : std::uint_least8_t { + SP_CSS_TEXT_ALIGN_START, + SP_CSS_TEXT_ALIGN_END, + SP_CSS_TEXT_ALIGN_LEFT, + SP_CSS_TEXT_ALIGN_RIGHT, + SP_CSS_TEXT_ALIGN_CENTER, + SP_CSS_TEXT_ALIGN_JUSTIFY + // also <string> is allowed, but only within table calls +}; + +enum SPCSSTextTransform : std::uint_least8_t { + SP_CSS_TEXT_TRANSFORM_CAPITALIZE, + SP_CSS_TEXT_TRANSFORM_UPPERCASE, + SP_CSS_TEXT_TRANSFORM_LOWERCASE, + SP_CSS_TEXT_TRANSFORM_NONE +}; + +enum SPCSSDirection : std::uint_least8_t { + SP_CSS_DIRECTION_LTR, + SP_CSS_DIRECTION_RTL +}; + +enum SPCSSWritingMode : std::uint_least8_t { + SP_CSS_WRITING_MODE_LR_TB, + SP_CSS_WRITING_MODE_RL_TB, + SP_CSS_WRITING_MODE_TB_RL, + SP_CSS_WRITING_MODE_TB_LR +}; + +// CSS WRITING MODES 3 +enum SPCSSTextOrientation : std::uint_least8_t { + SP_CSS_TEXT_ORIENTATION_MIXED, + SP_CSS_TEXT_ORIENTATION_UPRIGHT, + SP_CSS_TEXT_ORIENTATION_SIDEWAYS +}; + +enum SPTextAnchor : std::uint_least8_t { + SP_CSS_TEXT_ANCHOR_START, + SP_CSS_TEXT_ANCHOR_MIDDLE, + SP_CSS_TEXT_ANCHOR_END +}; + +enum SPWhiteSpace : std::uint_least8_t { + SP_CSS_WHITE_SPACE_NORMAL, + SP_CSS_WHITE_SPACE_PRE, + SP_CSS_WHITE_SPACE_NOWRAP, + SP_CSS_WHITE_SPACE_PREWRAP, + SP_CSS_WHITE_SPACE_PRELINE +}; + +// Not complete list +enum SPCSSBaseline : std::uint_least8_t { + SP_CSS_BASELINE_AUTO, + SP_CSS_BASELINE_ALPHABETIC, + SP_CSS_BASELINE_IDEOGRAPHIC, + SP_CSS_BASELINE_HANGING, + SP_CSS_BASELINE_MATHEMATICAL, + SP_CSS_BASELINE_CENTRAL, + SP_CSS_BASELINE_MIDDLE, + SP_CSS_BASELINE_TEXT_BEFORE_EDGE, + SP_CSS_BASELINE_TEXT_AFTER_EDGE, + SP_CSS_BASELINE_SIZE // Size of enum, keep last. +}; + +enum SPCSSBaselineShift : std::uint_least8_t { + SP_CSS_BASELINE_SHIFT_BASELINE, + SP_CSS_BASELINE_SHIFT_SUB, + SP_CSS_BASELINE_SHIFT_SUPER +}; + +enum SPVisibility : std::uint_least8_t { + SP_CSS_VISIBILITY_HIDDEN, + SP_CSS_VISIBILITY_COLLAPSE, + SP_CSS_VISIBILITY_VISIBLE +}; + +enum SPOverflow : std::uint_least8_t { + SP_CSS_OVERFLOW_VISIBLE, + SP_CSS_OVERFLOW_HIDDEN, + SP_CSS_OVERFLOW_SCROLL, + SP_CSS_OVERFLOW_AUTO +}; + +/// \todo more display types +enum SPCSSDisplay : std::uint_least8_t { + SP_CSS_DISPLAY_NONE, + SP_CSS_DISPLAY_INLINE, + SP_CSS_DISPLAY_BLOCK, + SP_CSS_DISPLAY_LIST_ITEM, + SP_CSS_DISPLAY_RUN_IN, + SP_CSS_DISPLAY_COMPACT, + SP_CSS_DISPLAY_MARKER, + SP_CSS_DISPLAY_TABLE, + SP_CSS_DISPLAY_INLINE_TABLE, + SP_CSS_DISPLAY_TABLE_ROW_GROUP, + SP_CSS_DISPLAY_TABLE_HEADER_GROUP, + SP_CSS_DISPLAY_TABLE_FOOTER_GROUP, + SP_CSS_DISPLAY_TABLE_ROW, + SP_CSS_DISPLAY_TABLE_COLUMN_GROUP, + SP_CSS_DISPLAY_TABLE_COLUMN, + SP_CSS_DISPLAY_TABLE_CELL, + SP_CSS_DISPLAY_TABLE_CAPTION +}; + +enum SPIsolation : std::uint_least8_t { + SP_CSS_ISOLATION_AUTO, + SP_CSS_ISOLATION_ISOLATE +}; + +enum SPBlendMode : std::uint_least8_t { + SP_CSS_BLEND_NORMAL, + SP_CSS_BLEND_MULTIPLY, + SP_CSS_BLEND_SCREEN, + SP_CSS_BLEND_DARKEN, + SP_CSS_BLEND_LIGHTEN, + SP_CSS_BLEND_OVERLAY, + SP_CSS_BLEND_COLORDODGE, + SP_CSS_BLEND_COLORBURN, + SP_CSS_BLEND_HARDLIGHT, + SP_CSS_BLEND_SOFTLIGHT, + SP_CSS_BLEND_DIFFERENCE, + SP_CSS_BLEND_EXCLUSION, + SP_CSS_BLEND_HUE, + SP_CSS_BLEND_SATURATION, + SP_CSS_BLEND_COLOR, + SP_CSS_BLEND_LUMINOSITY, + SP_CSS_BLEND_ENDMODE +}; + +enum SPEnableBackground : std::uint_least8_t { + SP_CSS_BACKGROUND_ACCUMULATE, + SP_CSS_BACKGROUND_NEW +}; + +enum SPColorInterpolation : std::uint_least8_t { + SP_CSS_COLOR_INTERPOLATION_AUTO, + SP_CSS_COLOR_INTERPOLATION_SRGB, + SP_CSS_COLOR_INTERPOLATION_LINEARRGB +}; + +enum SPColorRendering : std::uint_least8_t { + SP_CSS_COLOR_RENDERING_AUTO, + SP_CSS_COLOR_RENDERING_OPTIMIZESPEED, + SP_CSS_COLOR_RENDERING_OPTIMIZEQUALITY +}; + +/* Last two are CSS4 Image values... for the momement prefaced with -inkscape. */ +enum SPImageRendering : std::uint_least8_t { + SP_CSS_IMAGE_RENDERING_AUTO, + SP_CSS_IMAGE_RENDERING_OPTIMIZESPEED, + SP_CSS_IMAGE_RENDERING_OPTIMIZEQUALITY, + SP_CSS_IMAGE_RENDERING_CRISPEDGES, + SP_CSS_IMAGE_RENDERING_PIXELATED +}; + +enum SPShapeRendering : std::uint_least8_t { + SP_CSS_SHAPE_RENDERING_AUTO, + SP_CSS_SHAPE_RENDERING_OPTIMIZESPEED, + SP_CSS_SHAPE_RENDERING_CRISPEDGES, + SP_CSS_SHAPE_RENDERING_GEOMETRICPRECISION +}; + +enum SPTextRendering : std::uint_least8_t { + SP_CSS_TEXT_RENDERING_AUTO, + SP_CSS_TEXT_RENDERING_OPTIMIZESPEED, + SP_CSS_TEXT_RENDERING_OPTIMIZELEGIBILITY, + SP_CSS_TEXT_RENDERING_GEOMETRICPRECISION +}; + +enum SPVectorEffect : std::uint_least8_t { + SP_VECTOR_EFFECT_NONE = 0, + SP_VECTOR_EFFECT_NON_SCALING_STROKE = 1, + SP_VECTOR_EFFECT_NON_SCALING_SIZE = 2, + SP_VECTOR_EFFECT_NON_ROTATION = 4, + SP_VECTOR_EFFECT_FIXED_POSITION = 8 +}; + +struct SPStyleEnum { + char const *key; + int value; +}; + +static SPStyleEnum const enum_fill_rule[] = { + {"nonzero", SP_WIND_RULE_NONZERO}, + {"evenodd", SP_WIND_RULE_EVENODD}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_stroke_linecap[] = { + {"butt", SP_STROKE_LINECAP_BUTT}, + {"round", SP_STROKE_LINECAP_ROUND}, + {"square", SP_STROKE_LINECAP_SQUARE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_stroke_linejoin[] = { + {"miter", SP_STROKE_LINEJOIN_MITER}, + {"round", SP_STROKE_LINEJOIN_ROUND}, + {"bevel", SP_STROKE_LINEJOIN_BEVEL}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_style[] = { + {"normal", SP_CSS_FONT_STYLE_NORMAL}, + {"italic", SP_CSS_FONT_STYLE_ITALIC}, + {"oblique", SP_CSS_FONT_STYLE_OBLIQUE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_size[] = { + {"xx-small", SP_CSS_FONT_SIZE_XX_SMALL}, + {"x-small", SP_CSS_FONT_SIZE_X_SMALL}, + {"small", SP_CSS_FONT_SIZE_SMALL}, + {"medium", SP_CSS_FONT_SIZE_MEDIUM}, + {"large", SP_CSS_FONT_SIZE_LARGE}, + {"x-large", SP_CSS_FONT_SIZE_X_LARGE}, + {"xx-large", SP_CSS_FONT_SIZE_XX_LARGE}, + {"smaller", SP_CSS_FONT_SIZE_SMALLER}, + {"larger", SP_CSS_FONT_SIZE_LARGER}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant[] = { + {"normal", SP_CSS_FONT_VARIANT_NORMAL}, + {"small-caps", SP_CSS_FONT_VARIANT_SMALL_CAPS}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_weight[] = { + {"100", SP_CSS_FONT_WEIGHT_100}, + {"200", SP_CSS_FONT_WEIGHT_200}, + {"300", SP_CSS_FONT_WEIGHT_300}, + {"400", SP_CSS_FONT_WEIGHT_400}, + {"500", SP_CSS_FONT_WEIGHT_500}, + {"600", SP_CSS_FONT_WEIGHT_600}, + {"700", SP_CSS_FONT_WEIGHT_700}, + {"800", SP_CSS_FONT_WEIGHT_800}, + {"900", SP_CSS_FONT_WEIGHT_900}, + {"normal", SP_CSS_FONT_WEIGHT_NORMAL}, + {"bold", SP_CSS_FONT_WEIGHT_BOLD}, + {"lighter", SP_CSS_FONT_WEIGHT_LIGHTER}, + {"bolder", SP_CSS_FONT_WEIGHT_BOLDER}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_stretch[] = { + {"ultra-condensed", SP_CSS_FONT_STRETCH_ULTRA_CONDENSED}, + {"extra-condensed", SP_CSS_FONT_STRETCH_EXTRA_CONDENSED}, + {"condensed", SP_CSS_FONT_STRETCH_CONDENSED}, + {"semi-condensed", SP_CSS_FONT_STRETCH_SEMI_CONDENSED}, + {"normal", SP_CSS_FONT_STRETCH_NORMAL}, + {"semi-expanded", SP_CSS_FONT_STRETCH_SEMI_EXPANDED}, + {"expanded", SP_CSS_FONT_STRETCH_EXPANDED}, + {"extra-expanded", SP_CSS_FONT_STRETCH_EXTRA_EXPANDED}, + {"ultra-expanded", SP_CSS_FONT_STRETCH_ULTRA_EXPANDED}, + {"narrower", SP_CSS_FONT_STRETCH_NARROWER}, + {"wider", SP_CSS_FONT_STRETCH_WIDER}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_ligatures[] = { + {"none", SP_CSS_FONT_VARIANT_LIGATURES_NONE}, + {"common-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_COMMON}, + {"discretionary-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY}, + {"historical-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL}, + {"contextual", SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL}, + {"normal", SP_CSS_FONT_VARIANT_LIGATURES_NORMAL}, + {"no-common-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_NOCOMMON}, + {"no-discretionary-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_NODISCRETIONARY}, + {"no-historical-ligatures", SP_CSS_FONT_VARIANT_LIGATURES_NOHISTORICAL}, + {"no-contextual", SP_CSS_FONT_VARIANT_LIGATURES_NOCONTEXTUAL}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_position[] = { + {"normal", SP_CSS_FONT_VARIANT_POSITION_NORMAL}, + {"sub", SP_CSS_FONT_VARIANT_POSITION_SUB}, + {"super", SP_CSS_FONT_VARIANT_POSITION_SUPER}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_caps[] = { + {"normal", SP_CSS_FONT_VARIANT_CAPS_NORMAL}, + {"small-caps", SP_CSS_FONT_VARIANT_CAPS_SMALL}, + {"all-small-caps", SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL}, + {"petite-caps", SP_CSS_FONT_VARIANT_CAPS_PETITE}, + {"all-petite-caps", SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE}, + {"unicase", SP_CSS_FONT_VARIANT_CAPS_UNICASE}, + {"titling-caps", SP_CSS_FONT_VARIANT_CAPS_TITLING}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_numeric[] = { + {"normal", SP_CSS_FONT_VARIANT_NUMERIC_NORMAL}, + {"lining-nums", SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS}, + {"oldstyle-nums", SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS}, + {"proportional-nums", SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS}, + {"tabular-nums", SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS}, + {"diagonal-fractions", SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS}, + {"stacked-fractions", SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS}, + {"ordinal", SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL}, + {"slashed-zero", SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_alternates[] = { + {"normal", SP_CSS_FONT_VARIANT_ALTERNATES_NORMAL}, + {"historical-forms", SP_CSS_FONT_VARIANT_ALTERNATES_HISTORICAL_FORMS}, + {"stylistic", SP_CSS_FONT_VARIANT_ALTERNATES_STYLISTIC}, + {"styleset", SP_CSS_FONT_VARIANT_ALTERNATES_STYLESET}, + {"character_variant", SP_CSS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT}, + {"swash", SP_CSS_FONT_VARIANT_ALTERNATES_SWASH}, + {"ornaments", SP_CSS_FONT_VARIANT_ALTERNATES_ORNAMENTS}, + {"annotation", SP_CSS_FONT_VARIANT_ALTERNATES_ANNOTATION}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_font_variant_east_asian[] = { + {"normal", SP_CSS_FONT_VARIANT_EAST_ASIAN_NORMAL}, + {"jis78", SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78}, + {"jis83", SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83}, + {"jis90", SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90}, + {"jis04", SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04}, + {"simplified", SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED}, + {"traditional", SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL}, + {"full-width", SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH}, + {"proportional-width", SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH}, + {"ruby", SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_text_align[] = { + {"start", SP_CSS_TEXT_ALIGN_START}, + {"end", SP_CSS_TEXT_ALIGN_END}, + {"left", SP_CSS_TEXT_ALIGN_LEFT}, + {"right", SP_CSS_TEXT_ALIGN_RIGHT}, + {"center", SP_CSS_TEXT_ALIGN_CENTER}, + {"justify", SP_CSS_TEXT_ALIGN_JUSTIFY}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_text_transform[] = { + {"capitalize", SP_CSS_TEXT_TRANSFORM_CAPITALIZE}, + {"uppercase", SP_CSS_TEXT_TRANSFORM_UPPERCASE}, + {"lowercase", SP_CSS_TEXT_TRANSFORM_LOWERCASE}, + {"none", SP_CSS_TEXT_TRANSFORM_NONE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_text_anchor[] = { + {"start", SP_CSS_TEXT_ANCHOR_START}, + {"middle", SP_CSS_TEXT_ANCHOR_MIDDLE}, + {"end", SP_CSS_TEXT_ANCHOR_END}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_white_space[] = { + {"normal", SP_CSS_WHITE_SPACE_NORMAL }, + {"pre", SP_CSS_WHITE_SPACE_PRE }, + {"nowrap", SP_CSS_WHITE_SPACE_NOWRAP }, + {"pre-wrap", SP_CSS_WHITE_SPACE_PREWRAP}, + {"pre-line", SP_CSS_WHITE_SPACE_PRELINE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_direction[] = { + {"ltr", SP_CSS_DIRECTION_LTR}, + {"rtl", SP_CSS_DIRECTION_RTL}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_writing_mode[] = { + /* Note that using the same enumerator for lr as lr-tb means we write as lr-tb even if the + * input file said lr. We prefer writing lr-tb on the grounds that the spec says the initial + * value is lr-tb rather than lr. + * + * ECMA scripts may be surprised to find tb-rl in DOM if they set the attribute to rl, so + * sharing enumerators for different strings may be a bug (once we support ecma script). + */ + // SVG 1.1 Deprecated but still must be supported in SVG 2. + {"lr-tb", SP_CSS_WRITING_MODE_LR_TB}, + {"rl-tb", SP_CSS_WRITING_MODE_RL_TB}, + {"tb-rl", SP_CSS_WRITING_MODE_TB_RL}, + {"lr", SP_CSS_WRITING_MODE_LR_TB}, + {"rl", SP_CSS_WRITING_MODE_RL_TB}, + {"tb", SP_CSS_WRITING_MODE_TB_RL}, + // SVG 2 & CSS 3 Writing Modes + {"horizontal-tb", SP_CSS_WRITING_MODE_LR_TB}, // This is correct, 'direction' distinguishes between 'lr' and 'rl'. + {"vertical-rl", SP_CSS_WRITING_MODE_TB_RL}, + {"vertical-lr", SP_CSS_WRITING_MODE_TB_LR}, + {nullptr, -1} +}; + +// CSS WRITING MODES 3 +static SPStyleEnum const enum_text_orientation[] = { + {"mixed", SP_CSS_TEXT_ORIENTATION_MIXED}, // Default + {"upright", SP_CSS_TEXT_ORIENTATION_UPRIGHT}, + {"sideways", SP_CSS_TEXT_ORIENTATION_SIDEWAYS}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_baseline[] = { + {"auto", SP_CSS_BASELINE_AUTO}, // Default + {"alphabetic", SP_CSS_BASELINE_ALPHABETIC}, + {"ideographic", SP_CSS_BASELINE_IDEOGRAPHIC}, + {"hanging", SP_CSS_BASELINE_HANGING}, + {"mathematical", SP_CSS_BASELINE_MATHEMATICAL}, + {"central", SP_CSS_BASELINE_CENTRAL}, + {"middle", SP_CSS_BASELINE_MIDDLE}, + {"text-before-edge", SP_CSS_BASELINE_TEXT_BEFORE_EDGE}, + {"text-after-edge", SP_CSS_BASELINE_TEXT_AFTER_EDGE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_baseline_shift[] = { + {"baseline", SP_CSS_BASELINE_SHIFT_BASELINE}, + {"sub", SP_CSS_BASELINE_SHIFT_SUB}, + {"super", SP_CSS_BASELINE_SHIFT_SUPER}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_visibility[] = { + {"hidden", SP_CSS_VISIBILITY_HIDDEN}, + {"collapse", SP_CSS_VISIBILITY_COLLAPSE}, + {"visible", SP_CSS_VISIBILITY_VISIBLE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_overflow[] = { + {"visible", SP_CSS_OVERFLOW_VISIBLE}, + {"hidden", SP_CSS_OVERFLOW_HIDDEN}, + {"scroll", SP_CSS_OVERFLOW_SCROLL}, + {"auto", SP_CSS_OVERFLOW_AUTO}, + {nullptr, -1} +}; + +// CSS Compositing and Blending Level 1 +static SPStyleEnum const enum_isolation[] = { + {"auto", SP_CSS_ISOLATION_AUTO}, + {"isolate", SP_CSS_ISOLATION_ISOLATE}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_blend_mode[] = { + {"normal", SP_CSS_BLEND_NORMAL}, + {"multiply", SP_CSS_BLEND_MULTIPLY}, + {"screen", SP_CSS_BLEND_SCREEN}, + {"darken", SP_CSS_BLEND_DARKEN}, + {"lighten", SP_CSS_BLEND_LIGHTEN}, + {"overlay", SP_CSS_BLEND_OVERLAY}, + {"color-dodge", SP_CSS_BLEND_COLORDODGE}, + {"color-burn", SP_CSS_BLEND_COLORBURN}, + {"hard-light", SP_CSS_BLEND_HARDLIGHT}, + {"soft-light", SP_CSS_BLEND_SOFTLIGHT}, + {"difference", SP_CSS_BLEND_DIFFERENCE}, + {"exclusion", SP_CSS_BLEND_EXCLUSION}, + {"hue", SP_CSS_BLEND_HUE}, + {"saturation", SP_CSS_BLEND_SATURATION}, + {"color", SP_CSS_BLEND_COLOR}, + {"luminosity", SP_CSS_BLEND_LUMINOSITY}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_display[] = { + {"none", SP_CSS_DISPLAY_NONE}, + {"inline", SP_CSS_DISPLAY_INLINE}, + {"block", SP_CSS_DISPLAY_BLOCK}, + {"list-item", SP_CSS_DISPLAY_LIST_ITEM}, + {"run-in", SP_CSS_DISPLAY_RUN_IN}, + {"compact", SP_CSS_DISPLAY_COMPACT}, + {"marker", SP_CSS_DISPLAY_MARKER}, + {"table", SP_CSS_DISPLAY_TABLE}, + {"inline-table", SP_CSS_DISPLAY_INLINE_TABLE}, + {"table-row-group", SP_CSS_DISPLAY_TABLE_ROW_GROUP}, + {"table-header-group", SP_CSS_DISPLAY_TABLE_HEADER_GROUP}, + {"table-footer-group", SP_CSS_DISPLAY_TABLE_FOOTER_GROUP}, + {"table-row", SP_CSS_DISPLAY_TABLE_ROW}, + {"table-column-group", SP_CSS_DISPLAY_TABLE_COLUMN_GROUP}, + {"table-column", SP_CSS_DISPLAY_TABLE_COLUMN}, + {"table-cell", SP_CSS_DISPLAY_TABLE_CELL}, + {"table-caption", SP_CSS_DISPLAY_TABLE_CAPTION}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_shape_rendering[] = { + {"auto", SP_CSS_SHAPE_RENDERING_AUTO}, + {"optimizeSpeed", SP_CSS_SHAPE_RENDERING_OPTIMIZESPEED}, + {"crispEdges", SP_CSS_SHAPE_RENDERING_CRISPEDGES}, + {"geometricPrecision", SP_CSS_SHAPE_RENDERING_GEOMETRICPRECISION}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_color_rendering[] = { + {"auto", SP_CSS_COLOR_RENDERING_AUTO}, + {"optimizeSpeed", SP_CSS_COLOR_RENDERING_OPTIMIZESPEED}, + {"optimizeQuality", SP_CSS_COLOR_RENDERING_OPTIMIZEQUALITY}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_image_rendering[] = { + {"auto", SP_CSS_IMAGE_RENDERING_AUTO}, + {"optimizeSpeed", SP_CSS_IMAGE_RENDERING_OPTIMIZESPEED}, + {"optimizeQuality", SP_CSS_IMAGE_RENDERING_OPTIMIZEQUALITY}, + {"crisp-edges", SP_CSS_IMAGE_RENDERING_CRISPEDGES}, + {"pixelated", SP_CSS_IMAGE_RENDERING_PIXELATED}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_text_rendering[] = { + {"auto", SP_CSS_TEXT_RENDERING_AUTO}, + {"optimizeSpeed", SP_CSS_TEXT_RENDERING_OPTIMIZESPEED}, + {"optimizeLegibility", SP_CSS_TEXT_RENDERING_OPTIMIZELEGIBILITY}, + {"geometricPrecision", SP_CSS_TEXT_RENDERING_GEOMETRICPRECISION}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_enable_background[] = { + {"accumulate", SP_CSS_BACKGROUND_ACCUMULATE}, + {"new", SP_CSS_BACKGROUND_NEW}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_clip_rule[] = { + {"nonzero", SP_WIND_RULE_NONZERO}, + {"evenodd", SP_WIND_RULE_EVENODD}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_color_interpolation[] = { + {"auto", SP_CSS_COLOR_INTERPOLATION_AUTO}, + {"sRGB", SP_CSS_COLOR_INTERPOLATION_SRGB}, + {"linearRGB", SP_CSS_COLOR_INTERPOLATION_LINEARRGB}, + {nullptr, -1} +}; + +static SPStyleEnum const enum_vector_effect[] = { + {"none", SP_VECTOR_EFFECT_NONE}, + {"non-scaling-stroke", SP_VECTOR_EFFECT_NON_SCALING_STROKE}, + {"non-scaling-size", SP_VECTOR_EFFECT_NON_SCALING_SIZE}, + {"non-rotation", SP_VECTOR_EFFECT_NON_ROTATION}, + {"fixed-position", SP_VECTOR_EFFECT_FIXED_POSITION}, + {nullptr, -1} +}; + + +#endif // SEEN_SP_STYLE_ENUMS_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/style-internal.cpp b/src/style-internal.cpp new file mode 100644 index 0000000..efeeda1 --- /dev/null +++ b/src/style-internal.cpp @@ -0,0 +1,3229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG stylesheets implementation - Classes used by SPStyle class. + */ + +/* Authors: + * C++ conversion: + * Tavmjong Bah <tavmjong@free.fr> + * Legacy C implementation: + * Lauris Kaplinski <lauris@kaplinski.com> + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2005 Monash University + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2014, 2018 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/regex.h> + +#include "style-internal.h" +#include "style.h" + +#include "bad-uri-exception.h" +#include "extract-uri.h" +#include "inkscape.h" +#include "preferences.h" +#include "streq.h" +#include "strneq.h" + +#include "object/sp-text.h" + +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" + +#include "util/units.h" + +// TODO REMOVE OR MAKE MEMBER FUNCTIONS +void sp_style_fill_paint_server_ref_changed( SPObject *old_ref, SPObject *ref, SPStyle *style); +void sp_style_stroke_paint_server_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style); +void sp_style_filter_ref_changed( SPObject *old_ref, SPObject *ref, SPStyle *style); +void sp_style_set_ipaint_to_uri(SPStyle *style, SPIPaint *paint, const Inkscape::URI *uri, SPDocument *document); +void sp_style_set_ipaint_to_uri_string (SPStyle *style, SPIPaint *paint, const gchar *uri); + +using Inkscape::CSSOStringStream; + +// SPIBase -------------------------------------------------------------- + +Glib::ustring const &SPIBase::name() const +{ + static Glib::ustring names[SPAttributeEnum_SIZE]; + auto &name = names[id()]; + if (name.empty()) { + auto const *namecstr = sp_attribute_name(id()); + name = namecstr ? namecstr : "anonymous"; + } + return name; +} + +// Standard criteria for writing a property +// dfp == different from parent +inline bool should_write( guint const flags, bool set, bool dfp, bool src) { + + bool should_write = false; + if ( ((flags & SP_STYLE_FLAG_ALWAYS) && src) || + ((flags & SP_STYLE_FLAG_IFSET) && set && src) || + ((flags & SP_STYLE_FLAG_IFDIFF) && set && src && dfp)) { + should_write = true; + } + return should_write; +} + +bool SPIBase::shall_write(guint const flags, SPStyleSrc const &style_src_req, SPIBase const *const base) const +{ + // Is this class different from the SPIBase given, this is used in Object-to-Path + SPIBase const *const my_base = dynamic_cast<const SPIBase*>(base); + bool dfp = (!inherits || !my_base || (my_base != this)); // Different from parent + bool src = (style_src_req == style_src || !(flags & SP_STYLE_FLAG_IFSRC)); + return should_write(flags, set, dfp, src); +} + +const Glib::ustring SPIBase::write(guint const flags, SPStyleSrc const &style_src_req, SPIBase const *const base) const +{ + if (shall_write(flags, style_src_req, base)) { + auto value = this->get_value(); + if ( !value.empty() ) { + return (name() + ":" + value + important_str() + ";"); + } + } + return Glib::ustring(""); +} + + +/** + * If str.endswith("!important") then assign stripped = str[:-10].rstrip() and return true. + * Otherwise, leave stripped unmodified and return false. + */ +static bool strip_important(gchar const *str, std::string &stripped) +{ + assert(str != nullptr); + + constexpr size_t N = 10; // strlen("!important") + auto pos = strlen(str); + + if (pos >= N && strncmp(str + pos - N, "!important", N) == 0) { + pos -= N; + + // strip whitespace from the right + while (pos > 0 && g_ascii_isspace(str[pos - 1])) { + --pos; + } + + stripped.assign(str, pos); + return true; + } + + return false; +} + +void SPIBase::readIfUnset(gchar const *str, SPStyleSrc source) +{ + if (!str) + return; + + bool has_important = false; + std::string stripped; + + // '!important' is invalid on attributes + if (source != SP_STYLE_SRC_ATTRIBUTE) { + has_important = strip_important(str, stripped); + if (has_important) { + str = stripped.c_str(); + } + } + + if (!set || (has_important && !important)) { + style_src = source; + read(str); + if (set) { + if (has_important) { + important = true; + } + } + } +} + + +// SPIFloat ------------------------------------------------------------- + +void +SPIFloat::read( gchar const *str ) { + + if( !str ) return; + + if ( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else { + gfloat value_tmp; + if (sp_svg_number_read_f(str, &value_tmp)) { + set = true; + inherit = false; + value = value_tmp; + } + } +} + +const Glib::ustring SPIFloat::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + return Glib::ustring::format(this->value); +} + +void +SPIFloat::cascade( const SPIBase* const parent ) { + if( const SPIFloat* p = dynamic_cast<const SPIFloat*>(parent) ) { + if( (inherits && !set) || inherit ) value = p->value; + } else { + std::cerr << "SPIFloat::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIFloat::merge( const SPIBase* const parent ) { + if( const SPIFloat* p = dynamic_cast<const SPIFloat*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + value = p->value; + } + } + } else { + std::cerr << "SPIFloat::merge(): Incorrect parent type" << std::endl; + } +} + +bool +SPIFloat::operator==(const SPIBase& rhs) { + if( const SPIFloat* r = dynamic_cast<const SPIFloat*>(&rhs) ) { + return (value == r->value && SPIBase::operator==(rhs)); + } else { + return false; + } +} + + + +// SPIScale24 ----------------------------------------------------------- + +void +SPIScale24::read( gchar const *str ) { + + if( !str ) return; + + if ( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else { + gfloat value_in; + if (sp_svg_number_read_f(str, &value_in)) { + set = true; + inherit = false; + value_in = CLAMP(value_in, 0.0, 1.0); + value = SP_SCALE24_FROM_FLOAT( value_in ); + } + } +} + +const Glib::ustring SPIScale24::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + return Glib::ustring::format(SP_SCALE24_TO_FLOAT(this->value)); +} + +void +SPIScale24::cascade( const SPIBase* const parent ) { + if( const SPIScale24* p = dynamic_cast<const SPIScale24*>(parent) ) { + if( (inherits && !set) || inherit ) value = p->value; + } else { + std::cerr << "SPIScale24::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIScale24::merge( const SPIBase* const parent ) { + if( const SPIScale24* p = dynamic_cast<const SPIScale24*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + value = p->value; + } + } else { + // Needed only for 'opacity' and 'stop-opacity' which do not inherit. See comment at bottom of file. + if (id() != SP_PROP_OPACITY && id() != SP_PROP_STOP_OPACITY) + std::cerr << "SPIScale24::merge: unhandled property: " << name() << std::endl; + if( !set || (!inherit && value == SP_SCALE24_MAX) ) { + value = p->value; + set = (value != SP_SCALE24_MAX); + } else { + if( inherit ) value = p->value; // Insures child is up-to-date + value = SP_SCALE24_MUL( value, p->value ); + inherit = (inherit && p->inherit && (p->value == 0 || p->value == SP_SCALE24_MAX) ); + set = (inherit || value < SP_SCALE24_MAX); + } + } + } else { + std::cerr << "SPIScale24::merge(): Incorrect parent type" << std::endl; + } +} + +bool +SPIScale24::operator==(const SPIBase& rhs) { + if( const SPIScale24* r = dynamic_cast<const SPIScale24*>(&rhs) ) { + return (value == r->value && SPIBase::operator==(rhs)); + } else { + return false; + } +} + + + +// SPILength ------------------------------------------------------------ + +void +SPILength::read( gchar const *str ) { + + if( !str ) return; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + unit = SP_CSS_UNIT_NONE; + value = computed = 0.0; + } else { + gdouble value_tmp; + gchar *e; + /** \todo fixme: Move this to standard place (Lauris) */ + value_tmp = g_ascii_strtod(str, &e); + if ( !std::isfinite(value_tmp) ) { // fix for bug lp:935157 + return; + } + if ((gchar const *) e != str) { + + value = value_tmp; + if (!*e) { + /* Userspace */ + unit = SP_CSS_UNIT_NONE; + computed = value; + } else if (!strcmp(e, "px")) { + /* Userspace */ + unit = SP_CSS_UNIT_PX; + computed = value; + } else if (!strcmp(e, "pt")) { + /* Userspace / DEVICESCALE */ + unit = SP_CSS_UNIT_PT; + computed = Inkscape::Util::Quantity::convert(value, "pt", "px"); + } else if (!strcmp(e, "pc")) { + unit = SP_CSS_UNIT_PC; + computed = Inkscape::Util::Quantity::convert(value, "pc", "px"); + } else if (!strcmp(e, "mm")) { + unit = SP_CSS_UNIT_MM; + computed = Inkscape::Util::Quantity::convert(value, "mm", "px"); + } else if (!strcmp(e, "cm")) { + unit = SP_CSS_UNIT_CM; + computed = Inkscape::Util::Quantity::convert(value, "cm", "px"); + } else if (!strcmp(e, "in")) { + unit = SP_CSS_UNIT_IN; + computed = Inkscape::Util::Quantity::convert(value, "in", "px"); + } else if (!strcmp(e, "em")) { + /* EM square */ + unit = SP_CSS_UNIT_EM; + if( style ) { + computed = value * style->font_size.computed; + } else { + computed = value * SPIFontSize::font_size_default; + } + } else if (!strcmp(e, "ex")) { + /* ex square */ + unit = SP_CSS_UNIT_EX; + if( style ) { + computed = value * style->font_size.computed * 0.5; // FIXME + } else { + computed = value * SPIFontSize::font_size_default * 0.5; + } + } else if (!strcmp(e, "%")) { + /* Percentage */ + unit = SP_CSS_UNIT_PERCENT; + value = value * 0.01; + if (id() == SP_PROP_LINE_HEIGHT) { + // See: http://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + if( style ) { + computed = value * style->font_size.computed; + } else { + computed = value * SPIFontSize::font_size_default; + } + } + } else { + /* Invalid */ + return; + } + set = true; + inherit = false; + } + } +} + +const Glib::ustring SPILength::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto value = this->computed; + auto unit_out = Glib::ustring(""); + switch (this->unit) { + case SP_CSS_UNIT_NONE: + break; + case SP_CSS_UNIT_PX: + unit_out = "px"; + break; + case SP_CSS_UNIT_PT: + case SP_CSS_UNIT_PC: + case SP_CSS_UNIT_MM: + case SP_CSS_UNIT_CM: + case SP_CSS_UNIT_IN: + unit_out = sp_style_get_css_unit_string(this->unit); + value = Inkscape::Util::Quantity::convert(this->computed, "px", unit_out); + break; + case SP_CSS_UNIT_EM: + case SP_CSS_UNIT_EX: + unit_out = sp_style_get_css_unit_string(this->unit); + value = this->value; + break; + case SP_CSS_UNIT_PERCENT: + unit_out = "%"; + value = this->value * 100.0; + break; + default: + /* Invalid */ + break; + } + return Glib::ustring::format(value) + unit_out; +} + +void +SPILength::cascade( const SPIBase* const parent ) { + if( const SPILength* p = dynamic_cast<const SPILength*>(parent) ) { + if( (inherits && !set) || inherit ) { + unit = p->unit; + value = p->value; + computed = p->computed; + } else { + // Recalculate based on new font-size, font-family inherited from parent + double const em = style->font_size.computed; + if (unit == SP_CSS_UNIT_EM) { + computed = value * em; + } else if (unit == SP_CSS_UNIT_EX) { + // FIXME: Get x height from libnrtype or pango. + computed = value * em * 0.5; + } else if (unit == SP_CSS_UNIT_PERCENT && id() == SP_PROP_LINE_HEIGHT) { + // Special case + computed = value * em; + } + } + } else { + std::cerr << "SPILength::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPILength::merge( const SPIBase* const parent ) { + if( const SPILength* p = dynamic_cast<const SPILength*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + unit = p->unit; + value = p->value; + computed = p->computed; + + // Fix up so values are correct + switch (p->unit) { + case SP_CSS_UNIT_EM: + case SP_CSS_UNIT_EX: + value *= p->style->font_size.computed / style->font_size.computed; + /** \todo + * FIXME: Have separate ex ratio parameter. + * Get x height from libnrtype or pango. + */ + if (!std::isfinite(value)) { + value = computed; + unit = SP_CSS_UNIT_NONE; + } + break; + + default: + break; + } + } + } + } else { + std::cerr << "SPIFloat::merge(): Incorrect parent type" << std::endl; + } +} + +void SPILength::setDouble(double v) +{ + unit = SP_CSS_UNIT_NONE; + value = v; + computed = v; + value_default = v; +} + +// Generate a string and allow emove name for parsing dasharray, etc. +const Glib::ustring SPILength::toString(bool wname) const +{ + CSSOStringStream os; + if (wname) { + os << name() << ":"; + } + os << this->get_value(); + if (wname) { + os << important_str(); + os << ";"; + } + return os.str(); +} + +bool +SPILength::operator==(const SPIBase& rhs) { + if( const SPILength* r = dynamic_cast<const SPILength*>(&rhs) ) { + + if( unit != r->unit ) return false; + + // If length depends on external parameter, lengths cannot be equal. + if (unit == SP_CSS_UNIT_EM) return false; + if (unit == SP_CSS_UNIT_EX) return false; + if (unit == SP_CSS_UNIT_PERCENT) return false; + if (r->unit == SP_CSS_UNIT_EM) return false; + if (r->unit == SP_CSS_UNIT_EX) return false; + if (r->unit == SP_CSS_UNIT_PERCENT) return false; + + return (computed == r->computed ); + } else { + return false; + } +} + +// SPILengthOrNormal ---------------------------------------------------- + +void +SPILengthOrNormal::read( gchar const *str ) { + + if( !str ) return; + + if ( !strcmp(str, "normal") ) { + set = true; + inherit = false; + unit = SP_CSS_UNIT_NONE; + value = computed = 0.0; + normal = true; + } else { + SPILength::read( str ); + normal = false; + } +}; + +const Glib::ustring SPILengthOrNormal::get_value() const +{ + if (this->normal) return Glib::ustring("normal"); + return SPILength::get_value(); +} + +void +SPILengthOrNormal::cascade( const SPIBase* const parent ) { + if( const SPILengthOrNormal* p = dynamic_cast<const SPILengthOrNormal*>(parent) ) { + if( (inherits && !set) || inherit ) { + normal = p->normal; + } + SPILength::cascade( parent ); + } else { + std::cerr << "SPILengthOrNormal::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPILengthOrNormal::merge( const SPIBase* const parent ) { + if( const SPILengthOrNormal* p = dynamic_cast<const SPILengthOrNormal*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + normal = p->normal; + SPILength::merge( parent ); + } + } + } +} + +bool +SPILengthOrNormal::operator==(const SPIBase& rhs) { + if( const SPILengthOrNormal* r = dynamic_cast<const SPILengthOrNormal*>(&rhs) ) { + if( normal && r->normal ) { return true; } + if( normal != r->normal ) { return false; } + return SPILength::operator==(rhs); + } else { + return false; + } +} + + +// SPIFontVariationSettings ---------------------------------------------------- + +void +SPIFontVariationSettings::read( gchar const *str ) { + + if( !str ) return; + + if ( !strcmp(str, "normal") ) { + set = true; + inherit = false; + normal = true; + axes.clear(); + return; + } + + + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple(",", str); + + // Match a pattern of a CSS <string> of length 4, whitespace, CSS <number>. + // (CSS string is quoted with double quotes). + + // Matching must use a Glib::ustring or matching may produce + // subtle errors which may be shown by an "Invalid byte sequence + // in conversion input" error. + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("\"(\\w{4})\"\\s+([-+]?\\d*\\.?\\d+([eE][-+]?\\d+)?)"); + Glib::MatchInfo matchInfo; + + for (auto token: tokens) { + regex->match(token, matchInfo); + if (matchInfo.matches()) { + float value = std::stod(matchInfo.fetch(2)); + axes.insert(std::pair<Glib::ustring,float>(matchInfo.fetch(1), value)); + } + } + + if (!axes.empty()) { + set = true; + inherit = false; + normal = false; + } +}; + +const Glib::ustring SPIFontVariationSettings::get_value() const +{ + if (this->normal) return Glib::ustring("normal"); + auto ret = Glib::ustring(""); + for(auto it: axes) { + ret += "'" + it.first + "' " + Glib::ustring::format(it.second) + ", "; + } + if (!ret.empty()) ret.erase(ret.size() - 2); + return ret; +} + +void +SPIFontVariationSettings::cascade( const SPIBase* const parent ) { + if( const SPIFontVariationSettings* p = dynamic_cast<const SPIFontVariationSettings*>(parent) ) { + if( !set || inherit ) { // Always inherits + normal = p->normal; + axes.clear(); + axes = p->axes; + } + } else { + std::cerr << "SPIFontVariationSettings::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIFontVariationSettings::merge( const SPIBase* const parent ) { + if( const SPIFontVariationSettings* p = dynamic_cast<const SPIFontVariationSettings*>(parent) ) { + // if( inherits ) { 'font-variation-settings' always inherits. + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + normal = p->normal; + axes = p->axes; + } + } +} + +bool +SPIFontVariationSettings::operator==(const SPIBase& rhs) { + if( const SPIFontVariationSettings* r = dynamic_cast<const SPIFontVariationSettings*>(&rhs) ) { + if( normal && r->normal ) { return true; } + if( normal != r->normal ) { return false; } + return axes == r->axes; + } else { + return false; + } +} + +// Generate a string useful for passing to Pango, etc. +const Glib::ustring +SPIFontVariationSettings::toString() const { + + Inkscape::CSSOStringStream os; + for (const auto & axe : axes){ + os << axe.first << "=" << axe.second << ","; + } + + std::string string = os.str(); // Glib::ustring doesn't have pop_back() + if (!string.empty()) { + string.pop_back(); // Delete extra comma at end + } + + return string; +} + +// Helpers for SPIEnum ----------------------------------------------------- + +// The default exists to satisfy linking of derived classes but must never be called +template <typename T> static SPStyleEnum const *get_enums() { g_assert_not_reached(); return nullptr; } + +template <> SPStyleEnum const *get_enums<SPBlendMode>() { return enum_blend_mode; } +template <> SPStyleEnum const *get_enums<SPColorInterpolation>() { return enum_color_interpolation; } +template <> SPStyleEnum const *get_enums<SPColorRendering>() { return enum_color_rendering; } +template <> SPStyleEnum const *get_enums<SPCSSBaseline>() { return enum_baseline; } +template <> SPStyleEnum const *get_enums<SPCSSDirection>() { return enum_direction; } +template <> SPStyleEnum const *get_enums<SPCSSDisplay>() { return enum_display; } +template <> SPStyleEnum const *get_enums<SPCSSFontVariantAlternates>() { return enum_font_variant_alternates; } +template <> SPStyleEnum const *get_enums<SPCSSTextAlign>() { return enum_text_align; } +template <> SPStyleEnum const *get_enums<SPCSSTextOrientation>() { return enum_text_orientation; } +template <> SPStyleEnum const *get_enums<SPCSSTextTransform>() { return enum_text_transform; } +template <> SPStyleEnum const *get_enums<SPCSSWritingMode>() { return enum_writing_mode; } +template <> SPStyleEnum const *get_enums<SPEnableBackground>() { return enum_enable_background; } +template <> SPStyleEnum const *get_enums<SPImageRendering>() { return enum_image_rendering; } +template <> SPStyleEnum const *get_enums<SPIsolation>() { return enum_isolation; } +template <> SPStyleEnum const *get_enums<SPOverflow>() { return enum_overflow; } +template <> SPStyleEnum const *get_enums<SPShapeRendering>() { return enum_shape_rendering; } +template <> SPStyleEnum const *get_enums<SPStrokeCapType>() { return enum_stroke_linecap; } +template <> SPStyleEnum const *get_enums<SPStrokeJoinType>() { return enum_stroke_linejoin; } +template <> SPStyleEnum const *get_enums<SPTextAnchor>() { return enum_text_anchor; } +template <> SPStyleEnum const *get_enums<SPTextRendering>() { return enum_text_rendering; } +template <> SPStyleEnum const *get_enums<SPVisibility>() { return enum_visibility; } +template <> SPStyleEnum const *get_enums<SPWhiteSpace>() { return enum_white_space; } +template <> SPStyleEnum const *get_enums<SPWindRule>() { return enum_clip_rule; } +template <> SPStyleEnum const *get_enums<SPCSSFontStyle>() { return enum_font_style; } +template <> SPStyleEnum const *get_enums<SPCSSFontVariant>() { return enum_font_variant; } +template <> SPStyleEnum const *get_enums<SPCSSFontWeight>() { return enum_font_weight; } +template <> SPStyleEnum const *get_enums<SPCSSFontStretch>() { return enum_font_stretch; } +template <> SPStyleEnum const *get_enums<SPCSSFontVariantPosition>() { return enum_font_variant_position; } +template <> SPStyleEnum const *get_enums<SPCSSFontVariantCaps>() { return enum_font_variant_caps; } + +// SPIEnum -------------------------------------------------------------- + +template <typename T> +void SPIEnum<T>::update_computed() +{ + computed = value; +} + +template <> +void SPIEnum<SPCSSFontWeight>::update_computed() +{ + // The following is defined in CSS 2.1 + if (value == SP_CSS_FONT_WEIGHT_NORMAL) { + computed = SP_CSS_FONT_WEIGHT_400; + } else if (value == SP_CSS_FONT_WEIGHT_BOLD) { + computed = SP_CSS_FONT_WEIGHT_700; + } else { + computed = value; + } +} + +template <typename T> +void SPIEnum<T>::read(gchar const *str) +{ + + if( !str ) return; + + if( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else { + auto const *enums = get_enums<T>(); + for (unsigned i = 0; enums[i].key; i++) { + if (!strcmp(str, enums[i].key)) { + set = true; + inherit = false; + value = static_cast<T>(enums[i].value); + /* Save copying for values not needing it */ + break; + } + } + + // type-specialized subroutine + update_computed(); + } +} + +template <typename T> +const Glib::ustring SPIEnum<T>::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto const *enums = get_enums<T>(); + for (unsigned i = 0; enums[i].key; ++i) { + if (enums[i].value == static_cast< gint > (this->value) ) { + return Glib::ustring(enums[i].key); + } + } + return Glib::ustring(""); +} + +template <> +void SPIEnum<SPCSSFontWeight>::update_computed_cascade(SPCSSFontWeight const &p_computed) +{ + // strictly, 'bolder' and 'lighter' should go to the next weight + // expressible in the current font family, but that's difficult to + // find out, so jumping by 3 seems an appropriate approximation + if (value == SP_CSS_FONT_WEIGHT_LIGHTER) { + computed = static_cast<SPCSSFontWeight>(std::max<int>(SP_CSS_FONT_WEIGHT_100, int(p_computed) - 3)); + } else if (value == SP_CSS_FONT_WEIGHT_BOLDER) { + computed = static_cast<SPCSSFontWeight>(std::min<int>(SP_CSS_FONT_WEIGHT_900, p_computed + 3)); + } +} + +template <> +void SPIEnum<SPCSSFontStretch>::update_computed_cascade(SPCSSFontStretch const &p_computed) +{ + if (value == SP_CSS_FONT_STRETCH_NARROWER) { + computed = + static_cast<SPCSSFontStretch>(std::max<int>(SP_CSS_FONT_STRETCH_ULTRA_CONDENSED, int(p_computed) - 1)); + } else if (value == SP_CSS_FONT_STRETCH_WIDER) { + computed = static_cast<SPCSSFontStretch>(std::min<int>(SP_CSS_FONT_STRETCH_ULTRA_EXPANDED, p_computed + 1)); + } +} + +template <typename T> +void SPIEnum<T>::cascade(const SPIBase *const parent) +{ + if (const auto *p = dynamic_cast<const SPIEnum<T> *>(parent)) { + if( inherits && (!set || inherit) ) { + computed = p->computed; + } else { + // type-specialized subroutine + update_computed_cascade(p->computed); + } + } else { + std::cerr << "SPIEnum<T>::cascade(): Incorrect parent type" << std::endl; + } +} + +// FIXME Handle font_stretch and font_weight (relative values) New derived class? +template <typename T> +void SPIEnum<T>::update_value_merge(SPIEnum<T> const &p, T smaller, T larger) +{ + g_assert(set); + + if (value == p.value) { + // Leave as is, what does applying "wider" twice do? + } else if ((value == smaller && p.value == larger) || // + (value == larger && p.value == smaller)) { + // Values cancel, unset + set = false; + } else if (value == smaller || value == larger) { + value = computed; + inherit = false; + } +} + +template <> +void SPIEnum<SPCSSFontWeight>::update_value_merge(SPIEnum<SPCSSFontWeight> const &p) +{ + update_value_merge(p, SP_CSS_FONT_WEIGHT_LIGHTER, SP_CSS_FONT_WEIGHT_BOLDER); +} + +template <> +void SPIEnum<SPCSSFontStretch>::update_value_merge(SPIEnum<SPCSSFontStretch> const &p) +{ + update_value_merge(p, SP_CSS_FONT_STRETCH_NARROWER, SP_CSS_FONT_STRETCH_WIDER); +} + +template <typename T> +void SPIEnum<T>::merge(const SPIBase *const parent) +{ + if (const auto *p = dynamic_cast<const SPIEnum<T> *>(parent)) { + if( inherits ) { + if( p->set && !p->inherit ) { + if( !set || inherit ) { + set = p->set; + inherit = p->inherit; + value = p->value; + computed = p->computed; // Different from value for font-weight and font-stretch + } else { + // type-specialized subroutine + update_value_merge(*p); + } + } + } + } +} + +template <typename T> +bool SPIEnum<T>::operator==(const SPIBase &rhs) +{ + if (auto *r = dynamic_cast<const SPIEnum<T> *>(&rhs)) { + return (computed == r->computed && SPIBase::operator==(rhs)); + } else { + return false; + } +} + + +#if 0 +// SPIEnumBits ---------------------------------------------------------- +// Used for 'font-variant-xxx' +void +SPIEnumBits::read( gchar const *str ) { + + if( !str ) return; + if( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else { + for (unsigned i = 0; enums[i].key; i++) { + if (!strcmp(str, enums[i].key)) { + set = true; + inherit = false; + value |= enums[i].value; + } + } + /* Save copying for values not needing it */ + computed = value; + } +} + +const Glib::ustring SPIEnumBits::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->value == 0) return Glib::ustring("normal"); + auto ret = Glib::ustring(""); + for (unsigned i = 0; enums[i].key; ++i) { + if (this->value & enums[i].value) { + if (!ret.empty()) ret += " "; + ret += enums[i].key; + } + } + return ret; +} +#endif + +// SPILigatures ----------------------------------------------------- +// Used for 'font-variant-ligatures' +void +SPILigatures::read( gchar const *str ) { + + if( !str ) return; + + value = SP_CSS_FONT_VARIANT_LIGATURES_NORMAL; + if( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else if (!strcmp(str, "normal" )) { + // Defaults for TrueType + inherit = false; + set = true; + } else if (!strcmp(str, "none" )) { + value = SP_CSS_FONT_VARIANT_LIGATURES_NONE; + inherit = false; + set = true; + } else { + // We need to parse in order + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s+", str ); + auto const *enums = enum_font_variant_ligatures; + for(auto & token : tokens) { + for (unsigned j = 0; enums[j].key; ++j ) { + if (token.compare( enums[j].key ) == 0 ) { + set = true; + inherit = false; + if( enums[j].value < SP_CSS_FONT_VARIANT_LIGATURES_NOCOMMON ) { + // Turn on + value |= enums[j].value; + } else { + // Turn off + value &= ~(enums[j].value >> 4); + } + } + } + } + } + computed = value; +} + +/* FIXME:: This whole class is bogus and should be an SPIBitEnum TODO */ + +const Glib::ustring SPILigatures::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->value == SP_CSS_FONT_VARIANT_LIGATURES_NONE) return Glib::ustring("none"); + if (this->value == SP_CSS_FONT_VARIANT_LIGATURES_NORMAL) return Glib::ustring("normal"); + auto ret = Glib::ustring(""); + if (!(value & SP_CSS_FONT_VARIANT_LIGATURES_COMMON)) + ret += "no-common-ligatures "; + if (value & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY) + ret += "discretionary-ligatures "; + if (value & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ) + ret += "historical-ligatures "; + if ( !(value & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL) ) + ret += "no-contextual "; + ret.erase(ret.size() - 1); + return ret; +} + +// SPINumeric ----------------------------------------------------- +// Used for 'font-variant-numeric' +void +SPINumeric::read( gchar const *str ) { + + if( !str ) return; + + value = SP_CSS_FONT_VARIANT_NUMERIC_NORMAL; + if( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else if (!strcmp(str, "normal" )) { + // Defaults for TrueType + inherit = false; + set = true; + } else { + // We need to parse in order + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s+", str ); + auto const *enums = enum_font_variant_numeric; + for(auto & token : tokens) { + for (unsigned j = 0; enums[j].key; ++j ) { + if (token.compare( enums[j].key ) == 0 ) { + set = true; + inherit = false; + value |= enums[j].value; + + // Must switch off incompatible value + switch (enums[j].value ) { + case SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS; + break; + case SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS; + break; + + case SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS; + break; + case SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS; + break; + + case SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS; + break; + case SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS: + value &= ~SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS; + break; + + case SP_CSS_FONT_VARIANT_NUMERIC_NORMAL: + case SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL: + case SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO: + // Do nothing + break; + + default: + std::cerr << "SPINumeric::read(): Invalid value." << std::endl; + break; + } + } + } + } + } + computed = value; +} + +/* FIXME:: This whole class is bogus and should be an SPIBitEnum TODO */ +const Glib::ustring SPINumeric::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->value == 0) return Glib::ustring("normal"); + auto ret = Glib::ustring(""); + auto enums = enum_font_variant_numeric; + for (unsigned i = 1; enums[i].key; ++i) { + // Bitmap is shifted by 1 because normal is zero + if (this->value & (1 << (i - 1))) { + if (!ret.empty()) ret += " "; + ret += enums[i].key; + } + } + return ret; +} + +// SPIEastAsian --------------------------------------------------- +// Used for 'font-variant-east-asian' +void +SPIEastAsian::read( gchar const *str ) { + + if( !str ) return; + + value = SP_CSS_FONT_VARIANT_EAST_ASIAN_NORMAL; + if( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else if (!strcmp(str, "normal" )) { + // Defaults for TrueType + inherit = false; + set = true; + } else { + // We need to parse in order + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s+", str ); + auto const *enums = enum_font_variant_east_asian; + for(auto & token : tokens) { + for (unsigned j = 0; enums[j].key; ++j ) { + if (token.compare( enums[j].key ) == 0 ) { + set = true; + inherit = false; + + // Must switch off incompatible value (turn on correct one below) + switch (enums[j].value ) { + case SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL: + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78; + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83; + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90; + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04; + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED; + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL; + break; + + case SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH: + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH; + break; + case SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH: + value &= ~SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH; + break; + + case SP_CSS_FONT_VARIANT_EAST_ASIAN_NORMAL: + case SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY: + // Do nothing + break; + + default: + std::cerr << "SPIEastasian::read(): Invalid value." << std::endl; + break; + } + + value |= enums[j].value; + } + } + } + } + computed = value; +} + +const Glib::ustring SPIEastAsian::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->value == 0) return Glib::ustring("normal"); + auto ret = Glib::ustring(""); + unsigned j = 1; + auto enums = enum_font_variant_east_asian; + for (unsigned i = 0; enums[i].key; ++i) { + if (this->value & (1 << i)) { + if (!ret.empty()) ret += " "; + ret += enums[i].key; + } + } + return ret; +} + +// SPIString ------------------------------------------------------------ + +void +SPIString::read( gchar const *str ) { + + if( !str ) return; + + clear(); + + if (style_src == SP_STYLE_SRC_ATTRIBUTE && id() == SP_ATTR_D) { + return; + } + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + } else if (!g_strcmp0(str, get_default_value())) { + // no need to copy string + set = true; + } else { + Glib::ustring str_temp; + + if (id() == SP_PROP_FONT_FAMILY) { + // Family names may be quoted in CSS, internally we use unquoted names. + str_temp = str; + css_font_family_unquote( str_temp ); + str = str_temp.c_str(); + } else if (id() == SP_PROP_INKSCAPE_FONT_SPEC) { + str_temp = str; + css_unquote( str_temp ); + str = str_temp.c_str(); + } + + set = true; + _value = g_strdup(str); + } +} + + +/** + * Value as it should be written to CSS representation, including quotes if needed. + */ +const Glib::ustring SPIString::get_value() const +{ + Glib::ustring val; + + if (set && inherit) { + val = "inherit"; + } else if (auto *v = value()) { + val = v; + + if (id() == SP_PROP_FONT_FAMILY) { + css_font_family_quote(val); + } else if (id() == SP_PROP_INKSCAPE_FONT_SPEC) { + css_quote(val); + } + } + + return val; +} + +char const *SPIString::value() const +{ + return _value ? _value : get_default_value(); +} + +char const *SPIString::get_default_value() const +{ + switch (id()) { + case SP_PROP_FONT_FAMILY: + return "sans-serif"; + case SP_PROP_FONT_FEATURE_SETTINGS: + return "normal"; + default: + return nullptr; + } +} + + +void +SPIString::clear() { + SPIBase::clear(); + g_free(_value); + _value = nullptr; +} + +void +SPIString::cascade( const SPIBase* const parent ) { + if( const SPIString* p = dynamic_cast<const SPIString*>(parent) ) { + if( inherits && (!set || inherit) ) { + g_free(_value); + _value = g_strdup(p->_value); + } + } else { + std::cerr << "SPIString::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIString::merge( const SPIBase* const parent ) { + if( const SPIString* p = dynamic_cast<const SPIString*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + g_free(_value); + _value = g_strdup(p->_value); + } + } + } +} + +bool +SPIString::operator==(const SPIBase& rhs) { + if( const SPIString* r = dynamic_cast<const SPIString*>(&rhs) ) { + return g_strcmp0(_value, r->_value) == 0 && SPIBase::operator==(rhs); + } else { + return false; + } +} + + +// SPIShapes ------------------------------------------------------------ + +SPIShapes::~SPIShapes() { + hrefs_clear(); +} + +SPIShapes::SPIShapes() + : SPIString(false) +{ +} + + +//SPIShapes::~SPIShapes() { +// clear(); // Will segfault if called here. Seems to be already cleared. +//} + + +// Used to add/remove listeners for text wrapped in shapes. +// Note: this is done differently than for patterns, etc. where presentation attributes can be used. +// 'shape-inside' and 'shape-subtract' are only properties. +void +SPIShapes::read( gchar const *str) { + + if (!style) { + std::cerr << "SPIShapes::read: no style!" << std::endl; + return; + } + + if( !str ) return; + + SPIString::read(str); + + // The object/repr this property is connected to.. + SPObject* object = style->object; + if (!object) { + std::cout << " No object" << std::endl; + return; + } + + // clear(); // Already cleared! (In SPStyle::read.) Calling again causes segfault. + + // Add new listeners + std::vector<Glib::ustring> shapes_url = Glib::Regex::split_simple(" ", str); + for (auto shape_url : shapes_url) { + if ( shape_url.compare(0,5,"url(#") != 0 || shape_url.compare(shape_url.size()-1,1,")") != 0 ){ + std::cerr << "SPIShapes::read: Invalid shape value: " << shape_url << std::endl; + } else { + auto uri = extract_uri(shape_url.c_str()); // Do before we erase "url(#" + + shape_url.erase(0,5); + shape_url.erase(shape_url.size()-1,1); + + shape_ids.push_back(shape_url); + + // This ups the href count of the shape. This is required so that vacuuming a + // document does not delete shapes stored in <defs>. + SPShapeReference *href = new SPShapeReference(object); + + if (href->try_attach(uri.c_str())) { + hrefs.emplace_back(href); + } else { + delete href; + } + } + } +} + +void +SPIShapes::clear() { + + SPIBase::clear(); + + shape_ids.clear(); + + hrefs_clear(); +} + +void SPIShapes::hrefs_clear() +{ + for (auto href : hrefs) { + delete href; + } + hrefs.clear(); +} + +// SPIColor ------------------------------------------------------------- + +// Used for 'color', 'text-decoration-color', 'flood-color', 'lighting-color', and 'stop-color'. +// (The last three have yet to be implemented.) +// CSS3: 'currentcolor' is allowed value and is equal to inherit for the 'color' property. +// FIXME: We should preserve named colors, hsl colors, etc. +void SPIColor::read( gchar const *str ) { + + if( !str ) return; + + set = false; + inherit = false; + currentcolor = false; + if ( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else if ( !strcmp(str, "currentColor") ) { + set = true; + currentcolor = true; + if (id() == SP_PROP_COLOR) { + inherit = true; // CSS3 + } else { + setColor( style->color.value.color ); + } + } else { + guint32 const rgb0 = sp_svg_read_color(str, 0xff); + if (rgb0 != 0xff) { + setColor(rgb0); + set = true; + } + } +} + +const Glib::ustring SPIColor::get_value() const +{ + // currentcolor goes first to handle special case for 'color' property + if (this->currentcolor) return Glib::ustring("currentColor"); + if (this->inherit) return Glib::ustring("inherit"); + return this->value.color.toString(); +} + +void +SPIColor::cascade( const SPIBase* const parent ) { + if( const SPIColor* p = dynamic_cast<const SPIColor*>(parent) ) { + if( (inherits && !set) || inherit) { // FIXME verify for 'color' + if( !(inherit && currentcolor) ) currentcolor = p->currentcolor; + setColor( p->value.color ); + } else { + // Add CSS4 Color: Lighter, Darker + } + } else { + std::cerr << "SPIColor::cascade(): Incorrect parent type" << std::endl; + } + +} + +void +SPIColor::merge( const SPIBase* const parent ) { + if( const SPIColor* p = dynamic_cast<const SPIColor*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + currentcolor = p->currentcolor; + value.color = p->value.color; + } + } + } +} + +bool +SPIColor::operator==(const SPIBase& rhs) { + if( const SPIColor* r = dynamic_cast<const SPIColor*>(&rhs) ) { + + if ( (this->currentcolor != r->currentcolor ) || + (this->value.color != r->value.color ) || + (this->value.color.icc != r->value.color.icc ) || + (this->value.color.icc && r->value.color.icc && + this->value.color.icc->colorProfile != r->value.color.icc->colorProfile && + this->value.color.icc->colors != r->value.color.icc->colors ) ) { + return false; + } + + return SPIBase::operator==(rhs); + + } else { + return false; + } +} + + + +// SPIPaint ------------------------------------------------------------- + +// Paint is used for 'fill' and 'stroke'. SPIPaint perhaps should be derived from SPIColor. +// 'style' is set in SPStyle::SPStyle or in the legacy SPIPaint::read( gchar, style, document ) +// It is needed for computed value when value is 'currentColor'. It is also needed to +// find the object for creating an href (this is done through document but should be done +// directly so document not needed.. FIXME). + +SPIPaint::~SPIPaint() { + if( value.href ) { + clear(); + delete value.href; + value.href = nullptr; + } +} + +/** + * Set SPIPaint object from string. + * + * \pre paint == \&style.fill || paint == \&style.stroke. + */ +void +SPIPaint::read( gchar const *str ) { + + // std::cout << "SPIPaint::read: Entrance: " << " |" << (str?str:"null") << "|" << std::endl; + // if( style ) { + // std::cout << " document: " << (void*)style->document << std::endl; + // std::cout << " object: " << (style->object?"present":"null") << std::endl; + // if( style->object ) + // std::cout << " : " << (style->object->getId()?style->object->getId():"no ID") + // << " document: " << (style->object->document?"yes":"no") << std::endl; + // } + + if(!str ) return; + + reset( false ); // Do not init + + // Is this necessary? + while (g_ascii_isspace(*str)) { + ++str; + } + + if (streq(str, "inherit")) { + set = true; + inherit = true; + } else { + // Read any URL first. The other values can be stand-alone or backup to the URL. + + if ( strneq(str, "url", 3) ) { + + // FIXME: THE FOLLOWING CODE SHOULD BE PUT IN A PRIVATE FUNCTION FOR REUSE + auto uri = extract_uri(str, &str); // std::string + if(uri.empty()) { + std::cerr << "SPIPaint::read: url is empty or invalid" << std::endl; + } else if (!style ) { + std::cerr << "SPIPaint::read: url with empty SPStyle pointer" << std::endl; + } else { + set = true; + SPDocument *document = (style->object) ? style->object->document : nullptr; + + // Create href if not done already + if (!value.href) { + + if (style->object) { + value.href = new SPPaintServerReference(style->object); + } else if (document) { + value.href = new SPPaintServerReference(document); + } else { + std::cerr << "SPIPaint::read: No valid object or document!" << std::endl; + return; + } + + if (this == &style->fill) { + style->fill_ps_changed_connection = value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); + } else { + style->stroke_ps_changed_connection = value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); + } + } + + // TODO check what this does in light of move away from union + sp_style_set_ipaint_to_uri_string(style, this, uri.c_str()); + } + } + + while ( g_ascii_isspace(*str) ) { + ++str; + } + + if (streq(str, "currentColor")) { + set = true; + paintOrigin = SP_CSS_PAINT_ORIGIN_CURRENT_COLOR; + if (style) { + setColor( style->color.value.color ); + } else { + // Normally an SPIPaint is part of an SPStyle and the value of 'color' is + // available. SPIPaint can be used 'stand-alone' (e.g. to parse color values) in + // which case a value of 'currentColor' is meaningless, thus we shouldn't reach + // here. + std::cerr << "SPIPaint::read(): value is 'currentColor' but 'color' not available." << std::endl; + setColor( 0 ); + } + } else if (streq(str, "context-fill")) { + set = true; + paintOrigin = SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + } else if (streq(str, "context-stroke")) { + set = true; + paintOrigin = SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + } else if (streq(str, "none")) { + set = true; + noneSet = true; + } else { + guint32 const rgb0 = sp_svg_read_color(str, &str, 0xff); + if (rgb0 != 0xff) { + setColor( rgb0 ); + set = true; + + while (g_ascii_isspace(*str)) { + ++str; + } + if (strneq(str, "icc-color(", 10)) { + SVGICCColor* tmp = new SVGICCColor(); + if ( ! sp_svg_read_icc_color( str, &str, tmp ) ) { + delete tmp; + tmp = nullptr; + } + value.color.icc = tmp; + } + } + } + } +} + +// Stand-alone read (Legacy read()), used multiple places, e.g. sp-stop.cpp +// This function should not be necessary. FIXME +void +SPIPaint::read( gchar const *str, SPStyle &style_in, SPDocument *document_in ) { + style = &style_in; + style->document = document_in; + read( str ); +} + +const Glib::ustring SPIPaint::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->noneSet) return Glib::ustring("none"); + // url must go first as other values can serve as fallbacks + auto ret = Glib::ustring(""); + if (this->value.href && this->value.href->getURI()) { + ret += this->value.href->getURI()->cssStr(); + } + switch(this->paintOrigin) { + case SP_CSS_PAINT_ORIGIN_CURRENT_COLOR: + if (!ret.empty()) ret += " "; + ret += "currentColor"; + break; + case SP_CSS_PAINT_ORIGIN_CONTEXT_FILL: + if (!ret.empty()) ret += " "; + ret += "context-fill"; + break; + case SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE: + if (!ret.empty()) ret += " "; + ret += "context-stroke"; + break; + case SP_CSS_PAINT_ORIGIN_NORMAL: + if (this->colorSet) { + char color_buf[8]; + sp_svg_write_color(color_buf, sizeof(color_buf), this->value.color.toRGBA32(0)); + if (!ret.empty()) ret += " "; + ret += color_buf; + } + if (this->value.color.icc) { + ret += " icc-color("; + ret += this->value.color.icc->colorProfile; + for(auto i: this->value.color.icc->colors) { + ret += ", " + Glib::ustring::format(i); + } + ret += ")"; + } + break; + } + return ret; +} + +void +SPIPaint::clear() { + // std::cout << "SPIPaint::clear(): " << name << std::endl; + reset( true ); // Reset and Init +} + +void +SPIPaint::reset( bool init ) { + + // std::cout << "SPIPaint::reset(): " << name << " " << init << std::endl; + SPIBase::clear(); + paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + colorSet = false; + noneSet = false; + value.color.set( false ); + if (value.href){ + if (value.href->getObject()) { + value.href->detach(); + } + } + if( init ) { + if (id() == SP_PROP_FILL) { + // 'black' is default for 'fill' + setColor(0.0, 0.0, 0.0); + } else if (id() == SP_PROP_TEXT_DECORATION_COLOR) { + // currentcolor = true; + } + } +} + +void +SPIPaint::cascade( const SPIBase* const parent ) { + + // std::cout << "SPIPaint::cascade" << std::endl; + if( const SPIPaint* p = dynamic_cast<const SPIPaint*>(parent) ) { + if (!set || inherit) { // Always inherits + + reset( false ); // Do not init + + if( p->isPaintserver() ) { + if( p->value.href) { + // Why can we use p->document ? + sp_style_set_ipaint_to_uri( style, this, p->value.href->getURI(), p->value.href->getOwnerDocument()); + } else { + std::cerr << "SPIPaint::cascade: Expected paint server not found." << std::endl; + } + } else if( p->isColor() ) { + setColor( p->value.color ); + } else if( p->isNoneSet() ) { + noneSet = true; + } else if( p->paintOrigin == SP_CSS_PAINT_ORIGIN_CURRENT_COLOR ) { + paintOrigin = SP_CSS_PAINT_ORIGIN_CURRENT_COLOR; + setColor( style->color.value.color ); + } else if( isNone() ) { + // + } else { + g_assert_not_reached(); + } + } else { + if( paintOrigin == SP_CSS_PAINT_ORIGIN_CURRENT_COLOR ) { + // Update in case color value changed. + setColor( style->color.value.color ); + } + } + + } else { + std::cerr << "SPIPaint::cascade(): Incorrect parent type" << std::endl; + } + +} + +void +SPIPaint::merge( const SPIBase* const parent ) { + if( const SPIPaint* p = dynamic_cast<const SPIPaint*>(parent) ) { + // if( inherits ) { Paint always inherits + if( (!set || inherit) && p->set && !(p->inherit) ) { + this->cascade( parent ); // Must call before setting 'set' + set = p->set; + inherit = p->inherit; + } + } +} + +bool +SPIPaint::operator==(const SPIBase& rhs) { + + if( const SPIPaint* r = dynamic_cast<const SPIPaint*>(&rhs) ) { + + if ( (this->isColor() != r->isColor() ) || + (this->isPaintserver() != r->isPaintserver() ) || + (this->paintOrigin != r->paintOrigin ) ) { + return false; + } + + if ( this->isPaintserver() ) { + if( this->value.href == nullptr || r->value.href == nullptr || + this->value.href->getObject() != r->value.href->getObject() ) { + return false; + } + } + + if ( this->isColor() ) { + if ( (this->value.color != r->value.color ) || + (this->value.color.icc != r->value.color.icc ) || + (this->value.color.icc && r->value.color.icc && + this->value.color.icc->colorProfile != r->value.color.icc->colorProfile && + this->value.color.icc->colors != r->value.color.icc->colors ) ) { + return false; + } + } + + return SPIBase::operator==(rhs); + + } else { + return false; + } +} + + + +// SPIPaintOrder -------------------------------------------------------- + +void +SPIPaintOrder::read( gchar const *str ) { + + if( !str ) return; + + g_free(value); + set = false; + inherit = false; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + } else { + set = true; + value = g_strdup(str); + + if (!strcmp(value, "normal")) { + layer[0] = SP_CSS_PAINT_ORDER_NORMAL; + layer_set[0] = true; + } else { + // This certainly can be done more efficiently + gchar** c = g_strsplit(value, " ", PAINT_ORDER_LAYERS + 1); + bool used[3] = {false, false, false}; + unsigned int i = 0; + for( ; i < PAINT_ORDER_LAYERS; ++i ) { + if( c[i] ) { + layer_set[i] = false; + if( !strcmp( c[i], "fill")) { + layer[i] = SP_CSS_PAINT_ORDER_FILL; + layer_set[i] = true; + used[0] = true; + } else if( !strcmp( c[i], "stroke")) { + layer[i] = SP_CSS_PAINT_ORDER_STROKE; + layer_set[i] = true; + used[1] = true; + } else if( !strcmp( c[i], "markers")) { + layer[i] = SP_CSS_PAINT_ORDER_MARKER; + layer_set[i] = true; + used[2] = true; + } else { + std::cerr << "sp_style_read_ipaintorder: illegal value: " << c[i] << std::endl; + break; + } + } else { + break; + } + } + g_strfreev(c); + + // Fill out rest of the layers using the default order + if( !used[0] && i < PAINT_ORDER_LAYERS ) { + layer[i] = SP_CSS_PAINT_ORDER_FILL; + layer_set[i] = false; + ++i; + } + if( !used[1] && i < PAINT_ORDER_LAYERS ) { + layer[i] = SP_CSS_PAINT_ORDER_STROKE; + layer_set[i] = false; + ++i; + } + if( !used[2] && i < PAINT_ORDER_LAYERS ) { + layer[i] = SP_CSS_PAINT_ORDER_MARKER; + layer_set[i] = false; + } + } + } +} + +const Glib::ustring SPIPaintOrder::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto ret = Glib::ustring(""); + for( unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i ) { + if (layer_set[i]) { + if (!ret.empty()) ret += " "; + switch (this->layer[i]) { + case SP_CSS_PAINT_ORDER_NORMAL: + ret += "normal"; + assert( i == 0 ); + break; + case SP_CSS_PAINT_ORDER_FILL: + ret += "fill"; + break; + case SP_CSS_PAINT_ORDER_STROKE: + ret += "stroke"; + break; + case SP_CSS_PAINT_ORDER_MARKER: + ret += "markers"; + break; + } + } else { + break; + } + } + return ret; +} + +void +SPIPaintOrder::cascade( const SPIBase* const parent ) { + if( const SPIPaintOrder* p = dynamic_cast<const SPIPaintOrder*>(parent) ) { + if (!set || inherit) { // Always inherits + for( unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i ) { + layer[i] = p->layer[i]; + layer_set[i] = p->layer_set[i]; + } + g_free( value ); + value = g_strdup(p->value); + } + } else { + std::cerr << "SPIPaintOrder::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIPaintOrder::merge( const SPIBase* const parent ) { + if( const SPIPaintOrder* p = dynamic_cast<const SPIPaintOrder*>(parent) ) { + // if( inherits ) { PaintOrder always inherits + if( (!set || inherit) && p->set && !(p->inherit) ) { + this->cascade( parent ); // Must call be setting 'set' + set = p->set; + inherit = p->inherit; + } + } +} + +bool +SPIPaintOrder::operator==(const SPIBase& rhs) { + if( const SPIPaintOrder* r = dynamic_cast<const SPIPaintOrder*>(&rhs) ) { + if( layer[0] == SP_CSS_PAINT_ORDER_NORMAL && + r->layer[0] == SP_CSS_PAINT_ORDER_NORMAL ) return SPIBase::operator==(rhs); + for (unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i ) { + if( layer[i] != r->layer[i] ) return false; + } + return SPIBase::operator==(rhs); + } else { + return false; + } +} + + + +// SPIFilter ------------------------------------------------------------ + +SPIFilter::~SPIFilter() { + if( href ) { + clear(); + delete href; + href = nullptr; + } +} + +void +SPIFilter::read( gchar const *str ) { + + if( !str ) return; + + clear(); + + if ( streq(str, "inherit") ) { + set = true; + inherit = true; + } else if (streq(str, "none")) { + set = true; + } else if (strneq(str, "url", 3)) { + auto uri = extract_uri(str); + if (uri.empty()) { + std::cerr << "SPIFilter::read: url is empty or invalid" << std::endl; + return; + } else if (!style) { + std::cerr << "SPIFilter::read: url with empty SPStyle pointer" << std::endl; + return; + } + set = true; + + // Create href if not already done. + if (!href) { + if (style->object) { + href = new SPFilterReference(style->object); + } + // Do we have href now? + if ( href ) { + href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_filter_ref_changed), style)); + } else { + std::cerr << "SPIFilter::read(): Could not allocate 'href'" << std::endl; + return; + } + } + + // We have href + try { + href->attach(Inkscape::URI(uri.c_str())); + } catch (Inkscape::BadURIException &e) { + std::cerr << "SPIFilter::read() " << e.what() << std::endl; + href->detach(); + } + + } else { + std::cerr << "SPIFilter::read(): malformed value: " << str << std::endl; + } +} + +const Glib::ustring SPIFilter::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->href) return this->href->getURI()->cssStr(); + return Glib::ustring(""); +} + +void +SPIFilter::clear() { + + SPIBase::clear(); + if( href ) { + if( href->getObject() ) { + href->detach(); + } + } +} + +void +SPIFilter::cascade( const SPIBase* const parent ) { + if( const SPIFilter* p = dynamic_cast<const SPIFilter*>(parent) ) { + if( inherit ) { // Only inherits if 'inherit' true/ + // FIXME: This is rather unlikely so ignore for now. + (void)p; + std::cerr << "SPIFilter::cascade: value 'inherit' not supported." << std::endl; + } else { + // Do nothing + } + } else { + std::cerr << "SPIFilter::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIFilter::merge( const SPIBase* const parent ) { + if( const SPIFilter* p = dynamic_cast<const SPIFilter*>(parent) ) { + // The "correct" thing to do is to combine the filter primitives. + // The next best thing is to keep any filter on this object. If there + // is no filter on this object, then use any filter on the parent. + if( (!set || inherit) && p->href && p->href->getObject() ) { // is the getObject() needed? + set = p->set; + inherit = p->inherit; + if( href ) { + // If we already have an href, use it (unlikely but heck...) + if( href->getObject() ) { + href->detach(); + } + } else { + // If we don't have an href, create it + if( style->document ) { // FIXME + href = new SPFilterReference(style->document); + //href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_filter_ref_changed), style)); + } else if (style->object) { + href = new SPFilterReference(style->object); + } + } + if( href ) { + // If we now have an href, try to attach parent filter + try { + href->attach(*p->href->getURI()); + } catch (Inkscape::BadURIException &e) { + std::cerr << "SPIFilter::merge: " << e.what() << std::endl; + href->detach(); + } + } + } + } +} + +// FIXME +bool +SPIFilter::operator==(const SPIBase& rhs) { + if( const SPIFilter* r = dynamic_cast<const SPIFilter*>(&rhs) ) { + (void)r; + return true; + } else { + return false; + } +} + + + +// SPIDashArray --------------------------------------------------------- + +void +SPIDashArray::read( gchar const *str ) { + + if( !str ) return; + + set = true; + + if( strcmp( str, "inherit") == 0 ) { + inherit = true; + return; + } + + values.clear(); + + if( strcmp(str, "none") == 0) { + return; + } + + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[(,\\s|\\s)]+", str); + + bool LineSolid = true; + + for (auto token : tokens) { + SPILength spilength; + spilength.read(token.c_str()); + if (spilength.value > 0.00000001) + LineSolid = false; + values.push_back(spilength); + } + + if (LineSolid) { + values.clear(); + } + return; +} + +const Glib::ustring SPIDashArray::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->values.empty()) return Glib::ustring("none"); + auto ret = Glib::ustring(""); + for(auto value: this->values) { + if (!ret.empty()) ret += ", "; + ret += value.toString(); + } + return ret; +} + +void +SPIDashArray::cascade( const SPIBase* const parent ) { + if( const SPIDashArray* p = dynamic_cast<const SPIDashArray*>(parent) ) { + if( !set || inherit ) values = p->values; // Always inherits + } else { + std::cerr << "SPIDashArray::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPIDashArray::merge( const SPIBase* const parent ) { + if( const SPIDashArray* p = dynamic_cast<const SPIDashArray*>(parent) ) { + if( inherits ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + values = p->values; + } + } + } else { + std::cerr << "SPIDashArray::merge(): Incorrect parent type" << std::endl; + } +} + +bool +SPIDashArray::operator==(const SPIBase& rhs) { + if (const SPIDashArray *r = dynamic_cast<const SPIDashArray *>(&rhs)) { + if (values.size() != r->values.size()) { + return false; + } + for (int i = 0; i < values.size(); i++) { + if (values[i] != r->values[i]) { + return false; + } + } + } + return SPIBase::operator==(rhs); +} + + +// SPIFontSize ---------------------------------------------------------- + +/** Indexed by SP_CSS_FONT_SIZE_blah. These seem a bit small */ +float const SPIFontSize::font_size_table[] = {6.0, 8.0, 10.0, 12.0, 14.0, 18.0, 24.0}; +float const SPIFontSize::font_size_default = 12.0; + +void +SPIFontSize::read( gchar const *str ) { + + if( !str ) return; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + } else if ((*str == 'x') || (*str == 's') || (*str == 'm') || (*str == 'l')) { + // xx-small, x-small, etc. + for (unsigned i = 0; enum_font_size[i].key; i++) { + if (!strcmp(str, enum_font_size[i].key)) { + set = true; + inherit = false; + type = SP_FONT_SIZE_LITERAL; + literal = enum_font_size[i].value; + return; + } + } + /* Invalid */ + return; + } else { + SPILength length; + length.set = false; + length.read( str ); + if( length.set ) { + set = true; + inherit = length.inherit; + unit = length.unit; + value = length.value; + computed = length.computed; + /* Set a minimum font size to something much smaller than should ever (ever!) be encountered in a real file. + If a bad SVG file is encountered and this is zero odd things + might happen because the inverse is used in some scaling actions. + */ + if ( computed <= 1.0e-32 ) { computed = 1.0e-32; } + if( unit == SP_CSS_UNIT_PERCENT ) { + type = SP_FONT_SIZE_PERCENTAGE; + } else { + type = SP_FONT_SIZE_LENGTH; + } + } + return; + } +} + +const Glib::ustring SPIFontSize::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + auto ret = Glib::ustring(""); + switch (this->type) { + case SP_FONT_SIZE_LITERAL: + for (unsigned i = 0; enum_font_size[i].key; i++) { + if (enum_font_size[i].value == static_cast< gint > (this->literal) ) { + if (!ret.empty()) ret += " "; + ret += enum_font_size[i].key; + } + } + break; + case SP_FONT_SIZE_LENGTH: + if (prefs->getBool("/options/font/textOutputPx", true)) { + unit = SP_CSS_UNIT_PX; + } + ret += Glib::ustring::format(sp_style_css_size_px_to_units(this->computed, unit)); + ret += sp_style_get_css_unit_string(unit); + break; + case SP_FONT_SIZE_PERCENTAGE: + return Glib::ustring::format(this->value * 100.0) + "%"; + default: + g_error("Invalid FontSize value, not writing it."); + } + return ret; +} + +void +SPIFontSize::cascade( const SPIBase* const parent ) { + if( const SPIFontSize* p = dynamic_cast<const SPIFontSize*>(parent) ) { + if( !set || inherit ) { // Always inherits + computed = p->computed;value = p->value; + + + // Calculate computed based on parent as needed + } else if( type == SP_FONT_SIZE_LITERAL ) { + if( literal < SP_CSS_FONT_SIZE_SMALLER ) { + computed = font_size_table[ literal ]; + } else if( literal == SP_CSS_FONT_SIZE_SMALLER ) { + computed = p->computed / 1.2; + } else if( literal == SP_CSS_FONT_SIZE_LARGER ) { + computed = p->computed * 1.2; + } else { + std::cerr << "SPIFontSize::cascade: Illegal literal value" << std::endl; + } + } else if( type == SP_FONT_SIZE_PERCENTAGE ) { + // Percentage for font size is relative to parent computed (rather than viewport) + computed = p->computed * value; + } else if( type == SP_FONT_SIZE_LENGTH ) { + switch ( unit ) { + case SP_CSS_UNIT_EM: + /* Relative to parent font size */ + computed = p->computed * value; + break; + case SP_CSS_UNIT_EX: + /* Relative to parent font size */ + computed = p->computed * value * 0.5; /* Hack FIXME */ + break; + default: + /* No change */ + break; + } + } + /* Set a minimum font size to something much smaller than should ever (ever!) be encountered in a real file. + If a bad SVG file is encountered and this is zero odd things + might happen because the inverse is used in some scaling actions. + */ + if ( computed <= 1.0e-32 ) { computed = 1.0e-32; } + } else { + std::cerr << "SPIFontSize::cascade(): Incorrect parent type" << std::endl; + } +} + +double +SPIFontSize::relative_fraction() const { + + switch (type) { + case SP_FONT_SIZE_LITERAL: { + switch (literal) { + case SP_CSS_FONT_SIZE_SMALLER: + return 5.0 / 6.0; + + case SP_CSS_FONT_SIZE_LARGER: + return 6.0 / 5.0; + + default: + g_assert_not_reached(); + } + } + + case SP_FONT_SIZE_PERCENTAGE: + return value; + + case SP_FONT_SIZE_LENGTH: { + switch (unit ) { + case SP_CSS_UNIT_EM: + return value; + + case SP_CSS_UNIT_EX: + return value * 0.5; + + default: + g_assert_not_reached(); + } + } + } + g_assert_not_reached(); +} + +void +SPIFontSize::merge( const SPIBase* const parent ) { + if( const SPIFontSize* p = dynamic_cast<const SPIFontSize*>(parent) ) { + if( p->set && !(p->inherit) ) { + // Parent has definined font-size + if( (!set || inherit) ) { + // Computed value same as parent + set = p->set; + inherit = p->inherit; + type = p->type; + unit = p->unit; + literal = p->literal; + value = p->value; + computed = p->computed; // Just to be sure + } else if ( type == SP_FONT_SIZE_LENGTH && + unit != SP_CSS_UNIT_EM && + unit != SP_CSS_UNIT_EX ) { + // Absolute size, computed value already set + } else if ( type == SP_FONT_SIZE_LITERAL && + literal < SP_CSS_FONT_SIZE_SMALLER ) { + // Absolute size, computed value already set + //g_assert( literal < G_N_ELEMENTS(font_size_table) ); + g_assert( computed == font_size_table[literal] ); + } else { + // Relative size + double const child_frac( relative_fraction() ); + set = true; + inherit = false; + computed = p->computed * child_frac; + + if ( ( p->type == SP_FONT_SIZE_LITERAL && + p->literal < SP_CSS_FONT_SIZE_SMALLER ) || + ( p->type == SP_FONT_SIZE_LENGTH && + p->unit != SP_CSS_UNIT_EM && + p->unit != SP_CSS_UNIT_EX ) ) { + // Parent absolute size + type = SP_FONT_SIZE_LENGTH; + + } else { + // Parent relative size + double const parent_frac( p->relative_fraction() ); + if( type == SP_FONT_SIZE_LENGTH ) { + // ex/em + value *= parent_frac; + } else { + value = parent_frac * child_frac; + type = SP_FONT_SIZE_PERCENTAGE; + } + } + } // Relative size + /* Set a minimum font size to something much smaller than should ever (ever!) be encountered in a real file. + If a bad SVG file is encountered and this is zero odd things + might happen because the inverse is used in some scaling actions. + */ + if ( computed <= 1.0e-32 ) { computed = 1.0e-32; } + } // Parent set and not inherit + } else { + std::cerr << "SPIFontSize::merge(): Incorrect parent type" << std::endl; + } +} + +// What about different SVG units? +bool +SPIFontSize::operator==(const SPIBase& rhs) { + if( const SPIFontSize* r = dynamic_cast<const SPIFontSize*>(&rhs) ) { + if( type != r->type ) { return false;} + if( type == SP_FONT_SIZE_LENGTH ) { + if( computed != r->computed ) { return false;} + } else if (type == SP_FONT_SIZE_LITERAL ) { + if( literal != r->literal ) { return false;} + } else { + if( value != r->value ) { return false;} + } + return SPIBase::operator==(rhs); + } else { + return false; + } +} + + + +// SPIFont ---------------------------------------------------------- + +void +SPIFont::read( gchar const *str ) { + + if( !str ) return; + + if( !style ) { + std::cerr << "SPIFont::read(): style is void" << std::endl; + return; + } + + if ( !strcmp(str, "inherit") ) { + set = true; + inherit = true; + } else { + + // Break string into white space separated tokens + std::stringstream os( str ); + Glib::ustring param; + + while (os >> param) { + + // CSS is case insensitive but we're comparing against lowercase strings + Glib::ustring lparam = param.lowercase(); + + if (lparam == "/" ) { + // line_height follows... note: font-size already read + + os >> param; + lparam = param.lowercase(); + style->line_height.readIfUnset( lparam.c_str() ); + + } else { + // Try to parse each property in turn + + decltype(style->font_style) test_style; + test_style.read( lparam.c_str() ); + if( test_style.set ) { + style->font_style = test_style; + continue; + } + + // font-variant (Note: only CSS2.1 value small-caps is valid in shortcut.) + decltype(style->font_variant) test_variant; + test_variant.read( lparam.c_str() ); + if( test_variant.set ) { + style->font_variant = test_variant; + continue; + } + + // font-weight + decltype(style->font_weight) test_weight; + test_weight.read( lparam.c_str() ); + if( test_weight.set ) { + style->font_weight = test_weight; + continue; + } + + // font-stretch (added in CSS 3 Fonts) + decltype(style->font_stretch) test_stretch; + test_stretch.read( lparam.c_str() ); + if( test_stretch.set ) { + style->font_stretch = test_stretch; + continue; + } + + // font-size + decltype(style->font_size) test_size; + test_size.read( lparam.c_str() ); + if( test_size.set ) { + style->font_size = test_size; + continue; + } + + // No valid property value found. + break; + } + } // params + + // The rest must be font-family... + std::string str_s = str; // Why this extra step? + std::string family = str_s.substr( str_s.find( param ) ); + + style->font_family.readIfUnset( family.c_str() ); + + // Everything in shorthand is set per CSS rules, this works since + // properties are read backwards from end to start. + style->font_style.set = true; + style->font_variant.set = true; + style->font_weight.set = true; + style->font_stretch.set = true; + style->font_size.set = true; + style->line_height.set = true; + style->font_family.set = true; + // style->font_size_adjust.set = true; + // style->font_kerning.set = true; + // style->font_language_override.set = true;; + } +} + +const Glib::ustring SPIFont::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + // At the moment, do nothing. We could add a preference to write out + // 'font' shorthand rather than longhand properties. + /* SPIFontSize const *const my_base = dynamic_cast<const SPIFontSize*>(base); + if ( (flags & SP_STYLE_FLAG_ALWAYS) || + ((flags & SP_STYLE_FLAG_IFSET) && this->set) || + ((flags & SP_STYLE_FLAG_IFDIFF) && this->set + && (!my_base->set || this != my_base ))) + { + }*/ + + return Glib::ustring(""); +} + +// void +// SPIFont::cascade( const SPIBase* const parent ) { +// } + +// void +// SPIFont::merge( const SPIBase* const parent ) { +// } + +// Does nothing... +bool +SPIFont::operator==(const SPIBase& rhs) { + if( /* const SPIFont* r = */ dynamic_cast<const SPIFont*>(&rhs) ) { + return SPIBase::operator==(rhs); + } else { + return false; + } +} + + + +// SPIBaselineShift ----------------------------------------------------- + +void +SPIBaselineShift::read( gchar const *str ) { + + if( !str ) return; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + } else if ((*str == 'b') || (*str == 's')) { + // baseline or sub or super + for (unsigned i = 0; enum_baseline_shift[i].key; i++) { + if (!strcmp(str, enum_baseline_shift[i].key)) { + set = true; + inherit = false; + type = SP_BASELINE_SHIFT_LITERAL; + literal = enum_baseline_shift[i].value; + return; + } + } + /* Invalid */ + return; + } else { + SPILength length; + length.read( str ); + set = length.set; + inherit = length.inherit; + unit = length.unit; + value = length.value; + computed = length.computed; + if( unit == SP_CSS_UNIT_PERCENT ) { + type = SP_BASELINE_SHIFT_PERCENTAGE; + } else { + type = SP_BASELINE_SHIFT_LENGTH; + } + return; + } +} + +const Glib::ustring SPIBaselineShift::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto ret = Glib::ustring(""); + switch (this->type) { + case SP_BASELINE_SHIFT_LITERAL: + for (unsigned i = 0; enum_baseline_shift[i].key; i++) { + if (enum_baseline_shift[i].value == static_cast< gint > (this->literal) ) { + if (!ret.empty()) ret += " "; + ret += enum_baseline_shift[i].key; + } + } + break; + case SP_BASELINE_SHIFT_LENGTH: + if( this->unit == SP_CSS_UNIT_EM || this->unit == SP_CSS_UNIT_EX ) { + ret += Glib::ustring::format(this->value); + ret += (this->unit == SP_CSS_UNIT_EM ? "em" : "ex"); + } else { + // must specify px, see inkscape bug 1221626, mozilla bug 234789 + ret += Glib::ustring::format(this->computed) + "px"; + } + break; + case SP_BASELINE_SHIFT_PERCENTAGE: + return Glib::ustring::format(this->value * 100.0) + "%"; + } + return ret; +} + +void +SPIBaselineShift::cascade( const SPIBase* const parent ) { + if( const SPIBaselineShift* p = dynamic_cast<const SPIBaselineShift*>(parent) ) { + SPIFontSize *pfont_size = &(p->style->font_size); + g_assert( pfont_size != nullptr ); + + if( !set || inherit ) { + computed = p->computed; // Shift relative to parent shift, corrected below + } else if (type == SP_BASELINE_SHIFT_LITERAL) { + if( literal == SP_CSS_BASELINE_SHIFT_BASELINE ) { + computed = 0; // No change + } else if (literal == SP_CSS_BASELINE_SHIFT_SUB ) { + // Should use subscript position from font relative to alphabetic baseline + // OpenOffice, Adobe: -0.33, Word -0.14, LaTex about -0.2. + computed = -0.2 * pfont_size->computed; + } else if (literal == SP_CSS_BASELINE_SHIFT_SUPER ) { + // Should use superscript position from font relative to alphabetic baseline + // OpenOffice, Adobe: 0.33, Word 0.35, LaTex about 0.45. + computed = 0.4 * pfont_size->computed; + } else { + /* Illegal value */ + } + } else if (type == SP_BASELINE_SHIFT_PERCENTAGE) { + // Percentage for baseline shift is relative to computed "line-height" + // which is just font-size (see SVG1.1 'font'). + computed = pfont_size->computed * value; + } else if (type == SP_BASELINE_SHIFT_LENGTH) { + switch (unit) { + case SP_CSS_UNIT_EM: + computed = value * pfont_size->computed; + break; + case SP_CSS_UNIT_EX: + computed = value * 0.5 * pfont_size->computed; + break; + default: + /* No change */ + break; + } + } + // baseline-shifts are relative to parent baseline + computed += p->computed; + + } else { + std::cerr << "SPIBaselineShift::cascade(): Incorrect parent type" << std::endl; + } +} + +// This was not defined in the legacy C code, it needs some serious thinking (but is low priority). +// FIX ME +void +SPIBaselineShift::merge( const SPIBase* const parent ) { + if( const SPIBaselineShift* p = dynamic_cast<const SPIBaselineShift*>(parent) ) { + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + value = p->value; + } + } else { + std::cerr << "SPIBaselineShift::merge(): Incorrect parent type" << std::endl; + } +} + +// This is not used but we have it for completeness, it has not been tested. +bool +SPIBaselineShift::operator==(const SPIBase& rhs) { + if( const SPIBaselineShift* r = dynamic_cast<const SPIBaselineShift*>(&rhs) ) { + if( type != r->type ) return false; + if( type == SP_BASELINE_SHIFT_LENGTH ) { + if( computed != r->computed ) return false; + } else if ( type == SP_BASELINE_SHIFT_LITERAL ) { + if( literal != r->literal ) return false; + } else { + if( value != r->value ) return false; + } + return SPIBase::operator==(rhs); + } else { + return false; + } +} + +bool +SPIBaselineShift::isZero() const { + if( type == SP_BASELINE_SHIFT_LITERAL ) { + if( literal == SP_CSS_BASELINE_SHIFT_BASELINE ) return true; + } else { + if( value == 0.0 ) return true; + } + return false; +} + + + +// SPITextDecorationLine ------------------------------------------------ + +void +SPITextDecorationLine::read( gchar const *str ) { + + if( !str ) return; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + } else if (!strcmp(str, "none")) { + set = true; + inherit = false; + underline = false; + overline = false; + line_through = false; + blink = false; + } else { + bool found_one = false; + bool hit_one = false; + + // CSS 2 keywords + bool found_underline = false; + bool found_overline = false; + bool found_line_through = false; + bool found_blink = false; + + // This method ignores inlineid keys and extra delimiters, so " ,,, blink hello" will set + // blink and ignore hello + const gchar *hstr = str; + while (true) { + if (*str == ' ' || *str == ',' || *str == '\0'){ + int slen = str - hstr; + // CSS 2 keywords + while(true){ // not really a loop, used to avoid a goto + hit_one = true; // most likely we will + if ((slen == 9) && strneq(hstr, "underline", slen)){ found_underline = true; break; } + if ((slen == 8) && strneq(hstr, "overline", slen)){ found_overline = true; break; } + if ((slen == 12) && strneq(hstr, "line-through", slen)){ found_line_through = true; break; } + if ((slen == 5) && strneq(hstr, "blink", slen)){ found_blink = true; break; } + if ((slen == 4) && strneq(hstr, "none", slen)){ break; } + + hit_one = false; // whatever this thing is, we do not recognize it + break; + } + found_one |= hit_one; + if(*str == '\0')break; + hstr = str + 1; + } + str++; + } + if (found_one) { + set = true; + inherit = false; + underline = found_underline; + overline = found_overline; + line_through = found_line_through; + blink = found_blink; + } + else { + set = false; + inherit = false; + } + } +} + +const Glib::ustring SPITextDecorationLine::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto ret = Glib::ustring(""); + if (this->underline) ret += " underline"; + if (this->overline) ret += " overline"; + if (this->line_through) ret += " line-through"; + if (this->blink) ret += " blink"; // Deprecated + if (ret.empty()) ret += "none"; + return ret; +} + +void +SPITextDecorationLine::cascade( const SPIBase* const parent ) { + if( const SPITextDecorationLine* p = dynamic_cast<const SPITextDecorationLine*>(parent) ) { + if( inherits && (!set || inherit) ) { + underline = p->underline; + overline = p->overline; + line_through = p->line_through; + blink = p->blink; + } + } else { + std::cerr << "SPITextDecorationLine::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPITextDecorationLine::merge( const SPIBase* const parent ) { + if( const SPITextDecorationLine* p = dynamic_cast<const SPITextDecorationLine*>(parent) ) { + if( inherits ) { // Always inherits... but special rules? + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + underline = p->underline; + overline = p->overline; + line_through = p->line_through; + blink = p->blink; + } + } + } +} + +bool +SPITextDecorationLine::operator==(const SPIBase& rhs) { + if( const SPITextDecorationLine* r = dynamic_cast<const SPITextDecorationLine*>(&rhs) ) { + return + (underline == r->underline ) && + (overline == r->overline ) && + (line_through == r->line_through ) && + (blink == r->blink ) && + SPIBase::operator==(rhs); + } else { + return false; + } +} + + + +// SPITextDecorationStyle ----------------------------------------------- + +void +SPITextDecorationStyle::read( gchar const *str ) { + + if( !str ) return; + + set = false; + inherit = false; + + solid = true; // Default + isdouble = false; + dotted = false; + dashed = false; + wavy = false; + + if (!strcmp(str, "inherit")) { + set = true; + inherit = true; + solid = false; + } else { + // note, these are CSS 3 keywords + bool found_solid = false; + bool found_double = false; + bool found_dotted = false; + bool found_dashed = false; + bool found_wavy = false; + bool found_one = false; + + // this method ignores inlineid keys and extra delimiters, so " ,,, style hello" will set style and ignore hello + // if more than one style is present, the first is used + const gchar *hstr = str; + while (true) { + if (*str == ' ' || *str == ',' || *str == '\0'){ + int slen = str - hstr; + if ( (slen == 5) && strneq(hstr, "solid", slen)){ found_solid = true; found_one = true; break; } + else if ((slen == 6) && strneq(hstr, "double", slen)){ found_double = true; found_one = true; break; } + else if ((slen == 6) && strneq(hstr, "dotted", slen)){ found_dotted = true; found_one = true; break; } + else if ((slen == 6) && strneq(hstr, "dashed", slen)){ found_dashed = true; found_one = true; break; } + else if ((slen == 4) && strneq(hstr, "wavy", slen)){ found_wavy = true; found_one = true; break; } + if(*str == '\0')break; // nothing more to test + hstr = str + 1; + } + str++; + } + if(found_one){ + set = true; + solid = found_solid; + isdouble = found_double; + dotted = found_dotted; + dashed = found_dashed; + wavy = found_wavy; + } + else { + set = false; + inherit = false; + } + } +} + +const Glib::ustring SPITextDecorationStyle::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + if (this->solid) return Glib::ustring("solid"); + if (this->isdouble) return Glib::ustring("double"); + if (this->dotted) return Glib::ustring("dotted"); + if (this->dashed) return Glib::ustring("dashed"); + if (this->wavy) return Glib::ustring("wavy"); + g_error("SPITextDecorationStyle::write(): No valid value for property"); + return Glib::ustring(""); +} + +void +SPITextDecorationStyle::cascade( const SPIBase* const parent ) { + if( const SPITextDecorationStyle* p = dynamic_cast<const SPITextDecorationStyle*>(parent) ) { + if( inherits && (!set || inherit) ) { + solid = p->solid; + isdouble = p->isdouble; + dotted = p->dotted; + dashed = p->dashed; + wavy = p->wavy; + } + } else { + std::cerr << "SPITextDecorationStyle::cascade(): Incorrect parent type" << std::endl; + } +} + +void +SPITextDecorationStyle::merge( const SPIBase* const parent ) { + if( const SPITextDecorationStyle* p = dynamic_cast<const SPITextDecorationStyle*>(parent) ) { + if( inherits ) { // Always inherits... but special rules? + if( (!set || inherit) && p->set && !(p->inherit) ) { + set = p->set; + inherit = p->inherit; + solid = p->solid; + isdouble = p->isdouble; + dotted = p->dotted; + dashed = p->dashed; + wavy = p->wavy; + } + } + } +} + +bool +SPITextDecorationStyle::operator==(const SPIBase& rhs) { + if( const SPITextDecorationStyle* r = dynamic_cast<const SPITextDecorationStyle*>(&rhs) ) { + return + (solid == r->solid ) && + (isdouble == r->isdouble ) && + (dotted == r->dotted ) && + (dashed == r->dashed ) && + (wavy == r->wavy ) && + SPIBase::operator==(rhs); + } else { + return false; + } +} + + + +// TextDecorationColor is handled by SPIPaint (should be SPIColor), default value is "currentColor" +// FIXME + + + +// SPITextDecoration ---------------------------------------------------- + +void +SPITextDecoration::read( gchar const *str ) { + + if( !str ) return; + + bool is_css3 = false; + + decltype(style->text_decoration_line) test_line; + test_line.read( str ); + if( test_line.set ) { + style->text_decoration_line = test_line; + } + + decltype(style->text_decoration_style) test_style; + test_style.read( str ); + if( test_style.set ) { + style->text_decoration_style = test_style; + is_css3 = true; + } + + // the color routine must be fed one token at a time - if multiple colors are found the LAST + // one is used ???? then why break on set? + + // This could certainly be designed better + decltype(style->text_decoration_color) test_color; + test_color.setStylePointer( style ); + test_color.read( "currentColor" ); // Default value + test_color.set = false; + const gchar *hstr = str; + while (true) { + if (*str == ' ' || *str == ',' || *str == '\0'){ + int slen = str - hstr; + gchar *frag = g_strndup(hstr,slen+1); // only send one piece at a time, since keywords may be intermixed + + if( strcmp( frag, "none" ) != 0 ) { // 'none' not allowed + test_color.read( frag ); + } + + free(frag); + if( test_color.set ) { + style->text_decoration_color = test_color; + is_css3 = true; + break; + } + test_color.read( "currentColor" ); // Default value + test_color.set = false; + if( *str == '\0' )break; + hstr = str + 1; + } + str++; + } + + // If we read a style or color then we have CSS3 which require any non-set values to be + // set to their default values. + if( is_css3 ) { + style->text_decoration_line.set = true; + style->text_decoration_style.set = true; + style->text_decoration_color.set = true; + } + + // If we set text_decoration_line, then update style_td (for CSS2 text-decoration) + if( style->text_decoration_line.set ) { + style_td = style; + } +} + +// Returns CSS2 'text-decoration' (using settings in SPTextDecorationLine) +// This is required until all SVG renderers support CSS3 'text-decoration' +const Glib::ustring SPITextDecoration::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + return style->text_decoration_line.get_value(); +} +const Glib::ustring +SPITextDecoration::write( guint const flags, SPStyleSrc const &style_src_req, SPIBase const *const base) const { + SPITextDecoration const *const my_base = dynamic_cast<const SPITextDecoration*>(base); + if ( (flags & SP_STYLE_FLAG_ALWAYS) || + ((flags & SP_STYLE_FLAG_IFSET) && style->text_decoration_line.set) || + ((flags & SP_STYLE_FLAG_IFDIFF) && style->text_decoration_line.set + && (!my_base->style->text_decoration_line.set || + style->text_decoration_line != my_base->style->text_decoration_line ))) + { + return (name() + ":" + this->get_value() + important_str() + ";"); + } + return Glib::ustring(""); +} + +void +SPITextDecoration::cascade( const SPIBase* const parent ) { + if( const SPITextDecoration* p = dynamic_cast<const SPITextDecoration*>(parent) ) { + if( style_td == nullptr ) { + style_td = p->style_td; + } + } else { + std::cerr << "SPITextDecoration::cascade(): Incorrect parent type" << std::endl; + } + +} + +void +SPITextDecoration::merge( const SPIBase* const parent ) { + if( const SPITextDecoration* p = dynamic_cast<const SPITextDecoration*>(parent) ) { + if( style_td == nullptr ) { + style_td = p->style_td; + } + } else { + std::cerr << "SPITextDecoration::merge(): Incorrect parent type" << std::endl; + } + +} + +// Use CSS2 value +bool +SPITextDecoration::operator==(const SPIBase& rhs) { + if( const SPITextDecoration* r = dynamic_cast<const SPITextDecoration*>(&rhs) ) { + return (style->text_decoration_line == r->style->text_decoration_line && + SPIBase::operator==(rhs)); + } else { + return false; + } +} + +// SPIVectorEffect ------------------------------------------------ + +void +SPIVectorEffect::read( gchar const *str ) { + + if( !str ) return; + + if (!strcmp(str, "none")) { + set = true; + stroke = false; + size = false; + rotate = false; + fixed = false; + } else { + bool found_one = false; + bool hit_one = false; + + bool found_stroke = false; + bool found_size = false; + bool found_rotate = false; + bool found_fixed = false; + + const gchar *hstr = str; + while (true) { + if (*str == ' ' || *str == ',' || *str == '\0'){ + int slen = str - hstr; + + while(true){ // not really a loop, used to avoid a goto + hit_one = true; // most likely we will + if ((slen == 18) && strneq(hstr, "non-scaling-stroke", slen)){ found_stroke = true; break; } + if ((slen == 16) && strneq(hstr, "non-scaling-size", slen)){ found_size = true; break; } + if ((slen == 12) && strneq(hstr, "non-rotation", slen)){ found_rotate = true; break; } + if ((slen == 14) && strneq(hstr, "fixed-position", slen)){ found_fixed = true; break; } + if ((slen == 4) && strneq(hstr, "none", slen)){ break; } + + hit_one = false; // whatever this thing is, we do not recognize it + break; + } + found_one |= hit_one; + if(*str == '\0')break; + hstr = str + 1; + } + str++; + } + if (found_one) { + set = true; + stroke = found_stroke; + size = found_size; + rotate = found_rotate; + fixed = found_fixed; + } + else { + set = false; + } + } + + // std::cout << " stroke: " << stroke + // << " size: " << size + // << " rotate: " << rotate + // << " fixed: " << fixed + // << std::endl; +} + +const Glib::ustring SPIVectorEffect::get_value() const +{ + if (this->inherit) return Glib::ustring("inherit"); + auto ret = Glib::ustring(""); + if (this->stroke) ret += " non-scaling-stroke"; + if (this->size) ret += " non-scaling-size"; + if (this->rotate) ret += " non-rotation"; + if (this->fixed) ret += " fixed-position"; + if (ret.empty()) { + ret += "none"; + } else { + ret.erase(0, 1); + } + return ret; +} + +// Does not inherit! +// void +// SPIVectorEffect::cascade( const SPIBase* const parent ) { +// } + +// void +// SPIVectorEffect::merge( const SPIBase* const parent ) { +// } + +bool +SPIVectorEffect::operator==(const SPIBase& rhs) { + if( const SPIVectorEffect* r = dynamic_cast<const SPIVectorEffect*>(&rhs) ) { + return + (stroke == r->stroke) && + (size == r->size ) && + (rotate == r->rotate) && + (fixed == r->fixed ) && + SPIBase::operator==(rhs); + } else { + return false; + } +} + + +// template instantiation +template class SPIEnum<SPBlendMode>; +template class SPIEnum<SPColorInterpolation>; +template class SPIEnum<SPColorRendering>; +template class SPIEnum<SPCSSBaseline>; +template class SPIEnum<SPCSSDirection>; +template class SPIEnum<SPCSSDisplay>; +template class SPIEnum<SPCSSFontVariantAlternates>; +template class SPIEnum<SPCSSTextAlign>; +template class SPIEnum<SPCSSTextOrientation>; +template class SPIEnum<SPCSSTextTransform>; +template class SPIEnum<SPCSSWritingMode>; +template class SPIEnum<SPEnableBackground>; +template class SPIEnum<SPImageRendering>; +template class SPIEnum<SPIsolation>; +template class SPIEnum<SPOverflow>; +template class SPIEnum<SPShapeRendering>; +template class SPIEnum<SPStrokeCapType>; +template class SPIEnum<SPStrokeJoinType>; +template class SPIEnum<SPTextAnchor>; +template class SPIEnum<SPTextRendering>; +template class SPIEnum<SPVisibility>; +template class SPIEnum<SPWhiteSpace>; +template class SPIEnum<SPWindRule>; +template class SPIEnum<SPCSSFontStretch>; +template class SPIEnum<SPCSSFontStyle>; +template class SPIEnum<SPCSSFontVariant>; +template class SPIEnum<SPCSSFontVariantPosition>; +template class SPIEnum<SPCSSFontVariantCaps>; +template class SPIEnum<SPCSSFontWeight>; +template class SPIEnum<uint_least16_t>; +template class SPIEnum<uint_least8_t>; + + + +/* ---------------------------- NOTES ----------------------------- */ + +/* + * opacity's effect is cumulative; we set the new value to the combined effect. The default value + * for opacity is 1.0, not inherit. stop-opacity also does not inherit. (Note that stroke-opacity + * and fill-opacity are quite different from opacity, and don't need any special handling.) + * + * Cases: + * - parent & child were each previously unset, in which case the effective + * opacity value is 1.0, and style should remain unset. + * - parent was previously unset (so computed opacity value of 1.0) + * and child was set to inherit. The merged child should + * get a value of 1.0, and shouldn't inherit (lest the new parent + * has a different opacity value). Given that opacity's default + * value is 1.0 (rather than inherit), we might as well have the + * merged child's opacity be unset. + * - parent was previously unset (so opacity 1.0), and child was set to a number. + * The merged child should retain its existing settings (though it doesn't matter + * if we make it unset if that number was 1.0). + * - parent was inherit and child was unset. Merged child should be set to inherit. + * - parent was inherit and child was inherit. (We can't in general reproduce this + * effect (short of introducing a new group), but setting opacity to inherit is rare.) + * If the inherited value was strictly between 0.0 and 1.0 (exclusive) then the merged + * child's value should be set to the product of the two, i.e. the square of the + * inherited value, and should not be marked as inherit. (This decision assumes that it + * is more important to retain the effective opacity than to retain the inheriting + * effect, and assumes that the inheriting effect either isn't important enough to create + * a group or isn't common enough to bother maintaining the code to create a group.) If + * the inherited value was 0.0 or 1.0, then marking the merged child as inherit comes + * closer to maintaining the effect. + * - parent was inherit and child was set to a numerical value. If the child's value + * was 1.0, then the merged child should have the same settings as the parent. + * If the child's value was 0, then the merged child should also be set to 0. + * If the child's value was anything else, then we do the same as for the inherit/inherit + * case above: have the merged child set to the product of the two opacities and not + * marked as inherit, for the same reasons as for that case. + * - parent was set to a value, and child was unset. The merged child should have + * parent's settings. + * - parent was set to a value, and child was inherit. The merged child should + * be set to the product, i.e. the square of the parent's value. + * - parent & child are each set to a value. The merged child should be set to the + * product. + */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/style-internal.h b/src/style-internal.h new file mode 100644 index 0000000..f465bb8 --- /dev/null +++ b/src/style-internal.h @@ -0,0 +1,1296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_STYLE_INTERNAL_H +#define SEEN_SP_STYLE_INTERNAL_H + +/** \file + * SPStyle internal: classes that are internal to SPStyle + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2014, 2018 Tavmjong Bah + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> +#include <vector> +#include <map> + +#include "attributes.h" +#include "style-enums.h" + +#include "color.h" + +#include "object/sp-marker-loc.h" +#include "object/sp-filter.h" +#include "object/sp-filter-reference.h" +#include "object/sp-paint-server-reference.h" +#include "object/sp-shape-reference.h" + +#include "object/uri.h" + +#include "svg/svg-icc-color.h" + +#include "xml/repr.h" + + +static const unsigned SP_STYLE_FLAG_ALWAYS (1 << 2); +static const unsigned SP_STYLE_FLAG_IFSET (1 << 0); +static const unsigned SP_STYLE_FLAG_IFDIFF (1 << 1); +static const unsigned SP_STYLE_FLAG_IFSRC (1 << 3); // If source matches + +enum SPStyleSrc { + SP_STYLE_SRC_UNSET, + SP_STYLE_SRC_ATTRIBUTE, // fill="red" + SP_STYLE_SRC_STYLE_PROP, // style="fill:red" + SP_STYLE_SRC_STYLE_SHEET, // .red { fill:red; } +}; + +/* General comments: + * + * This code is derived from the original C style code in style.cpp. + * + * Overview: + * Style can be obtained (in order of precedence) [CHECK] + * 1. "style" property in an element (style="fill:red"). + * 2. Style sheet, internal or external (<style> rect {fill:red;}</style>). + * 3. Attributes in an element (fill="red"). + * 4. Parent's style. + * A later property overrides an earlier property. This is implemented by + * reading in the properties backwards. If a property is already set, it + * prevents an earlier property from being read. + * + * A declaration with an "!important" rule overrides any other declarations (except those that + * also have an "!important" rule). Attributes can not use the "!important" rule and the rule + * is not inherited. + * + * In order for cascading to work, each element in the tree must be read in from top to bottom + * (parent before child). At each step, if a style property is not explicitly set, the property + * value is taken from the parent. Some properties have "computed" values that depend on: + * the parent's value (e.g. "font-size:larger"), + * another property value ("stroke-width":1em"), or + * an external value ("stroke-width:5%"). + * + * To summarize: + * + * An explicitly set value (including 'inherit') has a 'true' "set" flag. + * The "value" is either explicitly set or inherited. + * The "computed" value (if present) is calculated from "value" and some other input. + * + * Functions: + * write(): Write a property and its value to a string. + * Flags: + * ALWAYS: Always write out property. + * IFSET: Write a property if 'set' flag is true, otherwise return empty string. + * IFDIFF: Write a property if computed values are different, otherwise return empty string, + * This is only used for text!! + * IFSRC Write a property if the source matches the requested source (style sheet, etc.). + * + * read(): Set a property value from a string. + * clear(): Set a property to its default value and set the 'set' flag to false. + * cascade(): Cascade the parent's property values to the child if the child's property + * is unset (and it allows inheriting) or the value is 'inherit'. + * Calculate computed values that depend on parent. + * This requires that the parent already be updated. + * merge(): Merge the property values of a child and a parent that is being deleted, + * attempting to preserve the style of the child. + * operator=: Assignment operator required due to use of templates (in original C code). + * operator==: True if computed values are equal. TO DO: DEFINE EXACTLY WHAT THIS MEANS + * operator!=: Inverse of operator==. + * + * + * Outside dependencies: + * + * The C structures that these classes are evolved from were designed to be embedded in to the + * style structure (i.e they are "internal" and thus have an "I" in the SPI prefix). However, + * they should be reasonably stand-alone and can provide some functionality outside of the style + * structure (i.e. reading and writing style strings). Some properties do need access to other + * properties from the same object (e.g. SPILength sometimes needs to know font size) to + * calculate 'computed' values. Inheritance, of course, requires access to the parent object's + * style class. + * + * The only real outside dependency is SPObject... which is needed in the cases of SPIPaint and + * SPIFilter for setting up the "href". (Currently, SPDocument is needed but this dependency + * should be removed as an "href" only needs the SPDocument for attaching an external document to + * the XML tree [see uri-references.cpp]. If SPDocument is really needed, it can be obtained from + * SPObject.) + * + */ + +/// Virtual base class for all SPStyle internal classes +class SPIBase +{ + +public: + SPIBase(bool inherits_ = true) + : inherits(inherits_), + set(false), + inherit(false), + important(false), + style_src(SP_STYLE_SRC_STYLE_PROP), // Default to property, see bug 1662285. + style(nullptr) + {} + + virtual ~SPIBase() + = default; + + virtual void read( gchar const *str ) = 0; + virtual void readIfUnset(gchar const *str, SPStyleSrc source = SP_STYLE_SRC_STYLE_PROP); + + Glib::ustring important_str() const { + return Glib::ustring(important ? " !important" : ""); + } + + virtual void readAttribute( Inkscape::XML::Node *repr ) { + readIfUnset(repr->attribute(name().c_str()), SP_STYLE_SRC_ATTRIBUTE); + } + + virtual const Glib::ustring get_value() const = 0; + virtual bool shall_write( guint const flags = SP_STYLE_FLAG_IFSET, + SPStyleSrc const &style_src_req = SP_STYLE_SRC_STYLE_PROP, + SPIBase const *const base = nullptr ) const; + virtual const Glib::ustring write( guint const flags = SP_STYLE_FLAG_IFSET, + SPStyleSrc const &style_src_req = SP_STYLE_SRC_STYLE_PROP, + SPIBase const *const base = nullptr ) const; + virtual void clear() { + set = false, inherit = false, important = false; + } + + virtual void cascade( const SPIBase* const parent ) = 0; + virtual void merge( const SPIBase* const parent ) = 0; + + virtual void setStylePointer( SPStyle *style_in ) { + style = style_in; + } + + // Explicit assignment operator required due to templates. + SPIBase& operator=(const SPIBase& rhs) = default; + + // Check apples being compared to apples + virtual bool operator==(const SPIBase& rhs) { + return id() == rhs.id(); + } + + virtual bool operator!=(const SPIBase& rhs) { + return !(*this == rhs); + } + + virtual SPAttributeEnum id() const { return SP_ATTR_INVALID; } + Glib::ustring const &name() const; + + // To do: make private +public: + bool inherits : 1; // Property inherits by default from parent. + bool set : 1; // Property has been explicitly set (vs. inherited). + bool inherit : 1; // Property value set to 'inherit'. + bool important : 1; // Property rule 'important' has been explicitly set. + SPStyleSrc style_src : 2; // Source (attribute, style attribute, style-sheet). + + // To do: make private after g_asserts removed +public: + SPStyle* style; // Used by SPIPaint, SPIFilter... to find values of other properties +}; + + +/** + * Decorator which overrides SPIBase::id() + */ +template <SPAttributeEnum Id, class Base> +class TypedSPI : public Base { + public: + using Base::Base; + + /** + * Get the attribute enum + */ + SPAttributeEnum id() const override { return Id; } + static SPAttributeEnum static_id() { return Id; } + + /** + * Upcast to the base class + */ + Base *upcast() { return static_cast<Base *>(this); } + Base const *upcast() const { return static_cast<Base const *>(this); } +}; + + +/// Float type internal to SPStyle. (Only 'stroke-miterlimit') +class SPIFloat : public SPIBase +{ + +public: + SPIFloat(float value_default = 0.0 ) + : value(value_default), + value_default(value_default) + {} + + ~SPIFloat() override = default; + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + value = value_default; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIFloat& operator=(const SPIFloat& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + float value = 0.0; + +private: + float value_default = 0.0; +}; + +/* + * One might think that the best value for SP_SCALE24_MAX would be ((1<<24)-1), which allows the + * greatest possible precision for fitting [0, 1] fractions into 24 bits. + * + * However, in practice, that gives a problem with 0.5, which falls half way between two fractions + * of ((1<<24)-1). What's worse is that casting double(1<<23) / ((1<<24)-1) to float on x86 + * produces wrong rounding behaviour, resulting in a fraction of ((1<<23)+2.0f) / (1<<24) rather + * than ((1<<23)+1.0f) / (1<<24) as one would expect, let alone ((1<<23)+0.0f) / (1<<24) as one + * would ideally like for this example. + * + * The value (1<<23) is thus best if one considers float conversions alone. + * + * The value 0xff0000 can exactly represent all 8-bit alpha channel values, + * and can exactly represent all multiples of 0.1. I haven't yet tested whether + * rounding bugs still get in the way of conversions to & from float, but my instinct is that + * it's fairly safe because 0xff fits three times inside float's significand. + * + * We should probably use the value 0xffff00 once we support 16 bits per channel and/or LittleCMS, + * though that might need to be accompanied by greater use of double instead of float for + * colours and opacities, to be safe from rounding bugs. + */ +static const unsigned SP_SCALE24_MAX = 0xff0000; +#define SP_SCALE24_TO_FLOAT(v) ((double) (v) / SP_SCALE24_MAX) +#define SP_SCALE24_FROM_FLOAT(v) unsigned(((v) * SP_SCALE24_MAX) + .5) + +/** Returns a scale24 for the product of two scale24 values. */ +#define SP_SCALE24_MUL(_v1, _v2) unsigned((double)(_v1) * (_v2) / SP_SCALE24_MAX + .5) + + +/// 24 bit data type internal to SPStyle. +// Used only for opacity, fill-opacity, stroke-opacity. +// Opacity does not inherit but stroke-opacity and fill-opacity do. +class SPIScale24 : public SPIBase +{ + static unsigned get_default() { return SP_SCALE24_MAX; } + +public: + SPIScale24(bool inherits = true ) + : SPIBase(inherits), + value(get_default()) + {} + + ~SPIScale24() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + value = get_default(); + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIScale24& operator=(const SPIScale24& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + + // To do: make private +public: + unsigned value : 24; +}; + + +enum SPCSSUnit { + SP_CSS_UNIT_NONE, + SP_CSS_UNIT_PX, + SP_CSS_UNIT_PT, + SP_CSS_UNIT_PC, + SP_CSS_UNIT_MM, + SP_CSS_UNIT_CM, + SP_CSS_UNIT_IN, + SP_CSS_UNIT_EM, + SP_CSS_UNIT_EX, + SP_CSS_UNIT_PERCENT +}; + + +/// Length type internal to SPStyle. +// Needs access to 'font-size' and 'font-family' for computed values. +// Used for 'stroke-width' 'stroke-dash-offset' ('none' not handled), text-indent +class SPILength : public SPIBase +{ + +public: + SPILength(float value = 0) + : unit(SP_CSS_UNIT_NONE), + value(value), + computed(value), + value_default(value) + {} + + ~SPILength() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + unit = SP_CSS_UNIT_NONE, value = value_default; + computed = value_default; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPILength& operator=(const SPILength& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + void setDouble(double v); + virtual const Glib::ustring toString(bool wname = false) const; + + // To do: make private + public: + unsigned unit : 4; + float value = 0.f; + float computed = 0.f; + +private: + float value_default = 0.f; +}; + + +/// Extended length type internal to SPStyle. +// Used for: line-height, letter-spacing, word-spacing +class SPILengthOrNormal : public SPILength +{ + +public: + SPILengthOrNormal(float value = 0) + : SPILength(value), + normal(true) + {} + + ~SPILengthOrNormal() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPILength::clear(); + normal = true; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPILengthOrNormal& operator=(const SPILengthOrNormal& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + bool normal : 1; +}; + + +/// Extended length type internal to SPStyle. +// Used for: font-variation-settings +class SPIFontVariationSettings : public SPIBase +{ + +public: + SPIFontVariationSettings() + : normal(true) + {} + + ~SPIFontVariationSettings() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + axes.clear(); + normal = true; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIFontVariationSettings& operator=(const SPIFontVariationSettings& rhs) { + SPIBase::operator=(rhs); + axes = rhs.axes; + normal = rhs.normal; + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + virtual const Glib::ustring toString() const; + + // To do: make private +public: + bool normal : 1; + bool inherit : 1; + std::map<Glib::ustring, float> axes; +}; + + +/// Enum type internal to SPStyle. +// Used for many properties. 'font-stretch' and 'font-weight' must be special cased. +template <typename T> +class SPIEnum : public SPIBase +{ + +public: + SPIEnum(T value = T(), bool inherits = true) : + SPIBase(inherits), + value(value), + value_default(value) + { + update_computed(); + } + + ~SPIEnum() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + value = value_default; + update_computed(); + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIEnum& operator=(const SPIEnum& rhs) { + SPIBase::operator=(rhs); + value = rhs.value; + computed = rhs.computed; + value_default = rhs.value_default; + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + T value{}; + T computed{}; + +private: + T value_default{}; + + //! Update computed from value + void update_computed(); + //! Update computed from parent computed + void update_computed_cascade(T const &parent_computed) {} + //! Update value from parent + //! @pre computed is up to date + void update_value_merge(SPIEnum<T> const &) {} + void update_value_merge(SPIEnum<T> const &, T, T); +}; + + +#if 0 +/// SPIEnum w/ bits, allows values with multiple key words. +class SPIEnumBits : public SPIEnum +{ + +public: + SPIEnumBits( Glib::ustring const &name, SPStyleEnum const *enums, unsigned value = 0, bool inherits = true ) : + SPIEnum( name, enums, value, inherits ) + {} + + ~SPIEnumBits() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; +}; +#endif + + +/// SPIEnum w/ extra bits. The 'font-variants-ligatures' property is a complete mess that needs +/// special handling. For OpenType fonts the values 'common-ligatures', 'contextual', +/// 'no-discretionary-ligatures', and 'no-historical-ligatures' are not useful but we still must be +/// able to parse them. +using _SPCSSFontVariantLigatures_int = typename std::underlying_type<SPCSSFontVariantLigatures>::type; +class SPILigatures : public SPIEnum<_SPCSSFontVariantLigatures_int> +{ + +public: + SPILigatures() : + SPIEnum<_SPCSSFontVariantLigatures_int>(SP_CSS_FONT_VARIANT_LIGATURES_NORMAL) + {} + + ~SPILigatures() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; +}; + + +/// SPIEnum w/ extra bits. The 'font-variants-numeric' property is a complete mess that needs +/// special handling. Multiple key words can be specified, some exclusive of others. +using _SPCSSFontVariantNumeric_int = typename std::underlying_type<SPCSSFontVariantNumeric>::type; +class SPINumeric : public SPIEnum<_SPCSSFontVariantNumeric_int> +{ + +public: + SPINumeric() : + SPIEnum<_SPCSSFontVariantNumeric_int>(SP_CSS_FONT_VARIANT_NUMERIC_NORMAL) + {} + + ~SPINumeric() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; +}; + + +/// SPIEnum w/ extra bits. The 'font-variants-east-asian' property is a complete mess that needs +/// special handling. Multiple key words can be specified, some exclusive of others. +using _SPCSSFontVariantEastAsian_int = typename std::underlying_type<SPCSSFontVariantEastAsian>::type; +class SPIEastAsian : public SPIEnum<_SPCSSFontVariantEastAsian_int> +{ + +public: + SPIEastAsian() : + SPIEnum<_SPCSSFontVariantEastAsian_int>(SP_CSS_FONT_VARIANT_EAST_ASIAN_NORMAL) + {} + + ~SPIEastAsian() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; +}; + + +/// String type internal to SPStyle. +// Used for 'marker', ..., 'font', 'font-family', 'inkscape-font-specification' +class SPIString : public SPIBase +{ + +public: + SPIString(bool inherits = true) + : SPIBase(inherits) + {} + + SPIString(const SPIString &rhs) { *this = rhs; } + + ~SPIString() override { + g_free(_value); + } + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override; // TODO check about value and value_default + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIString& operator=(const SPIString& rhs) { + if (this == &rhs) { + return *this; + } + SPIBase::operator=(rhs); + g_free(_value); + _value = g_strdup(rhs._value); + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + //! Get value if set, or inherited value, or default value (may be NULL) + char const *value() const; + + private: + char const *get_default_value() const; + + gchar *_value = nullptr; +}; + +/// Shapes type internal to SPStyle. +// Used for 'shape-inside', shape-subtract' +// Differs from SPIString by creating/deleting listeners on referenced shapes. +class SPIShapes : public SPIString +{ + void hrefs_clear(); + +public: + ~SPIShapes() override; + SPIShapes(); + SPIShapes(const SPIShapes &) = delete; // Copying causes problems with hrefs. + void read( gchar const *str ) override; + void clear() override; + +public: + // TODO eliminate shape_ids, because shape_ids[0] == hrefs[0]->getObject()->getId() + std::vector<Glib::ustring> shape_ids; + std::vector<SPShapeReference *> hrefs; + //std::vector<std::unique_ptr<SPShapeReference> > hrefs; +}; + +/// Color type internal to SPStyle, FIXME Add string value to store SVG named color. +class SPIColor : public SPIBase +{ + +public: + SPIColor(bool inherits = true) + : SPIBase(inherits) + , currentcolor(false) + { + value.color.set(0); + } + + ~SPIColor() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + value.color.set(0); + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIColor& operator=(const SPIColor& rhs) { + SPIBase::operator=(rhs); + currentcolor = rhs.currentcolor; + value.color = rhs.value.color; + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + void setColor( float r, float g, float b ) { + value.color.set( r, g, b ); + } + + void setColor( guint32 val ) { + value.color.set( val ); + } + + void setColor( SPColor const& color ) { + value.color = color; + } + +public: + bool currentcolor : 1; + // FIXME: remove structure and derive SPIPaint from this class. + struct { + SPColor color; + } value; +}; + + + +#define SP_STYLE_FILL_SERVER(s) ((const_cast<SPStyle *> (s))->getFillPaintServer()) +#define SP_STYLE_STROKE_SERVER(s) ((const_cast<SPStyle *> (s))->getStrokePaintServer()) + +// SVG 2 +enum SPPaintOrigin { + SP_CSS_PAINT_ORIGIN_NORMAL, + SP_CSS_PAINT_ORIGIN_CURRENT_COLOR, + SP_CSS_PAINT_ORIGIN_CONTEXT_FILL, + SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE +}; + + +/// Paint type internal to SPStyle. +class SPIPaint : public SPIBase +{ + +public: + SPIPaint() + : paintOrigin(SP_CSS_PAINT_ORIGIN_NORMAL), + colorSet(false), + noneSet(false) { + value.href = nullptr; + clear(); + } + + ~SPIPaint() override; // Clear and delete href. + void read( gchar const *str ) override; + virtual void read( gchar const *str, SPStyle &style, SPDocument *document = nullptr); + const Glib::ustring get_value() const override; + void clear() override; + virtual void reset( bool init ); // Used internally when reading or cascading + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIPaint& operator=(const SPIPaint& rhs) { + SPIBase::operator=(rhs); + paintOrigin = rhs.paintOrigin; + colorSet = rhs.colorSet; + noneSet = rhs.noneSet; + value.color = rhs.value.color; + value.href = rhs.value.href; + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + bool isSameType( SPIPaint const & other ) const { + return (isPaintserver() == other.isPaintserver()) && (colorSet == other.colorSet) && (paintOrigin == other.paintOrigin); + } + + bool isNoneSet() const { + return noneSet; + } + + bool isNone() const { + return !colorSet && !isPaintserver() && (paintOrigin == SP_CSS_PAINT_ORIGIN_NORMAL); + } // TODO refine + + bool isColor() const { + return colorSet && !isPaintserver(); + } + + bool isPaintserver() const { + return value.href && value.href->getObject() != nullptr; + } + + void setColor( float r, float g, float b ) { + value.color.set( r, g, b ); colorSet = true; + } + + void setColor( guint32 val ) { + value.color.set( val ); colorSet = true; + } + + void setColor( SPColor const& color ) { + value.color = color; colorSet = true; + } + + void setNone() {noneSet = true; colorSet=false;} + + // To do: make private +public: + SPPaintOrigin paintOrigin : 2; + bool colorSet : 1; + bool noneSet : 1; + struct { + SPPaintServerReference *href; + SPColor color; + } value; +}; + + +// SVG 2 +enum SPPaintOrderLayer { + SP_CSS_PAINT_ORDER_NORMAL, + SP_CSS_PAINT_ORDER_FILL, + SP_CSS_PAINT_ORDER_STROKE, + SP_CSS_PAINT_ORDER_MARKER +}; + +// Normal maybe should be moved out as is done in other classes. +// This could be replaced by a generic enum class where multiple keywords are allowed and +// where order matters (in contrast to 'text-decoration-line' where order does not matter). + +// Each layer represents a layer of paint which can be a fill, a stroke, or markers. +const size_t PAINT_ORDER_LAYERS = 3; + +/// Paint order type internal to SPStyle +class SPIPaintOrder : public SPIBase +{ + +public: + SPIPaintOrder() { + this->clear(); + } + + SPIPaintOrder(const SPIPaintOrder &rhs) { *this = rhs; } + + ~SPIPaintOrder() override { + g_free( value ); + } + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + for( unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i ) { + layer[i] = SP_CSS_PAINT_ORDER_NORMAL; + layer_set[i] = false; + } + g_free(value); + value = nullptr; + } + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIPaintOrder& operator=(const SPIPaintOrder& rhs) { + if (this == &rhs) { + return *this; + } + SPIBase::operator=(rhs); + for( unsigned i = 0; i < PAINT_ORDER_LAYERS; ++i ) { + layer[i] = rhs.layer[i]; + layer_set[i] = rhs.layer_set[i]; + } + g_free(value); + value = g_strdup(rhs.value); + return *this; + } + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + + // To do: make private +public: + SPPaintOrderLayer layer[PAINT_ORDER_LAYERS]; + bool layer_set[PAINT_ORDER_LAYERS]; + gchar *value = nullptr; // Raw string +}; + + +/// Filter type internal to SPStyle +class SPIDashArray : public SPIBase +{ + +public: + SPIDashArray() = default; + + ~SPIDashArray() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + values.clear(); + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIDashArray& operator=(const SPIDashArray& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private, change double to SVGLength +public: + std::vector<SPILength> values; +}; + +/// Filter type internal to SPStyle +class SPIFilter : public SPIBase +{ + +public: + SPIFilter() + : SPIBase(false) + {} + + ~SPIFilter() override; + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override; + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIFilter& operator=(const SPIFilter& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + SPFilterReference *href = nullptr; +}; + + + +enum { + SP_FONT_SIZE_LITERAL, + SP_FONT_SIZE_LENGTH, + SP_FONT_SIZE_PERCENTAGE +}; + +/// Fontsize type internal to SPStyle (also used by libnrtype/Layout-TNG-Input.cpp). +class SPIFontSize : public SPIBase +{ + +public: + SPIFontSize() { + this->clear(); + } + + ~SPIFontSize() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + type = SP_FONT_SIZE_LITERAL, unit = SP_CSS_UNIT_NONE, + literal = SP_CSS_FONT_SIZE_MEDIUM, value = 12.0, computed = 12.0; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIFontSize& operator=(const SPIFontSize& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + +public: + static float const font_size_default; + + // To do: make private +public: + unsigned type : 2; + unsigned unit : 4; + unsigned literal : 4; + float value; + float computed; + +private: + double relative_fraction() const; + static float const font_size_table[]; +}; + + +/// Font type internal to SPStyle ('font' shorthand) +class SPIFont : public SPIBase +{ + +public: + SPIFont() = default; + + ~SPIFont() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + } + + void cascade( const SPIBase* const /*parent*/ ) override + {} // Done in dependent properties + + void merge( const SPIBase* const /*parent*/ ) override + {} + + SPIFont& operator=(const SPIFont& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } +}; + + +enum { + SP_BASELINE_SHIFT_LITERAL, + SP_BASELINE_SHIFT_LENGTH, + SP_BASELINE_SHIFT_PERCENTAGE +}; + +/// Baseline shift type internal to SPStyle. (This is actually just like SPIFontSize) +class SPIBaselineShift : public SPIBase +{ + +public: + SPIBaselineShift() + : SPIBase(false) { + this->clear(); + } + + ~SPIBaselineShift() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + type=SP_BASELINE_SHIFT_LITERAL, unit=SP_CSS_UNIT_NONE, + literal = SP_CSS_BASELINE_SHIFT_BASELINE, value = 0.0, computed = 0.0; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPIBaselineShift& operator=(const SPIBaselineShift& rhs) = default; + + // This is not used but we have it for completeness, it has not been tested. + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + bool isZero() const; + + // To do: make private +public: + unsigned type : 2; + unsigned unit : 4; + unsigned literal: 2; + float value; // Can be negative + float computed; +}; + +// CSS 2. Changes in CSS 3, where description is for TextDecorationLine, NOT TextDecoration +// See http://www.w3.org/TR/css-text-decor-3/ + +// CSS3 2.2 +/// Text decoration line type internal to SPStyle. THIS SHOULD BE A GENERIC CLASS +class SPITextDecorationLine : public SPIBase +{ + +public: + SPITextDecorationLine() { + this->clear(); + } + + ~SPITextDecorationLine() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + underline = false, overline = false, line_through = false, blink = false; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPITextDecorationLine& operator=(const SPITextDecorationLine& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + bool underline : 1; + bool overline : 1; + bool line_through : 1; + bool blink : 1; // "Conforming user agents are not required to support this value." yay! +}; + +// CSS3 2.2 +/// Text decoration style type internal to SPStyle. THIS SHOULD JUST BE SPIEnum! +class SPITextDecorationStyle : public SPIBase +{ + +public: + SPITextDecorationStyle() { + this->clear(); + } + + ~SPITextDecorationStyle() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + solid = true, isdouble = false, dotted = false, dashed = false, wavy = false; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPITextDecorationStyle& operator=(const SPITextDecorationStyle& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + bool solid : 1; + bool isdouble : 1; // cannot use "double" as it is a reserved keyword + bool dotted : 1; + bool dashed : 1; + bool wavy : 1; +}; + + + +// This class reads in both CSS2 and CSS3 'text-decoration' property. It passes the line, style, +// and color parts to the appropriate CSS3 long-hand classes for reading and storing values. When +// writing out data, we write all four properties, with 'text-decoration' being written out with +// the CSS2 format. This allows CSS1/CSS2 renderers to at least render lines, even if they are not +// the right style. (See http://www.w3.org/TR/css-text-decor-3/#text-decoration-property ) + +/// Text decoration type internal to SPStyle. +class SPITextDecoration : public SPIBase +{ + +public: + SPITextDecoration() = default; + + ~SPITextDecoration() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + const Glib::ustring write( guint const flags = SP_STYLE_FLAG_IFSET, + SPStyleSrc const &style_src_req = SP_STYLE_SRC_STYLE_PROP, + SPIBase const *const base = nullptr ) const override; + void clear() override { + SPIBase::clear(); + style_td = nullptr; + } + + void cascade( const SPIBase* const parent ) override; + void merge( const SPIBase* const parent ) override; + + SPITextDecoration& operator=(const SPITextDecoration& rhs) { + SPIBase::operator=(rhs); + return *this; + } + + // Use CSS2 value + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + +public: + SPStyle* style_td = nullptr; // Style to be used for drawing CSS2 text decorations +}; + + +// These are used to implement text_decoration. The values are not saved to or read from SVG file +struct SPITextDecorationData { + float phase_length; // length along text line,used for phase for dot/dash/wavy + bool tspan_line_start; // is first span on a line + bool tspan_line_end; // is last span on a line + float tspan_width; // from libnrtype, when it calculates spans + float ascender; // the rest from tspan's font + float descender; + float underline_thickness; + float underline_position; + float line_through_thickness; + float line_through_position; +}; + +/// Vector Effects. THIS SHOULD BE A GENERIC CLASS +class SPIVectorEffect : public SPIBase +{ + +public: + SPIVectorEffect() + : SPIBase(false) + { + this->clear(); + } + + ~SPIVectorEffect() override + = default; + + void read( gchar const *str ) override; + const Glib::ustring get_value() const override; + void clear() override { + SPIBase::clear(); + stroke = false; + size = false; + rotate = false; + fixed = false; + } + + // Does not inherit + void cascade( const SPIBase* const parent ) override {}; + void merge( const SPIBase* const parent ) override {}; + + SPIVectorEffect& operator=(const SPIVectorEffect& rhs) = default; + + bool operator==(const SPIBase& rhs) override; + bool operator!=(const SPIBase& rhs) override { + return !(*this == rhs); + } + + // To do: make private +public: + bool stroke : 1; + bool size : 1; + bool rotate : 1; + bool fixed : 1; +}; + +#endif // SEEN_SP_STYLE_INTERNAL_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/style.cpp b/src/style.cpp new file mode 100644 index 0000000..b7e7f92 --- /dev/null +++ b/src/style.cpp @@ -0,0 +1,1730 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG stylesheets implementation. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Peter Moulder <pmoulder@mail.csse.monash.edu.au> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Tavmjong Bah <tavmjong@free.fr> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2005 Monash University + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2014-2015 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style.h" + +#include <cstring> +#include <string> +#include <algorithm> +#include <unordered_map> +#include <vector> + +#include <glibmm/regex.h> + +#include "attributes.h" +#include "bad-uri-exception.h" +#include "document.h" +#include "preferences.h" + +#include "display/canvas-bpath.h" + +#include "3rdparty/libcroco/cr-sel-eng.h" + +#include "object/sp-paint-server.h" +#include "object/uri-references.h" +#include "object/uri.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg.h" + +#include "util/units.h" + +#include "xml/croco-node-iface.h" +#include "xml/simple-document.h" + +using Inkscape::CSSOStringStream; + +#define BMAX 8192 + +struct SPStyleEnum; + +int SPStyle::_count = 0; + +/*######################### +## FORWARD DECLARATIONS +#########################*/ +void sp_style_filter_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style); +void sp_style_fill_paint_server_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style); +void sp_style_stroke_paint_server_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style); + +static void sp_style_object_release(SPObject *object, SPStyle *style); +static CRSelEng *sp_repr_sel_eng(); + +/** + * Helper class for SPStyle property member lookup by SPAttributeEnum or + * by name, and for iterating over ordered members. + */ +class SPStylePropHelper { + SPStylePropHelper() { +#define REGISTER_PROPERTY(id, member, name) \ + g_assert(decltype(SPStyle::member)::static_id() == id); \ + _register(reinterpret_cast<SPIBasePtr>(&SPStyle::member), id) /* name unused */ + + // SVG 2: Attributes promoted to properties + REGISTER_PROPERTY(SP_ATTR_D, d, "d"); + + // 'color' must be before 'fill', 'stroke', 'text-decoration-color', ... + REGISTER_PROPERTY(SP_PROP_COLOR, color, "color"); + + // 'font-size'/'font' must be before properties that need to know em, ex size (SPILength, + // SPILengthOrNormal) + REGISTER_PROPERTY(SP_PROP_FONT_STYLE, font_style, "font-style"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT, font_variant, "font-variant"); + REGISTER_PROPERTY(SP_PROP_FONT_WEIGHT, font_weight, "font-weight"); + REGISTER_PROPERTY(SP_PROP_FONT_STRETCH, font_stretch, "font-stretch"); + REGISTER_PROPERTY(SP_PROP_FONT_SIZE, font_size, "font-size"); + REGISTER_PROPERTY(SP_PROP_LINE_HEIGHT, line_height, "line-height"); + REGISTER_PROPERTY(SP_PROP_FONT_FAMILY, font_family, "font-family"); + REGISTER_PROPERTY(SP_PROP_FONT, font, "font"); + REGISTER_PROPERTY(SP_PROP_INKSCAPE_FONT_SPEC, font_specification, "-inkscape-font-specification"); + + // Font variants + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_LIGATURES, font_variant_ligatures, "font-variant-ligatures"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_POSITION, font_variant_position, "font-variant-position"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_CAPS, font_variant_caps, "font-variant-caps"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_NUMERIC, font_variant_numeric, "font-variant-numeric"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_ALTERNATES, font_variant_alternates, "font-variant-alternates"); + REGISTER_PROPERTY(SP_PROP_FONT_VARIANT_EAST_ASIAN, font_variant_east_asian, "font-variant-east-asian"); + REGISTER_PROPERTY(SP_PROP_FONT_FEATURE_SETTINGS, font_feature_settings, "font-feature-settings"); + + // Variable Fonts + REGISTER_PROPERTY(SP_PROP_FONT_VARIATION_SETTINGS, font_variation_settings, "font-variation-settings"); + + REGISTER_PROPERTY(SP_PROP_TEXT_INDENT, text_indent, "text-indent"); + REGISTER_PROPERTY(SP_PROP_TEXT_ALIGN, text_align, "text-align"); + + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION, text_decoration, "text-decoration"); + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION_LINE, text_decoration_line, "text-decoration-line"); + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION_STYLE, text_decoration_style, "text-decoration-style"); + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION_COLOR, text_decoration_color, "text-decoration-color"); + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION_FILL, text_decoration_fill, "text-decoration-fill"); + REGISTER_PROPERTY(SP_PROP_TEXT_DECORATION_STROKE, text_decoration_stroke, "text-decoration-stroke"); + + REGISTER_PROPERTY(SP_PROP_LETTER_SPACING, letter_spacing, "letter-spacing"); + REGISTER_PROPERTY(SP_PROP_WORD_SPACING, word_spacing, "word-spacing"); + REGISTER_PROPERTY(SP_PROP_TEXT_TRANSFORM, text_transform, "text-transform"); + + REGISTER_PROPERTY(SP_PROP_WRITING_MODE, writing_mode, "writing-mode"); + REGISTER_PROPERTY(SP_PROP_DIRECTION, direction, "direction"); + REGISTER_PROPERTY(SP_PROP_TEXT_ORIENTATION, text_orientation, "text-orientation"); + REGISTER_PROPERTY(SP_PROP_DOMINANT_BASELINE, dominant_baseline, "dominant-baseline"); + REGISTER_PROPERTY(SP_PROP_BASELINE_SHIFT, baseline_shift, "baseline-shift"); + REGISTER_PROPERTY(SP_PROP_TEXT_ANCHOR, text_anchor, "text-anchor"); + REGISTER_PROPERTY(SP_PROP_WHITE_SPACE, white_space, "white-space"); + + REGISTER_PROPERTY(SP_PROP_SHAPE_INSIDE, shape_inside, "shape-inside"); + REGISTER_PROPERTY(SP_PROP_SHAPE_SUBTRACT, shape_subtract, "shape-subtract"); + REGISTER_PROPERTY(SP_PROP_SHAPE_PADDING, shape_padding, "shape-padding"); + REGISTER_PROPERTY(SP_PROP_SHAPE_MARGIN, shape_margin, "shape-margin"); + REGISTER_PROPERTY(SP_PROP_INLINE_SIZE, inline_size, "inline-size"); + + REGISTER_PROPERTY(SP_PROP_CLIP_RULE, clip_rule, "clip-rule"); + REGISTER_PROPERTY(SP_PROP_DISPLAY, display, "display"); + REGISTER_PROPERTY(SP_PROP_OVERFLOW, overflow, "overflow"); + REGISTER_PROPERTY(SP_PROP_VISIBILITY, visibility, "visibility"); + REGISTER_PROPERTY(SP_PROP_OPACITY, opacity, "opacity"); + + REGISTER_PROPERTY(SP_PROP_ISOLATION, isolation, "isolation"); + REGISTER_PROPERTY(SP_PROP_MIX_BLEND_MODE, mix_blend_mode, "mix-blend-mode"); + + REGISTER_PROPERTY(SP_PROP_COLOR_INTERPOLATION, color_interpolation, "color-interpolation"); + REGISTER_PROPERTY(SP_PROP_COLOR_INTERPOLATION_FILTERS, color_interpolation_filters, "color-interpolation-filters"); + + REGISTER_PROPERTY(SP_PROP_SOLID_COLOR, solid_color, "solid-color"); + REGISTER_PROPERTY(SP_PROP_SOLID_OPACITY, solid_opacity, "solid-opacity"); + + REGISTER_PROPERTY(SP_PROP_VECTOR_EFFECT, vector_effect, "vector-effect"); + + REGISTER_PROPERTY(SP_PROP_FILL, fill, "fill"); + REGISTER_PROPERTY(SP_PROP_FILL_OPACITY, fill_opacity, "fill-opacity"); + REGISTER_PROPERTY(SP_PROP_FILL_RULE, fill_rule, "fill-rule"); + + REGISTER_PROPERTY(SP_PROP_STROKE, stroke, "stroke"); + REGISTER_PROPERTY(SP_PROP_STROKE_WIDTH, stroke_width, "stroke-width"); + REGISTER_PROPERTY(SP_PROP_STROKE_LINECAP, stroke_linecap, "stroke-linecap"); + REGISTER_PROPERTY(SP_PROP_STROKE_LINEJOIN, stroke_linejoin, "stroke-linejoin"); + REGISTER_PROPERTY(SP_PROP_STROKE_MITERLIMIT, stroke_miterlimit, "stroke-miterlimit"); + REGISTER_PROPERTY(SP_PROP_STROKE_DASHARRAY, stroke_dasharray, "stroke-dasharray"); + REGISTER_PROPERTY(SP_PROP_STROKE_DASHOFFSET, stroke_dashoffset, "stroke-dashoffset"); + REGISTER_PROPERTY(SP_PROP_STROKE_OPACITY, stroke_opacity, "stroke-opacity"); + + REGISTER_PROPERTY(SP_PROP_MARKER, marker, "marker"); + REGISTER_PROPERTY(SP_PROP_MARKER_START, marker_start, "marker-start"); + REGISTER_PROPERTY(SP_PROP_MARKER_MID, marker_mid, "marker-mid"); + REGISTER_PROPERTY(SP_PROP_MARKER_END, marker_end, "marker-end"); + + REGISTER_PROPERTY(SP_PROP_PAINT_ORDER, paint_order, "paint-order"); + + REGISTER_PROPERTY(SP_PROP_FILTER, filter, "filter"); + + REGISTER_PROPERTY(SP_PROP_COLOR_RENDERING, color_rendering, "color-rendering"); + REGISTER_PROPERTY(SP_PROP_IMAGE_RENDERING, image_rendering, "image-rendering"); + REGISTER_PROPERTY(SP_PROP_SHAPE_RENDERING, shape_rendering, "shape-rendering"); + REGISTER_PROPERTY(SP_PROP_TEXT_RENDERING, text_rendering, "text-rendering"); + + REGISTER_PROPERTY(SP_PROP_ENABLE_BACKGROUND, enable_background, "enable-background"); + + REGISTER_PROPERTY(SP_PROP_STOP_COLOR, stop_color, "stop-color"); + REGISTER_PROPERTY(SP_PROP_STOP_OPACITY, stop_opacity, "stop-opacity"); + } + + // this is a singleton, copy not allowed + SPStylePropHelper(SPStylePropHelper const&) = delete; +public: + + /** + * Singleton instance + */ + static SPStylePropHelper &instance() { + static SPStylePropHelper _instance; + return _instance; + } + + /** + * Get property pointer by enum + */ + SPIBase *get(SPStyle *style, SPAttributeEnum id) { + auto it = m_id_map.find(id); + if (it != m_id_map.end()) { + return _get(style, it->second); + } + return nullptr; + } + + /** + * Get property pointer by name + */ + SPIBase *get(SPStyle *style, const std::string &name) { + return get(style, sp_attribute_lookup(name.c_str())); + } + + /** + * Get a vector of property pointers + * \todo provide iterator instead + */ + std::vector<SPIBase *> get_vector(SPStyle *style) { + std::vector<SPIBase *> v; + v.reserve(m_vector.size()); + for (auto ptr : m_vector) { + v.push_back(_get(style, ptr)); + } + return v; + } + +private: + SPIBase *_get(SPStyle *style, SPIBasePtr ptr) { return &(style->*ptr); } + + void _register(SPIBasePtr ptr, SPAttributeEnum id) { + m_vector.push_back(ptr); + + if (id != SP_ATTR_INVALID) { + m_id_map[id] = ptr; + } + } + + std::unordered_map</* SPAttributeEnum */ int, SPIBasePtr> m_id_map; + std::vector<SPIBasePtr> m_vector; +}; + +auto &_prop_helper = SPStylePropHelper::instance(); + +// C++11 allows one constructor to call another... might be useful. The original C code +// had separate calls to create SPStyle, one with only SPDocument and the other with only +// SPObject as parameters. +SPStyle::SPStyle(SPDocument *document_in, SPObject *object_in) : + + // Unimplemented SVG 1.1: alignment-baseline, clip, clip-path, color-profile, cursor, + // dominant-baseline, flood-color, flood-opacity, font-size-adjust, + // glyph-orientation-horizontal, glyph-orientation-vertical, kerning, lighting-color, + // pointer-events, unicode-bidi + + // 'font', 'font-size', and 'font-family' must come first as other properties depend on them + // for calculated values (through 'em' and 'ex'). ('ex' is currently not read.) + // The following properties can depend on 'em' and 'ex': + // baseline-shift, kerning, letter-spacing, stroke-dash-offset, stroke-width, word-spacing, + // Non-SVG 1.1: text-indent, line-spacing + + // Hidden in SPIFontStyle: (to be refactored) + // font-family + // font-specification + + + // SVG 2 attributes promoted to properties. (When geometry properties are added, move after font.) + d( false), // SPIString Not inherited! + + // Font related properties and 'font' shorthand + font_style( SP_CSS_FONT_STYLE_NORMAL), + font_variant( SP_CSS_FONT_VARIANT_NORMAL), + font_weight( SP_CSS_FONT_WEIGHT_NORMAL), + font_stretch( SP_CSS_FONT_STRETCH_NORMAL), + font_size(), + line_height( 1.25 ), // SPILengthOrNormal + font_family( ), // SPIString w/default + font(), // SPIFont + font_specification( ), // SPIString + + // Font variants (Features) + font_variant_ligatures( ), + font_variant_position( SP_CSS_FONT_VARIANT_POSITION_NORMAL), + font_variant_caps( SP_CSS_FONT_VARIANT_CAPS_NORMAL), + font_variant_numeric( ), + font_variant_alternates(SP_CSS_FONT_VARIANT_ALTERNATES_NORMAL), + font_variant_east_asian(), + font_feature_settings( ), + + // Variable Fonts + font_variation_settings(), // SPIFontVariationSettings + + // Text related properties + text_indent( ), // SPILength + text_align( SP_CSS_TEXT_ALIGN_START), + + letter_spacing( ), // SPILengthOrNormal + word_spacing( ), // SPILengthOrNormal + text_transform( SP_CSS_TEXT_TRANSFORM_NONE), + + direction( SP_CSS_DIRECTION_LTR), + writing_mode( SP_CSS_WRITING_MODE_LR_TB), + text_orientation( SP_CSS_TEXT_ORIENTATION_MIXED), + dominant_baseline( SP_CSS_BASELINE_AUTO), + baseline_shift(), + text_anchor( SP_CSS_TEXT_ANCHOR_START), + white_space( SP_CSS_WHITE_SPACE_NORMAL), + + // SVG 2 Text Wrapping + shape_inside( ), // SPIString + shape_subtract( ), // SPIString + shape_padding( ), // SPILength for now + shape_margin( ), // SPILength for now + inline_size( ), // SPILength for now + + text_decoration(), + text_decoration_line(), + text_decoration_style(), + text_decoration_color( ), // SPIColor + text_decoration_fill( ), // SPIPaint + text_decoration_stroke( ), // SPIPaint + + // General visual properties + clip_rule( SP_WIND_RULE_NONZERO), + display( SP_CSS_DISPLAY_INLINE, false), + overflow( SP_CSS_OVERFLOW_VISIBLE, false), + visibility( SP_CSS_VISIBILITY_VISIBLE), + opacity( false), + + isolation( SP_CSS_ISOLATION_AUTO), + mix_blend_mode( SP_CSS_BLEND_NORMAL), + + paint_order(), // SPIPaintOrder + + // Color properties + color( ), // SPIColor + color_interpolation( SP_CSS_COLOR_INTERPOLATION_SRGB), + color_interpolation_filters(SP_CSS_COLOR_INTERPOLATION_LINEARRGB), + + // Solid color properties + solid_color( ), // SPIColor + solid_opacity( ), + + // Vector effects + vector_effect(), + + // Fill properties + fill( ), // SPIPaint + fill_opacity( ), + fill_rule( SP_WIND_RULE_NONZERO), + + // Stroke properites + stroke( ), // SPIPaint + stroke_width( 1.0), // SPILength + stroke_linecap( SP_STROKE_LINECAP_BUTT), + stroke_linejoin( SP_STROKE_LINEJOIN_MITER), + stroke_miterlimit( 4), // SPIFloat (only use of float!) + stroke_dasharray(), // SPIDashArray + stroke_dashoffset( ), // SPILength for now + + stroke_opacity( ), + + marker( ), // SPIString + marker_start( ), // SPIString + marker_mid( ), // SPIString + marker_end( ), // SPIString + + // Filter properties + filter(), + enable_background( SP_CSS_BACKGROUND_ACCUMULATE, false), + + // Rendering hint properties + color_rendering( SP_CSS_COLOR_RENDERING_AUTO), + image_rendering( SP_CSS_IMAGE_RENDERING_AUTO), + shape_rendering( SP_CSS_SHAPE_RENDERING_AUTO), + text_rendering( SP_CSS_TEXT_RENDERING_AUTO), + + // Stop color and opacity + stop_color( false), // SPIColor, does not inherit + stop_opacity( false) // Does not inherit +{ + // std::cout << "SPStyle::SPStyle( SPDocument ): Entrance: (" << _count << ")" << std::endl; + // std::cout << " Document: " << (document_in?"present":"null") << std::endl; + // std::cout << " Object: " + // << (object_in?(object_in->getId()?object_in->getId():"id null"):"object null") << std::endl; + + // static bool first = true; + // if( first ) { + // std::cout << "Size of SPStyle: " << sizeof(SPStyle) << std::endl; + // std::cout << " SPIBase: " << sizeof(SPIBase) << std::endl; + // std::cout << " SPIFloat: " << sizeof(SPIFloat) << std::endl; + // std::cout << " SPIScale24: " << sizeof(SPIScale24) << std::endl; + // std::cout << " SPILength: " << sizeof(SPILength) << std::endl; + // std::cout << " SPILengthOrNormal: " << sizeof(SPILengthOrNormal) << std::endl; + // std::cout << " SPIColor: " << sizeof(SPIColor) << std::endl; + // std::cout << " SPIPaint: " << sizeof(SPIPaint) << std::endl; + // std::cout << " SPITextDecorationLine" << sizeof(SPITextDecorationLine) << std::endl; + // std::cout << " Glib::ustring:" << sizeof(Glib::ustring) << std::endl; + // std::cout << " SPColor: " << sizeof(SPColor) << std::endl; + // first = false; + // } + + ++_count; // Poor man's memory leak detector + + _refcount = 1; + + cloned = false; + + object = object_in; + if( object ) { + g_assert( SP_IS_OBJECT(object) ); + document = object->document; + release_connection = + object->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_style_object_release), this)); + + cloned = object->cloned; + + } else { + document = document_in; + } + + // 'font' shorthand requires access to included properties. + font.setStylePointer( this ); + + // Properties that depend on 'font-size' for calculating lengths. + baseline_shift.setStylePointer( this ); + text_indent.setStylePointer( this ); + line_height.setStylePointer( this ); + letter_spacing.setStylePointer( this ); + word_spacing.setStylePointer( this ); + stroke_width.setStylePointer( this ); + stroke_dashoffset.setStylePointer( this ); + shape_padding.setStylePointer( this ); + shape_margin.setStylePointer( this ); + inline_size.setStylePointer( this ); + + // Properties that depend on 'color' + text_decoration_color.setStylePointer( this ); + fill.setStylePointer( this ); + stroke.setStylePointer( this ); + // color.setStylePointer( this ); // Doesn't need reference to self + + // 'text_decoration' shorthand requires access to included properties. + text_decoration.setStylePointer( this ); + + // SPIPaint, SPIFilter needs access to 'this' (SPStyle) + // for setting up signals... 'fill', 'stroke' already done + filter.setStylePointer( this ); + shape_inside.setStylePointer( this ); + shape_subtract.setStylePointer( this ); + + // Used to iterate over markers + marker_ptrs[SP_MARKER_LOC] = ▮ + marker_ptrs[SP_MARKER_LOC_START] = &marker_start; + marker_ptrs[SP_MARKER_LOC_MID] = &marker_mid; + marker_ptrs[SP_MARKER_LOC_END] = &marker_end; + + + // This might be too resource hungary... but for now it possible to loop over properties + _properties = _prop_helper.get_vector(this); +} + +SPStyle::~SPStyle() { + + // std::cout << "SPStyle::~SPStyle" << std::endl; + --_count; // Poor man's memory leak detector. + + // Remove connections + release_connection.disconnect(); + fill_ps_changed_connection.disconnect(); + stroke_ps_changed_connection.disconnect(); + + // The following should be moved into SPIPaint and SPIFilter + if (fill.value.href) { + fill_ps_modified_connection.disconnect(); + } + + if (stroke.value.href) { + stroke_ps_modified_connection.disconnect(); + } + + if (filter.href) { + filter_modified_connection.disconnect(); + } + + // Conjecture: all this SPStyle ref counting is not needed. SPObject creates an instance of + // SPStyle when it is constructed and deletes it when it is destructed. The refcount is + // incremented and decremented only in the files: display/drawing-item.cpp, + // display/nr-filter-primitive.cpp, and libnrtype/Layout-TNG-Input.cpp. + if( _refcount > 1 ) { + std::cerr << "SPStyle::~SPStyle: ref count greater than 1! " << _refcount << std::endl; + } + // std::cout << "SPStyle::~SPStyle(): Exit\n" << std::endl; +} + +const std::vector<SPIBase *> SPStyle::properties() { return this->_properties; } + +void +SPStyle::clear(SPAttributeEnum id) { + SPIBase *p = _prop_helper.get(this, id); + if (p) { + p->clear(); + } else { + g_warning("Unimplemented style property %d", id); + } +} + +void +SPStyle::clear() { + for (auto * p : _properties) { + p->clear(); + } + + // Release connection to object, created in constructor. + release_connection.disconnect(); + + // href->detach() called in fill->clear()... + fill_ps_modified_connection.disconnect(); + if (fill.value.href) { + delete fill.value.href; + fill.value.href = nullptr; + } + stroke_ps_modified_connection.disconnect(); + if (stroke.value.href) { + delete stroke.value.href; + stroke.value.href = nullptr; + } + filter_modified_connection.disconnect(); + if (filter.href) { + delete filter.href; + filter.href = nullptr; + } + + if (document) { + filter.href = new SPFilterReference(document); + filter.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_filter_ref_changed), this)); + + fill.value.href = new SPPaintServerReference(document); + fill_ps_changed_connection = fill.value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), this)); + + stroke.value.href = new SPPaintServerReference(document); + stroke_ps_changed_connection = stroke.value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), this)); + } + + cloned = false; + +} + +// Matches void sp_style_read(SPStyle *style, SPObject *object, Inkscape::XML::Node *repr) +void +SPStyle::read( SPObject *object, Inkscape::XML::Node *repr ) { + + // std::cout << "SPstyle::read( SPObject, Inkscape::XML::Node ): Entrance: " + // << (object?(object->getId()?object->getId():"id null"):"object null") << " " + // << (repr?(repr->name()?repr->name():"no name"):"repr null") + // << std::endl; + g_assert(repr != nullptr); + g_assert(!object || (object->getRepr() == repr)); + + // // Uncomment to verify that we don't need to call clear. + // std::cout << " Creating temp style for testing" << std::endl; + // SPStyle *temp = new SPStyle(); + // if( !(*temp == *this ) ) std::cout << "SPStyle::read: Need to clear" << std::endl; + // delete temp; + + clear(); // FIXME, If this isn't here, EVERYTHING stops working! Why? + + if (object && object->cloned) { + cloned = true; + } + + /* 1. Style attribute */ + // std::cout << " MERGING STYLE ATTRIBUTE" << std::endl; + gchar const *val = repr->attribute("style"); + if( val != nullptr && *val ) { + _mergeString( val ); + } + + /* 2 Style sheet */ + if (object) { + _mergeObjectStylesheet( object ); + } + + /* 3 Presentation attributes */ + for (auto * p : _properties) { + // Shorthands are not allowed as presentation properites. Note: text-decoration and + // font-variant are converted to shorthands in CSS 3 but can still be read as a + // non-shorthand for compatibility with older renders, so they should not be in this list. + if (p->id() != SP_PROP_FONT && p->id() != SP_PROP_MARKER) { + p->readAttribute( repr ); + } + } + + /* 4 Cascade from parent */ + if( object ) { + if( object->parent ) { + cascade( object->parent->style ); + } + } else if( repr->parent() ) { // When does this happen? + // std::cout << "SPStyle::read(): reading via repr->parent()" << std::endl; + SPStyle *parent = new SPStyle(); + parent->read( nullptr, repr->parent() ); + cascade( parent ); + delete parent; + } +} + +/** + * Read style properties from object's repr. + * + * 1. Reset existing object style + * 2. Load current effective object style + * 3. Load i attributes from immediate parent (which has to be up-to-date) + */ +void +SPStyle::readFromObject( SPObject *object ) { + + // std::cout << "SPStyle::readFromObject: "<< (object->getId()?object->getId():"null")<< std::endl; + + g_return_if_fail(object != nullptr); + g_return_if_fail(SP_IS_OBJECT(object)); + + Inkscape::XML::Node *repr = object->getRepr(); + g_return_if_fail(repr != nullptr); + + read( object, repr ); +} + +/** + * Read style properties from preferences. + * @param path Preferences directory from which the style should be read + */ +void +SPStyle::readFromPrefs(Glib::ustring const &path) { + + g_return_if_fail(!path.empty()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // not optimal: we reconstruct the node based on the prefs, then pass it to + // sp_style_read for actual processing. + Inkscape::XML::SimpleDocument *tempdoc = new Inkscape::XML::SimpleDocument; + Inkscape::XML::Node *tempnode = tempdoc->createElement("prefs"); + + std::vector<Inkscape::Preferences::Entry> attrs = prefs->getAllEntries(path); + for (auto & attr : attrs) { + tempnode->setAttribute(attr.getEntryName(), attr.getString()); + } + + read( nullptr, tempnode ); + + Inkscape::GC::release(tempnode); + Inkscape::GC::release(tempdoc); + delete tempdoc; +} + +// Matches sp_style_merge_property(SPStyle *style, gint id, gchar const *val) +void +SPStyle::readIfUnset(SPAttributeEnum id, gchar const *val, SPStyleSrc const &source ) { + + // std::cout << "SPStyle::readIfUnset: Entrance: " << id << ": " << (val?val:"null") << std::endl; + // To Do: If it is not too slow, use std::map instead of std::vector inorder to remove switch() + // (looking up SP_PROP_xxxx already uses a hash). + g_return_if_fail(val != nullptr); + + switch (id) { + /* SVG */ + /* Clip/Mask */ + case SP_PROP_CLIP_PATH: + /** \todo + * This is a workaround. Inkscape only supports 'clip-path' as SVG attribute, not as + * style property. By having both CSS and SVG attribute set, editing of clip-path + * will fail, since CSS always overwrites SVG attributes. + * Fixes Bug #324849 + */ + g_warning("attribute 'clip-path' given as CSS"); + + //XML Tree being directly used here. + if (object) { + object->setAttribute("clip-path", val); + } + return; + case SP_PROP_MASK: + /** \todo + * See comment for SP_PROP_CLIP_PATH + */ + g_warning("attribute 'mask' given as CSS"); + + //XML Tree being directly used here. + if (object) { + object->setAttribute("mask", val); + } + return; + case SP_PROP_FILTER: + if( !filter.inherit ) filter.readIfUnset( val, source ); + return; + case SP_PROP_COLOR_INTERPOLATION: + // We read it but issue warning + color_interpolation.readIfUnset( val, source ); + if( color_interpolation.value != SP_CSS_COLOR_INTERPOLATION_SRGB ) { + g_warning("Inkscape currently only supports color-interpolation = sRGB"); + } + return; + } + + auto p = _prop_helper.get(this, id); + if (p) { + p->readIfUnset(val, source); + } else { + g_warning("Unimplemented style property %d", id); + } +} + +// return if is seted property +bool SPStyle::isSet(SPAttributeEnum id) +{ + bool set = false; + switch (id) { + case SP_PROP_CLIP_PATH: + return set; + case SP_PROP_MASK: + return set; + case SP_PROP_FILTER: + if (!filter.inherit) + set = filter.set; + return set; + case SP_PROP_COLOR_INTERPOLATION: + // We read it but issue warning + return color_interpolation.set; + } + + auto p = _prop_helper.get(this, id); + if (p) { + return p->set; + } else { + g_warning("Unimplemented style property %d", id); + return set; + } +} + +/** + * Outputs the style to a CSS string. + * + * Use with SP_STYLE_FLAG_ALWAYS for copying an object's complete cascaded style to + * style_clipboard. + * + * Use with SP_STYLE_FLAG_IFDIFF and a pointer to the parent class when you need a CSS string for + * an object in the document tree. + * + * \pre flags in {IFSET, ALWAYS, IFDIFF}. + * \pre base. + * \post ret != NULL. + */ +Glib::ustring +SPStyle::write( guint const flags, SPStyleSrc const &style_src_req, SPStyle const *const base ) const { + + // std::cout << "SPStyle::write: flags: " << flags << std::endl; + + Glib::ustring style_string; + for(std::vector<SPIBase*>::size_type i = 0; i != _properties.size(); ++i) { + if( base != nullptr ) { + style_string += _properties[i]->write( flags, style_src_req, base->_properties[i] ); + } else { + style_string += _properties[i]->write( flags, style_src_req, nullptr ); + } + } + + // Remove trailing ';' + if( style_string.size() > 0 ) { + style_string.erase( style_string.size() - 1 ); + } + return style_string; +} + +// Corresponds to sp_style_merge_from_parent() +/** + * Sets computed values in \a style, which may involve inheriting from (or in some other way + * calculating from) corresponding computed values of \a parent. + * + * References: http://www.w3.org/TR/SVG11/propidx.html shows what properties inherit by default. + * http://www.w3.org/TR/SVG11/styling.html#Inheritance gives general rules as to what it means to + * inherit a value. http://www.w3.org/TR/REC-CSS2/cascade.html#computed-value is more precise + * about what the computed value is (not obvious for lengths). + * + * \pre \a parent's computed values are already up-to-date. + */ +void +SPStyle::cascade( SPStyle const *const parent ) { + // std::cout << "SPStyle::cascade: " << (object->getId()?object->getId():"null") << std::endl; + for(std::vector<SPIBase*>::size_type i = 0; i != _properties.size(); ++i) { + _properties[i]->cascade( parent->_properties[i] ); + } +} + +// Corresponds to sp_style_merge_from_dying_parent() +/** + * Combine \a style and \a parent style specifications into a single style specification that + * preserves (as much as possible) the effect of the existing \a style being a child of \a parent. + * + * Called when the parent repr is to be removed (e.g. the parent is a \<use\> element that is being + * unlinked), in which case we copy/adapt property values that are explicitly set in \a parent, + * trying to retain the same visual appearance once the parent is removed. Interesting cases are + * when there is unusual interaction with the parent's value (opacity, display) or when the value + * can be specified as relative to the parent computed value (font-size, font-weight etc.). + * + * Doesn't update computed values of \a style. For correctness, you should subsequently call + * sp_style_merge_from_parent against the new parent (presumably \a parent's parent) even if \a + * style was previously up-to-date wrt \a parent. + * + * \pre \a parent's computed values are already up-to-date. + * (\a style's computed values needn't be up-to-date.) + */ +void +SPStyle::merge( SPStyle const *const parent ) { + // std::cout << "SPStyle::merge" << std::endl; + for(std::vector<SPIBase*>::size_type i = 0; i != _properties.size(); ++i) { + _properties[i]->merge( parent->_properties[i] ); + } +} + +/** + * Parses a style="..." string and merges it with an existing SPStyle. + */ +void +SPStyle::mergeString( gchar const *const p ) { + _mergeString( p ); +} + +/** + * Append an existing css statement into this style, used in css editing + * always appends declarations as STYLE_SHEET properties. + */ +void +SPStyle::mergeStatement( CRStatement *statement ) { + if (statement->type != RULESET_STMT) { + return; + } + CRDeclaration *decl_list = nullptr; + cr_statement_ruleset_get_declarations (statement, &decl_list); + if (decl_list) { + _mergeDeclList(decl_list, SP_STYLE_SRC_STYLE_SHEET); + } +} + +// Mostly for unit testing +bool +SPStyle::operator==(const SPStyle& rhs) { + + // Uncomment for testing + // for(std::vector<SPIBase*>::size_type i = 0; i != _properties.size(); ++i) { + // if( *_properties[i] != *rhs._properties[i]) + // std::cout << _properties[i]->name << ": " + // << _properties[i]->write(SP_STYLE_FLAG_ALWAYS,NULL) << " " + // << rhs._properties[i]->write(SP_STYLE_FLAG_ALWAYS,NULL) + // << (*_properties[i] == *rhs._properties[i]) << std::endl; + // } + + for(std::vector<SPIBase*>::size_type i = 0; i != _properties.size(); ++i) { + if( *_properties[i] != *rhs._properties[i]) return false; + } + return true; +} + +void +SPStyle::_mergeString( gchar const *const p ) { + + // std::cout << "SPStyle::_mergeString: " << (p?p:"null") << std::endl; + CRDeclaration *const decl_list + = cr_declaration_parse_list_from_buf(reinterpret_cast<guchar const *>(p), CR_UTF_8); + if (decl_list) { + _mergeDeclList( decl_list, SP_STYLE_SRC_STYLE_PROP ); + cr_declaration_destroy(decl_list); + } +} + +void +SPStyle::_mergeDeclList( CRDeclaration const *const decl_list, SPStyleSrc const &source ) { + + // std::cout << "SPStyle::_mergeDeclList" << std::endl; + + // In reverse order, as later declarations to take precedence over earlier ones. + // (Properties are only set if not previously set. See: + // Ref: http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order point 4.) + if (decl_list->next) { + _mergeDeclList( decl_list->next, source ); + } + _mergeDecl( decl_list, source ); +} + +void +SPStyle::_mergeDecl( CRDeclaration const *const decl, SPStyleSrc const &source ) { + + // std::cout << "SPStyle::_mergeDecl" << std::endl; + + auto prop_idx = sp_attribute_lookup(decl->property->stryng->str); + if (prop_idx != SP_ATTR_INVALID) { + /** \todo + * effic: Test whether the property is already set before trying to + * convert to string. Alternatively, set from CRTerm directly rather + * than converting to string. + */ + if (!isSet(prop_idx) || decl->important) { + guchar *const str_value_unsigned = cr_term_to_string(decl->value); + gchar *const str_value = reinterpret_cast<gchar *>(str_value_unsigned); + + // Add "!important" rule if necessary as this is not handled by cr_term_to_string(). + gchar const *important = decl->important ? " !important" : ""; + Inkscape::CSSOStringStream os; + os << str_value << important; + + readIfUnset(prop_idx, os.str().c_str(), source); + g_free(str_value); + } + } +} + +void +SPStyle::_mergeProps( CRPropList *const props ) { + + // std::cout << "SPStyle::_mergeProps" << std::endl; + + // In reverse order, as later declarations to take precedence over earlier ones. + if (props) { + _mergeProps( cr_prop_list_get_next( props ) ); + CRDeclaration *decl = nullptr; + cr_prop_list_get_decl(props, &decl); + _mergeDecl( decl, SP_STYLE_SRC_STYLE_SHEET ); + } +} + +void +SPStyle::_mergeObjectStylesheet( SPObject const *const object ) { + + // std::cout << "SPStyle::_mergeObjectStylesheet: " << (object->getId()?object->getId():"null") << std::endl; + + static CRSelEng *sel_eng = nullptr; + if (!sel_eng) { + sel_eng = sp_repr_sel_eng(); + } + + CRPropList *props = nullptr; + + //XML Tree being directly used here while it shouldn't be. + CRStatus status = + cr_sel_eng_get_matched_properties_from_cascade(sel_eng, + object->document->getStyleCascade(), + object->getRepr(), + &props); + g_return_if_fail(status == CR_OK); + /// \todo Check what errors can occur, and handle them properly. + if (props) { + _mergeProps(props); + cr_prop_list_destroy(props); + } +} + +std::string +SPStyle::getFontFeatureString() { + + std::string feature_string; + + if ( !(font_variant_ligatures.value & SP_CSS_FONT_VARIANT_LIGATURES_COMMON) ) + feature_string += "liga 0, clig 0, "; + if ( font_variant_ligatures.value & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY ) + feature_string += "dlig, "; + if ( font_variant_ligatures.value & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ) + feature_string += "hlig, "; + if ( !(font_variant_ligatures.value & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL) ) + feature_string += "calt 0, "; + + switch (font_variant_position.value) { + case SP_CSS_FONT_VARIANT_POSITION_SUB: + feature_string += "subs, "; + break; + case SP_CSS_FONT_VARIANT_POSITION_SUPER: + feature_string += "sups, "; + } + + switch (font_variant_caps.value) { + case SP_CSS_FONT_VARIANT_CAPS_SMALL: + feature_string += "smcp, "; + break; + case SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL: + feature_string += "smcp, c2sc, "; + break; + case SP_CSS_FONT_VARIANT_CAPS_PETITE: + feature_string += "pcap, "; + break; + case SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE: + feature_string += "pcap, c2pc, "; + break; + case SP_CSS_FONT_VARIANT_CAPS_UNICASE: + feature_string += "unic, "; + break; + case SP_CSS_FONT_VARIANT_CAPS_TITLING: + feature_string += "titl, "; + } + + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS ) + feature_string += "lnum, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS ) + feature_string += "onum, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS ) + feature_string += "pnum, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS ) + feature_string += "tnum, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS ) + feature_string += "frac, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS ) + feature_string += "afrc, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL ) + feature_string += "ordn, "; + if ( font_variant_numeric.value & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO ) + feature_string += "zero, "; + + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78 ) + feature_string += "jp78, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83 ) + feature_string += "jp83, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90 ) + feature_string += "jp90, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04 ) + feature_string += "jp04, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED ) + feature_string += "smpl, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL ) + feature_string += "trad, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH ) + feature_string += "fwid, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH ) + feature_string += "pwid, "; + if( font_variant_east_asian.value & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY ) + feature_string += "ruby, "; + + char const *val = font_feature_settings.value(); + if (val[0] && strcmp(val, "normal")) { + // We do no sanity checking... + feature_string += val; + feature_string += ", "; + } + + if (feature_string.empty()) { + feature_string = "normal"; + } else { + // Remove last ", " + feature_string.resize(feature_string.size() - 2); + } + + return feature_string; +} + + +// Internal +/** + * Release callback. + */ +static void +sp_style_object_release(SPObject *object, SPStyle *style) +{ + (void)object; // TODO + style->object = nullptr; +} + +// Internal +/** + * Emit style modified signal on style's object if the filter changed. + */ +static void +sp_style_filter_ref_modified(SPObject *obj, guint flags, SPStyle *style) +{ + (void)flags; // TODO + SPFilter *filter=static_cast<SPFilter *>(obj); + if (style->getFilter() == filter) + { + if (style->object) { + style->object->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } +} + +// Internal +/** + * Gets called when the filter is (re)attached to the style + */ +void +sp_style_filter_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style) +{ + if (old_ref) { + (dynamic_cast<SPFilter *>( old_ref ))->_refcount--; + style->filter_modified_connection.disconnect(); + } + if ( SP_IS_FILTER(ref)) + { + (dynamic_cast<SPFilter *>( ref ))->_refcount++; + style->filter_modified_connection = + ref->connectModified(sigc::bind(sigc::ptr_fun(&sp_style_filter_ref_modified), style)); + } + + sp_style_filter_ref_modified(ref, 0, style); +} + +/** + * Emit style modified signal on style's object if server is style's fill + * or stroke paint server. + */ +static void +sp_style_paint_server_ref_modified(SPObject *obj, guint flags, SPStyle *style) +{ + (void)flags; // TODO + SPPaintServer *server = static_cast<SPPaintServer *>(obj); + + if ((style->fill.isPaintserver()) + && style->getFillPaintServer() == server) + { + if (style->object) { + /** \todo + * fixme: I do not know, whether it is optimal - we are + * forcing reread of everything (Lauris) + */ + /** \todo + * fixme: We have to use object_modified flag, because parent + * flag is only available downstreams. + */ + style->object->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } else if ((style->stroke.isPaintserver()) + && style->getStrokePaintServer() == server) + { + if (style->object) { + /// \todo fixme: + style->object->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } else if (server) { + g_assert_not_reached(); + } +} + +/** + * Gets called when the paintserver is (re)attached to the style + */ +void +sp_style_fill_paint_server_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style) +{ + if (old_ref) { + style->fill_ps_modified_connection.disconnect(); + } + if (SP_IS_PAINT_SERVER(ref)) { + style->fill_ps_modified_connection = + ref->connectModified(sigc::bind(sigc::ptr_fun(&sp_style_paint_server_ref_modified), style)); + } + + style->signal_fill_ps_changed.emit(old_ref, ref); + sp_style_paint_server_ref_modified(ref, 0, style); +} + +/** + * Gets called when the paintserver is (re)attached to the style + */ +void +sp_style_stroke_paint_server_ref_changed(SPObject *old_ref, SPObject *ref, SPStyle *style) +{ + if (old_ref) { + style->stroke_ps_modified_connection.disconnect(); + } + if (SP_IS_PAINT_SERVER(ref)) { + style->stroke_ps_modified_connection = + ref->connectModified(sigc::bind(sigc::ptr_fun(&sp_style_paint_server_ref_modified), style)); + } + + style->signal_stroke_ps_changed.emit(old_ref, ref); + sp_style_paint_server_ref_modified(ref, 0, style); +} + +// Called in display/drawing-item.cpp, display/nr-filter-primitive.cpp, libnrtype/Layout-TNG-Input.cpp +/** + * Increase refcount of style. + */ +SPStyle * +sp_style_ref(SPStyle *style) +{ + g_return_val_if_fail(style != nullptr, NULL); + + style->style_ref(); // Increase ref count + + return style; +} + +// Called in display/drawing-item.cpp, display/nr-filter-primitive.cpp, libnrtype/Layout-TNG-Input.cpp +/** + * Decrease refcount of style with possible destruction. + */ +SPStyle * +sp_style_unref(SPStyle *style) +{ + g_return_val_if_fail(style != nullptr, NULL); + if (style->style_unref() < 1) { + delete style; + return nullptr; + } + return style; +} + +static CRSelEng * +sp_repr_sel_eng() +{ + CRSelEng *const ret = cr_sel_eng_new(); + cr_sel_eng_set_node_iface(ret, &Inkscape::XML::croco_node_iface); + + /** \todo + * Check whether we need to register any pseudo-class handlers. + * libcroco has its own default handlers for first-child and lang. + * + * We probably want handlers for link and arguably visited (though + * inkscape can't visit links at the time of writing). hover etc. + * more useful in inkview than the editor inkscape. + * + * http://www.w3.org/TR/SVG11/styling.html#StylingWithCSS says that + * the following should be honoured, at least by inkview: + * :hover, :active, :focus, :visited, :link. + */ + + g_assert(ret); + return ret; +} + +// The following functions should be incorporated into SPIPaint. FIXME +// Called in: style.cpp, style-internal.cpp +void +sp_style_set_ipaint_to_uri(SPStyle *style, SPIPaint *paint, const Inkscape::URI *uri, SPDocument *document) +{ + if (!paint->value.href) { + + if (style->object) { + // Should not happen as href should have been created in SPIPaint. (TODO: Removed code duplication.) + paint->value.href = new SPPaintServerReference(style->object); + + } else if (document) { + // Used by desktop style (no object to attach to!). + paint->value.href = new SPPaintServerReference(document); + + } else { + std::cerr << "sp_style_set_ipaint_to_uri: No valid object or document!" << std::endl; + return; + } + + if (paint == &style->fill) { + style->fill_ps_changed_connection = paint->value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_fill_paint_server_ref_changed), style)); + } else { + style->stroke_ps_changed_connection = paint->value.href->changedSignal().connect(sigc::bind(sigc::ptr_fun(sp_style_stroke_paint_server_ref_changed), style)); + } + } + + if (paint->value.href){ + if (paint->value.href->getObject()){ + paint->value.href->detach(); + } + + try { + paint->value.href->attach(*uri); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + paint->value.href->detach(); + } + } +} + +// Called in: style.cpp, style-internal.cpp +void +sp_style_set_ipaint_to_uri_string (SPStyle *style, SPIPaint *paint, const gchar *uri) +{ + try { + const Inkscape::URI IURI(uri); + sp_style_set_ipaint_to_uri(style, paint, &IURI, style->document); + } catch (...) { + g_warning("URI failed to parse: %s", uri); + } +} + +// Called in: desktop-style.cpp +void sp_style_set_to_uri(SPStyle *style, bool isfill, Inkscape::URI const *uri) +{ + sp_style_set_ipaint_to_uri(style, style->getFillOrStroke(isfill), uri, style->document); +} + +// Called in: widgets/font-selector.cpp, widgets/text-toolbar.cpp, ui/dialog/text-edit.cpp +gchar const * +sp_style_get_css_unit_string(int unit) +{ + // specify px by default, see inkscape bug 1221626, mozilla bug 234789 + // This is a problematic fix as some properties (e.g. 'line-height') have + // different behaviour if there is no unit. + switch (unit) { + + case SP_CSS_UNIT_NONE: return "px"; + case SP_CSS_UNIT_PX: return "px"; + case SP_CSS_UNIT_PT: return "pt"; + case SP_CSS_UNIT_PC: return "pc"; + case SP_CSS_UNIT_MM: return "mm"; + case SP_CSS_UNIT_CM: return "cm"; + case SP_CSS_UNIT_IN: return "in"; + case SP_CSS_UNIT_EM: return "em"; + case SP_CSS_UNIT_EX: return "ex"; + case SP_CSS_UNIT_PERCENT: return "%"; + default: return "px"; + } + return "px"; +} + +// Called in: style-internal.cpp, widgets/text-toolbar.cpp, ui/dialog/text-edit.cpp +/* + * Convert a size in pixels into another CSS unit size + */ +double +sp_style_css_size_px_to_units(double size, int unit, double font_size) +{ + double unit_size = size; + + if (font_size == 0) { + g_warning("sp_style_get_css_font_size_units: passed in zero font_size"); + font_size = SP_CSS_FONT_SIZE_DEFAULT; + } + + switch (unit) { + + case SP_CSS_UNIT_NONE: unit_size = size; break; + case SP_CSS_UNIT_PX: unit_size = size; break; + case SP_CSS_UNIT_PT: unit_size = Inkscape::Util::Quantity::convert(size, "px", "pt"); break; + case SP_CSS_UNIT_PC: unit_size = Inkscape::Util::Quantity::convert(size, "px", "pc"); break; + case SP_CSS_UNIT_MM: unit_size = Inkscape::Util::Quantity::convert(size, "px", "mm"); break; + case SP_CSS_UNIT_CM: unit_size = Inkscape::Util::Quantity::convert(size, "px", "cm"); break; + case SP_CSS_UNIT_IN: unit_size = Inkscape::Util::Quantity::convert(size, "px", "in"); break; + case SP_CSS_UNIT_EM: unit_size = size / font_size; break; + case SP_CSS_UNIT_EX: unit_size = size * 2.0 / font_size ; break; + case SP_CSS_UNIT_PERCENT: unit_size = size * 100.0 / font_size; break; + + default: + g_warning("sp_style_get_css_font_size_units conversion to %d not implemented.", unit); + break; + } + + return unit_size; +} + +// Called in: widgets/text-toolbar.cpp, ui/dialog/text-edit.cpp +/* + * Convert a size in a CSS unit size to pixels + */ +double +sp_style_css_size_units_to_px(double size, int unit, double font_size) +{ + if (unit == SP_CSS_UNIT_PX) { + return size; + } + //g_message("sp_style_css_size_units_to_px %f %d = %f px", size, unit, out); + return size * (size / sp_style_css_size_px_to_units(size, unit, font_size));; +} + + +// FIXME: Everything below this line belongs in a different file - css-chemistry? + +void +sp_style_set_property_url (SPObject *item, gchar const *property, SPObject *linked, bool recursive) +{ + Inkscape::XML::Node *repr = item->getRepr(); + + if (repr == nullptr) return; + + SPCSSAttr *css = sp_repr_css_attr_new(); + if (linked) { + gchar *val = g_strdup_printf("url(#%s)", linked->getId()); + sp_repr_css_set_property(css, property, val); + g_free(val); + } else { + sp_repr_css_unset_property(css, "filter"); + } + + if (recursive) { + sp_repr_css_change_recursive(repr, css, "style"); + } else { + sp_repr_css_change(repr, css, "style"); + } + sp_repr_css_attr_unref(css); +} + +/** + * \pre style != NULL. + * \pre flags in {IFSET, ALWAYS}. + * Only used by sp_css_attr_from_object() and in splivarot.cpp - sp_item_path_outline(). + */ +SPCSSAttr * +sp_css_attr_from_style(SPStyle const *const style, guint const flags) +{ + g_return_val_if_fail(style != nullptr, NULL); + g_return_val_if_fail(((flags & SP_STYLE_FLAG_IFSET) || + (flags & SP_STYLE_FLAG_ALWAYS)), + NULL); + Glib::ustring style_str = style->write(flags); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, style_str.c_str()); + return css; +} + +// Called in: selection-chemistry.cpp, widgets/stroke-marker-selector.cpp, widgets/stroke-style.cpp, +// ui/tools/freehand-base.cpp +/** + * \pre object != NULL + * \pre flags in {IFSET, ALWAYS}. + */ +SPCSSAttr *sp_css_attr_from_object(SPObject *object, guint const flags) +{ + g_return_val_if_fail(((flags == SP_STYLE_FLAG_IFSET) || + (flags == SP_STYLE_FLAG_ALWAYS) ), + NULL); + SPCSSAttr * result = nullptr; + if (object->style) { + result = sp_css_attr_from_style(object->style, flags); + } + return result; +} + +// Called in: selection-chemistry.cpp, ui/dialog/inkscape-preferences.cpp +/** + * Unset any text-related properties + */ +SPCSSAttr * +sp_css_attr_unset_text(SPCSSAttr *css) +{ + sp_repr_css_set_property(css, "font", nullptr); + sp_repr_css_set_property(css, "-inkscape-font-specification", nullptr); + sp_repr_css_set_property(css, "font-size", nullptr); + sp_repr_css_set_property(css, "font-size-adjust", nullptr); // not implemented yet + sp_repr_css_set_property(css, "font-style", nullptr); + sp_repr_css_set_property(css, "font-variant", nullptr); + sp_repr_css_set_property(css, "font-weight", nullptr); + sp_repr_css_set_property(css, "font-stretch", nullptr); + sp_repr_css_set_property(css, "font-family", nullptr); + sp_repr_css_set_property(css, "text-indent", nullptr); + sp_repr_css_set_property(css, "text-align", nullptr); + sp_repr_css_set_property(css, "line-height", nullptr); + sp_repr_css_set_property(css, "letter-spacing", nullptr); + sp_repr_css_set_property(css, "word-spacing", nullptr); + sp_repr_css_set_property(css, "text-transform", nullptr); + sp_repr_css_set_property(css, "direction", nullptr); + sp_repr_css_set_property(css, "writing-mode", nullptr); + sp_repr_css_set_property(css, "text-orientation", nullptr); + sp_repr_css_set_property(css, "text-anchor", nullptr); + sp_repr_css_set_property(css, "white-space", nullptr); + sp_repr_css_set_property(css, "shape-inside", nullptr); + sp_repr_css_set_property(css, "shape-subtract", nullptr); + sp_repr_css_set_property(css, "shape-padding", nullptr); + sp_repr_css_set_property(css, "shape-margin", nullptr); + sp_repr_css_set_property(css, "inline-size", nullptr); + sp_repr_css_set_property(css, "kerning", nullptr); // not implemented yet + sp_repr_css_set_property(css, "dominant-baseline", nullptr); // not implemented yet + sp_repr_css_set_property(css, "alignment-baseline", nullptr); // not implemented yet + sp_repr_css_set_property(css, "baseline-shift", nullptr); + + sp_repr_css_set_property(css, "text-decoration", nullptr); + sp_repr_css_set_property(css, "text-decoration-line", nullptr); + sp_repr_css_set_property(css, "text-decoration-color", nullptr); + sp_repr_css_set_property(css, "text-decoration-style", nullptr); + + sp_repr_css_set_property(css, "font-variant-ligatures", nullptr); + sp_repr_css_set_property(css, "font-variant-position", nullptr); + sp_repr_css_set_property(css, "font-variant-caps", nullptr); + sp_repr_css_set_property(css, "font-variant-numeric", nullptr); + sp_repr_css_set_property(css, "font-variant-alternates", nullptr); + sp_repr_css_set_property(css, "font-variant-east-asian", nullptr); + sp_repr_css_set_property(css, "font-feature-settings", nullptr); + + return css; +} + +// ui/dialog/inkscape-preferences.cpp +/** + * Unset properties that should not be set for default tool style. + * This list needs to be reviewed. + */ +SPCSSAttr * +sp_css_attr_unset_blacklist(SPCSSAttr *css) +{ + sp_repr_css_set_property(css, "color", nullptr); + sp_repr_css_set_property(css, "clip-rule", nullptr); + sp_repr_css_set_property(css, "d", nullptr); + sp_repr_css_set_property(css, "display", nullptr); + sp_repr_css_set_property(css, "overflow", nullptr); + sp_repr_css_set_property(css, "visibility", nullptr); + sp_repr_css_set_property(css, "isolation", nullptr); + sp_repr_css_set_property(css, "mix-blend-mode", nullptr); + sp_repr_css_set_property(css, "color-interpolation", nullptr); + sp_repr_css_set_property(css, "color-interpolation-filters", nullptr); + sp_repr_css_set_property(css, "solid-color", nullptr); + sp_repr_css_set_property(css, "solid-opacity", nullptr); + sp_repr_css_set_property(css, "fill-rule", nullptr); + sp_repr_css_set_property(css, "color-rendering", nullptr); + sp_repr_css_set_property(css, "image-rendering", nullptr); + sp_repr_css_set_property(css, "shape-rendering", nullptr); + sp_repr_css_set_property(css, "text-rendering", nullptr); + sp_repr_css_set_property(css, "enable-background", nullptr); + + return css; +} + +// Called in style.cpp +static bool +is_url(char const *p) +{ + if (p == nullptr) + return false; +/** \todo + * FIXME: I'm not sure if this applies to SVG as well, but CSS2 says any URIs + * in property values must start with 'url('. + */ + return (g_ascii_strncasecmp(p, "url(", 4) == 0); +} + +// Called in: ui/dialog/inkscape-preferences.cpp, ui/tools/tweek-tool.cpp +/** + * Unset any properties that contain URI values. + * + * Used for storing style that will be reused across documents when carrying + * the referenced defs is impractical. + */ +SPCSSAttr * +sp_css_attr_unset_uris(SPCSSAttr *css) +{ +// All properties that may hold <uri> or <paint> according to SVG 1.1 + if (is_url(sp_repr_css_property(css, "clip-path", nullptr))) sp_repr_css_set_property(css, "clip-path", nullptr); + if (is_url(sp_repr_css_property(css, "color-profile", nullptr))) sp_repr_css_set_property(css, "color-profile", nullptr); + if (is_url(sp_repr_css_property(css, "cursor", nullptr))) sp_repr_css_set_property(css, "cursor", nullptr); + if (is_url(sp_repr_css_property(css, "filter", nullptr))) sp_repr_css_set_property(css, "filter", nullptr); + if (is_url(sp_repr_css_property(css, "marker", nullptr))) sp_repr_css_set_property(css, "marker", nullptr); + if (is_url(sp_repr_css_property(css, "marker-start", nullptr))) sp_repr_css_set_property(css, "marker-start", nullptr); + if (is_url(sp_repr_css_property(css, "marker-mid", nullptr))) sp_repr_css_set_property(css, "marker-mid", nullptr); + if (is_url(sp_repr_css_property(css, "marker-end", nullptr))) sp_repr_css_set_property(css, "marker-end", nullptr); + if (is_url(sp_repr_css_property(css, "mask", nullptr))) sp_repr_css_set_property(css, "mask", nullptr); + if (is_url(sp_repr_css_property(css, "fill", nullptr))) sp_repr_css_set_property(css, "fill", nullptr); + if (is_url(sp_repr_css_property(css, "stroke", nullptr))) sp_repr_css_set_property(css, "stroke", nullptr); + + return css; +} + +// Called in style.cpp +/** + * Scale a single-value property. + */ +static void +sp_css_attr_scale_property_single(SPCSSAttr *css, gchar const *property, + double ex, bool only_with_units = false) +{ + gchar const *w = sp_repr_css_property(css, property, nullptr); + if (w) { + gchar *units = nullptr; + double wd = g_ascii_strtod(w, &units) * ex; + if (w == units) {// nothing converted, non-numeric value + return; + } + if (only_with_units && (units == nullptr || *units == '\0' || *units == '%' || *units == 'e')) { + // only_with_units, but no units found, so do nothing. + // 'e' matches 'em' or 'ex' + return; + } + Inkscape::CSSOStringStream os; + os << wd << units; // reattach units + sp_repr_css_set_property(css, property, os.str().c_str()); + } +} + +// Called in style.cpp for stroke-dasharray +/** + * Scale a list-of-values property. + */ +static void +sp_css_attr_scale_property_list(SPCSSAttr *css, gchar const *property, double ex) +{ + gchar const *string = sp_repr_css_property(css, property, nullptr); + if (string) { + Inkscape::CSSOStringStream os; + gchar **a = g_strsplit(string, ",", 10000); + bool first = true; + for (gchar **i = a; i != nullptr; i++) { + gchar *w = *i; + if (w == nullptr) + break; + gchar *units = nullptr; + double wd = g_ascii_strtod(w, &units) * ex; + if (w == units) {// nothing converted, non-numeric value ("none" or "inherit"); do nothing + g_strfreev(a); + return; + } + if (!first) { + os << ","; + } + os << wd << units; // reattach units + first = false; + } + sp_repr_css_set_property(css, property, os.str().c_str()); + g_strfreev(a); + } +} + +// Called in: text-editing.cpp, +/** + * Scale any properties that may hold <length> by ex. + */ +SPCSSAttr * +sp_css_attr_scale(SPCSSAttr *css, double ex) +{ + sp_css_attr_scale_property_single(css, "baseline-shift", ex); + sp_css_attr_scale_property_single(css, "stroke-width", ex); + sp_css_attr_scale_property_list (css, "stroke-dasharray", ex); + sp_css_attr_scale_property_single(css, "stroke-dashoffset", ex); + sp_css_attr_scale_property_single(css, "font-size", ex, true); + sp_css_attr_scale_property_single(css, "kerning", ex); + sp_css_attr_scale_property_single(css, "letter-spacing", ex); + sp_css_attr_scale_property_single(css, "word-spacing", ex); + sp_css_attr_scale_property_single(css, "line-height", ex, true); + + return css; +} + + +/** + * Quote and/or escape string for writing to CSS, changing strings in place. + * See: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + */ +void +css_quote(Glib::ustring &val) +{ + Glib::ustring out; + bool quote = false; + + // Can't wait for C++11! + for( Glib::ustring::iterator it = val.begin(); it != val.end(); ++it) { + if(g_ascii_isalnum(*it) || *it=='-' || *it=='_' || *it > 0xA0) { + out += *it; + } else if (*it == '\'') { + // Single quotes require escaping and quotes. + out += '\\'; + out += *it; + quote = true; + } else { + // Quote everything else including spaces. + // (CSS Fonts Level 3 recommends quoting with spaces.) + out += *it; + quote = true; + } + if( it == val.begin() && !g_ascii_isalpha(*it) ) { + // A non-ASCII/non-alpha initial value on any identifier needs quotes. + // (Actually it's a bit more complicated but as it never hurts to quote...) + quote = true; + } + } + if( quote ) { + out.insert( out.begin(), '\'' ); + out += '\''; + } + val = out; +} + + +/** + * Quote font names in font-family lists, changing string in place. + * We use unquoted names internally but some need to be quoted in CSS. + */ +void +css_font_family_quote(Glib::ustring &val) +{ + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", val ); + + val.erase(); + for(auto & token : tokens) { + css_quote( token ); + val += token + ", "; + } + if( val.size() > 1 ) + val.erase( val.size() - 2 ); // Remove trailing ", " +} + + +// Called in style-internal.cpp, xml/repr-css.cpp +/** + * Remove paired single and double quotes from a string, changing string in place. + */ +void +css_unquote(Glib::ustring &val) +{ + if( val.size() > 1 && + ( (val[0] == '"' && val[val.size()-1] == '"' ) || + (val[0] == '\'' && val[val.size()-1] == '\'' ) ) ) { + + val.erase( 0, 1 ); + val.erase( val.size()-1 ); + } +} + +// Called in style-internal.cpp, text-toolbar.cpp +/** + * Remove paired single and double quotes from font names in font-family lists, + * changing string in place. + * We use unquoted family names internally but CSS sometimes uses quoted names. + */ +void +css_font_family_unquote(Glib::ustring &val) +{ + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", val ); + + val.erase(); + for(auto & token : tokens) { + css_unquote( token ); + val += token + ", "; + } + if( val.size() > 1 ) + val.erase( val.size() - 2 ); // Remove trailing ", " +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/style.h b/src/style.h new file mode 100644 index 0000000..19922cb --- /dev/null +++ b/src/style.h @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_STYLE_H +#define SEEN_SP_STYLE_H + +/** \file + * SPStyle - a style object for SPItem objects + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2014 Tavmjong Bah + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style-enums.h" +#include "style-internal.h" + +#include <sigc++/connection.h> +#include <iostream> +#include <vector> +#include "3rdparty/libcroco/cr-declaration.h" +#include "3rdparty/libcroco/cr-prop-list.h" + +enum SPAttributeEnum : unsigned; + +// Define SPIBasePtr, a Pointer to a data member of SPStyle of type SPIBase; +typedef SPIBase SPStyle::*SPIBasePtr; + +namespace Inkscape { +namespace XML { +class Node; +} +} + + +/// An SVG style object. +class SPStyle { + +public: + + SPStyle(SPDocument *document = nullptr, SPObject *object = nullptr);// document is ignored if valid object given + ~SPStyle(); + const std::vector<SPIBase *> properties(); + void clear(); + void clear(SPAttributeEnum id); + void read(SPObject *object, Inkscape::XML::Node *repr); + void readFromObject(SPObject *object); + void readFromPrefs(Glib::ustring const &path); + bool isSet(SPAttributeEnum id); + void readIfUnset(SPAttributeEnum id, char const *val, SPStyleSrc const &source = SP_STYLE_SRC_STYLE_PROP ); + Glib::ustring write( unsigned int const flags = SP_STYLE_FLAG_IFSET, + SPStyleSrc const &style_src_req = SP_STYLE_SRC_STYLE_PROP, + SPStyle const *const base = nullptr ) const; + void cascade( SPStyle const *const parent ); + void merge( SPStyle const *const parent ); + void mergeString( char const *const p ); + void mergeStatement( CRStatement *statement ); + bool operator==(const SPStyle& rhs); + + int style_ref() { ++_refcount; return _refcount; } + int style_unref() { --_refcount; return _refcount; } + int refCount() { return _refcount; } + +private: + void _mergeString( char const *const p ); + void _mergeDeclList( CRDeclaration const *const decl_list, SPStyleSrc const &source ); + void _mergeDecl( CRDeclaration const *const decl, SPStyleSrc const &source ); + void _mergeProps( CRPropList *const props ); + void _mergeObjectStylesheet( SPObject const *const object ); + +private: + int _refcount; + static int _count; // Poor man's leak detector + +// FIXME: Make private +public: + /** Object we are attached to */ + SPObject *object; + /** Document we are associated with */ + SPDocument *document; + +private: + /// Pointers to all the properties (for looping through them) + std::vector<SPIBase *> _properties; + + // Shorthand for better readability + template <SPAttributeEnum Id, class Base> + using T = TypedSPI<Id, Base>; + +public: + + /* ----------------------- THE PROPERTIES ------------------------- */ + /* Match order in style.cpp. */ + + /* SVG 2 attributes promoted to properties. */ + + /** Path data */ + T<SP_ATTR_D, SPIString> d; + + /* Font ---------------------------- */ + + /** Font style */ + T<SP_PROP_FONT_STYLE, SPIEnum<SPCSSFontStyle>> font_style; + /** Which substyle of the font (CSS 2. CSS 3 redefines as shorthand) */ + T<SP_PROP_FONT_VARIANT, SPIEnum<SPCSSFontVariant>> font_variant; + /** Weight of the font */ + T<SP_PROP_FONT_WEIGHT, SPIEnum<SPCSSFontWeight>> font_weight; + /** Stretch of the font */ + T<SP_PROP_FONT_STRETCH, SPIEnum<SPCSSFontStretch>> font_stretch; + /** Size of the font */ + T<SP_PROP_FONT_SIZE, SPIFontSize> font_size; + /** Line height (css2 10.8.1) */ + T<SP_PROP_LINE_HEIGHT, SPILengthOrNormal> line_height; + /** Font family */ + T<SP_PROP_FONT_FAMILY, SPIString> font_family; + /** Font shorthand */ + T<SP_PROP_FONT, SPIFont> font; + /** Full font name, as font_factory::ConstructFontSpecification would give, for internal use. */ + T<SP_PROP_INKSCAPE_FONT_SPEC, SPIString> font_specification; + + /* Font variants -------------------- */ + /** Font variant ligatures */ + T<SP_PROP_FONT_VARIANT_LIGATURES, SPILigatures> font_variant_ligatures; + /** Font variant position (subscript/superscript) */ + T<SP_PROP_FONT_VARIANT_POSITION, SPIEnum<SPCSSFontVariantPosition>> font_variant_position; + /** Font variant caps (small caps) */ + T<SP_PROP_FONT_VARIANT_CAPS, SPIEnum<SPCSSFontVariantCaps>> font_variant_caps; + /** Font variant numeric (numerical formatting) */ + T<SP_PROP_FONT_VARIANT_NUMERIC, SPINumeric> font_variant_numeric; + /** Font variant alternates (alternates/swatches) */ + T<SP_PROP_FONT_VARIANT_ALTERNATES, SPIEnum<SPCSSFontVariantAlternates>> font_variant_alternates; + /** Font variant East Asian */ + T<SP_PROP_FONT_VARIANT_EAST_ASIAN, SPIEastAsian> font_variant_east_asian; + /** Font feature settings (Low level access to TrueType tables) */ + T<SP_PROP_FONT_FEATURE_SETTINGS, SPIString> font_feature_settings; + + /** Font variation settings (Low level access to OpenType variable font design-coordinate values) */ + T<SP_PROP_FONT_VARIATION_SETTINGS, SPIFontVariationSettings> font_variation_settings; + + /* Text ----------------------------- */ + + /** First line indent of paragraphs (css2 16.1) */ + T<SP_PROP_TEXT_INDENT, SPILength> text_indent; + /** text alignment (css2 16.2) (not to be confused with text-anchor) */ + T<SP_PROP_TEXT_ALIGN, SPIEnum<SPCSSTextAlign>> text_align; + + /** letter spacing (css2 16.4) */ + T<SP_PROP_LETTER_SPACING, SPILengthOrNormal> letter_spacing; + /** word spacing (also css2 16.4) */ + T<SP_PROP_WORD_SPACING, SPILengthOrNormal> word_spacing; + /** capitalization (css2 16.5) */ + T<SP_PROP_TEXT_TRANSFORM, SPIEnum<SPCSSTextTransform>> text_transform; + + /* CSS3 Text */ + /** text direction (svg1.1) */ + T<SP_PROP_DIRECTION, SPIEnum<SPCSSDirection>> direction; + /** Writing mode (svg1.1 10.7.2, CSS Writing Modes 3) */ + T<SP_PROP_WRITING_MODE, SPIEnum<SPCSSWritingMode>> writing_mode; + /** Text orientation (CSS Writing Modes 3) */ + T<SP_PROP_TEXT_ORIENTATION, SPIEnum<SPCSSTextOrientation>> text_orientation; + /** Dominant baseline (svg1.1) */ + T<SP_PROP_DOMINANT_BASELINE, SPIEnum<SPCSSBaseline>> dominant_baseline; + /** Baseline shift (svg1.1 10.9.2) */ + T<SP_PROP_BASELINE_SHIFT, SPIBaselineShift> baseline_shift; + + /* SVG */ + /** Anchor of the text (svg1.1 10.9.1) */ + T<SP_PROP_TEXT_ANCHOR, SPIEnum<SPTextAnchor>> text_anchor; + + /** white space (svg2) */ + T<SP_PROP_WHITE_SPACE, SPIEnum<SPWhiteSpace>> white_space; + + /** SVG2 Text Wrapping */ + T<SP_PROP_SHAPE_INSIDE, SPIShapes> shape_inside; + T<SP_PROP_SHAPE_SUBTRACT, SPIShapes> shape_subtract; + T<SP_PROP_SHAPE_PADDING, SPILength> shape_padding; + T<SP_PROP_SHAPE_MARGIN, SPILength> shape_margin; + T<SP_PROP_INLINE_SIZE, SPILength> inline_size; + + /* Text Decoration ----------------------- */ + + /** text decoration (css2 16.3.1) */ + T<SP_PROP_TEXT_DECORATION, SPITextDecoration> text_decoration; + /** CSS 3 2.1, 2.2, 2.3 */ + /** Not done yet, test_decoration3 = css3 2.4*/ + T<SP_PROP_TEXT_DECORATION_LINE, SPITextDecorationLine> text_decoration_line; + T<SP_PROP_TEXT_DECORATION_STYLE, SPITextDecorationStyle> text_decoration_style; // SPIEnum? Only one can be set at time. + T<SP_PROP_TEXT_DECORATION_COLOR, SPIColor> text_decoration_color; + T<SP_PROP_TEXT_DECORATION_FILL, SPIPaint> text_decoration_fill; + T<SP_PROP_TEXT_DECORATION_STROKE, SPIPaint> text_decoration_stroke; + // used to implement text_decoration, not saved to or read from SVG file + SPITextDecorationData text_decoration_data; + + // 16.3.2 is text-shadow. That's complicated. + + /* General visual properties ------------- */ + + /** clip-rule: 0 nonzero, 1 evenodd */ + T<SP_PROP_CLIP_RULE, SPIEnum<SPWindRule>> clip_rule; + + /** display */ + T<SP_PROP_DISPLAY, SPIEnum<SPCSSDisplay>> display; + + /** overflow */ + T<SP_PROP_OVERFLOW, SPIEnum<SPOverflow>> overflow; + + /** visibility */ + T<SP_PROP_VISIBILITY, SPIEnum<SPVisibility>> visibility; + + /** opacity */ + T<SP_PROP_OPACITY, SPIScale24> opacity; + + /** mix-blend-mode: CSS Compositing and Blending Level 1 */ + T<SP_PROP_ISOLATION, SPIEnum<SPIsolation>> isolation; + T<SP_PROP_MIX_BLEND_MODE, SPIEnum<SPBlendMode>> mix_blend_mode; + + T<SP_PROP_PAINT_ORDER, SPIPaintOrder> paint_order; + + /** color */ + T<SP_PROP_COLOR, SPIColor> color; + /** color-interpolation */ + T<SP_PROP_COLOR_INTERPOLATION, SPIEnum<SPColorInterpolation>> color_interpolation; + /** color-interpolation-filters */ + T<SP_PROP_COLOR_INTERPOLATION_FILTERS, SPIEnum<SPColorInterpolation>> color_interpolation_filters; + + /** solid-color */ + T<SP_PROP_SOLID_COLOR, SPIColor> solid_color; + /** solid-opacity */ + T<SP_PROP_SOLID_OPACITY, SPIScale24> solid_opacity; + + /** vector effect */ + T<SP_PROP_VECTOR_EFFECT, SPIVectorEffect> vector_effect; + + /** fill */ + T<SP_PROP_FILL, SPIPaint> fill; + /** fill-opacity */ + T<SP_PROP_FILL_OPACITY, SPIScale24> fill_opacity; + /** fill-rule: 0 nonzero, 1 evenodd */ + T<SP_PROP_FILL_RULE, SPIEnum<SPWindRule>> fill_rule; + + /** stroke */ + T<SP_PROP_STROKE, SPIPaint> stroke; + /** stroke-width */ + T<SP_PROP_STROKE_WIDTH, SPILength> stroke_width; + /** stroke-linecap */ + T<SP_PROP_STROKE_LINECAP, SPIEnum<SPStrokeCapType>> stroke_linecap; + /** stroke-linejoin */ + T<SP_PROP_STROKE_LINEJOIN, SPIEnum<SPStrokeJoinType>> stroke_linejoin; + /** stroke-miterlimit */ + T<SP_PROP_STROKE_MITERLIMIT, SPIFloat> stroke_miterlimit; + /** stroke-dasharray */ + T<SP_PROP_STROKE_DASHARRAY, SPIDashArray> stroke_dasharray; + /** stroke-dashoffset */ + T<SP_PROP_STROKE_DASHOFFSET, SPILength> stroke_dashoffset; + /** stroke-opacity */ + T<SP_PROP_STROKE_OPACITY, SPIScale24> stroke_opacity; + + /** Marker list */ + T<SP_PROP_MARKER, SPIString> marker; + T<SP_PROP_MARKER_START, SPIString> marker_start; + T<SP_PROP_MARKER_MID, SPIString> marker_mid; + T<SP_PROP_MARKER_END, SPIString> marker_end; + SPIString* marker_ptrs[SP_MARKER_LOC_QTY]; + + /* Filter effects ------------------------ */ + + /** Filter effect */ + T<SP_PROP_FILTER, SPIFilter> filter; + /** normally not used, but duplicates the Gaussian blur deviation (if any) from the attached + filter when the style is used for querying */ + // TODO remove, find other logic for querying + T<SP_ATTR_INVALID, SPILength> filter_gaussianBlur_deviation; + /** enable-background, used for defining where filter effects get their background image */ + T<SP_PROP_ENABLE_BACKGROUND, SPIEnum<SPEnableBackground>> enable_background; + + /** gradient-stop */ + T<SP_PROP_STOP_COLOR, SPIColor> stop_color; + T<SP_PROP_STOP_OPACITY, SPIScale24> stop_opacity; + + /* Rendering hints ----------------------- */ + + /** hints on how to render: e.g. speed vs. accuracy. + * As of April, 2013, only image_rendering used. */ + T<SP_PROP_COLOR_RENDERING, SPIEnum<SPColorRendering>> color_rendering; + T<SP_PROP_IMAGE_RENDERING, SPIEnum<SPImageRendering>> image_rendering; + T<SP_PROP_SHAPE_RENDERING, SPIEnum<SPShapeRendering>> shape_rendering; + T<SP_PROP_TEXT_RENDERING, SPIEnum<SPTextRendering>> text_rendering; + + /* ----------------------- END PROPERTIES ------------------------- */ + + /// style belongs to a cloned object + bool cloned; + + sigc::connection release_connection; + + sigc::connection filter_modified_connection; + sigc::connection fill_ps_modified_connection; + sigc::connection stroke_ps_modified_connection; + sigc::connection fill_ps_changed_connection; + sigc::connection stroke_ps_changed_connection; + + /** + * Emitted when paint server object, fill paint refers to, is changed. That is + * when the reference starts pointing to a different address in memory. + * + * NB It is different from fill_ps_modified signal. When paint server is modified + * it means some of it's attributes or children change. + */ + sigc::signal<void, SPObject *, SPObject *> signal_fill_ps_changed; + /** + * Emitted when paint server object, fill paint refers to, is changed. That is + * when the reference starts pointing to a different address in memory. + */ + sigc::signal<void, SPObject *, SPObject *> signal_stroke_ps_changed; + + SPObject *getFilter() { return (filter.href) ? filter.href->getObject() : nullptr; } + SPObject const *getFilter() const { return (filter.href) ? filter.href->getObject() : nullptr; } + Inkscape::URI const *getFilterURI() const { return (filter.href) ? filter.href->getURI() : nullptr; } + + SPPaintServer *getFillPaintServer() { return (fill.value.href) ? fill.value.href->getObject() : nullptr; } + SPPaintServer const *getFillPaintServer() const { return (fill.value.href) ? fill.value.href->getObject() : nullptr; } + Inkscape::URI const *getFillURI() const { return (fill.value.href) ? fill.value.href->getURI() : nullptr; } + + SPPaintServer *getStrokePaintServer() { return (stroke.value.href) ? stroke.value.href->getObject() : nullptr; } + SPPaintServer const *getStrokePaintServer() const { return (stroke.value.href) ? stroke.value.href->getObject() : nullptr; } + Inkscape::URI const *getStrokeURI() const { return (stroke.value.href) ? stroke.value.href->getURI() : nullptr; } + + /** + * Return a font feature string useful for Pango. + */ + std::string getFontFeatureString(); + + /** + * Get either the fill or the stroke property + */ + SPIPaint *getFillOrStroke(bool fill_) { return fill_ ? fill.upcast() : stroke.upcast(); } + SPIPaint const *getFillOrStroke(bool fill_) const { return fill_ ? fill.upcast() : stroke.upcast(); } +}; + +SPStyle *sp_style_ref(SPStyle *style); // SPStyle::ref(); + +SPStyle *sp_style_unref(SPStyle *style); // SPStyle::unref(); + +void sp_style_set_to_uri(SPStyle *style, bool isfill, Inkscape::URI const *uri); // ? + +char const *sp_style_get_css_unit_string(int unit); // No change? + +#define SP_CSS_FONT_SIZE_DEFAULT 12.0 +double sp_style_css_size_px_to_units(double size, int unit, double font_size = SP_CSS_FONT_SIZE_DEFAULT); // No change? +double sp_style_css_size_units_to_px(double size, int unit, double font_size = SP_CSS_FONT_SIZE_DEFAULT); // No change? + + +SPCSSAttr *sp_css_attr_from_style (SPStyle const *const style, unsigned int flags); +SPCSSAttr *sp_css_attr_from_object(SPObject *object, unsigned int flags = SP_STYLE_FLAG_IFSET); +SPCSSAttr *sp_css_attr_unset_text(SPCSSAttr *css); +SPCSSAttr *sp_css_attr_unset_blacklist(SPCSSAttr *css); +SPCSSAttr *sp_css_attr_unset_uris(SPCSSAttr *css); +SPCSSAttr *sp_css_attr_scale(SPCSSAttr *css, double ex); + +void sp_style_unset_property_attrs(SPObject *o); + +void sp_style_set_property_url (SPObject *item, char const *property, SPObject *linked, bool recursive); + +void css_quote( Glib::ustring &val ); // Add quotes around CSS values +void css_unquote( Glib::ustring &val ); // Remove quotes from CSS values (style-internal.cpp, xml/repr-css.cpp) +void css_font_family_quote( Glib::ustring &val ); // style-internal.cpp, text-toolbar.cpp +void css_font_family_unquote( Glib::ustring &val ); // style-internal.cpp, text-toolbar.cpp + +Glib::ustring css2_escape_quote(char const *val); + +#endif // SEEN_SP_STYLE_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/svg/CMakeLists.txt b/src/svg/CMakeLists.txt new file mode 100644 index 0000000..e52febe --- /dev/null +++ b/src/svg/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(svg_SRC + css-ostringstream.cpp + path-string.cpp + # sp-svg.def + stringstream.cpp + strip-trailing-zeros.cpp + svg-affine.cpp + svg-color.cpp + svg-angle.cpp + svg-length.cpp + svg-path.cpp + # test-stubs.cpp + + + # ------- + # Headers + css-ostringstream.h + path-string.h + stringstream.h + strip-trailing-zeros.h + svg-affine-test.h + svg-color-test.h + svg-color.h + svg-icc-color.h + svg-angle.h + svg-length-test.h + svg-length.h + svg-path-geom-test.h + svg.h + # test-stubs.h + +) + +# add_inkscape_lib(svg_LIB "${svg_SRC}") +add_inkscape_source("${svg_SRC}") diff --git a/src/svg/HACKING b/src/svg/HACKING new file mode 100644 index 0000000..e3d8b10 --- /dev/null +++ b/src/svg/HACKING @@ -0,0 +1,7 @@ +Here are svg specific functions, i.e. value parsing & creation. +Most of these are written by Raph Levien for gill. I'll include correct +copyright notices one day too. + +Lauris Kaplinski +<lauris@ariman.ee> + diff --git a/src/svg/README b/src/svg/README new file mode 100644 index 0000000..777f8b8 --- /dev/null +++ b/src/svg/README @@ -0,0 +1,8 @@ + + +This directory contains SVG utilities. + +To do: + +* Move to "util/svg". +* Clean up. diff --git a/src/svg/css-ostringstream.cpp b/src/svg/css-ostringstream.cpp new file mode 100644 index 0000000..e476f9f --- /dev/null +++ b/src/svg/css-ostringstream.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "svg/css-ostringstream.h" +#include "svg/strip-trailing-zeros.h" +#include "preferences.h" + +Inkscape::CSSOStringStream::CSSOStringStream() +{ + /* These two are probably unnecessary now that we provide our own operator<< for float and + * double. */ + ostr.imbue(std::locale::classic()); + ostr.setf(std::ios::showpoint); + + /* This one is (currently) needed though, as we currently use ostr.precision as a sort of + variable for storing the desired precision: see our two precision methods and our operator<< + methods for float and double. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + ostr.precision(prefs->getInt("/options/svgoutput/numericprecision", 8)); +} + +static void +write_num(Inkscape::CSSOStringStream &os, unsigned const prec, double const d) +{ + char buf[32]; // haven't thought about how much is really required. + switch (prec) { + case 9: g_ascii_formatd(buf, sizeof(buf), "%.9f", d); break; + case 8: g_ascii_formatd(buf, sizeof(buf), "%.8f", d); break; + case 7: g_ascii_formatd(buf, sizeof(buf), "%.7f", d); break; + case 6: g_ascii_formatd(buf, sizeof(buf), "%.6f", d); break; + case 5: g_ascii_formatd(buf, sizeof(buf), "%.5f", d); break; + case 4: g_ascii_formatd(buf, sizeof(buf), "%.4f", d); break; + case 3: g_ascii_formatd(buf, sizeof(buf), "%.3f", d); break; + case 2: g_ascii_formatd(buf, sizeof(buf), "%.2f", d); break; + case 1: g_ascii_formatd(buf, sizeof(buf), "%.1f", d); break; + case 0: g_ascii_formatd(buf, sizeof(buf), "%.0f", d); break; + case 10: default: g_ascii_formatd(buf, sizeof(buf), "%.10f", d); break; + } + os << strip_trailing_zeros(buf); +} + +Inkscape::CSSOStringStream & +operator<<(Inkscape::CSSOStringStream &os, float const d) +{ + /* Try as integer first. */ + { + long const n = long(d); + if (d == n) { + os << n; + return os; + } + } + + write_num(os, os.precision(), d); + return os; +} + +Inkscape::CSSOStringStream & +operator<<(Inkscape::CSSOStringStream &os, double const d) +{ + /* Try as integer first. */ + { + long const n = long(d); + if (d == n) { + os << n; + return os; + } + } + + write_num(os, os.precision(), d); + return os; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/css-ostringstream.h b/src/svg/css-ostringstream.h new file mode 100644 index 0000000..0df0406 --- /dev/null +++ b/src/svg/css-ostringstream.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SVG_CSS_OSTRINGSTREAM_H_INKSCAPE +#define SVG_CSS_OSTRINGSTREAM_H_INKSCAPE + +#include <sstream> + +namespace Inkscape { + +typedef std::ios_base &(*std_oct_type)(std::ios_base &); + +/** + * A thin wrapper around std::ostringstream, but writing floating point numbers in the format + * required by CSS: `.' as decimal separator, no `e' notation, no nan or inf. + */ +class CSSOStringStream { +private: + std::ostringstream ostr; + +public: + CSSOStringStream(); + +#define INK_CSS_STR_OP(_t) \ + CSSOStringStream &operator<<(_t arg) { \ + ostr << arg; \ + return *this; \ + } + + INK_CSS_STR_OP(char) + INK_CSS_STR_OP(signed char) + INK_CSS_STR_OP(unsigned char) + INK_CSS_STR_OP(short) + INK_CSS_STR_OP(unsigned short) + INK_CSS_STR_OP(int) + INK_CSS_STR_OP(unsigned int) + INK_CSS_STR_OP(long) + INK_CSS_STR_OP(unsigned long) + INK_CSS_STR_OP(char const *) + INK_CSS_STR_OP(signed char const *) + INK_CSS_STR_OP(unsigned char const *) + INK_CSS_STR_OP(std::string const &) + INK_CSS_STR_OP(std_oct_type) + +#undef INK_CSS_STR_OP + + std::string str() const { + return ostr.str(); + } + + std::streamsize precision() const { + return ostr.precision(); + } + + std::streamsize precision(std::streamsize p) { + return ostr.precision(p); + } +}; + +} + +Inkscape::CSSOStringStream &operator<<(Inkscape::CSSOStringStream &os, float d); + +Inkscape::CSSOStringStream &operator<<(Inkscape::CSSOStringStream &os, double d); + + +#endif /* !SVG_CSS_OSTRINGSTREAM_H_INKSCAPE */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/path-string.cpp b/src/svg/path-string.cpp new file mode 100644 index 0000000..95771ce --- /dev/null +++ b/src/svg/path-string.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::SVG::PathString - builder for SVG path strings + *//* + * Authors: see git history + * + * Copyright 2008 Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Copyright 2013 Tavmjong Bah <tavmjong@free.fr> + * Copyright (C) 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "svg/path-string.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "preferences.h" + +// 1<=numericprecision<=16, doubles are only accurate upto (slightly less than) 16 digits (and less than one digit doesn't make sense) +// Please note that these constants are used to allocate sufficient space to hold serialized numbers +static int const minprec = 1; +static int const maxprec = 16; + +int Inkscape::SVG::PathString::numericprecision; +int Inkscape::SVG::PathString::minimumexponent; +Inkscape::SVG::PATHSTRING_FORMAT Inkscape::SVG::PathString::format; + +Inkscape::SVG::PathString::PathString() : + force_repeat_commands(!Inkscape::Preferences::get()->getBool("/options/svgoutput/disable_optimizations" ) && Inkscape::Preferences::get()->getBool("/options/svgoutput/forcerepeatcommands")) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + format = (PATHSTRING_FORMAT)prefs->getIntLimited("/options/svgoutput/pathstring_format", 1, 0, PATHSTRING_FORMAT_SIZE - 1 ); + numericprecision = std::max<int>(minprec,std::min<int>(maxprec, prefs->getInt("/options/svgoutput/numericprecision", 8))); + minimumexponent = prefs->getInt("/options/svgoutput/minimumexponent", -8); +} + +// For absolute and relative paths... the entire path is kept in the "tail". +// For optimized path, at a switch between absolute and relative, add tail to commonbase. +void Inkscape::SVG::PathString::_appendOp(char abs_op, char rel_op) { + bool abs_op_repeated = _abs_state.prevop == abs_op && !force_repeat_commands; + bool rel_op_repeated = _rel_state.prevop == rel_op && !force_repeat_commands; + + // For absolute and relative paths... do nothing. + switch (format) { + case PATHSTRING_ABSOLUTE: + if ( !abs_op_repeated ) _abs_state.appendOp(abs_op); + break; + case PATHSTRING_RELATIVE: + if ( !rel_op_repeated ) _rel_state.appendOp(rel_op); + break; + case PATHSTRING_OPTIMIZE: + { + unsigned int const abs_added_size = abs_op_repeated ? 0 : 2; + unsigned int const rel_added_size = rel_op_repeated ? 0 : 2; + if ( _rel_state.str.size()+2 < _abs_state.str.size()+abs_added_size ) { + + // Store common prefix + commonbase += _rel_state.str; + _rel_state.str.clear(); + // Copy rel to abs + _abs_state = _rel_state; + _abs_state.switches++; + abs_op_repeated = false; + // We do not have to copy abs to rel: + // _rel_state.str.size()+2 < _abs_state.str.size()+abs_added_size + // _rel_state.str.size()+rel_added_size < _abs_state.str.size()+2 + // _abs_state.str.size()+2 > _rel_state.str.size()+rel_added_size + } else if ( _abs_state.str.size()+2 < _rel_state.str.size()+rel_added_size ) { + + // Store common prefix + commonbase += _abs_state.str; + _abs_state.str.clear(); + // Copy abs to rel + _rel_state = _abs_state; + _abs_state.switches++; + rel_op_repeated = false; + } + if ( !abs_op_repeated ) _abs_state.appendOp(abs_op); + if ( !rel_op_repeated ) _rel_state.appendOp(rel_op); + } + break; + default: + std::cout << "Better not be here!" << std::endl; + } +} + +void Inkscape::SVG::PathString::State::append(Geom::Coord v) { + str += ' '; + appendNumber(v); +} + +void Inkscape::SVG::PathString::State::append(Geom::Point p) { + str += ' '; + appendNumber(p[Geom::X]); + str += ','; + appendNumber(p[Geom::Y]); +} + +void Inkscape::SVG::PathString::State::append(Geom::Coord v, Geom::Coord& rv) { + str += ' '; + appendNumber(v, rv); +} + +void Inkscape::SVG::PathString::State::append(Geom::Point p, Geom::Point &rp) { + str += ' '; + appendNumber(p[Geom::X], rp[Geom::X]); + str += ','; + appendNumber(p[Geom::Y], rp[Geom::Y]); +} + +// NOTE: The following appendRelativeCoord function will not be exact if the total number of digits needed +// to represent the difference exceeds the precision of a double. This is not very likely though, and if +// it does happen the imprecise value is not likely to be chosen (because it will probably be a lot longer +// than the absolute value). + +// NOTE: This assumes v and r are already rounded (this includes flushing to zero if they are < 10^minexp) +void Inkscape::SVG::PathString::State::appendRelativeCoord(Geom::Coord v, Geom::Coord r) { + int const minexp = minimumexponent-numericprecision+1; + int const digitsEnd = (int)floor(log10(std::min(fabs(v),fabs(r)))) - numericprecision; // Position just beyond the last significant digit of the smallest (in absolute sense) number + double const roundeddiff = floor((v-r)*pow(10.,-digitsEnd-1)+.5); + int const numDigits = (int)floor(log10(fabs(roundeddiff)))+1; // Number of digits in roundeddiff + if (r == 0) { + appendNumber(v, numericprecision, minexp); + } else if (v == 0) { + appendNumber(-r, numericprecision, minexp); + } else if (numDigits>0) { + appendNumber(v-r, numDigits, minexp); + } else { + // This assumes the input numbers are already rounded to 'precision' digits + str += '0'; + } +} + +void Inkscape::SVG::PathString::State::appendRelative(Geom::Point p, Geom::Point r) { + str += ' '; + appendRelativeCoord(p[Geom::X], r[Geom::X]); + str += ','; + appendRelativeCoord(p[Geom::Y], r[Geom::Y]); +} + +void Inkscape::SVG::PathString::State::appendRelative(Geom::Coord v, Geom::Coord r) { + str += ' '; + appendRelativeCoord(v, r); +} + +void Inkscape::SVG::PathString::State::appendNumber(double v, int precision, int minexp) { + size_t const reserve = precision+1+1+1+1+3; // Just large enough to hold the maximum number of digits plus a sign, a period, the letter 'e', another sign and three digits for the exponent + size_t const oldsize = str.size(); + str.append(reserve, (char)0); + char* begin_of_num = const_cast<char*>(str.data()+oldsize); // Slightly evil, I know (but std::string should be storing its data in one big block of memory, so...) + size_t added = sp_svg_number_write_de(begin_of_num, reserve, v, precision, minexp); + str.resize(oldsize+added); // remove any trailing characters +} + +void Inkscape::SVG::PathString::State::appendNumber(double v, double &rv, int precision, int minexp) { + size_t const oldsize = str.size(); + appendNumber(v, precision, minexp); + char* begin_of_num = const_cast<char*>(str.data()+oldsize); // Slightly evil, I know (but std::string should be storing its data in one big block of memory, so...) + sp_svg_number_read_d(begin_of_num, &rv); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/path-string.h b/src/svg/path-string.h new file mode 100644 index 0000000..53080ba --- /dev/null +++ b/src/svg/path-string.h @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::SVG::PathString - builder for SVG path strings + *//* + * Authors: see git history + * + * Copyright 2007 MenTaLguY <mental@rydia.net> + * Copyright 2008 Jasper van de Gronde <th.v.d.gronde@hccnet.nl> + * Copyright 2013 Tavmjong Bah <tavmjong@free.fr> + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SVG_PATH_STRING_H +#define SEEN_INKSCAPE_SVG_PATH_STRING_H + +#include <2geom/point.h> +#include <cstdio> +#include <glibmm/ustring.h> +#include <string> + +namespace Inkscape { + +namespace SVG { + +// Relative vs. absolute coordinates +enum PATHSTRING_FORMAT { + PATHSTRING_ABSOLUTE, // Use only absolute coordinates + PATHSTRING_RELATIVE, // Use only relative coordinates + PATHSTRING_OPTIMIZE, // Optimize for path string length + PATHSTRING_FORMAT_SIZE +}; + +/** + * Builder for SVG path strings. + */ +class PathString { +public: + PathString(); + + // default copy + // default assign + + std::string const &string() { + std::string const &t = tail(); + final.reserve(commonbase.size()+t.size()); + final = commonbase; + final += tail(); + // std::cout << " final: " << final << std::endl; + return final; + } + + operator std::string const &() { + return string(); + } + + operator Glib::ustring const () const { + return commonbase + tail(); + } + + char const *c_str() { + return string().c_str(); + } + + PathString &moveTo(Geom::Coord x, Geom::Coord y) { + return moveTo(Geom::Point(x, y)); + } + + PathString &moveTo(Geom::Point p) { + _appendOp('M','m'); + _appendPoint(p, true); + + _initial_point = _current_point; + return *this; + } + + PathString &lineTo(Geom::Coord x, Geom::Coord y) { + return lineTo(Geom::Point(x, y)); + } + + PathString &lineTo(Geom::Point p) { + _appendOp('L','l'); + _appendPoint(p, true); + return *this; + } + + PathString &horizontalLineTo(Geom::Coord x) { + _appendOp('H','h'); + _appendX(x, true); + return *this; + } + + PathString &verticalLineTo(Geom::Coord y) { + _appendOp('V','v'); + _appendY(y, true); + return *this; + } + + PathString &quadTo(Geom::Coord cx, Geom::Coord cy, Geom::Coord x, Geom::Coord y) { + return quadTo(Geom::Point(cx, cy), Geom::Point(x, y)); + } + + PathString &quadTo(Geom::Point c, Geom::Point p) { + _appendOp('Q','q'); + _appendPoint(c, false); + _appendPoint(p, true); + return *this; + } + + PathString &curveTo(Geom::Coord x0, Geom::Coord y0, + Geom::Coord x1, Geom::Coord y1, + Geom::Coord x, Geom::Coord y) + { + return curveTo(Geom::Point(x0, y0), Geom::Point(x1, y1), Geom::Point(x, y)); + } + + PathString &curveTo(Geom::Point c0, Geom::Point c1, Geom::Point p) { + _appendOp('C','c'); + _appendPoint(c0, false); + _appendPoint(c1, false); + _appendPoint(p, true); + return *this; + } + + /** + * \param rot the angle in degrees + */ + PathString &arcTo(Geom::Coord rx, Geom::Coord ry, Geom::Coord rot, + bool large_arc, bool sweep, + Geom::Point p) + { + _appendOp('A','a'); + _appendValue(Geom::Point(rx,ry)); + _appendValue(rot); + _appendFlag(large_arc); + _appendFlag(sweep); + _appendPoint(p, true); + return *this; + } + + PathString &closePath() { + + _abs_state.appendOp('Z'); + _rel_state.appendOp('z'); + + _current_point = _initial_point; + return *this; + } + +private: + + void _appendOp(char abs_op, char rel_op); + + void _appendFlag(bool flag) { + _abs_state.append(flag); + _rel_state.append(flag); + } + + void _appendValue(Geom::Coord v) { + _abs_state.append(v); + _rel_state.append(v); + } + + void _appendValue(Geom::Point p) { + _abs_state.append(p); + _rel_state.append(p); + } + + void _appendX(Geom::Coord x, bool sc) { + double rx; + _abs_state.append(x, rx); + _rel_state.appendRelative(rx, _current_point[Geom::X]); + if (sc) _current_point[Geom::X] = rx; + } + + void _appendY(Geom::Coord y, bool sc) { + double ry; + _abs_state.append(y, ry); + _rel_state.appendRelative(ry, _current_point[Geom::Y]); + if (sc) _current_point[Geom::Y] = ry; + } + + void _appendPoint(Geom::Point p, bool sc) { + Geom::Point rp; + _abs_state.append(p, rp); + _rel_state.appendRelative(rp, _current_point); + if (sc) _current_point = rp; + } + + struct State { + State() { prevop = 0; switches = 0; } + + void appendOp(char op) { + if (prevop != 0) str += ' '; + str += op; + prevop = ( op == 'M' ? 'L' : op == 'm' ? 'l' : op ); + } + + void append(bool flag) { + str += ' '; + str += ( flag ? '1' : '0' ); + } + + void append(Geom::Coord v); + void append(Geom::Point v); + void append(Geom::Coord v, Geom::Coord& rv); + void append(Geom::Point p, Geom::Point& rp); + void appendRelative(Geom::Coord v, Geom::Coord r); + void appendRelative(Geom::Point p, Geom::Point r); + + bool operator<=(const State& s) const { + if ( str.size() < s.str.size() ) return true; + if ( str.size() > s.str.size() ) return false; + if ( switches < s.switches ) return true; + if ( switches > s.switches ) return false; + return true; + } + + // Note: changing this to Glib::ustring might cause problems in path-string.cpp because it assumes that + // size() returns the size of the string in BYTES (and Glib::ustring::resize is terribly slow) + std::string str; + unsigned int switches; + char prevop; + + private: + void appendNumber(double v, int precision=numericprecision, int minexp=minimumexponent); + void appendNumber(double v, double &rv, int precision=numericprecision, int minexp=minimumexponent); + void appendRelativeCoord(Geom::Coord v, Geom::Coord r); + } _abs_state, _rel_state; // State with the last operator being an absolute/relative operator + + Geom::Point _initial_point; + Geom::Point _current_point; + + // If both states have a common prefix it is stored here. + // Separating out the common prefix prevents repeated copying between the states + // to cause a quadratic time complexity (in the number of characters/operators) + std::string commonbase; + std::string final; + std::string const &tail() const { + return ( (format == PATHSTRING_ABSOLUTE) || + (format == PATHSTRING_OPTIMIZE && _abs_state <= _rel_state ) ? + _abs_state.str : _rel_state.str ); + } + + static PATHSTRING_FORMAT format; + bool const force_repeat_commands; + static int numericprecision; + static int minimumexponent; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/sp-svg.def b/src/svg/sp-svg.def new file mode 100644 index 0000000..6336f9b --- /dev/null +++ b/src/svg/sp-svg.def @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +EXPORTS + gnome_canvas_bpath_def_art_finish + gnome_canvas_bpath_def_closepath + gnome_canvas_bpath_def_curveto + gnome_canvas_bpath_def_free + gnome_canvas_bpath_def_lineto + gnome_canvas_bpath_def_moveto + gnome_canvas_bpath_def_new + gnome_canvas_bpath_def_new_from + gnome_canvas_bpath_def_ref + sp_svg_length_read + sp_svg_length_read_ldd + sp_svg_length_unset + sp_svg_length_update + sp_svg_number_read_d + sp_svg_number_read_f + sp_svg_number_write_de + sp_svg_number_write_f + sp_svg_number_write_fe + sp_svg_read_color + sp_svg_read_path + sp_svg_read_percentage + sp_svg_transform_read + sp_svg_transform_write + sp_svg_write_color + sp_svg_write_path diff --git a/src/svg/stringstream.cpp b/src/svg/stringstream.cpp new file mode 100644 index 0000000..3a56e9f --- /dev/null +++ b/src/svg/stringstream.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "svg/stringstream.h" +#include "svg/strip-trailing-zeros.h" +#include "preferences.h" +#include <2geom/point.h> + +Inkscape::SVGOStringStream::SVGOStringStream() +{ + /* These two are probably unnecessary now that we provide our own operator<< for float and + * double. */ + ostr.imbue(std::locale::classic()); + ostr.setf(std::ios::showpoint); + + /* This one is (currently) needed though, as we currently use ostr.precision as a sort of + variable for storing the desired precision: see our two precision methods and our operator<< + methods for float and double. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + ostr.precision(prefs->getInt("/options/svgoutput/numericprecision", 8)); +} + +Inkscape::SVGOStringStream & +operator<<(Inkscape::SVGOStringStream &os, float d) +{ + /* Try as integer first. */ + { + int const n = int(d); + if (d == n) { + os << n; + return os; + } + } + + std::ostringstream s; + s.imbue(std::locale::classic()); + s.flags(os.setf(std::ios::showpoint)); + s.precision(os.precision()); + s << d; + os << strip_trailing_zeros(s.str()); + return os; +} + +Inkscape::SVGOStringStream & +operator<<(Inkscape::SVGOStringStream &os, double d) +{ + /* Try as integer first. */ + { + int const n = int(d); + if (d == n) { + os << n; + return os; + } + } + + std::ostringstream s; + s.imbue(std::locale::classic()); + s.flags(os.setf(std::ios::showpoint)); + s.precision(os.precision()); + s << d; + os << strip_trailing_zeros(s.str()); + return os; +} + +Inkscape::SVGOStringStream & +operator<<(Inkscape::SVGOStringStream &os, Geom::Point const & p) +{ + os << p[0] << ',' << p[1]; + return os; +} + +Inkscape::SVGIStringStream::SVGIStringStream():std::istringstream() +{ + this->imbue(std::locale::classic()); + this->setf(std::ios::showpoint); + + /* This one is (currently) needed though, as we currently use ostr.precision as a sort of + variable for storing the desired precision: see our two precision methods and our operator<< + methods for float and double. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->precision(prefs->getInt("/options/svgoutput/numericprecision", 8)); +} + +Inkscape::SVGIStringStream::SVGIStringStream(const std::string& str):std::istringstream(str) +{ + this->imbue(std::locale::classic()); + this->setf(std::ios::showpoint); + + /* This one is (currently) needed though, as we currently use ostr.precision as a sort of + variable for storing the desired precision: see our two precision methods and our operator<< + methods for float and double. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->precision(prefs->getInt("/options/svgoutput/numericprecision", 8)); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/stringstream.h b/src/svg/stringstream.h new file mode 100644 index 0000000..f5d9073 --- /dev/null +++ b/src/svg/stringstream.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_STRINGSTREAM_H +#define INKSCAPE_STRINGSTREAM_H + +#include <sstream> +#include <string> + +#include <2geom/forward.h> + +namespace Inkscape { + +typedef std::ios_base &(*std_oct_type)(std::ios_base &); + +class SVGOStringStream { +private: + std::ostringstream ostr; + +public: + SVGOStringStream(); + +#define INK_SVG_STR_OP(_t) \ + SVGOStringStream &operator<<(_t arg) { \ + ostr << arg; \ + return *this; \ + } + + INK_SVG_STR_OP(char) + INK_SVG_STR_OP(signed char) + INK_SVG_STR_OP(unsigned char) + INK_SVG_STR_OP(short) + INK_SVG_STR_OP(unsigned short) + INK_SVG_STR_OP(int) + INK_SVG_STR_OP(unsigned int) + INK_SVG_STR_OP(long) + INK_SVG_STR_OP(unsigned long) + INK_SVG_STR_OP(char const *) + INK_SVG_STR_OP(signed char const *) + INK_SVG_STR_OP(unsigned char const *) + INK_SVG_STR_OP(std::string const &) + INK_SVG_STR_OP(std_oct_type) + +#undef INK_SVG_STR_OP + + std::string str() const { + return ostr.str(); + } + + void str (std::string &s) { + ostr.str(s); + } + + std::streamsize precision() const { + return ostr.precision(); + } + + std::streamsize precision(std::streamsize p) { + return ostr.precision(p); + } + + std::ios::fmtflags setf(std::ios::fmtflags fmtfl) { + return ostr.setf(fmtfl); + } + + std::ios::fmtflags setf(std::ios::fmtflags fmtfl, std::ios::fmtflags mask) { + return ostr.setf(fmtfl, mask); + } + + void unsetf(std::ios::fmtflags mask) { + ostr.unsetf(mask); + } +}; + +class SVGIStringStream:public std::istringstream { + +public: + SVGIStringStream(); + SVGIStringStream(const std::string &str); +}; + +} + +Inkscape::SVGOStringStream &operator<<(Inkscape::SVGOStringStream &os, float d); + +Inkscape::SVGOStringStream &operator<<(Inkscape::SVGOStringStream &os, double d); + +Inkscape::SVGOStringStream &operator<<(Inkscape::SVGOStringStream &os, Geom::Point const & p); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/strip-trailing-zeros.cpp b/src/svg/strip-trailing-zeros.cpp new file mode 100644 index 0000000..8abe4fa --- /dev/null +++ b/src/svg/strip-trailing-zeros.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glib.h> + +#include "svg/strip-trailing-zeros.h" + +std::string +strip_trailing_zeros(std::string str) +{ + std::string::size_type p_ix = str.find('.'); + if (p_ix != std::string::npos) { + std::string::size_type e_ix = str.find('e', p_ix); + /* N.B. In some contexts (e.g. CSS) it is an error for a number to contain `e'. fixme: + * Default to avoiding `e', e.g. using sprintf(str, "%17f", d). Add a new function that + * allows use of `e' and use that function only where the spec allows it. + */ + std::string::size_type nz_ix = str.find_last_not_of('0', (e_ix == std::string::npos + ? e_ix + : e_ix - 1)); + if (nz_ix == std::string::npos || nz_ix < p_ix || nz_ix >= e_ix) { + g_error("have `.' but couldn't find non-0"); + } else { + str.erase(str.begin() + (nz_ix == p_ix + ? p_ix + : nz_ix + 1), + (e_ix == std::string::npos + ? str.end() + : str.begin() + e_ix)); + } + } + return str; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/strip-trailing-zeros.h b/src/svg/strip-trailing-zeros.h new file mode 100644 index 0000000..1c5f537 --- /dev/null +++ b/src/svg/strip-trailing-zeros.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SVG_STRIP_TRAILING_ZEROS_H_SEEN +#define SVG_STRIP_TRAILING_ZEROS_H_SEEN + +#include <string> + +std::string strip_trailing_zeros(std::string str); + + +#endif /* !SVG_STRIP_TRAILING_ZEROS_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-affine-test.h b/src/svg/svg-affine-test.h new file mode 100644 index 0000000..fcb9db0 --- /dev/null +++ b/src/svg/svg-affine-test.h @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> + +#include "svg/svg.h" +#include "streq.h" +#include <2geom/affine.h> +#include <algorithm> +#include <glib.h> +#include <iostream> +#include <math.h> +#include <utility> + +class SvgAffineTest : public CxxTest::TestSuite +{ +private: + struct test_t { + char const * str; + Geom::Affine matrix; + }; + struct approx_equal_pred { + bool operator()(Geom::Affine const &ref, Geom::Affine const &cm) const + { + double maxabsdiff = 0; + for(size_t i=0; i<6; i++) { + maxabsdiff = std::max(std::abs(ref[i]-cm[i]), maxabsdiff); + } + return maxabsdiff < 1e-14; + } + }; + static test_t const read_matrix_tests[3]; + static test_t const read_translate_tests[3]; + static test_t const read_scale_tests[3]; + static test_t const read_rotate_tests[4]; + static test_t const read_skew_tests[3]; + static char const * const read_fail_tests[25]; + static test_t const write_matrix_tests[2]; + static test_t const write_translate_tests[3]; + static test_t const write_scale_tests[3]; + static test_t const write_rotate_tests[3]; + static test_t const write_skew_tests[3]; +public: + SvgAffineTest() { + } + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static SvgAffineTest *createSuite() { return new SvgAffineTest(); } + static void destroySuite( SvgAffineTest *suite ) { delete suite; } + + void testReadIdentity() + { + char const* strs[] = { + //0, + "", + "matrix(1,0,0,1,0,0)", + "translate(0,0)", + "scale(1,1)", + "rotate(0,0,0)", + "skewX(0)", + "skewY(0)"}; + size_t n = G_N_ELEMENTS(strs); + for(size_t i=0; i<n; i++) { + Geom::Affine cm; + TSM_ASSERT(strs[i] , sp_svg_transform_read(strs[i], &cm)); + TSM_ASSERT_EQUALS(strs[i] , Geom::identity() , cm); + } + } + + void testWriteIdentity() + { + gchar str = sp_svg_transform_write(Geom::identity()); + TS_ASSERT_EQUALS(str, NULL); + g_free(str); + } + + void testReadMatrix() + { + for(size_t i=0; i<G_N_ELEMENTS(read_matrix_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_matrix_tests[i].str , sp_svg_transform_read(read_matrix_tests[i].str, &cm)); + TSM_ASSERT_RELATION(read_matrix_tests[i].str , approx_equal_pred , read_matrix_tests[i].matrix , cm); + } + } + + void testReadTranslate() + { + for(size_t i=0; i<G_N_ELEMENTS(read_translate_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_translate_tests[i].str , sp_svg_transform_read(read_translate_tests[i].str, &cm)); + TSM_ASSERT_RELATION(read_translate_tests[i].str , approx_equal_pred , read_translate_tests[i].matrix , cm); + } + } + + void testReadScale() + { + for(size_t i=0; i<G_N_ELEMENTS(read_scale_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_scale_tests[i].str , sp_svg_transform_read(read_scale_tests[i].str, &cm)); + TSM_ASSERT_RELATION(read_scale_tests[i].str , approx_equal_pred , read_scale_tests[i].matrix , cm); + } + } + + void testReadRotate() + { + for(size_t i=0; i<G_N_ELEMENTS(read_rotate_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_rotate_tests[i].str , sp_svg_transform_read(read_rotate_tests[i].str, &cm)); + TSM_ASSERT_RELATION(read_rotate_tests[i].str , approx_equal_pred , read_rotate_tests[i].matrix , cm); + } + } + + void testReadSkew() + { + for(size_t i=0; i<G_N_ELEMENTS(read_skew_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_skew_tests[i].str , sp_svg_transform_read(read_skew_tests[i].str, &cm)); + TSM_ASSERT_RELATION(read_skew_tests[i].str , approx_equal_pred , read_skew_tests[i].matrix , cm); + } + } + + void testWriteMatrix() + { + for(size_t i=0; i<G_N_ELEMENTS(write_matrix_tests); i++) { + char * str = sp_svg_transform_write(write_matrix_tests[i].matrix); + TS_ASSERT_RELATION(streq_rel , str , write_matrix_tests[i].str); + g_free(str); + } + } + + void testWriteTranslate() + { + for(size_t i=0; i<G_N_ELEMENTS(write_translate_tests); i++) { + char * str = sp_svg_transform_write(write_translate_tests[i].matrix); + TS_ASSERT_RELATION(streq_rel , str , write_translate_tests[i].str); + g_free(str); + } + } + + void testWriteScale() + { + for(size_t i=0; i<G_N_ELEMENTS(write_scale_tests); i++) { + char * str = sp_svg_transform_write(write_scale_tests[i].matrix); + TS_ASSERT_RELATION(streq_rel , str , write_scale_tests[i].str); + g_free(str); + } + } + + void testWriteRotate() + { + for(size_t i=0; i<G_N_ELEMENTS(write_rotate_tests); i++) { + char * str = sp_svg_transform_write(write_rotate_tests[i].matrix); + TS_ASSERT_RELATION(streq_rel , str , write_rotate_tests[i].str); + g_free(str); + } + } + + void testWriteSkew() + { + for(size_t i=0; i<G_N_ELEMENTS(write_skew_tests); i++) { + char * str = sp_svg_transform_write(write_skew_tests[i].matrix); + TS_ASSERT_RELATION(streq_rel , str , write_skew_tests[i].str); + g_free(str); + } + } + + void testReadConcatenation() + { + // NOTE: According to the SVG specification (see the syntax at http://www.w3.org/TR/SVG/coords.html#TransformAttribute + // there should be 1 or more comma-wsp sequences between transforms... This doesn't make sense and it seems + // likely that instead of a + they meant a ? (zero or one comma-wsp sequences). + char const * str = "skewY(17)skewX(9)translate(7,13)scale(2)rotate(13)translate(3,5)"; + Geom::Affine ref(2.0199976232558053, 1.0674773585906016, -0.14125199392774669, 1.9055550612095459, 14.412730624347654, 28.499820929377454); // Precomputed using Mathematica + Geom::Affine cm; + TS_ASSERT(sp_svg_transform_read(str, &cm)); + TS_ASSERT_RELATION(approx_equal_pred , ref , cm); + } + + void testReadFailures() + { + for(size_t i=0; i<G_N_ELEMENTS(read_fail_tests); i++) { + Geom::Affine cm; + TSM_ASSERT(read_fail_tests[i] , !sp_svg_transform_read(read_fail_tests[i], &cm)); + } + } +}; + +static double const DEGREE = M_PI/180.; + +SvgAffineTest::test_t const SvgAffineTest::read_matrix_tests[3] = { + {"matrix(0,0,0,0,0,0)",Geom::Affine(0,0,0,0,0,0)}, + {" matrix(1,2,3,4,5,6)",Geom::Affine(1,2,3,4,5,6)}, + {"matrix (1 2 -3,-4,5e6,-6e-7)",Geom::Affine(1,2,-3,-4,5e6,-6e-7)}}; +SvgAffineTest::test_t const SvgAffineTest::read_translate_tests[3] = { + {"translate(1)",Geom::Affine(1,0,0,1,1,0)}, + {"translate(1,1)",Geom::Affine(1,0,0,1,1,1)}, + {"translate(-1e3 .123e2)",Geom::Affine(1,0,0,1,-1e3,.123e2)}}; +SvgAffineTest::test_t const SvgAffineTest::read_scale_tests[3] = { + {"scale(2)",Geom::Affine(2,0,0,2,0,0)}, + {"scale(2,3)",Geom::Affine(2,0,0,3,0,0)}, + {"scale(0.1e-2 -.475e0)",Geom::Affine(0.1e-2,0,0,-.475e0,0,0)}}; +SvgAffineTest::test_t const SvgAffineTest::read_rotate_tests[4] = { + {"rotate(13 )",Geom::Affine(cos(13.*DEGREE),sin(13.*DEGREE),-sin(13.*DEGREE),cos(13.*DEGREE),0,0)}, + {"rotate(-13)",Geom::Affine(cos(-13.*DEGREE),sin(-13.*DEGREE),-sin(-13.*DEGREE),cos(-13.*DEGREE),0,0)}, + {"rotate(373)",Geom::Affine(cos(13.*DEGREE),sin(13.*DEGREE),-sin(13.*DEGREE),cos(13.*DEGREE),0,0)}, + {"rotate(13,7,11)",Geom::Affine(cos(13.*DEGREE),sin(13.*DEGREE),-sin(13.*DEGREE),cos(13.*DEGREE),(1-cos(13.*DEGREE))*7+sin(13.*DEGREE)*11,(1-cos(13.*DEGREE))*11-sin(13.*DEGREE)*7)}}; +SvgAffineTest::test_t const SvgAffineTest::read_skew_tests[3] = { + {"skewX( 30)",Geom::Affine(1,0,tan(30.*DEGREE),1,0,0)}, + {"skewX(-30)",Geom::Affine(1,0,tan(-30.*DEGREE),1,0,0)}, + {"skewY(390)",Geom::Affine(1,tan(30.*DEGREE),0,1,0,0)}}; +char const * const SvgAffineTest::read_fail_tests[25] = { + "matrix((1,2,3,4,5,6)", + "matrix((1,2,3,4,5,6))", + "matrix(1,2,3,4,5,6))", + "matrix(,1,2,3,4,5,6)", + "matrix(1,2,3,4,5,6,)", + "matrix(1,2,3,4,5,)", + "matrix(1,2,3,4,5)", + "matrix(1,2,3,4,5e6-3)", // Here numbers HAVE to be separated by a comma-wsp sequence + "matrix(1,2,3,4,5e6.3)", // Here numbers HAVE to be separated by a comma-wsp sequence + "translate()", + "translate(,)", + "translate(1,)", + "translate(1,6,)", + "translate(1,6,0)", + "scale()", + "scale(1,6,2)", + "rotate()", + "rotate(1,6)", + "rotate(1,6,)", + "rotate(1,6,3,4)", + "skewX()", + "skewX(-)", + "skewX(.)", + "skewY(,)", + "skewY(1,2)"}; + +SvgAffineTest::test_t const SvgAffineTest::write_matrix_tests[2] = { + {"matrix(1,2,3,4,5,6)",Geom::Affine(1,2,3,4,5,6)}, + {"matrix(-1,2123,3,0.4,1e-8,1e20)",Geom::Affine(-1,2.123e3,3+1e-14,0.4,1e-8,1e20)}}; +SvgAffineTest::test_t const SvgAffineTest::write_translate_tests[3] = { + {"translate(1,1)",Geom::Affine(1,0,0,1,1,1)}, + {"translate(1)",Geom::Affine(1,0,0,1,1,0)}, + {"translate(-1345,0.123)",Geom::Affine(1,0,0,1,-1.345e3,.123)}}; +SvgAffineTest::test_t const SvgAffineTest::write_scale_tests[3] = { + {"scale(0)",Geom::Affine(0,0,0,0,0,0)}, + {"scale(7)",Geom::Affine(7,0,0,7,0,0)}, + {"scale(2,3)",Geom::Affine(2,0,0,3,0,0)}}; +SvgAffineTest::test_t const SvgAffineTest::write_rotate_tests[3] = { + {"rotate(13)",Geom::Affine(cos(13.*DEGREE),sin(13.*DEGREE),-sin(13.*DEGREE),cos(13.*DEGREE),0,0)}, + {"rotate(-13,7,11)",Geom::Affine(cos(-13.*DEGREE),sin(-13.*DEGREE),-sin(-13.*DEGREE),cos(-13.*DEGREE),(1-cos(-13.*DEGREE))*7+sin(-13.*DEGREE)*11,(1-cos(-13.*DEGREE))*11-sin(-13.*DEGREE)*7)}, + {"rotate(-34.5,6.7,89)",Geom::Affine(cos(-34.5*DEGREE),sin(-34.5*DEGREE),-sin(-34.5*DEGREE),cos(-34.5*DEGREE),(1-cos(-34.5*DEGREE))*6.7+sin(-34.5*DEGREE)*89,(1-cos(-34.5*DEGREE))*89-sin(-34.5*DEGREE)*6.7)}}; +SvgAffineTest::test_t const SvgAffineTest::write_skew_tests[3] = { + {"skewX(30)",Geom::Affine(1,0,tan(30.*DEGREE),1,0,0)}, + {"skewX(-30)",Geom::Affine(1,0,tan(-30.*DEGREE),1,0,0)}, + {"skewY(30)",Geom::Affine(1,tan(30.*DEGREE),0,1,0,0)}}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-affine.cpp b/src/svg/svg-affine.cpp new file mode 100644 index 0000000..e5fcce6 --- /dev/null +++ b/src/svg/svg-affine.cpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * SVG data parser + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Raph Levien <raph@acm.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 1999 Raph Levien + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <cstdlib> +#include <cstdio> +#include <glib.h> +#include <2geom/transforms.h> +#include "svg.h" +#include "preferences.h" + +bool +sp_svg_transform_read(gchar const *str, Geom::Affine *transform) +{ + int idx; + char keyword[32]; + double args[6]; + int n_args; + size_t key_len; + + if (str == nullptr) return false; + + Geom::Affine a(Geom::identity()); + + idx = 0; + while (str[idx]) { + /* skip initial whitespace */ + while (g_ascii_isspace (str[idx])) idx++; + + // SVG2: allow commas in separation of transforms + if (str[idx] == ',') { + ++idx; + while (g_ascii_isspace(str[idx])) + ++idx; + } + + /* parse keyword */ + for (key_len = 0; key_len < sizeof (keyword); key_len++) { + char c; + + c = str[idx]; + if (g_ascii_isalpha (c) || c == '-') { + keyword[key_len] = str[idx++]; + } else { + break; + } + } + if (key_len >= sizeof (keyword)) return false; + keyword[key_len] = '\0'; + + /* skip whitespace */ + while (g_ascii_isspace (str[idx])) idx++; + + if (str[idx] != '(') return false; + idx++; + + for (n_args = 0; n_args < 6; n_args++) { + char c; + char *end_ptr; + + /* skip whitespace */ + while (g_ascii_isspace (str[idx])) idx++; + c = str[idx]; + if (g_ascii_isdigit (c) || c == '+' || c == '-' || c == '.') { + if (n_args == sizeof (args) / sizeof (args[0])) return false; /* Too many args */ + args[n_args] = g_ascii_strtod (str + idx, &end_ptr); + + //printf("took %d chars from '%s' to make %f\n", + // end_ptr-(str+idx), + // str+idx, + // args[n_args]); + + idx = end_ptr - (char *) str; + + while (g_ascii_isspace (str[idx])) idx++; + + /* skip optional comma */ + if (str[idx] == ',') idx++; + } else if (c == ')') { + break; + } else { + return false; + } + } + idx++; + + /* ok, have parsed keyword and args, now modify the transform */ + if (!strcmp (keyword, "matrix")) { + if (n_args != 6) return false; + a = (*((Geom::Affine *) &(args)[0])) * a; + } else if (!strcmp (keyword, "translate")) { + if (n_args == 1) { + args[1] = 0; + } else if (n_args != 2) { + return false; + } + a = Geom::Translate(args[0], args[1]) * a; + } else if (!strcmp (keyword, "scale")) { + if (n_args == 1) { + args[1] = args[0]; + } else if (n_args != 2) { + return false; + } + a = Geom::Scale(args[0], args[1]) * a; + } else if (!strcmp (keyword, "rotate")) { + if (n_args != 1 && n_args != 3) { + return false; + } + Geom::Rotate const rot(Geom::rad_from_deg(args[0])); + if (n_args == 3) { + a = ( Geom::Translate(-args[1], -args[2]) + * rot + * Geom::Translate(args[1], args[2]) + * Geom::Affine(a) ); + } else { + a = rot * a; + } + } else if (!strcmp (keyword, "skewX")) { + if (n_args != 1) return false; + a = ( Geom::Affine(1, 0, + tan(args[0] * M_PI / 180.0), 1, + 0, 0) + * a ); + } else if (!strcmp (keyword, "skewY")) { + if (n_args != 1) return false; + a = ( Geom::Affine(1, tan(args[0] * M_PI / 180.0), + 0, 1, + 0, 0) + * a ); + } else { + return false; /* unknown keyword */ + } + /* Skip trailing whitespace */ + while (g_ascii_isspace (str[idx])) idx++; + } + + *transform = a; + return true; +} + +#define EQ(a,b) (fabs ((a) - (b)) < 1e-9) + +gchar * +sp_svg_transform_write(Geom::Affine const &transform) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // this must be a bit grater than EPSILON + double e = 1e-5 * transform.descrim(); + int prec = prefs->getInt("/options/svgoutput/numericprecision", 8); + int min_exp = prefs->getInt("/options/svgoutput/minimumexponent", -8); + + // Special case: when all fields of the affine are zero, + // the optimized transformation is scale(0) + if (transform[0] == 0 && transform[1] == 0 && transform[2] == 0 && + transform[3] == 0 && transform[4] == 0 && transform[5] == 0) + { + return g_strdup("scale(0)"); + } + + // FIXME legacy C code! + // the function sp_svg_number_write_de is stopping me from using a proper C++ string + + gchar c[256]; // string buffer + unsigned p = 0; // position in the buffer + + if (transform.isIdentity()) { + // We are more or less identity, so no transform attribute needed: + return nullptr; + } else if (transform.isScale()) { + // We are more or less a uniform scale + strcpy (c + p, "scale("); + p += 6; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[0], prec, min_exp ); + if (Geom::are_near(transform[0], transform[3], e)) { + c[p++] = ')'; + c[p] = '\000'; + } else { + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[3], prec, min_exp ); + c[p++] = ')'; + c[p] = '\000'; + } + } else if (transform.isTranslation()) { + // We are more or less a pure translation + strcpy (c + p, "translate("); + p += 10; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[4], prec, min_exp ); + if (Geom::are_near(transform[5], 0.0, e)) { + c[p++] = ')'; + c[p] = '\000'; + } else { + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[5], prec, min_exp ); + c[p++] = ')'; + c[p] = '\000'; + } + } else if (transform.isRotation()) { + // We are more or less a pure rotation + strcpy(c + p, "rotate("); + p += 7; + + double angle = std::atan2(transform[1], transform[0]) * (180 / M_PI); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, angle, prec, min_exp); + + c[p++] = ')'; + c[p] = '\000'; + } else if (transform.withoutTranslation().isRotation()) { + // Solution found by Johan Engelen + // Refer to the matrix in svg-affine-test.h + + // We are a rotation about a special axis + strcpy(c + p, "rotate("); + p += 7; + + double angle = std::atan2(transform[1], transform[0]) * (180 / M_PI); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, angle, prec, min_exp); + c[p++] = ','; + + Geom::Affine const& m = transform; + double tx = (m[2]*m[5]+m[4]-m[4]*m[3]) / (1-m[3]-m[0]+m[0]*m[3]-m[2]*m[1]); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, tx, prec, min_exp); + + c[p++] = ','; + + double ty = (m[1]*tx + m[5]) / (1 - m[3]); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, ty, prec, min_exp); + + c[p++] = ')'; + c[p] = '\000'; + } else if (transform.isHShear()) { + // We are more or less a pure skewX + strcpy(c + p, "skewX("); + p += 6; + + double angle = atan(transform[2]) * (180 / M_PI); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, angle, prec, min_exp); + + c[p++] = ')'; + c[p] = '\000'; + } else if (transform.isVShear()) { + // We are more or less a pure skewY + strcpy(c + p, "skewY("); + p += 6; + + double angle = atan(transform[1]) * (180 / M_PI); + p += sp_svg_number_write_de(c + p, sizeof(c) - p, angle, prec, min_exp); + + c[p++] = ')'; + c[p] = '\000'; + } else { + strcpy (c + p, "matrix("); + p += 7; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[0], prec, min_exp ); + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[1], prec, min_exp ); + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[2], prec, min_exp ); + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[3], prec, min_exp ); + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[4], prec, min_exp ); + c[p++] = ','; + p += sp_svg_number_write_de( c + p, sizeof(c) - p, transform[5], prec, min_exp ); + c[p++] = ')'; + c[p] = '\000'; + } + + assert(p <= sizeof(c)); + return g_strdup(c); +} + + +gchar * +sp_svg_transform_write(Geom::Affine const *transform) +{ + return sp_svg_transform_write(*transform); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-angle.cpp b/src/svg/svg-angle.cpp new file mode 100644 index 0000000..e82d0de --- /dev/null +++ b/src/svg/svg-angle.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * \file src/svg/svg-angle.cpp + * \brief SVG angle type + */ +/* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glib.h> + +#include "svg/svg-angle.h" +#include "util/units.h" + + +static bool sp_svg_angle_read_lff(gchar const *str, SVGAngle::Unit &unit, float &val, float &computed); + +SVGAngle::SVGAngle() + : _set(false) + , unit(NONE) + , value(0) + , computed(0) +{ +} + +/* Angle */ + +bool SVGAngle::read(gchar const *str) +{ + if (!str) { + return false; + } + + SVGAngle::Unit u; + float v; + float c; + if (!sp_svg_angle_read_lff(str, u, v, c)) { + return false; + } + + _set = true; + unit = u; + value = v; + computed = c; + + return true; +} + +void SVGAngle::unset(SVGAngle::Unit u, float v, float c) { + _set = false; + unit = u; + value = v; + computed = c; +} + +void SVGAngle::readOrUnset(gchar const *str, Unit u, float v, float c) { + if (!read(str)) { + unset(u, v, c); + } +} + +static bool sp_svg_angle_read_lff(gchar const *str, SVGAngle::Unit &unit, float &val, float &computed) +{ + if (!str) { + return false; + } + + gchar const *e; + float const v = g_ascii_strtod(str, (char **) &e); + if (e == str) { + return false; + } + + if (!e[0]) { + /* Unitless (defaults to degrees)*/ + unit = SVGAngle::NONE; + val = v; + computed = v; + return true; + } else if (!g_ascii_isalnum(e[0])) { + if (g_ascii_isspace(e[0]) && e[1] && g_ascii_isalpha(e[1])) { + return false; // spaces between value and unit are not allowed + } else { + /* Unitless (defaults to degrees)*/ + unit = SVGAngle::NONE; + val = v; + computed = v; + return true; + } + } else { + if (strncmp(e, "deg", 3) == 0) { + unit = SVGAngle::DEG; + val = v; + computed = v; + } else if (strncmp(e, "grad", 4) == 0) { + unit = SVGAngle::GRAD; + val = v; + computed = Inkscape::Util::Quantity::convert(v, "grad", "°"); + } else if (strncmp(e, "rad", 3) == 0) { + unit = SVGAngle::RAD; + val = v; + computed = Inkscape::Util::Quantity::convert(v, "rad", "°"); + } else if (strncmp(e, "turn", 4) == 0) { + unit = SVGAngle::TURN; + val = v; + computed = Inkscape::Util::Quantity::convert(v, "turn", "°"); + } else { + return false; + } + return true; + } + + /* Invalid */ + return false; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-angle.h b/src/svg/svg-angle.h new file mode 100644 index 0000000..6ed5915 --- /dev/null +++ b/src/svg/svg-angle.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SVG_ANGLE_H +#define SEEN_SP_SVG_ANGLE_H + +/** + * \file src/svg/svg-angle.h + * \brief SVG angle type + */ +/* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +class SVGAngle +{ +public: + SVGAngle(); + + enum Unit { + NONE, + DEG, + GRAD, + RAD, + TURN, + LAST_UNIT = TURN + }; + + // The object's value is valid / exists in SVG. + bool _set; + + // The unit of value. + Unit unit; + + // The value of this SVGAngle as found in the SVG. + float value; + + // The value in degrees. + float computed; + + float operator=(float v) { + _set = true; + unit = NONE; + value = computed = v; + return v; + } + + bool read(gchar const *str); + void unset(Unit u = NONE, float v = 0, float c = 0); + void readOrUnset(gchar const *str, Unit u = NONE, float v = 0, float c = 0); +}; + +#endif // SEEN_SP_SVG_ANGLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-color-test.h b/src/svg/svg-color-test.h new file mode 100644 index 0000000..b1723cb --- /dev/null +++ b/src/svg/svg-color-test.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> +#include <cassert> +#include <cstdlib> + +#include "preferences.h" +#include "svg/svg-color.h" +#include "svg/svg-icc-color.h" + +class SVGColorTest : public CxxTest::TestSuite +{ + struct simpleIccCase { + unsigned numEntries; + bool shouldPass; + char const* name; + char const* str; + }; + +public: + void check_rgb24(unsigned const rgb24) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + char css[8]; + prefs->setBool("/options/svgoutput/usenamedcolors", false); + sp_svg_write_color(css, sizeof(css), rgb24 << 8); + TS_ASSERT_EQUALS(sp_svg_read_color(css, 0xff), + rgb24 << 8); + prefs->setBool("/options/svgoutput/usenamedcolors", true); + sp_svg_write_color(css, sizeof(css), rgb24 << 8); + TS_ASSERT_EQUALS(sp_svg_read_color(css, 0xff), + rgb24 << 8); + } + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static SVGColorTest *createSuite() { return new SVGColorTest(); } + static void destroySuite( SVGColorTest *suite ) { delete suite; } + + void testWrite() + { + unsigned const components[] = {0, 0x80, 0xff, 0xc0, 0x77}; + unsigned const nc = G_N_ELEMENTS(components); + for (unsigned i = nc*nc*nc; i--;) { + unsigned tmp = i; + unsigned rgb24 = 0; + for (unsigned c = 0; c < 3; ++c) { + unsigned const component = components[tmp % nc]; + rgb24 = (rgb24 << 8) | component; + tmp /= nc; + } + assert( tmp == 0 ); + check_rgb24(rgb24); + } + + /* And a few completely random ones. */ + for (unsigned i = 500; i--;) { /* Arbitrary number of iterations. */ + unsigned const rgb24 = (std::rand() >> 4) & 0xffffff; + check_rgb24(rgb24); + } + } + + void testReadColor() + { + gchar const* val[] = {"#f0f", "#ff00ff", "rgb(255,0,255)", "fuchsia"}; + size_t const n = sizeof(val)/sizeof(*val); + for(size_t i=0; i<n; i++) { + gchar const* end = 0; + guint32 result = sp_svg_read_color( val[i], &end, 0x3 ); + TS_ASSERT_EQUALS( result, 0xff00ff00 ); + TS_ASSERT_LESS_THAN( val[i], end ); + } + } + + void testIccColor() + { + simpleIccCase cases[] = { + {1, true, "named", "icc-color(named, 3)"}, + {0, false, "", "foodle"}, + {1, true, "a", "icc-color(a, 3)"}, + {4, true, "named", "icc-color(named, 3, 0, 0.1, 2.5)"}, + {0, false, "", "icc-color(named, 3"}, + {0, false, "", "icc-color(space named, 3)"}, + {0, false, "", "icc-color(tab\tnamed, 3)"}, + {0, false, "", "icc-color(0name, 3)"}, + {0, false, "", "icc-color(-name, 3)"}, + {1, true, "positive", "icc-color(positive, +3)"}, + {1, true, "negative", "icc-color(negative, -3)"}, + {1, true, "positive", "icc-color(positive, +0.1)"}, + {1, true, "negative", "icc-color(negative, -0.1)"}, + {0, false, "", "icc-color(named, value)"}, + {1, true, "hyphen-name", "icc-color(hyphen-name, 1)"}, + {1, true, "under_name", "icc-color(under_name, 1)"}, + }; + + for ( size_t i = 0; i < G_N_ELEMENTS(cases); i++ ) { + SVGICCColor tmp; + gchar const* str = cases[i].str; + gchar const* result = 0; + + std::string testDescr( cases[i].str ); + + bool parseRet = sp_svg_read_icc_color( str, &result, &tmp ); + TSM_ASSERT_EQUALS( testDescr, parseRet, cases[i].shouldPass ); + TSM_ASSERT_EQUALS( testDescr, tmp.colors.size(), cases[i].numEntries ); + if ( cases[i].shouldPass ) { + TSM_ASSERT_DIFFERS( testDescr, str, result ); + TSM_ASSERT_EQUALS( testDescr, tmp.colorProfile, std::string(cases[i].name) ); + } else { + TSM_ASSERT_EQUALS( testDescr, str, result ); + TSM_ASSERT( testDescr, tmp.colorProfile.empty() ); + } + } + } + +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-color.cpp b/src/svg/svg-color.cpp new file mode 100644 index 0000000..1c8c021 --- /dev/null +++ b/src/svg/svg-color.cpp @@ -0,0 +1,659 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * \file + * Reading \& writing of SVG/CSS colors. + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstdlib> +#include <cstdio> // sprintf +#include <cstring> +#include <string> +#include <cmath> +#include <glib.h> // g_assert +#include <cerrno> + +#include <map> + +#include "colorspace.h" +#include "strneq.h" +#include "preferences.h" +#include "svg-color.h" +#include "svg-icc-color.h" + +#include "color.h" +#if defined(HAVE_LIBLCMS2) + +#include <vector> +#include "object/color-profile.h" + +#include "document.h" +#include "inkscape.h" +#include "profile-manager.h" +#endif // defined(HAVE_LIBLCMS2) + +#include "cms-system.h" + +struct SPSVGColor { + unsigned long rgb; + const std::string name; +}; + +/* + * These are the colors defined in the SVG standard + */ +static SPSVGColor const sp_svg_color_named[] = { + { 0xF0F8FF, "aliceblue" }, + { 0xFAEBD7, "antiquewhite" }, + { 0x00FFFF, "aqua" }, + { 0x7FFFD4, "aquamarine" }, + { 0xF0FFFF, "azure" }, + { 0xF5F5DC, "beige" }, + { 0xFFE4C4, "bisque" }, + { 0x000000, "black" }, + { 0xFFEBCD, "blanchedalmond" }, + { 0x0000FF, "blue" }, + { 0x8A2BE2, "blueviolet" }, + { 0xA52A2A, "brown" }, + { 0xDEB887, "burlywood" }, + { 0x5F9EA0, "cadetblue" }, + { 0x7FFF00, "chartreuse" }, + { 0xD2691E, "chocolate" }, + { 0xFF7F50, "coral" }, + { 0x6495ED, "cornflowerblue" }, + { 0xFFF8DC, "cornsilk" }, + { 0xDC143C, "crimson" }, + { 0x00FFFF, "cyan" }, + { 0x00008B, "darkblue" }, + { 0x008B8B, "darkcyan" }, + { 0xB8860B, "darkgoldenrod" }, + { 0xA9A9A9, "darkgray" }, + { 0x006400, "darkgreen" }, + { 0xA9A9A9, "darkgrey" }, + { 0xBDB76B, "darkkhaki" }, + { 0x8B008B, "darkmagenta" }, + { 0x556B2F, "darkolivegreen" }, + { 0xFF8C00, "darkorange" }, + { 0x9932CC, "darkorchid" }, + { 0x8B0000, "darkred" }, + { 0xE9967A, "darksalmon" }, + { 0x8FBC8F, "darkseagreen" }, + { 0x483D8B, "darkslateblue" }, + { 0x2F4F4F, "darkslategray" }, + { 0x2F4F4F, "darkslategrey" }, + { 0x00CED1, "darkturquoise" }, + { 0x9400D3, "darkviolet" }, + { 0xFF1493, "deeppink" }, + { 0x00BFFF, "deepskyblue" }, + { 0x696969, "dimgray" }, + { 0x696969, "dimgrey" }, + { 0x1E90FF, "dodgerblue" }, + { 0xB22222, "firebrick" }, + { 0xFFFAF0, "floralwhite" }, + { 0x228B22, "forestgreen" }, + { 0xFF00FF, "fuchsia" }, + { 0xDCDCDC, "gainsboro" }, + { 0xF8F8FF, "ghostwhite" }, + { 0xFFD700, "gold" }, + { 0xDAA520, "goldenrod" }, + { 0x808080, "gray" }, + { 0x808080, "grey" }, + { 0x008000, "green" }, + { 0xADFF2F, "greenyellow" }, + { 0xF0FFF0, "honeydew" }, + { 0xFF69B4, "hotpink" }, + { 0xCD5C5C, "indianred" }, + { 0x4B0082, "indigo" }, + { 0xFFFFF0, "ivory" }, + { 0xF0E68C, "khaki" }, + { 0xE6E6FA, "lavender" }, + { 0xFFF0F5, "lavenderblush" }, + { 0x7CFC00, "lawngreen" }, + { 0xFFFACD, "lemonchiffon" }, + { 0xADD8E6, "lightblue" }, + { 0xF08080, "lightcoral" }, + { 0xE0FFFF, "lightcyan" }, + { 0xFAFAD2, "lightgoldenrodyellow" }, + { 0xD3D3D3, "lightgray" }, + { 0x90EE90, "lightgreen" }, + { 0xD3D3D3, "lightgrey" }, + { 0xFFB6C1, "lightpink" }, + { 0xFFA07A, "lightsalmon" }, + { 0x20B2AA, "lightseagreen" }, + { 0x87CEFA, "lightskyblue" }, + { 0x778899, "lightslategray" }, + { 0x778899, "lightslategrey" }, + { 0xB0C4DE, "lightsteelblue" }, + { 0xFFFFE0, "lightyellow" }, + { 0x00FF00, "lime" }, + { 0x32CD32, "limegreen" }, + { 0xFAF0E6, "linen" }, + { 0xFF00FF, "magenta" }, + { 0x800000, "maroon" }, + { 0x66CDAA, "mediumaquamarine" }, + { 0x0000CD, "mediumblue" }, + { 0xBA55D3, "mediumorchid" }, + { 0x9370DB, "mediumpurple" }, + { 0x3CB371, "mediumseagreen" }, + { 0x7B68EE, "mediumslateblue" }, + { 0x00FA9A, "mediumspringgreen" }, + { 0x48D1CC, "mediumturquoise" }, + { 0xC71585, "mediumvioletred" }, + { 0x191970, "midnightblue" }, + { 0xF5FFFA, "mintcream" }, + { 0xFFE4E1, "mistyrose" }, + { 0xFFE4B5, "moccasin" }, + { 0xFFDEAD, "navajowhite" }, + { 0x000080, "navy" }, + { 0xFDF5E6, "oldlace" }, + { 0x808000, "olive" }, + { 0x6B8E23, "olivedrab" }, + { 0xFFA500, "orange" }, + { 0xFF4500, "orangered" }, + { 0xDA70D6, "orchid" }, + { 0xEEE8AA, "palegoldenrod" }, + { 0x98FB98, "palegreen" }, + { 0xAFEEEE, "paleturquoise" }, + { 0xDB7093, "palevioletred" }, + { 0xFFEFD5, "papayawhip" }, + { 0xFFDAB9, "peachpuff" }, + { 0xCD853F, "peru" }, + { 0xFFC0CB, "pink" }, + { 0xDDA0DD, "plum" }, + { 0xB0E0E6, "powderblue" }, + { 0x800080, "purple" }, + { 0x663399, "rebeccapurple" }, + { 0xFF0000, "red" }, + { 0xBC8F8F, "rosybrown" }, + { 0x4169E1, "royalblue" }, + { 0x8B4513, "saddlebrown" }, + { 0xFA8072, "salmon" }, + { 0xF4A460, "sandybrown" }, + { 0x2E8B57, "seagreen" }, + { 0xFFF5EE, "seashell" }, + { 0xA0522D, "sienna" }, + { 0xC0C0C0, "silver" }, + { 0x87CEEB, "skyblue" }, + { 0x6A5ACD, "slateblue" }, + { 0x708090, "slategray" }, + { 0x708090, "slategrey" }, + { 0xFFFAFA, "snow" }, + { 0x00FF7F, "springgreen" }, + { 0x4682B4, "steelblue" }, + { 0xD2B48C, "tan" }, + { 0x008080, "teal" }, + { 0xD8BFD8, "thistle" }, + { 0xFF6347, "tomato" }, + { 0x40E0D0, "turquoise" }, + { 0xEE82EE, "violet" }, + { 0xF5DEB3, "wheat" }, + { 0xFFFFFF, "white" }, + { 0xF5F5F5, "whitesmoke" }, + { 0xFFFF00, "yellow" }, + { 0x9ACD32, "yellowgreen" } +}; + +static std::map<std::string, unsigned long> sp_svg_create_color_hash(); + +guint32 sp_svg_read_color(gchar const *str, guint32 const dfl) +{ + return sp_svg_read_color(str, nullptr, dfl); +} + +static guint32 internal_sp_svg_read_color(gchar const *str, gchar const **end_ptr, guint32 def) +{ + static std::map<std::string, unsigned long> colors; + guint32 val = 0; + + if (str == nullptr) return def; + while ((*str <= ' ') && *str) str++; + if (!*str) return def; + + if (str[0] == '#') { + gint i; + for (i = 1; str[i]; i++) { + int hexval; + if (str[i] >= '0' && str[i] <= '9') + hexval = str[i] - '0'; + else if (str[i] >= 'A' && str[i] <= 'F') + hexval = str[i] - 'A' + 10; + else if (str[i] >= 'a' && str[i] <= 'f') + hexval = str[i] - 'a' + 10; + else + break; + val = (val << 4) + hexval; + } + /* handle #rgb case */ + if (i == 1 + 3) { + val = ((val & 0xf00) << 8) | + ((val & 0x0f0) << 4) | + (val & 0x00f); + val |= val << 4; + } else if (i != 1 + 6) { + /* must be either 3 or 6 digits. */ + return def; + } + if (end_ptr) { + *end_ptr = str + i; + } + } else if (strneq(str, "rgb(", 4)) { + bool hasp, hasd; + gchar *s, *e; + gdouble r, g, b; + + s = (gchar *) str + 4; + hasp = false; + hasd = false; + + r = g_ascii_strtod(s, &e); + if (s == e) return def; + s = e; + if (*s == '%') { + hasp = true; + s += 1; + } else { + hasd = true; + } + while (*s && g_ascii_isspace(*s)) s += 1; + if (*s != ',') return def; + s += 1; + while (*s && g_ascii_isspace(*s)) s += 1; + g = g_ascii_strtod(s, &e); + if (s == e) return def; + s = e; + if (*s == '%') { + hasp = true; + s += 1; + } else { + hasd = true; + } + while (*s && g_ascii_isspace(*s)) s += 1; + if (*s != ',') return def; + s += 1; + while (*s && g_ascii_isspace(*s)) s += 1; + b = g_ascii_strtod(s, &e); + if (s == e) return def; + s = e; + if (*s == '%') { + hasp = true; + s += 1; + } else { + hasd = true; + } + while(*s && g_ascii_isspace(*s)) s += 1; + if (*s != ')') { + return def; + } + ++s; + if (hasp && hasd) return def; + if (hasp) { + val = static_cast<guint>(floor(CLAMP(r, 0.0, 100.0) * 2.559999)) << 24; + val |= (static_cast<guint>(floor(CLAMP(g, 0.0, 100.0) * 2.559999)) << 16); + val |= (static_cast<guint>(floor(CLAMP(b, 0.0, 100.0) * 2.559999)) << 8); + } else { + val = static_cast<guint>(CLAMP(r, 0, 255)) << 24; + val |= (static_cast<guint>(CLAMP(g, 0, 255)) << 16); + val |= (static_cast<guint>(CLAMP(b, 0, 255)) << 8); + } + if (end_ptr) { + *end_ptr = s; + } + return val; + } else if (strneq(str, "hsl(", 4)) { + + gchar *ptr = (gchar *) str + 4; + + gchar *e; // ptr after read + + double h = g_ascii_strtod(ptr, &e); // Read h (0-360) + if (ptr == e) return def; // Read failed + ptr = e; + + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + if (*ptr != ',') return def; // Need comma + ptr += 1; + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + + double s = g_ascii_strtod(ptr, &e); // Read s (percent) + if (ptr == e) return def; // Read failed + ptr = e; + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + if (*ptr != '%') return def; // Need % + ptr += 1; + + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + if (*ptr != ',') return def; // Need comma + ptr += 1; + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + + double l = g_ascii_strtod(ptr, &e); // Read l (percent) + if (ptr == e) return def; // Read failed + ptr = e; + while (*ptr && g_ascii_isspace(*ptr)) ptr += 1; // Remove any white space + if (*ptr != '%') return def; // Need % + ptr += 1; + + if (end_ptr) { + *end_ptr = ptr; + } + + + // Normalize to 0..1 + h /= 360.0; + s /= 100.0; + l /= 100.0; + + gfloat rgb[3]; + + SPColor::hsl_to_rgb_floatv( rgb, h, s, l ); + + val = static_cast<guint>(floor(CLAMP(rgb[0], 0.0, 1.0) * 255.9999)) << 24; + val |= (static_cast<guint>(floor(CLAMP(rgb[1], 0.0, 1.0) * 255.9999)) << 16); + val |= (static_cast<guint>(floor(CLAMP(rgb[2], 0.0, 1.0) * 255.9999)) << 8); + return val; + + } else { + gint i; + if (colors.empty()) { + colors = sp_svg_create_color_hash(); + } + gchar c[32]; + for (i = 0; i < 31; i++) { + if (str[i] == ';' || g_ascii_isspace(str[i])) { + c[i] = '\0'; + break; + } + c[i] = g_ascii_tolower(str[i]); + if (!str[i]) break; + } + c[31] = '\0'; + + if (colors.count(std::string(c))) { + val = colors[std::string(c)]; + } + else { + return def; + } + if (end_ptr) { + *end_ptr = str + i; + } + } + + return (val << 8); +} + +guint32 sp_svg_read_color(gchar const *str, gchar const **end_ptr, guint32 dfl) +{ + /* I've been rather hurried in editing the above to add support for end_ptr, so I'm adding + * this check wrapper. */ + gchar const *end = str; + guint32 const ret = internal_sp_svg_read_color(str, &end, dfl); + g_assert(((ret == dfl) && (end == str)) + || (((ret & 0xff) == 0) + && (str < end))); + if (str < end) { + gchar *buf = (gchar *) g_malloc(end + 1 - str); + memcpy(buf, str, end - str); + buf[end - str] = '\0'; + gchar const *buf_end = buf; + guint32 const check = internal_sp_svg_read_color(buf, &buf_end, 1); + g_assert(check == ret + && buf_end - buf == end - str); + g_free(buf); + + if ( end_ptr ) { + *end_ptr = end; + } + } + return ret; +} + + +/** + * Converts an RGB colour expressed in form 0x00rrggbb to a CSS/SVG representation of that colour. + * The result is valid even in SVG Tiny or non-SVG CSS. + */ +static void rgb24_to_css(char *const buf, unsigned const rgb24) +{ + g_assert(rgb24 < (1u << 24)); + + /* SVG 1.1 Full allows additional colour names not supported by SVG Tiny, but we don't bother + * with them: it's good for these colours to be copyable to non-SVG CSS stylesheets and for + * documents to be more viewable in SVG Tiny/Basic viewers; and some of the SVG Full names are + * less meaningful than hex equivalents anyway. And it's easier for a person to map from the + * restricted set because the only component values are {00,80,ff}, other than silver 0xc0c0c0. + */ + + char const *src = nullptr; + switch (rgb24) { + /* Extracted mechanically from the table at + * http://www.w3.org/TR/REC-html40/types.html#h-6.5 .*/ + case 0x000000: src = "black"; break; + case 0xc0c0c0: src = "silver"; break; + case 0x808080: src = "gray"; break; + case 0xffffff: src = "white"; break; + case 0x800000: src = "maroon"; break; + case 0xff0000: src = "red"; break; + case 0x800080: src = "purple"; break; + case 0xff00ff: src = "fuchsia"; break; + case 0x008000: src = "green"; break; + case 0x00ff00: src = "lime"; break; + case 0x808000: src = "olive"; break; + case 0xffff00: src = "yellow"; break; + case 0x000080: src = "navy"; break; + case 0x0000ff: src = "blue"; break; + case 0x008080: src = "teal"; break; + case 0x00ffff: src = "aqua"; break; + + default: { + if ((rgb24 & 0xf0f0f) * 0x11 == rgb24) { + /* Can use the shorter three-digit form #rgb instead of #rrggbb. */ + std::sprintf(buf, "#%x%x%x", + (rgb24 >> 16) & 0xf, + (rgb24 >> 8) & 0xf, + rgb24 & 0xf); + } else { + std::sprintf(buf, "#%06x", rgb24); + } + break; + } + } + if (src) { + strcpy(buf, src); + } + + // assert(sp_svg_read_color(buf, 0xff) == (rgb24 << 8)); +} + +/** + * Converts an RGBA32 colour to a CSS/SVG representation of the RGB portion of that colour. The + * result is valid even in SVG Tiny or non-SVG CSS. + * + * \param rgba32 Colour expressed in form 0xrrggbbaa. + * \pre buflen \>= 8. + */ +void sp_svg_write_color(gchar *buf, unsigned const buflen, guint32 const rgba32) +{ + g_assert(8 <= buflen); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + unsigned const rgb24 = rgba32 >> 8; + if ( prefs->getBool("/options/svgoutput/usenamedcolors") && + !prefs->getBool("/options/svgoutput/disable_optimizations" )) { + rgb24_to_css(buf, rgb24); + } else { + g_snprintf(buf, buflen, "#%06x", rgb24); + } +} + +static std::map<std::string, unsigned long> +sp_svg_create_color_hash() +{ + std::map<std::string, unsigned long> colors; + + for (const auto & i : sp_svg_color_named) { + colors[i.name] = i.rgb; + } + return colors; +} + +#if defined(HAVE_LIBLCMS2) + +void icc_color_to_sRGB(SVGICCColor* icc, guchar* r, guchar* g, guchar* b) +{ + if (icc) { + g_message("profile name: %s", icc->colorProfile.c_str()); + Inkscape::ColorProfile* prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(icc->colorProfile.c_str()); + if ( prof ) { + guchar color_out[4] = {0,0,0,0}; + cmsHTRANSFORM trans = prof->getTransfToSRGB8(); + if ( trans ) { + std::vector<colorspace::Component> comps = colorspace::getColorSpaceInfo( prof ); + + size_t count = Inkscape::CMSSystem::getChannelCount( prof ); + size_t cap = std::min(count, comps.size()); + guchar color_in[4]; + for (size_t i = 0; i < cap; i++) { + color_in[i] = static_cast<guchar>((((gdouble)icc->colors[i]) * 256.0) * (gdouble)comps[i].scale); + g_message("input[%d]: %d", (int)i, (int)color_in[i]); + } + + Inkscape::CMSSystem::doTransform( trans, color_in, color_out, 1 ); +g_message("transform to sRGB done"); + } + *r = color_out[0]; + *g = color_out[1]; + *b = color_out[2]; + } + } +} +#endif //defined(HAVE_LIBLCMS2) + +/* + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + */ +bool sp_svg_read_icc_color( gchar const *str, gchar const **end_ptr, SVGICCColor* dest ) +{ + bool good = true; + + if ( end_ptr ) { + *end_ptr = str; + } + if ( dest ) { + dest->colorProfile.clear(); + dest->colors.clear(); + } + + if ( !str ) { + // invalid input + good = false; + } else { + while ( g_ascii_isspace(*str) ) { + str++; + } + + good = strneq( str, "icc-color(", 10 ); + + if ( good ) { + str += 10; + while ( g_ascii_isspace(*str) ) { + str++; + } + + if ( !g_ascii_isalpha(*str) + && ( !(0x080 & *str) ) + && (*str != '_') + && (*str != ':') ) { + // Name must start with a certain type of character + good = false; + } else { + while ( g_ascii_isdigit(*str) || g_ascii_isalpha(*str) + || (*str == '-') || (*str == ':') || (*str == '_') || (*str == '.') ) { + if ( dest ) { + dest->colorProfile += *str; + } + str++; + } + while ( g_ascii_isspace(*str) || *str == ',' ) { + str++; + } + } + } + + if ( good ) { + while ( *str && *str != ')' ) { + if ( g_ascii_isdigit(*str) || *str == '.' || *str == '-' || *str == '+') { + gchar* endPtr = nullptr; + gdouble dbl = g_ascii_strtod( str, &endPtr ); + if ( !errno ) { + if ( dest ) { + dest->colors.push_back( dbl ); + } + str = endPtr; + } else { + good = false; + break; + } + + while ( g_ascii_isspace(*str) || *str == ',' ) { + str++; + } + } else { + break; + } + } + } + + // We need to have ended on a closing parenthesis + if ( good ) { + while ( g_ascii_isspace(*str) ) { + str++; + } + good &= (*str == ')'); + } + } + + if ( good ) { + if ( end_ptr ) { + *end_ptr = str; + } + } else { + if ( dest ) { + dest->colorProfile.clear(); + dest->colors.clear(); + } + } + + return good; +} + + +bool sp_svg_read_icc_color( gchar const *str, SVGICCColor* dest ) +{ + return sp_svg_read_icc_color(str, nullptr, dest); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-color.h b/src/svg/svg-color.h new file mode 100644 index 0000000..6b1cc02 --- /dev/null +++ b/src/svg/svg-color.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SVG_SVG_COLOR_H_SEEN +#define SVG_SVG_COLOR_H_SEEN + +typedef unsigned int guint32; +struct SVGICCColor; + +guint32 sp_svg_read_color(char const *str, unsigned int dfl); +guint32 sp_svg_read_color(char const *str, char const **end_ptr, guint32 def); +void sp_svg_write_color(char *buf, unsigned int buflen, unsigned int rgba32); + +bool sp_svg_read_icc_color( char const *str, char const **end_ptr, SVGICCColor* dest ); +bool sp_svg_read_icc_color( char const *str, SVGICCColor* dest ); +void icc_color_to_sRGB(SVGICCColor* dest, unsigned char* r, unsigned char* g, unsigned char* b); + +#endif /* !SVG_SVG_COLOR_H_SEEN */ diff --git a/src/svg/svg-icc-color.h b/src/svg/svg-icc-color.h new file mode 100644 index 0000000..2a1f89e --- /dev/null +++ b/src/svg/svg-icc-color.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SVG_ICC_COLOR_H_SEEN +#define SVG_ICC_COLOR_H_SEEN + +#include <string> +#include <vector> + +/** + * An icc-color specification. Corresponds to the DOM interface of the same name. + * + * Referenced by SPIPaint. + */ +struct SVGICCColor { + std::string colorProfile; + std::vector<double> colors; +}; + + +#endif /* !SVG_ICC_COLOR_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-length-test.h b/src/svg/svg-length-test.h new file mode 100644 index 0000000..38f2046 --- /dev/null +++ b/src/svg/svg-length-test.h @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> + +#include "svg/svg-length.h" +#include <glib.h> +#include <utility> + +// function internal to svg-length.cpp: +gchar const *sp_svg_length_get_css_units(SVGLength::Unit unit); + +class SvgLengthTest : public CxxTest::TestSuite +{ +private: + struct test_t { + char const* str; SVGLength::Unit unit; float value; float computed; + }; + struct testd_t { + char const* str; double val; int prec; int minexp; + }; + static test_t const absolute_tests[12]; + static test_t const relative_tests[3]; + static char const * fail_tests[8]; + +public: + SvgLengthTest() { + } + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static SvgLengthTest *createSuite() { return new SvgLengthTest(); } + static void destroySuite( SvgLengthTest *suite ) { delete suite; } + + void testRead() + { + for(size_t i=0; i<G_N_ELEMENTS(absolute_tests); i++) { + SVGLength len; + TSM_ASSERT(absolute_tests[i].str , len.read(absolute_tests[i].str)); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.unit , absolute_tests[i].unit); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.value , absolute_tests[i].value); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.computed , absolute_tests[i].computed); + } + for(size_t i=0; i<G_N_ELEMENTS(relative_tests); i++) { + SVGLength len; + TSM_ASSERT(relative_tests[i].str , len.read(relative_tests[i].str)); + len.update(7,13,19); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.unit , relative_tests[i].unit); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.value , relative_tests[i].value); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.computed , relative_tests[i].computed); + } + for(size_t i=0; i<G_N_ELEMENTS(fail_tests); i++) { + SVGLength len; + TSM_ASSERT(fail_tests[i] , !len.read(fail_tests[i])); + } + } + + void testReadOrUnset() + { + for(size_t i=0; i<G_N_ELEMENTS(absolute_tests); i++) { + SVGLength len; + len.readOrUnset(absolute_tests[i].str); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.unit , absolute_tests[i].unit); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.value , absolute_tests[i].value); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.computed , absolute_tests[i].computed); + } + for(size_t i=0; i<G_N_ELEMENTS(relative_tests); i++) { + SVGLength len; + len.readOrUnset(relative_tests[i].str); + len.update(7,13,19); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.unit , relative_tests[i].unit); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.value , relative_tests[i].value); + TSM_ASSERT_EQUALS(relative_tests[i].str , len.computed , relative_tests[i].computed); + } + for(size_t i=0; i<G_N_ELEMENTS(fail_tests); i++) { + SVGLength len; + len.readOrUnset(fail_tests[i], SVGLength::INCH, 123, 456); + TSM_ASSERT_EQUALS(fail_tests[i] , len.unit , SVGLength::INCH); + TSM_ASSERT_EQUALS(fail_tests[i] , len.value , 123); + TSM_ASSERT_EQUALS(fail_tests[i] , len.computed , 456); + } + } + + void testReadAbsolute() + { + for(size_t i=0; i<G_N_ELEMENTS(absolute_tests); i++) { + SVGLength len; + TSM_ASSERT(absolute_tests[i].str , len.readAbsolute(absolute_tests[i].str)); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.unit , absolute_tests[i].unit); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.value , absolute_tests[i].value); + TSM_ASSERT_EQUALS(absolute_tests[i].str , len.computed , absolute_tests[i].computed); + } + for(size_t i=0; i<G_N_ELEMENTS(relative_tests); i++) { + SVGLength len; + TSM_ASSERT(relative_tests[i].str , !len.readAbsolute(relative_tests[i].str)); + } + for(size_t i=0; i<G_N_ELEMENTS(fail_tests); i++) { + SVGLength len; + TSM_ASSERT(fail_tests[i] , !len.readAbsolute(fail_tests[i])); + } + } + + void testEnumMappedToString() + { + for ( int i = (static_cast<int>(SVGLength::NONE) + 1); i <= static_cast<int>(SVGLength::LAST_UNIT); i++ ) { + SVGLength::Unit target = static_cast<SVGLength::Unit>(i); + // PX is a special case where we don't have a unit string + if ( (target != SVGLength::PX) ) { + gchar const* val = sp_svg_length_get_css_units(target); + TSM_ASSERT_DIFFERS(i, val, ""); + } + } + } + + // Ensure that all unit suffix strings used are allowed by SVG + void testStringsAreValidSVG() + { + gchar const* valid[] = {"", "em", "ex", "px", "pt", "pc", "cm", "mm", "in", "%"}; + std::set<std::string> validStrings(valid, valid + G_N_ELEMENTS(valid)); + for ( int i = (static_cast<int>(SVGLength::NONE) + 1); i <= static_cast<int>(SVGLength::LAST_UNIT); i++ ) { + SVGLength::Unit target = static_cast<SVGLength::Unit>(i); + gchar const* val = sp_svg_length_get_css_units(target); + TSM_ASSERT(i, validStrings.find(std::string(val)) != validStrings.end()); + } + } + + // Ensure that all unit suffix strings allowed by SVG are covered by enum + void testValidSVGStringsSupported() + { + // Note that "px" is omitted from the list, as it will be assumed to be so if not explicitly set. + gchar const* valid[] = {"em", "ex", "pt", "pc", "cm", "mm", "in", "%"}; + std::set<std::string> validStrings(valid, valid + G_N_ELEMENTS(valid)); + for ( int i = (static_cast<int>(SVGLength::NONE) + 1); i <= static_cast<int>(SVGLength::LAST_UNIT); i++ ) { + SVGLength::Unit target = static_cast<SVGLength::Unit>(i); + gchar const* val = sp_svg_length_get_css_units(target); + std::set<std::string>::iterator iter = validStrings.find(std::string(val)); + if (iter != validStrings.end()) { + validStrings.erase(iter); + } + } + TSM_ASSERT_EQUALS(validStrings, validStrings.size(), 0u); + } + + void testPlaces() + { + testd_t const precTests[] = { + {"760", 761.92918978947023, 2, -8}, + {"761.9", 761.92918978947023, 4, -8}, + }; + + for ( size_t i = 0; i < G_N_ELEMENTS(precTests); i++ ) { + char buf[256] = {0}; + memset(buf, 0xCC, sizeof(buf)); // Make it easy to detect an overrun. + unsigned int retval = sp_svg_number_write_de( buf, sizeof(buf), precTests[i].val, precTests[i].prec, precTests[i].minexp ); + TSM_ASSERT_EQUALS("Number of chars written", retval, strlen(precTests[i].str)); + TSM_ASSERT_EQUALS("Numeric string written", std::string(buf), std::string(precTests[i].str)); + TSM_ASSERT_EQUALS(std::string("Buffer overrun ") + precTests[i].str, '\xCC', buf[retval + 1]); + } + } + + // TODO: More tests +}; + +SvgLengthTest::test_t const SvgLengthTest::absolute_tests[12] = { + {"0", SVGLength::NONE, 0 , 0}, + {"1", SVGLength::NONE, 1 , 1}, + {"1.00001", SVGLength::NONE, 1.00001 , 1.00001}, + {"1px", SVGLength::PX , 1 , 1}, + {".1px", SVGLength::PX , 0.1 , 0.1}, + {"100pt", SVGLength::PT , 100 , 400.0/3.0}, + {"1e2pt", SVGLength::PT , 100 , 400.0/3.0}, + {"3pc", SVGLength::PC , 3 , 48}, + {"-3.5pc", SVGLength::PC , -3.5 , -3.5*16.0}, + {"1.2345678mm", SVGLength::MM , 1.2345678, 1.2345678f*96.0/25.4}, // TODO: More precise constants? (a 7 digit constant when the default precision is 8 digits?) + {"123.45678cm", SVGLength::CM , 123.45678 , 123.45678f*96.0/2.54}, // Note that svg_length_read is casting the result from g_ascii_strtod to float. + {"73.162987in", SVGLength::INCH, 73.162987 , 73.162987f*96.0/1.00}}; +SvgLengthTest::test_t const SvgLengthTest::relative_tests[3] = { + {"123em", SVGLength::EM, 123, 123. * 7.}, + {"123ex", SVGLength::EX, 123, 123. * 13.}, + {"123%", SVGLength::PERCENT, 1.23, 1.23 * 19.}}; +char const * SvgLengthTest::fail_tests[8] = { + "123 px", + "123e", + "123e+m", + "123ec", + "123pxt", + "--123", + "", + "px"}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-length.cpp b/src/svg/svg-length.cpp new file mode 100644 index 0000000..a84c38b --- /dev/null +++ b/src/svg/svg-length.cpp @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SVG data parser + *//* + * Authors: see git history + + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <cstring> +#include <string> +#include <glib.h> +#include <iostream> +#include <vector> + +#include "svg.h" +#include "stringstream.h" +#include "util/units.h" + +static unsigned sp_svg_length_read_lff(gchar const *str, SVGLength::Unit *unit, float *val, float *computed, char **next); + +#ifndef MAX +# define MAX(a,b) ((a < b) ? (b) : (a)) +#endif + +unsigned int sp_svg_number_read_f(gchar const *str, float *val) +{ + if (!str) { + return 0; + } + + char *e; + float const v = g_ascii_strtod(str, &e); + if ((gchar const *) e == str) { + return 0; + } + + *val = v; + return 1; +} + +unsigned int sp_svg_number_read_d(gchar const *str, double *val) +{ + if (!str) { + return 0; + } + + char *e; + double const v = g_ascii_strtod(str, &e); + if ((gchar const *) e == str) { + return 0; + } + + *val = v; + return 1; +} + +// TODO must add a buffer length parameter for safety: +// rewrite using std::string? +static unsigned int sp_svg_number_write_ui(gchar *buf, unsigned int val) +{ + unsigned int i = 0; + char c[16u]; + do { + c[16u - (++i)] = '0' + (val % 10u); + val /= 10u; + } while (val > 0u); + + memcpy(buf, &c[16u - i], i); + buf[i] = 0; + + return i; +} + +// TODO unsafe code ignoring bufLen +// rewrite using std::string? +static unsigned int sp_svg_number_write_i(gchar *buf, int bufLen, int val) +{ + int p = 0; + unsigned int uval; + if (val < 0) { + buf[p++] = '-'; + uval = (unsigned int)-val; + } else { + uval = (unsigned int)val; + } + + p += sp_svg_number_write_ui(buf+p, uval); + + return p; +} + +// TODO unsafe code ignoring bufLen +// rewrite using std::string? +static unsigned sp_svg_number_write_d(gchar *buf, int bufLen, double val, unsigned int tprec, unsigned int fprec) +{ + /* Process sign */ + int i = 0; + if (val < 0.0) { + buf[i++] = '-'; + val = fabs(val); + } + + /* Determine number of integral digits */ + int idigits = 0; + if (val >= 1.0) { + idigits = (int) floor(log10(val)) + 1; + } + + /* Determine the actual number of fractional digits */ + fprec = MAX(static_cast<int>(fprec), static_cast<int>(tprec) - idigits); + /* Round value */ + val += 0.5 / pow(10.0, fprec); + /* Extract integral and fractional parts */ + double dival = floor(val); + double fval = val - dival; + /* Write integra */ + if (idigits > (int)tprec) { + i += sp_svg_number_write_ui(buf + i, (unsigned int)floor(dival/pow(10.0, idigits-tprec) + .5)); + for(unsigned int j=0; j<(unsigned int)idigits-tprec; j++) { + buf[i+j] = '0'; + } + i += idigits-tprec; + } else { + i += sp_svg_number_write_ui(buf + i, (unsigned int)dival); + } + int end_i = i; + if (fprec > 0 && fval > 0.0) { + buf[i++] = '.'; + do { + fval *= 10.0; + dival = floor(fval); + fval -= dival; + int const int_dival = (int) dival; + buf[i++] = '0' + int_dival; + if (int_dival != 0) { + end_i = i; + } + fprec -= 1; + } while(fprec > 0 && fval > 0.0); + } + buf[end_i] = 0; + return end_i; +} + +unsigned int sp_svg_number_write_de(gchar *buf, int bufLen, double val, unsigned int tprec, int min_exp) +{ + int eval = (int)floor(log10(fabs(val))); + if (val == 0.0 || eval < min_exp) { + return sp_svg_number_write_ui(buf, 0); + } + unsigned int maxnumdigitsWithoutExp = // This doesn't include the sign because it is included in either representation + eval<0?tprec+(unsigned int)-eval+1: + eval+1<(int)tprec?tprec+1: + (unsigned int)eval+1; + unsigned int maxnumdigitsWithExp = tprec + ( eval<0 ? 4 : 3 ); // It's not necessary to take larger exponents into account, because then maxnumdigitsWithoutExp is DEFINITELY larger + if (maxnumdigitsWithoutExp <= maxnumdigitsWithExp) { + return sp_svg_number_write_d(buf, bufLen, val, tprec, 0); + } else { + val = eval < 0 ? val * pow(10.0, -eval) : val / pow(10.0, eval); + int p = sp_svg_number_write_d(buf, bufLen, val, tprec, 0); + buf[p++] = 'e'; + p += sp_svg_number_write_i(buf + p, bufLen - p, eval); + return p; + } +} + +SVGLength::SVGLength() + : _set(false) + , unit(NONE) + , value(0) + , computed(0) +{ +} + +/* Length */ + +bool SVGLength::read(gchar const *str) +{ + if (!str) { + return false; + } + + SVGLength::Unit u; + float v; + float c; + if (!sp_svg_length_read_lff(str, &u, &v, &c, nullptr)) { + return false; + } + + if (!std::isfinite(v)) { + return false; + } + + _set = true; + unit = u; + value = v; + computed = c; + + return true; +} + +static bool svg_length_absolute_unit(SVGLength::Unit u) +{ + return (u != SVGLength::EM && u != SVGLength::EX && u != SVGLength::PERCENT); +} + +bool SVGLength::readAbsolute(gchar const *str) +{ + if (!str) { + return false; + } + + SVGLength::Unit u; + float v; + float c; + if (!sp_svg_length_read_lff(str, &u, &v, &c, nullptr)) { + return false; + } + + if (svg_length_absolute_unit(u) == false) { + return false; + } + + _set = true; + unit = u; + value = v; + computed = c; + + return true; +} + + +unsigned int sp_svg_length_read_computed_absolute(gchar const *str, float *length) +{ + if (!str) { + return 0; + } + + SVGLength::Unit unit; + float computed; + if (!sp_svg_length_read_lff(str, &unit, nullptr, &computed, nullptr)) { + // failed to read + return 0; + } + + if (svg_length_absolute_unit(unit) == false) { + return 0; + } + + *length = computed; + + return 1; +} + +std::vector<SVGLength> sp_svg_length_list_read(gchar const *str) +{ + if (!str) { + return std::vector<SVGLength>(); + } + + SVGLength::Unit unit; + float value; + float computed; + char *next = (char *) str; + std::vector<SVGLength> list; + + while (sp_svg_length_read_lff(next, &unit, &value, &computed, &next)) { + + SVGLength length; + length.set(unit, value, computed); + list.push_back(length); + + while (next && *next && + (*next == ',' || *next == ' ' || *next == '\n' || *next == '\r' || *next == '\t')) { + // the list can be comma- or space-separated, but we will be generous and accept + // a mix, including newlines and tabs + next++; + } + + if (!next || !*next) { + break; + } + } + + return list; +} + + +#define UVAL(a,b) (((unsigned int) (a) << 8) | (unsigned int) (b)) + +static unsigned sp_svg_length_read_lff(gchar const *str, SVGLength::Unit *unit, float *val, float *computed, char **next) +{ +/* note: this function is sometimes fed a string with several consecutive numbers, e.g. by sp_svg_length_list_read. +So after the number, the string does not necessarily have a \0 or a unit, it might also contain a space or comma and then the next number! +*/ + + if (!str) { + return 0; + } + + gchar const *e; + float const v = g_ascii_strtod(str, (char **) &e); + if (e == str) { + return 0; + } + + if (!e[0]) { + /* Unitless */ + if (unit) { + *unit = SVGLength::NONE; + } + if (val) { + *val = v; + } + if (computed) { + *computed = v; + } + if (next) { + *next = nullptr; // no more values + } + return 1; + } else if (!g_ascii_isalnum(e[0])) { + /* Unitless or percent */ + if (e[0] == '%') { + /* Percent */ + if (e[1] && g_ascii_isalnum(e[1])) { + return 0; + } + if (unit) { + *unit = SVGLength::PERCENT; + } + if (val) { + *val = v * 0.01; + } + if (computed) { + *computed = v * 0.01; + } + if (next) { + *next = (char *) e + 1; + } + return 1; + } else if (g_ascii_isspace(e[0]) && e[1] && g_ascii_isalpha(e[1])) { + return 0; // spaces between value and unit are not allowed + } else { + /* Unitless */ + if (unit) { + *unit = SVGLength::NONE; + } + if (val) { + *val = v; + } + if (computed) { + *computed = v; + } + if (next) { + *next = (char *) e; + } + return 1; + } + } else if (e[1] && !g_ascii_isalnum(e[2])) { + /* TODO: Allow the number of px per inch to vary (document preferences, X server + * or whatever). E.g. don't fill in computed here, do it at the same time as + * percentage units are done. */ + unsigned int const uval = UVAL(e[0], e[1]); + switch (uval) { + case UVAL('p','x'): + if (unit) { + *unit = SVGLength::PX; + } + if (computed) { + *computed = v; + } + break; + case UVAL('p','t'): + if (unit) { + *unit = SVGLength::PT; + } + if (computed) { + *computed = Inkscape::Util::Quantity::convert(v, "pt", "px"); + } + break; + case UVAL('p','c'): + if (unit) { + *unit = SVGLength::PC; + } + if (computed) { + *computed = Inkscape::Util::Quantity::convert(v, "pc", "px"); + } + break; + case UVAL('m','m'): + if (unit) { + *unit = SVGLength::MM; + } + if (computed) { + *computed = Inkscape::Util::Quantity::convert(v, "mm", "px"); + } + break; + case UVAL('c','m'): + if (unit) { + *unit = SVGLength::CM; + } + if (computed) { + *computed = Inkscape::Util::Quantity::convert(v, "cm", "px"); + } + break; + case UVAL('i','n'): + if (unit) { + *unit = SVGLength::INCH; + } + if (computed) { + *computed = Inkscape::Util::Quantity::convert(v, "in", "px"); + } + break; + case UVAL('e','m'): + if (unit) { + *unit = SVGLength::EM; + } + break; + case UVAL('e','x'): + if (unit) { + *unit = SVGLength::EX; + } + break; + default: + /* Invalid */ + return 0; + break; + } + if (val) { + *val = v; + } + if (next) { + *next = (char *) e + 2; + } + return 1; + } + + /* Invalid */ + return 0; +} + +unsigned int sp_svg_length_read_ldd(gchar const *str, SVGLength::Unit *unit, double *value, double *computed) +{ + float a; + float b; + unsigned int r = sp_svg_length_read_lff(str, unit, &a, &b, nullptr); + if (r) { + if (value) { + *value = a; + } + if (computed) { + *computed = b; + } + } + return r; +} + +std::string SVGLength::write() const +{ + return sp_svg_length_write_with_units(*this); +} + +void SVGLength::set(SVGLength::Unit u, float v) +{ + _set = true; + unit = u; + Glib::ustring hack("px"); + switch( unit ) { + case NONE: + case PX: + case EM: + case EX: + case PERCENT: + break; + case PT: + hack = "pt"; + break; + case PC: + hack = "pc"; + break; + case MM: + hack = "pt"; + break; + case CM: + hack = "pt"; + break; + case INCH: + hack = "pt"; + break; + default: + break; + } + value = v; + computed = Inkscape::Util::Quantity::convert(v, hack, "px"); +} + +void SVGLength::set(SVGLength::Unit u, float v, float c) +{ + _set = true; + unit = u; + value = v; + computed = c; +} + +void SVGLength::unset(SVGLength::Unit u, float v, float c) +{ + _set = false; + unit = u; + value = v; + computed = c; +} + +void SVGLength::scale(double scale) +{ + value *= scale; + computed *= scale; +} + +void SVGLength::update(double em, double ex, double scale) +{ + if (unit == EM) { + computed = value * em; + } else if (unit == EX) { + computed = value * ex; + } else if (unit == PERCENT) { + computed = value * scale; + } +} + +double sp_svg_read_percentage(char const *str, double def) +{ + if (str == nullptr) { + return def; + } + + char *u; + double v = g_ascii_strtod(str, &u); + while (isspace(*u)) { + if (*u == '\0') { + return v; + } + u++; + } + if (*u == '%') { + v /= 100.0; + } + + return v; +} + +gchar const *sp_svg_length_get_css_units(SVGLength::Unit unit) +{ + switch (unit) { + case SVGLength::NONE: return ""; + case SVGLength::PX: return ""; + case SVGLength::PT: return "pt"; + case SVGLength::PC: return "pc"; + case SVGLength::MM: return "mm"; + case SVGLength::CM: return "cm"; + case SVGLength::INCH: return "in"; + case SVGLength::EM: return "em"; + case SVGLength::EX: return "ex"; + case SVGLength::PERCENT: return "%"; + } + return ""; +} + +/** + * N.B.\ This routine will sometimes return strings with `e' notation, so is unsuitable for CSS + * lengths (which don't allow scientific `e' notation). + */ +std::string sp_svg_length_write_with_units(SVGLength const &length) +{ + Inkscape::SVGOStringStream os; + if (length.unit == SVGLength::PERCENT) { + os << 100*length.value << sp_svg_length_get_css_units(length.unit); + } else { + os << length.value << sp_svg_length_get_css_units(length.unit); + } + return os.str(); +} + + +void SVGLength::readOrUnset(gchar const *str, Unit u, float v, float c) +{ + if (!read(str)) { + unset(u, v, c); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/svg/svg-length.h b/src/svg/svg-length.h new file mode 100644 index 0000000..17e04a2 --- /dev/null +++ b/src/svg/svg-length.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Carl Hetherington <inkscape@carlh.net> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_SVG_LENGTH_H +#define SEEN_SP_SVG_LENGTH_H + +#include <string> + +/** + * SVG length type + */ +class SVGLength { +public: + SVGLength(); + + enum Unit { + NONE, + PX, + PT, + PC, + MM, + CM, + INCH, + EM, + EX, + PERCENT, + LAST_UNIT = PERCENT + }; + + // The object's value is valid / exists in SVG. + bool _set; + + // The unit of value. + Unit unit; + + // The value of this SVGLength as found in the SVG. + float value; + + // The value in pixels (value * pixels/unit). + float computed; + + float operator=(float v) { + _set = true; + unit = NONE; + value = computed = v; + return v; + } + + bool read(char const *str); + void readOrUnset(char const *str, Unit u = NONE, float v = 0, float c = 0); + bool readAbsolute(char const *str); + std::string write() const; + // To set 'v' use '=' + void set(Unit u, float v); // Sets computed value based on u and v. + void set(Unit u, float v, float c); // Sets all three values. + void unset(Unit u = NONE, float v = 0, float c = 0); + void scale(double scale); // Scales length (value, computed), leaving unit alone. + void update(double em, double ex, double scale); // Updates computed value +}; + +#endif // SEEN_SP_SVG_LENGTH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/svg/svg-path-geom-test.h b/src/svg/svg-path-geom-test.h new file mode 100644 index 0000000..acfcc28 --- /dev/null +++ b/src/svg/svg-path-geom-test.h @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2015 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> +#include "2geom/coord.h" +#include "2geom/curves.h" +#include "2geom/pathvector.h" +#include "svg/svg.h" +#include "preferences.h" +#include "streq.h" +#include <stdio.h> +#include <string> +#include <vector> +#include <glib.h> + +class SvgPathGeomTest : public CxxTest::TestSuite +{ +private: + std::vector<std::string> rectanglesAbsoluteClosed; + std::vector<std::string> rectanglesRelativeClosed; + std::vector<std::string> rectanglesAbsoluteOpen; + std::vector<std::string> rectanglesRelativeOpen; + std::vector<std::string> rectanglesAbsoluteClosed2; + std::vector<std::string> rectanglesRelativeClosed2; + Geom::PathVector rectanglepvopen; + Geom::PathVector rectanglepvclosed; + Geom::PathVector rectanglepvclosed2; +public: + SvgPathGeomTest() { + // Lots of ways to define the same rectangle + rectanglesAbsoluteClosed.push_back("M 1,2 L 4,2 L 4,8 L 1,8 z"); + rectanglesAbsoluteClosed.push_back("M 1,2 4,2 4,8 1,8 z"); + rectanglesAbsoluteClosed.push_back("M 1,2 H 4 V 8 H 1 z"); + rectanglesRelativeClosed.push_back("m 1,2 l 3,0 l 0,6 l -3,0 z"); + rectanglesRelativeClosed.push_back("m 1,2 3,0 0,6 -3,0 z"); + rectanglesRelativeClosed.push_back("m 1,2 h 3 v 6 h -3 z"); + rectanglesAbsoluteOpen.push_back("M 1,2 L 4,2 L 4,8 L 1,8 L 1,2"); + rectanglesAbsoluteOpen.push_back("M 1,2 4,2 4,8 1,8 1,2"); + rectanglesAbsoluteOpen.push_back("M 1,2 H 4 V 8 H 1 V 2"); + rectanglesRelativeOpen.push_back("m 1,2 l 3,0 l 0,6 l -3,0 l 0,-6"); + rectanglesRelativeOpen.push_back("m 1,2 3,0 0,6 -3,0 0,-6"); + rectanglesRelativeOpen.push_back("m 1,2 h 3 v 6 h -3 v -6"); + rectanglesAbsoluteClosed2.push_back("M 1,2 L 4,2 L 4,8 L 1,8 L 1,2 z"); + rectanglesAbsoluteClosed2.push_back("M 1,2 4,2 4,8 1,8 1,2 z"); + rectanglesAbsoluteClosed2.push_back("M 1,2 H 4 V 8 H 1 V 2 z"); + rectanglesRelativeClosed2.push_back("m 1,2 l 3,0 l 0,6 l -3,0 l 0,-6 z"); + rectanglesRelativeClosed2.push_back("m 1,2 3,0 0,6 -3,0 0,-6 z"); + rectanglesRelativeClosed2.push_back("m 1,2 h 3 v 6 h -3 v -6 z"); + rectanglepvopen.push_back(Geom::Path(Geom::Point(1,2))); + rectanglepvopen.back().append(Geom::LineSegment(Geom::Point(1,2),Geom::Point(4,2))); + rectanglepvopen.back().append(Geom::LineSegment(Geom::Point(4,2),Geom::Point(4,8))); + rectanglepvopen.back().append(Geom::LineSegment(Geom::Point(4,8),Geom::Point(1,8))); + rectanglepvopen.back().append(Geom::LineSegment(Geom::Point(1,8),Geom::Point(1,2))); + rectanglepvclosed.push_back(Geom::Path(Geom::Point(1,2))); + rectanglepvclosed.back().append(Geom::LineSegment(Geom::Point(1,2),Geom::Point(4,2))); + rectanglepvclosed.back().append(Geom::LineSegment(Geom::Point(4,2),Geom::Point(4,8))); + rectanglepvclosed.back().append(Geom::LineSegment(Geom::Point(4,8),Geom::Point(1,8))); + rectanglepvclosed.back().close(); + rectanglepvclosed2.push_back(Geom::Path(Geom::Point(1,2))); + rectanglepvclosed2.back().append(Geom::LineSegment(Geom::Point(1,2),Geom::Point(4,2))); + rectanglepvclosed2.back().append(Geom::LineSegment(Geom::Point(4,2),Geom::Point(4,8))); + rectanglepvclosed2.back().append(Geom::LineSegment(Geom::Point(4,8),Geom::Point(1,8))); + rectanglepvclosed2.back().append(Geom::LineSegment(Geom::Point(1,8),Geom::Point(1,2))); + rectanglepvclosed2.back().close(); + // TODO: Also test some (smooth) cubic/quadratic beziers and elliptical arcs + // TODO: Should we make it mandatory that h/v in the path data results in a H/VLineSegment? + // If so, the tests should be modified to reflect this. + } + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static SvgPathGeomTest *createSuite() { return new SvgPathGeomTest(); } + static void destroySuite( SvgPathGeomTest *suite ) { delete suite; } + + void testReadRectanglesAbsoluteClosed() + { + for(size_t i=0; i<rectanglesAbsoluteClosed.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesAbsoluteClosed[i].c_str()); + TSM_ASSERT(rectanglesAbsoluteClosed[i].c_str(), bpathEqual(pv,rectanglepvclosed)); + } + } + + void testReadRectanglesRelativeClosed() + { + for(size_t i=0; i<rectanglesRelativeClosed.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesRelativeClosed[i].c_str()); + TSM_ASSERT(rectanglesRelativeClosed[i].c_str(), bpathEqual(pv,rectanglepvclosed)); + } + } + + void testReadRectanglesAbsoluteOpen() + { + for(size_t i=0; i<rectanglesAbsoluteOpen.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesAbsoluteOpen[i].c_str()); + TSM_ASSERT(rectanglesAbsoluteOpen[i].c_str(), bpathEqual(pv,rectanglepvopen)); + } + } + + void testReadRectanglesRelativeOpen() + { + for(size_t i=0; i<rectanglesRelativeOpen.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesRelativeOpen[i].c_str()); + TSM_ASSERT(rectanglesRelativeOpen[i].c_str(), bpathEqual(pv,rectanglepvopen)); + } + } + + void testReadRectanglesAbsoluteClosed2() + { + for(size_t i=0; i<rectanglesAbsoluteClosed2.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesAbsoluteClosed2[i].c_str()); + TSM_ASSERT(rectanglesAbsoluteClosed2[i].c_str(), bpathEqual(pv,rectanglepvclosed2)); + } + } + + void testReadRectanglesRelativeClosed2() + { + for(size_t i=0; i<rectanglesRelativeClosed2.size(); i++) { + Geom::PathVector pv = sp_svg_read_pathv(rectanglesRelativeClosed2[i].c_str()); + TSM_ASSERT(rectanglesRelativeClosed2[i].c_str(), bpathEqual(pv,rectanglepvclosed2)); + } + } + + void testReadConcatenatedPaths() + { + // Note that finalPoint doesn't actually return the final point of the path, just the last given point... (but since this might be intentional and we're not testing lib2geom here, we just specify the final point explicitly + Geom::PathVector pv_good; + pv_good.push_back(rectanglepvclosed.back()); + pv_good.push_back(rectanglepvopen.back() * Geom::Translate(1,2)/* * Geom::Translate(pv_good[0].finalPoint())*/); + pv_good.push_back(rectanglepvclosed.back() * Geom::Translate(2,4)/* *Geom::Translate(pv_good[1].finalPoint())*/); + pv_good.push_back(rectanglepvopen.back()); + pv_good[0].close(); + pv_good[1].close(false); + pv_good[2].close(); + pv_good[3].close(false); + std::string path_str = rectanglesAbsoluteClosed[0] + rectanglesRelativeOpen[0] + rectanglesRelativeClosed[0] + rectanglesAbsoluteOpen[0]; + Geom::PathVector pv = sp_svg_read_pathv(path_str.c_str()); + TS_ASSERT(bpathEqual(pv,pv_good)); + } + + void testReadZeroLengthSubpaths() { + // Per the SVG 1.1 specification (section F5) zero-length subpaths are relevant + Geom::PathVector pv_good; + pv_good.push_back(Geom::Path(Geom::Point(0,0))); + pv_good.push_back(Geom::Path(Geom::Point(1,1))); + pv_good.back().append(Geom::LineSegment(Geom::Point(1,1),Geom::Point(2,2))); + pv_good.push_back(Geom::Path(Geom::Point(3,3))); + pv_good.back().close(); + pv_good.push_back(Geom::Path(Geom::Point(4,4))); + pv_good.back().append(Geom::LineSegment(Geom::Point(4,4),Geom::Point(5,5))); + pv_good.back().close(); + pv_good.push_back(Geom::Path(Geom::Point(6,6))); + { // Test absolute version + char const * path_str = "M 0,0 M 1,1 L 2,2 M 3,3 z M 4,4 L 5,5 z M 6,6"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + { // Test relative version + char const * path_str = "m 0,0 m 1,1 l 1,1 m 1,1 z m 1,1 l 1,1 z m 2,2"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + } + + void testReadImplicitMoveto() { + TS_WARN("Currently lib2geom (/libnr) has no way of specifying the difference between 'M 0,0 ... z M 0,0 L 1,0' and 'M 0,0 ... z L 1,0', the SVG specification does state that these should be handled differently with respect to markers however, see the description of the 'orient' attribute of the 'marker' element."); + Geom::PathVector pv_good; + pv_good.push_back(Geom::Path(Geom::Point(1,1))); + pv_good.back().append(Geom::LineSegment(Geom::Point(1,1),Geom::Point(2,2))); + pv_good.back().close(); + pv_good.push_back(Geom::Path(Geom::Point(1,1))); + pv_good.back().append(Geom::LineSegment(Geom::Point(1,1),Geom::Point(3,3))); + pv_good.back().close(); + { // Test absolute version + char const * path_str = "M 1,1 L 2,2 z L 3,3 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + { // Test relative version + char const * path_str = "M 1,1 l 1,1 z l 2,2 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + } + + void testReadFloatingPoint() { + Geom::PathVector pv_good1; + pv_good1.push_back(Geom::Path(Geom::Point(.01,.02))); + pv_good1.back().append(Geom::LineSegment(Geom::Point(.01,.02),Geom::Point(.04,.02))); + pv_good1.back().append(Geom::LineSegment(Geom::Point(.04,.02),Geom::Point(1.5,1.6))); + pv_good1.back().append(Geom::LineSegment(Geom::Point(1.5,1.6),Geom::Point(.01,.08))); + pv_good1.back().append(Geom::LineSegment(Geom::Point(.01,.08),Geom::Point(.01,.02))); + pv_good1.back().close(); + { // Test decimals + char const * path_str = "M .01,.02 L.04.02 L1.5,1.6L0.01,0.08 .01.02 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good1)); + } + Geom::PathVector pv_good2; + pv_good2.push_back(Geom::Path(Geom::Point(.01,.02))); + pv_good2.back().append(Geom::LineSegment(Geom::Point(.01,.02),Geom::Point(.04,.02))); + pv_good2.back().append(Geom::LineSegment(Geom::Point(.04,.02),Geom::Point(1.5,1.6))); + pv_good2.back().append(Geom::LineSegment(Geom::Point(1.5,1.6),Geom::Point(.01,.08))); + pv_good2.back().close(); + { // Test exponent + char const * path_str = "M 1e-2,.2e-1 L 0.004e1,0.0002e+2 L0150E-2,1.6e0L1.0e-2,80e-3 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good2)); + } + } + + void testReadImplicitSeparation() { + // Coordinates need not be separated by whitespace if they can still be read unambiguously + Geom::PathVector pv_good; + pv_good.push_back(Geom::Path(Geom::Point(.1,.2))); + pv_good.back().append(Geom::LineSegment(Geom::Point(.1,.2),Geom::Point(.4,.2))); + pv_good.back().append(Geom::LineSegment(Geom::Point(.4,.2),Geom::Point(.4,.8))); + pv_good.back().append(Geom::LineSegment(Geom::Point(.4,.8),Geom::Point(.1,.8))); + pv_good.back().close(); + { // Test absolute + char const * path_str = "M .1.2+0.4.2e0.4e0+8e-1.1.8 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + { // Test relative + char const * path_str = "m .1.2+0.3.0e0.0e0+6e-1-.3.0 z"; + Geom::PathVector pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,pv_good)); + } + } + + void testReadErrorMisplacedCharacter() { + char const * path_str; + Geom::PathVector pv; + // Comma in the wrong place (commas may only appear between parameters) + path_str = "M 1,2 4,2 4,8 1,8 z , m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Comma in the wrong place (commas may only appear between parameters) + path_str = "M 1,2 4,2 4,8 1,8 z m,13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Period in the wrong place (no numbers after a 'z') + path_str = "M 1,2 4,2 4,8 1,8 z . m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Sign in the wrong place (no numbers after a 'z') + path_str = "M 1,2 4,2 4,8 1,8 z + - m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Digit in the wrong place (no numbers after a 'z') + path_str = "M 1,2 4,2 4,8 1,8 z 9809 m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Digit in the wrong place (no numbers after a 'z') + path_str = "M 1,2 4,2 4,8 1,8 z 9809 876 m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + } + + void testReadErrorUnrecognizedCharacter() { + char const * path_str; + Geom::PathVector pv; + // Unrecognized character + path_str = "M 1,2 4,2 4,8 1,8 z&m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Unrecognized character + path_str = "M 1,2 4,2 4,8 1,8 z m &13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + } + + void testReadErrorTypo() { + char const * path_str; + Geom::PathVector pv; + // Typo + path_str = "M 1,2 4,2 4,8 1,8 z j 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + + // Typo + path_str = "M 1,2 4,2 4,8 1,8 L 1,2 x m 13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvopen)); + } + + void testReadErrorIllformedNumbers() { + char const * path_str; + Geom::PathVector pv; + // Double exponent + path_str = "M 1,2 4,2 4,8 1,8 z m 13e4e5,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Double sign + path_str = "M 1,2 4,2 4,8 1,8 z m +-13,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Double sign + path_str = "M 1,2 4,2 4,8 1,8 z m 13e+-12,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // No digit + path_str = "M 1,2 4,2 4,8 1,8 z m .e12,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // No digit + path_str = "M 1,2 4,2 4,8 1,8 z m .,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // No digit + path_str = "M 1,2 4,2 4,8 1,8 z m +,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // No digit + path_str = "M 1,2 4,2 4,8 1,8 z m +.e+,15"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + } + + void testReadErrorJunk() { + char const * path_str; + Geom::PathVector pv; + // Junk + path_str = "M 1,2 4,2 4,8 1,8 z j 357 hkjh.,34e34 90ih6kj4 h5k6vlh4N.,6,45wikuyi3yere..3487 m 13,23"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + } + + void testReadErrorStopReading() { + char const * path_str; + Geom::PathVector pv; + // Unrecognized parameter + path_str = "M 1,2 4,2 4,8 1,8 z m #$%,23,34"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Invalid parameter + path_str = "M 1,2 4,2 4,8 1,8 z m #$%,23,34"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + // Illformed parameter + path_str = "M 1,2 4,2 4,8 1,8 z m +-12,23,34"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvclosed)); + + // "Third" parameter + path_str = "M 1,2 4,2 4,8 1,8 1,2,3 M 12,23"; + pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(path_str, bpathEqual(pv,rectanglepvopen)); + } + + void testRoundTrip() { + // This is the easiest way to (also) test writing path data, as a path can be written in more than one way. + Geom::PathVector pv; + Geom::PathVector new_pv; + std::string org_path_str; + char * path_str; + // Rectangle (closed) + org_path_str = rectanglesAbsoluteClosed[0]; + pv = sp_svg_read_pathv(org_path_str.c_str()); + path_str = sp_svg_write_path(pv); + new_pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(org_path_str.c_str(), bpathEqual(pv,new_pv)); + g_free(path_str); + // Rectangle (open) + org_path_str = rectanglesAbsoluteOpen[0]; + pv = sp_svg_read_pathv(org_path_str.c_str()); + path_str = sp_svg_write_path(pv); + new_pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(org_path_str.c_str(), bpathEqual(pv,new_pv)); + g_free(path_str); + // Concatenated rectangles + org_path_str = rectanglesAbsoluteClosed[0] + rectanglesRelativeOpen[0] + rectanglesRelativeClosed[0] + rectanglesAbsoluteOpen[0]; + pv = sp_svg_read_pathv(org_path_str.c_str()); + path_str = sp_svg_write_path(pv); + new_pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(org_path_str.c_str(), bpathEqual(pv,new_pv)); + g_free(path_str); + // Zero-length subpaths + org_path_str = "M 0,0 M 1,1 L 2,2 M 3,3 z M 4,4 L 5,5 z M 6,6"; + pv = sp_svg_read_pathv(org_path_str.c_str()); + path_str = sp_svg_write_path(pv); + new_pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(org_path_str.c_str(), bpathEqual(pv,new_pv)); + g_free(path_str); + // Floating-point + org_path_str = "M .01,.02 L 0.04,0.02 L.04,.08L0.01,0.08 z""M 1e-2,.2e-1 L 0.004e1,0.0002e+2 L04E-2,.08e0L1.0e-2,80e-3 z"; + pv = sp_svg_read_pathv(org_path_str.c_str()); + path_str = sp_svg_write_path(pv); + new_pv = sp_svg_read_pathv(path_str); + TSM_ASSERT(org_path_str.c_str(), bpathEqual(pv, new_pv, 1e-17)); + g_free(path_str); + } + + void testMinexpPrecision() { + Geom::PathVector pv; + char * path_str; + // Default values + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/svgoutput/allowrelativecoordinates", true); + prefs->setBool("/options/svgoutput/forcerepeatcommands", false); + prefs->setInt("/options/svgoutput/numericprecision", 8); + prefs->setInt("/options/svgoutput/minimumexponent", -8); + pv = sp_svg_read_pathv("M 123456781,1.23456781e-8 L 123456782,1.23456782e-8 L 123456785,1.23456785e-8 L 10123456400,1.23456785e-8 L 123456789,1.23456789e-8 L 123456789,101.234564e-8 L 123456789,1.23456789e-8"); + path_str = sp_svg_write_path(pv); + TS_ASSERT_RELATION( streq_rel , "m 123456780,1.2345678e-8 0,0 10,1e-15 9999999210,0 -9999999210,0 0,9.99999921e-7 0,-9.99999921e-7" , path_str ); + g_free(path_str); + } + +private: + bool bpathEqual(Geom::PathVector const &a, Geom::PathVector const &b, double eps = 1e-16) { + if (a.size() != b.size()) { + char temp[100]; + sprintf(temp, "PathVectors not the same size: %u != %u", static_cast<unsigned int>(a.size()),static_cast<unsigned int>( b.size())); + TS_FAIL(temp); + return false; + } + for(size_t i=0; i<a.size(); i++) { + Geom::Path const &pa = a[i]; + Geom::Path const &pb = b[i]; + if (pa.closed() && !pb.closed()) { + char temp[100]; + sprintf(temp, "Left subpath is closed, right subpath is open. Subpath: %u", static_cast<unsigned int>(i)); + TS_FAIL(temp); + return false; + } + if (!pa.closed() && pb.closed()) { + char temp[100]; + sprintf(temp, "Right subpath is closed, left subpath is open. Subpath: %u", static_cast<unsigned int>(i)); + TS_FAIL(temp); + return false; + } + if (pa.size() != pb.size()) { + char temp[100]; + sprintf(temp, "Not the same number of segments: %u != %u, subpath: %u", static_cast<unsigned int>(pa.size()), static_cast<unsigned int>(pb.size()), static_cast<unsigned int>(i)); + TS_FAIL(temp); + return false; + } + for(size_t j=0; j<pa.size(); j++) { + Geom::Curve const* ca = &pa[j]; + Geom::Curve const* cb = &pb[j]; + if (typeid(*ca) == typeid(*cb)) + { + if(Geom::LineSegment const *la = dynamic_cast<Geom::LineSegment const*>(ca)) + { + Geom::LineSegment const *lb = dynamic_cast<Geom::LineSegment const*>(cb); + if (!Geom::are_near((*la)[0],(*lb)[0], eps)) { + char temp[200]; + sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[0][Geom::X], (*la)[0][Geom::Y], (*lb)[0][Geom::X], (*lb)[0][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + if (!Geom::are_near((*la)[1],(*lb)[1], eps)) { + char temp[200]; + sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[1][Geom::X], (*la)[1][Geom::Y], (*lb)[1][Geom::X], (*lb)[1][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + } + else if(Geom::CubicBezier const *la = dynamic_cast<Geom::CubicBezier const*>(ca)) + { + Geom::CubicBezier const *lb = dynamic_cast<Geom::CubicBezier const*>(cb); + if (!Geom::are_near((*la)[0],(*lb)[0], eps)) { + char temp[200]; + sprintf(temp, "Different start of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[0][Geom::X], (*la)[0][Geom::Y], (*lb)[0][Geom::X], (*lb)[0][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + if (!Geom::are_near((*la)[1],(*lb)[1], eps)) { + char temp[200]; + sprintf(temp, "Different 1st control point: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[1][Geom::X], (*la)[1][Geom::Y], (*lb)[1][Geom::X], (*lb)[1][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + if (!Geom::are_near((*la)[2],(*lb)[2], eps)) { + char temp[200]; + sprintf(temp, "Different 2nd control point: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[2][Geom::X], (*la)[2][Geom::Y], (*lb)[2][Geom::X], (*lb)[2][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + if (!Geom::are_near((*la)[3],(*lb)[3], eps)) { + char temp[200]; + sprintf(temp, "Different end of segment: (%g,%g) != (%g,%g), subpath: %u, segment: %u", (*la)[3][Geom::X], (*la)[3][Geom::Y], (*lb)[3][Geom::X], (*lb)[3][Geom::Y], static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + } + else + { + char temp[200]; + sprintf(temp, "Unknown curve type: %s, subpath: %u, segment: %u", typeid(*ca).name(), static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + } + } + else // not same type + { + char temp[200]; + sprintf(temp, "Different curve types: %s != %s, subpath: %u, segment: %u", typeid(*ca).name(), typeid(*cb).name(), static_cast<unsigned int>(i), static_cast<unsigned int>(j)); + TS_FAIL(temp); + return false; + } + } + } + return true; + } +}; + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/svg-path.cpp b/src/svg/svg-path.cpp new file mode 100644 index 0000000..1414d69 --- /dev/null +++ b/src/svg/svg-path.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * svg-path.cpp: Parse SVG path element data into bezier path. + * Authors: + * Johan Engelen + * (old nartbpath code that has been deleted: Raph Levien <raph@artofcode.com>) + * (old nartbpath code that has been deleted: Lauris Kaplinski <lauris@ximian.com>) + * + * Copyright (C) 2000 Eazel, Inc. + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2008 Johan Engelen + * + * Copyright (C) 2000-2008 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glib.h> // g_assert() + +#include <2geom/pathvector.h> +#include <2geom/curves.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/path-sink.h> +#include <2geom/svg-path-parser.h> + +#include "svg/svg.h" +#include "svg/path-string.h" + +/* + * Parses the path in str. When an error is found in the pathstring, this method + * returns a truncated path up to where the error was found in the pathstring. + * Returns an empty PathVector when str==NULL + */ +Geom::PathVector sp_svg_read_pathv(char const * str) +{ + Geom::PathVector pathv; + if (!str) + return pathv; // return empty pathvector when str == NULL + + Geom::PathBuilder builder(pathv); + Geom::SVGPathParser parser(builder); + parser.setZSnapThreshold(Geom::EPSILON); + + try { + parser.parse(str); + } + catch (Geom::SVGPathParseError &e) { + builder.flush(); + // This warning is extremely annoying when testing + //g_warning("Malformed SVG path, truncated path up to where error was found.\n Input path=\"%s\"\n Parsed path=\"%s\"", str, sp_svg_write_path(pathv)); + } + + return pathv; +} + +static void sp_svg_write_curve(Inkscape::SVG::PathString & str, Geom::Curve const * c) { + // TODO: this code needs to removed and replaced by appropriate path sink + if(Geom::LineSegment const *line_segment = dynamic_cast<Geom::LineSegment const *>(c)) { + // don't serialize stitch segments + if (!dynamic_cast<Geom::Path::StitchSegment const *>(c)) { + if (line_segment->initialPoint()[Geom::X] == line_segment->finalPoint()[Geom::X]) { + str.verticalLineTo( line_segment->finalPoint()[Geom::Y] ); + } else if (line_segment->initialPoint()[Geom::Y] == line_segment->finalPoint()[Geom::Y]) { + str.horizontalLineTo( line_segment->finalPoint()[Geom::X] ); + } else { + str.lineTo( (*line_segment)[1][0], (*line_segment)[1][1] ); + } + } + } + else if(Geom::QuadraticBezier const *quadratic_bezier = dynamic_cast<Geom::QuadraticBezier const *>(c)) { + str.quadTo( (*quadratic_bezier)[1][0], (*quadratic_bezier)[1][1], + (*quadratic_bezier)[2][0], (*quadratic_bezier)[2][1] ); + } + else if(Geom::CubicBezier const *cubic_bezier = dynamic_cast<Geom::CubicBezier const *>(c)) { + str.curveTo( (*cubic_bezier)[1][0], (*cubic_bezier)[1][1], + (*cubic_bezier)[2][0], (*cubic_bezier)[2][1], + (*cubic_bezier)[3][0], (*cubic_bezier)[3][1] ); + } + else if(Geom::EllipticalArc const *elliptical_arc = dynamic_cast<Geom::EllipticalArc const *>(c)) { + str.arcTo( elliptical_arc->ray(Geom::X), elliptical_arc->ray(Geom::Y), + Geom::deg_from_rad(elliptical_arc->rotationAngle()), + elliptical_arc->largeArc(), elliptical_arc->sweep(), + elliptical_arc->finalPoint() ); + } else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c->toSBasis(), 0.1); + + //recurse to convert the new path resulting from the sbasis to svgd + for(const auto & iter : sbasis_path) { + sp_svg_write_curve(str, &iter); + } + } +} + +static void sp_svg_write_path(Inkscape::SVG::PathString & str, Geom::Path const & p) { + str.moveTo( p.initialPoint()[0], p.initialPoint()[1] ); + + for(Geom::Path::const_iterator cit = p.begin(); cit != p.end_open(); ++cit) { + sp_svg_write_curve(str, &(*cit)); + } + + if (p.closed()) { + str.closePath(); + } +} + +gchar * sp_svg_write_path(Geom::PathVector const &p) { + Inkscape::SVG::PathString str; + + for(const auto & pit : p) { + sp_svg_write_path(str, pit); + } + + return g_strdup(str.c_str()); +} + +gchar * sp_svg_write_path(Geom::Path const &p) { + Inkscape::SVG::PathString str; + + sp_svg_write_path(str, p); + + return g_strdup(str.c_str()); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/svg/svg.h b/src/svg/svg.h new file mode 100644 index 0000000..a65bdf2 --- /dev/null +++ b/src/svg/svg.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_SVG_H +#define SEEN_SP_SVG_H + +/* + * SVG data parser + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include <cstring> +#include <string> + +#include "svg/svg-length.h" +#include <2geom/forward.h> + +/* Generic */ + +/* + * These are very-very simple: + * - they accept everything libc strtod accepts + * - no valid end character checking + * Return FALSE and let val untouched on error + */ + +unsigned int sp_svg_number_read_f( const char *str, float *val ); +unsigned int sp_svg_number_read_d( const char *str, double *val ); + +/* + * No buffer overflow checking is done, so better wrap them if needed + */ +unsigned int sp_svg_number_write_de( char *buf, int bufLen, double val, unsigned int tprec, int min_exp ); + +/* Length */ + +/* + * Parse number with optional unit specifier: + * - for px, pt, pc, mm, cm, computed is final value according to SVG spec + * - for em, ex, and % computed is left untouched + * - % is divided by 100 (i.e. 100% is 1.0) + * !isalnum check is done at the end + * Any return value pointer can be NULL + */ + +unsigned int sp_svg_length_read_computed_absolute( const char *str, float *length ); +std::vector<SVGLength> sp_svg_length_list_read( const char *str ); +unsigned int sp_svg_length_read_ldd( const char *str, SVGLength::Unit *unit, double *value, double *computed ); + +std::string sp_svg_length_write_with_units(SVGLength const &length); + +bool sp_svg_transform_read(char const *str, Geom::Affine *transform); + +char *sp_svg_transform_write(Geom::Affine const &transform); +char *sp_svg_transform_write(Geom::Affine const *transform); + +double sp_svg_read_percentage( const char * str, double def ); + +/* NB! As paths can be long, we use here dynamic string */ + +Geom::PathVector sp_svg_read_pathv( char const * str ); +char * sp_svg_write_path( Geom::PathVector const &p ); +char * sp_svg_write_path( Geom::Path const &p ); + +#endif // SEEN_SP_SVG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/test-stubs.cpp b/src/svg/test-stubs.cpp new file mode 100644 index 0000000..b511c7e --- /dev/null +++ b/src/svg/test-stubs.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Stub out functions when building tests + * + * Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "svg/test-stubs.h" +#include <map> +#include <string> + +std::map<std::string,long long int> int_prefs; + +void +prefs_set_int_attribute(gchar const *path, gchar const *attr, long long int val) +{ + int_prefs[std::string(path) + '/' + std::string(attr)] = val; +} + +long long int +prefs_get_int_attribute(gchar const *path, gchar const *attr, long long int def) +{ + std::map<std::string,long long int>::const_iterator it=int_prefs.find(std::string(path) + '/' + std::string(attr)); + long long int ret = it==int_prefs.end() ? def : it->second; + return ret; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/svg/test-stubs.h b/src/svg/test-stubs.h new file mode 100644 index 0000000..0275f61 --- /dev/null +++ b/src/svg/test-stubs.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Stub out functions when building tests + * + * Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_TEST_STUBS_H +#define SEEN_TEST_STUBS_H + +#include <glib.h> + +long long int prefs_get_int_attribute(gchar const *path, gchar const *attr, long long int def); + +#endif /* !SEEN_TEST_STUBS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/syseq.h b/src/syseq.h new file mode 100644 index 0000000..3bd09f1 --- /dev/null +++ b/src/syseq.h @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SYSEQ_H +#define SEEN_SYSEQ_H + +/* + * Auxiliary routines to solve systems of linear equations in several variants and sizes. + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> +#include <iomanip> +#include <vector> +#include <cmath> + +namespace SysEq { + +enum SolutionKind { + unique = 0, + ambiguous, + no_solution, + solution_exists // FIXME: remove this; does not yield enough information +}; + +inline void explain(SolutionKind sol) { + switch (sol) { + case SysEq::unique: + std::cout << "unique" << std::endl; + break; + case SysEq::ambiguous: + std::cout << "ambiguous" << std::endl; + break; + case SysEq::no_solution: + std::cout << "no solution" << std::endl; + break; + case SysEq::solution_exists: + std::cout << "solution exists" << std::endl; + break; + } +} + +inline double +determinant3x3 (double A[3][3]) { + return (A[0][0]*A[1][1]*A[2][2] + + A[0][1]*A[1][2]*A[2][0] + + A[0][2]*A[1][0]*A[2][1] - + A[0][0]*A[1][2]*A[2][1] - + A[0][1]*A[1][0]*A[2][2] - + A[0][2]*A[1][1]*A[2][0]); +} + +/* Determinant of the 3x3 matrix having a, b, and c as columns */ +inline double +determinant3v (const double a[3], const double b[3], const double c[3]) { + return (a[0]*b[1]*c[2] + + a[1]*b[2]*c[0] + + a[2]*b[0]*c[1] - + a[0]*b[2]*c[1] - + a[1]*b[0]*c[2] - + a[2]*b[1]*c[0]); +} + +/* Copy the elements of A into B */ +template <int S, int T> +inline void copy_mat(double A[S][T], double B[S][T]) { + for (int i = 0; i < S; ++i) { + for (int j = 0; j < T; ++j) { + B[i][j] = A[i][j]; + } + } +} + +template <int S, int T> +inline void print_mat (const double A[S][T]) { + std::cout.setf(std::ios::left, std::ios::internal); + for (int i = 0; i < S; ++i) { + for (int j = 0; j < T; ++j) { + printf ("%8.2f ", A[i][j]); + } + std::cout << std::endl;; + } +} + +/* Multiplication of two matrices */ +template <int S, int U, int T> +inline void multiply(double A[S][U], double B[U][T], double res[S][T]) { + for (int i = 0; i < S; ++i) { + for (int j = 0; j < T; ++j) { + double sum = 0; + for (int k = 0; k < U; ++k) { + sum += A[i][k] * B[k][j]; + } + res[i][j] = sum; + } + } +} + +/* + * Multiplication of a matrix with a vector (for convenience, because with the previous + * multiplication function we would always have to write v[i][0] for elements of the vector. + */ +template <int S, int T> +inline void multiply(double A[S][T], double v[T], double res[S]) { + for (int i = 0; i < S; ++i) { + double sum = 0; + for (int k = 0; k < T; ++k) { + sum += A[i][k] * v[k]; + } + res[i] = sum; + } +} + +// Remark: Since we are using templates, we cannot separate declarations from definitions (which would +// result in linker errors but have to include the definitions here for the following functions. +// FIXME: Maybe we should rework all this by using vector<vector<double> > structures for matrices +// instead of double[S][T]. This would allow us to avoid templates. Would the performance degrade? + +/* + * Find the element of maximal absolute value in row i that + * does not lie in one of the columns given in avoid_cols. + */ +template <int S, int T> +static int find_pivot(const double A[S][T], unsigned int i, std::vector<int> const &avoid_cols) { + if (i >= S) { + return -1; + } + int pos = -1; + double max = 0; + for (int j = 0; j < T; ++j) { + if (std::find(avoid_cols.begin(), avoid_cols.end(), j) != avoid_cols.end()) { + continue; // skip "forbidden" columns + } + if (fabs(A[i][j]) > max) { + pos = j; + max = fabs(A[i][j]); + } + } + return pos; +} + +/* + * Performs a single 'exchange step' in the Gauss-Jordan algorithm (i.e., swapping variables in the + * two vectors). + */ +template <int S, int T> +static void gauss_jordan_step (double A[S][T], int row, int col) { + double piv = A[row][col]; // pivot element + /* adapt the entries of the matrix, first outside the pivot row/column */ + for (int k = 0; k < S; ++k) { + if (k == row) continue; + for (int l = 0; l < T; ++l) { + if (l == col) continue; + A[k][l] -= A[k][col] * A[row][l] / piv; + } + } + /* now adapt the pivot column ... */ + for (int k = 0; k < S; ++k) { + if (k == row) continue; + A[k][col] /= piv; + } + /* and the pivot row */ + for (int l = 0; l < T; ++l) { + if (l == col) continue; + A[row][l] /= -piv; + } + /* finally, set the element at the pivot position itself */ + A[row][col] = 1/piv; +} + +/* + * Perform Gauss-Jordan elimination on the matrix A, optionally avoiding a given column during pivot search + */ +template <int S, int T> +static std::vector<int> gauss_jordan (double A[S][T], int avoid_col = -1) { + std::vector<int> cols_used; + if (avoid_col != -1) { + cols_used.push_back (avoid_col); + } + for (int i = 0; i < S; ++i) { + /* for each row find a pivot element of maximal absolute value, skipping the columns that were used before */ + int col = find_pivot<S,T>(A, i, cols_used); + cols_used.push_back(col); + if (col == -1) { + // no non-zero elements in the row + return cols_used; + } + + /* if pivot search was successful we can perform a Gauss-Jordan step */ + gauss_jordan_step<S,T> (A, i, col); + } + if (avoid_col != -1) { + // since the columns that were used will be needed later on, we need to clean up the column vector + cols_used.erase(cols_used.begin()); + } + return cols_used; +} + +/* compute the modified value that x[index] needs to assume so that in the end we have x[index]/x[T-1] = val */ +template <int S, int T> +static double projectify (std::vector<int> const &cols, const double B[S][T], const double x[T], + const int index, const double val) { + double val_proj = 0.0; + if (index != -1) { + int c = -1; + for (int i = 0; i < S; ++i) { + if (cols[i] == T-1) { + c = i; + break; + } + } + if (c == -1) { + std::cout << "Something is wrong. Rethink!!" << std::endl; + return SysEq::no_solution; + } + + double sp = 0; + for (int j = 0; j < T; ++j) { + if (j == index) continue; + sp += B[c][j] * x[j]; + } + double mu = 1 - val * B[c][index]; + if (fabs(mu) < 1E-6) { + std::cout << "No solution since adapted value is too close to zero" << std::endl; + return SysEq::no_solution; + } + val_proj = sp*val/mu; + } else { + val_proj = val; // FIXME: Is this correct? + } + return val_proj; +} + +/** + * Solve the linear system of equations \a A * \a x = \a v where we additionally stipulate + * \a x[\a index] = \a val if \a index is not -1. The system is solved using Gauss-Jordan + * elimination so that we can gracefully handle the case that zero or infinitely many + * solutions exist. + * + * Since our application will be to finding preimages of projective mappings, we provide + * an additional argument \a proj. If this is true, we find a solution of + * \a x[\a index]/\a x[\T - 1] = \a val instead (i.e., we want the corresponding coordinate + * of the _affine image_ of the point with homogeneous coordinate vector \a x to be equal + * to \a val. + * + * Remark: We don't need this but it would be relatively simple to let the calling function + * prescripe the value of _multiple_ components of the solution vector instead of only a single one. + */ +template <int S, int T> SolutionKind gaussjord_solve (double A[S][T], double x[T], double v[S], + int index = -1, double val = 0.0, bool proj = false) { + double B[S][T]; + //copy_mat<S,T>(A,B); + SysEq::copy_mat<S,T>(A,B); + std::vector<int> cols = gauss_jordan<S,T>(B, index); + if (std::find(cols.begin(), cols.end(), -1) != cols.end()) { + // pivot search failed for some row so the system is not solvable + return SysEq::no_solution; + } + + /* the vector x is filled with the coefficients of the desired solution vector at appropriate places; + * the other components are set to zero, and we additionally set x[index] = val if applicable + */ + std::vector<int>::iterator k; + for (int j = 0; j < S; ++j) { + x[cols[j]] = v[j]; + } + for (int j = 0; j < T; ++j) { + k = std::find(cols.begin(), cols.end(), j); + if (k == cols.end()) { + x[j] = 0; + } + } + + // we need to adapt the value if we are in the "projective case" (see above) + double val_new = (proj ? projectify<S,T>(cols, B, x, index, val) : val); + + if (index >= 0 && index < T) { + // we want the specified coefficient of the solution vector to have a given value + x[index] = val_new; + } + + /* the final solution vector is now obtained as the product B*x, where B is the matrix + * obtained by Gauss-Jordan manipulation of A; we use w as an auxiliary vector and + * afterwards copy the result back to x + */ + double w[S]; + SysEq::multiply<S,T>(B,x,w); // initializes w + for (int j = 0; j < S; ++j) { + x[cols[j]] = w[j]; + } + + if (S + (index == -1 ? 0 : 1) == T) { + return SysEq::unique; + } else { + return SysEq::ambiguous; + } +} + +} // namespace SysEq + +#endif /* __SYSEQ_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/text-chemistry-impl.h b/src/text-chemistry-impl.h new file mode 100644 index 0000000..01bec1e --- /dev/null +++ b/src/text-chemistry-impl.h @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TEXT_CHEMISTRY_IMPL_H +#define SEEN_TEXT_CHEMISTRY_IMPL_H +/* + * Text commands template implementation + * + * Authors: + * Iskren Chernev <iskren.chernev@gmail.com> + * + * Copyright (C) 2004 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include "style.h" +#include "xml/node.h" + +Glib::ustring text_relink_shapes_str(gchar const *prop, + std::map<Glib::ustring, Glib::ustring> const &old_to_new); + +inline Inkscape::XML::Node *text_obj_or_node_to_node(SPObject *obj) { + return obj->getRepr(); +} + +inline Inkscape::XML::Node *text_obj_or_node_to_node(Inkscape::XML::Node *node) { + return node; +} + +template<typename InIter> +text_refs_t text_categorize_refs(SPDocument *doc, InIter begin, InIter end, text_ref_t which) { + text_refs_t res; + std::set<Glib::ustring> int_ext; + auto idVisitor = [doc, which, &res, &int_ext](const Glib::ustring &id) { + auto ref_obj = doc->getObjectById(id); + if (sp_repr_is_def(ref_obj->getRepr())) { + if (which & TEXT_REF_DEF) { + res.emplace_back(id, TEXT_REF_DEF); + } + } else { + int_ext.insert(id); + } + }; + // Visit all shape references, detect the refs, and put internal and + // external ids in int_ext. + for (auto it = begin; it != end; ++it) { + sp_repr_visit_descendants( + text_obj_or_node_to_node(*it), + [doc, &int_ext, &idVisitor](Inkscape::XML::Node *crnt) { + if (!(crnt->name() && strcmp("svg:text", crnt->name()) == 0)) { + return true; + } + + auto crnt_obj = doc->getObjectById(crnt->attribute("id")); + { + const auto &inside_ids = crnt_obj->style->shape_inside.shape_ids; + std::for_each(inside_ids.begin(), inside_ids.end(), idVisitor); + + const auto &subtract_ids = crnt_obj->style->shape_subtract.shape_ids; + std::for_each(subtract_ids.begin(), subtract_ids.end(), idVisitor); + } + // Do not recurse into svg:text elements children + return false; + }); + } + + if (!(which & (TEXT_REF_INTERNAL | TEXT_REF_EXTERNAL))) { + // We already discovered the defs, bail out if nothing else is + // required. + return res; + } + // Visit all root elements, recursively and see which ones are in int_ext, + // therefore discovering the internal ids. + for (auto it = begin; it != end; ++it) { + sp_repr_visit_descendants( + text_obj_or_node_to_node(*it), + [which, &res, &int_ext](Inkscape::XML::Node *crnt) { + + auto id = crnt->attribute("id"); + auto find_iter = id ? int_ext.find(id) : int_ext.end(); + if (find_iter != int_ext.end()) { + if (which & TEXT_REF_INTERNAL) { + res.emplace_back(id, TEXT_REF_INTERNAL); + } + int_ext.erase(find_iter); + // don't recurse into children of a matched element + return false; + } + + return true; + }); + } + + // What is left in int_ext are the external ones + if (which & TEXT_REF_EXTERNAL) { + for (auto const &ext_id : int_ext) { + res.emplace_back(ext_id, TEXT_REF_EXTERNAL); + } + } + + return res; +} + +template<typename InIterOrig, typename InIterCopy> +void text_relink_refs(text_refs_t const &refs, InIterOrig origBegin, InIterOrig origEnd, InIterCopy copyBegin) { + // get all ids of text refs + std::set<Glib::ustring> all_refs; + for (auto const &ref : refs) { + all_refs.insert(ref.first); + } + + // find a mapping from old ids to new ids + std::map<Glib::ustring, Glib::ustring> old_to_new; + for (auto itOrig = origBegin, itCopy = copyBegin; itOrig != origEnd; ++itOrig, ++itCopy) { + sp_repr_visit_descendants( + text_obj_or_node_to_node(*itOrig), + text_obj_or_node_to_node(*itCopy), + [&all_refs, &old_to_new](Inkscape::XML::Node *a, Inkscape::XML::Node *b) { + if (a->attribute("id") && all_refs.find(a->attribute("id")) != all_refs.end()) { + old_to_new[a->attribute("id")] = b->attribute("id"); + return false; + } + return true; + }); + } + + if (all_refs.size() != old_to_new.size()) { + std::cerr << "text_relink_refs: Failed to match all references! all:" << all_refs.size() + << " matched:" << old_to_new.size() << std::endl; + } + // relink references + for (auto itOrig = origBegin, itCopy = copyBegin; itOrig != origEnd; ++itOrig, ++itCopy) { + sp_repr_visit_descendants( + text_obj_or_node_to_node(*itCopy), + [&old_to_new](Inkscape::XML::Node *crnt) { + if (strcmp("svg:text", crnt->name()) == 0) { + SPCSSAttr* css = sp_repr_css_attr (crnt, "style"); + for (auto &&prop : {"shape-inside", "shape-subtract"}) { + if (auto prop_str = sp_repr_css_property(css, prop, nullptr)) { + sp_repr_css_set_property(css, prop, text_relink_shapes_str(prop_str, old_to_new).c_str()); + } + } + sp_repr_css_set (crnt, css, "style"); + return false; + } + return true; + }); + } +} + +#endif // SEEN_TEXT_CHEMISTRY_IMPL_H diff --git a/src/text-chemistry.cpp b/src/text-chemistry.cpp new file mode 100644 index 0000000..04061fd --- /dev/null +++ b/src/text-chemistry.cpp @@ -0,0 +1,649 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Text commands + * + * Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "text-chemistry.h" +#include "text-editing.h" +#include "verbs.h" + +#include "object/sp-flowdiv.h" +#include "object/sp-flowregion.h" +#include "object/sp-flowtext.h" +#include "object/sp-rect.h" +#include "object/sp-textpath.h" +#include "object/sp-tspan.h" +#include "style.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; + +static SPItem * +flowtext_in_selection(Inkscape::Selection *selection) +{ + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + if (SP_IS_FLOWTEXT(*i)) + return *i; + } + return nullptr; +} + +static SPItem * +text_or_flowtext_in_selection(Inkscape::Selection *selection) +{ + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) + return *i; + } + return nullptr; +} + +static SPItem * +shape_in_selection(Inkscape::Selection *selection) +{ + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + if (SP_IS_SHAPE(*i)) + return *i; + } + return nullptr; +} + +void +text_put_on_path() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return; + + Inkscape::Selection *selection = desktop->getSelection(); + + SPItem *text = text_or_flowtext_in_selection(selection); + SPItem *shape = shape_in_selection(selection); + + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + + if (!text || !shape || boost::distance(selection->items()) != 2) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text and a path</b> to put text on path.")); + return; + } + + if (SP_IS_TEXT_TEXTPATH(text)) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("This text object is <b>already put on a path</b>. Remove it from the path first. Use <b>Shift+D</b> to look up its path.")); + return; + } + + if (SP_IS_RECT(shape)) { + // rect is the only SPShape which is not <path> yet, and thus SVG forbids us from putting text on it + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("You cannot put text on a rectangle in this version. Convert rectangle to path first.")); + return; + } + + // if a flowed text is selected, convert it to a regular text object + if (SP_IS_FLOWTEXT(text)) { + + if (!SP_FLOWTEXT(text)->layout.outputExists()) { + desktop->getMessageStack()-> + flash(Inkscape::WARNING_MESSAGE, + _("The flowed text(s) must be <b>visible</b> in order to be put on a path.")); + } + + Inkscape::XML::Node *repr = SP_FLOWTEXT(text)->getAsText(); + + if (!repr) return; + + Inkscape::XML::Node *parent = text->getRepr()->parent(); + parent->appendChild(repr); + + SPItem *new_item = (SPItem *) desktop->getDocument()->getObjectByRepr(repr); + new_item->doWriteTransform(text->transform); + new_item->updateRepr(); + + Inkscape::GC::release(repr); + text->deleteObject(); // delete the original flowtext + + desktop->getDocument()->ensureUpToDate(); + + selection->clear(); + + text = new_item; // point to the new text + } + + if (SP_IS_TEXT(text)) { + // Replace any new lines (including sodipodi:role="line") by spaces. + dynamic_cast<SPText *>(text)->remove_newlines(); + } + + Inkscape::Text::Layout const *layout = te_get_layout(text); + Inkscape::Text::Layout::Alignment text_alignment = layout->paragraphAlignment(layout->begin()); + + // remove transform from text, but recursively scale text's fontsize by the expansion + SP_TEXT(text)->_adjustFontsizeRecursive (text, text->transform.descrim()); + text->removeAttribute("transform"); + + // make a list of text children + std::vector<Inkscape::XML::Node *> text_reprs; + for(auto& o: text->children) { + text_reprs.push_back(o.getRepr()); + } + + // create textPath and put it into the text + Inkscape::XML::Node *textpath = xml_doc->createElement("svg:textPath"); + // reference the shape + gchar *href_str = g_strdup_printf("#%s", shape->getRepr()->attribute("id")); + textpath->setAttribute("xlink:href", href_str); + g_free(href_str); + if (text_alignment == Inkscape::Text::Layout::RIGHT) { + textpath->setAttribute("startOffset", "100%"); + } else if (text_alignment == Inkscape::Text::Layout::CENTER) { + textpath->setAttribute("startOffset", "50%"); + } + text->getRepr()->addChild(textpath, nullptr); + + for (auto i=text_reprs.rbegin();i!=text_reprs.rend();++i) { + // Make a copy of each text child + Inkscape::XML::Node *copy = (*i)->duplicate(xml_doc); + // We cannot have multiline in textpath, so remove line attrs from tspans + if (!strcmp(copy->name(), "svg:tspan")) { + copy->removeAttribute("sodipodi:role"); + copy->removeAttribute("x"); + copy->removeAttribute("y"); + } + // remove the old repr from under text + text->getRepr()->removeChild(*i); + // put its copy into under textPath + textpath->addChild(copy, nullptr); // fixme: copy id + } + + // x/y are useless with textpath, and confuse Batik 1.5 + text->removeAttribute("x"); + text->removeAttribute("y"); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Put text on path")); +} + +void +text_remove_from_path() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text on path</b> to remove it from path.")); + return; + } + + bool did = false; + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPObject *obj = *i; + + if (SP_IS_TEXT_TEXTPATH(obj)) { + SPObject *tp = obj->firstChild(); + + did = true; + + sp_textpath_to_text(tp); + } + } + + if (!did) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No texts-on-paths</b> in the selection.")); + } else { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Remove text from path")); + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + selection->setList(vec); // reselect to update statusbar description + } +} + +static void +text_remove_all_kerns_recursively(SPObject *o) +{ + o->removeAttribute("dx"); + o->removeAttribute("dy"); + o->removeAttribute("rotate"); + + // if x contains a list, leave only the first value + gchar const *x = o->getRepr()->attribute("x"); + if (x) { + gchar **xa_space = g_strsplit(x, " ", 0); + gchar **xa_comma = g_strsplit(x, ",", 0); + if (xa_space && *xa_space && *(xa_space + 1)) { + o->setAttribute("x", *xa_space); + } else if (xa_comma && *xa_comma && *(xa_comma + 1)) { + o->setAttribute("x", *xa_comma); + } + g_strfreev(xa_space); + g_strfreev(xa_comma); + } + + for (auto& i: o->children) { + text_remove_all_kerns_recursively(&i); + i.requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); + } +} + +//FIXME: must work with text selection +void +text_remove_all_kerns() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>text(s)</b> to remove kerns from.")); + return; + } + + bool did = false; + + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPObject *obj = *i; + + if (!SP_IS_TEXT(obj) && !SP_IS_TSPAN(obj) && !SP_IS_FLOWTEXT(obj)) { + continue; + } + + text_remove_all_kerns_recursively(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); + did = true; + } + + if (!did) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("Select <b>text(s)</b> to remove kerns from.")); + } else { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Remove manual kerns")); + } +} + +void +text_flow_into_shape() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return; + + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::Selection *selection = desktop->getSelection(); + + SPItem *text = text_or_flowtext_in_selection(selection); + SPItem *shape = shape_in_selection(selection); + + if (!text || !shape || boost::distance(selection->items()) < 2) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a text</b> and one or more <b>paths or shapes</b> to flow text into frame.")); + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/use_svg2", true)) { + // SVG 2 Text + + if (SP_IS_TEXT(text)) { + + // Make list of all shapes. + Glib::ustring shape_inside; + auto items = selection->items(); + for (auto item : items) { + if (SP_IS_SHAPE(item)) { + shape_inside += "url(#"; + shape_inside += item->getId(); + shape_inside += ") "; + } + } + + // Remove extra space at end. + if (shape_inside.length() > 1) { + shape_inside.erase (shape_inside.length() - 1); + } + + // Set 'shape-inside' property. + SPCSSAttr* css = sp_repr_css_attr (text->getRepr(), "style"); + sp_repr_css_set_property (css, "shape-inside", shape_inside.c_str()); + sp_repr_css_set_property (css, "white-space", "pre"); // Respect new lines. + sp_repr_css_set (text->getRepr(), css, "style"); + } + + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, + _("Flow text into shape")); + + + } else { + // SVG 1.2 Flowed Text + + if (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)) { + // remove transform from text, but recursively scale text's fontsize by the expansion + auto ex = i2i_affine(text, shape->parent).descrim(); + SP_TEXT(text)->_adjustFontsizeRecursive(text, ex); + text->removeAttribute("transform"); + } + + Inkscape::XML::Node *root_repr = xml_doc->createElement("svg:flowRoot"); + root_repr->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + root_repr->setAttribute("style", text->getRepr()->attribute("style")); // fixme: transfer style attrs too + shape->parent->getRepr()->appendChild(root_repr); + SPObject *root_object = doc->getObjectByRepr(root_repr); + g_return_if_fail(SP_IS_FLOWTEXT(root_object)); + + Inkscape::XML::Node *region_repr = xml_doc->createElement("svg:flowRegion"); + root_repr->appendChild(region_repr); + SPObject *object = doc->getObjectByRepr(region_repr); + g_return_if_fail(SP_IS_FLOWREGION(object)); + + /* Add clones */ + auto items = selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPItem *item = *i; + if (SP_IS_SHAPE(item)){ + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + gchar *href_str = g_strdup_printf("#%s", item->getRepr()->attribute("id")); + clone->setAttribute("xlink:href", href_str); + g_free(href_str); + + // add the new clone to the region + region_repr->appendChild(clone); + } + } + + if (SP_IS_TEXT(text)) { // flow from text, as string + Inkscape::XML::Node *para_repr = xml_doc->createElement("svg:flowPara"); + root_repr->appendChild(para_repr); + object = doc->getObjectByRepr(para_repr); + g_return_if_fail(SP_IS_FLOWPARA(object)); + + Inkscape::Text::Layout const *layout = te_get_layout(text); + Glib::ustring text_ustring = sp_te_get_string_multiline(text, layout->begin(), layout->end()); + + Inkscape::XML::Node *text_repr = xml_doc->createTextNode(text_ustring.c_str()); // FIXME: transfer all formatting! and convert newlines into flowParas! + para_repr->appendChild(text_repr); + + Inkscape::GC::release(para_repr); + Inkscape::GC::release(text_repr); + + } else { // reflow an already flowed text, preserving paras + for(auto& o: text->children) { + if (SP_IS_FLOWPARA(&o)) { + Inkscape::XML::Node *para_repr = o.getRepr()->duplicate(xml_doc); + root_repr->appendChild(para_repr); + object = doc->getObjectByRepr(para_repr); + g_return_if_fail(SP_IS_FLOWPARA(object)); + Inkscape::GC::release(para_repr); + } + } + } + + text->deleteObject(true); + + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, + _("Flow text into shape")); + + desktop->getSelection()->set(SP_ITEM(root_object)); + + Inkscape::GC::release(root_repr); + Inkscape::GC::release(region_repr); + + } +} + +void +text_unflow () +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + return; + + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::Selection *selection = desktop->getSelection(); + + if (!text_or_flowtext_in_selection(selection) || boost::distance(selection->items()) < 1) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>a flowed text</b> to unflow it.")); + return; + } + + std::vector<SPItem*> new_objs; + std::vector<SPItem *> old_objs; + + auto items = selection->items(); + for (auto i : items) { + + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + SPText *text = dynamic_cast<SPText *>(i); + + if (flowtext) { + + // we discard transform when unflowing, but we must preserve expansion which is visible as + // font size multiplier + double ex = (flowtext->transform).descrim(); + + if (sp_te_get_string_multiline(flowtext) == nullptr) { // flowtext is empty + continue; + } + + /* Create <text> */ + Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + /* Set style */ + rtext->setAttribute("style", flowtext->getRepr()->attribute("style")); // fixme: transfer style attrs too; + // and from descendants + + Geom::OptRect bbox = flowtext->geometricBounds(flowtext->i2doc_affine()); + if (bbox) { + Geom::Point xy = bbox->min(); + sp_repr_set_svg_double(rtext, "x", xy[Geom::X]); + sp_repr_set_svg_double(rtext, "y", xy[Geom::Y]); + } + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? + rtext->addChild(rtspan, nullptr); + + gchar *text_string = sp_te_get_string_multiline(flowtext); + Inkscape::XML::Node *text_repr = xml_doc->createTextNode(text_string); // FIXME: transfer all formatting!!! + free(text_string); + rtspan->appendChild(text_repr); + + flowtext->parent->getRepr()->appendChild(rtext); + SPObject *text_object = doc->getObjectByRepr(rtext); + + // restore the font size multiplier from the flowtext's transform + SPText *text = SP_TEXT(text_object); + text->_adjustFontsizeRecursive(text, ex); + + new_objs.push_back((SPItem *)text_object); + old_objs.push_back(flowtext); + + Inkscape::GC::release(rtext); + Inkscape::GC::release(rtspan); + Inkscape::GC::release(text_repr); + + } else if (text){ + + if (text->has_shape_inside()) { + + Inkscape::XML::Node *rtext = text->getRepr(); + + // Position unflowed text near shape. + Geom::OptRect bbox = text->geometricBounds(text->i2doc_affine()); + if (bbox) { + Geom::Point xy = bbox->min(); + sp_repr_set_svg_double(rtext, "x", xy[Geom::X]); + sp_repr_set_svg_double(rtext, "y", xy[Geom::Y]); + } + + // Remove 'shape-inside' property. + SPCSSAttr *css = sp_repr_css_attr(rtext, "style"); + sp_repr_css_unset_property(css, "shape-inside"); + sp_repr_css_change(rtext, css, "style"); + sp_repr_css_attr_unref(css); + + // We'll leave tspans alone other than stripping 'x' and 'y' (this will preserve + // styling). + // We'll also remove temporarily 'sodipodi:role' (which shouldn't be + // necessary later). + for (auto j : text->childList(false)) { + SPTSpan* tspan = dynamic_cast<SPTSpan *>(j); + if (tspan) { + tspan->getRepr()->setAttribute("x", nullptr); + tspan->getRepr()->setAttribute("y", nullptr); + tspan->getRepr()->setAttribute("sodipodi:role", nullptr); + } + } + } + } + } + + // For flowtext objects. + if (new_objs.size() != 0) { + + // Update selection + selection->clear(); + reverse(new_objs.begin(), new_objs.end()); + selection->setList(new_objs); + + // Delete old objects + for (auto i : old_objs) { + i->deleteObject(true); + } + } + + DocumentUndo::done(doc, SP_VERB_CONTEXT_TEXT, + _("Unflow flowed text")); +} + +void +flowtext_to_text() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>flowed text(s)</b> to convert.")); + return; + } + + bool did = false; + bool ignored = false; + + std::vector<Inkscape::XML::Node*> reprs; + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + for(auto item : items){ + + if (!SP_IS_FLOWTEXT(item)) + continue; + + if (!SP_FLOWTEXT(item)->layout.outputExists()) { + ignored = true; + continue; + } + + Inkscape::XML::Node *repr = SP_FLOWTEXT(item)->getAsText(); + + if (!repr) break; + + did = true; + + Inkscape::XML::Node *parent = item->getRepr()->parent(); + parent->addChild(repr, item->getRepr()); + + SPItem *new_item = reinterpret_cast<SPItem *>(desktop->getDocument()->getObjectByRepr(repr)); + new_item->doWriteTransform(item->transform); + new_item->updateRepr(); + + Inkscape::GC::release(repr); + item->deleteObject(); + + reprs.push_back(repr); + } + + if (did) { + DocumentUndo::done(desktop->getDocument(), + SP_VERB_OBJECT_FLOWTEXT_TO_TEXT, + _("Convert flowed text to text")); + selection->setReprList(reprs); + } else if (ignored) { + // no message for (did && ignored) because it is immediately overwritten + desktop->getMessageStack()-> + flash(Inkscape::ERROR_MESSAGE, + _("Flowed text(s) must be <b>visible</b> in order to be converted.")); + + } else { + desktop->getMessageStack()-> + flash(Inkscape::ERROR_MESSAGE, + _("<b>No flowed text(s)</b> to convert in the selection.")); + } + +} + + +Glib::ustring text_relink_shapes_str(gchar const *prop, std::map<Glib::ustring, Glib::ustring> const &old_to_new) { + std::vector<Glib::ustring> shapes_url = Glib::Regex::split_simple(" ", prop); + Glib::ustring res; + for (auto shape_url : shapes_url) { + if (shape_url.compare(0, 5, "url(#") != 0 || shape_url.compare(shape_url.size() - 1, 1, ")") != 0) { + std::cerr << "text_relink_shapes_str: Invalid shape value: " << shape_url << std::endl; + } else { + auto old_id = shape_url.substr(5, shape_url.size() - 6); + auto find_it = old_to_new.find(old_id); + if (find_it != old_to_new.end()) { + res.append("url(#").append(find_it->second).append(") "); + } else { + std::cerr << "Failed to replace reference " << old_id << std::endl; + } + } + } + // remove trailing space + if (!res.empty()) { + assert(res.raw().back() == ' '); + res.resize(res.size() - 1); + } + return res; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/text-chemistry.h b/src/text-chemistry.h new file mode 100644 index 0000000..56bad24 --- /dev/null +++ b/src/text-chemistry.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TEXT_CHEMISTRY_H +#define SEEN_TEXT_CHEMISTRY_H + +// TODO move to selection-chemistry? + +/* + * Text commands + * + * Authors: + * bulia byak + * + * Copyright (C) 2004 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +void text_put_on_path(); +void text_remove_from_path(); +void text_remove_all_kerns(); +void text_flow_into_shape(); +void text_unflow(); +void flowtext_to_text(); +enum text_ref_t { TEXT_REF_DEF = 0x1, TEXT_REF_EXTERNAL = 0x2, TEXT_REF_INTERNAL = 0x4, }; +using text_refs_t = std::vector<std::pair<Glib::ustring, text_ref_t>>; +template<typename InIter> +text_refs_t text_categorize_refs(SPDocument *doc, InIter begin, InIter end, text_ref_t which); +template<typename InIterOrig, typename InIterCopy> +void text_relink_refs(text_refs_t const &refs, + InIterOrig origBegin, InIterOrig origEnd, InIterCopy copyBegin); + +#include "text-chemistry-impl.h" + +#endif // SEEN_TEXT_CHEMISTRY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/text-editing.cpp b/src/text-editing.cpp new file mode 100644 index 0000000..03f8070 --- /dev/null +++ b/src/text-editing.cpp @@ -0,0 +1,2181 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Parent class for text and flowtext + * + * Authors: + * bulia byak + * Richard Hughes + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-5 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include <cstring> +#include <string> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "text-editing.h" + +#include "object/sp-textpath.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowregion.h" +#include "object/sp-item-group.h" +#include "object/sp-tref.h" +#include "object/sp-tspan.h" +#include "style.h" + +#include "util/units.h" + +#include "xml/attribute-record.h" +#include "xml/sp-css-attr.h" + +static const gchar *tref_edit_message = _("You cannot edit <b>cloned character data</b>."); +static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend = false); + +static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration); + +Inkscape::Text::Layout const * te_get_layout (SPItem const *item) +{ + if (SP_IS_TEXT(item)) { + return &(SP_TEXT(item)->layout); + } else if (SP_IS_FLOWTEXT (item)) { + return &(SP_FLOWTEXT(item)->layout); + } + return nullptr; +} + +static void te_update_layout_now (SPItem *item) +{ + if (SP_IS_TEXT(item)) + SP_TEXT(item)->rebuildLayout(); + else if (SP_IS_FLOWTEXT (item)) + SP_FLOWTEXT(item)->rebuildLayout(); + item->updateRepr(); +} + +void te_update_layout_now_recursive(SPItem *item) +{ + if (SP_IS_GROUP(item)) { + std::vector<SPItem*> item_list = sp_item_group_item_list(SP_GROUP(item)); + for(auto list_item : item_list){ + te_update_layout_now_recursive(list_item); + } + } else if (SP_IS_TEXT(item)) + SP_TEXT(item)->rebuildLayout(); + else if (SP_IS_FLOWTEXT (item)) + SP_FLOWTEXT(item)->rebuildLayout(); + item->updateRepr(); +} + +bool sp_te_output_is_empty(SPItem const *item) +{ + Inkscape::Text::Layout const *layout = te_get_layout(item); + return layout->begin() == layout->end(); +} + +bool sp_te_input_is_empty(SPObject const *item) +{ + bool empty = true; + if (SP_IS_STRING(item)) { + empty = SP_STRING(item)->string.empty(); + } else { + for (auto& child: item->children) { + if (!sp_te_input_is_empty(&child)) { + empty = false; + break; + } + } + } + return empty; +} + +Inkscape::Text::Layout::iterator +sp_te_get_position_by_coords (SPItem const *item, Geom::Point const &i_p) +{ + Geom::Affine im (item->i2dt_affine ()); + im = im.inverse(); + + Geom::Point p = i_p * im; + Inkscape::Text::Layout const *layout = te_get_layout(item); + return layout->getNearestCursorPositionTo(p); +} + +std::vector<Geom::Point> sp_te_create_selection_quads(SPItem const *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, Geom::Affine const &transform) +{ + if (start == end) + return std::vector<Geom::Point>(); + Inkscape::Text::Layout const *layout = te_get_layout(item); + if (layout == nullptr) + return std::vector<Geom::Point>(); + + return layout->createSelectionShape(start, end, transform); +} + +void +sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, Geom::Point &p0, Geom::Point &p1) +{ + Inkscape::Text::Layout const *layout = te_get_layout(item); + double height, rotation; + layout->queryCursorShape(position, p0, height, rotation); + p1 = Geom::Point(p0[Geom::X] + height * sin(rotation), p0[Geom::Y] - height * cos(rotation)); // valgrind warns that rotation is not initialized here. Why is to be seen in queryCursorShape +} + +SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position) +{ + SPObject const *pos_obj = sp_te_object_at_position(text, position); + SPStyle *result = (pos_obj) ? pos_obj->style : nullptr; + return result; +} + +SPObject const * sp_te_object_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position) +{ + Inkscape::Text::Layout const *layout = te_get_layout(text); + if (layout == nullptr) { + return nullptr; + } + SPObject *rawptr = nullptr; + layout->getSourceOfCharacter(position, &rawptr); + SPObject const *pos_obj = rawptr; + if (pos_obj == nullptr) { + pos_obj = text; + } + while (pos_obj->style == nullptr) { + pos_obj = pos_obj->parent; // not interested in SPStrings + } + return pos_obj; +} + +/* + * for debugging input + * +char * dump_hexy(const gchar * utf8) +{ + static char buffer[1024]; + + buffer[0]='\0'; + for (const char *ptr=utf8; *ptr; ptr++) { + sprintf(buffer+strlen(buffer),"x%02X",(unsigned char)*ptr); + } + return buffer; +} +*/ + +Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, gchar const *utf8) +{ + iterator_pair pair; + sp_te_delete(item, start, end, pair); + return sp_te_insert(item, pair.first, utf8); +} + + +/* ***************************************************************************************************/ +// I N S E R T I N G T E X T + +static bool is_line_break_object(SPObject const *object) +{ + bool is_line_break = false; + + if (object) { + if (SP_IS_TEXT(object) + || (SP_IS_TSPAN(object) && SP_TSPAN(object)->role != SP_TSPAN_ROLE_UNSPECIFIED) + || SP_IS_TEXTPATH(object) + || SP_IS_FLOWDIV(object) + || SP_IS_FLOWPARA(object) + || SP_IS_FLOWLINE(object) + || SP_IS_FLOWREGIONBREAK(object)) { + + is_line_break = true; + } + } + + return is_line_break; +} + +/** returns the attributes for an object, or NULL if it isn't a text, +tspan, tref, or textpath. */ +static TextTagAttributes* attributes_for_object(SPObject *object) +{ + if (SP_IS_TSPAN(object)) + return &SP_TSPAN(object)->attributes; + if (SP_IS_TEXT(object)) + return &SP_TEXT(object)->attributes; + if (SP_IS_TREF(object)) + return &SP_TREF(object)->attributes; + if (SP_IS_TEXTPATH(object)) + return &SP_TEXTPATH(object)->attributes; + return nullptr; +} + +static const char * span_name_for_text_object(SPObject const *object) +{ + if (SP_IS_TEXT(object)) return "svg:tspan"; + else if (SP_IS_FLOWTEXT(object)) return "svg:flowSpan"; + return nullptr; +} + +unsigned sp_text_get_length(SPObject const *item) +{ + unsigned length = 0; + + if (SP_IS_STRING(item)) { + length = SP_STRING(item)->string.length(); + } else { + if (is_line_break_object(item)) { + length++; + } + + for (auto& child: item->children) { + if (SP_IS_STRING(&child)) { + length += SP_STRING(&child)->string.length(); + } else { + length += sp_text_get_length(&child); + } + } + } + + return length; +} + +unsigned sp_text_get_length_upto(SPObject const *item, SPObject const *upto) +{ + unsigned length = 0; + + // The string is the lowest level and the length can be counted directly. + if (SP_IS_STRING(item)) { + return SP_STRING(item)->string.length(); + } + + // Take care of new lines... + if (is_line_break_object(item) && !SP_IS_TEXT(item)) { + if (item != item->parent->firstChild()) { + // add 1 for each newline + length++; + } + } + + // Count the length of the children + for (auto& child: item->children) { + if (upto && &child == upto) { + // hit upto, return immediately + return length; + } + if (SP_IS_STRING(&child)) { + length += SP_STRING(&child)->string.length(); + } + else { + if (upto && child.isAncestorOf(upto)) { + // upto is below us, recurse and break loop + length += sp_text_get_length_upto(&child, upto); + return length; + } else { + // recurse and go to the next sibling + length += sp_text_get_length_upto(&child, upto); + } + } + } + return length; +} + +static Inkscape::XML::Node* duplicate_node_without_children(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node const *old_node) +{ + switch (old_node->type()) { + case Inkscape::XML::ELEMENT_NODE: { + Inkscape::XML::Node *new_node = xml_doc->createElement(old_node->name()); + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attributes = old_node->attributeList(); + GQuark const id_key = g_quark_from_string("id"); + for ( ; attributes ; attributes++) { + if (attributes->key == id_key) continue; + new_node->setAttribute(g_quark_to_string(attributes->key), attributes->value); + } + return new_node; + } + + case Inkscape::XML::TEXT_NODE: + return xml_doc->createTextNode(old_node->content()); + + case Inkscape::XML::COMMENT_NODE: + return xml_doc->createComment(old_node->content()); + + case Inkscape::XML::PI_NODE: + return xml_doc->createPI(old_node->name(), old_node->content()); + + case Inkscape::XML::DOCUMENT_NODE: + return nullptr; // this had better never happen + } + return nullptr; +} + +/** returns the sum of the (recursive) lengths of all the SPStrings prior +to \a item at the same level. */ +static unsigned sum_sibling_text_lengths_before(SPObject const *item) +{ + unsigned char_index = 0; + for (auto& sibling: item->parent->children) { + if (&sibling == item) { + break; + } + char_index += sp_text_get_length(&sibling); + } + return char_index; +} + +/** splits the attributes for the first object at the given \a char_index +and moves the ones after that point into \a second_item. */ +static void split_attributes(SPObject *first_item, SPObject *second_item, unsigned char_index) +{ + TextTagAttributes *first_attrs = attributes_for_object(first_item); + TextTagAttributes *second_attrs = attributes_for_object(second_item); + if (first_attrs && second_attrs) + first_attrs->split(char_index, second_attrs); +} + +/** recursively divides the XML node tree into two objects: the original will +contain all objects up to and including \a split_obj and the returned value +will be the new leaf which represents the copy of \a split_obj and extends +down the tree with new elements all the way to the common root which is the +parent of the first line break node encountered. +*/ +static SPObject* split_text_object_tree_at(SPObject *split_obj, unsigned char_index) +{ + Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc(); + if (is_line_break_object(split_obj)) { + Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr()); + split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr()); + Inkscape::GC::release(new_node); + split_attributes(split_obj, split_obj->getNext(), char_index); + return split_obj->getNext(); + } else if (!SP_IS_TSPAN(split_obj) && + !SP_IS_FLOWTSPAN(split_obj) && + !SP_IS_STRING(split_obj)) { + std::cerr << "split_text_object_tree_at: Illegal split object type! (Illegal document structure.)" << std::endl; + return nullptr; + } + + unsigned char_count_before = sum_sibling_text_lengths_before(split_obj); + SPObject *duplicate_obj = split_text_object_tree_at(split_obj->parent, char_index + char_count_before); + + if (duplicate_obj == nullptr) { + // Illegal document structure (no line break object). + return nullptr; + } + + // copy the split node + Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr()); + duplicate_obj->getRepr()->appendChild(new_node); + Inkscape::GC::release(new_node); + + // sort out the copied attributes (x/y/dx/dy/rotate) + split_attributes(split_obj, duplicate_obj->firstChild(), char_index); + + // then move all the subsequent nodes + split_obj = split_obj->getNext(); + while (split_obj) { + Inkscape::XML::Node *move_repr = split_obj->getRepr(); + SPObject *next_obj = split_obj->getNext(); // this is about to become invalidated by removeChild() + Inkscape::GC::anchor(move_repr); + split_obj->parent->getRepr()->removeChild(move_repr); + duplicate_obj->getRepr()->appendChild(move_repr); + Inkscape::GC::release(move_repr); + + split_obj = next_obj; + } + return duplicate_obj->firstChild(); +} + +/** inserts a new line break at the given position in a text or flowtext +object. If the position is in the middle of a span, the XML tree must be +chopped in two such that the line can be created at the root of the text +element. Returns an iterator pointing just after the inserted break. */ +Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *item, Inkscape::Text::Layout::iterator &position) +{ + // Disable newlines in a textpath; TODO: maybe on Enter in a textpath, separate it into two + // texpaths attached to the same path, with a vertical shift + if (SP_IS_TEXT_TEXTPATH (item) || SP_IS_TREF(item)) + return position; + + Inkscape::Text::Layout const *layout = te_get_layout(item); + + // If this is plain SVG 1.1 text object without a tspan with sodipodi:role="line", we need + // to wrap it or our custom line breaking code won't work! + bool need_to_wrap = false; + SPText* text_object = dynamic_cast<SPText *>(item); + if (text_object && !text_object->has_shape_inside() && !text_object->has_inline_size()) { + + need_to_wrap = true; + for (auto child : item->childList(false)) { + auto tspan = dynamic_cast<SPTSpan *>(child); + if (tspan && tspan->role == SP_TSPAN_ROLE_LINE) { + // Already wrapped + need_to_wrap = false; + break; + } + } + + if (need_to_wrap) { + + // We'll need to rebuild layout, so store character postion: + int char_index = layout->iteratorToCharIndex(position); + + // Create wrapping tspan. + Inkscape::XML::Node *text_repr = text_object->getRepr(); + Inkscape::XML::Document *xml_doc = text_repr->document(); + Inkscape::XML::Node *new_tspan_repr = xml_doc->createElement("svg:tspan"); + new_tspan_repr->setAttribute("sodipodi:role", "line"); + + // Move text content to tspan and add tspan to text object. + // To do: This moves <desc> and <title> too. + move_child_nodes(text_repr, new_tspan_repr); + text_repr->appendChild(new_tspan_repr); + + // Need to find new iterator. + text_object->rebuildLayout(); + position = layout->charIndexToIterator(char_index); + } + } + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + SPObject *split_obj = nullptr; + Glib::ustring::iterator split_text_iter; + if (position != layout->end()) { + layout->getSourceOfCharacter(position, &split_obj, &split_text_iter); + } + + if (split_obj == nullptr || is_line_break_object(split_obj)) { + + if (split_obj == nullptr) split_obj = item->lastChild(); + + if (SP_IS_TREF(split_obj)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + return position; + } + + if (split_obj) { + Inkscape::XML::Document *xml_doc = split_obj->document->getReprDoc(); + Inkscape::XML::Node *new_node = duplicate_node_without_children(xml_doc, split_obj->getRepr()); + // if we finaly go to a text element without TSpan we mist set content to none + // new_node->setContent(""); + split_obj->parent->getRepr()->addChild(new_node, split_obj->getRepr()); + Inkscape::GC::release(new_node); + } + + } else if (SP_IS_STRING(split_obj)) { + + // If the parent is a tref, editing on this particular string is disallowed. + if (SP_IS_TREF(split_obj->parent)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + return position; + } + + Glib::ustring *string = &SP_STRING(split_obj)->string; + unsigned char_index = 0; + for (Glib::ustring::iterator it = string->begin() ; it != split_text_iter ; ++it) + char_index++; + // we need to split the entire text tree into two + + SPObject *object = split_text_object_tree_at(split_obj, char_index); + if (object == nullptr) { + // Illegal document structure + return position; + } + + SPString *new_string = SP_STRING(object); + new_string->getRepr()->setContent(&*split_text_iter.base()); // a little ugly + string->erase(split_text_iter, string->end()); + split_obj->getRepr()->setContent(string->c_str()); + // TODO: if the split point was at the beginning of a span we have a whole load of empty elements to clean up + } else { + // TODO + // I think the only case to put here is arbitrary gaps, which nobody uses yet + } + + item->updateRepr(); + unsigned char_index = layout->iteratorToCharIndex(position); + te_update_layout_now(item); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return layout->charIndexToIterator(char_index + 1); +} + +/** finds the first SPString after the given position, including children, excluding parents */ +static SPString* sp_te_seek_next_string_recursive(SPObject *start_obj) +{ + while (start_obj) { + if (start_obj->hasChildren()) { + SPString *found_string = sp_te_seek_next_string_recursive(start_obj->firstChild()); + if (found_string) { + return found_string; + } + } + if (SP_IS_STRING(start_obj)) { + return SP_STRING(start_obj); + } + start_obj = start_obj->getNext(); + if (is_line_break_object(start_obj)) { + break; // don't cross line breaks + } + } + return nullptr; +} + +/** inserts the given characters into the given string and inserts +corresponding new x/y/dx/dy/rotate attributes into all its parents. */ +static void insert_into_spstring(SPString *string_item, Glib::ustring::iterator iter_at, gchar const *utf8) +{ + unsigned char_index = 0; + unsigned char_count = g_utf8_strlen(utf8, -1); + Glib::ustring *string = &SP_STRING(string_item)->string; + + for (Glib::ustring::iterator it = string->begin() ; it != iter_at ; ++it) + char_index++; + string->replace(iter_at, iter_at, utf8); + + SPObject *parent_item = string_item; + for ( ; ; ) { + char_index += sum_sibling_text_lengths_before(parent_item); + parent_item = parent_item->parent; + TextTagAttributes *attributes = attributes_for_object(parent_item); + if (!attributes) break; + attributes->insert(char_index, char_count); + } +} + +/** Inserts the given text into a text or flowroot object. Line breaks +cannot be inserted using this function, see sp_te_insert_line(). Returns +an iterator pointing just after the inserted text. */ +Inkscape::Text::Layout::iterator +sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, gchar const *utf8) +{ + if (!g_utf8_validate(utf8,-1,nullptr)) { + g_warning("Trying to insert invalid utf8"); + return position; + } + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::Text::Layout const *layout = te_get_layout(item); + Glib::ustring::iterator iter_text; + // we want to insert after the previous char, not before the current char. + // it makes a difference at span boundaries + Inkscape::Text::Layout::iterator it_prev_char = position; + bool cursor_at_start = !it_prev_char.prevCharacter(); + bool cursor_at_end = position == layout->end(); + SPObject *source_obj = nullptr; + layout->getSourceOfCharacter(it_prev_char, &source_obj, &iter_text); + if (SP_IS_STRING(source_obj)) { + // If the parent is a tref, editing on this particular string is disallowed. + if (SP_IS_TREF(source_obj->parent)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + return position; + } + + // Now the simple case can begin... + if (!cursor_at_start){ + ++iter_text; + } + SPString *string_item = SP_STRING(source_obj); + insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : iter_text, utf8); + } else { + // the not-so-simple case where we're at a line break or other control char; add to the next child/sibling SPString + Inkscape::XML::Document *xml_doc = item->getRepr()->document(); + if (cursor_at_start) { + source_obj = item; + if (source_obj->hasChildren()) { + source_obj = source_obj->firstChild(); + if (SP_IS_FLOWTEXT(item)) { + while (SP_IS_FLOWREGION(source_obj) || SP_IS_FLOWREGIONEXCLUDE(source_obj)) { + source_obj = source_obj->getNext(); + } + if (source_obj == nullptr) { + source_obj = item; + } + } + } + if (source_obj == item && SP_IS_FLOWTEXT(item)) { + Inkscape::XML::Node *para = xml_doc->createElement("svg:flowPara"); + item->getRepr()->appendChild(para); + source_obj = item->lastChild(); + } + } else + source_obj = source_obj->getNext(); + + if (source_obj) { // never fails + SPString *string_item = sp_te_seek_next_string_recursive(source_obj); + if (string_item == nullptr) { + // need to add an SPString in this (pathological) case + Inkscape::XML::Node *rstring = xml_doc->createTextNode(""); + source_obj->getRepr()->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + g_assert(SP_IS_STRING(source_obj->firstChild())); + string_item = SP_STRING(source_obj->firstChild()); + } + // If the parent is a tref, editing on this particular string is disallowed. + if (SP_IS_TREF(string_item->parent)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + return position; + } + + insert_into_spstring(string_item, cursor_at_end ? string_item->string.end() : string_item->string.begin(), utf8); + } + } + + item->updateRepr(); + unsigned char_index = layout->iteratorToCharIndex(position); + te_update_layout_now(item); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return layout->charIndexToIterator(char_index + g_utf8_strlen(utf8, -1)); +} + + +/* ***************************************************************************************************/ +// D E L E T I N G T E X T + +/** moves all the children of \a from_repr to \a to_repr, either before +the existing children or after them. Order is maintained. The empty +\a from_repr is not deleted. */ +static void move_child_nodes(Inkscape::XML::Node *from_repr, Inkscape::XML::Node *to_repr, bool prepend) +{ + while (from_repr->childCount()) { + Inkscape::XML::Node *child = prepend ? from_repr->lastChild() : from_repr->firstChild(); + Inkscape::GC::anchor(child); + from_repr->removeChild(child); + if (prepend) to_repr->addChild(child, nullptr); + else to_repr->appendChild(child); + Inkscape::GC::release(child); + } +} + +/** returns the object in the tree which is the closest ancestor of both +\a one and \a two. It will never return anything higher than \a text. */ +static SPObject* get_common_ancestor(SPObject *text, SPObject *one, SPObject *two) +{ + if (one == nullptr || two == nullptr) + return text; + SPObject *common_ancestor = one; + if (SP_IS_STRING(common_ancestor)) + common_ancestor = common_ancestor->parent; + while (!(common_ancestor == two || common_ancestor->isAncestorOf(two))) { + g_assert(common_ancestor != text); + common_ancestor = common_ancestor->parent; + } + return common_ancestor; +} + +/** positions \a para_obj and \a text_iter to be pointing at the end +of the last string in the last leaf object of \a para_obj. If the last +leaf is not an SPString then \a text_iter will be unchanged. */ +static void move_to_end_of_paragraph(SPObject **para_obj, Glib::ustring::iterator *text_iter) +{ + while ((*para_obj)->hasChildren()) + *para_obj = (*para_obj)->lastChild(); + if (SP_IS_STRING(*para_obj)) + *text_iter = SP_STRING(*para_obj)->string.end(); +} + +/** delete the line break pointed to by \a item by merging its children into +the next suitable object and deleting \a item. Returns the object after the +ones that have just been moved and sets \a next_is_sibling accordingly. */ +static SPObject* delete_line_break(SPObject *root, SPObject *item, bool *next_is_sibling) +{ + Inkscape::XML::Node *this_repr = item->getRepr(); + SPObject *next_item = nullptr; + unsigned moved_char_count = sp_text_get_length(item) - 1; // the -1 is because it's going to count the line break + + /* some sample cases (the div is the item to be deleted, the * represents where to put the new span): + <div></div><p>*text</p> + <p><div></div>*text</p> + <p><div></div></p><p>*text</p> + */ + Inkscape::XML::Document *xml_doc = item->getRepr()->document(); + Inkscape::XML::Node *new_span_repr = xml_doc->createElement(span_name_for_text_object(root)); + + new_span_repr->setAttributeOrRemoveIfEmpty("dx", this_repr->attribute("dx")); + new_span_repr->setAttributeOrRemoveIfEmpty("dy", this_repr->attribute("dy")); + new_span_repr->setAttributeOrRemoveIfEmpty("rotate", this_repr->attribute("rotate")); + + SPObject *following_item = item; + while (following_item->getNext() == nullptr) { + following_item = following_item->parent; + g_assert(following_item != root); + } + following_item = following_item->getNext(); + + SPObject *new_parent_item; + if (SP_IS_STRING(following_item)) { + new_parent_item = following_item->parent; + new_parent_item->getRepr()->addChild(new_span_repr, following_item->getPrev() ? following_item->getPrev()->getRepr() : nullptr); + next_item = following_item; + *next_is_sibling = true; + } else { + new_parent_item = following_item; + next_item = new_parent_item->firstChild(); + *next_is_sibling = true; + if (next_item == nullptr) { + next_item = new_parent_item; + *next_is_sibling = false; + } + new_parent_item->getRepr()->addChild(new_span_repr, nullptr); + } + + // work around a bug in sp_style_write_difference() which causes the difference + // not to be written if the second param has a style set which the first does not + // by causing the first param to have everything set + SPCSSAttr *dest_node_attrs = sp_repr_css_attr(new_parent_item->getRepr(), "style"); + SPCSSAttr *this_node_attrs = sp_repr_css_attr(this_repr, "style"); + SPCSSAttr *this_node_attrs_inherited = sp_repr_css_attr_inherited(this_repr, "style"); + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = dest_node_attrs->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *key = g_quark_to_string(attrs->key); + gchar const *this_attr = this_node_attrs_inherited->attribute(key); + if ((this_attr == nullptr || strcmp(attrs->value, this_attr)) && this_node_attrs->attribute(key) == nullptr) + this_node_attrs->setAttribute(key, this_attr); + } + sp_repr_css_attr_unref(this_node_attrs_inherited); + sp_repr_css_attr_unref(this_node_attrs); + sp_repr_css_attr_unref(dest_node_attrs); + sp_repr_css_change(new_span_repr, this_node_attrs, "style"); + + TextTagAttributes *attributes = attributes_for_object(new_parent_item); + if (attributes) + attributes->insert(0, moved_char_count); + move_child_nodes(this_repr, new_span_repr); + this_repr->parent()->removeChild(this_repr); + return next_item; +} + +/** erases the given characters from the given string and deletes the +corresponding x/y/dx/dy/rotate attributes from all its parents. */ +static void erase_from_spstring(SPString *string_item, Glib::ustring::iterator iter_from, Glib::ustring::iterator iter_to) +{ + unsigned char_index = 0; + unsigned char_count = 0; + Glib::ustring *string = &SP_STRING(string_item)->string; + + for (Glib::ustring::iterator it = string->begin() ; it != iter_from ; ++it){ + char_index++; + } + for (Glib::ustring::iterator it = iter_from ; it != iter_to ; ++it){ + char_count++; + } + string->erase(iter_from, iter_to); + string_item->getRepr()->setContent(string->c_str()); + + SPObject *parent_item = string_item; + for ( ; ; ) { + char_index += sum_sibling_text_lengths_before(parent_item); + parent_item = parent_item->parent; + TextTagAttributes *attributes = attributes_for_object(parent_item); + if (attributes == nullptr) { + break; + } + + attributes->erase(char_index, char_count); + attributes->writeTo(parent_item->getRepr()); + } +} + +/* Deletes the given characters from a text or flowroot object. This is +quite a complicated operation, partly due to the cleanup that is done if all +the text in a subobject has been deleted, and partly due to the difficulty +of figuring out what is a line break and how to delete one. Returns the +real start and ending iterators based on the situation. */ +bool +sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start, + Inkscape::Text::Layout::iterator const &end, iterator_pair &iter_pair) +{ + bool success = false; + + iter_pair.first = start; + iter_pair.second = end; + + if (start == end) return success; + + if (start > end) { + iter_pair.first = end; + iter_pair.second = start; + } + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + Inkscape::Text::Layout const *layout = te_get_layout(item); + SPObject *start_item = nullptr, *end_item = nullptr; + Glib::ustring::iterator start_text_iter, end_text_iter; + layout->getSourceOfCharacter(iter_pair.first, &start_item, &start_text_iter); + layout->getSourceOfCharacter(iter_pair.second, &end_item, &end_text_iter); + if (start_item == nullptr) { + return success; // start is at end of text + } + if (is_line_break_object(start_item)) { + move_to_end_of_paragraph(&start_item, &start_text_iter); + } + if (end_item == nullptr) { + end_item = item->lastChild(); + move_to_end_of_paragraph(&end_item, &end_text_iter); + } else if (is_line_break_object(end_item)) { + move_to_end_of_paragraph(&end_item, &end_text_iter); + } + + SPObject *common_ancestor = get_common_ancestor(item, start_item, end_item); + + bool has_text_decoration = false; + gchar const *root_style = (item)->getRepr()->attribute("style"); + if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true; + + if (start_item == end_item) { + // the quick case where we're deleting stuff all from the same string + if (SP_IS_STRING(start_item)) { // always true (if it_start != it_end anyway) + // If the parent is a tref, editing on this particular string is disallowed. + if (SP_IS_TREF(start_item->parent)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + } else { + erase_from_spstring(SP_STRING(start_item), start_text_iter, end_text_iter); + success = true; + } + } + } else { + SPObject *sub_item = start_item; + // walk the tree from start_item to end_item, deleting as we go + while (sub_item != item) { + if (sub_item == end_item) { + if (SP_IS_STRING(sub_item)) { + // If the parent is a tref, editing on this particular string is disallowed. + if (SP_IS_TREF(sub_item->parent)) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, tref_edit_message); + break; + } + + Glib::ustring *string = &SP_STRING(sub_item)->string; + erase_from_spstring(SP_STRING(sub_item), string->begin(), end_text_iter); + success = true; + } + break; + } + if (SP_IS_STRING(sub_item)) { + SPString *string = SP_STRING(sub_item); + if (sub_item == start_item) + erase_from_spstring(string, start_text_iter, string->string.end()); + else + erase_from_spstring(string, string->string.begin(), string->string.end()); + success = true; + } + // walk to the next item in the tree + if (sub_item->hasChildren()) + sub_item = sub_item->firstChild(); + else { + SPObject *next_item; + do { + bool is_sibling = true; + next_item = sub_item->getNext(); + if (next_item == nullptr) { + next_item = sub_item->parent; + is_sibling = false; + } + + if (is_line_break_object(sub_item)) + next_item = delete_line_break(item, sub_item, &is_sibling); + + sub_item = next_item; + if (is_sibling) break; + // no more siblings, go up a parent + } while (sub_item != item && sub_item != end_item); + } + } + } + + while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){}; + te_update_layout_now(item); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + layout->validateIterator(&iter_pair.first); + layout->validateIterator(&iter_pair.second); + return success; +} + + +/* ***************************************************************************************************/ +// P L A I N T E X T F U N C T I O N S + +/** Gets a text-only representation of the given text or flowroot object, +replacing line break elements with '\n'. */ +static void sp_te_get_ustring_multiline(SPObject const *root, Glib::ustring *string, bool *pending_line_break) +{ + if (*pending_line_break) { + *string += '\n'; + } + for (auto& child: root->children) { + if (SP_IS_STRING(&child)) { + *string += SP_STRING(&child)->string; + } else { + sp_te_get_ustring_multiline(&child, string, pending_line_break); + } + } + if (!SP_IS_TEXT(root) && !SP_IS_TEXTPATH(root) && is_line_break_object(root)) { + *pending_line_break = true; + } +} + +/** Gets a text-only representation of the given text or flowroot object, +replacing line break elements with '\n'. The return value must be free()d. */ +gchar * +sp_te_get_string_multiline (SPItem const *text) +{ + Glib::ustring string; + bool pending_line_break = false; + + if (!SP_IS_TEXT(text) && !SP_IS_FLOWTEXT(text)) return nullptr; + sp_te_get_ustring_multiline(text, &string, &pending_line_break); + if (string.empty()) return nullptr; + return strdup(string.data()); +} + +/** Gets a text-only representation of the characters in a text or flowroot +object from \a start to \a end only. Line break elements are replaced with +'\n'. */ +Glib::ustring +sp_te_get_string_multiline (SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end) +{ + if (start == end) return ""; + Inkscape::Text::Layout::iterator first, last; + if (start < end) { + first = start; + last = end; + } else { + first = end; + last = start; + } + Inkscape::Text::Layout const *layout = te_get_layout(text); + Glib::ustring result; + // not a particularly fast piece of code. I'll optimise it if people start to notice. + for ( ; first < last ; first.nextCharacter()) { + SPObject *char_item = nullptr; + Glib::ustring::iterator text_iter; + layout->getSourceOfCharacter(first, &char_item, &text_iter); + if (SP_IS_STRING(char_item)) { + result += *text_iter; + } else { + result += '\n'; + } + } + return result; +} + +void +sp_te_set_repr_text_multiline(SPItem *text, gchar const *str) +{ + g_return_if_fail (text != nullptr); + g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)); + + Inkscape::XML::Document *xml_doc = text->getRepr()->document(); + Inkscape::XML::Node *repr; + SPObject *object; + bool is_textpath = false; + if (SP_IS_TEXT_TEXTPATH (text)) { + repr = text->firstChild()->getRepr(); + object = text->firstChild(); + is_textpath = true; + } else { + repr = text->getRepr(); + object = text; + } + + if (!str) { + str = ""; + } + gchar *content = g_strdup (str); + + repr->setContent(""); + for (auto& child: object->childList(false)) { + if (!SP_IS_FLOWREGION(child) && !SP_IS_FLOWREGIONEXCLUDE(child)) { + repr->removeChild(child->getRepr()); + } + } + + gchar *p = content; + while (p) { + gchar *e = strchr (p, '\n'); + if (is_textpath) { + if (e) *e = ' '; // no lines for textpath, replace newlines with spaces + } else { + if (e) *e = '\0'; + Inkscape::XML::Node *rtspan; + if (SP_IS_TEXT(text)) { // create a tspan for each line + rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); + } else { // create a flowPara for each line + rtspan = xml_doc->createElement("svg:flowPara"); + } + Inkscape::XML::Node *rstr = xml_doc->createTextNode(p); + rtspan->addChild(rstr, nullptr); + Inkscape::GC::release(rstr); + repr->appendChild(rtspan); + Inkscape::GC::release(rtspan); + } + p = (e) ? e + 1 : nullptr; + } + if (is_textpath) { + Inkscape::XML::Node *rstr = xml_doc->createTextNode(content); + repr->addChild(rstr, nullptr); + Inkscape::GC::release(rstr); + } + + g_free (content); +} + +/* ***************************************************************************************************/ +// K E R N I N G A N D S P A C I N G + +/** Returns the attributes block and the character index within that block +which represents the iterator \a position. */ +TextTagAttributes* +text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index) +{ + if (item == nullptr || char_index == nullptr || !SP_IS_TEXT(item)) { + return nullptr; // flowtext doesn't support kerning yet + } + SPText *text = SP_TEXT(item); + + SPObject *source_item = nullptr; + Glib::ustring::iterator source_text_iter; + text->layout.getSourceOfCharacter(position, &source_item, &source_text_iter); + + if (!SP_IS_STRING(source_item)) { + return nullptr; + } + Glib::ustring *string = &SP_STRING(source_item)->string; + *char_index = sum_sibling_text_lengths_before(source_item); + for (Glib::ustring::iterator it = string->begin() ; it != source_text_iter ; ++it) { + ++*char_index; + } + + return attributes_for_object(source_item->parent); +} + +void +sp_te_adjust_kerning_screen (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, Geom::Point by) +{ + // divide increment by zoom + // divide increment by matrix expansion + gdouble factor = 1 / desktop->current_zoom(); + Geom::Affine t (item->i2doc_affine()); + factor = factor / t.descrim(); + by = factor * by; + + unsigned char_index; + TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index); + if (attributes) attributes->addToDxDy(char_index, by); + if (start != end) { + attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index); + if (attributes) attributes->addToDxDy(char_index, -by); + } + + item->updateRepr(); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void sp_te_adjust_dx(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta) +{ + unsigned char_index = 0; + TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index); + if (attributes) { + attributes->addToDx(char_index, delta); + } + if (start != end) { + attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index); + if (attributes) { + attributes->addToDx(char_index, -delta); + } + } + + item->updateRepr(); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void sp_te_adjust_dy(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop * /*desktop*/, double delta) +{ + unsigned char_index = 0; + TextTagAttributes *attributes = text_tag_attributes_at_position(item, std::min(start, end), &char_index); + if (attributes) { + attributes->addToDy(char_index, delta); + } + if (start != end) { + attributes = text_tag_attributes_at_position(item, std::max(start, end), &char_index); + if (attributes) { + attributes->addToDy(char_index, -delta); + } + } + + item->updateRepr(); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble pixels) +{ + // divide increment by zoom + // divide increment by matrix expansion + gdouble factor = 1 / desktop->current_zoom(); + Geom::Affine t (text->i2doc_affine()); + factor = factor / t.descrim(); + Inkscape::Text::Layout const *layout = te_get_layout(text); + if (layout == nullptr) return; + SPObject *source_item = nullptr; + layout->getSourceOfCharacter(std::min(start, end), &source_item); + if (source_item == nullptr) { + return; + } + gdouble degrees = (180/M_PI) * atan2(pixels, source_item->parent->style->font_size.computed / factor); + + sp_te_adjust_rotation(text, start, end, desktop, degrees); +} + +void +sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees) +{ + unsigned char_index; + TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index); + if (attributes == nullptr) return; + + if (start != end) { + for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) { + attributes = text_tag_attributes_at_position(text, it, &char_index); + if (attributes) attributes->addToRotate(char_index, degrees); + } + } else + attributes->addToRotate(char_index, degrees); + + text->updateRepr(); + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void sp_te_set_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop */*desktop*/, gdouble degrees) +{ + unsigned char_index = 0; + TextTagAttributes *attributes = text_tag_attributes_at_position(text, std::min(start, end), &char_index); + if (attributes != nullptr) { + if (start != end) { + for (Inkscape::Text::Layout::iterator it = std::min(start, end) ; it != std::max(start, end) ; it.nextCharacter()) { + attributes = text_tag_attributes_at_position(text, it, &char_index); + if (attributes) { + attributes->setRotate(char_index, degrees); + } + } + } else { + attributes->setRotate(char_index, degrees); + } + + text->updateRepr(); + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +void +sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, gdouble by) +{ + g_return_if_fail (text != nullptr); + g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)); + + Inkscape::Text::Layout const *layout = te_get_layout(text); + + gdouble val; + SPObject *source_obj = nullptr; + unsigned nb_let; + layout->getSourceOfCharacter(std::min(start, end), &source_obj); + + if (source_obj == nullptr) { // end of text + source_obj = text->lastChild(); + } + if (SP_IS_STRING(source_obj)) { + source_obj = source_obj->parent; + } + + SPStyle *style = source_obj->style; + + // calculate real value + /* TODO: Consider calculating val unconditionally, i.e. drop the first `if' line, and + get rid of the `else val = 0.0'. Similarly below and in sp-string.cpp. */ + if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex + if (style->letter_spacing.unit == SP_CSS_UNIT_EM) { + val = style->font_size.computed * style->letter_spacing.value; + } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) { + val = style->font_size.computed * style->letter_spacing.value * 0.5; + } else { // unknown unit - should not happen + val = 0.0; + } + } else { // there's a real value in .computed, or it's zero + val = style->letter_spacing.computed; + } + + if (start == end) { + while (!is_line_break_object(source_obj)) { // move up the tree so we apply to the closest paragraph + source_obj = source_obj->parent; + } + nb_let = sp_text_get_length(source_obj); + } else { + nb_let = abs(layout->iteratorToCharIndex(end) - layout->iteratorToCharIndex(start)); + } + + // divide increment by zoom and by the number of characters in the line, + // so that the entire line is expanded by by pixels, no matter what its length + gdouble const zoom = desktop->current_zoom(); + gdouble const zby = (by + / (zoom * (nb_let > 1 ? nb_let - 1 : 1)) + / SP_ITEM(source_obj)->i2doc_affine().descrim()); + val += zby; + + if (start == end) { + // set back value to entire paragraph + style->letter_spacing.normal = FALSE; + if (style->letter_spacing.value != 0 && style->letter_spacing.computed == 0) { // set in em or ex + if (style->letter_spacing.unit == SP_CSS_UNIT_EM) { + style->letter_spacing.value = val / style->font_size.computed; + } else if (style->letter_spacing.unit == SP_CSS_UNIT_EX) { + style->letter_spacing.value = val / style->font_size.computed * 2; + } + } else { + style->letter_spacing.computed = val; + } + + style->letter_spacing.set = TRUE; + } else { + // apply to selection only + SPCSSAttr *css = sp_repr_css_attr_new(); + char string_val[40]; + g_snprintf(string_val, sizeof(string_val), "%f", val); + sp_repr_css_set_property(css, "letter-spacing", string_val); + sp_te_apply_style(text, start, end, css); + sp_repr_css_attr_unref(css); + } + + text->updateRepr(); + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + +// Only used for page-up and page-down and sp_te_adjust_linespacing_screen +double +sp_te_get_average_linespacing (SPItem *text) +{ + Inkscape::Text::Layout const *layout = te_get_layout(text); + if (!layout) + return 0; + + unsigned line_count = layout->lineIndex(layout->end()); + double all_lines_height = layout->characterAnchorPoint(layout->end())[Geom::Y] - layout->characterAnchorPoint(layout->begin())[Geom::Y]; + double average_line_height = all_lines_height / (line_count == 0 ? 1 : line_count); + return average_line_height; +} + +/** Adjust the line height by 'amount'. + * If top_level is true then objects without 'line-height' set or withwill get a set value, + * otherwise objects that inherit line-height will not get onw=e. + */ +void +sp_te_adjust_line_height (SPObject *object, double amount, double average, bool top_level = true) { + + SPStyle *style = object->style; + + // Always set if top level true. + // Also set if line_height is set to a non-zero value. + if (top_level || + (style->line_height.set && !style->line_height.inherit && style->line_height.computed != 0)){ + + // Scale default values + if (!style->line_height.set || style->line_height.inherit || style->line_height.normal) { + style->line_height.set = TRUE; + style->line_height.inherit = FALSE; + style->line_height.normal = FALSE; + style->line_height.unit = SP_CSS_UNIT_NONE; + style->line_height.value = style->line_height.computed = Inkscape::Text::Layout::LINE_HEIGHT_NORMAL; + } + + switch (style->line_height.unit) { + + case SP_CSS_UNIT_NONE: + default: + // Multiplier-type units, stored in computed + if (fabs(style->line_height.computed) < 0.001) { + style->line_height.computed = amount < 0.0 ? -0.001 : 0.001; + // the formula below could get stuck at zero + } else { + style->line_height.computed *= (average + amount) / average; + } + style->line_height.value = style->line_height.computed; + break; + + + // Relative units, stored in value + case SP_CSS_UNIT_EM: + case SP_CSS_UNIT_EX: + case SP_CSS_UNIT_PERCENT: + if (fabs(style->line_height.value) < 0.001) { + style->line_height.value = amount < 0.0 ? -0.001 : 0.001; + } else { + style->line_height.value *= (average + amount) / average; + } + break; + + + // Absolute units + case SP_CSS_UNIT_PX: + style->line_height.computed += amount; + style->line_height.value = style->line_height.computed; + break; + case SP_CSS_UNIT_PT: + style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pt"); + style->line_height.value = style->line_height.computed; + break; + case SP_CSS_UNIT_PC: + style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "pc"); + style->line_height.value = style->line_height.computed; + break; + case SP_CSS_UNIT_MM: + style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "mm"); + style->line_height.value = style->line_height.computed; + break; + case SP_CSS_UNIT_CM: + style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "cm"); + style->line_height.value = style->line_height.computed; + break; + case SP_CSS_UNIT_IN: + style->line_height.computed += Inkscape::Util::Quantity::convert(amount, "px", "in"); + style->line_height.value = style->line_height.computed; + break; + } + object->updateRepr(); + } + + std::vector<SPObject*> children = object->childList(false); + for (auto child: children) { + sp_te_adjust_line_height (child, amount, average, false); + } +} + +void +sp_te_adjust_linespacing_screen (SPItem *text, Inkscape::Text::Layout::iterator const &/*start*/, Inkscape::Text::Layout::iterator const &/*end*/, SPDesktop *desktop, gdouble by) +{ + // TODO: use start and end iterators to delineate the area to be affected + g_return_if_fail (text != nullptr); + g_return_if_fail (SP_IS_TEXT(text) || SP_IS_FLOWTEXT(text)); + + Inkscape::Text::Layout const *layout = te_get_layout(text); + + double average_line_height = sp_te_get_average_linespacing (text); + if (fabs(average_line_height) < 0.001) average_line_height = 0.001; + + // divide increment by zoom and by the number of lines, + // so that the entire object is expanded by by pixels + unsigned line_count = layout->lineIndex(layout->end()); + gdouble zby = by / (desktop->current_zoom() * (line_count == 0 ? 1 : line_count)); + + // divide increment by matrix expansion + Geom::Affine t(text->i2doc_affine()); + zby = zby / t.descrim(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/tools/text/line_spacing_mode", 0); + if (mode == 0) { // Adaptive: <text> line-spacing is zero, only scale children. + std::vector<SPObject*> children = text->childList(false); + for (auto child: children) { + sp_te_adjust_line_height (child, zby, average_line_height, false); + } + } else { + sp_te_adjust_line_height (text, zby, average_line_height, true); + } + + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_TEXT_LAYOUT_MODIFIED_FLAG); +} + + +/* ***************************************************************************************************/ +// S T Y L E A P P L I C A T I O N + + +/** converts an iterator to a character index, mainly because ustring::substr() +doesn't have a version that takes iterators as parameters. */ +static unsigned char_index_of_iterator(Glib::ustring const &string, Glib::ustring::const_iterator text_iter) +{ + unsigned n = 0; + for (Glib::ustring::const_iterator it = string.begin() ; it != string.end() && it != text_iter ; ++it) + n++; + return n; +} + +// Move to style.h? +/** applies the given style string on top of the existing styles for \a item, +as opposed to sp_style_merge_from_style_string which merges its parameter +underneath the existing styles (ie ignoring already set properties). */ +static void overwrite_style_with_string(SPObject *item, gchar const *style_string) +{ + SPStyle style(item->document); + style.mergeString(style_string); + gchar const *item_style_string = item->getRepr()->attribute("style"); + if (item_style_string && *item_style_string) { + style.mergeString(item_style_string); + } + Glib::ustring new_style_string = style.write(); + item->setAttributeOrRemoveIfEmpty("style", new_style_string); +} + +// Move to style.h? +/** Returns true if the style of \a parent and the style of \a child are +equivalent (and hence the children of both will appear the same). It is a +limitation of the current implementation that \a parent must be a (not +necessarily immediate) ancestor of \a child. */ +static bool objects_have_equal_style(SPObject const *parent, SPObject const *child) +{ + // the obvious implementation of strcmp(style_write_all(parent), style_write_all(child)) + // will not work. Firstly because of an inheritance bug in style.cpp that has + // implications too large for me to feel safe fixing, but mainly because the css spec + // requires that the computed value is inherited, not the specified value. + g_assert(parent->isAncestorOf(child)); + + Glib::ustring parent_style = parent->style->write( SP_STYLE_FLAG_ALWAYS ); + + // we have to write parent_style then read it again, because some properties format their values + // differently depending on whether they're set or not (*cough*dash-offset*cough*) + SPStyle parent_spstyle(parent->document); + parent_spstyle.mergeString(parent_style.c_str()); + parent_style = parent_spstyle.write(SP_STYLE_FLAG_ALWAYS); + + Glib::ustring child_style_construction; + while (child != parent) { + // FIXME: this assumes that child's style is only in style= whereas it can also be in css attributes! + char const *style_text = child->getRepr()->attribute("style"); + if (style_text && *style_text) { + child_style_construction.insert(0, style_text); + child_style_construction.insert(0, 1, ';'); + } + child = child->parent; + } + child_style_construction.insert(0, parent_style); + + SPStyle child_spstyle(parent->document); + child_spstyle.mergeString(child_style_construction.c_str()); + Glib::ustring child_style = child_spstyle.write(SP_STYLE_FLAG_ALWAYS); + + bool equal = (child_style == parent_style); // Glib::ustring overloads == operator + return equal; +} + +/** returns true if \a first and \a second contain all the same attributes +with the same values as each other. Note that we have to compare both +forwards and backwards to make sure we don't miss any attributes that are +in one but not the other. */ +static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second) +{ + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + attrs = second->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + return true; +} + +/** sets the given css attribute on this object and all its descendants. +Annoyingly similar to sp_desktop_apply_css_recursive(), except without the +transform stuff. */ +static void apply_css_recursive(SPObject *o, SPCSSAttr const *css) +{ + sp_repr_css_change(o->getRepr(), const_cast<SPCSSAttr*>(css), "style"); + + for (auto& child: o->children) { + if (sp_repr_css_property(const_cast<SPCSSAttr*>(css), "opacity", nullptr) != nullptr) { + // Unset properties which are accumulating and thus should not be set recursively. + // For example, setting opacity 0.5 on a group recursively would result in the visible opacity of 0.25 for an item in the group. + SPCSSAttr *css_recurse = sp_repr_css_attr_new(); + sp_repr_css_merge(css_recurse, const_cast<SPCSSAttr*>(css)); + sp_repr_css_set_property(css_recurse, "opacity", nullptr); + apply_css_recursive(&child, css_recurse); + sp_repr_css_attr_unref(css_recurse); + } else { + apply_css_recursive(&child, const_cast<SPCSSAttr*>(css)); + } + } +} + +/** applies the given style to all the objects at the given level and below +which are between \a start_item and \a end_item, creating spans as necessary. +If \a start_item or \a end_item are NULL then the style is applied to all +objects to the beginning or end respectively. \a span_object_name is the +name of the xml for a text span (ie tspan or flowspan). */ +static void recursively_apply_style(SPObject *common_ancestor, SPCSSAttr const *css, SPObject *start_item, Glib::ustring::iterator start_text_iter, SPObject *end_item, Glib::ustring::iterator end_text_iter, char const *span_object_name) +{ + bool passed_start = start_item == nullptr ? true : false; + Inkscape::XML::Document *xml_doc = common_ancestor->document->getReprDoc(); + + for (SPObject *child = common_ancestor->firstChild() ; child ; child = child->getNext()) { + if (start_item == child) { + passed_start = true; + } + + if (passed_start) { + if (end_item && child->isAncestorOf(end_item)) { + recursively_apply_style(child, css, nullptr, start_text_iter, end_item, end_text_iter, span_object_name); + break; + } + // apply style + + // note that when adding stuff we must make sure that 'child' stays valid so the for loop keeps working. + // often this means that new spans are created before child and child is modified only + if (SP_IS_STRING(child)) { + SPString *string_item = SP_STRING(child); + bool surround_entire_string = true; + + Inkscape::XML::Node *child_span = xml_doc->createElement(span_object_name); + sp_repr_css_set(child_span, const_cast<SPCSSAttr*>(css), "style"); // better hope that prototype wasn't nonconst for a good reason + SPObject *prev_item = child->getPrev(); + Inkscape::XML::Node *prev_repr = prev_item ? prev_item->getRepr() : nullptr; + + if (child == start_item || child == end_item) { + surround_entire_string = false; + if (start_item == end_item && start_text_iter != string_item->string.begin()) { + // eg "abcDEFghi" -> "abc"<span>"DEF"</span>"ghi" + unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter); + unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter); + + Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str()); + common_ancestor->getRepr()->addChild(text_before, prev_repr); + common_ancestor->getRepr()->addChild(child_span, text_before); + Inkscape::GC::release(text_before); + Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index, end_char_index - start_char_index).c_str()); + child_span->appendChild(text_in_span); + Inkscape::GC::release(text_in_span); + child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str()); + } else if (child == end_item) { + // eg "ABCdef" -> <span>"ABC"</span>"def" + // (includes case where start_text_iter == begin()) + // NB: we might create an empty string here. Doesn't matter, it'll get cleaned up later + unsigned end_char_index = char_index_of_iterator(string_item->string, end_text_iter); + + common_ancestor->getRepr()->addChild(child_span, prev_repr); + Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(0, end_char_index).c_str()); + child_span->appendChild(text_in_span); + Inkscape::GC::release(text_in_span); + child->getRepr()->setContent(string_item->string.substr(end_char_index).c_str()); + } else if (start_text_iter != string_item->string.begin()) { + // eg "abcDEF" -> "abc"<span>"DEF"</span> + unsigned start_char_index = char_index_of_iterator(string_item->string, start_text_iter); + + Inkscape::XML::Node *text_before = xml_doc->createTextNode(string_item->string.substr(0, start_char_index).c_str()); + common_ancestor->getRepr()->addChild(text_before, prev_repr); + common_ancestor->getRepr()->addChild(child_span, text_before); + Inkscape::GC::release(text_before); + Inkscape::XML::Node *text_in_span = xml_doc->createTextNode(string_item->string.substr(start_char_index).c_str()); + child_span->appendChild(text_in_span); + Inkscape::GC::release(text_in_span); + child->deleteObject(); + child = common_ancestor->get_child_by_repr(child_span); + + } else + surround_entire_string = true; + } + if (surround_entire_string) { + Inkscape::XML::Node *child_repr = child->getRepr(); + common_ancestor->getRepr()->addChild(child_span, child_repr); + Inkscape::GC::anchor(child_repr); + common_ancestor->getRepr()->removeChild(child_repr); + child_span->appendChild(child_repr); + Inkscape::GC::release(child_repr); + child = common_ancestor->get_child_by_repr(child_span); + } + Inkscape::GC::release(child_span); + + } else if (child != end_item) { // not a string and we're applying to the entire object. This is easy + apply_css_recursive(child, css); + } + + } else { // !passed_start + if (child->isAncestorOf(start_item)) { + recursively_apply_style(child, css, start_item, start_text_iter, end_item, end_text_iter, span_object_name); + if (end_item && child->isAncestorOf(end_item)) + break; // only happens when start_item == end_item (I think) + passed_start = true; + } + } + + if (end_item == child) + break; + } +} + +/* if item is at the beginning of a tree it doesn't matter which element +it points to so for neatness we would like it to point to the highest +possible child of \a common_ancestor. There is no iterator return because +a string can never be an ancestor. + +eg: <span><span>*ABC</span>DEFghi</span> where * is the \a item. We would +like * to point to the inner span because we can apply style to that whole +span. */ +static SPObject* ascend_while_first(SPObject *item, Glib::ustring::iterator text_iter, SPObject *common_ancestor) +{ + if (item == common_ancestor) + return item; + if (SP_IS_STRING(item)) + if (text_iter != SP_STRING(item)->string.begin()) + return item; + for ( ; ; ) { + SPObject *parent = item->parent; + if (parent == common_ancestor) { + break; + } + if (item != parent->firstChild()) { + break; + } + item = parent; + } + return item; +} + + +/** empty spans: abc<span></span>def + -> abcdef */ +static bool tidy_operator_empty_spans(SPObject **item, bool /*has_text_decoration*/) +{ + bool result = false; + if ( !(*item)->hasChildren() + && !is_line_break_object(*item) + && !(SP_IS_STRING(*item) && !SP_STRING(*item)->string.empty()) + ) { + SPObject *next = (*item)->getNext(); + (*item)->deleteObject(); + *item = next; + result = true; + } + return result; +} + +/** inexplicable spans: abc<span style="">def</span>ghi + -> "abc""def""ghi" +the repeated strings will be merged by another operator. */ +static bool tidy_operator_inexplicable_spans(SPObject **item, bool /*has_text_decoration*/) +{ + //XML Tree being directly used here while it shouldn't be. + if (*item && sp_repr_is_meta_element((*item)->getRepr())) { + return false; + } + if (SP_IS_STRING(*item)) { + return false; + } + if (is_line_break_object(*item)) { + return false; + } + TextTagAttributes *attrs = attributes_for_object(*item); + if (attrs && attrs->anyAttributesSet()) { + return false; + } + if (!objects_have_equal_style((*item)->parent, *item)) { + return false; + } + SPObject *next = *item; + while ((*item)->hasChildren()) { + Inkscape::XML::Node *repr = (*item)->firstChild()->getRepr(); + Inkscape::GC::anchor(repr); + (*item)->getRepr()->removeChild(repr); + (*item)->parent->getRepr()->addChild(repr, next->getRepr()); + Inkscape::GC::release(repr); + next = next->getNext(); + } + (*item)->deleteObject(); + *item = next; + return true; +} + +/** repeated spans: <font a>abc</font><font a>def</font> + -> <font a>abcdef</font> */ +static bool tidy_operator_repeated_spans(SPObject **item, bool /*has_text_decoration*/) +{ + SPObject *first = *item; + SPObject *second = first->getNext(); + if (second == nullptr) return false; + + Inkscape::XML::Node *first_repr = first->getRepr(); + Inkscape::XML::Node *second_repr = second->getRepr(); + + if (first_repr->type() != second_repr->type()) return false; + + if (SP_IS_STRING(first) && SP_IS_STRING(second)) { + // also amalgamate consecutive SPStrings into one + Glib::ustring merged_string = SP_STRING(first)->string + SP_STRING(second)->string; + first->getRepr()->setContent(merged_string.c_str()); + second_repr->parent()->removeChild(second_repr); + return true; + } + + // merge consecutive spans with identical styles into one + if (first_repr->type() != Inkscape::XML::ELEMENT_NODE) return false; + if (strcmp(first_repr->name(), second_repr->name()) != 0) return false; + if (is_line_break_object(second)) return false; + gchar const *first_style = first_repr->attribute("style"); + gchar const *second_style = second_repr->attribute("style"); + if (!((first_style == nullptr && second_style == nullptr) + || (first_style != nullptr && second_style != nullptr && !strcmp(first_style, second_style)))) + return false; + + // all our tests passed: do the merge + TextTagAttributes *attributes_first = attributes_for_object(first); + TextTagAttributes *attributes_second = attributes_for_object(second); + if (attributes_first && attributes_second && attributes_second->anyAttributesSet()) { + TextTagAttributes attributes_first_copy = *attributes_first; + attributes_first->join(attributes_first_copy, *attributes_second, sp_text_get_length(first)); + } + move_child_nodes(second_repr, first_repr); + second_repr->parent()->removeChild(second_repr); + return true; + // *item is still the next object to process +} + +/** redundant nesting: <font a><font b>abc</font></font> + -> <font b>abc</font> + excessive nesting: <font a><size 1>abc</size></font> + -> <font a,size 1>abc</font> */ +static bool tidy_operator_excessive_nesting(SPObject **item, bool /*has_text_decoration*/) +{ + if (!(*item)->hasChildren()) { + return false; + } + if ((*item)->firstChild() != (*item)->lastChild()) { + return false; + } + if (SP_IS_FLOWREGION((*item)->firstChild()) || SP_IS_FLOWREGIONEXCLUDE((*item)->firstChild())) { + return false; + } + if (SP_IS_STRING((*item)->firstChild())) { + return false; + } + if (is_line_break_object((*item)->firstChild())) { + return false; + } + TextTagAttributes *attrs = attributes_for_object((*item)->firstChild()); + if (attrs && attrs->anyAttributesSet()) { + return false; + } + gchar const *child_style = (*item)->firstChild()->getRepr()->attribute("style"); + if (child_style && *child_style) { + overwrite_style_with_string(*item, child_style); + } + move_child_nodes((*item)->firstChild()->getRepr(), (*item)->getRepr()); + (*item)->firstChild()->deleteObject(); + return true; +} + +/** helper for tidy_operator_redundant_double_nesting() */ +static bool redundant_double_nesting_processor(SPObject **item, SPObject *child, bool prepend) +{ + if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) { + return false; + } + if (SP_IS_STRING(child)) { + return false; + } + if (is_line_break_object(child)) { + return false; + } + if (is_line_break_object(*item)) { + return false; + } + TextTagAttributes *attrs = attributes_for_object(child); + if (attrs && attrs->anyAttributesSet()) { + return false; + } + if (!objects_have_equal_style((*item)->parent, child)) { + return false; + } + + Inkscape::XML::Node *insert_after_repr = nullptr; + if (!prepend) { + insert_after_repr = (*item)->getRepr(); + } else if ((*item)->getPrev()) { + insert_after_repr = (*item)->getPrev()->getRepr(); + } + while (child->getRepr()->childCount()) { + Inkscape::XML::Node *move_repr = child->getRepr()->firstChild(); + Inkscape::GC::anchor(move_repr); + child->getRepr()->removeChild(move_repr); + (*item)->parent->getRepr()->addChild(move_repr, insert_after_repr); + Inkscape::GC::release(move_repr); + insert_after_repr = move_repr; // I think this will stay valid long enough. It's garbage collected these days. + } + child->deleteObject(); + return true; +} + +/** redundant double nesting: <font b><font a><font b>abc</font>def</font>ghi</font> + -> <font b>abc<font a>def</font>ghi</font> +this function does its work when the parameter is the <font a> tag in the +example. You may note that this only does its work when the doubly-nested +child is the first or last. The other cases are called 'style inversion' +below, and I'm not yet convinced that the result of that operation will be +tidier in all cases. */ +static bool tidy_operator_redundant_double_nesting(SPObject **item, bool /*has_text_decoration*/) +{ + if (!(*item)->hasChildren()) return false; + if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is excessive nesting, done above + if (redundant_double_nesting_processor(item, (*item)->firstChild(), true)) + return true; + if (redundant_double_nesting_processor(item, (*item)->lastChild(), false)) + return true; + return false; +} + +/** helper for tidy_operator_redundant_semi_nesting(). Checks a few things, +then compares the styles for item+child versus just child. If they're equal, +tidying is possible. */ +static bool redundant_semi_nesting_processor(SPObject **item, SPObject *child, bool prepend) +{ + if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child)) + return false; + if (SP_IS_STRING(child)) return false; + if (is_line_break_object(child)) return false; + if (is_line_break_object(*item)) return false; + TextTagAttributes *attrs = attributes_for_object(child); + if (attrs && attrs->anyAttributesSet()) return false; + attrs = attributes_for_object(*item); + if (attrs && attrs->anyAttributesSet()) return false; + + SPCSSAttr *css_child_and_item = sp_repr_css_attr_new(); + SPCSSAttr *css_child_only = sp_repr_css_attr_new(); + gchar const *item_style = (*item)->getRepr()->attribute("style"); + if (item_style && *item_style) { + sp_repr_css_attr_add_from_string(css_child_and_item, item_style); + } + gchar const *child_style = child->getRepr()->attribute("style"); + if (child_style && *child_style) { + sp_repr_css_attr_add_from_string(css_child_and_item, child_style); + sp_repr_css_attr_add_from_string(css_child_only, child_style); + } + bool equal = css_attrs_are_equal(css_child_only, css_child_and_item); + sp_repr_css_attr_unref(css_child_and_item); + sp_repr_css_attr_unref(css_child_only); + if (!equal) return false; + + Inkscape::XML::Document *xml_doc = (*item)->getRepr()->document(); + Inkscape::XML::Node *new_span = xml_doc->createElement((*item)->getRepr()->name()); + if (prepend) { + SPObject *prev = (*item)->getPrev(); + (*item)->parent->getRepr()->addChild(new_span, prev ? prev->getRepr() : nullptr); + } else { + (*item)->parent->getRepr()->addChild(new_span, (*item)->getRepr()); + } + new_span->setAttribute("style", child->getRepr()->attribute("style")); + move_child_nodes(child->getRepr(), new_span); + Inkscape::GC::release(new_span); + child->deleteObject(); + return true; +} + +/** redundant semi-nesting: <font a><font b>abc</font>def</font> + -> <font b>abc</font><font>def</font> +test this by applying a colour to a region, then a different colour to +a partially-overlapping region. */ +static bool tidy_operator_redundant_semi_nesting(SPObject **item, bool /*has_text_decoration*/) +{ + if (!(*item)->hasChildren()) return false; + if ((*item)->firstChild() == (*item)->lastChild()) return false; // this is redundant nesting, done above + if (redundant_semi_nesting_processor(item, (*item)->firstChild(), true)) + return true; + if (redundant_semi_nesting_processor(item, (*item)->lastChild(), false)) + return true; + return false; +} + + +/* tidy_operator_styled_whitespace commented out: not only did it have bugs, + * but it did *not* preserve the rendering: spaces in different font sizes, + * for instance, have different width, so moving them out of tspans changes + * the document. cf https://bugs.launchpad.net/inkscape/+bug/1477723 +*/ + +#if 0 +/** helper for tidy_operator_styled_whitespace(), finds the last string object +in a paragraph which is not \a not_obj. */ +static SPString* find_last_string_child_not_equal_to(SPObject *root, SPObject *not_obj) +{ + for (SPObject *child = root->lastChild() ; child ; child = child->getPrev()) + { + if (child == not_obj) continue; + if (child->hasChildren()) { + SPString *ret = find_last_string_child_not_equal_to(child, not_obj); + if (ret) return ret; + } else if (SP_IS_STRING(child)) + return SP_STRING(child); + } + return NULL; +} + +/** whitespace-only spans: + abc<font> </font>def + -> abc<font></font> def + abc<b><i>def</i> </b>ghi + -> abc<b><i>def</i></b> ghi + + visible text-decoration changes on white spaces should not be subject + to this sort of processing. So + abc<text-decoration-color> </text-decoration-color>def + is unchanged. +*/ +static bool tidy_operator_styled_whitespace(SPObject **item, bool has_text_decoration) +{ + // any type of visible text decoration is OK as pure spaces, so do nothing here in that case. + if (!SP_IS_STRING(*item) || has_text_decoration ) { + return false; + } + Glib::ustring const &str = SP_STRING(*item)->string; + for (Glib::ustring::const_iterator it = str.begin() ; it != str.end() ; ++it) { + if (!g_unichar_isspace(*it)) { + return false; + } + } + + + SPObject *test_item = *item; + SPString *next_string; + for ( ; ; ) { // find the next string + next_string = sp_te_seek_next_string_recursive(test_item->getNext()); + if (next_string) { + next_string->string.insert(0, str); + break; + } + for ( ; ; ) { // go up one item in the xml + test_item = test_item->parent; + if (is_line_break_object(test_item)) { + break; + } + if (SP_IS_FLOWTEXT(test_item)) { + return false; + } + SPObject *next = test_item->getNext(); + if (next) { + test_item = next; + break; + } + } + if (is_line_break_object(test_item)) { // no next string, see if there's a prev string + next_string = find_last_string_child_not_equal_to(test_item, *item); + if (next_string == NULL) { + return false; // an empty paragraph + } + next_string->string = str + next_string->string; + break; + } + } + next_string->getRepr()->setContent(next_string->string.c_str()); + SPObject *delete_obj = *item; + *item = (*item)->getNext(); + delete_obj->deleteObject(); + return true; +} +#endif + +/* possible tidy operators that are not yet implemented, either because +they are difficult, occur infrequently, or because I'm not sure that the +output is tidier in all cases: + duplicate styles in line break elements: <div italic><para italic>abc</para></div> + -> <div italic><para>abc</para></div> + style inversion: <font a>abc<font b>def<font a>ghi</font>jkl</font>mno</font> + -> <font a>abc<font b>def</font>ghi<font b>jkl</font>mno</font> + mistaken precedence: <font a,size 1>abc</font><size 1>def</size> + -> <size 1><font a>abc</font>def</size> +*/ + +/** Recursively walks the xml tree calling a set of cleanup operations on +every child. Returns true if any changes were made to the tree. + +All the tidy operators return true if they made changes, and alter their +parameter to point to the next object that should be processed, or NULL. +They must not significantly alter (ie delete) any ancestor elements of the +one they are passed. + +It may be that some of the later tidy operators that I wrote are actually +general cases of the earlier operators, and hence the special-case-only +versions can be removed. I haven't analysed my work in detail to figure +out if this is so. */ +static bool tidy_xml_tree_recursively(SPObject *root, bool has_text_decoration) +{ + gchar const *root_style = (root)->getRepr()->attribute("style"); + if(root_style && strstr(root_style,"text-decoration"))has_text_decoration = true; + static bool (* const tidy_operators[])(SPObject**, bool) = { + tidy_operator_empty_spans, + tidy_operator_inexplicable_spans, + tidy_operator_repeated_spans, + tidy_operator_excessive_nesting, + tidy_operator_redundant_double_nesting, + tidy_operator_redundant_semi_nesting + }; + bool changes = false; + + for (SPObject *child = root->firstChild() ; child != nullptr ; ) { + if (SP_IS_FLOWREGION(child) || SP_IS_FLOWREGIONEXCLUDE(child) || SP_IS_TREF(child)) { + child = child->getNext(); + continue; + } + if (child->hasChildren()) { + changes |= tidy_xml_tree_recursively(child, has_text_decoration); + } + + unsigned i; + for (i = 0 ; i < sizeof(tidy_operators) / sizeof(tidy_operators[0]) ; i++) { + if (tidy_operators[i](&child, has_text_decoration)) { + changes = true; + break; + } + } + if (i == sizeof(tidy_operators) / sizeof(tidy_operators[0])) { + child = child->getNext(); + } + } + return changes; +} + +/** Applies the given CSS fragment to the characters of the given text or +flowtext object between \a start and \a end, creating or removing span +elements as necessary and optimal. */ +void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css) +{ + // in the comments in the code below, capital letters are inside the application region, lowercase are outside + if (start == end) return; + Inkscape::Text::Layout::iterator first, last; + if (start < end) { + first = start; + last = end; + } else { + first = end; + last = start; + } + Inkscape::Text::Layout const *layout = te_get_layout(text); + SPObject *start_item = nullptr, *end_item = nullptr; + Glib::ustring::iterator start_text_iter, end_text_iter; + layout->getSourceOfCharacter(first, &start_item, &start_text_iter); + layout->getSourceOfCharacter(last, &end_item, &end_text_iter); + if (start_item == nullptr) { + return; // start is at end of text + } + if (is_line_break_object(start_item)) { + start_item = start_item->getNext(); + } + if (is_line_break_object(end_item)) { + end_item = end_item->getNext(); + } + if (end_item == nullptr) { + end_item = text; + } + + + /* Special case: With a tref, we only want to change its style when the whole + * string is selected, in which case the style can be applied directly to the + * tref node. If only part of the tref's string child is selected, just return. */ + + if (!sp_tref_fully_contained(start_item, start_text_iter, end_item, end_text_iter)) { + + return; + } + + /* stage 1: applying the style. Go up to the closest common ancestor of + start and end and then semi-recursively apply the style to all the + objects in between. The semi-recursion is because it's only necessary + at the beginning and end; the style can just be applied to the root + child in the middle. + eg: <span>abcDEF</span><span>GHI</span><span>JKLmno</span> + The recursion may involve creating new spans. + */ + SPObject *common_ancestor = get_common_ancestor(text, start_item, end_item); + + // bug #168370 (consider parent transform and viewBox) + // snipplet copied from desktop-style.cpp sp_desktop_apply_css_recursive(...) + SPCSSAttr *css_set = sp_repr_css_attr_new(); + sp_repr_css_merge(css_set, const_cast<SPCSSAttr*>(css)); + { + Geom::Affine const local(SP_ITEM(common_ancestor)->i2doc_affine()); + double const ex(local.descrim()); + if ( ( ex != 0. ) + && ( ex != 1. ) ) { + sp_css_attr_scale(css_set, 1/ex); + } + } + + start_item = ascend_while_first(start_item, start_text_iter, common_ancestor); + end_item = ascend_while_first(end_item, end_text_iter, common_ancestor); + recursively_apply_style(common_ancestor, css_set, start_item, start_text_iter, end_item, end_text_iter, span_name_for_text_object(text)); + sp_repr_css_attr_unref(css_set); + + /* stage 2: cleanup the xml tree (of which there are multiple passes) */ + /* discussion: this stage requires a certain level of inventiveness because + it's not clear what the best representation is in many cases. An ideal + implementation would provide some sort of scoring function to rate the + ugliness of a given xml tree and try to reduce said function, but providing + the various possibilities to be rated is non-trivial. Instead, I have opted + for a multi-pass technique which simply recognises known-ugly patterns and + has matching routines for optimising the patterns it finds. It's reasonably + easy to add new pattern matching processors. If everything gets disastrous + and neither option can be made to work, a fallback could be to reduce + everything to a single level of nesting and drop all pretense of + roundtrippability. */ + bool has_text_decoration = false; + gchar const *root_style = (text)->getRepr()->attribute("style"); + if(root_style && strstr(root_style,"text-decoration")) has_text_decoration = true; + while (tidy_xml_tree_recursively(common_ancestor, has_text_decoration)){}; + + // if we only modified subobjects this won't have been automatically sent + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); +} + +bool is_part_of_text_subtree (SPObject *obj) +{ + return (SP_IS_TSPAN(obj) + || SP_IS_TEXT(obj) + || SP_IS_FLOWTEXT(obj) + || SP_IS_FLOWTSPAN(obj) + || SP_IS_FLOWDIV(obj) + || SP_IS_FLOWPARA(obj) + || SP_IS_FLOWLINE(obj) + || SP_IS_FLOWREGIONBREAK(obj)); +} + +bool is_top_level_text_object (SPObject *obj) +{ + return (SP_IS_TEXT(obj) + || SP_IS_FLOWTEXT(obj)); +} + +bool has_visible_text(SPObject *obj) +{ + bool hasVisible = false; + + if (SP_IS_STRING(obj) && !SP_STRING(obj)->string.empty()) { + hasVisible = true; // maybe we should also check that it's not all whitespace? + } else { + for (auto& child: obj->children) { + if (has_visible_text(const_cast<SPObject *>(&child))) { + hasVisible = true; + break; + } + } + } + + return hasVisible; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/text-editing.h b/src/text-editing.h new file mode 100644 index 0000000..f0faaef --- /dev/null +++ b/src/text-editing.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_TEXT_EDITING_H +#define SEEN_SP_TEXT_EDITING_H + +/* + * Text editing functions common for for text and flowtext + * + * Authors: + * bulia byak + * Richard Hughes + * + * Copyright (C) 2004-5 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> // std::pair +#include "libnrtype/Layout-TNG.h" +#include "text-tag-attributes.h" + +class SPCSSAttr; +class SPDesktop; +class SPItem; +class SPObject; +class SPStyle; + +typedef std::pair<Inkscape::Text::Layout::iterator, Inkscape::Text::Layout::iterator> iterator_pair; + + +Inkscape::Text::Layout const * te_get_layout (SPItem const *item); + +void te_update_layout_now_recursive(SPItem *item); + +/** Returns true if there are no visible characters on the canvas. */ +bool sp_te_output_is_empty(SPItem const *item); + +/** Returns true if the user has typed nothing in the text box. */ +bool sp_te_input_is_empty(SPObject const *item); + +/** Recursively gets the length of all the SPStrings at or below the given +\a item. Also adds 1 for each line break encountered. */ +unsigned sp_text_get_length(SPObject const *item); + +/** Recursively gets the length of all the SPStrings at or below the given +\a item, before and not including \a upto. Also adds 1 for each line break encountered. */ +unsigned sp_text_get_length_upto(SPObject const *item, SPObject const *upto); + +std::vector<Geom::Point> sp_te_create_selection_quads(SPItem const *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, Geom::Affine const &transform); + +Inkscape::Text::Layout::iterator sp_te_get_position_by_coords (SPItem const *item, Geom::Point const &i_p); +void sp_te_get_cursor_coords (SPItem const *item, Inkscape::Text::Layout::iterator const &position, Geom::Point &p0, Geom::Point &p1); +double sp_te_get_average_linespacing (SPItem *text); + +SPStyle const * sp_te_style_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position); +SPObject const * sp_te_object_at_position(SPItem const *text, Inkscape::Text::Layout::iterator const &position); + +Inkscape::Text::Layout::iterator sp_te_insert(SPItem *item, Inkscape::Text::Layout::iterator const &position, char const *utf8); +Inkscape::Text::Layout::iterator sp_te_replace(SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, char const *utf8); +Inkscape::Text::Layout::iterator sp_te_insert_line (SPItem *text, Inkscape::Text::Layout::iterator &position); +bool sp_te_delete (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, iterator_pair &iter_pair); + +char *sp_te_get_string_multiline(SPItem const *text); +Glib::ustring sp_te_get_string_multiline(SPItem const *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end); +void sp_te_set_repr_text_multiline(SPItem *text, char const *str); + +TextTagAttributes* +text_tag_attributes_at_position(SPItem *item, Inkscape::Text::Layout::iterator const &position, unsigned *char_index); + +void sp_te_adjust_kerning_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, Geom::Point by); +void sp_te_adjust_dx (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double delta); +void sp_te_adjust_dy (SPItem *item, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double delta); + +void sp_te_adjust_rotation_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double pixels); +void sp_te_adjust_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double degrees); +void sp_te_set_rotation(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double degrees); + +void sp_te_adjust_tspan_letterspacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double by); +void sp_te_adjust_linespacing_screen(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPDesktop *desktop, double by); +void sp_te_apply_style(SPItem *text, Inkscape::Text::Layout::iterator const &start, Inkscape::Text::Layout::iterator const &end, SPCSSAttr const *css); + +bool is_part_of_text_subtree (SPObject *obj); +bool is_top_level_text_object (SPObject *obj); +bool has_visible_text (SPObject *obj); + +#endif // SEEN_SP_TEXT_EDITING_H diff --git a/src/text-tag-attributes.h b/src/text-tag-attributes.h new file mode 100644 index 0000000..7a57d36 --- /dev/null +++ b/src/text-tag-attributes.h @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief contains and manages the attributes common to all types of text tag + * + * The five attributes x, y, dx, dy and rotate (todo: textlength, lengthadjust) + * are permitted on all of text, tspan and textpath elements so we need a class + * to abstract the management of those attributes from the actual type of the + * element. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_TEXT_TAG_ATTRIBUTES_H +#define INKSCAPE_TEXT_TAG_ATTRIBUTES_H + +#include <utility> +#include <vector> +#include <glib.h> +#include "libnrtype/Layout-TNG.h" +#include "svg/svg-length.h" + +namespace Inkscape { +namespace XML { +class Node; +} +} + +class TextTagAttributes { +public: + TextTagAttributes() = default; + TextTagAttributes(Inkscape::Text::Layout::OptionalTextTagAttrs attrs) + : attributes(std::move(attrs)) {} + + /// Fill in all the fields of #attributes from the given node. + void readFrom(Inkscape::XML::Node const *node); + + /** Process the parameters from the set() function of SPObject. + Returns true if \a key was a recognised attribute. */ + bool readSingleAttribute(unsigned key, gchar const *value, SPStyle const *style, Geom::Rect const *viewport); + + /// Write out all the contents of #attributes to the given node. + void writeTo(Inkscape::XML::Node *node) const; + + /// Update relative values + void update( double em, double ex, double w, double h ); + + /** For tspan role=line elements we should not use the set x,y + coordinates since that would overrule the values calculated by the + text layout engine, however if there are more than one element in + the x or y vectors we can presume that the user set them and hence + they should be copied. This function detects that condition so the + \a use_xy parameter to mergeInto() can be set correctly. */ + bool singleXYCoordinates() const; + + /** Returns false if all of the vectors are zero length. */ + bool anyAttributesSet() const; + + /** Implements the rules for overlaying the contents of the class + (treated as the child object) on top of previously existing + attributes from \a parent_attrs using the rules described in + SVG 1.1 section 10.5. \a parent_attrs_offset can be used to require + that only fields from \a parent_attrs starting at that index will + be used. Basically, the algorithm is that if a child attribute + exists that will be used, otherwise the parent attribute will be used, + otherwise the vector will end. textLength is never merged with parent. */ + void mergeInto(Inkscape::Text::Layout::OptionalTextTagAttrs *output, Inkscape::Text::Layout::OptionalTextTagAttrs const &parent_attrs, unsigned parent_attrs_offset, bool copy_xy, bool copy_dxdyrotate) const; + + /** Deletes all the values from all the vectors beginning at + \a start_index and extending for \a n fields. This is what you want + to do when deleting characters from the corresponding text. */ + void erase(unsigned start_index, unsigned n); + + /** Inserts \a n new values in all the stored vectors at \a + start_index. This is what you want to do when inserting characters + in the corresponding text. If a vector is shorter than \a start_index + it will not be extended (the defaults are fine). dx, dy and rotate + will be extended with zero values, x and y will be extended with + linearly interpolated values. TODO: The inserted values should probably + be unset but sp_svg_length_list_read() can't cope with that. */ + void insert(unsigned start_index, unsigned n); + + /** Divides the stored attributes into two, at the given index. The + first section (0..index-1) stay in this object, the second section + (index..end) go in \a second. This function is generally used when + line breaking. */ + void split(unsigned index, TextTagAttributes *second); + + /** Overwrites all the attributes contained in this object with the + given parameters by putting \a first at the beginning, then the + contents of \a second after \a second_index. */ + void join(TextTagAttributes const &first, TextTagAttributes const &second, unsigned second_index); + + /** Applies the given transformation to the stored coordinates. Pairs + of x and y coordinates are multiplied by the matrix and the dx and dy + vectors are multiplied by the given parameters. rotate is not altered. + If \a extend_zero_length is true, then if the x or y vectors are empty + they will be made length 1 in order to store the newly calculated + position. */ + void transform(Geom::Affine const &matrix, double scale_x, double scale_y, bool extend_zero_length = false); + + /** Gets current value of dx vector at \a index. */ + double getDx(unsigned index); + + /** Gets current value of dy vector at \a index. */ + double getDy(unsigned index); + + /** Adds the given value to the dx vector at the given + \a index. The vector is extended if necessary. */ + void addToDx(unsigned index, double delta); + + /** Adds the given value to the dy vector at the given + \a index. The vector is extended if necessary. */ + void addToDy(unsigned index, double delta); + + /** Adds the given values to the dx and dy vectors at the given + \a index. The vectors are extended if necessary. */ + void addToDxDy(unsigned index, Geom::Point const &adjust); + + /** Gets current value of rotate vector at \a index. */ + double getRotate(unsigned index); + + /** Adds the given value to the rotate vector at the given \a index. The + vector is extended if necessary. Delta is measured in degrees, clockwise + positive. */ + void addToRotate(unsigned index, double delta); + + /** Sets rotate vector at the given \a index. The vector is extended if + necessary. Angle is measured in degrees, clockwise positive. */ + void setRotate(unsigned index, double angle); + + /** Returns the first coordinates in the x and y vectors. If either + is zero length, 0.0 is used for that coordinate. */ + Geom::Point firstXY() const; + + /** Sets the first coordinates in the x and y vectors. */ + void setFirstXY(Geom::Point &point); + + /** Gets first value in the x vector as an SVGLength. Not guaranteed to remain valid. */ + SVGLength* getFirstXLength(); + + /** Gets first value in the y vector as an SVGLength. Not guaranteed to remain valid. */ + SVGLength* getFirstYLength(); + + SVGLength *getTextLength() { return &(attributes.textLength); } + int getLengthAdjust() { return attributes.lengthAdjust; } + +private: + /// This holds the actual values. + Inkscape::Text::Layout::OptionalTextTagAttrs attributes; + + /** Does the reverse of readSingleAttribute(), converting a vector<> to + its SVG string representation and writing it in to \a node. Used by + writeTo(). */ + static void writeSingleAttributeVector(Inkscape::XML::Node *node, gchar const *key, std::vector<SVGLength> const &attr_vector); + + /** Writes a single length value to \a node. Used by + writeTo(). */ + static void writeSingleAttributeLength(Inkscape::XML::Node *node, gchar const *key, const SVGLength &length); + + /** Does mergeInto() for one member of #attributes. If \a overlay_list + is NULL then it does a simple copy of parent elements, starting at + \a parent_offset. */ + static void mergeSingleAttribute(std::vector<SVGLength> *output_list, std::vector<SVGLength> const &parent_list, unsigned parent_offset, std::vector<SVGLength> const *overlay_list = nullptr); + + /// Does the work for erase(). + static void eraseSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n); + + /// Does the work for insert(). + static void insertSingleAttribute(std::vector<SVGLength> *attr_vector, unsigned start_index, unsigned n, bool is_xy); + + /// Does the work for split(). + static void splitSingleAttribute(std::vector<SVGLength> *first_vector, unsigned index, std::vector<SVGLength> *second_vector, bool trimZeros); + + /// Does the work for join(). + static void joinSingleAttribute(std::vector<SVGLength> *dest_vector, std::vector<SVGLength> const &first_vector, std::vector<SVGLength> const &second_vector, unsigned second_index); +}; + + +#endif /* !INKSCAPE_TEXT_TAG_ATTRIBUTES_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/trace/CMakeLists.txt b/src/trace/CMakeLists.txt new file mode 100644 index 0000000..45b3125 --- /dev/null +++ b/src/trace/CMakeLists.txt @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(trace_SRC + filterset.cpp + imagemap.cpp + imagemap-gdk.cpp + quantize.cpp + siox.cpp + trace.cpp + + potrace/inkscape-potrace.cpp + autotrace/inkscape-autotrace.cpp + depixelize/inkscape-depixelize.cpp + + # ------- + # Headers + filterset.h + imagemap-gdk.h + imagemap.h + pool.h + quantize.h + siox.h + trace.h + + potrace/bitmap.h + potrace/inkscape-potrace.h + autotrace/inkscape-autotrace.h + depixelize/inkscape-depixelize.h +) + +add_inkscape_source("${trace_SRC}") + diff --git a/src/trace/README b/src/trace/README new file mode 100644 index 0000000..f436aa0 --- /dev/null +++ b/src/trace/README @@ -0,0 +1,18 @@ + +This directory contains code for converting bitmap images to vector images. +The subdirectories contain code for the three tracers used in Inkscape: potrace +(external dependency), autotrace, and libdepixelize (currently in src/3rdparty). + + +To do: + +* Think about conceptually changing how the tracing works: Ideally, it should + be three steps clearly separated: + 1/ Preprocessing (color reduction, blurring, background removing, image + inversion...) + 2/ Tracing the preprocessed image, using some tracing engine + 3/ Post-processing (suppressing speckles, optimizing paths...) + + The main problem is that the tracing engine sometimes *also* does 1 and 3, so + there is some discussion to have whether this can be done. + diff --git a/src/trace/autotrace/inkscape-autotrace.cpp b/src/trace/autotrace/inkscape-autotrace.cpp new file mode 100644 index 0000000..156a7b3 --- /dev/null +++ b/src/trace/autotrace/inkscape-autotrace.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * This is the C++ glue between Inkscape and Autotrace + *//* + * + * Authors: + * Marc Jeanmougin + * + * Copyright (C) 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "inkscape-autotrace.h" + +extern "C" { +#include "3rdparty/autotrace/autotrace.h" +#include "3rdparty/autotrace/output.h" +#include "3rdparty/autotrace/spline.h" +} + +#include <glibmm/i18n.h> +#include <gtkmm/main.h> +#include <iomanip> + +#include "trace/filterset.h" +#include "trace/imagemap-gdk.h" +#include "trace/quantize.h" + +#include "desktop.h" +#include "message-stack.h" +#include <inkscape.h> + +#include "object/sp-path.h" + +#include <svg/path-string.h> + +using Glib::ustring; + +static void updateGui() +{ + //## Allow the GUI to update + Gtk::Main::iteration(false); // at least once, non-blocking + while (Gtk::Main::events_pending()) + Gtk::Main::iteration(); +} + +namespace Inkscape { + +namespace Trace { + +namespace Autotrace { + +static guchar* to_3channels(GdkPixbuf* input) { + int imgsize = gdk_pixbuf_get_height(input) * gdk_pixbuf_get_width(input); + guchar *out = (guchar*)malloc(3 * imgsize); + int x=0; + guchar* pix = gdk_pixbuf_get_pixels (input); + int rs = gdk_pixbuf_get_rowstride (input); + for(int row=0;row<gdk_pixbuf_get_height(input);row++) { + for (int col=0;col<gdk_pixbuf_get_width(input);col++) { + guchar alpha = *(pix + row * rs + col * 4 + 3); + guchar white = 255 - alpha; + for(int chan=0;chan<3;chan++) { + guchar *pnew = (pix + row * rs + col * 3 + chan); + guchar *pold = (pix + row * rs + col * 4 + chan); + out[x++] = (guchar)(((int)(*pold) * (int)alpha / 256) + white); + } + } + } + return out; +} + + +/** + * + */ +AutotraceTracingEngine::AutotraceTracingEngine() + : keepGoing(1) + , traceType(TRACE_OUTLINE) + , invert(false) +{ + /* get default parameters */ + opts = at_fitting_opts_new(); + opts->background_color = at_color_new(255,255,255); + autotrace_init(); +} + +AutotraceTracingEngine::~AutotraceTracingEngine() { at_fitting_opts_free(opts); } + + + +// TODO +Glib::RefPtr<Gdk::Pixbuf> AutotraceTracingEngine::preview(Glib::RefPtr<Gdk::Pixbuf> thePixbuf) { + //auto x = thePixbuf.copy(); + guchar *pb = to_3channels(thePixbuf->gobj()); + return Gdk::Pixbuf::create_from_data(pb, thePixbuf->get_colorspace(), false, 8, thePixbuf->get_width(), thePixbuf->get_height(), (thePixbuf->get_width()*3)); + +} + +int test_cancel (void* keepGoing){return !(* ((int*)keepGoing));} + +/** + * This is the working method of this interface, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return the path data that is compatible with the d="" attribute + * of an SVG <path> element. + */ +std::vector<TracingEngineResult> AutotraceTracingEngine::trace(Glib::RefPtr<Gdk::Pixbuf> pixbuf) +{ + GdkPixbuf *pb1 = pixbuf->gobj(); + guchar *pb = to_3channels(pb1); + + at_bitmap *bitmap = +// at_bitmap_new(gdk_pixbuf_get_width(pb), gdk_pixbuf_get_height(pb), gdk_pixbuf_get_n_channels(pb)); +// bitmap->bitmap = gdk_pixbuf_get_pixels(pb); + at_bitmap_new(gdk_pixbuf_get_width(pb1), gdk_pixbuf_get_height(pb1), 3); + bitmap->bitmap = pb; + + at_splines_type *splines = at_splines_new_full(bitmap, opts, NULL, NULL, NULL, NULL, test_cancel, &keepGoing); + // at_output_write_func wfunc = at_output_get_handler_by_suffix("svg"); + at_spline_writer *wfunc = at_output_get_handler_by_suffix("svg"); + + + int height = splines->height; + // const at_splines_type spline = *splines; + at_spline_list_array_type spline = *splines; + + unsigned this_list; + at_spline_list_type list; + at_color last_color = { 0, 0, 0 }; + + std::stringstream theStyle; + std::stringstream thePath; + char color[10]; + int nNodes = 0; + + std::vector<TracingEngineResult> res; + + // at_splines_write(wfunc, stdout, "", NULL, splines, NULL, NULL); + + for (this_list = 0; this_list < SPLINE_LIST_ARRAY_LENGTH(spline); this_list++) { + unsigned this_spline; + at_spline_type first; + + list = SPLINE_LIST_ARRAY_ELT(spline, this_list); + first = SPLINE_LIST_ELT(list, 0); + + if (this_list == 0 || !at_color_equal(&list.color, &last_color)) { + if (this_list > 0) { + if (!(spline.centerline || list.open)) { + thePath << "z"; + nNodes++; + } + TracingEngineResult ter(theStyle.str(), thePath.str(), nNodes); + res.push_back(ter); + theStyle.clear(); + thePath.clear(); + nNodes = 0; + } + sprintf(color, "#%02x%02x%02x;", list.color.r, list.color.g, list.color.b); + + theStyle << ((spline.centerline || list.open) ? "stroke:" : "fill:") << color + << ((spline.centerline || list.open) ? "fill:" : "stroke:") << "none"; + } + thePath << "M" << START_POINT(first).x << " " << height - START_POINT(first).y; + nNodes++; + for (this_spline = 0; this_spline < SPLINE_LIST_LENGTH(list); this_spline++) { + at_spline_type s = SPLINE_LIST_ELT(list, this_spline); + + if (SPLINE_DEGREE(s) == AT_LINEARTYPE) { + thePath << "L" << END_POINT(s).x << " " << height - END_POINT(s).y; + nNodes++; + } + else { + thePath << "C" << CONTROL1(s).x << " " << height - CONTROL1(s).y << " " << CONTROL2(s).x << " " + << height - CONTROL2(s).y << " " << END_POINT(s).x << " " << height - END_POINT(s).y; + nNodes++; + } + last_color = list.color; + } + } + if (!(spline.centerline || list.open)) + thePath << "z"; + nNodes++; + if (SPLINE_LIST_ARRAY_LENGTH(spline) > 0) { + TracingEngineResult ter(theStyle.str(), thePath.str(), nNodes); + res.push_back(ter); + theStyle.clear(); + thePath.clear(); + nNodes = 0; + } + + return res; +} + + +/** + * Abort the thread that is executing getPathDataFromPixbuf() + */ +void AutotraceTracingEngine::abort() +{ + // g_message("PotraceTracingEngine::abort()\n"); + keepGoing = 0; +} + + + +} // namespace Autotrace +} // namespace Trace +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/autotrace/inkscape-autotrace.h b/src/trace/autotrace/inkscape-autotrace.h new file mode 100644 index 0000000..f12eb35 --- /dev/null +++ b/src/trace/autotrace/inkscape-autotrace.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * This is the C++ glue between Inkscape and Autotrace + *//* + * + * Authors: + * Marc Jeanmougin + * + * Copyright (C) 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Autotrace is available at http://github.com/autotrace/autotrace. + * + */ + +#ifndef __INKSCAPE_AUTOTRACE_H__ +#define __INKSCAPE_AUTOTRACE_H__ + +#include "3rdparty/autotrace/autotrace.h" +#include <trace/trace.h> + +namespace Inkscape { + +namespace Trace { + +namespace Autotrace { +enum TraceType { TRACE_CENTERLINE, TRACE_OUTLINE }; + +class AutotraceTracingEngine : public TracingEngine { + + public: + /** + * + */ + AutotraceTracingEngine(); + + /** + * + */ + ~AutotraceTracingEngine() override; + + + /** + * Sets/gets parameters + */ + // TODO + + /** + * This is the working method of this implementing class, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return the path data that is compatible with the d="" attribute + * of an SVG <path> element. + */ + std::vector<TracingEngineResult> trace(Glib::RefPtr<Gdk::Pixbuf> pixbuf) override; + + /** + * Abort the thread that is executing getPathDataFromPixbuf() + */ + void abort() override; + + /** + * + */ + Glib::RefPtr<Gdk::Pixbuf> preview(Glib::RefPtr<Gdk::Pixbuf> pixbuf); + + /** + * + */ + int keepGoing; + + //private: + // autotrace_param_t *autotraceParams; + TraceType traceType; + at_fitting_opts_type *opts; + + //## do I invert at the end? + bool invert; + +}; // class AutotraceTracingEngine + + + +} // namespace Autotrace +} // namespace Trace +} // namespace Inkscape + + +#endif //__INKSCAPE_POTRACE_H__ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/depixelize/inkscape-depixelize.cpp b/src/trace/depixelize/inkscape-depixelize.cpp new file mode 100644 index 0000000..23897f8 --- /dev/null +++ b/src/trace/depixelize/inkscape-depixelize.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the C++ glue between Inkscape and Potrace + * + * Authors: + * Bob Jamison <rjamison@titan.com> + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Potrace, the wonderful tracer located at http://potrace.sourceforge.net, + * is provided by the generosity of Peter Selinger, to whom we are grateful. + * + */ + +#include "inkscape-depixelize.h" + +#include <glibmm/i18n.h> +#include <gtkmm/main.h> +#include <gtkmm.h> +#include <iomanip> + +#include "desktop.h" +#include "message-stack.h" +#include "helper/geom.h" +#include "object/sp-path.h" + +#include <svg/path-string.h> +#include <svg/svg.h> +#include <svg/svg-color.h> +#include "svg/css-ostringstream.h" + +using Glib::ustring; + + + +namespace Inkscape { + +namespace Trace { + +namespace Depixelize { + + +/** + * + */ +DepixelizeTracingEngine::DepixelizeTracingEngine() + : keepGoing(1) + , traceType(TRACE_VORONOI) +{ + params = new ::Tracer::Kopf2011::Options(); +} + + + +DepixelizeTracingEngine::DepixelizeTracingEngine(TraceType traceType, double curves, int islands, int sparsePixels, + double sparseMultiplier) + : keepGoing(1) + , traceType(traceType) +{ + params = new ::Tracer::Kopf2011::Options(); + params->curvesMultiplier = curves; + params->islandsWeight = islands; + params->sparsePixelsRadius = sparsePixels; + params->sparsePixelsMultiplier = sparseMultiplier; + params->nthreads = Inkscape::Preferences::get()->getIntLimited("/options/threading/numthreads", +#ifdef HAVE_OPENMP + omp_get_num_procs(), +#else + 1, +#endif // HAVE_OPENMP + 1, 256); +} + +DepixelizeTracingEngine::~DepixelizeTracingEngine() { delete params; } + +std::vector<TracingEngineResult> DepixelizeTracingEngine::trace(Glib::RefPtr<Gdk::Pixbuf> pixbuf) +{ + if (pixbuf->get_width() > 256 || pixbuf->get_height() > 256) { + char *msg = _("Image looks too big. Process may take a while and it is" + " wise to save your document before continuing." + "\n\nContinue the procedure (without saving)?"); + Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL, true); + +// if (dialog.run() != Gtk::RESPONSE_OK) +// return; +// TODO + } + + ::Tracer::Splines splines; + + if (traceType == TRACE_VORONOI) + splines = ::Tracer::Kopf2011::to_voronoi(pixbuf, *params); + else + splines = ::Tracer::Kopf2011::to_splines(pixbuf, *params); + + std::vector<TracingEngineResult> res; + + for (::Tracer::Splines::const_iterator it = splines.begin(), end = splines.end(); it != end; ++it) { + gchar b[64]; + sp_svg_write_color(b, sizeof(b), + SP_RGBA32_U_COMPOSE(unsigned(it->rgba[0]), + unsigned(it->rgba[1]), + unsigned(it->rgba[2]), + unsigned(it->rgba[3]))); + Inkscape::CSSOStringStream osalpha; + osalpha << float(it->rgba[3]) / 255.; + gchar* style = g_strdup_printf("fill:%s;fill-opacity:%s;", b, osalpha.str().c_str()); + printf("%s\n", style); + TracingEngineResult r(style, sp_svg_write_path(it->pathVector), count_pathvector_nodes(it->pathVector)); + res.push_back(r); + g_free(style); + } + return res; +} + +void DepixelizeTracingEngine::abort() { keepGoing = 0; } + +Glib::RefPtr<Gdk::Pixbuf> DepixelizeTracingEngine::preview(Glib::RefPtr<Gdk::Pixbuf> pixbuf) { return pixbuf; } + + +} // namespace Depixelize +} // namespace Trace +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/depixelize/inkscape-depixelize.h b/src/trace/depixelize/inkscape-depixelize.h new file mode 100644 index 0000000..228e724 --- /dev/null +++ b/src/trace/depixelize/inkscape-depixelize.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the C++ glue between Inkscape and Potrace + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Potrace, the wonderful tracer located at http://potrace.sourceforge.net, + * is provided by the generosity of Peter Selinger, to whom we are grateful. + * + */ + +#ifndef __INKSCAPE_DEPIXTRACE_H__ +#define __INKSCAPE_DEPIXTRACE_H__ + +#include <trace/trace.h> +#include "3rdparty/libdepixelize/kopftracer2011.h" + +struct GrayMap_def; +typedef GrayMap_def GrayMap; + +namespace Inkscape { + +namespace Trace { + +namespace Depixelize { + +enum TraceType + { + TRACE_VORONOI, + TRACE_BSPLINES + }; + + +class DepixelizeTracingEngine : public TracingEngine +{ + + public: + + /** + * + */ + DepixelizeTracingEngine(); + DepixelizeTracingEngine(TraceType traceType, double curves, int islands, int sparsePixels, double sparseMultiplier); + + /** + * + */ + ~DepixelizeTracingEngine() override; + + /** + * This is the working method of this implementing class, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return the path data that is compatible with the d="" attribute + * of an SVG <path> element. + */ + std::vector<TracingEngineResult> trace( + Glib::RefPtr<Gdk::Pixbuf> pixbuf) override; + + /** + * Abort the thread that is executing getPathDataFromPixbuf() + */ + void abort() override; + + /** + * + */ + Glib::RefPtr<Gdk::Pixbuf> preview(Glib::RefPtr<Gdk::Pixbuf> pixbuf); + + /** + * + */ + int keepGoing; + + ::Tracer::Kopf2011::Options *params; + TraceType traceType; + +};//class PotraceTracingEngine + + + +} // namespace Depixelize +} // namespace Trace +} // namespace Inkscape + + +#endif //__INKSCAPE_TRACE_H__ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/filterset.cpp b/src/trace/filterset.cpp new file mode 100644 index 0000000..fae6d66 --- /dev/null +++ b/src/trace/filterset.cpp @@ -0,0 +1,422 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Some filters for Potrace in Inkscape + * + * Authors: + * Bob Jamison <rjamison@titan.com> + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstdio> +#include <cstdlib> + +#include "imagemap-gdk.h" +#include "filterset.h" +#include "quantize.h" + +/*######################################################################### +### G A U S S I A N (smoothing) +#########################################################################*/ + +/** + * + */ +static int gaussMatrix[] = +{ + 2, 4, 5, 4, 2, + 4, 9, 12, 9, 4, + 5, 12, 15, 12, 5, + 4, 9, 12, 9, 4, + 2, 4, 5, 4, 2 +}; + + +/** + * + */ +GrayMap *grayMapGaussian(GrayMap *me) +{ + int width = me->width; + int height = me->height; + int firstX = 2; + int lastX = width-3; + int firstY = 2; + int lastY = height-3; + + GrayMap *newGm = GrayMapCreate(width, height); + if (!newGm) + return nullptr; + + for (int y = 0 ; y<height ; y++) + { + for (int x = 0 ; x<width ; x++) + { + /* image boundaries */ + if (x<firstX || x>lastX || y<firstY || y>lastY) + { + newGm->setPixel(newGm, x, y, me->getPixel(me, x, y)); + continue; + } + + /* all other pixels */ + int gaussIndex = 0; + unsigned long sum = 0; + for (int i= y-2 ; i<=y+2 ; i++) + { + for (int j= x-2; j<=x+2 ; j++) + { + int weight = gaussMatrix[gaussIndex++]; + sum += me->getPixel(me, j, i) * weight; + } + } + sum /= 159; + newGm->setPixel(newGm, x, y, sum); + } + } + + return newGm; +} + + + + + +/** + * + */ +RgbMap *rgbMapGaussian(RgbMap *me) +{ + int width = me->width; + int height = me->height; + int firstX = 2; + int lastX = width-3; + int firstY = 2; + int lastY = height-3; + + RgbMap *newGm = RgbMapCreate(width, height); + if (!newGm) + return nullptr; + + for (int y = 0 ; y<height ; y++) + { + for (int x = 0 ; x<width ; x++) + { + /* image boundaries */ + if (x<firstX || x>lastX || y<firstY || y>lastY) + { + newGm->setPixelRGB(newGm, x, y, me->getPixel(me, x, y)); + continue; + } + + /* all other pixels */ + int gaussIndex = 0; + int sumR = 0; + int sumG = 0; + int sumB = 0; + for (int i= y-2 ; i<=y+2 ; i++) + { + for (int j= x-2; j<=x+2 ; j++) + { + int weight = gaussMatrix[gaussIndex++]; + RGB rgb = me->getPixel(me, j, i); + sumR += weight * (int)rgb.r; + sumG += weight * (int)rgb.g; + sumB += weight * (int)rgb.b; + } + } + RGB rout; + rout.r = ( sumR / 159 ) & 0xff; + rout.g = ( sumG / 159 ) & 0xff; + rout.b = ( sumB / 159 ) & 0xff; + newGm->setPixelRGB(newGm, x, y, rout); + } + } + + return newGm; + +} + + + + +/*######################################################################### +### C A N N Y E D G E D E T E C T I O N +#########################################################################*/ + + +static int sobelX[] = +{ + -1, 0, 1 , + -2, 0, 2 , + -1, 0, 1 +}; + +static int sobelY[] = +{ + 1, 2, 1 , + 0, 0, 0 , + -1, -2, -1 +}; + + + +/** + * Perform Sobel convolution on a GrayMap + */ +static GrayMap *grayMapSobel(GrayMap *gm, + double dLowThreshold, double dHighThreshold) +{ + int width = gm->width; + int height = gm->height; + int firstX = 1; + int lastX = width-2; + int firstY = 1; + int lastY = height-2; + + GrayMap *newGm = GrayMapCreate(width, height); + if (!newGm) + return nullptr; + + for (int y = 0 ; y<height ; y++) + { + for (int x = 0 ; x<width ; x++) + { + unsigned long sum = 0; + /* image boundaries */ + if (x<firstX || x>lastX || y<firstY || y>lastY) + { + sum = 0; + } + else + { + /* ### SOBEL FILTERING #### */ + long sumX = 0; + long sumY = 0; + int sobelIndex = 0; + for (int i= y-1 ; i<=y+1 ; i++) + { + for (int j= x-1; j<=x+1 ; j++) + { + sumX += gm->getPixel(gm, j, i) * + sobelX[sobelIndex++]; + } + } + + sobelIndex = 0; + for (int i= y-1 ; i<=y+1 ; i++) + { + for (int j= x-1; j<=x+1 ; j++) + { + sumY += gm->getPixel(gm, j, i) * + sobelY[sobelIndex++]; + } + } + /*### GET VALUE ### */ + sum = abs(sumX) + abs(sumY); + + if (sum > 765) + sum = 765; + +#if 0 + /*### GET ORIENTATION (slow, pedantic way) ### */ + double orient = 0.0; + if (sumX==0) + { + if (sumY==0) + orient = 0.0; + else if (sumY<0) + { + sumY = -sumY; + orient = 90.0; + } + else + orient = 90.0; + } + else + { + orient = 57.295779515 * atan2( ((double)sumY),((double)sumX) ); + if (orient < 0.0) + orient += 180.0; + } + + /*### GET EDGE DIRECTION ### */ + int edgeDirection = 0; + if (orient < 22.5) + edgeDirection = 0; + else if (orient < 67.5) + edgeDirection = 45; + else if (orient < 112.5) + edgeDirection = 90; + else if (orient < 157.5) + edgeDirection = 135; +#else + /*### GET EDGE DIRECTION (fast way) ### */ + int edgeDirection = 0; /*x,y=0*/ + if (sumX==0) + { + if (sumY!=0) + edgeDirection = 90; + } + else + { + /*long slope = sumY*1024/sumX;*/ + long slope = (sumY << 10)/sumX; + if (slope > 2472 || slope< -2472) /*tan(67.5)*1024*/ + edgeDirection = 90; + else if (slope > 414) /*tan(22.5)*1024*/ + edgeDirection = 45; + else if (slope < -414) /*-tan(22.5)*1024*/ + edgeDirection = 135; + } + +#endif + /* printf("%ld %ld %f %d\n", sumX, sumY, orient, edgeDirection); */ + + /*### Get two adjacent pixels in edge direction ### */ + unsigned long leftPixel; + unsigned long rightPixel; + if (edgeDirection == 0) + { + leftPixel = gm->getPixel(gm, x-1, y); + rightPixel = gm->getPixel(gm, x+1, y); + } + else if (edgeDirection == 45) + { + leftPixel = gm->getPixel(gm, x-1, y+1); + rightPixel = gm->getPixel(gm, x+1, y-1); + } + else if (edgeDirection == 90) + { + leftPixel = gm->getPixel(gm, x, y-1); + rightPixel = gm->getPixel(gm, x, y+1); + } + else /*135 */ + { + leftPixel = gm->getPixel(gm, x-1, y-1); + rightPixel = gm->getPixel(gm, x+1, y+1); + } + + /*### Compare current value to adjacent pixels ### */ + /*### if less that either, suppress it ### */ + if (sum < leftPixel || sum < rightPixel) + sum = 0; + else + { + unsigned long highThreshold = + (unsigned long)(dHighThreshold * 765.0); + unsigned long lowThreshold = + (unsigned long)(dLowThreshold * 765.0); + if (sum >= highThreshold) + sum = 765; /* EDGE. 3*255 this needs to be settable */ + else if (sum < lowThreshold) + sum = 0; /* NONEDGE */ + else + { + if ( gm->getPixel(gm, x-1, y-1)> highThreshold || + gm->getPixel(gm, x , y-1)> highThreshold || + gm->getPixel(gm, x+1, y-1)> highThreshold || + gm->getPixel(gm, x-1, y )> highThreshold || + gm->getPixel(gm, x+1, y )> highThreshold || + gm->getPixel(gm, x-1, y+1)> highThreshold || + gm->getPixel(gm, x , y+1)> highThreshold || + gm->getPixel(gm, x+1, y+1)> highThreshold) + sum = 765; /* EDGE fix me too */ + else + sum = 0; /* NONEDGE */ + } + } + + + }/* else */ + if (sum==0) /* invert light & dark */ + sum = 765; + else + sum = 0; + newGm->setPixel(newGm, x, y, sum); + }/* for (x) */ + }/* for (y) */ + + return newGm; +} + + + + + +/** + * + */ +GrayMap * +grayMapCanny(GrayMap *gm, double lowThreshold, double highThreshold) +{ + if (!gm) + return nullptr; + + GrayMap *cannyGm = grayMapSobel(gm, lowThreshold, highThreshold); + if (!cannyGm) + return nullptr; + /*cannyGm->writePPM(cannyGm, "canny.ppm");*/ + + return cannyGm; +} + + + +/*######################################################################### +### Q U A N T I Z A T I O N +#########################################################################*/ + +/** + * Experimental. Work on this later + */ +GrayMap *quantizeBand(RgbMap *rgbMap, int nrColors) +{ + + RgbMap *gaussMap = rgbMapGaussian(rgbMap); + //gaussMap->writePPM(gaussMap, "rgbgauss.ppm"); + + IndexedMap *qMap = rgbMapQuantize(gaussMap, nrColors); + //qMap->writePPM(qMap, "rgbquant.ppm"); + gaussMap->destroy(gaussMap); + + GrayMap *gm = GrayMapCreate(rgbMap->width, rgbMap->height); + + // RGB is quantized. There should now be a small set of (R+G+B) + for (int y=0 ; y<qMap->height ; y++) + { + for (int x=0 ; x<qMap->width ; x++) + { + RGB rgb = qMap->getPixelValue(qMap, x, y); + int sum = rgb.r + rgb.g + rgb.b; + if (sum & 1) + sum = 765; + else + sum = 0; + // printf("%d %d %d : %d\n", rgb.r, rgb.g, rgb.b, index); + gm->setPixel(gm, x, y, sum); + } + } + + qMap->destroy(qMap); + + return gm; +} + + +/*######################################################################### +### E N D O F F I L E +#########################################################################*/ + + + + + + + + + + diff --git a/src/trace/filterset.h b/src/trace/filterset.h new file mode 100644 index 0000000..d8da650 --- /dev/null +++ b/src/trace/filterset.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Some filters for Potrace in Inkscape + * + * Authors: + * Bob Jamison <rjamison@titan.com> + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILTERSET_H__ +#define __FILTERSET_H__ + +#include "imagemap.h" + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Apply gaussian blur to an GrayMap + */ +GrayMap *grayMapGaussian(GrayMap *gmap); + +/** + * Apply gaussian bluf to an RgbMap + */ +RgbMap *rgbMapGaussian(RgbMap *rgbmap); + +/** + * + */ +GrayMap *grayMapCanny(GrayMap *gmap, + double lowThreshold, double highThreshold); + +/** + * + */ +GrayMap *quantizeBand(RgbMap *rgbmap, int nrColors); + + + +#ifdef __cplusplus +} +#endif + + +#endif /* __FILTERSET_H__ */ + +/*######################################################################### +### E N D O F F I L E +#########################################################################*/ diff --git a/src/trace/imagemap-gdk.cpp b/src/trace/imagemap-gdk.cpp new file mode 100644 index 0000000..d1c1c4e --- /dev/null +++ b/src/trace/imagemap-gdk.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cstdlib> + +#include "imagemap-gdk.h" + + +/*######################################################################### +## G R A Y M A P +#########################################################################*/ + +GrayMap *gdkPixbufToGrayMap(GdkPixbuf *buf) +{ + if (!buf) + return nullptr; + + int width = gdk_pixbuf_get_width(buf); + int height = gdk_pixbuf_get_height(buf); + guchar *pixdata = gdk_pixbuf_get_pixels(buf); + int rowstride = gdk_pixbuf_get_rowstride(buf); + int n_channels = gdk_pixbuf_get_n_channels(buf); + + GrayMap *grayMap = GrayMapCreate(width, height); + if (!grayMap) + return nullptr; + + //### Fill in the odd cells with RGB values + int x,y; + int row = 0; + for (y=0 ; y<height ; y++) + { + guchar *p = pixdata + row; + for (x=0 ; x<width ; x++) + { + int alpha = (int)p[3]; + int white = 3 * (255-alpha); + unsigned long sample = (int)p[0] + (int)p[1] +(int)p[2]; + unsigned long bright = sample * alpha / 256 + white; + grayMap->setPixel(grayMap, x, y, bright); + p += n_channels; + } + row += rowstride; + } + + return grayMap; +} + +GdkPixbuf *grayMapToGdkPixbuf(GrayMap *grayMap) +{ + if (!grayMap) + return nullptr; + + guchar *pixdata = (guchar *) + malloc(sizeof(guchar) * grayMap->width * grayMap->height * 3); + if (!pixdata) + return nullptr; + + int n_channels = 3; + int rowstride = grayMap->width * 3; + + GdkPixbuf *buf = gdk_pixbuf_new_from_data(pixdata, GDK_COLORSPACE_RGB, + 0, 8, grayMap->width, grayMap->height, + rowstride, (GdkPixbufDestroyNotify)g_free, nullptr); + + //### Fill in the odd cells with RGB values + int x,y; + int row = 0; + for (y=0 ; y<grayMap->height ; y++) + { + guchar *p = pixdata + row; + for (x=0 ; x<grayMap->width ; x++) + { + unsigned long pix = grayMap->getPixel(grayMap, x, y) / 3; + p[0] = p[1] = p[2] = (guchar)(pix & 0xff); + p += n_channels; + } + row += rowstride; + } + + return buf; +} + + + +/*######################################################################### +## P A C K E D P I X E L M A P +#########################################################################*/ + +PackedPixelMap *gdkPixbufToPackedPixelMap(GdkPixbuf *buf) +{ + if (!buf) + return nullptr; + + int width = gdk_pixbuf_get_width(buf); + int height = gdk_pixbuf_get_height(buf); + guchar *pixdata = gdk_pixbuf_get_pixels(buf); + int rowstride = gdk_pixbuf_get_rowstride(buf); + int n_channels = gdk_pixbuf_get_n_channels(buf); + + PackedPixelMap *ppMap = PackedPixelMapCreate(width, height); + if (!ppMap) + return nullptr; + + //### Fill in the cells with RGB values + int x,y; + int row = 0; + for (y=0 ; y<height ; y++) + { + guchar *p = pixdata + row; + for (x=0 ; x<width ; x++) + { + int alpha = (int)p[3]; + int white = 255 - alpha; + int r = (int)p[0]; r = r * alpha / 256 + white; + int g = (int)p[1]; g = g * alpha / 256 + white; + int b = (int)p[2]; b = b * alpha / 256 + white; + + ppMap->setPixel(ppMap, x, y, r, g, b); + p += n_channels; + } + row += rowstride; + } + + return ppMap; +} + + +/*######################################################################### +## R G B M A P +#########################################################################*/ + +RgbMap *gdkPixbufToRgbMap(GdkPixbuf *buf) +{ + if (!buf) + return nullptr; + + int width = gdk_pixbuf_get_width(buf); + int height = gdk_pixbuf_get_height(buf); + guchar *pixdata = gdk_pixbuf_get_pixels(buf); + int rowstride = gdk_pixbuf_get_rowstride(buf); + int n_channels = gdk_pixbuf_get_n_channels(buf); + + RgbMap *rgbMap = RgbMapCreate(width, height); + if (!rgbMap) + return nullptr; + + //### Fill in the cells with RGB values + int x,y; + int row = 0; + for (y=0 ; y<height ; y++) + { + guchar *p = pixdata + row; + for (x=0 ; x<width ; x++) + { + int alpha = (int)p[3]; + int white = 255 - alpha; + int r = (int)p[0]; r = r * alpha / 256 + white; + int g = (int)p[1]; g = g * alpha / 256 + white; + int b = (int)p[2]; b = b * alpha / 256 + white; + + rgbMap->setPixel(rgbMap, x, y, r, g, b); + p += n_channels; + } + row += rowstride; + } + + return rgbMap; +} + + + +/*######################################################################### +## I N D E X E D M A P +#########################################################################*/ + + +GdkPixbuf *indexedMapToGdkPixbuf(IndexedMap *iMap) +{ + if (!iMap) + return nullptr; + + guchar *pixdata = (guchar *) + malloc(sizeof(guchar) * iMap->width * iMap->height * 3); + if (!pixdata) + return nullptr; + + int n_channels = 3; + int rowstride = iMap->width * 3; + + GdkPixbuf *buf = gdk_pixbuf_new_from_data(pixdata, GDK_COLORSPACE_RGB, + 0, 8, iMap->width, iMap->height, + rowstride, (GdkPixbufDestroyNotify)g_free, nullptr); + + //### Fill in the cells with RGB values + int x,y; + int row = 0; + for (y=0 ; y<iMap->height ; y++) + { + guchar *p = pixdata + row; + for (x=0 ; x<iMap->width ; x++) + { + RGB rgb = iMap->getPixelValue(iMap, x, y); + p[0] = rgb.r & 0xff; + p[1] = rgb.g & 0xff; + p[2] = rgb.b & 0xff; + p += n_channels; + } + row += rowstride; + } + + return buf; +} + +/*######################################################################### +## E N D O F F I L E +#########################################################################*/ diff --git a/src/trace/imagemap-gdk.h b/src/trace/imagemap-gdk.h new file mode 100644 index 0000000..d0eaf23 --- /dev/null +++ b/src/trace/imagemap-gdk.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __GRAYMAP_GDK_H__ +#define __GRAYMAP_GDK_H__ + +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + +#include "imagemap.h" + +#include <gdk-pixbuf/gdk-pixbuf.h> + +/*######################################################################### +### I M A G E M A P --- GDK +#########################################################################*/ + + + +#ifdef __cplusplus +extern "C" { +#endif + +GrayMap *gdkPixbufToGrayMap(GdkPixbuf *buf); +GdkPixbuf *grayMapToGdkPixbuf(GrayMap *grayMap); +PackedPixelMap *gdkPixbufToPackedPixelMap(GdkPixbuf *buf); +RgbMap *gdkPixbufToRgbMap(GdkPixbuf *buf); +GdkPixbuf *indexedMapToGdkPixbuf(IndexedMap *iMap); + + +#ifdef __cplusplus +} +#endif + + +#endif /* __GRAYMAP_GDK_H__ */ + +/*######################################################################### +### E N D O F F I L E +#########################################################################*/ diff --git a/src/trace/imagemap.cpp b/src/trace/imagemap.cpp new file mode 100644 index 0000000..796575a --- /dev/null +++ b/src/trace/imagemap.cpp @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cstdlib> + +#include "imagemap.h" + +#include "io/sys.h" + +/*######################################################################### +### G R A Y M A P +#########################################################################*/ + + +static void gSetPixel(GrayMap *me, int x, int y, unsigned long val) +{ + if (val>765) + val = 765; + unsigned long *pix = me->rows[y] + x; + *pix = val; +} + +static unsigned long gGetPixel(GrayMap *me, int x, int y) +{ + unsigned long *pix = me->rows[y] + x; + return *pix; +} + + +static int gWritePPM(GrayMap *me, char *fileName) +{ + if (!fileName) + return FALSE; + + FILE *f = fopen(fileName, "wb"); + if (!f) + return FALSE; + + fprintf(f, "P6 %d %d 255\n", me->width, me->height); + + for (int y=0 ; y<me->height; y++) + { + for (int x=0 ; x<me->width ; x++) + { + unsigned long pix = me->getPixel(me, x, y) / 3; + unsigned char pixb = (unsigned char) (pix & 0xff); + fputc(pixb, f); + fputc(pixb, f); + fputc(pixb, f); + } + } + fclose(f); + return TRUE; +} + + +static void gDestroy(GrayMap *me) +{ + if (me->pixels) + free(me->pixels); + if (me->rows) + free(me->rows); + free(me); +} + +GrayMap *GrayMapCreate(int width, int height) +{ + + GrayMap *me = (GrayMap *)malloc(sizeof(GrayMap)); + if (!me) + return nullptr; + + /** methods **/ + me->setPixel = gSetPixel; + me->getPixel = gGetPixel; + me->writePPM = gWritePPM; + me->destroy = gDestroy; + + /** fields **/ + me->width = width; + me->height = height; + me->pixels = (unsigned long *) + malloc(sizeof(unsigned long) * width * height); + if (!me->pixels) + { + free(me); + return nullptr; + } + me->rows = (unsigned long **) + malloc(sizeof(unsigned long *) * height); + if (!me->rows) + { + free(me->pixels); + free(me); + return nullptr; + } + + unsigned long *row = me->pixels; + for (int i=0 ; i<height ; i++) + { + me->rows[i] = row; + row += width; + } + + return me; +} + + + + + +/*######################################################################### +### P A C K E D P I X E L M A P +#########################################################################*/ + + + +static void ppSetPixel(PackedPixelMap *me, int x, int y, int r, int g, int b) +{ + unsigned long *pix = me->rows[y] + x; + *pix = (((unsigned long)r)<<16 & 0xff0000L) | + (((unsigned long)g)<< 8 & 0x00ff00L) | + (((unsigned long)b) & 0x0000ffL); +} + +static void ppSetPixelLong(PackedPixelMap *me, int x, int y, unsigned long rgb) +{ + unsigned long *pix = me->rows[y] + x; + *pix = rgb; +} + +static unsigned long ppGetPixel(PackedPixelMap *me, int x, int y) +{ + unsigned long *pix = me->rows[y] + x; + return *pix; +} + + + +static int ppWritePPM(PackedPixelMap *me, char *fileName) +{ + if (!fileName) + return FALSE; + + FILE *f = fopen(fileName, "wb"); + if (!f) + return FALSE; + + fprintf(f, "P6 %d %d 255\n", me->width, me->height); + + for (int y=0 ; y<me->height; y++) + { + for (int x=0 ; x<me->width ; x++) + { + unsigned long rgb = me->getPixel(me, x, y); + unsigned char r = (unsigned char) ((rgb>>16) & 0xff); + unsigned char g = (unsigned char) ((rgb>> 8) & 0xff); + unsigned char b = (unsigned char) ((rgb ) & 0xff); + fputc(r, f); + fputc(g, f); + fputc(b, f); + } + } + fclose(f); + return TRUE; +} + + +static void ppDestroy(PackedPixelMap *me) +{ + if (me->pixels) + free(me->pixels); + if (me->rows) + free(me->rows); + free(me); +} + + + +PackedPixelMap *PackedPixelMapCreate(int width, int height) +{ + + PackedPixelMap *me = (PackedPixelMap *)malloc(sizeof(PackedPixelMap)); + if (!me) + return nullptr; + + /** methods **/ + me->setPixel = ppSetPixel; + me->setPixelLong = ppSetPixelLong; + me->getPixel = ppGetPixel; + me->writePPM = ppWritePPM; + me->destroy = ppDestroy; + + + /** fields **/ + me->width = width; + me->height = height; + me->pixels = (unsigned long *) malloc(sizeof(unsigned long) * width * height); + if (!me->pixels){ + free(me); + return nullptr; + } + me->rows = (unsigned long **) malloc(sizeof(unsigned long *) * height); + if (!me->rows){ + free(me->pixels); //allocated as me->pixels is not NULL here: see previous check + free(me); + return nullptr; + } + + unsigned long *row = me->pixels; + for (int i=0 ; i<height ; i++) + { + me->rows[i] = row; + row += width; + } + + return me; +} + + + +/*######################################################################### +### R G B M A P +#########################################################################*/ + + + +static void rSetPixel(RgbMap *me, int x, int y, int r, int g, int b) +{ + RGB *pix = me->rows[y] + x; + pix->r = r; + pix->g = g; + pix->b = b; +} + +static void rSetPixelRGB(RgbMap *me, int x, int y, RGB rgb) +{ + RGB *pix = me->rows[y] + x; + *pix = rgb; +} + +static RGB rGetPixel(RgbMap *me, int x, int y) +{ + RGB *pix = me->rows[y] + x; + return *pix; +} + + + +static int rWritePPM(RgbMap *me, char *fileName) +{ + if (!fileName) + return FALSE; + + FILE *f = fopen(fileName, "wb"); + if (!f) + return FALSE; + + fprintf(f, "P6 %d %d 255\n", me->width, me->height); + + for (int y=0 ; y<me->height; y++) + { + for (int x=0 ; x<me->width ; x++) + { + RGB rgb = me->getPixel(me, x, y); + fputc(rgb.r, f); + fputc(rgb.g, f); + fputc(rgb.b, f); + } + } + fclose(f); + return TRUE; +} + + +static void rDestroy(RgbMap *me) +{ + if (me->pixels){ + free(me->pixels); + } + if (me->rows){ + free(me->rows); + } + free(me); +} + + + +RgbMap *RgbMapCreate(int width, int height) +{ + + RgbMap *me = (RgbMap *)malloc(sizeof(RgbMap)); + if (!me){ + return nullptr; + } + + /** methods **/ + me->setPixel = rSetPixel; + me->setPixelRGB = rSetPixelRGB; + me->getPixel = rGetPixel; + me->writePPM = rWritePPM; + me->destroy = rDestroy; + + + /** fields **/ + me->width = width; + me->height = height; + me->pixels = (RGB *) malloc(sizeof(RGB) * width * height); + if (!me->pixels){ + free(me); + return nullptr; + } + me->rows = (RGB **) malloc(sizeof(RGB *) * height); + if (!me->rows){ + free(me->pixels); //allocated as me->pixels is not NULL here: see previous check + free(me); + return nullptr; + } + + RGB *row = me->pixels; + for (int i=0 ; i<height ; i++){ + me->rows[i] = row; + row += width; + } + + return me; +} + + + + +/*######################################################################### +### I N D E X E D M A P +#########################################################################*/ + + + +static void iSetPixel(IndexedMap *me, int x, int y, unsigned int index) +{ + unsigned int *pix = me->rows[y] + x; + *pix = index; +} + + +static unsigned int iGetPixel(IndexedMap *me, int x, int y) +{ + unsigned int *pix = me->rows[y] + x; + return *pix; +} + +static RGB iGetPixelValue(IndexedMap *me, int x, int y) +{ + unsigned int *pix = me->rows[y] + x; + RGB rgb = me->clut[((*pix)&0xff)]; + return rgb; +} + + + +static int iWritePPM(IndexedMap *me, char *fileName) +{ + if (!fileName) + return FALSE; + + FILE *f = fopen(fileName, "wb"); + if (!f) + return FALSE; + + fprintf(f, "P6 %d %d 255\n", me->width, me->height); + + for (int y=0 ; y<me->height; y++) + { + for (int x=0 ; x<me->width ; x++) + { + RGB rgb = me->getPixelValue(me, x, y); + fputc(rgb.r, f); + fputc(rgb.g, f); + fputc(rgb.b, f); + } + } + fclose(f); + return TRUE; +} + + +static void iDestroy(IndexedMap *me) +{ + if (me->pixels){ + free(me->pixels); + } + if (me->rows){ + free(me->rows); + } + free(me); +} + + + +IndexedMap *IndexedMapCreate(int width, int height) +{ + + IndexedMap *me = (IndexedMap *)malloc(sizeof(IndexedMap)); + if (!me) + return nullptr; + + /** methods **/ + me->setPixel = iSetPixel; + me->getPixel = iGetPixel; + me->getPixelValue = iGetPixelValue; + me->writePPM = iWritePPM; + me->destroy = iDestroy; + + + /** fields **/ + me->width = width; + me->height = height; + me->pixels = (unsigned int *) malloc(sizeof(unsigned int) * width * height); + if (!me->pixels){ + free(me); + return nullptr; + } + me->rows = (unsigned int **) malloc(sizeof(unsigned int *) * height); + if (!me->rows){ + free(me->pixels); //allocated as me->pixels is not NULL here: see previous check + free(me); + return nullptr; + } + + unsigned int *row = me->pixels; + for (int i=0 ; i<height ; i++){ + me->rows[i] = row; + row += width; + } + + me->nrColors = 0; + + RGB rgb; + rgb.r = rgb.g = rgb.b = 0; + for (auto & i : me->clut){ + i = rgb; + } + + return me; +} + + + + + + +/*######################################################################### +### E N D O F F I L E +#########################################################################*/ diff --git a/src/trace/imagemap.h b/src/trace/imagemap.h new file mode 100644 index 0000000..9da057a --- /dev/null +++ b/src/trace/imagemap.h @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __IMAGEMAP_H__ +#define __IMAGEMAP_H__ + +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + + +/*######################################################################### +### G R A Y M A P +#########################################################################*/ + + +typedef struct GrayMap_def GrayMap; + +#define GRAYMAP_BLACK 0 +#define GRAYMAP_WHITE 765 + +/** + * + */ +struct GrayMap_def +{ + + /*################# + ### METHODS + #################*/ + + /** + * + */ + void (*setPixel)(GrayMap *me, int x, int y, unsigned long val); + + /** + * + */ + unsigned long (*getPixel)(GrayMap *me, int x, int y); + + /** + * + */ + int (*writePPM)(GrayMap *me, char *fileName); + + + + /** + * + */ + void (*destroy)(GrayMap *me); + + + + /*################# + ### FIELDS + #################*/ + + /** + * + */ + int width; + + /** + * + */ + int height; + + /** + * The pixel array + */ + unsigned long *pixels; + + /** + * Pointer to the beginning of each row + */ + unsigned long **rows; + +}; + +#ifdef __cplusplus +extern "C" { +#endif + +GrayMap *GrayMapCreate(int width, int height); + +#ifdef __cplusplus +} +#endif + + + + +/*######################################################################### +### P A C K E D P I X E L M A P +#########################################################################*/ + + +typedef struct PackedPixelMap_def PackedPixelMap; + +/** + * + */ +struct PackedPixelMap_def +{ + + /*################# + ### METHODS + #################*/ + + /** + * + */ + void (*setPixel)(PackedPixelMap *me, int x, int y, int r, int g, int b); + + + /** + * + */ + void (*setPixelLong)(PackedPixelMap *me, int x, int y, unsigned long rgb); + + + /** + * + */ + unsigned long (*getPixel)(PackedPixelMap *me, int x, int y); + + + /** + * + */ + int (*writePPM)(PackedPixelMap *me, char *fileName); + + + + /** + * + */ + void (*destroy)(PackedPixelMap *me); + + + + /*################# + ### FIELDS + #################*/ + + /** + * + */ + int width; + + /** + * + */ + int height; + + /** + * The allocated array of pixels + */ + unsigned long *pixels; + + /** + * Pointers to the beginning of each row of pixels + */ + unsigned long **rows; + + +}; + + + +#ifdef __cplusplus +extern "C" { +#endif + +PackedPixelMap *PackedPixelMapCreate(int width, int height); + +#ifdef __cplusplus +} +#endif + + + +/*######################################################################### +### R G B M A P +#########################################################################*/ + +struct RGB +{ + unsigned char r; + unsigned char g; + unsigned char b; +}; + + + +typedef struct RgbMap_def RgbMap; + +/** + * + */ +struct RgbMap_def +{ + + /*################# + ### METHODS + #################*/ + + /** + * + */ + void (*setPixel)(RgbMap *me, int x, int y, int r, int g, int b); + + + /** + * + */ + void (*setPixelRGB)(RgbMap *me, int x, int y, RGB rgb); + + /** + * + */ + RGB (*getPixel)(RgbMap *me, int x, int y); + + /** + * + */ + int (*writePPM)(RgbMap *me, char *fileName); + + + + /** + * + */ + void (*destroy)(RgbMap *me); + + + + /*################# + ### FIELDS + #################*/ + + /** + * + */ + int width; + + /** + * + */ + int height; + + /** + * The allocated array of pixels + */ + RGB *pixels; + + /** + * Pointers to the beginning of each row of pixels + */ + RGB **rows; + +}; + + + +#ifdef __cplusplus +extern "C" { +#endif + +RgbMap *RgbMapCreate(int width, int height); + +#ifdef __cplusplus +} +#endif + + + + +/*######################################################################### +### I N D E X E D M A P +#########################################################################*/ + + +typedef struct IndexedMap_def IndexedMap; + +/** + * + */ +struct IndexedMap_def +{ + + /*################# + ### METHODS + #################*/ + + /** + * + */ + void (*setPixel)(IndexedMap *me, int x, int y, unsigned int index); + + + /** + * + */ + unsigned int (*getPixel)(IndexedMap *me, int x, int y); + + /** + * + */ + RGB (*getPixelValue)(IndexedMap *me, int x, int y); + + /** + * + */ + int (*writePPM)(IndexedMap *me, char *fileName); + + + + /** + * + */ + void (*destroy)(IndexedMap *me); + + + + /*################# + ### FIELDS + #################*/ + + /** + * + */ + int width; + + /** + * + */ + int height; + + /** + * The allocated array of pixels + */ + unsigned int *pixels; + + /** + * Pointers to the beginning of each row of pixels + */ + unsigned int **rows; + + /** + * + */ + int nrColors; + + /** + * Color look up table + */ + RGB clut[256]; + +}; + + + +#ifdef __cplusplus +extern "C" { +#endif + +IndexedMap *IndexedMapCreate(int width, int height); + +#ifdef __cplusplus +} +#endif + + + + +#endif /* __IMAGEMAP_H__ */ + +/*######################################################################### +### E N D O F F I L E +#########################################################################*/ diff --git a/src/trace/pool.h b/src/trace/pool.h new file mode 100644 index 0000000..a6fed59 --- /dev/null +++ b/src/trace/pool.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Pool memory allocation + * + * Authors: + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// not thread safe (a pool cannot be shared by threads safely) + +/* +-- principle: + + - user operations on a pool of objects of type T are: + - T *draw() : obtain a unused slot to store an object T + - void drop(T *) : realease a slot + +-- implementation: + + - a pool for objects T is: + + * blocks[64] : an array of allocated blocks of memory: + |---0--> block with capacity 64 + |---1--> block with capacity 64 + |---2--> block with capacity 128 + |---3--> block with capacity 128 + |---4--> block with capacity 256 + |---5--> block with capacity 256 + |---6--> block with capacity 512 + |---7--> not yet allocated + : + |---k--> not yet allocated (future capacity ~ 2^(6+k/2)) + : + '--63--> not yet allocated + * cblock : the index of the next unallocated block (here 7). + * next : a pointer to an unused slot inside an allocated bloc + + - the first bytes of an unallocated slot inside a bloc are used to store a + pointer to some other unallocated slot. (this way, we keep a list of all + unused slots starting at <next>) + + - insertions and deletions in this list are done at the root <next>. + if <next> points to NULL (no slots are availlable) when a draw() + operation is performed a new block is allocated, and the unused slots + list is filled with the allocated slots. + + - memory is freed only at pool's deletion. + +*/ + +#include <cstdlib> + +template <typename T> +class pool { + + public: + + pool() + { + cblock = 0; + size = sizeof(T) > sizeof(void *) ? sizeof(T) : sizeof(void *); + next = nullptr; + for (auto & k : block) { + k = nullptr; + } + } + + ~pool() + { + for (int k = 0; k < cblock; k++) { + free(block[k]); + } + } + + T *draw() + { + if (!next) addblock(); + void *p = next; + next = *(void **)p; + return (T *) p; + } + + void drop(T *p) + { + *(void **)p = next; + next = (void *) p; + } + + private: + + int size; + int cblock; + void *block[64]; //enough to store unlimited number of objects, if 64 is changed: see constructor too + void *next; + + void addblock() + { + int i = cblock++; + int blocksize = 1 << (6 + (i/2)); + //printf("pool allocating block: %d (size:%d)...", i, blocksize);//debug + block[i] = (void *)malloc(blocksize * size); + if (!block[i]) throw std::bad_alloc(); + char *p = (char *)block[i]; + for (int k = 0; k < blocksize - 1; k++) + { + *(void**)p = (void *)(p + size); + p += size; + } + *(void **)p = next; + next = block[i]; + //printf("done\n");//debug + } + +}; + diff --git a/src/trace/potrace/bitmap.h b/src/trace/potrace/bitmap.h new file mode 100644 index 0000000..bb50b2d --- /dev/null +++ b/src/trace/potrace/bitmap.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * inline macros for accessing bitmaps + */ +/* Copyright (C) 2001-2015 Peter Selinger. + This file is part of Potrace. It is free software and it is covered + by the GNU General Public License. See the file COPYING for details. */ + +#ifndef BITMAP_H +#define BITMAP_H + +#include <cstring> +#include <cstdlib> +#include <cerrno> +#include <cstddef> + +/* The bitmap type is defined in potracelib.h */ +#include "potracelib.h" + +/* The present file defines some convenient macros and static inline + functions for accessing bitmaps. Since they only produce inline + code, they can be conveniently shared by the library and frontends, + if desired */ + +/* ---------------------------------------------------------------------- */ +/* some measurements */ + +#define BM_WORDSIZE ((int)sizeof(potrace_word)) +#define BM_WORDBITS (8*BM_WORDSIZE) +#define BM_HIBIT (((potrace_word)1)<<(BM_WORDBITS-1)) +#define BM_ALLBITS (~(potrace_word)0) + +/* macros for accessing pixel at index (x,y). U* macros omit the + bounds check. */ + +#define bm_scanline(bm, y) ((bm)->map + (ptrdiff_t)(y)*(ptrdiff_t)(bm)->dy) +#define bm_index(bm, x, y) (&bm_scanline(bm, y)[(x)/BM_WORDBITS]) +#define bm_mask(x) (BM_HIBIT >> ((x) & (BM_WORDBITS-1))) +#define bm_range(x, a) ((int)(x) >= 0 && (int)(x) < (a)) +#define bm_safe(bm, x, y) (bm_range(x, (bm)->w) && bm_range(y, (bm)->h)) +#define BM_UGET(bm, x, y) ((*bm_index(bm, x, y) & bm_mask(x)) != 0) +#define BM_USET(bm, x, y) (*bm_index(bm, x, y) |= bm_mask(x)) +#define BM_UCLR(bm, x, y) (*bm_index(bm, x, y) &= ~bm_mask(x)) +#define BM_UINV(bm, x, y) (*bm_index(bm, x, y) ^= bm_mask(x)) +#define BM_UPUT(bm, x, y, b) ((b) ? BM_USET(bm, x, y) : BM_UCLR(bm, x, y)) +#define BM_GET(bm, x, y) (bm_safe(bm, x, y) ? BM_UGET(bm, x, y) : 0) +#define BM_SET(bm, x, y) (bm_safe(bm, x, y) ? BM_USET(bm, x, y) : 0) +#define BM_CLR(bm, x, y) (bm_safe(bm, x, y) ? BM_UCLR(bm, x, y) : 0) +#define BM_INV(bm, x, y) (bm_safe(bm, x, y) ? BM_UINV(bm, x, y) : 0) +#define BM_PUT(bm, x, y, b) (bm_safe(bm, x, y) ? BM_UPUT(bm, x, y, b) : 0) + +/* free the given bitmap. Leaves errno untouched. */ +static inline void bm_free(potrace_bitmap_t *bm) { + if (bm) { + free(bm->map); + } + free(bm); +} + +/* return new un-initialized bitmap. NULL with errno on error. + Assumes w, h >= 0. */ +static inline potrace_bitmap_t *bm_new(int w, int h) { + potrace_bitmap_t *bm; + int dy = w == 0 ? 0 : (w - 1) / BM_WORDBITS + 1; + ptrdiff_t size = (ptrdiff_t)dy * (ptrdiff_t)h * (ptrdiff_t)BM_WORDSIZE; + + /* check for overflow error */ + if (size < 0 || (h != 0 && dy != 0 && size / h / dy != BM_WORDSIZE)) { + errno = ENOMEM; + return nullptr; + } + + bm = (potrace_bitmap_t *) malloc(sizeof(potrace_bitmap_t)); + if (!bm) { + return nullptr; + } + bm->w = w; + bm->h = h; + bm->dy = dy; + bm->map = (potrace_word *) malloc(size); + if (!bm->map) { + free(bm); + return nullptr; + } + return bm; +} + +/* clear the given bitmap. Set all bits to c. */ +static inline void bm_clear(potrace_bitmap_t *bm, int c) { + /* Note: if the bitmap was created with bm_new, then it is + guaranteed that size will fit into the ptrdiff_t type. */ + ptrdiff_t size = (ptrdiff_t)bm->dy * (ptrdiff_t)bm->h * (ptrdiff_t)BM_WORDSIZE; + memset(bm->map, c ? -1 : 0, size); +} + +/* duplicate the given bitmap. Return NULL on error with errno set. */ +static inline potrace_bitmap_t *bm_dup(const potrace_bitmap_t *bm) { + potrace_bitmap_t *bm1 = bm_new(bm->w, bm->h); + ptrdiff_t size = (ptrdiff_t)bm->dy * (ptrdiff_t)bm->h * (ptrdiff_t)BM_WORDSIZE; + if (!bm1) { + return nullptr; + } + memcpy(bm1->map, bm->map, size); + return bm1; +} + +/* invert the given bitmap. */ +static inline void bm_invert(potrace_bitmap_t *bm) { + ptrdiff_t i; + ptrdiff_t size = (ptrdiff_t)bm->dy * (ptrdiff_t)bm->h; + + for (i = 0; i < size; i++) { + bm->map[i] ^= BM_ALLBITS; + } +} + +#endif /* BITMAP_H */ diff --git a/src/trace/potrace/inkscape-potrace.cpp b/src/trace/potrace/inkscape-potrace.cpp new file mode 100644 index 0000000..710e5cf --- /dev/null +++ b/src/trace/potrace/inkscape-potrace.cpp @@ -0,0 +1,661 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the C++ glue between Inkscape and Potrace + * + * Authors: + * Bob Jamison <rjamison@titan.com> + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Potrace, the wonderful tracer located at http://potrace.sourceforge.net, + * is provided by the generosity of Peter Selinger, to whom we are grateful. + * + */ + +#include "inkscape-potrace.h" + +#include <glibmm/i18n.h> +#include <gtkmm/main.h> +#include <iomanip> + +#include "trace/filterset.h" +#include "trace/quantize.h" +#include "trace/imagemap-gdk.h" + +#include <inkscape.h> +#include "desktop.h" +#include "message-stack.h" + +#include "object/sp-path.h" + +#include <svg/path-string.h> +#include "bitmap.h" + +using Glib::ustring; + +static void updateGui() +{ + //## Allow the GUI to update + Gtk::Main::iteration(false); //at least once, non-blocking + while( Gtk::Main::events_pending() ) + Gtk::Main::iteration(); + +} + + +static void potraceStatusCallback(double /*progress*/, void *userData) /* callback fn */ +{ + updateGui(); + + if (!userData) + return; + + //g_message("progress: %f\n", progress); + + //Inkscape::Trace::Potrace::PotraceTracingEngine *engine = + // (Inkscape::Trace::Potrace::PotraceTracingEngine *)userData; +} + + +namespace { +ustring twohex( int value ) +{ + return ustring::format(std::hex, std::setfill(L'0'), std::setw(2), value); +} +} // namespace + + +//required by potrace +namespace Inkscape { + +namespace Trace { + +namespace Potrace { + + +/** + * + */ +PotraceTracingEngine::PotraceTracingEngine() : + keepGoing(1), + traceType(TRACE_BRIGHTNESS), + invert(false), + quantizationNrColors(8), + brightnessThreshold(0.45), + brightnessFloor(0), + cannyHighThreshold(0.65), + multiScanNrColors(8), + multiScanStack(true), + multiScanSmooth(false), + multiScanRemoveBackground(false) +{ + /* get default parameters */ + potraceParams = potrace_param_default(); + potraceParams->progress.callback = potraceStatusCallback; + potraceParams->progress.data = (void *)this; +} + +PotraceTracingEngine::PotraceTracingEngine(TraceType traceType, bool invert, int quantizationNrColors, double brightnessThreshold, double brightnessFloor, double cannyHighThreshold, int multiScanNrColors, bool multiScanStack, bool multiScanSmooth, bool multiScanRemoveBackground) : + keepGoing(1), traceType(traceType), invert(invert), quantizationNrColors(quantizationNrColors), brightnessThreshold(brightnessThreshold), brightnessFloor(brightnessFloor), cannyHighThreshold(cannyHighThreshold), multiScanNrColors(multiScanNrColors) , multiScanStack(multiScanStack), multiScanSmooth(multiScanSmooth), multiScanRemoveBackground(multiScanRemoveBackground) +{ + potraceParams = potrace_param_default(); + potraceParams->progress.callback = potraceStatusCallback; + potraceParams->progress.data = (void *)this; +} + + +PotraceTracingEngine::~PotraceTracingEngine() +{ + potrace_param_free(potraceParams); +} + + + + +struct Point +{ + double x; + double y; +}; + + +/** + * Check a point against a list of points to see if it + * has already occurred. + */ +static bool hasPoint(std::vector<Point> &points, double x, double y) +{ + for (auto p : points) + { + if (p.x == x && p.y == y) + return true; + } + return false; +} + + +/** + * Recursively descend the potrace_path_t node tree, writing paths in SVG + * format into the output stream. The Point vector is used to prevent + * redundant paths. Returns number of paths processed. + */ +static long writePaths(PotraceTracingEngine *engine, potrace_path_t *plist, + Inkscape::SVG::PathString& data, std::vector<Point> &points) +{ + long nodeCount = 0L; + + potrace_path_t *node; + for (node=plist; node ; node=node->sibling) + { + potrace_curve_t *curve = &(node->curve); + //g_message("node->fm:%d\n", node->fm); + if (!curve->n) + continue; + const potrace_dpoint_t *pt = curve->c[curve->n - 1]; + double x0 = 0.0; + double y0 = 0.0; + double x1 = 0.0; + double y1 = 0.0; + double x2 = pt[2].x; + double y2 = pt[2].y; + //Have we been here already? + if (hasPoint(points, x2, y2)) + { + //g_message("duplicate point: (%f,%f)\n", x2, y2); + continue; + } + else + { + Point p; + p.x = x2; p.y = y2; + points.push_back(p); + } + data.moveTo(x2, y2); + nodeCount++; + + for (int i=0 ; i<curve->n ; i++) + { + if (!engine->keepGoing) + return 0L; + pt = curve->c[i]; + x0 = pt[0].x; + y0 = pt[0].y; + x1 = pt[1].x; + y1 = pt[1].y; + x2 = pt[2].x; + y2 = pt[2].y; + switch (curve->tag[i]) + { + case POTRACE_CORNER: + data.lineTo(x1, y1).lineTo(x2, y2); + break; + case POTRACE_CURVETO: + data.curveTo(x0, y0, x1, y1, x2, y2); + break; + default: + break; + } + nodeCount++; + } + data.closePath(); + + for (potrace_path_t *child=node->childlist; child ; child=child->sibling) + { + nodeCount += writePaths(engine, child, data, points); + } + } + + return nodeCount; + +} + + +static GrayMap *filter(PotraceTracingEngine &engine, GdkPixbuf * pixbuf) +{ + if (!pixbuf) + return nullptr; + + GrayMap *newGm = nullptr; + + /*### Color quantization -- banding ###*/ + if (engine.traceType == TRACE_QUANT) + { + RgbMap *rgbmap = gdkPixbufToRgbMap(pixbuf); + //rgbMap->writePPM(rgbMap, "rgb.ppm"); + newGm = quantizeBand(rgbmap, + engine.quantizationNrColors); + rgbmap->destroy(rgbmap); + //return newGm; + } + + /*### Brightness threshold ###*/ + else if ( engine.traceType == TRACE_BRIGHTNESS || + engine.traceType == TRACE_BRIGHTNESS_MULTI ) + { + GrayMap *gm = gdkPixbufToGrayMap(pixbuf); + + newGm = GrayMapCreate(gm->width, gm->height); + double floor = 3.0 * + ( engine.brightnessFloor * 256.0 ); + double cutoff = 3.0 * + ( engine.brightnessThreshold * 256.0 ); + for (int y=0 ; y<gm->height ; y++) + { + for (int x=0 ; x<gm->width ; x++) + { + double brightness = (double)gm->getPixel(gm, x, y); + if (brightness >= floor && brightness < cutoff) + newGm->setPixel(newGm, x, y, GRAYMAP_BLACK); //black pixel + else + newGm->setPixel(newGm, x, y, GRAYMAP_WHITE); //white pixel + } + } + + gm->destroy(gm); + //newGm->writePPM(newGm, "brightness.ppm"); + //return newGm; + } + + /*### Canny edge detection ###*/ + else if (engine.traceType == TRACE_CANNY) + { + GrayMap *gm = gdkPixbufToGrayMap(pixbuf); + newGm = grayMapCanny(gm, 0.1, engine.cannyHighThreshold); + gm->destroy(gm); + //newGm->writePPM(newGm, "canny.ppm"); + //return newGm; + } + + /*### Do I invert the image? ###*/ + if (newGm && engine.invert) + { + for (int y=0 ; y<newGm->height ; y++) + { + for (int x=0 ; x<newGm->width ; x++) + { + unsigned long brightness = newGm->getPixel(newGm, x, y); + brightness = 765 - brightness; + newGm->setPixel(newGm, x, y, brightness); + } + } + } + + return newGm;//none of the above +} + + +static IndexedMap *filterIndexed(PotraceTracingEngine &engine, GdkPixbuf * pixbuf) +{ + if (!pixbuf) + return nullptr; + + IndexedMap *newGm = nullptr; + + RgbMap *gm = gdkPixbufToRgbMap(pixbuf); + if (engine.multiScanSmooth) + { + RgbMap *gaussMap = rgbMapGaussian(gm); + newGm = rgbMapQuantize(gaussMap, engine.multiScanNrColors); + gaussMap->destroy(gaussMap); + } + else + { + newGm = rgbMapQuantize(gm, engine.multiScanNrColors); + } + gm->destroy(gm); + + if (engine.traceType == TRACE_QUANT_MONO) + { + //Turn to grays + for (int i=0 ; i<newGm->nrColors ; i++) + { + RGB rgb = newGm->clut[i]; + int grayVal = (rgb.r + rgb.g + rgb.b) / 3; + rgb.r = rgb.g = rgb.b = grayVal; + newGm->clut[i] = rgb; + } + } + + return newGm; +} + + + + +Glib::RefPtr<Gdk::Pixbuf> +PotraceTracingEngine::preview(Glib::RefPtr<Gdk::Pixbuf> thePixbuf) +{ + GdkPixbuf *pixbuf = thePixbuf->gobj(); + + if ( traceType == TRACE_QUANT_COLOR || + traceType == TRACE_QUANT_MONO ) + { + IndexedMap *gm = filterIndexed(*this, pixbuf); + if (!gm) + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + + Glib::RefPtr<Gdk::Pixbuf> newBuf = + Glib::wrap(indexedMapToGdkPixbuf(gm), false); + + gm->destroy(gm); + + return newBuf; + } + else + { + GrayMap *gm = filter(*this, pixbuf); + if (!gm) + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + + Glib::RefPtr<Gdk::Pixbuf> newBuf = + Glib::wrap(grayMapToGdkPixbuf(gm), false); + + gm->destroy(gm); + + return newBuf; + } +} + + +//*This is the core inkscape-to-potrace binding +std::string PotraceTracingEngine::grayMapToPath(GrayMap *grayMap, long *nodeCount) +{ + if (!keepGoing) + { + g_warning("aborted"); + return ""; + } + + potrace_bitmap_t *potraceBitmap = bm_new(grayMap->width, grayMap->height); + bm_clear(potraceBitmap, 0); + + //##Read the data out of the GrayMap + for (int y=0 ; y<grayMap->height ; y++) + { + for (int x=0 ; x<grayMap->width ; x++) + { + BM_UPUT(potraceBitmap, x, y, + grayMap->getPixel(grayMap, x, y) ? 0 : 1); + } + } + + //##Debug + /* + FILE *f = fopen("poimage.pbm", "wb"); + bm_writepbm(f, bm); + fclose(f); + */ + + /* trace a bitmap*/ + potrace_state_t *potraceState = potrace_trace(potraceParams, + potraceBitmap); + + //## Free the Potrace bitmap + bm_free(potraceBitmap); + + if (!keepGoing) + { + g_warning("aborted"); + potrace_state_free(potraceState); + return ""; + } + + Inkscape::SVG::PathString data; + + //## copy the path information into our d="" attribute string + std::vector<Point> points; + long thisNodeCount = writePaths(this, potraceState->plist, data, points); + + /* free a potrace items */ + potrace_state_free(potraceState); + + if (!keepGoing) + return ""; + + if ( nodeCount) + *nodeCount = thisNodeCount; + + return data.string(); +} + + + +/** + * This is called for a single scan + */ +std::vector<TracingEngineResult> PotraceTracingEngine::traceSingle(GdkPixbuf * thePixbuf) +{ + + std::vector<TracingEngineResult> results; + + if (!thePixbuf) + return results; + + brightnessFloor = 0.0; //important to set this + + GrayMap *grayMap = filter(*this, thePixbuf); + if (!grayMap) + return results; + + long nodeCount = 0L; + std::string d = grayMapToPath(grayMap, &nodeCount); + + grayMap->destroy(grayMap); + + char const *style = "fill:#000000"; + + //g_message("### GOT '%s' \n", d); + TracingEngineResult result(style, d, nodeCount); + results.push_back(result); + + return results; +} + + +/** + * This allows routines that already generate GrayMaps to skip image filtering, + * increasing performance. + */ +std::vector<TracingEngineResult> PotraceTracingEngine::traceGrayMap(GrayMap *grayMap) +{ + + std::vector<TracingEngineResult> results; + + brightnessFloor = 0.0; //important to set this + + long nodeCount = 0L; + std::string d = grayMapToPath(grayMap, &nodeCount); + + char const *style = "fill:#000000"; + + //g_message("### GOT '%s' \n", d); + TracingEngineResult result(style, d, nodeCount); + results.push_back(result); + + return results; +} + +/** + * Called for multiple-scanning algorithms + */ +std::vector<TracingEngineResult> PotraceTracingEngine::traceBrightnessMulti(GdkPixbuf * thePixbuf) +{ + std::vector<TracingEngineResult> results; + + if ( thePixbuf ) { + double low = 0.2; //bottom of range + double high = 0.9; //top of range + double delta = (high - low ) / ((double)multiScanNrColors); + + brightnessFloor = 0.0; //Set bottom to black + + int traceCount = 0; + + for ( brightnessThreshold = low ; + brightnessThreshold <= high ; + brightnessThreshold += delta) { + GrayMap *grayMap = filter(*this, thePixbuf); + if ( grayMap ) { + long nodeCount = 0L; + std::string d = grayMapToPath(grayMap, &nodeCount); + + grayMap->destroy(grayMap); + + if ( !d.empty() ) { + //### get style info + int grayVal = (int)(256.0 * brightnessThreshold); + ustring style = ustring::compose("fill-opacity:1.0;fill:#%1%2%3", twohex(grayVal), twohex(grayVal), twohex(grayVal) ); + + //g_message("### GOT '%s' \n", style.c_str()); + TracingEngineResult result(style, d, nodeCount); + results.push_back(result); + + if (!multiScanStack) { + brightnessFloor = brightnessThreshold; + } + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + ustring msg = ustring::compose(_("Trace: %1. %2 nodes"), traceCount++, nodeCount); + desktop->getMessageStack()->flash(Inkscape::NORMAL_MESSAGE, msg); + } + } + } + } + + //# Remove the bottom-most scan, if requested + if (results.size() > 1 && multiScanRemoveBackground) { + results.erase(results.end() - 1); + } + } + + return results; +} + + +/** + * Quantization + */ +std::vector<TracingEngineResult> PotraceTracingEngine::traceQuant(GdkPixbuf * thePixbuf) +{ + std::vector<TracingEngineResult> results; + + if (thePixbuf) { + IndexedMap *iMap = filterIndexed(*this, thePixbuf); + if ( iMap ) { + //Create and clear a gray map + GrayMap *gm = GrayMapCreate(iMap->width, iMap->height); + for (int row=0 ; row<gm->height ; row++) { + for (int col=0 ; col<gm->width ; col++) { + gm->setPixel(gm, col, row, GRAYMAP_WHITE); + } + } + + for (int colorIndex=0 ; colorIndex<iMap->nrColors ; colorIndex++) { + // Make a gray map for each color index + for (int row=0 ; row<iMap->height ; row++) { + for (int col=0 ; col<iMap->width ; col++) { + int indx = (int) iMap->getPixel(iMap, col, row); + if (indx == colorIndex) { + gm->setPixel(gm, col, row, GRAYMAP_BLACK); //black + } else if (!multiScanStack) { + gm->setPixel(gm, col, row, GRAYMAP_WHITE); //white + } + } + } + + //## Now we have a traceable graymap + long nodeCount = 0L; + std::string d = grayMapToPath(gm, &nodeCount); + + if ( !d.empty() ) { + //### get style info + RGB rgb = iMap->clut[colorIndex]; + ustring style = ustring::compose("fill:#%1%2%3", twohex(rgb.r), twohex(rgb.g), twohex(rgb.b) ); + + //g_message("### GOT '%s' \n", style.c_str()); + TracingEngineResult result(style, d, nodeCount); + results.push_back(result); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + ustring msg = ustring::compose(_("Trace: %1. %2 nodes"), colorIndex, nodeCount); + desktop->getMessageStack()->flash(Inkscape::NORMAL_MESSAGE, msg); + } + } + }// for colorIndex + + gm->destroy(gm); + iMap->destroy(iMap); + } + + //# Remove the bottom-most scan, if requested + if (results.size() > 1 && multiScanRemoveBackground) { + results.erase(results.end() - 1); + } + } + + return results; +} + + +/** + * This is the working method of this interface, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return the path data that is compatible with the d="" attribute + * of an SVG <path> element. + */ +std::vector<TracingEngineResult> +PotraceTracingEngine::trace(Glib::RefPtr<Gdk::Pixbuf> pixbuf) +{ + + GdkPixbuf *thePixbuf = pixbuf->gobj(); + + //Set up for messages + keepGoing = 1; + + if ( traceType == TRACE_QUANT_COLOR || + traceType == TRACE_QUANT_MONO ) + { + return traceQuant(thePixbuf); + } + else if ( traceType == TRACE_BRIGHTNESS_MULTI ) + { + return traceBrightnessMulti(thePixbuf); + } + else + { + return traceSingle(thePixbuf); + } +} + + +/** + * Abort the thread that is executing getPathDataFromPixbuf() + */ +void PotraceTracingEngine::abort() +{ + //g_message("PotraceTracingEngine::abort()\n"); + keepGoing = 0; +} + + + + +} // namespace Potrace +} // namespace Trace +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/potrace/inkscape-potrace.h b/src/trace/potrace/inkscape-potrace.h new file mode 100644 index 0000000..1385521 --- /dev/null +++ b/src/trace/potrace/inkscape-potrace.h @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the C++ glue between Inkscape and Potrace + * + * Authors: + * Bob Jamison <rjamison@titan.com> + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Potrace, the wonderful tracer located at http://potrace.sourceforge.net, + * is provided by the generosity of Peter Selinger, to whom we are grateful. + * + */ + +#ifndef __INKSCAPE_POTRACE_H__ +#define __INKSCAPE_POTRACE_H__ + +#include <trace/trace.h> +#include <potracelib.h> + +struct GrayMap_def; +typedef GrayMap_def GrayMap; + +namespace Inkscape { + +namespace Trace { + +namespace Potrace { + +enum TraceType + { + TRACE_BRIGHTNESS, + TRACE_BRIGHTNESS_MULTI, + TRACE_CANNY, + TRACE_QUANT, + TRACE_QUANT_COLOR, + TRACE_QUANT_MONO + }; + + +class PotraceTracingEngine : public TracingEngine +{ + + public: + + /** + * + */ + PotraceTracingEngine(); + PotraceTracingEngine(TraceType traceType, bool invert, int quantizationNrColors, double brightnessThreshold, double brightnessFloor, double cannyHighThreshold, int multiScanNrColors, bool multiScanStack, bool multiScanSmooth, bool multiScanRemoveBackground); + + /** + * + */ + ~PotraceTracingEngine() override; + + /** + * This is the working method of this implementing class, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return the path data that is compatible with the d="" attribute + * of an SVG <path> element. + */ + std::vector<TracingEngineResult> trace( + Glib::RefPtr<Gdk::Pixbuf> pixbuf) override; + + /** + * Abort the thread that is executing getPathDataFromPixbuf() + */ + void abort() override; + + /** + * + */ + Glib::RefPtr<Gdk::Pixbuf> preview(Glib::RefPtr<Gdk::Pixbuf> pixbuf); + + /** + * + */ + int keepGoing; + + std::vector<TracingEngineResult>traceGrayMap(GrayMap *grayMap); + + potrace_param_t *potraceParams; + TraceType traceType; + + //## do I invert at the end? + bool invert; + + //## Color-->b&w quantization + int quantizationNrColors; + + //## brightness items + double brightnessThreshold; + double brightnessFloor; //usually 0.0 + + //## canny items + double cannyHighThreshold; + + //## Color-->multiscan quantization + int multiScanNrColors; + bool multiScanStack; //do we tile or stack? + bool multiScanSmooth;//do we use gaussian filter? + bool multiScanRemoveBackground; //do we remove the bottom trace? + + private: + /** + * This is the actual wrapper of the call to Potrace. nodeCount + * returns the count of nodes created. May be NULL if ignored. + */ + std::string grayMapToPath(GrayMap *gm, long *nodeCount); + + std::vector<TracingEngineResult>traceBrightnessMulti(GdkPixbuf *pixbuf); + std::vector<TracingEngineResult>traceQuant(GdkPixbuf *pixbuf); + std::vector<TracingEngineResult>traceSingle(GdkPixbuf *pixbuf); + + +};//class PotraceTracingEngine + + + +} // namespace Potrace +} // namespace Trace +} // namespace Inkscape + + +#endif //__INKSCAPE_POTRACE_H__ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/trace/quantize.cpp b/src/trace/quantize.cpp new file mode 100644 index 0000000..93dbb35 --- /dev/null +++ b/src/trace/quantize.cpp @@ -0,0 +1,597 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Quantization for Inkscape + * + * Authors: + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cassert> +#include <cstdio> +#include <cstdlib> +#include <new> + +#include "pool.h" +#include "imagemap.h" +#include "quantize.h" + +typedef struct Ocnode_def Ocnode; + +/** + * an octree node datastructure + */ +struct Ocnode_def +{ + Ocnode *parent; // parent node + Ocnode **ref; // node's reference + Ocnode *child[8]; // children + int nchild; // number of children + int width; // width level of this node + RGB rgb; // rgb's prefix of that node + unsigned long weight; // number of pixels this node accounts for + unsigned long rs, gs, bs; // sum of pixels colors this node accounts for + int nleaf; // number of leaves under this node + unsigned long mi; // minimum impact +}; + +/* +-- algorithm principle: + +- inspired by the octree method, we associate a tree to a given color map + +- nodes in those trees have this shape: + + parent + | + color_prefix(stored in rgb):width + colors_sum(stored in rs,gs,bs)/weight + / | \ + child1 child2 child3 + +- (grayscale) trees associated to pixels with colors 87 = 0b1010111 and + 69 = 0b1000101 are: + + . . <-- roots of the trees + | | + 1010111:0 and 1000101:0 <-- color prefixes, written in binary form + 87/1 69/1 <-- color sums, written in decimal form + +- the result of merging the two trees is: + + . + | + 10:5 <----- longest common prefix and binary width + 156/2 <---. of the covered color range. + / \ | + 1000101:0 1010111:0 '- sum of colors and quantity of pixels + 69/1 87/1 this node accounts for + + one should consider three cases when two trees are to be merged: + - one tree range is included in the range of the other one, and the first + tree has to be inserted as a child (or merged with the corresponding + child) of the other. + - their ranges are the same, and their children have to be merged under + a single root. + - ranges have no intersection, and a fork node has to be created (like in + the given example). + +- a tree for an image is built dividing the image in 2 parts and merging + the trees obtained recursively for the two parts. a tree for a one pixel + part is a leaf like one of those which were given above. + +- last, this tree is reduced a specified number of leaves, deleting first + leaves with minimal impact i.e. [ weight * 2^(2*parentwidth) ] value : + a fair approximation of the impact a leaf removal would have on the final + result : it's the corresponding covered area times the square of the + introduced color distance. + + deletion of a node A below a node with only two children is done as + follows : + + - when the brother is a leaf, the brother is deleted as well, both nodes + are then represented by their father. + + | | + . ==> . + / \ + A . + + - otherwise the deletion of A deletes also his father, which plays no + role anymore: + + | | + . ==> \ + / \ | + A . . + / \ / \ + + in that way, every leaf removal operation really decreases the remaining + total number of leaves by one. + +- very last, color indexes are attributed to leaves; associated colors are + averages, computed from weight and color components sums. + +-- improvements to the usual octree method: + +- since this algorithm shall often be used to perform quantization using a + very low (2-16) set of colors and not with a usual 256 value, we choose + more carefully which nodes are to be deleted. + +- depth of leaves is not fixed to an arbitrary number (which should be 8 + when color components are in 0-255), so there is no need to go down to a + depth of 8 for each pixel (at full precision), unless it is really + required. + +- tree merging also fastens the overall tree building, and intermediate + processing could be done. + +- a huge optimization against the stupid removal algorithm (i.e. find a best + match over the whole tree, remove it and do it again) was implemented: + nodes are marked with the minimal impact of the removal of a leaf below + it. we proceed to the removal recursively. we stop when current removal + level is above the current node minimal, otherwise reached leaves are + removed, and every change over minimal impacts is propagated back to the + whole tree when the recursion ends. + +-- specific optimizations + +- pool allocation is used to allocate nodes (increased performance on large + images). + +*/ + +inline RGB operator>>(RGB rgb, int s) +{ + RGB res; + res.r = rgb.r >> s; res.g = rgb.g >> s; res.b = rgb.b >> s; + return res; +} +inline bool operator==(RGB rgb1, RGB rgb2) +{ + return (rgb1.r == rgb2.r && rgb1.g == rgb2.g && rgb1.b == rgb2.b); +} + +inline int childIndex(RGB rgb) +{ + return (((rgb.r)&1)<<2) | (((rgb.g)&1)<<1) | (((rgb.b)&1)); +} + +/** + * allocate a new node + */ +inline Ocnode *ocnodeNew(pool<Ocnode> *pool) +{ + Ocnode *node = pool->draw(); + node->ref = nullptr; + node->parent = nullptr; + node->nchild = 0; + for (auto & i : node->child) i = nullptr; + node->mi = 0; + return node; +} + +inline void ocnodeFree(pool<Ocnode> *pool, Ocnode *node) { + pool->drop(node); +} + + +/** + * free a full octree + */ +static void octreeDelete(pool<Ocnode> *pool, Ocnode *node) +{ + if (!node) return; + for (auto & i : node->child) + octreeDelete(pool, i); + ocnodeFree(pool, node); +} + +/** + * pretty-print an octree, debugging purposes + */ +#if 0 +static void ocnodePrint(Ocnode *node, int indent) +{ + if (!node) return; + printf("width:%d weight:%lu rgb:%6x nleaf:%d mi:%lu\n", + node->width, + node->weight, + (unsigned int)( + ((node->rs / node->weight) << 16) + + ((node->gs / node->weight) << 8) + + (node->bs / node->weight)), + node->nleaf, + node->mi + ); + for (int i = 0; i < 8; i++) if (node->child[i]) + { + for (int k = 0; k < indent; k++) printf(" ");//indentation + printf("[%d:%p] ", i, node->child[i]); + ocnodePrint(node->child[i], indent+2); + } +} + +void octreePrint(Ocnode *node) +{ + printf("<<octree>>\n"); + if (node) printf("[r:%p] ", node); ocnodePrint(node, 2); +} +#endif + +/** + * builds a single <rgb> color leaf at location <ref> + */ +static void ocnodeLeaf(pool<Ocnode> *pool, Ocnode **ref, RGB rgb) +{ + assert(ref); + Ocnode *node = ocnodeNew(pool); + node->width = 0; + node->rgb = rgb; + node->rs = rgb.r; node->gs = rgb.g; node->bs = rgb.b; + node->weight = 1; + node->nleaf = 1; + node->mi = 0; + node->ref = ref; + *ref = node; +} + +/** + * merge nodes <node1> and <node2> at location <ref> with parent <parent> + */ +static int octreeMerge(pool<Ocnode> *pool, Ocnode *parent, Ocnode **ref, Ocnode *node1, Ocnode *node2) +{ + assert(ref); + if (!node1 && !node2) return 0; + assert(node1 != node2); + if (parent && !*ref) parent->nchild++; + if (!node1) + { + *ref = node2; node2->ref = ref; node2->parent = parent; + return node2->nleaf; + } + if (!node2) + { + *ref = node1; node1->ref = ref; node1->parent = parent; + return node1->nleaf; + } + int dwitdth = node1->width - node2->width; + if (dwitdth > 0 && node1->rgb == node2->rgb >> dwitdth) + { + //place node2 below node1 + { *ref = node1; node1->ref = ref; node1->parent = parent; } + int i = childIndex(node2->rgb >> (dwitdth - 1)); + node1->rs += node2->rs; node1->gs += node2->gs; node1->bs += node2->bs; + node1->weight += node2->weight; + node1->mi = 0; + if (node1->child[i]) node1->nleaf -= node1->child[i]->nleaf; + node1->nleaf += + octreeMerge(pool, node1, &node1->child[i], node1->child[i], node2); + return node1->nleaf; + } + else if (dwitdth < 0 && node2->rgb == node1->rgb >> (-dwitdth)) + { + //place node1 below node2 + { *ref = node2; node2->ref = ref; node2->parent = parent; } + int i = childIndex(node1->rgb >> (-dwitdth - 1)); + node2->rs += node1->rs; node2->gs += node1->gs; node2->bs += node1->bs; + node2->weight += node1->weight; + node2->mi = 0; + if (node2->child[i]) node2->nleaf -= node2->child[i]->nleaf; + node2->nleaf += + octreeMerge(pool, node2, &node2->child[i], node2->child[i], node1); + return node2->nleaf; + } + else + { + //nodes have either no intersection or the same root + Ocnode *newnode; + newnode = ocnodeNew(pool); + newnode->rs = node1->rs + node2->rs; + newnode->gs = node1->gs + node2->gs; + newnode->bs = node1->bs + node2->bs; + newnode->weight = node1->weight + node2->weight; + { *ref = newnode; newnode->ref = ref; newnode->parent = parent; } + if (dwitdth == 0 && node1->rgb == node2->rgb) + { + //merge the nodes in <newnode> + newnode->width = node1->width; // == node2->width + newnode->rgb = node1->rgb; // == node2->rgb + newnode->nchild = 0; + newnode->nleaf = 0; + if (node1->nchild == 0 && node2->nchild == 0) + newnode->nleaf = 1; + else + for (int i = 0; i < 8; i++) + if (node1->child[i] || node2->child[i]) + newnode->nleaf += + octreeMerge(pool, newnode, &newnode->child[i], + node1->child[i], node2->child[i]); + ocnodeFree(pool, node1); ocnodeFree(pool, node2); + return newnode->nleaf; + } + else + { + //use <newnode> as a fork node with children <node1> and <node2> + int newwidth = + node1->width > node2->width ? node1->width : node2->width; + RGB rgb1 = node1->rgb >> (newwidth - node1->width); + RGB rgb2 = node2->rgb >> (newwidth - node2->width); + //according to the previous tests <rgb1> != <rgb2> before the loop + while (!(rgb1 == rgb2)) + { rgb1 = rgb1 >> 1; rgb2 = rgb2 >> 1; newwidth++; }; + newnode->width = newwidth; + newnode->rgb = rgb1; // == rgb2 + newnode->nchild = 2; + newnode->nleaf = node1->nleaf + node2->nleaf; + int i1 = childIndex(node1->rgb >> (newwidth - node1->width - 1)); + int i2 = childIndex(node2->rgb >> (newwidth - node2->width - 1)); + node1->parent = newnode; + node1->ref = &newnode->child[i1]; + newnode->child[i1] = node1; + node2->parent = newnode; + node2->ref = &newnode->child[i2]; + newnode->child[i2] = node2; + return newnode->nleaf; + } + } +} + +/** + * upatade mi value for leaves + */ +static inline void ocnodeMi(Ocnode *node) +{ + node->mi = node->parent ? + node->weight << (2 * node->parent->width) : 0; +} + +/** + * remove leaves whose prune impact value is lower than <lvl>. at most + * <count> leaves are removed, and <count> is decreased on each removal. + * all parameters including minimal impact values are regenerated. + */ +static void ocnodeStrip(pool<Ocnode> *pool, Ocnode **ref, int *count, unsigned long lvl) +{ + Ocnode *node = *ref; + if (!count || !node) return; + assert(ref == node->ref); + if (node->nchild == 0) // leaf node + { + if (!node->mi) ocnodeMi(node); //mi generation may be required + if (node->mi > lvl) return; //leaf is above strip level + ocnodeFree(pool, node); + *ref = nullptr; + (*count)--; + } + else + { + if (node->mi && node->mi > lvl) //node is above strip level + return; + node->nchild = 0; + node->nleaf = 0; + node->mi = 0; + Ocnode **lonelychild = nullptr; + for (auto & i : node->child) if (i) + { + ocnodeStrip(pool, &i, count, lvl); + if (i) + { + lonelychild = &i; + node->nchild++; + node->nleaf += i->nleaf; + if (!node->mi || node->mi > i->mi) + node->mi = i->mi; + } + } + // tree adjustments + if (node->nchild == 0) + { + (*count)++; + node->nleaf = 1; + ocnodeMi(node); + } + else if (node->nchild == 1) + { + if ((*lonelychild)->nchild == 0) + { + //remove the <lonelychild> leaf under a 1 child node + node->nchild = 0; + node->nleaf = 1; + ocnodeMi(node); + ocnodeFree(pool, *lonelychild); + *lonelychild = nullptr; + } + else + { + //make a bridge to <lonelychild> over a 1 child node + (*lonelychild)->parent = node->parent; + (*lonelychild)->ref = ref; + ocnodeFree(pool, node); + *ref = *lonelychild; + } + } + } +} + +/** + * reduce the leaves of an octree to a given number + */ +static void octreePrune(pool<Ocnode> *pool, Ocnode **ref, int ncolor) +{ + assert(ref); + assert(ncolor > 0); + //printf("pruning down to %d colors:\n", ncolor);//debug + int n = (*ref)->nleaf - ncolor; + if (!*ref || n <= 0) return; + while (n > 0) + { + //printf("removals to go: %10d\t", n);//debug + //printf("current prune impact: %10lu\n", (*ref)->mi);//debug + //calling strip with global minimum impact of the tree + ocnodeStrip(pool, ref, &n, (*ref)->mi); + } +} + +/** + * build an octree associated to the area of a color map <rgbmap>, + * included in the specified (x1,y1)--(x2,y2) rectangle. + */ +static void octreeBuildArea(pool<Ocnode> *pool, RgbMap *rgbmap, Ocnode **ref, + int x1, int y1, int x2, int y2, int ncolor) +{ + int dx = x2 - x1, dy = y2 - y1; + int xm = x1 + dx/2, ym = y1 + dy/2; + Ocnode *ref1 = nullptr; + Ocnode *ref2 = nullptr; + if (dx == 1 && dy == 1) + ocnodeLeaf(pool, ref, rgbmap->getPixel(rgbmap, x1, y1)); + else if (dx > dy) + { + octreeBuildArea(pool, rgbmap, &ref1, x1, y1, xm, y2, ncolor); + octreeBuildArea(pool, rgbmap, &ref2, xm, y1, x2, y2, ncolor); + octreeMerge(pool, nullptr, ref, ref1, ref2); + } + else + { + octreeBuildArea(pool, rgbmap, &ref1, x1, y1, x2, ym, ncolor); + octreeBuildArea(pool, rgbmap, &ref2, x1, ym, x2, y2, ncolor); + octreeMerge(pool, nullptr, ref, ref1, ref2); + } + + //octreePrune(ref, 2*ncolor); + //affects result quality for almost same performance :/ +} + +/** + * build an octree associated to the <rgbmap> color map, + * pruned to <ncolor> colors. + */ +static Ocnode *octreeBuild(pool<Ocnode> *pool, RgbMap *rgbmap, int ncolor) +{ + //create the octree + Ocnode *node = nullptr; + octreeBuildArea(pool, + rgbmap, &node, + 0, 0, rgbmap->width, rgbmap->height, ncolor + ); + + //prune the octree + octreePrune(pool, &node, ncolor); + + //octreePrint(node);//debug + + return node; +} + +/** + * compute the color palette associated to an octree. + */ +static void octreeIndex(Ocnode *node, RGB *rgbpal, int *index) +{ + if (!node) return; + if (node->nchild == 0) + { + rgbpal[*index].r = node->rs / node->weight; + rgbpal[*index].g = node->gs / node->weight; + rgbpal[*index].b = node->bs / node->weight; + (*index)++; + } + else + for (auto & i : node->child) + if (i) + octreeIndex(i, rgbpal, index); +} + +/** + * compute the squared distance between two colors + */ +static int distRGB(RGB rgb1, RGB rgb2) +{ + return + (rgb1.r - rgb2.r) * (rgb1.r - rgb2.r) + + (rgb1.g - rgb2.g) * (rgb1.g - rgb2.g) + + (rgb1.b - rgb2.b) * (rgb1.b - rgb2.b); +} + +/** + * find the index of closest color in a palette + */ +static int findRGB(RGB *rgbpal, int ncolor, RGB rgb) +{ + //assert(ncolor > 0); + //assert(rgbpal); + int index = -1, dist = 0; + for (int k = 0; k < ncolor; k++) + { + int d = distRGB(rgbpal[k], rgb); + if (index == -1 || d < dist) { dist = d; index = k; } + } + return index; +} + +/** + * (qsort) compare two colors for brightness + */ +static int compRGB(const void *a, const void *b) +{ + RGB *ra = (RGB *)a, *rb = (RGB *)b; + return (ra->r + ra->g + ra->b) - (rb->r + rb->g + rb->b); +} + +/** + * quantize an RGB image to a reduced number of colors. + */ +IndexedMap *rgbMapQuantize(RgbMap *rgbmap, int ncolor) +{ + assert(rgbmap); + assert(ncolor > 0); + + IndexedMap *newmap = nullptr; + + pool<Ocnode> pool; + + Ocnode *tree = nullptr; + try { + tree = octreeBuild(&pool, rgbmap, ncolor); + } + catch (std::bad_alloc &ex) { + //should do smthg else? + } + + if ( tree ) { + RGB *rgbpal = new RGB[ncolor]; + int indexes = 0; + octreeIndex(tree, rgbpal, &indexes); + + octreeDelete(&pool, tree); + + // stacking with increasing contrasts + qsort((void *)rgbpal, indexes, sizeof(RGB), compRGB); + + // make the new map + newmap = IndexedMapCreate(rgbmap->width, rgbmap->height); + if (newmap) { + // fill in the color lookup table + for (int i = 0; i < indexes; i++) { + newmap->clut[i] = rgbpal[i]; + } + newmap->nrColors = indexes; + + // fill in new map pixels + for (int y = 0; y < rgbmap->height; y++) { + for (int x = 0; x < rgbmap->width; x++) { + RGB rgb = rgbmap->getPixel(rgbmap, x, y); + int index = findRGB(rgbpal, ncolor, rgb); + newmap->setPixel(newmap, x, y, index); + } + } + } + delete[] rgbpal; + } + + return newmap; +} diff --git a/src/trace/quantize.h b/src/trace/quantize.h new file mode 100644 index 0000000..43942f5 --- /dev/null +++ b/src/trace/quantize.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Quantization for Inkscape + * + * Authors: + * Stéphane Gimenez <dev@gim.name> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __QUANTIZE_H__ +#define __QUANTIZE_H__ + +#include "imagemap.h" + +/** + * Quantize an RGB image to a reduced number of colors. + */ +IndexedMap *rgbMapQuantize(RgbMap *rgbmap, int nrColors); + +#endif /* __QUANTIZE_H__ */ diff --git a/src/trace/siox.cpp b/src/trace/siox.cpp new file mode 100644 index 0000000..12d7c20 --- /dev/null +++ b/src/trace/siox.cpp @@ -0,0 +1,1733 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Copyright 2005, 2006 by Gerald Friedland, Kristian Jantz and Lars Knipping + + Conversion to C++ for Inkscape by Bob Jamison + + Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "siox.h" + +#include <cmath> +#include <cstdarg> +#include <map> +#include <algorithm> +#include <cstdlib> + + +namespace org +{ + +namespace siox +{ + + + +//######################################################################## +//# C L A B +//######################################################################## + +/** + * Convert integer A, R, G, B values into an pixel value. + */ +static unsigned long getRGB(int a, int r, int g, int b) +{ + if (a<0) a=0; + else if (a>255) a=255; + + if (r<0) r=0; + else if (r>255) r=255; + + if (g<0) g=0; + else if (g>255) g=255; + + if (b<0) b=0; + else if (b>255) b=255; + + return (a<<24)|(r<<16)|(g<<8)|b; +} + + + +/** + * Convert float A, R, G, B values (0.0-1.0) into an pixel value. + */ +static unsigned long getRGB(float a, float r, float g, float b) +{ + return getRGB((int)(a * 256.0), + (int)(r * 256.0), + (int)(g * 256.0), + (int)(b * 256.0)); +} + + + +//######################################### +//# Root approximations for large speedup. +//# By njh! +//######################################### +static const int ROOT_TAB_SIZE = 16; +static float cbrt_table[ROOT_TAB_SIZE +1]; + +double CieLab::cbrt(double x) +{ + double y = cbrt_table[int(x*ROOT_TAB_SIZE )]; // assuming x \in [0, 1] + y = (2.0 * y + x/(y*y))/3.0; + y = (2.0 * y + x/(y*y))/3.0; // polish twice + return y; +} + +static float qn_table[ROOT_TAB_SIZE +1]; + +double CieLab::qnrt(double x) +{ + double y = qn_table[int(x*ROOT_TAB_SIZE )]; // assuming x \in [0, 1] + double Y = y*y; + y = (4.0*y + x/(Y*Y))/5.0; + Y = y*y; + y = (4.0*y + x/(Y*Y))/5.0; // polish twice + return y; +} + +double CieLab::pow24(double x) +{ + double onetwo = x*qnrt(x); + return onetwo*onetwo; +} + + +static bool _clab_inited_ = false; +void CieLab::init() +{ + if (!_clab_inited_) + { + cbrt_table[0] = pow(float(1)/float(ROOT_TAB_SIZE*2), 0.3333); + qn_table[0] = pow(float(1)/float(ROOT_TAB_SIZE*2), 0.2); + for(int i = 1; i < ROOT_TAB_SIZE +1; i++) + { + cbrt_table[i] = pow(float(i)/float(ROOT_TAB_SIZE), 0.3333); + qn_table[i] = pow(float(i)/float(ROOT_TAB_SIZE), 0.2); + } + _clab_inited_ = true; + } +} + + + +/** + * Construct this CieLab from a packed-pixel ARGB value + */ +CieLab::CieLab(unsigned long rgb) +{ + init(); + + int ir = (rgb>>16) & 0xff; + int ig = (rgb>> 8) & 0xff; + int ib = (rgb ) & 0xff; + + float fr = ((float)ir) / 255.0; + float fg = ((float)ig) / 255.0; + float fb = ((float)ib) / 255.0; + + //printf("fr:%f fg:%f fb:%f\n", fr, fg, fb); + if (fr > 0.04045) + //fr = (float) pow((fr + 0.055) / 1.055, 2.4); + fr = (float) pow24((fr + 0.055) / 1.055); + else + fr = fr / 12.92; + + if (fg > 0.04045) + //fg = (float) pow((fg + 0.055) / 1.055, 2.4); + fg = (float) pow24((fg + 0.055) / 1.055); + else + fg = fg / 12.92; + + if (fb > 0.04045) + //fb = (float) pow((fb + 0.055) / 1.055, 2.4); + fb = (float) pow24((fb + 0.055) / 1.055); + else + fb = fb / 12.92; + + // Use white = D65 + const float x = fr * 0.4124 + fg * 0.3576 + fb * 0.1805; + const float y = fr * 0.2126 + fg * 0.7152 + fb * 0.0722; + const float z = fr * 0.0193 + fg * 0.1192 + fb * 0.9505; + + float vx = x / 0.95047; + float vy = y; + float vz = z / 1.08883; + + //printf("vx:%f vy:%f vz:%f\n", vx, vy, vz); + if (vx > 0.008856) + //vx = (float) pow(vx, 0.3333); + vx = (float) cbrt(vx); + else + vx = (7.787 * vx) + (16.0 / 116.0); + + if (vy > 0.008856) + //vy = (float) pow(vy, 0.3333); + vy = (float) cbrt(vy); + else + vy = (7.787 * vy) + (16.0 / 116.0); + + if (vz > 0.008856) + //vz = (float) pow(vz, 0.3333); + vz = (float) cbrt(vz); + else + vz = (7.787 * vz) + (16.0 / 116.0); + + C = 0; + L = 116.0 * vy - 16.0; + A = 500.0 * (vx - vy); + B = 200.0 * (vy - vz); +} + + + +/** + * Return this CieLab's value converted to a packed-pixel ARGB value + */ +unsigned long CieLab::toRGB() +{ + float vy = (L + 16.0) / 116.0; + float vx = A / 500.0 + vy; + float vz = vy - B / 200.0; + + float vx3 = vx * vx * vx; + float vy3 = vy * vy * vy; + float vz3 = vz * vz * vz; + + if (vy3 > 0.008856) + vy = vy3; + else + vy = (vy - 16.0 / 116.0) / 7.787; + + if (vx3 > 0.008856) + vx = vx3; + else + vx = (vx - 16.0 / 116.0) / 7.787; + + if (vz3 > 0.008856) + vz = vz3; + else + vz = (vz - 16.0 / 116.0) / 7.787; + + vx *= 0.95047; //use white = D65 + vz *= 1.08883; + + float vr =(float)(vx * 3.2406 + vy * -1.5372 + vz * -0.4986); + float vg =(float)(vx * -0.9689 + vy * 1.8758 + vz * 0.0415); + float vb =(float)(vx * 0.0557 + vy * -0.2040 + vz * 1.0570); + + if (vr > 0.0031308) + vr = (float)(1.055 * pow(vr, (1.0 / 2.4)) - 0.055); + else + vr = 12.92 * vr; + + if (vg > 0.0031308) + vg = (float)(1.055 * pow(vg, (1.0 / 2.4)) - 0.055); + else + vg = 12.92 * vg; + + if (vb > 0.0031308) + vb = (float)(1.055 * pow(vb, (1.0 / 2.4)) - 0.055); + else + vb = 12.92 * vb; + + return getRGB(0.0, vr, vg, vb); +} + + +/** + * Squared Euclidian distance between this and another color + */ +float CieLab::diffSq(const CieLab &other) +{ + float sum=0.0; + sum += (L - other.L) * (L - other.L); + sum += (A - other.A) * (A - other.A); + sum += (B - other.B) * (B - other.B); + return sum; +} + +/** + * Computes squared euclidian distance in CieLab space for two colors + * given as RGB values. + */ +float CieLab::diffSq(unsigned int rgb1, unsigned int rgb2) +{ + CieLab c1(rgb1); + CieLab c2(rgb2); + float euclid = c1.diffSq(c2); + return euclid; +} + + +/** + * Computes squared euclidian distance in CieLab space for two colors + * given as RGB values. + */ +float CieLab::diff(unsigned int rgb0, unsigned int rgb1) +{ + return (float) sqrt(diffSq(rgb0, rgb1)); +} + + + +//######################################################################## +//# T U P E L +//######################################################################## + +/** + * Helper class for storing the minimum distances to a cluster centroid + * in background and foreground and the index to the centroids in each + * signature for a given color. + */ +class Tupel { +public: + + Tupel() + { + minBgDist = 0.0f; + indexMinBg = 0; + minFgDist = 0.0f; + indexMinFg = 0; + } + Tupel(float minBgDistArg, long indexMinBgArg, + float minFgDistArg, long indexMinFgArg) + { + minBgDist = minBgDistArg; + indexMinBg = indexMinBgArg; + minFgDist = minFgDistArg; + indexMinFg = indexMinFgArg; + } + Tupel(const Tupel &other) + { + minBgDist = other.minBgDist; + indexMinBg = other.indexMinBg; + minFgDist = other.minFgDist; + indexMinFg = other.indexMinFg; + } + Tupel &operator=(const Tupel &other) + = default; + virtual ~Tupel() + = default; + + float minBgDist; + long indexMinBg; + float minFgDist; + long indexMinFg; + + }; + + + +//######################################################################## +//# S I O X I M A G E +//######################################################################## + +/** + * Create an image with the given width and height + */ +SioxImage::SioxImage(unsigned int widthArg, unsigned int heightArg) +{ + init(widthArg, heightArg); +} + +/** + * Copy constructor + */ +SioxImage::SioxImage(const SioxImage &other) +{ + pixdata = nullptr; + cmdata = nullptr; + assign(other); +} + +/** + * Assignment + */ +SioxImage &SioxImage::operator=(const SioxImage &other) +{ + assign(other); + return *this; +} + + +/** + * Clean up after use. + */ +SioxImage::~SioxImage() +{ + if (pixdata) delete[] pixdata; + if (cmdata) delete[] cmdata; +} + +/** + * Error logging + */ +void SioxImage::error(const char *fmt, ...) +{ + char msgbuf[256]; + va_list args; + va_start(args, fmt); + vsnprintf(msgbuf, 255, fmt, args); + va_end(args) ; +#ifdef HAVE_GLIB + g_warning("SioxImage error: %s\n", msgbuf); +#else + fprintf(stderr, "SioxImage error: %s\n", msgbuf); +#endif +} + + +/** + * Returns true if the previous operation on this image + * was successful, else false. + */ +bool SioxImage::isValid() +{ + return valid; +} + +/** + * Sets whether an operation was successful, and whether + * this image should be considered a valid one. + * was successful, else false. + */ +void SioxImage::setValid(bool val) +{ + valid = val; +} + + +/** + * Set a pixel at the x,y coordinates to the given value. + * If the coordinates are out of range, do nothing. + */ +void SioxImage::setPixel(unsigned int x, + unsigned int y, + unsigned int pixval) +{ + if (x >= width || y >= height) + { + error("setPixel: out of bounds (%d,%d)/(%d,%d)", + x, y, width, height); + return; + } + unsigned long offset = width * y + x; + pixdata[offset] = pixval; +} + +/** + * Set a pixel at the x,y coordinates to the given r, g, b values. + * If the coordinates are out of range, do nothing. + */ +void SioxImage::setPixel(unsigned int x, unsigned int y, + unsigned int a, + unsigned int r, + unsigned int g, + unsigned int b) +{ + if (x >= width || y >= height) + { + error("setPixel: out of bounds (%d,%d)/(%d,%d)", + x, y, width, height); + return; + } + unsigned long offset = width * y + x; + unsigned int pixval = ((a << 24) & 0xff000000) | + ((r << 16) & 0x00ff0000) | + ((g << 8) & 0x0000ff00) | + ((b ) & 0x000000ff); + pixdata[offset] = pixval; +} + + + +/** + * Get a pixel at the x,y coordinates given. If + * the coordinates are out of range, return 0; + */ +unsigned int SioxImage::getPixel(unsigned int x, unsigned int y) +{ + if (x >= width || y >= height) + { + error("getPixel: out of bounds (%d,%d)/(%d,%d)", + x, y, width, height); + return 0L; + } + unsigned long offset = width * y + x; + return pixdata[offset]; +} + +/** + * Return the image data buffer + */ +unsigned int *SioxImage::getImageData() +{ + return pixdata; +} + +/** + * Set a confidence value at the x,y coordinates to the given value. + * If the coordinates are out of range, do nothing. + */ +void SioxImage::setConfidence(unsigned int x, + unsigned int y, + float confval) +{ + if (x >= width || y >= height) + { + error("setConfidence: out of bounds (%d,%d)/(%d,%d)", + x, y, width, height); + return; + } + unsigned long offset = width * y + x; + cmdata[offset] = confval; +} + +/** + * Get a confidence valueat the x,y coordinates given. If + * the coordinates are out of range, return 0; + */ +float SioxImage::getConfidence(unsigned int x, unsigned int y) +{ + if (x >= width || y >= height) + { + g_warning("getConfidence: out of bounds (%d,%d)/(%d,%d)", + x, y, width, height); + return 0.0; + } + unsigned long offset = width * y + x; + return cmdata[offset]; +} + +/** + * Return the confidence data buffer + */ +float *SioxImage::getConfidenceData() +{ + return cmdata; +} + + +/** + * Return the width of this image + */ +int SioxImage::getWidth() +{ + return width; +} + +/** + * Return the height of this image + */ +int SioxImage::getHeight() +{ + return height; +} + +/** + * Initialize values. Used by constructors + */ +void SioxImage::init(unsigned int widthArg, unsigned int heightArg) +{ + valid = true; + width = widthArg; + height = heightArg; + imageSize = width * height; + pixdata = new unsigned int[imageSize]; + cmdata = new float[imageSize]; + for (unsigned long i=0 ; i<imageSize ; i++) + { + pixdata[i] = 0; + cmdata[i] = 0.0; + } +} + +/** + * Assign values to that of another + */ +void SioxImage::assign(const SioxImage &other) +{ + if (pixdata) delete[] pixdata; + if (cmdata) delete[] cmdata; + valid = other.valid; + width = other.width; + height = other.height; + imageSize = width * height; + pixdata = new unsigned int[imageSize]; + cmdata = new float[imageSize]; + for (unsigned long i=0 ; i<imageSize ; i++) + { + pixdata[i] = other.pixdata[i]; + cmdata[i] = other.cmdata[i]; + } +} + + +/** + * Write the image to a PPM file + */ +bool SioxImage::writePPM(const std::string &fileName) +{ + + FILE *f = fopen(fileName.c_str(), "wb"); + if (!f) + return false; + + fprintf(f, "P6 %u %u 255\n", width, height); + + for (unsigned int y=0 ; y<height; y++) + { + for (unsigned int x=0 ; x<width ; x++) + { + unsigned int rgb = getPixel(x, y); + //unsigned int alpha = (rgb>>24) & 0xff; + unsigned int r = ((rgb>>16) & 0xff); + unsigned int g = ((rgb>> 8) & 0xff); + unsigned int b = ((rgb ) & 0xff); + fputc((unsigned char) r, f); + fputc((unsigned char) g, f); + fputc((unsigned char) b, f); + } + } + fclose(f); + return true; +} + + +#ifdef HAVE_GLIB + +/** + * Create an image from a GdkPixbuf + */ +SioxImage::SioxImage(GdkPixbuf *buf) +{ + if (!buf) + return; + + unsigned int width = gdk_pixbuf_get_width(buf); + unsigned int height = gdk_pixbuf_get_height(buf); + init(width, height); //DO THIS NOW!! + + + guchar *pixldata = gdk_pixbuf_get_pixels(buf); + int rowstride = gdk_pixbuf_get_rowstride(buf); + int n_channels = gdk_pixbuf_get_n_channels(buf); + + //### Fill in the cells with RGB values + int row = 0; + for (unsigned int y=0 ; y<height ; y++) + { + guchar *p = pixldata + row; + for (unsigned int x=0 ; x<width ; x++) + { + int r = (int)p[0]; + int g = (int)p[1]; + int b = (int)p[2]; + int alpha = (int)p[3]; + + setPixel(x, y, alpha, r, g, b); + p += n_channels; + } + row += rowstride; + } + +} + + +/** + * Create a GdkPixbuf from this image + */ +GdkPixbuf *SioxImage::getGdkPixbuf() +{ + bool has_alpha = true; + int n_channels = has_alpha ? 4 : 3; + + guchar *pixdata = (guchar *) + malloc(sizeof(guchar) * width * height * n_channels); + if (!pixdata) + return nullptr; + + int rowstride = width * n_channels; + + GdkPixbuf *buf = gdk_pixbuf_new_from_data(pixdata, + GDK_COLORSPACE_RGB, + has_alpha, 8, width, height, + rowstride, (GdkPixbufDestroyNotify)free, nullptr); + + //### Fill in the cells with RGB values + int row = 0; + for (unsigned int y=0 ; y < height ; y++) + { + guchar *p = pixdata + row; + for (unsigned x=0 ; x < width ; x++) + { + unsigned int rgb = getPixel(x, y); + p[0] = (rgb >> 16) & 0xff;//r + p[1] = (rgb >> 8) & 0xff;//g + p[2] = (rgb ) & 0xff;//b + if ( n_channels > 3 ) + { + p[3] = (rgb >> 24) & 0xff;//a + } + p += n_channels; + } + row += rowstride; + } + return buf; +} + +#endif /* GLIB */ + + + + +//######################################################################## +//# S I O X +//######################################################################## + +//############## +//## PUBLIC +//############## + +/** + * Confidence corresponding to a certain foreground region (equals one). + */ +const float Siox::CERTAIN_FOREGROUND_CONFIDENCE=1.0f; + +/** + * Confidence for a region likely being foreground. + */ +const float Siox::FOREGROUND_CONFIDENCE=0.8f; + +/** + * Confidence for foreground or background type being equally likely. + */ +const float Siox::UNKNOWN_REGION_CONFIDENCE=0.5f; + +/** + * Confidence for a region likely being background. + */ +const float Siox::BACKGROUND_CONFIDENCE=0.1f; + +/** + * Confidence corresponding to a certain background reagion (equals zero). + */ +const float Siox::CERTAIN_BACKGROUND_CONFIDENCE=0.0f; + +/** + * Construct a Siox engine + */ +Siox::Siox() : + sioxObserver(nullptr), + keepGoing(true), + width(0), + height(0), + pixelCount(0), + image(nullptr), + cm(nullptr), + labelField(nullptr) +{ + init(); +} + +/** + * Construct a Siox engine + */ +Siox::Siox(SioxObserver *observer) : + sioxObserver(observer), + keepGoing(true), + width(0), + height(0), + pixelCount(0), + image(nullptr), + cm(nullptr), + labelField(nullptr) +{ + init(); +} + + +/** + * + */ +Siox::~Siox() +{ + cleanup(); +} + + +/** + * Error logging + */ +void Siox::error(const char *fmt, ...) +{ + char msgbuf[256]; + va_list args; + va_start(args, fmt); + vsnprintf(msgbuf, 255, fmt, args); + va_end(args) ; +#ifdef HAVE_GLIB + g_warning("Siox error: %s\n", msgbuf); +#else + fprintf(stderr, "Siox error: %s\n", msgbuf); +#endif +} + +/** + * Trace logging + */ +void Siox::trace(const char *fmt, ...) +{ + char msgbuf[256]; + va_list args; + va_start(args, fmt); + vsnprintf(msgbuf, 255, fmt, args); + va_end(args) ; +#ifdef HAVE_GLIB + g_message("Siox: %s\n", msgbuf); +#else + fprintf(stdout, "Siox: %s\n", msgbuf); +#endif +} + + + +/** + * Progress reporting + */ +bool Siox::progressReport(float percentCompleted) +{ + if (!sioxObserver) + return true; + + bool ret = sioxObserver->progress(percentCompleted); + + if (!ret) + { + trace("User selected abort"); + keepGoing = false; + } + + return ret; +} + + + + +/** + * Extract the foreground of the original image, according + * to the values in the confidence matrix. + */ +SioxImage Siox::extractForeground(const SioxImage &originalImage, + unsigned int backgroundFillColor) +{ + trace("### Start"); + + init(); + keepGoing = true; + + SioxImage workImage = originalImage; + + //# fetch some info from the image + width = workImage.getWidth(); + height = workImage.getHeight(); + pixelCount = width * height; + image = workImage.getImageData(); + cm = workImage.getConfidenceData(); + labelField = new int[pixelCount]; + + trace("### Creating signatures"); + + //#### create color signatures + std::vector<CieLab> knownBg; + std::vector<CieLab> knownFg; + CieLab *imageClab = new CieLab[pixelCount]; + for (unsigned long i=0 ; i<pixelCount ; i++) + { + float conf = cm[i]; + unsigned int pix = image[i]; + CieLab lab(pix); + imageClab[i] = lab; + if (conf <= BACKGROUND_CONFIDENCE) + knownBg.push_back(lab); + else if (conf >= FOREGROUND_CONFIDENCE) + knownFg.push_back(lab); + } + + /* + std::vector<CieLab> imageClab; + for (int y = 0 ; y < workImage.getHeight() ; y++) + for (int x = 0 ; x < workImage.getWidth() ; x++) + { + float cm = workImage.getConfidence(x, y); + unsigned int pix = workImage.getPixel(x, y); + CieLab lab(pix); + imageClab.push_back(lab); + if (cm <= BACKGROUND_CONFIDENCE) + knownBg.push_back(lab); //note: uses CieLab(rgb) + else if (cm >= FOREGROUND_CONFIDENCE) + knownFg.push_back(lab); + } + */ + + if (!progressReport(10.0)) + { + error("User aborted"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + trace("knownBg:%u knownFg:%u", static_cast<unsigned int>(knownBg.size()), static_cast<unsigned int>(knownFg.size())); + + + std::vector<CieLab> bgSignature ; + if (!colorSignature(knownBg, bgSignature, 3)) + { + error("Could not create background signature"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + if (!progressReport(30.0)) + { + error("User aborted"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + + std::vector<CieLab> fgSignature ; + if (!colorSignature(knownFg, fgSignature, 3)) + { + error("Could not create foreground signature"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + //trace("### bgSignature:%d", bgSignature.size()); + + if (bgSignature.empty()) + { + // segmentation impossible + error("Signature size is < 1. Segmentation is impossible"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + if (!progressReport(30.0)) + { + error("User aborted"); + workImage.setValid(false); + delete[] imageClab; + delete[] labelField; + return workImage; + } + + + // classify using color signatures, + // classification cached in hashmap for drb and speedup purposes + trace("### Analyzing image"); + + std::map<unsigned int, Tupel> hs; + + unsigned int progressResolution = pixelCount / 10; + + for (unsigned int i=0; i<pixelCount; i++) + { + if (i % progressResolution == 0) + { + float progress = + 30.0 + 60.0 * (float)i / (float)pixelCount; + //trace("### progress:%f", progress); + if (!progressReport(progress)) + { + error("User aborted"); + delete[] imageClab; + delete[] labelField; + workImage.setValid(false); + return workImage; + } + } + + if (cm[i] >= FOREGROUND_CONFIDENCE) + { + cm[i] = CERTAIN_FOREGROUND_CONFIDENCE; + } + else if (cm[i] <= BACKGROUND_CONFIDENCE) + { + cm[i] = CERTAIN_BACKGROUND_CONFIDENCE; + } + else // somewhere in between + { + bool isBackground = true; + std::map<unsigned int, Tupel>::iterator iter = hs.find(i); + if (iter != hs.end()) //found + { + Tupel tupel = iter->second; + isBackground = tupel.minBgDist <= tupel.minFgDist; + } + else + { + CieLab lab = imageClab[i]; + float minBg = lab.diffSq(bgSignature[0]); + int minIndex = 0; + for (unsigned int j=1; j<bgSignature.size() ; j++) + { + float d = lab.diffSq(bgSignature[j]); + if (d<minBg) + { + minBg = d; + minIndex = j; + } + } + Tupel tupel(0.0f, 0, 0.0f, 0); + tupel.minBgDist = minBg; + tupel.indexMinBg = minIndex; + float minFg = 1.0e6f; + minIndex = -1; + for (unsigned int j = 0 ; j < fgSignature.size() ; j++) + { + float d = lab.diffSq(fgSignature[j]); + if (d < minFg) + { + minFg = d; + minIndex = j; + } + } + tupel.minFgDist = minFg; + tupel.indexMinFg = minIndex; + if (fgSignature.empty()) + { + isBackground = (minBg <= clusterSize); + // remove next line to force behaviour of old algorithm + //error("foreground signature does not exist"); + //delete[] labelField; + //workImage.setValid(false); + //return workImage; + } + else + { + isBackground = minBg < minFg; + } + hs[image[i]] = tupel; + } + + if (isBackground) + cm[i] = CERTAIN_BACKGROUND_CONFIDENCE; + else + cm[i] = CERTAIN_FOREGROUND_CONFIDENCE; + } + } + + + delete[] imageClab; + + trace("### postProcessing"); + + + //#### postprocessing + smooth(cm, width, height, 0.333f, 0.333f, 0.333f); // average + normalizeMatrix(cm, pixelCount); + erode(cm, width, height); + keepOnlyLargeComponents(UNKNOWN_REGION_CONFIDENCE, 1.0/*sizeFactorToKeep*/); + + //for (int i=0; i < 2/*smoothness*/; i++) + // smooth(cm, width, height, 0.333f, 0.333f, 0.333f); // average + + normalizeMatrix(cm, pixelCount); + + for (unsigned int i=0; i < pixelCount; i++) + { + if (cm[i] >= UNKNOWN_REGION_CONFIDENCE) + cm[i] = CERTAIN_FOREGROUND_CONFIDENCE; + else + cm[i] = CERTAIN_BACKGROUND_CONFIDENCE; + } + + keepOnlyLargeComponents(UNKNOWN_REGION_CONFIDENCE, 1.5/*sizeFactorToKeep*/); + fillColorRegions(); + dilate(cm, width, height); + + if (!progressReport(100.0)) + { + error("User aborted"); + delete[] labelField; + workImage.setValid(false); + return workImage; + } + + + //#### We are done. Now clear everything but the background + for (unsigned long i = 0; i<pixelCount ; i++) + { + float conf = cm[i]; + if (conf < FOREGROUND_CONFIDENCE) + image[i] = backgroundFillColor; + } + + delete[] labelField; + + trace("### Done"); + keepGoing = false; + return workImage; +} + + + +//############## +//## PRIVATE +//############## + +/** + * Initialize the Siox engine to its 'pristine' state. + * Performed at the beginning of extractForeground(). + */ +void Siox::init() +{ + limits[0] = 0.64f; + limits[1] = 1.28f; + limits[2] = 2.56f; + + float negLimits[3]; + negLimits[0] = -limits[0]; + negLimits[1] = -limits[1]; + negLimits[2] = -limits[2]; + + clusterSize = sqrEuclidianDist(limits, 3, negLimits); +} + + +/** + * Clean up any debris from processing. + */ +void Siox::cleanup() +{ +} + + + + +/** + * Stage 1 of the color signature work. 'dims' will be either + * 2 for grays, or 3 for colors + */ +void Siox::colorSignatureStage1(CieLab *points, + unsigned int leftBase, + unsigned int rightBase, + unsigned int recursionDepth, + unsigned int *clusterCount, + const unsigned int dims) +{ + + unsigned int currentDim = recursionDepth % dims; + CieLab point = points[leftBase]; + float min = point(currentDim); + float max = min; + + for (unsigned int i = leftBase + 1; i < rightBase ; i++) + { + point = points[i]; + float curval = point(currentDim); + if (curval < min) min = curval; + if (curval > max) max = curval; + } + + //Do the Rubner-rule split (sounds like a dance) + if (max - min > limits[currentDim]) + { + float pivotPoint = (min + max) / 2.0; //average + unsigned int left = leftBase; + unsigned int right = rightBase - 1; + + //# partition points according to the dimension + while (true) + { + while ( true ) + { + point = points[left]; + if (point(currentDim) > pivotPoint) + break; + left++; + } + while ( true ) + { + point = points[right]; + if (point(currentDim) <= pivotPoint) + break; + right--; + } + + if (left > right) + break; + + point = points[left]; + points[left] = points[right]; + points[right] = point; + + left++; + right--; + } + + //# Recurse and create sub-trees + colorSignatureStage1(points, leftBase, left, + recursionDepth + 1, clusterCount, dims); + colorSignatureStage1(points, left, rightBase, + recursionDepth + 1, clusterCount, dims); + } + else + { + //create a leaf + CieLab newpoint; + + newpoint.C = rightBase - leftBase; + + for (; leftBase < rightBase ; leftBase++) + { + newpoint.add(points[leftBase]); + } + + //printf("clusters:%d\n", *clusters); + + if (newpoint.C != 0) + newpoint.mul(1.0 / (float)newpoint.C); + points[*clusterCount] = newpoint; + (*clusterCount)++; + } +} + + + +/** + * Stage 2 of the color signature work + */ +void Siox::colorSignatureStage2(CieLab *points, + unsigned int leftBase, + unsigned int rightBase, + unsigned int recursionDepth, + unsigned int *clusterCount, + const float threshold, + const unsigned int dims) +{ + unsigned int currentDim = recursionDepth % dims; + CieLab point = points[leftBase]; + float min = point(currentDim); + float max = min; + + for (unsigned int i = leftBase+ 1; i < rightBase; i++) + { + point = points[i]; + float curval = point(currentDim); + if (curval < min) min = curval; + if (curval > max) max = curval; + } + + //Do the Rubner-rule split (sounds like a dance) + if (max - min > limits[currentDim]) + { + float pivotPoint = (min + max) / 2.0; //average + unsigned int left = leftBase; + unsigned int right = rightBase - 1; + + //# partition points according to the dimension + while (true) + { + while ( true ) + { + point = points[left]; + if (point(currentDim) > pivotPoint) + break; + left++; + } + while ( true ) + { + point = points[right]; + if (point(currentDim) <= pivotPoint) + break; + right--; + } + + if (left > right) + break; + + point = points[left]; + points[left] = points[right]; + points[right] = point; + + left++; + right--; + } + + //# Recurse and create sub-trees + colorSignatureStage2(points, leftBase, left, + recursionDepth + 1, clusterCount, threshold, dims); + colorSignatureStage2(points, left, rightBase, + recursionDepth + 1, clusterCount, threshold, dims); + } + else + { + //### Create a leaf + unsigned int sum = 0; + for (unsigned int i = leftBase; i < rightBase; i++) + sum += points[i].C; + + if ((float)sum >= threshold) + { + float scale = (float)(rightBase - leftBase); + CieLab newpoint; + + for (; leftBase < rightBase; leftBase++) + newpoint.add(points[leftBase]); + + if (scale != 0.0) + newpoint.mul(1.0 / scale); + points[*clusterCount] = newpoint; + (*clusterCount)++; + } + } +} + + + +/** + * Main color signature method + */ +bool Siox::colorSignature(const std::vector<CieLab> &inputVec, + std::vector<CieLab> &result, + const unsigned int dims) +{ + + unsigned int length = inputVec.size(); + + if (length < 1) // no error. just don't do anything + return true; + + CieLab *input = new CieLab [length]; + + if (!input) + { + error("Could not allocate buffer for signature"); + return false; + } + for (unsigned int i=0 ; i < length ; i++) + { + input[i] = inputVec[i]; + } + + unsigned int stage1length = 0; + colorSignatureStage1(input, 0, length, 0, &stage1length, dims); + + unsigned int stage2length = 0; + colorSignatureStage2(input, 0, stage1length, 0, &stage2length, length * 0.001, dims); + + result.clear(); + for (unsigned int i=0 ; i < stage2length ; i++) + { + result.push_back(input[i]); + } + + delete[] input; + + return true; +} + + + +/** + * + */ +void Siox::keepOnlyLargeComponents(float threshold, + double sizeFactorToKeep) +{ + for (unsigned long idx = 0 ; idx<pixelCount ; idx++) + labelField[idx] = -1; + + int curlabel = 0; + int maxregion= 0; + int maxblob = 0; + + // slow but easy to understand: + std::vector<int> labelSizes; + for (unsigned long i=0 ; i<pixelCount ; i++) + { + int regionCount = 0; + if (labelField[i] == -1 && cm[i] >= threshold) + { + regionCount = depthFirstSearch(i, threshold, curlabel++); + labelSizes.push_back(regionCount); + } + + if (regionCount>maxregion) + { + maxregion = regionCount; + maxblob = curlabel-1; + } + } + + for (unsigned int i=0 ; i<pixelCount ; i++) + { + if (labelField[i] != -1) + { + // remove if the component is to small + if (labelSizes[labelField[i]] * sizeFactorToKeep < maxregion) + cm[i] = CERTAIN_BACKGROUND_CONFIDENCE; + + // add maxblob always to foreground + if (labelField[i] == maxblob) + cm[i] = CERTAIN_FOREGROUND_CONFIDENCE; + } + } + +} + + + +int Siox::depthFirstSearch(int startPos, + float threshold, int curLabel) +{ + // stores positions of labeled pixels, where the neighbours + // should still be checked for processing: + + //trace("startPos:%d threshold:%f curLabel:%d", + // startPos, threshold, curLabel); + + std::vector<int> pixelsToVisit; + int componentSize = 0; + + if (labelField[startPos]==-1 && cm[startPos]>=threshold) + { + labelField[startPos] = curLabel; + componentSize++; + pixelsToVisit.push_back(startPos); + } + + + while (!pixelsToVisit.empty()) + { + int pos = pixelsToVisit[pixelsToVisit.size() - 1]; + pixelsToVisit.erase(pixelsToVisit.end() - 1); + unsigned int x = pos % width; + unsigned int y = pos / width; + + // check all four neighbours + int left = pos - 1; + if (((int)x)-1>=0 && labelField[left]==-1 && cm[left]>=threshold) + { + labelField[left]=curLabel; + componentSize++; + pixelsToVisit.push_back(left); + } + + int right = pos + 1; + if (x+1 < width && labelField[right]==-1 && cm[right]>=threshold) + { + labelField[right]=curLabel; + componentSize++; + pixelsToVisit.push_back(right); + } + + int top = pos - width; + if (((int)y)-1>=0 && labelField[top]==-1 && cm[top]>=threshold) + { + labelField[top]=curLabel; + componentSize++; + pixelsToVisit.push_back(top); + } + + int bottom = pos + width; + if (y+1 < height && labelField[bottom]==-1 + && cm[bottom]>=threshold) + { + labelField[bottom]=curLabel; + componentSize++; + pixelsToVisit.push_back(bottom); + } + + } + return componentSize; +} + + + +/** + * + */ +void Siox::fillColorRegions() +{ + for (unsigned long idx = 0 ; idx<pixelCount ; idx++) + labelField[idx] = -1; + + //int maxRegion=0; // unused now + std::vector<int> pixelsToVisit; + for (unsigned long i=0; i<pixelCount; i++) + { // for all pixels + if (labelField[i]!=-1 || cm[i]<UNKNOWN_REGION_CONFIDENCE) + { + continue; // already visited or bg + } + + unsigned int origColor = image[i]; + unsigned long curLabel = i+1; + labelField[i] = curLabel; + cm[i] = CERTAIN_FOREGROUND_CONFIDENCE; + + // int componentSize = 1; + pixelsToVisit.push_back(i); + // depth first search to fill region + while (!pixelsToVisit.empty()) + { + int pos = pixelsToVisit[pixelsToVisit.size() - 1]; + pixelsToVisit.erase(pixelsToVisit.end() - 1); + unsigned int x=pos % width; + unsigned int y=pos / width; + // check all four neighbours + int left = pos-1; + if (((int)x)-1 >= 0 && labelField[left] == -1 + && CieLab::diff(image[left], origColor)<1.0) + { + labelField[left]=curLabel; + cm[left]=CERTAIN_FOREGROUND_CONFIDENCE; + // ++componentSize; + pixelsToVisit.push_back(left); + } + int right = pos+1; + if (x+1 < width && labelField[right]==-1 + && CieLab::diff(image[right], origColor)<1.0) + { + labelField[right]=curLabel; + cm[right]=CERTAIN_FOREGROUND_CONFIDENCE; + // ++componentSize; + pixelsToVisit.push_back(right); + } + int top = pos - width; + if (((int)y)-1>=0 && labelField[top]==-1 + && CieLab::diff(image[top], origColor)<1.0) + { + labelField[top]=curLabel; + cm[top]=CERTAIN_FOREGROUND_CONFIDENCE; + // ++componentSize; + pixelsToVisit.push_back(top); + } + int bottom = pos + width; + if (y+1 < height && labelField[bottom]==-1 + && CieLab::diff(image[bottom], origColor)<1.0) + { + labelField[bottom]=curLabel; + cm[bottom]=CERTAIN_FOREGROUND_CONFIDENCE; + // ++componentSize; + pixelsToVisit.push_back(bottom); + } + } + //if (componentSize>maxRegion) { + // maxRegion=componentSize; + //} + } +} + + + + +/** + * Applies the morphological dilate operator. + * + * Can be used to close small holes in the given confidence matrix. + */ +void Siox::dilate(float *cm, int xres, int yres) +{ + + for (int y=0; y<yres; y++) + { + for (int x=0; x<xres-1; x++) + { + int idx=(y*xres)+x; + if (cm[idx+1]>cm[idx]) + cm[idx]=cm[idx+1]; + } + } + + for (int y=0; y<yres; y++) + { + for (int x=xres-1; x>=1; x--) + { + int idx=(y*xres)+x; + if (cm[idx-1]>cm[idx]) + cm[idx]=cm[idx-1]; + } + } + + for (int y=0; y<yres-1; y++) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + if (cm[((y+1)*xres)+x] > cm[idx]) + cm[idx]=cm[((y+1)*xres)+x]; + } + } + + for (int y=yres-1; y>=1; y--) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + if (cm[((y-1)*xres)+x] > cm[idx]) + cm[idx]=cm[((y-1)*xres)+x]; + } + } +} + +/** + * Applies the morphological erode operator. + */ +void Siox::erode(float *cm, int xres, int yres) +{ + for (int y=0; y<yres; y++) + { + for (int x=0; x<xres-1; x++) + { + int idx=(y*xres)+x; + if (cm[idx+1] < cm[idx]) + cm[idx]=cm[idx+1]; + } + } + for (int y=0; y<yres; y++) + { + for (int x=xres-1; x>=1; x--) + { + int idx=(y*xres)+x; + if (cm[idx-1] < cm[idx]) + cm[idx]=cm[idx-1]; + } + } + for (int y=0; y<yres-1; y++) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + if (cm[((y+1)*xres)+x] < cm[idx]) + cm[idx]=cm[((y+1)*xres)+x]; + } + } + for (int y=yres-1; y>=1; y--) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + if (cm[((y-1)*xres)+x] < cm[idx]) + cm[idx]=cm[((y-1)*xres)+x]; + } + } +} + + + +/** + * Normalizes the matrix to values to [0..1]. + */ +void Siox::normalizeMatrix(float *cm, int cmSize) +{ + float max= -1000000.0f; + for (int i=0; i<cmSize; i++) + if (cm[i] > max) max=cm[i]; + + //good to use STL, but max() is not iterative + //float max = *std::max(cm, cm + cmSize); + + if (max<=0.0 || max==1.0) + return; + + float alpha=1.00f/max; + premultiplyMatrix(alpha, cm, cmSize); +} + +/** + * Multiplies matrix with the given scalar. + */ +void Siox::premultiplyMatrix(float alpha, float *cm, int cmSize) +{ + for (int i=0; i<cmSize; i++) + cm[i]=alpha*cm[i]; +} + +/** + * Blurs confidence matrix with a given symmetrically weighted kernel. + * + * In the standard case confidence matrix entries are between 0...1 and + * the weight factors sum up to 1. + */ +void Siox::smooth(float *cm, int xres, int yres, + float f1, float f2, float f3) +{ + for (int y=0; y<yres; y++) + { + for (int x=0; x<xres-2; x++) + { + int idx=(y*xres)+x; + cm[idx]=f1*cm[idx]+f2*cm[idx+1]+f3*cm[idx+2]; + } + } + for (int y=0; y<yres; y++) + { + for (int x=xres-1; x>=2; x--) + { + int idx=(y*xres)+x; + cm[idx]=f3*cm[idx-2]+f2*cm[idx-1]+f1*cm[idx]; + } + } + for (int y=0; y<yres-2; y++) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + cm[idx]=f1*cm[idx]+f2*cm[((y+1)*xres)+x]+f3*cm[((y+2)*xres)+x]; + } + } + for (int y=yres-1; y>=2; y--) + { + for (int x=0; x<xres; x++) + { + int idx=(y*xres)+x; + cm[idx]=f3*cm[((y-2)*xres)+x]+f2*cm[((y-1)*xres)+x]+f1*cm[idx]; + } + } +} + +/** + * Squared Euclidian distance of p and q. + */ +float Siox::sqrEuclidianDist(float *p, int pSize, float *q) +{ + float sum=0.0; + for (int i=0; i<pSize; i++) + { + float v = p[i] - q[i]; + sum += v*v; + } + return sum; +} + + + + + + +} // namespace siox +} // namespace org + +//######################################################################## +//# E N D O F F I L E +//######################################################################## + diff --git a/src/trace/siox.h b/src/trace/siox.h new file mode 100644 index 0000000..e6012e9 --- /dev/null +++ b/src/trace/siox.h @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SIOX_H +#define SEEN_SIOX_H +/* + * Copyright 2005, 2006 by Gerald Friedland, Kristian Jantz and Lars Knipping + * + * Conversion to C++ for Inkscape by Bob Jamison + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * Note by Bob Jamison: + * After translating the siox.org Java API to C++ and receiving an + * education into this wonderful code, I began again, + * and started this version using lessons learned. This version is + * an attempt to provide an dependency-free SIOX engine that anyone + * can use in their project with minimal effort. + * + * Many thanks to the fine people at siox.org. + */ + +#include <string> +#include <vector> + +#define HAVE_GLIB + +#ifdef HAVE_GLIB +#include <glib.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#endif + + +namespace org +{ + +namespace siox +{ + + +//######################################################################## +//# C L A B +//######################################################################## + +/** + * + */ +class CieLab +{ +public: + + /** + * + */ + CieLab() + { + init(); + C = 0; + L = A = B = 0.0f; + } + + + /** + * + */ + CieLab(unsigned long rgb); + + + /** + * + */ + CieLab(float lArg, float aArg, float bArg) + { + init(); + C = 0; + L = lArg; + A = aArg; + B = bArg; + } + + + /** + * + */ + CieLab(const CieLab &other) + { + init(); + C = other.C; + L = other.L; + A = other.A; + B = other.B; + } + + + /** + * + */ + CieLab &operator=(const CieLab &other) + { + init(); + C = other.C; + L = other.L; + A = other.A; + B = other.B; + return *this; + } + + /** + * + */ + virtual ~CieLab() + = default; + + /** + * Retrieve a CieLab value via index. + */ + virtual float operator()(unsigned int index) + { + if (index==0) return L; + else if (index==1) return A; + else if (index==2) return B; + else return 0; + } + + + /** + * + */ + virtual void add(const CieLab &other) + { + C += other.C; + L += other.L; + A += other.A; + B += other.B; + } + + + /** + * + */ + virtual void mul(float scale) + { + L *= scale; + A *= scale; + B *= scale; + } + + + /** + * + */ + virtual unsigned long toRGB(); + + /** + * Approximate cube roots + */ + double cbrt(double x); + + /** + * + */ + double qnrt(double x); + + /** + * Raise to the 2.4 power + */ + double pow24(double x); + + /** + * Squared Euclidian distance between this and another color + */ + float diffSq(const CieLab &other); + + /** + * Computes squared euclidian distance in CieLab space for two colors + * given as RGB values. + */ + static float diffSq(unsigned int rgb1, unsigned int rgb2); + + /** + * Computes squared euclidian distance in CieLab space for two colors + * given as RGB values. + */ + static float diff(unsigned int rgb0, unsigned int rgb1); + + + unsigned int C; + float L; + float A; + float B; + +private: + + /** + * + */ + void init(); + + +}; + + +//######################################################################## +//# S I O X I M A G E +//######################################################################## + +/** + * This is a generic image type that provides a consistent interface + * to Siox, so that developers will not need to worry about data arrays. + */ +class SioxImage +{ +public: + + /** + * Create an image with the given width and height + */ + SioxImage(unsigned int width, unsigned int height); + + /** + * Copy constructor + */ + SioxImage(const SioxImage &other); + + /** + * Assignment + */ + SioxImage &operator=(const SioxImage &other); + + /** + * Clean up after use. + */ + virtual ~SioxImage(); + + /** + * Returns true if the previous operation on this image + * was successful, else false. + */ + virtual bool isValid(); + + /** + * Sets whether an operation was successful, and whether + * this image should be considered a valid one. + * was successful, else false. + */ + virtual void setValid(bool val); + + /** + * Set a pixel at the x,y coordinates to the given value. + * If the coordinates are out of range, do nothing. + */ + virtual void setPixel(unsigned int x, + unsigned int y, + unsigned int pixval); + + /** + * Set a pixel at the x,y coordinates to the given r, g, b values. + * If the coordinates are out of range, do nothing. + */ + virtual void setPixel(unsigned int x, unsigned int y, + unsigned int a, + unsigned int r, + unsigned int g, + unsigned int b); + + /** + * Get a pixel at the x,y coordinates given. If + * the coordinates are out of range, return 0 + */ + virtual unsigned int getPixel(unsigned int x, unsigned int y); + + + /** + * Return the image data buffer + */ + virtual unsigned int *getImageData(); + + /** + * Set a confidence value at the x,y coordinates to the given value. + * If the coordinates are out of range, do nothing. + */ + virtual void setConfidence(unsigned int x, + unsigned int y, + float conf); + + /** + * Get a confidence value at the x,y coordinates given. If + * the coordinates are out of range, return 0 + */ + virtual float getConfidence(unsigned int x, unsigned int y); + + /** + * Return the confidence data buffer + */ + virtual float *getConfidenceData(); + + /** + * Return the width of this image + */ + virtual int getWidth(); + + /** + * Return the height of this image + */ + virtual int getHeight(); + + /** + * Saves this image as a simple color PPM + */ + bool writePPM(const std::string &fileName); + + + +#ifdef HAVE_GLIB + + /** + * Special constructor to create an image from a GdkPixbuf. + */ + SioxImage(GdkPixbuf *buf); + + /** + * Creates a GdkPixbuf from this image. The user must + * remember to destroy the image when no longer needed. + * with g_free(pixbuf) + */ + GdkPixbuf *getGdkPixbuf(); + +#endif + +private: + + SioxImage() + = default; + + /** + * Assign values to that of another + */ + void assign(const SioxImage &other); + + /** + * Initialize values. Used by constructors + */ + void init(unsigned int width, unsigned int height); + + bool valid; + + unsigned int width; + + unsigned int height; + + unsigned long imageSize; + + /** + * Pixel data + */ + unsigned int *pixdata; + + /** + * Confidence matrix data + */ + float *cmdata; + +private: + + /** + * Error logging + */ + void error(const char *fmt, ...) G_GNUC_PRINTF(2,3); + +}; + + + +//######################################################################## +//# S I O X O B S E R V E R +//######################################################################## +class Siox; + +/** + * This is a class for observing the progress of a Siox engine. Overload + * the methods in your subclass to get the desired behaviour. + */ +class SioxObserver +{ +public: + + /** + * Constructor. Context can point to anything, and is usually + * used to point to a C++ object or C state object, to delegate + * callback processing to something else. Use NULL to ignore. + */ + SioxObserver(void *contextArg) : context(nullptr) + { context = contextArg; } + + /** + * Destructor + */ + virtual ~SioxObserver() + = default; + + /** + * Informs the observer how much has been completed. + * Return false if the processing should be aborted. + */ + virtual bool progress(float /*percentCompleted*/) + { + return true; + } + + /** + * Send an error string to the Observer. Processing will + * be halted. + */ + virtual void error(const std::string &/*msg*/) + { + } + +protected: + + void *context; + +}; + + + +//######################################################################## +//# S I O X +//######################################################################## + +/** + * + */ +class Siox +{ +public: + + /** + * Confidence corresponding to a certain foreground region (equals one). + */ + static const float CERTAIN_FOREGROUND_CONFIDENCE; //=1.0f; + + /** + * Confidence for a region likely being foreground. + */ + static const float FOREGROUND_CONFIDENCE; //=0.8f; + + /** + * Confidence for foreground or background type being equally likely. + */ + static const float UNKNOWN_REGION_CONFIDENCE; //=0.5f; + + /** + * Confidence for a region likely being background. + */ + static const float BACKGROUND_CONFIDENCE; //=0.1f; + + /** + * Confidence corresponding to a certain background reagion (equals zero). + */ + static const float CERTAIN_BACKGROUND_CONFIDENCE; //=0.0f; + + /** + * Construct a Siox engine + */ + Siox(); + + /** + * Construct a Siox engine. Use null to ignore + */ + Siox(SioxObserver *observer); + + /** + * + */ + virtual ~Siox(); + + /** + * Extract the foreground of the original image, according + * to the values in the confidence matrix. If the operation fails, + * sioxImage.isValid() will be false. + * backgroundFillColor is any ARGB color, such as 0xffffff (white) + * or 0x000000 (black) + */ + virtual SioxImage extractForeground(const SioxImage &originalImage, + unsigned int backgroundFillColor); + +private: + + SioxObserver *sioxObserver; + + /** + * Progress reporting + */ + bool progressReport(float percentCompleted); + + /** + * Flag this as false during processing to abort + */ + bool keepGoing; + + /** + * Image width + */ + unsigned int width; + + /** + * Image height + */ + unsigned int height; + + /** + * Image size in pixels + */ + unsigned long pixelCount; + + /** + * Image data + */ + unsigned int *image; + + /** + * Image confidence matrix + */ + float *cm; + + /** + * Markup for image editing + */ + int *labelField; + + + /** + * Our signature limits + */ + float limits[3]; + + /** + * Maximum distance of two lab values. + */ + float clusterSize; + + /** + * Initialize the Siox engine to its 'pristine' state. + * Performed at the beginning of extractForeground(). + */ + void init(); + + /** + * Clean up any debris from processing. + */ + void cleanup(); + + /** + * Error logging + */ + void error(const char *fmt, ...) G_GNUC_PRINTF(2,3); + + /** + * Trace logging + */ + void trace(const char *fmt, ...) G_GNUC_PRINTF(2,3); + + /** + * Stage 1 of the color signature work. 'dims' will be either + * 2 for grays, or 3 for colors + */ + void colorSignatureStage1(CieLab *points, + unsigned int leftBase, + unsigned int rightBase, + unsigned int recursionDepth, + unsigned int *clusters, + const unsigned int dims); + + /** + * Stage 2 of the color signature work + */ + void colorSignatureStage2(CieLab *points, + unsigned int leftBase, + unsigned int rightBase, + unsigned int recursionDepth, + unsigned int *clusters, + const float threshold, + const unsigned int dims); + + /** + * Main color signature method + */ + bool colorSignature(const std::vector<CieLab> &inputVec, + std::vector<CieLab> &result, + const unsigned int dims); + + + /** + * + */ + void keepOnlyLargeComponents(float threshold, + double sizeFactorToKeep); + + /** + * + */ + int depthFirstSearch(int startPos, float threshold, int curLabel); + + + /** + * + */ + void fillColorRegions(); + + /** + * Applies the morphological dilate operator. + * + * Can be used to close small holes in the given confidence matrix. + */ + void dilate(float *cm, int xres, int yres); + + /** + * Applies the morphological erode operator. + */ + void erode(float *cm, int xres, int yres); + + /** + * Normalizes the matrix to values to [0..1]. + */ + void normalizeMatrix(float *cm, int cmSize); + + /** + * Multiplies matrix with the given scalar. + */ + void premultiplyMatrix(float alpha, float *cm, int cmSize); + + /** + * Blurs confidence matrix with a given symmetrically weighted kernel. + */ + void smooth(float *cm, int xres, int yres, + float f1, float f2, float f3); + + /** + * Squared Euclidian distance of p and q. + */ + float sqrEuclidianDist(float *p, int pSize, float *q); + +}; + + + + +} // namespace siox +} // namespace org + +#endif // SEEN_SIOX_H +//######################################################################## +//# E N D O F F I L E +//######################################################################## diff --git a/src/trace/trace.cpp b/src/trace/trace.cpp new file mode 100644 index 0000000..4ffff86 --- /dev/null +++ b/src/trace/trace.cpp @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A generic interface for plugging different + * autotracers into Inkscape. + * + * Authors: + * Bob Jamison <rjamison@earthlink.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Bob Jamison + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "trace/potrace/inkscape-potrace.h" + +#include "inkscape.h" +#include "desktop.h" + +#include "document.h" +#include "document-undo.h" +#include "message-stack.h" +#include <glibmm/i18n.h> +#include <gtkmm/main.h> +#include "selection.h" +#include "xml/repr.h" +#include "xml/attribute-record.h" +#include <2geom/transforms.h> +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-shape.h" + +#include "object/sp-item.h" +#include "object/sp-shape.h" +#include "object/sp-image.h" + +#include "siox.h" +#include "imagemap-gdk.h" + +namespace Inkscape { +namespace Trace { + +SPImage *Tracer::getSelectedSPImage() +{ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + { + g_warning("Trace: No active desktop"); + return nullptr; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + + Inkscape::Selection *sel = desktop->getSelection(); + if (!sel) + { + char *msg = _("Select an <b>image</b> to trace"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + return nullptr; + } + + if (sioxEnabled) + { + SPImage *img = nullptr; + auto list = sel->items(); + std::vector<SPItem *> items; + sioxShapes.clear(); + + /* + First, things are selected top-to-bottom, so we need to invert + them as bottom-to-top so that we can discover the image and any + SPItems above it + */ + for (auto i=list.begin() ; list.end()!=i ; ++i) + { + if (!SP_IS_ITEM(*i)) + { + continue; + } + SPItem *item = *i; + items.insert(items.begin(), item); + } + std::vector<SPItem *>::iterator iter; + for (iter = items.begin() ; iter!= items.end() ; ++iter) + { + SPItem *item = *iter; + if (SP_IS_IMAGE(item)) + { + if (img) //we want only one + { + char *msg = _("Select only one <b>image</b> to trace"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return nullptr; + } + img = SP_IMAGE(item); + } + else // if (img) //# items -after- the image in tree (above it in Z) + { + if (SP_IS_SHAPE(item)) + { + SPShape *shape = SP_SHAPE(item); + sioxShapes.push_back(shape); + } + } + } + + if (!img || sioxShapes.size() < 1) + { + char *msg = _("Select one image and one or more shapes above it"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return nullptr; + } + return img; + } + else + //### SIOX not enabled. We want exactly one image selected + { + SPItem *item = sel->singleItem(); + if (!item) + { + char *msg = _("Select an <b>image</b> to trace"); //same as above + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + return nullptr; + } + + if (!SP_IS_IMAGE(item)) + { + char *msg = _("Select an <b>image</b> to trace"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + return nullptr; + } + + SPImage *img = SP_IMAGE(item); + + return img; + } + +} + + + +typedef org::siox::SioxImage SioxImage; +typedef org::siox::SioxObserver SioxObserver; +typedef org::siox::Siox Siox; + + +class TraceSioxObserver : public SioxObserver +{ +public: + + /** + * + */ + TraceSioxObserver (void *contextArg) : + SioxObserver(contextArg) + {} + + /** + * + */ + ~TraceSioxObserver () override + = default; + + /** + * Informs the observer how much has been completed. + * Return false if the processing should be aborted. + */ + bool progress(float /*percentCompleted*/) override + { + //Tracer *tracer = (Tracer *)context; + //## Allow the GUI to update + Gtk::Main::iteration(false); //at least once, non-blocking + while( Gtk::Main::events_pending() ) + Gtk::Main::iteration(); + return true; + } + + /** + * Send an error string to the Observer. Processing will + * be halted. + */ + void error(const std::string &/*msg*/) override + { + //Tracer *tracer = (Tracer *)context; + } + + +}; + + + + + +Glib::RefPtr<Gdk::Pixbuf> Tracer::sioxProcessImage(SPImage *img, Glib::RefPtr<Gdk::Pixbuf>origPixbuf) +{ + if (!sioxEnabled) + return origPixbuf; + + if (origPixbuf == lastOrigPixbuf) + return lastSioxPixbuf; + + //g_message("siox: start"); + + //Convert from gdk, so a format we know. By design, the pixel + //format in PackedPixelMap is identical to what is needed by SIOX + SioxImage simage(origPixbuf->gobj()); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + { + g_warning("%s", _("Trace: No active desktop")); + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + + Inkscape::Selection *sel = desktop->getSelection(); + if (!sel) + { + char *msg = _("Select an <b>image</b> to trace"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + } + + Inkscape::DrawingItem *aImg = img->get_arenaitem(desktop->dkey); + //g_message("img: %d %d %d %d\n", aImg->bbox.x0, aImg->bbox.y0, + // aImg->bbox.x1, aImg->bbox.y1); + + double width = aImg->geometricBounds()->width(); + double height = aImg->geometricBounds()->height(); + + double iwidth = simage.getWidth(); + double iheight = simage.getHeight(); + + double iwscale = width / iwidth; + double ihscale = height / iheight; + + std::vector<Inkscape::DrawingItem *> arenaItems; + std::vector<SPShape *>::iterator iter; + for (iter = sioxShapes.begin() ; iter!=sioxShapes.end() ; ++iter) + { + SPItem *item = *iter; + Inkscape::DrawingItem *aItem = item->get_arenaitem(desktop->dkey); + arenaItems.push_back(aItem); + } + + //g_message("%d arena items\n", arenaItems.size()); + + //PackedPixelMap *dumpMap = PackedPixelMapCreate( + // simage.getWidth(), simage.getHeight()); + + //g_message("siox: start selection"); + + for (int row=0 ; row<iheight ; row++) + { + double ypos = aImg->geometricBounds()->top() + ihscale * (double) row; + for (int col=0 ; col<simage.getWidth() ; col++) + { + //Get absolute X,Y position + double xpos = aImg->geometricBounds()->left() + iwscale * (double)col; + Geom::Point point(xpos, ypos); + point *= aImg->transform(); + //point *= imgMat; + //point = desktop->doc2dt(point); + //g_message("x:%f y:%f\n", point[0], point[1]); + bool weHaveAHit = false; + std::vector<Inkscape::DrawingItem *>::iterator aIter; + for (aIter = arenaItems.begin() ; aIter!=arenaItems.end() ; ++aIter) + { + Inkscape::DrawingItem *arenaItem = *aIter; + arenaItem->drawing().update(); + if (arenaItem->pick(point, 1.0f, 1)) + { + weHaveAHit = true; + break; + } + } + + if (weHaveAHit) + { + //g_message("hit!\n"); + //dumpMap->setPixelLong(dumpMap, col, row, 0L); + simage.setConfidence(col, row, + Siox::UNKNOWN_REGION_CONFIDENCE); + } + else + { + //g_message("miss!\n"); + //dumpMap->setPixelLong(dumpMap, col, row, + // simage.getPixel(col, row)); + simage.setConfidence(col, row, + Siox::CERTAIN_BACKGROUND_CONFIDENCE); + } + } + } + + //g_message("siox: selection done"); + + //dumpMap->writePPM(dumpMap, "siox1.ppm"); + //dumpMap->destroy(dumpMap); + + //## ok we have our pixel buf + TraceSioxObserver observer(this); + Siox sengine(&observer); + SioxImage result = sengine.extractForeground(simage, 0xffffff); + if (!result.isValid()) + { + g_warning("%s", _("Invalid SIOX result")); + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + } + + //result.writePPM("siox2.ppm"); + + Glib::RefPtr<Gdk::Pixbuf> newPixbuf = Glib::wrap(result.getGdkPixbuf()); + + //g_message("siox: done"); + + lastSioxPixbuf = newPixbuf; + + return newPixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> Tracer::getSelectedImage() +{ + + + SPImage *img = getSelectedSPImage(); + if (!img) + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + + if (!img->pixbuf) + return Glib::RefPtr<Gdk::Pixbuf>(nullptr); + + GdkPixbuf *raw_pb = img->pixbuf->getPixbufRaw(false); + GdkPixbuf *trace_pb = gdk_pixbuf_copy(raw_pb); + if (img->pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { + convert_pixels_argb32_to_pixbuf( + gdk_pixbuf_get_pixels(trace_pb), + gdk_pixbuf_get_width(trace_pb), + gdk_pixbuf_get_height(trace_pb), + gdk_pixbuf_get_rowstride(trace_pb)); + } + + Glib::RefPtr<Gdk::Pixbuf> pixbuf = Glib::wrap(trace_pb, false); + + if (sioxEnabled) + { + Glib::RefPtr<Gdk::Pixbuf> sioxPixbuf = + sioxProcessImage(img, pixbuf); + if (!sioxPixbuf) + { + return pixbuf; + } + else + { + return sioxPixbuf; + } + } + else + { + return pixbuf; + } + +} + + + +//######################################################################### +//# T R A C E +//######################################################################### + +void Tracer::enableSiox(bool enable) +{ + sioxEnabled = enable; +} + + +void Tracer::traceThread() +{ + //## Remember. NEVER leave this method without setting + //## engine back to NULL + + //## Prepare our kill flag. We will watch this later to + //## see if the main thread wants us to stop + keepGoing = true; + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) + { + g_warning("Trace: No active desktop\n"); + return; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + + Inkscape::Selection *selection = desktop->getSelection(); + + if (!SP_ACTIVE_DOCUMENT) + { + char *msg = _("Trace: No active document"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + engine = nullptr; + return; + } + SPDocument *doc = SP_ACTIVE_DOCUMENT; + doc->ensureUpToDate(); + + + SPImage *img = getSelectedSPImage(); + if (!img) + { + engine = nullptr; + return; + } + + GdkPixbuf *trace_pb = gdk_pixbuf_copy(img->pixbuf->getPixbufRaw(false)); + if (img->pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { + convert_pixels_argb32_to_pixbuf( + gdk_pixbuf_get_pixels(trace_pb), + gdk_pixbuf_get_width(trace_pb), + gdk_pixbuf_get_height(trace_pb), + gdk_pixbuf_get_rowstride(trace_pb)); + } + + Glib::RefPtr<Gdk::Pixbuf> pixbuf = Glib::wrap(trace_pb, false); + + pixbuf = sioxProcessImage(img, pixbuf); + + if (!pixbuf) + { + char *msg = _("Trace: Image has no bitmap data"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + //g_warning(msg); + engine = nullptr; + return; + } + + msgStack->flash(Inkscape::NORMAL_MESSAGE, _("Trace: Starting trace...")); + desktop->updateCanvasNow(); + + std::vector<TracingEngineResult> results = + engine->trace(pixbuf); + //printf("nrPaths:%d\n", results.size()); + int nrPaths = results.size(); + + //### Check if we should stop + if (!keepGoing || nrPaths<1) + { + engine = nullptr; + return; + } + + //### Get pointers to the <image> and its parent + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *imgRepr = SP_OBJECT(img)->getRepr(); + Inkscape::XML::Node *par = imgRepr->parent(); + + //### Get some information for the new transform() + double x = 0.0; + double y = 0.0; + double width = 0.0; + double height = 0.0; + double dval = 0.0; + + if (sp_repr_get_double(imgRepr, "x", &dval)) + x = dval; + if (sp_repr_get_double(imgRepr, "y", &dval)) + y = dval; + + if (sp_repr_get_double(imgRepr, "width", &dval)) + width = dval; + if (sp_repr_get_double(imgRepr, "height", &dval)) + height = dval; + + double iwidth = (double)pixbuf->get_width(); + double iheight = (double)pixbuf->get_height(); + + double iwscale = width / iwidth; + double ihscale = height / iheight; + + Geom::Translate trans(x, y); + Geom::Scale scal(iwscale, ihscale); + + //# Convolve scale, translation, and the original transform + Geom::Affine tf(scal * trans); + tf *= img->transform; + + + //#OK. Now let's start making new nodes + + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *groupRepr = nullptr; + + //# if more than 1, make a <g>roup of <path>s + if (nrPaths > 1) + { + groupRepr = xml_doc->createElement("svg:g"); + par->addChild(groupRepr, imgRepr); + } + + long totalNodeCount = 0L; + + for (auto result : results) + { + totalNodeCount += result.getNodeCount(); + + Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path"); + pathRepr->setAttributeOrRemoveIfEmpty("style", result.getStyle()); + pathRepr->setAttributeOrRemoveIfEmpty("d", result.getPathData()); + + if (nrPaths > 1) + groupRepr->addChild(pathRepr, nullptr); + else + par->addChild(pathRepr, imgRepr); + + //### Apply the transform from the image to the new shape + SPObject *reprobj = doc->getObjectByRepr(pathRepr); + if (reprobj) + { + SPItem *newItem = SP_ITEM(reprobj); + newItem->doWriteTransform(tf); + } + if (nrPaths == 1) + { + selection->clear(); + selection->add(pathRepr); + } + Inkscape::GC::release(pathRepr); + } + + // If we have a group, then focus on, then forget it + if (nrPaths > 1) + { + selection->clear(); + selection->add(groupRepr); + Inkscape::GC::release(groupRepr); + } + + //## inform the document, so we can undo + DocumentUndo::done(doc, SP_VERB_SELECTION_TRACE, _("Trace bitmap")); + + engine = nullptr; + + char *msg = g_strdup_printf(_("Trace: Done. %ld nodes created"), totalNodeCount); + msgStack->flash(Inkscape::NORMAL_MESSAGE, msg); + g_free(msg); + +} + + + + + +void Tracer::trace(TracingEngine *theEngine) +{ + //Check if we are already running + if (engine) + return; + + engine = theEngine; + +#if HAVE_THREADS + //Ensure that thread support is running + if (!Glib::thread_supported()) + Glib::thread_init(); + + //Create our thread and run it + Glib::Thread::create( + sigc::mem_fun(*this, &Tracer::traceThread), false); +#else + traceThread(); +#endif + +} + + + + + +void Tracer::abort() +{ + + //## Inform Trace's working thread + keepGoing = false; + + if (engine) + { + engine->abort(); + } + +} + + + +} // namespace Trace + +} // namespace Inkscape + + +//######################################################################### +//# E N D O F F I L E +//######################################################################### + diff --git a/src/trace/trace.h b/src/trace/trace.h new file mode 100644 index 0000000..ddc4b96 --- /dev/null +++ b/src/trace/trace.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bob Jamison <rjamison@titan.com> + * + * Copyright (C) 2004-2006 Bob Jamison + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_TRACE_H +#define SEEN_TRACE_H + +# include <cstring> + +#include <glibmm/refptr.h> +#include <gdkmm/pixbuf.h> +#include <utility> +#include <vector> + +class SPImage; +class SPItem; +class SPShape; + +namespace Inkscape { + +namespace Trace { + + + +/** + * + */ +class TracingEngineResult +{ + +public: + + /** + * + */ + TracingEngineResult(std::string theStyle, + std::string thePathData, + long theNodeCount) : + style(std::move(theStyle)), + pathData(std::move(thePathData)), + nodeCount(theNodeCount) + {} + + TracingEngineResult(const TracingEngineResult &other) + { assign(other); } + + virtual TracingEngineResult &operator=(const TracingEngineResult &other) + { assign(other); return *this; } + + + /** + * + */ + virtual ~TracingEngineResult() + = default; + + + /** + * + */ + std::string getStyle() + { return style; } + + /** + * + */ + std::string getPathData() + { return pathData; } + + /** + * + */ + long getNodeCount() + { return nodeCount; } + +private: + + void assign(const TracingEngineResult &other) + { + style = other.style; + pathData = other.pathData; + nodeCount = other.nodeCount; + } + + std::string style; + + std::string pathData; + + long nodeCount; + +}; + + + +/** + * A generic interface for plugging different + * autotracers into Inkscape. + */ +class TracingEngine +{ + + public: + + /** + * + */ + TracingEngine() + = default; + + /** + * + */ + virtual ~TracingEngine() + = default; + + /** + * This is the working method of this interface, and all + * implementing classes. Take a GdkPixbuf, trace it, and + * return a style attribute and the path data that is + * compatible with the d="" attribute + * of an SVG <path> element. + */ + virtual std::vector<TracingEngineResult> trace( + Glib::RefPtr<Gdk::Pixbuf> /*pixbuf*/) = 0; + + /** + * Abort the thread that is executing getPathDataFromPixbuf() + */ + virtual void abort() = 0; + + + +};//class TracingEngine + + + + + + + + + +/** + * This simple class allows a generic wrapper around a given + * TracingEngine object. Its purpose is to provide a gateway + * to a variety of tracing engines, while maintaining a + * consistent interface. + */ +class Tracer +{ + +public: + + + /** + * + */ + Tracer() + { + engine = nullptr; + sioxEnabled = false; + } + + + + /** + * + */ + ~Tracer() + = default; + + + /** + * A convenience method to allow other software to 'see' the + * same image that this class sees. + */ + Glib::RefPtr<Gdk::Pixbuf> getSelectedImage(); + + /** + * This is the main working method. Trace the selected image, if + * any, and create a <path> element from it, inserting it into + * the current document. + */ + void trace(TracingEngine *engine); + + + /** + * Abort the thread that is executing convertImageToPath() + */ + void abort(); + + /** + * Whether we want to enable SIOX subimage selection. + */ + void enableSiox(bool enable); + + +private: + + /** + * This is the single path code that is called by its counterpart above. + * Threaded method that does single bitmap--->path conversion. + */ + void traceThread(); + + /** + * This is true during execution. Setting it to false (like abort() + * does) should inform the threaded code that it needs to stop + */ + bool keepGoing; + + /** + * During tracing, this is Non-null, and refers to the + * engine that is currently doing the tracing. + */ + TracingEngine *engine; + + /** + * Get the selected image. Also check for any SPItems over it, in + * case the user wants SIOX pre-processing. + */ + SPImage *getSelectedSPImage(); + + std::vector<SPShape *> sioxShapes; + + bool sioxEnabled; + + /** + * Process a GdkPixbuf, according to which areas have been + * obscured in the GUI. + */ + Glib::RefPtr<Gdk::Pixbuf> sioxProcessImage(SPImage *img, Glib::RefPtr<Gdk::Pixbuf> origPixbuf); + + Glib::RefPtr<Gdk::Pixbuf> lastSioxPixbuf; + Glib::RefPtr<Gdk::Pixbuf> lastOrigPixbuf; + +};//class Tracer + + + + +} // namespace Trace + +} // namespace Inkscape + + + +#endif // SEEN_TRACE_H + +//######################################################################### +//# E N D O F F I L E +//######################################################################### + diff --git a/src/transf_mat_3x4.cpp b/src/transf_mat_3x4.cpp new file mode 100644 index 0000000..391783b --- /dev/null +++ b/src/transf_mat_3x4.cpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3x4 transformation matrix to map points from projective 3-space into the projective plane + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "transf_mat_3x4.h" +#include <2geom/affine.h> +#include "svg/stringstream.h" +#include "syseq.h" + +namespace Proj { + +TransfMat3x4::TransfMat3x4 () { + for (unsigned int i = 0; i < 3; ++i) { + for (unsigned int j = 0; j < 4; ++j) { + tmat[i][j] = (i == j ? 1 : 0); // or should we initialize all values with 0? does it matter at all? + } + } +} + +TransfMat3x4::TransfMat3x4 (Proj::Pt2 vp_x, Proj::Pt2 vp_y, Proj::Pt2 vp_z, Proj::Pt2 origin) { + for (unsigned int i = 0; i < 3; ++i) { + tmat[i][0] = vp_x[i]; + tmat[i][1] = vp_y[i]; + tmat[i][2] = vp_z[i]; + tmat[i][3] = origin[i]; + } +} + +TransfMat3x4::TransfMat3x4(TransfMat3x4 const &rhs) { + for (unsigned int i = 0; i < 3; ++i) { + for (unsigned int j = 0; j < 4; ++j) { + tmat[i][j] = rhs.tmat[i][j]; + } + } +} + +Pt2 +TransfMat3x4::column (Proj::Axis axis) const { + return Proj::Pt2 (tmat[0][axis], tmat[1][axis], tmat[2][axis]); +} + +Pt2 +TransfMat3x4::image (Pt3 const &point) { + double x = tmat[0][0] * point[0] + tmat[0][1] * point[1] + tmat[0][2] * point[2] + tmat[0][3] * point[3]; + double y = tmat[1][0] * point[0] + tmat[1][1] * point[1] + tmat[1][2] * point[2] + tmat[1][3] * point[3]; + double w = tmat[2][0] * point[0] + tmat[2][1] * point[1] + tmat[2][2] * point[2] + tmat[2][3] * point[3]; + + return Pt2 (x, y, w); +} + +Pt3 +TransfMat3x4::preimage (Geom::Point const &pt, double coord, Proj::Axis axis) { + const double init_val = std::numeric_limits<double>::quiet_NaN(); + double x[4] = { init_val, init_val, init_val, init_val }; + double v[3] = { pt[Geom::X], pt[Geom::Y], 1.0 }; + int index = (int) axis; + + SysEq::SolutionKind sol = SysEq::gaussjord_solve<3,4>(tmat, x, v, index, coord, true); + + if (sol != SysEq::unique) { + if (sol == SysEq::no_solution) { + g_print ("No solution. Please investigate.\n"); + } else { + g_print ("Infinitely many solutions. Please investigate.\n"); + } + } + return Pt3(x[0], x[1], x[2], x[3]); +} + +void +TransfMat3x4::set_image_pt (Proj::Axis axis, Proj::Pt2 const &pt) { + // FIXME: Do we need to adapt the coordinates in any way or can we just use them as they are? + for (int i = 0; i < 3; ++i) { + tmat[i][axis] = pt[i]; + } +} + +void +TransfMat3x4::toggle_finite (Proj::Axis axis) { + g_return_if_fail (axis != Proj::W); + if (has_finite_image(axis)) { + Geom::Point dir (column(axis).affine()); + Geom::Point origin (column(Proj::W).affine()); + dir -= origin; + set_column (axis, Proj::Pt2(dir[Geom::X], dir[Geom::Y], 0)); + } else { + Proj::Pt2 dir (column(axis)); + Proj::Pt2 origin (column(Proj::W).affine()); + dir = dir + origin; + dir[2] = 1.0; + set_column (axis, dir); + } +} + +gchar * +TransfMat3x4::pt_to_str (Proj::Axis axis) { + Inkscape::SVGOStringStream os; + os << tmat[0][axis] << " : " + << tmat[1][axis] << " : " + << tmat[2][axis]; + return g_strdup(os.str().c_str()); +} + +/* Check for equality (with a small tolerance epsilon) */ +bool +TransfMat3x4::operator==(const TransfMat3x4 &rhs) const +{ + // Should we allow a certain tolerance or "normalize" the matrices first? + for (int i = 0; i < 3; ++i) { + Proj::Pt2 pt1 = column(Proj::axes[i]); + Proj::Pt2 pt2 = rhs.column(Proj::axes[i]); + if (pt1 != pt2) { + return false; + } + } + return true; +} + +/* Multiply a projective matrix by an affine matrix (by only multiplying the 'affine part' of the + * projective matrix) */ +TransfMat3x4 +TransfMat3x4::operator*(Geom::Affine const &A) const { + TransfMat3x4 ret; + + for (int j = 0; j < 4; ++j) { + ret.tmat[0][j] = A[0]*tmat[0][j] + A[2]*tmat[1][j] + A[4]*tmat[2][j]; + ret.tmat[1][j] = A[1]*tmat[0][j] + A[3]*tmat[1][j] + A[5]*tmat[2][j]; + ret.tmat[2][j] = tmat[2][j]; + } + + return ret; +} + +// FIXME: Shouldn't rather operator* call operator*= for efficiency? (Because in operator*= +// there is in principle no need to create a temporary object, which happens in the assignment) +TransfMat3x4 & +TransfMat3x4::operator*=(Geom::Affine const &A) { + *this = *this * A; + return *this; +} + +void +TransfMat3x4::copy_tmat(double rhs[3][4]) { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 4; ++j) { + rhs[i][j] = tmat[i][j]; + } + } +} + +void +TransfMat3x4::print () const { + g_print ("Transformation matrix:\n"); + for (const auto & i : tmat) { + g_print (" "); + for (double j : i) { + g_print ("%8.2f ", j); + } + g_print ("\n"); + } +} + +void +TransfMat3x4::normalize_column (Proj::Axis axis) { + Proj::Pt2 new_col(column(axis)); + new_col.normalize(); + set_image_pt(axis, new_col); +} + + +} // namespace Proj + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/transf_mat_3x4.h b/src/transf_mat_3x4.h new file mode 100644 index 0000000..20096e5 --- /dev/null +++ b/src/transf_mat_3x4.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TRANSF_MAT_3x4_H +#define SEEN_TRANSF_MAT_3x4_H + +/* + * 3x4 transformation matrix to map points from projective 3-space into the projective plane + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "proj_pt.h" +#include "axis-manip.h" + +namespace Proj { + +class TransfMat3x4 { +public: + TransfMat3x4(); + TransfMat3x4(Pt2 vp_x, Pt2 vp_y, Pt2 vp_z, Pt2 origin); + TransfMat3x4(TransfMat3x4 const &rhs); + Pt2 column (Proj::Axis axis) const; + Pt2 image (Pt3 const &point); + Pt3 preimage (Geom::Point const &pt, double coord = 0, Axis = Z); + void set_image_pt (Proj::Axis axis, Proj::Pt2 const &pt); + void toggle_finite (Proj::Axis axis); + double get_infinite_angle (Proj::Axis axis) { + if (has_finite_image(axis)) { + return Geom::infinity(); + } + Pt2 vp(column(axis)); + return Geom::atan2(Geom::Point(vp[0], vp[1])) * 180.0/M_PI; + } + void set_infinite_direction (Proj::Axis axis, double angle) { // angle is in degrees + if (tmat[2][axis] != 0) return; // don't set directions for finite VPs + + double a = angle * M_PI/180; + Geom::Point pt(tmat[0][axis], tmat[1][axis]); + double rad = Geom::L2(pt); + set_image_pt(axis, Proj::Pt2(cos (a) * rad, sin (a) * rad, 0.0)); + } + inline bool has_finite_image (Proj::Axis axis) { return (tmat[2][axis] != 0.0); } + + char * pt_to_str (Proj::Axis axis); + + bool operator==(const TransfMat3x4 &rhs) const; + TransfMat3x4 operator*(Geom::Affine const &A) const; + TransfMat3x4 &operator*=(Geom::Affine const &A); + + void print() const; + + void copy_tmat(double rhs[3][4]); + +private: + // FIXME: Is changing a single column allowed when a projective coordinate system is specified!?!?! + void normalize_column (Proj::Axis axis); + inline void set_column (Proj::Axis axis, Proj::Pt2 pt) { + tmat[0][axis] = pt[0]; + tmat[1][axis] = pt[1]; + tmat[2][axis] = pt[2]; + } + double tmat[3][4]; +}; + +} // namespace Proj + +#endif /* __TRANSF_MAT_3x4_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt new file mode 100644 index 0000000..de23597 --- /dev/null +++ b/src/ui/CMakeLists.txt @@ -0,0 +1,477 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(ui_SRC + clipboard.cpp + contextmenu.cpp + control-manager.cpp + dialog-events.cpp + draw-anchor.cpp + drag-and-drop.cpp + icon-loader.cpp + interface.cpp + monitor.cpp + pref-pusher.cpp + previewholder.cpp + selected-color.cpp + shape-editor.cpp + shape-editor-knotholders.cpp + simple-pref-pusher.cpp + tool-factory.cpp + tools-switch.cpp + util.cpp + uxmanager.cpp + + cache/svg_preview_cache.cpp + + desktop/menubar.cpp + + tool/control-point-selection.cpp + tool/control-point.cpp + tool/curve-drag-point.cpp + tool/event-utils.cpp + tool/manipulator.cpp + tool/modifier-tracker.cpp + tool/multi-path-manipulator.cpp + tool/node.cpp + tool/path-manipulator.cpp + tool/selectable-control-point.cpp + tool/selector.cpp + tool/transform-handle-set.cpp + + toolbar/arc-toolbar.cpp + toolbar/box3d-toolbar.cpp + toolbar/calligraphy-toolbar.cpp + toolbar/connector-toolbar.cpp + toolbar/dropper-toolbar.cpp + toolbar/eraser-toolbar.cpp + toolbar/gradient-toolbar.cpp + toolbar/lpe-toolbar.cpp + toolbar/measure-toolbar.cpp + toolbar/mesh-toolbar.cpp + toolbar/node-toolbar.cpp + toolbar/paintbucket-toolbar.cpp + toolbar/pencil-toolbar.cpp + toolbar/rect-toolbar.cpp + toolbar/select-toolbar.cpp + toolbar/snap-toolbar.cpp + toolbar/spiral-toolbar.cpp + toolbar/spray-toolbar.cpp + toolbar/star-toolbar.cpp + toolbar/text-toolbar.cpp + toolbar/toolbar.cpp + toolbar/tweak-toolbar.cpp + toolbar/zoom-toolbar.cpp + + tools/arc-tool.cpp + tools/box3d-tool.cpp + tools/calligraphic-tool.cpp + tools/connector-tool.cpp + tools/dropper-tool.cpp + tools/dynamic-base.cpp + tools/eraser-tool.cpp + tools/flood-tool.cpp + tools/freehand-base.cpp + tools/gradient-tool.cpp + tools/lpe-tool.cpp + tools/measure-tool.cpp + tools/mesh-tool.cpp + tools/node-tool.cpp + tools/pencil-tool.cpp + tools/pen-tool.cpp + tools/rect-tool.cpp + tools/select-tool.cpp + tools/spiral-tool.cpp + tools/spray-tool.cpp + tools/star-tool.cpp + tools/text-tool.cpp + tools/tool-base.cpp + tools/tweak-tool.cpp + tools/zoom-tool.cpp + + dialog/aboutbox.cpp + dialog/align-and-distribute.cpp + dialog/calligraphic-profile-rename.cpp + dialog/clonetiler.cpp + dialog/color-item.cpp + dialog/attrdialog.cpp + dialog/debug.cpp + dialog/desktop-tracker.cpp + dialog/dialog-manager.cpp + dialog/dialog.cpp + dialog/dock-behavior.cpp + dialog/document-metadata.cpp + dialog/document-properties.cpp + dialog/export.cpp + dialog/extension-editor.cpp + dialog/extensions.cpp + dialog/filedialog.cpp + dialog/filedialogimpl-gtkmm.cpp + dialog/fill-and-stroke.cpp + dialog/filter-editor.cpp + dialog/filter-effects-dialog.cpp + dialog/find.cpp + dialog/floating-behavior.cpp + dialog/font-substitution.cpp + dialog/glyphs.cpp + dialog/grid-arrange-tab.cpp + dialog/guides.cpp + dialog/icon-preview.cpp + dialog/inkscape-preferences.cpp + dialog/input.cpp + dialog/knot-properties.cpp + dialog/layer-properties.cpp + dialog/layers.cpp + dialog/livepatheffect-add.cpp + dialog/livepatheffect-editor.cpp + dialog/lpe-fillet-chamfer-properties.cpp + dialog/lpe-powerstroke-properties.cpp + dialog/memory.cpp + dialog/messages.cpp + dialog/new-from-template.cpp + dialog/object-attributes.cpp + dialog/object-properties.cpp + dialog/objects.cpp + dialog/polar-arrange-tab.cpp + dialog/print-colors-preview-dialog.cpp + dialog/print.cpp + dialog/selectorsdialog.cpp + dialog/styledialog.cpp + dialog/svg-fonts-dialog.cpp + dialog/svg-preview.cpp + dialog/swatches.cpp + dialog/symbols.cpp + dialog/paint-servers.cpp + dialog/tags.cpp + dialog/template-load-tab.cpp + dialog/template-widget.cpp + dialog/text-edit.cpp + dialog/tile.cpp + dialog/tracedialog.cpp + dialog/transformation.cpp + dialog/undo-history.cpp + dialog/xml-tree.cpp + dialog/save-template-dialog.cpp + + widget/iconrenderer.cpp + widget/alignment-selector.cpp + widget/anchor-selector.cpp + widget/button.cpp + widget/clipmaskicon.cpp + widget/color-entry.cpp + widget/color-icc-selector.cpp + widget/color-notebook.cpp + widget/color-picker.cpp + widget/color-preview.cpp icon-loader.cpp + widget/color-scales.cpp + widget/color-slider.cpp + widget/color-wheel-selector.cpp + widget/combo-box-entry-tool-item.cpp + widget/combo-tool-item.cpp + widget/dash-selector.cpp + widget/dock-item.cpp + widget/dock.cpp + widget/entity-entry.cpp + widget/entry.cpp + widget/filter-effect-chooser.cpp + widget/font-button.cpp + widget/font-selector.cpp + widget/font-selector-toolbar.cpp + widget/font-variants.cpp + widget/font-variations.cpp + widget/frame.cpp + widget/highlight-picker.cpp + widget/imagetoggler.cpp + widget/ink-color-wheel.cpp + widget/ink-flow-box.cpp + widget/ink-ruler.cpp + widget/ink-spinscale.cpp + widget/insertordericon.cpp + widget/label-tool-item.cpp + widget/labelled.cpp + widget/layer-selector.cpp + widget/layertypeicon.cpp + widget/licensor.cpp + widget/notebook-page.cpp + widget/object-composite-settings.cpp + widget/page-sizer.cpp + widget/panel.cpp + widget/point.cpp + widget/preferences-widget.cpp + widget/preview.cpp + widget/random.cpp + widget/registered-widget.cpp + widget/registry.cpp + widget/rendering-options.cpp + widget/rotateable.cpp + widget/scalar-unit.cpp + widget/scalar.cpp + widget/selected-style.cpp + widget/spin-button-tool-item.cpp + widget/spin-scale.cpp + widget/spin-slider.cpp + widget/spinbutton.cpp + widget/style-subject.cpp + widget/style-swatch.cpp + widget/text.cpp + widget/tolerance-slider.cpp + widget/unit-menu.cpp + widget/unit-tracker.cpp + + view/svg-view-widget.cpp + view/view.cpp + view/view-widget.cpp + + + # ------- + # Headers + clipboard.h + contextmenu.h + control-manager.h + control-types.h + dialog-events.h + drag-and-drop.h + draw-anchor.h + event-debug.h + icon-names.h + icon-loader.h + interface.h + monitor.h + pref-pusher.h + previewable.h + previewholder.h + selected-color.h + shape-editor.h + simple-pref-pusher.h + tool-factory.h + tools-switch.h + util.h + uxmanager.h + + cache/svg_preview_cache.h + + desktop/menubar.cpp + + dialog/aboutbox.h + dialog/align-and-distribute.h + dialog/arrange-tab.h + dialog/behavior.h + dialog/calligraphic-profile-rename.h + dialog/clonetiler.h + dialog/color-item.h + dialog/attrdialog.h + dialog/debug.h + dialog/desktop-tracker.h + dialog/dialog-manager.h + dialog/dialog.h + dialog/dock-behavior.h + dialog/document-metadata.h + dialog/document-properties.h + dialog/export.h + dialog/extension-editor.h + dialog/extensions.h + dialog/filedialog.h + dialog/filedialogimpl-gtkmm.h + dialog/filedialogimpl-win32.h + dialog/fill-and-stroke.h + dialog/filter-editor.h + dialog/filter-effects-dialog.h + dialog/find.h + dialog/floating-behavior.h + dialog/font-substitution.h + dialog/glyphs.h + dialog/grid-arrange-tab.h + dialog/guides.h + dialog/icon-preview.h + dialog/inkscape-preferences.h + dialog/input.h + dialog/knot-properties.h + dialog/layer-properties.h + dialog/layers.h + dialog/livepatheffect-add.h + dialog/livepatheffect-editor.h + dialog/lpe-fillet-chamfer-properties.h + dialog/lpe-powerstroke-properties.h + dialog/memory.h + dialog/messages.h + dialog/new-from-template.h + dialog/object-attributes.h + dialog/object-properties.h + dialog/objects.h + dialog/panel-dialog.h + dialog/polar-arrange-tab.h + dialog/print-colors-preview-dialog.h + dialog/print.h + dialog/selectorsdialog.h + dialog/styledialog.h + dialog/svg-fonts-dialog.h + dialog/svg-preview.h + dialog/swatches.h + dialog/symbols.h + dialog/paint-servers.h + dialog/tags.h + dialog/template-load-tab.h + dialog/template-widget.h + dialog/text-edit.h + dialog/tile.h + dialog/tracedialog.h + dialog/transformation.h + dialog/undo-history.h + dialog/xml-tree.h + dialog/save-template-dialog.h + + tool/commit-events.h + tool/control-point-selection.h + tool/control-point.h + tool/curve-drag-point.h + tool/event-utils.h + tool/manipulator.h + tool/modifier-tracker.h + tool/multi-path-manipulator.h + tool/node-types.h + tool/node.h + tool/path-manipulator.h + tool/selectable-control-point.h + tool/selector.h + tool/shape-record.h + tool/transform-handle-set.h + + toolbar/arc-toolbar.h + toolbar/box3d-toolbar.h + toolbar/calligraphy-toolbar.h + toolbar/connector-toolbar.h + toolbar/dropper-toolbar.h + toolbar/eraser-toolbar.h + toolbar/gradient-toolbar.h + toolbar/lpe-toolbar.h + toolbar/measure-toolbar.h + toolbar/mesh-toolbar.h + toolbar/node-toolbar.h + toolbar/paintbucket-toolbar.h + toolbar/pencil-toolbar.h + toolbar/rect-toolbar.h + toolbar/select-toolbar.h + toolbar/snap-toolbar.h + toolbar/spiral-toolbar.h + toolbar/spray-toolbar.h + toolbar/star-toolbar.h + toolbar/text-toolbar.h + toolbar/toolbar.h + toolbar/tweak-toolbar.h + toolbar/zoom-toolbar.h + + tools/arc-tool.h + tools/box3d-tool.h + tools/calligraphic-tool.h + tools/connector-tool.h + tools/dropper-tool.h + tools/dynamic-base.h + tools/eraser-tool.h + tools/flood-tool.h + tools/freehand-base.h + tools/gradient-tool.h + tools/lpe-tool.h + tools/measure-tool.h + tools/mesh-tool.h + tools/node-tool.h + tools/pen-tool.h + tools/pencil-tool.h + tools/rect-tool.h + tools/select-tool.h + tools/spiral-tool.h + tools/spray-tool.h + tools/star-tool.h + tools/text-tool.h + tools/tool-base.h + tools/tweak-tool.h + tools/zoom-tool.h + + widget/iconrenderer.h + widget/alignment-selector.h + widget/anchor-selector.h + widget/attr-widget.h + widget/button.h + widget/clipmaskicon.h + widget/color-entry.h + widget/color-icc-selector.h + widget/color-notebook.h + widget/color-picker.h + widget/color-preview.h + widget/color-scales.h + widget/color-slider.h + widget/color-wheel-selector.h + widget/combo-enums.h + widget/combo-box-entry-tool-item.h + widget/combo-tool-item.h + widget/dash-selector.h + widget/dock-item.h + widget/dock.h + widget/entity-entry.h + widget/entry.h + widget/filter-effect-chooser.h + widget/font-button.h + widget/font-selector.h + widget/font-selector-toolbar.h + widget/font-variants.h + widget/font-variations.h + widget/frame.h + widget/highlight-picker.h + widget/insertordericon.h + widget/imagetoggler.h + widget/ink-color-wheel.h + widget/ink-flow-box.h + widget/ink-ruler.h + widget/ink-spinscale.h + widget/label-tool-item.h + widget/labelled.h + widget/layer-selector.h + widget/layertypeicon.h + widget/licensor.h + widget/notebook-page.h + widget/object-composite-settings.h + widget/page-sizer.h + widget/pages-skeleton.h + widget/panel.h + widget/point.h + widget/preferences-widget.h + widget/preview.h + widget/random.h + widget/registered-enums.h + widget/registered-widget.h + widget/registry.h + widget/rendering-options.h + widget/rotateable.h + widget/scalar-unit.h + widget/scalar.h + widget/selected-style.h + widget/spin-button-tool-item.h + widget/spin-scale.h + widget/spin-slider.h + widget/spinbutton.h + widget/style-subject.h + widget/style-swatch.h + widget/text.h + widget/tolerance-slider.h + widget/unit-menu.h + widget/unit-tracker.h + + view/edit-widget-interface.h + view/svg-view-widget.h + view/view.h + view/view-widget.h +) + +if(WIN32) + list(APPEND ui_SRC + dialog/filedialogimpl-win32.cpp + ) +endif() + +add_inkscape_source("${ui_SRC}") + +set ( ui_spellcheck_SRC + dialog/spellcheck.cpp + dialog/spellcheck.h +) + +if ("${HAVE_ASPELL}") + add_inkscape_source("${ui_spellcheck_SRC}") +endif() diff --git a/src/ui/README b/src/ui/README new file mode 100644 index 0000000..3929271 --- /dev/null +++ b/src/ui/README @@ -0,0 +1,21 @@ + + +This directory contains all the code related to the Graphical User Interface. + +To do: + +* Move all GTK related code here. +* Better organize directories: + + - cache + - dialog + - menu + - onscreen + - toolbar + - tools + - view? + - widget + -- basic + -- composite + -- etc. + diff --git a/src/ui/cache/README b/src/ui/cache/README new file mode 100644 index 0000000..1cdbaee --- /dev/null +++ b/src/ui/cache/README @@ -0,0 +1,3 @@ +This directory is for utility code for maintaining caches of UI things, +such as symbol previews, thumbnails, font lists, brushes, dashes, etc. + diff --git a/src/ui/cache/svg_preview_cache.cpp b/src/ui/cache/svg_preview_cache.cpp new file mode 100644 index 0000000..d6a35fb --- /dev/null +++ b/src/ui/cache/svg_preview_cache.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * SVGPreview: Preview cache + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Bryce Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <gtk/gtk.h> + +#include <2geom/transforms.h> + +#include "selection.h" +#include "inkscape.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing.h" + + +#include "ui/cache/svg_preview_cache.h" + +GdkPixbuf* render_pixbuf(Inkscape::Drawing &drawing, double scale_factor, Geom::Rect const &dbox, unsigned psize) +{ + drawing.root()->setTransform(Geom::Scale(scale_factor)); + + Geom::IntRect ibox = (dbox * Geom::Scale(scale_factor)).roundOutwards(); + + drawing.update(ibox); + + /* Find visible area */ + int width = ibox.width(); + int height = ibox.height(); + int dx = psize; + int dy = psize; + dx = (dx - width)/2; // watch out for size, since 'unsigned'-'signed' can cause problems if the result is negative + dy = (dy - height)/2; + + Geom::IntRect area = Geom::IntRect::from_xywh( + ibox.min() - Geom::IntPoint(dx, dy), Geom::IntPoint(psize, psize)); + + /* Render */ + cairo_surface_t *s = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, psize, psize); + Inkscape::DrawingContext dc(s, area.min()); + + drawing.render(dc, area, Inkscape::DrawingItem::RENDER_BYPASS_CACHE); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +namespace Inkscape { +namespace UI { +namespace Cache { + +SvgPreview::SvgPreview() += default; + +SvgPreview::~SvgPreview() +{ + for (auto & i : _pixmap_cache) + { + g_object_unref(i.second); + i.second = NULL; + } +} + +Glib::ustring SvgPreview::cache_key(gchar const *uri, gchar const *name, unsigned psize) const { + Glib::ustring key; + key += (uri!=nullptr) ? uri : ""; + key += ":"; + key += (name!=nullptr) ? name : "unknown"; + key += ":"; + key += psize; + return key; +} + +GdkPixbuf* SvgPreview::get_preview_from_cache(const Glib::ustring& key) { + std::map<Glib::ustring, GdkPixbuf *>::iterator found = _pixmap_cache.find(key); + if ( found != _pixmap_cache.end() ) { + return found->second; + } + return nullptr; +} + +void SvgPreview::set_preview_in_cache(const Glib::ustring& key, GdkPixbuf* px) { + g_object_ref(px); + _pixmap_cache[key] = px; +} + +GdkPixbuf* SvgPreview::get_preview(const gchar* uri, const gchar* id, Inkscape::DrawingItem */*root*/, + double /*scale_factor*/, unsigned int psize) { + // First try looking up the cached preview in the cache map + Glib::ustring key = cache_key(uri, id, psize); + GdkPixbuf* px = get_preview_from_cache(key); + + if (px == nullptr) { + /* + px = render_pixbuf(root, scale_factor, dbox, psize); + set_preview_in_cache(key, px); + */ + } + return px; +} + +void SvgPreview::remove_preview_from_cache(const Glib::ustring& key) { + std::map<Glib::ustring, GdkPixbuf *>::iterator found = _pixmap_cache.find(key); + if ( found != _pixmap_cache.end() ) { + g_object_unref(found->second); + found->second = NULL; + _pixmap_cache.erase(key); + } +} + + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/cache/svg_preview_cache.h b/src/ui/cache/svg_preview_cache.h new file mode 100644 index 0000000..19c4548 --- /dev/null +++ b/src/ui/cache/svg_preview_cache.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Preview cache + */ +/* + * Copyright (C) 2007 Bryce W. Harrington <bryce@bryceharrington.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H +#define SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H + +#include <map> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <glibmm/ustring.h> +#include <2geom/rect.h> + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +} // namespace Inkscape + + +GdkPixbuf* render_pixbuf(Inkscape::Drawing &drawing, double scale_factor, const Geom::Rect& dbox, unsigned psize); + +namespace Inkscape { +namespace UI { +namespace Cache { + +class SvgPreview { + protected: + std::map<Glib::ustring, GdkPixbuf*> _pixmap_cache; + + public: + SvgPreview(); + ~SvgPreview(); + + Glib::ustring cache_key(gchar const *uri, gchar const *name, unsigned psize) const; + GdkPixbuf* get_preview_from_cache(const Glib::ustring& key); + void set_preview_in_cache(const Glib::ustring& key, GdkPixbuf* px); + GdkPixbuf* get_preview(const gchar* uri, const gchar* id, Inkscape::DrawingItem *root, double scale_factor, unsigned int psize); + void remove_preview_from_cache(const Glib::ustring& key); +}; + +}; // namespace Cache +}; // namespace UI +}; // namespace Inkscape + + + +#endif // SEEN_INKSCAPE_UI_SVG_PREVIEW_CACHE_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/clipboard.cpp b/src/ui/clipboard.cpp new file mode 100644 index 0000000..0914293 --- /dev/null +++ b/src/ui/clipboard.cpp @@ -0,0 +1,1682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * System-wide clipboard management - implementation. + *//* + * Authors: + * see git history + * Krzysztof KosiÅ„ski <tweenk@o2.pl> + * Jon A. Cruz <jon@joncruz.org> + * Incorporates some code from selection-chemistry.cpp, see that file for more credits. + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <giomm/application.h> +#include <gtkmm/clipboard.h> +#include "ui/clipboard.h" + +// TODO: reduce header bloat if possible + +#include "file.h" // for file_import, used in _pasteImage +#include <glibmm/i18n.h> +#include <glib/gstdio.h> // for g_file_set_contents etc., used in _onGet and paste +#include "inkgc/gc-core.h" +#include "xml/repr.h" +#include "xml/sp-css-attr.h" +#include "inkscape.h" +#include "desktop.h" + +#include "desktop-style.h" // for sp_desktop_set_style, used in _pasteStyle +#include "document.h" +#include "message-stack.h" +#include "context-fns.h" +#include "ui/tools/dropper-tool.h" // used in copy() +#include "extension/db.h" // extension database +#include "extension/input.h" +#include "extension/output.h" +#include "selection-chemistry.h" +#include <2geom/transforms.h> +#include "gradient-drag.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/parameter/path.h" +#include "ui/tools/text-tool.h" +#include "text-editing.h" +#include "text-chemistry.h" +#include "ui/tools-switch.h" +#include "path-chemistry.h" +#include "util/units.h" +#include "helper/png-write.h" +#include "extension/find_extension_by_mime.h" + +#include "object/box3d.h" +#include "object/persp3d.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-hatch.h" +#include "object/sp-item-transform.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-pattern.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-flowtext.h" +#include "object/sp-textpath.h" +#include "object/sp-use.h" +#include "style.h" + +#include "svg/svg.h" // for sp_svg_transform_write, used in _copySelection +#include "svg/css-ostringstream.h" // used in copy +#include "svg/svg-color.h" + +/// Made up mimetype to represent Gdk::Pixbuf clipboard contents. +#define CLIPBOARD_GDK_PIXBUF_TARGET "image/x-gdk-pixbuf" + +#define CLIPBOARD_TEXT_TARGET "text/plain" + +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace Inkscape { +namespace UI { + + +/** + * Default implementation of the clipboard manager. + */ +class ClipboardManagerImpl : public ClipboardManager { +public: + void copy(ObjectSet *set) override; + void copyPathParameter(Inkscape::LivePathEffect::PathParam *) override; + void copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol) override; + bool paste(SPDesktop *desktop, bool in_place) override; + bool pasteStyle(ObjectSet *set) override; + bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) override; + bool pastePathEffect(ObjectSet *set) override; + Glib::ustring getPathParameter(SPDesktop* desktop) override; + Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) override; + std::vector<Glib::ustring> getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) override; + const gchar *getFirstObjectID() override; + + ClipboardManagerImpl(); + ~ClipboardManagerImpl() override; + +private: + void _copySelection(ObjectSet *); + void _copyUsedDefs(SPItem *); + void _copyGradient(SPGradient *); + void _copyPattern(SPPattern *); + void _copyHatch(SPHatch *); + void _copyTextPath(SPTextPath *); + Inkscape::XML::Node *_copyNode(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + Inkscape::XML::Node *_copyIgnoreDup(Inkscape::XML::Node *, Inkscape::XML::Document *, Inkscape::XML::Node *); + + bool _pasteImage(SPDocument *doc); + bool _pasteText(SPDesktop *desktop); + void _applyPathEffect(SPItem *, gchar const *); + SPDocument *_retrieveClipboard(Glib::ustring = ""); + + // clipboard callbacks + void _onGet(Gtk::SelectionData &, guint); + void _onClear(); + + // various helpers + void _createInternalClipboard(); + void _discardInternalClipboard(); + Inkscape::XML::Node *_createClipNode(); + Geom::Scale _getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y); + Glib::ustring _getBestTarget(); + void _setClipboardTargets(); + void _setClipboardColor(guint32); + void _userWarn(SPDesktop *, char const *); + + // private properites + SPDocument *_clipboardSPDoc; ///< Document that stores the clipboard until someone requests it + Inkscape::XML::Node *_defs; ///< Reference to the clipboard document's defs node + Inkscape::XML::Node *_root; ///< Reference to the clipboard's root node + Inkscape::XML::Node *_clipnode; ///< The node that holds extra information + Inkscape::XML::Document *_doc; ///< Reference to the clipboard's Inkscape::XML::Document + std::set<SPItem*> cloned_elements; + std::vector<SPCSSAttr*> te_selected_style; + std::vector<unsigned> te_selected_style_positions; + int nr_blocks = 0; + unsigned copied_style_length = 0; + + + // we need a way to copy plain text AND remember its style; + // the standard _clipnode is only available in an SVG tree, hence this special storage + SPCSSAttr *_text_style; ///< Style copied along with plain text fragment + + Glib::RefPtr<Gtk::Clipboard> _clipboard; ///< Handle to the system wide clipboard - for convenience + std::list<Glib::ustring> _preferred_targets; ///< List of supported clipboard targets +}; + + +ClipboardManagerImpl::ClipboardManagerImpl() + : _clipboardSPDoc(nullptr), + _defs(nullptr), + _root(nullptr), + _clipnode(nullptr), + _doc(nullptr), + _text_style(nullptr), + _clipboard( Gtk::Clipboard::get() ) +{ + // Clipboard Formats: http://msdn.microsoft.com/en-us/library/ms649013(VS.85).aspx + // On Windows, most graphical applications can handle CF_DIB/CF_BITMAP and/or CF_ENHMETAFILE + // GTK automatically presents an "image/bmp" target as CF_DIB/CF_BITMAP + // Presenting "image/x-emf" as CF_ENHMETAFILE must be done by Inkscape ? + + // push supported clipboard targets, in order of preference + _preferred_targets.emplace_back("image/x-inkscape-svg"); + _preferred_targets.emplace_back("image/svg+xml"); + _preferred_targets.emplace_back("image/svg+xml-compressed"); + _preferred_targets.emplace_back("image/x-emf"); + _preferred_targets.emplace_back("CF_ENHMETAFILE"); + _preferred_targets.emplace_back("WCF_ENHMETAFILE"); // seen on Wine + _preferred_targets.emplace_back("application/pdf"); + _preferred_targets.emplace_back("image/x-adobe-illustrator"); + + // Clipboard requests on app termination can cause undesired extension + // popup windows. Clearing the clipboard can prevent this. + auto application = Gio::Application::get_default(); + if (application) { + application->signal_shutdown().connect_notify([this]() { this->_discardInternalClipboard(); }); + } +} + + +ClipboardManagerImpl::~ClipboardManagerImpl() = default; + + +/** + * Copy selection contents to the clipboard. + */ +void ClipboardManagerImpl::copy(ObjectSet *set) +{ + if ( set->desktop() ) { + SPDesktop *desktop = set->desktop(); + + // Special case for when the gradient dragger is active - copies gradient color + if (desktop->event_context->get_drag()) { + GrDrag *drag = desktop->event_context->get_drag(); + if (drag->hasSelection()) { + guint32 col = drag->getColor(); + + // set the color as clipboard content (text in RRGGBBAA format) + _setClipboardColor(col); + + // create a style with this color on fill and opacity in master opacity, so it can be + // pasted on other stops or objects + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = sp_repr_css_attr_new(); + // print and set properties + gchar color_str[16]; + g_snprintf(color_str, 16, "#%06x", col >> 8); + sp_repr_css_set_property(_text_style, "fill", color_str); + float opacity = SP_RGBA32_A_F(col); + if (opacity > 1.0) { + opacity = 1.0; // safeguard + } + Inkscape::CSSOStringStream opcss; + opcss << opacity; + sp_repr_css_set_property(_text_style, "opacity", opcss.str().data()); + + _discardInternalClipboard(); + return; + } + } + + // Special case for when the color picker ("dropper") is active - copies color under cursor + if (tools_isactive(desktop, TOOLS_DROPPER)) { + //_setClipboardColor(sp_dropper_context_get_color(desktop->event_context)); + _setClipboardColor(SP_DROPPER_CONTEXT(desktop->event_context)->get_color()); + _discardInternalClipboard(); + return; + } + + // Special case for when the text tool is active - if some text is selected, copy plain text, + // not the object that holds it; also copy the style at cursor into + if (tools_isactive(desktop, TOOLS_TEXT)) { + _discardInternalClipboard(); + Glib::ustring selected_text = Inkscape::UI::Tools::sp_text_get_selected_text(desktop->event_context); + _clipboard->set_text(selected_text); + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + _text_style = Inkscape::UI::Tools::sp_text_get_style_at_cursor(desktop->event_context); + return; + } + } + if (set->isEmpty()) { // check whether something is selected + _userWarn(set->desktop(), _("Nothing was copied.")); + return; + } + _discardInternalClipboard(); + + _createInternalClipboard(); // construct a new clipboard document + _copySelection(set); // copy all items in the selection to the internal clipboard + fit_canvas_to_drawing(_clipboardSPDoc); + + _setClipboardTargets(); +} + + +/** + * Copy a Live Path Effect path parameter to the clipboard. + * @param pp The path parameter to store in the clipboard. + */ +void ClipboardManagerImpl::copyPathParameter(Inkscape::LivePathEffect::PathParam *pp) +{ + if ( pp == nullptr ) { + return; + } + gchar *svgd = sp_svg_write_path( pp->get_pathvector() ); + if ( svgd == nullptr || *svgd == '\0' ) { + return; + } + + _discardInternalClipboard(); + _createInternalClipboard(); + + Inkscape::XML::Node *pathnode = _doc->createElement("svg:path"); + pathnode->setAttribute("d", svgd); + g_free(svgd); + _root->appendChild(pathnode); + Inkscape::GC::release(pathnode); + + fit_canvas_to_drawing(_clipboardSPDoc); + _setClipboardTargets(); +} + +/** + * Copy a symbol from the symbol dialog. + * @param symbol The Inkscape::XML::Node for the symbol. + */ +void ClipboardManagerImpl::copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol) +{ + //std::cout << "ClipboardManagerImpl::copySymbol" << std::endl; + if ( symbol == nullptr ) { + return; + } + + _discardInternalClipboard(); + _createInternalClipboard(); + + // We add "_duplicate" to have a well defined symbol name that + // bypasses the "prevent_id_classes" routine. We'll get rid of it + // when we paste. + Inkscape::XML::Node *repr = symbol->duplicate(_doc); + Glib::ustring symbol_name = repr->attribute("id"); + + symbol_name += "_inkscape_duplicate"; + repr->setAttribute("id", symbol_name); + _defs->appendChild(repr); + + Glib::ustring id("#"); + id += symbol->attribute("id"); + + gdouble scale_units = 1; // scale from "px" to "document-units" + Inkscape::XML::Node *nv_repr = SP_ACTIVE_DESKTOP->getNamedView()->getRepr(); + if (nv_repr->attribute("inkscape:document-units")) + scale_units = Inkscape::Util::Quantity::convert(1, "px", nv_repr->attribute("inkscape:document-units")); + SPObject *cmobj = _clipboardSPDoc->getObjectByRepr(repr); + if (cmobj && !user_symbol) { // convert only stock symbols + if (!Geom::are_near(scale_units, 1.0, Geom::EPSILON)) { + dynamic_cast<SPGroup *>(cmobj)->scaleChildItemsRec( + Geom::Scale(scale_units), Geom::Point(0, SP_ACTIVE_DESKTOP->getDocument()->getHeight().value("px")), + false); + } + } + + Inkscape::XML::Node *use = _doc->createElement("svg:use"); + use->setAttribute("xlink:href", id ); + // Set a default style in <use> rather than <symbol> so it can be changed. + use->setAttribute("style", style ); + if (!Geom::are_near(scale_units, 1.0, Geom::EPSILON)) { + gchar *transform_str = sp_svg_transform_write(Geom::Scale(1.0/scale_units)); + use->setAttribute("transform", transform_str); + g_free(transform_str); + } + _root->appendChild(use); + + // This min and max sets offsets, we don't have any so set to zero. + sp_repr_set_point(_clipnode, "min", Geom::Point(0,0)); + sp_repr_set_point(_clipnode, "max", Geom::Point(0,0)); + + fit_canvas_to_drawing(_clipboardSPDoc); + _setClipboardTargets(); +} + +/** + * Paste from the system clipboard into the active desktop. + * @param in_place Whether to put the contents where they were when copied. + */ +bool ClipboardManagerImpl::paste(SPDesktop *desktop, bool in_place) +{ + // do any checking whether we really are able to paste before requesting the contents + if ( desktop == nullptr ) { + return false; + } + if ( Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false ) { + return false; + } + + Glib::ustring target = _getBestTarget(); + + // Special cases of clipboard content handling go here + // Note that target priority is determined in _getBestTarget. + // TODO: Handle x-special/gnome-copied-files and text/uri-list to support pasting files + + // if there is an image on the clipboard, paste it + if ( target == CLIPBOARD_GDK_PIXBUF_TARGET ) { + return _pasteImage(desktop->doc()); + } + // if there's only text, paste it into a selected text object or create a new one + if ( target == CLIPBOARD_TEXT_TARGET ) { + return _pasteText(desktop); + } + + // otherwise, use the import extensions + SPDocument *tempdoc = _retrieveClipboard(target); + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return false; + } + + sp_import_document(desktop, tempdoc, in_place); + tempdoc->doUnref(); + + // _copySelection() has put all items in groups, now ungroup them (preserves transform + // relationships of clones, text-on-path, etc.) + desktop->selection->ungroup(); + + return true; +} + +/** + * Returns the id of the first visible copied object. + */ +const gchar *ClipboardManagerImpl::getFirstObjectID() +{ + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + return nullptr; + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + if (!root) { + return nullptr; + } + + Inkscape::XML::Node *ch = root->firstChild(); + while (ch != nullptr && + strcmp(ch->name(), "svg:g") && + strcmp(ch->name(), "svg:path") && + strcmp(ch->name(), "svg:use") && + strcmp(ch->name(), "svg:text") && + strcmp(ch->name(), "svg:image") && + strcmp(ch->name(), "svg:rect") && + strcmp(ch->name(), "svg:ellipse") + ) { + ch = ch->next(); + } + + if (ch) { + return ch->attribute("id"); + } + + return nullptr; +} + + +/** + * Implements the Paste Style action. + */ +bool ClipboardManagerImpl::pasteStyle(ObjectSet *set) +{ + if (set->desktop() == nullptr) { + return false; + } + + // check whether something is selected + if (set->isEmpty()) { + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste style to.")); + return false; + } + + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + // no document, but we can try _text_style + if (_text_style) { + sp_desktop_set_style(set, set->desktop(), _text_style); + return true; + } else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + return false; + } + } + + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + + bool pasted = false; + + if (clipnode) { + set->document()->importDefs(tempdoc); + SPCSSAttr *style = sp_repr_css_attr(clipnode, "style"); + sp_desktop_set_style(set, set->desktop(), style); + pasted = true; + } + else { + _userWarn(set->desktop(), _("No style on the clipboard.")); + } + + tempdoc->doUnref(); + return pasted; +} + + +/** + * Resize the selection or each object in the selection to match the clipboard's size. + * @param separately Whether to scale each object in the selection separately + * @param apply_x Whether to scale the width of objects / selection + * @param apply_y Whether to scale the height of objects / selection + */ +bool ClipboardManagerImpl::pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) +{ + if (!apply_x && !apply_y) { + return false; // pointless parameters + } + +/* if ( desktop == NULL ) { + return false; + } + Inkscape::Selection *selection = desktop->getSelection();*/ + if (set->isEmpty()) { + if(set->desktop()) + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste size to.")); + return false; + } + + // FIXME: actually, this should accept arbitrary documents + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc == nullptr ) { + if(set->desktop()) + _userWarn(set->desktop(), _("No size on the clipboard.")); + return false; + } + + // retrieve size information from the clipboard + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + bool pasted = false; + if (clipnode) { + Geom::Point min, max; + sp_repr_get_point(clipnode, "min", &min); + sp_repr_get_point(clipnode, "max", &max); + + // resize each object in the selection + if (separately) { + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (item) { + Geom::OptRect obj_size = item->desktopVisualBounds(); + if ( obj_size ) { + item->scale_rel(_getScale(set->desktop(), min, max, *obj_size, apply_x, apply_y)); + } + } else { + g_assert_not_reached(); + } + } + } + // resize the selection as a whole + else { + Geom::OptRect sel_size = set->visualBounds(); + if ( sel_size ) { + set->setScaleRelative(sel_size->midpoint(), + _getScale(set->desktop(), min, max, *sel_size, apply_x, apply_y)); + } + } + pasted = true; + } + tempdoc->doUnref(); + return pasted; +} + + +/** + * Applies a path effect from the clipboard to the selected path. + */ +bool ClipboardManagerImpl::pastePathEffect(ObjectSet *set) +{ + /** @todo FIXME: pastePathEffect crashes when moving the path with the applied effect, + segfaulting in fork_private_if_necessary(). */ + + if ( set->desktop() == nullptr ) { + return false; + } + + //Inkscape::Selection *selection = desktop->getSelection(); + if (!set || set->isEmpty()) { + _userWarn(set->desktop(), _("Select <b>object(s)</b> to paste live path effect to.")); + return false; + } + + SPDocument *tempdoc = _retrieveClipboard("image/x-inkscape-svg"); + if ( tempdoc ) { + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + if ( clipnode ) { + gchar const *effectstack = clipnode->attribute("inkscape:path-effect"); + if ( effectstack ) { + set->document()->importDefs(tempdoc); + // make sure all selected items are converted to paths first (i.e. rectangles) + set->toLPEItems(); + auto itemlist= set->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + _applyPathEffect(item, effectstack); + } + + return true; + } + } + } + + // no_effect: + _userWarn(set->desktop(), _("No effect on the clipboard.")); + return false; +} + + +/** + * Get LPE path data from the clipboard. + * @return The retrieved path data (contents of the d attribute), or "" if no path was found + */ +Glib::ustring ClipboardManagerImpl::getPathParameter(SPDesktop* desktop) +{ + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + Inkscape::XML::Node *path = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth + if ( path == nullptr ) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + tempdoc->doUnref(); + return ""; + } + gchar const *svgd = path->attribute("d"); + return svgd; +} + + +/** + * Get object id of a shape or text item from the clipboard. + * @return The retrieved id string (contents of the id attribute), or "" if no shape or text item was found. + */ +Glib::ustring ClipboardManagerImpl::getShapeOrTextObjectId(SPDesktop *desktop) +{ + // https://bugs.launchpad.net/inkscape/+bug/1293979 + // basically, when we do a depth-first search, we're stopping + // at the first object to be <svg:path> or <svg:text>. + // but that could then return the id of the object's + // clip path or mask, not the original path! + + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return ""; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + + Inkscape::XML::Node *repr = sp_repr_lookup_name(root, "svg:path", -1); // unlimited search depth + if ( repr == nullptr ) { + repr = sp_repr_lookup_name(root, "svg:text", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:ellipse", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:rect", -1); + } + if (repr == nullptr) { + repr = sp_repr_lookup_name(root, "svg:circle", -1); + } + + + if ( repr == nullptr ) { + _userWarn(desktop, _("Clipboard does not contain a path.")); + tempdoc->doUnref(); + return ""; + } + gchar const *svgd = repr->attribute("id"); + return svgd; +} + +/** + * Get all objects id from the clipboard. + * @return A vector containing all IDs or empty if no shape or text item was found. + * type. Set to "*" to retrieve all elements of the types vector inside, feel free to populate more + */ +std::vector<Glib::ustring> ClipboardManagerImpl::getElementsOfType(SPDesktop *desktop, gchar const* type, gint maxdepth) +{ + std::vector<Glib::ustring> result; + SPDocument *tempdoc = _retrieveClipboard(); // any target will do here + if ( tempdoc == nullptr ) { + _userWarn(desktop, _("Nothing on the clipboard.")); + return result; + } + Inkscape::XML::Node *root = tempdoc->getReprRoot(); + + // 1293979: strip out the defs of the document + root->removeChild(tempdoc->getDefs()->getRepr()); + std::vector<Inkscape::XML::Node const *> reprs; + if (strcmp(type, "*") == 0){ + //TODO:Fill vector with all possible elements + std::vector<Glib::ustring> types; + types.push_back((Glib::ustring)"svg:path"); + types.push_back((Glib::ustring)"svg:circle"); + types.push_back((Glib::ustring)"svg:rect"); + types.push_back((Glib::ustring)"svg:ellipse"); + types.push_back((Glib::ustring)"svg:text"); + types.push_back((Glib::ustring)"svg:use"); + types.push_back((Glib::ustring)"svg:g"); + types.push_back((Glib::ustring)"svg:image"); + for (auto type_elem : types) { + std::vector<Inkscape::XML::Node const *> reprs_found = sp_repr_lookup_name_many(root, type_elem.c_str(), maxdepth); // unlimited search depth + reprs.insert(reprs.end(), reprs_found.begin(), reprs_found.end()); + } + } else { + reprs = sp_repr_lookup_name_many(root, type, maxdepth); + } + for (auto node : reprs) { + result.emplace_back(node->attribute("id")); + } + if ( result.empty() ) { + _userWarn(desktop, ((Glib::ustring)_("Clipboard does not contain any.") + (Glib::ustring)type).c_str()); + tempdoc->doUnref(); + return result; + } + return result; +} + +/** + * Iterate over a list of items and copy them to the clipboard. + */ +void ClipboardManagerImpl::_copySelection(ObjectSet *selection) +{ + // copy the defs used by all items + auto itemlist= selection->items(); + cloned_elements.clear(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (item) { + _copyUsedDefs(item); + } else { + g_assert_not_reached(); + } + } + + // copy the representation of the items + std::vector<SPObject*> sorted_items(itemlist.begin(), itemlist.end()); + { + // Get external text references and add them to sorted_items + auto ext_refs = text_categorize_refs(selection->document(), + sorted_items.begin(), sorted_items.end(), + TEXT_REF_EXTERNAL); + for (auto const &ext_ref : ext_refs) { + sorted_items.push_back(selection->document()->getObjectById(ext_ref.first)); + } + } + sort(sorted_items.begin(), sorted_items.end(), sp_object_compare_position_bool); + + //remove already copied elements from cloned_elements + std::vector<SPItem*>tr; + for(auto cloned_element : cloned_elements){ + if(std::find(sorted_items.begin(),sorted_items.end(),cloned_element)!=sorted_items.end()) + tr.push_back(cloned_element); + } + for(auto & it : tr){ + cloned_elements.erase(it); + } + + // One group per shared parent + std::map<SPObject const *, Inkscape::XML::Node *> groups; + + sorted_items.insert(sorted_items.end(),cloned_elements.begin(),cloned_elements.end()); + for(auto sorted_item : sorted_items){ + SPItem *item = dynamic_cast<SPItem*>(sorted_item); + if (item) { + // Create a group with the parent transform. This group will be ungrouped when pasting + // und takes care of transform relationships of clones, text-on-path, etc. + auto &group = groups[item->parent]; + if (!group) { + group = _doc->createElement("svg:g"); + _root->appendChild(group); + Inkscape::GC::release(group); + + if (auto parent = dynamic_cast<SPItem *>(item->parent)) { + auto transform_str = sp_svg_transform_write(parent->i2doc_affine()); + group->setAttributeOrRemoveIfEmpty("transform", transform_str); + g_free(transform_str); + } + } + + Inkscape::XML::Node *obj = item->getRepr(); + Inkscape::XML::Node *obj_copy; + if(cloned_elements.find(item)==cloned_elements.end()) + obj_copy = _copyNode(obj, _doc, group); + else + obj_copy = _copyNode(obj, _doc, _clipnode); + + // copy complete inherited style + SPCSSAttr *css = sp_repr_css_attr_inherited(obj, "style"); + for (auto iter : item->style->properties()) { + if (iter->style_src == SP_STYLE_SRC_STYLE_SHEET) { + css->setAttributeOrRemoveIfEmpty(iter->name(), iter->get_value()); + } + } + sp_repr_css_set(obj_copy, css, "style"); + sp_repr_css_attr_unref(css); + } + } + + // copy style for Paste Style action + if (!sorted_items.empty()) { + SPObject *object = sorted_items[0]; + SPItem *item = dynamic_cast<SPItem *>(object); + if (item) { + SPCSSAttr *style = take_style_from_item(item); + sp_repr_css_set(_clipnode, style, "style"); + sp_repr_css_attr_unref(style); + } + // copy path effect from the first path + if (object) { + gchar const *effect =object->getRepr()->attribute("inkscape:path-effect"); + if (effect) { + _clipnode->setAttribute("inkscape:path-effect", effect); + } + } + } + + Geom::OptRect size = selection->visualBounds(); + if (size) { + sp_repr_set_point(_clipnode, "min", size->min()); + sp_repr_set_point(_clipnode, "max", size->max()); + } + +} + + +/** + * Recursively copy all the definitions used by a given item to the clipboard defs. + */ +void ClipboardManagerImpl::_copyUsedDefs(SPItem *item) +{ + SPUse *use=dynamic_cast<SPUse *>(item); + if (use && use->get_original()) { + if(cloned_elements.insert(use->get_original()).second) + _copyUsedDefs(use->get_original()); + } + + // copy fill and stroke styles (patterns and gradients) + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) { + _copyGradient(dynamic_cast<SPGradient *>(server)); + } + SPPattern *pattern = dynamic_cast<SPPattern *>(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast<SPHatch *>(server); + if (hatch) { + _copyHatch(hatch); + } + } + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( dynamic_cast<SPLinearGradient *>(server) || dynamic_cast<SPRadialGradient *>(server) || dynamic_cast<SPMeshGradient *>(server) ) { + _copyGradient(dynamic_cast<SPGradient *>(server)); + } + SPPattern *pattern = dynamic_cast<SPPattern *>(server); + if (pattern) { + _copyPattern(pattern); + } + SPHatch *hatch = dynamic_cast<SPHatch *>(server); + if (hatch) { + _copyHatch(hatch); + } + } + + // For shapes, copy all of the shape's markers + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + for (auto & i : shape->_marker) { + if (i) { + _copyNode(i->getRepr(), _doc, _defs); + } + } + } + + // For 3D boxes, copy perspectives + { + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + _copyNode(box3d_get_perspective(box)->getRepr(), _doc, _defs); + } + } + + // Copy text elements + { + SPText *text = dynamic_cast<SPText *>(item); + if (text) { + // Copy text paths + SPTextPath *textpath = dynamic_cast<SPTextPath *>(text->firstChild()); + if (textpath) { + _copyTextPath(textpath); + } + // Copy text shape-inside + Inkscape::XML::Node* rectangle = text->get_first_rectangle(); + if (rectangle) { + _copyNode(rectangle, _doc, _defs); + } + } + if (text) { + for (auto &&shape_prop_ptr : { + reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_inside), + reinterpret_cast<SPIShapes SPStyle::*>(&SPStyle::shape_subtract) }) { + for (auto const &shape_id : (text->style->*shape_prop_ptr).shape_ids) { + auto shape_repr = text->document->getObjectById(shape_id)->getRepr(); + if (sp_repr_is_def(shape_repr)) { + _copyIgnoreDup(shape_repr, _doc, _defs); + } + } + } + } + } + + + // Copy clipping objects + if (SPObject *clip = item->getClipObject()) { + _copyNode(clip->getRepr(), _doc, _defs); + } + // Copy mask objects + if (SPObject *mask = item->getMaskObject()) { + _copyNode(mask->getRepr(), _doc, _defs); + // recurse into the mask for its gradients etc. + for(auto& o: mask->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } + } + + // Copy filters + if (style->getFilter()) { + SPObject *filter = style->getFilter(); + if (dynamic_cast<SPFilter *>(filter)) { + _copyNode(filter->getRepr(), _doc, _defs); + } + } + + // For lpe items, copy lpe stack if applicable + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + if (lpeitem->hasPathEffect()) { + PathEffectList path_effect_list( *lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (lpeobj) { + _copyNode(lpeobj->getRepr(), _doc, _defs); + } + } + } + } + + // recurse + for(auto& o: item->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&o); + if (childItem) { + _copyUsedDefs(childItem); + } + } +} + +/** + * Copy a single gradient to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyGradient(SPGradient *gradient) +{ + while (gradient) { + // climb up the refs, copying each one in the chain + _copyNode(gradient->getRepr(), _doc, _defs); + if (gradient->ref){ + gradient = gradient->ref->getObject(); + } + else { + gradient = nullptr; + } + } +} + + +/** + * Copy a single pattern to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyPattern(SPPattern *pattern) +{ + // climb up the references, copying each one in the chain + while (pattern) { + _copyNode(pattern->getRepr(), _doc, _defs); + + // items in the pattern may also use gradients and other patterns, so recurse + for (auto& child: pattern->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (pattern->ref){ + pattern = pattern->ref->getObject(); + } + else{ + pattern = nullptr; + } + } +} + +/** + * Copy a single hatch to the clipboard document's defs element. + */ +void ClipboardManagerImpl::_copyHatch(SPHatch *hatch) +{ + // climb up the references, copying each one in the chain + while (hatch) { + _copyNode(hatch->getRepr(), _doc, _defs); + + for (auto &child : hatch->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + _copyUsedDefs(childItem); + } + } + if (hatch->ref) { + hatch = hatch->ref->getObject(); + } else { + hatch = nullptr; + } + } +} + + +/** + * Copy a text path to the clipboard's defs element. + */ +void ClipboardManagerImpl::_copyTextPath(SPTextPath *tp) +{ + SPItem *path = sp_textpath_get_path_item(tp); + if (!path) { + return; + } + // textpaths that aren't in defs (on the canvas) shouldn't be copied because if + // both objects are being copied already, this ends up stealing the refs id. + if(path->parent && SP_IS_DEFS(path->parent)) { + _copyIgnoreDup(path->getRepr(), _doc, _defs); + } +} + + +/** + * Copy a single XML node from one document to another. + * @param node The node to be copied + * @param target_doc The document to which the node is to be copied + * @param parent The node in the target document which will become the parent of the copied node + * @return Pointer to the copied node + */ +Inkscape::XML::Node *ClipboardManagerImpl::_copyNode(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + +Inkscape::XML::Node *ClipboardManagerImpl::_copyIgnoreDup(Inkscape::XML::Node *node, Inkscape::XML::Document *target_doc, Inkscape::XML::Node *parent) +{ + if (sp_repr_lookup_child(_root, "id", node->attribute("id"))) { + // node already copied + return nullptr; + } + Inkscape::XML::Node *dup = node->duplicate(target_doc); + parent->appendChild(dup); + Inkscape::GC::release(dup); + return dup; +} + + +/** + * Retrieve a bitmap image from the clipboard and paste it into the active document. + */ +bool ClipboardManagerImpl::_pasteImage(SPDocument *doc) +{ + if ( doc == nullptr ) { + return false; + } + + // retrieve image data + Glib::RefPtr<Gdk::Pixbuf> img = _clipboard->wait_for_image(); +#ifdef _WIN32 + // For some reason the first call to wait_for_image() often fails, despite image data being available + // TODO: Figure out why that is and remove this hack. + if (!img) { + img = _clipboard->wait_for_image(); + } +#endif + if (!img) { + return false; + } + + Inkscape::Extension::Extension *png = Inkscape::Extension::find_by_mime("image/png"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring attr_saved = prefs->getString("/dialogs/import/link"); + bool ask_saved = prefs->getBool("/dialogs/import/ask"); + prefs->setString("/dialogs/import/link", "embed"); + prefs->setBool("/dialogs/import/ask", false); + png->set_gui(false); + + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", NULL ); + img->save(filename, "png"); + file_import(doc, filename, png); + g_free(filename); + prefs->setString("/dialogs/import/link", attr_saved); + prefs->setBool("/dialogs/import/ask", ask_saved); + png->set_gui(true); + + return true; +} + +/** + * Paste text into the selected text object or create a new one to hold it. + */ +bool ClipboardManagerImpl::_pasteText(SPDesktop *desktop) +{ + if ( desktop == nullptr ) { + return false; + } + + // if the text editing tool is active, paste the text into the active text object + if (tools_isactive(desktop, TOOLS_TEXT)) { + return Inkscape::UI::Tools::sp_text_paste_inline(desktop->event_context); + } + return false; + /* return false; + //apply the saved style to pasted text + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const clip_text = refClipboard->wait_for_text(); + Glib::ustring text(clip_text); + if(text.length() == copied_style_length) + { + Inkscape::UI::Tools::TextTool *tc = SP_TEXT_CONTEXT(desktop->event_context); + // we realy only want to inherit container style (to act as 0.92 and faster performance) + // maybe for 1.0 we can make a special type of clipboard + // that handle layout or maybe we can use the last desktop text style + // so I comment unneded code. + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + Inkscape::Text::Layout::iterator it_next; + Inkscape::Text::Layout::iterator it = tc->text_sel_end; + SPText *textitem = dynamic_cast<SPText *>(tc->text); + if (textitem) { + textitem->rebuildLayout(); + } + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text); + if (flowtext) { + flowtext->rebuildLayout(); + } + // we realy only want to inherit container style + SPCSSAttr *css = take_style_from_item(tc->text); + for (int i = 0; i < nr_blocks; ++i) + { + gchar const *w = sp_repr_css_property(css, "font-size", "0px"); + + // Don't set font-size if it wasn't set. + if (w && strcmp(w, "0px") != 0) { + sp_repr_css_set_property(te_selected_style[i], "font-size", w); + } + } + + for (unsigned int i = 0; i < text.length(); ++i) + it.prevCharacter(); + + it_next = layout->charIndexToIterator(layout->iteratorToCharIndex(it)); + + for (int i = 0; i < nr_blocks; ++i) + { + for (unsigned int j = te_selected_style_positions[i]; j < te_selected_style_positions[i+1]; ++j) + it_next.nextCharacter(); + + // sp_te_apply_style(tc->text, it, it_next, te_selected_style[i]); + te_update_layout_now_recursive(tc->text); + tc->text_sel_end = it; + for (unsigned int j = te_selected_style_positions[i]; j < te_selected_style_positions[i+1]; ++j) + it.nextCharacter(); + } + } + return true; + + } + // old(try to parse the text as a color and, if successful, apply it as the current style) + // we realy only want to inherit container style + // maybe for 1.0 we can make a special type of clipboard + // that handle layout or maybe we can use the last desktop text style + SPCSSAttr *css = sp_repr_css_attr_parse_color_to_fill(_clipboard->wait_for_text()); + if (css) { + sp_desktop_set_style(desktop, css); + return true; + } + return false; + */ +} + + +/** + * Applies a pasted path effect to a given item. + */ +void ClipboardManagerImpl::_applyPathEffect(SPItem *item, gchar const *effectstack) +{ + if ( item == nullptr ) { + return; + } + + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && effectstack) { + std::istringstream iss(effectstack); + std::string href; + while (std::getline(iss, href, ';')) + { + SPObject *obj = sp_uri_reference_resolve(_clipboardSPDoc, href.c_str()); + if (!obj) { + return; + } + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(obj); + if (lpeobj) { + lpeitem->addPathEffect(lpeobj); + } + } + // for each effect in the stack, check if we need to fork it before adding it to the item + lpeitem->forkPathEffectsIfNecessary(1); + } +} + + +/** + * Retrieve the clipboard contents as a document. + * @return Clipboard contents converted to SPDocument, or NULL if no suitable content was present + */ +SPDocument *ClipboardManagerImpl::_retrieveClipboard(Glib::ustring required_target) +{ + Glib::ustring best_target; + if ( required_target == "" ) { + best_target = _getBestTarget(); + } else { + best_target = required_target; + } + + if ( best_target == "" ) { + return nullptr; + } + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-import", NULL ); + + bool file_saved = false; + Glib::ustring target = best_target; + +#ifdef _WIN32 + if (best_target == "CF_ENHMETAFILE" || best_target == "WCF_ENHMETAFILE") + { // Try to save clipboard data as en emf file (using win32 api) + if (OpenClipboard(NULL)) { + HGLOBAL hglb = GetClipboardData(CF_ENHMETAFILE); + if (hglb) { + HENHMETAFILE hemf = CopyEnhMetaFile((HENHMETAFILE) hglb, filename); + if (hemf) { + file_saved = true; + target = "image/x-emf"; + DeleteEnhMetaFile(hemf); + } + } + CloseClipboard(); + } + } +#endif + + if (!file_saved) { + if ( !_clipboard->wait_is_target_available(best_target) ) { + return nullptr; + } + + // doing this synchronously makes better sense + // TODO: use another method because this one is badly broken imo. + // from documentation: "Returns: A SelectionData object, which will be invalid if retrieving the given target failed." + // I don't know how to check whether an object is 'valid' or not, unusable if that's not possible... + Gtk::SelectionData sel = _clipboard->wait_for_contents(best_target); + target = sel.get_target(); // this can crash if the result was invalid of last function. No way to check for this :( + + // FIXME: Temporary hack until we add memory input. + // Save the clipboard contents to some file, then read it + g_file_set_contents(filename, (const gchar *) sel.get_data(), sel.get_length(), nullptr); + } + + // there is no specific plain SVG input extension, so if we can paste the Inkscape SVG format, + // we use the image/svg+xml mimetype to look up the input extension + if (target == "image/x-inkscape-svg") { + target = "image/svg+xml"; + } + // Use the EMF extension to import metafiles + if (target == "CF_ENHMETAFILE" || target == "WCF_ENHMETAFILE") { + target = "image/x-emf"; + } + + Inkscape::Extension::DB::InputList inlist; + Inkscape::Extension::db.get_input_list(inlist); + Inkscape::Extension::DB::InputList::const_iterator in = inlist.begin(); + for (; in != inlist.end() && target != (*in)->get_mimetype() ; ++in) { + }; + if ( in == inlist.end() ) { + return nullptr; // this shouldn't happen unless _getBestTarget returns something bogus + } + + SPDocument *tempdoc = nullptr; + try { + tempdoc = (*in)->open(filename); + tempdoc->doRef(); + } catch (...) { + } + g_unlink(filename); + g_free(filename); + + return tempdoc; +} + + +/** + * Callback called when some other application requests data from Inkscape. + * + * Finds a suitable output extension to save the internal clipboard document, + * then saves it to memory and sets the clipboard contents. + */ +void ClipboardManagerImpl::_onGet(Gtk::SelectionData &sel, guint /*info*/) +{ + if (_clipboardSPDoc == nullptr) + return; + + Glib::ustring target = sel.get_target(); + if (target == "") { + return; // this shouldn't happen + } + + if (target == CLIPBOARD_TEXT_TARGET) { + target = "image/x-inkscape-svg"; + } + + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + }; + if ( out == outlist.end() && target != "image/png") { + // This happens when hitting "optpng" extensions + return; + } + + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export", NULL ); + gchar *data = nullptr; + gsize len; + + // XXX This is a crude fix for clipboards accessing extensions + // Remove when gui is extracted from extension execute and uses exceptions. + bool previous_gui = INKSCAPE.use_gui(); + INKSCAPE.use_gui(false); + + try { + if (out == outlist.end() && target == "image/png") + { + gdouble dpi = Inkscape::Util::Quantity::convert(1, "in", "px"); + guint32 bgcolor = 0x00000000; + + Geom::Point origin (_clipboardSPDoc->getRoot()->x.computed, _clipboardSPDoc->getRoot()->y.computed); + Geom::Rect area = Geom::Rect(origin, origin + _clipboardSPDoc->getDimensions()); + + unsigned long int width = (unsigned long int) (area.width() + 0.5); + unsigned long int height = (unsigned long int) (area.height() + 0.5); + + // read from namedview + Inkscape::XML::Node *nv = _clipboardSPDoc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")) { + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")) { + double opacity = 1.0; + sp_repr_get_double(nv, "inkscape:pageopacity", &opacity); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + std::vector<SPItem*> x; + sp_export_png_file(_clipboardSPDoc, filename, area, width, height, dpi, dpi, bgcolor, nullptr, nullptr, true, x); + } + else + { + if (!(*out)->loaded()) { + // Need to load the extension. + (*out)->set_state(Inkscape::Extension::Extension::STATE_LOADED); + } + + (*out)->save(_clipboardSPDoc, filename, true); + } + g_file_get_contents(filename, &data, &len, nullptr); + + sel.set(8, (guint8 const *) data, len); + } catch (...) { + } + + INKSCAPE.use_gui(previous_gui); + g_unlink(filename); // delete the temporary file + g_free(filename); + g_free(data); +} + + +/** + * Callback when someone else takes the clipboard. + * + * When the clipboard owner changes, this callback clears the internal clipboard document + * to reduce memory usage. + */ +void ClipboardManagerImpl::_onClear() +{ + // why is this called before _onGet??? + //_discardInternalClipboard(); +} + + +/** + * Creates an internal clipboard document from scratch. + */ +void ClipboardManagerImpl::_createInternalClipboard() +{ + if ( _clipboardSPDoc == nullptr ) { + _clipboardSPDoc = SPDocument::createNewDoc(nullptr, false, true); + //g_assert( _clipboardSPDoc != NULL ); + _defs = _clipboardSPDoc->getDefs()->getRepr(); + _doc = _clipboardSPDoc->getReprDoc(); + _root = _clipboardSPDoc->getReprRoot(); + + if (SP_ACTIVE_DOCUMENT) { + _clipboardSPDoc->setDocumentBase(SP_ACTIVE_DOCUMENT->getDocumentBase()); + } + + _clipnode = _doc->createElement("inkscape:clipboard"); + _root->appendChild(_clipnode); + Inkscape::GC::release(_clipnode); + + // once we create a SVG document, style will be stored in it, so flush _text_style + if (_text_style) { + sp_repr_css_attr_unref(_text_style); + _text_style = nullptr; + } + } +} + + +/** + * Deletes the internal clipboard document. + */ +void ClipboardManagerImpl::_discardInternalClipboard() +{ + if ( _clipboardSPDoc != nullptr ) { + _clipboardSPDoc->doUnref(); + _clipboardSPDoc = nullptr; + _defs = nullptr; + _doc = nullptr; + _root = nullptr; + _clipnode = nullptr; + } +} + + +/** + * Get the scale to resize an item, based on the command and desktop state. + */ +Geom::Scale ClipboardManagerImpl::_getScale(SPDesktop *desktop, Geom::Point const &min, Geom::Point const &max, Geom::Rect const &obj_rect, bool apply_x, bool apply_y) +{ + double scale_x = 1.0; + double scale_y = 1.0; + + if (apply_x) { + scale_x = (max[Geom::X] - min[Geom::X]) / obj_rect[Geom::X].extent(); + } + if (apply_y) { + scale_y = (max[Geom::Y] - min[Geom::Y]) / obj_rect[Geom::Y].extent(); + } + // If the "lock aspect ratio" button is pressed and we paste only a single coordinate, + // resize the second one by the same ratio too + if (desktop && desktop->isToolboxButtonActive("lock")) { + if (apply_x && !apply_y) { + scale_y = scale_x; + } + if (apply_y && !apply_x) { + scale_x = scale_y; + } + } + + return Geom::Scale(scale_x, scale_y); +} + + +/** + * Find the most suitable clipboard target. + */ +Glib::ustring ClipboardManagerImpl::_getBestTarget() +{ + auto targets = _clipboard->wait_for_targets(); + + // clipboard target debugging snippet + /* + g_message("Begin clipboard targets"); + for ( std::list<Glib::ustring>::iterator x = targets.begin() ; x != targets.end(); ++x ) + g_message("Clipboard target: %s", (*x).data()); + g_message("End clipboard targets\n"); + //*/ + + for (auto & _preferred_target : _preferred_targets) + { + if ( std::find(targets.begin(), targets.end(), _preferred_target) != targets.end() ) { + return _preferred_target; + } + } +#ifdef _WIN32 + if (OpenClipboard(NULL)) + { // If both bitmap and metafile are present, pick the one that was exported first. + UINT format = EnumClipboardFormats(0); + while (format) { + if (format == CF_ENHMETAFILE || format == CF_DIB || format == CF_BITMAP) { + break; + } + format = EnumClipboardFormats(format); + } + CloseClipboard(); + + if (format == CF_ENHMETAFILE) { + return "CF_ENHMETAFILE"; + } + if (format == CF_DIB || format == CF_BITMAP) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + } + + if (IsClipboardFormatAvailable(CF_ENHMETAFILE)) { + return "CF_ENHMETAFILE"; + } +#endif + if (_clipboard->wait_is_image_available()) { + return CLIPBOARD_GDK_PIXBUF_TARGET; + } + if (_clipboard->wait_is_text_available()) { + return CLIPBOARD_TEXT_TARGET; + } + + return ""; +} + + +/** + * Set the clipboard targets to reflect the mimetypes Inkscape can output. + */ +void ClipboardManagerImpl::_setClipboardTargets() +{ + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + std::vector<Gtk::TargetEntry> target_list; + + bool plaintextSet = false; + for (Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin() ; out != outlist.end() ; ++out) { + if ( !(*out)->deactivated() ) { + Glib::ustring mime = (*out)->get_mimetype(); + if (mime != CLIPBOARD_TEXT_TARGET) { + if ( !plaintextSet && (mime.find("svg") == Glib::ustring::npos) ) { + target_list.emplace_back(CLIPBOARD_TEXT_TARGET); + plaintextSet = true; + } + target_list.emplace_back(mime); + } + } + } + + // Add PNG export explicitly since there is no extension for this... + // On Windows, GTK will also present this as a CF_DIB/CF_BITMAP + target_list.emplace_back( "image/png" ); + + _clipboard->set(target_list, + sigc::mem_fun(*this, &ClipboardManagerImpl::_onGet), + sigc::mem_fun(*this, &ClipboardManagerImpl::_onClear)); + +#ifdef _WIN32 + // If the "image/x-emf" target handled by the emf extension would be + // presented as a CF_ENHMETAFILE automatically (just like an "image/bmp" + // is presented as a CF_BITMAP) this code would not be needed.. ??? + // Or maybe there is some other way to achieve the same? + + // Note: Metafile is the only format that is rendered and stored in clipboard + // on Copy, all other formats are rendered only when needed by a Paste command. + + // FIXME: This should at least be rewritten to use "delayed rendering". + // If possible make it delayed rendering by using GTK API only. + + if (OpenClipboard(NULL)) { + if ( _clipboardSPDoc != NULL ) { + const Glib::ustring target = "image/x-emf"; + + Inkscape::Extension::DB::OutputList outlist; + Inkscape::Extension::db.get_output_list(outlist); + Inkscape::Extension::DB::OutputList::const_iterator out = outlist.begin(); + for ( ; out != outlist.end() && target != (*out)->get_mimetype() ; ++out) { + } + if ( out != outlist.end() ) { + // FIXME: Temporary hack until we add support for memory output. + // Save to a temporary file, read it back and then set the clipboard contents + gchar *filename = g_build_filename( g_get_user_cache_dir(), "inkscape-clipboard-export.emf", NULL ); + + try { + (*out)->save(_clipboardSPDoc, filename); + HENHMETAFILE hemf = GetEnhMetaFileA(filename); + if (hemf) { + SetClipboardData(CF_ENHMETAFILE, hemf); + DeleteEnhMetaFile(hemf); + } + } catch (...) { + } + g_unlink(filename); // delete the temporary file + g_free(filename); + } + } + CloseClipboard(); + } +#endif +} + + +/** + * Set the string representation of a 32-bit RGBA color as the clipboard contents. + */ +void ClipboardManagerImpl::_setClipboardColor(guint32 color) +{ + gchar colorstr[16]; + g_snprintf(colorstr, 16, "%08x", color); + _clipboard->set_text(colorstr); +} + + +/** + * Put a notification on the message stack. + */ +void ClipboardManagerImpl::_userWarn(SPDesktop *desktop, char const *msg) +{ + if(desktop) + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, msg); +} + +/* ####################################### + ClipboardManager class + ####################################### */ + +ClipboardManager *ClipboardManager::_instance = nullptr; + +ClipboardManager::ClipboardManager() = default; +ClipboardManager::~ClipboardManager() = default; +ClipboardManager *ClipboardManager::get() +{ + if ( _instance == nullptr ) { + _instance = new ClipboardManagerImpl; + } + + return _instance; +} + +} // namespace Inkscape +} // namespace IO + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/clipboard.h b/src/ui/clipboard.h new file mode 100644 index 0000000..aa96a21 --- /dev/null +++ b/src/ui/clipboard.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief System-wide clipboard management - class declaration + *//* + * Authors: see git history + * Krzysztof KosiÅ„ski <tweenk@o2.pl> + * Jon A. Cruz <jon@joncruz.org> + * + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_CLIPBOARD_H +#define SEEN_INKSCAPE_CLIPBOARD_H + +#include <glibmm/ustring.h> + +// forward declarations +class SPDesktop; +namespace Inkscape { +class ObjectSet; +namespace XML { class Node; } +namespace LivePathEffect { class PathParam; } + +namespace UI { + +/** + * @brief System-wide clipboard manager + * + * ClipboardManager takes care of manipulating the system clipboard in response + * to user actions. It holds a complete SPDocument as the contents. This document + * is exported using output extensions when other applications request data. + * Copying to another instance of Inkscape is special-cased, because of the extra + * data required (i.e. style, size, Live Path Effects parameters, etc.) + */ + +class ClipboardManager { +public: + virtual void copy(ObjectSet *set) = 0; + virtual void copyPathParameter(Inkscape::LivePathEffect::PathParam *) = 0; + virtual void copySymbol(Inkscape::XML::Node* symbol, gchar const* style, bool user_symbol = true) = 0; + virtual bool paste(SPDesktop *desktop, bool in_place = false) = 0; + virtual bool pasteStyle(ObjectSet *set) = 0; + virtual bool pasteSize(ObjectSet *set, bool separately, bool apply_x, bool apply_y) = 0; + virtual bool pastePathEffect(ObjectSet *set) = 0; + virtual Glib::ustring getPathParameter(SPDesktop* desktop) = 0; + virtual Glib::ustring getShapeOrTextObjectId(SPDesktop *desktop) = 0; + virtual std::vector<Glib::ustring> getElementsOfType(SPDesktop *desktop, gchar const* type = "*", gint maxdepth = -1) = 0; + virtual const gchar *getFirstObjectID() = 0; + static ClipboardManager *get(); +protected: + ClipboardManager(); // singleton + virtual ~ClipboardManager(); +private: + ClipboardManager(const ClipboardManager &) = delete; ///< no copy + ClipboardManager &operator=(const ClipboardManager &) = delete; ///< no assign + + static ClipboardManager *_instance; +}; + +} // namespace IO +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/contextmenu.cpp b/src/ui/contextmenu.cpp new file mode 100644 index 0000000..e8851e6 --- /dev/null +++ b/src/ui/contextmenu.cpp @@ -0,0 +1,1008 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Context menu + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2010 authors + * Copyright (C) 1999-2005 authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "contextmenu.h" + +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> + +#include <gtkmm/box.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/image.h> +#include <gtkmm/separatormenuitem.h> + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" +#include "selection-chemistry.h" +#include "shortcuts.h" + +#include "helper/action-context.h" +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "live_effects/lpe-powerclip.h" +#include "live_effects/lpe-powermask.h" + +#include "object/sp-anchor.h" +#include "object/sp-clippath.h" +#include "object/sp-image.h" +#include "object/sp-mask.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/layer-properties.h" +#include "verbs.h" + +using Inkscape::DocumentUndo; + +static bool temporarily_block_actions = false; + +ContextMenu::ContextMenu(SPDesktop *desktop, SPItem *item) : + _item(item), + MIGroup(), + MIParent(_("Go to parent")) +{ + _object = static_cast<SPObject *>(item); + _desktop = desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_icons = prefs->getInt("/theme/menuIcons_canvas", true); + + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_UNDO), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_REDO), show_icons); + AddSeparator(); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_CUT), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_COPY), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_PASTE), show_icons); + AddSeparator(); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_DUPLICATE), show_icons); + AppendItemFromVerb(Inkscape::Verb::get(SP_VERB_EDIT_DELETE), show_icons); + + positionOfLastDialog = 10; // 9 in front + 1 for the separator in the next if; used to position the dialog menu entries below each other + /* Item menu */ + if (item!=nullptr) { + AddSeparator(); + MakeObjectMenu(); + } + AddSeparator(); + /* Lock/Unock Hide/Unhide*/ + auto point_doc = _desktop->point() * _desktop->dt2doc(); + Geom::Rect b(point_doc, point_doc + Geom::Point(1, 1)); + std::vector< SPItem * > down_items = _desktop->getDocument()->getItemsPartiallyInBox( _desktop->dkey, b, true, true, true, true); + bool has_down_hidden = false; + bool has_down_locked = false; + for(auto & down_item : down_items){ + if(down_item->isHidden()) { + has_down_hidden = true; + } + if(down_item->isLocked()) { + has_down_locked = true; + } + } + Gtk::MenuItem* mi; + + mi = Gtk::manage(new Gtk::MenuItem(_("Hide Selected Objects"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::HideSelected)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Unhide Objects Below"),true)); + mi->signal_activate().connect(sigc::bind<std::vector< SPItem * > >(sigc::mem_fun(*this, &ContextMenu::UnHideBelow), down_items)); + if (!has_down_hidden) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Lock Selected Objects"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::LockSelected)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + mi = Gtk::manage(new Gtk::MenuItem(_("Unlock Objects Below"),true)); + mi->signal_activate().connect(sigc::bind<std::vector< SPItem * > >(sigc::mem_fun(*this, &ContextMenu::UnLockBelow), down_items)); + if (!has_down_locked) { + mi->set_sensitive(false); + } + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + /* layer menu */ + SPGroup *group=nullptr; + if (item) { + if (SP_IS_GROUP(item)) { + group = SP_GROUP(item); + } else if ( item != _desktop->currentRoot() && SP_IS_GROUP(item->parent) ) { + group = SP_GROUP(item->parent); + } + } + + if (( group && group != _desktop->currentLayer() ) || + ( _desktop->currentLayer() != _desktop->currentRoot() && _desktop->currentLayer()->parent != _desktop->currentRoot() ) ) { + AddSeparator(); + } + + if ( group && group != _desktop->currentLayer() ) { + /* TRANSLATORS: #%1 is the id of the group e.g. <g id="#g7">, not a number. */ + MIGroup.set_label (Glib::ustring::compose(_("Enter group #%1"), group->getId())); + MIGroup.set_data("group", group); + MIGroup.signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ContextMenu::EnterGroup),&MIGroup)); + MIGroup.show(); + append(MIGroup); + } + + if ( _desktop->currentLayer() != _desktop->currentRoot() ) { + if ( _desktop->currentLayer()->parent != _desktop->currentRoot() ) { + MIParent.signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::LeaveGroup)); + MIParent.show(); + append(MIParent); + + /* Pop selection out of group */ + Gtk::MenuItem* miu = Gtk::manage(new Gtk::MenuItem(_("_Pop selection out of group"), true)); + miu->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateUngroupPopSelection)); + miu->show(); + append(*miu); + } + } + + signal_map().connect(sigc::mem_fun(*this, &ContextMenu::ShiftIcons)); +} + +ContextMenu::~ContextMenu(void) += default; + +Gtk::SeparatorMenuItem* ContextMenu::AddSeparator() +{ + Gtk::SeparatorMenuItem* sep = Gtk::manage(new Gtk::SeparatorMenuItem()); + sep->show(); + append(*sep); + return sep; +} + +void ContextMenu::EnterGroup(Gtk::MenuItem* mi) +{ + _desktop->setCurrentLayer(reinterpret_cast<SPObject *>(mi->get_data("group"))); + _desktop->selection->clear(); +} + +void ContextMenu::LeaveGroup() +{ + _desktop->setCurrentLayer(_desktop->currentLayer()->parent); +} + +void ContextMenu::LockSelected() +{ + auto itemlist = _desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i) { + (*i)->setLocked(true); + } +} + +void ContextMenu::HideSelected() +{ + auto itemlist =_desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i) { + (*i)->setHidden(true); + } +} + +void ContextMenu::UnLockBelow(std::vector<SPItem *> items) +{ + _desktop->selection->clear(); + for(auto & item : items) { + if (item->isLocked()) { + item->setLocked(false); + _desktop->selection->add(item); + } + } +} + +void ContextMenu::UnHideBelow(std::vector<SPItem *> items) +{ + _desktop->selection->clear(); + for(auto & item : items) { + if (item->isHidden()) { + item->setHidden(false); + _desktop->selection->add(item); + } + } +} + +/* + * Some day when the right-click menus are ready to start working + * smarter with the verbs, we'll need to change this NULL being + * sent to sp_action_perform to something useful, or set some kind + * of global "right-clicked position" variable for actions to + * investigate when they're called. + */ +static void +context_menu_item_on_my_activate(void */*object*/, SPAction *action) +{ + if (!temporarily_block_actions) { + sp_action_perform(action, nullptr); + } +} + +static void +context_menu_item_on_my_select(void */*object*/, SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, action->tip); +} + +static void +context_menu_item_on_my_deselect(void */*object*/, SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->clear(); +} + + +// TODO: Update this to allow radio items to be used +void ContextMenu::AppendItemFromVerb(Inkscape::Verb *verb, bool show_icon) +{ + SPAction *action; + SPDesktop *view = _desktop; + + if (verb->get_code() == SP_VERB_NONE) { + Gtk::MenuItem *item = AddSeparator(); + item->show(); + append(*item); + } else { + action = verb->get_action(Inkscape::ActionContext(view)); + if (!action) { + return; + } + // Create the menu item itself + auto const item = Gtk::manage(new Gtk::MenuItem()); + + // Now create the label and add it to the menu item (with mnemonic) + auto const label = Gtk::manage(new Gtk::AccelLabel(action->name, true)); + label->set_xalign(0.0); + sp_shortcut_add_accelerator(GTK_WIDGET(item->gobj()), sp_shortcut_get_primary(verb)); + label->set_accel_widget(*item); + + // If there is an image associated with the action, then we can add it as an icon for the menu item + if (show_icon && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + auto const icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // create a box to hold icon and label as GtkMenuItem derives from GtkBin and can only hold one child + auto const box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + + item->add(*box); + } else { + item->add(*label); + } + + action->signal_set_sensitive.connect(sigc::mem_fun(*this, &ContextMenu::set_sensitive)); + action->signal_set_name.connect(sigc::mem_fun(*item, &ContextMenu::set_name)); + + if (!action->sensitive) { + item->set_sensitive(FALSE); + } + + item->set_events(Gdk::KEY_PRESS_MASK); + item->signal_activate().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_activate),item,action)); + item->signal_select().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_select),item,action)); + item->signal_deselect().connect(sigc::bind(sigc::ptr_fun(context_menu_item_on_my_deselect),item,action)); + item->show_all(); + + append(*item); + } +} + +void ContextMenu::MakeObjectMenu() +{ + if (SP_IS_ITEM(_object)) { + MakeItemMenu(); + } + + if (SP_IS_GROUP(_object)) { + MakeGroupMenu(); + } + + if (SP_IS_ANCHOR(_object)) { + MakeAnchorMenu(); + } + + if (SP_IS_IMAGE(_object)) { + MakeImageMenu(); + } + + if (SP_IS_SHAPE(_object)) { + MakeShapeMenu(); + } + + if (SP_IS_TEXT(_object)) { + MakeTextMenu(); + } +} + +void ContextMenu::MakeItemMenu () +{ + Gtk::MenuItem* mi; + + /* Item dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Object Properties..."),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemProperties)); + mi->show(); + append(*mi);//insert(*mi,positionOfLastDialog++); + + AddSeparator(); + + /* Select item */ + if (Inkscape::Verb::getbyid( "org.inkscape.follow_link" )) { + mi = Gtk::manage(new Gtk::MenuItem(_("_Select This"), true)); + if (_desktop->selection->includes(_item)) { + mi->set_sensitive(FALSE); + } else { + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemSelectThis)); + } + mi->show(); + append(*mi); + } + + + mi = Gtk::manage(new Gtk::MenuItem(_("Select Same"))); + mi->show(); + Gtk::Menu *select_same_submenu = Gtk::manage(new Gtk::Menu()); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } + mi->set_submenu(*select_same_submenu); + append(*mi); + + /* Select same fill and stroke */ + mi = Gtk::manage(new Gtk::MenuItem(_("Fill and Stroke"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameFillStroke)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same fill color */ + mi = Gtk::manage(new Gtk::MenuItem(_("Fill Color"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameFillColor)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke color */ + mi = Gtk::manage(new Gtk::MenuItem(_("Stroke Color"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameStrokeColor)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke style */ + mi = Gtk::manage(new Gtk::MenuItem(_("Stroke Style"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameStrokeStyle)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Select same stroke style */ + mi = Gtk::manage(new Gtk::MenuItem(_("Object Type"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SelectSameObjectType)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + select_same_submenu->append(*mi); + + /* Move to layer */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Move to Layer..."), true)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } else { + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemMoveTo)); + } + mi->show(); + append(*mi); + + /* Create link */ + mi = Gtk::manage(new Gtk::MenuItem(_("Create _Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ItemCreateLink)); + mi->set_sensitive(!SP_IS_ANCHOR(_item)); + mi->show(); + append(*mi); + + bool ClipRefOK=false; + bool MaskRefOK=false; + if (_item && _item->getClipObject()) { + ClipRefOK = true; + } + if (_item && _item->getMaskObject()) { + MaskRefOK = true; + } + /* Set mask */ + mi = Gtk::manage(new Gtk::MenuItem(_("Set Mask"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SetMask)); + if (ClipRefOK || MaskRefOK) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); + + /* Release mask */ + mi = Gtk::manage(new Gtk::MenuItem(_("Release Mask"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ReleaseMask)); + if (MaskRefOK) { + mi->set_sensitive(TRUE); + } else { + mi->set_sensitive(FALSE); + } + mi->show(); + append(*mi); + + /*SSet Clip Group */ + mi = Gtk::manage(new Gtk::MenuItem(_("Create Clip G_roup"),true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::CreateGroupClip)); + mi->set_sensitive(TRUE); + mi->show(); + append(*mi); + + /* Set Clip */ + mi = Gtk::manage(new Gtk::MenuItem(_("Set Cl_ip"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SetClip)); + if (ClipRefOK || MaskRefOK) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); + + /* Release Clip */ + mi = Gtk::manage(new Gtk::MenuItem(_("Release C_lip"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ReleaseClip)); + if (ClipRefOK) { + mi->set_sensitive(TRUE); + } else { + mi->set_sensitive(FALSE); + } + mi->show(); + append(*mi); + + /* Group */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Group"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateGroup)); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } else { + mi->set_sensitive(TRUE); + } + mi->show(); + append(*mi); +} + +void ContextMenu::SelectSameFillStroke() +{ + sp_select_same_fill_stroke_style(_desktop, true, true, true); +} + +void ContextMenu::SelectSameFillColor() +{ + sp_select_same_fill_stroke_style(_desktop, true, false, false); +} + +void ContextMenu::SelectSameStrokeColor() +{ + sp_select_same_fill_stroke_style(_desktop, false, true, false); +} + +void ContextMenu::SelectSameStrokeStyle() +{ + sp_select_same_fill_stroke_style(_desktop, false, false, true); +} + +void ContextMenu::SelectSameObjectType() +{ + sp_select_same_object_type(_desktop); +} + +void ContextMenu::ItemProperties() +{ + _desktop->selection->set(_item); + _desktop->_dlg_mgr->showDialog("ObjectProperties"); +} + +void ContextMenu::ItemSelectThis() +{ + _desktop->selection->set(_item); +} + +void ContextMenu::ItemMoveTo() +{ + Inkscape::UI::Dialogs::LayerPropertiesDialog::showMove(_desktop, _desktop->currentLayer()); +} + + + +void ContextMenu::ItemCreateLink() +{ + Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:a"); + _item->parent->getRepr()->addChild(repr, _item->getRepr()); + SPObject *object = _item->document->getObjectByRepr(repr); + g_return_if_fail(SP_IS_ANCHOR(object)); + + const char *id = _item->getRepr()->attribute("id"); + Inkscape::XML::Node *child = _item->getRepr()->duplicate(xml_doc); + _item->deleteObject(false); + repr->addChild(child, nullptr); + child->setAttribute("id", id); + + Inkscape::GC::release(repr); + Inkscape::GC::release(child); + + Inkscape::DocumentUndo::done(object->document, SP_VERB_NONE, _("Create link")); + + _desktop->selection->set(SP_ITEM(object)); + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +void ContextMenu::SetMask() +{ + _desktop->selection->setMask(false, false); +} + +void ContextMenu::ReleaseMask() +{ + Inkscape::LivePathEffect::sp_remove_powermask(_desktop->selection); + _desktop->selection->unsetMask(false); +} + +void ContextMenu::CreateGroupClip() +{ + _desktop->selection->setClipGroup(); +} + +void ContextMenu::SetClip() +{ + _desktop->selection->setMask(true, false); +} + + +void ContextMenu::ReleaseClip() +{ + Inkscape::LivePathEffect::sp_remove_powerclip(_desktop->selection); + _desktop->selection->unsetMask(true); +} + +void ContextMenu::MakeGroupMenu() +{ + /* Ungroup */ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Ungroup"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ActivateUngroup)); + mi->show(); + append(*mi); +} + +void ContextMenu::ActivateGroup() +{ + _desktop->selection->group(); +} + +void ContextMenu::ActivateUngroup() +{ + std::vector<SPItem*> children; + + sp_item_group_ungroup(static_cast<SPGroup*>(_item), children); + _desktop->selection->setList(children); +} + +void ContextMenu::ActivateUngroupPopSelection() +{ + _desktop->selection->popFromGroup(); +} + + +void ContextMenu::MakeAnchorMenu() +{ + Gtk::MenuItem* mi; + + /* Link dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("Link _Properties..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkProperties)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Select item */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Follow Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkFollow)); + mi->show(); + append(*mi); + + /* Reset transformations */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Remove Link"), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::AnchorLinkRemove)); + mi->show(); + append(*mi); +} + +void ContextMenu::AnchorLinkProperties() +{ + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +void ContextMenu::AnchorLinkFollow() +{ + + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + // Opening the selected links with a python extension + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.follow_link" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::AnchorLinkRemove() +{ + std::vector<SPItem*> children; + sp_item_group_ungroup(static_cast<SPAnchor*>(_item), children, false); + Inkscape::DocumentUndo::done(_desktop->doc(), SP_VERB_NONE, _("Remove link")); +} + +void ContextMenu::MakeImageMenu () +{ + Gtk::MenuItem* mi; + Inkscape::XML::Node *ir = _object->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + + /* Image properties */ + mi = Gtk::manage(new Gtk::MenuItem(_("Image _Properties..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageProperties)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Edit externally */ + mi = Gtk::manage(new Gtk::MenuItem(_("Edit Externally..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageEdit)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) { + mi->set_sensitive( FALSE ); + } + + /* Trace Bitmap */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Trace Bitmap..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageTraceBitmap)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if (_desktop->selection->isEmpty()) { + mi->set_sensitive(FALSE); + } + + /* Embed image */ + if (Inkscape::Verb::getbyid( "org.inkscape.filter.selected.embed_image" )) { + mi = Gtk::manage(new Gtk::MenuItem(C_("Context menu", "Embed Image"))); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageEmbed)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) { + mi->set_sensitive( FALSE ); + } + } + + /* Extract image */ + if (Inkscape::Verb::getbyid( "org.inkscape.filter.extract_image" )) { + mi = Gtk::manage(new Gtk::MenuItem(C_("Context menu", "Extract Image..."))); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::ImageExtract)); + mi->show(); + insert(*mi,positionOfLastDialog++); + if ( (!href) || ((strncmp(href, "data:", 5) != 0)) ) { + mi->set_sensitive( FALSE ); + } + } +} + +void ContextMenu::ImageProperties() +{ + _desktop->_dlg_mgr->showDialog("ObjectAttributes"); +} + +Glib::ustring ContextMenu::getImageEditorName(bool is_svg) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value; + if (!is_svg) { + Glib::ustring choices = prefs->getString("/options/bitmapeditor/value"); + if (!choices.empty()) { + value = choices; + } + else { + value = "gimp"; + } + } else { + Glib::ustring choices = prefs->getString("/options/svgeditor/value"); + if (!choices.empty()) { + value = choices; + } + else { + value = "inkscape"; + } + } + return value; +} + +void ContextMenu::ImageEdit() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + GError* errThing = nullptr; + Glib::ustring bmpeditor = getImageEditorName(); + Glib::ustring cmdline = bmpeditor; + Glib::ustring name; + Glib::ustring fullname; + +#ifdef _WIN32 + // g_spawn_command_line_sync parsing is done according to Unix shell rules, + // not Windows command interpreter rules. Thus we need to enclose the + // executable path with single quotes. + int index = cmdline.find(".exe"); + if ( index < 0 ) index = cmdline.find(".bat"); + if ( index < 0 ) index = cmdline.find(".com"); + if ( index >= 0 ) { + Glib::ustring editorBin = cmdline.substr(0, index + 4).c_str(); + Glib::ustring args = cmdline.substr(index + 4, cmdline.length()).c_str(); + editorBin.insert(0, "'"); + editorBin.append("'"); + cmdline = editorBin; + cmdline.append(args); + } else { + // Enclose the whole command line if no executable path can be extracted. + cmdline.insert(0, "'"); + cmdline.append("'"); + } +#endif + + auto itemlist= _desktop->selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + Inkscape::XML::Node *ir = (*i)->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + + if (strncmp (href,"file:",5) == 0) { + // URI to filename conversion + name = g_filename_from_uri(href, nullptr, nullptr); + } else { + name.append(href); + } + + if (Glib::path_is_absolute(name)) { + fullname = name; + } else if (SP_ACTIVE_DOCUMENT->getDocumentBase()) { + fullname = Glib::build_filename(SP_ACTIVE_DOCUMENT->getDocumentBase(), name); + } else { + fullname = Glib::build_filename(Glib::get_current_dir(), name); + } + if (name.substr(name.find_last_of(".") + 1) == "SVG" || + name.substr(name.find_last_of(".") + 1) == "svg" ) + { + cmdline.erase(0, bmpeditor.length()); + Glib::ustring svgeditor = getImageEditorName(true); + cmdline = svgeditor.append(cmdline); + } + cmdline.append(" '"); + cmdline.append(fullname.c_str()); + cmdline.append("'"); + } + + //g_warning("##Command line: %s\n", cmdline.c_str()); + + g_spawn_command_line_async(cmdline.c_str(), &errThing); + + if ( errThing ) { + g_warning("Problem launching editor (%d). %s", errThing->code, errThing->message); + (_desktop->messageStack())->flash(Inkscape::ERROR_MESSAGE, errThing->message); + g_error_free(errThing); + errThing = nullptr; + } +} + +void ContextMenu::ImageTraceBitmap() +{ + INKSCAPE.dialogs_unhide(); + _desktop->_dlg_mgr->showDialog("Trace"); +} + +void ContextMenu::ImageEmbed() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.filter.selected.embed_image" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::ImageExtract() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + Inkscape::Verb *verb = Inkscape::Verb::getbyid( "org.inkscape.filter.extract_image" ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void ContextMenu::MakeShapeMenu () +{ + Gtk::MenuItem* mi; + + /* Item dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Fill and Stroke..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::FillSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); +} + +void ContextMenu::FillSettings() +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("FillAndStroke"); +} + +void ContextMenu::MakeTextMenu () +{ + Gtk::MenuItem* mi; + + /* Fill and Stroke dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Fill and Stroke..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::FillSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); + + /* Edit Text dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("_Text and Font..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::TextSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); + +#if HAVE_ASPELL + /* Spellcheck dialog */ + mi = Gtk::manage(new Gtk::MenuItem(_("Check Spellin_g..."), true)); + mi->signal_activate().connect(sigc::mem_fun(*this, &ContextMenu::SpellcheckSettings)); + mi->show(); + insert(*mi,positionOfLastDialog++); +#endif +} + +void ContextMenu::TextSettings () +{ + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("TextFont"); +} + +void ContextMenu::SpellcheckSettings () +{ +#if HAVE_ASPELL + if (_desktop->selection->isEmpty()) { + _desktop->selection->set(_item); + } + + _desktop->_dlg_mgr->showDialog("SpellCheck"); +#endif +} + +void ContextMenu::ShiftIcons() +{ + static auto provider = Gtk::CssProvider::create(); + static bool provider_added = false; + + Gtk::MenuItem *menuitem = nullptr; + Gtk::Box *content = nullptr; + Gtk::Image *icon = nullptr; + + static int current_shift = 0; + int calculated_shift = 0; + + // install CssProvider for our custom styles + if (!provider_added) { + auto const screen = Gdk::Screen::get_default(); + Gtk::StyleContext::add_provider_for_screen(screen, provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + provider_added = true; + } + + // get the first MenuItem with an image (i.e. "ImageMenuItem" as named below) + std::vector<Gtk::Widget *> children = get_children(); + for (auto child: children) { + if (child->get_name() == "ImageMenuItem") { + menuitem = static_cast<Gtk::MenuItem *>(child); + content = static_cast<Gtk::Box *>(menuitem->get_child()); + icon = static_cast<Gtk::Image *>(content->get_children()[0]); + break; + } + } + + // calculate how far we have to shift the icon to fit it into the empty space between menuitem and its content + if (icon) { + auto allocation_menuitem = menuitem->get_allocation(); + auto allocation_icon = icon->get_allocation(); + + if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) { + calculated_shift = allocation_menuitem.get_width() - allocation_icon.get_x() - allocation_icon.get_width(); + } else { + calculated_shift = -allocation_icon.get_x(); + } + } + + // install CSS to shift icon, use a threshold to avoid overly frequent updates + // (gtk's own calculations for the reserved space are off by a few pixels if there is no check/radio item in a menu) + if (calculated_shift && std::abs(current_shift - calculated_shift) > 2) { + current_shift = calculated_shift; + + std::string css_str; + if (menuitem->get_direction() == Gtk::TEXT_DIR_RTL) { + css_str = "#ImageMenuItem image {margin-right:" + std::to_string(-calculated_shift) + "px;}"; + } else { + css_str = "#ImageMenuItem image {margin-left:" + std::to_string(calculated_shift) + "px;}"; + } + provider->load_from_data(css_str); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/contextmenu.h b/src/ui/contextmenu.h new file mode 100644 index 0000000..0e1bcff --- /dev/null +++ b/src/ui/contextmenu.h @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONTEXTMENU_H +#define SEEN_CONTEXTMENU_H + +/* + * Context menu + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/menu.h> + +class SPDesktop; +class SPItem; +class SPObject; + +namespace Gtk { +class SeparatorMenuItem; +} + +namespace Inkscape { +class Verb; +} + +/** + * Implements the Inkscape context menu. + * + * For the context menu implementation, the ContextMenu class stores the object + * that was selected in a private data member. This should be fairly safe to do + * and a pointer to the SPItem as well as SPObject class are kept. + * All callbacks of the context menu entries are implemented as private + * functions. + * + * @todo add callbacks to destroy the context menu when it is closed (=key or mouse button pressed out of the scope of the context menu) + */ +class ContextMenu : public Gtk::Menu +{ + public: + /** + * The ContextMenu constructor contains all code to create and show the + * menu entries (aka child widgets). + * + * @param desktop pointer to the desktop the user is currently working on. + * @param item SPItem pointer to the object selected at the time the ContextMenu is created. + */ + ContextMenu(SPDesktop *desktop, SPItem *item); + ~ContextMenu() override; + + /** + * install CSS to shift menu icons into the space reserved for toggles (i.e. check and radio items) + * + * TODO: This should be private but we already re-use this code in ui/interface.cpp which is not c++ified yet. + * In future ContextMenu and the (to be created) class for the menu bar should then be derived from one common base class. + */ + void ShiftIcons(); + private: + SPItem *_item; // pointer to the object selected at the time the ContextMenu is created + SPObject *_object; // pointer to the object selected at the time the ContextMenu is created + SPDesktop *_desktop; //pointer to the desktop the user was currently working on at the time the ContextMenu is created + + int positionOfLastDialog; + + Gtk::MenuItem MIGroup; //menu entry to enter a group + Gtk::MenuItem MIParent; //menu entry to leave a group + + /** + * auxiliary function that adds a separator line in the context menu + */ + Gtk::SeparatorMenuItem* AddSeparator(); + + /** + * Appends a custom menu UI from a verb. + * + * c++ified version of sp_ui_menu_append_item. + * @see sp_ui_menu_append_item_from_verb and synchronize/drop that function when c++ifying other code in interface.cpp + * + * @param show_icon True if an icon should be displayed before the menu item's label + * + */ + void AppendItemFromVerb(Inkscape::Verb *verb, bool show_icon = false); + + /** + * main function which is responsible for creating the context sensitive menu items, + * calls subfunctions below to create the menu entry widgets. + */ + void MakeObjectMenu (); + /** + * creates menu entries for an SP_TYPE_ITEM object + */ + void MakeItemMenu (); + /** + * creates menu entries for a grouped object + */ + void MakeGroupMenu (); + /** + * creates menu entries for an anchor object + */ + void MakeAnchorMenu (); + /** + * creates menu entries for a bitmap image object + */ + void MakeImageMenu (); + /** + * creates menu entries for a shape object + */ + void MakeShapeMenu (); + /** + * creates menu entries for a text object + */ + void MakeTextMenu (); + + void EnterGroup(Gtk::MenuItem* mi); + void LeaveGroup(); + void LockSelected(); + void HideSelected(); + void UnLockBelow(std::vector<SPItem *> items); + void UnHideBelow(std::vector<SPItem *> items); + ////////////////////////////////////////// + //callbacks for the context menu entries of an SP_TYPE_ITEM object + void ItemProperties(); + void ItemSelectThis(); + void ItemMoveTo(); + void SelectSameFillStroke(); + void SelectSameFillColor(); + void SelectSameStrokeColor(); + void SelectSameStrokeStyle(); + void SelectSameObjectType(); + void ItemCreateLink(); + void CreateGroupClip(); + void SetMask(); + void ReleaseMask(); + void SetClip(); + void ReleaseClip(); + ////////////////////////////////////////// + + + /** + * callback, is executed on clicking the anchor "Group" and "Ungroup" menu entry + */ + void ActivateUngroupPopSelection(); + void ActivateUngroup(); + void ActivateGroup(); + + void AnchorLinkProperties(); + /** + * placeholder for callback to be executed on clicking the anchor "Follow link" context menu entry + * @todo add code to follow link externally + */ + void AnchorLinkFollow(); + + /** + * callback, is executed on clicking the anchor "Link remove" menu entry + */ + void AnchorLinkRemove(); + + + /** + * callback, opens the image properties dialog and is executed on clicking the context menu entry with similar name + */ + void ImageProperties(); + + /** + * callback, is executed on clicking the image "Edit Externally" menu entry + */ + void ImageEdit(); + + /** + * auxiliary function that loads the external image editor name from the settings. + */ + Glib::ustring getImageEditorName(bool is_svg = false); + + /** + * callback, is executed on clicking the "Embed Image" menu entry + */ + void ImageEmbed(); + + /** + * callback, is executed on clicking the "Trace Bitmap" menu entry + */ + void ImageTraceBitmap(); + + /** + * callback, is executed on clicking the "Extract Image" menu entry + */ + void ImageExtract(); + + + /** + * callback, is executed on clicking the "Fill and Stroke" menu entry + */ + void FillSettings(); + + + /** + * callback, is executed on clicking the "Text and Font" menu entry + */ + void TextSettings(); + + /** + * callback, is executed on clicking the "Check spelling" menu entry + */ + void SpellcheckSettings(); +}; +#endif // SEEN_CONTEXT_MENU_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/control-manager.cpp b/src/ui/control-manager.cpp new file mode 100644 index 0000000..0330b5b --- /dev/null +++ b/src/ui/control-manager.cpp @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Central facade for accessing and managing on-canvas controls. + * + * Author: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "control-manager.h" + +#include <algorithm> +#include <set> + +#include <glib-object.h> + +#include "display/sodipodi-ctrl.h" // for SP_TYPE_CTRL +#include "display/sp-ctrlline.h" +#include "display/sp-ctrlcurve.h" +#include "preferences.h" + +using Inkscape::ControlFlags; + +namespace { + +// Note: The following operator overloads are local to this file at the moment to discourage flag manipulation elsewhere. +/* +ControlFlags operator |(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast<ControlFlags>(static_cast<int>(lhs) | static_cast<int>(rhs)); +} +*/ + +ControlFlags operator &(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast<ControlFlags>(static_cast<int>(lhs) & static_cast<int>(rhs)); +} + +ControlFlags operator ^(ControlFlags lhs, ControlFlags rhs) +{ + return static_cast<ControlFlags>(static_cast<int>(lhs) ^ static_cast<int>(rhs)); +} + +ControlFlags& operator ^=(ControlFlags &lhs, ControlFlags rhs) +{ + lhs = lhs ^ rhs; + return lhs; +} + +} // namespace + +// Default color for line: +#define LINE_COLOR_PRIMARY 0x0000ff7f +#define LINE_COLOR_SECONDARY 0xff00007f +#define LINE_COLOR_TERTIARY 0xffff007f + +namespace Inkscape { + +class ControlManagerImpl +{ +public: + ControlManagerImpl(ControlManager &manager); + + ~ControlManagerImpl() = default; + + SPCanvasItem *createControl(SPCanvasGroup *parent, ControlType type); + + void setControlSize(int size, bool force = false); + + void track(SPCanvasItem *anchor); + + sigc::connection connectCtrlSizeChanged(const sigc::slot<void> &slot); + + void updateItem(SPCanvasItem *item); + + bool setControlType(SPCanvasItem *item, ControlType type); + + bool setControlResize(SPCanvasItem *item, int ctrlResize); + + void setSelected(SPCanvasItem *item, bool selected); + +private: + static void thingFinalized(gpointer data, GObject *wasObj); + + void thingFinalized(GObject *wasObj); + + class PrefListener : public Inkscape::Preferences::Observer + { + public: + PrefListener(ControlManagerImpl &manager) : Inkscape::Preferences::Observer("/options/grabsize/value"), _mgr(manager) {} + ~PrefListener() override = default; + + void notify(Inkscape::Preferences::Entry const &val) override { + int size = val.getIntLimited(3, 1, 7); + _mgr.setControlSize(size); + } + + ControlManagerImpl &_mgr; + }; + + + ControlManager &_manager; + sigc::signal<void> _sizeChangedSignal; + PrefListener _prefHook; + int _size; // Size from the grabsize preference + int _resize; // Way size should change from grabsize + std::vector<SPCanvasItem *> _itemList; + std::map<Inkscape::ControlType, std::vector<unsigned int> > _sizeTable; + std::map<Inkscape::ControlType, GType> _typeTable; + std::map<Inkscape::ControlType, SPCtrlShapeType> _ctrlToShape; + std::set<Inkscape::ControlType> _resizeOnSelect; +}; + +ControlManagerImpl::ControlManagerImpl(ControlManager &manager) : + _manager(manager), + _sizeChangedSignal(), + _prefHook(*this), + _size(3), + _resize(3), + _itemList(), + _sizeTable() +{ + _typeTable[CTRL_TYPE_UNKNOWN] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_LPE] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_ADJ_HANDLE] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_ANCHOR] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_INVISIPOINT] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_AUTO] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_CUSP] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_SMOOTH] = SP_TYPE_CTRL; + _typeTable[CTRL_TYPE_NODE_SYMETRICAL] = SP_TYPE_CTRL; + + _typeTable[CTRL_TYPE_LINE] = SP_TYPE_CTRLLINE; + + // ------- + _ctrlToShape[CTRL_TYPE_UNKNOWN] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_LPE] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_NODE_CUSP] = SP_CTRL_SHAPE_DIAMOND; + _ctrlToShape[CTRL_TYPE_NODE_SMOOTH] = SP_CTRL_SHAPE_SQUARE; + _ctrlToShape[CTRL_TYPE_NODE_AUTO] = SP_CTRL_SHAPE_CIRCLE; + _ctrlToShape[CTRL_TYPE_NODE_SYMETRICAL] = SP_CTRL_SHAPE_SQUARE; + + _ctrlToShape[CTRL_TYPE_ADJ_HANDLE] = SP_CTRL_SHAPE_CIRCLE; + _ctrlToShape[CTRL_TYPE_INVISIPOINT] = SP_CTRL_SHAPE_SQUARE; + + // ------- + + _resizeOnSelect.insert(CTRL_TYPE_NODE_AUTO); + _resizeOnSelect.insert(CTRL_TYPE_NODE_CUSP); + _resizeOnSelect.insert(CTRL_TYPE_NODE_SMOOTH); + _resizeOnSelect.insert(CTRL_TYPE_NODE_SYMETRICAL); + + // ------- + + // The size of the controls is determined by the grabsize preference; see the "Handle size" parameter in + // the input/output group, on the "input devices" tab; this parameter ranges from 1 to 7; When selecting a control, we + // increase the size by an additional 2 pixels, if _resizeOnSelect is true (see setSelected()) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->addObserver(_prefHook); + + _size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 7); + + // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids + // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the + // control has an even width). + { + unsigned int sizes[] = {7, 7, 7, 7, 7, 7, 7}; + _sizeTable[CTRL_TYPE_UNKNOWN] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_ADJ_HANDLE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_ANCHOR] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17}; + _sizeTable[CTRL_TYPE_POINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_ROTATE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_SIZER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_SHAPER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17}; + _sizeTable[CTRL_TYPE_NODE_AUTO] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_NODE_CUSP] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15}; + _sizeTable[CTRL_TYPE_NODE_SMOOTH] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + _sizeTable[CTRL_TYPE_NODE_SYMETRICAL] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = {1, 1, 1, 1, 1, 1, 1}; + _sizeTable[CTRL_TYPE_INVISIPOINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } + { + unsigned int sizes[] = { 5, 7, 9, 11, 13, 15, 17 }; + _sizeTable[CTRL_TYPE_LPE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0]))); + } +} + + +void ControlManagerImpl::setControlSize(int size, bool force) +{ + if ((size < 1) || (size > 7)) { + g_warning("Illegal logical size set: %d", size); + } else if (force || (size != _size)) { + _size = size; + + for (auto & it : _itemList) + { + if (it) { + updateItem(it); + } + } + + //_sizeChangedSignal.emit(); + } +} + +SPCanvasItem *ControlManagerImpl::createControl(SPCanvasGroup *parent, ControlType type) +{ + SPCanvasItem *item = nullptr; + unsigned int targetSize = _sizeTable[type][_size - 1]; + switch (type) + { + case CTRL_TYPE_ADJ_HANDLE: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape",SP_CTRL_SHAPE_CIRCLE, + "size", targetSize, + "filled", 1, + "fill_color", 0xffffff7f, + "stroked", 1, + "stroke_color", 0x0000ff7f, + NULL); + break; + case CTRL_TYPE_ANCHOR: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "size", targetSize, + "filled", 1, + "fill_color", 0xffffff7f, + "stroked", 1, + "stroke_color", 0x000000ff, + NULL); + break; + case CTRL_TYPE_NODE_AUTO: + case CTRL_TYPE_NODE_CUSP: + case CTRL_TYPE_NODE_SMOOTH: + case CTRL_TYPE_NODE_SYMETRICAL: + { + SPCtrlShapeType shape = _ctrlToShape[_ctrlToShape.count(type) ? type : CTRL_TYPE_UNKNOWN]; + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape", shape, + "size", targetSize, + NULL); + break; + } + case CTRL_TYPE_INVISIPOINT: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, + "shape", SP_CTRL_SHAPE_SQUARE, + "size", targetSize, + NULL); + break; + case CTRL_TYPE_UNKNOWN: + case CTRL_TYPE_LPE: + default: + item = sp_canvas_item_new(parent, SP_TYPE_CTRL, nullptr); + } + if (item) { + item->ctrlType = type; + } + return item; +} + +void ControlManagerImpl::track(SPCanvasItem *item) +{ + g_object_weak_ref( G_OBJECT(item), ControlManagerImpl::thingFinalized, this ); + + _itemList.push_back(item); + + setControlSize(_size, true); +} + +sigc::connection ControlManagerImpl::connectCtrlSizeChanged(const sigc::slot<void> &slot) +{ + return _sizeChangedSignal.connect(slot); +} + +void ControlManagerImpl::updateItem(SPCanvasItem *item) +{ + if (item) { + unsigned int target = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", target, NULL); + + sp_canvas_item_request_update(item); + } +} + +bool ControlManagerImpl::setControlType(SPCanvasItem *item, ControlType type) +{ + bool accepted = false; + if (item && (item->ctrlType == type)) { + // nothing to do + accepted = true; + } else if (item) { + if (_ctrlToShape.count(type) && (_typeTable[type] == _typeTable[item->ctrlType])) { // compatible? + unsigned int targetSize = _sizeTable[type][_size - 1] + item->ctrlResize; + SPCtrlShapeType targetShape = _ctrlToShape[type]; + + g_object_set(item, "shape", targetShape, "size", targetSize, NULL); + item->ctrlType = type; + accepted = true; + } + } + + return accepted; +} + +bool ControlManagerImpl::setControlResize(SPCanvasItem *item, int ctrlResize) +{ + // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids + // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the + // control has an even width). ctrlResize should therefore be an even number, such that the sum (targetSize) + // is also odd + if(item) { + item->ctrlResize = ctrlResize; + unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", targetSize, NULL); + return true; + } + return false; +} + +void ControlManagerImpl::setSelected(SPCanvasItem *item, bool selected) +{ + if (_manager.isSelected(item) != selected) { + item->ctrlFlags ^= CTRL_FLAG_SELECTED; // toggle, since we know it is different + + if (selected && _resizeOnSelect.count(item->ctrlType)) { + item->ctrlResize = 2; + } else { + item->ctrlResize = 0; + } + + // TODO refresh colors + unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize; + g_object_set(item, "size", targetSize, NULL); + } +} + +void ControlManagerImpl::thingFinalized(gpointer data, GObject *wasObj) +{ + if (data) { + reinterpret_cast<ControlManagerImpl *>(data)->thingFinalized(wasObj); + } +} + +void ControlManagerImpl::thingFinalized(GObject *wasObj) +{ + SPCanvasItem *wasItem = reinterpret_cast<SPCanvasItem *>(wasObj); + if (wasItem) + { + std::vector<SPCanvasItem *>::iterator it = std::find(_itemList.begin(), _itemList.end(), wasItem); + if (it != _itemList.end()) + { + _itemList.erase(it); + } + } +} + + +// ---------------------------------------------------- + +ControlManager::ControlManager() : + _impl(new ControlManagerImpl(*this)) +{ +} + +ControlManager::~ControlManager() += default; + +ControlManager &ControlManager::getManager() +{ + static ControlManager instance; + + return instance; +} + + +SPCanvasItem *ControlManager::createControl(SPCanvasGroup *parent, ControlType type) +{ + return _impl->createControl(parent, type); +} + +SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, CtrlLineType type) +{ + SPCtrlLine *line = SP_CTRLLINE(sp_canvas_item_new(parent, SP_TYPE_CTRLLINE, nullptr)); + if (line) { + line->ctrlType = CTRL_TYPE_LINE; + + line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY : + (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY); + line->setCoords(0, 0, 0, 0); + } + return line; +} + +SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, Geom::Point const &p1, Geom::Point const &p2, CtrlLineType type) +{ + SPCtrlLine *line = createControlLine(parent, type); + if (line) { + line->setCoords(p1, p2); + } + return line; +} + +SPCtrlCurve *ControlManager::createControlCurve(SPCanvasGroup *parent, Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3, CtrlLineType type) +{ + SPCtrlCurve *line = SP_CTRLCURVE(sp_canvas_item_new(parent, SP_TYPE_CTRLCURVE, nullptr)); + if (line) { + line->ctrlType = CTRL_TYPE_LINE; + + line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY : + (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY); + line->setCoords(p0, p1, p2, p3); + } + return line; +} + +void ControlManager::track(SPCanvasItem *item) +{ + _impl->track(item); +} + +sigc::connection ControlManager::connectCtrlSizeChanged(const sigc::slot<void> &slot) +{ + return _impl->connectCtrlSizeChanged(slot); +} + +void ControlManager::updateItem(SPCanvasItem *item) +{ + return _impl->updateItem(item); +} + +bool ControlManager::setControlType(SPCanvasItem *item, ControlType type) +{ + return _impl->setControlType(item, type); +} + +bool ControlManager::setControlResize(SPCanvasItem *item, int ctrlResize) +{ + return _impl->setControlResize(item, ctrlResize); +} + +bool ControlManager::isActive(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_ACTIVE) != 0; +} + +void ControlManager::setActive(SPCanvasItem *item, bool active) +{ + if (isActive(item) != active) { + item->ctrlFlags ^= CTRL_FLAG_ACTIVE; // toggle, since we know it is different + // TODO refresh size/colors + } +} + +bool ControlManager::isPrelight(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_PRELIGHT) != 0; +} + +void ControlManager::setPrelight(SPCanvasItem *item, bool prelight) +{ + if (isPrelight(item) != prelight) { + item->ctrlFlags ^= CTRL_FLAG_PRELIGHT; // toggle, since we know it is different + // TODO refresh size/colors + } +} + +bool ControlManager::isSelected(SPCanvasItem *item) const +{ + return (item->ctrlFlags & CTRL_FLAG_SELECTED) != 0; +} + +void ControlManager::setSelected(SPCanvasItem *item, bool selected) +{ + _impl->setSelected(item, selected); +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/control-manager.h b/src/ui/control-manager.h new file mode 100644 index 0000000..12ac19a --- /dev/null +++ b/src/ui/control-manager.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::ControlManager - Coordinates creation and styling of nodes, handles, etc. + * + * Author: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_CONTROL_MANAGER_H +#define SEEN_INKSCAPE_CONTROL_MANAGER_H + +#include <memory> +#include <sigc++/sigc++.h> + +#include "ui/control-types.h" + +struct SPCanvasGroup; +struct SPCanvasItem; +struct SPCtrlLine; +struct SPCtrlCurve; + +namespace Geom +{ + +class Point; + +} // namespace Geom + +namespace Inkscape { + +enum CtrlLineType { + CTLINE_PRIMARY, + CTLINE_SECONDARY, + CTLINE_TERTIARY, +}; + + +class ControlManagerImpl; + +class ControlManager +{ +public: + + static ControlManager &getManager(); + + ~ControlManager(); + + sigc::connection connectCtrlSizeChanged(const sigc::slot<void> &slot); + + SPCanvasItem *createControl(SPCanvasGroup *parent, ControlType type); + + SPCtrlLine *createControlLine(SPCanvasGroup *parent, CtrlLineType type = CTLINE_PRIMARY); + + SPCtrlLine *createControlLine(SPCanvasGroup *parent, Geom::Point const &p1, Geom::Point const &p2, CtrlLineType type = CTLINE_PRIMARY); + + SPCtrlCurve *createControlCurve(SPCanvasGroup *parent, Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3, CtrlLineType type = CTLINE_PRIMARY); + + void track(SPCanvasItem *item); + + void updateItem(SPCanvasItem *item); + + bool setControlType(SPCanvasItem *item, ControlType type); + + bool setControlResize(SPCanvasItem *item, int ctrlResize); + + bool isActive(SPCanvasItem *item) const; + void setActive(SPCanvasItem *item, bool active); + + bool isPrelight(SPCanvasItem *item) const; + void setPrelight(SPCanvasItem *item, bool prelight); + + bool isSelected(SPCanvasItem *item) const; + void setSelected(SPCanvasItem *item, bool selected); + +private: + ControlManager(); + std::unique_ptr<ControlManagerImpl> _impl; + friend class ControlManagerImpl; +}; + +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_CONTROL_MANAGER_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/control-types.h b/src/ui/control-types.h new file mode 100644 index 0000000..896ccf8 --- /dev/null +++ b/src/ui/control-types.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_CONTROL_TYPES_H +#define SEEN_UI_CONTROL_TYPES_H + +/* + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2012 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +namespace Inkscape +{ + +// Rough initial set. Most likely needs refinement. +enum ControlType { + CTRL_TYPE_UNKNOWN, + CTRL_TYPE_ADJ_HANDLE, + CTRL_TYPE_ANCHOR, + CTRL_TYPE_POINT, + CTRL_TYPE_ROTATE, + CTRL_TYPE_SIZER, + CTRL_TYPE_SHAPER, + CTRL_TYPE_LINE, + CTRL_TYPE_LPE, + CTRL_TYPE_NODE_AUTO, + CTRL_TYPE_NODE_CUSP, + CTRL_TYPE_NODE_SMOOTH, + CTRL_TYPE_NODE_SYMETRICAL, + CTRL_TYPE_INVISIPOINT +}; + +/** + * Flags for internal representation/tracking. + */ +enum ControlFlags { + CTRL_FLAG_NORMAL = 0, + CTRL_FLAG_ACTIVE = 1 << 0, + CTRL_FLAG_PRELIGHT = 1 << 1, + CTRL_FLAG_SELECTED = 1 << 2, +}; + +} // namespace Inkscape + +#endif // SEEN_UI_CONTROL_TYPES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/desktop/README b/src/ui/desktop/README new file mode 100644 index 0000000..e2932ca --- /dev/null +++ b/src/ui/desktop/README @@ -0,0 +1,27 @@ + + +This directory contains code related to the Inkscape desktop, that is +code that is directly used by the InkscapeWindow class and in linking +the desktop to the canvas. It should not contain basic widgets, +dialogs, toolbars, etc. + +To do: + +* widgets/desktop-widget.h/cpp should disappear with code ending up in either + InkscapeWindow.h/cpp or desktop.h/cpp (or in new files). + +* ui/view/view-widget.h/cpp should disappear ('view' should be member of window) + +* desktop.h/cpp should only contain code that links the desktop to the canvas. + +* Convert GUI to use actions where possible. + +* Future Structure: + Main menu bar (menubar.h/.cpp) + Tool bar + Multipaned widget containing + Dialogs + Tools + Canvas + Palette (maybe turn into dialog). + Status bar diff --git a/src/ui/desktop/menubar.cpp b/src/ui/desktop/menubar.cpp new file mode 100644 index 0000000..68c989a --- /dev/null +++ b/src/ui/desktop/menubar.cpp @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Desktop main menu bar code. + */ +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * Alex Valavanis <valavanisalex@gmail.com> + * Patrick Storz <eduard.braun2@gmx.de> + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <iostream> + +#include "inkscape.h" +#include "inkscape-application.h" // Open recent + +#include "message-context.h" +#include "shortcuts.h" + +#include "helper/action.h" +#include "helper/action-context.h" + +#include "object/sp-namedview.h" + +#include "ui/contextmenu.h" // Shift to make room for icons +#include "ui/icon-loader.h" +#include "ui/view/view.h" +#include "ui/uxmanager.h" // To Do: Convert to actions + +#ifdef GDK_WINDOWING_QUARTZ +#include <gtkosxapplication.h> +#endif + +// ================= Common ==================== + +std::vector<std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *>> menuitems; +unsigned int lastverb = -1; + + +/** + * Get menu item (if it was registered in `menuitems`) + */ +Gtk::MenuItem *get_menu_item_for_verb(unsigned int verb, Inkscape::UI::View::View *view) +{ + for (auto &item : menuitems) { + if (item.first.first == verb && item.second == view) { + return item.first.second; + } + } + return nullptr; +} + +#ifdef GDK_WINDOWING_QUARTZ +/** + * Update menubar for macOS + * + * Can be used as a glib timeout callback. + */ +static gboolean sync_menubar(gpointer = nullptr) +{ + auto osxapp = gtkosx_application_get(); + if (osxapp) { + gtkosx_application_sync_menubar(osxapp); + } + return false; +} +#endif + +// Sets tip +static void +select_action(SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, action->tip); +} + +// Clears tip +static void +deselect_action(SPAction *action) +{ + sp_action_get_view(action)->tipsMessageContext()->clear(); +} + +// Trigger action +static void item_activate(Gtk::MenuItem *menuitem, SPAction *action) +{ + if (action->verb->get_code() == lastverb) { + lastverb = -1; + return; + } + lastverb = action->verb->get_code(); + sp_action_perform(action, nullptr); + lastverb = -1; + +#ifdef GDK_WINDOWING_QUARTZ + sync_menubar(); +#endif +} + +static void set_menuitems(unsigned int emitting_verb, bool value) +{ + for (auto menu : menuitems) { + if (menu.second == SP_ACTIVE_DESKTOP) { + if (emitting_verb == menu.first.first) { + if (emitting_verb == lastverb) { + lastverb = -1; + return; + } + lastverb = emitting_verb; + Gtk::CheckMenuItem *check = dynamic_cast<Gtk::CheckMenuItem *>(menu.first.second); + Gtk::RadioMenuItem *radio = dynamic_cast<Gtk::RadioMenuItem *>(menu.first.second); + if (radio) { + radio->property_active() = value; + } else if (check) { + check->property_active() = value; + } + lastverb = -1; + } + } + } +} + +// Change label name (used in the Undo/Redo menu items). +static void +set_name(Glib::ustring const &name, Gtk::MenuItem* menuitem) +{ + if (menuitem) { + Gtk::Widget* widget = menuitem->get_child(); + + // Label is either child of menuitem + Gtk::Label* label = dynamic_cast<Gtk::Label*>(widget); + + // Or wrapped inside a box which is a child of menuitem (if with icon). + if (!label) { + Gtk::Box* box = dynamic_cast<Gtk::Box*>(widget); + if (box) { + std::vector<Gtk::Widget*> children = box->get_children(); + for (auto child: children) { + label = dynamic_cast<Gtk::Label*>(child); + if (label) break; + } + } + } + + if (label) { + label->set_markup_with_mnemonic(name); + } else { + std::cerr << "set_name: could not find label!" << std::endl; + } + } +} + +/* Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + * + * TODO: This code already exists as a C++ version in the class ContextMenu so we can simply wrap + * it here. In future ContextMenu and the (to be created) class for the menu bar should then + * be derived from one common base class. + * + * TODO: This code is called everytime a menu is opened. We can certainly find a way to call it once. + */ +static void +shift_icons(Gtk::Menu* menu) +{ + ContextMenu *contextmenu = static_cast<ContextMenu*>(menu); + contextmenu->ShiftIcons(); +} + +// ================= MenuItem ==================== + +Gtk::MenuItem* +build_menu_item_from_verb(SPAction* action, + bool show_icon, + bool radio = false, + Gtk::RadioMenuItem::Group *group = nullptr) +{ + Gtk::MenuItem* menuitem = nullptr; + + if (radio) { + menuitem = Gtk::manage(new Gtk::RadioMenuItem(*group)); + } else { + menuitem = Gtk::manage(new Gtk::MenuItem()); + } + + Gtk::AccelLabel* label = Gtk::manage(new Gtk::AccelLabel(action->name, true)); + label->set_xalign(0.0); + label->set_accel_widget(*menuitem); + sp_shortcut_add_accelerator((GtkWidget*)menuitem->gobj(), sp_shortcut_get_primary(action->verb)); + + // If there is an image associated with the action, we can add it as an icon for the menu item. + if (show_icon && action->image) { + menuitem->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image* image = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can + // only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + box->pack_start(*image, false, false, 0); + box->pack_start(*label, true, true, 0); + menuitem->add(*box); + } else { + menuitem->add(*label); + } + menuitem->signal_activate().connect( + sigc::bind<Gtk::MenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action)); + menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action)); + menuitem->signal_deselect().connect(sigc::bind<SPAction*>(sigc::ptr_fun(&deselect_action), action)); + + action->signal_set_sensitive.connect( + sigc::bind<0>(sigc::ptr_fun(>k_widget_set_sensitive), (GtkWidget*)menuitem->gobj())); + action->signal_set_name.connect( + sigc::bind<Gtk::MenuItem*>(sigc::ptr_fun(&set_name), menuitem)); + + // initialize sensitivity with verb default + sp_action_set_sensitive(action, action->verb->get_default_sensitive()); + + return menuitem; +} + +// =============== CheckMenuItem ================== + +bool getStateFromPref(SPDesktop *dt, Glib::ustring item) +{ + Glib::ustring pref_path; + + if (dt->is_focusMode()) { + pref_path = "/focus/"; + } else if (dt->is_fullscreen()) { + pref_path = "/fullscreen/"; + } else { + pref_path = "/window/"; + } + + pref_path += item; + pref_path += "/state"; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return prefs->getBool(pref_path, false); +} + +// I wonder if this can be done without hard coding names. +static void +checkitem_update(Gtk::CheckMenuItem* menuitem, SPAction* action) +{ + bool active = false; + if (action && action->id) { + Glib::ustring id = action->id; + SPDesktop* dt = static_cast<SPDesktop*>(sp_action_get_view(action)); + + if (id == "ToggleGrid") { + active = dt->gridsEnabled(); + + } else if (id == "EditGuidesToggleLock") { + active = dt->namedview->lockguides; + + } else if (id == "ToggleGuides") { + active = dt->namedview->getGuides(); + + } else if (id == "ToggleRotationLock") { + active = dt->get_rotation_lock(); + + } + else if (id == "ViewCmsToggle") { + active = dt->colorProfAdjustEnabled(); + } + else if (id == "ViewSplitModeToggle") { + active = dt->splitMode(); + + } else if (id == "ViewXRayToggle") { + active = dt->xrayMode(); + + } else if (id == "ToggleCommandsToolbar") { + active = getStateFromPref(dt, "commands"); + + } else if (id == "ToggleSnapToolbar") { + active = getStateFromPref(dt, "snaptoolbox"); + + } else if (id == "ToggleToolToolbar") { + active = getStateFromPref(dt, "toppanel"); + + } else if (id == "ToggleToolbox") { + active = getStateFromPref(dt, "toolbox"); + + } else if (id == "ToggleRulers") { + active = getStateFromPref(dt, "rulers"); + + } else if (id == "ToggleScrollbars") { + active = getStateFromPref(dt, "scrollbars"); + + } else if (id == "TogglePalette") { + active = getStateFromPref(dt, "panels"); // Rename? + + } else if (id == "ToggleStatusbar") { + active = getStateFromPref(dt, "statusbar"); + + } else if (id == "FlipHorizontal") { + active = dt->is_flipped(SPDesktop::FLIP_HORIZONTAL); + + } else if (id == "FlipVertical") { + active = dt->is_flipped(SPDesktop::FLIP_VERTICAL); + + } else { + std::cerr << "checkitem_update: unhandled item: " << id << std::endl; + } + + menuitem->set_active(active); + } else { + std::cerr << "checkitem_update: unknown action" << std::endl; + } +} + +static Gtk::CheckMenuItem* +build_menu_check_item_from_verb(SPAction* action) +{ + // This does not work for some reason! + // Gtk::CheckMenuItem* menuitem = Gtk::manage(new Gtk::CheckMenuItem(action->name, true)); + // sp_shortcut_add_accelerator(GTK_WIDGET(menuitem->gobj()), sp_shortcut_get_primary(action->verb)); + + GtkWidget *item = gtk_check_menu_item_new_with_mnemonic(action->name); + sp_shortcut_add_accelerator(item, sp_shortcut_get_primary(action->verb)); + Gtk::CheckMenuItem* menuitem = Gtk::manage(Glib::wrap(GTK_CHECK_MENU_ITEM(item))); + + // Set initial state before connecting signals. + checkitem_update(menuitem, action); + + menuitem->signal_toggled().connect( + sigc::bind<Gtk::CheckMenuItem*, SPAction*>(sigc::ptr_fun(&item_activate), menuitem, action)); + menuitem->signal_select().connect( sigc::bind<SPAction*>(sigc::ptr_fun(&select_action), action)); + menuitem->signal_deselect().connect(sigc::bind<SPAction*>(sigc::ptr_fun(&deselect_action), action)); + + return menuitem; +} + + +// ================= Tasks Submenu ============== +static void +task_activated(SPDesktop* dt, int number) +{ + Inkscape::UI::UXManager::getInstance()->setTask(dt, number); + +#ifdef GDK_WINDOWING_QUARTZ + // call later, crashes during startup if called directly + g_idle_add(sync_menubar, nullptr); +#endif +} + +// Sets tip +static void +select_task(SPDesktop* dt, Glib::ustring tip) +{ + dt->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, tip.c_str()); +} + +// Clears tip +static void +deselect_task(SPDesktop* dt) +{ + dt->tipsMessageContext()->clear(); +} + +static void +add_tasks(Gtk::MenuShell* menu, SPDesktop* dt) +{ + const Glib::ustring data[3][2] = { + { C_("Interface setup", "Default"), _("Default interface setup") }, + { C_("Interface setup", "Custom"), _("Setup for custom task") }, + { C_("Interface setup", "Wide"), _("Setup for widescreen work") } + }; + + int active = Inkscape::UI::UXManager::getInstance()->getDefaultTask(dt); + + Gtk::RadioMenuItem::Group group; + + for (unsigned int i = 0; i < 3; ++i) { + + Gtk::RadioMenuItem* menuitem = Gtk::manage(new Gtk::RadioMenuItem(group, data[i][0])); + if (menuitem) { + if (active == i) { + menuitem->set_active(true); + } + + menuitem->signal_toggled().connect( + sigc::bind<SPDesktop*, int>(sigc::ptr_fun(&task_activated), dt, i)); + menuitem->signal_select().connect( + sigc::bind<SPDesktop*, Glib::ustring>(sigc::ptr_fun(&select_task), dt, data[i][1])); + menuitem->signal_deselect().connect( + sigc::bind<SPDesktop*>(sigc::ptr_fun(&deselect_task),dt)); + + menu->append(*menuitem); + } + } +} + + +static void +sp_recent_open(Gtk::RecentChooser* recentchooser) +{ + Glib::ustring uri = recentchooser->get_current_uri(); + + Glib::RefPtr<Gio::File> file = Gio::File::create_for_uri(uri); + + ConcreteInkscapeApplication<Gtk::Application>* app = &(ConcreteInkscapeApplication<Gtk::Application>::get_instance()); + + app->create_window(file); +} + +// =================== Main Menu ================ +// Recursively build menu and submenus. +void +build_menu(Gtk::MenuShell* menu, Inkscape::XML::Node* xml, Inkscape::UI::View::View* view, bool show_icons = true) +{ + if (menu == nullptr) { + std::cerr << "build_menu: menu is nullptr" << std::endl; + return; + } + + if (xml == nullptr) { + std::cerr << "build_menu: xml is nullptr" << std::endl; + return; + } + + // user preference for icons in menus (1: show all, -1: hide all; 0: theme chooses per icon) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_icons_pref = prefs->getInt("/theme/menuIcons", 0); + + Gtk::RadioMenuItem::Group group; + + for (auto menu_ptr = xml; menu_ptr != nullptr; menu_ptr = menu_ptr->next()) { + + if (menu_ptr->name()) { + + // show menu icons for current item? + bool show_icons_curr = show_icons; + if (show_icons_pref == 1) { // show all icons per global pref + show_icons_curr = true; + } else if (show_icons_pref == -1) { // hide all icons per global pref + show_icons_curr = false; + } else { // set according to 'show-icons' attribute in theme's XML file; value is fully inherited + const char *str = menu_ptr->attribute("show-icons"); + if (str) { + Glib::ustring ustr = str; + if (ustr == "true") { + show_icons_curr = true; + } else if (ustr == "false") { + show_icons_curr = false; + } else { + std::cerr << "build_menu: invalid value for 'show-icons' (use 'true' or 'false')." + << ustr << std::endl; + } + } + } + + Glib::ustring name = menu_ptr->name(); + + if (name == "inkscape") { + build_menu(menu, menu_ptr->firstChild(), view, show_icons_curr); + continue; + } + + if (name == "submenu") { + const char *name = menu_ptr->attribute("name"); + if (!name) { + g_warning("menus.xml: skipping submenu without name."); + continue; + } + + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_(name), true)); + Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu()); + build_menu(submenu, menu_ptr->firstChild(), view, show_icons_curr); + menuitem->set_submenu(*submenu); + menu->append(*menuitem); + + submenu->signal_map().connect( + sigc::bind<Gtk::Menu*>(sigc::ptr_fun(&shift_icons), submenu)); + + continue; + } + + if (name == "contextmenu") { + if (menu_ptr->attribute("id")) { + Glib::ustring id = menu_ptr->attribute("id"); + if (id == "canvas" || id == "layers" || id == "objects") { + Glib::ustring prefname = Glib::ustring::compose("/theme/menuIcons_%1", id); + prefs->setBool(prefname, show_icons_curr); + } else { + std::cerr << "build_menu: invalid contextmenu id: " << id << std::endl; + } + } else { + std::cerr << "build_menu: contextmenu element needs a valid id" << std::endl; + } + continue; + } + + if (name == "verb") { + if (menu_ptr->attribute("verb-id") != nullptr) { + Glib::ustring verb_name = menu_ptr->attribute("verb-id"); + + Inkscape::Verb *verb = Inkscape::Verb::getbyid(verb_name.c_str()); + if (verb != nullptr && verb->get_code() != SP_VERB_NONE) { + + SPAction* action = verb->get_action(Inkscape::ActionContext(view)); + if (menu_ptr->attribute("check") != nullptr) { + Gtk::MenuItem *menuitem = build_menu_check_item_from_verb(action); + if (menuitem) { + std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *> + verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view); + menuitems.push_back(verbmenuitem); + menu->append(*menuitem); + } + } else if (menu_ptr->attribute("radio") != nullptr) { + Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr, true, &group); + if (menuitem) { + if (menu_ptr->attribute("default") != nullptr) { + auto radiomenuitem = dynamic_cast<Gtk::RadioMenuItem*>(menuitem); + if (radiomenuitem) { + radiomenuitem->set_active(true); + } + } + std::pair<std::pair<unsigned int, Gtk::MenuItem *>, Inkscape::UI::View::View *> + verbmenuitem = std::make_pair(std::make_pair(verb->get_code(), menuitem), view); + menuitems.push_back(verbmenuitem); + menu->append(*menuitem); + } + } else { + Gtk::MenuItem* menuitem = build_menu_item_from_verb(action, show_icons_curr); + if (menuitem) { + menu->append(*menuitem); + } + +#ifdef GDK_WINDOWING_QUARTZ + // for moving menu items to "Inkscape" menu + switch (verb->get_code()) { + case SP_VERB_DIALOG_DISPLAY: + case SP_VERB_DIALOG_INPUT: + case SP_VERB_HELP_ABOUT: + menuitems.emplace_back(std::make_pair(verb->get_code(), menuitem), view); + } +#endif + } + } else if (true +#ifndef HAVE_ASPELL + && strcmp(verb_name.c_str(), "DialogSpellcheck") != 0 +#endif + ) { + std::cerr << "build_menu: no verb with id: " << verb_name << std::endl; + } + } + continue; + } + + // This is used only for wide-screen vs non-wide-screen displays. + // The code should be rewritten to use actions like everything else here. + if (name == "task-checkboxes") { + add_tasks(menu, static_cast<SPDesktop*>(view)); + continue; + } + + if (name == "recent-file-list") { + + // Filter recent files to those already opened in Inkscape. + Glib::RefPtr<Gtk::RecentFilter> recentfilter = Gtk::RecentFilter::create(); + recentfilter->add_application(g_get_prgname()); + recentfilter->add_application("org.inkscape.Inkscape"); + recentfilter->add_application("inkscape"); +#ifdef _WIN32 + recentfilter->add_application("inkscape.exe"); +#endif + + Gtk::RecentChooserMenu* recentchoosermenu = Gtk::manage(new Gtk::RecentChooserMenu()); + int max = Inkscape::Preferences::get()->getInt("/options/maxrecentdocuments/value"); + recentchoosermenu->set_limit(max); + recentchoosermenu->set_sort_type(Gtk::RECENT_SORT_MRU); // Sort most recent first. + recentchoosermenu->set_show_tips(true); + recentchoosermenu->set_show_not_found(false); + recentchoosermenu->add_filter(recentfilter); + recentchoosermenu->signal_item_activated().connect( + sigc::bind<Gtk::RecentChooserMenu*>(sigc::ptr_fun(&sp_recent_open), recentchoosermenu)); + + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::MenuItem(_("Open _Recent"), true)); + menuitem->set_submenu(*recentchoosermenu); + menu->append(*menuitem); + continue; + } + + if (name == "separator") { + Gtk::MenuItem* menuitem = Gtk::manage(new Gtk::SeparatorMenuItem()); + menu->append(*menuitem); + continue; + } + + // Comments and items handled elsewhere. + if (name == "comment" || + name == "filters-list" || + name == "effects-list" ) { + continue; + } + + std::cerr << "build_menu: unhandled option: " << name << std::endl; + + } else { + std::cerr << "build_menu: xml node has no name!" << std::endl; + } + } +} + +Gtk::MenuBar* +build_menubar(Inkscape::UI::View::View* view) +{ + Gtk::MenuBar* menubar = Gtk::manage(new Gtk::MenuBar()); + build_menu(menubar, INKSCAPE.get_menus()->parent(), view); + SP_ACTIVE_DESKTOP->_menu_update.connect(sigc::ptr_fun(&set_menuitems)); + return menubar; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/desktop/menubar.h b/src/ui/desktop/menubar.h new file mode 100644 index 0000000..065fb67 --- /dev/null +++ b/src/ui/desktop/menubar.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_DESKTOP_MENUBAR_H +#define SEEN_DESKTOP_MENUBAR_H + +/** + * @file + * Desktop main menu bar code. + */ +/* + * Authors: + * Tavmjong Bah + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +namespace Gtk { + class MenuBar; + class MenuItem; +} + +namespace Inkscape { +namespace UI { +namespace View { + class View; +} +} +} + +bool getStateFromPref(SPDesktop *dt, Glib::ustring item); +Gtk::MenuBar* build_menubar(Inkscape::UI::View::View* view); +Gtk::MenuItem *get_menu_item_for_verb(unsigned int verb, Inkscape::UI::View::View *); + +#endif // SEEN_DESKTOP_MENUBAR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog-events.cpp b/src/ui/dialog-events.cpp new file mode 100644 index 0000000..17be2fe --- /dev/null +++ b/src/ui/dialog-events.cpp @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Event handler for dialog windows. + */ +/* Authors: + * bulia byak <bulia@dr.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2003-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/entry.h> +#include <gtkmm/window.h> + +#include "desktop.h" +#include "inkscape.h" +#include "include/macros.h" +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.h" + + +/** + * Remove focus from window to whoever it is transient for. + */ +void sp_dialog_defocus_cpp(Gtk::Window *win) +{ + //find out the document window we're transient for + Gtk::Window *w = win->get_transient_for(); + + //switch to it + if (w) { + w->present(); + } +} + +void +sp_dialog_defocus (GtkWindow *win) +{ + GtkWindow *w; + //find out the document window we're transient for + w = gtk_window_get_transient_for(GTK_WINDOW(win)); + //switch to it + + if (w) { + gtk_window_present (w); + } +} + + +/** + * Callback to defocus a widget's parent dialog. + */ +void sp_dialog_defocus_callback_cpp(Gtk::Entry *e) +{ + sp_dialog_defocus_cpp(dynamic_cast<Gtk::Window *>(e->get_toplevel())); +} + +void +sp_dialog_defocus_callback (GtkWindow * /*win*/, gpointer data) +{ + sp_dialog_defocus( GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(data))) ); +} + + + +void +sp_dialog_defocus_on_enter_cpp (Gtk::Entry *e) +{ + e->signal_activate().connect(sigc::bind(sigc::ptr_fun(&sp_dialog_defocus_callback_cpp), e)); +} + +void +sp_dialog_defocus_on_enter (GtkWidget *w) +{ + g_signal_connect ( G_OBJECT (w), "activate", + G_CALLBACK (sp_dialog_defocus_callback), w ); +} + + + +gboolean +sp_dialog_event_handler (GtkWindow *win, GdkEvent *event, gpointer data) +{ + +// if the focus is inside the Text and Font textview, do nothing + GObject *dlg = G_OBJECT(data); + if (g_object_get_data (dlg, "eatkeys")) { + return FALSE; + } + + gboolean ret = FALSE; + + switch (event->type) { + + case GDK_KEY_PRESS: + + switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: + sp_dialog_defocus (win); + ret = TRUE; + break; + case GDK_KEY_F4: + case GDK_KEY_w: + case GDK_KEY_W: + // close dialog + if (MOD__CTRL_ONLY(event)) { + + /* this code sends a delete_event to the dialog, + * instead of just destroying it, so that the + * dialog can do some housekeeping, such as remember + * its position. + */ + GdkEventAny event; + GtkWidget *widget = GTK_WIDGET(win); + event.type = GDK_DELETE; + event.window = gtk_widget_get_window (widget); + event.send_event = TRUE; + g_object_ref (G_OBJECT (event.window)); + gtk_main_do_event(reinterpret_cast<GdkEvent*>(&event)); + g_object_unref (G_OBJECT (event.window)); + + ret = TRUE; + } + break; + default: // pass keypress to the canvas + break; + } + default: + ; + } + + return ret; + +} + + + +/** + * Make the argument dialog transient to the currently active document + * window. + */ +void sp_transientize(GtkWidget *dialog) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); +#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs + // _set_skip_taskbar_hint makes transient dialogs NON-transient! When dialogs + // are made transient (_set_transient_for), they are already removed from + // the taskbar in Win32. + if (prefs->getBool( "/options/dialogsskiptaskbar/value")) { + gtk_window_set_skip_taskbar_hint (GTK_WINDOW (dialog), TRUE); + } +#endif + + gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (transient_policy) { + + // if there's an active document window, attach dialog to it as a transient: + + if ( SP_ACTIVE_DESKTOP ) + { + SP_ACTIVE_DESKTOP->setWindowTransient (dialog, transient_policy); + } + } +} // end of sp_transientize() + +void on_transientize (SPDesktop *desktop, win_data *wd ) +{ + sp_transientize_callback (desktop, wd); +} + +void +sp_transientize_callback ( SPDesktop *desktop, win_data *wd ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited( "/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 1; +#endif + + if (!transient_policy) + return; + + if (wd->win) + { + desktop->setWindowTransient (wd->win, transient_policy); + } +} + +void on_dialog_hide (GtkWidget *w) +{ + if (w) + gtk_widget_hide (w); +} + +void on_dialog_unhide (GtkWidget *w) +{ + if (w) + gtk_widget_show (w); +} + +gboolean +sp_dialog_hide(GObject * /*object*/, gpointer data) +{ + GtkWidget *dlg = GTK_WIDGET(data); + + if (dlg) + gtk_widget_hide (dlg); + + return TRUE; +} + + + +gboolean +sp_dialog_unhide(GObject * /*object*/, gpointer data) +{ + GtkWidget *dlg = GTK_WIDGET(data); + + if (dlg) + gtk_widget_show (dlg); + + return TRUE; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog-events.h b/src/ui/dialog-events.h new file mode 100644 index 0000000..7bc7483 --- /dev/null +++ b/src/ui/dialog-events.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Event handler for dialog windows + */ +/* Authors: + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2003-2014 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOG_EVENTS_H +#define SEEN_DIALOG_EVENTS_H + +#include <gtk/gtk.h> + +/* + * event callback can only accept one argument, but we need two, + * hence this struct. + * each dialog has a local static copy: + * win is the dialog window + * stop is the transientize semaphore: when 0, retransientizing this dialog + * is allowed + */ + +namespace Gtk { +class Window; +class Entry; +} + +class SPDesktop; + +struct win_data { + GtkWidget *win; + guint stop; +}; + + +gboolean sp_dialog_event_handler ( GtkWindow *win, + GdkEvent *event, + gpointer data ); + +void sp_dialog_defocus_cpp (Gtk::Window *win); +void sp_dialog_defocus_callback_cpp(Gtk::Entry *e); +void sp_dialog_defocus_on_enter_cpp(Gtk::Entry *e); + +void sp_dialog_defocus ( GtkWindow *win ); +void sp_dialog_defocus_callback ( GtkWindow *win, gpointer data ); +void sp_dialog_defocus_on_enter ( GtkWidget *w ); +void sp_transientize ( GtkWidget *win ); + +void on_transientize ( SPDesktop *desktop, + win_data *wd ); + +void sp_transientize_callback ( SPDesktop *desktop, + win_data *wd ); + +void on_dialog_hide (GtkWidget *w); +void on_dialog_unhide (GtkWidget *w); + +//gboolean sp_dialog_hide (GObject *object, gpointer data); +//gboolean sp_dialog_unhide (GObject *object, gpointer data); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/aboutbox.cpp b/src/ui/dialog/aboutbox.cpp new file mode 100644 index 0000000..72d31cc --- /dev/null +++ b/src/ui/dialog/aboutbox.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape About box - implementation. + */ +/* Authors: + * Derek P. Moore <derekm@hackunix.org> + * MenTaLguY <mental@rydia.net> + * Kees Cook <kees@outflux.net> + * Jon Phillips <jon@rejon.org> + * Abhishek Sharma + * + * Copyright (C) 2004 Derek P. Moore + * Copyright 2004 Kees Cook + * Copyright 2004 Jon Phillips + * Copyright 2005 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "aboutbox.h" + +#include <fstream> + +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> + +#include <gtkmm/aspectframe.h> +#include <gtkmm/textview.h> + +#include "document.h" +#include "inkscape-version.h" +#include "path-prefix.h" +#include "text-editing.h" + +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/view/svg-view-widget.h" + +#include "util/units.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static AboutBox *window=nullptr; + +void AboutBox::show_about() { + if (!window) + window = new AboutBox(); + window->show(); +} + +void AboutBox::hide_about() { + if (window) + window->hide(); +} + +/** + * Constructor + */ +AboutBox::AboutBox() + : _splash_widget(nullptr) +{ + // call this first + initStrings(); + + // Insert the Splash widget. This is placed directly into the + // content area of the dialog, whereas everything else is placed + // automatically by the Gtk::AboutDialog parent class + build_splash_widget(); + if (_splash_widget) { + get_content_area()->pack_end(*manage(_splash_widget), true, true); + _splash_widget->show_all(); + } + + // Set Application metadata, which will be automatically + // inserted into text widgets by the Gtk::AboutDialog parent class + // clang-format off + set_program_name ( "Inkscape"); + set_version ( Inkscape::version_string); + set_logo_icon_name( INKSCAPE_ICON("org.inkscape.Inkscape")); + set_website ( "https://www.inkscape.org"); + set_website_label (_("Inkscape website")); + set_license_type (Gtk::LICENSE_GPL_3_0); + set_copyright (_("© 2020 Inkscape Developers")); + set_comments (_("Open Source Scalable Vector Graphics Editor\n" + "Draw Freely.")); + // clang-format on + + get_content_area()->set_border_width(3); + get_action_area()->set_border_width(3); +} + +/** + * @brief Create a Gtk::AspectFrame containing the splash image + */ +void AboutBox::build_splash_widget() { + /* TRANSLATORS: This is the filename of the `About Inkscape' picture in + the `screens' directory. Thus the translation of "about.svg" should be + the filename of its translated version, e.g. about.zh.svg for Chinese. + + Please don't translate the filename unless the translated picture exists. */ + + // Try to get the translated version of the 'About Inkscape' file first. If the + // translation fails, or if the file does not exist, then fall-back to the + // default untranslated "about.svg" file + // + // FIXME? INKSCAPE_SCREENSDIR and "about.svg" are in UTF-8, not the + // native filename encoding... and the filename passed to sp_document_new + // should be in UTF-*8.. + auto about = Glib::build_filename(INKSCAPE_SCREENSDIR, _("about.svg")); + if (!Glib::file_test (about, Glib::FILE_TEST_EXISTS)) { + about = Glib::build_filename(INKSCAPE_SCREENSDIR, "about.svg"); + } + + // Create an Inkscape document from the 'About Inkscape' picture + SPDocument *doc=SPDocument::createNewDoc (about.c_str(), TRUE); + + // Leave _splash_widget as a nullptr if there is no document + if(doc) { + SPObject *version = doc->getObjectById("version"); + if ( version && SP_IS_TEXT(version) ) { + sp_te_set_repr_text_multiline (SP_TEXT (version), Inkscape::version_string); + } + doc->ensureUpToDate(); + + auto viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + + // temporary hack: halve the dimensions so the dialog will fit + double width=doc->getWidth().value("px") / 2.0; + double height=doc->getHeight().value("px") / 2.0; + viewer->setResize(width, height); + + _splash_widget = new Gtk::AspectFrame(); + _splash_widget->unset_label(); + _splash_widget->set_shadow_type(Gtk::SHADOW_NONE); + _splash_widget->property_ratio() = width / height; + _splash_widget->add(*viewer); + } +} + +/** + * @brief Read the author and translator credits from file + */ +void AboutBox::initStrings() { + //############################## + //# A U T H O R S + //############################## + + // Create an empty vector to store the list of authors + std::vector<Glib::ustring> authors; + + // Try to copy the list of authors from the "AUTHORS" file, which + // should have been installed into the share/doc directory + auto authors_filename = Glib::build_filename(INKSCAPE_DOCDIR, "AUTHORS"); + std::ifstream authors_filestream(authors_filename); + if(authors_filestream) { + std::string author_line; + + while (std::getline(authors_filestream, author_line)) { + authors.emplace_back(author_line); + } + } + + // Set the author credits in this dialog, using the author list + set_authors(authors); + + //############################## + //# T R A N S L A T O R S + //############################## + + Glib::ustring translators_text; + + // TRANSLATORS: Put here your name (and other national contributors') + // one per line in the form of: name surname (email). Use \n for newline. + Glib::ustring thisTranslation = _("translator-credits"); + + /** + * See if the translators for the current language + * made an entry for "translator-credits". If it exists, + * put it at the top of the window, add some space between + * it and the list of all translators. + * + * NOTE: Do we need 2 more .po entries for titles: + * "translators for this language" + * "all translators" ?? + */ + if (thisTranslation != "translator-credits") { + translators_text.append(thisTranslation); + translators_text.append("\n\n\n"); + } + + auto translators_filename = Glib::build_filename(INKSCAPE_DOCDIR, "TRANSLATORS"); + + if (Glib::file_test (translators_filename, Glib::FILE_TEST_EXISTS)) { + auto all_translators = Glib::file_get_contents(translators_filename); + translators_text.append(all_translators); + } + + set_translator_credits(translators_text); +} + +void AboutBox::on_response(int response_id) { + hide(); +} +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/aboutbox.h b/src/ui/dialog/aboutbox.h new file mode 100644 index 0000000..e5b9f27 --- /dev/null +++ b/src/ui/dialog/aboutbox.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape About box + * + * The standard Gnome::UI::About class doesn't include a place to stuff + * a renderable View that holds the classic Inkscape "about.svg". + */ +/* Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2005 Kees Cook + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ABOUTBOX_H +#define INKSCAPE_UI_DIALOG_ABOUTBOX_H + +#include <gtkmm/aboutdialog.h> + +namespace Gtk { +class AspectFrame; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class AboutBox : public Gtk::AboutDialog { + +public: + + static void show_about(); + static void hide_about(); + +private: + + AboutBox(); + + /** A widget containing an SVG "splash screen" + * image to display in the content area of the dialo + */ + Gtk::AspectFrame *_splash_widget; + + void initStrings(); + void build_splash_widget(); + + void on_response(int response_id) override; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_ABOUTBOX_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/align-and-distribute.cpp b/src/ui/dialog/align-and-distribute.cpp new file mode 100644 index 0000000..31605a0 --- /dev/null +++ b/src/ui/dialog/align-and-distribute.cpp @@ -0,0 +1,1339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Align and Distribute dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * Tim Dwyer <tgdwyer@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include <2geom/transforms.h> + +#include <utility> + +#include "align-and-distribute.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "graphlayout.h" +#include "inkscape.h" +#include "preferences.h" +#include "removeoverlap.h" +#include "text-editing.h" +#include "unclump.h" +#include "verbs.h" + +#include "object/sp-flowtext.h" +#include "object/sp-item-transform.h" +#include "object/sp-root.h" +#include "object/sp-text.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/spinbutton.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/////////helper classes////////////////////////////////// + +Action::Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog): + _dialog(dialog), + _id(std::move(id)), + _parent(parent) +{ + Gtk::Image* pIcon = Gtk::manage(new Gtk::Image()); + pIcon = sp_get_icon_image(_id, Gtk::ICON_SIZE_LARGE_TOOLBAR); + Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + + pButton->signal_clicked() + .connect(sigc::mem_fun(*this, &Action::on_button_click)); + pButton->set_tooltip_text(tiptext); + parent.attach(*pButton, column, row, 1, 1); +} + + +void ActionAlign::do_node_action(Inkscape::UI::Tools::NodeTool *nt, int verb) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prev_pref = prefs->getInt("/dialogs/align/align-nodes-to"); + switch(verb){ + case SP_VERB_ALIGN_HORIZONTAL_LEFT: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_CENTER: + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_HORIZONTAL_RIGHT: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::Y); + break; + case SP_VERB_ALIGN_VERTICAL_TOP: + prefs->setInt("/dialogs/align/align-nodes-to", MAX_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_CENTER: + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_VERTICAL_BOTTOM: + prefs->setInt("/dialogs/align/align-nodes-to", MIN_NODE ); + nt->_multipath->alignNodes(Geom::X); + break; + case SP_VERB_ALIGN_BOTH_CENTER: + nt->_multipath->alignNodes(Geom::X); + nt->_multipath->alignNodes(Geom::Y); + break; + default:return; + } + prefs->setInt("/dialogs/align/align-nodes-to", prev_pref ); +} + +void ActionAlign::do_action(SPDesktop *desktop, int index) +{ + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool sel_as_group = prefs->getBool("/dialogs/align/sel-as-groups"); + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + Coeffs a = _allCoeffs[index]; // copy + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + Selection::CompareSize horiz = (a.mx0 != 0.0) || (a.mx1 != 0.0) + ? Selection::VERTICAL : Selection::HORIZONTAL; + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(horiz); + break; + case SMALLEST: + focus = selection->smallestItem(horiz); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) + b = focus->desktopPreferredBounds(); + + g_return_if_fail(b); + + if (desktop->is_yaxisdown()) { + std::swap(a.my0, a.my1); + std::swap(a.sy0, a.sy1); + } + + // Generate the move point from the selected bounding box + Geom::Point mp = Geom::Point(a.mx0 * b->min()[Geom::X] + a.mx1 * b->max()[Geom::X], + a.my0 * b->min()[Geom::Y] + a.my1 * b->max()[Geom::Y]); + + if (sel_as_group) { + if (focus) { + // use bounding box of all selected elements except the "focused" element + Inkscape::ObjectSet copy; + copy.add(selection->objects().begin(), selection->objects().end()); + copy.remove(focus); + b = copy.preferredBounds(); + } else { + // use bounding box of all selected elements + b = selection->preferredBounds(); + } + } + + //Move each item in the selected list separately + bool changed = false; + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + if (!sel_as_group) + b = (item)->desktopPreferredBounds(); + if (b && (!focus || (item) != focus)) { + Geom::Point const sp(a.sx0 * b->min()[Geom::X] + a.sx1 * b->max()[Geom::X], + a.sy0 * b->min()[Geom::Y] + a.sy1 * b->max()[Geom::Y]); + Geom::Point const mp_rel( mp - sp ); + if (LInfty(mp_rel) > 1e-9) { + item->move_rel(Geom::Translate(mp_rel)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done( desktop->getDocument() , SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align")); + } +} + + +ActionAlign::Coeffs const ActionAlign::_allCoeffs[19] = { + {1., 0., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR}, + {1., 0., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT}, + {.5, .5, 0., 0., .5, .5, 0., 0., SP_VERB_ALIGN_HORIZONTAL_CENTER}, + {0., 1., 0., 0., 0., 1., 0., 0., SP_VERB_ALIGN_HORIZONTAL_RIGHT}, + {0., 1., 0., 0., 1., 0., 0., 0., SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR}, + {0., 0., 0., 1., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP}, + {0., 0., .5, .5, 0., 0., .5, .5, SP_VERB_ALIGN_VERTICAL_CENTER}, + {0., 0., 1., 0., 0., 0., 1., 0., SP_VERB_ALIGN_VERTICAL_BOTTOM}, + {0., 0., 1., 0., 0., 0., 0., 1., SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR}, + {1., 0., 0., 1., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT}, + {0., 1., 0., 1., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT}, + {0., 1., 1., 0., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT}, + {1., 0., 1., 0., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT}, + {0., 1., 1., 0., 1., 0., 0., 1., SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR}, + {1., 0., 1., 0., 0., 1., 0., 1., SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR}, + {1., 0., 0., 1., 0., 1., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR}, + {0., 1., 0., 1., 1., 0., 1., 0., SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR}, + {.5, .5, .5, .5, .5, .5, .5, .5, SP_VERB_ALIGN_BOTH_CENTER} +}; + +void ActionAlign::do_verb_action(SPDesktop *desktop, int verb) +{ + Inkscape::UI::Tools::ToolBase *event_context = desktop->getEventContext(); + if (INK_IS_NODE_TOOL(event_context)) { + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + if(!nt->_selected_nodes->empty()){ + do_node_action(nt, verb); + return; + } + } + do_action(desktop, verb_to_coeff(verb)); +} + +int ActionAlign::verb_to_coeff(int verb) { + + for(guint i = 0; i < G_N_ELEMENTS(_allCoeffs); i++) { + if (_allCoeffs[i].verb_id == verb) { + return i; + } + } + + return -1; +} + +BBoxSort::BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd) : + item(pItem), + bbox (bounds) +{ + anchor = kBegin * bbox.min()[orientation] + kEnd * bbox.max()[orientation]; +} +BBoxSort::BBoxSort(const BBoxSort &rhs) + //NOTE : this copy ctor is called O(sort) when sorting the vector + //this is bad. The vector should be a vector of pointers. + //But I'll wait the bohem GC before doing that += default; + +bool operator< (const BBoxSort &a, const BBoxSort &b) +{ + return (a.anchor < b.anchor); +} + +class ActionDistribute : public Action { +public : + ActionDistribute(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + bool onInterSpace, + Geom::Dim2 orientation, + double kBegin, double kEnd + ): + Action(id, tiptext, row, column, + dialog.distribute_table(), dialog), + _dialog(dialog), + _onInterSpace(onInterSpace), + _orientation(orientation), + _kBegin(kBegin), + _kEnd( kEnd) + {} + +private : + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + std::vector<SPItem*>::iterator second(selected.begin()); + ++second; + if (second == selected.end()) return; + + double kBegin = _kBegin; + double kEnd = _kEnd; + if (_orientation == Geom::Y && desktop->is_yaxisdown()) { + kBegin = 1. - kBegin; + kEnd = 1. - kEnd; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + std::vector< BBoxSort > sorted; + for (auto item : selected){ + Geom::OptRect bbox = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, _orientation, kBegin, kEnd); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + unsigned int len = sorted.size(); + bool changed = false; + if (_onInterSpace) + { + //overall bboxes span + float dist = (sorted.back().bbox.max()[_orientation] - + sorted.front().bbox.min()[_orientation]); + //space eaten by bboxes + float span = 0; + for (unsigned int i = 0; i < len; i++) + { + span += sorted[i].bbox[_orientation].extent(); + } + //new distance between each bbox + float step = (dist - span) / (len - 1); + float pos = sorted.front().bbox.min()[_orientation]; + for ( std::vector<BBoxSort> ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + if (!Geom::are_near(pos, it->bbox.min()[_orientation], 1e-6)) { + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it->bbox.min()[_orientation]; + it->item->move_rel(Geom::Translate(t)); + changed = true; + } + pos += it->bbox[_orientation].extent(); + pos += step; + } + } + else + { + //overall anchor span + float dist = sorted.back().anchor - sorted.front().anchor; + //distance between anchors + float step = dist / (len - 1); + + for ( unsigned int i = 0; i < len ; i ++ ) + { + BBoxSort & it(sorted[i]); + //new anchor position + float pos = sorted.front().anchor + i * step; + //Don't move if we are really close + if (!Geom::are_near(pos, it.anchor, 1e-6)) { + //Compute translation + Geom::Point t(0.0, 0.0); + t[_orientation] = pos - it.anchor; + //translate + it.item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + if (changed) { + DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute")); + } + } + guint _index; + AlignAndDistribute &_dialog; + bool _onInterSpace; + Geom::Dim2 _orientation; + + double _kBegin; + double _kEnd; + +}; + + +class ActionNode : public Action { +public : + ActionNode(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint column, + AlignAndDistribute &dialog, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, 0, column, + dialog.nodes_table(), dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + + void on_button_click() override { + if (!_dialog.getDesktop()) { + return; + } + + Inkscape::UI::Tools::ToolBase *event_context = _dialog.getDesktop()->getEventContext(); + + if (!INK_IS_NODE_TOOL(event_context)) { + return; + } + + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(event_context); + + if (_distribute) { + nt->_multipath->distributeNodes(_orientation); + } else { + nt->_multipath->alignNodes(_orientation); + } + } +}; + +class ActionRemoveOverlaps : public Action { +private: + Gtk::Label removeOverlapXGapLabel; + Gtk::Label removeOverlapYGapLabel; + Inkscape::UI::Widget::SpinButton removeOverlapXGap; + Inkscape::UI::Widget::SpinButton removeOverlapYGap; + +public: + ActionRemoveOverlaps(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column + 4, + dialog.removeOverlap_table(), dialog) + { + dialog.removeOverlap_table().set_column_spacing(3); + + removeOverlapXGap.set_digits(1); + removeOverlapXGap.set_size_request(60, -1); + removeOverlapXGap.set_increments(1.0, 0); + removeOverlapXGap.set_range(-1000.0, 1000.0); + removeOverlapXGap.set_value(0); + removeOverlapXGap.set_tooltip_text(_("Minimum horizontal gap (in px units) between bounding boxes")); + //TRANSLATORS: "H:" stands for horizontal gap + removeOverlapXGapLabel.set_text_with_mnemonic(C_("Gap", "_H:")); + removeOverlapXGapLabel.set_mnemonic_widget(removeOverlapXGap); + + removeOverlapYGap.set_digits(1); + removeOverlapYGap.set_size_request(60, -1); + removeOverlapYGap.set_increments(1.0, 0); + removeOverlapYGap.set_range(-1000.0, 1000.0); + removeOverlapYGap.set_value(0); + removeOverlapYGap.set_tooltip_text(_("Minimum vertical gap (in px units) between bounding boxes")); + /* TRANSLATORS: Vertical gap */ + removeOverlapYGapLabel.set_text_with_mnemonic(C_("Gap", "_V:")); + removeOverlapYGapLabel.set_mnemonic_widget(removeOverlapYGap); + + dialog.removeOverlap_table().attach(removeOverlapXGapLabel, column, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapXGap, column+1, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGapLabel, column+2, row, 1, 1); + dialog.removeOverlap_table().attach(removeOverlapYGap, column+3, row, 1, 1); + } + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // xGap and yGap are the minimum space required between bounding rectangles. + double const xGap = removeOverlapXGap.get_value(); + double const yGap = removeOverlapYGap.get_value(); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + removeoverlap(vec, xGap, yGap); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Remove overlaps")); + } +}; + +class ActionGraphLayout : public Action { +public: + ActionGraphLayout(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + graphlayout(vec); + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Arrange connector network")); + } +}; + +class ActionExchangePositions : public Action { +public: + enum SortOrder { + None, + ZOrder, + Clockwise + }; + + ActionExchangePositions(Glib::ustring const &id, + Glib::ustring const &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, SortOrder order = None) : + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog), + sortOrder(order) + {}; + + +private : + const SortOrder sortOrder; + static boost::optional<Geom::Point> center; + + static bool sort_compare(const SPItem * a,const SPItem * b) { + if (a == nullptr) return false; + if (b == nullptr) return true; + if (center) { + Geom::Point point_a = a->getCenter() - (*center); + Geom::Point point_b = b->getCenter() - (*center); + // First criteria: Sort according to the angle to the center point + double angle_a = atan2(double(point_a[Geom::Y]), double(point_a[Geom::X])); + double angle_b = atan2(double(point_b[Geom::Y]), double(point_b[Geom::X])); + double dt_yaxisdir = SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->yaxisdir() : 1; + angle_a *= -dt_yaxisdir; + angle_b *= -dt_yaxisdir; + if (angle_a != angle_b) return (angle_a < angle_b); + // Second criteria: Sort according to the distance the center point + Geom::Coord length_a = point_a.length(); + Geom::Coord length_b = point_b.length(); + if (length_a != length_b) return (length_a > length_b); + } + // Last criteria: Sort according to the z-coordinate + return sp_item_repr_compare_position(a,b)<0; + } + + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + // sort the list + if (sortOrder != None) { + if (sortOrder == Clockwise) { + center = selection->center(); + } else { // sorting by ZOrder is outomatically done by not setting the center + center.reset(); + } + sort(selected.begin(),selected.end(),sort_compare); + } + + Geom::Point p1 = selected.back()->getCenter(); + for (SPItem *item : selected) + { + Geom::Point p2 = item->getCenter(); + Geom::Point delta = p1 - p2; + item->move_rel(Geom::Translate(delta[Geom::X],delta[Geom::Y] )); + p1 = p2; + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Exchange Positions")); + } +}; + +// instantiate the private static member +boost::optional<Geom::Point> ActionExchangePositions::center; + +class ActionUnclump : public Action { +public : + ActionUnclump(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + if (!_dialog.getDesktop()) return; + + // see comment in ActionAlign above + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + auto tmp = _dialog.getDesktop()->getSelection()->items(); + std::vector<SPItem*> x(tmp.begin(), tmp.end()); + unclump (x); + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Unclump")); + } +}; + +class ActionRandomize : public Action { +public : + ActionRandomize(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog): + Action(id, tiptext, row, column, + dialog.rearrange_table(), dialog) + {} + +private : + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box"); + Geom::OptRect sel_bbox = !prefs_bbox ? selection->visualBounds() : selection->geometricBounds(); + if (!sel_bbox) { + return; + } + + // This bbox is cached between calls to randomize, so that there's no growth nor shrink + // nor drift on sequential randomizations. Discard cache on global (or better active + // desktop's) selection_change signal. + if (!_dialog.randomize_bbox) { + _dialog.randomize_bbox = *sel_bbox; + } + + // see comment in ActionAlign above + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + for (auto item : selected) + { + desktop->getDocument()->ensureUpToDate(); + Geom::OptRect item_box = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds(); + if (item_box) { + // find new center, staying within bbox + double x = _dialog.randomize_bbox->min()[Geom::X] + (*item_box)[Geom::X].extent() /2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::X].extent() - (*item_box)[Geom::X].extent()); + double y = _dialog.randomize_bbox->min()[Geom::Y] + (*item_box)[Geom::Y].extent()/2 + + g_random_double_range (0, (*_dialog.randomize_bbox)[Geom::Y].extent() - (*item_box)[Geom::Y].extent()); + // displacement is the new center minus old: + Geom::Point t = Geom::Point (x, y) - 0.5*(item_box->max() + item_box->min()); + item->move_rel(Geom::Translate(t)); + } + } + + // restore compensation setting + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Randomize positions")); + } +}; + +struct Baselines +{ + SPItem *_item; + Geom::Point _base; + Geom::Dim2 _orientation; + Baselines(SPItem *item, Geom::Point base, Geom::Dim2 orientation) : + _item (item), + _base (base), + _orientation (orientation) + {} +}; + +static bool operator< (const Baselines &a, const Baselines &b) +{ + return (a._base[a._orientation] < b._base[b._orientation]); +} + +class ActionBaseline : public Action { +public : + ActionBaseline(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, + guint column, + AlignAndDistribute &dialog, + Gtk::Grid &table, + Geom::Dim2 orientation, bool distribute): + Action(id, tiptext, row, column, + table, dialog), + _orientation(orientation), + _distribute(distribute) + {} + +private : + Geom::Dim2 _orientation; + bool _distribute; + void on_button_click() override + { + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + Inkscape::Selection *selection = desktop->getSelection(); + if (!selection) return; + + std::vector<SPItem*> selected(selection->items().begin(), selection->items().end()); + + //Check 2 or more selected objects + if (selected.size() < 2) return; + + Geom::Point b_min = Geom::Point (HUGE_VAL, HUGE_VAL); + Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL); + + std::vector<Baselines> sorted; + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X]; + if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y]; + if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X]; + if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y]; + Baselines b (item, base, _orientation); + sorted.push_back(b); + } + } + } + + if (sorted.size() <= 1) return; + + //sort baselines + std::stable_sort(sorted.begin(), sorted.end()); + + bool changed = false; + + if (_distribute) { + double step = (b_max[_orientation] - b_min[_orientation])/(sorted.size() - 1); + for (unsigned int i = 0; i < sorted.size(); i++) { + SPItem *item = sorted[i]._item; + Geom::Point base = sorted[i]._base; + Geom::Point t(0.0, 0.0); + t[_orientation] = b_min[_orientation] + step * i - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Distribute text baselines")); + } + + } else { //align + Geom::Point ref_point; + SPItem *focus = nullptr; + Geom::OptRect b = Geom::OptRect(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + switch (AlignTarget(prefs->getInt("/dialogs/align/align-to", 6))) + { + case LAST: + focus = SP_ITEM(selected.back()); + break; + case FIRST: + focus = SP_ITEM(selected.front()); + break; + case BIGGEST: + focus = selection->largestItem(Selection::AREA); + break; + case SMALLEST: + focus = selection->smallestItem(Selection::AREA); + break; + case PAGE: + b = desktop->getDocument()->preferredBounds(); + break; + case DRAWING: + b = desktop->getDocument()->getRoot()->desktopPreferredBounds(); + break; + case SELECTION: + b = selection->preferredBounds(); + break; + default: + g_assert_not_reached (); + break; + }; + + if(focus) { + if (SP_IS_TEXT (focus) || SP_IS_FLOWTEXT (focus)) { + ref_point = *(te_get_layout(focus)->baselineAnchorPoint())*(focus->i2dt_affine()); + } else { + ref_point = focus->desktopPreferredBounds()->min(); + } + } else { + ref_point = b->min(); + } + + for (auto item : selected) + { + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT (item)) { + Inkscape::Text::Layout const *layout = te_get_layout(item); + boost::optional<Geom::Point> pt = layout->baselineAnchorPoint(); + if (pt) { + Geom::Point base = *pt * (item)->i2dt_affine(); + Geom::Point t(0.0, 0.0); + t[_orientation] = ref_point[_orientation] - base[_orientation]; + item->move_rel(Geom::Translate(t)); + changed = true; + } + } + } + + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + _("Align text baselines")); + } + } + } +}; + + + +static void on_tool_changed(AlignAndDistribute *daad) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && desktop->getEventContext()) + daad->setMode(tools_active(desktop) == TOOLS_NODES); + else + daad->setMode(false); + +} + +static void on_selection_changed(AlignAndDistribute *daad) +{ + daad->randomize_bbox = Geom::OptRect(); +} + +///////////////////////////////////////////////////////// + + + + +AlignAndDistribute::AlignAndDistribute() + : UI::Widget::Panel("/dialogs/align", SP_VERB_DIALOG_ALIGN_DISTRIBUTE), + randomize_bbox(), + _alignFrame(_("Align")), + _distributeFrame(_("Distribute")), + _rearrangeFrame(_("Rearrange")), + _removeOverlapFrame(_("Remove overlaps")), + _nodesFrame(_("Nodes")), + _alignTable(), + _distributeTable(), + _rearrangeTable(), + _removeOverlapTable(), + _nodesTable(), + _anchorLabel(_("Relative to: ")), + _anchorLabelNode(_("Relative to: ")) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + //Instantiate the align buttons + addAlignButton(INKSCAPE_ICON("align-horizontal-right-to-anchor"), + _("Align right edges of objects to the left edge of the anchor"), + 0, 0); + addAlignButton(INKSCAPE_ICON("align-horizontal-left"), + _("Align left edges"), + 0, 1); + addAlignButton(INKSCAPE_ICON("align-horizontal-center"), + _("Center on vertical axis"), + 0, 2); + addAlignButton(INKSCAPE_ICON("align-horizontal-right"), + _("Align right sides"), + 0, 3); + addAlignButton(INKSCAPE_ICON("align-horizontal-left-to-anchor"), + _("Align left edges of objects to the right edge of the anchor"), + 0, 4); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom-to-anchor"), + _("Align bottom edges of objects to the top edge of the anchor"), + 1, 0); + addAlignButton(INKSCAPE_ICON("align-vertical-top"), + _("Align top edges"), + 1, 1); + addAlignButton(INKSCAPE_ICON("align-vertical-center"), + _("Center on horizontal axis"), + 1, 2); + addAlignButton(INKSCAPE_ICON("align-vertical-bottom"), + _("Align bottom edges"), + 1, 3); + addAlignButton(INKSCAPE_ICON("align-vertical-top-to-anchor"), + _("Align top edges of objects to the bottom edge of the anchor"), + 1, 4); + + //Baseline aligns + addBaselineButton(INKSCAPE_ICON("align-horizontal-baseline"), + _("Align baseline anchors of texts horizontally"), + 0, 5, this->align_table(), Geom::X, false); + addBaselineButton(INKSCAPE_ICON("align-vertical-baseline"), + _("Align baselines of texts"), + 1, 5, this->align_table(), Geom::Y, false); + + //The distribute buttons + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-gaps"), + _("Make horizontal gaps between objects equal"), + 0, 4, true, Geom::X, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-left"), + _("Distribute left edges equidistantly"), + 0, 1, false, Geom::X, 1., 0.); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-center"), + _("Distribute centers equidistantly horizontally"), + 0, 2, false, Geom::X, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-horizontal-right"), + _("Distribute right edges equidistantly"), + 0, 3, false, Geom::X, 0., 1.); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-gaps"), + _("Make vertical gaps between objects equal"), + 1, 4, true, Geom::Y, .5, .5); + + addDistributeButton(INKSCAPE_ICON("distribute-vertical-top"), + _("Distribute top edges equidistantly"), + 1, 1, false, Geom::Y, 0, 1); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-center"), + _("Distribute centers equidistantly vertically"), + 1, 2, false, Geom::Y, .5, .5); + addDistributeButton(INKSCAPE_ICON("distribute-vertical-bottom"), + _("Distribute bottom edges equidistantly"), + 1, 3, false, Geom::Y, 1., 0.); + + //Baseline distribs + addBaselineButton(INKSCAPE_ICON("distribute-horizontal-baseline"), + _("Distribute baseline anchors of texts horizontally"), + 0, 5, this->distribute_table(), Geom::X, true); + addBaselineButton(INKSCAPE_ICON("distribute-vertical-baseline"), + _("Distribute baselines of texts vertically"), + 1, 5, this->distribute_table(), Geom::Y, true); + + // Rearrange + //Graph Layout + addGraphLayoutButton(INKSCAPE_ICON("distribute-graph"), + _("Nicely arrange selected connector network"), + 0, 0); + addExchangePositionsButton(INKSCAPE_ICON("exchange-positions"), + _("Exchange positions of selected objects - selection order"), + 0, 1); + addExchangePositionsByZOrderButton(INKSCAPE_ICON("exchange-positions-zorder"), + _("Exchange positions of selected objects - stacking order"), + 0, 2); + addExchangePositionsClockwiseButton(INKSCAPE_ICON("exchange-positions-clockwise"), + _("Exchange positions of selected objects - clockwise rotate"), + 0, 3); + + //Randomize & Unclump + addRandomizeButton(INKSCAPE_ICON("distribute-randomize"), + _("Randomize centers in both dimensions"), + 0, 4); + addUnclumpButton(INKSCAPE_ICON("distribute-unclump"), + _("Unclump objects: try to equalize edge-to-edge distances"), + 0, 5); + + //Remove overlaps + addRemoveOverlapsButton(INKSCAPE_ICON("distribute-remove-overlaps"), + _("Move objects as little as possible so that their bounding boxes do not overlap"), + 0, 0); + + //Node Mode buttons + // NOTE: "align nodes vertically" means "move nodes vertically until they align on a common + // _horizontal_ line". This is analogous to what the "align-vertical-center" icon means. + // There is no doubt some ambiguity. For this reason the descriptions are different. + addNodeButton(INKSCAPE_ICON("align-vertical-node"), + _("Align selected nodes to a common horizontal line"), + 0, Geom::X, false); + addNodeButton(INKSCAPE_ICON("align-horizontal-node"), + _("Align selected nodes to a common vertical line"), + 1, Geom::Y, false); + addNodeButton(INKSCAPE_ICON("distribute-horizontal-node"), + _("Distribute selected nodes horizontally"), + 2, Geom::X, true); + addNodeButton(INKSCAPE_ICON("distribute-vertical-node"), + _("Distribute selected nodes vertically"), + 3, Geom::Y, true); + + //Rest of the widgetry + + _combo.append(_("Last selected")); + _combo.append(_("First selected")); + _combo.append(_("Biggest object")); + _combo.append(_("Smallest object")); + _combo.append(_("Page")); + _combo.append(_("Drawing")); + _combo.append(_("Selection Area")); + _combo.set_active(prefs->getInt("/dialogs/align/align-to", 6)); + _combo.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_ref_change)); + + _comboNode.append(_("Last selected")); + _comboNode.append(_("First selected")); + _comboNode.append(_("Middle of selection")); + _comboNode.append(_("Min value")); + _comboNode.append(_("Max value")); + _comboNode.set_active(prefs->getInt("/dialogs/align/align-nodes-to", 2)); + _comboNode.signal_changed().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_node_ref_change)); + + Gtk::Image* selgrp_icon = Gtk::manage(new Gtk::Image()); + selgrp_icon = sp_get_icon_image("align-sel-as-group", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _selgrp.add(*selgrp_icon); + + _selgrp.set_active(prefs->getBool("/dialogs/align/sel-as-groups")); + _selgrp.set_relief(Gtk::RELIEF_NONE); + _selgrp.set_tooltip_text(_("Treat selection as group")); + _selgrp.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_selgrp_toggled)); + _anchorBox.pack_end(_selgrp, false, false); + _anchorBox.pack_end(_combo, false, false); + _anchorBox.pack_end(_anchorLabel, false, false); + + _anchorBoxNode.pack_end(_comboNode, false, false); + _anchorBoxNode.pack_end(_anchorLabelNode, false, false); + + Gtk::Image* oncanvas_icon = Gtk::manage(new Gtk::Image()); + oncanvas_icon = sp_get_icon_image("align-on-canvas", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _oncanvas.add(*oncanvas_icon); + + _oncanvas.set_relief(Gtk::RELIEF_NONE); + _oncanvas.set_tooltip_text(_("Enable on-canvas alignment handles.")); + _anchorBox.pack_start(_oncanvas, false, false); + _oncanvas.set_active(prefs->getBool("/dialogs/align/oncanvas")); + _oncanvas.signal_toggled().connect(sigc::mem_fun(*this, &AlignAndDistribute::on_oncanvas_toggled)); + + // Right align the buttons + _alignTableBox.pack_end(_alignTable, false, false); + _distributeTableBox.pack_end(_distributeTable, false, false); + _rearrangeTableBox.pack_end(_rearrangeTable, false, false); + _removeOverlapTableBox.pack_end(_removeOverlapTable, false, false); + _nodesTableBox.pack_end(_nodesTable, false, false); + + _alignBox.pack_start(_anchorBox); + _alignBox.pack_start(_selgrpBox); + _alignBox.pack_start(_alignTableBox); + + _alignBoxNode.pack_start(_anchorBoxNode, false, false); + _alignBoxNode.pack_start(_nodesTableBox); + + + _alignFrame.add(_alignBox); + _distributeFrame.add(_distributeTableBox); + _rearrangeFrame.add(_rearrangeTableBox); + _removeOverlapFrame.add(_removeOverlapTableBox); + _nodesFrame.add(_alignBoxNode); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(4); + + // Notebook for individual transformations + + contents->pack_start(_alignFrame, true, true); + contents->pack_start(_distributeFrame, true, true); + contents->pack_start(_rearrangeFrame, true, true); + contents->pack_start(_removeOverlapFrame, true, true); + contents->pack_start(_nodesFrame, true, false); + + //Connect to the global tool change signal + _toolChangeConn = INKSCAPE.signal_eventcontext_set.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_tool_changed), this))); + + // Connect to the global selection change, to invalidate cached randomize_bbox + _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::bind(sigc::ptr_fun(&on_selection_changed), this))); + randomize_bbox = Geom::OptRect(); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &AlignAndDistribute::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + on_tool_changed (this); // set current mode +} + +AlignAndDistribute::~AlignAndDistribute() +{ + for (auto & it : _actionList) { + delete it; + } + + _toolChangeConn.disconnect(); + _selChangeConn.disconnect(); + _desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +void AlignAndDistribute::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + on_tool_changed (this); + } +} + + +void AlignAndDistribute::on_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-to", _combo.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_node_ref_change(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/align-nodes-to", _comboNode.get_active_row_number()); + + //Make blink the master +} + +void AlignAndDistribute::on_selgrp_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/sel-as-groups", _selgrp.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::on_oncanvas_toggled(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/align/oncanvas", _oncanvas.get_active()); + + //Make blink the master +} + +void AlignAndDistribute::setMode(bool nodeEdit) +{ + //Act on widgets used in node mode + void ( Gtk::Widget::*mNode) () = nodeEdit ? + &Gtk::Widget::show_all : &Gtk::Widget::hide; + + //Act on widgets used in selection mode + void ( Gtk::Widget::*mSel) () = nodeEdit ? + &Gtk::Widget::hide : &Gtk::Widget::show_all; + + ((_alignFrame).*(mSel))(); + ((_distributeFrame).*(mSel))(); + ((_rearrangeFrame).*(mSel))(); + ((_removeOverlapFrame).*(mSel))(); + ((_nodesFrame).*(mNode))(); + _getContents()->queue_resize(); + +} +void AlignAndDistribute::addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionAlign( + id, tiptext, row, col, + *this , col + row * 5)); +} +void AlignAndDistribute::addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd) +{ + _actionList.push_back( + new ActionDistribute( + id, tiptext, row, col, *this , + onInterSpace, orientation, + kBegin, kEnd + ) + ); +} + +void AlignAndDistribute::addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionNode( + id, tiptext, col, + *this, orientation, distribute)); +} + +void AlignAndDistribute::addRemoveOverlapsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRemoveOverlaps( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addGraphLayoutButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionGraphLayout( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addExchangePositionsByZOrderButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::ZOrder) + ); +} + +void AlignAndDistribute::addExchangePositionsClockwiseButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionExchangePositions( + id, tiptext, row, col, *this, ActionExchangePositions::Clockwise) + ); +} + +void AlignAndDistribute::addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionUnclump( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col) +{ + _actionList.push_back( + new ActionRandomize( + id, tiptext, row, col, *this) + ); +} + +void AlignAndDistribute::addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute) +{ + _actionList.push_back( + new ActionBaseline( + id, tiptext, row, col, + *this, table, orientation, distribute)); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/align-and-distribute.h b/src/ui/dialog/align-and-distribute.h new file mode 100644 index 0000000..175ebdc --- /dev/null +++ b/src/ui/dialog/align-and-distribute.h @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Align and Distribute dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Aubanel MONNIER <aubi@libertysurf.fr> + * Frank Felfe <innerspace@iname.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H +#define INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +#include <list> +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include <gtkmm/frame.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/label.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/grid.h> + +#include "2geom/rect.h" +#include "ui/dialog/desktop-tracker.h" + +class SPItem; + +namespace Inkscape { +namespace UI { +namespace Tools{ +class NodeTool; +} +namespace Dialog { + +class Action; + + +class AlignAndDistribute : public Widget::Panel { +public: + AlignAndDistribute(); + ~AlignAndDistribute() override; + + static AlignAndDistribute &getInstance() { return *new AlignAndDistribute(); } + + Gtk::Grid &align_table(){return _alignTable;} + Gtk::Grid &distribute_table(){return _distributeTable;} + Gtk::Grid &rearrange_table(){return _rearrangeTable;} + Gtk::Grid &removeOverlap_table(){return _removeOverlapTable;} + Gtk::Grid &nodes_table(){return _nodesTable;} + + void setMode(bool nodeEdit); + + Geom::OptRect randomize_bbox; + +protected: + + void on_ref_change(); + void on_node_ref_change(); + void on_selgrp_toggled(); + void on_oncanvas_toggled(); + void addDistributeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, bool onInterSpace, + Geom::Dim2 orientation, float kBegin, float kEnd); + void addAlignButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addNodeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint col, Geom::Dim2 orientation, bool distribute); + void addRemoveOverlapsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addGraphLayoutButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsByZOrderButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addExchangePositionsClockwiseButton(const Glib::ustring &id, + const Glib::ustring tiptext, + guint row, guint col); + void addUnclumpButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addRandomizeButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col); + void addBaselineButton(const Glib::ustring &id, const Glib::ustring tiptext, + guint row, guint col, Gtk::Grid &table, Geom::Dim2 orientation, bool distribute); + void setTargetDesktop(SPDesktop *desktop); + + std::list<Action *> _actionList; + UI::Widget::Frame _alignFrame, _distributeFrame, _rearrangeFrame, _removeOverlapFrame, _nodesFrame; + Gtk::Grid _alignTable, _distributeTable, _rearrangeTable, _removeOverlapTable, _nodesTable; + Gtk::HBox _anchorBox; + Gtk::HBox _selgrpBox; + Gtk::VBox _alignBox; + Gtk::VBox _alignBoxNode; + Gtk::HBox _alignTableBox; + Gtk::HBox _distributeTableBox; + Gtk::HBox _rearrangeTableBox; + Gtk::HBox _removeOverlapTableBox; + Gtk::HBox _nodesTableBox; + Gtk::Label _anchorLabel; + Gtk::Label _anchorLabelNode; + Gtk::ToggleButton _selgrp; + Gtk::ToggleButton _oncanvas; + Gtk::ComboBoxText _combo; + Gtk::HBox _anchorBoxNode; + Gtk::ComboBoxText _comboNode; + + SPDesktop *_desktop; + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + sigc::connection _toolChangeConn; + sigc::connection _selChangeConn; +private: + AlignAndDistribute(AlignAndDistribute const &d) = delete; + AlignAndDistribute& operator=(AlignAndDistribute const &d) = delete; +}; + + +struct BBoxSort +{ + SPItem *item; + float anchor; + Geom::Rect bbox; + BBoxSort(SPItem *pItem, Geom::Rect const &bounds, Geom::Dim2 orientation, double kBegin, double kEnd); + BBoxSort(const BBoxSort &rhs); +}; +bool operator< (const BBoxSort &a, const BBoxSort &b); + + +class Action { +public : + + enum AlignTarget { LAST=0, FIRST, BIGGEST, SMALLEST, PAGE, DRAWING, SELECTION }; + enum AlignTargetNode { LAST_NODE=0, FIRST_NODE, MID_NODE, MIN_NODE, MAX_NODE }; + Action(Glib::ustring id, + const Glib::ustring &tiptext, + guint row, guint column, + Gtk::Grid &parent, + AlignAndDistribute &dialog); + + virtual ~Action()= default; + + AlignAndDistribute &_dialog; + +private : + virtual void on_button_click(){} + + Glib::ustring _id; + Gtk::Grid &_parent; +}; + + +class ActionAlign : public Action { +public : + struct Coeffs { + double mx0, mx1, my0, my1; + double sx0, sx1, sy0, sy1; + int verb_id; + }; + ActionAlign(const Glib::ustring &id, + const Glib::ustring &tiptext, + guint row, guint column, + AlignAndDistribute &dialog, + guint coeffIndex): + Action(id, tiptext, row, column, + dialog.align_table(), dialog), + _index(coeffIndex), + _dialog(dialog) + {} + + /* + * Static function called to align from a keyboard shortcut + */ + static void do_verb_action(SPDesktop *desktop, int verb); + static int verb_to_coeff(int verb); + +private : + + + void on_button_click() override { + //Retrieve selected objects + SPDesktop *desktop = _dialog.getDesktop(); + if (!desktop) return; + + do_action(desktop, _index); + } + + static void do_action(SPDesktop *desktop, int index); + static void do_node_action(Inkscape::UI::Tools::NodeTool *nt, int index); + + guint _index; + AlignAndDistribute &_dialog; + + static const Coeffs _allCoeffs[19]; + +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_ALIGN_AND_DISTRIBUTE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/arrange-tab.h b/src/ui/dialog/arrange-tab.h new file mode 100644 index 0000000..42e141e --- /dev/null +++ b/src/ui/dialog/arrange-tab.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arrange tools base class + */ +/* Authors: + * * Declara Denis + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_ARRANGE_TAB_H + +#include <gtkmm/box.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * This interface should be implemented by each arrange mode. + * The class is a Gtk::VBox and will be displayed as a tab in + * the dialog + */ +class ArrangeTab : public Gtk::VBox +{ +public: + ArrangeTab() = default;; + ~ArrangeTab() override = default;; + + /** + * Do the actual work! This method is invoked to actually arrange the + * selection + */ + virtual void arrange() = 0; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +#endif /* INKSCAPE_UI_DIALOG_ARRANGE_TAB_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/attrdialog.cpp b/src/ui/dialog/attrdialog.cpp new file mode 100644 index 0000000..175c2ad --- /dev/null +++ b/src/ui/dialog/attrdialog.cpp @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#include "attrdialog.h" + +#include "verbs.h" +#include "selection.h" +#include "document-undo.h" +#include "message-context.h" +#include "message-stack.h" +#include "style.h" +#include "ui/icon-loader.h" +#include "ui/widget/iconrenderer.h" + +#include "xml/node-event-vector.h" +#include "xml/attribute-record.h" + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +static void on_attr_changed (Inkscape::XML::Node * repr, + const gchar * name, + const gchar * /*old_value*/, + const gchar * new_value, + bool /*is_interactive*/, + gpointer data) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, name, new_value); +} + +static void on_content_changed (Inkscape::XML::Node * repr, + gchar const * oldcontent, + gchar const * newcontent, + gpointer data) +{ + ATTR_DIALOG(data)->onAttrChanged(repr, "content", repr->content()); +} + +Inkscape::XML::NodeEventVector _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_attr_changed, + on_content_changed, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog); +/** + * Constructor + * A treeview whose each row corresponds to an XML attribute of a selected node + * New attribute can be added by clicking '+' at bottom of the attr pane. '-' + */ +AttrDialog::AttrDialog() + : UI::Widget::Panel("/dialogs/attr", SP_VERB_DIALOG_ATTR) + , _desktop(nullptr) + , _repr(nullptr) +{ + set_size_request(20, 15); + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + _treeView.set_headers_visible(true); + _treeView.set_hover_selection(true); + _treeView.set_activate_on_single_click(true); + _treeView.set_can_focus(false); + _scrolledWindow.add(_treeView); + _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + _store = Gtk::ListStore::create(_attrColumns); + _treeView.set_model(_store); + + Inkscape::UI::Widget::IconRenderer * addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + + _treeView.append_column("", *addRenderer); + Gtk::TreeViewColumn *col = _treeView.get_column(0); + if (col) { + auto add_icon = Gtk::manage(sp_get_icon_image("list-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + col->set_clickable(true); + col->set_widget(*add_icon); + add_icon->set_tooltip_text(_("Add a new attribute")); + add_icon->show(); + auto button = add_icon->get_parent()->get_parent()->get_parent(); + // Assign the button event so that create happens BEFORE delete. If this code + // isn't in this exact way, the onAttrDelete is called when the header lines are pressed. + button->signal_button_release_event().connect(sigc::mem_fun(*this, &AttrDialog::onAttrCreate), false); + } + addRenderer->signal_activated().connect(sigc::mem_fun(*this, &AttrDialog::onAttrDelete)); + _treeView.signal_key_press_event().connect(sigc::mem_fun(*this, &AttrDialog::onKeyPressed)); + _treeView.set_search_column(-1); + + _nameRenderer = Gtk::manage(new Gtk::CellRendererText()); + _nameRenderer->property_editable() = true; + _nameRenderer->property_placeholder_text().set_value(_("Attribute Name")); + _nameRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::nameEdited)); + _nameRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startNameEdit)); + _treeView.append_column(_("Name"), *_nameRenderer); + _nameCol = _treeView.get_column(1); + if (_nameCol) { + _nameCol->set_resizable(true); + _nameCol->add_attribute(_nameRenderer->property_text(), _attrColumns._attributeName); + } + status.set_halign(Gtk::ALIGN_START); + status.set_valign(Gtk::ALIGN_CENTER); + status.set_size_request(1, -1); + status.set_markup(""); + status.set_line_wrap(true); + status.get_style_context()->add_class("inksmall"); + status_box.pack_start(status, TRUE, TRUE, 0); + _getContents()->pack_end(status_box, false, false, 2); + + _message_stack = std::make_shared<Inkscape::MessageStack>(); + _message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack)); + _message_changed_connection = + _message_stack->connectChanged(sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj()))); + + _valueRenderer = Gtk::manage(new Gtk::CellRendererText()); + _valueRenderer->property_editable() = true; + _valueRenderer->property_placeholder_text().set_value(_("Attribute Value")); + _valueRenderer->property_ellipsize().set_value(Pango::ELLIPSIZE_END); + _valueRenderer->signal_edited().connect(sigc::mem_fun(*this, &AttrDialog::valueEdited)); + _valueRenderer->signal_editing_started().connect(sigc::mem_fun(*this, &AttrDialog::startValueEdit)); + _treeView.append_column(_("Value"), *_valueRenderer); + _valueCol = _treeView.get_column(2); + if (_valueCol) { + _valueCol->add_attribute(_valueRenderer->property_text(), _attrColumns._attributeValueRender); + } + _popover = Gtk::manage(new Gtk::Popover()); + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + _textview = Gtk::manage(new Gtk::TextView()); + _textview->set_wrap_mode(Gtk::WrapMode::WRAP_CHAR); + _textview->set_editable(true); + _textview->set_monospace(true); + _textview->set_border_width(6); + _textview->signal_map().connect(sigc::mem_fun(*this, &AttrDialog::textViewMap)); + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.add(*_textview); + _scrolled_text_view.set_max_content_height(450); + _scrolled_text_view.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _scrolled_text_view.set_propagate_natural_width(true); + Gtk::Label *helpreturn = Gtk::manage(new Gtk::Label(_("Shift+Return new line"))); + helpreturn->get_style_context()->add_class("inksmall"); + Gtk::Button *apply = Gtk::manage(new Gtk::Button()); + Gtk::Image *icon = Gtk::manage(sp_get_icon_image("on-outline", 26)); + apply->set_relief(Gtk::RELIEF_NONE); + icon->show(); + apply->add(*icon); + apply->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueEditedPop)); + Gtk::Button *cancel = Gtk::manage(new Gtk::Button()); + icon = Gtk::manage(sp_get_icon_image("off-outline", 26)); + cancel->set_relief(Gtk::RELIEF_NONE); + icon->show(); + cancel->add(*icon); + cancel->signal_clicked().connect(sigc::mem_fun(*this, &AttrDialog::valueCanceledPop)); + hbox->pack_end(*apply, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*cancel, Gtk::PACK_SHRINK, 3); + hbox->pack_end(*helpreturn, Gtk::PACK_SHRINK, 3); + vbox->pack_start(_scrolled_text_view, Gtk::PACK_EXPAND_WIDGET, 3); + vbox->pack_start(*hbox, Gtk::PACK_EXPAND_WIDGET, 3); + _popover->add(*vbox); + _popover->show(); + _popover->set_relative_to(_treeView); + _popover->set_position(Gtk::PositionType::POS_BOTTOM); + _popover->signal_closed().connect(sigc::mem_fun(*this, &AttrDialog::popClosed)); + _popover->get_style_context()->add_class("attrpop"); + attr_reset_context(0); + _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + setDesktop(getDesktop()); + // I couldent get the signal go well not using C way signals + g_signal_connect(GTK_WIDGET(_popover->gobj()), "key-press-event", G_CALLBACK(key_callback), this); + _popover->hide(); + _updating = false; +} + +void AttrDialog::textViewMap() +{ + auto vscroll = _scrolled_text_view.get_vadjustment(); + int height = vscroll->get_upper() + 12; // padding 6+6 + if (height < 450) { + _scrolled_text_view.set_min_content_height(height); + vscroll->set_value(vscroll->get_lower()); + } else { + _scrolled_text_view.set_min_content_height(450); + } +} + +gboolean sp_show_pop_map(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + attrdialog->textViewMap(); + return FALSE; +} + +static gboolean key_callback(GtkWidget *widget, GdkEventKey *event, AttrDialog *attrdialog) +{ + switch (event->keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (attrdialog->_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + attrdialog->valueEditedPop(); + attrdialog->_popover->hide(); + return true; + } else { + g_timeout_add(50, &sp_show_pop_map, attrdialog); + } + } + } break; + } + return false; +} + +/** + * Prepare value string suitable for display in a Gtk::CellRendererText + * + * Value is truncated at the first new line character (if any) and a visual indicator and ellipsis is added. + * Overall length is limited as well to prevent performance degradation for very long values. + * + * @param value Raw attribute value as UTF-8 encoded string + * @return Single-line string with fixed maximum length + */ +static Glib::ustring prepare_rendervalue(const char *value) +{ + constexpr int MAX_LENGTH = 500; // maximum length of string before it's truncated for performance reasons + // ~400 characters fit horizontally on a WQHD display, so 500 should be plenty + + Glib::ustring renderval; + + // truncate to MAX_LENGTH + if (g_utf8_strlen(value, -1) > MAX_LENGTH) { + renderval = Glib::ustring(value, MAX_LENGTH) + "…"; + } else { + renderval = value; + } + + // truncate at first newline (if present) and add a visual indicator + auto ind = renderval.find('\n'); + if (ind != Glib::ustring::npos) { + renderval.replace(ind, Glib::ustring::npos, " ⎠…"); + } + + return renderval; +} + + +/** + * @brief AttrDialog::~AttrDialog + * Class destructor + */ +AttrDialog::~AttrDialog() +{ + setDesktop(nullptr); + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; + _message_changed_connection.~connection(); +} + +void AttrDialog::startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &AttrDialog::onNameKeyPressed), entry)); +} + + +gboolean sp_show_attr_pop(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + attrdialog->_popover->show_all(); + + return FALSE; +} + +gboolean sp_close_entry(gpointer data) +{ + Gtk::CellEditable *cell = reinterpret_cast<Gtk::CellEditable *>(data); + if (cell) { + cell->property_editing_canceled() = true; + cell->remove_widget(); + } + return FALSE; +} + +void AttrDialog::startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + int width = 0; + int height = 0; + int colwidth = _valueCol->get_width(); + _textview->set_size_request(510, -1); + _popover->set_size_request(520, -1); + valuepath = path; + entry->get_layout()->get_pixel_size(width, height); + Gtk::TreeIter iter = *_store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + if (row[_attrColumns._attributeValue] != row[_attrColumns._attributeValueRender] || colwidth - 10 < width || + name == "content") { + valueediting = entry->get_text(); + Gdk::Rectangle rect; + _treeView.get_cell_area((Gtk::TreeModel::Path)iter, *_valueCol, rect); + if (_popover->get_position() == Gtk::PositionType::POS_BOTTOM) { + rect.set_y(rect.get_y() + 20); + } + _popover->set_pointing_to(rect); + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(row[_attrColumns._attributeValue]); + _textview->set_buffer(textbuffer); + g_timeout_add(50, &sp_close_entry, cell); + g_timeout_add(50, &sp_show_attr_pop, this); + } else { + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &AttrDialog::onValueKeyPressed), entry)); + } + } +} + +void AttrDialog::popClosed() +{ + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(""); + _textview->set_buffer(textbuffer); + _scrolled_text_view.set_min_content_height(20); +} + +/** + * @brief AttrDialog::setDesktop + * @param desktop + * This function sets the 'desktop' for the CSS pane. + */ +void AttrDialog::setDesktop(SPDesktop* desktop) +{ + _desktop = desktop; +} + +/** + * @brief AttrDialog::setRepr + * Set the internal xml object that I'm working on right now. + */ +void AttrDialog::setRepr(Inkscape::XML::Node * repr) +{ + if ( repr == _repr ) return; + if (_repr) { + _store->clear(); + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + _repr = repr; + if (repr) { + Inkscape::GC::anchor(_repr); + _repr->addListener(&_repr_events, this); + _repr->synthesizeEvents(&_repr_events, this); + } +} + +void AttrDialog::setUndo(Glib::ustring const &event_description) +{ + SPDocument *document = this->_desktop->doc(); + DocumentUndo::done(document, SP_VERB_DIALOG_XML_EDITOR, event_description); +} + +void AttrDialog::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget) +{ + if (widget) { + gtk_label_set_markup(GTK_LABEL(widget), message ? message : ""); + } +} + +/** + * Sets the AttrDialog status bar, depending on which attr is selected. + */ +void AttrDialog::attr_reset_context(gint attr) +{ + if (attr == 0) { + _message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> attribute to edit.")); + } else { + const gchar *name = g_quark_to_string(attr); + _message_context->setF( + Inkscape::NORMAL_MESSAGE, + _("Attribute <b>%s</b> selected. Press <b>Ctrl+Enter</b> when done editing to commit changes."), name); + } +} + +/** + * @brief AttrDialog::onAttrChanged + * This is called when the XML has an updated attribute + */ +void AttrDialog::onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value) +{ + if (_updating) { + return; + } + Glib::ustring renderval; + if (new_value) { + renderval = prepare_rendervalue(new_value); + } + for(auto iter: this->_store->children()) + { + Gtk::TreeModel::Row row = *iter; + Glib::ustring col_name = row[_attrColumns._attributeName]; + if(name == col_name) { + if(new_value) { + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + new_value = nullptr; // Don't make a new one + } else { + _store->erase(iter); + } + break; + } + } + if (new_value) { + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_attrColumns._attributeName] = name; + row[_attrColumns._attributeValue] = new_value; + row[_attrColumns._attributeValueRender] = renderval; + } +} + +/** + * @brief AttrDialog::onAttrCreate + * This function is a slot to signal_clicked for '+' button panel. + */ +bool AttrDialog::onAttrCreate(GdkEventButton *event) +{ + if(event->type == GDK_BUTTON_RELEASE && event->button == 1 && this->_repr) { + Gtk::TreeIter iter = _store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + _treeView.set_cursor(path, *_nameCol, true); + grab_focus(); + return true; + } + return false; +} + +/** + * @brief AttrDialog::onAttrDelete + * @param event + * @return true + * Delete the attribute from the xml + */ +void AttrDialog::onAttrDelete(Glib::ustring path) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if (row) { + Glib::ustring name = row[_attrColumns._attributeName]; + if (name == "content") { + return; + } else { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + } +} + +/** + * @brief AttrDialog::onKeyPressed + * @param event + * @return true + * Delete or create elements based on key presses + */ +bool AttrDialog::onKeyPressed(GdkEventKey *event) +{ + bool ret = false; + if(this->_repr) { + auto selection = this->_treeView.get_selection(); + Gtk::TreeModel::Row row = *(selection->get_selected()); + Gtk::TreeIter iter = *(selection->get_selected()); + switch (event->keyval) + { + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: { + // Create new attribute (repeat code, fold into above event!) + Glib::ustring name = row[_attrColumns._attributeName]; + if (name != "content") { + this->_store->erase(row); + this->_repr->removeAttribute(name); + this->setUndo(_("Delete attribute")); + } + ret = true; + } break; + case GDK_KEY_plus: + case GDK_KEY_Insert: + { + // Create new attribute (repeat code, fold into above event!) + Gtk::TreeIter iter = this->_store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + this->_treeView.set_cursor(path, *this->_nameCol, true); + grab_focus(); + ret = true; + } break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (_popover->is_visible()) { + if (!(event->state & GDK_SHIFT_MASK)) { + valueEditedPop(); + _popover->hide(); + ret = true; + } + } + } break; + } + } + return ret; +} + +bool AttrDialog::onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + + +bool AttrDialog::onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyPressed"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +gboolean sp_attrdialog_store_move_to_next(gpointer data) +{ + AttrDialog *attrdialog = reinterpret_cast<AttrDialog *>(data); + auto selection = attrdialog->_treeView.get_selection(); + Gtk::TreeIter iter = *(selection->get_selected()); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + Gtk::TreeViewColumn *focus_column; + attrdialog->_treeView.get_cursor(path, focus_column); + if (path == attrdialog->_modelpath && focus_column == attrdialog->_treeView.get_column(1)) { + attrdialog->_treeView.set_cursor(attrdialog->_modelpath, *attrdialog->_valueCol, true); + } + return FALSE; +} + +/** + * + * + * @brief AttrDialog::nameEdited + * @param event + * @return + * Called when the name is edited in the TreeView editable column + */ +void AttrDialog::nameEdited (const Glib::ustring& path, const Glib::ustring& name) +{ + Gtk::TreeIter iter = *_store->get_iter(path); + _modelpath = (Gtk::TreeModel::Path)iter; + Gtk::TreeModel::Row row = *iter; + if(row && this->_repr) { + Glib::ustring old_name = row[_attrColumns._attributeName]; + if (old_name == name) { + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + grab_focus(); + return; + } + if (old_name == "content") { + return; + } + // Do not allow empty name (this would delete the attribute) + if (name.empty()) { + return; + } + // Do not allow duplicate names + const auto children = _store->children(); + for (const auto &child : children) { + if (name == child[_attrColumns._attributeName]) { + return; + } + } + if(std::any_of(name.begin(), name.end(), isspace)) { + return; + } + // Copy old value and remove old name + Glib::ustring value; + if (!old_name.empty()) { + value = row[_attrColumns._attributeValue]; + _updating = true; + _repr->removeAttribute(old_name); + _updating = false; + } + + // Do the actual renaming and set new value + row[_attrColumns._attributeName] = name; + grab_focus(); + _updating = true; + _repr->setAttributeOrRemoveIfEmpty(name, value); // use char * overload (allows empty attribute values) + _updating = false; + g_timeout_add(50, &sp_attrdialog_store_move_to_next, this); + this->setUndo(_("Rename attribute")); + } +} + +void AttrDialog::valueEditedPop() +{ + Glib::ustring value = _textview->get_buffer()->get_text(); + valueEdited(valuepath, value); + valueediting = ""; + _popover->hide(); +} + +void AttrDialog::valueCanceledPop() +{ + if (!valueediting.empty()) { + Glib::RefPtr<Gtk::TextBuffer> textbuffer = Gtk::TextBuffer::create(); + textbuffer->set_text(valueediting); + _textview->set_buffer(textbuffer); + } + _popover->hide(); +} + +/** + * @brief AttrDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void AttrDialog::valueEdited (const Glib::ustring& path, const Glib::ustring& value) +{ + Gtk::TreeModel::Row row = *_store->get_iter(path); + if(row && this->_repr) { + Glib::ustring name = row[_attrColumns._attributeName]; + Glib::ustring old_value = row[_attrColumns._attributeValue]; + if (old_value == value) { + return; + } + if(name.empty()) return; + if (name == "content") { + _repr->setContent(value.c_str()); + } else { + _repr->setAttributeOrRemoveIfEmpty(name, value); + } + if(!value.empty()) { + row[_attrColumns._attributeValue] = value; + Glib::ustring renderval = prepare_rendervalue(value.c_str()); + row[_attrColumns._attributeValueRender] = renderval; + } + Inkscape::Selection *selection = _desktop->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + this->setUndo(_("Change attribute value")); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/dialog/attrdialog.h b/src/ui/dialog/attrdialog.h new file mode 100644 index 0000000..23437f0 --- /dev/null +++ b/src/ui/dialog/attrdialog.h @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for XML attributes based on Gtk TreeView + */ +/* Authors: + * Martin Owens + * + * Copyright (C) Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPLv2 or later, read the file 'COPYING' for more information + */ + +#ifndef SEEN_UI_DIALOGS_ATTRDIALOG_H +#define SEEN_UI_DIALOGS_ATTRDIALOG_H + +#include "desktop.h" +#include "message.h" +#include <gtkmm/dialog.h> +#include <gtkmm/liststore.h> +#include <gtkmm/popover.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> +#include <gtkmm/treeview.h> +#include <ui/widget/panel.h> + +#define ATTR_DIALOG(obj) (dynamic_cast<Inkscape::UI::Dialog::AttrDialog*>((Inkscape::UI::Dialog::AttrDialog*)obj)) + +namespace Inkscape { +class MessageStack; +class MessageContext; +namespace UI { +namespace Dialog { + +/** + * @brief The AttrDialog class + * This dialog allows to add, delete and modify XML attributes created in the + * xml editor. + */ +class AttrDialog : public UI::Widget::Panel +{ +public: + AttrDialog(); + ~AttrDialog() override; + + static AttrDialog &getInstance() { return *new AttrDialog(); } + + // Data structure + class AttrColumns : public Gtk::TreeModel::ColumnRecord { + public: + AttrColumns() { + add(_attributeName); + add(_attributeValue); + add(_attributeValueRender); + } + Gtk::TreeModelColumn<Glib::ustring> _attributeName; + Gtk::TreeModelColumn<Glib::ustring> _attributeValue; + Gtk::TreeModelColumn<Glib::ustring> _attributeValueRender; + }; + AttrColumns _attrColumns; + + // TreeView + Gtk::TreeView _treeView; + Glib::RefPtr<Gtk::ListStore> _store; + Gtk::CellRendererText *_nameRenderer; + Gtk::CellRendererText *_valueRenderer; + Gtk::TreeViewColumn *_nameCol; + Gtk::TreeViewColumn *_valueCol; + Gtk::TreeModel::Path _modelpath; + Gtk::Popover *_popover; + Gtk::TextView *_textview; + Glib::ustring valuepath; + Glib::ustring valueediting; + + /** + * Status bar + */ + std::shared_ptr<Inkscape::MessageStack> _message_stack; + std::unique_ptr<Inkscape::MessageContext> _message_context; + + // Widgets + Gtk::VBox _mainBox; + Gtk::ScrolledWindow _scrolledWindow; + Gtk::ScrolledWindow _scrolled_text_view; + Gtk::HBox _buttonBox; + Gtk::Button _buttonAddAttribute; + // Variables - Inkscape + SPDesktop* _desktop; + Inkscape::XML::Node* _repr; + Gtk::HBox status_box; + Gtk::Label status; + bool _updating; + + // Helper functions + void setDesktop(SPDesktop* desktop) override; + void setRepr(Inkscape::XML::Node * repr); + void setUndo(Glib::ustring const &event_description); + /** + * Sets the XML status bar, depending on which attr is selected. + */ + void attr_reset_context(gint attr); + static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog); + + /** + * Signal handlers + */ + sigc::connection _message_changed_connection; + void onAttrChanged(Inkscape::XML::Node *repr, const gchar * name, const gchar * new_value); + bool onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + bool onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + void onAttrDelete(Glib::ustring path); + bool onAttrCreate(GdkEventButton *event); + bool onKeyPressed(GdkEventKey *event); + void popClosed(); + void startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + void nameEdited(const Glib::ustring &path, const Glib::ustring &name); + void valueEdited(const Glib::ustring &path, const Glib::ustring &value); + void textViewMap(); + void valueCanceledPop(); + void valueEditedPop(); +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // ATTRDIALOG_H diff --git a/src/ui/dialog/behavior.h b/src/ui/dialog/behavior.h new file mode 100644 index 0000000..1b6c7c7 --- /dev/null +++ b/src/ui/dialog/behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog behavior interface + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_BEHAVIOR_H + +#include <gtkmm/button.h> +#include <gtkmm/box.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Dialog; + +namespace Behavior { + +class Behavior; + +typedef Behavior *(*BehaviorFactory)(Dialog &dialog); + +template <typename T> +Behavior *create(Dialog &dialog) +{ + return T::create(dialog); +} + + +class Behavior { + +public: + virtual ~Behavior() = default; + + /** Gtk::Dialog methods */ + virtual operator Gtk::Widget&() =0; + virtual GtkWidget *gobj() =0; + virtual void present() =0; + virtual Gtk::Box *get_vbox() =0; + virtual void show() =0; + virtual void hide() =0; + virtual void show_all_children() =0; + virtual void resize(int width, int height) =0; + virtual void move(int x, int y) =0; + virtual void set_position(Gtk::WindowPosition) =0; + virtual void set_size_request(int width, int height) =0; + virtual void size_request(Gtk::Requisition &requisition) =0; + virtual void get_position(int &x, int &y) =0; + virtual void get_size(int &width, int &height) =0; + virtual void set_title(Glib::ustring title) =0; + virtual void set_sensitive(bool sensitive) =0; + + /** Gtk::Dialog signal proxies */ + virtual Glib::SignalProxy0<void> signal_show() =0; + virtual Glib::SignalProxy0<void> signal_hide() =0; + virtual Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() =0; + + /** Custom signal handlers */ + virtual void onHideF12() =0; + virtual void onShowF12() =0; + virtual void onShutdown() =0; + virtual void onDesktopActivated(SPDesktop *desktop) =0; + +protected: + Behavior(Dialog &dialog) + : _dialog (dialog) + { } + + Dialog& _dialog; //< reference to the owner + +private: + Behavior() = delete; // no constructor without params + Behavior(const Behavior &) = delete; // no copy + Behavior &operator=(const Behavior &) = delete; // no assign +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif //INKSCAPE_UI_DIALOG_BEHAVIOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/calligraphic-profile-rename.cpp b/src/ui/dialog/calligraphic-profile-rename.cpp new file mode 100644 index 0000000..604ac7f --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for naming calligraphic profiles. + * + * @note This file is in the wrong directory because of link order issues - + * it is required by widgets/toolbox.cpp, and libspwidgets.a comes after + * libinkdialogs.a in the current link order. + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "calligraphic-profile-rename.h" +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> + +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +CalligraphicProfileRename::CalligraphicProfileRename() : + _layout_table(Gtk::manage(new Gtk::Grid())), + _applied(false) +{ + set_title(_("Edit profile")); + + auto mainVBox = get_content_area(); + _layout_table->set_column_spacing(4); + _layout_table->set_row_spacing(4); + + _profile_name_entry.set_activates_default(true); + + _profile_name_label.set_label(_("Profile name:")); + _profile_name_label.set_halign(Gtk::ALIGN_END); + _profile_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table->attach(_profile_name_label, 0, 0, 1, 1); + + _profile_name_entry.set_hexpand(); + _layout_table->attach(_profile_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(*_layout_table, false, false, 4); + // Buttons + _close_button.set_use_underline(); + _close_button.set_label(_("_Cancel")); + _close_button.set_can_default(); + + _delete_button.set_use_underline(true); + _delete_button.set_label(_("_Delete")); + _delete_button.set_can_default(); + _delete_button.set_visible(false); + + _apply_button.set_use_underline(true); + _apply_button.set_label(_("_Save")); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)); + _delete_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_delete)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &CalligraphicProfileRename::_apply)); + + signal_delete_event().connect( sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &CalligraphicProfileRename::_close)), true ) ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_delete_button, Gtk::RESPONSE_DELETE_EVENT); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +void CalligraphicProfileRename::_apply() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = false; + _close(); +} + +void CalligraphicProfileRename::_delete() +{ + _profile_name = _profile_name_entry.get_text(); + _applied = true; + _deleted = true; + _close(); +} + +void CalligraphicProfileRename::_close() +{ + this->Gtk::Dialog::hide(); +} + +void CalligraphicProfileRename::show(SPDesktop *desktop, const Glib::ustring profile_name) +{ + CalligraphicProfileRename &dial = instance(); + dial._applied=false; + dial._deleted=false; + dial.set_modal(true); + + dial._profile_name = profile_name; + dial._profile_name_entry.set_text(profile_name); + + if (profile_name.empty()) { + dial.set_title(_("Add profile")); + dial._delete_button.set_visible(false); + + } else { + dial.set_title(_("Edit profile")); + dial._delete_button.set_visible(true); + } + + desktop->setWindowTransient (dial.gobj()); + dial.property_destroy_with_parent() = true; + // dial.Gtk::Dialog::show(); + //dial.present(); + dial.run(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/calligraphic-profile-rename.h b/src/ui/dialog/calligraphic-profile-rename.h new file mode 100644 index 0000000..195275b --- /dev/null +++ b/src/ui/dialog/calligraphic-profile-rename.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for naming calligraphic profiles + */ +/* Author: + * Aubanel MONNIER + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H +#define INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H + +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> + +namespace Gtk { +class Grid; +} + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class CalligraphicProfileRename : public Gtk::Dialog { +public: + CalligraphicProfileRename(); + ~CalligraphicProfileRename() override = default; + Glib::ustring getName() const { + return "CalligraphicProfileRename"; + } + + static void show(SPDesktop *desktop, const Glib::ustring profile_name); + static bool applied() { + return instance()._applied; + } + static bool deleted() { + return instance()._deleted; + } + static Glib::ustring getProfileName() { + return instance()._profile_name; + } + +protected: + void _close(); + void _apply(); + void _delete(); + + Gtk::Label _profile_name_label; + Gtk::Entry _profile_name_entry; + Gtk::Grid* _layout_table; + + Gtk::Button _close_button; + Gtk::Button _delete_button; + Gtk::Button _apply_button; + Glib::ustring _profile_name; + bool _applied; + bool _deleted; +private: + static CalligraphicProfileRename &instance() { + static CalligraphicProfileRename instance_; + return instance_; + } + CalligraphicProfileRename(CalligraphicProfileRename const &) = delete; // no copy + CalligraphicProfileRename &operator=(CalligraphicProfileRename const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_CALLIGRAPHIC_PROFILE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/clonetiler.cpp b/src/ui/dialog/clonetiler.cpp new file mode 100644 index 0000000..edc88ba --- /dev/null +++ b/src/ui/dialog/clonetiler.cpp @@ -0,0 +1,2834 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Clone tiling dialog + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Romain de Bossoreille + * + * Copyright (C) 2004-2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "clonetiler.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/radiobutton.h> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "message-stack.h" +#include "unclump.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "ui/icon-loader.h" + +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-use.h" + +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/unit-menu.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SB_MARGIN 1 +#define VB_MARGIN 4 + +static Glib::ustring const prefs_path = "/dialogs/clonetiler/"; + +static Inkscape::Drawing *trace_drawing = nullptr; +static unsigned trace_visionkey; +static gdouble trace_zoom; +static SPDocument *trace_doc = nullptr; + +CloneTiler::CloneTiler () : + UI::Widget::Panel("/dialogs/clonetiler/", SP_VERB_DIALOG_CLONETILER), + desktop(nullptr), + deskTrack(), + table_row_labels(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + + { + auto prefs = Inkscape::Preferences::get(); + + auto mainbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(mainbox), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (mainbox), 6); + + contents->pack_start (*Gtk::manage(Glib::wrap(mainbox)), true, true, 0); + + nb = gtk_notebook_new (); + gtk_box_pack_start (GTK_BOX (mainbox), nb, FALSE, FALSE, 0); + + + // Symmetry + { + GtkWidget *vb = new_tab (nb, _("_Symmetry")); + + /* TRANSLATORS: For the following 17 symmetry groups, see + * http://www.bib.ulb.ac.be/coursmath/doc/17.htm (visual examples); + * http://www.clarku.edu/~djoyce/wallpaper/seventeen.html (English vocabulary); or + * http://membres.lycos.fr/villemingerard/Geometri/Sym1D.htm (French vocabulary). + */ + struct SymGroups { + gint group; + Glib::ustring label; + } const sym_groups[] = { + // TRANSLATORS: "translation" means "shift" / "displacement" here. + {TILE_P1, _("<b>P1</b>: simple translation")}, + {TILE_P2, _("<b>P2</b>: 180° rotation")}, + {TILE_PM, _("<b>PM</b>: reflection")}, + // TRANSLATORS: "glide reflection" is a reflection and a translation combined. + // For more info, see http://mathforum.org/sum95/suzanne/symsusan.html + {TILE_PG, _("<b>PG</b>: glide reflection")}, + {TILE_CM, _("<b>CM</b>: reflection + glide reflection")}, + {TILE_PMM, _("<b>PMM</b>: reflection + reflection")}, + {TILE_PMG, _("<b>PMG</b>: reflection + 180° rotation")}, + {TILE_PGG, _("<b>PGG</b>: glide reflection + 180° rotation")}, + {TILE_CMM, _("<b>CMM</b>: reflection + reflection + 180° rotation")}, + {TILE_P4, _("<b>P4</b>: 90° rotation")}, + {TILE_P4M, _("<b>P4M</b>: 90° rotation + 45° reflection")}, + {TILE_P4G, _("<b>P4G</b>: 90° rotation + 90° reflection")}, + {TILE_P3, _("<b>P3</b>: 120° rotation")}, + {TILE_P31M, _("<b>P31M</b>: reflection + 120° rotation, dense")}, + {TILE_P3M1, _("<b>P3M1</b>: reflection + 120° rotation, sparse")}, + {TILE_P6, _("<b>P6</b>: 60° rotation")}, + {TILE_P6M, _("<b>P6M</b>: reflection + 60° rotation")}, + }; + + gint current = prefs->getInt(prefs_path + "symmetrygroup", 0); + + // Add a new combo box widget with the list of symmetry groups to the vbox + auto combo = Gtk::manage(new Gtk::ComboBoxText()); + combo->set_tooltip_text(_("Select one of the 17 symmetry groups for the tiling")); + + // Hack to add markup support + auto cell_list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo->gobj())); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo->gobj()), + GTK_CELL_RENDERER(cell_list->data), + "markup", 0, NULL); + + for (const auto & sg : sym_groups) { + // Add the description of the symgroup to a new row + combo->append(sg.label); + } + + gtk_box_pack_start (GTK_BOX (vb), GTK_WIDGET(combo->gobj()), FALSE, FALSE, SB_MARGIN); + + combo->set_active(current); + combo->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::symgroup_changed), combo)); + } + + table_row_labels = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + // Shift + { + GtkWidget *vb = new_tab (nb, _("S_hift")); + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) horizontally by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("<b>Shift X:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per row (in % of tile width)"), "shiftx_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal shift per column (in % of tile width)"), "shiftx_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal shift by this percentage"), "shiftx_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "shift" means: the tiles will be shifted (offset) vertically by this amount + // xgettext:no-c-format + gtk_label_set_markup (GTK_LABEL(l), _("<b>Shift Y:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per row (in % of tile height)"), "shifty_per_j", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical shift per column (in % of tile height)"), "shifty_per_i", + -10000, 10000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox ( + _("Randomize the vertical shift by this percentage"), "shifty_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Exponent:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox ( + _("Whether rows are spaced evenly (1), converge (<1) or diverge (>1)"), "shifty_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox ( + _("Whether columns are spaced evenly (1), converge (<1) or diverge (>1)"), "shiftx_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each row"), "shifty_alternate"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of shifts for each column"), "shiftx_alternate"); + table_attach (table, l, 0, 5, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Cumulate the shifts for each row"), "shifty_cumulate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Cumulate the shifts for each column"), "shiftx_cumulate"); + table_attach (table, l, 0, 6, 3); + } + + { // Exclude tile width and height in shift + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Exclude tile:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Exclude tile height in shift"), "shifty_excludeh"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Exclude tile width in shift"), "shiftx_excludew"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Scale + { + GtkWidget *vb = new_tab (nb, _("Sc_ale")); + + GtkWidget *table = table_x_y_rand (2); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // X + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Scale X:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per row (in % of tile width)"), "scalex_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Horizontal scale per column (in % of tile width)"), "scalex_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the horizontal scale by this percentage"), "scalex_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 2, 4); + } + + // Y + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Scale Y:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per row (in % of tile height)"), "scaley_per_j", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Vertical scale per column (in % of tile height)"), "scaley_per_i", + -100, 1000, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the vertical scale by this percentage"), "scaley_rand", + 0, 1000, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Exponent + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Exponent:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Whether row scaling is uniform (1), converge (<1) or diverge (>1)"), "scaley_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Whether column scaling is uniform (1), converge (<1) or diverge (>1)"), "scalex_exp", + 0, 10, "", true); + table_attach (table, l, 0, 4, 3); + } + + // Logarithmic (as in logarithmic spiral) + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Base:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scaley_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = spinbox (_("Base for a logarithmic spiral: not used (0), converge (<1), or diverge (>1)"), "scalex_log", + 0, 10, "", false); + table_attach (table, l, 0, 5, 3); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 6, 1); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each row"), "scaley_alternate"); + table_attach (table, l, 0, 6, 2); + } + + { + auto l = checkbox (_("Alternate the sign of scales for each column"), "scalex_alternate"); + table_attach (table, l, 0, 6, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 7, 1); + } + + { + auto l = checkbox (_("Cumulate the scales for each row"), "scaley_cumulate"); + table_attach (table, l, 0, 7, 2); + } + + { + auto l = checkbox (_("Cumulate the scales for each column"), "scalex_cumulate"); + table_attach (table, l, 0, 7, 3); + } + + } + + + // Rotation + { + GtkWidget *vb = new_tab (nb, _("_Rotation")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Angle + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Angle:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each row"), "rotate_per_j", + -180, 180, "°"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox ( + // xgettext:no-c-format + _("Rotate tiles by this angle for each column"), "rotate_per_i", + -180, 180, "°"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the rotation angle by this percentage"), "rotate_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each row"), "rotate_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the rotation direction for each column"), "rotate_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + { // Cumulate + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Cumulate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Cumulate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = checkbox (_("Cumulate the rotation for each row"), "rotate_cumulatej"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = checkbox (_("Cumulate the rotation for each column"), "rotate_cumulatei"); + table_attach (table, l, 0, 4, 3); + } + + } + + + // Blur and opacity + { + GtkWidget *vb = new_tab (nb, _("_Blur & opacity")); + + GtkWidget *table = table_x_y_rand (1); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + + // Blur + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Blur:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each row"), "blur_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Blur tiles by this percentage for each column"), "blur_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile blur by this percentage"), "blur_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each row"), "blur_alternatej"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = checkbox (_("Alternate the sign of blur change for each column"), "blur_alternatei"); + table_attach (table, l, 0, 3, 3); + } + + + + // Dissolve + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>Opacity:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each row"), "opacity_per_j", + 0, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Decrease tile opacity by this percentage for each column"), "opacity_per_i", + 0, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the tile opacity by this percentage"), "opacity_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + { // alternates + GtkWidget *l = gtk_label_new (""); + // TRANSLATORS: "Alternate" is a verb here + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each row"), "opacity_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of opacity change for each column"), "opacity_alternatei"); + table_attach (table, l, 0, 5, 3); + } + } + + + // Color + { + GtkWidget *vb = new_tab (nb, _("Co_lor")); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *l = gtk_label_new (_("Initial color: ")); + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + + guint32 rgba = 0x000000ff | sp_svg_read_color (prefs->getString(prefs_path + "initial_color").data(), 0x000000ff); + color_picker = new Inkscape::UI::Widget::ColorPicker (*new Glib::ustring(_("Initial color of tiled clones")), *new Glib::ustring(_("Initial color for clones (works only if the original has unset fill or stroke or on spray tool in copy mode)")), rgba, false); + color_changed_connection = color_picker->connectChanged(sigc::mem_fun(*this, &CloneTiler::on_picker_color_changed)); + + gtk_box_pack_start (GTK_BOX (hb), reinterpret_cast<GtkWidget*>(color_picker->gobj()), FALSE, FALSE, 0); + + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + } + + + GtkWidget *table = table_x_y_rand (3); + gtk_box_pack_start (GTK_BOX (vb), table, FALSE, FALSE, 0); + + // Hue + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>H:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 2, 1); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each row"), "hue_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 2, 2); + } + + { + auto l = spinbox (_("Change the tile hue by this percentage for each column"), "hue_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 2, 3); + } + + { + auto l = spinbox (_("Randomize the tile hue by this percentage"), "hue_rand", + 0, 100, "%"); + table_attach (table, l, 0, 2, 4); + } + + + // Saturation + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>S:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 3, 1); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each row"), "saturation_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 3, 2); + } + + { + auto l = spinbox (_("Change the color saturation by this percentage for each column"), "saturation_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 3, 3); + } + + { + auto l = spinbox (_("Randomize the color saturation by this percentage"), "saturation_rand", + 0, 100, "%"); + table_attach (table, l, 0, 3, 4); + } + + // Lightness + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<b>L:</b>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 4, 1); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each row"), "lightness_per_j", + -100, 100, "%"); + table_attach (table, l, 0, 4, 2); + } + + { + auto l = spinbox (_("Change the color lightness by this percentage for each column"), "lightness_per_i", + -100, 100, "%"); + table_attach (table, l, 0, 4, 3); + } + + { + auto l = spinbox (_("Randomize the color lightness by this percentage"), "lightness_rand", + 0, 100, "%"); + table_attach (table, l, 0, 4, 4); + } + + + { // alternates + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<small>Alternate:</small>")); + gtk_label_set_xalign(GTK_LABEL(l), 0.0); + gtk_size_group_add_widget(table_row_labels, l); + table_attach (table, l, 1, 5, 1); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each row"), "color_alternatej"); + table_attach (table, l, 0, 5, 2); + } + + { + auto l = checkbox (_("Alternate the sign of color changes for each column"), "color_alternatei"); + table_attach (table, l, 0, 5, 3); + } + + } + + // Trace + { + GtkWidget *vb = new_tab (nb, _("_Trace")); + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_container_set_border_width(GTK_CONTAINER(hb), 4); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), hb, FALSE, FALSE, 0); + + _b = Gtk::manage(new Gtk::CheckButton(_("Trace the drawing under the clones/sprayed items"))); + _b->set_data("uncheckable", GINT_TO_POINTER(TRUE)); + bool old = prefs->getBool(prefs_path + "dotrace"); + _b->set_active(old); + _b->set_tooltip_text(_("For each clone/sprayed item, pick a value from the drawing in its location and apply it")); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(_b->gobj()), FALSE, FALSE, 0); + _b->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::do_pick_toggled)); + } + + { + auto vvb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_set_homogeneous(GTK_BOX(vvb), FALSE); + gtk_box_pack_start (GTK_BOX (vb), vvb, FALSE, FALSE, 0); + _dotrace = vvb; + + { + GtkWidget *frame = gtk_frame_new (_("1. Pick from the drawing:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Color"))); + radio->set_tooltip_text(_("Pick the visible color and opacity")); + table_attach(table, radio, 0.0, 1, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_COLOR)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_COLOR); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Opacity"))); + radio->set_tooltip_text(_("Pick the total accumulated opacity")); + table_attach (table, radio, 0.0, 2, 1); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_OPACITY)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_OPACITY); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("R"))); + radio->set_tooltip_text(_("Pick the Red component of the color")); + table_attach (table, radio, 0.0, 1, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_R)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_R); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("G"))); + radio->set_tooltip_text(_("Pick the Green component of the color")); + table_attach (table, radio, 0.0, 2, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_G)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_G); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("B"))); + radio->set_tooltip_text(_("Pick the Blue component of the color")); + table_attach (table, radio, 0.0, 3, 2); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_B)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_B); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color hue", "H"))); + radio->set_tooltip_text(_("Pick the hue of the color")); + table_attach (table, radio, 0.0, 1, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_H)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_H); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color saturation", "S"))); + radio->set_tooltip_text(_("Pick the saturation of the color")); + table_attach (table, radio, 0.0, 2, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_S)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_S); + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, C_("Clonetiler color lightness", "L"))); + radio->set_tooltip_text(_("Pick the lightness of the color")); + table_attach (table, radio, 0.0, 3, 3); + radio->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_switched), PICK_L)); + radio->set_active(prefs->getInt(prefs_path + "pick", 0) == PICK_L); + } + + } + + { + GtkWidget *frame = gtk_frame_new (_("2. Tweak the picked value:")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, VB_MARGIN); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Gamma-correct:")); + table_attach (table, l, 1.0, 1, 1); + } + { + auto l = spinbox (_("Shift the mid-range of the picked value upwards (>0) or downwards (<0)"), "gamma_picked", + -10, 10, ""); + table_attach (table, l, 0.0, 1, 2); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Randomize:")); + table_attach (table, l, 1.0, 1, 3); + } + { + auto l = spinbox (_("Randomize the picked value by this percentage"), "rand_picked", + 0, 100, "%"); + table_attach (table, l, 0.0, 1, 4); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("Invert:")); + table_attach (table, l, 1.0, 2, 1); + } + { + auto l = checkbox (_("Invert the picked value"), "invert_picked"); + table_attach (table, l, 0.0, 2, 2); + } + } + + { + GtkWidget *frame = gtk_frame_new (_("3. Apply the value to the clones':")); + gtk_box_pack_start (GTK_BOX (vvb), frame, FALSE, FALSE, 0); + + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + gtk_container_set_border_width(GTK_CONTAINER(table), 4); + gtk_container_add(GTK_CONTAINER(frame), table); + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Presence"))); + bool old = prefs->getBool(prefs_path + "pick_to_presence", true); + b->set_active(old); + b->set_tooltip_text(_("Each clone is created with the probability determined by the picked value in that point")); + table_attach (table, b, 0.0, 1, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_presence")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Size"))); + bool old = prefs->getBool(prefs_path + "pick_to_size"); + b->set_active(old); + b->set_tooltip_text(_("Each clone's size is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 1); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_size")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Color"))); + bool old = prefs->getBool(prefs_path + "pick_to_color", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone is painted by the picked color (the original must have unset fill or stroke)")); + table_attach (table, b, 0.0, 1, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_color")); + } + + { + auto b = Gtk::manage(new Gtk::CheckButton(_("Opacity"))); + bool old = prefs->getBool(prefs_path + "pick_to_opacity", false); + b->set_active(old); + b->set_tooltip_text(_("Each clone's opacity is determined by the picked value in that point")); + table_attach (table, b, 0.0, 2, 2); + b->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::pick_to), b, "pick_to_opacity")); + } + } + gtk_widget_set_sensitive (vvb, prefs->getBool(prefs_path + "dotrace")); + } + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup (GTK_LABEL(l), _("Apply to tiled clones:")); + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + // Rows/columns, width/height + { + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 4); + gtk_grid_set_column_spacing(GTK_GRID(table), 6); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + gtk_box_pack_start (GTK_BOX (mainbox), table, FALSE, FALSE, 0); + + { + _rowscols = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "jmax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many rows in the tiling")); + sb->set_width_chars (7); + _rowscols->pack_start(*sb, true, true, 0); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "jmax")); + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + _rowscols->pack_start(*l, true, true, 0); + } + + { + auto a = Gtk::Adjustment::create(0.0, 1, 500, 1, 10, 0); + int value = prefs->getInt(prefs_path + "imax", 2); + a->set_value (value); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, 1.0, 0); + sb->set_tooltip_text (_("How many columns in the tiling")); + sb->set_width_chars (7); + _rowscols->pack_start(*sb, true, true, 0); + + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::xy_changed), a, "imax")); + } + + table_attach (table, GTK_WIDGET(_rowscols->gobj()), 0.0, 1, 2); + } + + { + _widthheight = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + + // unitmenu + unit_menu = new Inkscape::UI::Widget::UnitMenu(); + unit_menu->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + unit_menu->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); + unitChangedConn = unit_menu->signal_changed().connect(sigc::mem_fun(*this, &CloneTiler::unit_changed)); + + { + // Width spinbutton + fill_width = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillwidth", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_width->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_width, 1.0, 2); + e->set_tooltip_text (_("Width of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + _widthheight->pack_start(*e, true, true, 0); + fill_width->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_width_changed)); + } + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup("×"); + _widthheight->pack_start(*l, true, true, 0); + } + + { + // Height spinbutton + fill_height = Gtk::Adjustment::create(0.0, -1e6, 1e6, 1.0, 10.0, 0); + + double value = prefs->getDouble(prefs_path + "fillheight", 50.0); + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + gdouble const units = Inkscape::Util::Quantity::convert(value, "px", unit); + fill_height->set_value (units); + + auto e = new Inkscape::UI::Widget::SpinButton(fill_height, 1.0, 2); + e->set_tooltip_text (_("Height of the rectangle to be filled")); + e->set_width_chars (7); + e->set_digits (4); + _widthheight->pack_start(*e, true, true, 0); + fill_height->signal_value_changed().connect(sigc::mem_fun(*this, &CloneTiler::fill_height_changed)); + } + + _widthheight->pack_start(*unit_menu, true, true, 0); + table_attach (table, GTK_WIDGET(_widthheight->gobj()), 0.0, 2, 2); + + } + + // Switch + Gtk::RadioButtonGroup rb_group; + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Rows, columns: "))); + radio->set_tooltip_text(_("Create the specified number of rows and columns")); + table_attach (table, GTK_WIDGET(radio->gobj()), 0.0, 1, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_create)); + + if (!prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + { + auto radio = Gtk::manage(new Gtk::RadioButton(rb_group, _("Width, height: "))); + radio->set_tooltip_text(_("Fill the specified width and height with the tiling")); + table_attach (table, GTK_WIDGET(radio->gobj()), 0.0, 2, 1); + radio->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::switch_to_fill)); + + if (prefs->getBool(prefs_path + "fillrect")) { + radio->set_active(true); + } + } + } + + + // Use saved pos + { + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + gtk_box_pack_start (GTK_BOX (mainbox), GTK_WIDGET(hb->gobj()), FALSE, FALSE, 0); + + _cb_keep_bbox = Gtk::manage(new Gtk::CheckButton(_("Use saved size and position of the tile"))); + auto keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + _cb_keep_bbox->set_active(keepbbox); + _cb_keep_bbox->set_tooltip_text(_("Pretend that the size and position of the tile are the same " + "as the last time you tiled it (if any), instead of using the " + "current size")); + hb->pack_start(*_cb_keep_bbox, false, false, 0); + _cb_keep_bbox->signal_toggled().connect(sigc::mem_fun(*this, &CloneTiler::keep_bbox_toggled)); + } + + // Statusbar + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_end (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + GtkWidget *l = gtk_label_new(""); + _status = l; + gtk_box_pack_start (GTK_BOX (hb), l, FALSE, FALSE, 0); + } + + // Buttons + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start (GTK_BOX (mainbox), hb, FALSE, FALSE, 0); + + { + auto b = Gtk::manage(new Gtk::Button()); + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup_with_mnemonic(_(" <b>_Create</b> ")); + b->add(*l); + b->set_tooltip_text(_("Create and tile the clones of the selection")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::apply)); + gtk_box_pack_end (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + { // buttons which are enabled only when there are tiled clones + auto sb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_box_set_homogeneous(GTK_BOX(sb), FALSE); + gtk_box_pack_end (GTK_BOX (hb), sb, FALSE, FALSE, 0); + _buttons_on_tiles = sb; + { + // TRANSLATORS: if a group of objects are "clumped" together, then they + // are unevenly spread in the given amount of space - as shown in the + // diagrams on the left in the following screenshot: + // http://www.inkscape.org/screenshots/gallery/inkscape-0.42-CVS-tiles-unclump.png + // So unclumping is the process of spreading a number of objects out more evenly. + auto b = Gtk::manage(new Gtk::Button(_(" _Unclump "), true)); + b->set_tooltip_text(_("Spread out clones to reduce clumping; can be applied repeatedly")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::unclump)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" Re_move "), true)); + b->set_tooltip_text(_("Remove existing tiled clones of the selected object (siblings only)")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::on_remove_button_clicked)); + gtk_box_pack_end (GTK_BOX (sb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + + // connect to global selection changed signal (so we can change desktops) and + // external_change (so we're not fooled by undo) + selectChangedConn = INKSCAPE.signal_selection_changed.connect(sigc::mem_fun(*this, &CloneTiler::change_selection)); + externChangedConn = INKSCAPE.signal_external_change.connect(sigc::mem_fun(*this, &CloneTiler::external_change)); + + // update now + change_selection(SP_ACTIVE_DESKTOP->getSelection()); + } + + { + auto b = Gtk::manage(new Gtk::Button(_(" R_eset "), true)); + // TRANSLATORS: "change" is a noun here + b->set_tooltip_text(_("Reset all shifts, scales, rotates, opacity and color changes in the dialog to zero")); + b->signal_clicked().connect(sigc::mem_fun(*this, &CloneTiler::reset)); + gtk_box_pack_start (GTK_BOX (hb), GTK_WIDGET(b->gobj()), FALSE, FALSE, 0); + } + } + + gtk_widget_show_all (mainbox); + } + + show_all(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &CloneTiler::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + +} + +CloneTiler::~CloneTiler () +{ + //subselChangedConn.disconnect(); + //selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); + externChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); + color_changed_connection.disconnect(); +} + +void CloneTiler::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void CloneTiler::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + this->desktop = desktop; + } +} + +void CloneTiler::on_picker_color_changed(guint rgba) +{ + static bool is_updating = false; + if (is_updating || !SP_ACTIVE_DESKTOP) + return; + + is_updating = true; + + gchar c[32]; + sp_svg_write_color(c, sizeof(c), rgba); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(prefs_path + "initial_color", c); + + is_updating = false; +} + +void CloneTiler::change_selection(Inkscape::Selection *selection) +{ + if (selection->isEmpty()) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<small>Nothing selected.</small>")); + return; + } + + if (boost::distance(selection->items()) > 1) { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<small>More than one object selected.</small>")); + return; + } + + guint n = number_of_clones(selection->singleItem()); + if (n > 0) { + gtk_widget_set_sensitive (_buttons_on_tiles, TRUE); + gchar *sta = g_strdup_printf (_("<small>Object has <b>%d</b> tiled clones.</small>"), n); + gtk_label_set_markup (GTK_LABEL(_status), sta); + g_free (sta); + } else { + gtk_widget_set_sensitive (_buttons_on_tiles, FALSE); + gtk_label_set_markup (GTK_LABEL(_status), _("<small>Object has no tiled clones.</small>")); + } +} + +void CloneTiler::external_change() +{ + change_selection(SP_ACTIVE_DESKTOP->getSelection()); +} + +Geom::Affine CloneTiler::get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ) +{ + + // Shift (in units of tile width or height) ------------- + double delta_shifti = 0.0; + double delta_shiftj = 0.0; + + if( shiftx_alternate ) { + delta_shifti = (double)(i%2); + } else { + if( shiftx_cumulate ) { // Should the delta shifts be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_shifti = (double)(i*i); + } else { + delta_shifti = (double)i; + } + } + + if( shifty_alternate ) { + delta_shiftj = (double)(j%2); + } else { + if( shifty_cumulate ) { + delta_shiftj = (double)(j*j); + } else { + delta_shiftj = (double)j; + } + } + + // Random shift, only calculate if non-zero. + double delta_shiftx_rand = 0.0; + double delta_shifty_rand = 0.0; + if( shiftx_rand != 0.0 ) delta_shiftx_rand = shiftx_rand * g_random_double_range (-1, 1); + if( shifty_rand != 0.0 ) delta_shifty_rand = shifty_rand * g_random_double_range (-1, 1); + + + // Delta shift (units of tile width/height) + double di = shiftx_per_i * delta_shifti + shiftx_per_j * delta_shiftj + delta_shiftx_rand; + double dj = shifty_per_i * delta_shifti + shifty_per_j * delta_shiftj + delta_shifty_rand; + + // Shift in actual x and y, used below + double dx = w * di; + double dy = h * dj; + + double shifti = di; + double shiftj = dj; + + // Include tile width and height in shift if required + if( !shiftx_excludew ) shifti += i; + if( !shifty_excludeh ) shiftj += j; + + // Add exponential shift if necessary + double shifti_sign = (shifti > 0.0) ? 1.0 : -1.0; + shifti = shifti_sign * pow(fabs(shifti), shiftx_exp); + double shiftj_sign = (shiftj > 0.0) ? 1.0 : -1.0; + shiftj = shiftj_sign * pow(fabs(shiftj), shifty_exp); + + // Final shift + Geom::Affine rect_translate (Geom::Translate (w * shifti, h * shiftj)); + + // Rotation (in degrees) ------------ + double delta_rotationi = 0.0; + double delta_rotationj = 0.0; + + if( rotate_alternatei ) { + delta_rotationi = (double)(i%2); + } else { + if( rotate_cumulatei ) { + delta_rotationi = (double)(i*i + i)/2.0; + } else { + delta_rotationi = (double)i; + } + } + + if( rotate_alternatej ) { + delta_rotationj = (double)(j%2); + } else { + if( rotate_cumulatej ) { + delta_rotationj = (double)(j*j + j)/2.0; + } else { + delta_rotationj = (double)j; + } + } + + double delta_rotate_rand = 0.0; + if( rotate_rand != 0.0 ) delta_rotate_rand = rotate_rand * 180.0 * g_random_double_range (-1, 1); + + double dr = rotate_per_i * delta_rotationi + rotate_per_j * delta_rotationj + delta_rotate_rand; + + // Scale (times the original) ----------- + double delta_scalei = 0.0; + double delta_scalej = 0.0; + + if( scalex_alternate ) { + delta_scalei = (double)(i%2); + } else { + if( scalex_cumulate ) { // Should the delta scales be cumulative (i.e. 1, 1+2, 1+2+3, ...) + delta_scalei = (double)(i*i + i)/2.0; + } else { + delta_scalei = (double)i; + } + } + + if( scaley_alternate ) { + delta_scalej = (double)(j%2); + } else { + if( scaley_cumulate ) { + delta_scalej = (double)(j*j + j)/2.0; + } else { + delta_scalej = (double)j; + } + } + + // Random scale, only calculate if non-zero. + double delta_scalex_rand = 0.0; + double delta_scaley_rand = 0.0; + if( scalex_rand != 0.0 ) delta_scalex_rand = scalex_rand * g_random_double_range (-1, 1); + if( scaley_rand != 0.0 ) delta_scaley_rand = scaley_rand * g_random_double_range (-1, 1); + // But if random factors are same, scale x and y proportionally + if( scalex_rand == scaley_rand ) delta_scalex_rand = delta_scaley_rand; + + // Total delta scale + double scalex = 1.0 + scalex_per_i * delta_scalei + scalex_per_j * delta_scalej + delta_scalex_rand; + double scaley = 1.0 + scaley_per_i * delta_scalei + scaley_per_j * delta_scalej + delta_scaley_rand; + + if( scalex < 0.0 ) scalex = 0.0; + if( scaley < 0.0 ) scaley = 0.0; + + // Add exponential scale if necessary + if ( scalex_exp != 1.0 ) scalex = pow( scalex, scalex_exp ); + if ( scaley_exp != 1.0 ) scaley = pow( scaley, scaley_exp ); + + // Add logarithmic factor if necessary + if ( scalex_log > 0.0 ) scalex = pow( scalex_log, scalex - 1.0 ); + if ( scaley_log > 0.0 ) scaley = pow( scaley_log, scaley - 1.0 ); + // Alternative using rotation angle + //if ( scalex_log != 1.0 ) scalex *= pow( scalex_log, M_PI*dr/180 ); + //if ( scaley_log != 1.0 ) scaley *= pow( scaley_log, M_PI*dr/180 ); + + + // Calculate transformation matrices, translating back to "center of tile" (rotation center) before transforming + Geom::Affine drot_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI*dr/180) * Geom::Translate(cx, cy); + + Geom::Affine dscale_c = Geom::Translate(-cx, -cy) * Geom::Scale (scalex, scaley) * Geom::Translate(cx, cy); + + Geom::Affine d_s_r = dscale_c * drot_c; + + Geom::Affine rotate_180_c = Geom::Translate(-cx, -cy) * Geom::Rotate (M_PI) * Geom::Translate(cx, cy); + + Geom::Affine rotate_90_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/2) * Geom::Translate(cx, cy); + Geom::Affine rotate_m90_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/2) * Geom::Translate(cx, cy); + + Geom::Affine rotate_120_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-2*M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m120_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( 2*M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine rotate_60_c = Geom::Translate(-cx, -cy) * Geom::Rotate (-M_PI/3) * Geom::Translate(cx, cy); + Geom::Affine rotate_m60_c = Geom::Translate(-cx, -cy) * Geom::Rotate ( M_PI/3) * Geom::Translate(cx, cy); + + Geom::Affine flip_x = Geom::Translate(-cx, -cy) * Geom::Scale (-1, 1) * Geom::Translate(cx, cy); + Geom::Affine flip_y = Geom::Translate(-cx, -cy) * Geom::Scale (1, -1) * Geom::Translate(cx, cy); + + + // Create tile with required symmetry + const double cos60 = cos(M_PI/3); + const double sin60 = sin(M_PI/3); + const double cos30 = cos(M_PI/6); + const double sin30 = sin(M_PI/6); + + switch (type) { + + case TILE_P1: + return d_s_r * rect_translate; + break; + + case TILE_P2: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + break; + + case TILE_PM: + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PG: + if (j % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_CM: + if ((i + j) % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + break; + + case TILE_PMM: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_PMG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * rotate_180_c * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_PGG: + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_y * rect_translate; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_180_c * rect_translate; + } else { + return d_s_r * rotate_180_c * flip_y * rect_translate; + } + } + break; + + case TILE_CMM: + if (j % 4 == 0) { + if (i % 2 == 0) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else if (j % 4 == 1) { + if (i % 2 == 0) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } else if (j % 4 == 2) { + if (i % 2 == 1) { + return d_s_r * rect_translate; + } else { + return d_s_r * flip_x * rect_translate; + } + } else { + if (i % 2 == 1) { + return d_s_r * flip_y * rect_translate; + } else { + return d_s_r * flip_x * flip_y * rect_translate; + } + } + break; + + case TILE_P4: + { + Geom::Affine ori (Geom::Translate ((w + h) * pow((i/2), shiftx_exp) + dx, (h + w) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate (w/2 + h/2, -h/2 + w/2)); + Geom::Affine dia2 (Geom::Translate (-w/2 + h/2, h/2 + w/2)); + if (j % 2 == 0) { + if (i % 2 == 0) { + return d_s_r * ori; + } else { + return d_s_r * rotate_m90_c * dia1 * ori; + } + } else { + if (i % 2 == 0) { + return d_s_r * rotate_90_c * dia2 * ori; + } else { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } + } + break; + + case TILE_P4M: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow((j/2), shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 - h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 - h/2)); + if (j % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_m90_c * dia1 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_90_c * dia2 * Geom::Translate(0, h) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate(h, 0) * Geom::Translate(0, h) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_y * flip_x * Geom::Translate(w, 0) * Geom::Translate(0, h) * ori; + } + } + } + break; + + case TILE_P4G: + { + double max = MAX(w, h); + Geom::Affine ori (Geom::Translate ((max + max) * pow((i/4), shiftx_exp) + dx, (max + max) * pow(j, shifty_exp) + dy)); + Geom::Affine dia1 (Geom::Translate ( w/2 + h/2, h/2 - w/2)); + Geom::Affine dia2 (Geom::Translate (-h/2 + w/2, w/2 + h/2)); + if (((i/4) + j) % 2 == 0) { + if (i % 4 == 0) { + return d_s_r * ori; + } else if (i % 4 == 1) { + return d_s_r * rotate_m90_c * dia1 * ori; + } else if (i % 4 == 2) { + return d_s_r * rotate_90_c * dia2 * ori; + } else if (i % 4 == 3) { + return d_s_r * rotate_180_c * dia1 * dia2 * ori; + } + } else { + if (i % 4 == 0) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } else if (i % 4 == 1) { + return d_s_r * flip_y * rotate_m90_c * dia1 * Geom::Translate (-h, 0) * ori; + } else if (i % 4 == 2) { + return d_s_r * flip_y * rotate_90_c * dia2 * Geom::Translate (h, 0) * ori; + } else if (i % 4 == 3) { + return d_s_r * flip_x * Geom::Translate (w, 0) * ori; + } + } + } + break; + + case TILE_P3: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (w/2 + w/2 * cos60, -(w/2 * sin60))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60))); + } else { + width = h * cos (M_PI/6); + height = h; + dia1 = Geom::Affine (Geom::Translate (h/2 * cos30, -(h/2 * sin30))); + dia2 = dia1 * Geom::Affine (Geom::Translate (0, h/2)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/3) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 3 == 0) { + return d_s_r * ori; + } else if (i % 3 == 1) { + return d_s_r * rotate_m120_c * dia1 * ori; + } else if (i % 3 == 2) { + return d_s_r * rotate_120_c * dia2 * ori; + } + } + break; + + case TILE_P31M: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((i/6) + 0.5*(j%2), shiftx_exp) + dx, (w * cos30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + ori = Geom::Affine (Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (2*h - h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P3M1: + { + double width; + double height; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + if (w > h) { + width = w + w * cos60; + height = 2 * w * sin60; + dia1 = Geom::Affine (Geom::Translate (0, h/2) * Geom::Translate (w/2, 0) * Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, -h/2 * sin30) ); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, 2 * (w/2 * sin60 - h/2 * sin30))); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } else { + width = 2 * h * cos (M_PI/6); + height = 2 * h; + dia1 = Geom::Affine (Geom::Translate (0, -h/2) * Geom::Translate (h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (0, h/2)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-h * cos30, h * sin30)); + } + Geom::Affine ori (Geom::Translate (width * pow((2*(i/6) + j%2), shiftx_exp) + dx, (height/2) * pow(j, shifty_exp) + dy)); + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * flip_y * rotate_m120_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * flip_y * rotate_120_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * flip_y * Geom::Translate(0, h) * ori; + } + } + break; + + case TILE_P6: + { + Geom::Affine ori; + Geom::Affine dia1; + Geom::Affine dia2; + Geom::Affine dia3; + Geom::Affine dia4; + Geom::Affine dia5; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/6) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (w/2, 0)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-w/2, 0)); + } else { + ori = Geom::Affine(Geom::Translate (2*h * cos30 * pow((i/6 + 0.5*(j%2)), shiftx_exp) + dx, (h + h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia3 = dia2 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia4 = dia3 * dia1.inverse(); + dia5 = dia3 * dia2.inverse(); + } + if (i % 6 == 0) { + return d_s_r * ori; + } else if (i % 6 == 1) { + return d_s_r * rotate_m60_c * dia1 * ori; + } else if (i % 6 == 2) { + return d_s_r * rotate_m120_c * dia2 * ori; + } else if (i % 6 == 3) { + return d_s_r * rotate_180_c * dia3 * ori; + } else if (i % 6 == 4) { + return d_s_r * rotate_120_c * dia4 * ori; + } else if (i % 6 == 5) { + return d_s_r * rotate_60_c * dia5 * ori; + } + } + break; + + case TILE_P6M: + { + + Geom::Affine ori; + Geom::Affine dia1, dia2, dia3, dia4, dia5, dia6, dia7, dia8, dia9, dia10; + if (w > h) { + ori = Geom::Affine(Geom::Translate (w * pow((2*(i/12) + (j%2)), shiftx_exp) + dx, (2*w * sin60) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (w/2, h/2) * Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (-h/2 * cos30, h/2 * sin30)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-h/2 * cos30, h/2 * sin30) * Geom::Translate (w * cos60, 0) * Geom::Translate (-h/2 * cos30, -h/2 * sin30)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (-h/2 * cos30, -h/2 * sin30) * Geom::Translate (-w/2 * cos60, w/2 * sin60) * Geom::Translate (w/2, -h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } else { + ori = Geom::Affine(Geom::Translate (4*h * cos30 * pow((i/12 + 0.5*(j%2)), shiftx_exp) + dx, (2*h + 2*h * sin30) * pow(j, shifty_exp) + dy)); + dia1 = Geom::Affine (Geom::Translate (-w/2, -h/2) * Geom::Translate (h/2 * cos30, -h/2 * sin30) * Geom::Translate (w/2 * cos60, w/2 * sin60)); + dia2 = dia1 * Geom::Affine (Geom::Translate (h * cos30, -h * sin30)); + dia3 = dia2 * Geom::Affine (Geom::Translate (-w/2 * cos60, -w/2 * sin60) * Geom::Translate (h * cos30, 0) * Geom::Translate (-w/2 * cos60, w/2 * sin60)); + dia4 = dia3 * Geom::Affine (Geom::Translate (h * cos30, h * sin30)); + dia5 = dia4 * Geom::Affine (Geom::Translate (w/2 * cos60, -w/2 * sin60) * Geom::Translate (h/2 * cos30, h/2 * sin30) * Geom::Translate (-w/2, h/2)); + dia6 = dia5 * Geom::Affine (Geom::Translate (0, h)); + dia7 = dia6 * dia1.inverse(); + dia8 = dia6 * dia2.inverse(); + dia9 = dia6 * dia3.inverse(); + dia10 = dia6 * dia4.inverse(); + } + if (i % 12 == 0) { + return d_s_r * ori; + } else if (i % 12 == 1) { + return d_s_r * flip_y * rotate_m60_c * dia1 * ori; + } else if (i % 12 == 2) { + return d_s_r * rotate_m60_c * dia2 * ori; + } else if (i % 12 == 3) { + return d_s_r * flip_y * rotate_m120_c * dia3 * ori; + } else if (i % 12 == 4) { + return d_s_r * rotate_m120_c * dia4 * ori; + } else if (i % 12 == 5) { + return d_s_r * flip_x * dia5 * ori; + } else if (i % 12 == 6) { + return d_s_r * flip_x * flip_y * dia6 * ori; + } else if (i % 12 == 7) { + return d_s_r * flip_y * rotate_120_c * dia7 * ori; + } else if (i % 12 == 8) { + return d_s_r * rotate_120_c * dia8 * ori; + } else if (i % 12 == 9) { + return d_s_r * flip_y * rotate_60_c * dia9 * ori; + } else if (i % 12 == 10) { + return d_s_r * rotate_60_c * dia10 * ori; + } else if (i % 12 == 11) { + return d_s_r * flip_y * Geom::Translate (0, h) * ori; + } + } + break; + + default: + break; + } + + return Geom::identity(); +} + +bool CloneTiler::is_a_clone_of(SPObject *tile, SPObject *obj) +{ + bool result = false; + char *id_href = nullptr; + + if (obj) { + Inkscape::XML::Node *obj_repr = obj->getRepr(); + id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + } + + if (dynamic_cast<SPUse *>(tile) && + tile->getRepr()->attribute("xlink:href") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("xlink:href"))) && + tile->getRepr()->attribute("inkscape:tiled-clone-of") && + (!id_href || !strcmp(id_href, tile->getRepr()->attribute("inkscape:tiled-clone-of")))) + { + result = true; + } else { + result = false; + } + if (id_href) { + g_free(id_href); + id_href = nullptr; + } + return result; +} + +void CloneTiler::trace_hide_tiled_clones_recursively(SPObject *from) +{ + if (!trace_drawing) + return; + + for (auto& o: from->children) { + SPItem *item = dynamic_cast<SPItem *>(&o); + if (item && is_a_clone_of(&o, nullptr)) { + item->invoke_hide(trace_visionkey); // FIXME: hide each tiled clone's original too! + } + trace_hide_tiled_clones_recursively (&o); + } +} + +void CloneTiler::trace_setup(SPDocument *doc, gdouble zoom, SPItem *original) +{ + trace_drawing = new Inkscape::Drawing(); + /* Create ArenaItem and set transform */ + trace_visionkey = SPItem::display_key_new(1); + trace_doc = doc; + trace_drawing->setRoot(trace_doc->getRoot()->invoke_show(*trace_drawing, trace_visionkey, SP_ITEM_SHOW_DISPLAY)); + + // hide the (current) original and any tiled clones, we only want to pick the background + original->invoke_hide(trace_visionkey); + trace_hide_tiled_clones_recursively(trace_doc->getRoot()); + + trace_doc->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + trace_doc->ensureUpToDate(); + + trace_zoom = zoom; +} + +guint32 CloneTiler::trace_pick(Geom::Rect box) +{ + if (!trace_drawing) { + return 0; + } + + trace_drawing->root()->setTransform(Geom::Scale(trace_zoom)); + trace_drawing->update(); + + /* Item integer bbox in points */ + Geom::IntRect ibox = (box * Geom::Scale(trace_zoom)).roundOutwards(); + + /* Find visible area */ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ibox.width(), ibox.height()); + Inkscape::DrawingContext dc(s, ibox.min()); + /* Render */ + trace_drawing->render(dc, ibox); + double R = 0, G = 0, B = 0, A = 0; + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + + return SP_RGBA32_F_COMPOSE (R, G, B, A); +} + +void CloneTiler::trace_finish() +{ + if (trace_doc) { + trace_doc->getRoot()->invoke_hide(trace_visionkey); + delete trace_drawing; + trace_doc = nullptr; + trace_drawing = nullptr; + } +} + +void CloneTiler::unclump() +{ + auto desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + auto selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to unclump.")); + return; + } + + auto obj = selection->singleItem(); + auto parent = obj->parent; + + std::vector<SPItem*> to_unclump; // not including the original + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_unclump.push_back((SPItem*)&child); + } + } + + desktop->getDocument()->ensureUpToDate(); + reverse(to_unclump.begin(),to_unclump.end()); + ::unclump (to_unclump); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Unclump tiled clones")); +} + +guint CloneTiler::number_of_clones(SPObject *obj) +{ + SPObject *parent = obj->parent; + + guint n = 0; + + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + n ++; + } + } + + return n; +} + +void CloneTiler::remove(bool do_undo/* = true*/) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + Inkscape::Selection *selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty() || boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>one object</b> whose tiled clones to remove.")); + return; + } + + SPObject *obj = selection->singleItem(); + SPObject *parent = obj->parent; + +// remove old tiling + std::vector<SPObject *> to_delete; + for (auto& child: parent->children) { + if (is_a_clone_of (&child, obj)) { + to_delete.push_back(&child); + } + } + for (auto obj:to_delete) { + g_assert(obj != nullptr); + obj->deleteObject(); + } + + change_selection (selection); + + if (do_undo) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Delete tiled clones")); + } +} + +Geom::Rect CloneTiler::transform_rect(Geom::Rect const &r, Geom::Affine const &m) +{ + using Geom::X; + using Geom::Y; + Geom::Point const p1 = r.corner(1) * m; + Geom::Point const p2 = r.corner(2) * m; + Geom::Point const p3 = r.corner(3) * m; + Geom::Point const p4 = r.corner(4) * m; + return Geom::Rect( + Geom::Point( + std::min(std::min(p1[X], p2[X]), std::min(p3[X], p4[X])), + std::min(std::min(p1[Y], p2[Y]), std::min(p3[Y], p4[Y]))), + Geom::Point( + std::max(std::max(p1[X], p2[X]), std::max(p3[X], p4[X])), + std::max(std::max(p1[Y], p2[Y]), std::max(p3[Y], p4[Y])))); +} + +/** +Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same +probability of being displaced. + */ +double CloneTiler::randomize01(double val, double rand) +{ + double base = MIN (val - rand, 1 - 2*rand); + if (base < 0) { + base = 0; + } + val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); + return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... +} + + +void CloneTiler::apply() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Selection *selection = desktop->getSelection(); + + // check if something is selected + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select an <b>object</b> to clone.")); + return; + } + + // Check if more than one object is selected. + if (boost::distance(selection->items()) > 1) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, _("If you want to clone several objects, <b>group</b> them and <b>clone the group</b>.")); + return; + } + + // set "busy" cursor + desktop->setWaitingCursor(); + + // set statusbar text + gtk_label_set_markup (GTK_LABEL(_status), _("<small>Creating tiled clones...</small>")); + gtk_widget_queue_draw(GTK_WIDGET(_status)); + + SPObject *obj = selection->singleItem(); + if (!obj) { + // Should never happen (empty selection checked above). + std::cerr << "CloneTiler::clonetile_apply(): No object in single item selection!!!" << std::endl; + return; + } + Inkscape::XML::Node *obj_repr = obj->getRepr(); + const char *id_href = g_strdup_printf("#%s", obj_repr->attribute("id")); + SPObject *parent = obj->parent; + + remove(false); + + Geom::Scale scale = desktop->getDocument()->getDocumentScale().inverse(); + double scale_units = scale[Geom::X]; // Use just x direction.... + + double shiftx_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_i", 0, -10000, 10000); + double shifty_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_i", 0, -10000, 10000); + double shiftx_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_per_j", 0, -10000, 10000); + double shifty_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_per_j", 0, -10000, 10000); + double shiftx_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shiftx_rand", 0, 0, 1000); + double shifty_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "shifty_rand", 0, 0, 1000); + double shiftx_exp = prefs->getDoubleLimited(prefs_path + "shiftx_exp", 1, 0, 10); + double shifty_exp = prefs->getDoubleLimited(prefs_path + "shifty_exp", 1, 0, 10); + bool shiftx_alternate = prefs->getBool(prefs_path + "shiftx_alternate"); + bool shifty_alternate = prefs->getBool(prefs_path + "shifty_alternate"); + bool shiftx_cumulate = prefs->getBool(prefs_path + "shiftx_cumulate"); + bool shifty_cumulate = prefs->getBool(prefs_path + "shifty_cumulate"); + bool shiftx_excludew = prefs->getBool(prefs_path + "shiftx_excludew"); + bool shifty_excludeh = prefs->getBool(prefs_path + "shifty_excludeh"); + + double scalex_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_i", 0, -100, 1000); + double scaley_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_i", 0, -100, 1000); + double scalex_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_per_j", 0, -100, 1000); + double scaley_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_per_j", 0, -100, 1000); + double scalex_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scalex_rand", 0, 0, 1000); + double scaley_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "scaley_rand", 0, 0, 1000); + double scalex_exp = prefs->getDoubleLimited(prefs_path + "scalex_exp", 1, 0, 10); + double scaley_exp = prefs->getDoubleLimited(prefs_path + "scaley_exp", 1, 0, 10); + double scalex_log = prefs->getDoubleLimited(prefs_path + "scalex_log", 0, 0, 10); + double scaley_log = prefs->getDoubleLimited(prefs_path + "scaley_log", 0, 0, 10); + bool scalex_alternate = prefs->getBool(prefs_path + "scalex_alternate"); + bool scaley_alternate = prefs->getBool(prefs_path + "scaley_alternate"); + bool scalex_cumulate = prefs->getBool(prefs_path + "scalex_cumulate"); + bool scaley_cumulate = prefs->getBool(prefs_path + "scaley_cumulate"); + + double rotate_per_i = prefs->getDoubleLimited(prefs_path + "rotate_per_i", 0, -180, 180); + double rotate_per_j = prefs->getDoubleLimited(prefs_path + "rotate_per_j", 0, -180, 180); + double rotate_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "rotate_rand", 0, 0, 100); + bool rotate_alternatei = prefs->getBool(prefs_path + "rotate_alternatei"); + bool rotate_alternatej = prefs->getBool(prefs_path + "rotate_alternatej"); + bool rotate_cumulatei = prefs->getBool(prefs_path + "rotate_cumulatei"); + bool rotate_cumulatej = prefs->getBool(prefs_path + "rotate_cumulatej"); + + double blur_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_i", 0, 0, 100); + double blur_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_per_j", 0, 0, 100); + bool blur_alternatei = prefs->getBool(prefs_path + "blur_alternatei"); + bool blur_alternatej = prefs->getBool(prefs_path + "blur_alternatej"); + double blur_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "blur_rand", 0, 0, 100); + + double opacity_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_i", 0, 0, 100); + double opacity_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_per_j", 0, 0, 100); + bool opacity_alternatei = prefs->getBool(prefs_path + "opacity_alternatei"); + bool opacity_alternatej = prefs->getBool(prefs_path + "opacity_alternatej"); + double opacity_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "opacity_rand", 0, 0, 100); + + Glib::ustring initial_color = prefs->getString(prefs_path + "initial_color"); + double hue_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_j", 0, -100, 100); + double hue_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_per_i", 0, -100, 100); + double hue_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "hue_rand", 0, 0, 100); + double saturation_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_j", 0, -100, 100); + double saturation_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_per_i", 0, -100, 100); + double saturation_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "saturation_rand", 0, 0, 100); + double lightness_per_j = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_j", 0, -100, 100); + double lightness_per_i = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_per_i", 0, -100, 100); + double lightness_rand = 0.01 * prefs->getDoubleLimited(prefs_path + "lightness_rand", 0, 0, 100); + bool color_alternatej = prefs->getBool(prefs_path + "color_alternatej"); + bool color_alternatei = prefs->getBool(prefs_path + "color_alternatei"); + + int type = prefs->getInt(prefs_path + "symmetrygroup", 0); + bool keepbbox = prefs->getBool(prefs_path + "keepbbox", true); + int imax = prefs->getInt(prefs_path + "imax", 2); + int jmax = prefs->getInt(prefs_path + "jmax", 2); + + bool fillrect = prefs->getBool(prefs_path + "fillrect"); + double fillwidth = scale_units*prefs->getDoubleLimited(prefs_path + "fillwidth", 50, 0, 1e6); + double fillheight = scale_units*prefs->getDoubleLimited(prefs_path + "fillheight", 50, 0, 1e6); + + bool dotrace = prefs->getBool(prefs_path + "dotrace"); + int pick = prefs->getInt(prefs_path + "pick"); + bool pick_to_presence = prefs->getBool(prefs_path + "pick_to_presence"); + bool pick_to_size = prefs->getBool(prefs_path + "pick_to_size"); + bool pick_to_color = prefs->getBool(prefs_path + "pick_to_color"); + bool pick_to_opacity = prefs->getBool(prefs_path + "pick_to_opacity"); + double rand_picked = 0.01 * prefs->getDoubleLimited(prefs_path + "rand_picked", 0, 0, 100); + bool invert_picked = prefs->getBool(prefs_path + "invert_picked"); + double gamma_picked = prefs->getDoubleLimited(prefs_path + "gamma_picked", 0, -10, 10); + + SPItem *item = dynamic_cast<SPItem *>(obj); + if (dotrace) { + trace_setup(desktop->getDocument(), 1.0, item); + } + + Geom::Point center; + double w = 0; + double h = 0; + double x0 = 0; + double y0 = 0; + + if (keepbbox && + obj_repr->attribute("inkscape:tile-w") && + obj_repr->attribute("inkscape:tile-h") && + obj_repr->attribute("inkscape:tile-x0") && + obj_repr->attribute("inkscape:tile-y0") && + obj_repr->attribute("inkscape:tile-cx") && + obj_repr->attribute("inkscape:tile-cy")) { + + double cx = 0; + double cy = 0; + sp_repr_get_double (obj_repr, "inkscape:tile-cx", &cx); + sp_repr_get_double (obj_repr, "inkscape:tile-cy", &cy); + center = Geom::Point (cx, cy); + + sp_repr_get_double (obj_repr, "inkscape:tile-w", &w); + sp_repr_get_double (obj_repr, "inkscape:tile-h", &h); + sp_repr_get_double (obj_repr, "inkscape:tile-x0", &x0); + sp_repr_get_double (obj_repr, "inkscape:tile-y0", &y0); + } else { + bool prefs_bbox = prefs->getBool("/tools/bounding_box", false); + SPItem::BBoxType bbox_type = ( !prefs_bbox ? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX ); + Geom::OptRect r = item->documentBounds(bbox_type); + if (r) { + w = scale_units*r->dimensions()[Geom::X]; + h = scale_units*r->dimensions()[Geom::Y]; + x0 = scale_units*r->min()[Geom::X]; + y0 = scale_units*r->min()[Geom::Y]; + center = scale_units*desktop->dt2doc(item->getCenter()); + + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cx", center[Geom::X]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-cy", center[Geom::Y]); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-w", w); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-h", h); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-x0", x0); + sp_repr_set_svg_double(obj_repr, "inkscape:tile-y0", y0); + } else { + center = Geom::Point(0, 0); + w = h = 0; + x0 = y0 = 0; + } + } + + Geom::Point cur(0, 0); + Geom::Rect bbox_original (Geom::Point (x0, y0), Geom::Point (x0 + w, y0 + h)); + double perimeter_original = (w + h)/4; + + // The integers i and j are reserved for tile column and row. + // The doubles x and y are used for coordinates + for (int i = 0; + fillrect? + (fabs(cur[Geom::X]) < fillwidth && i < 200) // prevent "freezing" with too large fillrect, arbitrarily limit rows + : (i < imax); + i ++) { + for (int j = 0; + fillrect? + (fabs(cur[Geom::Y]) < fillheight && j < 200) // prevent "freezing" with too large fillrect, arbitrarily limit cols + : (j < jmax); + j ++) { + + // Note: We create a clone at 0,0 too, right over the original, in case our clones are colored + + // Get transform from symmetry, shift, scale, rotation + Geom::Affine orig_t = get_transform (type, i, j, center[Geom::X], center[Geom::Y], w, h, + shiftx_per_i, shifty_per_i, + shiftx_per_j, shifty_per_j, + shiftx_rand, shifty_rand, + shiftx_exp, shifty_exp, + shiftx_alternate, shifty_alternate, + shiftx_cumulate, shifty_cumulate, + shiftx_excludew, shifty_excludeh, + scalex_per_i, scaley_per_i, + scalex_per_j, scaley_per_j, + scalex_rand, scaley_rand, + scalex_exp, scaley_exp, + scalex_log, scaley_log, + scalex_alternate, scaley_alternate, + scalex_cumulate, scaley_cumulate, + rotate_per_i, rotate_per_j, + rotate_rand, + rotate_alternatei, rotate_alternatej, + rotate_cumulatei, rotate_cumulatej ); + Geom::Affine parent_transform = (((SPItem*)item->parent)->i2doc_affine())*(item->document->getRoot()->c2p.inverse()); + Geom::Affine t = parent_transform*orig_t*parent_transform.inverse(); + cur = center * t - center; + if (fillrect) { + if ((cur[Geom::X] > fillwidth) || (cur[Geom::Y] > fillheight)) { // off limits + continue; + } + } + + gchar color_string[32]; *color_string = 0; + + // Color tab + if (!initial_color.empty()) { + guint32 rgba = sp_svg_read_color (initial_color.data(), 0x000000ff); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + double eff_i = (color_alternatei? (i%2) : (i)); + double eff_j = (color_alternatej? (j%2) : (j)); + + hsl[0] += hue_per_i * eff_i + hue_per_j * eff_j + hue_rand * g_random_double_range (-1, 1); + double notused; + hsl[0] = modf( hsl[0], ¬used ); // Restrict to 0-1 + hsl[1] += saturation_per_i * eff_i + saturation_per_j * eff_j + saturation_rand * g_random_double_range (-1, 1); + hsl[1] = CLAMP (hsl[1], 0, 1); + hsl[2] += lightness_per_i * eff_i + lightness_per_j * eff_j + lightness_rand * g_random_double_range (-1, 1); + hsl[2] = CLAMP (hsl[2], 0, 1); + + float rgb[3]; + SPColor::hsl_to_rgb_floatv (rgb, hsl[0], hsl[1], hsl[2]); + sp_svg_write_color(color_string, sizeof(color_string), SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1.0)); + } + + // Blur + double blur = 0.0; + { + int eff_i = (blur_alternatei? (i%2) : (i)); + int eff_j = (blur_alternatej? (j%2) : (j)); + blur = (blur_per_i * eff_i + blur_per_j * eff_j + blur_rand * g_random_double_range (-1, 1)); + blur = CLAMP (blur, 0, 1); + } + + // Opacity + double opacity = 1.0; + { + int eff_i = (opacity_alternatei? (i%2) : (i)); + int eff_j = (opacity_alternatej? (j%2) : (j)); + opacity = 1 - (opacity_per_i * eff_i + opacity_per_j * eff_j + opacity_rand * g_random_double_range (-1, 1)); + opacity = CLAMP (opacity, 0, 1); + } + + // Trace tab + if (dotrace) { + Geom::Rect bbox_t = transform_rect (bbox_original, t*Geom::Scale(1.0/scale_units)); + + guint32 rgba = trace_pick (bbox_t); + float r = SP_RGBA32_R_F(rgba); + float g = SP_RGBA32_G_F(rgba); + float b = SP_RGBA32_B_F(rgba); + float a = SP_RGBA32_A_F(rgba); + + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + + gdouble val = 0; + switch (pick) { + case PICK_COLOR: + val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max + break; + case PICK_OPACITY: + val = a; + break; + case PICK_R: + val = r; + break; + case PICK_G: + val = g; + break; + case PICK_B: + val = b; + break; + case PICK_H: + val = hsl[0]; + break; + case PICK_S: + val = hsl[1]; + break; + case PICK_L: + val = 1 - hsl[2]; + break; + default: + break; + } + + if (rand_picked > 0) { + val = randomize01 (val, rand_picked); + r = randomize01 (r, rand_picked); + g = randomize01 (g, rand_picked); + b = randomize01 (b, rand_picked); + } + + if (gamma_picked != 0) { + double power; + if (gamma_picked > 0) + power = 1/(1 + fabs(gamma_picked)); + else + power = 1 + fabs(gamma_picked); + + val = pow (val, power); + r = pow (r, power); + g = pow (g, power); + b = pow (b, power); + } + + if (invert_picked) { + val = 1 - val; + r = 1 - r; + g = 1 - g; + b = 1 - b; + } + + val = CLAMP (val, 0, 1); + r = CLAMP (r, 0, 1); + g = CLAMP (g, 0, 1); + b = CLAMP (b, 0, 1); + + // recompose tweaked color + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + + if (pick_to_presence) { + if (g_random_double_range (0, 1) > val) { + continue; // skip! + } + } + if (pick_to_size) { + t = parent_transform * Geom::Translate(-center[Geom::X], -center[Geom::Y]) + * Geom::Scale (val, val) * Geom::Translate(center[Geom::X], center[Geom::Y]) + * parent_transform.inverse() * t; + } + if (pick_to_opacity) { + opacity *= val; + } + if (pick_to_color) { + sp_svg_write_color(color_string, sizeof(color_string), rgba); + } + } + + if (opacity < 1e-6) { // invisibly transparent, skip + continue; + } + + if (fabs(t[0]) + fabs (t[1]) + fabs(t[2]) + fabs(t[3]) < 1e-6) { // too small, skip + continue; + } + + // Create the clone + Inkscape::XML::Node *clone = obj_repr->document()->createElement("svg:use"); + clone->setAttribute("x", "0"); + clone->setAttribute("y", "0"); + clone->setAttribute("inkscape:tiled-clone-of", id_href); + clone->setAttribute("xlink:href", id_href); + + Geom::Point new_center; + bool center_set = false; + if (obj_repr->attribute("inkscape:transform-center-x") || obj_repr->attribute("inkscape:transform-center-y")) { + new_center = scale_units*desktop->dt2doc(item->getCenter()) * orig_t; + center_set = true; + } + + gchar *affinestr=sp_svg_transform_write(t); + clone->setAttribute("transform", affinestr); + g_free(affinestr); + + if (opacity < 1.0) { + sp_repr_set_css_double(clone, "opacity", opacity); + } + + if (*color_string) { + clone->setAttribute("fill", color_string); + clone->setAttribute("stroke", color_string); + } + + // add the new clone to the top of the original's parent + parent->getRepr()->appendChild(clone); + + if (blur > 0.0) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + double perimeter = perimeter_original * t.descrim(); + double radius = blur * perimeter; + // this is necessary for all newly added clones to have correct bboxes, + // otherwise filters won't work: + desktop->getDocument()->ensureUpToDate(); + // it's hard to figure out exact width/height of the tile without having an object + // that we can take bbox of; however here we only need a lower bound so that blur + // margins are not too small, and the perimeter should work + SPFilter *constructed = new_filter_gaussian_blur(desktop->getDocument(), radius, t.descrim(), t.expansionX(), t.expansionY(), perimeter, perimeter); + sp_style_set_property_url (clone_object, "filter", constructed, false); + } + + if (center_set) { + SPObject *clone_object = desktop->getDocument()->getObjectByRepr(clone); + SPItem *item = dynamic_cast<SPItem *>(clone_object); + if (clone_object && item) { + clone_object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + item->setCenter(desktop->doc2dt(new_center)); + clone_object->updateRepr(); + } + } + + Inkscape::GC::release(clone); + } + cur[Geom::Y] = 0; + } + + if (dotrace) { + trace_finish (); + } + + change_selection(selection); + + desktop->clearWaitingCursor(); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_DIALOG_CLONETILER, + _("Create tiled clones")); +} + +GtkWidget * CloneTiler::new_tab(GtkWidget *nb, const gchar *label) +{ + GtkWidget *l = gtk_label_new_with_mnemonic (label); + auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, VB_MARGIN); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + gtk_container_set_border_width (GTK_CONTAINER (vb), VB_MARGIN); + gtk_notebook_append_page (GTK_NOTEBOOK (nb), vb, l); + return vb; +} + +void CloneTiler::checkbox_toggled(Gtk::ToggleButton *tb, + const Glib::ustring &attr) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + attr, tb->get_active()); +} + +Gtk::Widget * CloneTiler::checkbox(const char *tip, + const Glib::ustring &attr) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, VB_MARGIN)); + auto b = Gtk::manage(new Gtk::CheckButton()); + b->set_tooltip_text(tip); + + auto const prefs = Inkscape::Preferences::get(); + auto const value = prefs->getBool(prefs_path + attr); + b->set_active(value); + + hb->pack_start(*b, false, true); + b->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::checkbox_toggled), b, attr)); + + b->set_data("uncheckable", GINT_TO_POINTER(true)); + + return hb; +} + +void CloneTiler::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + pref, adj->get_value()); +} + +Gtk::Widget * CloneTiler::spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent/* = false*/) +{ + auto hb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + + { + // Parameters for adjustment + auto const initial_value = (exponent ? 1.0 : 0.0); + auto const step_increment = (exponent ? 0.01 : 0.1); + auto const page_increment = (exponent ? 0.05 : 0.4); + + auto a = Gtk::Adjustment::create(initial_value, + lower, + upper, + step_increment, + page_increment); + + auto const climb_rate = (exponent ? 0.01 : 0.1); + auto const digits = (exponent ? 2 : 1); + + auto sb = new Inkscape::UI::Widget::SpinButton(a, climb_rate, digits); + + sb->set_tooltip_text (tip); + sb->set_width_chars (5); + sb->set_digits(3); + hb->pack_start(*sb, false, false, SB_MARGIN); + + auto prefs = Inkscape::Preferences::get(); + auto value = prefs->getDoubleLimited(prefs_path + attr, exponent? 1.0 : 0.0, lower, upper); + a->set_value (value); + a->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &CloneTiler::value_changed), a, attr)); + + if (exponent) { + sb->set_data ("oneable", GINT_TO_POINTER(TRUE)); + } else { + sb->set_data ("zeroable", GINT_TO_POINTER(TRUE)); + } + } + + { + auto l = Gtk::manage(new Gtk::Label("")); + l->set_markup(suffix); + hb->pack_start(*l); + } + + return hb; +} + +void CloneTiler::symgroup_changed(Gtk::ComboBox *cb) +{ + auto prefs = Inkscape::Preferences::get(); + auto group_new = cb->get_active_row_number(); + prefs->setInt(prefs_path + "symmetrygroup", group_new); +} + +void CloneTiler::xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + pref, (int) floor(adj->get_value() + 0.5)); +} + +void CloneTiler::keep_bbox_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "keepbbox", _cb_keep_bbox->get_active()); +} + +void CloneTiler::pick_to(Gtk::ToggleButton *tb, Glib::ustring const &pref) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + pref, tb->get_active()); +} + + +void CloneTiler::reset_recursive(GtkWidget *w) +{ + if (w && G_IS_OBJECT(w)) { + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "zeroable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 0); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "oneable")); + if (r && GTK_IS_SPIN_BUTTON(w)) { // spinbutton + GtkAdjustment *a = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON(w)); + gtk_adjustment_set_value (a, 1); + } + } + { + int r = GPOINTER_TO_INT (g_object_get_data(G_OBJECT(w), "uncheckable")); + if (r && GTK_IS_TOGGLE_BUTTON(w)) { // checkbox + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(w), FALSE); + } + } + } + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> c = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for ( auto i : c ) { + reset_recursive(i->gobj()); + } + } +} + +void CloneTiler::reset() +{ + reset_recursive(GTK_WIDGET(this->gobj())); +} + +void CloneTiler::table_attach(GtkWidget *table, Gtk::Widget *widget, float align, int row, int col) +{ + table_attach(table, GTK_WIDGET(widget->gobj()), align, row, col); +} + +void CloneTiler::table_attach(GtkWidget *table, GtkWidget *widget, float align, int row, int col) +{ + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + gtk_widget_set_valign(widget, GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(table), widget, col, row, 1, 1); +} + +GtkWidget * CloneTiler::table_x_y_rand(int values) +{ + auto table = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(table), 6); + gtk_grid_set_column_spacing(GTK_GRID(table), 8); + + gtk_container_set_border_width (GTK_CONTAINER (table), VB_MARGIN); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-rows", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("<small>Per row:</small>")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 2); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + GtkWidget *i = sp_get_icon_image("object-columns", GTK_ICON_SIZE_MENU); + gtk_box_pack_start(GTK_BOX(hb), i, FALSE, FALSE, 2); + + GtkWidget *l = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(l), _("<small>Per column:</small>")); + gtk_box_pack_start(GTK_BOX(hb), l, FALSE, FALSE, 2); + + table_attach(table, hb, 0, 1, 3); + } + + { + GtkWidget *l = gtk_label_new (""); + gtk_label_set_markup (GTK_LABEL(l), _("<small>Randomize:</small>")); + table_attach (table, l, 0, 1, 4); + } + + return table; +} + +void CloneTiler::pick_switched(PickType v) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + "pick", v); +} + +void CloneTiler::switch_to_create() +{ + if (_rowscols) { + _rowscols->set_sensitive(true); + } + if (_widthheight) { + _widthheight->set_sensitive(false); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", false); +} + + +void CloneTiler::switch_to_fill() +{ + if (_rowscols) { + _rowscols->set_sensitive(false); + } + if (_widthheight) { + _widthheight->set_sensitive(true); + } + + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(prefs_path + "fillrect", true); +} + +void CloneTiler::fill_width_changed() +{ + auto const raw_dist = fill_width->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillwidth", pixels); +} + +void CloneTiler::fill_height_changed() +{ + auto const raw_dist = fill_height->get_value(); + auto const unit = unit_menu->getUnit(); + auto const pixels = Inkscape::Util::Quantity::convert(raw_dist, unit, "px"); + + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble(prefs_path + "fillheight", pixels); +} + +void CloneTiler::unit_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble width_pixels = prefs->getDouble(prefs_path + "fillwidth"); + gdouble height_pixels = prefs->getDouble(prefs_path + "fillheight"); + + Inkscape::Util::Unit const *unit = unit_menu->getUnit(); + + gdouble width_value = Inkscape::Util::Quantity::convert(width_pixels, "px", unit); + gdouble height_value = Inkscape::Util::Quantity::convert(height_pixels, "px", unit); + gtk_adjustment_set_value(fill_width->gobj(), width_value); + gtk_adjustment_set_value(fill_height->gobj(), height_value); +} + +void CloneTiler::do_pick_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + auto active = _b->get_active(); + prefs->setBool(prefs_path + "dotrace", active); + + if (_dotrace) { + gtk_widget_set_sensitive (_dotrace, active); + } +} + +void CloneTiler::show_page_trace() +{ + gtk_notebook_set_current_page(GTK_NOTEBOOK(nb),6); + _b->set_active(false); +} + + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/clonetiler.h b/src/ui/dialog/clonetiler.h new file mode 100644 index 0000000..47c20ad --- /dev/null +++ b/src/ui/dialog/clonetiler.h @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Clone tiling dialog + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2004 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __SP_CLONE_TILER_H__ +#define __SP_CLONE_TILER_H__ + +#include "ui/widget/panel.h" + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/color-picker.h" + +namespace Gtk { + class CheckButton; + class ComboBox; + class ToggleButton; +} + +class SPItem; +class SPObject; + +namespace Geom { + class Rect; + class Affine; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +} + +namespace Dialog { + +class CloneTiler : public Widget::Panel { +public: + CloneTiler(); + ~CloneTiler() override; + + static CloneTiler &getInstance() { return *new CloneTiler(); } + void show_page_trace(); +protected: + enum PickType { + PICK_COLOR, + PICK_OPACITY, + PICK_R, + PICK_G, + PICK_B, + PICK_H, + PICK_S, + PICK_L + }; + + GtkWidget * new_tab(GtkWidget *nb, const gchar *label); + GtkWidget * table_x_y_rand(int values); + Gtk::Widget * spinbox(const char *tip, + const Glib::ustring &attr, + double lower, + double upper, + const gchar *suffix, + bool exponent = false); + Gtk::Widget * checkbox(const char *tip, + const Glib::ustring &attr); + void table_attach(GtkWidget *table, GtkWidget *widget, float align, int row, int col); + void table_attach(GtkWidget *table, Gtk::Widget *widget, float align, int row, int col); + + void symgroup_changed(Gtk::ComboBox *cb); + void on_picker_color_changed(guint rgba); + void trace_hide_tiled_clones_recursively(SPObject *from); + guint number_of_clones(SPObject *obj); + void trace_setup(SPDocument *doc, gdouble zoom, SPItem *original); + guint32 trace_pick(Geom::Rect box); + void trace_finish(); + bool is_a_clone_of(SPObject *tile, SPObject *obj); + Geom::Rect transform_rect(Geom::Rect const &r, Geom::Affine const &m); + double randomize01(double val, double rand); + + void apply(); + void change_selection(Inkscape::Selection *selection); + void checkbox_toggled(Gtk::ToggleButton *tb, + Glib::ustring const &attr); + void do_pick_toggled(); + void external_change(); + void fill_width_changed(); + void fill_height_changed(); + void keep_bbox_toggled(); + void on_remove_button_clicked() {remove();} + void pick_switched(PickType); + void pick_to(Gtk::ToggleButton *tb, + Glib::ustring const &pref); + void remove(bool do_undo = true); + void reset(); + void reset_recursive(GtkWidget *w); + void switch_to_create(); + void switch_to_fill(); + void unclump(); + void unit_changed(); + void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref); + void xy_changed(Glib::RefPtr<Gtk::Adjustment> &adj, Glib::ustring const &pref); + + Geom::Affine get_transform( + // symmetry group + int type, + + // row, column + int i, int j, + + // center, width, height of the tile + double cx, double cy, + double w, double h, + + // values from the dialog: + // Shift + double shiftx_per_i, double shifty_per_i, + double shiftx_per_j, double shifty_per_j, + double shiftx_rand, double shifty_rand, + double shiftx_exp, double shifty_exp, + int shiftx_alternate, int shifty_alternate, + int shiftx_cumulate, int shifty_cumulate, + int shiftx_excludew, int shifty_excludeh, + + // Scale + double scalex_per_i, double scaley_per_i, + double scalex_per_j, double scaley_per_j, + double scalex_rand, double scaley_rand, + double scalex_exp, double scaley_exp, + double scalex_log, double scaley_log, + int scalex_alternate, int scaley_alternate, + int scalex_cumulate, int scaley_cumulate, + + // Rotation + double rotate_per_i, double rotate_per_j, + double rotate_rand, + int rotate_alternatei, int rotate_alternatej, + int rotate_cumulatei, int rotate_cumulatej + ); + + +private: + CloneTiler(CloneTiler const &d) = delete; + CloneTiler& operator=(CloneTiler const &d) = delete; + + Gtk::CheckButton *_b; + Gtk::CheckButton *_cb_keep_bbox; + GtkWidget *nb; + SPDesktop *desktop; + DesktopTracker deskTrack; + Inkscape::UI::Widget::ColorPicker *color_picker; + GtkSizeGroup* table_row_labels; + Inkscape::UI::Widget::UnitMenu *unit_menu; + + Glib::RefPtr<Gtk::Adjustment> fill_width; + Glib::RefPtr<Gtk::Adjustment> fill_height; + + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection externChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection color_changed_connection; + sigc::connection unitChangedConn; + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + // Variables that used to be set using GObject + GtkWidget *_buttons_on_tiles; + GtkWidget *_dotrace; + GtkWidget *_status; + Gtk::Box *_rowscols; + Gtk::Box *_widthheight; + +}; + + +enum { + TILE_P1, + TILE_P2, + TILE_PM, + TILE_PG, + TILE_CM, + TILE_PMM, + TILE_PMG, + TILE_PGG, + TILE_CMM, + TILE_P4, + TILE_P4M, + TILE_P4G, + TILE_P3, + TILE_P31M, + TILE_P3M1, + TILE_P6, + TILE_P6M +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/color-item.cpp b/src/ui/dialog/color-item.cpp new file mode 100644 index 0000000..acec954 --- /dev/null +++ b/src/ui/dialog/color-item.cpp @@ -0,0 +1,751 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cerrno> + +#include <gtkmm/label.h> +#include <glibmm/i18n.h> + +#include "color-item.h" + +#include "desktop.h" + +#include "desktop-style.h" +#include "display/cairo-utils.h" +#include "document.h" +#include "document-undo.h" +#include "inkscape.h" // for SP_ACTIVE_DESKTOP +#include "io/resource.h" +#include "io/sys.h" +#include "message-context.h" +#include "svg/svg-color.h" +#include "verbs.h" +#include "widgets/gradient-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static std::vector<std::string> mimeStrings; +static std::map<std::string, guint> mimeToInt; + + +#if ENABLE_MAGIC_COLORS +// TODO remove this soon: +extern std::vector<SwatchPage*> possible; +#endif // ENABLE_MAGIC_COLORS + + +#if ENABLE_MAGIC_COLORS +static bool bruteForce( SPDocument* document, Inkscape::XML::Node* node, Glib::ustring const& match, int r, int g, int b ) +{ + bool changed = false; + + if ( node ) { + gchar const * val = node->attribute("inkscape:x-fill-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "fill", c ); + + sp_desktop_apply_css_recursive( obj, css, true ); + static_cast<SPItem*>(obj)->updateRepr(); + + changed = true; + } + + val = node->attribute("inkscape:x-stroke-tag"); + if ( val && (match == val) ) { + SPObject *obj = document->getObjectByRepr( node ); + + gchar c[64] = {0}; + sp_svg_write_color( c, sizeof(c), SP_RGBA32_U_COMPOSE( r, g, b, 0xff ) ); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, "stroke", c ); + + sp_desktop_apply_css_recursive( (SPItem*)obj, css, true ); + ((SPItem*)obj)->updateRepr(); + + changed = true; + } + + Inkscape::XML::Node* first = node->firstChild(); + changed |= bruteForce( document, first, match, r, g, b ); + + changed |= bruteForce( document, node->next(), match, r, g, b ); + } + + return changed; +} +#endif // ENABLE_MAGIC_COLORS + +void +ColorItem::handleClick() { + buttonClicked(false); +} + +void +ColorItem::handleSecondaryClick(gint /*arg1*/) { + buttonClicked(true); +} + +bool +ColorItem::handleEnterNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + gchar* msg = g_strdup_printf(_("Color: <b>%s</b>; <b>Click</b> to set fill, <b>Shift+click</b> to set stroke"), + def.descr.c_str()); + desktop->tipsMessageContext()->set(Inkscape::INFORMATION_MESSAGE, msg); + g_free(msg); + } + + return false; +} + +bool +ColorItem::handleLeaveNotify(GdkEventCrossing* /*event*/) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if ( desktop ) { + desktop->tipsMessageContext()->clear(); + } + + return false; +} + +static void dieDieDie( GObject *obj, gpointer user_data ) +{ + g_message("die die die %p %p", obj, user_data ); +} + +static bool getBlock( std::string& dst, guchar ch, std::string const & str ) +{ + bool good = false; + std::string::size_type pos = str.find(ch); + if ( pos != std::string::npos ) + { + std::string::size_type pos2 = str.find( '(', pos ); + if ( pos2 != std::string::npos ) { + std::string::size_type endPos = str.find( ')', pos2 ); + if ( endPos != std::string::npos ) { + dst = str.substr( pos2 + 1, (endPos - pos2 - 1) ); + good = true; + } + } + } + return good; +} + +static bool popVal( guint64& numVal, std::string& str ) +{ + bool good = false; + std::string::size_type endPos = str.find(','); + if ( endPos == std::string::npos ) { + endPos = str.length(); + } + + if ( endPos != std::string::npos && endPos > 0 ) { + std::string xxx = str.substr( 0, endPos ); + const gchar* ptr = xxx.c_str(); + gchar* endPtr = nullptr; + numVal = g_ascii_strtoull( ptr, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == ptr) ) { + // failed conversion + } else { + good = true; + str.erase( 0, endPos + 1 ); + } + } + + return good; +} + +// TODO resolve this more cleanly: +extern bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data); + +void +ColorItem::drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc) +{ + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + int width = 32; + int height = 24; + + if (def.getType() != ege::PaintDef::RGB){ + GError *error; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, + &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename, width, height, false); + g_free(localFilename); + dc->set_icon(pixbuf, 0, 0); + } else { + Glib::RefPtr<Gdk::Pixbuf> pixbuf; + if (getGradient() ){ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_pattern_t *gradient = getGradient()->create_preview_pattern(width); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, gradient); + cairo_paint(ct); + cairo_destroy(ct); + cairo_pattern_destroy(gradient); + cairo_surface_flush(s); + + pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + } else { + pixbuf = Gdk::Pixbuf::create( Gdk::COLORSPACE_RGB, false, 8, width, height ); + guint32 fillWith = (0xff000000 & (def.getR() << 24)) + | (0x00ff0000 & (def.getG() << 16)) + | (0x0000ff00 & (def.getB() << 8)); + pixbuf->fill( fillWith ); + } + dc->set_icon(pixbuf, 0, 0); + } +} + +//"drag-drop" +// gboolean dragDropColorData( GtkWidget *widget, +// GdkDragContext *drag_context, +// gint x, +// gint y, +// guint time, +// gpointer user_data) +// { +// // TODO finish + +// return TRUE; +// } + + +SwatchPage::SwatchPage() + : _prefWidth(0) +{ +} + +SwatchPage::~SwatchPage() += default; + + +ColorItem::ColorItem(ege::PaintDef::ColorType type) : + def(type), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::ColorItem( unsigned int r, unsigned int g, unsigned int b, Glib::ustring& name ) : + def( r, g, b, name ), + _isFill(false), + _isStroke(false), + _isLive(false), + _linkIsTone(false), + _linkPercent(0), + _linkGray(0), + _linkSrc(nullptr), + _grad(nullptr), + _pattern(nullptr) +{ +} + +ColorItem::~ColorItem() +{ + if (_pattern != nullptr) { + cairo_pattern_destroy(_pattern); + } +} + +ColorItem::ColorItem(ColorItem const &other) : + Inkscape::UI::Previewable() +{ + if ( this != &other ) { + *this = other; + } +} + +ColorItem &ColorItem::operator=(ColorItem const &other) +{ + if ( this != &other ) { + def = other.def; + + // TODO - correct linkage + _linkSrc = other._linkSrc; + g_message("Erk!"); + } + return *this; +} + +void ColorItem::setState( bool fill, bool stroke ) +{ + if ( (_isFill != fill) || (_isStroke != stroke) ) { + _isFill = fill; + _isStroke = stroke; + + for ( auto widget : _previews ) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + + if (preview) { + int val = preview->get_linked(); + val &= ~(UI::Widget::PREVIEW_FILL | UI::Widget::PREVIEW_STROKE); + if ( _isFill ) { + val |= UI::Widget::PREVIEW_FILL; + } + if ( _isStroke ) { + val |= UI::Widget::PREVIEW_STROKE; + } + preview->set_linked(static_cast<UI::Widget::LinkType>(val)); + } + } + } +} + +void ColorItem::setGradient(SPGradient *grad) +{ + if (_grad != grad) { + _grad = grad; + // TODO regen and push to listeners + } + + setName( gr_prepare_label(_grad) ); +} + +void ColorItem::setName(const Glib::ustring name) +{ + //def.descr = name; + + for (auto widget : _previews) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + auto label = dynamic_cast<Gtk::Label *>(widget); + if (preview) { + preview->set_tooltip_text(name); + } + else if (label) { + label->set_text(name); + } + } +} + +void ColorItem::setPattern(cairo_pattern_t *pattern) +{ + if (pattern) { + cairo_pattern_reference(pattern); + } + if (_pattern) { + cairo_pattern_destroy(_pattern); + } + _pattern = pattern; + + _updatePreviews(); +} + +void +ColorItem::_dragGetColorData(const Glib::RefPtr<Gdk::DragContext>& /*drag_context*/, + Gtk::SelectionData &data, + guint info, + guint /*time*/) +{ + std::string key; + if ( info < mimeStrings.size() ) { + key = mimeStrings[info]; + } else { + g_warning("ERROR: unknown value (%d)", info); + } + + if ( !key.empty() ) { + char* tmp = nullptr; + int len = 0; + int format = 0; + def.getMIMEData(key, tmp, len, format); + if ( tmp ) { + data.set(key, format, (guchar*)tmp, len ); + delete[] tmp; + } + } +} + +void ColorItem::_dropDataIn( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData */*data*/, + guint /*info*/, + guint /*event_time*/, + gpointer /*user_data*/) +{ +} + +void ColorItem::_colorDefChanged(void* data) +{ + ColorItem* item = reinterpret_cast<ColorItem*>(data); + if ( item ) { + item->_updatePreviews(); + } +} + +void ColorItem::_updatePreviews() +{ + for (auto widget : _previews) { + auto preview = dynamic_cast<UI::Widget::Preview *>(widget); + if (preview) { + _regenPreview(preview); + preview->queue_draw(); + } + } + + for (auto & _listener : _listeners) { + guint r = def.getR(); + guint g = def.getG(); + guint b = def.getB(); + + if ( _listener->_linkIsTone ) { + r = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * _listener->_linkGray) + ((100 - _listener->_linkPercent) * b) ) / 100; + } else { + r = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * r) ) / 100; + g = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * g) ) / 100; + b = ( (_listener->_linkPercent * 255) + ((100 - _listener->_linkPercent) * b) ) / 100; + } + + _listener->def.setRGB( r, g, b ); + } + + +#if ENABLE_MAGIC_COLORS + // Look for objects using this color + { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if ( desktop ) { + SPDocument* document = desktop->getDocument(); + Inkscape::XML::Node *rroot = document->getReprRoot(); + if ( rroot ) { + + // Find where this thing came from + Glib::ustring paletteName; + bool found = false; + int index = 0; + for ( std::vector<SwatchPage*>::iterator it2 = possible.begin(); it2 != possible.end() && !found; ++it2 ) { + SwatchPage* curr = *it2; + index = 0; + for ( boost::ptr_vector<ColorItem>::iterator zz = curr->_colors.begin(); zz != curr->_colors.end(); ++zz ) { + if ( this == &*zz ) { + found = true; + paletteName = curr->_name; + break; + } else { + index++; + } + } + } + + if ( !paletteName.empty() ) { + gchar* str = g_strdup_printf("%d|", index); + paletteName.insert( 0, str ); + g_free(str); + str = 0; + + if ( bruteForce( document, rroot, paletteName, def.getR(), def.getG(), def.getB() ) ) { + SPDocumentUndo::done( document , SP_VERB_DIALOG_SWATCHES, + _("Change color definition")); + } + } + } + } + } +#endif // ENABLE_MAGIC_COLORS + +} + +void ColorItem::_regenPreview(UI::Widget::Preview * preview) +{ + if ( def.getType() != ege::PaintDef::RGB ) { + using Inkscape::IO::Resource::get_path; + using Inkscape::IO::Resource::PIXMAPS; + using Inkscape::IO::Resource::SYSTEM; + GError *error = nullptr; + gsize bytesRead = 0; + gsize bytesWritten = 0; + gchar *localFilename = + g_filename_from_utf8(get_path(SYSTEM, PIXMAPS, "remove-color.png"), -1, &bytesRead, &bytesWritten, &error); + auto pixbuf = Gdk::Pixbuf::create_from_file(localFilename); + if (!pixbuf) { + g_warning("Null pixbuf for %p [%s]", localFilename, localFilename ); + } + g_free(localFilename); + + preview->set_pixbuf(pixbuf); + } + else if ( !_pattern ){ + preview->set_color((def.getR() << 8) | def.getR(), + (def.getG() << 8) | def.getG(), + (def.getB() << 8) | def.getB() ); + } else { + // These correspond to PREVIEW_PIXBUF_WIDTH and VBLOCK from swatches.cpp + // TODO: the pattern to draw should be in the widget that draws the preview, + // so the preview can be scalable + int w = 128; + int h = 16; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + cairo_t *ct = cairo_create(s); + cairo_set_source(ct, _pattern); + cairo_paint(ct); + cairo_destroy(ct); + cairo_surface_flush(s); + + auto pixbuf = Glib::wrap(ink_pixbuf_create_from_cairo_surface(s)); + preview->set_pixbuf(pixbuf); + } + + preview->set_linked(static_cast<UI::Widget::LinkType>( (_linkSrc ? UI::Widget::PREVIEW_LINK_IN : 0) + | (_listeners.empty() ? 0 : UI::Widget::PREVIEW_LINK_OUT) + | (_isLive ? UI::Widget::PREVIEW_LINK_OTHER:0)) ); +} + +Gtk::Widget* +ColorItem::getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) +{ + Gtk::Widget* widget = nullptr; + if ( style == UI::Widget::PREVIEW_STYLE_BLURB) { + Gtk::Label *lbl = new Gtk::Label(def.descr); + lbl->set_halign(Gtk::ALIGN_START); + lbl->set_valign(Gtk::ALIGN_CENTER); + widget = lbl; + } else { + auto preview = Gtk::manage(new UI::Widget::Preview()); + preview->set_name("ColorItemPreview"); + + _regenPreview(preview); + + preview->set_details((UI::Widget::ViewType)view, + (UI::Widget::PreviewSize)size, + ratio, + border ); + + def.addCallback( _colorDefChanged, this ); + preview->set_focus_on_click(false); + preview->set_tooltip_text(def.descr); + + preview->signal_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleClick)); + preview->signal_alt_clicked().connect(sigc::mem_fun(*this, &ColorItem::handleSecondaryClick)); + preview->signal_button_press_event().connect(sigc::bind(sigc::ptr_fun(&colorItemHandleButtonPress), preview, this)); + + { + auto listing = def.getMIMETypes(); + std::vector<Gtk::TargetEntry> entries; + + for ( auto str : listing ) { + auto target = str.c_str(); + guint flags = 0; + if ( mimeToInt.find(str) == mimeToInt.end() ){ + // these next lines are order-dependent: + mimeToInt[str] = mimeStrings.size(); + mimeStrings.push_back(str); + } + auto info = mimeToInt[target]; + Gtk::TargetEntry entry(target, (Gtk::TargetFlags)flags, info); + entries.push_back(entry); + } + + preview->drag_source_set(entries, Gdk::BUTTON1_MASK, + Gdk::DragAction(Gdk::ACTION_MOVE | Gdk::ACTION_COPY) ); + } + + preview->signal_drag_data_get().connect(sigc::mem_fun(*this, &ColorItem::_dragGetColorData)); + preview->signal_drag_begin().connect(sigc::mem_fun(*this, &ColorItem::drag_begin)); + preview->signal_enter_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleEnterNotify)); + preview->signal_leave_notify_event().connect(sigc::mem_fun(*this, &ColorItem::handleLeaveNotify)); + + widget = preview; + } + + _previews.push_back( widget ); + + return widget; +} + +void ColorItem::buttonClicked(bool secondary) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + char const * attrName = secondary ? "stroke" : "fill"; + + SPCSSAttr *css = sp_repr_css_attr_new(); + Glib::ustring descr; + switch (def.getType()) { + case ege::PaintDef::CLEAR: { + // TODO actually make this clear + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Remove stroke color") : _("Remove fill color"); + break; + } + case ege::PaintDef::NONE: { + sp_repr_css_set_property( css, attrName, "none" ); + descr = secondary? _("Set stroke color to none") : _("Set fill color to none"); + break; + } +//mark + case ege::PaintDef::RGB: { + Glib::ustring colorspec; + if ( _grad ){ + colorspec = "url(#"; + colorspec += _grad->getId(); + colorspec += ")"; + } else { + gchar c[64]; + guint32 rgba = (def.getR() << 24) | (def.getG() << 16) | (def.getB() << 8) | 0xff; + sp_svg_write_color(c, sizeof(c), rgba); + colorspec = c; + } +//end mark + sp_repr_css_set_property( css, attrName, colorspec.c_str() ); + descr = secondary? _("Set stroke color from swatch") : _("Set fill color from swatch"); + break; + } + } + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + + DocumentUndo::done( desktop->getDocument(), SP_VERB_DIALOG_SWATCHES, descr.c_str() ); + } +} + +void ColorItem::_wireMagicColors( SwatchPage *colorSet ) +{ + if ( colorSet ) + { + for ( boost::ptr_vector<ColorItem>::iterator it = colorSet->_colors.begin(); it != colorSet->_colors.end(); ++it ) + { + std::string::size_type pos = it->def.descr.find("*{"); + if ( pos != std::string::npos ) + { + std::string subby = it->def.descr.substr( pos + 2 ); + std::string::size_type endPos = subby.find("}*"); + if ( endPos != std::string::npos ) + { + subby.erase( endPos ); + //g_message("FOUND MAGIC at '%s'", (*it)->def.descr.c_str()); + //g_message(" '%s'", subby.c_str()); + + if ( subby.find('E') != std::string::npos ) + { + it->def.setEditable( true ); + } + + if ( subby.find('L') != std::string::npos ) + { + it->_isLive = true; + } + + std::string part; + // Tint. index + 1 more val. + if ( getBlock( part, 'T', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + it->_linkTint( colorSet->_colors[colorIndex], percent ); + } + } + } + + // Shade/tone. index + 1 or 2 more val. + if ( getBlock( part, 'S', subby ) ) { + guint64 colorIndex = 0; + if ( popVal( colorIndex, part ) ) { + guint64 percent = 0; + if ( popVal( percent, part ) ) { + guint64 grayLevel = 0; + if ( !popVal( grayLevel, part ) ) { + grayLevel = 0; + } + it->_linkTone( colorSet->_colors[colorIndex], percent, grayLevel ); + } + } + } + + } + } + } + } +} + + +void ColorItem::_linkTint( ColorItem& other, int percent ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = false; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = 0; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +void ColorItem::_linkTone( ColorItem& other, int percent, int grayLevel ) +{ + if ( !_linkSrc ) + { + other._listeners.push_back(this); + _linkIsTone = true; + _linkPercent = percent; + if ( _linkPercent > 100 ) + _linkPercent = 100; + if ( _linkPercent < 0 ) + _linkPercent = 0; + _linkGray = grayLevel; + _linkSrc = &other; + + ColorItem::_colorDefChanged(&other); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/color-item.h b/src/ui/dialog/color-item.h new file mode 100644 index 0000000..7210ed2 --- /dev/null +++ b/src/ui/dialog/color-item.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape color swatch UI item. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_COLOR_ITEM_H +#define SEEN_DIALOGS_COLOR_ITEM_H + +#include <boost/ptr_container/ptr_vector.hpp> + +#include "widgets/ege-paint-def.h" +#include "ui/previewable.h" + +class SPGradient; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ColorItem; + +class SwatchPage +{ +public: + SwatchPage(); + ~SwatchPage(); + + Glib::ustring _name; + int _prefWidth; + boost::ptr_vector<ColorItem> _colors; +}; + + +/** + * The color swatch you see on screen as a clickable box. + */ +class ColorItem : public Inkscape::UI::Previewable +{ + friend void _loadPaletteFile( gchar const *filename ); +public: + ColorItem( ege::PaintDef::ColorType type ); + ColorItem( unsigned int r, unsigned int g, unsigned int b, + Glib::ustring& name ); + ~ColorItem() override; + ColorItem(ColorItem const &other); + virtual ColorItem &operator=(ColorItem const &other); + Gtk::Widget* getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) override; + void buttonClicked(bool secondary = false); + + void setGradient(SPGradient *grad); + SPGradient * getGradient() const { return _grad; } + void setPattern(cairo_pattern_t *pattern); + void setName(const Glib::ustring name); + + void setState( bool fill, bool stroke ); + bool isFill() { return _isFill; } + bool isStroke() { return _isStroke; } + + ege::PaintDef def; + +private: + + static void _dropDataIn( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data); + + void _dragGetColorData(const Glib::RefPtr<Gdk::DragContext> &drag_context, + Gtk::SelectionData &data, + guint info, + guint time); + + static void _wireMagicColors( SwatchPage *colorSet ); + static void _colorDefChanged(void* data); + + void _updatePreviews(); + void _regenPreview(UI::Widget::Preview * preview); + + void _linkTint( ColorItem& other, int percent ); + void _linkTone( ColorItem& other, int percent, int grayLevel ); + void drag_begin(const Glib::RefPtr<Gdk::DragContext> &dc); + void handleClick(); + void handleSecondaryClick(gint arg1); + bool handleEnterNotify(GdkEventCrossing* event); + bool handleLeaveNotify(GdkEventCrossing* event); + + std::vector<Gtk::Widget*> _previews; + + bool _isFill; + bool _isStroke; + bool _isLive; + bool _linkIsTone; + int _linkPercent; + int _linkGray; + ColorItem* _linkSrc; + SPGradient* _grad; + cairo_pattern_t *_pattern; + std::vector<ColorItem*> _listeners; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_COLOR_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/debug.cpp b/src/ui/dialog/debug.cpp new file mode 100644 index 0000000..af2f08b --- /dev/null +++ b/src/ui/dialog/debug.cpp @@ -0,0 +1,259 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dialog that displays log messages. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/dialog.h> +#include <gtkmm/textview.h> +#include <gtkmm/menubar.h> +#include <gtkmm/scrolledwindow.h> +#include <glibmm/i18n.h> + +#include "debug.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A very simple dialog for displaying Inkscape messages - implementation. + */ +class DebugDialogImpl : public DebugDialog, public Gtk::Dialog +{ +public: + DebugDialogImpl(); + ~DebugDialogImpl() override; + + void show() override; + void hide() override; + void clear() override; + void message(char const *msg) override; + void captureLogMessages() override; + void releaseLogMessages() override; + +private: + Gtk::MenuBar menuBar; + Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; +}; + +void DebugDialogImpl::clear() +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + +DebugDialogImpl::DebugDialogImpl() +{ + set_title(_("Messages")); + set_size_request(300, 400); + auto mainVBox = get_content_area(); + + //## Add a menu for clear() + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem(_("_File"), true)); + item->set_submenu(fileMenu); + menuBar.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("_Clear"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::clear)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Capture log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::captureLogMessages)); + fileMenu.append(*item); + + item = Gtk::manage(new Gtk::MenuItem(_("Release log messages"))); + item->signal_activate().connect(sigc::mem_fun(*this, &DebugDialogImpl::releaseLogMessages)); + fileMenu.append(*item); + + mainVBox->pack_start(menuBar, Gtk::PACK_SHRINK); + + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + mainVBox->pack_start(textScroll); + + show_all_children(); + + message("ready."); + message("enable log display by setting "); + message("dialogs.debug 'redirect' attribute to 1 in preferences.xml"); + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; +} + + +DebugDialog *DebugDialog::create() +{ + DebugDialog *dialog = new DebugDialogImpl(); + return dialog; +} + +DebugDialogImpl::~DebugDialogImpl() += default; + +void DebugDialogImpl::show() +{ + //call super() + Gtk::Dialog::show(); + //sp_transientize(GTK_WIDGET(gobj())); //Make transient + raise(); + Gtk::Dialog::present(); +} + +void DebugDialogImpl::hide() +{ + // call super + Gtk::Dialog::hide(); +} + +void DebugDialogImpl::message(char const *msg) +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +/* static instance, to reduce dependencies */ +static DebugDialog *debugDialogInstance = nullptr; + +DebugDialog *DebugDialog::getInstance() +{ + if (!debugDialogInstance) { + debugDialogInstance = new DebugDialogImpl(); + } + return debugDialogInstance; +} + + + +void DebugDialog::showInstance() +{ + DebugDialog *debugDialog = getInstance(); + debugDialog->show(); + // this is not a real memleak because getInstance() only creates a debug dialog once, and returns that instance for all subsequent calls + // cppcheck-suppress memleak +} + + + + +/*##### THIS IS THE IMPORTANT PART ##### */ +static void dialogLoggingFunction(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + DebugDialogImpl *dlg = static_cast<DebugDialogImpl *>(user_data); + dlg->message(messageText); +} + + +void DebugDialogImpl::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingFunction, (gpointer)this); + } + message("log capture started"); +} + +void DebugDialogImpl::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message("log capture discontinued"); +} + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/debug.h b/src/ui/dialog/debug.h new file mode 100644 index 0000000..4520c73 --- /dev/null +++ b/src/ui/dialog/debug.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for displaying Inkscape messages + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOGS_DEBUGDIALOG_H +#define SEEN_UI_DIALOGS_DEBUGDIALOG_H + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * @brief A very simple dialog for displaying Inkscape messages. + * + * Messages sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +class DebugDialog +{ +public: + DebugDialog() = default;; + /** + * Factory method + */ + static DebugDialog *create(); + + /** + * Destructor + */ + virtual ~DebugDialog() = default;; + + + /** + * Show the dialog + */ + virtual void show() = 0; + + /** + * Do not show the dialog + */ + virtual void hide() = 0; + + /** + * @brief Clear all information from the dialog + * + * Also a public method. Remove all text from the dialog + */ + virtual void clear() = 0; + + /** + * Display a message + */ + virtual void message(char const *msg) = 0; + + /** + * Redirect g_log() messages to this widget + */ + virtual void captureLogMessages() = 0; + + /** + * Return g_log() messages to normal handling + */ + virtual void releaseLogMessages() = 0; + + /** + * Factory method. Use this to create a new DebugDialog + */ + static DebugDialog *getInstance(); + + /** + * Show the instance above + */ + static void showInstance(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif /* __DEBUGDIALOG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/desktop-tracker.cpp b/src/ui/dialog/desktop-tracker.cpp new file mode 100644 index 0000000..67f2cef --- /dev/null +++ b/src/ui/dialog/desktop-tracker.cpp @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widgets/desktop-widget.h" + +#include "desktop-tracker.h" + +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +DesktopTracker::DesktopTracker() : + base(nullptr), + desktop(nullptr), + widget(nullptr), + hierID(0), + trackActive(false), + desktopChangedSig() +{ +} + +DesktopTracker::~DesktopTracker() +{ + disconnect(); +} + +void DesktopTracker::connect(GtkWidget *widget) +{ + disconnect(); + + this->widget = widget; + + // Use C/gobject callbacks to avoid gtkmm rewrap-during-destruct issues: + hierID = g_signal_connect( G_OBJECT(widget), "hierarchy-changed", G_CALLBACK(hierarchyChangeCB), this ); + inkID = INKSCAPE.signal_activate_desktop.connect( + sigc::bind( + sigc::ptr_fun(&DesktopTracker::activateDesktopCB), this) + ); + + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } +} + +void DesktopTracker::disconnect() +{ + if (hierID) { + if (widget) { + g_signal_handler_disconnect(G_OBJECT(widget), hierID); + } + hierID = 0; + } + if (inkID.connected()) { + inkID.disconnect(); + } +} + +void DesktopTracker::setBase(SPDesktop *desktop) +{ + if (this->base != desktop) { + base = desktop; + // Do not override an existing target desktop + if (!this->desktop) { + setDesktop(desktop); + } + } +} + +SPDesktop *DesktopTracker::getBase() const +{ + return base; +} + +SPDesktop *DesktopTracker::getDesktop() const +{ + return desktop; +} + +sigc::connection DesktopTracker::connectDesktopChanged( const sigc::slot<void, SPDesktop*> & slot ) +{ + return desktopChangedSig.connect(slot); +} + +void DesktopTracker::activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ) +{ + if (self && self->trackActive) { + self->setDesktop(desktop); + } + //return FALSE; +} + +bool DesktopTracker::hierarchyChangeCB(GtkWidget * /*widget*/, GtkWidget* /*prev*/, DesktopTracker *self) +{ + if (self) { + self->handleHierarchyChange(); + } + return false; +} + +void DesktopTracker::handleHierarchyChange() +{ + GtkWidget *wdgt = gtk_widget_get_ancestor(widget, SP_TYPE_DESKTOP_WIDGET); + bool newFlag = (wdgt == nullptr); // true means not in an SPDesktopWidget, thus floating. + if (wdgt && !base) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if (dtw && dtw->desktop) { + setBase(dtw->desktop); // may also set desktop + } + } + if (newFlag != trackActive) { + trackActive = newFlag; + if (trackActive) { + setDesktop(SP_ACTIVE_DESKTOP); + } else if (desktop != base) { + setDesktop(getBase()); + } + } +} + +void DesktopTracker::setDesktop(SPDesktop *desktop) +{ + if (desktop != this->desktop) { + this->desktop = desktop; + desktopChangedSig.emit(desktop); + } +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/desktop-tracker.h b/src/ui/dialog/desktop-tracker.h new file mode 100644 index 0000000..7b94390 --- /dev/null +++ b/src/ui/dialog/desktop-tracker.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOG_DESKTOP_TRACKER +#define SEEN_DIALOG_DESKTOP_TRACKER + +#include <cstddef> +#include <sigc++/connection.h> + +typedef struct _GtkWidget GtkWidget; +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { + +namespace UI { +namespace Dialog { + +class DesktopTracker +{ +public: + DesktopTracker(); + virtual ~DesktopTracker(); + + void connect(GtkWidget *widget); + void disconnect(); + + SPDesktop *getDesktop() const; + + void setBase(SPDesktop *desktop); + SPDesktop *getBase() const; + + sigc::connection connectDesktopChanged( const sigc::slot<void, SPDesktop*> & slot ); + +private: + static void activateDesktopCB(SPDesktop *desktop, DesktopTracker *self ); + static bool hierarchyChangeCB(GtkWidget *widget, GtkWidget* prev, DesktopTracker *self); + + void handleHierarchyChange(); + void setDesktop(SPDesktop *desktop); + + SPDesktop *base; + SPDesktop *desktop; + GtkWidget *widget; + unsigned long hierID; + sigc::connection inkID; + bool trackActive; + sigc::signal<void, SPDesktop*> desktopChangedSig; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOG_DESKTOP_TRACKER +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-manager.cpp b/src/ui/dialog/dialog-manager.cpp new file mode 100644 index 0000000..02bb270 --- /dev/null +++ b/src/ui/dialog/dialog-manager.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Jon Phillips <jon@rejon.org> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/dialog-manager.h" + +#include "style.h" +#include "ui/dialog/align-and-distribute.h" +#include "ui/dialog/document-metadata.h" +#include "ui/dialog/document-properties.h" +#include "ui/dialog/extension-editor.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/dialog/filter-editor.h" +#include "ui/dialog/filter-effects-dialog.h" +#include "ui/dialog/find.h" +#include "ui/dialog/glyphs.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/dialog/input.h" +#include "ui/dialog/livepatheffect-editor.h" +#include "ui/dialog/memory.h" +#include "ui/dialog/messages.h" +#include "ui/dialog/paint-servers.h" +#include "ui/dialog/symbols.h" +#include "ui/dialog/tile.h" +# include "ui/dialog/tracedialog.h" + +#include "ui/dialog/transformation.h" +#include "ui/dialog/undo-history.h" +#include "ui/dialog/panel-dialog.h" +#include "ui/dialog/layers.h" +#include "ui/dialog/icon-preview.h" +//#include "ui/dialog/print-colors-preview-dialog.h" +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/export.h" +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/object-properties.h" +#include "ui/dialog/objects.h" +#include "ui/dialog/selectorsdialog.h" + +#if HAVE_ASPELL +# include "ui/dialog/spellcheck.h" +#endif + +#include "ui/dialog/tags.h" + +#include "ui/dialog/styledialog.h" +#include "ui/dialog/svg-fonts-dialog.h" +#include "ui/dialog/text-edit.h" +#include "ui/dialog/xml-tree.h" +#include "util/ege-appear-time-tracker.h" +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +using namespace Behavior; + +template <typename T, typename B> +inline Dialog *create() { return PanelDialog<B>::template create<T>(); } + +} + +/** + * This class is provided as a container for Inkscape's various + * dialogs. This allows InkscapeApplication to treat the various + * dialogs it invokes, as abstractions. + * + * DialogManager is essentially a cache of dialogs. It lets us + * initialize dialogs lazily - instead of constructing them during + * application startup, they're constructed the first time they're + * actually invoked by InkscapeApplication. The constructed + * dialog is held here after that, so future invocations of the + * dialog don't need to get re-constructed each time. The memory for + * the dialogs are then reclaimed when the DialogManager is destroyed. + * + * In addition, DialogManager also serves as a signal manager for + * dialogs. It provides a set of signals that can be sent to all + * dialogs for doing things such as hiding/unhiding them, etc. + * DialogManager ensures that every dialog it handles will listen + * to these signals. + * + */ +DialogManager::DialogManager() { + + using namespace Behavior; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + // The preferences dialog is broken, the DockBehavior code resizes it's floating window to the smallest size + registerFactory("InkscapePreferences", &create<InkscapePreferences, FloatingBehavior>); + + if (dialogs_type == FLOATING) { + registerFactory("AlignAndDistribute", &create<AlignAndDistribute, FloatingBehavior>); + registerFactory("DocumentMetadata", &create<DocumentMetadata, FloatingBehavior>); + registerFactory("DocumentProperties", &create<DocumentProperties, FloatingBehavior>); + registerFactory("ExtensionEditor", &create<ExtensionEditor, FloatingBehavior>); + registerFactory("FillAndStroke", &create<FillAndStroke, FloatingBehavior>); + registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, FloatingBehavior>); + registerFactory("FilterEditorDialog", &create<FilterEditorDialog, FloatingBehavior>); + registerFactory("Find", &create<Find, FloatingBehavior>); + registerFactory("Glyphs", &create<GlyphsPanel, FloatingBehavior>); + registerFactory("IconPreviewPanel", &create<IconPreviewPanel, FloatingBehavior>); + registerFactory("LayersPanel", &create<LayersPanel, FloatingBehavior>); + registerFactory("ObjectsPanel", &create<ObjectsPanel, FloatingBehavior>); + registerFactory("TagsPanel", &create<TagsPanel, FloatingBehavior>); + registerFactory("LivePathEffect", &create<LivePathEffectEditor, FloatingBehavior>); + registerFactory("Memory", &create<Memory, FloatingBehavior>); + registerFactory("Messages", &create<Messages, FloatingBehavior>); + registerFactory("ObjectAttributes", &create<ObjectAttributes, FloatingBehavior>); + registerFactory("ObjectProperties", &create<ObjectProperties, FloatingBehavior>); +// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, FloatingBehavior>); + registerFactory("SvgFontsDialog", &create<SvgFontsDialog, FloatingBehavior>); + registerFactory("Swatches", &create<SwatchesPanel, FloatingBehavior>); + registerFactory("TileDialog", &create<ArrangeDialog, FloatingBehavior>); + registerFactory("Symbols", &create<SymbolsDialog, FloatingBehavior>); + registerFactory("PaintServers", &create<PaintServersDialog, FloatingBehavior>); + registerFactory("StyleDialog", &create<StyleDialog, FloatingBehavior>); + registerFactory("Trace", &create<TraceDialog, FloatingBehavior>); + + registerFactory("Transformation", &create<Transformation, FloatingBehavior>); + registerFactory("UndoHistory", &create<UndoHistory, FloatingBehavior>); + registerFactory("InputDevices", &create<InputDialog, FloatingBehavior>); + registerFactory("TextFont", &create<TextEdit, FloatingBehavior>); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create<SpellCheck, FloatingBehavior>); +#endif + + registerFactory("Export", &create<Export, FloatingBehavior>); + registerFactory("CloneTiler", &create<CloneTiler, FloatingBehavior>); + registerFactory("XmlTree", &create<XmlTree, FloatingBehavior>); + registerFactory("Selectors", &create<SelectorsDialog, FloatingBehavior>); + + } else { + + registerFactory("AlignAndDistribute", &create<AlignAndDistribute, DockBehavior>); + registerFactory("DocumentMetadata", &create<DocumentMetadata, DockBehavior>); + registerFactory("DocumentProperties", &create<DocumentProperties, DockBehavior>); + registerFactory("ExtensionEditor", &create<ExtensionEditor, DockBehavior>); + registerFactory("FillAndStroke", &create<FillAndStroke, DockBehavior>); + registerFactory("FilterEffectsDialog", &create<FilterEffectsDialog, DockBehavior>); + registerFactory("FilterEditorDialog", &create<FilterEditorDialog, DockBehavior>); + registerFactory("Find", &create<Find, DockBehavior>); + registerFactory("Glyphs", &create<GlyphsPanel, DockBehavior>); + registerFactory("IconPreviewPanel", &create<IconPreviewPanel, DockBehavior>); + registerFactory("LayersPanel", &create<LayersPanel, DockBehavior>); + registerFactory("ObjectsPanel", &create<ObjectsPanel, DockBehavior>); + registerFactory("TagsPanel", &create<TagsPanel, DockBehavior>); + registerFactory("LivePathEffect", &create<LivePathEffectEditor, DockBehavior>); + registerFactory("Memory", &create<Memory, DockBehavior>); + registerFactory("Messages", &create<Messages, DockBehavior>); + registerFactory("ObjectAttributes", &create<ObjectAttributes, DockBehavior>); + registerFactory("ObjectProperties", &create<ObjectProperties, DockBehavior>); +// registerFactory("PrintColorsPreviewDialog", &create<PrintColorsPreviewDialog, DockBehavior>); + registerFactory("SvgFontsDialog", &create<SvgFontsDialog, DockBehavior>); + registerFactory("Swatches", &create<SwatchesPanel, DockBehavior>); + registerFactory("TileDialog", &create<ArrangeDialog, DockBehavior>); + registerFactory("Symbols", &create<SymbolsDialog, DockBehavior>); + registerFactory("PaintServers", &create<PaintServersDialog, DockBehavior>); + registerFactory("Trace", &create<TraceDialog, DockBehavior>); + + registerFactory("Transformation", &create<Transformation, DockBehavior>); + registerFactory("UndoHistory", &create<UndoHistory, DockBehavior>); + registerFactory("InputDevices", &create<InputDialog, DockBehavior>); + registerFactory("TextFont", &create<TextEdit, DockBehavior>); + +#if HAVE_ASPELL + registerFactory("SpellCheck", &create<SpellCheck, DockBehavior>); +#endif + + registerFactory("Export", &create<Export, DockBehavior>); + registerFactory("CloneTiler", &create<CloneTiler, DockBehavior>); + registerFactory("XmlTree", &create<XmlTree, DockBehavior>); + registerFactory("Selectors", &create<SelectorsDialog, DockBehavior>); + } +} + +DialogManager::~DialogManager() { + // TODO: Disconnect the signals + // TODO: Do we need to explicitly delete the dialogs? + // Appears to cause a segfault if we do +} + + +DialogManager &DialogManager::getInstance() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int dialogs_type = prefs->getIntLimited("/options/dialogtype/value", DOCK, 0, 1); + + /* Use singleton behavior for floating dialogs */ + if (dialogs_type == FLOATING) { + static DialogManager *instance = nullptr; + + if (!instance) + instance = new DialogManager(); + return *instance; + } + + return *new DialogManager(); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(gchar const *name, + DialogManager::DialogFactory factory) +{ + registerFactory(g_quark_from_string(name), factory); +} + +/** + * Registers a dialog factory function used to create the named dialog. + */ +void DialogManager::registerFactory(GQuark name, + DialogManager::DialogFactory factory) +{ + _factory_map[name] = factory; +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(gchar const *name) { + return getDialog(g_quark_from_string(name)); +} + +/** + * Fetches the named dialog, creating it if it has not already been + * created (assuming a factory has been registered for it). + */ +Dialog *DialogManager::getDialog(GQuark name) { + DialogMap::iterator dialog_found; + dialog_found = _dialog_map.find(name); + + Dialog *dialog=nullptr; + if ( dialog_found != _dialog_map.end() ) { + dialog = dialog_found->second; + } else { + FactoryMap::iterator factory_found; + factory_found = _factory_map.find(name); + + if ( factory_found != _factory_map.end() ) { + dialog = factory_found->second(); + _dialog_map[name] = dialog; + } + } + + return dialog; +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(gchar const *name, bool grabfocus) { + showDialog(g_quark_from_string(name), grabfocus); +} + +/** + * Shows the named dialog, creating it if necessary. + */ +void DialogManager::showDialog(GQuark name, bool /*grabfocus*/) { + bool wantTiming = Inkscape::Preferences::get()->getBool("/dialogs/debug/trackAppear", false); + GTimer *timer = (wantTiming) ? g_timer_new() : nullptr; // if needed, must be created/started before getDialog() + Dialog *dialog = getDialog(name); + if ( dialog ) { + if ( wantTiming ) { + gchar const * nameStr = g_quark_to_string(name); + ege::AppearTimeTracker *tracker = new ege::AppearTimeTracker(timer, dialog->gobj(), nameStr); + tracker->setAutodelete(true); + timer = nullptr; + } + // should check for grabfocus, but lp:1348927 prevents it + dialog->present(); + } + + if ( timer ) { + g_timer_destroy(timer); + timer = nullptr; + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog-manager.h b/src/ui/dialog/dialog-manager.h new file mode 100644 index 0000000..5b22189 --- /dev/null +++ b/src/ui/dialog/dialog-manager.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Object for managing a set of dialogs, including their signals and + * construction/caching/destruction of them. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Jon Phillips <jon@rejon.org> + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MANAGER_H +#define INKSCAPE_UI_DIALOG_MANAGER_H + +#include "dialog.h" +#include <map> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class DialogManager { +public: + typedef Dialog *(*DialogFactory)(); + + DialogManager(); + virtual ~DialogManager(); + + static DialogManager &getInstance(); + + // sigc::signal<void> show_dialogs; + // sigc::signal<void> show_f12; + // sigc::signal<void> hide_dialogs; + // sigc::signal<void> hide_f12; + // sigc::signal<void> transientize; + + /* generic dialog management start */ + typedef std::map<GQuark, DialogFactory> FactoryMap; + typedef std::map<GQuark, Dialog*> DialogMap; + + void registerFactory(gchar const *name, DialogFactory factory); + void registerFactory(GQuark name, DialogFactory factory); + Dialog *getDialog(gchar const* dlgName); + Dialog *getDialog(GQuark dlgName); + void showDialog(gchar const *name, bool grabfocus=true); + void showDialog(GQuark name, bool grabfocus=true); + +protected: + DialogManager(DialogManager const &d); // no copy + DialogManager& operator=(DialogManager const &d); // no assign + + FactoryMap _factory_map; //< factories to create dialogs + DialogMap _dialog_map; //< map of already created dialogs +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_MANAGER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog.cpp b/src/ui/dialog/dialog.cpp new file mode 100644 index 0000000..cf94097 --- /dev/null +++ b/src/ui/dialog/dialog.cpp @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Base class for dialogs in Inkscape - implementation. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * buliabyak@gmail.com + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dialog-manager.h" +#include <gtkmm/dialog.h> + +#include <gdk/gdkkeysyms.h> + +#include <utility> + +#include "inkscape.h" +#include "ui/monitor.h" +#include "ui/tools/tool-base.h" +#include "desktop.h" + +#include "shortcuts.h" +#include "ui/interface.h" +#include "verbs.h" +#include "ui/tool/event-utils.h" + +#define MIN_ONSCREEN_DISTANCE 50 + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +gboolean sp_retransientize_again(gpointer dlgPtr) +{ + Dialog *dlg = static_cast<Dialog *>(dlgPtr); + dlg->retransientize_suppress = false; + return FALSE; // so that it is only called once +} + +//===================================================================== + +Dialog::Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path, int verb_num, + Glib::ustring apply_label) + : _user_hidden(false), + _hiddenF12(false), + retransientize_suppress(false), + _prefs_path(prefs_path), + _verb_num(verb_num), + _title(), + _apply_label(std::move(apply_label)), + _desktop(nullptr), + _is_active_desktop(true), + _behavior(nullptr) +{ + gchar title[500]; + + if (verb_num) { + sp_ui_dialog_title_string (Inkscape::Verb::get(verb_num), title); + } + + _title = title; + _behavior = behavior_factory(*this); + _desktop = SP_ACTIVE_DESKTOP; + + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(Glib::wrap(_behavior->gobj())); + INKSCAPE.signal_activate_desktop.connect(sigc::mem_fun(*this, &Dialog::onDesktopActivated)); + INKSCAPE.signal_dialogs_hide.connect(sigc::mem_fun(*this, &Dialog::onHideF12)); + INKSCAPE.signal_dialogs_unhide.connect(sigc::mem_fun(*this, &Dialog::onShowF12)); + INKSCAPE.signal_shut_down.connect(sigc::mem_fun(*this, &Dialog::onShutdown)); + INKSCAPE.signal_change_theme.connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes), widg)); + + Glib::wrap(gobj())->signal_event().connect(sigc::mem_fun(*this, &Dialog::_onEvent)); + Glib::wrap(gobj())->signal_key_press_event().connect(sigc::mem_fun(*this, &Dialog::_onKeyPress)); + + read_geometry(); + sp_add_top_window_classes(widg); +} + +Dialog::~Dialog() +{ + save_geometry(); + delete _behavior; + _behavior = nullptr; +} + + +//--------------------------------------------------------------------- + + +void Dialog::onDesktopActivated(SPDesktop *desktop) +{ + _is_active_desktop = (desktop == _desktop); + _behavior->onDesktopActivated(desktop); +} + +void Dialog::onShutdown() +{ + save_geometry(); + //_user_hidden = true; + _behavior->onShutdown(); +} + +void Dialog::onHideF12() +{ + _hiddenF12 = true; + _behavior->onHideF12(); +} + +void Dialog::onShowF12() +{ + if (_user_hidden) + return; + + if (_hiddenF12) { + _behavior->onShowF12(); + } + + _hiddenF12 = false; +} + + +inline Dialog::operator Gtk::Widget &() { return *_behavior; } +inline GtkWidget *Dialog::gobj() { return _behavior->gobj(); } +inline void Dialog::present() { _behavior->present(); } +inline Gtk::Box *Dialog::get_vbox() { return _behavior->get_vbox(); } +inline void Dialog::hide() { _behavior->hide(); } +inline void Dialog::show() { _behavior->show(); } +inline void Dialog::show_all_children() { _behavior->show_all_children(); } +inline void Dialog::set_size_request(int width, int height) { _behavior->set_size_request(width, height); } +inline void Dialog::size_request(Gtk::Requisition &requisition) { _behavior->size_request(requisition); } +inline void Dialog::get_position(int &x, int &y) { _behavior->get_position(x, y); } +inline void Dialog::get_size(int &width, int &height) { _behavior->get_size(width, height); } +inline void Dialog::resize(int width, int height) { _behavior->resize(width, height); } +inline void Dialog::move(int x, int y) { _behavior->move(x, y); } +inline void Dialog::set_position(Gtk::WindowPosition position) { _behavior->set_position(position); } +inline void Dialog::set_title(Glib::ustring title) { _behavior->set_title(title); } +inline void Dialog::set_sensitive(bool sensitive) { _behavior->set_sensitive(sensitive); } + +Glib::SignalProxy0<void> Dialog::signal_show() { return _behavior->signal_show(); } +Glib::SignalProxy0<void> Dialog::signal_hide() { return _behavior->signal_hide(); } + +void Dialog::read_geometry() +{ + _user_hidden = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int x = prefs->getInt(_prefs_path + "/x", -1000); + int y = prefs->getInt(_prefs_path + "/y", -1000); + int w = prefs->getInt(_prefs_path + "/w", 0); + int h = prefs->getInt(_prefs_path + "/h", 0); + + // g_print ("read %d %d %d %d\n", x, y, w, h); + + // If there are stored height and width values for the dialog, + // resize the window to match; otherwise we leave it at its default + if (w != 0 && h != 0) { + resize(w, h); + } + + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + auto const screen_width = monitor_geometry.get_width(); + auto const screen_height = monitor_geometry.get_height(); + + // If there are stored values for where the dialog should be + // located, then restore the dialog to that position. + // also check if (x,y) is actually onscreen with the current screen dimensions + if ( (x >= 0) && (y >= 0) && (x < (screen_width-MIN_ONSCREEN_DISTANCE)) && (y < (screen_height-MIN_ONSCREEN_DISTANCE)) ) { + move(x, y); + } else { + // ...otherwise just put it in the middle of the screen + set_position(Gtk::WIN_POS_CENTER); + } + +} + + +void Dialog::save_geometry() +{ + int y, x, w, h; + + get_position(x, y); + get_size(w, h); + + // g_print ("write %d %d %d %d\n", x, y, w, h); + + if (x<0) x=0; + if (y<0) y=0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(_prefs_path + "/x", x); + prefs->setInt(_prefs_path + "/y", y); + prefs->setInt(_prefs_path + "/w", w); + prefs->setInt(_prefs_path + "/h", h); + +} + +void +Dialog::save_status(int visible, int state, int placement) +{ + // Only save dialog status for dialogs on the "last document" + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop != nullptr || !_is_active_desktop ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs) { + prefs->setInt(_prefs_path + "/visible", visible); + prefs->setInt(_prefs_path + "/state", state); + prefs->setInt(_prefs_path + "/placement", placement); + } +} + + +void Dialog::_handleResponse(int response_id) +{ + switch (response_id) { + case Gtk::RESPONSE_CLOSE: { + _close(); + break; + } + } +} + +bool Dialog::_onDeleteEvent(GdkEventAny */*event*/) +{ + save_geometry(); + _user_hidden = true; + + return false; +} + +bool Dialog::_onEvent(GdkEvent *event) +{ + bool ret = false; + + switch (event->type) { + case GDK_KEY_PRESS: { + switch (Inkscape::UI::Tools::get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: { + _defocus(); + ret = true; + break; + } + case GDK_KEY_F4: + case GDK_KEY_w: + case GDK_KEY_W: { + if (Inkscape::UI::held_only_control(event->key)) { + _close(); + ret = true; + } + break; + } + default: { // pass keypress to the canvas + break; + } + } + } + default: + ; + } + + return ret; +} + +bool Dialog::_onKeyPress(GdkEventKey *event) +{ + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event((GdkEventKey*)event); + return sp_shortcut_invoke(shortcut, SP_ACTIVE_DESKTOP); +} + +void Dialog::_apply() +{ + g_warning("Apply button clicked for dialog [Dialog::_apply()]"); +} + +void Dialog::_close() +{ + _behavior->hide(); + _onDeleteEvent(nullptr); +} + +void Dialog::_defocus() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(desktop->canvas)); + + // make sure the canvas window is present before giving it focus + Gtk::Window *toplevel_window = dynamic_cast<Gtk::Window *>(canvas->get_toplevel()); + if (toplevel_window) + toplevel_window->present(); + + canvas->grab_focus(); + } +} + +Inkscape::Selection* +Dialog::_getSelection() +{ + return SP_ACTIVE_DESKTOP->getSelection(); +} + +void sp_add_top_window_classes_callback(Gtk::Widget *widg) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Gtk::Widget *canvas = Glib::wrap(GTK_WIDGET(desktop->canvas)); + Gtk::Window *toplevel_window = dynamic_cast<Gtk::Window *>(canvas->get_toplevel()); + if (toplevel_window) { + Gtk::Window *current_window = dynamic_cast<Gtk::Window *>(widg); + if (!current_window) { + current_window = dynamic_cast<Gtk::Window *>(widg->get_toplevel()); + } + if (current_window) { + if (toplevel_window->get_style_context()->has_class("dark")) { + current_window->get_style_context()->add_class("dark"); + current_window->get_style_context()->remove_class("bright"); + } else { + current_window->get_style_context()->add_class("bright"); + current_window->get_style_context()->remove_class("dark"); + } + if (toplevel_window->get_style_context()->has_class("symbolic")) { + current_window->get_style_context()->add_class("symbolic"); + current_window->get_style_context()->remove_class("regular"); + } else { + current_window->get_style_context()->remove_class("symbolic"); + current_window->get_style_context()->add_class("regular"); + } + } + } + } +} + +void sp_add_top_window_classes(Gtk::Widget *widg) +{ + if (!widg) { + return; + } + if (!widg->get_realized()) { + widg->signal_realize().connect(sigc::bind(sigc::ptr_fun(&sp_add_top_window_classes_callback), widg)); + } else { + sp_add_top_window_classes_callback(widg); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dialog.h b/src/ui/dialog/dialog.h new file mode 100644 index 0000000..012bbc6 --- /dev/null +++ b/src/ui/dialog/dialog.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Base class for dialogs in Inkscape + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_H +#define INKSCAPE_DIALOG_H + +#include "dock-behavior.h" +#include "floating-behavior.h" + +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { +class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +enum BehaviorType { FLOATING, DOCK }; + +gboolean sp_retransientize_again(gpointer dlgPtr); +void sp_dialog_shutdown(GObject *object, gpointer dlgPtr); + +/** + * Base class for Inkscape dialogs. + * + * UI::Dialog::Dialog is a base class for all dialogs in Inkscape. The + * purpose of this class is to provide a unified place for ensuring + * style and behavior. Specifically, this class provides functionality + * for saving and restoring the size and position of dialogs (through + * the user's preferences file). + * + * It also provides some general purpose signal handlers for things like + * showing and hiding all dialogs. + * + * Fundamental parts of the dialog's behavior are controlled by + * a UI::Dialog::Behavior subclass instance connected to the dialog. + * + * @see UI::Widget::Panel panel class from which the dialogs are actually derived from. + * @see UI::Dialog::DialogManager manages the dialogs within inkscape. + * @see UI::Dialog::PanelDialog which links Panel and Dialog together in a dockable and floatable dialog. + */ +class Dialog { + +public: + + /** + * Constructor. + * + * @param behavior_factory floating or docked. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + Dialog(Behavior::BehaviorFactory behavior_factory, const char *prefs_path = nullptr, + int verb_num = 0, Glib::ustring apply_label = ""); + + virtual ~Dialog(); + + virtual void onDesktopActivated(SPDesktop*); + virtual void onShutdown(); + + /* Hide and show dialogs */ + virtual void onHideF12(); + virtual void onShowF12(); + + virtual operator Gtk::Widget &(); + virtual GtkWidget *gobj(); + virtual void present(); + virtual Gtk::Box *get_vbox(); + virtual void show(); + virtual void hide(); + virtual void show_all_children(); + virtual void set_size_request(int, int); + virtual void size_request(Gtk::Requisition &); + virtual void get_position(int &x, int &y); + virtual void get_size(int &width, int &height); + virtual void resize(int width, int height); + virtual void move(int x, int y); + virtual void set_position(Gtk::WindowPosition position); + virtual void set_title(Glib::ustring title); + virtual void set_sensitive(bool sensitive=true); + + virtual Glib::SignalProxy0<void> signal_show(); + virtual Glib::SignalProxy0<void> signal_hide(); + + bool _user_hidden; // when it is closed by the user, to prevent repopping on f12 + bool _hiddenF12; + + /** + * Read window position from preferences. + */ + void read_geometry(); + + /** + * Save window position to preferences. + */ + void save_geometry(); + void save_status(int visible, int state, int placement); + + bool retransientize_suppress; // when true, do not retransientize (prevents races when switching new windows too fast) + +protected: + Glib::ustring const _prefs_path; + int _verb_num; + Glib::ustring _title; + Glib::ustring _apply_label; + SPDesktop * _desktop; + bool _is_active_desktop; + + virtual void _handleResponse(int response_id); + + virtual bool _onDeleteEvent (GdkEventAny*); + virtual bool _onEvent(GdkEvent *event); + virtual bool _onKeyPress(GdkEventKey *event); + + virtual void _apply(); + + /* Closes the dialog window. + * + * This code sends a delete_event to the dialog, + * instead of just destroying it, so that the + * dialog can do some housekeeping, such as remember + * its position. + */ + virtual void _close(); + virtual void _defocus(); + + Inkscape::Selection* _getSelection(); + + sigc::connection _desktop_activated_connection; + sigc::connection _dialogs_hidden_connection; + sigc::connection _dialogs_unhidden_connection; + sigc::connection _shutdown_connection; + sigc::connection _change_theme_connection; + + private: + Behavior::Behavior* _behavior; + + Dialog() = delete; // no constructor without params + + Dialog(Dialog const &d) = delete; // no copy + Dialog& operator=(Dialog const &d) = delete; // no assign + + friend class Behavior::FloatingBehavior; + friend class Behavior::DockBehavior; +}; + +void sp_add_top_window_classes(Gtk::Widget *widg); +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + + +#endif //INKSCAPE_DIALOG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dock-behavior.cpp b/src/ui/dialog/dock-behavior.cpp new file mode 100644 index 0000000..9ea9d07 --- /dev/null +++ b/src/ui/dialog/dock-behavior.cpp @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" +#include "ui/widget/dock.h" +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + + +DockBehavior::DockBehavior(Dialog &dialog) : + Behavior(dialog), + _dock_item(*SP_ACTIVE_DESKTOP->getDock(), + Inkscape::Verb::get(dialog._verb_num)->get_id(), dialog._title.c_str(), + (Inkscape::Verb::get(dialog._verb_num)->get_image() ? + Inkscape::Verb::get(dialog._verb_num)->get_image() : ""), + static_cast<Widget::DockItem::State>( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/state", + UI::Widget::DockItem::DOCKED_STATE)), + static_cast<GdlDockPlacement>( + Inkscape::Preferences::get()->getInt(_dialog._prefs_path + "/placement", + GDL_DOCK_TOP))) + +{ + // Connect signals + _signal_hide_connection = signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onHide)); + signal_show().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onShow)); + _dock_item.signal_state_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::Behavior::DockBehavior::_onStateChanged)); + if (_dock_item.getState() == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) { + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } + } +} + +DockBehavior::~DockBehavior() += default; + + +Behavior * +DockBehavior::create(Dialog &dialog) +{ + return new DockBehavior(dialog); +} + + +DockBehavior::operator Gtk::Widget &() +{ + return _dock_item.getWidget(); +} + +GtkWidget * +DockBehavior::gobj() +{ + return _dock_item.gobj(); +} + +Gtk::VBox * +DockBehavior::get_vbox() +{ + return _dock_item.get_vbox(); +} + +void +DockBehavior::present() +{ + bool was_attached = _dock_item.isAttached(); + + _dock_item.present(); + + if (!was_attached) + _dialog.read_geometry(); +} + +void +DockBehavior::hide() +{ + _signal_hide_connection.block(); + _dock_item.hide(); + _signal_hide_connection.unblock(); +} + +void +DockBehavior::show() +{ + _dock_item.show(); +} + +void +DockBehavior::show_all_children() +{ + get_vbox()->show_all_children(); +} + +void +DockBehavior::get_position(int &x, int &y) +{ + _dock_item.get_position(x, y); +} + +void +DockBehavior::get_size(int &width, int &height) +{ + _dock_item.get_size(width, height); +} + +void +DockBehavior::resize(int width, int height) +{ + _dock_item.resize(width, height); +} + +void +DockBehavior::move(int x, int y) +{ + _dock_item.move(x, y); +} + +void +DockBehavior::set_position(Gtk::WindowPosition position) +{ + _dock_item.set_position(position); +} + +void +DockBehavior::set_size_request(int width, int height) +{ + _dock_item.set_size_request(width, height); +} + +void +DockBehavior::size_request(Gtk::Requisition &requisition) +{ + _dock_item.size_request(requisition); +} + +void +DockBehavior::set_title(Glib::ustring title) +{ + _dock_item.set_title(title); +} + +void DockBehavior::set_sensitive(bool sensitive) +{ + // TODO check this. Seems to be bad that we ignore the parameter + get_vbox()->set_sensitive(); +} + + +void +DockBehavior::_onHide() +{ + _dialog.save_geometry(); + _dialog._user_hidden = true; +} + +void +DockBehavior::_onShow() +{ + _dialog._user_hidden = false; +} + +void +DockBehavior::_onStateChanged(Widget::DockItem::State /*prev_state*/, + Widget::DockItem::State new_state) +{ +// TODO probably need to avoid window calls unless the state is different. Check. + + if (new_state == Widget::DockItem::FLOATING_STATE) { + if (Gtk::Window *floating_win = _dock_item.getWindow()) + sp_transientize(GTK_WIDGET(floating_win->gobj())); + } +} + +void +DockBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +DockBehavior::onShowF12() +{ + present(); +} + +void +DockBehavior::onShutdown() +{ + int visible = _dock_item.isIconified() || !_dialog._user_hidden; + int status = (_dock_item.getState() == Inkscape::UI::Widget::DockItem::UNATTACHED) ? _dock_item.getPrevState() : _dock_item.getState(); + _dialog.save_status( visible, status, _dock_item.getPlacement() ); +} + +void +DockBehavior::onDesktopActivated(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited( "/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + Gtk::Window *floating_win = _dock_item.getWindow(); + + if (floating_win) { + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (GtkWindow *dialog_win = floating_win->gobj()) { + + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); + } +} + + +/* Signal wrappers */ + +Glib::SignalProxy0<void> +DockBehavior::signal_show() { return _dock_item.signal_show(); } + +Glib::SignalProxy0<void> +DockBehavior::signal_hide() { return _dock_item.signal_hide(); } + +Glib::SignalProxy1<bool, GdkEventAny *> +DockBehavior::signal_delete_event() { return _dock_item.signal_delete_event(); } + +Glib::SignalProxy0<void> +DockBehavior::signal_drag_begin() { return _dock_item.signal_drag_begin(); } + +Glib::SignalProxy1<void, bool> +DockBehavior::signal_drag_end() { return _dock_item.signal_drag_end(); } + + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/dock-behavior.h b/src/ui/dialog/dock-behavior.h new file mode 100644 index 0000000..85b8244 --- /dev/null +++ b/src/ui/dialog/dock-behavior.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dockable dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H + +#include "ui/widget/dock-item.h" +#include "behavior.h" + +namespace Gtk { + class Paned; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class DockBehavior : public Behavior { + +public: + static Behavior *create(Dialog& dialog); + + ~DockBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget&() override; + GtkWidget *gobj() override; + void present() override; + Gtk::VBox *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition& requisition) override; + void get_position(int& x, int& y) override; + void get_size(int& width, int& height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0<void> signal_show() override; + Glib::SignalProxy0<void> signal_hide() override; + Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() override; + Glib::SignalProxy0<void> signal_drag_begin(); + Glib::SignalProxy1<void, bool> signal_drag_end(); + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + Widget::DockItem _dock_item; + + DockBehavior(Dialog& dialog); + + /** Internal helpers */ + Gtk::Paned *_getPaned(); //< gives the parent pane, if the dock item has one + void _requestHeight(int height); //< tries to resize the dock item to the requested height + + /** Internal signal handlers */ + void _onHide(); + void _onShow(); + bool _onDeleteEvent(GdkEventAny *event); + void _onStateChanged(Widget::DockItem::State prev_state, Widget::DockItem::State new_state); + bool _onKeyPress(GdkEventKey *event); + + sigc::connection _signal_hide_connection; + sigc::connection _signal_key_press_event_connection; + +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCK_BEHAVIOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/document-metadata.cpp b/src/ui/dialog/document-metadata.cpp new file mode 100644 index 0000000..a4c2707 --- /dev/null +++ b/src/ui/dialog/document-metadata.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document metadata dialog, Gtkmm-style. + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * + * Copyright (C) 2000 - 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document-metadata.h" +#include "desktop.h" +#include "rdf.h" +#include "verbs.h" + +#include "object/sp-namedview.h" + +#include "ui/widget/entity-entry.h" +#include "xml/node-event-vector.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 15 + +//=================================================== + +//--------------------------------------------------- + +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +DocumentMetadata & +DocumentMetadata::getInstance() +{ + DocumentMetadata &instance = *new DocumentMetadata(); + instance.init(); + return instance; +} + + +DocumentMetadata::DocumentMetadata() + : UI::Widget::Panel("/dialogs/documentmetadata", SP_VERB_DIALOG_METADATA) +{ + hide(); + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _page_metadata1.set_border_width(4); + _page_metadata2.set_border_width(4); + + _page_metadata1.set_column_spacing(2); + _page_metadata2.set_column_spacing(2); + _page_metadata1.set_row_spacing(2); + _page_metadata2.set_row_spacing(2); + + _notebook.append_page(_page_metadata1, _("Metadata")); + _notebook.append_page(_page_metadata2, _("License")); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentMetadata::_handleDeactivateDesktop)); + + build_metadata(); +} + +void +DocumentMetadata::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + + show_all_children(); +} + +DocumentMetadata::~DocumentMetadata() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + + for (auto & it : _rdflist) + delete it; +} + +// TODO: This duplicates code in document-properties.cpp +void +DocumentMetadata::build_metadata() +{ + using Inkscape::UI::Widget::EntityEntry; + + _page_metadata1.show(); + + Gtk::Label *label = Gtk::manage (new Gtk::Label); + label->set_markup (_("<b>Dublin Core Entities</b>")); + label->set_halign(Gtk::ALIGN_START); + label->set_valign(Gtk::ALIGN_CENTER); + + _page_metadata1.attach(*label, 0, 0, 2, 1); + + /* add generic metadata entry areas */ + struct rdf_work_entity_t * entity; + int row = 1; + for (entity = rdf_work_entities; entity && entity->name; entity++, row++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + EntityEntry *w = EntityEntry::create (entity, _wr); + _rdflist.push_back (w); + + w->_label.set_halign(Gtk::ALIGN_START); + w->_label.set_valign(Gtk::ALIGN_CENTER); + _page_metadata1.attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1.attach(*w->_packable, 1, row, 1, 1); + } + } + + _page_metadata2.show(); + + row = 0; + Gtk::Label *llabel = Gtk::manage (new Gtk::Label); + llabel->set_markup (_("<b>License</b>")); + llabel->set_halign(Gtk::ALIGN_START); + llabel->set_valign(Gtk::ALIGN_CENTER); + _page_metadata2.attach(*llabel, 0, row, 2, 1); + + /* add license selector pull-down and URI */ + ++row; + _licensor.init (_wr); + + _licensor.set_hexpand(); + _licensor.set_valign(Gtk::ALIGN_CENTER); + _page_metadata2.attach(_licensor, 1, row, 1, 1); +} + +/** + * Update dialog widgets from desktop. + */ +void DocumentMetadata::update() +{ + if (_wr.isUpdating()) return; + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + _wr.setUpdating (false); +} + +void +DocumentMetadata::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + update(); +} + +void +DocumentMetadata::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); +} + +//-------------------------------------------------------------------- + +/** + * Called when XML node attribute changed; updates dialog widgets. + */ +static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data) +{ + if (DocumentMetadata *dialog = static_cast<DocumentMetadata *>(data)) + dialog->update(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/document-metadata.h b/src/ui/dialog/document-metadata.h new file mode 100644 index 0000000..7004dca --- /dev/null +++ b/src/ui/dialog/document-metadata.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Metadata dialog + */ +/* Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004, 2005, 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H + +#include <list> +#include <cstddef> +#include "ui/widget/panel.h" +#include <gtkmm/notebook.h> +#include <gtkmm/grid.h> + +#include "inkscape.h" +#include "ui/widget/licensor.h" +#include "ui/widget/registry.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + } + namespace Dialog { + +typedef std::list<UI::Widget::EntityEntry*> RDElist; + +class DocumentMetadata : public Inkscape::UI::Widget::Panel { +public: + void update(); + + static DocumentMetadata &getInstance(); + + static void destroy(); + +protected: + void build_metadata(); + void init(); + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + Gtk::Notebook _notebook; + + Gtk::Grid _page_metadata1; + Gtk::Grid _page_metadata2; + + //--------------------------------------------------------------- + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + UI::Widget::Registry _wr; + +private: + ~DocumentMetadata() override; + DocumentMetadata(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_METADATA_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/document-properties.cpp b/src/ui/dialog/document-properties.cpp new file mode 100644 index 0000000..31f59ec --- /dev/null +++ b/src/ui/dialog/document-properties.cpp @@ -0,0 +1,1708 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Document properties dialog, Gtkmm-style. + */ +/* Authors: + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Diederik van Lierop <mail@diedenrezi.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000 - 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <vector> +#include "style.h" +#include "rdf.h" +#include "verbs.h" + +#include "display/canvas-grid.h" +#include "document-properties.h" +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "io/sys.h" +#include "object/sp-root.h" +#include "object/sp-script.h" +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/widget/entity-entry.h" +#include "ui/widget/notebook-page.h" +#include "xml/node-event-vector.h" + +#if defined(HAVE_LIBLCMS2) +#include "object/color-profile.h" +#endif // defined(HAVE_LIBLCMS2) + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define SPACE_SIZE_X 15 +#define SPACE_SIZE_Y 10 + + +//=================================================== + +//--------------------------------------------------- + +static void on_child_added(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node *ref, void * data); +static void on_repr_attr_changed (Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer); + +static Inkscape::XML::NodeEventVector const _repr_events = { + on_child_added, // child_added + on_child_removed, // child_removed + on_repr_attr_changed, + nullptr, // content_changed + nullptr // order_changed +}; + +static void docprops_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + +DocumentProperties& DocumentProperties::getInstance() +{ + DocumentProperties &instance = *new DocumentProperties(); + instance.init(); + + return instance; +} + +DocumentProperties::DocumentProperties() + : UI::Widget::Panel("/dialogs/documentoptions", SP_VERB_DIALOG_NAMEDVIEW) + , _page_page(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_guides(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_snap(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_cms(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_scripting(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_external_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_embedded_scripts(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata1(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + , _page_metadata2(Gtk::manage(new UI::Widget::NotebookPage(1, 1))) + //--------------------------------------------------------------- + , _rcb_antialias(_("Use antialiasing"), _("If unset, no antialiasing will be done on the drawing"), "shape-rendering", _wr, false, nullptr, nullptr, nullptr, "crispEdges") + , _rcb_checkerboard(_("Checkerboard background"), _("If set, use a colored checkerboard for the canvas background"), "inkscape:pagecheckerboard", _wr, false) + , _rcb_canb(_("Show page _border"), _("If set, rectangular page border is shown"), "showborder", _wr, false) + , _rcb_bord(_("Border on _top of drawing"), _("If set, border is always on top of the drawing"), "borderlayer", _wr, false) + , _rcb_shad(_("_Show border shadow"), _("If set, page border shows a shadow on its right and lower side"), "inkscape:showpageshadow", _wr, false) + , _rcp_bg(_("Back_ground color:"), _("Background color"), _("Color of the canvas background. Note: opacity is ignored except when exporting to bitmap."), "pagecolor", "inkscape:pageopacity", _wr) + , _rcp_bord(_("Border _color:"), _("Page border color"), _("Color of the page border"), "bordercolor", "borderopacity", _wr) + , _rum_deflt(_("Display _units:"), "inkscape:document-units", _wr) + , _page_sizer(_wr) + //--------------------------------------------------------------- + //General snap options + , _rcb_sgui(_("Show _guides"), _("Show or hide guides"), "showguides", _wr) + , _rcb_lgui(_("Lock all guides"), _("Toggle lock of all guides in the document"), "inkscape:lockguides", _wr) + , _rcp_gui(_("Guide co_lor:"), _("Guideline color"), _("Color of guidelines"), "guidecolor", "guideopacity", _wr) + , _rcp_hgui(_("_Highlight color:"), _("Highlighted guideline color"), _("Color of a guideline when it is under mouse"), "guidehicolor", "guidehiopacity", _wr) + , _create_guides_btn(_("Create guides around the page")) + , _delete_guides_btn(_("Delete all guides")) + //--------------------------------------------------------------- + , _rsu_sno(_("Snap _distance"), _("Snap only when _closer than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to objects"), _("Always snap to objects, regardless of their distance"), + _("If set, objects only snap to another object when it's within the range specified below"), + "objecttolerance", _wr) + //Options for snapping to grids + , _rsu_sn(_("Snap d_istance"), _("Snap only when c_loser than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to grid"), _("Always snap to grids, regardless of the distance"), + _("If set, objects only snap to a grid line when it's within the range specified below"), + "gridtolerance", _wr) + //Options for snapping to guides + , _rsu_gusn(_("Snap dist_ance"), _("Snap only when close_r than:"), _("Always snap"), + _("Snapping distance, in screen pixels, for snapping to guides"), _("Always snap to guides, regardless of the distance"), + _("If set, objects only snap to a guide when it's within the range specified below"), + "guidetolerance", _wr) + //--------------------------------------------------------------- + , _rcb_snclp(_("Snap to clip paths"), _("When snapping to paths, then also try snapping to clip paths"), "inkscape:snap-path-clip", _wr) + , _rcb_snmsk(_("Snap to mask paths"), _("When snapping to paths, then also try snapping to mask paths"), "inkscape:snap-path-mask", _wr) + , _rcb_perp(_("Snap perpendicularly"), _("When snapping to paths or guides, then also try snapping perpendicularly"), "inkscape:snap-perpendicular", _wr) + , _rcb_tang(_("Snap tangentially"), _("When snapping to paths or guides, then also try snapping tangentially"), "inkscape:snap-tangential", _wr) + //--------------------------------------------------------------- + , _grids_label_crea("", Gtk::ALIGN_START) + , _grids_button_new(C_("Grid", "_New"), _("Create new grid.")) + , _grids_button_remove(C_("Grid", "_Remove"), _("Remove selected grid.")) + , _grids_label_def("", Gtk::ALIGN_START) +{ + _getContents()->set_spacing (4); + _getContents()->pack_start(_notebook, true, true); + + _notebook.append_page(*_page_page, _("Page")); + _notebook.append_page(*_page_guides, _("Guides")); + _notebook.append_page(_grids_vbox, _("Grids")); + _notebook.append_page(*_page_snap, _("Snap")); + _notebook.append_page(*_page_cms, _("Color")); + _notebook.append_page(*_page_scripting, _("Scripting")); + _notebook.append_page(*_page_metadata1, _("Metadata")); + _notebook.append_page(*_page_metadata2, _("License")); + + _wr.setUpdating (true); + build_page(); + build_guides(); + build_gridspage(); + build_snap(); +#if defined(HAVE_LIBLCMS2) + build_cms(); +#endif // defined(HAVE_LIBLCMS2) + build_scripting(); + build_metadata(); + _wr.setUpdating (false); + + _grids_button_new.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onNewGrid)); + _grids_button_remove.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::onRemoveGrid)); + + signalDocumentReplaced().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDocumentReplaced)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleActivateDesktop)); + signalDeactiveDesktop().connect(sigc::mem_fun(*this, &DocumentProperties::_handleDeactivateDesktop)); + + _rum_deflt._changed_connection.block(); + _rum_deflt.getUnitMenu()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::onDocUnitChange)); +} + +void DocumentProperties::init() +{ + update(); + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->addListener (&_repr_events, this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->addListener (&_repr_events, this); + + show_all_children(); + _grids_button_remove.hide(); +} + +DocumentProperties::~DocumentProperties() +{ + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + repr->removeListenerByData (this); + Inkscape::XML::Node *root = getDesktop()->getDocument()->getRoot()->getRepr(); + root->removeListenerByData (this); + + for (auto & it : _rdflist) + delete it; +} + +//======================================================================== + +/** + * Helper function that sets widgets in a 2 by n table. + * arr has two entries per table row. Each row is in the following form: + * widget, widget -> function adds a widget in each column. + * nullptr, widget -> function adds a widget that occupies the row. + * label, nullptr -> function adds label that occupies the row. + * nullptr, nullptr -> function adds an empty box that occupies the row. + * This used to be a helper function for a 3 by n table + */ +void attach_all(Gtk::Grid &table, Gtk::Widget *const arr[], unsigned const n) +{ + for (unsigned i = 0, r = 0; i < n; i += 2) { + if (arr[i] && arr[i+1]) { + arr[i]->set_hexpand(); + arr[i+1]->set_hexpand(); + arr[i]->set_valign(Gtk::ALIGN_CENTER); + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + table.attach(*arr[i], 0, r, 1, 1); + table.attach(*arr[i+1], 1, r, 1, 1); + } else { + if (arr[i+1]) { + Gtk::AttachOptions yoptions = (Gtk::AttachOptions)0; + if (dynamic_cast<Inkscape::UI::Widget::PageSizer*>(arr[i+1])) { + // only the PageSizer in Document Properties|Page should be stretched vertically + yoptions = Gtk::FILL|Gtk::EXPAND; + } + arr[i+1]->set_hexpand(); + + if (yoptions & Gtk::EXPAND) + arr[i+1]->set_vexpand(); + else + arr[i+1]->set_valign(Gtk::ALIGN_CENTER); + + table.attach(*arr[i+1], 0, r, 2, 1); + } else if (arr[i]) { + Gtk::Label& label = reinterpret_cast<Gtk::Label&>(*arr[i]); + + label.set_hexpand(); + label.set_halign(Gtk::ALIGN_START); + label.set_valign(Gtk::ALIGN_CENTER); + table.attach(label, 0, r, 2, 1); + } else { + auto space = Gtk::manage (new Gtk::Box); + space->set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + space->set_halign(Gtk::ALIGN_CENTER); + space->set_valign(Gtk::ALIGN_CENTER); + table.attach(*space, 0, r, 1, 1); + } + } + ++r; + } +} + +void DocumentProperties::build_page() +{ + _page_page->show(); + + Gtk::Label* label_gen = Gtk::manage (new Gtk::Label); + label_gen->set_markup (_("<b>General</b>")); + + Gtk::Label *label_for = Gtk::manage (new Gtk::Label); + label_for->set_markup (_("<b>Page Size</b>")); + + Gtk::Label* label_bkg = Gtk::manage (new Gtk::Label); + label_bkg->set_markup (_("<b>Background</b>")); + + Gtk::Label* label_bdr = Gtk::manage (new Gtk::Label); + label_bdr->set_markup (_("<b>Border</b>")); + + Gtk::Label* label_dsp = Gtk::manage (new Gtk::Label); + label_dsp->set_markup (_("<b>Display</b>")); + + _page_sizer.init(); + + _rcb_doc_props_left.set_border_width(4); + _rcb_doc_props_left.set_row_spacing(4); + _rcb_doc_props_left.set_column_spacing(4); + _rcb_doc_props_right.set_border_width(4); + _rcb_doc_props_right.set_row_spacing(4); + _rcb_doc_props_right.set_column_spacing(4); + + Gtk::Widget *const widget_array[] = + { + label_gen, nullptr, + nullptr, &_rum_deflt, + nullptr, nullptr, + label_for, nullptr, + nullptr, &_page_sizer, + nullptr, nullptr, + &_rcb_doc_props_left, &_rcb_doc_props_right, + }; + attach_all(_page_page->table(), widget_array, G_N_ELEMENTS(widget_array)); + + Gtk::Widget *const widget_array_left[] = + { + label_bkg, nullptr, + nullptr, &_rcb_checkerboard, + nullptr, &_rcp_bg, + label_dsp, nullptr, + nullptr, &_rcb_antialias, + }; + attach_all(_rcb_doc_props_left, widget_array_left, G_N_ELEMENTS(widget_array_left)); + + Gtk::Widget *const widget_array_right[] = + { + label_bdr, nullptr, + nullptr, &_rcb_canb, + nullptr, &_rcb_bord, + nullptr, &_rcb_shad, + nullptr, &_rcp_bord, + }; + attach_all(_rcb_doc_props_right, widget_array_right, G_N_ELEMENTS(widget_array_right)); + + std::list<Gtk::Widget*> _slaveList; + _slaveList.push_back(&_rcb_bord); + _slaveList.push_back(&_rcb_shad); + _slaveList.push_back(&_rcp_bord); + _rcb_canb.setSlaveWidgets(_slaveList); +} + +void DocumentProperties::build_guides() +{ + _page_guides->show(); + + Gtk::Label *label_gui = Gtk::manage (new Gtk::Label); + label_gui->set_markup (_("<b>Guides</b>")); + + _rum_deflt.set_margin_start(0); + _rcp_bg.set_margin_start(0); + _rcp_bord.set_margin_start(0); + _rcp_gui.set_margin_start(0); + _rcp_hgui.set_margin_start(0); + _rcp_gui.set_hexpand(); + _rcp_hgui.set_hexpand(); + _rcb_sgui.set_hexpand(); + auto inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + inner->add(_rcb_sgui); + inner->add(_rcb_lgui); + inner->add(_rcp_gui); + inner->add(_rcp_hgui); + auto spacer = Gtk::manage(new Gtk::Label()); + Gtk::Widget *const widget_array[] = + { + label_gui, nullptr, + inner, spacer, + nullptr, nullptr, + nullptr, &_create_guides_btn, + nullptr, &_delete_guides_btn + }; + attach_all(_page_guides->table(), widget_array, G_N_ELEMENTS(widget_array)); + inner->set_hexpand(false); + + _create_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::create_guides_around_page)); + _delete_guides_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::delete_all_guides)); +} + +void DocumentProperties::build_snap() +{ + _page_snap->show(); + + Gtk::Label *label_o = Gtk::manage (new Gtk::Label); + label_o->set_markup (_("<b>Snap to objects</b>")); + Gtk::Label *label_gr = Gtk::manage (new Gtk::Label); + label_gr->set_markup (_("<b>Snap to grids</b>")); + Gtk::Label *label_gu = Gtk::manage (new Gtk::Label); + label_gu->set_markup (_("<b>Snap to guides</b>")); + Gtk::Label *label_m = Gtk::manage (new Gtk::Label); + label_m->set_markup (_("<b>Miscellaneous</b>")); + + auto spacer = Gtk::manage(new Gtk::Label()); + + Gtk::Widget *const array[] = + { + label_o, nullptr, + nullptr, _rsu_sno._vbox, + &_rcb_snclp, spacer, + nullptr, &_rcb_snmsk, + nullptr, nullptr, + label_gr, nullptr, + nullptr, _rsu_sn._vbox, + nullptr, nullptr, + label_gu, nullptr, + nullptr, _rsu_gusn._vbox, + nullptr, nullptr, + label_m, nullptr, + nullptr, &_rcb_perp, + nullptr, &_rcb_tang + }; + attach_all(_page_snap->table(), array, G_N_ELEMENTS(array)); + } + +void DocumentProperties::create_guides_around_page() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_GUIDES_AROUND_PAGE ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +void DocumentProperties::delete_all_guides() +{ + SPDesktop *dt = getDesktop(); + Verb *verb = Verb::get( SP_VERB_EDIT_DELETE_ALL_GUIDES ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + +#if defined(HAVE_LIBLCMS2) +/// Populates the available color profiles combo box +void DocumentProperties::populate_available_profiles(){ + _AvailableProfilesListStore->clear(); // Clear any existing items in the combo box + + // Iterate through the list of profiles and add the name to the combo box. + bool home = true; // initial value doesn't matter, it's just to avoid a compiler warning + bool first = true; + for (auto &profile: ColorProfile::getProfileFilesWithNames()) { + Gtk::TreeModel::Row row; + + // add a separator between profiles from the user's home directory and system profiles + if (!first && profile.isInHome != home) + { + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = "<separator>"; + row[_AvailableProfilesListColumns.nameColumn] = "<separator>"; + row[_AvailableProfilesListColumns.separatorColumn] = true; + } + home = profile.isInHome; + first = false; + + row = *(_AvailableProfilesListStore->append()); + row[_AvailableProfilesListColumns.fileColumn] = profile.filename; + row[_AvailableProfilesListColumns.nameColumn] = profile.name; + row[_AvailableProfilesListColumns.separatorColumn] = false; + } +} + +/** + * Cleans up name to remove disallowed characters. + * Some discussion at http://markmail.org/message/bhfvdfptt25kgtmj + * Allowed ASCII first characters: ':', 'A'-'Z', '_', 'a'-'z' + * Allowed ASCII remaining chars add: '-', '.', '0'-'9', + * + * @param str the string to clean up. + */ +static void sanitizeName( Glib::ustring& str ) +{ + if (str.size() > 0) { + char val = str.at(0); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && (val != '_') + && (val != ':')) { + str.insert(0, "_"); + } + for (Glib::ustring::size_type i = 1; i < str.size(); i++) { + char val = str.at(i); + if (((val < 'A') || (val > 'Z')) + && ((val < 'a') || (val > 'z')) + && ((val < '0') || (val > '9')) + && (val != '_') + && (val != ':') + && (val != '-') + && (val != '.')) { + str.replace(i, 1, "-"); + } + } + } +} + +/// Links the selected color profile in the combo box to the document +void DocumentProperties::linkSelectedProfile() +{ + //store this profile in the SVG document (create <color-profile> element in the XML) + // TODO remove use of 'active' desktop + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + // Find the index of the currently-selected row in the color profiles combobox + Gtk::TreeModel::iterator iter = _AvailableProfilesList.get_active(); + + if (!iter) { + g_warning("No color profile available."); + return; + } + + // Read the filename and description from the list of available profiles + Glib::ustring file = (*iter)[_AvailableProfilesListColumns.fileColumn]; + Glib::ustring name = (*iter)[_AvailableProfilesListColumns.nameColumn]; + + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj); + if (!strcmp(prof->href, file.c_str())) + return; + } + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *cprofRepr = xml_doc->createElement("svg:color-profile"); + gchar* tmp = g_strdup(name.c_str()); + Glib::ustring nameStr = tmp ? tmp : "profile"; // TODO add some auto-numbering to avoid collisions + sanitizeName(nameStr); + cprofRepr->setAttribute("name", nameStr); + cprofRepr->setAttribute("xlink:href", Glib::filename_to_uri(Glib::filename_from_utf8(file))); + cprofRepr->setAttribute("id", file); + + + // Checks whether there is a defs element. Creates it when needed + Inkscape::XML::Node *defsRepr = sp_repr_lookup_name(xml_doc, "svg:defs"); + if (!defsRepr) { + defsRepr = xml_doc->createElement("svg:defs"); + xml_doc->root()->addChild(defsRepr, nullptr); + } + + g_assert(desktop->doc()->getDefs()); + defsRepr->addChild(cprofRepr, nullptr); + + // TODO check if this next line was sometimes needed. It being there caused an assertion. + //Inkscape::GC::release(defsRepr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_LINK_COLOR_PROFILE, _("Link Color Profile")); + + populate_linked_profiles_box(); + } +} + +struct _cmp { + bool operator()(const SPObject * const & a, const SPObject * const & b) + { + const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a); + const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*b); + gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; + } +}; + +template <typename From, typename To> +struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } }; + +void DocumentProperties::populate_linked_profiles_box() +{ + _LinkedProfilesListStore->clear(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + if (! current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + + std::set<Inkscape::ColorProfile *, Inkscape::ColorProfile::pointerComparator> _current; + std::transform(current.begin(), + current.end(), + std::inserter(_current, _current.begin()), + static_caster<SPObject, Inkscape::ColorProfile>()); + + for (auto &profile: _current) { + Gtk::TreeModel::Row row = *(_LinkedProfilesListStore->append()); + row[_LinkedProfilesListColumns.nameColumn] = profile->name; +// row[_LinkedProfilesListColumns.previewColumn] = "Color Preview"; + } +} + +void DocumentProperties::external_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _ExternalScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::embedded_scripts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbeddedScriptsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::linked_profiles_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _EmbProfContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void DocumentProperties::cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbProfContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbProfContextMenu.accelerate(parent); +} + + +void DocumentProperties::external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _ExternalScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _ExternalScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _EmbeddedScriptsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _EmbeddedScriptsContextMenu.accelerate(parent); +} + +void DocumentProperties::onColorProfileSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _LinkedProfilesList.get_selection(); + if (sel) { + _unlink_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + + +void DocumentProperties::removeSelectedProfile(){ + Glib::ustring name; + if(_LinkedProfilesList.get_selection()) { + Gtk::TreeModel::iterator i = _LinkedProfilesList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_LinkedProfilesListColumns.nameColumn]; + } else { + return; + } + } + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "iccprofile" ); + for (auto obj : current) { + Inkscape::ColorProfile* prof = reinterpret_cast<Inkscape::ColorProfile*>(obj); + if (!name.compare(prof->name)){ + prof->deleteObject(true, false); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_COLOR_PROFILE, _("Remove linked color profile")); + break; // removing the color profile likely invalidates part of the traversed list, stop traversing here. + } + } + + populate_linked_profiles_box(); + onColorProfileSelectRow(); +} + +bool DocumentProperties::_AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter) +{ + bool separator = (*iter)[_AvailableProfilesListColumns.separatorColumn]; + return separator; +} + +void DocumentProperties::build_cms() +{ + _page_cms->show(); + Gtk::Label *label_link= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_link->set_markup (_("<b>Linked Color Profiles:</b>")); + Gtk::Label *label_avail = Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_avail->set_markup (_("<b>Available Color Profiles:</b>")); + + _link_btn.set_tooltip_text(_("Link Profile")); + docprops_style_button(_link_btn, INKSCAPE_ICON("list-add")); + + _unlink_btn.set_tooltip_text(_("Unlink Profile")); + docprops_style_button(_unlink_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_link->set_hexpand(); + label_link->set_halign(Gtk::ALIGN_START); + label_link->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_link, 0, row, 3, 1); + + row++; + + _LinkedProfilesListScroller.set_hexpand(); + _LinkedProfilesListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_LinkedProfilesListScroller, 0, row, 3, 1); + + row++; + + Gtk::HBox* spacer = Gtk::manage(new Gtk::HBox()); + spacer->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer->set_hexpand(); + spacer->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*spacer, 0, row, 3, 1); + + row++; + + label_avail->set_hexpand(); + label_avail->set_halign(Gtk::ALIGN_START); + label_avail->set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(*label_avail, 0, row, 3, 1); + + row++; + + _AvailableProfilesList.set_hexpand(); + _AvailableProfilesList.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_AvailableProfilesList, 0, row, 1, 1); + + _link_btn.set_halign(Gtk::ALIGN_CENTER); + _link_btn.set_valign(Gtk::ALIGN_CENTER); + _link_btn.set_margin_start(2); + _link_btn.set_margin_end(2); + _page_cms->table().attach(_link_btn, 1, row, 1, 1); + + _unlink_btn.set_halign(Gtk::ALIGN_CENTER); + _unlink_btn.set_valign(Gtk::ALIGN_CENTER); + _page_cms->table().attach(_unlink_btn, 2, row, 1, 1); + + // Set up the Available Profiles combo box + _AvailableProfilesListStore = Gtk::ListStore::create(_AvailableProfilesListColumns); + _AvailableProfilesList.set_model(_AvailableProfilesListStore); + _AvailableProfilesList.pack_start(_AvailableProfilesListColumns.nameColumn); + _AvailableProfilesList.set_row_separator_func(sigc::mem_fun(*this, &DocumentProperties::_AvailableProfilesList_separator)); + + populate_available_profiles(); + + //# Set up the Linked Profiles combo box + _LinkedProfilesListStore = Gtk::ListStore::create(_LinkedProfilesListColumns); + _LinkedProfilesList.set_model(_LinkedProfilesListStore); + _LinkedProfilesList.append_column(_("Profile Name"), _LinkedProfilesListColumns.nameColumn); +// _LinkedProfilesList.append_column(_("Color Preview"), _LinkedProfilesListColumns.previewColumn); + _LinkedProfilesList.set_headers_visible(false); +// TODO restore? _LinkedProfilesList.set_fixed_height_mode(true); + + populate_linked_profiles_box(); + + _LinkedProfilesListScroller.add(_LinkedProfilesList); + _LinkedProfilesListScroller.set_shadow_type(Gtk::SHADOW_IN); + _LinkedProfilesListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _LinkedProfilesListScroller.set_size_request(-1, 90); + + _link_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::linkSelectedProfile)); + _unlink_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + _LinkedProfilesList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onColorProfileSelectRow) ); + + _LinkedProfilesList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::linked_profiles_list_button_release)); + cms_create_popup_menu(_LinkedProfilesList, sigc::mem_fun(*this, &DocumentProperties::removeSelectedProfile)); + + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "defs" ); + if (!current.empty()) { + _emb_profiles_observer.set((*(current.begin()))->parent); + } + _emb_profiles_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_linked_profiles_box)); + onColorProfileSelectRow(); +} +#endif // defined(HAVE_LIBLCMS2) + +void DocumentProperties::build_scripting() +{ + _page_scripting->show(); + + _page_scripting->table().attach(_scripting_notebook, 0, 0, 1, 1); + + _scripting_notebook.append_page(*_page_external_scripts, _("External scripts")); + _scripting_notebook.append_page(*_page_embedded_scripts, _("Embedded scripts")); + + //# External scripts tab + _page_external_scripts->show(); + Gtk::Label *label_external= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_external->set_markup (_("<b>External script files:</b>")); + + _external_add_btn.set_tooltip_text(_("Add the current file name or browse for a file")); + docprops_style_button(_external_add_btn, INKSCAPE_ICON("list-add")); + + _external_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_external_remove_btn, INKSCAPE_ICON("list-remove")); + + gint row = 0; + + label_external->set_hexpand(); + label_external->set_halign(Gtk::ALIGN_START); + label_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*label_external, 0, row, 3, 1); + + row++; + + _ExternalScriptsListScroller.set_hexpand(); + _ExternalScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_ExternalScriptsListScroller, 0, row, 3, 1); + + row++; + + Gtk::HBox* spacer_external = Gtk::manage(new Gtk::HBox()); + spacer_external->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + + spacer_external->set_hexpand(); + spacer_external->set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(*spacer_external, 0, row, 3, 1); + + row++; + + _script_entry.set_hexpand(); + _script_entry.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_script_entry, 0, row, 1, 1); + + _external_add_btn.set_halign(Gtk::ALIGN_CENTER); + _external_add_btn.set_valign(Gtk::ALIGN_CENTER); + _external_add_btn.set_margin_start(2); + _external_add_btn.set_margin_end(2); + + _page_external_scripts->table().attach(_external_add_btn, 1, row, 1, 1); + + _external_remove_btn.set_halign(Gtk::ALIGN_CENTER); + _external_remove_btn.set_valign(Gtk::ALIGN_CENTER); + _page_external_scripts->table().attach(_external_remove_btn, 2, row, 1, 1); + + //# Set up the External Scripts box + _ExternalScriptsListStore = Gtk::ListStore::create(_ExternalScriptsListColumns); + _ExternalScriptsList.set_model(_ExternalScriptsListStore); + _ExternalScriptsList.append_column(_("Filename"), _ExternalScriptsListColumns.filenameColumn); + _ExternalScriptsList.set_headers_visible(true); +// TODO restore? _ExternalScriptsList.set_fixed_height_mode(true); + + + //# Embedded scripts tab + _page_embedded_scripts->show(); + Gtk::Label *label_embedded= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded->set_markup (_("<b>Embedded script files:</b>")); + + _embed_new_btn.set_tooltip_text(_("New")); + docprops_style_button(_embed_new_btn, INKSCAPE_ICON("list-add")); + + _embed_remove_btn.set_tooltip_text(_("Remove")); + docprops_style_button(_embed_remove_btn, INKSCAPE_ICON("list-remove")); + + _embed_button_box.set_layout (Gtk::BUTTONBOX_START); + _embed_button_box.add(_embed_new_btn); + _embed_button_box.add(_embed_remove_btn); + + row = 0; + + label_embedded->set_hexpand(); + label_embedded->set_halign(Gtk::ALIGN_START); + label_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded, 0, row, 3, 1); + + row++; + + _EmbeddedScriptsListScroller.set_hexpand(); + _EmbeddedScriptsListScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedScriptsListScroller, 0, row, 3, 1); + + row++; + + _embed_button_box.set_hexpand(); + _embed_button_box.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_embed_button_box, 0, row, 1, 1); + + row++; + + Gtk::HBox* spacer_embedded = Gtk::manage(new Gtk::HBox()); + spacer_embedded->set_size_request(SPACE_SIZE_X, SPACE_SIZE_Y); + spacer_embedded->set_hexpand(); + spacer_embedded->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*spacer_embedded, 0, row, 3, 1); + + row++; + + //# Set up the Embedded Scripts box + _EmbeddedScriptsListStore = Gtk::ListStore::create(_EmbeddedScriptsListColumns); + _EmbeddedScriptsList.set_model(_EmbeddedScriptsListStore); + _EmbeddedScriptsList.append_column(_("Script id"), _EmbeddedScriptsListColumns.idColumn); + _EmbeddedScriptsList.set_headers_visible(true); +// TODO restore? _EmbeddedScriptsList.set_fixed_height_mode(true); + + //# Set up the Embedded Scripts content box + Gtk::Label *label_embedded_content= Gtk::manage (new Gtk::Label("", Gtk::ALIGN_START)); + label_embedded_content->set_markup (_("<b>Content:</b>")); + + label_embedded_content->set_hexpand(); + label_embedded_content->set_halign(Gtk::ALIGN_START); + label_embedded_content->set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(*label_embedded_content, 0, row, 3, 1); + + row++; + + _EmbeddedContentScroller.set_hexpand(); + _EmbeddedContentScroller.set_valign(Gtk::ALIGN_CENTER); + _page_embedded_scripts->table().attach(_EmbeddedContentScroller, 0, row, 3, 1); + + _EmbeddedContentScroller.add(_EmbeddedContent); + _EmbeddedContentScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedContentScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _EmbeddedContentScroller.set_size_request(-1, 140); + + _EmbeddedScriptsList.signal_cursor_changed().connect(sigc::mem_fun(*this, &DocumentProperties::changeEmbeddedScript)); + _EmbeddedScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onEmbeddedScriptSelectRow) ); + + _ExternalScriptsList.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &DocumentProperties::onExternalScriptSelectRow) ); + + _EmbeddedContent.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::editEmbeddedScript)); + + populate_script_lists(); + + _ExternalScriptsListScroller.add(_ExternalScriptsList); + _ExternalScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _ExternalScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _ExternalScriptsListScroller.set_size_request(-1, 90); + + _external_add_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addExternalScript)); + + _EmbeddedScriptsListScroller.add(_EmbeddedScriptsList); + _EmbeddedScriptsListScroller.set_shadow_type(Gtk::SHADOW_IN); + _EmbeddedScriptsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _EmbeddedScriptsListScroller.set_size_request(-1, 90); + + _embed_new_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::addEmbeddedScript)); + +#if defined(HAVE_LIBLCMS2) + + _external_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + _embed_remove_btn.signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); + + _ExternalScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::external_scripts_list_button_release)); + external_create_popup_menu(_ExternalScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeExternalScript)); + + _EmbeddedScriptsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &DocumentProperties::embedded_scripts_list_button_release)); + embedded_create_popup_menu(_EmbeddedScriptsList, sigc::mem_fun(*this, &DocumentProperties::removeEmbeddedScript)); +#endif // defined(HAVE_LIBLCMS2) + +//TODO: review this observers code: + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + if (! current.empty()) { + _scripts_observer.set((*(current.begin()))->parent); + } + _scripts_observer.signal_changed().connect(sigc::mem_fun(*this, &DocumentProperties::populate_script_lists)); + onEmbeddedScriptSelectRow(); + onExternalScriptSelectRow(); +} + +// TODO: This duplicates code in document-metadata.cpp +void DocumentProperties::build_metadata() +{ + using Inkscape::UI::Widget::EntityEntry; + + _page_metadata1->show(); + + Gtk::Label *label = Gtk::manage (new Gtk::Label); + label->set_markup (_("<b>Dublin Core Entities</b>")); + label->set_halign(Gtk::ALIGN_START); + label->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach (*label, 0,0,2,1); + + /* add generic metadata entry areas */ + struct rdf_work_entity_t * entity; + int row = 1; + for (entity = rdf_work_entities; entity && entity->name; entity++, row++) { + if ( entity->editable == RDF_EDIT_GENERIC ) { + EntityEntry *w = EntityEntry::create (entity, _wr); + _rdflist.push_back (w); + + w->_label.set_halign(Gtk::ALIGN_START); + w->_label.set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(w->_label, 0, row, 1, 1); + + w->_packable->set_hexpand(); + w->_packable->set_valign(Gtk::ALIGN_CENTER); + _page_metadata1->table().attach(*w->_packable, 1, row, 1, 1); + } + } + + Gtk::Button *button_save = Gtk::manage (new Gtk::Button(_("_Save as default"),true)); + button_save->set_tooltip_text(_("Save this metadata as the default metadata")); + Gtk::Button *button_load = Gtk::manage (new Gtk::Button(_("Use _default"),true)); + button_load->set_tooltip_text(_("Use the previously saved default metadata here")); + + auto box_buttons = Gtk::manage (new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + box_buttons->pack_start(*button_save, true, true, 6); + box_buttons->pack_start(*button_load, true, true, 6); + _page_metadata1->pack_end(*box_buttons, false, false, 0); + + button_save->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::save_default_metadata)); + button_load->signal_clicked().connect(sigc::mem_fun(*this, &DocumentProperties::load_default_metadata)); + + _page_metadata2->show(); + + row = 0; + Gtk::Label *llabel = Gtk::manage (new Gtk::Label); + llabel->set_markup (_("<b>License</b>")); + llabel->set_halign(Gtk::ALIGN_START); + llabel->set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(*llabel, 0, row, 2, 1); + + /* add license selector pull-down and URI */ + ++row; + _licensor.init (_wr); + + _licensor.set_hexpand(); + _licensor.set_valign(Gtk::ALIGN_CENTER); + _page_metadata2->table().attach(_licensor, 0, row, 2, 1); +} + +void DocumentProperties::addExternalScript(){ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + g_warning("No active desktop"); + return; + } + + if (_script_entry.get_text().empty() ) { + // Click Add button with no filename, show a Browse dialog + browseExternalScript(); + } + + if (!_script_entry.get_text().empty()) { + + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + scriptRepr->setAttributeOrRemoveIfEmpty("xlink:href", _script_entry.get_text()); + _script_entry.set_text(""); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_ADD_EXTERNAL_SCRIPT, _("Add external script...")); + + populate_script_lists(); + } + +} + +static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr; + +void DocumentProperties::browseExternalScript() { + + //# Get the current directory for finding files + static Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + + Glib::ustring attr = prefs->getString(_prefs_path); + if (!attr.empty()) open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.empty()) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!selectPrefsFileInstance) { + selectPrefsFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::CUSTOM_TYPE, + _("Select a script to load")); + selectPrefsFileInstance->addFilterMenu("Javascript Files", "*.js"); + } + + //# Show the dialog + bool const success = selectPrefsFileInstance->show(); + + if (!success) { + return; + } + + //# User selected something. Get name and type + Glib::ustring fileName = selectPrefsFileInstance->getFilename(); + + _script_entry.set_text(fileName); +} + +void DocumentProperties::addEmbeddedScript(){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop){ + g_warning("No active desktop"); + } else { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *scriptRepr = xml_doc->createElement("svg:script"); + + xml_doc->root()->addChild(scriptRepr, nullptr); + + // inform the document, so we can undo + DocumentUndo::done(desktop->doc(), SP_VERB_EDIT_ADD_EMBEDDED_SCRIPT, _("Add embedded script...")); + + populate_script_lists(); + } +} + +void DocumentProperties::removeExternalScript(){ + Glib::ustring name; + if(_ExternalScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _ExternalScriptsList.get_selection()->get_selected(); + + if(i){ + name = (*i)[_ExternalScriptsListColumns.filenameColumn]; + } else { + return; + } + } + + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (obj) { + SPScript* script = dynamic_cast<SPScript *>(obj); + if (script && (name == script->xlinkhref)) { + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EXTERNAL_SCRIPT, _("Remove external script")); + } + } + } + } + + populate_script_lists(); +} + +void DocumentProperties::removeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + SPObject* obj = SP_ACTIVE_DOCUMENT->getObjectById(id); + if (obj) { + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + sp_repr_unparent(repr); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_REMOVE_EMBEDDED_SCRIPT, _("Remove embedded script")); + } + } + + populate_script_lists(); +} + +void DocumentProperties::onExternalScriptSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _ExternalScriptsList.get_selection(); + if (sel) { + _external_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::onEmbeddedScriptSelectRow() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = _EmbeddedScriptsList.get_selection(); + if (sel) { + _embed_remove_btn.set_sensitive(sel->count_selected_rows () > 0); + } +} + +void DocumentProperties::changeEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + bool voidscript=true; + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (id == obj->getId()){ + int count = (int) obj->children.size(); + + if (count>1) + g_warning("TODO: Found a script element with multiple (%d) child nodes! We must implement support for that!", count); + + //XML Tree being used directly here while it shouldn't be. + SPObject* child = obj->firstChild(); + //TODO: shouldn't we get all children instead of simply the first child? + + if (child && child->getRepr()){ + const gchar* content = child->getRepr()->content(); + if (content){ + voidscript=false; + _EmbeddedContent.get_buffer()->set_text(content); + } + } + } + } + + if (voidscript) + _EmbeddedContent.get_buffer()->set_text(""); +} + +void DocumentProperties::editEmbeddedScript(){ + Glib::ustring id; + if(_EmbeddedScriptsList.get_selection()) { + Gtk::TreeModel::iterator i = _EmbeddedScriptsList.get_selection()->get_selected(); + + if(i){ + id = (*i)[_EmbeddedScriptsListColumns.idColumn]; + } else { + return; + } + } + + Inkscape::XML::Document *xml_doc = SP_ACTIVE_DOCUMENT->getReprDoc(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + for (auto obj : current) { + if (id == obj->getId()){ + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node *repr = obj->getRepr(); + if (repr){ + auto tmp = obj->children | boost::adaptors::transformed([](SPObject& o) { return &o; }); + std::vector<SPObject*> vec(tmp.begin(), tmp.end()); + for (auto &child: vec) { + child->deleteObject(); + } + obj->appendChildRepr(xml_doc->createTextNode(_EmbeddedContent.get_buffer()->get_text().c_str())); + + //TODO repr->set_content(_EmbeddedContent.get_buffer()->get_text()); + + // inform the document, so we can undo + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_EDIT_EMBEDDED_SCRIPT, _("Edit embedded script")); + } + } + } +} + +void DocumentProperties::populate_script_lists(){ + _ExternalScriptsListStore->clear(); + _EmbeddedScriptsListStore->clear(); + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList( "script" ); + if (!current.empty()) { + SPObject *obj = *(current.begin()); + g_assert(obj != nullptr); + _scripts_observer.set(obj->parent); + } + for (auto obj : current) { + SPScript* script = dynamic_cast<SPScript *>(obj); + g_assert(script != nullptr); + if (script->xlinkhref) + { + Gtk::TreeModel::Row row = *(_ExternalScriptsListStore->append()); + row[_ExternalScriptsListColumns.filenameColumn] = script->xlinkhref; + } + else // Embedded scripts + { + Gtk::TreeModel::Row row = *(_EmbeddedScriptsListStore->append()); + row[_EmbeddedScriptsListColumns.idColumn] = obj->getId(); + } + } +} + +/** +* Called for _updating_ the dialog (e.g. when a new grid was manually added in XML) +*/ +void DocumentProperties::update_gridspage() +{ + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + + int prev_page_count = _grids_notebook.get_n_pages(); + int prev_page_pos = _grids_notebook.get_current_page(); + + //remove all tabs + while (_grids_notebook.get_n_pages() != 0) { + _grids_notebook.remove_page(-1); // this also deletes the page. + } + + //add tabs + for(auto grid : nv->grids) { + if (!grid->repr->attribute("id")) continue; // update_gridspage is called again when "id" is added + Glib::ustring name(grid->repr->attribute("id")); + const char *icon = nullptr; + switch (grid->getGridType()) { + case GRID_RECTANGULAR: + icon = "grid-rectangular"; + break; + case GRID_AXONOMETRIC: + icon = "grid-axonometric"; + break; + default: + break; + } + _grids_notebook.append_page(*grid->newWidget(), _createPageTabLabel(name, icon)); + } + _grids_notebook.show_all(); + + int cur_page_count = _grids_notebook.get_n_pages(); + if (cur_page_count > 0) { + _grids_button_remove.set_sensitive(true); + + // The following is not correct if grid added/removed via XML + if (cur_page_count == prev_page_count + 1) { + _grids_notebook.set_current_page(cur_page_count - 1); + } else if (cur_page_count == prev_page_count) { + _grids_notebook.set_current_page(prev_page_pos); + } else if (cur_page_count == prev_page_count - 1) { + _grids_notebook.set_current_page(prev_page_pos < 1 ? 0 : prev_page_pos - 1); + } + } else { + _grids_button_remove.set_sensitive(false); + } +} + +/** + * Build grid page of dialog. + */ +void DocumentProperties::build_gridspage() +{ + /// \todo FIXME: gray out snapping when grid is off. + /// Dissenting view: you want snapping without grid. + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + (void)nv; + + _grids_label_crea.set_markup(_("<b>Creation</b>")); + _grids_label_def.set_markup(_("<b>Defined grids</b>")); + _grids_hbox_crea.pack_start(_grids_combo_gridtype, true, true); + _grids_hbox_crea.pack_start(_grids_button_new, true, true); + + for (gint t = 0; t <= GRID_MAXTYPENR; t++) { + _grids_combo_gridtype.append( CanvasGrid::getName( (GridType) t ) ); + } + _grids_combo_gridtype.set_active_text( CanvasGrid::getName(GRID_RECTANGULAR) ); + + _grids_space.set_size_request (SPACE_SIZE_X, SPACE_SIZE_Y); + + _grids_vbox.set_border_width(4); + _grids_vbox.set_spacing(4); + _grids_vbox.pack_start(_grids_label_crea, false, false); + _grids_vbox.pack_start(_grids_hbox_crea, false, false); + _grids_vbox.pack_start(_grids_space, false, false); + _grids_vbox.pack_start(_grids_label_def, false, false); + _grids_vbox.pack_start(_grids_notebook, false, false); + _grids_vbox.pack_start(_grids_button_remove, false, false); + + update_gridspage(); +} + + + +/** + * Update dialog widgets from desktop. Also call updateWidget routines of the grids. + */ +void DocumentProperties::update() +{ + if (_wr.isUpdating()) return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + + _wr.setUpdating (true); + set_sensitive (true); + + //-----------------------------------------------------------page page + _rcb_checkerboard.setActive (nv->pagecheckerboard); + _rcp_bg.setRgba32 (nv->pagecolor); + _rcb_canb.setActive (nv->showborder); + _rcb_bord.setActive (nv->borderlayer == SP_BORDER_LAYER_TOP); + _rcp_bord.setRgba32 (nv->bordercolor); + _rcb_shad.setActive (nv->showpageshadow); + + SPRoot *root = dt->getDocument()->getRoot(); + _rcb_antialias.set_xml_target(root->getRepr(), dt->getDocument()); + _rcb_antialias.setActive(root->style->shape_rendering.computed != SP_CSS_SHAPE_RENDERING_CRISPEDGES); + + if (nv->display_units) { + _rum_deflt.setUnit (nv->display_units->abbr); + } + + double doc_w = dt->getDocument()->getRoot()->width.value; + Glib::ustring doc_w_unit = unit_table.getUnit(dt->getDocument()->getRoot()->width.unit)->abbr; + if (doc_w_unit == "") { + doc_w_unit = "px"; + } else if (doc_w_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_w_unit = "px"; + doc_w = dt->getDocument()->getRoot()->viewBox.width(); + } + double doc_h = dt->getDocument()->getRoot()->height.value; + Glib::ustring doc_h_unit = unit_table.getUnit(dt->getDocument()->getRoot()->height.unit)->abbr; + if (doc_h_unit == "") { + doc_h_unit = "px"; + } else if (doc_h_unit == "%" && dt->getDocument()->getRoot()->viewBox_set) { + doc_h_unit = "px"; + doc_h = dt->getDocument()->getRoot()->viewBox.height(); + } + _page_sizer.setDim(Inkscape::Util::Quantity(doc_w, doc_w_unit), Inkscape::Util::Quantity(doc_h, doc_h_unit)); + _page_sizer.updateFitMarginsUI(nv->getRepr()); + _page_sizer.updateScaleUI(); + + //-----------------------------------------------------------guide page + + _rcb_sgui.setActive (nv->showguides); + _rcb_lgui.setActive (nv->lockguides); + _rcp_gui.setRgba32 (nv->guidecolor); + _rcp_hgui.setRgba32 (nv->guidehicolor); + + //-----------------------------------------------------------snap page + + _rsu_sno.setValue (nv->snap_manager.snapprefs.getObjectTolerance()); + _rsu_sn.setValue (nv->snap_manager.snapprefs.getGridTolerance()); + _rsu_gusn.setValue (nv->snap_manager.snapprefs.getGuideTolerance()); + _rcb_snclp.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP)); + _rcb_snmsk.setActive (nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK)); + _rcb_perp.setActive (nv->snap_manager.snapprefs.getSnapPerp()); + _rcb_tang.setActive (nv->snap_manager.snapprefs.getSnapTang()); + + //-----------------------------------------------------------grids page + + update_gridspage(); + + //------------------------------------------------Color Management page + +#if defined(HAVE_LIBLCMS2) + populate_linked_profiles_box(); + populate_available_profiles(); +#endif // defined(HAVE_LIBLCMS2) + + //-----------------------------------------------------------meta pages + /* update the RDF entities */ + for (auto & it : _rdflist) + it->update (SP_ACTIVE_DOCUMENT); + + _licensor.update (SP_ACTIVE_DOCUMENT); + + + _wr.setUpdating (false); +} + +// TODO: copied from fill-and-stroke.cpp factor out into new ui/widget file? +Gtk::HBox& +DocumentProperties::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 0)); + _tab_label_box->set_spacing(4); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +//-------------------------------------------------------------------- + +void DocumentProperties::on_response (int id) +{ + if (id == Gtk::RESPONSE_DELETE_EVENT || id == Gtk::RESPONSE_CLOSE) + { + _rcp_bg.closeWindow(); + _rcp_bord.closeWindow(); + _rcp_gui.closeWindow(); + _rcp_hgui.closeWindow(); + } + + if (id == Gtk::RESPONSE_CLOSE) + hide(); +} + +void DocumentProperties::load_default_metadata() +{ + /* Get the data RDF entities data from preferences*/ + for (auto & it : _rdflist) { + it->load_from_preferences (); + } +} + +void DocumentProperties::save_default_metadata() +{ + /* Save these RDF entities to preferences*/ + for (auto & it : _rdflist) { + it->save_to_preferences (SP_ACTIVE_DOCUMENT); + } +} + + +void DocumentProperties::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = document->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleActivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->addListener(&_repr_events, this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->addListener(&_repr_events, this); + update(); +} + +void DocumentProperties::_handleDeactivateDesktop(SPDesktop *desktop) +{ + Inkscape::XML::Node *repr = desktop->getNamedView()->getRepr(); + repr->removeListenerByData(this); + Inkscape::XML::Node *root = desktop->getDocument()->getRoot()->getRepr(); + root->removeListenerByData(this); +} + +static void on_child_added(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update_gridspage(); +} + +static void on_child_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, void *data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update_gridspage(); +} + + + +/** + * Called when XML node attribute changed; updates dialog widgets. + */ +static void on_repr_attr_changed(Inkscape::XML::Node *, gchar const *, gchar const *, gchar const *, bool, gpointer data) +{ + if (DocumentProperties *dialog = static_cast<DocumentProperties *>(data)) + dialog->update(); +} + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void DocumentProperties::onNewGrid() +{ + SPDesktop *dt = getDesktop(); + Inkscape::XML::Node *repr = dt->getNamedView()->getRepr(); + SPDocument *doc = dt->getDocument(); + + Glib::ustring typestring = _grids_combo_gridtype.get_active_text(); + CanvasGrid::writeNewGridToRepr(repr, doc, CanvasGrid::getGridTypeFromName(typestring.c_str())); + + // toggle grid showing to ON: + dt->showGrids(true); +} + + +void DocumentProperties::onRemoveGrid() +{ + gint pagenum = _grids_notebook.get_current_page(); + if (pagenum == -1) // no pages + return; + + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + Inkscape::CanvasGrid * found_grid = nullptr; + if( pagenum < (gint)nv->grids.size()) + found_grid = nv->grids[pagenum]; + + if (found_grid) { + // delete the grid that corresponds with the selected tab + // when the grid is deleted from SVG, the SPNamedview handler automatically deletes the object, so found_grid becomes an invalid pointer! + found_grid->repr->parent()->removeChild(found_grid->repr); + DocumentUndo::done(dt->getDocument(), SP_VERB_DIALOG_NAMEDVIEW, _("Remove grid")); + } +} + +/** Callback for document unit change. */ +/* This should not effect anything in the SVG tree (other than "inkscape:document-units"). + This should only effect values displayed in the GUI. */ +void DocumentProperties::onDocUnitChange() +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + // Don't execute when change is being undone + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + // Don't execute when initializing widgets + if (_wr.isUpdating()) { + return; + } + + + Inkscape::XML::Node *repr = getDesktop()->getNamedView()->getRepr(); + /*Inkscape::Util::Unit const *old_doc_unit = unit_table.getUnit("px"); + if(repr->attribute("inkscape:document-units")) { + old_doc_unit = unit_table.getUnit(repr->attribute("inkscape:document-units")); + }*/ + Inkscape::Util::Unit const *doc_unit = _rum_deflt.getUnit(); + + // Set document unit + Inkscape::SVGOStringStream os; + os << doc_unit->abbr; + repr->setAttribute("inkscape:document-units", os.str()); + + _page_sizer.updateScaleUI(); + + // Disable changing of SVG Units. The intent here is to change the units in the UI, not the units in SVG. + // This code should be moved (and fixed) once we have an "SVG Units" setting that sets what units are used in SVG data. +#if 0 + // Set viewBox + if (doc->getRoot()->viewBox_set) { + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->setViewBox(doc->getRoot()->viewBox*Geom::Scale(scale)); + } else { + Inkscape::Util::Quantity width = doc->getWidth(); + Inkscape::Util::Quantity height = doc->getHeight(); + doc->setViewBox(Geom::Rect::from_xywh(0, 0, width.value(doc_unit), height.value(doc_unit))); + } + + // TODO: Fix bug in nodes tool instead of switching away from it + if (tools_active(getDesktop()) == TOOLS_NODES) { + tools_switch(getDesktop(), TOOLS_SELECT); + } + + // Scale and translate objects + // set transform options to scale all things with the transform, so all things scale properly after the viewbox change. + /// \todo this "low-level" code of changing viewbox/unit should be moved somewhere else + + // save prefs + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + { + ShapeEditor::blockSetItem(true); + gdouble viewscale = 1.0; + Geom::Rect vb = doc->getRoot()->viewBox; + if ( !vb.hasZeroArea() ) { + gdouble viewscale_w = doc->getWidth().value("px") / vb.width(); + gdouble viewscale_h = doc->getHeight().value("px")/ vb.height(); + viewscale = std::min(viewscale_h, viewscale_w); + } + gdouble scale = Inkscape::Util::Quantity::convert(1, old_doc_unit, doc_unit); + doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(-viewscale*doc->getRoot()->viewBox.min()[Geom::X] + + (doc->getWidth().value("px") - viewscale*doc->getRoot()->viewBox.width())/2, + viewscale*doc->getRoot()->viewBox.min()[Geom::Y] + + (doc->getHeight().value("px") + viewscale*doc->getRoot()->viewBox.height())/2), + false); + ShapeEditor::blockSetItem(false); + } + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); +#endif + + doc->setModifiedSinceSave(); + + DocumentUndo::done(doc, SP_VERB_NONE, _("Changed default display unit")); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/document-properties.h b/src/ui/dialog/document-properties.h new file mode 100644 index 0000000..8b4e088 --- /dev/null +++ b/src/ui/dialog/document-properties.h @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * \brief Document Properties dialog + */ +/* Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2006-2008 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/liststore.h> +#include <gtkmm/notebook.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/textview.h> + +#include "ui/widget/page-sizer.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "ui/widget/tolerance-slider.h" +#include "ui/widget/panel.h" +#include "ui/widget/licensor.h" + +#include "xml/helper-observer.h" + +namespace Inkscape { + namespace XML { + class Node; + } + namespace UI { + namespace Widget { + class EntityEntry; + class NotebookPage; + } + namespace Dialog { + +typedef std::list<UI::Widget::EntityEntry*> RDElist; + +class DocumentProperties : public UI::Widget::Panel { +public: + void update(); + static DocumentProperties &getInstance(); + static void destroy(); + + void update_gridspage(); + +protected: + void build_page(); + void build_grid(); + void build_guides(); + void build_snap(); + void build_gridspage(); + + void create_guides_around_page(); + void delete_all_guides(); +#if defined(HAVE_LIBLCMS2) + void build_cms(); +#endif // defined(HAVE_LIBLCMS2) + void build_scripting(); + void build_metadata(); + void init(); + + virtual void on_response (int); +#if defined(HAVE_LIBLCMS2) + void populate_available_profiles(); + void populate_linked_profiles_box(); + void linkSelectedProfile(); + void removeSelectedProfile(); + void onColorProfileSelectRow(); + void linked_profiles_list_button_release(GdkEventButton* event); + void cms_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); +#endif // defined(HAVE_LIBLCMS2) + + void external_scripts_list_button_release(GdkEventButton* event); + void embedded_scripts_list_button_release(GdkEventButton* event); + void populate_script_lists(); + void addExternalScript(); + void browseExternalScript(); + void addEmbeddedScript(); + void removeExternalScript(); + void removeEmbeddedScript(); + void changeEmbeddedScript(); + void onExternalScriptSelectRow(); + void onEmbeddedScriptSelectRow(); + void editEmbeddedScript(); + void external_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void embedded_create_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void load_default_metadata(); + void save_default_metadata(); + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleActivateDesktop(SPDesktop *desktop); + void _handleDeactivateDesktop(SPDesktop *desktop); + + Inkscape::XML::SignalObserver _emb_profiles_observer, _scripts_observer; + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_page; + UI::Widget::NotebookPage *_page_guides; + UI::Widget::NotebookPage *_page_snap; + UI::Widget::NotebookPage *_page_cms; + UI::Widget::NotebookPage *_page_scripting; + + Gtk::Notebook _scripting_notebook; + UI::Widget::NotebookPage *_page_external_scripts; + UI::Widget::NotebookPage *_page_embedded_scripts; + + UI::Widget::NotebookPage *_page_metadata1; + UI::Widget::NotebookPage *_page_metadata2; + + Gtk::VBox _grids_vbox; + + UI::Widget::Registry _wr; + //--------------------------------------------------------------- + Gtk::Grid _rcb_doc_props_left; + Gtk::Grid _rcb_doc_props_right; + UI::Widget::RegisteredCheckButton _rcb_antialias; + UI::Widget::RegisteredCheckButton _rcb_checkerboard; + UI::Widget::RegisteredCheckButton _rcb_canb; + UI::Widget::RegisteredCheckButton _rcb_bord; + UI::Widget::RegisteredCheckButton _rcb_shad; + UI::Widget::RegisteredColorPicker _rcp_bg; + UI::Widget::RegisteredColorPicker _rcp_bord; + UI::Widget::RegisteredUnitMenu _rum_deflt; + UI::Widget::PageSizer _page_sizer; + //--------------------------------------------------------------- + UI::Widget::RegisteredCheckButton _rcb_sgui; + UI::Widget::RegisteredCheckButton _rcb_lgui; + UI::Widget::RegisteredColorPicker _rcp_gui; + UI::Widget::RegisteredColorPicker _rcp_hgui; + Gtk::Button _create_guides_btn; + Gtk::Button _delete_guides_btn; + //--------------------------------------------------------------- + UI::Widget::ToleranceSlider _rsu_sno; + UI::Widget::ToleranceSlider _rsu_sn; + UI::Widget::ToleranceSlider _rsu_gusn; + UI::Widget::RegisteredCheckButton _rcb_snclp; + UI::Widget::RegisteredCheckButton _rcb_snmsk; + UI::Widget::RegisteredCheckButton _rcb_perp; + UI::Widget::RegisteredCheckButton _rcb_tang; + //--------------------------------------------------------------- + Gtk::Button _link_btn; + Gtk::Button _unlink_btn; + class AvailableProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + AvailableProfilesColumns() + { add(fileColumn); add(nameColumn); add(separatorColumn); } + Gtk::TreeModelColumn<Glib::ustring> fileColumn; + Gtk::TreeModelColumn<Glib::ustring> nameColumn; + Gtk::TreeModelColumn<bool> separatorColumn; + }; + AvailableProfilesColumns _AvailableProfilesListColumns; + Glib::RefPtr<Gtk::ListStore> _AvailableProfilesListStore; + Gtk::ComboBox _AvailableProfilesList; + bool _AvailableProfilesList_separator(const Glib::RefPtr<Gtk::TreeModel>& model, const Gtk::TreeModel::iterator& iter); + class LinkedProfilesColumns : public Gtk::TreeModel::ColumnRecord + { + public: + LinkedProfilesColumns() + { add(nameColumn); add(previewColumn); } + Gtk::TreeModelColumn<Glib::ustring> nameColumn; + Gtk::TreeModelColumn<Glib::ustring> previewColumn; + }; + LinkedProfilesColumns _LinkedProfilesListColumns; + Glib::RefPtr<Gtk::ListStore> _LinkedProfilesListStore; + Gtk::TreeView _LinkedProfilesList; + Gtk::ScrolledWindow _LinkedProfilesListScroller; + Gtk::Menu _EmbProfContextMenu; + + //--------------------------------------------------------------- + Gtk::Button _external_add_btn; + Gtk::Button _external_remove_btn; + Gtk::Button _embed_new_btn; + Gtk::Button _embed_remove_btn; + Gtk::ButtonBox _embed_button_box; + + class ExternalScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ExternalScriptsColumns() + { add(filenameColumn); } + Gtk::TreeModelColumn<Glib::ustring> filenameColumn; + }; + ExternalScriptsColumns _ExternalScriptsListColumns; + class EmbeddedScriptsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + EmbeddedScriptsColumns() + { add(idColumn); } + Gtk::TreeModelColumn<Glib::ustring> idColumn; + }; + EmbeddedScriptsColumns _EmbeddedScriptsListColumns; + Glib::RefPtr<Gtk::ListStore> _ExternalScriptsListStore; + Glib::RefPtr<Gtk::ListStore> _EmbeddedScriptsListStore; + Gtk::TreeView _ExternalScriptsList; + Gtk::TreeView _EmbeddedScriptsList; + Gtk::ScrolledWindow _ExternalScriptsListScroller; + Gtk::ScrolledWindow _EmbeddedScriptsListScroller; + Gtk::Menu _ExternalScriptsContextMenu; + Gtk::Menu _EmbeddedScriptsContextMenu; + Gtk::Entry _script_entry; + Gtk::TextView _EmbeddedContent; + Gtk::ScrolledWindow _EmbeddedContentScroller; + //--------------------------------------------------------------- + + Gtk::Notebook _grids_notebook; + Gtk::HBox _grids_hbox_crea; + Gtk::Label _grids_label_crea; + Gtk::Button _grids_button_new; + Gtk::Button _grids_button_remove; + Gtk::ComboBoxText _grids_combo_gridtype; + Gtk::Label _grids_label_def; + Gtk::HBox _grids_space; + //--------------------------------------------------------------- + + RDElist _rdflist; + UI::Widget::Licensor _licensor; + + Gtk::HBox& _createPageTabLabel(const Glib::ustring& label, const char *label_image); + +private: + DocumentProperties(); + ~DocumentProperties() override; + + // callback methods for buttons on grids page. + void onNewGrid(); + void onRemoveGrid(); + + // callback for document unit change + void onDocUnitChange(); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_DOCUMENT_PREFERENCES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/export.cpp b/src/ui/dialog/export.cpp new file mode 100644 index 0000000..7d68559 --- /dev/null +++ b/src/ui/dialog/export.cpp @@ -0,0 +1,1948 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Peter Bostrom + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 1999-2007, 2012 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// This has to be included prior to anything that includes setjmp.h, it croaks otherwise +#include <png.h> + +#include <gtkmm/box.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> +#include <gtkmm/spinbutton.h> + +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> + +#include <gdl/gdl-dock-item.h> + +#include "document-undo.h" +#include "document.h" +#include "file.h" +#include "inkscape.h" +#include "preferences.h" +#include "selection-chemistry.h" +#include "verbs.h" + +// required to set status message after export +#include "desktop.h" +#include "message-stack.h" + +#include "helper/png-write.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog-events.h" +#include "ui/interface.h" +#include "ui/widget/unit-menu.h" + +#include "extension/db.h" +#include "extension/output.h" + + +#ifdef _WIN32 +#include <windows.h> +#include <commdlg.h> +#include <gdk/gdkwin32.h> +#include <glibmm/fileutils.h> +#endif + +#define SP_EXPORT_MIN_SIZE 1.0 + +#define DPI_BASE Inkscape::Util::Quantity::convert(1, "in", "px") + +#define EXPORT_COORD_PRECISION 3 + +#include "export.h" + +using Inkscape::Util::unit_table; + +namespace { + +class MessageCleaner +{ +public: + MessageCleaner(Inkscape::MessageId messageId, SPDesktop *desktop) : + _desktop(desktop), + _messageId(messageId) + { + } + + ~MessageCleaner() + { + if (_messageId && _desktop) { + _desktop->messageStack()->cancel(_messageId); + } + } + +private: + MessageCleaner(MessageCleaner const &other) = delete; + MessageCleaner &operator=(MessageCleaner const &other) = delete; + + SPDesktop *_desktop; + Inkscape::MessageId _messageId; +}; + +} // namespace + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** A list of strings that is used both in the preferences, and in the + data fields to describe the various values of \c selection_type. */ +static const char * selection_names[SELECTION_NUMBER_OF] = { + "page", "drawing", "selection", "custom" +}; + +/** The names on the buttons for the various selection types. */ +static const char * selection_labels[SELECTION_NUMBER_OF] = { + N_("_Page"), N_("_Drawing"), N_("_Selection"), N_("_Custom") +}; + +Export::Export () : + UI::Widget::Panel("/dialogs/export/", SP_VERB_DIALOG_EXPORT), + current_key(SELECTION_PAGE), + original_name(), + doc_export_name(), + filename_modified(false), + was_empty(true), + update(false), + togglebox(true, 0), + area_box(false, 3), + singleexport_box(false, 0), + size_box(false, 3), + file_box(false, 3), + unitbox(false, 0), + unit_selector(), + units_label(_("Units:")), + filename_box(false, 5), + browse_label(_("_Export As..."), true), + browse_image(), + batch_box(false, 5), + batch_export(_("B_atch export all selected objects")), + interlacing(_("Use interlacing")), + bitdepth_label(_("Bit depth")), + bitdepth_cb(), + zlib_label(_("Compression")), + zlib_compression(), + pHYs_label(_("pHYs dpi")), + pHYs_sb(pHYs_adj, 1.0, 2), + antialiasing_label(_("Antialiasing")), + antialiasing_cb(), + hide_box(false, 3), + hide_export(_("Hide all except selected")), + closeWhenDone(_("Close when complete")), + button_box(false, 3), + _prog(), + prog_dlg(nullptr), + interrupted(false), + prefs(nullptr), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + batch_export.set_use_underline(); + batch_export.set_tooltip_text(_("Export each selected object into its own PNG file, using export hints if any (caution, overwrites without asking!)")); + hide_export.set_use_underline(); + hide_export.set_tooltip_text(_("In the exported image, hide all objects except those that are selected")); + interlacing.set_use_underline(); + interlacing.set_tooltip_text(_("Enables ADAM7 interlacing for PNG output. This results in slightly heavier images, but big images will look better sooner when loading the file")); + closeWhenDone.set_use_underline(); + closeWhenDone.set_tooltip_text(_("Once the export completes, close this dialog")); + prefs = Inkscape::Preferences::get(); + + singleexport_box.set_border_width(0); + + /* Export area frame */ + { + Gtk::Label* lbl = new Gtk::Label(_("<b>Export area</b>"), Gtk::ALIGN_START); + lbl->set_use_markup(true); + area_box.pack_start(*lbl); + + /* Units box */ + /* gets added to the vbox later, but the unit selector is needed + earlier than that */ + unit_selector.setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + unit_selector.setUnit(desktop->getNamedView()->display_units->abbr); + } + unitChangedConn = unit_selector.signal_changed().connect(sigc::mem_fun(*this, &Export::onUnitChanged)); + unitbox.pack_end(unit_selector, false, false, 0); + unitbox.pack_end(units_label, false, false, 3); + + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + selectiontype_buttons[i] = new Gtk::RadioButton(_(selection_labels[i]), true); + if (i > 0) { + Gtk::RadioButton::Group group = selectiontype_buttons[0]->get_group(); + selectiontype_buttons[i]->set_group(group); + } + selectiontype_buttons[i]->set_mode(false); + togglebox.pack_start(*selectiontype_buttons[i], false, true, 0); + selectiontype_buttons[i]->signal_clicked().connect(sigc::mem_fun(*this, &Export::onAreaToggled)); + } + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + x0_adj = createSpinbutton ( "x0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 0, _("_x0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX0Change); + + x1_adj = createSpinbutton ( "x1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 0, 1, _("x_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaX1Change); + + width_adj = createSpinbutton ( "width", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 0, 2, _("Wid_th:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaWidthChange); + + y0_adj = createSpinbutton ( "y0", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 0, _("_y0:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY0Change); + + y1_adj = createSpinbutton ( "y1", 0.0, -1000000.0, 1000000.0, 0.1, 1.0, + t, 2, 1, _("y_1:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaY1Change); + + height_adj = createSpinbutton ( "height", 0.0, 0.0, PNG_UINT_31_MAX, 0.1, 1.0, + t, 2, 2, _("Hei_ght:"), "", EXPORT_COORD_PRECISION, 1, + &Export::onAreaHeightChange); + + area_box.pack_start(togglebox, false, false, 3); + area_box.pack_start(*t, false, false, 0); + area_box.pack_start(unitbox, false, false, 0); + + area_box.set_border_width(3); + singleexport_box.pack_start(area_box, false, false, 0); + + } // end of area box + + /* Bitmap size frame */ + { + size_box.set_border_width(3); + bm_label = new Gtk::Label(_("<b>Image size</b>"), Gtk::ALIGN_START); + bm_label->set_use_markup(true); + size_box.pack_start(*bm_label, false, false, 0); + + auto t = new Gtk::Grid(); + t->set_row_spacing(4); + t->set_column_spacing(4); + + size_box.pack_start(*t); + + bmwidth_adj = createSpinbutton ( "bmwidth", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 0, + _("_Width:"), _("pixels at"), 0, 1, + &Export::onBitmapWidthChange); + + xdpi_adj = createSpinbutton ( "xdpi", + prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 0, + "", _("dp_i"), 2, 1, + &Export::onExportXdpiChange); + + bmheight_adj = createSpinbutton ( "bmheight", 16.0, 1.0, 1000000.0, 1.0, 10.0, + t, 0, 1, + _("_Height:"), _("pixels at"), 0, 1, + &Export::onBitmapHeightChange); + + /** TODO + * There's no way to set ydpi currently, so we use the defaultxdpi value here, too... + */ + ydpi_adj = createSpinbutton ( "ydpi", prefs->getDouble("/dialogs/export/defaultxdpi/value", DPI_BASE), + 0.01, 100000.0, 0.1, 1.0, t, 3, 1, + "", _("dpi"), 2, 0, nullptr ); + + singleexport_box.pack_start(size_box, Gtk::PACK_SHRINK); + } + + /* File entry */ + { + file_box.set_border_width(3); + flabel = new Gtk::Label(_("<b>_Filename</b>"), Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + flabel->set_use_markup(true); + file_box.pack_start(*flabel, false, false, 0); + + set_default_filename(); + + filename_box.pack_start (filename_entry, true, true, 0); + + Gtk::HBox* browser_im_label = new Gtk::HBox(false, 3); + browse_image.set_from_icon_name("folder", Gtk::ICON_SIZE_BUTTON); + browser_im_label->pack_start(browse_image); + browser_im_label->pack_start(browse_label); + browse_button.add(*browser_im_label); + filename_box.pack_end (browse_button, false, false); + + file_box.add(filename_box); + + original_name = filename_entry.get_text(); + + // focus is in the filename initially: + filename_entry.grab_focus(); + + // mnemonic in frame label moves focus to filename: + flabel->set_mnemonic_widget(filename_entry); + + singleexport_box.pack_start(file_box, Gtk::PACK_SHRINK); + } + + batch_export.set_sensitive(true); + batch_box.pack_start(batch_export, false, false, 3); + + hide_export.set_sensitive(true); + hide_export.set_active (prefs->getBool("/dialogs/export/hideexceptselected/value", false)); + hide_box.pack_start(hide_export, false, false, 3); + + + /* Export Button row */ + export_button.set_label(_("_Export")); + export_button.set_use_underline(); + export_button.set_tooltip_text (_("Export the bitmap file with these settings")); + + button_box.set_border_width(3); + button_box.pack_start(closeWhenDone, true, true, 0); + button_box.pack_end(export_button, false, false, 0); + + /*Advanced*/ + Gtk::Label *label_advanced = Gtk::manage(new Gtk::Label(_("Advanced"),true)); + expander.set_label_widget(*label_advanced); + expander.set_vexpand(false); + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + for(auto i : modes_list) + bitdepth_cb.append(i); + bitdepth_cb.set_active_text("RGBA_8"); + bitdepth_cb.set_hexpand(); + const char* const zlist[]={"Z_NO_COMPRESSION","Z_BEST_SPEED","2","3","4","5","Z_DEFAULT_COMPRESSION","7","8","Z_BEST_COMPRESSION"}; + for(auto i : zlist) + zlib_compression.append(i); + zlib_compression.set_active_text("Z_DEFAULT_COMPRESSION"); + pHYs_adj = Gtk::Adjustment::create(0, 0, 100000, 0.1, 1.0, 0); + pHYs_sb.set_adjustment(pHYs_adj); + pHYs_sb.set_width_chars(7); + pHYs_sb.set_tooltip_text( _("Will force-set the physical dpi for the png file. Set this to 72 if you're planning to work on your png with Photoshop") ); + zlib_compression.set_hexpand(); + const char* const antialising_list[] = {"CAIRO_ANTIALIAS_NONE","CAIRO_ANTIALIAS_FAST","CAIRO_ANTIALIAS_GOOD (default)","CAIRO_ANTIALIAS_BEST"}; + for(auto i : antialising_list) + antialiasing_cb.append(i); + antialiasing_cb.set_active_text(antialising_list[2]); + bitdepth_label.set_halign(Gtk::ALIGN_START); + zlib_label.set_halign(Gtk::ALIGN_START); + pHYs_label.set_halign(Gtk::ALIGN_START); + antialiasing_label.set_halign(Gtk::ALIGN_START); + auto table = new Gtk::Grid(); + expander.add(*table); + // gtk_container_add(GTK_CONTAINER(expander.gobj()), (GtkWidget*)(table->gobj())); + table->set_border_width(4); + table->attach(interlacing,0,0,1,1); + table->attach(bitdepth_label,0,1,1,1); + table->attach(bitdepth_cb,1,1,1,1); + table->attach(zlib_label,0,2,1,1); + table->attach(zlib_compression,1,2,1,1); + table->attach(pHYs_label,0,3,1,1); + table->attach(pHYs_sb,1,3,1,1); + table->attach(antialiasing_label,0,4,1,1); + table->attach(antialiasing_cb,1,4,1,1); + table->show(); + + /* Main dialog */ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + contents->pack_start(singleexport_box, Gtk::PACK_SHRINK); + contents->pack_start(batch_box, Gtk::PACK_SHRINK); + contents->pack_start(hide_box, Gtk::PACK_SHRINK); + contents->pack_start(expander, Gtk::PACK_SHRINK); + contents->pack_end(button_box, Gtk::PACK_SHRINK); + contents->pack_end(_prog, Gtk::PACK_SHRINK); + + /* Signal handlers */ + filename_entry.signal_changed().connect( sigc::mem_fun(*this, &Export::onFilenameModified) ); + // pressing enter in the filename field is the same as clicking export: + filename_entry.signal_activate().connect(sigc::mem_fun(*this, &Export::onExport) ); + browse_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBrowse)); + batch_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onBatchClicked)); + export_button.signal_clicked().connect(sigc::mem_fun(*this, &Export::onExport)); + hide_export.signal_clicked().connect(sigc::mem_fun(*this, &Export::onHideExceptSelected)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Export::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + setExporting(false); + + findDefaultSelection(); + onAreaToggled(); +} + +Export::~Export () +{ + was_empty = TRUE; + + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void Export::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Export::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Export::onSelectionChanged))); + + //// Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &Export::onSelectionModified))); + } + } +} + +/* + * set the default filename to be that of the current path + document + * with .png extension + * + * One thing to notice here is that this filename may get + * overwritten, but it won't happen here. The filename gets + * written into the text field, but then the button to select + * the area gets set. In that code the filename can be changed + * if there are some with presidence in the document. So, while + * this code sets the name first, it may not be the one users + * really see. + */ +void Export::set_default_filename () { + + if ( SP_ACTIVE_DOCUMENT && SP_ACTIVE_DOCUMENT->getDocumentURI() ) + { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + const gchar *uri = doc->getDocumentURI(); + auto &&text_extension = get_file_save_extension(Inkscape::Extension::FILE_SAVE_METHOD_SAVE_AS); + Inkscape::Extension::Output * oextension = nullptr; + + if (!text_extension.empty()) { + oextension = dynamic_cast<Inkscape::Extension::Output *>(Inkscape::Extension::db.get(text_extension.c_str())); + } + + if (oextension != nullptr) { + gchar * old_extension = oextension->get_extension(); + if (g_str_has_suffix(uri, old_extension)) { + gchar * uri_copy; + gchar * extension_point; + gchar * final_name; + + uri_copy = g_strdup(uri); + extension_point = g_strrstr(uri_copy, old_extension); + extension_point[0] = '\0'; + + final_name = g_strconcat(uri_copy, ".png", NULL); + filename_entry.set_text(final_name); + filename_entry.set_position(strlen(final_name)); + + g_free(final_name); + g_free(uri_copy); + } + } else { + gchar *name = g_strconcat(uri, ".png", NULL); + filename_entry.set_text(name); + filename_entry.set_position(strlen(name)); + + g_free(name); + } + + doc_export_name = filename_entry.get_text(); + } + else if ( SP_ACTIVE_DOCUMENT ) + { + Glib::ustring filename = create_filepath_from_id (_("bitmap"), filename_entry.get_text()); + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + + doc_export_name = filename_entry.get_text(); + } +} + +Glib::RefPtr<Gtk::Adjustment> Export::createSpinbutton( gchar const * /*key*/, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ) +{ + auto adj = Gtk::Adjustment::create(val, min, max, step, page, 0); + + int pos = 0; + Gtk::Label *l = nullptr; + + if (!ll.empty()) { + l = new Gtk::Label(ll,true); + l->set_halign(Gtk::ALIGN_END); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive(sensitive); + pos++; + } + + auto sb = new Gtk::SpinButton(adj, 1.0, digits); + sb->set_hexpand(); + t->attach(*sb, x + pos, y, 1, 1); + + sb->set_width_chars(7); + sb->set_sensitive (sensitive); + pos++; + + if (l) { + l->set_mnemonic_widget(*sb); + } + + if (!lr.empty()) { + l = new Gtk::Label(lr,true); + l->set_halign(Gtk::ALIGN_START); + l->set_valign(Gtk::ALIGN_CENTER); + l->set_hexpand(); + t->attach(*l, x + pos, y, 1, 1); + l->set_sensitive (sensitive); + pos++; + l->set_mnemonic_widget (*sb); + } + + if (cb) { + adj->signal_value_changed().connect( sigc::mem_fun(*this, cb) ); + } + + return adj; +} // end of createSpinbutton() + + +Glib::ustring Export::create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text) +{ + if (id.empty()) + { /* This should never happen */ + id = "bitmap"; + } + + Glib::ustring directory; + + if (!file_entry_text.empty()) { + directory = Glib::path_get_dirname(file_entry_text); + } + + if (directory.empty()) { + /* Grab document directory */ + const gchar* docURI = SP_ACTIVE_DOCUMENT->getDocumentURI(); + if (docURI) { + directory = Glib::path_get_dirname(docURI); + } + } + + if (directory.empty()) { + directory = Inkscape::IO::Resource::homedir_path(nullptr); + } + + Glib::ustring filename = Glib::build_filename(directory, id+".png"); + return filename; +} + +void Export::onBatchClicked () +{ + if (batch_export.get_active()) { + singleexport_box.set_sensitive(false); + } else { + singleexport_box.set_sensitive(true); + } +} + +void Export::updateCheckbuttons () +{ + gint num = (gint) boost::distance(SP_ACTIVE_DESKTOP->getSelection()->items()); + if (num >= 2) { + batch_export.set_sensitive(true); + batch_export.set_label(g_strdup_printf (ngettext("B_atch export %d selected object","B_atch export %d selected objects",num), num)); + } else { + batch_export.set_active (false); + batch_export.set_sensitive(false); + } + + //hide_export.set_sensitive (num > 0); +} + +inline void Export::findDefaultSelection() +{ + selection_type key = SELECTION_NUMBER_OF; + + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + key = SELECTION_SELECTION; + } + + /* Try using the preferences */ + if (key == SELECTION_NUMBER_OF) { + + int i = SELECTION_NUMBER_OF; + + Glib::ustring what = prefs->getString("/dialogs/export/exportarea/value"); + + if (!what.empty()) { + for (i = 0; i < SELECTION_NUMBER_OF; i++) { + if (what == selection_names[i]) { + break; + } + } + } + + key = (selection_type)i; + } + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_SELECTION; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + updateCheckbuttons (); +} + + +/** + * If selection changed or a different document activated, we must + * recalculate any chosen areas. + */ +void Export::onSelectionChanged() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + + if ((current_key == SELECTION_DRAWING || current_key == SELECTION_PAGE) && + (SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false && + was_empty) { + current_key = SELECTION_SELECTION; + selectiontype_buttons[current_key]->set_active(true); + } + was_empty = (SP_ACTIVE_DESKTOP->getSelection())->isEmpty(); + + if ( selection && + SELECTION_CUSTOM != current_key) { + onAreaToggled(); + } + + updateCheckbuttons (); +} + +void Export::onSelectionModified ( guint /*flags*/ ) +{ + Inkscape::Selection * Sel; + switch (current_key) { + case SELECTION_DRAWING: + if ( SP_ACTIVE_DESKTOP ) { + SPDocument *doc; + doc = SP_ACTIVE_DESKTOP->getDocument(); + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + if (bbox) { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + case SELECTION_SELECTION: + Sel = SP_ACTIVE_DESKTOP->getSelection(); + if (Sel->isEmpty() == false) { + Geom::OptRect bbox = Sel->visualBounds(); + if (bbox) + { + setArea ( bbox->left(), + bbox->top(), + bbox->right(), + bbox->bottom()); + } + } + break; + default: + /* Do nothing for page or for custom */ + break; + } + + return; +} + +/// Called when one of the selection buttons was toggled. +void Export::onAreaToggled () +{ + if (update) { + return; + } + + /* Find which button is active */ + selection_type key = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + if (selectiontype_buttons[i]->get_active()) { + key = (selection_type)i; + } + } + + if ( SP_ACTIVE_DESKTOP ) + { + SPDocument *doc; + Geom::OptRect bbox; + bbox = Geom::Rect(Geom::Point(0.0, 0.0),Geom::Point(0.0, 0.0)); + doc = SP_ACTIVE_DESKTOP->getDocument(); + + /* Notice how the switch is used to 'fall through' here to get + various backups. If you modify this without noticing you'll + probably screw something up. */ + switch (key) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) + { + bbox = SP_ACTIVE_DESKTOP->getSelection()->visualBounds(); + /* Only if there is a selection that we can set + do we break, otherwise we fall through to the + drawing */ + // std::cout << "Using selection: SELECTION" << std::endl; + key = SELECTION_SELECTION; + break; + } + case SELECTION_DRAWING: + /** \todo + * This returns wrong values if the document has a viewBox. + */ + bbox = doc->getRoot()->desktopVisualBounds(); + /* If the drawing is valid, then we'll use it and break + otherwise we drop through to the page settings */ + if (bbox) { + // std::cout << "Using selection: DRAWING" << std::endl; + key = SELECTION_DRAWING; + break; + } + case SELECTION_PAGE: + bbox = Geom::Rect(Geom::Point(0.0, 0.0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + + // std::cout << "Using selection: PAGE" << std::endl; + key = SELECTION_PAGE; + break; + case SELECTION_CUSTOM: + default: + break; + } // switch + + current_key = key; + + // remember area setting + prefs->setString("/dialogs/export/exportarea/value", selection_names[current_key]); + + if ( key != SELECTION_CUSTOM && bbox ) { + setArea ( bbox->min()[Geom::X], + bbox->min()[Geom::Y], + bbox->max()[Geom::X], + bbox->max()[Geom::Y]); + } + + } // end of if ( SP_ACTIVE_DESKTOP ) + + if (SP_ACTIVE_DESKTOP && !filename_modified) { + + Glib::ustring filename; + float xdpi = 0.0, ydpi = 0.0; + + switch (key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + sp_document_get_export_hints (doc, filename, &xdpi, &ydpi); + + if (filename.empty()) { + if (!doc_export_name.empty()) { + filename = doc_export_name; + } + } + break; + } + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + + SP_ACTIVE_DESKTOP->getSelection()->getExportHints(filename, &xdpi, &ydpi); + + /* If we still don't have a filename -- let's build + one that's nice */ + if (filename.empty()) { + const gchar * id = "object"; + auto reprlst = SP_ACTIVE_DESKTOP->getSelection()->xmlNodes(); + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + if (repr->attribute("id")) { + id = repr->attribute("id"); + break; + } + } + + filename = create_filepath_from_id (id, filename_entry.get_text()); + } + } + break; + case SELECTION_CUSTOM: + default: + break; + } + + if (!filename.empty()) { + original_name = filename; + filename_entry.set_text(filename); + filename_entry.set_position(filename.length()); + } + + if (xdpi != 0.0) { + setValue(xdpi_adj, xdpi); + } + + /* These can't be separate, and setting x sets y, so for + now setting this is disabled. Hopefully it won't be in + the future */ + if (FALSE && ydpi != 0.0) { + setValue(ydpi_adj, ydpi); + } + } + + return; +} // end of sp_export_area_toggled() + +/// Called when dialog is deleted +bool Export::onProgressDelete (GdkEventAny * /*event*/) +{ + interrupted = true; + return TRUE; +} // end of sp_export_progress_delete() + + +/// Called when progress is cancelled +void Export::onProgressCancel () +{ + interrupted = true; +} // end of sp_export_progress_cancel() + + +/// Called for every progress iteration +unsigned int Export::onProgressCallback(float value, void *dlg) +{ + Gtk::Dialog *dlg2 = reinterpret_cast<Gtk::Dialog*>(dlg); + + Export *self = reinterpret_cast<Export *>(dlg2->get_data("exportPanel")); + if (self->interrupted) + return FALSE; + + gint current = GPOINTER_TO_INT(dlg2->get_data("current")); + gint total = GPOINTER_TO_INT(dlg2->get_data("total")); + if (total > 0) { + double completed = current; + completed /= static_cast<double>(total); + + value = completed + (value / static_cast<double>(total)); + } + + Gtk::ProgressBar *prg = reinterpret_cast<Gtk::ProgressBar *>(dlg2->get_data("progress")); + prg->set_fraction(value); + + if (self) { + self->_prog.set_fraction(value); + } + + int evtcount = 0; + while ((evtcount < 16) && gdk_events_pending()) { + gtk_main_iteration_do(FALSE); + evtcount += 1; + } + + gtk_main_iteration_do(FALSE); + return TRUE; +} // end of sp_export_progress_callback() + +void Export::setExporting(bool exporting, Glib::ustring const &text) +{ + if (exporting) { + _prog.set_text(text); + _prog.set_fraction(0.0); + _prog.set_sensitive(true); + + export_button.set_sensitive(false); + } else { + _prog.set_text(""); + _prog.set_fraction(0.0); + _prog.set_sensitive(false); + + export_button.set_sensitive(true); + } +} + +Gtk::Dialog * Export::create_progress_dialog (Glib::ustring progress_text) { + Gtk::Dialog *dlg = new Gtk::Dialog(_("Export in progress"), TRUE); + dlg->set_transient_for( *(INKSCAPE.active_desktop()->getToplevel()) ); + + Gtk::ProgressBar *prg = new Gtk::ProgressBar (); + prg->set_text(progress_text); + dlg->set_data ("progress", prg); + auto CA = dlg->get_content_area(); + CA->pack_start(*prg, FALSE, FALSE, 4); + + Gtk::Button* btn = dlg->add_button (_("_Cancel"),Gtk::RESPONSE_CANCEL ); + + btn->signal_clicked().connect( sigc::mem_fun(*this, &Export::onProgressCancel) ); + dlg->signal_delete_event().connect( sigc::mem_fun(*this, &Export::onProgressDelete) ); + + dlg->show_all (); + return dlg; +} + +// FIXME: Some lib function should be available to do this ... +Glib::ustring Export::filename_add_extension (Glib::ustring filename, Glib::ustring extension) +{ + auto pos = int(filename.size()) - int(extension.size()); + if (pos > 0 && filename[pos - 1] == '.' && filename.substr(pos).lowercase() == extension.lowercase()) { + return filename; + } + + return filename + "." + extension; +} + +Glib::ustring Export::absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename) +{ + Glib::ustring path; + //Make relative paths go from the document location, if possible: + if (!Glib::path_is_absolute(filename) && doc->getDocumentURI()) { + Glib::ustring dirname = Glib::path_get_dirname(doc->getDocumentURI()); + if (!dirname.empty()) { + path = Glib::build_filename(dirname, filename); + } + } + if (path.empty()) { + path = filename; + } + return path; +} + +// Called when unit is changed +void Export::onUnitChanged() +{ + onAreaToggled(); +} + +void Export::onHideExceptSelected () +{ + prefs->setBool("/dialogs/export/hideexceptselected/value", hide_export.get_active()); +} + +/// Called when export button is clicked +void Export::onExport () +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) return; + + SPNamedView *nv = desktop->getNamedView(); + SPDocument *doc = desktop->getDocument(); + + bool exportSuccessful = false; + + bool hide = hide_export.get_active (); + + // Advanced parameters + bool do_interlace = (interlacing.get_active()); + float pHYs = 0; + int zlib = zlib_compression.get_active_row_number() ; + const char* const modes_list[]={"Gray_1", "Gray_2","Gray_4","Gray_8","Gray_16","RGB_8","RGB_16","GrayAlpha_8","GrayAlpha_16","RGBA_8","RGBA_16"}; + int colortypes[] = {0,0,0,0,0,2,2,4,4,6,6}; //keep in sync with modes_list in Export constructor. values are from libpng doc. + int bitdepths[] = {1,2,4,8,16,8,16,8,16,8,16}; + int color_type = colortypes[bitdepth_cb.get_active_row_number()] ; + int bit_depth = bitdepths[bitdepth_cb.get_active_row_number()] ; + int antialiasing = antialiasing_cb.get_active_row_number(); + + + if (batch_export.get_active ()) { + // Batch export of selected objects + + gint num = (gint) boost::distance(desktop->getSelection()->items()); + gint n = 0; + + if (num < 1) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No items selected.")); + return; + } + + prog_dlg = create_progress_dialog(Glib::ustring::compose(_("Exporting %1 files"), num)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 files"), num)); + + gint export_count = 0; + + auto itemlist= desktop->getSelection()->items(); + for(auto i = itemlist.begin();i!=itemlist.end() && !interrupted ;++i){ + SPItem *item = *i; + + prog_dlg->set_data("current", GINT_TO_POINTER(n)); + prog_dlg->set_data("total", GINT_TO_POINTER(num)); + onProgressCallback(0.0, prog_dlg); + + // retrieve export filename hint + const gchar *filename = item->getRepr()->attribute("inkscape:export-filename"); + Glib::ustring path; + if (!filename) { + Glib::ustring tmp; + path = create_filepath_from_id(item->getId(), tmp); + } else { + path = absolutize_path_from_document_location(doc, filename); + } + + // retrieve export dpi hints + const gchar *dpi_hint = item->getRepr()->attribute("inkscape:export-xdpi"); // only xdpi, ydpi is always the same now + gdouble dpi = 0.0; + if (dpi_hint) { + dpi = atof(dpi_hint); + } + if (dpi == 0.0) { + dpi = getValue(xdpi_adj); + } + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : dpi; + + Geom::OptRect area = item->documentVisualBounds(); + if (area) { + gint width = (gint) (area->width() * dpi / DPI_BASE + 0.5); + gint height = (gint) (area->height() * dpi / DPI_BASE + 0.5); + + if (width > 1 && height > 1) { + // Do export + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + MessageCleaner msgCleanup(desktop->messageStack()->pushF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file <b>%s</b>..."), safeFile), desktop); + MessageCleaner msgFlashCleanup(desktop->messageStack()->flashF(Inkscape::IMMEDIATE_MESSAGE, + _("Exporting file <b>%s</b>..."), safeFile), desktop); + std::vector<SPItem*> x; + std::vector<SPItem*> selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + if (!sp_export_png_file (doc, path.c_str(), + *area, width, height, pHYs, pHYs, + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + TRUE, // overwrite without asking + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + )) { + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flashF(Inkscape::ERROR_MESSAGE, + _("Could not export to filename <b>%s</b>."), safeFile); + + sp_ui_error_dialog(error); + g_free(error); + } else { + ++export_count; // one more item exported successfully + } + g_free(safeFile); + } + } + + n++; + } + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, + _("Successfully exported <b>%d</b> files from <b>%d</b> selected items."), export_count, num); + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + exportSuccessful = (export_count > 0); + } else { + Glib::ustring filename = filename_entry.get_text(); + + if (filename.empty()) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("You have to enter a filename.")); + sp_ui_error_dialog(_("You have to enter a filename")); + return; + } + + float const x0 = getValuePx(x0_adj); + float const y0 = getValuePx(y0_adj); + float const x1 = getValuePx(x1_adj); + float const y1 = getValuePx(y1_adj); + float const xdpi = getValue(xdpi_adj); + float const ydpi = getValue(ydpi_adj); + pHYs = (pHYs_adj->get_value() > 0.01) ? pHYs_adj->get_value() : xdpi; + unsigned long int const width = int(getValue(bmwidth_adj) + 0.5); + unsigned long int const height = int(getValue(bmheight_adj) + 0.5); + + if (!((x1 > x0) && (y1 > y0) && (width > 0) && (height > 0))) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The chosen area to be exported is invalid.")); + sp_ui_error_dialog(_("The chosen area to be exported is invalid")); + return; + } + + // make sure that .png is the extension of the file: + Glib::ustring const filename_ext = filename_add_extension(filename, "png"); + filename_entry.set_text(filename_ext); + filename_entry.set_position(filename_ext.length()); + Glib::ustring path = absolutize_path_from_document_location(doc, filename_ext); + + Glib::ustring dirname = Glib::path_get_dirname(path); + if ( dirname.empty() + || !Inkscape::IO::file_test(dirname.c_str(), (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) ) + { + gchar *safeDir = Inkscape::IO::sanitizeString(dirname.c_str()); + gchar *error = g_strdup_printf(_("Directory %s does not exist or is not a directory.\n"), + safeDir); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeDir); + g_free(error); + return; + } + + Glib::ustring fn = path_get_basename (path); + + /* TRANSLATORS: %1 will be the filename, %2 the width, and %3 the height of the image */ + prog_dlg = create_progress_dialog (Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), fn, width, height)); + prog_dlg->set_data("exportPanel", this); + setExporting(true, Glib::ustring::compose(_("Exporting %1 (%2 x %3)"), fn, width, height)); + + prog_dlg->set_data("current", GINT_TO_POINTER(0)); + prog_dlg->set_data("total", GINT_TO_POINTER(0)); + + auto area = Geom::Rect(Geom::Point(x0, y0), Geom::Point(x1, y1)) * desktop->dt2doc(); + + /* Do export */ + std::vector<SPItem*> x; + std::vector<SPItem*> selected(desktop->getSelection()->items().begin(), desktop->getSelection()->items().end()); + ExportResult status = sp_export_png_file(desktop->getDocument(), path.c_str(), + area, width, height, pHYs, pHYs, //previously xdpi, ydpi. + nv->pagecolor, + onProgressCallback, (void*)prog_dlg, + FALSE, + hide ? selected : x, + do_interlace, color_type, bit_depth, zlib, antialiasing + ); + if (status == EXPORT_ERROR) { + gchar * safeFile = Inkscape::IO::sanitizeString(path.c_str()); + gchar * error = g_strdup_printf(_("Could not export to filename %s.\n"), safeFile); + + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, error); + sp_ui_error_dialog(error); + + g_free(safeFile); + g_free(error); + } else if (status == EXPORT_OK) { + exportSuccessful = true; + gchar *safeFile = Inkscape::IO::sanitizeString(path.c_str()); + + desktop->messageStack()->flashF(Inkscape::INFORMATION_MESSAGE, _("Drawing exported to <b>%s</b>."), safeFile); + + g_free(safeFile); + } else { + desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Export aborted.")); + } + + /* Reset the filename so that it can be changed again by changing + selections and all that */ + original_name = filename_ext; + filename_modified = false; + + setExporting(false); + delete prog_dlg; + prog_dlg = nullptr; + interrupted = false; + + /* Setup the values in the document */ + switch (current_key) { + case SELECTION_PAGE: + case SELECTION_DRAWING: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + Inkscape::XML::Node * repr = doc->getReprRoot(); + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + + gchar const *temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + case SELECTION_SELECTION: { + SPDocument * doc = SP_ACTIVE_DOCUMENT; + bool modified = false; + + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + auto reprlst = desktop->getSelection()->xmlNodes(); + + for(auto i=reprlst.begin(); reprlst.end() != i; ++i) { + Inkscape::XML::Node * repr = *i; + const gchar * temp_string; + Glib::ustring dir = Glib::path_get_dirname(filename.c_str()); + const gchar* docURI=SP_ACTIVE_DOCUMENT->getDocumentURI(); + Glib::ustring docdir; + if (docURI) + { + docdir = Glib::path_get_dirname(docURI); + } + if (repr->attribute("id") == nullptr || + !(filename_ext.find_last_of(repr->attribute("id")) && + ( !docURI || + (dir == docdir)))) { + temp_string = repr->attribute("inkscape:export-filename"); + if (temp_string == nullptr || (filename_ext != temp_string)) { + repr->setAttribute("inkscape:export-filename", filename_ext); + modified = true; + } + } + temp_string = repr->attribute("inkscape:export-xdpi"); + if (temp_string == nullptr || xdpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-xdpi", xdpi); + modified = true; + } + temp_string = repr->attribute("inkscape:export-ydpi"); + if (temp_string == nullptr || ydpi != atof(temp_string)) { + sp_repr_set_svg_double(repr, "inkscape:export-ydpi", ydpi); + modified = true; + } + } + DocumentUndo::setUndoSensitive(doc, saved); + + if (modified) { + doc->setModifiedSinceSave(); + } + break; + } + default: + break; + } + } + + if (exportSuccessful && closeWhenDone.get_active()) { + for ( Gtk::Container *parent = get_parent(); parent; parent = parent->get_parent()) { + if ( GDL_IS_DOCK_ITEM(parent->gobj()) ) { + GdlDockItem *item = GDL_DOCK_ITEM(parent->gobj()); + if (item) { + gdl_dock_item_hide_item(item); + } + break; + } + } + } +} // end of sp_export_export_clicked() + +/// Called when Browse button is clicked +/// @todo refactor this code to use ui/dialogs/filedialog.cpp +void Export::onBrowse () +{ + GtkWidget *fs; + Glib::ustring filename; + + fs = gtk_file_chooser_dialog_new (_("Select a filename for exporting"), + (GtkWindow*)desktop->getToplevel(), + GTK_FILE_CHOOSER_ACTION_SAVE, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, + NULL ); + + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(fs), false); + + sp_transientize (fs); + + gtk_window_set_modal(GTK_WINDOW (fs), true); + + filename = filename_entry.get_text(); + + if (filename.empty()) { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + + gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (fs), filename.c_str()); + +#ifdef _WIN32 + // code in this section is borrowed from ui/dialogs/filedialogimpl-win32.cpp + OPENFILENAMEW opf; + WCHAR filter_string[20]; + wcsncpy(filter_string, L"PNG#*.png##", 11); + filter_string[3] = L'\0'; + filter_string[9] = L'\0'; + filter_string[10] = L'\0'; + WCHAR* title_string = (WCHAR*)g_utf8_to_utf16(_("Select a filename for exporting"), -1, NULL, NULL, NULL); + WCHAR* extension_string = (WCHAR*)g_utf8_to_utf16("*.png", -1, NULL, NULL, NULL); + // Copy the selected file name, converting from UTF-8 to UTF-16 + std::string dirname = Glib::path_get_dirname(filename.raw()); + if ( !Glib::file_test(dirname, Glib::FILE_TEST_EXISTS) || + Glib::file_test(filename, Glib::FILE_TEST_IS_DIR) || + dirname.empty() ) + { + Glib::ustring tmp; + filename = create_filepath_from_id(tmp, tmp); + } + WCHAR _filename[_MAX_PATH + 1]; + memset(_filename, 0, sizeof(_filename)); + gunichar2* utf16_path_string = g_utf8_to_utf16(filename.c_str(), -1, NULL, NULL, NULL); + wcsncpy(_filename, reinterpret_cast<wchar_t*>(utf16_path_string), _MAX_PATH); + g_free(utf16_path_string); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Glib::RefPtr<const Gdk::Window> parentWindow = desktop->getToplevel()->get_window(); + g_assert(parentWindow->gobj() != NULL); + + opf.hwndOwner = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); + opf.lpstrFilter = filter_string; + opf.lpstrCustomFilter = 0; + opf.nMaxCustFilter = 0L; + opf.nFilterIndex = 1L; + opf.lpstrFile = _filename; + opf.nMaxFile = _MAX_PATH; + opf.lpstrFileTitle = NULL; + opf.nMaxFileTitle=0; + opf.lpstrInitialDir = 0; + opf.lpstrTitle = title_string; + opf.nFileOffset = 0; + opf.nFileExtension = 2; + opf.lpstrDefExt = extension_string; + opf.lpfnHook = NULL; + opf.lCustData = 0; + opf.Flags = OFN_PATHMUSTEXIST; + opf.lStructSize = sizeof(OPENFILENAMEW); + if (GetSaveFileNameW(&opf) != 0) + { + // Copy the selected file name, converting from UTF-16 to UTF-8 + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)opf.lpstrFile, _MAX_PATH, NULL, NULL, NULL); + filename_entry.set_text(utf8string); + filename_entry.set_position(strlen(utf8string)); + g_free(utf8string); + + } + g_free(extension_string); + g_free(title_string); + +#else + if (gtk_dialog_run (GTK_DIALOG (fs)) == GTK_RESPONSE_ACCEPT) + { + gchar *file; + + file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (fs)); + + gchar * utf8file = g_filename_to_utf8( file, -1, nullptr, nullptr, nullptr ); + filename_entry.set_text (utf8file); + filename_entry.set_position(strlen(utf8file)); + + g_free(utf8file); + g_free(file); + } +#endif + + gtk_widget_destroy (fs); + + return; +} // end of sp_export_browse_clicked() + +// TODO: Move this to nr-rect-fns.h. +bool Export::bbox_equal(Geom::Rect const &one, Geom::Rect const &two) +{ + double const epsilon = pow(10.0, -EXPORT_COORD_PRECISION); + return ( + (fabs(one.min()[Geom::X] - two.min()[Geom::X]) < epsilon) && + (fabs(one.min()[Geom::Y] - two.min()[Geom::Y]) < epsilon) && + (fabs(one.max()[Geom::X] - two.max()[Geom::X]) < epsilon) && + (fabs(one.max()[Geom::Y] - two.max()[Geom::Y]) < epsilon) + ); +} + +/** + *This function is used to detect the current selection setting + * based on the values in the x0, y0, x1 and y0 fields. + * + * One of the most confusing parts of this function is why the array + * is built at the beginning. What needs to happen here is that we + * should always check the current selection to see if it is the valid + * one. While this is a performance improvement it is also a usability + * one during the cases where things like selections and drawings match + * size. This way buttons change less 'randomly' (at least in the eyes + * of the user). To do this an array is built where the current selection + * type is placed first, and then the others in an order from smallest + * to largest (this can be configured by reshuffling \c test_order). + * + * All of the values in this function are rounded to two decimal places + * because that is what is shown to the user. While everything is kept + * more accurate than that, the user can't control more accurate than + * that, so for this to work for them - it needs to check on that level + * of accuracy. + * + * @todo finish writing this up. + */ +void Export::detectSize() { + static const selection_type test_order[SELECTION_NUMBER_OF] = {SELECTION_SELECTION, SELECTION_DRAWING, SELECTION_PAGE, SELECTION_CUSTOM}; + selection_type this_test[SELECTION_NUMBER_OF + 1]; + selection_type key = SELECTION_NUMBER_OF; + + Geom::Point x(getValuePx(x0_adj), + getValuePx(y0_adj)); + Geom::Point y(getValuePx(x1_adj), + getValuePx(y1_adj)); + Geom::Rect current_bbox(x, y); + + this_test[0] = current_key; + for (int i = 0; i < SELECTION_NUMBER_OF; i++) { + this_test[i + 1] = test_order[i]; + } + + for (int i = 0; + i < SELECTION_NUMBER_OF + 1 && + key == SELECTION_NUMBER_OF && + SP_ACTIVE_DESKTOP != nullptr; + i++) { + switch (this_test[i]) { + case SELECTION_SELECTION: + if ((SP_ACTIVE_DESKTOP->getSelection())->isEmpty() == false) { + Geom::OptRect bbox = (SP_ACTIVE_DESKTOP->getSelection())->bounds(SPItem::VISUAL_BBOX); + + if ( bbox && bbox_equal(*bbox,current_bbox)) { + key = SELECTION_SELECTION; + } + } + break; + case SELECTION_DRAWING: { + SPDocument *doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::OptRect bbox = doc->getRoot()->desktopVisualBounds(); + + if ( bbox && bbox_equal(*bbox,current_bbox) ) { + key = SELECTION_DRAWING; + } + break; + } + + case SELECTION_PAGE: { + SPDocument *doc; + + doc = SP_ACTIVE_DESKTOP->getDocument(); + + Geom::Point x(0.0, 0.0); + Geom::Point y(doc->getWidth().value("px"), + doc->getHeight().value("px")); + Geom::Rect bbox(x, y); + + if (bbox_equal(bbox,current_bbox)) { + key = SELECTION_PAGE; + } + + break; + } + default: + break; + } + } + // std::cout << std::endl; + + if (key == SELECTION_NUMBER_OF) { + key = SELECTION_CUSTOM; + } + + current_key = key; + selectiontype_buttons[current_key]->set_active(true); + + return; +} /* sp_export_detect_size */ + +/// Called when area x0 value is changed +void Export::areaXChange(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float x0, x1, xdpi, width; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + width = floor ((x1 - x0) * xdpi / DPI_BASE + 0.5); + + if (width < SP_EXPORT_MIN_SIZE) { + width = SP_EXPORT_MIN_SIZE; + + if (adj == x1_adj) { + x1 = x0 + width * DPI_BASE / xdpi; + setValuePx(x1_adj, x1); + } else { + x0 = x1 - width * DPI_BASE / xdpi; + setValuePx(x0_adj, x0); + } + } + + setValuePx(width_adj, x1 - x0); + setValue(bmwidth_adj, width); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_x_value_changed() + +/// Called when area y0 value is changed. +void Export::areaYChange(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float y0, y1, ydpi, height; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + ydpi = getValue(ydpi_adj); + + height = floor ((y1 - y0) * ydpi / DPI_BASE + 0.5); + + if (height < SP_EXPORT_MIN_SIZE) { + //const gchar *key; + height = SP_EXPORT_MIN_SIZE; + //key = (const gchar *)g_object_get_data(G_OBJECT (adj), "key"); + if (adj == y1_adj) { + //if (!strcmp (key, "y0")) { + y1 = y0 + height * DPI_BASE / ydpi; + setValuePx(y1_adj, y1); + } else { + y0 = y1 - height * DPI_BASE / ydpi; + setValuePx(y0_adj, y0); + } + } + + setValuePx(height_adj, y1 - y0); + setValue(bmheight_adj, height); + + detectSize(); + + update = false; + + return; +} // end of sp_export_area_y_value_changed() + +/// Called when x1-x0 or area width is changed +void Export::onAreaWidthChange() +{ + if (update) { + return; + } + + update = true; + + float x0 = getValuePx(x0_adj); + float xdpi = getValue(xdpi_adj); + float width = getValuePx(width_adj); + float bmwidth = floor(width * xdpi / DPI_BASE + 0.5); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + + bmwidth = SP_EXPORT_MIN_SIZE; + width = bmwidth * DPI_BASE / xdpi; + setValuePx(width_adj, width); + } + + setValuePx(x1_adj, x0 + width); + setValue(bmwidth_adj, bmwidth); + + update = false; + + return; +} // end of sp_export_area_width_value_changed() + +/// Called when y1-y0 or area height is changed. +void Export::onAreaHeightChange() +{ + if (update) { + return; + } + + update = true; + + float y0 = getValuePx(y0_adj); + //float y1 = sp_export_value_get_px(y1_adj); + float ydpi = getValue(ydpi_adj); + float height = getValuePx(height_adj); + float bmheight = floor (height * ydpi / DPI_BASE + 0.5); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + height = bmheight * DPI_BASE / ydpi; + setValuePx(height_adj, height); + } + + setValuePx(y1_adj, y0 + height); + setValue(bmheight_adj, bmheight); + + update = false; + + return; +} // end of sp_export_area_height_value_changed() + +/** + * A function to set the ydpi. + * @param base The export dialog. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ +void Export::setImageY() +{ + float y0, y1, xdpi; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmheight_adj, (y1 - y0) * xdpi / DPI_BASE); + + return; +} // end of setImageY() + +/** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ +void Export::setImageX() +{ + float x0, x1, xdpi; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + setValue(ydpi_adj, xdpi); + setValue(bmwidth_adj, (x1 - x0) * xdpi / DPI_BASE); + + return; +} // end of setImageX() + +/// Called when pixel width is changed +void Export::onBitmapWidthChange () +{ + float x0, x1, bmwidth, xdpi; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + bmwidth = getValue(bmwidth_adj); + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + setValue(bmwidth_adj, bmwidth); + } + + xdpi = bmwidth * DPI_BASE / (x1 - x0); + setValue(xdpi_adj, xdpi); + + setImageY (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/// Called when pixel height is changed +void Export::onBitmapHeightChange () +{ + float y0, y1, bmheight, xdpi; + + if (update) { + return; + } + + update = true; + + y0 = getValuePx(y0_adj); + y1 = getValuePx(y1_adj); + bmheight = getValue(bmheight_adj); + + if (bmheight < SP_EXPORT_MIN_SIZE) { + bmheight = SP_EXPORT_MIN_SIZE; + setValue(bmheight_adj, bmheight); + } + + xdpi = bmheight * DPI_BASE / (y1 - y0); + setValue(xdpi_adj, xdpi); + + setImageX (); + + update = false; + + return; +} // end of sp_export_bitmap_width_value_changed() + +/** + * A function to adjust the bitmap width when the xdpi value changes. + * + * The first thing this function checks is to see if we are doing an + * update. If we are, this function just returns because there is another + * instance of it that will handle everything for us. If there is a + * units change, we also assume that everyone is being updated appropriately + * and there is nothing for us to do. + * + * If we're the highest level function, we set the update flag, and + * continue on our way. + * + * All of the values are grabbed using the \c sp_export_value_get functions + * (call to the _pt ones for x0 and x1 but just standard for xdpi). The + * xdpi value is saved in the preferences for the next time the dialog + * is opened. (does the selection dpi need to be set here?) + * + * A check is done to to ensure that we aren't outputting an invalid width, + * this is set by SP_EXPORT_MIN_SIZE. If that is the case the dpi is + * changed to make it valid. + * + * After all of this the bitmap width is changed. + * + * We also change the ydpi. This is a temporary hack as these can not + * currently be independent. This is likely to change in the future. + * + */ +void Export::onExportXdpiChange() +{ + float x0, x1, xdpi, bmwidth; + + if (update) { + return; + } + + update = true; + + x0 = getValuePx(x0_adj); + x1 = getValuePx(x1_adj); + xdpi = getValue(xdpi_adj); + + // remember xdpi setting + prefs->setDouble("/dialogs/export/defaultxdpi/value", xdpi); + + bmwidth = (x1 - x0) * xdpi / DPI_BASE; + + if (bmwidth < SP_EXPORT_MIN_SIZE) { + bmwidth = SP_EXPORT_MIN_SIZE; + if (x1 != x0) + xdpi = bmwidth * DPI_BASE / (x1 - x0); + else + xdpi = DPI_BASE; + setValue(xdpi_adj, xdpi); + } + + setValue(bmwidth_adj, bmwidth); + + setImageY (); + + update = false; + + return; +} // end of sp_export_xdpi_value_changed() + + +/** + * A function to change the area that is used for the exported. + * bitmap. + * + * This function just calls \c sp_export_value_set_px for each of the + * parameters that is passed in. This allows for setting them all in + * one convenient area. + * + * Update is set to suspend all of the other test running while all the + * values are being set up. This allows for a performance increase, but + * it also means that the wrong type won't be detected with only some of + * the values set. After all the values are set everyone is told that + * there has been an update. + * + * @param x0 Horizontal upper left hand corner of the picture in points. + * @param y0 Vertical upper left hand corner of the picture in points. + * @param x1 Horizontal lower right hand corner of the picture in points. + * @param y1 Vertical lower right hand corner of the picture in points. + */ +void Export::setArea( double x0, double y0, double x1, double y1 ) +{ + update = true; + setValuePx(x1_adj, x1); + setValuePx(y1_adj, y1); + setValuePx(x0_adj, x0); + setValuePx(y0_adj, y0); + update = false; + + areaXChange (x1_adj); + areaYChange (y1_adj); + + return; +} + +/** + * Sets the value of an adjustment. + * + * @param adj The adjustment widget + * @param val What value to set it to. + */ +void Export::setValue(Glib::RefPtr<Gtk::Adjustment>& adj, double val ) +{ + if (adj) { + adj->set_value(val); + } +} + +/** + * A function to set a value using the units points. + * + * This function first gets the adjustment for the key that is passed + * in. It then figures out what units are currently being used in the + * dialog. After doing all of that, it then converts the incoming + *value and sets the adjustment. + * + * @param adj The adjustment widget + * @param val What the value should be in points. + */ +void Export::setValuePx(Glib::RefPtr<Gtk::Adjustment>& adj, double val) +{ + Unit const *unit = unit_selector.getUnit(); + + setValue(adj, Inkscape::Util::Quantity::convert(val, "px", unit)); + + return; +} + +/** + * Get the value of an adjustment in the export dialog. + * + * This function gets the adjustment from the data field in the export + * dialog. It then grabs the value from the adjustment. + * + * @param adj The adjustment widget + * + * @return The value in the specified adjustment. + */ +float Export::getValue(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + if (!adj) { + g_message("sp_export_value_get : adj is NULL"); + return 0.0; + } + return adj->get_value(); +} + +/** + * Grabs a value in the export dialog and converts the unit + * to points. + * + * This function, at its most basic, is a call to \c sp_export_value_get + * to get the value of the adjustment. It then finds the units that + * are being used by looking at the "units" attribute of the export + * dialog. Using that it converts the returned value into points. + * + * @param adj The adjustment widget + * + * @return The value in the adjustment in points. + */ +float Export::getValuePx(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + float value = getValue( adj); + Unit const *unit = unit_selector.getUnit(); + + return Inkscape::Util::Quantity::convert(value, unit, "px"); +} // end of sp_export_value_get_px() + +/** + * This function is called when the filename is changed by + * anyone. It resets the virgin bit. + * + * This function gets called when the text area is modified. It is + * looking for the case where the text area is modified from its + * original value. In that case it sets the "filename-modified" bit + * to TRUE. If the text dialog returns back to the original text, the + * bit gets reset. This should stop simple mistakes. + */ +void Export::onFilenameModified() +{ + if (original_name == filename_entry.get_text()) { + filename_modified = false; + } else { + filename_modified = true; + } + + return; +} // end sp_export_filename_modified + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/export.h b/src/ui/dialog/export.h new file mode 100644 index 0000000..9210ac5 --- /dev/null +++ b/src/ui/dialog/export.h @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 1999-2007 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SP_EXPORT_H +#define SP_EXPORT_H + +#include <gtkmm/progressbar.h> +#include <gtkmm/expander.h> +#include <gtkmm/grid.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** What type of button is being pressed */ +enum selection_type { + SELECTION_PAGE = 0, /**< Export the whole page */ + SELECTION_DRAWING, /**< Export everything drawn on the page */ + SELECTION_SELECTION, /**< Export everything that is selected */ + SELECTION_CUSTOM, /**< Allows the user to set the region exported */ + SELECTION_NUMBER_OF /**< A counter for the number of these guys */ +}; + +/** + * A dialog widget to export to various image formats such as bitmap and png. + * + * Creates a dialog window for exporting an image to a bitmap if one doesn't already exist and + * shows it to the user. If the dialog has already been created, it simply shows the window. + * + */ +class Export : public Widget::Panel { +public: + Export (); + ~Export () override; + + static Export &getInstance() { + return *new Export(); + } + +private: + + /** + * A function to set the xdpi. + * + * This function grabs all of the x values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + * + */ + void setImageX(); + + /** + * A function to set the ydpi. + * + * This function grabs all of the y values and then figures out the + * new bitmap size based on the changing dpi value. The dpi value is + * gotten from the xdpi setting as these can not currently be independent. + */ + void setImageY(); + bool bbox_equal(Geom::Rect const &one, Geom::Rect const &two); + void updateCheckbuttons (); + inline void findDefaultSelection(); + void detectSize(); + void setArea ( double x0, double y0, double x1, double y1); + /* + * Getter/setter style functions for the spinbuttons + */ + void setValue(Glib::RefPtr<Gtk::Adjustment>& adj, double val); + void setValuePx(Glib::RefPtr<Gtk::Adjustment>& adj, double val); + float getValue(Glib::RefPtr<Gtk::Adjustment>& adj); + float getValuePx(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Helper function to create, style and pack spinbuttons for the export dialog. + * + * Creates a new spin button for the export dialog. + * @param key The name of the spin button + * @param val A default value for the spin button + * @param min Minimum value for the spin button + * @param max Maximum value for the spin button + * @param step The step size for the spin button + * @param page Size of the page increment + * @param t Table to put the spin button in + * @param x X location in the table \c t to start with + * @param y Y location in the table \c t to start with + * @param ll Text to put on the left side of the spin button (optional) + * @param lr Text to put on the right side of the spin button (optional) + * @param digits Number of digits to display after the decimal + * @param sensitive Whether the spin button is sensitive or not + * @param cb Callback for when this spin button is changed (optional) + * + * No unit_selector is stored in the created spinbutton, relies on external unit management + */ + Glib::RefPtr<Gtk::Adjustment> createSpinbutton( gchar const *key, float val, float min, float max, + float step, float page, + Gtk::Grid *t, int x, int y, + const Glib::ustring& ll, const Glib::ustring& lr, + int digits, unsigned int sensitive, + void (Export::*cb)() ); + + /** + * One of the area select radio buttons was pressed + */ + void onAreaToggled(); + + /** + * Export button callback + */ + void onExport (); + + /** + * File Browse button callback + */ + void onBrowse (); + + /** + * Area X value changed callback + */ + void onAreaX0Change() { + areaXChange(x0_adj); + } ; + void onAreaX1Change() { + areaXChange(x1_adj); + } ; + void areaXChange(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Area Y value changed callback + */ + void onAreaY0Change() { + areaYChange(y0_adj); + } ; + void onAreaY1Change() { + areaYChange(y1_adj); + } ; + void areaYChange(Glib::RefPtr<Gtk::Adjustment>& adj); + + /** + * Unit changed callback + */ + void onUnitChanged(); + + /** + * Hide except selected callback + */ + void onHideExceptSelected (); + + /** + * Area width value changed callback + */ + void onAreaWidthChange (); + + /** + * Area height value changed callback + */ + void onAreaHeightChange (); + + /** + * Bitmap width value changed callback + */ + void onBitmapWidthChange (); + + /** + * Bitmap height value changed callback + */ + void onBitmapHeightChange (); + + /** + * Export xdpi value changed callback + */ + void onExportXdpiChange (); + + /** + * Batch export callback + */ + void onBatchClicked (); + + /** + * Inkscape selection change callback + */ + void onSelectionChanged (); + void onSelectionModified (guint flags); + + /** + * Filename modified callback + */ + void onFilenameModified (); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Creates progress dialog for batch exporting. + * + * @param progress_text Text to be shown in the progress bar + */ + Gtk::Dialog * create_progress_dialog (Glib::ustring progress_text); + + /** + * Callback to be used in for loop to update the progress bar. + * + * @param value number between 0 and 1 indicating the fraction of progress (0.17 = 17 % progress) + * @param dlg void pointer to the Gtk::Dialog progress dialog + */ + static unsigned int onProgressCallback(float value, void *dlg); + + /** + * Callback for pressing the cancel button. + */ + void onProgressCancel (); + + /** + * Callback invoked on closing the progress dialog. + */ + bool onProgressDelete (GdkEventAny *event); + + /** + * Handles state changes as exporting starts or stops. + */ + void setExporting(bool exporting, Glib::ustring const &text = ""); + + /* + * Utility filename and path functions + */ + void set_default_filename (); + Glib::ustring create_filepath_from_id (Glib::ustring id, const Glib::ustring &file_entry_text); + Glib::ustring filename_add_extension (Glib::ustring filename, Glib::ustring extension); + Glib::ustring absolutize_path_from_document_location (SPDocument *doc, const Glib::ustring &filename); + + /* + * Currently selected export area type + */ + selection_type current_key; + /* + * Original name for the export object + */ + Glib::ustring original_name; + Glib::ustring doc_export_name; + /* + * Was the Original name modified + */ + bool filename_modified; + bool was_empty; + /* + * Flag to stop simultaneous updates + */ + bool update; + + /* Area selection radio buttons */ + Gtk::HBox togglebox; + Gtk::RadioButton *selectiontype_buttons[SELECTION_NUMBER_OF]; + + Gtk::VBox area_box; + Gtk::VBox singleexport_box; + + /* Custom size widgets */ + Glib::RefPtr<Gtk::Adjustment> x0_adj; + Glib::RefPtr<Gtk::Adjustment> x1_adj; + Glib::RefPtr<Gtk::Adjustment> y0_adj; + Glib::RefPtr<Gtk::Adjustment> y1_adj; + Glib::RefPtr<Gtk::Adjustment> width_adj; + Glib::RefPtr<Gtk::Adjustment> height_adj; + + /* Bitmap size widgets */ + Glib::RefPtr<Gtk::Adjustment> bmwidth_adj; + Glib::RefPtr<Gtk::Adjustment> bmheight_adj; + Glib::RefPtr<Gtk::Adjustment> xdpi_adj; + Glib::RefPtr<Gtk::Adjustment> ydpi_adj; + + Gtk::VBox size_box; + Gtk::Label* bm_label; + + Gtk::VBox file_box; + Gtk::Label *flabel; + Gtk::Entry filename_entry; + + /* Unit selector widgets */ + Gtk::HBox unitbox; + Inkscape::UI::Widget::UnitMenu unit_selector; + Gtk::Label units_label; + + /* Filename widgets */ + Gtk::HBox filename_box; + Gtk::Button browse_button; + Gtk::Label browse_label; + Gtk::Image browse_image; + + Gtk::HBox batch_box; + Gtk::CheckButton batch_export; + + Gtk::HBox hide_box; + Gtk::CheckButton hide_export; + + Gtk::CheckButton closeWhenDone; + + /* Advanced */ + Gtk::Expander expander; + Gtk::CheckButton interlacing; + Gtk::Label bitdepth_label; + Gtk::ComboBoxText bitdepth_cb; + Gtk::Label zlib_label; + Gtk::ComboBoxText zlib_compression; + Gtk::Label pHYs_label; + Glib::RefPtr<Gtk::Adjustment> pHYs_adj; + Gtk::SpinButton pHYs_sb; + Gtk::Label antialiasing_label; + Gtk::ComboBoxText antialiasing_cb; + + /* Export Button widgets */ + Gtk::HBox button_box; + Gtk::Button export_button; + + Gtk::ProgressBar _prog; + + Gtk::Dialog *prog_dlg; + bool interrupted; // indicates whether export needs to be interrupted (read: user pressed cancel in the progress dialog) + + Inkscape::Preferences *prefs; + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection unitChangedConn; + +}; + +} +} +} +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/extension-editor.cpp b/src/ui/dialog/extension-editor.cpp new file mode 100644 index 0000000..1852010 --- /dev/null +++ b/src/ui/dialog/extension-editor.cpp @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Extension editor dialog. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension-editor.h" +#include <glibmm/i18n.h> + +#include <gtkmm/frame.h> +#include <gtkmm/notebook.h> + +#include "verbs.h" +#include "preferences.h" +#include "ui/interface.h" + +#include "extension/db.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Create a new ExtensionEditor dialog. + * + * This function creates a new extension editor dialog. The dialog + * consists of two basic areas. The left side is a tree widget, which + * is only used as a list. And the right side is a notebook of information + * about the selected extension. A handler is set up so that when + * a new extension is selected, the notebooks are changed appropriately. + */ +ExtensionEditor::ExtensionEditor() + : UI::Widget::Panel("/dialogs/extensioneditor", SP_VERB_DIALOG_EXTENSIONEDITOR) +{ + _notebook_info.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _notebook_params.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + //Main HBox + Gtk::HBox* hbox_list_page = Gtk::manage(new Gtk::HBox()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + _getContents()->add(*hbox_list_page); + + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _page_list.append_column("name",_page_list_columns._col_name); + Glib::RefPtr<Gtk::TreeSelection> page_list_selection = _page_list.get_selection(); + page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &ExtensionEditor::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + + //Pages + Gtk::VBox* vbox_page = Gtk::manage(new Gtk::VBox()); + hbox_list_page->pack_start(*vbox_page, true, true, 0); + Gtk::Notebook * notebook = Gtk::manage(new Gtk::Notebook()); + notebook->append_page(_notebook_info, *Gtk::manage(new Gtk::Label(_("Information")))); + notebook->append_page(_notebook_params, *Gtk::manage(new Gtk::Label(_("Parameters")))); + vbox_page->pack_start(*notebook, true, true, 0); + + Inkscape::Extension::db.foreach(dbfunc, this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring defaultext = prefs->getString("/dialogs/extensioneditor/selected-extension"); + if (defaultext.empty()) defaultext = "org.inkscape.input.svg"; + this->setExtension(defaultext); + + show_all_children(); +} + +/** + * Destroys the extension editor dialog. + */ +ExtensionEditor::~ExtensionEditor() += default; + +void +ExtensionEditor::setExtension(Glib::ustring extension_id) { + _selection_search = extension_id; + _page_list_model->foreach_iter(sigc::mem_fun(*this, &ExtensionEditor::setExtensionIter)); + return; +} + +bool +ExtensionEditor::setExtensionIter(const Gtk::TreeModel::iterator &iter) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_page_list_columns._col_id] == _selection_search) { + _page_list.get_selection()->select(iter); + return true; + } + return false; +} + +/** + * Called every time a new extension is selected + * + * This function is set up to handle the signal for a changed extension + * from the tree view in the left pane. It figure out which extension + * is selected and updates the widgets to have data for that extension. + */ +void ExtensionEditor::on_pagelist_selection_changed() +{ + Glib::RefPtr<Gtk::TreeSelection> selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + /* Get the row info */ + Gtk::TreeModel::Row row = *iter; + Glib::ustring id = row[_page_list_columns._col_id]; + Glib::ustring name = row[_page_list_columns._col_name]; + + /* Set the selection in the preferences */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/dialogs/extensioneditor/selected-extension", id); + + /* Adjust the dialog's title */ + gchar title[500]; + sp_ui_dialog_title_string (Inkscape::Verb::get(SP_VERB_DIALOG_EXTENSIONEDITOR), title); + Glib::ustring utitle(title); + // set_title(utitle + ": " + name); + + /* Clear the notbook pages */ + _notebook_info.remove(); + _notebook_params.remove(); + + Inkscape::Extension::Extension * ext = Inkscape::Extension::db.get(id.c_str()); + + /* Make sure we have all the widgets */ + Gtk::Widget * info = nullptr; + Gtk::Widget * params = nullptr; + + if (ext != nullptr) { + info = ext->get_info_widget(); + params = ext->get_params_widget(); + } + + /* Place them in the pages */ + if (info != nullptr) { + _notebook_info.add(*info); + } + if (params != nullptr) { + _notebook_params.add(*params); + } + + } + + return; +} + +/** + * A function to pass to the iterator in the Extensions Database. + * + * This function is a static function with the prototype required for + * the Extension Database's foreach function. It will get called for + * every extension in the database, and will then turn around and + * call the more object oriented function \c add_extension in the + * ExtensionEditor. + * + * @param in_plug The extension to evaluate. + * @param in_data A pointer to the Extension Editor class. + */ +void ExtensionEditor::dbfunc(Inkscape::Extension::Extension * in_plug, gpointer in_data) +{ + ExtensionEditor * ee = static_cast<ExtensionEditor *>(in_data); + ee->add_extension(in_plug); + return; +} + +/** + * Adds an extension into the tree model. + * + * This function takes the data out of the extension and puts it + * into the tree model for the dialog. + * + * @param ext The extension to add. + * @return The iterator representing the location in the tree model. + */ +Gtk::TreeModel::iterator ExtensionEditor::add_extension(Inkscape::Extension::Extension * ext) +{ + Gtk::TreeModel::iterator iter; + + iter = _page_list_model->append(); + + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = ext->get_name(); + row[_page_list_columns._col_id] = ext->get_id(); + + return iter; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/extension-editor.h b/src/ui/dialog/extension-editor.h new file mode 100644 index 0000000..403ee1f --- /dev/null +++ b/src/ui/dialog/extension-editor.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Extension editor dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H +#define INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H + +#include "ui/widget/panel.h" + +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/scrolledwindow.h> + +#include "extension/extension.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ExtensionEditor : public UI::Widget::Panel { +public: + ExtensionEditor(); + ~ExtensionEditor() override; + + static ExtensionEditor &getInstance() { return *new ExtensionEditor(); } + +protected: + /** \brief The view of the list of extensions on the left of the dialog */ + Gtk::TreeView _page_list; + /** \brief The model for the list of extensions */ + Glib::RefPtr<Gtk::TreeStore> _page_list_model; + /** \brief The notebook page that contains information */ + Gtk::ScrolledWindow _notebook_info; + /** \brief The notebook page that holds all the parameters */ + Gtk::ScrolledWindow _notebook_params; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + /** \brief Creates the Page List model by adding all of the + members of the class as column records. */ + PageListModelColumns() { + Gtk::TreeModelColumnRecord::add(_col_name); + Gtk::TreeModelColumnRecord::add(_col_id); + } + /** \brief Name of the extension */ + Gtk::TreeModelColumn<Glib::ustring> _col_name; + /** \brief ID of the extension */ + Gtk::TreeModelColumn<Glib::ustring> _col_id; + }; + PageListModelColumns _page_list_columns; + +private: + /** \brief A 'global' variable to help search through and select + an item in the extension list */ + Glib::ustring _selection_search; + + ExtensionEditor(ExtensionEditor const &d) = delete; + ExtensionEditor& operator=(ExtensionEditor const &d) = delete; + + void on_pagelist_selection_changed(); + static void dbfunc (Inkscape::Extension::Extension * in_plug, gpointer in_data); + Gtk::TreeModel::iterator add_extension (Inkscape::Extension::Extension * ext); + bool setExtensionIter(const Gtk::TreeModel::iterator &iter); +public: + void setExtension(Glib::ustring extension_id); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_EXTENSION_EDITOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/extensions.cpp b/src/ui/dialog/extensions.cpp new file mode 100644 index 0000000..f39c7d5 --- /dev/null +++ b/src/ui/dialog/extensions.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog with information about extensions. + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extensions.h" +#include "extension/extension.h" +#include <gtkmm/scrolledwindow.h> + +#include "extension/db.h" + + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +using Inkscape::Extension::Extension; + +ExtensionsPanel &ExtensionsPanel::getInstance() +{ + ExtensionsPanel &instance = *new ExtensionsPanel(); + + instance.rescan(); + + return instance; +} + + +ExtensionsPanel::ExtensionsPanel() : + _showAll(false) +{ + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + + _view.set_editable(false); + + scroller->add(_view); + add(*scroller); + + rescan(); + + show_all_children(); +} + +void ExtensionsPanel::set_full(bool full) +{ + if ( full != _showAll ) { + _showAll = full; + rescan(); + } +} + +void ExtensionsPanel::listCB( Inkscape::Extension::Extension * in_plug, gpointer in_data ) +{ + ExtensionsPanel * self = static_cast<ExtensionsPanel*>(in_data); + + const char* stateStr; + Extension::state_t state = in_plug->get_state(); + switch ( state ) { + case Extension::STATE_LOADED: + { + stateStr = "loaded"; + } + break; + case Extension::STATE_UNLOADED: + { + stateStr = "unloaded"; + } + break; + case Extension::STATE_DEACTIVATED: + { + stateStr = "deactivated"; + } + break; + default: + stateStr = "unknown"; + } + + if ( self->_showAll || in_plug->deactivated() ) { + gchar* line = g_strdup_printf( "%s %s\n \"%s\"", stateStr, in_plug->get_name(), in_plug->get_id() ); + + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), line ); + self->_view.get_buffer()->insert( self->_view.get_buffer()->end(), "\n" ); + g_free(line); + } + + return; +} + +void ExtensionsPanel::rescan() +{ + _view.get_buffer()->set_text("Extensions:\n"); +// g_message("/------------------"); + + Inkscape::Extension::db.foreach(listCB, (gpointer)this); + +// g_message("\\------------------"); +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/extensions.h b/src/ui/dialog/extensions.h new file mode 100644 index 0000000..16d2e91 --- /dev/null +++ b/src/ui/dialog/extensions.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A simple dialog with information about extensions + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 The Inkscape Organization + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_EXTENSIONS_H +#define SEEN_EXTENSIONS_H + +#include "ui/widget/panel.h" +#include <gtkmm/textview.h> + +namespace Inkscape { +namespace Extension { +class Extension; +} +} + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +/** + * A panel that displays information about extensions. + */ +class ExtensionsPanel : public Inkscape::UI::Widget::Panel +{ +public: + ExtensionsPanel(); + + static ExtensionsPanel &getInstance(); + + void set_full(bool full); + +private: + ExtensionsPanel(ExtensionsPanel const &) = delete; // no copy + ExtensionsPanel &operator=(ExtensionsPanel const &) = delete; // no assign + + static void listCB(Inkscape::Extension::Extension *in_plug, gpointer in_data); + + void rescan(); + + bool _showAll; + Gtk::TextView _view; +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +#endif // SEEN_EXTENSIONS_H diff --git a/src/ui/dialog/filedialog.cpp b/src/ui/dialog/filedialog.cpp new file mode 100644 index 0000000..fdce498 --- /dev/null +++ b/src/ui/dialog/filedialog.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialog.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef _WIN32 +# include "filedialogimpl-win32.h" +# include "preferences.h" +#endif + +#include "filedialogimpl-gtkmm.h" + +#include "ui/dialog-events.h" +#include "extension/output.h" + +#include <glibmm/convert.h> + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### U T I L I T Y +#########################################################################*/ + +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext) +{ + int strLen = str.length(); + int extLen = ext.length(); + if (extLen > strLen) + return false; + int strpos = strLen-1; + for (int extpos = extLen-1 ; extpos>=0 ; extpos--, strpos--) + { + Glib::ustring::value_type ch = str[strpos]; + if (ch != ext[extpos]) + { + if ( ((ch & 0xff80) != 0) || + static_cast<Glib::ustring::value_type>( g_ascii_tolower( static_cast<gchar>(0x07f & ch) ) ) != ext[extpos] ) + { + return false; + } + } + } + return true; +} + +bool isValidImageFile(const Glib::ustring &fileName) +{ + std::vector<Gdk::PixbufFormat>formats = Gdk::Pixbuf::get_formats(); + for (auto format : formats) + { + std::vector<Glib::ustring>extensions = format.get_extensions(); + for (auto ext : extensions) + { + if (hasSuffix(fileName, ext)) + return true; + } + } + return false; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Public factory. Called by file.cpp, among others. + */ +FileOpenDialog *FileOpenDialog::create(Gtk::Window &parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title) +{ +#ifdef _WIN32 + FileOpenDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileOpenDialogImplWin32(parentWindow, path, fileTypes, title); + } else { + dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); + } +#else + FileOpenDialog *dialog = new FileOpenDialogImplGtk(parentWindow, path, fileTypes, title); +#endif + + return dialog; +} + +Glib::ustring FileOpenDialog::getFilename() +{ + return myFilename; +} + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Public factory method. Used in file.cpp + */ +FileSaveDialog *FileSaveDialog::create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) +{ +#ifdef _WIN32 + FileSaveDialog *dialog = NULL; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool( "/options/desktopintegration/value")) { + dialog = new FileSaveDialogImplWin32(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } else { + dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); + } +#else + FileSaveDialog *dialog = new FileSaveDialogImplGtk(parentWindow, path, fileTypes, title, default_key, docTitle, save_method); +#endif + return dialog; +} + +Glib::ustring FileSaveDialog::getFilename() +{ + return myFilename; +} + +Glib::ustring FileSaveDialog::getDocTitle() +{ + return myDocTitle; +} + +//void FileSaveDialog::change_path(const Glib::ustring& path) +//{ +// myFilename = path; +//} + +void FileSaveDialog::appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension) +{ + if (!outputExtension) + return; + + try { + bool appendExtension = true; + Glib::ustring utf8Name = Glib::filename_to_utf8( path ); + Glib::ustring::size_type pos = utf8Name.rfind('.'); + if ( pos != Glib::ustring::npos ) { + Glib::ustring trail = utf8Name.substr( pos ); + Glib::ustring foldedTrail = trail.casefold(); + if ( (trail == ".") + | (foldedTrail != Glib::ustring( outputExtension->get_extension() ).casefold() + && ( knownExtensions.find(foldedTrail) != knownExtensions.end() ) ) ) { + utf8Name = utf8Name.erase( pos ); + } else { + appendExtension = false; + } + } + + if (appendExtension) { + utf8Name = utf8Name + outputExtension->get_extension(); + myFilename = Glib::filename_from_utf8( utf8Name ); + } + } catch ( Glib::ConvertError& e ) { + // ignore + } +} + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialog.h b/src/ui/dialog/filedialog.h new file mode 100644 index 0000000..8989171 --- /dev/null +++ b/src/ui/dialog/filedialog.h @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Virtual base definitions for native file dialogs + */ +/* Authors: + * Bob Jamison <rwjj@earthlink.net> + * Joel Holdsworth + * Inkscape Guys + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2008, Inkscape Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOG_H__ +#define __FILE_DIALOG_H__ + +#include <vector> +#include <set> + +#include "extension/system.h" + +#include <glibmm/ustring.h> + +class SPDocument; + +namespace Inkscape { +namespace Extension { +class Extension; +class Output; +} +} + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/** + * Used for setting filters and options, and + * reading them back from user selections. + */ +enum FileDialogType { + SVG_TYPES, + IMPORT_TYPES, + EXPORT_TYPES, + EXE_TYPES, + SWATCH_TYPES, + CUSTOM_TYPE + }; + +/** + * Used for returning the type selected in a SaveAs + */ +enum FileDialogSelectionType { + SVG_NAMESPACE, + SVG_NAMESPACE_WITH_EXTENSIONS + }; + + +/** + * Return true if the string ends with the given suffix + */ +bool hasSuffix(const Glib::ustring &str, const Glib::ustring &ext); + +/** + * Return true if the image is loadable by Gdk, else false + */ +bool isValidImageFile(const Glib::ustring &fileName); + +/** + * This class provides an implementation-independent API for + * file "Open" dialogs. Using a standard interface obviates the need + * for ugly #ifdefs in file open code + */ +class FileOpenDialog +{ +public: + + + /** + * Constructor .. do not call directly + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + FileOpenDialog() + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + */ + static FileOpenDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileOpenDialog() = default;; + + /** + * Show an OpenFile file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() = 0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + Glib::ustring getFilename(); + + virtual std::vector<Glib::ustring> getFilenames() = 0; + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + /** + * Filename that was given + */ + Glib::ustring myFilename; + +}; //FileOpenDialog + + + + + + +/** + * This class provides an implementation-independent API for + * file "Save" dialogs. + */ +class FileSaveDialog +{ +public: + + /** + * Constructor. Do not call directly . Use the factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + FileSaveDialog () + = default;; + + /** + * Factory. + * @param path the directory where to start searching + * @param fileTypes one of FileDialogTypes + * @param title the title of the dialog + * @param key a list of file types from which the user can select + */ + static FileSaveDialog *create(Gtk::Window& parentWindow, + const Glib::ustring &path, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + + /** + * Destructor. + * Perform any necessary cleanups. + */ + virtual ~FileSaveDialog() = default;; + + + /** + * Show an SaveAs file selector. + * @return the selected path if user selected one, else NULL + */ + virtual bool show() =0; + + /** + * Return the 'key' (filetype) of the selection, if any + * @return a pointer to a string if successful (which must + * be later freed with g_free(), else NULL. + */ + virtual Inkscape::Extension::Extension * getSelectionType() = 0; + + virtual void setSelectionType( Inkscape::Extension::Extension * key ) = 0; + + /** + * Get the file name chosen by the user. Valid after an [OK] + */ + Glib::ustring getFilename (); + + /** + * Get the document title chosen by the user. Valid after an [OK] + */ + Glib::ustring getDocTitle (); + + virtual Glib::ustring getCurrentDirectory() = 0; + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern) = 0; + +protected: + + /** + * Filename that was given + */ + Glib::ustring myFilename; + + /** + * Doc Title that was given + */ + Glib::ustring myDocTitle; + + /** + * List of known file extensions. + */ + std::map<Glib::ustring, Inkscape::Extension::Output*> knownExtensions; + + + void appendExtension(Glib::ustring& path, Inkscape::Extension::Output* outputExtension); + +}; //FileSaveDialog + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* __FILE_DIALOG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialogimpl-gtkmm.cpp b/src/ui/dialog/filedialogimpl-gtkmm.cpp new file mode 100644 index 0000000..5874cac --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.cpp @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialogimpl.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> + +#include <glibmm/convert.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <glibmm/regex.h> +#include <gtkmm/expander.h> + +#include "filedialogimpl-gtkmm.h" + +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "preferences.h" + +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "ui/dialog-events.h" +#include "ui/view/svg-view-widget.h" + +// Routines from file.cpp +#undef INK_DUMP_FILENAME_CONV + +#ifdef INK_DUMP_FILENAME_CONV +void dump_str(const gchar *str, const gchar *prefix); +void dump_ustr(const Glib::ustring &ustr); +#endif + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +//######################################################################## +//### U T I L I T Y +//######################################################################## + +void fileDialogExtensionToPattern(Glib::ustring &pattern, Glib::ustring &extension) +{ + for (unsigned int ch : extension) { + if (Glib::Unicode::isalpha(ch)) { + pattern += '['; + pattern += Glib::Unicode::toupper(ch); + pattern += Glib::Unicode::tolower(ch); + pattern += ']'; + } else { + pattern += ch; + } + } +} + + +void findEntryWidgets(Gtk::Container *parent, std::vector<Gtk::Entry *> &result) +{ + if (!parent) { + return; + } + std::vector<Gtk::Widget *> children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_ENTRY(wid)) + result.push_back(dynamic_cast<Gtk::Entry *>(child)); + else if (GTK_IS_CONTAINER(wid)) + findEntryWidgets(dynamic_cast<Gtk::Container *>(child), result); + } +} + +void findExpanderWidgets(Gtk::Container *parent, std::vector<Gtk::Expander *> &result) +{ + if (!parent) + return; + std::vector<Gtk::Widget *> children = parent->get_children(); + for (auto child : children) { + GtkWidget *wid = child->gobj(); + if (GTK_IS_EXPANDER(wid)) + result.push_back(dynamic_cast<Gtk::Expander *>(child)); + else if (GTK_IS_CONTAINER(wid)) + findExpanderWidgets(dynamic_cast<Gtk::Container *>(child), result); + } +} + + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +void FileDialogBaseGtk::internalSetup() +{ + // Open executable file dialogs don't need the preview panel + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool enablePreview = prefs->getBool(preferenceBase + "/enable_preview", true); + bool enableSVGExport = prefs->getBool(preferenceBase + "/enable_svgexport", false); + + previewCheckbox.set_label(Glib::ustring(_("Enable preview"))); + previewCheckbox.set_active(enablePreview); + + previewCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_previewEnabledCB)); + + svgexportCheckbox.set_label(Glib::ustring(_("Export as SVG 1.1 per settings in Preference Dialog."))); + svgexportCheckbox.set_active(enableSVGExport); + + svgexportCheckbox.signal_toggled().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_svgexportEnabledCB)); + + // Catch selection-changed events, so we can adjust the text widget + signal_update_preview().connect(sigc::mem_fun(*this, &FileDialogBaseGtk::_updatePreviewCallback)); + + //###### Add a preview widget + set_preview_widget(svgPreview); + set_preview_widget_active(enablePreview); + set_use_preview_label(false); + } +} + + +void FileDialogBaseGtk::cleanup(bool showConfirmed) +{ + if (_dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (showConfirmed) { + prefs->setBool(preferenceBase + "/enable_preview", previewCheckbox.get_active()); + } + } +} + + +void FileDialogBaseGtk::_previewEnabledCB() +{ + bool enabled = previewCheckbox.get_active(); + set_preview_widget_active(enabled); + if (enabled) { + _updatePreviewCallback(); + } else { + // Clears out any current preview image. + svgPreview.showNoPreview(); + } +} + +void FileDialogBaseGtk::_svgexportEnabledCB() +{ + bool enabled = svgexportCheckbox.get_active(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(preferenceBase + "/enable_svgexport", enabled); +} + + + +/** + * Callback for checking if the preview needs to be redrawn + */ +void FileDialogBaseGtk::_updatePreviewCallback() +{ + Glib::ustring fileName = get_preview_filename(); + bool enabled = previewCheckbox.get_active(); + + if (fileName.empty()) { + fileName = get_preview_uri(); + } + + if (enabled && !fileName.empty()) { + svgPreview.set(fileName, _dialogType); + } else { + svgPreview.showNoPreview(); + } +} + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplGtk::FileOpenDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_OPEN, fileTypes, "/dialogs/open") +{ + + + if (_dialogType == EXE_TYPES) { + /* One file at a time */ + set_select_multiple(false); + } else { + /* And also Multiple Files */ + set_select_multiple(true); + } + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (open, import, etc...)*/ + _dialogType = fileTypes; + + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') + udir.erase(len - 1); + if (_dialogType == EXE_TYPES) { + set_filename(udir.c_str()); + } else { + set_current_folder(udir.c_str()); + } + } + + if (_dialogType != EXE_TYPES) { + set_extra_widget(previewCheckbox); + } + + //###### Add the file types menu + createFilterMenu(); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Open"), Gtk::RESPONSE_OK)); + + //###### Allow easy access to our examples folder + if (Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(INKSCAPE_EXAMPLESDIR, G_FILE_TEST_IS_DIR) && g_path_is_absolute(INKSCAPE_EXAMPLESDIR)) { + add_shortcut_folder(INKSCAPE_EXAMPLESDIR); + } +} + +/** + * Destructor + */ +FileOpenDialogImplGtk::~FileOpenDialogImplGtk() += default; + +void FileOpenDialogImplGtk::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_(name.c_str())); + allFilter->add_pattern(pattern); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); +} + +void FileOpenDialogImplGtk::createFilterMenu() +{ + if (_dialogType == CUSTOM_TYPE) { + return; + } + + if (_dialogType == EXE_TYPES) { + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + } else { + auto allInkscapeFilter = Gtk::FileFilter::create(); + allInkscapeFilter->set_name(_("All Inkscape Files")); + + auto allFilter = Gtk::FileFilter::create(); + allFilter->set_name(_("All Files")); + allFilter->add_pattern("*"); + + auto allImageFilter = Gtk::FileFilter::create(); + allImageFilter->set_name(_("All Images")); + + auto allVectorFilter = Gtk::FileFilter::create(); + allVectorFilter->set_name(_("All Vectors")); + + auto allBitmapFilter = Gtk::FileFilter::create(); + allBitmapFilter->set_name(_("All Bitmaps")); + extensionMap[Glib::ustring(_("All Inkscape Files"))] = nullptr; + add_filter(allInkscapeFilter); + + extensionMap[Glib::ustring(_("All Files"))] = nullptr; + add_filter(allFilter); + + extensionMap[Glib::ustring(_("All Images"))] = nullptr; + add_filter(allImageFilter); + + extensionMap[Glib::ustring(_("All Vectors"))] = nullptr; + add_filter(allVectorFilter); + + extensionMap[Glib::ustring(_("All Bitmaps"))] = nullptr; + add_filter(allBitmapFilter); + + // patterns added dynamically below + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + for (auto imod : extension_list) + { + // FIXME: would be nice to grey them out instead of not listing them + if (imod->deactivated()) + continue; + + Glib::ustring upattern("*"); + Glib::ustring extension = imod->get_extension(); + fileDialogExtensionToPattern(upattern, extension); + + Glib::ustring uname(imod->get_filetypename(true)); + + auto filter = Gtk::FileFilter::create(); + filter->set_name(uname); + filter->add_pattern(upattern); + add_filter(filter); + extensionMap[uname] = imod; + +// g_message("ext %s:%s '%s'\n", ioext->name, ioext->mimetype, upattern.c_str()); + allInkscapeFilter->add_pattern(upattern); + if (strncmp("image", imod->get_mimetype(), 5) == 0) + allImageFilter->add_pattern(upattern); + + // uncomment this to find out all mime types supported by Inkscape import/open + // g_print ("%s\n", imod->get_mimetype()); + + // I don't know of any other way to define "bitmap" formats other than by listing them + if (strncmp("image/png", imod->get_mimetype(), 9) == 0 || + strncmp("image/jpeg", imod->get_mimetype(), 10) == 0 || + strncmp("image/gif", imod->get_mimetype(), 9) == 0 || + strncmp("image/x-icon", imod->get_mimetype(), 12) == 0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22) == 0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18) == 0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/bmp", imod->get_mimetype(), 9) == 0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18) == 0 || + strncmp("image/tiff", imod->get_mimetype(), 10) == 0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15) == 0 || + strncmp("image/x-tga", imod->get_mimetype(), 11) == 0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11) == 0) + { + allBitmapFilter->add_pattern(upattern); + } else { + allVectorFilter->add_pattern(upattern); + } + } + } + return; +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileOpenDialogImplGtk::show() +{ + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + hide(); + + if (b == Gtk::RESPONSE_OK) { + // This is a hack, to avoid the warning messages that + // Gtk::FileChooser::get_filter() returns + // should be: Gtk::FileFilter *filter = get_filter(); + GtkFileChooser *gtkFileChooser = Gtk::FileChooser::gobj(); + GtkFileFilter *filter = gtk_file_chooser_get_filter(gtkFileChooser); + if (filter) { + // Get which extension was chosen, if any + extension = extensionMap[gtk_file_filter_get_name(filter)]; + } + myFilename = get_filename(); + + if (myFilename.empty()) { + myFilename = get_uri(); + } + + cleanup(true); + return true; + } else { + cleanup(false); + return false; + } +} + + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileOpenDialogImplGtk::getSelectionType() +{ + return extension; +} + + +/** + * Get the file name chosen by the user. Valid after an [OK] + */ +Glib::ustring FileOpenDialogImplGtk::getFilename() +{ + return myFilename; +} + + +/** + * To Get Multiple filenames selected at-once. + */ +std::vector<Glib::ustring> FileOpenDialogImplGtk::getFilenames() +{ + auto result_tmp = get_filenames(); + + // Copy filenames to a vector of type Glib::ustring + std::vector<Glib::ustring> result; + + for (auto it : result_tmp) + result.emplace_back(it); + + if (result.empty()) { + result = get_uris(); + } + + return result; +} + +Glib::ustring FileOpenDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Constructor + */ +FileSaveDialogImplGtk::FileSaveDialogImplGtk(Gtk::Window &parentWindow, const Glib::ustring &dir, + FileDialogType fileTypes, const Glib::ustring &title, + const Glib::ustring & /*default_key*/, const gchar *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) + : FileDialogBaseGtk(parentWindow, title, Gtk::FILE_CHOOSER_ACTION_SAVE, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "/dialogs/save_copy" + : "/dialogs/save_as") + , save_method(save_method) + , fromCB(false) +{ + FileSaveDialog::myDocTitle = docTitle; + + /* One file at a time */ + set_select_multiple(false); + + set_local_only(false); + + /* Initialize to Autodetect */ + extension = nullptr; + /* No filename to start out with */ + myFilename = ""; + + /* Set our dialog type (save, export, etc...)*/ + _dialogType = fileTypes; + + /* Set the pwd and/or the filename */ + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if ((len != 0) && (udir[len - 1] == '\\')) { + udir.erase(len - 1); + } + myFilename = udir; + } + + //###### Add the file types menu + // createFilterMenu(); + + //###### Do we want the .xxx extension automatically added? + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + fileTypeCheckbox.set_label(Glib::ustring(_("Append filename extension automatically"))); + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_copy/append_extension", true)); + } else { + fileTypeCheckbox.set_active(prefs->getBool("/dialogs/save_as/append_extension", true)); + } + + if (_dialogType != CUSTOM_TYPE) + createFileTypeMenu(); + + fileTypeComboBox.set_size_request(200, 40); + fileTypeComboBox.signal_changed().connect(sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileTypeChangedCallback)); + + + childBox.pack_start(checksBox); + childBox.pack_end(fileTypeComboBox); + checksBox.pack_start(fileTypeCheckbox); + checksBox.pack_start(previewCheckbox); + checksBox.pack_start(svgexportCheckbox); + + set_extra_widget(childBox); + + // Let's do some customization + fileNameEntry = nullptr; + Gtk::Container *cont = get_toplevel(); + std::vector<Gtk::Entry *> entries; + findEntryWidgets(cont, entries); + // g_message("Found %d entry widgets\n", entries.size()); + if (!entries.empty()) { + // Catch when user hits [return] on the text field + fileNameEntry = entries[0]; + fileNameEntry->signal_activate().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameEntryChangedCallback)); + } + signal_selection_changed().connect( + sigc::mem_fun(*this, &FileSaveDialogImplGtk::fileNameChanged)); + + // Let's do more customization + std::vector<Gtk::Expander *> expanders; + findExpanderWidgets(cont, expanders); + // g_message("Found %d expander widgets\n", expanders.size()); + if (!expanders.empty()) { + // Always show the file list + Gtk::Expander *expander = expanders[0]; + expander->set_expanded(true); + } + + // allow easy access to the user's own templates folder + using namespace Inkscape::IO::Resource; + char const *templates = Inkscape::IO::Resource::get_path(USER, TEMPLATES); + if (Inkscape::IO::file_test(templates, G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(templates, G_FILE_TEST_IS_DIR) && g_path_is_absolute(templates)) { + add_shortcut_folder(templates); + } + + // if (extension == NULL) + // checkbox.set_sensitive(FALSE); + + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + set_default(*add_button(_("_Save"), Gtk::RESPONSE_OK)); + + show_all_children(); +} + +/** + * Destructor + */ +FileSaveDialogImplGtk::~FileSaveDialogImplGtk() += default; + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileNameEntryChangedCallback() +{ + if (!fileNameEntry) + return; + + Glib::ustring fileName = fileNameEntry->get_text(); + if (!Glib::get_charset()) // If we are not utf8 + fileName = Glib::filename_to_utf8(fileName); + + // g_message("User hit return. Text is '%s'\n", fileName.c_str()); + + if (!Glib::path_is_absolute(fileName)) { + // try appending to the current path + // not this way: fileName = get_current_folder() + "/" + fileName; + std::vector<Glib::ustring> pathSegments; + pathSegments.emplace_back(get_current_folder()); + pathSegments.push_back(fileName); + fileName = Glib::build_filename(pathSegments); + } + + // g_message("path:'%s'\n", fileName.c_str()); + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) { + set_current_folder(fileName); + } else if (/*Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)*/ true) { + // dialog with either (1) select a regular file or (2) cd to dir + // simulate an 'OK' + set_filename(fileName); + response(Gtk::RESPONSE_OK); + } +} + + + +/** + * Callback for fileNameEntry widget + */ +void FileSaveDialogImplGtk::fileTypeChangedCallback() +{ + int sel = fileTypeComboBox.get_active_row_number(); + if ((sel < 0) || (sel >= (int)fileTypes.size())) + return; + + FileType type = fileTypes[sel]; + // g_message("selected: %s\n", type.name.c_str()); + + extension = type.extension; + auto filter = Gtk::FileFilter::create(); + filter->add_pattern(type.pattern); + set_filter(filter); + + if (fromCB) { + //do not update if called from a name change + fromCB = false; + return; + } + + updateNameAndExtension(); +} + +void FileSaveDialogImplGtk::fileNameChanged() { + Glib::ustring name = get_filename(); + Glib::ustring::size_type pos = name.rfind('.'); + if ( pos == Glib::ustring::npos ) return; + Glib::ustring ext = name.substr( pos ).casefold(); + if (extension && Glib::ustring(dynamic_cast<Inkscape::Extension::Output *>(extension)->get_extension()).casefold() == ext ) return; + if (knownExtensions.find(ext) == knownExtensions.end()) return; + fromCB = true; + fileTypeComboBox.set_active_text(knownExtensions[ext]->get_filetypename(true)); +} + +void FileSaveDialogImplGtk::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + //#Let user choose + FileType guessType; + guessType.name = name; + guessType.pattern = pattern; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + +void FileSaveDialogImplGtk::createFileTypeMenu() +{ + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + knownExtensions.clear(); + + for (auto omod : extension_list) { + // FIXME: would be nice to grey them out instead of not listing them + if (omod->deactivated()) + continue; + + FileType type; + type.name = omod->get_filetypename(true); + type.pattern = "*"; + Glib::ustring extension = omod->get_extension(); + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(extension.casefold(), omod)); + fileDialogExtensionToPattern(type.pattern, extension); + type.extension = omod; + fileTypeComboBox.append(type.name); + fileTypes.push_back(type); + } + + //#Let user choose + FileType guessType; + guessType.name = _("Guess from extension"); + guessType.pattern = "*"; + guessType.extension = nullptr; + fileTypeComboBox.append(guessType.name); + fileTypes.push_back(guessType); + + + fileTypeComboBox.set_active(0); + fileTypeChangedCallback(); // call at least once to set the filter +} + + + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool FileSaveDialogImplGtk::show() +{ + change_path(myFilename); + set_modal(TRUE); // Window + sp_transientize(GTK_WIDGET(gobj())); // Make transient + gint b = run(); // Dialog + svgPreview.showNoPreview(); + set_preview_widget_active(false); + hide(); + + if (b == Gtk::RESPONSE_OK) { + updateNameAndExtension(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Store changes of the "Append filename automatically" checkbox back to preferences. + if (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) { + prefs->setBool("/dialogs/save_copy/append_extension", fileTypeCheckbox.get_active()); + } else { + prefs->setBool("/dialogs/save_as/append_extension", fileTypeCheckbox.get_active()); + } + + Inkscape::Extension::store_file_extension_in_prefs((extension != nullptr ? extension->get_id() : ""), save_method); + + cleanup(true); + + return true; + } else { + cleanup(false); + return false; + } +} + + +/** + * Get the file extension type that was selected by the user. Valid after an [OK] + */ +Inkscape::Extension::Extension *FileSaveDialogImplGtk::getSelectionType() +{ + return extension; +} + +void FileSaveDialogImplGtk::setSelectionType(Inkscape::Extension::Extension *key) +{ + // If no pointer to extension is passed in, look up based on filename extension. + if (!key) { + // Not quite UTF-8 here. + gchar *filenameLower = g_ascii_strdown(myFilename.c_str(), -1); + for (int i = 0; !key && (i < (int)fileTypes.size()); i++) { + Inkscape::Extension::Output *ext = dynamic_cast<Inkscape::Extension::Output *>(fileTypes[i].extension); + if (ext && ext->get_extension()) { + gchar *extensionLower = g_ascii_strdown(ext->get_extension(), -1); + if (g_str_has_suffix(filenameLower, extensionLower)) { + key = fileTypes[i].extension; + } + g_free(extensionLower); + } + } + g_free(filenameLower); + } + + // Ensure the proper entry in the combo box is selected. + if (key) { + extension = key; + gchar const *extensionID = extension->get_id(); + if (extensionID) { + for (int i = 0; i < (int)fileTypes.size(); i++) { + Inkscape::Extension::Extension *ext = fileTypes[i].extension; + if (ext) { + gchar const *id = ext->get_id(); + if (id && (strcmp(extensionID, id) == 0)) { + int oldSel = fileTypeComboBox.get_active_row_number(); + if (i != oldSel) { + fileTypeComboBox.set_active(i); + } + break; + } + } + } + } + } +} + +Glib::ustring FileSaveDialogImplGtk::getCurrentDirectory() +{ + return get_current_folder(); +} + + +/*void +FileSaveDialogImplGtk::change_title(const Glib::ustring& title) +{ + set_title(title); +}*/ + +/** + * Change the default save path location. + */ +void FileSaveDialogImplGtk::change_path(const Glib::ustring &path) +{ + myFilename = path; + + if (Glib::file_test(myFilename, Glib::FILE_TEST_IS_DIR)) { + // fprintf(stderr,"set_current_folder(%s)\n",myFilename.c_str()); + set_current_folder(myFilename); + } else { + // fprintf(stderr,"set_filename(%s)\n",myFilename.c_str()); + if (Glib::file_test(myFilename, Glib::FILE_TEST_EXISTS)) { + set_filename(myFilename); + } else { + std::string dirName = Glib::path_get_dirname(myFilename); + if (dirName != get_current_folder()) { + set_current_folder(dirName); + } + } + Glib::ustring basename = Glib::path_get_basename(myFilename); + // fprintf(stderr,"set_current_name(%s)\n",basename.c_str()); + try + { + set_current_name(Glib::filename_to_utf8(basename)); + } + catch (Glib::ConvertError &e) + { + g_warning("Error converting save filename to UTF-8."); + // try a fallback. + set_current_name(basename); + } + } +} + +void FileSaveDialogImplGtk::updateNameAndExtension() +{ + // Pick up any changes the user has typed in. + Glib::ustring tmp = get_filename(); + + if (tmp.empty()) { + tmp = get_uri(); + } + + if (!tmp.empty()) { + myFilename = tmp; + } + + Inkscape::Extension::Output *newOut = extension ? dynamic_cast<Inkscape::Extension::Output *>(extension) : nullptr; + if (fileTypeCheckbox.get_active() && newOut) { + // Append the file extension if it's not already present and display it in the file name entry field + appendExtension(myFilename, newOut); + change_path(myFilename); + } +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialogimpl-gtkmm.h b/src/ui/dialog/filedialogimpl-gtkmm.h new file mode 100644 index 0000000..430f6b6 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-gtkmm.h @@ -0,0 +1,313 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h + */ +/* Authors: + * Bob Jamison + * Johan Engelen <johan@shouraizou.nl> + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2008 Authors + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __FILE_DIALOGIMPL_H__ +#define __FILE_DIALOGIMPL_H__ + +//Gtk includes +#include <gtkmm/filechooserdialog.h> +#include <glib/gstdio.h> + +#include "filedialog.h" +#include "svg-preview.h" + +namespace Gtk { +class CheckButton; +class ComboBoxText; +class Expander; +} + +namespace Inkscape { + class URI; + +namespace UI { + +namespace View { + class SVGViewWidget; +} + +namespace Dialog { + +/*######################################################################### +### Utility +#########################################################################*/ +void +fileDialogExtensionToPattern(Glib::ustring &pattern, + Glib::ustring &extension); + +void +findEntryWidgets(Gtk::Container *parent, + std::vector<Gtk::Entry *> &result); + +void +findExpanderWidgets(Gtk::Container *parent, + std::vector<Gtk::Expander *> &result); + +class FileType +{ + public: + FileType(): name(), pattern(),extension(nullptr) {} + ~FileType() = default; + Glib::ustring name; + Glib::ustring pattern; + Inkscape::Extension::Extension *extension; +}; + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/** + * This class is the base implementation for the others. This + * reduces redundancies and bugs. + */ +class FileDialogBaseGtk : public Gtk::FileChooserDialog +{ +public: + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const Glib::ustring &title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + FileDialogBaseGtk(Gtk::Window& parentWindow, const char *title, + Gtk::FileChooserAction dialogType, FileDialogType type, gchar const* preferenceBase) : + Gtk::FileChooserDialog(parentWindow, title, dialogType), + preferenceBase(preferenceBase ? preferenceBase : "unknown"), + _dialogType(type) + { + internalSetup(); + } + + /** + * + */ + ~FileDialogBaseGtk() override + = default; + +protected: + void cleanup( bool showConfirmed ); + + Glib::ustring const preferenceBase; + /** + * What type of 'open' are we? (open, import, place, etc) + */ + FileDialogType _dialogType; + + /** + * Our svg preview widget + */ + SVGPreview svgPreview; + + /** + * Child widgets + */ + Gtk::CheckButton previewCheckbox; + Gtk::CheckButton svgexportCheckbox; + +private: + void internalSetup(); + + /** + * Callback for user changing preview checkbox + */ + void _previewEnabledCB(); + + /** + * Callback for seeing if the preview needs to be drawn + */ + void _updatePreviewCallback(); + + /** + * Callback to for SVG 2 to SVG 1.1 export. + */ + void _svgexportEnabledCB(); +}; + + + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/** + * Our implementation class for the FileOpenDialog interface.. + */ +class FileOpenDialogImplGtk : public FileOpenDialog, public FileDialogBaseGtk +{ +public: + + FileOpenDialogImplGtk(Gtk::Window& parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title); + + ~FileOpenDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + + Glib::ustring getFilename(); + + std::vector<Glib::ustring> getFilenames() override; + + Glib::ustring getCurrentDirectory() override; + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering patter (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + void addFilterMenu(Glib::ustring name, Glib::ustring pattern) override; + +private: + + /** + * Create a filter menu for this type of dialog + */ + void createFilterMenu(); + + + /** + * Filter name->extension lookup + */ + std::map<Glib::ustring, Inkscape::Extension::Extension *> extensionMap; + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + +}; + + + +//######################################################################## +//# F I L E S A V E +//######################################################################## + +/** + * Our implementation of the FileSaveDialog interface. + */ +class FileSaveDialogImplGtk : public FileSaveDialog, public FileDialogBaseGtk +{ + +public: + FileSaveDialogImplGtk(Gtk::Window &parentWindow, + const Glib::ustring &dir, + FileDialogType fileTypes, + const Glib::ustring &title, + const Glib::ustring &default_key, + const gchar* docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + ~FileSaveDialogImplGtk() override; + + bool show() override; + + Inkscape::Extension::Extension *getSelectionType() override; + void setSelectionType( Inkscape::Extension::Extension * key ) override; + + Glib::ustring getCurrentDirectory() override; + void addFileType(Glib::ustring name, Glib::ustring pattern) override; + +private: + //void change_title(const Glib::ustring& title); + void change_path(const Glib::ustring& path); + void updateNameAndExtension(); + + /** + * The file save method (essentially whether the dialog was invoked by "Save as ..." or "Save a + * copy ..."), which is used to determine file extensions and save paths. + */ + Inkscape::Extension::FileSaveMethod save_method; + + /** + * Fix to allow the user to type the file name + */ + Gtk::Entry *fileNameEntry; + + + /** + * Allow the specification of the output file type + */ + Gtk::ComboBoxText fileTypeComboBox; + + + /** + * Data mirror of the combo box + */ + std::vector<FileType> fileTypes; + + //# Child widgets + Gtk::HBox childBox; + Gtk::VBox checksBox; + + Gtk::CheckButton fileTypeCheckbox; + + /** + * Callback for user input into fileNameEntry + */ + void fileTypeChangedCallback(); + + /** + * Create a filter menu for this type of dialog + */ + void createFileTypeMenu(); + + + /** + * The extension to use to write this file + */ + Inkscape::Extension::Extension *extension; + + /** + * Callback for user input into fileNameEntry + */ + void fileNameEntryChangedCallback(); + void fileNameChanged(); + bool fromCB; +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif /*__FILE_DIALOGIMPL_H__*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialogimpl-win32.cpp b/src/ui/dialog/filedialogimpl-win32.cpp new file mode 100644 index 0000000..8c37176 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.cpp @@ -0,0 +1,1937 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of native file dialogs for Win32. + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef _WIN32 + +#include "filedialogimpl-win32.h" +// General includes +#include <cairomm/win32_surface.h> +#include <gdk/gdkwin32.h> +#include <gdkmm/general.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <list> +#include <vector> + +//Inkscape includes +#include "display/cairo-utils.h" +#include "document.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "filedialog.h" +#include "helper/pixbuf-ops.h" +#include "preferences.h" +#include "util/units.h" + + +using namespace Glib; +using namespace Cairo; +using namespace Gdk::Cairo; + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +const int PREVIEW_WIDENING = 150; +const int WINDOW_WIDTH_MINIMUM = 32; +const int WINDOW_WIDTH_FALLBACK = 450; +const int WINDOW_HEIGHT_MINIMUM = 32; +const int WINDOW_HEIGHT_FALLBACK = 360; +const char PreviewWindowClassName[] = "PreviewWnd"; +const unsigned long MaxPreviewFileSize = 10240; // kB + +#define IDC_SHOW_PREVIEW 1000 + +struct Filter +{ + gunichar2* name; + glong name_length; + gunichar2* filter; + glong filter_length; + Inkscape::Extension::Extension* mod; +}; + +ustring utf16_to_ustring(const wchar_t *utf16string, int utf16length = -1) +{ + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)utf16string, + utf16length, NULL, NULL, NULL); + ustring result(utf8string); + g_free(utf8string); + + return result; +} + +namespace { + +int sanitizeWindowSizeParam( int size, int delta, int minimum, int fallback ) +{ + int result = size; + if ( size < minimum ) { + g_warning( "Window size %d is less than cutoff.", size ); + result = fallback - delta; + } + result += delta; + return result; +} + +} // namespace + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +FileDialogBaseWin32::FileDialogBaseWin32(Gtk::Window &parent, + const Glib::ustring &dir, const gchar *title, + FileDialogType type, gchar const* /*preferenceBase*/) : + dialogType(type), + parent(parent), + _current_directory(dir) +{ + _main_loop = NULL; + + _filter_index = 1; + _filter_count = 0; + + _title = (wchar_t*)g_utf8_to_utf16(title, -1, NULL, NULL, NULL); + g_assert(_title != NULL); + + Glib::RefPtr<const Gdk::Window> parentWindow = parent.get_window(); + g_assert(parentWindow->gobj() != NULL); + _ownerHwnd = (HWND)gdk_win32_window_get_handle((GdkWindow*)parentWindow->gobj()); +} + +FileDialogBaseWin32::~FileDialogBaseWin32() +{ + g_free(_title); +} + +Inkscape::Extension::Extension *FileDialogBaseWin32::getSelectionType() +{ + return _extension; +} + +Glib::ustring FileDialogBaseWin32::getCurrentDirectory() +{ + return _current_directory; +} + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +bool FileOpenDialogImplWin32::_show_preview = true; + +/** + * Constructor. Not called directly. Use the factory. + */ +FileOpenDialogImplWin32::FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const gchar *title) : + FileDialogBaseWin32(parent, dir, title, fileTypes, "dialogs.open") +{ + // Initialize to Autodetect + _extension = NULL; + + // Set our dialog type (open, import, etc...) + dialogType = fileTypes; + + _show_preview_button_bitmap = NULL; + _preview_wnd = NULL; + _file_dialog_wnd = NULL; + _base_window_proc = NULL; + + _preview_file_size = 0; + _preview_bitmap = NULL; + _preview_file_icon = NULL; + _preview_document_width = 0; + _preview_document_height = 0; + _preview_image_width = 0; + _preview_image_height = 0; + _preview_emf_image = false; + + _mutex = NULL; + + if (dialogType != CUSTOM_TYPE) + createFilterMenu(); +} + + +/** + * Destructor + */ +FileOpenDialogImplWin32::~FileOpenDialogImplWin32() +{ + if(_filter != NULL) + delete[] _filter; + if(_extension_map != NULL) + delete[] _extension_map; +} + +void FileOpenDialogImplWin32::addFilterMenu(Glib::ustring name, Glib::ustring pattern) +{ + std::list<Filter> filter_list; + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list +} + +void FileOpenDialogImplWin32::createFilterMenu() +{ + std::list<Filter> filter_list; + + int extension_index = 0; + int filter_length = 1; + + if (dialogType == CUSTOM_TYPE) { + return; + } + + if (dialogType != EXE_TYPES) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_preview = prefs->getBool("/dialogs/open/enable_preview", true); + + // Compose the filter string + Inkscape::Extension::DB::InputList extension_list; + Inkscape::Extension::db.get_input_list(extension_list); + + ustring all_inkscape_files_filter, all_image_files_filter, all_vectors_filter, all_bitmaps_filter; + Filter all_files, all_inkscape_files, all_image_files, all_vectors, all_bitmaps; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_inkscape_files_filter_name = _("All Inkscape Files"); + const gchar *all_image_files_filter_name = _("All Images"); + const gchar *all_vectors_filter_name = _("All Vectors"); + const gchar *all_bitmaps_filter_name = _("All Bitmaps"); + + // Calculate the amount of memory required + int filter_count = 5; // 5 - one for each filter type + + for (Inkscape::Extension::DB::InputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Filter filter; + + Inkscape::Extension::Input *imod = *current_item; + if (imod->deactivated()) continue; + + // Type + filter.name = g_utf8_to_utf16(imod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + // Extension + const gchar *file_extension_name = imod->get_extension(); + filter.filter = g_utf8_to_utf16(file_extension_name, -1, NULL, &filter.filter_length, NULL); + + filter.mod = imod; + filter_list.push_back(filter); + + filter_length += filter.name_length + filter.filter_length + 3; // Add 3 for two \0s and a * + + // Add to the "All Inkscape Files" Entry + if(all_inkscape_files_filter.length() > 0) + all_inkscape_files_filter += ";*"; + all_inkscape_files_filter += file_extension_name; + if( strncmp("image", imod->get_mimetype(), 5) == 0) + { + // Add to the "All Image Files" Entry + if(all_image_files_filter.length() > 0) + all_image_files_filter += ";*"; + all_image_files_filter += file_extension_name; + } + + // I don't know of any other way to define "bitmap" formats other than by listing them + // if you change it here, do the same change in filedialogimpl-gtkmm + if ( + strncmp("image/png", imod->get_mimetype(), 9)==0 || + strncmp("image/jpeg", imod->get_mimetype(), 10)==0 || + strncmp("image/gif", imod->get_mimetype(), 9)==0 || + strncmp("image/x-icon", imod->get_mimetype(), 12)==0 || + strncmp("image/x-navi-animation", imod->get_mimetype(), 22)==0 || + strncmp("image/x-cmu-raster", imod->get_mimetype(), 18)==0 || + strncmp("image/x-xpixmap", imod->get_mimetype(), 15)==0 || + strncmp("image/bmp", imod->get_mimetype(), 9)==0 || + strncmp("image/vnd.wap.wbmp", imod->get_mimetype(), 18)==0 || + strncmp("image/tiff", imod->get_mimetype(), 10)==0 || + strncmp("image/x-xbitmap", imod->get_mimetype(), 15)==0 || + strncmp("image/x-tga", imod->get_mimetype(), 11)==0 || + strncmp("image/x-pcx", imod->get_mimetype(), 11)==0 + ) { + if(all_bitmaps_filter.length() > 0) + all_bitmaps_filter += ";*"; + all_bitmaps_filter += file_extension_name; + } else { + if(all_vectors_filter.length() > 0) + all_vectors_filter += ";*"; + all_vectors_filter += file_extension_name; + } + + filter_count++; + } + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter bitmap files + all_bitmaps.name = g_utf8_to_utf16(all_bitmaps_filter_name, + -1, NULL, &all_bitmaps.name_length, NULL); + all_bitmaps.filter = g_utf8_to_utf16(all_bitmaps_filter.data(), + -1, NULL, &all_bitmaps.filter_length, NULL); + all_bitmaps.mod = NULL; + filter_list.push_front(all_bitmaps); + + // Filter vector files + all_vectors.name = g_utf8_to_utf16(all_vectors_filter_name, + -1, NULL, &all_vectors.name_length, NULL); + all_vectors.filter = g_utf8_to_utf16(all_vectors_filter.data(), + -1, NULL, &all_vectors.filter_length, NULL); + all_vectors.mod = NULL; + filter_list.push_front(all_vectors); + + // Filter Image Files + all_image_files.name = g_utf8_to_utf16(all_image_files_filter_name, + -1, NULL, &all_image_files.name_length, NULL); + all_image_files.filter = g_utf8_to_utf16(all_image_files_filter.data(), + -1, NULL, &all_image_files.filter_length, NULL); + all_image_files.mod = NULL; + filter_list.push_front(all_image_files); + + // Filter Inkscape Files + all_inkscape_files.name = g_utf8_to_utf16(all_inkscape_files_filter_name, + -1, NULL, &all_inkscape_files.name_length, NULL); + all_inkscape_files.filter = g_utf8_to_utf16(all_inkscape_files_filter.data(), + -1, NULL, &all_inkscape_files.filter_length, NULL); + all_inkscape_files.mod = NULL; + filter_list.push_front(all_inkscape_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_inkscape_files.filter_length + + all_inkscape_files.name_length + 3 + + all_image_files.filter_length + + all_image_files.name_length + 3 + + all_vectors.filter_length + + all_vectors.name_length + 3 + + all_bitmaps.filter_length + + all_bitmaps.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } else { + // Executables only + ustring all_exe_files_filter = "*.exe;*.bat;*.com"; + Filter all_exe_files, all_files; + + const gchar *all_files_filter_name = _("All Files"); + const gchar *all_exe_files_filter_name = _("All Executable Files"); + + // Calculate the amount of memory required + int filter_count = 2; // 2 - All Files and All Executable Files + + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter.data(), + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + // Filter All Files + all_files.name = g_utf8_to_utf16(all_files_filter_name, + -1, NULL, &all_files.name_length, NULL); + all_files.filter = NULL; + all_files.filter_length = 0; + all_files.mod = NULL; + filter_list.push_front(all_files); + + filter_length += all_files.name_length + 3 + + all_exe_files.filter_length + + all_exe_files.name_length + 3 + + 1; + // Add 3 for 2*2 \0s and a *, and 1 for a trailing \0 + } + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 2; // Select the 2nd filter in the list - 2 is NOT the 3rd +} + +void FileOpenDialogImplWin32::GetOpenFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_mutex != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + memset(&ofn, 0, sizeof(ofn)); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_HIDEREADONLY | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetOpenFileName_hookproc; + ofn.lCustData = (LPARAM)this; + + _result = GetOpenFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + _mutex->lock(); + _finished = true; + _mutex->unlock(); +} + +void FileOpenDialogImplWin32::register_preview_wnd_class() +{ + HINSTANCE hInstance = GetModuleHandle(NULL); + const WNDCLASSA PreviewWndClass = + { + CS_HREDRAW | CS_VREDRAW, + preview_wnd_proc, + 0, + 0, + hInstance, + NULL, + LoadCursor(hInstance, IDC_ARROW), + (HBRUSH)(COLOR_BTNFACE + 1), + NULL, + PreviewWindowClassName + }; + + RegisterClassA(&PreviewWndClass); +} + +UINT_PTR CALLBACK FileOpenDialogImplWin32::GetOpenFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast<FileOpenDialogImplWin32*>(ofn->lCustData); + + // Make the window a bit wider + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + + // Don't show the preview when opening executable files + if ( pImpl->dialogType == EXE_TYPES) { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left, + rcRect.bottom - rcRect.top, + FALSE); + } else { + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + rcRect.right - rcRect.left + PREVIEW_WIDENING, + rcRect.bottom - rcRect.top, + FALSE); + } + + // Subclass the parent + pImpl->_base_window_proc = (WNDPROC)GetWindowLongPtr(hParentWnd, GWLP_WNDPROC); + SetWindowLongPtr(hParentWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(file_dialog_subclass_proc)); + + if ( pImpl->dialogType != EXE_TYPES) { + // Add a button to the toolbar + pImpl->_toolbar_wnd = FindWindowEx(hParentWnd, NULL, "ToolbarWindow32", NULL); + + pImpl->_show_preview_button_bitmap = LoadBitmap( + hInstance, MAKEINTRESOURCE(IDC_SHOW_PREVIEW)); + TBADDBITMAP tbAddBitmap = {NULL, reinterpret_cast<UINT_PTR>(pImpl->_show_preview_button_bitmap)}; + const int iBitmapIndex = SendMessage(pImpl->_toolbar_wnd, + TB_ADDBITMAP, 1, (LPARAM)&tbAddBitmap); + + + TBBUTTON tbButton; + memset(&tbButton, 0, sizeof(TBBUTTON)); + tbButton.iBitmap = iBitmapIndex; + tbButton.idCommand = IDC_SHOW_PREVIEW; + tbButton.fsState = (pImpl->_show_preview ? TBSTATE_CHECKED : 0) + | TBSTATE_ENABLED; + tbButton.fsStyle = TBSTYLE_CHECK; + tbButton.iString = (INT_PTR)_("Show Preview"); + SendMessage(pImpl->_toolbar_wnd, TB_ADDBUTTONS, 1, (LPARAM)&tbButton); + + // Create preview pane + register_preview_wnd_class(); + } + + pImpl->_mutex->lock(); + + pImpl->_file_dialog_wnd = hParentWnd; + + if ( pImpl->dialogType != EXE_TYPES) { + pImpl->_preview_wnd = + CreateWindowA(PreviewWindowClassName, "", + WS_CHILD | WS_VISIBLE, + 0, 0, 100, 100, hParentWnd, NULL, hInstance, NULL); + SetWindowLongPtr(pImpl->_preview_wnd, GWLP_USERDATA, ofn->lCustData); + } + + pImpl->_mutex->unlock(); + + pImpl->layout_dialog(); + } + break; + + case WM_NOTIFY: + { + + OFNOTIFY *pOFNotify = reinterpret_cast<OFNOTIFY*>(lParam); + switch(pOFNotify->hdr.code) + { + case CDN_SELCHANGE: + { + if(pImpl != NULL) + { + // Get the file name + pImpl->_mutex->lock(); + + SendMessage(pOFNotify->hdr.hwndFrom, CDM_GETFILEPATH, + sizeof(pImpl->_path_string) / sizeof(wchar_t), + (LPARAM)pImpl->_path_string); + + pImpl->_file_selected = true; + + pImpl->_mutex->unlock(); + } + } + break; + } + } + break; + + case WM_CLOSE: + pImpl->_mutex->lock(); + pImpl->_preview_file_size = 0; + + pImpl->_file_dialog_wnd = NULL; + DestroyWindow(pImpl->_preview_wnd); + pImpl->_preview_wnd = NULL; + DeleteObject(pImpl->_show_preview_button_bitmap); + pImpl->_show_preview_button_bitmap = NULL; + pImpl->_mutex->unlock(); + + break; + } + + // Use default dialog behaviour + return 0; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = CallWindowProc(pImpl->_base_window_proc, hwnd, uMsg, wParam, lParam); + + switch(uMsg) + { + case WM_SHOWWINDOW: + if(wParam != 0) + pImpl->layout_dialog(); + break; + + case WM_SIZE: + pImpl->layout_dialog(); + break; + + case WM_COMMAND: + if(wParam == IDC_SHOW_PREVIEW) + { + const bool enable = SendMessage(pImpl->_toolbar_wnd, + TB_ISBUTTONCHECKED, IDC_SHOW_PREVIEW, 0) != 0; + pImpl->enable_preview(enable); + } + break; + } + + return lResult; +} + +LRESULT CALLBACK FileOpenDialogImplWin32::preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + const int CaptionPadding = 4; + const int IconSize = 32; + + FileOpenDialogImplWin32 *pImpl = reinterpret_cast<FileOpenDialogImplWin32*> + (GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + LRESULT lResult = 0; + + switch(uMsg) + { + case WM_ERASEBKGND: + // Do nothing to erase the background + // - otherwise there'll be flicker + lResult = 1; + break; + + case WM_PAINT: + { + // Get the client rect + RECT rcClient; + GetClientRect(hwnd, &rcClient); + + // Prepare to paint + PAINTSTRUCT paint_struct; + HDC dc = BeginPaint(hwnd, &paint_struct); + + HFONT hCaptionFont = reinterpret_cast<HFONT>(SendMessage(GetParent(hwnd), + WM_GETFONT, 0, 0)); + HFONT hOldFont = static_cast<HFONT>(SelectObject(dc, hCaptionFont)); + SetBkMode(dc, TRANSPARENT); + + pImpl->_mutex->lock(); + + if(pImpl->_path_string[0] == 0) + { + WCHAR* noFileText=(WCHAR*)g_utf8_to_utf16(_("No file selected"), + -1, NULL, NULL, NULL); + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + DrawTextW(dc, noFileText, -1, &rcClient, + DT_CENTER | DT_VCENTER | DT_NOPREFIX); + g_free(noFileText); + } + else if(pImpl->_preview_bitmap != NULL) + { + BITMAP bitmap; + GetObject(pImpl->_preview_bitmap, sizeof(bitmap), &bitmap); + const int destX = (rcClient.right - bitmap.bmWidth) / 2; + + // Render the image + HDC hSrcDC = CreateCompatibleDC(dc); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hSrcDC, pImpl->_preview_bitmap); + + BitBlt(dc, destX, 0, bitmap.bmWidth, bitmap.bmHeight, + hSrcDC, 0, 0, SRCCOPY); + + SelectObject(hSrcDC, hOldBitmap); + DeleteDC(hSrcDC); + + // Fill in the background area + HRGN hEraseRgn = CreateRectRgn(rcClient.left, rcClient.top, + rcClient.right, rcClient.bottom); + HRGN hImageRgn = CreateRectRgn(destX, 0, + destX + bitmap.bmWidth, bitmap.bmHeight); + CombineRgn(hEraseRgn, hEraseRgn, hImageRgn, RGN_DIFF); + + FillRgn(dc, hEraseRgn, GetSysColorBrush(COLOR_3DFACE)); + + DeleteObject(hImageRgn); + DeleteObject(hEraseRgn); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + bitmap.bmHeight + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = pImpl->format_caption( + szCaption, sizeof(szCaption) / sizeof(WCHAR)); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else if(pImpl->_preview_file_icon != NULL) + { + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + + // Draw the files icon + const int destX = (rcClient.right - IconSize) / 2; + DrawIconEx(dc, destX, 0, pImpl->_preview_file_icon, + IconSize, IconSize, 0, NULL, + DI_NORMAL | DI_COMPAT); + + // Draw the caption on + RECT rcCaptionRect = {rcClient.left, + rcClient.top + IconSize + CaptionPadding, + rcClient.right, rcClient.bottom}; + + WCHAR szFileName[_MAX_FNAME], szCaption[_MAX_FNAME + 32]; + _wsplitpath(pImpl->_path_string, NULL, NULL, szFileName, NULL); + + const int iLength = snwprintf(szCaption, + sizeof(szCaption), L"%ls\n%d kB", + szFileName, pImpl->_preview_file_size); + + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS); + } + else + { + // Can't show anything! + FillRect(dc, &rcClient, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1)); + } + + pImpl->_mutex->unlock(); + + // Finish painting + SelectObject(dc, hOldFont); + EndPaint(hwnd, &paint_struct); + } + + break; + + case WM_DESTROY: + pImpl->free_preview(); + break; + + default: + lResult = DefWindowProc(hwnd, uMsg, wParam, lParam); + break; + } + + return lResult; +} + +void FileOpenDialogImplWin32::enable_preview(bool enable) +{ + if (_show_preview != enable) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/open/enable_preview", enable); + } + _show_preview = enable; + + // Relayout the dialog + ShowWindow(_preview_wnd, enable ? SW_SHOW : SW_HIDE); + layout_dialog(); + + // Load or unload the preview + if(enable) + { + _mutex->lock(); + _file_selected = true; + _mutex->unlock(); + } + else free_preview(); +} + +void FileOpenDialogImplWin32::layout_dialog() +{ + union RECTPOINTS + { + RECT r; + POINT p[2]; + }; + + const float MaxExtentScale = 2.0f / 3.0f; + + RECT rcClient; + GetClientRect(_file_dialog_wnd, &rcClient); + + // Re-layout the dialog + HWND hFileListWnd = GetDlgItem(_file_dialog_wnd, lst2); + HWND hFolderComboWnd = GetDlgItem(_file_dialog_wnd, cmb2); + + + RECT rcFolderComboRect; + RECTPOINTS rcFileList; + GetWindowRect(hFileListWnd, &rcFileList.r); + GetWindowRect(hFolderComboWnd, &rcFolderComboRect); + const int iPadding = rcFileList.r.top - rcFolderComboRect.bottom; + MapWindowPoints(NULL, _file_dialog_wnd, rcFileList.p, 2); + + RECT rcPreview; + RECT rcBody = {rcFileList.r.left, rcFileList.r.top, + rcClient.right - iPadding, rcFileList.r.bottom}; + rcFileList.r.right = rcBody.right; + + if(_show_preview && dialogType != EXE_TYPES) + { + rcPreview.top = rcBody.top; + rcPreview.left = rcClient.right - (rcBody.bottom - rcBody.top); + const int iMaxExtent = (int)(MaxExtentScale * (float)(rcBody.left + rcBody.right)) + iPadding / 2; + if(rcPreview.left < iMaxExtent) rcPreview.left = iMaxExtent; + rcPreview.bottom = rcBody.bottom; + rcPreview.right = rcBody.right; + + // Re-layout the preview box + _mutex->lock(); + + _preview_width = rcPreview.right - rcPreview.left; + _preview_height = rcPreview.bottom - rcPreview.top; + + _mutex->unlock(); + + render_preview(); + + MoveWindow(_preview_wnd, rcPreview.left, rcPreview.top, + _preview_width, _preview_height, TRUE); + + rcFileList.r.right = rcPreview.left - iPadding; + } + + // Re-layout the file list box + MoveWindow(hFileListWnd, rcFileList.r.left, rcFileList.r.top, + rcFileList.r.right - rcFileList.r.left, + rcFileList.r.bottom - rcFileList.r.top, TRUE); + + // Re-layout the toolbar + RECTPOINTS rcToolBar; + GetWindowRect(_toolbar_wnd, &rcToolBar.r); + MapWindowPoints(NULL, _file_dialog_wnd, rcToolBar.p, 2); + MoveWindow(_toolbar_wnd, rcToolBar.r.left, rcToolBar.r.top, + rcToolBar.r.right - rcToolBar.r.left, rcToolBar.r.bottom - rcToolBar.r.top, TRUE); +} + +void FileOpenDialogImplWin32::file_selected() +{ + // Destroy any previous previews + free_preview(); + + + // Determine if the file exists + DWORD attributes = GetFileAttributesW(_path_string); + if(attributes == 0xFFFFFFFF || + attributes == FILE_ATTRIBUTE_DIRECTORY) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Check the file exists and get the file size + HANDLE file_handle = CreateFileW(_path_string, GENERIC_READ, + FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if(file_handle == INVALID_HANDLE_VALUE) return; + const DWORD file_size = GetFileSize(file_handle, NULL); + if (file_size == INVALID_FILE_SIZE) return; + _preview_file_size = file_size / 1024; + CloseHandle(file_handle); + + if(_show_preview) load_preview(); +} + +void FileOpenDialogImplWin32::load_preview() +{ + // Destroy any previous previews + free_preview(); + + // Try to get the file icon + SHFILEINFOW fileInfo; + if(SUCCEEDED(SHGetFileInfoW(_path_string, 0, &fileInfo, + sizeof(fileInfo), SHGFI_ICON | SHGFI_LARGEICON))) + _preview_file_icon = fileInfo.hIcon; + + // Will this file be too big? + if(_preview_file_size > MaxPreviewFileSize) + { + InvalidateRect(_preview_wnd, NULL, FALSE); + return; + } + + // Prepare to render a preview + const Glib::ustring svg = ".svg"; + const Glib::ustring svgz = ".svgz"; + const Glib::ustring emf = ".emf"; + const Glib::ustring wmf = ".wmf"; + const Glib::ustring path = utf16_to_ustring(_path_string); + + bool success = false; + + _preview_document_width = _preview_document_height = 0; + + if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) && + (hasSuffix(path, svg) || hasSuffix(path, svgz))) + success = set_svg_preview(); + else if (hasSuffix(path, emf) || hasSuffix(path, wmf)) + success = set_emf_preview(); + else if (isValidImageFile(path)) + success = set_image_preview(); + else { + // Show no preview + } + + if(success) render_preview(); + + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +void FileOpenDialogImplWin32::free_preview() +{ + _mutex->lock(); + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + if(_preview_file_icon != NULL) + DestroyIcon(_preview_file_icon); + _preview_file_icon = NULL; + + _preview_bitmap_image.reset(); + _preview_emf_image = false; + _mutex->unlock(); +} + +bool FileOpenDialogImplWin32::set_svg_preview() +{ + const int PreviewSize = 512; + + gchar *utf8string = g_utf16_to_utf8((const gunichar2*)_path_string, + _MAX_PATH, NULL, NULL, NULL); + SPDocument *svgDoc = SPDocument::createNewDoc (utf8string, 0); + g_free(utf8string); + + // Check the document loaded properly + if (svgDoc == NULL) { + return false; + } + if (svgDoc->getRoot() == NULL) + { + svgDoc->doUnref(); + return false; + } + + // Get the size of the document + Inkscape::Util::Quantity svgWidth = svgDoc->getWidth(); + Inkscape::Util::Quantity svgHeight = svgDoc->getHeight(); + const double svgWidth_px = svgWidth.value("px"); + const double svgHeight_px = svgHeight.value("px"); + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = PreviewSize / svgWidth_px; + const double scaleFactorY = PreviewSize / svgHeight_px; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + // Now get the resized values + const int scaledSvgWidth = round(scaleFactor * svgWidth_px); + const int scaledSvgHeight = round(scaleFactor * svgHeight_px); + + const double dpi = 96*scaleFactor; + Inkscape::Pixbuf * pixbuf = sp_generate_internal_bitmap(svgDoc, NULL, 0, 0, svgWidth_px, svgHeight_px, scaledSvgWidth, scaledSvgHeight, dpi, dpi, (guint32) 0xffffff00, NULL); + + // Tidy up + svgDoc->doUnref(); + if (pixbuf == NULL) { + return false; + } + + // Create the GDK pixbuf + _mutex->lock(); + _preview_bitmap_image = Glib::wrap(pixbuf->getPixbufRaw()); + _preview_document_width = svgWidth_px; + _preview_document_height = svgHeight_px; + _preview_image_width = scaledSvgWidth; + _preview_image_height = scaledSvgHeight; + _mutex->unlock(); + + return true; +} + +void FileOpenDialogImplWin32::destroy_svg_rendering(const guint8 *buffer) +{ + g_assert(buffer != NULL); + g_free((void*)buffer); +} + +bool FileOpenDialogImplWin32::set_image_preview() +{ + const Glib::ustring path = utf16_to_ustring(_path_string, _MAX_PATH); + + bool successful = false; + + _mutex->lock(); + + try { + _preview_bitmap_image = Gdk::Pixbuf::create_from_file(path); + if (_preview_bitmap_image) { + _preview_image_width = _preview_bitmap_image->get_width(); + _preview_document_width = _preview_image_width; + _preview_image_height = _preview_bitmap_image->get_height(); + _preview_document_height = _preview_image_height; + successful = true; + } + } + catch (const Gdk::PixbufError&) {} + catch (const Glib::FileError&) {} + + _mutex->unlock(); + + return successful; +} + +// Aldus Placeable Header =================================================== +// Since we are a 32bit app, we have to be sure this structure compiles to +// be identical to a 16 bit app's version. To do this, we use the #pragma +// to adjust packing, we use a WORD for the hmf handle, and a SMALL_RECT +// for the bbox rectangle. +#pragma pack( push ) +#pragma pack( 2 ) +struct APMHEADER +{ + DWORD dwKey; + WORD hmf; + SMALL_RECT bbox; + WORD wInch; + DWORD dwReserved; + WORD wCheckSum; +}; +using PAPMHEADER = APMHEADER *; +#pragma pack( pop ) + + +static HENHMETAFILE +MyGetEnhMetaFileW( const WCHAR *filename ) +{ + // Try open as Enhanced Metafile + HENHMETAFILE hemf = GetEnhMetaFileW(filename); + + if (!hemf) { + // Try open as Windows Metafile + HMETAFILE hmf = GetMetaFileW(filename); + + METAFILEPICT mp; + HDC hDC; + + if (!hmf) { + WCHAR szTemp[MAX_PATH]; + + DWORD dw = GetShortPathNameW( filename, szTemp, MAX_PATH ); + if (dw) { + hmf = GetMetaFileW( szTemp ); + } + } + + if (hmf) { + // Convert Windows Metafile to Enhanced Metafile + DWORD nSize = GetMetaFileBitsEx( hmf, 0, NULL ); + + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = GetMetaFileBitsEx( hmf, nSize, lpvData ); + if (dw) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = 1000; + mp.yExt = 1000; + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Make an enhanced metafile from the windows metafile + hemf = SetWinMetaFileBits( nSize, lpvData, hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + DeleteMetaFile( hmf ); + } + delete[] lpvData; + } + else { + DeleteMetaFile( hmf ); + } + } + else { + DeleteMetaFile( hmf ); + } + } + else { + // Try open as Aldus Placeable Metafile + HANDLE hFile; + hFile = CreateFileW( filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); + + if (hFile != INVALID_HANDLE_VALUE) { + DWORD nSize = GetFileSize( hFile, NULL ); + if (nSize) { + BYTE *lpvData = new BYTE[nSize]; + if (lpvData) { + DWORD dw = ReadFile( hFile, lpvData, nSize, &nSize, NULL ); + if (dw) { + if ( ((PAPMHEADER)lpvData)->dwKey == 0x9ac6cdd7l ) { + // Fill out a METAFILEPICT structure + mp.mm = MM_ANISOTROPIC; + mp.xExt = ((PAPMHEADER)lpvData)->bbox.Right - ((PAPMHEADER)lpvData)->bbox.Left; + mp.xExt = ( mp.xExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.yExt = ((PAPMHEADER)lpvData)->bbox.Bottom - ((PAPMHEADER)lpvData)->bbox.Top; + mp.yExt = ( mp.yExt * 2540l ) / (DWORD)(((PAPMHEADER)lpvData)->wInch); + mp.hMF = NULL; + // Get a reference DC + hDC = GetDC( NULL ); + // Create an enhanced metafile from the bits + hemf = SetWinMetaFileBits( nSize, lpvData+sizeof(APMHEADER), hDC, &mp ); + // Clean up + ReleaseDC( NULL, hDC ); + } + } + delete[] lpvData; + } + } + CloseHandle( hFile ); + } + } + } + + return hemf; +} + + +bool FileOpenDialogImplWin32::set_emf_preview() +{ + _mutex->lock(); + + BOOL ok = FALSE; + + DWORD w = 0; + DWORD h = 0; + + HENHMETAFILE hemf = MyGetEnhMetaFileW( _path_string ); + + if (hemf) + { + ENHMETAHEADER emh; + ZeroMemory(&emh, sizeof(emh)); + ok = GetEnhMetaFileHeader(hemf, sizeof(emh), &emh) != 0; + + w = (emh.rclFrame.right - emh.rclFrame.left); + h = (emh.rclFrame.bottom - emh.rclFrame.top); + + DeleteEnhMetaFile(hemf); + } + + if (ok) + { + const int PreviewSize = 512; + + // Get the size of the document + const double emfWidth = w; + const double emfHeight = h; + + _preview_document_width = emfWidth / 2540 * 96; // width is in units of 0.01 mm + _preview_document_height = emfHeight / 2540 * 96; // height is in units of 0.01 mm + _preview_image_width = emfWidth; + _preview_image_height = emfHeight; + + _preview_emf_image = true; + } + + _mutex->unlock(); + + return ok; +} + +void FileOpenDialogImplWin32::render_preview() +{ + double x, y; + const double blurRadius = 8; + const double halfBlurRadius = blurRadius / 2; + const int shaddowOffsetX = 0; + const int shaddowOffsetY = 2; + const int pagePadding = 5; + const double shaddowAlpha = 0.75; + + // Is the preview showing? + if(!_show_preview) + return; + + // Do we have anything to render? + _mutex->lock(); + + if(!_preview_bitmap_image && !_preview_emf_image) + { + _mutex->unlock(); + return; + } + + // Tidy up any previous bitmap renderings + if(_preview_bitmap != NULL) + DeleteObject(_preview_bitmap); + _preview_bitmap = NULL; + + // Calculate the size of the caption + int captionHeight = 0; + + if(_preview_wnd != NULL) + { + RECT rcCaptionRect; + WCHAR szCaption[_MAX_FNAME + 32]; + const int iLength = format_caption(szCaption, + sizeof(szCaption) / sizeof(WCHAR)); + + HDC dc = GetDC(_preview_wnd); + DrawTextW(dc, szCaption, iLength, &rcCaptionRect, + DT_CENTER | DT_TOP | DT_NOPREFIX | DT_PATH_ELLIPSIS | DT_CALCRECT); + ReleaseDC(_preview_wnd, dc); + + captionHeight = rcCaptionRect.bottom - rcCaptionRect.top; + } + + // Find the minimum scale to fit the image inside the preview area + const double scaleFactorX = ((double)_preview_width - pagePadding * 2 - blurRadius) / _preview_image_width; + const double scaleFactorY = ((double)_preview_height - pagePadding * 2 - shaddowOffsetY - halfBlurRadius - captionHeight) / _preview_image_height; + const double scaleFactor = (scaleFactorX > scaleFactorY) ? scaleFactorY : scaleFactorX; + + // Now get the resized values + const double scaledSvgWidth = scaleFactor * _preview_image_width; + const double scaledSvgHeight = scaleFactor * _preview_image_height; + + const int svgX = pagePadding + halfBlurRadius; + const int svgY = pagePadding; + + const int frameX = svgX - pagePadding; + const int frameY = svgY - pagePadding; + const int frameWidth = scaledSvgWidth + pagePadding * 2; + const int frameHeight = scaledSvgHeight + pagePadding * 2; + + const int totalWidth = (int)ceil(frameWidth + blurRadius); + const int totalHeight = (int)ceil(frameHeight + blurRadius); + + // Prepare the drawing surface + HDC hDC = GetDC(_preview_wnd); + HDC hMemDC = CreateCompatibleDC(hDC); + _preview_bitmap = CreateCompatibleBitmap(hDC, totalWidth, totalHeight); + HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, _preview_bitmap); + Cairo::RefPtr<Win32Surface> surface = Win32Surface::create(hMemDC); + Cairo::RefPtr<Context> context = Context::create(surface); + + // Paint the background to match the dialog colour + const COLORREF background = GetSysColor(COLOR_3DFACE); + context->set_source_rgb( + GetRValue(background) / 255.0, + GetGValue(background) / 255.0, + GetBValue(background) / 255.0); + context->paint(); + + //----- Draw the drop shadow -----// + + // Left Edge + x = frameX + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr<LinearGradient> leftEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + leftEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + leftEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(leftEdgeFade); + context->rectangle (x, frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Right Edge + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + Cairo::RefPtr<LinearGradient> rightEdgeFade = LinearGradient::create( + x, 0.0, x + blurRadius, 0.0); + rightEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + rightEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(rightEdgeFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + shaddowOffsetY + halfBlurRadius, + blurRadius, frameHeight - blurRadius); + context->fill(); + + // Top Edge + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<LinearGradient> topEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + topEdgeFade->add_color_stop_rgba (0, 0, 0, 0, 0); + topEdgeFade->add_color_stop_rgba (1, 0, 0, 0, shaddowAlpha); + context->set_source(topEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Bottom Edge + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<LinearGradient> bottomEdgeFade = LinearGradient::create( + 0.0, y, 0.0, y + blurRadius); + bottomEdgeFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomEdgeFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomEdgeFade); + context->rectangle (frameX + shaddowOffsetX + halfBlurRadius, y, + frameWidth - blurRadius, blurRadius); + context->fill(); + + // Top Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> topLeftCornerFade = RadialGradient::create( + x + blurRadius, y + blurRadius, 0, x + blurRadius, y + blurRadius, blurRadius); + topLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Top Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> topRightCornerFade = RadialGradient::create( + x, y + blurRadius, 0, x, y + blurRadius, blurRadius); + topRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + topRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(topRightCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Left Corner + x = frameX + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> bottomLeftCornerFade = RadialGradient::create( + x + blurRadius, y, 0, x + blurRadius, y, blurRadius); + bottomLeftCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomLeftCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomLeftCornerFade); + context->rectangle (x, y, blurRadius, blurRadius); + context->fill(); + + // Bottom Right Corner + x = frameX + frameWidth + shaddowOffsetX - halfBlurRadius; + y = frameY + frameHeight + shaddowOffsetY - halfBlurRadius; + Cairo::RefPtr<RadialGradient> bottomRightCornerFade = RadialGradient::create( + x, y, 0, x, y, blurRadius); + bottomRightCornerFade->add_color_stop_rgba (0, 0, 0, 0, shaddowAlpha); + bottomRightCornerFade->add_color_stop_rgba (1, 0, 0, 0, 0); + context->set_source(bottomRightCornerFade); + context->rectangle (frameX + frameWidth + shaddowOffsetX - halfBlurRadius, + frameY + frameHeight + shaddowOffsetY - halfBlurRadius, + blurRadius, blurRadius); + context->fill(); + + // Draw the frame + context->set_line_width(1); + context->rectangle (frameX, frameY, frameWidth, frameHeight); + + context->set_source_rgb(1.0, 1.0, 1.0); + context->fill_preserve(); + context->set_source_rgb(0.25, 0.25, 0.25); + context->stroke_preserve(); + + // Draw the image + if(_preview_bitmap_image) // Is the image a pixbuf? + { + // Set the transformation + const Cairo::Matrix matrix( + scaleFactor, 0, + 0, scaleFactor, + svgX, svgY); + context->set_matrix (matrix); + + // Render the image + set_source_pixbuf (context, _preview_bitmap_image, 0, 0); + context->paint(); + + // Reset the transformation + context->set_identity_matrix(); + } + + // Draw the inner frame + context->set_source_rgb(0.75, 0.75, 0.75); + context->rectangle (svgX, svgY, scaledSvgWidth, scaledSvgHeight); + context->stroke(); + + _mutex->unlock(); + + // Finish drawing + surface->finish(); + + if (_preview_emf_image) { + HENHMETAFILE hemf = MyGetEnhMetaFileW(_path_string); + if (hemf) { + RECT rc; + rc.top = svgY+2; + rc.left = svgX+2; + rc.bottom = scaledSvgHeight-2; + rc.right = scaledSvgWidth-2; + PlayEnhMetaFile(hMemDC, hemf, &rc); + DeleteEnhMetaFile(hemf); + } + } + + SelectObject(hMemDC, hOldBitmap) ; + DeleteDC(hMemDC); + + // Refresh the preview pane + InvalidateRect(_preview_wnd, NULL, FALSE); +} + +int FileOpenDialogImplWin32::format_caption(wchar_t *caption, int caption_size) +{ + wchar_t szFileName[_MAX_FNAME]; + _wsplitpath(_path_string, NULL, NULL, szFileName, NULL); + + return snwprintf(caption, caption_size, + L"%ls\n%d\u2009kB\n%d\u2009px \xD7 %d\u2009px", szFileName, _preview_file_size, + (int)_preview_document_width, (int)_preview_document_height); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileOpenDialogImplWin32::show() +{ + // We can only run one worker thread at a time + if(_mutex != NULL) return false; + +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _finished = false; + _file_selected = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + +#if GLIB_CHECK_VERSION(2,32,0) + _mutex = new Glib::Threads::Mutex(); + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread))) +#else + _mutex = new Glib::Mutex(); + if(Glib::Thread::create(sigc::mem_fun(*this, &FileOpenDialogImplWin32::GetOpenFileName_thread), true)) +#endif + { + while(1) + { + g_main_context_iteration(g_main_context_default(), FALSE); + + if(_mutex->trylock()) + { + // Read mutexed data + const bool finished = _finished; + const bool is_file_selected = _file_selected; + _file_selected = false; + _mutex->unlock(); + + if(finished) break; + if(is_file_selected) file_selected(); + } + + Sleep(10); + } + } + + // Tidy up + delete _mutex; + _mutex = NULL; + + return _result; +} + +/** + * To Get Multiple filenames selected at-once. + */ +std::vector<Glib::ustring>FileOpenDialogImplWin32::getFilenames() +{ + std::vector<Glib::ustring> result; + result.push_back(getFilename()); + return result; +} + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/** + * Constructor + */ +FileSaveDialogImplWin32::FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &/*default_key*/, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method) : + FileDialogBaseWin32(parent, dir, title, fileTypes, + (save_method == Inkscape::Extension::FILE_SAVE_METHOD_SAVE_COPY) ? "dialogs.save_copy" : "dialogs.save_as"), + _title_label(NULL), + _title_edit(NULL) +{ + FileSaveDialog::myDocTitle = docTitle; + createFilterMenu(); + + /* The code below sets the default file name */ + myFilename = ""; + if (dir.size() > 0) { + Glib::ustring udir(dir); + Glib::ustring::size_type len = udir.length(); + // leaving a trailing backslash on the directory name leads to the infamous + // double-directory bug on win32 + if (len != 0 && udir[len - 1] == '\\') udir.erase(len - 1); + + // Remove the extension: remove everything past the last period found past the last slash + // (not for CUSTOM_TYPE as we can not automatically add a file extension in that case yet) + if (dialogType == CUSTOM_TYPE) { + myFilename = udir; + } else { + size_t last_slash_index = udir.find_last_of( '\\' ); + size_t last_period_index = udir.find_last_of( '.' ); + if (last_period_index > last_slash_index) { + myFilename = udir.substr(0, last_period_index ); + } + } + + // remove one slash if double + if (1 + myFilename.find("\\\\",2)) { + myFilename.replace(myFilename.find("\\\\",2), 1, ""); + } + } +} + +FileSaveDialogImplWin32::~FileSaveDialogImplWin32() +{ +} + +void FileSaveDialogImplWin32::createFilterMenu() +{ + std::list<Filter> filter_list; + + knownExtensions.clear(); + + // Compose the filter string + Glib::ustring all_inkscape_files_filter, all_image_files_filter; + Inkscape::Extension::DB::OutputList extension_list; + Inkscape::Extension::db.get_output_list(extension_list); + + int filter_count = 0; + int filter_length = 1; + + for (Inkscape::Extension::DB::OutputList::iterator current_item = extension_list.begin(); + current_item != extension_list.end(); ++current_item) + { + Inkscape::Extension::Output *omod = *current_item; + if (omod->deactivated()) continue; + + filter_count++; + + Filter filter; + + // Extension + const gchar *filter_extension = omod->get_extension(); + filter.filter = g_utf8_to_utf16( + filter_extension, -1, NULL, &filter.filter_length, NULL); + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(filter_extension).casefold(), omod)); + + // Type + filter.name = g_utf8_to_utf16(omod->get_filetypename(true), -1, NULL, &filter.name_length, NULL); + + filter.mod = omod; + + filter_length += filter.name_length + + filter.filter_length + 3; // Add 3 for two \0s and a * + + filter_list.push_back(filter); + } + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = 0; + + _filter_count = extension_index; + _filter_index = 1; // A value of 1 selects the 1st filter - NOT the 2nd +} + + +void FileSaveDialogImplWin32::addFileType(Glib::ustring name, Glib::ustring pattern) +{ + std::list<Filter> filter_list; + + knownExtensions.clear(); + + Filter all_exe_files; + + const gchar *all_exe_files_filter_name = name.data(); + const gchar *all_exe_files_filter = pattern.data(); + + // Calculate the amount of memory required + int filter_count = 1; + int filter_length = 1; + + // Filter Executable Files + all_exe_files.name = g_utf8_to_utf16(all_exe_files_filter_name, + -1, NULL, &all_exe_files.name_length, NULL); + all_exe_files.filter = g_utf8_to_utf16(all_exe_files_filter, + -1, NULL, &all_exe_files.filter_length, NULL); + all_exe_files.mod = NULL; + filter_list.push_front(all_exe_files); + + filter_length = all_exe_files.name_length + all_exe_files.filter_length + 3; // Add 3 for two \0s and a * + + knownExtensions.insert(std::pair<Glib::ustring, Inkscape::Extension::Output*>(Glib::ustring(all_exe_files_filter).casefold(), NULL)); + + int extension_index = 0; + _extension_map = new Inkscape::Extension::Extension*[filter_count]; + + _filter = new wchar_t[filter_length]; + wchar_t *filterptr = _filter; + + for(std::list<Filter>::iterator filter_iterator = filter_list.begin(); + filter_iterator != filter_list.end(); ++filter_iterator) + { + const Filter &filter = *filter_iterator; + + wcsncpy(filterptr, (wchar_t*)filter.name, filter.name_length); + filterptr += filter.name_length; + g_free(filter.name); + + *(filterptr++) = L'\0'; + *(filterptr++) = L'*'; + + if(filter.filter != NULL) + { + wcsncpy(filterptr, (wchar_t*)filter.filter, filter.filter_length); + filterptr += filter.filter_length; + g_free(filter.filter); + } + + *(filterptr++) = L'\0'; + + // Associate this input extension with the file type name + _extension_map[extension_index++] = filter.mod; + } + *(filterptr++) = L'\0'; + + _filter_count = extension_index; + _filter_index = 1; // Select the 1st filter in the list + + +} + +void FileSaveDialogImplWin32::GetSaveFileName_thread() +{ + OPENFILENAMEW ofn; + + g_assert(_main_loop != NULL); + + WCHAR* current_directory_string = (WCHAR*)g_utf8_to_utf16( + _current_directory.data(), _current_directory.length(), + NULL, NULL, NULL); + + // Copy the selected file name, converting from UTF-8 to UTF-16 + memset(_path_string, 0, sizeof(_path_string)); + gunichar2* utf16_path_string = g_utf8_to_utf16( + myFilename.data(), -1, NULL, NULL, NULL); + wcsncpy(_path_string, (wchar_t*)utf16_path_string, _MAX_PATH); + g_free(utf16_path_string); + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = _ownerHwnd; + ofn.lpstrFile = _path_string; + ofn.nMaxFile = _MAX_PATH; + ofn.nFilterIndex = _filter_index; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = current_directory_string; + ofn.lpstrTitle = _title; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING; + ofn.lpstrFilter = _filter; + ofn.nFilterIndex = _filter_index; + ofn.lpfnHook = GetSaveFileName_hookproc; + ofn.lpstrDefExt = L"svg\0"; + ofn.lCustData = (LPARAM)this; + _result = GetSaveFileNameW(&ofn) != 0; + + g_assert(ofn.nFilterIndex >= 1 && ofn.nFilterIndex <= _filter_count); + _filter_index = ofn.nFilterIndex; + _extension = _extension_map[ofn.nFilterIndex - 1]; + + // Copy the selected file name, converting from UTF-16 to UTF-8 + myFilename = utf16_to_ustring(_path_string, _MAX_PATH); + + // Tidy up + g_free(current_directory_string); + + g_main_loop_quit(_main_loop); +} + +/** + * Show this dialog modally. Return true if user hits [OK] + */ +bool +FileSaveDialogImplWin32::show() +{ +#if !GLIB_CHECK_VERSION(2,32,0) + if(!Glib::thread_supported()) + Glib::thread_init(); +#endif + + _result = false; + _main_loop = g_main_loop_new(g_main_context_default(), FALSE); + + if(_main_loop != NULL) + { +#if GLIB_CHECK_VERSION(2,32,0) + if(Glib::Threads::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread))) +#else + if(Glib::Thread::create(sigc::mem_fun(*this, &FileSaveDialogImplWin32::GetSaveFileName_thread), true)) +#endif + g_main_loop_run(_main_loop); + + if(_result && _extension) + appendExtension(myFilename, (Inkscape::Extension::Output*)_extension); + } + + return _result; +} + +void FileSaveDialogImplWin32::setSelectionType( Inkscape::Extension::Extension * /*key*/ ) +{ + // If no pointer to extension is passed in, look up based on filename extension. + +} + + +UINT_PTR CALLBACK FileSaveDialogImplWin32::GetSaveFileName_hookproc( + HWND hdlg, UINT uiMsg, WPARAM, LPARAM lParam) +{ + FileSaveDialogImplWin32 *pImpl = reinterpret_cast<FileSaveDialogImplWin32*> + (GetWindowLongPtr(hdlg, GWLP_USERDATA)); + + switch(uiMsg) + { + case WM_INITDIALOG: + { + HWND hParentWnd = GetParent(hdlg); + HINSTANCE hInstance = GetModuleHandle(NULL); + + // get size/pos of typical combo box + RECT rEDT1, rCB1, rROOT, rST; + GetWindowRect(GetDlgItem(hParentWnd, cmb1), &rCB1); + GetWindowRect(GetDlgItem(hParentWnd, cmb13), &rEDT1); + GetWindowRect(GetDlgItem(hParentWnd, stc2), &rST); + GetWindowRect(hdlg, &rROOT); + int ydelta = rCB1.top - rEDT1.top; + if ( ydelta < 0 ) { + g_warning("Negative dialog ydelta"); + ydelta = 0; + } + + // Make the window a bit longer + // Note: we have a width delta of 1 because there is a suspicion that MoveWindow() to the same size causes zero-width results. + RECT rcRect; + GetWindowRect(hParentWnd, &rcRect); + MoveWindow(hParentWnd, rcRect.left, rcRect.top, + sanitizeWindowSizeParam( rcRect.right - rcRect.left, 1, WINDOW_WIDTH_MINIMUM, WINDOW_WIDTH_FALLBACK ), + sanitizeWindowSizeParam( rcRect.bottom - rcRect.top, ydelta, WINDOW_HEIGHT_MINIMUM, WINDOW_HEIGHT_FALLBACK ), + FALSE); + + // It is not necessary to delete stock objects by calling DeleteObject + HGDIOBJ dlgFont = GetStockObject(DEFAULT_GUI_FONT); + + // Set the pointer to the object + OPENFILENAMEW *ofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetWindowLongPtr(hdlg, GWLP_USERDATA, ofn->lCustData); + SetWindowLongPtr(hParentWnd, GWLP_USERDATA, ofn->lCustData); + pImpl = reinterpret_cast<FileSaveDialogImplWin32*>(ofn->lCustData); + + // Create the Title label and edit control + wchar_t *title_label_str = (wchar_t *)g_utf8_to_utf16(_("Title:"), -1, NULL, NULL, NULL); + pImpl->_title_label = CreateWindowExW(0, L"STATIC", title_label_str, + WS_VISIBLE|WS_CHILD, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.left-rST.left, rST.bottom-rST.top, + hParentWnd, NULL, hInstance, NULL); + g_free(title_label_str); + + if(pImpl->_title_label) { + if(dlgFont) SendMessage(pImpl->_title_label, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_label, NULL, rST.left-rROOT.left, rST.top+ydelta-rROOT.top, + rCB1.left-rST.left, rST.bottom-rST.top, SWP_SHOWWINDOW|SWP_NOZORDER); + } + + pImpl->_title_edit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", + WS_VISIBLE|WS_CHILD|WS_TABSTOP|ES_AUTOHSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, + hParentWnd, NULL, hInstance, NULL); + if(pImpl->_title_edit) { + if(dlgFont) SendMessage(pImpl->_title_edit, WM_SETFONT, (WPARAM)dlgFont, MAKELPARAM(FALSE, 0)); + SetWindowPos(pImpl->_title_edit, NULL, rCB1.left-rROOT.left, rCB1.top+ydelta-rROOT.top, + rCB1.right-rCB1.left, rCB1.bottom-rCB1.top, SWP_SHOWWINDOW|SWP_NOZORDER); + SetWindowTextW(pImpl->_title_edit, + (const wchar_t*)g_utf8_to_utf16(pImpl->myDocTitle.c_str(), -1, NULL, NULL, NULL)); + } + } + break; + case WM_DESTROY: + { + if(pImpl->_title_edit) { + int length = GetWindowTextLengthW(pImpl->_title_edit)+1; + wchar_t* temp_title = new wchar_t[length]; + GetWindowTextW(pImpl->_title_edit, temp_title, length); + pImpl->myDocTitle = g_utf16_to_utf8((gunichar2*)temp_title, -1, NULL, NULL, NULL); + delete[] temp_title; + DestroyWindow(pImpl->_title_label); + pImpl->_title_label = NULL; + DestroyWindow(pImpl->_title_edit); + pImpl->_title_edit = NULL; + } + } + break; + } + + // Use default dialog behaviour + return 0; +} + +} } } // namespace Dialog, UI, Inkscape + +#endif // ifdef _WIN32 + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filedialogimpl-win32.h b/src/ui/dialog/filedialogimpl-win32.h new file mode 100644 index 0000000..9dedaa3 --- /dev/null +++ b/src/ui/dialog/filedialogimpl-win32.h @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of native file dialogs for Win32 + */ +/* Authors: + * Joel Holdsworth + * The Inkscape Organization + * + * Copyright (C) 2004-2008 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> + +#ifdef _WIN32 + +#include "filedialogimpl-gtkmm.h" + +#include "inkgc/gc-core.h" + +#include <windows.h> + + +namespace Inkscape +{ +namespace UI +{ +namespace Dialog +{ + +/*######################################################################### +### F I L E D I A L O G B A S E C L A S S +#########################################################################*/ + +/// This class is the base implementation of a MS Windows +/// file dialog. +class FileDialogBaseWin32 +{ +protected: + /// Abstract Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + /// @param preferenceBase The preferences key + FileDialogBaseWin32(Gtk::Window &parent, const Glib::ustring &dir, + const char *title, FileDialogType type, + gchar const *preferenceBase); + + /// Destructor + ~FileDialogBaseWin32(); + +public: + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + Inkscape::Extension::Extension* getSelectionType(); + + /// Get the path of the current directory + Glib::ustring getCurrentDirectory(); + + +protected: + /// The dialog type + FileDialogType dialogType; + + /// A pointer to the GTK main-loop context object. This + /// is used to keep the rest of the inkscape UI running + /// while the file dialog is displayed + GMainLoop *_main_loop; + + /// The result of the call to GetOpenFileName. If true + /// the user clicked OK, if false the user clicked cancel + bool _result; + + /// The parent window + Gtk::Window &parent; + + /// The windows handle of the parent window + HWND _ownerHwnd; + + /// The path of the directory that is currently being + /// browsed + Glib::ustring _current_directory; + + /// The title of the dialog in UTF-16 + wchar_t *_title; + + /// The path of the currently selected file in UTF-16 + wchar_t _path_string[_MAX_PATH]; + + /// The filter string for GetOpenFileName in UTF-16 + wchar_t *_filter; + + /// The index of the currently selected filter. + /// This value must be greater than or equal to 1, + /// and less than or equal to _filter_count. + unsigned int _filter_index; + + /// The number of filters registered + unsigned int _filter_count; + + /// An array of the extensions associated with the + /// file types of each filter. So the Nth entry of + /// this array corresponds to the extension of the Nth + /// filter in the list. NULL if no specific extension is + /// specified/ + Inkscape::Extension::Extension **_extension_map; + + /// The currently selected extension. Valid after an [OK] + Inkscape::Extension::Extension *_extension; +}; + + +/*######################################################################### +### F I L E O P E N +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetOpenFileName API +class FileOpenDialogImplWin32 : public FileOpenDialog, public FileDialogBaseWin32 +{ +public: + /// Constructor + /// @param parent The parent window for the dialog + /// @param dir The directory to begin browing from + /// @param title The title caption for the dialog in UTF-8 + /// @param type The dialog type + FileOpenDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title); + + /// Destructor + virtual ~FileOpenDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Gets a list of the selected file names + /// @return Returns an STL vector filled with the + /// GTK names of the selected files + std::vector<Glib::ustring> getFilenames(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + + /// Add a custom file filter menu item + /// @param name - Name of the filter (such as "Javscript") + /// @param pattern - File filtering patter (such as "*.js") + /// Use the FileDialogType::CUSTOM_TYPE in constructor to not include other file types + virtual void addFilterMenu(Glib::ustring name, Glib::ustring pattern); + +private: + + /// Create filter menu for this type of dialog + void createFilterMenu(); + + + /// The handle of the preview pane window + HWND _preview_wnd; + + /// The handle of the file dialog window + HWND _file_dialog_wnd; + + /// A pointer to the standard window proc of the + /// unhooked file dialog + WNDPROC _base_window_proc; + + /// The handle of the bitmap of the "show preview" + /// toggle button + HBITMAP _show_preview_button_bitmap; + + /// The handle of the toolbar's window + HWND _toolbar_wnd; + + /// This flag is set true when the preview should be + /// shown, or false when it should be hidden + static bool _show_preview; + + + /// The current width of the preview pane in pixels + int _preview_width; + + /// The current height of the preview pane in pixels + int _preview_height; + + /// The handle of the windows to display within the + /// preview pane, or NULL if no image should be displayed + HBITMAP _preview_bitmap; + + /// The windows shell icon for the selected file + HICON _preview_file_icon; + + /// The size of the preview file in kilobytes + unsigned long _preview_file_size; + + + /// The width of the document to be shown in the preview panel + double _preview_document_width; + + /// The width of the document to be shown in the preview panel + double _preview_document_height; + + /// The width of the rendered preview image in pixels + int _preview_image_width; + + /// The height of the rendered preview image in pixels + int _preview_image_height; + + /// A GDK Pixbuf of the rendered preview to be displayed + Glib::RefPtr<Gdk::Pixbuf> _preview_bitmap_image; + + /// This flag is set true if a file has been selected + bool _file_selected; + + /// This flag is set true when the GetOpenFileName call + /// has returned + bool _finished; + + /// This mutex is used to ensure that the worker thread + /// that calls GetOpenFileName cannot collide with the + /// main Inkscape thread +#if GLIB_CHECK_VERSION(2,32,0) + Glib::Threads::Mutex *_mutex; +#else + Glib::Mutex *_mutex; +#endif + + + /// The controller function for the thread which calls + /// GetOpenFileName + void GetOpenFileName_thread(); + + /// Registers the Windows Class of the preview panel window + static void register_preview_wnd_class(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetOpenFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + + /// A message proc which wraps the standard dialog proc, + /// but intercepts some calls + static LRESULT CALLBACK file_dialog_subclass_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// The message proc for the preview panel window + static LRESULT CALLBACK preview_wnd_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + + /// Lays out the controls in the file dialog given it's + /// current size + /// GetOpenFileName thread only. + void layout_dialog(); + + /// Enables or disables the file preview. + /// GetOpenFileName thread only. + void enable_preview(bool enable); + + /// This function is called in the App thread when a file had + /// been selected + void file_selected(); + + /// Loads and renders the unshrunk preview image. + /// Main app thread only. + void load_preview(); + + /// Frees all the allocated objects associated with the file + /// currently being previewed + /// Main app thread only. + void free_preview(); + + /// Loads preview for an SVG or SVGZ file. + /// Main app thread only. + /// @return Returns true if the SVG loaded successfully + bool set_svg_preview(); + + /// A callback to allow this class to dispose of the + /// memory block of the rendered SVG bitmap + /// @buffer buffer The buffer to free + static void destroy_svg_rendering(const guint8 *buffer); + + /// Loads the preview for a raster image + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_image_preview(); + + /// Loads the preview for a meta file + /// Main app thread only. + /// @return Returns true if the image loaded successfully + bool set_emf_preview(); + + /// This flag is set true when a meta file is previewed + bool _preview_emf_image; + + /// Renders the unshrunk preview image to a windows HTBITMAP + /// which can be painted in the preview pain. + /// Main app thread only. + void render_preview(); + + /// Formats the caption in UTF-16 for the preview image + /// @param caption The buffer to format the caption string into + /// @param caption_size The number of wchar_ts in the caption buffer + /// @return Returns the number of characters in caption string + int format_caption(wchar_t *caption, int caption_size); +}; + + +/*######################################################################### +### F I L E S A V E +#########################################################################*/ + +/// An Inkscape compatible wrapper around MS Windows GetSaveFileName API +class FileSaveDialogImplWin32 : public FileSaveDialog, public FileDialogBaseWin32 +{ + +public: + FileSaveDialogImplWin32(Gtk::Window &parent, + const Glib::ustring &dir, + FileDialogType fileTypes, + const char *title, + const Glib::ustring &default_key, + const char *docTitle, + const Inkscape::Extension::FileSaveMethod save_method); + + /// Destructor + virtual ~FileSaveDialogImplWin32(); + + /// Shows the file dialog, and blocks until a file + /// has been selected. + /// @return Returns true if the user selected a + /// file, or false if the user pressed cancel. + bool show(); + + /// Get the path of the current directory + virtual Glib::ustring getCurrentDirectory() + { return FileDialogBaseWin32::getCurrentDirectory(); } + + /// Gets the currently selected extension. Valid after an [OK] + /// @return Returns a pointer to the selected extension, or NULL + /// if the selected filter requires an automatic type detection + virtual Inkscape::Extension::Extension* getSelectionType() + { return FileDialogBaseWin32::getSelectionType(); } + + virtual void setSelectionType( Inkscape::Extension::Extension *key ); + + virtual void addFileType(Glib::ustring name, Glib::ustring pattern); + +private: + /// A handle to the title label and edit box + HWND _title_label; + HWND _title_edit; + + /// Create a filter menu for this type of dialog + void createFilterMenu(); + + /// The controller function for the thread which calls + /// GetSaveFileName + void GetSaveFileName_thread(); + + /// A message proc which is called by the standard dialog + /// proc + static UINT_PTR CALLBACK GetSaveFileName_hookproc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam); + +}; + + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/fill-and-stroke.cpp b/src/ui/dialog/fill-and-stroke.cpp new file mode 100644 index 0000000..1de7ede --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill and Stroke dialog - implementation. + * + * Based on the old sp_object_properties_dialog. + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "desktop-style.h" +#include "document.h" +#include "fill-and-stroke.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "preferences.h" +#include "verbs.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/notebook-page.h" + +#include "widgets/fill-style.h" +#include "widgets/paint-selector.h" +#include "widgets/stroke-style.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FillAndStroke::FillAndStroke() + : UI::Widget::Panel("/dialogs/fillstroke", SP_VERB_DIALOG_FILL_STROKE) + , _page_fill(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_paint(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _page_stroke_style(Gtk::manage(new UI::Widget::NotebookPage(1, 1, true, true))) + , _composite_settings(SP_VERB_DIALOG_FILL_STROKE, "fillstroke", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY) + , deskTrack() + , targetDesktop(nullptr) + , fillWdgt(nullptr) + , strokeWdgt(nullptr) + , desktopChangeConn() +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(2); + contents->pack_start(_notebook, true, true); + + _notebook.append_page(*_page_fill, _createPageTabLabel(_("_Fill"), INKSCAPE_ICON("object-fill"))); + _notebook.append_page(*_page_stroke_paint, _createPageTabLabel(_("Stroke _paint"), INKSCAPE_ICON("object-stroke"))); + _notebook.append_page(*_page_stroke_style, _createPageTabLabel(_("Stroke st_yle"), INKSCAPE_ICON("object-stroke-style"))); + _notebook.set_vexpand(true); + + _notebook.signal_switch_page().connect(sigc::mem_fun(this, &FillAndStroke::_onSwitchPage)); + + _layoutPageFill(); + _layoutPageStrokePaint(); + _layoutPageStrokeStyle(); + + contents->pack_end(_composite_settings, Gtk::PACK_SHRINK); + + show_all_children(); + + _composite_settings.setSubject(&_subject); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FillAndStroke::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +FillAndStroke::~FillAndStroke() +{ + _composite_settings.setSubject(nullptr); + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void FillAndStroke::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void FillAndStroke::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + targetDesktop = desktop; + if (fillWdgt) { + sp_fill_style_widget_set_desktop(fillWdgt, desktop); + } + if (strokeWdgt) { + sp_fill_style_widget_set_desktop(strokeWdgt, desktop); + } + if (strokeStyleWdgt) { + sp_stroke_style_widget_set_desktop(strokeStyleWdgt, desktop); + } + _composite_settings.setSubject(&_subject); + } +} + +void FillAndStroke::_onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + _savePagePref(pagenum); +} + +void +FillAndStroke::_savePagePref(guint page_num) +{ + // remember the current page + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/dialogs/fillstroke/page", page_num); +} + +void +FillAndStroke::_layoutPageFill() +{ + fillWdgt = Gtk::manage(sp_fill_style_widget_new()); + _page_fill->table().attach(*fillWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokePaint() +{ + strokeWdgt = Gtk::manage(sp_stroke_style_paint_widget_new()); + _page_stroke_paint->table().attach(*strokeWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::_layoutPageStrokeStyle() +{ + strokeStyleWdgt = sp_stroke_style_line_widget_new(); + strokeStyleWdgt->set_hexpand(); + strokeStyleWdgt->set_halign(Gtk::ALIGN_START); + + _page_stroke_style->table().attach(*strokeStyleWdgt, 0, 0, 1, 1); +} + +void +FillAndStroke::showPageFill() +{ + present(); + _notebook.set_current_page(0); + _savePagePref(0); + +} + +void +FillAndStroke::showPageStrokePaint() +{ + present(); + _notebook.set_current_page(1); + _savePagePref(1); +} + +void +FillAndStroke::showPageStrokeStyle() +{ + present(); + _notebook.set_current_page(2); + _savePagePref(2); + +} + +Gtk::HBox& +FillAndStroke::_createPageTabLabel(const Glib::ustring& label, const char *label_image) +{ + Gtk::HBox *_tab_label_box = Gtk::manage(new Gtk::HBox(false, 4)); + + auto img = Gtk::manage(sp_get_icon_image(label_image, Gtk::ICON_SIZE_MENU)); + _tab_label_box->pack_start(*img); + + Gtk::Label *_tab_label = Gtk::manage(new Gtk::Label(label, true)); + _tab_label_box->pack_start(*_tab_label); + _tab_label_box->show_all(); + + return *_tab_label_box; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/fill-and-stroke.h b/src/ui/dialog/fill-and-stroke.h new file mode 100644 index 0000000..f425969 --- /dev/null +++ b/src/ui/dialog/fill-and-stroke.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill and Stroke dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004--2007 Authors + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H +#define INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H + +#include "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "ui/dialog/desktop-tracker.h" + +#include <gtkmm/notebook.h> +#include "ui/widget/style-subject.h" + +namespace Inkscape { +namespace UI { + +namespace Widget { +class NotebookPage; +} + +namespace Dialog { + +class FillAndStroke : public UI::Widget::Panel { +public: + FillAndStroke(); + ~FillAndStroke() override; + + static FillAndStroke &getInstance() { return *new FillAndStroke(); } + + + void setDesktop(SPDesktop *desktop) override; + + //void selectionChanged(Inkscape::Selection *selection); + + void showPageFill(); + void showPageStrokePaint(); + void showPageStrokeStyle(); + +protected: + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage *_page_fill; + UI::Widget::NotebookPage *_page_stroke_paint; + UI::Widget::NotebookPage *_page_stroke_style; + + UI::Widget::StyleSubject::Selection _subject; + UI::Widget::ObjectCompositeSettings _composite_settings; + + Gtk::HBox &_createPageTabLabel(const Glib::ustring &label, + const char *label_image); + + void _layoutPageFill(); + void _layoutPageStrokePaint(); + void _layoutPageStrokeStyle(); + void _savePagePref(guint page_num); + void _onSwitchPage(Gtk::Widget *page, guint pagenum); + +private: + FillAndStroke(FillAndStroke const &d) = delete; + FillAndStroke& operator=(FillAndStroke const &d) = delete; + + void setTargetDesktop(SPDesktop *desktop); + + DesktopTracker deskTrack; + SPDesktop *targetDesktop; + Gtk::Widget *fillWdgt; + Gtk::Widget *strokeWdgt; + Gtk::Widget *strokeStyleWdgt; + sigc::connection desktopChangeConn; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +#endif // INKSCAPE_UI_DIALOG_FILL_AND_STROKE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filter-editor.cpp b/src/ui/dialog/filter-editor.cpp new file mode 100644 index 0000000..0fae983 --- /dev/null +++ b/src/ui/dialog/filter-editor.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <string> + +#include <gtkmm.h> + +#include <gdkmm/display.h> +#include <gdkmm/seat.h> + +#include <glibmm/convert.h> +#include <glibmm/error.h> +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/stringutils.h> + +#include "desktop.h" +#include "dialog-manager.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-editor.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "io/sys.h" +#include "io/resource.h" + +#include "object/filters/blend.h" +#include "object/filters/colormatrix.h" +#include "object/filters/componenttransfer.h" +#include "object/filters/componenttransfer-funcnode.h" +#include "object/filters/convolvematrix.h" +#include "object/filters/distantlight.h" +#include "object/filters/merge.h" +#include "object/filters/mergenode.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" +#include "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/widget/spinbutton.h" + +using namespace Inkscape::Filters; +using namespace Inkscape::IO::Resource; +namespace Inkscape { +namespace UI { +namespace Dialog { + +FilterEditorDialog::FilterEditorDialog() : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS) +{ + + const std::string req_widgets[] = {"FilterEditor", "FilterList", "FilterFERX", "FilterFERY", "FilterFERH", "FilterFERW", "FilterPreview", "FilterPrimitiveDescImage", "FilterPrimitiveList", "FilterPrimitiveDescText", "FilterPrimitiveAdd"}; + Glib::ustring gladefile = get_filename(UIS, "dialog-filter-editor.glade"); + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch(const Glib::Error& ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + + Gtk::Object* test; + for(std::string w:req_widgets) { + builder->get_widget(w,test); + if(!test){ + g_warning("Required widget %s does not exist", w.c_str()); + return; + } + } + + builder->get_widget("FilterEditor", FilterEditor); + _getContents()->add(*FilterEditor); + +//test + Gtk::ComboBox *OptionList; + builder->get_widget("OptionList",OptionList); + FilterStore = builder->get_object("FilterStore"); + Glib::RefPtr<Gtk::ListStore> fs = Glib::RefPtr<Gtk::ListStore>::cast_static(FilterStore); + Gtk::TreeModel::Row row = *(fs->append()); + + + + + +} +FilterEditorDialog::~FilterEditorDialog()= default; + + + + + + +} // Never put these namespaces together unless you are using gcc 6+ +} +} // P.S. This is for Inkscape::UI::Dialog + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filter-editor.h b/src/ui/dialog/filter-editor.h new file mode 100644 index 0000000..e20ecbf --- /dev/null +++ b/src/ui/dialog/filter-editor.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Editor dialog + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EDITOR_H +#define INKSCAPE_UI_DIALOG_FILTER_EDITOR_H + +#include <gtkmm/notebook.h> +#include <gtkmm/sizegroup.h> +#include <gtkmm/builder.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/combobox.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/liststore.h> + +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> + +#include "ui/widget/panel.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FilterEditorDialog : public UI::Widget::Panel { +public: + + FilterEditorDialog(); + ~FilterEditorDialog() override; + + static FilterEditorDialog &getInstance() + { return *new FilterEditorDialog(); } + +// void set_attrs_locked(const bool); +private: + Glib::RefPtr<Gtk::Builder> builder; + Glib::RefPtr<Glib::Object> FilterStore; + Gtk::Box *FilterEditor; +}; +} +} +} +#endif diff --git a/src/ui/dialog/filter-effects-dialog.cpp b/src/ui/dialog/filter-effects-dialog.cpp new file mode 100644 index 0000000..cff7e3b --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.cpp @@ -0,0 +1,3114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Filter Effects dialog. + */ +/* Authors: + * Nicholas Bishop <nicholasbishop@gmail.org> + * Rodrigo Kumpera <kumpera@gmail.com> + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/imagemenuitem.h> + +#include <gdkmm/display.h> +#include <gdkmm/general.h> +#include <gdkmm/seat.h> + +#include <gtkmm/checkbutton.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/sizegroup.h> + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include <glibmm/main.h> +#include <glibmm/convert.h> + +#include <utility> + +#include "desktop.h" +#include "dialog-manager.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "filter-effects-dialog.h" +#include "filter-enums.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/colormatrix.h" +#include "object/filters/componenttransfer.h" +#include "object/filters/componenttransfer-funcnode.h" +#include "object/filters/convolvematrix.h" +#include "object/filters/distantlight.h" +#include "object/filters/merge.h" +#include "object/filters/mergenode.h" +#include "object/filters/pointlight.h" +#include "object/filters/spotlight.h" +#include "style.h" + +#include "svg/svg-color.h" + +#include "ui/dialog/filedialog.h" +#include "ui/widget/filter-effect-chooser.h" +#include "ui/widget/spinbutton.h" + +#include "io/sys.h" + + +using namespace Inkscape::Filters; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::AttrWidget; +using Inkscape::UI::Widget::ComboBoxEnum; +using Inkscape::UI::Widget::DualSpinScale; +using Inkscape::UI::Widget::SpinScale; + + +// Returns the number of inputs available for the filter primitive type +static int input_count(const SPFilterPrimitive* prim) +{ + if(!prim) + return 0; + else if(SP_IS_FEBLEND(prim) || SP_IS_FECOMPOSITE(prim) || SP_IS_FEDISPLACEMENTMAP(prim)) + return 2; + else if(SP_IS_FEMERGE(prim)) { + // Return the number of feMergeNode connections plus an extra + return (int) (prim->children.size() + 1); + } + else + return 1; +} + +class CheckButtonAttr : public Gtk::CheckButton, public AttrWidget +{ +public: + CheckButtonAttr(bool def, const Glib::ustring& label, + Glib::ustring tv, Glib::ustring fv, + const SPAttributeEnum a, char* tip_text) + : Gtk::CheckButton(label), + AttrWidget(a, def), + _true_val(std::move(tv)), _false_val(std::move(fv)) + { + signal_toggled().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + Glib::ustring get_as_attribute() const override + { + return get_active() ? _true_val : _false_val; + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + if(_true_val == val) + set_active(true); + else if(_false_val == val) + set_active(false); + } else { + set_active(get_default()->as_bool()); + } + } +private: + const Glib::ustring _true_val, _false_val; +}; + +class SpinButtonAttr : public Inkscape::UI::Widget::SpinButton, public AttrWidget +{ +public: + SpinButtonAttr(double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, double def, char* tip_text) + : Inkscape::UI::Widget::SpinButton(climb_rate, digits), + AttrWidget(a, def) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + set_range(lower, upper); + set_increments(step_inc, 0); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + } + + Glib::ustring get_as_attribute() const override + { + const double val = get_value(); + + if(get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val){ + set_value(Glib::Ascii::strtod(val)); + } else { + set_value(get_default()->as_double()); + } + } +}; + +template< typename T> class ComboWithTooltip : public Gtk::EventBox +{ +public: + ComboWithTooltip<T>(T default_value, const Util::EnumDataConverter<T>& c, const SPAttributeEnum a = SP_ATTR_INVALID, char* tip_text = nullptr) + { + if (tip_text) { + set_tooltip_text(tip_text); + } + combo = new ComboBoxEnum<T>(default_value, c, a, false); + add(*combo); + show_all(); + } + + ~ComboWithTooltip() override + { + delete combo; + } + + ComboBoxEnum<T>* get_attrwidget() + { + return combo; + } +private: + ComboBoxEnum<T>* combo; +}; + +// Contains an arbitrary number of spin buttons that use separate attributes +class MultiSpinButton : public Gtk::HBox +{ +public: + MultiSpinButton(double lower, double upper, double step_inc, + double climb_rate, int digits, std::vector<SPAttributeEnum> attrs, std::vector<double> default_values, std::vector<char*> tip_text) + { + g_assert(attrs.size()==default_values.size()); + g_assert(attrs.size()==tip_text.size()); + set_spacing(4); + for(unsigned i = 0; i < attrs.size(); ++i) { + unsigned index = attrs.size() - 1 - i; + _spins.push_back(new SpinButtonAttr(lower, upper, step_inc, climb_rate, digits, attrs[index], default_values[index], tip_text[index])); + pack_end(*_spins.back(), false, false); + } + } + + ~MultiSpinButton() override + { + for(auto & _spin : _spins) + delete _spin; + } + + std::vector<SpinButtonAttr*>& get_spinbuttons() + { + return _spins; + } +private: + std::vector<SpinButtonAttr*> _spins; +}; + +// Contains two spinbuttons that describe a NumberOptNumber +class DualSpinButton : public Gtk::HBox, public AttrWidget +{ +public: + DualSpinButton(char* def, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, char* tt1, char* tt2) + : AttrWidget(a, def), //TO-DO: receive default num-opt-num as parameter in the constructor + _s1(climb_rate, digits), _s2(climb_rate, digits) + { + if (tt1) { + _s1.set_tooltip_text(tt1); + } + if (tt2) { + _s2.set_tooltip_text(tt2); + } + _s1.set_range(lower, upper); + _s2.set_range(lower, upper); + _s1.set_increments(step_inc, 0); + _s2.set_increments(step_inc, 0); + + _s1.signal_value_changed().connect(signal_attr_changed().make_slot()); + _s2.signal_value_changed().connect(signal_attr_changed().make_slot()); + + set_spacing(4); + pack_end(_s2, false, false); + pack_end(_s1, false, false); + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton1() + { + return _s1; + } + + Inkscape::UI::Widget::SpinButton& get_spinbutton2() + { + return _s2; + } + + Glib::ustring get_as_attribute() const override + { + double v1 = _s1.get_value(); + double v2 = _s2.get_value(); + + if(_s1.get_digits() == 0) { + v1 = (int)v1; + v2 = (int)v2; + } + + return Glib::Ascii::dtostr(v1) + " " + Glib::Ascii::dtostr(v2); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + NumberOptNumber n; + if(val) { + n.set(val); + } else { + n.set(get_default()->as_charptr()); + } + _s1.set_value(n.getNumber()); + _s2.set_value(n.getOptNumber()); + + } +private: + Inkscape::UI::Widget::SpinButton _s1, _s2; +}; + +class ColorButton : public Gtk::ColorButton, public AttrWidget +{ +public: + ColorButton(unsigned int def, const SPAttributeEnum a, char* tip_text) + : AttrWidget(a, def) + { + signal_color_set().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + + Gdk::RGBA col; + col.set_rgba_u(65535, 65535, 65535); + set_rgba(col); + } + + // Returns the color in 'rgb(r,g,b)' form. + Glib::ustring get_as_attribute() const override + { + // no doubles here, so we can use the standard string stream. + std::ostringstream os; + + const auto c = get_rgba(); + const int r = c.get_red_u() / 257, g = c.get_green_u() / 257, b = c.get_blue_u() / 257;//TO-DO: verify this. This sounds a lot strange! shouldn't it be 256? + os << "rgb(" << r << "," << g << "," << b << ")"; + return os.str(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + guint32 i = 0; + if(val) { + i = sp_svg_read_color(val, 0xFFFFFFFF); + } else { + i = (guint32) get_default()->as_uint(); + } + const int r = SP_RGBA32_R_U(i), g = SP_RGBA32_G_U(i), b = SP_RGBA32_B_U(i); + + Gdk::RGBA col; + col.set_rgba_u(r * 256, g * 256, b * 256); + set_rgba(col); + } +}; + +// Used for tableValue in feComponentTransfer +class EntryAttr : public Gtk::Entry, public AttrWidget +{ +public: + EntryAttr(const SPAttributeEnum a, char* tip_text) + : AttrWidget(a) + { + signal_changed().connect(signal_attr_changed().make_slot()); + if (tip_text) { + set_tooltip_text(tip_text); + } + } + + // No validity checking is done + Glib::ustring get_as_attribute() const override + { + return get_text(); + } + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + set_text( val ); + } else { + set_text( "" ); + } + } +}; + +/* Displays/Edits the matrix for feConvolveMatrix or feColorMatrix */ +class FilterEffectsDialog::MatrixAttr : public Gtk::Frame, public AttrWidget +{ +public: + MatrixAttr(const SPAttributeEnum a, char* tip_text = nullptr) + : AttrWidget(a), _locked(false) + { + _model = Gtk::ListStore::create(_columns); + _tree.set_model(_model); + _tree.set_headers_visible(false); + _tree.show(); + add(_tree); + set_shadow_type(Gtk::SHADOW_IN); + if (tip_text) { + _tree.set_tooltip_text(tip_text); + } + } + + std::vector<double> get_values() const + { + std::vector<double> vec; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) + vec.push_back(iter[_columns.cols[c]]); + } + return vec; + } + + void set_values(const std::vector<double>& v) + { + unsigned i = 0; + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + if(i >= v.size()) + return; + iter[_columns.cols[c]] = v[i]; + ++i; + } + } + } + + Glib::ustring get_as_attribute() const override + { + // use SVGOStringStream to output SVG-compatible doubles + Inkscape::SVGOStringStream os; + + for(const auto & iter : _model->children()) { + for(unsigned c = 0; c < _tree.get_columns().size(); ++c) { + os << iter[_columns.cols[c]] << " "; + } + } + + return os.str(); + } + + void set_from_attribute(SPObject* o) override + { + if(o) { + if(SP_IS_FECONVOLVEMATRIX(o)) { + SPFeConvolveMatrix* conv = SP_FECONVOLVEMATRIX(o); + int cols, rows; + cols = (int)conv->order.getNumber(); + if(cols > 5) + cols = 5; + rows = conv->order.optNumber_set ? (int)conv->order.getOptNumber() : cols; + update(o, rows, cols); + } + else if(SP_IS_FECOLORMATRIX(o)) + update(o, 4, 5); + } + } +private: + class MatrixColumns : public Gtk::TreeModel::ColumnRecord + { + public: + MatrixColumns() + { + cols.resize(5); + for(auto & col : cols) + add(col); + } + std::vector<Gtk::TreeModelColumn<double> > cols; + }; + + void update(SPObject* o, const int rows, const int cols) + { + if(_locked) + return; + + _model->clear(); + + _tree.remove_all_columns(); + + std::vector<gdouble>* values = nullptr; + if(SP_IS_FECOLORMATRIX(o)) + values = &SP_FECOLORMATRIX(o)->values; + else if(SP_IS_FECONVOLVEMATRIX(o)) + values = &SP_FECONVOLVEMATRIX(o)->kernelMatrix; + else + return; + + if(o) { + int ndx = 0; + + for(int i = 0; i < cols; ++i) { + _tree.append_column_numeric_editable("", _columns.cols[i], "%.2f"); + dynamic_cast<Gtk::CellRendererText*>( + _tree.get_column_cell_renderer(i))->signal_edited().connect( + sigc::mem_fun(*this, &MatrixAttr::rebind)); + } + + for(int r = 0; r < rows; ++r) { + Gtk::TreeRow row = *(_model->append()); + // Default to identity matrix + for(int c = 0; c < cols; ++c, ++ndx) + row[_columns.cols[c]] = ndx < (int)values->size() ? (*values)[ndx] : (r == c ? 1 : 0); + } + } + } + + void rebind(const Glib::ustring&, const Glib::ustring&) + { + _locked = true; + signal_attr_changed()(); + _locked = false; + } + + bool _locked; + Gtk::TreeView _tree; + Glib::RefPtr<Gtk::ListStore> _model; + MatrixColumns _columns; +}; + +// Displays a matrix or a slider for feColorMatrix +class FilterEffectsDialog::ColorMatrixValues : public Gtk::Frame, public AttrWidget +{ +public: + ColorMatrixValues() + : AttrWidget(SP_ATTR_VALUES), + // TRANSLATORS: this dialog is accessible via menu Filters - Filter editor + _matrix(SP_ATTR_VALUES, _("This matrix determines a linear transform on color space. Each line affects one of the color components. Each column determines how much of each color component from the input is passed to the output. The last column does not depend on input colors, so can be used to adjust a constant component value.")), + _saturation("", 0, 0, 1, 0.1, 0.01, 2, SP_ATTR_VALUES), + _angle("", 0, 0, 360, 0.1, 0.01, 1, SP_ATTR_VALUES), + _label(C_("Label", "None"), Gtk::ALIGN_START), + _use_stored(false), + _saturation_store(0), + _angle_store(0) + { + _matrix.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _saturation.signal_attr_changed().connect(signal_attr_changed().make_slot()); + _angle.signal_attr_changed().connect(signal_attr_changed().make_slot()); + signal_attr_changed().connect(sigc::mem_fun(*this, &ColorMatrixValues::update_store)); + + _matrix.show(); + _saturation.show(); + _angle.show(); + _label.show(); + _label.set_sensitive(false); + + set_shadow_type(Gtk::SHADOW_NONE); + } + + void set_from_attribute(SPObject* o) override + { + if(SP_IS_FECOLORMATRIX(o)) { + SPFeColorMatrix* col = SP_FECOLORMATRIX(o); + remove(); + switch(col->type) { + case COLORMATRIX_SATURATE: + add(_saturation); + if(_use_stored) + _saturation.set_value(_saturation_store); + else + _saturation.set_from_attribute(o); + break; + case COLORMATRIX_HUEROTATE: + add(_angle); + if(_use_stored) + _angle.set_value(_angle_store); + else + _angle.set_from_attribute(o); + break; + case COLORMATRIX_LUMINANCETOALPHA: + add(_label); + break; + case COLORMATRIX_MATRIX: + default: + add(_matrix); + if(_use_stored) + _matrix.set_values(_matrix_store); + else + _matrix.set_from_attribute(o); + break; + } + _use_stored = true; + } + } + + Glib::ustring get_as_attribute() const override + { + const Widget* w = get_child(); + if(w == &_label) + return ""; + else + return dynamic_cast<const AttrWidget*>(w)->get_as_attribute(); + } + + void clear_store() + { + _use_stored = false; + } +private: + void update_store() + { + const Widget* w = get_child(); + if(w == &_matrix) + _matrix_store = _matrix.get_values(); + else if(w == &_saturation) + _saturation_store = _saturation.get_value(); + else if(w == &_angle) + _angle_store = _angle.get_value(); + } + + MatrixAttr _matrix; + SpinScale _saturation; + SpinScale _angle; + Gtk::Label _label; + + // Store separate values for the different color modes + bool _use_stored; + std::vector<double> _matrix_store; + double _saturation_store; + double _angle_store; +}; + +static Inkscape::UI::Dialog::FileOpenDialog * selectFeImageFileInstance = nullptr; + +//Displays a chooser for feImage input +//It may be a filename or the id for an SVG Element +//described in xlink:href syntax +class FileOrElementChooser : public Gtk::HBox, public AttrWidget +{ +public: + FileOrElementChooser(const SPAttributeEnum a) + : AttrWidget(a) + { + pack_start(_entry, false, false); + pack_start(_fromFile, false, false); + pack_start(_fromSVGElement, false, false); + + _fromFile.set_label(_("Image File")); + _fromFile.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_file)); + + _fromSVGElement.set_label(_("Selected SVG Element")); + _fromSVGElement.signal_clicked().connect(sigc::mem_fun(*this, &FileOrElementChooser::select_svg_element)); + + _entry.signal_changed().connect(signal_attr_changed().make_slot()); + + show_all(); + + } + + // Returns the element in xlink:href form. + Glib::ustring get_as_attribute() const override + { + return _entry.get_text(); + } + + + void set_from_attribute(SPObject* o) override + { + const gchar* val = attribute_value(o); + if(val) { + _entry.set_text(val); + } else { + _entry.set_text(""); + } + } + + void set_desktop(SPDesktop* d){ + _desktop = d; + } + +private: + void select_svg_element(){ + Inkscape::Selection* sel = _desktop->getSelection(); + if (sel->isEmpty()) return; + Inkscape::XML::Node* node = sel->xmlNodes().front(); + if (!node || !node->matchAttributeName("id")) return; + + std::ostringstream xlikhref; + xlikhref << "#" << node->attribute("id"); + _entry.set_text(xlikhref.str()); + } + + void select_file(){ + + //# Get the current directory for finding files + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring open_path; + Glib::ustring attr = prefs->getString("/dialogs/open/path"); + if (!attr.empty()) + open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + + //# If no open path, default to our home directory + if (open_path.size() < 1) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog if we don't already have one + if (!selectFeImageFileInstance) { + selectFeImageFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *_desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::SVG_TYPES,/*TODO: any image, not just svg*/ + (char const *)_("Select an image to be used as feImage input")); + } + + //# Show the dialog + bool const success = selectFeImageFileInstance->show(); + if (!success) + return; + + //# User selected something. Get name and type + Glib::ustring fileName = selectFeImageFileInstance->getFilename(); + + if (fileName.size() > 0) { + + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + fileName = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + open_path = fileName; + open_path.append(G_DIR_SEPARATOR_S); + prefs->setString("/dialogs/open/path", open_path); + + _entry.set_text(fileName); + } + return; + } + + Gtk::Entry _entry; + Gtk::Button _fromFile; + Gtk::Button _fromSVGElement; + SPDesktop* _desktop; +}; + +class FilterEffectsDialog::Settings +{ +public: + typedef sigc::slot<void, const AttrWidget*> SetAttrSlot; + + Settings(FilterEffectsDialog& d, Gtk::Box& b, SetAttrSlot slot, const int maxtypes) + : _dialog(d), _set_attr_slot(std::move(slot)), _current_type(-1), _max_types(maxtypes) + { + _groups.resize(_max_types); + _attrwidgets.resize(_max_types); + _size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + + for(int i = 0; i < _max_types; ++i) { + _groups[i] = new Gtk::VBox(false, 3); + b.set_spacing(4); + b.pack_start(*_groups[i], Gtk::PACK_SHRINK); + } + //_current_type = 0; If set to 0 then update_and_show() fails to update properly. + } + + ~Settings() + { + for(int i = 0; i < _max_types; ++i) { + delete _groups[i]; + for(auto & j : _attrwidgets[i]) + delete j; + } + } + + // Show the active settings group and update all the AttrWidgets with new values + void show_and_update(const int t, SPObject* ob) + { + if(t != _current_type) { + type(t); + for(auto & _group : _groups) + _group->hide(); + } + if(t >= 0) { + _groups[t]->show(); // Do not use show_all(), it shows children than should be hidden + } + _dialog.set_attrs_locked(true); + for(auto & i : _attrwidgets[_current_type]) + i->set_from_attribute(ob); + _dialog.set_attrs_locked(false); + } + + int get_current_type() const + { + return _current_type; + } + + void type(const int t) + { + _current_type = t; + } + + void add_no_params() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect does not require any parameters."))); + add_widget(lbl, ""); + } + + void add_notimplemented() + { + Gtk::Label* lbl = Gtk::manage(new Gtk::Label(_("This SVG filter effect is not yet implemented in Inkscape."))); + add_widget(lbl, ""); + } + + // LightSource + LightSourceControl* add_lightsource(); + + // Component Transfer Values + ComponentTransferValues* add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel); + + // CheckButton + CheckButtonAttr* add_checkbutton(bool def, const SPAttributeEnum attr, const Glib::ustring& label, + const Glib::ustring& tv, const Glib::ustring& fv, char* tip_text = nullptr) + { + CheckButtonAttr* cb = new CheckButtonAttr(def, label, tv, fv, attr, tip_text); + add_widget(cb, ""); + add_attr_widget(cb); + return cb; + } + + // ColorButton + ColorButton* add_color(unsigned int def, const SPAttributeEnum attr, const Glib::ustring& label, char* tip_text = nullptr) + { + ColorButton* col = new ColorButton(def, attr, tip_text); + add_widget(col, label); + add_attr_widget(col); + return col; + } + + // Matrix + MatrixAttr* add_matrix(const SPAttributeEnum attr, const Glib::ustring& label, char* tip_text) + { + MatrixAttr* conv = new MatrixAttr(attr, tip_text); + add_widget(conv, label); + add_attr_widget(conv); + return conv; + } + + // ColorMatrixValues + ColorMatrixValues* add_colormatrixvalues(const Glib::ustring& label) + { + ColorMatrixValues* cmv = new ColorMatrixValues(); + add_widget(cmv, label); + add_attr_widget(cmv); + return cmv; + } + + // SpinScale + SpinScale* add_spinscale(double def, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, const double climb, const int digits, char* tip_text = nullptr) + { + Glib::ustring tip_text2; + if (tip_text) + tip_text2 = tip_text; + SpinScale* spinslider = new SpinScale("", def, lo, hi, step_inc, climb, digits, attr, tip_text2); + add_widget(spinslider, label); + add_attr_widget(spinslider); + return spinslider; + } + + // DualSpinScale + DualSpinScale* add_dualspinscale(const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, + const Glib::ustring tip_text1 = "", + const Glib::ustring tip_text2 = "") + { + DualSpinScale* dss = new DualSpinScale("", "", lo, lo, hi, step_inc, climb, digits, attr, tip_text1, tip_text2); + add_widget(dss, label); + add_attr_widget(dss); + return dss; + } + + // SpinButton + SpinButtonAttr* add_spinbutton(double defalt_value, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip = nullptr) + { + SpinButtonAttr* sb = new SpinButtonAttr(lo, hi, step_inc, climb, digits, attr, defalt_value, tip); + add_widget(sb, label); + add_attr_widget(sb); + return sb; + } + + // DualSpinButton + DualSpinButton* add_dualspinbutton(char* defalt_value, const SPAttributeEnum attr, const Glib::ustring& label, + const double lo, const double hi, const double step_inc, + const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + DualSpinButton* dsb = new DualSpinButton(defalt_value, lo, hi, step_inc, climb, digits, attr, tip1, tip2); + add_widget(dsb, label); + add_attr_widget(dsb); + return dsb; + } + + // MultiSpinButton + MultiSpinButton* add_multispinbutton(double def1, double def2, const SPAttributeEnum attr1, const SPAttributeEnum attr2, + const Glib::ustring& label, const double lo, const double hi, + const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr) + { + std::vector<SPAttributeEnum> attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + + std::vector<double> default_values; + default_values.push_back(def1); + default_values.push_back(def2); + + std::vector<char*> tips; + tips.push_back(tip1); + tips.push_back(tip2); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + MultiSpinButton* add_multispinbutton(double def1, double def2, double def3, const SPAttributeEnum attr1, const SPAttributeEnum attr2, + const SPAttributeEnum attr3, const Glib::ustring& label, const double lo, + const double hi, const double step_inc, const double climb, const int digits, char* tip1 = nullptr, char* tip2 = nullptr, char* tip3 = nullptr) + { + std::vector<SPAttributeEnum> attrs; + attrs.push_back(attr1); + attrs.push_back(attr2); + attrs.push_back(attr3); + + std::vector<double> default_values; + default_values.push_back(def1); + default_values.push_back(def2); + default_values.push_back(def3); + + std::vector<char*> tips; + tips.push_back(tip1); + tips.push_back(tip2); + tips.push_back(tip3); + + MultiSpinButton* msb = new MultiSpinButton(lo, hi, step_inc, climb, digits, attrs, default_values, tips); + add_widget(msb, label); + for(auto & i : msb->get_spinbuttons()) + add_attr_widget(i); + return msb; + } + + // FileOrElementChooser + FileOrElementChooser* add_fileorelement(const SPAttributeEnum attr, const Glib::ustring& label) + { + FileOrElementChooser* foech = new FileOrElementChooser(attr); + foech->set_desktop(_dialog.getDesktop()); + add_widget(foech, label); + add_attr_widget(foech); + return foech; + } + + // ComboBoxEnum + template<typename T> ComboBoxEnum<T>* add_combo(T default_value, const SPAttributeEnum attr, + const Glib::ustring& label, + const Util::EnumDataConverter<T>& conv, char* tip_text = nullptr) + { + ComboWithTooltip<T>* combo = new ComboWithTooltip<T>(default_value, conv, attr, tip_text); + add_widget(combo, label); + add_attr_widget(combo->get_attrwidget()); + return combo->get_attrwidget(); + } + + // Entry + EntryAttr* add_entry(const SPAttributeEnum attr, + const Glib::ustring& label, + char* tip_text = nullptr) + { + EntryAttr* entry = new EntryAttr(attr, tip_text); + add_widget(entry, label); + add_attr_widget(entry); + return entry; + } + + Glib::RefPtr<Gtk::SizeGroup> _size_group; +private: + void add_attr_widget(AttrWidget* a) + { + _attrwidgets[_current_type].push_back(a); + a->signal_attr_changed().connect(sigc::bind(_set_attr_slot, a)); + } + + /* Adds a new settings widget using the specified label. The label will be formatted with a colon + and all widgets within the setting group are aligned automatically. */ + void add_widget(Gtk::Widget* w, const Glib::ustring& label) + { + Gtk::HBox *hb = Gtk::manage(new Gtk::HBox); + hb->set_spacing(12); + + if (label != "") { + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(label)); + lbl->set_xalign(0.0); + hb->pack_start(*lbl, Gtk::PACK_SHRINK); + _size_group->add_widget(*lbl); + } + + hb->pack_start(*w, Gtk::PACK_EXPAND_WIDGET); + _groups[_current_type]->pack_start(*hb, Gtk::PACK_EXPAND_WIDGET); + hb->show_all(); + } + + std::vector<Gtk::VBox*> _groups; + FilterEffectsDialog& _dialog; + SetAttrSlot _set_attr_slot; + std::vector<std::vector< AttrWidget*> > _attrwidgets; + int _current_type, _max_types; +}; + +// Displays sliders and/or tables for feComponentTransfer +class FilterEffectsDialog::ComponentTransferValues : public Gtk::Frame, public AttrWidget +{ +public: + ComponentTransferValues(FilterEffectsDialog& d, SPFeFuncNode::Channel channel) + : AttrWidget(SP_ATTR_INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(*this, &ComponentTransferValues::set_func_attr), COMPONENTTRANSFER_TYPE_ERROR), + _type(ComponentTransferTypeConverter, SP_ATTR_TYPE, false), + _channel(channel), + _funcNode(nullptr) + { + set_shadow_type(Gtk::SHADOW_IN); + add(_box); + _box.add(_type); + _box.reorder_child(_type, 0); + _type.signal_changed().connect(sigc::mem_fun(*this, &ComponentTransferValues::on_type_changed)); + + _settings.type(COMPONENTTRANSFER_TYPE_LINEAR); + _settings.add_spinscale(1, SP_ATTR_SLOPE, _("Slope"), -10, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_INTERCEPT, _("Intercept"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_GAMMA); + _settings.add_spinscale(1, SP_ATTR_AMPLITUDE, _("Amplitude"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(1, SP_ATTR_EXPONENT, _("Exponent"), 0, 10, 0.1, 0.01, 2); + _settings.add_spinscale(0, SP_ATTR_OFFSET, _("Offset"), -10, 10, 0.1, 0.01, 2); + + _settings.type(COMPONENTTRANSFER_TYPE_TABLE); + _settings.add_entry(SP_ATTR_TABLEVALUES, _("Table")); + + _settings.type(COMPONENTTRANSFER_TYPE_DISCRETE); + _settings.add_entry(SP_ATTR_TABLEVALUES, _("Discrete")); + + //_settings.type(COMPONENTTRANSFER_TYPE_IDENTITY); + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + } + + // FuncNode can be in any order so we must search to find correct one. + SPFeFuncNode* find_node(SPFeComponentTransfer* ct) + { + SPFeFuncNode* funcNode = nullptr; + bool found = false; + for(auto& node: ct->children) { + funcNode = SP_FEFUNCNODE(&node); + if( funcNode->channel == _channel ) { + found = true; + break; + } + } + if( !found ) + funcNode = nullptr; + + return funcNode; + } + + void set_func_attr(const AttrWidget* input) + { + _dialog.set_attr( _funcNode, input->get_attribute(), input->get_as_attribute().c_str()); + } + + // Set new type and update widget visibility + void set_from_attribute(SPObject* o) override + { + // See componenttransfer.cpp + if(SP_IS_FECOMPONENTTRANSFER(o)) { + SPFeComponentTransfer* ct = SP_FECOMPONENTTRANSFER(o); + + _funcNode = find_node(ct); + if( _funcNode ) { + _type.set_from_attribute( _funcNode ); + } else { + // Create <funcNode> + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = nullptr; + switch(_channel) { + case SPFeFuncNode::R: + repr = xml_doc->createElement("svg:feFuncR"); + break; + case SPFeFuncNode::G: + repr = xml_doc->createElement("svg:feFuncG"); + break; + case SPFeFuncNode::B: + repr = xml_doc->createElement("svg:feFuncB"); + break; + case SPFeFuncNode::A: + repr = xml_doc->createElement("svg:feFuncA"); + break; + } + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Now we should find it! + _funcNode = find_node(ct); + if( _funcNode ) { + _funcNode->setAttribute( "type", "identity" ); + } else { + //std::cout << "ERROR ERROR: feFuncX not found!" << std::endl; + } + } + } + + update(); + } + } + +private: + void on_type_changed() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + + _funcNode->setAttributeOrRemoveIfEmpty("type", _type.get_as_attribute()); + + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("New transfer function type")); + update(); + } + } + + void update() + { + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && _funcNode) { + _settings.show_and_update(_type.get_active_data()->id, _funcNode); + } + } + +public: + Glib::ustring get_as_attribute() const override + { + return ""; + } + + FilterEffectsDialog& _dialog; + Gtk::VBox _box; + Settings _settings; + ComboBoxEnum<FilterComponentTransferType> _type; + SPFeFuncNode::Channel _channel; // RGBA + SPFeFuncNode* _funcNode; +}; + +// Settings for the three light source objects +class FilterEffectsDialog::LightSourceControl : public AttrWidget +{ +public: + LightSourceControl(FilterEffectsDialog& d) + : AttrWidget(SP_ATTR_INVALID), + _dialog(d), + _settings(d, _box, sigc::mem_fun(_dialog, &FilterEffectsDialog::set_child_attr_direct), LIGHT_ENDSOURCE), + _light_label(_("Light Source:")), + _light_source(LightSourceConverter), + _locked(false) + { + _light_label.set_xalign(0.0); + _settings._size_group->add_widget(_light_label); + _light_box.pack_start(_light_label, Gtk::PACK_SHRINK); + _light_box.pack_start(_light_source, Gtk::PACK_EXPAND_WIDGET); + _light_box.show_all(); + _light_box.set_spacing(12); + + _box.add(_light_box); + _box.reorder_child(_light_box, 0); + _light_source.signal_changed().connect(sigc::mem_fun(*this, &LightSourceControl::on_source_changed)); + + // FIXME: these range values are complete crap + + _settings.type(LIGHT_DISTANT); + _settings.add_spinscale(0, SP_ATTR_AZIMUTH, _("Azimuth:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the XY plane, in degrees")); + _settings.add_spinscale(0, SP_ATTR_ELEVATION, _("Elevation:"), 0, 360, 1, 1, 0, _("Direction angle for the light source on the YZ plane, in degrees")); + + _settings.type(LIGHT_POINT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + + _settings.type(LIGHT_SPOT); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, SP_ATTR_X, SP_ATTR_Y, SP_ATTR_Z, _("Location:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, /*default z:*/ (double) 0, + SP_ATTR_POINTSATX, SP_ATTR_POINTSATY, SP_ATTR_POINTSATZ, + _("Points At:"), -99999, 99999, 1, 100, 0, _("X coordinate"), _("Y coordinate"), _("Z coordinate")); + _settings.add_spinscale(1, SP_ATTR_SPECULAREXPONENT, _("Specular Exponent:"), 1, 100, 1, 1, 0, _("Exponent value controlling the focus for the light source")); + //TODO: here I have used 100 degrees as default value. But spec says that if not specified, no limiting cone is applied. So, there should be a way for the user to set a "no limiting cone" option. + _settings.add_spinscale(100, SP_ATTR_LIMITINGCONEANGLE, _("Cone Angle:"), 1, 100, 1, 1, 0, _("This is the angle between the spot light axis (i.e. the axis between the light source and the point to which it is pointing at) and the spot light cone. No light is projected outside this cone.")); + + _settings.type(-1); // Force update_and_show() to show/hide windows correctly + + } + + Gtk::VBox& get_box() + { + return _box; + } +protected: + Glib::ustring get_as_attribute() const override + { + return ""; + } + void set_from_attribute(SPObject* o) override + { + if(_locked) + return; + + _locked = true; + + SPObject* child = o->firstChild(); + + if(SP_IS_FEDISTANTLIGHT(child)) + _light_source.set_active(0); + else if(SP_IS_FEPOINTLIGHT(child)) + _light_source.set_active(1); + else if(SP_IS_FESPOTLIGHT(child)) + _light_source.set_active(2); + else + _light_source.set_active(-1); + + update(); + + _locked = false; + } +private: + void on_source_changed() + { + if(_locked) + return; + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim) { + _locked = true; + + SPObject* child = prim->firstChild(); + const int ls = _light_source.get_active_row_number(); + // Check if the light source type has changed + if(!(ls == -1 && !child) && + !(ls == 0 && SP_IS_FEDISTANTLIGHT(child)) && + !(ls == 1 && SP_IS_FEPOINTLIGHT(child)) && + !(ls == 2 && SP_IS_FESPOTLIGHT(child))) { + if(child) + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(child->getRepr()); + + if(ls != -1) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement(_light_source.get_active_data()->key.c_str()); + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + } + + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("New light source")); + update(); + } + + _locked = false; + } + } + + void update() + { + _box.hide(); + _box.show(); + _light_box.show_all(); + + SPFilterPrimitive* prim = _dialog._primitive_list.get_selected(); + if(prim && prim->firstChild()) + _settings.show_and_update(_light_source.get_active_data()->id, prim->firstChild()); + } + + FilterEffectsDialog& _dialog; + Gtk::VBox _box; + Settings _settings; + Gtk::HBox _light_box; + Gtk::Label _light_label; + ComboBoxEnum<LightSource> _light_source; + bool _locked; +}; + + // ComponentTransferValues +FilterEffectsDialog::ComponentTransferValues* FilterEffectsDialog::Settings::add_componenttransfervalues(const Glib::ustring& label, SPFeFuncNode::Channel channel) + { + ComponentTransferValues* ct = new ComponentTransferValues(_dialog, channel); + add_widget(ct, label); + add_attr_widget(ct); + return ct; + } + + +FilterEffectsDialog::LightSourceControl* FilterEffectsDialog::Settings::add_lightsource() +{ + LightSourceControl* ls = new LightSourceControl(_dialog); + add_attr_widget(ls); + add_widget(&ls->get_box(), ""); + return ls; +} + +static Gtk::Menu * create_popup_menu(Gtk::Widget& parent, + sigc::slot<void> dup, + sigc::slot<void> rem) +{ + auto menu = Gtk::manage(new Gtk::Menu); + + Gtk::MenuItem* mi = Gtk::manage(new Gtk::MenuItem(_("_Duplicate"),true)); + mi->signal_activate().connect(dup); + mi->show(); + menu->append(*mi); + + mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + menu->append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + menu->accelerate(parent); + + return menu; +} + +/*** FilterModifier ***/ +FilterEffectsDialog::FilterModifier::FilterModifier(FilterEffectsDialog& d) + : _desktop(nullptr), + _deskTrack(), + _dialog(d), + _add(_("_New"), true), + _observer(new Inkscape::XML::SignalObserver) +{ + Gtk::ScrolledWindow* sw = Gtk::manage(new Gtk::ScrolledWindow); + pack_start(*sw); + pack_start(_add, false, false); + sw->add(_list); + + _model = Gtk::ListStore::create(_columns); + _list.set_model(_model); + _cell_toggle.set_active(true); + const int selcol = _list.append_column("", _cell_toggle); + Gtk::TreeViewColumn* col = _list.get_column(selcol - 1); + if(col) + col->add_attribute(_cell_toggle.property_active(), _columns.sel); + _list.append_column_editable(_("_Filter"), _columns.label); + ((Gtk::CellRendererText*)_list.get_column(1)->get_first_cell())-> + signal_edited().connect(sigc::mem_fun(*this, &FilterEffectsDialog::FilterModifier::on_name_edited)); + + _list.append_column("#", _columns.count); + _list.get_column(2)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(2)->set_expand(false); + _list.get_column(2)->set_reorderable(true); + + sw->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _list.get_column(1)->set_resizable(true); + _list.get_column(1)->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + _list.get_column(1)->set_expand(true); + + _list.set_reorderable(true); + _list.enable_model_drag_dest (Gdk::ACTION_MOVE); + + _list.signal_drag_drop().connect( sigc::mem_fun(*this, &FilterModifier::on_filter_move), false ); + + sw->set_shadow_type(Gtk::SHADOW_IN); + show_all_children(); + _add.signal_clicked().connect(sigc::mem_fun(*this, &FilterModifier::add_filter)); + _cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &FilterModifier::on_selection_toggled)); + _list.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &FilterModifier::filter_list_button_release)); + + _menu = create_popup_menu(*this, + sigc::mem_fun(*this, &FilterModifier::duplicate_filter), + sigc::mem_fun(*this, &FilterModifier::remove_filter)); + + Gtk::MenuItem *item = Gtk::manage(new Gtk::MenuItem(_("R_ename"), true)); + item->signal_activate().connect(sigc::mem_fun(*this, &FilterModifier::rename_filter)); + item->show(); + _menu->append(*item); + _menu->accelerate(*this); + + _list.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FilterModifier::on_filter_selection_changed)); + _observer->signal_changed().connect(signal_filter_changed().make_slot()); + + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &FilterModifier::setTargetDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + update_filters(); +} + +FilterEffectsDialog::FilterModifier::~FilterModifier() +{ + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _resource_changed.disconnect(); + _doc_replaced.disconnect(); +} + +void FilterEffectsDialog::FilterModifier::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + if (_desktop) { + _selectChangedConn.disconnect(); + _selectModifiedConn.disconnect(); + _doc_replaced.disconnect(); + _resource_changed.disconnect(); + _dialog.setDesktop(nullptr); + } + _desktop = desktop; + if (desktop) { + if (desktop->selection) { + _selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FilterModifier::on_change_selection))); + _selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FilterModifier::on_modified_selection))); + } + _doc_replaced = desktop->connectDocumentReplaced( sigc::mem_fun(*this, &FilterModifier::on_document_replaced)); + _resource_changed = desktop->getDocument()->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + _dialog.setDesktop(desktop); + + update_filters(); + } + } +} + +// When the document changes, update connection to resources +void FilterEffectsDialog::FilterModifier::on_document_replaced(SPDesktop * /*desktop*/, SPDocument *document) +{ + if (_resource_changed) { + _resource_changed.disconnect(); + } + if (document) + { + _resource_changed = document->connectResourcesChanged("filter",sigc::mem_fun(*this, &FilterModifier::update_filters)); + } + + update_filters(); +} + +// When the selection changes, show the active filter(s) in the dialog +void FilterEffectsDialog::FilterModifier::on_change_selection() +{ + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + update_selection(selection); +} + +void FilterEffectsDialog::FilterModifier::on_modified_selection( guint flags ) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + on_change_selection(); + } +} + +// Update each filter's sel property based on the current object selection; +// If the filter is not used by any selected object, sel = 0, +// otherwise sel is set to the total number of filters in use by selected objects +// If only one filter is in use, it is selected +void FilterEffectsDialog::FilterModifier::update_selection(Selection *sel) +{ + if (!sel) { + return; + } + + std::set<SPObject*> used; + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPObject *obj = *i; + SPStyle *style = obj->style; + if (!style || !SP_IS_ITEM(obj)) { + continue; + } + + if (style->filter.set && style->getFilter()) { + SP_ITEM(obj)->bbox_valid = FALSE; + used.insert(style->getFilter()); + } else { + used.insert(nullptr); + } + } + + const int size = used.size(); + + for (Gtk::TreeIter iter = _model->children().begin(); iter != _model->children().end(); ++iter) { + if (used.find((*iter)[_columns.filter]) != used.end()) { + // If only one filter is in use by the selection, select it + if (size == 1) { + _list.get_selection()->select(iter); + } + (*iter)[_columns.sel] = size; + } else { + (*iter)[_columns.sel] = 0; + } + } + update_counts(); +} + +void FilterEffectsDialog::FilterModifier::on_filter_selection_changed() +{ + _observer->set(get_selected_filter()); + signal_filter_changed()(); +} + +void FilterEffectsDialog::FilterModifier::on_name_edited(const Glib::ustring& path, const Glib::ustring& text) +{ + Gtk::TreeModel::iterator iter = _model->get_iter(path); + + if(iter) { + SPFilter* filter = (*iter)[_columns.filter]; + filter->setLabel(text.c_str()); + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Rename filter")); + if(iter) + (*iter)[_columns.label] = text; + } +} + +bool FilterEffectsDialog::FilterModifier::on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int /*x*/, int /*y*/, guint /*time*/) { + +//const Gtk::TreeModel::Path& /*path*/) { +/* The code below is bugged. Use of "object->getRepr()->setPosition(0)" is dangerous! + Writing back the reordered list to XML (reordering XML nodes) should be implemented differently. + Note that the dialog does also not update its list of filters when the order is manually changed + using the XML dialog + for(Gtk::TreeModel::iterator i = _model->children().begin(); i != _model->children().end(); ++i) { + SPObject* object = (*i)[_columns.filter]; + if(object && object->getRepr()) ; + object->getRepr()->setPosition(0); + } +*/ + return false; +} + +void FilterEffectsDialog::FilterModifier::on_selection_toggled(const Glib::ustring& path) +{ + Gtk::TreeIter iter = _model->get_iter(path); + + if(iter) { + SPDesktop *desktop = _dialog.getDesktop(); + SPDocument *doc = desktop->getDocument(); + SPFilter* filter = (*iter)[_columns.filter]; + Inkscape::Selection *sel = desktop->getSelection(); + + /* If this filter is the only one used in the selection, unset it */ + if((*iter)[_columns.sel] == 1) + filter = nullptr; + + auto itemlist= sel->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + SPItem * item = *i; + SPStyle *style = item->style; + g_assert(style != nullptr); + + if (filter) { + sp_style_set_property_url(item, "filter", filter, false); + } else { + ::remove_filter(item, false); + } + + item->requestDisplayUpdate((SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG )); + } + + update_selection(sel); + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Apply filter")); + } +} + + +void FilterEffectsDialog::FilterModifier::update_counts() +{ + for(const auto & i : _model->children()) { + SPFilter* f = SP_FILTER(i[_columns.filter]); + i[_columns.count] = f->getRefCount(); + } +} + +/* Add all filters in the document to the combobox. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::FilterModifier::update_filters() +{ + SPDesktop* desktop = _dialog.getDesktop(); + SPDocument* document = desktop->getDocument(); + + // Workaround for 1.0, not needed in 1.1 (which properly disconnects signals) + if (!document) { + return; + } + + std::vector<SPObject *> filters = document->getResourceList( "filter" ); + + _model->clear(); + + for (auto filter : filters) { + Gtk::TreeModel::Row row = *_model->append(); + SPFilter* f = SP_FILTER(filter); + row[_columns.filter] = f; + const gchar* lbl = f->label(); + const gchar* id = f->getId(); + row[_columns.label] = lbl ? lbl : (id ? id : "filter"); + } + + update_selection(desktop->selection); + _dialog.update_filter_general_settings_view(); +} + +SPFilter* FilterEffectsDialog::FilterModifier::get_selected_filter() +{ + if(_list.get_selection()) { + Gtk::TreeModel::iterator i = _list.get_selection()->get_selected(); + + if(i) + return (*i)[_columns.filter]; + } + + return nullptr; +} + +void FilterEffectsDialog::FilterModifier::select_filter(const SPFilter* filter) +{ + if(filter) { + for(Gtk::TreeModel::iterator i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.filter] == filter) { + _list.get_selection()->select(i); + break; + } + } + } +} + +void FilterEffectsDialog::FilterModifier::filter_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + const bool sensitive = get_selected_filter() != nullptr; + auto items = _menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + + _menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void FilterEffectsDialog::FilterModifier::add_filter() +{ + SPDocument* doc = _dialog.getDesktop()->getDocument(); + SPFilter* filter = new_filter(doc); + + const int count = _model->children().size(); + std::ostringstream os; + os << _("filter") << count; + filter->setLabel(os.str().c_str()); + + update_filters(); + + select_filter(filter); + + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter")); +} + +void FilterEffectsDialog::FilterModifier::remove_filter() +{ + SPFilter *filter = get_selected_filter(); + + if(filter) { + SPDocument* doc = filter->document; + + // Delete all references to this filter + std::vector<SPItem*> x,y; + std::vector<SPItem*> all = get_all_items(x, _desktop->currentRoot(), _desktop, false, false, true, y); + for(std::vector<SPItem*>::const_iterator i=all.begin(); all.end() != i; ++i) { + if (!SP_IS_ITEM(*i)) { + continue; + } + SPItem *item = *i; + if (!item->style) { + continue; + } + + const SPIFilter *ifilter = &(item->style->filter); + if (ifilter && ifilter->href) { + const SPObject *obj = ifilter->href->getObject(); + if (obj && obj == (SPObject *)filter) { + ::remove_filter(item, false); + } + } + } + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(filter->getRepr()); + + DocumentUndo::done(doc, SP_VERB_DIALOG_FILTER_EFFECTS, _("Remove filter")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::duplicate_filter() +{ + SPFilter* filter = get_selected_filter(); + + if (filter) { + Inkscape::XML::Node *repr = filter->getRepr(); + Inkscape::XML::Node *parent = repr->parent(); + repr = repr->duplicate(repr->document()); + parent->appendChild(repr); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter")); + + update_filters(); + } +} + +void FilterEffectsDialog::FilterModifier::rename_filter() +{ + _list.set_cursor(_model->get_path(_list.get_selection()->get_selected()), *_list.get_column(1), true); +} + +FilterEffectsDialog::CellRendererConnection::CellRendererConnection() + : Glib::ObjectBase(typeid(CellRendererConnection)), + _primitive(*this, "primitive", nullptr), + _text_width(0) +{} + +Glib::PropertyProxy<void*> FilterEffectsDialog::CellRendererConnection::property_primitive() +{ + return _primitive.get_proxy(); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const +{ + PrimitiveList& primlist = dynamic_cast<PrimitiveList&>(widget); + minimum_width = natural_width = size * primlist.primitive_count() + primlist.get_input_type_width() * 6; +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int /* height */, + int& minimum_width, + int& natural_width) const +{ + get_preferred_width(widget, minimum_width, natural_width); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const +{ + // Scale the height depending on the number of inputs, unless it's + // the first primitive, in which case there are no connections + SPFilterPrimitive* prim = SP_FILTER_PRIMITIVE(_primitive.get_value()); + minimum_height = natural_height = size * input_count(prim); +} + +void FilterEffectsDialog::CellRendererConnection::get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int /* width */, + int& minimum_height, + int& natural_height) const +{ + get_preferred_height(widget, minimum_height, natural_height); +} + +/*** PrimitiveList ***/ +FilterEffectsDialog::PrimitiveList::PrimitiveList(FilterEffectsDialog& d) + : _dialog(d), + _in_drag(0), + _observer(new Inkscape::XML::SignalObserver) +{ + signal_draw().connect(sigc::mem_fun(*this, &PrimitiveList::on_draw_signal)); + + add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK); + + _model = Gtk::ListStore::create(_columns); + + set_reorderable(true); + + set_model(_model); + append_column(_("_Effect"), _columns.type); + get_column(0)->set_resizable(true); + set_headers_visible(); + + _observer->signal_changed().connect(signal_primitive_changed().make_slot()); + get_selection()->signal_changed().connect(sigc::mem_fun(*this, &PrimitiveList::on_primitive_selection_changed)); + signal_primitive_changed().connect(sigc::mem_fun(*this, &PrimitiveList::queue_draw)); + + init_text(); + + int cols_count = append_column(_("Connections"), _connection_cell); + Gtk::TreeViewColumn* col = get_column(cols_count - 1); + if(col) + col->add_attribute(_connection_cell.property_primitive(), _columns.primitive); +} + +// Sets up a vertical Pango context/layout, and returns the largest +// width needed to render the FilterPrimitiveInput labels. +void FilterEffectsDialog::PrimitiveList::init_text() +{ + // Set up a vertical context+layout + Glib::RefPtr<Pango::Context> context = create_pango_context(); + const Pango::Matrix matrix = {0, -1, 1, 0, 0, 0}; + context->set_matrix(matrix); + _vertical_layout = Pango::Layout::create(context); + + // Store the maximum height and width of the an input type label + // for later use in drawing and measuring. + _input_type_height = _input_type_width = 0; + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + int fontw, fonth; + _vertical_layout->get_pixel_size(fontw, fonth); + if(fonth > _input_type_width) + _input_type_width = fonth; + if (fontw > _input_type_height) + _input_type_height = fontw; + } +} + +sigc::signal<void>& FilterEffectsDialog::PrimitiveList::signal_primitive_changed() +{ + return _signal_primitive_changed; +} + +void FilterEffectsDialog::PrimitiveList::on_primitive_selection_changed() +{ + _observer->set(get_selected()); + signal_primitive_changed()(); + _dialog._color_matrix_values->clear_store(); +} + +/* Add all filter primitives in the current to the list. + Keeps the same selection if possible, otherwise selects the first element */ +void FilterEffectsDialog::PrimitiveList::update() +{ + SPFilter* f = _dialog._filter_modifier.get_selected_filter(); + const SPFilterPrimitive* active_prim = get_selected(); + _model->clear(); + + if(f) { + bool active_found = false; + _dialog._primitive_box->set_sensitive(true); + _dialog.update_filter_general_settings_view(); + for(auto& prim_obj: f->children) { + SPFilterPrimitive *prim = SP_FILTER_PRIMITIVE(&prim_obj); + if(!prim) { + break; + } + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.primitive] = prim; + + //XML Tree being used directly here while it shouldn't be. + row[_columns.type_id] = FPConverter.get_id_from_key(prim->getRepr()->name()); + row[_columns.type] = _(FPConverter.get_label(row[_columns.type_id]).c_str()); + + if (prim->getId()) { + row[_columns.id] = Glib::ustring(prim->getId()); + } + + if(prim == active_prim) { + get_selection()->select(row); + active_found = true; + } + } + + if(!active_found && _model->children().begin()) + get_selection()->select(_model->children().begin()); + + columns_autosize(); + + int width, height; + get_size_request(width, height); + if (height == -1) { + // Need to account for the height of the input type text (rotated text) as well as the + // column headers. Input type text height determined in init_text() by measuring longest + // string. Column header height determined by mapping y coordinate of visible + // rectangle to widget coordinates. + Gdk::Rectangle vis; + int vis_x, vis_y; + get_visible_rect(vis); + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + set_size_request(width, _input_type_height + 2 + vis_y); + } + } + else { + _dialog._primitive_box->set_sensitive(false); + set_size_request(-1, -1); + } +} + +void FilterEffectsDialog::PrimitiveList::set_menu(Gtk::Widget& parent, + sigc::slot<void> dup, + sigc::slot<void> rem) +{ + _primitive_menu = create_popup_menu(parent, dup, rem); +} + +SPFilterPrimitive* FilterEffectsDialog::PrimitiveList::get_selected() +{ + if(_dialog._filter_modifier.get_selected_filter()) { + Gtk::TreeModel::iterator i = get_selection()->get_selected(); + if(i) + return (*i)[_columns.primitive]; + } + + return nullptr; +} + +void FilterEffectsDialog::PrimitiveList::select(SPFilterPrimitive* prim) +{ + for(Gtk::TreeIter i = _model->children().begin(); + i != _model->children().end(); ++i) { + if((*i)[_columns.primitive] == prim) + get_selection()->select(i); + } +} + +void FilterEffectsDialog::PrimitiveList::remove_selected() +{ + SPFilterPrimitive* prim = get_selected(); + + if(prim) { + _observer->set(nullptr); + _model->erase(get_selection()->get_selected()); + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(prim->getRepr()); + + DocumentUndo::done(_dialog.getDesktop()->getDocument(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove filter primitive")); + + update(); + } +} + +bool FilterEffectsDialog::PrimitiveList::on_draw_signal(const Cairo::RefPtr<Cairo::Context> & cr) +{ + cr->set_line_width(1.0); + // In GTK+ 3, the draw function receives the widget window, not the + // bin_window (i.e., just the area under the column headers). We + // therefore translate the origin of our coordinate system to account for this + int x_origin, y_origin; + convert_bin_window_to_widget_coords(0,0,x_origin,y_origin); + cr->translate(x_origin, y_origin); + + auto sc = gtk_widget_get_style_context(GTK_WIDGET(gobj())); + GdkRGBA bg_color, fg_color; + gtk_style_context_get_color(sc, GTK_STATE_FLAG_NORMAL, &fg_color); + gtk_style_context_get_background_color(sc, GTK_STATE_FLAG_NORMAL, &bg_color); + GdkRGBA orig_color {fg_color}; + + auto lerp = [](double v0, double v1, double t){ return (1.0 - t) * v0 + t * v1; }; + fg_color.red = lerp(bg_color.red, orig_color.red, 0.95); + fg_color.green = lerp(bg_color.green, orig_color.green, 0.95); + fg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.95); + bg_color.red = lerp(bg_color.red, orig_color.red, 0.05); + bg_color.green = lerp(bg_color.green, orig_color.green, 0.05); + bg_color.blue = lerp(bg_color.blue, orig_color.blue, 0.05); + GdkRGBA mid_color { + lerp(bg_color.red, fg_color.red, 0.65), + lerp(bg_color.green, fg_color.green, 0.65), + lerp(bg_color.blue, fg_color.blue, 0.65), + fg_color.alpha}; + + SPFilterPrimitive* prim = get_selected(); + int row_count = get_model()->children().size(); + + int fheight = CellRendererConnection::size; + Gdk::Rectangle rct, vis; + Gtk::TreeIter row = get_model()->children().begin(); + int text_start_x = 0; + if(row) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + get_visible_rect(vis); + text_start_x = rct.get_x() + rct.get_width() - get_input_type_width() * FPInputConverter._length + 1; + + for(unsigned int i = 0; i < FPInputConverter._length; ++i) { + _vertical_layout->set_text(_(FPInputConverter.get_label((FilterPrimitiveInput)i).c_str())); + const int x = text_start_x + get_input_type_width() * i; + cr->save(); + gdk_cairo_set_source_rgba(cr->cobj(), &bg_color); + cr->rectangle(x, 0, get_input_type_width(), vis.get_height()); + cr->fill_preserve(); + + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + cr->move_to(x + get_input_type_width(), 5); + cr->rotate_degrees(90); + _vertical_layout->show_in_cairo_context(cr); + + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + cr->move_to(x, 0); + cr->line_to(x, vis.get_height()); + cr->stroke(); + cr->restore(); + } + cr->rectangle(vis.get_x(), 0, vis.get_width(), vis.get_height()); + cairo_clip(cr->cobj()); + } + + int row_index = 0; + for(; row != get_model()->children().end(); ++row, ++row_index) { + get_cell_area(get_model()->get_path(row), *get_column(1), rct); + const int x = rct.get_x(), y = rct.get_y(), h = rct.get_height(); + + // Check mouse state + int mx, my; + Gdk::ModifierType mask; + + auto display = get_bin_window()->get_display(); + auto seat = display->get_default_seat(); + auto device = seat->get_pointer(); + cairo_set_line_width (cr->cobj(),0.5); + get_bin_window()->get_device_position(device, mx, my, mask); + + // Outline the bottom of the connection area + const int outline_x = x + fheight * (row_count - row_index); + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + + cr->move_to(vis.get_x(), y + h); + cr->line_to(outline_x, y + h); + // Side outline + cr->line_to(outline_x, y - 1); + + cr->stroke(); + cr->restore(); + + std::vector<Gdk::Point> con_poly; + int con_drag_y = 0; + int con_drag_x = 0; + bool inside; + const SPFilterPrimitive* row_prim = (*row)[_columns.primitive]; + const int inputs = input_count(row_prim); + + if(SP_IS_FEMERGE(row_prim)) { + for(int i = 0; i < inputs; ++i) { + inside = do_connection_node(row, i, con_poly, mx, my); + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + if(_in_drag == (i + 1)) + { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + if(_in_drag != (i + 1) || row_prim != prim) + { + draw_connection(cr, row, i, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + } + } + else { + // Draw "in" shape + inside = do_connection_node(row, 0, con_poly, mx, my); + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in" connection + if(_in_drag != 1 || row_prim != prim) + { + draw_connection(cr, row, SP_ATTR_IN, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + + if(inputs == 2) { + // Draw "in2" shape + inside = do_connection_node(row, 1, con_poly, mx, my); + if(_in_drag == 2) + { + con_drag_y = con_poly[2].get_y(); + con_drag_x = con_poly[2].get_x(); + } + + cr->save(); + + gdk_cairo_set_source_rgba(cr->cobj(), inside ? &mid_color : &fg_color); + + draw_connection_node(cr, con_poly, inside); + + cr->restore(); + + // Draw "in2" connection + if(_in_drag != 2 || row_prim != prim) + { + draw_connection(cr, row, SP_ATTR_IN2, text_start_x, outline_x, con_poly[2].get_y(), row_count, fg_color, mid_color); + } + } + } + + // Draw drag connection + if(row_prim == prim && _in_drag) { + cr->save(); + gdk_cairo_set_source_rgba(cr->cobj(), &orig_color); + cr->move_to(con_drag_x, con_drag_y); + cr->line_to(mx, con_drag_y); + cr->line_to(mx, my); + cr->stroke(); + cr->restore(); + } + } + + return true; +} + +void FilterEffectsDialog::PrimitiveList::draw_connection(const Cairo::RefPtr<Cairo::Context>& cr, + const Gtk::TreeIter& input, const int attr, + const int text_start_x, const int x1, const int y1, + const int row_count, const GdkRGBA fg_color, const GdkRGBA mid_color) +{ + cr->save(); + + int src_id = 0; + Gtk::TreeIter res = find_result(input, attr, src_id); + + const bool is_first = input == get_model()->children().begin(); + const bool is_merge = SP_IS_FEMERGE((SPFilterPrimitive*)(*input)[_columns.primitive]); + const bool use_default = !res && !is_merge; + + if(res == input || (use_default && is_first)) { + // Draw straight connection to a standard input + // Draw a lighter line for an implicit connection to a standard input + const int tw = get_input_type_width(); + gint end_x = text_start_x + tw * src_id + (int)(tw * 0.5f) + 1; + + if(use_default && is_first) + gdk_cairo_set_source_rgba(cr->cobj(), &mid_color); + else + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + + cr->rectangle(end_x-2, y1-2, 5, 5); + cr->fill_preserve(); + cr->move_to(x1, y1); + cr->line_to(end_x, y1); + cr->stroke(); + } + else { + // Draw an 'L'-shaped connection to another filter primitive + // If no connection is specified, draw a light connection to the previous primitive + if(use_default) { + res = input; + --res; + } + + if(res) { + Gdk::Rectangle rct; + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(get_model()->get_path(res), *get_column(1), rct); + const int row_index = find_index(res); + const int x2 = rct.get_x() + fheight * (row_count - row_index) - fheight / 2; + const int y2 = rct.get_y() + rct.get_height(); + + // Draw a bevelled 'L'-shaped connection + gdk_cairo_set_source_rgba(cr->cobj(), &fg_color); + cr->move_to(x1, y1); + cr->line_to(x2-fheight/4, y1); + cr->line_to(x2, y1-fheight/4); + cr->line_to(x2, y2); + cr->stroke(); + } + } + cr->restore(); +} + +// Draw the triangular outline of the connection node, and fill it +// if desired +void FilterEffectsDialog::PrimitiveList::draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr, + const std::vector<Gdk::Point>& points, + const bool fill) +{ + cr->save(); + cr->move_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + cr->line_to(points[1].get_x()+0.5, points[1].get_y()+0.5); + cr->line_to(points[2].get_x()+0.5, points[2].get_y()+0.5); + cr->line_to(points[0].get_x()+0.5, points[0].get_y()+0.5); + + if(fill) cr->fill(); + else cr->stroke(); + + cr->restore(); +} + +// Creates a triangle outline of the connection node and returns true if (x,y) is inside the node +bool FilterEffectsDialog::PrimitiveList::do_connection_node(const Gtk::TreeIter& row, const int input, + std::vector<Gdk::Point>& points, + const int ix, const int iy) +{ + Gdk::Rectangle rct; + const int icnt = input_count((*row)[_columns.primitive]); + + get_cell_area(get_model()->get_path(_model->children().begin()), *get_column(1), rct); + const int fheight = CellRendererConnection::size; + + get_cell_area(_model->get_path(row), *get_column(1), rct); + const float h = rct.get_height() / icnt; + + const int x = rct.get_x() + fheight * (_model->children().size() - find_index(row)); + const int con_w = (int)(fheight * 0.35f); + const int con_y = (int)(rct.get_y() + (h / 2) - con_w + (input * h)); + points.clear(); + points.emplace_back(x, con_y); + points.emplace_back(x, con_y + con_w * 2); + points.emplace_back(x - con_w, con_y + con_w); + + return ix >= x - h && iy >= con_y && ix <= x && iy <= points[1].get_y(); +} + +const Gtk::TreeIter FilterEffectsDialog::PrimitiveList::find_result(const Gtk::TreeIter& start, + const int attr, int& src_id) +{ + SPFilterPrimitive* prim = (*start)[_columns.primitive]; + Gtk::TreeIter target = _model->children().end(); + int image = 0; + + if(SP_IS_FEMERGE(prim)) { + int c = 0; + bool found = false; + for (auto& o: prim->children) { + if(c == attr && SP_IS_FEMERGENODE(&o)) { + image = SP_FEMERGENODE(&o)->input; + found = true; + } + ++c; + } + if(!found) + return target; + } + else { + if(attr == SP_ATTR_IN) + image = prim->image_in; + else if(attr == SP_ATTR_IN2) { + if(SP_IS_FEBLEND(prim)) + image = SP_FEBLEND(prim)->in2; + else if(SP_IS_FECOMPOSITE(prim)) + image = SP_FECOMPOSITE(prim)->in2; + else if(SP_IS_FEDISPLACEMENTMAP(prim)) + image = SP_FEDISPLACEMENTMAP(prim)->in2; + else + return target; + } + else + return target; + } + + if(image >= 0) { + for(Gtk::TreeIter i = _model->children().begin(); + i != start; ++i) { + if(((SPFilterPrimitive*)(*i)[_columns.primitive])->image_out == image) + target = i; + } + return target; + } + else if(image < -1) { + src_id = -(image + 2); + return start; + } + + return target; +} + +int FilterEffectsDialog::PrimitiveList::find_index(const Gtk::TreeIter& target) +{ + int i = 0; + for(Gtk::TreeIter iter = _model->children().begin(); + iter != target; ++iter, ++i){}; + return i; +} + +bool FilterEffectsDialog::PrimitiveList::on_button_press_event(GdkEventButton* e) +{ + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + const int x = (int)e->x, y = (int)e->y; + int cx, cy; + + _drag_prim = nullptr; + + if(get_path_at_pos(x, y, path, col, cx, cy)) { + Gtk::TreeIter iter = _model->get_iter(path); + std::vector<Gdk::Point> points; + + _drag_prim = (*iter)[_columns.primitive]; + const int icnt = input_count(_drag_prim); + + for(int i = 0; i < icnt; ++i) { + if(do_connection_node(_model->get_iter(path), i, points, x, y)) { + _in_drag = i + 1; + break; + } + } + + queue_draw(); + } + + if(_in_drag) { + _scroll_connection = Glib::signal_timeout().connect(sigc::mem_fun(*this, &PrimitiveList::on_scroll_timeout), 150); + _autoscroll_x = 0; + _autoscroll_y = 0; + get_selection()->select(path); + return true; + } + else + return Gtk::TreeView::on_button_press_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_motion_notify_event(GdkEventMotion* e) +{ + const int speed = 10; + const int limit = 15; + + Gdk::Rectangle vis; + get_visible_rect(vis); + int vis_x, vis_y; + + int vis_x2, vis_y2; + convert_widget_to_tree_coords(vis.get_x(), vis.get_y(), vis_x2, vis_y2); + + convert_tree_to_widget_coords(vis.get_x(), vis.get_y(), vis_x, vis_y); + const int top = vis_y + vis.get_height(); + const int right_edge = vis_x + vis.get_width(); + + // When autoscrolling during a connection drag, set the speed based on + // where the mouse is in relation to the edges. + if(e->y < vis_y) + _autoscroll_y = -(int)(speed + (vis_y - e->y) / 5); + else if(e->y < vis_y + limit) + _autoscroll_y = -speed; + else if(e->y > top) + _autoscroll_y = (int)(speed + (e->y - top) / 5); + else if(e->y > top - limit) + _autoscroll_y = speed; + else + _autoscroll_y = 0; + + double e2 = ( e->x - vis_x2/2); + // horizontal scrolling + if(e2 < vis_x) + _autoscroll_x = -(int)(speed + (vis_x - e2) / 5); + else if(e2 < vis_x + limit) + _autoscroll_x = -speed; + else if(e2 > right_edge) + _autoscroll_x = (int)(speed + (e2 - right_edge) / 5); + else if(e2 > right_edge - limit) + _autoscroll_x = speed; + else + _autoscroll_x = 0; + + + + queue_draw(); + + return Gtk::TreeView::on_motion_notify_event(e); +} + +bool FilterEffectsDialog::PrimitiveList::on_button_release_event(GdkEventButton* e) +{ + SPFilterPrimitive *prim = get_selected(), *target; + + _scroll_connection.disconnect(); + + if(_in_drag && prim) { + Gtk::TreePath path; + Gtk::TreeViewColumn* col; + int cx, cy; + + if(get_path_at_pos((int)e->x, (int)e->y, path, col, cx, cy)) { + const gchar *in_val = nullptr; + Glib::ustring result; + Gtk::TreeIter target_iter = _model->get_iter(path); + target = (*target_iter)[_columns.primitive]; + col = get_column(1); + + Gdk::Rectangle rct; + get_cell_area(path, *col, rct); + const int twidth = get_input_type_width(); + const int sources_x = rct.get_width() - twidth * FPInputConverter._length; + if(cx > sources_x) { + int src = (cx - sources_x) / twidth; + if (src < 0) { + src = 0; + } else if(src >= static_cast<int>(FPInputConverter._length)) { + src = FPInputConverter._length - 1; + } + result = FPInputConverter.get_key((FilterPrimitiveInput)src); + in_val = result.c_str(); + } + else { + // Ensure that the target comes before the selected primitive + for(Gtk::TreeIter iter = _model->children().begin(); + iter != get_selection()->get_selected(); ++iter) { + if(iter == target_iter) { + Inkscape::XML::Node *repr = target->getRepr(); + // Make sure the target has a result + const gchar *gres = repr->attribute("result"); + if(!gres) { + result = SP_FILTER(prim->parent)->get_new_result_name(); + repr->setAttributeOrRemoveIfEmpty("result", result); + in_val = result.c_str(); + } + else + in_val = gres; + break; + } + } + } + + if(SP_IS_FEMERGE(prim)) { + int c = 1; + bool handled = false; + for (auto& o: prim->children) { + if(c == _in_drag && SP_IS_FEMERGENODE(&o)) { + // If input is null, delete it + if(!in_val) { + + //XML Tree being used directly here while it shouldn't be. + sp_repr_unparent(o.getRepr()); + DocumentUndo::done(prim->document, SP_VERB_DIALOG_FILTER_EFFECTS, + _("Remove merge node")); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } else { + _dialog.set_attr(&o, SP_ATTR_IN, in_val); + } + handled = true; + break; + } + ++c; + } + // Add new input? + if(!handled && c == _in_drag && in_val) { + Inkscape::XML::Document *xml_doc = prim->document->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:feMergeNode"); + repr->setAttribute("inkscape:collect", "always"); + + //XML Tree being used directly here while it shouldn't be. + prim->getRepr()->appendChild(repr); + SPFeMergeNode *node = SP_FEMERGENODE(prim->document->getObjectByRepr(repr)); + Inkscape::GC::release(repr); + _dialog.set_attr(node, SP_ATTR_IN, in_val); + (*get_selection()->get_selected())[_columns.primitive] = prim; + } + } + else { + if(_in_drag == 1) + _dialog.set_attr(prim, SP_ATTR_IN, in_val); + else if(_in_drag == 2) + _dialog.set_attr(prim, SP_ATTR_IN2, in_val); + } + } + + _in_drag = 0; + queue_draw(); + + _dialog.update_settings_view(); + } + + if((e->type == GDK_BUTTON_RELEASE) && (e->button == 3)) { + const bool sensitive = get_selected() != nullptr; + auto items = _primitive_menu->get_children(); + items[0]->set_sensitive(sensitive); + items[1]->set_sensitive(sensitive); + + _primitive_menu->popup_at_pointer(reinterpret_cast<GdkEvent *>(e)); + + return true; + } + else + return Gtk::TreeView::on_button_release_event(e); +} + +// Checks all of prim's inputs, removes any that use result +static void check_single_connection(SPFilterPrimitive* prim, const int result) +{ + if (prim && (result >= 0)) { + if (prim->image_in == result) { + prim->removeAttribute("in"); + } + + if (SP_IS_FEBLEND(prim)) { + if (SP_FEBLEND(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FECOMPOSITE(prim)) { + if (SP_FECOMPOSITE(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } else if (SP_IS_FEDISPLACEMENTMAP(prim)) { + if (SP_FEDISPLACEMENTMAP(prim)->in2 == result) { + prim->removeAttribute("in2"); + } + } + } +} + +// Remove any connections going to/from prim_iter that forward-reference other primitives +void FilterEffectsDialog::PrimitiveList::sanitize_connections(const Gtk::TreeIter& prim_iter) +{ + SPFilterPrimitive *prim = (*prim_iter)[_columns.primitive]; + bool before = true; + + for(Gtk::TreeIter iter = _model->children().begin(); + iter != _model->children().end(); ++iter) { + if(iter == prim_iter) + before = false; + else { + SPFilterPrimitive* cur_prim = (*iter)[_columns.primitive]; + if(before) + check_single_connection(cur_prim, prim->image_out); + else + check_single_connection(prim, cur_prim->image_out); + } + } +} + +// Reorder the filter primitives to match the list order +void FilterEffectsDialog::PrimitiveList::on_drag_end(const Glib::RefPtr<Gdk::DragContext>& /*dc*/) +{ + SPFilter* filter = _dialog._filter_modifier.get_selected_filter(); + int ndx = 0; + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + prim->getRepr()->setPosition(ndx); + break; + } + } + + for (Gtk::TreeModel::iterator iter = _model->children().begin(); + iter != _model->children().end(); ++iter, ++ndx) { + SPFilterPrimitive* prim = (*iter)[_columns.primitive]; + if (prim && prim == _drag_prim) { + sanitize_connections(iter); + get_selection()->select(iter); + break; + } + } + + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Reorder filter primitive")); +} + +// If a connection is dragged towards the top or bottom of the list, the list should scroll to follow. +bool FilterEffectsDialog::PrimitiveList::on_scroll_timeout() +{ + if(_autoscroll_y) { + auto a = dynamic_cast<Gtk::ScrolledWindow*>(get_parent())->get_vadjustment(); + double v = a->get_value() + _autoscroll_y; + + if(v < 0) + v = 0; + if(v > a->get_upper() - a->get_page_size()) + v = a->get_upper() - a->get_page_size(); + + a->set_value(v); + + queue_draw(); + } + + + if(_autoscroll_x) { + auto a_h = dynamic_cast<Gtk::ScrolledWindow*>(get_parent())->get_hadjustment(); + double h = a_h->get_value() + _autoscroll_x; + + if(h < 0) + h = 0; + if(h > a_h->get_upper() - a_h->get_page_size()) + h = a_h->get_upper() - a_h->get_page_size(); + + a_h->set_value(h); + + queue_draw(); + } + + return true; +} + +int FilterEffectsDialog::PrimitiveList::primitive_count() const +{ + return _model->children().size(); +} + +int FilterEffectsDialog::PrimitiveList::get_input_type_width() const +{ + // Maximum font height calculated in initText() and stored in _input_type_width. + // Add 2 to font height to account for rectangle around text. + return _input_type_width + 2; +} + +/*** FilterEffectsDialog ***/ + +FilterEffectsDialog::FilterEffectsDialog() + : UI::Widget::Panel("/dialogs/filtereffects", SP_VERB_DIALOG_FILTER_EFFECTS), + _add_primitive_type(FPConverter), + _add_primitive(_("Add Effect:")), + _empty_settings(_("No effect selected"), Gtk::ALIGN_START), + _no_filter_selected(_("No filter selected"), Gtk::ALIGN_START), + _settings_initialized(false), + _locked(false), + _attr_lock(false), + _filter_modifier(*this), + _primitive_list(*this) +{ + _settings = new Settings(*this, _settings_tab1, sigc::mem_fun(*this, &FilterEffectsDialog::set_attr_direct), + NR_FILTER_ENDPRIMITIVETYPE); + _filter_general_settings = new Settings(*this, _settings_tab2, sigc::mem_fun(*this, &FilterEffectsDialog::set_filternode_attr), + 1); + + // Initialize widget hierarchy + auto hpaned = Gtk::manage(new Gtk::Paned()); + _primitive_box = Gtk::manage(new Gtk::Paned(Gtk::ORIENTATION_VERTICAL)); + + _sw_infobox = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::ScrolledWindow* sw_prims = Gtk::manage(new Gtk::ScrolledWindow); + Gtk::HBox* infobox = Gtk::manage(new Gtk::HBox(/*homogeneous:*/false, /*spacing:*/4)); + Gtk::HBox* hb_prims = Gtk::manage(new Gtk::HBox); + Gtk::VBox* vb_prims = Gtk::manage(new Gtk::VBox); + Gtk::VBox* vb_desc = Gtk::manage(new Gtk::VBox); + + Gtk::VBox* prim_vbox_p = Gtk::manage(new Gtk::VBox); + Gtk::VBox* prim_vbox_i = Gtk::manage(new Gtk::VBox); + + sw_prims->add(_primitive_list); + + prim_vbox_p->pack_start(*sw_prims, true, true); + prim_vbox_i->pack_start(*vb_prims, true, true); + + _primitive_box->pack1(*prim_vbox_p); + _primitive_box->pack2(*prim_vbox_i, false, false); + + hpaned->pack1(_filter_modifier); + hpaned->pack2(*_primitive_box); + _getContents()->add(*hpaned); + + _infobox_icon.set_halign(Gtk::ALIGN_START); + _infobox_icon.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_halign(Gtk::ALIGN_START); + _infobox_desc.set_valign(Gtk::ALIGN_START); + _infobox_desc.set_justify(Gtk::JUSTIFY_LEFT); + _infobox_desc.set_line_wrap(true); + _infobox_desc.set_size_request(300, -1); + + vb_desc->pack_start(_infobox_desc, true, true); + + infobox->pack_start(_infobox_icon, false, false); + infobox->pack_start(*vb_desc, true, true); + + //_sw_infobox->set_size_request(-1, -1); + _sw_infobox->set_size_request(300, -1); + _sw_infobox->add(*infobox); + + //vb_prims->set_size_request(-1, 50); + vb_prims->pack_start(*hb_prims, false, false); + vb_prims->pack_start(*_sw_infobox, true, true); + + hb_prims->pack_start(_add_primitive, false, false); + hb_prims->pack_start(_add_primitive_type, true, true); + _getContents()->pack_start(_settings_tabs, false, false); + _settings_tabs.append_page(_settings_tab1, _("Effect parameters")); + _settings_tabs.append_page(_settings_tab2, _("Filter General Settings")); + + _primitive_list.signal_primitive_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_settings_view)); + _filter_modifier.signal_filter_changed().connect( + sigc::mem_fun(_primitive_list, &PrimitiveList::update)); + + _add_primitive_type.signal_changed().connect( + sigc::mem_fun(*this, &FilterEffectsDialog::update_primitive_infobox)); + + sw_prims->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + sw_prims->set_shadow_type(Gtk::SHADOW_IN); + _sw_infobox->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + +// al_settings->set_padding(0, 0, 12, 0); +// fr_settings->set_shadow_type(Gtk::SHADOW_NONE); +// ((Gtk::Label*)fr_settings->get_label_widget())->set_use_markup(); + _add_primitive.signal_clicked().connect(sigc::mem_fun(*this, &FilterEffectsDialog::add_primitive)); + _primitive_list.set_menu(*this, sigc::mem_fun(*this, &FilterEffectsDialog::duplicate_primitive), + sigc::mem_fun(_primitive_list, &PrimitiveList::remove_selected)); + + show_all_children(); + init_settings_widgets(); + _primitive_list.update(); + update_primitive_infobox(); +} + +FilterEffectsDialog::~FilterEffectsDialog() +{ + delete _settings; + delete _filter_general_settings; +} + +void FilterEffectsDialog::set_attrs_locked(const bool l) +{ + _locked = l; +} + +void FilterEffectsDialog::show_all_vfunc() +{ + UI::Widget::Panel::show_all_vfunc(); + + update_settings_view(); +} + +void FilterEffectsDialog::init_settings_widgets() +{ + // TODO: Find better range/climb-rate/digits values for the SpinScales, + // most of the current values are complete guesses! + + _settings_tab1.set_border_width(4); + _settings_tab2.set_border_width(4); + + _empty_settings.set_sensitive(false); + _settings_tab1.pack_start(_empty_settings); + + _no_filter_selected.set_sensitive(false); + _settings_tab2.pack_start(_no_filter_selected); + _settings_initialized = true; + + _filter_general_settings->type(0); + _filter_general_settings->add_multispinbutton(/*default x:*/ (double) -0.1, /*default y:*/ (double) -0.1, SP_ATTR_X, SP_ATTR_Y, _("Coordinates:"), -100, 100, 0.01, 0.1, 2, _("X coordinate of the left corners of filter effects region"), _("Y coordinate of the upper corners of filter effects region")); + _filter_general_settings->add_multispinbutton(/*default width:*/ (double) 1.2, /*default height:*/ (double) 1.2, SP_ATTR_WIDTH, SP_ATTR_HEIGHT, _("Dimensions:"), 0, 1000, 0.01, 0.1, 2, _("Width of filter effects region"), _("Height of filter effects region")); + + _settings->type(NR_FILTER_BLEND); + _settings->add_combo(SP_CSS_BLEND_NORMAL, SP_ATTR_MODE, _("Mode:"), SPBlendModeConverter); + + _settings->type(NR_FILTER_COLORMATRIX); + ComboBoxEnum<FilterColorMatrixType>* colmat = _settings->add_combo(COLORMATRIX_MATRIX, SP_ATTR_TYPE, _("Type:"), ColorMatrixTypeConverter, _("Indicates the type of matrix operation. The keyword 'matrix' indicates that a full 5x4 matrix of values will be provided. The other keywords represent convenience shortcuts to allow commonly used color operations to be performed without specifying a complete matrix.")); + _color_matrix_values = _settings->add_colormatrixvalues(_("Value(s):")); + colmat->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::update_color_matrix)); + + _settings->type(NR_FILTER_COMPONENTTRANSFER); + _settings->add_componenttransfervalues(_("R:"), SPFeFuncNode::R); + _settings->add_componenttransfervalues(_("G:"), SPFeFuncNode::G); + _settings->add_componenttransfervalues(_("B:"), SPFeFuncNode::B); + _settings->add_componenttransfervalues(_("A:"), SPFeFuncNode::A); + + _settings->type(NR_FILTER_COMPOSITE); + _settings->add_combo(COMPOSITE_OVER, SP_ATTR_OPERATOR, _("Operator:"), CompositeOperatorConverter); + _k1 = _settings->add_spinscale(0, SP_ATTR_K1, _("K1:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k2 = _settings->add_spinscale(0, SP_ATTR_K2, _("K2:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k3 = _settings->add_spinscale(0, SP_ATTR_K3, _("K3:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + _k4 = _settings->add_spinscale(0, SP_ATTR_K4, _("K4:"), -10, 10, 0.1, 0.01, 2, _("If the arithmetic operation is chosen, each result pixel is computed using the formula k1*i1*i2 + k2*i1 + k3*i2 + k4 where i1 and i2 are the pixel values of the first and second inputs respectively.")); + + _settings->type(NR_FILTER_CONVOLVEMATRIX); + _convolve_order = _settings->add_dualspinbutton((char*)"3", SP_ATTR_ORDER, _("Size:"), 1, 5, 1, 1, 0, _("width of the convolve matrix"), _("height of the convolve matrix")); + _convolve_target = _settings->add_multispinbutton(/*default x:*/ (double) 0, /*default y:*/ (double) 0, SP_ATTR_TARGETX, SP_ATTR_TARGETY, _("Target:"), 0, 4, 1, 1, 0, _("X coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point."), _("Y coordinate of the target point in the convolve matrix. The convolution is applied to pixels around this point.")); + //TRANSLATORS: for info on "Kernel", see http://en.wikipedia.org/wiki/Kernel_(matrix) + _convolve_matrix = _settings->add_matrix(SP_ATTR_KERNELMATRIX, _("Kernel:"), _("This matrix describes the convolve operation that is applied to the input image in order to calculate the pixel colors at the output. Different arrangements of values in this matrix result in various possible visual effects. An identity matrix would lead to a motion blur effect (parallel to the matrix diagonal) while a matrix filled with a constant non-zero value would lead to a common blur effect.")); + _convolve_order->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::convolve_order_changed)); + _settings->add_spinscale(0, SP_ATTR_DIVISOR, _("Divisor:"), 0, 1000, 1, 0.1, 2, _("After applying the kernelMatrix to the input image to yield a number, that number is divided by divisor to yield the final destination color value. A divisor that is the sum of all the matrix values tends to have an evening effect on the overall color intensity of the result.")); + _settings->add_spinscale(0, SP_ATTR_BIAS, _("Bias:"), -10, 10, 1, 0.01, 1, _("This value is added to each component. This is useful to define a constant value as the zero response of the filter.")); + _settings->add_combo(CONVOLVEMATRIX_EDGEMODE_DUPLICATE, SP_ATTR_EDGEMODE, _("Edge Mode:"), ConvolveMatrixEdgeModeConverter, _("Determines how to extend the input image as necessary with color values so that the matrix operations can be applied when the kernel is positioned at or near the edge of the input image.")); + _settings->add_checkbutton(false, SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + + _settings->type(NR_FILTER_DIFFUSELIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SP_PROP_LIGHTING_COLOR, _("Diffuse Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_SURFACESCALE, _("Surface Scale:"), -5, 5, 0.01, 0.001, 3, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SP_ATTR_DIFFUSECONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_dualspinscale(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_DISPLACEMENTMAP); + _settings->add_spinscale(0, SP_ATTR_SCALE, _("Scale:"), 0, 100, 1, 0.01, 1, _("This defines the intensity of the displacement effect.")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_XCHANNELSELECTOR, _("X displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the X direction")); + _settings->add_combo(DISPLACEMENTMAP_CHANNEL_ALPHA, SP_ATTR_YCHANNELSELECTOR, _("Y displacement:"), DisplacementMapChannelConverter, _("Color component that controls the displacement in the Y direction")); + + _settings->type(NR_FILTER_FLOOD); + _settings->add_color(/*default: black*/ 0, SP_PROP_FLOOD_COLOR, _("Flood Color:"), _("The whole filter region will be filled with this color.")); + _settings->add_spinscale(1, SP_PROP_FLOOD_OPACITY, _("Opacity:"), 0, 1, 0.1, 0.01, 2); + + _settings->type(NR_FILTER_GAUSSIANBLUR); + _settings->add_dualspinscale(SP_ATTR_STDDEVIATION, _("Standard Deviation:"), 0.01, 100, 1, 0.01, 2, _("The standard deviation for the blur operation.")); + + _settings->type(NR_FILTER_MERGE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_MORPHOLOGY); + _settings->add_combo(MORPHOLOGY_OPERATOR_ERODE, SP_ATTR_OPERATOR, _("Operator:"), MorphologyOperatorConverter, _("Erode: performs \"thinning\" of input image.\nDilate: performs \"fattenning\" of input image.")); + _settings->add_dualspinscale(SP_ATTR_RADIUS, _("Radius:"), 0, 100, 1, 0.01, 1); + + _settings->type(NR_FILTER_IMAGE); + _settings->add_fileorelement(SP_ATTR_XLINK_HREF, _("Source of Image:")); + _image_x = _settings->add_entry(SP_ATTR_X,_("X"),_("X")); + _image_x->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_x_changed)); + //This commented because we want the default empty value of X or Y and couldent get it from SpinButton + //_image_y = _settings->add_spinbutton(0, SP_ATTR_Y, _("Y:"), -DBL_MAX, DBL_MAX, 1, 1, 5, _("Y")); + _image_y = _settings->add_entry(SP_ATTR_Y,_("Y"),_("Y")); + _image_y->signal_attr_changed().connect(sigc::mem_fun(*this, &FilterEffectsDialog::image_y_changed)); + _settings->type(NR_FILTER_OFFSET); + _settings->add_checkbutton(false, SP_ATTR_PRESERVEALPHA, _("Preserve Alpha"), "true", "false", _("If set, the alpha channel won't be altered by this filter primitive.")); + _settings->add_spinscale(0, SP_ATTR_DX, _("Delta X:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted to the right")); + _settings->add_spinscale(0, SP_ATTR_DY, _("Delta Y:"), -100, 100, 1, 0.01, 2, _("This is how far the input image gets shifted downwards")); + + _settings->type(NR_FILTER_SPECULARLIGHTING); + _settings->add_color(/*default: white*/ 0xffffffff, SP_PROP_LIGHTING_COLOR, _("Specular Color:"), _("Defines the color of the light source")); + _settings->add_spinscale(1, SP_ATTR_SURFACESCALE, _("Surface Scale:"), -5, 5, 0.1, 0.01, 2, _("This value amplifies the heights of the bump map defined by the input alpha channel")); + _settings->add_spinscale(1, SP_ATTR_SPECULARCONSTANT, _("Constant:"), 0, 5, 0.1, 0.01, 2, _("This constant affects the Phong lighting model.")); + _settings->add_spinscale(1, SP_ATTR_SPECULAREXPONENT, _("Exponent:"), 1, 50, 1, 0.01, 1, _("Exponent for specular term, larger is more \"shiny\".")); + _settings->add_dualspinscale(SP_ATTR_KERNELUNITLENGTH, _("Kernel Unit Length:"), 0.01, 10, 1, 0.01, 1); + _settings->add_lightsource(); + + _settings->type(NR_FILTER_TILE); + _settings->add_no_params(); + + _settings->type(NR_FILTER_TURBULENCE); +// _settings->add_checkbutton(false, SP_ATTR_STITCHTILES, _("Stitch Tiles"), "stitch", "noStitch"); + _settings->add_combo(TURBULENCE_TURBULENCE, SP_ATTR_TYPE, _("Type:"), TurbulenceTypeConverter, _("Indicates whether the filter primitive should perform a noise or turbulence function.")); + _settings->add_dualspinscale(SP_ATTR_BASEFREQUENCY, _("Base Frequency:"), 0, 1, 0.001, 0.01, 3); + _settings->add_spinscale(1, SP_ATTR_NUMOCTAVES, _("Octaves:"), 1, 10, 1, 1, 0); + _settings->add_spinscale(0, SP_ATTR_SEED, _("Seed:"), 0, 1000, 1, 1, 0, _("The starting number for the pseudo random number generator.")); +} + +void FilterEffectsDialog::add_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + SPFilterPrimitive* prim = filter_add_primitive(filter, _add_primitive_type.get_active_data()->id); + + _primitive_list.select(prim); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Add filter primitive")); + } +} + +void FilterEffectsDialog::update_primitive_infobox() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + switch(_add_primitive_type.get_active_data()->id){ + case(NR_FILTER_BLEND): + _infobox_icon.set_from_icon_name("feBlend-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feBlend</b> filter primitive provides 4 image blending modes: screen, multiply, darken and lighten.")); + break; + case(NR_FILTER_COLORMATRIX): + _infobox_icon.set_from_icon_name("feColorMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feColorMatrix</b> filter primitive applies a matrix transformation to color of each rendered pixel. This allows for effects like turning object to grayscale, modifying color saturation and changing color hue.")); + break; + case(NR_FILTER_COMPONENTTRANSFER): + _infobox_icon.set_from_icon_name("feComponentTransfer-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feComponentTransfer</b> filter primitive manipulates the input's color components (red, green, blue, and alpha) according to particular transfer functions, allowing operations like brightness and contrast adjustment, color balance, and thresholding.")); + break; + case(NR_FILTER_COMPOSITE): + _infobox_icon.set_from_icon_name("feComposite-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feComposite</b> filter primitive composites two images using one of the Porter-Duff blending modes or the arithmetic mode described in SVG standard. Porter-Duff blending modes are essentially logical operations between the corresponding pixel values of the images.")); + break; + case(NR_FILTER_CONVOLVEMATRIX): + _infobox_icon.set_from_icon_name("feConvolveMatrix-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feConvolveMatrix</b> lets you specify a Convolution to be applied on the image. Common effects created using convolution matrices are blur, sharpening, embossing and edge detection. Note that while gaussian blur can be created using this filter primitive, the special gaussian blur primitive is faster and resolution-independent.")); + break; + case(NR_FILTER_DIFFUSELIGHTING): + _infobox_icon.set_from_icon_name("feDiffuseLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feDiffuseLighting</b> and feSpecularLighting filter primitives create \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_DISPLACEMENTMAP): + _infobox_icon.set_from_icon_name("feDisplacementMap-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feDisplacementMap</b> filter primitive displaces the pixels in the first input using the second input as a displacement map, that shows from how far the pixel should come from. Classical examples are whirl and pinch effects.")); + break; + case(NR_FILTER_FLOOD): + _infobox_icon.set_from_icon_name("feFlood-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feFlood</b> filter primitive fills the region with a given color and opacity. It is usually used as an input to other filters to apply color to a graphic.")); + break; + case(NR_FILTER_GAUSSIANBLUR): + _infobox_icon.set_from_icon_name("feGaussianBlur-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feGaussianBlur</b> filter primitive uniformly blurs its input. It is commonly used together with feOffset to create a drop shadow effect.")); + break; + case(NR_FILTER_IMAGE): + _infobox_icon.set_from_icon_name("feImage-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feImage</b> filter primitive fills the region with an external image or another part of the document.")); + break; + case(NR_FILTER_MERGE): + _infobox_icon.set_from_icon_name("feMerge-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feMerge</b> filter primitive composites several temporary images inside the filter primitive to a single image. It uses normal alpha compositing for this. This is equivalent to using several feBlend primitives in 'normal' mode or several feComposite primitives in 'over' mode.")); + break; + case(NR_FILTER_MORPHOLOGY): + _infobox_icon.set_from_icon_name("feMorphology-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feMorphology</b> filter primitive provides erode and dilate effects. For single-color objects erode makes the object thinner and dilate makes it thicker.")); + break; + case(NR_FILTER_OFFSET): + _infobox_icon.set_from_icon_name("feOffset-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feOffset</b> filter primitive offsets the image by an user-defined amount. For example, this is useful for drop shadows, where the shadow is in a slightly different position than the actual object.")); + break; + case(NR_FILTER_SPECULARLIGHTING): + _infobox_icon.set_from_icon_name("feSpecularLighting-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feDiffuseLighting</b> and <b>feSpecularLighting</b> filter primitives create \"embossed\" shadings. The input's alpha channel is used to provide depth information: higher opacity areas are raised toward the viewer and lower opacity areas recede away from the viewer.")); + break; + case(NR_FILTER_TILE): + _infobox_icon.set_from_icon_name("feTile-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feTile</b> filter primitive tiles a region with an input graphic. The source tile is defined by the filter primitive subregion of the input.")); + break; + case(NR_FILTER_TURBULENCE): + _infobox_icon.set_from_icon_name("feTurbulence-icon", Gtk::ICON_SIZE_DIALOG); + _infobox_desc.set_markup(_("The <b>feTurbulence</b> filter primitive renders Perlin noise. This kind of noise is useful in simulating several nature phenomena like clouds, fire and smoke and in generating complex textures like marble or granite.")); + break; + default: + g_assert(false); + break; + } + //_infobox_icon.set_pixel_size(96); + _infobox_icon.set_pixel_size(64); +} + +void FilterEffectsDialog::duplicate_primitive() +{ + SPFilter* filter = _filter_modifier.get_selected_filter(); + SPFilterPrimitive* origprim = _primitive_list.get_selected(); + + if (filter && origprim) { + Inkscape::XML::Node *repr; + repr = origprim->getRepr()->duplicate(origprim->getRepr()->document()); + filter->getRepr()->appendChild(repr); + + DocumentUndo::done(filter->document, SP_VERB_DIALOG_FILTER_EFFECTS, _("Duplicate filter primitive")); + + _primitive_list.update(); + } +} + +void FilterEffectsDialog::convolve_order_changed() +{ + _convolve_matrix->set_from_attribute(_primitive_list.get_selected()); + _convolve_target->get_spinbuttons()[0]->get_adjustment()->set_upper(_convolve_order->get_spinbutton1().get_value() - 1); + _convolve_target->get_spinbuttons()[1]->get_adjustment()->set_upper(_convolve_order->get_spinbutton2().get_value() - 1); +} + +bool number_or_empy(const Glib::ustring& text) { + if (text.empty()) { + return true; + } + double n = atof( text.c_str() ); + if (n == 0.0 && strcmp(text.c_str(), "0") != 0 && strcmp(text.c_str(), "0.0") != 0) { + return false; + } + else { + return true; + } +} + +void FilterEffectsDialog::image_x_changed() +{ + if (number_or_empy(_image_x->get_text())) { + _image_x->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::image_y_changed() +{ + if (number_or_empy(_image_y->get_text())) { + _image_y->set_from_attribute(_primitive_list.get_selected()); + } +} + +void FilterEffectsDialog::set_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_filternode_attr(const AttrWidget* input) +{ + if(!_locked) { + _attr_lock = true; + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(input->get_attribute()); + if (filter && name && filter->getRepr()){ + filter->setAttributeOrRemoveIfEmpty(name, input->get_as_attribute()); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + _attr_lock = false; + } +} + +void FilterEffectsDialog::set_child_attr_direct(const AttrWidget* input) +{ + set_attr(_primitive_list.get_selected()->firstChild(), input->get_attribute(), input->get_as_attribute().c_str()); +} + +void FilterEffectsDialog::set_attr(SPObject* o, const SPAttributeEnum attr, const gchar* val) +{ + if(!_locked) { + _attr_lock = true; + + SPFilter *filter = _filter_modifier.get_selected_filter(); + const gchar* name = (const gchar*)sp_attribute_name(attr); + if(filter && name && o) { + update_settings_sensitivity(); + + o->setAttribute(name, val); + filter->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "filtereffects:"; + undokey += name; + DocumentUndo::maybeDone(filter->document, undokey.c_str(), SP_VERB_DIALOG_FILTER_EFFECTS, + _("Set filter primitive attribute")); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_filter_general_settings_view() +{ + if(_settings_initialized != true) return; + + if(!_locked) { + _attr_lock = true; + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + else { + std::vector<Gtk::Widget*> vect = _settings_tab2.get_children(); + vect[0]->hide(); + _no_filter_selected.show(); + } + + _attr_lock = false; + } +} + +void FilterEffectsDialog::update_settings_view() +{ + update_settings_sensitivity(); + + if(_attr_lock) + return; + +//First Tab + + std::vector<Gtk::Widget*> vect1 = _settings_tab1.get_children(); + for(auto & i : vect1) + i->hide(); + _empty_settings.show(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/showfiltersinfobox/value", true)){ + _sw_infobox->show(); + } else { + _sw_infobox->hide(); + } + + SPFilterPrimitive* prim = _primitive_list.get_selected(); + + if(prim && prim->getRepr()) { + + //XML Tree being used directly here while it shouldn't be. + _settings->show_and_update(FPConverter.get_id_from_key(prim->getRepr()->name()), prim); + _empty_settings.hide(); + } + +//Second Tab + + std::vector<Gtk::Widget*> vect2 = _settings_tab2.get_children(); + vect2[0]->hide(); + _no_filter_selected.show(); + + SPFilter* filter = _filter_modifier.get_selected_filter(); + + if(filter) { + _filter_general_settings->show_and_update(0, filter); + _no_filter_selected.hide(); + } + +} + +void FilterEffectsDialog::update_settings_sensitivity() +{ + SPFilterPrimitive* prim = _primitive_list.get_selected(); + const bool use_k = SP_IS_FECOMPOSITE(prim) && SP_FECOMPOSITE(prim)->composite_operator == COMPOSITE_ARITHMETIC; + _k1->set_sensitive(use_k); + _k2->set_sensitive(use_k); + _k3->set_sensitive(use_k); + _k4->set_sensitive(use_k); + +} + +void FilterEffectsDialog::update_color_matrix() +{ + _color_matrix_values->set_from_attribute(_primitive_list.get_selected()); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/filter-effects-dialog.h b/src/ui/dialog/filter-effects-dialog.h new file mode 100644 index 0000000..0ed28d1 --- /dev/null +++ b/src/ui/dialog/filter-effects-dialog.h @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Filter Effects dialog + */ +/* Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Rodrigo Kumpera <kumpera@gmail.com> + * insaner + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H +#define INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H + +#include <memory> + +#include <gtkmm/notebook.h> + +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> + +#include "attributes.h" +#include "display/nr-filter-types.h" +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/combo-enums.h" +#include "ui/widget/panel.h" +#include "ui/widget/spin-scale.h" + +#include "xml/helper-observer.h" + +class SPFilter; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class EntryAttr; +//class SpinButtonAttr; +class DualSpinButton; +class MultiSpinButton; +class FilterEffectsDialog : public UI::Widget::Panel { +public: + + FilterEffectsDialog(); + ~FilterEffectsDialog() override; + + static FilterEffectsDialog &getInstance() + { return *new FilterEffectsDialog(); } + + void set_attrs_locked(const bool); +protected: + void show_all_vfunc() override; +private: + + class FilterModifier : public Gtk::VBox + { + public: + FilterModifier(FilterEffectsDialog&); + ~FilterModifier() override; + + SPFilter* get_selected_filter(); + void select_filter(const SPFilter*); + + sigc::signal<void>& signal_filter_changed() + { + return _signal_filter_changed; + } + private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(filter); + add(label); + add(sel); + add(count); + } + + Gtk::TreeModelColumn<SPFilter*> filter; + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<int> sel; + Gtk::TreeModelColumn<int> count; + }; + + void setTargetDesktop(SPDesktop *desktop); + + void on_document_replaced(SPDesktop *desktop, SPDocument *document); + void on_change_selection(); + void on_modified_selection( guint flags ); + + void update_selection(Selection *); + void on_filter_selection_changed(); + + void on_name_edited(const Glib::ustring&, const Glib::ustring&); + bool on_filter_move(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/); + void on_selection_toggled(const Glib::ustring&); + + void update_counts(); + void update_filters(); + void filter_list_button_release(GdkEventButton*); + void add_filter(); + void remove_filter(); + void duplicate_filter(); + void rename_filter(); + + /** + * Stores the current desktop. + */ + SPDesktop *_desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker _deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection _selectChangedConn; + sigc::connection _selectModifiedConn; + sigc::connection _doc_replaced; + sigc::connection _resource_changed; + + FilterEffectsDialog& _dialog; + Gtk::TreeView _list; + Glib::RefPtr<Gtk::ListStore> _model; + Columns _columns; + Gtk::CellRendererToggle _cell_toggle; + Gtk::Button _add; + Gtk::Menu *_menu; + sigc::signal<void> _signal_filter_changed; + std::unique_ptr<Inkscape::XML::SignalObserver> _observer; + }; + + class PrimitiveColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PrimitiveColumns() + { + add(primitive); + add(type_id); + add(type); + add(id); + } + + Gtk::TreeModelColumn<SPFilterPrimitive*> primitive; + Gtk::TreeModelColumn<Inkscape::Filters::FilterPrimitiveType> type_id; + Gtk::TreeModelColumn<Glib::ustring> type; + Gtk::TreeModelColumn<Glib::ustring> id; + }; + + class CellRendererConnection : public Gtk::CellRenderer + { + public: + CellRendererConnection(); + Glib::PropertyProxy<void*> property_primitive(); + + static const int size = 24; + + protected: + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_width_for_height_vfunc(Gtk::Widget& widget, + int height, + int& minimum_width, + int& natural_width) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& minimum_height, + int& natural_height) const override; + + void get_preferred_height_for_width_vfunc(Gtk::Widget& widget, + int width, + int& minimum_height, + int& natural_height) const override; + private: + // void* should be SPFilterPrimitive*, some weirdness with properties prevents this + Glib::Property<void*> _primitive; + int _text_width; + }; + + class PrimitiveList : public Gtk::TreeView + { + public: + PrimitiveList(FilterEffectsDialog&); + + sigc::signal<void>& signal_primitive_changed(); + + void update(); + void set_menu(Gtk::Widget &parent, + sigc::slot<void> dup, + sigc::slot<void> rem); + + SPFilterPrimitive* get_selected(); + void select(SPFilterPrimitive *prim); + void remove_selected(); + + int primitive_count() const; + int get_input_type_width() const; + + protected: + bool on_draw_signal(const Cairo::RefPtr<Cairo::Context> &cr); + + + bool on_button_press_event(GdkEventButton*) override; + bool on_motion_notify_event(GdkEventMotion*) override; + bool on_button_release_event(GdkEventButton*) override; + void on_drag_end(const Glib::RefPtr<Gdk::DragContext>&) override; + private: + void init_text(); + + void draw_connection_node(const Cairo::RefPtr<Cairo::Context>& cr, + const std::vector<Gdk::Point>& points, + const bool fill); + + bool do_connection_node(const Gtk::TreeIter& row, const int input, std::vector<Gdk::Point>& points, + const int ix, const int iy); + + const Gtk::TreeIter find_result(const Gtk::TreeIter& start, const int attr, int& src_id); + int find_index(const Gtk::TreeIter& target); + void draw_connection(const Cairo::RefPtr<Cairo::Context>& cr, + const Gtk::TreeIter&, const int attr, const int text_start_x, + const int x1, const int y1, const int row_count, + const GdkRGBA fg_color, const GdkRGBA mid_color); + void sanitize_connections(const Gtk::TreeIter& prim_iter); + void on_primitive_selection_changed(); + bool on_scroll_timeout(); + + FilterEffectsDialog& _dialog; + Glib::RefPtr<Gtk::ListStore> _model; + PrimitiveColumns _columns; + CellRendererConnection _connection_cell; + Gtk::Menu *_primitive_menu; + Glib::RefPtr<Pango::Layout> _vertical_layout; + int _in_drag; + SPFilterPrimitive* _drag_prim; + sigc::signal<void> _signal_primitive_changed; + sigc::connection _scroll_connection; + int _autoscroll_y; + int _autoscroll_x; + std::unique_ptr<Inkscape::XML::SignalObserver> _observer; + int _input_type_width; + int _input_type_height; + }; + + void init_settings_widgets(); + + // Handlers + void add_primitive(); + void remove_primitive(); + void duplicate_primitive(); + void convolve_order_changed(); + void image_x_changed(); + void image_y_changed(); + + void set_attr_direct(const UI::Widget::AttrWidget*); + void set_child_attr_direct(const UI::Widget::AttrWidget*); + void set_filternode_attr(const UI::Widget::AttrWidget*); + void set_attr(SPObject*, const SPAttributeEnum, const gchar* val); + void update_settings_view(); + void update_filter_general_settings_view(); + void update_settings_sensitivity(); + void update_color_matrix(); + void update_primitive_infobox(); + + // Primitives Info Box + Gtk::Label _infobox_desc; + Gtk::Image _infobox_icon; + Gtk::ScrolledWindow* _sw_infobox; + + // View/add primitives + Gtk::Paned* _primitive_box; + + UI::Widget::ComboBoxEnum<Inkscape::Filters::FilterPrimitiveType> _add_primitive_type; + Gtk::Button _add_primitive; + + // Bottom pane (filter effect primitive settings) + Gtk::Notebook _settings_tabs; + Gtk::VBox _settings_tab2; + Gtk::VBox _settings_tab1; + Gtk::Label _empty_settings; + Gtk::Label _no_filter_selected; + bool _settings_initialized; + + class Settings; + class MatrixAttr; + class ColorMatrixValues; + class ComponentTransferValues; + class LightSourceControl; + Settings* _settings; + Settings* _filter_general_settings; + + // Color Matrix + ColorMatrixValues* _color_matrix_values; + + // Component Transfer + ComponentTransferValues* _component_transfer_values; + + // Convolve Matrix + MatrixAttr* _convolve_matrix; + DualSpinButton* _convolve_order; + MultiSpinButton* _convolve_target; + + // Image + EntryAttr* _image_x; + EntryAttr* _image_y; + + // For controlling setting sensitivity + Gtk::Widget* _k1, *_k2, *_k3, *_k4; + + // To prevent unwanted signals + bool _locked; + bool _attr_lock; + + // These go last since they depend on the prior initialization of + // other FilterEffectsDialog members + FilterModifier _filter_modifier; + PrimitiveList _primitive_list; + + FilterEffectsDialog(FilterEffectsDialog const &d); + FilterEffectsDialog& operator=(FilterEffectsDialog const &d); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FILTER_EFFECTS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/find.cpp b/src/ui/dialog/find.cpp new file mode 100644 index 0000000..a6d53bb --- /dev/null +++ b/src/ui/dialog/find.cpp @@ -0,0 +1,1076 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Johan Engelen <goejendaagh@zonnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "find.h" + +#include <gtkmm/entry.h> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "text-editing.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-line.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-polyline.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "object/sp-text.h" +#include "object/sp-tref.h" +#include "object/sp-tspan.h" +#include "object/sp-use.h" + +#include "ui/dialog-events.h" + +#include "xml/attribute-record.h" +#include "xml/node-iterators.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Find::Find() + : UI::Widget::Panel("/dialogs/find", SP_VERB_DIALOG_FIND), + + entry_find(_("F_ind:"), _("Find objects by their content or properties (exact or partial match)")), + entry_replace(_("R_eplace:"), _("Replace match with this value")), + + check_scope_all(_("_All")), + check_scope_layer(_("Current _layer")), + check_scope_selection(_("Sele_ction")), + check_searchin_text(_("_Text")), + check_searchin_property(_("_Properties")), + vbox_searchin(false, false), + frame_searchin(_("Search in")), + frame_scope(_("Scope")), + + check_case_sensitive(_("Case sensiti_ve")), + check_exact_match(_("E_xact match")), + check_include_hidden(_("Include _hidden")), + check_include_locked(_("Include loc_ked")), + expander_options(_("Options")), + frame_options(_("General")), + + check_ids(_("_ID")), + check_attributename(_("Attribute _name")), + check_attributevalue(_("Attri_bute value")), + check_style(_("_Style")), + check_font(_("F_ont")), + frame_properties(_("Properties")), + + check_alltypes(_("All types")), + check_rects(_("Rectangles")), + check_ellipses(_("Ellipses")), + check_stars(_("Stars")), + check_spirals(_("Spirals")), + check_paths(_("Paths")), + check_texts(_("Texts")), + check_groups(_("Groups")), + check_clones( + //TRANSLATORS: "Clones" is a noun indicating type of object to find + C_("Find dialog", "Clones")), + + check_images(_("Images")), + check_offsets(_("Offsets")), + frame_types(_("Object types")), + + status(""), + button_find(_("_Find")), + button_replace(_("_Replace All")), + _action_replace(false), + blocked(false), + desktop(nullptr), + deskTrack() + +{ + _left_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + _right_size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + button_find.set_use_underline(); + button_find.set_tooltip_text(_("Select all objects matching the selection criteria")); + button_replace.set_use_underline(); + button_replace.set_tooltip_text(_("Replace all matches")); + check_scope_all.set_use_underline(); + check_scope_all.set_tooltip_text(_("Search in all layers")); + check_scope_layer.set_use_underline(); + check_scope_layer.set_tooltip_text(_("Limit search to the current layer")); + check_scope_selection.set_use_underline(); + check_scope_selection.set_tooltip_text(_("Limit search to the current selection")); + check_searchin_text.set_use_underline(); + check_searchin_text.set_tooltip_text(_("Search in text objects")); + check_searchin_property.set_use_underline(); + check_searchin_property.set_tooltip_text(_("Search in object properties, styles, attributes and IDs")); + check_case_sensitive.set_use_underline(); + check_case_sensitive.set_tooltip_text(_("Match upper/lower case")); + check_case_sensitive.set_active(false); + check_exact_match.set_use_underline(); + check_exact_match.set_tooltip_text(_("Match whole objects only")); + check_exact_match.set_active(false); + check_include_hidden.set_use_underline(); + check_include_hidden.set_tooltip_text(_("Include hidden objects in search")); + check_include_hidden.set_active(false); + check_include_locked.set_use_underline(); + check_include_locked.set_tooltip_text(_("Include locked objects in search")); + check_include_locked.set_active(false); + check_ids.set_use_underline(); + check_ids.set_tooltip_text(_("Search id name")); + check_ids.set_active(true); + check_attributename.set_use_underline(); + check_attributename.set_tooltip_text(_("Search attribute name")); + check_attributename.set_active(false); + check_attributevalue.set_use_underline(); + check_attributevalue.set_tooltip_text(_("Search attribute value")); + check_attributevalue.set_active(true); + check_style.set_use_underline(); + check_style.set_tooltip_text(_("Search style")); + check_style.set_active(true); + check_font.set_use_underline(); + check_font.set_tooltip_text(_("Search fonts")); + check_font.set_active(false); + check_alltypes.set_use_underline(); + check_alltypes.set_tooltip_text(_("Search all object types")); + check_alltypes.set_active(true); + check_rects.set_use_underline(); + check_rects.set_tooltip_text(_("Search rectangles")); + check_rects.set_active(false); + check_ellipses.set_use_underline(); + check_ellipses.set_tooltip_text(_("Search ellipses, arcs, circles")); + check_ellipses.set_active(false); + check_stars.set_use_underline(); + check_stars.set_tooltip_text(_("Search stars and polygons")); + check_stars.set_active(false); + check_spirals.set_use_underline(); + check_spirals.set_tooltip_text(_("Search spirals")); + check_spirals.set_active(false); + check_paths.set_use_underline(); + check_paths.set_tooltip_text(_("Search paths, lines, polylines")); + check_paths.set_active(false); + check_texts.set_use_underline(); + check_texts.set_tooltip_text(_("Search text objects")); + check_texts.set_active(false); + check_groups.set_use_underline(); + check_groups.set_tooltip_text(_("Search groups")); + check_groups.set_active(false), + check_clones.set_use_underline(); + check_clones.set_tooltip_text(_("Search clones")); + check_clones.set_active(false); + check_images.set_use_underline(); + check_images.set_tooltip_text(_("Search images")); + check_images.set_active(false); + check_offsets.set_use_underline(); + check_offsets.set_tooltip_text(_("Search offset objects")); + check_offsets.set_active(false); + + entry_find.getEntry()->set_width_chars(25); + entry_find.child_property_fill(*entry_find.getEntry()) = true; + entry_find.child_property_expand(*entry_find.getEntry()) = true; + entry_replace.getEntry()->set_width_chars(25); + entry_replace.child_property_fill(*entry_replace.getEntry()) = true; + entry_replace.child_property_expand(*entry_replace.getEntry()) = true; + + Gtk::RadioButtonGroup grp_searchin = check_searchin_text.get_group(); + check_searchin_property.set_group(grp_searchin); + vbox_searchin.pack_start(check_searchin_text, Gtk::PACK_SHRINK); + vbox_searchin.pack_start(check_searchin_property, Gtk::PACK_SHRINK); + frame_searchin.add(vbox_searchin); + + Gtk::RadioButtonGroup grp_scope = check_scope_all.get_group(); + check_scope_layer.set_group(grp_scope); + check_scope_selection.set_group(grp_scope); + vbox_scope.pack_start(check_scope_all, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_layer, Gtk::PACK_SHRINK); + vbox_scope.pack_start(check_scope_selection, Gtk::PACK_SHRINK); + hbox_searchin.set_spacing(12); + hbox_searchin.pack_start(frame_searchin, Gtk::PACK_SHRINK); + hbox_searchin.pack_start(frame_scope, Gtk::PACK_SHRINK); + frame_scope.add(vbox_scope); + + vbox_options1.pack_start(check_case_sensitive, Gtk::PACK_SHRINK); + vbox_options1.pack_start(check_include_hidden, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_exact_match, Gtk::PACK_SHRINK); + vbox_options2.pack_start(check_include_locked, Gtk::PACK_SHRINK); + _left_size_group->add_widget(check_case_sensitive); + _left_size_group->add_widget(check_include_hidden); + _right_size_group->add_widget(check_exact_match); + _right_size_group->add_widget(check_include_locked); + hbox_options.set_spacing(4); + hbox_options.pack_start(vbox_options1, Gtk::PACK_SHRINK); + hbox_options.pack_start(vbox_options2, Gtk::PACK_SHRINK); + frame_options.add(hbox_options); + + vbox_properties1.pack_start(check_ids, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_style, Gtk::PACK_SHRINK); + vbox_properties1.pack_start(check_font, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributevalue, Gtk::PACK_SHRINK); + vbox_properties2.pack_start(check_attributename, Gtk::PACK_SHRINK); + vbox_properties2.set_valign(Gtk::ALIGN_START); + _left_size_group->add_widget(check_ids); + _left_size_group->add_widget(check_style); + _left_size_group->add_widget(check_font); + _right_size_group->add_widget(check_attributevalue); + _right_size_group->add_widget(check_attributename); + hbox_properties.set_spacing(4); + hbox_properties.pack_start(vbox_properties1, Gtk::PACK_SHRINK); + hbox_properties.pack_start(vbox_properties2, Gtk::PACK_SHRINK); + frame_properties.add(hbox_properties); + + vbox_types1.pack_start(check_alltypes, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_paths, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_texts, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_groups, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_clones, Gtk::PACK_SHRINK); + vbox_types1.pack_start(check_images, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_offsets, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_rects, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_ellipses, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_stars, Gtk::PACK_SHRINK); + vbox_types2.pack_start(check_spirals, Gtk::PACK_SHRINK); + vbox_types2.set_valign(Gtk::ALIGN_END); + _left_size_group->add_widget(check_alltypes); + _left_size_group->add_widget(check_paths); + _left_size_group->add_widget(check_texts); + _left_size_group->add_widget(check_groups); + _left_size_group->add_widget(check_clones); + _left_size_group->add_widget(check_images); + _right_size_group->add_widget(check_offsets); + _right_size_group->add_widget(check_rects); + _right_size_group->add_widget(check_ellipses); + _right_size_group->add_widget(check_stars); + _right_size_group->add_widget(check_spirals); + hbox_types.set_spacing(4); + hbox_types.pack_start(vbox_types1, Gtk::PACK_SHRINK); + hbox_types.pack_start(vbox_types2, Gtk::PACK_SHRINK); + frame_types.add(hbox_types); + + vbox_expander.set_spacing(4); + vbox_expander.pack_start(frame_options, true, true); + vbox_expander.pack_start(frame_properties, true, true); + vbox_expander.pack_start(frame_types, true, true); + + expander_options.set_use_underline(); + expander_options.add(vbox_expander); + + box_buttons.set_layout(Gtk::BUTTONBOX_END); + box_buttons.set_spacing(6); + box_buttons.pack_start(button_find, true, true); + box_buttons.pack_start(button_replace, true, true); + hboxbutton_row.set_spacing(6); + hboxbutton_row.pack_start(status, true, true); + hboxbutton_row.pack_end(box_buttons, true, true); + + Gtk::Box *contents = _getContents(); + contents->set_spacing(6); + contents->pack_start(entry_find, false, false); + contents->pack_start(entry_replace, false, false); + contents->pack_start(hbox_searchin, false, false); + contents->pack_start(expander_options, false, false); + contents->pack_end(hboxbutton_row, false, false); + + checkProperties.push_back(&check_ids); + checkProperties.push_back(&check_style); + checkProperties.push_back(&check_font); + checkProperties.push_back(&check_attributevalue); + checkProperties.push_back(&check_attributename); + + checkTypes.push_back(&check_paths); + checkTypes.push_back(&check_texts); + checkTypes.push_back(&check_groups); + checkTypes.push_back(&check_clones); + checkTypes.push_back(&check_images); + checkTypes.push_back(&check_offsets); + checkTypes.push_back(&check_rects); + checkTypes.push_back(&check_ellipses); + checkTypes.push_back(&check_stars); + checkTypes.push_back(&check_spirals); + + // set signals to handle clicks + expander_options.property_expanded().signal_changed().connect(sigc::mem_fun(*this, &Find::onExpander)); + button_find.signal_clicked().connect(sigc::mem_fun(*this, &Find::onFind)); + button_replace.signal_clicked().connect(sigc::mem_fun(*this, &Find::onReplace)); + check_searchin_text.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinText)); + check_searchin_property.signal_clicked().connect(sigc::mem_fun(*this, &Find::onSearchinProperty)); + check_alltypes.signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleAlltypes)); + + for (auto & checkProperty : checkProperties) { + checkProperty->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + for (auto & checkType : checkTypes) { + checkType->signal_clicked().connect(sigc::mem_fun(*this, &Find::onToggleCheck)); + } + + onSearchinText(); + onToggleAlltypes(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Find::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + SPItem *item = selection->singleItem(); + if (item) { + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + gchar *str; + str = sp_te_get_string_multiline (item); + entry_find.getEntry()->set_text(str); + } + } + + button_find.set_can_default(); + //button_find.grab_default(); // activatable by Enter + entry_find.getEntry()->grab_focus(); +} + +Find::~Find() +{ + desktopChangeConn.disconnect(); + selectChangedConn.disconnect(); + deskTrack.disconnect(); +} + +void Find::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void Find::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Find::onSelectionChange))); + } + } +} + +void Find::onSelectionChange() +{ + if (!blocked) { + status.set_text(""); + } +} + +/*######################################################################## +# FIND helper functions +########################################################################*/ + +Glib::ustring Find::find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall) +{ + Glib::ustring ustr = str; + Glib::ustring ufind = find; + if (!casematch) { + ufind = ufind.lowercase(); + } + gsize n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch); + while (n != std::string::npos) { + ustr.replace(n, ufind.length(), replace); + if (!replaceall) { + return ustr; + } + // Start the next search after the last replace character to avoid infinite loops (replace "a" with "aaa" etc) + n = find_strcmp_pos(ustr.c_str(), ufind.c_str(), exact, casematch, n + strlen(replace) + 1); + } + return ustr; +} + +gsize Find::find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start/*=0*/) +{ + Glib::ustring ustr = str ? str : ""; + Glib::ustring ufind = find; + + if (!casematch) { + ustr = ustr.lowercase(); + ufind = ufind.lowercase(); + } + + gsize pos = std::string::npos; + if (exact) { + if (ustr == ufind) { + pos = 0; + } + } else { + pos = ustr.find(ufind, start); + } + + return pos; +} + + +bool Find::find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch) +{ + return (std::string::npos != find_strcmp_pos(str, find, exact, casematch)); +} + +bool Find::item_text_match (SPItem *item, const gchar *find, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + const gchar *item_text = sp_te_get_string_multiline (item); + if (item_text == nullptr) { + return false; + } + bool found = find_strcmp(item_text, find, exact, casematch); + + if (found && replace) { + Glib::ustring ufind = find; + if (!casematch) { + ufind = ufind.lowercase(); + } + + Inkscape::Text::Layout const *layout = te_get_layout (item); + if (!layout) { + return found; + } + + gchar* replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gsize n = find_strcmp_pos(item_text, ufind.c_str(), exact, casematch); + static Inkscape::Text::Layout::iterator _begin_w; + static Inkscape::Text::Layout::iterator _end_w; + while (n != std::string::npos) { + _begin_w = layout->charIndexToIterator(n); + _end_w = layout->charIndexToIterator(n + strlen(find)); + sp_te_replace(item, _begin_w, _end_w, replace_text); + item_text = sp_te_get_string_multiline (item); + n = find_strcmp_pos(item_text, ufind.c_str(), exact, casematch, n + strlen(replace_text) + 1); + } + + g_free(replace_text); + } + + return found; + } + return false; +} + + +bool Find::item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + if (dynamic_cast<SPString *>(item)) { // SPStrings have "on demand" ids which are useless for searching + return false; + } + + const gchar *item_id = item->getRepr()->attribute("id"); + if (item_id == nullptr) { + return false; + } + + bool found = find_strcmp(item_id, id, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_id, id, replace_text , exact, casematch, true); + if (new_item_style != item_id) { + item->setAttribute("id", new_item_style); + } + g_free(replace_text); + } + + return found; +} + +bool Find::item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + if (item->getRepr() == nullptr) { + return false; + } + + gchar *item_style = g_strdup(item->getRepr()->attribute("style")); + if (item_style == nullptr) { + return false; + } + + bool found = find_strcmp(item_style, text, exact, casematch); + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(item_style, text, replace_text , exact, casematch, true); + if (new_item_style != item_style) { + item->setAttribute("style", new_item_style); + } + g_free(replace_text); + } + + g_free(item_style); + return found; +} + +bool Find::item_attr_match(SPItem *item, const gchar *text, bool exact, bool /*casematch*/, bool replace/*=false*/) +{ + bool found = false; + + if (item->getRepr() == nullptr) { + return false; + } + + gchar *attr_value = g_strdup(item->getRepr()->attribute(text)); + if (exact) { + found = (attr_value != nullptr); + } else { + found = item->getRepr()->matchAttributeName(text); + } + g_free(attr_value); + + // TODO - Rename attribute name ? + if (found && replace) { + found = false; + } + + return found; +} + +bool Find::item_attrvalue_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool replace/*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> iter = item->getRepr()->attributeList(); + for (; iter; ++iter) { + const gchar* key = g_quark_to_string(iter->key); + gchar *attr_value = g_strdup(item->getRepr()->attribute(key)); + bool found = find_strcmp(attr_value, text, exact, casematch); + if (found) { + ret = true; + } + + if (found && replace) { + gchar * replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + Glib::ustring new_item_style = find_replace(attr_value, text, replace_text , exact, casematch, true); + if (new_item_style != attr_value) { + item->setAttribute(key, new_item_style); + } + } + + g_free(attr_value); + } + + return ret; +} + + +bool Find::item_font_match(SPItem *item, const gchar *text, bool exact, bool casematch, bool /*replace*/ /*=false*/) +{ + bool ret = false; + + if (item->getRepr() == nullptr) { + return false; + } + + const gchar *item_style = item->getRepr()->attribute("style"); + if (item_style == nullptr) { + return false; + } + + std::vector<Glib::ustring> vFontTokenNames; + vFontTokenNames.emplace_back("font-family:"); + vFontTokenNames.emplace_back("-inkscape-font-specification:"); + + std::vector<Glib::ustring> vStyleTokens = Glib::Regex::split_simple(";", item_style); + for (auto & vStyleToken : vStyleTokens) { + Glib::ustring token = vStyleToken; + for (const auto & vFontTokenName : vFontTokenNames) { + if ( token.find(vFontTokenName) != std::string::npos) { + Glib::ustring font1 = Glib::ustring(vFontTokenName).append(text); + bool found = find_strcmp(token.c_str(), font1.c_str(), exact, casematch); + if (found) { + ret = true; + if (_action_replace) { + gchar *replace_text = g_strdup(entry_replace.getEntry()->get_text().c_str()); + gchar *orig_str = g_strdup(token.c_str()); + // Exact match fails since the "font-family:" is in the token, since the find was exact it still works with false below + Glib::ustring new_item_style = find_replace(orig_str, text, replace_text , false /*exact*/, casematch, true); + if (new_item_style != orig_str) { + vStyleToken = new_item_style; + } + g_free(orig_str); + g_free(replace_text); + } + } + } + } + } + + if (ret && _action_replace) { + Glib::ustring new_item_style; + for (const auto & vStyleToken : vStyleTokens) { + new_item_style.append(vStyleToken).append(";"); + } + new_item_style.erase(new_item_style.size()-1); + item->setAttribute("style", new_item_style); + } + + return ret; +} + + +std::vector<SPItem*> Find::filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch) +{ + Glib::ustring tmp = entry_find.getEntry()->get_text(); + if (tmp.empty()) { + return l; + } + gchar* text = g_strdup(tmp.c_str()); + + std::vector<SPItem*> in = l; + std::vector<SPItem*> out; + + if (check_searchin_text.get_active()) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_text_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_text_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + else if (check_searchin_property.get_active()) { + + bool ids = check_ids.get_active(); + bool style = check_style.get_active(); + bool font = check_font.get_active(); + bool attrname = check_attributename.get_active(); + bool attrvalue = check_attributevalue.get_active(); + + if (ids) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + if (item_id_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_id_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (style) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_style_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)){ + out.push_back(*i); + if (_action_replace) { + item_style_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrname) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_attr_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attr_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (attrvalue) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_attrvalue_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(), *i)) { + out.push_back(*i); + if (_action_replace) { + item_attrvalue_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + + if (font) { + for (std::vector<SPItem*>::const_reverse_iterator i=in.rbegin(); in.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_font_match(item, text, exact, casematch)) { + if (out.end()==find(out.begin(),out.end(),*i)) { + out.push_back(*i); + if (_action_replace) { + item_font_match(item, text, exact, casematch, _action_replace); + } + } + } + } + } + + } + + g_free(text); + + return out; +} + + +bool Find::item_type_match (SPItem *item) +{ + bool all =check_alltypes.get_active(); + + if ( dynamic_cast<SPRect *>(item)) { + return ( all ||check_rects.get_active()); + + } else if (dynamic_cast<SPGenericEllipse *>(item)) { + return ( all || check_ellipses.get_active()); + + } else if (dynamic_cast<SPStar *>(item) || dynamic_cast<SPPolygon *>(item)) { + return ( all || check_stars.get_active()); + + } else if (dynamic_cast<SPSpiral *>(item)) { + return ( all || check_spirals.get_active()); + + } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPLine *>(item) || dynamic_cast<SPPolyLine *>(item)) { + return (all || check_paths.get_active()); + + } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPTSpan *>(item) || + dynamic_cast<SPTRef *>(item) || dynamic_cast<SPString *>(item) || + dynamic_cast<SPFlowtext *>(item) || dynamic_cast<SPFlowdiv *>(item) || + dynamic_cast<SPFlowtspan *>(item) || dynamic_cast<SPFlowpara *>(item)) { + return (all || check_texts.get_active()); + + } else if (dynamic_cast<SPGroup *>(item) && !desktop->isLayer(item) ) { // never select layers! + return (all || check_groups.get_active()); + + } else if (dynamic_cast<SPUse *>(item)) { + return (all || check_clones.get_active()); + + } else if (dynamic_cast<SPImage *>(item)) { + return (all || check_images.get_active()); + + } else if (dynamic_cast<SPOffset *>(item)) { + return (all || check_offsets.get_active()); + } + + return false; +} + +std::vector<SPItem*> Find::filter_types (std::vector<SPItem*> &l) +{ + std::vector<SPItem*> n; + for (std::vector<SPItem*>::const_reverse_iterator i=l.rbegin(); l.rend() != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item_type_match(item)) { + n.push_back(*i); + } + } + return n; +} + + +std::vector<SPItem*> &Find::filter_list (std::vector<SPItem*> &l, bool exact, bool casematch) +{ + l = filter_types (l); + l = filter_fields (l, exact, casematch); + return l; +} + +std::vector<SPItem*> &Find::all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked) +{ + if (dynamic_cast<SPDefs *>(r)) { + return l; // we're not interested in items in defs + } + + if (!strcmp(r->getRepr()->name(), "svg:metadata")) { + return l; // we're not interested in metadata + } + + for (auto& child: r->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item && !child.cloned && !desktop->isLayer(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.insert(l.begin(),(SPItem*)&child); + } + } + l = all_items (&child, l, hidden, locked); + } + return l; +} + +std::vector<SPItem*> &Find::all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked) +{ + auto itemlist= s->items(); + for (auto i=boost::rbegin(itemlist); boost::rend(itemlist) != i; ++i) { + SPObject *obj = *i; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + if (item && !item->cloned && !desktop->isLayer(item)) { + if (!ancestor || ancestor->isAncestorOf(item)) { + if ((hidden || !desktop->itemIsHidden(item)) && (locked || !item->isLocked())) { + l.push_back(*i); + } + } + } + if (!ancestor || ancestor->isAncestorOf(item)) { + l = all_items(item, l, hidden, locked); + } + } + return l; +} + + + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +void Find::onFind() +{ + _action_replace = false; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onReplace() +{ + if (entry_find.getEntry()->get_text().length() < 1) { + status.set_text(_("Nothing to replace")); + return; + } + _action_replace = true; + onAction(); + + // Return focus to the find entry + entry_find.getEntry()->grab_focus(); +} + +void Find::onAction() +{ + + bool hidden = check_include_hidden.get_active(); + bool locked = check_include_locked.get_active(); + bool exact = check_exact_match.get_active(); + bool casematch = check_case_sensitive.get_active(); + blocked = true; + + std::vector<SPItem*> l; + if (check_scope_selection.get_active()) { + if (check_scope_layer.get_active()) { + l = all_selection_items (desktop->selection, l, desktop->currentLayer(), hidden, locked); + } else { + l = all_selection_items (desktop->selection, l, nullptr, hidden, locked); + } + } else { + if (check_scope_layer.get_active()) { + l = all_items (desktop->currentLayer(), l, hidden, locked); + } else { + l = all_items(desktop->getDocument()->getRoot(), l, hidden, locked); + } + } + guint all = l.size(); + + std::vector<SPItem*> n = filter_list (l, exact, casematch); + + if (!n.empty()) { + int count = n.size(); + desktop->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, + // TRANSLATORS: "%s" is replaced with "exact" or "partial" when this string is displayed + ngettext("<b>%d</b> object found (out of <b>%d</b>), %s match.", + "<b>%d</b> objects found (out of <b>%d</b>), %s match.", + count), + count, all, exact? _("exact") : _("partial")); + if (_action_replace){ + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 match replaced","%1 matches replaced",count), count)); + } + else { + // TRANSLATORS: "%1" is replaced with the number of matches + status.set_text(Glib::ustring::compose(ngettext("%1 object found","%1 objects found",count), count)); + bool attributenameyok = !check_attributename.get_active(); + button_replace.set_sensitive(attributenameyok); + } + + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(n); + SPObject *obj = n[0]; + SPItem *item = dynamic_cast<SPItem *>(obj); + g_assert(item != nullptr); + scroll_to_show_item(desktop, item); + + if (_action_replace) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Replace text or property")); + } + + } else { + status.set_text(_("Nothing found")); + if (!check_scope_selection.get_active()) { + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + } + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No objects found")); + } + blocked = false; + +} + +void Find::onToggleCheck () +{ + bool objectok = false; + status.set_text(""); + + if (check_alltypes.get_active()) { + objectok = true; + } + for (auto & checkType : checkTypes) { + if (checkType->get_active()) { + objectok = true; + } + } + + if (!objectok) { + status.set_text(_("Select an object type")); + } + + + bool propertyok = false; + + if (!check_searchin_property.get_active()) { + propertyok = true; + } else { + + for (auto & checkProperty : checkProperties) { + if (checkProperty->get_active()) { + propertyok = true; + } + } + } + + if (!propertyok) { + status.set_text(_("Select a property")); + } + + // Can't replace attribute names + // bool attributenameyok = !check_attributename.get_active(); + + button_find.set_sensitive(objectok && propertyok); + // button_replace.set_sensitive(objectok && propertyok && attributenameyok); + button_replace.set_sensitive(false); +} + +void Find::onToggleAlltypes () +{ + bool all =check_alltypes.get_active(); + for (auto & checkType : checkTypes) { + checkType->set_sensitive(!all); + } + + onToggleCheck(); +} + +void Find::onSearchinText () +{ + searchinToggle(false); + onToggleCheck(); +} + +void Find::onSearchinProperty () +{ + searchinToggle(true); + onToggleCheck(); +} + +void Find::searchinToggle(bool on) +{ + for (auto & checkProperty : checkProperties) { + checkProperty->set_sensitive(on); + } +} + +void Find::onExpander () +{ + if (!expander_options.get_expanded()) + squeeze_window(); +} + +/*######################################################################## +# UTILITY +########################################################################*/ + +void Find::squeeze_window() +{ + // TODO: resize dialog window when the expander is closed + // set_size_request(-1, -1); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/find.h b/src/ui/dialog/find.h new file mode 100644 index 0000000..0eeeee0 --- /dev/null +++ b/src/ui/dialog/find.h @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Find dialog + */ +/* Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_FIND_H +#define INKSCAPE_UI_DIALOG_FIND_H + +#include "ui/widget/panel.h" +#include "ui/widget/entry.h" +#include "ui/widget/frame.h" + +#include <gtkmm/box.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/expander.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/sizegroup.h> + +#include "ui/dialog/desktop-tracker.h" + +class SPItem; +class SPObject; + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Dialog { + +/** + * The Find class defines the Find and replace dialog. + * + * The Find and replace dialog allows you to search within the + * current document for specific text or properties of items. + * Matches items are highlighted and can be replaced as well. + * Scope can be limited to the entire document, current layer or selected items. + * Other options allow searching on specific object types and properties. + */ + +class Find : public UI::Widget::Panel { +public: + Find(); + ~Find() override; + + /** + * Helper function which returns a new instance of the dialog. + * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager). + */ + static Find &getInstance() { return *new Find(); } + +protected: + + + /** + * Callbacks for pressing the dialog buttons. + */ + void onFind(); + void onReplace(); + void onExpander(); + void onAction(); + void onToggleAlltypes(); + void onToggleCheck(); + void onSearchinText(); + void onSearchinProperty(); + + /** + * Toggle all the properties checkboxes + */ + void searchinToggle(bool on); + + /** + * Returns true if the SPItem 'item' has the same id + * + * @param item the SPItem to check + * @param id the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_id_match (SPItem *item, const gchar *id, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text content + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + * + */ + bool item_text_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same text in the style attribute + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_style_match (SPItem *item, const gchar *text, bool exact, bool casematch, bool replace=false); + /** + * Returns true if found the SPItem 'item' has the same attribute name + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attr_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same attribute value + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_attrvalue_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Returns true if the SPItem 'item' has the same font values + * + * @param item the SPItem to check + * @param name the value to compare with + * @param exact do an exact match + * @param casematch match the text case exactly + * @param replace replace the value if found + * + */ + bool item_font_match (SPItem *item, const gchar *name, bool exact, bool casematch, bool replace=false); + /** + * Function to filter a list of items based on the item type by calling each item_XXX_match function + */ + std::vector<SPItem*> filter_fields (std::vector<SPItem*> &l, bool exact, bool casematch); + bool item_type_match (SPItem *item); + std::vector<SPItem*> filter_types (std::vector<SPItem*> &l); + std::vector<SPItem*> & filter_list (std::vector<SPItem*> &l, bool exact, bool casematch); + + /** + * Find a string within a string and returns true if found with options for exact and casematching + */ + bool find_strcmp(const gchar *str, const gchar *find, bool exact, bool casematch); + + /** + * Find a string within a string and return the position with options for exact, casematching and search start location + */ + gsize find_strcmp_pos(const gchar *str, const gchar *find, bool exact, bool casematch, gsize start=0); + + /** + * Replace a string with another string with options for exact and casematching and replace once/all + */ + Glib::ustring find_replace(const gchar *str, const gchar *find, const gchar *replace, bool exact, bool casematch, bool replaceall); + + /** + * recursive function to return a list of all the items in the SPObject tree + * + */ + std::vector<SPItem*> & all_items (SPObject *r, std::vector<SPItem*> &l, bool hidden, bool locked); + /** + * to return a list of all the selected items + * + */ + std::vector<SPItem*> & all_selection_items (Inkscape::Selection *s, std::vector<SPItem*> &l, SPObject *ancestor, bool hidden, bool locked); + + /** + * Shrink the dialog size when the expander widget is closed + * Currently not working, no known way to do this + */ + void squeeze_window(); + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + /** + * Called when desktop selection changes + */ + void onSelectionChange(); + +private: + Find(Find const &d) = delete; + Find& operator=(Find const &d) = delete; + + /* + * Find and replace combo box widgets + */ + UI::Widget::Entry entry_find; + UI::Widget::Entry entry_replace; + + /** + * Scope and search in widgets + */ + Gtk::RadioButton check_scope_all; + Gtk::RadioButton check_scope_layer; + Gtk::RadioButton check_scope_selection; + Gtk::RadioButton check_searchin_text; + Gtk::RadioButton check_searchin_property; + Gtk::HBox hbox_searchin; + Gtk::VBox vbox_scope; + Gtk::VBox vbox_searchin; + UI::Widget::Frame frame_searchin; + UI::Widget::Frame frame_scope; + + /** + * General option widgets + */ + Gtk::CheckButton check_case_sensitive; + Gtk::CheckButton check_exact_match; + Gtk::CheckButton check_include_hidden; + Gtk::CheckButton check_include_locked; + Gtk::VBox vbox_options1; + Gtk::VBox vbox_options2; + Gtk::HBox hbox_options; + Gtk::VBox vbox_expander; + Gtk::Expander expander_options; + UI::Widget::Frame frame_options; + + /** + * Property type widgets + */ + Gtk::CheckButton check_ids; + Gtk::CheckButton check_attributename; + Gtk::CheckButton check_attributevalue; + Gtk::CheckButton check_style; + Gtk::CheckButton check_font; + Gtk::HBox hbox_properties; + Gtk::VBox vbox_properties1; + Gtk::VBox vbox_properties2; + UI::Widget::Frame frame_properties; + + /** + * A vector of all the properties widgets for easy processing + */ + std::vector<Gtk::CheckButton *> checkProperties; + + /** + * Object type widgets + */ + Gtk::CheckButton check_alltypes; + Gtk::CheckButton check_rects; + Gtk::CheckButton check_ellipses; + Gtk::CheckButton check_stars; + Gtk::CheckButton check_spirals; + Gtk::CheckButton check_paths; + Gtk::CheckButton check_texts; + Gtk::CheckButton check_groups; + Gtk::CheckButton check_clones; + Gtk::CheckButton check_images; + Gtk::CheckButton check_offsets; + Gtk::VBox vbox_types1; + Gtk::VBox vbox_types2; + Gtk::HBox hbox_types; + UI::Widget::Frame frame_types; + + Glib::RefPtr<Gtk::SizeGroup> _left_size_group; + Glib::RefPtr<Gtk::SizeGroup> _right_size_group; + + /** + * A vector of all the check option widgets for easy processing + */ + std::vector<Gtk::CheckButton *> checkTypes; + + //Gtk::HBox hbox_text; + + /** + * Action Buttons and status + */ + Gtk::Label status; + Gtk::Button button_find; + Gtk::Button button_replace; + Gtk::ButtonBox box_buttons; + Gtk::HBox hboxbutton_row; + + /** + * Finding or replacing + */ + bool _action_replace; + bool blocked; + + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FIND_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/floating-behavior.cpp b/src/ui/dialog/floating-behavior.cpp new file mode 100644 index 0000000..804f1f0 --- /dev/null +++ b/src/ui/dialog/floating-behavior.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Floating dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/dialog.h> +#include <glibmm/main.h> + +#include "floating-behavior.h" +#include "dialog.h" + +#include "inkscape.h" +#include "desktop.h" +#include "ui/dialog-events.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +FloatingBehavior::FloatingBehavior(Dialog &dialog) : + Behavior(dialog), + _d (new Gtk::Dialog(_dialog._title)) + ,_dialog_active(_d->property_is_active()) + ,_trans_focus(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-focus", 0.95, 0.0, 1.0)) + ,_trans_blur(Inkscape::Preferences::get()->getDoubleLimited("/dialogs/transparency/on-blur", 0.50, 0.0, 1.0)) + ,_trans_time(Inkscape::Preferences::get()->getIntLimited("/dialogs/transparency/animate-time", 100, 0, 5000)) +{ + hide(); + + signal_delete_event().connect(sigc::mem_fun(_dialog, &Inkscape::UI::Dialog::Dialog::_onDeleteEvent)); + + sp_transientize(GTK_WIDGET(_d->gobj())); + _dialog.retransientize_suppress = false; +} + +FloatingBehavior::~FloatingBehavior() +{ + delete _d; + _d = nullptr; +} + +Behavior * +FloatingBehavior::create(Dialog &dialog) +{ + return new FloatingBehavior(dialog); +} + +inline FloatingBehavior::operator Gtk::Widget &() { return *_d; } +inline GtkWidget *FloatingBehavior::gobj() { return GTK_WIDGET(_d->gobj()); } +inline Gtk::Box* FloatingBehavior::get_vbox() { + return _d->get_content_area(); +} +inline void FloatingBehavior::present() { _d->present(); } +inline void FloatingBehavior::hide() { _d->hide(); } +inline void FloatingBehavior::show() { _d->show(); } +inline void FloatingBehavior::show_all_children() { _d->show_all_children(); } +inline void FloatingBehavior::resize(int width, int height) { _d->resize(width, height); } +inline void FloatingBehavior::move(int x, int y) { _d->move(x, y); } +inline void FloatingBehavior::set_position(Gtk::WindowPosition position) { _d->set_position(position); } +inline void FloatingBehavior::set_size_request(int width, int height) { _d->set_size_request(width, height); } +inline void FloatingBehavior::size_request(Gtk::Requisition &requisition) { + Gtk::Requisition requisition_natural; + _d->get_preferred_size(requisition, requisition_natural); +} +inline void FloatingBehavior::get_position(int &x, int &y) { _d->get_position(x, y); } +inline void FloatingBehavior::get_size(int &width, int &height) { _d->get_size(width, height); } +inline void FloatingBehavior::set_title(Glib::ustring title) { _d->set_title(title); } +inline void FloatingBehavior::set_sensitive(bool sensitive) { _d->set_sensitive(sensitive); } + +Glib::SignalProxy0<void> FloatingBehavior::signal_show() { return _d->signal_show(); } +Glib::SignalProxy0<void> FloatingBehavior::signal_hide() { return _d->signal_hide(); } +Glib::SignalProxy1<bool, GdkEventAny *> FloatingBehavior::signal_delete_event () { return _d->signal_delete_event(); } + + +void +FloatingBehavior::onHideF12() +{ + _dialog.save_geometry(); + hide(); +} + +void +FloatingBehavior::onShowF12() +{ + show(); + _dialog.read_geometry(); +} + +void +FloatingBehavior::onShutdown() +{ + _dialog.save_status(!_dialog._user_hidden, 1, GDL_DOCK_TOP); // Make sure 1 == DockItem::FLOATING_STATE +} + +void +FloatingBehavior::onDesktopActivated (SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint transient_policy = prefs->getIntLimited("/options/transientpolicy/value", 1, 0, 2); + +#ifdef _WIN32 // Win32 special code to enable transient dialogs + transient_policy = 2; +#endif + + if (!transient_policy) + return; + + GtkWindow *dialog_win = GTK_WINDOW(_d->gobj()); + + if (_dialog.retransientize_suppress) { + /* if retransientizing of this dialog is still forbidden after + * previous call warning turned off because it was confusingly fired + * when loading many files from command line + */ + + // g_warning("Retranzientize aborted! You're switching windows too fast!"); + return; + } + + if (dialog_win) + { + _dialog.retransientize_suppress = true; // disallow other attempts to retranzientize this dialog + + desktop->setWindowTransient (dialog_win); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2 && ! _dialog._hiddenF12 && !_dialog._user_hidden) { + // without this, a transient window not always emerges on top + gtk_window_present (dialog_win); + } + } + + // we're done, allow next retransientizing not sooner than after 120 msec + g_timeout_add (120, (GSourceFunc) sp_retransientize_again, (gpointer) &_dialog); +} + + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/floating-behavior.h b/src/ui/dialog/floating-behavior.h new file mode 100644 index 0000000..d4e9ebe --- /dev/null +++ b/src/ui/dialog/floating-behavior.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A floating dialog implementation. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H +#define INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H + +#include <glibmm/property.h> +#include "behavior.h" + +namespace Gtk { +class Dialog; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { +namespace Behavior { + +class FloatingBehavior : public Behavior { + +public: + static Behavior *create(Dialog &dialog); + + ~FloatingBehavior() override; + + /** Gtk::Dialog methods */ + operator Gtk::Widget &() override; + GtkWidget *gobj() override; + void present() override; + Gtk::Box *get_vbox() override; + void show() override; + void hide() override; + void show_all_children() override; + void resize(int width, int height) override; + void move(int x, int y) override; + void set_position(Gtk::WindowPosition) override; + void set_size_request(int width, int height) override; + void size_request(Gtk::Requisition &requisition) override; + void get_position(int &x, int &y) override; + void get_size(int& width, int &height) override; + void set_title(Glib::ustring title) override; + void set_sensitive(bool sensitive) override; + + /** Gtk::Dialog signal proxies */ + Glib::SignalProxy0<void> signal_show() override; + Glib::SignalProxy0<void> signal_hide() override; + Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event() override; + + /** Custom signal handlers */ + void onHideF12() override; + void onShowF12() override; + void onDesktopActivated(SPDesktop *desktop) override; + void onShutdown() override; + +private: + FloatingBehavior(Dialog& dialog); + + Gtk::Dialog *_d; //< the actual dialog + + Glib::PropertyProxy_ReadOnly<bool> _dialog_active; //< Variable proxy to track whether the dialog is the active window + float _trans_focus; //< The percentage opacity when the dialog is focused + float _trans_blur; //< The percentage opactiy when the dialog is not focused + int _trans_time; //< The amount of time (in ms) for the dialog to change it's transparency +}; + +} // namespace Behavior +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_FLOATING_BEHAVIOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/font-substitution.cpp b/src/ui/dialog/font-substitution.cpp new file mode 100644 index 0000000..bf91259 --- /dev/null +++ b/src/ui/dialog/font-substitution.cpp @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <set> + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include <gtkmm/messagedialog.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> + +#include "font-substitution.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "text-editing.h" + +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" +#include "object/sp-tspan.h" + +#include "libnrtype/FontFactory.h" +#include "libnrtype/font-instance.h" + +#include "ui/dialog-events.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +FontSubstitution::FontSubstitution() += default; + +FontSubstitution::~FontSubstitution() += default; + +void +FontSubstitution::checkFontSubstitutions(SPDocument* doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int show_dlg = prefs->getInt("/options/font/substitutedlg", 0); + if (show_dlg) { + Glib::ustring out; + std::vector<SPItem*> l = getFontReplacedItems(doc, &out); + if (out.length() > 0) { + show(out, l); + } + } +} + +void +FontSubstitution::show(Glib::ustring out, std::vector<SPItem*> &l) +{ + Gtk::MessageDialog warning(_("\nSome fonts are not available and have been substituted."), + false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK, true); + warning.set_resizable(true); + warning.set_title(_("Font substitution")); + + GtkWidget *dlg = GTK_WIDGET(warning.gobj()); + sp_transientize(dlg); + + Gtk::TextView * textview = new Gtk::TextView(); + textview->set_editable(false); + textview->set_wrap_mode(Gtk::WRAP_WORD); + textview->show(); + textview->get_buffer()->set_text(_(out.c_str())); + + Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow(); + scrollwindow->add(*textview); + scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollwindow->set_shadow_type(Gtk::SHADOW_IN); + scrollwindow->set_size_request(0, 100); + scrollwindow->show(); + + Gtk::CheckButton *cbSelect = new Gtk::CheckButton(); + cbSelect->set_label(_("Select all the affected items")); + cbSelect->set_active(true); + cbSelect->show(); + + Gtk::CheckButton *cbWarning = new Gtk::CheckButton(); + cbWarning->set_label(_("Don't show this warning again")); + cbWarning->show(); + + auto box = warning.get_content_area(); + box->set_spacing(2); + box->pack_start(*scrollwindow, true, true, 4); + box->pack_start(*cbSelect, false, false, 0); + box->pack_start(*cbWarning, false, false, 0); + + warning.run(); + + if (cbWarning->get_active()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/options/font/substitutedlg", 0); + } + + if (cbSelect->get_active()) { + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->setList(l); + } + +} + +/* + * Find all the fonts that are in the document but not available on the users system + * and have been substituted for other fonts + * + * Return a list of SPItems where fonts have been substituted. + * + * Walk through all the objects ... + * a. Build up a list of the objects with fonts defined in the style attribute + * b. Build up a list of the objects rendered fonts - taken for the objects layout/spans + * If there are fonts in a. that are not in b. then those fonts have been substituted. + */ +std::vector<SPItem*> FontSubstitution::getFontReplacedItems(SPDocument* doc, Glib::ustring *out) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + std::vector<SPItem*> allList; + std::vector<SPItem*> outList,x,y; + std::set<Glib::ustring> setErrors; + std::set<Glib::ustring> setFontSpans; + std::map<SPItem *, Glib::ustring> mapFontStyles; + + allList = get_all_items(x, doc->getRoot(), desktop, false, false, true, y); + for(auto item : allList){ + SPStyle *style = item->style; + Glib::ustring family = ""; + + if (is_top_level_text_object (item)) { + // Should only need to check the first span, since the others should be covered by TSPAN's etc + family = te_get_layout(item)->getFontFamily(0); + setFontSpans.insert(family); + } + else if (SP_IS_TEXTPATH(item)) { + SPTextPath const *textpath = SP_TEXTPATH(item); + if (textpath->originalPath != nullptr) { + family = SP_TEXT(item->parent)->layout.getFontFamily(0); + setFontSpans.insert(family); + } + } + else if (SP_IS_TSPAN(item) || SP_IS_FLOWTSPAN(item)) { + // is_part_of_text_subtree (item) + // TSPAN layout comes from the parent->layout->_spans + SPObject *parent_text = item; + while (parent_text && !SP_IS_TEXT(parent_text)) { + parent_text = parent_text->parent; + } + if (parent_text != nullptr) { + family = SP_TEXT(parent_text)->layout.getFontFamily(0); + // Add all the spans fonts to the set + for (unsigned int f=0; f < parent_text->children.size(); f++) { + family = SP_TEXT(parent_text)->layout.getFontFamily(f); + setFontSpans.insert(family); + } + } + } + + if (style) { + gchar const *style_font = nullptr; + if (style->font_family.set) + style_font = style->font_family.value(); + else if (style->font_specification.set) + style_font = style->font_specification.value(); + else + style_font = style->font_family.value(); + + if (style_font) { + if (has_visible_text(item)) { + mapFontStyles.insert(std::make_pair (item, style_font)); + } + } + } + } + + // Check if any document styles are not in the actual layout + std::map<SPItem *, Glib::ustring>::const_reverse_iterator mapIter; + for (mapIter = mapFontStyles.rbegin(); mapIter != mapFontStyles.rend(); ++mapIter) { + SPItem *item = mapIter->first; + Glib::ustring fonts = mapIter->second; + + // CSS font fallbacks can have more that one font listed, split the font list + std::vector<Glib::ustring> vFonts = Glib::Regex::split_simple("," , fonts); + bool fontFound = false; + for(auto font : vFonts) { + // trim whitespace + size_t startpos = font.find_first_not_of(" \n\r\t"); + size_t endpos = font.find_last_not_of(" \n\r\t"); + if(( std::string::npos == startpos ) || ( std::string::npos == endpos)) { + continue; // empty font name + } + font = font.substr( startpos, endpos-startpos+1 ); + std::set<Glib::ustring>::const_iterator iter = setFontSpans.find(font); + if (iter != setFontSpans.end() || + font == Glib::ustring("sans-serif") || + font == Glib::ustring("Sans") || + font == Glib::ustring("serif") || + font == Glib::ustring("Serif") || + font == Glib::ustring("monospace") || + font == Glib::ustring("Monospace")) { + fontFound = true; + break; + } + } + if (fontFound == false) { + Glib::ustring subName = getSubstituteFontName(fonts); + Glib::ustring err = Glib::ustring::compose( + _("Font '%1' substituted with '%2'"), fonts.c_str(), subName.c_str()); + setErrors.insert(err); + outList.push_back(item); + } + } + + std::set<Glib::ustring>::const_iterator setIter; + for (setIter = setErrors.begin(); setIter != setErrors.end(); ++setIter) { + Glib::ustring err = (*setIter); + out->append(err + "\n"); + g_warning("%s", err.c_str()); + } + + return outList; +} + + +Glib::ustring FontSubstitution::getSubstituteFontName (Glib::ustring font) +{ + Glib::ustring out = font; + + PangoFontDescription *descr = pango_font_description_new(); + pango_font_description_set_family(descr,font.c_str()); + font_instance *res = (font_factory::Default())->Face(descr); + if (res->pFont) { + PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont); + out = sp_font_description_get_family(nFaceDesc); + } + pango_font_description_free(descr); + + return out; +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/font-substitution.h b/src/ui/dialog/font-substitution.h new file mode 100644 index 0000000..107c534 --- /dev/null +++ b/src/ui/dialog/font-substitution.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief FontSubstitution dialog + */ +/* Authors: + * + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_FONT_SUBSTITUTION_H +#define INKSCAPE_UI_FONT_SUBSTITUTION_H + +#include <glibmm/ustring.h> + +class SPItem; +class SPDocument; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class FontSubstitution { +public: + FontSubstitution(); + virtual ~FontSubstitution(); + void checkFontSubstitutions(SPDocument* doc); + void show(Glib::ustring out, std::vector<SPItem*> &l); + + static FontSubstitution &getInstance() { return *new FontSubstitution(); } + Glib::ustring getSubstituteFontName (Glib::ustring font); + +protected: + std::vector<SPItem*> getFontReplacedItems(SPDocument* doc, Glib::ustring *out); + +private: + FontSubstitution(FontSubstitution const &d) = delete; + FontSubstitution& operator=(FontSubstitution const &d) = delete; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_FONT_SUBSTITUTION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/glyphs.cpp b/src/ui/dialog/glyphs.cpp new file mode 100644 index 0000000..0603f48 --- /dev/null +++ b/src/ui/dialog/glyphs.cpp @@ -0,0 +1,822 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * Abhishek Sharma + * Tavmjong Bah + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include "glyphs.h" + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/grid.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" // for SPDocumentUndo::done() +#include "selection.h" +#include "text-editing.h" +#include "verbs.h" + +#include "libnrtype/font-instance.h" +#include "libnrtype/font-lister.h" + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" + +#include "ui/widget/font-selector.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +GlyphsPanel &GlyphsPanel::getInstance() +{ + return *new GlyphsPanel(); +} + + +static std::map<GUnicodeScript, Glib::ustring> & getScriptToName() +{ + static bool init = false; + static std::map<GUnicodeScript, Glib::ustring> mappings; + if (!init) { + init = true; + mappings[G_UNICODE_SCRIPT_INVALID_CODE] = _("all"); + mappings[G_UNICODE_SCRIPT_COMMON] = _("common"); + mappings[G_UNICODE_SCRIPT_INHERITED] = _("inherited"); + mappings[G_UNICODE_SCRIPT_ARABIC] = _("Arabic"); + mappings[G_UNICODE_SCRIPT_ARMENIAN] = _("Armenian"); + mappings[G_UNICODE_SCRIPT_BENGALI] = _("Bengali"); + mappings[G_UNICODE_SCRIPT_BOPOMOFO] = _("Bopomofo"); + mappings[G_UNICODE_SCRIPT_CHEROKEE] = _("Cherokee"); + mappings[G_UNICODE_SCRIPT_COPTIC] = _("Coptic"); + mappings[G_UNICODE_SCRIPT_CYRILLIC] = _("Cyrillic"); + mappings[G_UNICODE_SCRIPT_DESERET] = _("Deseret"); + mappings[G_UNICODE_SCRIPT_DEVANAGARI] = _("Devanagari"); + mappings[G_UNICODE_SCRIPT_ETHIOPIC] = _("Ethiopic"); + mappings[G_UNICODE_SCRIPT_GEORGIAN] = _("Georgian"); + mappings[G_UNICODE_SCRIPT_GOTHIC] = _("Gothic"); + mappings[G_UNICODE_SCRIPT_GREEK] = _("Greek"); + mappings[G_UNICODE_SCRIPT_GUJARATI] = _("Gujarati"); + mappings[G_UNICODE_SCRIPT_GURMUKHI] = _("Gurmukhi"); + mappings[G_UNICODE_SCRIPT_HAN] = _("Han"); + mappings[G_UNICODE_SCRIPT_HANGUL] = _("Hangul"); + mappings[G_UNICODE_SCRIPT_HEBREW] = _("Hebrew"); + mappings[G_UNICODE_SCRIPT_HIRAGANA] = _("Hiragana"); + mappings[G_UNICODE_SCRIPT_KANNADA] = _("Kannada"); + mappings[G_UNICODE_SCRIPT_KATAKANA] = _("Katakana"); + mappings[G_UNICODE_SCRIPT_KHMER] = _("Khmer"); + mappings[G_UNICODE_SCRIPT_LAO] = _("Lao"); + mappings[G_UNICODE_SCRIPT_LATIN] = _("Latin"); + mappings[G_UNICODE_SCRIPT_MALAYALAM] = _("Malayalam"); + mappings[G_UNICODE_SCRIPT_MONGOLIAN] = _("Mongolian"); + mappings[G_UNICODE_SCRIPT_MYANMAR] = _("Myanmar"); + mappings[G_UNICODE_SCRIPT_OGHAM] = _("Ogham"); + mappings[G_UNICODE_SCRIPT_OLD_ITALIC] = _("Old Italic"); + mappings[G_UNICODE_SCRIPT_ORIYA] = _("Oriya"); + mappings[G_UNICODE_SCRIPT_RUNIC] = _("Runic"); + mappings[G_UNICODE_SCRIPT_SINHALA] = _("Sinhala"); + mappings[G_UNICODE_SCRIPT_SYRIAC] = _("Syriac"); + mappings[G_UNICODE_SCRIPT_TAMIL] = _("Tamil"); + mappings[G_UNICODE_SCRIPT_TELUGU] = _("Telugu"); + mappings[G_UNICODE_SCRIPT_THAANA] = _("Thaana"); + mappings[G_UNICODE_SCRIPT_THAI] = _("Thai"); + mappings[G_UNICODE_SCRIPT_TIBETAN] = _("Tibetan"); + mappings[G_UNICODE_SCRIPT_CANADIAN_ABORIGINAL] = _("Canadian Aboriginal"); + mappings[G_UNICODE_SCRIPT_YI] = _("Yi"); + mappings[G_UNICODE_SCRIPT_TAGALOG] = _("Tagalog"); + mappings[G_UNICODE_SCRIPT_HANUNOO] = _("Hanunoo"); + mappings[G_UNICODE_SCRIPT_BUHID] = _("Buhid"); + mappings[G_UNICODE_SCRIPT_TAGBANWA] = _("Tagbanwa"); + mappings[G_UNICODE_SCRIPT_BRAILLE] = _("Braille"); + mappings[G_UNICODE_SCRIPT_CYPRIOT] = _("Cypriot"); + mappings[G_UNICODE_SCRIPT_LIMBU] = _("Limbu"); + mappings[G_UNICODE_SCRIPT_OSMANYA] = _("Osmanya"); + mappings[G_UNICODE_SCRIPT_SHAVIAN] = _("Shavian"); + mappings[G_UNICODE_SCRIPT_LINEAR_B] = _("Linear B"); + mappings[G_UNICODE_SCRIPT_TAI_LE] = _("Tai Le"); + mappings[G_UNICODE_SCRIPT_UGARITIC] = _("Ugaritic"); + mappings[G_UNICODE_SCRIPT_NEW_TAI_LUE] = _("New Tai Lue"); + mappings[G_UNICODE_SCRIPT_BUGINESE] = _("Buginese"); + mappings[G_UNICODE_SCRIPT_GLAGOLITIC] = _("Glagolitic"); + mappings[G_UNICODE_SCRIPT_TIFINAGH] = _("Tifinagh"); + mappings[G_UNICODE_SCRIPT_SYLOTI_NAGRI] = _("Syloti Nagri"); + mappings[G_UNICODE_SCRIPT_OLD_PERSIAN] = _("Old Persian"); + mappings[G_UNICODE_SCRIPT_KHAROSHTHI] = _("Kharoshthi"); + mappings[G_UNICODE_SCRIPT_UNKNOWN] = _("unassigned"); + mappings[G_UNICODE_SCRIPT_BALINESE] = _("Balinese"); + mappings[G_UNICODE_SCRIPT_CUNEIFORM] = _("Cuneiform"); + mappings[G_UNICODE_SCRIPT_PHOENICIAN] = _("Phoenician"); + mappings[G_UNICODE_SCRIPT_PHAGS_PA] = _("Phags-pa"); + mappings[G_UNICODE_SCRIPT_NKO] = _("N'Ko"); + mappings[G_UNICODE_SCRIPT_KAYAH_LI] = _("Kayah Li"); + mappings[G_UNICODE_SCRIPT_LEPCHA] = _("Lepcha"); + mappings[G_UNICODE_SCRIPT_REJANG] = _("Rejang"); + mappings[G_UNICODE_SCRIPT_SUNDANESE] = _("Sundanese"); + mappings[G_UNICODE_SCRIPT_SAURASHTRA] = _("Saurashtra"); + mappings[G_UNICODE_SCRIPT_CHAM] = _("Cham"); + mappings[G_UNICODE_SCRIPT_OL_CHIKI] = _("Ol Chiki"); + mappings[G_UNICODE_SCRIPT_VAI] = _("Vai"); + mappings[G_UNICODE_SCRIPT_CARIAN] = _("Carian"); + mappings[G_UNICODE_SCRIPT_LYCIAN] = _("Lycian"); + mappings[G_UNICODE_SCRIPT_LYDIAN] = _("Lydian"); + mappings[G_UNICODE_SCRIPT_AVESTAN] = _("Avestan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BAMUM] = _("Bamum"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_EGYPTIAN_HIEROGLYPHS] = _("Egyptian Hieroglpyhs"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_IMPERIAL_ARAMAIC] = _("Imperial Aramaic"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PAHLAVI]= _("Inscriptional Pahlavi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_INSCRIPTIONAL_PARTHIAN]= _("Inscriptional Parthian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_JAVANESE] = _("Javanese"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_KAITHI] = _("Kaithi"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_LISU] = _("Lisu"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_MEETEI_MAYEK] = _("Meetei Mayek"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_SOUTH_ARABIAN] = _("Old South Arabian"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_OLD_TURKIC] = _("Old Turkic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_SAMARITAN] = _("Samaritan"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_THAM] = _("Tai Tham"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_TAI_VIET] = _("Tai Viet"); // Since: 2.26 + mappings[G_UNICODE_SCRIPT_BATAK] = _("Batak"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_BRAHMI] = _("Brahmi"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_MANDAIC] = _("Mandaic"); // Since: 2.28 + mappings[G_UNICODE_SCRIPT_CHAKMA] = _("Chakma"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_CURSIVE] = _("Meroitic Cursive"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MEROITIC_HIEROGLYPHS] = _("Meroitic Hieroglyphs"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_MIAO] = _("Miao"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SHARADA] = _("Sharada"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_SORA_SOMPENG] = _("Sora Sompeng"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_TAKRI] = _("Takri"); // Since: 2.32 + mappings[G_UNICODE_SCRIPT_BASSA_VAH] = _("Bassa"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_CAUCASIAN_ALBANIAN] = _("Caucasian Albanian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_DUPLOYAN] = _("Duployan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_ELBASAN] = _("Elbasan"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_GRANTHA] = _("Grantha"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHOJKI] = _("Khojki"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_KHUDAWADI] = _("Khudawadi, Sindhi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_LINEAR_A] = _("Linear A"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MAHAJANI] = _("Mahajani"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MANICHAEAN] = _("Manichaean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MENDE_KIKAKUI] = _("Mende Kikakui"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MODI] = _("Modi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_MRO] = _("Mro"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_NABATAEAN] = _("Nabataean"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_NORTH_ARABIAN] = _("Old North Arabian"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_OLD_PERMIC] = _("Old Permic"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAHAWH_HMONG] = _("Pahawh Hmong"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PALMYRENE] = _("Palmyrene"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PAU_CIN_HAU] = _("Pau Cin Hau"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_PSALTER_PAHLAVI] = _("Psalter Pahlavi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_SIDDHAM] = _("Siddham"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_TIRHUTA] = _("Tirhuta"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_WARANG_CITI] = _("Warang Citi"); // Since: 2.42 + mappings[G_UNICODE_SCRIPT_AHOM] = _("Ahom"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_ANATOLIAN_HIEROGLYPHS]= _("Anatolian Hieroglyphs"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_HATRAN] = _("Hatran"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_MULTANI] = _("Multani"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_OLD_HUNGARIAN] = _("Old Hungarian"); // Since: 2.48 + mappings[G_UNICODE_SCRIPT_SIGNWRITING] = _("Signwriting"); // Since: 2.48 +/* + mappings[G_UNICODE_SCRIPT_ADLAM] = _("Adlam"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_BHAIKSUKI] = _("Bhaiksuki"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MARCHEN] = _("Marchen"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_NEWA] = _("Newa"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_OSAGE] = _("Osage"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_TANGUT] = _("Tangut"); // Since: 2.50 + mappings[G_UNICODE_SCRIPT_MASARAM_GONDI] = _("Masaram Gondi"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_NUSHU] = _("Nushu"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_SOYOMBO] = _("Soyombo"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_ZANABAZAR_SQUARE] = _("Zanabazar Square"); // Since: 2.54 + mappings[G_UNICODE_SCRIPT_DOGRA] = _("Dogra"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_GUNJALA_GONDI] = _("Gunjala Gondi"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_HANIFI_ROHINGYA] = _("Hanifi Rohingya"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MAKASAR] = _("Makasar"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_MEDEFAIDRIN] = _("Medefaidrin"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_OLD_SOGDIAN] = _("Old Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_SOGDIAN] = _("Sogdian"); // Since: 2.58 + mappings[G_UNICODE_SCRIPT_ELYMAIC] = _("Elym"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NANDINAGARI] = _("Nand"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_NYIAKENG_PUACHUE_HMONG]= _("Rohg"); // Since: 2.62 + mappings[G_UNICODE_SCRIPT_WANCHO] = _("Wcho"); // Since: 2.62 +*/ + } + return mappings; +} + +typedef std::pair<gunichar, gunichar> Range; +typedef std::pair<Range, Glib::ustring> NamedRange; + +static std::vector<NamedRange> & getRanges() +{ + static bool init = false; + static std::vector<NamedRange> ranges; + if (!init) { + init = true; + ranges.emplace_back(std::make_pair(0x00000, 0x2FFFF), _("all")); + ranges.emplace_back(std::make_pair(0x00000, 0x0FFFF), _("Basic Plane")); + ranges.emplace_back(std::make_pair(0x10000, 0x1FFFF), _("Extended Multilingual Plane")); + ranges.emplace_back(std::make_pair(0x20000, 0x2FFFF), _("Supplementary Ideographic Plane")); + + ranges.emplace_back(std::make_pair(0x0000, 0x007F), _("Basic Latin")); + ranges.emplace_back(std::make_pair(0x0080, 0x00FF), _("Latin-1 Supplement")); + ranges.emplace_back(std::make_pair(0x0100, 0x017F), _("Latin Extended-A")); + ranges.emplace_back(std::make_pair(0x0180, 0x024F), _("Latin Extended-B")); + ranges.emplace_back(std::make_pair(0x0250, 0x02AF), _("IPA Extensions")); + ranges.emplace_back(std::make_pair(0x02B0, 0x02FF), _("Spacing Modifier Letters")); + ranges.emplace_back(std::make_pair(0x0300, 0x036F), _("Combining Diacritical Marks")); + ranges.emplace_back(std::make_pair(0x0370, 0x03FF), _("Greek and Coptic")); + ranges.emplace_back(std::make_pair(0x0400, 0x04FF), _("Cyrillic")); + ranges.emplace_back(std::make_pair(0x0500, 0x052F), _("Cyrillic Supplement")); + ranges.emplace_back(std::make_pair(0x0530, 0x058F), _("Armenian")); + ranges.emplace_back(std::make_pair(0x0590, 0x05FF), _("Hebrew")); + ranges.emplace_back(std::make_pair(0x0600, 0x06FF), _("Arabic")); + ranges.emplace_back(std::make_pair(0x0700, 0x074F), _("Syriac")); + ranges.emplace_back(std::make_pair(0x0750, 0x077F), _("Arabic Supplement")); + ranges.emplace_back(std::make_pair(0x0780, 0x07BF), _("Thaana")); + ranges.emplace_back(std::make_pair(0x07C0, 0x07FF), _("NKo")); + ranges.emplace_back(std::make_pair(0x0800, 0x083F), _("Samaritan")); + ranges.emplace_back(std::make_pair(0x0900, 0x097F), _("Devanagari")); + ranges.emplace_back(std::make_pair(0x0980, 0x09FF), _("Bengali")); + ranges.emplace_back(std::make_pair(0x0A00, 0x0A7F), _("Gurmukhi")); + ranges.emplace_back(std::make_pair(0x0A80, 0x0AFF), _("Gujarati")); + ranges.emplace_back(std::make_pair(0x0B00, 0x0B7F), _("Oriya")); + ranges.emplace_back(std::make_pair(0x0B80, 0x0BFF), _("Tamil")); + ranges.emplace_back(std::make_pair(0x0C00, 0x0C7F), _("Telugu")); + ranges.emplace_back(std::make_pair(0x0C80, 0x0CFF), _("Kannada")); + ranges.emplace_back(std::make_pair(0x0D00, 0x0D7F), _("Malayalam")); + ranges.emplace_back(std::make_pair(0x0D80, 0x0DFF), _("Sinhala")); + ranges.emplace_back(std::make_pair(0x0E00, 0x0E7F), _("Thai")); + ranges.emplace_back(std::make_pair(0x0E80, 0x0EFF), _("Lao")); + ranges.emplace_back(std::make_pair(0x0F00, 0x0FFF), _("Tibetan")); + ranges.emplace_back(std::make_pair(0x1000, 0x109F), _("Myanmar")); + ranges.emplace_back(std::make_pair(0x10A0, 0x10FF), _("Georgian")); + ranges.emplace_back(std::make_pair(0x1100, 0x11FF), _("Hangul Jamo")); + ranges.emplace_back(std::make_pair(0x1200, 0x137F), _("Ethiopic")); + ranges.emplace_back(std::make_pair(0x1380, 0x139F), _("Ethiopic Supplement")); + ranges.emplace_back(std::make_pair(0x13A0, 0x13FF), _("Cherokee")); + ranges.emplace_back(std::make_pair(0x1400, 0x167F), _("Unified Canadian Aboriginal Syllabics")); + ranges.emplace_back(std::make_pair(0x1680, 0x169F), _("Ogham")); + ranges.emplace_back(std::make_pair(0x16A0, 0x16FF), _("Runic")); + ranges.emplace_back(std::make_pair(0x1700, 0x171F), _("Tagalog")); + ranges.emplace_back(std::make_pair(0x1720, 0x173F), _("Hanunoo")); + ranges.emplace_back(std::make_pair(0x1740, 0x175F), _("Buhid")); + ranges.emplace_back(std::make_pair(0x1760, 0x177F), _("Tagbanwa")); + ranges.emplace_back(std::make_pair(0x1780, 0x17FF), _("Khmer")); + ranges.emplace_back(std::make_pair(0x1800, 0x18AF), _("Mongolian")); + ranges.emplace_back(std::make_pair(0x18B0, 0x18FF), _("Unified Canadian Aboriginal Syllabics Extended")); + ranges.emplace_back(std::make_pair(0x1900, 0x194F), _("Limbu")); + ranges.emplace_back(std::make_pair(0x1950, 0x197F), _("Tai Le")); + ranges.emplace_back(std::make_pair(0x1980, 0x19DF), _("New Tai Lue")); + ranges.emplace_back(std::make_pair(0x19E0, 0x19FF), _("Khmer Symbols")); + ranges.emplace_back(std::make_pair(0x1A00, 0x1A1F), _("Buginese")); + ranges.emplace_back(std::make_pair(0x1A20, 0x1AAF), _("Tai Tham")); + ranges.emplace_back(std::make_pair(0x1B00, 0x1B7F), _("Balinese")); + ranges.emplace_back(std::make_pair(0x1B80, 0x1BBF), _("Sundanese")); + ranges.emplace_back(std::make_pair(0x1C00, 0x1C4F), _("Lepcha")); + ranges.emplace_back(std::make_pair(0x1C50, 0x1C7F), _("Ol Chiki")); + ranges.emplace_back(std::make_pair(0x1CD0, 0x1CFF), _("Vedic Extensions")); + ranges.emplace_back(std::make_pair(0x1D00, 0x1D7F), _("Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x1D80, 0x1DBF), _("Phonetic Extensions Supplement")); + ranges.emplace_back(std::make_pair(0x1DC0, 0x1DFF), _("Combining Diacritical Marks Supplement")); + ranges.emplace_back(std::make_pair(0x1E00, 0x1EFF), _("Latin Extended Additional")); + ranges.emplace_back(std::make_pair(0x1F00, 0x1FFF), _("Greek Extended")); + ranges.emplace_back(std::make_pair(0x2000, 0x206F), _("General Punctuation")); + ranges.emplace_back(std::make_pair(0x2070, 0x209F), _("Superscripts and Subscripts")); + ranges.emplace_back(std::make_pair(0x20A0, 0x20CF), _("Currency Symbols")); + ranges.emplace_back(std::make_pair(0x20D0, 0x20FF), _("Combining Diacritical Marks for Symbols")); + ranges.emplace_back(std::make_pair(0x2100, 0x214F), _("Letterlike Symbols")); + ranges.emplace_back(std::make_pair(0x2150, 0x218F), _("Number Forms")); + ranges.emplace_back(std::make_pair(0x2190, 0x21FF), _("Arrows")); + ranges.emplace_back(std::make_pair(0x2200, 0x22FF), _("Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2300, 0x23FF), _("Miscellaneous Technical")); + ranges.emplace_back(std::make_pair(0x2400, 0x243F), _("Control Pictures")); + ranges.emplace_back(std::make_pair(0x2440, 0x245F), _("Optical Character Recognition")); + ranges.emplace_back(std::make_pair(0x2460, 0x24FF), _("Enclosed Alphanumerics")); + ranges.emplace_back(std::make_pair(0x2500, 0x257F), _("Box Drawing")); + ranges.emplace_back(std::make_pair(0x2580, 0x259F), _("Block Elements")); + ranges.emplace_back(std::make_pair(0x25A0, 0x25FF), _("Geometric Shapes")); + ranges.emplace_back(std::make_pair(0x2600, 0x26FF), _("Miscellaneous Symbols")); + ranges.emplace_back(std::make_pair(0x2700, 0x27BF), _("Dingbats")); + ranges.emplace_back(std::make_pair(0x27C0, 0x27EF), _("Miscellaneous Mathematical Symbols-A")); + ranges.emplace_back(std::make_pair(0x27F0, 0x27FF), _("Supplemental Arrows-A")); + ranges.emplace_back(std::make_pair(0x2800, 0x28FF), _("Braille Patterns")); + ranges.emplace_back(std::make_pair(0x2900, 0x297F), _("Supplemental Arrows-B")); + ranges.emplace_back(std::make_pair(0x2980, 0x29FF), _("Miscellaneous Mathematical Symbols-B")); + ranges.emplace_back(std::make_pair(0x2A00, 0x2AFF), _("Supplemental Mathematical Operators")); + ranges.emplace_back(std::make_pair(0x2B00, 0x2BFF), _("Miscellaneous Symbols and Arrows")); + ranges.emplace_back(std::make_pair(0x2C00, 0x2C5F), _("Glagolitic")); + ranges.emplace_back(std::make_pair(0x2C60, 0x2C7F), _("Latin Extended-C")); + ranges.emplace_back(std::make_pair(0x2C80, 0x2CFF), _("Coptic")); + ranges.emplace_back(std::make_pair(0x2D00, 0x2D2F), _("Georgian Supplement")); + ranges.emplace_back(std::make_pair(0x2D30, 0x2D7F), _("Tifinagh")); + ranges.emplace_back(std::make_pair(0x2D80, 0x2DDF), _("Ethiopic Extended")); + ranges.emplace_back(std::make_pair(0x2DE0, 0x2DFF), _("Cyrillic Extended-A")); + ranges.emplace_back(std::make_pair(0x2E00, 0x2E7F), _("Supplemental Punctuation")); + ranges.emplace_back(std::make_pair(0x2E80, 0x2EFF), _("CJK Radicals Supplement")); + ranges.emplace_back(std::make_pair(0x2F00, 0x2FDF), _("Kangxi Radicals")); + ranges.emplace_back(std::make_pair(0x2FF0, 0x2FFF), _("Ideographic Description Characters")); + ranges.emplace_back(std::make_pair(0x3000, 0x303F), _("CJK Symbols and Punctuation")); + ranges.emplace_back(std::make_pair(0x3040, 0x309F), _("Hiragana")); + ranges.emplace_back(std::make_pair(0x30A0, 0x30FF), _("Katakana")); + ranges.emplace_back(std::make_pair(0x3100, 0x312F), _("Bopomofo")); + ranges.emplace_back(std::make_pair(0x3130, 0x318F), _("Hangul Compatibility Jamo")); + ranges.emplace_back(std::make_pair(0x3190, 0x319F), _("Kanbun")); + ranges.emplace_back(std::make_pair(0x31A0, 0x31BF), _("Bopomofo Extended")); + ranges.emplace_back(std::make_pair(0x31C0, 0x31EF), _("CJK Strokes")); + ranges.emplace_back(std::make_pair(0x31F0, 0x31FF), _("Katakana Phonetic Extensions")); + ranges.emplace_back(std::make_pair(0x3200, 0x32FF), _("Enclosed CJK Letters and Months")); + ranges.emplace_back(std::make_pair(0x3300, 0x33FF), _("CJK Compatibility")); + ranges.emplace_back(std::make_pair(0x3400, 0x4DBF), _("CJK Unified Ideographs Extension A")); + ranges.emplace_back(std::make_pair(0x4DC0, 0x4DFF), _("Yijing Hexagram Symbols")); + ranges.emplace_back(std::make_pair(0x4E00, 0x9FFF), _("CJK Unified Ideographs")); + ranges.emplace_back(std::make_pair(0xA000, 0xA48F), _("Yi Syllables")); + ranges.emplace_back(std::make_pair(0xA490, 0xA4CF), _("Yi Radicals")); + ranges.emplace_back(std::make_pair(0xA4D0, 0xA4FF), _("Lisu")); + ranges.emplace_back(std::make_pair(0xA500, 0xA63F), _("Vai")); + ranges.emplace_back(std::make_pair(0xA640, 0xA69F), _("Cyrillic Extended-B")); + ranges.emplace_back(std::make_pair(0xA6A0, 0xA6FF), _("Bamum")); + ranges.emplace_back(std::make_pair(0xA700, 0xA71F), _("Modifier Tone Letters")); + ranges.emplace_back(std::make_pair(0xA720, 0xA7FF), _("Latin Extended-D")); + ranges.emplace_back(std::make_pair(0xA800, 0xA82F), _("Syloti Nagri")); + ranges.emplace_back(std::make_pair(0xA830, 0xA83F), _("Common Indic Number Forms")); + ranges.emplace_back(std::make_pair(0xA840, 0xA87F), _("Phags-pa")); + ranges.emplace_back(std::make_pair(0xA880, 0xA8DF), _("Saurashtra")); + ranges.emplace_back(std::make_pair(0xA8E0, 0xA8FF), _("Devanagari Extended")); + ranges.emplace_back(std::make_pair(0xA900, 0xA92F), _("Kayah Li")); + ranges.emplace_back(std::make_pair(0xA930, 0xA95F), _("Rejang")); + ranges.emplace_back(std::make_pair(0xA960, 0xA97F), _("Hangul Jamo Extended-A")); + ranges.emplace_back(std::make_pair(0xA980, 0xA9DF), _("Javanese")); + ranges.emplace_back(std::make_pair(0xAA00, 0xAA5F), _("Cham")); + ranges.emplace_back(std::make_pair(0xAA60, 0xAA7F), _("Myanmar Extended-A")); + ranges.emplace_back(std::make_pair(0xAA80, 0xAADF), _("Tai Viet")); + ranges.emplace_back(std::make_pair(0xABC0, 0xABFF), _("Meetei Mayek")); + ranges.emplace_back(std::make_pair(0xAC00, 0xD7AF), _("Hangul Syllables")); + ranges.emplace_back(std::make_pair(0xD7B0, 0xD7FF), _("Hangul Jamo Extended-B")); + ranges.emplace_back(std::make_pair(0xD800, 0xDB7F), _("High Surrogates")); + ranges.emplace_back(std::make_pair(0xDB80, 0xDBFF), _("High Private Use Surrogates")); + ranges.emplace_back(std::make_pair(0xDC00, 0xDFFF), _("Low Surrogates")); + ranges.emplace_back(std::make_pair(0xE000, 0xF8FF), _("Private Use Area")); + ranges.emplace_back(std::make_pair(0xF900, 0xFAFF), _("CJK Compatibility Ideographs")); + ranges.emplace_back(std::make_pair(0xFB00, 0xFB4F), _("Alphabetic Presentation Forms")); + ranges.emplace_back(std::make_pair(0xFB50, 0xFDFF), _("Arabic Presentation Forms-A")); + ranges.emplace_back(std::make_pair(0xFE00, 0xFE0F), _("Variation Selectors")); + ranges.emplace_back(std::make_pair(0xFE10, 0xFE1F), _("Vertical Forms")); + ranges.emplace_back(std::make_pair(0xFE20, 0xFE2F), _("Combining Half Marks")); + ranges.emplace_back(std::make_pair(0xFE30, 0xFE4F), _("CJK Compatibility Forms")); + ranges.emplace_back(std::make_pair(0xFE50, 0xFE6F), _("Small Form Variants")); + ranges.emplace_back(std::make_pair(0xFE70, 0xFEFF), _("Arabic Presentation Forms-B")); + ranges.emplace_back(std::make_pair(0xFF00, 0xFFEF), _("Halfwidth and Fullwidth Forms")); + ranges.emplace_back(std::make_pair(0xFFF0, 0xFFFF), _("Specials")); + + // Selected ranges in Extended Multilingual Plane + ranges.emplace_back(std::make_pair(0x1F300, 0x1F5FF), _("Miscellaneous Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1F600, 0x1F64F), _("Emoticons")); + ranges.emplace_back(std::make_pair(0x1F650, 0x1F67F), _("Ornamental Dingbats")); + ranges.emplace_back(std::make_pair(0x1F680, 0x1F6FF), _("Transport and Map Symbols")); + ranges.emplace_back(std::make_pair(0x1F700, 0x1F77F), _("Alchemical Symbols")); + ranges.emplace_back(std::make_pair(0x1F780, 0x1F7FF), _("Geometric Shapes Extended")); + ranges.emplace_back(std::make_pair(0x1F800, 0x1F8FF), _("Supplemental Arrows-C")); + ranges.emplace_back(std::make_pair(0x1F900, 0x1F9FF), _("Supplemental Symbols and Pictographs")); + ranges.emplace_back(std::make_pair(0x1FA00, 0x1FA7F), _("Chess Symbols")); + ranges.emplace_back(std::make_pair(0x1FA80, 0x1FAFF), _("Symbols and Pictographs Extended-A")); + + } + + return ranges; +} + +class GlyphColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn<gunichar> code; + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> tooltip; + + GlyphColumns() + { + add(code); + add(name); + add(tooltip); + } +}; + +GlyphColumns *GlyphsPanel::getColumns() +{ + static GlyphColumns *columns = new GlyphColumns(); + + return columns; +} + +/** + * Constructor + */ +GlyphsPanel::GlyphsPanel() : + Inkscape::UI::Widget::Panel("/dialogs/glyphs", SP_VERB_DIALOG_GLYPHS), + store(Gtk::ListStore::create(*getColumns())), + deskTrack(), + instanceConns(), + desktopConns() +{ + auto table = new Gtk::Grid(); + table->set_row_spacing(4); + table->set_column_spacing(4); + _getContents()->pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET); + guint row = 0; + +// ------------------------------- + + { + fontSelector = new Inkscape::UI::Widget::FontSelector (false, false); + fontSelector->set_name ("UnicodeCharacters"); + + sigc::connection conn = + fontSelector->connectChanged(sigc::hide(sigc::mem_fun(*this, &GlyphsPanel::rebuild))); + instanceConns.push_back(conn); + + table->attach(*Gtk::manage(fontSelector), 0, row, 3, 1); + row++; + } + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Script: ")); + + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + scriptCombo = Gtk::manage(new Gtk::ComboBoxText()); + for (auto & it : getScriptToName()) + { + scriptCombo->append(it.second); + } + + scriptCombo->set_active_text(getScriptToName()[G_UNICODE_SCRIPT_INVALID_CODE]); + sigc::connection conn = scriptCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + scriptCombo->set_halign(Gtk::ALIGN_START); + scriptCombo->set_valign(Gtk::ALIGN_START); + scriptCombo->set_hexpand(); + table->attach(*scriptCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + { + auto label = new Gtk::Label(_("Range: ")); + table->attach( *Gtk::manage(label), 0, row, 1, 1); + + rangeCombo = Gtk::manage(new Gtk::ComboBoxText()); + for (auto & it : getRanges()) { + rangeCombo->append(it.second); + } + + rangeCombo->set_active_text(getRanges()[4].second); + sigc::connection conn = rangeCombo->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::rebuild)); + instanceConns.push_back(conn); + + rangeCombo->set_halign(Gtk::ALIGN_START); + rangeCombo->set_valign(Gtk::ALIGN_START); + rangeCombo->set_hexpand(); + table->attach(*rangeCombo, 1, row, 1, 1); + } + + row++; + +// ------------------------------- + + GlyphColumns *columns = getColumns(); + + iconView = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store)); + iconView->set_name("UnicodeIconView"); + iconView->set_markup_column(columns->name); + iconView->set_tooltip_column(2); // Uses Pango markup, must use column number. + iconView->set_margin(0); + iconView->set_item_padding(0); + iconView->set_row_spacing(0); + iconView->set_column_spacing(0); + + sigc::connection conn; + conn = iconView->signal_item_activated().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphActivated)); + instanceConns.push_back(conn); + conn = iconView->signal_selection_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::glyphSelectionChanged)); + instanceConns.push_back(conn); + + + Gtk::ScrolledWindow *scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->add(*Gtk::manage(iconView)); + scroller->set_hexpand(); + scroller->set_vexpand(); + table->attach(*Gtk::manage(scroller), 0, row, 3, 1); + + row++; + +// ------------------------------- + + Gtk::HBox *box = new Gtk::HBox(); + + entry = new Gtk::Entry(); + conn = entry->signal_changed().connect(sigc::mem_fun(*this, &GlyphsPanel::calcCanInsert)); + instanceConns.push_back(conn); + entry->set_width_chars(18); + box->pack_start(*Gtk::manage(entry), Gtk::PACK_SHRINK); + + Gtk::Label *pad = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_SHRINK); + + label = new Gtk::Label(" "); + box->pack_start(*Gtk::manage(label), Gtk::PACK_SHRINK); + + pad = new Gtk::Label(""); + box->pack_start(*Gtk::manage(pad), Gtk::PACK_EXPAND_WIDGET); + + insertBtn = new Gtk::Button(_("Append")); + conn = insertBtn->signal_clicked().connect(sigc::mem_fun(*this, &GlyphsPanel::insertText)); + instanceConns.push_back(conn); + insertBtn->set_can_default(); + insertBtn->set_sensitive(false); + + box->pack_end(*Gtk::manage(insertBtn), Gtk::PACK_SHRINK); + box->set_hexpand(); + table->attach( *Gtk::manage(box), 0, row, 3, 1); + + row++; + +// ------------------------------- + + + show_all_children(); + + // Connect this up last + conn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &GlyphsPanel::setTargetDesktop) ); + instanceConns.push_back(conn); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +GlyphsPanel::~GlyphsPanel() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + instanceConns.clear(); + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); +} + + +void GlyphsPanel::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void GlyphsPanel::setTargetDesktop(SPDesktop *desktop) +{ + if (targetDesktop != desktop) { + if (targetDesktop) { + for (auto & desktopConn : desktopConns) { + desktopConn.disconnect(); + } + desktopConns.clear(); + } + + targetDesktop = desktop; + + if (targetDesktop && targetDesktop->selection) { + sigc::connection conn = desktop->selection->connectChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, true))); + desktopConns.push_back(conn); + + // Text selection within selected items has changed: + conn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &GlyphsPanel::readSelection), true, false))); + desktopConns.push_back(conn); + + // Must check flags, so can't call performUpdate() directly. + conn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &GlyphsPanel::selectionModifiedCB))); + desktopConns.push_back(conn); + + readSelection(true, true); + } + } +} + +// Append selected glyphs to selected text +void GlyphsPanel::insertText() +{ + SPItem *textItem = nullptr; + auto itemlist= targetDesktop->selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + textItem = *i; + break; + } + } + + if (textItem) { + Glib::ustring glyphs; + if (entry->get_text_length() > 0) { + glyphs = entry->get_text(); + } else { + auto itemArray = iconView->get_selected_items(); + + if (!itemArray.empty()) { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + glyphs = ch; + } + } + + if (!glyphs.empty()) { + Glib::ustring combined; + gchar *str = sp_te_get_string_multiline(textItem); + if (str) { + combined = str; + g_free(str); + str = nullptr; + } + combined += glyphs; + sp_te_set_repr_text_multiline(textItem, combined.c_str()); + DocumentUndo::done(targetDesktop->doc(), SP_VERB_CONTEXT_TEXT, _("Append text")); + } + } +} + +void GlyphsPanel::glyphActivated(Gtk::TreeModel::Path const & path) +{ + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + Glib::ustring tmp; + tmp += ch; + + int startPos = 0; + int endPos = 0; + if (entry->get_selection_bounds(startPos, endPos)) { + // there was something selected. + entry->delete_text(startPos, endPos); + } + startPos = entry->get_position(); + entry->insert_text(tmp, -1, startPos); + entry->set_position(startPos); +} + +void GlyphsPanel::glyphSelectionChanged() +{ + auto itemArray = iconView->get_selected_items(); + + if (itemArray.empty()) { + label->set_text(" "); + } else { + Gtk::TreeModel::Path const & path = *itemArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + gunichar ch = (*row)[getColumns()->code]; + + + Glib::ustring scriptName; + GUnicodeScript script = g_unichar_get_script(ch); + std::map<GUnicodeScript, Glib::ustring> mappings = getScriptToName(); + if (mappings.find(script) != mappings.end()) { + scriptName = mappings[script]; + } + gchar * tmp = g_strdup_printf("U+%04X %s", ch, scriptName.c_str()); + label->set_text(tmp); + } + calcCanInsert(); +} + +void GlyphsPanel::selectionModifiedCB(guint flags) +{ + bool style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + + bool content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + + readSelection(style, content); +} + +void GlyphsPanel::calcCanInsert() +{ + int items = 0; + auto itemlist= targetDesktop->selection->items(); + for(auto i=itemlist.begin(); itemlist.end() != i; ++i) { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) { + ++items; + } + } + + bool enable = (items == 1); + if (enable) { + enable &= (!iconView->get_selected_items().empty() + || (entry->get_text_length() > 0)); + } + + if (enable != insertBtn->is_sensitive()) { + insertBtn->set_sensitive(enable); + } +} + +void GlyphsPanel::readSelection( bool updateStyle, bool updateContent ) +{ + calcCanInsert(); + + if (updateStyle) { + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + + // Update family/style based on selection. + fontlister->selection_update(); + + // Update GUI (based on fontlister values). + fontSelector->update_font (); + } +} + + +void GlyphsPanel::rebuild() +{ + Glib::ustring fontspec = fontSelector->get_fontspec(); + + font_instance* font = nullptr; + if( !fontspec.empty() ) { + font = font_factory::Default()->FaceFromFontSpecification( fontspec.c_str() ); + } + + if (font) { + + GUnicodeScript script = G_UNICODE_SCRIPT_INVALID_CODE; + Glib::ustring scriptName = scriptCombo->get_active_text(); + std::map<GUnicodeScript, Glib::ustring> items = getScriptToName(); + for (auto & item : items) { + if (scriptName == item.second) { + script = item.first; + break; + } + } + + // Disconnect the model while we update it. Simple work-around for 5x+ performance boost. + Glib::RefPtr<Gtk::ListStore> tmp = Gtk::ListStore::create(*getColumns()); + iconView->set_model(tmp); + + gunichar lower = 0x00001; + gunichar upper = 0x2FFFF; + int active = rangeCombo->get_active_row_number(); + if (active >= 0) { + lower = getRanges()[active].first.first; + upper = getRanges()[active].first.second; + } + std::vector<gunichar> present; + for (gunichar ch = lower; ch <= upper; ch++) { + int glyphId = font->MapUnicodeChar(ch); + if (glyphId > 0) { + if ((script == G_UNICODE_SCRIPT_INVALID_CODE) || (script == g_unichar_get_script(ch))) { + present.push_back(ch); + } + } + } + + GlyphColumns *columns = getColumns(); + store->clear(); + for (unsigned int & it : present) + { + Gtk::ListStore::iterator row = store->append(); + Glib::ustring tmp; + tmp += it; + tmp = Glib::Markup::escape_text(tmp); // Escape '&', '<', etc. + (*row)[columns->code] = it; + (*row)[columns->name] = "<span font_desc=\"" + fontspec + "\">" + tmp + "</span>"; + (*row)[columns->tooltip] = "<span font_desc=\"" + fontspec + "\" size=\"42000\">" + tmp + "</span>"; + } + + // Reconnect the model once it has been updated: + iconView->set_model(store); + } +} + + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/glyphs.h b/src/ui/dialog/glyphs.h new file mode 100644 index 0000000..3307683 --- /dev/null +++ b/src/ui/dialog/glyphs.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2010 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOGS_GLYPHS_H +#define SEEN_DIALOGS_GLYPHS_H + +#include "ui/widget/panel.h" +#include <gtkmm/treemodel.h> +#include "ui/dialog/desktop-tracker.h" + + +namespace Gtk { +class ComboBoxText; +class Entry; +class IconView; +class Label; +class ListStore; +} + +namespace Inkscape { +namespace UI { + +namespace Widget { +class FontSelector; +} + +namespace Dialog { + +class GlyphColumns; + +/** + * A panel that displays character glyphs. + */ +class GlyphsPanel : public Inkscape::UI::Widget::Panel +{ +public: + GlyphsPanel(); + ~GlyphsPanel() override; + + static GlyphsPanel& getInstance(); + + void setDesktop(SPDesktop *desktop) override; + +protected: + +private: + GlyphsPanel(GlyphsPanel const &) = delete; // no copy + GlyphsPanel &operator=(GlyphsPanel const &) = delete; // no assign + + static GlyphColumns *getColumns(); + + void rebuild(); + + void glyphActivated(Gtk::TreeModel::Path const & path); + void glyphSelectionChanged(); + void setTargetDesktop(SPDesktop *desktop); + void selectionModifiedCB(guint flags); + void readSelection( bool updateStyle, bool updateContent ); + void calcCanInsert(); + void insertText(); + + + Glib::RefPtr<Gtk::ListStore> store; + Gtk::IconView *iconView; + Gtk::Entry *entry; + Gtk::Label *label; + Gtk::Button *insertBtn; + Gtk::ComboBoxText *scriptCombo; + Gtk::ComboBoxText *rangeCombo; + Inkscape::UI::Widget::FontSelector *fontSelector; + SPDesktop *targetDesktop; + DesktopTracker deskTrack; + + std::vector<sigc::connection> instanceConns; + std::vector<sigc::connection> desktopConns; +}; + + +} // namespace Dialogs +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_DIALOGS_GLYPHS_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/grid-arrange-tab.cpp b/src/ui/dialog/grid-arrange-tab.cpp new file mode 100644 index 0000000..81baf78 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.cpp @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for creating grid type arrangements of selected objects + * + * Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +//#define DEBUG_GRID_ARRANGE 1 + +#include "ui/dialog/grid-arrange-tab.h" +#include <glibmm/i18n.h> + +#include <gtkmm/grid.h> + +#include <2geom/transforms.h> + +#include "verbs.h" +#include "preferences.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" +//#include "sp-item-transform.h" FIXME +#include "ui/dialog/tile.h" // for Inkscape::UI::Dialog::ArrangeDialog + + /* + * Sort items by their x co-ordinates, taking account of y (keeps rows intact) + * + * <0 *elem1 goes before *elem2 + * 0 *elem1 == *elem2 + * >0 *elem1 goes after *elem2 + */ + static bool sp_compare_x_position(SPItem *first, SPItem *second) + { + using Geom::X; + using Geom::Y; + + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + double const a_height = a->dimensions()[Y]; + double const b_height = b->dimensions()[Y]; + + bool a_in_b_vert = false; + if ((a->min()[Y] < b->min()[Y] + 0.1) && (a->min()[Y] > b->min()[Y] - b_height)) { + a_in_b_vert = true; + } else if ((b->min()[Y] < a->min()[Y] + 0.1) && (b->min()[Y] > a->min()[Y] - a_height)) { + a_in_b_vert = true; + } else if (b->min()[Y] == a->min()[Y]) { + a_in_b_vert = true; + } else { + a_in_b_vert = false; + } + + if (!a_in_b_vert) { // a and b are not in the same row + return (a->min()[Y] < b->min()[Y]); + } + return (a->min()[X] < b->min()[X]); + } + + /* + * Sort items by their y co-ordinates. + */ + static bool sp_compare_y_position(SPItem *first, SPItem *second) + { + Geom::OptRect a = first->documentVisualBounds(); + Geom::OptRect b = second->documentVisualBounds(); + + if ( !a || !b ) { + // FIXME? + return false; + } + + if (a->min()[Geom::Y] > b->min()[Geom::Y]) { + return false; + } + if (a->min()[Geom::Y] < b->min()[Geom::Y]) { + return true; + } + + return false; + } + + + namespace Inkscape { + namespace UI { + namespace Dialog { + + + //######################################################################### + //## E V E N T S + //######################################################################### + + /* + * + * This arranges the selection in a grid pattern. + * + */ + + void GridArrangeTab::arrange() + { + + int cnt,row_cnt,col_cnt,a,row,col; + double grid_left,grid_top,col_width,row_height,paddingx,paddingy,width, height, new_x, new_y; + double total_col_width,total_row_height; + col_width = 0; + row_height = 0; + total_col_width=0; + total_row_height=0; + + // check for correct numbers in the row- and col-spinners + on_col_spinbutton_changed(); + on_row_spinbutton_changed(); + + // set padding to manual values + paddingx = XPadding.getValue("px"); + paddingy = YPadding.getValue("px"); + + std::vector<double> row_heights; + std::vector<double> col_widths; + std::vector<double> row_ys; + std::vector<double> col_xs; + + int NoOfCols = NoOfColsSpinner.get_value_as_int(); + int NoOfRows = NoOfRowsSpinner.get_value_as_int(); + + width = 0; + for (a=0;a<NoOfCols; a++){ + col_widths.push_back(width); + } + + height = 0; + for (a=0;a<NoOfRows; a++){ + row_heights.push_back(height); + } + grid_left = 99999; + grid_top = 99999; + + SPDesktop *desktop = Parent->getDesktop(); + desktop->getDocument()->ensureUpToDate(); + + Inkscape::Selection *selection = desktop->getSelection(); + std::vector<SPItem*> items; + if (selection) { + items.insert(items.end(), selection->items().begin(), selection->items().end()); + } + + for(auto item : items){ + Geom::OptRect b = item->documentVisualBounds(); + if (!b) { + continue; + } + + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + + if (b->min()[Geom::X] < grid_left) { + grid_left = b->min()[Geom::X]; + } + if (b->min()[Geom::Y] < grid_top) { + grid_top = b->min()[Geom::Y]; + } + if (width > col_width) { + col_width = width; + } + if (height > row_height) { + row_height = height; + } + } + + + // require the sorting done before we can calculate row heights etc. + + g_return_if_fail(selection); + std::vector<SPItem*> sorted(selection->items().begin(), selection->items().end()); + sort(sorted.begin(),sorted.end(),sp_compare_y_position); + sort(sorted.begin(),sorted.end(),sp_compare_x_position); + + + // Calculate individual Row and Column sizes if necessary + + + cnt=0; + const std::vector<SPItem*> sizes(sorted); + for (auto item : sizes) { + Geom::OptRect b = item->documentVisualBounds(); + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + if (width > col_widths[(cnt % NoOfCols)]) { + col_widths[(cnt % NoOfCols)] = width; + } + if (height > row_heights[(cnt / NoOfCols)]) { + row_heights[(cnt / NoOfCols)] = height; + } + } + + cnt++; + } + + + /// Make sure the top and left of the grid don't move by compensating for align values. + if (RowHeightButton.get_active()){ + grid_top = grid_top - (((row_height - row_heights[0]) / 2)*(VertAlign)); + } + if (ColumnWidthButton.get_active()){ + grid_left = grid_left - (((col_width - col_widths[0]) /2)*(HorizAlign)); + } + + #ifdef DEBUG_GRID_ARRANGE + g_print("\n cx = %f cy= %f gridleft=%f",cx,cy,grid_left); + #endif + + // Calculate total widths and heights, allowing for columns and rows non uniformly sized. + + if (ColumnWidthButton.get_active()){ + total_col_width = col_width * NoOfCols; + col_widths.clear(); + for (a=0;a<NoOfCols; a++){ + col_widths.push_back(col_width); + } + } else { + for (a = 0; a < (int)col_widths.size(); a++) + { + total_col_width += col_widths[a] ; + } + } + + if (RowHeightButton.get_active()){ + total_row_height = row_height * NoOfRows; + row_heights.clear(); + for (a=0;a<NoOfRows; a++){ + row_heights.push_back(row_height); + } + } else { + for (a = 0; a < (int)row_heights.size(); a++) + { + total_row_height += row_heights[a] ; + } + } + + + Geom::OptRect sel_bbox = selection->visualBounds(); + // Fit to bbox, calculate padding between rows accordingly. + if ( sel_bbox && !SpaceManualRadioButton.get_active() ){ +#ifdef DEBUG_GRID_ARRANGE +g_print("\n row = %f col = %f selection x= %f selection y = %f", total_row_height,total_col_width, b.extent(Geom::X), b.extent(Geom::Y)); +#endif + paddingx = (sel_bbox->width() - total_col_width) / (NoOfCols -1); + paddingy = (sel_bbox->height() - total_row_height) / (NoOfRows -1); + } + +/* + Horizontal align - Left = 0 + Centre = 1 + Right = 2 + + Vertical align - Top = 0 + Middle = 1 + Bottom = 2 + + X position is calculated by taking the grids left co-ord, adding the distance to the column, + then adding 1/2 the spacing multiplied by the align variable above, + Y position likewise, takes the top of the grid, adds the y to the current row then adds the padding in to align it. + +*/ + + // Calculate row and column x and y coords required to allow for columns and rows which are non uniformly sized. + + for (a=0;a<NoOfCols; a++){ + if (a<1) col_xs.push_back(0); + else col_xs.push_back(col_widths[a-1]+paddingx+col_xs[a-1]); + } + + + for (a=0;a<NoOfRows; a++){ + if (a<1) row_ys.push_back(0); + else row_ys.push_back(row_heights[a-1]+paddingy+row_ys[a-1]); + } + + cnt=0; + std::vector<SPItem*>::iterator it = sorted.begin(); + for (row_cnt=0; ((it != sorted.end()) && (row_cnt<NoOfRows)); ++row_cnt) { + + std::vector<SPItem *> current_row; + col_cnt = 0; + for(;it!=sorted.end()&&col_cnt<NoOfCols;++it) { + current_row.push_back(*it); + col_cnt++; + } + + for (auto item:current_row) { + Inkscape::XML::Node *repr = item->getRepr(); + Geom::OptRect b = item->documentVisualBounds(); + Geom::Point min; + if (b) { + width = b->dimensions()[Geom::X]; + height = b->dimensions()[Geom::Y]; + min = b->min(); + } else { + width = height = 0; + min = Geom::Point(0, 0); + } + + row = cnt / NoOfCols; + col = cnt % NoOfCols; + + new_x = grid_left + (((col_widths[col] - width)/2)*HorizAlign) + col_xs[col]; + new_y = grid_top + (((row_heights[row] - height)/2)*VertAlign) + row_ys[row]; + + Geom::Point move = Geom::Point(new_x, new_y) - min; + Geom::Affine const affine = Geom::Affine(Geom::Translate(move)); + item->set_i2d_affine(item->i2doc_affine() * affine * item->document->doc2dt()); + item->doWriteTransform(item->transform); + item->updateRepr(); + cnt +=1; + } + } + + DocumentUndo::done(desktop->getDocument(), SP_VERB_SELECTION_ARRANGE, + _("Arrange in a grid")); + +} + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * changed value in # of columns spinbox. + */ +void GridArrangeTab::on_row_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + SPDesktop *desktop = Parent->getDesktop(); + + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + g_return_if_fail( selection ); + + int selcount = (int) boost::distance(selection->items()); + + double PerCol = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(PerCol); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", NoOfColsSpinner.get_value()); + updating=false; +} + +/** + * changed value in # of rows spinbox. + */ +void GridArrangeTab::on_col_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + g_return_if_fail(selection); + + int selcount = (int) boost::distance(selection->items()); + + double PerRow = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(PerRow); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/NoOfCols", PerRow); + + updating=false; +} + +/** + * changed value in x padding spinbox. + */ +void GridArrangeTab::on_xpad_spinbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/XPad", XPadding.getValue("px")); + +} + +/** + * changed value in y padding spinbox. + */ +void GridArrangeTab::on_ypad_spinbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/YPad", YPadding.getValue("px")); +} + + +/** + * checked/unchecked autosize Rows button. + */ +void GridArrangeTab::on_RowSize_checkbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (RowHeightButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/AutoRowSize", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/AutoRowSize", -20); + } + RowHeightBox.set_sensitive ( !RowHeightButton.get_active()); +} + +/** + * checked/unchecked autosize Rows button. + */ +void GridArrangeTab::on_ColSize_checkbutton_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (ColumnWidthButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/AutoColSize", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/AutoColSize", -20); + } + ColumnWidthBox.set_sensitive ( !ColumnWidthButton.get_active()); +} + +/** + * changed value in columns spinbox. + */ +void GridArrangeTab::on_rowSize_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/RowHeight", RowHeightSpinner.get_value()); + updating=false; + +} + +/** + * changed value in rows spinbox. + */ +void GridArrangeTab::on_colSize_spinbutton_changed() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + // in turn, prevent listener from responding + updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/dialogs/gridtiler/ColWidth", ColumnWidthSpinner.get_value()); + updating=false; + +} + +/** + * changed Radio button in Spacing group. + */ +void GridArrangeTab::Spacing_button_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (SpaceManualRadioButton.get_active()) { + prefs->setDouble("/dialogs/gridtiler/SpacingType", 20); + } else { + prefs->setDouble("/dialogs/gridtiler/SpacingType", -20); + } + + XPadding.set_sensitive ( SpaceManualRadioButton.get_active()); + YPadding.set_sensitive ( SpaceManualRadioButton.get_active()); +} + +/** + * changed Anchor selection widget. + */ +void GridArrangeTab::Align_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + VertAlign = AlignmentSelector.getVerticalAlignment(); + prefs->setInt("/dialogs/gridtiler/VertAlign", VertAlign); + HorizAlign = AlignmentSelector.getHorizontalAlignment(); + prefs->setInt("/dialogs/gridtiler/HorizAlign", HorizAlign); +} + +/** + * Desktop selection changed + */ +void GridArrangeTab::updateSelection() +{ + // quit if run by the attr_changed listener + if (updating) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // in turn, prevent listener from responding + updating = true; + SPDesktop *desktop = Parent->getDesktop(); + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + std::vector<SPItem*> items; + if (selection) { + items.insert(items.end(), selection->items().begin(), selection->items().end()); + } + + if (!items.empty()) { + int selcount = items.size(); + + if (NoOfColsSpinner.get_value() > 1 && NoOfRowsSpinner.get_value() > 1){ + // Update the number of rows assuming number of columns wanted remains same. + double NoOfRows = ceil(selcount / NoOfColsSpinner.get_value()); + NoOfRowsSpinner.set_value(NoOfRows); + + // if the selection has less than the number set for one row, reduce it appropriately + if (selcount < NoOfColsSpinner.get_value()) { + double NoOfCols = ceil(selcount / NoOfRowsSpinner.get_value()); + NoOfColsSpinner.set_value(NoOfCols); + prefs->setInt("/dialogs/gridtiler/NoOfCols", NoOfCols); + } + } else { + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + NoOfRowsSpinner.set_value(PerRow); + NoOfColsSpinner.set_value(PerCol); + prefs->setInt("/dialogs/gridtiler/NoOfCols", static_cast<int>(PerCol)); + } + } + + updating = false; +} + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +GridArrangeTab::GridArrangeTab(ArrangeDialog *parent) + : Parent(parent), + XPadding(_("X:"), _("Horizontal spacing between columns."), UNIT_TYPE_LINEAR, "", "object-columns", &PaddingUnitMenu), + YPadding(_("Y:"), _("Vertical spacing between rows."), XPadding, "", "object-rows"), + PaddingTable(Gtk::manage(new Gtk::Grid())) +{ + // bool used by spin button callbacks to stop loops where they change each other. + updating = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // could not do this in gtkmm - there's no Gtk::SizeGroup public constructor (!) + GtkSizeGroup *_col1 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col2 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + GtkSizeGroup *_col3 = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + { + // Selection Change signal + INKSCAPE.signal_selection_changed.connect(sigc::hide<0>(sigc::mem_fun(*this, &GridArrangeTab::updateSelection))); + } + + Gtk::Box *contents = this; + +#define MARGIN 2 + + //##Set up the panel + + SPDesktop *desktop = Parent->getDesktop(); + + Inkscape::Selection *selection = desktop ? desktop->selection : nullptr; + g_return_if_fail( selection ); + int selcount = 1; + if (!selection->isEmpty()) { + selcount = (int) boost::distance(selection->items()); + } + + + /*#### Number of Rows ####*/ + + double PerRow = ceil(sqrt(selcount)); + double PerCol = ceil(sqrt(selcount)); + + #ifdef DEBUG_GRID_ARRANGE + g_print("/n PerRox = %f PerCol = %f selcount = %d",PerRow,PerCol,selcount); + #endif + + NoOfRowsLabel.set_text_with_mnemonic(_("_Rows:")); + NoOfRowsLabel.set_mnemonic_widget(NoOfRowsSpinner); + NoOfRowsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + NoOfRowsBox.pack_start(NoOfRowsLabel, false, false, MARGIN); + + NoOfRowsSpinner.set_digits(0); + NoOfRowsSpinner.set_increments(1, 0); + NoOfRowsSpinner.set_range(1.0, 10000.0); + NoOfRowsSpinner.set_value(PerCol); + NoOfRowsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_col_spinbutton_changed)); + NoOfRowsSpinner.set_tooltip_text(_("Number of rows")); + NoOfRowsBox.pack_start(NoOfRowsSpinner, false, false, MARGIN); + gtk_size_group_add_widget(_col1, (GtkWidget *) NoOfRowsBox.gobj()); + + RowHeightButton.set_label(_("Equal _height")); + RowHeightButton.set_use_underline(true); + double AutoRow = prefs->getDouble("/dialogs/gridtiler/AutoRowSize", 15); + if (AutoRow>0) + AutoRowSize=true; + else + AutoRowSize=false; + RowHeightButton.set_active(AutoRowSize); + + NoOfRowsBox.pack_start(RowHeightButton, false, false, MARGIN); + + RowHeightButton.set_tooltip_text(_("If not set, each row has the height of the tallest object in it")); + RowHeightButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_RowSize_checkbutton_changed)); + + SpinsHBox.pack_start(NoOfRowsBox, false, false, MARGIN); + + + /*#### Label for X ####*/ + padXByYLabel.set_label(" "); + XByYLabelVBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + XByYLabelVBox.pack_start(padXByYLabel, false, false, MARGIN); + XByYLabel.set_markup(" × "); + XByYLabelVBox.pack_start(XByYLabel, false, false, MARGIN); + SpinsHBox.pack_start(XByYLabelVBox, false, false, MARGIN); + gtk_size_group_add_widget(_col2, GTK_WIDGET(XByYLabelVBox.gobj())); + + /*#### Number of columns ####*/ + + NoOfColsLabel.set_text_with_mnemonic(_("_Columns:")); + NoOfColsLabel.set_mnemonic_widget(NoOfColsSpinner); + NoOfColsBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + NoOfColsBox.pack_start(NoOfColsLabel, false, false, MARGIN); + + NoOfColsSpinner.set_digits(0); + NoOfColsSpinner.set_increments(1, 0); + NoOfColsSpinner.set_range(1.0, 10000.0); + NoOfColsSpinner.set_value(PerRow); + NoOfColsSpinner.signal_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_row_spinbutton_changed)); + NoOfColsSpinner.set_tooltip_text(_("Number of columns")); + NoOfColsBox.pack_start(NoOfColsSpinner, false, false, MARGIN); + gtk_size_group_add_widget(_col3, GTK_WIDGET(NoOfColsBox.gobj())); + + ColumnWidthButton.set_label(_("Equal _width")); + ColumnWidthButton.set_use_underline(true); + double AutoCol = prefs->getDouble("/dialogs/gridtiler/AutoColSize", 15); + if (AutoCol>0) + AutoColSize=true; + else + AutoColSize=false; + ColumnWidthButton.set_active(AutoColSize); + NoOfColsBox.pack_start(ColumnWidthButton, false, false, MARGIN); + + ColumnWidthButton.set_tooltip_text(_("If not set, each column has the width of the widest object in it")); + ColumnWidthButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ColSize_checkbutton_changed)); + + SpinsHBox.pack_start(NoOfColsBox, false, false, MARGIN); + + TileBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + TileBox.pack_start(SpinsHBox, false, false, MARGIN); + + VertAlign = prefs->getInt("/dialogs/gridtiler/VertAlign", 1); + HorizAlign = prefs->getInt("/dialogs/gridtiler/HorizAlign", 1); + + // Anchor selection widget + AlignLabel.set_label(_("Alignment:")); + AlignLabel.set_halign(Gtk::ALIGN_START); + AlignLabel.set_valign(Gtk::ALIGN_CENTER); + AlignmentSelector.setAlignment(HorizAlign, VertAlign); + AlignmentSelector.on_selectionChanged().connect(sigc::mem_fun(*this, &GridArrangeTab::Align_changed)); + TileBox.pack_start(AlignLabel, false, false, MARGIN); + TileBox.pack_start(AlignmentSelector, true, false, MARGIN); + + { + /*#### Radio buttons to control spacing manually or to fit selection bbox ####*/ + SpaceByBBoxRadioButton.set_label(_("_Fit into selection box")); + SpaceByBBoxRadioButton.set_use_underline (true); + SpaceByBBoxRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); + SpacingGroup = SpaceByBBoxRadioButton.get_group(); + + SpacingVBox.pack_start(SpaceByBBoxRadioButton, false, false, MARGIN); + + SpaceManualRadioButton.set_label(_("_Set spacing:")); + SpaceManualRadioButton.set_use_underline (true); + SpaceManualRadioButton.set_group(SpacingGroup); + SpaceManualRadioButton.signal_toggled().connect(sigc::mem_fun(*this, &GridArrangeTab::Spacing_button_changed)); + SpacingVBox.pack_start(SpaceManualRadioButton, false, false, MARGIN); + + TileBox.pack_start(SpacingVBox, false, false, MARGIN); + } + + { + /*#### Padding ####*/ + PaddingUnitMenu.setUnitType(UNIT_TYPE_LINEAR); + PaddingUnitMenu.setUnit("px"); + + YPadding.setDigits(5); + YPadding.setIncrements(0.2, 0); + YPadding.setRange(-10000, 10000); + double yPad = prefs->getDouble("/dialogs/gridtiler/YPad", 15); + YPadding.setValue(yPad, "px"); + YPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_ypad_spinbutton_changed)); + + XPadding.setDigits(5); + XPadding.setIncrements(0.2, 0); + XPadding.setRange(-10000, 10000); + double xPad = prefs->getDouble("/dialogs/gridtiler/XPad", 15); + XPadding.setValue(xPad, "px"); + + XPadding.signal_value_changed().connect(sigc::mem_fun(*this, &GridArrangeTab::on_xpad_spinbutton_changed)); + } + + PaddingTable->set_border_width(MARGIN); + PaddingTable->set_row_spacing(MARGIN); + PaddingTable->set_column_spacing(MARGIN); + PaddingTable->attach(XPadding, 0, 0, 1, 1); + PaddingTable->attach(PaddingUnitMenu, 1, 0, 1, 1); + PaddingTable->attach(YPadding, 0, 1, 1, 1); + + TileBox.pack_start(*PaddingTable, false, false, MARGIN); + + contents->set_border_width(4); + contents->pack_start(TileBox); + + double SpacingType = prefs->getDouble("/dialogs/gridtiler/SpacingType", 15); + if (SpacingType>0) { + ManualSpacing=true; + } else { + ManualSpacing=false; + } + SpaceManualRadioButton.set_active(ManualSpacing); + SpaceByBBoxRadioButton.set_active(!ManualSpacing); + XPadding.set_sensitive (ManualSpacing); + YPadding.set_sensitive (ManualSpacing); + + //## The OK button FIXME + /*TileOkButton = addResponseButton(C_("Rows and columns dialog","_Arrange"), GTK_RESPONSE_APPLY); + TileOkButton->set_use_underline(true); + TileOkButton->set_tooltip_text(_("Arrange selected objects"));*/ + + show_all_children(); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/grid-arrange-tab.h b/src/ui/dialog/grid-arrange-tab.h new file mode 100644 index 0000000..ff6afb8 --- /dev/null +++ b/src/ui/dialog/grid-arrange-tab.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Grid + */ +/* Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/dialog/arrange-tab.h" + +#include "ui/widget/anchor-selector.h" +#include "ui/widget/spinbutton.h" + +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/radiobuttongroup.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * Dialog for tiling an object + */ +class GridArrangeTab : public ArrangeTab { +public: + GridArrangeTab(ArrangeDialog *parent); + ~GridArrangeTab() override = default;; + + /** + * Do the actual work + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + // Callbacks from spinbuttons + void on_row_spinbutton_changed(); + void on_col_spinbutton_changed(); + void on_xpad_spinbutton_changed(); + void on_ypad_spinbutton_changed(); + void on_RowSize_checkbutton_changed(); + void on_ColSize_checkbutton_changed(); + void on_rowSize_spinbutton_changed(); + void on_colSize_spinbutton_changed(); + void Spacing_button_changed(); + void Align_changed(); + + +private: + GridArrangeTab(GridArrangeTab const &d) = delete; // no copy + void operator=(GridArrangeTab const &d) = delete; // no assign + + ArrangeDialog *Parent; + + bool userHidden; + bool updating; + + Gtk::Box TileBox; + Gtk::Button *TileOkButton; + Gtk::Button *TileCancelButton; + + // Number selected label + Gtk::Label SelectionContentsLabel; + + + Gtk::Box AlignHBox; + Gtk::Box SpinsHBox; + + // Number per Row + Gtk::Box NoOfColsBox; + Gtk::Label NoOfColsLabel; + Inkscape::UI::Widget::SpinButton NoOfColsSpinner; + bool AutoRowSize; + Gtk::CheckButton RowHeightButton; + + Gtk::Box XByYLabelVBox; + Gtk::Label padXByYLabel; + Gtk::Label XByYLabel; + + // Number per Column + Gtk::Box NoOfRowsBox; + Gtk::Label NoOfRowsLabel; + Inkscape::UI::Widget::SpinButton NoOfRowsSpinner; + bool AutoColSize; + Gtk::CheckButton ColumnWidthButton; + + // Alignment + Gtk::Label AlignLabel; + Inkscape::UI::Widget::AnchorSelector AlignmentSelector; + double VertAlign; + double HorizAlign; + + Inkscape::UI::Widget::UnitMenu PaddingUnitMenu; + Inkscape::UI::Widget::ScalarUnit XPadding; + Inkscape::UI::Widget::ScalarUnit YPadding; + Gtk::Grid *PaddingTable; + + // BBox or manual spacing + Gtk::VBox SpacingVBox; + Gtk::RadioButtonGroup SpacingGroup; + Gtk::RadioButton SpaceByBBoxRadioButton; + Gtk::RadioButton SpaceManualRadioButton; + bool ManualSpacing; + + // Row height + Gtk::Box RowHeightBox; + Inkscape::UI::Widget::SpinButton RowHeightSpinner; + + // Column width + Gtk::Box ColumnWidthBox; + Inkscape::UI::Widget::SpinButton ColumnWidthSpinner; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_GRID_ARRANGE_TAB_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/guides.cpp b/src/ui/dialog/guides.cpp new file mode 100644 index 0000000..03743b8 --- /dev/null +++ b/src/ui/dialog/guides.cpp @@ -0,0 +1,358 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Simple guideline dialog. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Andrius R. <knutux@gmail.com> + * Johan Engelen + * Abhishek Sharma + * + * Copyright (C) 1999-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "guides.h" + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +#include "display/guideline.h" + +#include "ui/dialog-events.h" +#include "ui/tools/tool-base.h" + +#include "widgets/desktop-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +GuidelinePropertiesDialog::GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop) +: _desktop(desktop), _guide(guide), + _locked_toggle(_("Lo_cked")), + _relative_toggle(_("Rela_tive change")), + _spin_button_x(C_("Guides", "_X:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _spin_button_y(C_("Guides", "_Y:"), "", UNIT_TYPE_LINEAR, "", "", &_unit_menu), + _label_entry(_("_Label:"), _("Optionally give this guideline a name")), + _spin_angle(_("_Angle:"), "", UNIT_TYPE_RADIAL), + _mode(true), _oldpos(0.,0.), _oldangle(0.0) +{ + _locked_toggle.set_use_underline(); + _locked_toggle.set_tooltip_text(_("Lock the movement of guides")); + _relative_toggle.set_use_underline(); + _relative_toggle.set_tooltip_text(_("Move and/or rotate the guide relative to current settings")); +} + +bool GuidelinePropertiesDialog::_relative_toggle_status = false; // initialize relative checkbox status for when this dialog is opened for first time +Glib::ustring GuidelinePropertiesDialog::_angle_unit_status = DEG; // initialize angle unit status + +GuidelinePropertiesDialog::~GuidelinePropertiesDialog() { + // save current status + _relative_toggle_status = _relative_toggle.get_active(); + _angle_unit_status = _spin_angle.getUnit()->abbr; +} + +void GuidelinePropertiesDialog::showDialog(SPGuide *guide, SPDesktop *desktop) { + GuidelinePropertiesDialog dialog(guide, desktop); + dialog._setup(); + dialog.run(); +} + +void GuidelinePropertiesDialog::_modeChanged() +{ + _mode = !_relative_toggle.get_active(); + if (!_mode) { + // relative + _spin_angle.setValue(0); + + _spin_button_y.setValue(0); + _spin_button_x.setValue(0); + } else { + // absolute + _spin_angle.setValueKeepUnit(_oldangle, DEG); + + _spin_button_x.setValueKeepUnit(_oldpos[Geom::X], "px"); + _spin_button_y.setValueKeepUnit(_oldpos[Geom::Y], "px"); + } +} + +void GuidelinePropertiesDialog::_onOK() +{ + double deg_angle = _spin_angle.getValue(DEG); + if (!_mode) + deg_angle += _oldangle; + Geom::Point normal; + if ( deg_angle == 90. || deg_angle == 270. || deg_angle == -90. || deg_angle == -270.) { + normal = Geom::Point(1.,0.); + } else if ( deg_angle == 0. || deg_angle == 180. || deg_angle == -180.) { + normal = Geom::Point(0.,1.); + } else { + double rad_angle = Geom::rad_from_deg( deg_angle ); + normal = Geom::rot90(Geom::Point::polar(rad_angle, 1.0)); + } + //To allow reposition from dialog + _guide->set_locked(false, false); + + _guide->set_normal(normal, true); + + double const points_x = _spin_button_x.getValue("px"); + double const points_y = _spin_button_y.getValue("px"); + Geom::Point newpos(points_x, points_y); + if (!_mode) + newpos += _oldpos; + + _guide->moveto(newpos, true); + + const gchar* name = g_strdup( _label_entry.getEntry()->get_text().c_str() ); + + _guide->set_label(name, true); + + const bool locked = _locked_toggle.get_active(); + + _guide->set_locked(locked, true); + + g_free((gpointer) name); + + const auto c = _color.get_rgba(); + unsigned r = c.get_red_u()/257, g = c.get_green_u()/257, b = c.get_blue_u()/257; + //TODO: why 257? verify this! + // don't know why, but introduced: 761f7da58cd6d625b88c24eee6fae1b7fa3bfcdd + + _guide->set_color(r, g, b, true); + + DocumentUndo::done(_guide->document, SP_VERB_NONE, + _("Set guide properties")); +} + +void GuidelinePropertiesDialog::_onDelete() +{ + SPDocument *doc = _guide->document; + sp_guide_remove(_guide); + DocumentUndo::done(doc, SP_VERB_NONE, + _("Delete guide")); +} + +void GuidelinePropertiesDialog::_onDuplicate() +{ + _guide->duplicate(); + DocumentUndo::done(_guide->document, SP_VERB_NONE, _("Duplicate guide")); +} + +void GuidelinePropertiesDialog::_response(gint response) +{ + switch (response) { + case Gtk::RESPONSE_OK: + _onOK(); + break; + case -12: + _onDelete(); + break; + case -13: + _onDuplicate(); + break; + case Gtk::RESPONSE_CANCEL: + break; + case Gtk::RESPONSE_DELETE_EVENT: + break; + default: + g_assert_not_reached(); + } +} + +void GuidelinePropertiesDialog::_setup() { + set_title(_("Guideline")); + add_button(_("_OK"), Gtk::RESPONSE_OK); + add_button(_("_Duplicate"), -13); + add_button(_("_Delete"), -12); + add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _layout_table.set_border_width(4); + + mainVBox->pack_start(_layout_table, false, false, 0); + + _label_name.set_label("foo0"); + _label_name.set_halign(Gtk::ALIGN_START); + _label_name.set_valign(Gtk::ALIGN_CENTER); + + _label_descr.set_label("foo1"); + _label_descr.set_halign(Gtk::ALIGN_START); + _label_descr.set_valign(Gtk::ALIGN_CENTER); + + _label_name.set_halign(Gtk::ALIGN_FILL); + _label_name.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_name, 0, 0, 3, 1); + + _label_descr.set_halign(Gtk::ALIGN_FILL); + _label_descr.set_valign(Gtk::ALIGN_FILL); + _layout_table.attach(_label_descr, 0, 1, 3, 1); + + _label_entry.set_halign(Gtk::ALIGN_FILL); + _label_entry.set_valign(Gtk::ALIGN_FILL); + _label_entry.set_hexpand(); + _layout_table.attach(_label_entry, 1, 2, 2, 1); + + _color.set_halign(Gtk::ALIGN_FILL); + _color.set_valign(Gtk::ALIGN_FILL); + _color.set_hexpand(); + _color.set_margin_end(6); + _layout_table.attach(_color, 1, 3, 2, 1); + + // unitmenus + /* fixme: We should allow percents here too, as percents of the canvas size */ + _unit_menu.setUnitType(UNIT_TYPE_LINEAR); + _unit_menu.setUnit("px"); + if (_desktop->namedview->display_units) { + _unit_menu.setUnit( _desktop->namedview->display_units->abbr ); + } + _spin_angle.setUnit(_angle_unit_status); + + // position spinbuttons + _spin_button_x.setDigits(3); + _spin_button_x.setIncrements(1.0, 10.0); + _spin_button_x.setRange(-1e6, 1e6); + _spin_button_y.setDigits(3); + _spin_button_y.setIncrements(1.0, 10.0); + _spin_button_y.setRange(-1e6, 1e6); + + _spin_button_x.set_halign(Gtk::ALIGN_FILL); + _spin_button_x.set_valign(Gtk::ALIGN_FILL); + _spin_button_x.set_hexpand(); + _layout_table.attach(_spin_button_x, 1, 4, 1, 1); + + _spin_button_y.set_halign(Gtk::ALIGN_FILL); + _spin_button_y.set_valign(Gtk::ALIGN_FILL); + _spin_button_y.set_hexpand(); + _layout_table.attach(_spin_button_y, 1, 5, 1, 1); + + _unit_menu.set_halign(Gtk::ALIGN_FILL); + _unit_menu.set_valign(Gtk::ALIGN_FILL); + _unit_menu.set_margin_end(6); + _layout_table.attach(_unit_menu, 2, 4, 1, 1); + + // angle spinbutton + _spin_angle.setDigits(3); + _spin_angle.setIncrements(1.0, 10.0); + _spin_angle.setRange(-3600., 3600.); + + _spin_angle.set_halign(Gtk::ALIGN_FILL); + _spin_angle.set_valign(Gtk::ALIGN_FILL); + _spin_angle.set_hexpand(); + _layout_table.attach(_spin_angle, 1, 6, 2, 1); + + // mode radio button + _relative_toggle.set_halign(Gtk::ALIGN_FILL); + _relative_toggle.set_valign(Gtk::ALIGN_FILL); + _relative_toggle.set_hexpand(); + _relative_toggle.set_margin_start(6); + _layout_table.attach(_relative_toggle, 1, 7, 2, 1); + + // locked radio button + _locked_toggle.set_halign(Gtk::ALIGN_FILL); + _locked_toggle.set_valign(Gtk::ALIGN_FILL); + _locked_toggle.set_hexpand(); + _locked_toggle.set_margin_start(6); + _layout_table.attach(_locked_toggle, 1, 8, 2, 1); + + _relative_toggle.signal_toggled().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_modeChanged)); + _relative_toggle.set_active(_relative_toggle_status); + + bool global_guides_lock = _desktop->namedview->lockguides; + if(global_guides_lock){ + _locked_toggle.set_sensitive(false); + } + _locked_toggle.set_active(_guide->getLocked()); + + // don't know what this exactly does, but it results in that the dialog closes when entering a value and pressing enter (see LP bug 484187) + g_signal_connect_swapped(G_OBJECT(_spin_button_x.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_button_y.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + g_signal_connect_swapped(G_OBJECT(_spin_angle.getWidget()->gobj()), "activate", + G_CALLBACK(gtk_window_activate_default), gobj()); + + + // dialog + set_default_response(Gtk::RESPONSE_OK); + signal_response().connect(sigc::mem_fun(*this, &GuidelinePropertiesDialog::_response)); + + // initialize dialog + _oldpos = _guide->getPoint(); + if (_guide->isVertical()) { + _oldangle = 90; + } else if (_guide->isHorizontal()) { + _oldangle = 0; + } else { + _oldangle = Geom::deg_from_rad( std::atan2( - _guide->getNormal()[Geom::X], _guide->getNormal()[Geom::Y] ) ); + } + + { + // FIXME holy crap!!! + Inkscape::XML::Node *repr = _guide->getRepr(); + const gchar *guide_id = repr->attribute("id"); + gchar *label = g_strdup_printf(_("Guideline ID: %s"), guide_id); + _label_name.set_label(label); + g_free(label); + } + { + gchar *guide_description = _guide->description(false); + gchar *label = g_strdup_printf(_("Current: %s"), guide_description); + g_free(guide_description); + _label_descr.set_markup(label); + g_free(label); + } + + // init name entry + _label_entry.getEntry()->set_text(_guide->getLabel() ? _guide->getLabel() : ""); + + Gdk::RGBA c; + c.set_rgba(((_guide->getColor()>>24)&0xff) / 255.0, ((_guide->getColor()>>16)&0xff) / 255.0, ((_guide->getColor()>>8)&0xff) / 255.0); + _color.set_rgba(c); + + _modeChanged(); // sets values of spinboxes. + + if ( _oldangle == 90. || _oldangle == 270. || _oldangle == -90. || _oldangle == -270.) { + _spin_button_x.grabFocusAndSelectEntry(); + } else if ( _oldangle == 0. || _oldangle == 180. || _oldangle == -180.) { + _spin_button_y.grabFocusAndSelectEntry(); + } else { + _spin_angle.grabFocusAndSelectEntry(); + } + + set_position(Gtk::WIN_POS_MOUSE); + + show_all_children(); + set_modal(true); + _desktop->setWindowTransient (gobj()); + property_destroy_with_parent() = true; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/guides.h b/src/ui/dialog/guides.h new file mode 100644 index 0000000..abfc7d3 --- /dev/null +++ b/src/ui/dialog/guides.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Andrius R. <knutux@gmail.com> + * Johan Engelen + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_GUIDELINE_H +#define INKSCAPE_DIALOG_GUIDELINE_H + +#include <gtkmm/checkbutton.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/dialog.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> + +#include "ui/widget/unit-menu.h" +#include "ui/widget/scalar-unit.h" +#include "ui/widget/entry.h" +#include <2geom/point.h> + +class SPGuide; +class SPDesktop; + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitMenu; +}; + +namespace Dialogs { + +/** + * Dialog for modifying guidelines. + */ +class GuidelinePropertiesDialog : public Gtk::Dialog { +public: + GuidelinePropertiesDialog(SPGuide *guide, SPDesktop *desktop); + ~GuidelinePropertiesDialog() override; + + Glib::ustring getName() const { return "GuidelinePropertiesDialog"; } + + static void showDialog(SPGuide *guide, SPDesktop *desktop); + +protected: + void _setup(); + + void _onOK(); + void _onDelete(); + void _onDuplicate(); + + void _response(gint response); + void _modeChanged(); + +private: + GuidelinePropertiesDialog(GuidelinePropertiesDialog const &) = delete; // no copy + GuidelinePropertiesDialog &operator=(GuidelinePropertiesDialog const &) = delete; // no assign + + SPDesktop *_desktop; + SPGuide *_guide; + + Gtk::Grid _layout_table; + Gtk::Label _label_name; + Gtk::Label _label_descr; + Gtk::CheckButton _locked_toggle; + Gtk::CheckButton _relative_toggle; + static bool _relative_toggle_status; // remember the status of the _relative_toggle_status button across instances + Inkscape::UI::Widget::UnitMenu _unit_menu; + Inkscape::UI::Widget::ScalarUnit _spin_button_x; + Inkscape::UI::Widget::ScalarUnit _spin_button_y; + Inkscape::UI::Widget::Entry _label_entry; + Gtk::ColorButton _color; + + Inkscape::UI::Widget::ScalarUnit _spin_angle; + static Glib::ustring _angle_unit_status; // remember the status of the _relative_toggle_status button across instances + + bool _mode; + Geom::Point _oldpos; + gdouble _oldangle; +}; + +} // namespace +} // namespace +} // namespace + + +#endif // INKSCAPE_DIALOG_GUIDELINE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/icon-preview.cpp b/src/ui/dialog/icon-preview.cpp new file mode 100644 index 0000000..f8e73e3 --- /dev/null +++ b/src/ui/dialog/icon-preview.cpp @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2005,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/timer.h> +#include <glibmm/main.h> + +#include <gtkmm/buttonbox.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/frame.h> + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "icon-preview.h" + +#include "ui/widget/frame.h" + +extern "C" { +// takes doc, drawing, icon, and icon name to produce pixels +guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + const gchar *name, unsigned int psize, unsigned &stride); +} + +#define noICON_VERBOSE 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +IconPreviewPanel &IconPreviewPanel::getInstance() +{ + IconPreviewPanel *instance = new IconPreviewPanel(); + + instance->refreshPreview(); + + return *instance; +} + +//######################################################################### +//## E V E N T S +//######################################################################### + +void IconPreviewPanel::on_button_clicked(int which) +{ + if ( hot != which ) { + buttons[hot]->set_active( false ); + + hot = which; + updateMagnify(); + _getContents()->queue_draw(); + } +} + + + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +IconPreviewPanel::IconPreviewPanel() : + UI::Widget::Panel("/dialogs/iconpreview", SP_VERB_VIEW_ICON_PREVIEW), + deskTrack(), + desktop(nullptr), + document(nullptr), + drawing(nullptr), + visionkey(0), + timer(nullptr), + renderTimer(nullptr), + pending(false), + minDelay(0.1), + targetId(), + hot(1), + selectionButton(nullptr), + desktopChangeConn(), + docReplacedConn(), + docModConn(), + selChangedConn() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + numEntries = 0; + + bool pack = prefs->getBool("/iconpreview/pack", true); + + std::vector<Glib::ustring> pref_sizes = prefs->getAllDirs("/iconpreview/sizes/default"); + std::vector<int> rawSizes; + + for (auto & pref_size : pref_sizes) { + if (prefs->getBool(pref_size + "/show", true)) { + int sizeVal = prefs->getInt(pref_size + "/value", -1); + if (sizeVal > 0) { + rawSizes.push_back(sizeVal); + } + } + } + + if ( !rawSizes.empty() ) { + numEntries = rawSizes.size(); + sizes = new int[numEntries]; + int i = 0; + for ( std::vector<int>::iterator it = rawSizes.begin(); it != rawSizes.end(); ++it, ++i ) { + sizes[i] = *it; + } + } + + if ( numEntries < 1 ) + { + numEntries = 5; + sizes = new int[numEntries]; + sizes[0] = 16; + sizes[1] = 24; + sizes[2] = 32; + sizes[3] = 48; + sizes[4] = 128; + } + + pixMem = new guchar*[numEntries]; + images = new Gtk::Image*[numEntries]; + labels = new Glib::ustring*[numEntries]; + buttons = new Gtk::ToggleToolButton*[numEntries]; + + + for ( int i = 0; i < numEntries; i++ ) { + char *label = g_strdup_printf(_("%d x %d"), sizes[i], sizes[i]); + labels[i] = new Glib::ustring(label); + g_free(label); + pixMem[i] = nullptr; + images[i] = nullptr; + } + + + magLabel.set_label( *labels[hot] ); + + Gtk::VBox* magBox = new Gtk::VBox(); + + UI::Widget::Frame *magFrame = Gtk::manage(new UI::Widget::Frame(_("Magnified:"))); + magFrame->add( magnified ); + + magBox->pack_start( *magFrame, Gtk::PACK_EXPAND_WIDGET ); + magBox->pack_start( magLabel, Gtk::PACK_SHRINK ); + + + Gtk::VBox *verts = new Gtk::VBox(); + Gtk::HBox *horiz = nullptr; + int previous = 0; + int avail = 0; + for ( int i = numEntries - 1; i >= 0; --i ) { + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + pixMem[i] = new guchar[sizes[i] * stride]; + memset( pixMem[i], 0x00, sizes[i] * stride ); + + GdkPixbuf *pb = gdk_pixbuf_new_from_data( pixMem[i], GDK_COLORSPACE_RGB, TRUE, 8, sizes[i], sizes[i], stride, /*(GdkPixbufDestroyNotify)g_free*/nullptr, nullptr ); + GtkImage* img = GTK_IMAGE( gtk_image_new_from_pixbuf( pb ) ); + images[i] = Glib::wrap(img); + Glib::ustring label(*labels[i]); + buttons[i] = new Gtk::ToggleToolButton(label); + buttons[i]->set_active( i == hot ); + if ( prefs->getBool("/iconpreview/showFrames", true) ) { + Gtk::Frame *frame = new Gtk::Frame(); + frame->set_shadow_type(Gtk::SHADOW_ETCHED_IN); + frame->add(*images[i]); + buttons[i]->set_icon_widget(*Gtk::manage(frame)); + } else { + buttons[i]->set_icon_widget(*images[i]); + } + + buttons[i]->set_tooltip_text(label); + + buttons[i]->signal_clicked().connect( sigc::bind<int>( sigc::mem_fun(*this, &IconPreviewPanel::on_button_clicked), i) ); + + buttons[i]->set_halign(Gtk::ALIGN_CENTER); + buttons[i]->set_valign(Gtk::ALIGN_CENTER); + + if ( !pack || ( (avail == 0) && (previous == 0) ) ) { + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + previous = sizes[i]; + avail = sizes[i]; + } else { + int pad = 12; + if ((avail < pad) || ((sizes[i] > avail) && (sizes[i] < previous))) { + horiz = nullptr; + } + if ((horiz == nullptr) && (sizes[i] <= previous)) { + avail = previous; + } + if (sizes[i] <= avail) { + if (!horiz) { + horiz = Gtk::manage(new Gtk::HBox()); + avail = previous; + verts->pack_end(*horiz, Gtk::PACK_SHRINK); + } + horiz->pack_start(*(buttons[i]), Gtk::PACK_EXPAND_WIDGET); + avail -= sizes[i]; + avail -= pad; // a little extra for padding + } else { + horiz = nullptr; + verts->pack_end(*(buttons[i]), Gtk::PACK_SHRINK); + } + } + } + + iconBox.pack_start(splitter); + splitter.pack1( *magBox, true, false ); + UI::Widget::Frame *actuals = Gtk::manage(new UI::Widget::Frame (_("Actual Size:"))); + actuals->set_border_width(4); + actuals->add(*verts); + splitter.pack2( *actuals, false, false ); + + + selectionButton = new Gtk::CheckButton(C_("Icon preview window", "Sele_ction"), true);//selectionButton = (Gtk::ToggleButton*) gtk_check_button_new_with_mnemonic(_("_Selection")); // , GTK_RESPONSE_APPLY + magBox->pack_start( *selectionButton, Gtk::PACK_SHRINK ); + selectionButton->set_tooltip_text(_("Selection only or whole document")); + selectionButton->signal_clicked().connect( sigc::mem_fun(*this, &IconPreviewPanel::modeToggled) ); + + gint val = prefs->getBool("/iconpreview/selectionOnly"); + selectionButton->set_active( val != 0 ); + + + _getContents()->pack_start(iconBox, Gtk::PACK_SHRINK); + + show_all_children(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &IconPreviewPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +IconPreviewPanel::~IconPreviewPanel() +{ + setDesktop(nullptr); + if (timer) { + timer->stop(); + delete timer; + timer = nullptr; + } + if ( renderTimer ) { + renderTimer->stop(); + delete renderTimer; + renderTimer = nullptr; + } + + selChangedConn.disconnect(); + docModConn.disconnect(); + docReplacedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +//######################################################################### +//## M E T H O D S +//######################################################################### + + +#if ICON_VERBOSE +static Glib::ustring getTimestr() +{ + Glib::ustring str; + gint64 micr = g_get_monotonic_time(); + gint64 mins = ((int)round(micr / 60000000)) % 60; + gdouble dsecs = micr / 1000000; + gchar *ptr = g_strdup_printf(":%02u:%f", mins, dsecs); + str = ptr; + g_free(ptr); + ptr = 0; + return str; +} +#endif // ICON_VERBOSE + +void IconPreviewPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + SPDocument *newDoc = (desktop) ? desktop->doc() : nullptr; + + if ( desktop != this->desktop ) { + docReplacedConn.disconnect(); + selChangedConn.disconnect(); + + this->desktop = Panel::getDesktop(); + if ( this->desktop ) { + docReplacedConn = this->desktop->connectDocumentReplaced(sigc::hide<0>(sigc::mem_fun(this, &IconPreviewPanel::setDocument))); + if ( this->desktop->selection && Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + selChangedConn = this->desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + } + } + setDocument(newDoc); + deskTrack.setBase(desktop); +} + +void IconPreviewPanel::setDocument( SPDocument *document ) +{ + if (this->document != document) { + docModConn.disconnect(); + if (drawing) { + this->document->getRoot()->invoke_hide(visionkey); + delete drawing; + drawing = nullptr; + } + this->document = document; + if (this->document) { + drawing = new Inkscape::Drawing(); + visionkey = SPItem::display_key_new(1); + drawing->setRoot(this->document->getRoot()->invoke_show(*drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + + if ( Inkscape::Preferences::get()->getBool("/iconpreview/autoRefresh", true) ) { + docModConn = this->document->connectModified(sigc::hide(sigc::mem_fun(this, &IconPreviewPanel::queueRefresh))); + } + queueRefresh(); + } + } +} + +void IconPreviewPanel::refreshPreview() +{ + SPDesktop *desktop = getDesktop(); + if (!timer) { + timer = new Glib::Timer(); + } + if (timer->elapsed() < minDelay) { +#if ICON_VERBOSE + g_message( "%s Deferring refresh as too soon. calling queueRefresh()", getTimestr().c_str() ); +#endif //ICON_VERBOSE + // Do not refresh too quickly + queueRefresh(); + } else if ( desktop && desktop->doc() ) { +#if ICON_VERBOSE + g_message( "%s Refreshing preview.", getTimestr().c_str() ); +#endif // ICON_VERBOSE + bool hold = Inkscape::Preferences::get()->getBool("/iconpreview/selectionHold", true); + SPObject *target = nullptr; + if ( selectionButton && selectionButton->get_active() ) + { + target = (hold && !targetId.empty()) ? desktop->doc()->getObjectById( targetId.c_str() ) : nullptr; + if ( !target ) { + targetId.clear(); + Inkscape::Selection * sel = desktop->getSelection(); + if ( sel ) { + //g_message("found a selection to play with"); + + auto items = sel->items(); + for(auto i=items.begin();!target && i!=items.end();++i){ + SPItem* item = *i; + gchar const *id = item->getId(); + if ( id ) { + targetId = id; + target = item; + } + } + } + } + } else { + target = desktop->currentRoot(); + } + if ( target ) { + renderPreview(target); + } +#if ICON_VERBOSE + g_message( "%s resetting timer", getTimestr().c_str() ); +#endif // ICON_VERBOSE + timer->reset(); + } +} + +bool IconPreviewPanel::refreshCB() +{ + bool callAgain = true; + if (!timer) { + timer = new Glib::Timer(); + } + if ( timer->elapsed() > minDelay ) { +#if ICON_VERBOSE + g_message( "%s refreshCB() timer has progressed", getTimestr().c_str() ); +#endif // ICON_VERBOSE + callAgain = false; + refreshPreview(); +#if ICON_VERBOSE + g_message( "%s refreshCB() setting pending false", getTimestr().c_str() ); +#endif // ICON_VERBOSE + pending = false; + } + return callAgain; +} + +void IconPreviewPanel::queueRefresh() +{ + if (!pending) { + pending = true; +#if ICON_VERBOSE + g_message( "%s queueRefresh() Setting pending true", getTimestr().c_str() ); +#endif // ICON_VERBOSE + if (!timer) { + timer = new Glib::Timer(); + } + Glib::signal_idle().connect( sigc::mem_fun(this, &IconPreviewPanel::refreshCB), Glib::PRIORITY_DEFAULT_IDLE ); + } +} + +void IconPreviewPanel::modeToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool selectionOnly = (selectionButton && selectionButton->get_active()); + prefs->setBool("/iconpreview/selectionOnly", selectionOnly); + if ( !selectionOnly ) { + targetId.clear(); + } + + refreshPreview(); +} + +void overlayPixels(guchar *px, int width, int height, int stride, + unsigned r, unsigned g, unsigned b) +{ + int bytesPerPixel = 4; + int spacing = 4; + for ( int y = 0; y < height; y += spacing ) { + guchar *ptr = px + y * stride; + for ( int x = 0; x < width; x += spacing ) { + *(ptr++) = r; + *(ptr++) = g; + *(ptr++) = b; + *(ptr++) = 0xff; + + ptr += bytesPerPixel * (spacing - 1); + } + } + + if ( width > 1 && height > 1 ) { + // point at the last pixel + guchar *ptr = px + ((height-1) * stride) + ((width - 1) * bytesPerPixel); + + if ( width > 2 ) { + px[4] = r; + px[5] = g; + px[6] = b; + px[7] = 0xff; + + ptr[-12] = r; + ptr[-11] = g; + ptr[-10] = b; + ptr[-9] = 0xff; + } + + ptr[-4] = r; + ptr[-3] = g; + ptr[-2] = b; + ptr[-1] = 0xff; + + px[0 + stride] = r; + px[1 + stride] = g; + px[2 + stride] = b; + px[3 + stride] = 0xff; + + ptr[0 - stride] = r; + ptr[1 - stride] = g; + ptr[2 - stride] = b; + ptr[3 - stride] = 0xff; + + if ( height > 2 ) { + ptr[0 - stride * 3] = r; + ptr[1 - stride * 3] = g; + ptr[2 - stride * 3] = b; + ptr[3 - stride * 3] = 0xff; + } + } +} + +// takes doc, drawing, icon, and icon name to produce pixels +extern "C" guchar * +sp_icon_doc_icon( SPDocument *doc, Inkscape::Drawing &drawing, + gchar const *name, unsigned psize, + unsigned &stride) +{ + bool const dump = Inkscape::Preferences::get()->getBool("/debug/icons/dumpSvg"); + guchar *px = nullptr; + + if (doc) { + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_ITEM(object)) { + SPItem *item = SP_ITEM(object); + // Find bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if ( object->parent == nullptr ) + { + dbox = Geom::Rect(Geom::Point(0, 0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px"))); + } + + /* This is in document coordinates, i.e. pixels */ + if ( dbox ) { + /* Update to renderable state */ + double sf = 1.0; + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + /* Item integer bbox in points */ + // NOTE: previously, each rect coordinate was rounded using floor(c + 0.5) + Geom::IntRect ibox = dbox->roundOutwards(); + + if ( dump ) { + g_message( " box --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + int width = ibox.width(); + int height = ibox.height(); + + if ( dump ) { + g_message( " vis --'%s' (%d,%d)", name, width, height ); + } + + { + int block = std::max(width, height); + if (block != static_cast<int>(psize) ) { + if ( dump ) { + g_message(" resizing" ); + } + sf = (double)psize / (double)block; + + drawing.root()->setTransform(Geom::Scale(sf)); + drawing.update(); + + auto scaled_box = *dbox * Geom::Scale(sf); + ibox = scaled_box.roundOutwards(); + if ( dump ) { + g_message( " box2 --'%s' (%f,%f)-(%f,%f)", name, (double)ibox.left(), (double)ibox.top(), (double)ibox.right(), (double)ibox.bottom() ); + } + + /* Find button visible area */ + width = ibox.width(); + height = ibox.height(); + if ( dump ) { + g_message( " vis2 --'%s' (%d,%d)", name, width, height ); + } + } + } + + Geom::IntPoint pdim(psize, psize); + int dx, dy; + //dx = (psize - width) / 2; + //dy = (psize - height) / 2; + dx=dy=psize; + dx=(dx-width)/2; // watch out for psize, since 'unsigned'-'signed' can cause problems if the result is negative + dy=(dy-height)/2; + Geom::IntRect area = Geom::IntRect::from_xywh(ibox.min() - Geom::IntPoint(dx,dy), pdim); + /* Actual renderable area */ + Geom::IntRect ua = *Geom::intersect(ibox, area); + + if ( dump ) { + g_message( " area --'%s' (%f,%f)-(%f,%f)", name, (double)area.left(), (double)area.top(), (double)area.right(), (double)area.bottom() ); + g_message( " ua --'%s' (%f,%f)-(%f,%f)", name, (double)ua.left(), (double)ua.top(), (double)ua.right(), (double)ua.bottom() ); + } + + stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, psize); + + /* Set up pixblock */ + px = g_new(guchar, stride * psize); + memset(px, 0x00, stride * psize); + + /* Render */ + cairo_surface_t *s = cairo_image_surface_create_for_data(px, + CAIRO_FORMAT_ARGB32, psize, psize, stride); + Inkscape::DrawingContext dc(s, ua.min()); + + SPNamedView *nv = sp_document_namedview(doc, nullptr); + float bg_r = SP_RGBA32_R_F(nv->pagecolor); + float bg_g = SP_RGBA32_G_F(nv->pagecolor); + float bg_b = SP_RGBA32_B_F(nv->pagecolor); + float bg_a = SP_RGBA32_A_F(nv->pagecolor); + + cairo_t *cr = cairo_create(s); + cairo_set_source_rgba(cr, bg_r, bg_g, bg_b, bg_a); + cairo_rectangle(cr, 0, 0, psize, psize); + cairo_fill(cr); + cairo_save(cr); + cairo_destroy(cr); + + drawing.render(dc, ua); + cairo_surface_destroy(s); + + // convert to GdkPixbuf format + convert_pixels_argb32_to_pixbuf(px, psize, psize, stride); + + if ( Inkscape::Preferences::get()->getBool("/debug/icons/overlaySvg") ) { + overlayPixels( px, psize, psize, stride, 0x00, 0x00, 0xff ); + } + } + } + } + + return px; +} // end of sp_icon_doc_icon() + + +void IconPreviewPanel::renderPreview( SPObject* obj ) +{ + SPDocument * doc = obj->document; + gchar const * id = obj->getId(); + if ( !renderTimer ) { + renderTimer = new Glib::Timer(); + } + renderTimer->reset(); + +#if ICON_VERBOSE + g_message("%s setting up to render '%s' as the icon", getTimestr().c_str(), id ); +#endif // ICON_VERBOSE + + for ( int i = 0; i < numEntries; i++ ) { + unsigned unused; + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, sizes[i]); + guchar *px = sp_icon_doc_icon(doc, *drawing, id, sizes[i], unused); +// g_message( " size %d %s", sizes[i], (px ? "worked" : "failed") ); + if ( px ) { + memcpy( pixMem[i], px, sizes[i] * stride ); + g_free( px ); + px = nullptr; + } else { + memset( pixMem[i], 0, sizes[i] * stride ); + } + images[i]->set(images[i]->get_pixbuf()); + // images[i]->queue_draw(); + } + updateMagnify(); + + renderTimer->stop(); + minDelay = std::max( 0.1, renderTimer->elapsed() * 3.0 ); +#if ICON_VERBOSE + g_message(" render took %f seconds.", renderTimer->elapsed()); +#endif // ICON_VERBOSE +} + +void IconPreviewPanel::updateMagnify() +{ + Glib::RefPtr<Gdk::Pixbuf> buf = images[hot]->get_pixbuf()->scale_simple( 128, 128, Gdk::INTERP_NEAREST ); + magLabel.set_label( *labels[hot] ); + magnified.set( buf ); + // magnified.queue_draw(); + // magnified.get_parent()->queue_draw(); +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/icon-preview.h b/src/ui/dialog/icon-preview.h new file mode 100644 index 0000000..938bbbf --- /dev/null +++ b/src/ui/dialog/icon-preview.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A simple dialog for previewing icon representation. + */ +/* Authors: + * Jon A. Cruz + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004,2005 The Inkscape Organization + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_ICON_PREVIEW_H +#define SEEN_ICON_PREVIEW_H + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/label.h> +#include <gtkmm/paned.h> +#include <gtkmm/image.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/toggletoolbutton.h> + +#include "ui/widget/panel.h" +#include "desktop-tracker.h" + +class SPObject; +namespace Glib { +class Timer; +} + +namespace Inkscape { +class Drawing; +namespace UI { +namespace Dialog { + + +/** + * A panel that displays an icon preview + */ +class IconPreviewPanel : public UI::Widget::Panel +{ +public: + IconPreviewPanel(); + //IconPreviewPanel(Glib::ustring const &label); + ~IconPreviewPanel() override; + + static IconPreviewPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void refreshPreview(); + void modeToggled(); + +private: + IconPreviewPanel(IconPreviewPanel const &) = delete; // no copy + IconPreviewPanel &operator=(IconPreviewPanel const &) = delete; // no assign + + + DesktopTracker deskTrack; + SPDesktop *desktop; + SPDocument *document; + Drawing *drawing; + unsigned int visionkey; + Glib::Timer *timer; + Glib::Timer *renderTimer; + bool pending; + gdouble minDelay; + + Gtk::VBox iconBox; + Gtk::Paned splitter; + Glib::ustring targetId; + int hot; + int numEntries; + int* sizes; + + Gtk::Image magnified; + Gtk::Label magLabel; + + Gtk::ToggleButton *selectionButton; + + guchar** pixMem; + Gtk::Image** images; + Glib::ustring** labels; + Gtk::ToggleToolButton** buttons; + sigc::connection desktopChangeConn; + sigc::connection docReplacedConn; + sigc::connection docModConn; + sigc::connection selChangedConn; + + + void setDocument( SPDocument *document ); + void on_button_clicked(int which); + void renderPreview( SPObject* obj ); + void updateMagnify(); + void queueRefresh(); + bool refreshCB(); +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_ICON_PREVIEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp new file mode 100644 index 0000000..72a8107 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -0,0 +1,2772 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inkscape Preferences dialog - implementation. + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004-2013 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "inkscape-preferences.h" + +#include <gio/gio.h> +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/miscutils.h> +#include <gtk/gtksettings.h> +#include <gtkmm/cssprovider.h> +#include <gtkmm/main.h> +#include <gtkmm/recentinfo.h> +#include <gtkmm/recentmanager.h> + +#include "auto-save.h" +#include "cms-system.h" +#include "document.h" +#include "enums.h" +#include "inkscape-window.h" +#include "inkscape.h" +#include "message-stack.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selcue.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "shortcuts.h" +#include "verbs.h" + +/* #include "display/cairo-utils.h" */ +#include "display/canvas-grid.h" +#include "display/nr-filter-gaussian.h" + +#include "extension/internal/gdkpixbuf-input.h" + +#include "include/gtkmm_version.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "object/color-profile.h" +#include "style.h" +#include "svg/svg-color.h" +#include "ui/interface.h" +#include "ui/widget/style-swatch.h" +#include "widgets/desktop-widget.h" +#include <fstream> + +#if HAVE_ASPELL +# include "ui/dialog/spellcheck.h" // for get_available_langs +# ifdef _WIN32 +# include <windows.h> +# endif +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::UI::Widget::DialogPage; +using Inkscape::UI::Widget::PrefCheckButton; +using Inkscape::UI::Widget::PrefRadioButton; +using Inkscape::UI::Widget::PrefSpinButton; +using Inkscape::UI::Widget::StyleSwatch; +using Inkscape::CMSSystem; + +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + x.erase(x.find_last_not_of(' ') + 1); + +InkscapePreferences::InkscapePreferences() + : UI::Widget::Panel ("/dialogs/preferences", SP_VERB_DIALOG_DISPLAY), + _minimum_width(0), + _minimum_height(0), + _natural_width(0), + _natural_height(0), + _current_page(nullptr), + _init(true) +{ + //get the width of a spinbutton + Inkscape::UI::Widget::SpinButton* sb = new Inkscape::UI::Widget::SpinButton; + sb->set_width_chars(6); + _getContents()->add(*sb); + show_all_children(); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + sb->get_preferred_size(sreq_natural, sreq); + _sb_width = sreq.width; + _getContents()->remove(*sb); + delete sb; + + //Main HBox + auto hbox_list_page = Gtk::manage(new Gtk::Box()); + hbox_list_page->set_border_width(12); + hbox_list_page->set_spacing(12); + _getContents()->add(*hbox_list_page); + + //Pagelist + Gtk::Frame* list_frame = Gtk::manage(new Gtk::Frame()); + Gtk::ScrolledWindow* scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + hbox_list_page->pack_start(*list_frame, false, true, 0); + _page_list.set_headers_visible(false); + scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->set_propagate_natural_width(); + scrolled_window->set_propagate_natural_height(); + scrolled_window->add(_page_list); + list_frame->set_shadow_type(Gtk::SHADOW_IN); + list_frame->add(*scrolled_window); + _page_list_model = Gtk::TreeStore::create(_page_list_columns); + _page_list.set_model(_page_list_model); + _page_list.append_column("name",_page_list_columns._col_name); + Glib::RefPtr<Gtk::TreeSelection> page_list_selection = _page_list.get_selection(); + page_list_selection->signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::on_pagelist_selection_changed)); + page_list_selection->set_mode(Gtk::SELECTION_BROWSE); + + //Pages + auto vbox_page = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Frame* title_frame = Gtk::manage(new Gtk::Frame()); + + Gtk::ScrolledWindow* pageScroller = Gtk::manage(new Gtk::ScrolledWindow()); + pageScroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + pageScroller->set_propagate_natural_width(); + pageScroller->set_propagate_natural_height(); + pageScroller->add(*vbox_page); + hbox_list_page->pack_start(*pageScroller, true, true, 0); + + title_frame->add(_page_title); + vbox_page->pack_start(*title_frame, false, false, 0); + vbox_page->pack_start(_page_frame, true, true, 0); + _page_frame.set_shadow_type(Gtk::SHADOW_IN); + title_frame->set_shadow_type(Gtk::SHADOW_IN); + + initPageTools(); + initPageUI(); + initPageBehavior(); + initPageIO(); + + initPageSystem(); + initPageBitmaps(); + initPageRendering(); + initPageSpellcheck(); + + + signalPresent().connect(sigc::mem_fun(*this, &InkscapePreferences::_presentPages)); + + //calculate the size request for this dialog + _page_list.expand_all(); + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::GetSizeRequest)); + _page_list.collapse_all(); +} + +InkscapePreferences::~InkscapePreferences() += default; + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, int id) +{ + return AddPage(p, title, Gtk::TreeModel::iterator() , id); +} + +Gtk::TreeModel::iterator InkscapePreferences::AddPage(DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id) +{ + Gtk::TreeModel::iterator iter; + if (parent) + iter = _page_list_model->append((*parent).children()); + else + iter = _page_list_model->append(); + Gtk::TreeModel::Row row = *iter; + row[_page_list_columns._col_name] = title; + row[_page_list_columns._col_id] = id; + row[_page_list_columns._col_page] = &p; + return iter; +} + +void InkscapePreferences::AddSelcueCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show selection cue"), prefs_path + "/selcue", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display a selection cue (the same as in selector)")); +} + +void InkscapePreferences::AddGradientCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) +{ + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Enable gradient editing"), prefs_path + "/gradientdrag", def_value); + p.add_line( false, "", *cb, "", _("Whether selected objects display gradient editing controls")); +} + +void InkscapePreferences::AddConvertGuidesCheckbox(DialogPage &p, Glib::ustring const &prefs_path, bool def_value) { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Conversion to guides uses edges instead of bounding box"), prefs_path + "/convertguides", def_value); + p.add_line( false, "", *cb, "", _("Converting an object to guides places these along the object's true edges (imitating the object's shape), not along the bounding box")); +} + +void InkscapePreferences::AddDotSizeSpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/dot-size", 0.0, 1000.0, 0.1, 10.0, def_value, false, false); + p.add_line( false, _("Ctrl+click _dot size:"), *sb, _("times current stroke width"), + _("Size of dots created with Ctrl+click (relative to current stroke width)"), + false ); +} + +void InkscapePreferences::AddBaseSimplifySpinbutton(DialogPage &p, Glib::ustring const &prefs_path, double def_value) +{ + PrefSpinButton* sb = Gtk::manage( new PrefSpinButton); + sb->init ( prefs_path + "/base-simplify", 0.0, 100.0, 1.0, 10.0, def_value, false, false); + p.add_line( false, _("Base simplify:"), *sb, _("on dynamic LPE simplify"), + _("Base simplify of dynamic LPE based simplify"), + false ); +} + + +static void StyleFromSelectionToTool(Glib::ustring const &prefs_path, StyleSwatch *swatch) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) + return; + + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("<b>No objects selected</b> to take the style from.")); + return; + } + SPItem *item = selection->singleItem(); + if (!item) { + /* TODO: If each item in the selection has the same style then don't consider it an error. + * Maybe we should try to handle multiple selections anyway, e.g. the intersection of the + * style attributes for the selected items. */ + desktop->getMessageStack()->flash(Inkscape::ERROR_MESSAGE, + _("<b>More than one object selected.</b> Cannot take style from multiple objects.")); + return; + } + + SPCSSAttr *css = take_style_from_item (item); + + if (!css) return; + + // remove black-listed properties + css = sp_css_attr_unset_blacklist (css); + + // only store text style for the text tool + if (prefs_path != "/tools/text") { + css = sp_css_attr_unset_text (css); + } + + // we cannot store properties with uris - they will be invalid in other documents + css = sp_css_attr_unset_uris (css); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setStyle(prefs_path + "/style", css); + sp_repr_css_attr_unref (css); + + // update the swatch + if (swatch) { + SPCSSAttr *css = prefs->getInheritedStyle(prefs_path + "/style"); + swatch->setStyle (css); + sp_repr_css_attr_unref(css); + } +} + +void InkscapePreferences::AddNewObjectsStyle(DialogPage &p, Glib::ustring const &prefs_path, const gchar *banner) +{ + if (banner) + p.add_group_header(banner); + else + p.add_group_header( _("Style of new objects")); + PrefRadioButton* current = Gtk::manage( new PrefRadioButton); + current->init ( _("Last used style"), prefs_path + "/usecurrent", 1, true, nullptr); + p.add_line( true, "", *current, "", + _("Apply the style you last set on an object")); + + PrefRadioButton* own = Gtk::manage( new PrefRadioButton); + auto hb = Gtk::manage( new Gtk::Box); + own->init ( _("This tool's own style:"), prefs_path + "/usecurrent", 0, false, current); + own->set_halign(Gtk::ALIGN_START); + own->set_valign(Gtk::ALIGN_START); + hb->add(*own); + p.set_tip( *own, _("Each tool may store its own style to apply to the newly created objects. Use the button below to set it.")); + p.add_line( true, "", *hb, "", ""); + + // style swatch + Gtk::Button* button = Gtk::manage( new Gtk::Button(_("Take from selection"), true)); + StyleSwatch *swatch = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getInt(prefs_path + "/usecurrent")) { + button->set_sensitive(false); + } + + SPCSSAttr *css = prefs->getStyle(prefs_path + "/style"); + swatch = new StyleSwatch(css, _("This tool's style of new objects")); + hb->add(*swatch); + sp_repr_css_attr_unref(css); + + button->signal_clicked().connect( sigc::bind( sigc::ptr_fun(StyleFromSelectionToTool), prefs_path, swatch) ); + own->changed_signal.connect( sigc::mem_fun(*button, &Gtk::Button::set_sensitive) ); + p.add_line( true, "", *button, "", + _("Remember the style of the (first) selected object as this tool's style")); +} + +void InkscapePreferences::initPageTools() +{ + Gtk::TreeModel::iterator iter_tools = this->AddPage(_page_tools, _("Tools"), PREFS_PAGE_TOOLS); + this->AddPage(_page_selector, _("Selector"), iter_tools, PREFS_PAGE_TOOLS_SELECTOR); + this->AddPage(_page_node, _("Node"), iter_tools, PREFS_PAGE_TOOLS_NODE); + + // shapes + Gtk::TreeModel::iterator iter_shapes = this->AddPage(_page_shapes, _("Shapes"), iter_tools, PREFS_PAGE_TOOLS_SHAPES); + this->AddPage(_page_rectangle, _("Rectangle"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_RECT); + this->AddPage(_page_ellipse, _("Ellipse"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_ELLIPSE); + this->AddPage(_page_star, _("Star"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_STAR); + this->AddPage(_page_3dbox, _("3D Box"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_3DBOX); + this->AddPage(_page_spiral, _("Spiral"), iter_shapes, PREFS_PAGE_TOOLS_SHAPES_SPIRAL); + + this->AddPage(_page_pen, _("Pen"), iter_tools, PREFS_PAGE_TOOLS_PEN); + this->AddPage(_page_pencil, _("Pencil"), iter_tools, PREFS_PAGE_TOOLS_PENCIL); + this->AddPage(_page_calligraphy, _("Calligraphy"), iter_tools, PREFS_PAGE_TOOLS_CALLIGRAPHY); + this->AddPage(_page_text, C_("ContextVerb", "Text"), iter_tools, PREFS_PAGE_TOOLS_TEXT); + + this->AddPage(_page_gradient, _("Gradient"), iter_tools, PREFS_PAGE_TOOLS_GRADIENT); + this->AddPage(_page_dropper, _("Dropper"), iter_tools, PREFS_PAGE_TOOLS_DROPPER); + this->AddPage(_page_paintbucket, _("Paint Bucket"), iter_tools, PREFS_PAGE_TOOLS_PAINTBUCKET); + + this->AddPage(_page_tweak, _("Tweak"), iter_tools, PREFS_PAGE_TOOLS_TWEAK); + this->AddPage(_page_spray, _("Spray"), iter_tools, PREFS_PAGE_TOOLS_SPRAY); + this->AddPage(_page_eraser, _("Eraser"), iter_tools, PREFS_PAGE_TOOLS_ERASER); + this->AddPage(_page_connector, _("Connector"), iter_tools, PREFS_PAGE_TOOLS_CONNECTOR); +#ifdef WITH_LPETOOL + this->AddPage(_page_lpetool, _("LPE Tool"), iter_tools, PREFS_PAGE_TOOLS_LPETOOL); +#endif // WITH_LPETOOL + this->AddPage(_page_zoom, _("Zoom"), iter_tools, PREFS_PAGE_TOOLS_ZOOM); + this->AddPage(_page_measure, C_("ContextVerb", "Measure"), iter_tools, PREFS_PAGE_TOOLS_MEASURE); + + _path_tools = _page_list.get_model()->get_path(iter_tools); + + _page_tools.add_group_header( _("Bounding box to use")); + _t_bbox_visual.init ( _("Visual bounding box"), "/tools/bounding_box", 0, false, nullptr); // 0 means visual + _page_tools.add_line( true, "", _t_bbox_visual, "", + _("This bounding box includes stroke width, markers, filter margins, etc.")); + _t_bbox_geometric.init ( _("Geometric bounding box"), "/tools/bounding_box", 1, true, &_t_bbox_visual); // 1 means geometric + _page_tools.add_line( true, "", _t_bbox_geometric, "", + _("This bounding box includes only the bare path")); + + _page_tools.add_group_header( _("Conversion to guides")); + _t_cvg_keep_objects.init ( _("Keep objects after conversion to guides"), "/tools/cvg_keep_objects", false); + _page_tools.add_line( true, "", _t_cvg_keep_objects, "", + _("When converting an object to guides, don't delete the object after the conversion")); + _t_cvg_convert_whole_groups.init ( _("Treat groups as a single object"), "/tools/cvg_convert_whole_groups", false); + _page_tools.add_line( true, "", _t_cvg_convert_whole_groups, "", + _("Treat groups as a single object during conversion to guides rather than converting each child separately")); + + _pencil_average_all_sketches.init ( _("Average all sketches"), "/tools/freehand/pencil/average_all_sketches", false); + _calligrapy_use_abs_size.init ( _("Width is in absolute units"), "/tools/calligraphic/abs_width", false); + _calligrapy_keep_selected.init ( _("Select new path"), "/tools/calligraphic/keep_selected", true); + _connector_ignore_text.init( _("Don't attach connectors to text objects"), "/tools/connector/ignoretext", true); + + //Selector + + + AddSelcueCheckbox(_page_selector, "/tools/select", false); + AddGradientCheckbox(_page_selector, "/tools/select", false); + _page_selector.add_group_header( _("When transforming, show")); + _t_sel_trans_obj.init ( _("Objects"), "/tools/select/show", "content", true, nullptr); + _page_selector.add_line( true, "", _t_sel_trans_obj, "", + _("Show the actual objects when moving or transforming")); + _t_sel_trans_outl.init ( _("Box outline"), "/tools/select/show", "outline", false, &_t_sel_trans_obj); + _page_selector.add_line( true, "", _t_sel_trans_outl, "", + _("Show only a box outline of the objects when moving or transforming")); + _page_selector.add_group_header( _("Per-object selection cue")); + _t_sel_cue_none.init ( C_("Selection cue", "None"), "/options/selcue/value", Inkscape::SelCue::NONE, false, nullptr); + _page_selector.add_line( true, "", _t_sel_cue_none, "", + _("No per-object selection indication")); + _t_sel_cue_mark.init ( _("Mark"), "/options/selcue/value", Inkscape::SelCue::MARK, true, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_mark, "", + _("Each selected object has a diamond mark in the top left corner")); + _t_sel_cue_box.init ( _("Box"), "/options/selcue/value", Inkscape::SelCue::BBOX, false, &_t_sel_cue_none); + _page_selector.add_line( true, "", _t_sel_cue_box, "", + _("Each selected object displays its bounding box")); + + //Node + AddSelcueCheckbox(_page_node, "/tools/nodes", true); + AddGradientCheckbox(_page_node, "/tools/nodes", true); + _page_node.add_group_header( _("Path outline")); + _t_node_pathoutline_color.init(_("Path outline color"), "/tools/nodes/highlight_color", 0xff0000ff); + _page_node.add_line( false, "", _t_node_pathoutline_color, "", _("Selects the color used for showing the path outline"), false); + _t_node_show_outline.init(_("Always show outline"), "/tools/nodes/show_outline", false); + _page_node.add_line( true, "", _t_node_show_outline, "", _("Show outlines for all paths, not only invisible paths")); + _t_node_live_outline.init(_("Update outline when dragging nodes"), "/tools/nodes/live_outline", false); + _page_node.add_line( true, "", _t_node_live_outline, "", _("Update the outline when dragging or transforming nodes; if this is off, the outline will only update when completing a drag")); + _t_node_live_objects.init(_("Update paths when dragging nodes"), "/tools/nodes/live_objects", false); + _page_node.add_line( true, "", _t_node_live_objects, "", _("Update paths when dragging or transforming nodes; if this is off, paths will only be updated when completing a drag")); + _t_node_show_path_direction.init(_("Show path direction on outlines"), "/tools/nodes/show_path_direction", false); + _page_node.add_line( true, "", _t_node_show_path_direction, "", _("Visualize the direction of selected paths by drawing small arrows in the middle of each outline segment")); + _t_node_pathflash_enabled.init ( _("Show temporary path outline"), "/tools/nodes/pathflash_enabled", false); + _page_node.add_line( true, "", _t_node_pathflash_enabled, "", _("When hovering over a path, briefly flash its outline")); + _t_node_pathflash_selected.init ( _("Show temporary outline for selected paths"), "/tools/nodes/pathflash_selected", false); + _page_node.add_line( true, "", _t_node_pathflash_selected, "", _("Show temporary outline even when a path is selected for editing")); + _t_node_pathflash_timeout.init("/tools/nodes/pathflash_timeout", 0, 10000.0, 100.0, 100.0, 1000.0, true, false); + _page_node.add_line( false, _("_Flash time:"), _t_node_pathflash_timeout, "ms", _("Specifies how long the path outline will be visible after a mouse-over (in milliseconds); specify 0 to have the outline shown until mouse leaves the path"), false); + _page_node.add_group_header(_("Editing preferences")); + _t_node_single_node_transform_handles.init(_("Show transform handles for single nodes"), "/tools/nodes/single_node_transform_handles", false); + _page_node.add_line( true, "", _t_node_single_node_transform_handles, "", _("Show transform handles even when only a single node is selected")); + _t_node_delete_preserves_shape.init(_("Deleting nodes preserves shape"), "/tools/nodes/delete_preserves_shape", true); + _page_node.add_line( true, "", _t_node_delete_preserves_shape, "", _("Move handles next to deleted nodes to resemble original shape; hold Ctrl to get the other behavior")); + + //Tweak + this->AddNewObjectsStyle(_page_tweak, "/tools/tweak", _("Object paint style")); + AddSelcueCheckbox(_page_tweak, "/tools/tweak", true); + AddGradientCheckbox(_page_tweak, "/tools/tweak", false); + + //Zoom + AddSelcueCheckbox(_page_zoom, "/tools/zoom", true); + AddGradientCheckbox(_page_zoom, "/tools/zoom", false); + + //Measure + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Ignore first and last points"), "/tools/measure/ignore_1st_and_last", true); + _page_measure.add_line( false, "", *cb, "", _("The start and end of the measurement tool's control line will not be considered for calculating lengths. Only lengths between actual curve intersections will be displayed.")); + + //Shapes + _path_shapes = _page_list.get_model()->get_path(iter_shapes); + this->AddSelcueCheckbox(_page_shapes, "/tools/shapes", true); + this->AddGradientCheckbox(_page_shapes, "/tools/shapes", true); + + //Rectangle + this->AddNewObjectsStyle(_page_rectangle, "/tools/shapes/rect"); + this->AddConvertGuidesCheckbox(_page_rectangle, "/tools/shapes/rect", true); + + //3D box + this->AddNewObjectsStyle(_page_3dbox, "/tools/shapes/3dbox"); + this->AddConvertGuidesCheckbox(_page_3dbox, "/tools/shapes/3dbox", true); + + //Ellipse + this->AddNewObjectsStyle(_page_ellipse, "/tools/shapes/arc"); + + //Star + this->AddNewObjectsStyle(_page_star, "/tools/shapes/star"); + + //Spiral + this->AddNewObjectsStyle(_page_spiral, "/tools/shapes/spiral"); + + //Pencil + this->AddSelcueCheckbox(_page_pencil, "/tools/freehand/pencil", true); + this->AddNewObjectsStyle(_page_pencil, "/tools/freehand/pencil"); + this->AddDotSizeSpinbutton(_page_pencil, "/tools/freehand/pencil", 3.0); + this->AddBaseSimplifySpinbutton(_page_pencil, "/tools/freehand/pencil", 25.0); + _page_pencil.add_group_header( _("Sketch mode")); + _page_pencil.add_line( true, "", _pencil_average_all_sketches, "", + _("If on, the sketch result will be the normal average of all sketches made, instead of averaging the old result with the new sketch")); + + //Pen + this->AddSelcueCheckbox(_page_pen, "/tools/freehand/pen", true); + this->AddNewObjectsStyle(_page_pen, "/tools/freehand/pen"); + this->AddDotSizeSpinbutton(_page_pen, "/tools/freehand/pen", 3.0); + + //Calligraphy + this->AddSelcueCheckbox(_page_calligraphy, "/tools/calligraphic", false); + this->AddNewObjectsStyle(_page_calligraphy, "/tools/calligraphic"); + _page_calligraphy.add_line( false, "", _calligrapy_use_abs_size, "", + _("If on, pen width is in absolute units (px) independent of zoom; otherwise pen width depends on zoom so that it looks the same at any zoom")); + _page_calligraphy.add_line( false, "", _calligrapy_keep_selected, "", + _("If on, each newly created object will be selected (deselecting previous selection)")); + + //Text + this->AddSelcueCheckbox(_page_text, "/tools/text", true); + this->AddGradientCheckbox(_page_text, "/tools/text", true); + { + PrefCheckButton* cb = Gtk::manage( new PrefCheckButton); + cb->init ( _("Show font samples in the drop-down list"), "/tools/text/show_sample_in_list", true); + _page_text.add_line( false, "", *cb, "", _("Show font samples alongside font names in the drop-down list in Text bar")); + + _font_dialog.init(_("Show font substitution warning dialog"), "/options/font/substitutedlg", false); + _page_text.add_line( false, "", _font_dialog, "", _("Show font substitution warning dialog when requested fonts are not available on the system")); + + cb = Gtk::manage(new PrefCheckButton); + cb->init ( _("Use SVG2 auto-flowed text"), "/tools/text/use_svg2", true); + _page_text.add_line( false, "", *cb, "", _("Use SVG2 auto-flowed text instead of SVG1.2 auto-flowed text. (Recommended)")); + } + + //_page_text.add_group_header( _("Text units")); + //_font_output_px.init ( _("Always output text size in pixels (px)"), "/options/font/textOutputPx", true); + //_page_text.add_line( true, "", _font_output_px, "", _("Always convert the text size units above into pixels (px) before saving to file")); + + _page_text.add_group_header( _("Font directories")); + _font_fontsdir_system.init( _("Use Inkscape's fonts directory"), "/options/font/use_fontsdir_system", true); + _page_text.add_line( true, "", _font_fontsdir_system, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's global \"share\" directory")); + _font_fontsdir_user.init( _("Use user's fonts directory"), "/options/font/use_fontsdir_user", true); + _page_text.add_line( true, "", _font_fontsdir_user, "", _("Load additional fonts from \"fonts\" directory located in Inkscape's user configuration directory")); + _font_fontdirs_custom.init("/options/font/custom_fontdirs", 50); + _page_text.add_line(true, _("Additional font directories"), _font_fontdirs_custom, "", _("Load additional fonts from custom locations (one path per line)"), true); + + + this->AddNewObjectsStyle(_page_text, "/tools/text"); + + //Spray + AddSelcueCheckbox(_page_spray, "/tools/spray", true); + AddGradientCheckbox(_page_spray, "/tools/spray", false); + + //Eraser + this->AddNewObjectsStyle(_page_eraser, "/tools/eraser"); + + //Paint Bucket + this->AddSelcueCheckbox(_page_paintbucket, "/tools/paintbucket", false); + this->AddNewObjectsStyle(_page_paintbucket, "/tools/paintbucket"); + + //Gradient + this->AddSelcueCheckbox(_page_gradient, "/tools/gradient", true); + _misc_forkvectors.init( _("Prevent sharing of gradient definitions"), "/options/forkgradientvectors/value", true); + _page_gradient.add_line( false, "", _misc_forkvectors, "", + _("When on, shared gradient definitions are automatically forked on change; uncheck to allow sharing of gradient definitions so that editing one object may affect other objects using the same gradient"), true); + _misc_gradienteditor.init( _("Use legacy Gradient Editor"), "/dialogs/gradienteditor/showlegacy", false); + _page_gradient.add_line( false, "", _misc_gradienteditor, "", + _("When on, the Gradient Edit button in the Fill & Stroke dialog will show the legacy Gradient Editor dialog, when off the Gradient Tool will be used"), true); + + _misc_gradientangle.init("/dialogs/gradienteditor/angle", -359, 359, 1, 90, 0, false, false); + _page_gradient.add_line( false, _("Linear gradient _angle:"), _misc_gradientangle, "", + _("Default angle of new linear gradients in degrees (clockwise from horizontal)"), false); + + + //Dropper + this->AddSelcueCheckbox(_page_dropper, "/tools/dropper", true); + this->AddGradientCheckbox(_page_dropper, "/tools/dropper", true); + + //Connector + this->AddSelcueCheckbox(_page_connector, "/tools/connector", true); + _page_connector.add_line(false, "", _connector_ignore_text, "", + _("If on, connector attachment points will not be shown for text objects")); + +#ifdef WITH_LPETOOL + //LPETool + //disabled, because the LPETool is not finished yet. + this->AddNewObjectsStyle(_page_lpetool, "/tools/lpetool"); +#endif // WITH_LPETOOL +} + +static void _inkscape_fill_gtk(const gchar *path, GHashTable *t) +{ + const gchar *dir_entry; + GDir *dir = g_dir_open(path, 0, NULL); + + if (!dir) + return; + + while ((dir_entry = g_dir_read_name(dir))) { + gchar *filename = g_build_filename(path, dir_entry, "gtk-3.0", "gtk.css", NULL); + + if (g_file_test(filename, G_FILE_TEST_IS_REGULAR) && !g_hash_table_contains(t, dir_entry)) + g_hash_table_add(t, g_strdup(dir_entry)); + + g_free(filename); + } + + g_dir_close(dir); +} + +void InkscapePreferences::get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, + guint32 &colorsetwarning, guint32 &colorseterror) +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (themeiconname == prefs->getString("/theme/defaultIconTheme")) { + themeiconname = "hicolor"; + } + Glib::ustring prefix = ""; + if (prefs->getBool("/theme/darkTheme", false)) { + prefix = ".dark "; + } + Glib::ustring higlight = get_filename(ICONS, Glib::ustring(themeiconname + "/highlights.css").c_str(), false, true); + if (!higlight.empty()) { + std::ifstream ifs(higlight); + std::string content((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>())); + Glib::ustring result; + size_t startpos = content.find(prefix + ".base"); + size_t endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA base_color = Gdk::RGBA(result); + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".success"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA success_color = Gdk::RGBA(result); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".warning"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA warning_color = Gdk::RGBA(result); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + } + content.erase(0, endpos + 1); + startpos = content.find(prefix + ".error"); + endpos = content.find("}"); + if (startpos != std::string::npos) { + result = content.substr(startpos, endpos - startpos); + size_t startposin = result.find("fill:"); + size_t endposin = result.find(";"); + result = result.substr(startposin + 5, endposin - (startposin + 5)); + REMOVE_SPACES(result); + Gdk::RGBA error_color = Gdk::RGBA(result); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + } + } +} + +void InkscapePreferences::resetIconsColors(bool themechange) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (!prefs->getBool("/theme/symbolicIcons", false)) { + _symbolic_base_colors.set_sensitive(false); + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + return; + } + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + // This colors are setted on style.css of inkscape + Gdk::RGBA base_color = _symbolic_base_color.get_style_context()->get_color(); + // This is a hack to fix a proble style is not updated enoght fast on + // chage from dark to bright themes + if (themechange) { + base_color = _symbolic_base_color.get_style_context()->get_background_color(); + } + Gdk::RGBA success_color = _symbolic_success_color.get_style_context()->get_color(); + Gdk::RGBA warning_color = _symbolic_warning_color.get_style_context()->get_color(); + Gdk::RGBA error_color = _symbolic_error_color.get_style_context()->get_color(); + SPColor base_color_sp(base_color.get_red(), base_color.get_green(), base_color.get_blue()); + SPColor success_color_sp(success_color.get_red(), success_color.get_green(), success_color.get_blue()); + SPColor warning_color_sp(warning_color.get_red(), warning_color.get_green(), warning_color.get_blue()); + SPColor error_color_sp(error_color.get_red(), error_color.get_green(), error_color.get_blue()); + guint32 colorsetbase = base_color_sp.toRGBA32(base_color.get_alpha()); + guint32 colorsetsuccess = success_color_sp.toRGBA32(success_color.get_alpha()); + guint32 colorsetwarning = warning_color_sp.toRGBA32(warning_color.get_alpha()); + guint32 colorseterror = error_color_sp.toRGBA32(error_color.get_alpha()); + get_highlight_colors(colorsetbase, colorsetsuccess, colorsetwarning, colorseterror); + _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + prefs->setUInt("/theme/" + themeiconname + "/symbolicBaseColor", colorsetbase); + prefs->setUInt("/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + prefs->setUInt("/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + prefs->setUInt("/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + if (prefs->getBool("/theme/symbolicDefaultColors", true)) { + _symbolic_base_color.setSensitive(false); + _symbolic_success_color.setSensitive(false); + _symbolic_warning_color.setSensitive(false); + _symbolic_error_color.setSensitive(false); + /* _complementary_colors->get_style_context()->add_class("disabled"); */ + } + changeIconsColors(); + } else { + _symbolic_base_color.setSensitive(true); + _symbolic_success_color.setSensitive(true); + _symbolic_warning_color.setSensitive(true); + _symbolic_error_color.setSensitive(true); + /* _complementary_colors->get_style_context()->remove_class("disabled"); */ + } +} + +void InkscapePreferences::resetIconsColorsWrapper() { resetIconsColors(false); } + +void InkscapePreferences::changeIconsColors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.setRgba32(colorsetbase); + _symbolic_success_color.setRgba32(colorsetsuccess); + _symbolic_warning_color.setRgba32(colorsetwarning); + _symbolic_error_color.setRgba32(colorseterror); + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + Gtk::CssProvider::create(); + Glib::ustring css_str = ""; + if (prefs->getBool("/theme/symbolicIcons", false)) { + css_str = INKSCAPE.get_symbolic_colors(); + } + try { + INKSCAPE.colorizeprovider->load_from_data(css_str); + } catch (const Gtk::CssProviderError &ex) { + g_critical("CSSProviderError::load_from_data(): failed to load '%s'\n(%s)", css_str.c_str(), ex.what().c_str()); + } + Gtk::StyleContext::add_provider_for_screen(screen, INKSCAPE.colorizeprovider, + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} + +void InkscapePreferences::toggleSymbolic() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + if (window ) { + window->get_style_context()->add_class("symbolic"); + window->get_style_context()->remove_class("regular"); + } + _symbolic_base_colors.set_sensitive(true); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + } else { + if (window) { + window->get_style_context()->add_class("regular"); + window->get_style_context()->remove_class("symbolic"); + } + auto const screen = Gdk::Screen::get_default(); + if (INKSCAPE.colorizeprovider) { + Gtk::StyleContext::remove_provider_for_screen(screen, INKSCAPE.colorizeprovider); + } + _symbolic_base_colors.set_sensitive(false); + } + INKSCAPE.signal_change_theme.emit(); +} + +void InkscapePreferences::themeChange() +{ + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool darktheme = prefs->getBool("/theme/preferDarkTheme", false); + Glib::ustring themename = prefs->getString("/theme/gtkTheme"); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + GtkSettings *settings = gtk_settings_get_default(); + g_object_set(settings, "gtk-theme-name", themename.c_str(), NULL); + g_object_set(settings, "gtk-application-prefer-dark-theme", darktheme, NULL); + bool dark = themename.find(":dark") != std::string::npos; + if (!dark) { + Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context(); + Gdk::RGBA rgba; + bool background_set = stylecontext->lookup_color("theme_bg_color", rgba); + if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) { + dark = true; + } + } + Gtk::Widget *dialog_window = Glib::wrap(gobj()); + bool toggled = prefs->getBool("/theme/darkTheme", false) != dark; + if (dark) { + prefs->setBool("/theme/darkTheme", true); + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + prefs->setBool("/theme/darkTheme", false); + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + INKSCAPE.signal_change_theme.emit(); + resetIconsColors(toggled); + } +} + +void InkscapePreferences::symbolicThemeCheck() +{ + using namespace Inkscape::IO::Resource; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + bool symbolic = false; + GtkSettings *settings = gtk_settings_get_default(); + if (settings) { + if (themeiconname != "") { + g_object_set(settings, "gtk-icon-theme-name", themeiconname.c_str(), NULL); + } + } + if (prefs->getString("/theme/defaultIconTheme") != prefs->getString("/theme/iconTheme")) { + auto folders = get_foldernames(ICONS, { "application" }); + for (auto &folder : folders) { + auto path = folder; + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + if (folder == prefs->getString("/theme/iconTheme")) { +#ifdef _WIN32 + path += g_win32_locale_filename_from_utf8("/symbolic/actions"); +#else + path += "/symbolic/actions"; +#endif + std::vector<Glib::ustring> symbolic_icons = get_filenames(path, { ".svg" }, {}); + if (symbolic_icons.size() > 0) { + symbolic = true; + symbolic_icons.clear(); + } + } + } + } else { + symbolic = true; + } + if (_symbolic_icons.get_parent()) { + if (!symbolic) { + _symbolic_icons.set_active(false); + _symbolic_icons.get_parent()->hide(); + _symbolic_base_colors.get_parent()->hide(); + _symbolic_base_color.get_parent()->get_parent()->hide(); + _symbolic_success_color.get_parent()->get_parent()->hide(); + } else { + _symbolic_icons.get_parent()->show(); + _symbolic_base_colors.get_parent()->show(); + _symbolic_base_color.get_parent()->get_parent()->show(); + _symbolic_success_color.get_parent()->get_parent()->show(); + } + } + if (symbolic) { + if (prefs->getBool("/theme/symbolicDefaultColors", true) || + !prefs->getEntry("/theme/" + themeiconname + "/symbolicBaseColor").isValid()) { + resetIconsColors(); + } else { + changeIconsColors(); + } + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + colorsetbase); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", colorsetsuccess); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", colorsetwarning); + _symbolic_error_color.init(_("Color for symbolic error icons:"), + "/theme/" + themeiconname + "/symbolicErrorColor", colorseterror); + } +} +/* void sp_mix_colors(cairo_t *ct, int pos, SPColor a, SPColor b) +{ + double arcEnd=2*M_PI; + cairo_set_source_rgba(ct, 1, 1, 1, 1); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, a.v.c[0], a.v.c[1], a.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); + cairo_set_source_rgba(ct, b.v.c[0], b.v.c[1], b.v.c[2], 0.5); + cairo_arc(ct,pos,13,12,0,arcEnd); + cairo_fill(ct); +} + +Glib::RefPtr< Gdk::Pixbuf > sp_mix_colors() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + SPColor success(colorsetsuccess); + SPColor warning(colorsetwarning); + SPColor error(colorseterror); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 82, 26); + cairo_t *ct = cairo_create(s); + // success + warning + sp_mix_colors(ct, 13, success, warning); + sp_mix_colors(ct, 41, success, error); + sp_mix_colors(ct, 69, warning, error); + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, 82, 26); + cairo_surface_destroy(s); + return pixbuf; +} */ + +void InkscapePreferences::changeIconsColor(guint32 /*color*/) +{ + changeIconsColors(); + /* _complementary_colors->set(sp_mix_colors()); */ +} + +void InkscapePreferences::initPageUI() +{ + Gtk::TreeModel::iterator iter_ui = this->AddPage(_page_ui, _("Interface"), PREFS_PAGE_UI); + _path_ui = _page_list.get_model()->get_path(iter_ui); + + Glib::ustring languages[] = {_("System default"), + _("Albanian (sq)"), _("Arabic (ar)"), _("Armenian (hy)"), _("Assamese (as)"), _("Azerbaijani (az)"), + _("Basque (eu)"), _("Belarusian (be)"), _("Bulgarian (bg)"), _("Bengali (bn)"), _("Bengali/Bangladesh (bn_BD)"), _("Bodo (brx)"), _("Breton (br)"), + _("Catalan (ca)"), _("Valencian Catalan (ca@valencia)"), _("Chinese/China (zh_CN)"), _("Chinese/Taiwan (zh_TW)"), _("Croatian (hr)"), _("Czech (cs)"), + _("Danish (da)"), _("Dogri (doi)"), _("Dutch (nl)"), _("Dzongkha (dz)"), + _("German (de)"), _("Greek (el)"), + _("English (en)"), _("English/Australia (en_AU)"), _("English/Canada (en_CA)"), _("English/Great Britain (en_GB)"), _("Esperanto (eo)"), _("Estonian (et)"), + _("Farsi (fa)"), _("Finnish (fi)"), _("French (fr)"), + _("Galician (gl)"), _("Gujarati (gu)"), + _("Hebrew (he)"), _("Hindi (hi)"), _("Hungarian (hu)"), + _("Icelandic (is)"), _("Indonesian (id)"), _("Irish (ga)"), _("Italian (it)"), + _("Japanese (ja)"), + _("Kannada (kn)"), _("Kashmiri in Perso-Arabic script (ks@aran)"), _("Kashmiri in Devanagari script (ks@deva)"), _("Khmer (km)"), _("Kinyarwanda (rw)"), _("Konkani (kok)"), _("Konkani in Latin script (kok@latin)"), _("Korean (ko)"), + _("Latvian (lv)"), _("Lithuanian (lt)"), + _("Macedonian (mk)"), _("Maithili (mai)"), _("Malayalam (ml)"), _("Manipuri (mni)"), _("Manipuri in Bengali script (mni@beng)"), _("Marathi (mr)"), _("Mongolian (mn)"), + _("Nepali (ne)"), _("Norwegian BokmÃ¥l (nb)"), _("Norwegian Nynorsk (nn)"), + _("Odia (or)"), + _("Panjabi (pa)"), _("Polish (pl)"), _("Portuguese (pt)"), _("Portuguese/Brazil (pt_BR)"), + _("Romanian (ro)"), _("Russian (ru)"), + _("Sanskrit (sa)"), _("Santali (sat)"), _("Santali in Devanagari script (sat@deva)"), _("Serbian (sr)"), _("Serbian in Latin script (sr@latin)"), + _("Sindhi (sd)"), _("Sindhi in Devanagari script (sd@deva)"), _("Slovak (sk)"), _("Slovenian (sl)"), _("Spanish (es)"), _("Spanish/Mexico (es_MX)"), _("Swedish (sv)"), + _("Tamil (ta)"), _("Telugu (te)"), _("Thai (th)"), _("Turkish (tr)"), + _("Ukrainian (uk)"), _("Urdu (ur)"), + _("Vietnamese (vi)")}; + Glib::ustring langValues[] = {"", + "sq", "ar", "hy", "as", "az", + "eu", "be", "bg", "bn", "bn_BD", "brx", "br", + "ca", "ca@valencia", "zh_CN", "zh_TW", "hr", "cs", + "da", "doi", "nl", "dz", + "de", "el", + "en", "en_AU", "en_CA", "en_GB", "eo", "et", + "fa", "fi", "fr", + "gl", "gu", + "he", "hi", "hu", + "is", "id", "ga", "it", + "ja", + "kn", "ks@aran", "ks@deva", "km", "rw", "kok", "kok@latin", "ko", + "lv", "lt", + "mk", "mai", "ml", "mni", "mni@beng", "mr", "mn", + "ne", "nb", "nn", + "or", + "pa", "pl", "pt", "pt_BR", + "ro", "ru", + "sa", "sat", "sat@deva", "sr", "sr@latin", + "sd", "sd@deva", "sk", "sl", "es", "es_MX", "sv", + "ta", "te", "th", "tr", + "uk", "ur", + "vi" }; + + { + // sorting languages according to translated name + int i = 0; + int j = 0; + int n = sizeof( languages ) / sizeof( Glib::ustring ); + Glib::ustring key_language; + Glib::ustring key_langValue; + for ( j = 1 ; j < n ; j++ ) { + key_language = languages[j]; + key_langValue = langValues[j]; + i = j-1; + while ( i >= 0 + && ( ( languages[i] > key_language + && langValues[i] != "" ) + || key_langValue == "" ) ) + { + languages[i+1] = languages[i]; + langValues[i+1] = langValues[i]; + i--; + } + languages[i+1] = key_language; + langValues[i+1] = key_langValue; + } + } + + _ui_languages.init( "/ui/language", languages, langValues, G_N_ELEMENTS(languages), languages[0]); + _page_ui.add_line( false, _("Language (requires restart):"), _ui_languages, "", + _("Set the language for menus and number formats"), false); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + _ui_colorsliders_top.init( _("Work-around color sliders not drawing"), "/options/workarounds/colorsontop", false); + _page_ui.add_line( false, "", _ui_colorsliders_top, "", + _("When on, will attempt to work around bugs in certain GTK themes drawing color sliders"), true); + + + _misc_recent.init("/options/maxrecentdocuments/value", 0.0, 1000.0, 1.0, 1.0, 1.0, true, false); + + Gtk::Button* reset_recent = Gtk::manage(new Gtk::Button(_("Clear list"))); + reset_recent->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_open_recent_clicked)); + + _page_ui.add_line( false, _("Maximum documents in Open _Recent:"), _misc_recent, "", + _("Set the maximum length of the Open Recent list in the File menu, or clear the list"), false, reset_recent); + + _ui_zoom_correction.init(300, 30, 0.01, 500.0, 1.0, 10.0, 1.0); + _page_ui.add_line( false, _("_Zoom correction factor (in %):"), _ui_zoom_correction, "", + _("Adjust the slider until the length of the ruler on your screen matches its real length. This information is used when zooming to 1:1, 1:2, etc., to display objects in their true sizes"), true); + + _ui_partialdynamic.init( _("Enable dynamic relayout for incomplete sections"), "/options/workarounds/dynamicnotdone", false); + _page_ui.add_line( false, "", _ui_partialdynamic, "", + _("When on, will allow dynamic layout of components that are not completely finished being refactored"), true); + + /* show infobox */ + _show_filters_info_box.init( _("Show filter primitives infobox (requires restart)"), "/options/showfiltersinfobox/value", true); + _page_ui.add_line(false, "", _show_filters_info_box, "", + _("Show icons and descriptions for the filter primitives available at the filter effects dialog")); + + { + Glib::ustring dockbarstyleLabels[] = {_("Icons only"), _("Text only"), _("Icons and text")}; + int dockbarstyleValues[] = {0, 1, 2}; + + /* dockbar style */ + _dockbar_style.init( "/options/dock/dockbarstyle", dockbarstyleLabels, dockbarstyleValues, G_N_ELEMENTS(dockbarstyleLabels), 0); + _page_ui.add_line(false, _("Dockbar style (requires restart):"), _dockbar_style, "", + _("Selects whether the vertical bars on the dockbar will show text labels, icons, or both"), false); + + Glib::ustring switcherstyleLabels[] = {_("Text only"), _("Icons only"), _("Icons and text")}; /* see bug #1098437 */ + int switcherstyleValues[] = {0, 1, 2}; + + /* switcher style */ + _switcher_style.init( "/options/dock/switcherstyle", switcherstyleLabels, switcherstyleValues, G_N_ELEMENTS(switcherstyleLabels), 0); + _page_ui.add_line(false, _("Switcher style (requires restart):"), _switcher_style, "", + _("Selects whether the dockbar switcher will show text labels, icons, or both"), false); + } + + _ui_yaxisdown.init( _("Origin at upper left with y-axis pointing down (requires restart)"), "/options/yaxisdown", true); + _page_ui.add_line( false, "", _ui_yaxisdown, "", + _("When off, origin is at lower left corner and y-axis points up"), true); + + _ui_rotationlock.init(_("Lock canvas rotation by default"), "/options/rotationlock", false); + _page_ui.add_line(false, "", _ui_rotationlock, "", + _("When enabled, common actions which normally rotate the canvas no longer do so by default"), true); + + + _mouse_grabsize.init("/options/grabsize/value", 1, 7, 1, 2, 3, 0); + _page_ui.add_line(false, _("_Handle size:"), _mouse_grabsize, "", + _("Set the relative size of node handles"), true); + + // Theme + _page_theme.add_group_header(_("Theme changes")); + { + using namespace Inkscape::IO::Resource; + GHashTable *t; + GHashTableIter iter; + gchar *theme, *path; + gchar **builtin_themes; + GList *list, *l; + guint i; + const gchar *const *dirs; + + t = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + /* Builtin themes */ + builtin_themes = g_resources_enumerate_children("/org/gtk/libgtk/theme", G_RESOURCE_LOOKUP_FLAGS_NONE, NULL); + for (i = 0; builtin_themes[i] != NULL; i++) { + if (g_str_has_suffix(builtin_themes[i], "/")) + g_hash_table_add(t, g_strndup(builtin_themes[i], strlen(builtin_themes[i]) - 1)); + } + g_strfreev(builtin_themes); + + path = g_build_filename(g_get_user_data_dir(), "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + path = g_build_filename(g_get_home_dir(), ".themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + + dirs = g_get_system_data_dirs(); + for (i = 0; dirs[i]; i++) { + path = g_build_filename(dirs[i], "themes", NULL); + _inkscape_fill_gtk(path, t); + g_free(path); + } + + list = NULL; + g_hash_table_iter_init(&iter, t); + while (g_hash_table_iter_next(&iter, (gpointer *)&theme, NULL)) + list = g_list_insert_sorted(list, theme, (GCompareFunc)strcmp); + + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + for (l = list; l; l = l->next) { + theme = (gchar *)l->data; + labels.emplace_back(theme); + values.emplace_back(theme); + } + labels.emplace_back(_("Use system theme")); + values.push_back(prefs->getString("/theme/defaultTheme")); + g_list_free(list); + g_hash_table_destroy(t); + + _gtk_theme.init("/theme/gtkTheme", labels, values, "Adwaita"); + _page_theme.add_line(false, _("Change Gtk theme:"), _gtk_theme, "", "", false); + _gtk_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + } + _sys_user_themes_dir_copy.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("Open themes folder")); + _page_theme.add_line(true, _("User themes: "), _sys_user_themes_dir_copy, "", _("Location of the user’s themes"), true, Gtk::manage(new Gtk::Box())); + _dark_theme.init(_("Use dark theme"), "/theme/preferDarkTheme", false); + _page_theme.add_line(true, "", _dark_theme, "", _("Use dark theme"), true); + _dark_theme.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::themeChange)); + // Icons + _page_theme.add_group_header(_("Display icons")); + { + using namespace Inkscape::IO::Resource; + auto folders = get_foldernames(ICONS, { "application" }); + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> values; + for (auto &folder : folders) { + // from https://stackoverflow.com/questions/8520560/get-a-file-name-from-a-path#8520871 + // Maybe we can link boost path utilities + // Remove directory if present. + // Do this before extension removal incase directory has a period character. + const size_t last_slash_idx = folder.find_last_of("\\/"); + if (std::string::npos != last_slash_idx) { + folder.erase(0, last_slash_idx + 1); + } + + labels.push_back(folder); + values.push_back(folder); + } + std::sort(labels.begin(), labels.end()); + std::sort(values.begin(), values.end()); + labels.erase(unique(labels.begin(), labels.end()), labels.end()); + values.erase(unique(values.begin(), values.end()), values.end()); + labels.emplace_back(_("Use system icons")); + values.push_back(prefs->getString("/theme/defaultIconTheme")); + _icon_theme.init("/theme/iconTheme", labels, values, "hicolor"); + _page_theme.add_line(false, _("Change icon theme:"), _icon_theme, "", "", false); + _icon_theme.signal_changed().connect(sigc::mem_fun(*this, &InkscapePreferences::symbolicThemeCheck)); + _sys_user_icons_dir_copy.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_theme.add_line(true, _("User icons: "), _sys_user_icons_dir_copy, "", _("Location of the user’s icons"), true, Gtk::manage(new Gtk::Box())); + } + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + _symbolic_icons.init(_("Use symbolic icons"), "/theme/symbolicIcons", false); + _symbolic_icons.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::toggleSymbolic)); + _page_theme.add_line(true, "", _symbolic_icons, "", "", true); + _symbolic_base_colors.init(_("Use default colors for icons"), "/theme/symbolicDefaultColors", true); + _symbolic_base_colors.signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::resetIconsColorsWrapper)); + _page_theme.add_line(true, "", _symbolic_base_colors, "", "", true); + _symbolic_base_color.init(_("Color for symbolic icons:"), "/theme/" + themeiconname + "/symbolicBaseColor", + 0x2E3436ff); + _symbolic_success_color.init(_("Color for symbolic success icons:"), + "/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + _symbolic_warning_color.init(_("Color for symbolic warning icons:"), + "/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + _symbolic_error_color.init(_("Color for symbolic error icons:"), "/theme/" + themeiconname + "/symbolicErrorColor", + 0xCC0000ff); + _symbolic_base_color.get_style_context()->add_class("system_base_color"); + _symbolic_success_color.get_style_context()->add_class("system_success_color"); + _symbolic_warning_color.get_style_context()->add_class("system_warning_color"); + _symbolic_error_color.get_style_context()->add_class("system_error_color"); + _symbolic_base_color.get_style_context()->add_class("symboliccolors"); + _symbolic_success_color.get_style_context()->add_class("symboliccolors"); + _symbolic_warning_color.get_style_context()->add_class("symboliccolors"); + _symbolic_error_color.get_style_context()->add_class("symboliccolors"); + _symbolic_base_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_warning_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_success_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + _symbolic_error_color.connectChanged(sigc::mem_fun(this, &InkscapePreferences::changeIconsColor)); + /* _complementary_colors = Gtk::manage(new Gtk::Image()); */ + Gtk::Box *icon_buttons = Gtk::manage(new Gtk::Box()); + icon_buttons->pack_start(_symbolic_base_color, true, true, 4); + _page_theme.add_line(false, "", *icon_buttons, _("Icon color"), + _("Base color for icons. Some icons changes need reload"), false); + Gtk::Box *icon_buttons_hight = Gtk::manage(new Gtk::Box()); + icon_buttons_hight->pack_start(_symbolic_success_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_warning_color, true, true, 4); + icon_buttons_hight->pack_start(_symbolic_error_color, true, true, 4); + /* icon_buttons_hight->pack_start(*_complementary_colors, true, true, 4); */ + _page_theme.add_line(false, "", *icon_buttons_hight, _("Highlights"), + _("Highlights colors, some symbolic icon themes use it. Some icons changes need reload"), + false); + Gtk::Box *icon_buttons_def = Gtk::manage(new Gtk::Box()); + resetIconsColors(); + changeIconsColor(0xffffffff); + _page_theme.add_line(false, "", *icon_buttons_def, "", + _("Reset theme colors, some symbolic icon themes use it. Some icons changes need reload"), + false); + { + Glib::ustring sizeLabels[] = { C_("Icon size", "Larger"), C_("Icon size", "Large"), C_("Icon size", "Small"), + C_("Icon size", "Smaller") }; + int sizeValues[] = { 3, 2, 0, 1 }; + // "Larger" is 3 to not break existing preference files. Should fix in GTK3 + + _misc_small_tools.init("/toolbox/tools/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Toolbox icon size:"), _misc_small_tools, "", + _("Set the size for the tool icons (requires restart)"), false); + + _misc_small_toolbar.init("/toolbox/small", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 0); + _page_theme.add_line(false, _("Control bar icon size:"), _misc_small_toolbar, "", + _("Set the size for the icons in tools' control bars to use (requires restart)"), false); + + _misc_small_secondary.init("/toolbox/secondary", sizeLabels, sizeValues, G_N_ELEMENTS(sizeLabels), 1); + _page_theme.add_line(false, _("Secondary toolbar icon size:"), _misc_small_secondary, "", + _("Set the size for the icons in secondary toolbars to use (requires restart)"), false); + } + { + Glib::ustring menu_icons_labels[] = {_("Yes"), _("No"), _("Theme decides")}; + int menu_icons_values[] = {1, -1, 0}; + _menu_icons.init("/theme/menuIcons", menu_icons_labels, menu_icons_values, G_N_ELEMENTS(menu_icons_labels), 0); + _page_theme.add_line(false, _("Show icons in menus:"), _menu_icons, "", + _("You can either enable or disable all icons in menus. By default the theme determines which icons to display by using the 'show-icons' attribute in its 'menus.xml' file. (requires restart)"), false); + } + + this->AddPage(_page_theme, _("Theme"), iter_ui, PREFS_PAGE_UI_THEME); + symbolicThemeCheck(); + // Windows + _win_save_geom.init ( _("Save and restore window geometry for each document"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_FILE, true, nullptr); + _win_save_geom_prefs.init ( _("Remember and use last window's geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_LAST, false, &_win_save_geom); + _win_save_geom_off.init ( _("Don't save window geometry"), "/options/savewindowgeometry/value", PREFS_WINDOW_GEOMETRY_NONE, false, &_win_save_geom); + + _win_save_dialog_pos_on.init ( _("Save and restore dialogs status"), "/options/savedialogposition/value", 1, true, nullptr); + _win_save_dialog_pos_off.init ( _("Don't save dialogs status"), "/options/savedialogposition/value", 0, false, &_win_save_dialog_pos_on); + + _win_dockable.init ( _("Dockable"), "/options/dialogtype/value", 1, true, nullptr); + _win_floating.init ( _("Floating"), "/options/dialogtype/value", 0, false, &_win_dockable); + + + _win_native.init ( _("Native open/save dialogs"), "/options/desktopintegration/value", 1, true, nullptr); + _win_gtk.init ( _("GTK open/save dialogs"), "/options/desktopintegration/value", 0, false, &_win_native); + + _win_hide_task.init ( _("Dialogs are hidden in taskbar"), "/options/dialogsskiptaskbar/value", true); + _win_save_viewport.init ( _("Save and restore documents viewport"), "/options/savedocviewport/value", true); + _win_zoom_resize.init ( _("Zoom when window is resized"), "/options/stickyzoom/value", false); + _win_ontop_none.init ( C_("Dialog on top", "None"), "/options/transientpolicy/value", 0, false, nullptr); + _win_ontop_normal.init ( _("Normal"), "/options/transientpolicy/value", 1, true, &_win_ontop_none); + _win_ontop_agressive.init ( _("Aggressive"), "/options/transientpolicy/value", 2, false, &_win_ontop_none); + + { + Glib::ustring defaultSizeLabels[] = {C_("Window size", "Default"), + C_("Window size", "Small"), + C_("Window size", "Large"), + C_("Window size", "Maximized")}; + int defaultSizeValues[] = {PREFS_WINDOW_SIZE_NATURAL, + PREFS_WINDOW_SIZE_SMALL, + PREFS_WINDOW_SIZE_LARGE, + PREFS_WINDOW_SIZE_MAXIMIZED}; + + _win_default_size.init( "/options/defaultwindowsize/value", defaultSizeLabels, defaultSizeValues, G_N_ELEMENTS(defaultSizeLabels), PREFS_WINDOW_SIZE_NATURAL); + _page_windows.add_line( false, _("Default window size:"), _win_default_size, "", + _("Set the default window size"), false); + } + + _page_windows.add_group_header( _("Saving window geometry (size and position)")); + _page_windows.add_line( true, "", _win_save_geom_off, "", + _("Let the window manager determine placement of all windows")); + _page_windows.add_line( true, "", _win_save_geom_prefs, "", + _("Remember and use the last window's geometry (saves geometry to user preferences)")); + _page_windows.add_line( true, "", _win_save_geom, "", + _("Save and restore window geometry for each document (saves geometry in the document)")); + + _page_windows.add_group_header( _("Saving dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_off, "", + _("Don't save dialogs status")); + _page_windows.add_line( true, "", _win_save_dialog_pos_on, "", + _("Save and restore dialogs status (the last open windows dialogs are saved when it closes)")); + + + + _page_windows.add_group_header( _("Dialog behavior (requires restart)")); + _page_windows.add_line( true, "", _win_dockable, "", + _("Dockable")); + _page_windows.add_line( true, "", _win_floating, "", + _("Floating")); +#ifdef _WIN32 + _page_windows.add_group_header( _("Desktop integration")); + _page_windows.add_line( true, "", _win_native, "", + _("Use Windows like open and save dialogs")); + _page_windows.add_line( true, "", _win_gtk, "", + _("Use GTK open and save dialogs ")); +#endif + +#ifndef _WIN32 // non-Win32 special code to enable transient dialogs + _page_windows.add_group_header( _("Dialogs on top:")); + + _page_windows.add_line( true, "", _win_ontop_none, "", + _("Dialogs are treated as regular windows")); + _page_windows.add_line( true, "", _win_ontop_normal, "", + _("Dialogs stay on top of document windows")); + _page_windows.add_line( true, "", _win_ontop_agressive, "", + _("Same as Normal but may work better with some window managers")); +#endif + + _page_windows.add_group_header( _("Miscellaneous")); +#ifndef _WIN32 // FIXME: Temporary Win32 special code to enable transient dialogs + _page_windows.add_line( true, "", _win_hide_task, "", + _("Whether dialog windows are to be hidden in the window manager taskbar")); +#endif + _page_windows.add_line( true, "", _win_zoom_resize, "", + _("Zoom drawing when document window is resized, to keep the same area visible (this is the default which can be changed in any window using the button above the right scrollbar)")); + _page_windows.add_line( true, "", _win_save_viewport, "", + _("Save documents viewport (zoom and panning position). Useful to turn off when sharing version controlled files.")); + this->AddPage(_page_windows, _("Windows"), iter_ui, PREFS_PAGE_UI_WINDOWS); + + // Grids + _page_grids.add_group_header( _("Line color when zooming out")); + + _grids_no_emphasize_on_zoom.init( _("Minor grid line color"), "/options/grids/no_emphasize_when_zoomedout", 1, true, nullptr); + _page_grids.add_line( true, "", _grids_no_emphasize_on_zoom, "", _("The gridlines will be shown in minor grid line color"), false); + _grids_emphasize_on_zoom.init( _("Major grid line color"), "/options/grids/no_emphasize_when_zoomedout", 0, false, &_grids_no_emphasize_on_zoom); + _page_grids.add_line( true, "", _grids_emphasize_on_zoom, "", _("The gridlines will be shown in major grid line color"), false); + + _page_grids.add_group_header( _("Default grid settings")); + + _page_grids.add_line( true, "", _grids_notebook, "", "", false); + _grids_notebook.append_page(_grids_xy, CanvasGrid::getName( GRID_RECTANGULAR )); + _grids_notebook.append_page(_grids_axonom, CanvasGrid::getName( GRID_AXONOMETRIC )); + _grids_xy_units.init("/options/grids/xy/units"); + _grids_xy.add_line( false, _("Grid units:"), _grids_xy_units, "", "", false); + _grids_xy_origin_x.init("/options/grids/xy/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_y.init("/options/grids/xy/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_xy_origin_x.set_digits(5); + _grids_xy_origin_y.set_digits(5); + _grids_xy.add_line( false, _("Origin X:"), _grids_xy_origin_x, "", _("X coordinate of grid origin"), false); + _grids_xy.add_line( false, _("Origin Y:"), _grids_xy_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_xy_spacing_x.init("/options/grids/xy/spacing_x", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_y.init("/options/grids/xy/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_xy_spacing_x.set_digits(5); + _grids_xy_spacing_y.set_digits(5); + _grids_xy.add_line( false, _("Spacing X:"), _grids_xy_spacing_x, "", _("Distance between vertical grid lines"), false); + _grids_xy.add_line( false, _("Spacing Y:"), _grids_xy_spacing_y, "", _("Distance between horizontal grid lines"), false); + + _grids_xy_color.init(_("Minor grid line color:"), "/options/grids/xy/color", GRID_DEFAULT_COLOR); + _grids_xy.add_line( false, _("Minor grid line color:"), _grids_xy_color, "", _("Color used for normal grid lines"), false); + _grids_xy_empcolor.init(_("Major grid line color:"), "/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_xy.add_line( false, _("Major grid line color:"), _grids_xy_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_xy_empspacing.init("/options/grids/xy/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_xy.add_line( false, _("Major grid line every:"), _grids_xy_empspacing, "", "", false); + _grids_xy_dotted.init( _("Show dots instead of lines"), "/options/grids/xy/dotted", false); + _grids_xy.add_line( false, "", _grids_xy_dotted, "", _("If set, display dots at gridpoints instead of gridlines"), false); + + // CanvasAxonomGrid properties: + _grids_axonom_units.init("/options/grids/axonom/units"); + _grids_axonom.add_line( false, _("Grid units:"), _grids_axonom_units, "", "", false); + _grids_axonom_origin_x.init("/options/grids/axonom/origin_x", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_y.init("/options/grids/axonom/origin_y", -10000.0, 10000.0, 0.1, 1.0, 0.0, false, false); + _grids_axonom_origin_x.set_digits(5); + _grids_axonom_origin_y.set_digits(5); + _grids_axonom.add_line( false, _("Origin X:"), _grids_axonom_origin_x, "", _("X coordinate of grid origin"), false); + _grids_axonom.add_line( false, _("Origin Y:"), _grids_axonom_origin_y, "", _("Y coordinate of grid origin"), false); + _grids_axonom_spacing_y.init("/options/grids/axonom/spacing_y", -10000.0, 10000.0, 0.1, 1.0, 1.0, false, false); + _grids_axonom_spacing_y.set_digits(5); + _grids_axonom.add_line( false, _("Spacing Y:"), _grids_axonom_spacing_y, "", _("Base length of z-axis"), false); + _grids_axonom_angle_x.init("/options/grids/axonom/angle_x", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom_angle_z.init("/options/grids/axonom/angle_z", -360.0, 360.0, 1.0, 10.0, 30.0, false, false); + _grids_axonom.add_line( false, _("Angle X:"), _grids_axonom_angle_x, "", _("Angle of x-axis"), false); + _grids_axonom.add_line( false, _("Angle Z:"), _grids_axonom_angle_z, "", _("Angle of z-axis"), false); + _grids_axonom_color.init(_("Minor grid line color:"), "/options/grids/axonom/color", GRID_DEFAULT_COLOR); + _grids_axonom.add_line( false, _("Minor grid line color:"), _grids_axonom_color, "", _("Color used for normal grid lines"), false); + _grids_axonom_empcolor.init(_("Major grid line color:"), "/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR); + _grids_axonom.add_line( false, _("Major grid line color:"), _grids_axonom_empcolor, "", _("Color used for major (highlighted) grid lines"), false); + _grids_axonom_empspacing.init("/options/grids/axonom/empspacing", 1.0, 1000.0, 1.0, 5.0, 5.0, true, false); + _grids_axonom.add_line( false, _("Major grid line every:"), _grids_axonom_empspacing, "", "", false); + + this->AddPage(_page_grids, _("Grids"), iter_ui, PREFS_PAGE_UI_GRIDS); + + initKeyboardShortcuts(iter_ui); +} + +#if defined(HAVE_LIBLCMS2) +static void profileComboChanged( Gtk::ComboBoxText* combo ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int rowNum = combo->get_active_row_number(); + if ( rowNum < 1 ) { + prefs->setString("/options/displayprofile/uri", ""); + } else { + Glib::ustring active = combo->get_active_text(); + + Glib::ustring path = CMSSystem::getPathForProfile(active); + if ( !path.empty() ) { + prefs->setString("/options/displayprofile/uri", path); + } + } +} + +static void proofComboChanged( Gtk::ComboBoxText* combo ) +{ + Glib::ustring active = combo->get_active_text(); + Glib::ustring path = CMSSystem::getPathForProfile(active); + + if ( !path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/uri", path); + } +} + +static void gamutColorChanged( Gtk::ColorButton* btn ) { + auto rgba = btn->get_rgba(); + auto r = rgba.get_red_u(); + auto g = rgba.get_green_u(); + auto b = rgba.get_blue_u(); + + gchar* tmp = g_strdup_printf("#%02x%02x%02x", (r >> 8), (g >> 8), (b >> 8) ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/softproof/gamutcolor", tmp); + g_free(tmp); +} +#endif // defined(HAVE_LIBLCMS2) + +void InkscapePreferences::initPageIO() +{ + Gtk::TreeModel::iterator iter_io = this->AddPage(_page_io, _("Input/Output"), PREFS_PAGE_IO); + _path_io = _page_list.get_model()->get_path(iter_io); + + _save_use_current_dir.init( _("Use current directory for \"Save As ...\""), "/dialogs/save_as/use_current_dir", true); + _page_io.add_line( false, "", _save_use_current_dir, "", + _("When this option is on, the \"Save as...\" and \"Save a Copy...\" dialogs will always open in the directory where the currently open document is; when it's off, each will open in the directory where you last saved a file using it"), true); + + _misc_comment.init( _("Add label comments to printing output"), "/printing/debug/show-label-comments", false); + _page_io.add_line( false, "", _misc_comment, "", + _("When on, a comment will be added to the raw print output, marking the rendered output for an object with its label"), true); + + _misc_default_metadata.init( _("Add default metadata to new documents"), "/metadata/addToNewFile", false); + _page_io.add_line( false, "", _misc_default_metadata, "", + _("Add default metadata to new documents. Default metadata can be set from Document Properties->Metadata."), true); + + // Input devices options + _mouse_sens.init ( "/options/cursortolerance/value", 0.0, 30.0, 1.0, 1.0, 8.0, true, false); + _page_mouse.add_line( false, _("_Grab sensitivity:"), _mouse_sens, _("pixels (requires restart)"), + _("How close on the screen you need to be to an object to be able to grab it with mouse (in screen pixels)"), false); + _mouse_thres.init ( "/options/dragtolerance/value", 0.0, 20.0, 1.0, 1.0, 4.0, true, false); + _page_mouse.add_line( false, _("_Click/drag threshold:"), _mouse_thres, _("pixels"), + _("Maximum mouse drag (in screen pixels) which is considered a click, not a drag"), false); + + _mouse_use_ext_input.init( _("Use pressure-sensitive tablet (requires restart)"), "/options/useextinput/value", true); + _page_mouse.add_line(false, "",_mouse_use_ext_input, "", + _("Use the capabilities of a tablet or other pressure-sensitive device. Disable this only if you have problems with the tablet (you can still use it as a mouse)")); + + _mouse_switch_on_ext_input.init( _("Switch tool based on tablet device (requires restart)"), "/options/switchonextinput/value", false); + _page_mouse.add_line(false, "",_mouse_switch_on_ext_input, "", + _("Change tool as different devices are used on the tablet (pen, eraser, mouse)")); + this->AddPage(_page_mouse, _("Input devices"), iter_io, PREFS_PAGE_IO_MOUSE); + + // SVG output options + _svgoutput_usenamedcolors.init( _("Use named colors"), "/options/svgoutput/usenamedcolors", false); + _page_svgoutput.add_line( false, "", _svgoutput_usenamedcolors, "", _("If set, write the CSS name of the color when available (e.g. 'red' or 'magenta') instead of the numeric value"), false); + + _page_svgoutput.add_group_header( _("XML formatting")); + + _svgoutput_inlineattrs.init( _("Inline attributes"), "/options/svgoutput/inlineattrs", false); + _page_svgoutput.add_line( true, "", _svgoutput_inlineattrs, "", _("Put attributes on the same line as the element tag"), false); + + _svgoutput_indent.init("/options/svgoutput/indent", 0.0, 1000.0, 1.0, 2.0, 2.0, true, false); + _page_svgoutput.add_line( true, _("_Indent, spaces:"), _svgoutput_indent, "", _("The number of spaces to use for indenting nested elements; set to 0 for no indentation"), false); + + _page_svgoutput.add_group_header( _("Path data")); + + int const numPathstringFormat = 3; + Glib::ustring pathstringFormatLabels[numPathstringFormat] = {_("Absolute"), _("Relative"), _("Optimized")}; + int pathstringFormatValues[numPathstringFormat] = {0, 1, 2}; + + _svgoutput_pathformat.init("/options/svgoutput/pathstring_format", pathstringFormatLabels, pathstringFormatValues, numPathstringFormat, 2); + _page_svgoutput.add_line( true, _("Path string format:"), _svgoutput_pathformat, "", _("Path data should be written: only with absolute coordinates, only with relative coordinates, or optimized for string length (mixed absolute and relative coordinates)"), false); + + _svgoutput_forcerepeatcommands.init( _("Force repeat commands"), "/options/svgoutput/forcerepeatcommands", false); + _page_svgoutput.add_line( true, "", _svgoutput_forcerepeatcommands, "", _("Force repeating of the same path command (for example, 'L 1,2 L 3,4' instead of 'L 1,2 3,4')"), false); + + _page_svgoutput.add_group_header( _("Numbers")); + + _svgoutput_numericprecision.init("/options/svgoutput/numericprecision", 1.0, 16.0, 1.0, 2.0, 8.0, true, false); + _page_svgoutput.add_line( true, _("_Numeric precision:"), _svgoutput_numericprecision, "", _("Significant figures of the values written to the SVG file"), false); + + _svgoutput_minimumexponent.init("/options/svgoutput/minimumexponent", -32.0, -1, 1.0, 2.0, -8.0, true, false); + _page_svgoutput.add_line( true, _("Minimum _exponent:"), _svgoutput_minimumexponent, "", _("The smallest number written to SVG is 10 to the power of this exponent; anything smaller is written as zero"), false); + + /* Code to add controls for attribute checking options */ + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Improper Attributes Actions")); + + _svgoutput_attrwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_attributes_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_attrwarn, "", _("Print warning if invalid or non-useful attributes found. Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_attrremove.init( _("Remove attributes"), "/options/svgoutput/incorrect_attributes_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_attrremove, "", _("Delete invalid or non-useful attributes from element tag"), false); + + /* Add incorrect style properties options */ + _page_svgoutput.add_group_header( _("Inappropriate Style Properties Actions")); + + _svgoutput_stylepropwarn.init( _("Print warnings"), "/options/svgoutput/incorrect_style_properties_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropwarn, "", _("Print warning if inappropriate style properties found (i.e. 'font-family' set on a <rect>). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_stylepropremove.init( _("Remove style properties"), "/options/svgoutput/incorrect_style_properties_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_stylepropremove, "", _("Delete inappropriate style properties"), false); + + /* Add default or inherited style properties options */ + _page_svgoutput.add_group_header( _("Non-useful Style Properties Actions")); + + _svgoutput_styledefaultswarn.init( _("Print warnings"), "/options/svgoutput/style_defaults_warn", true); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultswarn, "", _("Print warning if redundant style properties found (i.e. if a property has the default value and a different value is not inherited or if value is the same as would be inherited). Database files located in inkscape_data_dir/attributes."), false); + _svgoutput_styledefaultsremove.init( _("Remove style properties"), "/options/svgoutput/style_defaults_remove", false); + _page_svgoutput.add_line( true, "", _svgoutput_styledefaultsremove, "", _("Delete redundant style properties"), false); + + _page_svgoutput.add_group_header( _("Check Attributes and Style Properties on")); + + _svgoutput_check_reading.init( _("Reading"), "/options/svgoutput/check_on_reading", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_reading, "", _("Check attributes and style properties on reading in SVG files (including those internal to Inkscape which will slow down startup)"), false); + _svgoutput_check_editing.init( _("Editing"), "/options/svgoutput/check_on_editing", false); + _page_svgoutput.add_line( true, "", _svgoutput_check_editing, "", _("Check attributes and style properties while editing SVG files (may slow down Inkscape, mostly useful for debugging)"), false); + _svgoutput_check_writing.init( _("Writing"), "/options/svgoutput/check_on_writing", true); + _page_svgoutput.add_line( true, "", _svgoutput_check_writing, "", _("Check attributes and style properties on writing out SVG files"), false); + + this->AddPage(_page_svgoutput, _("SVG output"), iter_io, PREFS_PAGE_IO_SVGOUTPUT); + + // SVG Export Options ========================================== + + // SVG 2 Fallbacks + _page_svgexport.add_group_header( _("SVG 2")); + _svgexport_insert_text_fallback.init( _("Insert SVG 1.1 fallback in text."), "/options/svgexport/text_insertfallback", true ); + _svgexport_insert_mesh_polyfill.init( _("Insert Mesh Gradient JavaScript polyfill."), "/options/svgexport/mesh_insertpolyfill", true ); + _svgexport_insert_hatch_polyfill.init( _("Insert Hatch Paint Server JavaScript polyfill."), "/options/svgexport/hatch_insertpolyfill", true ); + + _page_svgexport.add_line( false, "", _svgexport_insert_text_fallback, "", _("Adds fallback options for non-SVG 2 renderers."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_mesh_polyfill, "", _("Adds JavaScript polyfill to render meshes."), false); + _page_svgexport.add_line( false, "", _svgexport_insert_hatch_polyfill, "", _("Adds JavaScript polyfill to render hatches (linear and absolute paths)."), false); + + // SVG Export Options (SVG 2 -> SVG 1) + _page_svgexport.add_group_header( _("SVG 2 to SVG 1.1")); + + _svgexport_remove_marker_auto_start_reverse.init( _("Replace markers with 'auto_start_reverse'."), "/options/svgexport/marker_autostartreverse", false); + _svgexport_remove_marker_context_paint.init( _("Replace markers using 'context_paint' or 'context_fill'."), "/options/svgexport/marker_contextpaint", false); + + _page_svgexport.add_line( false, "", _svgexport_remove_marker_auto_start_reverse, "", _("SVG 2 allows markers to automatically be reversed at start of path."), false); + _page_svgexport.add_line( false, "", _svgexport_remove_marker_context_paint, "", _("SVG 2 allows markers to automatically match stroke color."), false); + + this->AddPage(_page_svgexport, _("SVG export"), iter_io, PREFS_PAGE_IO_SVGEXPORT); + + + // CMS options + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const numIntents = 4; + /* TRANSLATORS: see http://www.newsandtech.com/issues/2004/03-04/pt/03-04_rendering.htm */ + Glib::ustring intentLabels[numIntents] = {_("Perceptual"), _("Relative Colorimetric"), _("Saturation"), _("Absolute Colorimetric")}; + int intentValues[numIntents] = {0, 1, 2, 3}; + +#if !defined(HAVE_LIBLCMS2) + Gtk::Label* lbl = new Gtk::Label(_("(Note: Color management has been disabled in this build)")); + _page_cms.add_line( false, "", *lbl, "", "", true); +#endif // !defined(HAVE_LIBLCMS2) + + _page_cms.add_group_header( _("Display adjustment")); + + Glib::ustring tmpStr; + for (auto &profile: ColorProfile::getBaseProfileDirs()) { + gchar* part = g_strdup_printf( "\n%s", profile.filename.c_str() ); + tmpStr += part; + g_free(part); + } + + gchar* profileTip = g_strdup_printf(_("The ICC profile to use to calibrate display output.\nSearched directories:%s"), tmpStr.c_str()); + _page_cms.add_line( true, _("Display profile:"), _cms_display_profile, "", + profileTip, false); + g_free(profileTip); + profileTip = nullptr; + + _cms_from_display.init( _("Retrieve profile from display"), "/options/displayprofile/from_display", false); + _page_cms.add_line( true, "", _cms_from_display, "", +#ifdef GDK_WINDOWING_X11 + _("Retrieve profiles from those attached to displays via XICC"), false); +#else + _("Retrieve profiles from those attached to displays"), false); +#endif // GDK_WINDOWING_X11 + + + _cms_intent.init("/options/displayprofile/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Display rendering intent:"), _cms_intent, "", + _("The rendering intent to use to calibrate display output"), false); + + _page_cms.add_group_header( _("Proofing")); + + _cms_softproof.init( _("Simulate output on screen"), "/options/softproof/enable", false); + _page_cms.add_line( true, "", _cms_softproof, "", + _("Simulates output of target device"), false); + + _cms_gamutwarn.init( _("Mark out of gamut colors"), "/options/softproof/gamutwarn", false); + _page_cms.add_line( true, "", _cms_gamutwarn, "", + _("Highlights colors that are out of gamut for the target device"), false); + + Glib::ustring colorStr = prefs->getString("/options/softproof/gamutcolor"); + + Gdk::RGBA tmpColor( colorStr.empty() ? "#00ff00" : colorStr); + _cms_gamutcolor.set_rgba( tmpColor ); + + _page_cms.add_line( true, _("Out of gamut warning color:"), _cms_gamutcolor, "", + _("Selects the color used for out of gamut warning"), false); + + _page_cms.add_line( true, _("Device profile:"), _cms_proof_profile, "", + _("The ICC profile to use to simulate device output"), false); + + _cms_proof_intent.init("/options/softproof/intent", intentLabels, intentValues, numIntents, 0); + _page_cms.add_line( true, _("Device rendering intent:"), _cms_proof_intent, "", + _("The rendering intent to use to calibrate device output"), false); + + _cms_proof_blackpoint.init( _("Black point compensation"), "/options/softproof/bpc", false); + _page_cms.add_line( true, "", _cms_proof_blackpoint, "", + _("Enables black point compensation"), false); + + _cms_proof_preserveblack.init( _("Preserve black"), "/options/softproof/preserveblack", false); + +#if !defined(HAVE_LIBLCMS2) + _page_cms.add_line( true, "", _cms_proof_preserveblack, +#if defined(cmsFLAGS_PRESERVEBLACK) + "", +#else + _("(LittleCMS 1.15 or later required)"), +#endif // defined(cmsFLAGS_PRESERVEBLACK) + _("Preserve K channel in CMYK -> CMYK transforms"), false); +#endif // !defined(HAVE_LIBLCMS2) + +#if !defined(cmsFLAGS_PRESERVEBLACK) + _cms_proof_preserveblack.set_sensitive( false ); +#endif // !defined(cmsFLAGS_PRESERVEBLACK) + + +#if defined(HAVE_LIBLCMS2) + { + std::vector<Glib::ustring> names = ::Inkscape::CMSSystem::getDisplayNames(); + Glib::ustring current = prefs->getString( "/options/displayprofile/uri" ); + + gint index = 0; + _cms_display_profile.append(_("<none>")); + index++; + for (auto & name : names) { + _cms_display_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_display_profile.set_active(index); + } + index++; + } + if ( current.empty() ) { + _cms_display_profile.set_active(0); + } + + names = ::Inkscape::CMSSystem::getSoftproofNames(); + current = prefs->getString("/options/softproof/uri"); + index = 0; + for (auto & name : names) { + _cms_proof_profile.append( name ); + Glib::ustring path = CMSSystem::getPathForProfile(name); + if ( !path.empty() && path == current ) { + _cms_proof_profile.set_active(index); + } + index++; + } + } + + _cms_gamutcolor.signal_color_set().connect( sigc::bind( sigc::ptr_fun(gamutColorChanged), &_cms_gamutcolor) ); + + _cms_display_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(profileComboChanged), &_cms_display_profile) ); + _cms_proof_profile.signal_changed().connect( sigc::bind( sigc::ptr_fun(proofComboChanged), &_cms_proof_profile) ); +#else + // disable it, but leave it visible + _cms_intent.set_sensitive( false ); + _cms_display_profile.set_sensitive( false ); + _cms_from_display.set_sensitive( false ); + _cms_softproof.set_sensitive( false ); + _cms_gamutwarn.set_sensitive( false ); + _cms_gamutcolor.set_sensitive( false ); + _cms_proof_intent.set_sensitive( false ); + _cms_proof_profile.set_sensitive( false ); + _cms_proof_blackpoint.set_sensitive( false ); + _cms_proof_preserveblack.set_sensitive( false ); +#endif // defined(HAVE_LIBLCMS2) + + this->AddPage(_page_cms, _("Color management"), iter_io, PREFS_PAGE_IO_CMS); + + // Autosave options + _save_autosave_enable.init( _("Enable autosave (requires restart)"), "/options/autosave/enable", true); + _page_autosave.add_line(false, "", _save_autosave_enable, "", _("Automatically save the current document(s) at a given interval, thus minimizing loss in case of a crash"), false); + _save_autosave_path.init("/options/autosave/path", true); + if (prefs->getString("/options/autosave/path").empty()) { + // Show the default fallback "tmp dir" if autosave path is not set. + _save_autosave_path.set_text(Glib::build_filename(Glib::get_user_cache_dir(), "inkscape")); + } + _page_autosave.add_line(false, C_("Filesystem", "Autosave _directory:"), _save_autosave_path, "", _("The directory where autosaves will be written. This should be an absolute path (starts with / on UNIX or a drive letter such as C: on Windows). "), false); + _save_autosave_interval.init("/options/autosave/interval", 1.0, 10800.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Interval (in minutes):"), _save_autosave_interval, "", _("Interval (in minutes) at which document will be autosaved"), false); + _save_autosave_max.init("/options/autosave/max", 1.0, 100.0, 1.0, 10.0, 10.0, true, false); + _page_autosave.add_line(false, _("_Maximum number of autosaves:"), _save_autosave_max, "", _("Maximum number of autosaved files; use this to limit the storage space used"), false); + + /* When changing the interval or enabling/disabling the autosave function, + * update our running configuration + * + * FIXME! + * AutoSave::restart() should be called AFTER the values have been changed + * (which cannot be guaranteed from here) - use a PrefObserver somewhere. + */ + _save_autosave_enable.signal_toggled( ).connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + _save_autosave_interval.signal_changed().connect( sigc::ptr_fun(Inkscape::AutoSave::restart), true); + + this->AddPage(_page_autosave, _("Autosave"), iter_io, PREFS_PAGE_IO_AUTOSAVE); +} + +void InkscapePreferences::initPageBehavior() +{ + Gtk::TreeModel::iterator iter_behavior = this->AddPage(_page_behavior, _("Behavior"), PREFS_PAGE_BEHAVIOR); + _path_behavior = _page_list.get_model()->get_path(iter_behavior); + + _misc_simpl.init("/options/simplifythreshold/value", 0.0001, 1.0, 0.0001, 0.0010, 0.0010, false, false); + _page_behavior.add_line( false, _("_Simplification threshold:"), _misc_simpl, "", + _("How strong is the Node tool's Simplify command by default. If you invoke this command several times in quick succession, it will act more and more aggressively; invoking it again after a pause restores the default threshold."), false); + + _markers_color_stock.init ( _("Color stock markers the same color as object"), "/options/markers/colorStockMarkers", true); + _markers_color_custom.init ( _("Color custom markers the same color as object"), "/options/markers/colorCustomMarkers", false); + _markers_color_update.init ( _("Update marker color when object color changes"), "/options/markers/colorUpdateMarkers", true); + + // Selecting options + _sel_all.init ( _("Select in all layers"), "/options/kbselection/inlayer", PREFS_SELECTION_ALL, false, nullptr); + _sel_current.init ( _("Select only within current layer"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER, true, &_sel_all); + _sel_recursive.init ( _("Select in current layer and sublayers"), "/options/kbselection/inlayer", PREFS_SELECTION_LAYER_RECURSIVE, false, &_sel_all); + _sel_hidden.init ( _("Ignore hidden objects and layers"), "/options/kbselection/onlyvisible", true); + _sel_locked.init ( _("Ignore locked objects and layers"), "/options/kbselection/onlysensitive", true); + _sel_layer_deselects.init ( _("Deselect upon layer change"), "/options/selection/layerdeselect", true); + + _page_select.add_line( false, "", _sel_layer_deselects, "", + _("Uncheck this to be able to keep the current objects selected when the current layer changes")); + + _page_select.add_group_header( _("Ctrl+A, Tab, Shift+Tab")); + _page_select.add_line( true, "", _sel_all, "", + _("Make keyboard selection commands work on objects in all layers")); + _page_select.add_line( true, "", _sel_current, "", + _("Make keyboard selection commands work on objects in current layer only")); + _page_select.add_line( true, "", _sel_recursive, "", + _("Make keyboard selection commands work on objects in current layer and all its sublayers")); + _page_select.add_line( true, "", _sel_hidden, "", + _("Uncheck this to be able to select objects that are hidden (either by themselves or by being in a hidden layer)")); + _page_select.add_line( true, "", _sel_locked, "", + _("Uncheck this to be able to select objects that are locked (either by themselves or by being in a locked layer)")); + + _sel_cycle.init ( _("Wrap when cycling objects in z-order"), "/options/selection/cycleWrap", true); + + _page_select.add_group_header( _("Alt+Scroll Wheel")); + _page_select.add_line( true, "", _sel_cycle, "", + _("Wrap around at start and end when cycling objects in z-order")); + + this->AddPage(_page_select, _("Selecting"), iter_behavior, PREFS_PAGE_BEHAVIOR_SELECTING); + + // Transforms options + _trans_scale_stroke.init ( _("Scale stroke width"), "/options/transform/stroke", true); + _trans_scale_corner.init ( _("Scale rounded corners in rectangles"), "/options/transform/rectcorners", false); + _trans_gradient.init ( _("Transform gradients"), "/options/transform/gradient", true); + _trans_pattern.init ( _("Transform patterns"), "/options/transform/pattern", false); + _trans_optimized.init ( _("Optimized"), "/options/preservetransform/value", 0, true, nullptr); + _trans_preserved.init ( _("Preserved"), "/options/preservetransform/value", 1, false, &_trans_optimized); + + _page_transforms.add_line( false, "", _trans_scale_stroke, "", + _("When scaling objects, scale the stroke width by the same proportion")); + _page_transforms.add_line( false, "", _trans_scale_corner, "", + _("When scaling rectangles, scale the radii of rounded corners")); + _page_transforms.add_line( false, "", _trans_gradient, "", + _("Move gradients (in fill or stroke) along with the objects")); + _page_transforms.add_line( false, "", _trans_pattern, "", + _("Move patterns (in fill or stroke) along with the objects")); + _page_transforms.add_group_header( _("Store transformation")); + _page_transforms.add_line( true, "", _trans_optimized, "", + _("If possible, apply transformation to objects without adding a transform= attribute")); + _page_transforms.add_line( true, "", _trans_preserved, "", + _("Always store transformation as a transform= attribute on objects")); + + this->AddPage(_page_transforms, _("Transforms"), iter_behavior, PREFS_PAGE_BEHAVIOR_TRANSFORMS); + + _dash_scale.init(_("Scale dashes with stroke"), "/options/dash/scale", true); + _page_dashes.add_line(false, "", _dash_scale, "", _("When changing stroke width, scale dash array")); + + this->AddPage(_page_dashes, _("Dashes"), iter_behavior, PREFS_PAGE_BEHAVIOR_DASHES); + + // Scrolling options + _scroll_wheel.init ( "/options/wheelscroll/value", 0.0, 1000.0, 1.0, 1.0, 40.0, true, false); + _page_scrolling.add_line( false, _("Mouse _wheel scrolls by:"), _scroll_wheel, _("pixels"), + _("One mouse wheel notch scrolls by this distance in screen pixels (horizontally with Shift)"), false); + _page_scrolling.add_group_header( _("Ctrl+arrows")); + _scroll_arrow_px.init ( "/options/keyscroll/value", 0.0, 1000.0, 1.0, 1.0, 10.0, true, false); + _page_scrolling.add_line( true, _("Sc_roll by:"), _scroll_arrow_px, _("pixels"), + _("Pressing Ctrl+arrow key scrolls by this distance (in screen pixels)"), false); + _scroll_arrow_acc.init ( "/options/scrollingacceleration/value", 0.0, 5.0, 0.01, 1.0, 0.35, false, false); + _page_scrolling.add_line( true, _("_Acceleration:"), _scroll_arrow_acc, "", + _("Pressing and holding Ctrl+arrow will gradually speed up scrolling (0 for no acceleration)"), false); + _page_scrolling.add_group_header( _("Autoscrolling")); + _scroll_auto_speed.init ( "/options/autoscrollspeed/value", 0.0, 5.0, 0.01, 1.0, 0.7, false, false); + _page_scrolling.add_line( true, _("_Speed:"), _scroll_auto_speed, "", + _("How fast the canvas autoscrolls when you drag beyond canvas edge (0 to turn autoscroll off)"), false); + _scroll_auto_thres.init ( "/options/autoscrolldistance/value", -600.0, 600.0, 1.0, 1.0, -10.0, true, false); + _page_scrolling.add_line( true, _("_Threshold:"), _scroll_auto_thres, _("pixels"), + _("How far (in screen pixels) you need to be from the canvas edge to trigger autoscroll; positive is outside the canvas, negative is within the canvas"), false); + _scroll_space.init ( _("Mouse move pans when Space is pressed"), "/options/spacebarpans/value", true); + _page_scrolling.add_line( true, "", _scroll_space, "", + _("When on, pressing and holding Space and dragging pans canvas")); + _wheel_zoom.init ( _("Mouse wheel zooms by default"), "/options/wheelzooms/value", false); + _page_scrolling.add_line( false, "", _wheel_zoom, "", + _("When on, mouse wheel zooms without Ctrl and scrolls canvas with Ctrl; when off, it zooms with Ctrl and scrolls without Ctrl")); + this->AddPage(_page_scrolling, _("Scrolling"), iter_behavior, PREFS_PAGE_BEHAVIOR_SCROLLING); + + // Snapping options + _page_snapping.add_group_header( _("Snap defaults")); + + _snap_default.init( _("Enable snapping in new documents"), "/options/snapdefault/value", true); + _page_snapping.add_line( true, "", _snap_default, "", + _("Initial state of snapping in new documents and non-Inkscape SVGs. Snap status is subsequently saved per-document.")); + + _page_snapping.add_group_header( _("Snap indicator")); + + _snap_indicator.init( _("Enable snap indicator"), "/options/snapindicator/value", true); + _page_snapping.add_line( true, "", _snap_indicator, "", + _("After snapping, a symbol is drawn at the point that has snapped")); + + _snap_indicator.changed_signal.connect( sigc::mem_fun(_snap_persistence, &Gtk::Widget::set_sensitive) ); + + _snap_persistence.init("/options/snapindicatorpersistence/value", 0.1, 10, 0.1, 1, 2, 1); + _page_snapping.add_line( true, _("Snap indicator persistence (in seconds):"), _snap_persistence, "", + _("Controls how long the snap indicator message will be shown, before it disappears"), true); + + _page_snapping.add_group_header( _("What should snap")); + + _snap_closest_only.init( _("Only snap the node closest to the pointer"), "/options/snapclosestonly/value", false); + _page_snapping.add_line( true, "", _snap_closest_only, "", + _("Only try to snap the node that is initially closest to the mouse pointer")); + + _snap_weight.init("/options/snapweight/value", 0, 1, 0.1, 0.2, 0.5, 1); + _page_snapping.add_line( true, _("_Weight factor:"), _snap_weight, "", + _("When multiple snap solutions are found, then Inkscape can either prefer the closest transformation (when set to 0), or prefer the node that was initially the closest to the pointer (when set to 1)"), true); + + _snap_mouse_pointer.init( _("Snap the mouse pointer when dragging a constrained knot"), "/options/snapmousepointer/value", false); + _page_snapping.add_line( true, "", _snap_mouse_pointer, "", + _("When dragging a knot along a constraint line, then snap the position of the mouse pointer instead of snapping the projection of the knot onto the constraint line")); + + _page_snapping.add_group_header( _("Delayed snap")); + + _snap_delay.init("/options/snapdelay/value", 0, 1, 0.1, 0.2, 0, 1); + _page_snapping.add_line( true, _("Delay (in seconds):"), _snap_delay, "", + _("Postpone snapping as long as the mouse is moving, and then wait an additional fraction of a second. This additional delay is specified here. When set to zero or to a very small number, snapping will be immediate."), true); + + this->AddPage(_page_snapping, _("Snapping"), iter_behavior, PREFS_PAGE_BEHAVIOR_SNAPPING); + + // Steps options + _steps_arrow.init ( "/options/nudgedistance/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //nudgedistance is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("_Arrow keys move by:"), _steps_arrow, "", + _("Pressing an arrow key moves selected object(s) or node(s) by this distance"), false); + _steps_scale.init ( "/options/defaultscale/value", 0.0, 1000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + //defaultscale is limited to 1000 in select-context.cpp: use the same limit here + _page_steps.add_line( false, _("> and < _scale by:"), _steps_scale, "", + _("Pressing > or < scales selection up or down by this increment"), false); + _steps_inset.init ( "/options/defaultoffsetwidth/value", 0.0, 3000.0, 0.01, 2.0, UNIT_TYPE_LINEAR, "px"); + _page_steps.add_line( false, _("_Inset/Outset by:"), _steps_inset, "", + _("Inset and Outset commands displace the path by this distance"), false); + _steps_compass.init ( _("Compass-like display of angles"), "/options/compassangledisplay/value", true); + _page_steps.add_line( false, "", _steps_compass, "", + _("When on, angles are displayed with 0 at north, 0 to 360 range, positive clockwise; otherwise with 0 at east, -180 to 180 range, positive counterclockwise")); + int const num_items = 18; + Glib::ustring labels[num_items] = {"90", "60", "45", "36", "30", "22.5", "18", "15", "12", "10", "7.5", "6", "5", "3", "2", "1", "0.5", C_("Rotation angle", "None")}; + int values[num_items] = {2, 3, 4, 5, 6, 8, 10, 12, 15, 18, 24, 30, 36, 60, 90, 180, 360, 0}; + _steps_rot_snap.set_size_request(_sb_width); + _steps_rot_snap.init("/options/rotationsnapsperpi/value", labels, values, num_items, 12); + _page_steps.add_line( false, _("_Rotation snaps every:"), _steps_rot_snap, _("degrees"), + _("Rotating with Ctrl pressed snaps every that much degrees; also, pressing [ or ] rotates by this amount"), false); + _steps_rot_relative.init ( _("Relative snapping of guideline angles"), "/options/relativeguiderotationsnap/value", false); + _page_steps.add_line( false, "", _steps_rot_relative, "", + _("When on, the snap angles when rotating a guideline will be relative to the original angle")); + _steps_zoom.init ( "/options/zoomincrement/value", 101.0, 500.0, 1.0, 1.0, M_SQRT2, true, true); + _page_steps.add_line( false, _("_Zoom in/out by:"), _steps_zoom, _("%"), + _("Zoom tool click, +/- keys, and middle click zoom in and out by this multiplier"), false); + _middle_mouse_zoom.init ( _("Zoom with middle mouse click"), "/options/middlemousezoom/value", true); + _page_steps.add_line( true, "", _middle_mouse_zoom, "", + _("When on, clicking the middle mouse button (usually the mouse wheel) makes zoom.")); + _steps_rotate.init ( "/options/rotateincrement/value", 1, 90, 1.0, 5.0, 15, false, false); + _page_steps.add_line( false, _("_Rotate canvas by:"), _steps_rotate, _("degrees"), + _("Rotate canvas clockwise and counter-clockwise by this amount."), false); + this->AddPage(_page_steps, _("Steps"), iter_behavior, PREFS_PAGE_BEHAVIOR_STEPS); + + // Clones options + _clone_option_parallel.init ( _("Move in parallel"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_PARALLEL, true, nullptr); + _clone_option_stay.init ( _("Stay unmoved"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_UNMOVED, false, &_clone_option_parallel); + _clone_option_transform.init ( _("Move according to transform"), "/options/clonecompensation/value", + SP_CLONE_COMPENSATION_NONE, false, &_clone_option_parallel); + _clone_option_unlink.init ( _("Are unlinked"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_UNLINK, true, nullptr); + _clone_option_delete.init ( _("Are deleted"), "/options/cloneorphans/value", + SP_CLONE_ORPHANS_DELETE, false, &_clone_option_unlink); + + _page_clones.add_group_header( _("Moving original: clones and linked offsets")); + _page_clones.add_line(true, "", _clone_option_parallel, "", + _("Clones are translated by the same vector as their original")); + _page_clones.add_line(true, "", _clone_option_stay, "", + _("Clones preserve their positions when their original is moved")); + _page_clones.add_line(true, "", _clone_option_transform, "", + _("Each clone moves according to the value of its transform= attribute; for example, a rotated clone will move in a different direction than its original")); + _page_clones.add_group_header( _("Deleting original: clones")); + _page_clones.add_line(true, "", _clone_option_unlink, "", + _("Orphaned clones are converted to regular objects")); + _page_clones.add_line(true, "", _clone_option_delete, "", + _("Orphaned clones are deleted along with their original")); + + _page_clones.add_group_header( _("Duplicating original+clones/linked offset")); + + _clone_relink_on_duplicate.init ( _("Relink duplicated clones"), "/options/relinkclonesonduplicate/value", false); + _page_clones.add_line(true, "", _clone_relink_on_duplicate, "", + _("When duplicating a selection containing both a clone and its original (possibly in groups), relink the duplicated clone to the duplicated original instead of the old original")); + + _page_clones.add_group_header( _("Unlinking clones")); + _clone_to_curves.init ( _("Path operations unlink clones"), "/options/pathoperationsunlink/value", true); + _page_clones.add_line(true, "", _clone_to_curves, "", + _("The following path operations will unlink clones: Stroke to path, Object to path, Boolean operations, Combine, Break apart")); + + //TRANSLATORS: Heading for the Inkscape Preferences "Clones" Page + this->AddPage(_page_clones, _("Clones"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLONES); + + // Clip paths and masks options + _mask_mask_on_top.init ( _("When applying, use the topmost selected object as clippath/mask"), "/options/maskobject/topmost", true); + _page_mask.add_line(false, "", _mask_mask_on_top, "", + _("Uncheck this to use the bottom selected object as the clipping path or mask")); + _mask_mask_remove.init ( _("Remove clippath/mask object after applying"), "/options/maskobject/remove", true); + _page_mask.add_line(false, "", _mask_mask_remove, "", + _("After applying, remove the object used as the clipping path or mask from the drawing")); + + _page_mask.add_group_header( _("Before applying")); + + _mask_grouping_none.init( _("Do not group clipped/masked objects"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_NONE, true, nullptr); + _mask_grouping_separate.init( _("Put every clipped/masked object in its own group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_SEPARATE, false, &_mask_grouping_none); + _mask_grouping_all.init( _("Put all clipped/masked objects into one group"), "/options/maskobject/grouping", PREFS_MASKOBJECT_GROUPING_ALL, false, &_mask_grouping_none); + + _page_mask.add_line(true, "", _mask_grouping_none, "", + _("Apply clippath/mask to every object")); + + _page_mask.add_line(true, "", _mask_grouping_separate, "", + _("Apply clippath/mask to groups containing single object")); + + _page_mask.add_line(true, "", _mask_grouping_all, "", + _("Apply clippath/mask to group containing all objects")); + + _page_mask.add_group_header( _("After releasing")); + + _mask_ungrouping.init ( _("Ungroup automatically created groups"), "/options/maskobject/ungrouping", true); + _page_mask.add_line(true, "", _mask_ungrouping, "", + _("Ungroup groups created when setting clip/mask")); + + this->AddPage(_page_mask, _("Clippaths and masks"), iter_behavior, PREFS_PAGE_BEHAVIOR_MASKS); + + + _page_markers.add_group_header( _("Stroke Style Markers")); + _page_markers.add_line( true, "", _markers_color_stock, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_custom, "", + _("Stroke color same as object, fill color either object fill color or marker fill color")); + _page_markers.add_line( true, "", _markers_color_update, "", + _("Update marker color when object color changes")); + + this->AddPage(_page_markers, _("Markers"), iter_behavior, PREFS_PAGE_BEHAVIOR_MARKERS); + + + _page_cleanup.add_group_header( _("Document cleanup")); + _cleanup_swatches.init ( _("Remove unused swatches when doing a document cleanup"), "/options/cleanupswatches/value", false); // text label + _page_cleanup.add_line( true, "", _cleanup_swatches, "", + _("Remove unused swatches when doing a document cleanup")); // tooltip + this->AddPage(_page_cleanup, _("Cleanup"), iter_behavior, PREFS_PAGE_BEHAVIOR_CLEANUP); +} + +void InkscapePreferences::initPageRendering() +{ + + /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded + _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 8.0, 1.0, 2.0, 4.0, true, false); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), + _("Configure number of processors/threads to use when rendering filters"), false); + + // rendering cache + _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false); + _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false); + + // rendering tile multiplier + _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false); + _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", + _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); + + // rendering xray radius + _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false); + _page_rendering.add_line(false, _("Rendering XRay radius:"), _rendering_xray_radius, "", + _("XRay mode radius preview"), false); + + { + // if these GTK constants ever change, consider adding a compatibility shim to SPCanvas::addIdle() + static_assert(G_PRIORITY_HIGH_IDLE == 100, "G_PRIORITY_HIGH_IDLE must be 100 to match preferences.xml"); + static_assert(G_PRIORITY_DEFAULT_IDLE == 200, "G_PRIORITY_DEFAULT_IDLE must be 200 to match preferences.xml"); + + Glib::ustring redrawPriorityLabels[] = {_("Responsive"), _("Conservative")}; + int redrawPriorityValues[] = {G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE}; + + // redraw priority + _rendering_redraw_priority.init("/options/redrawpriority/value", redrawPriorityLabels, redrawPriorityValues, G_N_ELEMENTS(redrawPriorityLabels), 0); + _page_rendering.add_line(false, _("Redraw while editing:"), _rendering_redraw_priority, "", + _("Set how quickly the canvas display is updated while editing objects"), false); + } + + /* blur quality */ + _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", + BLUR_QUALITY_BEST, false, nullptr); + _blur_quality_better.init ( _("Better quality (slower)"), "/options/blurquality/value", + BLUR_QUALITY_BETTER, false, &_blur_quality_best); + _blur_quality_normal.init ( _("Average quality"), "/options/blurquality/value", + BLUR_QUALITY_NORMAL, true, &_blur_quality_best); + _blur_quality_worse.init ( _("Lower quality (faster)"), "/options/blurquality/value", + BLUR_QUALITY_WORSE, false, &_blur_quality_best); + _blur_quality_worst.init ( _("Lowest quality (fastest)"), "/options/blurquality/value", + BLUR_QUALITY_WORST, false, &_blur_quality_best); + + _page_rendering.add_group_header( _("Gaussian blur quality for display")); + _page_rendering.add_line( true, "", _blur_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _blur_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _blur_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _blur_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _blur_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + + /* filter quality */ + _filter_quality_best.init ( _("Best quality (slowest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BEST, false, nullptr); + _filter_quality_better.init ( _("Better quality (slower)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_BETTER, false, &_filter_quality_best); + _filter_quality_normal.init ( _("Average quality"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_NORMAL, true, &_filter_quality_best); + _filter_quality_worse.init ( _("Lower quality (faster)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORSE, false, &_filter_quality_best); + _filter_quality_worst.init ( _("Lowest quality (fastest)"), "/options/filterquality/value", + Inkscape::Filters::FILTER_QUALITY_WORST, false, &_filter_quality_best); + + _page_rendering.add_group_header( _("Filter effects quality for display")); + _page_rendering.add_line( true, "", _filter_quality_best, "", + _("Best quality, but display may be very slow at high zooms (bitmap export always uses best quality)")); + _page_rendering.add_line( true, "", _filter_quality_better, "", + _("Better quality, but slower display")); + _page_rendering.add_line( true, "", _filter_quality_normal, "", + _("Average quality, acceptable display speed")); + _page_rendering.add_line( true, "", _filter_quality_worse, "", + _("Lower quality (some artifacts), but display is faster")); + _page_rendering.add_line( true, "", _filter_quality_worst, "", + _("Lowest quality (considerable artifacts), but display is fastest")); + + this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); +} + +void InkscapePreferences::initPageBitmaps() +{ + /* Note: /options/bitmapoversample removed with Cairo renderer */ + _page_bitmaps.add_group_header( _("Edit")); + _misc_bitmap_autoreload.init(_("Automatically reload images"), "/options/bitmapautoreload/value", true); + _page_bitmaps.add_line( false, "", _misc_bitmap_autoreload, "", + _("Automatically reload linked images when file is changed on disk")); + _misc_bitmap_editor.init("/options/bitmapeditor/value", true); + _page_bitmaps.add_line( false, _("_Bitmap editor:"), _misc_bitmap_editor, "", "", true); + _misc_svg_editor.init("/options/svgeditor/value", true); + _page_bitmaps.add_line( false, _("_SVG editor:"), _misc_svg_editor, "", "", true); + + _page_bitmaps.add_group_header( _("Export")); + _importexport_export_res.init("/dialogs/export/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default export _resolution:"), _importexport_export_res, _("dpi"), + _("Default image resolution (in dots per inch) in the Export dialog"), false); + _page_bitmaps.add_group_header( _("Create")); + _bitmap_copy_res.init("/options/createbitmap/resolution", 1.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Resolution for Create Bitmap _Copy:"), _bitmap_copy_res, _("dpi"), + _("Resolution used by the Create Bitmap Copy command"), false); + + _page_bitmaps.add_group_header( _("Import")); + _bitmap_ask.init(_("Ask about linking and scaling when importing bitmap images"), "/dialogs/import/ask", true); + _page_bitmaps.add_line( true, "", _bitmap_ask, "", + _("Pop-up linking and scaling dialog when importing bitmap image.")); + _svg_ask.init(_("Ask about linking and scaling when importing SVG images"), "/dialogs/import/ask_svg", true); + _page_bitmaps.add_line( true, "", _svg_ask, "", + _("Pop-up linking and scaling dialog when importing SVG image.")); + + { + Glib::ustring labels[] = {_("Embed"), _("Link")}; + Glib::ustring values[] = {"embed", "link"}; + _bitmap_link.init("/dialogs/import/link", labels, values, G_N_ELEMENTS(values), "link"); + _page_bitmaps.add_line( false, _("Bitmap import/open mode:"), _bitmap_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("Include"), _("Embed"), _("Link")}; + Glib::ustring values[] = {"include", "embed", "link"}; + _svg_link.init("/dialogs/import/import_mode_svg", labels, values, G_N_ELEMENTS(values), "include"); + _page_bitmaps.add_line( false, _("SVG import mode:"), _svg_link, "", "", false); + } + + { + Glib::ustring labels[] = {_("None (auto)"), _("Smooth (optimizeQuality)"), _("Blocky (optimizeSpeed)") }; + Glib::ustring values[] = {"auto", "optimizeQuality", "optimizeSpeed"}; + _bitmap_scale.init("/dialogs/import/scale", labels, values, G_N_ELEMENTS(values), "scale"); + _page_bitmaps.add_line( false, _("Image scale (image-rendering):"), _bitmap_scale, "", "", false); + } + + /* Note: /dialogs/import/quality removed use of in r12542 */ + _importexport_import_res.init("/dialogs/import/defaultxdpi/value", 0.0, 6000.0, 1.0, 1.0, Inkscape::Util::Quantity::convert(1, "in", "px"), true, false); + _page_bitmaps.add_line( false, _("Default _import resolution:"), _importexport_import_res, _("dpi"), + _("Default import resolution (in dots per inch) for bitmap and SVG import"), false); + _importexport_import_res_override.init(_("Override file resolution"), "/dialogs/import/forcexdpi", false); + _page_bitmaps.add_line( false, "", _importexport_import_res_override, "", + _("Use default bitmap resolution in favor of information from file")); + + _page_bitmaps.add_group_header( _("Render")); + // rendering outlines for pixmap image tags + _rendering_image_outline.init( _("Images in Outline Mode"), "/options/rendering/imageinoutlinemode", false); + _page_bitmaps.add_line(false, "", _rendering_image_outline, "", _("When active will render images while in outline mode instead of a red box with an x. This is useful for manual tracing.")); + + this->AddPage(_page_bitmaps, _("Imported Images"), PREFS_PAGE_BITMAPS); +} + +void InkscapePreferences::initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui) +{ + std::vector<Glib::ustring> fileNames; + std::vector<Glib::ustring> fileLabels; + + sp_shortcut_get_file_names(&fileLabels, &fileNames); + + _kb_filelist.init( "/options/kbshortcuts/shortcutfile", &fileLabels[0], &fileNames[0], fileLabels.size(), fileNames[0]); + + Glib::ustring tooltip(_("Select a file of predefined shortcuts to use. Any customized shortcuts you create will be added separately to ")); + tooltip += Glib::ustring(IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, "default.xml")); + + _page_keyshortcuts.add_line( false, _("Shortcut file:"), _kb_filelist, "", tooltip.c_str(), false); + + _kb_search.init("/options/kbshortcuts/value", true); + _page_keyshortcuts.add_line( false, _("Search:"), _kb_search, "", "", true); + + _kb_store = Gtk::TreeStore::create( _kb_columns ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); // only sort in onKBListKeyboardShortcuts() + + _kb_filter = Gtk::TreeModelFilter::create(_kb_store); + _kb_filter->set_visible_func (sigc::mem_fun(*this, &InkscapePreferences::onKBSearchFilter)); + + _kb_shortcut_renderer.property_editable() = true; + + _kb_tree.set_model(_kb_filter); + _kb_tree.append_column(_("Name"), _kb_columns.name); + _kb_tree.append_column(_("Shortcut"), _kb_shortcut_renderer); + _kb_tree.append_column(_("Description"), _kb_columns.description); + _kb_tree.append_column(_("ID"), _kb_columns.id); + + _kb_tree.set_expander_column(*_kb_tree.get_column(0)); + + _kb_tree.get_column(0)->set_resizable(true); + _kb_tree.get_column(0)->set_clickable(true); + _kb_tree.get_column(0)->set_fixed_width (200); + + _kb_tree.get_column(1)->set_resizable(true); + _kb_tree.get_column(1)->set_clickable(true); + _kb_tree.get_column(1)->set_fixed_width (150); + //_kb_tree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), _kb_columns.shortcut); + _kb_tree.get_column(1)->set_cell_data_func(_kb_shortcut_renderer, sigc::ptr_fun(InkscapePreferences::onKBShortcutRenderer)); + + _kb_tree.get_column(2)->set_resizable(true); + _kb_tree.get_column(2)->set_clickable(true); + + _kb_tree.get_column(3)->set_resizable(true); + _kb_tree.get_column(3)->set_clickable(true); + + _kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeEdited) ); + _kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBTreeCleared) ); + + Gtk::ScrolledWindow* scroller = new Gtk::ScrolledWindow(); + scroller->add(_kb_tree); + + int row = 3; + + scroller->set_hexpand(); + scroller->set_vexpand(); + _page_keyshortcuts.attach(*scroller, 0, row, 2, 1); + + row++; + + auto box_buttons = Gtk::manage(new Gtk::ButtonBox); + + box_buttons->set_layout(Gtk::BUTTONBOX_END); + box_buttons->set_spacing(4); + + box_buttons->set_hexpand(); + _page_keyshortcuts.attach(*box_buttons, 0, row, 3, 1); + + auto kb_reset = Gtk::manage(new Gtk::Button(_("Reset"))); + kb_reset->set_use_underline(); + kb_reset->set_tooltip_text(_("Remove all your customized keyboard shortcuts, and revert to the shortcuts in the shortcut file listed above")); + box_buttons->pack_start(*kb_reset, true, true, 6); + box_buttons->set_child_secondary(*kb_reset); + + auto kb_import = Gtk::manage(new Gtk::Button(_("Import ..."))); + kb_import->set_use_underline(); + kb_import->set_tooltip_text(_("Import custom keyboard shortcuts from a file")); + box_buttons->pack_end(*kb_import, true, true, 6); + + auto kb_export = Gtk::manage(new Gtk::Button(_("Export ..."))); + kb_export->set_use_underline(); + kb_export->set_tooltip_text(_("Export custom keyboard shortcuts to a file")); + box_buttons->pack_end(*kb_export, true, true, 6); + + kb_reset->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBReset) ); + kb_import->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBImport) ); + kb_export->signal_clicked().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBExport) ); + _kb_search.signal_key_release_event().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBSearchKeyEvent) ); + _kb_filelist.signal_changed().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBList) ); + _page_keyshortcuts.signal_realize().connect( sigc::mem_fun(*this, &InkscapePreferences::onKBRealize) ); + + this->AddPage(_page_keyshortcuts, _("Keyboard Shortcuts"), iter_ui, PREFS_PAGE_UI_KEYBOARD_SHORTCUTS); + + _kb_shortcuts_loaded = false; + Gtk::TreeStore::iterator iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = "Loading ..."; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + +} + +void InkscapePreferences::onKBList() +{ + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBReset() +{ + sp_shortcuts_delete_all_from_file(); + sp_shortcut_init(); + onKBListKeyboardShortcuts(); +} + +void InkscapePreferences::onKBImport() +{ + if (sp_shortcut_file_import()) { + onKBListKeyboardShortcuts(); + } +} + +void InkscapePreferences::onKBExport() +{ + sp_shortcut_file_export(); +} + +bool InkscapePreferences::onKBSearchKeyEvent(GdkEventKey * /*event*/) +{ + _kb_filter->refilter(); + return FALSE; +} + +void InkscapePreferences::onKBTreeCleared(const Glib::ustring& path) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + Glib::ustring id = (*iter)[_kb_columns.id]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + // Remove current shortcut from file + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + + sp_shortcut_init(); + onKBListKeyboardShortcuts(); + +} + +void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode) +{ + Gtk::TreeModel::iterator iter = _kb_filter->get_iter(path); + + Glib::ustring id = (*iter)[_kb_columns.id]; + Glib::ustring current_shortcut = (*iter)[_kb_columns.shortcut]; + unsigned int const current_shortcut_id = (*iter)[_kb_columns.shortcutid]; + + Inkscape::Verb *const verb = Inkscape::Verb::getbyid(id.c_str()); + if (!verb) { + return; + } + + unsigned int const new_shortcut_id = sp_shortcut_get_from_gdk_event(accel_key, accel_mods, hardware_keycode); + if (new_shortcut_id && (new_shortcut_id != current_shortcut_id)) { + // check if there is currently a verb assigned to this shortcut; if yes ask if the shortcut should be reassigned + Inkscape::Verb *current_verb = sp_shortcut_get_verb(new_shortcut_id); + if (current_verb) { + Glib::ustring verb_name = _(current_verb->get_name()); + Glib::ustring::size_type pos = 0; + while ((pos = verb_name.find('_', pos)) != verb_name.npos) { // strip mnemonics + verb_name.erase(pos, 1); + } + Glib::ustring message = Glib::ustring::compose(_("Keyboard shortcut \"%1\"\nis already assigned to \"%2\""), + sp_shortcut_get_label(new_shortcut_id), verb_name); + Gtk::MessageDialog dialog(message, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO, true); + dialog.set_title(_("Reassign shortcut?")); + dialog.set_secondary_text(_("Are you sure you want to reassign this shortcut?")); + dialog.set_transient_for(*dynamic_cast<Gtk::Window *>(get_toplevel())); + int response = dialog.run(); + if (response != Gtk::RESPONSE_YES) { + return; + } + } + + // Delete current shortcut if it existed + sp_shortcut_delete_from_file(id.c_str(), current_shortcut_id); + // Delete any references to the new shortcut + sp_shortcut_delete_from_file(id.c_str(), new_shortcut_id); + // Add the new shortcut + sp_shortcut_add_to_file(id.c_str(), new_shortcut_id); + + sp_shortcut_init(); + onKBListKeyboardShortcuts(); + } +} + +bool InkscapePreferences::onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter) +{ + Glib::ustring search = _kb_search.get_text().lowercase(); + if (search.empty()) { + return TRUE; + } + + Glib::ustring name = (*iter)[_kb_columns.name]; + Glib::ustring desc = (*iter)[_kb_columns.description]; + Glib::ustring shortcut = (*iter)[_kb_columns.shortcut]; + Glib::ustring id = (*iter)[_kb_columns.id]; + + if (id.empty()) { + return TRUE; // Keep all group nodes visible + } + + return (name.lowercase().find(search) != name.npos + || shortcut.lowercase().find(search) != name.npos + || desc.lowercase().find(search) != name.npos + || id.lowercase().find(search) != name.npos); +} + +void InkscapePreferences::onKBRealize() +{ + if (!_kb_shortcuts_loaded /*&& _current_page == &_page_keyshortcuts*/) { + _kb_shortcuts_loaded = true; + onKBListKeyboardShortcuts(); + } +} + +InkscapePreferences::ModelColumns &InkscapePreferences::onKBGetCols() +{ + static InkscapePreferences::ModelColumns cols; + return cols; +} + +void InkscapePreferences::onKBShortcutRenderer(Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) { + + Glib::ustring shortcut = (*iter)[onKBGetCols().shortcut]; + unsigned int user_set = (*iter)[onKBGetCols().user_set]; + Gtk::CellRendererAccel *accel = dynamic_cast<Gtk::CellRendererAccel *>(renderer); + if (user_set) { + accel->property_markup() = Glib::ustring("<span foreground=\"blue\"> " + shortcut + " </span>").c_str(); + } else { + accel->property_markup() = Glib::ustring("<span> " + shortcut + " </span>").c_str(); + } +} + +void InkscapePreferences::onKBListKeyboardShortcuts() +{ + // Save the current selection + Gtk::TreeStore::iterator iter = _kb_tree.get_selection()->get_selected(); + Glib::ustring selected_id = ""; + if (iter) { + selected_id = (*iter)[_kb_columns.id]; + } + + _kb_store->clear(); + + std::vector<Verb *>verbs = Inkscape::Verb::getList(); + + for (auto verb : verbs) { + + if (!verb) { + continue; + } + if (!verb->get_name()){ + continue; + } + + Gtk::TreeStore::Path path; + if (_kb_store->iter_is_valid(_kb_store->get_iter("0"))) { + path = _kb_store->get_path(_kb_store->get_iter("0")); + } + + // Find this group in the tree + Glib::ustring group = verb->get_group() ? _(verb->get_group()) : _("Misc"); + Glib::ustring verb_id = verb->get_id(); + if (verb_id .compare(0,26,"org.inkscape.effect.filter") == 0) { + group = _("Filters"); + } + Gtk::TreeStore::iterator iter_group; + bool found = false; + while (path) { + iter_group = _kb_store->get_iter(path); + if (!_kb_store->iter_is_valid(iter_group)) { + break; + } + Glib::ustring name = (*iter_group)[_kb_columns.name]; + if ((*iter_group)[_kb_columns.name] == group) { + found = true; + break; + } + path.next(); + } + + if (!found) { + // Add the group if not there + iter_group = _kb_store->append(); + (*iter_group)[_kb_columns.name] = group; + (*iter_group)[_kb_columns.shortcut] = ""; + (*iter_group)[_kb_columns.id] = ""; + (*iter_group)[_kb_columns.description] = ""; + (*iter_group)[_kb_columns.shortcutid] = 0; + (*iter_group)[_kb_columns.user_set] = 0; + } + + // Remove the key accelerators from the verb name + Glib::ustring name = _(verb->get_name()); + std::string::size_type k = 0; + while((k=name.find('_',k))!=name.npos) { + name.erase(k, 1); + } + + // Get the shortcut label + unsigned int shortcut_id = sp_shortcut_get_primary(verb); + Glib::ustring shortcut_label = ""; + if (shortcut_id != GDK_KEY_VoidSymbol) { + gchar* str = sp_shortcut_get_label(shortcut_id); + if (str) { + shortcut_label = Glib::Markup::escape_text(str); + g_free(str); + str = nullptr; + } + } + // Add the verb to the group + Gtk::TreeStore::iterator row = _kb_store->append(iter_group->children()); + (*row)[_kb_columns.name] = name; + (*row)[_kb_columns.shortcut] = shortcut_label; + (*row)[_kb_columns.description] = verb->get_short_tip() ? _(verb->get_short_tip()) : ""; + (*row)[_kb_columns.shortcutid] = shortcut_id; + (*row)[_kb_columns.id] = verb->get_id(); + (*row)[_kb_columns.user_set] = sp_shortcut_is_user_set(verb); + + if (selected_id == verb->get_id()) { + Gtk::TreeStore::Path sel_path = _kb_filter->convert_child_path_to_path(_kb_store->get_path(row)); + _kb_tree.expand_to_path(sel_path); + _kb_tree.get_selection()->select(sel_path); + } + } + + // re-order once after updating (then disable ordering again to increase performance) + _kb_store->set_sort_column (_kb_columns.id, Gtk::SORT_ASCENDING ); + _kb_store->set_sort_column ( GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING ); + + if (selected_id.empty()) { + _kb_tree.expand_to_path(_kb_store->get_path(_kb_store->get_iter("0:1"))); + } + +} + +void InkscapePreferences::initPageSpellcheck() +{ +#if HAVE_ASPELL + + std::vector<Glib::ustring> languages; + std::vector<Glib::ustring> langValues; + + languages.emplace_back(C_("Spellchecker language", "None")); + langValues.emplace_back(""); + + for (auto const &lang : SpellCheck::get_available_langs()) { + languages.emplace_back(lang); + langValues.emplace_back(lang); + } + + _spell_language.init( "/dialogs/spellcheck/lang", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Language:"), _spell_language, "", + _("Set the main spell check language"), false); + + _spell_language2.init( "/dialogs/spellcheck/lang2", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Second language:"), _spell_language2, "", + _("Set the second spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _spell_language3.init( "/dialogs/spellcheck/lang3", &languages[0], &langValues[0], languages.size(), languages[0]); + _page_spellcheck.add_line( false, _("Third language:"), _spell_language3, "", + _("Set the third spell check language; checking will only stop on words unknown in ALL chosen languages"), false); + + _spell_ignorenumbers.init( _("Ignore words with digits"), "/dialogs/spellcheck/ignorenumbers", true); + _page_spellcheck.add_line( false, "", _spell_ignorenumbers, "", + _("Ignore words containing digits, such as \"R2D2\""), true); + + _spell_ignoreallcaps.init( _("Ignore words in ALL CAPITALS"), "/dialogs/spellcheck/ignoreallcaps", false); + _page_spellcheck.add_line( false, "", _spell_ignoreallcaps, "", + _("Ignore words in all capitals, such as \"IUPAC\""), true); + + this->AddPage(_page_spellcheck, _("Spellcheck"), PREFS_PAGE_SPELLCHECK); +#endif +} + +static void appendList( Glib::ustring& tmp, const gchar* const*listing ) +{ + for (const gchar* const* ptr = listing; *ptr; ptr++) { + tmp += *ptr; + tmp += "\n"; + } +} + +void InkscapePreferences::initPageSystem() +{ + _misc_latency_skew.init("/debug/latency/skew", 0.5, 2.0, 0.01, 0.10, 1.0, false, false); + _page_system.add_line( false, _("Latency _skew:"), _misc_latency_skew, _("(requires restart)"), + _("Factor by which the event clock is skewed from the actual time (0.9766 on some systems)"), false); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _misc_namedicon_delay.init( _("Pre-render named icons"), "/options/iconrender/named_nodelay", false); + _page_system.add_line( false, "", _misc_namedicon_delay, "", + _("When on, named icons will be rendered before displaying the ui. This is for working around bugs in GTK+ named icon notification"), true); + + _page_system.add_group_header( _("System info")); + + _sys_user_prefs.set_text(prefs->getPrefsFilename()); + _sys_user_prefs.set_editable(false); + Gtk::Button* reset_prefs = Gtk::manage(new Gtk::Button(_("Reset Preferences"))); + reset_prefs->signal_clicked().connect(sigc::mem_fun(*this, &InkscapePreferences::on_reset_prefs_clicked)); + + _page_system.add_line(true, _("User preferences: "), _sys_user_prefs, "", + _("Location of the user’s preferences file"), true, reset_prefs); + + _sys_user_config.init((char const *)Inkscape::IO::Resource::profile_path(""), _("Open preferences folder")); + _page_system.add_line(true, _("User config: "), _sys_user_config, "", _("Location of users configuration"), true); + + _sys_user_extension_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::EXTENSIONS, ""), + _("Open extensions folder")); + _page_system.add_line(true, _("User extensions: "), _sys_user_extension_dir, "", + _("Location of the user’s extensions"), true); + + _sys_user_themes_dir.init(g_build_filename(g_get_user_data_dir(), "themes", NULL), _("Open themes folder")); + _page_system.add_line(true, _("User themes: "), _sys_user_themes_dir, "", _("Location of the user’s themes"), true); + + _sys_user_icons_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::ICONS, ""), + _("Open icons folder")); + _page_system.add_line(true, _("User icons: "), _sys_user_icons_dir, "", _("Location of the user’s icons"), true); + + _sys_user_templates_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::TEMPLATES, ""), + _("Open templates folder")); + _page_system.add_line(true, _("User templates: "), _sys_user_templates_dir, "", + _("Location of the user’s templates"), true); + + _sys_user_symbols_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::SYMBOLS, ""), + _("Open symbols folder")); + + _page_system.add_line(true, _("User symbols: "), _sys_user_symbols_dir, "", _("Location of the user’s symbols"), + true); + + _sys_user_paint_servers_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PAINT, ""), + _("Open paint servers folder")); + + _page_system.add_line(true, _("User paint servers: "), _sys_user_paint_servers_dir, "", + _("Location of the user’s paint servers"), true); + + _sys_user_palettes_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::PALETTES, ""), + _("Open palettes folder")); + _page_system.add_line(true, _("User palettes: "), _sys_user_palettes_dir, "", _("Location of the user’s palettes"), + true); + + _sys_user_keys_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::KEYS, ""), + _("Open keyboard shortcuts folder")); + _page_system.add_line(true, _("User keys: "), _sys_user_keys_dir, "", + _("Location of the user’s keyboard mapping files"), true); + + _sys_user_ui_dir.init((char const *)IO::Resource::get_path(IO::Resource::USER, IO::Resource::UIS, ""), + _("Open user interface folder")); + _page_system.add_line(true, _("User UI: "), _sys_user_ui_dir, "", + _("Location of the user’s user interface description files"), true); + + _sys_user_cache.set_text(g_get_user_cache_dir()); + _sys_user_cache.set_editable(false); + _page_system.add_line(true, _("User cache: "), _sys_user_cache, "", _("Location of user’s cache"), true); + + Glib::ustring tmp_dir = prefs->getString("/options/autosave/path"); + if (tmp_dir.empty()) { + tmp_dir = Glib::build_filename(Glib::get_user_cache_dir(), "inkscape"); + } + _sys_tmp_files.set_text(tmp_dir); + _sys_tmp_files.set_editable(false); + _page_system.add_line(true, _("Temporary files: "), _sys_tmp_files, "", _("Location of the temporary files used for autosave"), true); + + _sys_data.set_text( INKSCAPE_DATADIR_REAL ); + _sys_data.set_editable(false); + _page_system.add_line(true, _("Inkscape data: "), _sys_data, "", _("Location of Inkscape data"), true); + + _sys_extension_dir.set_text(INKSCAPE_EXTENSIONDIR); + _sys_extension_dir.set_editable(false); + _page_system.add_line(true, _("Inkscape extensions: "), _sys_extension_dir, "", _("Location of the Inkscape extensions"), true); + + Glib::ustring tmp; + appendList( tmp, g_get_system_data_dirs() ); + _sys_systemdata.get_buffer()->insert(_sys_systemdata.get_buffer()->end(), tmp); + _sys_systemdata.set_editable(false); + _sys_systemdata_scroll.add(_sys_systemdata); + _sys_systemdata_scroll.set_size_request(100, 80); + _sys_systemdata_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_systemdata_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("System data: "), _sys_systemdata_scroll, "", _("Locations of system data"), true); + + tmp = ""; + gchar** paths = nullptr; + gint count = 0; + gtk_icon_theme_get_search_path(gtk_icon_theme_get_default(), &paths, &count); + appendList( tmp, paths ); + g_strfreev(paths); + _sys_icon.get_buffer()->insert(_sys_icon.get_buffer()->end(), tmp); + _sys_icon.set_editable(false); + _sys_icon_scroll.add(_sys_icon); + _sys_icon_scroll.set_size_request(100, 80); + _sys_icon_scroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _sys_icon_scroll.set_shadow_type(Gtk::SHADOW_IN); + _page_system.add_line(true, _("Icon theme: "), _sys_icon_scroll, "", _("Locations of icon themes"), true); + + this->AddPage(_page_system, _("System"), PREFS_PAGE_SYSTEM); +} + +bool InkscapePreferences::GetSizeRequest(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + DialogPage* page = row[_page_list_columns._col_page]; + _page_frame.add(*page); + this->show_all_children(); + Gtk::Requisition sreq_minimum; + Gtk::Requisition sreq_natural; + _getContents()->get_preferred_size(sreq_minimum, sreq_natural); + _minimum_width = std::max(_minimum_width, sreq_minimum.width); + _minimum_height = std::max(_minimum_height, sreq_minimum.height); + _natural_width = std::max(_natural_width, sreq_natural.width); + _natural_height = std::max(_natural_height, sreq_natural.height); + _page_frame.remove(); + return false; +} + +bool InkscapePreferences::PresentPage(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int desired_page = prefs->getInt("/dialogs/preferences/page", 0); + _init = false; + if (desired_page == row[_page_list_columns._col_id]) + { + if (desired_page >= PREFS_PAGE_TOOLS && desired_page <= PREFS_PAGE_TOOLS_CONNECTOR) + _page_list.expand_row(_path_tools, false); + if (desired_page >= PREFS_PAGE_TOOLS_SHAPES && desired_page <= PREFS_PAGE_TOOLS_SHAPES_SPIRAL) + _page_list.expand_row(_path_shapes, false); + if (desired_page >= PREFS_PAGE_UI && desired_page <= PREFS_PAGE_UI_KEYBOARD_SHORTCUTS) + _page_list.expand_row(_path_ui, false); + if (desired_page >= PREFS_PAGE_BEHAVIOR && desired_page <= PREFS_PAGE_BEHAVIOR_MASKS) + _page_list.expand_row(_path_behavior, false); + if (desired_page >= PREFS_PAGE_IO && desired_page <= PREFS_PAGE_IO_OPENCLIPART) + _page_list.expand_row(_path_io, false); + _page_list.get_selection()->select(iter); + if (desired_page == PREFS_PAGE_UI_THEME) + symbolicThemeCheck(); + return true; + } + return false; +} + +void InkscapePreferences::on_reset_open_recent_clicked() +{ + Glib::RefPtr<Gtk::RecentManager> manager = Gtk::RecentManager::get_default(); + std::vector< Glib::RefPtr< Gtk::RecentInfo > > recent_list = manager->get_items(); + + // Remove only elements that were added by Inkscape + // TODO: This should likely preserve items that were also accessed by other apps. + // However there does not seem to be straightforward way to delete only an application from an item. + for (auto e : recent_list) { + if (e->has_application(g_get_prgname()) + || e->has_application("org.inkscape.Inkscape") + || e->has_application("inkscape") +#ifdef _WIN32 + || e->has_application("inkscape.exe") +#endif + ) { + manager->remove_item(e->get_uri()); + } + } +} + +void InkscapePreferences::on_reset_prefs_clicked() +{ + Inkscape::Preferences::get()->reset(); +} + +void InkscapePreferences::on_pagelist_selection_changed() +{ + // show new selection + Glib::RefPtr<Gtk::TreeSelection> selection = _page_list.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if(iter) + { + if (_current_page) + _page_frame.remove(); + Gtk::TreeModel::Row row = *iter; + _current_page = row[_page_list_columns._col_page]; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!_init) { + prefs->setInt("/dialogs/preferences/page", row[_page_list_columns._col_id]); + } + Glib::ustring col_name_escaped = Glib::Markup::escape_text( row[_page_list_columns._col_name] ); + _page_title.set_markup("<span size='large'><b>" + col_name_escaped + "</b></span>"); + _page_frame.add(*_current_page); + _current_page->show(); + while (Gtk::Main::events_pending()) + { + Gtk::Main::iteration(); + } + this->show_all_children(); + if (prefs->getInt("/dialogs/preferences/page", 0) == PREFS_PAGE_UI_THEME) { + symbolicThemeCheck(); + } + } +} + +void InkscapePreferences::_presentPages() +{ + _page_list_model->foreach_iter(sigc::mem_fun(*this, &InkscapePreferences::PresentPage)); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h new file mode 100644 index 0000000..af7aa26 --- /dev/null +++ b/src/ui/dialog/inkscape-preferences.h @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Inkscape Preferences dialog + */ +/* Authors: + * Carl Hetherington + * Marco Scholten + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004-2013 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H +#define INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H + +#include <iostream> +#include <vector> +#include "ui/widget/preferences-widget.h" +#include <cstddef> +#include <gtkmm/colorbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/frame.h> +#include <gtkmm/notebook.h> +#include <gtkmm/textview.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treemodelfilter.h> + +#include "ui/widget/panel.h" + +// UPDATE THIS IF YOU'RE ADDING PREFS PAGES. +// Otherwise the commands that open the dialog with the new page will fail. + +enum { + PREFS_PAGE_TOOLS, + PREFS_PAGE_TOOLS_SELECTOR, + PREFS_PAGE_TOOLS_NODE, + PREFS_PAGE_TOOLS_TWEAK, + PREFS_PAGE_TOOLS_ZOOM, + PREFS_PAGE_TOOLS_MEASURE, + PREFS_PAGE_TOOLS_SHAPES, + PREFS_PAGE_TOOLS_SHAPES_RECT, + PREFS_PAGE_TOOLS_SHAPES_3DBOX, + PREFS_PAGE_TOOLS_SHAPES_ELLIPSE, + PREFS_PAGE_TOOLS_SHAPES_STAR, + PREFS_PAGE_TOOLS_SHAPES_SPIRAL, + PREFS_PAGE_TOOLS_PENCIL, + PREFS_PAGE_TOOLS_PEN, + PREFS_PAGE_TOOLS_CALLIGRAPHY, + PREFS_PAGE_TOOLS_TEXT, + PREFS_PAGE_TOOLS_SPRAY, + PREFS_PAGE_TOOLS_ERASER, + PREFS_PAGE_TOOLS_PAINTBUCKET, + PREFS_PAGE_TOOLS_GRADIENT, + PREFS_PAGE_TOOLS_DROPPER, + PREFS_PAGE_TOOLS_CONNECTOR, + PREFS_PAGE_TOOLS_LPETOOL, + PREFS_PAGE_UI, + PREFS_PAGE_UI_THEME, + PREFS_PAGE_UI_WINDOWS, + PREFS_PAGE_UI_GRIDS, + PREFS_PAGE_UI_KEYBOARD_SHORTCUTS, + PREFS_PAGE_BEHAVIOR, + PREFS_PAGE_BEHAVIOR_SELECTING, + PREFS_PAGE_BEHAVIOR_TRANSFORMS, + PREFS_PAGE_BEHAVIOR_DASHES, + PREFS_PAGE_BEHAVIOR_SCROLLING, + PREFS_PAGE_BEHAVIOR_SNAPPING, + PREFS_PAGE_BEHAVIOR_STEPS, + PREFS_PAGE_BEHAVIOR_CLONES, + PREFS_PAGE_BEHAVIOR_MASKS, + PREFS_PAGE_BEHAVIOR_MARKERS, + PREFS_PAGE_BEHAVIOR_CLEANUP, + PREFS_PAGE_IO, + PREFS_PAGE_IO_MOUSE, + PREFS_PAGE_IO_SVGOUTPUT, + PREFS_PAGE_IO_SVGEXPORT, + PREFS_PAGE_IO_CMS, + PREFS_PAGE_IO_AUTOSAVE, + PREFS_PAGE_IO_OPENCLIPART, + PREFS_PAGE_SYSTEM, + PREFS_PAGE_BITMAPS, + PREFS_PAGE_RENDERING, + PREFS_PAGE_SPELLCHECK +}; + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InkscapePreferences : public UI::Widget::Panel { +public: + ~InkscapePreferences() override; + + static InkscapePreferences &getInstance() { return *new InkscapePreferences(); } + +protected: + Gtk::Frame _page_frame; + Gtk::Label _page_title; + Gtk::TreeView _page_list; + Glib::RefPtr<Gtk::TreeStore> _page_list_model; + + //Pagelist model columns: + class PageListModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PageListModelColumns() + { Gtk::TreeModelColumnRecord::add(_col_name); Gtk::TreeModelColumnRecord::add(_col_page); Gtk::TreeModelColumnRecord::add(_col_id); } + Gtk::TreeModelColumn<Glib::ustring> _col_name; + Gtk::TreeModelColumn<int> _col_id; + Gtk::TreeModelColumn<UI::Widget::DialogPage*> _col_page; + }; + PageListModelColumns _page_list_columns; + + Gtk::TreeModel::Path _path_tools; + Gtk::TreeModel::Path _path_shapes; + Gtk::TreeModel::Path _path_ui; + Gtk::TreeModel::Path _path_behavior; + Gtk::TreeModel::Path _path_io; + + UI::Widget::DialogPage _page_tools; + UI::Widget::DialogPage _page_selector; + UI::Widget::DialogPage _page_node; + UI::Widget::DialogPage _page_tweak; + UI::Widget::DialogPage _page_spray; + UI::Widget::DialogPage _page_zoom; + UI::Widget::DialogPage _page_measure; + UI::Widget::DialogPage _page_shapes; + UI::Widget::DialogPage _page_pencil; + UI::Widget::DialogPage _page_pen; + UI::Widget::DialogPage _page_calligraphy; + UI::Widget::DialogPage _page_text; + UI::Widget::DialogPage _page_gradient; + UI::Widget::DialogPage _page_connector; + UI::Widget::DialogPage _page_dropper; + UI::Widget::DialogPage _page_lpetool; + + UI::Widget::DialogPage _page_rectangle; + UI::Widget::DialogPage _page_3dbox; + UI::Widget::DialogPage _page_ellipse; + UI::Widget::DialogPage _page_star; + UI::Widget::DialogPage _page_spiral; + UI::Widget::DialogPage _page_paintbucket; + UI::Widget::DialogPage _page_eraser; + + UI::Widget::DialogPage _page_ui; + UI::Widget::DialogPage _page_theme; + UI::Widget::DialogPage _page_windows; + UI::Widget::DialogPage _page_grids; + + UI::Widget::DialogPage _page_behavior; + UI::Widget::DialogPage _page_select; + UI::Widget::DialogPage _page_transforms; + UI::Widget::DialogPage _page_dashes; + UI::Widget::DialogPage _page_scrolling; + UI::Widget::DialogPage _page_snapping; + UI::Widget::DialogPage _page_steps; + UI::Widget::DialogPage _page_clones; + UI::Widget::DialogPage _page_mask; + UI::Widget::DialogPage _page_markers; + UI::Widget::DialogPage _page_cleanup; + + UI::Widget::DialogPage _page_io; + UI::Widget::DialogPage _page_mouse; + UI::Widget::DialogPage _page_svgoutput; + UI::Widget::DialogPage _page_svgexport; + UI::Widget::DialogPage _page_cms; + UI::Widget::DialogPage _page_autosave; + + UI::Widget::DialogPage _page_rendering; + UI::Widget::DialogPage _page_system; + UI::Widget::DialogPage _page_bitmaps; + UI::Widget::DialogPage _page_spellcheck; + + UI::Widget::DialogPage _page_keyshortcuts; + + UI::Widget::PrefSpinButton _mouse_sens; + UI::Widget::PrefSpinButton _mouse_thres; + UI::Widget::PrefSlider _mouse_grabsize; + UI::Widget::PrefCheckButton _mouse_use_ext_input; + UI::Widget::PrefCheckButton _mouse_switch_on_ext_input; + + UI::Widget::PrefSpinButton _scroll_wheel; + UI::Widget::PrefSpinButton _scroll_arrow_px; + UI::Widget::PrefSpinButton _scroll_arrow_acc; + UI::Widget::PrefSpinButton _scroll_auto_speed; + UI::Widget::PrefSpinButton _scroll_auto_thres; + UI::Widget::PrefCheckButton _scroll_space; + UI::Widget::PrefCheckButton _wheel_zoom; + + Gtk::Scale *_slider_snapping_delay; + + UI::Widget::PrefCheckButton _snap_default; + UI::Widget::PrefCheckButton _snap_indicator; + UI::Widget::PrefCheckButton _snap_closest_only; + UI::Widget::PrefCheckButton _snap_mouse_pointer; + + UI::Widget::PrefCombo _steps_rot_snap; + UI::Widget::PrefCheckButton _steps_rot_relative; + UI::Widget::PrefCheckButton _steps_compass; + UI::Widget::PrefSpinUnit _steps_arrow; + UI::Widget::PrefSpinUnit _steps_scale; + UI::Widget::PrefSpinUnit _steps_inset; + UI::Widget::PrefSpinButton _steps_zoom; + UI::Widget::PrefCheckButton _middle_mouse_zoom; + UI::Widget::PrefSpinButton _steps_rotate; + + UI::Widget::PrefRadioButton _t_sel_trans_obj; + UI::Widget::PrefRadioButton _t_sel_trans_outl; + UI::Widget::PrefRadioButton _t_sel_cue_none; + UI::Widget::PrefRadioButton _t_sel_cue_mark; + UI::Widget::PrefRadioButton _t_sel_cue_box; + UI::Widget::PrefRadioButton _t_bbox_visual; + UI::Widget::PrefRadioButton _t_bbox_geometric; + + UI::Widget::PrefCheckButton _t_cvg_keep_objects; + UI::Widget::PrefCheckButton _t_cvg_convert_whole_groups; + UI::Widget::PrefCheckButton _t_node_show_outline; + UI::Widget::PrefCheckButton _t_node_live_outline; + UI::Widget::PrefCheckButton _t_node_live_objects; + UI::Widget::PrefCheckButton _t_node_pathflash_enabled; + UI::Widget::PrefCheckButton _t_node_pathflash_selected; + UI::Widget::PrefSpinButton _t_node_pathflash_timeout; + UI::Widget::PrefCheckButton _t_node_show_path_direction; + UI::Widget::PrefCheckButton _t_node_single_node_transform_handles; + UI::Widget::PrefCheckButton _t_node_delete_preserves_shape; + UI::Widget::PrefColorPicker _t_node_pathoutline_color; + + UI::Widget::PrefCombo _gtk_theme; + UI::Widget::PrefOpenFolder _sys_user_themes_dir_copy; + UI::Widget::PrefOpenFolder _sys_user_icons_dir_copy; + UI::Widget::PrefCombo _icon_theme; + UI::Widget::PrefCheckButton _dark_theme; + UI::Widget::PrefCheckButton _symbolic_icons; + UI::Widget::PrefCheckButton _symbolic_base_colors; + UI::Widget::PrefColorPicker _symbolic_base_color; + UI::Widget::PrefColorPicker _symbolic_warning_color; + UI::Widget::PrefColorPicker _symbolic_error_color; + UI::Widget::PrefColorPicker _symbolic_success_color; + /* Gtk::Image *_complementary_colors; */ + UI::Widget::PrefCombo _misc_small_toolbar; + UI::Widget::PrefCombo _misc_small_secondary; + UI::Widget::PrefCombo _misc_small_tools; + UI::Widget::PrefCombo _menu_icons; + + Gtk::Button _apply_theme; + + UI::Widget::PrefRadioButton _win_dockable; + UI::Widget::PrefRadioButton _win_floating; + UI::Widget::PrefRadioButton _win_native; + UI::Widget::PrefRadioButton _win_gtk; + UI::Widget::PrefRadioButton _win_save_dialog_pos_on; + UI::Widget::PrefRadioButton _win_save_dialog_pos_off; + UI::Widget::PrefCombo _win_default_size; + UI::Widget::PrefRadioButton _win_ontop_none; + UI::Widget::PrefRadioButton _win_ontop_normal; + UI::Widget::PrefRadioButton _win_ontop_agressive; + UI::Widget::PrefRadioButton _win_save_geom_off; + UI::Widget::PrefRadioButton _win_save_geom; + UI::Widget::PrefRadioButton _win_save_geom_prefs; + UI::Widget::PrefCheckButton _win_hide_task; + UI::Widget::PrefCheckButton _win_save_viewport; + UI::Widget::PrefCheckButton _win_zoom_resize; + + UI::Widget::PrefCheckButton _pencil_average_all_sketches; + + UI::Widget::PrefCheckButton _calligrapy_use_abs_size; + UI::Widget::PrefCheckButton _calligrapy_keep_selected; + + UI::Widget::PrefCheckButton _connector_ignore_text; + + UI::Widget::PrefRadioButton _clone_option_parallel; + UI::Widget::PrefRadioButton _clone_option_stay; + UI::Widget::PrefRadioButton _clone_option_transform; + UI::Widget::PrefRadioButton _clone_option_unlink; + UI::Widget::PrefRadioButton _clone_option_delete; + UI::Widget::PrefCheckButton _clone_relink_on_duplicate; + UI::Widget::PrefCheckButton _clone_to_curves; + + UI::Widget::PrefCheckButton _mask_mask_on_top; + UI::Widget::PrefCheckButton _mask_mask_remove; + UI::Widget::PrefRadioButton _mask_grouping_none; + UI::Widget::PrefRadioButton _mask_grouping_separate; + UI::Widget::PrefRadioButton _mask_grouping_all; + UI::Widget::PrefCheckButton _mask_ungrouping; + + UI::Widget::PrefRadioButton _blur_quality_best; + UI::Widget::PrefRadioButton _blur_quality_better; + UI::Widget::PrefRadioButton _blur_quality_normal; + UI::Widget::PrefRadioButton _blur_quality_worse; + UI::Widget::PrefRadioButton _blur_quality_worst; + UI::Widget::PrefRadioButton _filter_quality_best; + UI::Widget::PrefRadioButton _filter_quality_better; + UI::Widget::PrefRadioButton _filter_quality_normal; + UI::Widget::PrefRadioButton _filter_quality_worse; + UI::Widget::PrefRadioButton _filter_quality_worst; + UI::Widget::PrefCheckButton _show_filters_info_box; + UI::Widget::PrefCombo _dockbar_style; + UI::Widget::PrefCombo _switcher_style; + UI::Widget::PrefCheckButton _rendering_image_outline; + UI::Widget::PrefSpinButton _rendering_cache_size; + UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_xray_radius; + UI::Widget::PrefCombo _rendering_redraw_priority; + UI::Widget::PrefSpinButton _filter_multi_threaded; + + UI::Widget::PrefCheckButton _trans_scale_stroke; + UI::Widget::PrefCheckButton _trans_scale_corner; + UI::Widget::PrefCheckButton _trans_gradient; + UI::Widget::PrefCheckButton _trans_pattern; + UI::Widget::PrefRadioButton _trans_optimized; + UI::Widget::PrefRadioButton _trans_preserved; + + UI::Widget::PrefCheckButton _dash_scale; + + UI::Widget::PrefRadioButton _sel_all; + UI::Widget::PrefRadioButton _sel_current; + UI::Widget::PrefRadioButton _sel_recursive; + UI::Widget::PrefCheckButton _sel_hidden; + UI::Widget::PrefCheckButton _sel_locked; + UI::Widget::PrefCheckButton _sel_layer_deselects; + UI::Widget::PrefCheckButton _sel_cycle; + + UI::Widget::PrefCheckButton _markers_color_stock; + UI::Widget::PrefCheckButton _markers_color_custom; + UI::Widget::PrefCheckButton _markers_color_update; + + UI::Widget::PrefCheckButton _cleanup_swatches; + + UI::Widget::PrefSpinButton _importexport_export_res; + UI::Widget::PrefSpinButton _importexport_import_res; + UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefSlider _snap_delay; + UI::Widget::PrefSlider _snap_weight; + UI::Widget::PrefSlider _snap_persistence; + UI::Widget::PrefCheckButton _font_dialog; + UI::Widget::PrefCombo _font_unit_type; + UI::Widget::PrefCheckButton _font_output_px; + UI::Widget::PrefCheckButton _font_fontsdir_system; + UI::Widget::PrefCheckButton _font_fontsdir_user; + UI::Widget::PrefMultiEntry _font_fontdirs_custom; + + UI::Widget::PrefCheckButton _misc_comment; + UI::Widget::PrefCheckButton _misc_default_metadata; + UI::Widget::PrefCheckButton _misc_forkvectors; + UI::Widget::PrefCheckButton _misc_gradienteditor; + UI::Widget::PrefSpinButton _misc_gradientangle; + UI::Widget::PrefCheckButton _misc_scripts; + UI::Widget::PrefCheckButton _misc_namedicon_delay; + + // System page + // Gtk::Button *_apply_theme; + UI::Widget::PrefSpinButton _misc_latency_skew; + UI::Widget::PrefSpinButton _misc_simpl; + Gtk::Entry _sys_user_prefs; + Gtk::Entry _sys_tmp_files; + Gtk::Entry _sys_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_config; + UI::Widget::PrefOpenFolder _sys_user_extension_dir; + UI::Widget::PrefOpenFolder _sys_user_themes_dir; + UI::Widget::PrefOpenFolder _sys_user_ui_dir; + UI::Widget::PrefOpenFolder _sys_user_icons_dir; + UI::Widget::PrefOpenFolder _sys_user_keys_dir; + UI::Widget::PrefOpenFolder _sys_user_palettes_dir; + UI::Widget::PrefOpenFolder _sys_user_templates_dir; + UI::Widget::PrefOpenFolder _sys_user_symbols_dir; + UI::Widget::PrefOpenFolder _sys_user_paint_servers_dir; + Gtk::Entry _sys_user_cache; + Gtk::Entry _sys_data; + Gtk::TextView _sys_icon; + Gtk::ScrolledWindow _sys_icon_scroll; + Gtk::TextView _sys_systemdata; + Gtk::ScrolledWindow _sys_systemdata_scroll; + + // UI page + UI::Widget::PrefCombo _ui_languages; + UI::Widget::PrefCheckButton _ui_colorsliders_top; + UI::Widget::PrefSpinButton _misc_recent; + UI::Widget::PrefCheckButton _ui_partialdynamic; + UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _ui_yaxisdown; + UI::Widget::PrefCheckButton _ui_rotationlock; + + //Spellcheck + UI::Widget::PrefCombo _spell_language; + UI::Widget::PrefCombo _spell_language2; + UI::Widget::PrefCombo _spell_language3; + UI::Widget::PrefCheckButton _spell_ignorenumbers; + UI::Widget::PrefCheckButton _spell_ignoreallcaps; + + // Bitmaps + UI::Widget::PrefCombo _misc_overs_bitmap; + UI::Widget::PrefEntryFileButtonHBox _misc_bitmap_editor; + UI::Widget::PrefEntryFileButtonHBox _misc_svg_editor; + UI::Widget::PrefCheckButton _misc_bitmap_autoreload; + UI::Widget::PrefSpinButton _bitmap_copy_res; + UI::Widget::PrefCheckButton _bitmap_ask; + UI::Widget::PrefCheckButton _svg_ask; + UI::Widget::PrefCombo _bitmap_link; + UI::Widget::PrefCombo _svg_link; + UI::Widget::PrefCombo _bitmap_scale; + UI::Widget::PrefSpinButton _bitmap_import_quality; + + UI::Widget::PrefEntry _kb_search; + UI::Widget::PrefCombo _kb_filelist; + + UI::Widget::PrefCheckButton _save_use_current_dir; + UI::Widget::PrefCheckButton _save_autosave_enable; + UI::Widget::PrefSpinButton _save_autosave_interval; + UI::Widget::PrefEntry _save_autosave_path; + UI::Widget::PrefSpinButton _save_autosave_max; + + Gtk::ComboBoxText _cms_display_profile; + UI::Widget::PrefCheckButton _cms_from_display; + UI::Widget::PrefCombo _cms_intent; + + UI::Widget::PrefCheckButton _cms_softproof; + UI::Widget::PrefCheckButton _cms_gamutwarn; + Gtk::ColorButton _cms_gamutcolor; + Gtk::ComboBoxText _cms_proof_profile; + UI::Widget::PrefCombo _cms_proof_intent; + UI::Widget::PrefCheckButton _cms_proof_blackpoint; + UI::Widget::PrefCheckButton _cms_proof_preserveblack; + + Gtk::Notebook _grids_notebook; + UI::Widget::PrefRadioButton _grids_no_emphasize_on_zoom; + UI::Widget::PrefRadioButton _grids_emphasize_on_zoom; + UI::Widget::DialogPage _grids_xy; + UI::Widget::DialogPage _grids_axonom; + // CanvasXYGrid properties: + UI::Widget::PrefUnit _grids_xy_units; + UI::Widget::PrefSpinButton _grids_xy_origin_x; + UI::Widget::PrefSpinButton _grids_xy_origin_y; + UI::Widget::PrefSpinButton _grids_xy_spacing_x; + UI::Widget::PrefSpinButton _grids_xy_spacing_y; + UI::Widget::PrefColorPicker _grids_xy_color; + UI::Widget::PrefColorPicker _grids_xy_empcolor; + UI::Widget::PrefSpinButton _grids_xy_empspacing; + UI::Widget::PrefCheckButton _grids_xy_dotted; + // CanvasAxonomGrid properties: + UI::Widget::PrefUnit _grids_axonom_units; + UI::Widget::PrefSpinButton _grids_axonom_origin_x; + UI::Widget::PrefSpinButton _grids_axonom_origin_y; + UI::Widget::PrefSpinButton _grids_axonom_spacing_y; + UI::Widget::PrefSpinButton _grids_axonom_angle_x; + UI::Widget::PrefSpinButton _grids_axonom_angle_z; + UI::Widget::PrefColorPicker _grids_axonom_color; + UI::Widget::PrefColorPicker _grids_axonom_empcolor; + UI::Widget::PrefSpinButton _grids_axonom_empspacing; + + // SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_usenamedcolors; + UI::Widget::PrefSpinButton _svgoutput_numericprecision; + UI::Widget::PrefSpinButton _svgoutput_minimumexponent; + UI::Widget::PrefCheckButton _svgoutput_inlineattrs; + UI::Widget::PrefSpinButton _svgoutput_indent; + UI::Widget::PrefCombo _svgoutput_pathformat; + UI::Widget::PrefCheckButton _svgoutput_forcerepeatcommands; + + // Attribute Checking controls for SVG Output page: + UI::Widget::PrefCheckButton _svgoutput_attrwarn; + UI::Widget::PrefCheckButton _svgoutput_attrremove; + UI::Widget::PrefCheckButton _svgoutput_stylepropwarn; + UI::Widget::PrefCheckButton _svgoutput_stylepropremove; + UI::Widget::PrefCheckButton _svgoutput_styledefaultswarn; + UI::Widget::PrefCheckButton _svgoutput_styledefaultsremove; + UI::Widget::PrefCheckButton _svgoutput_check_reading; + UI::Widget::PrefCheckButton _svgoutput_check_editing; + UI::Widget::PrefCheckButton _svgoutput_check_writing; + + // SVG Output export: + UI::Widget::PrefCheckButton _svgexport_insert_text_fallback; + UI::Widget::PrefCheckButton _svgexport_insert_mesh_polyfill; + UI::Widget::PrefCheckButton _svgexport_insert_hatch_polyfill; + UI::Widget::PrefCheckButton _svgexport_remove_marker_auto_start_reverse; + UI::Widget::PrefCheckButton _svgexport_remove_marker_context_paint; + + + /* + * Keyboard shortcut members + */ + class ModelColumns: public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(name); + add(id); + add(shortcut); + add(description); + add(shortcutid); + add(user_set); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> shortcut; + Gtk::TreeModelColumn<Glib::ustring> description; + Gtk::TreeModelColumn<unsigned int> shortcutid; + Gtk::TreeModelColumn<unsigned int> user_set; + }; + ModelColumns _kb_columns; + static ModelColumns &onKBGetCols(); + Glib::RefPtr<Gtk::TreeStore> _kb_store; + Gtk::TreeView _kb_tree; + Gtk::CellRendererAccel _kb_shortcut_renderer; + Glib::RefPtr<Gtk::TreeModelFilter> _kb_filter; + gboolean _kb_shortcuts_loaded; + + int _minimum_width; + int _minimum_height; + int _natural_width; + int _natural_height; + bool GetSizeRequest(const Gtk::TreeModel::iterator& iter); + void get_preferred_width_vfunc (int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_width_for_height_vfunc (int height, int& minimum_width, int& natural_width) const override { + minimum_width = _minimum_width; + natural_width = _natural_width; + } + void get_preferred_height_vfunc (int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + void get_preferred_height_for_width_vfunc (int width, int& minimum_height, int& natural_height) const override { + minimum_height = _minimum_height; + natural_height = _natural_height; + } + int _sb_width; + UI::Widget::DialogPage* _current_page; + + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, int id); + Gtk::TreeModel::iterator AddPage(UI::Widget::DialogPage& p, Glib::ustring title, Gtk::TreeModel::iterator parent, int id); + bool PresentPage(const Gtk::TreeModel::iterator& iter); + + static void AddSelcueCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddGradientCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddConvertGuidesCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddFirstAndLastCheckbox(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, bool def_value); + static void AddDotSizeSpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddBaseSimplifySpinbutton(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, double def_value); + static void AddNewObjectsStyle(UI::Widget::DialogPage& p, Glib::ustring const &prefs_path, const gchar* banner = nullptr); + + void on_pagelist_selection_changed(); + void on_reset_open_recent_clicked(); + void on_reset_prefs_clicked(); + + void initPageTools(); + void initPageUI(); + void initPageBehavior(); + void initPageIO(); + + void initPageRendering(); + void initPageSpellcheck(); + void initPageBitmaps(); + void initPageSystem(); + void initPageI18n(); // Do we still need it? + void initKeyboardShortcuts(Gtk::TreeModel::iterator iter_ui); + + void _presentPages(); + + /* + * Functions for the Keyboard shortcut editor panel + */ + void onKBReset(); + void onKBImport(); + void onKBExport(); + void onKBList(); + void onKBRealize(); + void onKBListKeyboardShortcuts(); + void onKBTreeEdited (const Glib::ustring& path, guint accel_key, Gdk::ModifierType accel_mods, guint hardware_keycode); + void onKBTreeCleared(const Glib::ustring& path_string); + bool onKBSearchKeyEvent(GdkEventKey *event); + bool onKBSearchFilter(const Gtk::TreeModel::const_iterator& iter); + static void onKBShortcutRenderer(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + +private: + void themeChange(); + void symbolicThemeCheck(); + void toggleSymbolic(); + void changeIconsColors(); + void resetIconsColors(bool themechange = false); + void resetIconsColorsWrapper(); + void changeIconsColor(guint32 /*color*/); + void get_highlight_colors(guint32 &colorsetbase, guint32 &colorsetsuccess, guint32 &colorsetwarning, + guint32 &colorseterror); + + InkscapePreferences(); + InkscapePreferences(InkscapePreferences const &d); + InkscapePreferences operator=(InkscapePreferences const &d); + bool _init; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_INKSCAPE_PREFERENCES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/input.cpp b/src/ui/dialog/input.cpp new file mode 100644 index 0000000..438914d --- /dev/null +++ b/src/ui/dialog/input.cpp @@ -0,0 +1,1792 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Input devices dialog (new) - implementation. + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include <set> +#include <list> +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/buttonbox.h> +#include <gtkmm/cellrenderercombo.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/grid.h> +#include <gtkmm/liststore.h> +#include <gtkmm/menubar.h> +#include <gtkmm/notebook.h> +#include <gtkmm/paned.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treestore.h> +#include <gtkmm/eventbox.h> + +#include "device-manager.h" +#include "preferences.h" + +#include "input.h" + +/* XPM */ +static char const * core_xpm[] = { +"16 16 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" ", +" ", +" .++++++. ", +" +@+@@+@+ ", +" +@+@@+@+ ", +" +.+..+.+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" +@@@@@@+ ", +" .++++++. ", +" ", +" ", +" "}; + +/* XPM */ +static char const *eraser[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOO OO", +"OOOOOOOOOOOO . O", +"OOOOOOOOOOO . OO", +"OOOOOOOOOO . OOO", +"OOOOOOOOO . OOOO", +"OOOOOOOO . OOOOO", +"OOOOOOOXo OOOOOO", +"OOOOOOXoXOOOOOOO", +"OOOOOXoXOOOOOOOO", +"OOOOXoXOOOOOOOOO", +"OOOXoXOOOOOOOOOO", +"OOXoXOOOOOOOOOOO", +"OOXXOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *mouse[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXX XXXXXXX", +"XXXXX . XXXXXXX", +"XXXX .... XXXXXX", +"XXXX .... XXXXXX", +"XXXXX .... XXXXX", +"XXXXX .... XXXXX", +"XXXXXX .... XXXX", +"XXXXXX .... XXXX", +"XXXXXXX . XXXXX", +"XXXXXXX XXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *pen[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXX XX", +"XXXXXXXXXXXX . X", +"XXXXXXXXXXX . XX", +"XXXXXXXXXX . XXX", +"XXXXXXXXX . XXXX", +"XXXXXXXX . XXXXX", +"XXXXXXX . XXXXXX", +"XXXXXX . XXXXXXX", +"XXXXX . XXXXXXXX", +"XXXX . XXXXXXXXX", +"XXX . XXXXXXXXXX", +"XX . XXXXXXXXXXX", +"XX XXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *sidebuttons[] = { +/* columns rows colors chars-per-pixel */ +"16 16 4 1", +" c black", +". c #808080", +"o c green", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO", +"O..............O", +"O.OOOOOOOOOOOO.O", +"O OOOOOOOO O", +"O o OOOOOOOO o O", +"O o OOOOOOOO o O", +"O OOOOOOOO O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O.OOOOOOOOOOOO.O", +"O..............O", +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOOOO" +}; + +/* XPM */ +static char const *tablet[] = { +/* columns rows colors chars-per-pixel */ +"16 16 3 1", +" c black", +". c gray100", +"X c None", +/* pixels */ +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX", +"X X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X ............ X", +"X X", +"XXXXXXXXXXXXXXXX", +"XXXXXXXXXXXXXXXX" +}; + +/* XPM */ +static char const *tip[] = { +/* columns rows colors chars-per-pixel */ +"16 16 5 1", +" c black", +". c green", +"X c #808080", +"o c gray100", +"O c None", +/* pixels */ +"OOOOOOOOOOOOOOOO", +"OOOOOOOOOOOOOXOO", +"OOOOOOOOOOOOXoXO", +"OOOOOOOOOOOXoXOO", +"OOOOOOOOOOXoXOOO", +"OOOOOOOOOXoXOOOO", +"OOOOOOOOXoXOOOOO", +"OOOOOOO oXOOOOOO", +"OOOOOO . OOOOOOO", +"OOOOO . OOOOOOOO", +"OOOO . OOOOOOOOO", +"OOO . OOOOOOOOOO", +"OO . OOOOOOOOOOO", +"OO OOOOOOOOOOOO", +"OOOOXXXXXOOOOOOO", +"OOOOOOOOOXXXXXOO" +}; + +/* XPM */ +static char const *button_none[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c #808080", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX .. XX", +"X .XX. X", +"X.XX X.X", +"X.X XX.X", +"X .XX. X", +"XX .. XX", +"XXXXXXXX" +}; +/* XPM */ +static char const *button_off[] = { +/* columns rows colors chars-per-pixel */ +"8 8 4 1", +" c black", +". c #808080", +"X c gray100", +"o c None", +/* pixels */ +"oooooooo", +"oo. .oo", +"o. XX .o", +"o XXXX o", +"o XXXX o", +"o. XX .o", +"oo. .oo", +"oooooooo" +}; +/* XPM */ +static char const *button_on[] = { +/* columns rows colors chars-per-pixel */ +"8 8 3 1", +" c black", +". c green", +"X c None", +/* pixels */ +"XXXXXXXX", +"XX XX", +"X .. X", +"X .... X", +"X .... X", +"X .. X", +"XX XX", +"XXXXXXXX" +}; + +/* XPM */ +static char const * axis_none_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #808080", +" ", +" .++++++++++++++++++. ", +" .+ . .+. ", +" + . . . + ", +" + . . . + ", +" .+. . +. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_off_xpm[] = { +"24 8 4 1", +" c None", +". c #808080", +"+ c #000000", +"@ c #FFFFFF", +" ", +" .++++++++++++++++++. ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" +@@@@@@@@@@@@@@@@@@@@+ ", +" .+@@@@@@@@@@@@@@@@@@+. ", +" .++++++++++++++++++. ", +" "}; +/* XPM */ +static char const * axis_on_xpm[] = { +"24 8 3 1", +" c None", +". c #000000", +"+ c #00FF00", +" ", +" .................... ", +" ..++++++++++++++++++.. ", +" .++++++++++++++++++++. ", +" .++++++++++++++++++++. ", +" ..++++++++++++++++++.. ", +" .................... ", +" "}; + +using Inkscape::InputDevice; + +namespace Inkscape { +namespace UI { +namespace Dialog { + + + +class DeviceModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + Gtk::TreeModelColumn<bool> toggler; + Gtk::TreeModelColumn<Glib::ustring> expander; + Gtk::TreeModelColumn<Glib::ustring> description; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > thumbnail; + Gtk::TreeModelColumn<Glib::RefPtr<InputDevice const> > device; + Gtk::TreeModelColumn<Gdk::InputMode> mode; + + DeviceModelColumns() { add(toggler), add(expander), add(description); add(thumbnail); add(device); add(mode); } +}; + +static std::map<Gdk::InputMode, Glib::ustring> &getModeToString() +{ + static std::map<Gdk::InputMode, Glib::ustring> mapping; + if (mapping.empty()) { + mapping[Gdk::MODE_DISABLED] = _("Disabled"); + mapping[Gdk::MODE_SCREEN] = C_("Input device", "Screen"); + mapping[Gdk::MODE_WINDOW] = _("Window"); + } + + return mapping; +} + +static int getModeId(Gdk::InputMode im) +{ + if (im == Gdk::MODE_DISABLED) return 0; + if (im == Gdk::MODE_SCREEN) return 1; + if (im == Gdk::MODE_WINDOW) return 2; + + return 0; +} + +static std::map<Glib::ustring, Gdk::InputMode> &getStringToMode() +{ + static std::map<Glib::ustring, Gdk::InputMode> mapping; + if (mapping.empty()) { + mapping[_("Disabled")] = Gdk::MODE_DISABLED; + mapping[_("Screen")] = Gdk::MODE_SCREEN; + mapping[_("Window")] = Gdk::MODE_WINDOW; + } + + return mapping; +} + + + +class InputDialogImpl : public InputDialog { +public: + InputDialogImpl(); + ~InputDialogImpl() override = default; + +private: + class ConfPanel : public Gtk::VBox + { + public: + ConfPanel(); + ~ConfPanel() override; + + class Blink : public Preferences::Observer + { + public: + Blink(ConfPanel &parent); + ~Blink() override; + void notify(Preferences::Entry const &new_val) override; + + ConfPanel &parent; + }; + + static void commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store); + static void setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + static void commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store); + static void setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter); + + void saveSettings(); + void onTreeSelect(); + void useExtToggled(); + + void onModeChange(); + void setKeys(gint count); + void setAxis(gint count); + + Glib::RefPtr<Gtk::TreeStore> confDeviceStore; + Gtk::TreeIter confDeviceIter; + Gtk::TreeView confDeviceTree; + Gtk::ScrolledWindow confDeviceScroller; + Blink watcher; + Gtk::CheckButton useExt; + Gtk::Button save; + Gtk::Paned pane; + Gtk::VBox detailsBox; + Gtk::HBox titleFrame; + Gtk::Label titleLabel; + Inkscape::UI::Widget::Frame axisFrame; + Inkscape::UI::Widget::Frame keysFrame; + Gtk::VBox axisVBox; + Gtk::ComboBoxText modeCombo; + Gtk::Label modeLabel; + Gtk::HBox modeBox; + + class KeysColumns : public Gtk::TreeModel::ColumnRecord + { + public: + KeysColumns() + { + add(name); + add(value); + } + ~KeysColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> value; + }; + + KeysColumns keysColumns; + KeysColumns axisColumns; + + Glib::RefPtr<Gtk::ListStore> axisStore; + Gtk::TreeView axisTree; + Gtk::ScrolledWindow axisScroll; + + Glib::RefPtr<Gtk::ListStore> keysStore; + Gtk::TreeView keysTree; + Gtk::ScrolledWindow keysScroll; + Gtk::CellRendererAccel _kb_shortcut_renderer; + + + }; + + static DeviceModelColumns &getCols(); + + enum PixId {PIX_CORE, PIX_PEN, PIX_MOUSE, PIX_TIP, PIX_TABLET, PIX_ERASER, PIX_SIDEBUTTONS, + PIX_BUTTONS_NONE, PIX_BUTTONS_ON, PIX_BUTTONS_OFF, + PIX_AXIS_NONE, PIX_AXIS_ON, PIX_AXIS_OFF}; + + static Glib::RefPtr<Gdk::Pixbuf> getPix(PixId id); + + std::map<Glib::ustring, std::set<guint> > buttonMap; + std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap; + + GdkInputSource lastSourceSeen; + Glib::ustring lastDevnameSeen; + + Glib::RefPtr<Gtk::TreeStore> deviceStore; + Gtk::TreeIter deviceIter; + Gtk::TreeView deviceTree; + Inkscape::UI::Widget::Frame testFrame; + Inkscape::UI::Widget::Frame axisFrame; + Gtk::ScrolledWindow treeScroller; + Gtk::ScrolledWindow detailScroller; + Gtk::Paned splitter; + Gtk::Paned split2; + Gtk::Label devName; + Gtk::Label devKeyCount; + Gtk::Label devAxesCount; + Gtk::ComboBoxText axesCombo; + Gtk::ProgressBar axesValues[6]; + Gtk::Grid axisTable; + Gtk::ComboBoxText buttonCombo; + Gtk::ComboBoxText linkCombo; + sigc::connection linkConnection; + Gtk::Label keyVal; + Gtk::Entry keyEntry; + Gtk::Notebook topHolder; + Gtk::Image testThumb; + Gtk::Image testButtons[24]; + Gtk::Image testAxes[8]; + Gtk::Grid imageTable; + Gtk::EventBox testDetector; + + ConfPanel cfgPanel; + + + static void setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet ); + void setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ); + void updateTestButtons( Glib::ustring const& key, gint hotButton ); + void updateTestAxes( Glib::ustring const& key, GdkDevice* dev ); + void mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev); + Glib::ustring getKeyFor( GdkDevice* device ); + bool eventSnoop(GdkEvent* event); + void linkComboChanged(); + void resyncToSelection(); + void handleDeviceChange(Glib::RefPtr<InputDevice const> device); + void updateDeviceAxes(Glib::RefPtr<InputDevice const> device); + void updateDeviceButtons(Glib::RefPtr<InputDevice const> device); + static void updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree); + + static bool findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result); + static bool findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result); + +}; // class InputDialogImpl + + +DeviceModelColumns &InputDialogImpl::getCols() +{ + static DeviceModelColumns cols; + return cols; +} + +Glib::RefPtr<Gdk::Pixbuf> InputDialogImpl::getPix(PixId id) +{ + static std::map<PixId, Glib::RefPtr<Gdk::Pixbuf> > mappings; + + mappings[PIX_CORE] = Gdk::Pixbuf::create_from_xpm_data(core_xpm); + mappings[PIX_PEN] = Gdk::Pixbuf::create_from_xpm_data(pen); + mappings[PIX_MOUSE] = Gdk::Pixbuf::create_from_xpm_data(mouse); + mappings[PIX_TIP] = Gdk::Pixbuf::create_from_xpm_data(tip); + mappings[PIX_TABLET] = Gdk::Pixbuf::create_from_xpm_data(tablet); + mappings[PIX_ERASER] = Gdk::Pixbuf::create_from_xpm_data(eraser); + mappings[PIX_SIDEBUTTONS] = Gdk::Pixbuf::create_from_xpm_data(sidebuttons); + + mappings[PIX_BUTTONS_NONE] = Gdk::Pixbuf::create_from_xpm_data(button_none); + mappings[PIX_BUTTONS_ON] = Gdk::Pixbuf::create_from_xpm_data(button_on); + mappings[PIX_BUTTONS_OFF] = Gdk::Pixbuf::create_from_xpm_data(button_off); + + mappings[PIX_AXIS_NONE] = Gdk::Pixbuf::create_from_xpm_data(axis_none_xpm); + mappings[PIX_AXIS_ON] = Gdk::Pixbuf::create_from_xpm_data(axis_on_xpm); + mappings[PIX_AXIS_OFF] = Gdk::Pixbuf::create_from_xpm_data(axis_off_xpm); + + Glib::RefPtr<Gdk::Pixbuf> pix; + if (mappings.find(id) != mappings.end()) { + pix = mappings[id]; + } + + return pix; +} + + +// Now that we've defined the *Impl class, we can do the method to acquire one. +InputDialog &InputDialog::getInstance() +{ + InputDialog *dialog = new InputDialogImpl(); + return *dialog; +} + + +InputDialogImpl::InputDialogImpl() : + InputDialog(), + + lastSourceSeen((GdkInputSource)-1), + lastDevnameSeen(""), + deviceStore(Gtk::TreeStore::create(getCols())), + deviceIter(), + deviceTree(deviceStore), + testFrame(_("Test Area")), + axisFrame(_("Axis")), + treeScroller(), + detailScroller(), + splitter(), + split2(Gtk::ORIENTATION_VERTICAL), + axisTable(), + linkCombo(), + topHolder(), + imageTable(), + testDetector(), + cfgPanel() +{ + Gtk::Box *contents = _getContents(); + + treeScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + treeScroller.set_shadow_type(Gtk::SHADOW_IN); + treeScroller.add(deviceTree); + treeScroller.set_size_request(50, 0); + + split2.pack1(axisFrame, false, false); + split2.pack2(testFrame, true, true); + + splitter.pack1(treeScroller); + splitter.pack2(split2); + + testDetector.add(imageTable); + testFrame.add(testDetector); + testThumb.set(getPix(PIX_TABLET)); + testThumb.set_margin_top(24); + testThumb.set_margin_bottom(24); + testThumb.set_margin_start(24); + testThumb.set_margin_end(24); + testThumb.set_hexpand(); + testThumb.set_vexpand(); + imageTable.attach(testThumb, 0, 0, 8, 1); + + { + guint col = 0; + guint row = 1; + for (auto & testButton : testButtons) { + testButton.set(getPix(PIX_BUTTONS_NONE)); + imageTable.attach(testButton, col, row, 1, 1); + col++; + if (col > 7) { + col = 0; + row++; + } + } + + col = 0; + for (auto & testAxe : testAxes) { + testAxe.set(getPix(PIX_AXIS_NONE)); + imageTable.attach(testAxe, col * 2, row, 2, 1); + col++; + if (col > 3) { + col = 0; + row++; + } + } + } + + + // This is a hidden preference to enable the "hardware" details in a separate tab + // By default this is not available to users + if (Preferences::get()->getBool("/dialogs/inputdevices/test")) { + topHolder.append_page(cfgPanel, _("Configuration")); + topHolder.append_page(splitter, _("Hardware")); + topHolder.show_all(); + topHolder.set_current_page(0); + contents->pack_start(topHolder); + } else { + contents->pack_start(cfgPanel); + } + + + int rowNum = 0; + + axisFrame.add(axisTable); + + Gtk::Label *lbl = Gtk::manage(new Gtk::Label(_("Link:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + linkCombo.append(_("None")); + linkCombo.set_active_text(_("None")); + linkCombo.set_sensitive(false); + linkConnection = linkCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::linkComboChanged)); + axisTable.attach(linkCombo, 1, rowNum, 1, 1); + rowNum++; + + lbl = Gtk::manage(new Gtk::Label(_("Axes count:"))); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devAxesCount, 1, rowNum, 1, 1); + rowNum++; + + for (auto & axesValue : axesValues) { + lbl = Gtk::manage(new Gtk::Label(_("axis:"))); + lbl->set_hexpand(); + axisTable.attach(*lbl, 0, rowNum, 1, 1); + + axesValue.set_hexpand(); + axisTable.attach(axesValue, 1, rowNum, 1, 1); + axesValue.set_sensitive(false); + + rowNum++; + + + } + + lbl = Gtk::manage(new Gtk::Label(_("Button count:"))); + + axisTable.attach(*lbl, 0, rowNum, 1, 1); + axisTable.attach(devKeyCount, 1, rowNum, 1, 1); + + rowNum++; + + axisTable.attach(keyVal, 0, rowNum, 2, 1); + + rowNum++; + + testDetector.signal_event().connect(sigc::mem_fun(*this, &InputDialogImpl::eventSnoop)); + + // TODO: Extension event stuff has been removed from public API in GTK+ 3 + // Need to check that this hasn't broken anything + testDetector.add_events(Gdk::POINTER_MOTION_MASK|Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK |Gdk::PROXIMITY_IN_MASK|Gdk::PROXIMITY_OUT_MASK|Gdk::SCROLL_MASK); + + axisTable.attach(keyEntry, 0, rowNum, 2, 1); + + rowNum++; + + + axisTable.set_sensitive(false); + +//- 16x16/devices +// gnome-dev-mouse-optical +// input-mouse +// input-tablet +// mouse + + //Add the TreeView's view columns: + deviceTree.append_column("I", getCols().thumbnail); + deviceTree.append_column("Bar", getCols().description); + + deviceTree.set_enable_tree_lines(); + deviceTree.set_headers_visible(false); + deviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::resyncToSelection)); + + + setupTree( deviceStore, deviceIter ); + + Inkscape::DeviceManager::getManager().signalDeviceChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::handleDeviceChange)); + Inkscape::DeviceManager::getManager().signalAxesChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceAxes)); + Inkscape::DeviceManager::getManager().signalButtonsChanged().connect(sigc::mem_fun(*this, &InputDialogImpl::updateDeviceButtons)); + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), deviceIter, &deviceTree)); + + deviceTree.expand_all(); + show_all_children(); +} + +class TabletTmp { +public: + TabletTmp() = default; + + Glib::ustring name; + std::list<Glib::RefPtr<InputDevice const> > devices; +}; + +static Glib::ustring getCommon( std::list<Glib::ustring> const &names ) +{ + Glib::ustring result; + + if ( !names.empty() ) { + size_t pos = 0; + bool match = true; + while ( match ) { + if ( names.begin()->length() > pos ) { + gunichar ch = (*names.begin())[pos]; + for (const auto & name : names) { + if ( (pos >= name.length()) + || (name[pos] != ch) ) { + match = false; + break; + } + } + if (match) { + result += ch; + pos++; + } + } else { + match = false; + } + } + } + + return result; +} + + +void InputDialogImpl::ConfPanel::onModeChange() +{ + Glib::ustring newText = modeCombo.get_active_text(); + + Glib::RefPtr<Gtk::TreeSelection> sel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = sel->get_selected(); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + +} + + +void InputDialogImpl::setupTree( Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeIter &tablet ) +{ + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + if ( !devList.empty() ) { + //Gtk::TreeModel::Row row = *(store->append()); + //row[getCols().description] = _("Hardware"); + + // Let's make some tablets!!! + std::list<TabletTmp> tablets; + std::set<Glib::ustring> consumed; + + // Phase 1 - figure out which tablets are present + for (auto dev : devList) { + if ( dev ) { + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + consumed.insert( dev->getId() ); + if ( tablets.empty() ) { + TabletTmp tmp; + tablets.push_back(tmp); + } + tablets.back().devices.push_back(dev); + } + } else { + g_warning("Null device in list"); + } + } + + // Phase 2 - build a UI for the present devices + for (auto & it : tablets) { + tablet = store->prepend(/*row.children()*/); + Gtk::TreeModel::Row childrow = *tablet; + if ( it.name.empty() ) { + // Check to see if we can derive one + std::list<Glib::ustring> names; + for (auto & device : it.devices) { + names.push_back( device->getName() ); + } + Glib::ustring common = getCommon(names); + if ( !common.empty() ) { + it.name = common; + } + } + childrow[getCols().description] = it.name.empty() ? _("Tablet") : it.name ; + childrow[getCols().thumbnail] = getPix(PIX_TABLET); + + // Check if there is an eraser we can link to a pen + for ( std::list<Glib::RefPtr<InputDevice const> >::iterator it2 = it.devices.begin(); it2 != it.devices.end(); ++it2 ) { + Glib::RefPtr<InputDevice const> dev = *it2; + if ( dev->getSource() == Gdk::SOURCE_PEN ) { + for (auto dev2 : it.devices) { + if ( dev2->getSource() == Gdk::SOURCE_ERASER ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), dev2->getId()); + break; // only check the first eraser... for now + } + break; // only check the first pen... for now + } + } + } + + for (auto dev : it.devices) { + Gtk::TreeModel::Row deviceRow = *(store->append(childrow.children())); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + switch ( dev->getSource() ) { + case Gdk::SOURCE_MOUSE: + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + break; + case Gdk::SOURCE_PEN: + if (deviceRow[getCols().description] == _("pad")) { + deviceRow[getCols().thumbnail] = getPix(PIX_SIDEBUTTONS); + } else { + deviceRow[getCols().thumbnail] = getPix(PIX_TIP); + } + break; + case Gdk::SOURCE_CURSOR: + deviceRow[getCols().thumbnail] = getPix(PIX_MOUSE); + break; + case Gdk::SOURCE_ERASER: + deviceRow[getCols().thumbnail] = getPix(PIX_ERASER); + break; + default: + ; // nothing + } + } + } + + for (auto dev : devList) { + if ( dev && (consumed.find( dev->getId() ) == consumed.end()) ) { + Gtk::TreeModel::Row deviceRow = *(store->prepend(/*row.children()*/)); + deviceRow[getCols().description] = dev->getName(); + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + deviceRow[getCols().thumbnail] = getPix(PIX_CORE); + } + } + + } else { + g_warning("No devices found"); + } +} + + +InputDialogImpl::ConfPanel::ConfPanel() : + Gtk::VBox(), + confDeviceStore(Gtk::TreeStore::create(getCols())), + confDeviceIter(), + confDeviceTree(confDeviceStore), + confDeviceScroller(), + watcher(*this), + useExt(_("_Use pressure-sensitive tablet (requires restart)"), true), + save(_("_Save"), true), + detailsBox(false, 4), + titleFrame(false, 4), + titleLabel(""), + axisFrame(_("Axes")), + keysFrame(_("Keys")), + modeLabel(_("Mode:")), + modeBox(false, 4) + +{ + + + confDeviceScroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + confDeviceScroller.set_shadow_type(Gtk::SHADOW_IN); + confDeviceScroller.add(confDeviceTree); + confDeviceScroller.set_size_request(120, 0); + + /* class Foo : public Gtk::TreeModel::ColumnRecord { + public : + Gtk::TreeModelColumn<Glib::ustring> one; + Foo() {add(one);} + }; + static Foo foo; + + //Add the TreeView's view columns: + { + Gtk::CellRendererToggle *rendr = new Gtk::CellRendererToggle(); + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("xx", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setCellStateToggle)); + rendr->signal_toggled().connect(sigc::bind(sigc::ptr_fun(commitCellStateChange), confDeviceStore)); + } + }*/ + + //int expPos = confDeviceTree.append_column("", getCols().expander); + + confDeviceTree.append_column("I", getCols().thumbnail); + confDeviceTree.append_column("Bar", getCols().description); + + //confDeviceTree.get_column(0)->set_fixed_width(100); + //confDeviceTree.get_column(1)->set_expand(); + +/* { + Gtk::TreeViewColumn *col = new Gtk::TreeViewColumn("X", *rendr); + if (col) { + confDeviceTree.append_column(*col); + col->set_cell_data_func(*rendr, sigc::ptr_fun(setModeCellString)); + rendr->signal_edited().connect(sigc::bind(sigc::ptr_fun(commitCellModeChange), confDeviceStore)); + rendr->property_editable() = true; + } + }*/ + + //confDeviceTree.set_enable_tree_lines(); + confDeviceTree.property_enable_tree_lines() = false; + confDeviceTree.property_enable_grid_lines() = false; + confDeviceTree.set_headers_visible(false); + //confDeviceTree.set_expander_column( *confDeviceTree.get_column(expPos - 1) ); + + confDeviceTree.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onTreeSelect)); + + setupTree( confDeviceStore, confDeviceIter ); + + Inkscape::DeviceManager::getManager().signalLinkChanged().connect(sigc::bind(sigc::ptr_fun(&InputDialogImpl::updateDeviceLinks), confDeviceIter, &confDeviceTree)); + + confDeviceTree.expand_all(); + + useExt.set_active(Preferences::get()->getBool("/options/useextinput/value")); + useExt.signal_toggled().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::useExtToggled)); + + auto buttonBox = Gtk::manage(new Gtk::ButtonBox); + buttonBox->set_layout (Gtk::BUTTONBOX_END); + //Gtk::Alignment *align = new Gtk::Alignment(Gtk::ALIGN_END, Gtk::ALIGN_START, 0, 0); + buttonBox->add(save); + save.signal_clicked().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::saveSettings)); + + titleFrame.pack_start(titleLabel, true, true); + //titleFrame.set_shadow_type(Gtk::SHADOW_IN); + + modeCombo.append(getModeToString()[Gdk::MODE_DISABLED]); + modeCombo.append(getModeToString()[Gdk::MODE_SCREEN]); + modeCombo.append(getModeToString()[Gdk::MODE_WINDOW]); + modeCombo.set_tooltip_text(_("A device can be 'Disabled', its co-ordinates mapped to the whole 'Screen', or to a single (usually focused) 'Window'")); + modeCombo.signal_changed().connect(sigc::mem_fun(*this, &InputDialogImpl::ConfPanel::onModeChange)); + + modeBox.pack_start(modeLabel, false, false); + modeBox.pack_start(modeCombo, true, true); + + axisVBox.add(axisScroll); + axisFrame.add(axisVBox); + + keysFrame.add(keysScroll); + + /** + * Scrolled Window + */ + keysScroll.add(keysTree); + keysScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + keysScroll.set_shadow_type(Gtk::SHADOW_IN); + keysScroll.set_size_request(120, 80); + + keysStore = Gtk::ListStore::create(keysColumns); + + _kb_shortcut_renderer.property_editable() = true; + + keysTree.set_model(keysStore); + keysTree.set_headers_visible(false); + keysTree.append_column("Name", keysColumns.name); + keysTree.append_column("Value", keysColumns.value); + + //keysTree.append_column("Value", _kb_shortcut_renderer); + //keysTree.get_column(1)->add_attribute(_kb_shortcut_renderer.property_text(), keysColumns.value); + //_kb_shortcut_renderer.signal_accel_edited().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeEdited) ); + //_kb_shortcut_renderer.signal_accel_cleared().connect( sigc::mem_fun(*this, &InputDialogImpl::onKBTreeCleared) ); + + axisStore = Gtk::ListStore::create(axisColumns); + + axisTree.set_model(axisStore); + axisTree.set_headers_visible(false); + axisTree.append_column("Name", axisColumns.name); + axisTree.append_column("Value", axisColumns.value); + + /** + * Scrolled Window + */ + axisScroll.add(axisTree); + axisScroll.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + axisScroll.set_shadow_type(Gtk::SHADOW_IN); + axisScroll.set_size_request(0, 150); + + pane.pack1(confDeviceScroller); + pane.pack2(detailsBox); + + detailsBox.pack_start(titleFrame, false, false, 6); + detailsBox.pack_start(modeBox, false, false, 6); + detailsBox.pack_start(axisFrame, false, false); + detailsBox.pack_start(keysFrame, false, false); + detailsBox.set_border_width(4); + + pack_start(pane, true, true); + pack_start(useExt, Gtk::PACK_SHRINK); + pack_start(*buttonBox, false, false); + + // Select the first device + confDeviceTree.get_selection()->select(confDeviceStore->get_iter("0")); + +} + +InputDialogImpl::ConfPanel::~ConfPanel() += default; + +void InputDialogImpl::ConfPanel::setModeCellString(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererCombo *combo = dynamic_cast<Gtk::CellRendererCombo *>(rndr); + if (combo) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (dev && (getModeToString().find(mode) != getModeToString().end())) { + combo->property_text() = getModeToString()[mode]; + } else { + combo->property_text() = ""; + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellModeChange(Glib::ustring const &path, Glib::ustring const &newText, Glib::RefPtr<Gtk::TreeStore> store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev && (getStringToMode().find(newText) != getStringToMode().end())) { + Gdk::InputMode mode = getStringToMode()[newText]; + Inkscape::DeviceManager::getManager().setMode( dev->getId(), mode ); + } + } + + +} + +void InputDialogImpl::ConfPanel::setCellStateToggle(Gtk::CellRenderer *rndr, Gtk::TreeIter const &iter) +{ + if (iter) { + Gtk::CellRendererToggle *toggle = dynamic_cast<Gtk::CellRendererToggle *>(rndr); + if (toggle) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + toggle->set_active(mode != Gdk::MODE_DISABLED); + } else { + toggle->set_active(false); + } + } + } +} + +void InputDialogImpl::ConfPanel::commitCellStateChange(Glib::ustring const &path, Glib::RefPtr<Gtk::TreeStore> store) +{ + Gtk::TreeIter iter = store->get_iter(path); + if (iter) { + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if (dev) { + Gdk::InputMode mode = (*iter)[getCols().mode]; + if (mode == Gdk::MODE_DISABLED) { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_SCREEN ); + } else { + Inkscape::DeviceManager::getManager().setMode( dev->getId(), Gdk::MODE_DISABLED ); + } + } + } +} + +void InputDialogImpl::ConfPanel::onTreeSelect() +{ + Glib::RefPtr<Gtk::TreeSelection> treeSel = confDeviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + Gdk::InputMode mode = (*iter)[getCols().mode]; + modeCombo.set_active(getModeId(mode)); + + titleLabel.set_markup("<b>" + row[getCols().description] + "</b>"); + + if (dev) { + setKeys(dev->getNumKeys()); + setAxis(dev->getNumAxes()); + } + } +} +void InputDialogImpl::ConfPanel::saveSettings() +{ + Inkscape::DeviceManager::getManager().saveConfig(); +} + +void InputDialogImpl::ConfPanel::useExtToggled() +{ + bool active = useExt.get_active(); + if (active != Preferences::get()->getBool("/options/useextinput/value")) { + Preferences::get()->setBool("/options/useextinput/value", active); + if (active) { + // As a work-around for a common problem, enable tablet toggles on the calligraphic tool. + // Covered in Launchpad bug #196195. + Preferences::get()->setBool("/tools/tweak/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usepressure", true); + Preferences::get()->setBool("/tools/calligraphic/usetilt", true); + } + } +} + +InputDialogImpl::ConfPanel::Blink::Blink(ConfPanel &parent) : + Preferences::Observer("/options/useextinput/value"), + parent(parent) +{ + Preferences::get()->addObserver(*this); +} + +InputDialogImpl::ConfPanel::Blink::~Blink() +{ + Preferences::get()->removeObserver(*this); +} + +void InputDialogImpl::ConfPanel::Blink::notify(Preferences::Entry const &new_val) +{ + parent.useExt.set_active(new_val.getBool()); +} + +void InputDialogImpl::handleDeviceChange(Glib::RefPtr<InputDevice const> device) +{ +// g_message("OUCH!!!! for %p hits %s", &device, device->getId().c_str()); + std::vector<Glib::RefPtr<Gtk::TreeStore> > stores; + stores.push_back(deviceStore); + stores.push_back(cfgPanel.confDeviceStore); + + for (auto & store : stores) { + Gtk::TreeModel::iterator deviceIter; + store->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + if ( deviceIter ) { + Gdk::InputMode mode = device->getMode(); + Gtk::TreeModel::Row row = *deviceIter; + if (row[getCols().mode] != mode) { + row[getCols().mode] = mode; + } + } + } +} + +void InputDialogImpl::updateDeviceAxes(Glib::RefPtr<InputDevice const> device) +{ + gint live = device->getLiveAxes(); + + std::map<guint, std::pair<guint, gdouble> > existing = axesMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( (existing.find(num) == existing.end()) || (existing[num].first < 2) ) { + axesMap[device->getId()][num].first = 2; + axesMap[device->getId()][num].second = 0.0; + } + } + } + updateTestAxes( device->getId(), nullptr ); +} + +void InputDialogImpl::updateDeviceButtons(Glib::RefPtr<InputDevice const> device) +{ + gint live = device->getLiveButtons(); + std::set<guint> existing = buttonMap[device->getId()]; + gint mask = 0x1; + for ( gint num = 0; num < 32; num++, mask <<= 1) { + if ( (mask & live) != 0 ) { + if ( existing.find(num) == existing.end() ) { + buttonMap[device->getId()].insert(num); + } + } + } + updateTestButtons(device->getId(), -1); +} + + +bool InputDialogImpl::findDevice(const Gtk::TreeModel::iterator& iter, + Glib::ustring id, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if ( dev && (dev->getId() == id) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +bool InputDialogImpl::findDeviceByLink(const Gtk::TreeModel::iterator& iter, + Glib::ustring link, + Gtk::TreeModel::iterator* result) +{ + bool stop = false; + Glib::RefPtr<InputDevice const> dev = (*iter)[getCols().device]; + if ( dev && (dev->getLink() == link) ) { + if ( result ) { + *result = iter; + } + stop = true; + } + return stop; +} + +void InputDialogImpl::updateDeviceLinks(Glib::RefPtr<InputDevice const> device, Gtk::TreeIter tabletIter, Gtk::TreeView *tree) +{ + Glib::RefPtr<Gtk::TreeStore> deviceStore = Glib::RefPtr<Gtk::TreeStore>::cast_dynamic(tree->get_model()); + +// g_message("Links!!!! for %p hits [%s] with link of [%s]", &device, device->getId().c_str(), device->getLink().c_str()); + Gtk::TreeModel::iterator deviceIter; + deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDevice), + device->getId(), + &deviceIter) ); + + if ( deviceIter ) { + // Found the device concerned. Can proceed. + + if ( device->getLink().empty() ) { + // is now unlinked +// g_message("Item %s is unlinked", device->getId().c_str()); + if ( deviceIter->parent() != tabletIter ) { + // Not the child of the tablet. move on up + + Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(tabletIter->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + } else { + // is linking + if ( deviceIter->parent() == tabletIter ) { + // Simple case. Not already linked + + Gtk::TreeIter newGroup = deviceStore->append(tabletIter->children()); + (*newGroup)[getCols().description] = _("Pen"); + (*newGroup)[getCols().thumbnail] = getPix(PIX_PEN); + + Glib::RefPtr<InputDevice const> dev = (*deviceIter)[getCols().device]; + Glib::ustring descr = (*deviceIter)[getCols().description]; + Glib::RefPtr<Gdk::Pixbuf> thumb = (*deviceIter)[getCols().thumbnail]; + + Gtk::TreeModel::Row deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + + + Gtk::TreeModel::iterator linkIter; + deviceStore->foreach_iter( sigc::bind<Glib::ustring, Gtk::TreeModel::iterator*>( + sigc::ptr_fun(&InputDialogImpl::findDeviceByLink), + device->getId(), + &linkIter) ); + if ( linkIter ) { + dev = (*linkIter)[getCols().device]; + descr = (*linkIter)[getCols().description]; + thumb = (*linkIter)[getCols().thumbnail]; + + deviceRow = *deviceStore->append(newGroup->children()); + deviceRow[getCols().description] = descr; + deviceRow[getCols().thumbnail] = thumb; + deviceRow[getCols().device] = dev; + deviceRow[getCols().mode] = dev->getMode(); + Gtk::TreeModel::iterator oldParent = linkIter->parent(); + deviceStore->erase(linkIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + } + + Gtk::TreeModel::iterator oldParent = deviceIter->parent(); + deviceStore->erase(deviceIter); + if ( oldParent->children().empty() ) { + deviceStore->erase(oldParent); + } + tree->expand_row(Gtk::TreePath(newGroup), true); + } + } + } +} + +void InputDialogImpl::linkComboChanged() { + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + if ( dev ) { + if ( linkCombo.get_active_row_number() == 0 ) { + // It is the "None" entry + DeviceManager::getManager().setLinkedTo(dev->getId(), ""); + } else { + Glib::ustring linkName = linkCombo.get_active_text(); + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( linkName == (*it)->getName() ) { + DeviceManager::getManager().setLinkedTo(dev->getId(), (*it)->getId()); + break; + } + } + } + } + } +} + +void InputDialogImpl::resyncToSelection() { + bool clear = true; + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> dev = row[getCols().device]; + + if ( dev ) { + axisTable.set_sensitive(true); + + linkConnection.block(); + linkCombo.remove_all(); + linkCombo.append(_("None")); + linkCombo.set_active(0); + if ( dev->getSource() != Gdk::SOURCE_MOUSE ) { + Glib::ustring linked = dev->getLink(); + std::list<Glib::RefPtr<InputDevice const> > devList = Inkscape::DeviceManager::getManager().getDevices(); + for ( std::list<Glib::RefPtr<InputDevice const> >::const_iterator it = devList.begin(); it != devList.end(); ++it ) { + if ( ((*it)->getSource() != Gdk::SOURCE_MOUSE) && ((*it) != dev) ) { + linkCombo.append((*it)->getName().c_str()); + if ( (linked.length() > 0) && (linked == (*it)->getId()) ) { + linkCombo.set_active_text((*it)->getName().c_str()); + } + } + } + linkCombo.set_sensitive(true); + } else { + linkCombo.set_sensitive(false); + } + linkConnection.unblock(); + + clear = false; + devName.set_label(row[getCols().description]); + axisFrame.set_label(row[getCols().description]); + setupValueAndCombo( dev->getNumAxes(), dev->getNumAxes(), devAxesCount, axesCombo); + setupValueAndCombo( dev->getNumKeys(), dev->getNumKeys(), devKeyCount, buttonCombo); + + + } + } + + axisTable.set_sensitive(!clear); + if (clear) { + axisFrame.set_label(""); + devName.set_label(""); + devAxesCount.set_label(""); + devKeyCount.set_label(""); + } +} + +void InputDialogImpl::ConfPanel::setAxis(gint count) +{ + /* + * TODO - Make each axis editable + */ + axisStore->clear(); + + static Glib::ustring axesLabels[6] = {_("X"), _("Y"), _("Pressure"), _("X tilt"), _("Y tilt"), _("Wheel")}; + + for ( gint barNum = 0; barNum < static_cast<gint>(G_N_ELEMENTS(axesLabels)); barNum++ ) { + + Gtk::TreeModel::Row row = *(axisStore->append()); + row[axisColumns.name] = axesLabels[barNum]; + if (barNum < count) { + row[axisColumns.value] = Glib::ustring::format(barNum+1); + } else { + row[axisColumns.value] = C_("Input device axe", "None"); + } + } + +} +void InputDialogImpl::ConfPanel::setKeys(gint count) +{ + /* + * TODO - Make each key assignable + */ + + keysStore->clear(); + + for (gint i = 0; i < count; i++) { + Gtk::TreeModel::Row row = *(keysStore->append()); + row[keysColumns.name] = Glib::ustring::format(i+1); + row[keysColumns.value] = _("Disabled"); + } + + +} +void InputDialogImpl::setupValueAndCombo( gint reported, gint actual, Gtk::Label& label, Gtk::ComboBoxText& combo ) +{ + gchar *tmp = g_strdup_printf("%d", reported); + label.set_label(tmp); + g_free(tmp); + + combo.remove_all(); + for ( gint i = 1; i <= reported; ++i ) { + tmp = g_strdup_printf("%d", i); + combo.append(tmp); + g_free(tmp); + } + + if ( (1 <= actual) && (actual <= reported) ) { + combo.set_active(actual - 1); + } +} + +void InputDialogImpl::updateTestButtons( Glib::ustring const& key, gint hotButton ) +{ + for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testButtons)); i++ ) { + if ( buttonMap[key].find(i) != buttonMap[key].end() ) { + if ( i == hotButton ) { + testButtons[i].set(getPix(PIX_BUTTONS_ON)); + } else { + testButtons[i].set(getPix(PIX_BUTTONS_OFF)); + } + } else { + testButtons[i].set(getPix(PIX_BUTTONS_NONE)); + } + } +} + +void InputDialogImpl::updateTestAxes( Glib::ustring const& key, GdkDevice* dev ) +{ + //static gdouble epsilon = 0.0001; + { + Glib::RefPtr<Gtk::TreeSelection> treeSel = deviceTree.get_selection(); + Gtk::TreeModel::iterator iter = treeSel->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring val = row[getCols().description]; + Glib::RefPtr<InputDevice const> idev = row[getCols().device]; + if ( !idev || (idev->getId() != key) ) { + dev = nullptr; + } + } + } + + for ( gint i = 0; i < static_cast<gint>(G_N_ELEMENTS(testAxes)); i++ ) { + if ( axesMap[key].find(i) != axesMap[key].end() ) { + switch ( axesMap[key][i].first ) { + case 0: + case 1: + testAxes[i].set(getPix(PIX_AXIS_NONE)); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + axesValues[i].set_sensitive(false); + } + break; + case 2: + testAxes[i].set(getPix(PIX_AXIS_OFF)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + break; + case 3: + testAxes[i].set(getPix(PIX_AXIS_ON)); + axesValues[i].set_sensitive(true); + if ( dev && (i < static_cast<gint>(G_N_ELEMENTS(axesValues)) ) ) { + + // FIXME: Device axis ranges are inaccessible in GTK+ 3 and + // are deprecated in GTK+ 2. Progress-bar ranges are disabled + // until we find an alternative solution + + // if ( (dev->axes[i].max - dev->axes[i].min) > epsilon ) { + axesValues[i].set_sensitive(true); + // axesValues[i].set_fraction( (axesMap[key][i].second- dev->axes[i].min) / (dev->axes[i].max - dev->axes[i].min) ); + // } + + gchar* str = g_strdup_printf("%f", axesMap[key][i].second); + axesValues[i].set_text(str); + g_free(str); + } + } + + } else { + testAxes[i].set(getPix(PIX_AXIS_NONE)); + } + } + if ( !dev ) { + for (auto & axesValue : axesValues) { + axesValue.set_fraction(0.0); + axesValue.set_text(""); + axesValue.set_sensitive(false); + } + } +} + +void InputDialogImpl::mapAxesValues( Glib::ustring const& key, gdouble const * axes, GdkDevice* dev ) +{ + guint numAxes = gdk_device_get_n_axes(dev); + + static gdouble epsilon = 0.0001; + if ( (numAxes > 0) && axes) { + for ( guint axisNum = 0; axisNum < numAxes; axisNum++ ) { + // 0 == new, 1 == set value, 2 == changed value, 3 == active + gdouble diff = axesMap[key][axisNum].second - axes[axisNum]; + switch(axesMap[key][axisNum].first) { + case 0: + { + axesMap[key][axisNum].first = 1; + axesMap[key][axisNum].second = axes[axisNum]; + } + break; + case 1: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { +// g_message("Axis %d changed on %s]", axisNum, key.c_str()); + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + DeviceManager::getManager().addAxis(key, axisNum); + } + } + break; + case 2: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].first = 3; + axesMap[key][axisNum].second = axes[axisNum]; + updateTestAxes(key, dev); + } + } + break; + case 3: + { + if ( (diff > epsilon) || (diff < -epsilon) ) { + axesMap[key][axisNum].second = axes[axisNum]; + } else { + axesMap[key][axisNum].first = 2; + updateTestAxes(key, dev); + } + } + } + } + } + // std::map<Glib::ustring, std::map<guint, std::pair<guint, gdouble> > > axesMap; +} + +Glib::ustring InputDialogImpl::getKeyFor( GdkDevice* device ) +{ + Glib::ustring key; + + GdkInputSource source = gdk_device_get_source(device); + const gchar *name = gdk_device_get_name(device); + + switch ( source ) { + case GDK_SOURCE_MOUSE: + key = "M:"; + break; + case GDK_SOURCE_CURSOR: + key = "C:"; + break; + case GDK_SOURCE_PEN: + key = "P:"; + break; + case GDK_SOURCE_ERASER: + key = "E:"; + break; + default: + key = "?:"; + } + key += name; + + return key; +} + +bool InputDialogImpl::eventSnoop(GdkEvent* event) +{ + int modmod = 0; + + GdkInputSource source = lastSourceSeen; + Glib::ustring devName = lastDevnameSeen; + Glib::ustring key; + gint hotButton = -1; + + switch ( event->type ) { + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + { + GdkEventKey* keyEvt = reinterpret_cast<GdkEventKey*>(event); + gchar* name = gtk_accelerator_name(keyEvt->keyval, static_cast<GdkModifierType>(keyEvt->state)); + keyVal.set_label(name); +// g_message("%d KEY state:0x%08x 0x%04x [%s]", keyEvt->type, keyEvt->state, keyEvt->keyval, name); + g_free(name); + } + break; + case GDK_BUTTON_PRESS: + modmod = 1; + // fallthrough + case GDK_BUTTON_RELEASE: + { + GdkEventButton* btnEvt = reinterpret_cast<GdkEventButton*>(event); + if ( btnEvt->device ) { + key = getKeyFor(btnEvt->device); + source = gdk_device_get_source(btnEvt->device); + devName = gdk_device_get_name(btnEvt->device); + mapAxesValues(key, btnEvt->axes, btnEvt->device); + + if ( buttonMap[key].find(btnEvt->button) == buttonMap[key].end() ) { +// g_message("New button found for %s = %d", key.c_str(), btnEvt->button); + buttonMap[key].insert(btnEvt->button); + DeviceManager::getManager().addButton(key, btnEvt->button); + } + hotButton = modmod ? btnEvt->button : -1; + updateTestButtons(key, hotButton); + } + gchar* name = gtk_accelerator_name(0, static_cast<GdkModifierType>(btnEvt->state)); + keyVal.set_label(name); +// g_message("%d BTN state:0x%08x %c %4d [%s] dev:%p [%s] ", +// btnEvt->type, btnEvt->state, +// (modmod ? '+':'-'), +// btnEvt->button, name, btnEvt->device, +// (btnEvt->device ? btnEvt->device->name : "null") + +// ); + g_free(name); + } + break; + case GDK_MOTION_NOTIFY: + { + GdkEventMotion* btnMtn = reinterpret_cast<GdkEventMotion*>(event); + if ( btnMtn->device ) { + key = getKeyFor(btnMtn->device); + source = gdk_device_get_source(btnMtn->device); + devName = gdk_device_get_name(btnMtn->device); + mapAxesValues(key, btnMtn->axes, btnMtn->device); + } + gchar* name = gtk_accelerator_name(0, static_cast<GdkModifierType>(btnMtn->state)); + keyVal.set_label(name); +// g_message("%d MOV state:0x%08x [%s] dev:%p [%s] %3.2f %3.2f %3.2f %3.2f %3.2f %3.2f", btnMtn->type, btnMtn->state, +// name, btnMtn->device, +// (btnMtn->device ? btnMtn->device->name : "null"), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 0)) ? btnMtn->axes[0]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 1)) ? btnMtn->axes[1]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 2)) ? btnMtn->axes[2]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 3)) ? btnMtn->axes[3]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 4)) ? btnMtn->axes[4]:0), +// ((btnMtn->device && btnMtn->axes && (btnMtn->device->num_axes > 5)) ? btnMtn->axes[5]:0) +// ); + g_free(name); + } + break; + default: + ;// nothing + } + + + if ( (lastSourceSeen != source) || (lastDevnameSeen != devName) ) { + switch (source) { + case GDK_SOURCE_MOUSE: { + testThumb.set(getPix(PIX_CORE)); + break; + } + case GDK_SOURCE_CURSOR: { +// g_message("flip to cursor"); + testThumb.set(getPix(PIX_MOUSE)); + break; + } + case GDK_SOURCE_PEN: { + if (devName == _("pad")) { +// g_message("flip to pad"); + testThumb.set(getPix(PIX_SIDEBUTTONS)); + } else { +// g_message("flip to pen"); + testThumb.set(getPix(PIX_TIP)); + } + break; + } + case GDK_SOURCE_ERASER: { +// g_message("flip to eraser"); + testThumb.set(getPix(PIX_ERASER)); + break; + } + /// \fixme GTK3 added new GDK_SOURCEs that should be handled here! + case GDK_SOURCE_KEYBOARD: + case GDK_SOURCE_TOUCHSCREEN: + case GDK_SOURCE_TOUCHPAD: + case GDK_SOURCE_TRACKPOINT: + case GDK_SOURCE_TABLET_PAD: + g_warning("InputDialogImpl::eventSnoop : unhandled GDK_SOURCE type!"); + break; + } + + updateTestButtons(key, hotButton); + lastSourceSeen = source; + lastDevnameSeen = devName; + } + + return false; +} + + +} // end namespace Inkscape +} // end namespace UI +} // end namespace Dialog + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/input.h b/src/ui/dialog/input.h new file mode 100644 index 0000000..a756cc5 --- /dev/null +++ b/src/ui/dialog/input.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Input devices dialog (new) + */ +/* Author: + * Jon A. Cruz + * + * Copyright (C) 2008 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_INPUT_H +#define INKSCAPE_UI_DIALOG_INPUT_H + + +#include "verbs.h" +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class InputDialog : public UI::Widget::Panel +{ +public: + static InputDialog &getInstance(); + + InputDialog() : UI::Widget::Panel("/dialogs/inputdevices", SP_VERB_DIALOG_INPUT) {} + ~InputDialog() override = default; +}; + +} // namespace Dialog +} // namesapce UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_INPUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/knot-properties.cpp b/src/ui/dialog/knot-properties.cpp new file mode 100644 index 0000000..708c90e --- /dev/null +++ b/src/ui/dialog/knot-properties.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/knot-properties.h" + +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include "inkscape.h" +#include "util/units.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +KnotPropertiesDialog::KnotPropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Close"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + _unit_name = ""; + // Layer name widgets + _knot_x_entry.set_activates_default(true); + _knot_x_entry.set_digits(4); + _knot_x_entry.set_increments(1,1); + _knot_x_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_x_entry.set_hexpand(); + _knot_x_label.set_label(_("Position X:")); + _knot_x_label.set_halign(Gtk::ALIGN_END); + _knot_x_label.set_valign(Gtk::ALIGN_CENTER); + + _knot_y_entry.set_activates_default(true); + _knot_y_entry.set_digits(4); + _knot_y_entry.set_increments(1,1); + _knot_y_entry.set_range(-G_MAXDOUBLE, G_MAXDOUBLE); + _knot_y_entry.set_hexpand(); + _knot_y_label.set_label(_("Position Y:")); + _knot_y_label.set_halign(Gtk::ALIGN_END); + _knot_y_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_knot_x_label, 0, 0, 1, 1); + _layout_table.attach(_knot_x_entry, 1, 0, 1, 1); + + _layout_table.attach(_knot_y_label, 0, 1, 1, 1); + _layout_table.attach(_knot_y_entry, 1, 1, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &KnotPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &KnotPropertiesDialog::_close)), + true + ) + ); + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_knot_y_entry); +} + +KnotPropertiesDialog::~KnotPropertiesDialog() { + + _setDesktop(nullptr); +} + +void KnotPropertiesDialog::showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name) +{ + KnotPropertiesDialog *dialog = new KnotPropertiesDialog(); + dialog->_setDesktop(desktop); + dialog->_setKnotPoint(pt->position(), unit_name); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Knot Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +KnotPropertiesDialog::_apply() +{ + double d_x = Inkscape::Util::Quantity::convert(_knot_x_entry.get_value(), _unit_name, "px"); + double d_y = Inkscape::Util::Quantity::convert(_knot_y_entry.get_value(), _unit_name, "px"); + _knotpoint->moveto(Geom::Point(d_x, d_y)); + _knotpoint->moved_signal.emit(_knotpoint, _knotpoint->position(), 0); + _close(); +} + +void +KnotPropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool KnotPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void KnotPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void KnotPropertiesDialog::_setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name) +{ + _unit_name = unit_name; + _knot_x_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.x(), "px", _unit_name)); + _knot_y_entry.set_value( Inkscape::Util::Quantity::convert(knotpoint.y(), "px", _unit_name)); + _knot_x_label.set_label(g_strdup_printf(_("Position X (%s):"), _unit_name.c_str())); + _knot_y_label.set_label(g_strdup_printf(_("Position Y (%s):"), _unit_name.c_str())); +} + +void KnotPropertiesDialog::_setPt(const SPKnot *pt) +{ + _knotpoint = const_cast<SPKnot *>(pt); +} + +void KnotPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/knot-properties.h b/src/ui/dialog/knot-properties.h new file mode 100644 index 0000000..fb88fad --- /dev/null +++ b/src/ui/dialog/knot-properties.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_KNOT_PROPERTIES_H +#define INKSCAPE_DIALOG_KNOT_PROPERTIES_H + +#include <gtkmm/dialog.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> +#include <2geom/point.h> +#include "knot.h" +#include "ui/tools/measure-tool.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + + +class KnotPropertiesDialog : public Gtk::Dialog { + public: + KnotPropertiesDialog(); + ~KnotPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, const SPKnot *pt, Glib::ustring const unit_name); + +protected: + + SPDesktop *_desktop; + SPKnot *_knotpoint; + + Gtk::Label _knot_x_label; + Gtk::SpinButton _knot_x_entry; + Gtk::Label _knot_y_label; + Gtk::SpinButton _knot_y_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + Glib::ustring _unit_name; + + sigc::connection _destroy_connection; + + static KnotPropertiesDialog &_instance() { + static KnotPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const SPKnot *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint, Glib::ustring const unit_name); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + friend class Inkscape::UI::Tools::MeasureTool; + +private: + KnotPropertiesDialog(KnotPropertiesDialog const &); // no copy + KnotPropertiesDialog &operator=(KnotPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/layer-properties.cpp b/src/ui/dialog/layer-properties.cpp new file mode 100644 index 0000000..083f104 --- /dev/null +++ b/src/ui/dialog/layer-properties.cpp @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layer-properties.h" +#include <glibmm/i18n.h> +#include <glibmm/main.h> + +#include "inkscape.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "verbs.h" +#include "selection-chemistry.h" +#include "ui/icon-names.h" +#include "ui/widget/imagetoggler.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +LayerPropertiesDialog::LayerPropertiesDialog() + : _strategy(nullptr), + _desktop(nullptr), + _layer(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + auto mainVBox = get_content_area(); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _layer_name_entry.set_activates_default(true); + _layer_name_label.set_label(_("Layer name:")); + _layer_name_label.set_halign(Gtk::ALIGN_START); + _layer_name_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_layer_name_label, 0, 0, 1, 1); + + _layer_name_entry.set_halign(Gtk::ALIGN_FILL); + _layer_name_entry.set_valign(Gtk::ALIGN_FILL); + _layer_name_entry.set_hexpand(); + _layout_table.attach(_layer_name_entry, 1, 0, 1, 1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &LayerPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &LayerPropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &LayerPropertiesDialog::_close)), + true + ) + ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); +} + +LayerPropertiesDialog::~LayerPropertiesDialog() { + + _setDesktop(nullptr); + _setLayer(nullptr); +} + +void LayerPropertiesDialog::_showDialog(LayerPropertiesDialog::Strategy &strategy, + SPDesktop *desktop, SPObject *layer) +{ + LayerPropertiesDialog *dialog = new LayerPropertiesDialog(); + + dialog->_strategy = &strategy; + dialog->_setDesktop(desktop); + dialog->_setLayer(layer); + + dialog->_strategy->setup(*dialog); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +LayerPropertiesDialog::_apply() +{ + g_assert(_strategy != nullptr); + + _strategy->perform(*this); + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_NONE, + _("Add layer")); + + _close(); +} + +void +LayerPropertiesDialog::_close() +{ + _setLayer(nullptr); + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +void +LayerPropertiesDialog::_setup_position_controls() { + if ( nullptr == _layer || _desktop->currentRoot() == _layer ) { + // no layers yet, so option above/below/sublayer is useless + return; + } + + _position_visible = true; + _dropdown_list = Gtk::ListStore::create(_dropdown_columns); + _layer_position_combo.set_model(_dropdown_list); + _layer_position_combo.pack_start(_label_renderer); + _layer_position_combo.set_cell_data_func(_label_renderer, + sigc::mem_fun(*this, &LayerPropertiesDialog::_prepareLabelRenderer)); + + Gtk::ListStore::iterator row; + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_ABOVE); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Above current"))); + _layer_position_combo.set_active(row); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_BELOW); + row->set_value(_dropdown_columns.name, Glib::ustring(_("Below current"))); + row = _dropdown_list->append(); + row->set_value(_dropdown_columns.position, LPOS_CHILD); + row->set_value(_dropdown_columns.name, Glib::ustring(_("As sublayer of current"))); + + _layer_position_label.set_label(_("Position:")); + _layer_position_label.set_halign(Gtk::ALIGN_START); + _layer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layer_position_combo.set_halign(Gtk::ALIGN_FILL); + _layer_position_combo.set_valign(Gtk::ALIGN_FILL); + _layer_position_combo.set_hexpand(); + _layout_table.attach(_layer_position_combo, 1, 1, 1, 1); + + _layout_table.attach(_layer_position_label, 0, 1, 1, 1); + + show_all_children(); +} + +void +LayerPropertiesDialog::_setup_layers_controls() { + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + _store = Gtk::TreeStore::create( *zoop ); + _tree.set_model( _store ); + _tree.set_headers_visible(false); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + } + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + } + + Gtk::CellRendererText *_text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + Gtk::TreeView::Column *_name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleKeyEvent), false ); + _tree.signal_button_press_event().connect_notify( sigc::mem_fun(*this, &LayerPropertiesDialog::_handleButtonEvent) ); + + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + _scroller.set_size_request(220, 180); + + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if ( root ) { + SPObject* target = _desktop->currentLayer(); + _store->clear(); + _addLayer( document, SP_OBJECT(root), nullptr, target, 0 ); + } + + _layout_table.remove(_layer_name_entry); + _layout_table.remove(_layer_name_label); + + _scroller.set_halign(Gtk::ALIGN_FILL); + _scroller.set_valign(Gtk::ALIGN_FILL); + _scroller.set_hexpand(); + _scroller.set_vexpand(); + _layout_table.attach(_scroller, 0, 1, 2, 1); + + show_all_children(); +} + +void LayerPropertiesDialog::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + int _maxNestDepth = 20; + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->id, child->label() ); +#endif // DUMP_LAYERS + + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = child; + row[_model->_colLabel] = child->label() ? child->label() : child->getId(); + row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; + + if ( target && child == target ) { + _tree.expand_to_path( _store->get_path(iter) ); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + select->select(iter); + + //_checkTreeSelection(); + } + + _addLayer( doc, child, &row, target, level + 1 ); + } + } + } +} + +SPObject* LayerPropertiesDialog::_selectedLayer() +{ + SPObject* obj = nullptr; + + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[_model->_colObject]; + } + + return obj; +} + +bool LayerPropertiesDialog::_handleKeyEvent(GdkEventKey *event) +{ + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _strategy->perform(*this); + _close(); + return true; + } + break; + } + return false; +} + +void LayerPropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _strategy->perform(*this); + _close(); + } +} + +/** Formats the label for a given layer row + */ +void LayerPropertiesDialog::_prepareLabelRenderer( + Gtk::TreeModel::const_iterator const &row +) { + Glib::ustring name=(*row)[_dropdown_columns.name]; + _label_renderer.property_markup() = name.c_str(); +} + +void LayerPropertiesDialog::Rename::setup(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + dialog.set_title(_("Rename Layer")); + gchar const *name = desktop->currentLayer()->label(); + dialog._layer_name_entry.set_text(( name ? name : _("Layer") )); + dialog._apply_button.set_label(_("_Rename")); +} + +void LayerPropertiesDialog::Rename::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + desktop->layer_manager->renameLayer( desktop->currentLayer(), + (gchar *)name.c_str(), + FALSE + ); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, + _("Rename layer")); + // TRANSLATORS: This means "The layer has been renamed" + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Renamed layer")); +} + +void LayerPropertiesDialog::Create::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Add Layer")); + + // Set the initial name to the "next available" layer name + LayerManager *mgr = dialog._desktop->layer_manager; + Glib::ustring newName = mgr->getNextLayerName(nullptr, dialog._desktop->currentLayer()->label()); + dialog._layer_name_entry.set_text(newName.c_str()); + dialog._apply_button.set_label(_("_Add")); + dialog._setup_position_controls(); +} + +void LayerPropertiesDialog::Create::perform(LayerPropertiesDialog &dialog) { + SPDesktop *desktop=dialog._desktop; + + LayerRelativePosition position = LPOS_ABOVE; + + if (dialog._position_visible) { + Gtk::ListStore::iterator activeRow(dialog._layer_position_combo.get_active()); + position = activeRow->get_value(dialog._dropdown_columns.position); + } + Glib::ustring name(dialog._layer_name_entry.get_text()); + if (name.empty()) + return; + + SPObject *new_layer=Inkscape::create_layer(desktop->currentRoot(), dialog._layer, position); + + if (!name.empty()) { + desktop->layer_manager->renameLayer( new_layer, (gchar *)name.c_str(), TRUE ); + } + desktop->getSelection()->clear(); + desktop->setCurrentLayer(new_layer); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("New layer created.")); +} + +void LayerPropertiesDialog::Move::setup(LayerPropertiesDialog &dialog) { + dialog.set_title(_("Move to Layer")); + //TODO: find an unused layer number, forming name from _("Layer ") + "%d" + dialog._layer_name_entry.set_text(_("Layer")); + dialog._apply_button.set_label(_("_Move")); + dialog._setup_layers_controls(); +} + +void LayerPropertiesDialog::Move::perform(LayerPropertiesDialog &dialog) { + + SPObject *moveto = dialog._selectedLayer(); + dialog._desktop->selection->toLayer(moveto); +} + +void LayerPropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +void LayerPropertiesDialog::_setLayer(SPObject *layer) { + if (layer) { + sp_object_ref(layer, nullptr); + } + if (_layer) { + sp_object_unref(_layer, nullptr); + } + _layer = layer; +} + +} // namespace +} // namespace +} // namespace + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/layer-properties.h b/src/ui/dialog/layer-properties.h new file mode 100644 index 0000000..7cdd130 --- /dev/null +++ b/src/ui/dialog/layer-properties.h @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LAYER_PROPERTIES_H +#define INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> + +#include "layer-fns.h" +#include "ui/widget/layer-selector.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class LayerPropertiesDialog : public Gtk::Dialog { + public: + LayerPropertiesDialog(); + ~LayerPropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showRename(SPDesktop *desktop, SPObject *layer) { + _showDialog(Rename::instance(), desktop, layer); + } + static void showCreate(SPDesktop *desktop, SPObject *layer) { + _showDialog(Create::instance(), desktop, layer); + } + static void showMove(SPDesktop *desktop, SPObject *layer) { + _showDialog(Move::instance(), desktop, layer); + } + +protected: + struct Strategy { + virtual ~Strategy() = default; + virtual void setup(LayerPropertiesDialog &)=0; + virtual void perform(LayerPropertiesDialog &)=0; + }; + struct Rename : public Strategy { + static Rename &instance() { static Rename instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Create : public Strategy { + static Create &instance() { static Create instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + struct Move : public Strategy { + static Move &instance() { static Move instance; return instance; } + void setup(LayerPropertiesDialog &dialog) override; + void perform(LayerPropertiesDialog &dialog) override; + }; + + friend struct Rename; + friend struct Create; + friend struct Move; + + Strategy *_strategy; + SPDesktop *_desktop; + SPObject *_layer; + + class PositionDropdownColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<LayerRelativePosition> position; + Gtk::TreeModelColumn<Glib::ustring> name; + + PositionDropdownColumns() { + add(position); add(name); + } + }; + + Gtk::Label _layer_name_label; + Gtk::Entry _layer_name_entry; + Gtk::Label _layer_position_label; + Gtk::ComboBox _layer_position_combo; + Gtk::Grid _layout_table; + + bool _position_visible; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPObject*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colVisible; + Gtk::TreeModelColumn<bool> _colLocked; + }; + + Gtk::TreeView _tree; + ModelColumns* _model; + Glib::RefPtr<Gtk::TreeStore> _store; + Gtk::ScrolledWindow _scroller; + + + PositionDropdownColumns _dropdown_columns; + Gtk::CellRendererText _label_renderer; + Glib::RefPtr<Gtk::ListStore> _dropdown_list; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static LayerPropertiesDialog &_instance() { + static LayerPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setLayer(SPObject *layer); + + static void _showDialog(Strategy &strategy, SPDesktop *desktop, SPObject *layer); + void _apply(); + void _close(); + + void _setup_position_controls(); + void _setup_layers_controls(); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + void _addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + SPObject* _selectedLayer(); + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + +private: + LayerPropertiesDialog(LayerPropertiesDialog const &) = delete; // no copy + LayerPropertiesDialog &operator=(LayerPropertiesDialog const &) = delete; // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/layers.cpp b/src/ui/dialog/layers.cpp new file mode 100644 index 0000000..a268426 --- /dev/null +++ b/src/ui/dialog/layers.cpp @@ -0,0 +1,1004 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for layers + * + * Authors: + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "layers.h" + +#include <gtkmm/icontheme.h> +#include <gtkmm/separatormenuitem.h> +#include <glibmm/main.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-root.h" + +#include "svg/css-ostringstream.h" + +#include "ui/contextmenu.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/imagetoggler.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +LayersPanel& LayersPanel::getInstance() +{ + return *new LayersPanel(); +} + +enum { + COL_VISIBLE = 1, + COL_LOCKED +}; + +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + DRAGNDROP +}; + +class LayersPanel::InternalUIBounce +{ +public: + int _actionCode; + SPObject* _target; +}; + +void LayersPanel::_styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ) +{ + bool set = false; + + if ( iconName ) { + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + btn.set_relief(Gtk::RELIEF_NONE); + set = true; + } + + if ( desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + if ( !set && action && action->image ) { + GtkWidget *child = sp_get_icon_image(action->image, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + set = true; + } + + if ( action && action->tip ) { + btn.set_tooltip_text (action->tip); + } + } + } + + if ( !set && fallback ) { + btn.set_label( fallback ); + } +} + + +Gtk::MenuItem& LayersPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &LayersPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +void LayersPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(_desktop)); + if ( action ) { + sp_action_perform( action, nullptr ); +// } else { +// g_message("no action"); + } +// } else { +// g_message("no verb for %u", code); + } +// } else { +// g_message("no active desktop"); + } +} + +// SP_VERB_LAYER_NEXT, +// SP_VERB_LAYER_PREV, +void LayersPanel::_takeAction( int val ) +{ + if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_target = _selectedLayer(); + Glib::signal_timeout().connect( sigc::mem_fun(*this, &LayersPanel::_executeAction), 0 ); + } +} + +bool LayersPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _pending + && ( + (_pending->_actionCode == BUTTON_NEW || _pending->_actionCode == DRAGNDROP) + || !( (_desktop && _desktop->currentLayer()) + && (_desktop->currentLayer() != _pending->_target) + ) + ) + ) { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + break; + case BUTTON_BOTTOM: + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + break; + case BUTTON_UP: + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + break; + case BUTTON_DOWN: + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + break; + case BUTTON_DUPLICATE: + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + break; + case BUTTON_DELETE: + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +class LayersPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPObject*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colVisible; + Gtk::TreeModelColumn<bool> _colLocked; +}; + +void LayersPanel::_updateLayer( SPObject *layer ) { + _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &LayersPanel::_checkForUpdated), layer) ); +} + +bool LayersPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + /* + * We get notified of layer update here (from layer->setLabel()) before layer->label() is set + * with the correct value (sp-object bug?). So use the inkscape:label attribute instead which + * has the correct value (bug #168351) + */ + //row[_model->_colLabel] = layer->label() ? layer->label() : layer->defaultLabel(); + gchar const *label = layer->getAttribute("inkscape:label"); + row[_model->_colLabel] = label ? label : layer->defaultLabel(); + row[_model->_colVisible] = SP_IS_ITEM(layer) ? !SP_ITEM(layer)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false; + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_selectLayer( SPObject *layer ) { + if ( !layer || (_desktop && _desktop->doc() && (layer == _desktop->doc()->getRoot())) ) { + if ( _tree.get_selection()->count_selected_rows() != 0 ) { + _tree.get_selection()->unselect_all(); + } + } else { + _store->foreach( sigc::bind<SPObject*>(sigc::mem_fun(*this, &LayersPanel::_checkForSelected), layer) ); + } + + _checkTreeSelection(); +} + +bool LayersPanel::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer) +{ + bool stopGoing = false; + + Gtk::TreeModel::Row row = *iter; + if ( layer == row[_model->_colObject] ) + { + _tree.expand_to_path( path ); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + + select->select(iter); + + stopGoing = true; + } + + return stopGoing; +} + +void LayersPanel::_layersChanged() +{ +// g_message("_layersChanged()"); + if (_desktop) { + SPDocument* document = _desktop->doc(); + g_return_if_fail(document != nullptr); // bug #158: Crash on File>Quit + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); + if ( _desktop->layer_manager && _desktop->layer_manager->includes( root ) ) { + SPObject* target = _desktop->currentLayer(); + _store->clear(); + + #if DUMP_LAYERS + g_message("root:%p {%s} [%s]", root, root->id, root->label() ); + #endif // DUMP_LAYERS + _addLayer( document, root, nullptr, target, 0 ); + } + _selectedConnection.unblock(); + } + } +} + +void LayersPanel::_addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ) +{ + if ( _desktop && _desktop->layer_manager && layer && (level < _maxNestDepth) ) { + unsigned int counter = _desktop->layer_manager->childCount(layer); + for ( unsigned int i = 0; i < counter; i++ ) { + SPObject *child = _desktop->layer_manager->nthChildOf(layer, i); + if ( child ) { +#if DUMP_LAYERS + g_message(" %3d layer:%p {%s} [%s]", level, child, child->getId(), child->label() ); +#endif // DUMP_LAYERS + + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = child; + row[_model->_colLabel] = child->defaultLabel(); + row[_model->_colVisible] = SP_IS_ITEM(child) ? !SP_ITEM(child)->isHidden() : false; + row[_model->_colLocked] = SP_IS_ITEM(child) ? SP_ITEM(child)->isLocked() : false; + + if ( target && child == target ) { + _tree.expand_to_path( _store->get_path(iter) ); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + select->select(iter); + + _checkTreeSelection(); + } + + _addLayer( doc, child, &row, target, level + 1 ); + } + } + } +} + +SPObject* LayersPanel::_selectedLayer() +{ + SPObject* obj = nullptr; + + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[_model->_colObject]; + } + + return obj; +} + +void LayersPanel::_pushTreeSelectionToCurrent() +{ + // TODO hunt down the possible API abuse in getting NULL + if ( _desktop && _desktop->layer_manager && _desktop->currentRoot() ) { + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + SPObject* curr = _desktop->currentLayer(); + if ( curr != inTree ) { + _desktop->layer_manager->setCurrentLayer( inTree ); + } + } else { + _desktop->layer_manager->setCurrentLayer( _desktop->doc()->getRoot() ); + } + } +} + +void LayersPanel::_checkTreeSelection() +{ + bool sensitive = false; + bool sensitiveNonTop = false; + bool sensitiveNonBottom = false; + if ( _tree.get_selection()->count_selected_rows() > 0 ) { + sensitive = true; + + SPObject* inTree = _selectedLayer(); + if ( inTree ) { + + sensitiveNonTop = (Inkscape::next_layer(inTree->parent, inTree) != nullptr); + sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != nullptr); + + } + } + + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } +} + +void LayersPanel::_preToggle( GdkEvent const *event ) +{ + + if ( _toggleEvent ) { + gdk_event_free(_toggleEvent); + _toggleEvent = nullptr; + } + + if ( event && (event->type == GDK_BUTTON_PRESS) ) { + // Make a copy so we can keep it around. + _toggleEvent = gdk_event_copy(const_cast<GdkEvent*>(event)); + } +} + +void LayersPanel::_toggled( Glib::ustring const& str, int targetCol ) +{ + g_return_if_fail(_desktop != nullptr); + + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + Glib::ustring tmp = row[_model->_colLabel]; + + SPObject* obj = row[_model->_colObject]; + SPItem* item = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + if ( item ) { + switch ( targetCol ) { + case COL_VISIBLE: + { + bool newValue = !row[_model->_colVisible]; + row[_model->_colVisible] = newValue; + item->setHidden( !newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Unhide layer") : _("Hide layer")); + } + break; + + case COL_LOCKED: + { + bool newValue = !row[_model->_colLocked]; + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_LAYERS, + newValue? _("Lock layer") : _("Unlock layer")); + } + break; + } + } + Inkscape::SelectionHelper::fixSelection(_desktop); +} + +bool LayersPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + // TODO - fix to a better is-popup function + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + + _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } + } + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1) + && (event->state & GDK_MOD1_MASK)) { + // Alt left click on the visible/lock columns - eat this event to keep row selection + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1) || + col == _tree.get_column(COL_LOCKED-1)) { + return true; + } + } + } + + // TODO - ImageToggler doesn't seem to handle Shift/Alt clicks - so we deal with them here. + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1) + && (event->state & (GDK_SHIFT_MASK | GDK_MOD1_MASK))) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( obj ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } + } + } + + + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + return false; +} + +/* + * Drap and drop within the tree + * Save the drag source and drop target SPObjects and if its a drag between layers or into (sublayer) a layer + */ +bool LayersPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/) +{ + int cell_x = 0, cell_y = 0; + Gtk::TreeModel::Path target_path; + Gtk::TreeView::Column *target_column; + SPObject *selected = _selectedLayer(); + + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source = ( selected && SP_IS_ITEM(selected) ) ? SP_ITEM(selected) : nullptr; + + if (_tree.get_path_at_pos (x, y, target_path, target_column, cell_x, cell_y)) { + // Are we before, inside or after the drop layer + Gdk::Rectangle rect; + _tree.get_background_area (target_path, *target_column, rect); + int cell_height = rect.get_height(); + _dnd_into = (cell_y > (int)(cell_height * 1/3) && cell_y <= (int)(cell_height * 2/3)); + if (cell_y > (int)(cell_height * 2/3)) { + Gtk::TreeModel::Path next_path = target_path; + next_path.next(); + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into parent + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level + _dnd_target = nullptr; + } + } + } + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + _dnd_target = ( obj && SP_IS_ITEM(obj) ) ? SP_ITEM(obj) : nullptr; + } + } + + _takeAction(DRAGNDROP); + + return false; +} + +/* + * Move a layer in response to a drag & drop action + */ +void LayersPanel::_doTreeMove( ) +{ + if (_dnd_source && _dnd_source->getRepr() ) { + if(!_dnd_target){ + _dnd_source->doWriteTransform(_dnd_source->i2doc_affine() * _dnd_source->document->getRoot()->i2doc_affine().inverse()); + }else{ + SPItem* parent = _dnd_into ? _dnd_target : dynamic_cast<SPItem*>(_dnd_target->parent); + if(parent){ + Geom::Affine move = _dnd_source->i2doc_affine() * parent->i2doc_affine().inverse(); + _dnd_source->doWriteTransform(move); + } + } + _dnd_source->moveTo(_dnd_target, _dnd_into); + _selectLayer(_dnd_source); + _dnd_source = nullptr; + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Move layer")); + } +} + + +void LayersPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameLayer(row, new_text); +} + +void LayersPanel::_renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop && _desktop->layer_manager) { + SPObject* obj = row[_model->_colObject]; + if ( obj ) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + _desktop->layer_manager->renameLayer( obj, name.c_str(), FALSE ); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename layer")); + } + + } + } +} + +bool LayersPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Constructor + */ +LayersPanel::LayersPanel() : + UI::Widget::Panel("/dialogs/layers", SP_VERB_DIALOG_LAYERS), + deskTrack(), + _maxNestDepth(20), + _desktop(nullptr), + _model(nullptr), + _pending(nullptr), + _toggleEvent(nullptr), + _compositeSettings(SP_VERB_DIALOG_LAYERS, "layers", + UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::OPACITY | + UI::Widget::SimpleFilterModifier::BLUR), + desktopChangeConn() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _maxNestDepth = prefs->getIntLimited("/dialogs/layers/maxDepth", 20, 1, 1000); + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _tree.set_model( _store ); + _tree.set_headers_visible(false); + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + eyeRenderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_VISIBLE) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + } + + + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->signal_pre_toggle().connect( sigc::mem_fun(*this, &LayersPanel::_preToggle) ); + renderer->signal_toggled().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_toggled), (int)COL_LOCKED) ); + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + } + + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(nameColNum + 1); + + _compositeSettings.setSubject(&_subject); + + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &LayersPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &LayersPanel::_rowSelectFunction) ); + + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &LayersPanel::_handleDragDrop), false); + + _text_renderer->property_editable() = true; + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &LayersPanel::_handleEdited) ); + + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &LayersPanel::_handleButtonEvent), false ); + + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _watching.push_back( &_compositeSettings ); + + _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + _layersPage.pack_end(_compositeSettings, Gtk::PACK_SHRINK); + _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_NEW, INKSCAPE_ICON("list-add"), C_("Layers", "New") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_BOTTOM, INKSCAPE_ICON("go-bottom"), C_("Layers", "Bot") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_LOWER, INKSCAPE_ICON("go-down"), C_("Layers", "Dn") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_RAISE, INKSCAPE_ICON("go-up"), C_("Layers", "Up") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_TO_TOP, INKSCAPE_ICON("go-top"), C_("Layers", "Top") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + +// btn = Gtk::manage( new Gtk::Button("Dup") ); +// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) ); +// _buttonsRow.add( *btn ); + + btn = Gtk::manage( new Gtk::Button() ); + _styleButton( *btn, targetDesktop, SP_VERB_LAYER_DELETE, INKSCAPE_ICON("list-remove"), _("X") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + + + // ------------------------------------------------------- + { + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_layers", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RAISE, (int)BUTTON_UP ) ); + _watchingNonBottom.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOWER, (int)BUTTON_DOWN ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast<ContextMenu*>(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + + + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + setDesktop( targetDesktop ); + + show_all_children(); + + // restorePanelPrefs(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LayersPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +LayersPanel::~LayersPanel() +{ + setDesktop(nullptr); + + _compositeSettings.setSubject(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + + +void LayersPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _layerChangedConnection.disconnect(); + _layerUpdatedConnection.disconnect(); + _changedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //setLabel( _desktop->doc()->name ); + + LayerManager *mgr = _desktop->layer_manager; + if ( mgr ) { + _layerChangedConnection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayersPanel::_selectLayer) ); + _layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayersPanel::_updateLayer) ); + _changedConnection = mgr->connectChanged( sigc::mem_fun(*this, &LayersPanel::_layersChanged) ); + } + + _layersChanged(); + } + } + deskTrack.setBase(desktop); +} + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/layers.h b/src/ui/dialog/layers.h new file mode 100644 index 0000000..8a66eef --- /dev/null +++ b/src/ui/dialog/layers.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for layer UI. + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2006,2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LAYERS_PANEL_H +#define SEEN_LAYERS_PANEL_H + +#include <gtkmm/box.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "ui/widget/object-composite-settings.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" + +class SPObject; + +namespace Inkscape { + +class LayerManager; + +namespace UI { +namespace Dialog { + + +/** + * A panel that displays layers. + */ +class LayersPanel : public UI::Widget::Panel +{ +public: + LayersPanel(); + ~LayersPanel() override; + + static LayersPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + +private: + class ModelColumns; + class InternalUIBounce; + + LayersPanel(LayersPanel const &) = delete; // no copy + LayersPanel &operator=(LayersPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback ); + void _fireAction( unsigned int code ); + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _preToggle( GdkEvent const *event ); + void _toggled( Glib::ustring const& str, int targetCol ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameLayer(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + void _checkTreeSelection(); + + void _takeAction( int val ); + bool _executeAction(); + + bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _updateLayer(SPObject *layer); + bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* layer); + + void _selectLayer(SPObject *layer); + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer); + + void _layersChanged(); + void _addLayer( SPDocument* doc, SPObject* layer, Gtk::TreeModel::Row* parentRow, SPObject* target, int level ); + + SPObject* _selectedLayer(); + + // Hooked to the layer manager: + sigc::connection _layerChangedConnection; + sigc::connection _layerUpdatedConnection; + sigc::connection _changedConnection; + sigc::connection _addedConnection; + sigc::connection _removedConnection; + + // Internal + sigc::connection _selectedConnection; + + DesktopTracker deskTrack; + int _maxNestDepth; + SPDesktop* _desktop; + ModelColumns* _model; + InternalUIBounce* _pending; + gboolean _dnd_into; + SPItem* _dnd_source; + SPItem* _dnd_target; + GdkEvent* _toggleEvent; + bool _show_contextmenu_icons; + + Glib::RefPtr<Gtk::TreeStore> _store; + std::vector<Gtk::Widget*> _watching; + std::vector<Gtk::Widget*> _watchingNonTop; + std::vector<Gtk::Widget*> _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _layersPage; + + UI::Widget::StyleSubject::CurrentLayer _subject; + UI::Widget::ObjectCompositeSettings _compositeSettings; + sigc::connection desktopChangeConn; +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_LAYERS_PANEL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-add.cpp b/src/ui/dialog/livepatheffect-add.cpp new file mode 100644 index 0000000..8dbe30a --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.cpp @@ -0,0 +1,933 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-add.h" +#include "desktop.h" +#include "dialog.h" +#include "io/resource.h" +#include "live_effects/effect.h" +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "preferences.h" +#include <cmath> +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +bool sp_has_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + return true; + } + return false; +} + +void sp_add_fav(Glib::ustring effect) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + if (!sp_has_fav(effect)) { + prefs->setString("/dialogs/livepatheffect/favs", favlist + effect + ";"); + } +} + +void sp_remove_fav(Glib::ustring effect) +{ + if (sp_has_fav(effect)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring favlist = prefs->getString("/dialogs/livepatheffect/favs"); + effect += ";"; + size_t pos = favlist.find(effect); + if (pos != std::string::npos) { + favlist.erase(pos, effect.length()); + prefs->setString("/dialogs/livepatheffect/favs", favlist); + } + } +} + +bool LivePathEffectAdd::mouseover(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2); + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, cursor); + g_object_unref(cursor); + return true; +} + +bool LivePathEffectAdd::mouseout(GdkEventCrossing *evt, GtkWidget *wdg) +{ + GdkWindow *window = gtk_widget_get_window(wdg); + gdk_window_set_cursor(window, nullptr); + hide_pop_description(evt); + return true; +} + +LivePathEffectAdd::LivePathEffectAdd() + : converter(Inkscape::LivePathEffect::LPETypeConverter) + , _applied(false) + , _showfavs(false) +{ + Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-add.glade"); + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + _builder->get_widget("LPEDialogSelector", _LPEDialogSelector); + _builder->get_widget("LPESelectorFlowBox", _LPESelectorFlowBox); + _builder->get_widget("LPESelectorEffectInfoPop", _LPESelectorEffectInfoPop); + _builder->get_widget("LPEFilter", _LPEFilter); + _builder->get_widget("LPEInfo", _LPEInfo); + _builder->get_widget("LPEExperimental", _LPEExperimental); + _builder->get_widget("LPEScrolled", _LPEScrolled); + _builder->get_widget("LPESelectorEffectEventFavShow", _LPESelectorEffectEventFavShow); + _builder->get_widget("LPESelectorEffectInfoEventBox", _LPESelectorEffectInfoEventBox); + _builder->get_widget("LPESelectorEffectRadioPackLess", _LPESelectorEffectRadioPackLess); + _builder->get_widget("LPESelectorEffectRadioPackMore", _LPESelectorEffectRadioPackMore); + _builder->get_widget("LPESelectorEffectRadioList", _LPESelectorEffectRadioList); + + _LPEFilter->signal_search_changed().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_search)); + _LPEDialogSelector->add_events(Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK | Gdk::KEY_PRESS_MASK); + _LPESelectorFlowBox->signal_set_focus_child().connect(sigc::mem_fun(*this, &LivePathEffectAdd::on_focus)); + + gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-livepatheffect-effect.glade"); + for (int i = 0; i < static_cast<int>(converter._length); ++i) { + Glib::RefPtr<Gtk::Builder> builder_effect; + try { + builder_effect = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(i); + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + LPESelectorEffect->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + Gtk::EventBox *LPESelectorEffectEventExpander; + builder_effect->get_widget("LPESelectorEffectEventExpander", LPESelectorEffectEventExpander); + LPESelectorEffectEventExpander->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>>(sigc::mem_fun(*this, &LivePathEffectAdd::expand), builder_effect)); + LPESelectorEffectEventExpander->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffectEventExpander->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + const Glib::ustring label = _(converter.get_label(data->id).c_str()); + const Glib::ustring untranslated_label = converter.get_untranslated_label(data->id); + if (untranslated_label == label) { + LPEName->set_text(label); + } else { + LPEName->set_markup((label + "\n<span size='x-small'>" + untranslated_label + "</span>").c_str()); + } + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + const Glib::ustring description = _(converter.get_description(data->id).c_str()); + LPEDescription->set_text(description); + Gtk::ToggleButton *LPEExperimentalToggle; + builder_effect->get_widget("LPEExperimentalToggle", LPEExperimentalToggle); + bool active = converter.get_experimental(data->id) ? true : false; + LPEExperimentalToggle->set_active(active); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + LPEIcon->set_from_icon_name(converter.get_icon(data->id), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + Gtk::EventBox *LPESelectorEffectEventInfo; + builder_effect->get_widget("LPESelectorEffectEventInfo", LPESelectorEffectEventInfo); + LPESelectorEffectEventInfo->signal_enter_notify_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::pop_description), builder_effect)); + Gtk::EventBox *LPESelectorEffectEventFav; + builder_effect->get_widget("LPESelectorEffectEventFav", LPESelectorEffectEventFav); + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFav->get_child()); + if (sp_has_fav(LPEName->get_text())) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + LPESelectorEffectEventFav->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + LPESelectorEffectEventFavTop->signal_button_press_event().connect(sigc::bind<Glib::RefPtr<Gtk::Builder>>( + sigc::mem_fun(*this, &LivePathEffectAdd::fav_toggler), builder_effect)); + Gtk::Image *favtop = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(LPEName->get_text())) { + favtop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favtop->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + Gtk::EventBox *LPESelectorEffectEventApply; + builder_effect->get_widget("LPESelectorEffectEventApply", LPESelectorEffectEventApply); + LPESelectorEffectEventApply->signal_button_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::apply), builder_effect, &converter.data(i))); + LPESelectorEffectEventApply->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + LPESelectorEffectEventApply->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffectEventApply->gobj()))); + Gtk::ButtonBox *LPESelectorButtonBox; + builder_effect->get_widget("LPESelectorButtonBox", LPESelectorButtonBox); + LPESelectorButtonBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorButtonBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(LPESelectorEffect->gobj()))); + LPESelectorEffect->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(LPESelectorEffect->gobj()))); + _LPESelectorFlowBox->insert(*LPESelectorEffect, i); + LPESelectorEffect->get_parent()->signal_key_press_event().connect( + sigc::bind<Glib::RefPtr<Gtk::Builder>, const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *>( + sigc::mem_fun(*this, &LivePathEffectAdd::on_press_enter), builder_effect, &converter.data(i))); + LPESelectorEffect->get_parent()->get_style_context()->add_class( + ("LPEIndex" + Glib::ustring::format(i)).c_str()); + } + _LPESelectorFlowBox->set_activate_on_single_click(false); + _visiblelpe = _LPESelectorFlowBox->get_children().size(); + _LPEInfo->set_visible(false); + _LPESelectorEffectRadioPackLess->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 0)); + _LPESelectorEffectRadioPackMore->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 1)); + _LPESelectorEffectRadioList->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &LivePathEffectAdd::viewChanged), 2)); + _LPESelectorEffectEventFavShow->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectEventFavShow->gobj()))); + _LPESelectorEffectEventFavShow->signal_button_press_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::show_fav_toggler)); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::hide_pop_description)); + _LPESelectorEffectInfoEventBox->signal_enter_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseover), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPESelectorEffectInfoEventBox->signal_leave_notify_event().connect(sigc::bind<GtkWidget *>( + sigc::mem_fun(*this, &LivePathEffectAdd::mouseout), GTK_WIDGET(_LPESelectorEffectInfoEventBox->gobj()))); + _LPEExperimental->property_active().signal_changed().connect( + sigc::mem_fun(*this, &LivePathEffectAdd::reload_effect_list)); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + int width; + int height; + window->get_size(width, height); + _LPEDialogSelector->resize(std::min(width - 300, 1440), std::min(height - 300, 900)); + _LPEDialogSelector->set_transient_for(*window); + _LPESelectorFlowBox->set_focus_vadjustment(_LPEScrolled->get_vadjustment()); + _LPEDialogSelector->show_all_children(); + _lasteffect = nullptr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + switch (mode) { + case 0: + _LPESelectorEffectRadioPackLess->set_active(); + viewChanged(0); + break; + case 1: + _LPESelectorEffectRadioPackMore->set_active(); + viewChanged(1); + break; + default: + _LPESelectorEffectRadioList->set_active(); + viewChanged(2); + } + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(_LPEDialogSelector); + INKSCAPE.signal_change_theme.connect(sigc::bind(sigc::ptr_fun(sp_add_top_window_classes), widg)); + sp_add_top_window_classes(widg); +} +const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *LivePathEffectAdd::getActiveData() +{ + return instance()._to_add; +} + +void LivePathEffectAdd::viewChanged(gint mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool changed = false; + if (mode == 2 && !_LPEDialogSelector->get_style_context()->has_class("LPEList")) { + _LPEDialogSelector->get_style_context()->add_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(1); + changed = true; + } else if (mode == 1 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackMore")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } else if (mode == 0 && !_LPEDialogSelector->get_style_context()->has_class("LPEPackLess")) { + _LPEDialogSelector->get_style_context()->remove_class("LPEList"); + _LPEDialogSelector->get_style_context()->add_class("LPEPackLess"); + _LPEDialogSelector->get_style_context()->remove_class("LPEPackMore"); + _LPESelectorFlowBox->set_max_children_per_line(30); + changed = true; + } + prefs->setInt("/dialogs/livepatheffect/dialogmode", mode); + if (changed) { + _LPESelectorFlowBox->unset_sort_func(); + _LPESelectorFlowBox->set_sort_func(sigc::mem_fun(this, &LivePathEffectAdd::on_sort)); + std::vector<Gtk::FlowBoxChild *> selected = _LPESelectorFlowBox->get_selected_children(); + if (selected.size() == 1) { + _LPESelectorFlowBox->get_selected_children()[0]->grab_focus(); + } + } +} + +void LivePathEffectAdd::on_focus(Gtk::Widget *widget) +{ + Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(widget); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (child && mode != 2) { + for (auto i : _LPESelectorFlowBox->get_children()) { + Gtk::FlowBoxChild *leitem = dynamic_cast<Gtk::FlowBoxChild *>(i); + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(leitem->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Box *actions = dynamic_cast<Gtk::Box *>(contents[5]); + if (actions) { + actions->set_visible(false); + } + Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (expander) { + expander->set_visible(true); + } + } + } + } + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::EventBox *expander = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (expander) { + expander->set_visible(false); + } + } + } + + child->show_all_children(); + _LPESelectorFlowBox->select_child(*child); + } +} + +bool LivePathEffectAdd::pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::Image *LPESelectorEffectInfo; + builder_effect->get_widget("LPESelectorEffectInfo", LPESelectorEffectInfo); + _LPESelectorEffectInfoPop->set_relative_to(*LPESelectorEffectInfo); + + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Label *LPEDescription; + builder_effect->get_widget("LPEDescription", LPEDescription); + Gtk::Image *LPEIcon; + builder_effect->get_widget("LPEIcon", LPEIcon); + + Gtk::Image *LPESelectorEffectInfoIcon; + _builder->get_widget("LPESelectorEffectInfoIcon", LPESelectorEffectInfoIcon); + LPESelectorEffectInfoIcon->set_from_icon_name(LPEIcon->get_icon_name(), Gtk::IconSize(Gtk::ICON_SIZE_DIALOG)); + + Gtk::Label *LPESelectorEffectInfoName; + _builder->get_widget("LPESelectorEffectInfoName", LPESelectorEffectInfoName); + LPESelectorEffectInfoName->set_text(LPEName->get_text()); + + Gtk::Label *LPESelectorEffectInfoDescription; + _builder->get_widget("LPESelectorEffectInfoDescription", LPESelectorEffectInfoDescription); + LPESelectorEffectInfoDescription->set_text(LPEDescription->get_text()); + + _LPESelectorEffectInfoPop->show(); + + return true; +} + +bool LivePathEffectAdd::hide_pop_description(GdkEventCrossing *evt) +{ + _LPESelectorEffectInfoPop->hide(); + return true; +} + +bool LivePathEffectAdd::fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::Label *LPEName; + builder_effect->get_widget("LPEName", LPEName); + Gtk::Image *LPESelectorEffectFav; + builder_effect->get_widget("LPESelectorEffectFav", LPESelectorEffectFav); + Gtk::Image *LPESelectorEffectFavTop; + builder_effect->get_widget("LPESelectorEffectFavTop", LPESelectorEffectFavTop); + Gtk::EventBox *LPESelectorEffectEventFavTop; + builder_effect->get_widget("LPESelectorEffectEventFavTop", LPESelectorEffectEventFavTop); + if (LPESelectorEffectFav && LPESelectorEffectEventFavTop) { + if (sp_has_fav(LPEName->get_text())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + } else { + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + } + LPESelectorEffectFavTop->set_from_icon_name("draw-star-outline", + Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_remove_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + if (_showfavs) { + reload_effect_list(); + } + } else { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectFavTop->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectFav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + sp_add_fav(LPEName->get_text()); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpefav"); + LPESelectorEffect->get_parent()->get_style_context()->remove_class("lpenormal"); + LPESelectorEffect->get_parent()->get_style_context()->add_class("lpe"); + } + } + return true; +} + +bool LivePathEffectAdd::show_fav_toggler(GdkEventButton *evt) +{ + _showfavs = !_showfavs; + Gtk::Image *favimage = dynamic_cast<Gtk::Image *>(_LPESelectorEffectEventFavShow->get_child()); + if (favimage) { + if (_showfavs) { + favimage->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } else { + favimage->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + } + } + reload_effect_list(); + return true; +} + +bool LivePathEffectAdd::apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add) +{ + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + _LPESelectorFlowBox->select_child(*flowboxchild); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; +} + +bool LivePathEffectAdd::on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add) +{ + if (key->keyval == 65293 || key->keyval == 65421) { + _to_add = to_add; + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *flowboxchild = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + if (flowboxchild && flowboxchild->get_style_context()->has_class("lpedisabled")) { + return true; + } + _applied = true; + _lasteffect = flowboxchild; + _LPEDialogSelector->response(Gtk::RESPONSE_APPLY); + _LPEDialogSelector->hide(); + return true; + } + return false; +} + +bool LivePathEffectAdd::expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect) +{ + Gtk::EventBox *LPESelectorEffect; + builder_effect->get_widget("LPESelectorEffect", LPESelectorEffect); + Gtk::FlowBoxChild *child = dynamic_cast<Gtk::FlowBoxChild *>(LPESelectorEffect->get_parent()); + if (child) { + child->grab_focus(); + } + return true; +} + + + +bool LivePathEffectAdd::on_filter(Gtk::FlowBoxChild *child) +{ + std::vector<Glib::ustring> classes = child->get_style_context()->list_classes(); + int pos = 0; + for (auto childclass : classes) { + size_t s = childclass.find("LPEIndex", 0); + if (s != -1) { + childclass = childclass.erase(0, 8); + pos = std::stoi(childclass); + } + } + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = &converter.data(pos); + bool disable = false; + if (_item_type == "group" && !converter.get_on_group(data->id)) { + disable = true; + } else if (_item_type == "shape" && !converter.get_on_shape(data->id)) { + disable = true; + } else if (_item_type == "path" && !converter.get_on_path(data->id)) { + disable = true; + } + + if (!_has_clip && data->id == Inkscape::LivePathEffect::POWERCLIP) { + disable = true; + } + if (!_has_mask && data->id == Inkscape::LivePathEffect::POWERMASK) { + disable = true; + } + + if (disable) { + child->get_style_context()->add_class("lpedisabled"); + } else { + child->get_style_context()->remove_class("lpedisabled"); + } + child->set_valign(Gtk::ALIGN_START); + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child->get_child()); + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + std::vector<Gtk::Widget *> content_overlay = overlay->get_children(); + + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + if (!sp_has_fav(lpename->get_text()) && _showfavs) { + return false; + } + Gtk::ToggleButton *experimental = dynamic_cast<Gtk::ToggleButton *>(contents[3]); + if (experimental) { + if (experimental->get_active() && !_LPEExperimental->get_active()) { + return false; + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + size_t s = lpedesc->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + if (_LPEFilter->get_text().length() < 1) { + _visiblelpe++; + return true; + } + if (lpename) { + size_t s = lpename->get_text().uppercase().find(_LPEFilter->get_text().uppercase(), 0); + if (s != -1) { + _visiblelpe++; + return true; + } + } + } + } + return false; +} + +void LivePathEffectAdd::reload_effect_list() +{ + /* if(_LPEExperimental->get_active()) { + _LPEExperimental->get_style_context()->add_class("active"); + } else { + _LPEExperimental->get_style_context()->remove_class("active"); + } */ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("You don't have any favorites yet, please disable the favorites star")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_text(_("This is your favorite effects")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } +} + +void LivePathEffectAdd::on_search() +{ + _visiblelpe = 0; + _LPESelectorFlowBox->invalidate_filter(); + if (_showfavs) { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } + } else { + if (_visiblelpe == 0) { + _LPEInfo->set_text(_("Your search do a empty result, please try again")); + _LPEInfo->set_visible(true); + _LPEInfo->get_style_context()->add_class("lpeinfowarn"); + } else { + _LPEInfo->set_visible(false); + _LPEInfo->get_style_context()->remove_class("lpeinfowarn"); + } + } +} + +int LivePathEffectAdd::on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2) +{ + Glib::ustring name1 = ""; + Glib::ustring name2 = ""; + Gtk::EventBox *eventbox = dynamic_cast<Gtk::EventBox *>(child1->get_child()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint mode = prefs->getInt("/dialogs/livepatheffect/dialogmode", 0); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + name1 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child1->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child1->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + if (overlay) { + std::vector<Gtk::Widget *> contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(40); + icon->set_margin_right(25); + overlay->set_margin_right(5); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(0); + overlay->set_margin_right(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]); + if (LPESelectorEffectEventFavTop) { + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child1->get_style_context()->add_class("lpefav"); + child1->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name1)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child1->get_style_context()->remove_class("lpefav"); + child1->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child1->get_style_context()->add_class("lpe"); + } + } + } + } + eventbox = dynamic_cast<Gtk::EventBox *>(child2->get_child()); + if (mode == 2) { + eventbox->set_halign(Gtk::ALIGN_START); + } else { + eventbox->set_halign(Gtk::ALIGN_CENTER); + } + if (eventbox) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(eventbox->get_child()); + if (mode == 2) { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + } else { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + } + if (box) { + std::vector<Gtk::Widget *> contents = box->get_children(); + Gtk::Label *lpename = dynamic_cast<Gtk::Label *>(contents[1]); + name2 = lpename->get_text(); + if (lpename) { + if (mode == 2) { + lpename->set_justify(Gtk::JUSTIFY_LEFT); + lpename->set_halign(Gtk::ALIGN_START); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(-1); + lpename->set_max_width_chars(-1); + } else { + lpename->set_justify(Gtk::JUSTIFY_CENTER); + lpename->set_halign(Gtk::ALIGN_CENTER); + lpename->set_valign(Gtk::ALIGN_CENTER); + lpename->set_width_chars(14); + lpename->set_max_width_chars(23); + } + } + Gtk::EventBox *lpemore = dynamic_cast<Gtk::EventBox *>(contents[4]); + if (lpemore) { + if (mode == 2) { + lpemore->hide(); + } else { + if (child2->is_selected()) { + lpemore->hide(); + } else { + lpemore->show(); + } + } + } + Gtk::ButtonBox *lpebuttonbox = dynamic_cast<Gtk::ButtonBox *>(contents[5]); + if (lpebuttonbox) { + if (mode == 2) { + lpebuttonbox->hide(); + } else { + if (child2->is_selected()) { + lpebuttonbox->show(); + } else { + lpebuttonbox->hide(); + } + } + } + Gtk::Label *lpedesc = dynamic_cast<Gtk::Label *>(contents[2]); + if (lpedesc) { + if (mode == 2) { + lpedesc->show(); + lpedesc->set_justify(Gtk::JUSTIFY_LEFT); + lpedesc->set_halign(Gtk::ALIGN_START); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_END); + } else { + lpedesc->hide(); + lpedesc->set_justify(Gtk::JUSTIFY_CENTER); + lpedesc->set_halign(Gtk::ALIGN_CENTER); + lpedesc->set_valign(Gtk::ALIGN_CENTER); + lpedesc->set_ellipsize(Pango::ELLIPSIZE_NONE); + } + } + Gtk::Overlay *overlay = dynamic_cast<Gtk::Overlay *>(contents[0]); + if (overlay) { + std::vector<Gtk::Widget *> contents_overlay = overlay->get_children(); + Gtk::Image *icon = dynamic_cast<Gtk::Image *>(contents_overlay[0]); + if (icon) { + if (mode == 2) { + icon->set_pixel_size(33); + icon->set_margin_right(40); + } else { + icon->set_pixel_size(60); + icon->set_margin_right(0); + } + } + Gtk::EventBox *LPESelectorEffectEventFavTop = dynamic_cast<Gtk::EventBox *>(contents_overlay[1]); + Gtk::Image *fav = dynamic_cast<Gtk::Image *>(LPESelectorEffectEventFavTop->get_child()); + if (LPESelectorEffectEventFavTop) { + if (sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + child2->get_style_context()->add_class("lpefav"); + child2->get_style_context()->remove_class("lpenormal"); + } else if (!sp_has_fav(name2)) { + fav->set_from_icon_name("draw-star-outline", Gtk::IconSize(Gtk::ICON_SIZE_SMALL_TOOLBAR)); + LPESelectorEffectEventFavTop->set_visible(false); + LPESelectorEffectEventFavTop->hide(); + child2->get_style_context()->remove_class("lpefav"); + child2->get_style_context()->add_class("lpenormal"); + } + if (mode == 2) { + LPESelectorEffectEventFavTop->set_visible(true); + LPESelectorEffectEventFavTop->show(); + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_CENTER); + } else { + LPESelectorEffectEventFavTop->set_halign(Gtk::ALIGN_END); + LPESelectorEffectEventFavTop->set_valign(Gtk::ALIGN_START); + } + child2->get_style_context()->add_class("lpe"); + } + } + } + } + std::vector<Glib::ustring> effect; + effect.push_back(name1); + effect.push_back(name2); + sort(effect.begin(), effect.end()); + /* if (sp_has_fav(name1) && sp_has_fav(name2)) { + return effect[0] == name1?-1:1; + } + if (sp_has_fav(name1)) { + return -1; + } */ + if (effect[0] == name1) { //&& !sp_has_fav(name2)) { + return -1; + } + return 1; +} + + +void LivePathEffectAdd::onClose() { _LPEDialogSelector->hide(); } + +void LivePathEffectAdd::onKeyEvent(GdkEventKey *evt) +{ + if (evt->keyval == GDK_KEY_Escape) { + onClose(); + } +} + +void LivePathEffectAdd::show(SPDesktop *desktop) +{ + LivePathEffectAdd &dial = instance(); + Inkscape::Selection *sel = desktop->getSelection(); + if (sel && !sel->isEmpty()) { + SPItem *item = sel->singleItem(); + if (item) { + SPShape *shape = dynamic_cast<SPShape *>(item); + SPPath *path = dynamic_cast<SPPath *>(item); + SPGroup *group = dynamic_cast<SPGroup *>(item); + dial._has_clip = (item->getClipObject() != nullptr); + dial._has_mask = (item->getMaskObject() != nullptr); + dial._item_type = ""; + if (group) { + dial._item_type = "group"; + } else if (path) { + dial._item_type = "path"; + } else if (shape) { + dial._item_type = "shape"; + } else { + dial._LPEDialogSelector->hide(); + return; + } + } + } + dial._applied = false; + dial._LPESelectorFlowBox->unset_sort_func(); + dial._LPESelectorFlowBox->unset_filter_func(); + dial._LPESelectorFlowBox->set_filter_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_filter)); + dial._LPESelectorFlowBox->set_sort_func(sigc::mem_fun(dial, &LivePathEffectAdd::on_sort)); + Glib::RefPtr<Gtk::Adjustment> vadjust = dial._LPEScrolled->get_vadjustment(); + vadjust->set_value(vadjust->get_lower()); + dial._LPEDialogSelector->show(); + int searchlen = dial._LPEFilter->get_text().length(); + if (searchlen > 0) { + dial._LPEFilter->select_region (0, searchlen); + dial._LPESelectorFlowBox->unselect_all(); + } else if (dial._lasteffect) { + dial._lasteffect->grab_focus(); + } + dial._LPEDialogSelector->run(); + dial._LPEDialogSelector->hide(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-add.h b/src/ui/dialog/livepatheffect-add.h new file mode 100644 index 0000000..bd8dca4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-add.h @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for adding a live path effect. + * + * Author: + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H +#define INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H + +#include "live_effects/effect-enum.h" +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/dialog.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/flowboxchild.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/overlay.h> +#include <gtkmm/popover.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/searchentry.h> +#include <gtkmm/stylecontext.h> +#include <gtkmm/switch.h> +#include <gtkmm/viewport.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to list the live path effects that can be added + * + */ +class LivePathEffectAdd { + public: + LivePathEffectAdd(); + ~LivePathEffectAdd() = default; + ; + + /** + * Show the dialog + */ + static void show(SPDesktop *desktop); + /** + * Returns true is the "Add" button was pressed + */ + static bool isApplied() { return instance()._applied; } + + static const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *getActiveData(); + + protected: + /** + * Close button was clicked + */ + void onClose(); + bool on_filter(Gtk::FlowBoxChild *child); + int on_sort(Gtk::FlowBoxChild *child1, Gtk::FlowBoxChild *child2); + void on_search(); + void on_focus(Gtk::Widget *widg); + bool pop_description(GdkEventCrossing *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool hide_pop_description(GdkEventCrossing *evt); + bool fav_toggler(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool apply(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add); + bool on_press_enter(GdkEventKey *key, Glib::RefPtr<Gtk::Builder> builder_effect, + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *to_add); + bool expand(GdkEventButton *evt, Glib::RefPtr<Gtk::Builder> builder_effect); + bool show_fav_toggler(GdkEventButton *evt); + void viewChanged(gint mode); + bool mouseover(GdkEventCrossing *evt, GtkWidget *wdg); + bool mouseout(GdkEventCrossing *evt, GtkWidget *wdg); + void reload_effect_list(); + /** + * Add button was clicked + */ + void onAdd(); + /** + * Tree was clicked + */ + void onButtonEvent(GdkEventButton* evt); + + /** + * Key event + */ + void onKeyEvent(GdkEventKey* evt); +private: + Gtk::Button _add_button; + Gtk::Button _close_button; + Gtk::Dialog *_LPEDialogSelector; + Glib::RefPtr<Gtk::Builder> _builder; + Gtk::FlowBox *_LPESelectorFlowBox; + Gtk::Popover *_LPESelectorEffectInfoPop; + Gtk::EventBox *_LPESelectorEffectEventFavShow; + Gtk::EventBox *_LPESelectorEffectInfoEventBox; + Gtk::RadioButton *_LPESelectorEffectRadioList; + Gtk::RadioButton *_LPESelectorEffectRadioPackLess; + Gtk::RadioButton *_LPESelectorEffectRadioPackMore; + Gtk::Switch *_LPEExperimental; + Gtk::SearchEntry *_LPEFilter; + Gtk::ScrolledWindow *_LPEScrolled; + Gtk::Label *_LPEInfo; + Gtk::Box *_LPESelector; + guint _visiblelpe; + Glib::ustring _item_type; + bool _has_clip; + bool _has_mask; + Gtk::FlowBoxChild *_lasteffect; + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *_to_add; + bool _showfavs; + bool _applied; + class Effect; + const LivePathEffect::EnumEffectDataConverter<LivePathEffect::EffectType> &converter; + static LivePathEffectAdd &instance() + { + static LivePathEffectAdd instance_; + return instance_; + } + LivePathEffectAdd(LivePathEffectAdd const &) = delete; // no copy + LivePathEffectAdd &operator=(LivePathEffectAdd const &) = delete; // no assign +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_DIALOG_LIVEPATHEFFECT_ADD_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-editor.cpp b/src/ui/dialog/livepatheffect-editor.cpp new file mode 100644 index 0000000..472f9ac --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.cpp @@ -0,0 +1,634 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Live Path Effect editing dialog - implementation. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Steren Giannini <steren.giannini@gmail.com> + * Bastien Bouclet <bgkweb@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "livepatheffect-editor.h" + +#include <gtkmm/expander.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "livepatheffect-add.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" + +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-use.h" +#include "object/sp-text.h" + +#include "ui/icon-names.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/imagetoggler.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/*#################### + * Callback functions + */ + + +void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data) +{ + LivePathEffectEditor *lpeeditor = static_cast<LivePathEffectEditor *>(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->onSelectionChanged(selection); + lpeeditor->_on_button_release(nullptr); //to force update widgets +} + +void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data) +{ + + LivePathEffectEditor *lpeeditor = static_cast<LivePathEffectEditor *>(data); + lpeeditor->lpe_list_locked = false; + lpeeditor->onSelectionChanged(selection); +} + +static void lpe_style_button(Gtk::Button& btn, char const* iconName) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add(*Gtk::manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); +} + + +/* + * LivePathEffectEditor + * + * TRANSLATORS: this dialog is accessible via menu Path - Path Effect Editor... + * + */ + +LivePathEffectEditor::LivePathEffectEditor() + : UI::Widget::Panel("/dialogs/livepatheffect", SP_VERB_DIALOG_LIVE_PATH_EFFECT), + deskTrack(), + lpe_list_locked(false), + effectwidget(nullptr), + status_label("", Gtk::ALIGN_CENTER), + effectcontrol_frame(""), + button_add(), + button_remove(), + button_up(), + button_down(), + current_desktop(nullptr), + current_lpeitem(nullptr), + current_lperef(nullptr) +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(4); + + //Add the TreeView, inside a ScrolledWindow, with the button underneath: + scrolled_window.add(effectlist_view); + scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrolled_window.set_shadow_type(Gtk::SHADOW_IN); + scrolled_window.set_size_request(210, 70); + + effectapplication_hbox.set_spacing(4); + effectcontrol_vbox.set_spacing(4); + + effectlist_vbox.pack_start(scrolled_window, Gtk::PACK_EXPAND_WIDGET); + effectlist_vbox.pack_end(toolbar_hbox, Gtk::PACK_SHRINK); + effectcontrol_eventbox.add_events(Gdk::BUTTON_RELEASE_MASK); + effectcontrol_eventbox.signal_button_release_event().connect(sigc::mem_fun(*this, &LivePathEffectEditor::_on_button_release) ); + effectcontrol_eventbox.add(effectcontrol_vbox); + effectcontrol_frame.add(effectcontrol_eventbox); + + button_add.set_tooltip_text(_("Add path effect")); + lpe_style_button(button_add, INKSCAPE_ICON("list-add")); + button_add.set_relief(Gtk::RELIEF_NONE); + + button_remove.set_tooltip_text(_("Delete current path effect")); + lpe_style_button(button_remove, INKSCAPE_ICON("list-remove")); + button_remove.set_relief(Gtk::RELIEF_NONE); + + button_up.set_tooltip_text(_("Raise the current path effect")); + lpe_style_button(button_up, INKSCAPE_ICON("go-up")); + button_up.set_relief(Gtk::RELIEF_NONE); + + button_down.set_tooltip_text(_("Lower the current path effect")); + lpe_style_button(button_down, INKSCAPE_ICON("go-down")); + button_down.set_relief(Gtk::RELIEF_NONE); + + // Add toolbar items to toolbar + toolbar_hbox.set_layout (Gtk::BUTTONBOX_END); + toolbar_hbox.add( button_add ); + toolbar_hbox.set_child_secondary( button_add , true); + toolbar_hbox.add( button_remove ); + toolbar_hbox.set_child_secondary( button_remove , true); + toolbar_hbox.add( button_up ); + toolbar_hbox.add( button_down ); + + //Create the Tree model: + effectlist_store = Gtk::ListStore::create(columns); + effectlist_view.set_model(effectlist_store); + effectlist_view.set_headers_visible(false); + + // Handle tree selections + effectlist_selection = effectlist_view.get_selection(); + effectlist_selection->signal_changed().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_effect_selection_changed) ); + + //Add the visibility icon column: + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = effectlist_view.append_column("is_visible", *eyeRenderer) - 1; + eyeRenderer->signal_toggled().connect( sigc::mem_fun(*this, &LivePathEffectEditor::on_visibility_toggled) ); + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = effectlist_view.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), columns.col_visible ); + } + + //Add the effect name column: + effectlist_view.append_column("Effect", columns.col_name); + + contents->pack_start(effectlist_vbox, true, true); + contents->pack_start(status_label, false, false); + contents->pack_start(effectcontrol_frame, false, false); + + effectcontrol_frame.hide(); + + // connect callback functions to buttons + button_add.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onAdd)); + button_remove.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onRemove)); + button_up.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onUp)); + button_down.signal_clicked().connect(sigc::mem_fun(*this, &LivePathEffectEditor::onDown)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &LivePathEffectEditor::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); +} + +LivePathEffectEditor::~LivePathEffectEditor() +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } +} + +bool LivePathEffectEditor::_on_button_release(GdkEventButton* button_event) { + Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + return true; + } + Gtk::TreeModel::iterator it = sel->get_selected(); + LivePathEffect::LPEObjectReference * lperef = (*it)[columns.lperef]; + if (lperef && current_lpeitem && current_lperef != lperef) { + if (lperef->getObject()) { + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + } + } + } + return true; +} + +void +LivePathEffectEditor::showParams(LivePathEffect::Effect& effect) +{ + if (effectwidget && !effect.refresh_widgets) { + return; + } + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + effectwidget = effect.newWidget(); + effectcontrol_frame.set_label(effect.getName()); + effectcontrol_vbox.pack_start(*effectwidget, true, true); + + button_remove.show(); + status_label.hide(); + effectcontrol_frame.show(); + effectcontrol_vbox.show_all_children(); + // fixme: add resizing of dialog + effect.refresh_widgets = false; +} + +void +LivePathEffectEditor::selectInList(LivePathEffect::Effect* effect) +{ + Gtk::TreeNodeChildren chi = effectlist_view.get_model()->children(); + for (Gtk::TreeIter ci = chi.begin() ; ci != chi.end(); ci++) { + if (ci->get_value(columns.lperef)->lpeobject->get_lpe() == effect && effectlist_view.get_selection()) + effectlist_view.get_selection()->select(ci); + } +} + + +void +LivePathEffectEditor::showText(Glib::ustring const &str) +{ + if (effectwidget) { + effectcontrol_vbox.remove(*effectwidget); + delete effectwidget; + effectwidget = nullptr; + } + + status_label.show(); + status_label.set_label(str); + + effectcontrol_frame.hide(); + + // fixme: do resizing of dialog ? +} + +void +LivePathEffectEditor::set_sensitize_all(bool sensitive) +{ + //combo_effecttype.set_sensitive(sensitive); + button_add.set_sensitive(sensitive); + button_remove.set_sensitive(sensitive); + effectlist_view.set_sensitive(sensitive); + button_up.set_sensitive(sensitive); + button_down.set_sensitive(sensitive); +} + +void +LivePathEffectEditor::onSelectionChanged(Inkscape::Selection *sel) +{ + if (lpe_list_locked) { + // this was triggered by selecting a row in the list, so skip reloading + lpe_list_locked = false; + return; + } + current_lpeitem = nullptr; + effectlist_store->clear(); + + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if ( item ) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + effect_list_reload(lpeitem); + current_lpeitem = lpeitem; + set_sensitize_all(true); + if ( lpeitem->hasPathEffect() ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } else { + showText(_("Unknown effect is applied")); + } + } else { + showText(_("Click button to add an effect")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } + } else { + SPUse *use = dynamic_cast<SPUse *>(item); + if ( use ) { + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *orig = use->get_original(); + if ( dynamic_cast<SPShape *>(orig) || + dynamic_cast<SPGroup *>(orig) || + dynamic_cast<SPText *>(orig) ) + { + // Note that an SP_USE cannot have an LPE applied, so we only need to worry about the "add effect" case. + set_sensitize_all(true); + showText(_("Click add button to convert clone")); + button_remove.set_sensitive(false); + button_up.set_sensitive(false); + button_down.set_sensitive(false); + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } + } + } else { + showText(_("Only one item can be selected")); + set_sensitize_all(false); + } + } else { + showText(_("Select a path or shape")); + set_sensitize_all(false); + } +} + +/* + * First clears the effectlist_store, then appends all effects from the effectlist. + */ +void +LivePathEffectEditor::effect_list_reload(SPLPEItem *lpeitem) +{ + effectlist_store->clear(); + + PathEffectList effectlist = lpeitem->getEffectList(); + PathEffectList::iterator it; + for( it = effectlist.begin() ; it!=effectlist.end(); ++it) + { + if ( !(*it)->lpeobject ) { + continue; + } + + if ((*it)->lpeobject->get_lpe()) { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = (*it)->lpeobject->get_lpe()->getName(); + row[columns.lperef] = *it; + row[columns.col_visible] = (*it)->lpeobject->get_lpe()->isVisible(); + } else { + Gtk::TreeModel::Row row = *(effectlist_store->append()); + row[columns.col_name] = _("Unknown effect"); + row[columns.lperef] = *it; + row[columns.col_visible] = false; + } + } +} + + +void +LivePathEffectEditor::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + + if ( desktop == current_desktop ) { + return; + } + + if (current_desktop) { + selection_changed_connection.disconnect(); + selection_modified_connection.disconnect(); + } + + lpe_list_locked = false; + current_desktop = desktop; + if (desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + selection_changed_connection = selection->connectChanged( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_changed), this ) ); + selection_modified_connection = selection->connectModified( + sigc::bind (sigc::ptr_fun(&lpeeditor_selection_modified), this ) ); + + onSelectionChanged(selection); + } else { + onSelectionChanged(nullptr); + } +} + +/*######################################################################## +# BUTTON CLICK HANDLERS (callbacks) +########################################################################*/ + +// TODO: factor out the effect applying code which can be called from anywhere. (selection-chemistry.cpp also needs it) +void +LivePathEffectEditor::onAdd() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + if ( dynamic_cast<SPLPEItem *>(item) ) { + // show effectlist dialog + using Inkscape::UI::Dialog::LivePathEffectAdd; + LivePathEffectAdd::show(current_desktop); + if ( !LivePathEffectAdd::isApplied()) { + return; + } + + SPDocument *doc = current_desktop->doc(); + + const LivePathEffect::EnumEffectData<LivePathEffect::EffectType> *data = + LivePathEffectAdd::getActiveData(); + if (!data) { + return; + } + item = sel->singleItem(); // get new item + + LivePathEffect::Effect::createAndApply(data->key.c_str(), doc, item); + + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } else { + SPUse *use = dynamic_cast<SPUse *>(item); + if ( use ) { + // item is a clone. do not show effectlist dialog. + // convert to path, apply CLONE_ORIGINAL LPE, link it to the cloned path + + // test whether linked object is supported by the CLONE_ORIGINAL LPE + SPItem *orig = use->get_original(); + if ( dynamic_cast<SPShape *>(orig) || + dynamic_cast<SPGroup *>(orig) || + dynamic_cast<SPText *>(orig) ) + { + // select original + sel->set(orig); + + // delete clone but remember its id and transform + gchar *id = g_strdup(item->getRepr()->attribute("id")); + gchar *transform = g_strdup(item->getRepr()->attribute("transform")); + item->deleteObject(false); + item = nullptr; + + // run sp_selection_clone_original_path_lpe + sel->cloneOriginalPathLPE(true); + + SPItem *new_item = sel->singleItem(); + // Check that the cloning was successful. We don't want to change the ID of the original referenced path! + if (new_item && (new_item != orig)) { + new_item->setAttribute("id", id); + new_item->setAttribute("transform", transform); + } + g_free(id); + g_free(transform); + + /// \todo Add the LPE stack of the original path? + + SPDocument *doc = current_desktop->doc(); + DocumentUndo::done(doc, SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Create and apply Clone original path effect")); + + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + } + } + } +} + +void +LivePathEffectEditor::onRemove() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + sp_lpe_item_update_patheffect(lpeitem, false, false); + lpeitem->removeCurrentPathEffect(false); + current_lperef = nullptr; + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Remove path effect") ); + lpe_list_locked = false; + onSelectionChanged(sel); + } + } + +} + +void LivePathEffectEditor::onUp() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->upCurrentPathEffect(); + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect up") ); + + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::onDown() +{ + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + Inkscape::LivePathEffect::Effect *lpe = lpeitem->getCurrentLPE(); + lpeitem->downCurrentPathEffect(); + + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + _("Move path effect down") ); + effect_list_reload(lpeitem); + if (lpe) { + showParams(*lpe); + lpe_list_locked = true; + selectInList(lpe); + } + } + } +} + +void LivePathEffectEditor::on_effect_selection_changed() +{ + Glib::RefPtr<Gtk::TreeSelection> sel = effectlist_view.get_selection(); + if (sel->count_selected_rows () == 0) { + button_remove.set_sensitive(false); + return; + } + button_remove.set_sensitive(true); + Gtk::TreeModel::iterator it = sel->get_selected(); + LivePathEffect::LPEObjectReference * lperef = (*it)[columns.lperef]; + + if (lperef && current_lpeitem && current_lperef != lperef) { + //The last condition ignore Gtk::TreeModel may occasionally be changed emitted when nothing has happened + if (lperef->getObject()) { + lpe_list_locked = true; // prevent reload of the list which would lose selection + current_lpeitem->setCurrentPathEffect(lperef); + current_lperef = lperef; + LivePathEffect::Effect * effect = lperef->lpeobject->get_lpe(); + if (effect) { + effect->refresh_widgets = true; + showParams(*effect); + //To reload knots and helper paths + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + if (item) { + sel->clear(); + sel->add(item); + Inkscape::UI::Tools::sp_update_helperpath(); + } + } + } + } + } +} + +void LivePathEffectEditor::on_visibility_toggled( Glib::ustring const& str ) +{ + + Gtk::TreeModel::Children::iterator iter = effectlist_view.get_model()->get_iter(str); + Gtk::TreeModel::Row row = *iter; + + LivePathEffect::LPEObjectReference * lpeobjref = row[columns.lperef]; + + if ( lpeobjref && lpeobjref->lpeobject->get_lpe() ) { + bool newValue = !row[columns.col_visible]; + row[columns.col_visible] = newValue; + /* FIXME: this explicit writing to SVG is wrong. The lpe_item should have a method to disable/enable an effect within its stack. + * So one can call: lpe_item->setActive(lpeobjref->lpeobject); */ + lpeobjref->lpeobject->get_lpe()->getRepr()->setAttribute("is_visible", newValue ? "true" : "false"); + Inkscape::Selection *sel = _getSelection(); + if ( sel && !sel->isEmpty() ) { + SPItem *item = sel->singleItem(); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if ( lpeitem ) { + lpeobjref->lpeobject->get_lpe()->doOnVisibilityToggled(lpeitem); + } + } + DocumentUndo::done( current_desktop->getDocument(), SP_VERB_DIALOG_LIVE_PATH_EFFECT, + newValue ? _("Activate path effect") : _("Deactivate path effect")); + } +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/livepatheffect-editor.h b/src/ui/dialog/livepatheffect-editor.h new file mode 100644 index 0000000..30524e4 --- /dev/null +++ b/src/ui/dialog/livepatheffect-editor.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Live Path Effect editing dialog + */ +/* Author: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Author + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H +#define INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H + +#include "ui/widget/panel.h" + +#include <gtkmm/label.h> +#include <gtkmm/frame.h> +#include "ui/widget/combo-enums.h" +#include "ui/widget/frame.h" +#include "object/sp-item.h" +#include "live_effects/effect-enum.h" +#include <gtkmm/liststore.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/treeview.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/buttonbox.h> +#include "ui/dialog/desktop-tracker.h" + +class SPDesktop; +class SPLPEItem; + +namespace Inkscape { + +namespace LivePathEffect { + class Effect; + class LPEObjectReference; +} + +namespace UI { +namespace Dialog { + +class LivePathEffectEditor : public UI::Widget::Panel { +public: + LivePathEffectEditor(); + ~LivePathEffectEditor() override; + + static LivePathEffectEditor &getInstance() { return *new LivePathEffectEditor(); } + + void onSelectionChanged(Inkscape::Selection *sel); + void onSelectionModified(Inkscape::Selection *sel); + virtual void on_effect_selection_changed(); + void setDesktop(SPDesktop *desktop) override; + +private: + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + sigc::connection selection_changed_connection; + sigc::connection selection_modified_connection; + + // void add_entry(const char* name ); + void effect_list_reload(SPLPEItem *lpeitem); + + void set_sensitize_all(bool sensitive); + void showParams(LivePathEffect::Effect& effect); + void showText(Glib::ustring const &str); + void selectInList(LivePathEffect::Effect* effect); + + // callback methods for buttons on grids page. + void onAdd(); + void onRemove(); + void onUp(); + void onDown(); + + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ModelColumns() + { + add(col_name); + add(lperef); + add(col_visible); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> col_name; + Gtk::TreeModelColumn<LivePathEffect::LPEObjectReference *> lperef; + Gtk::TreeModelColumn<bool> col_visible; + }; + + bool lpe_list_locked; + //Inkscape::UI::Widget::ComboBoxEnum<LivePathEffect::EffectType> combo_effecttype; + + Gtk::Widget * effectwidget; + Gtk::Label status_label; + UI::Widget::Frame effectcontrol_frame; + Gtk::HBox effectapplication_hbox; + Gtk::VBox effectcontrol_vbox; + Gtk::EventBox effectcontrol_eventbox; + Gtk::VBox effectlist_vbox; + ModelColumns columns; + Gtk::ScrolledWindow scrolled_window; + Gtk::TreeView effectlist_view; + Glib::RefPtr<Gtk::ListStore> effectlist_store; + Glib::RefPtr<Gtk::TreeSelection> effectlist_selection; + + void on_visibility_toggled( Glib::ustring const& str); + bool _on_button_release(GdkEventButton* button_event); + Gtk::ButtonBox toolbar_hbox; + Gtk::Button button_add; + Gtk::Button button_remove; + Gtk::Button button_up; + Gtk::Button button_down; + + SPDesktop * current_desktop; + + SPLPEItem * current_lpeitem; + + LivePathEffect::LPEObjectReference * current_lperef; + + friend void lpeeditor_selection_changed (Inkscape::Selection * selection, gpointer data); + friend void lpeeditor_selection_modified (Inkscape::Selection * selection, guint /*flags*/, gpointer data); + + LivePathEffectEditor(LivePathEffectEditor const &d); + LivePathEffectEditor& operator=(LivePathEffectEditor const &d); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_LIVE_PATH_EFFECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/lpe-fillet-chamfer-properties.cpp b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp new file mode 100644 index 0000000..bbf4b31 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.cpp @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include "lpe-fillet-chamfer-properties.h" +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "message-stack.h" + +#include "selection-chemistry.h" + +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +FilletChamferPropertiesDialog::FilletChamferPropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + mainVBox->set_homogeneous(false); + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _fillet_chamfer_position_numeric.set_digits(4); + _fillet_chamfer_position_numeric.set_increments(1,1); + //todo: get tha max aloable infinity freeze the widget + _fillet_chamfer_position_numeric.set_range(0., SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_position_numeric.set_hexpand(); + _fillet_chamfer_position_label.set_label(_("Radius (pixels):")); + _fillet_chamfer_position_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_position_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_position_label, 0, 0, 1, 1); + _layout_table.attach(_fillet_chamfer_position_numeric, 1, 0, 1, 1); + _fillet_chamfer_chamfer_subdivisions.set_digits(0); + _fillet_chamfer_chamfer_subdivisions.set_increments(1,1); + //todo: get tha max aloable infinity freeze the widget + _fillet_chamfer_chamfer_subdivisions.set_range(0, SCALARPARAM_G_MAXDOUBLE); + _fillet_chamfer_chamfer_subdivisions.set_hexpand(); + _fillet_chamfer_chamfer_subdivisions_label.set_label(_("Chamfer subdivisions:")); + _fillet_chamfer_chamfer_subdivisions_label.set_halign(Gtk::ALIGN_END); + _fillet_chamfer_chamfer_subdivisions_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions_label, 0, 1, 1, 1); + _layout_table.attach(_fillet_chamfer_chamfer_subdivisions, 1, 1, 1, 1); + _fillet_chamfer_type_fillet.set_label(_("Fillet")); + _fillet_chamfer_type_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_fillet.set_label(_("Inverse fillet")); + _fillet_chamfer_type_inverse_fillet.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_chamfer.set_label(_("Chamfer")); + _fillet_chamfer_type_chamfer.set_group(_fillet_chamfer_type_group); + _fillet_chamfer_type_inverse_chamfer.set_label(_("Inverse chamfer")); + _fillet_chamfer_type_inverse_chamfer.set_group(_fillet_chamfer_type_group); + + + mainVBox->pack_start(_layout_table, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_fillet, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_chamfer, true, true, 4); + mainVBox->pack_start(_fillet_chamfer_type_inverse_chamfer, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_apply)); + + signal_delete_event().connect(sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &FilletChamferPropertiesDialog::_close)), + true)); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_fillet_chamfer_position_numeric); +} + +FilletChamferPropertiesDialog::~FilletChamferPropertiesDialog() +{ + + _setDesktop(nullptr); +} + +void FilletChamferPropertiesDialog::showDialog( + SPDesktop *desktop, + double _amount, + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt, + bool _use_distance, + bool _aprox_radius, + Satellite _satellite) +{ + FilletChamferPropertiesDialog *dialog = new FilletChamferPropertiesDialog(); + + dialog->_setDesktop(desktop); + dialog->_setUseDistance(_use_distance); + dialog->_setAprox(_aprox_radius); + dialog->_setAmount(_amount); + dialog->_setSatellite(_satellite); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Fillet-Chamfer")); + dialog->_apply_button.set_label(_("_Modify")); + + dialog->set_modal(true); + desktop->setWindowTransient(dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void FilletChamferPropertiesDialog::_apply() +{ + + double d_pos = _fillet_chamfer_position_numeric.get_value(); + if (d_pos >= 0) { + if (_fillet_chamfer_type_fillet.get_active() == true) { + _satellite.satellite_type = FILLET; + } else if (_fillet_chamfer_type_inverse_fillet.get_active() == true) { + _satellite.satellite_type = INVERSE_FILLET; + } else if (_fillet_chamfer_type_inverse_chamfer.get_active() == true) { + _satellite.satellite_type = INVERSE_CHAMFER; + } else { + _satellite.satellite_type = CHAMFER; + } + if (_flexible) { + if (d_pos > 99.99999 || d_pos < 0) { + d_pos = 0; + } + d_pos = d_pos / 100; + } + _satellite.amount = d_pos; + size_t steps = (size_t)_fillet_chamfer_chamfer_subdivisions.get_value(); + if (steps < 1) { + steps = 1; + } + _satellite.steps = steps; + _knotpoint->knot_set_offset(_satellite); + } + _close(); +} + +void FilletChamferPropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool FilletChamferPropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + return false; +} + +void FilletChamferPropertiesDialog::_handleButtonEvent(GdkEventButton *event) +{ + if ((event->type == GDK_2BUTTON_PRESS) && (event->button == 1)) { + _apply(); + } +} + +void FilletChamferPropertiesDialog::_setSatellite(Satellite satellite) +{ + double position; + std::string distance_or_radius = std::string(_("Radius")); + if (_aprox) { + distance_or_radius = std::string(_("Radius approximated")); + } + if (_use_distance) { + distance_or_radius = std::string(_("Knot distance")); + } + if (satellite.is_time) { + position = _amount * 100; + _flexible = true; + _fillet_chamfer_position_label.set_label(_("Position (%):")); + } else { + _flexible = false; + std::string posConcat = Glib::ustring::compose (_("%1:"), distance_or_radius); + _fillet_chamfer_position_label.set_label(_(posConcat.c_str())); + position = _amount; + } + _fillet_chamfer_position_numeric.set_value(position); + _fillet_chamfer_chamfer_subdivisions.set_value(satellite.steps); + if (satellite.satellite_type == FILLET) { + _fillet_chamfer_type_fillet.set_active(true); + } else if (satellite.satellite_type == INVERSE_FILLET) { + _fillet_chamfer_type_inverse_fillet.set_active(true); + } else if (satellite.satellite_type == CHAMFER) { + _fillet_chamfer_type_chamfer.set_active(true); + } else if (satellite.satellite_type == INVERSE_CHAMFER) { + _fillet_chamfer_type_inverse_chamfer.set_active(true); + } + _satellite = satellite; +} + +void FilletChamferPropertiesDialog::_setPt( + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt) +{ + _knotpoint = const_cast< + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity *>( + pt); +} + + +void FilletChamferPropertiesDialog::_setAmount(double amount) +{ + _amount = amount; +} + + + +void FilletChamferPropertiesDialog::_setUseDistance(bool use_knot_distance) +{ + _use_distance = use_knot_distance; +} + +void FilletChamferPropertiesDialog::_setAprox(bool _aprox_radius) +{ + _aprox = _aprox_radius; +} + +void FilletChamferPropertiesDialog::_setDesktop(SPDesktop *desktop) +{ + if (desktop) { + Inkscape::GC::anchor(desktop); + } + if (_desktop) { + Inkscape::GC::release(_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 diff --git a/src/ui/dialog/lpe-fillet-chamfer-properties.h b/src/ui/dialog/lpe-fillet-chamfer-properties.h new file mode 100644 index 0000000..26a0569 --- /dev/null +++ b/src/ui/dialog/lpe-fillet-chamfer-properties.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * + * From the code of Liam P.White from his Power Stroke Knot dialog + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H +#define INKSCAPE_DIALOG_FILLET_CHAMFER_PROPERTIES_H + +#include <2geom/point.h> +#include <gtkmm.h> +#include "live_effects/parameter/satellitesarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class FilletChamferPropertiesDialog : public Gtk::Dialog { +public: + FilletChamferPropertiesDialog(); + ~FilletChamferPropertiesDialog() override; + + Glib::ustring getName() const + { + return "LayerPropertiesDialog"; + } + + static void showDialog(SPDesktop *desktop, double _amount, + const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt, + bool _use_distance, + bool _aprox_radius, + Satellite _satellite); + +protected: + + SPDesktop *_desktop; + Inkscape::LivePathEffect::FilletChamferKnotHolderEntity * + _knotpoint; + + Gtk::Label _fillet_chamfer_position_label; + Gtk::SpinButton _fillet_chamfer_position_numeric; + Gtk::RadioButton::Group _fillet_chamfer_type_group; + Gtk::RadioButton _fillet_chamfer_type_fillet; + Gtk::RadioButton _fillet_chamfer_type_inverse_fillet; + Gtk::RadioButton _fillet_chamfer_type_chamfer; + Gtk::RadioButton _fillet_chamfer_type_inverse_chamfer; + Gtk::Label _fillet_chamfer_chamfer_subdivisions_label; + Gtk::SpinButton _fillet_chamfer_chamfer_subdivisions; + + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static FilletChamferPropertiesDialog &_instance() + { + static FilletChamferPropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity *pt); + void _setUseDistance(bool use_knot_distance); + void _setAprox(bool aprox_radius); + void _setAmount(double amount); + void _setSatellite(Satellite satellite); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton *event); + + void _apply(); + void _close(); + bool _flexible; + Satellite _satellite; + bool _use_distance; + double _amount; + bool _aprox; + + friend class Inkscape::LivePathEffect:: + FilletChamferKnotHolderEntity; + +private: + FilletChamferPropertiesDialog( + FilletChamferPropertiesDialog const &); // no copy + FilletChamferPropertiesDialog &operator=( + FilletChamferPropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/lpe-powerstroke-properties.cpp b/src/ui/dialog/lpe-powerstroke-properties.cpp new file mode 100644 index 0000000..1e5d1b1 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dialog for renaming layers. + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * Andrius R. <knutux@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2006 Andrius R. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-powerstroke-properties.h" +#include <boost/lexical_cast.hpp> +#include <glibmm/i18n.h> +#include "inkscape.h" +#include "desktop.h" +#include "document-undo.h" +#include "layer-manager.h" + +#include "selection-chemistry.h" +//#include "event-context.h" + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +PowerstrokePropertiesDialog::PowerstrokePropertiesDialog() + : _desktop(nullptr), + _knotpoint(nullptr), + _position_visible(false), + _close_button(_("_Cancel"), true) +{ + Gtk::Box *mainVBox = get_content_area(); + + _layout_table.set_row_spacing(4); + _layout_table.set_column_spacing(4); + + // Layer name widgets + _powerstroke_position_entry.set_activates_default(true); + _powerstroke_position_entry.set_digits(4); + _powerstroke_position_entry.set_increments(1,1); + _powerstroke_position_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_position_entry.set_hexpand(); + _powerstroke_position_label.set_label(_("Position:")); + _powerstroke_position_label.set_halign(Gtk::ALIGN_END); + _powerstroke_position_label.set_valign(Gtk::ALIGN_CENTER); + + _powerstroke_width_entry.set_activates_default(true); + _powerstroke_width_entry.set_digits(4); + _powerstroke_width_entry.set_increments(1,1); + _powerstroke_width_entry.set_range(-SCALARPARAM_G_MAXDOUBLE, SCALARPARAM_G_MAXDOUBLE); + _powerstroke_width_entry.set_hexpand(); + _powerstroke_width_label.set_label(_("Width:")); + _powerstroke_width_label.set_halign(Gtk::ALIGN_END); + _powerstroke_width_label.set_valign(Gtk::ALIGN_CENTER); + + _layout_table.attach(_powerstroke_position_label,0,0,1,1); + _layout_table.attach(_powerstroke_position_entry,1,0,1,1); + _layout_table.attach(_powerstroke_width_label, 0,1,1,1); + _layout_table.attach(_powerstroke_width_entry, 1,1,1,1); + + mainVBox->pack_start(_layout_table, true, true, 4); + + // Buttons + _close_button.set_can_default(); + + _apply_button.set_use_underline(true); + _apply_button.set_can_default(); + + _close_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)); + _apply_button.signal_clicked() + .connect(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_apply)); + + signal_delete_event().connect( + sigc::bind_return( + sigc::hide(sigc::mem_fun(*this, &PowerstrokePropertiesDialog::_close)), + true + ) + ); + + add_action_widget(_close_button, Gtk::RESPONSE_CLOSE); + add_action_widget(_apply_button, Gtk::RESPONSE_APPLY); + + _apply_button.grab_default(); + + show_all_children(); + + set_focus(_powerstroke_width_entry); +} + +PowerstrokePropertiesDialog::~PowerstrokePropertiesDialog() { + + _setDesktop(nullptr); +} + +void PowerstrokePropertiesDialog::showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + PowerstrokePropertiesDialog *dialog = new PowerstrokePropertiesDialog(); + + dialog->_setDesktop(desktop); + dialog->_setKnotPoint(knotpoint); + dialog->_setPt(pt); + + dialog->set_title(_("Modify Node Position")); + dialog->_apply_button.set_label(_("_Move")); + + dialog->set_modal(true); + desktop->setWindowTransient (dialog->gobj()); + dialog->property_destroy_with_parent() = true; + + dialog->show(); + dialog->present(); +} + +void +PowerstrokePropertiesDialog::_apply() +{ + double d_pos = _powerstroke_position_entry.get_value(); + double d_width = _powerstroke_width_entry.get_value(); + _knotpoint->knot_set_offset(Geom::Point(d_pos, d_width)); + _close(); +} + +void +PowerstrokePropertiesDialog::_close() +{ + _setDesktop(nullptr); + destroy_(); + Glib::signal_idle().connect( + sigc::bind_return( + sigc::bind(sigc::ptr_fun<void*, void>(&::operator delete), this), + false + ) + ); +} + +bool PowerstrokePropertiesDialog::_handleKeyEvent(GdkEventKey * /*event*/) +{ + + /*switch (get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + _apply(); + return true; + } + break; + }*/ + return false; +} + +void PowerstrokePropertiesDialog::_handleButtonEvent(GdkEventButton* event) +{ + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + _apply(); + } +} + +void PowerstrokePropertiesDialog::_setKnotPoint(Geom::Point knotpoint) +{ + _powerstroke_position_entry.set_value(knotpoint.x()); + _powerstroke_width_entry.set_value(knotpoint.y()); +} + +void PowerstrokePropertiesDialog::_setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt) +{ + _knotpoint = const_cast<Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *>(pt); +} + +void PowerstrokePropertiesDialog::_setDesktop(SPDesktop *desktop) { + if (desktop) { + Inkscape::GC::anchor (desktop); + } + if (_desktop) { + Inkscape::GC::release (_desktop); + } + _desktop = desktop; +} + +} // namespace +} // namespace +} // namespace + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/lpe-powerstroke-properties.h b/src/ui/dialog/lpe-powerstroke-properties.h new file mode 100644 index 0000000..0bf6539 --- /dev/null +++ b/src/ui/dialog/lpe-powerstroke-properties.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for renaming layers + */ +/* Author: + * Bryce W. Harrington <bryce@bryceharrington.com> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H +#define INKSCAPE_DIALOG_POWERSTROKE_PROPERTIES_H + +#include <2geom/point.h> +#include <gtkmm.h> +#include "live_effects/parameter/powerstrokepointarray.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialogs { + +class PowerstrokePropertiesDialog : public Gtk::Dialog { + public: + PowerstrokePropertiesDialog(); + ~PowerstrokePropertiesDialog() override; + + Glib::ustring getName() const { return "LayerPropertiesDialog"; } + + static void showDialog(SPDesktop *desktop, Geom::Point knotpoint, const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + +protected: + + SPDesktop *_desktop; + Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *_knotpoint; + + Gtk::Label _powerstroke_position_label; + Gtk::SpinButton _powerstroke_position_entry; + Gtk::Label _powerstroke_width_label; + Gtk::SpinButton _powerstroke_width_entry; + Gtk::Grid _layout_table; + bool _position_visible; + + Gtk::Button _close_button; + Gtk::Button _apply_button; + + sigc::connection _destroy_connection; + + static PowerstrokePropertiesDialog &_instance() { + static PowerstrokePropertiesDialog instance; + return instance; + } + + void _setDesktop(SPDesktop *desktop); + void _setPt(const Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity *pt); + + void _apply(); + void _close(); + + void _setKnotPoint(Geom::Point knotpoint); + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); + + bool _handleKeyEvent(GdkEventKey *event); + void _handleButtonEvent(GdkEventButton* event); + + friend class Inkscape::LivePathEffect::PowerStrokePointArrayParamKnotHolderEntity; + +private: + PowerstrokePropertiesDialog(PowerstrokePropertiesDialog const &); // no copy + PowerstrokePropertiesDialog &operator=(PowerstrokePropertiesDialog const &); // no assign +}; + +} // namespace +} // namespace +} // namespace + + +#endif //INKSCAPE_DIALOG_LAYER_PROPERTIES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/memory.cpp b/src/ui/dialog/memory.cpp new file mode 100644 index 0000000..a4210ca --- /dev/null +++ b/src/ui/dialog/memory.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Memory statistics dialog. + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/memory.h" +#include <glibmm/i18n.h> +#include <glibmm/main.h> + +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/dialog.h> + +#include "inkgc/gc-core.h" +#include "debug/heap.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +namespace { + +Glib::ustring format_size(std::size_t value) { + if (!value) { + return Glib::ustring("0"); + } + + typedef std::vector<char> Digits; + typedef std::vector<Digits *> Groups; + + Groups groups; + + Digits *digits; + + while (value) { + unsigned places=3; + digits = new Digits(); + digits->reserve(places); + + while ( value && places ) { + digits->push_back('0' + (char)( value % 10 )); + value /= 10; + --places; + } + + groups.push_back(digits); + } + + Glib::ustring temp; + + while (true) { + digits = groups.back(); + while (!digits->empty()) { + temp.append(1, digits->back()); + digits->pop_back(); + } + delete digits; + + groups.pop_back(); + if (groups.empty()) { + break; + } + + temp.append(","); + } + + return temp; +} + +} + +struct Memory::Private { + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<Glib::ustring> used; + Gtk::TreeModelColumn<Glib::ustring> slack; + Gtk::TreeModelColumn<Glib::ustring> total; + + ModelColumns() { add(name); add(used); add(slack); add(total); } + }; + + Private() { + model = Gtk::ListStore::create(columns); + view.set_model(model); + view.append_column(_("Heap"), columns.name); + view.append_column(_("In Use"), columns.used); + // TRANSLATORS: "Slack" refers to memory which is in the heap but currently unused. + // More typical usage is to call this memory "free" rather than "slack". + view.append_column(_("Slack"), columns.slack); + view.append_column(_("Total"), columns.total); + } + + void update(); + + void start_update_task(); + void stop_update_task(); + + ModelColumns columns; + Glib::RefPtr<Gtk::ListStore> model; + Gtk::TreeView view; + + sigc::connection update_task; +}; + +void Memory::Private::update() { + Debug::Heap::Stats total = { 0, 0 }; + + int aggregate_features = Debug::Heap::SIZE_AVAILABLE | Debug::Heap::USED_AVAILABLE; + Gtk::ListStore::iterator row; + + row = model->children().begin(); + + for ( unsigned i = 0 ; i < Debug::heap_count() ; i++ ) { + Debug::Heap *heap=Debug::get_heap(i); + if (heap) { + Debug::Heap::Stats stats=heap->stats(); + int features=heap->features(); + + aggregate_features &= features; + + if ( row == model->children().end() ) { + row = model->append(); + } + + row->set_value(columns.name, Glib::ustring(heap->name())); + if ( features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(stats.size)); + total.size += stats.size; + } else { + row->set_value(columns.total, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(stats.bytes_used)); + total.bytes_used += stats.bytes_used; + } else { + row->set_value(columns.used, Glib::ustring(_("Unknown"))); + } + if ( features & Debug::Heap::SIZE_AVAILABLE && + features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(stats.size - stats.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + } + } + + if ( row == model->children().end() ) { + row = model->append(); + } + + Glib::ustring value; + + row->set_value(columns.name, Glib::ustring(_("Combined"))); + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE ) { + row->set_value(columns.total, format_size(total.size)); + } else { + row->set_value(columns.total, Glib::ustring("> ") + format_size(total.size)); + } + + if ( aggregate_features & Debug::Heap::USED_AVAILABLE ) { + row->set_value(columns.used, format_size(total.bytes_used)); + } else { + row->set_value(columns.used, Glib::ustring("> ") + format_size(total.bytes_used)); + } + + if ( aggregate_features & Debug::Heap::SIZE_AVAILABLE && + aggregate_features & Debug::Heap::USED_AVAILABLE ) + { + row->set_value(columns.slack, format_size(total.size - total.bytes_used)); + } else { + row->set_value(columns.slack, Glib::ustring(_("Unknown"))); + } + + ++row; + + while ( row != model->children().end() ) { + row = model->erase(row); + } +} + +void Memory::Private::start_update_task() { + update_task.disconnect(); + update_task = Glib::signal_timeout().connect( + sigc::bind_return(sigc::mem_fun(*this, &Private::update), true), + 500 + ); +} + +void Memory::Private::stop_update_task() { + update_task.disconnect(); +} + +Memory::Memory() + : UI::Widget::Panel("/dialogs/memory", SP_VERB_HELP_MEMORY), + _private(*(new Memory::Private())) +{ + _getContents()->pack_start(_private.view); + + _private.update(); + + addResponseButton(_("Recalculate"), Gtk::RESPONSE_APPLY); + + show_all_children(); + + signal_show().connect(sigc::mem_fun(_private, &Private::start_update_task)); + signal_hide().connect(sigc::mem_fun(_private, &Private::stop_update_task)); + + _private.start_update_task(); +} + +Memory::~Memory() { + delete &_private; +} + +void Memory::_apply() { + GC::Core::gcollect(); + _private.update(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/memory.h b/src/ui/dialog/memory.h new file mode 100644 index 0000000..5e6a917 --- /dev/null +++ b/src/ui/dialog/memory.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Memory statistics dialog + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_DIALOG_MEMORY_H +#define SEEN_INKSCAPE_UI_DIALOG_MEMORY_H + +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Memory : public UI::Widget::Panel { +public: + Memory(); + ~Memory() override; + + static Memory &getInstance() { return *new Memory(); } + +protected: + void _apply() override; + +private: + Memory(Memory const &d) = delete; // no copy + void operator=(Memory const &d) = delete; // no assign + + struct Private; + Private &_private; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/messages.cpp b/src/ui/dialog/messages.cpp new file mode 100644 index 0000000..85ba840 --- /dev/null +++ b/src/ui/dialog/messages.cpp @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Messages dialog - implementation. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "messages.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +//######################################################################### +//## E V E N T S +//######################################################################### + +/** + * Also a public method. Remove all text from the dialog + */ +void Messages::clear() +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + buffer->erase(buffer->begin(), buffer->end()); +} + + +//######################################################################### +//## C O N S T R U C T O R / D E S T R U C T O R +//######################################################################### +/** + * Constructor + */ +Messages::Messages() + : UI::Widget::Panel("/dialogs/messages", SP_VERB_DIALOG_DEBUG), + buttonClear(_("_Clear"), _("Clear log messages")), + checkCapture(_("Capture log messages"), _("Capture log messages")) +{ + Gtk::Box *contents = _getContents(); + + /* + * Menu replaced with buttons + * + menuBar.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_File"), fileMenu) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("_Clear"), + sigc::mem_fun(*this, &Messages::clear) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Capture log messages"), + sigc::mem_fun(*this, &Messages::captureLogMessages) ) ); + fileMenu.items().push_back( Gtk::Menu_Helpers::MenuElem(_("Release log messages"), + sigc::mem_fun(*this, &Messages::releaseLogMessages) ) ); + contents->pack_start(menuBar, Gtk::PACK_SHRINK); + */ + + //### Set up the text widget + messageText.set_editable(false); + textScroll.add(messageText); + textScroll.set_policy(Gtk::POLICY_ALWAYS, Gtk::POLICY_ALWAYS); + contents->pack_start(textScroll); + + buttonBox.set_spacing(6); + buttonBox.pack_start(checkCapture, true, true, 6); + buttonBox.pack_end(buttonClear, false, false, 10); + contents->pack_start(buttonBox, Gtk::PACK_SHRINK); + + // sick of this thing shrinking too much + set_size_request(400, 300); + + show_all_children(); + + message(_("Ready.")); + + buttonClear.signal_clicked().connect(sigc::mem_fun(*this, &Messages::clear)); + checkCapture.signal_clicked().connect(sigc::mem_fun(*this, &Messages::toggleCapture)); + + /* + * TODO - Setting this preference doesn't capture messages that the user can see. + * Inkscape creates an instance of a dialog on startup and sends messages there, but when the user + * opens the dialog View > Messages the DialogManager creates a new instance of this class that is not capturing messages. + * + * message(_("Enable log display by setting dialogs.debug 'redirect' attribute to 1 in preferences.xml")); + */ + + handlerDefault = 0; + handlerGlibmm = 0; + handlerAtkmm = 0; + handlerPangomm = 0; + handlerGdkmm = 0; + handlerGtkmm = 0; + +} + +Messages::~Messages() += default; + + +//######################################################################### +//## M E T H O D S +//######################################################################### + +void Messages::message(char *msg) +{ + Glib::RefPtr<Gtk::TextBuffer> buffer = messageText.get_buffer(); + Glib::ustring uMsg = msg; + if (uMsg[uMsg.length()-1] != '\n') + uMsg += '\n'; + buffer->insert (buffer->end(), uMsg); +} + +// dialogLoggingCallback is already used in debug.cpp +static void dialogLoggingCallback(const gchar */*log_domain*/, + GLogLevelFlags /*log_level*/, + const gchar *messageText, + gpointer user_data) +{ + Messages *dlg = static_cast<Messages *>(user_data); + dlg->message(const_cast<char*>(messageText)); + +} + +void Messages::toggleCapture() +{ + if (checkCapture.get_active()) { + captureLogMessages(); + } else { + releaseLogMessages(); + } +} + +void Messages::captureLogMessages() +{ + /* + This might likely need more code, to capture Gtkmm + and Glibmm warnings, or maybe just simply grab stdout/stderr + */ + GLogLevelFlags flags = (GLogLevelFlags) (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | + G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG); + if ( !handlerDefault ) { + handlerDefault = g_log_set_handler(nullptr, flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGlibmm ) { + handlerGlibmm = g_log_set_handler("glibmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerAtkmm ) { + handlerAtkmm = g_log_set_handler("atkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerPangomm ) { + handlerPangomm = g_log_set_handler("pangomm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGdkmm ) { + handlerGdkmm = g_log_set_handler("gdkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + if ( !handlerGtkmm ) { + handlerGtkmm = g_log_set_handler("gtkmm", flags, + dialogLoggingCallback, (gpointer)this); + } + message(_("Log capture started.")); +} + +void Messages::releaseLogMessages() +{ + if ( handlerDefault ) { + g_log_remove_handler(nullptr, handlerDefault); + handlerDefault = 0; + } + if ( handlerGlibmm ) { + g_log_remove_handler("glibmm", handlerGlibmm); + handlerGlibmm = 0; + } + if ( handlerAtkmm ) { + g_log_remove_handler("atkmm", handlerAtkmm); + handlerAtkmm = 0; + } + if ( handlerPangomm ) { + g_log_remove_handler("pangomm", handlerPangomm); + handlerPangomm = 0; + } + if ( handlerGdkmm ) { + g_log_remove_handler("gdkmm", handlerGdkmm); + handlerGdkmm = 0; + } + if ( handlerGtkmm ) { + g_log_remove_handler("gtkmm", handlerGtkmm); + handlerGtkmm = 0; + } + message(_("Log capture stopped.")); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/messages.h b/src/ui/dialog/messages.h new file mode 100644 index 0000000..b3ad393 --- /dev/null +++ b/src/ui/dialog/messages.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Messages dialog + * + * A very simple dialog for displaying Inkscape messages. Messages + * sent to g_log(), g_warning(), g_message(), ets, are routed here, + * in order to avoid messing with the startup console. + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_MESSAGES_H +#define INKSCAPE_UI_DIALOG_MESSAGES_H + +#include <gtkmm/box.h> +#include <gtkmm/textview.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/menubar.h> +#include <gtkmm/menu.h> +#include <gtkmm/scrolledwindow.h> + +#include <glibmm/i18n.h> + +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class Messages : public UI::Widget::Panel { +public: + Messages(); + ~Messages() override; + + static Messages &getInstance() { return *new Messages(); } + + /** + * Clear all information from the dialog + */ + void clear(); + + /** + * Display a message + */ + void message(char *msg); + + /** + * Redirect g_log() messages to this widget + */ + void captureLogMessages(); + + /** + * Return g_log() messages to normal handling + */ + void releaseLogMessages(); + + void toggleCapture(); + +protected: + //Gtk::MenuBar menuBar; + //Gtk::Menu fileMenu; + Gtk::ScrolledWindow textScroll; + Gtk::TextView messageText; + Gtk::HBox buttonBox; + Gtk::Button buttonClear; + Gtk::CheckButton checkCapture; + + //Handler ID's + guint handlerDefault; + guint handlerGlibmm; + guint handlerAtkmm; + guint handlerPangomm; + guint handlerGdkmm; + guint handlerGtkmm; + +private: + Messages(Messages const &d) = delete; + Messages operator=(Messages const &d) = delete; +}; + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_MESSAGES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/new-from-template.cpp b/src/ui/dialog/new-from-template.cpp new file mode 100644 index 0000000..bfccdb6 --- /dev/null +++ b/src/ui/dialog/new-from-template.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog - implementation + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "new-from-template.h" +#include "file.h" + +#include "include/gtkmm_version.h" + +namespace Inkscape { +namespace UI { + + +NewFromTemplate::NewFromTemplate() + : _create_template_button(_("Create from template")) +{ + set_title(_("New From Template")); + resize(400, 400); + + _main_widget = new TemplateLoadTab(this); + + get_content_area()->pack_start(*_main_widget); + + _create_template_button.set_halign(Gtk::ALIGN_END); + _create_template_button.set_valign(Gtk::ALIGN_END); + _create_template_button.set_margin_end(15); + + get_content_area()->pack_end(_create_template_button, Gtk::PACK_SHRINK); + + _create_template_button.signal_clicked().connect( + sigc::mem_fun(*this, &NewFromTemplate::_createFromTemplate)); + _create_template_button.set_sensitive(false); + + show_all(); +} + +NewFromTemplate::~NewFromTemplate() +{ + delete _main_widget; +} + +void NewFromTemplate::setCreateButtonSensitive(bool value) +{ + _create_template_button.set_sensitive(value); +} + +void NewFromTemplate::_createFromTemplate() +{ + _main_widget->createTemplate(); + _onClose(); +} + +void NewFromTemplate::_onClose() +{ + response(0); +} + +void NewFromTemplate::load_new_from_template() +{ + NewFromTemplate dl; + dl.run(); +} + +} +} diff --git a/src/ui/dialog/new-from-template.h b/src/ui/dialog/new-from-template.h new file mode 100644 index 0000000..a308fe4 --- /dev/null +++ b/src/ui/dialog/new-from-template.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template main dialog + */ +/* Authors: + * Jan Darowski <jan.darowski@gmail.com>, supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_NEW_FROM_TEMPLATE_H + +#include <gtkmm/dialog.h> +#include <gtkmm/button.h> + +#include "template-load-tab.h" + + +namespace Inkscape { +namespace UI { + + +class NewFromTemplate : public Gtk::Dialog +{ + +friend class TemplateLoadTab; +public: + static void load_new_from_template(); + void setCreateButtonSensitive(bool value); + ~NewFromTemplate() override; + +private: + NewFromTemplate(); + Gtk::Button _create_template_button; + TemplateLoadTab* _main_widget; + + void _createFromTemplate(); + void _onClose(); +}; + +} +} +#endif diff --git a/src/ui/dialog/object-attributes.cpp b/src/ui/dialog/object-attributes.cpp new file mode 100644 index 0000000..f129854 --- /dev/null +++ b/src/ui/dialog/object-attributes.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-anchor.h" +#include "object/sp-image.h" + +#include "ui/dialog/object-attributes.h" +#include "ui/dialog/dialog-manager.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct SPAttrDesc { + gchar const *label; + gchar const *attribute; +}; + +static const SPAttrDesc anchor_desc[] = { + { N_("Href:"), "xlink:href"}, + { N_("Target:"), "target"}, + { N_("Type:"), "xlink:type"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkRoleAttribute + // Identifies the type of the related resource with an absolute URI + { N_("Role:"), "xlink:role"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkArcRoleAttribute + // For situations where the nature/role alone isn't enough, this offers an additional URI defining the purpose of the link. + { N_("Arcrole:"), "xlink:arcrole"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkTitleAttribute + { N_("Title:"), "xlink:title"}, + { N_("Show:"), "xlink:show"}, + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/linking.html#AElementXLinkActuateAttribute + { N_("Actuate:"), "xlink:actuate"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_desc[] = { + { N_("URL:"), "xlink:href"}, + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +static const SPAttrDesc image_nohref_desc[] = { + { N_("X:"), "x"}, + { N_("Y:"), "y"}, + { N_("Width:"), "width"}, + { N_("Height:"), "height"}, + { nullptr, nullptr} +}; + +ObjectAttributes::ObjectAttributes () : + UI::Widget::Panel("/dialogs/objectattr/", SP_VERB_DIALOG_ATTR), + blocked (false), + CurrentItem(nullptr), + attrTable(Gtk::manage(new SPAttributeTable())), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn() +{ + attrTable->show(); + widget_setup(); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectAttributes::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +ObjectAttributes::~ObjectAttributes () +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void ObjectAttributes::widget_setup () +{ + if (blocked) + { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + SPItem *item = selection->singleItem(); + if (!item) + { + set_sensitive (false); + CurrentItem = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + return; + } + + blocked = true; + + // CPPIFY + SPObject *obj = item; //to get the selected item +// GObjectClass *klass = G_OBJECT_GET_CLASS(obj); //to deduce the object's type +// GType type = G_TYPE_FROM_CLASS(klass); + const SPAttrDesc *desc; + +// if (type == SP_TYPE_ANCHOR) + if (SP_IS_ANCHOR(item)) + { + desc = anchor_desc; + } +// else if (type == SP_TYPE_IMAGE) + else if (SP_IS_IMAGE(item)) + { + Inkscape::XML::Node *ir = obj->getRepr(); + const gchar *href = ir->attribute("xlink:href"); + if ( (!href) || ((strncmp(href, "data:", 5) == 0)) ) + { + desc = image_nohref_desc; + } + else + { + desc = image_desc; + } + } + else + { + blocked = false; + set_sensitive (false); + return; + } + + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> attrs; + if (CurrentItem != item) + { + int len = 0; + while (desc[len].label) + { + labels.emplace_back(desc[len].label); + attrs.emplace_back(desc[len].attribute); + len += 1; + } + attrTable->set_object(obj, labels, attrs, (GtkWidget*)gobj()); + CurrentItem = item; + } + else + { + attrTable->change_object(obj); + } + + set_sensitive (true); + show_all(); + blocked = false; +} + +void ObjectAttributes::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &ObjectAttributes::widget_setup))); + + // Must check flags, so can't call widget_setup() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &ObjectAttributes::selectionModifiedCB))); + } + widget_setup(); + } +} + +void ObjectAttributes::selectionModifiedCB( guint flags ) +{ + if (flags & ( SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG) ) { + attrTable->reread_properties(); + } +} + + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/object-attributes.h b/src/ui/dialog/object-attributes.h new file mode 100644 index 0000000..ad718ea --- /dev/null +++ b/src/ui/dialog/object-attributes.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic object attribute editor + *//* + * Authors: + * see git history + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_OBJECT_ATTRIBUTES_H +#define SEEN_DIALOGS_OBJECT_ATTRIBUTES_H + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +class SPAttributeTable; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object attributes (currently for images and links). + */ +class ObjectAttributes : public Widget::Panel { +public: + ObjectAttributes (); + ~ObjectAttributes () override; + + /** + * Returns a new instance of the object attributes dialog. + * + * Auxiliary function needed by the DialogManager. + */ + static ObjectAttributes &getInstance() { return *new ObjectAttributes(); } + + /** + * Updates entries and other child widgets on selection change, object modification, etc. + */ + void widget_setup(); + +private: + /** + * Is UI update bloched? + */ + bool blocked; + + /** + * Contains a pointer to the currently selected item (NULL in case nothing is or multiple objects are selected). + */ + SPItem *CurrentItem; + + /** + * Child widget to show the object attributes. + * + * attrTable makes the labels and edit boxes for the attributes defined + * in the SPAttrDesc arrays at the top of the cpp-file. This widgets also + * ensures object attribute modifications by the user are set. + */ + SPAttributeTable *attrTable; + + /** + * Stores the current desktop. + */ + SPDesktop *desktop; + + /** + * Auxiliary widget to keep track of desktop changes for the floating dialog. + */ + DesktopTracker deskTrack; + + /** + * Link to callback function for a change in desktop (window). + */ + sigc::connection desktopChangeConn; + + /** + * Link to callback function for a selection change. + */ + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + + /** + * Link to callback function for a modification of the selected object. + */ + sigc::connection selectModifiedConn; + + /** + * Callback function invoked by the desktop tracker in case of a modification of the selected object. + */ + void selectionModifiedCB( guint flags ); + + /* + * Can be invoked for setting the desktop. Currently not used. + */ + // void setDesktop(SPDesktop *desktop); + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/object-properties.cpp b/src/ui/dialog/object-properties.cpp new file mode 100644 index 0000000..b2594a0 --- /dev/null +++ b/src/ui/dialog/object-properties.cpp @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * c++ version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + */ + +#include "object-properties.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/grid.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" +#include "style.h" +#include "style-enums.h" + +#include "object/sp-image.h" + +#include "widgets/sp-attribute-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ObjectProperties::ObjectProperties() + : UI::Widget::Panel("/dialogs/object/", SP_VERB_DIALOG_ITEM) + , _blocked(false) + , _current_item(nullptr) + , _label_id(_("_ID:"), true) + , _label_label(_("_Label:"), true) + , _label_title(_("_Title:"), true) + , _label_dpi(_("_DPI SVG:"), true) + , _label_image_rendering(_("_Image Rendering:"), true) + , _cb_hide(_("_Hide"), true) + , _cb_lock(_("L_ock"), true) + , _cb_aspect_ratio(_("Preserve Ratio"), true) + , _exp_interactivity(_("_Interactivity"), true) + , _attr_table(Gtk::manage(new SPAttributeTable())) + , _desktop(nullptr) +{ + //initialize labels for the table at the bottom of the dialog + _int_attrs.emplace_back("onclick"); + _int_attrs.emplace_back("onmouseover"); + _int_attrs.emplace_back("onmouseout"); + _int_attrs.emplace_back("onmousedown"); + _int_attrs.emplace_back("onmouseup"); + _int_attrs.emplace_back("onmousemove"); + _int_attrs.emplace_back("onfocusin"); + _int_attrs.emplace_back("onfocusout"); + _int_attrs.emplace_back("onload"); + + _int_labels.emplace_back("onclick:"); + _int_labels.emplace_back("onmouseover:"); + _int_labels.emplace_back("onmouseout:"); + _int_labels.emplace_back("onmousedown:"); + _int_labels.emplace_back("onmouseup:"); + _int_labels.emplace_back("onmousemove:"); + _int_labels.emplace_back("onfocusin:"); + _int_labels.emplace_back("onfocusout:"); + _int_labels.emplace_back("onload:"); + + _desktop_changed_connection = _desktop_tracker.connectDesktopChanged( + sigc::mem_fun(*this, &ObjectProperties::_setTargetDesktop) + ); + _desktop_tracker.connect(GTK_WIDGET(gobj())); + + _init(); +} + +ObjectProperties::~ObjectProperties() +{ + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + _desktop_changed_connection.disconnect(); + _desktop_tracker.disconnect(); +} + +void ObjectProperties::_init() +{ + Gtk::Box *contents = _getContents(); + contents->set_spacing(0); + + auto grid_top = Gtk::manage(new Gtk::Grid()); + grid_top->set_row_spacing(4); + grid_top->set_column_spacing(0); + grid_top->set_border_width(4); + + contents->pack_start(*grid_top, false, false, 0); + + + /* Create the label for the object id */ + _label_id.set_label(_label_id.get_label() + " "); + _label_id.set_halign(Gtk::ALIGN_START); + _label_id.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_id, 0, 0, 1, 1); + + /* Create the entry box for the object id */ + _entry_id.set_tooltip_text(_("The id= attribute (only letters, digits, and the characters .-_: allowed)")); + _entry_id.set_max_length(64); + _entry_id.set_hexpand(); + _entry_id.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_id, 1, 0, 1, 1); + + _label_id.set_mnemonic_widget(_entry_id); + + // pressing enter in the id field is the same as clicking Set: + _entry_id.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + // focus is in the id field initially: + _entry_id.grab_focus(); + + + /* Create the label for the object label */ + _label_label.set_label(_label_label.get_label() + " "); + _label_label.set_halign(Gtk::ALIGN_START); + _label_label.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_label, 0, 1, 1, 1); + + /* Create the entry box for the object label */ + _entry_label.set_tooltip_text(_("A freeform label for the object")); + _entry_label.set_max_length(256); + + _entry_label.set_hexpand(); + _entry_label.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_label, 1, 1, 1, 1); + + _label_label.set_mnemonic_widget(_entry_label); + + // pressing enter in the label field is the same as clicking Set: + _entry_label.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + + /* Create the label for the object title */ + _label_title.set_label(_label_title.get_label() + " "); + _label_title.set_halign(Gtk::ALIGN_START); + _label_title.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_title, 0, 2, 1, 1); + + /* Create the entry box for the object title */ + _entry_title.set_sensitive (FALSE); + _entry_title.set_max_length (256); + + _entry_title.set_hexpand(); + _entry_title.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_entry_title, 1, 2, 1, 1); + + _label_title.set_mnemonic_widget(_entry_title); + // pressing enter in the label field is the same as clicking Set: + _entry_title.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Create the frame for the object description */ + Gtk::Label *label_desc = Gtk::manage(new Gtk::Label(_("_Description:"), true)); + UI::Widget::Frame *frame_desc = Gtk::manage(new UI::Widget::Frame("", FALSE)); + frame_desc->set_label_widget(*label_desc); + frame_desc->set_padding (0,0,0,0); + contents->pack_start(*frame_desc, true, true, 0); + + /* Create the text view box for the object description */ + _ft_description.set_border_width(4); + _ft_description.set_sensitive(FALSE); + frame_desc->add(_ft_description); + _ft_description.set_shadow_type(Gtk::SHADOW_IN); + + _tv_description.set_wrap_mode(Gtk::WRAP_WORD); + _tv_description.get_buffer()->set_text(""); + _ft_description.add(_tv_description); + _tv_description.add_mnemonic_label(*label_desc); + + /* Create the label for the object title */ + _label_dpi.set_label(_label_dpi.get_label() + " "); + _label_dpi.set_halign(Gtk::ALIGN_START); + _label_dpi.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_dpi, 0, 3, 1, 1); + + /* Create the entry box for the SVG DPI */ + _spin_dpi.set_digits(2); + _spin_dpi.set_range(1, 1200); + grid_top->attach(_spin_dpi, 1, 3, 1, 1); + + _label_dpi.set_mnemonic_widget(_spin_dpi); + // pressing enter in the label field is the same as clicking Set: + _spin_dpi.signal_activate().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Image rendering */ + /* Create the label for the object ImageRendering */ + _label_image_rendering.set_label(_label_image_rendering.get_label() + " "); + _label_image_rendering.set_halign(Gtk::ALIGN_START); + _label_image_rendering.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_label_image_rendering, 0, 4, 1, 1); + + /* Create the combo box text for the 'image-rendering' property */ + for (unsigned i = 0; enum_image_rendering[i].key; ++i) { + _combo_image_rendering.append(enum_image_rendering[i].key); + } + _combo_image_rendering.set_tooltip_text(_("The 'image-rendering' property can influence how a bitmap is re-scaled:\n" + "\t• 'auto' no preference (usually smooth but blurred)\n" + "\t• 'optimizeQuality' prefer rendering quality (usually smooth but blurred)\n" + "\t• 'optimizeSpeed' prefer rendering speed (usually blocky)\n" + "\t• 'crisp-edges' rescale without blurring edges (often blocky)\n" + "\t• 'pixelated' render blocky\n" + "Note that the specification of this property is not finalized. " + "Support and interpretation of these values varies between renderers.")); + + _combo_image_rendering.set_valign(Gtk::ALIGN_CENTER); + grid_top->attach(_combo_image_rendering, 1, 4, 1, 1); + + _label_image_rendering.set_mnemonic_widget(_combo_image_rendering); + + _combo_image_rendering.signal_changed().connect( + sigc::mem_fun(this, &ObjectProperties::_imageRenderingChanged) + ); + + + + /* Check boxes */ + Gtk::HBox *hb_checkboxes = Gtk::manage(new Gtk::HBox()); + contents->pack_start(*hb_checkboxes, Gtk::PACK_SHRINK, 0); + + auto grid_cb = Gtk::manage(new Gtk::Grid()); + grid_cb->set_row_homogeneous(); + grid_cb->set_column_homogeneous(true); + + grid_cb->set_border_width(4); + hb_checkboxes->pack_start(*grid_cb, true, true, 0); + + /* Hide */ + _cb_hide.set_tooltip_text (_("Check to make the object invisible")); + _cb_hide.set_hexpand(); + _cb_hide.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_hide, 0, 0, 1, 1); + + _cb_hide.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_hiddenToggled)); + + /* Lock */ + // TRANSLATORS: "Lock" is a verb here + _cb_lock.set_tooltip_text(_("Check to make the object insensitive (not selectable by mouse)")); + _cb_lock.set_hexpand(); + _cb_lock.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_lock, 1, 0, 1, 1); + + _cb_lock.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_sensitivityToggled)); + + /* Preserve aspect ratio */ + _cb_aspect_ratio.set_tooltip_text(_("Check to preserve aspect ratio on images")); + _cb_aspect_ratio.set_hexpand(); + _cb_aspect_ratio.set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(_cb_aspect_ratio, 0, 1, 1, 1); + + _cb_aspect_ratio.signal_toggled().connect(sigc::mem_fun(this, &ObjectProperties::_aspectRatioToggled)); + + + /* Button for setting the object's id, label, title and description. */ + Gtk::Button *btn_set = Gtk::manage(new Gtk::Button(_("_Set"), true)); + btn_set->set_hexpand(); + btn_set->set_valign(Gtk::ALIGN_CENTER); + grid_cb->attach(*btn_set, 1, 1, 1, 1); + + btn_set->signal_clicked().connect(sigc::mem_fun(this, &ObjectProperties::_labelChanged)); + + /* Interactivity options */ + _exp_interactivity.set_vexpand(false); + contents->pack_start(_exp_interactivity, Gtk::PACK_SHRINK); + + show_all(); + update(); +} + +void ObjectProperties::update() +{ + if (_blocked || !_desktop) { + return; + } + if (SP_ACTIVE_DESKTOP != _desktop) { + return; + } + + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + Gtk::Box *contents = _getContents(); + + if (!selection->singleItem()) { + contents->set_sensitive (false); + _current_item = nullptr; + //no selection anymore or multiple objects selected, means that we need + //to close the connections to the previously selected object + _attr_table->clear(); + return; + } else { + contents->set_sensitive (true); + } + + SPItem *item = selection->singleItem(); + if (_current_item == item) + { + //otherwise we would end up wasting resources through the modify selection + //callback when moving an object (endlessly setting the labels and recreating _attr_table) + return; + } + _blocked = true; + _cb_aspect_ratio.set_active(g_strcmp0(item->getAttribute("preserveAspectRatio"), "none") != 0); + _cb_lock.set_active(item->isLocked()); /* Sensitive */ + _cb_hide.set_active(item->isExplicitlyHidden()); /* Hidden */ + + if (item->cloned) { + /* ID */ + _entry_id.set_text(""); + _entry_id.set_sensitive(FALSE); + _label_id.set_text(_("Ref")); + + /* Label */ + _entry_label.set_text(""); + _entry_label.set_sensitive(FALSE); + _label_label.set_text(_("Ref")); + + } else { + SPObject *obj = static_cast<SPObject*>(item); + + /* ID */ + _entry_id.set_text(obj->getId() ? obj->getId() : ""); + _entry_id.set_sensitive(TRUE); + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + + /* Label */ + char const *currentlabel = obj->label(); + char const *placeholder = ""; + if (!currentlabel) { + currentlabel = ""; + placeholder = obj->defaultLabel(); + } + _entry_label.set_text(currentlabel); + _entry_label.set_placeholder_text(placeholder); + _entry_label.set_sensitive(TRUE); + + /* Title */ + gchar *title = obj->title(); + if (title) { + _entry_title.set_text(title); + g_free(title); + } + else { + _entry_title.set_text(""); + } + _entry_title.set_sensitive(TRUE); + + /* Image Rendering */ + if (SP_IS_IMAGE(item)) { + _combo_image_rendering.show(); + _label_image_rendering.show(); + _combo_image_rendering.set_active(obj->style->image_rendering.value); + if (obj->getAttribute("inkscape:svg-dpi")) { + _spin_dpi.set_value(std::stod(obj->getAttribute("inkscape:svg-dpi"))); + _spin_dpi.show(); + _label_dpi.show(); + } else { + _spin_dpi.hide(); + _label_dpi.hide(); + } + } else { + _combo_image_rendering.hide(); + _combo_image_rendering.unset_active(); + _label_image_rendering.hide(); + _spin_dpi.hide(); + _label_dpi.hide(); + } + + /* Description */ + gchar *desc = obj->desc(); + if (desc) { + _tv_description.get_buffer()->set_text(desc); + g_free(desc); + } else { + _tv_description.get_buffer()->set_text(""); + } + _ft_description.set_sensitive(TRUE); + + if (_current_item == nullptr) { + _attr_table->set_object(obj, _int_labels, _int_attrs, (GtkWidget*) _exp_interactivity.gobj()); + } else { + _attr_table->change_object(obj); + } + _attr_table->show_all(); + } + _current_item = item; + _blocked = false; +} + +void ObjectProperties::_labelChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + /* Retrieve the label widget for the object's id */ + gchar *id = g_strdup(_entry_id.get_text().c_str()); + g_strcanon(id, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.:", '_'); + if (g_strcmp0(id, item->getId()) == 0) { + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + } else if (!*id || !isalnum (*id)) { + _label_id.set_text(_("Id invalid! ")); + } else if (SP_ACTIVE_DOCUMENT->getObjectById(id) != nullptr) { + _label_id.set_text(_("Id exists! ")); + } else { + SPException ex; + _label_id.set_markup_with_mnemonic(_("_ID:") + Glib::ustring(" ")); + SP_EXCEPTION_INIT(&ex); + item->setAttribute("id", id, &ex); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set object ID")); + } + g_free(id); + + /* Retrieve the label widget for the object's label */ + Glib::ustring label = _entry_label.get_text(); + + /* Give feedback on success of setting the drawing object's label + * using the widget's label text + */ + SPObject *obj = static_cast<SPObject*>(item); + char const *currentlabel = obj->label(); + if (label.compare(currentlabel ? currentlabel : "")) { + obj->setLabel(label.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object label")); + } + + /* Retrieve the title */ + if (obj->setTitle(_entry_title.get_text().c_str())) { + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object title")); + } + + /* Retrieve the DPI */ + if (SP_IS_IMAGE(obj)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + obj->setAttribute("inkscape:svg-dpi", dpi_value); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set image DPI")); + } + + /* Retrieve the description */ + Gtk::TextBuffer::iterator start, end; + _tv_description.get_buffer()->get_bounds(start, end); + Glib::ustring desc = _tv_description.get_buffer()->get_text(start, end, TRUE); + if (obj->setDesc(desc.c_str())) { + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set object description")); + } + + _blocked = false; +} + +void ObjectProperties::_imageRenderingChanged() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail (item != nullptr); + + _blocked = true; + + Glib::ustring scale = _combo_image_rendering.get_active_text(); + + // We should unset if the parent computed value is auto and the desired value is auto. + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + Inkscape::XML::Node *image_node = item->getRepr(); + if (image_node) { + sp_repr_css_change(image_node, css, "style"); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _("Set image rendering option")); + } + sp_repr_css_attr_unref(css); + + _blocked = false; +} + +void ObjectProperties::_sensitivityToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setLocked(_cb_lock.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_lock.get_active() ? _("Lock object") : _("Unlock object")); + _blocked = false; +} + +void ObjectProperties::_aspectRatioToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + + const char *active; + if (_cb_aspect_ratio.get_active()) { + active = "xMidYMid"; + } + else { + active = "none"; + } + /* Retrieve the DPI */ + if (SP_IS_IMAGE(item)) { + Glib::ustring dpi_value = Glib::ustring::format(_spin_dpi.get_value()); + item->setAttribute("preserveAspectRatio", active); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, _("Set preserve ratio")); + } + _blocked = false; +} + +void ObjectProperties::_hiddenToggled() +{ + if (_blocked) { + return; + } + + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + g_return_if_fail(item != nullptr); + + _blocked = true; + item->setExplicitlyHidden(_cb_hide.get_active()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_ITEM, + _cb_hide.get_active() ? _("Hide object") : _("Unhide object")); + _blocked = false; +} + +void ObjectProperties::_setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + _desktop_tracker.setBase(desktop); +} + +void ObjectProperties::_setTargetDesktop(SPDesktop *desktop) +{ + if (this->_desktop != desktop) { + if (this->_desktop) { + _subselection_changed_connection.disconnect(); + _selection_changed_connection.disconnect(); + } + this->_desktop = desktop; + if (desktop && desktop->selection) { + _selection_changed_connection = desktop->selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + _subselection_changed_connection = desktop->connectToolSubselectionChanged( + sigc::hide(sigc::mem_fun(*this, &ObjectProperties::update)) + ); + } + update(); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/object-properties.h b/src/ui/dialog/object-properties.h new file mode 100644 index 0000000..a1974c0 --- /dev/null +++ b/src/ui/dialog/object-properties.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file Object properties dialog. + */ +/* + * Inkscape, an Open Source vector graphics editor + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (C) 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * c++version based on former C-version (GPL v2+) with authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Abhishek Sharma + */ + +#ifndef SEEN_DIALOGS_ITEM_PROPERTIES_H +#define SEEN_DIALOGS_ITEM_PROPERTIES_H + +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" + +#include <gtkmm/checkbutton.h> +#include <gtkmm/entry.h> +#include <gtkmm/expander.h> +#include <gtkmm/frame.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/textview.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/dialog/desktop-tracker.h" + +class SPAttributeTable; +class SPDesktop; +class SPItem; + +namespace Gtk { +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * A dialog widget to show object properties. + * + * A widget to enter an ID, label, title and description for an object. + * In addition it allows to edit the properties of an object. + */ +class ObjectProperties : public Widget::Panel { +public: + ObjectProperties(); + ~ObjectProperties() override; + + static ObjectProperties &getInstance() { return *new ObjectProperties(); } + + /// Updates entries and other child widgets on selection change, object modification, etc. + void update(); + +private: + bool _blocked; + SPItem *_current_item; //to store the current item, for not wasting resources + std::vector<Glib::ustring> _int_attrs; + std::vector<Glib::ustring> _int_labels; + + Gtk::Label _label_id; //the label for the object ID + Gtk::Entry _entry_id; //the entry for the object ID + Gtk::Label _label_label; //the label for the object label + Gtk::Entry _entry_label; //the entry for the object label + Gtk::Label _label_title; //the label for the object title + Gtk::Entry _entry_title; //the entry for the object title + + Gtk::Label _label_image_rendering; // the label for 'image-rendering' + Gtk::ComboBoxText _combo_image_rendering; // the combo box text for 'image-rendering' + + Gtk::Frame _ft_description; //the frame for the text of the object description + Gtk::TextView _tv_description; //the text view object showing the object description + + Gtk::CheckButton _cb_hide; //the check button hide + Gtk::CheckButton _cb_lock; //the check button lock + Gtk::CheckButton _cb_aspect_ratio; //the preserve aspect ratio of images + + Gtk::Label _label_dpi; //the entry for the dpi value + Gtk::SpinButton _spin_dpi; //the expander for interactivity + Gtk::Expander _exp_interactivity; //the expander for interactivity + SPAttributeTable *_attr_table; //the widget for showing the on... names at the bottom + + SPDesktop *_desktop; + DesktopTracker _desktop_tracker; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + sigc::connection _subselection_changed_connection; + + /// Constructor auxiliary function creating the child widgets. + void _init(); + + /// Sets object properties (ID, label, title, description) on user input. + void _labelChanged(); + + /// Callback for 'image-rendering'. + void _imageRenderingChanged(); + + /// Callback for checkbox Lock. + void _sensitivityToggled(); + + /// Callback for checkbox Hide. + void _hiddenToggled(); + + /// Callback for checkbox Preserve Aspect Ratio. + void _aspectRatioToggled(); + + /// Can be invoked for setting the desktop. Currently not used. + void _setDesktop(SPDesktop *desktop); + + /// Is invoked by the desktop tracker when the desktop changes. + void _setTargetDesktop(SPDesktop *desktop); +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/objects.cpp b/src/ui/dialog/objects.cpp new file mode 100644 index 0000000..f9e14e7 --- /dev/null +++ b/src/ui/dialog/objects.cpp @@ -0,0 +1,2363 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for objects (originally developed for Ponyscape, an Inkscape derivative) + * + * Authors: + * Theodore Janeczko + * Tweaked by Liam P White for use in Inkscape + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "objects.h" + +#include <gtkmm/icontheme.h> +#include <gtkmm/imagemenuitem.h> +#include <gtkmm/separatormenuitem.h> +#include <glibmm/main.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "layer-manager.h" +#include "shortcuts.h" +#include "verbs.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "include/gtkmm_version.h" + +#include "object/filters/blend.h" +#include "object/filters/gaussian-blur.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "style.h" + +#include "ui/contextmenu.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/selected-color.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/clipmaskicon.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/highlight-picker.h" +#include "ui/widget/imagetoggler.h" +#include "ui/widget/insertordericon.h" +#include "ui/widget/layertypeicon.h" + +#include "xml/node-observer.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::XML::Node; + +/** + * Gets an instance of the Objects panel + */ +ObjectsPanel& ObjectsPanel::getInstance() +{ + return *new ObjectsPanel(); +} + +/** + * Column enumeration + */ +enum { + COL_VISIBLE = 1, + COL_LOCKED, + COL_TYPE, +// COL_INSERTORDER, + COL_CLIPMASK, + COL_HIGHLIGHT +}; + +/** + * Button enumeration + */ +enum { + BUTTON_NEW = 0, + BUTTON_RENAME, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DUPLICATE, + BUTTON_DELETE, + BUTTON_SOLO, + BUTTON_SHOW_ALL, + BUTTON_HIDE_ALL, + BUTTON_LOCK_OTHERS, + BUTTON_LOCK_ALL, + BUTTON_UNLOCK_ALL, + BUTTON_SETCLIP, + BUTTON_CLIPGROUP, +// BUTTON_SETINVCLIP, + BUTTON_UNSETCLIP, + BUTTON_SETMASK, + BUTTON_UNSETMASK, + BUTTON_GROUP, + BUTTON_UNGROUP, + BUTTON_COLLAPSE_ALL, + DRAGNDROP, + UPDATE_TREE +}; + +/** + * Xml node observer for observing objects in the document + */ +class ObjectsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { +public: + /** + * Creates a new object watcher + * @param pnl The panel to which the object watcher belongs + * @param obj The object to watch + */ + ObjectWatcher(ObjectsPanel* pnl, SPObject* obj) : + _pnl(pnl), + _obj(obj), + _repr(obj->getRepr()), + _highlightAttr(g_quark_from_string("inkscape:highlight-color")), + _lockedAttr(g_quark_from_string("sodipodi:insensitive")), + _labelAttr(g_quark_from_string("inkscape:label")), + _groupAttr(g_quark_from_string("inkscape:groupmode")), + _styleAttr(g_quark_from_string("style")), + _clipAttr(g_quark_from_string("clip-path")), + _maskAttr(g_quark_from_string("mask")) + { + _repr->addObserver(*this); + } + + ~ObjectWatcher() override { + _repr->removeObserver(*this); + } + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChangedWrapper( _obj ); + } + } + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &node, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + /* Weird things happen on undo! we get notified about the child being removed, but after that we still get + * notified for attributes being changed on this XML node! In that case the corresponding SPObject might already + * have been deleted and the pointer to might be invalid, leading to a segfault if we're not carefull. + * So after we initiated the update of the treeview using _objectsChangedWrapper() in notifyChildRemoved(), the + * _pending_update flag is set, and we will no longer process any notifyAttributeChanged() + * Reproducing the crash: new document -> open objects panel -> draw freehand line -> undo -> segfault (but only + * if we don't check for _pending_update) */ + if ( _pnl && (!_pnl->_pending_update) && _obj ) { + if ( name == _lockedAttr || name == _labelAttr || name == _highlightAttr || name == _groupAttr || name == _styleAttr || name == _clipAttr || name == _maskAttr ) { + _pnl->_updateObject(_obj, name == _highlightAttr); + if ( name == _styleAttr ) { + _pnl->_updateComposite(); + } + } + } + } + + /** + * Objects panel to which this watcher belongs + */ + ObjectsPanel* _pnl; + + /** + * The object that is being observed + */ + SPObject* _obj; + + /** + * The xml representation of the object that is being observed + */ + Inkscape::XML::Node* _repr; + + /* These are quarks which define the attributes that we are observing */ + GQuark _highlightAttr; + GQuark _lockedAttr; + GQuark _labelAttr; + GQuark _groupAttr; + GQuark _styleAttr; + GQuark _clipAttr; + GQuark _maskAttr; +}; + +class ObjectsPanel::InternalUIBounce +{ +public: + int _actionCode; + sigc::connection _signal; +}; + +class ObjectsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colVisible); + add(_colLocked); + add(_colLabel); + add(_colType); + add(_colHighlight); + add(_colClipMask); + add(_colPrevSelectionState); + //add(_colInsertOrder); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<SPItem*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colVisible; + Gtk::TreeModelColumn<bool> _colLocked; + Gtk::TreeModelColumn<int> _colType; + Gtk::TreeModelColumn<guint32> _colHighlight; + Gtk::TreeModelColumn<int> _colClipMask; + Gtk::TreeModelColumn<bool> _colPrevSelectionState; + //Gtk::TreeModelColumn<int> _colInsertOrder; +}; + +/** + * Stylizes a button using the given icon name and tooltip + */ +void ObjectsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show( child ); + btn.add( *Gtk::manage(Glib::wrap(child)) ); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + +/** + * Adds an item to the pop-up (right-click) menu + * @param desktop The active destktop + * @param code Action code + * @param id Button id for callback function + * @return The generated menu item + */ +Gtk::MenuItem& ObjectsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, int id ) +{ + Verb *verb = Verb::get( code ); + g_assert(verb); + SPAction *action = verb->get_action(Inkscape::ActionContext(desktop)); + + Gtk::MenuItem* item = Gtk::manage(new Gtk::MenuItem()); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(action->name, true)); + label->set_xalign(0.0); + + if (_show_contextmenu_icons && action->image) { + item->set_name("ImageMenuItem"); // custom name to identify our "ImageMenuItems" + Gtk::Image *icon = Gtk::manage(sp_get_icon_image(action->image, Gtk::ICON_SIZE_MENU)); + + // Create a box to hold icon and label as Gtk::MenuItem derives from GtkBin and can only hold one child + Gtk::Box *box = Gtk::manage(new Gtk::Box()); + box->pack_start(*icon, false, false, 0); + box->pack_start(*label, true, true, 0); + item->add(*box); + } else { + item->add(*label); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &ObjectsPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +/** + * Attach a watcher to the XML node of an item, which will signal us in case of changes to that item or node + * @param item The item of which the XML node is to be watched + */ +void ObjectsPanel::_addWatcher(SPItem *item) { + bool used = true; // Any newly created watcher is obviously being used + auto iter = _objectWatchers.find(item); + if (iter == _objectWatchers.end()) { // If not found then watcher doesn't exist yet + ObjectsPanel::ObjectWatcher *w = new ObjectsPanel::ObjectWatcher(this, item); + _objectWatchers.emplace(item, std::make_pair(w, used)); + } else { // Found; no need to create a new watcher; just flag it as "in use" + (*iter).second.second = used; + } +} + +/** + * Delete the watchers, which signal us in case of changes to the item being watched + * @param only_unused Only delete those watchers that are no longer in use + */ +void ObjectsPanel::_removeWatchers(bool only_unused = false) { + // Delete all watchers (optionally only those which are not in use) + auto iter = _objectWatchers.begin(); + while (iter != _objectWatchers.end()) { + bool used = (*iter).second.second; + bool delete_watcher = (!only_unused) || (only_unused && !used); + if ( delete_watcher ) { + ObjectsPanel::ObjectWatcher *w = (*iter).second.first; + delete w; + iter = _objectWatchers.erase(iter); + } else { + // It must be in use, so the used "field" should be set to true; + // However, when _removeWatchers is being called, we will already have processed the complete queue ... + g_assert(_tree_update_queue.empty()); + // .. and we can preemptively flag it as unused for the processing of the next queue + (*iter).second.second = false; // It will be set to true again by _addWatcher, if in use + iter++; + } + } +} +/** + * Call function for asynchronous invocation of _objectsChanged + */ +void ObjectsPanel::_objectsChangedWrapper(SPObject */*obj*/) { + // We used to call _objectsChanged with a reference to _obj, + // but since _obj wasn't used, I'm dropping that for now + _takeAction(UPDATE_TREE); +} + +/** + * Callback function for when an object changes. Essentially refreshes the entire tree + * @param obj Object which was changed (currently not used as the entire tree is recreated) + */ +void ObjectsPanel::_objectsChanged(SPObject */*obj*/) +{ + if (_desktop) { + //Get the current document's root and use that to enumerate the tree + SPDocument* document = _desktop->doc(); + SPRoot* root = document->getRoot(); + if ( root ) { + _selectedConnection.block(); // Will be unblocked after the queue has been processed fully + _documentChangedCurrentLayer.block(); + + //Clear the tree store + _store->clear(); // This will increment it's stamp, making all old iterators + _tree_cache.clear(); // invalid. So we will also clear our own cache, as well + _tree_update_queue.clear(); // as any remaining update queue + + // Temporarily detach the TreeStore from the TreeView to slightly reduce flickering, and to speed up + // Note: if we truly want to eliminate the flickering, we should implement double buffering on the _store, + // but maybe this is a bit too much effort/bloat for too little gain? + _tree.unset_model(); + + //Add all items recursively; we will do this asynchronously, by first filling a queue, which is rather fast + _queueObject( root, nullptr ); + //However, the processing of this queue is slow, so this is done at a low priority and in small chunks. Using + //only small chunks keeps Inkscape responsive, for example while using the spray tool. After processing each + //of the chunks, Inkscape will check if there are other tasks with a high priority, for example when user is + //spraying. If so, the sprayed objects will be added first, and the whole updating will be restarted before + //it even finished. + _paths_to_be_expanded.clear(); + _processQueue_sig.disconnect(); // Might be needed in case objectsChanged is called directly, and not through objectsChangedWrapper() + _processQueue_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_processQueue), 0, Glib::PRIORITY_DEFAULT_IDLE+100); + } + } +} + +/** + * Recursively adds the children of the given item to the tree + * @param obj Root object to add to the tree + * @param parentRow Parent tree row (or NULL if adding to tree root) + */ +void ObjectsPanel::_queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow) +{ + bool already_expanded = false; + + for(auto& child: obj->children) { + if (SP_IS_ITEM(&child)) { + //Add the item to the tree, basically only creating an empty row in the tree view + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + + //Add the item to a queue, so we can fill in the data in each row asynchronously + //at a later stage. See the comments in _objectsChanged() for more details + bool expand = SP_IS_GROUP(obj) && SP_GROUP(obj)->expanded() && (not already_expanded); + _tree_update_queue.emplace_back(SP_ITEM(&child), iter, expand); + + already_expanded = expand || already_expanded; // We need to expand only a single child in each group + + //If the item is a group, recursively add its children + if (SP_IS_GROUP(&child)) { + Gtk::TreeModel::Row row = *iter; + _queueObject(&child, &row); + } + } + } +} + +/** + * Walks through the queue in small chunks, and fills in the rows in the tree view accordingly + * @return False if the queue has been fully emptied + */ +bool ObjectsPanel::_processQueue() { + auto queue_iter = _tree_update_queue.begin(); + auto queue_end = _tree_update_queue.end(); + int count = 0; + + while (queue_iter != queue_end) { + //The queue is a list of tuples; expand the tuples + SPItem *item = std::get<0>(*queue_iter); + Gtk::TreeModel::iterator iter = std::get<1>(*queue_iter); + bool expanded = std::get<2>(*queue_iter); + //Add the object to the tree view and tree cache + _addObjectToTree(item, *iter, expanded); + _tree_cache.emplace(item, *iter); + + /* Update the watchers; No watcher shall be deleted before the processing of the queue has + * finished; we need to keep watching for items that might have been deleted while the queue, + * which is being processed on idle, was not yet empty. This is because when an item is deleted, the + * queue is still holding a pointer to it. The NotifyChildRemoved method of the watcher will stop the + * processing of the queue and prevent a segmentation fault, but only if there is a watcher in place*/ + _addWatcher(item); + + queue_iter = _tree_update_queue.erase(queue_iter); + count++; + if (count == 100 && (!_tree_update_queue.empty())) { + return true; // we have not yet reached the end of the queue, so return true to keep the timeout signal alive + } + } + + //We have reached the end of the queue, and it is safe to remove any watchers + _removeWatchers(true); // ... but only remove those that are no longer in use + + // Now we can bring the tree view back to life safely + _tree.set_model(_store); // Attach the store again to the tree view this sets search columns as -1 + _tree.set_search_column(_model->_colLabel);//set search column again + + // Expand the tree; this is kept outside of _addObjectToTree() and _processQueue() to allow + // temporarily detaching the store from the tree, which slightly reduces flickering + for (auto path: _paths_to_be_expanded) { + _tree.expand_to_path(path); + _tree.collapse_row(path); + } + + _blockAllSignals(false); + _objectsSelected(_desktop->selection); //Set the tree selection; will also invoke _checkTreeSelection() + _pending_update = false; + return false; // Return false to kill the timeout signal that kept calling _processQueue +} + +/** + * Fills in the details of an item in the already existing row of the tree view + * @param item Item of which the name, visibility, lock status, etc, will be filled in + * @param row Row where the item is residing + * @param expanded True if the item is part of a group that is shown as expanded in the tree view + */ +void ObjectsPanel::_addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &row, bool expanded) +{ + SPGroup * group = SP_IS_GROUP(item) ? SP_GROUP(item) : nullptr; + + row[_model->_colObject] = item; + gchar const * label = item->label() ? item->label() : item->getId(); + row[_model->_colLabel] = label ? label : item->defaultLabel(); + row[_model->_colVisible] = !item->isHidden(); + row[_model->_colLocked] = !item->isSensitive(); + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + row[_model->_colPrevSelectionState] = false; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + //If our parent object is a group and it's expanded, expand the tree + if (expanded) { + _paths_to_be_expanded.emplace_back(_store->get_path(row)); + } +} + +/** + * Updates an item in the tree and optionally recursively updates the item's children + * @param obj The item to update in the tree + * @param recurse Whether to recurse through the item's children + */ +void ObjectsPanel::_updateObject( SPObject *obj, bool recurse ) { + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(SP_ITEM(obj), tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found our item in the tree; now update it! + SPItem * item = SP_IS_ITEM(obj) ? SP_ITEM(obj) : nullptr; + SPGroup * group = SP_IS_GROUP(obj) ? SP_GROUP(obj) : nullptr; + + gchar const * label = obj->label() ? obj->label() : obj->getId(); + row[_model->_colLabel] = label ? label : obj->defaultLabel(); + row[_model->_colVisible] = item ? !item->isHidden() : false; + row[_model->_colLocked] = item ? !item->isSensitive() : false; + row[_model->_colType] = group ? (group->layerMode() == SPGroup::LAYER ? 2 : 1) : 0; + row[_model->_colHighlight] = item ? (item->isHighlightSet() ? item->highlight_color() : item->highlight_color() & 0xffffff00) : 0; + row[_model->_colClipMask] = item ? ( + (item->getClipObject() ? 1 : 0) | + (item->getMaskObject() ? 2 : 0) + ) : 0; + //row[_model->_colInsertOrder] = group ? (group->insertBottom() ? 2 : 1) : 0; + + if (recurse){ + for (auto& iter: obj->children) { + _updateObject(&iter, recurse); + } + } + } +} + +/** + * Updates the composite controls for the selected item + */ +void ObjectsPanel::_updateComposite() { + if (!_blockCompositeUpdate) + { + //Set the default values + bool setValues = true; + + //Get/set the values + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_compositingChanged), &setValues)); + + } +} + +/** + * Sets the compositing values for the first selected item in the tree + * @param iter Current tree item + * @param setValues Whether to set the compositing values + */ +void ObjectsPanel::_compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ) +{ + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (*setValues) + { + _setCompositingValues(item); + *setValues = false; + } + } +} + +/** + * Occurs when the current desktop selection changes + * @param sel The current selection + */ +void ObjectsPanel::_objectsSelected( Selection *sel ) { + + bool setOpacity = true; + _selectedConnection.block(); + + _tree.get_selection()->unselect_all(); + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + + SPItem *item = nullptr; + auto items = sel->items(); + for(auto i=items.begin(); i!=items.end(); ++i){ + item = *i; + if (setOpacity) + { + _setCompositingValues(item); + setOpacity = false; + } + _updateObjectSelected(item, (*i)==items.back(), false); + } + if (!item) { + if (_desktop->currentLayer() && SP_IS_ITEM(_desktop->currentLayer())) { + item = SP_ITEM(_desktop->currentLayer()); + _setCompositingValues(item); + _updateObjectSelected(item, false, true); + } + } + _selectedConnection.unblock(); + _checkTreeSelection(); +} + +/** + * Helper function for setting the compositing values + * @param item Item to use for setting the compositing values + */ +void ObjectsPanel::_setCompositingValues(SPItem *item) +{ + // Block the connections to avoid interference + _isolationConnection.block(); + _opacityConnection.block(); + _blendConnection.block(); + _blurConnection.block(); + + // Set the isolation + auto isolation = item->style->isolation.set ? item->style->isolation.value : SP_CSS_ISOLATION_AUTO; + _filter_modifier.set_isolation_mode(isolation, true); + // Set the opacity + double opacity = (item->style->opacity.set ? SP_SCALE24_TO_FLOAT(item->style->opacity.value) : 1); + opacity *= 100; // Display in percent. + _filter_modifier.set_opacity_value(opacity); + // Set the blend mode + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, true); + } else { + _filter_modifier.set_blend_mode(item->style->mix_blend_mode.value, true); + } + if (_filter_modifier.get_blend_mode() == SP_CSS_BLEND_NORMAL) { + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + auto blend = filter_get_legacy_blend(item); + _filter_modifier.set_blend_mode(blend, true); + } + } + SPGaussianBlur *spblur = nullptr; + if (item->style->getFilter()) { + for (auto& primitive_obj: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive_obj)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive_obj) && !spblur) { + //Get the blur value + spblur = SP_GAUSSIANBLUR(&primitive_obj); + } + } + } + + //Set the blur value + double blur_value = 0; + if (spblur) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); // calculating the bbox is expensive; only do this if we have an spblur in the first place + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + blur_value = spblur->stdDeviation.getNumber() * 400 / perimeter; + } + } + _filter_modifier.set_blur_value(blur_value); + + //Unblock connections + _isolationConnection.unblock(); + _blurConnection.unblock(); + _blendConnection.unblock(); + _opacityConnection.unblock(); +} + +// See the comment in objects.h for _tree_cache +/** + * Find the specified item in the tree cache + * @param iter Current tree item + * @param tree_iter Tree_iter will point to the row in which the tree item was found + * @return True if found + */ +bool ObjectsPanel::_findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter) { + if (not item) { + return false; + } + + try { + tree_iter = _tree_cache.at(item); + } + catch (std::out_of_range) { + // Apparently, item cannot be found in the tree_cache, which could mean that + // - the tree and/or tree_cache are out-dated or in the process of being updated. + // - a layer is selected, which is not visible in the objects panel (see _objectsSelected()) + // Anyway, this doesn't seem all that critical, so no warnings; just return false + return false; + } + + /* If the row in the tree has been deleted, and an old tree_cache is being used, then we will + * get a segmentation fault crash somewhere here; so make sure iters don't linger around! + * We can only check the validity as done below, but this is rather slow according to the + * documentation (adds 0.25 s for a 2k long tree). But better safe than sorry + */ + if (not _store->iter_is_valid(tree_iter)) { + g_critical("Invalid iterator to Gtk::tree in objects panel; just prevented a segfault!"); + return false; + } + + return true; +} + + +/** + * Find the specified item in the tree store and (de)select it, optionally scrolling to the item + * @param item Item to select in the tree + * @param scrollto Whether to scroll to the item + * @param expand If true, the path in the tree towards item will be expanded + */ +void ObjectsPanel::_updateObjectSelected(SPItem* item, bool scrollto, bool expand) +{ + Gtk::TreeModel::iterator tree_iter; + if (_findInTreeCache(item, tree_iter)) { + Gtk::TreeModel::Row row = *tree_iter; + + //We found the item! Expand to the path and select it in the tree. + Gtk::TreePath path = _store->get_path(tree_iter); + _tree.expand_to_path( path ); + if (!expand) + // but don't expand itself, just the path + _tree.collapse_row(path); + + Glib::RefPtr<Gtk::TreeSelection> select = _tree.get_selection(); + + select->select(tree_iter); + row[_model->_colPrevSelectionState] = true; + if (scrollto) { + //Scroll to the item in the tree + _tree.scroll_to_row(path, 0.5); + } + } +} + +/** + * Pushes the current tree selection to the canvas + */ +void ObjectsPanel::_pushTreeSelectionToCurrent() +{ + if ( _desktop && _desktop->currentRoot() ) { + //block connections for selection and compositing values to prevent interference + _selectionChangedConnection.block(); + _documentChangedCurrentLayer.block(); + //Clear the selection and then iterate over the tree selection, pushing each item to the desktop + _desktop->selection->clear(); + if (_tree.get_selection()->count_selected_rows() == 0) { + _store->foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_clearPrevSelectionState)); + } + bool setOpacity = true; + bool first_pass = true; + _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + first_pass = false; + _store->foreach_iter(sigc::bind<bool *>(sigc::mem_fun(*this, &ObjectsPanel::_selectItemCallback), &setOpacity, &first_pass)); + + //unblock connections, unless we were already blocking them beforehand + _selectionChangedConnection.unblock(); + _documentChangedCurrentLayer.unblock(); + + _checkTreeSelection(); + } +} + +/** + * Helper function for pushing the current tree selection to the current desktop + * @param iter Current tree item + * @param setCompositingValues Whether to set the compositing values + */ +bool ObjectsPanel::_selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setCompositingValues, bool *first_pass) +{ + Gtk::TreeModel::Row row = *iter; + bool selected = _tree.get_selection()->is_selected(iter); + if (selected) { // All items selected in the treeview will be added to the current selection + /* Adding/removing only the items that were selected or deselected since the previous call to _pushTreeSelectionToCurrent() + * is very slow on large documents, because _desktop->selection->remove(item) needs to traverse the whole ObjectSet to find + * the item to be removed. When all N objects are selected in a document, clearing the whole selection would require O(N^2) + * That's why we simply clear the complete selection using _desktop->selection->clear(), and re-add all items one by one. + * This is much faster. + */ + + /* On the first pass, we will add only the items that were selected before too. Then, on the second pass, we will add the + * newly selected items such that the last selected items will be actually last. This is needed for example when the user + * wants to align relative to the last selected item. + */ + if (*first_pass == row[_model->_colPrevSelectionState]) { + SPItem *item = row[_model->_colObject]; + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) { + //If the item is not a layer, then select it and set the current layer to its parent (if it's the first item) + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item->parent); + } + _desktop->selection->add(item); + } else { + //If the item is a layer, set the current layer + if (_desktop->selection->isEmpty()) { + _desktop->setCurrentLayer(item); + } + } + if (*setCompositingValues) { + //Only set the compositing values for the first item <-- TODO: We have this comment here, but this has not actually been implemented? + _setCompositingValues(item); + *setCompositingValues = false; + } + } + } + + if (not *first_pass) { + row[_model->_colPrevSelectionState] = selected; + } + + return false; +} + +bool ObjectsPanel::_clearPrevSelectionState( const Gtk::TreeModel::iterator& iter) { + Gtk::TreeModel::Row row = *iter; + row[_model->_colPrevSelectionState] = false; + SPItem *item = row[_model->_colObject]; + return false; +} + +/** + * Handles button sensitivity + */ +void ObjectsPanel::_checkTreeSelection() +{ + bool sensitive = _tree.get_selection()->count_selected_rows() > 0; + //TODO: top/bottom sensitivity + bool sensitiveNonTop = true; + bool sensitiveNonBottom = true; + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } + + _tree.set_reorderable(sensitive); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected +} + +/** + * Sets visibility of items in the tree + * @param iter Current item in the tree + * @param visible Whether the item should be visible or not + */ +void ObjectsPanel::_setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setHidden( !visible ); + row[_model->_colVisible] = visible; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Sets sensitivity of items in the tree + * @param iter Current item in the tree + * @param locked Whether the item should be locked + */ +void ObjectsPanel::_setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->setLocked( locked ); + row[_model->_colLocked] = locked; + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Handles keyboard events + * @param event Keyboard event passed in from GDK + * @return Whether the event should be eaten (om nom nom) + */ +bool ObjectsPanel::_handleKeyEvent(GdkEventKey *event) +{ + if (!_desktop) + return false; + + unsigned int shortcut; + shortcut = sp_shortcut_get_for_event(event); + + switch (shortcut) { + // how to get users key binding for the action “start-interactive-search†?? + // ctrl+f is just the default + case GDK_KEY_f | SP_SHORTCUT_CONTROL_MASK: + return false; + break; + // shall we slurp ctrl+w to close panel? + + // defocus: + case GDK_KEY_Escape: + if (_desktop->canvas) { + gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); + return true; + } + break; + } + + // invoke user defined shortcuts first + bool done = sp_shortcut_invoke(shortcut, _desktop); + if (done) + return true; + + // handle events for the treeview + bool empty = _desktop->selection->isEmpty(); + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn *focus_column = nullptr; + + _tree.get_cursor(path, focus_column); + if (focus_column == _name_column && !_text_renderer->property_editable()) { + //Rename item + _text_renderer->property_editable() = true; + _tree.set_cursor(path, *_name_column, true); + grab_focus(); + return true; + } + return false; + break; + } + } + return false; +} + +/** + * Handles mouse events + * @param event Mouse event from GDK + * @return whether to eat the event (om nom nom) + */ +bool ObjectsPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + static bool overVisible = false; + + //Right mouse button was clicked, launch the pop-up menu + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); + _popupMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + + if (_tree.get_selection()->is_selected(path)) { + return true; + } + } + } + + //Left mouse button was pressed! In order to handle multiple item drag & drop, + //we need to defer selection by setting the select function so that the tree doesn't + //automatically select anything. In order to handle multiple item icon clicking, + //we need to eat the event. There might be a better way to do both of these... + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { + overVisible = false; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_VISIBLE-1)) { + //Click on visible column, eat this event to keep row selection + overVisible = true; + return true; + } else if (col == _tree.get_column(COL_LOCKED-1) || + col == _tree.get_column(COL_TYPE-1) || + //col == _tree.get_column(COL_INSERTORDER - 1) || + col == _tree.get_column(COL_HIGHLIGHT-1)) { + //Click on an icon column, eat this event to keep row selection + return true; + } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { + //Click on a selected item with no modifiers, defer selection to the mouse-up by + //setting the select function to _noSelection + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_noSelection)); + _defer_target = path; + } + } + } + + //Restore the selection function to allow tree selection on mouse button release + if ( event->type == GDK_BUTTON_RELEASE) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction)); + } + + //CellRenderers do not have good support for dealing with multiple items, so + //we handle all events on them here + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (_defer_target) { + //We had deferred a selection target, select it here (assuming no drag & drop) + if (_defer_target == path && !(event->x == 0 && event->y == 0)) + { + _tree.set_cursor(path, *col, false); + } + _defer_target = Gtk::TreeModel::Path(); + } + else { + if (event->state & GDK_SHIFT_MASK) { + // Shift left click on the visible/lock columns toggles "solo" mode + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _takeAction(BUTTON_SOLO); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _takeAction(BUTTON_LOCK_OTHERS); + } + } else if (event->state & GDK_MOD1_MASK) { + // Alt+left click on the visible/lock columns toggles "solo" mode and preserves selection + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (col == _tree.get_column(COL_VISIBLE - 1)) { + _desktop->toggleLayerSolo( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:solo", SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + _desktop->toggleLockOtherLayers( item ); + DocumentUndo::maybeDone(_desktop->doc(), "layer:lockothers", SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + } + } else { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + + if (col == _tree.get_column(COL_VISIBLE - 1)) { + if (overVisible) { + //Toggle visibility + bool newValue = !row[_model->_colVisible]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the visibility + //for all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setVisibleIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its visibility + row[_model->_colVisible] = newValue; + item->setHidden(!newValue); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Unhide objects") : _("Hide objects")); + overVisible = false; + } + } else if (col == _tree.get_column(COL_LOCKED - 1)) { + //Toggle locking + bool newValue = !row[_model->_colLocked]; + if (_tree.get_selection()->is_selected(path)) + { + //If the current row is selected, toggle the sensitivity for + //all selected items + _tree.get_selection()->selected_foreach_iter(sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setLockedIter), newValue)); + } + else + { + //If the current row is not selected, toggle just its sensitivity + row[_model->_colLocked] = newValue; + item->setLocked( newValue ); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Lock objects") : _("Unlock objects")); + + } else if (col == _tree.get_column(COL_TYPE - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item between a group and a layer + SPGroup * g = SP_GROUP(item); + bool newValue = g->layerMode() == SPGroup::LAYER; + row[_model->_colType] = newValue ? 1: 2; + g->setLayerMode(newValue ? SPGroup::GROUP : SPGroup::LAYER); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Layer to group") : _("Group to layer")); + _pushTreeSelectionToCurrent(); + } + } /*else if (col == _tree.get_column(COL_INSERTORDER - 1)) { + if (SP_IS_GROUP(item)) + { + //Toggle the current item's insert order + SPGroup * g = SP_GROUP(item); + bool newValue = !g->insertBottom(); + row[_model->_colInsertOrder] = newValue ? 2: 1; + g->setInsertBottom(newValue); + g->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_OBJECTS, + newValue? _("Set insert mode bottom") : _("Set insert mode top")); + } + }*/ else if (col == _tree.get_column(COL_HIGHLIGHT - 1)) { + //Clear the highlight targets + _highlight_target.clear(); + if (_tree.get_selection()->is_selected(path)) + { + //If the current item is selected, store all selected items + //in the highlight source + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeHighlightTarget)); + } else { + //If the current item is not selected, store only it in the highlight source + _storeHighlightTarget(iter); + } + if (_selectedColor) + { + //Set up the color selector + SPColor color; + color.set( row[_model->_colHighlight] ); + _selectedColor->setColorAlpha(color, SP_RGBA32_A_F(row[_model->_colHighlight])); + } + //Show the color selector dialog + _colorSelectorDialog.show(); + } + } + } + } + } + + //Second mouse button press, set double click status for when the mouse is released + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + //Double click on mouse button release, if we're over the label column, edit + //the item name + if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { + doubleclick = 0; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { + // Double click on the Layer name, enable editing + _text_renderer->property_editable() = true; + _tree.set_cursor (path, *_name_column, true); + grab_focus(); + } + } + + return false; +} + +/** + * Stores items in the highlight target vector to manipulate with the color selector + * @param iter Current tree item to store + */ +void ObjectsPanel::_storeHighlightTarget(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + _highlight_target.push_back(item); + } +} + +/* + * Drag and drop within the tree + */ +bool ObjectsPanel::_handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& /*context*/, int x, int y, guint /*time*/) +{ + //Set up our defaults and clear the source vector + _dnd_into = false; + _dnd_target = nullptr; + _dnd_source.clear(); + _dnd_source_includes_layer = false; + + //Add all selected items to the source vector + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_storeDragSource)); + + bool cancel_dnd = false; + bool dnd_to_top_at_end = false; + + Gtk::TreeModel::Path target_path; + Gtk::TreeViewDropPosition pos; + if (_tree.get_dest_row_at_pos(x, y, target_path, pos)) { + // SPItem::moveTo() will be used to move the selected items to their new position, but + // moveTo() can only "drop before"; we therefore need to find the next path and drop + // the selection just before it, instead of "dropping after" the target path + if (pos == Gtk::TREE_VIEW_DROP_AFTER) { + Gtk::TreeModel::Path next_path = target_path; + if (_tree.row_expanded(next_path)) { + next_path.down(); // The next path is at a lower level in the hierarchy, i.e. in a layer or group + } else { + next_path.next(); // The next path is at the same level + } + // A next path might however not be present, if we're dropping at the end of the tree view + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" of the treeview ; we'll get the parent group or layer of the last + // item, and drop into that parent + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into the parent of the last item + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level, completely at the end of the treeview; + dnd_to_top_at_end = true; + } + } + } + + if (dnd_to_top_at_end) { + g_assert(_dnd_target == nullptr); + } else { + // Find the SPItem corresponding to the target_path/row at which we're dropping our selection + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + _dnd_target = row[_model->_colObject]; //Set the drop target + if ((pos == Gtk::TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == Gtk::TREE_VIEW_DROP_INTO_OR_AFTER)) { + // Trying to drop into a layer or group + if (SP_IS_GROUP(_dnd_target)) { + _dnd_into = true; + } else { + // If the target is not a group (or layer), then we cannot drop into it (unless we + // would create a group on the fly), so we will cancel the drag and drop action. + cancel_dnd = true; + } + } + // If the source selection contains a layer however, then it can not be dropped ... + bool c1 = target_path.size() > 1; // .. below the top-level + bool c2 = SP_IS_GROUP(_dnd_target) and _dnd_into; // .. or in any group (at the top level) + if (_dnd_source_includes_layer and (c1 or c2)) { + cancel_dnd = true; + } + } else { + cancel_dnd = true; + } + } + } + + if (not cancel_dnd) { + _takeAction(DRAGNDROP); + } + + return true; // If True: then we're signaling here that nothing needs to be done by the TreeView; we're updating ourselves.. +} + +/** + * Stores all selected items as the drag source + * @param iter Current tree item + */ +void ObjectsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) { + _dnd_source.push_back(item); + if (SP_IS_GROUP(item) && (SP_GROUP(item)->layerMode() == SPGroup::LAYER)) { + _dnd_source_includes_layer = true; + } + } +} + +/* + * Move a selection of items in response to a drag & drop action + */ +void ObjectsPanel::_doTreeMove( ) +{ + g_assert(_desktop != nullptr); + g_assert(_document != nullptr); + + std::vector<gchar *> idvector; + + //Clear the desktop selection + _desktop->selection->clear(); + while (!_dnd_source.empty()) + { + SPItem *obj = _dnd_source.back(); + _dnd_source.pop_back(); + + if (obj != _dnd_target) { + //Store the object id (for selection later) and move the object + idvector.push_back(g_strdup(obj->getId())); + obj->moveTo(_dnd_target, _dnd_into); + } + } + //Select items + while (!idvector.empty()) { + //Grab the id from the vector, get the item in the document and select it + gchar * id = idvector.back(); + idvector.pop_back(); + SPObject *obj = _document->getObjectById(id); + g_free(id); + if (obj && SP_IS_ITEM(obj)) { + SPItem *item = SP_ITEM(obj); + if (!SP_IS_GROUP(item) || SP_GROUP(item)->layerMode() != SPGroup::LAYER) + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); + _desktop->selection->add(item); + } + else + { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item); + } + } + } + + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Moved objects")); +} + +/** + * Prevents the treeview from emiting and responding to most signals; needed when it's not up to date + */ +void ObjectsPanel::_blockAllSignals(bool should_block = true) { + + // incoming signals + _documentChangedCurrentLayer.block(should_block); + _isolationConnection.block(should_block); + _opacityConnection.block(should_block); + _blendConnection.block(should_block); + _blurConnection.block(should_block); + if (_pending && should_block) { + // Kill any pending UI event, e.g. a delete or drag 'n drop action, which could + // become unpredictable after the tree has been updated + _pending->_signal.disconnect(); + } + + _selectionChangedConnection.block(should_block); + // outgoing signal + _selectedConnection.block(should_block); + + // These are not blocked: desktopChangeConn, _documentChangedConnection +} + +/** + * Fires the action verb + */ +void ObjectsPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(_desktop); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +bool ObjectsPanel::_executeUpdate() { + _objectsChanged(nullptr); + return false; +} + +/** + * Executes the given button action during the idle time + */ +void ObjectsPanel::_takeAction( int val ) +{ + if (val == UPDATE_TREE) { + _pending_update = true; + // We might already have been updating the tree, but new data is available now + // so we will then first cancel the old update before scheduling a new one + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _blockAllSignals(true); + //_store->clear(); + _tree_cache.clear(); + _executeUpdate_sig = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeUpdate), 500, Glib::PRIORITY_DEFAULT_IDLE+50); + // In the spray tool, updating the tree competes in priority with the redrawing of the canvas, + // see SPCanvas::addIdle(), which is set to UPDATE_PRIORITY (=G_PRIORITY_DEFAULT_IDLE). We + // should take a lower priority (= higher value) to keep the spray tool updating longer, and to prevent + // the objects-panel from clogging the processor; however, once the spraying slows down, the tree might + // get updated anyway. + } else if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + _pending->_signal = Glib::signal_timeout().connect( sigc::mem_fun(*this, &ObjectsPanel::_executeAction), 0 ); + } +} + +/** + * Executes the pending button action + */ +bool ObjectsPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _document && _pending) + { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_LAYER_NEW ); + } + break; + case BUTTON_RENAME: + { + _fireAction( SP_VERB_LAYER_RENAME ); + } + break; + case BUTTON_TOP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_TOP ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_FRONT); + } + } + break; + case BUTTON_BOTTOM: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_TO_BOTTOM ); + } + else + { + _fireAction( SP_VERB_SELECTION_TO_BACK); + } + } + break; + case BUTTON_UP: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_RAISE ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_UP ); + } + } + break; + case BUTTON_DOWN: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_LOWER ); + } + else + { + _fireAction( SP_VERB_SELECTION_STACK_DOWN ); + } + } + break; + case BUTTON_DUPLICATE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DUPLICATE ); + } + else + { + _fireAction( SP_VERB_EDIT_DUPLICATE ); + } + } + break; + case BUTTON_DELETE: + { + if (_desktop->selection->isEmpty()) + { + _fireAction( SP_VERB_LAYER_DELETE ); + } + else + { + _fireAction( SP_VERB_EDIT_DELETE ); + } + } + break; + case BUTTON_SOLO: + { + _fireAction( SP_VERB_LAYER_SOLO ); + } + break; + case BUTTON_SHOW_ALL: + { + _fireAction( SP_VERB_LAYER_SHOW_ALL ); + } + break; + case BUTTON_HIDE_ALL: + { + _fireAction( SP_VERB_LAYER_HIDE_ALL ); + } + break; + case BUTTON_LOCK_OTHERS: + { + _fireAction( SP_VERB_LAYER_LOCK_OTHERS ); + } + break; + case BUTTON_LOCK_ALL: + { + _fireAction( SP_VERB_LAYER_LOCK_ALL ); + } + break; + case BUTTON_UNLOCK_ALL: + { + _fireAction( SP_VERB_LAYER_UNLOCK_ALL ); + } + break; + case BUTTON_CLIPGROUP: + { + _fireAction ( SP_VERB_OBJECT_CREATE_CLIP_GROUP ); + } + case BUTTON_SETCLIP: + { + _fireAction( SP_VERB_OBJECT_SET_CLIPPATH ); + } + break; + case BUTTON_UNSETCLIP: + { + _fireAction( SP_VERB_OBJECT_UNSET_CLIPPATH ); + } + break; + case BUTTON_SETMASK: + { + _fireAction( SP_VERB_OBJECT_SET_MASK ); + } + break; + case BUTTON_UNSETMASK: + { + _fireAction( SP_VERB_OBJECT_UNSET_MASK ); + } + break; + case BUTTON_GROUP: + { + _fireAction( SP_VERB_SELECTION_GROUP ); + } + break; + case BUTTON_UNGROUP: + { + _fireAction( SP_VERB_SELECTION_UNGROUP ); + } + break; + case BUTTON_COLLAPSE_ALL: + { + for (auto& obj: _document->getRoot()->children) { + if (SP_IS_GROUP(&obj)) { + _setCollapsed(SP_GROUP(&obj)); + } + } + _objectsChanged(_document->getRoot()); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + // The notifyChildOrderChanged signal will ensure that the TreeView gets updated + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + +/** + * Handles an unsuccessful item label edit (escape pressed, etc.) + */ +void ObjectsPanel::_handleEditingCancelled() +{ + _text_renderer->property_editable() = false; +} + +/** + * Handle a successful item label edit + * @param path Tree path of the item currently being edited + * @param new_text New label text + */ +void ObjectsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameObject(row, new_text); + _text_renderer->property_editable() = false; +} + +/** + * Renames an item in the tree + * @param row Tree row + * @param name New label to give to the item + */ +void ObjectsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop) { + SPItem* item = row[_model->_colObject]; + if ( item ) { + gchar const* oldLabel = item->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + item->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } + } +} + +/** + * A row selection function used by the tree that doesn't allow any new items to be selected. + * Currently, this is used to allow multi-item drag & drop. + */ +bool ObjectsPanel::_noSelection( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) +{ + return false; +} + +/** + * Default row selection function taken from the layers dialog + */ +bool ObjectsPanel::_rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast<GdkEventButton const*>(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast<GdkEventButton const*>(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +/** + * Sets a group to be collapsed and recursively collapses its children + * @param group The group to collapse + */ +void ObjectsPanel::_setCollapsed(SPGroup * group) +{ + group->setExpanded(false); + group->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + for (auto& iter: group->children) { + if (SP_IS_GROUP(&iter)) { + _setCollapsed(SP_GROUP(&iter)); + } + } +} + +/** + * Sets a group to be expanded or collapsed + * @param iter Current tree item + * @param isexpanded Whether to expand or collapse + */ +void ObjectsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) +{ + Gtk::TreeModel::Row row = *iter; + + SPItem* item = row[_model->_colObject]; + if (item && SP_IS_GROUP(item)) + { + if (isexpanded) + { + //If we're expanding, simply perform the expansion + SP_GROUP(item)->setExpanded(isexpanded); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + else + { + //If we're collapsing, we need to recursively collapse, so call our helper function + _setCollapsed(SP_GROUP(item)); + } + } +} + +/** + * Callback for when the highlight color is changed + * @param csel Color selector + * @param cp Objects panel + */ +void ObjectsPanel::_highlightPickerColorMod() +{ + SPColor color; + float alpha = 0; + _selectedColor->colorAlpha(color, alpha); + + guint32 rgba = color.toRGBA32( alpha ); + + //Set the highlight color for all items in the _highlight_target (all selected items) + for (auto target : _highlight_target) + { + target->setHighlightColor(rgba); + target->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + DocumentUndo::maybeDone(SP_ACTIVE_DOCUMENT, "highlight", SP_VERB_DIALOG_OBJECTS, _("Set object highlight color")); +} + +/** + * Callback for when the opacity value is changed + */ +void ObjectsPanel::_opacityValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_opacityChangedIter)); + DocumentUndo::maybeDone(_document, "opacity", SP_VERB_DIALOG_OBJECTS, _("Set object opacity")); + _blockCompositeUpdate = false; +} + +/** + * Change the opacity of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_opacityChangedIter(const Gtk::TreeIter& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + item->style->opacity.set = TRUE; + item->style->opacity.value = SP_SCALE24_FROM_FLOAT(_filter_modifier.get_opacity_value() / 100); + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the isolation value is changed + */ +void ObjectsPanel::_isolationValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_isolationChangedIter)); + DocumentUndo::maybeDone(_document, "isolation", SP_VERB_DIALOG_OBJECTS, _("Set object isolation")); + _blockCompositeUpdate = false; +} + +/** + * Change the isolation of the selected items in the tree + * @param iter Current tree item + */ +void ObjectsPanel::_isolationChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem *item = row[_model->_colObject]; + if (item) { + item->style->isolation.set = TRUE; + item->style->isolation.value = _filter_modifier.get_isolation_mode(); + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.set = TRUE; + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blend mode is changed + */ +void ObjectsPanel::_blendValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &ObjectsPanel::_blendChangedIter)); + DocumentUndo::done(_document, SP_VERB_DIALOG_OBJECTS, _("Set object blend mode")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blend mode of the selected tree items + * @param iter Current tree item + * @param blendmode Blend mode to set + */ +void ObjectsPanel::_blendChangedIter(const Gtk::TreeIter &iter) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + // < 1.0 filter based blend removal + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + remove_filter_legacy_blend(item); + } + item->style->mix_blend_mode.set = TRUE; + if (_filter_modifier.get_blend_mode() && + item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) + { + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + } else { + item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Callback for when the blur value has changed + */ +void ObjectsPanel::_blurValueChanged() +{ + _blockCompositeUpdate = true; + _tree.get_selection()->selected_foreach_iter(sigc::bind<double>(sigc::mem_fun(*this, &ObjectsPanel::_blurChangedIter), _filter_modifier.get_blur_value())); + DocumentUndo::maybeDone(_document, "blur", SP_VERB_DIALOG_OBJECTS, _("Set object blur")); + _blockCompositeUpdate = false; +} + +/** + * Sets the blur value for the selected items in the tree + * @param iter Current tree item + * @param blur Blur value to set + */ +void ObjectsPanel::_blurChangedIter(const Gtk::TreeIter& iter, double blur) +{ + Gtk::TreeModel::Row row = *iter; + SPItem* item = row[_model->_colObject]; + if (item) + { + //Since blur and blend are both filters, we need to set both at the same time + SPStyle *style = item->style; + if (style) { + Geom::OptRect bbox = item->bounds(SPItem::GEOMETRIC_BBOX); + double radius; + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + radius = blur * perimeter / 400; + } else { + radius = 0; + } + + if (radius != 0) { + // The modify function expects radius to be in display pixels. + Geom::Affine i2d (item->i2dt_affine()); + double expansion = i2d.descrim(); + radius *= expansion; + SPFilter *filter = modify_filter_gaussian_blur_from_item(_document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } else if (item->style->filter.set && item->style->getFilter()) { + for (auto& primitive: item->style->getFilter()->children) { + if (!SP_IS_FILTER_PRIMITIVE(&primitive)) { + break; + } + if (SP_IS_GAUSSIANBLUR(&primitive)) { + primitive.deleteObject(); + break; + } + } + if (!item->style->getFilter()->firstChild()) { + remove_filter(item, false); + } + } + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } +} + +/** + * Constructor + */ +ObjectsPanel::ObjectsPanel() : + UI::Widget::Panel("/dialogs/objects", SP_VERB_DIALOG_OBJECTS), + _rootWatcher(nullptr), + _deskTrack(), + _desktop(nullptr), + _document(nullptr), + _model(nullptr), + _pending(nullptr), + _pending_update(false), + _toggleEvent(nullptr), + _defer_target(), + _visibleHeader(C_("Visibility", "V")), + _lockHeader(C_("Lock", "L")), + _typeHeader(C_("Type", "T")), + _clipmaskHeader(C_("Clip and mask", "CM")), + _highlightHeader(C_("Highlight", "HL")), + _nameHeader(_("Label")), + _filter_modifier( UI::Widget::SimpleFilterModifier::ISOLATION | + UI::Widget::SimpleFilterModifier::BLEND | + UI::Widget::SimpleFilterModifier::BLUR | + UI::Widget::SimpleFilterModifier::OPACITY ), + _colorSelectorDialog("dialogs.colorpickerwindow") +{ + //Create the tree model and store + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + //Set up the tree + _tree.set_model( _store ); + _tree.set_headers_visible(true); + _tree.set_reorderable(false); // Reorderable means that we allow drag-and-drop, but we only allow that when at least one row is selected + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + //Create the column CellRenderers + //Visible + Inkscape::UI::Widget::ImageToggler *eyeRenderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden")) ); + int visibleColNum = _tree.append_column("vis", *eyeRenderer) - 1; + eyeRenderer->property_activatable() = true; + Gtk::TreeViewColumn* col = _tree.get_column(visibleColNum); + if ( col ) { + col->add_attribute( eyeRenderer->property_active(), _model->_colVisible ); + // In order to get tooltips on header, we must create our own label. + _visibleHeader.set_tooltip_text(_("Toggle visibility of Layer, Group, or Object.")); + _visibleHeader.show(); + col->set_widget( _visibleHeader ); + } + + //Locked + Inkscape::UI::Widget::ImageToggler * renderer = Gtk::manage( new Inkscape::UI::Widget::ImageToggler( + INKSCAPE_ICON("object-locked"), INKSCAPE_ICON("object-unlocked")) ); + int lockedColNum = _tree.append_column("lock", *renderer) - 1; + renderer->property_activatable() = true; + col = _tree.get_column(lockedColNum); + if ( col ) { + col->add_attribute( renderer->property_active(), _model->_colLocked ); + _lockHeader.set_tooltip_text(_("Toggle lock of Layer, Group, or Object.")); + _lockHeader.show(); + col->set_widget( _lockHeader ); + } + + //Type + Inkscape::UI::Widget::LayerTypeIcon * typeRenderer = Gtk::manage( new Inkscape::UI::Widget::LayerTypeIcon()); + int typeColNum = _tree.append_column("type", *typeRenderer) - 1; + typeRenderer->property_activatable() = true; + col = _tree.get_column(typeColNum); + if ( col ) { + col->add_attribute( typeRenderer->property_active(), _model->_colType ); + _typeHeader.set_tooltip_text(_("Type: Layer, Group, or Object. Clicking on Layer or Group icon, toggles between the two types.")); + _typeHeader.show(); + col->set_widget( _typeHeader ); + } + + //Insert order (LiamW: unused) + /*Inkscape::UI::Widget::InsertOrderIcon * insertRenderer = Gtk::manage( new Inkscape::UI::Widget::InsertOrderIcon()); + int insertColNum = _tree.append_column("type", *insertRenderer) - 1; + col = _tree.get_column(insertColNum); + if ( col ) { + col->add_attribute( insertRenderer->property_active(), _model->_colInsertOrder ); + }*/ + + //Clip/mask + Inkscape::UI::Widget::ClipMaskIcon * clipRenderer = Gtk::manage( new Inkscape::UI::Widget::ClipMaskIcon()); + int clipColNum = _tree.append_column("clipmask", *clipRenderer) - 1; + col = _tree.get_column(clipColNum); + if ( col ) { + col->add_attribute( clipRenderer->property_active(), _model->_colClipMask ); + _clipmaskHeader.set_tooltip_text(_("Is object clipped and/or masked?")); + _clipmaskHeader.show(); + col->set_widget( _clipmaskHeader ); + } + + //Highlight + Inkscape::UI::Widget::HighlightPicker * highlightRenderer = Gtk::manage( new Inkscape::UI::Widget::HighlightPicker()); + int highlightColNum = _tree.append_column("highlight", *highlightRenderer) - 1; + col = _tree.get_column(highlightColNum); + if ( col ) { + col->add_attribute( highlightRenderer->property_active(), _model->_colHighlight ); + _highlightHeader.set_tooltip_text(_("Highlight color of outline in Node tool. Click to set. If alpha is zero, use inherited color.")); + _highlightHeader.show(); + col->set_widget( _highlightHeader ); + } + + //Label + _text_renderer = Gtk::manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + if( _name_column ) { + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + _nameHeader.set_tooltip_text(_("Layer/Group/Object label (inkscape:label). Double-click to set. Default value is object 'id'.")); + _nameHeader.show(); + _name_column->set_widget( _nameHeader ); + } + + //Set the expander and search columns + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + _tree.set_search_column(_model->_colLabel); + // use ctrl+f to start search + _tree.set_enable_search(false); + + //Set up the tree selection + _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &ObjectsPanel::_rowSelectFunction) ); + + //Set up tree signals + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleButtonEvent), false ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleKeyEvent), false ); + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleDragDrop), false); + _tree.signal_row_collapsed().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), false)); + _tree.signal_row_expanded().connect( sigc::bind<bool>(sigc::mem_fun(*this, &ObjectsPanel::_setExpanded), true)); + + //Set up the label editing signals + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEdited) ); + _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &ObjectsPanel::_handleEditingCancelled) ); + + //Set up the scroller window and pack the page + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _page.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + + //Set up the compositing items + _blendConnection = _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blendValueChanged)); + _blurConnection = _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectsPanel::_blurValueChanged)); + _opacityConnection = _filter_modifier.signal_opacity_changed().connect( sigc::mem_fun(*this, &ObjectsPanel::_opacityValueChanged)); + _isolationConnection = _filter_modifier.signal_isolation_changed().connect( + sigc::mem_fun(*this, &ObjectsPanel::_isolationValueChanged)); + //Pack the compositing functions and the button row + _page.pack_end(_filter_modifier, Gtk::PACK_SHRINK); + _page.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + //Pack into the panel contents + _getContents()->pack_start(_page, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + //Set up the button row + + + //Add object/layer + Gtk::Button* btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-add"), _("Add layer...")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Remove object + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("list-remove"), _("Remove object")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + //Move to bottom + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-bottom"), _("Move To Bottom")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_BOTTOM) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move down + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-down"), _("Move Down")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_DOWN) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move up + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-up"), _("Move Up")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_UP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Move to top + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("go-top"), _("Move To Top")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_TOP) ); + _watchingNonTop.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + //Collapse all + btn = Gtk::manage( new Gtk::Button() ); + _styleButton(*btn, INKSCAPE_ICON("format-indent-less"), _("Collapse All")); + btn->set_relief(Gtk::RELIEF_NONE); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &ObjectsPanel::_takeAction), (int)BUTTON_COLLAPSE_ALL) ); + _watchingNonBottom.push_back( btn ); + _buttonsPrimary.pack_end(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + _watching.push_back(&_filter_modifier); + + //Set up the pop-up menu + // ------------------------------------------------------- + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _show_contextmenu_icons = prefs->getBool("/theme/menuIcons_objects", true); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_RENAME, (int)BUTTON_RENAME ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_NEW, (int)BUTTON_NEW ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SOLO, (int)BUTTON_SOLO ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_SHOW_ALL, (int)BUTTON_SHOW_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_HIDE_ALL, (int)BUTTON_HIDE_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_OTHERS, (int)BUTTON_LOCK_OTHERS ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_LOCK_ALL, (int)BUTTON_LOCK_ALL ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_LAYER_UNLOCK_ALL, (int)BUTTON_UNLOCK_ALL ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watchingNonTop.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_UP, (int)BUTTON_UP) ); + _watchingNonBottom.push_back( &_addPopupItem(targetDesktop, SP_VERB_SELECTION_STACK_DOWN, (int)BUTTON_DOWN) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_GROUP, (int)BUTTON_GROUP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_SELECTION_UNGROUP, (int)BUTTON_UNGROUP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_CLIPPATH, (int)BUTTON_SETCLIP ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_CREATE_CLIP_GROUP, (int)BUTTON_CLIPGROUP ) ); + + //will never be implemented + //_watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, (int)BUTTON_SETINVCLIP ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_CLIPPATH, (int)BUTTON_UNSETCLIP ) ); + + _popupMenu.append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_SET_MASK, (int)BUTTON_SETMASK ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_OBJECT_UNSET_MASK, (int)BUTTON_UNSETMASK ) ); + + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DUPLICATE, (int)BUTTON_DUPLICATE ) ); + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_EDIT_DELETE, (int)BUTTON_DELETE ) ); + + _popupMenu.show_all_children(); + + // Install CSS to shift icons into the space reserved for toggles (i.e. check and radio items). + _popupMenu.signal_map().connect(sigc::mem_fun(static_cast<ContextMenu*>(&_popupMenu), &ContextMenu::ShiftIcons)); + } + // ------------------------------------------------------- + + //Set initial sensitivity of buttons + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + //Set up the color selection dialog + GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); + sp_transientize(dlg); + + _colorSelectorDialog.hide(); + _colorSelectorDialog.set_title (_("Select Highlight Color")); + _colorSelectorDialog.set_border_width (4); + _colorSelectorDialog.property_modal() = true; + _selectedColor.reset(new Inkscape::UI::SelectedColor); + Gtk::Widget *color_selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(*_selectedColor)); + _colorSelectorDialog.get_content_area()->pack_start ( + *color_selector, true, true, 0); + + _selectedColor->signal_dragged.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_released.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + _selectedColor->signal_changed.connect(sigc::mem_fun(*this, &ObjectsPanel::_highlightPickerColorMod)); + + color_selector->show(); + + setDesktop( targetDesktop ); + + show_all_children(); + + //Connect the desktop changed connection + desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &ObjectsPanel::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); +} + +/** + * Callback method that will be called when the desktop is destroyed + */ +void ObjectsPanel::_desktopDestroyed(SPDesktop* /*desktop*/) { + // We need to make sure that we're not trying to update the tree after the desktop has vanished, e.g. + // when closing Inkscape. Preferably, we would have done so in the destructor of the ObjectsPanel. But + // as this destructor is never ever called, we will do this by attaching to the desktop_destroyed signal + // instead + _processQueue_sig.disconnect(); + _executeUpdate_sig.disconnect(); + _desktop = nullptr; +} + +/** + * Destructor + */ +ObjectsPanel::~ObjectsPanel() +{ + // Never being called, not even when closing Inkscape? + + //Close the highlight selection dialog + _colorSelectorDialog.hide(); + + //Set the desktop to null, which will disconnect all object watchers + setDesktop(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +/** + * Sets the current document + */ +void ObjectsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) +{ + //Clear all object watchers + _removeWatchers(); + + //Delete the root watcher + if (_rootWatcher) + { + _rootWatcher->_repr->removeObserver(*_rootWatcher); + delete _rootWatcher; + _rootWatcher = nullptr; + } + + _document = document; + + if (document && document->getRoot() && document->getRoot()->getRepr()) + { + //Create a new root watcher for the document and then call _objectsChanged to fill the tree + _rootWatcher = new ObjectsPanel::ObjectWatcher(this, document->getRoot()); + document->getRoot()->getRepr()->addObserver(*_rootWatcher); + _objectsChanged(document->getRoot()); + } +} + +/** + * Set the current panel desktop + */ +void ObjectsPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _documentChangedConnection.disconnect(); + _documentChangedCurrentLayer.disconnect(); + _selectionChangedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //Connect desktop signals + _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &ObjectsPanel::setDocument)); + + _documentChangedCurrentLayer = _desktop->connectCurrentLayerChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsChangedWrapper)); + + _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &ObjectsPanel::_objectsSelected)); + + _desktopDestroyedConnection = _desktop->connectDestroy( sigc::mem_fun(*this, &ObjectsPanel::_desktopDestroyed)); + + setDocument(_desktop, _desktop->doc()); + } else { + setDocument(nullptr, nullptr); + } + } + _deskTrack.setBase(desktop); +} +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +//should be okay to put these here because they are never referenced anywhere else +using namespace Inkscape::UI::Tools; + +void SPItem::setHighlightColor(guint32 const color) +{ + g_free(_highlightColor); + if (color & 0x000000ff) + { + _highlightColor = g_strdup_printf("%u", color); + } + else + { + _highlightColor = nullptr; + } + + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +void SPItem::unsetHighlightColor() +{ + g_free(_highlightColor); + _highlightColor = nullptr; + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + tools_switch(tool->desktop, TOOLS_NODES); + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/objects.h b/src/ui/dialog/objects.h new file mode 100644 index 0000000..dff1498 --- /dev/null +++ b/src/ui/dialog/objects.h @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for objects UI. + * + * Authors: + * Theodore Janeczko + * Tavmjong Bah + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Tavmjong Bah 2017 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_OBJECTS_PANEL_H +#define SEEN_OBJECTS_PANEL_H + +#include <gtkmm/box.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/scale.h> +#include <gtkmm/dialog.h> +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "desktop-tracker.h" +#include "ui/widget/style-subject.h" +#include "selection.h" +#include "ui/widget/filter-effect-chooser.h" + +class SPObject; +class SPGroup; +struct SPColorSelector; + +namespace Inkscape { + +namespace UI { + +class SelectedColor; + +namespace Dialog { + + +/** + * A panel that displays objects. + */ +class ObjectsPanel : public UI::Widget::Panel +{ +public: + ObjectsPanel(); + ~ObjectsPanel() override; + + static ObjectsPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void setDocument( SPDesktop* desktop, SPDocument* document); + +private: + //Internal Classes: + class ModelColumns; + class InternalUIBounce; + class ObjectWatcher; + + //Connections, Watchers, Trackers: + + //Document root watcher + ObjectsPanel::ObjectWatcher* _rootWatcher; + + //All object watchers + std::map<SPItem*, std::pair<ObjectsPanel::ObjectWatcher*, bool> > _objectWatchers; + + //Connection for when the desktop changes + sigc::connection desktopChangeConn; + + //Connection for when the desktop is destroyed (I.e. its deconstructor is called) + sigc::connection _desktopDestroyedConnection; + + //Connection for when the document changes + sigc::connection _documentChangedConnection; + + //Connection for when the active layer changes + sigc::connection _documentChangedCurrentLayer; + + //Connection for when the active selection in the document changes + sigc::connection _selectionChangedConnection; + + //Connection for when the selection in the dialog changes + sigc::connection _selectedConnection; + + //Connections for when the opacity/blend/blur of the active selection in the document changes + sigc::connection _isolationConnection; + sigc::connection _opacityConnection; + sigc::connection _blendConnection; + sigc::connection _blurConnection; + + sigc::connection _processQueue_sig; + sigc::connection _executeUpdate_sig; + + //Desktop tracker for grabbing the desktop changed connection + DesktopTracker _deskTrack; + + //Members: + + //The current desktop + SPDesktop* _desktop; + + //The current document + SPDocument* _document; + + //Tree data model + ModelColumns* _model; + + //Prevents the composite controls from updating + bool _blockCompositeUpdate; + + // + InternalUIBounce* _pending; + bool _pending_update; + + //Whether the drag & drop was dragged into an item + gboolean _dnd_into; + + //List of drag & drop source items + std::vector<SPItem*> _dnd_source; + + //Drag & drop target item + SPItem* _dnd_target; + + // Whether the drag sources include a layer + bool _dnd_source_includes_layer; + + //List of items to change the highlight on + std::vector<SPItem*> _highlight_target; + + //Show icons in the context menu + bool _show_contextmenu_icons; + + //GUI Members: + + GdkEvent* _toggleEvent; + + Gtk::TreeModel::Path _defer_target; + + Glib::RefPtr<Gtk::TreeStore> _store; + std::list<std::tuple<SPItem*, Gtk::TreeModel::iterator, bool> > _tree_update_queue; + //When the user selects an item in the document, we need to find that item in the tree view + //and highlight it. When looking up a specific item in the tree though, we don't want to have + //to iterate through the whole list, as this would take too long if the list is very long. So + //we will use a std::map for this instead, which is much faster (and call it _tree_cache). It + //would have been cleaner to create our own custom tree model, as described here + //https://en.wikibooks.org/wiki/GTK%2B_By_Example/Tree_View/Tree_Models + std::map<SPItem*, Gtk::TreeModel::iterator> _tree_cache; + std::list<SPItem *> _selected_objects_order; // ordered by time of selection + std::list<Gtk::TreePath> _paths_to_be_expanded; + + std::vector<Gtk::Widget*> _watching; + std::vector<Gtk::Widget*> _watchingNonTop; + std::vector<Gtk::Widget*> _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _page; + + Gtk::Label _visibleHeader; + Gtk::Label _lockHeader; + Gtk::Label _typeHeader; + Gtk::Label _clipmaskHeader; + Gtk::Label _highlightHeader; + Gtk::Label _nameHeader; + + /* Composite Settings (blend, blur, opacity). */ + Inkscape::UI::Widget::SimpleFilterModifier _filter_modifier; + + Gtk::Dialog _colorSelectorDialog; + std::unique_ptr<Inkscape::UI::SelectedColor> _selectedColor; + + //Methods: + + ObjectsPanel(ObjectsPanel const &) = delete; // no copy + ObjectsPanel &operator=(ObjectsPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, char const* iconName, char const* tooltip ); + void _fireAction( unsigned int code ); + + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, int id ); + + void _setVisibleIter( const Gtk::TreeModel::iterator& iter, const bool visible ); + void _setLockedIter( const Gtk::TreeModel::iterator& iter, const bool locked ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + + void _storeHighlightTarget(const Gtk::TreeModel::iterator& iter); + void _storeDragSource(const Gtk::TreeModel::iterator& iter); + bool _handleDragDrop(const Glib::RefPtr<Gdk::DragContext>& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + bool _selectItemCallback(const Gtk::TreeModel::iterator& iter, bool *setOpacity, bool *first_pass); + bool _clearPrevSelectionState(const Gtk::TreeModel::iterator& iter); + void _desktopDestroyed(SPDesktop* desktop); + + void _checkTreeSelection(); + + void _blockAllSignals(bool should_block); + void _takeAction( int val ); + bool _executeAction(); + bool _executeUpdate(); + + void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded ); + void _setCollapsed(SPGroup * group); + + bool _noSelection( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + bool _rowSelectFunction( Glib::RefPtr<Gtk::TreeModel> const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _compositingChanged( const Gtk::TreeModel::iterator& iter, bool *setValues ); + void _updateComposite(); + void _setCompositingValues(SPItem *item); + + bool _findInTreeCache(SPItem* item, Gtk::TreeModel::iterator &tree_iter); + void _updateObject(SPObject *obj, bool recurse); + + void _objectsSelected(Selection *sel); + void _updateObjectSelected(SPItem* item, bool scrollto, bool expand); + + void _removeWatchers(bool only_unused); + void _addWatcher(SPItem* item); + void _objectsChangedWrapper(SPObject *obj); + void _objectsChanged(SPObject *obj); + bool _processQueue(); + void _queueObject(SPObject* obj, Gtk::TreeModel::Row* parentRow); + void _addObjectToTree(SPItem* item, const Gtk::TreeModel::Row &parentRow, bool expanded); + + void _isolationChangedIter(const Gtk::TreeIter &iter); + void _isolationValueChanged(); + + void _opacityChangedIter(const Gtk::TreeIter& iter); + void _opacityValueChanged(); + + void _blendChangedIter(const Gtk::TreeIter &iter); + void _blendValueChanged(); + + void _blurChangedIter(const Gtk::TreeIter& iter, double blur); + void _blurValueChanged(); + + void _highlightPickerColorMod(); +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_OBJECTS_PANEL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/paint-servers.cpp b/src/ui/dialog/paint-servers.cpp new file mode 100644 index 0000000..ec49312 --- /dev/null +++ b/src/ui/dialog/paint-servers.cpp @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> +#include <map> +#include <utility> + +#include <giomm/listmodel.h> +#include <glibmm/regex.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/iconview.h> +#include <gtkmm/liststore.h> +#include <gtkmm/stockid.h> +#include <gtkmm/switch.h> + +#include "document.h" +#include "inkscape.h" +#include "paint-servers.h" +#include "path-prefix.h" +#include "style.h" +#include "verbs.h" + +#include "io/resource.h" +#include "object/sp-defs.h" +#include "object/sp-hatch.h" +#include "object/sp-pattern.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "ui/cache/svg_preview_cache.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static Glib::ustring const wrapper = R"=====( +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <defs id="Defs"/> + <rect id="Back" x="0" y="0" width="100px" height="100px" fill="lightgray"/> + <rect id="Rect" x="0" y="0" width="100px" height="100px" stroke="black"/> +</svg> +)====="; + +class PaintServersColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> id; + Gtk::TreeModelColumn<Glib::ustring> paint; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> pixbuf; + Gtk::TreeModelColumn<Glib::ustring> document; + + PaintServersColumns() { + add(id); + add(paint); + add(pixbuf); + add(document); + } +}; + +PaintServersColumns *PaintServersDialog::getColumns() { return new PaintServersColumns(); } + +// Constructor +PaintServersDialog::PaintServersDialog(gchar const *prefsPath) + : Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_PAINT) + , desktop(SP_ACTIVE_DESKTOP) + , target_selected(true) + , ALLDOCS(_("All paint servers")) + , CURRENTDOC(_("Current document")) +{ + current_store = ALLDOCS; + + store[ALLDOCS] = Gtk::ListStore::create(*getColumns()); + store[CURRENTDOC] = Gtk::ListStore::create(*getColumns()); + + // Grid holding the contents + Gtk::Grid *grid = Gtk::manage(new Gtk::Grid()); + grid->set_margin_start(3); + grid->set_margin_end(3); + grid->set_margin_top(3); + grid->set_row_spacing(3); + _getContents()->pack_start(*grid, Gtk::PACK_EXPAND_WIDGET); + + // Grid row 0 + Gtk::Label *file_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Server")) + ": ")); + grid->attach(*file_label, 0, 0, 1, 1); + + dropdown = Gtk::manage(new Gtk::ComboBoxText()); + dropdown->append(ALLDOCS); + dropdown->append(CURRENTDOC); + document_map[CURRENTDOC] = desktop->getDocument(); + dropdown->set_active_text(ALLDOCS); + dropdown->set_hexpand(); + grid->attach(*dropdown, 1, 0, 1, 1); + + // Grid row 1 + Gtk::Label *fill_label = Gtk::manage(new Gtk::Label(Glib::ustring(_("Change")) + ": ")); + grid->attach(*fill_label, 0, 1, 1, 1); + + target_dropdown = Gtk::manage(new Gtk::ComboBoxText()); + target_dropdown->append(_("Fill")); + target_dropdown->append(_("Stroke")); + target_dropdown->set_active_text(_("Fill")); + target_dropdown->set_hexpand(); + grid->attach(*target_dropdown, 1, 1, 1, 1); + + // Grid row 2 + icon_view = Gtk::manage(new Gtk::IconView( + static_cast<Glib::RefPtr<Gtk::TreeModel>>(store[current_store]) + )); + icon_view->set_tooltip_column(0); + icon_view->set_pixbuf_column(2); + icon_view->set_size_request(200, 300); + icon_view->show_all_children(); + icon_view->set_selection_mode(Gtk::SELECTION_SINGLE); + icon_view->set_activate_on_single_click(true); + + Gtk::ScrolledWindow *scroller = Gtk::manage(new Gtk::ScrolledWindow()); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->set_hexpand(); + scroller->set_vexpand(); + scroller->add(*icon_view); + grid->attach(*scroller, 0, 2, 2, 1); + + // Events + target_dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_target_changed) + ); + + dropdown->signal_changed().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_document_changed) + ); + + icon_view->signal_item_activated().connect( + sigc::mem_fun(*this, &PaintServersDialog::on_item_activated) + ); + + desktop->getDocument()->getDefs()->connectModified( + sigc::mem_fun(*this, &PaintServersDialog::load_current_document) + ); + + // Get wrapper document (rectangle to fill with paint server). + preview_document = SPDocument::createNewDocFromMem(wrapper.c_str(), wrapper.length(), true); + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + if (!rect || !defs) { + std::cerr << "PaintServersDialog::PaintServersDialog: Failed to get wrapper defs or rectangle!!" << std::endl; + } + + // Set up preview document. + unsigned key = SPItem::display_key_new(1); + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY)); + + // Load paint servers from resource files + load_sources(); +} + +PaintServersDialog::~PaintServersDialog() = default; + +// Get url or color value. +Glib::ustring get_url(Glib::ustring paint) +{ + + Glib::MatchInfo matchInfo; + + // Paint server + static Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create(":(url\\(#([A-z0-9\\-_\\.#])*\\))"); + regex1->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + // Color + static Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create(":(([A-z0-9#])*)"); + regex2->match(paint, matchInfo); + + if (matchInfo.matches()) { + return matchInfo.fetch(1); + } + + return Glib::ustring(); +} + +// This is too complicated to use selectors! +void recurse_find_paint(SPObject* in, std::vector<Glib::ustring>& list) +{ + + g_return_if_fail(in != nullptr); + + // Add paint servers in <defs> section. + if (dynamic_cast<SPPaintServer *>(in)) { + if (in->getId()) { + // Need to check as one can't construct Glib::ustring with nullptr. + list.push_back (Glib::ustring("url(#") + in->getId() + ")"); + } + // Don't recurse into paint servers. + return; + } + + // Add paint servers referenced by shapes. + if (dynamic_cast<SPShape *>(in)) { + list.push_back (get_url(in->style->fill.write())); + list.push_back (get_url(in->style->stroke.write())); + } + + for (auto child: in->childList(false)) { + recurse_find_paint(child, list); + } +} + +// Load paint servers from all the files associated +void PaintServersDialog::load_sources() +{ + + // Extract paints from the current file + load_document(desktop->getDocument()); + + // Extract out paints from files in share/paint. + for (auto &path : get_filenames(Inkscape::IO::Resource::PAINT, { ".svg" })) { + SPDocument *document = SPDocument::createNewDoc(path.c_str(), FALSE); + + load_document(document); + } +} + +Glib::RefPtr<Gdk::Pixbuf> PaintServersDialog::get_pixbuf(SPDocument *document, Glib::ustring paint, Glib::ustring *id) +{ + + SPObject *rect = preview_document->getObjectById("Rect"); + SPObject *defs = preview_document->getObjectById("Defs"); + + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + if (paint.empty()) { + return pixbuf; + } + + // Set style on wrapper + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", paint.c_str()); + rect->changeCSS(css, "style"); + sp_repr_css_attr_unref(css); + + // Insert paint into defs if required + Glib::MatchInfo matchInfo; + static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([A-Za-z0-9#._-]*)\\)"); + regex->match(paint, matchInfo); + if (matchInfo.matches()) { + *id = matchInfo.fetch(1); + + // Delete old paint if necessary + std::vector<SPObject *> old_paints = preview_document->getObjectsBySelector("defs > *"); + for (auto paint : old_paints) { + paint->deleteObject(false); + } + + // Add new paint + SPObject *new_paint = document->getObjectById(*id); + if (!new_paint) { + std::cerr << "PaintServersDialog::load_document: cannot find paint server: " << id << std::endl; + return pixbuf; + } + + // Create a copy repr of the paint + Inkscape::XML::Document *xml_doc = preview_document->getReprDoc(); + Inkscape::XML::Node *repr = new_paint->getRepr()->duplicate(xml_doc); + defs->appendChild(repr); + } else { + // Temporary block solid color fills. + return pixbuf; + } + + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + Geom::OptRect dbox = dynamic_cast<SPItem *>(rect)->visualBounds(); + + if (!dbox) { + return pixbuf; + } + + double size = std::max(dbox->width(), dbox->height()); + + pixbuf = Glib::wrap(render_pixbuf(renderDrawing, 1, *dbox, size)); + + return pixbuf; +} + +// Load paint server from the given document +void PaintServersDialog::load_document(SPDocument *document) +{ + PaintServersColumns *columns = getColumns(); + Glib::ustring document_title; + if (!document->getRoot()->title()) { + document_title = CURRENTDOC; + } else { + document_title = Glib::ustring(document->getRoot()->title()); + } + bool has_server_elements = false; + + // Find all paints + std::vector<Glib::ustring> paints; + recurse_find_paint(document->getRoot(), paints); + + // Sort and remove duplicates. + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + if (paints.size() && store.find(document_title) == store.end()) { + store[document_title] = Gtk::ListStore::create(*getColumns()); + } + + // iterating though servers + for (auto paint : paints) { + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + // Save as a ListStore column + Gtk::ListStore::iterator iter = store[ALLDOCS]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + + iter = store[document_title]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + has_server_elements = true; + } + + if (has_server_elements && document_map.find(document_title) == document_map.end()) { + document_map[document_title] = document; + dropdown->append(document_title); + } +} + +void PaintServersDialog::load_current_document(SPObject * /*object*/, guint /*flags*/) +{ + std::unique_ptr<PaintServersColumns> columns(getColumns()); + SPDocument *document = desktop->getDocument(); + Glib::RefPtr<Gtk::ListStore> current = store[CURRENTDOC]; + + std::vector<Glib::ustring> paints; + std::vector<Glib::ustring> paints_current; + std::vector<Glib::ustring> paints_missing; + recurse_find_paint(document->getDefs(), paints); + + std::sort(paints.begin(), paints.end()); + paints.erase(std::unique(paints.begin(), paints.end()), paints.end()); + + // Delete the server from the store if it doesn't exist in the current document + for (auto iter = current->children().begin(); iter != current->children().end();) { + Gtk::TreeRow server = *iter; + + if (std::find(paints.begin(), paints.end(), server[columns->paint]) == paints.end()) { + iter = current->erase(server); + } else { + paints_current.push_back(server[columns->paint]); + iter++; + } + } + + for (auto s : paints) { + if (std::find(paints_current.begin(), paints_current.end(), s) == paints_current.end()) { + std::cout << "missing " << s << std::endl; + paints_missing.push_back(s); + } + } + + if (!paints_missing.size()) { + return; + } + + for (auto paint : paints_missing) { + Glib::RefPtr<Gdk::Pixbuf> pixbuf(nullptr); + Glib::ustring id; + pixbuf = get_pixbuf(document, paint, &id); + if (!pixbuf) { + continue; + } + + Gtk::ListStore::iterator iter = current->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = CURRENTDOC; + } +} + +void PaintServersDialog::on_target_changed() +{ + target_selected = !target_selected; +} + +void PaintServersDialog::on_document_changed() +{ + current_store = dropdown->get_active_text(); + icon_view->set_model(store[current_store]); +} + +void PaintServersDialog::on_item_activated(const Gtk::TreeModel::Path& path) +{ + // Get the current selected elements + Selection *selection = desktop->getSelection(); + std::vector<SPObject*> const selected_items(selection->items().begin(), selection->items().end()); + + if (!selected_items.size()) { + return; + } + + PaintServersColumns *columns = getColumns(); + Gtk::ListStore::iterator iter = store[current_store]->get_iter(path); + Glib::ustring id = (*iter)[columns->id]; + Glib::ustring paint = (*iter)[columns->paint]; + Glib::RefPtr<Gdk::Pixbuf> pixbuf = (*iter)[columns->pixbuf]; + Glib::ustring document_title = (*iter)[columns->document]; + SPDocument *document = document_map[document_title]; + SPObject *paint_server = document->getObjectById(id); + SPDocument *document_target = desktop->getDocument(); + + bool paint_server_exists = false; + for (auto server : store[CURRENTDOC]->children()) { + if (server[columns->id] == id) { + paint_server_exists = true; + break; + } + } + + if (!paint_server_exists) { + // Add the paint server to the current document definition + Inkscape::XML::Document *xml_doc = document_target->getReprDoc(); + Inkscape::XML::Node *repr = paint_server->getRepr()->duplicate(xml_doc); + document_target->getDefs()->appendChild(repr); + Inkscape::GC::release(repr); + + // Add the pixbuf to the current document store + iter = store[CURRENTDOC]->append(); + (*iter)[columns->id] = id; + (*iter)[columns->paint] = paint; + (*iter)[columns->pixbuf] = pixbuf; + (*iter)[columns->document] = document_title; + } + + // Recursively find elements in groups, if any + std::vector<SPObject*> items; + for (auto item : selected_items) { + std::vector<SPObject*> current_items = extract_elements(item); + items.insert(std::end(items), std::begin(current_items), std::end(current_items)); + } + + for (auto item : items) { + item->style->getFillOrStroke(target_selected)->read(paint.c_str()); + item->updateRepr(); + } + + document_target->collectOrphans(); +} + +std::vector<SPObject*> PaintServersDialog::extract_elements(SPObject* item) +{ + std::vector<SPObject*> elements; + std::vector<SPObject*> children = item->childList(false); + if (!children.size()) { + elements.push_back(item); + } else { + for (auto e : children) { + std::vector<SPObject*> current_items = extract_elements(e); + elements.insert(std::end(elements), std::begin(current_items), std::end(current_items)); + } + } + + return elements; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/paint-servers.h b/src/ui/dialog/paint-servers.h new file mode 100644 index 0000000..d3b8f58 --- /dev/null +++ b/src/ui/dialog/paint-servers.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Paint Servers dialog + */ +/* Authors: + * Valentin Ionita + * + * Copyright (C) 2019 Valentin Ionita + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PAINT_SERVERS_H +#define INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include "display/drawing.h" +#include "ui/widget/panel.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PaintServersColumns; // For Gtk::ListStore + +/** + * This dialog serves as a preview for different types of paint servers, + * currently only predefined. It can set the fill or stroke of the selected + * object to the to the paint server you select. + * + * Patterns and hatches are loaded from the preferences paths and displayed + * for each document, for all documents and for the current document. + */ + +class PaintServersDialog : public Inkscape::UI::Widget::Panel { + +public: + PaintServersDialog(gchar const *prefsPath = "/dialogs/paint"); + ~PaintServersDialog() override; + + static PaintServersDialog &getInstance() { return *new PaintServersDialog(); }; + PaintServersDialog(PaintServersDialog const &) = delete; + PaintServersDialog &operator=(PaintServersDialog const &) = delete; + + private: + static PaintServersColumns *getColumns(); + void load_sources(); + void load_document(SPDocument *document); + void load_current_document(SPObject *, guint); + Glib::RefPtr<Gdk::Pixbuf> get_pixbuf(SPDocument *, Glib::ustring, Glib::ustring *); + void on_target_changed(); + void on_document_changed(); + void on_item_activated(const Gtk::TreeModel::Path &path); + std::vector<SPObject *> extract_elements(SPObject *item); + + const Glib::ustring ALLDOCS; + const Glib::ustring CURRENTDOC; + std::map<Glib::ustring, Glib::RefPtr<Gtk::ListStore>> store; + Glib::ustring current_store; + std::map<Glib::ustring, SPDocument *> document_map; + SPDocument *preview_document; + Inkscape::Drawing renderDrawing; + Gtk::ComboBoxText *dropdown; + Gtk::IconView *icon_view; + SPDesktop *desktop; + Gtk::ComboBoxText *target_dropdown; + bool target_selected; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // SEEN INKSCAPE_UI_DIALOG_PAINT_SERVERS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/panel-dialog.h b/src/ui/dialog/panel-dialog.h new file mode 100644 index 0000000..eb7d9f3 --- /dev/null +++ b/src/ui/dialog/panel-dialog.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A panel holding dialog + */ +/* Authors: + * C 2007 Gustav Broberg <broberg@kth.se> + * C 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_PANEL_DIALOG_H +#define INKSCAPE_PANEL_DIALOG_H + +#include <glibmm/i18n.h> +#include <gtkmm/dialog.h> + +#include "verbs.h" +#include "dialog.h" +#include "ui/dialog/swatches.h" +#include "ui/dialog/floating-behavior.h" +#include "ui/dialog/dock-behavior.h" +#include "inkscape.h" +#include "desktop.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Auxiliary class for the link between UI::Dialog::PanelDialog and UI::Dialog::Dialog. + * + * PanelDialog handles signals emitted when a desktop changes, either changing to a + * different desktop or a new document. + */ +class PanelDialogBase { +public: + PanelDialogBase(UI::Widget::Panel &panel, char const */*prefs_path*/, int const /*verb_num*/) : + _panel (panel) { } + + virtual void present() = 0; + virtual ~PanelDialogBase() = default; + + virtual UI::Widget::Panel &getPanel() { return _panel; } + +protected: + + inline virtual void _propagateDocumentReplaced(SPDesktop* desktop, SPDocument *document); + inline virtual void _propagateDesktopActivated(SPDesktop *); + inline virtual void _propagateDesktopDeactivated(SPDesktop *); + + UI::Widget::Panel &_panel; + sigc::connection _document_replaced_connection; +}; + +/** + * Bridges UI::Widget::Panel and UI::Dialog::Dialog. + * + * Where Dialog handles window behaviour, such as closing, position, etc, and where + * Panel is the actual container for dialog child widgets (and from where the dialog + * content is made), PanelDialog links these two classes together to create a + * dockable and floatable dialog. The link with Dialog is made via PanelDialogBase. + */ +template <typename Behavior> +class PanelDialog : public PanelDialogBase, public Inkscape::UI::Dialog::Dialog { + +public: + /** + * Constructor. + * + * @param contents panel with the actual dialog content. + * @param prefs_path characteristic path for loading/saving dialog position. + * @param verb_num the dialog verb. + */ + PanelDialog(UI::Widget::Panel &contents, char const *prefs_path, int const verb_num); + + ~PanelDialog() override = default; + + template <typename T> + static PanelDialog<Behavior> *create(); + + inline void present() override; + +private: + template <typename T> + static PanelDialog<Behavior> *_create(); + + inline void _presentDialog(); + + PanelDialog() = delete; + PanelDialog(PanelDialog<Behavior> const &d) = delete; // no copy + PanelDialog<Behavior>& operator=(PanelDialog<Behavior> const &d) = delete; // no assign +}; + + +void PanelDialogBase::_propagateDocumentReplaced(SPDesktop *desktop, SPDocument *document) +{ + _panel.signalDocumentReplaced().emit(desktop, document); +} + +void PanelDialogBase::_propagateDesktopActivated(SPDesktop *desktop) +{ + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialogBase::_propagateDocumentReplaced)); + _panel.signalActivateDesktop().emit(desktop); +} + +void PanelDialogBase::_propagateDesktopDeactivated(SPDesktop *desktop) +{ + _document_replaced_connection.disconnect(); + _panel.signalDeactiveDesktop().emit(desktop); +} + + +template <typename B> +PanelDialog<B>::PanelDialog(Widget::Panel &panel, char const *prefs_path, int const verb_num) : + PanelDialogBase(panel, prefs_path, verb_num), + Dialog(&B::create, prefs_path, verb_num) +{ + Gtk::Box *vbox = get_vbox(); + _panel.signalResponse().connect(sigc::mem_fun(*this, &PanelDialog::_handleResponse)); + _panel.signalPresent().connect(sigc::mem_fun(*this, &PanelDialog::_presentDialog)); + + vbox->pack_start(_panel, true, true, 0); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + _propagateDesktopActivated(desktop); + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(*this, &PanelDialog::_propagateDocumentReplaced)); + + show_all_children(); +} + +template <typename B> template <typename P> +PanelDialog<B> *PanelDialog<B>::create() +{ + return _create<P>(); +} + +template <typename B> template <typename P> +PanelDialog<B> *PanelDialog<B>::_create() +{ + UI::Widget::Panel &panel = P::getInstance(); + return new PanelDialog<B>(panel, panel.getPrefsPath(), panel.getVerb()); +} + +template <typename B> +void PanelDialog<B>::present() +{ + _panel.present(); +} + +template <typename B> +void PanelDialog<B>::_presentDialog() +{ + Dialog::present(); +} + +template <> inline +void PanelDialog<Behavior::FloatingBehavior>::present() +{ + Dialog::present(); + _panel.present(); +} + +template <> inline +void PanelDialog<Behavior::FloatingBehavior>::_presentDialog() +{ +} + +/** + * Specialized factory method for panel dialogs with floating behavior in order to make them work as + * singletons, i.e. allow them track the current active desktop. + */ +template <> +template <typename P> +PanelDialog<Behavior::FloatingBehavior> *PanelDialog<Behavior::FloatingBehavior>::create() +{ + auto instance = _create<P>(); + + INKSCAPE.signal_activate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_propagateDesktopActivated) + ); + INKSCAPE.signal_deactivate_desktop.connect( + sigc::mem_fun(*instance, &PanelDialog<Behavior::FloatingBehavior>::_propagateDesktopDeactivated) + ); + + return instance; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_PANEL_DIALOG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/polar-arrange-tab.cpp b/src/ui/dialog/polar-arrange-tab.cpp new file mode 100644 index 0000000..67f1442 --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.cpp @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/messagedialog.h> + +#include <2geom/transforms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "verbs.h" + +#include "object/sp-ellipse.h" +#include "object/sp-item-transform.h" + +#include "ui/dialog/polar-arrange-tab.h" +#include "ui/dialog/tile.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +PolarArrangeTab::PolarArrangeTab(ArrangeDialog *parent_) + : parent(parent_), + parametersTable(), + centerY("", C_("Polar arrange tab", "Y coordinate of the center"), UNIT_TYPE_LINEAR), + centerX("", C_("Polar arrange tab", "X coordinate of the center"), centerY), + radiusY("", C_("Polar arrange tab", "Y coordinate of the radius"), UNIT_TYPE_LINEAR), + radiusX("", C_("Polar arrange tab", "X coordinate of the radius"), radiusY), + angleY("", C_("Polar arrange tab", "Ending angle"), UNIT_TYPE_RADIAL), + angleX("", C_("Polar arrange tab", "Starting angle"), angleY) +{ + anchorPointLabel.set_text(C_("Polar arrange tab", "Anchor point:")); + anchorPointLabel.set_halign(Gtk::ALIGN_START); + pack_start(anchorPointLabel, false, false); + + anchorBoundingBoxRadio.set_label(C_("Polar arrange tab", "Objects' bounding boxes:")); + anchorRadioGroup = anchorBoundingBoxRadio.get_group(); + anchorBoundingBoxRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorBoundingBoxRadio, false, false); + + pack_start(anchorSelector, false, false); + + anchorObjectPivotRadio.set_label(C_("Polar arrange tab", "Objects' rotational centers")); + anchorObjectPivotRadio.set_group(anchorRadioGroup); + anchorObjectPivotRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_anchor_radio_changed)); + pack_start(anchorObjectPivotRadio, false, false); + + arrangeOnLabel.set_text(C_("Polar arrange tab", "Arrange on:")); + arrangeOnLabel.set_halign(Gtk::ALIGN_START); + pack_start(arrangeOnLabel, false, false); + + arrangeOnFirstCircleRadio.set_label(C_("Polar arrange tab", "First selected circle/ellipse/arc")); + arrangeRadioGroup = arrangeOnFirstCircleRadio.get_group(); + arrangeOnFirstCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnFirstCircleRadio, false, false); + + arrangeOnLastCircleRadio.set_label(C_("Polar arrange tab", "Last selected circle/ellipse/arc")); + arrangeOnLastCircleRadio.set_group(arrangeRadioGroup); + arrangeOnLastCircleRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnLastCircleRadio, false, false); + + arrangeOnParametersRadio.set_label(C_("Polar arrange tab", "Parameterized:")); + arrangeOnParametersRadio.set_group(arrangeRadioGroup); + arrangeOnParametersRadio.signal_toggled().connect(sigc::mem_fun(*this, &PolarArrangeTab::on_arrange_radio_changed)); + pack_start(arrangeOnParametersRadio, false, false); + + centerLabel.set_text(C_("Polar arrange tab", "Center X/Y:")); + parametersTable.attach(centerLabel, 0, 0, 1, 1); + centerX.setDigits(2); + centerX.setIncrements(0.2, 0); + centerX.setRange(-10000, 10000); + centerX.setValue(0, "px"); + centerY.setDigits(2); + centerY.setIncrements(0.2, 0); + centerY.setRange(-10000, 10000); + centerY.setValue(0, "px"); + parametersTable.attach(centerX, 1, 0, 1, 1); + parametersTable.attach(centerY, 2, 0, 1, 1); + + radiusLabel.set_text(C_("Polar arrange tab", "Radius X/Y:")); + parametersTable.attach(radiusLabel, 0, 1, 1, 1); + radiusX.setDigits(2); + radiusX.setIncrements(0.2, 0); + radiusX.setRange(0.001, 10000); + radiusX.setValue(100, "px"); + radiusY.setDigits(2); + radiusY.setIncrements(0.2, 0); + radiusY.setRange(0.001, 10000); + radiusY.setValue(100, "px"); + parametersTable.attach(radiusX, 1, 1, 1, 1); + parametersTable.attach(radiusY, 2, 1, 1, 1); + + angleLabel.set_text(_("Angle X/Y:")); + parametersTable.attach(angleLabel, 0, 2, 1, 1); + angleX.setDigits(2); + angleX.setIncrements(0.2, 0); + angleX.setRange(-10000, 10000); + angleX.setValue(0, "°"); + angleY.setDigits(2); + angleY.setIncrements(0.2, 0); + angleY.setRange(-10000, 10000); + angleY.setValue(180, "°"); + parametersTable.attach(angleX, 1, 2, 1, 1); + parametersTable.attach(angleY, 2, 2, 1, 1); + parametersTable.set_row_spacing(4); + parametersTable.set_column_spacing(4); + pack_start(parametersTable, false, false); + + rotateObjectsCheckBox.set_label(_("Rotate objects")); + rotateObjectsCheckBox.set_active(true); + pack_start(rotateObjectsCheckBox, false, false); + + centerX.set_sensitive(false); + centerY.set_sensitive(false); + angleX.set_sensitive(false); + angleY.set_sensitive(false); + radiusX.set_sensitive(false); + radiusY.set_sensitive(false); + + set_border_width(4); +} + +/** + * This function rotates an item around a given point by a given amount + * @param item item to rotate + * @param center center of the rotation to perform + * @param rotation amount to rotate the object by + */ +static void rotateAround(SPItem *item, Geom::Point center, Geom::Rotate const &rotation) +{ + Geom::Translate const s(center); + Geom::Affine affine = Geom::Affine(s).inverse() * Geom::Affine(rotation) * Geom::Affine(s); + + // Save old center + center = item->getCenter(); + + item->set_i2d_affine(item->i2dt_affine() * affine); + item->doWriteTransform(item->transform); + + if(item->isCenterSet()) + { + item->setCenter(center * affine); + item->updateRepr(); + } +} + +/** + * Calculates the angle at which to put an object given the total amount + * of objects, the index of the objects as well as the arc start and end + * points + * @param arcBegin angle at which the arc begins + * @param arcEnd angle at which the arc ends + * @param count number of objects in the selection + * @param n index of the object in the selection + */ +static float calcAngle(float arcBegin, float arcEnd, int count, int n) +{ + float arcLength = arcEnd - arcBegin; + float delta = std::abs(std::abs(arcLength) - 2*M_PI); + if(delta > 0.01) count--; // If not a complete circle, put an object also at the extremes of the arc; + + float angle = n / (float)count; + // Normalize for arcLength: + angle = angle * arcLength; + angle += arcBegin; + + return angle; +} + +/** + * Calculates the point at which an object needs to be, given the center of the ellipse, + * it's radius (x and y), as well as the angle + */ +static Geom::Point calcPoint(float cx, float cy, float rx, float ry, float angle) +{ + return Geom::Point(cx + cos(angle) * rx, cy + sin(angle) * ry); +} + +/** + * Returns the selected anchor point in desktop coordinates. If anchor + * is 0 to 8, then a bounding box point has been chosen. If it is 9 however + * the rotational center is chosen. + */ +static Geom::Point getAnchorPoint(int anchor, SPItem *item) +{ + Geom::Point source; + + Geom::OptRect bbox = item->documentVisualBounds(); + + switch(anchor) + { + case 0: // Top - Left + case 3: // Middle - Left + case 6: // Bottom - Left + source[0] = bbox->min()[Geom::X]; + break; + case 1: // Top - Middle + case 4: // Middle - Middle + case 7: // Bottom - Middle + source[0] = (bbox->min()[Geom::X] + bbox->max()[Geom::X]) / 2.0f; + break; + case 2: // Top - Right + case 5: // Middle - Right + case 8: // Bottom - Right + source[0] = bbox->max()[Geom::X]; + break; + }; + + switch(anchor) + { + case 0: // Top - Left + case 1: // Top - Middle + case 2: // Top - Right + source[1] = bbox->min()[Geom::Y]; + break; + case 3: // Middle - Left + case 4: // Middle - Middle + case 5: // Middle - Right + source[1] = (bbox->min()[Geom::Y] + bbox->max()[Geom::Y]) / 2.0f; + break; + case 6: // Bottom - Left + case 7: // Bottom - Middle + case 8: // Bottom - Right + source[1] = bbox->max()[Geom::Y]; + break; + }; + + // If using center + if(anchor == 9) + source = item->getCenter(); + else + { + source *= item->document->doc2dt(); + } + + return source; +} + +/** + * Moves an SPItem to a given location, the location is based on the given anchor point. + * @param anchor 0 to 8 are the various bounding box points like follows: + * 0 1 2 + * 3 4 5 + * 6 7 8 + * Anchor mode 9 is the rotational center of the object + * @param item Item to move + * @param p point at which to move the object + */ +static void moveToPoint(int anchor, SPItem *item, Geom::Point p) +{ + item->move_rel(Geom::Translate(p - getAnchorPoint(anchor, item))); +} + +void PolarArrangeTab::arrange() +{ + Inkscape::Selection *selection = parent->getDesktop()->getSelection(); + const std::vector<SPItem*> tmp(selection->items().begin(), selection->items().end()); + SPGenericEllipse *referenceEllipse = nullptr; // Last ellipse in selection + + bool arrangeOnEllipse = !arrangeOnParametersRadio.get_active(); + bool arrangeOnFirstEllipse = arrangeOnEllipse && arrangeOnFirstCircleRadio.get_active(); + float yaxisdir = parent->getDesktop()->yaxisdir(); + + int count = 0; + for(auto item : tmp) + { + if(arrangeOnEllipse) + { + if(arrangeOnFirstEllipse) + { + // The first selected ellipse is actually the last one in the list + if(SP_IS_GENERICELLIPSE(item)) + referenceEllipse = SP_GENERICELLIPSE(item); + } else { + // The last selected ellipse is actually the first in list + if(SP_IS_GENERICELLIPSE(item) && referenceEllipse == nullptr) + referenceEllipse = SP_GENERICELLIPSE(item); + } + } + ++count; + } + + float cx, cy; // Center of the ellipse + float rx, ry; // Radiuses of the ellipse in x and y direction + float arcBeg, arcEnd; // begin and end angles for arcs + Geom::Affine transformation; // Any additional transformation to apply to the objects + + if(arrangeOnEllipse) + { + if(referenceEllipse == nullptr) + { + Gtk::MessageDialog dialog(_("Couldn't find an ellipse in selection"), false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_CLOSE, true); + dialog.run(); + return; + } else { + cx = referenceEllipse->cx.value; + cy = referenceEllipse->cy.value; + rx = referenceEllipse->rx.value; + ry = referenceEllipse->ry.value; + arcBeg = referenceEllipse->start; + arcEnd = referenceEllipse->end; + + transformation = referenceEllipse->i2dt_affine(); + + // We decrement the count by 1 as we are not going to lay + // out the reference ellipse + --count; + } + + } else { + // Read options from UI + cx = centerX.getValue("px"); + cy = centerY.getValue("px"); + rx = radiusX.getValue("px"); + ry = radiusY.getValue("px"); + arcBeg = angleX.getValue("rad"); + arcEnd = angleY.getValue("rad") * yaxisdir; + transformation.setIdentity(); + referenceEllipse = nullptr; + } + + int anchor = 9; + if(anchorBoundingBoxRadio.get_active()) + { + anchor = anchorSelector.getHorizontalAlignment() + + anchorSelector.getVerticalAlignment() * 3; + } + + Geom::Point realCenter = Geom::Point(cx, cy) * transformation; + + int i = 0; + for(auto item : tmp) + { + // Ignore the reference ellipse if any + if(item != referenceEllipse) + { + float angle = calcAngle(arcBeg, arcEnd, count, i); + Geom::Point newLocation = calcPoint(cx, cy, rx, ry, angle) * transformation; + + moveToPoint(anchor, item, newLocation); + + if(rotateObjectsCheckBox.get_active()) { + // Calculate the angle by which to rotate each object + angle = -atan2f(-yaxisdir * (newLocation.x() - realCenter.x()), -yaxisdir * (newLocation.y() - realCenter.y())); + rotateAround(item, newLocation, Geom::Rotate(angle)); + } + + ++i; + } + } + + DocumentUndo::done(parent->getDesktop()->getDocument(), SP_VERB_SELECTION_ARRANGE, + _("Arrange on ellipse")); +} + +void PolarArrangeTab::updateSelection() +{ +} + +void PolarArrangeTab::on_arrange_radio_changed() +{ + bool arrangeParametric = arrangeOnParametersRadio.get_active(); + + centerX.set_sensitive(arrangeParametric); + centerY.set_sensitive(arrangeParametric); + + angleX.set_sensitive(arrangeParametric); + angleY.set_sensitive(arrangeParametric); + + radiusX.set_sensitive(arrangeParametric); + radiusY.set_sensitive(arrangeParametric); + + parametersTable.set_visible(arrangeParametric); +} + +void PolarArrangeTab::on_anchor_radio_changed() +{ + bool anchorBoundingBox = anchorBoundingBoxRadio.get_active(); + + anchorSelector.set_sensitive(anchorBoundingBox); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/polar-arrange-tab.h b/src/ui/dialog/polar-arrange-tab.h new file mode 100644 index 0000000..0c5acde --- /dev/null +++ b/src/ui/dialog/polar-arrange-tab.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @brief Arranges Objects into a Circle/Ellipse + */ +/* Authors: + * Declara Denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H +#define INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H + +#include "ui/widget/scalar-unit.h" +#include "ui/widget/anchor-selector.h" +#include "ui/dialog/arrange-tab.h" + +#include <gtkmm/radiobutton.h> +#include <gtkmm/radiobuttongroup.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeDialog; + +/** + * PolarArrangeTab is a Tab displayed in the Arrange dialog and contains + * enables the user to arrange objects on a circular or elliptical shape + */ +class PolarArrangeTab : public ArrangeTab { +public: + PolarArrangeTab(ArrangeDialog *parent_); + ~PolarArrangeTab() override = default;; + + /** + * Do the actual arrangement + */ + void arrange() override; + + /** + * Respond to selection change + */ + void updateSelection(); + + void on_anchor_radio_changed(); + void on_arrange_radio_changed(); + +private: + PolarArrangeTab(PolarArrangeTab const &d) = delete; // no copy + void operator=(PolarArrangeTab const &d) = delete; // no assign + + ArrangeDialog *parent; + + Gtk::Label anchorPointLabel; + + Gtk::RadioButtonGroup anchorRadioGroup; + Gtk::RadioButton anchorBoundingBoxRadio; + Gtk::RadioButton anchorObjectPivotRadio; + Inkscape::UI::Widget::AnchorSelector anchorSelector; + + Gtk::Label arrangeOnLabel; + + Gtk::RadioButtonGroup arrangeRadioGroup; + Gtk::RadioButton arrangeOnFirstCircleRadio; + Gtk::RadioButton arrangeOnLastCircleRadio; + Gtk::RadioButton arrangeOnParametersRadio; + + Gtk::Grid parametersTable; + + Gtk::Label centerLabel; + Inkscape::UI::Widget::ScalarUnit centerY; + Inkscape::UI::Widget::ScalarUnit centerX; + + Gtk::Label radiusLabel; + Inkscape::UI::Widget::ScalarUnit radiusY; + Inkscape::UI::Widget::ScalarUnit radiusX; + + Gtk::Label angleLabel; + Inkscape::UI::Widget::ScalarUnit angleY; + Inkscape::UI::Widget::ScalarUnit angleX; + + Gtk::CheckButton rotateObjectsCheckBox; + + +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* INKSCAPE_UI_DIALOG_POLAR_ARRANGE_TAB_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/print-colors-preview-dialog.cpp b/src/ui/dialog/print-colors-preview-dialog.cpp new file mode 100644 index 0000000..6371af9 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print Colors Preview dialog - implementation. + */ +/* Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* +#include "desktop.h" +#include "print-colors-preview-dialog.h" +#include "preferences.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +//Yes, I know we shouldn't hardcode CMYK. This class needs to be refactored +// in order to accommodate spot colors and color components defined using +// ICC colors. --Juca + +void PrintColorsPreviewDialog::toggle_cyan(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/cyan", cyan->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_magenta(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/magenta", magenta->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_yellow(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/yellow", yellow->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +void PrintColorsPreviewDialog::toggle_black(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/printcolorspreview/black", black->get_active()); + + SPDesktop *desktop = getDesktop(); + desktop->setDisplayModePrintColorsPreview(); +} + +PrintColorsPreviewDialog::PrintColorsPreviewDialog() + : UI::Widget::Panel("/dialogs/printcolorspreview", SP_VERB_DIALOG_PRINT_COLORS_PREVIEW) +{ + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + + cyan = new Gtk::ToggleButton(_("Cyan")); + vbox->pack_start( *cyan, false, false ); +// tips.set_tip((*cyan), _("Render cyan separation")); + cyan->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_cyan) ); + + magenta = new Gtk::ToggleButton(_("Magenta")); + vbox->pack_start( *magenta, false, false ); +// tips.set_tip((*magenta), _("Render magenta separation")); + magenta->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_magenta) ); + + yellow = new Gtk::ToggleButton(_("Yellow")); + vbox->pack_start( *yellow, false, false ); +// tips.set_tip((*yellow), _("Render yellow separation")); + yellow->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_yellow) ); + + black = new Gtk::ToggleButton(_("Black")); + vbox->pack_start( *black, false, false ); +// tips.set_tip((*black), _("Render black separation")); + black->signal_clicked().connect( sigc::mem_fun(*this, &PrintColorsPreviewDialog::toggle_black) ); + + gint val; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + val = prefs->getBool("/options/printcolorspreview/cyan"); + cyan->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/magenta"); + magenta->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/yellow"); + yellow->set_active( val != 0 ); + val = prefs->getBool("/options/printcolorspreview/black"); + black->set_active( val != 0 ); + + _getContents()->add(*vbox); + _getContents()->show_all(); +} + +PrintColorsPreviewDialog::~PrintColorsPreviewDialog(){} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape +*/ diff --git a/src/ui/dialog/print-colors-preview-dialog.h b/src/ui/dialog/print-colors-preview-dialog.h new file mode 100644 index 0000000..e40f987 --- /dev/null +++ b/src/ui/dialog/print-colors-preview-dialog.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print Colors Preview dialog + */ +/* Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H +#define INKSCAPE_UI_DIALOG_PRINT_COLORS_PREVIEW_H + +#include "ui/widget/panel.h" +#include "verbs.h" + +#include <gtkmm/box.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class PrintColorsPreviewDialog : public UI::Widget::Panel { +public: + PrintColorsPreviewDialog(); + ~PrintColorsPreviewDialog(); + + static PrintColorsPreviewDialog &getInstance() + { return *new PrintColorsPreviewDialog(); } + +private: + void toggle_cyan(); + void toggle_magenta(); + void toggle_yellow(); + void toggle_black(); + + Gtk::ToggleButton* cyan; + Gtk::ToggleButton* magenta; + Gtk::ToggleButton* yellow; + Gtk::ToggleButton* black; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //#ifndef INKSCAPE_UI_PRINT_COLORS_PREVIEW_H diff --git a/src/ui/dialog/print.cpp b/src/ui/dialog/print.cpp new file mode 100644 index 0000000..006961b --- /dev/null +++ b/src/ui/dialog/print.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Print dialog. + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * Abhishek Sharma + * Patrick McDermott + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2017 Patrick McDermott + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include <gtkmm.h> + +#include "inkscape.h" +#include "preferences.h" +#include "print.h" + +#include "extension/internal/cairo-render-context.h" +#include "extension/internal/cairo-renderer.h" +#include "document.h" + +#include "util/units.h" +#include "helper/png-write.h" +#include "svg/svg-color.h" + +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +Print::Print(SPDocument *doc, SPItem *base) : + _doc (doc), + _base (base) +{ + g_assert (_doc); + g_assert (_base); + + _printop = Gtk::PrintOperation::create(); + + // set up dialog title, based on document name + const Glib::ustring jobname = _doc->getDocumentName() ? _doc->getDocumentName() : _("SVG Document"); + Glib::ustring title = _("Print"); + title += " "; + title += jobname; + _printop->set_job_name(title); + + _printop->set_unit(Gtk::UNIT_POINTS); + Glib::RefPtr<Gtk::PageSetup> page_setup = Gtk::PageSetup::create(); + + // Default to a custom paper size, in case we can't find a more specific size + gdouble doc_width = _doc->getWidth().value("pt"); + gdouble doc_height = _doc->getHeight().value("pt"); + page_setup->set_paper_size( + Gtk::PaperSize("custom", "custom", doc_width, doc_height, Gtk::UNIT_POINTS)); + + // Some print drivers, like the EPSON's ESC/P-R CUPS driver, don't accept custom + // page sizes, so we'll try to find a known page size. + // GTK+'s known paper sizes always have a longer height than width, so we'll rotate + // the page and set its orientation to landscape as necessary in order to match a paper size. + // Unfortunately, some printers, like Epilog laser cutters, don't understand landscape + // mode. + // As a compromise, we'll only rotate the page if we actually find a matching paper size, + // since laser cutter beds tend to be custom sizes. + Gtk::PageOrientation orientation = Gtk::PAGE_ORIENTATION_PORTRAIT; + if (_doc->getWidth().value("pt") > _doc->getHeight().value("pt")) { + orientation = Gtk::PAGE_ORIENTATION_REVERSE_LANDSCAPE; + std::swap(doc_width, doc_height); + } + + // attempt to match document size against known paper sizes + std::vector<Gtk::PaperSize> known_sizes = Gtk::PaperSize::get_paper_sizes(false); + for (auto& size : known_sizes) { + if (fabs(size.get_width(Gtk::UNIT_POINTS) - doc_width) >= 1.0) { + // width (short edge) doesn't match + continue; + } + if (fabs(size.get_height(Gtk::UNIT_POINTS) - doc_height) >= 1.0) { + // height (short edge) doesn't match + continue; + } + // size matches + page_setup->set_paper_size(size); + page_setup->set_orientation(orientation); + break; + } + + _printop->set_default_page_setup(page_setup); + _printop->set_use_full_page(true); + + // set up signals + _workaround._doc = _doc; + _workaround._base = _base; + _workaround._tab = &_tab; + _printop->signal_create_custom_widget().connect(sigc::mem_fun(*this, &Print::create_custom_widget)); + _printop->signal_begin_print().connect(sigc::mem_fun(*this, &Print::begin_print)); + _printop->signal_draw_page().connect(sigc::mem_fun(*this, &Print::draw_page)); + + // build custom preferences tab + _printop->set_custom_tab_label(_("Rendering")); +} + +void Print::draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/) +{ + // TODO: If the user prints multiple copies we render the whole page for each copy + // It would be more efficient to render the page once (e.g. in "begin_print") + // and simply print this result as often as necessary + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + //printf("%s %d\n",__FUNCTION__, page_nr); + + if (_workaround._tab->as_bitmap()) { + // Render as exported PNG + prefs->setBool("/dialogs/printing/asbitmap", true); + gdouble width = (_workaround._doc)->getWidth().value("px"); + gdouble height = (_workaround._doc)->getHeight().value("px"); + gdouble dpi = _workaround._tab->bitmap_dpi(); + prefs->setDouble("/dialogs/printing/dpi", dpi); + + std::string tmp_png; + std::string tmp_base = "inkscape-print-png-XXXXXX"; + + int tmp_fd; + if ( (tmp_fd = Glib::file_open_tmp(tmp_png, tmp_base)) >= 0) { + close(tmp_fd); + + guint32 bgcolor = 0x00000000; + Inkscape::XML::Node *nv = _workaround._doc->getReprNamedView(); + if (nv && nv->attribute("pagecolor")){ + bgcolor = sp_svg_read_color(nv->attribute("pagecolor"), 0xffffff00); + } + if (nv && nv->attribute("inkscape:pageopacity")){ + double opacity = 1.0; + sp_repr_get_double (nv, "inkscape:pageopacity", &opacity); + bgcolor |= SP_COLOR_F_TO_U(opacity); + } + + sp_export_png_file(_workaround._doc, tmp_png.c_str(), 0.0, 0.0, + width, height, + (unsigned long)(Inkscape::Util::Quantity::convert(width, "px", "in") * dpi), + (unsigned long)(Inkscape::Util::Quantity::convert(height, "px", "in") * dpi), + dpi, dpi, bgcolor, nullptr, nullptr, true, std::vector<SPItem*>()); + + // This doesn't seem to work: + //context->set_cairo_context ( Cairo::Context::create (Cairo::ImageSurface::create_from_png (tmp_png) ), dpi, dpi ); + // + // so we'll use a surface pattern blat instead... + // + // but the C++ interface isn't implemented in cairomm: + //context->get_cairo_context ()->set_source_surface(Cairo::ImageSurface::create_from_png (tmp_png) ); + // + // so do it in C: + { + Cairo::RefPtr<Cairo::ImageSurface> png = Cairo::ImageSurface::create_from_png (tmp_png); + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_matrix_t m; + cairo_get_matrix(cr, &m); + cairo_scale(cr, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi, Inkscape::Util::Quantity::convert(1, "in", "pt") / dpi); + // FIXME: why is the origin offset?? + cairo_set_source_surface(cr, png->cobj(), 0, 0); + cairo_paint(cr); + cairo_set_matrix(cr, &m); + } + + // Clean up + unlink (tmp_png.c_str()); + } + else { + g_warning("%s", _("Could not open temporary PNG for bitmap printing")); + } + } + else { + // Render as vectors + prefs->setBool("/dialogs/printing/asbitmap", false); + Inkscape::Extension::Internal::CairoRenderer renderer; + Inkscape::Extension::Internal::CairoRenderContext *ctx = renderer.createContext(); + + // ctx->setPSLevel(CAIRO_PS_LEVEL_3); + ctx->setTextToPath(false); + ctx->setFilterToBitmap(true); + ctx->setBitmapResolution(72); + + cairo_t *cr = gtk_print_context_get_cairo_context (context->gobj()); + cairo_surface_t *surface = cairo_get_target(cr); + cairo_matrix_t ctm; + cairo_get_matrix(cr, &ctm); + + bool ret = ctx->setSurfaceTarget (surface, true, &ctm); + if (ret) { + ret = renderer.setupDocument (ctx, _workaround._doc, TRUE, 0., nullptr); + if (ret) { + renderer.renderItem(ctx, _workaround._base); + ctx->finish(false); // do not finish the cairo_surface_t - it's owned by our GtkPrintContext! + } + else { + g_warning("%s", _("Could not set up Document")); + } + } + else { + g_warning("%s", _("Failed to set CairoRenderContext")); + } + + // Clean up + renderer.destroyContext(ctx); + } + +} + +Gtk::Widget *Print::create_custom_widget() +{ + //printf("%s\n",__FUNCTION__); + return &_tab; +} + +void Print::begin_print(const Glib::RefPtr<Gtk::PrintContext>&) +{ + //printf("%s\n",__FUNCTION__); + _printop->set_n_pages(1); +} + +Gtk::PrintOperationResult Print::run(Gtk::PrintOperationAction, Gtk::Window &parent_window) +{ + // Remember to restore the previous print settings + _printop->set_print_settings(SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings); + + try { + Gtk::PrintOperationResult res = _printop->run(Gtk::PRINT_OPERATION_ACTION_PRINT_DIALOG, parent_window); + + // Save printer settings (but only on success) + if (res == Gtk::PRINT_OPERATION_RESULT_APPLY) { + SP_ACTIVE_DESKTOP->printer_settings._gtk_print_settings = _printop->get_print_settings(); + } + + return res; + } catch (const Glib::Error &e) { + g_warning("Failed to print '%s': %s", _doc->getDocumentName(), e.what().c_str()); + } + + return Gtk::PRINT_OPERATION_RESULT_ERROR; +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/print.h b/src/ui/dialog/print.h new file mode 100644 index 0000000..d015210 --- /dev/null +++ b/src/ui/dialog/print.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Print dialog + */ +/* Authors: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_PRINT_H +#define INKSCAPE_UI_DIALOG_PRINT_H + +#include "ui/widget/rendering-options.h" +#include <gtkmm/printoperation.h> // GtkMM + +class SPItem; +class SPDocument; + + +/* + * gtk 2.12.0 has a bug (http://bugzilla.gnome.org/show_bug.cgi?id=482089) + * where it fails to correctly deal with gtkmm signal management. As a result + * we have call gtk directly instead of doing a much cleaner version of + * this printing dialog, using full gtkmmification. (The bug was fixed + * in 2.12.1, so when the Inkscape gtk minimum version is bumped there, + * we can revert Inkscape commit 16865. + */ +struct workaround_gtkmm +{ + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions *_tab; +}; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +struct PrinterSettings { + Glib::RefPtr<Gtk::PrintSettings> _gtk_print_settings; +}; + +class Print { +public: + Print(SPDocument *doc, SPItem *base); + Gtk::PrintOperationResult run(Gtk::PrintOperationAction, Gtk::Window &parent_window); + +protected: + +private: + Glib::RefPtr<Gtk::PrintOperation> _printop; + SPDocument *_doc; + SPItem *_base; + Inkscape::UI::Widget::RenderingOptions _tab; + + struct workaround_gtkmm _workaround; + + void draw_page(const Glib::RefPtr<Gtk::PrintContext>& context, int /*page_nr*/); + Gtk::Widget *create_custom_widget(); + void begin_print(const Glib::RefPtr<Gtk::PrintContext>&); +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_PRINT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/save-template-dialog.cpp b/src/ui/dialog/save-template-dialog.cpp new file mode 100644 index 0000000..5a8afa9 --- /dev/null +++ b/src/ui/dialog/save-template-dialog.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "save-template-dialog.h" +#include "file.h" +#include "io/resource.h" + +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +SaveTemplate::SaveTemplate() : + Gtk::Dialog(_("Save Document as Template")), + name_label(_("Name: "), Gtk::ALIGN_START), + author_label(_("Author: "), Gtk::ALIGN_START), + description_label(_("Description: "), Gtk::ALIGN_START), + keywords_label(_("Keywords: "), Gtk::ALIGN_START), + is_default_template(_("Set as default template")) +{ + resize(400, 200); + + name_text.set_hexpand(true); + + grid.attach(name_label, 0, 0, 1, 1); + grid.attach(name_text, 1, 0, 1, 1); + + grid.attach(author_label, 0, 1, 1, 1); + grid.attach(author_text, 1, 1, 1, 1); + + grid.attach(description_label, 0, 2, 1, 1); + grid.attach(description_text, 1, 2, 1, 1); + + grid.attach(keywords_label, 0, 3, 1, 1); + grid.attach(keywords_text, 1, 3, 1, 1); + + auto content_area = get_content_area(); + + content_area->set_spacing(5); + + content_area->add(grid); + content_area->add(is_default_template); + + name_text.signal_changed().connect( sigc::mem_fun(*this, + &SaveTemplate::on_name_changed) ); + + add_button("Cancel", Gtk::RESPONSE_CANCEL); + add_button("Save", Gtk::RESPONSE_OK); + + set_response_sensitive(Gtk::RESPONSE_OK, false); + set_default_response(Gtk::RESPONSE_CANCEL); + + show_all(); +} + +void SaveTemplate::on_name_changed() { + + if (name_text.get_text_length() == 0) { + + set_response_sensitive(Gtk::RESPONSE_OK, false); + } else { + + set_response_sensitive(Gtk::RESPONSE_OK, true); + } +} + +bool SaveTemplate::save_template(Gtk::Window &parentWindow) { + + return sp_file_save_template(parentWindow, name_text.get_text(), + author_text.get_text(), description_text.get_text(), + keywords_text.get_text(), is_default_template.get_active()); +} + +void SaveTemplate::save_document_as_template(Gtk::Window &parentWindow) { + + SaveTemplate dialog; + + auto operation_done = false; + + while (operation_done == false) { + + auto user_response = dialog.run(); + + if (user_response == Gtk::RESPONSE_OK) + operation_done = dialog.save_template(parentWindow); + else + operation_done = true; + } +} + +} +} +} diff --git a/src/ui/dialog/save-template-dialog.h b/src/ui/dialog/save-template-dialog.h new file mode 100644 index 0000000..8cf2d8e --- /dev/null +++ b/src/ui/dialog/save-template-dialog.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H +#define INKSCAPE_SEEN_UI_DIALOG_SAVE_TEMPLATE_H + +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/box.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> +#include <gtkmm/checkbutton.h> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SaveTemplate : public Gtk::Dialog +{ + +public: + + static void save_document_as_template(Gtk::Window &parentWindow); + +protected: + + void on_name_changed(); + +private: + + Gtk::Grid grid; + + Gtk::Label name_label; + Gtk::Entry name_text; + + Gtk::Label author_label; + Gtk::Entry author_text; + + Gtk::Label description_label; + Gtk::Entry description_text; + + Gtk::Label keywords_label; + Gtk::Entry keywords_text; + + Gtk::CheckButton is_default_template; + + SaveTemplate(); + bool save_template(Gtk::Window &parentWindow); + +}; +} +} +} +#endif diff --git a/src/ui/dialog/selectorsdialog.cpp b/src/ui/dialog/selectorsdialog.cpp new file mode 100644 index 0000000..280bfdf --- /dev/null +++ b/src/ui/dialog/selectorsdialog.cpp @@ -0,0 +1,1508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * Jabiertxof + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selectorsdialog.h" +#include "attribute-rel-svg.h" +#include "document-undo.h" +#include "inkscape.h" +#include "selection.h" +#include "style.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/widget/iconrenderer.h" +#include "verbs.h" + +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include <map> +#include <regex> +#include <utility> + +// G_MESSAGES_DEBUG=DEBUG_SELECTORSDIALOG gdb ./inkscape +// #define DEBUG_SELECTORSDIALOG +// #define G_LOG_DOMAIN "SELECTORSDIALOG" + +using Inkscape::DocumentUndo; +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; + +/** + * This macro is used to remove spaces around selectors or any strings when + * parsing is done to update XML style element or row labels in this dialog. + */ +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + if (x.size() && x[0] == ',') \ + x.erase(0, 1); \ + if (x.size() && x[x.size() - 1] == ',') \ + x.erase(x.size() - 1, 1); \ + x.erase(x.find_last_not_of(' ') + 1); + +namespace Inkscape { +namespace UI { +namespace Dialog { + +// Keeps a watch on style element +class SelectorsDialog::NodeObserver : public Inkscape::XML::NodeObserver { + public: + NodeObserver(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeObserver: Constructor"); + }; + + void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + + SelectorsDialog *_selectorsdialog; +}; + + +void SelectorsDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + + g_debug("SelectorsDialog::NodeObserver::notifyContentChanged"); + _selectorsdialog->_scroollock = true; + _selectorsdialog->_updating = false; + _selectorsdialog->_readStyleElement(); + _selectorsdialog->_selectRow(); +} + + +// Keeps a watch for new/removed/changed nodes +// (Must update objects that selectors match.) +class SelectorsDialog::NodeWatcher : public Inkscape::XML::NodeObserver { + public: + NodeWatcher(SelectorsDialog *selectorsdialog) + : _selectorsdialog(selectorsdialog) + { + g_debug("SelectorsDialog::NodeWatcher: Constructor"); + }; + + void notifyChildAdded( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeAdded(child); + } + + void notifyChildRemoved( Inkscape::XML::Node &/*node*/, + Inkscape::XML::Node &child, + Inkscape::XML::Node */*prev*/ ) override + { + _selectorsdialog->_nodeRemoved(child); + } + + void notifyAttributeChanged( Inkscape::XML::Node &node, + GQuark qname, + Util::ptr_shared /*old_value*/, + Util::ptr_shared /*new_value*/ ) override { + + static GQuark const CODE_id = g_quark_from_static_string("id"); + static GQuark const CODE_class = g_quark_from_static_string("class"); + + if (qname == CODE_id || qname == CODE_class) { + _selectorsdialog->_nodeChanged(node); + } + } + + SelectorsDialog *_selectorsdialog; +}; + +void SelectorsDialog::_nodeAdded(Inkscape::XML::Node &node) +{ + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (_textNode == &repr) { + _textNode = nullptr; + } + + _readStyleElement(); + _selectRow(); +} + +void SelectorsDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + + g_debug("SelectorsDialog::NodeChanged"); + + _scroollock = true; + + _readStyleElement(); + _selectRow(); +} + +SelectorsDialog::TreeStore::TreeStore() = default; + + +/** + * Allow dragging only selectors. + */ +bool SelectorsDialog::TreeStore::row_draggable_vfunc(const Gtk::TreeModel::Path &path) const +{ + g_debug("SelectorsDialog::TreeStore::row_draggable_vfunc"); + + auto unconstThis = const_cast<SelectorsDialog::TreeStore *>(this); + const_iterator iter = unconstThis->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + bool is_draggable = row[_selectorsdialog->_mColumns._colType] == SELECTOR; + return is_draggable; + } + return Gtk::TreeStore::row_draggable_vfunc(path); +} + +/** + * Allow dropping only in between other selectors. + */ +bool SelectorsDialog::TreeStore::row_drop_possible_vfunc(const Gtk::TreeModel::Path &dest, + const Gtk::SelectionData &selection_data) const +{ + g_debug("SelectorsDialog::TreeStore::row_drop_possible_vfunc"); + + Gtk::TreeModel::Path dest_parent = dest; + dest_parent.up(); + return dest_parent.empty(); +} + + +// This is only here to handle updating style element after a drag and drop. +void SelectorsDialog::TreeStore::on_row_deleted(const TreeModel::Path &path) +{ + if (_selectorsdialog->_updating) + return; // Don't write if we deleted row (other than from DND) + + g_debug("on_row_deleted"); + _selectorsdialog->_writeStyleElement(); + _selectorsdialog->_readStyleElement(); +} + + +Glib::RefPtr<SelectorsDialog::TreeStore> SelectorsDialog::TreeStore::create(SelectorsDialog *selectorsdialog) +{ + g_debug("SelectorsDialog::TreeStore::create"); + + SelectorsDialog::TreeStore *store = new SelectorsDialog::TreeStore(); + store->_selectorsdialog = selectorsdialog; + store->set_column_types(store->_selectorsdialog->_mColumns); + return Glib::RefPtr<SelectorsDialog::TreeStore>(store); +} + +/** + * Constructor + * A treeview and a set of two buttons are added to the dialog. _addSelector + * adds selectors to treeview. _delSelector deletes the selector from the dialog. + * Any addition/deletion of the selectors updates XML style element accordingly. + */ +SelectorsDialog::SelectorsDialog() + : UI::Widget::Panel("/dialogs/selectors", SP_VERB_DIALOG_SELECTORS) + , _updating(false) + , _textNode(nullptr) + , _scroolpos(0) + , _scroollock(false) + , _desktopTracker() +{ + g_debug("SelectorsDialog::SelectorsDialog"); + + m_nodewatcher.reset(new SelectorsDialog::NodeWatcher(this)); + m_styletextwatcher.reset(new SelectorsDialog::NodeObserver(this)); + + // Tree + Inkscape::UI::Widget::IconRenderer * addRenderer = manage( + new Inkscape::UI::Widget::IconRenderer() ); + addRenderer->add_icon("edit-delete"); + addRenderer->add_icon("list-add"); + addRenderer->add_icon("empty-icon"); + _store = TreeStore::create(this); + _treeView.set_model(_store); + + _treeView.set_headers_visible(false); + _treeView.enable_model_drag_source(); + _treeView.enable_model_drag_dest( Gdk::ACTION_MOVE ); + int addCol = _treeView.append_column("", *addRenderer) - 1; + Gtk::TreeViewColumn *col = _treeView.get_column(addCol); + if ( col ) { + col->add_attribute(addRenderer->property_icon(), _mColumns._colType); + } + + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + addCol = _treeView.append_column("CSS Selector", *label) - 1; + col = _treeView.get_column(addCol); + if (col) { + col->add_attribute(label->property_text(), _mColumns._colSelector); + col->add_attribute(label->property_weight(), _mColumns._colSelected); + } + _treeView.set_expander_column(*(_treeView.get_column(1))); + + + // Signal handlers + _treeView.signal_button_release_event().connect( // Needs to be release, not press. + sigc::mem_fun(*this, &SelectorsDialog::_handleButtonEvent), false); + + _treeView.signal_button_release_event().connect_notify( + sigc::mem_fun(*this, &SelectorsDialog::_buttonEventsSelectObjs), false); + + _treeView.signal_row_expanded().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowExpand)); + + _treeView.signal_row_collapsed().connect(sigc::mem_fun(*this, &SelectorsDialog::_rowCollapse)); + + _showWidgets(); + + // Document & Desktop + _desktop_changed_connection = + _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &SelectorsDialog::_handleDesktopChanged)); + _desktopTracker.connect(GTK_WIDGET(gobj())); + + _document_replaced_connection = + getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _selection_changed_connection = getDesktop()->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + // Add watchers + _updateWatchers(getDesktop()); + + // Load tree + _readStyleElement(); + _selectRow(); + + if (!_store->children().empty()) { + _del.show(); + } + show_all(); +} + + +void SelectorsDialog::_vscrool() +{ + if (!_scroollock) { + _scroolpos = _vadj->get_value(); + } else { + _vadj->set_value(_scroolpos); + _scroollock = false; + } +} + +void SelectorsDialog::_showWidgets() +{ + // Pack widgets + g_debug("SelectorsDialog::_showWidgets"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = prefs->getBool("/dialogs/selectors/vertical", true); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _selectors_box.set_orientation(Gtk::ORIENTATION_VERTICAL); + _selectors_box.set_name("SelectorsDialog"); + _scrolled_window_selectors.add(_treeView); + _scrolled_window_selectors.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _vadj = _scrolled_window_selectors.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_vscrool)); + _selectors_box.pack_start(_scrolled_window_selectors, Gtk::PACK_EXPAND_WIDGET); + /* Gtk::Label *dirtogglerlabel = Gtk::manage(new Gtk::Label(_("Paned vertical"))); + dirtogglerlabel->get_style_context()->add_class("inksmall"); + _direction.property_active() = dir; + _direction.property_active().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection)); + _direction.get_style_context()->add_class("inkswitch"); */ + _styleButton(_create, "list-add", "Add a new CSS Selector"); + _create.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_addSelector)); + _styleButton(_del, "list-remove", "Remove a CSS Selector"); + _button_box.pack_start(_create, Gtk::PACK_SHRINK); + _button_box.pack_start(_del, Gtk::PACK_SHRINK); + Gtk::RadioButton::Group group; + Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton()); + Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton()); + _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal")); + _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical")); + _horizontal->set_group(group); + _vertical->set_group(group); + _vertical->set_active(dir); + _vertical->signal_toggled().connect( + sigc::bind(sigc::mem_fun(*this, &SelectorsDialog::_toggleDirection), _vertical)); + _horizontal->property_draw_indicator() = false; + _vertical->property_draw_indicator() = false; + _button_box.pack_end(*_horizontal, false, false, 0); + _button_box.pack_end(*_vertical, false, false, 0); + _del.signal_clicked().connect(sigc::mem_fun(*this, &SelectorsDialog::_delSelector)); + _del.hide(); + _style_dialog = new StyleDialog; + _style_dialog->set_name("StyleDialog"); + _paned.pack1(*_style_dialog, Gtk::SHRINK); + _paned.pack2(_selectors_box, true, true); + _paned.set_wide_handle(true); + Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + contents->pack_start(_paned, Gtk::PACK_EXPAND_WIDGET); + contents->pack_start(_button_box, false, false, 0); + contents->set_valign(Gtk::ALIGN_FILL); + contents->child_property_fill(_paned); + Gtk::ScrolledWindow *dialog_scroller = new Gtk::ScrolledWindow(); + dialog_scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + dialog_scroller->set_shadow_type(Gtk::SHADOW_IN); + dialog_scroller->add(*Gtk::manage(contents)); + _getContents()->pack_start(*dialog_scroller, Gtk::PACK_EXPAND_WIDGET); + show_all(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + int panedpos = prefs->getInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &SelectorsDialog::_childresized)); + _paned.signal_size_allocate().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedresized)); + _paned.signal_realize().connect(sigc::mem_fun(*this, &SelectorsDialog::_panedrealized)); + _updating = true; + _paned.property_position() = panedpos; + _updating = false; + set_size_request(320, 260); + set_name("SelectorsAndStyleDialog"); +} + +void SelectorsDialog::_panedresized(Gtk::Allocation allocation) +{ + g_debug("SelectorsDialog::_panedresized"); + _resized(); +} + +void SelectorsDialog::_panedrealized() { _style_dialog->readStyleElement(); } + +void SelectorsDialog::_childresized() +{ + g_debug("SelectorsDialog::_childresized"); + _resized(); +} + +void SelectorsDialog::_resized() +{ + g_debug("SelectorsDialog::_resized"); + _scroollock = true; + if (_updating) { + return; + } + _updating = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = !prefs->getBool("/dialogs/selectors/vertical", true); + int max = int(_paned.property_max_position() * 0.95); + int min = int(_paned.property_max_position() * 0.05); + if (_paned.property_position() > max) { + _paned.property_position() = max; + } + if (_paned.property_position() < min) { + _paned.property_position() = min; + } + + prefs->setInt("/dialogs/selectors/panedpos", _paned.property_position()); + _updating = false; +} + +void SelectorsDialog::_toggleDirection(Gtk::RadioButton *vertical) +{ + g_debug("SelectorsDialog::_toggleDirection"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = vertical->get_active(); + prefs->setBool("/dialogs/selectors/vertical", dir); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + int widthpos = _paned.property_max_position() - _paned.property_min_position(); + prefs->setInt("/dialogs/selectors/panedpos", widthpos / 2); + _paned.property_position() = widthpos / 2; +} + +/** + * Class destructor + */ +SelectorsDialog::~SelectorsDialog() +{ + g_debug("SelectorsDialog::~SelectorsDialog"); + _desktop_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + _selection_changed_connection.disconnect(); +} + + +/** + * @return Inkscape::XML::Node* pointing to a style element's text node. + * Returns the style element's text node. If there is no style element, one is created. + * Ditto for text node. + */ +Inkscape::XML::Node *SelectorsDialog::_getStyleTextNode(bool create_if_missing) +{ + g_debug("SelectorsDialog::_getStyleTextNode"); + + auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing); + + if (_textNode != textNode) { + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + } + + _textNode = textNode; + + if (_textNode) { + _textNode->addObserver(*m_styletextwatcher); + } + } + + return textNode; +} + +/** + * Fill the Gtk::TreeStore from the svg:style element. + */ +void SelectorsDialog::_readStyleElement() +{ + g_debug("SelectorsDialog::_readStyleElement(): updating %s", (_updating ? "true" : "false")); + + if (_updating) return; // Don't read if we wrote style element. + _updating = true; + _scroollock = true; + Inkscape::XML::Node * textNode = _getStyleTextNode(); + + // Get content from style text node. + std::string content = (textNode && textNode->content()) ? textNode->content() : ""; + + // Remove end-of-lines (check it works on Windoze). + content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); + + // Remove comments (/* xxx */) +#if 0 + while(content.find("/*") != std::string::npos) { + size_t start = content.find("/*"); + content.erase(start, (content.find("*\/", start) - start) +2); + } +#endif + + // First split into selector/value chunks. + // An attempt to use Glib::Regex failed. A C++11 version worked but + // reportedly has problems on Windows. Using split_simple() is simpler + // and probably faster. + // + // Glib::RefPtr<Glib::Regex> regex1 = + // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); + // + // Glib::MatchInfo minfo; + // regex1->match(content, minfo); + + // Split on curly brackets. Even tokens are selectors, odd are values. + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content); + + // If text node is empty, return (avoids problem with negative below). + if (tokens.size() == 0) { + _store->clear(); + _updating = false; + return; + } + _treeView.show_all(); + std::vector<std::pair<Glib::ustring, bool>> expanderstatus; + for (unsigned i = 0; i < tokens.size() - 1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + if (!selectordata.empty()) { + selector = selectordata.back(); + } + selector = _style_dialog->fixCSSSelectors(selector); + for (auto &row : _store->children()) { + Glib::ustring selectorold = row[_mColumns._colSelector]; + if (selectorold == selector) { + expanderstatus.emplace_back(selector, row[_mColumns._colExpand]); + } + } + } + _store->clear(); + bool rewrite = false; + + + std::vector<SPObject *> objVec; + for (unsigned i = 0; i < tokens.size()-1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + for (auto selectoritem : selectordata) { + if (selectordata[selectordata.size() - 1] == selectoritem) { + selector = selectoritem; + } else { + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selectoritem + ";"; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } + } + Glib::ustring selector_old = selector; + selector = _style_dialog->fixCSSSelectors(selector); + if (selector_old != selector) { + rewrite = true; + } + + if (selector.empty() || selector == "* > .inkscapehacktmp") { + continue; + } + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[,]+", selector); + coltype colType = SELECTOR; + // Get list of objects selector matches + objVec = _getObjVec(selector); + + Glib::ustring properties; + // Check to make sure we do have a value to match selector. + if ((i+1) < tokens.size()) { + properties = tokens[i+1]; + } else { + std::cerr << "SelectorsDialog::_readStyleElement(): Missing values " + "for last selector!" + << std::endl; + } + REMOVE_SPACES(properties); + bool colExpand = false; + for (auto rowstatus : expanderstatus) { + if (selector == rowstatus.first) { + colExpand = rowstatus.second; + } + } + std::vector<Glib::ustring> properties_data = Glib::Regex::split_simple(";", properties); + Gtk::TreeModel::Row row = *(_store->append()); + row[_mColumns._colSelector] = selector; + row[_mColumns._colExpand] = colExpand; + row[_mColumns._colType] = colType; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = properties; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + // Add as children, objects that match selector. + for (auto &obj : objVec) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->append(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = colType == OBJECT; + childrow[_mColumns._colObj] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + + + _updating = false; + if (rewrite) { + _writeStyleElement(); + } + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +void SelectorsDialog::_rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_expand()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = true; +} + +void SelectorsDialog::_rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path) +{ + g_debug("SelectorsDialog::_row_collapse()"); + Gtk::TreeModel::Row row = *iter; + row[_mColumns._colExpand] = false; +} +/** + * Update the content of the style element as selectors (or objects) are added/removed. + */ +void SelectorsDialog::_writeStyleElement() +{ + + if (_updating) { + return; + } + + g_debug("SelectorsDialog::_writeStyleElement"); + + _scroollock = true; + _updating = true; + Glib::ustring styleContent = ""; + for (auto& row: _store->children()) { + Glib::ustring selector = row[_mColumns._colSelector]; +#if 0 + REMOVE_SPACES(selector); + size_t len = selector.size(); + if(selector[len-1] == ','){ + selector.erase(len-1); + } + row[_mColumns._colSelector] = selector; +#endif + if (row[_mColumns._colType] == OTHER) { + styleContent = selector + styleContent; + } else { + styleContent = styleContent + selector + " { " + row[_mColumns._colProperties] + " }\n"; + } + } + // We could test if styleContent is empty and then delete the style node here but there is no + // harm in keeping it around ... + Inkscape::XML::Node *textNode = _getStyleTextNode(true); + bool empty = false; + if (styleContent.empty()) { + empty = true; + styleContent = "* > .inkscapehacktmp{}"; + } + textNode->setContent(styleContent.c_str()); + INKSCAPE.readStyleSheets(true); + if (empty) { + styleContent = ""; + textNode->setContent(styleContent.c_str()); + } + textNode->setContent(styleContent.c_str()); + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_SELECTORS, _("Edited style element.")); + + _updating = false; + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + g_debug("SelectorsDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +/** + * Update the watchers on objects. + */ +void SelectorsDialog::_updateWatchers(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::_updateWatchers"); + + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } + + if (desktop) { + m_root = desktop->getDocument()->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } +} +/* +void sp_get_selector_active(Glib::ustring &selector) +{ + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + REMOVE_SPACES(selector); + Glib::ustring toadd = Glib::ustring(selector); + Glib::ustring toparse = Glib::ustring(selector); + Glib::ustring tag = ""; + if (toadd[0] != '.' || toadd[0] != '#') { + auto i = std::min(toadd.find("#"), toadd.find(".")); + tag = toadd.substr(0,i-1); + toparse.erase(0, i-1); + } + auto i = toparse.find("#"); + toparse.erase(i, 1); + auto j = toparse.find("#"); + if (j == std::string::npos) { + selector = ""; + } else if (i != std::string::npos) { + Glib::ustring post = toadd.substr(0,i-1); + Glib::ustring pre = toadd.substr(i, (toadd.size()-1)-i); + selector = tag + pre + post; + } +} */ + +Glib::ustring sp_get_selector_classes(Glib::ustring selector) //, SelectorType selectortype, Glib::ustring id = "") +{ + g_debug("SelectorsDialog::sp_get_selector_classes"); + + std::pair<Glib::ustring, Glib::ustring> result; + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[ ]+", selector); + selector = tokensplus[tokensplus.size() - 1]; + // Erase any comma/space + REMOVE_SPACES(selector); + Glib::ustring toparse = Glib::ustring(selector); + selector = Glib::ustring(""); + auto i = toparse.find("."); + if (i == std::string::npos) { + return ""; + } + if (toparse[0] != '.' && toparse[0] != '#') { + i = std::min(toparse.find("#"), toparse.find(".")); + Glib::ustring tag = toparse.substr(0, i); + if (!SPAttributeRelSVG::isSVGElement(tag)) { + return selector; + } + if (i != std::string::npos) { + toparse.erase(0, i); + } + } + i = toparse.find("#"); + if (i != std::string::npos) { + toparse.erase(i, 1); + } + auto j = toparse.find("#"); + if (j != std::string::npos) { + return selector; + } + if (i != std::string::npos) { + toparse.insert(i, "#"); + if (i) { + Glib::ustring post = toparse.substr(0, i); + Glib::ustring pre = toparse.substr(i, toparse.size() - i); + toparse = pre + post; + } + auto k = toparse.find("."); + if (k != std::string::npos) { + toparse = toparse.substr(k, toparse.size() - k); + } + } + return toparse; +} + +/** + * @param row + * Add selected objects on the desktop to the selector corresponding to 'row'. + */ +void SelectorsDialog::_addToSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_addToSelector: Entrance"); + if (*row) { + // Store list of selected elements on desktop (not to be confused with selector). + _updating = true; + if (row[_mColumns._colType] == OTHER) { + return; + } + Inkscape::Selection *selection = getDesktop()->getSelection(); + std::vector<SPObject *> toAddObjVec(selection->objects().begin(), selection->objects().end()); + Glib::ustring multiselector = row[_mColumns._colSelector]; + std::vector<SPObject *> objVec = _getObjVec(multiselector); + row[_mColumns._colObj] = objVec; + row[_mColumns._colExpand] = true; + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector); + for (auto &obj : toAddObjVec) { + auto *id = obj->getId(); + if (!id) + continue; + for (auto tok : tokens) { + Glib::ustring clases = sp_get_selector_classes(tok); + if (!clases.empty()) { + _insertClass(obj, clases); + std::vector<SPObject *> currentobjs = _getObjVec(multiselector); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + std::vector<SPObject *> currentobjs = _getObjVec(multiselector); + bool insertid = true; + for (auto currentobj : currentobjs) { + if (g_strcmp0(currentobj->getId(), id) == 0) { + insertid = false; + } + } + if (insertid) { + multiselector = multiselector + ",#" + id; + } + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + objVec = _getObjVec(multiselector); + row[_mColumns._colSelector] = multiselector; + row[_mColumns._colObj] = objVec; + _updating = false; + + // Add entry to style element + for (auto &obj : toAddObjVec) { + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + SPCSSAttr *css_selector = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + Glib::ustring selprops = row[_mColumns._colProperties]; + sp_repr_css_attr_add_from_string(css_selector, selprops.c_str()); + for (List<AttributeRecord const> iter = css_selector->attributeList(); iter; ++iter) { + gchar const *key = g_quark_to_string(iter->key); + css->setAttribute(key, nullptr); + } + sp_repr_css_write_string(css, css_str); + sp_repr_css_attr_unref(css); + sp_repr_css_attr_unref(css_selector); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + _writeStyleElement(); + } +} + +/** + * @param row + * Remove the object corresponding to 'row' from the parent selector. + */ +void SelectorsDialog::_removeFromSelector(Gtk::TreeModel::Row row) +{ + g_debug("SelectorsDialog::_removeFromSelector: Entrance"); + if (*row) { + _scroollock = true; + _updating = true; + SPObject *obj = nullptr; + Glib::ustring objectLabel = row[_mColumns._colSelector]; + Gtk::TreeModel::iterator iter = row->parent(); + if (iter) { + Gtk::TreeModel::Row parent = *iter; + Glib::ustring multiselector = parent[_mColumns._colSelector]; + REMOVE_SPACES(multiselector); + obj = _getObjVec(objectLabel)[0]; + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", multiselector); + Glib::ustring selector = ""; + for (auto tok : tokens) { + if (tok.empty()) { + continue; + } + // TODO: handle when other selectors has the removed class applied to maybe not remove + Glib::ustring clases = sp_get_selector_classes(tok); + if (!clases.empty()) { + _removeClass(obj, tok, true); + } + auto i = tok.find(row[_mColumns._colSelector]); + if (i == std::string::npos) { + selector = selector.empty() ? tok : selector + "," + tok; + } + } + REMOVE_SPACES(selector); + if (selector.empty()) { + _store->erase(parent); + + } else { + _store->erase(row); + parent[_mColumns._colSelector] = selector; + parent[_mColumns._colExpand] = true; + parent[_mColumns._colObj] = _getObjVec(selector); + } + } + _updating = false; + + // Add entry to style element + _writeStyleElement(); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } +} + + +/** + * @param sel + * @return This function returns a comma separated list of ids for objects in input vector. + * It is used in creating an 'id' selector. It relies on objects having 'id's. + */ +Glib::ustring SelectorsDialog::_getIdList(std::vector<SPObject *> sel) +{ + g_debug("SelectorsDialog::_getIdList"); + + Glib::ustring str; + for (auto& obj: sel) { + char const *id = obj->getId(); + if (id) { + if (!str.empty()) { + str.append(", "); + } + str.append("#").append(id); + } + } + return str; +} + +/** + * @param selector: a valid CSS selector string. + * @return objVec: a vector of pointers to SPObject's the selector matches. + * Return a vector of all objects that selector matches. + */ +std::vector<SPObject *> SelectorsDialog::_getObjVec(Glib::ustring selector) +{ + + g_debug("SelectorsDialog::_getObjVec: | %s |", selector.c_str()); + + g_assert(selector.find(";") == Glib::ustring::npos); + + return getDesktop()->getDocument()->getObjectsBySelector(selector); +} + + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + for (auto& obj: objVec) { + _insertClass(obj, className); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_insertClass(SPObject *obj, const Glib::ustring &className) +{ + g_debug("SelectorsDialog::_insertClass"); + + Glib::ustring classAttr = Glib::ustring(""); + if (obj->getRepr()->attribute("class")) { + classAttr = obj->getRepr()->attribute("class"); + } + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className); + std::sort(tokens.begin(), tokens.end()); + tokens.erase(std::unique(tokens.begin(), tokens.end()), tokens.end()); + std::vector<Glib::ustring> tokensplus = Glib::Regex::split_simple("[\\s]+", classAttr); + for (auto tok : tokens) { + bool exist = false; + for (auto &tokenplus : tokensplus) { + if (tokenplus == tok) { + exist = true; + } + } + if (!exist) { + classAttr = classAttr.empty() ? tok : classAttr + " " + tok; + } + } + obj->getRepr()->setAttribute("class", classAttr); +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all) +{ + g_debug("SelectorsDialog::_removeClass"); + + for (auto &obj : objVec) { + _removeClass(obj, className, all); + } +} + +/** + * @param objs: list of objects to insert class + * @param class: class to insert + * Insert a class name into objects' 'class' attribute. + */ +void SelectorsDialog::_removeClass(SPObject *obj, const Glib::ustring &className, bool all) // without "." +{ + g_debug("SelectorsDialog::_removeClass"); + + if (obj->getRepr()->attribute("class")) { + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[.]+", className); + Glib::ustring classAttr = obj->getRepr()->attribute("class"); + Glib::ustring classAttrRestore = classAttr; + bool notfound = false; + for (auto tok : tokens) { + auto i = classAttr.find(tok); + if (i != std::string::npos) { + classAttr.erase(i, tok.length()); + } else { + notfound = true; + } + } + if (all && notfound) { + classAttr = classAttrRestore; + } + REMOVE_SPACES(classAttr); + if (classAttr.empty()) { + obj->getRepr()->removeAttribute("class"); + } else { + obj->getRepr()->setAttribute("class", classAttr); + } + } +} + + +/** + * @param eventX + * @param eventY + * This function selects objects in the drawing corresponding to the selector + * selected in the treeview. + */ +void SelectorsDialog::_selectObjects(int eventX, int eventY) +{ + g_debug("SelectorsDialog::_selectObjects: %d, %d", eventX, eventY); + Gtk::TreeViewColumn *col = _treeView.get_column(1); + Gtk::TreeModel::Path path; + int x2 = 0; + int y2 = 0; + // To do: We should be able to do this via passing in row. + if (_treeView.get_path_at_pos(eventX, eventY, path, col, x2, y2)) { + if (_lastpath.size() && _lastpath == path) { + return; + } + if (col == _treeView.get_column(1) && x2 > 25) { + getDesktop()->selection->clear(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Gtk::TreeModel::Children children = row.children(); + if (children.empty() || children.size() == 1) { + _del.show(); + } + std::vector<SPObject *> objVec = row[_mColumns._colObj]; + + for (auto obj : objVec) { + getDesktop()->selection->add(obj); + } + } + _lastpath = path; + } + } +} + +/** + * This function opens a dialog to add a selector. The dialog is prefilled + * with an 'id' selector containing a list of the id's of selected objects + * or with a 'class' selector if no objects are selected. + */ +void SelectorsDialog::_addSelector() +{ + g_debug("SelectorsDialog::_addSelector: Entrance"); + _scroollock = true; + // Store list of selected elements on desktop (not to be confused with selector). + Inkscape::Selection* selection = getDesktop()->getSelection(); + std::vector<SPObject *> objVec( selection->objects().begin(), + selection->objects().end() ); + + // ==== Create popup dialog ==== + Gtk::Dialog *textDialogPtr = new Gtk::Dialog(); + textDialogPtr->property_modal() = true; + textDialogPtr->property_title() = _("CSS selector"); + textDialogPtr->property_window_position() = Gtk::WIN_POS_CENTER_ON_PARENT; + textDialogPtr->add_button(_("Cancel"), Gtk::RESPONSE_CANCEL); + textDialogPtr->add_button(_("Add"), Gtk::RESPONSE_OK); + + Gtk::Entry *textEditPtr = manage ( new Gtk::Entry() ); + textEditPtr->signal_activate().connect( + sigc::bind<Gtk::Dialog *>(sigc::mem_fun(*this, &SelectorsDialog::_closeDialog), textDialogPtr)); + textDialogPtr->get_content_area()->pack_start(*textEditPtr, Gtk::PACK_SHRINK); + + Gtk::Label *textLabelPtr = manage(new Gtk::Label(_("Invalid CSS selector."))); + textDialogPtr->get_content_area()->pack_start(*textLabelPtr, Gtk::PACK_SHRINK); + + /** + * By default, the entrybox contains 'Class1' as text. However, if object(s) + * is(are) selected and user clicks '+' at the bottom of dialog, the + * entrybox will have the id(s) of the selected objects as text. + */ + if (getDesktop()->getSelection()->isEmpty()) { + textEditPtr->set_text(".Class1"); + } else { + textEditPtr->set_text(_getIdList(objVec)); + } + + Gtk::Requisition sreq1, sreq2; + textDialogPtr->get_preferred_size(sreq1, sreq2); + int minWidth = 200; + int minHeight = 100; + minWidth = (sreq2.width > minWidth ? sreq2.width : minWidth ); + minHeight = (sreq2.height > minHeight ? sreq2.height : minHeight); + textDialogPtr->set_size_request(minWidth, minHeight); + textEditPtr->show(); + textLabelPtr->hide(); + textDialogPtr->show(); + + + // ==== Get response ==== + int result = -1; + bool invalid = true; + Glib::ustring selectorValue; + Glib::ustring originalValue; + while (invalid) { + result = textDialogPtr->run(); + if (result != Gtk::RESPONSE_OK) { // Cancel, close dialog, etc. + textDialogPtr->hide(); + delete textDialogPtr; + return; + } + /** + * @brief selectorName + * This string stores selector name. The text from entrybox is saved as name + * for selector. If the entrybox is empty, the text (thus selectorName) is + * set to ".Class1" + */ + originalValue = Glib::ustring(textEditPtr->get_text()); + selectorValue = _style_dialog->fixCSSSelectors(originalValue); + _del.show(); + if (originalValue.find("@import ") == std::string::npos && selectorValue.empty()) { + textLabelPtr->show(); + } else { + invalid = false; + } + } + delete textDialogPtr; + // ==== Handle response ==== + // If class selector, add selector name to class attribute for each object + REMOVE_SPACES(selectorValue); + if (originalValue.find("@import ") != std::string::npos) { + std::vector<SPObject *> objVecEmpty; + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colSelector] = originalValue; + row[_mColumns._colExpand] = false; + row[_mColumns._colType] = OTHER; + row[_mColumns._colObj] = objVecEmpty; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + } else { + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selectorValue); + for (auto &obj : objVec) { + for (auto tok : tokens) { + Glib::ustring clases = sp_get_selector_classes(tok); + if (clases.empty()) { + continue; + } + _insertClass(obj, clases); + std::vector<SPObject *> currentobjs = _getObjVec(selectorValue); + bool removeclass = true; + for (auto currentobj : currentobjs) { + if (currentobj == obj) { + removeclass = false; + } + } + if (removeclass) { + _removeClass(obj, clases); + } + } + } + objVec = _getObjVec(selectorValue); + Gtk::TreeModel::Row row = *(_store->prepend()); + row[_mColumns._colExpand] = true; + row[_mColumns._colType] = SELECTOR; + row[_mColumns._colSelector] = selectorValue; + row[_mColumns._colObj] = objVec; + row[_mColumns._colProperties] = ""; + row[_mColumns._colVisible] = true; + row[_mColumns._colSelected] = 400; + for (auto &obj : objVec) { + auto *id = obj->getId(); + if (!id) + continue; + Gtk::TreeModel::Row childrow = *(_store->prepend(row->children())); + childrow[_mColumns._colSelector] = "#" + Glib::ustring(id); + childrow[_mColumns._colExpand] = false; + childrow[_mColumns._colType] = OBJECT; + childrow[_mColumns._colObj] = std::vector<SPObject *>(1, obj); + childrow[_mColumns._colProperties] = ""; // Unused + childrow[_mColumns._colVisible] = true; // Unused + childrow[_mColumns._colSelected] = 400; + } + } + // Add entry to style element + _writeStyleElement(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +void SelectorsDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + +/** + * This function deletes selector when '-' at the bottom is clicked. + * Note: If deleting a class selector, class attributes are NOT changed. + */ +void SelectorsDialog::_delSelector() +{ + g_debug("SelectorsDialog::_delSelector"); + + _scroollock = true; + Glib::RefPtr<Gtk::TreeSelection> refTreeSelection = _treeView.get_selection(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + Gtk::TreeModel::iterator iter = refTreeSelection->get_selected(); + if (iter) { + _vscrool(); + Gtk::TreeModel::Row row = *iter; + if (row.children().size() > 2) { + return; + } + _updating = true; + _store->erase(iter); + _updating = false; + _writeStyleElement(); + _del.hide(); + _scroollock = false; + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } +} + +/** + * @param event + * @return + * Handles the event when '+' button in front of a selector name is clicked or when a '-' button in + * front of a child object is clicked. In the first case, the selected objects on the desktop (if + * any) are added as children of the selector in the treeview. In the latter case, the object + * corresponding to the row is removed from the selector. + */ +bool SelectorsDialog::_handleButtonEvent(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_handleButtonEvent: Entrance"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + _scroollock = true; + Gtk::TreeViewColumn *col = nullptr; + Gtk::TreeModel::Path path; + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + int x2 = 0; + int y2 = 0; + + if (_treeView.get_path_at_pos(x, y, path, col, x2, y2)) { + if (col == _treeView.get_column(0)) { + _vscrool(); + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + if (!row.parent()) { + _addToSelector(row); + } else { + _removeFromSelector(row); + } + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); + } + } + } + return false; +} + +// ------------------------------------------------------------------- + +class PropertyData +{ +public: + PropertyData() = default;; + PropertyData(Glib::ustring name) : _name(std::move(name)) {}; + + void _setSheetValue(Glib::ustring value) { _sheetValue = value; }; + void _setAttrValue(Glib::ustring value) { _attrValue = value; }; + Glib::ustring _getName() { return _name; }; + Glib::ustring _getSheetValue() { return _sheetValue; }; + Glib::ustring _getAttrValue() { return _attrValue; }; + +private: + Glib::ustring _name; + Glib::ustring _sheetValue; + Glib::ustring _attrValue; +}; + +// ------------------------------------------------------------------- + + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void SelectorsDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) +{ + g_debug("SelectorsDialog::handleDocumentReplaced()"); + + _selection_changed_connection.disconnect(); + + _updateWatchers(desktop); + + if (!desktop) + return; + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + + _readStyleElement(); + _selectRow(); +} + + +/* + * When a dialog is floating, it is connected to the active desktop. + */ +void SelectorsDialog::_handleDesktopChanged(SPDesktop *desktop) +{ + g_debug("SelectorsDialog::handleDesktopReplaced()"); + + if (getDesktop() == desktop) { + // This will happen after construction of dialog. We've already + // set up signals so just return. + return; + } + + _selection_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + + setDesktop( desktop ); + + _selection_changed_connection = desktop->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &SelectorsDialog::_handleSelectionChanged))); + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &SelectorsDialog::_handleDocumentReplaced)); + + _updateWatchers(desktop); + _readStyleElement(); + _selectRow(); +} + + +/* + * Handle a change in which objects are selected in a document. + */ +void SelectorsDialog::_handleSelectionChanged() +{ + g_debug("SelectorsDialog::_handleSelectionChanged()"); + _lastpath.clear(); + _treeView.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _readStyleElement(); + _selectRow(); +} + + +/** + * @param event + * This function detects single or double click on a selector in any row. Clicking + * on a selector selects the matching objects on the desktop. A double click will + * in addition open the CSS dialog. + */ +void SelectorsDialog::_buttonEventsSelectObjs(GdkEventButton *event) +{ + g_debug("SelectorsDialog::_buttonEventsSelectObjs"); + _treeView.get_selection()->set_mode(Gtk::SELECTION_SINGLE); + _updating = true; + _del.show(); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + int x = static_cast<int>(event->x); + int y = static_cast<int>(event->y); + _selectObjects(x, y); + } + _updating = false; +} + + +/** + * This function selects the row in treeview corresponding to an object selected + * in the drawing. If more than one row matches, the first is chosen. + */ +void SelectorsDialog::_selectRow() +{ + _scroollock = true; + g_debug("SelectorsDialog::_selectRow: updating: %s", (_updating ? "true" : "false")); + _del.hide(); + std::vector<Gtk::TreeModel::Path> selectedrows = _treeView.get_selection()->get_selected_rows(); + if (selectedrows.size() == 1) { + Gtk::TreeModel::Row row = *_store->get_iter(selectedrows[0]); + if (!row->parent() && row->children().size() < 2) { + _del.show(); + } + if (!row->parent()) { + _style_dialog->setCurrentSelector(row[_mColumns._colSelector]); + } + } else if (selectedrows.size() == 0) { + _del.show(); + } + if (_updating || !getDesktop()) return; // Avoid updating if we have set row via dialog. + + _treeView.get_selection()->unselect_all(); + Gtk::TreeModel::Children children = _store->children(); + Inkscape::Selection* selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (!selection->isEmpty()) { + obj = selection->objects().back(); + } else { + _style_dialog->setCurrentSelector(""); + } + for (auto row : children) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + subrow[_mColumns._colSelected] = 400; + } + } + for (auto obj : selection->items()) { + for (auto row : children) { + Gtk::TreeModel::Children subchildren = row->children(); + for (auto subrow : subchildren) { + std::vector<SPObject *> objVec = subrow[_mColumns._colObj]; + if (obj == objVec[0]) { + _treeView.get_selection()->select(row); + row[_mColumns._colVisible] = true; + subrow[_mColumns._colSelected] = 700; + } + } + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + } + for (auto row : children) { + if (row[_mColumns._colExpand]) { + _treeView.expand_to_path(Gtk::TreePath(row)); + } + } + _vadj->set_value(std::min(_scroolpos, _vadj->get_upper())); +} + +/** + * @param btn + * @param iconName + * @param tooltip + * Set the style of '+' and '-' buttons at the bottom of dialog. + */ +void SelectorsDialog::_styleButton(Gtk::Button &btn, char const *iconName, char const *tooltip) +{ + g_debug("SelectorsDialog::_styleButton"); + + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn.add(*manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/selectorsdialog.h b/src/ui/dialog/selectorsdialog.h new file mode 100644 index 0000000..2c0e363 --- /dev/null +++ b/src/ui/dialog/selectorsdialog.h @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SELECTORSDIALOG_H +#define SELECTORSDIALOG_H + +#include "ui/dialog/desktop-tracker.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/styledialog.h" +#include "ui/widget/panel.h" +#include <gtkmm/dialog.h> +#include <gtkmm/paned.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/switch.h> +#include <gtkmm/treemodelfilter.h> +#include <gtkmm/treeselection.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <ui/widget/panel.h> + +#include "xml/helper-observer.h" + +#include <memory> +#include <vector> + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * @brief The SelectorsDialog class + * A list of CSS selectors will show up in this dialog. This dialog allows one to + * add and delete selectors. Elements can be added to and removed from the selectors + * in the dialog. Selection of any selector row selects the matching objects in + * the drawing and vice-versa. (Only simple selectors supported for now.) + * + * This class must keep two things in sync: + * 1. The text node of the style element. + * 2. The Gtk::TreeModel. + */ +class SelectorsDialog : public Widget::Panel { + + public: + ~SelectorsDialog() override; + // No default constructor, noncopyable, nonassignable + SelectorsDialog(); + SelectorsDialog(SelectorsDialog const &d) = delete; + SelectorsDialog operator=(SelectorsDialog const &d) = delete; + static SelectorsDialog &getInstance() { return *new SelectorsDialog(); } + + private: + // Monitor <style> element for changes. + class NodeObserver; + + // Monitor all objects for addition/removal/attribute change + class NodeWatcher; + enum SelectorType { CLASS, ID, TAG }; + void _nodeAdded( Inkscape::XML::Node &repr ); + void _nodeRemoved( Inkscape::XML::Node &repr ); + void _nodeChanged( Inkscape::XML::Node &repr ); + // Data structure + enum coltype { OBJECT, SELECTOR, OTHER }; + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(_colSelector); + add(_colExpand); + add(_colType); + add(_colObj); + add(_colProperties); + add(_colVisible); + add(_colSelected); + } + Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Selector or matching object id. + Gtk::TreeModelColumn<bool> _colExpand; // Open/Close store row. + Gtk::TreeModelColumn<gint> _colType; // Selector row or child object row. + Gtk::TreeModelColumn<std::vector<SPObject *> > _colObj; // List of matching objects. + Gtk::TreeModelColumn<Glib::ustring> _colProperties; // List of properties. + Gtk::TreeModelColumn<bool> _colVisible; // Make visible or not. + Gtk::TreeModelColumn<gint> _colSelected; // Make selected. + }; + ModelColumns _mColumns; + + // Override Gtk::TreeStore to control drag-n-drop (only allow dragging and dropping of selectors). + // See: https://developer.gnome.org/gtkmm-tutorial/stable/sec-treeview-examples.html.en + // + // TreeStore implements simple drag and drop (DND) but there appears no way to know when a DND + // has been completed (other than doing the whole DND ourselves). As a hack, we use + // on_row_deleted to trigger write of style element. + class TreeStore : public Gtk::TreeStore { + protected: + TreeStore(); + bool row_draggable_vfunc(const Gtk::TreeModel::Path& path) const override; + bool row_drop_possible_vfunc(const Gtk::TreeModel::Path& path, + const Gtk::SelectionData& selection_data) const override; + void on_row_deleted(const TreeModel::Path& path) override; + + public: + static Glib::RefPtr<SelectorsDialog::TreeStore> create(SelectorsDialog *styledialog); + + private: + SelectorsDialog *_selectorsdialog; + }; + + // TreeView + Glib::RefPtr<Gtk::TreeModelFilter> _modelfilter; + Glib::RefPtr<TreeStore> _store; + Gtk::TreeView _treeView; + Gtk::TreeModel::Path _lastpath; + // Widgets + StyleDialog *_style_dialog; + Gtk::Paned _paned; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Box _button_box; + Gtk::Box _selectors_box; + Gtk::ScrolledWindow _scrolled_window_selectors; + + Gtk::Button _del; + Gtk::Button _create; + // Reading and writing the style element. + Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false); + void _readStyleElement(); + void _writeStyleElement(); + + // Update watchers + std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher; + std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher; + void _updateWatchers(SPDesktop *); + + // Manipulate Tree + void _addToSelector(Gtk::TreeModel::Row row); + void _removeFromSelector(Gtk::TreeModel::Row row); + Glib::ustring _getIdList(std::vector<SPObject *>); + std::vector<SPObject *> _getObjVec(Glib::ustring selector); + void _insertClass(const std::vector<SPObject *>& objVec, const Glib::ustring& className); + void _insertClass(SPObject *obj, const Glib::ustring &className); + void _removeClass(const std::vector<SPObject *> &objVec, const Glib::ustring &className, bool all = false); + void _removeClass(SPObject *obj, const Glib::ustring &className, bool all = false); + void _toggleDirection(Gtk::RadioButton *vertical); + void _showWidgets(); + void _resized(); + void _childresized(); + void _panedresized(Gtk::Allocation allocation); + + void _selectObjects(int, int); + // Variables + double _scroolpos; + bool _scroollock; + bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop + Inkscape::XML::Node *m_root = nullptr; + Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver. + + // Signals and handlers - External + sigc::connection _document_replaced_connection; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void _handleDesktopChanged(SPDesktop* desktop); + void _handleSelectionChanged(); + void _panedrealized(); + void _rowExpand(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _rowCollapse(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _closeDialog(Gtk::Dialog *textDialogPtr); + + DesktopTracker _desktopTracker; + + Inkscape::XML::SignalObserver _objObserver; // Track object in selected row (for style change). + + // Signal and handlers - Internal + void _addSelector(); + void _delSelector(); + bool _handleButtonEvent(GdkEventButton *event); + void _buttonEventsSelectObjs(GdkEventButton *event); + void _selectRow(); // Select row in tree when selection changed. + void _vscrool(); + + // GUI + void _styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip); +}; + +} // namespace Dialogc +} // namespace UI +} // namespace Inkscape + +#endif // SELECTORSDIALOG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/spellcheck.cpp b/src/ui/dialog/spellcheck.cpp new file mode 100644 index 0000000..44e8302 --- /dev/null +++ b/src/ui/dialog/spellcheck.cpp @@ -0,0 +1,815 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spellcheck dialog. + */ +/* Authors: + * bulia byak <bulia@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "spellcheck.h" +#include "message-stack.h" + +#include "inkscape.h" +#include "document.h" +#include "desktop.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/inkscape-preferences.h" // for PREFS_PAGE_SPELLCHECK +#include "ui/tools-switch.h" +#include "ui/tools/text-tool.h" + +#include "text-editing.h" +#include "selection-chemistry.h" +#include "display/curve.h" +#include "document-undo.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-object.h" +#include "object/sp-root.h" +#include "object/sp-string.h" +#include "object/sp-text.h" +#include "object/sp-tref.h" + +#include <glibmm/i18n.h> + +#ifdef _WIN32 +#include <windows.h> +#endif + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/** + * Get the list of installed aspell dictionaries/languages + */ +std::vector<std::string> SpellCheck::get_available_langs() +{ + std::vector<std::string> langs; + +#if HAVE_ASPELL + auto *config = new_aspell_config(); + auto const *dlist = get_aspell_dict_info_list(config); + auto *elements = aspell_dict_info_list_elements(dlist); + + for (AspellDictInfo const *entry; (entry = aspell_dict_info_enumeration_next(elements)) != nullptr;) { + // skip duplicates (I get "de_DE" twice) + if (!langs.empty() && langs.back() == entry->name) { + continue; + } + + langs.emplace_back(entry->name); + } + + delete_aspell_dict_info_enumeration(elements); + delete_aspell_config(config); +#endif + + return langs; +} + +static void show_spellcheck_preferences_dialog() +{ + Inkscape::Preferences::get()->setInt("/dialogs/preferences/page", PREFS_PAGE_SPELLCHECK); + SP_ACTIVE_DESKTOP->_dlg_mgr->showDialog("InkscapePreferences"); +} + +SpellCheck::SpellCheck () : + UI::Widget::Panel("/dialogs/spellcheck/", SP_VERB_DIALOG_SPELLCHECK), + _text(nullptr), + _layout(nullptr), + _stops(0), + _adds(0), + _working(false), + _local_change(false), + _prefs(nullptr), + accept_button(_("_Accept"), true), + ignoreonce_button(_("_Ignore once"), true), + ignore_button(_("_Ignore"), true), + add_button(_("A_dd"), true), + dictionary_label(_("Language")), + dictionary_hbox(false, 0), + stop_button(_("_Stop"), true), + start_button(_("_Start"), true), + desktop(nullptr), + deskTrack() +{ + _prefs = Inkscape::Preferences::get(); + + // take languages from prefs + for (const char *langkey : { "lang", "lang2", "lang3" }) { + auto lang = _prefs->getString(_prefs_path + langkey); + if (!lang.empty()) { + _langs.push_back(lang); + } + } + + banner_hbox.set_layout(Gtk::BUTTONBOX_START); + banner_hbox.add(banner_label); + + if (_langs.empty()) { + _langs = get_available_langs(); + + if (_langs.empty()) { + banner_label.set_markup("<i>No aspell dictionaries installed</i>"); + } + } + + scrolled_window.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrolled_window.set_shadow_type(Gtk::SHADOW_IN); + scrolled_window.set_size_request(120, 96); + scrolled_window.add(tree_view); + + model = Gtk::ListStore::create(tree_columns); + tree_view.set_model(model); + tree_view.append_column(_("Suggestions:"), tree_columns.suggestions); + + if (!_langs.empty()) { + for (auto const &lang : _langs) { + dictionary_combo.append(lang); + } + dictionary_combo.set_active(0); + } + + accept_button.set_tooltip_text(_("Accept the chosen suggestion")); + ignoreonce_button.set_tooltip_text(_("Ignore this word only once")); + ignore_button.set_tooltip_text(_("Ignore this word in this session")); + add_button.set_tooltip_text(_("Add this word to the chosen dictionary")); + pref_button.set_tooltip_text(_("Preferences")); + pref_button.set_image_from_icon_name("preferences-system"); + + dictionary_hbox.pack_start(dictionary_label, false, false, 6); + dictionary_hbox.pack_start(dictionary_combo, true, true, 0); + dictionary_hbox.pack_start(pref_button, false, false, 0); + + changebutton_vbox.set_spacing(4); + changebutton_vbox.pack_start(accept_button, false, false, 0); + changebutton_vbox.pack_start(ignoreonce_button, false, false, 0); + changebutton_vbox.pack_start(ignore_button, false, false, 0); + changebutton_vbox.pack_start(add_button, false, false, 0); + + suggestion_hbox.pack_start (scrolled_window, true, true, 4); + suggestion_hbox.pack_end (changebutton_vbox, false, false, 0); + + stop_button.set_tooltip_text(_("Stop the check")); + start_button.set_tooltip_text(_("Start the check")); + + actionbutton_hbox.set_layout(Gtk::BUTTONBOX_END); + actionbutton_hbox.set_spacing(4); + actionbutton_hbox.add(stop_button); + actionbutton_hbox.add(start_button); + + /* + * Main dialog + */ + Gtk::Box *contents = _getContents(); + contents->set_spacing(6); + contents->pack_start (banner_hbox, false, false, 0); + contents->pack_start (suggestion_hbox, true, true, 0); + contents->pack_start (dictionary_hbox, false, false, 0); + contents->pack_start (action_sep, false, false, 6); + contents->pack_start (actionbutton_hbox, false, false, 0); + + /* + * Signal handlers + */ + accept_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAccept)); + ignoreonce_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnoreOnce)); + ignore_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onIgnore)); + add_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onAdd)); + start_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStart)); + stop_button.signal_clicked().connect(sigc::mem_fun(*this, &SpellCheck::onStop)); + tree_view.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onTreeSelectionChange)); + dictionary_combo.signal_changed().connect(sigc::mem_fun(*this, &SpellCheck::onLanguageChanged)); + pref_button.signal_clicked().connect(sigc::ptr_fun(show_spellcheck_preferences_dialog)); + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &SpellCheck::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children (); + + tree_view.set_sensitive(false); + accept_button.set_sensitive(false); + ignore_button.set_sensitive(false); + ignoreonce_button.set_sensitive(false); + add_button.set_sensitive(false); + stop_button.set_sensitive(false); +} + +SpellCheck::~SpellCheck() +{ + clearRects(); + disconnect(); + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void SpellCheck::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void SpellCheck::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + this->desktop = desktop; + if (_working) { + // Stop and start on the new desktop + finished(); + onStart(); + } + } +} + +void SpellCheck::clearRects() +{ + for(auto t : _rects) { + sp_canvas_item_hide(t); + sp_canvas_item_destroy(t); + } + _rects.clear(); +} + +void SpellCheck::disconnect() +{ + if (_release_connection) { + _release_connection.disconnect(); + } + if (_modified_connection) { + _modified_connection.disconnect(); + } +} + +void SpellCheck::allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked) +{ + if (!desktop) + return; // no desktop to check + + if (SP_IS_DEFS(r)) + return; // we're not interested in items in defs + + if (!strcmp(r->getRepr()->name(), "svg:metadata")) { + return; // we're not interested in metadata + } + + for (auto& child: r->children) { + if (SP_IS_ITEM (&child) && !child.cloned && !desktop->isLayer(SP_ITEM(&child))) { + if ((hidden || !desktop->itemIsHidden(SP_ITEM(&child))) && (locked || !SP_ITEM(&child)->isLocked())) { + if (SP_IS_TEXT(&child) || SP_IS_FLOWTEXT(&child)) + l.push_back(static_cast<SPItem*>(&child)); + } + } + allTextItems (&child, l, hidden, locked); + } + return; +} + +bool +SpellCheck::textIsValid (SPObject *root, SPItem *text) +{ + std::vector<SPItem*> l; + allTextItems (root, l, false, true); + return (std::find(l.begin(), l.end(), text) != l.end()); +} + +bool SpellCheck::compareTextBboxes (gconstpointer a, gconstpointer b)//returns a<b +{ + SPItem *i1 = SP_ITEM(a); + SPItem *i2 = SP_ITEM(b); + + Geom::OptRect bbox1 = i1->documentVisualBounds(); + Geom::OptRect bbox2 = i2->documentVisualBounds(); + if (!bbox1 || !bbox2) { + return false; + } + + // vector between top left corners + Geom::Point diff = bbox1->min() - bbox2->min(); + + return diff[Geom::Y] == 0 ? (diff[Geom::X] < 0) : (diff[Geom::Y] < 0); +} + +// We regenerate and resort the list every time, because user could have changed it while the +// dialog was waiting +SPItem *SpellCheck::getText (SPObject *root) +{ + std::vector<SPItem*> l; + allTextItems (root, l, false, true); + std::sort(l.begin(),l.end(),SpellCheck::compareTextBboxes); + + for (auto item:l) { + if(_seen_objects.insert(item).second) + return item; + } + return nullptr; +} + +void +SpellCheck::nextText() +{ + disconnect(); + + _text = getText(_root); + if (_text) { + + _modified_connection = (SP_OBJECT(_text))->connectModified(sigc::mem_fun(*this, &SpellCheck::onObjModified)); + _release_connection = (SP_OBJECT(_text))->connectRelease(sigc::mem_fun(*this, &SpellCheck::onObjReleased)); + + _layout = te_get_layout (_text); + _begin_w = _layout->begin(); + } + _end_w = _begin_w; + _word.clear(); +} + +void SpellCheck::deleteSpeller() { +#if HAVE_ASPELL + if (_speller) { + aspell_speller_save_all_word_lists(_speller); + delete_aspell_speller(_speller); + _speller = nullptr; + } +#endif +} + +bool SpellCheck::updateSpeller() { +#if HAVE_ASPELL + deleteSpeller(); + + auto lang = dictionary_combo.get_active_text(); + if (!lang.empty()) { + AspellConfig *config = new_aspell_config(); + aspell_config_replace(config, "lang", lang.c_str()); + aspell_config_replace(config, "encoding", "UTF-8"); + AspellCanHaveError *ret = new_aspell_speller(config); + delete_aspell_config(config); + if (aspell_error(ret) != nullptr) { + banner_label.set_text(aspell_error_message(ret)); + delete_aspell_can_have_error(ret); + } else { + _speller = to_aspell_speller(ret); + } + } + + return _speller != nullptr; +#else + return false; +#endif +} + +bool +SpellCheck::init(SPDesktop *d) +{ + desktop = d; + + start_button.set_sensitive(false); + + _stops = 0; + _adds = 0; + clearRects(); + + if (!updateSpeller()) + return false; + + _root = desktop->getDocument()->getRoot(); + + // empty the list of objects we've checked + _seen_objects.clear(); + + // grab first text + nextText(); + + _working = true; + + return true; +} + +void +SpellCheck::finished () +{ + deleteSpeller(); + + clearRects(); + disconnect(); + + //desktop->clearWaitingCursor(); + + tree_view.unset_model(); + tree_view.set_sensitive(false); + accept_button.set_sensitive(false); + ignore_button.set_sensitive(false); + ignoreonce_button.set_sensitive(false); + add_button.set_sensitive(false); + stop_button.set_sensitive(false); + start_button.set_sensitive(true); + + { + gchar *label; + if (_stops) + label = g_strdup_printf(_("<b>Finished</b>, <b>%d</b> words added to dictionary"), _adds); + else + label = g_strdup_printf("%s", _("<b>Finished</b>, nothing suspicious found")); + banner_label.set_markup(label); + g_free(label); + } + + _seen_objects.clear(); + + desktop = nullptr; + _root = nullptr; + + _working = false; +} + +bool +SpellCheck::nextWord() +{ + if (!_working) + return false; + + if (!_text) { + finished(); + return false; + } + _word.clear(); + + while (_word.size() == 0) { + _begin_w = _end_w; + + if (!_layout || _begin_w == _layout->end()) { + nextText(); + return false; + } + + if (!_layout->isStartOfWord(_begin_w)) { + _begin_w.nextStartOfWord(); + } + + _end_w = _begin_w; + _end_w.nextEndOfWord(); + _word = sp_te_get_string_multiline (_text, _begin_w, _end_w); + } + + // try to link this word with the next if separated by ' + SPObject *char_item = nullptr; + Glib::ustring::iterator text_iter; + _layout->getSourceOfCharacter(_end_w, &char_item, &text_iter); + if (SP_IS_STRING(char_item)) { + int this_char = *text_iter; + if (this_char == '\'' || this_char == 0x2019) { + Inkscape::Text::Layout::iterator end_t = _end_w; + end_t.nextCharacter(); + _layout->getSourceOfCharacter(end_t, &char_item, &text_iter); + if (SP_IS_STRING(char_item)) { + int this_char = *text_iter; + if (g_ascii_isalpha(this_char)) { // 's + _end_w.nextEndOfWord(); + _word = sp_te_get_string_multiline (_text, _begin_w, _end_w); + } + } + } + } + + // skip words containing digits + if (_prefs->getInt(_prefs_path + "ignorenumbers") != 0) { + bool digits = false; + for (unsigned int i : _word) { + if (g_unichar_isdigit(i)) { + digits = true; + break; + } + } + if (digits) { + return false; + } + } + + // skip ALL-CAPS words + if (_prefs->getInt(_prefs_path + "ignoreallcaps") != 0) { + bool allcaps = true; + for (unsigned int i : _word) { + if (!g_unichar_isupper(i)) { + allcaps = false; + break; + } + } + if (allcaps) { + return false; + } + } + + int have = 0; + +#if HAVE_ASPELL + // run it by all active spellers + if (_speller) { + have += aspell_speller_check(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + if (have == 0) { // not found in any! + _stops ++; + + //desktop->clearWaitingCursor(); + + // display it in window + { + gchar *label = g_strdup_printf(_("Not in dictionary: <b>%s</b>"), _word.c_str()); + banner_label.set_markup(label); + g_free(label); + } + + tree_view.set_sensitive(true); + ignore_button.set_sensitive(true); + ignoreonce_button.set_sensitive(true); + add_button.set_sensitive(true); + stop_button.set_sensitive(true); + + // draw rect + std::vector<Geom::Point> points = + _layout->createSelectionShape(_begin_w, _end_w, _text->i2dt_affine()); + if (points.size() >= 4) { // we may not have a single quad if this is a clipped part of text on path; in that case skip drawing the rect + Geom::Point tl, br; + tl = br = points.front(); + for (auto & point : points) { + if (point[Geom::X] < tl[Geom::X]) + tl[Geom::X] = point[Geom::X]; + if (point[Geom::Y] < tl[Geom::Y]) + tl[Geom::Y] = point[Geom::Y]; + if (point[Geom::X] > br[Geom::X]) + br[Geom::X] = point[Geom::X]; + if (point[Geom::Y] > br[Geom::Y]) + br[Geom::Y] = point[Geom::Y]; + } + + // expand slightly + Geom::Rect area = Geom::Rect(tl, br); + double mindim = fabs(tl[Geom::Y] - br[Geom::Y]); + if (fabs(tl[Geom::X] - br[Geom::X]) < mindim) + mindim = fabs(tl[Geom::X] - br[Geom::X]); + area.expandBy(MAX(0.05 * mindim, 1)); + + // create canvas path rectangle, red stroke + SPCanvasItem *rect = sp_canvas_bpath_new(desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(rect), 0xff0000ff, 3.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(rect), 0, SP_WIND_RULE_NONZERO); + SPCurve *curve = new SPCurve(); + curve->moveto(area.corner(0)); + curve->lineto(area.corner(1)); + curve->lineto(area.corner(2)); + curve->lineto(area.corner(3)); + curve->lineto(area.corner(0)); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(rect), curve); + sp_canvas_item_show(rect); + _rects.push_back(rect); + + // scroll to make it all visible + Geom::Point const center = desktop->get_display_area().midpoint(); + area.expandBy(0.5 * mindim); + Geom::Point scrollto; + double dist = 0; + for (unsigned corner = 0; corner < 4; corner ++) { + if (Geom::L2(area.corner(corner) - center) > dist) { + dist = Geom::L2(area.corner(corner) - center); + scrollto = area.corner(corner); + } + } + desktop->scroll_to_point (scrollto, 1.0); + } + + // select text; if in Text tool, position cursor to the beginning of word + // unless it is already in the word + if (desktop->selection->singleItem() != _text) + desktop->selection->set (_text); + if (tools_isactive(desktop, TOOLS_TEXT)) { + Inkscape::Text::Layout::iterator *cursor = + sp_text_context_get_cursor_position(SP_TEXT_CONTEXT(desktop->event_context), _text); + if (!cursor) // some other text is selected there + desktop->selection->set (_text); + else if (*cursor <= _begin_w || *cursor >= _end_w) + sp_text_context_place_cursor (SP_TEXT_CONTEXT(desktop->event_context), _text, _begin_w); + } + +#if HAVE_ASPELL + + // get suggestions + model = Gtk::ListStore::create(tree_columns); + tree_view.set_model(model); + unsigned n_sugg = 0; + + if (_speller) { + const AspellWordList *wl = aspell_speller_suggest(_speller, _word.c_str(), -1); + AspellStringEnumeration * els = aspell_word_list_elements(wl); + const char *sugg; + Gtk::TreeModel::iterator iter; + + while ((sugg = aspell_string_enumeration_next(els)) != nullptr) { + iter = model->append(); + Gtk::TreeModel::Row row = *iter; + row[tree_columns.suggestions] = sugg; + + // select first suggestion + if (++n_sugg == 1) { + tree_view.get_selection()->select(iter); + } + } + delete_aspell_string_enumeration(els); + } + + accept_button.set_sensitive(n_sugg > 0); + +#endif /* HAVE_ASPELL */ + + return true; + + } + return false; +} + + + +void +SpellCheck::deleteLastRect () +{ + if (!_rects.empty()) { + sp_canvas_item_hide(_rects.back()); + sp_canvas_item_destroy(_rects.back()); + _rects.pop_back(); // pop latest-prepended rect + } +} + +void SpellCheck::doSpellcheck () +{ + if (_langs.empty()) { + return; + } + + banner_label.set_markup(_("<i>Checking...</i>")); + + //desktop->setWaitingCursor(); + + while (_working) + if (nextWord()) + break; +} + +void SpellCheck::onTreeSelectionChange() +{ + accept_button.set_sensitive(true); +} + +void SpellCheck::onObjModified (SPObject* /* blah */, unsigned int /* bleh */) +{ + if (_local_change) { // this was a change by this dialog, i.e. an Accept, skip it + _local_change = false; + return; + } + + if (_working && _root) { + // user may have edited the text we're checking; try to do the most sensible thing in this + // situation + + // just in case, re-get text's layout + _layout = te_get_layout (_text); + + // re-get the word + _layout->validateIterator(&_begin_w); + _end_w = _begin_w; + _end_w.nextEndOfWord(); + Glib::ustring word_new = sp_te_get_string_multiline (_text, _begin_w, _end_w); + if (word_new != _word) { + _end_w = _begin_w; + deleteLastRect (); + doSpellcheck (); // recheck this word and go ahead if it's ok + } + } +} + +void SpellCheck::onObjReleased (SPObject* /* blah */) +{ + if (_working && _root) { + // the text object was deleted + deleteLastRect (); + nextText(); + doSpellcheck (); // get next text and continue + } +} + +void SpellCheck::onAccept () +{ + // insert chosen suggestion + + Glib::RefPtr<Gtk::TreeSelection> selection = tree_view.get_selection(); + Gtk::TreeModel::iterator iter = selection->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + Glib::ustring sugg = row[tree_columns.suggestions]; + + if (sugg.length() > 0) { + //g_print("chosen: %s\n", sugg); + _local_change = true; + sp_te_replace(_text, _begin_w, _end_w, sugg.c_str()); + // find the end of the word anew + _end_w = _begin_w; + _end_w.nextEndOfWord(); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Fix spelling")); + } + } + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnore () +{ +#if HAVE_ASPELL + if (_speller) { + aspell_speller_add_to_session(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onIgnoreOnce () +{ + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onAdd () +{ + _adds++; + +#if HAVE_ASPELL + if (_speller) { + aspell_speller_add_to_personal(_speller, _word.c_str(), -1); + } +#endif /* HAVE_ASPELL */ + + deleteLastRect(); + doSpellcheck(); +} + +void +SpellCheck::onStop () +{ + finished(); +} + +void +SpellCheck::onStart () +{ + if (init (SP_ACTIVE_DESKTOP)) + doSpellcheck(); +} + +void SpellCheck::onLanguageChanged() +{ + if (!_working) { + onStart(); + return; + } + + if (!updateSpeller()) { + return; + } + + // recheck current word + _end_w = _begin_w; + deleteLastRect(); + doSpellcheck(); +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/spellcheck.h b/src/ui/dialog/spellcheck.h new file mode 100644 index 0000000..39d1285 --- /dev/null +++ b/src/ui/dialog/spellcheck.h @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Spellcheck dialog + */ +/* Authors: + * bulia byak <bulia@users.sf.net> + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SPELLCHECK_H +#define SEEN_SPELLCHECK_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <vector> +#include <set> + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/buttonbox.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/separator.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treeview.h> + +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +#include "text-editing.h" + +#if HAVE_ASPELL +#include <aspell.h> +#endif /* HAVE_ASPELL */ + +class SPDesktop; +class SPObject; +class SPItem; +class SPCanvasItem; + +namespace Inkscape { +class Preferences; + +namespace UI { +namespace Dialog { + +/** + * + * A dialog widget to checking spelling of text elements in the document + * Uses ASpell and one of the languages set in the users preference file + * + */ +class SpellCheck : public Widget::Panel { +public: + SpellCheck (); + ~SpellCheck () override; + + static SpellCheck &getInstance() { return *new SpellCheck(); } + + static std::vector<std::string> get_available_langs(); + +private: + + /** + * Remove the highlight rectangle form the canvas + */ + void clearRects(); + + /** + * Release handlers to the selected item + */ + void disconnect(); + + /** + * Returns a list of all the text items in the SPObject + */ + void allTextItems (SPObject *r, std::vector<SPItem *> &l, bool hidden, bool locked); + + /** + * Is text inside the SPOject's tree + */ + bool textIsValid (SPObject *root, SPItem *text); + + /** + * Compare the visual bounds of 2 SPItems referred to by a and b + */ + static bool compareTextBboxes (gconstpointer a, gconstpointer b); + SPItem *getText (SPObject *root); + void nextText (); + + /** + * Initialize the controls and aspell + */ + bool init (SPDesktop *desktop); + + /** + * Cleanup after spellcheck is finished + */ + void finished (); + + /** + * Find the next word to spell check + */ + bool nextWord(); + void deleteLastRect (); + void doSpellcheck (); + + /** + * Update speller from language combobox + * @return true if update was successful + */ + bool updateSpeller(); + void deleteSpeller(); + + /** + * Accept button clicked + */ + void onAccept (); + + /** + * Ignore button clicked + */ + void onIgnore (); + + /** + * Ignore once button clicked + */ + void onIgnoreOnce (); + + /** + * Add button clicked + */ + void onAdd (); + + /** + * Stop button clicked + */ + void onStop (); + + /** + * Start button clicked + */ + void onStart (); + + /** + * Language selection changed + */ + void onLanguageChanged(); + + /** + * Selected object modified on canvas + */ + void onObjModified (SPObject* /* blah */, unsigned int /* bleh */); + + /** + * Selected object removed from canvas + */ + void onObjReleased (SPObject* /* blah */); + + /** + * Selection in suggestions text view changed + */ + void onTreeSelectionChange(); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void setTargetDesktop(SPDesktop *desktop); + + SPObject *_root; + +#if HAVE_ASPELL + AspellSpeller *_speller = nullptr; +#endif /* HAVE_ASPELL */ + + /** + * list of canvasitems (currently just rects) that mark misspelled things on canvas + */ + std::vector<SPCanvasItem *> _rects; + + /** + * list of text objects we have already checked in this session + */ + std::set<SPItem *> _seen_objects; + + /** + * the object currently being checked + */ + SPItem *_text; + + /** + * current objects layout + */ + Inkscape::Text::Layout const *_layout; + + /** + * iterators for the start and end of the current word + */ + Inkscape::Text::Layout::iterator _begin_w; + Inkscape::Text::Layout::iterator _end_w; + + /** + * the word we're checking + */ + Glib::ustring _word; + + /** + * counters for the number of stops and dictionary adds + */ + int _stops; + int _adds; + + /** + * true if we are in the middle of a check + */ + bool _working; + + /** + * connect to the object being checked in case it is modified or deleted by user + */ + sigc::connection _modified_connection; + sigc::connection _release_connection; + + /** + * true if the spell checker dialog has changed text, to suppress modified callback + */ + bool _local_change; + + Inkscape::Preferences *_prefs; + + std::vector<std::string> _langs; + + /* + * Dialogs widgets + */ + Gtk::Label banner_label; + Gtk::ButtonBox banner_hbox; + Gtk::ScrolledWindow scrolled_window; + Gtk::TreeView tree_view; + Glib::RefPtr<Gtk::ListStore> model; + + Gtk::HBox suggestion_hbox; + Gtk::VBox changebutton_vbox; + Gtk::Button accept_button; + Gtk::Button ignoreonce_button; + Gtk::Button ignore_button; + + Gtk::Button add_button; + Gtk::Button pref_button; + Gtk::Label dictionary_label; + Gtk::ComboBoxText dictionary_combo; + Gtk::HBox dictionary_hbox; + Gtk::Separator action_sep; + Gtk::Button stop_button; + Gtk::Button start_button; + Gtk::ButtonBox actionbutton_hbox; + + SPDesktop * desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + + class TreeColumns : public Gtk::TreeModel::ColumnRecord + { + public: + TreeColumns() + { + add(suggestions); + } + ~TreeColumns() override = default; + Gtk::TreeModelColumn<Glib::ustring> suggestions; + }; + TreeColumns tree_columns; + +}; + +} +} +} + +#endif /* !SEEN_SPELLCHECK_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/dialog/styledialog.cpp b/src/ui/dialog/styledialog.cpp new file mode 100644 index 0000000..872b3bf --- /dev/null +++ b/src/ui/dialog/styledialog.cpp @@ -0,0 +1,1690 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS styles + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * Jabiertxof + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "styledialog.h" +#include "attribute-rel-svg.h" +#include "attributes.h" +#include "document-undo.h" +#include "inkscape.h" +#include "io/resource.h" +#include "selection.h" +#include "style-internal.h" +#include "style.h" +#include "svg/svg-color.h" +#include "ui/icon-loader.h" +#include "ui/widget/iconrenderer.h" +#include "verbs.h" +#include "xml/attribute-record.h" +#include "xml/node-observer.h" +#include "xml/sp-css-attr.h" + +#include <map> +#include <regex> +#include <utility> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +// G_MESSAGES_DEBUG=DEBUG_STYLEDIALOG gdb ./inkscape +// #define DEBUG_STYLEDIALOG +// #define G_LOG_DOMAIN "STYLEDIALOG" + +using Inkscape::DocumentUndo; +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; + +/** + * This macro is used to remove spaces around selectors or any strings when + * parsing is done to update XML style element or row labels in this dialog. + */ +#define REMOVE_SPACES(x) \ + x.erase(0, x.find_first_not_of(' ')); \ + x.erase(x.find_last_not_of(' ') + 1); + +namespace Inkscape { + +/** + * Get the first <style> element's first text node. If no such node exists and + * `create_if_missing` is false, then return NULL. + * + * Only finds <style> elements in root or in root-level <defs>. + */ +XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing) +{ + static GQuark const CODE_svg_style = g_quark_from_static_string("svg:style"); + static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs"); + + XML::Node *styleNode = nullptr; + XML::Node *textNode = nullptr; + + for (auto *node = root->firstChild(); node; node = node->next()) { + if (node->code() == CODE_svg_defs) { + textNode = get_first_style_text_node(node, false); + if (textNode != nullptr) { + return textNode; + } + } + + if (node->code() == CODE_svg_style) { + styleNode = node; + break; + } + } + + if (styleNode == nullptr) { + if (!create_if_missing) + return nullptr; + + styleNode = root->document()->createElement("svg:style"); + root->addChild(styleNode, nullptr); + Inkscape::GC::release(styleNode); + } + + for (auto *node = styleNode->firstChild(); node; node = node->next()) { + if (node->type() == XML::TEXT_NODE) { + textNode = node; + break; + } + } + + if (textNode == nullptr) { + if (!create_if_missing) + return nullptr; + + textNode = root->document()->createTextNode(""); + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + + return textNode; +} + +namespace UI { +namespace Dialog { + +// Keeps a watch on style element +class StyleDialog::NodeObserver : public Inkscape::XML::NodeObserver { + public: + NodeObserver(StyleDialog *styledialog) + : _styledialog(styledialog) + { + g_debug("StyleDialog::NodeObserver: Constructor"); + }; + + void notifyContentChanged(Inkscape::XML::Node &node, Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override; + + StyleDialog *_styledialog; +}; + + +void StyleDialog::NodeObserver::notifyContentChanged(Inkscape::XML::Node & /*node*/, + Inkscape::Util::ptr_shared /*old_content*/, + Inkscape::Util::ptr_shared /*new_content*/) +{ + + g_debug("StyleDialog::NodeObserver::notifyContentChanged"); + _styledialog->_updating = false; + _styledialog->readStyleElement(); +} + + +// Keeps a watch for new/removed/changed nodes +// (Must update objects that selectors match.) +class StyleDialog::NodeWatcher : public Inkscape::XML::NodeObserver { + public: + NodeWatcher(StyleDialog *styledialog) + : _styledialog(styledialog) + { + g_debug("StyleDialog::NodeWatcher: Constructor"); + }; + + void notifyChildAdded(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child, + Inkscape::XML::Node * /*prev*/) override + { + _styledialog->_nodeAdded(child); + } + + void notifyChildRemoved(Inkscape::XML::Node & /*node*/, Inkscape::XML::Node &child, + Inkscape::XML::Node * /*prev*/) override + { + _styledialog->_nodeRemoved(child); + } + /* void notifyContentChanged(Inkscape::XML::Node &node, + Inkscape::Util::ptr_shared old_content, + Inkscape::Util::ptr_shared new_content) override{ + if ( _styledialog && _repr && _textNode == node) { + _styledialog->_stylesheetChanged( node ); + } + }; + */ + void notifyAttributeChanged(Inkscape::XML::Node &node, GQuark qname, Util::ptr_shared /*old_value*/, + Util::ptr_shared /*new_value*/) override + { + static GQuark const CODE_id = g_quark_from_static_string("id"); + static GQuark const CODE_class = g_quark_from_static_string("class"); + static GQuark const CODE_style = g_quark_from_static_string("style"); + + if (qname == CODE_id || qname == CODE_class || qname == CODE_style) { + _styledialog->_nodeChanged(node); + } + } + + StyleDialog *_styledialog; +}; + +void StyleDialog::_nodeAdded(Inkscape::XML::Node &node) +{ + readStyleElement(); +} + +void StyleDialog::_nodeRemoved(Inkscape::XML::Node &repr) +{ + if (_textNode == &repr) { + _textNode = nullptr; + } + + readStyleElement(); +} + +void StyleDialog::_nodeChanged(Inkscape::XML::Node &object) +{ + g_debug("StyleDialog::_nodeChanged"); + readStyleElement(); +} + +/* void +StyleDialog::_stylesheetChanged( Inkscape::XML::Node &repr ) { + std::cout << "Style tag modified" << std::endl; + readStyleElement(); +} */ + +/** + * Constructor + * A treeview and a set of two buttons are added to the dialog. _addSelector + * adds selectors to treeview. _delSelector deletes the selector from the dialog. + * Any addition/deletion of the selectors updates XML style element accordingly. + */ +StyleDialog::StyleDialog() + : UI::Widget::Panel("/dialogs/style", SP_VERB_DIALOG_STYLE) + , _updating(false) + , _textNode(nullptr) + , _scroolpos(0) + , _desktopTracker() + , _deleted_pos(0) + , _deletion(false) +{ + g_debug("StyleDialog::StyleDialog"); + + m_nodewatcher.reset(new StyleDialog::NodeWatcher(this)); + m_styletextwatcher.reset(new StyleDialog::NodeObserver(this)); + + // Pack widgets + _mainBox.pack_start(_scrolledWindow, Gtk::PACK_EXPAND_WIDGET); + _scrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + _styleBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + _styleBox.set_valign(Gtk::ALIGN_START); + _scrolledWindow.add(_styleBox); + Gtk::Label *infotoggler = Gtk::manage(new Gtk::Label(_("Edit Full Stylesheet"))); + infotoggler->get_style_context()->add_class("inksmall"); + _vadj = _scrolledWindow.get_vadjustment(); + _vadj->signal_value_changed().connect(sigc::mem_fun(*this, &StyleDialog::_vscrool)); + _mainBox.set_orientation(Gtk::ORIENTATION_VERTICAL); + + _getContents()->pack_start(_mainBox, Gtk::PACK_EXPAND_WIDGET); + // Document & Desktop + _desktop_changed_connection = + _desktopTracker.connectDesktopChanged(sigc::mem_fun(*this, &StyleDialog::_handleDesktopChanged)); + _desktopTracker.connect(GTK_WIDGET(gobj())); + + _document_replaced_connection = + getDesktop()->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); + + _selection_changed_connection = getDesktop()->getSelection()->connectChanged( + sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + + // Add watchers + _updateWatchers(getDesktop()); + + // Load tree + readStyleElement(); +} + +void StyleDialog::_vscrool() +{ + if (!_scroollock) { + _scroolpos = _vadj->get_value(); + } else { + _vadj->set_value(_scroolpos); + _scroollock = false; + } +} + +Glib::ustring StyleDialog::fixCSSSelectors(Glib::ustring selector) +{ + g_debug("SelectorsDialog::fixCSSSelectors"); + REMOVE_SPACES(selector); + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[,]+", selector); + Glib::ustring my_selector = selector + " {"; // Parsing fails sometimes without '{'. Fix me + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.c_str(), CR_UTF_8); + for (auto token : tokens) { + REMOVE_SPACES(token); + std::vector<Glib::ustring> subtokens = Glib::Regex::split_simple("[ ]+", token); + for (auto subtoken : subtokens) { + REMOVE_SPACES(subtoken); + Glib::ustring my_selector = subtoken + " {"; // Parsing fails sometimes without '{'. Fix me + CRSelector *cr_selector = cr_selector_parse_from_buf((guchar *)my_selector.c_str(), CR_UTF_8); + gchar *selectorchar = reinterpret_cast<gchar *>(cr_selector_to_string(cr_selector)); + if (selectorchar) { + Glib::ustring toadd = Glib::ustring(selectorchar); + g_free(selectorchar); + if (toadd[0] != '.' && toadd[0] != '#' && toadd.size() > 1) { + auto i = std::min(toadd.find("#"), toadd.find(".")); + Glib::ustring tag = toadd; + if (i != std::string::npos) { + tag = tag.substr(0, i); + } + if (!SPAttributeRelSVG::isSVGElement(tag)) { + if (tokens.size() == 1) { + tag = "." + tag; + return tag; + } else { + return ""; + } + } + } + } + } + } + if (cr_selector) { + return selector; + } + return ""; +} + +/** + * Class destructor + */ +StyleDialog::~StyleDialog() +{ + g_debug("StyleDialog::~StyleDialog"); + _desktop_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + _selection_changed_connection.disconnect(); +} + +void StyleDialog::_reload() { readStyleElement(); } + +/** + * @return Inkscape::XML::Node* pointing to a style element's text node. + * Returns the style element's text node. If there is no style element, one is created. + * Ditto for text node. + */ +Inkscape::XML::Node *StyleDialog::_getStyleTextNode(bool create_if_missing) +{ + g_debug("StyleDialog::_getStyleTextNoded"); + + auto textNode = Inkscape::get_first_style_text_node(m_root, create_if_missing); + + if (_textNode != textNode) { + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + } + + _textNode = textNode; + + if (_textNode) { + _textNode->addObserver(*m_styletextwatcher); + } + } + + return textNode; +} + + +Glib::RefPtr<Gtk::TreeModel> StyleDialog::_selectTree(Glib::ustring selector) +{ + g_debug("StyleDialog::_selectTree"); + + Gtk::Label *selectorlabel; + Glib::RefPtr<Gtk::TreeModel> model; + for (auto fullstyle : _styleBox.get_children()) { + Gtk::Box *style = dynamic_cast<Gtk::Box *>(fullstyle); + for (auto stylepart : style->get_children()) { + switch (style->child_property_position(*stylepart)) { + case 0: { + Gtk::Box *selectorbox = dynamic_cast<Gtk::Box *>(stylepart); + for (auto styleheader : selectorbox->get_children()) { + if (!selectorbox->child_property_position(*styleheader)) { + selectorlabel = dynamic_cast<Gtk::Label *>(styleheader); + } + } + break; + } + case 1: { + Glib::ustring wdg_selector = selectorlabel->get_text(); + if (wdg_selector == selector) { + Gtk::TreeView *treeview = dynamic_cast<Gtk::TreeView *>(stylepart); + if (treeview) { + return treeview->get_model(); + } + } + break; + } + default: + break; + } + } + } + return model; +} + +void StyleDialog::setCurrentSelector(Glib::ustring current_selector) +{ + g_debug("StyleDialog::setCurrentSelector"); + _current_selector = current_selector; + readStyleElement(); +} + +// copied from style.cpp:1499 +static bool is_url(char const *p) +{ + if (p == nullptr) + return false; + /** \todo + * FIXME: I'm not sure if this applies to SVG as well, but CSS2 says any URIs + * in property values must start with 'url('. + */ + return (g_ascii_strncasecmp(p, "url(", 4) == 0); +} + +/** + * Fill the Gtk::TreeStore from the svg:style element. + */ +void StyleDialog::readStyleElement() +{ + g_debug("StyleDialog::readStyleElement"); + + if (_updating) + return; // Don't read if we wrote style element. + _updating = true; + _scroollock = true; + Inkscape::XML::Node *textNode = _getStyleTextNode(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + + // Get content from style text node. + std::string content = (textNode && textNode->content()) ? textNode->content() : ""; + + // Remove end-of-lines (check it works on Windoze). + content.erase(std::remove(content.begin(), content.end(), '\n'), content.end()); + + // Remove comments (/* xxx */) + + bool breakme = false; + size_t start = content.find("/*"); + size_t open = content.find("{", start + 1); + size_t close = content.find("}", start + 1); + size_t end = content.find("*/", close + 1); + while (!breakme) { + if (open == std::string::npos || close == std::string::npos || end == std::string::npos) { + breakme = true; + break; + } + while (open < close) { + open = content.find("{", close + 1); + close = content.find("}", close + 1); + end = content.find("*/", close + 1); + size_t reopen = content.find("{", close + 1); + if (open == std::string::npos || end == std::string::npos || end < reopen) { + if (end < reopen) { + content = content.erase(start, end - start + 2); + } else { + breakme = true; + } + break; + } + } + start = content.find("/*", start + 1); + open = content.find("{", start + 1); + close = content.find("}", start + 1); + end = content.find("*/", close + 1); + } + + // First split into selector/value chunks. + // An attempt to use Glib::Regex failed. A C++11 version worked but + // reportedly has problems on Windows. Using split_simple() is simpler + // and probably faster. + // + // Glib::RefPtr<Glib::Regex> regex1 = + // Glib::Regex::create("([^\\{]+)\\{([^\\{]+)\\}"); + // + // Glib::MatchInfo minfo; + // regex1->match(content, minfo); + + // Split on curly brackets. Even tokens are selectors, odd are values. + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("[}{]", content); + _owner_style.clear(); + // If text node is empty, return (avoids problem with negative below). + + for (auto child : _styleBox.get_children()) { + _styleBox.remove(*child); + delete child; + } + Inkscape::Selection *selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = getDesktop()->getDocument()->getXMLDialogSelectedObject(); + if (obj && !obj->getRepr()) { + obj = nullptr; // treat detached object as no selection + } + } + + Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-css.glade"); + Glib::RefPtr<Gtk::Builder> _builder; + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + gint selectorpos = 0; + Gtk::Box *css_selector_container; + _builder->get_widget("CSSSelectorContainer", css_selector_container); + Gtk::Label *css_selector; + _builder->get_widget("CSSSelector", css_selector); + Gtk::EventBox *css_selector_event_add; + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + css_selector->set_text("element"); + Gtk::TreeView *css_tree; + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_element"); + Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns); + css_tree->set_model(store); + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "style_properties", selectorpos)); + Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_placeholder_text() = _("value"); + value->property_editable() = true; + value->signal_edited().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->set_expand(true); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + Inkscape::UI::Widget::IconRenderer *urlRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + urlRenderer->add_icon("empty-icon"); + urlRenderer->add_icon("edit-redo"); + int urlCol = css_tree->append_column(" ", *urlRenderer) - 1; + Gtk::TreeViewColumn *urlcol = css_tree->get_column(urlCol); + if (urlcol) { + urlcol->set_min_width(40); + urlcol->set_max_width(40); + urlRenderer->signal_activated().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onLinkObj), store)); + urlcol->add_attribute(urlRenderer->property_icon(), _mColumns._colLinked); + } + std::map<Glib::ustring, Glib::ustring> attr_prop; + Gtk::TreeModel::Path path; + bool empty = true; + if (obj && obj->getRepr()->attribute("style")) { + Glib::ustring style = obj->getRepr()->attribute("style"); + attr_prop = parseStyle(style); + for (auto iter : obj->style->properties()) { + if (attr_prop.count(iter->name())) { + empty = false; + Gtk::TreeModel::Row row = *(store->prepend()); + row[_mColumns._colSelector] = "style_properties"; + row[_mColumns._colSelectorPos] = 0; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = iter->name(); + row[_mColumns._colValue] = iter->get_value(); + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + row[_mColumns._colHref] = nullptr; + row[_mColumns._colLinked] = false; + if (is_url(iter->get_value().c_str())) { + Glib::ustring id = iter->get_value(); + id = id.substr(5, id.size() - 6); + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById(id.c_str()))) { + row[_mColumns._colHref] = elemref; + row[_mColumns._colLinked] = true; + } + } + _addOwnerStyle(iter->name(), "style attribute"); + } + } + // this is to fix a bug on cairo win: + // https://gitlab.freedesktop.org/cairo/cairo/issues/338 + // TODO: check if inkscape min cairo version has applied the patch proposed and remove (3 times) + if (empty) { + css_tree->hide(); + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + } + selectorpos++; + if (tokens.size() == 0) { + _updating = false; + return; + } + for (unsigned i = 0; i < tokens.size() - 1; i += 2) { + Glib::ustring selector = tokens[i]; + REMOVE_SPACES(selector); // Remove leading/trailing spaces + // Get list of objects selector matches + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + Glib::ustring selector_orig = selector; + if (!selectordata.empty()) { + selector = selectordata.back(); + } + std::vector<SPObject *> objVec = _getObjVec(selector); + if (obj) { + bool stop = true; + for (auto objel : objVec) { + if (objel == obj) { + stop = false; + } + } + if (stop) { + _updating = false; + selectorpos++; + continue; + } + } + if (!obj && _current_selector != "" && _current_selector != selector) { + _updating = false; + selectorpos++; + continue; + } + if (!obj) { + bool present = false; + for (auto objv : objVec) { + for (auto objsel : selection->objects()) { + if (objv == objsel) { + present = true; + break; + } + } + if (present) { + break; + } + } + if (!present) { + _updating = false; + selectorpos++; + continue; + } + } + Glib::ustring properties; + // Check to make sure we do have a value to match selector. + if ((i + 1) < tokens.size()) { + properties = tokens[i + 1]; + } else { + std::cerr << "StyleDialog::readStyleElement: Missing values " + "for last selector!" + << std::endl; + } + Glib::RefPtr<Gtk::Builder> _builder; + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + Gtk::Box *css_selector_container; + _builder->get_widget("CSSSelectorContainer", css_selector_container); + Gtk::Label *css_selector; + _builder->get_widget("CSSSelector", css_selector); + Gtk::EventBox *css_selector_event_box; + _builder->get_widget("CSSSelectorEventBox", css_selector_event_box); + Gtk::Entry *css_edit_selector; + _builder->get_widget("CSSEditSelector", css_edit_selector); + Gtk::EventBox *css_selector_event_add; + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + css_selector->set_text(selector); + Gtk::TreeView *css_tree; + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_sheet"); + Glib::RefPtr<Gtk::TreeStore> store = Gtk::TreeStore::create(_mColumns); + css_tree->set_model(store); + // I comment this feature, is working but seems obscure to undertand + // the user can edit selector name in current implementation + /* css_selector_event_box->signal_button_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorStartEdit), css_selector, css_edit_selector)); + css_edit_selector->signal_key_press_event().connect(sigc::bind( + sigc::mem_fun(*this, &StyleDialog::_selectorEditKeyPress), store, css_selector, css_edit_selector)); + css_edit_selector->signal_activate().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_selectorActivate), store, css_selector, css_edit_selector)); + */ + Inkscape::UI::Widget::IconRenderer *addRenderer = manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererToggle *isactive = Gtk::manage(new Gtk::CellRendererToggle()); + isactive->property_activatable() = true; + addCol = css_tree->append_column(" ", *isactive) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(isactive->property_active(), _mColumns._colActive); + isactive->signal_toggled().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_activeToggled), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_editable() = true; + value->property_placeholder_text() = _("value"); + value->signal_edited().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>>(sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + Glib::ustring style = properties; + Glib::ustring comments = ""; + while (style.find("/*") != std::string::npos) { + size_t beg = style.find("/*"); + size_t end = style.find("*/"); + if (end != std::string::npos && beg != std::string::npos) { + comments = comments.append(style, beg + 2, end - beg - 2); + style = style.erase(beg, end - beg + 2); + } + } + std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet = parseStyle(style); + std::map<Glib::ustring, Glib::ustring> attr_prop_styleshet_comments = parseStyle(comments); + std::map<Glib::ustring, std::pair<Glib::ustring, bool>> result_props; + for (auto styled : attr_prop_styleshet) { + result_props[styled.first] = std::make_pair(styled.second, true); + } + for (auto styled : attr_prop_styleshet_comments) { + result_props[styled.first] = std::make_pair(styled.second, false); + } + empty = true; + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, selector_orig, selectorpos)); + if (obj) { + for (auto iter : result_props) { + empty = false; + Gtk::TreeIter iterstore = store->append(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore; + Gtk::TreeModel::Row row = *(iterstore); + row[_mColumns._colSelector] = selector_orig; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = iter.second.second; + row[_mColumns._colName] = iter.first; + row[_mColumns._colValue] = iter.second.first; + const Glib::ustring value = row[_mColumns._colValue]; + if (iter.second.second) { + Glib::ustring val = ""; + for (auto iterprop : obj->style->properties()) { + if (iterprop->style_src != SP_STYLE_SRC_UNSET && iterprop->name() == iter.first) { + val = iterprop->get_value(); + break; + } + } + guint32 r1 = 0; // if there's no color, return black + r1 = sp_svg_read_color(value.c_str(), r1); + guint32 r2 = 0; // if there's no color, return black + r2 = sp_svg_read_color(val.c_str(), r2); + if (attr_prop.count(iter.first) || (value != val && (r1 == 0 || r1 != r2))) { + row[_mColumns._colStrike] = true; + row[_mColumns._colOwner] = Glib::ustring(""); + } else { + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + _addOwnerStyle(iter.first, selector); + } + } else { + row[_mColumns._colStrike] = true; + Glib::ustring tooltiptext = _("This value is commented"); + row[_mColumns._colOwner] = tooltiptext; + } + } + } else { + for (auto iter : result_props) { + empty = false; + Gtk::TreeModel::Row row = *(store->prepend()); + row[_mColumns._colSelector] = selector_orig; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = iter.second.second; + row[_mColumns._colName] = iter.first; + row[_mColumns._colValue] = iter.second.first; + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Stylesheet value"); + } + } + if (empty) { + css_tree->hide(); + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + selectorpos++; + } + try { + _builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + _builder->get_widget("CSSSelector", css_selector); + css_selector->set_text("element.attributes"); + _builder->get_widget("CSSSelectorContainer", css_selector_container); + _builder->get_widget("CSSSelectorEventAdd", css_selector_event_add); + css_selector_event_add->add_events(Gdk::BUTTON_RELEASE_MASK); + store = Gtk::TreeStore::create(_mColumns); + _builder->get_widget("CSSTree", css_tree); + css_tree->get_style_context()->add_class("style_attribute"); + css_tree->set_model(store); + css_selector_event_add->signal_button_release_event().connect( + sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *, Glib::ustring, gint>( + sigc::mem_fun(*this, &StyleDialog::_addRow), store, css_tree, "attributes", selectorpos)); + bool hasattributes = false; + empty = true; + if (obj) { + for (auto iter : obj->style->properties()) { + if (iter->style_src != SP_STYLE_SRC_UNSET) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) { + const gchar *attr = obj->getRepr()->attribute(iter->name().c_str()); + if (attr) { + if (!hasattributes) { + Inkscape::UI::Widget::IconRenderer *addRenderer = + manage(new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + int addCol = css_tree->append_column(" ", *addRenderer) - 1; + Gtk::TreeViewColumn *col = css_tree->get_column(addCol); + if (col) { + addRenderer->signal_activated().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_onPropDelete), store)); + } + Gtk::CellRendererText *label = Gtk::manage(new Gtk::CellRendererText()); + label->property_placeholder_text() = _("property"); + label->property_editable() = true; + label->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>, Gtk::TreeView *>( + sigc::mem_fun(*this, &StyleDialog::_nameEdited), store, css_tree)); + label->signal_editing_started().connect(sigc::mem_fun(*this, &StyleDialog::_startNameEdit)); + addCol = css_tree->append_column(" ", *label) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->set_resizable(true); + col->add_attribute(label->property_text(), _mColumns._colName); + } + Gtk::CellRendererText *value = Gtk::manage(new Gtk::CellRendererText()); + value->property_placeholder_text() = _("value"); + value->property_editable() = true; + value->signal_edited().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_valueEdited), store)); + value->signal_editing_started().connect(sigc::bind<Glib::RefPtr<Gtk::TreeStore>>( + sigc::mem_fun(*this, &StyleDialog::_startValueEdit), store)); + + addCol = css_tree->append_column(" ", *value) - 1; + col = css_tree->get_column(addCol); + if (col) { + col->add_attribute(value->property_text(), _mColumns._colValue); + col->add_attribute(value->property_strikethrough(), _mColumns._colStrike); + } + } + empty = false; + Gtk::TreeIter iterstore = store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iterstore; + Gtk::TreeModel::Row row = *(iterstore); + row[_mColumns._colSelector] = "attributes"; + row[_mColumns._colSelectorPos] = selectorpos; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = iter->name(); + row[_mColumns._colValue] = attr; + if (_owner_style.find(iter->name()) != _owner_style.end()) { + row[_mColumns._colStrike] = true; + Glib::ustring tooltiptext = Glib::ustring(""); + row[_mColumns._colOwner] = tooltiptext; + } else { + row[_mColumns._colStrike] = false; + row[_mColumns._colOwner] = Glib::ustring("Current value"); + _addOwnerStyle(iter->name(), "inline attributes"); + } + hasattributes = true; + } + } + } + } + if (empty) { + css_tree->hide(); + } + if (!hasattributes) { + for (auto widg : css_selector_container->get_children()) { + delete widg; + } + } + _styleBox.pack_start(*css_selector_container, Gtk::PACK_EXPAND_WIDGET); + } + for (auto selector : _styleBox.get_children()) { + Gtk::Box *box = dynamic_cast<Gtk::Box *>(&selector[0]); + if (box) { + std::vector<Gtk::Widget *> childs = box->get_children(); + if (childs.size() > 1) { + Gtk::TreeView *css_tree = dynamic_cast<Gtk::TreeView *>(childs[1]); + if (css_tree) { + Glib::RefPtr<Gtk::TreeModel> model = css_tree->get_model(); + if (model) { + model->foreach_iter(sigc::mem_fun(*this, &StyleDialog::_on_foreach_iter)); + } + } + } + } + } + if (obj) { + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + _mainBox.show_all_children(); + _updating = false; +} + +bool StyleDialog::_selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit) +{ + g_debug("StyleDialog::_selectorStartEdit"); + if (event->type == GDK_BUTTON_RELEASE && event->button == 1) { + selector->hide(); + selector_edit->set_text(selector->get_text()); + selector_edit->show(); + } + return false; +} + +/* void StyleDialog::_selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry +*selector_edit) +{ + g_debug("StyleDialog::_selectorEditKeyPress"); + Glib::ustring newselector = fixCSSSelectors(selector_edit->get_text()); + if (newselector.empty()) { + selector_edit->get_style_context()->add_class("system_error_color"); + return; + } + _writeStyleElement(store, selector->get_text(), selector_edit->get_text()); +} */ + +bool StyleDialog::_selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, + Gtk::Entry *selector_edit) +{ + g_debug("StyleDialog::_selectorEditKeyPress"); + switch (event->keyval) { + case GDK_KEY_Escape: + selector->show(); + selector_edit->hide(); + selector_edit->get_style_context()->remove_class("system_error_color"); + break; + } + return false; +} + +bool StyleDialog::_on_foreach_iter(const Gtk::TreeModel::iterator &iter) +{ + g_debug("StyleDialog::_on_foreach_iter"); + + Gtk::TreeModel::Row row = *(iter); + Glib::ustring owner = row[_mColumns._colOwner]; + if (owner.empty()) { + Glib::ustring value = _owner_style[row[_mColumns._colName]]; + Glib::ustring tooltiptext = Glib::ustring(_("Invalid property set")); + if (!value.empty()) { + tooltiptext = Glib::ustring(_("Used in ") + _owner_style[row[_mColumns._colName]]); + } + row[_mColumns._colOwner] = tooltiptext; + } + return false; +} + +void StyleDialog::_onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_onLinkObj"); + + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row && row[_mColumns._colLinked]) { + SPObject *linked = row[_mColumns._colHref]; + if (linked) { + Inkscape::Selection *selection = getDesktop()->getSelection(); + getDesktop()->getDocument()->setXMLDialogSelectedObject(linked); + selection->clear(); + selection->set(linked); + } + } +} + +/** + * @brief StyleDialog::_onPropDelete + * @param event + * @return true + * Delete the attribute from the style + */ +void StyleDialog::_onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_onPropDelete"); + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Glib::ustring selector = row[_mColumns._colSelector]; + row[_mColumns._colName] = ""; + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + _deletion = true; + _writeStyleElement(store, selector); + } +} + +void StyleDialog::_addOwnerStyle(Glib::ustring name, Glib::ustring selector) +{ + g_debug("StyleDialog::_addOwnerStyle"); + + if (_owner_style.find(name) == _owner_style.end()) { + _owner_style[name] = selector; + } +} + + +/** + * @brief StyleDialog::parseStyle + * + * Convert a style string into a vector map. This should be moved to style.cpp + * + */ +std::map<Glib::ustring, Glib::ustring> StyleDialog::parseStyle(Glib::ustring style_string) +{ + g_debug("StyleDialog::parseStyle"); + + std::map<Glib::ustring, Glib::ustring> ret; + + REMOVE_SPACES(style_string); // We'd use const, but we need to trip spaces + std::vector<Glib::ustring> props = r_props->split(style_string); + + for (auto token : props) { + REMOVE_SPACES(token); + + if (token.empty()) + break; + std::vector<Glib::ustring> pair = r_pair->split(token); + + if (pair.size() > 1) { + ret[pair[0]] = pair[1]; + } + } + return ret; +} + + +/** + * Update the content of the style element as selectors (or objects) are added/removed. + */ +void StyleDialog::_writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector, + Glib::ustring new_selector) +{ + g_debug("StyleDialog::_writeStyleElemen"); + if (_updating) { + return; + } + _scroollock = true; + Inkscape::Selection *selection = getDesktop()->getSelection(); + SPObject *obj = nullptr; + if (selection->objects().size() == 1) { + obj = selection->objects().back(); + } + if (!obj) { + obj = getDesktop()->getDocument()->getXMLDialogSelectedObject(); + } + if (selection->objects().size() < 2 && !obj) { + readStyleElement(); + return; + } + _updating = true; + gint selectorpos = 0; + std::string styleContent = ""; + if (selector != "style_properties" && selector != "attributes") { + if (!new_selector.empty()) { + selector = new_selector; + } + std::vector<Glib::ustring> selectordata = Glib::Regex::split_simple(";", selector); + for (auto selectoritem : selectordata) { + if (selectordata[selectordata.size() - 1] == selectoritem) { + selector = selectoritem; + } else { + styleContent = styleContent + selectoritem + ";\n"; + } + } + styleContent = styleContent + "\n" + selector + " { \n"; + } + selectorpos = _deleted_pos; + for (auto &row : store->children()) { + selector = row[_mColumns._colSelector]; + selectorpos = row[_mColumns._colSelectorPos]; + Glib::ustring opencomment = ""; + Glib::ustring closecomment = ""; + if (selector != "style_properties" && selector != "attributes") { + opencomment = row[_mColumns._colActive] ? " " : " /*"; + closecomment = row[_mColumns._colActive] ? "\n" : "*/\n"; + } + Glib::ustring name = row[_mColumns._colName]; + Glib::ustring value = row[_mColumns._colValue]; + if (!(name.empty() && value.empty())) { + styleContent = styleContent + opencomment + name + ":" + value + ";" + closecomment; + } + } + if (selector != "style_properties" && selector != "attributes") { + styleContent = styleContent + "}"; + } + if (selector == "style_properties") { + _updating = true; + obj->getRepr()->setAttribute("style", styleContent, false); + _updating = false; + } else if (selector == "attributes") { + for (auto iter : obj->style->properties()) { + auto key = iter->id(); + if (key != SP_PROP_FONT && key != SP_ATTR_D && key != SP_PROP_MARKER) { + const gchar *attr = obj->getRepr()->attribute(iter->name().c_str()); + if (attr) { + _updating = true; + obj->getRepr()->removeAttribute(iter->name()); + _updating = false; + } + } + } + for (auto &row : store->children()) { + Glib::ustring name = row[_mColumns._colName]; + Glib::ustring value = row[_mColumns._colValue]; + if (!(name.empty() && value.empty())) { + _updating = true; + obj->getRepr()->setAttribute(name, value); + _updating = false; + } + } + } else if (!selector.empty()) { // styleshet + // We could test if styleContent is empty and then delete the style node here but there is no + // harm in keeping it around ... + + std::string pos = std::to_string(selectorpos); + std::string selectormatch = "("; + for (; selectorpos > 1; selectorpos--) { + selectormatch = selectormatch + "[^}]*?}"; + } + selectormatch = selectormatch + ")([^}]*?})((.|\n)*)"; + + Inkscape::XML::Node *textNode = _getStyleTextNode(true); + std::regex e(selectormatch.c_str()); + std::string content = (textNode->content() ? textNode->content() : ""); + std::string result; + std::regex_replace(std::back_inserter(result), content.begin(), content.end(), e, "$1" + styleContent + "$3"); + bool empty = false; + if (result.empty()) { + empty = true; + result = "* > .inkscapehacktmp{}"; + } + textNode->setContent(result.c_str()); + INKSCAPE.readStyleSheets(true); + if (empty) { + textNode->setContent(""); + } + } + _updating = false; + readStyleElement(); + SPDocument *document = SP_ACTIVE_DOCUMENT; + for (auto iter : document->getObjectsBySelector(selector)) { + iter->style->readFromObject(iter); + iter->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + DocumentUndo::done(SP_ACTIVE_DOCUMENT, SP_VERB_DIALOG_STYLE, _("Edited style element.")); + + g_debug("StyleDialog::_writeStyleElement(): | %s |", styleContent.c_str()); +} + +bool StyleDialog::_addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree, + Glib::ustring selector, gint pos) +{ + g_debug("StyleDialog::_addRow"); + + if (evt->type == GDK_BUTTON_RELEASE && evt->button == 1) { + Gtk::TreeIter iter = store->prepend(); + Gtk::TreeModel::Path path = (Gtk::TreeModel::Path)iter; + Gtk::TreeModel::Row row = *(iter); + row[_mColumns._colSelector] = selector; + row[_mColumns._colSelectorPos] = pos; + row[_mColumns._colActive] = true; + row[_mColumns._colName] = ""; + row[_mColumns._colValue] = ""; + row[_mColumns._colStrike] = false; + gint col = 2; + if (pos < 1) { + col = 1; + } + css_tree->show(); + css_tree->set_cursor(path, *(css_tree->get_column(col)), true); + grab_focus(); + return true; + } + return false; +} + +void StyleDialog::_setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]) +{ + g_debug("StyleDialog::_setAutocompletion"); + + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column (_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(0); + entry_completion->set_popup_completion(true); + gint counter = 0; + const char * key = cssenum[counter].key; + while (key) { + Gtk::TreeModel::Row row = *(completionModel->prepend()); + row[_mCSSData._colCSSData] = Glib::ustring(key); + counter++; + key = cssenum[counter].key; + } + entry->set_completion(entry_completion); +} +/*Harcode values non in enum*/ +void StyleDialog::_setAutocompletion(Gtk::Entry *entry, Glib::ustring name) +{ + g_debug("StyleDialog::_setAutocompletion"); + + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column(_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(0); + entry_completion->set_popup_completion(true); + if (name == "paint-order") { + Gtk::TreeModel::Row row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("fill markers stroke"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("fill stroke markers"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("stroke markers fill"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("stroke fill markers"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("markers fill stroke"); + row = *(completionModel->append()); + row[_mCSSData._colCSSData] = Glib::ustring("markers stroke fill"); + } + entry->set_completion(entry_completion); +} + +void +StyleDialog::_startValueEdit(Gtk::CellEditable* cell, const Glib::ustring& path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_startValueEdit"); + _deletion = false; + _scroollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + Glib::ustring name = row[_mColumns._colName]; + if (name == "paint-order") { + _setAutocompletion(entry, name); + } else if (name == "fill-rule") { + _setAutocompletion(entry, enum_fill_rule); + } else if (name == "stroke-linecap") { + _setAutocompletion(entry, enum_stroke_linecap); + } else if (name == "stroke-linejoin") { + _setAutocompletion(entry, enum_stroke_linejoin); + } else if (name == "font-style") { + _setAutocompletion(entry, enum_font_style); + } else if (name == "font-variant") { + _setAutocompletion(entry, enum_font_variant); + } else if (name == "font-weight") { + _setAutocompletion(entry, enum_font_weight); + } else if (name == "font-stretch") { + _setAutocompletion(entry, enum_font_stretch); + } else if (name == "font-variant-position") { + _setAutocompletion(entry, enum_font_variant_position); + } else if (name == "text-align") { + _setAutocompletion(entry, enum_text_align); + } else if (name == "text-transform") { + _setAutocompletion(entry, enum_text_transform); + } else if (name == "text-anchor") { + _setAutocompletion(entry, enum_text_anchor); + } else if (name == "white-space") { + _setAutocompletion(entry, enum_white_space); + } else if (name == "direction") { + _setAutocompletion(entry, enum_direction); + } else if (name == "baseline-shift") { + _setAutocompletion(entry, enum_baseline_shift); + } else if (name == "visibility") { + _setAutocompletion(entry, enum_visibility); + } else if (name == "overflow") { + _setAutocompletion(entry, enum_overflow); + } else if (name == "display") { + _setAutocompletion(entry, enum_display); + } else if (name == "shape-rendering") { + _setAutocompletion(entry, enum_shape_rendering); + } else if (name == "color-rendering") { + _setAutocompletion(entry, enum_color_rendering); + } else if (name == "overflow") { + _setAutocompletion(entry, enum_overflow); + } else if (name == "clip-rule") { + _setAutocompletion(entry, enum_clip_rule); + } else if (name == "color-interpolation") { + _setAutocompletion(entry, enum_color_interpolation); + } + entry->signal_key_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyReleased), entry)); + entry->signal_key_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onValueKeyPressed), entry)); + } +} + +void StyleDialog::_startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path) +{ + _deletion = false; + g_debug("StyleDialog::_startNameEdit"); + _scroollock = true; + Glib::RefPtr<Gtk::ListStore> completionModel = Gtk::ListStore::create(_mCSSData); + Glib::RefPtr<Gtk::EntryCompletion> entry_completion = Gtk::EntryCompletion::create(); + entry_completion->set_model(completionModel); + entry_completion->set_text_column(_mCSSData._colCSSData); + entry_completion->set_minimum_key_length(1); + entry_completion->set_popup_completion(true); + for (auto prop : sp_attribute_name_list(true)) { + Gtk::TreeModel::Row row = *(completionModel->append()); + row[_mCSSData._colCSSData] = prop; + } + Gtk::Entry *entry = dynamic_cast<Gtk::Entry *>(cell); + entry->set_completion(entry_completion); + entry->signal_key_release_event().connect( + sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyReleased), entry)); + entry->signal_key_press_event().connect(sigc::bind(sigc::mem_fun(*this, &StyleDialog::_onNameKeyPressed), entry)); +} + + +gboolean sp_styledialog_store_move_to_next(gpointer data) +{ + StyleDialog *styledialog = reinterpret_cast<StyleDialog *>(data); + if (!styledialog->_deletion) { + auto selection = styledialog->_current_css_tree->get_selection(); + Gtk::TreeIter iter = *(selection->get_selected()); + Gtk::TreeModel::Path model = (Gtk::TreeModel::Path)iter; + if (model == styledialog->_current_path) { + styledialog->_current_css_tree->set_cursor(styledialog->_current_path, *styledialog->_current_value_col, + true); + } + } + return FALSE; +} + +/** + * @brief StyleDialog::nameEdited + * @param event + * @return + * Called when the name is edited in the TreeView editable column + */ +void StyleDialog::_nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store, + Gtk::TreeView *css_tree) +{ + g_debug("StyleDialog::_nameEdited"); + + _scroollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + _current_path = (Gtk::TreeModel::Path)*store->get_iter(path); + + if (row) { + _current_css_tree = css_tree; + Glib::ustring finalname = name; + auto i = std::min(finalname.find(";"), finalname.find(":")); + if (i != std::string::npos) { + finalname.erase(i, name.size() - i); + } + gint pos = row[_mColumns._colSelectorPos]; + bool write = false; + if (row[_mColumns._colName] != finalname && row[_mColumns._colValue] != "") { + write = true; + } + Glib::ustring selector = row[_mColumns._colSelector]; + Glib::ustring value = row[_mColumns._colValue]; + bool is_attr = selector == "attributes"; + Glib::ustring old_name = row[_mColumns._colName]; + row[_mColumns._colName] = finalname; + if (finalname.empty() && value.empty()) { + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + } + gint col = 3; + if (pos < 1 || is_attr) { + col = 2; + } + _current_value_col = css_tree->get_column(col); + if (write && old_name != name) { + _writeStyleElement(store, selector); + /* + I think is better comment this, is enoght update on value change + if (selector != "style_properties" && selector != "attributes") { + std::vector<SPObject *> objs = _getObjVec(selector); + for (auto obj : objs){ + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + css->setAttribute(name, nullptr); + sp_repr_css_write_string(css, css_str); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } */ + } else { + g_timeout_add(50, &sp_styledialog_store_move_to_next, this); + grab_focus(); + } + } +} + +/** + * @brief StyleDialog::valueEdited + * @param event + * @return + * Called when the value is edited in the TreeView editable column + */ +void StyleDialog::_valueEdited(const Glib::ustring &path, const Glib::ustring &value, + Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_valueEdited"); + + _scroollock = true; + + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + Glib::ustring finalvalue = value; + auto i = std::min(finalvalue.find(";"), finalvalue.find(":")); + if (i != std::string::npos) { + finalvalue.erase(i, finalvalue.size() - i); + } + Glib::ustring old_value = row[_mColumns._colValue]; + if (old_value == finalvalue) { + return; + } + row[_mColumns._colValue] = finalvalue; + Glib::ustring selector = row[_mColumns._colSelector]; + Glib::ustring name = row[_mColumns._colName]; + if (name.empty() && finalvalue.empty()) { + _deleted_pos = row[_mColumns._colSelectorPos]; + store->erase(row); + } + _writeStyleElement(store, selector); + if (selector != "style_properties" && selector != "attributes") { + std::vector<SPObject *> objs = _getObjVec(selector); + for (auto obj : objs) { + Glib::ustring css_str = ""; + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, obj->getRepr()->attribute("style")); + css->removeAttribute(name); + sp_repr_css_write_string(css, css_str); + obj->getRepr()->setAttribute("style", css_str); + obj->style->readFromObject(obj); + obj->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } + } +} + +void StyleDialog::_activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store) +{ + g_debug("StyleDialog::_activeToggled"); + + _scroollock = true; + Gtk::TreeModel::Row row = *store->get_iter(path); + if (row) { + row[_mColumns._colActive] = !row[_mColumns._colActive]; + Glib::ustring selector = row[_mColumns._colSelector]; + _writeStyleElement(store, selector); + } +} + +bool StyleDialog::_onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +bool StyleDialog::_onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onNameKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_equal: + case GDK_KEY_colon: + entry->editing_done(); + ret = true; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_semicolon: { + Glib::ustring text = entry->get_text(); + auto i = std::min(text.find(";"), text.find(":")); + if (i != std::string::npos) { + entry->editing_done(); + ret = true; + } + break; + } + } + return ret; +} + +bool StyleDialog::_onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_Tab: + case GDK_KEY_KP_Tab: + entry->editing_done(); + ret = true; + break; + } + return ret; +} + +bool StyleDialog::_onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry) +{ + g_debug("StyleDialog::_onValueKeyReleased"); + bool ret = false; + switch (event->keyval) { + case GDK_KEY_semicolon: + entry->editing_done(); + ret = true; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_colon: { + Glib::ustring text = entry->get_text(); + auto i = std::min(text.find(";"), text.find(":")); + if (i != std::string::npos) { + entry->editing_done(); + ret = true; + } + break; + } + } + return ret; +} + +/** + * Update the watchers on objects. + */ +void StyleDialog::_updateWatchers(SPDesktop *desktop) + +{ + g_debug("StyleDialog::_updateWatchers"); + + if (_textNode) { + _textNode->removeObserver(*m_styletextwatcher); + _textNode = nullptr; + } + + if (m_root) { + m_root->removeSubtreeObserver(*m_nodewatcher); + m_root = nullptr; + } + + if (desktop) { + m_root = desktop->getDocument()->getReprRoot(); + m_root->addSubtreeObserver(*m_nodewatcher); + } +} + + +/** + * @param selector: a valid CSS selector string. + * @return objVec: a vector of pointers to SPObject's the selector matches. + * Return a vector of all objects that selector matches. + */ +std::vector<SPObject *> StyleDialog::_getObjVec(Glib::ustring selector) +{ + g_debug("StyleDialog::_getObjVec"); + + g_assert(selector.find(";") == Glib::ustring::npos); + + return getDesktop()->getDocument()->getObjectsBySelector(selector); +} + +void StyleDialog::_closeDialog(Gtk::Dialog *textDialogPtr) { textDialogPtr->response(Gtk::RESPONSE_OK); } + + +/** + * Handle document replaced. (Happens when a default document is immediately replaced by another + * document in a new window.) + */ +void StyleDialog::_handleDocumentReplaced(SPDesktop *desktop, SPDocument * /* document */) +{ + g_debug("StyleDialog::handleDocumentReplaced()"); + + _selection_changed_connection.disconnect(); + + _updateWatchers(desktop); + + if (!desktop) + return; + + _selection_changed_connection = + desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + + readStyleElement(); +} + + +/* + * When a dialog is floating, it is connected to the active desktop. + */ +void StyleDialog::_handleDesktopChanged(SPDesktop *desktop) +{ + g_debug("StyleDialog::handleDesktopReplaced()"); + + if (getDesktop() == desktop) { + // This will happen after construction of dialog. We've already + // set up signals so just return. + return; + } + + _selection_changed_connection.disconnect(); + _document_replaced_connection.disconnect(); + setDesktop(desktop); + + _selection_changed_connection = + desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &StyleDialog::_handleSelectionChanged))); + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &StyleDialog::_handleDocumentReplaced)); + + _updateWatchers(desktop); + readStyleElement(); +} + + +/* + * Handle a change in which objects are selected in a document. + */ +void StyleDialog::_handleSelectionChanged() +{ + g_debug("StyleDialog::_handleSelectionChanged()"); + _scroolpos = 0; + _vadj->set_value(0); + readStyleElement(); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/styledialog.h b/src/ui/dialog/styledialog.h new file mode 100644 index 0000000..2d96d48 --- /dev/null +++ b/src/ui/dialog/styledialog.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A dialog for CSS selectors + */ +/* Authors: + * Kamalpreet Kaur Grewal + * Tavmjong Bah + * + * Copyright (C) Kamalpreet Kaur Grewal 2016 <grewalkamal005@gmail.com> + * Copyright (C) Tavmjong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef STYLEDIALOG_H +#define STYLEDIALOG_H + +#include "style-enums.h" +#include <glibmm/regex.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/builder.h> +#include <gtkmm/celleditable.h> +#include <gtkmm/cellrenderercombo.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/entrycompletion.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/switch.h> +#include <gtkmm/tooltip.h> +#include <gtkmm/treemodelfilter.h> +#include <gtkmm/treeselection.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> +#include <gtkmm/viewport.h> +#include <ui/widget/panel.h> + +#include "ui/dialog/desktop-tracker.h" + +#include "xml/helper-observer.h" + +#include <memory> +#include <vector> + +namespace Inkscape { + +XML::Node *get_first_style_text_node(XML::Node *root, bool create_if_missing); + +namespace UI { +namespace Dialog { + +/** + * @brief The StyleDialog class + * A list of CSS selectors will show up in this dialog. This dialog allows one to + * add and delete selectors. Elements can be added to and removed from the selectors + * in the dialog. Selection of any selector row selects the matching objects in + * the drawing and vice-versa. (Only simple selectors supported for now.) + * + * This class must keep two things in sync: + * 1. The text node of the style element. + * 2. The Gtk::TreeModel. + */ +class StyleDialog : public Widget::Panel { + + public: + ~StyleDialog() override; + // No default constructor, noncopyable, nonassignable + StyleDialog(); + StyleDialog(StyleDialog const &d) = delete; + StyleDialog operator=(StyleDialog const &d) = delete; + + static StyleDialog &getInstance() { return *new StyleDialog(); } + void setCurrentSelector(Glib::ustring current_selector); + Gtk::TreeView *_current_css_tree; + Gtk::TreeViewColumn *_current_value_col; + Gtk::TreeModel::Path _current_path; + bool _deletion; + Glib::ustring fixCSSSelectors(Glib::ustring selector); + void readStyleElement(); + + private: + // Monitor <style> element for changes. + class NodeObserver; + // Monitor all objects for addition/removal/attribute change + class NodeWatcher; + Glib::RefPtr<Glib::Regex> r_props = Glib::Regex::create("\\s*;\\s*"); + Glib::RefPtr<Glib::Regex> r_pair = Glib::Regex::create("\\s*:\\s*"); + void _nodeAdded(Inkscape::XML::Node &repr); + void _nodeRemoved(Inkscape::XML::Node &repr); + void _nodeChanged(Inkscape::XML::Node &repr); + /* void _stylesheetChanged( Inkscape::XML::Node &repr ); */ + // Data structure + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() + { + add(_colActive); + add(_colName); + add(_colValue); + add(_colStrike); + add(_colSelector); + add(_colSelectorPos); + add(_colOwner); + add(_colLinked); + add(_colHref); + } + Gtk::TreeModelColumn<bool> _colActive; // Active or inactive property + Gtk::TreeModelColumn<Glib::ustring> _colName; // Name of the property. + Gtk::TreeModelColumn<Glib::ustring> _colValue; // Value of the property. + Gtk::TreeModelColumn<bool> _colStrike; // Property not used, overloaded + Gtk::TreeModelColumn<Glib::ustring> _colSelector; // Style or matching object id. + Gtk::TreeModelColumn<gint> _colSelectorPos; // Position of the selector to handle dup selectors + Gtk::TreeModelColumn<Glib::ustring> _colOwner; // Store the owner of the property for popup + Gtk::TreeModelColumn<bool> _colLinked; // Other object linked + Gtk::TreeModelColumn<SPObject *> _colHref; // Is going to another object + }; + ModelColumns _mColumns; + + class CSSData : public Gtk::TreeModel::ColumnRecord { + public: + CSSData() { add(_colCSSData); } + Gtk::TreeModelColumn<Glib::ustring> _colCSSData; // Name of the property. + }; + CSSData _mCSSData; + guint _deleted_pos; + // Widgets + Gtk::ScrolledWindow _scrolledWindow; + Glib::RefPtr<Gtk::Adjustment> _vadj; + Gtk::Box _mainBox; + Gtk::Box _styleBox; + // Reading and writing the style element. + Inkscape::XML::Node *_getStyleTextNode(bool create_if_missing = false); + Glib::RefPtr<Gtk::TreeModel> _selectTree(Glib::ustring selector); + void _writeStyleElement(Glib::RefPtr<Gtk::TreeStore> store, Glib::ustring selector, + Glib::ustring new_selector = ""); + // void _selectorActivate(Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, Gtk::Entry *selector_edit); + bool _selectorEditKeyPress(GdkEventKey *event, Glib::RefPtr<Gtk::TreeStore> store, Gtk::Label *selector, + Gtk::Entry *selector_edit); + bool _selectorStartEdit(GdkEventButton *event, Gtk::Label *selector, Gtk::Entry *selector_edit); + void _activeToggled(const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store); + bool _addRow(GdkEventButton *evt, Glib::RefPtr<Gtk::TreeStore> store, Gtk::TreeView *css_tree, + Glib::ustring selector, gint pos); + void _onPropDelete(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store); + void _nameEdited(const Glib::ustring &path, const Glib::ustring &name, Glib::RefPtr<Gtk::TreeStore> store, + Gtk::TreeView *css_tree); + bool _onNameKeyReleased(GdkEventKey *event, Gtk::Entry *entry); + bool _onValueKeyReleased(GdkEventKey *event, Gtk::Entry *entry); + bool _onNameKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + bool _onValueKeyPressed(GdkEventKey *event, Gtk::Entry *entry); + void _onLinkObj(Glib::ustring path, Glib::RefPtr<Gtk::TreeStore> store); + void _valueEdited(const Glib::ustring &path, const Glib::ustring &value, Glib::RefPtr<Gtk::TreeStore> store); + void _startNameEdit(Gtk::CellEditable *cell, const Glib::ustring &path); + + void _startValueEdit(Gtk::CellEditable *cell, const Glib::ustring &path, Glib::RefPtr<Gtk::TreeStore> store); + void _setAutocompletion(Gtk::Entry *entry, SPStyleEnum const cssenum[]); + void _setAutocompletion(Gtk::Entry *entry, Glib::ustring name); + bool _on_foreach_iter(const Gtk::TreeModel::iterator &iter); + void _reload(); + void _vscrool(); + bool _scroollock; + double _scroolpos; + Glib::ustring _current_selector; + + // Update watchers + std::unique_ptr<Inkscape::XML::NodeObserver> m_nodewatcher; + std::unique_ptr<Inkscape::XML::NodeObserver> m_styletextwatcher; + void _updateWatchers(SPDesktop *); + + // Manipulate Tree + std::vector<SPObject *> _getObjVec(Glib::ustring selector); + std::map<Glib::ustring, Glib::ustring> parseStyle(Glib::ustring style_string); + std::map<Glib::ustring, Glib::ustring> _owner_style; + void _addOwnerStyle(Glib::ustring name, Glib::ustring selector); + // Variables + Inkscape::XML::Node *m_root = nullptr; + Inkscape::XML::Node *_textNode; // Track so we know when to add a NodeObserver. + bool _updating; // Prevent cyclic actions: read <-> write, select via dialog <-> via desktop + + // Signals and handlers - External + sigc::connection _document_replaced_connection; + sigc::connection _desktop_changed_connection; + sigc::connection _selection_changed_connection; + + void _handleDocumentReplaced(SPDesktop *desktop, SPDocument *document); + void _handleDesktopChanged(SPDesktop *desktop); + void _handleSelectionChanged(); + void _closeDialog(Gtk::Dialog *textDialogPtr); + DesktopTracker _desktopTracker; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif // STYLEDIALOG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/svg-fonts-dialog.cpp b/src/ui/dialog/svg-fonts-dialog.cpp new file mode 100644 index 0000000..9f191d1 --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.cpp @@ -0,0 +1,1067 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * SVG Fonts dialog - implementation. + */ +/* Authors: + * Felipe C. da S. Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2008 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <message-stack.h> +#include <sstream> + +#include <gtkmm/scale.h> +#include <gtkmm/notebook.h> +#include <gtkmm/imagemenuitem.h> +#include <glibmm/stringutils.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" +#include "svg-fonts-dialog.h" +#include "verbs.h" + +#include "display/nr-svgfonts.h" +#include "include/gtkmm_version.h" +#include "object/sp-defs.h" +#include "object/sp-font-face.h" +#include "object/sp-font.h" +#include "object/sp-glyph-kerning.h" +#include "object/sp-glyph.h" +#include "object/sp-missing-glyph.h" +#include "svg/svg.h" +#include "xml/repr.h" + +SvgFontDrawingArea::SvgFontDrawingArea(): + _x(0), + _y(0), + _svgfont(nullptr), + _text() +{ +} + +void SvgFontDrawingArea::set_svgfont(SvgFont* svgfont){ + _svgfont = svgfont; +} + +void SvgFontDrawingArea::set_text(Glib::ustring text){ + _text = text; + redraw(); +} + +void SvgFontDrawingArea::set_size(int x, int y){ + _x = x; + _y = y; + ((Gtk::Widget*) this)->set_size_request(_x, _y); +} + +void SvgFontDrawingArea::redraw(){ + ((Gtk::Widget*) this)->queue_draw(); +} + +bool SvgFontDrawingArea::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) { + if (_svgfont){ + cr->set_font_face( Cairo::RefPtr<Cairo::FontFace>(new Cairo::FontFace(_svgfont->get_font_face(), false /* does not have reference */)) ); + cr->set_font_size (_y-20); + cr->move_to (10, 10); + cr->show_text (_text.c_str()); + + // Draw some lines to show line area. + cr->set_source_rgb( 0.5, 0.5, 0.5 ); + cr->move_to ( 0, 10); + cr->line_to (_x, 10); + cr->stroke(); + cr->move_to ( 0, _y-10); + cr->line_to (_x, _y-10); + cr->stroke(); + } + return true; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/* +Gtk::HBox* SvgFontsDialog::AttrEntry(gchar* lbl, const SPAttributeEnum attr){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + Gtk::Entry* entry = Gtk::manage(new Gtk::Entry()); + hbox->add(* entry ); + hbox->show_all(); + + entry->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_attr_changed)); + return hbox; +} +*/ + +SvgFontsDialog::AttrEntry::AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr){ + this->dialog = d; + this->attr = attr; + entry.set_tooltip_text(tooltip); + auto label = new Gtk::Label(lbl); + this->pack_start(*Gtk::manage(label), false, false, 4); + this->pack_end(entry, true, true); + this->show_all(); + + entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrEntry::on_attr_changed)); +} + +void SvgFontsDialog::AttrEntry::set_text(char* t){ + if (!t) return; + entry.set_text(t); +} + +// 'font-family' has a problem as it is also a presentation attribute for <text> +void SvgFontsDialog::AttrEntry::on_attr_changed(){ + + SPObject* o = nullptr; + for (auto& node: dialog->get_selected_spfont()->children) { + switch(this->attr){ + case SP_PROP_FONT_FAMILY: + if (SP_IS_FONTFACE(&node)){ + o = &node; + continue; + } + break; + default: + o = nullptr; + } + } + + const gchar* name = (const gchar*)sp_attribute_name(this->attr); + if(name && o) { + o->setAttribute((const gchar*) name, this->entry.get_text()); + o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "svgfonts:"; + undokey += name; + DocumentUndo::maybeDone(o->document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS, + _("Set SVG Font attribute")); + } + +} + +SvgFontsDialog::AttrSpin::AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr) { + + this->dialog = d; + this->attr = attr; + spin.set_tooltip_text(tooltip); + auto label = new Gtk::Label(lbl); + this->set_border_width(2); + this->set_spacing(6); + this->pack_start(*Gtk::manage(label), false, false); + this->pack_end(spin, true, true); + this->show_all(); + spin.set_range(0, 4096); + spin.set_increments(16, 0); + spin.signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::AttrSpin::on_attr_changed)); +} + +void SvgFontsDialog::AttrSpin::set_range(double low, double high){ + spin.set_range(low, high); +} + +void SvgFontsDialog::AttrSpin::set_value(double v){ + spin.set_value(v); +} + +void SvgFontsDialog::AttrSpin::on_attr_changed(){ + + SPObject* o = nullptr; + switch (this->attr) { + + // <font> attributes + case SP_ATTR_HORIZ_ORIGIN_X: + case SP_ATTR_HORIZ_ORIGIN_Y: + case SP_ATTR_HORIZ_ADV_X: + case SP_ATTR_VERT_ORIGIN_X: + case SP_ATTR_VERT_ORIGIN_Y: + case SP_ATTR_VERT_ADV_Y: + o = this->dialog->get_selected_spfont(); + break; + + // <font-face> attributes + case SP_ATTR_UNITS_PER_EM: + case SP_ATTR_ASCENT: + case SP_ATTR_DESCENT: + case SP_ATTR_CAP_HEIGHT: + case SP_ATTR_X_HEIGHT: + for (auto& node: dialog->get_selected_spfont()->children){ + if (SP_IS_FONTFACE(&node)){ + o = &node; + continue; + } + } + break; + + default: + o = nullptr; + } + + const gchar* name = (const gchar*)sp_attribute_name(this->attr); + if(name && o) { + std::ostringstream temp; + temp << this->spin.get_value(); + o->setAttribute(name, temp.str()); + o->parent->requestModified(SP_OBJECT_MODIFIED_FLAG); + + Glib::ustring undokey = "svgfonts:"; + undokey += name; + DocumentUndo::maybeDone(o->document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS, + _("Set SVG Font attribute")); + } + +} + +Gtk::HBox* SvgFontsDialog::AttrCombo(gchar* lbl, const SPAttributeEnum /*attr*/){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + hbox->add(* Gtk::manage(new Gtk::ComboBox()) ); + hbox->show_all(); + return hbox; +} + +/* +Gtk::HBox* SvgFontsDialog::AttrSpin(gchar* lbl){ + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + hbox->add(* Gtk::manage(new Gtk::Label(lbl)) ); + hbox->add(* Gtk::manage(new Inkscape::UI::Widget::SpinBox()) ); + hbox->show_all(); + return hbox; +}*/ + +/*** SvgFontsDialog ***/ + +GlyphComboBox::GlyphComboBox()= default; + +void GlyphComboBox::update(SPFont* spfont){ + if (!spfont) return; + + this->remove_all(); + + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)){ + this->append((static_cast<SPGlyph*>(&node))->unicode); + } + } +} + +void SvgFontsDialog::on_kerning_value_changed(){ + if (!get_selected_kerning_pair()) { + return; + } + + SPDocument* document = this->getDesktop()->getDocument(); + + //TODO: I am unsure whether this is the correct way of calling SPDocumentUndo::maybe_done + Glib::ustring undokey = "svgfonts:hkern:k:"; + undokey += this->kerning_pair->u1->attribute_string(); + undokey += ":"; + undokey += this->kerning_pair->u2->attribute_string(); + + //slider values increase from right to left so that they match the kerning pair preview + + //XML Tree being directly used here while it shouldn't be. + this->kerning_pair->setAttribute("k", Glib::Ascii::dtostr(get_selected_spfont()->horiz_adv_x - kerning_slider->get_value())); + DocumentUndo::maybeDone(document, undokey.c_str(), SP_VERB_DIALOG_SVG_FONTS, _("Adjust kerning value")); + + //populate_kerning_pairs_box(); + kerning_preview.redraw(); + _font_da.redraw(); +} + +void SvgFontsDialog::glyphs_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _GlyphsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::kerning_pairs_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _KerningPairsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::fonts_list_button_release(GdkEventButton* event) +{ + if((event->type == GDK_BUTTON_RELEASE) && (event->button == 3)) { + _FontsContextMenu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } +} + +void SvgFontsDialog::create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _GlyphsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _GlyphsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _KerningPairsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _KerningPairsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem) +{ + auto mi = Gtk::manage(new Gtk::MenuItem(_("_Remove"), true)); + _FontsContextMenu.append(*mi); + mi->signal_activate().connect(rem); + mi->show(); + _FontsContextMenu.accelerate(parent); +} + +void SvgFontsDialog::update_sensitiveness(){ + if (get_selected_spfont()){ + global_vbox.set_sensitive(true); + glyphs_vbox.set_sensitive(true); + kerning_vbox.set_sensitive(true); + } else { + global_vbox.set_sensitive(false); + glyphs_vbox.set_sensitive(false); + kerning_vbox.set_sensitive(false); + } +} + +/* Add all fonts in the document to the combobox. */ +void SvgFontsDialog::update_fonts() +{ + SPDesktop* desktop = this->getDesktop(); + SPDocument* document = desktop->getDocument(); + std::vector<SPObject *> fonts = document->getResourceList( "font" ); + + _model->clear(); + for (auto font : fonts) { + Gtk::TreeModel::Row row = *_model->append(); + SPFont* f = SP_FONT(font); + row[_columns.spfont] = f; + row[_columns.svgfont] = new SvgFont(f); + const gchar* lbl = f->label(); + const gchar* id = f->getId(); + row[_columns.label] = lbl ? lbl : (id ? id : "font"); + } + + update_sensitiveness(); +} + +void SvgFontsDialog::on_preview_text_changed(){ + _font_da.set_text(_preview_entry.get_text()); +} + +void SvgFontsDialog::on_kerning_pair_selection_changed(){ + SPGlyphKerning* kern = get_selected_kerning_pair(); + if (!kern) { + kerning_preview.set_text(""); + return; + } + Glib::ustring str; + str += kern->u1->sample_glyph(); + str += kern->u2->sample_glyph(); + + kerning_preview.set_text(str); + this->kerning_pair = kern; + + //slider values increase from right to left so that they match the kerning pair preview + kerning_slider->set_value(get_selected_spfont()->horiz_adv_x - kern->k); +} + +void SvgFontsDialog::update_global_settings_tab(){ + SPFont* font = get_selected_spfont(); + if (!font) return; + + _horiz_adv_x_spin->set_value(font->horiz_adv_x); + _horiz_origin_x_spin->set_value(font->horiz_origin_x); + _horiz_origin_y_spin->set_value(font->horiz_origin_y); + + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + _familyname_entry->set_text((SP_FONTFACE(&obj))->font_family); + _units_per_em_spin->set_value((SP_FONTFACE(&obj))->units_per_em); + _ascent_spin->set_value((SP_FONTFACE(&obj))->ascent); + _descent_spin->set_value((SP_FONTFACE(&obj))->descent); + _x_height_spin->set_value((SP_FONTFACE(&obj))->x_height); + _cap_height_spin->set_value((SP_FONTFACE(&obj))->cap_height); + } + } +} + +void SvgFontsDialog::on_font_selection_changed(){ + SPFont* spfont = this->get_selected_spfont(); + if (!spfont) return; + + SvgFont* svgfont = this->get_selected_svgfont(); + first_glyph.update(spfont); + second_glyph.update(spfont); + kerning_preview.set_svgfont(svgfont); + _font_da.set_svgfont(svgfont); + _font_da.redraw(); + + kerning_slider->set_range(0, spfont->horiz_adv_x); + kerning_slider->set_draw_value(false); + kerning_slider->set_value(0); + + update_global_settings_tab(); + populate_glyphs_box(); + populate_kerning_pairs_box(); + update_sensitiveness(); +} + +SPGlyphKerning* SvgFontsDialog::get_selected_kerning_pair() +{ + Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected(); + if(i) + return (*i)[_KerningPairsListColumns.spnode]; + return nullptr; +} + +SvgFont* SvgFontsDialog::get_selected_svgfont() +{ + Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected(); + if(i) + return (*i)[_columns.svgfont]; + return nullptr; +} + +SPFont* SvgFontsDialog::get_selected_spfont() +{ + Gtk::TreeModel::iterator i = _FontsList.get_selection()->get_selected(); + if(i) + return (*i)[_columns.spfont]; + return nullptr; +} + +SPGlyph* SvgFontsDialog::get_selected_glyph() +{ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if(i) + return (*i)[_GlyphsListColumns.glyph_node]; + return nullptr; +} + +Gtk::VBox* SvgFontsDialog::global_settings_tab(){ + _font_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + _horiz_adv_x_spin = new AttrSpin( this, (gchar*) _("Horiz. Advance X"), _("Average amount of horizontal space each letter takes up."), SP_ATTR_HORIZ_ADV_X); + _horiz_origin_x_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin X"), _("Average horizontal origin location for each letter."), SP_ATTR_HORIZ_ORIGIN_X); + _horiz_origin_y_spin = new AttrSpin( this, (gchar*) _("Horiz. Origin Y"), _("Average vertical origin location for each letter."), SP_ATTR_HORIZ_ORIGIN_Y); + _font_face_label = new Gtk::Label(Glib::ustring("<b>") + _("Font Face Attributes") + "</b>", Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + _familyname_entry = new AttrEntry(this, (gchar*) _("Family Name:"), _("Name of the font as it appears in font selectors and css font-family properties."), SP_PROP_FONT_FAMILY); + _units_per_em_spin = new AttrSpin( this, (gchar*) _("Units per em"), _("Number of display units each letter takes up."), SP_ATTR_UNITS_PER_EM); + _ascent_spin = new AttrSpin( this, (gchar*) _("Ascent:"), _("Amount of space taken up by accenders like the tall line on the letter 'h'."), SP_ATTR_ASCENT); + _descent_spin = new AttrSpin( this, (gchar*) _("Descent:"), _("Amount of space taken up by decenders like the tail on the letter 'g'."), SP_ATTR_DESCENT); + _cap_height_spin = new AttrSpin( this, (gchar*) _("Cap Height:"), _("The height of a capital letter above the baseline like the letter 'H' or 'I'."), SP_ATTR_CAP_HEIGHT); + _x_height_spin = new AttrSpin( this, (gchar*) _("x Height:"), _("The height of a lower-case letter above the baseline like the letter 'x'."), SP_ATTR_X_HEIGHT); + + //_descent_spin->set_range(-4096,0); + _font_label->set_use_markup(); + _font_face_label->set_use_markup(); + + global_vbox.set_border_width(2); + global_vbox.pack_start(*_font_label); + global_vbox.pack_start(*_horiz_adv_x_spin); + global_vbox.pack_start(*_horiz_origin_x_spin); + global_vbox.pack_start(*_horiz_origin_y_spin); + global_vbox.pack_start(*_font_face_label); + global_vbox.pack_start(*_familyname_entry); + global_vbox.pack_start(*_units_per_em_spin); + global_vbox.pack_start(*_ascent_spin); + global_vbox.pack_start(*_descent_spin); + global_vbox.pack_start(*_cap_height_spin); + global_vbox.pack_start(*_x_height_spin); + +/* global_vbox->add(*AttrCombo((gchar*) _("Style:"), SP_PROP_FONT_STYLE)); + global_vbox->add(*AttrCombo((gchar*) _("Variant:"), SP_PROP_FONT_VARIANT)); + global_vbox->add(*AttrCombo((gchar*) _("Weight:"), SP_PROP_FONT_WEIGHT)); +*/ + + return &global_vbox; +} + +void +SvgFontsDialog::populate_glyphs_box() +{ + if (!_GlyphsListStore) return; + _GlyphsListStore->clear(); + + SPFont* spfont = this->get_selected_spfont(); + _glyphs_observer.set(spfont); + + for (auto& node: spfont->children) { + if (SP_IS_GLYPH(&node)){ + Gtk::TreeModel::Row row = *(_GlyphsListStore->append()); + row[_GlyphsListColumns.glyph_node] = static_cast<SPGlyph*>(&node); + row[_GlyphsListColumns.glyph_name] = (static_cast<SPGlyph*>(&node))->glyph_name; + row[_GlyphsListColumns.unicode] = (static_cast<SPGlyph*>(&node))->unicode; + row[_GlyphsListColumns.advance] = (static_cast<SPGlyph*>(&node))->horiz_adv_x; + } + } +} + +void +SvgFontsDialog::populate_kerning_pairs_box() +{ + if (!_KerningPairsListStore) return; + _KerningPairsListStore->clear(); + + SPFont* spfont = this->get_selected_spfont(); + + for (auto& node: spfont->children) { + if (SP_IS_HKERN(&node)){ + Gtk::TreeModel::Row row = *(_KerningPairsListStore->append()); + row[_KerningPairsListColumns.first_glyph] = (static_cast<SPGlyphKerning*>(&node))->u1->attribute_string().c_str(); + row[_KerningPairsListColumns.second_glyph] = (static_cast<SPGlyphKerning*>(&node))->u2->attribute_string().c_str(); + row[_KerningPairsListColumns.kerning_value] = (static_cast<SPGlyphKerning*>(&node))->k; + row[_KerningPairsListColumns.spnode] = static_cast<SPGlyphKerning*>(&node); + } + } +} + +SPGlyph *new_glyph(SPDocument* document, SPFont *font, const int count) +{ + g_return_val_if_fail(font != nullptr, NULL); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new glyph + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:glyph"); + + std::ostringstream os; + os << _("glyph") << " " << count; + repr->setAttribute("glyph-name", os.str()); + + // Append the new glyph node to the current font + font->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + SPGlyph *g = SP_GLYPH( document->getObjectByRepr(repr) ); + + g_assert(g != nullptr); + g_assert(SP_IS_GLYPH(g)); + + return g; +} + +void SvgFontsDialog::update_glyphs(){ + SPFont* font = get_selected_spfont(); + if (!font) return; + populate_glyphs_box(); + populate_kerning_pairs_box(); + first_glyph.update(font); + second_glyph.update(font); + get_selected_svgfont()->refresh(); + _font_da.redraw(); +} + +void SvgFontsDialog::add_glyph(){ + const int count = _GlyphsListStore->children().size(); + SPDocument* doc = this->getDesktop()->getDocument(); + /* SPGlyph* glyph =*/ new_glyph(doc, get_selected_spfont(), count+1); + + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add glyph")); + + update_glyphs(); +} + +Geom::PathVector +SvgFontsDialog::flip_coordinate_system(Geom::PathVector pathv){ + double units_per_em = 1024; + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + sp_repr_get_double(obj.getRepr(), "units-per-em", &units_per_em); + } + } + double baseline_offset = units_per_em - get_selected_spfont()->horiz_origin_y; + //This matrix flips y-axis and places the origin at baseline + Geom::Affine m(Geom::Coord(1),Geom::Coord(0),Geom::Coord(0),Geom::Coord(-1),Geom::Coord(0),Geom::Coord(baseline_offset)); + return pathv*m; +} + +void SvgFontsDialog::set_glyph_description_from_selected_path(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + SPDocument* doc = desktop->getDocument(); + Inkscape::Selection* sel = desktop->getSelection(); + if (sel->isEmpty()){ + char *msg = _("Select a <b>path</b> to define the curves of a glyph"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Inkscape::XML::Node* node = sel->xmlNodes().front(); + if (!node) return;//TODO: should this be an assert? + if (!node->matchAttributeName("d") || !node->attribute("d")){ + char *msg = _("The selected object does not have a <b>path</b> description."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } //TODO: //Is there a better way to tell it to to the user? + + SPGlyph* glyph = get_selected_glyph(); + if (!glyph){ + char *msg = _("No glyph selected in the SVGFonts dialog."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d")); + + //XML Tree being directly used here while it shouldn't be. + gchar *str = sp_svg_write_path (flip_coordinate_system(pathv)); + glyph->setAttribute("d", str); + g_free(str); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves")); + + update_glyphs(); +} + +void SvgFontsDialog::missing_glyph_description_from_selected_path(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + Inkscape::MessageStack *msgStack = desktop->getMessageStack(); + SPDocument* doc = desktop->getDocument(); + Inkscape::Selection* sel = desktop->getSelection(); + if (sel->isEmpty()){ + char *msg = _("Select a <b>path</b> to define the curves of a glyph"); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } + + Inkscape::XML::Node* node = sel->xmlNodes().front(); + if (!node) return;//TODO: should this be an assert? + if (!node->matchAttributeName("d") || !node->attribute("d")){ + char *msg = _("The selected object does not have a <b>path</b> description."); + msgStack->flash(Inkscape::ERROR_MESSAGE, msg); + return; + } //TODO: //Is there a better way to tell it to to the user? + + Geom::PathVector pathv = sp_svg_read_pathv(node->attribute("d")); + + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_MISSING_GLYPH(&obj)){ + + //XML Tree being directly used here while it shouldn't be. + gchar *str = sp_svg_write_path (flip_coordinate_system(pathv)); + obj.setAttribute("d", str); + g_free(str); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph curves")); + } + } + + update_glyphs(); +} + +void SvgFontsDialog::reset_missing_glyph_description(){ + SPDesktop* desktop = this->getDesktop(); + if (!desktop) { + g_warning("SvgFontsDialog: No active desktop"); + return; + } + + SPDocument* doc = desktop->getDocument(); + for (auto& obj: get_selected_spfont()->children) { + if (SP_IS_MISSING_GLYPH(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("d", "M0,0h1000v1024h-1000z"); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Reset missing-glyph")); + } + } + + update_glyphs(); +} + +void SvgFontsDialog::glyph_name_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("glyph-name", str); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Edit glyph name")); + + update_glyphs(); +} + +void SvgFontsDialog::glyph_unicode_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + glyph->setAttribute("unicode", str); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph unicode")); + + update_glyphs(); +} + +void SvgFontsDialog::glyph_advance_edit(const Glib::ustring&, const Glib::ustring& str){ + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if (!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + //XML Tree being directly used here while it shouldn't be. + std::istringstream is(str); + double value; + // Check if input valid + if ((is >> value)) { + glyph->setAttribute("horiz-adv-x", str); + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Set glyph advance")); + + update_glyphs(); + } else { + std::cerr << "SvgFontDialog::glyph_advance_edit: Error in input: " << str << std::endl; + } +} + +void SvgFontsDialog::remove_selected_font(){ + SPFont* font = get_selected_spfont(); + if (!font) return; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(font->getRepr()); + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove font")); + + update_fonts(); +} + +void SvgFontsDialog::remove_selected_glyph(){ + if(!_GlyphsList.get_selection()) return; + + Gtk::TreeModel::iterator i = _GlyphsList.get_selection()->get_selected(); + if(!i) return; + + SPGlyph* glyph = (*i)[_GlyphsListColumns.glyph_node]; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(glyph->getRepr()); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove glyph")); + + update_glyphs(); +} + +void SvgFontsDialog::remove_selected_kerning_pair(){ + if(!_KerningPairsList.get_selection()) return; + + Gtk::TreeModel::iterator i = _KerningPairsList.get_selection()->get_selected(); + if(!i) return; + + SPGlyphKerning* pair = (*i)[_KerningPairsListColumns.spnode]; + + //XML Tree being directly used here while it shouldn't be. + sp_repr_unparent(pair->getRepr()); + + SPDocument* doc = this->getDesktop()->getDocument(); + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Remove kerning pair")); + + update_glyphs(); +} + +Gtk::VBox* SvgFontsDialog::glyphs_tab(){ + _GlyphsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::glyphs_list_button_release)); + create_glyphs_popup_menu(_GlyphsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_glyph)); + + Gtk::HBox* missing_glyph_hbox = Gtk::manage(new Gtk::HBox(false, 4)); + Gtk::Label* missing_glyph_label = Gtk::manage(new Gtk::Label(_("Missing Glyph:"))); + missing_glyph_hbox->set_hexpand(false); + missing_glyph_hbox->pack_start(*missing_glyph_label, false,false); + missing_glyph_hbox->pack_start(missing_glyph_button, false,false); + missing_glyph_hbox->pack_start(missing_glyph_reset_button, false,false); + + missing_glyph_button.set_label(_("From selection...")); + missing_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::missing_glyph_description_from_selected_path)); + missing_glyph_reset_button.set_label(_("Reset")); + missing_glyph_reset_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::reset_missing_glyph_description)); + + glyphs_vbox.set_border_width(4); + glyphs_vbox.set_spacing(4); + glyphs_vbox.pack_start(*missing_glyph_hbox, false,false); + + glyphs_vbox.add(_GlyphsListScroller); + _GlyphsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _GlyphsListScroller.set_size_request(-1, 290); + _GlyphsListScroller.add(_GlyphsList); + _GlyphsListStore = Gtk::ListStore::create(_GlyphsListColumns); + _GlyphsList.set_model(_GlyphsListStore); + _GlyphsList.append_column_editable(_("Glyph name"), _GlyphsListColumns.glyph_name); + _GlyphsList.append_column_editable(_("Matching string"), _GlyphsListColumns.unicode); + _GlyphsList.append_column_numeric_editable(_("Advance"), _GlyphsListColumns.advance, "%.2f"); + Gtk::HBox* hb = Gtk::manage(new Gtk::HBox(false, 4)); + add_glyph_button.set_label(_("Add Glyph")); + add_glyph_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_glyph)); + + hb->pack_start(add_glyph_button, false,false); + hb->pack_start(glyph_from_path_button, false,false); + + glyphs_vbox.pack_start(*hb, false, false); + glyph_from_path_button.set_label(_("Get curves from selection...")); + glyph_from_path_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::set_glyph_description_from_selected_path)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(0))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_name_edit)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(1))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_unicode_edit)); + + dynamic_cast<Gtk::CellRendererText*>( _GlyphsList.get_column_cell_renderer(2))->signal_edited().connect( + sigc::mem_fun(*this, &SvgFontsDialog::glyph_advance_edit)); + + _glyphs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::update_glyphs)); + + return &glyphs_vbox; +} + +void SvgFontsDialog::add_kerning_pair(){ + if (first_glyph.get_active_text() == "" || + second_glyph.get_active_text() == "") return; + + //look for this kerning pair on the currently selected font + this->kerning_pair = nullptr; + for (auto& node: get_selected_spfont()->children) { + //TODO: It is not really correct to get only the first byte of each string. + //TODO: We should also support vertical kerning + if (SP_IS_HKERN(&node) && (static_cast<SPGlyphKerning*>(&node))->u1->contains((gchar) first_glyph.get_active_text().c_str()[0]) + && (static_cast<SPGlyphKerning*>(&node))->u2->contains((gchar) second_glyph.get_active_text().c_str()[0]) ){ + this->kerning_pair = static_cast<SPGlyphKerning*>(&node); + continue; + } + } + + if (this->kerning_pair) return; //We already have this kerning pair + + SPDocument* document = this->getDesktop()->getDocument(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new hkern node + Inkscape::XML::Node *repr = xml_doc->createElement("svg:hkern"); + + repr->setAttribute("u1", first_glyph.get_active_text()); + repr->setAttribute("u2", second_glyph.get_active_text()); + repr->setAttribute("k", "0"); + + // Append the new hkern node to the current font + get_selected_spfont()->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // get corresponding object + this->kerning_pair = SP_HKERN( document->getObjectByRepr(repr) ); + + DocumentUndo::done(document, SP_VERB_DIALOG_SVG_FONTS, _("Add kerning pair")); +} + +Gtk::VBox* SvgFontsDialog::kerning_tab(){ + _KerningPairsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::kerning_pairs_list_button_release)); + create_kerning_pairs_popup_menu(_KerningPairsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_kerning_pair)); + +//Kerning Setup: + kerning_vbox.set_border_width(4); + kerning_vbox.set_spacing(4); + // kerning_vbox.add(*Gtk::manage(new Gtk::Label(_("Kerning Setup")))); + Gtk::HBox* kerning_selector = Gtk::manage(new Gtk::HBox()); + kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("1st Glyph:"))), false, false); + kerning_selector->pack_start(first_glyph, true, true, 4); + kerning_selector->pack_start(*Gtk::manage(new Gtk::Label(_("2nd Glyph:"))), false, false); + kerning_selector->pack_start(second_glyph, true, true, 4); + kerning_selector->pack_start(add_kernpair_button, true, true); + add_kernpair_button.set_label(_("Add pair")); + add_kernpair_button.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_kerning_pair)); + _KerningPairsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_pair_selection_changed)); + kerning_slider->signal_value_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_kerning_value_changed)); + + kerning_vbox.pack_start(*kerning_selector, false,false); + + kerning_vbox.pack_start(_KerningPairsListScroller, true,true); + _KerningPairsListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _KerningPairsListScroller.add(_KerningPairsList); + _KerningPairsListStore = Gtk::ListStore::create(_KerningPairsListColumns); + _KerningPairsList.set_model(_KerningPairsListStore); + _KerningPairsList.append_column(_("First Unicode range"), _KerningPairsListColumns.first_glyph); + _KerningPairsList.append_column(_("Second Unicode range"), _KerningPairsListColumns.second_glyph); +// _KerningPairsList.append_column_numeric_editable(_("Kerning Value"), _KerningPairsListColumns.kerning_value, "%f"); + + kerning_vbox.pack_start((Gtk::Widget&) kerning_preview, false,false); + + // kerning_slider has a big handle. Extra padding added + Gtk::HBox* kerning_amount_hbox = Gtk::manage(new Gtk::HBox(false, 8)); + kerning_vbox.pack_start(*kerning_amount_hbox, false,false); + kerning_amount_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Kerning Value:"))), false,false); + kerning_amount_hbox->pack_start(*kerning_slider, true,true); + + kerning_preview.set_size(300 + 20, 150 + 20); + _font_da.set_size(300 + 50 + 20, 60 + 20); + + return &kerning_vbox; +} + +SPFont *new_font(SPDocument *document) +{ + g_return_val_if_fail(document != nullptr, NULL); + + SPDefs *defs = document->getDefs(); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + // create a new font + Inkscape::XML::Node *repr = xml_doc->createElement("svg:font"); + + //By default, set the horizontal advance to 1024 units + repr->setAttribute("horiz-adv-x", "1024"); + + // Append the new font node to defs + defs->getRepr()->appendChild(repr); + + //create a missing glyph + Inkscape::XML::Node *fontface; + fontface = xml_doc->createElement("svg:font-face"); + fontface->setAttribute("units-per-em", "1024"); + repr->appendChild(fontface); + + //create a missing glyph + Inkscape::XML::Node *mg; + mg = xml_doc->createElement("svg:missing-glyph"); + mg->setAttribute("d", "M0,0h1000v1024h-1000z"); + repr->appendChild(mg); + + // get corresponding object + SPFont *f = SP_FONT( document->getObjectByRepr(repr) ); + + g_assert(f != nullptr); + g_assert(SP_IS_FONT(f)); + Inkscape::GC::release(mg); + Inkscape::GC::release(repr); + return f; +} + +void set_font_family(SPFont* font, char* str){ + if (!font) return; + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("font-family", str); + } + } + + DocumentUndo::done(font->document, SP_VERB_DIALOG_SVG_FONTS, _("Set font family")); +} + +void SvgFontsDialog::add_font(){ + SPDocument* doc = this->getDesktop()->getDocument(); + SPFont* font = new_font(doc); + + const int count = _model->children().size(); + std::ostringstream os, os2; + os << _("font") << " " << count; + font->setLabel(os.str().c_str()); + + os2 << "SVGFont " << count; + for (auto& obj: font->children) { + if (SP_IS_FONTFACE(&obj)){ + //XML Tree being directly used here while it shouldn't be. + obj.setAttribute("font-family", os2.str()); + } + } + + update_fonts(); +// select_font(font); + + DocumentUndo::done(doc, SP_VERB_DIALOG_SVG_FONTS, _("Add font")); +} + +SvgFontsDialog::SvgFontsDialog() + : UI::Widget::Panel("/dialogs/svgfonts", SP_VERB_DIALOG_SVG_FONTS), + _add(_("_New"), true) +{ + kerning_slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + _add.signal_clicked().connect(sigc::mem_fun(*this, &SvgFontsDialog::add_font)); + + Gtk::HBox* hbox = Gtk::manage(new Gtk::HBox()); + Gtk::VBox* vbox = Gtk::manage(new Gtk::VBox()); + + vbox->pack_start(_FontsList); + vbox->pack_start(_add, false, false); + hbox->add(*vbox); + hbox->add(_font_settings); + _getContents()->add(*hbox); + +//List of SVGFonts declared in a document: + _model = Gtk::ListStore::create(_columns); + _FontsList.set_model(_model); + _FontsList.append_column_editable(_("_Fonts"), _columns.label); + _FontsList.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_font_selection_changed)); + + this->update_fonts(); + + Gtk::Notebook *tabs = Gtk::manage(new Gtk::Notebook()); + tabs->set_scrollable(); + + tabs->append_page(*global_settings_tab(), _("_Global Settings"), true); + tabs->append_page(*glyphs_tab(), _("_Glyphs"), true); + tabs->append_page(*kerning_tab(), _("_Kerning"), true); + + _font_settings.add(*tabs); + +//Text Preview: + _preview_entry.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::on_preview_text_changed)); + _getContents()->pack_start((Gtk::Widget&) _font_da, false, false); + _preview_entry.set_text(_("Sample Text")); + _font_da.set_text(_("Sample Text")); + + Gtk::HBox* preview_entry_hbox = Gtk::manage(new Gtk::HBox(false, 4)); + _getContents()->pack_start(*preview_entry_hbox, false, false); // Non-latin characters may need more height. + preview_entry_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Preview Text:"))), false, false); + preview_entry_hbox->pack_start(_preview_entry, true, true); + + _FontsList.signal_button_release_event().connect_notify(sigc::mem_fun(*this, &SvgFontsDialog::fonts_list_button_release)); + create_fonts_popup_menu(_FontsList, sigc::mem_fun(*this, &SvgFontsDialog::remove_selected_font)); + + _defs_observer.set(this->getDesktop()->getDocument()->getDefs()); + _defs_observer.signal_changed().connect(sigc::mem_fun(*this, &SvgFontsDialog::update_fonts)); + + _getContents()->show_all(); +} + +SvgFontsDialog::~SvgFontsDialog()= default; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/svg-fonts-dialog.h b/src/ui/dialog/svg-fonts-dialog.h new file mode 100644 index 0000000..ae07d7f --- /dev/null +++ b/src/ui/dialog/svg-fonts-dialog.h @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief SVG Fonts dialog + */ +/* Authors: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * + * Copyright (C) 2008 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_H +#define INKSCAPE_UI_DIALOG_SVG_FONTS_H + +#include "ui/widget/panel.h" +#include <2geom/pathvector.h> +#include "ui/widget/spinbutton.h" + +#include <gtkmm/box.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/entry.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> + +#include "attributes.h" +#include "xml/helper-observer.h" + +namespace Gtk { +class Scale; +} + +class SPGlyph; +class SPGlyphKerning; +class SvgFont; + +class SvgFontDrawingArea : Gtk::DrawingArea{ +public: + SvgFontDrawingArea(); + void set_text(Glib::ustring); + void set_svgfont(SvgFont*); + void set_size(int x, int y); + void redraw(); +private: + int _x,_y; + SvgFont* _svgfont; + Glib::ustring _text; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; +}; + +class SPFont; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class GlyphComboBox : public Gtk::ComboBoxText { +public: + GlyphComboBox(); + void update(SPFont*); +}; + +class SvgFontsDialog : public UI::Widget::Panel { +public: + SvgFontsDialog(); + ~SvgFontsDialog() override; + + static SvgFontsDialog &getInstance() { return *new SvgFontsDialog(); } + + void update_fonts(); + SvgFont* get_selected_svgfont(); + SPFont* get_selected_spfont(); + SPGlyph* get_selected_glyph(); + SPGlyphKerning* get_selected_kerning_pair(); + + //TODO: these methods should be private, right?! + void on_font_selection_changed(); + void on_kerning_pair_selection_changed(); + void on_preview_text_changed(); + void on_kerning_pair_changed(); + void on_kerning_value_changed(); + void on_setfontdata_changed(); + void add_font(); + Geom::PathVector flip_coordinate_system(Geom::PathVector pathv); + bool updating; + + // Used for font-family + class AttrEntry : public Gtk::HBox + { + public: + AttrEntry(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr); + void set_text(char*); + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Gtk::Entry entry; + SPAttributeEnum attr; + }; + + class AttrSpin : public Gtk::HBox + { + public: + AttrSpin(SvgFontsDialog* d, gchar* lbl, Glib::ustring tooltip, const SPAttributeEnum attr); + void set_value(double v); + void set_range(double low, double high); + Inkscape::UI::Widget::SpinButton* getSpin() { return &spin; } + private: + SvgFontsDialog* dialog; + void on_attr_changed(); + Inkscape::UI::Widget::SpinButton spin; + SPAttributeEnum attr; + }; + +private: + void update_glyphs(); + void update_sensitiveness(); + void update_global_settings_tab(); + void populate_glyphs_box(); + void populate_kerning_pairs_box(); + void set_glyph_description_from_selected_path(); + void missing_glyph_description_from_selected_path(); + void reset_missing_glyph_description(); + void add_glyph(); + void glyph_unicode_edit(const Glib::ustring&, const Glib::ustring&); + void glyph_name_edit( const Glib::ustring&, const Glib::ustring&); + void glyph_advance_edit(const Glib::ustring&, const Glib::ustring&); + void remove_selected_glyph(); + void remove_selected_font(); + void remove_selected_kerning_pair(); + + void add_kerning_pair(); + + void create_glyphs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void glyphs_list_button_release(GdkEventButton* event); + + void create_fonts_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void fonts_list_button_release(GdkEventButton* event); + + void create_kerning_pairs_popup_menu(Gtk::Widget& parent, sigc::slot<void> rem); + void kerning_pairs_list_button_release(GdkEventButton* event); + + Inkscape::XML::SignalObserver _defs_observer; //in order to update fonts + Inkscape::XML::SignalObserver _glyphs_observer; + + Gtk::HBox* AttrCombo(gchar* lbl, const SPAttributeEnum attr); +// Gtk::HBox* AttrSpin(gchar* lbl, const SPAttributeEnum attr); + Gtk::VBox* global_settings_tab(); + + // <font> + Gtk::Label* _font_label; + AttrSpin* _horiz_adv_x_spin; + AttrSpin* _horiz_origin_x_spin; + AttrSpin* _horiz_origin_y_spin; + + // <font-face> + Gtk::Label* _font_face_label; + AttrEntry* _familyname_entry; + AttrSpin* _units_per_em_spin; + AttrSpin* _ascent_spin; + AttrSpin* _descent_spin; + AttrSpin* _cap_height_spin; + AttrSpin* _x_height_spin; + + Gtk::VBox* kerning_tab(); + Gtk::VBox* glyphs_tab(); + Gtk::Button _add; + Gtk::Button add_glyph_button; + Gtk::Button glyph_from_path_button; + Gtk::Button missing_glyph_button; + Gtk::Button missing_glyph_reset_button; + + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(spfont); + add(svgfont); + add(label); + } + + Gtk::TreeModelColumn<SPFont*> spfont; + Gtk::TreeModelColumn<SvgFont*> svgfont; + Gtk::TreeModelColumn<Glib::ustring> label; + }; + Glib::RefPtr<Gtk::ListStore> _model; + Columns _columns; + Gtk::TreeView _FontsList; + + class GlyphsColumns : public Gtk::TreeModel::ColumnRecord + { + public: + GlyphsColumns() + { + add(glyph_node); + add(glyph_name); + add(unicode); + add(advance); + } + + Gtk::TreeModelColumn<SPGlyph*> glyph_node; + Gtk::TreeModelColumn<Glib::ustring> glyph_name; + Gtk::TreeModelColumn<Glib::ustring> unicode; + Gtk::TreeModelColumn<double> advance; + }; + GlyphsColumns _GlyphsListColumns; + Glib::RefPtr<Gtk::ListStore> _GlyphsListStore; + Gtk::TreeView _GlyphsList; + Gtk::ScrolledWindow _GlyphsListScroller; + + class KerningPairColumns : public Gtk::TreeModel::ColumnRecord + { + public: + KerningPairColumns() + { + add(first_glyph); + add(second_glyph); + add(kerning_value); + add(spnode); + } + + Gtk::TreeModelColumn<Glib::ustring> first_glyph; + Gtk::TreeModelColumn<Glib::ustring> second_glyph; + Gtk::TreeModelColumn<double> kerning_value; + Gtk::TreeModelColumn<SPGlyphKerning*> spnode; + }; + KerningPairColumns _KerningPairsListColumns; + Glib::RefPtr<Gtk::ListStore> _KerningPairsListStore; + Gtk::TreeView _KerningPairsList; + Gtk::ScrolledWindow _KerningPairsListScroller; + Gtk::Button add_kernpair_button; + + Gtk::VBox _font_settings; + Gtk::VBox global_vbox; + Gtk::VBox glyphs_vbox; + Gtk::VBox kerning_vbox; + Gtk::Entry _preview_entry; + + Gtk::Menu _FontsContextMenu; + Gtk::Menu _GlyphsContextMenu; + Gtk::Menu _KerningPairsContextMenu; + + SvgFontDrawingArea _font_da, kerning_preview; + GlyphComboBox first_glyph, second_glyph; + SPGlyphKerning* kerning_pair; + Inkscape::UI::Widget::SpinButton setwidth_spin; + Gtk::Scale* kerning_slider; + + class EntryWidget : public Gtk::HBox + { + public: + EntryWidget() + { + this->add(this->_label); + this->add(this->_entry); + } + void set_label(const gchar* l){ + this->_label.set_text(l); + } + private: + Gtk::Label _label; + Gtk::Entry _entry; + }; + EntryWidget _font_family, _font_variant; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //#ifndef INKSCAPE_UI_DIALOG_SVG_FONTS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/svg-preview.cpp b/src/ui/dialog/svg-preview.cpp new file mode 100644 index 0000000..0e83ab8 --- /dev/null +++ b/src/ui/dialog/svg-preview.cpp @@ -0,0 +1,476 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Implementation of the file dialog interfaces defined in filedialogimpl.h. + */ +/* Authors: + * Bob Jamison + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * + * Copyright (C) 2004-2007 Bob Jamison + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2007-2008 Joel Holdsworth + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> +#include <fstream> + +#include <glibmm/i18n.h> +#include <glib/gstdio.h> // GStatBuf + +#include "svg-preview.h" + +#include "document.h" +#include "ui/view/svg-view-widget.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/*######################################################################### +### SVG Preview Widget +#########################################################################*/ + +bool SVGPreview::setDocument(SPDocument *doc) +{ + if (viewer) { + viewer->setDocument(doc); + } else { + viewer = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + pack_start(*viewer, true, true); + } + + if (document) { + delete document; + } + document = doc; + + show_all(); + + return true; +} + + +bool SVGPreview::setFileName(Glib::ustring &theFileName) +{ + Glib::ustring fileName = theFileName; + + fileName = Glib::filename_to_utf8(fileName); + + /** + * I don't know why passing false to keepalive is bad. But it + * prevents the display of an svg with a non-ascii filename + */ + SPDocument *doc = SPDocument::createNewDoc(fileName.c_str(), true); + if (!doc) { + g_warning("SVGView: error loading document '%s'\n", fileName.c_str()); + return false; + } + + setDocument(doc); + + return true; +} + + + +bool SVGPreview::setFromMem(char const *xmlBuffer) +{ + if (!xmlBuffer) + return false; + + gint len = (gint)strlen(xmlBuffer); + SPDocument *doc = SPDocument::createNewDocFromMem(xmlBuffer, len, false); + if (!doc) { + g_warning("SVGView: error loading buffer '%s'\n", xmlBuffer); + return false; + } + + setDocument(doc); + + return true; +} + + + +void SVGPreview::showImage(Glib::ustring &theFileName) +{ + Glib::ustring fileName = theFileName; + + // Let's get real width and height from SVG file. These are template + // files so we assume they are well formed. + + // std::cout << "SVGPreview::showImage: " << theFileName << std::endl; + std::string width; + std::string height; + + /*##################################### + # LET'S HAVE SOME FUN WITH SVG! + # Instead of just loading an image, why + # don't we make a lovely little svg and + # display it nicely? + #####################################*/ + + // Arbitrary size of svg doc -- rather 'portrait' shaped + gint previewWidth = 400; + gint previewHeight = 600; + + // Get some image info. Smart pointer does not need to be deleted + Glib::RefPtr<Gdk::Pixbuf> img(nullptr); + try + { + img = Gdk::Pixbuf::create_from_file(fileName); + } + catch (const Glib::FileError &e) + { + g_message("caught Glib::FileError in SVGPreview::showImage"); + return; + } + catch (const Gdk::PixbufError &e) + { + g_message("Gdk::PixbufError in SVGPreview::showImage"); + return; + } + catch (...) + { + g_message("Caught ... in SVGPreview::showImage"); + return; + } + + gint imgWidth = img->get_width(); + gint imgHeight = img->get_height(); + + Glib::ustring svg = ".svg"; + if (hasSuffix(fileName, svg)) { + std::ifstream input(theFileName.c_str()); + if( !input ) { + std::cerr << "SVGPreview::showImage: Failed to open file: " << theFileName << std::endl; + } else { + + std::string token; + + Glib::MatchInfo match_info; + Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("width=\"(.*)\""); + Glib::RefPtr<Glib::Regex> regex2 = Glib::Regex::create("height=\"(.*)\""); + + while( !input.eof() && (height.empty() || width.empty()) ) { + + input >> token; + // std::cout << "|" << token << "|" << std::endl; + + if (regex1->match(token, match_info)) { + width = match_info.fetch(1).raw(); + } + + if (regex2->match(token, match_info)) { + height = match_info.fetch(1).raw(); + } + + } + } + } + + // TODO: replace int to string conversion with std::to_string when fully C++11 compliant + if (height.empty() || width.empty()) { + std::ostringstream s_width; + std::ostringstream s_height; + s_width << imgWidth; + s_height << imgHeight; + width = s_width.str(); + height = s_height.str(); + } + + // Find the minimum scale to fit the image inside the preview area + double scaleFactorX = (0.9 * (double)previewWidth) / ((double)imgWidth); + double scaleFactorY = (0.9 * (double)previewHeight) / ((double)imgHeight); + double scaleFactor = scaleFactorX; + if (scaleFactorX > scaleFactorY) + scaleFactor = scaleFactorY; + + // Now get the resized values + gint scaledImgWidth = (int)(scaleFactor * (double)imgWidth); + gint scaledImgHeight = (int)(scaleFactor * (double)imgHeight); + + // center the image on the area + gint imgX = (previewWidth - scaledImgWidth) / 2; + gint imgY = (previewHeight - scaledImgHeight) / 2; + + // wrap a rectangle around the image + gint rectX = imgX - 1; + gint rectY = imgY - 1; + gint rectWidth = scaledImgWidth + 2; + gint rectHeight = scaledImgHeight + 2; + + // Our template. Modify to taste + gchar const *xformat = R"A( +<svg width="%d" height="%d" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect width="100%" height="100%" style="fill:#eeeeee"/> + <image x="%d" y="%d" width="%d" height="%d" xlink:href="%s"/> + <rect x="%d" y="%d" width="%d" height="%d" style="fill:none;stroke:black"/> + <text x="50%" y="55%" style="font-family:sans-serif;font-size:24px;text-anchor:middle">%s x %s</text> +</svg> +)A"; + + // if (!Glib::get_charset()) //If we are not utf8 + fileName = Glib::filename_to_utf8(fileName); + // Filenames in xlinks are decoded, so any % will break without this. + auto encodedName = Glib::uri_escape_string(fileName); + + // Fill in the template + /* FIXME: Do proper XML quoting for fileName. */ + gchar *xmlBuffer = + g_strdup_printf(xformat, previewWidth, previewHeight, imgX, imgY, scaledImgWidth, scaledImgHeight, + encodedName.c_str(), rectX, rectY, rectWidth, rectHeight, width.c_str(), height.c_str() ); + + // g_message("%s\n", xmlBuffer); + + // now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); +} + + + +void SVGPreview::showNoPreview() +{ + // Are we already showing it? + if (showingNoPreview) + return; + + // Our template. Modify to taste + gchar const *xformat = R"B( +<svg width="400" height="600" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g transform="translate(-160,90)" style="opacity:0.10"> + <path style="fill:white" + d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225 + 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/> + <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802 + C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478 + 356.11361 467.54478 342.93607 476.95792 339.17168 z" + id="droplet01" /> + <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338 + C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298 + 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477 + 287.18046 343.1206 286.46194 340.42914 z" + id= "droplet02"/> + <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634 + C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141 + 308.81083 505.53237 308.13423 510.35756 306.92856 z" + id="droplet03"/> + <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223 + L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577 + L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279 + C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865 + C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263 + 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346 + 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439 + 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302 + 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397 + 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739 + 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708 + 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567 + 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716 + 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428 + 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378 + 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915 + 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062 + 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111 + 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538 + 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215 + 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949 + 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118 + 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919 + 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685 + 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608 + 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299 + 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346 + 274.45012 297.36372 269.98057 L 290.92738 261.61333 z" + id="mountainDroplet"/> + </g> + <text xml:space="preserve" x="200" y="320" + style="font-size:32px;font-weight:bold;text-anchor:middle">%s</text> +</svg> +)B"; + + // Fill in the template + gchar *xmlBuffer = g_strdup_printf(xformat, _("No preview")); + + // g_message("%s\n", xmlBuffer); + + // Now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); + showingNoPreview = true; +} + + +/** + * Inform the user that the svg file is too large to be displayed. + * This does not check for sizes of embedded images (yet) + */ +void SVGPreview::showTooLarge(long fileLength) +{ + // Our template. Modify to taste + gchar const *xformat = R"C( +<svg width="400" height="600" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g transform="translate(-160,90)" style="opacity:0.10"> + <path style="fill:white" + d="M 397.64309 320.25301 L 280.39197 282.517 L 250.74227 124.83447 L 345.08225 + 29.146783 L 393.59996 46.667064 L 483.89679 135.61619 L 397.64309 320.25301 z"/> + <path d="M 476.95792 339.17168 C 495.78197 342.93607 499.54842 356.11361 495.78197 359.87802 + C 492.01856 363.6434 482.6065 367.40781 475.07663 361.76014 C 467.54478 + 356.11361 467.54478 342.93607 476.95792 339.17168 z" + id="droplet01" /> + <path d="M 286.46194 340.42914 C 284.6277 340.91835 269.30405 327.71337 257.16909 333.8338 + C 245.03722 339.95336 236.89276 353.65666 248.22676 359.27982 C 259.56184 364.90298 + 267.66433 358.41867 277.60113 351.44119 C 287.53903 344.46477 + 287.18046 343.1206 286.46194 340.42914 z" + id= "droplet02"/> + <path d="M 510.35756 306.92856 C 520.59494 304.36879 544.24333 306.92856 540.47688 321.98634 + C 536.71354 337.04806 504.71297 331.39827 484.00371 323.87156 C 482.12141 + 308.81083 505.53237 308.13423 510.35756 306.92856 z" + id="droplet03"/> + <path d="M 359.2403 21.362537 C 347.92693 21.362537 336.6347 25.683095 327.96556 34.35223 + L 173.87387 188.41466 C 165.37697 196.9114 161.1116 207.95813 160.94269 219.04577 + L 160.88418 219.04577 C 160.88418 219.08524 160.94076 219.12322 160.94269 219.16279 + C 160.94033 219.34888 160.88418 219.53256 160.88418 219.71865 L 161.14748 219.71865 + C 164.0966 230.93917 240.29699 245.24198 248.79866 253.74346 C 261.63771 266.58263 + 199.5652 276.01151 212.4041 288.85074 C 225.24316 301.68979 289.99433 313.6933 302.8346 + 326.53254 C 315.67368 339.37161 276.5961 353.04289 289.43532 365.88196 C 302.27439 + 378.72118 345.40201 362.67257 337.5908 396.16198 C 354.92909 413.50026 391.10302 + 405.2208 415.32417 387.88252 C 428.16323 375.04345 390.6948 376.17577 403.53397 + 363.33668 C 416.37304 350.49745 448.78128 350.4282 476.08902 319.71589 C 465.09739 + 302.62116 429.10801 295.34136 441.94719 282.50217 C 454.78625 269.66311 479.74708 + 276.18423 533.60644 251.72479 C 559.89837 239.78398 557.72636 230.71459 557.62567 + 219.71865 C 557.62356 219.48727 557.62567 219.27892 557.62567 219.04577 L 557.56716 + 219.04577 C 557.3983 207.95812 553.10345 196.9114 544.60673 188.41466 L 390.54428 + 34.35223 C 381.87515 25.683095 370.55366 21.362537 359.2403 21.362537 z M 357.92378 + 41.402939 C 362.95327 41.533963 367.01541 45.368018 374.98006 50.530832 L 447.76915 + 104.50827 C 448.56596 105.02498 449.32484 105.564 450.02187 106.11735 C 450.7189 106.67062 + 451.3556 107.25745 451.95277 107.84347 C 452.54997 108.42842 453.09281 109.01553 453.59111 + 109.62808 C 454.08837 110.24052 454.53956 110.86661 454.93688 111.50048 C 455.33532 112.13538 + 455.69164 112.78029 455.9901 113.43137 C 456.28877 114.08363 456.52291 114.75639 456.7215 + 115.42078 C 456.92126 116.08419 457.08982 116.73973 457.18961 117.41019 C 457.28949 + 118.08184 457.33588 118.75535 457.33588 119.42886 L 414.21245 98.598549 L 409.9118 + 131.16055 L 386.18512 120.04324 L 349.55654 144.50131 L 335.54288 96.1703 L 317.4919 + 138.4453 L 267.08369 143.47735 L 267.63956 121.03795 C 267.63956 115.64823 296.69685 + 77.915899 314.39075 68.932902 L 346.77721 45.674327 C 351.55594 42.576634 354.90608 + 41.324327 357.92378 41.402939 z M 290.92738 261.61333 C 313.87149 267.56365 339.40299 + 275.37038 359.88393 275.50997 L 360.76161 284.72563 C 343.2235 282.91785 306.11346 + 274.45012 297.36372 269.98057 L 290.92738 261.61333 z" + id="mountainDroplet"/> + </g> + <text xml:space="preserve" x="200" y="280" + style="font-size:20px;font-weight:bold;text-anchor:middle">%.1f MB</text> + <text xml:space="preserve" x="200" y="360" + style="font-size:20px;font-weight:bold;text-anchor:middle">%s</text> +</svg> +)C"; + + + // Fill in the template + double floatFileLength = ((double)fileLength) / 1048576.0; + // printf("%ld %f\n", fileLength, floatFileLength); + + gchar *xmlBuffer = + g_strdup_printf(xformat, floatFileLength, _("Too large for preview")); + + // g_message("%s\n", xmlBuffer); + + // now show it! + setFromMem(xmlBuffer); + g_free(xmlBuffer); +} + +bool SVGPreview::set(Glib::ustring &fileName, int dialogType) +{ + + if (!Glib::file_test(fileName, Glib::FILE_TEST_EXISTS)) { + showNoPreview(); + return false; + } + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_DIR)) { + showNoPreview(); + return false; + } + + if (Glib::file_test(fileName, Glib::FILE_TEST_IS_REGULAR)) { + Glib::ustring fileNameUtf8 = Glib::filename_to_utf8(fileName); + gchar *fName = const_cast<gchar *>( + fileNameUtf8.c_str()); // const-cast probably not necessary? (not necessary on Windows version of stat()) + GStatBuf info; + if (g_stat(fName, &info)) // stat returns 0 upon success + { + g_warning("SVGPreview::set() : %s : %s", fName, strerror(errno)); + return false; + } + if (info.st_size > 0xA00000L) { + showingNoPreview = false; + showTooLarge(info.st_size); + return false; + } + } + + Glib::ustring svg = ".svg"; + Glib::ustring svgz = ".svgz"; + + if ((dialogType == SVG_TYPES || dialogType == IMPORT_TYPES) && + (hasSuffix(fileName, svg) || hasSuffix(fileName, svgz))) { + bool retval = setFileName(fileName); + showingNoPreview = false; + return retval; + } else if (isValidImageFile(fileName)) { + showImage(fileName); + showingNoPreview = false; + return true; + } else { + showNoPreview(); + return false; + } +} + + +SVGPreview::SVGPreview() + : document(nullptr) + , viewer(nullptr) + , showingNoPreview(false) +{ + set_size_request(200, 300); +} + +SVGPreview::~SVGPreview() +{ + if (viewer) { + viewer->setDocument(nullptr); + } + delete document; +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/dialog/svg-preview.h b/src/ui/dialog/svg-preview.h new file mode 100644 index 0000000..8263e15 --- /dev/null +++ b/src/ui/dialog/svg-preview.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Implementation of the file dialog interfaces defined in filedialogimpl.h + */ +/* Authors: + * Bob Jamison + * Johan Engelen <johan@shouraizou.nl> + * Joel Holdsworth + * Bruno Dilly + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004-2008 Authors + * Copyright (C) 2004-2007 The Inkscape Organization + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __SVG_PREVIEW_H__ +#define __SVG_PREVIEW_H__ + +//Gtk includes +#include <gtkmm.h> + +//General includes +#include <unistd.h> +#include <sys/stat.h> +#include <cerrno> + +#include "filedialog.h" + + +namespace Gtk { +class Expander; +} + +namespace Inkscape { + class URI; + +namespace UI { + +namespace View { + class SVGViewWidget; +} + +namespace Dialog { + +/*######################################################################### +### SVG Preview Widget +#########################################################################*/ + +/** + * Simple class for displaying an SVG file in the "preview widget." + * Currently, this is just a wrapper of the sp_svg_view Gtk widget. + * Hopefully we will eventually replace with a pure Gtkmm widget. + */ +class SVGPreview : public Gtk::VBox +{ +public: + + SVGPreview(); + + ~SVGPreview() override; + + bool setDocument(SPDocument *doc); + + bool setFileName(Glib::ustring &fileName); + + bool setFromMem(char const *xmlBuffer); + + bool set(Glib::ustring &fileName, int dialogType); + + bool setURI(URI &uri); + + /** + * Show image embedded in SVG + */ + void showImage(Glib::ustring &fileName); + + /** + * Show the "No preview" image + */ + void showNoPreview(); + + /** + * Show the "Too large" image + */ + void showTooLarge(long fileLength); + +private: + /** + * The svg document we are currently showing + */ + SPDocument *document; + + /** + * The sp_svg_view widget + */ + Inkscape::UI::View::SVGViewWidget *viewer; + + /** + * are we currently showing the "no preview" image? + */ + bool showingNoPreview; + +}; + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif /*__SVG_PREVIEW_H__*/ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/swatches.cpp b/src/ui/dialog/swatches.cpp new file mode 100644 index 0000000..c79b660 --- /dev/null +++ b/src/ui/dialog/swatches.cpp @@ -0,0 +1,1418 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Jon A. Cruz + * John Bintz + * Abhishek Sharma + * + * Copyright (C) 2005 Jon A. Cruz + * Copyright (C) 2008 John Bintz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include <algorithm> +#include <iomanip> +#include <set> + +#include "swatches.h" +#include <gtkmm/radiomenuitem.h> + +#include <gtkmm/menu.h> +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/separatormenuitem.h> +#include <gtkmm/menubutton.h> + +#include <glibmm/i18n.h> +#include <glibmm/main.h> +#include <glibmm/timer.h> +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> + +#include "color-item.h" +#include "desktop.h" + +#include "desktop-style.h" +#include "document.h" +#include "document-undo.h" +#include "extension/db.h" +#include "inkscape.h" +#include "io/sys.h" +#include "io/resource.h" +#include "message-context.h" +#include "path-prefix.h" + +#include "ui/previewholder.h" +#include "widgets/desktop-widget.h" +#include "widgets/gradient-vector.h" +#include "display/cairo-utils.h" + +#include "object/sp-defs.h" +#include "object/sp-gradient-reference.h" + +#include "dialog-manager.h" +#include "verbs.h" +#include "gradient-chemistry.h" +#include "helper/action.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +enum { + SWATCHES_SETTINGS_SIZE = 0, + SWATCHES_SETTINGS_MODE = 1, + SWATCHES_SETTINGS_SHAPE = 2, + SWATCHES_SETTINGS_WRAP = 3, + SWATCHES_SETTINGS_BORDER = 4, + SWATCHES_SETTINGS_PALETTE = 5 +}; + +#define VBLOCK 16 +#define PREVIEW_PIXBUF_WIDTH 128 + +void _loadPaletteFile( gchar const *filename, gboolean user=FALSE ); + +std::list<SwatchPage*> userSwatchPages; +std::list<SwatchPage*> systemSwatchPages; +static std::map<SPDocument*, SwatchPage*> docPalettes; +static std::vector<DocTrack*> docTrackings; +static std::map<SwatchesPanel*, SPDocument*> docPerPanel; + + +class SwatchesPanelHook : public SwatchesPanel +{ +public: + static void convertGradient( GtkMenuItem *menuitem, gpointer userData ); + static void deleteGradient( GtkMenuItem *menuitem, gpointer userData ); +}; + +static void handleClick( GtkWidget* /*widget*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(false); + } +} + +static void handleSecondaryClick( GtkWidget* /*widget*/, gint /*arg1*/, gpointer callback_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(callback_data); + if ( item ) { + item->buttonClicked(true); + } +} + +static GtkWidget* popupMenu = nullptr; +static GtkWidget *popupSubHolder = nullptr; +static GtkWidget *popupSub = nullptr; +static std::vector<Glib::ustring> popupItems; +static std::vector<GtkWidget*> popupExtras; +static ColorItem* bounceTarget = nullptr; +static SwatchesPanel* bouncePanel = nullptr; + +static void redirClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + handleClick( GTK_WIDGET(menuitem), bounceTarget ); + } +} + +static void redirSecondaryClick( GtkMenuItem *menuitem, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + handleSecondaryClick( GTK_WIDGET(menuitem), 0, bounceTarget ); + } +} + +static void editGradientImpl( SPDesktop* desktop, SPGradient* gr ) +{ + if ( gr ) { + bool shown = false; + if ( desktop && desktop->doc() ) { + Inkscape::Selection *selection = desktop->getSelection(); + std::vector<SPItem*> const items(selection->items().begin(), selection->items().end()); + if (!items.empty()) { + SPStyle query( desktop->doc() ); + int result = objects_query_fillstroke((items), &query, true); + if ( (result == QUERY_STYLE_MULTIPLE_SAME) || (result == QUERY_STYLE_SINGLE) ) { + // could be pertinent + if (query.fill.isPaintserver()) { + SPPaintServer* server = query.getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* grad = SP_GRADIENT(server); + if ( grad->isSwatch() && grad->getId() == gr->getId()) { + desktop->_dlg_mgr->showDialog("FillAndStroke"); + shown = true; + } + } + } + } + } + } + + if (!shown) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) { + // Legacy gradient dialog + GtkWidget *dialog = sp_gradient_vector_editor_new( gr ); + gtk_widget_show( dialog ); + } else { + // Invoke the gradient tool + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT ); + if ( verb ) { + SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) ); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } + } + } +} + +static void editGradient( GtkMenuItem */*menuitem*/, gpointer /*user_data*/ ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + SPDocument *doc = desktop ? desktop->doc() : nullptr; + if (doc) { + std::string targetName(bounceTarget->def.descr); + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( targetName == grad->getId() ) { + editGradientImpl( desktop, grad ); + break; + } + } + } + } +} + +void SwatchesPanelHook::convertGradient( GtkMenuItem * /*menuitem*/, gpointer userData ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + SPDocument *doc = desktop ? desktop->doc() : nullptr; + gint index = GPOINTER_TO_INT(userData); + if ( doc && (index >= 0) && (static_cast<guint>(index) < popupItems.size()) ) { + Glib::ustring targetName = popupItems[index]; + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + + if ( targetName == grad->getId() ) { + grad->setSwatch(); + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); + break; + } + } + } + } +} + +void SwatchesPanelHook::deleteGradient( GtkMenuItem */*menuitem*/, gpointer /*userData*/ ) +{ + if ( bounceTarget ) { + SwatchesPanel* swp = bouncePanel; + SPDesktop* desktop = swp ? swp->getDesktop() : nullptr; + sp_gradient_unset_swatch(desktop, bounceTarget->def.descr); + } +} + +static SwatchesPanel* findContainingPanel( GtkWidget *widget ) +{ + SwatchesPanel *swp = nullptr; + + std::map<GtkWidget*, SwatchesPanel*> rawObjects; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); it != docPerPanel.end(); ++it) { + rawObjects[GTK_WIDGET(it->first->gobj())] = it->first; + } + + for (GtkWidget* curr = widget; curr && !swp; curr = gtk_widget_get_parent(curr)) { + if (rawObjects.find(curr) != rawObjects.end()) { + swp = rawObjects[curr]; + } + } + + return swp; +} + +static void removeit( GtkWidget *widget, gpointer data ) +{ + gtk_container_remove( GTK_CONTAINER(data), widget ); +} + +/* extern'ed from color-item.cpp */ +bool colorItemHandleButtonPress(GdkEventButton* event, UI::Widget::Preview *preview, gpointer user_data) +{ + gboolean handled = FALSE; + + if ( event && (event->button == 3) && (event->type == GDK_BUTTON_PRESS) ) { + SwatchesPanel* swp = findContainingPanel( GTK_WIDGET(preview->gobj()) ); + + if ( !popupMenu ) { + popupMenu = gtk_menu_new(); + GtkWidget* child = nullptr; + + //TRANSLATORS: An item in context menu on a colour in the swatches + child = gtk_menu_item_new_with_label(_("Set fill")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(redirClick), + user_data); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + + //TRANSLATORS: An item in context menu on a colour in the swatches + child = gtk_menu_item_new_with_label(_("Set stroke")); + + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(redirSecondaryClick), + user_data); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + + child = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_menu_item_new_with_label(_("Delete")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(SwatchesPanelHook::deleteGradient), + user_data ); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + gtk_widget_set_sensitive( child, FALSE ); + + child = gtk_menu_item_new_with_label(_("Edit...")); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(editGradient), + user_data ); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + popupExtras.push_back(child); + + child = gtk_menu_item_new_with_label(_("Convert")); + gtk_menu_shell_append(GTK_MENU_SHELL(popupMenu), child); + //popupExtras.push_back(child); + //gtk_widget_set_sensitive( child, FALSE ); + { + popupSubHolder = child; + popupSub = gtk_menu_new(); + gtk_menu_item_set_submenu( GTK_MENU_ITEM(child), popupSub ); + } + + gtk_widget_show_all(popupMenu); + } + + if ( user_data ) { + ColorItem* item = reinterpret_cast<ColorItem*>(user_data); + bool show = swp && (swp->getSelectedIndex() == 0); + for (auto & popupExtra : popupExtras) { + gtk_widget_set_sensitive(popupExtra, show); + } + + bounceTarget = item; + bouncePanel = swp; + popupItems.clear(); + if ( popupMenu ) { + gtk_container_foreach(GTK_CONTAINER(popupSub), removeit, popupSub); + bool processed = false; + GtkWidget *wdgt = gtk_widget_get_ancestor(GTK_WIDGET(preview->gobj()), SP_TYPE_DESKTOP_WIDGET); + if ( wdgt ) { + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(wdgt); + if ( dtw && dtw->desktop ) { + // Pick up all gradients with vectors + std::vector<SPObject *> gradients = (dtw->desktop->doc())->getResourceList("gradient"); + gint index = 0; + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && !grad->isSwatch() ) { + //gl = g_slist_prepend(gl, curr->data); + processed = true; + GtkWidget *child = gtk_menu_item_new_with_label(grad->getId()); + gtk_menu_shell_append(GTK_MENU_SHELL(popupSub), child); + + popupItems.emplace_back(grad->getId()); + g_signal_connect( G_OBJECT(child), + "activate", + G_CALLBACK(SwatchesPanelHook::convertGradient), + GINT_TO_POINTER(index) ); + index++; + } + } + + gtk_widget_show_all(popupSub); + } + } + gtk_widget_set_sensitive( popupSubHolder, processed ); + gtk_menu_popup_at_pointer(GTK_MENU(popupMenu), reinterpret_cast<GdkEvent *>(event)); + handled = TRUE; + } + } + } + + return handled; +} + + +static char* trim( char* str ) { + char* ret = str; + while ( *str && (*str == ' ' || *str == '\t') ) { + str++; + } + ret = str; + while ( *str ) { + str++; + } + str--; + while ( str >= ret && (( *str == ' ' || *str == '\t' ) || *str == '\r' || *str == '\n') ) { + *str-- = 0; + } + return ret; +} + +static void skipWhitespace( char*& str ) { + while ( *str == ' ' || *str == '\t' ) { + str++; + } +} + +static bool parseNum( char*& str, int& val ) { + val = 0; + while ( '0' <= *str && *str <= '9' ) { + val = val * 10 + (*str - '0'); + str++; + } + bool retval = !(*str == 0 || *str == ' ' || *str == '\t' || *str == '\r' || *str == '\n'); + return retval; +} + + +void _loadPaletteFile(Glib::ustring path, gboolean user/*=FALSE*/) +{ + Glib::ustring filename = Glib::path_get_basename(path); + char block[1024]; + FILE *f = Inkscape::IO::fopen_utf8name(path.c_str(), "r"); + if ( f ) { + char* result = fgets( block, sizeof(block), f ); + if ( result ) { + if ( strncmp( "GIMP Palette", block, 12 ) == 0 ) { + bool inHeader = true; + bool hasErr = false; + + SwatchPage *onceMore = new SwatchPage(); + onceMore->_name = filename.c_str(); + + do { + result = fgets( block, sizeof(block), f ); + block[sizeof(block) - 1] = 0; + if ( result ) { + if ( block[0] == '#' ) { + // ignore comment + } else { + char *ptr = block; + // very simple check for header versus entry + while ( *ptr == ' ' || *ptr == '\t' ) { + ptr++; + } + if ( (*ptr == 0) || (*ptr == '\r') || (*ptr == '\n') ) { + // blank line. skip it. + } else if ( '0' <= *ptr && *ptr <= '9' ) { + // should be an entry link + inHeader = false; + ptr = block; + Glib::ustring name(""); + skipWhitespace(ptr); + if ( *ptr ) { + int r = 0; + int g = 0; + int b = 0; + hasErr = parseNum(ptr, r); + if ( !hasErr ) { + skipWhitespace(ptr); + hasErr = parseNum(ptr, g); + } + if ( !hasErr ) { + skipWhitespace(ptr); + hasErr = parseNum(ptr, b); + } + if ( !hasErr && *ptr ) { + char* n = trim(ptr); + if (n != nullptr && *n) { + name = g_dpgettext2(nullptr, "Palette", n); + } + if (name == "") { + name = Glib::ustring::compose("#%1%2%3", + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), r), + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), g), + Glib::ustring::format(std::hex, std::setw(2), std::setfill(L'0'), b) + ).uppercase(); + } + } + if ( !hasErr ) { + // Add the entry now + Glib::ustring nameStr(name); + ColorItem* item = new ColorItem( r, g, b, nameStr ); + onceMore->_colors.push_back(item); + } + } else { + hasErr = true; + } + } else { + if ( !inHeader ) { + // Hmmm... probably bad. Not quite the format we want? + hasErr = true; + } else { + char* sep = strchr(result, ':'); + if ( sep ) { + *sep = 0; + char* val = trim(sep + 1); + char* name = trim(result); + if ( *name ) { + if ( strcmp( "Name", name ) == 0 ) + { + onceMore->_name = val; + } + else if ( strcmp( "Columns", name ) == 0 ) + { + gchar* endPtr = nullptr; + guint64 numVal = g_ascii_strtoull( val, &endPtr, 10 ); + if ( (numVal == G_MAXUINT64) && (ERANGE == errno) ) { + // overflow + } else if ( (numVal == 0) && (endPtr == val) ) { + // failed conversion + } else { + onceMore->_prefWidth = numVal; + } + } + } else { + // error + hasErr = true; + } + } else { + // error + hasErr = true; + } + } + } + } + } + } while ( result && !hasErr ); + if ( !hasErr ) { + if (user) + userSwatchPages.push_back(onceMore); + else + systemSwatchPages.push_back(onceMore); +#if ENABLE_MAGIC_COLORS + ColorItem::_wireMagicColors( onceMore ); +#endif // ENABLE_MAGIC_COLORS + } else { + delete onceMore; + } + } + } + + fclose(f); + } +} + +static bool +compare_swatch_names(SwatchPage const *a, SwatchPage const *b) { + + return g_utf8_collate(a->_name.c_str(), b->_name.c_str()) < 0; +} + +static void load_palettes() +{ + static bool init_done = false; + + if (init_done) { + return; + } + init_done = true; + + for (auto &filename: Inkscape::IO::Resource::get_filenames(Inkscape::IO::Resource::PALETTES, {".gpl"})) { + bool userPalette = Inkscape::IO::file_is_writable(filename.c_str()); + _loadPaletteFile(filename, userPalette); + } + + // Sort the list of swatches by name, grouped by user/system + userSwatchPages.sort(compare_swatch_names); + systemSwatchPages.sort(compare_swatch_names); +} + +SwatchesPanel& SwatchesPanel::getInstance() +{ + return *new SwatchesPanel(); +} + + +/** + * Constructor + */ +SwatchesPanel::SwatchesPanel(gchar const* prefsPath) : + Inkscape::UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SWATCHES), + _menu(nullptr), + _holder(nullptr), + _clear(nullptr), + _remove(nullptr), + _currentIndex(0), + _currentDesktop(nullptr), + _currentDocument(nullptr) +{ + _holder = new PreviewHolder(); + + _build_menu(); + + auto menu_button = Gtk::manage(new Gtk::MenuButton()); + menu_button->set_halign(Gtk::ALIGN_END); + menu_button->set_relief(Gtk::RELIEF_NONE); + menu_button->set_image_from_icon_name("pan-start-symbolic", Gtk::ICON_SIZE_SMALL_TOOLBAR); + menu_button->set_popup(*_menu); + + auto box = Gtk::manage(new Gtk::Box()); + + if (_prefs_path == "/dialogs/swatches") { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + box->pack_start(*menu_button, Gtk::PACK_SHRINK); + } else { + box->set_orientation(Gtk::ORIENTATION_HORIZONTAL); + box->pack_end(*menu_button, Gtk::PACK_SHRINK); + _updateSettings(SWATCHES_SETTINGS_MODE, 1); + _holder->setOrientation(SP_ANCHOR_SOUTH); + } + + box->pack_start(*_holder, Gtk::PACK_EXPAND_WIDGET); + _getContents()->pack_start(*box); + + load_palettes(); + + Gtk::RadioMenuItem* hotItem = nullptr; + _clear = new ColorItem( ege::PaintDef::CLEAR ); + _remove = new ColorItem( ege::PaintDef::NONE ); + + if (docPalettes.empty()) { + SwatchPage *docPalette = new SwatchPage(); + + docPalette->_name = "Auto"; + docPalettes[nullptr] = docPalette; + } + + if ( !systemSwatchPages.empty() || !userSwatchPages.empty()) { + SwatchPage* first = nullptr; + int index = 0; + Glib::ustring targetName; + if ( !_prefs_path.empty() ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + targetName = prefs->getString(_prefs_path + "/palette"); + if (!targetName.empty()) { + if (targetName == "Auto") { + first = docPalettes[nullptr]; + } else { + std::vector<SwatchPage*> pages = _getSwatchSets(); + for (auto & page : pages) { + if ( page->_name == targetName ) { + first = page; + break; + } + index++; + } + } + } + } + + if ( !first ) { + first = docPalettes[nullptr]; + _currentIndex = 0; + } else { + _currentIndex = index; + } + + _rebuild(); + + Gtk::RadioMenuItem::Group groupOne; + + int i = 0; + std::vector<SwatchPage*> swatchSets = _getSwatchSets(); + for (auto curr : swatchSets) { + Gtk::RadioMenuItem* single = Gtk::manage(new Gtk::RadioMenuItem(groupOne, curr->_name)); + if ( curr == first ) { + hotItem = single; + } + _regItem(single, i); + + i++; + } + } + + if ( hotItem ) { + hotItem->set_active(); + } + + show_all_children(); +} + +SwatchesPanel::~SwatchesPanel() +{ + _trackDocument( this, nullptr ); + + _documentConnection.disconnect(); + _selChanged.disconnect(); + + if ( _clear ) { + delete _clear; + } + if ( _remove ) { + delete _remove; + } + if ( _holder ) { + delete _holder; + } + + delete _menu; +} + +void SwatchesPanel::_build_menu() +{ + guint panel_size = 0, panel_mode = 0, panel_ratio = 100, panel_border = 0; + bool panel_wrap = false; + if (!_prefs_path.empty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + panel_wrap = prefs->getBool(_prefs_path + "/panel_wrap"); + panel_size = prefs->getIntLimited(_prefs_path + "/panel_size", 1, 0, UI::Widget::PREVIEW_SIZE_HUGE); + panel_mode = prefs->getIntLimited(_prefs_path + "/panel_mode", 1, 0, 10); + panel_ratio = prefs->getIntLimited(_prefs_path + "/panel_ratio", 100, 0, 500 ); + panel_border = prefs->getIntLimited(_prefs_path + "/panel_border", UI::Widget::BORDER_NONE, 0, 2 ); + } + + _menu = new Gtk::Menu(); + + if (_prefs_path == "/dialogs/swatches") { + Gtk::RadioMenuItem::Group group; + Glib::ustring list_label(_("List")); + Glib::ustring grid_label(_("Grid")); + Gtk::RadioMenuItem *list_item = Gtk::manage(new Gtk::RadioMenuItem(group, list_label)); + Gtk::RadioMenuItem *grid_item = Gtk::manage(new Gtk::RadioMenuItem(group, grid_label)); + + if (panel_mode == 0) { + list_item->set_active(true); + } else if (panel_mode == 1) { + grid_item->set_active(true); + } + + _menu->append(*list_item); + _menu->append(*grid_item); + _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + list_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 0)); + grid_item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_MODE, 1)); + } + + { + Glib::ustring heightItemLabel(C_("Swatches", "Size")); + + //TRANSLATORS: Indicates size of colour swatches + const gchar *heightLabels[] = { + NC_("Swatches height", "Tiny"), + NC_("Swatches height", "Small"), + NC_("Swatches height", "Medium"), + NC_("Swatches height", "Large"), + NC_("Swatches height", "Huge") + }; + + Gtk::MenuItem *sizeItem = Gtk::manage(new Gtk::MenuItem(heightItemLabel)); + Gtk::Menu *sizeMenu = Gtk::manage(new Gtk::Menu()); + sizeItem->set_submenu(*sizeMenu); + + Gtk::RadioMenuItem::Group heightGroup; + for (unsigned int i = 0; i < G_N_ELEMENTS(heightLabels); i++) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches height", heightLabels[i])); + Gtk::RadioMenuItem* _item = Gtk::manage(new Gtk::RadioMenuItem(heightGroup, _label)); + sizeMenu->append(*_item); + if (i == panel_size) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SIZE, i)); + } + + _menu->append(*sizeItem); + } + + { + Glib::ustring widthItemLabel(C_("Swatches", "Width")); + + //TRANSLATORS: Indicates width of colour swatches + const gchar *widthLabels[] = { + NC_("Swatches width", "Narrower"), + NC_("Swatches width", "Narrow"), + NC_("Swatches width", "Medium"), + NC_("Swatches width", "Wide"), + NC_("Swatches width", "Wider") + }; + + Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); + Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); + item->set_submenu(*type_menu); + _menu->append(*item); + + Gtk::RadioMenuItem::Group widthGroup; + + guint values[] = {0, 25, 50, 100, 200, 400}; + guint hot_index = 3; + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + // Assume all values are in increasing order + if ( values[i] <= panel_ratio ) { + hot_index = i; + } + } + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches width", widthLabels[i])); + Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); + type_menu->append(*_item); + if ( i <= hot_index ) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_SHAPE, values[i])); + } + } + + { + Glib::ustring widthItemLabel(C_("Swatches", "Border")); + + //TRANSLATORS: Indicates border of colour swatches + const gchar *widthLabels[] = { + NC_("Swatches border", "None"), + NC_("Swatches border", "Solid"), + NC_("Swatches border", "Wide"), + }; + + Gtk::MenuItem *item = Gtk::manage( new Gtk::MenuItem(widthItemLabel)); + Gtk::Menu *type_menu = Gtk::manage(new Gtk::Menu()); + item->set_submenu(*type_menu); + _menu->append(*item); + + Gtk::RadioMenuItem::Group widthGroup; + + guint values[] = {0, 1, 2}; + guint hot_index = 0; + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + // Assume all values are in increasing order + if ( values[i] <= panel_border ) { + hot_index = i; + } + } + for ( guint i = 0; i < G_N_ELEMENTS(widthLabels); ++i ) { + Glib::ustring _label(g_dpgettext2(nullptr, "Swatches border", widthLabels[i])); + Gtk::RadioMenuItem *_item = Gtk::manage(new Gtk::RadioMenuItem(widthGroup, _label)); + type_menu->append(*_item); + if ( i <= hot_index ) { + _item->set_active(true); + } + _item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_BORDER, values[i])); + } + } + + if (_prefs_path == "/embedded/swatches") { + //TRANSLATORS: "Wrap" indicates how colour swatches are displayed + Glib::ustring wrap_label(C_("Swatches","Wrap")); + Gtk::CheckMenuItem *check = Gtk::manage(new Gtk::CheckMenuItem(wrap_label)); + check->set_active(panel_wrap); + _menu->append(*check); + + check->signal_toggled().connect(sigc::bind<Gtk::CheckMenuItem*>(sigc::mem_fun(*this, &SwatchesPanel::_wrapToggled), check)); + } + + _menu->append(*Gtk::manage(new Gtk::SeparatorMenuItem())); + + _menu->show_all(); + + _updateSettings(SWATCHES_SETTINGS_SIZE, panel_size); + _updateSettings(SWATCHES_SETTINGS_MODE, panel_mode); + _updateSettings(SWATCHES_SETTINGS_SHAPE, panel_ratio); + _updateSettings(SWATCHES_SETTINGS_WRAP, panel_wrap); + _updateSettings(SWATCHES_SETTINGS_BORDER, panel_border); +} + +void SwatchesPanel::setDesktop( SPDesktop* desktop ) +{ + if ( desktop != _currentDesktop ) { + if ( _currentDesktop ) { + _documentConnection.disconnect(); + _selChanged.disconnect(); + } + + _currentDesktop = desktop; + + if ( desktop ) { + _currentDesktop->selection->connectChanged( + sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); + + _currentDesktop->selection->connectModified( + sigc::hide(sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection)))); + + _currentDesktop->connectToolSubselectionChanged( + sigc::hide(sigc::mem_fun(*this, &SwatchesPanel::_updateFromSelection))); + + sigc::bound_mem_functor1<void, SwatchesPanel, SPDocument*> first = sigc::mem_fun(*this, &SwatchesPanel::_setDocument); + sigc::slot<void, SPDocument*> base2 = first; + sigc::slot<void,SPDesktop*, SPDocument*> slot2 = sigc::hide<0>( base2 ); + _documentConnection = desktop->connectDocumentReplaced( slot2 ); + + _setDocument( desktop->doc() ); + } else { + _setDocument(nullptr); + } + } +} + + +void SwatchesPanel::_updateSettings(int settings, int value) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + switch (settings) { + case SWATCHES_SETTINGS_SIZE: { + prefs->setInt(_prefs_path + "/panel_size", value); + + auto curr_type = _holder->getPreviewType(); + guint curr_ratio = _holder->getPreviewRatio(); + auto curr_border = _holder->getPreviewBorder(); + + switch (value) { + case 0: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_TINY, curr_type, curr_ratio, curr_border); + break; + case 1: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_SMALL, curr_type, curr_ratio, curr_border); + break; + case 2: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_MEDIUM, curr_type, curr_ratio, curr_border); + break; + case 3: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_BIG, curr_type, curr_ratio, curr_border); + break; + case 4: + _holder->setStyle(UI::Widget::PREVIEW_SIZE_HUGE, curr_type, curr_ratio, curr_border); + break; + default: + break; + } + + break; + } + case SWATCHES_SETTINGS_MODE: { + prefs->setInt(_prefs_path + "/panel_mode", value); + + auto curr_size = _holder->getPreviewSize(); + guint curr_ratio = _holder->getPreviewRatio(); + auto curr_border = _holder->getPreviewBorder(); + switch (value) { + case 0: + _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_LIST, curr_ratio, curr_border); + break; + case 1: + _holder->setStyle(curr_size, UI::Widget::VIEW_TYPE_GRID, curr_ratio, curr_border); + break; + default: + break; + } + break; + } + case SWATCHES_SETTINGS_SHAPE: { + prefs->setInt(_prefs_path + "/panel_ratio", value); + + auto curr_type = _holder->getPreviewType(); + auto curr_size = _holder->getPreviewSize(); + auto curr_border = _holder->getPreviewBorder(); + + _holder->setStyle(curr_size, curr_type, value, curr_border); + break; + } + case SWATCHES_SETTINGS_BORDER: { + prefs->setInt(_prefs_path + "/panel_border", value); + + auto curr_size = _holder->getPreviewSize(); + auto curr_type = _holder->getPreviewType(); + guint curr_ratio = _holder->getPreviewRatio(); + + switch (value) { + case 0: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_NONE); + break; + case 1: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_SOLID); + break; + case 2: + _holder->setStyle(curr_size, curr_type, curr_ratio, UI::Widget::BORDER_WIDE); + break; + default: + break; + } + break; + } + case SWATCHES_SETTINGS_WRAP: { + prefs->setBool(_prefs_path + "/panel_wrap", value); + _holder->setWrap(value); + break; + } + case SWATCHES_SETTINGS_PALETTE: { + std::vector<SwatchPage*> pages = _getSwatchSets(); + if (value >= 0 && value < static_cast<int>(pages.size()) ) { + _currentIndex = value; + + prefs->setString(_prefs_path + "/palette", pages[_currentIndex]->_name); + + _rebuild(); + } + } + default: + break; + } +} + +void SwatchesPanel::_wrapToggled(Gtk::CheckMenuItem* toggler) +{ + if (toggler) { + _updateSettings(SWATCHES_SETTINGS_WRAP, toggler->get_active() ? 1 : 0); + } +} + +void SwatchesPanel::_regItem(Gtk::MenuItem* item, int id) +{ + _menu->append(*item); + item->signal_activate().connect(sigc::bind<int, int>(sigc::mem_fun(*this, &SwatchesPanel::_updateSettings), SWATCHES_SETTINGS_PALETTE, id)); + item->show(); +} + + +class DocTrack +{ +public: + DocTrack(SPDocument *doc, sigc::connection &gradientRsrcChanged, sigc::connection &defsChanged, sigc::connection &defsModified) : + doc(doc->doRef()), + updatePending(false), + lastGradientUpdate(0.0), + gradientRsrcChanged(gradientRsrcChanged), + defsChanged(defsChanged), + defsModified(defsModified) + { + if ( !timer ) { + timer = new Glib::Timer(); + refreshTimer = Glib::signal_timeout().connect( sigc::ptr_fun(handleTimerCB), 33 ); + } + timerRefCount++; + } + + ~DocTrack() + { + timerRefCount--; + if ( timerRefCount <= 0 ) { + refreshTimer.disconnect(); + timerRefCount = 0; + if ( timer ) { + timer->stop(); + delete timer; + timer = nullptr; + } + } + if (doc) { + gradientRsrcChanged.disconnect(); + defsChanged.disconnect(); + defsModified.disconnect(); + doc->doUnref(); + doc = nullptr; + } + } + + static bool handleTimerCB(); + + /** + * Checks if update should be queued or executed immediately. + * + * @return true if the update was queued and should not be immediately executed. + */ + static bool queueUpdateIfNeeded(SPDocument *doc); + + static Glib::Timer *timer; + static int timerRefCount; + static sigc::connection refreshTimer; + + SPDocument *doc; + bool updatePending; + double lastGradientUpdate; + sigc::connection gradientRsrcChanged; + sigc::connection defsChanged; + sigc::connection defsModified; + +private: + DocTrack(DocTrack const &) = delete; // no copy + DocTrack &operator=(DocTrack const &) = delete; // no assign +}; + +Glib::Timer *DocTrack::timer = nullptr; +int DocTrack::timerRefCount = 0; +sigc::connection DocTrack::refreshTimer; + +static const double DOC_UPDATE_THREASHOLD = 0.090; + +bool DocTrack::handleTimerCB() +{ + double now = timer->elapsed(); + + std::vector<DocTrack *> needCallback; + for (auto track : docTrackings) { + if ( track->updatePending && ( (now - track->lastGradientUpdate) >= DOC_UPDATE_THREASHOLD) ) { + needCallback.push_back(track); + } + } + + for (auto track : needCallback) { + if ( std::find(docTrackings.begin(), docTrackings.end(), track) != docTrackings.end() ) { // Just in case one gets deleted while we are looping + // Note: calling handleDefsModified will call queueUpdateIfNeeded and thus update the time and flag. + SwatchesPanel::handleDefsModified(track->doc); + } + } + + return true; +} + +bool DocTrack::queueUpdateIfNeeded( SPDocument *doc ) +{ + bool deferProcessing = false; + for (auto track : docTrackings) { + if ( track->doc == doc ) { + double now = timer->elapsed(); + double elapsed = now - track->lastGradientUpdate; + + if ( elapsed < DOC_UPDATE_THREASHOLD ) { + deferProcessing = true; + track->updatePending = true; + } else { + track->lastGradientUpdate = now; + track->updatePending = false; + } + + break; + } + } + return deferProcessing; +} + +void SwatchesPanel::_trackDocument( SwatchesPanel *panel, SPDocument *document ) +{ + SPDocument *oldDoc = nullptr; + if (docPerPanel.find(panel) != docPerPanel.end()) { + oldDoc = docPerPanel[panel]; + if (!oldDoc) { + docPerPanel.erase(panel); // Should not be needed, but clean up just in case. + } + } + if (oldDoc != document) { + if (oldDoc) { + docPerPanel[panel] = nullptr; + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + if (!found) { + for (std::vector<DocTrack*>::iterator it = docTrackings.begin(); it != docTrackings.end(); ++it){ + if ((*it)->doc == oldDoc) { + delete *it; + docTrackings.erase(it); + break; + } + } + } + } + + if (document) { + bool found = false; + for (std::map<SwatchesPanel*, SPDocument*>::iterator it = docPerPanel.begin(); (it != docPerPanel.end()) && !found; ++it) { + found = (it->second == document); + } + docPerPanel[panel] = document; + if (!found) { + sigc::connection conn1 = document->connectResourcesChanged( "gradient", sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleGradientsChange), document) ); + sigc::connection conn2 = document->getDefs()->connectRelease( sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document)) ); + sigc::connection conn3 = document->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&SwatchesPanel::handleDefsModified), document))) ); + + DocTrack *dt = new DocTrack(document, conn1, conn2, conn3); + docTrackings.push_back(dt); + + if (docPalettes.find(document) == docPalettes.end()) { + SwatchPage *docPalette = new SwatchPage(); + docPalette->_name = "Auto"; + docPalettes[document] = docPalette; + } + } + } + } +} + +void SwatchesPanel::_setDocument( SPDocument *document ) +{ + if ( document != _currentDocument ) { + _trackDocument(this, document); + _currentDocument = document; + handleGradientsChange( document ); + } +} + +static void recalcSwatchContents(SPDocument* doc, + boost::ptr_vector<ColorItem> &tmpColors, + std::map<ColorItem*, cairo_pattern_t*> &previewMappings, + std::map<ColorItem*, SPGradient*> &gradMappings) +{ + std::vector<SPGradient*> newList; + std::vector<SPObject *> gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->isSwatch() ) { + newList.push_back(SP_GRADIENT(gradient)); + } + } + + if ( !newList.empty() ) { + std::reverse(newList.begin(), newList.end()); + for (auto grad : newList) + { + cairo_surface_t *preview = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, + PREVIEW_PIXBUF_WIDTH, VBLOCK); + cairo_t *ct = cairo_create(preview); + + Glib::ustring name( grad->getId() ); + ColorItem* item = new ColorItem( 0, 0, 0, name ); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_pattern_t *gradient = grad->create_preview_pattern(PREVIEW_PIXBUF_WIDTH); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_set_source(ct, gradient); + cairo_paint(ct); + + cairo_destroy(ct); + cairo_pattern_destroy(gradient); + cairo_pattern_destroy(check); + + cairo_pattern_t *prevpat = cairo_pattern_create_for_surface(preview); + cairo_surface_destroy(preview); + + previewMappings[item] = prevpat; + + tmpColors.push_back(item); + gradMappings[item] = grad; + } + } +} + +void SwatchesPanel::handleGradientsChange(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; + if (docPalette) { + boost::ptr_vector<ColorItem> tmpColors; + std::map<ColorItem*, cairo_pattern_t*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + for (auto & tmpPrev : tmpPrevs) { + tmpPrev.first->setPattern(tmpPrev.second); + cairo_pattern_destroy(tmpPrev.second); + } + + for (auto & tmpGrad : tmpGrads) { + tmpGrad.first->setGradient(tmpGrad.second); + } + + docPalette->_colors.swap(tmpColors); + + // Figure out which SwatchesPanel instances are affected and update them. + + for (auto & it : docPerPanel) { + if (it.second == document) { + SwatchesPanel* swp = it.first; + std::vector<SwatchPage*> pages = swp->_getSwatchSets(); + SwatchPage* curr = pages[swp->_currentIndex]; + if (curr == docPalette) { + swp->_rebuild(); + } + } + } + } +} + +void SwatchesPanel::handleDefsModified(SPDocument *document) +{ + SwatchPage *docPalette = (docPalettes.find(document) != docPalettes.end()) ? docPalettes[document] : nullptr; + if (docPalette && !DocTrack::queueUpdateIfNeeded(document) ) { + boost::ptr_vector<ColorItem> tmpColors; + std::map<ColorItem*, cairo_pattern_t*> tmpPrevs; + std::map<ColorItem*, SPGradient*> tmpGrads; + recalcSwatchContents(document, tmpColors, tmpPrevs, tmpGrads); + + if ( tmpColors.size() != docPalette->_colors.size() ) { + handleGradientsChange(document); + } else { + int cap = std::min(docPalette->_colors.size(), tmpColors.size()); + for (int i = 0; i < cap; i++) { + ColorItem *newColor = &tmpColors[i]; + ColorItem *oldColor = &docPalette->_colors[i]; + if ( (newColor->def.getType() != oldColor->def.getType()) || + (newColor->def.getR() != oldColor->def.getR()) || + (newColor->def.getG() != oldColor->def.getG()) || + (newColor->def.getB() != oldColor->def.getB()) ) { + oldColor->def.setRGB(newColor->def.getR(), newColor->def.getG(), newColor->def.getB()); + } + if (tmpGrads.find(newColor) != tmpGrads.end()) { + oldColor->setGradient(tmpGrads[newColor]); + } + if ( tmpPrevs.find(newColor) != tmpPrevs.end() ) { + oldColor->setPattern(tmpPrevs[newColor]); + } + } + } + + for (auto & tmpPrev : tmpPrevs) { + cairo_pattern_destroy(tmpPrev.second); + } + } +} + + +std::vector<SwatchPage*> SwatchesPanel::_getSwatchSets() const +{ + std::vector<SwatchPage*> tmp; + if (docPalettes.find(_currentDocument) != docPalettes.end()) { + tmp.push_back(docPalettes[_currentDocument]); + } + + tmp.insert(tmp.end(), userSwatchPages.begin(), userSwatchPages.end()); + tmp.insert(tmp.end(), systemSwatchPages.begin(), systemSwatchPages.end()); + + return tmp; +} + +void SwatchesPanel::_updateFromSelection() +{ + SwatchPage *docPalette = (docPalettes.find(_currentDocument) != docPalettes.end()) ? docPalettes[_currentDocument] : nullptr; + if ( docPalette ) { + Glib::ustring fillId; + Glib::ustring strokeId; + + SPStyle tmpStyle(_currentDesktop->getDocument()); + int result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_FILL ); + switch (result) { + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + if (tmpStyle.fill.set && tmpStyle.fill.isPaintserver()) { + SPPaintServer* server = tmpStyle.getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* target = nullptr; + SPGradient* grad = SP_GRADIENT(server); + + if ( grad->isSwatch() ) { + target = grad; + } else if ( grad->ref ) { + SPGradient *tmp = grad->ref->getObject(); + if ( tmp && tmp->isSwatch() ) { + target = tmp; + } + } + if ( target ) { + //XML Tree being used directly here while it shouldn't be + gchar const* id = target->getRepr()->attribute("id"); + if ( id ) { + fillId = id; + } + } + } + } + break; + } + } + + result = sp_desktop_query_style( _currentDesktop, &tmpStyle, QUERY_STYLE_PROPERTY_STROKE ); + switch (result) { + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + if (tmpStyle.stroke.set && tmpStyle.stroke.isPaintserver()) { + SPPaintServer* server = tmpStyle.getStrokePaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient* target = nullptr; + SPGradient* grad = SP_GRADIENT(server); + if ( grad->isSwatch() ) { + target = grad; + } else if ( grad->ref ) { + SPGradient *tmp = grad->ref->getObject(); + if ( tmp && tmp->isSwatch() ) { + target = tmp; + } + } + if ( target ) { + //XML Tree being used directly here while it shouldn't be + gchar const* id = target->getRepr()->attribute("id"); + if ( id ) { + strokeId = id; + } + } + } + } + break; + } + } + + for (auto & _color : docPalette->_colors) { + ColorItem* item = &_color; + bool isFill = (fillId == item->def.descr); + bool isStroke = (strokeId == item->def.descr); + item->setState( isFill, isStroke ); + } + } +} + +void SwatchesPanel::_rebuild() +{ + std::vector<SwatchPage*> pages = _getSwatchSets(); + SwatchPage* curr = pages[_currentIndex]; + _holder->clear(); + + if ( curr->_prefWidth > 0 ) { + _holder->setColumnPref( curr->_prefWidth ); + } + _holder->freezeUpdates(); + // TODO restore once 'clear' works _holder->addPreview(_clear); + _holder->addPreview(_remove); + for (auto & _color : curr->_colors) { + _holder->addPreview(&_color); + } + _holder->thawUpdates(); +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/swatches.h b/src/ui/dialog/swatches.h new file mode 100644 index 0000000..da10911 --- /dev/null +++ b/src/ui/dialog/swatches.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Color swatches dialog + */ +/* Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DIALOGS_SWATCHES_H +#define SEEN_DIALOGS_SWATCHES_H + +#include "ui/widget/panel.h" + +namespace Gtk { + class Menu; + class MenuItem; + class CheckMenuItem; +} + +namespace Inkscape { +namespace UI { + +class PreviewHolder; + +namespace Dialog { + +class ColorItem; +class SwatchPage; +class DocTrack; + +/** + * A panel that displays paint swatches. + * + * It comes in two flavors, depending on the prefsPath argument passed to + * the constructor: the default "/dialog/swatches" is just a regular panel; + * the "/embedded/swatches/" is the horizontal color swatches at the bottom + * of window. + */ +class SwatchesPanel : public Inkscape::UI::Widget::Panel +{ +public: + SwatchesPanel(gchar const* prefsPath = "/dialogs/swatches"); + ~SwatchesPanel() override; + + static SwatchesPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + virtual SPDesktop* getDesktop() {return _currentDesktop;} + + virtual int getSelectedIndex() {return _currentIndex;} // temporary + +protected: + static void handleGradientsChange(SPDocument *document); + + virtual void _updateFromSelection(); + virtual void _setDocument( SPDocument *document ); + virtual void _rebuild(); + + virtual std::vector<SwatchPage*> _getSwatchSets() const; + +private: + SwatchesPanel(SwatchesPanel const &) = delete; // no copy + SwatchesPanel &operator=(SwatchesPanel const &) = delete; // no assign + + void _build_menu(); + + static void _trackDocument( SwatchesPanel *panel, SPDocument *document ); + static void handleDefsModified(SPDocument *document); + + PreviewHolder* _holder; + ColorItem* _clear; + ColorItem* _remove; + int _currentIndex; + SPDesktop* _currentDesktop; + SPDocument* _currentDocument; + + void _regItem(Gtk::MenuItem* item, int id); + + void _updateSettings(int settings, int value); + + void _wrapToggled(Gtk::CheckMenuItem *toggler); + + Gtk::Menu *_menu; + + sigc::connection _documentConnection; + sigc::connection _selChanged; + + friend class DocTrack; +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_SWATCHES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/symbols.cpp b/src/ui/dialog/symbols.cpp new file mode 100644 index 0000000..b03bd5d --- /dev/null +++ b/src/ui/dialog/symbols.cpp @@ -0,0 +1,1403 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Symbols dialog. + */ +/* Authors: + * Copyright (C) 2012 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <iostream> +#include <algorithm> +#include <locale> +#include <sstream> +#include <fstream> +#include <regex> + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> +#include <glibmm/regex.h> +#include <glibmm/stringutils.h> + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "path-prefix.h" +#include "selection.h" +#include "symbols.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "io/resource.h" +#include "io/sys.h" +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "object/sp-symbol.h" +#include "object/sp-use.h" +#include "ui/cache/svg_preview_cache.h" +#include "ui/clipboard.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#ifdef WITH_LIBVISIO + #include <libvisio/libvisio.h> + + // TODO: Drop this check when librevenge is widespread. + #if WITH_LIBVISIO01 + #include <librevenge-stream/librevenge-stream.h> + + using librevenge::RVNGFileStream; + using librevenge::RVNGString; + using librevenge::RVNGStringVector; + using librevenge::RVNGPropertyList; + using librevenge::RVNGSVGDrawingGenerator; + #else + #include <libwpd-stream/libwpd-stream.h> + + typedef WPXFileStream RVNGFileStream; + typedef libvisio::VSDStringVector RVNGStringVector; + #endif +#endif + + +namespace Inkscape { +namespace UI { + +namespace Dialog { + +// See: http://developer.gnome.org/gtkmm/stable/classGtk_1_1TreeModelColumnRecord.html +class SymbolColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + Gtk::TreeModelColumn<Glib::ustring> symbol_id; + Gtk::TreeModelColumn<Glib::ustring> symbol_title; + Gtk::TreeModelColumn<Glib::ustring> symbol_doc_title; + Gtk::TreeModelColumn< Glib::RefPtr<Gdk::Pixbuf> > symbol_image; + + + SymbolColumns() { + add(symbol_id); + add(symbol_title); + add(symbol_doc_title); + add(symbol_image); + } +}; + +SymbolColumns* SymbolsDialog::getColumns() +{ + SymbolColumns* columns = new SymbolColumns(); + return columns; +} + +/** + * Constructor + */ +SymbolsDialog::SymbolsDialog( gchar const* prefsPath ) : + UI::Widget::Panel(prefsPath, SP_VERB_DIALOG_SYMBOLS), + store(Gtk::ListStore::create(*getColumns())), + all_docs_processed(false), + icon_view(nullptr), + current_desktop(nullptr), + desk_track(), + current_document(nullptr), + preview_document(nullptr), + instanceConns(), + CURRENTDOC(_("Current document")), + ALLDOCS(_("All symbol sets")) +{ + + /******************** Table *************************/ + auto table = new Gtk::Grid(); + + table->set_margin_start(3); + table->set_margin_end(3); + table->set_margin_top(4); + // panel is a locked Gtk::VBox + _getContents()->pack_start(*Gtk::manage(table), Gtk::PACK_EXPAND_WIDGET); + guint row = 0; + + /******************** Symbol Sets *************************/ + Gtk::Label* label_set = new Gtk::Label(Glib::ustring(_("Symbol set")) + ": "); + table->attach(*Gtk::manage(label_set),0,row,1,1); + symbol_set = new Gtk::ComboBoxText(); // Fill in later + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + symbol_set->set_active_text(CURRENTDOC); + symbol_set->set_hexpand(); + + table->attach(*Gtk::manage(symbol_set),1,row,1,1); + sigc::connection connSet = symbol_set->signal_changed().connect( + sigc::mem_fun(*this, &SymbolsDialog::rebuild)); + instanceConns.push_back(connSet); + + ++row; + + /******************** Separator *************************/ + + + Gtk::Separator* separator = Gtk::manage(new Gtk::Separator()); // Search + separator->set_margin_top(10); + separator->set_margin_bottom(10); + table->attach(*Gtk::manage(separator),0,row,2,1); + + ++row; + + /******************** Search *************************/ + + + search = Gtk::manage(new Gtk::SearchEntry()); // Search + search->set_tooltip_text(_("Return to start search.")); + search->signal_key_press_event().connect_notify( sigc::mem_fun(*this, &SymbolsDialog::beforeSearch)); + search->signal_key_release_event().connect_notify(sigc::mem_fun(*this, &SymbolsDialog::unsensitive)); + + search->set_margin_bottom(6); + search->signal_search_changed().connect(sigc::mem_fun(*this, &SymbolsDialog::clearSearch)); + table->attach(*Gtk::manage(search),0,row,2,1); + search_str = ""; + + ++row; + + + /********************* Icon View **************************/ + SymbolColumns* columns = getColumns(); + + icon_view = new Gtk::IconView(static_cast<Glib::RefPtr<Gtk::TreeModel> >(store)); + //icon_view->set_text_column( columns->symbol_id ); + icon_view->set_tooltip_column( 1 ); + icon_view->set_pixbuf_column( columns->symbol_image ); + // Giving the iconview a small minimum size will help users understand + // What the dialog does. + icon_view->set_size_request( 100, 250 ); + + std::vector< Gtk::TargetEntry > targets; + targets.emplace_back( "application/x-inkscape-paste"); + + icon_view->enable_model_drag_source (targets, Gdk::BUTTON1_MASK, Gdk::ACTION_COPY); + icon_view->signal_drag_data_get().connect( + sigc::mem_fun(*this, &SymbolsDialog::iconDragDataGet)); + + sigc::connection connIconChanged; + connIconChanged = icon_view->signal_selection_changed().connect( + sigc::mem_fun(*this, &SymbolsDialog::iconChanged)); + instanceConns.push_back(connIconChanged); + + scroller = new Gtk::ScrolledWindow(); + scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS); + scroller->add(*Gtk::manage(icon_view)); + scroller->set_hexpand(); + scroller->set_vexpand(); + + overlay = new Gtk::Overlay(); + overlay->set_hexpand(); + overlay->set_vexpand(); + overlay->add(* scroller); + overlay->get_style_context()->add_class("forcebright"); + scroller->set_size_request(100, 250); + table->attach(*Gtk::manage(overlay), 0, row, 2, 1); + + /*************************Overlays******************************/ + overlay_opacity = new Gtk::Image(); + overlay_opacity->set_halign(Gtk::ALIGN_START); + overlay_opacity->set_valign(Gtk::ALIGN_START); + overlay_opacity->get_style_context()->add_class("rawstyle"); + + // No results + overlay_icon = sp_get_icon_image("searching", Gtk::ICON_SIZE_DIALOG); + overlay_icon->set_pixel_size(110); + overlay_icon->set_halign(Gtk::ALIGN_CENTER); + overlay_icon->set_valign(Gtk::ALIGN_START); + + overlay_icon->set_margin_top(45); + + overlay_title = new Gtk::Label(); + overlay_title->set_halign(Gtk::ALIGN_CENTER ); + overlay_title->set_valign(Gtk::ALIGN_START ); + overlay_title->set_justify(Gtk::JUSTIFY_CENTER); + overlay_title->set_margin_top(155); + + overlay_desc = new Gtk::Label(); + overlay_desc->set_halign(Gtk::ALIGN_CENTER); + overlay_desc->set_valign(Gtk::ALIGN_START); + overlay_desc->set_margin_top(180); + overlay_desc->set_justify(Gtk::JUSTIFY_CENTER); + + overlay->add_overlay(*overlay_opacity); + overlay->add_overlay(*overlay_icon); + overlay->add_overlay(*overlay_title); + overlay->add_overlay(*overlay_desc); + + previous_height = 0; + previous_width = 0; + ++row; + + /******************** Progress *******************************/ + progress = new Gtk::HBox(); + progress_bar = Gtk::manage(new Gtk::ProgressBar()); + table->attach(*Gtk::manage(progress),0,row, 2, 1); + progress->pack_start(* progress_bar, Gtk::PACK_EXPAND_WIDGET); + progress->set_margin_top(15); + progress->set_margin_bottom(15); + progress->set_margin_start(20); + progress->set_margin_end(20); + + ++row; + + /******************** Tools *******************************/ + tools = new Gtk::HBox(); + + //tools->set_layout( Gtk::BUTTONBOX_END ); + scroller->set_hexpand(); + table->attach(*Gtk::manage(tools),0,row,2,1); + + auto add_symbol_image = Gtk::manage(sp_get_icon_image("symbol-add", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + add_symbol = Gtk::manage(new Gtk::Button()); + add_symbol->add(*add_symbol_image); + add_symbol->set_tooltip_text(_("Add Symbol from the current document.")); + add_symbol->set_relief( Gtk::RELIEF_NONE ); + add_symbol->set_focus_on_click( false ); + add_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::insertSymbol)); + tools->pack_start(* add_symbol, Gtk::PACK_SHRINK); + + auto remove_symbolImage = Gtk::manage(sp_get_icon_image("symbol-remove", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + remove_symbol = Gtk::manage(new Gtk::Button()); + remove_symbol->add(*remove_symbolImage); + remove_symbol->set_tooltip_text(_("Remove Symbol from the current document.")); + remove_symbol->set_relief( Gtk::RELIEF_NONE ); + remove_symbol->set_focus_on_click( false ); + remove_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::revertSymbol)); + tools->pack_start(* remove_symbol, Gtk::PACK_SHRINK); + + Gtk::Label* spacer = Gtk::manage(new Gtk::Label("")); + tools->pack_start(* Gtk::manage(spacer)); + + // Pack size (controls display area) + pack_size = 2; // Default 32px + + auto packMoreImage = Gtk::manage(sp_get_icon_image("pack-more", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + more = Gtk::manage(new Gtk::Button()); + more->add(*packMoreImage); + more->set_tooltip_text(_("Display more icons in row.")); + more->set_relief( Gtk::RELIEF_NONE ); + more->set_focus_on_click( false ); + more->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packmore)); + tools->pack_start(* more, Gtk::PACK_SHRINK); + + auto packLessImage = Gtk::manage(sp_get_icon_image("pack-less", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + fewer = Gtk::manage(new Gtk::Button()); + fewer->add(*packLessImage); + fewer->set_tooltip_text(_("Display fewer icons in row.")); + fewer->set_relief( Gtk::RELIEF_NONE ); + fewer->set_focus_on_click( false ); + fewer->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::packless)); + tools->pack_start(* fewer, Gtk::PACK_SHRINK); + + // Toggle scale to fit on/off + auto fit_symbolImage = Gtk::manage(sp_get_icon_image("symbol-fit", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + fit_symbol = Gtk::manage(new Gtk::ToggleButton()); + fit_symbol->add(*fit_symbolImage); + fit_symbol->set_tooltip_text(_("Toggle 'fit' symbols in icon space.")); + fit_symbol->set_relief( Gtk::RELIEF_NONE ); + fit_symbol->set_focus_on_click( false ); + fit_symbol->set_active( true ); + fit_symbol->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::rebuild)); + tools->pack_start(* fit_symbol, Gtk::PACK_SHRINK); + + // Render size (scales symbols within display area) + scale_factor = 0; // Default 1:1 * pack_size/pack_size default + auto zoom_outImage = Gtk::manage(sp_get_icon_image("symbol-smaller", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + zoom_out = Gtk::manage(new Gtk::Button()); + zoom_out->add(*zoom_outImage); + zoom_out->set_tooltip_text(_("Make symbols smaller by zooming out.")); + zoom_out->set_relief( Gtk::RELIEF_NONE ); + zoom_out->set_focus_on_click( false ); + zoom_out->set_sensitive( false ); + zoom_out->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomout)); + tools->pack_start(* zoom_out, Gtk::PACK_SHRINK); + + auto zoom_inImage = Gtk::manage(sp_get_icon_image("symbol-bigger", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + zoom_in = Gtk::manage(new Gtk::Button()); + zoom_in->add(*zoom_inImage); + zoom_in->set_tooltip_text(_("Make symbols bigger by zooming in.")); + zoom_in->set_relief( Gtk::RELIEF_NONE ); + zoom_in->set_focus_on_click( false ); + zoom_in->set_sensitive( false ); + zoom_in->signal_clicked().connect(sigc::mem_fun(*this, &SymbolsDialog::zoomin)); + tools->pack_start(* zoom_in, Gtk::PACK_SHRINK); + + ++row; + + sensitive = true; + + current_desktop = SP_ACTIVE_DESKTOP; + current_document = current_desktop->getDocument(); + preview_document = symbolsPreviewDoc(); /* Template to render symbols in */ + preview_document->ensureUpToDate(); /* Necessary? */ + key = SPItem::display_key_new(1); + renderDrawing.setRoot(preview_document->getRoot()->invoke_show(renderDrawing, key, SP_ITEM_SHOW_DISPLAY )); + + // This might need to be a global variable so setTargetDesktop can modify it + SPDefs *defs = current_document->getDefs(); + sigc::connection defsModifiedConn = defs->connectModified(sigc::mem_fun(*this, &SymbolsDialog::defsModified)); + instanceConns.push_back(defsModifiedConn); + + sigc::connection selectionChangedConn = current_desktop->selection->connectChanged( + sigc::mem_fun(*this, &SymbolsDialog::selectionChanged)); + instanceConns.push_back(selectionChangedConn); + + sigc::connection documentReplacedConn = current_desktop->connectDocumentReplaced( + sigc::mem_fun(*this, &SymbolsDialog::documentReplaced)); + instanceConns.push_back(documentReplacedConn); + getSymbolsTitle(); + icons_found = false; + + addSymbolsInDoc(current_document); /* Defaults to current document */ + sigc::connection desktopChangeConn = + desk_track.connectDesktopChanged( sigc::mem_fun(*this, &SymbolsDialog::setTargetDesktop) ); + instanceConns.push_back( desktopChangeConn ); + desk_track.connect(GTK_WIDGET(gobj())); +} + +SymbolsDialog::~SymbolsDialog() +{ + for (auto & instanceConn : instanceConns) { + instanceConn.disconnect(); + } + idleconn.disconnect(); + instanceConns.clear(); + desk_track.disconnect(); +} + +SymbolsDialog& SymbolsDialog::getInstance() +{ + return *new SymbolsDialog(); +} + +void SymbolsDialog::packless() { + if(pack_size < 4) { + pack_size++; + rebuild(); + } +} + +void SymbolsDialog::packmore() { + if(pack_size > 0) { + pack_size--; + rebuild(); + } +} + +void SymbolsDialog::zoomin() { + if(scale_factor < 4) { + scale_factor++; + rebuild(); + } +} + +void SymbolsDialog::zoomout() { + if(scale_factor > -8) { + scale_factor--; + rebuild(); + } +} + +void SymbolsDialog::rebuild() { + + if (!sensitive) { + return; + } + + if( fit_symbol->get_active() ) { + zoom_in->set_sensitive( false ); + zoom_out->set_sensitive( false ); + } else { + zoom_in->set_sensitive( true); + zoom_out->set_sensitive( true ); + } + store->clear(); + SPDocument* symbol_document = selectedSymbols(); + icons_found = false; + //We are not in search all docs + if (search->get_text() != _("Searching...") && search->get_text() != _("Loading all symbols...")) { + Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text()); + if (current == ALLDOCS && search->get_text() != "") { + searchsymbols(); + return; + } + } + if (symbol_document) { + addSymbolsInDoc(symbol_document); + } else { + showOverlay(); + } +} +void SymbolsDialog::showOverlay() { + Glib::ustring current = Glib::Markup::escape_text(symbol_set->get_active_text()); + if (current == ALLDOCS && !l.size()) + { + overlay_icon->hide(); + if (!all_docs_processed ) { + overlay_icon->show(); + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + + Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("First search can be slow.")) + Glib::ustring("</span>")); + } else if (!icons_found && !search_str.empty()) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term.")) + Glib::ustring("</span>")); + } else { + overlay_icon->show(); + overlay_title->set_markup(Glib::ustring("<spansize=\"large\">") + + Glib::ustring(_("Search in all symbol sets...")) + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + Glib::ustring("</span>")); + } + } else if (!number_symbols && (current != CURRENTDOC || !search_str.empty())) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + + Glib::ustring("</span>")); + } else if (!number_symbols && current == CURRENTDOC) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No symbols found")) + + Glib::ustring("</span>")); + overlay_desc->set_markup( + Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("No symbols in current document.\nChoose a different symbol set\nor add a new symbol.")) + + Glib::ustring("</span>")); + } else if (!icons_found && !search_str.empty()) { + overlay_title->set_markup(Glib::ustring("<span size=\"large\">") + Glib::ustring(_("No results found")) + + Glib::ustring("</span>")); + overlay_desc->set_markup(Glib::ustring("<span size=\"small\">") + + Glib::ustring(_("Try a different search term,\nor switch to a different symbol set.")) + + Glib::ustring("</span>")); + } + gint width = scroller->get_allocated_width(); + gint height = scroller->get_allocated_height(); + if (previous_height != height || previous_width != width) { + previous_height = height; + previous_width = width; + overlay_opacity->set_size_request(width, height); + overlay_opacity->set(getOverlay(width, height)); + } + overlay_opacity->hide(); + overlay_icon->show(); + overlay_title->show(); + overlay_desc->show(); + if (l.size()) { + overlay_opacity->show(); + overlay_icon->hide(); + overlay_title->hide(); + overlay_desc->hide(); + } +} + +void SymbolsDialog::hideOverlay() { + overlay_opacity->hide(); + overlay_icon->hide(); + overlay_title->hide(); + overlay_desc->hide(); +} + +void SymbolsDialog::insertSymbol() { + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_SYMBOL ); + SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop) ); + sp_action_perform (action, nullptr); +} + +void SymbolsDialog::revertSymbol() { + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_EDIT_UNSYMBOL ); + SPAction *action = verb->get_action(Inkscape::ActionContext( (Inkscape::UI::View::View *) current_desktop ) ); + sp_action_perform (action, nullptr); +} + +void SymbolsDialog::iconDragDataGet(const Glib::RefPtr<Gdk::DragContext>& /*context*/, Gtk::SelectionData& data, guint /*info*/, guint /*time*/) +{ + auto iconArray = icon_view->get_selected_items(); + + if( iconArray.empty() ) { + //std::cout << " iconArray empty: huh? " << std::endl; + } else { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + Glib::ustring symbol_id = (*row)[getColumns()->symbol_id]; + GdkAtom dataAtom = gdk_atom_intern( "application/x-inkscape-paste", FALSE ); + gtk_selection_data_set( data.gobj(), dataAtom, 9, (guchar*)symbol_id.c_str(), symbol_id.length() ); + } + +} + +void SymbolsDialog::defsModified(SPObject * /*object*/, guint /*flags*/) +{ + Glib::ustring doc_title = symbol_set->get_active_text(); + if (doc_title != ALLDOCS && !symbol_sets[doc_title] ) { + rebuild(); + } +} + +void SymbolsDialog::selectionChanged(Inkscape::Selection *selection) { + Glib::ustring symbol_id = selectedSymbolId(); + Glib::ustring doc_title = selectedSymbolDocTitle(); + if (!doc_title.empty()) { + SPDocument* symbol_document = symbol_sets[doc_title]; + if (!symbol_document) { + //we are in global search so get the original symbol document by title + symbol_document = selectedSymbols(); + } + if (symbol_document) { + SPObject* symbol = symbol_document->getObjectById(symbol_id); + if(symbol && !selection->includes(symbol)) { + icon_view->unselect_all(); + } + } + } +} + +void SymbolsDialog::documentReplaced(SPDesktop *desktop, SPDocument *document) +{ + current_desktop = desktop; + current_document = document; + rebuild(); +} + +SPDocument* SymbolsDialog::selectedSymbols() { + /* OK, we know symbol name... now we need to copy it to clipboard, bon chance! */ + Glib::ustring doc_title = symbol_set->get_active_text(); + if (doc_title == ALLDOCS) { + return nullptr; + } + SPDocument* symbol_document = symbol_sets[doc_title]; + if( !symbol_document ) { + symbol_document = getSymbolsSet(doc_title).second; + // Symbol must be from Current Document (this method of checking should be language independent). + if( !symbol_document ) { + // Symbol must be from Current Document (this method of + // checking should be language independent). + symbol_document = current_document; + add_symbol->set_sensitive( true ); + remove_symbol->set_sensitive( true ); + } else { + add_symbol->set_sensitive( false ); + remove_symbol->set_sensitive( false ); + } + } + return symbol_document; +} + +Glib::ustring SymbolsDialog::selectedSymbolId() { + + auto iconArray = icon_view->get_selected_items(); + + if( !iconArray.empty() ) { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + return (*row)[getColumns()->symbol_id]; + } + return Glib::ustring(""); +} + +Glib::ustring SymbolsDialog::selectedSymbolDocTitle() { + + auto iconArray = icon_view->get_selected_items(); + + if( !iconArray.empty() ) { + Gtk::TreeModel::Path const & path = *iconArray.begin(); + Gtk::ListStore::iterator row = store->get_iter(path); + return (*row)[getColumns()->symbol_doc_title]; + } + return Glib::ustring(""); +} + +Glib::ustring SymbolsDialog::documentTitle(SPDocument* symbol_doc) { + if (symbol_doc) { + SPRoot * root = symbol_doc->getRoot(); + gchar * title = root->title(); + if (title) { + return ellipsize(Glib::ustring(title), 33); + } + g_free(title); + } + Glib::ustring current = symbol_set->get_active_text(); + if (current == CURRENTDOC) { + return current; + } + return _("Untitled document"); +} + +void SymbolsDialog::iconChanged() { + + Glib::ustring symbol_id = selectedSymbolId(); + SPDocument* symbol_document = selectedSymbols(); + if (!symbol_document) { + //we are in global search so get the original symbol document by title + Glib::ustring doc_title = selectedSymbolDocTitle(); + if (!doc_title.empty()) { + symbol_document = symbol_sets[doc_title]; + } + } + if (symbol_document) { + SPObject* symbol = symbol_document->getObjectById(symbol_id); + + if( symbol ) { + if( symbol_document == current_document ) { + // Select the symbol on the canvas so it can be manipulated + current_desktop->selection->set( symbol, false ); + } + // Find style for use in <use> + // First look for default style stored in <symbol> + gchar const* style = symbol->getAttribute("inkscape:symbol-style"); + if( !style ) { + // If no default style in <symbol>, look in documents. + if( symbol_document == current_document ) { + style = styleFromUse( symbol_id.c_str(), current_document ); + } else { + style = symbol_document->getReprRoot()->attribute("style"); + } + } + + ClipboardManager *cm = ClipboardManager::get(); + cm->copySymbol(symbol->getRepr(), style, symbol_document == current_document); + } + } +} + +#ifdef WITH_LIBVISIO + +#if WITH_LIBVISIO01 +// Extend libvisio's native RVNGSVGDrawingGenerator with support for extracting stencil names (to be used as ID/title) +class REVENGE_API RVNGSVGDrawingGenerator_WithTitle : public RVNGSVGDrawingGenerator { + public: + RVNGSVGDrawingGenerator_WithTitle(RVNGStringVector &output, RVNGStringVector &titles, const RVNGString &nmSpace) + : RVNGSVGDrawingGenerator(output, nmSpace) + , _titles(titles) + {} + + void startPage(const RVNGPropertyList &propList) override + { + RVNGSVGDrawingGenerator::startPage(propList); + if (propList["draw:name"]) { + _titles.append(propList["draw:name"]->getStr()); + } else { + _titles.append(""); + } + } + + private: + RVNGStringVector &_titles; +}; +#endif + +// Read Visio stencil files +SPDocument* read_vss(Glib::ustring filename, Glib::ustring name ) { + gchar *fullname; + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + fullname = g_win32_locale_filename_from_utf8(filename.c_str()); + #else + fullname = strdup(filename.c_str()); + #endif + + RVNGFileStream input(fullname); + g_free(fullname); + + if (!libvisio::VisioDocument::isSupported(&input)) { + return nullptr; + } + RVNGStringVector output; + RVNGStringVector titles; +#if WITH_LIBVISIO01 + RVNGSVGDrawingGenerator_WithTitle generator(output, titles, "svg"); + + if (!libvisio::VisioDocument::parseStencils(&input, &generator)) { +#else + if (!libvisio::VisioDocument::generateSVGStencils(&input, output)) { +#endif + return nullptr; + } + if (output.empty()) { + return nullptr; + } + + // prepare a valid title for the symbol file + Glib::ustring title = Glib::Markup::escape_text(name); + // prepare a valid id prefix for symbols libvisio doesn't give us a name for + Glib::RefPtr<Glib::Regex> regex1 = Glib::Regex::create("[^a-zA-Z0-9_-]"); + Glib::ustring id = regex1->replace(name, 0, "_", Glib::REGEX_MATCH_PARTIAL); + + Glib::ustring tmpSVGOutput; + tmpSVGOutput += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"; + tmpSVGOutput += "<svg\n"; + tmpSVGOutput += " xmlns=\"http://www.w3.org/2000/svg\"\n"; + tmpSVGOutput += " xmlns:svg=\"http://www.w3.org/2000/svg\"\n"; + tmpSVGOutput += " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"; + tmpSVGOutput += " version=\"1.1\"\n"; + tmpSVGOutput += " style=\"fill:none;stroke:#000000;stroke-width:2\">\n"; + tmpSVGOutput += " <title>"; + tmpSVGOutput += title; + tmpSVGOutput += "\n"; + tmpSVGOutput += " \n"; + + // Each "symbol" is in its own SVG file, we wrap with and merge into one file. + for (unsigned i=0; ireplace(titles[i].cstr(), 0, "_", Glib::REGEX_MATCH_PARTIAL); + } else { + ss << id << "_" << i; + } + + tmpSVGOutput += " \n"; + +#if WITH_LIBVISIO01 + if (titles.size() == output.size() && titles[i] != "") { + tmpSVGOutput += " " + Glib::ustring(RVNGString::escapeXML(titles[i].cstr()).cstr()) + "\n"; + } +#endif + + std::istringstream iss( output[i].cstr() ); + std::string line; + while( std::getline( iss, line ) ) { + if( line.find( "svg:svg" ) == std::string::npos ) { + tmpSVGOutput += " " + line + "\n"; + } + } + + tmpSVGOutput += " \n"; + } + + tmpSVGOutput += " \n"; + tmpSVGOutput += "\n"; + return SPDocument::createNewDocFromMem( tmpSVGOutput.c_str(), strlen( tmpSVGOutput.c_str()), false ); + +} +#endif + +/* Hunts preference directories for symbol files */ +void SymbolsDialog::getSymbolsTitle() { + + using namespace Inkscape::IO::Resource; + Glib::ustring title; + number_docs = 0; + std::regex matchtitle (".*?(.*?)<(/| /)"); + for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) { + if(Glib::str_has_suffix(filename, ".vss")) { + std::size_t found = filename.find_last_of("/\\"); + filename = filename.substr(found+1); + title = filename.erase(filename.rfind('.')); + if(title.empty()) { + title = _("Unnamed Symbols"); + } + symbol_sets[title]= nullptr; + ++number_docs; + } else { + std::ifstream infile(filename); + std::string line; + while (std::getline(infile, line)) { + std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy); + if (!title_res.empty()) { + title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str()); + symbol_sets[ellipsize(Glib::ustring(title_res), 33)]= nullptr; + ++number_docs; + break; + } + std::string::size_type position_exit = line.find ("append(symbol_document_map.first); + } +} + +/* Hunts preference directories for symbol files */ +std::pair +SymbolsDialog::getSymbolsSet(Glib::ustring title) +{ + SPDocument* symbol_doc = nullptr; + Glib::ustring current = symbol_set->get_active_text(); + if (current == CURRENTDOC) { + return std::make_pair(CURRENTDOC, symbol_doc); + } + if (symbol_sets[title]) { + sensitive = false; + symbol_set->remove_all(); + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + for(auto const &symbol_document_map : symbol_sets) { + if (CURRENTDOC != symbol_document_map.first) { + symbol_set->append(symbol_document_map.first); + } + } + symbol_set->set_active_text(title); + sensitive = true; + return std::make_pair(title, symbol_sets[title]); + } + using namespace Inkscape::IO::Resource; + Glib::ustring new_title; + + std::regex matchtitle (".*?(.*?)<(/| /)"); + for(auto &filename: get_filenames(SYMBOLS, {".svg", ".vss"})) { + if(Glib::str_has_suffix(filename, ".vss")) { +#ifdef WITH_LIBVISIO + std::size_t pos = filename.find_last_of("/\\"); + Glib::ustring filename_short = ""; + if (pos != std::string::npos) { + filename_short = filename.substr(pos+1); + } + if (filename_short == title + ".vss") { + new_title = title; + symbol_doc = read_vss(Glib::ustring(filename), title); + } +#endif + } else { + std::ifstream infile(filename); + std::string line; + while (std::getline(infile, line)) { + std::string title_res = std::regex_replace (line, matchtitle,"$1",std::regex_constants::format_no_copy); + if (!title_res.empty()) { + title_res = g_dpgettext2(nullptr, "Symbol", title_res.c_str()); + new_title = ellipsize(Glib::ustring(title_res), 33); + } + std::size_t pos = filename.find_last_of("/\\"); + Glib::ustring filename_short = ""; + if (pos != std::string::npos) { + filename_short = filename.substr(pos+1); + } + if (title == new_title || filename_short == title + ".svg") { + new_title = title; + if(Glib::str_has_suffix(filename, ".svg")) { + symbol_doc = SPDocument::createNewDoc(filename.c_str(), FALSE); + } + } + if (symbol_doc) { + break; + } + std::string::size_type position_exit = line.find ("remove_all(); + symbol_set->append(CURRENTDOC); + symbol_set->append(ALLDOCS); + for(auto const &symbol_document_map : symbol_sets) { + if (CURRENTDOC != symbol_document_map.first) { + symbol_set->append(symbol_document_map.first); + } + } + symbol_set->set_active_text(new_title); + sensitive = true; + } + return std::make_pair(new_title, symbol_doc); +} + +void SymbolsDialog::symbolsInDocRecursive (SPObject *r, std::map > &l, Glib::ustring doc_title) +{ + if(!r) return; + + // Stop multiple counting of same symbol + if ( dynamic_cast(r) ) { + return; + } + + if ( dynamic_cast(r)) { + Glib::ustring id = r->getAttribute("id"); + gchar * title = r->title(); + if(title) { + l[doc_title + title + id] = std::make_pair(doc_title,dynamic_cast(r)); + } else { + l[Glib::ustring(_("notitle_")) + id] = std::make_pair(doc_title,dynamic_cast(r)); + } + g_free(title); + } + for (auto& child: r->children) { + symbolsInDocRecursive(&child, l, doc_title); + } +} + +std::map > +SymbolsDialog::symbolsInDoc( SPDocument* symbol_document, Glib::ustring doc_title) +{ + + std::map > l; + if (symbol_document) { + symbolsInDocRecursive (symbol_document->getRoot(), l , doc_title); + } + return l; +} + +void SymbolsDialog::useInDoc (SPObject *r, std::vector &l) +{ + + if ( dynamic_cast(r) ) { + l.push_back(dynamic_cast(r)); + } + + for (auto& child: r->children) { + useInDoc( &child, l ); + } +} + +std::vector SymbolsDialog::useInDoc( SPDocument* useDocument) { + std::vector l; + useInDoc (useDocument->getRoot(), l); + return l; +} + +// Returns style from first element found that references id. +// This is a last ditch effort to find a style. +gchar const* SymbolsDialog::styleFromUse( gchar const* id, SPDocument* document) { + + gchar const* style = nullptr; + std::vector l = useInDoc( document ); + for( auto use:l ) { + if ( use ) { + gchar const *href = use->getRepr()->attribute("xlink:href"); + if( href ) { + Glib::ustring href2(href); + Glib::ustring id2(id); + id2 = "#" + id2; + if( !href2.compare(id2) ) { + style = use->getRepr()->attribute("style"); + break; + } + } + } + } + return style; +} + +void SymbolsDialog::clearSearch() +{ + if(search->get_text().empty() && sensitive) { + enableWidgets(false); + search_str = ""; + store->clear(); + SPDocument* symbol_document = selectedSymbols(); + if (symbol_document) { + //We are not in search all docs + icons_found = false; + addSymbolsInDoc(symbol_document); + } else { + showOverlay(); + enableWidgets(true); + } + } +} + +void SymbolsDialog::enableWidgets(bool enable) +{ + symbol_set->set_sensitive(enable); + search->set_sensitive(enable); + tools ->set_sensitive(enable); +} + +void SymbolsDialog::beforeSearch(GdkEventKey* evt) +{ + sensitive = false; + search_str = search->get_text().lowercase(); + if (evt->keyval != GDK_KEY_Return) { + return; + } + searchsymbols(); +} + +void SymbolsDialog::searchsymbols() +{ + progress_bar->set_fraction(0.0); + enableWidgets(false); + SPDocument *symbol_document = selectedSymbols(); + if (symbol_document) { + // We are not in search all docs + search->set_text(_("Searching...")); + store->clear(); + icons_found = false; + addSymbolsInDoc(symbol_document); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect(sigc::mem_fun(*this, &SymbolsDialog::callbackAllSymbols)); + search->set_text(_("Loading all symbols...")); + } +} + +void SymbolsDialog::unsensitive(GdkEventKey* evt) +{ + sensitive = true; +} + +bool SymbolsDialog::callbackSymbols(){ + if (l.size()) { + showOverlay(); + for (auto symbol_data = l.begin(); symbol_data != l.end();) { + Glib::ustring doc_title = symbol_data->second.first; + SPSymbol * symbol = symbol_data->second.second; + counter_symbols ++; + gchar *symbol_title_char = symbol->title(); + gchar *symbol_desc_char = symbol->description(); + bool found = false; + if (symbol_title_char) { + Glib::ustring symbol_title = Glib::ustring(symbol_title_char).lowercase(); + auto pos = symbol_title.rfind(search_str); + if (pos != std::string::npos) { + found = true; + } + if (!found && symbol_desc_char) { + Glib::ustring symbol_desc = Glib::ustring(symbol_desc_char).lowercase(); + auto pos = symbol_desc.rfind(search_str); + if (pos != std::string::npos) { + found = true; + } + } + } + if (symbol && (search_str.empty() || found)) { + addSymbol( symbol, doc_title); + icons_found = true; + } + + progress_bar->set_fraction(((100.0/number_symbols) * counter_symbols)/100.0); + symbol_data = l.erase(l.begin()); + //to get more items and best performance + int modulus = number_symbols > 200 ? 50 : (number_symbols/4); + g_free(symbol_title_char); + g_free(symbol_desc_char); + if (modulus && counter_symbols % modulus == 0 && !l.empty()) { + return true; + } + } + if (!icons_found && !search_str.empty()) { + showOverlay(); + } else { + hideOverlay(); + } + progress_bar->set_fraction(0); + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + return false; + } + return true; +} + +bool SymbolsDialog::callbackAllSymbols(){ + Glib::ustring current = symbol_set->get_active_text(); + if (current == ALLDOCS && search->get_text() == _("Loading all symbols...")) { + size_t counter = 0; + std::map symbol_sets_tmp = symbol_sets; + for(auto const &symbol_document_map : symbol_sets_tmp) { + ++counter; + SPDocument* symbol_document = symbol_document_map.second; + if (symbol_document) { + continue; + } + symbol_document = getSymbolsSet(symbol_document_map.first).second; + symbol_set->set_active_text(ALLDOCS); + if (!symbol_document) { + continue; + } + progress_bar->set_fraction(((100.0/number_docs) * counter)/100.0); + return true; + } + symbol_sets_tmp.clear(); + hideOverlay(); + all_docs_processed = true; + addSymbols(); + progress_bar->set_fraction(0); + search->set_text("Searching..."); + return false; + } + return true; +} + +Glib::ustring SymbolsDialog::ellipsize(Glib::ustring data, size_t limit) { + if (data.length() > limit) { + data = data.substr(0, limit-3); + return data + "..."; + } + return data; +} + +void SymbolsDialog::addSymbolsInDoc(SPDocument* symbol_document) { + + if (!symbol_document) { + return; //Search all + } + Glib::ustring doc_title = documentTitle(symbol_document); + progress_bar->set_fraction(0.0); + counter_symbols = 0; + l = symbolsInDoc(symbol_document, doc_title); + number_symbols = l.size(); + if (!number_symbols) { + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + idleconn.disconnect(); + showOverlay(); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols)); + } +} + +void SymbolsDialog::addSymbols() { + store->clear(); + icons_found = false; + for(auto const &symbol_document_map : symbol_sets) { + SPDocument* symbol_document = symbol_document_map.second; + if (!symbol_document) { + continue; + } + Glib::ustring doc_title = documentTitle(symbol_document); + std::map > l_tmp = symbolsInDoc(symbol_document, doc_title); + for(auto &p : l_tmp ) { + l[p.first] = p.second; + } + l_tmp.clear(); + } + counter_symbols = 0; + progress_bar->set_fraction(0.0); + number_symbols = l.size(); + if (!number_symbols) { + showOverlay(); + idleconn.disconnect(); + sensitive = false; + search->set_text(search_str); + sensitive = true; + enableWidgets(true); + } else { + idleconn.disconnect(); + idleconn = Glib::signal_idle().connect( sigc::mem_fun(*this, &SymbolsDialog::callbackSymbols)); + } +} + +void SymbolsDialog::addSymbol( SPObject* symbol, Glib::ustring doc_title) +{ + gchar const *id = symbol->getRepr()->attribute("id"); + + if (doc_title.empty()) { + doc_title = CURRENTDOC; + } else { + doc_title = g_dpgettext2(nullptr, "Symbol", doc_title.c_str()); + } + + Glib::ustring symbol_title; + gchar *title = symbol->title(); // From title element + if (title) { + symbol_title = Glib::ustring::compose("%1 (%2)", g_dpgettext2(nullptr, "Symbol", title), doc_title.c_str()); + } else { + symbol_title = Glib::ustring::compose("%1 %2 (%3)", _("Symbol without title"), Glib::ustring(id), doc_title); + } + g_free(title); + + Glib::RefPtr pixbuf = drawSymbol( symbol ); + if( pixbuf ) { + Gtk::ListStore::iterator row = store->append(); + SymbolColumns* columns = getColumns(); + (*row)[columns->symbol_id] = Glib::ustring( id ); + (*row)[columns->symbol_title] = Glib::Markup::escape_text(symbol_title); + (*row)[columns->symbol_doc_title] = Glib::Markup::escape_text(doc_title); + (*row)[columns->symbol_image] = pixbuf; + delete columns; + } +} + +/* + * Returns image of symbol. + * + * Symbols normally are not visible. They must be referenced by a + * element. A temporary document is created with a dummy + * element and a element that references the symbol + * element. Each real symbol is swapped in for the dummy symbol and + * the temporary document is rendered. + */ +Glib::RefPtr +SymbolsDialog::drawSymbol(SPObject *symbol) +{ + // Create a copy repr of the symbol with id="the_symbol" + Inkscape::XML::Document *xml_doc = preview_document->getReprDoc(); + Inkscape::XML::Node *repr = symbol->getRepr()->duplicate(xml_doc); + repr->setAttribute("id", "the_symbol"); + + // Replace old "the_symbol" in preview_document by new. + Inkscape::XML::Node *root = preview_document->getReprRoot(); + SPObject *symbol_old = preview_document->getObjectById("the_symbol"); + if (symbol_old) { + symbol_old->deleteObject(false); + } + + // First look for default style stored in + gchar const* style = repr->attribute("inkscape:symbol-style"); + if( !style ) { + // If no default style in , look in documents. + if( symbol->document == current_document ) { + gchar const *id = symbol->getRepr()->attribute("id"); + style = styleFromUse( id, symbol->document ); + } else { + style = symbol->document->getReprRoot()->attribute("style"); + } + } + // Last ditch effort to provide some default styling + if( !style ) style = "fill:#bbbbbb;stroke:#808080"; + + // This is for display in Symbols dialog only + if( style ) repr->setAttribute( "style", style ); + + // BUG: Symbols don't work if defined outside of . Causes Inkscape + // crash when trying to read in such a file. + root->appendChild(repr); + //defsrepr->appendChild(repr); + Inkscape::GC::release(repr); + + // Uncomment this to get the preview_document documents saved (useful for debugging) + // FILE *fp = fopen (g_strconcat(id, ".svg", NULL), "w"); + // sp_repr_save_stream(preview_document->getReprDoc(), fp); + // fclose (fp); + + // Make sure preview_document is up-to-date. + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + // Make sure we have symbol in preview_document + SPObject *object_temp = preview_document->getObjectById( "the_use" ); + preview_document->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + preview_document->ensureUpToDate(); + + SPItem *item = dynamic_cast(object_temp); + g_assert(item != nullptr); + unsigned psize = SYMBOL_ICON_SIZES[pack_size]; + + Glib::RefPtr pixbuf(nullptr); + // We could use cache here, but it doesn't really work with the structure + // of this user interface and we've already cached the pixbuf in the gtklist + + // Find object's bbox in document. + // Note symbols can have own viewport... ignore for now. + //Geom::OptRect dbox = item->geometricBounds(); + Geom::OptRect dbox = item->documentVisualBounds(); + + if (dbox) { + /* Scale symbols to fit */ + double scale = 1.0; + double width = dbox->width(); + double height = dbox->height(); + + if( width == 0.0 ) width = 1.0; + if( height == 0.0 ) height = 1.0; + + if( fit_symbol->get_active() ) + scale = psize / ceil(std::max(width, height)); + else + scale = pow( 2.0, scale_factor/2.0 ) * psize / 32.0; + + pixbuf = Glib::wrap(render_pixbuf(renderDrawing, scale, *dbox, psize)); + } + + return pixbuf; +} + +/* + * Return empty doc to render symbols in. + * Symbols are by default not rendered so a element is + * provided. + */ +SPDocument* SymbolsDialog::symbolsPreviewDoc() +{ + // BUG: must be inside + gchar const *buffer = +"" +" " +" " +" " +" " +""; + return SPDocument::createNewDocFromMem( buffer, strlen(buffer), FALSE ); +} + +/* + * Update image widgets + */ +Glib::RefPtr +SymbolsDialog::getOverlay(gint width, gint height) +{ + cairo_surface_t *surface; + cairo_t *cr; + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cr = cairo_create (surface); + cairo_set_source_rgba(cr, 1, 1, 1, 0.75); + cairo_rectangle (cr, 0, 0, width, height); + cairo_fill (cr); + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(surface); + cairo_destroy (cr); + return Glib::wrap(pixbuf); +} + +void SymbolsDialog::setTargetDesktop(SPDesktop *desktop) +{ + if (this->current_desktop != desktop) { + this->current_desktop = desktop; + if( !symbol_sets[symbol_set->get_active_text()] ) { + // Symbol set is from Current document, update + rebuild(); + } + } +} + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-basic-offset:2 + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=2:tabstop=8:softtabstop=2:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/symbols.h b/src/ui/dialog/symbols.h new file mode 100644 index 0000000..382d310 --- /dev/null +++ b/src/ui/dialog/symbols.h @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Symbols dialog + */ +/* Authors: + * Tavmjong Bah, Martin Owens + * + * Copyright (C) 2012 Tavmjong Bah + * 2013 Martin Owens + * 2017 Jabiertxo Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_SYMBOLS_H +#define INKSCAPE_UI_DIALOG_SYMBOLS_H + +#include + +#include + +#include "display/drawing.h" +#include "include/gtkmm_version.h" +#include "ui/dialog/desktop-tracker.h" +#include "ui/widget/panel.h" + +class SPObject; +class SPSymbol; +class SPUse; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class SymbolColumns; // For Gtk::ListStore + +/** + * A dialog that displays selectable symbols and allows users to drag or paste + * those symbols from the dialog into the document. + * + * Symbol documents are loaded from the preferences paths and displayed in a + * drop-down list to the user. The user then selects which of the symbols + * documents they want to get symbols from. The first document in the list is + * always the current document. + * + * This then updates an icon-view with all the symbols available. Selecting one + * puts it onto the clipboard. Dragging it or pasting it onto the canvas copies + * the symbol from the symbol document, into the current document and places a + * new & context, Gtk::SelectionData& selection_data, guint info, guint time); + void getSymbolsTitle(); + Glib::ustring documentTitle(SPDocument* doc); + std::pair getSymbolsSet(Glib::ustring title); + void addSymbol( SPObject* symbol, Glib::ustring doc_title); + SPDocument* symbolsPreviewDoc(); + void symbolsInDocRecursive (SPObject *r, std::map > &l, Glib::ustring doc_title); + std::map > symbolsInDoc( SPDocument* document, Glib::ustring doc_title); + void useInDoc(SPObject *r, std::vector &l); + std::vector useInDoc( SPDocument* document); + void beforeSearch(GdkEventKey* evt); + void unsensitive(GdkEventKey* evt); + void searchsymbols(); + void addSymbols(); + void addSymbolsInDoc(SPDocument* document); + void showOverlay(); + void hideOverlay(); + void clearSearch(); + bool callbackSymbols(); + bool callbackAllSymbols(); + void enableWidgets(bool enable); + Glib::ustring ellipsize(Glib::ustring data, size_t limit); + gchar const* styleFromUse( gchar const* id, SPDocument* document); + Glib::RefPtr drawSymbol(SPObject *symbol); + Glib::RefPtr getOverlay(gint width, gint height); + /* Keep track of all symbol template documents */ + std::map symbol_sets; + std::map > l; + // Index into sizes which is selected + int pack_size; + // Scale factor + int scale_factor; + bool sensitive; + double previous_height; + double previous_width; + bool all_docs_processed; + size_t number_docs; + size_t number_symbols; + size_t counter_symbols; + bool icons_found; + Glib::RefPtr store; + Glib::ustring search_str; + Gtk::ComboBoxText* symbol_set; + Gtk::ProgressBar* progress_bar; + Gtk::HBox* progress; + Gtk::SearchEntry* search; + Gtk::IconView* icon_view; + Gtk::Button* add_symbol; + Gtk::Button* remove_symbol; + Gtk::Button* zoom_in; + Gtk::Button* zoom_out; + Gtk::Button* more; + Gtk::Button* fewer; + Gtk::HBox* tools; + Gtk::Overlay* overlay; + Gtk::Image* overlay_icon; + Gtk::Image* overlay_opacity; + Gtk::Label* overlay_title; + Gtk::Label* overlay_desc; + Gtk::ScrolledWindow *scroller; + Gtk::ToggleButton* fit_symbol; + Gtk::IconSize iconsize; + void setTargetDesktop(SPDesktop *desktop); + SPDesktop* current_desktop; + DesktopTracker desk_track; + SPDocument* current_document; + SPDocument* preview_document; /* Document to render single symbol */ + + sigc::connection idleconn; + + /* For rendering the template drawing */ + unsigned key; + Inkscape::Drawing renderDrawing; + + std::vector instanceConns; +}; + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + +#endif // INKSCAPE_UI_DIALOG_SYMBOLS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tags.cpp b/src/ui/dialog/tags.cpp new file mode 100644 index 0000000..35400ab --- /dev/null +++ b/src/ui/dialog/tags.cpp @@ -0,0 +1,1125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple panel for tags + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tags.h" +#include +#include +#include + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "verbs.h" + +#include "helper/action.h" +#include "include/gtkmm_version.h" +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-object-group.h" +#include "svg/css-ostringstream.h" +#include "ui/icon-loader.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/iconrenderer.h" +#include "ui/widget/layertypeicon.h" +#include "xml/node-observer.h" + +//#define DUMP_LAYERS 1 + +namespace Inkscape { +namespace UI { +namespace Dialog { + +using Inkscape::XML::Node; + +TagsPanel& TagsPanel::getInstance() +{ + return *new TagsPanel(); +} + +enum { + COL_ADD = 1 +}; + +enum { + BUTTON_NEW = 0, + BUTTON_TOP, + BUTTON_BOTTOM, + BUTTON_UP, + BUTTON_DOWN, + BUTTON_DELETE, + DRAGNDROP +}; + +class TagsPanel::ObjectWatcher : public Inkscape::XML::NodeObserver { +public: + ObjectWatcher(TagsPanel* pnl, SPObject* obj, Inkscape::XML::Node * repr) : + _pnl(pnl), + _obj(obj), + _repr(repr), + _labelAttr(g_quark_from_string("inkscape:label")) + {} + + ObjectWatcher(TagsPanel* pnl, SPObject* obj) : + _pnl(pnl), + _obj(obj), + _repr(obj->getRepr()), + _labelAttr(g_quark_from_string("inkscape:label")) + {} + + void notifyChildAdded( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyChildRemoved( Node &/*node*/, Node &/*child*/, Node */*prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyChildOrderChanged( Node &/*node*/, Node &/*child*/, Node */*old_prev*/, Node */*new_prev*/ ) override + { + if ( _pnl && _obj ) { + _pnl->_objectsChanged( _obj ); + } + } + void notifyContentChanged( Node &/*node*/, Util::ptr_shared /*old_content*/, Util::ptr_shared /*new_content*/ ) override {} + void notifyAttributeChanged( Node &/*node*/, GQuark name, Util::ptr_shared /*old_value*/, Util::ptr_shared /*new_value*/ ) override { + + static GQuark const _labelID = g_quark_from_string("id"); + + if ( _pnl && _obj ) { + if ( name == _labelAttr || name == _labelID ) { + _pnl->_updateObject( _obj); + } + } + } + + TagsPanel* _pnl; + SPObject* _obj; + Inkscape::XML::Node* _repr; + GQuark _labelAttr; +}; + +class TagsPanel::InternalUIBounce +{ +public: + int _actionCode; +}; + +void TagsPanel::_styleButton(Gtk::Button& btn, char const* iconName, char const* tooltip) +{ + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn.add(*manage(Glib::wrap(child))); + btn.set_relief(Gtk::RELIEF_NONE); + btn.set_tooltip_text (tooltip); +} + + +Gtk::MenuItem& TagsPanel::_addPopupItem( SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback, int id ) +{ + GtkWidget* iconWidget = nullptr; + const char* label = nullptr; + + if ( iconName ) { + iconWidget = sp_get_icon_image(iconName, GTK_ICON_SIZE_MENU); + } + + if ( desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(desktop); + if ( !iconWidget && action && action->image ) { + iconWidget = sp_get_icon_image(action->image, GTK_ICON_SIZE_MENU); + } + + if ( action ) { + // label = action->name; + } + } + } + + if ( !label && fallback ) { + label = fallback; + } + + Gtk::Widget* wrapped = nullptr; + if ( iconWidget ) { + wrapped = Gtk::manage(Glib::wrap(iconWidget)); + wrapped->show(); + } + + + Gtk::MenuItem* item = nullptr; + + if (wrapped) { + item = Gtk::manage(new Gtk::ImageMenuItem(*wrapped, label, true)); + } else { + item = Gtk::manage(new Gtk::MenuItem(label, true)); + } + + item->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &TagsPanel::_takeAction), id)); + _popupMenu.append(*item); + + return *item; +} + +void TagsPanel::_fireAction( unsigned int code ) +{ + if ( _desktop ) { + Verb *verb = Verb::get( code ); + if ( verb ) { + SPAction *action = verb->get_action(_desktop); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +void TagsPanel::_takeAction( int val ) +{ + if ( !_pending ) { + _pending = new InternalUIBounce(); + _pending->_actionCode = val; + Glib::signal_timeout().connect( sigc::mem_fun(*this, &TagsPanel::_executeAction), 0 ); + } +} + +bool TagsPanel::_executeAction() +{ + // Make sure selected layer hasn't changed since the action was triggered + if ( _pending) + { + int val = _pending->_actionCode; +// SPObject* target = _pending->_target; + bool empty = _desktop->selection->isEmpty(); + + switch ( val ) { + case BUTTON_NEW: + { + _fireAction( SP_VERB_TAG_NEW ); + } + break; + case BUTTON_TOP: + { + _fireAction( empty ? SP_VERB_LAYER_TO_TOP : SP_VERB_SELECTION_TO_FRONT); + } + break; + case BUTTON_BOTTOM: + { + _fireAction( empty ? SP_VERB_LAYER_TO_BOTTOM : SP_VERB_SELECTION_TO_BACK ); + } + break; + case BUTTON_UP: + { + _fireAction( empty ? SP_VERB_LAYER_RAISE : SP_VERB_SELECTION_RAISE ); + } + break; + case BUTTON_DOWN: + { + _fireAction( empty ? SP_VERB_LAYER_LOWER : SP_VERB_SELECTION_LOWER ); + } + break; + case BUTTON_DELETE: + { + std::vector todelete; + _tree.get_selection()->selected_foreach_iter(sigc::bind*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + for (auto obj : todelete) { + if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + //obj->parent->getRepr()->removeChild(obj->getRepr()); + obj->deleteObject(true, true); + } + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + break; + case DRAGNDROP: + { + _doTreeMove( ); + } + break; + } + + delete _pending; + _pending = nullptr; + } + + return false; +} + + +class TagsPanel::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colParentObject); + add(_colObject); + add(_colLabel); + add(_colAddRemove); + add(_colAllowAddRemove); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn _colParentObject; + Gtk::TreeModelColumn _colObject; + Gtk::TreeModelColumn _colLabel; + Gtk::TreeModelColumn _colAddRemove; + Gtk::TreeModelColumn _colAllowAddRemove; +}; + +void TagsPanel::_checkForDeleted(const Gtk::TreeIter& iter, std::vector* todelete) +{ + Gtk::TreeRow row = *iter; + SPObject * obj = row[_model->_colObject]; + if (obj && obj->parent) { + todelete->push_back(obj); + } +} + +void TagsPanel::_updateObject( SPObject *obj ) { + _store->foreach( sigc::bind(sigc::mem_fun(*this, &TagsPanel::_checkForUpdated), obj) ); +} + +bool TagsPanel::_checkForUpdated(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj) +{ + Gtk::TreeModel::Row row = *iter; + if ( obj == row[_model->_colObject] ) + { + /* + * We get notified of layer update here (from layer->setLabel()) before layer->label() is set + * with the correct value (sp-object bug?). So use the inkscape:label attribute instead which + * has the correct value (bug #168351) + */ + //row[_model->_colLabel] = layer->label() ? layer->label() : layer->getId(); + gchar const *label; + SPTagUse * use = SP_IS_TAG_USE(obj) ? SP_TAG_USE(obj) : nullptr; + if (use && use->ref->isAttached()) { + label = use->ref->getObject()->getAttribute("inkscape:label"); + if (!label || !label[0]) { + label = use->ref->getObject()->getId(); + } + } else { + label = obj->getAttribute("inkscape:label"); + } + row[_model->_colLabel] = label ? label : obj->getId(); + row[_model->_colAddRemove] = SP_IS_TAG(obj); + } + + return false; +} + +void TagsPanel::_objectsSelected( Selection *sel ) { + + _selectedConnection.block(); + _tree.get_selection()->unselect_all(); + auto tmp = sel->objects(); + for(auto i = tmp.begin(); i != tmp.end(); ++i) + { + SPObject *obj = *i; + _store->foreach(sigc::bind( sigc::mem_fun(*this, &TagsPanel::_checkForSelected), obj)); + } + _selectedConnection.unblock(); + _checkTreeSelection(); +} + +bool TagsPanel::_checkForSelected(const Gtk::TreePath &/*path*/, const Gtk::TreeIter& iter, SPObject* obj) +{ + Gtk::TreeModel::Row row = *iter; + SPObject * it = row[_model->_colObject]; + if ( it && SP_IS_TAG_USE(it) && SP_TAG_USE(it)->ref->getObject() == obj ) + { + Glib::RefPtr select = _tree.get_selection(); + + select->select(iter); + } + return false; +} + +// TODO does not look good that we're ignoring the passed in root. Investigate. +void TagsPanel::_objectsChanged(SPObject* root) +{ + while (!_objectWatchers.empty()) + { + TagsPanel::ObjectWatcher *w = _objectWatchers.back(); + w->_repr->removeObserver(*w); + _objectWatchers.pop_back(); + delete w; + } + + if (_desktop) { + SPDocument* document = _desktop->doc(); + SPDefs* root = document->getDefs(); + if ( root ) { + _selectedConnection.block(); + _store->clear(); + _addObject( document, root, nullptr ); + _selectedConnection.unblock(); + _objectsSelected(_desktop->selection); + _checkTreeSelection(); + } + } +} + +void TagsPanel::_addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow ) +{ + if ( _desktop && obj ) { + for (auto& child: obj->children) { + if (SP_IS_TAG(&child)) + { + Gtk::TreeModel::iterator iter = parentRow ? _store->prepend(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = &child; + row[_model->_colParentObject] = NULL; + row[_model->_colLabel] = child.label() ? child.label() : child.getId(); + row[_model->_colAddRemove] = 1; + row[_model->_colAllowAddRemove] = true; + + _tree.expand_to_path( _store->get_path(iter) ); + + TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child); + child.getRepr()->addObserver(*w); + _objectWatchers.push_back(w); + _addObject( doc, &child, &row ); + } + } + if (SP_IS_TAG(obj) && obj->firstChild()) + { + Gtk::TreeModel::iterator iteritems = parentRow ? _store->append(parentRow->children()) : _store->prepend(); + Gtk::TreeModel::Row rowitems = *iteritems; + rowitems[_model->_colObject] = NULL; + rowitems[_model->_colParentObject] = obj; + rowitems[_model->_colLabel] = _("Items"); + rowitems[_model->_colAddRemove] = 0; + rowitems[_model->_colAllowAddRemove] = false; + + _tree.expand_to_path( _store->get_path(iteritems) ); + + for (auto& child: obj->children) { + if (SP_IS_TAG_USE(&child)) + { + SPItem *item = SP_TAG_USE(&child)->ref->getObject(); + Gtk::TreeModel::iterator iter = _store->prepend(rowitems->children()); + Gtk::TreeModel::Row row = *iter; + row[_model->_colObject] = &child; + row[_model->_colParentObject] = NULL; + row[_model->_colLabel] = item ? (item->label() ? item->label() : item->getId()) : SP_TAG_USE(&child)->href; + row[_model->_colAddRemove] = 0; + row[_model->_colAllowAddRemove] = true; + + if (SP_TAG(obj)->expanded()) { + _tree.expand_to_path( _store->get_path(iter) ); + } + + if (item) { + TagsPanel::ObjectWatcher *w = new TagsPanel::ObjectWatcher(this, &child, item->getRepr()); + item->getRepr()->addObserver(*w); + _objectWatchers.push_back(w); + } + } + } + } + } +} + +void TagsPanel::_select_tag( SPTag * tag ) +{ + for (auto& child: tag->children) { + if (SP_IS_TAG(&child)) { + _select_tag(SP_TAG(&child)); + } else if (SP_IS_TAG_USE(&child)) { + SPObject * obj = SP_TAG_USE(&child)->ref->getObject(); + if (obj) { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(obj->parent); + _desktop->selection->add(obj); + } + } + } +} + +void TagsPanel::_selected_row_callback( const Gtk::TreeModel::iterator& iter ) +{ + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + if (obj) { + if (SP_IS_TAG(obj)) { + _select_tag(SP_TAG(obj)); + } else if (SP_IS_TAG_USE(obj)) { + SPObject * item = SP_TAG_USE(obj)->ref->getObject(); + if (item) { + if (_desktop->selection->isEmpty()) _desktop->setCurrentLayer(item->parent); + _desktop->selection->add(item); + } + } + } + } +} + +void TagsPanel::_pushTreeSelectionToCurrent() +{ + _selectionChangedConnection.block(); + // TODO hunt down the possible API abuse in getting NULL + if ( _desktop && _desktop->currentRoot() ) { + _desktop->selection->clear(); + _tree.get_selection()->selected_foreach_iter( sigc::mem_fun(*this, &TagsPanel::_selected_row_callback)); + } + _selectionChangedConnection.unblock(); + + _checkTreeSelection(); +} + +void TagsPanel::_checkTreeSelection() +{ + bool sensitive = _tree.get_selection()->count_selected_rows() > 0; + bool sensitiveNonTop = true; + bool sensitiveNonBottom = true; +// if ( _tree.get_selection()->count_selected_rows() > 0 ) { +// sensitive = true; +// +// SPObject* inTree = _selectedLayer(); +// if ( inTree ) { +// +// sensitiveNonTop = (Inkscape::Nex(inTree->parent, inTree) != 0); +// sensitiveNonBottom = (Inkscape::previous_layer(inTree->parent, inTree) != 0); +// +// } +// } + + + for (auto & it : _watching) { + it->set_sensitive( sensitive ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( sensitiveNonTop ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( sensitiveNonBottom ); + } +} + +bool TagsPanel::_handleKeyEvent(GdkEventKey *event) +{ + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + case GDK_KEY_F2: { + Gtk::TreeModel::iterator iter = _tree.get_selection()->get_selected(); + if (iter && !_text_renderer->property_editable()) { + Gtk::TreeRow row = *iter; + SPObject * obj = row[_model->_colObject]; + if (obj && SP_IS_TAG(obj)) { + Gtk::TreeModel::Path *path = new Gtk::TreeModel::Path(iter); + // Edit the layer label + _text_renderer->property_editable() = true; + _tree.set_cursor(*path, *_name_column, true); + grab_focus(); + return true; + } + } + } + case GDK_KEY_Delete: { + std::vector todelete; + _tree.get_selection()->selected_foreach_iter(sigc::bind*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + if (!todelete.empty()) { + for (auto obj : todelete) { + if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + //obj->parent->getRepr()->removeChild(obj->getRepr()); + obj->deleteObject(true, true); + } + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + return true; + } + break; + } + return false; +} + +bool TagsPanel::_handleButtonEvent(GdkEventButton* event) +{ + static unsigned doubleclick = 0; + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 3) ) { + // TODO - fix to a better is-popup function + Gtk::TreeModel::Path path; + int x = static_cast(event->x); + int y = static_cast(event->y); + if ( _tree.get_path_at_pos( x, y, path ) ) { + _checkTreeSelection(); +#if GTKMM_CHECK_VERSION(3,22,0) + _popupMenu.popup_at_pointer(reinterpret_cast(event)); +#else + _popupMenu.popup(event->button, event->time); +#endif + if (_tree.get_selection()->is_selected(path)) { + return true; + } + } + } + + if ( (event->type == GDK_BUTTON_PRESS) && (event->button == 1)) { + // Alt left click on the visible/lock columns - eat this event to keep row selection + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (col == _tree.get_column(COL_ADD-1)) { + down_at_add = true; + return true; + } else if ( !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) & _tree.get_selection()->is_selected(path) ) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_noSelection)); + _defer_target = path; + } else { + down_at_add = false; + } + } else { + down_at_add = false; + } + } + + if ( event->type == GDK_BUTTON_RELEASE) { + _tree.get_selection()->set_select_function(sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction)); + } + + // TODO - ImageToggler doesn't seem to handle Shift/Alt clicks - so we deal with them here. + if ( (event->type == GDK_BUTTON_RELEASE) && (event->button == 1)) { + + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) ) { + if (_defer_target) { + if (_defer_target == path && !(event->x == 0 && event->y == 0)) + { + _tree.set_cursor(path, *col, false); + } + _defer_target = Gtk::TreeModel::Path(); + } else { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colObject]; + + if (obj) { + if (col == _tree.get_column(COL_ADD - 1) && down_at_add) { + if (SP_IS_TAG(obj)) { + bool wasadded = false; + auto items= _desktop->selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPObject *newobj = *i; + bool addchild = true; + for (auto& child: obj->children) { + if (SP_IS_TAG_USE(&child) && SP_TAG_USE(&child)->ref->getObject() == newobj) { + addchild = false; + } + } + if (addchild) { + Inkscape::XML::Node *clone = _document->getReprDoc()->createElement("inkscape:tagref"); + clone->setAttribute("xlink:href", g_strdup_printf("#%s", newobj->getRepr()->attribute("id")), false); + obj->appendChild(clone); + wasadded = true; + } + } + if (wasadded) { + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Add selection to set")); + } + } else { + std::vector todelete; + // FIXME unnecessary use of XML tree + _tree.get_selection()->selected_foreach_iter(sigc::bind*>(sigc::mem_fun(*this, &TagsPanel::_checkForDeleted), &todelete)); + if (!todelete.empty()) { + for (auto tobj : todelete) { + if (tobj && tobj->parent && tobj->getRepr() && tobj->parent->getRepr()) { + //tobj->parent->getRepr()->removeChild(tobj->getRepr()); + tobj->deleteObject(true, true); + } + } + } else if (obj && obj->parent && obj->getRepr() && obj->parent->getRepr()) { + obj->parent->getRepr()->removeChild(obj->getRepr()); + } + DocumentUndo::done(_document, SP_VERB_DIALOG_TAGS, _("Remove from selection set")); + } + } + } + } + } + } + + + if ( (event->type == GDK_2BUTTON_PRESS) && (event->button == 1) ) { + doubleclick = 1; + } + + if ( event->type == GDK_BUTTON_RELEASE && doubleclick) { + doubleclick = 0; + Gtk::TreeModel::Path path; + Gtk::TreeViewColumn* col = nullptr; + int x = static_cast(event->x); + int y = static_cast(event->y); + int x2 = 0; + int y2 = 0; + if ( _tree.get_path_at_pos( x, y, path, col, x2, y2 ) && col == _name_column) { + Gtk::TreeModel::Children::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colObject]; + if (obj && (SP_IS_TAG(obj) || (SP_IS_TAG_USE(obj) && SP_TAG_USE(obj)->ref->getObject()))) { + // Double click on the Layer name, enable editing + _text_renderer->property_editable() = true; + _tree.set_cursor (path, *_name_column, true); + grab_focus(); + } + } + } + + return false; +} + +void TagsPanel::_storeDragSource(const Gtk::TreeModel::iterator& iter) +{ + Gtk::TreeModel::Row row = *iter; + SPObject* obj = row[_model->_colObject]; + SPTag* item = ( obj && SP_IS_TAG(obj) ) ? SP_TAG(obj) : nullptr; + if (item) + { + _dnd_source.push_back(item); + } +} + +/* + * Drap and drop within the tree + * Save the drag source and drop target SPObjects and if its a drag between layers or into (sublayer) a layer + */ +bool TagsPanel::_handleDragDrop(const Glib::RefPtr& /*context*/, int x, int y, guint /*time*/) +{ + int cell_x = 0, cell_y = 0; + Gtk::TreeModel::Path target_path; + Gtk::TreeView::Column *target_column; + + _dnd_into = true; + _dnd_target = _document->getDefs(); + _dnd_source.clear(); + _tree.get_selection()->selected_foreach_iter(sigc::mem_fun(*this, &TagsPanel::_storeDragSource)); + + if (_dnd_source.empty()) { + return true; + } + + if (_tree.get_path_at_pos (x, y, target_path, target_column, cell_x, cell_y)) { + // Are we before, inside or after the drop layer + Gdk::Rectangle rect; + _tree.get_background_area (target_path, *target_column, rect); + int cell_height = rect.get_height(); + _dnd_into = (cell_y > (int)(cell_height * 1/3) && cell_y <= (int)(cell_height * 2/3)); + if (cell_y > (int)(cell_height * 2/3)) { + Gtk::TreeModel::Path next_path = target_path; + next_path.next(); + if (_store->iter_is_valid(_store->get_iter(next_path))) { + target_path = next_path; + } else { + // Dragging to the "end" + Gtk::TreeModel::Path up_path = target_path; + up_path.up(); + if (_store->iter_is_valid(_store->get_iter(up_path))) { + // Drop into parent + target_path = up_path; + _dnd_into = true; + } else { + // Drop into the top level + _dnd_target = _document->getDefs(); + _dnd_into = true; + } + } + } + Gtk::TreeModel::iterator iter = _store->get_iter(target_path); + if (_store->iter_is_valid(iter)) { + Gtk::TreeModel::Row row = *iter; + SPObject *obj = row[_model->_colObject]; + SPObject *pobj = row[_model->_colParentObject]; + if (obj) { + if (SP_IS_TAG(obj)) { + _dnd_target = SP_TAG(obj); + } else if (SP_IS_TAG(obj->parent)) { + _dnd_target = SP_TAG(obj->parent); + _dnd_into = true; + } + } else if (pobj && SP_IS_TAG(pobj)) { + _dnd_target = SP_TAG(pobj); + _dnd_into = true; + } else { + return true; + } + } + } + + _takeAction(DRAGNDROP); + + return false; +} + +/* + * Move a layer in response to a drag & drop action + */ +void TagsPanel::_doTreeMove( ) +{ + if (_dnd_target) { + for (auto src : _dnd_source) + { + if (src != _dnd_target) { + src->moveTo(_dnd_target, _dnd_into); + } + } + _desktop->selection->clear(); + // moveTo may have deleted src pointers, don't use them anymore + _dnd_source.clear(); + DocumentUndo::done( _desktop->doc() , SP_VERB_DIALOG_TAGS, + _("Moved sets")); + } +} + + +void TagsPanel::_handleEdited(const Glib::ustring& path, const Glib::ustring& new_text) +{ + Gtk::TreeModel::iterator iter = _tree.get_model()->get_iter(path); + Gtk::TreeModel::Row row = *iter; + + _renameObject(row, new_text); + _text_renderer->property_editable() = false; +} + +void TagsPanel::_handleEditingCancelled() +{ + _text_renderer->property_editable() = false; +} + +void TagsPanel::_renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name) +{ + if ( row && _desktop) { + SPObject* obj = row[_model->_colObject]; + if ( obj ) { + if (SP_IS_TAG(obj)) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + obj->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } else if (SP_IS_TAG_USE(obj) && (obj = SP_TAG_USE(obj)->ref->getObject())) { + gchar const* oldLabel = obj->label(); + if ( !name.empty() && (!oldLabel || name != oldLabel) ) { + obj->setLabel(name.c_str()); + DocumentUndo::done( _desktop->doc() , SP_VERB_NONE, + _("Rename object")); + } + } + } + } +} + +bool TagsPanel::_noSelection( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool /*currentlySelected*/ ) +{ + return false; +} + +bool TagsPanel::_rowSelectFunction( Glib::RefPtr const & /*model*/, Gtk::TreeModel::Path const & /*path*/, bool currentlySelected ) +{ + bool val = true; + if ( !currentlySelected && _toggleEvent ) + { + GdkEvent* event = gtk_get_current_event(); + if ( event ) { + // (keep these checks separate, so we know when to call gdk_event_free() + if ( event->type == GDK_BUTTON_PRESS ) { + GdkEventButton const* target = reinterpret_cast(_toggleEvent); + GdkEventButton const* evtb = reinterpret_cast(event); + + if ( (evtb->window == target->window) + && (evtb->send_event == target->send_event) + && (evtb->time == target->time) + && (evtb->state == target->state) + ) + { + // Ooooh! It's a magic one + val = false; + } + } + gdk_event_free(event); + } + } + return val; +} + +void TagsPanel::_setExpanded(const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& /*path*/, bool isexpanded) +{ + Gtk::TreeModel::Row row = *iter; + + SPObject* obj = row[_model->_colParentObject]; + if (obj && SP_IS_TAG(obj)) + { + SP_TAG(obj)->setExpanded(isexpanded); + obj->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } +} + +/** + * Constructor + */ +TagsPanel::TagsPanel() : + UI::Widget::Panel("/dialogs/tags", SP_VERB_DIALOG_TAGS), + _rootWatcher(nullptr), + deskTrack(), + _desktop(nullptr), + _document(nullptr), + _model(nullptr), + _pending(nullptr), + _toggleEvent(nullptr), + _defer_target(), + desktopChangeConn() +{ + //Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + ModelColumns *zoop = new ModelColumns(); + _model = zoop; + + _store = Gtk::TreeStore::create( *zoop ); + + _tree.set_model( _store ); + _tree.set_headers_visible(false); + _tree.set_reorderable(true); + _tree.enable_model_drag_dest (Gdk::ACTION_MOVE); + + // This string is constructed to use already translated strings. + // The tooltip applies to the whole tree area. It would be better + // if the tooltip was split into parts and only applied to the + // icons but doing that is quite complicated. + Glib::ustring tooltip_string = "'+': "; + tooltip_string += (_("Add selection to set")); + tooltip_string += "; '×': "; + tooltip_string += (_("Remove from selection set")); + _tree.set_tooltip_text( tooltip_string ); + + Inkscape::UI::Widget::IconRenderer * addRenderer = manage( new Inkscape::UI::Widget::IconRenderer()); + addRenderer->add_icon("edit-delete"); + addRenderer->add_icon("list-add"); + + int addColNum = _tree.append_column("type", *addRenderer) - 1; + Gtk::TreeViewColumn *col = _tree.get_column(addColNum); + if ( col ) { + col->add_attribute( addRenderer->property_icon(), _model->_colAddRemove ); + col->add_attribute( addRenderer->property_visible(), _model->_colAllowAddRemove ); + } + + _text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree.append_column("Name", *_text_renderer) - 1; + _name_column = _tree.get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree.set_expander_column( *_tree.get_column(nameColNum) ); + + _tree.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE); + _selectedConnection = _tree.get_selection()->signal_changed().connect( sigc::mem_fun(*this, &TagsPanel::_pushTreeSelectionToCurrent) ); + _tree.get_selection()->set_select_function( sigc::mem_fun(*this, &TagsPanel::_rowSelectFunction) ); + + _tree.signal_drag_drop().connect( sigc::mem_fun(*this, &TagsPanel::_handleDragDrop), false); + _collapsedConnection = _tree.signal_row_collapsed().connect( sigc::bind(sigc::mem_fun(*this, &TagsPanel::_setExpanded), false)); + _expandedConnection = _tree.signal_row_expanded().connect( sigc::bind(sigc::mem_fun(*this, &TagsPanel::_setExpanded), true)); + + _text_renderer->signal_edited().connect( sigc::mem_fun(*this, &TagsPanel::_handleEdited) ); + _text_renderer->signal_editing_canceled().connect( sigc::mem_fun(*this, &TagsPanel::_handleEditingCancelled) ); + + _tree.signal_button_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false ); + _tree.signal_button_release_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleButtonEvent), false ); + _tree.signal_key_press_event().connect( sigc::mem_fun(*this, &TagsPanel::_handleKeyEvent), false ); + + _scroller.add( _tree ); + _scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + _scroller.set_shadow_type(Gtk::SHADOW_IN); + Gtk::Requisition sreq; + Gtk::Requisition sreq_natural; + _scroller.get_preferred_size(sreq_natural, sreq); + int minHeight = 70; + if (sreq.height < minHeight) { + // Set a min height to see the layers when used with Ubuntu liboverlay-scrollbar + _scroller.set_size_request(sreq.width, minHeight); + } + + _layersPage.pack_start( _scroller, Gtk::PACK_EXPAND_WIDGET ); + + _layersPage.pack_end(_buttonsRow, Gtk::PACK_SHRINK); + + _getContents()->pack_start(_layersPage, Gtk::PACK_EXPAND_WIDGET); + + SPDesktop* targetDesktop = getDesktop(); + + Gtk::Button* btn = manage( new Gtk::Button() ); + _styleButton(*btn, "list-add", _("Add a new selection set") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_takeAction), (int)BUTTON_NEW) ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + +// btn = manage( new Gtk::Button("Dup") ); +// btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &LayersPanel::_takeAction), (int)BUTTON_DUPLICATE) ); +// _buttonsRow.add( *btn ); + + btn = manage( new Gtk::Button() ); + _styleButton( *btn, "list-remove", _("Remove Item/Set") ); + btn->signal_clicked().connect( sigc::bind( sigc::mem_fun(*this, &TagsPanel::_takeAction), (int)BUTTON_DELETE) ); + _watching.push_back( btn ); + _buttonsSecondary.pack_start(*btn, Gtk::PACK_SHRINK); + + _buttonsRow.pack_start(_buttonsSecondary, Gtk::PACK_EXPAND_WIDGET); + _buttonsRow.pack_end(_buttonsPrimary, Gtk::PACK_EXPAND_WIDGET); + + // ------------------------------------------------------- + { + _watching.push_back( &_addPopupItem( targetDesktop, SP_VERB_TAG_NEW, nullptr, "Add a new selection set", (int)BUTTON_NEW ) ); + + _popupMenu.show_all_children(); + } + // ------------------------------------------------------- + + + + for (auto & it : _watching) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonTop) { + it->set_sensitive( false ); + } + for (auto & it : _watchingNonBottom) { + it->set_sensitive( false ); + } + + setDesktop( targetDesktop ); + + show_all_children(); + + // restorePanelPrefs(); + + // Connect this up last + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &TagsPanel::setDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); +} + +TagsPanel::~TagsPanel() +{ + + setDesktop(nullptr); + + if ( _model ) + { + delete _model; + _model = nullptr; + } + + if (_pending) { + delete _pending; + _pending = nullptr; + } + + if ( _toggleEvent ) + { + gdk_event_free( _toggleEvent ); + _toggleEvent = nullptr; + } + + desktopChangeConn.disconnect(); + deskTrack.disconnect(); +} + +void TagsPanel::setDocument(SPDesktop* /*desktop*/, SPDocument* document) +{ + while (!_objectWatchers.empty()) + { + TagsPanel::ObjectWatcher *w = _objectWatchers.back(); + w->_repr->removeObserver(*w); + _objectWatchers.pop_back(); + delete w; + } + + if (_rootWatcher) + { + _rootWatcher->_repr->removeObserver(*_rootWatcher); + delete _rootWatcher; + _rootWatcher = nullptr; + } + + _document = document; + + if (document && document->getDefs() && document->getDefs()->getRepr()) + { + _rootWatcher = new TagsPanel::ObjectWatcher(this, document->getDefs()); + document->getDefs()->getRepr()->addObserver(*_rootWatcher); + _objectsChanged(document->getDefs()); + } +} + +void TagsPanel::setDesktop( SPDesktop* desktop ) +{ + Panel::setDesktop(desktop); + + if ( desktop != _desktop ) { + _documentChangedConnection.disconnect(); + _selectionChangedConnection.disconnect(); + if ( _desktop ) { + _desktop = nullptr; + } + + _desktop = Panel::getDesktop(); + if ( _desktop ) { + //setLabel( _desktop->doc()->name ); + _documentChangedConnection = _desktop->connectDocumentReplaced( sigc::mem_fun(*this, &TagsPanel::setDocument)); + _selectionChangedConnection = _desktop->selection->connectChanged( sigc::mem_fun(*this, &TagsPanel::_objectsSelected)); + + setDocument(_desktop, _desktop->doc()); + } + } + deskTrack.setBase(desktop); +} + + + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tags.h b/src/ui/dialog/tags.h new file mode 100644 index 0000000..f8f1215 --- /dev/null +++ b/src/ui/dialog/tags.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for tags UI. + * + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_TAGS_PANEL_H +#define SEEN_TAGS_PANEL_H + +#include +#include +#include +#include +#include +#include "ui/widget/spinbutton.h" +#include "ui/widget/panel.h" +#include "object/sp-tag.h" +#include "object/sp-tag-use.h" +#include "object/sp-tag-use-reference.h" +#include "desktop-tracker.h" +#include "selection.h" +#include "ui/widget/filter-effect-chooser.h" + +class SPObject; +class SPTag; +struct SPColorSelector; + +namespace Inkscape { + +namespace UI { +namespace Dialog { + + +/** + * A panel that displays layers. + */ +class TagsPanel : public UI::Widget::Panel +{ +public: + TagsPanel(); + ~TagsPanel() override; + + static TagsPanel& getInstance(); + + void setDesktop( SPDesktop* desktop ) override; + void setDocument( SPDesktop* desktop, SPDocument* document); + +protected: + friend void sp_highlight_picker_color_mod(SPColorSelector *csel, GObject *cp); +private: + class ModelColumns; + class InternalUIBounce; + class ObjectWatcher; + + TagsPanel(TagsPanel const &) = delete; // no copy + TagsPanel &operator=(TagsPanel const &) = delete; // no assign + + void _styleButton( Gtk::Button& btn, char const* iconName, char const* tooltip ); + void _fireAction( unsigned int code ); + Gtk::MenuItem& _addPopupItem( SPDesktop *desktop, unsigned int code, char const* iconName, char const* fallback, int id ); + + bool _handleButtonEvent(GdkEventButton *event); + bool _handleKeyEvent(GdkEventKey *event); + + void _storeDragSource(const Gtk::TreeModel::iterator& iter); + bool _handleDragDrop(const Glib::RefPtr& context, int x, int y, guint time); + void _handleEdited(const Glib::ustring& path, const Glib::ustring& new_text); + void _handleEditingCancelled(); + + void _doTreeMove(); + void _renameObject(Gtk::TreeModel::Row row, const Glib::ustring& name); + + void _pushTreeSelectionToCurrent(); + void _selected_row_callback( const Gtk::TreeModel::iterator& iter ); + void _select_tag( SPTag * tag ); + + void _checkTreeSelection(); + + void _takeAction( int val ); + bool _executeAction(); + + void _setExpanded( const Gtk::TreeModel::iterator& iter, const Gtk::TreeModel::Path& path, bool isexpanded ); + + bool _noSelection( Glib::RefPtr const & model, Gtk::TreeModel::Path const & path, bool b ); + bool _rowSelectFunction( Glib::RefPtr const & model, Gtk::TreeModel::Path const & path, bool b ); + + void _updateObject(SPObject *obj); + bool _checkForUpdated(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPObject* obj); + + void _objectsSelected(Selection *sel); + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPObject* layer); + + void _objectsChanged(SPObject *root); + void _addObject( SPDocument* doc, SPObject* obj, Gtk::TreeModel::Row* parentRow ); + + void _checkForDeleted(const Gtk::TreeIter& iter, std::vector* todelete); + +// std::vector groupConnections; + TagsPanel::ObjectWatcher* _rootWatcher; + std::vector _objectWatchers; + + // Hooked to the layer manager: + sigc::connection _documentChangedConnection; + sigc::connection _selectionChangedConnection; + + sigc::connection _changedConnection; + sigc::connection _addedConnection; + sigc::connection _removedConnection; + + // Internal + sigc::connection _selectedConnection; + sigc::connection _expandedConnection; + sigc::connection _collapsedConnection; + + DesktopTracker deskTrack; + SPDesktop* _desktop; + SPDocument* _document; + ModelColumns* _model; + InternalUIBounce* _pending; + gboolean _dnd_into; + std::vector _dnd_source; + SPObject* _dnd_target; + + GdkEvent* _toggleEvent; + bool down_at_add; + + Gtk::TreeModel::Path _defer_target; + + Glib::RefPtr _store; + std::vector _watching; + std::vector _watchingNonTop; + std::vector _watchingNonBottom; + + Gtk::TreeView _tree; + Gtk::CellRendererText *_text_renderer; + Gtk::TreeView::Column *_name_column; + Gtk::Box _buttonsRow; + Gtk::Box _buttonsPrimary; + Gtk::Box _buttonsSecondary; + Gtk::ScrolledWindow _scroller; + Gtk::Menu _popupMenu; + Inkscape::UI::Widget::SpinButton _spinBtn; + Gtk::VBox _layersPage; + + sigc::connection desktopChangeConn; + +}; + + + +} //namespace Dialogs +} //namespace UI +} //namespace Inkscape + + + +#endif // SEEN_OBJECTS_PANEL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/template-load-tab.cpp b/src/ui/dialog/template-load-tab.cpp new file mode 100644 index 0000000..6f5c654 --- /dev/null +++ b/src/ui/dialog/template-load-tab.cpp @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template abstract tab implementation + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "template-widget.h" +#include "new-from-template.h" + +#include +#include +#include +#include +#include +#include + +#include "extension/extension.h" +#include "extension/db.h" +#include "inkscape.h" +#include "file.h" +#include "path-prefix.h" + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace UI { + +TemplateLoadTab::TemplateLoadTab(NewFromTemplate* parent) + : _current_keyword("") + , _keywords_combo(true) + , _current_search_type(ALL) + , _parent_widget(parent) +{ + set_border_width(10); + + _info_widget = Gtk::manage(new TemplateWidget()); + + Gtk::Label *title; + title = Gtk::manage(new Gtk::Label(_("Search:"))); + _search_box.pack_start(*title, Gtk::PACK_SHRINK); + _search_box.pack_start(_keywords_combo, Gtk::PACK_SHRINK, 5); + + _tlist_box.pack_start(_search_box, Gtk::PACK_SHRINK, 10); + + pack_start(_tlist_box, Gtk::PACK_SHRINK); + pack_start(*_info_widget, Gtk::PACK_EXPAND_WIDGET, 5); + + Gtk::ScrolledWindow *scrolled; + scrolled = Gtk::manage(new Gtk::ScrolledWindow()); + scrolled->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled->add(_tlist_view); + _tlist_box.pack_start(*scrolled, Gtk::PACK_EXPAND_WIDGET, 5); + + _keywords_combo.signal_changed().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_keywordSelected)); + this->show_all(); + + _loadTemplates(); + _initLists(); +} + + +TemplateLoadTab::~TemplateLoadTab() += default; + + +void TemplateLoadTab::createTemplate() +{ + _info_widget->create(); +} + + +void TemplateLoadTab::_onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*) +{ + createTemplate(); + NewFromTemplate* parent = static_cast (this->get_toplevel()); + parent->_onClose(); +} + +void TemplateLoadTab::_displayTemplateInfo() +{ + Glib::RefPtr templateSelectionRef = _tlist_view.get_selection(); + if (templateSelectionRef->get_selected()) { + _current_template = (*templateSelectionRef->get_selected())[_columns.textValue]; + + _info_widget->display(_tdata[_current_template]); + _parent_widget->setCreateButtonSensitive(true); + } + +} + + +void TemplateLoadTab::_initKeywordsList() +{ + _keywords_combo.append(_("All")); + + for (const auto & _keyword : _keywords){ + _keywords_combo.append(_keyword); + } +} + + +void TemplateLoadTab::_initLists() +{ + _tlist_store = Gtk::ListStore::create(_columns); + _tlist_view.set_model(_tlist_store); + _tlist_view.append_column("", _columns.textValue); + _tlist_view.set_headers_visible(false); + + _initKeywordsList(); + _refreshTemplatesList(); + + Glib::RefPtr templateSelectionRef = + _tlist_view.get_selection(); + templateSelectionRef->signal_changed().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_displayTemplateInfo)); + + _tlist_view.signal_row_activated().connect( + sigc::mem_fun(*this, &TemplateLoadTab::_onRowActivated)); +} + +void TemplateLoadTab::_keywordSelected() +{ + _current_keyword = _keywords_combo.get_active_text(); + if (_current_keyword == ""){ + _current_keyword = _keywords_combo.get_entry_text(); + _current_search_type = USER_SPECIFIED; + } + else + _current_search_type = LIST_KEYWORD; + + if (_current_keyword == "" || _current_keyword == _("All")) + _current_search_type = ALL; + + _refreshTemplatesList(); +} + + +void TemplateLoadTab::_refreshTemplatesList() +{ + _tlist_store->clear(); + + switch (_current_search_type){ + case ALL :{ + for (auto & it : _tdata) { + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + break; + } + + case LIST_KEYWORD: { + for (auto & it : _tdata) { + if (it.second.keywords.count(_current_keyword.lowercase()) != 0){ + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + } + break; + } + + case USER_SPECIFIED : { + for (auto & it : _tdata) { + if (it.second.keywords.count(_current_keyword.lowercase()) != 0 || + it.second.display_name.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos || + it.second.author.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos || + it.second.short_description.lowercase().find(_current_keyword.lowercase()) != Glib::ustring::npos) + { + Gtk::TreeModel::iterator iter = _tlist_store->append(); + Gtk::TreeModel::Row row = *iter; + row[_columns.textValue] = it.first; + } + } + break; + } + } + + // reselect item + Gtk::TreeIter* item_to_select = nullptr; + for (Gtk::TreeModel::Children::iterator it = _tlist_store->children().begin(); it != _tlist_store->children().end(); ++it) { + Gtk::TreeModel::Row row = *it; + if (_current_template == row[_columns.textValue]) { + item_to_select = new Gtk::TreeIter(it); + break; + } + } + if (_tlist_store->children().size() == 1) { + delete item_to_select; + item_to_select = new Gtk::TreeIter(_tlist_store->children().begin()); + } + if (item_to_select) { + _tlist_view.get_selection()->select(*item_to_select); + delete item_to_select; + } else { + _current_template = ""; + _info_widget->clear(); + _parent_widget->setCreateButtonSensitive(false); + } +} + + +void TemplateLoadTab::_loadTemplates() +{ + for(auto &filename: get_filenames(TEMPLATES, {".svg"}, {"default."})) { + TemplateData tmp = _processTemplateFile(filename); + if (tmp.display_name != "") + _tdata[tmp.display_name] = tmp; + + } + // procedural templates + _getProceduralTemplates(); +} + + +TemplateLoadTab::TemplateData TemplateLoadTab::_processTemplateFile(const std::string &path) +{ + TemplateData result; + result.path = path; + result.is_procedural = false; + result.preview_name = ""; + + // convert path into valid template name + result.display_name = Glib::path_get_basename(path); + gsize n = 0; + while ((n = result.display_name.find_first_of("_", 0)) < Glib::ustring::npos){ + result.display_name.replace(n, 1, 1, ' '); + } + n = result.display_name.rfind(".svg"); + result.display_name.replace(n, 4, 1, ' '); + + Inkscape::XML::Document *rdoc = sp_repr_read_file(path.data(), SP_SVG_NS_URI); + if (rdoc){ + Inkscape::XML::Node *root = rdoc->root(); + if (strcmp(root->name(), "svg:svg") != 0){ // Wrong file format + return result; + } + + Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(root, "inkscape:templateinfo"); + if (!templateinfo) { + templateinfo = sp_repr_lookup_name(root, "inkscape:_templateinfo"); // backwards-compatibility + } + + if (templateinfo == nullptr) // No template info + return result; + _getDataFromNode(templateinfo, result); + } + + return result; +} + +void TemplateLoadTab::_getProceduralTemplates() +{ + std::list effects; + Inkscape::Extension::db.get_effect_list(effects); + + std::list::iterator it = effects.begin(); + while (it != effects.end()){ + Inkscape::XML::Node *repr = (*it)->get_repr(); + Inkscape::XML::Node *templateinfo = sp_repr_lookup_name(repr, "inkscape:templateinfo"); + if (!templateinfo) { + templateinfo = sp_repr_lookup_name(repr, "inkscape:_templateinfo"); // backwards-compatibility + } + + if (templateinfo){ + TemplateData result; + result.display_name = (*it)->get_name(); + result.is_procedural = true; + result.path = ""; + result.tpl_effect = *it; + + _getDataFromNode(templateinfo, result, *it); + _tdata[result.display_name] = result; + } + ++it; + } +} + +// if the template data comes from a procedural template (aka Effect extension), +// attempt to translate within the extension's context (which might use a different gettext textdomain) +const char *_translate(const char* msgid, Extension::Extension *extension) +{ + if (extension) { + return extension->get_translation(msgid); + } else { + return _(msgid); + } +} + +void TemplateLoadTab::_getDataFromNode(Inkscape::XML::Node *dataNode, TemplateData &data, Extension::Extension *extension) +{ + Inkscape::XML::Node *currentData; + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:name")) != nullptr) + data.display_name = _translate(currentData->firstChild()->content(), extension); + else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_name")) != nullptr) // backwards-compatibility + data.display_name = _translate(currentData->firstChild()->content(), extension); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:author")) != nullptr) + data.author = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:shortdesc")) != nullptr) + data.short_description = _translate(currentData->firstChild()->content(), extension); + else if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_shortdesc")) != nullptr) // backwards-compatibility + data.short_description = _translate(currentData->firstChild()->content(), extension); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:preview")) != nullptr) + data.preview_name = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:date")) != nullptr) + data.creation_date = currentData->firstChild()->content(); + + if ((currentData = sp_repr_lookup_name(dataNode, "inkscape:_keywords")) != nullptr){ + Glib::ustring tplKeywords = _translate(currentData->firstChild()->content(), extension); + while (!tplKeywords.empty()){ + std::size_t pos = tplKeywords.find_first_of(" "); + if (pos == Glib::ustring::npos) + pos = tplKeywords.size(); + + Glib::ustring keyword = tplKeywords.substr(0, pos).data(); + data.keywords.insert(keyword.lowercase()); + _keywords.insert(keyword.lowercase()); + + if (pos == tplKeywords.size()) + break; + tplKeywords.erase(0, pos+1); + } + } +} + +} +} diff --git a/src/ui/dialog/template-load-tab.h b/src/ui/dialog/template-load-tab.h new file mode 100644 index 0000000..2b8f98f --- /dev/null +++ b/src/ui/dialog/template-load-tab.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template abstract tab class + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H +#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_LOAD_TAB_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xml/node.h" +#include "io/resource.h" +#include "extension/effect.h" + + +namespace Inkscape { + +namespace Extension { +class Extension; +} + +namespace UI { + +class TemplateWidget; +class NewFromTemplate; + +class TemplateLoadTab : public Gtk::HBox +{ + +public: + struct TemplateData + { + bool is_procedural; + std::string path; + Glib::ustring display_name; + Glib::ustring author; + Glib::ustring short_description; + Glib::ustring long_description; // unused + Glib::ustring preview_name; + Glib::ustring creation_date; + std::set keywords; + Inkscape::Extension::Effect *tpl_effect; + }; + + TemplateLoadTab(NewFromTemplate* parent); + ~TemplateLoadTab() override; + virtual void createTemplate(); + +protected: + class StringModelColumns : public Gtk::TreeModelColumnRecord + { + public: + StringModelColumns() + { + add(textValue); + } + + Gtk::TreeModelColumn textValue; + }; + + Glib::ustring _current_keyword; + Glib::ustring _current_template; + std::map _tdata; + std::set _keywords; + + + virtual void _displayTemplateInfo(); + virtual void _initKeywordsList(); + virtual void _refreshTemplatesList(); + void _loadTemplates(); + void _initLists(); + + Gtk::VBox _tlist_box; + Gtk::HBox _search_box; + TemplateWidget *_info_widget; + + Gtk::ComboBoxText _keywords_combo; + + Gtk::TreeView _tlist_view; + Glib::RefPtr _tlist_store; + StringModelColumns _columns; + +private: + enum SearchType + { + LIST_KEYWORD, + USER_SPECIFIED, + ALL + }; + + SearchType _current_search_type; + NewFromTemplate* _parent_widget; + + void _getDataFromNode(Inkscape::XML::Node *, TemplateData &, Extension::Extension *extension=nullptr); + void _getProceduralTemplates(); + void _getTemplatesFromDomain(Inkscape::IO::Resource::Domain domain); + void _keywordSelected(); + TemplateData _processTemplateFile(const std::string &); + + void _onRowActivated(const Gtk::TreeModel::Path &, Gtk::TreeViewColumn*); +}; + +} +} + +#endif diff --git a/src/ui/dialog/template-widget.cpp b/src/ui/dialog/template-widget.cpp new file mode 100644 index 0000000..c5c4522 --- /dev/null +++ b/src/ui/dialog/template-widget.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template - templates widget - implementation + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "template-widget.h" + +#include +#include + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "file.h" +#include "inkscape.h" + +#include "extension/implementation/implementation.h" + +#include "object/sp-namedview.h" + +namespace Inkscape { +namespace UI { + + +TemplateWidget::TemplateWidget() + : _more_info_button(_("More info")) + , _short_description_label(" ") + , _template_name_label(_("no template selected")) + , _effect_prefs(nullptr) +{ + pack_start(_template_name_label, Gtk::PACK_SHRINK, 10); + pack_start(_preview_box, Gtk::PACK_SHRINK, 0); + + _preview_box.pack_start(_preview_image, Gtk::PACK_EXPAND_PADDING, 15); + _preview_box.pack_start(_preview_render, Gtk::PACK_EXPAND_PADDING, 10); + + _short_description_label.set_line_wrap(true); + + _more_info_button.set_halign(Gtk::ALIGN_END); + _more_info_button.set_valign(Gtk::ALIGN_CENTER); + pack_end(_more_info_button, Gtk::PACK_SHRINK); + + pack_end(_short_description_label, Gtk::PACK_SHRINK, 5); + + _more_info_button.signal_clicked().connect( + sigc::mem_fun(*this, &TemplateWidget::_displayTemplateDetails)); + _more_info_button.set_sensitive(false); +} + + +void TemplateWidget::create() +{ + if (_current_template.display_name == "") + return; + + if (_current_template.is_procedural){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDesktop *desc = sp_file_new_default(); + _current_template.tpl_effect->effect(desc); + DocumentUndo::clearUndo(desc->getDocument()); + desc->getDocument()->setModifiedSinceSave(false); + + // Apply cx,cy etc. from document + sp_namedview_window_from_document( desc ); + + if (desktop) + desktop->clearWaitingCursor(); + } + else { + sp_file_new(_current_template.path); + } +} + + +void TemplateWidget::display(TemplateLoadTab::TemplateData data) +{ + clear(); + _current_template = data; + + _template_name_label.set_text(_current_template.display_name); + _short_description_label.set_text(_current_template.short_description); + + if (data.preview_name != ""){ + std::string imagePath = Glib::build_filename(Glib::path_get_dirname(_current_template.path), _current_template.preview_name); + _preview_image.set(imagePath); + _preview_image.show(); + } + else if (!data.is_procedural){ + Glib::ustring gPath = data.path.c_str(); + _preview_render.showImage(gPath); + _preview_render.show(); + } + + if (data.is_procedural){ + _effect_prefs = data.tpl_effect->get_imp()->prefs_effect(data.tpl_effect, SP_ACTIVE_DESKTOP, nullptr, nullptr); + pack_start(*_effect_prefs); + } + _more_info_button.set_sensitive(true); +} + +void TemplateWidget::clear() +{ + _template_name_label.set_text(""); + _short_description_label.set_text(""); + _preview_render.hide(); + _preview_image.hide(); + if (_effect_prefs != nullptr){ + remove (*_effect_prefs); + _effect_prefs = nullptr; + } + _more_info_button.set_sensitive(false); +} + +void TemplateWidget::_displayTemplateDetails() +{ + Glib::ustring message = _current_template.display_name + "\n\n"; + + if (!_current_template.author.empty()) { + message += _("Author"); + message += ": "; + message += _current_template.author + " " + _current_template.creation_date + "\n\n"; + } + + if (!_current_template.keywords.empty()){ + message += _("Keywords"); + message += ":"; + for (const auto & keyword : _current_template.keywords) { + message += " "; + message += keyword; + } + message += "\n\n"; + } + + if (!_current_template.path.empty()) { + message += _("Path"); + message += ": "; + message += _current_template.path; + message += "\n\n"; + } + + Gtk::MessageDialog dl(message, false, Gtk::MESSAGE_OTHER); + dl.run(); +} + +} +} diff --git a/src/ui/dialog/template-widget.h b/src/ui/dialog/template-widget.h new file mode 100644 index 0000000..5d7023b --- /dev/null +++ b/src/ui/dialog/template-widget.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New From Template - template widget + */ +/* Authors: + * Jan Darowski , supervised by Krzysztof Kosiński + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H +#define INKSCAPE_SEEN_UI_DIALOG_TEMPLATE_WIDGET_H + +#include "svg-preview.h" + +#include + +#include "template-load-tab.h" + + +namespace Inkscape { +namespace UI { + + +class TemplateWidget : public Gtk::VBox +{ +public: + TemplateWidget (); + void create(); + void display(TemplateLoadTab::TemplateData); + void clear(); + +private: + TemplateLoadTab::TemplateData _current_template; + + Gtk::Button _more_info_button; + Gtk::HBox _preview_box; + Gtk::Image _preview_image; + Dialog::SVGPreview _preview_render; + Gtk::Label _short_description_label; + Gtk::Label _template_name_label; + Gtk::Widget *_effect_prefs; + + void _displayTemplateDetails(); +}; + +} +} + +#endif diff --git a/src/ui/dialog/text-edit.cpp b/src/ui/dialog/text-edit.cpp new file mode 100644 index 0000000..652798a --- /dev/null +++ b/src/ui/dialog/text-edit.cpp @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Text editing dialog. + */ +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * Abhishek Sharma + * John Smith + * Tavmjong Bah + * + * Copyright (C) 1999-2013 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "text-edit.h" + +#include +#include + +#ifdef WITH_GTKSPELL +extern "C" { +# include +} +#endif + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "style.h" +#include "text-editing.h" +#include "verbs.h" + +#include +#include +#include + +#include "object/sp-flowtext.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" + +#include "svg/css-ostringstream.h" +#include "ui/icon-names.h" +#include "ui/toolbar/text-toolbar.h" +#include "ui/widget/font-selector.h" + +#include "util/units.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +TextEdit::TextEdit() + : UI::Widget::Panel("/dialogs/textandfont", SP_VERB_DIALOG_TEXT), + font_label(_("_Font"), true), + text_label(_("_Text"), true), + feat_label(_("_Features"), true), + setasdefault_button(_("Set as _default")), + close_button(_("_Close"), true), + apply_button(_("_Apply"), true), + desktop(nullptr), + deskTrack(), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn(), + blocked(false), + /* + TRANSLATORS: Test string used in text and font dialog (when no + * text has been entered) to get a preview of the font. Choose + * some representative characters that users of your locale will be + * interested in.*/ + samplephrase(_("AaBbCcIiPpQq12369$\342\202\254\302\242?.;/()")) +{ + + /* Font tab -------------------------------- */ + + /* Font selector */ + // Do nothing. + + /* Font preview */ + preview_label.set_ellipsize (Pango::ELLIPSIZE_END); + preview_label.set_justify (Gtk::JUSTIFY_CENTER); + preview_label.set_line_wrap (false); + + font_vbox.set_border_width(4); + font_vbox.pack_start(font_selector, true, true); + font_vbox.pack_start(preview_label, false, false, 4); + + /* Features tab ---------------------------- */ + + /* Features preview */ + preview_label2.set_ellipsize (Pango::ELLIPSIZE_END); + preview_label2.set_justify (Gtk::JUSTIFY_CENTER); + preview_label2.set_line_wrap (false); + + feat_vbox.set_border_width(4); + feat_vbox.pack_start(font_features, true, true); + feat_vbox.pack_start(preview_label2, false, false, 4); + + /* Text tab -------------------------------- */ + scroller.set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + scroller.set_shadow_type(Gtk::SHADOW_IN); + + text_buffer = gtk_text_buffer_new (nullptr); + text_view = gtk_text_view_new_with_buffer (text_buffer); + gtk_text_view_set_wrap_mode ((GtkTextView *) text_view, GTK_WRAP_WORD); + +#ifdef WITH_GTKSPELL + /* + TODO: Use computed xml:lang attribute of relevant element, if present, to specify the + language (either as 2nd arg of gtkspell_new_attach, or with explicit + gtkspell_set_language call in; see advanced.c example in gtkspell docs). + onReadSelection looks like a suitable place. + */ + GtkSpellChecker * speller = gtk_spell_checker_new(); + + if (! gtk_spell_checker_attach(speller, GTK_TEXT_VIEW(text_view))) { + g_print("gtkspell error:\n"); + } +#endif + + gtk_widget_set_size_request (text_view, -1, 64); + gtk_text_view_set_editable (GTK_TEXT_VIEW (text_view), TRUE); + scroller.add(*Gtk::manage(Glib::wrap(text_view))); + text_vbox.pack_start(scroller, true, true, 0); + + /* Notebook -----------------------------------*/ + notebook.set_name( "TextEdit Notebook" ); + notebook.append_page(font_vbox, font_label); + notebook.append_page(feat_vbox, feat_label); + notebook.append_page(text_vbox, text_label); + + /* Buttons (below notebook) ------------------ */ + setasdefault_button.set_use_underline(true); + apply_button.set_can_default(); + button_row.pack_start(setasdefault_button, false, false, 0); + button_row.pack_end(close_button, false, false, VB_MARGIN); + button_row.pack_end(apply_button, false, false, VB_MARGIN); + + Gtk::Box *contents = _getContents(); + contents->set_name("TextEdit Dialog Box"); + contents->set_spacing(4); + contents->pack_start(notebook, true, true); + contents->pack_start(button_row, false, false, VB_MARGIN); + + /* Signal handlers */ + g_signal_connect ( G_OBJECT (text_buffer), "changed", G_CALLBACK (onTextChange), this ); + setasdefault_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onSetDefault)); + apply_button.signal_clicked().connect(sigc::mem_fun(*this, &TextEdit::onApply)); + close_button.signal_clicked().connect(sigc::bind(_signal_response.make_slot(), GTK_RESPONSE_CLOSE)); + fontChangedConn = font_selector.connectChanged (sigc::mem_fun(*this, &TextEdit::onFontChange)); + fontFeaturesChangedConn = font_features.connectChanged(sigc::mem_fun(*this, &TextEdit::onChange)); + notebook.signal_switch_page().connect(sigc::mem_fun(*this, &TextEdit::onFontFeatures)); + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &TextEdit::setTargetDesktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + + font_selector.set_name ("TextEdit"); + + show_all_children(); +} + +TextEdit::~TextEdit() +{ + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + desktopChangeConn.disconnect(); + deskTrack.disconnect(); + fontChangedConn.disconnect(); + fontFeaturesChangedConn.disconnect(); +} + +void TextEdit::onSelectionModified(guint flags ) +{ + gboolean style, content; + + style = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_OBJECT_STYLE_MODIFIED_FLAG )) != 0 ); + + content = ((flags & ( SP_OBJECT_CHILD_MODIFIED_FLAG | + SP_TEXT_CONTENT_MODIFIED_FLAG )) != 0 ); + + onReadSelection (style, content); +} + +void TextEdit::onReadSelection ( gboolean dostyle, gboolean /*docontent*/ ) +{ + if (blocked) + return; + + if (!desktop || SP_ACTIVE_DESKTOP != desktop) + { + return; + } + + blocked = true; + + SPItem *text = getSelectedTextItem (); + + Glib::ustring phrase = samplephrase; + + if (text) + { + guint items = getSelectedTextCount (); + if (items == 1) { + gtk_widget_set_sensitive (text_view, TRUE); + } else { + gtk_widget_set_sensitive (text_view, FALSE); + } + apply_button.set_sensitive ( false ); + setasdefault_button.set_sensitive ( true ); + + gchar *str; + str = sp_te_get_string_multiline (text); + if (str) { + if (items == 1) { + gtk_text_buffer_set_text (text_buffer, str, strlen (str)); + gtk_text_buffer_set_modified (text_buffer, FALSE); + } + phrase = str; + + } else { + gtk_text_buffer_set_text (text_buffer, "", 0); + } + + text->getRepr(); // was being called but result ignored. Check this. + } else { + gtk_widget_set_sensitive (text_view, FALSE); + apply_button.set_sensitive ( false ); + setasdefault_button.set_sensitive ( false ); + } + + if (dostyle) { + + // create temporary style + SPStyle query(SP_ACTIVE_DOCUMENT); + + // Query style from desktop into it. This returns a result flag and fills query with the + // style of subselection, if any, or selection + + int result_numbers = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + + // If querying returned nothing, read the style from the text tool prefs (default style for new texts). + if (result_numbers == QUERY_STYLE_NOTHING) { + query.readFromPrefs("/tools/text"); + } + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + // Update family/style based on selection. + font_lister->selection_update(); + Glib::ustring fontspec = font_lister->get_fontspec(); + + // Update Font Face. + font_selector.update_font (); + + // Update Size. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double size = sp_style_css_size_px_to_units(query.font_size.computed, unit); + font_selector.update_size (size); + selected_fontsize = size; + // Update font features (variant) widget + //int result_features = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTVARIANTS); + int result_features = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTFEATURESETTINGS); + font_features.update( &query, result_features == QUERY_STYLE_MULTIPLE_DIFFERENT, fontspec ); + Glib::ustring features = font_features.get_markup(); + + // Update Preview + setPreviewText (fontspec, features, phrase); + } + + blocked = false; +} + + +void TextEdit::setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase) +{ + if (font_spec.empty()) { + preview_label.set_markup(""); + preview_label2.set_markup(""); + return; + } + + // Limit number of lines in preview to arbitrary amount to prevent Text and Font dialog + // from growing taller than a desktop + const int max_lines = 4; + // Ignore starting empty lines; they would show up as nothing + auto start_pos = phrase.find_first_not_of(" \n\r\t"); + if (start_pos == Glib::ustring::npos) { + start_pos = 0; + } + // Now take up to max_lines + auto end_pos = Glib::ustring::npos; + auto from = start_pos; + for (int i = 0; i < max_lines; ++i) { + end_pos = phrase.find("\n", from); + if (end_pos == Glib::ustring::npos) { break; } + from = end_pos + 1; + } + Glib::ustring phrase_trimmed = phrase.substr(start_pos, end_pos != Glib::ustring::npos ? end_pos - start_pos : end_pos); + + Glib::ustring font_spec_escaped = Glib::Markup::escape_text( font_spec ); + Glib::ustring phrase_escaped = Glib::Markup::escape_text(phrase_trimmed); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double pt_size = + Inkscape::Util::Quantity::convert( + sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit), "px", "pt"); + pt_size = std::min(pt_size, 100.0); + + // Pango font size is in 1024ths of a point + Glib::ustring size = std::to_string( int(pt_size * PANGO_SCALE) ); + Glib::ustring markup = ""; + + preview_label.set_markup (markup); + preview_label2.set_markup (markup); +} + + +SPItem *TextEdit::getSelectedTextItem () +{ + if (!SP_ACTIVE_DESKTOP) + return nullptr; + + auto tmp= SP_ACTIVE_DESKTOP->getSelection()->items(); + for(auto i=tmp.begin();i!=tmp.end();++i) + { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) + return *i; + } + + return nullptr; +} + + +unsigned TextEdit::getSelectedTextCount () +{ + if (!SP_ACTIVE_DESKTOP) + return 0; + + unsigned int items = 0; + + auto tmp= SP_ACTIVE_DESKTOP->getSelection()->items(); + for(auto i=tmp.begin();i!=tmp.end();++i) + { + if (SP_IS_TEXT(*i) || SP_IS_FLOWTEXT(*i)) + ++items; + } + + return items; +} + +void TextEdit::onSelectionChange() +{ + onReadSelection (TRUE, TRUE); +} + +void TextEdit::updateObjectText ( SPItem *text ) +{ + GtkTextIter start, end; + + // write text + if (gtk_text_buffer_get_modified (text_buffer)) { + gtk_text_buffer_get_bounds (text_buffer, &start, &end); + gchar *str = gtk_text_buffer_get_text (text_buffer, &start, &end, TRUE); + sp_te_set_repr_text_multiline (text, str); + g_free (str); + gtk_text_buffer_set_modified (text_buffer, FALSE); + } +} + +SPCSSAttr *TextEdit::fillTextStyle () +{ + SPCSSAttr *css = sp_repr_css_attr_new (); + + Glib::ustring fontspec = font_selector.get_fontspec(); + + if( !fontspec.empty() ) { + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->fill_css( css, fontspec ); + + // TODO, possibly move this to FontLister::set_css to be shared. + Inkscape::CSSOStringStream os; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + if (prefs->getBool("/options/font/textOutputPx", true)) { + os << sp_style_css_size_units_to_px(font_selector.get_fontsize(), unit) + << sp_style_get_css_unit_string(SP_CSS_UNIT_PX); + } else { + os << font_selector.get_fontsize() << sp_style_get_css_unit_string(unit); + } + sp_repr_css_set_property (css, "font-size", os.str().c_str()); + } + + // Font features + font_features.fill_css( css ); + + return css; +} + +void TextEdit::onSetDefault() +{ + SPCSSAttr *css = fillTextStyle (); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + blocked = true; + prefs->mergeStyle("/tools/text/style", css); + blocked = false; + + sp_repr_css_attr_unref (css); + + setasdefault_button.set_sensitive ( false ); +} + +void TextEdit::onApply() +{ + blocked = true; + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + unsigned items = 0; + auto item_list = desktop->getSelection()->items(); + SPCSSAttr *css = fillTextStyle (); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for(auto i=item_list.begin();i!=item_list.end();++i){ + // apply style to the reprs of all text objects in the selection + if (SP_IS_TEXT (*i) || (SP_IS_FLOWTEXT (*i)) ) { + ++items; + } + } + if (items == 1) { + double factor = font_selector.get_fontsize() / selected_fontsize; + prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", factor); + } + sp_desktop_set_style(desktop, css, true); + + if (items == 0) { + // no text objects; apply style to prefs for new objects + prefs->mergeStyle("/tools/text/style", css); + setasdefault_button.set_sensitive ( false ); + + } else if (items == 1) { + // exactly one text object; now set its text, too + SPItem *item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + if (SP_IS_TEXT (item) || SP_IS_FLOWTEXT(item)) { + updateObjectText (item); + SPStyle *item_style = item->style; + if (SP_IS_TEXT(item) && item_style->inline_size.value == 0) { + css = sp_css_attr_from_style(item_style, SP_STYLE_FLAG_IFSET); + sp_repr_css_unset_property(css, "inline-size"); + item->changeCSS(css, "style"); + } + } + } + + // Update FontLister + Glib::ustring fontspec = font_selector.get_fontspec(); + if( !fontspec.empty() ) { + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_fontspec( fontspec, false ); + } + + // complete the transaction + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Set text style")); + apply_button.set_sensitive ( false ); + + sp_repr_css_attr_unref (css); + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument()); + + blocked = false; +} + +void TextEdit::onFontFeatures(Gtk::Widget * widgt, int pos) +{ + if (pos == 1) { + Glib::ustring fontspec = font_selector.get_fontspec(); + if (!fontspec.empty()) { + font_instance *res = font_factory::Default()->FaceFromFontSpecification(fontspec.c_str()); + if (res && !res->fulloaded) { + res->InitTheFace(true); + font_features.update_opentype(fontspec); + } + } + } +} + +void TextEdit::onChange() +{ + if (blocked) { + return; + } + + GtkTextIter start; + GtkTextIter end; + gtk_text_buffer_get_bounds (text_buffer, &start, &end); + gchar *str = gtk_text_buffer_get_text(text_buffer, &start, &end, TRUE); + + Glib::ustring fontspec = font_selector.get_fontspec(); + Glib::ustring features = font_features.get_markup(); + const gchar *phrase = str && *str ? str : samplephrase.c_str(); + setPreviewText(fontspec, features, phrase); + g_free (str); + + SPItem *text = getSelectedTextItem(); + if (text) { + apply_button.set_sensitive ( true ); + } + + setasdefault_button.set_sensitive ( true); +} + +void TextEdit::onTextChange (GtkTextBuffer *text_buffer, TextEdit *self) +{ + self->onChange(); +} + +void TextEdit::onFontChange(Glib::ustring fontspec) +{ + // Is not necesary update open type features this done when user click on font features tab + onChange(); +} + +void TextEdit::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void TextEdit::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &TextEdit::onSelectionChange))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &TextEdit::onSelectionChange))); + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &TextEdit::onSelectionModified))); + } + //widget_setup(); + onReadSelection (TRUE, TRUE); + } +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/text-edit.h b/src/ui/dialog/text-edit.h new file mode 100644 index 0000000..7a40c77 --- /dev/null +++ b/src/ui/dialog/text-edit.h @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Text-edit + */ +/* Authors: + * Lauris Kaplinski + * bulia byak + * Johan Engelen + * John Smith + * Kris De Gussem + * Tavmjong Bah + * + * Copyright (C) 1999-2013 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_TEXT_EDIT_H +#define INKSCAPE_UI_DIALOG_TEXT_EDIT_H + +#include +#include +#include +#include +#include +#include "ui/widget/panel.h" +#include "ui/widget/frame.h" +#include "ui/dialog/desktop-tracker.h" + +#include "ui/widget/font-selector.h" +#include "ui/widget/font-variants.h" + +class SPItem; +struct SPFontSelector; +class font_instance; +class SPCSSAttr; + +namespace Inkscape { +namespace UI { +namespace Dialog { + +#define VB_MARGIN 4 +/** + * The TextEdit class defines the Text and font dialog. + * + * The Text and font dialog allows you to set the font family, style and size + * and shows a preview of the result. The dialogs layout settings include + * horizontal and vertical alignment and inter line distance. + */ +class TextEdit : public UI::Widget::Panel { +public: + TextEdit(); + ~TextEdit() override; + + /** + * Helper function which returns a new instance of the dialog. + * getInstance is needed by the dialog manager (Inkscape::UI::Dialog::DialogManager). + */ + static TextEdit &getInstance() { return *new TextEdit(); } + +protected: + + /** + * Callback for pressing the default button. + */ + void onSetDefault (); + + /** + * Callback for pressing the apply button. + */ + void onApply (); + void onSelectionChange (); + void onSelectionModified (guint flags); + + /** + * Called whenever something 'changes' on canvas. + * + * onReadSelection gets the currently selected item from the canvas and sets all the controls in this dialog to the correct state. + * + * @param dostyle Indicates whether the modification of the user includes a style change. + * @param content Indicates whether the modification of the user includes a style change. Actually refers to the question if we do want to show the content? (Parameter currently not used) + */ + void onReadSelection (gboolean style, gboolean content); + + /** + * Callback invoked when the user modifies the text of the selected text object. + * + * onTextChange is responsible for initiating the commands after the user + * modified the text in the selected object. The UI of the dialog is + * updated. The subfunction setPreviewText updates the preview label. + * + * @param self pointer to the current instance of the dialog. + */ + void onChange (); + void onFontFeatures (Gtk::Widget * widgt, int pos); + static void onTextChange (GtkTextBuffer *text_buffer, TextEdit *self); + + /** + * Callback invoked when the user modifies the font through the dialog or the tools control bar. + * + * onFontChange updates the dialog UI. The subfunction setPreviewText updates the preview label. + * + * @param fontspec for the text to be previewed. + */ + void onFontChange (Glib::ustring fontspec); + + /** + * Get the selected text off the main canvas. + * + * @return SPItem pointer to the selected text object + */ + SPItem *getSelectedTextItem (); + + /** + * Count the number of text objects in the selection on the canvas. + */ + unsigned getSelectedTextCount (); + + /** + * Helper function to create markup from a fontspec and display in the preview label. + * + * @param fontspec for the text to be previewed. + * @param font_features for text to be previewed (in CSS format). + * @param phrase text to be shown. + */ + void setPreviewText (Glib::ustring font_spec, Glib::ustring font_features, Glib::ustring phrase); + + void updateObjectText ( SPItem *text ); + SPCSSAttr *fillTextStyle (); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Is invoked by the desktop tracker when the desktop changes. + * + * @see DesktopTracker + */ + void setTargetDesktop(SPDesktop *desktop); + + + +private: + + /* + * All the dialogs widgets + */ + Gtk::Notebook notebook; + + // Tab 1: Font ---------------------- // + Gtk::VBox font_vbox; + Gtk::Label font_label; + + Inkscape::UI::Widget::FontSelector font_selector; + Inkscape::UI::Widget::FontVariations font_variations; + Gtk::Label preview_label; // Share with variants tab? + + // Tab 2: Text ---------------------- // + Gtk::VBox text_vbox; + Gtk::Label text_label; + + Gtk::ScrolledWindow scroller; + GtkWidget *text_view; // TODO - Convert this to a Gtk::TextView, but GtkSpell doesn't seem to work with it + GtkTextBuffer *text_buffer; + + // Tab 3: Features ----------------- // + Gtk::VBox feat_vbox; + Inkscape::UI::Widget::FontVariants font_features; + Gtk::Label feat_label; + Gtk::Label preview_label2; // Could reparent preview_label but having a second label is probably easier. + + // Shared ------- ------------------ // + Gtk::HBox button_row; + Gtk::Button setasdefault_button; + Gtk::Button close_button; + Gtk::Button apply_button; + + // Signals + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection fontChangedConn; + sigc::connection fontFeaturesChangedConn; + + // Other + double selected_fontsize; + bool blocked; + const Glib::ustring samplephrase; + + + TextEdit(TextEdit const &d) = delete; + TextEdit operator=(TextEdit const &d) = delete; +}; + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif // INKSCAPE_UI_DIALOG_TEXT_EDIT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tile.cpp b/src/ui/dialog/tile.cpp new file mode 100644 index 0000000..ba980cb --- /dev/null +++ b/src/ui/dialog/tile.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple dialog for creating grid type arrangements of selected objects + * + * Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Abhishek Sharma + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/dialog/grid-arrange-tab.h" +#include "ui/dialog/polar-arrange-tab.h" + +#include + +#include "tile.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +ArrangeDialog::ArrangeDialog() + : UI::Widget::Panel("/dialogs/gridtiler", SP_VERB_SELECTION_ARRANGE), + _gridArrangeTab(new GridArrangeTab(this)), + _polarArrangeTab(new PolarArrangeTab(this)) +{ + Gtk::Box *contents = this->_getContents(); + + _notebook.append_page(*_gridArrangeTab, C_("Arrange dialog", "Rectangular grid")); + _notebook.append_page(*_polarArrangeTab, C_("Arrange dialog", "Polar Coordinates")); + _arrangeBox.pack_start(_notebook); + + _arrangeButton = this->addResponseButton(C_("Arrange dialog","_Arrange"), GTK_RESPONSE_APPLY); + _arrangeButton->set_use_underline(true); + _arrangeButton->set_tooltip_text(_("Arrange selected objects")); + contents->pack_start(_arrangeBox); + //show_all_children(); +} + + +void ArrangeDialog::on_show() +{ + UI::Widget::Panel::on_show(); + _polarArrangeTab->on_arrange_radio_changed(); +} + +void ArrangeDialog::_apply() +{ + switch(_notebook.get_current_page()) + { + case 0: + _gridArrangeTab->arrange(); + break; + case 1: + _polarArrangeTab->arrange(); + break; + } +} + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tile.h b/src/ui/dialog/tile.h new file mode 100644 index 0000000..26836e1 --- /dev/null +++ b/src/ui/dialog/tile.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Dialog for creating grid type arrangements of selected objects + */ +/* Authors: + * Bob Jamison ( based off trace dialog) + * John Cliff + * Other dudes from The Inkscape Organization + * Declara Denis + * + * Copyright (C) 2004 Bob Jamison + * Copyright (C) 2004 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOG_TILE_H +#define SEEN_UI_DIALOG_TILE_H + +#include +#include +#include +#include + +#include "ui/widget/panel.h" + +namespace Gtk { +class Button; +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class ArrangeTab; +class GridArrangeTab; +class PolarArrangeTab; + +class ArrangeDialog : public UI::Widget::Panel { +private: + Gtk::VBox _arrangeBox; + Gtk::Notebook _notebook; + + GridArrangeTab *_gridArrangeTab; + PolarArrangeTab *_polarArrangeTab; + + Gtk::Button *_arrangeButton; + +public: + ArrangeDialog(); + ~ArrangeDialog() override = default;; + + /** + * Callback from Apply + */ + void _apply() override; + + void on_show() override; + + static ArrangeDialog& getInstance() { return *new ArrangeDialog(); } +}; + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + + +#endif /* __TILEDIALOG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/tracedialog.cpp b/src/ui/dialog/tracedialog.cpp new file mode 100644 index 0000000..00c461d --- /dev/null +++ b/src/ui/dialog/tracedialog.cpp @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bitmap tracing settings dialog - second implementation. + */ +/* Authors: + * Marc Jeanmougin + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tracedialog.h" + +#include +#include +#include +#include +#include +#include + + +#include + +#include "desktop-tracker.h" +#include "desktop.h" +#include "selection.h" + +#include "inkscape.h" +#include "io/resource.h" +#include "io/sys.h" +#include "trace/autotrace/inkscape-autotrace.h" +#include "trace/potrace/inkscape-potrace.h" +#include "trace/depixelize/inkscape-depixelize.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +class TraceDialogImpl2 : public TraceDialog { + public: + TraceDialogImpl2(); + ~TraceDialogImpl2() override; + + private: + Inkscape::Trace::Tracer tracer; + void traceProcess(bool do_i_trace); + void abort(); + + void previewCallback(); + bool previewResize(const Cairo::RefPtr&); + void traceCallback(); + void onSelectionModified(guint flags); + void onSetDefaults(); + + void setDesktop(SPDesktop *desktop) override; + void setTargetDesktop(SPDesktop *desktop); + + SPDesktop *desktop; + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; + sigc::connection selectChangedConn; + sigc::connection selectModifiedConn; + + Glib::RefPtr builder; + + Glib::RefPtr MS_scans, PA_curves, PA_islands, PA_sparse1, PA_sparse2, SS_AT_ET_T, SS_AT_FI_T, SS_BC_T, SS_CQ_T, + SS_ED_T, optimize, smooth, speckles; + Gtk::ComboBoxText *CBT_SS, *CBT_MS; + Gtk::CheckButton *CB_invert, *CB_MS_smooth, *CB_MS_stack, *CB_MS_rb, *CB_speckles, *CB_smooth, *CB_optimize, + /* *CB_live,*/ *CB_SIOX; + Gtk::RadioButton *RB_PA_voronoi; + Gtk::Button *B_RESET, *B_STOP, *B_OK, *B_Update; + Gtk::Box *mainBox; + Gtk::Stack *choice_scan; + Gtk::Notebook *choice_tab; + Glib::RefPtr scaledPreview; + Gtk::DrawingArea *previewArea; +}; + +void TraceDialogImpl2::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +void TraceDialogImpl2::setTargetDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (this->desktop) { + selectChangedConn.disconnect(); + selectModifiedConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectModifiedConn = desktop->selection->connectModified( + sigc::hide<0>(sigc::mem_fun(*this, &TraceDialogImpl2::onSelectionModified))); + } + } +} + +void TraceDialogImpl2::traceProcess(bool do_i_trace) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) + desktop->setWaitingCursor(); + + if (CB_SIOX->get_active()) + tracer.enableSiox(true); + else + tracer.enableSiox(false); + + Glib::ustring type = + choice_scan->get_visible_child_name() == "SingleScan" ? CBT_SS->get_active_text() : CBT_MS->get_active_text(); + + Inkscape::Trace::Potrace::TraceType potraceType; + // Inkscape::Trace::Autotrace::TraceType autotraceType; + + bool use_autotrace = false; + Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO + + if (type == _("Brightness cutoff")) + potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS; + else if (type == _("Edge detection")) + potraceType = Inkscape::Trace::Potrace::TRACE_CANNY; + else if (type == _("Color quantization")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT; + else if (type == _("Autotrace")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = 2; + } + else if (type == _("Centerline tracing (autotrace)")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = 2; + ate.opts->centerline = true; + ate.opts->preserve_width = true; + } + else if (type == _("Brightness steps")) + potraceType = Inkscape::Trace::Potrace::TRACE_BRIGHTNESS_MULTI; + else if (type == _("Colors")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_COLOR; + else if (type == _("Grays")) + potraceType = Inkscape::Trace::Potrace::TRACE_QUANT_MONO; + else if (type == _("Autotrace (slower)")) + { + // autotraceType = Inkscape::Trace::Autotrace::TRACE_CENTERLINE + use_autotrace = true; + ate.opts->color_count = (int)MS_scans->get_value() + 1; + } + else + { + g_warning("Should not happen!"); + } + ate.opts->filter_iterations = (int) SS_AT_FI_T->get_value(); + ate.opts->error_threshold = SS_AT_ET_T->get_value(); + + Inkscape::Trace::Potrace::PotraceTracingEngine pte( + potraceType, CB_invert->get_active(), (int)SS_CQ_T->get_value(), SS_BC_T->get_value(), + 0., // Brightness floor + SS_ED_T->get_value(), (int)MS_scans->get_value(), CB_MS_stack->get_active(), CB_MS_smooth->get_active(), + CB_MS_rb->get_active()); + pte.potraceParams->opticurve = CB_optimize->get_active(); + pte.potraceParams->opttolerance = optimize->get_value(); + pte.potraceParams->alphamax = CB_smooth->get_active() ? smooth->get_value() : 0; + pte.potraceParams->turdsize = CB_speckles->get_active() ? (int)speckles->get_value() : 0; + + + + //Inkscape::Trace::Autotrace::AutotraceTracingEngine ate; // TODO + Inkscape::Trace::Depixelize::DepixelizeTracingEngine dte(RB_PA_voronoi->get_active() ? Inkscape::Trace::Depixelize::TraceType::TRACE_VORONOI : Inkscape::Trace::Depixelize::TraceType::TRACE_BSPLINES, PA_curves->get_value(), (int) PA_islands->get_value(), (int) PA_sparse1->get_value(), PA_sparse2->get_value() ); + + + Glib::RefPtr pixbuf = tracer.getSelectedImage(); + if (pixbuf) { + Glib::RefPtr preview = use_autotrace ? ate.preview(pixbuf) : pte.preview(pixbuf); + if (preview) { + int width = preview->get_width(); + int height = preview->get_height(); + const Gtk::Allocation &vboxAlloc = previewArea->get_allocation(); + double scaleFX = vboxAlloc.get_width() / (double)width; + double scaleFY = vboxAlloc.get_height() / (double)height; + double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX; + int newWidth = (int)(((double)width) * scaleFactor); + int newHeight = (int)(((double)height) * scaleFactor); + scaledPreview = preview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST); + previewArea->queue_draw(); + } + } + if (do_i_trace){ + if (choice_tab->get_current_page() == 1){ + tracer.trace(&dte); + printf("dt\n"); + } else if (use_autotrace) { + tracer.trace(&ate); + printf("at\n"); + } else if (choice_tab->get_current_page() == 0) { + tracer.trace(&pte); + printf("pt\n"); + } + } + + if (desktop) + desktop->clearWaitingCursor(); +} + +bool TraceDialogImpl2::previewResize(const Cairo::RefPtr& cr) +{ + if (!scaledPreview) return false; // return early + int width = scaledPreview->get_width(); + int height = scaledPreview->get_height(); + const Gtk::Allocation &vboxAlloc = previewArea->get_allocation(); + double scaleFX = vboxAlloc.get_width() / (double)width; + double scaleFY = vboxAlloc.get_height() / (double)height; + double scaleFactor = scaleFX > scaleFY ? scaleFY : scaleFX; + int newWidth = (int)(((double)width) * scaleFactor); + int newHeight = (int)(((double)height) * scaleFactor); + int offsetX = (vboxAlloc.get_width() - newWidth)/2; + int offsetY = (vboxAlloc.get_height() - newHeight)/2; + + Glib::RefPtr temp = scaledPreview->scale_simple(newWidth, newHeight, Gdk::INTERP_NEAREST); + Gdk::Cairo::set_source_pixbuf(cr, temp, offsetX, offsetY); + cr->paint(); + return false; +} + +void TraceDialogImpl2::abort() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) + desktop->clearWaitingCursor(); + tracer.abort(); +} + +void TraceDialogImpl2::onSelectionModified(guint flags) +{ + if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_PARENT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG)) { + previewCallback(); + } +} + +void TraceDialogImpl2::onSetDefaults() +{ + MS_scans->set_value(8); + PA_curves->set_value(1); + PA_islands->set_value(5); + PA_sparse1->set_value(4); + PA_sparse2->set_value(1); + SS_AT_FI_T->set_value(4); + SS_AT_ET_T->set_value(2); + SS_BC_T->set_value(0.45); + SS_CQ_T->set_value(64); + SS_ED_T->set_value(.65); + optimize->set_value(0.2); + smooth->set_value(1); + speckles->set_value(2); + CB_invert->set_active(false); + CB_MS_smooth->set_active(true); + CB_MS_stack->set_active(true); + CB_MS_rb->set_active(false); + CB_speckles->set_active(true); + CB_smooth->set_active(true); + CB_optimize->set_active(true); + //CB_live->set_active(false); + CB_SIOX->set_active(false); +} + +void TraceDialogImpl2::previewCallback() { traceProcess(false); } +void TraceDialogImpl2::traceCallback() { traceProcess(true); } + + +TraceDialogImpl2::TraceDialogImpl2() + : TraceDialog() +{ + const std::string req_widgets[] = { "MS_scans", "PA_curves", "PA_islands", "PA_sparse1", "PA_sparse2", + "SS_AT_FI_T", "SS_AT_ET_T", "SS_BC_T", "SS_CQ_T", "SS_ED_T", + "optimize", "smooth", "speckles", "CB_invert", "CB_MS_smooth", + "CB_MS_stack", "CB_MS_rb", "CB_speckles", "CB_smooth", "CB_optimize", + /*"CB_live",*/ "CB_SIOX", "CBT_SS", "CBT_MS", "B_RESET", + "B_STOP", "B_OK", "mainBox", "choice_tab", "choice_scan", + "previewArea" }; + Glib::ustring gladefile = get_filename(Inkscape::IO::Resource::UIS, "dialog-trace.glade"); + try { + builder = Gtk::Builder::create_from_file(gladefile); + } catch (const Glib::Error &ex) { + g_warning("Glade file loading failed for filter effect dialog"); + return; + } + + Glib::RefPtr test; + for (std::string w : req_widgets) { + test = builder->get_object(w); + if (!test) { + g_warning("Required widget %s does not exist", w.c_str()); + return; + } + } + +#define GET_O(name) \ + tmp = builder->get_object(#name); \ + name = Glib::RefPtr::cast_dynamic(tmp); + + Glib::RefPtr tmp; + +#define GET_W(name) builder->get_widget(#name, name); + GET_O(MS_scans) + GET_O(PA_curves) + GET_O(PA_islands) + GET_O(PA_sparse1) + GET_O(PA_sparse2) + GET_O(SS_AT_FI_T) + GET_O(SS_AT_ET_T) + GET_O(SS_BC_T) + GET_O(SS_CQ_T) + GET_O(SS_ED_T) + GET_O(optimize) + GET_O(smooth) + GET_O(speckles) + + GET_W(CB_invert) + GET_W(CB_MS_smooth) + GET_W(CB_MS_stack) + GET_W(CB_MS_rb) + GET_W(CB_speckles) + GET_W(CB_smooth) + GET_W(CB_optimize) + //GET_W(CB_live) + GET_W(CB_SIOX) + GET_W(RB_PA_voronoi) + GET_W(CBT_SS) + GET_W(CBT_MS) + GET_W(B_RESET) + GET_W(B_STOP) + GET_W(B_OK) + GET_W(B_Update) + GET_W(mainBox) + GET_W(choice_tab) + GET_W(choice_scan) + GET_W(previewArea) +#undef GET_W +#undef GET_O + _getContents()->add(*mainBox); + // show_all_children(); + desktopChangeConn = deskTrack.connectDesktopChanged(sigc::mem_fun(*this, &TraceDialogImpl2::setTargetDesktop)); + deskTrack.connect(GTK_WIDGET(gobj())); + + B_Update->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewCallback)); + B_OK->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::traceCallback)); + B_STOP->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::abort)); + B_RESET->signal_clicked().connect(sigc::mem_fun(*this, &TraceDialogImpl2::onSetDefaults)); + previewArea->signal_draw().connect(sigc::mem_fun(*this, &TraceDialogImpl2::previewResize)); +} + + +TraceDialogImpl2::~TraceDialogImpl2() +{ + selectChangedConn.disconnect(); + selectModifiedConn.disconnect(); + desktopChangeConn.disconnect(); +} + + + +TraceDialog &TraceDialog::getInstance() +{ + TraceDialog *dialog = new TraceDialogImpl2(); + return *dialog; +} + + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape diff --git a/src/ui/dialog/tracedialog.h b/src/ui/dialog/tracedialog.h new file mode 100644 index 0000000..d02e705 --- /dev/null +++ b/src/ui/dialog/tracedialog.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Bitmap tracing settings dialog + */ +/* Authors: + * Bob Jamison + * Other dudes from The Inkscape Organization + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __TRACEDIALOG_H__ +#define __TRACEDIALOG_H__ + +#include "ui/widget/panel.h" +#include "verbs.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * A dialog that displays log messages + */ +class TraceDialog : public UI::Widget::Panel +{ + +public: + + /** + * Constructor + */ + TraceDialog() : + UI::Widget::Panel("/dialogs/trace", SP_VERB_SELECTION_TRACE) + {} + + + /** + * Factory method + */ + static TraceDialog &getInstance(); + + /** + * Destructor + */ + ~TraceDialog() override = default;; + + +}; + + +} //namespace Dialog +} //namespace UI +} //namespace Inkscape + +#endif /* __TRACEDIALOG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/transformation.cpp b/src/ui/dialog/transformation.cpp new file mode 100644 index 0000000..f212654 --- /dev/null +++ b/src/ui/dialog/transformation.cpp @@ -0,0 +1,1171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Transform dialog - implementation. + */ +/* Authors: + * Bryce W. Harrington + * buliabyak@gmail.com + * Abhishek Sharma + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include <2geom/transforms.h> + +#include "align-and-distribute.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "transformation.h" +#include "verbs.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" +#include "ui/icon-loader.h" + +#include "ui/icon-names.h" + + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +static void on_selection_changed(Inkscape::Selection *selection, Transformation *daad) +{ + int page = daad->getCurrentPage(); + daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection); +} + +static void on_selection_modified(Inkscape::Selection *selection, Transformation *daad) +{ + int page = daad->getCurrentPage(); + daad->updateSelection((Inkscape::UI::Dialog::Transformation::PageType)page, selection); +} + +/*######################################################################## +# C O N S T R U C T O R +########################################################################*/ + +Transformation::Transformation() + : UI::Widget::Panel("/dialogs/transformation", SP_VERB_DIALOG_TRANSFORM), + _page_move (4, 2), + _page_scale (4, 2), + _page_rotate (4, 2), + _page_skew (4, 2), + _page_transform (3, 3), + _scalar_move_horizontal (_("_Horizontal:"), _("Horizontal displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR, + "", "transform-move-horizontal", &_units_move), + _scalar_move_vertical (_("_Vertical:"), _("Vertical displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR, + "", "transform-move-vertical", &_units_move), + _scalar_scale_horizontal(_("_Width:"), _("Horizontal size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS, + "", "transform-scale-horizontal", &_units_scale), + _scalar_scale_vertical (_("_Height:"), _("Vertical size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS, + "", "transform-scale-vertical", &_units_scale), + _scalar_rotate (_("A_ngle:"), _("Rotation angle (positive = counterclockwise)"), UNIT_TYPE_RADIAL, + "", "transform-rotate", &_units_rotate), + _scalar_skew_horizontal (_("_Horizontal:"), _("Horizontal skew angle (positive = counterclockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR, + "", "transform-skew-horizontal", &_units_skew), + _scalar_skew_vertical (_("_Vertical:"), _("Vertical skew angle (positive = clockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR, + "", "transform-skew-vertical", &_units_skew), + + _scalar_transform_a ("_A:", _("Transformation matrix element A")), + _scalar_transform_b ("_B:", _("Transformation matrix element B")), + _scalar_transform_c ("_C:", _("Transformation matrix element C")), + _scalar_transform_d ("_D:", _("Transformation matrix element D")), + _scalar_transform_e ("_E:", _("Transformation matrix element E")), + _scalar_transform_f ("_F:", _("Transformation matrix element F")), + + _counterclockwise_rotate (), + _clockwise_rotate (), + + _check_move_relative (_("Rela_tive move")), + _check_scale_proportional (_("_Scale proportionally")), + _check_apply_separately (_("Apply to each _object separately")), + _check_replace_matrix (_("Edit c_urrent matrix")) + +{ + _check_move_relative.set_use_underline(); + _check_move_relative.set_tooltip_text(_("Add the specified relative displacement to the current position; otherwise, edit the current absolute position directly")); + _check_scale_proportional.set_use_underline(); + _check_scale_proportional.set_tooltip_text(_("Preserve the width/height ratio of the scaled objects")); + _check_apply_separately.set_use_underline(); + _check_apply_separately.set_tooltip_text(_("Apply the scale/rotate/skew to each selected object separately; otherwise, transform the selection as a whole")); + _check_replace_matrix.set_use_underline(); + _check_replace_matrix.set_tooltip_text(_("Edit the current transform= matrix; otherwise, post-multiply transform= by this matrix")); + Gtk::Box *contents = _getContents(); + + contents->set_spacing(0); + + // Notebook for individual transformations + contents->pack_start(_notebook, false, false); + + _page_move.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_move, _("_Move"), true); + layoutPageMove(); + + _page_scale.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_scale, _("_Scale"), true); + layoutPageScale(); + + _page_rotate.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_rotate, _("_Rotate"), true); + layoutPageRotate(); + + _page_skew.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_skew, _("Ske_w"), true); + layoutPageSkew(); + + _page_transform.set_halign(Gtk::ALIGN_START); + _notebook.append_page(_page_transform, _("Matri_x"), true); + layoutPageTransform(); + + _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage)); + + // Apply separately + contents->pack_start(_check_apply_separately, false, false); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _check_apply_separately.set_active(prefs->getBool("/dialogs/transformation/applyseparately")); + _check_apply_separately.signal_toggled().connect(sigc::mem_fun(*this, &Transformation::onApplySeparatelyToggled)); + + // make sure all spinbuttons activate Apply on pressing Enter + ((Gtk::Entry *) (_scalar_move_horizontal.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_move_vertical.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_scale_horizontal.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_scale_vertical.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_rotate.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_skew_horizontal.getWidget()))->set_activates_default(true); + ((Gtk::Entry *) (_scalar_skew_vertical.getWidget()))->set_activates_default(true); + + updateSelection(PAGE_MOVE, _getSelection()); + + resetButton = addResponseButton(_("_Clear"), 0); + if (resetButton) { + resetButton->set_tooltip_text(_("Reset the values on the current tab to defaults")); + resetButton->set_sensitive(true); + resetButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onClear)); + } + + applyButton = addResponseButton(_("_Apply"), Gtk::RESPONSE_APPLY); + if (applyButton) { + applyButton->set_tooltip_text(_("Apply transformation to selection")); + applyButton->set_sensitive(false); + } + + // Connect to the global selection changed & modified signals + _selChangeConn = INKSCAPE.signal_selection_changed.connect(sigc::bind(sigc::ptr_fun(&on_selection_changed), this)); + _selModifyConn = INKSCAPE.signal_selection_modified.connect(sigc::hide<1>(sigc::bind(sigc::ptr_fun(&on_selection_modified), this))); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &Transformation::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + show_all_children(); +} + +Transformation::~Transformation() +{ + _selModifyConn.disconnect(); + _selChangeConn.disconnect(); + _desktopChangeConn.disconnect(); + _deskTrack.disconnect(); +} + +void Transformation::setTargetDesktop(SPDesktop *desktop) +{ + if (_desktop != desktop) { + _desktop = desktop; + } +} + +/*######################################################################## +# U T I L I T Y +########################################################################*/ + +void Transformation::presentPage(Transformation::PageType page) +{ + _notebook.set_current_page(page); + show(); + present(); +} + + + + +/*######################################################################## +# S E T U P L A Y O U T +########################################################################*/ + + +void Transformation::layoutPageMove() +{ + _units_move.setUnitType(UNIT_TYPE_LINEAR); + + // Setting default unit to document unit + SPDesktop *dt = getDesktop(); + SPNamedView *nv = dt->getNamedView(); + if (nv->display_units) { + _units_move.setUnit(nv->display_units->abbr); + } + + _scalar_move_horizontal.initScalar(-1e6, 1e6); + _scalar_move_horizontal.setDigits(3); + _scalar_move_horizontal.setIncrements(0.1, 1.0); + _scalar_move_horizontal.set_hexpand(); + + _scalar_move_vertical.initScalar(-1e6, 1e6); + _scalar_move_vertical.setDigits(3); + _scalar_move_vertical.setIncrements(0.1, 1.0); + _scalar_move_vertical.set_hexpand(); + + //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_HOR ); + + _page_move.table().attach(_scalar_move_horizontal, 0, 0, 2, 1); + _page_move.table().attach(_units_move, 2, 0, 1, 1); + + _scalar_move_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged)); + + //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_VER ); + _page_move.table().attach(_scalar_move_vertical, 0, 1, 2, 1); + + _scalar_move_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged)); + + // Relative moves + _page_move.table().attach(_check_move_relative, 0, 2, 2, 1); + + _check_move_relative.set_active(true); + _check_move_relative.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onMoveRelativeToggled)); +} + +void Transformation::layoutPageScale() +{ + _units_scale.setUnitType(UNIT_TYPE_DIMENSIONLESS); + _units_scale.setUnitType(UNIT_TYPE_LINEAR); + + _scalar_scale_horizontal.initScalar(-1e6, 1e6); + _scalar_scale_horizontal.setValue(100.0, "%"); + _scalar_scale_horizontal.setDigits(3); + _scalar_scale_horizontal.setIncrements(0.1, 1.0); + _scalar_scale_horizontal.setAbsoluteIsIncrement(true); + _scalar_scale_horizontal.setPercentageIsIncrement(true); + _scalar_scale_horizontal.set_hexpand(); + + _scalar_scale_vertical.initScalar(-1e6, 1e6); + _scalar_scale_vertical.setValue(100.0, "%"); + _scalar_scale_vertical.setDigits(3); + _scalar_scale_vertical.setIncrements(0.1, 1.0); + _scalar_scale_vertical.setAbsoluteIsIncrement(true); + _scalar_scale_vertical.setPercentageIsIncrement(true); + _scalar_scale_vertical.set_hexpand(); + + _page_scale.table().attach(_scalar_scale_horizontal, 0, 0, 2, 1); + + _scalar_scale_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onScaleXValueChanged)); + + _page_scale.table().attach(_units_scale, 2, 0, 1, 1); + _page_scale.table().attach(_scalar_scale_vertical, 0, 1, 2, 1); + + _scalar_scale_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onScaleYValueChanged)); + + _page_scale.table().attach(_check_scale_proportional, 0, 2, 2, 1); + + _check_scale_proportional.set_active(false); + _check_scale_proportional.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onScaleProportionalToggled)); + + //TODO: add a widget for selecting the fixed point in scaling, or honour rotation center? +} + +void Transformation::layoutPageRotate() +{ + _units_rotate.setUnitType(UNIT_TYPE_RADIAL); + + _scalar_rotate.initScalar(-360.0, 360.0); + _scalar_rotate.setDigits(3); + _scalar_rotate.setIncrements(0.1, 1.0); + _scalar_rotate.set_hexpand(); + + auto object_rotate_left_icon = Gtk::manage(sp_get_icon_image("object-rotate-left", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + _counterclockwise_rotate.add(*object_rotate_left_icon); + _counterclockwise_rotate.set_mode(false); + _counterclockwise_rotate.set_relief(Gtk::RELIEF_NONE); + _counterclockwise_rotate.set_tooltip_text(_("Rotate in a counterclockwise direction")); + + auto object_rotate_right_icon = Gtk::manage(sp_get_icon_image("object-rotate-right", Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + _clockwise_rotate.add(*object_rotate_right_icon); + _clockwise_rotate.set_mode(false); + _clockwise_rotate.set_relief(Gtk::RELIEF_NONE); + _clockwise_rotate.set_tooltip_text(_("Rotate in a clockwise direction")); + + Gtk::RadioButton::Group group = _counterclockwise_rotate.get_group(); + _clockwise_rotate.set_group(group); + + _page_rotate.table().attach(_scalar_rotate, 0, 0, 2, 1); + _page_rotate.table().attach(_units_rotate, 2, 0, 1, 1); + _page_rotate.table().attach(_counterclockwise_rotate, 3, 0, 1, 1); + _page_rotate.table().attach(_clockwise_rotate, 4, 0, 1, 1); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE) != getDesktop()->is_yaxisdown()) { + _counterclockwise_rotate.set_active(); + onRotateCounterclockwiseClicked(); + } else { + _clockwise_rotate.set_active(); + onRotateClockwiseClicked(); + } + + _scalar_rotate.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onRotateValueChanged)); + + _counterclockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateCounterclockwiseClicked)); + _clockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateClockwiseClicked)); + + //TODO: honour rotation center? +} + +void Transformation::layoutPageSkew() +{ + _units_skew.setUnitType(UNIT_TYPE_LINEAR); + _units_skew.setUnitType(UNIT_TYPE_DIMENSIONLESS); + _units_skew.setUnitType(UNIT_TYPE_RADIAL); + + _scalar_skew_horizontal.initScalar(-1e6, 1e6); + _scalar_skew_horizontal.setDigits(3); + _scalar_skew_horizontal.setIncrements(0.1, 1.0); + _scalar_skew_horizontal.set_hexpand(); + + _scalar_skew_vertical.initScalar(-1e6, 1e6); + _scalar_skew_vertical.setDigits(3); + _scalar_skew_vertical.setIncrements(0.1, 1.0); + _scalar_skew_vertical.set_hexpand(); + + _page_skew.table().attach(_scalar_skew_horizontal, 0, 0, 2, 1); + _page_skew.table().attach(_units_skew, 2, 0, 1, 1); + _page_skew.table().attach(_scalar_skew_vertical, 0, 1, 2, 1); + + _scalar_skew_horizontal.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged)); + _scalar_skew_vertical.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged)); + + //TODO: honour rotation center? +} + + + +void Transformation::layoutPageTransform() +{ + _scalar_transform_a.setWidgetSizeRequest(65, -1); + _scalar_transform_a.setRange(-1e10, 1e10); + _scalar_transform_a.setDigits(3); + _scalar_transform_a.setIncrements(0.1, 1.0); + _scalar_transform_a.setValue(1.0); + _scalar_transform_a.setWidthChars(6); + _scalar_transform_a.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_a, 0, 0, 1, 1); + + _scalar_transform_a.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + _scalar_transform_b.setWidgetSizeRequest(65, -1); + _scalar_transform_b.setRange(-1e10, 1e10); + _scalar_transform_b.setDigits(3); + _scalar_transform_b.setIncrements(0.1, 1.0); + _scalar_transform_b.setValue(0.0); + _scalar_transform_b.setWidthChars(6); + _scalar_transform_b.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_b, 0, 1, 1, 1); + + _scalar_transform_b.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + _scalar_transform_c.setWidgetSizeRequest(65, -1); + _scalar_transform_c.setRange(-1e10, 1e10); + _scalar_transform_c.setDigits(3); + _scalar_transform_c.setIncrements(0.1, 1.0); + _scalar_transform_c.setValue(0.0); + _scalar_transform_c.setWidthChars(6); + _scalar_transform_c.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_c, 1, 0, 1, 1); + + _scalar_transform_c.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_d.setWidgetSizeRequest(65, -1); + _scalar_transform_d.setRange(-1e10, 1e10); + _scalar_transform_d.setDigits(3); + _scalar_transform_d.setIncrements(0.1, 1.0); + _scalar_transform_d.setValue(1.0); + _scalar_transform_d.setWidthChars(6); + _scalar_transform_d.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_d, 1, 1, 1, 1); + + _scalar_transform_d.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_e.setWidgetSizeRequest(65, -1); + _scalar_transform_e.setRange(-1e10, 1e10); + _scalar_transform_e.setDigits(3); + _scalar_transform_e.setIncrements(0.1, 1.0); + _scalar_transform_e.setValue(0.0); + _scalar_transform_e.setWidthChars(6); + _scalar_transform_e.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_e, 2, 0, 1, 1); + + _scalar_transform_e.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + + _scalar_transform_f.setWidgetSizeRequest(65, -1); + _scalar_transform_f.setRange(-1e10, 1e10); + _scalar_transform_f.setDigits(3); + _scalar_transform_f.setIncrements(0.1, 1.0); + _scalar_transform_f.setValue(0.0); + _scalar_transform_f.setWidthChars(6); + _scalar_transform_f.set_hexpand(); + + _page_transform.table().attach(_scalar_transform_f, 2, 1, 1, 1); + + _scalar_transform_f.signal_value_changed() + .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged)); + + // Edit existing matrix + _page_transform.table().attach(_check_replace_matrix, 0, 2, 2, 1); + + _check_replace_matrix.set_active(false); + _check_replace_matrix.signal_toggled() + .connect(sigc::mem_fun(*this, &Transformation::onReplaceMatrixToggled)); +} + + +/*######################################################################## +# U P D A T E +########################################################################*/ + +void Transformation::updateSelection(PageType page, Inkscape::Selection *selection) +{ + if (!selection || selection->isEmpty()) + return; + + switch (page) { + case PAGE_MOVE: { + updatePageMove(selection); + break; + } + case PAGE_SCALE: { + updatePageScale(selection); + break; + } + case PAGE_ROTATE: { + updatePageRotate(selection); + break; + } + case PAGE_SKEW: { + updatePageSkew(selection); + break; + } + case PAGE_TRANSFORM: { + updatePageTransform(selection); + break; + } + case PAGE_QTY: { + break; + } + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, + selection && !selection->isEmpty()); +} + +void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum) +{ + updateSelection((PageType)pagenum, getDesktop()->getSelection()); +} + + +void Transformation::updatePageMove(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + if (!_check_move_relative.get_active()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double x = bbox->min()[Geom::X]; + double y = bbox->min()[Geom::Y]; + + double conversion = _units_move.getConversion("px"); + _scalar_move_horizontal.setValue(x / conversion); + _scalar_move_vertical.setValue(y / conversion); + } + } else { + // do nothing, so you can apply the same relative move to many objects in turn + } + _page_move.set_sensitive(true); + } else { + _page_move.set_sensitive(false); + } +} + +void Transformation::updatePageScale(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double w = bbox->dimensions()[Geom::X]; + double h = bbox->dimensions()[Geom::Y]; + _scalar_scale_horizontal.setHundredPercent(w); + _scalar_scale_vertical.setHundredPercent(h); + onScaleXValueChanged(); // to update x/y proportionality if switch is on + _page_scale.set_sensitive(true); + } else { + _page_scale.set_sensitive(false); + } + } else { + _page_scale.set_sensitive(false); + } +} + +void Transformation::updatePageRotate(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + _page_rotate.set_sensitive(true); + } else { + _page_rotate.set_sensitive(false); + } +} + +void Transformation::updatePageSkew(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + double w = bbox->dimensions()[Geom::X]; + double h = bbox->dimensions()[Geom::Y]; + _scalar_skew_vertical.setHundredPercent(w); + _scalar_skew_horizontal.setHundredPercent(h); + _page_skew.set_sensitive(true); + } else { + _page_skew.set_sensitive(false); + } + } else { + _page_skew.set_sensitive(false); + } +} + +void Transformation::updatePageTransform(Inkscape::Selection *selection) +{ + if (selection && !selection->isEmpty()) { + if (_check_replace_matrix.get_active()) { + Geom::Affine current (selection->items().front()->transform); // take from the first item in selection + + Geom::Affine new_displayed = current; + + _scalar_transform_a.setValue(new_displayed[0]); + _scalar_transform_b.setValue(new_displayed[1]); + _scalar_transform_c.setValue(new_displayed[2]); + _scalar_transform_d.setValue(new_displayed[3]); + _scalar_transform_e.setValue(new_displayed[4]); + _scalar_transform_f.setValue(new_displayed[5]); + } else { + // do nothing, so you can apply the same matrix to many objects in turn + } + _page_transform.set_sensitive(true); + } else { + _page_transform.set_sensitive(false); + } +} + + + + + +/*######################################################################## +# A P P L Y +########################################################################*/ + + + +void Transformation::_apply() +{ + Inkscape::Selection * const selection = _getSelection(); + if (!selection || selection->isEmpty()) + return; + + int const page = _notebook.get_current_page(); + + switch (page) { + case PAGE_MOVE: { + applyPageMove(selection); + break; + } + case PAGE_ROTATE: { + applyPageRotate(selection); + break; + } + case PAGE_SCALE: { + applyPageScale(selection); + break; + } + case PAGE_SKEW: { + applyPageSkew(selection); + break; + } + case PAGE_TRANSFORM: { + applyPageTransform(selection); + break; + } + } + + //Let's play with never turning this off + //setResponseSensitive(Gtk::RESPONSE_APPLY, false); +} + +void Transformation::applyPageMove(Inkscape::Selection *selection) +{ + double x = _scalar_move_horizontal.getValue("px"); + double y = _scalar_move_vertical.getValue("px"); + if (_check_move_relative.get_active()) { + y *= getDesktop()->yaxisdir(); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/dialogs/transformation/applyseparately")) { + // move selection as a whole + if (_check_move_relative.get_active()) { + selection->moveRelative(x, y); + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]); + } + } + } else { + + if (_check_move_relative.get_active()) { + // shift each object relatively to the previous one + std::vector selected(selection->items().begin(), selection->items().end()); + if (selected.empty()) return; + + if (fabs(x) > 1e-6) { + std::vector< BBoxSort > sorted; + for (auto item : selected) + { + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, Geom::X, x > 0? 1. : 0., x > 0? 0. : 1.); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + double move = x; + for ( std::vector ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + it->item->move_rel(Geom::Translate(move, 0)); + // move each next object by x relative to previous + move += x; + } + } + if (fabs(y) > 1e-6) { + std::vector< BBoxSort > sorted; + for (auto item : selected) + { + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + sorted.emplace_back(item, *bbox, Geom::Y, y > 0? 1. : 0., y > 0? 0. : 1.); + } + } + //sort bbox by anchors + std::stable_sort(sorted.begin(), sorted.end()); + + double move = y; + for ( std::vector ::iterator it (sorted.begin()); + it < sorted.end(); + ++it ) + { + it->item->move_rel(Geom::Translate(0, move)); + // move each next object by x relative to previous + move += y; + } + } + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]); + } + } + } + + DocumentUndo::done( selection->desktop()->getDocument() , SP_VERB_DIALOG_TRANSFORM, + _("Move")); +} + +void Transformation::applyPageScale(Inkscape::Selection *selection) +{ + double scaleX = _scalar_scale_horizontal.getValue("px"); + double scaleY = _scalar_scale_vertical.getValue("px"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool preserve = prefs->getBool("/options/preservetransform/value", false); + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto tmp= selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + Geom::OptRect bbox_pref = item->desktopPreferredBounds(); + Geom::OptRect bbox_geom = item->desktopGeometricBounds(); + if (bbox_pref && bbox_geom) { + double new_width = scaleX; + double new_height = scaleY; + // the values are increments! + if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent + new_width = scaleX/100 * bbox_pref->width(); + new_height = scaleY/100 * bbox_pref->height(); + } + if (fabs(new_width) < 1e-6) new_width = 1e-6; // not 0, as this would result in a nasty no-bbox object + if (fabs(new_height) < 1e-6) new_height = 1e-6; + + double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2; + double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2; + double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2; + double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2; + + Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + item->set_i2d_affine(item->i2dt_affine() * scaler); + item->doWriteTransform(item->transform); + } + } + } else { + Geom::OptRect bbox_pref = selection->preferredBounds(); + Geom::OptRect bbox_geom = selection->geometricBounds(); + if (bbox_pref && bbox_geom) { + // the values are increments! + double new_width = scaleX; + double new_height = scaleY; + if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent + new_width = scaleX/100 * bbox_pref->width(); + new_height = scaleY/100 * bbox_pref->height(); + } + if (fabs(new_width) < 1e-6) new_width = 1e-6; + if (fabs(new_height) < 1e-6) new_height = 1e-6; + + double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2; + double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2; + double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2; + double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2; + Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + + selection->applyAffine(scaler); + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Scale")); +} + +void Transformation::applyPageRotate(Inkscape::Selection *selection) +{ + double angle = _scalar_rotate.getValue(DEG); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE)) { + angle *= -1; + } + + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto tmp= selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + item->rotate_rel(Geom::Rotate (angle*M_PI/180.0)); + } + } else { + boost::optional center = selection->center(); + if (center) { + selection->rotateRelative(*center, angle); + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Rotate")); +} + +void Transformation::applyPageSkew(Inkscape::Selection *selection) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/transformation/applyseparately")) { + auto items = selection->items(); + for(auto i = items.begin();i!=items.end();++i){ + SPItem *item = *i; + + if (!_units_skew.isAbsolute()) { // percentage + double skewX = _scalar_skew_horizontal.getValue("%"); + double skewY = _scalar_skew_vertical.getValue("%"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + item->skew_rel(0.01*skewX, 0.01*skewY); + } else if (_units_skew.isRadial()) { //deg or rad + double angleX = _scalar_skew_horizontal.getValue("rad"); + double angleY = _scalar_skew_vertical.getValue("rad"); + if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON) + || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + double skewX = tan(angleX); + double skewY = tan(angleY); + skewX *= getDesktop()->yaxisdir(); + skewY *= getDesktop()->yaxisdir(); + item->skew_rel(skewX, skewY); + } else { // absolute displacement + double skewX = _scalar_skew_horizontal.getValue("px"); + double skewY = _scalar_skew_vertical.getValue("px"); + skewY *= getDesktop()->yaxisdir(); + Geom::OptRect bbox = item->desktopPreferredBounds(); + if (bbox) { + double width = bbox->dimensions()[Geom::X]; + double height = bbox->dimensions()[Geom::Y]; + if (fabs(skewX*skewY - width*height) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + item->skew_rel(skewX/height, skewY/width); + } + } + } + } else { // transform whole selection + Geom::OptRect bbox = selection->preferredBounds(); + boost::optional center = selection->center(); + + if ( bbox && center ) { + double width = bbox->dimensions()[Geom::X]; + double height = bbox->dimensions()[Geom::Y]; + + if (!_units_skew.isAbsolute()) { // percentage + double skewX = _scalar_skew_horizontal.getValue("%"); + double skewY = _scalar_skew_vertical.getValue("%"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + selection->skewRelative(*center, 0.01 * skewX, 0.01 * skewY); + } else if (_units_skew.isRadial()) { //deg or rad + double angleX = _scalar_skew_horizontal.getValue("rad"); + double angleY = _scalar_skew_vertical.getValue("rad"); + if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON) + || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON) + || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + double skewX = tan(angleX); + double skewY = tan(angleY); + skewX *= getDesktop()->yaxisdir(); + skewY *= getDesktop()->yaxisdir(); + selection->skewRelative(*center, skewX, skewY); + } else { // absolute displacement + double skewX = _scalar_skew_horizontal.getValue("px"); + double skewY = _scalar_skew_vertical.getValue("px"); + skewY *= getDesktop()->yaxisdir(); + if (fabs(skewX*skewY - width*height) < Geom::EPSILON) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + selection->skewRelative(*center, skewX / height, skewY / width); + } + } + } + + DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Skew")); +} + + +void Transformation::applyPageTransform(Inkscape::Selection *selection) +{ + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue(); + double f = _scalar_transform_f.getValue(); + + Geom::Affine displayed(a, b, c, d, e, f); + if (displayed.isSingular()) { + getDesktop()->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, not used.")); + return; + } + + if (_check_replace_matrix.get_active()) { + auto tmp = selection->items(); + for(auto i=tmp.begin();i!=tmp.end();++i){ + SPItem *item = *i; + item->set_item_transform(displayed); + item->updateRepr(); + } + } else { + selection->applyAffine(displayed); // post-multiply each object's transform + } + + DocumentUndo::done(selection->desktop()->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Edit transformation matrix")); +} + + + + + +/*######################################################################## +# V A L U E - C H A N G E D C A L L B A C K S +########################################################################*/ + +void Transformation::onMoveValueChanged() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onMoveRelativeToggled() +{ + Inkscape::Selection *selection = _getSelection(); + + if (!selection || selection->isEmpty()) + return; + + double x = _scalar_move_horizontal.getValue("px"); + double y = _scalar_move_vertical.getValue("px"); + + double conversion = _units_move.getConversion("px"); + + //g_message("onMoveRelativeToggled: %f, %f px\n", x, y); + + Geom::OptRect bbox = selection->preferredBounds(); + + if (bbox) { + if (_check_move_relative.get_active()) { + // From absolute to relative + _scalar_move_horizontal.setValue((x - bbox->min()[Geom::X]) / conversion); + _scalar_move_vertical.setValue(( y - bbox->min()[Geom::Y]) / conversion); + } else { + // From relative to absolute + _scalar_move_horizontal.setValue((bbox->min()[Geom::X] + x) / conversion); + _scalar_move_vertical.setValue(( bbox->min()[Geom::Y] + y) / conversion); + } + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onScaleXValueChanged() +{ + if (_scalar_scale_horizontal.setProgrammatically) { + _scalar_scale_horizontal.setProgrammatically = false; + return; + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); + + if (_check_scale_proportional.get_active()) { + if (!_units_scale.isAbsolute()) { // percentage, just copy over + _scalar_scale_vertical.setValue(_scalar_scale_horizontal.getValue("%")); + } else { + double scaleXPercentage = _scalar_scale_horizontal.getAsPercentage(); + _scalar_scale_vertical.setFromPercentage (scaleXPercentage); + } + } +} + +void Transformation::onScaleYValueChanged() +{ + if (_scalar_scale_vertical.setProgrammatically) { + _scalar_scale_vertical.setProgrammatically = false; + return; + } + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); + + if (_check_scale_proportional.get_active()) { + if (!_units_scale.isAbsolute()) { // percentage, just copy over + _scalar_scale_horizontal.setValue(_scalar_scale_vertical.getValue("%")); + } else { + double scaleYPercentage = _scalar_scale_vertical.getAsPercentage(); + _scalar_scale_horizontal.setFromPercentage (scaleYPercentage); + } + } +} + +void Transformation::onRotateValueChanged() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onRotateCounterclockwiseClicked() +{ + _scalar_rotate.setTooltipText(_("Rotation angle (positive = counterclockwise)")); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/rotateCounterClockwise", !getDesktop()->is_yaxisdown()); +} + +void Transformation::onRotateClockwiseClicked() +{ + _scalar_rotate.setTooltipText(_("Rotation angle (positive = clockwise)")); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/rotateCounterClockwise", getDesktop()->is_yaxisdown()); +} + +void Transformation::onSkewValueChanged() +{ + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onTransformValueChanged() +{ + + /* + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue(); + double f = _scalar_transform_f.getValue(); + + //g_message("onTransformValueChanged: (%f, %f, %f, %f, %f, %f)\n", + // a, b, c, d, e ,f); + */ + + setResponseSensitive(Gtk::RESPONSE_APPLY, true); +} + +void Transformation::onReplaceMatrixToggled() +{ + Inkscape::Selection *selection = _getSelection(); + + if (!selection || selection->isEmpty()) + return; + + double a = _scalar_transform_a.getValue(); + double b = _scalar_transform_b.getValue(); + double c = _scalar_transform_c.getValue(); + double d = _scalar_transform_d.getValue(); + double e = _scalar_transform_e.getValue(); + double f = _scalar_transform_f.getValue(); + + Geom::Affine displayed (a, b, c, d, e, f); + Geom::Affine current = selection->items().front()->transform; // take from the first item in selection + + Geom::Affine new_displayed; + if (_check_replace_matrix.get_active()) { + new_displayed = current; + } else { + new_displayed = current.inverse() * displayed; + } + + _scalar_transform_a.setValue(new_displayed[0]); + _scalar_transform_b.setValue(new_displayed[1]); + _scalar_transform_c.setValue(new_displayed[2]); + _scalar_transform_d.setValue(new_displayed[3]); + _scalar_transform_e.setValue(new_displayed[4]); + _scalar_transform_f.setValue(new_displayed[5]); +} + +void Transformation::onScaleProportionalToggled() +{ + onScaleXValueChanged(); + if (_scalar_scale_vertical.setProgrammatically) { + _scalar_scale_vertical.setProgrammatically = false; + } +} + + +void Transformation::onClear() +{ + int const page = _notebook.get_current_page(); + + switch (page) { + case PAGE_MOVE: { + Inkscape::Selection *selection = _getSelection(); + if (!selection || selection->isEmpty() || _check_move_relative.get_active()) { + _scalar_move_horizontal.setValue(0); + _scalar_move_vertical.setValue(0); + } else { + Geom::OptRect bbox = selection->preferredBounds(); + if (bbox) { + _scalar_move_horizontal.setValue(bbox->min()[Geom::X], "px"); + _scalar_move_vertical.setValue(bbox->min()[Geom::Y], "px"); + } + } + break; + } + case PAGE_ROTATE: { + _scalar_rotate.setValue(0); + break; + } + case PAGE_SCALE: { + _scalar_scale_horizontal.setValue(100, "%"); + _scalar_scale_vertical.setValue(100, "%"); + break; + } + case PAGE_SKEW: { + _scalar_skew_horizontal.setValue(0); + _scalar_skew_vertical.setValue(0); + break; + } + case PAGE_TRANSFORM: { + _scalar_transform_a.setValue(1); + _scalar_transform_b.setValue(0); + _scalar_transform_c.setValue(0); + _scalar_transform_d.setValue(1); + _scalar_transform_e.setValue(0); + _scalar_transform_f.setValue(0); + break; + } + } +} + +void Transformation::onApplySeparatelyToggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active()); +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/transformation.h b/src/ui/dialog/transformation.h new file mode 100644 index 0000000..f50e214 --- /dev/null +++ b/src/ui/dialog/transformation.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Transform dialog + */ +/* Author: + * Bryce W. Harrington + * + * Copyright (C) 2004, 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_TRANSFORMATION_H +#define INKSCAPE_UI_DIALOG_TRANSFORMATION_H + + +#include +#include + +#include "ui/widget/panel.h" +#include "ui/widget/notebook-page.h" +#include "ui/widget/scalar-unit.h" +#include "ui/dialog/desktop-tracker.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/** + * Transformation dialog. + * + * The transformation dialog allows to modify Inkscape objects. + * 5 transformation operations are currently possible: move, scale, + * rotate, skew and matrix. + */ +class Transformation : public UI::Widget::Panel +{ + +public: + + /** + * Constructor for Transformation. + * + * This does the initialization + * and layout of the dialog used for transforming SVG objects. It + * consists of 5 pages for the 5 operations it handles: + * 'Move' allows x,y translation of SVG objects + * 'Scale' allows linear resizing of SVG objects + * 'Rotate' allows rotating SVG objects by a degree + * 'Skew' allows skewing SVG objects + * 'Matrix' allows applying a generic affine transform on SVG objects, + * with the user specifying the 6 degrees of freedom manually. + * + * The dialog is implemented as a Gtk::Notebook with five pages. + * The pages are implemented using Inkscape's NotebookPage which + * is used to help make sure all of Inkscape's notebooks follow + * the same style. We then populate the pages with our widgets, + * we use the ScalarUnit class for this. + */ + Transformation(); + + /** + * Cleanup + */ + ~Transformation() override; + + /** + * Factory method. Create an instance of this class/interface + */ + static Transformation &getInstance() + { return *new Transformation(); } + + + /** + * Show the Move panel + */ + void setPageMove() + { presentPage(PAGE_MOVE); } + + + /** + * Show the Scale panel + */ + void setPageScale() + { presentPage(PAGE_SCALE); } + + + /** + * Show the Rotate panel + */ + void setPageRotate() + { presentPage(PAGE_ROTATE); } + + /** + * Show the Skew panel + */ + void setPageSkew() + { presentPage(PAGE_SKEW); } + + /** + * Show the Transform panel + */ + void setPageTransform() + { presentPage(PAGE_TRANSFORM); } + + + int getCurrentPage() + { return _notebook.get_current_page(); } + + enum PageType { + PAGE_MOVE, PAGE_SCALE, PAGE_ROTATE, PAGE_SKEW, PAGE_TRANSFORM, PAGE_QTY + }; + + void updateSelection(PageType page, Inkscape::Selection *selection); + +protected: + + Gtk::Notebook _notebook; + + UI::Widget::NotebookPage _page_move; + UI::Widget::NotebookPage _page_scale; + UI::Widget::NotebookPage _page_rotate; + UI::Widget::NotebookPage _page_skew; + UI::Widget::NotebookPage _page_transform; + + UI::Widget::UnitMenu _units_move; + UI::Widget::UnitMenu _units_scale; + UI::Widget::UnitMenu _units_rotate; + UI::Widget::UnitMenu _units_skew; + + UI::Widget::ScalarUnit _scalar_move_horizontal; + UI::Widget::ScalarUnit _scalar_move_vertical; + UI::Widget::ScalarUnit _scalar_scale_horizontal; + UI::Widget::ScalarUnit _scalar_scale_vertical; + UI::Widget::ScalarUnit _scalar_rotate; + UI::Widget::ScalarUnit _scalar_skew_horizontal; + UI::Widget::ScalarUnit _scalar_skew_vertical; + + UI::Widget::Scalar _scalar_transform_a; + UI::Widget::Scalar _scalar_transform_b; + UI::Widget::Scalar _scalar_transform_c; + UI::Widget::Scalar _scalar_transform_d; + UI::Widget::Scalar _scalar_transform_e; + UI::Widget::Scalar _scalar_transform_f; + + Gtk::RadioButton _counterclockwise_rotate; + Gtk::RadioButton _clockwise_rotate; + + Gtk::CheckButton _check_move_relative; + Gtk::CheckButton _check_scale_proportional; + Gtk::CheckButton _check_apply_separately; + Gtk::CheckButton _check_replace_matrix; + + SPDesktop *_desktop; + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + + /** + * Layout the GUI components, and prepare for use + */ + void layoutPageMove(); + void layoutPageScale(); + void layoutPageRotate(); + void layoutPageSkew(); + void layoutPageTransform(); + + void _apply() override; + void presentPage(PageType page); + + void onSwitchPage(Gtk::Widget *page, guint pagenum); + + /** + * Callbacks for when a user changes values on the panels + */ + void onMoveValueChanged(); + void onMoveRelativeToggled(); + void onScaleXValueChanged(); + void onScaleYValueChanged(); + void onRotateValueChanged(); + void onRotateCounterclockwiseClicked(); + void onRotateClockwiseClicked(); + void onSkewValueChanged(); + void onTransformValueChanged(); + void onReplaceMatrixToggled(); + void onScaleProportionalToggled(); + + void onClear(); + + void onApplySeparatelyToggled(); + + /** + * Called when the selection is updated, to make + * the panel(s) show the new values. + * Editor---->dialog + */ + void updatePageMove(Inkscape::Selection *); + void updatePageScale(Inkscape::Selection *); + void updatePageRotate(Inkscape::Selection *); + void updatePageSkew(Inkscape::Selection *); + void updatePageTransform(Inkscape::Selection *); + + /** + * Called when the Apply button is pushed + * Dialog---->editor + */ + void applyPageMove(Inkscape::Selection *); + void applyPageScale(Inkscape::Selection *); + void applyPageRotate(Inkscape::Selection *); + void applyPageSkew(Inkscape::Selection *); + void applyPageTransform(Inkscape::Selection *); + + void setTargetDesktop(SPDesktop* desktop); + +private: + + /** + * Copy constructor + */ + Transformation(Transformation const &d) = delete; + + /** + * Assignment operator + */ + Transformation operator=(Transformation const &d) = delete; + + Gtk::Button *applyButton; + Gtk::Button *resetButton; + Gtk::Button *cancelButton; + + sigc::connection _selChangeConn; + sigc::connection _selModifyConn; +}; + + + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + + + +#endif //INKSCAPE_UI_DIALOG_TRANSFORMATION_H + + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/undo-history.cpp b/src/ui/dialog/undo-history.cpp new file mode 100644 index 0000000..eae9ea6 --- /dev/null +++ b/src/ui/dialog/undo-history.cpp @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Undo History dialog - implementation. + */ +/* Author: + * Gustav Broberg + * Abhishek Sharma + * Jon A. Cruz + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "undo-history.h" + +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "ui/icon-loader.h" +#include "util/signal-blocker.h" + +#include "desktop.h" + + +namespace Inkscape { +namespace UI { +namespace Dialog { + +/* Rendering functions for custom cell renderers */ +void CellRendererSPIcon::render_vfunc(const Cairo::RefPtr& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) +{ + // if this event type doesn't have an icon... + if ( !Inkscape::Verb::get(_property_event_type)->get_image() ) return; + + // if the icon isn't cached, render it to a pixbuf + if ( !_icon_cache[_property_event_type] ) { + + Glib::ustring image_name = Inkscape::Verb::get(_property_event_type)->get_image(); + Gtk::Image* icon = Gtk::manage(new Gtk::Image()); + icon = sp_get_icon_image(image_name, Gtk::ICON_SIZE_MENU); + + if (icon) { + + // check icon type (inkscape, gtk, none) + if ( GTK_IS_IMAGE(icon->gobj()) ) { + _property_icon = sp_get_icon_pixbuf(image_name, 16); + } else { + delete icon; + return; + } + + delete icon; + property_pixbuf() = _icon_cache[_property_event_type] = _property_icon.get_value(); + } + + } else { + property_pixbuf() = _icon_cache[_property_event_type]; + } + + Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area, + cell_area, flags); +} + + +void CellRendererInt::render_vfunc(const Cairo::RefPtr& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) +{ + if( _filter(_property_number) ) { + std::ostringstream s; + s << _property_number << std::flush; + property_text() = s.str(); + Gtk::CellRendererText::render_vfunc(cr, widget, background_area, + cell_area, flags); + } +} + +const CellRendererInt::Filter& CellRendererInt::no_filter = CellRendererInt::NoFilter(); + +UndoHistory& UndoHistory::getInstance() +{ + return *new UndoHistory(); +} + +UndoHistory::UndoHistory() + : UI::Widget::Panel("/dialogs/undo-history", SP_VERB_DIALOG_UNDO_HISTORY), + _document_replaced_connection(), + _desktop(getDesktop()), + _document(_desktop ? _desktop->doc() : nullptr), + _event_log(_desktop ? _desktop->event_log : nullptr), + _columns(_event_log ? &_event_log->getColumns() : nullptr), + _scrolled_window(), + _event_list_store(), + _event_list_selection(_event_list_view.get_selection()), + _deskTrack(), + _desktopChangeConn(), + _callback_connections() +{ + if ( !_document || !_event_log || !_columns ) return; + + set_size_request(-1, 95); + + _getContents()->pack_start(_scrolled_window); + _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + + // connect with the EventLog + _connectEventLog(); + + _event_list_view.set_enable_search(false); + _event_list_view.set_headers_visible(false); + + CellRendererSPIcon* icon_renderer = Gtk::manage(new CellRendererSPIcon()); + icon_renderer->property_xpad() = 2; + icon_renderer->property_width() = 24; + int cols_count = _event_list_view.append_column("Icon", *icon_renderer); + + Gtk::TreeView::Column* icon_column = _event_list_view.get_column(cols_count-1); + icon_column->add_attribute(icon_renderer->property_event_type(), _columns->type); + + CellRendererInt* children_renderer = Gtk::manage(new CellRendererInt(greater_than_1)); + children_renderer->property_weight() = 600; // =Pango::WEIGHT_SEMIBOLD (not defined in old versions of pangomm) + children_renderer->property_xalign() = 1.0; + children_renderer->property_xpad() = 2; + children_renderer->property_width() = 24; + + cols_count = _event_list_view.append_column("Children", *children_renderer); + Gtk::TreeView::Column* children_column = _event_list_view.get_column(cols_count-1); + children_column->add_attribute(children_renderer->property_number(), _columns->child_count); + + Gtk::CellRendererText* description_renderer = Gtk::manage(new Gtk::CellRendererText()); + description_renderer->property_ellipsize() = Pango::ELLIPSIZE_END; + + cols_count = _event_list_view.append_column("Description", *description_renderer); + Gtk::TreeView::Column* description_column = _event_list_view.get_column(cols_count-1); + description_column->add_attribute(description_renderer->property_text(), _columns->description); + description_column->set_resizable(); + description_column->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE); + description_column->set_min_width (150); + + _event_list_view.set_expander_column( *_event_list_view.get_column(cols_count-1) ); + + _scrolled_window.add(_event_list_view); + + // connect EventLog callbacks + _callback_connections[EventLog::CALLB_SELECTION_CHANGE] = + _event_list_selection->signal_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onListSelectionChange)); + + _callback_connections[EventLog::CALLB_EXPAND] = + _event_list_view.signal_row_expanded().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onExpandEvent)); + + _callback_connections[EventLog::CALLB_COLLAPSE] = + _event_list_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onCollapseEvent)); + + _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &UndoHistory::setDesktop) ); + _deskTrack.connect(GTK_WIDGET(gobj())); + + // connect to be informed of document changes + signalDocumentReplaced().connect(sigc::mem_fun(*this, &UndoHistory::_handleDocumentReplaced)); + + show_all_children(); + + // scroll to the selected row + _event_list_view.set_cursor(_event_list_store->get_path(_event_log->getCurrEvent())); +} + +UndoHistory::~UndoHistory() +{ + _desktopChangeConn.disconnect(); +} + + +void UndoHistory::setDesktop(SPDesktop* desktop) +{ + Panel::setDesktop(desktop); + + EventLog *newEventLog = desktop ? desktop->event_log : nullptr; + if ((_desktop == desktop) && (_event_log == newEventLog)) { + // same desktop set + } + else + { + _connectDocument(desktop, desktop ? desktop->doc() : nullptr); + } +} + +void UndoHistory::_connectDocument(SPDesktop* desktop, SPDocument * /*document*/) +{ + // disconnect from prior + if (_event_log) { + _event_log->removeDialogConnection(&_event_list_view, &_callback_connections); + } + + SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]); + + _event_list_view.unset_model(); + + // connect to new EventLog/Desktop + _desktop = desktop; + _event_log = desktop ? desktop->event_log : nullptr; + _document = desktop ? desktop->doc() : nullptr; + _connectEventLog(); +} + +void UndoHistory::_connectEventLog() +{ + if (_event_log) { + _event_log->add_destroy_notify_callback(this, &_handleEventLogDestroyCB); + _event_list_store = _event_log->getEventListStore(); + + _event_list_view.set_model(_event_list_store); + + _event_log->addDialogConnection(&_event_list_view, &_callback_connections); + _event_list_view.scroll_to_row(_event_list_store->get_path(_event_list_selection->get_selected())); + } +} + +void UndoHistory::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document) +{ + if ((desktop != _desktop) || (document != _document)) { + _connectDocument(desktop, document); + } +} + +void *UndoHistory::_handleEventLogDestroyCB(void *data) +{ + void *result = nullptr; + if (data) { + UndoHistory *self = reinterpret_cast(data); + result = self->_handleEventLogDestroy(); + } + return result; +} + +// called *after* _event_log has been destroyed. +void *UndoHistory::_handleEventLogDestroy() +{ + if (_event_log) { + SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]); + + _event_list_view.unset_model(); + _event_list_store.reset(); + _event_log = nullptr; + } + + return nullptr; +} + +void +UndoHistory::_onListSelectionChange() +{ + + EventLog::const_iterator selected = _event_list_selection->get_selected(); + + /* If no event is selected in the view, find the right one and select it. This happens whenever + * a branch we're currently in is collapsed. + */ + if (!selected) { + + EventLog::iterator curr_event = _event_log->getCurrEvent(); + + if (curr_event->parent()) { + + EventLog::iterator curr_event_parent = curr_event->parent(); + EventLog::iterator last = curr_event_parent->children().end(); + + _event_log->blockNotifications(); + for ( --last ; curr_event != last ; ++curr_event ) { + DocumentUndo::redo(_document); + } + _event_log->blockNotifications(false); + + _event_log->setCurrEvent(curr_event); + _event_list_selection->select(curr_event_parent); + + } else { // this should not happen + _event_list_selection->select(curr_event); + } + + } else { + + EventLog::const_iterator last_selected = _event_log->getCurrEvent(); + + /* Selecting a collapsed parent event is equal to selecting the last child + * of that parent's branch. + */ + + if ( !selected->children().empty() && + !_event_list_view.row_expanded(_event_list_store->get_path(selected)) ) + { + selected = selected->children().end(); + --selected; + } + + // An event before the current one has been selected. Undo to the selected event. + if ( _event_list_store->get_path(selected) < + _event_list_store->get_path(last_selected) ) + { + _event_log->blockNotifications(); + + while ( selected != last_selected ) { + + DocumentUndo::undo(_document); + + if ( last_selected->parent() && + last_selected == last_selected->parent()->children().begin() ) + { + last_selected = last_selected->parent(); + _event_log->setCurrEventParent((EventLog::iterator)nullptr); + } else { + --last_selected; + if ( !last_selected->children().empty() ) { + _event_log->setCurrEventParent(last_selected); + last_selected = last_selected->children().end(); + --last_selected; + } + } + } + _event_log->blockNotifications(false); + _event_log->updateUndoVerbs(); + + } else { // An event after the current one has been selected. Redo to the selected event. + + _event_log->blockNotifications(); + + while ( selected != last_selected ) { + + DocumentUndo::redo(_document); + + if ( !last_selected->children().empty() ) { + _event_log->setCurrEventParent(last_selected); + last_selected = last_selected->children().begin(); + } else { + ++last_selected; + if ( last_selected->parent() && + last_selected == last_selected->parent()->children().end() ) + { + last_selected = last_selected->parent(); + ++last_selected; + _event_log->setCurrEventParent((EventLog::iterator)nullptr); + } + } + } + _event_log->blockNotifications(false); + + } + + _event_log->setCurrEvent(selected); + _event_log->updateUndoVerbs(); + } + +} + +void +UndoHistory::_onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/) +{ + if ( iter == _event_list_selection->get_selected() ) { + _event_list_selection->select(_event_log->getCurrEvent()); + } +} + +void +UndoHistory::_onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/) +{ + // Collapsing a branch we're currently in is equal to stepping to the last event in that branch + if ( iter == _event_log->getCurrEvent() ) { + EventLog::const_iterator curr_event_parent = _event_log->getCurrEvent(); + EventLog::const_iterator curr_event = curr_event_parent->children().begin(); + EventLog::const_iterator last = curr_event_parent->children().end(); + + _event_log->blockNotifications(); + DocumentUndo::redo(_document); + + for ( --last ; curr_event != last ; ++curr_event ) { + DocumentUndo::redo(_document); + } + _event_log->blockNotifications(false); + + _event_log->setCurrEvent(curr_event); + _event_log->setCurrEventParent(curr_event_parent); + _event_list_selection->select(curr_event_parent); + } +} + +const CellRendererInt::Filter& UndoHistory::greater_than_1 = UndoHistory::GreaterThan(1); + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/undo-history.h b/src/ui/dialog/undo-history.h new file mode 100644 index 0000000..244980b --- /dev/null +++ b/src/ui/dialog/undo-history.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Undo History dialog + */ +/* Author: + * Gustav Broberg + * Jon A. Cruz + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_DIALOG_UNDO_HISTORY_H +#define INKSCAPE_UI_DIALOG_UNDO_HISTORY_H + +#include "ui/widget/panel.h" +#include +#include +#include +#include +#include + +#include +#include + +#include "event-log.h" + +#include "ui/dialog/desktop-tracker.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Dialog { + + +/* Custom cell renderers */ + +class CellRendererSPIcon : public Gtk::CellRendererPixbuf { +public: + + CellRendererSPIcon() : + Glib::ObjectBase(typeid(CellRendererPixbuf)), + Gtk::CellRendererPixbuf(), + _property_icon(*this, "icon", Glib::RefPtr(nullptr)), + _property_event_type(*this, "event_type", 0) + { } + + Glib::PropertyProxy + property_event_type() { return _property_event_type.get_proxy(); } + +protected: + void render_vfunc(const Cairo::RefPtr& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override; +private: + + Glib::Property > _property_icon; + Glib::Property _property_event_type; + std::map > _icon_cache; + +}; + + +class CellRendererInt : public Gtk::CellRendererText { +public: + + struct Filter : std::unary_function { + virtual ~Filter() = default; + virtual bool operator() (const int&) const =0; + }; + + CellRendererInt(const Filter& filter=no_filter) : + Glib::ObjectBase(typeid(CellRendererText)), + Gtk::CellRendererText(), + _property_number(*this, "number", 0), + _filter (filter) + { } + + + Glib::PropertyProxy + property_number() { return _property_number.get_proxy(); } + + static const Filter& no_filter; + +protected: + void render_vfunc(const Cairo::RefPtr& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags) override; + +private: + + Glib::Property _property_number; + const Filter& _filter; + + struct NoFilter : Filter { bool operator() (const int& /*x*/) const override { return true; } }; +}; + +/** + * \brief Dialog for presenting document change history + * + * This dialog allows the user to undo and redo multiple events in a more convenient way + * than repateaded ctrl-z, ctrl-shift-z. + */ +class UndoHistory : public Widget::Panel { +public: + ~UndoHistory() override; + + static UndoHistory &getInstance(); + void setDesktop(SPDesktop* desktop) override; + + sigc::connection _document_replaced_connection; + +protected: + + SPDesktop *_desktop; + SPDocument *_document; + EventLog *_event_log; + + + const EventLog::EventModelColumns *_columns; + + Gtk::ScrolledWindow _scrolled_window; + + Glib::RefPtr _event_list_store; + Gtk::TreeView _event_list_view; + Glib::RefPtr _event_list_selection; + + DesktopTracker _deskTrack; + sigc::connection _desktopChangeConn; + + EventLog::CallbackMap _callback_connections; + + static void *_handleEventLogDestroyCB(void *data); + + void _connectDocument(SPDesktop* desktop, SPDocument *document); + void _connectEventLog(); + void _handleDocumentReplaced(SPDesktop* desktop, SPDocument *document); + void *_handleEventLogDestroy(); + void _onListSelectionChange(); + void _onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + void _onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &path); + +private: + UndoHistory(); + + // no default constructor, noncopyable, nonassignable + UndoHistory(UndoHistory const &d) = delete; + UndoHistory operator=(UndoHistory const &d) = delete; + + struct GreaterThan : CellRendererInt::Filter { + GreaterThan(int _i) : i (_i) {} + bool operator() (const int& x) const override { return x > i; } + int i; + }; + + static const CellRendererInt::Filter& greater_than_1; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_UNDO_HISTORY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/xml-tree.cpp b/src/ui/dialog/xml-tree.cpp new file mode 100644 index 0000000..dcc5c55 --- /dev/null +++ b/src/ui/dialog/xml-tree.cpp @@ -0,0 +1,968 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * XML editor. + */ +/* Authors: + * Lauris Kaplinski + * MenTaLguY + * bulia byak + * Johan Engelen + * David Turner + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 1999-2006 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include "xml-tree.h" + +#include +#include + + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "shortcuts.h" +#include "verbs.h" + +#include "object/sp-root.h" +#include "object/sp-string.h" + +#include "ui/dialog-events.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/tool-base.h" + +#include "widgets/sp-xmlview-tree.h" + +namespace Inkscape { +namespace UI { +namespace Dialog { + +XmlTree::XmlTree() + : UI::Widget::Panel("/dialogs/xml/", SP_VERB_DIALOG_XML_EDITOR) + , blocked(0) + , _message_stack(nullptr) + , _message_context(nullptr) + , current_desktop(nullptr) + , current_document(nullptr) + , selected_attr(0) + , selected_repr(nullptr) + , tree(nullptr) + , status("") + , new_window(nullptr) + , _updating(false) +{ + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + return; + } + + Gtk::Box *root = _getContents(); + Gtk::Box *contents = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + status.set_halign(Gtk::ALIGN_START); + status.set_valign(Gtk::ALIGN_CENTER); + status.set_size_request(1, -1); + status.set_markup(""); + status.set_line_wrap(true); + status.get_style_context()->add_class("inksmall"); + status_box.pack_start( status, TRUE, TRUE, 0); + contents->pack_start(_paned, true, true, 0); + contents->set_valign(Gtk::ALIGN_FILL); + contents->child_property_fill(_paned); + + _paned.set_vexpand(true); + _message_stack = std::make_shared(); + _message_context = std::unique_ptr(new Inkscape::MessageContext(_message_stack)); + _message_changed_connection = _message_stack->connectChanged( + sigc::bind(sigc::ptr_fun(_set_status_message), GTK_WIDGET(status.gobj()))); + + /* tree view */ + tree = SP_XMLVIEW_TREE(sp_xmlview_tree_new(nullptr, nullptr, nullptr)); + gtk_widget_set_tooltip_text( GTK_WIDGET(tree), _("Drag to reorder nodes") ); + + tree_toolbar.set_toolbar_style(Gtk::TOOLBAR_ICONS); + + auto xml_element_new_icon = Gtk::manage(sp_get_icon_image("xml-element-new", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_element_new_button.set_icon_widget(*xml_element_new_icon); + xml_element_new_button.set_label(_("New element node")); + xml_element_new_button.set_tooltip_text(_("New element node")); + xml_element_new_button.set_sensitive(false); + tree_toolbar.add(xml_element_new_button); + + auto xml_text_new_icon = Gtk::manage(sp_get_icon_image("xml-text-new", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_text_new_button.set_icon_widget(*xml_text_new_icon); + xml_text_new_button.set_label(_("New text node")); + xml_text_new_button.set_tooltip_text(_("New text node")); + xml_text_new_button.set_sensitive(false); + tree_toolbar.add(xml_text_new_button); + + auto xml_node_duplicate_icon = Gtk::manage(sp_get_icon_image("xml-node-duplicate", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_node_duplicate_button.set_icon_widget(*xml_node_duplicate_icon); + xml_node_duplicate_button.set_label(_("Duplicate node")); + xml_node_duplicate_button.set_tooltip_text(_("Duplicate node")); + xml_node_duplicate_button.set_sensitive(false); + tree_toolbar.add(xml_node_duplicate_button); + + tree_toolbar.add(separator); + + auto xml_node_delete_icon = Gtk::manage(sp_get_icon_image("xml-node-delete", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + xml_node_delete_button.set_icon_widget(*xml_node_delete_icon); + xml_node_delete_button.set_label(_("Delete node")); + xml_node_delete_button.set_tooltip_text(_("Delete node")); + xml_node_delete_button.set_sensitive(false); + tree_toolbar.add(xml_node_delete_button); + + tree_toolbar.add(separator2); + + auto format_indent_less_icon = Gtk::manage(sp_get_icon_image("format-indent-less", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + unindent_node_button.set_icon_widget(*format_indent_less_icon); + unindent_node_button.set_label(_("Unindent node")); + unindent_node_button.set_tooltip_text(_("Unindent node")); + unindent_node_button.set_sensitive(false); + tree_toolbar.add(unindent_node_button); + + auto format_indent_more_icon = Gtk::manage(sp_get_icon_image("format-indent-more", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + indent_node_button.set_icon_widget(*format_indent_more_icon); + indent_node_button.set_label(_("Indent node")); + indent_node_button.set_tooltip_text(_("Indent node")); + indent_node_button.set_sensitive(false); + tree_toolbar.add(indent_node_button); + + auto go_up_icon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + raise_node_button.set_icon_widget(*go_up_icon); + raise_node_button.set_label(_("Raise node")); + raise_node_button.set_tooltip_text(_("Raise node")); + raise_node_button.set_sensitive(false); + tree_toolbar.add(raise_node_button); + + auto go_down_icon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_LARGE_TOOLBAR)); + + lower_node_button.set_icon_widget(*go_down_icon); + lower_node_button.set_label(_("Lower node")); + lower_node_button.set_tooltip_text(_("Lower node")); + lower_node_button.set_sensitive(false); + tree_toolbar.add(lower_node_button); + + node_box.pack_start(tree_toolbar, FALSE, TRUE, 0); + + Gtk::ScrolledWindow *tree_scroller = new Gtk::ScrolledWindow(); + tree_scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + tree_scroller->set_shadow_type(Gtk::SHADOW_IN); + tree_scroller->add(*Gtk::manage(Glib::wrap(GTK_WIDGET(tree)))); + + node_box.pack_start(*Gtk::manage(tree_scroller)); + + node_box.pack_end(status_box, false, false, 2); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool attrtoggler = prefs->getBool("/dialogs/xml/attrtoggler", true); + bool dir = prefs->getBool("/dialogs/xml/vertical", true); + attributes = new AttrDialog(); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + _paned.set_wide_handle(true); + _paned.pack1(node_box, false, false); + /* attributes */ + Gtk::Box *actionsbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + actionsbox->set_valign(Gtk::ALIGN_START); + Gtk::Label *attrtogglerlabel = Gtk::manage(new Gtk::Label(_("Show attributes"))); + attrtogglerlabel->set_margin_right(5); + _attrswitch.get_style_context()->add_class("inkswitch"); + _attrswitch.get_style_context()->add_class("rawstyle"); + _attrswitch.property_active() = attrtoggler; + _attrswitch.property_active().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_attrtoggler)); + attrtogglerlabel->get_style_context()->add_class("inksmall"); + actionsbox->pack_start(*attrtogglerlabel, Gtk::PACK_SHRINK); + actionsbox->pack_start(_attrswitch, Gtk::PACK_SHRINK); + Gtk::RadioButton::Group group; + Gtk::RadioButton *_horizontal = Gtk::manage(new Gtk::RadioButton()); + Gtk::RadioButton *_vertical = Gtk::manage(new Gtk::RadioButton()); + _horizontal->set_image_from_icon_name(INKSCAPE_ICON("horizontal")); + _vertical->set_image_from_icon_name(INKSCAPE_ICON("vertical")); + _horizontal->set_group(group); + _vertical->set_group(group); + _vertical->set_active(dir); + _vertical->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &XmlTree::_toggleDirection), _vertical)); + _horizontal->property_draw_indicator() = false; + _vertical->property_draw_indicator() = false; + actionsbox->pack_end(*_horizontal, false, false, 0); + actionsbox->pack_end(*_vertical, false, false, 0); + _paned.pack2(*attributes, true, false); + contents->pack_start(*actionsbox, false, false, 0); + /* Signal handlers */ + GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(tree)); + g_signal_connect (G_OBJECT(selection), "changed", G_CALLBACK (on_tree_select_row), this); + g_signal_connect_after( G_OBJECT(tree), "tree_move", G_CALLBACK(after_tree_move), this); + + xml_element_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_element_node)); + xml_text_new_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_new_text_node)); + xml_node_duplicate_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_duplicate_node)); + xml_node_delete_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_delete_node)); + unindent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_unindent_node)); + indent_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_indent_node)); + raise_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_raise_node)); + lower_node_button.signal_clicked().connect(sigc::mem_fun(*this, &XmlTree::cmd_lower_node)); + + desktopChangeConn = deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &XmlTree::set_tree_desktop) ); + deskTrack.connect(GTK_WIDGET(gobj())); + set_name("XMLAndAttributesDialog"); + set_spacing(0); + set_size_request(320, 260); + show_all(); + + int panedpos = prefs->getInt("/dialogs/xml/panedpos", 200); + _paned.property_position() = panedpos; + _paned.property_position().signal_changed().connect(sigc::mem_fun(*this, &XmlTree::_resized)); + + tree_reset_context(); + root->pack_start(*Gtk::manage(contents), true, true); + g_assert(desktop != nullptr); + set_tree_desktop(desktop); + +} + +void XmlTree::_resized() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/dialogs/xml/panedpos", _paned.property_position()); +} + +void XmlTree::_toggleDirection(Gtk::RadioButton *vertical) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dir = vertical->get_active(); + prefs->setBool("/dialogs/xml/vertical", dir); + _paned.set_orientation(dir ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL); + _paned.check_resize(); + prefs->setInt("/dialogs/xml/panedpos", _paned.property_position()); +} + +void XmlTree::_attrtoggler() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool attrtoggler = !prefs->getBool("/dialogs/xml/attrtoggler", true); + prefs->setBool("/dialogs/xml/attrtoggler", attrtoggler); + if (attrtoggler) { + attributes->show(); + } else { + attributes->hide(); + } +} + +void XmlTree::present() +{ + set_tree_select(get_dt_select()); + + UI::Widget::Panel::present(); + + if (!_attrswitch.property_active()) { + attributes->hide(); + } +} + +XmlTree::~XmlTree () +{ + set_tree_desktop(nullptr); + if (current_desktop) { + current_desktop->getDocument()->setXMLDialogSelectedObject(nullptr); + } + _message_changed_connection.disconnect(); + _message_context = nullptr; + _message_stack = nullptr; + _message_changed_connection.~connection(); +} + +void XmlTree::setDesktop(SPDesktop *desktop) +{ + Panel::setDesktop(desktop); + deskTrack.setBase(desktop); +} + +/** + * Sets the XML status bar when the tree is selected. + */ +void XmlTree::tree_reset_context() +{ + _message_context->set(Inkscape::NORMAL_MESSAGE, + _("Click to select nodes, drag to rearrange.")); +} + + +void XmlTree::set_tree_desktop(SPDesktop *desktop) +{ + if ( desktop == current_desktop ) { + return; + } + + if (current_desktop) { + sel_changed_connection.disconnect(); + document_replaced_connection.disconnect(); + } + current_desktop = desktop; + if (desktop) { + sel_changed_connection = desktop->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed))); + document_replaced_connection = desktop->connectDocumentReplaced(sigc::mem_fun(this, &XmlTree::on_document_replaced)); + + set_tree_document(desktop->getDocument()); + } else { + set_tree_document(nullptr); + } + +} // end of set_tree_desktop() + + +void XmlTree::set_tree_document(SPDocument *document) +{ + if (document == current_document) { + return; + } + + if (current_document) { + document_uri_set_connection.disconnect(); + } + current_document = document; + if (current_document) { + + document_uri_set_connection = current_document->connectURISet(sigc::bind(sigc::ptr_fun(&on_document_uri_set), current_document)); + on_document_uri_set( current_document->getDocumentURI(), current_document ); + set_tree_repr(current_document->getReprRoot()); + } else { + set_tree_repr(nullptr); + } +} + + + +void XmlTree::set_tree_repr(Inkscape::XML::Node *repr) +{ + if (repr == selected_repr) { + return; + } + + sp_xmlview_tree_set_repr(tree, repr); + if (repr) { + set_tree_select(get_dt_select()); + } else { + set_tree_select(nullptr); + } + + propagate_tree_select(selected_repr); + +} + +/** + * Expand all parent nodes of `repr` + */ +static void expand_parents(SPXMLViewTree *tree, Inkscape::XML::Node *repr) +{ + auto parentrepr = repr->parent(); + if (!parentrepr) { + return; + } + + expand_parents(tree, parentrepr); + + GtkTreeIter node; + if (sp_xmlview_tree_get_repr_node(tree, parentrepr, &node)) { + GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node); + if (path) { + gtk_tree_view_expand_row(GTK_TREE_VIEW(tree), path, false); + } + } +} + +void XmlTree::set_tree_select(Inkscape::XML::Node *repr) +{ + if (selected_repr) { + Inkscape::GC::release(selected_repr); + } + + selected_repr = repr; + if (current_desktop) { + current_desktop->getDocument()->setXMLDialogSelectedObject(nullptr); + } + if (repr) { + GtkTreeIter node; + + Inkscape::GC::anchor(selected_repr); + + expand_parents(tree, repr); + + if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), repr, &node)) { + + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_unselect_all (selection); + + GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), &node); + gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(tree), path, nullptr, TRUE, 0.66, 0.0); + gtk_tree_selection_select_iter(selection, &node); + gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree), path, NULL, false); + gtk_tree_path_free(path); + + } else { + g_message("XmlTree::set_tree_select : Couldn't find repr node"); + } + } else { + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_unselect_all (selection); + + on_tree_unselect_row_disable(); + } + propagate_tree_select(repr); +} + + + +void XmlTree::propagate_tree_select(Inkscape::XML::Node *repr) +{ + if (repr && + (repr->type() == Inkscape::XML::ELEMENT_NODE || + repr->type() == Inkscape::XML::TEXT_NODE || + repr->type() == Inkscape::XML::COMMENT_NODE)) + { + attributes->setRepr(repr); + } else { + attributes->setRepr(nullptr); + } +} + + +Inkscape::XML::Node *XmlTree::get_dt_select() +{ + if (!current_desktop) { + return nullptr; + } + return current_desktop->getSelection()->singleRepr(); +} + + + +void XmlTree::set_dt_select(Inkscape::XML::Node *repr) +{ + if (!current_desktop) { + return; + } + + Inkscape::Selection *selection = current_desktop->getSelection(); + + SPObject *object; + if (repr) { + while ( ( repr->type() != Inkscape::XML::ELEMENT_NODE ) + && repr->parent() ) + { + repr = repr->parent(); + } // end of while loop + + object = current_desktop->getDocument()->getObjectByRepr(repr); + } else { + object = nullptr; + } + + blocked++; + if ( object && in_dt_coordsys(*object) + && !(SP_IS_STRING(object) || + current_desktop->isLayer(object) || + SP_IS_ROOT(object) ) ) + { + /* We cannot set selection to root or string - they are not items and selection is not + * equipped to deal with them */ + selection->set(SP_ITEM(object)); + } + + current_desktop->getDocument()->setXMLDialogSelectedObject(object); + + blocked--; + +} // end of set_dt_select() + + +void XmlTree::on_tree_select_row(GtkTreeSelection *selection, gpointer data) +{ + XmlTree *self = static_cast(data); + + if (self->blocked) { + return; + } + + // Defer the update after all events have been processed. Allows skipping + // of invalid intermediate selection states, like the automatic next row + // selection after `gtk_tree_store_remove`. + if (self->deferred_on_tree_select_row_id == 0) { + self->deferred_on_tree_select_row_id = // + g_idle_add(XmlTree::deferred_on_tree_select_row, data); + } +} + +gboolean XmlTree::deferred_on_tree_select_row(gpointer data) +{ + XmlTree *self = static_cast(data); + + self->deferred_on_tree_select_row_id = 0; + + GtkTreeIter iter; + GtkTreeModel *model; + + if (self->selected_repr) { + Inkscape::GC::release(self->selected_repr); + self->selected_repr = nullptr; + } + + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self->tree)); + + if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + // Nothing selected, update widgets + self->propagate_tree_select(nullptr); + self->set_dt_select(nullptr); + self->on_tree_unselect_row_disable(); + return FALSE; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(model, &iter); + g_assert(repr != nullptr); + + + self->selected_repr = repr; + Inkscape::GC::anchor(self->selected_repr); + + self->propagate_tree_select(self->selected_repr); + + self->set_dt_select(self->selected_repr); + + self->tree_reset_context(); + + self->on_tree_select_row_enable(&iter); + + return FALSE; +} + + +void XmlTree::after_tree_move(SPXMLViewTree * /*tree*/, gpointer value, gpointer data) +{ + XmlTree *self = static_cast(data); + guint val = GPOINTER_TO_UINT(value); + + if (val) { + DocumentUndo::done(self->current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Drag XML subtree")); + } else { + //DocumentUndo::cancel(self->current_document); + /* + * There was a problem with drag & drop, + * data is probably not synchronized, so reload the tree + */ + SPDocument *document = self->current_document; + self->set_tree_document(nullptr); + self->set_tree_document(document); + } +} + +void XmlTree::_set_status_message(Inkscape::MessageType /*type*/, const gchar *message, GtkWidget *widget) +{ + if (widget) { + gtk_label_set_markup(GTK_LABEL(widget), message ? message : ""); + } +} + +void XmlTree::on_tree_select_row_enable(GtkTreeIter *node) +{ + if (!node) { + return; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node); + Inkscape::XML::Node *parent=repr->parent(); + + //on_tree_select_row_enable_if_mutable + xml_node_duplicate_button.set_sensitive(xml_tree_node_mutable(node)); + xml_node_delete_button.set_sensitive(xml_tree_node_mutable(node)); + + //on_tree_select_row_enable_if_element + if (repr->type() == Inkscape::XML::ELEMENT_NODE) { + xml_element_new_button.set_sensitive(true); + xml_text_new_button.set_sensitive(true); + + } else { + xml_element_new_button.set_sensitive(false); + xml_text_new_button.set_sensitive(false); + } + + //on_tree_select_row_enable_if_has_grandparent + { + GtkTreeIter parent; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) { + GtkTreeIter grandparent; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &grandparent, &parent)) { + unindent_node_button.set_sensitive(true); + } else { + unindent_node_button.set_sensitive(false); + } + } else { + unindent_node_button.set_sensitive(false); + } + } + // on_tree_select_row_enable_if_indentable + gboolean indentable = FALSE; + + if (xml_tree_node_mutable(node)) { + Inkscape::XML::Node *prev; + + if ( parent && repr != parent->firstChild() ) { + g_assert(parent->firstChild()); + + // skip to the child just before the current repr + for ( prev = parent->firstChild() ; + prev && prev->next() != repr ; + prev = prev->next() ){}; + + if (prev && (prev->type() == Inkscape::XML::ELEMENT_NODE)) { + indentable = TRUE; + } + } + } + + indent_node_button.set_sensitive(indentable); + + //on_tree_select_row_enable_if_not_first_child + { + if ( parent && repr != parent->firstChild() ) { + raise_node_button.set_sensitive(true); + } else { + raise_node_button.set_sensitive(false); + } + } + + //on_tree_select_row_enable_if_not_last_child + { + if ( parent && (parent->parent() && repr->next())) { + lower_node_button.set_sensitive(true); + } else { + lower_node_button.set_sensitive(false); + } + } +} + + +gboolean XmlTree::xml_tree_node_mutable(GtkTreeIter *node) +{ + // top-level is immutable, obviously + GtkTreeIter parent; + if (!gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &parent, node)) { + return false; + } + + + // if not in base level (where namedview, defs, etc go), we're mutable + GtkTreeIter child; + if (gtk_tree_model_iter_parent(GTK_TREE_MODEL(tree->store), &child, &parent)) { + return true; + } + + Inkscape::XML::Node *repr; + repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), node); + g_assert(repr); + + // don't let "defs" or "namedview" disappear + if ( !strcmp(repr->name(),"svg:defs") || + !strcmp(repr->name(),"sodipodi:namedview") ) { + return false; + } + + // everyone else is okay, I guess. :) + return true; +} + + + +void XmlTree::on_tree_unselect_row_disable() +{ + xml_text_new_button.set_sensitive(false); + xml_element_new_button.set_sensitive(false); + xml_node_delete_button.set_sensitive(false); + xml_node_duplicate_button.set_sensitive(false); + unindent_node_button.set_sensitive(false); + indent_node_button.set_sensitive(false); + raise_node_button.set_sensitive(false); + lower_node_button.set_sensitive(false); +} + +void XmlTree::onCreateNameChanged() +{ + Glib::ustring text = name_entry->get_text(); + /* TODO: need to do checking a little more rigorous than this */ + create_button->set_sensitive(!text.empty()); +} + +void XmlTree::on_desktop_selection_changed() +{ + if (!blocked++) { + Inkscape::XML::Node *node = get_dt_select(); + set_tree_select(node); + } + blocked--; +} + +void XmlTree::on_document_replaced(SPDesktop *dt, SPDocument *doc) +{ + if (current_desktop) + sel_changed_connection.disconnect(); + + sel_changed_connection = dt->getSelection()->connectChanged(sigc::hide(sigc::mem_fun(this, &XmlTree::on_desktop_selection_changed))); + set_tree_document(doc); +} + +void XmlTree::on_document_uri_set(gchar const * /*uri*/, SPDocument * /*document*/) +{ +/* + * Seems to be no way to set the title on a docked dialog + gchar title[500]; + sp_ui_dialog_title_string(Inkscape::Verb::get(SP_VERB_DIALOG_XML_EDITOR), title); + gchar *t = g_strdup_printf("%s: %s", document->getName(), title); + //gtk_window_set_title(GTK_WINDOW(dlg), t); + g_free(t); +*/ +} + +gboolean XmlTree::quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/) +{ + switch (Inkscape::UI::Tools::get_latin_keyval (event)) { + case GDK_KEY_Escape: // defocus + gtk_widget_destroy(w); + return TRUE; + case GDK_KEY_Return: // create + case GDK_KEY_KP_Enter: + gtk_widget_destroy(w); + return TRUE; + } + return FALSE; +} + +void XmlTree::cmd_new_element_node() +{ + Gtk::Dialog dialog; + Gtk::Entry entry; + + dialog.get_content_area()->pack_start(entry); + dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL); + dialog.add_button("Create", Gtk::RESPONSE_OK); + dialog.show_all(); + + int result = dialog.run(); + if (result == Gtk::RESPONSE_OK) { + Glib::ustring new_name = entry.get_text(); + if (!new_name.empty()) { + Inkscape::XML::Document *xml_doc = current_document->getReprDoc(); + Inkscape::XML::Node *new_repr; + new_repr = xml_doc->createElement(new_name.c_str()); + Inkscape::GC::release(new_repr); + selected_repr->appendChild(new_repr); + set_tree_select(new_repr); + set_dt_select(new_repr); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Create new element node")); + } + } +} // end of cmd_new_element_node() + + +void XmlTree::cmd_new_text_node() +{ + g_assert(selected_repr != nullptr); + + Inkscape::XML::Document *xml_doc = current_document->getReprDoc(); + Inkscape::XML::Node *text = xml_doc->createTextNode(""); + selected_repr->appendChild(text); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, + Q_("Undo History / XML dialog|Create new text node")); + + set_tree_select(text); + set_dt_select(text); +} + +void XmlTree::cmd_duplicate_node() +{ + g_assert(selected_repr != nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + Inkscape::XML::Node *dup = selected_repr->duplicate(parent->document()); + parent->addChild(dup, selected_repr); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Duplicate node")); + + GtkTreeIter node; + + if (sp_xmlview_tree_get_repr_node(SP_XMLVIEW_TREE(tree), dup, &node)) { + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_select_iter(selection, &node); + } +} + +void XmlTree::cmd_delete_node() +{ + g_assert(selected_repr != nullptr); + + current_document->setXMLDialogSelectedObject(nullptr); + + Inkscape::XML::Node *parent = selected_repr->parent(); + + sp_repr_unparent(selected_repr); + + if (parent) { + auto parentobject = current_document->getObjectByRepr(parent); + if (parentobject) { + parentobject->requestDisplayUpdate(SP_OBJECT_CHILD_MODIFIED_FLAG); + } + } + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Delete node")); +} + +void XmlTree::cmd_raise_node() +{ + g_assert(selected_repr != nullptr); + + + Inkscape::XML::Node *parent = selected_repr->parent(); + g_return_if_fail(parent != nullptr); + g_return_if_fail(parent->firstChild() != selected_repr); + + Inkscape::XML::Node *ref = nullptr; + Inkscape::XML::Node *before = parent->firstChild(); + while (before && (before->next() != selected_repr)) { + ref = before; + before = before->next(); + } + + parent->changeOrder(selected_repr, ref); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Raise node")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + + + +void XmlTree::cmd_lower_node() +{ + g_assert(selected_repr != nullptr); + + g_return_if_fail(selected_repr->next() != nullptr); + Inkscape::XML::Node *parent = selected_repr->parent(); + + parent->changeOrder(selected_repr, selected_repr->next()); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Lower node")); + + set_tree_select(selected_repr); + set_dt_select(selected_repr); +} + +void XmlTree::cmd_indent_node() +{ + Inkscape::XML::Node *repr = selected_repr; + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + g_return_if_fail(parent != nullptr); + g_return_if_fail(parent->firstChild() != repr); + + Inkscape::XML::Node* prev = parent->firstChild(); + while (prev && (prev->next() != repr)) { + prev = prev->next(); + } + g_return_if_fail(prev != nullptr); + g_return_if_fail(prev->type() == Inkscape::XML::ELEMENT_NODE); + + Inkscape::XML::Node* ref = nullptr; + if (prev->firstChild()) { + for( ref = prev->firstChild() ; ref->next() ; ref = ref->next() ){}; + } + + parent->removeChild(repr); + prev->addChild(repr, ref); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Indent node")); + set_tree_select(repr); + set_dt_select(repr); + +} // end of cmd_indent_node() + + + +void XmlTree::cmd_unindent_node() +{ + Inkscape::XML::Node *repr = selected_repr; + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + g_return_if_fail(parent); + Inkscape::XML::Node *grandparent = parent->parent(); + g_return_if_fail(grandparent); + + parent->removeChild(repr); + grandparent->addChild(repr, parent); + + DocumentUndo::done(current_document, SP_VERB_DIALOG_XML_EDITOR, Q_("Undo History / XML dialog|Unindent node")); + + set_tree_select(repr); + set_dt_select(repr); + +} // end of cmd_unindent_node() + +/** Returns true iff \a item is suitable to be included in the selection, in particular + whether it has a bounding box in the desktop coordinate system for rendering resize handles. + + Descendents of nodes (markers etc.) return false, for example. +*/ +bool XmlTree::in_dt_coordsys(SPObject const &item) +{ + /* Definition based on sp_item_i2doc_affine. */ + SPObject const *child = &item; + g_return_val_if_fail(child != nullptr, false); + for(;;) { + if (!SP_IS_ITEM(child)) { + return false; + } + SPObject const * const parent = child->parent; + if (parent == nullptr) { + break; + } + child = parent; + } + g_assert(SP_IS_ROOT(child)); + /* Relevance: Otherwise, I'm not sure whether to return true or false. */ + return true; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/dialog/xml-tree.h b/src/ui/dialog/xml-tree.h new file mode 100644 index 0000000..b63417a --- /dev/null +++ b/src/ui/dialog/xml-tree.h @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * This is XML tree editor, which allows direct modifying of all elements + * of Inkscape document, including foreign ones. + *//* + * Authors: see git history + * Lauris Kaplinski, 2000 + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_DIALOGS_XML_TREE_H +#define SEEN_UI_DIALOGS_XML_TREE_H + +#include + +#include "ui/widget/panel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "message.h" + +#include "ui/dialog/attrdialog.h" +#include "ui/dialog/desktop-tracker.h" + + +class SPDesktop; +class SPObject; +struct SPXMLViewAttrList; +struct SPXMLViewContent; +struct SPXMLViewTree; + +namespace Inkscape { +class MessageStack; +class MessageContext; + +namespace XML { +class Node; +} + +namespace UI { +namespace Dialog { + +/** + * A dialog widget to view and edit the document xml + * + */ + +class XmlTree : public Widget::Panel { +public: + XmlTree (); + ~XmlTree () override; + + static XmlTree &getInstance() { return *new XmlTree(); } + +private: + + /** + * Is invoked by the desktop tracker when the desktop changes. + */ + void set_tree_desktop(SPDesktop *desktop); + + /** + * Is invoked when the document changes + */ + void set_tree_document(SPDocument *document); + + /** + * Select a node in the xml tree + */ + void set_tree_repr(Inkscape::XML::Node *repr); + + /** + * Sets the XML status bar when the tree is selected. + */ + void tree_reset_context(); + + /** + * Is the selected tree node editable + */ + gboolean xml_tree_node_mutable(GtkTreeIter *node); + + /** + * Callback to close the add dialog on Escape key + */ + static gboolean quit_on_esc (GtkWidget *w, GdkEventKey *event, GObject */*tbl*/); + + /** + * Select a node in the xml tree + */ + void set_tree_select(Inkscape::XML::Node *repr); + + /** + * Set the attribute list to match the selected node in the tree + */ + void propagate_tree_select(Inkscape::XML::Node *repr); + + /** + * Find the current desktop selection + */ + Inkscape::XML::Node *get_dt_select(); + + /** + * Select the current desktop selection + */ + void set_dt_select(Inkscape::XML::Node *repr); + + /** + * Callback for a node in the tree being selected + */ + static void on_tree_select_row(GtkTreeSelection *selection, gpointer data); + /** + * Callback for deferring the `on_tree_select_row` response in order to + * skip invalid intermediate selection states. In particular, + * `gtk_tree_store_remove` makes an undesired selection that we will + * immediately revert and don't want to an early response for. + */ + static gboolean deferred_on_tree_select_row(gpointer); + /// Event source ID for the last scheduled `deferred_on_tree_select_row` event. + guint deferred_on_tree_select_row_id = 0; + + /** + * Callback when a node is moved in the tree + */ + static void after_tree_move(SPXMLViewTree *tree, gpointer value, gpointer data); + + /** + * Callback for when an attribute is edited. + */ + //static void on_attr_edited(SPXMLViewAttrList *attributes, const gchar * name, const gchar * value, gpointer /*data*/); + + /** + * Callback for when attribute list values change + */ + //static void on_attr_row_changed(SPXMLViewAttrList *attributes, const gchar * name, gpointer data); + + /** + * Enable widgets based on current selections + */ + void on_tree_select_row_enable(GtkTreeIter *node); + void on_tree_unselect_row_disable(); + void on_tree_unselect_row_hide(); + void on_attr_unselect_row_disable(); + + void onNameChanged(); + void onCreateNameChanged(); + + /** + * Callbacks for changes in desktop selection and current document + */ + void on_desktop_selection_changed(); + void on_document_replaced(SPDesktop *dt, SPDocument *document); + static void on_document_uri_set(gchar const *uri, SPDocument *document); + + static void _set_status_message(Inkscape::MessageType type, const gchar *message, GtkWidget *dialog); + + /** + * Callbacks for toolbar buttons being pressed + */ + void cmd_new_element_node(); + void cmd_new_text_node(); + void cmd_duplicate_node(); + void cmd_delete_node(); + void cmd_raise_node(); + void cmd_lower_node(); + void cmd_indent_node(); + void cmd_unindent_node(); + + void present() override; + void _attrtoggler(); + void _toggleDirection(Gtk::RadioButton *vertical); + void _resized(); + bool in_dt_coordsys(SPObject const &item); + + /** + * Can be invoked for setting the desktop. Currently not used. + */ + void setDesktop(SPDesktop *desktop) override; + + /** + * Flag to ensure only one operation is performed at once + */ + gint blocked; + + bool _updating; + /** + * Status bar + */ + std::shared_ptr _message_stack; + std::unique_ptr _message_context; + + /** + * Signal handlers + */ + sigc::connection _message_changed_connection; + sigc::connection document_replaced_connection; + sigc::connection document_uri_set_connection; + sigc::connection sel_changed_connection; + + /** + * Current document and desktop this dialog is attached to + */ + SPDesktop *current_desktop; + SPDocument *current_document; + + gint selected_attr; + Inkscape::XML::Node *selected_repr; + + /* XmlTree Widgets */ + SPXMLViewTree *tree; + //SPXMLViewAttrList *attributes; + AttrDialog *attributes; + Gtk::Box *_attrbox; + + /* XML Node Creation pop-up window */ + Gtk::Entry *name_entry; + Gtk::Button *create_button; + Gtk::Paned _paned; + + Gtk::VBox node_box; + Gtk::HBox status_box; + Gtk::Switch _attrswitch; + Gtk::Label status; + Gtk::Toolbar tree_toolbar; + Gtk::ToolButton xml_element_new_button; + Gtk::ToolButton xml_text_new_button; + Gtk::ToolButton xml_node_delete_button; + Gtk::SeparatorToolItem separator; + Gtk::ToolButton xml_node_duplicate_button; + Gtk::SeparatorToolItem separator2; + Gtk::ToolButton unindent_node_button; + Gtk::ToolButton indent_node_button; + Gtk::ToolButton raise_node_button; + Gtk::ToolButton lower_node_button; + + GtkWidget *new_window; + + DesktopTracker deskTrack; + sigc::connection desktopChangeConn; +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/drag-and-drop.cpp b/src/ui/drag-and-drop.cpp new file mode 100644 index 0000000..d96d3e6 --- /dev/null +++ b/src/ui/drag-and-drop.cpp @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Drag and drop of drawings onto canvas. + */ + +/* Authors: + * + * Copyright (C) Tavmjong Bah 2019 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "drag-and-drop.h" + +#include // Internationalization + +#include "desktop-style.h" +#include "document-undo.h" +#include "gradient-drag.h" +#include "file.h" +#include "inkscape.h" // SP_ACTIVE_DESKTOP +#include "splivarot.h" +#include "style.h" + +#include "display/sp-canvas.h" // window to world transform + +#include "extension/db.h" +#include "extension/find_extension_by_mime.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" + +#include "svg/svg-color.h" // write color + +#include "ui/clipboard.h" +#include "ui/interface.h" +#include "ui/tools/tool-base.h" + +#include "widgets/ege-paint-def.h" + +using Inkscape::DocumentUndo; + +/* Drag and Drop */ +enum ui_drop_target_info { + URI_LIST, + SVG_XML_DATA, + SVG_DATA, + PNG_DATA, + JPEG_DATA, + IMAGE_DATA, + APP_X_INKY_COLOR, + APP_X_COLOR, + APP_OSWB_COLOR, + APP_X_INK_PASTE +}; + +static GtkTargetEntry ui_drop_target_entries [] = { + {(gchar *)"text/uri-list", 0, URI_LIST }, + {(gchar *)"image/svg+xml", 0, SVG_XML_DATA }, + {(gchar *)"image/svg", 0, SVG_DATA }, + {(gchar *)"image/png", 0, PNG_DATA }, + {(gchar *)"image/jpeg", 0, JPEG_DATA }, +#if ENABLE_MAGIC_COLORS + {(gchar *)"application/x-inkscape-color", 0, APP_X_INKY_COLOR}, +#endif // ENABLE_MAGIC_COLORS + {(gchar *)"application/x-oswb-color", 0, APP_OSWB_COLOR }, + {(gchar *)"application/x-color", 0, APP_X_COLOR }, + {(gchar *)"application/x-inkscape-paste", 0, APP_X_INK_PASTE } +}; + +static GtkTargetEntry *completeDropTargets = nullptr; +static int completeDropTargetsCount = 0; + +static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries); + +/* Drag and Drop */ +void +ink_drag_data_received(GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint /*event_time*/, + gpointer /*user_data*/) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + switch (info) { +#if ENABLE_MAGIC_COLORS + case APP_X_INKY_COLOR: + { + int destX = 0; + int destY = 0; + gtk_widget_translate_coordinates( widget, &(desktop->canvas->widget), x, y, &destX, &destY ); + Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) ); + + SPItem *item = desktop->getItemAtPoint( where, true ); + if ( item ) + { + bool fillnotstroke = (drag_context->action != GDK_ACTION_MOVE); + + if ( data->length >= 8 ) { + cmsHPROFILE srgbProf = cmsCreate_sRGBProfile(); + + gchar c[64] = {0}; + // Careful about endian issues. + guint16* dataVals = (guint16*)data->data; + sp_svg_write_color( c, sizeof(c), + SP_RGBA32_U_COMPOSE( + 0x0ff & (dataVals[0] >> 8), + 0x0ff & (dataVals[1] >> 8), + 0x0ff & (dataVals[2] >> 8), + 0xff // can't have transparency in the color itself + //0x0ff & (data->data[3] >> 8), + )); + SPCSSAttr *css = sp_repr_css_attr_new(); + bool updatePerformed = false; + + if ( data->length > 14 ) { + int flags = dataVals[4]; + + // piggie-backed palette entry info + int index = dataVals[5]; + Glib::ustring palName; + for ( int i = 0; i < dataVals[6]; i++ ) { + palName += (gunichar)dataVals[7+i]; + } + + // Now hook in a magic tag of some sort. + if ( !palName.empty() && (flags & 1) ) { + gchar* str = g_strdup_printf("%d|", index); + palName.insert( 0, str ); + g_free(str); + str = 0; + + item->setAttribute( + fillnotstroke ? "inkscape:x-fill-tag":"inkscape:x-stroke-tag", + palName.c_str(), + false ); + item->updateRepr(); + + sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", c ); + updatePerformed = true; + } + } + + if ( !updatePerformed ) { + sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", c ); + } + + sp_desktop_apply_css_recursive( item, css, true ); + item->updateRepr(); + + SPDocumentUndo::done( doc , SP_VERB_NONE, + _("Drop color")); + + if ( srgbProf ) { + cmsCloseProfile( srgbProf ); + } + } + } + } + break; +#endif // ENABLE_MAGIC_COLORS + + case APP_X_COLOR: + { + int destX = 0; + int destY = 0; + gtk_widget_translate_coordinates( widget, GTK_WIDGET(desktop->canvas), x, y, &destX, &destY ); + Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) ); + Geom::Point const button_dt(desktop->w2d(where)); + Geom::Point const button_doc(desktop->dt2doc(button_dt)); + + if ( gtk_selection_data_get_length (data) == 8 ) { + gchar colorspec[64] = {0}; + // Careful about endian issues. + guint16* dataVals = (guint16*)gtk_selection_data_get_data (data); + sp_svg_write_color( colorspec, sizeof(colorspec), + SP_RGBA32_U_COMPOSE( + 0x0ff & (dataVals[0] >> 8), + 0x0ff & (dataVals[1] >> 8), + 0x0ff & (dataVals[2] >> 8), + 0xff // can't have transparency in the color itself + //0x0ff & (data->data[3] >> 8), + )); + + SPItem *item = desktop->getItemAtPoint( where, true ); + + bool consumed = false; + if (desktop->event_context && desktop->event_context->get_drag()) { + consumed = desktop->event_context->get_drag()->dropColor(item, colorspec, button_dt); + if (consumed) { + DocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient") ); + desktop->event_context->get_drag()->updateDraggers(); + } + } + + //if (!consumed && tools_active(desktop, TOOLS_TEXT)) { + // consumed = sp_text_context_drop_color(c, button_doc); + // if (consumed) { + // SPDocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient stop")); + // } + //} + + if (!consumed && item) { + bool fillnotstroke = (gdk_drag_context_get_actions (drag_context) != GDK_ACTION_MOVE); + if (fillnotstroke && + (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) { + Path *livarot_path = Path_for_item(item, true, true); + livarot_path->ConvertWithBackData(0.04); + + boost::optional position = get_nearest_position_on_Path(livarot_path, button_doc); + if (position) { + Geom::Point nearest = get_point_on_Path(livarot_path, position->piece, position->t); + Geom::Point delta = nearest - button_doc; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + delta = desktop->d2w(delta); + double stroke_tolerance = + ( !item->style->stroke.isNone() ? + desktop->current_zoom() * + item->style->stroke_width.computed * + item->i2dt_affine().descrim() * 0.5 + : 0.0) + + prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if (Geom::L2 (delta) < stroke_tolerance) { + fillnotstroke = false; + } + } + delete livarot_path; + } + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", colorspec ); + + sp_desktop_apply_css_recursive( item, css, true ); + item->updateRepr(); + + DocumentUndo::done( doc , SP_VERB_NONE, + _("Drop color") ); + } + } + } + break; + + case APP_OSWB_COLOR: + { + bool worked = false; + Glib::ustring colorspec; + if ( gtk_selection_data_get_format (data) == 8 ) { + ege::PaintDef color; + worked = color.fromMIMEData("application/x-oswb-color", + reinterpret_cast(gtk_selection_data_get_data (data)), + gtk_selection_data_get_length (data), + gtk_selection_data_get_format (data)); + if ( worked ) { + if ( color.getType() == ege::PaintDef::CLEAR ) { + colorspec = ""; // TODO check if this is sufficient + } else if ( color.getType() == ege::PaintDef::NONE ) { + colorspec = "none"; + } else { + unsigned int r = color.getR(); + unsigned int g = color.getG(); + unsigned int b = color.getB(); + + SPGradient* matches = nullptr; + std::vector gradients = doc->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( color.descr == grad->getId() ) { + if ( grad->hasStops() ) { + matches = grad; + break; + } + } + } + if (matches) { + colorspec = "url(#"; + colorspec += matches->getId(); + colorspec += ")"; + } else { + gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b); + colorspec = tmp; + g_free(tmp); + } + } + } + } + if ( worked ) { + int destX = 0; + int destY = 0; + gtk_widget_translate_coordinates( widget, GTK_WIDGET(desktop->canvas), x, y, &destX, &destY ); + Geom::Point where( sp_canvas_window_to_world( desktop->canvas, Geom::Point( destX, destY ) ) ); + Geom::Point const button_dt(desktop->w2d(where)); + Geom::Point const button_doc(desktop->dt2doc(button_dt)); + + SPItem *item = desktop->getItemAtPoint( where, true ); + + bool consumed = false; + if (desktop->event_context && desktop->event_context->get_drag()) { + consumed = desktop->event_context->get_drag()->dropColor(item, colorspec.c_str(), button_dt); + if (consumed) { + DocumentUndo::done( doc , SP_VERB_NONE, _("Drop color on gradient") ); + desktop->event_context->get_drag()->updateDraggers(); + } + } + + if (!consumed && item) { + bool fillnotstroke = (gdk_drag_context_get_actions (drag_context) != GDK_ACTION_MOVE); + if (fillnotstroke && + (SP_IS_SHAPE(item) || SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item))) { + Path *livarot_path = Path_for_item(item, true, true); + livarot_path->ConvertWithBackData(0.04); + + boost::optional position = get_nearest_position_on_Path(livarot_path, button_doc); + if (position) { + Geom::Point nearest = get_point_on_Path(livarot_path, position->piece, position->t); + Geom::Point delta = nearest - button_doc; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + delta = desktop->d2w(delta); + double stroke_tolerance = + ( !item->style->stroke.isNone() ? + desktop->current_zoom() * + item->style->stroke_width.computed * + item->i2dt_affine().descrim() * 0.5 + : 0.0) + + prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if (Geom::L2 (delta) < stroke_tolerance) { + fillnotstroke = false; + } + } + delete livarot_path; + } + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property( css, fillnotstroke ? "fill":"stroke", colorspec.c_str() ); + + sp_desktop_apply_css_recursive( item, css, true ); + item->updateRepr(); + + DocumentUndo::done( doc , SP_VERB_NONE, + _("Drop color") ); + } + } + } + break; + + case SVG_DATA: + case SVG_XML_DATA: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/onimport", true); + gchar *svgdata = (gchar *)gtk_selection_data_get_data (data); + + Inkscape::XML::Document *rnewdoc = sp_repr_read_mem(svgdata, gtk_selection_data_get_length (data), SP_SVG_NS_URI); + + if (rnewdoc == nullptr) { + sp_ui_error_dialog(_("Could not parse SVG data")); + return; + } + + Inkscape::XML::Node *repr = rnewdoc->root(); + gchar const *style = repr->attribute("style"); + + Inkscape::XML::Node *newgroup = rnewdoc->createElement("svg:g"); + newgroup->setAttribute("style", style); + + Inkscape::XML::Document * xml_doc = doc->getReprDoc(); + for (Inkscape::XML::Node *child = repr->firstChild(); child != nullptr; child = child->next()) { + Inkscape::XML::Node *newchild = child->duplicate(xml_doc); + newgroup->appendChild(newchild); + } + + Inkscape::GC::release(rnewdoc); + + // Add it to the current layer + + // Greg's edits to add intelligent positioning of svg drops + SPObject *new_obj = nullptr; + new_obj = desktop->currentLayer()->appendChildRepr(newgroup); + + Inkscape::Selection *selection = desktop->getSelection(); + selection->set(SP_ITEM(new_obj)); + + // move to mouse pointer + { + desktop->getDocument()->ensureUpToDate(); + Geom::OptRect sel_bbox = selection->visualBounds(); + if (sel_bbox) { + Geom::Point m( desktop->point() - sel_bbox->midpoint() ); + selection->moveRelative(m, false); + } + } + + Inkscape::GC::release(newgroup); + DocumentUndo::done( doc, SP_VERB_NONE, + _("Drop SVG") ); + prefs->setBool("/options/onimport", false); + break; + } + + case URI_LIST: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/onimport", true); + gchar *uri = (gchar *)gtk_selection_data_get_data (data); + sp_ui_import_files(uri); + prefs->setBool("/options/onimport", false); + break; + } + + case APP_X_INK_PASTE: { + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + cm->paste(desktop); + DocumentUndo::done( doc, SP_VERB_NONE, _("Drop Symbol") ); + break; + } + + case PNG_DATA: + case JPEG_DATA: + case IMAGE_DATA: { + Inkscape::Extension::Extension *ext = Inkscape::Extension::find_by_mime((info == JPEG_DATA ? "image/jpeg" : "image/png")); + bool save = (strcmp(ext->get_param_optiongroup("link"), "embed") == 0); + ext->set_param_optiongroup("link", "embed"); + ext->set_gui(false); + + gchar *filename = g_build_filename( g_get_tmp_dir(), "inkscape-dnd-import", NULL ); + g_file_set_contents(filename, + reinterpret_cast(gtk_selection_data_get_data (data)), + gtk_selection_data_get_length (data), + nullptr); + file_import(doc, filename, ext); + g_free(filename); + + ext->set_param_optiongroup("link", save ? "embed" : "link"); + ext->set_gui(true); + DocumentUndo::done( doc , SP_VERB_NONE, + _("Drop bitmap image") ); + break; + } + } +} + +#include "ui/tools/gradient-tool.h" + +void ink_drag_motion( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData */*data*/, + guint /*info*/, + guint /*event_time*/, + gpointer /*user_data*/) +{ +// SPDocument *doc = SP_ACTIVE_DOCUMENT; +// SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + +// g_message("drag-n-drop motion (%4d, %4d) at %d", x, y, event_time); +} + +static void ink_drag_leave( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + guint /*event_time*/, + gpointer /*user_data*/ ) +{ +// g_message("drag-n-drop leave at %d", event_time); +} + +void +ink_drag_setup(Gtk::Widget* win) +{ + if ( completeDropTargets == nullptr || completeDropTargetsCount == 0 ) + { + std::vector types; + + std::vector list = Gdk::Pixbuf::get_formats(); + for (auto one:list) { + std::vector typesXX = one.get_mime_types(); + for (auto i:typesXX) { + types.push_back(i); + } + } + completeDropTargetsCount = nui_drop_target_entries + types.size(); + completeDropTargets = new GtkTargetEntry[completeDropTargetsCount]; + for ( int i = 0; i < (int)nui_drop_target_entries; i++ ) { + completeDropTargets[i] = ui_drop_target_entries[i]; + } + int pos = nui_drop_target_entries; + + for (auto & type : types) { + completeDropTargets[pos].target = g_strdup(type.c_str()); + completeDropTargets[pos].flags = 0; + completeDropTargets[pos].info = IMAGE_DATA; + pos++; + } + } + + gtk_drag_dest_set((GtkWidget*)win->gobj(), + GTK_DEST_DEFAULT_ALL, + completeDropTargets, + completeDropTargetsCount, + GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE)); + + g_signal_connect(G_OBJECT(win->gobj()), + "drag_data_received", + G_CALLBACK(ink_drag_data_received), + NULL); + + g_signal_connect(G_OBJECT(win->gobj()), + "drag_motion", + G_CALLBACK(ink_drag_motion), + NULL); + + g_signal_connect(G_OBJECT(win->gobj()), + "drag_leave", + G_CALLBACK(ink_drag_leave), + NULL); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/drag-and-drop.h b/src/ui/drag-and-drop.h new file mode 100644 index 0000000..fb3a445 --- /dev/null +++ b/src/ui/drag-and-drop.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_DRAG_AND_DROP_H +#define SEEN_CANVAS_DRAG_AND_DROP_H + +/** + * @file + * Drag and drop of drawings onto canvas. + */ + +/* Authors: + * + * Copyright (C) Tavmjong Bah 2019 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +void ink_drag_setup(Gtk::Widget* win); + +static void ink_drag_data_received(GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data); +static void ink_drag_motion( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data ); +static void ink_drag_leave( GtkWidget *widget, + GdkDragContext *drag_context, + guint event_time, + gpointer user_data ); + +#endif // SEEN_CANVAS_DRAG_AND_DROP_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/draw-anchor.cpp b/src/ui/draw-anchor.cpp new file mode 100644 index 0000000..1b32525 --- /dev/null +++ b/src/ui/draw-anchor.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Anchors implementation. + */ + +/* + * Authors: + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "ui/draw-anchor.h" +#include "desktop.h" +#include "ui/tools/tool-base.h" +#include "ui/tools/lpe-tool.h" +#include "display/sodipodi-ctrl.h" +#include "display/curve.h" +#include "ui/control-manager.h" + +using Inkscape::ControlManager; + +#define FILL_COLOR_NORMAL 0xffffff7f +#define FILL_COLOR_MOUSEOVER 0xff0000ff + +/** + * Creates an anchor object and initializes it. + */ +SPDrawAnchor *sp_draw_anchor_new(Inkscape::UI::Tools::FreehandBase *dc, SPCurve *curve, bool start, Geom::Point delta) +{ + if (SP_IS_LPETOOL_CONTEXT(dc)) { + // suppress all kinds of anchors in LPEToolContext + return nullptr; + } + + SPDrawAnchor *a = g_new(SPDrawAnchor, 1); + + a->dc = dc; + a->curve = curve; + curve->ref(); + a->start = start; + a->active = FALSE; + a->dp = delta; + a->ctrl = ControlManager::getManager().createControl(dc->getDesktop().getControls(), Inkscape::CTRL_TYPE_ANCHOR); + + SP_CTRL(a->ctrl)->moveto(delta); + + ControlManager::getManager().track(a->ctrl); + + return a; +} + +/** + * Destroys the anchor's canvas item and frees the anchor object. + */ +SPDrawAnchor *sp_draw_anchor_destroy(SPDrawAnchor *anchor) +{ + if (anchor->curve) { + anchor->curve->unref(); + } + if (anchor->ctrl) { + sp_canvas_item_destroy(anchor->ctrl); + } + g_free(anchor); + return nullptr; +} + +/** + * Test if point is near anchor, if so fill anchor on canvas and return + * pointer to it or NULL. + */ +SPDrawAnchor *sp_draw_anchor_test(SPDrawAnchor *anchor, Geom::Point w, bool activate) +{ + SPCtrl *ctrl = SP_CTRL(anchor->ctrl); + + if ( activate && ( Geom::LInfty( w - anchor->dc->getDesktop().d2w(anchor->dp) ) <= (ctrl->box.width() / 2.0) ) ) { + if (!anchor->active) { + ControlManager::getManager().setControlResize(anchor->ctrl, 4); + g_object_set(anchor->ctrl, "fill_color", FILL_COLOR_MOUSEOVER, NULL); + anchor->active = TRUE; + } + return anchor; + } + + if (anchor->active) { + ControlManager::getManager().setControlResize(anchor->ctrl, 0); + g_object_set(anchor->ctrl, "fill_color", FILL_COLOR_NORMAL, NULL); + anchor->active = FALSE; + } + + return nullptr; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/draw-anchor.h b/src/ui/draw-anchor.h new file mode 100644 index 0000000..b631a48 --- /dev/null +++ b/src/ui/draw-anchor.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_DRAW_ANCHOR_H +#define SEEN_DRAW_ANCHOR_H + +/** \file + * Drawing anchors. + */ + +#include <2geom/point.h> + +namespace Inkscape { +namespace UI { +namespace Tools { + +class FreehandBase; + +} +} +} + +class SPCurve; +struct SPCanvasItem; + +/// The drawing anchor. +/// \todo Make this a regular knot, this will allow setting statusbar tips. +struct SPDrawAnchor { + Inkscape::UI::Tools::FreehandBase *dc; + SPCurve *curve; + unsigned int start : 1; + unsigned int active : 1; + Geom::Point dp; + SPCanvasItem *ctrl; +}; + + +SPDrawAnchor *sp_draw_anchor_new(Inkscape::UI::Tools::FreehandBase *dc, SPCurve *curve, bool start, + Geom::Point delta); +SPDrawAnchor *sp_draw_anchor_destroy(SPDrawAnchor *anchor); +SPDrawAnchor *sp_draw_anchor_test(SPDrawAnchor *anchor, Geom::Point w, bool activate); + + +#endif /* !SEEN_DRAW_ANCHOR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/event-debug.h b/src/ui/event-debug.h new file mode 100644 index 0000000..150b847 --- /dev/null +++ b/src/ui/event-debug.h @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_EVENT_DEBUG_H +#define SEEN_UI_EVENT_DEBUG_H + +/** + * @file + * Dump event data. + */ +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +// See: https://developer.gnome.org/gdk3/stable/gdk3-Events.html + +inline void ui_dump_event (GdkEvent *event, Glib::ustring const &prefix, bool merge = true) { + + static GdkEventType old_type = GDK_NOTHING; + static unsigned count = 0; + + // Doesn't usually help to dump a zillion events of the same type (e.g. GDK_MOTION_NOTIFY). + ++count; + if (merge && event->type == old_type) { + if ( count == 1 ) { + std::cout << prefix << " ... ditto" << std::endl; + } + return; + } + count = 0; + old_type = event->type; + + std::cout << prefix << ": "; + + switch (event->type) { + + case GDK_KEY_PRESS: + std::cout << "GDK_KEY_PRESS: " << event->key.hardware_keycode << std::endl; + break; + case GDK_KEY_RELEASE: + std::cout << "GDK_KEY_RELEASE: " << event->key.hardware_keycode << std::endl; + break; + + case GDK_BUTTON_PRESS: + std::cout << "GDK_BUTTON_PRESS: " << event->button.button << std::endl; + break; + case GDK_2BUTTON_PRESS: + std::cout << "GDK_2BUTTON_PRESS: " << event->button.button << std::endl; + break; + case GDK_3BUTTON_PRESS: + std::cout << "GDK_3BUTTON_PRESS: " << event->button.button << std::endl; + break; + case GDK_BUTTON_RELEASE: + std::cout << "GDK_BUTTON_RELEASE: " << event->button.button << std::endl; + break; + + case GDK_SCROLL: + std::cout << "GDK_SCROLL" << std::endl; + break; + + case GDK_MOTION_NOTIFY: + std::cout << "GDK_MOTION_NOTIFY" << std::endl; + break; + case GDK_ENTER_NOTIFY: + std::cout << "GDK_ENTER_NOTIFY" << std::endl; + break; + case GDK_LEAVE_NOTIFY: + std::cout << "GDK_LEAVE_NOTIFY" << std::endl; + break; + + case GDK_TOUCH_BEGIN: + std::cout << "GDK_TOUCH_BEGIN" << std::endl; + break; + case GDK_TOUCH_UPDATE: + std::cout << "GDK_TOUCH_UPDATE" << std::endl; + break; + case GDK_TOUCH_END: + std::cout << "GDK_TOUCH_END" << std::endl; + break; + case GDK_TOUCH_CANCEL: + std::cout << "GDK_TOUCH_CANCEL" << std::endl; + break; + case GDK_TOUCHPAD_SWIPE: + std::cout << "GDK_TOUCHPAD_SWIPE" << std::endl; + break; + case GDK_TOUCHPAD_PINCH: + std::cout << "GDK_TOUCHPAD_PINCH" << std::endl; + break; + case GDK_PAD_BUTTON_PRESS: + std::cout << "GDK_PAD_BUTTON_PRESS" << std::endl; + break; + case GDK_PAD_BUTTON_RELEASE: + std::cout << "GDK_PAD_BUTTON_RELEASE" << std::endl; + break; + case GDK_PAD_RING: + std::cout << "GDK_PAD_RING" << std::endl; + break; + case GDK_PAD_STRIP: + std::cout << "GDK_PAD_STRIP" << std::endl; + break; + case GDK_PAD_GROUP_MODE: + std::cout << "GDK_PAD_GROUP_MODE" << std::endl; + break; + default: + std::cout << "GDK event not recognized!" << std::endl; + break; + } +} + +#endif // SEEN_UI_EVENT_DEBUG_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/icon-loader.cpp b/src/ui/icon-loader.cpp new file mode 100644 index 0000000..62159c2 --- /dev/null +++ b/src/ui/icon-loader.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Icon Loader + * + * Icon Loader management code + * + * Authors: + * Jabiertxo Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "icon-loader.h" +#include "inkscape.h" +#include "io/resource.h" +#include "svg/svg-color.h" +#include "widgets/toolbox.h" +#include +#include +#include +#include +#include + +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gint size) +{ + Gtk::Image *icon = new Gtk::Image(); + icon->set_from_icon_name(icon_name, Gtk::IconSize(Gtk::ICON_SIZE_BUTTON)); + icon->set_pixel_size(size); + return icon; +} + +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::IconSize icon_size) +{ + Gtk::Image *icon = new Gtk::Image(); + icon->set_from_icon_name(icon_name, icon_size); + return icon; +} + +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size) +{ + Gtk::Image *icon = new Gtk::Image(); + icon->set_from_icon_name(icon_name, icon_size); + return icon; +} + +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gchar const *prefs_size) +{ + Gtk::IconSize icon_size = Inkscape::UI::ToolboxFactory::prefToSize_mm(prefs_size); + return sp_get_icon_image(icon_name, icon_size); +} + +GtkWidget *sp_get_icon_image(Glib::ustring icon_name, GtkIconSize icon_size) +{ + return gtk_image_new_from_icon_name(icon_name.c_str(), icon_size); +} + +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, gint size) +{ + Glib::RefPtr display = Gdk::Display::get_default(); + Glib::RefPtr screen = display->get_default_screen(); + Glib::RefPtr icon_theme = Gtk::IconTheme::get_for_screen(screen); + Glib::RefPtr _icon_pixbuf; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name + Glib::ustring("-symbolic"), size, Gtk::ICON_LOOKUP_FORCE_SIZE); + if (iconinfo && SP_ACTIVE_DESKTOP->getToplevel()) { + bool was_symbolic = false; + Glib::ustring css_str = ""; + Glib::ustring themeiconname = prefs->getString("/theme/iconTheme"); + guint32 colorsetbase = prefs->getUInt("/theme/" + themeiconname + "/symbolicBaseColor", 0x2E3436ff); + guint32 colorsetsuccess = prefs->getUInt("/theme/" + themeiconname + "/symbolicSuccessColor", 0x4AD589ff); + guint32 colorsetwarning = prefs->getUInt("/theme/" + themeiconname + "/symbolicWarningColor", 0xF57900ff); + guint32 colorseterror = prefs->getUInt("/theme/" + themeiconname + "/symbolicErrorColor", 0xCC0000ff); + gchar colornamed[64]; + gchar colornamedsuccess[64]; + gchar colornamedwarning[64]; + gchar colornamederror[64]; + sp_svg_write_color(colornamed, sizeof(colornamed), colorsetbase); + sp_svg_write_color(colornamedsuccess, sizeof(colornamedsuccess), colorsetsuccess); + sp_svg_write_color(colornamedwarning, sizeof(colornamedwarning), colorsetwarning); + sp_svg_write_color(colornamederror, sizeof(colornamederror), colorseterror); + _icon_pixbuf = + iconinfo.load_symbolic(Gdk::RGBA(colornamed), Gdk::RGBA(colornamedsuccess), + Gdk::RGBA(colornamedwarning), Gdk::RGBA(colornamederror), was_symbolic); + } else { + Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + _icon_pixbuf = iconinfo.load_icon(); + } + } else { + Gtk::IconInfo iconinfo = icon_theme->lookup_icon(icon_name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + _icon_pixbuf = iconinfo.load_icon(); + } + return _icon_pixbuf; +} + +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::IconSize icon_size) +{ + int width, height; + Gtk::IconSize::lookup(icon_size, width, height); + return sp_get_icon_pixbuf(icon_name, width); +} + +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size) +{ + int width, height; + Gtk::IconSize::lookup(Gtk::IconSize(icon_size), width, height); + return sp_get_icon_pixbuf(icon_name, width); +} + +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size) +{ + gint width, height; + gtk_icon_size_lookup(icon_size, &width, &height); + return sp_get_icon_pixbuf(icon_name, width); +} + +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, gchar const *prefs_size) +{ + // Load icon based in preference size defined allowed values are: + //"/toolbox/tools/small" Toolbox icon size + //"/toolbox/small" Control bar icon size + //"/toolbox/secondary" Secondary toolbar icon size + GtkIconSize icon_size = Inkscape::UI::ToolboxFactory::prefToSize(prefs_size); + return sp_get_icon_pixbuf(icon_name, icon_size); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/icon-loader.h b/src/ui/icon-loader.h new file mode 100644 index 0000000..78975e2 --- /dev/null +++ b/src/ui/icon-loader.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Icon Loader + *//* + * Authors: + * see git history + * Jabiertxo Arraiza + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INK_ICON_LOADER_H +#define SEEN_INK_ICON_LOADER_H + +#include +#include + +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gint size); +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size); +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, Gtk::IconSize icon_size); +Gtk::Image *sp_get_icon_image(Glib::ustring icon_name, gchar const *prefs_sice); +GtkWidget *sp_get_icon_image(Glib::ustring icon_name, GtkIconSize icon_size); +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, gint size); +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::IconSize icon_size); +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, Gtk::BuiltinIconSize icon_size); +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, GtkIconSize icon_size); +Glib::RefPtr sp_get_icon_pixbuf(Glib::ustring icon_name, gchar const *prefs_sice); + +#endif // SEEN_INK_ICON_LOADER_H diff --git a/src/ui/icon-names.h b/src/ui/icon-names.h new file mode 100644 index 0000000..2ec0dce --- /dev/null +++ b/src/ui/icon-names.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Macro for icon names used in Inkscape + */ +/* Authors: + * Krzysztof KosiÅ„ski + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ICON_NAMES_H +#define SEEN_INKSCAPE_ICON_NAMES_H + +/** @brief Icon name annotation. + * Use this macro to mark strings which are used as icon names. + * This greatly simplifies tasks such as obtaining a full list of icons + * used by Inkscape. */ +#define INKSCAPE_ICON(icon) icon + +#endif /* ifdef SEEN_INKSCAPE_ICON_NAMES_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/interface.cpp b/src/ui/interface.cpp new file mode 100644 index 0000000..dba6051 --- /dev/null +++ b/src/ui/interface.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Main UI stuff. + */ +/* Authors: + * Lauris Kaplinski + * Frank Felfe + * bulia byak + * Jon A. Cruz + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2010 authors + * Copyright (C) 1999-2005 authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "document.h" +#include "enums.h" +#include "file.h" +#include "inkscape.h" +#include "inkscape-window.h" +#include "preferences.h" +#include "shortcuts.h" + +#include "extension/db.h" +#include "extension/effect.h" +#include "extension/find_extension_by_mime.h" +#include "extension/input.h" + +#include "helper/action.h" + +#include "io/sys.h" + +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog-events.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/dialog/layer-properties.h" +#include "ui/interface.h" + +#include "ui/view/svg-view-widget.h" + +#include "widgets/desktop-widget.h" + +static void sp_ui_import_one_file(char const *filename); +static void sp_ui_import_one_file_with_check(gpointer filename, gpointer unused); + +void +sp_ui_new_view() +{ + SPDocument *document; + + document = SP_ACTIVE_DOCUMENT; + if (!document) return; + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + InkscapeWindow* win = app->window_open (document); +} + +void +sp_ui_close_view(GtkWidget */*widget*/) +{ + SPDesktop *dt = SP_ACTIVE_DESKTOP; + + if (dt == nullptr) { + return; + } + + if (dt->shutdown()) { + return; // Shutdown operation has been canceled, so do nothing + } + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + InkscapeWindow* window = SP_ACTIVE_DESKTOP->getInkscapeWindow(); + + // If closing the last document, open a new document so Inkscape doesn't quit. + std::list desktops; + INKSCAPE.get_all_desktops(desktops); + if (desktops.size() == 1) { + + SPDocument* old_document = window->get_document(); + + Glib::ustring template_path = sp_file_default_template_uri(); + SPDocument *doc = app->document_new (template_path); + + app->document_swap (window, doc); + + if (app->document_window_count(old_document) == 0) { + app->document_close(old_document); + } + + // Are these necessary? + sp_namedview_window_from_document(dt); + sp_namedview_update_layers_from_document(dt); + + } else { + + app->destroy_window (window); + } +} + + +unsigned int +sp_ui_close_all() +{ + + ConcreteInkscapeApplication* app = &(ConcreteInkscapeApplication::get_instance()); + + app->destroy_all(); + + return true; +} + + +void +sp_ui_dialog_title_string(Inkscape::Verb *verb, gchar *c) +{ + SPAction *action; + unsigned int shortcut; + gchar *s; + gchar *atitle; + + action = verb->get_action(Inkscape::ActionContext()); + if (!action) + return; + + atitle = sp_action_get_title(action); + + s = g_stpcpy(c, atitle); + + g_free(atitle); + + shortcut = sp_shortcut_get_primary(verb); + if (shortcut!=GDK_KEY_VoidSymbol) { + gchar* key = sp_shortcut_get_label(shortcut); + s = g_stpcpy(s, " ("); + s = g_stpcpy(s, key); + g_stpcpy(s, ")"); + g_free(key); + } +} + + +Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view ) +{ + Glib::ustring prefPath; + + if (reinterpret_cast(view)->is_focusMode()) { + prefPath = "/focus/"; + } else if (reinterpret_cast(view)->is_fullscreen()) { + prefPath = "/fullscreen/"; + } else { + prefPath = "/window/"; + } + + return prefPath; +} + + +void +sp_ui_import_files(gchar *buffer) +{ + gchar** l = g_uri_list_extract_uris(buffer); + for (unsigned int i=0; i < g_strv_length(l); i++) { + gchar *f = g_filename_from_uri (l[i], nullptr, nullptr); + sp_ui_import_one_file_with_check(f, nullptr); + g_free(f); + } + g_strfreev(l); +} + +static void +sp_ui_import_one_file_with_check(gpointer filename, gpointer /*unused*/) +{ + if (filename) { + if (strlen((char const *)filename) > 2) + sp_ui_import_one_file((char const *)filename); + } +} + +static void +sp_ui_import_one_file(char const *filename) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + if (!doc) return; + + if (filename == nullptr) return; + + // Pass off to common implementation + // TODO might need to get the proper type of Inkscape::Extension::Extension + file_import( doc, filename, nullptr ); +} + +void +sp_ui_error_dialog(gchar const *message) +{ + GtkWidget *dlg; + gchar *safeMsg = Inkscape::IO::sanitizeString(message); + + dlg = gtk_message_dialog_new(nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, "%s", safeMsg); + sp_transientize(dlg); + gtk_window_set_resizable(GTK_WINDOW(dlg), FALSE); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); + g_free(safeMsg); +} + +bool +sp_ui_overwrite_file(gchar const *filename) +{ + bool return_value = FALSE; + + if (Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) { + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + gchar* baseName = g_path_get_basename( filename ); + gchar* dirName = g_path_get_dirname( filename ); + GtkWidget* dialog = gtk_message_dialog_new_with_markup( window->gobj(), + (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT), + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _( "A file named \"%s\" already exists. Do you want to replace it?\n\n" + "The file already exists in \"%s\". Replacing it will overwrite its contents." ), + baseName, + dirName + ); + gtk_dialog_add_buttons( GTK_DIALOG(dialog), + _("_Cancel"), GTK_RESPONSE_NO, + _("Replace"), GTK_RESPONSE_YES, + NULL ); + gtk_dialog_set_default_response( GTK_DIALOG(dialog), GTK_RESPONSE_YES ); + + if ( gtk_dialog_run( GTK_DIALOG(dialog) ) == GTK_RESPONSE_YES ) { + return_value = TRUE; + } else { + return_value = FALSE; + } + gtk_widget_destroy(dialog); + g_free( baseName ); + g_free( dirName ); + } else { + return_value = TRUE; + } + + return return_value; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/interface.h b/src/ui/interface.h new file mode 100644 index 0000000..e7e191a --- /dev/null +++ b/src/ui/interface.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_INTERFACE_H +#define SEEN_SP_INTERFACE_H + +/* + * Main UI stuff + * + * Authors: + * Lauris Kaplinski + * Frank Felfe + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +typedef struct _GtkWidget GtkWidget; + +namespace Inkscape { +class Verb; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI +} // namespace Inkscape + +/** + * \param widget unused + */ +void sp_ui_close_view (GtkWidget *widget); + +void sp_ui_new_view (); + +void sp_ui_import_files(gchar *buffer); + +/** + * This function is called to exit the program, and iterates through all + * open document view windows, attempting to close each in turn. If the + * view has unsaved information, the user will be prompted to save, + * discard, or cancel. + * + * Returns FALSE if the user cancels the close_all operation, TRUE + * otherwise. + */ +unsigned int sp_ui_close_all (); + +void sp_ui_dialog_title_string (Inkscape::Verb * verb, char* c); + +Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view ); + +/** + * + */ +void sp_ui_error_dialog (char const* message); +bool sp_ui_overwrite_file (char const* filename); + +#endif // SEEN_SP_INTERFACE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/monitor.cpp b/src/ui/monitor.cpp new file mode 100644 index 0000000..a9a3f83 --- /dev/null +++ b/src/ui/monitor.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief helper functions for retrieving monitor geometry, etc. + *//* + * Authors: + * see git history + * Patrick Storz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "include/gtkmm_version.h" + +namespace Inkscape { +namespace UI { + +/** get monitor geometry of primary monitor */ +Gdk::Rectangle get_monitor_geometry_primary() { + Gdk::Rectangle monitor_geometry; + auto const display = Gdk::Display::get_default(); + auto monitor = display->get_primary_monitor(); + + // Fallback to monitor number 0 if the user hasn't configured a primary monitor + if (!monitor) { + monitor = display->get_monitor(0); + } + + monitor->get_geometry(monitor_geometry); + return monitor_geometry; +} + +/** get monitor geometry of monitor containing largest part of window */ +Gdk::Rectangle get_monitor_geometry_at_window(const Glib::RefPtr& window) { + Gdk::Rectangle monitor_geometry; + auto const display = Gdk::Display::get_default(); + auto const monitor = display->get_monitor_at_window(window); + monitor->get_geometry(monitor_geometry); + return monitor_geometry; +} + +/** get monitor geometry of monitor at (or closest to) point on combined screen area */ +Gdk::Rectangle get_monitor_geometry_at_point(int x, int y) { + Gdk::Rectangle monitor_geometry; + auto const display = Gdk::Display::get_default(); + auto const monitor = display->get_monitor_at_point(x ,y); + monitor->get_geometry(monitor_geometry); + return monitor_geometry; +} + +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/ui/monitor.h b/src/ui/monitor.h new file mode 100644 index 0000000..78c105a --- /dev/null +++ b/src/ui/monitor.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief helper functions for retrieving monitor geometry, etc. + *//* + * Authors: + * see git history + * Patrick Storz + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_MONITOR_H +#define SEEN_MONITOR_H + +#include +#include + +namespace Inkscape { +namespace UI { + Gdk::Rectangle get_monitor_geometry_primary(); + Gdk::Rectangle get_monitor_geometry_at_window(const Glib::RefPtr& window); + Gdk::Rectangle get_monitor_geometry_at_point(int x, int y); +} +} + +#endif // SEEN_MONITOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/ui/pixmaps/README b/src/ui/pixmaps/README new file mode 100644 index 0000000..0b78941 --- /dev/null +++ b/src/ui/pixmaps/README @@ -0,0 +1,2 @@ + +This directory contains cursor xpm's and handles for resizing/skewing/rotating. diff --git a/src/ui/pixmaps/cursor-3dbox.xpm b/src/ui/pixmaps/cursor-3dbox.xpm new file mode 100644 index 0000000..c7fcb92 --- /dev/null +++ b/src/ui/pixmaps/cursor-3dbox.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_3dbox_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... . ", +" .+. ..+.. ", +" .+. ..++.++.. ", +" ... ..++.....++.. ", +" .++.........++. ", +" .+.............+. ", +" .++...........++. ", +" .+.++.......++.+. ", +" .+...+.....+...+. ", +" .+....++.++....+. ", +" .+......+......+. ", +" .+.....+.....+. ", +" .+.....+.....+. ", +" .+.....+.....+. ", +" .+.....+.....+. ", +" .++...+...++. ", +" ..+..+..+.. ", +" .+++++. ", +" ..+.. ", +" . ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-adj-a.xpm b/src/ui/pixmaps/cursor-adj-a.xpm new file mode 100644 index 0000000..7af3d9c --- /dev/null +++ b/src/ui/pixmaps/cursor-adj-a.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_adj_a_xpm[] = { +"32 32 3 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" .. ", +" .++. ", +" .++. ", +" .+..+. ", +" .+..+. ", +" .++++. ", +" .+ .+. ", +" .... .+..+. ", +" .++. . ...... ", +" .++. .+. ", +" ....++.... .+. ", +" .++++++++. .+. ", +" .++++++++. .+. ", +" ....++.... .+. ", +" .++. .+. ", +" .++. .+. ", +" .... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. .......... ", +" .+. .++++++++. ", +" .+. .++++++++. ", +" .+. .......... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" . ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-adj-h.xpm b/src/ui/pixmaps/cursor-adj-h.xpm new file mode 100644 index 0000000..5861934 --- /dev/null +++ b/src/ui/pixmaps/cursor-adj-h.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_adj_h_xpm[] = { +"32 32 3 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" ...... ", +" .+..+. ", +" .+..+. ", +" .+..+. ", +" .++++. ", +" .+..+. ", +" .+ .+. ", +" .... .+..+. ", +" .++. . ...... ", +" .++. .+. ", +" ....++.... .+. ", +" .++++++++. .+. ", +" .++++++++. .+. ", +" ....++.... .+. ", +" .++. .+. ", +" .++. .+. ", +" .... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. .......... ", +" .+. .++++++++. ", +" .+. .++++++++. ", +" .+. .......... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" . ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-adj-l.xpm b/src/ui/pixmaps/cursor-adj-l.xpm new file mode 100644 index 0000000..817ce44 --- /dev/null +++ b/src/ui/pixmaps/cursor-adj-l.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_adj_l_xpm[] = { +"32 32 3 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+.... ", +" .... .++++. ", +" .++. . ...... ", +" .++. .+. ", +" ....++.... .+. ", +" .++++++++. .+. ", +" .++++++++. .+. ", +" ....++.... .+. ", +" .++. .+. ", +" .++. .+. ", +" .... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. .......... ", +" .+. .++++++++. ", +" .+. .++++++++. ", +" .+. .......... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" . ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-adj-s.xpm b/src/ui/pixmaps/cursor-adj-s.xpm new file mode 100644 index 0000000..351a570 --- /dev/null +++ b/src/ui/pixmaps/cursor-adj-s.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_adj_s_xpm[] = { +"32 32 3 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" .. ", +" .++. ", +" .+..+. ", +" .+..+. ", +" .+... ", +" .+. ", +" .. .+. ", +" .... .+..+. ", +" .++. .. .++. ", +" .++. .+. .. ", +" ....++.... .+. ", +" .++++++++. .+. ", +" .++++++++. .+. ", +" ....++.... .+. ", +" .++. .+. ", +" .++. .+. ", +" .... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. .......... ", +" .+. .++++++++. ", +" .+. .++++++++. ", +" .+. .......... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" . ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-calligraphy.xpm b/src/ui/pixmaps/cursor-calligraphy.xpm new file mode 100644 index 0000000..193bd61 --- /dev/null +++ b/src/ui/pixmaps/cursor-calligraphy.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_calligraphy_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ... ", +" .+. ..+++. ", +" ... .++++++. . ", +" .++..++++. .+. ", +" .+. .++. .+. ", +" .++. .+. .+. ", +" .+. . .++. ", +" .+. .+. ", +" .++. .++. ", +" .+++.. ...++. ", +" .++++.++++. ", +" .++++++++. ", +" ..++++.. ", +" .... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-connector.xpm b/src/ui/pixmaps/cursor-connector.xpm new file mode 100644 index 0000000..86e8d17 --- /dev/null +++ b/src/ui/pixmaps/cursor-connector.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_connector_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ... ", +"....+.... .+. ", +" .+. .+. ", +" .+. .+. ", +" ... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ...........+. ", +" .+++++++++++. ", +" .+........... ", +" .+. ", +" .+. ", +" ...+... ", +" .+++++. ", +" .+++. ", +" .+. ", +" . ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-crosshairs.xpm b/src/ui/pixmaps/cursor-crosshairs.xpm new file mode 100644 index 0000000..407b760 --- /dev/null +++ b/src/ui/pixmaps/cursor-crosshairs.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_crosshairs_xpm[] = { +"32 32 3 1 7 7", +" c None", +". c #FFFFFF", +"+ c #000000", +" . ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ..... ..... ", +".+++++ +++++. ", +" ..... ..... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" . ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-dropper-f.xpm b/src/ui/pixmaps/cursor-dropper-f.xpm new file mode 100644 index 0000000..2804f66 --- /dev/null +++ b/src/ui/pixmaps/cursor-dropper-f.xpm @@ -0,0 +1,39 @@ +/* XPM */ +static const char * cursor_dropper_f_xpm[] = { +"32 32 4 1 5 5", +" c None", +"@ c Fill", +". c #FFFFFF", +"+ c #000000", +" ... ............", +" .+. .++++++++++.", +" .+. .+@@@@@@@@+.", +" .+. .+@@@@@@@@+.", +".... .... .+@@@@@@@@+.", +".+++ +++. .+@@@@@@@@+.", +".... .... .+@@@@@@@@+.", +" .+. .+@@@@@@@@+.", +" .+. .... .+@@@@@@@@+.", +" .+. .+++. .+@@@@@@@@+.", +" ... .+..+. .++++++++++.", +" .++..+. ............", +" .++..+. ", +" .++..+. ", +" .++..+. . ", +" .++..+.+. ", +" .++..+++. ", +" .++++.+. ", +" .++++.+.. ", +" .++++++.++. ", +" .++++++..+. ", +" ..+++++.+. ", +" .++++++. ", +" .+++++. ", +" .+++. ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-dropper-s.xpm b/src/ui/pixmaps/cursor-dropper-s.xpm new file mode 100644 index 0000000..c7cb9cc --- /dev/null +++ b/src/ui/pixmaps/cursor-dropper-s.xpm @@ -0,0 +1,39 @@ +/* XPM */ +static const char * cursor_dropper_s_xpm[] = { +"32 32 4 1 5 5", +" c None", +"@ s Fill", +". c #FFFFFF", +"+ c #000000", +" ... ...............", +" .+. .+++++++++++++.", +" .+. .+@@@@@@@@@@@+.", +" .+. .+@@@@@@@@@@@+.", +".... .... .+@@+++++++@@+.", +".+++ +++. .+@@+.....+@@+.", +".... .... .+@@+. .+@@+.", +" .+. .+@@+. .+@@+.", +" .+. .... .+@@+. .+@@+.", +" .+. .+++. .+@@+.....+@@+.", +" ... .+..+. .+@@+++++++@@+.", +" .++..+. .+@@@@@@@@@@@+.", +" .++..+. .+@@@@@@@@@@@+.", +" .++..+..+++++++++++++.", +" .++..+...............", +" .++..+.+. ", +" .++..+++. ", +" .++++.+. ", +" .++++.+.. ", +" .++++++.++. ", +" .++++++..+. ", +" ..+++++.+. ", +" .++++++. ", +" .+++++. ", +" .+++. ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-dropping-f.xpm b/src/ui/pixmaps/cursor-dropping-f.xpm new file mode 100644 index 0000000..8490ba7 --- /dev/null +++ b/src/ui/pixmaps/cursor-dropping-f.xpm @@ -0,0 +1,39 @@ +/* XPM */ +static const char * cursor_dropping_f_xpm[] = { +"32 32 4 1 5 5", +" c None", +". c #FFFFFF", +"+ c #000000", +"@ s Fill", +" ... ............", +" .+. .++++++++++.", +" .+. .+@@@@@@@@+.", +" .+. .+@@@@@@@@+.", +".... .... .+@@@@@@@@+.", +".+++ +++. .+@@@@@@@@+.", +".... .... .+@@@@@@@@+.", +" .+. ... .+@@@@@@@@+.", +" .+. .+++. .+@@@@@@@@+.", +" .+. .+..++. .+@@@@@@@@+.", +" ... .+.++++. .++++++++++.", +" ..+.+++++. ............", +" .++.++++++. ", +" .++.++++++. ", +" .++++++.. ", +" .+.++++. ", +" .+..++++. ", +" .+..++.+. ", +" .+..++. . ", +" .+++++. ", +" .+++++. ", +" .+++++. ", +" .++++. ", +" .+++. ", +" .... ", +" .+. ", +" .+++. ", +".++..+. ", +".+++.+. ", +".+++++. ", +" .+++. ", +" ... "}; diff --git a/src/ui/pixmaps/cursor-dropping-s.xpm b/src/ui/pixmaps/cursor-dropping-s.xpm new file mode 100644 index 0000000..3cc8965 --- /dev/null +++ b/src/ui/pixmaps/cursor-dropping-s.xpm @@ -0,0 +1,39 @@ +/* XPM */ +static const char * cursor_dropping_s_xpm[] = { +"32 32 4 1 5 5", +" c None", +". c #FFFFFF", +"+ c #000000", +"@ s Fill", +" ... ...............", +" .+. .+++++++++++++.", +" .+. .+@@@@@@@@@@@+.", +" .+. .+@@@@@@@@@@@+.", +".... .... .+@@+++++++@@+.", +".+++ +++. .+@@+.....+@@+.", +".... .... .+@@+. .+@@+.", +" .+. ... .+@@+. .+@@+.", +" .+. .+++..+@@+. .+@@+.", +" .+. .+..++.+@@+.....+@@+.", +" ... .+.+++.+@@+++++++@@+.", +" ..+.++++.+@@@@@@@@@@@+.", +" .++.+++++.+@@@@@@@@@@@+.", +" .++.++++++.+++++++++++++.", +" .++++++.................", +" .+.++++. ", +" .+..++++. ", +" .+..++.+. ", +" .+..++. . ", +" .+++++. ", +" .+++++. ", +" .+++++. ", +" .++++. ", +" .+++. ", +" .... ", +" .+. ", +" .+++. ", +".++..+. ", +".+++.+. ", +".+++++. ", +" .+++. ", +" ... "}; diff --git a/src/ui/pixmaps/cursor-ellipse.xpm b/src/ui/pixmaps/cursor-ellipse.xpm new file mode 100644 index 0000000..83a820f --- /dev/null +++ b/src/ui/pixmaps/cursor-ellipse.xpm @@ -0,0 +1,40 @@ +/* XPM */ +static char const *cursor_ellipse_xpm[] = { +"32 32 5 1 4 4", +" c None", +". c #FFFFFF", +"% c Stroke", +"* c Fill", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ....... ", +" ... ....%%%%%.... ", +" ..%%%*****%%%.. ", +" ..%***********%.. ", +" ..%*************%.. ", +" .%***************%. ", +" .%***************%. ", +" .%***************%. ", +" ..%*************%.. ", +" ..%***********%.. ", +" ..%%%*****%%%.. ", +" ....%%%%%.... ", +" ....... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-eraser.xpm b/src/ui/pixmaps/cursor-eraser.xpm new file mode 100644 index 0000000..b3f8f2d --- /dev/null +++ b/src/ui/pixmaps/cursor-eraser.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_eraser_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ... ", +" .+. .+++. ", +" .+. .+...+. ", +" ... .+.....+. ", +" .+.......+. ", +" .+.........+. ", +" .++..........+. ", +" .++...........+. ", +" .+.+...........+. ", +" .+..+..........++. ", +" .+...+.........++. ", +" .+...+.......+.+. ", +" .+...+.....+..+. ", +" .+...+...+...+. ", +" .+...+++...+. ", +" .+...+...+. ", +" .+..+..+. ", +" .+.+.+. ", +" .+++. ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-gradient-add.xpm b/src/ui/pixmaps/cursor-gradient-add.xpm new file mode 100644 index 0000000..ea8341b --- /dev/null +++ b/src/ui/pixmaps/cursor-gradient-add.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_gradient_add_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ..... ", +" ... .+++. ", +" .+.+. ", +" .+++. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+++. ", +" .+.+. ", +" .+++. ", +" ..... ", +" ... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ......+...... ", +" .+++++++++++. ", +" ......+...... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ... "}; diff --git a/src/ui/pixmaps/cursor-gradient.xpm b/src/ui/pixmaps/cursor-gradient.xpm new file mode 100644 index 0000000..92c9a9a --- /dev/null +++ b/src/ui/pixmaps/cursor-gradient.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_gradient_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ..... ", +" ... .+++. ", +" .+.+. ", +" .+++. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+++. ", +" .+.+. ", +" .+++. ", +" ..... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-measure.xpm b/src/ui/pixmaps/cursor-measure.xpm new file mode 100644 index 0000000..9f6497c --- /dev/null +++ b/src/ui/pixmaps/cursor-measure.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_measure_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. .. ", +" ... .++. ", +" .+..+. ", +" .+....+. ", +" .+..+...+. ", +" .+.+.....+. ", +" .+.......+. ", +" .+.+.....+. ", +" .+...+...+. ", +" .+.+.....+. ", +" .+.......+. ", +" .+.+.....+. ", +" .+...+...+. ", +" .+.+.....+. ", +" .+.......+. ", +" .+.+.....+. ", +" .+...+.+. ", +" .+.+.+. ", +" .+.+. ", +" .+. ", +" . ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-node-d.xpm b/src/ui/pixmaps/cursor-node-d.xpm new file mode 100644 index 0000000..e28bc73 --- /dev/null +++ b/src/ui/pixmaps/cursor-node-d.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_node_d_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" . ", +".+. ", +" .+. ", +" .++. ", +" .++. ", +" .+++. ", +" .+++. ", +" .++++. ", +" .++++. ", +" .+++++. ", +" .+++++. ", +" .++++++. ", +" .+++++. ", +" .+++.. ", +" .+. ++ ", +" . +++..+++ ", +" +..+..+..+ ", +" ++..+..+..++ ", +" +.+..+..+..+.+ ", +" +.+..+..+..+.+ ", +" +............+ ", +" +............+ ", +" +...........+ ", +" +..........+ ", +" +.........+ ", +" +........+ ", +" +.......+ ", +" +.......+ ", +" +++++++++ ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-node.xpm b/src/ui/pixmaps/cursor-node.xpm new file mode 100644 index 0000000..4d3bd94 --- /dev/null +++ b/src/ui/pixmaps/cursor-node.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_node_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #FFFFFF", +"+ c #000000", +" . ", +".+. ", +" .+. ", +" .++. ", +" .++. ", +" .+++. ", +" .+++. ", +" .++++. ", +" .++++. ", +" .+++++. ", +" .+++++. ", +" .++++++. ", +" .+++++. ", +" .+++.. ", +" .+. ", +" . ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-paintbucket.xpm b/src/ui/pixmaps/cursor-paintbucket.xpm new file mode 100644 index 0000000..0c6767f --- /dev/null +++ b/src/ui/pixmaps/cursor-paintbucket.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_paintbucket_xpm[] = { +"32 32 3 1 11 30", +" c None", +". c #000000", +"+ c #FFFFFF", +" ", +" ", +" ++ ++++ ", +" +..+ ++....+ ", +" +.++.+ ++..++++.+ ", +" +.++.++..+++++++.+ ", +" +.++...+++++++++.+ ", +" +.++...+++++++++++.+ ", +" +...++.++++++++++++.+ ", +" +...+++.++++++++++++.+ ", +" +.+..+++.+++++++++++.+ ", +" +.++..++.++++++++++++.+ ", +" +.+++.++.++++++++++++.+ ", +" +.+++.....+++++++++++.+ ", +" +.++++..+.+++++++++++.+ ", +" +.++++..++++++++++++.+ ", +" +.++++.++++++++++++..+ ", +" +..+++..+++++++++..++ ", +" +.++++.+++++++..++ ", +" +..+++.+++++..++ ", +" +.++..+++..++ ", +" +.....+..++ ", +" +......++ ", +" +....++ ", +" +...+ ", +" +...+ ", +" +..+ ", +" +.+ ", +" +.+ ", +" + ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-pen.xpm b/src/ui/pixmaps/cursor-pen.xpm new file mode 100644 index 0000000..79b68e9 --- /dev/null +++ b/src/ui/pixmaps/cursor-pen.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_pen_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. .... ", +" ... .+++... ", +" .+..+++.. ", +" .+.+...++.. ", +" .+.+....++. ", +" .+..+.....+. ", +" .+...+....+. ", +" .+...++...+. ", +" .+...++...+.. ", +" .+.......+.+. ", +" .+........+.+. ", +" .++.....++..+. ", +" ..+++.++....+. ", +" ...++.+.++. ", +" .++++.++. ", +" .+++++. ", +" .+++. ", +" .+. ", +" . ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-pencil.xpm b/src/ui/pixmaps/cursor-pencil.xpm new file mode 100644 index 0000000..92c6331 --- /dev/null +++ b/src/ui/pixmaps/cursor-pencil.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static const char * cursor_pencil_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ... ", +" ... .++... ", +" .+++++... ", +" .+++.+++.. ", +" .++.....++. ", +" .+........+. ", +" .+......+.+. ", +" .+....+++..+. ", +" .+...+...+..+. ", +" .+..+....+..+. ", +" .+.++.....+..+. ", +" .+..+.....+. +. ", +" .+..+.....+. ", +" .+..+... .+. ", +" .+..+. . . ", +" .+..+. . ", +" .+ .+. ", +" .+ . ", +" . ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-rect.xpm b/src/ui/pixmaps/cursor-rect.xpm new file mode 100644 index 0000000..e54b7ce --- /dev/null +++ b/src/ui/pixmaps/cursor-rect.xpm @@ -0,0 +1,40 @@ +/* XPM */ +static char const *cursor_rect_xpm[] = { +"32 32 5 1 4 4", +" c None", +". c #FFFFFF", +"% c Stroke", +"* c Fill", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ................. ", +" ... .%%%%%%%%%%%%%%%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%*************%. ", +" .%%%%%%%%%%%%%%%. ", +" ................. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-select-d.xpm b/src/ui/pixmaps/cursor-select-d.xpm new file mode 100644 index 0000000..e84e4de --- /dev/null +++ b/src/ui/pixmaps/cursor-select-d.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_select_d_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #000000", +"+ c #FFFFFF", +". ", +".. ", +".+. ", +".++. ", +".+++. ", +".++++. ", +".+++++. ", +".++++++. ", +".+++++++. ", +".++++++++. ", +".+++++++++. ", +".++++++..... ", +".++++++. ", +".++..++. ", +".+. .+++. ", +".. .++. ", +". .+++. . ", +" .+. .+. ", +" .. .+++. ", +" .+++++. ", +" . ..+.. . ", +" .+. .+. .+. ", +" .++...+...++. ", +" .+++++++++++++. ", +" .++...+...++. ", +" .+. .+. .+. ", +" . ..+.. . ", +" .+++++. ", +" .+++. ", +" .+. ", +" . ", +" "}; diff --git a/src/ui/pixmaps/cursor-select-m.xpm b/src/ui/pixmaps/cursor-select-m.xpm new file mode 100644 index 0000000..8dd8fb0 --- /dev/null +++ b/src/ui/pixmaps/cursor-select-m.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_select_m_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #000000", +"+ c #FFFFFF", +". ", +".. ", +".+. ", +".++. ", +".+++. ", +".++++. ", +".+++++. ", +".++++++. ", +".+++++++. ", +".++++++++. ", +".+++++++++. ", +".++++++..... ", +".++++++. ", +".++..++. ", +".+. .+++. ", +".. .++. ", +". .+++. + ", +" .+. +.+ ", +" .. +...+ ", +" +.....+ ", +" + ++.++ + ", +" +.+ +.+ +.+ ", +" +..+++.+++..+ ", +" +.............+ ", +" +..+++.+++..+ ", +" +.+ +.+ +.+ ", +" + ++.++ + ", +" +.....+ ", +" +...+ ", +" +.+ ", +" + ", +" "}; diff --git a/src/ui/pixmaps/cursor-select.xpm b/src/ui/pixmaps/cursor-select.xpm new file mode 100644 index 0000000..36b40c6 --- /dev/null +++ b/src/ui/pixmaps/cursor-select.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_select_xpm[] = { +"32 32 3 1 1 1", +" c None", +". c #000000", +"+ c #FFFFFF", +". ", +".. ", +".+. ", +".++. ", +".+++. ", +".++++. ", +".+++++. ", +".++++++. ", +".+++++++. ", +".++++++++. ", +".+++++++++. ", +".++++++..... ", +".++++++. ", +".++..++. ", +".+. .+++. ", +".. .++. ", +". .+++. ", +" .+. ", +" .. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-spiral.xpm b/src/ui/pixmaps/cursor-spiral.xpm new file mode 100644 index 0000000..1eaef1e --- /dev/null +++ b/src/ui/pixmaps/cursor-spiral.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_spiral_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ....... ", +" .+. ...+++++... ", +" ... ..++.....++.. ", +" .+.........+.. ", +" ..+...++++...+. ", +" .+...+....+..+.. ", +" .+..+......+..+. ", +" .+..+......+..+. ", +" .+..+...+..+..+. ", +" ..+..+++..+..+.. ", +" .+......+...+. ", +" ..++...+...+.. ", +" ...+++...+.. ", +" ......+.. ", +" .... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-spray-move.xpm b/src/ui/pixmaps/cursor-spray-move.xpm new file mode 100644 index 0000000..0c91839 --- /dev/null +++ b/src/ui/pixmaps/cursor-spray-move.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_spray_move_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ", +" ... ... ... ... ", +" ..+. ..+. ..+. ..+. ", +" .+++ .+++ .+++ .+++ ", +" ..+. .+++ .+++ ..+ ", +" .... .+++ .+++ .... ", +" ..+. ..+ ..+ ..+. ", +" .+++ ..+. ..+. .+++ ", +" ..+ .+++ .+++ ..+ ", +" .... ..+ ..+ .... ", +" ..+. .... .... ..+. ", +" .+++ ..+. ..+. .+++ ", +" ..+ .+++ .+++ ..+ ", +" ..+ ..+ ", +" ... ... ", +" ..+. ..+. ", +" .+++ .+++ ", +" ..+ ..+ ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-spray.xpm b/src/ui/pixmaps/cursor-spray.xpm new file mode 100644 index 0000000..19a4099 --- /dev/null +++ b/src/ui/pixmaps/cursor-spray.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_spray_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... .+. +. ", +" .+.+.+.+. ", +" .+...+...+. ", +" +...+.....+. ", +" . . . .+.+.......+. ", +" + + + .+.........+. ", +" . . . .+...........+. ", +" + + .+.............+. ", +" . . . .+.............+. ", +" + + + .+.............+. ", +" . . .+.............+. ", +" .+.............+. ", +" .+.............+. ", +" .+...........+. ", +" .+.........+. ", +" .+.......+. ", +" .+.....+. ", +" .+...+. ", +" .+.+. ", +" .+. ", +" . ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-star.xpm b/src/ui/pixmaps/cursor-star.xpm new file mode 100644 index 0000000..c9f2e85 --- /dev/null +++ b/src/ui/pixmaps/cursor-star.xpm @@ -0,0 +1,40 @@ +/* XPM */ +static char const *cursor_star_xpm[] = { +"32 32 5 1 4 4", +" c None", +". c #FFFFFF", +"% c Stroke", +"* c Fill", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... .. ", +" .+. .%%. ", +" .+. .%%. ", +" ... .%%. ", +" .%**%* ", +" ........%**%........ ", +" .%%%%%%****%%%%%%. ", +" .%%**********%%. ", +" ..%%******%%.. ", +" .%******%. ", +" .%******%. ", +" .%***%%***%. ", +" .%*%%..%%*%. ", +" .%*%.. ..%*%. ", +" .%%. .%%. ", +" .%. .%. ", +" .. .. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-text-insert.xpm b/src/ui/pixmaps/cursor-text-insert.xpm new file mode 100644 index 0000000..488791e --- /dev/null +++ b/src/ui/pixmaps/cursor-text-insert.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_text_insert_xpm[] = { +"32 32 3 1 7 10", +" c None", +". c #FFFFFF", +"+ c #000000", +" ....... ", +" .+++.+++. ", +" ...+... ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ...+... ", +" .+++.+++. ", +" ....... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-text.xpm b/src/ui/pixmaps/cursor-text.xpm new file mode 100644 index 0000000..6d74ae3 --- /dev/null +++ b/src/ui/pixmaps/cursor-text.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_text_xpm[] = { +"32 32 3 1 7 7", +" c None", +". c #FFFFFF", +"+ c #000000", +" . ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" ..... ..... ", +".+++++ +++++. ", +" ..... ..... ", +" .+. ", +" .+. ", +" .+. .... ", +" .+. .++++. ", +" .+. .++++. ", +" . .++++++. ", +" .++++++. ", +" .++++++. ", +" .+++..+++. ", +" .+++..+++. ", +" .++++++++. ", +" .++++++++++. ", +" .+++....+++. ", +" ... ... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-attract.xpm b/src/ui/pixmaps/cursor-tweak-attract.xpm new file mode 100644 index 0000000..264360b --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-attract.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_attract_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ", +" ..... .... ", +" .++++.. ..++++ ", +" .....++.....++.... ", +" .......+++++...... ", +" .++++.........++++ ", +" .....++.....++.... ", +" ..+++++.. ", +" ....... ", +" ....... ", +" ..+++++.. ", +" .....++.....++.... ", +" .++++.........++++ ", +" .......+++++...... ", +" .....++.....++.... ", +" .++++.. ..++++ ", +" ..... .... ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-color.xpm b/src/ui/pixmaps/cursor-tweak-color.xpm new file mode 100644 index 0000000..4bce642 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-color.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_color_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ......... ", +" ... ........... ", +" .......+....... ", +" ...+.+..+.+.... ", +" .......+.+..+.... ", +" ....+.++.+.+...+... ", +" ....+..+.+++++..... ", +" .....+++++.+..+.... ", +" ...+..+.++++++.+... ", +" ....++.+++.+.+..... ", +" .....++.+++++.++... ", +" ...+..++.++.+...... ", +" ....+..++.+.++.+... ", +" ......+..+.+....... ", +" ......+.+.+.+.... ", +" ...+........... ", +" .....+..+...... ", +" ........... ", +" ......... ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-less.xpm b/src/ui/pixmaps/cursor-tweak-less.xpm new file mode 100644 index 0000000..c669bca --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-less.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_less_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ", +" .. .. ", +" .+. .+. ", +" .+. .+. ", +" .+. .+. ", +" .+. ...... .+. ", +" .+..++++..+. ", +" .+++ +++. ", +" ..+ . . +.. ", +" .++.+..+.++. ", +" .+ .++. +. ", +" .+ .++. +. ", +" .++.+..+.++. ", +" ..+ . . +.. ", +" .+++ +++. ", +" .+..++++..+. ", +" .+. ...... .+. ", +" .+. .+. ", +" .+. .+. ", +" .+. .+. ", +" .. .. ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-more.xpm b/src/ui/pixmaps/cursor-tweak-more.xpm new file mode 100644 index 0000000..6321ce9 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-more.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_more_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ...... ", +" .+. ..++++.. ", +"....+.... .+++..+++. ", +".+++ +++. ..+.. ..+.. ", +"....+.... .++. .++. ", +" .+. .+. .+. ", +" .+. .+. .+. ", +" ... .++. .++. ", +" ..+.. ..+.. ", +" .+++..+++. ", +" ..++++.. ", +" ... .+ ...... .. ", +" .+. .+. .+. ", +" .+..+. +. .+. .+ ", +" .+.+. .+..+..+. ", +" .++.... .+.+.+. ", +" .+++++. .+++. ", +" ...... .+. ", +" ...... . ", +" ..++++.. ...... ", +" .+++..+++. ..++++.. ", +" ..+.. ..+.. .+++..+++. ", +" .++. .++. ..+.. ..+..", +" .+. .+. .++. .++.", +" .+. .+. .+. .+.", +" .++. .++. .+. .+.", +" ..+.. ..+.. .++. .++.", +" .+++..+++. ..+.. ..+..", +" ..++++.. .+++..+++. ", +" ...... ..++++.. ", +" ...... "}; diff --git a/src/ui/pixmaps/cursor-tweak-move-in.xpm b/src/ui/pixmaps/cursor-tweak-move-in.xpm new file mode 100644 index 0000000..d80fa03 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-move-in.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_move_in_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ........ ", +" .+++++++ ", +" .+++ ", +" .++++ ", +" .+.+++ ", +" .+..+++ ", +" .+...+++ ", +" .+. ..+++ ", +" .. ..+++ ", +" ..+++ ", +" ..+++ ", +" ..+++ ", +" ..++ ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-move-jitter.xpm b/src/ui/pixmaps/cursor-tweak-move-jitter.xpm new file mode 100644 index 0000000..7f0f811 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-move-jitter.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_move_jitter_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. + ", +" .+. +++ ", +"....+.... +.+.+ +++++++ ", +".+++ +++. +..+ .+ ....+++ ", +"....+.... . .+ . ++.+ ", +" .+. .+ ++..+ ", +" .+. .+ ++. .+ ", +" ... ++. .+ ", +" ++. .+ ", +" ++ +. .. ", +" .++ ", +" ..++ ", +" ..++ ", +" ..++ ", +" ..++ + ", +" ..++ + ", +" .. ..++ + ", +" .. .++ ..++ + ", +" +. ..++ ..+++ ", +" +. .++ ..++ .+ ", +" +..++ .+++++++. .+ ", +" +.++ ......... .+ ", +" +++ .+ ", +" ++..... + .+ + ", +" +++++++ + .+.+ +. ", +" +. .+++. ", +" +. .+. ", +" .+++++ . ", +" .+... ", +" .+ ", +" .+ "}; diff --git a/src/ui/pixmaps/cursor-tweak-move-out.xpm b/src/ui/pixmaps/cursor-tweak-move-out.xpm new file mode 100644 index 0000000..1fa0293 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-move-out.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_move_out_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ", +" ", +" ++ ", +" +++ ", +" .+++ ", +" ..+++ ", +" ..+++ ", +" ..+++ + ", +" ..+++ + ", +" ..+++ + ", +" ..+++ + ", +" ..++++ ", +" ..+++ ", +" .+++++++. ", +" ......... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-move.xpm b/src/ui/pixmaps/cursor-tweak-move.xpm new file mode 100644 index 0000000..67b1098 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-move.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_move_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... +++++++ ", +".+++ +++. ....+++ ", +"....+.... ++.+ ", +" .+. ++..+ ", +" .+. ++. .+ ", +" ... ++. .+ ", +" ++. .+ ", +" ++ +. .. ", +" .++ ", +" ..++ ", +" ..++ ", +" ..++ ", +" ..++ + ", +" ..++ + ", +" .. ..++ + ", +" .. .++ ..++ + ", +" +. ..++ ..+++ ", +" +. .++ ..++ ", +" +..++ .+++++++. ", +" +.++ ......... ", +" +++ ", +" ++..... ", +" +++++++ ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-push.xpm b/src/ui/pixmaps/cursor-tweak-push.xpm new file mode 100644 index 0000000..bf331e5 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-push.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_push_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ... ", +" ... ..... ", +" ..+++.. ", +" ..+...+.. ", +" .....++. .++.... ", +" .++++.. ..++++ ", +" ..... ... .... ", +" ..... ", +" ..+++.. ", +" ..+...+.. ", +" .....++. .++.... ", +" .++++.. ..++++ ", +" ..... .... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-repel.xpm b/src/ui/pixmaps/cursor-tweak-repel.xpm new file mode 100644 index 0000000..5dccefd --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-repel.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_repel_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ....... ", +" ..+++++.. ", +" .....++.....++..... ", +" .++++.........++++. ", +" .......+++++....... ", +" .....++.....++..... ", +" .++++.. ..++++. ", +" ..... ..... ", +" ", +" ..... ..... ", +" .++++.. ..++++. ", +" .....++.....++..... ", +" .......+++++....... ", +" .++++.........++++. ", +" .....++.....++..... ", +" ..+++++.. ", +" ....... ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm b/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm new file mode 100644 index 0000000..ecbbde8 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-rotate-clockwise.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_rotate_clockwise_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ... ", +" .+. .+. ", +" .+. .+. ", +" ... .+. ", +" .+. ", +" .+. ", +" .+. ", +" .+. ", +" .. .++. ", +" .+. .++.. ", +" .+. .++. ", +" .+. .++. ", +" .+......++. ", +" .+++++++++. ", +" .+........ ", +" .+. ", +" .+. ", +" .+. ", +" .. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm b/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm new file mode 100644 index 0000000..a3c2208 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_rotate_counterclockwise_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. . ", +"....+.... .+. ", +" .+. .+++. ", +" .+. .+.+.+. ", +" ... .+..+..+. ", +" .+. .+. .+. ", +" .. .+. .. ", +" .+. ", +" .+. ", +" .++. ", +" .++.. ", +" .++. ", +" .++. ", +" .........++. ", +" .+++++++++. ", +" ........... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-roughen.xpm b/src/ui/pixmaps/cursor-tweak-roughen.xpm new file mode 100644 index 0000000..1581b95 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-roughen.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_roughen_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ... ", +" ....+... ", +" . .+ +..+... ", +" ...+.+++.+..+.+... ", +" .++.+..+ . .+++++ ", +" ... . ..... ", +" ", +" ", +" .. ", +" ........++.. ..... ", +" .++++..+ +...++++ ", +" .....++.+..+++.... ", +" ......+.. ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-scale-down.xpm b/src/ui/pixmaps/cursor-tweak-scale-down.xpm new file mode 100644 index 0000000..b1b15af --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-scale-down.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_scale_down_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. .++++++++++++ ", +"....+.... .+. + ", +".+++ +++. .+. + ", +"....+.... .+. + ", +" .+. .+. + ", +" .+. .+. + ", +" ... .+. + ", +" .+. + ", +" .+. + ", +" .+. + ", +" .+..........+ ", +" .++++++++++++ ", +" ............. ", +" ", +" +. ", +" +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .++. ", +" .+++++ ", +" ..... ", +" .+++++ ", +" .+. + ", +" .+. + ", +" .+...+ ", +" .+++++ ", +" ...... ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-scale-up.xpm b/src/ui/pixmaps/cursor-tweak-scale-up.xpm new file mode 100644 index 0000000..dcc73c1 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-scale-up.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const * cursor_tweak_scale_up_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. .++++++++++++ ", +"....+.... .+. + ", +".+++ +++. .+. + ", +"....+.... .+. + ", +" .+. .+. + ", +" .+. .+. + ", +" ... .+. + ", +" .+. + ", +" .+. + ", +" .+. + ", +" .+..........+ ", +" .++++++++++++ ", +" ............. ", +" ", +" +++++ ", +" ++ ", +" +.+ ", +" +..+ ", +" +. .+ ", +" +. . ", +" +. ", +" ", +" .+++++ ", +" .+. + ", +" .+. + ", +" .+...+ ", +" .+++++ ", +" ...... ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-thicken.xpm b/src/ui/pixmaps/cursor-tweak-thicken.xpm new file mode 100644 index 0000000..ba7a2df --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-thicken.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_thicken_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ... ", +" ... ..... ", +" ..+++.. ", +" ..+...+.. ", +" .....++. .++.... ", +" .++++.. ..++++ ", +" ..... .... ", +" ", +" ", +" ", +" ..... .... ", +" .++++.. ..++++ ", +" .....++. .++.... ", +" ..+...+.. ", +" ..+++.. ", +" ..... ", +" ... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-tweak-thin.xpm b/src/ui/pixmaps/cursor-tweak-thin.xpm new file mode 100644 index 0000000..7d10fe7 --- /dev/null +++ b/src/ui/pixmaps/cursor-tweak-thin.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_thin_xpm[] = { +"32 32 3 1 4 4", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" .+. ", +" .+. ", +"....+.... ", +".+++ +++. ", +"....+.... ", +" .+. ", +" .+. ", +" ... ..... .... ", +" ...... ..... ", +" .++++.. ..++++ ", +" .....++.....++.... ", +" ..+++++.. ", +" ..... ", +" ", +" ", +" ..... ", +" ..+++++.. ", +" .....++.....++.... ", +" .++++.. ..++++ ", +" ...... ..... ", +" ..... .... ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-zoom-out.xpm b/src/ui/pixmaps/cursor-zoom-out.xpm new file mode 100644 index 0000000..8f35ad0 --- /dev/null +++ b/src/ui/pixmaps/cursor-zoom-out.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_zoom_out_xpm[] = { +"32 32 3 1 6 6", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" ..+++.. ", +" .++ ++. ", +" .+ +. ", +" .+ +. ", +".+ ..... +. ", +".+ +++++ +. ", +".+ ..... +. ", +" .+ +. ", +" .+ +. ", +" .++ ++.+.. ", +" ..+++..+.++. ", +" ... .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .++. ", +" .. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/cursor-zoom.xpm b/src/ui/pixmaps/cursor-zoom.xpm new file mode 100644 index 0000000..e869239 --- /dev/null +++ b/src/ui/pixmaps/cursor-zoom.xpm @@ -0,0 +1,38 @@ +/* XPM */ +static char const *cursor_zoom_xpm[] = { +"32 32 3 1 6 6", +" c None", +". c #FFFFFF", +"+ c #000000", +" ... ", +" ..+++.. ", +" .++ ++. ", +" .+ +. ", +" .+ .+. +. ", +".+ ..+.. +. ", +".+ +++++ +. ", +".+ ..+.. +. ", +" .+ .+. +. ", +" .+ +. ", +" .++ ++.+.. ", +" ..+++..+.++. ", +" ... .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .+ +. ", +" .++. ", +" .. ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" ", +" "}; diff --git a/src/ui/pixmaps/handles.xpm b/src/ui/pixmaps/handles.xpm new file mode 100644 index 0000000..9ef2b4b --- /dev/null +++ b/src/ui/pixmaps/handles.xpm @@ -0,0 +1,159 @@ +/* XPM */ +static char const *handle_scale_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" ", +" ....... ", +" .+++.. ", +" .++++. ", +" .+++++. ", +" ..+++++. . ", +" ...+++++... ", +" . .+++++.. ", +" .+++++. ", +" .++++. ", +" ..+++. ", +" ....... ", +" "}; + +/* XPM */ +static char const *handle_stretch_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" ", +" ", +" . . ", +" .. .. ", +" ..+...+.. ", +" ..+++++++.. ", +"..+++++++++..", +" ..+++++++.. ", +" ..+...+.. ", +" .. .. ", +" . . ", +" ", +" "}; + +/* XPM */ +static char const *handle_rotate_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" . ", +" .. ", +" .... ", +" ...++.. ", +" .++++++..", +" .+++++++. ", +" .++++..+. ", +" .+++. . ", +"...+++. ", +" ..++++. ", +" ..++. ", +" ... ", +" . "}; + +/* XPM */ +static char const *handle_skew_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" . . ", +" .. .. ", +" ......... ", +" ..+++++++.. ", +"..+++++++++..", +" ..+++++++.. ", +" ......... ", +" .. .. ", +" . . ", +" ", +" ", +" ", +" "}; + +/* XPM */ +static char const *handle_center_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" ", +" . ", +" . ", +" . ", +" ++.++ ", +" ++.++ ", +" ..... ..... ", +" ++.++ ", +" ++.++ ", +" . ", +" . ", +" . ", +" "}; + +/* XPM */ +static char const *handle_align_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" ", +" ", +" ", +" ........... ", +" .+++++++. ", +" .+++++. ", +" .+++. ", +" .+. ", +" . ", +".............", +".+++++++++++.", +".............", +" "}; + +/* XPM */ +static char const *handle_align_center_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +".............", +".+++++++++++.", +".+.........+.", +".+. + .+.", +".+. + .+.", +".+. + .+.", +".+.+++++++.+.", +".+. + .+.", +".+. + .+.", +".+. + .+.", +".+.........+.", +".+++++++++++.", +"............."}; + +/* XPM */ +static char const *handle_align_corner_xpm[] = { +"13 13 3 1", +" c None", +". c #000000", +"+ c #FFFFFF", +" . ", +" .. ...", +" .+. .+.", +" .++. .+.", +" .+++. .+.", +" .++++. .+.", +" .+++++. .+.", +"........ .+.", +" .+.", +" .+.", +" ..........+.", +" .++++++++++.", +" ............"}; diff --git a/src/ui/pref-pusher.cpp b/src/ui/pref-pusher.cpp new file mode 100644 index 0000000..3b84ba6 --- /dev/null +++ b/src/ui/pref-pusher.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "pref-pusher.h" + +#include + +namespace Inkscape { +namespace UI { +PrefPusher::PrefPusher( GtkToggleAction *act, Glib::ustring const &path, void (*callback)(gpointer), gpointer cbData ) : + Observer(path), + act(act), + callback(callback), + cbData(cbData), + freeze(false) +{ + g_signal_connect_after( G_OBJECT(act), "toggled", G_CALLBACK(toggleCB), this); + freeze = true; + gtk_toggle_action_set_active( act, Inkscape::Preferences::get()->getBool(observed_path) ); + freeze = false; + + Inkscape::Preferences::get()->addObserver(*this); +} + +PrefPusher::~PrefPusher() +{ + Inkscape::Preferences::get()->removeObserver(*this); +} + +void PrefPusher::toggleCB( GtkToggleAction * /*act*/, PrefPusher *self ) +{ + if (self) { + self->handleToggled(); + } +} + +void PrefPusher::handleToggled() +{ + if (!freeze) { + freeze = true; + Inkscape::Preferences::get()->setBool(observed_path, gtk_toggle_action_get_active( act )); + if (callback) { + (*callback)(cbData); + } + freeze = false; + } +} + +void PrefPusher::notify(Inkscape::Preferences::Entry const &newVal) +{ + bool newBool = newVal.getBool(); + bool oldBool = gtk_toggle_action_get_active(act); + + if (!freeze && (newBool != oldBool)) { + gtk_toggle_action_set_active( act, newBool ); + } +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/pref-pusher.h b/src/ui/pref-pusher.h new file mode 100644 index 0000000..0d62266 --- /dev/null +++ b/src/ui/pref-pusher.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PREF_PUSHER_H +#define SEEN_PREF_PUSHER_H + +#include "preferences.h" + +typedef struct _GtkToggleAction GtkToggleAction; + +namespace Inkscape { +namespace UI { + +/** + * A simple mediator class that keeps UI controls matched to the preference values they set. + */ +class PrefPusher : public Inkscape::Preferences::Observer +{ +public: + /** + * Constructor for a boolean value that syncs to the supplied path. + * Initializes the widget to the current preference stored state and registers callbacks + * for widget changes and preference changes. + * + * @param act the widget to synchronize preference with. + * @param path the path to the preference the widget is synchronized with. + * @param callback function to invoke when changes are pushed. + * @param cbData data to be passed on to the callback function. + */ + PrefPusher( GtkToggleAction *act, + Glib::ustring const & path, + void (*callback)(gpointer) = nullptr, + gpointer cbData = nullptr ); + + /** + * Destructor that unregisters the preference callback. + */ + ~PrefPusher() override; + + /** + * Callback method invoked when the preference setting changes. + */ + void notify(Inkscape::Preferences::Entry const &new_val) override; + + +private: + /** + * Callback hook invoked when the widget changes. + * + * @param act the toggle action widget that was changed. + * @param self the PrefPusher instance the callback was registered to. + */ + static void toggleCB( GtkToggleAction *act, PrefPusher *self ); + + /** + * Method to handle the widget change. + * + * @details Sets the observed path, based on the state of the toggle button + * and then runs the callback function + */ + void handleToggled(); + + GtkToggleAction *act; + void (*callback)(gpointer); + gpointer cbData; + bool freeze; +}; + +} +} +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/previewable.h b/src/ui/previewable.h new file mode 100644 index 0000000..c25f2db --- /dev/null +++ b/src/ui/previewable.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef SEEN_PREVIEWABLE_H +#define SEEN_PREVIEWABLE_H +/* + * A simple interface for previewing representations. + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include + +#include "widget/preview.h" + +namespace Inkscape { +namespace UI { + +enum PreviewStyle { + PREVIEW_STYLE_ICON = 0, + PREVIEW_STYLE_PREVIEW, + PREVIEW_STYLE_NAME, + PREVIEW_STYLE_BLURB, + PREVIEW_STYLE_ICON_NAME, + PREVIEW_STYLE_ICON_BLURB, + PREVIEW_STYLE_PREVIEW_NAME, + PREVIEW_STYLE_PREVIEW_BLURB +}; + + +class Previewable +{ +public: +// TODO need to add some nice parameters + virtual ~Previewable() = default; + virtual Gtk::Widget* getPreview(UI::Widget::PreviewStyle style, + UI::Widget::ViewType view, + UI::Widget::PreviewSize size, + guint ratio, + guint border) = 0; +}; + + +} //namespace UI +} //namespace Inkscape + + +#endif // SEEN_PREVIEWABLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/previewholder.cpp b/src/ui/previewholder.cpp new file mode 100644 index 0000000..2d6b8f8 --- /dev/null +++ b/src/ui/previewholder.cpp @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple interface for previewing representations. + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "previewable.h" +#include "previewholder.h" + +#include +#include +#include +#include +#include + +#define COLUMNS_FOR_SMALL 16 +#define COLUMNS_FOR_LARGE 8 +//#define COLUMNS_FOR_SMALL 48 +//#define COLUMNS_FOR_LARGE 32 + + +namespace Inkscape { +namespace UI { + + +PreviewHolder::PreviewHolder() : + Bin(), + _scroller(nullptr), + _insides(nullptr), + _prefCols(0), + _updatesFrozen(false), + _anchor(SP_ANCHOR_CENTER), + _baseSize(UI::Widget::PREVIEW_SIZE_SMALL), + _ratio(100), + _view(UI::Widget::VIEW_TYPE_LIST), + _wrap(false), + _border(UI::Widget::BORDER_NONE) +{ + set_name( "PreviewHolder" ); + _scroller = Gtk::manage(new Gtk::ScrolledWindow()); + _scroller->set_name( "PreviewHolderScroller" ); + _scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + + _insides = Gtk::manage(new Gtk::Grid()); + _insides->set_name( "PreviewHolderGrid" ); + _insides->set_column_spacing(8); + + _scroller->set_hexpand(); + _scroller->set_vexpand(); + _scroller->add( *_insides ); + + // Disable overlay scrolling as the scrollbar covers up swatches. + // For some reason this also makes the height 55px. + _scroller->set_overlay_scrolling(false); + + add(*_scroller); +} + +PreviewHolder::~PreviewHolder() += default; + +/** + * Translates vertical scrolling into horizontal + */ +bool PreviewHolder::on_scroll_event(GdkEventScroll *event) +{ + if (_wrap) { + return FALSE; + } + + // Scroll horizontally by page on mouse wheel + auto adj = _scroller->get_hadjustment(); + + if (!adj) { + return FALSE; + } + + double move; + switch (event->direction) { + case GDK_SCROLL_UP: + case GDK_SCROLL_LEFT: + move = -adj->get_page_size(); + break; + case GDK_SCROLL_DOWN: + case GDK_SCROLL_RIGHT: + move = adj->get_page_size(); + break; + case GDK_SCROLL_SMOOTH: + if (fabs(event->delta_y) <= fabs(event->delta_x)) { + return FALSE; + } +#ifdef GDK_WINDOWING_QUARTZ + move = event->delta_y; +#else + move = event->delta_y * adj->get_page_size(); +#endif + break; + default: + return FALSE; + } + + double value = adj->get_value() + move; + + adj->set_value(value); + + return TRUE; +} + +void PreviewHolder::clear() +{ + items.clear(); + _prefCols = 0; + // Kludge to restore scrollbars + if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) { + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER ); + } + rebuildUI(); +} + +/** + * Add a Previewable item to the PreviewHolder + * + * \param[in] preview The Previewable item to add + */ +void PreviewHolder::addPreview( Previewable* preview ) +{ + items.push_back(preview); + if ( !_updatesFrozen ) + { + int i = items.size() - 1; + + switch(_view) { + case UI::Widget::VIEW_TYPE_LIST: + { + Gtk::Widget* label = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_BLURB, + UI::Widget::VIEW_TYPE_LIST, + _baseSize, _ratio, _border)); + Gtk::Widget* item = Gtk::manage(preview->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, + UI::Widget::VIEW_TYPE_LIST, + _baseSize, _ratio, _border)); + + item->set_hexpand(); + item->set_vexpand(); + _insides->attach(*item, 0, i, 1, 1); + + label->set_hexpand(); + label->set_valign(Gtk::ALIGN_CENTER); + _insides->attach(*label, 1, i, 1, 1); + } + + break; + case UI::Widget::VIEW_TYPE_GRID: + { + Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, + UI::Widget::VIEW_TYPE_GRID, + _baseSize, _ratio, _border)); + + int ncols = 1; + int nrows = 1; + int col = 0; + int row = 0; + + // To get size + auto kids = _insides->get_children(); + int childCount = (int)kids.size(); + if (childCount > 0 ) { + + // Need already shown widget + calcGridSize( kids[0], items.size()+1, ncols, nrows ); + + // Column and row for the new widget + col = i % ncols; + row = i / ncols; + + } + + // Loop through the existing widgets and move them to new location + for ( int j = 1; j < childCount; j++ ) { + auto target = kids[childCount - (j + 1)]; + int col2 = j % ncols; + int row2 = j / ncols; + _insides->remove( *target ); + + target->set_hexpand(); + target->set_vexpand(); + _insides->attach( *target, col2, row2, 1, 1); + } + item->set_hexpand(); + item->set_vexpand(); + _insides->attach(*item, col, row, 1, 1); + } + } + + _scroller->show_all_children(); + } +} + +void PreviewHolder::freezeUpdates() +{ + _updatesFrozen = true; +} + +void PreviewHolder::thawUpdates() +{ + _updatesFrozen = false; + rebuildUI(); +} + +void +PreviewHolder::setStyle(UI::Widget::PreviewSize size, + UI::Widget::ViewType view, + guint ratio, + UI::Widget::BorderStyle border ) +{ + if ( size != _baseSize || view != _view || ratio != _ratio || border != _border ) { + _baseSize = size; + _view = view; + _ratio = ratio; + _border = border; + // Kludge to restore scrollbars + if ( !_wrap && (_view != UI::Widget::VIEW_TYPE_LIST) && (_anchor == SP_ANCHOR_NORTH || _anchor == SP_ANCHOR_SOUTH) ) { + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_NEVER ); + } + rebuildUI(); + } +} + +void PreviewHolder::setOrientation(SPAnchorType anchor) +{ + if ( _anchor != anchor ) + { + _anchor = anchor; + switch ( _anchor ) + { + case SP_ANCHOR_NORTH: + case SP_ANCHOR_SOUTH: + { + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER ); + } + break; + + case SP_ANCHOR_EAST: + case SP_ANCHOR_WEST: + { + _scroller->set_policy( Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC ); + } + break; + + default: + { + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + } + } + rebuildUI(); + } +} + +void PreviewHolder::setWrap( bool wrap ) +{ + if (_wrap != wrap) { + _wrap = wrap; + switch ( _anchor ) + { + case SP_ANCHOR_NORTH: + case SP_ANCHOR_SOUTH: + { + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, _wrap ? Gtk::POLICY_AUTOMATIC : Gtk::POLICY_NEVER ); + } + break; + default: + { + (void)0; + // do nothing; + } + } + rebuildUI(); + } +} + +void PreviewHolder::setColumnPref( int cols ) +{ + _prefCols = cols; +} + + +/** + * Calculate the grid side of a preview holder + * + * \param[in] item A sample preview widget. + * \param[in] itemCount The number of items to pack into the grid. + * \param[out] ncols The number of columns in grid. + * \param[out] nrows The number of rows in grid. + */ +void PreviewHolder::calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows ) +{ + // Initially set all items in a horizontal row + ncols = itemCount; + nrows = 1; + + if ( _anchor == SP_ANCHOR_SOUTH || _anchor == SP_ANCHOR_NORTH ) { + Gtk::Requisition req; + Gtk::Requisition req_natural; + _scroller->get_preferred_size(req, req_natural); + int currW = _scroller->get_width(); + if ( currW > req.width ) { + req.width = currW; + } + + auto hs = _scroller->get_hscrollbar(); + + if (_wrap && item != nullptr) { + + // Get width of bar. + int width_scroller = _scroller->get_width(); + + // Get width of one item (must be visible). + int minimum_width_item = 0; + int natural_width_item = 0; + item->get_preferred_width(minimum_width_item, natural_width_item); + + // Calculate columns and rows. + if (natural_width_item < 1) { + natural_width_item = 1; + } + ncols = width_scroller / natural_width_item - 1; + + // On first run, scroller width is not set correct... so we need to fudge it: + if (ncols < 2) { + ncols = itemCount/2; + nrows = 2; + } else { + nrows = itemCount / ncols; + } + } + } else { + ncols = (_baseSize == UI::Widget::PREVIEW_SIZE_SMALL || _baseSize == UI::Widget::PREVIEW_SIZE_TINY) ? + COLUMNS_FOR_SMALL : COLUMNS_FOR_LARGE; + if ( _prefCols > 0 ) { + ncols = _prefCols; + } + nrows = (itemCount + (ncols - 1)) / ncols; + if ( nrows < 1 ) { + nrows = 1; + } + } +} + +void PreviewHolder::rebuildUI() +{ + auto children = _insides->get_children(); + for (auto child : children) { + _insides->remove(*child); + delete child; + } + + _insides->set_column_spacing(0); + _insides->set_row_spacing(0); + if (_border == UI::Widget::BORDER_WIDE) { + _insides->set_column_spacing(1); + _insides->set_row_spacing(1); + } + + switch (_view) { + case UI::Widget::VIEW_TYPE_LIST: + { + _insides->set_column_spacing(8); + + for ( unsigned int i = 0; i < items.size(); i++ ) { + Gtk::Widget* label = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_BLURB, _view, _baseSize, _ratio, _border)); + //label->set_alignment(Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER); + + Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, _border)); + + item->set_hexpand(); + item->set_vexpand(); + _insides->attach(*item, 0, i, 1, 1); + + label->set_hexpand(); + label->set_valign(Gtk::ALIGN_CENTER); + _insides->attach(*label, 1, i, 1, 1); + } + } + break; + + case UI::Widget::VIEW_TYPE_GRID: + { + int col = 0; + int row = 0; + int ncols = 2; + int nrows = 1; + + for ( unsigned int i = 0; i < items.size(); i++ ) { + + // If this is the last row, flag so the previews can draw a bottom + UI::Widget::BorderStyle border = ((row == nrows -1) && (_border == UI::Widget::BORDER_SOLID)) ? + UI::Widget::BORDER_SOLID_LAST_ROW : _border; + + Gtk::Widget* item = Gtk::manage(items[i]->getPreview(UI::Widget::PREVIEW_STYLE_PREVIEW, _view, _baseSize, _ratio, border)); + item->set_hexpand(); + item->set_vexpand(); + + if (i == 0) { + // We need one item shown before we can call calcGridSize()... + _insides->attach( *item, 0, 0, 1, 1); + _scroller->show_all_children(); + calcGridSize( item, items.size(), ncols, nrows ); + } else { + // We've already calculated grid size. + _insides->attach( *item, col, row, 1, 1); + } + + if ( ++col >= ncols ) { + col = 0; + row++; + } + } + } + } + + _scroller->show_all_children(); +} + + + + + +} //namespace UI +} //namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/previewholder.h b/src/ui/previewholder.h new file mode 100644 index 0000000..aa1c39a --- /dev/null +++ b/src/ui/previewholder.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef SEEN_PREVIEW_HOLDER_H +#define SEEN_PREVIEW_HOLDER_H +/* + * A simple interface for previewing representations. + * Used by Swatches + * + * Authors: + * Jon A. Cruz + * + * Copyright (C) 2005 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +namespace Gtk { +class Grid; +class ScrolledWindow; +} + +#include "enums.h" + +namespace Inkscape { +namespace UI { + +class Previewable; + +class PreviewHolder : public Gtk::Bin +{ +public: + PreviewHolder(); + ~PreviewHolder() override; + + virtual void clear(); + virtual void addPreview( Previewable* preview ); + virtual void freezeUpdates(); + virtual void thawUpdates(); + virtual void setStyle(UI::Widget::PreviewSize size, + UI::Widget::ViewType view, + guint ratio, + UI::Widget::BorderStyle border); + virtual void setOrientation(SPAnchorType how); + virtual int getColumnPref() const { return _prefCols; } + virtual void setColumnPref( int cols ); + virtual UI::Widget::PreviewSize getPreviewSize() const { return _baseSize; } + virtual UI::Widget::ViewType getPreviewType() const { return _view; } + virtual guint getPreviewRatio() const { return _ratio; } + virtual UI::Widget::BorderStyle getPreviewBorder() const { return _border; } + virtual void setWrap( bool wrap ); + virtual bool getWrap() const { return _wrap; } + +protected: + bool on_scroll_event(GdkEventScroll*) override; + +private: + void rebuildUI(); + void calcGridSize( const Gtk::Widget* item, int itemCount, int& ncols, int& nrows ); + + std::vector items; + Gtk::ScrolledWindow *_scroller; + Gtk::Grid *_insides; + + int _prefCols; + bool _updatesFrozen; + SPAnchorType _anchor; + UI::Widget::PreviewSize _baseSize; + guint _ratio; + UI::Widget::ViewType _view; + bool _wrap; + UI::Widget::BorderStyle _border; +}; + +} //namespace UI +} //namespace Inkscape + +#endif // SEEN_PREVIEW_HOLDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/selected-color.cpp b/src/ui/selected-color.cpp new file mode 100644 index 0000000..d8bbab1 --- /dev/null +++ b/src/ui/selected-color.cpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Color selected in color selector widget. + * This file was created during the refactoring of SPColorSelector + *//* + * Authors: + * bulia byak + * Jon A. Cruz + * Tomasz Boczkowski + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include "svg/svg-icc-color.h" +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { + +double const SelectedColor::_EPSILON = 1e-4; + +SelectedColor::SelectedColor() + : _color(0) + , _alpha(1.0) + , _held(false) + , _virgin(true) + , _updating(false) +{ + +} + +SelectedColor::~SelectedColor() = default; + +void SelectedColor::setColor(SPColor const &color) +{ + setColorAlpha( color, _alpha); +} + +SPColor SelectedColor::color() const +{ + return _color; +} + +void SelectedColor::setAlpha(gfloat alpha) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + setColorAlpha( _color, alpha); +} + +gfloat SelectedColor::alpha() const +{ + return _alpha; +} + +void SelectedColor::setValue(guint32 value) +{ + SPColor color(value); + gfloat alpha = SP_RGBA32_A_F(value); + setColorAlpha(color, alpha); +} + +guint32 SelectedColor::value() const +{ + return color().toRGBA32(_alpha); +} + +void SelectedColor::setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal) +{ +#ifdef DUMP_CHANGE_INFO + g_message("SelectedColor::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s)", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():""), alpha, (emit_signal?"YES":"no")); +#endif + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + + if (_updating) { + return; + } + +#ifdef DUMP_CHANGE_INFO + g_message("---- SelectedColor::setColorAlpha virgin:%s !close:%s alpha is:%s", + (_virgin?"YES":"no"), + (!color.isClose( _color, _EPSILON )?"YES":"no"), + ((fabs((_alpha) - (alpha)) >= _EPSILON )?"YES":"no") + ); +#endif + + if ( _virgin || !color.isClose( _color, _EPSILON ) || + (fabs((_alpha) - (alpha)) >= _EPSILON )) { + + _virgin = false; + + _color = color; + _alpha = alpha; + + if (emit_signal) + { + _updating = true; + if (_held) { + signal_dragged.emit(); + } else { + signal_changed.emit(); + } + _updating = false; + } + +#ifdef DUMP_CHANGE_INFO + } else { + g_message("++++ SelectedColor::setColorAlpha color:%08x ==> _color:%08X isClose:%s", color.toRGBA32(alpha), _color.toRGBA32(_alpha), + (color.isClose( _color, _EPSILON )?"YES":"no")); +#endif + } +} + +void SelectedColor::colorAlpha(SPColor &color, gfloat &alpha) const { + color = _color; + alpha = _alpha; +} + +void SelectedColor::setHeld(bool held) { + if (_updating) { + return; + } + bool grabbed = held && !_held; + bool released = !held && _held; + + _held = held; + + _updating = true; + if (grabbed) { + signal_grabbed.emit(); + } + + if (released) { + signal_released.emit(); + // signal_changed.emit(); // TODO: signal_changed isn't emitted after dragging! + } + _updating = false; +} + +void SelectedColor::preserveICC() { + _color.icc = _color.icc ? new SVGICCColor(*_color.icc) : nullptr; +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/selected-color.h b/src/ui/selected-color.h new file mode 100644 index 0000000..1a00fc5 --- /dev/null +++ b/src/ui/selected-color.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Color selected in color selector widget. + * This file was created during the refactoring of SPColorSelector + *//* + * Authors: + * bulia byak + * Jon A. Cruz + * Tomasz Boczkowski + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SELECTED_COLOR +#define SEEN_SELECTED_COLOR + +#include +#include + +#include "color.h" + +namespace Gtk +{ + class Widget; +} + +namespace Inkscape { +namespace UI { + +class SelectedColor { +public: + SelectedColor(); + virtual ~SelectedColor(); + + // By default, disallow copy constructor and assignment operator + SelectedColor(SelectedColor const &obj) = delete; + SelectedColor& operator=(SelectedColor const &obj) = delete; + + void setColor(SPColor const &color); + SPColor color() const; + + void setAlpha(gfloat alpha); + gfloat alpha() const; + + void setValue(guint32 value); + guint32 value() const; + + void setColorAlpha(SPColor const &color, gfloat alpha, bool emit_signal = true); + void colorAlpha(SPColor &color, gfloat &alpha) const; + + void setHeld(bool held); + + void preserveICC(); + + sigc::signal signal_grabbed; + sigc::signal signal_dragged; + sigc::signal signal_released; + sigc::signal signal_changed; + +private: + SPColor _color; + /** + * Color alpha value guaranteed to be in [0, 1]. + */ + gfloat _alpha; + + bool _held; + /** + * This flag is true if no color is set yet + */ + bool _virgin; + + bool _updating; + + static double const _EPSILON; +}; + +class ColorSelectorFactory { +public: + virtual ~ColorSelectorFactory() = default; + + virtual Gtk::Widget* createWidget(SelectedColor &color) const = 0; + virtual Glib::ustring modeName() const = 0; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/shape-editor-knotholders.cpp b/src/ui/shape-editor-knotholders.cpp new file mode 100644 index 0000000..ef395fa --- /dev/null +++ b/src/ui/shape-editor-knotholders.cpp @@ -0,0 +1,1980 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Node editing extension to objects + * + * Authors: + * Lauris Kaplinski + * Mitsuru Oka + * Maximilian Albert + * Abhishek Sharma + * Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "preferences.h" +#include "desktop.h" +#include "knotholder.h" +#include "knot-holder-entity.h" +#include "style.h" + +#include "live_effects/effect.h" + +#include "object/box3d.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowtext.h" +#include "object/sp-item.h" +#include "object/sp-namedview.h" +#include "object/sp-offset.h" +#include "object/sp-pattern.h" +#include "object/sp-rect.h" +#include "object/sp-spiral.h" +#include "object/sp-star.h" +#include "object/sp-text.h" +#include "object/sp-textpath.h" +#include "object/sp-tspan.h" + +class RectKnotHolder : public KnotHolder { +public: + RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~RectKnotHolder() override = default;; +}; + +class Box3DKnotHolder : public KnotHolder { +public: + Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~Box3DKnotHolder() override = default;; +}; + +class ArcKnotHolder : public KnotHolder { +public: + ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~ArcKnotHolder() override = default;; +}; + +class StarKnotHolder : public KnotHolder { +public: + StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~StarKnotHolder() override = default;; +}; + +class SpiralKnotHolder : public KnotHolder { +public: + SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~SpiralKnotHolder() override = default;; +}; + +class OffsetKnotHolder : public KnotHolder { +public: + OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~OffsetKnotHolder() override = default;; +}; + +class TextKnotHolder : public KnotHolder { +public: + TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~TextKnotHolder() override = default;; +}; + +class FlowtextKnotHolder : public KnotHolder { +public: + FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~FlowtextKnotHolder() override = default;; +}; + +class MiscKnotHolder : public KnotHolder { +public: + MiscKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler); + ~MiscKnotHolder() override = default;; +}; + +namespace { + +static KnotHolder *sp_lpe_knot_holder(SPLPEItem *item, SPDesktop *desktop) +{ + KnotHolder *knot_holder = new KnotHolder(desktop, item, nullptr); + + Inkscape::LivePathEffect::Effect *effect = item->getCurrentLPE(); + effect->addHandles(knot_holder, item); + + return knot_holder; +} + +} // namespace + +namespace Inkscape { +namespace UI { + +KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop) +{ + KnotHolder *knotholder = nullptr; + + if (dynamic_cast(item)) { + knotholder = new RectKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + knotholder = new Box3DKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + knotholder = new ArcKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + knotholder = new StarKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + knotholder = new SpiralKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + knotholder = new OffsetKnotHolder(desktop, item, nullptr); + } else if (dynamic_cast(item)) { + SPText *text = dynamic_cast(item); + + // Do not allow conversion to 'inline-size' wrapped text if on path! + // might not be first child if or <desc> is present. + bool is_on_path = false; + for (auto child : text->childList(false)) { + if (dynamic_cast<SPTextPath *>(child)) is_on_path = true; + } + if (!is_on_path) { + knotholder = new TextKnotHolder(desktop, item, nullptr); + } + } else { + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item); + if (flowtext && flowtext->has_internal_frame()) { + knotholder = new FlowtextKnotHolder(desktop, flowtext->get_frame(nullptr), nullptr); + } else if ((item->style->fill.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getFillPaintServer())) || + (item->style->stroke.isPaintserver() && dynamic_cast<SPPattern *>(item->style->getStrokePaintServer()))) { + knotholder = new KnotHolder(desktop, item, nullptr); + knotholder->add_pattern_knotholder(); + } + } + if (!knotholder) knotholder = new KnotHolder(desktop, item, nullptr); + knotholder->add_filter_knotholder(); + + return knotholder; +} + +KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop) +{ + KnotHolder *knotholder = nullptr; + + SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item); + if (lpe && + lpe->getCurrentLPE() && + lpe->getCurrentLPE()->isVisible() && + lpe->getCurrentLPE()->providesKnotholder()) { + knotholder = sp_lpe_knot_holder(lpe, desktop); + } + return knotholder; +} + +} +} // namespace Inkscape + +/* SPRect */ + +/* handle for horizontal rounding radius */ +class RectKnotHolderEntityRX : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +/* handle for vertical rounding radius */ +class RectKnotHolderEntityRY : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +/* handle for width/height adjustment */ +class RectKnotHolderEntityWH : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + +protected: + void set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state); +}; + +/* handle for x/y adjustment */ +class RectKnotHolderEntityXY : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +/* handle for position */ +class RectKnotHolderEntityCenter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +Geom::Point +RectKnotHolderEntityRX::knot_get() const +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed + rect->width.computed - rect->rx.computed, rect->y.computed); +} + +void +RectKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + //In general we cannot just snap this radius to an arbitrary point, as we have only a single + //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap + //the radius then we should have a constrained snap. snap_knot_position() is unconstrained + Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(-1, 0)), state); + + if (state & GDK_CONTROL_MASK) { + gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; + rect->rx = rect->ry = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, temp); + } else { + rect->rx = CLAMP(rect->x.computed + rect->width.computed - s[Geom::X], 0.0, rect->width.computed / 2.0); + } + + update_knot(); + + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +RectKnotHolderEntityRX::knot_click(unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + if (state & GDK_SHIFT_MASK) { + /* remove rounding from rectangle */ + rect->getRepr()->removeAttribute("rx"); + rect->getRepr()->removeAttribute("ry"); + } else if (state & GDK_CONTROL_MASK) { + /* Ctrl-click sets the vertical rounding to be the same as the horizontal */ + rect->getRepr()->setAttribute("ry", rect->getRepr()->attribute("rx")); + } + +} + +Geom::Point +RectKnotHolderEntityRY::knot_get() const +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->ry.computed); +} + +void +RectKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + //In general we cannot just snap this radius to an arbitrary point, as we have only a single + //degree of freedom. For snapping to an arbitrary point we need two DOF. If we're going to snap + //the radius then we should have a constrained snap. snap_knot_position() is unconstrained + Geom::Point const s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed), Geom::Point(0, 1)), state); + + if (state & GDK_CONTROL_MASK) { // When holding control then rx will be kept equal to ry, + // resulting in a perfect circle (and not an ellipse) + gdouble temp = MIN(rect->height.computed, rect->width.computed) / 2.0; + rect->rx = rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, 0.0, temp); + } else { + if (!rect->rx._set || rect->rx.computed == 0) { + rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, + 0.0, + MIN(rect->height.computed / 2.0, rect->width.computed / 2.0)); + } else { + rect->ry = CLAMP(s[Geom::Y] - rect->y.computed, + 0.0, + rect->height.computed / 2.0); + } + } + + update_knot(); + + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +RectKnotHolderEntityRY::knot_click(unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + if (state & GDK_SHIFT_MASK) { + /* remove rounding */ + rect->getRepr()->removeAttribute("rx"); + rect->getRepr()->removeAttribute("ry"); + } else if (state & GDK_CONTROL_MASK) { + /* Ctrl-click sets the vertical rounding to be the same as the horizontal */ + rect->getRepr()->setAttribute("rx", rect->getRepr()->attribute("ry")); + } +} + +#define SGN(x) ((x)>0?1:((x)<0?-1:0)) + +static void sp_rect_clamp_radii(SPRect *rect) +{ + // clamp rounding radii so that they do not exceed width/height + if (2 * rect->rx.computed > rect->width.computed) { + rect->rx = 0.5 * rect->width.computed; + } + if (2 * rect->ry.computed > rect->height.computed) { + rect->ry = 0.5 * rect->height.computed; + } +} + +Geom::Point +RectKnotHolderEntityWH::knot_get() const +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed); +} + +void +RectKnotHolderEntityWH::set_internal(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + Geom::Point s = p; + + if (state & GDK_CONTROL_MASK) { + // original width/height when drag started + gdouble const w_orig = (origin[Geom::X] - rect->x.computed); + gdouble const h_orig = (origin[Geom::Y] - rect->y.computed); + + //original ratio + gdouble ratio = (w_orig / h_orig); + + // mouse displacement since drag started + gdouble minx = p[Geom::X] - origin[Geom::X]; + gdouble miny = p[Geom::Y] - origin[Geom::Y]; + + Geom::Point p_handle(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed); + + if (fabs(minx) > fabs(miny)) { + // snap to horizontal or diagonal + if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) { + // closer to the diagonal and in same-sign quarters, change both using ratio + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state); + minx = s[Geom::X] - origin[Geom::X]; + // Dead assignment: Value stored to 'miny' is never read + //miny = s[Geom::Y] - origin[Geom::Y]; + rect->height = MAX(h_orig + minx / ratio, 0); + } else { + // closer to the horizontal, change only width, height is h_orig + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state); + minx = s[Geom::X] - origin[Geom::X]; + // Dead assignment: Value stored to 'miny' is never read + //miny = s[Geom::Y] - origin[Geom::Y]; + rect->height = MAX(h_orig, 0); + } + rect->width = MAX(w_orig + minx, 0); + + } else { + // snap to vertical or diagonal + if (miny != 0 && fabs(minx/miny) > 0.5 * ratio && (SGN(minx) == SGN(miny))) { + // closer to the diagonal and in same-sign quarters, change both using ratio + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state); + // Dead assignment: Value stored to 'minx' is never read + //minx = s[Geom::X] - origin[Geom::X]; + miny = s[Geom::Y] - origin[Geom::Y]; + rect->width = MAX(w_orig + miny * ratio, 0); + } else { + // closer to the vertical, change only height, width is w_orig + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state); + // Dead assignment: Value stored to 'minx' is never read + //minx = s[Geom::X] - origin[Geom::X]; + miny = s[Geom::Y] - origin[Geom::Y]; + rect->width = MAX(w_orig, 0); + } + rect->height = MAX(h_orig + miny, 0); + + } + + } else { + // move freely + s = snap_knot_position(p, state); + rect->width = MAX(s[Geom::X] - rect->x.computed, 0); + rect->height = MAX(s[Geom::Y] - rect->y.computed, 0); + } + + sp_rect_clamp_radii(rect); + + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +RectKnotHolderEntityWH::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + set_internal(p, origin, state); + update_knot(); +} + +Geom::Point +RectKnotHolderEntityXY::knot_get() const +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed, rect->y.computed); +} + +void +RectKnotHolderEntityXY::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + // opposite corner (unmoved) + gdouble opposite_x = (rect->x.computed + rect->width.computed); + gdouble opposite_y = (rect->y.computed + rect->height.computed); + + // original width/height when drag started + gdouble w_orig = opposite_x - origin[Geom::X]; + gdouble h_orig = opposite_y - origin[Geom::Y]; + + Geom::Point s = p; + Geom::Point p_handle(rect->x.computed, rect->y.computed); + + // mouse displacement since drag started + gdouble minx = p[Geom::X] - origin[Geom::X]; + gdouble miny = p[Geom::Y] - origin[Geom::Y]; + + if (state & GDK_CONTROL_MASK) { + //original ratio + gdouble ratio = (w_orig / h_orig); + + if (fabs(minx) > fabs(miny)) { + // snap to horizontal or diagonal + if (minx != 0 && fabs(miny/minx) > 0.5 * 1/ratio && (SGN(minx) == SGN(miny))) { + // closer to the diagonal and in same-sign quarters, change both using ratio + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state); + minx = s[Geom::X] - origin[Geom::X]; + // Dead assignment: Value stored to 'miny' is never read + //miny = s[Geom::Y] - origin[Geom::Y]; + rect->y = MIN(origin[Geom::Y] + minx / ratio, opposite_y); + rect->height = MAX(h_orig - minx / ratio, 0); + } else { + // closer to the horizontal, change only width, height is h_orig + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-1, 0)), state); + minx = s[Geom::X] - origin[Geom::X]; + // Dead assignment: Value stored to 'miny' is never read + //miny = s[Geom::Y] - origin[Geom::Y]; + rect->y = MIN(origin[Geom::Y], opposite_y); + rect->height = MAX(h_orig, 0); + } + rect->x = MIN(s[Geom::X], opposite_x); + rect->width = MAX(w_orig - minx, 0); + } else { + // snap to vertical or diagonal + if (miny != 0 && fabs(minx/miny) > 0.5 *ratio && (SGN(minx) == SGN(miny))) { + // closer to the diagonal and in same-sign quarters, change both using ratio + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(-ratio, -1)), state); + // Dead assignment: Value stored to 'minx' is never read + //minx = s[Geom::X] - origin[Geom::X]; + miny = s[Geom::Y] - origin[Geom::Y]; + rect->x = MIN(origin[Geom::X] + miny * ratio, opposite_x); + rect->width = MAX(w_orig - miny * ratio, 0); + } else { + // closer to the vertical, change only height, width is w_orig + s = snap_knot_position_constrained(p, Inkscape::Snapper::SnapConstraint(p_handle, Geom::Point(0, -1)), state); + // Dead assignment: Value stored to 'minx' is never read + //minx = s[Geom::X] - origin[Geom::X]; + miny = s[Geom::Y] - origin[Geom::Y]; + rect->x = MIN(origin[Geom::X], opposite_x); + rect->width = MAX(w_orig, 0); + } + rect->y = MIN(s[Geom::Y], opposite_y); + rect->height = MAX(h_orig - miny, 0); + } + + } else { + // move freely + s = snap_knot_position(p, state); + minx = s[Geom::X] - origin[Geom::X]; + miny = s[Geom::Y] - origin[Geom::Y]; + + rect->x = MIN(s[Geom::X], opposite_x); + rect->y = MIN(s[Geom::Y], opposite_y); + rect->width = MAX(w_orig - minx, 0); + rect->height = MAX(h_orig - miny, 0); + } + + sp_rect_clamp_radii(rect); + + update_knot(); + + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +RectKnotHolderEntityCenter::knot_get() const +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed + (rect->width.computed / 2.), rect->y.computed + (rect->height.computed / 2.)); +} + +void +RectKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPRect *rect = dynamic_cast<SPRect *>(item); + g_assert(rect != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + rect->x = s[Geom::X] - (rect->width.computed / 2.); + rect->y = s[Geom::Y] - (rect->height.computed / 2.); + + // No need to call sp_rect_clamp_radii(): width and height haven't changed. + // No need to call update_knot(): the knot is set directly by the user. + + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +RectKnotHolder::RectKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + RectKnotHolderEntityRX *entity_rx = new RectKnotHolderEntityRX(); + RectKnotHolderEntityRY *entity_ry = new RectKnotHolderEntityRY(); + RectKnotHolderEntityWH *entity_wh = new RectKnotHolderEntityWH(); + RectKnotHolderEntityXY *entity_xy = new RectKnotHolderEntityXY(); + RectKnotHolderEntityCenter *entity_center = new RectKnotHolderEntityCenter(); + + entity_rx->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Adjust the <b>horizontal rounding</b> radius; with <b>Ctrl</b> " + "to make the vertical radius the same"), + SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR); + + entity_ry->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Adjust the <b>vertical rounding</b> radius; with <b>Ctrl</b> " + "to make the horizontal radius the same"), + SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR); + + entity_wh->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> " + "to lock ratio or stretch in one dimension only"), + SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR); + + entity_xy->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Adjust the <b>width and height</b> of the rectangle; with <b>Ctrl</b> " + "to lock ratio or stretch in one dimension only"), + SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR); + + entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Drag to move the rectangle"), + SP_KNOT_SHAPE_CROSS); + + entity.push_back(entity_rx); + entity.push_back(entity_ry); + entity.push_back(entity_wh); + entity.push_back(entity_xy); + entity.push_back(entity_center); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + +/* Box3D (= the new 3D box structure) */ + +class Box3DKnotHolderEntity : public KnotHolderEntity { +public: + Geom::Point knot_get() const override = 0; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override = 0; + + Geom::Point knot_get_generic(SPItem *item, unsigned int knot_id) const; + void knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &p, unsigned int state); +}; + +Geom::Point +Box3DKnotHolderEntity::knot_get_generic(SPItem *item, unsigned int knot_id) const +{ + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + return box3d_get_corner_screen(box, knot_id); + } else { + return Geom::Point(); // TODO investigate proper fallback + } +} + +void +Box3DKnotHolderEntity::knot_set_generic(SPItem *item, unsigned int knot_id, Geom::Point const &new_pos, unsigned int state) +{ + Geom::Point const s = snap_knot_position(new_pos, state); + + g_assert(item != nullptr); + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + g_assert(box != nullptr); + Geom::Affine const i2dt (item->i2dt_affine ()); + + Box3D::Axis movement; + if ((knot_id < 4) != (state & GDK_SHIFT_MASK)) { + movement = Box3D::XY; + } else { + movement = Box3D::Z; + } + + box3d_set_corner (box, knot_id, s * i2dt, movement, (state & GDK_CONTROL_MASK)); + box3d_set_z_orders(box); + box3d_position_set(box); +} + +class Box3DKnotHolderEntity0 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity1 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity2 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity3 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity4 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity5 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity6 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntity7 : public Box3DKnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class Box3DKnotHolderEntityCenter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +Geom::Point +Box3DKnotHolderEntity0::knot_get() const +{ + return knot_get_generic(item, 0); +} + +Geom::Point +Box3DKnotHolderEntity1::knot_get() const +{ + return knot_get_generic(item, 1); +} + +Geom::Point +Box3DKnotHolderEntity2::knot_get() const +{ + return knot_get_generic(item, 2); +} + +Geom::Point +Box3DKnotHolderEntity3::knot_get() const +{ + return knot_get_generic(item, 3); +} + +Geom::Point +Box3DKnotHolderEntity4::knot_get() const +{ + return knot_get_generic(item, 4); +} + +Geom::Point +Box3DKnotHolderEntity5::knot_get() const +{ + return knot_get_generic(item, 5); +} + +Geom::Point +Box3DKnotHolderEntity6::knot_get() const +{ + return knot_get_generic(item, 6); +} + +Geom::Point +Box3DKnotHolderEntity7::knot_get() const +{ + return knot_get_generic(item, 7); +} + +Geom::Point +Box3DKnotHolderEntityCenter::knot_get() const +{ + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + return box3d_get_center_screen(box); + } else { + return Geom::Point(); // TODO investigate proper fallback + } +} + +void +Box3DKnotHolderEntity0::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 0, new_pos, state); +} + +void +Box3DKnotHolderEntity1::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 1, new_pos, state); +} + +void +Box3DKnotHolderEntity2::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 2, new_pos, state); +} + +void +Box3DKnotHolderEntity3::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 3, new_pos, state); +} + +void +Box3DKnotHolderEntity4::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 4, new_pos, state); +} + +void +Box3DKnotHolderEntity5::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 5, new_pos, state); +} + +void +Box3DKnotHolderEntity6::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 6, new_pos, state); +} + +void +Box3DKnotHolderEntity7::knot_set(Geom::Point const &new_pos, Geom::Point const &/*origin*/, unsigned int state) +{ + knot_set_generic(item, 7, new_pos, state); +} + +void +Box3DKnotHolderEntityCenter::knot_set(Geom::Point const &new_pos, Geom::Point const &origin, unsigned int state) +{ + Geom::Point const s = snap_knot_position(new_pos, state); + + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + g_assert(box != nullptr); + Geom::Affine const i2dt (item->i2dt_affine ()); + + box3d_set_center(box, s * i2dt, origin * i2dt, !(state & GDK_SHIFT_MASK) ? Box3D::XY : Box3D::Z, + state & GDK_CONTROL_MASK); + + box3d_set_z_orders(box); + box3d_position_set(box); +} + +Box3DKnotHolder::Box3DKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + Box3DKnotHolderEntity0 *entity_corner0 = new Box3DKnotHolderEntity0(); + Box3DKnotHolderEntity1 *entity_corner1 = new Box3DKnotHolderEntity1(); + Box3DKnotHolderEntity2 *entity_corner2 = new Box3DKnotHolderEntity2(); + Box3DKnotHolderEntity3 *entity_corner3 = new Box3DKnotHolderEntity3(); + Box3DKnotHolderEntity4 *entity_corner4 = new Box3DKnotHolderEntity4(); + Box3DKnotHolderEntity5 *entity_corner5 = new Box3DKnotHolderEntity5(); + Box3DKnotHolderEntity6 *entity_corner6 = new Box3DKnotHolderEntity6(); + Box3DKnotHolderEntity7 *entity_corner7 = new Box3DKnotHolderEntity7(); + Box3DKnotHolderEntityCenter *entity_center = new Box3DKnotHolderEntityCenter(); + + entity_corner0->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner1->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner2->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner3->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box in X/Y direction; with <b>Shift</b> along the Z axis; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner4->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner5->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner6->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_corner7->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Resize box along the Z axis; with <b>Shift</b> in X/Y direction; " + "with <b>Ctrl</b> to constrain to the directions of edges or diagonals")); + + entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Move the box in perspective"), + SP_KNOT_SHAPE_CROSS); + + entity.push_back(entity_corner0); + entity.push_back(entity_corner1); + entity.push_back(entity_corner2); + entity.push_back(entity_corner3); + entity.push_back(entity_corner4); + entity.push_back(entity_corner5); + entity.push_back(entity_corner6); + entity.push_back(entity_corner7); + entity.push_back(entity_center); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + +/* SPArc */ + +class ArcKnotHolderEntityStart : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class ArcKnotHolderEntityEnd : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class ArcKnotHolderEntityRX : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class ArcKnotHolderEntityRY : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class ArcKnotHolderEntityCenter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +/* + * return values: + * 1 : inside + * 0 : on the curves + * -1 : outside + */ +static gint +sp_genericellipse_side(SPGenericEllipse *ellipse, Geom::Point const &p) +{ + gdouble dx = (p[Geom::X] - ellipse->cx.computed) / ellipse->rx.computed; + gdouble dy = (p[Geom::Y] - ellipse->cy.computed) / ellipse->ry.computed; + + gdouble s = dx * dx + dy * dy; + // We add a bit of a buffer, so there's a decent chance the user will + // be able to adjust the arc without the closed status flipping between + // open and closed during micro mouse movements. + if (s < 0.75) return 1; + if (s > 1.25) return -1; + return 0; +} + +void +ArcKnotHolderEntityStart::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12); + + SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item); + g_assert(arc != nullptr); + + gint side = sp_genericellipse_side(arc, p); + if(side != 0) { arc->setArcType( (side == -1) ? + SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE : + SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); } + + Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed); + Geom::Scale sc(arc->rx.computed, arc->ry.computed); + + double offset = arc->start - atan2(delta * sc.inverse()); + arc->start -= offset; + + if ((state & GDK_CONTROL_MASK) && snaps) { + double snaps_radian = M_PI/snaps; + arc->start = std::round(arc->start/snaps_radian) * snaps_radian; + } + if (state & GDK_SHIFT_MASK) { + arc->end -= offset; + } + + arc->normalize(); + arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +ArcKnotHolderEntityStart::knot_get() const +{ + SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item); + g_assert(ge != nullptr); + + return ge->getPointAtAngle(ge->start); +} + +void +ArcKnotHolderEntityStart::knot_click(unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + if (state & GDK_SHIFT_MASK) { + ge->end = ge->start = 0; + ge->updateRepr(); + } +} + +void +ArcKnotHolderEntityEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + int snaps = Inkscape::Preferences::get()->getInt("/options/rotationsnapsperpi/value", 12); + + SPGenericEllipse *arc = dynamic_cast<SPGenericEllipse *>(item); + g_assert(arc != nullptr); + + gint side = sp_genericellipse_side(arc, p); + if(side != 0) { arc->setArcType( (side == -1) ? + SP_GENERIC_ELLIPSE_ARC_TYPE_SLICE : + SP_GENERIC_ELLIPSE_ARC_TYPE_ARC); } + + Geom::Point delta = p - Geom::Point(arc->cx.computed, arc->cy.computed); + Geom::Scale sc(arc->rx.computed, arc->ry.computed); + + double offset = arc->end - atan2(delta * sc.inverse()); + arc->end -= offset; + + if ((state & GDK_CONTROL_MASK) && snaps) { + double snaps_radian = M_PI/snaps; + arc->end = std::round(arc->end/snaps_radian) * snaps_radian; + } + if (state & GDK_SHIFT_MASK) { + arc->start -= offset; + } + + arc->normalize(); + arc->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +ArcKnotHolderEntityEnd::knot_get() const +{ + SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item); + g_assert(ge != nullptr); + + return ge->getPointAtAngle(ge->end); +} + + +void +ArcKnotHolderEntityEnd::knot_click(unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + if (state & GDK_SHIFT_MASK) { + ge->end = ge->start = 0; + ge->updateRepr(); + } +} + + +void +ArcKnotHolderEntityRX::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + ge->rx = fabs( ge->cx.computed - s[Geom::X] ); + + if ( state & GDK_CONTROL_MASK ) { + ge->ry = ge->rx.computed; + } + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +ArcKnotHolderEntityRX::knot_get() const +{ + SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse const *>(item); + g_assert(ge != nullptr); + + return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(ge->rx.computed, 0)); +} + +void +ArcKnotHolderEntityRX::knot_click(unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + if (state & GDK_CONTROL_MASK) { + ge->ry = ge->rx.computed; + ge->updateRepr(); + } +} + +void +ArcKnotHolderEntityRY::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + ge->ry = fabs( ge->cy.computed - s[Geom::Y] ); + + if ( state & GDK_CONTROL_MASK ) { + ge->rx = ge->ry.computed; + } + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +ArcKnotHolderEntityRY::knot_get() const +{ + SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + return (Geom::Point(ge->cx.computed, ge->cy.computed) - Geom::Point(0, ge->ry.computed)); +} + +void +ArcKnotHolderEntityRY::knot_click(unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + if (state & GDK_CONTROL_MASK) { + ge->rx = ge->ry.computed; + ge->updateRepr(); + } +} + +void +ArcKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPGenericEllipse *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + ge->cx = s[Geom::X]; + ge->cy = s[Geom::Y]; + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +ArcKnotHolderEntityCenter::knot_get() const +{ + SPGenericEllipse const *ge = dynamic_cast<SPGenericEllipse *>(item); + g_assert(ge != nullptr); + + return Geom::Point(ge->cx.computed, ge->cy.computed); +} + + +ArcKnotHolder::ArcKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + ArcKnotHolderEntityRX *entity_rx = new ArcKnotHolderEntityRX(); + ArcKnotHolderEntityRY *entity_ry = new ArcKnotHolderEntityRY(); + ArcKnotHolderEntityStart *entity_start = new ArcKnotHolderEntityStart(); + ArcKnotHolderEntityEnd *entity_end = new ArcKnotHolderEntityEnd(); + ArcKnotHolderEntityCenter *entity_center = new ArcKnotHolderEntityCenter(); + + entity_rx->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Adjust ellipse <b>width</b>, with <b>Ctrl</b> to make circle"), + SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR); + + entity_ry->create(desktop, item, this, Inkscape::CTRL_TYPE_SIZER, + _("Adjust ellipse <b>height</b>, with <b>Ctrl</b> to make circle"), + SP_KNOT_SHAPE_SQUARE, SP_KNOT_MODE_XOR); + + entity_start->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Position the <b>start point</b> of the arc or segment; with <b>Shift</b> to move " + "with <b>end point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the " + "ellipse for arc, <b>outside</b> for segment"), + SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR); + + entity_end->create(desktop, item, this, Inkscape::CTRL_TYPE_ROTATE, + _("Position the <b>end point</b> of the arc or segment; with <b>Shift</b> to move " + "with <b>start point</b>; with <b>Ctrl</b> to snap angle; drag <b>inside</b> the " + "ellipse for arc, <b>outside</b> for segment"), + SP_KNOT_SHAPE_CIRCLE, SP_KNOT_MODE_XOR); + + entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Drag to move the ellipse"), + SP_KNOT_SHAPE_CROSS); + + entity.push_back(entity_rx); + entity.push_back(entity_ry); + entity.push_back(entity_start); + entity.push_back(entity_end); + entity.push_back(entity_center); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + +/* SPStar */ + +class StarKnotHolderEntity1 : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class StarKnotHolderEntity2 : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class StarKnotHolderEntityCenter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +void +StarKnotHolderEntity1::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPStar *star = dynamic_cast<SPStar *>(item); + g_assert(star != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + Geom::Point d = s - star->center; + + double arg1 = atan2(d); + double darg1 = arg1 - star->arg[0]; + + if (state & GDK_MOD1_MASK) { + star->randomized = darg1/(star->arg[0] - star->arg[1]); + } else if (state & GDK_SHIFT_MASK) { + star->rounded = darg1/(star->arg[0] - star->arg[1]); + } else if (state & GDK_CONTROL_MASK) { + star->r[0] = L2(d); + } else { + star->r[0] = L2(d); + star->arg[0] = arg1; + star->arg[1] += darg1; + } + star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +StarKnotHolderEntity2::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPStar *star = dynamic_cast<SPStar *>(item); + g_assert(star != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + if (star->flatsided == false) { + Geom::Point d = s - star->center; + + double arg1 = atan2(d); + double darg1 = arg1 - star->arg[1]; + + if (state & GDK_MOD1_MASK) { + star->randomized = darg1/(star->arg[0] - star->arg[1]); + } else if (state & GDK_SHIFT_MASK) { + star->rounded = fabs(darg1/(star->arg[0] - star->arg[1])); + } else if (state & GDK_CONTROL_MASK) { + star->r[1] = L2(d); + star->arg[1] = star->arg[0] + M_PI / star->sides; + } + else { + star->r[1] = L2(d); + star->arg[1] = atan2(d); + } + star->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +void +StarKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPStar *star = dynamic_cast<SPStar *>(item); + g_assert(star != nullptr); + + star->center = snap_knot_position(p, state); + + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +StarKnotHolderEntity1::knot_get() const +{ + g_assert(item != nullptr); + + SPStar const *star = dynamic_cast<SPStar const *>(item); + g_assert(star != nullptr); + + return sp_star_get_xy(star, SP_STAR_POINT_KNOT1, 0); + +} + +Geom::Point +StarKnotHolderEntity2::knot_get() const +{ + g_assert(item != nullptr); + + SPStar const *star = dynamic_cast<SPStar const *>(item); + g_assert(star != nullptr); + + return sp_star_get_xy(star, SP_STAR_POINT_KNOT2, 0); +} + +Geom::Point +StarKnotHolderEntityCenter::knot_get() const +{ + g_assert(item != nullptr); + + SPStar const *star = dynamic_cast<SPStar const *>(item); + g_assert(star != nullptr); + + return star->center; +} + +static void +sp_star_knot_click(SPItem *item, unsigned int state) +{ + SPStar *star = dynamic_cast<SPStar *>(item); + g_assert(star != nullptr); + + if (state & GDK_MOD1_MASK) { + star->randomized = 0; + star->updateRepr(); + } else if (state & GDK_SHIFT_MASK) { + star->rounded = 0; + star->updateRepr(); + } else if (state & GDK_CONTROL_MASK) { + star->arg[1] = star->arg[0] + M_PI / star->sides; + star->updateRepr(); + } +} + +void +StarKnotHolderEntity1::knot_click(unsigned int state) +{ + sp_star_knot_click(item, state); +} + +void +StarKnotHolderEntity2::knot_click(unsigned int state) +{ + sp_star_knot_click(item, state); +} + +StarKnotHolder::StarKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + SPStar *star = dynamic_cast<SPStar *>(item); + g_assert(item != nullptr); + + StarKnotHolderEntity1 *entity1 = new StarKnotHolderEntity1(); + entity1->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Adjust the <b>tip radius</b> of the star or polygon; " + "with <b>Shift</b> to round; with <b>Alt</b> to randomize")); + + entity.push_back(entity1); + + if (star->flatsided == false) { + StarKnotHolderEntity2 *entity2 = new StarKnotHolderEntity2(); + entity2->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Adjust the <b>base radius</b> of the star; with <b>Ctrl</b> to keep star rays " + "radial (no skew); with <b>Shift</b> to round; with <b>Alt</b> to randomize")); + entity.push_back(entity2); + } + + StarKnotHolderEntityCenter *entity_center = new StarKnotHolderEntityCenter(); + entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Drag to move the star"), + SP_KNOT_SHAPE_CROSS); + entity.push_back(entity_center); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + +/* SPSpiral */ + +class SpiralKnotHolderEntityInner : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +class SpiralKnotHolderEntityOuter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +class SpiralKnotHolderEntityCenter : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + + +/* + * set attributes via inner (t=t0) knot point: + * [default] increase/decrease inner point + * [shift] increase/decrease inner and outer arg synchronizely + * [control] constrain inner arg to round per PI/4 + */ +void +SpiralKnotHolderEntityInner::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + SPSpiral *spiral = dynamic_cast<SPSpiral *>(item); + g_assert(spiral != nullptr); + + gdouble dx = p[Geom::X] - spiral->cx; + gdouble dy = p[Geom::Y] - spiral->cy; + + gdouble moved_y = p[Geom::Y] - origin[Geom::Y]; + + if (state & GDK_MOD1_MASK) { + // adjust divergence by vertical drag, relative to rad + if (spiral->rad > 0) { + double exp_delta = 0.1*moved_y/(spiral->rad); // arbitrary multiplier to slow it down + spiral->exp += exp_delta; + if (spiral->exp < 1e-3) + spiral->exp = 1e-3; + } + } else { + // roll/unroll from inside + gdouble arg_t0; + spiral->getPolar(spiral->t0, nullptr, &arg_t0); + + gdouble arg_tmp = atan2(dy, dx) - arg_t0; + gdouble arg_t0_new = arg_tmp - floor((arg_tmp+M_PI)/(2.0*M_PI))*2.0*M_PI + arg_t0; + spiral->t0 = (arg_t0_new - spiral->arg) / (2.0*M_PI*spiral->revo); + + /* round inner arg per PI/snaps, if CTRL is pressed */ + if ( ( state & GDK_CONTROL_MASK ) + && ( fabs(spiral->revo) > SP_EPSILON_2 ) + && ( snaps != 0 ) ) { + gdouble arg = 2.0*M_PI*spiral->revo*spiral->t0 + spiral->arg; + double snaps_radian = M_PI/snaps; + spiral->t0 = (std::round(arg/snaps_radian)*snaps_radian - spiral->arg)/(2.0*M_PI*spiral->revo); + } + + spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999); + } + + spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +/* + * set attributes via outer (t=1) knot point: + * [default] increase/decrease revolution factor + * [control] constrain inner arg to round per PI/4 + */ +void +SpiralKnotHolderEntityOuter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + SPSpiral *spiral = dynamic_cast<SPSpiral *>(item); + g_assert(spiral != nullptr); + + gdouble dx = p[Geom::X] - spiral->cx; + gdouble dy = p[Geom::Y] - spiral->cy; + + if (state & GDK_SHIFT_MASK) { // rotate without roll/unroll + spiral->arg = atan2(dy, dx) - 2.0*M_PI*spiral->revo; + if (!(state & GDK_MOD1_MASK)) { + // if alt not pressed, change also rad; otherwise it is locked + spiral->rad = MAX(hypot(dx, dy), 0.001); + } + if ( ( state & GDK_CONTROL_MASK ) && snaps ) { + double snaps_radian = M_PI/snaps; + spiral->arg = std::round(spiral->arg/snaps_radian) * snaps_radian; + } + } else { // roll/unroll + // arg of the spiral outer end + double arg_1; + spiral->getPolar(1, nullptr, &arg_1); + + // its fractional part after the whole turns are subtracted + static double _2PI = 2.0 * M_PI; + double arg_r = arg_1 - std::round(arg_1/_2PI) * _2PI; + + // arg of the mouse point relative to spiral center + double mouse_angle = atan2(dy, dx); + if (mouse_angle < 0) + mouse_angle += _2PI; + + // snap if ctrl + if ( ( state & GDK_CONTROL_MASK ) && snaps ) { + double snaps_radian = M_PI/snaps; + mouse_angle = std::round(mouse_angle/snaps_radian) * snaps_radian; + } + + // by how much we want to rotate the outer point + double diff = mouse_angle - arg_r; + if (diff > M_PI) + diff -= _2PI; + else if (diff < -M_PI) + diff += _2PI; + + // calculate the new rad; + // the value of t corresponding to the angle arg_1 + diff: + double t_temp = ((arg_1 + diff) - spiral->arg)/(_2PI*spiral->revo); + // the rad at that t: + double rad_new = 0; + if (t_temp > spiral->t0) + spiral->getPolar(t_temp, &rad_new, nullptr); + + // change the revo (converting diff from radians to the number of turns) + spiral->revo += diff/(2*M_PI); + if (spiral->revo < 1e-3) + spiral->revo = 1e-3; + + // if alt not pressed and the values are sane, change the rad + if (!(state & GDK_MOD1_MASK) && rad_new > 1e-3 && rad_new/spiral->rad < 2) { + // adjust t0 too so that the inner point stays unmoved + double r0; + spiral->getPolar(spiral->t0, &r0, nullptr); + spiral->rad = rad_new; + spiral->t0 = pow(r0 / spiral->rad, 1.0/spiral->exp); + } + if (!std::isfinite(spiral->t0)) spiral->t0 = 0.0; + spiral->t0 = CLAMP(spiral->t0, 0.0, 0.999); + } + + spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +SpiralKnotHolderEntityCenter::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPSpiral *spiral = dynamic_cast<SPSpiral *>(item); + g_assert(spiral != nullptr); + + Geom::Point const s = snap_knot_position(p, state); + + spiral->cx = s[Geom::X]; + spiral->cy = s[Geom::Y]; + + spiral->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +Geom::Point +SpiralKnotHolderEntityInner::knot_get() const +{ + SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item); + g_assert(spiral != nullptr); + + return spiral->getXY(spiral->t0); +} + +Geom::Point +SpiralKnotHolderEntityOuter::knot_get() const +{ + SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item); + g_assert(spiral != nullptr); + + return spiral->getXY(1.0); +} + +Geom::Point +SpiralKnotHolderEntityCenter::knot_get() const +{ + SPSpiral const *spiral = dynamic_cast<SPSpiral const *>(item); + g_assert(spiral != nullptr); + + return Geom::Point(spiral->cx, spiral->cy); +} + +void +SpiralKnotHolderEntityInner::knot_click(unsigned int state) +{ + SPSpiral *spiral = dynamic_cast<SPSpiral *>(item); + g_assert(spiral != nullptr); + + if (state & GDK_MOD1_MASK) { + spiral->exp = 1; + spiral->updateRepr(); + } else if (state & GDK_SHIFT_MASK) { + spiral->t0 = 0; + spiral->updateRepr(); + } +} + +SpiralKnotHolder::SpiralKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + SpiralKnotHolderEntityCenter *entity_center = new SpiralKnotHolderEntityCenter(); + SpiralKnotHolderEntityInner *entity_inner = new SpiralKnotHolderEntityInner(); + SpiralKnotHolderEntityOuter *entity_outer = new SpiralKnotHolderEntityOuter(); + + // NOTE: entity_central and entity_inner can overlap. + // + // In that case it would be a problem if the center control point was ON + // TOP because it would steal the mouse focus and the user would loose the + // ability to access the inner control point using only the mouse. + // + // However if the inner control point is ON TOP, taking focus, the + // situation is a lot better: the user can still move the inner control + // point with the mouse to regain access to the center control point. + // + // So, create entity_inner AFTER entity_center; this ensures that + // entity_inner gets rendered ON TOP. + entity_center->create(desktop, item, this, Inkscape::CTRL_TYPE_POINT, + _("Drag to move the spiral"), + SP_KNOT_SHAPE_CROSS); + + entity_inner->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Roll/unroll the spiral from <b>inside</b>; with <b>Ctrl</b> to snap angle; " + "with <b>Alt</b> to converge/diverge")); + + entity_outer->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Roll/unroll the spiral from <b>outside</b>; with <b>Ctrl</b> to snap angle; " + "with <b>Shift</b> to scale/rotate; with <b>Alt</b> to lock radius")); + + entity.push_back(entity_center); + entity.push_back(entity_inner); + entity.push_back(entity_outer); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + +/* SPOffset */ + +class OffsetKnotHolderEntity : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +void +OffsetKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPOffset *offset = dynamic_cast<SPOffset *>(item); + g_assert(offset != nullptr); + + Geom::Point const p_snapped = snap_knot_position(p, state); + + offset->rad = sp_offset_distance_to_original(offset, p_snapped); + offset->knot = p_snapped; + offset->knotSet = true; + + offset->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + + +Geom::Point +OffsetKnotHolderEntity::knot_get() const +{ + SPOffset const *offset = dynamic_cast<SPOffset const *>(item); + g_assert(offset != nullptr); + + Geom::Point np; + sp_offset_top_point(offset,&np); + return np; +} + +OffsetKnotHolder::OffsetKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + OffsetKnotHolderEntity *entity_offset = new OffsetKnotHolderEntity(); + entity_offset->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Adjust the <b>offset distance</b>")); + entity.push_back(entity_offset); + + add_pattern_knotholder(); + add_hatch_knotholder(); +} + + +/* SPText */ +class TextKnotHolderEntityInlineSize : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_click(unsigned int state) override; +}; + +Geom::Point +TextKnotHolderEntityInlineSize::knot_get() const +{ + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + + SPStyle* style = text->style; + double inline_size = style->inline_size.computed; + unsigned mode = style->writing_mode.computed; + unsigned anchor = style->text_anchor.computed; + unsigned direction = style->direction.computed; + + Geom::Point p(text->attributes.firstXY()); + + if (text->has_inline_size()) { + // SVG 2 'inline-size' + + // Keep handle at end of text line. + if (mode == SP_CSS_WRITING_MODE_LR_TB || + mode == SP_CSS_WRITING_MODE_RL_TB) { + // horizontal + if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) { + p *= Geom::Translate (inline_size, 0); + } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate (inline_size/2.0, 0 ); + } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate (-inline_size/2.0, 0 ); + } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) { + p *= Geom::Translate (-inline_size, 0); + } + } else { + // vertical + if (anchor == SP_CSS_TEXT_ANCHOR_START) { + p *= Geom::Translate (0, inline_size); + } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate (0, inline_size/2.0); + } else if (anchor == SP_CSS_TEXT_ANCHOR_END) { + p *= Geom::Translate (0, -inline_size); + } + } + } else { + // Normal single line text. + Geom::OptRect bbox = text->geometricBounds(); // Check if this is best. + if (bbox) { + if (mode == SP_CSS_WRITING_MODE_LR_TB || + mode == SP_CSS_WRITING_MODE_RL_TB) { + // horizontal + if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) { + p *= Geom::Translate ((*bbox).width(), 0); + } else if ( direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate ((*bbox).width()/2, 0); + } else if ( direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate (-(*bbox).width()/2, 0); + } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) { + p *= Geom::Translate (-(*bbox).width(), 0); + } + } else { + // vertical + if (anchor == SP_CSS_TEXT_ANCHOR_START) { + p *= Geom::Translate (0, (*bbox).height()); + } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + p *= Geom::Translate (0, (*bbox).height()/2); + } else if (anchor == SP_CSS_TEXT_ANCHOR_END) { + p *= Geom::Translate (0, -(*bbox).height()); + } + p += Geom::Point((*bbox).width(), 0); // Keep on right side + } + } + } + + return p; +} + +// Conversion from Inkscape SVG 1.1 to SVG 2 'inline-size'. +void +TextKnotHolderEntityInlineSize::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + + SPStyle* style = text->style; + unsigned mode = style->writing_mode.computed; + unsigned anchor = style->text_anchor.computed; + unsigned direction = style->direction.computed; + + Geom::Point const s = snap_knot_position(p, state); + Geom::Point delta = s - text->attributes.firstXY(); + double size = 0.0; + if (mode == SP_CSS_WRITING_MODE_LR_TB || + mode == SP_CSS_WRITING_MODE_RL_TB) { + // horizontal + + size = delta[Geom::X]; + if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_START ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_END) ) { + // Do nothing + } else if ( (direction == SP_CSS_DIRECTION_LTR && anchor == SP_CSS_TEXT_ANCHOR_END ) || + (direction == SP_CSS_DIRECTION_RTL && anchor == SP_CSS_TEXT_ANCHOR_START) ) { + size = -size; + } else if ( anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + size = 2.0 * abs(size); + } else { + std::cerr << "TextKnotHolderEntityInlinSize: Should not be reached!" << std::endl; + } + + } else { + // vertical + + size = delta[Geom::Y]; + if (anchor == SP_CSS_TEXT_ANCHOR_START) { + // Do nothing + } else if (anchor == SP_CSS_TEXT_ANCHOR_END) { + size = -size; + } else if (anchor == SP_CSS_TEXT_ANCHOR_MIDDLE) { + size = 2.0 * abs(size); + } + } + + // Size should never be negative + if (size < 0.0) { + size = 0.0; + } + + // Set 'inline-size'. + text->style->inline_size.setDouble(size); + text->style->inline_size.set = true; + + // Ensure we respect new lines. + text->style->white_space.read("pre"); + text->style->white_space.set = true; + + // Convert sodipodi:role="line" to '\n'. + text->sodipodi_to_newline(); + + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + text->updateRepr(); +} + +// Conversion from SVG 2 'inline-size' to Inkscape's SVG 1.1. +void +TextKnotHolderEntityInlineSize::knot_click(unsigned int state) +{ + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + + if (state & GDK_CONTROL_MASK) { + + text->style->inline_size.clear(); + text->remove_svg11_fallback(); // Else 'x' and 'y' will be interpreted as absolute positions. + text->newline_to_sodipodi(); // Convert '\n' to tspans with sodipodi:role="line". + + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + text->updateRepr(); + } +} + +class TextKnotHolderEntityShapeInside : public KnotHolderEntity { +public: + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +Geom::Point +TextKnotHolderEntityShapeInside::knot_get() const +{ + // SVG 2 'shape-inside'. We only get here if there is a rectangle shape. + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + // we have a crash on undo cration so remove assert + // g_assert(text->style->shape_inside.set); + Geom::Point p; + if (text->style->shape_inside.set) { + Geom::OptRect frame = text->get_frame(); + if (frame) { + p = (*frame).corner(2); + } else { + std::cerr << "TextKnotHolderEntityShapeInside::knot_get(): no frame!" << std::endl; + } + } + return p; +} + +void +TextKnotHolderEntityShapeInside::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, unsigned int state) +{ + // Text in a shape: rectangle + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + g_assert(text->style->shape_inside.set); + + Geom::Point const s = snap_knot_position(p, state); + + Inkscape::XML::Node* rectangle = text->get_first_rectangle(); + double x = 0.0; + double y = 0.0; + sp_repr_get_double (rectangle, "x", &x); + sp_repr_get_double (rectangle, "y", &y); + double width = s[Geom::X] - x; + double height = s[Geom::Y] - y; + sp_repr_set_svg_double (rectangle, "width", width); + sp_repr_set_svg_double (rectangle, "height", height); + text->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + text->updateRepr(); +} + +TextKnotHolder::TextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + SPText *text = dynamic_cast<SPText *>(item); + g_assert(text != nullptr); + + if (text->style->shape_inside.set) { + // 'shape-inside' + TextKnotHolderEntityShapeInside *entity_shapeinside = new TextKnotHolderEntityShapeInside(); + + entity_shapeinside->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Adjust the <b>rectangular</b> region of the text."), + SP_KNOT_SHAPE_DIAMOND, SP_KNOT_MODE_XOR); + + entity.push_back(entity_shapeinside); + + } else { + // 'inline-size' or normal text + TextKnotHolderEntityInlineSize *entity_inlinesize = new TextKnotHolderEntityInlineSize(); + + entity_inlinesize->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Adjust the <b>inline size</b> (line length) of the text."), + SP_KNOT_SHAPE_DIAMOND, SP_KNOT_MODE_XOR); + + entity.push_back(entity_inlinesize); + } +} + + +// TODO: this is derived from RectKnotHolderEntityWH because it used the same static function +// set_internal as the latter before KnotHolderEntity was C++ified. Check whether this also makes +// sense logically. +class FlowtextKnotHolderEntity : public RectKnotHolderEntityWH { +public: + Geom::Point knot_get() const override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) override; +}; + +Geom::Point +FlowtextKnotHolderEntity::knot_get() const +{ + SPRect const *rect = dynamic_cast<SPRect const *>(item); + g_assert(rect != nullptr); + + return Geom::Point(rect->x.computed + rect->width.computed, rect->y.computed + rect->height.computed); +} + +void +FlowtextKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, unsigned int state) +{ + set_internal(p, origin, state); +} + +FlowtextKnotHolder::FlowtextKnotHolder(SPDesktop *desktop, SPItem *item, SPKnotHolderReleasedFunc relhandler) : + KnotHolder(desktop, item, relhandler) +{ + g_assert(item != nullptr); + + FlowtextKnotHolderEntity *entity_flowtext = new FlowtextKnotHolderEntity(); + entity_flowtext->create(desktop, item, this, Inkscape::CTRL_TYPE_SHAPER, + _("Drag to resize the <b>flowed text frame</b>")); + entity.push_back(entity_flowtext); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/shape-editor.cpp b/src/ui/shape-editor.cpp new file mode 100644 index 0000000..9125bbc --- /dev/null +++ b/src/ui/shape-editor.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::ShapeEditor + * This is a container class which contains a knotholder for shapes. + * It is attached to a single item. + *//* + * Authors: see git history + * bulia byak <buliabyak@users.sf.net> + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "document.h" +#include "knotholder.h" +#include "live_effects/effect.h" +#include "object/sp-lpe-item.h" + +#include "ui/shape-editor.h" +#include "xml/node-event-vector.h" + + +namespace Inkscape { +namespace UI { + +KnotHolder *createKnotHolder(SPItem *item, SPDesktop *desktop); +KnotHolder *createLPEKnotHolder(SPItem *item, SPDesktop *desktop); + +bool ShapeEditor::_blockSetItem = false; + +ShapeEditor::ShapeEditor(SPDesktop *dt, Geom::Affine edit_transform) : + desktop(dt), + knotholder(nullptr), + lpeknotholder(nullptr), + knotholder_listener_attached_for(nullptr), + lpeknotholder_listener_attached_for(nullptr), + _edit_transform(edit_transform) +{ +} + +ShapeEditor::~ShapeEditor() { + unset_item(); +} + +void ShapeEditor::unset_item(bool keep_knotholder) { + if (this->knotholder) { + Inkscape::XML::Node *old_repr = this->knotholder->repr; + if (old_repr && old_repr == knotholder_listener_attached_for) { + sp_repr_remove_listener_by_data(old_repr, this); + Inkscape::GC::release(old_repr); + knotholder_listener_attached_for = nullptr; + } + + if (!keep_knotholder) { + delete this->knotholder; + this->knotholder = nullptr; + } + } + if (this->lpeknotholder) { + Inkscape::XML::Node *old_repr = this->lpeknotholder->repr; + if (old_repr && old_repr == lpeknotholder_listener_attached_for) { + sp_repr_remove_listener_by_data(old_repr, this); + Inkscape::GC::release(old_repr); + lpeknotholder_listener_attached_for = nullptr; + } + + if (!keep_knotholder) { + delete this->lpeknotholder; + this->lpeknotholder = nullptr; + } + } +} + +bool ShapeEditor::has_knotholder() { + return this->knotholder != nullptr || this->lpeknotholder != nullptr; +} + +void ShapeEditor::update_knotholder() { + if (this->knotholder) + this->knotholder->update_knots(); + if (this->lpeknotholder) + this->lpeknotholder->update_knots(); +} + +bool ShapeEditor::has_local_change() { + return (this->knotholder && this->knotholder->local_change != 0) || (this->lpeknotholder && this->lpeknotholder->local_change != 0); +} + +void ShapeEditor::decrement_local_change() { + if (this->knotholder) { + this->knotholder->local_change = FALSE; + } + if (this->lpeknotholder) { + this->lpeknotholder->local_change = FALSE; + } +} + +void ShapeEditor::event_attr_changed(Inkscape::XML::Node * node, gchar const *name, gchar const *, gchar const *, bool, void *data) +{ + g_assert(data); + ShapeEditor *sh = static_cast<ShapeEditor *>(data); + bool changed_kh = false; + + if (sh->has_knotholder()) + { + changed_kh = !sh->has_local_change(); + sh->decrement_local_change(); + if (changed_kh) { + sh->reset_item(); + } + } +} + +static Inkscape::XML::NodeEventVector shapeeditor_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + ShapeEditor::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +void ShapeEditor::set_item(SPItem *item) { + if (_blockSetItem) { + return; + } + // this happens (and should only happen) when for an LPEItem having both knotholder and + // nodepath the knotholder is adapted; in this case we don't want to delete the knotholder + // since this freezes the handles + unset_item(true); + + if (item) { + Inkscape::XML::Node *repr; + if (!this->knotholder) { + // only recreate knotholder if none is present + this->knotholder = createKnotHolder(item, desktop); + } + SPLPEItem *lpe = dynamic_cast<SPLPEItem *>(item); + if (!(lpe && + lpe->getCurrentLPE() && + lpe->getCurrentLPE()->isVisible() && + lpe->getCurrentLPE()->providesKnotholder())) + { + delete this->lpeknotholder; + this->lpeknotholder = nullptr; + } + if (!this->lpeknotholder) { + // only recreate knotholder if none is present + this->lpeknotholder = createLPEKnotHolder(item, desktop); + } + if (this->knotholder) { + this->knotholder->setEditTransform(_edit_transform); + this->knotholder->update_knots(); + // setting new listener + repr = this->knotholder->repr; + if (repr != knotholder_listener_attached_for) { + Inkscape::GC::anchor(repr); + sp_repr_add_listener(repr, &shapeeditor_repr_events, this); + knotholder_listener_attached_for = repr; + } + } + if (this->lpeknotholder) { + this->lpeknotholder->setEditTransform(_edit_transform); + this->lpeknotholder->update_knots(); + // setting new listener + repr = this->lpeknotholder->repr; + if (repr != lpeknotholder_listener_attached_for) { + Inkscape::GC::anchor(repr); + sp_repr_add_listener(repr, &shapeeditor_repr_events, this); + lpeknotholder_listener_attached_for = repr; + } + } + } +} + + +/** FIXME: This thing is only called when the item needs to be updated in response to repr change. + Why not make a reload function in KnotHolder? */ +void ShapeEditor::reset_item() +{ + if (knotholder) { + SPObject *obj = desktop->getDocument()->getObjectByRepr(knotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject. + set_item(SP_ITEM(obj)); + } else if (lpeknotholder) { + SPObject *obj = desktop->getDocument()->getObjectByRepr(lpeknotholder_listener_attached_for); /// note that it is not certain that this is an SPItem; it could be a LivePathEffectObject. + set_item(SP_ITEM(obj)); + } +} + +/** + * Returns true if this ShapeEditor has a knot above which the mouse currently hovers. + */ +bool ShapeEditor::knot_mouseover() const { + if (this->knotholder) { + return knotholder->knot_mouseover(); + } + if (this->lpeknotholder) { + return lpeknotholder->knot_mouseover(); + } + + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/shape-editor.h b/src/ui/shape-editor.h new file mode 100644 index 0000000..d867ad9 --- /dev/null +++ b/src/ui/shape-editor.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::ShapeEditor + * This is a container class which contains a knotholder for shapes. + * It is attached to a single item. + *//* + * Authors: see git history + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SHAPE_EDITOR_H +#define SEEN_SHAPE_EDITOR_H + +class KnotHolder; +class LivePathEffectObject; +class SPDesktop; +class SPItem; + +namespace Inkscape { namespace XML { class Node; } +namespace UI { + +class ShapeEditor { +public: + + ShapeEditor(SPDesktop *desktop, Geom::Affine edit_transform = Geom::identity()); + ~ShapeEditor(); + + void set_item(SPItem *item); + void unset_item(bool keep_knotholder = false); + + void update_knotholder(); //((deprecated)) + + bool has_local_change(); + void decrement_local_change(); + + bool knot_mouseover() const; + KnotHolder *knotholder; + KnotHolder *lpeknotholder; + bool has_knotholder(); + static void blockSetItem(bool b) { _blockSetItem = b; } // kludge + static void event_attr_changed(Inkscape::XML::Node * /*repr*/, char const *name, char const * /*old_value*/, + char const * /*new_value*/, bool /*is_interactive*/, void *data); +private: + void reset_item(); + static bool _blockSetItem; + + SPDesktop *desktop; + Inkscape::XML::Node *knotholder_listener_attached_for; + Inkscape::XML::Node *lpeknotholder_listener_attached_for; + Geom::Affine _edit_transform; +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SHAPE_EDITOR_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : + diff --git a/src/ui/simple-pref-pusher.cpp b/src/ui/simple-pref-pusher.cpp new file mode 100644 index 0000000..bc71f27 --- /dev/null +++ b/src/ui/simple-pref-pusher.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "simple-pref-pusher.h" + +#include <gtkmm/toggletoolbutton.h> + +namespace Inkscape { +namespace UI { +SimplePrefPusher::SimplePrefPusher( Gtk::ToggleToolButton *btn, Glib::ustring const &path ) : + Observer(path), + _btn(btn), + freeze(false) +{ + freeze = true; + _btn->set_active( Inkscape::Preferences::get()->getBool(observed_path) ); + freeze = false; + + Inkscape::Preferences::get()->addObserver(*this); +} + +SimplePrefPusher::~SimplePrefPusher() +{ + Inkscape::Preferences::get()->removeObserver(*this); +} + +void +SimplePrefPusher::notify(Inkscape::Preferences::Entry const &newVal) +{ + bool newBool = newVal.getBool(); + bool oldBool = _btn->get_active(); + + if (!freeze && (newBool != oldBool)) { + _btn->set_active(newBool); + } +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/simple-pref-pusher.h b/src/ui/simple-pref-pusher.h new file mode 100644 index 0000000..c17c625 --- /dev/null +++ b/src/ui/simple-pref-pusher.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SIMPLE_PREF_PUSHER_H +#define SEEN_SIMPLE_PREF_PUSHER_H + +#include "preferences.h" + +namespace Gtk { +class ToggleToolButton; +} + +namespace Inkscape { +namespace UI { + +/** + * A simple mediator class that sets the state of a Gtk::ToggleToolButton when + * a preference is changed. Unlike the PrefPusher class, this does not provide + * the reverse process, so you still need to write your own handler for the + * "toggled" signal on the ToggleToolButton. + */ +class SimplePrefPusher : public Inkscape::Preferences::Observer +{ +public: + /** + * Constructor for a boolean value that syncs to the supplied path. + * Initializes the widget to the current preference stored state and registers callbacks + * for widget changes and preference changes. + * + * @param act the widget to synchronize preference with. + * @param path the path to the preference the widget is synchronized with. + * @param callback function to invoke when changes are pushed. + * @param cbData data to be passed on to the callback function. + */ + SimplePrefPusher(Gtk::ToggleToolButton *btn, + Glib::ustring const & path); + + /** + * Destructor that unregisters the preference callback. + */ + ~SimplePrefPusher() override; + + /** + * Callback method invoked when the preference setting changes. + */ + void notify(Inkscape::Preferences::Entry const &new_val) override; + + +private: + Gtk::ToggleToolButton *_btn; + bool freeze; +}; + +} +} +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool-factory.cpp b/src/ui/tool-factory.cpp new file mode 100644 index 0000000..8c80c39 --- /dev/null +++ b/src/ui/tool-factory.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for ToolBase tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tool-factory.h" + +#include "ui/tools/arc-tool.h" +#include "ui/tools/box3d-tool.h" +#include "ui/tools/calligraphic-tool.h" +#include "ui/tools/connector-tool.h" +#include "ui/tools/dropper-tool.h" +#include "ui/tools/eraser-tool.h" +#include "ui/tools/flood-tool.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/measure-tool.h" +#include "ui/tools/mesh-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/pencil-tool.h" +#include "ui/tools/rect-tool.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/spiral-tool.h" +#include "ui/tools/spray-tool.h" +#include "ui/tools/star-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/tools/tweak-tool.h" +#include "ui/tools/zoom-tool.h" + +using namespace Inkscape::UI::Tools; + +ToolBase *ToolFactory::createObject(std::string const& id) +{ + ToolBase *tool = nullptr; + + if (id == "/tools/shapes/arc") + tool = new ArcTool; + else if (id == "/tools/shapes/3dbox") + tool = new Box3dTool; + else if (id == "/tools/calligraphic") + tool = new CalligraphicTool; + else if (id == "/tools/connector") + tool = new ConnectorTool; + else if (id == "/tools/dropper") + tool = new DropperTool; + else if (id == "/tools/eraser") + tool = new EraserTool; + else if (id == "/tools/paintbucket") + tool = new FloodTool; + else if (id == "/tools/gradient") + tool = new GradientTool; + else if (id == "/tools/lpetool") + tool = new LpeTool; + else if (id == "/tools/measure") + tool = new MeasureTool; + else if (id == "/tools/mesh") + tool = new MeshTool; + else if (id == "/tools/nodes") + tool = new NodeTool; + else if (id == "/tools/freehand/pencil") + tool = new PencilTool; + else if (id == "/tools/freehand/pen") + tool = new PenTool; + else if (id == "/tools/shapes/rect") + tool = new RectTool; + else if (id == "/tools/select") + tool = new SelectTool; + else if (id == "/tools/shapes/spiral") + tool = new SpiralTool; + else if (id == "/tools/spray") + tool = new SprayTool; + else if (id == "/tools/shapes/star") + tool = new StarTool; + else if (id == "/tools/text") + tool = new TextTool; + else if (id == "/tools/tweak") + tool = new TweakTool; + else if (id == "/tools/zoom") + tool = new ZoomTool; + else + fprintf(stderr, "WARNING: unknown tool: %s", id.c_str()); + + return tool; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool-factory.h b/src/ui/tool-factory.h new file mode 100644 index 0000000..e6dcbd2 --- /dev/null +++ b/src/ui/tool-factory.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Factory for ToolBase tree + * + * Authors: + * Markus Engel + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef TOOL_FACTORY_SEEN +#define TOOL_FACTORY_SEEN + +#include <string> + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +} +} +} + +struct ToolFactory { + static Inkscape::UI::Tools::ToolBase *createObject(std::string const& id); +}; + + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/commit-events.h b/src/ui/tool/commit-events.h new file mode 100644 index 0000000..37fb861 --- /dev/null +++ b/src/ui/tool/commit-events.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Commit events. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_COMMIT_EVENTS_H +#define SEEN_UI_TOOL_COMMIT_EVENTS_H + +namespace Inkscape { +namespace UI { + +/// This is used to provide sensible messages on the undo stack. +enum CommitEvent { + COMMIT_MOUSE_MOVE, + COMMIT_KEYBOARD_MOVE_X, + COMMIT_KEYBOARD_MOVE_Y, + COMMIT_MOUSE_SCALE, + COMMIT_MOUSE_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_UNIFORM, + COMMIT_KEYBOARD_SCALE_X, + COMMIT_KEYBOARD_SCALE_Y, + COMMIT_MOUSE_ROTATE, + COMMIT_KEYBOARD_ROTATE, + COMMIT_MOUSE_SKEW_X, + COMMIT_MOUSE_SKEW_Y, + COMMIT_KEYBOARD_SKEW_X, + COMMIT_KEYBOARD_SKEW_Y, + COMMIT_FLIP_X, + COMMIT_FLIP_Y +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp new file mode 100644 index 0000000..186d782 --- /dev/null +++ b/src/ui/tool/control-point-selection.cpp @@ -0,0 +1,773 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Node selection - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/none.hpp> +#include "ui/tool/selectable-control-point.h" +#include <2geom/transforms.h> +#include "desktop.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/transform-handle-set.h" +#include "ui/tool/node.h" + + + +#include <gdk/gdkkeysyms.h> + +namespace Inkscape { +namespace UI { + +/** + * @class ControlPointSelection + * Group of selected control points. + * + * Some operations can be performed on all selected points regardless of their type, therefore + * this class is also a Manipulator. It handles the transformations of points using + * the keyboard. + * + * The exposed interface is similar to that of an STL set. Internally, a hash map is used. + * @todo Correct iterators (that don't expose the connection list) + */ + +/** @var ControlPointSelection::signal_update + * Fires when the display needs to be updated to reflect changes. + */ +/** @var ControlPointSelection::signal_point_changed + * Fires when a control point is added to or removed from the selection. + * The first param contains a pointer to the control point that changed sel. state. + * The second says whether the point is currently selected. + */ +/** @var ControlPointSelection::signal_commit + * Fires when a change that needs to be committed to XML happens. + */ + +ControlPointSelection::ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group) + : Manipulator(d) + , _handles(new TransformHandleSet(d, th_group)) + , _dragging(false) + , _handles_visible(true) + , _one_node_handles(false) +{ + signal_update.connect( sigc::bind( + sigc::mem_fun(*this, &ControlPointSelection::_updateTransformHandles), + true)); + ControlPoint::signal_mouseover_change.connect( + sigc::hide( + sigc::mem_fun(*this, &ControlPointSelection::_mouseoverChanged))); + _handles->signal_transform.connect( + sigc::mem_fun(*this, &ControlPointSelection::transform)); + _handles->signal_commit.connect( + sigc::mem_fun(*this, &ControlPointSelection::_commitHandlesTransform)); +} + +ControlPointSelection::~ControlPointSelection() +{ + clear(); + delete _handles; +} + +/** Add a control point to the selection. */ +std::pair<ControlPointSelection::iterator, bool> ControlPointSelection::insert(const value_type &x, bool notify, bool to_update) +{ + iterator found = _points.find(x); + if (found != _points.end()) { + return std::pair<iterator, bool>(found, false); + } + + found = _points.insert(x).first; + _points_list.push_back(x); + + x->updateState(); + + if (to_update) { + _update(); + } + if (notify) { + signal_selection_changed.emit(std::vector<key_type>(1, x), true); + } + + return std::pair<iterator, bool>(found, true); +} + +/** Remove a point from the selection. */ +void ControlPointSelection::erase(iterator pos, bool to_update) +{ + SelectableControlPoint *erased = *pos; + _points_list.remove(*pos); + _points.erase(pos); + erased->updateState(); + if (to_update) { + _update(); + } +} +ControlPointSelection::size_type ControlPointSelection::erase(const key_type &k, bool notify) +{ + iterator pos = _points.find(k); + if (pos == _points.end()) return 0; + erase(pos); + + if (notify) { + signal_selection_changed.emit(std::vector<key_type>(1, k), false); + } + return 1; +} +void ControlPointSelection::erase(iterator first, iterator last) +{ + std::vector<SelectableControlPoint *> out(first, last); + while (first != last) { + erase(first++, false); + } + _update(); + signal_selection_changed.emit(out, false); +} + +/** Remove all points from the selection, making it empty. */ +void ControlPointSelection::clear() +{ + if (empty()) { + return; + } + + std::vector<SelectableControlPoint *> out(begin(), end()); // begin() takes from _points + _points.clear(); + _points_list.clear(); + for (auto erased : out) { + erased->updateState(); + } + + _update(); + signal_selection_changed.emit(out, false); +} + +/** Select all points that this selection can contain. */ +void ControlPointSelection::selectAll() +{ + for (auto _all_point : _all_points) { + insert(_all_point, false, false); + } + std::vector<SelectableControlPoint *> out(_all_points.begin(), _all_points.end()); + if (!out.empty()) { + _update(); + signal_selection_changed.emit(out, true); + } +} +/** Select all points inside the given rectangle (in desktop coordinates). */ +void ControlPointSelection::selectArea(Geom::Rect const &r) +{ + std::vector<SelectableControlPoint *> out; + for (auto _all_point : _all_points) { + if (r.contains(*_all_point)) { + insert(_all_point, false, false); + out.push_back(_all_point); + } + } + if (!out.empty()) { + _update(); + signal_selection_changed.emit(out, true); + } +} +/** Unselect all selected points and select all unselected points. */ +void ControlPointSelection::invertSelection() +{ + std::vector<SelectableControlPoint *> in, out; + for (auto _all_point : _all_points) { + if (_all_point->selected()) { + in.push_back(_all_point); + erase(_all_point); + } + else { + out.push_back(_all_point); + insert(_all_point, false, false); + } + } + _update(); + if (!in.empty()) + signal_selection_changed.emit(in, false); + if (!out.empty()) + signal_selection_changed.emit(out, true); +} +void ControlPointSelection::spatialGrow(SelectableControlPoint *origin, int dir) +{ + bool grow = (dir > 0); + Geom::Point p = origin->position(); + double best_dist = grow ? HUGE_VAL : 0; + SelectableControlPoint *match = nullptr; + for (auto _all_point : _all_points) { + bool selected = _all_point->selected(); + if (grow && !selected) { + double dist = Geom::distance(_all_point->position(), p); + if (dist < best_dist) { + best_dist = dist; + match = _all_point; + } + } + if (!grow && selected) { + double dist = Geom::distance(_all_point->position(), p); + // use >= to also deselect the origin node when it's the last one selected + if (dist >= best_dist) { + best_dist = dist; + match = _all_point; + } + } + } + if (match) { + if (grow) insert(match); + else erase(match); + signal_selection_changed.emit(std::vector<value_type>(1, match), grow); + } +} + +/** Transform all selected control points by the given affine transformation. */ +void ControlPointSelection::transform(Geom::Affine const &m) +{ + for (auto cur : _points) { + cur->transform(m); + } + _updateBounds(); + // TODO preserving the rotation radius needs some rethinking... + if (_rot_radius) (*_rot_radius) *= m.descrim(); + if (_mouseover_rot_radius) (*_mouseover_rot_radius) *= m.descrim(); + signal_update.emit(); +} + +/** Align control points on the specified axis. */ +void ControlPointSelection::align(Geom::Dim2 axis) +{ + enum AlignTargetNode { LAST_NODE=0, FIRST_NODE, MID_NODE, MIN_NODE, MAX_NODE }; + if (empty()) return; + Geom::Dim2 d = static_cast<Geom::Dim2>((axis + 1) % 2); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + + Geom::OptInterval bound; + for (auto _point : _points) { + bound.unionWith(Geom::OptInterval(_point->position()[d])); + } + + if (!bound) { return; } + + double new_coord; + switch (AlignTargetNode(prefs->getInt("/dialogs/align/align-nodes-to", 2))){ + case FIRST_NODE: + new_coord=(_points_list.front())->position()[d]; + break; + case LAST_NODE: + new_coord=(_points_list.back())->position()[d]; + break; + case MID_NODE: + new_coord=bound->middle(); + break; + case MIN_NODE: + new_coord=bound->min(); + break; + case MAX_NODE: + new_coord=bound->max(); + break; + default: + return; + } + + for (auto _point : _points) { + Geom::Point pos = _point->position(); + pos[d] = new_coord; + _point->move(pos); + } +} + +/** Equdistantly distribute control points by moving them in the specified dimension. */ +void ControlPointSelection::distribute(Geom::Dim2 d) +{ + if (empty()) return; + + // this needs to be a multimap, otherwise it will fail when some points have the same coord + typedef std::multimap<double, SelectableControlPoint*> SortMap; + + SortMap sm; + Geom::OptInterval bound; + // first we insert all points into a multimap keyed by the aligned coord to sort them + // simultaneously we compute the extent of selection + for (auto _point : _points) { + Geom::Point pos = _point->position(); + sm.insert(std::make_pair(pos[d], _point)); + bound.unionWith(Geom::OptInterval(pos[d])); + } + + if (!bound) { return; } + + // now we iterate over the multimap and set aligned positions. + double step = size() == 1 ? 0 : bound->extent() / (size() - 1); + double start = bound->min(); + unsigned num = 0; + for (SortMap::iterator i = sm.begin(); i != sm.end(); ++i, ++num) { + Geom::Point pos = i->second->position(); + pos[d] = start + num * step; + i->second->move(pos); + } +} + +/** Get the bounds of the selection. + * @return Smallest rectangle containing the positions of all selected points, + * or nothing if the selection is empty */ +Geom::OptRect ControlPointSelection::pointwiseBounds() +{ + return _bounds; +} + +Geom::OptRect ControlPointSelection::bounds() +{ + return size() == 1 ? (*_points.begin())->bounds() : _bounds; +} + +void ControlPointSelection::showTransformHandles(bool v, bool one_node) +{ + _one_node_handles = one_node; + _handles_visible = v; + _updateTransformHandles(false); +} + +void ControlPointSelection::hideTransformHandles() +{ + _handles->setVisible(false); +} +void ControlPointSelection::restoreTransformHandles() +{ + _updateTransformHandles(true); +} + +void ControlPointSelection::toggleTransformHandlesMode() +{ + if (_handles->mode() == TransformHandleSet::MODE_SCALE) { + _handles->setMode(TransformHandleSet::MODE_ROTATE_SKEW); + if (size() == 1) { + _handles->rotationCenter().setVisible(false); + } + } else { + _handles->setMode(TransformHandleSet::MODE_SCALE); + } +} + +void ControlPointSelection::_pointGrabbed(SelectableControlPoint *point) +{ + hideTransformHandles(); + _dragging = true; + _grabbed_point = point; + _farthest_point = point; + double maxdist = 0; + Geom::Affine m; + m.setIdentity(); + for (auto _point : _points) { + _original_positions.insert(std::make_pair(_point, _point->position())); + _last_trans.insert(std::make_pair(_point, m)); + double dist = Geom::distance(*_grabbed_point, *_point); + if (dist > maxdist) { + maxdist = dist; + _farthest_point = _point; + } + } +} + +void ControlPointSelection::_pointDragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point abs_delta = new_pos - _original_positions[_grabbed_point]; + double fdist = Geom::distance(_original_positions[_grabbed_point], _original_positions[_farthest_point]); + if (held_only_alt(*event) && fdist > 0) { + // Sculpting + for (auto cur : _points) { + Geom::Affine trans; + trans.setIdentity(); + double dist = Geom::distance(_original_positions[cur], _original_positions[_grabbed_point]); + double deltafrac = 0.5 + 0.5 * cos(M_PI * dist/fdist); + if (dist != 0.0) { + // The sculpting transformation is not affine, but it can be + // locally approximated by one. Here we compute the local + // affine approximation of the sculpting transformation near + // the currently transformed point. We then transform the point + // by this approximation. This gives us sensible behavior for node handles. + // NOTE: probably it would be better to transform the node handles, + // but ControlPointSelection is supposed to work for any + // SelectableControlPoints, not only Nodes. We could create a specialized + // NodeSelection class that inherits from this one and move sculpting there. + Geom::Point origdx(Geom::EPSILON, 0); + Geom::Point origdy(0, Geom::EPSILON); + Geom::Point origp = _original_positions[cur]; + Geom::Point origpx = _original_positions[cur] + origdx; + Geom::Point origpy = _original_positions[cur] + origdy; + double distdx = Geom::distance(origpx, _original_positions[_grabbed_point]); + double distdy = Geom::distance(origpy, _original_positions[_grabbed_point]); + double deltafracdx = 0.5 + 0.5 * cos(M_PI * distdx/fdist); + double deltafracdy = 0.5 + 0.5 * cos(M_PI * distdy/fdist); + Geom::Point newp = origp + abs_delta * deltafrac; + Geom::Point newpx = origpx + abs_delta * deltafracdx; + Geom::Point newpy = origpy + abs_delta * deltafracdy; + Geom::Point newdx = (newpx - newp) / Geom::EPSILON; + Geom::Point newdy = (newpy - newp) / Geom::EPSILON; + + Geom::Affine itrans(newdx[Geom::X], newdx[Geom::Y], newdy[Geom::X], newdy[Geom::Y], 0, 0); + if (itrans.isSingular()) + itrans.setIdentity(); + + trans *= Geom::Translate(-cur->position()); + trans *= _last_trans[cur].inverse(); + trans *= itrans; + trans *= Geom::Translate(_original_positions[cur] + abs_delta * deltafrac); + _last_trans[cur] = itrans; + } else { + trans *= Geom::Translate(-cur->position() + _original_positions[cur] + abs_delta * deltafrac); + } + cur->transform(trans); + //cur->move(_original_positions[cur] + abs_delta * deltafrac); + } + } else { + Geom::Point delta = new_pos - _grabbed_point->position(); + for (auto cur : _points) { + cur->move(_original_positions[cur] + abs_delta); + } + _handles->rotationCenter().move(_handles->rotationCenter().position() + delta); + } + signal_update.emit(); +} + +void ControlPointSelection::_pointUngrabbed() +{ + _original_positions.clear(); + _last_trans.clear(); + _dragging = false; + _grabbed_point = _farthest_point = nullptr; + _updateBounds(); + restoreTransformHandles(); + signal_commit.emit(COMMIT_MOUSE_MOVE); +} + +bool ControlPointSelection::_pointClicked(SelectableControlPoint *p, GdkEventButton *event) +{ + // clicking a selected node should toggle the transform handles between rotate and scale mode, + // if they are visible + if (held_no_modifiers(*event) && _handles_visible && p->selected()) { + toggleTransformHandlesMode(); + return true; + } + return false; +} + +void ControlPointSelection::_mouseoverChanged() +{ + _mouseover_rot_radius = boost::none; +} + +void ControlPointSelection::_update() +{ + _updateBounds(); + _updateTransformHandles(false); + if (_bounds) { + _handles->rotationCenter().move(_bounds->midpoint()); + } +} + +void ControlPointSelection::_updateBounds() +{ + _rot_radius = boost::none; + _bounds = Geom::OptRect(); + for (auto cur : _points) { + Geom::Point p = cur->position(); + if (!_bounds) { + _bounds = Geom::Rect(p, p); + } else { + _bounds->expandTo(p); + } + } +} + +void ControlPointSelection::_updateTransformHandles(bool preserve_center) +{ + if (_dragging) return; + + if (_handles_visible && size() > 1) { + _handles->setBounds(*bounds(), preserve_center); + _handles->setVisible(true); + } else if (_one_node_handles && size() == 1) { // only one control point in selection + SelectableControlPoint *p = *begin(); + _handles->setBounds(p->bounds()); + _handles->rotationCenter().move(p->position()); + _handles->rotationCenter().setVisible(false); + _handles->setVisible(true); + } else { + _handles->setVisible(false); + } +} + +/** Moves the selected points along the supplied unit vector according to + * the modifier state of the supplied event. */ +bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) +{ + if (held_control(event)) return false; + unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + + Geom::Point delta = dir * num; + if (held_shift(event)) delta *= 10; + if (held_alt(event)) { + delta /= _desktop->current_zoom(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); + delta *= nudge; + } + + transform(Geom::Translate(delta)); + if (fabs(dir[Geom::X]) > 0) { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_X); + } else { + signal_commit.emit(COMMIT_KEYBOARD_MOVE_Y); + } + return true; +} + +/** + * Computes the distance to the farthest corner of the bounding box. + * Used to determine what it means to "rotate by one pixel". + */ +double ControlPointSelection::_rotationRadius(Geom::Point const &rc) +{ + if (empty()) return 1.0; // some safe value + Geom::Rect b = *bounds(); + double maxlen = 0; + for (unsigned i = 0; i < 4; ++i) { + double len = Geom::distance(b.corner(i), rc); + if (len > maxlen) maxlen = len; + } + return maxlen; +} + +/** + * Rotates the selected points in the given direction according to the modifier state + * from the supplied event. + * @param event Key event to take modifier state from + * @param dir Direction of rotation (math convention: 1 = counterclockwise, -1 = clockwise) + */ +bool ControlPointSelection::_keyboardRotate(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + Geom::Point rc; + + // rotate around the mouseovered point, or the selection's rotation center + // if nothing is mouseovered + double radius; + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + if (scp) { + rc = scp->position(); + if (!_mouseover_rot_radius) { + _mouseover_rot_radius = _rotationRadius(rc); + } + radius = *_mouseover_rot_radius; + } else { + rc = _handles->rotationCenter(); + if (!_rot_radius) { + _rot_radius = _rotationRadius(rc); + } + radius = *_rot_radius; + } + + double angle; + if (held_alt(event)) { + // Rotate by "one pixel". We interpret this as rotating by an angle that causes + // the topmost point of a circle circumscribed about the selection's bounding box + // to move on an arc 1 screen pixel long. + angle = atan2(1.0 / _desktop->current_zoom(), radius) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + // translate to origin, rotate, translate back to original position + Geom::Affine m = Geom::Translate(-rc) + * Geom::Rotate(angle) * Geom::Translate(rc); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_ROTATE); + return true; +} + + +bool ControlPointSelection::_keyboardScale(GdkEventKey const &event, int dir) +{ + if (empty()) return false; + + double maxext = bounds()->maxExtent(); + if (Geom::are_near(maxext, 0)) return false; + + Geom::Point center; + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + if (scp) { + center = scp->position(); + } else { + center = _handles->rotationCenter().position(); + } + + double length_change; + if (held_alt(event)) { + // Scale by "one pixel". It means shrink/grow 1px for the larger dimension + // of the bounding box. + length_change = 1.0 / _desktop->current_zoom() * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px"); + length_change *= dir; + } + double scale = (maxext + length_change) / maxext; + + Geom::Affine m = Geom::Translate(-center) * Geom::Scale(scale) * Geom::Translate(center); + transform(m); + signal_commit.emit(COMMIT_KEYBOARD_SCALE_UNIFORM); + return true; +} + +bool ControlPointSelection::_keyboardFlip(Geom::Dim2 d) +{ + if (empty()) return false; + + Geom::Scale scale_transform(1, 1); + if (d == Geom::X) { + scale_transform = Geom::Scale(-1, 1); + } else { + scale_transform = Geom::Scale(1, -1); + } + + SelectableControlPoint *scp = + dynamic_cast<SelectableControlPoint*>(ControlPoint::mouseovered_point); + Geom::Point center = scp ? scp->position() : _handles->rotationCenter().position(); + + Geom::Affine m = Geom::Translate(-center) * scale_transform * Geom::Translate(center); + transform(m); + signal_commit.emit(d == Geom::X ? COMMIT_FLIP_X : COMMIT_FLIP_Y); + return true; +} + +void ControlPointSelection::_commitHandlesTransform(CommitEvent ce) +{ + _updateBounds(); + _updateTransformHandles(true); + signal_commit.emit(ce); +} + +bool ControlPointSelection::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event) +{ + // implement generic event handling that should apply for all control point selections here; + // for example, keyboard moves and transformations. This way this functionality doesn't need + // to be duplicated in many places + // Later split out so that it can be reused in object selection + + switch (event->type) { + case GDK_KEY_PRESS: + // do not handle key events if the selection is empty + if (empty()) break; + + switch(shortcut_key(event->key)) { + // moves + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + return _keyboardMove(event->key, Geom::Point(0, -_desktop->yaxisdir())); + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + return _keyboardMove(event->key, Geom::Point(0, _desktop->yaxisdir())); + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + return _keyboardMove(event->key, Geom::Point(1, 0)); + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + return _keyboardMove(event->key, Geom::Point(-1, 0)); + + // rotates + case GDK_KEY_bracketleft: + return _keyboardRotate(event->key, -_desktop->yaxisdir()); + case GDK_KEY_bracketright: + return _keyboardRotate(event->key, _desktop->yaxisdir()); + + // scaling + case GDK_KEY_less: + case GDK_KEY_comma: + return _keyboardScale(event->key, -1); + case GDK_KEY_greater: + case GDK_KEY_period: + return _keyboardScale(event->key, 1); + + // TODO: skewing + + // flipping + // NOTE: H is horizontal flip, while Shift+H switches transform handle mode! + case GDK_KEY_h: + case GDK_KEY_H: + if (held_shift(event->key)) { + toggleTransformHandlesMode(); + return true; + } + // any modifiers except shift should cause no action + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::X); + case GDK_KEY_v: + case GDK_KEY_V: + if (held_any_modifiers(event->key)) break; + return _keyboardFlip(Geom::Y); + default: break; + } + break; + default: break; + } + return false; +} + +void ControlPointSelection::getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts) +{ + pts.clear(); + for (auto _point : _points) { + pts.emplace_back(_original_positions[_point], SNAPSOURCE_NODE_HANDLE); + } +} + +void ControlPointSelection::getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts) +{ + pts.clear(); + ControlPointSelection::Set &nodes = this->allPoints(); + for (auto node : nodes) { + if (!node->selected()) { + Node *n = static_cast<Node*>(node); + pts.push_back(n->snapCandidatePoint()); + } + } +} + +void ControlPointSelection::setOriginalPoints() +{ + _original_positions.clear(); + for (auto _point : _points) { + _original_positions.insert(std::make_pair(_point, _point->position())); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point-selection.h b/src/ui/tool/control-point-selection.h new file mode 100644 index 0000000..b50f1f2 --- /dev/null +++ b/src/ui/tool/control-point-selection.h @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Control point selection - stores a set of control points and applies transformations + * to them + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H +#define SEEN_UI_TOOL_CONTROL_POINT_SELECTION_H + +#include <list> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <boost/optional.hpp> +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/forward.h> +#include <2geom/point.h> +#include <2geom/rect.h> +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" +#include "snap-candidate.h" + +class SPDesktop; +struct SPCanvasGroup; + +namespace Inkscape { +namespace UI { +class TransformHandleSet; +class SelectableControlPoint; +} +} + +namespace Inkscape { +namespace UI { + +class ControlPointSelection : public Manipulator, public sigc::trackable { +public: + ControlPointSelection(SPDesktop *d, SPCanvasGroup *th_group); + ~ControlPointSelection() override; + typedef std::unordered_set<SelectableControlPoint *> set_type; + typedef set_type Set; // convenience alias + + typedef set_type::iterator iterator; + typedef set_type::const_iterator const_iterator; + typedef set_type::size_type size_type; + typedef SelectableControlPoint *value_type; + typedef SelectableControlPoint *key_type; + + // size + bool empty() { return _points.empty(); } + size_type size() { return _points.size(); } + + // iterators + iterator begin() { return _points.begin(); } + const_iterator begin() const { return _points.begin(); } + iterator end() { return _points.end(); } + const_iterator end() const { return _points.end(); } + + // insert + std::pair<iterator, bool> insert(const value_type& x, bool notify = true, bool to_update = true); + template <class InputIterator> + void insert(InputIterator first, InputIterator last) { + for (; first != last; ++first) { + insert(*first, false, false); + } + _update(); + signal_selection_changed.emit(std::vector<key_type>(first, last), true); + } + + // erase + void clear(); + void erase(iterator pos, bool to_update = true); + size_type erase(const key_type& k, bool notify = true); + void erase(iterator first, iterator last); + + // find + iterator find(const key_type &k) { + return _points.find(k); + } + + // Sometimes it is very useful to keep a list of all selectable points. + set_type const &allPoints() const { return _all_points; } + set_type &allPoints() { return _all_points; } + // ...for example in these methods. Another useful case is snapping. + void selectAll(); + void selectArea(Geom::Rect const &); + void invertSelection(); + void spatialGrow(SelectableControlPoint *origin, int dir); + + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + void transform(Geom::Affine const &m); + void align(Geom::Dim2 d); + void distribute(Geom::Dim2 d); + + Geom::OptRect pointwiseBounds(); + Geom::OptRect bounds(); + + bool transformHandlesEnabled() { return _handles_visible; } + void showTransformHandles(bool v, bool one_node); + // the two methods below do not modify the state; they are for use in manipulators + // that need to temporarily hide the handles, for example when moving a node + void hideTransformHandles(); + void restoreTransformHandles(); + void toggleTransformHandlesMode(); + + sigc::signal<void> signal_update; + // It turns out that emitting a signal after every point is selected or deselected is not too efficient, + // so this can be done in a massive group once the selection is finally changed. + sigc::signal<void, std::vector<SelectableControlPoint *>, bool> signal_selection_changed; + sigc::signal<void, CommitEvent> signal_commit; + + void getOriginalPoints(std::vector<Inkscape::SnapCandidatePoint> &pts); + void getUnselectedPoints(std::vector<Inkscape::SnapCandidatePoint> &pts); + void setOriginalPoints(); + //the purpose of this list is to keep track of first and last selected + std::list<SelectableControlPoint *> _points_list; + +private: + // The functions below are invoked from SelectableControlPoint. + // Previously they were connected to handlers when selecting, but this + // creates problems when dragging a point that was not selected. + void _pointGrabbed(SelectableControlPoint *); + void _pointDragged(Geom::Point &, GdkEventMotion *); + void _pointUngrabbed(); + bool _pointClicked(SelectableControlPoint *, GdkEventButton *); + void _mouseoverChanged(); + + void _update(); + void _updateTransformHandles(bool preserve_center); + void _updateBounds(); + bool _keyboardMove(GdkEventKey const &, Geom::Point const &); + bool _keyboardRotate(GdkEventKey const &, int); + bool _keyboardScale(GdkEventKey const &, int); + bool _keyboardFlip(Geom::Dim2); + void _keyboardTransform(Geom::Affine const &); + void _commitHandlesTransform(CommitEvent ce); + double _rotationRadius(Geom::Point const &); + + set_type _points; + + set_type _all_points; + std::unordered_map<SelectableControlPoint *, Geom::Point> _original_positions; + std::unordered_map<SelectableControlPoint *, Geom::Affine> _last_trans; + boost::optional<double> _rot_radius; + boost::optional<double> _mouseover_rot_radius; + Geom::OptRect _bounds; + TransformHandleSet *_handles; + SelectableControlPoint *_grabbed_point, *_farthest_point; + unsigned _dragging : 1; + unsigned _handles_visible : 1; + unsigned _one_node_handles : 1; + + friend class SelectableControlPoint; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp new file mode 100644 index 0000000..c6f18f2 --- /dev/null +++ b/src/ui/tool/control-point.cpp @@ -0,0 +1,637 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> + +#include <gdk/gdkkeysyms.h> +#include <gdkmm.h> + +#include <2geom/point.h> + +#include "desktop.h" +#include "message-context.h" + +#include "display/sp-canvas.h" +#include "display/snap-indicator.h" + +#include "object/sp-namedview.h" + +#include "ui/tools/tool-base.h" +#include "ui/control-manager.h" +#include "ui/tool/control-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/transform-handle-set.h" + +namespace Inkscape { +namespace UI { + + +// Default colors for control points +ControlPoint::ColorSet ControlPoint::_default_color_set = { + {0xffffff00, 0x01000000}, // normal fill, stroke + {0xff0000ff, 0x01000000}, // mouseover fill, stroke + {0x0000ffff, 0x01000000}, // clicked fill, stroke + // + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +ControlPoint *ControlPoint::mouseovered_point = nullptr; + +sigc::signal<void, ControlPoint*> ControlPoint::signal_mouseover_change; + +Geom::Point ControlPoint::_drag_event_origin(Geom::infinity(), Geom::infinity()); + +Geom::Point ControlPoint::_drag_origin(Geom::infinity(), Geom::infinity()); + +int const ControlPoint::_grab_event_mask = (GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + +bool ControlPoint::_drag_initiated = false; +bool ControlPoint::_event_grab = false; + +ControlPoint::ColorSet ControlPoint::invisible_cset = { + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000}, + {0x00000000, 0x00000000} +}; + +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ColorSet const &cset, SPCanvasGroup *group) : + _desktop(d), + _canvas_item(nullptr), + _cset(cset), + _state(STATE_NORMAL), + _position(initial_pos), + _lurking(false), + _double_clicked(false) +{ + _canvas_item = sp_canvas_item_new( + group ? group : _desktop->getControls(), SP_TYPE_CTRL, + "anchor", (SPAnchorType) anchor, "size", (unsigned int) pixbuf->get_width(), + "shape", SP_CTRL_SHAPE_BITMAP, "pixbuf", pixbuf->gobj(), + "filled", TRUE, "fill_color", _cset.normal.fill, + "stroked", TRUE, "stroke_color", _cset.normal.stroke, + "mode", SP_CTRL_MODE_XOR, NULL); + + _commonInit(); +} + +ControlPoint::ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + ControlType type, + ColorSet const &cset, SPCanvasGroup *group) : + _desktop(d), + _canvas_item(nullptr), + _cset(cset), + _state(STATE_NORMAL), + _position(initial_pos), + _lurking(false), + _double_clicked(false) +{ + _canvas_item = ControlManager::getManager().createControl(group ? group : _desktop->getControls(), type); + g_object_set(_canvas_item, + "anchor", anchor, + "filled", TRUE, "fill_color", _cset.normal.fill, + "stroked", TRUE, "stroke_color", _cset.normal.stroke, + "mode", SP_CTRL_MODE_XOR, NULL); + _commonInit(); +} + +ControlPoint::~ControlPoint() +{ + // avoid storing invalid points in mouseovered_point + if (this == mouseovered_point) { + _clearMouseover(); + } + + g_signal_handler_disconnect(G_OBJECT(_canvas_item), _event_handler_connection); + //sp_canvas_item_hide(_canvas_item); + sp_canvas_item_destroy(_canvas_item); +} + +void ControlPoint::_commonInit() +{ + SP_CTRL(_canvas_item)->moveto(_position); + _event_handler_connection = g_signal_connect(G_OBJECT(_canvas_item), "event", + G_CALLBACK(_event_handler), this); +} + +void ControlPoint::setPosition(Geom::Point const &pos) +{ + _position = pos; + SP_CTRL(_canvas_item)->moveto(pos); +} + +void ControlPoint::move(Geom::Point const &pos) +{ + setPosition(pos); +} + +void ControlPoint::transform(Geom::Affine const &m) { + move(position() * m); +} + +bool ControlPoint::visible() const +{ + return sp_canvas_item_is_visible(_canvas_item); +} + +void ControlPoint::setVisible(bool v) +{ + if (v) sp_canvas_item_show(_canvas_item); + else sp_canvas_item_hide(_canvas_item); +} + +Glib::ustring ControlPoint::format_tip(char const *format, ...) +{ + va_list args; + va_start(args, format); + char *dyntip = g_strdup_vprintf(format, args); + va_end(args); + Glib::ustring ret = dyntip; + g_free(dyntip); + return ret; +} + +unsigned int ControlPoint::_size() const +{ + unsigned int ret; + g_object_get(_canvas_item, "size", &ret, NULL); + return ret; +} + +SPCtrlShapeType ControlPoint::_shape() const +{ + SPCtrlShapeType ret; + g_object_get(_canvas_item, "shape", &ret, NULL); + return ret; +} + +SPAnchorType ControlPoint::_anchor() const +{ + SPAnchorType ret; + g_object_get(_canvas_item, "anchor", &ret, NULL); + return ret; +} + +Glib::RefPtr<Gdk::Pixbuf> ControlPoint::_pixbuf() +{ + GdkPixbuf *ret; + g_object_get(_canvas_item, "pixbuf", &ret, NULL); + return Glib::wrap(ret); +} + +// Same for setters. + +void ControlPoint::_setSize(unsigned int size) +{ + g_object_set(_canvas_item, "size", size, NULL); +} + +bool ControlPoint::_setControlType(Inkscape::ControlType type) +{ + return ControlManager::getManager().setControlType(_canvas_item, type); +} + +void ControlPoint::_setAnchor(SPAnchorType anchor) +{ + g_object_set(_canvas_item, "anchor", anchor, NULL); +} + +void ControlPoint::_setPixbuf(Glib::RefPtr<Gdk::Pixbuf> p) +{ + g_object_set(_canvas_item, "pixbuf", Glib::unwrap(p), NULL); +} + +// re-routes events into the virtual function +int ControlPoint::_event_handler(SPCanvasItem */*item*/, GdkEvent *event, ControlPoint *point) +{ + if ((point == nullptr) || (point->_desktop == nullptr)) { + return FALSE; + } + return point->_eventHandler(point->_desktop->event_context, event) ? TRUE : FALSE; +} + +// main event callback, which emits all other callbacks. +bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // NOTE the static variables below are shared for all points! + // TODO handle clicks and drags from other buttons too + + if (event == nullptr) + { + return false; + } + + if (event_context == nullptr) + { + return false; + } + if (_desktop == nullptr) + { + return false; + } + if(event_context->desktop !=_desktop) + { + g_warning ("ControlPoint: desktop pointers not equal!"); + //return false; + } + // offset from the pointer hotspot to the center of the grabbed knot in desktop coords + static Geom::Point pointer_offset; + // number of last doubleclicked button + static unsigned next_release_doubleclick = 0; + _double_clicked = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + GdkEventMotion em; + SPCanvas* Ca; + switch(event->type) + { + case GDK_BUTTON_PRESS: + next_release_doubleclick = 0; + if (event->button.button == 1 && !event_context->space_panning) { + // 1st mouse button click. internally, start dragging, but do not emit signals + // or change position until drag tolerance is exceeded. + _drag_event_origin[Geom::X] = event->button.x; + _drag_event_origin[Geom::Y] = event->button.y; + pointer_offset = _position - _desktop->w2d(_drag_event_origin); + _drag_initiated = false; + // route all events to this handler + sp_canvas_item_grab(_canvas_item, _grab_event_mask, nullptr, event->button.time); + _event_grab = true; + _setState(STATE_CLICKED); + return true; + } + return _event_grab; + + case GDK_2BUTTON_PRESS: + // store the button number for next release + next_release_doubleclick = event->button.button; + return true; + + case GDK_MOTION_NOTIFY: + Ca = _desktop->canvas; + em = event->motion; + combine_motion_events(Ca, em, 0); + + if (_event_grab && ! event_context->space_panning) { + _desktop->snapindicator->remove_snaptarget(); + bool transferred = false; + if (!_drag_initiated) { + bool t = fabs(em.x - _drag_event_origin[Geom::X]) <= drag_tolerance && + fabs(em.y - _drag_event_origin[Geom::Y]) <= drag_tolerance; + if (t){ + return true; + } + + // if we are here, it means the tolerance was just exceeded. + _drag_origin = _position; + transferred = grabbed(&em); + // _drag_initiated might change during the above virtual call + if (!_drag_initiated) { + // this guarantees smooth redraws while dragging + _desktop->canvas->forceFullRedrawAfterInterruptions(5); + _drag_initiated = true; + } + } + + if (!transferred) { + // dragging in progress + Geom::Point new_pos = _desktop->w2d(event_point(event->motion)) + pointer_offset; + // the new position is passed by reference and can be changed in the handlers. + dragged(new_pos, &em); + move(new_pos); + _updateDragTip(&em); // update dragging tip after moving to new position + + _desktop->scroll_to_point(new_pos); + _desktop->set_coordinate_status(_position); + sp_event_context_snap_delay_handler(event_context, nullptr, + (gpointer) this, &event->motion, + Inkscape::UI::Tools::DelayedSnapEvent::CONTROL_POINT_HANDLER); + } + return true; + } + break; + + case GDK_BUTTON_RELEASE: + if (_event_grab && event->button.button == 1) { + // If we have any pending snap event, then invoke it now! + // (This is needed because we might not have snapped on the latest GDK_MOTION_NOTIFY event + // if the mouse speed was too high. This is inherent to the snap-delay mechanism. + // We must snap at some point in time though, and this is our last chance) + // PS: For other contexts this is handled already in sp_event_context_item_handler or + // sp_event_context_root_handler + //if (_desktop && _desktop->event_context && _desktop->event_context->_delayed_snap_event) { + if (event_context->_delayed_snap_event) { + sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event); + } + + sp_canvas_item_ungrab(_canvas_item); + _setMouseover(this, event->button.state); + _event_grab = false; + + if (_drag_initiated) { + // it is the end of a drag + _desktop->canvas->endForcedFullRedraws(); + _drag_initiated = false; + ungrabbed(&event->button); + return true; + } else { + // it is the end of a click + if (next_release_doubleclick) { + _double_clicked = true; + return doubleclicked(&event->button); + } else { + return clicked(&event->button); + } + } + } + break; + + case GDK_ENTER_NOTIFY: + _setMouseover(this, event->crossing.state); + return true; + case GDK_LEAVE_NOTIFY: + _clearMouseover(); + return true; + + case GDK_GRAB_BROKEN: + if (_event_grab && !event->grab_broken.keyboard) { + { + ungrabbed(nullptr); + if (_drag_initiated) { + _desktop->canvas->endForcedFullRedraws(); + } + } + _setState(STATE_NORMAL); + _event_grab = false; + _drag_initiated = false; + return true; + } + break; + + // update tips on modifier state change + // TODO add ESC keybinding as drag cancel + case GDK_KEY_PRESS: + switch (Inkscape::UI::Tools::get_latin_keyval(&event->key)) + { + case GDK_KEY_Escape: { + // ignore Escape if this is not a drag + if (!_drag_initiated) break; + + // temporarily disable snapping - we might snap to a different place than we were initially + sp_event_context_discard_delayed_snap_event(event_context); + SnapPreferences &snapprefs = _desktop->namedview->snap_manager.snapprefs; + bool snap_save = snapprefs.getSnapEnabledGlobally(); + snapprefs.setSnapEnabledGlobally(false); + + Geom::Point new_pos = _drag_origin; + + // make a fake event for dragging + // ASSUMPTION: dragging a point without modifiers will never prevent us from moving it + // to its original position + GdkEventMotion fake; + fake.type = GDK_MOTION_NOTIFY; + fake.window = event->key.window; + fake.send_event = event->key.send_event; + fake.time = event->key.time; + fake.x = _drag_event_origin[Geom::X]; // these two are normally not used in handlers + fake.y = _drag_event_origin[Geom::Y]; // (and shouldn't be) + fake.axes = nullptr; + fake.state = 0; // unconstrained drag + fake.is_hint = FALSE; + fake.device = nullptr; + fake.x_root = -1; // not used in handlers (and shouldn't be) + fake.y_root = -1; // can be used as a flag to check for cancelled drag + + dragged(new_pos, &fake); + + sp_canvas_item_ungrab(_canvas_item); + _clearMouseover(); // this will also reset state to normal + _desktop->canvas->endForcedFullRedraws(); + _event_grab = false; + _drag_initiated = false; + + ungrabbed(nullptr); // ungrabbed handlers can handle a NULL event + snapprefs.setSnapEnabledGlobally(snap_save); + } + return true; + case GDK_KEY_Tab: + {// Downcast from ControlPoint to TransformHandle, if possible + // This is an ugly hack; we should have the transform handle intercept the keystrokes itself + TransformHandle *th = dynamic_cast<TransformHandle*>(this); + if (th) { + th->getNextClosestPoint(false); + return true; + } + break; + } + case GDK_KEY_ISO_Left_Tab: + {// Downcast from ControlPoint to TransformHandle, if possible + // This is an ugly hack; we should have the transform handle intercept the keystrokes itself + TransformHandle *th = dynamic_cast<TransformHandle*>(this); + if (th) { + th->getNextClosestPoint(true); + return true; + } + break; + } + default: + break; + } + // Do not break here, to allow for updating tooltips and such + case GDK_KEY_RELEASE: + if (mouseovered_point != this){ + return false; + } + if (_drag_initiated) { + return true; // this prevents the tool from overwriting the drag tip + } else { + unsigned state = state_after_event(event); + if (state != event->key.state) { + // we need to return true if there was a tip available, otherwise the tool's + // handler will process this event and set the tool's message, overwriting + // the point's message + return _updateTip(state); + } + } + break; + + default: break; + } + + // do not propagate events during grab - it might cause problems + return _event_grab; +} + +void ControlPoint::_setMouseover(ControlPoint *p, unsigned state) +{ + bool visible = p->visible(); + if (visible) { // invisible points shouldn't get mouseovered + p->_setState(STATE_MOUSEOVER); + } + p->_updateTip(state); + + if (visible && mouseovered_point != p) { + mouseovered_point = p; + signal_mouseover_change.emit(mouseovered_point); + } +} + +bool ControlPoint::_updateTip(unsigned state) +{ + Glib::ustring tip = _getTip(state); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +bool ControlPoint::_updateDragTip(GdkEventMotion *event) +{ + if (!_hasDragTips()) { + return false; + } + Glib::ustring tip = _getDragTip(event); + if (!tip.empty()) { + _desktop->event_context->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, + tip.data()); + return true; + } else { + _desktop->event_context->defaultMessageContext()->clear(); + return false; + } +} + +void ControlPoint::_clearMouseover() +{ + if (mouseovered_point) { + mouseovered_point->_desktop->event_context->defaultMessageContext()->clear(); + mouseovered_point->_setState(STATE_NORMAL); + mouseovered_point = nullptr; + signal_mouseover_change.emit(mouseovered_point); + } +} + +void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) +{ + if (!_event_grab) return; + + grabbed(event); + sp_canvas_item_ungrab(prev_point->_canvas_item); + sp_canvas_item_grab(_canvas_item, _grab_event_mask, nullptr, event->time); + + if (!_drag_initiated) { + _desktop->canvas->forceFullRedrawAfterInterruptions(5); + _drag_initiated = true; + } + + prev_point->_setState(STATE_NORMAL); + _setMouseover(this, event->state); +} + +void ControlPoint::_setState(State state) +{ + ColorEntry current = {0, 0}; + ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset; + switch(state) { + case STATE_NORMAL: + current = activeCset.normal; + break; + case STATE_MOUSEOVER: + current = activeCset.mouseover; + break; + case STATE_CLICKED: + current = activeCset.clicked; + break; + }; + _setColors(current); + _state = state; +} + +void ControlPoint::_handleControlStyling() +{ + if (_canvas_item->ctrlType != CTRL_TYPE_UNKNOWN) { + ControlManager::getManager().updateItem(_canvas_item); + } +} + +void ControlPoint::_setColors(ColorEntry colors) +{ + g_object_set(_canvas_item, "fill_color", colors.fill, "stroke_color", colors.stroke, NULL); +} + +bool ControlPoint::_isLurking() +{ + return _lurking; +} + +void ControlPoint::_setLurking(bool lurking) +{ + if (lurking != _lurking) { + _lurking = lurking; + _setState(_state); // TODO refactor out common part + } +} + + +bool ControlPoint::_is_drag_cancelled(GdkEventMotion *event) +{ + return !event || event->x_root == -1; +} + +// dummy implementations for handlers + +bool ControlPoint::grabbed(GdkEventMotion * /*event*/) +{ + return false; +} + +void ControlPoint::dragged(Geom::Point &/*new_pos*/, GdkEventMotion * /*event*/) +{ +} + +void ControlPoint::ungrabbed(GdkEventButton * /*event*/) +{ +} + +bool ControlPoint::clicked(GdkEventButton * /*event*/) +{ + return false; +} + +bool ControlPoint::doubleclicked(GdkEventButton * /*event*/) +{ + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/control-point.h b/src/ui/tool/control-point.h new file mode 100644 index 0000000..b97f3cb --- /dev/null +++ b/src/ui/tool/control-point.h @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2012 Authors + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CONTROL_POINT_H +#define SEEN_UI_TOOL_CONTROL_POINT_H + +#include <gdkmm/pixbuf.h> +#include <boost/utility.hpp> +#include <cstddef> +#include <sigc++/signal.h> +#include <sigc++/trackable.h> +#include <2geom/point.h> + +#include "ui/control-types.h" +#include "display/sodipodi-ctrl.h" +#include "enums.h" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +} +} +} + +namespace Inkscape { +namespace UI { + +/** + * Draggable point, the workhorse of on-canvas editing. + * + * Control points (formerly known as knots) are graphical representations of some significant + * point in the drawing. The drawing can be changed by dragging the point and the things that are + * attached to it with the mouse. Example things that could be edited with draggable points + * are gradient stops, the place where text is attached to a path, text kerns, nodes and handles + * in a path, and many more. + * + * @par Control point event handlers + * @par + * The control point has several virtual methods which allow you to react to things that + * happen to it. The most important ones are the grabbed, dragged, ungrabbed and moved functions. + * When a drag happens, the order of calls is as follows: + * - <tt>grabbed()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - <tt>dragged()</tt> + * - ... + * - <tt>dragged()</tt> + * - <tt>ungrabbed()</tt> + * + * The control point can also respond to clicks and double clicks. On a double click, + * clicked() is called, followed by doubleclicked(). When deriving from SelectableControlPoint, + * you need to manually call the superclass version at the appropriate point in your handler. + * + * @par Which method to override? + * @par + * You might wonder which hook to use when you want to do things when the point is relocated. + * Here are some tips: + * - If the point is used to edit an object, override the move() method. + * - If the point can usually be dragged wherever you like but can optionally be constrained + * to axes or the like, add a handler for <tt>signal_dragged</tt> that modifies its new + * position argument. + * - If the point has additional canvas items tied to it (like handle lines), override + * the setPosition() method. + */ +class ControlPoint : boost::noncopyable, public sigc::trackable { +public: + + /** + * Enumeration representing the possible states of the control point, used to determine + * its appearance. + * + * @todo resolve this to be in sync with the five standard GTK states. + */ + enum State { + /** Normal state. */ + STATE_NORMAL, + + /** Mouse is hovering over the control point. */ + STATE_MOUSEOVER, + + /** First mouse button pressed over the control point. */ + STATE_CLICKED + }; + + /** + * Destructor + */ + virtual ~ControlPoint(); + + /// @name Adjust the position of the control point + /// @{ + /** Current position of the control point. */ + Geom::Point const &position() const { return _position; } + + operator Geom::Point const &() { return _position; } + + /** + * Move the control point to new position with side effects. + * This is called after each drag. Override this method if only some positions make sense + * for a control point (like a point that must always be on a path and can't modify it), + * or when moving a control point changes the positions of other points. + */ + virtual void move(Geom::Point const &pos); + + /** + * Relocate the control point without side effects. + * Overload this method only if there is an additional graphical representation + * that must be updated (like the lines that connect handles to nodes). If you override it, + * you must also call the superclass implementation of the method. + * @todo Investigate whether this method should be protected + */ + virtual void setPosition(Geom::Point const &pos); + + /** + * Apply an arbitrary affine transformation to a control point. This is used + * by ControlPointSelection, and is important for things like nodes with handles. + * The default implementation simply moves the point according to the transform. + */ + virtual void transform(Geom::Affine const &m); + /// @} + + /// @name Toggle the point's visibility + /// @{ + bool visible() const; + + /** + * Set the visibility of the control point. An invisible point is not drawn on the canvas + * and cannot receive any events. If you want to have an invisible point that can respond + * to events, use <tt>invisible_cset</tt> as its color set. + */ + virtual void setVisible(bool v); + /// @} + + /// @name Transfer grab from another event handler + /// @{ + /** + * Transfer the grab to another point. This method allows one to create a draggable point + * that should be dragged instead of the one that received the grabbed signal. + * This is used to implement dragging out handles in the new node tool, for example. + * + * This method will NOT emit the ungrab signal of @c prev_point, because this would complicate + * using it with selectable control points. If you use this method while dragging, you must emit + * the ungrab signal yourself. + * + * Note that this will break horribly if you try to transfer grab between points in different + * desktops, which doesn't make much sense anyway. + */ + void transferGrab(ControlPoint *from, GdkEventMotion *event); + /// @} + + /// @name Inspect the state of the control point + /// @{ + State state() const { return _state; } + + bool mouseovered() const { return this == mouseovered_point; } + /// @} + + /** Holds the currently mouseovered control point. */ + static ControlPoint *mouseovered_point; + + /** + * Emitted when the mouseovered point changes. The parameter is the new mouseovered point. + * When a point ceases to be mouseovered, the parameter will be NULL. + */ + static sigc::signal<void, ControlPoint*> signal_mouseover_change; + + static Glib::ustring format_tip(char const *format, ...) G_GNUC_PRINTF(1,2); + + // temporarily public, until snap delay is refactored a little + virtual bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event); + SPDesktop *const _desktop; ///< The desktop this control point resides on. + + bool doubleClicked() {return _double_clicked;} + +protected: + + struct ColorEntry { + guint32 fill; + guint32 stroke; + }; + + /** + * Color entries for each possible state. + * @todo resolve this to be in sync with the five standard GTK states. + */ + struct ColorSet { + ColorEntry normal; + ColorEntry mouseover; + ColorEntry clicked; + ColorEntry selected_normal; + ColorEntry selected_mouseover; + ColorEntry selected_clicked; + }; + + /** + * A color set which you can use to create an invisible control that can still receive events. + */ + static ColorSet invisible_cset; + + /** + * Create a regular control point. + * Derive to have constructors with a reasonable number of parameters. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param type Logical type of the control point. + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + ControlType type, + ColorSet const &cset = _default_color_set, SPCanvasGroup *group = nullptr); + + /** + * Create a control point with a pixbuf-based visual representation. + * + * @param d Desktop for this control + * @param initial_pos Initial position of the control point in desktop coordinates + * @param anchor Where is the control point rendered relative to its desktop coordinates + * @param pixbuf Pixbuf to be used as the visual representation + * @param cset Colors of the point + * @param group The canvas group the point's canvas item should be created in + */ + ControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ColorSet const &cset = _default_color_set, SPCanvasGroup *group = nullptr); + + /// @name Handle control point events in subclasses + /// @{ + /** + * Called when the user moves the point beyond the drag tolerance with the first button held + * down. + * + * @param event Motion event when drag tolerance was exceeded. + * @return true if you called transferGrab() during this method. + */ + virtual bool grabbed(GdkEventMotion *event); + + /** + * Called while dragging, but before moving the knot to new position. + * + * @param pos Old position, always equal to position() + * @param new_pos New position (after drag). This is passed as a non-const reference, + * so you can change it from the handler - that's how constrained dragging is implemented. + * @param event Motion event. + */ + virtual void dragged(Geom::Point &new_pos, GdkEventMotion *event); + + /** + * Called when the control point finishes a drag. + * + * @param event Button release event + */ + virtual void ungrabbed(GdkEventButton *event); + + /** + * Called when the control point is clicked, at mouse button release. + * Improperly implementing this method can cause the default context menu not to appear when a control + * point is right-clicked. + * + * @param event Button release event + * @return true if the click had some effect, false if it did nothing. + */ + virtual bool clicked(GdkEventButton *event); + + /** + * Called when the control point is doubleclicked, at mouse button release. + * + * @param event Button release event + */ + virtual bool doubleclicked(GdkEventButton *event); + /// @} + + /// @name Manipulate the control point's appearance in subclasses + /// @{ + + /** + * Change the state of the knot. + * Alters the appearance of the knot to match one of the states: normal, mouseover + * or clicked. + */ + virtual void _setState(State state); + + void _handleControlStyling(); + + void _setColors(ColorEntry c); + + unsigned int _size() const; + + SPCtrlShapeType _shape() const; + + SPAnchorType _anchor() const; + + Glib::RefPtr<Gdk::Pixbuf> _pixbuf(); + + void _setSize(unsigned int size); + + bool _setControlType(Inkscape::ControlType type); + + void _setAnchor(SPAnchorType anchor); + + void _setPixbuf(Glib::RefPtr<Gdk::Pixbuf>); + + /** + * Determines if the control point is not visible yet still reacting to events. + * + * @return true if non-visible, false otherwise. + */ + bool _isLurking(); + + /** + * Sets the control point to be non-visible yet still reacting to events. + * + * @param lurking true to make non-visible, false otherwise. + */ + void _setLurking(bool lurking); + + /// @} + + virtual Glib::ustring _getTip(unsigned /*state*/) const { return ""; } + + virtual Glib::ustring _getDragTip(GdkEventMotion */*event*/) const { return ""; } + + virtual bool _hasDragTips() const { return false; } + + + SPCanvasItem * _canvas_item; ///< Visual representation of the control point. + + ColorSet const &_cset; ///< Colors used to represent the point + + State _state; + + static Geom::Point const &_last_click_event_point() { return _drag_event_origin; } + + static Geom::Point const &_last_drag_origin() { return _drag_origin; } + + static bool _is_drag_cancelled(GdkEventMotion *event); + + /** Events which should be captured when a handle is being dragged. */ + static int const _grab_event_mask; + + static bool _drag_initiated; + +private: + + ControlPoint(ControlPoint const &other); + + void operator=(ControlPoint const &other); + + static int _event_handler(SPCanvasItem *item, GdkEvent *event, ControlPoint *point); + + static void _setMouseover(ControlPoint *, unsigned state); + + static void _clearMouseover(); + + bool _updateTip(unsigned state); + + bool _updateDragTip(GdkEventMotion *event); + + void _setDefaultColors(); + + void _commonInit(); + + Geom::Point _position; ///< Current position in desktop coordinates + + gulong _event_handler_connection; + + bool _lurking; + + static ColorSet _default_color_set; + + /** Stores the window point over which the cursor was during the last mouse button press. */ + static Geom::Point _drag_event_origin; + + /** Stores the desktop point from which the last drag was initiated. */ + static Geom::Point _drag_origin; + + static bool _event_grab; + + bool _double_clicked; +}; + + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/curve-drag-point.cpp b/src/ui/tool/curve-drag-point.cpp new file mode 100644 index 0000000..4e878d0 --- /dev/null +++ b/src/ui/tool/curve-drag-point.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/curve-drag-point.h" +#include <glib/gi18n.h> +#include "desktop.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" + +#include "object/sp-namedview.h" +#include "object/sp-path.h" + +namespace Inkscape { +namespace UI { + + +bool CurveDragPoint::_drags_stroke = false; +bool CurveDragPoint::_segment_was_degenerate = false; + +CurveDragPoint::CurveDragPoint(PathManipulator &pm) : + ControlPoint(pm._multi_path_manipulator._path_data.node_data.desktop, Geom::Point(), SP_ANCHOR_CENTER, + CTRL_TYPE_INVISIPOINT, + invisible_cset, pm._multi_path_manipulator._path_data.dragpoint_group), + _pm(pm) +{ + setVisible(false); +} + +bool CurveDragPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // do not process any events when the manipulator is empty + if (_pm.empty()) { + setVisible(false); + return false; + } + return ControlPoint::_eventHandler(event_context, event); +} + +bool CurveDragPoint::grabbed(GdkEventMotion */*event*/) +{ + _pm._selection.hideTransformHandles(); + NodeList::iterator second = first.next(); + + // move the handles to 1/3 the length of the segment for line segments + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + _segment_was_degenerate = true; + + // delta is a vector equal 1/3 of distance from first to second + Geom::Point delta = (second->position() - first->position()) / 3.0; + // only update the nodes if the mode is bspline + if(!_pm._isBSpline()){ + first->front()->move(first->front()->position() + delta); + second->back()->move(second->back()->position() - delta); + } + _pm.update(); + } else { + _segment_was_degenerate = false; + } + return false; +} + +void CurveDragPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + if (!first || !first.next()) return; + NodeList::iterator second = first.next(); + + // special cancel handling - retract handles when if the segment was degenerate + if (_is_drag_cancelled(event) && _segment_was_degenerate) { + first->front()->retract(); + second->back()->retract(); + _pm.update(); + return; + } + + if (_drag_initiated && !(event->state & GDK_SHIFT_MASK)) { + SnapManager &m = _desktop->namedview->snap_manager; + SPItem *path = static_cast<SPItem *>(_pm._path); + m.setup(_desktop, true, path); // We will not try to snap to "path" itself + Inkscape::SnapCandidatePoint scp(new_pos, Inkscape::SNAPSOURCE_OTHER_HANDLE); + Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), false); + new_pos = sp.getPoint(); + m.unSetup(); + } + + // Magic Bezier Drag Equations follow! + // "weight" describes how the influence of the drag should be distributed + // among the handles; 0 = front handle only, 1 = back handle only. + double weight, t = _t; + if (t <= 1.0 / 6.0) weight = 0; + else if (t <= 0.5) weight = (pow((6 * t - 1) / 2.0, 3)) / 2; + else if (t <= 5.0 / 6.0) weight = (1 - pow((6 * (1-t) - 1) / 2.0, 3)) / 2 + 0.5; + else weight = 1; + + Geom::Point delta = new_pos - position(); + Geom::Point offset0 = ((1-weight)/(3*t*(1-t)*(1-t))) * delta; + Geom::Point offset1 = (weight/(3*t*t*(1-t))) * delta; + + //modified so that, if the trace is bspline, it only acts if the SHIFT key is pressed + if(!_pm._isBSpline()){ + first->front()->move(first->front()->position() + offset0); + second->back()->move(second->back()->position() + offset1); + }else if(weight>=0.8){ + if(held_shift(*event)){ + second->back()->move(new_pos); + } else { + second->move(second->position() + delta); + } + }else if(weight<=0.2){ + if(held_shift(*event)){ + first->back()->move(new_pos); + } else { + first->move(first->position() + delta); + } + }else{ + first->move(first->position() + delta); + second->move(second->position() + delta); + } + _pm.update(); +} + +void CurveDragPoint::ungrabbed(GdkEventButton *) +{ + _pm._updateDragPoint(_desktop->d2w(position())); + _pm._commit(_("Drag curve")); + _pm._selection.restoreTransformHandles(); +} + +bool CurveDragPoint::clicked(GdkEventButton *event) +{ + // This check is probably redundant + if (!first || event->button != 1) return false; + // the next iterator can be invalid if we click very near the end of path + NodeList::iterator second = first.next(); + if (!second) return false; + + // insert nodes on Ctrl+Alt+click + if (held_control(*event) && held_alt(*event)) { + _insertNode(false); + return true; + } + + if (held_shift(*event)) { + // if both nodes of the segment are selected, deselect; + // otherwise add to selection + if (first->selected() && second->selected()) { + _pm._selection.erase(first.ptr()); + _pm._selection.erase(second.ptr()); + } else { + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + } + } else { + // without Shift, take selection + _pm._selection.clear(); + _pm._selection.insert(first.ptr()); + _pm._selection.insert(second.ptr()); + } + return true; +} + +bool CurveDragPoint::doubleclicked(GdkEventButton *event) +{ + if (event->button != 1 || !first || !first.next()) return false; + _insertNode(true); + return true; +} + +void CurveDragPoint::_insertNode(bool take_selection) +{ + // The purpose of this call is to make way for the just created node. + // Otherwise clicks on the new node would only work after the user moves the mouse a bit. + // PathManipulator will restore visibility when necessary. + setVisible(false); + + _pm.insertNode(first, _t, take_selection); +} + +Glib::ustring CurveDragPoint::_getTip(unsigned state) const +{ + if (_pm.empty()) return ""; + if (!first || !first.next()) return ""; + bool linear = first->front()->isDegenerate() && first.next()->back()->isDegenerate(); + if(state_held_shift(state) && _pm._isBSpline()){ + return C_("Path segment tip", + "<b>Shift</b>: drag to open or move BSpline handles"); + } + if (state_held_shift(state)) { + return C_("Path segment tip", + "<b>Shift</b>: click to toggle segment selection"); + } + if (state_held_control(state) && state_held_alt(state)) { + return C_("Path segment tip", + "<b>Ctrl+Alt</b>: click to insert a node"); + } + if(_pm._isBSpline()){ + return C_("Path segment tip", + "<b>BSpline segment</b>: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } + if (linear) { + return C_("Path segment tip", + "<b>Linear segment</b>: drag to convert to a Bezier segment, " + "doubleclick to insert node, click to select (more: Shift, Ctrl+Alt)"); + } else { + return C_("Path segment tip", + "<b>Bezier segment</b>: drag to shape the segment, doubleclick to insert node, " + "click to select (more: Shift, Ctrl+Alt)"); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/curve-drag-point.h b/src/ui/tool/curve-drag-point.h new file mode 100644 index 0000000..bfe0ad7 --- /dev/null +++ b/src/ui/tool/curve-drag-point.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_CURVE_DRAG_POINT_H +#define SEEN_UI_TOOL_CURVE_DRAG_POINT_H + +#include "ui/tool/control-point.h" +#include "ui/tool/node.h" + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class PathManipulator; +struct PathSharedData; + +// This point should be invisible to the user - use the invisible_cset from control-point.h +// TODO make some methods from path-manipulator.cpp public so that this point doesn't have +// to be declared as a friend +/** + * An invisible point used to drag curves. This point is used by PathManipulator to allow editing + * of path segments by dragging them. It is defined in a separate file so that the node tool + * can check if the mouseovered control point is a curve drag point and update the cursor + * accordingly, without the need to drag in the full PathManipulator header. + */ +class CurveDragPoint : public ControlPoint { +public: + + CurveDragPoint(PathManipulator &pm); + void setSize(double sz) { _setSize(sz); } + void setTimeValue(double t) { _t = t; } + double getTimeValue() { return _t; } + void setIterator(NodeList::iterator i) { first = i; } + NodeList::iterator getIterator() { return first; } + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + +protected: + + Glib::ustring _getTip(unsigned state) const override; + void dragged(Geom::Point &, GdkEventMotion *) override; + bool grabbed(GdkEventMotion *) override; + void ungrabbed(GdkEventButton *) override; + bool clicked(GdkEventButton *) override; + bool doubleclicked(GdkEventButton *) override; + +private: + double _t; + PathManipulator &_pm; + NodeList::iterator first; + + static bool _drags_stroke; + static bool _segment_was_degenerate; + static Geom::Point _stroke_drag_origin; + void _insertNode(bool take_selection); +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp new file mode 100644 index 0000000..33a196d --- /dev/null +++ b/src/ui/tool/event-utils.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> +#include "display/sp-canvas.h" +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + + +guint shortcut_key(GdkEventKey const &event) +{ + guint shortcut_key = 0; + gdk_keymap_translate_keyboard_state( + Gdk::Display::get_default()->get_keymap(), + event.hardware_keycode, + (GdkModifierType) event.state, + 0 /*event->key.group*/, + &shortcut_key, nullptr, nullptr, nullptr); + return shortcut_key; +} + +unsigned combine_key_events(guint keyval, gint mask) +{ + GdkEvent *event_next; + gint i = 0; + + event_next = gdk_event_get(); + // while the next event is also a key notify with the same keyval and mask, + while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type == GDK_KEY_RELEASE) + && event_next->key.keyval == keyval + && (!mask || event_next->key.state & mask)) { + if (event_next->type == GDK_KEY_PRESS) + i ++; + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + } + // otherwise, put it back onto the queue + if (event_next) gdk_event_put(event_next); + + return i; +} + +unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask) +{ + if (canvas == nullptr) { + return false; + } + GdkEvent *event_next; + gint i = 0; + event.x -= canvas->_x0; + event.y -= canvas->_y0; + + event_next = gdk_event_get(); + // while the next event is also a motion notify + while (event_next && (event_next->type == GDK_MOTION_NOTIFY) + && (!mask || event_next->motion.state & mask)) + { + if (event_next->motion.device == event.device) { + GdkEventMotion &next = event_next->motion; + event.send_event = next.send_event; + event.time = next.time; + event.x = next.x; + event.y = next.y; + event.state = next.state; + event.is_hint = next.is_hint; + event.x_root = next.x_root; + event.y_root = next.y_root; + if (event.axes && next.axes) { + memcpy(event.axes, next.axes, gdk_device_get_n_axes(event.device)); + } + } + + // kill it + gdk_event_free(event_next); + event_next = gdk_event_get(); + i++; + } + // otherwise, put it back onto the queue + if (event_next) { + gdk_event_put(event_next); + } + event.x += canvas->_x0; + event.y += canvas->_y0; + + return i; +} + +/** Returns the modifier state valid after this event. Use this when you process events + * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */ +unsigned state_after_event(GdkEvent *event) +{ + unsigned state = 0; + switch (event->type) { + case GDK_KEY_PRESS: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + state |= GDK_SHIFT_MASK; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + state |= GDK_CONTROL_MASK; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + state |= GDK_MOD1_MASK; + break; + default: break; + } + break; + case GDK_KEY_RELEASE: + state = event->key.state; + switch(shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + state &= ~GDK_SHIFT_MASK; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + state &= ~GDK_CONTROL_MASK; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + state &= ~GDK_MOD1_MASK; + break; + default: break; + } + break; + default: break; + } + return state; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/event-utils.h b/src/ui/tool/event-utils.h new file mode 100644 index 0000000..3fd8f16 --- /dev/null +++ b/src/ui/tool/event-utils.h @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Collection of shorthands to deal with GDK events. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_EVENT_UTILS_H +#define SEEN_UI_TOOL_EVENT_UTILS_H + +#include <gdk/gdk.h> +#include <2geom/point.h> + +struct SPCanvas; + +namespace Inkscape { +namespace UI { + +inline bool state_held_shift(unsigned state) { + return state & GDK_SHIFT_MASK; +} +inline bool state_held_control(unsigned state) { + return state & GDK_CONTROL_MASK; +} +inline bool state_held_alt(unsigned state) { + return state & GDK_MOD1_MASK; +} +inline bool state_held_only_shift(unsigned state) { + return (state & GDK_SHIFT_MASK) && !(state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_control(unsigned state) { + return (state & GDK_CONTROL_MASK) && !(state & (GDK_SHIFT_MASK | GDK_MOD1_MASK)); +} +inline bool state_held_only_alt(unsigned state) { + return (state & GDK_MOD1_MASK) && !(state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)); +} +inline bool state_held_any_modifiers(unsigned state) { + return state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK); +} +inline bool state_held_no_modifiers(unsigned state) { + return !state_held_any_modifiers(state); +} +template <unsigned button> +inline bool state_held_button(unsigned state) { + return (button == 0 || button > 5) ? false : state & (GDK_BUTTON1_MASK << (button-1)); +} + + +/** Checks whether Shift was held when the event was generated. */ +template <typename E> +inline bool held_shift(E const &event) { + return state_held_shift(event.state); +} + +/** Checks whether Control was held when the event was generated. */ +template <typename E> +inline bool held_control(E const &event) { + return state_held_control(event.state); +} + +/** Checks whether Alt was held when the event was generated. */ +template <typename E> +inline bool held_alt(E const &event) { + return state_held_alt(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Ctrl was held when the event + * was generated. */ +template <typename E> +inline bool held_only_control(E const &event) { + return state_held_only_control(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Shift was held when the event + * was generated. */ +template <typename E> +inline bool held_only_shift(E const &event) { + return state_held_only_shift(event.state); +} + +/** True if from the set of Ctrl, Shift and Alt only Alt was held when the event + * was generated. */ +template <typename E> +inline bool held_only_alt(E const &event) { + return state_held_only_alt(event.state); +} + +template <typename E> +inline bool held_no_modifiers(E const &event) { + return state_held_no_modifiers(event.state); +} + +template <typename E> +inline bool held_any_modifiers(E const &event) { + return state_held_any_modifiers(event.state); +} + +template <typename E> +inline Geom::Point event_point(E const &event) { + return Geom::Point(event.x, event.y); +} + +/** Use like this: + * @code if (held_button<2>(event->motion)) { ... @endcode */ +template <unsigned button, typename E> +inline bool held_button(E const &event) { + return state_held_button<button>(event.state); +} + +guint shortcut_key(GdkEventKey const &event); +unsigned combine_key_events(guint keyval, gint mask); +unsigned combine_motion_events(SPCanvas *canvas, GdkEventMotion &event, gint mask); +unsigned state_after_event(GdkEvent *event); + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/manipulator.cpp b/src/ui/tool/manipulator.cpp new file mode 100644 index 0000000..a68de5e --- /dev/null +++ b/src/ui/tool/manipulator.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manipulator base class and manipulator group - implementation + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#include "ui/tool/node.h" +//#include "ui/tool/manipulator.h" + +namespace Inkscape { +namespace UI { + +/* +void Manipulator::_grabEvents() +{ + if (_group) _group->_grabEvents(boost::shared_ptr<Manipulator>(this)); +} +void Manipulator::_ungrabEvents() +{ + if (_group) _group->_ungrabEvents(boost::shared_ptr<Manipulator>(this)); +} + +ManipulatorGroup::ManipulatorGroup(SPDesktop *d) : + _desktop(d) +{ +} +ManipulatorGroup::~ManipulatorGroup() +{ +} + +void ManipulatorGroup::_grabEvents(boost::shared_ptr<Manipulator> m) +{ + if (!_grab) _grab = m; +} +void ManipulatorGroup::_ungrabEvents(boost::shared_ptr<Manipulator> m) +{ + if (_grab == m) _grab.reset(); +} + +void ManipulatorGroup::add(boost::shared_ptr<Manipulator> m) +{ + m->_group = this; + push_back(m); +} +void ManipulatorGroup::remove(boost::shared_ptr<Manipulator> m) +{ + for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) { + if ((*i) == m) { + erase(i); + break; + } + } + m->_group = 0; +} + +void ManipulatorGroup::clear() +{ + std::list<boost::shared_ptr<Manipulator> >::clear(); +} + +bool ManipulatorGroup::event(GdkEvent *event) +{ + if (_grab) { + return _grab->event(event); + } + + for (std::list<boost::shared_ptr<Manipulator> >::iterator i = begin(); i != end(); ++i) { + if ((*i)->event(event) || _grab) return true; + } + return false; +}*/ + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/manipulator.h b/src/ui/tool/manipulator.h new file mode 100644 index 0000000..308ad1c --- /dev/null +++ b/src/ui/tool/manipulator.h @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Manipulator - edits something on-canvas + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MANIPULATOR_H +#define SEEN_UI_TOOL_MANIPULATOR_H + +#include <set> +#include <map> +#include <cstddef> +#include <sigc++/sigc++.h> +#include <glib.h> +#include <gdk/gdk.h> +#include "ui/tools/tool-base.h" + +class SPDesktop; +namespace Inkscape { +namespace UI { + +class ManipulatorGroup; +class ControlPointSelection; + +/** + * @brief Tool component that processes events and does something in response to them. + * Note: this class is probably redundant. + */ +class Manipulator { +friend class ManipulatorGroup; +public: + Manipulator(SPDesktop *d) + : _desktop(d) + {} + virtual ~Manipulator() = default; + + /// Handle input event. Returns true if handled. + virtual bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *)=0; + SPDesktop *const _desktop; +}; + +/** + * @brief Tool component that edits something on the canvas using selectable control points. + * Note: this class is probably redundant. + */ +class PointManipulator : public Manipulator, public sigc::trackable { +public: + PointManipulator(SPDesktop *d, ControlPointSelection &sel) + : Manipulator(d) + , _selection(sel) + {} + + /// Type of extremum points to add in PathManipulator::insertNodeAtExtremum + enum ExtremumType { + EXTR_MIN_X = 0, + EXTR_MAX_X, + EXTR_MIN_Y, + EXTR_MAX_Y + }; +protected: + ControlPointSelection &_selection; +}; + +/** Manipulator that aggregates several manipulators of the same type. + * The order of invoking events on the member manipulators is undefined. + * To make this class more useful, derive from it and add actions that can be performed + * on all manipulators in the set. + * + * This is not used at the moment and is probably useless. */ +template <typename T> +class MultiManipulator : public PointManipulator { +public: + //typedef typename T::ItemType ItemType; + typedef typename std::pair<void*, std::shared_ptr<T> > MapPair; + typedef typename std::map<void*, std::shared_ptr<T> > MapType; + + MultiManipulator(SPDesktop *d, ControlPointSelection &sel) + : PointManipulator(d, sel) + {} + void addItem(void *item) { + std::shared_ptr<T> m(_createManipulator(item)); + _mmap.insert(MapPair(item, m)); + } + void removeItem(void *item) { + _mmap.erase(item); + } + void clear() { + _mmap.clear(); + } + bool contains(void *item) { + return _mmap.find(item) != _mmap.end(); + } + bool empty() { + return _mmap.empty(); + } + + void setItems(std::vector<gpointer> list) { // this function is not called anywhere ... delete ? + std::set<void*> to_remove; + for (typename MapType::iterator mi = _mmap.begin(); mi != _mmap.end(); ++mi) { + to_remove.insert(mi->first); + } + for (auto i:list) { + if (_isItemType(i)) { + // erase returns the number of items removed + // if nothing was removed, it means this item did not have a manipulator - add it + if (!to_remove.erase(i)) addItem(i); + } + } + for (auto ri : to_remove) { + removeItem(ri); + } + } + + /** Invoke a method on all managed manipulators. + * Example: + * @code m.invokeForAll(&SomeManipulator::someMethod); @endcode + */ + template <typename R> + void invokeForAll(R (T::*method)()) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A), A a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (T::*method)(A const &), A const &a) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (T::*method)(A,B), A a, B b) { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + ((i->second.get())->*method)(a, b); + } + } + + bool event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override { + for (typename MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + if ((*i).second->event(event_context, event)) return true; + } + return false; + } +protected: + virtual T *_createManipulator(void *item) = 0; + virtual bool _isItemType(void *item) = 0; + MapType _mmap; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/modifier-tracker.cpp b/src/ui/tool/modifier-tracker.cpp new file mode 100644 index 0000000..70c85a6 --- /dev/null +++ b/src/ui/tool/modifier-tracker.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include "ui/tool/event-utils.h" +#include "ui/tool/modifier-tracker.h" + +namespace Inkscape { +namespace UI { + +ModifierTracker::ModifierTracker() + : _left_shift(false) + , _right_shift(false) + , _left_ctrl(false) + , _right_ctrl(false) + , _left_alt(false) + , _right_alt(false) +{} + +bool ModifierTracker::event(GdkEvent *event) +{ + switch (event->type) { + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + _left_shift = true; + break; + case GDK_KEY_Shift_R: + _right_shift = true; + break; + case GDK_KEY_Control_L: + _left_ctrl = true; + break; + case GDK_KEY_Control_R: + _right_ctrl = true; + break; + case GDK_KEY_Alt_L: + _left_alt = true; + break; + case GDK_KEY_Alt_R: + _right_alt = true; + break; + } + break; + case GDK_KEY_RELEASE: + switch (shortcut_key(event->key)) { + case GDK_KEY_Shift_L: + _left_shift = false; + break; + case GDK_KEY_Shift_R: + _right_shift = false; + break; + case GDK_KEY_Control_L: + _left_ctrl = false; + break; + case GDK_KEY_Control_R: + _right_ctrl = false; + break; + case GDK_KEY_Alt_L: + _left_alt = false; + break; + case GDK_KEY_Alt_R: + _right_alt = false; + break; + } + break; + default: break; + } + + return false; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/modifier-tracker.h b/src/ui/tool/modifier-tracker.h new file mode 100644 index 0000000..c5762e5 --- /dev/null +++ b/src/ui/tool/modifier-tracker.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Fine-grained modifier tracker for event handling. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MODIFIER_TRACKER_H +#define SEEN_UI_TOOL_MODIFIER_TRACKER_H + +#include <gdk/gdk.h> + +namespace Inkscape { +namespace UI { + +class ModifierTracker { +public: + ModifierTracker(); + bool event(GdkEvent *); + + bool leftShift() const { return _left_shift; } + bool rightShift() const { return _right_shift; } + bool leftControl() const { return _left_ctrl; } + bool rightControl() const { return _right_ctrl; } + bool leftAlt() const { return _left_alt; } + bool rightAlt() const { return _right_alt; } + +private: + bool _left_shift; + bool _right_shift; + bool _left_ctrl; + bool _right_ctrl; + bool _left_alt; + bool _right_alt; +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_UI_TOOL_MODIFIER_TRACKER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/multi-path-manipulator.cpp b/src/ui/tool/multi-path-manipulator.cpp new file mode 100644 index 0000000..bd84cc1 --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.cpp @@ -0,0 +1,888 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Multi path manipulator - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <unordered_set> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "message-stack.h" +#include "node.h" +#include "verbs.h" + +#include "live_effects/lpeobject.h" + +#include "object/sp-path.h" + +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" + + +namespace Inkscape { +namespace UI { + +namespace { + +struct hash_nodelist_iterator + : public std::unary_function<NodeList::iterator, std::size_t> +{ + std::size_t operator()(NodeList::iterator i) const { + return std::hash<NodeList::iterator::pointer>()(&*i); + } +}; + +typedef std::pair<NodeList::iterator, NodeList::iterator> IterPair; +typedef std::vector<IterPair> IterPairList; +typedef std::unordered_set<NodeList::iterator, hash_nodelist_iterator> IterSet; +typedef std::multimap<double, IterPair> DistanceMap; +typedef std::pair<double, IterPair> DistanceMapItem; + +/** Find pairs of selected endnodes suitable for joining. */ +void find_join_iterators(ControlPointSelection &sel, IterPairList &pairs) +{ + IterSet join_iters; + + // find all endnodes in selection + for (auto i : sel) { + Node *node = dynamic_cast<Node*>(i); + if (!node) continue; + NodeList::iterator iter = NodeList::get_iterator(node); + if (!iter.next() || !iter.prev()) join_iters.insert(iter); + } + + if (join_iters.size() < 2) return; + + // Below we find the closest pairs. The algorithm is O(N^3). + // We can go down to O(N^2 log N) by using O(N^2) memory, by putting all pairs + // with their distances in a multimap (not worth it IMO). + while (join_iters.size() >= 2) { + double closest = DBL_MAX; + IterPair closest_pair; + for (IterSet::iterator i = join_iters.begin(); i != join_iters.end(); ++i) { + for (IterSet::iterator j = join_iters.begin(); j != i; ++j) { + double dist = Geom::distance(**i, **j); + if (dist < closest) { + closest = dist; + closest_pair = std::make_pair(*i, *j); + } + } + } + pairs.push_back(closest_pair); + join_iters.erase(closest_pair.first); + join_iters.erase(closest_pair.second); + } +} + +/** After this function, first should be at the end of path and second at the beginnning. + * @returns True if the nodes are in the same subpath */ +bool prepare_join(IterPair &join_iters) +{ + if (&NodeList::get(join_iters.first) == &NodeList::get(join_iters.second)) { + if (join_iters.first.next()) // if first is begin, swap the iterators + std::swap(join_iters.first, join_iters.second); + return true; + } + + NodeList &sp_first = NodeList::get(join_iters.first); + NodeList &sp_second = NodeList::get(join_iters.second); + if (join_iters.first.next()) { // first is begin + if (join_iters.second.next()) { // second is begin + sp_first.reverse(); + } else { // second is end + std::swap(join_iters.first, join_iters.second); + } + } else { // first is end + if (join_iters.second.next()) { // second is begin + // do nothing + } else { // second is end + sp_second.reverse(); + } + } + return false; +} +} // anonymous namespace + + +MultiPathManipulator::MultiPathManipulator(PathSharedData &data, sigc::connection &chg) + : PointManipulator(data.node_data.desktop, *data.node_data.selection) + , _path_data(data) + , _changed(chg) +{ + _selection.signal_commit.connect( + sigc::mem_fun(*this, &MultiPathManipulator::_commit)); + _selection.signal_selection_changed.connect( + sigc::hide( sigc::hide( + signal_coords_changed.make_slot()))); +} + +MultiPathManipulator::~MultiPathManipulator() +{ + _mmap.clear(); +} + +/** Remove empty manipulators. */ +void MultiPathManipulator::cleanup() +{ + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + if (i->second->empty()) i = _mmap.erase(i); + else ++i; + } +} + +/** + * Change the set of items to edit. + * + * This method attempts to preserve as much of the state as possible. + */ +void MultiPathManipulator::setItems(std::set<ShapeRecord> const &s) +{ + std::set<ShapeRecord> shapes(s); + + // iterate over currently edited items, modifying / removing them as necessary + for (MapType::iterator i = _mmap.begin(); i != _mmap.end();) { + std::set<ShapeRecord>::iterator si = shapes.find(i->first); + if (si == shapes.end()) { + // This item is no longer supposed to be edited - remove its manipulator + i = _mmap.erase(i); + } else { + ShapeRecord const &sr = i->first; + ShapeRecord const &sr_new = *si; + // if the shape record differs, replace the key only and modify other values + if (sr.edit_transform != sr_new.edit_transform || + sr.role != sr_new.role) + { + std::shared_ptr<PathManipulator> hold(i->second); + if (sr.edit_transform != sr_new.edit_transform) + hold->setControlsTransform(sr_new.edit_transform); + if (sr.role != sr_new.role) { + //hold->setOutlineColor(_getOutlineColor(sr_new.role)); + } + i = _mmap.erase(i); + _mmap.insert(std::make_pair(sr_new, hold)); + } else { + ++i; + } + shapes.erase(si); // remove the processed record + } + } + + // add newly selected items + for (const auto & r : shapes) { + LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(r.object); + if (!SP_IS_PATH(r.object) && !lpobj) continue; + std::shared_ptr<PathManipulator> newpm(new PathManipulator(*this, (SPPath*) r.object, + r.edit_transform, _getOutlineColor(r.role, r.object), r.lpe_key)); + newpm->showHandles(_show_handles); + // always show outlines for clips and masks + newpm->showOutline(_show_outline || r.role != SHAPE_ROLE_NORMAL); + newpm->showPathDirection(_show_path_direction); + newpm->setLiveOutline(_live_outline); + newpm->setLiveObjects(_live_objects); + _mmap.insert(std::make_pair(r, newpm)); + } +} + +void MultiPathManipulator::selectSubpaths() +{ + if (_selection.empty()) { + _selection.selectAll(); + } else { + invokeForAll(&PathManipulator::selectSubpaths); + } +} + +void MultiPathManipulator::shiftSelection(int dir) +{ + if (empty()) return; + + // 1. find last selected node + // 2. select the next node; if the last node or nothing is selected, + // select first node + MapType::iterator last_i; + SubpathList::iterator last_j; + NodeList::iterator last_k; + bool anything_found = false; + bool anynode_found = false; + + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + SubpathList &sp = i->second->subpathList(); + for (SubpathList::iterator j = sp.begin(); j != sp.end(); ++j) { + anynode_found = true; + for (NodeList::iterator k = (*j)->begin(); k != (*j)->end(); ++k) { + if (k->selected()) { + last_i = i; + last_j = j; + last_k = k; + anything_found = true; + // when tabbing backwards, we want the first node + if (dir == -1) goto exit_loop; + } + } + } + } + exit_loop: + + // NOTE: we should not assume the _selection contains only nodes + // in future it might also contain handles and other types of control points + // this is why we use a flag instead in the loop above, instead of calling + // selection.empty() + if (!anything_found) { + // select first / last node + // this should never fail because there must be at least 1 non-empty manipulator + if (anynode_found) { + if (dir == 1) { + _selection.insert((*_mmap.begin()->second->subpathList().begin())->begin().ptr()); + } else { + _selection.insert((--(*--(--_mmap.end())->second->subpathList().end())->end()).ptr()); + } + } + return; + } + + // three levels deep - w00t! + if (dir == 1) { + if (++last_k == (*last_j)->end()) { + // here, last_k points to the node to be selected + ++last_j; + if (last_j == last_i->second->subpathList().end()) { + ++last_i; + if (last_i == _mmap.end()) { + last_i = _mmap.begin(); + } + last_j = last_i->second->subpathList().begin(); + } + last_k = (*last_j)->begin(); + } + } else { + if (!last_k || last_k == (*last_j)->begin()) { + if (last_j == last_i->second->subpathList().begin()) { + if (last_i == _mmap.begin()) { + last_i = _mmap.end(); + } + --last_i; + last_j = last_i->second->subpathList().end(); + } + --last_j; + last_k = (*last_j)->end(); + } + --last_k; + } + _selection.clear(); + _selection.insert(last_k.ptr()); +} + +void MultiPathManipulator::invertSelectionInSubpaths() +{ + invokeForAll(&PathManipulator::invertSelectionInSubpaths); +} + +void MultiPathManipulator::setNodeType(NodeType type) +{ + if (_selection.empty()) return; + + // When all selected nodes are already cusp, retract their handles + bool retract_handles = (type == NODE_CUSP); + + for (auto i : _selection) { + Node *node = dynamic_cast<Node*>(i); + if (node) { + retract_handles &= (node->type() == NODE_CUSP); + node->setType(type); + } + } + + if (retract_handles) { + for (auto i : _selection) { + Node *node = dynamic_cast<Node*>(i); + if (node) { + node->front()->retract(); + node->back()->retract(); + } + } + } + + _done(retract_handles ? _("Retract handles") : _("Change node type")); +} + +void MultiPathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::setSegmentType, type); + if (type == SEGMENT_STRAIGHT) { + _done(_("Straighten segments")); + } else { + _done(_("Make segments curves")); + } +} + +void MultiPathManipulator::insertNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::insertNodes); + _done(_("Add nodes")); +} +void MultiPathManipulator::insertNodesAtExtrema(ExtremumType extremum) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::insertNodeAtExtremum, extremum); + _done(_("Add extremum nodes")); +} + +void MultiPathManipulator::insertNode(Geom::Point pt) +{ + // When double clicking to insert nodes, we might not have a selection of nodes (and we don't need one) + // so don't check for "_selection.empty()" here, contrary to the other methods above and below this one + invokeForAll(&PathManipulator::insertNode, pt); + _done(_("Add nodes")); +} + +void MultiPathManipulator::duplicateNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::duplicateNodes); + _done(_("Duplicate nodes")); +} + +void MultiPathManipulator::joinNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::hideDragPoint); + // Node join has two parts. In the first one we join two subpaths by fusing endpoints + // into one. In the second we fuse nodes in each subpath. + IterPairList joins; + NodeList::iterator preserve_pos; + Node *mouseover_node = dynamic_cast<Node*>(ControlPoint::mouseovered_point); + if (mouseover_node) { + preserve_pos = NodeList::get_iterator(mouseover_node); + } + find_join_iterators(_selection, joins); + + for (auto & join : joins) { + bool same_path = prepare_join(join); + NodeList &sp_first = NodeList::get(join.first); + NodeList &sp_second = NodeList::get(join.second); + join.first->setType(NODE_CUSP, false); + + Geom::Point joined_pos, pos_handle_front, pos_handle_back; + pos_handle_front = *join.second->front(); + pos_handle_back = *join.first->back(); + + // When we encounter the mouseover node, we unset the iterator - it will be invalidated + if (join.first == preserve_pos) { + joined_pos = *join.first; + preserve_pos = NodeList::iterator(); + } else if (join.second == preserve_pos) { + joined_pos = *join.second; + preserve_pos = NodeList::iterator(); + } else { + joined_pos = Geom::middle_point(*join.first, *join.second); + } + + // if the handles aren't degenerate, don't move them + join.first->move(joined_pos); + Node *joined_node = join.first.ptr(); + if (!join.second->front()->isDegenerate()) { + joined_node->front()->setPosition(pos_handle_front); + } + if (!join.first->back()->isDegenerate()) { + joined_node->back()->setPosition(pos_handle_back); + } + sp_second.erase(join.second); + + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + _selection.insert(join.first.ptr()); + } + + if (joins.empty()) { + // Second part replaces contiguous selections of nodes with single nodes + invokeForAll(&PathManipulator::weldNodes, preserve_pos); + } + + _doneWithCleanup(_("Join nodes"), true); +} + +void MultiPathManipulator::breakNodes() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::breakNodes); + _done(_("Break nodes"), true); +} + +void MultiPathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteNodes, keep_shape); + _doneWithCleanup(_("Delete nodes"), true); +} + +/** Join selected endpoints to create segments. */ +void MultiPathManipulator::joinSegments() +{ + if (_selection.empty()) return; + IterPairList joins; + find_join_iterators(_selection, joins); + + for (auto & join : joins) { + bool same_path = prepare_join(join); + NodeList &sp_first = NodeList::get(join.first); + NodeList &sp_second = NodeList::get(join.second); + join.first->setType(NODE_CUSP, false); + join.second->setType(NODE_CUSP, false); + if (same_path) { + sp_first.setClosed(true); + } else { + sp_first.splice(sp_first.end(), sp_second); + sp_second.kill(); + } + } + + if (joins.empty()) { + invokeForAll(&PathManipulator::weldSegments); + } + _doneWithCleanup("Join segments", true); +} + +void MultiPathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + invokeForAll(&PathManipulator::deleteSegments); + _doneWithCleanup("Delete segments", true); +} + +void MultiPathManipulator::alignNodes(Geom::Dim2 d) +{ + if (_selection.empty()) return; + _selection.align(d); + if (d == Geom::X) { + _done("Align nodes to a horizontal line"); + } else { + _done("Align nodes to a vertical line"); + } +} + +void MultiPathManipulator::distributeNodes(Geom::Dim2 d) +{ + if (_selection.empty()) return; + _selection.distribute(d); + if (d == Geom::X) { + _done("Distribute nodes horizontally"); + } else { + _done("Distribute nodes vertically"); + } +} + +void MultiPathManipulator::reverseSubpaths() +{ + if (_selection.empty()) { + invokeForAll(&PathManipulator::reverseSubpaths, false); + _done("Reverse subpaths"); + } else { + invokeForAll(&PathManipulator::reverseSubpaths, true); + _done("Reverse selected subpaths"); + } +} + +void MultiPathManipulator::move(Geom::Point const &delta) +{ + if (_selection.empty()) return; + _selection.transform(Geom::Translate(delta)); + _done("Move nodes"); +} + +void MultiPathManipulator::showOutline(bool show) +{ + for (auto & i : _mmap) { + // always show outlines for clipping paths and masks + i.second->showOutline(show || i.first.role != SHAPE_ROLE_NORMAL); + } + _show_outline = show; +} + +void MultiPathManipulator::showHandles(bool show) +{ + invokeForAll(&PathManipulator::showHandles, show); + _show_handles = show; +} + +void MultiPathManipulator::showPathDirection(bool show) +{ + invokeForAll(&PathManipulator::showPathDirection, show); + _show_path_direction = show; +} + +/** + * Set live outline update status. + * When set to true, outline will be updated continuously when dragging + * or transforming nodes. Otherwise it will only update when changes are committed + * to XML. + */ +void MultiPathManipulator::setLiveOutline(bool set) +{ + invokeForAll(&PathManipulator::setLiveOutline, set); + _live_outline = set; +} + +/** + * Set live object update status. + * When set to true, objects will be updated continuously when dragging + * or transforming nodes. Otherwise they will only update when changes are committed + * to XML. + */ +void MultiPathManipulator::setLiveObjects(bool set) +{ + invokeForAll(&PathManipulator::setLiveObjects, set); + _live_objects = set; +} + +void MultiPathManipulator::updateOutlineColors() +{ + //for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ++i) { + // i->second->setOutlineColor(_getOutlineColor(i->first.role)); + //} +} + +void MultiPathManipulator::updateHandles() +{ + invokeForAll(&PathManipulator::updateHandles); +} + +bool MultiPathManipulator::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + _tracker.event(event); + guint key = 0; + if (event->type == GDK_KEY_PRESS) { + key = shortcut_key(event->key); + } + + // Single handle adjustments go here. + if (_selection.size() == 1 && event->type == GDK_KEY_PRESS) { + do { + Node *n = dynamic_cast<Node *>(*_selection.begin()); + if (!n) break; + + PathManipulator &pm = n->nodeList().subpathList().pm(); + + int which = 0; + if (_tracker.rightAlt() || _tracker.rightControl()) { + which = 1; + } + if (_tracker.leftAlt() || _tracker.leftControl()) { + if (which != 0) break; // ambiguous + which = -1; + } + if (which == 0) break; // no handle chosen + bool one_pixel = _tracker.leftAlt() || _tracker.rightAlt(); + bool handled = true; + + switch (key) { + // single handle functions + // rotation + case GDK_KEY_bracketleft: + case GDK_KEY_braceleft: + pm.rotateHandle(n, which, -_desktop->yaxisdir(), one_pixel); + break; + case GDK_KEY_bracketright: + case GDK_KEY_braceright: + pm.rotateHandle(n, which, _desktop->yaxisdir(), one_pixel); + break; + // adjust length + case GDK_KEY_period: + case GDK_KEY_greater: + pm.scaleHandle(n, which, 1, one_pixel); + break; + case GDK_KEY_comma: + case GDK_KEY_less: + pm.scaleHandle(n, which, -1, one_pixel); + break; + default: + handled = false; + break; + } + + if (handled) return true; + } while(false); + } + + + switch (event->type) { + case GDK_KEY_PRESS: + switch (key) { + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // Insert - insert nodes in the middle of selected segments + insertNodes(); + return true; + case GDK_KEY_i: + case GDK_KEY_I: + if (held_only_shift(event->key)) { + // Shift+I - insert nodes (alternate keybinding for Mac keyboards + // that don't have the Insert key) + insertNodes(); + return true; + } + break; + case GDK_KEY_d: + case GDK_KEY_D: + if (held_only_shift(event->key)) { + duplicateNodes(); + return true; + } + case GDK_KEY_j: + case GDK_KEY_J: + if (held_only_shift(event->key)) { + // Shift+J - join nodes + joinNodes(); + return true; + } + if (held_only_alt(event->key)) { + // Alt+J - join segments + joinSegments(); + return true; + } + break; + case GDK_KEY_b: + case GDK_KEY_B: + if (held_only_shift(event->key)) { + // Shift+B - break nodes + breakNodes(); + return true; + } + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if (held_shift(event->key)) break; + if (held_alt(event->key)) { + // Alt+Delete - delete segments + deleteSegments(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool del_preserves_shape = prefs->getBool("/tools/nodes/delete_preserves_shape", true); + // pass keep_shape = true when: + // a) del preserves shape, and control is not pressed + // b) ctrl+del preserves shape (del_preserves_shape is false), and control is pressed + // Hence xor + guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0); + + //if the trace is bspline ( mode 2) + if(mode==2){ + // is this correct ? + if(del_preserves_shape ^ held_control(event->key)){ + deleteNodes(false); + } else { + deleteNodes(true); + } + } else { + deleteNodes(del_preserves_shape ^ held_control(event->key)); + } + + // Delete any selected gradient nodes as well + event_context->deleteSelectedDrag(held_control(event->key)); + } + return true; + case GDK_KEY_c: + case GDK_KEY_C: + if (held_only_shift(event->key)) { + // Shift+C - make nodes cusp + setNodeType(NODE_CUSP); + return true; + } + break; + case GDK_KEY_s: + case GDK_KEY_S: + if (held_only_shift(event->key)) { + // Shift+S - make nodes smooth + setNodeType(NODE_SMOOTH); + return true; + } + break; + case GDK_KEY_a: + case GDK_KEY_A: + if (held_only_shift(event->key)) { + // Shift+A - make nodes auto-smooth + setNodeType(NODE_AUTO); + return true; + } + break; + case GDK_KEY_y: + case GDK_KEY_Y: + if (held_only_shift(event->key)) { + // Shift+Y - make nodes symmetric + setNodeType(NODE_SYMMETRIC); + return true; + } + break; + case GDK_KEY_r: + case GDK_KEY_R: + if (held_only_shift(event->key)) { + // Shift+R - reverse subpaths + reverseSubpaths(); + return true; + } + break; + case GDK_KEY_l: + case GDK_KEY_L: + if (held_only_shift(event->key)) { + // Shift+L - make segments linear + setSegmentType(SEGMENT_STRAIGHT); + return true; + } + case GDK_KEY_u: + case GDK_KEY_U: + if (held_only_shift(event->key)) { + // Shift+U - make segments curves + setSegmentType(SEGMENT_CUBIC_BEZIER); + return true; + } + default: + break; + } + break; + case GDK_MOTION_NOTIFY: + combine_motion_events(_desktop->canvas, event->motion, 0); + for (auto & i : _mmap) { + if (i.second->event(event_context, event)) return true; + } + break; + default: break; + } + + return false; +} + +/** Commit changes to XML and add undo stack entry based on the action that was done. Invoked + * by sub-manipulators, for example TransformHandleSet and ControlPointSelection. */ +void MultiPathManipulator::_commit(CommitEvent cps) +{ + gchar const *reason = nullptr; + gchar const *key = nullptr; + switch(cps) { + case COMMIT_MOUSE_MOVE: + reason = _("Move nodes"); + break; + case COMMIT_KEYBOARD_MOVE_X: + reason = _("Move nodes horizontally"); + key = "node:move:x"; + break; + case COMMIT_KEYBOARD_MOVE_Y: + reason = _("Move nodes vertically"); + key = "node:move:y"; + break; + case COMMIT_MOUSE_ROTATE: + reason = _("Rotate nodes"); + break; + case COMMIT_KEYBOARD_ROTATE: + reason = _("Rotate nodes"); + key = "node:rotate"; + break; + case COMMIT_MOUSE_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + break; + case COMMIT_MOUSE_SCALE: + reason = _("Scale nodes"); + break; + case COMMIT_KEYBOARD_SCALE_UNIFORM: + reason = _("Scale nodes uniformly"); + key = "node:scale:uniform"; + break; + case COMMIT_KEYBOARD_SCALE_X: + reason = _("Scale nodes horizontally"); + key = "node:scale:x"; + break; + case COMMIT_KEYBOARD_SCALE_Y: + reason = _("Scale nodes vertically"); + key = "node:scale:y"; + break; + case COMMIT_MOUSE_SKEW_X: + reason = _("Skew nodes horizontally"); + key = "node:skew:x"; + break; + case COMMIT_MOUSE_SKEW_Y: + reason = _("Skew nodes vertically"); + key = "node:skew:y"; + break; + case COMMIT_FLIP_X: + reason = _("Flip nodes horizontally"); + break; + case COMMIT_FLIP_Y: + reason = _("Flip nodes vertically"); + break; + default: return; + } + + _selection.signal_update.emit(); + invokeForAll(&PathManipulator::writeXML); + if (key) { + DocumentUndo::maybeDone(_desktop->getDocument(), key, SP_VERB_CONTEXT_NODE, reason); + } else { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, reason); + } + signal_coords_changed.emit(); +} + +/** Commits changes to XML and adds undo stack entry. */ +void MultiPathManipulator::_done(gchar const *reason, bool alert_LPE) { + invokeForAll(&PathManipulator::update, alert_LPE); + invokeForAll(&PathManipulator::writeXML); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, reason); + signal_coords_changed.emit(); +} + +/** Commits changes to XML, adds undo stack entry and removes empty manipulators. */ +void MultiPathManipulator::_doneWithCleanup(gchar const *reason, bool alert_LPE) { + _changed.block(); + _done(reason, alert_LPE); + cleanup(); + _changed.unblock(); +} + +/** Get an outline color based on the shape's role (normal, mask, LPE parameter, etc.). */ +guint32 MultiPathManipulator::_getOutlineColor(ShapeRole role, SPObject *object) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch(role) { + case SHAPE_ROLE_CLIPPING_PATH: + return prefs->getColor("/tools/nodes/clipping_path_color", 0x00ff00ff); + case SHAPE_ROLE_MASK: + return prefs->getColor("/tools/nodes/mask_color", 0x0000ffff); + case SHAPE_ROLE_LPE_PARAM: + return prefs->getColor("/tools/nodes/lpe_param_color", 0x009000ff); + case SHAPE_ROLE_NORMAL: + default: + return SP_ITEM(object)->highlight_color(); + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/multi-path-manipulator.h b/src/ui/tool/multi-path-manipulator.h new file mode 100644 index 0000000..7fbb959 --- /dev/null +++ b/src/ui/tool/multi-path-manipulator.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Multi path manipulator - a tool component that edits multiple paths at once + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_MULTI_PATH_MANIPULATOR_H + +#include <cstddef> +#include <sigc++/connection.h> +#include "node.h" +#include "commit-events.h" +#include "manipulator.h" +#include "modifier-tracker.h" +#include "node-types.h" +#include "shape-record.h" + +struct SPCanvasGroup; + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; +struct PathSharedData; + +/** + * Manipulator that manages multiple path manipulators active at the same time. + */ +class MultiPathManipulator : public PointManipulator { +public: + MultiPathManipulator(PathSharedData &data, sigc::connection &chg); + ~MultiPathManipulator() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *event) override; + + bool empty() { return _mmap.empty(); } + unsigned size() { return _mmap.size(); } + void setItems(std::set<ShapeRecord> const &); + void clear() { _mmap.clear(); } + void cleanup(); + + void selectSubpaths(); + void shiftSelection(int dir); + void invertSelectionInSubpaths(); + + void setNodeType(NodeType t); + void setSegmentType(SegmentType t); + + void insertNodesAtExtrema(ExtremumType extremum); + void insertNodes(); + void insertNode(Geom::Point pt); + void alertLPE(); + void duplicateNodes(); + void joinNodes(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void joinSegments(); + void deleteSegments(); + void alignNodes(Geom::Dim2 d); + void distributeNodes(Geom::Dim2 d); + void reverseSubpaths(); + void move(Geom::Point const &delta); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void updateOutlineColors(); + void updateHandles(); + + sigc::signal<void> signal_coords_changed; /// Emitted whenever the coordinates + /// shown in the status bar need updating +private: + typedef std::pair<ShapeRecord, std::shared_ptr<PathManipulator> > MapPair; + typedef std::map<ShapeRecord, std::shared_ptr<PathManipulator> > MapType; + + template <typename R> + void invokeForAll(R (PathManipulator::*method)()) { + for (MapType::iterator i = _mmap.begin(); i != _mmap.end(); ) { + // Sometimes the PathManipulator got freed at loop end, thus + // invalidating the iterator so make sure that next_i will + // be a valid iterator and then assign i to it. + MapType::iterator next_i = i; + ++next_i; + // i->second is a std::shared_ptr so try to hold on to it so + // it won't get freed prematurely by the WriteXML() method or + // whatever. See https://bugs.launchpad.net/inkscape/+bug/1617615 + // Applicable to empty paths. + std::shared_ptr<PathManipulator> hold(i->second); + ((hold.get())->*method)(); + i = next_i; + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A), A a) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a); + } + } + template <typename R, typename A> + void invokeForAll(R (PathManipulator::*method)(A const &), A const &a) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a); + } + } + template <typename R, typename A, typename B> + void invokeForAll(R (PathManipulator::*method)(A,B), A a, B b) { + for (auto & i : _mmap) { + ((i.second.get())->*method)(a, b); + } + } + + void _commit(CommitEvent cps); + void _done(gchar const *reason, bool alert_LPE = true); + void _doneWithCleanup(gchar const *reason, bool alert_LPE = false); + guint32 _getOutlineColor(ShapeRole role, SPObject *object); + + MapType _mmap; +public: + PathSharedData const &_path_data; +private: + sigc::connection &_changed; + ModifierTracker _tracker; + bool _show_handles; + bool _show_outline; + bool _show_path_direction; + bool _live_outline; + bool _live_objects; + + friend class PathManipulator; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node-types.h b/src/ui/tool/node-types.h new file mode 100644 index 0000000..ee2d27f --- /dev/null +++ b/src/ui/tool/node-types.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Node types and other small enums. + * This file exists to reduce the number of includes pulled in by toolbox.cpp. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_TYPES_H +#define SEEN_UI_TOOL_NODE_TYPES_H + +namespace Inkscape { +namespace UI { + +/** Types of nodes supported in the node tool. */ +enum NodeType { + NODE_CUSP, ///< Cusp node - no handle constraints + NODE_SMOOTH, ///< Smooth node - handles must be colinear + NODE_AUTO, ///< Auto node - handles adjusted automatically based on neighboring nodes + NODE_SYMMETRIC, ///< Symmetric node - handles must be colinear and of equal length + NODE_LAST_REAL_TYPE, ///< Last real type of node - used for ctrl+click on a node + NODE_PICK_BEST = 100 ///< Select type based on handle positions +}; + +/** Types of segments supported in the node tool. */ +enum SegmentType { + SEGMENT_STRAIGHT, ///< Straight linear segment + SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node.cpp b/src/ui/tool/node.cpp new file mode 100644 index 0000000..fc09ca9 --- /dev/null +++ b/src/ui/tool/node.cpp @@ -0,0 +1,1924 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <atomic> +#include <iostream> +#include <stdexcept> +#include <boost/utility.hpp> + +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + +#include <2geom/bezier-utils.h> + +#include "desktop.h" +#include "multi-path-manipulator.h" +#include "snap.h" + +#include "display/sp-ctrlline.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-util.h" + +#include "ui/control-manager.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/tools-switch.h" + +namespace { + +Inkscape::ControlType nodeTypeToCtrlType(Inkscape::UI::NodeType type) +{ + Inkscape::ControlType result = Inkscape::CTRL_TYPE_NODE_CUSP; + switch(type) { + case Inkscape::UI::NODE_SMOOTH: + result = Inkscape::CTRL_TYPE_NODE_SMOOTH; + break; + case Inkscape::UI::NODE_AUTO: + result = Inkscape::CTRL_TYPE_NODE_AUTO; + break; + case Inkscape::UI::NODE_SYMMETRIC: + result = Inkscape::CTRL_TYPE_NODE_SYMETRICAL; + break; + case Inkscape::UI::NODE_CUSP: + default: + result = Inkscape::CTRL_TYPE_NODE_CUSP; + break; + } + return result; +} + +/** + * @brief provides means to estimate float point rounding error due to serialization to svg + * + * Keeps cached value up to date with preferences option `/options/svgoutput/numericprecision` + * to avoid costly direct reads + * */ +class SvgOutputPrecisionWatcher : public Inkscape::Preferences::Observer { +public: + /// Returns absolute \a value`s rounding serialization error based on current preferences settings + static double error_of(double value) { + return value * instance().rel_error; + } + + void notify(const Inkscape::Preferences::Entry &new_val) override { + int digits = new_val.getIntLimited(6, 1, 16); + set_numeric_precision(digits); + } + +private: + SvgOutputPrecisionWatcher() : Observer("/options/svgoutput/numericprecision"), rel_error(1) { + Inkscape::Preferences::get()->addObserver(*this); + int digits = Inkscape::Preferences::get()->getIntLimited("/options/svgoutput/numericprecision", 6, 1, 16); + set_numeric_precision(digits); + } + + ~SvgOutputPrecisionWatcher() override { + Inkscape::Preferences::get()->removeObserver(*this); + } + /// Update cached value of relative error with number of significant digits + void set_numeric_precision(int digits) { + double relative_error = 0.5; // the error is half of last digit + while (digits > 0) { + relative_error /= 10; + digits--; + } + rel_error = relative_error; + } + + static SvgOutputPrecisionWatcher &instance() { + static SvgOutputPrecisionWatcher _instance; + return _instance; + } + + std::atomic<double> rel_error; /// Cached relative error +}; + +/// Returns absolute error of \a point as if serialized to svg with current preferences +double serializing_error_of(const Geom::Point &point) { + return SvgOutputPrecisionWatcher::error_of(point.length()); +} + +/** + * @brief Returns true if three points are collinear within current serializing precision + * + * The algorithm of collinearity check is explicitly used to calculate the check error. + * + * This function can be sufficiently reduced or even removed completely if `Geom::are_collinear` + * would declare it's check algorithm as part of the public API. + * + * */ +bool are_collinear_within_serializing_error(const Geom::Point &A, const Geom::Point &B, const Geom::Point &C) { + const double tolerance_factor = 10; // to account other factors which increase uncertainty + const double tolerance_A = serializing_error_of(A) * tolerance_factor; + const double tolerance_B = serializing_error_of(B) * tolerance_factor; + const double tolerance_C = serializing_error_of(C) * tolerance_factor; + const double CB_length = (B - C).length(); + const double AB_length = (B - A).length(); + Geom::Point C_reflect_scaled = B + (B - C) / CB_length * AB_length; + double tolerance_C_reflect_scaled = tolerance_B + + (tolerance_B + tolerance_C) + * (1 + (tolerance_A + tolerance_B) / AB_length) + * (1 + (tolerance_C + tolerance_B) / CB_length); + return Geom::are_near(C_reflect_scaled, A, tolerance_C_reflect_scaled + tolerance_A); +} + +} // namespace + +namespace Inkscape { +namespace UI { + +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; + +ControlPoint::ColorSet Node::node_colors = { + {0xbfbfbf00, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff}, // clicked fill, stroke + // + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +ControlPoint::ColorSet Handle::_handle_colors = { + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff}, // clicked fill, stroke + // + {0xffffffff, 0x000000ff}, // normal fill, stroke + {0xff000000, 0x000000ff}, // mouseover fill, stroke + {0xff000000, 0x000000ff} // clicked fill, stroke +}; + +std::ostream &operator<<(std::ostream &out, NodeType type) +{ + switch(type) { + case NODE_CUSP: out << 'c'; break; + case NODE_SMOOTH: out << 's'; break; + case NODE_AUTO: out << 'a'; break; + case NODE_SYMMETRIC: out << 'z'; break; + default: out << 'b'; break; + } + return out; +} + +/** Computes an unit vector of the direction from first to second control point */ +static Geom::Point direction(Geom::Point const &first, Geom::Point const &second) { + return Geom::unit_vector(second - first); +} + +Geom::Point Handle::_saved_other_pos(0, 0); + +double Handle::_saved_length = 0.0; + +bool Handle::_drag_out = false; + +Handle::Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent) : + ControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER, + CTRL_TYPE_ADJ_HANDLE, + _handle_colors, data.handle_group), + _parent(parent), + _handle_line(ControlManager::getManager().createControlLine(data.handle_line_group)), + _degenerate(true) +{ + setVisible(false); +} + +Handle::~Handle() +{ + //sp_canvas_item_hide(_handle_line); + sp_canvas_item_destroy(_handle_line); +} + +void Handle::setVisible(bool v) +{ + ControlPoint::setVisible(v); + if (v) { + sp_canvas_item_show(_handle_line); + } else { + sp_canvas_item_hide(_handle_line); + } +} + +void Handle::move(Geom::Point const &new_pos) +{ + Handle *other = this->other(); + Node *node_towards = _parent->nodeToward(this); // node in direction of this handle + Node *node_away = _parent->nodeAwayFrom(this); // node in the opposite direction + Handle *towards = node_towards ? node_towards->handleAwayFrom(_parent) : nullptr; + Handle *towards_second = node_towards ? node_towards->handleToward(_parent) : nullptr; + double bspline_weight = 0.0; + + if (Geom::are_near(new_pos, _parent->position())) { + // The handle becomes degenerate. + // Adjust node type as necessary. + if (other->isDegenerate()) { + // If both handles become degenerate, convert to parent cusp node + _parent->setType(NODE_CUSP, false); + } else { + // Only 1 handle becomes degenerate + switch (_parent->type()) { + case NODE_AUTO: + case NODE_SYMMETRIC: + _parent->setType(NODE_SMOOTH, false); + break; + default: + // do nothing for other node types + break; + } + } + // If the segment between the handle and the node in its direction becomes linear, + // and there are smooth nodes at its ends, make their handles collinear with the segment. + if (towards && towards_second->isDegenerate()) { + if (node_towards->type() == NODE_SMOOTH) { + towards->setDirection(*_parent, *node_towards); + } + if (_parent->type() == NODE_SMOOTH) { + other->setDirection(*node_towards, *_parent); + } + } + setPosition(new_pos); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + return; + } + + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + // restrict movement to the line joining the nodes + Geom::Point direction = _parent->position() - node_away->position(); + Geom::Point delta = new_pos - _parent->position(); + // project the relative position on the direction line + Geom::Coord direction_length = Geom::L2sq(direction); + Geom::Point new_delta; + if (direction_length == 0) { + // joining line has zero length - any direction is okay, prevent division by zero + new_delta = delta; + } else { + new_delta = (Geom::dot(delta, direction) / direction_length) * direction; + } + setRelativePos(new_delta); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + + return; + } + + switch (_parent->type()) { + case NODE_AUTO: + _parent->setType(NODE_SMOOTH, false); + // fall through - auto nodes degrade into smooth nodes + case NODE_SMOOTH: { + // for smooth nodes, we need to rotate the opposite handle + // so that it's collinear with the dragged one, while conserving length. + other->setDirection(new_pos, *_parent); + } break; + case NODE_SYMMETRIC: + // for symmetric nodes, place the other handle on the opposite side + other->setRelativePos(-(new_pos - _parent->position())); + break; + default: break; + } + setPosition(new_pos); + + // move the handle and its opposite the same proportion + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, false)); + bspline_weight = _pm()._bsplineHandlePosition(this, false); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), bspline_weight)); + } + Inkscape::UI::Tools::sp_update_helperpath(); +} + +void Handle::setPosition(Geom::Point const &p) +{ + ControlPoint::setPosition(p); + _handle_line->setCoords(_parent->position(), position()); + + // update degeneration info and visibility + if (Geom::are_near(position(), _parent->position())) + _degenerate = true; + else _degenerate = false; + + if (_parent->_handles_shown && _parent->visible() && !_degenerate) { + setVisible(true); + } else { + setVisible(false); + } +} + +void Handle::setLength(double len) +{ + if (isDegenerate()) return; + Geom::Point dir = Geom::unit_vector(relativePos()); + setRelativePos(dir * len); +} + +void Handle::retract() +{ + move(_parent->position()); +} + +void Handle::setDirection(Geom::Point const &from, Geom::Point const &to) +{ + setDirection(to - from); +} + +void Handle::setDirection(Geom::Point const &dir) +{ + Geom::Point unitdir = Geom::unit_vector(dir); + setRelativePos(unitdir * length()); +} + +/** + * See also: Node::node_type_to_localized_string(NodeType type) + */ +char const *Handle::handle_type_to_localized_string(NodeType type) +{ + switch(type) { + case NODE_CUSP: + return _("Corner node handle"); + case NODE_SMOOTH: + return _("Smooth node handle"); + case NODE_SYMMETRIC: + return _("Symmetric node handle"); + case NODE_AUTO: + return _("Auto-smooth node handle"); + default: + return ""; + } +} + +bool Handle::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + switch (event->type) + { + case GDK_KEY_PRESS: + + switch (shortcut_key(event->key)) + { + case GDK_KEY_s: + case GDK_KEY_S: + + /* if Shift+S is pressed while hovering over a cusp node handle, + hold the handle in place; otherwise, process normally. + this handle is guaranteed not to be degenerate. */ + + if (held_only_shift(event->key) && _parent->_type == NODE_CUSP) { + + // make opposite handle collinear, + // but preserve length, unless degenerate + if (other()->isDegenerate()) + other()->setRelativePos(-relativePos()); + else + other()->setDirection(-relativePos()); + _parent->setType(NODE_SMOOTH, false); + + // update display + _parent->_pm().update(); + + // update undo history + _parent->_pm()._commit(_("Change node type")); + + return true; + } + break; + + case GDK_KEY_y: + case GDK_KEY_Y: + + /* if Shift+Y is pressed while hovering over a cusp, smooth, or auto node handle, + hold the handle in place; otherwise, process normally. + this handle is guaranteed not to be degenerate. */ + + if (held_only_shift(event->key) && (_parent->_type == NODE_CUSP || + _parent->_type == NODE_SMOOTH || + _parent->_type == NODE_AUTO)) { + + // make opposite handle collinear, and of equal length + other()->setRelativePos(-relativePos()); + _parent->setType(NODE_SYMMETRIC, false); + + // update display + _parent->_pm().update(); + + // update undo history + _parent->_pm()._commit(_("Change node type")); + + return true; + } + break; + } + break; + + case GDK_2BUTTON_PRESS: + + // double-click event to set the handles of a node + // to the position specified by DEFAULT_START_POWER + handle_2button_press(); + break; + } + + return ControlPoint::_eventHandler(event_context, event); +} + +// this function moves the handle and its opposite to the position specified by DEFAULT_START_POWER +void Handle::handle_2button_press(){ + if(_pm()._isBSpline()){ + setPosition(_pm()._bsplineHandleReposition(this, DEFAULT_START_POWER)); + this->other()->setPosition(_pm()._bsplineHandleReposition(this->other(), DEFAULT_START_POWER)); + _pm().update(); + } +} + +bool Handle::grabbed(GdkEventMotion *) +{ + _saved_other_pos = other()->position(); + _saved_length = _drag_out ? 0 : length(); + _pm()._handleGrabbed(); + return false; +} + +void Handle::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Point parent_pos = _parent->position(); + Geom::Point origin = _last_drag_origin(); + SnapManager &sm = _desktop->namedview->snap_manager; + bool snap = held_shift(*event) ? false : sm.someSnapperMightSnap(); + boost::optional<Inkscape::Snapper::SnapConstraint> ctrl_constraint; + + // with Alt, preserve length + if (held_alt(*event)) { + new_pos = parent_pos + Geom::unit_vector(new_pos - parent_pos) * _saved_length; + snap = false; + } + // with Ctrl, constrain to M_PI/rotationsnapsperpi increments from vertical + // and the original position. + if (held_control(*event)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = 2 * prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + + // note: if snapping to the original position is only desired in the original + // direction of the handle, change to Ray instead of Line + Geom::Line original_line(parent_pos, origin); + Geom::Line perp_line(parent_pos, parent_pos + Geom::rot90(origin - parent_pos)); + Geom::Point snap_pos = parent_pos + Geom::constrain_angle( + Geom::Point(0,0), new_pos - parent_pos, snaps, Geom::Point(1,0)); + Geom::Point orig_pos = original_line.pointAt(original_line.nearestTime(new_pos)); + Geom::Point perp_pos = perp_line.pointAt(perp_line.nearestTime(new_pos)); + + Geom::Point result = snap_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - snap_pos); + if (Geom::distance(orig_pos, new_pos) < Geom::distance(result, new_pos)) { + result = orig_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - orig_pos); + } + if (Geom::distance(perp_pos, new_pos) < Geom::distance(result, new_pos)) { + result = perp_pos; + ctrl_constraint = Inkscape::Snapper::SnapConstraint(parent_pos, parent_pos - perp_pos); + } + new_pos = result; + // move the handle and its opposite in X fixed positions depending on parameter "steps with control" + // by default in live BSpline + if(_pm()._isBSpline()){ + setPosition(new_pos); + int steps = _pm()._bsplineGetSteps(); + new_pos=_pm()._bsplineHandleReposition(this,ceilf(_pm()._bsplineHandlePosition(this, false)*steps)/steps); + } + } + + std::vector<Inkscape::SnapCandidatePoint> unselected; + // if the snap adjustment is activated and it is not BSpline + if (snap && !_pm()._isBSpline()) { + ControlPointSelection::Set &nodes = _parent->_selection.allPoints(); + for (auto node : nodes) { + Node *n = static_cast<Node*>(node); + unselected.push_back(n->snapCandidatePoint()); + } + sm.setupIgnoreSelection(_desktop, true, &unselected); + + Node *node_away = _parent->nodeAwayFrom(this); + if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) { + Inkscape::Snapper::SnapConstraint cl(_parent->position(), + _parent->position() - node_away->position()); + Inkscape::SnappedPoint p; + p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), cl); + new_pos = p.getPoint(); + } else if (ctrl_constraint) { + // NOTE: this is subtly wrong. + // We should get all possible constraints and snap along them using + // multipleConstrainedSnaps, instead of first snapping to angle and then to objects + Inkscape::SnappedPoint p; + p = sm.constrainedSnap(Inkscape::SnapCandidatePoint(new_pos, SNAPSOURCE_NODE_HANDLE), *ctrl_constraint); + new_pos = p.getPoint(); + } else { + sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_NODE_HANDLE); + } + sm.unSetup(); + } + + + // with Shift, if the node is cusp, rotate the other handle as well + if (_parent->type() == NODE_CUSP && !_drag_out) { + if (held_shift(*event)) { + Geom::Point other_relpos = _saved_other_pos - parent_pos; + other_relpos *= Geom::Rotate(Geom::angle_between(origin - parent_pos, new_pos - parent_pos)); + other()->setRelativePos(other_relpos); + } else { + // restore the position + other()->setPosition(_saved_other_pos); + } + } + // if it is BSpline, but SHIFT or CONTROL are not pressed, fix it in the original position + if(_pm()._isBSpline() && !held_shift(*event) && !held_control(*event)){ + new_pos=_last_drag_origin(); + } + move(new_pos); // needed for correct update, even though it's redundant + _pm().update(); +} + +void Handle::ungrabbed(GdkEventButton *event) +{ + // hide the handle if it's less than dragtolerance away from the node + // however, never do this for cancelled drag / broken grab + // TODO is this actually a good idea? + if (event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int drag_tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + Geom::Point dist = _desktop->d2w(_parent->position()) - _desktop->d2w(position()); + if (dist.length() <= drag_tolerance) { + move(_parent->position()); + } + } + + // HACK: If the handle was dragged out, call parent's ungrabbed handler, + // so that transform handles reappear + if (_drag_out) { + _parent->ungrabbed(event); + } + _drag_out = false; + + _pm()._handleUngrabbed(); +} + +bool Handle::clicked(GdkEventButton *event) +{ + _pm()._handleClicked(this, event); + return true; +} + +Handle const *Handle::other() const +{ + return const_cast<Handle *>(this)->other(); +} + +Handle *Handle::other() +{ + if (this == &_parent->_front) { + return &_parent->_back; + } else { + return &_parent->_front; + } +} + +static double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +Glib::ustring Handle::_getTip(unsigned state) const +{ + /* a trick to mark as BSpline if the node has no strength; + we are going to use it later to show the appropriate messages. + we cannot do it in any different way because the function is constant. */ + Handle *h = const_cast<Handle *>(this); + bool isBSpline = _pm()._isBSpline(); + bool can_shift_rotate = _parent->type() == NODE_CUSP && !other()->isDegenerate(); + Glib::ustring s = C_("Path handle tip", + "node control handle"); // not expected + + if (state_held_alt(state) && !isBSpline) { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate) { + s = format_tip(C_("Path handle tip", + "<b>Shift+Ctrl+Alt</b>: " + "preserve length and snap rotation angle to %g° increments, " + "and rotate both handles"), + snap_increment_degrees()); + } + else { + s = format_tip(C_("Path handle tip", + "<b>Ctrl+Alt</b>: " + "preserve length and snap rotation angle to %g° increments"), + snap_increment_degrees()); + } + } + else { + if (state_held_shift(state) && can_shift_rotate) { + s = C_("Path handle tip", + "<b>Shift+Alt</b>: preserve handle length and rotate both handles"); + } + else { + s = C_("Path handle tip", + "<b>Alt</b>: preserve handle length while dragging"); + } + } + } + else { + if (state_held_control(state)) { + if (state_held_shift(state) && can_shift_rotate && !isBSpline) { + s = format_tip(C_("Path handle tip", + "<b>Shift+Ctrl</b>: " + "snap rotation angle to %g° increments, and rotate both handles"), + snap_increment_degrees()); + } + else if (isBSpline) { + s = C_("Path handle tip", + "<b>Ctrl</b>: " + "Snap handle to steps defined in BSpline Live Path Effect"); + } + else { + s = format_tip(C_("Path handle tip", + "<b>Ctrl</b>: " + "snap rotation angle to %g° increments, click to retract"), + snap_increment_degrees()); + } + } + else if (state_held_shift(state) && can_shift_rotate && !isBSpline) { + s = C_("Path handle tip", + "<b>Shift</b>: rotate both handles by the same angle"); + } + else if (state_held_shift(state) && isBSpline) { + s = C_("Path handle tip", + "<b>Shift</b>: move handle"); + } + else { + char const *handletype = handle_type_to_localized_string(_parent->_type); + char const *more; + + if (can_shift_rotate && !isBSpline) { + more = C_("Path handle tip", + "Shift, Ctrl, Alt"); + } + else if (isBSpline) { + more = C_("Path handle tip", + "Ctrl"); + } + else { + more = C_("Path handle tip", + "Ctrl, Alt"); + } + + if (_parent->type() == NODE_CUSP) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "hover to lock" ", " + "Shift+S to make smooth" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_SMOOTH) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "hover to lock" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_AUTO) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to make smooth, " + "hover to lock" ", " + "Shift+Y to make symmetric" ". " + "(more: %s)"), + handletype, more); + } + else if (_parent->type() == NODE_SYMMETRIC) { + s = format_tip(C_("Path handle tip", + "<b>%s</b>: " + "drag to shape the path" ". " + "(more: %s)"), + handletype, more); + } + else if (isBSpline) { + double power = _pm()._bsplineHandlePosition(h); + s = format_tip(C_("Path handle tip", + "<b>BSpline node handle</b> (%.3g power): " + "Shift-drag to move, " + "double-click to reset. " + "(more: %s)"), + power, more); + } + else { + s = C_("Path handle tip", + "<b>unknown node handle</b>"); // not expected + } + } + } + + return (s); +} + +Glib::ustring Handle::_getDragTip(GdkEventMotion */*event*/) const +{ + Geom::Point dist = position() - _last_drag_origin(); + // report angle in mathematical convention + double angle = Geom::angle_between(Geom::Point(-1,0), position() - _parent->position()); + angle += M_PI; // angle is (-M_PI...M_PI] - offset by +pi and scale to 0...360 + angle *= 360.0 / (2 * M_PI); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px"); + Inkscape::Util::Quantity len_q = Inkscape::Util::Quantity(length(), "px"); + Glib::ustring x = x_q.string(_desktop->namedview->display_units); + Glib::ustring y = y_q.string(_desktop->namedview->display_units); + Glib::ustring len = len_q.string(_desktop->namedview->display_units); + Glib::ustring ret = format_tip(C_("Path handle tip", + "Move handle by %s, %s; angle %.2f°, length %s"), x.c_str(), y.c_str(), angle, len.c_str()); + return ret; +} + +Node::Node(NodeSharedData const &data, Geom::Point const &initial_pos) : + SelectableControlPoint(data.desktop, initial_pos, SP_ANCHOR_CENTER, + CTRL_TYPE_NODE_CUSP, + *data.selection, + node_colors, data.node_group), + _front(data, initial_pos, this), + _back(data, initial_pos, this), + _type(NODE_CUSP), + _handles_shown(false) +{ + // NOTE we do not set type here, because the handles are still degenerate +} + +Node const *Node::_next() const +{ + return const_cast<Node*>(this)->_next(); +} + +// NOTE: not using iterators won't make this much quicker because iterators can be 100% inlined. +Node *Node::_next() +{ + NodeList::iterator n = NodeList::get_iterator(this).next(); + if (n) { + return n.ptr(); + } else { + return nullptr; + } +} + +Node const *Node::_prev() const +{ + return const_cast<Node *>(this)->_prev(); +} + +Node *Node::_prev() +{ + NodeList::iterator p = NodeList::get_iterator(this).prev(); + if (p) { + return p.ptr(); + } else { + return nullptr; + } +} + +void Node::move(Geom::Point const &new_pos) +{ + // move handles when the node moves. + Geom::Point old_pos = position(); + Geom::Point delta = new_pos - position(); + + // save the previous nodes strength to apply it again once the node is moved + double nodeWeight = NO_POWER; + double nextNodeWeight = NO_POWER; + double prevNodeWeight = NO_POWER; + Node *n = this; + Node * nextNode = n->nodeToward(n->front()); + Node * prevNode = n->nodeToward(n->back()); + nodeWeight = fmax(_pm()._bsplineHandlePosition(n->front(), false),_pm()._bsplineHandlePosition(n->back(), false)); + if(prevNode){ + prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front()); + } + if(nextNode){ + nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back()); + } + + setPosition(new_pos); + + _front.setPosition(_front.position() + delta); + _back.setPosition(_back.position() + delta); + + // if the node has a smooth handle after a line segment, it should be kept collinear + // with the segment + _fixNeighbors(old_pos, new_pos); + + // move the affected handles. First the node ones, later the adjoining ones. + if(_pm()._isBSpline()){ + _front.setPosition(_pm()._bsplineHandleReposition(this->front(),nodeWeight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(),nodeWeight)); + if(prevNode){ + prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight)); + } + if(nextNode){ + nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight)); + } + } + Inkscape::UI::Tools::sp_update_helperpath(); +} + +void Node::transform(Geom::Affine const &m) +{ + + Geom::Point old_pos = position(); + + // save the previous nodes strength to apply it again once the node is moved + double nodeWeight = NO_POWER; + double nextNodeWeight = NO_POWER; + double prevNodeWeight = NO_POWER; + Node *n = this; + Node * nextNode = n->nodeToward(n->front()); + Node * prevNode = n->nodeToward(n->back()); + nodeWeight = _pm()._bsplineHandlePosition(n->front()); + if(prevNode){ + prevNodeWeight = _pm()._bsplineHandlePosition(prevNode->front()); + } + if(nextNode){ + nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back()); + } + + setPosition(position() * m); + _front.setPosition(_front.position() * m); + _back.setPosition(_back.position() * m); + + /* Affine transforms keep handle invariants for smooth and symmetric nodes, + * but smooth nodes at ends of linear segments and auto nodes need special treatment */ + _fixNeighbors(old_pos, position()); + + // move the involved handles. First the node ones, later the adjoining ones. + if(_pm()._isBSpline()){ + _front.setPosition(_pm()._bsplineHandleReposition(this->front(), nodeWeight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(), nodeWeight)); + if(prevNode){ + prevNode->front()->setPosition(_pm()._bsplineHandleReposition(prevNode->front(), prevNodeWeight)); + } + if(nextNode){ + nextNode->back()->setPosition(_pm()._bsplineHandleReposition(nextNode->back(), nextNodeWeight)); + } + } +} + +Geom::Rect Node::bounds() const +{ + Geom::Rect b(position(), position()); + b.expandTo(_front.position()); + b.expandTo(_back.position()); + return b; +} + +void Node::_fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos) +{ + // This method restores handle invariants for neighboring nodes, + // and invariants that are based on positions of those nodes for this one. + + // Fix auto handles + if (_type == NODE_AUTO) _updateAutoHandles(); + if (old_pos != new_pos) { + if (_next() && _next()->_type == NODE_AUTO) _next()->_updateAutoHandles(); + if (_prev() && _prev()->_type == NODE_AUTO) _prev()->_updateAutoHandles(); + } + + /* Fix smooth handles at the ends of linear segments. + Rotate the appropriate handle to be collinear with the segment. + If there is a smooth node at the other end of the segment, rotate it too. */ + Handle *handle, *other_handle; + Node *other; + if (_is_line_segment(this, _next())) { + handle = &_back; + other = _next(); + other_handle = &_next()->_front; + } else if (_is_line_segment(_prev(), this)) { + handle = &_front; + other = _prev(); + other_handle = &_prev()->_back; + } else return; + + if (_type == NODE_SMOOTH && !handle->isDegenerate()) { + handle->setDirection(other->position(), new_pos); + } + // also update the handle on the other end of the segment + if (other->_type == NODE_SMOOTH && !other_handle->isDegenerate()) { + other_handle->setDirection(new_pos, other->position()); + } +} + +void Node::_updateAutoHandles() +{ + // Recompute the position of automatic handles. For endnodes, retract both handles. + // (It's only possible to create an end auto node through the XML editor.) + if (isEndNode()) { + _front.retract(); + _back.retract(); + return; + } + + // auto nodes automatically adjust their handles to give + // an appearance of smoothness, no matter what their surroundings are. + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + if (len_next > 0 && len_prev > 0) { + // "dir" is an unit vector perpendicular to the bisector of the angle created + // by the previous node, this auto node and the next node. + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + // Handle lengths are equal to 1/3 of the distance from the adjacent node. + _back.setRelativePos(-dir * (len_prev / 3)); + _front.setRelativePos(dir * (len_next / 3)); + } else { + // If any of the adjacent nodes coincides, retract both handles. + _front.retract(); + _back.retract(); + } +} + +void Node::showHandles(bool v) +{ + _handles_shown = v; + if (!_front.isDegenerate()) { + _front.setVisible(v); + } + if (!_back.isDegenerate()) { + _back.setVisible(v); + } + +} + +void Node::updateHandles() +{ + _handleControlStyling(); + + _front._handleControlStyling(); + _back._handleControlStyling(); +} + + +void Node::setType(NodeType type, bool update_handles) +{ + if (type == NODE_PICK_BEST) { + pickBestType(); + updateState(); // The size of the control might have changed + return; + } + + // if update_handles is true, adjust handle positions to match the node type + // handle degenerate handles appropriately + if (update_handles) { + switch (type) { + case NODE_CUSP: + // nothing to do + break; + case NODE_AUTO: + // auto handles make no sense for endnodes + if (isEndNode()) return; + _updateAutoHandles(); + break; + case NODE_SMOOTH: { + // ignore attempts to make smooth endnodes. + if (isEndNode()) return; + // rotate handles to be collinear + // for degenerate nodes set positions like auto handles + bool prev_line = _is_line_segment(_prev(), this); + bool next_line = _is_line_segment(this, _next()); + if (_type == NODE_SMOOTH) { + // For a node that is already smooth and has a degenerate handle, + // drag out the second handle without changing the direction of the first one. + if (_front.isDegenerate()) { + double dist = Geom::distance(_next()->position(), position()); + _front.setRelativePos(Geom::unit_vector(-_back.relativePos()) * dist / 3); + } + if (_back.isDegenerate()) { + double dist = Geom::distance(_prev()->position(), position()); + _back.setRelativePos(Geom::unit_vector(-_front.relativePos()) * dist / 3); + } + } else if (isDegenerate()) { + _updateAutoHandles(); + } else if (_front.isDegenerate()) { + // if the front handle is degenerate and next path segment is a line, make back collinear; + // otherwise, pull out the other handle to 1/3 of distance to prev. + if (next_line) { + _back.setDirection(*_next(), *this); + } else if (_prev()) { + Geom::Point dir = direction(_back, *this); + _front.setRelativePos(Geom::distance(_prev()->position(), position()) / 3 * dir); + } + } else if (_back.isDegenerate()) { + if (prev_line) { + _front.setDirection(*_prev(), *this); + } else if (_next()) { + Geom::Point dir = direction(_front, *this); + _back.setRelativePos(Geom::distance(_next()->position(), position()) / 3 * dir); + } + } else { + /* both handles are extended. make collinear while keeping length. + first make back collinear with the vector front ---> back, + then make front collinear with back ---> node. + (not back ---> front, because back's position was changed in the first call) */ + _back.setDirection(_front, _back); + _front.setDirection(_back, *this); + } + } break; + case NODE_SYMMETRIC: + if (isEndNode()) return; // symmetric handles make no sense for endnodes + if (isDegenerate()) { + // similar to auto handles but set the same length for both + Geom::Point vec_next = _next()->position() - position(); + Geom::Point vec_prev = _prev()->position() - position(); + double len_next = vec_next.length(), len_prev = vec_prev.length(); + double len = (len_next + len_prev) / 6; // take 1/3 of average + if (len == 0) return; + + Geom::Point dir = Geom::unit_vector((len_prev / len_next) * vec_next - vec_prev); + _back.setRelativePos(-dir * len); + _front.setRelativePos(dir * len); + } else { + // Both handles are extended. Compute average length, use direction from + // back handle to front handle. This also works correctly for degenerates + double len = (_front.length() + _back.length()) / 2; + Geom::Point dir = direction(_back, _front); + _front.setRelativePos(dir * len); + _back.setRelativePos(-dir * len); + } + break; + default: break; + } + // in node type changes, for BSpline traces, we can either maintain them + // with NO_POWER power in border mode, or give them the default power in curve mode. + if(_pm()._isBSpline()){ + double weight = NO_POWER; + if(_pm()._bsplineHandlePosition(this->front()) != NO_POWER ){ + weight = DEFAULT_START_POWER; + } + _front.setPosition(_pm()._bsplineHandleReposition(this->front(), weight)); + _back.setPosition(_pm()._bsplineHandleReposition(this->back(), weight)); + } + } + _type = type; + _setControlType(nodeTypeToCtrlType(_type)); + updateState(); +} + +void Node::pickBestType() +{ + _type = NODE_CUSP; + bool front_degen = _front.isDegenerate(); + bool back_degen = _back.isDegenerate(); + bool both_degen = front_degen && back_degen; + bool neither_degen = !front_degen && !back_degen; + do { + // if both handles are degenerate, do nothing + if (both_degen) break; + // if neither are degenerate, check their respective positions + if (neither_degen) { + // for now do not automatically make nodes symmetric, it can be annoying + /*if (Geom::are_near(front_delta, -back_delta)) { + _type = NODE_SYMMETRIC; + break; + }*/ + if (are_collinear_within_serializing_error(_front.position(), position(), _back.position())) { + _type = NODE_SMOOTH; + break; + } + } + // check whether the handle aligns with the previous line segment. + // we know that if front is degenerate, back isn't, because + // both_degen was false + if (front_degen && _next() && _next()->_back.isDegenerate()) { + if (are_collinear_within_serializing_error(_next()->position(), position(), _back.position())) { + _type = NODE_SMOOTH; + break; + } + } else if (back_degen && _prev() && _prev()->_front.isDegenerate()) { + if (are_collinear_within_serializing_error(_prev()->position(), position(), _front.position())) { + _type = NODE_SMOOTH; + break; + } + } + } while (false); + _setControlType(nodeTypeToCtrlType(_type)); + updateState(); +} + +bool Node::isEndNode() const +{ + return !_prev() || !_next(); +} + +void Node::sink() +{ + sp_canvas_item_move_to_z(_canvas_item, 0); +} + +NodeType Node::parse_nodetype(char x) +{ + switch (x) { + case 'a': return NODE_AUTO; + case 'c': return NODE_CUSP; + case 's': return NODE_SMOOTH; + case 'z': return NODE_SYMMETRIC; + default: return NODE_PICK_BEST; + } +} + +bool Node::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + int dir = 0; + + switch (event->type) + { + case GDK_SCROLL: + if (event->scroll.direction == GDK_SCROLL_UP) { + dir = 1; + } else if (event->scroll.direction == GDK_SCROLL_DOWN) { + dir = -1; + } else if (event->scroll.direction == GDK_SCROLL_SMOOTH) { + dir = event->scroll.delta_y > 0 ? -1 : 1; + } else { + break; + } + if (held_control(event->scroll)) { + _linearGrow(dir); + } else { + _selection.spatialGrow(this, dir); + } + return true; + case GDK_KEY_PRESS: + switch (shortcut_key(event->key)) + { + case GDK_KEY_Page_Up: + dir = 1; + break; + case GDK_KEY_Page_Down: + dir = -1; + break; + default: goto bail_out; + } + + if (held_control(event->key)) { + _linearGrow(dir); + } else { + _selection.spatialGrow(this, dir); + } + return true; + + default: + break; + } + + bail_out: + return ControlPoint::_eventHandler(event_context, event); +} + +void Node::_linearGrow(int dir) +{ + // Interestingly, we do not need any help from PathManipulator when doing linear grow. + // First handle the trivial case of growing over an unselected node. + if (!selected() && dir > 0) { + _selection.insert(this); + return; + } + + NodeList::iterator this_iter = NodeList::get_iterator(this); + NodeList::iterator fwd = this_iter, rev = this_iter; + double distance_back = 0, distance_front = 0; + + // Linear grow is simple. We find the first unselected nodes in each direction + // and compare the linear distances to them. + if (dir > 0) { + if (!selected()) { + _selection.insert(this); + return; + } + + // find first unselected nodes on both sides + while (fwd && fwd->selected()) { + NodeList::iterator n = fwd.next(); + distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + if (fwd == this_iter) + // there is no unselected node in this cyclic subpath + return; + } + // do the same for the second direction. Do not check for equality with + // this node, because there is at least one unselected node in the subpath, + // so we are guaranteed to stop. + while (rev && rev->selected()) { + NodeList::iterator p = rev.prev(); + distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + + NodeList::iterator t; // node to select + if (fwd && rev) { + if (distance_front <= distance_back) t = fwd; + else t = rev; + } else { + if (fwd) t = fwd; + if (rev) t = rev; + } + if (t) _selection.insert(t.ptr()); + + // Linear shrink is more complicated. We need to find the farthest selected node. + // This means we have to check the entire subpath. We go in the direction in which + // the distance we traveled is lower. We do this until we run out of nodes (ends of path) + // or the two iterators meet. On the way, we store the last selected node and its distance + // in each direction (if any). At the end, we choose the one that is farther and deselect it. + } else { + // both iterators that store last selected nodes are initially empty + NodeList::iterator last_fwd, last_rev; + double last_distance_back = 0, last_distance_front = 0; + + while (rev || fwd) { + if (fwd && (!rev || distance_front <= distance_back)) { + if (fwd->selected()) { + last_fwd = fwd; + last_distance_front = distance_front; + } + NodeList::iterator n = fwd.next(); + if (n) distance_front += Geom::bezier_length(*fwd, fwd->_front, n->_back, *n); + fwd = n; + } else if (rev && (!fwd || distance_front > distance_back)) { + if (rev->selected()) { + last_rev = rev; + last_distance_back = distance_back; + } + NodeList::iterator p = rev.prev(); + if (p) distance_back += Geom::bezier_length(*rev, rev->_back, p->_front, *p); + rev = p; + } + // Check whether we walked the entire cyclic subpath. + // This is initially true because both iterators start from this node, + // so this check cannot go in the while condition. + // When this happens, we need to check the last node, pointed to by the iterators. + if (fwd && fwd == rev) { + if (!fwd->selected()) break; + NodeList::iterator fwdp = fwd.prev(), revn = rev.next(); + double df = distance_front + Geom::bezier_length(*fwdp, fwdp->_front, fwd->_back, *fwd); + double db = distance_back + Geom::bezier_length(*revn, revn->_back, rev->_front, *rev); + if (df > db) { + last_fwd = fwd; + last_distance_front = df; + } else { + last_rev = rev; + last_distance_back = db; + } + break; + } + } + + NodeList::iterator t; + if (last_fwd && last_rev) { + if (last_distance_front >= last_distance_back) t = last_fwd; + else t = last_rev; + } else { + if (last_fwd) t = last_fwd; + if (last_rev) t = last_rev; + } + if (t) _selection.erase(t.ptr()); + } +} + +void Node::_setState(State state) +{ + // change node size to match type and selection state + ControlManager &mgr = ControlManager::getManager(); + mgr.setSelected(_canvas_item, selected()); + switch (state) { + case STATE_NORMAL: + mgr.setActive(_canvas_item, false); + mgr.setPrelight(_canvas_item, false); + break; + case STATE_MOUSEOVER: + mgr.setActive(_canvas_item, false); + mgr.setPrelight(_canvas_item, true); + break; + case STATE_CLICKED: + mgr.setActive(_canvas_item, true); + mgr.setPrelight(_canvas_item, false); + // show the handles when selecting the nodes + if(_pm()._isBSpline()){ + this->front()->setPosition(_pm()._bsplineHandleReposition(this->front())); + this->back()->setPosition(_pm()._bsplineHandleReposition(this->back())); + } + break; + } + SelectableControlPoint::_setState(state); +} + +bool Node::grabbed(GdkEventMotion *event) +{ + if (SelectableControlPoint::grabbed(event)) { + return true; + } + + // Dragging out handles with Shift + drag on a node. + if (!held_shift(*event)) { + return false; + } + + Geom::Point evp = event_point(*event); + Geom::Point rel_evp = evp - _last_click_event_point(); + + // This should work even if dragtolerance is zero and evp coincides with node position. + double angle_next = HUGE_VAL; + double angle_prev = HUGE_VAL; + bool has_degenerate = false; + // determine which handle to drag out based on degeneration and the direction of drag + if (_front.isDegenerate() && _next()) { + Geom::Point next_relpos = _desktop->d2w(_next()->position()) + - _desktop->d2w(position()); + angle_next = fabs(Geom::angle_between(rel_evp, next_relpos)); + has_degenerate = true; + } + if (_back.isDegenerate() && _prev()) { + Geom::Point prev_relpos = _desktop->d2w(_prev()->position()) + - _desktop->d2w(position()); + angle_prev = fabs(Geom::angle_between(rel_evp, prev_relpos)); + has_degenerate = true; + } + if (!has_degenerate) { + return false; + } + + Handle *h = angle_next < angle_prev ? &_front : &_back; + + h->setPosition(_desktop->w2d(evp)); + h->setVisible(true); + h->transferGrab(this, event); + Handle::_drag_out = true; + return true; +} + +void Node::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + // For a note on how snapping is implemented in Inkscape, see snap.h. + SnapManager &sm = _desktop->namedview->snap_manager; + // even if we won't really snap, we might still call the one of the + // constrainedSnap() methods to enforce the constraints, so we need + // to setup the snapmanager anyway; this is also required for someSnapperMightSnap() + sm.setup(_desktop); + + // do not snap when Shift is pressed + bool snap = !held_shift(*event) && sm.someSnapperMightSnap(); + + Inkscape::SnappedPoint sp; + std::vector<Inkscape::SnapCandidatePoint> unselected; + if (snap) { + /* setup + * TODO We are doing this every time a snap happens. It should once be done only once + * per drag - maybe in the grabbed handler? + * TODO Unselected nodes vector must be valid during the snap run, because it is not + * copied. Fix this in snap.h and snap.cpp, then the above. + * TODO Snapping to unselected segments of selected paths doesn't work yet. */ + + // Build the list of unselected nodes. + typedef ControlPointSelection::Set Set; + Set &nodes = _selection.allPoints(); + for (auto node : nodes) { + if (!node->selected()) { + Node *n = static_cast<Node*>(node); + Inkscape::SnapCandidatePoint p(n->position(), n->_snapSourceType(), n->_snapTargetType()); + unselected.push_back(p); + } + } + sm.unSetup(); + sm.setupIgnoreSelection(_desktop, true, &unselected); + } + + // Snap candidate point for free snapping; this will consider snapping tangentially + // and perpendicularly and therefore the origin or direction vector must be set + Inkscape::SnapCandidatePoint scp_free(new_pos, _snapSourceType()); + + boost::optional<Geom::Point> front_point, back_point; + Geom::Point origin = _last_drag_origin(); + Geom::Point dummy_cp; + if (_front.isDegenerate()) { + if (_is_line_segment(this, _next())) { + front_point = _next()->position() - origin; + if (_next()->selected()) { + dummy_cp = _next()->position() - position(); + scp_free.addVector(dummy_cp); + } else { + dummy_cp = _next()->position(); + scp_free.addOrigin(dummy_cp); + } + } + } else { + front_point = _front.relativePos(); + scp_free.addVector(*front_point); + } + if (_back.isDegenerate()) { + if (_is_line_segment(_prev(), this)) { + back_point = _prev()->position() - origin; + if (_prev()->selected()) { + dummy_cp = _prev()->position() - position(); + scp_free.addVector(dummy_cp); + } else { + dummy_cp = _prev()->position(); + scp_free.addOrigin(dummy_cp); + } + } + } else { + back_point = _back.relativePos(); + scp_free.addVector(*back_point); + } + + if (held_control(*event)) { + // We're about to consider a constrained snap, which is already limited to 1D + // Therefore tangential or perpendicular snapping will not be considered, and therefore + // all calls above to scp_free.addVector() and scp_free.addOrigin() can be neglected + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + if (held_alt(*event)) { + // with Ctrl+Alt, constrain to handle lines + // project the new position onto a handle line that is closer; + // also snap to perpendiculars of handle lines + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + double min_angle = M_PI / snaps; + + boost::optional<Geom::Point> fperp_point, bperp_point; + if (front_point) { + constraints.emplace_back(origin, *front_point); + fperp_point = Geom::rot90(*front_point); + } + if (back_point) { + constraints.emplace_back(origin, *back_point); + bperp_point = Geom::rot90(*back_point); + } + // perpendiculars only snap when they are further than snap increment away + // from the second handle constraint + if (fperp_point && (!back_point || + (fabs(Geom::angle_between(*fperp_point, *back_point)) > min_angle && + fabs(Geom::angle_between(*fperp_point, *back_point)) < M_PI - min_angle))) + { + constraints.emplace_back(origin, *fperp_point); + } + if (bperp_point && (!front_point || + (fabs(Geom::angle_between(*bperp_point, *front_point)) > min_angle && + fabs(Geom::angle_between(*bperp_point, *front_point)) < M_PI - min_angle))) + { + constraints.emplace_back(origin, *bperp_point); + } + + sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event)); + } else { + // with Ctrl, constrain to axes + constraints.emplace_back(origin, Geom::Point(1, 0)); + constraints.emplace_back(origin, Geom::Point(0, 1)); + sp = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, _snapSourceType()), constraints, held_shift(*event)); + } + new_pos = sp.getPoint(); + } else if (snap) { + Inkscape::SnappedPoint sp = sm.freeSnap(scp_free); + new_pos = sp.getPoint(); + } + + sm.unSetup(); + + SelectableControlPoint::dragged(new_pos, event); +} + +bool Node::clicked(GdkEventButton *event) +{ + if(_pm()._nodeClicked(this, event)) + return true; + return SelectableControlPoint::clicked(event); +} + +Inkscape::SnapSourceType Node::_snapSourceType() const +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPSOURCE_NODE_SMOOTH; + return SNAPSOURCE_NODE_CUSP; +} +Inkscape::SnapTargetType Node::_snapTargetType() const +{ + if (_type == NODE_SMOOTH || _type == NODE_AUTO) + return SNAPTARGET_NODE_SMOOTH; + return SNAPTARGET_NODE_CUSP; +} + +Inkscape::SnapCandidatePoint Node::snapCandidatePoint() +{ + return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType()); +} + +Handle *Node::handleToward(Node *to) +{ + if (_next() == to) { + return front(); + } + if (_prev() == to) { + return back(); + } + g_error("Node::handleToward(): second node is not adjacent!"); + return nullptr; +} + +Node *Node::nodeToward(Handle *dir) +{ + if (front() == dir) { + return _next(); + } + if (back() == dir) { + return _prev(); + } + g_error("Node::nodeToward(): handle is not a child of this node!"); + return nullptr; +} + +Handle *Node::handleAwayFrom(Node *to) +{ + if (_next() == to) { + return back(); + } + if (_prev() == to) { + return front(); + } + g_error("Node::handleAwayFrom(): second node is not adjacent!"); + return nullptr; +} + +Node *Node::nodeAwayFrom(Handle *h) +{ + if (front() == h) { + return _prev(); + } + if (back() == h) { + return _next(); + } + g_error("Node::nodeAwayFrom(): handle is not a child of this node!"); + return nullptr; +} + +Glib::ustring Node::_getTip(unsigned state) const +{ + bool isBSpline = _pm()._isBSpline(); + Handle *h = const_cast<Handle *>(&_front); + Glib::ustring s = C_("Path node tip", + "node handle"); // not expected + + if (state_held_shift(state)) { + bool can_drag_out = (_next() && _front.isDegenerate()) || + (_prev() && _back.isDegenerate()); + + if (can_drag_out) { + /*if (state_held_control(state)) { + s = format_tip(C_("Path node tip", + "<b>Shift+Ctrl:</b> drag out a handle and snap its angle " + "to %f° increments"), snap_increment_degrees()); + }*/ + s = C_("Path node tip", + "<b>Shift</b>: drag out a handle, click to toggle selection"); + } + else { + s = C_("Path node tip", + "<b>Shift</b>: click to toggle selection"); + } + } + + else if (state_held_control(state)) { + if (state_held_alt(state)) { + s = C_("Path node tip", + "<b>Ctrl+Alt</b>: move along handle lines, click to delete node"); + } + else { + s = C_("Path node tip", + "<b>Ctrl</b>: move along axes, click to change node type"); + } + } + + else if (state_held_alt(state)) { + s = C_("Path node tip", + "<b>Alt</b>: sculpt nodes"); + } + + else { // No modifiers: assemble tip from node type + char const *nodetype = node_type_to_localized_string(_type); + double power = _pm()._bsplineHandlePosition(h); + + if (_selection.transformHandlesEnabled() && selected()) { + if (_selection.size() == 1) { + if (!isBSpline) { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + else { + s = format_tip(C_("Path node tip", + "<b>BSpline node</b> (%.3g power): " + "drag to shape the path" ". " + "(more: Shift, Ctrl, Alt)"), + power); + } + } + else { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "click to toggle scale/rotation handles" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + } + else if (!isBSpline) { + s = format_tip(C_("Path node tip", + "<b>%s</b>: " + "drag to shape the path" ", " + "click to select only this node" ". " + "(more: Shift, Ctrl, Alt)"), + nodetype); + } + else { + s = format_tip(C_("Path node tip", + "<b>BSpline node</b> (%.3g power): " + "drag to shape the path" ", " + "click to select only this node" ". " + "(more: Shift, Ctrl, Alt)"), + power); + } + } + + return (s); +} + +Glib::ustring Node::_getDragTip(GdkEventMotion */*event*/) const +{ + Geom::Point dist = position() - _last_drag_origin(); + + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(dist[Geom::X], "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(dist[Geom::Y], "px"); + Glib::ustring x = x_q.string(_desktop->namedview->display_units); + Glib::ustring y = y_q.string(_desktop->namedview->display_units); + Glib::ustring ret = format_tip(C_("Path node tip", "Move node by %s, %s"), x.c_str(), y.c_str()); + return ret; +} + +/** + * See also: Handle::handle_type_to_localized_string(NodeType type) + */ +char const *Node::node_type_to_localized_string(NodeType type) +{ + switch (type) { + case NODE_CUSP: + return _("Corner node"); + case NODE_SMOOTH: + return _("Smooth node"); + case NODE_SYMMETRIC: + return _("Symmetric node"); + case NODE_AUTO: + return _("Auto-smooth node"); + default: + return ""; + } +} + +bool Node::_is_line_segment(Node *first, Node *second) +{ + if (!first || !second) return false; + if (first->_next() == second) + return first->_front.isDegenerate() && second->_back.isDegenerate(); + if (second->_next() == first) + return second->_front.isDegenerate() && first->_back.isDegenerate(); + return false; +} + +NodeList::NodeList(SubpathList &splist) + : _list(splist) + , _closed(false) +{ + this->ln_list = this; + this->ln_next = this; + this->ln_prev = this; +} + +NodeList::~NodeList() +{ + clear(); +} + +bool NodeList::empty() +{ + return ln_next == this; +} + +NodeList::size_type NodeList::size() +{ + size_type sz = 0; + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_next) ++sz; + return sz; +} + +bool NodeList::closed() +{ + return _closed; +} + +bool NodeList::degenerate() +{ + return closed() ? empty() : ++begin() == end(); +} + +NodeList::iterator NodeList::before(double t, double *fracpart) +{ + double intpart; + *fracpart = std::modf(t, &intpart); + int index = intpart; + + iterator ret = begin(); + std::advance(ret, index); + return ret; +} + +NodeList::iterator NodeList::before(Geom::PathTime const &pvp) +{ + iterator ret = begin(); + std::advance(ret, pvp.curve_index); + return ret; +} + +NodeList::iterator NodeList::insert(iterator pos, Node *x) +{ + ListNode *ins = pos._node; + x->ln_next = ins; + x->ln_prev = ins->ln_prev; + ins->ln_prev->ln_next = x; + ins->ln_prev = x; + x->ln_list = this; + return iterator(x); +} + +void NodeList::splice(iterator pos, NodeList &list) +{ + splice(pos, list, list.begin(), list.end()); +} + +void NodeList::splice(iterator pos, NodeList &list, iterator i) +{ + NodeList::iterator j = i; + ++j; + splice(pos, list, i, j); +} + +void NodeList::splice(iterator pos, NodeList &/*list*/, iterator first, iterator last) +{ + ListNode *ins_beg = first._node, *ins_end = last._node, *at = pos._node; + for (ListNode *ln = ins_beg; ln != ins_end; ln = ln->ln_next) { + ln->ln_list = this; + } + ins_beg->ln_prev->ln_next = ins_end; + ins_end->ln_prev->ln_next = at; + at->ln_prev->ln_next = ins_beg; + + ListNode *atprev = at->ln_prev; + at->ln_prev = ins_end->ln_prev; + ins_end->ln_prev = ins_beg->ln_prev; + ins_beg->ln_prev = atprev; +} + +void NodeList::shift(int n) +{ + // 1. make the list perfectly cyclic + ln_next->ln_prev = ln_prev; + ln_prev->ln_next = ln_next; + // 2. find new begin + ListNode *new_begin = ln_next; + if (n > 0) { + for (; n > 0; --n) new_begin = new_begin->ln_next; + } else { + for (; n < 0; ++n) new_begin = new_begin->ln_prev; + } + // 3. relink begin to list + ln_next = new_begin; + ln_prev = new_begin->ln_prev; + new_begin->ln_prev->ln_next = this; + new_begin->ln_prev = this; +} + +void NodeList::reverse() +{ + for (ListNode *ln = ln_next; ln != this; ln = ln->ln_prev) { + std::swap(ln->ln_next, ln->ln_prev); + Node *node = static_cast<Node*>(ln); + Geom::Point save_pos = node->front()->position(); + node->front()->setPosition(node->back()->position()); + node->back()->setPosition(save_pos); + } + std::swap(ln_next, ln_prev); +} + +void NodeList::clear() +{ + // ugly but more efficient clearing mechanism + std::vector<ControlPointSelection *> to_clear; + std::vector<std::pair<SelectableControlPoint *, long> > nodes; + long in = -1; + for (iterator i = begin(); i != end(); ++i) { + SelectableControlPoint *rm = static_cast<Node*>(i._node); + if (std::find(to_clear.begin(), to_clear.end(), &rm->_selection) == to_clear.end()) { + to_clear.push_back(&rm->_selection); + ++in; + } + nodes.emplace_back(rm, in); + } + for (size_t i = 0, e = nodes.size(); i != e; ++i) { + to_clear[nodes[i].second]->erase(nodes[i].first, false); + } + std::vector<std::vector<SelectableControlPoint *> > emission; + for (long i = 0, e = to_clear.size(); i != e; ++i) { + emission.emplace_back(); + for (size_t j = 0, f = nodes.size(); j != f; ++j) { + if (nodes[j].second != i) + break; + emission[i].push_back(nodes[j].first); + } + } + + for (size_t i = 0, e = emission.size(); i != e; ++i) { + to_clear[i]->signal_selection_changed.emit(emission[i], false); + } + + for (iterator i = begin(); i != end();) + erase (i++); +} + +NodeList::iterator NodeList::erase(iterator i) +{ + // some gymnastics are required to ensure that the node is valid when deleted; + // otherwise the code that updates handle visibility will break + Node *rm = static_cast<Node*>(i._node); + ListNode *rmnext = rm->ln_next, *rmprev = rm->ln_prev; + ++i; + delete rm; + rmprev->ln_next = rmnext; + rmnext->ln_prev = rmprev; + return i; +} + +// TODO this method is very ugly! +// converting SubpathList to an intrusive list might allow us to get rid of it +void NodeList::kill() +{ + for (SubpathList::iterator i = _list.begin(); i != _list.end(); ++i) { + if (i->get() == this) { + _list.erase(i); + return; + } + } +} + +NodeList &NodeList::get(Node *n) { + return n->nodeList(); +} +NodeList &NodeList::get(iterator const &i) { + return *(i._node->ln_list); +} + + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/node.h b/src/ui/tool/node.h new file mode 100644 index 0000000..d4e09ca --- /dev/null +++ b/src/ui/tool/node.h @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Editable node and associated data structures. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_H +#define SEEN_UI_TOOL_NODE_H + +#include <iterator> +#include <iosfwd> +#include <stdexcept> +#include <cstddef> +#include <functional> + +#include "ui/tool/selectable-control-point.h" +#include "snapped-point.h" +#include "ui/tool/node-types.h" + +struct SPCtrlLine; + +namespace Inkscape { +namespace UI { +template <typename> class NodeIterator; +} +} + +namespace Inkscape { +namespace UI { + +class PathManipulator; +class MultiPathManipulator; + +class Node; +class Handle; +class NodeList; +class SubpathList; +template <typename> class NodeIterator; + +std::ostream &operator<<(std::ostream &, NodeType); + +/* +template <typename T> +struct ListMember { + T *next; + T *prev; +}; +struct SubpathMember : public ListMember<NodeListMember> { + Subpath *list; +}; +struct SubpathListMember : public ListMember<SubpathListMember> { + SubpathList *list; +}; +*/ + +struct ListNode { + ListNode *ln_next; + ListNode *ln_prev; + NodeList *ln_list; +}; + +struct NodeSharedData { + SPDesktop *desktop; + ControlPointSelection *selection; + SPCanvasGroup *node_group; + SPCanvasGroup *handle_group; + SPCanvasGroup *handle_line_group; +}; + +class Handle : public ControlPoint { +public: + + ~Handle() override; + inline Geom::Point relativePos() const; + inline double length() const; + bool isDegenerate() const { return _degenerate; } // True if the handle is retracted, i.e. has zero length. + + void setVisible(bool) override; + void move(Geom::Point const &p) override; + + void setPosition(Geom::Point const &p) override; + inline void setRelativePos(Geom::Point const &p); + void setLength(double len); + void retract(); + void setDirection(Geom::Point const &from, Geom::Point const &to); + void setDirection(Geom::Point const &dir); + Node *parent() { return _parent; } + Handle *other(); + Handle const *other() const; + + static char const *handle_type_to_localized_string(NodeType type); + +protected: + + Handle(NodeSharedData const &data, Geom::Point const &initial_pos, Node *parent); + virtual void handle_2button_press(); + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *event) override; + bool clicked(GdkEventButton *event) override; + + Glib::ustring _getTip(unsigned state) const override; + Glib::ustring _getDragTip(GdkEventMotion *event) const override; + bool _hasDragTips() const override { return true; } + +private: + + inline PathManipulator &_pm(); + inline PathManipulator &_pm() const; + Node *_parent; // the handle's lifetime does not extend beyond that of the parent node, + // so a naked pointer is OK and allows setting it during Node's construction + SPCtrlLine *_handle_line; + bool _degenerate; // True if the handle is retracted, i.e. has zero length. This is used often internally so it makes sense to cache this + + /** + * Control point of a cubic Bezier curve in a path. + * + * Handle keeps the node type invariant only for the opposite handle of the same node. + * Keeping the invariant on node moves is left to the %Node class. + */ + static Geom::Point _saved_other_pos; + + static double _saved_length; + static bool _drag_out; + static ColorSet _handle_colors; + friend class Node; +}; + +class Node : ListNode, public SelectableControlPoint { +public: + + /** + * Curve endpoint in an editable path. + * + * The method move() keeps node type invariants during translations. + */ + Node(NodeSharedData const &data, Geom::Point const &pos); + + Node(Node const &) = delete; + + void move(Geom::Point const &p) override; + void transform(Geom::Affine const &m) override; + Geom::Rect bounds() const override; + + NodeType type() const { return _type; } + + /** + * Sets the node type and optionally restores the invariants associated with the given type. + * @param type The type to set. + * @param update_handles Whether to restore invariants associated with the given type. + * Passing false is useful e.g. when initially creating the path, + * and when making cusp nodes during some node algorithms. + * Pass true when used in response to an UI node type button. + */ + void setType(NodeType type, bool update_handles = true); + + void showHandles(bool v); + + void updateHandles(); + + + /** + * Pick the best type for this node, based on the position of its handles. + * This is what assigns types to nodes created using the pen tool. + */ + void pickBestType(); // automatically determine the type from handle positions + + bool isDegenerate() const { return _front.isDegenerate() && _back.isDegenerate(); } + bool isEndNode() const; + Handle *front() { return &_front; } + Handle *back() { return &_back; } + + /** + * Gets the handle that faces the given adjacent node. + * Will abort with error if the given node is not adjacent. + */ + Handle *handleToward(Node *to); + + /** + * Gets the node in the direction of the given handle. + * Will abort with error if the handle doesn't belong to this node. + */ + Node *nodeToward(Handle *h); + + /** + * Gets the handle that goes in the direction opposite to the given adjacent node. + * Will abort with error if the given node is not adjacent. + */ + Handle *handleAwayFrom(Node *to); + + /** + * Gets the node in the direction opposite to the given handle. + * Will abort with error if the handle doesn't belong to this node. + */ + Node *nodeAwayFrom(Handle *h); + + NodeList &nodeList() { return *(static_cast<ListNode*>(this)->ln_list); } + NodeList &nodeList() const { return *(static_cast<ListNode const*>(this)->ln_list); } + + /** + * Move the node to the bottom of its canvas group. + * Useful for node break, to ensure that the selected nodes are above the unselected ones. + */ + void sink(); + + static NodeType parse_nodetype(char x); + static char const *node_type_to_localized_string(NodeType type); + + // temporarily public + /** Customized event handler to catch scroll events needed for selection grow/shrink. */ + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override; + + Inkscape::SnapCandidatePoint snapCandidatePoint(); + +protected: + + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + bool clicked(GdkEventButton *event) override; + + void _setState(State state) override; + Glib::ustring _getTip(unsigned state) const override; + Glib::ustring _getDragTip(GdkEventMotion *event) const override; + bool _hasDragTips() const override { return true; } + +private: + + void _fixNeighbors(Geom::Point const &old_pos, Geom::Point const &new_pos); + void _updateAutoHandles(); + + /** + * Select or deselect a node in this node's subpath based on its path distance from this node. + * @param dir If negative, shrink selection by one node; if positive, grow by one node. + */ + void _linearGrow(int dir); + + Node *_next(); + Node const *_next() const; + Node *_prev(); + Node const *_prev() const; + Inkscape::SnapSourceType _snapSourceType() const; + Inkscape::SnapTargetType _snapTargetType() const; + inline PathManipulator &_pm(); + inline PathManipulator &_pm() const; + + /** Determine whether two nodes are joined by a linear segment. */ + static bool _is_line_segment(Node *first, Node *second); + + // Handles are always present, but are not visible if they coincide with the node + // (are degenerate). A segment that has both handles degenerate is always treated + // as a line segment + Handle _front; ///< Node handle in the backward direction of the path + Handle _back; ///< Node handle in the forward direction of the path + NodeType _type; ///< Type of node - cusp, smooth... + bool _handles_shown; + static ColorSet node_colors; + + friend class Handle; + friend class NodeList; + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/// Iterator for editable nodes +/** Use this class for all operations that require some knowledge about the node's + * neighbors. It is a bidirectional iterator. + * + * Because paths can be cyclic, node iterators have two different ways to + * increment and decrement them. When using ++/--, the end iterator will eventually + * be returned. When using advance()/retreat(), the end iterator will only be returned + * when the path is open. If it's closed, calling advance() will cycle indefinitely. + * This is particularly useful for cases where the adjacency of nodes is more important + * than their sequence order. + * + * When @a i is a node iterator, then: + * - <code>++i</code> moves the iterator to the next node in sequence order; + * - <code>--i</code> moves the iterator to the previous node in sequence order; + * - <code>i.next()</code> returns the next node with wrap-around; + * - <code>i.prev()</code> returns the previous node with wrap-around; + * - <code>i.advance()</code> moves the iterator to the next node with wrap-around; + * - <code>i.retreat()</code> moves the iterator to the previous node with wrap-around. + * + * next() and prev() do not change their iterator. They can return the end iterator + * if the path is open. + * + * Unlike most other iterators, you can check whether you've reached the end of the list + * without having access to the iterator's container. + * Simply use <code>if (i) { ...</code> + * */ +template <typename N> +class NodeIterator + : public boost::bidirectional_iterator_helper<NodeIterator<N>, N, std::ptrdiff_t, + N *, N &> +{ +public: + typedef NodeIterator self; + NodeIterator() + : _node(nullptr) + {} + // default copy, default assign + + self &operator++() { + _node = (_node?_node->ln_next:nullptr); + return *this; + } + self &operator--() { + _node = (_node?_node->ln_prev:nullptr); + return *this; + } + bool operator==(self const &other) const { return _node == other._node; } + N &operator*() const { return *static_cast<N*>(_node); } + inline operator bool() const; // define after NodeList + /// Get a pointer to the underlying node. Equivalent to <code>&*i</code>. + N *get_pointer() const { return static_cast<N*>(_node); } + /// @see get_pointer() + N *ptr() const { return static_cast<N*>(_node); } + + self next() const { + self r(*this); + r.advance(); + return r; + } + self prev() const { + self r(*this); + r.retreat(); + return r; + } + self &advance(); + self &retreat(); +private: + NodeIterator(ListNode const *n) + : _node(const_cast<ListNode*>(n)) + {} + ListNode *_node; + friend class NodeList; +}; + +class NodeList : ListNode, boost::noncopyable { +public: + typedef std::size_t size_type; + typedef Node &reference; + typedef Node const &const_reference; + typedef Node *pointer; + typedef Node const *const_pointer; + typedef Node value_type; + typedef NodeIterator<value_type> iterator; + typedef NodeIterator<value_type const> const_iterator; + + // TODO Lame. Make this private and make SubpathList a factory + /** + * An editable list of nodes representing a subpath. + * + * It can optionally be cyclic to represent a closed path. + * The list has iterators that act like plain node iterators, but can also be used + * to obtain shared pointers to nodes. + */ + NodeList(SubpathList &_list); + + ~NodeList(); + + // no copy or assign + NodeList(NodeList const &) = delete; + void operator=(NodeList const &) = delete; + + // iterators + iterator begin() { return iterator(ln_next); } + iterator end() { return iterator(this); } + const_iterator begin() const { return const_iterator(ln_next); } + const_iterator end() const { return const_iterator(this); } + + // size + bool empty(); + size_type size(); + + // extra node-specific methods + bool closed(); + + /** + * A subpath is degenerate if it has no segments - either one node in an open path + * or no nodes in a closed path. + */ + bool degenerate(); + + void setClosed(bool c) { _closed = c; } + iterator before(double t, double *fracpart = nullptr); + iterator before(Geom::PathTime const &pvp); + const_iterator before(double t, double *fracpart = nullptr) const { + return const_cast<NodeList *>(this)->before(t, fracpart)._node; + } + const_iterator before(Geom::PathTime const &pvp) const { + return const_cast<NodeList *>(this)->before(pvp)._node; + } + + // list operations + + /** insert a node before pos. */ + iterator insert(iterator pos, Node *x); + + template <class InputIterator> + void insert(iterator pos, InputIterator first, InputIterator last) { + for (; first != last; ++first) insert(pos, *first); + } + void splice(iterator pos, NodeList &list); + void splice(iterator pos, NodeList &list, iterator i); + void splice(iterator pos, NodeList &list, iterator first, iterator last); + void reverse(); + void shift(int n); + void push_front(Node *x) { insert(begin(), x); } + void pop_front() { erase(begin()); } + void push_back(Node *x) { insert(end(), x); } + void pop_back() { erase(--end()); } + void clear(); + iterator erase(iterator pos); + iterator erase(iterator first, iterator last) { + NodeList::iterator ret = first; + while (first != last) ret = erase(first++); + return ret; + } + + // member access - undefined results when the list is empty + Node &front() { return *static_cast<Node*>(ln_next); } + Node &back() { return *static_cast<Node*>(ln_prev); } + + // HACK remove this subpath from its path. This will be removed later. + void kill(); + SubpathList &subpathList() { return _list; } + + static iterator get_iterator(Node *n) { return iterator(n); } + static const_iterator get_iterator(Node const *n) { return const_iterator(n); } + static NodeList &get(Node *n); + static NodeList &get(iterator const &i); +private: + + SubpathList &_list; + bool _closed; + + friend class Node; + friend class Handle; // required to access handle and handle line groups + friend class NodeIterator<Node>; + friend class NodeIterator<Node const>; +}; + +/** + * List of node lists. Represents an editable path. + * Editable path composed of one or more subpaths. + */ +class SubpathList : public std::list< std::shared_ptr<NodeList> > { +public: + typedef std::list< std::shared_ptr<NodeList> > list_type; + + SubpathList(PathManipulator &pm) : _path_manipulator(pm) {} + PathManipulator &pm() { return _path_manipulator; } + +private: + list_type _nodelists; + PathManipulator &_path_manipulator; + friend class NodeList; + friend class Node; + friend class Handle; +}; + + + +// define inline Handle funcs after definition of Node +inline Geom::Point Handle::relativePos() const { + return position() - _parent->position(); +} +inline void Handle::setRelativePos(Geom::Point const &p) { + setPosition(_parent->position() + p); +} +inline double Handle::length() const { + return relativePos().length(); +} +inline PathManipulator &Handle::_pm() { + return _parent->_pm(); +} +inline PathManipulator &Handle::_pm() const { + return _parent->_pm(); +} +inline PathManipulator &Node::_pm() { + return nodeList().subpathList().pm(); +} + +inline PathManipulator &Node::_pm() const { + return nodeList().subpathList().pm(); +} + +// definitions for node iterator +template <typename N> +NodeIterator<N>::operator bool() const { + return _node && static_cast<ListNode*>(_node->ln_list) != _node; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::advance() { + ++(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) ++(*this); + return *this; +} +template <typename N> +NodeIterator<N> &NodeIterator<N>::retreat() { + --(*this); + if (G_UNLIKELY(!*this) && _node->ln_list->closed()) --(*this); + return *this; +} + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/path-manipulator.cpp b/src/ui/tool/path-manipulator.cpp new file mode 100644 index 0000000..e5b319b --- /dev/null +++ b/src/ui/tool/path-manipulator.cpp @@ -0,0 +1,1756 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Path manipulator - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/bezier-utils.h> +#include <2geom/path-sink.h> + +#include <utility> + +#include "display/sp-canvas.h" +#include "display/sp-canvas-util.h" +#include "display/curve.h" +#include "display/canvas-bpath.h" + +#include "helper/geom.h" + +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/parameter/path.h" + +#include "object/sp-path.h" +#include "style.h" + +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" + +#include "xml/node-observer.h" + +namespace Inkscape { +namespace UI { + +namespace { +/// Types of path changes that we must react to. +enum PathChange { + PATH_CHANGE_D, + PATH_CHANGE_TRANSFORM +}; + +} // anonymous namespace +const double HANDLE_CUBIC_GAP = 0.001; +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; + + +/** + * Notifies the path manipulator when something changes the path being edited + * (e.g. undo / redo) + */ +class PathManipulatorObserver : public Inkscape::XML::NodeObserver { +public: + PathManipulatorObserver(PathManipulator *p, Inkscape::XML::Node *node) + : _pm(p) + , _node(node) + , _blocked(false) + { + Inkscape::GC::anchor(_node); + _node->addObserver(*this); + } + + ~PathManipulatorObserver() override { + _node->removeObserver(*this); + Inkscape::GC::release(_node); + } + + void notifyAttributeChanged(Inkscape::XML::Node &/*node*/, GQuark attr, + Util::ptr_shared, Util::ptr_shared) override + { + // do nothing if blocked + if (_blocked) return; + + GQuark path_d = g_quark_from_static_string("d"); + GQuark path_transform = g_quark_from_static_string("transform"); + GQuark lpe_quark = _pm->_lpe_key.empty() ? 0 : g_quark_from_string(_pm->_lpe_key.data()); + + // only react to "d" (path data) and "transform" attribute changes + if (attr == lpe_quark || attr == path_d) { + _pm->_externalChange(PATH_CHANGE_D); + } else if (attr == path_transform) { + _pm->_externalChange(PATH_CHANGE_TRANSFORM); + } + } + + void block() { _blocked = true; } + void unblock() { _blocked = false; } +private: + PathManipulator *_pm; + Inkscape::XML::Node *_node; + bool _blocked; +}; + +void build_segment(Geom::PathBuilder &, Node *, Node *); +PathManipulator::PathManipulator(MultiPathManipulator &mpm, SPObject *path, + Geom::Affine const &et, guint32 outline_color, Glib::ustring lpe_key) + : PointManipulator(mpm._path_data.node_data.desktop, *mpm._path_data.node_data.selection) + , _subpaths(*this) + , _multi_path_manipulator(mpm) + , _path(path) + , _spcurve(new SPCurve()) + , _dragpoint(new CurveDragPoint(*this)) + , /* XML Tree being used here directly while it shouldn't be*/_observer(new PathManipulatorObserver(this, path->getRepr())) + , _edit_transform(et) + , _show_handles(true) + , _show_outline(false) + , _show_path_direction(false) + , _live_outline(true) + , _live_objects(true) + , _is_bspline(false) + , _lpe_key(std::move(lpe_key)) +{ + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *pathshadow = dynamic_cast<SPPath *>(_path); + if (!lpeobj) { + _i2d_transform = pathshadow->i2dt_affine(); + } else { + _i2d_transform = Geom::identity(); + } + _d2i_transform = _i2d_transform.inverse(); + _dragpoint->setVisible(false); + + _getGeometry(); + + _outline = sp_canvas_bpath_new(_multi_path_manipulator._path_data.outline_group, nullptr); + sp_canvas_item_hide(_outline); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(_outline), outline_color, 1.0, + SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(_outline), 0, SP_WIND_RULE_NONZERO); + + _selection.signal_update.connect( + sigc::bind(sigc::mem_fun(*this, &PathManipulator::update), false)); + _selection.signal_selection_changed.connect( + sigc::mem_fun(*this, &PathManipulator::_selectionChangedM)); + _desktop->signal_zoom_changed.connect( + sigc::hide( sigc::mem_fun(*this, &PathManipulator::_updateOutlineOnZoomChange))); + + _createControlPointsFromGeometry(); + //Define if the path is BSpline on construction + _recalculateIsBSpline(); +} + +PathManipulator::~PathManipulator() +{ + delete _dragpoint; + delete _observer; + sp_canvas_item_destroy(_outline); + _spcurve->unref(); + clear(); +} + +/** Handle motion events to update the position of the curve drag point. */ +bool PathManipulator::event(Inkscape::UI::Tools::ToolBase * /*event_context*/, GdkEvent *event) +{ + if (empty()) return false; + + switch (event->type) + { + case GDK_MOTION_NOTIFY: + _updateDragPoint(event_point(event->motion)); + break; + default: + break; + } + return false; +} + +/** Check whether the manipulator has any nodes. */ +bool PathManipulator::empty() { + return !_path || _subpaths.empty(); +} + +/** Update the display and the outline of the path. + * \param alert_LPE if true, alerts an applied LPE to what the path is going to be changed to, so it can adjust its parameters for nicer user interfacing + */ +void PathManipulator::update(bool alert_LPE) +{ + _createGeometryFromControlPoints(alert_LPE); +} + +/** Store the changes to the path in XML. */ +void PathManipulator::writeXML() +{ + if (!_live_outline) + _updateOutline(); + + _setGeometry(); + + if (_path) { + _observer->block(); + if (!empty()) { + _path->updateRepr(); + _getXMLNode()->setAttribute(_nodetypesKey(), _createTypeString()); + } + else { + // this manipulator will have to be destroyed right after this call + _getXMLNode()->removeObserver(*_observer); + _path->deleteObject(true, true); + _path = nullptr; + } + _observer->unblock(); + } +} + +/** Remove all nodes from the path. */ +void PathManipulator::clear() +{ + // no longer necessary since nodes remove themselves from selection on destruction + //_removeNodesFromSelection(); + _subpaths.clear(); +} + +/** Select all nodes in subpaths that have something selected. */ +void PathManipulator::selectSubpaths() +{ + for (auto & _subpath : _subpaths) { + NodeList::iterator sp_start = _subpath->begin(), sp_end = _subpath->end(); + for (NodeList::iterator j = sp_start; j != sp_end; ++j) { + if (j->selected()) { + // if at least one of the nodes from this subpath is selected, + // select all nodes from this subpath + for (NodeList::iterator ins = sp_start; ins != sp_end; ++ins) + _selection.insert(ins.ptr()); + continue; + } + } + } +} + +/** Invert selection in the selected subpaths. */ +void PathManipulator::invertSelectionInSubpaths() +{ + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + // found selected node - invert selection in this subpath + for (NodeList::iterator k = _subpath->begin(); k != _subpath->end(); ++k) { + if (k->selected()) _selection.erase(k.ptr()); + else _selection.insert(k.ptr()); + } + // next subpath + break; + } + } + } +} + +/** Insert a new node in the middle of each selected segment. */ +void PathManipulator::insertNodes() +{ + if (_selection.size() < 2) return; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + NodeList::iterator k = j.next(); + if (k && j->selected() && k->selected()) { + j = subdivideSegment(j, 0.5); + _selection.insert(j.ptr()); + } + } + } +} + +void PathManipulator::insertNode(Geom::Point pt) +{ + Geom::Coord dist = _updateDragPoint(pt); + if (dist < 1e-5) { // 1e-6 is too small, as observed occasionally when inserting a node at a snapped intersection of paths + insertNode(_dragpoint->getIterator(), _dragpoint->getTimeValue(), true); + } +} + +void PathManipulator::insertNode(NodeList::iterator first, double t, bool take_selection) +{ + NodeList::iterator inserted = subdivideSegment(first, t); + if (take_selection) { + _selection.clear(); + } + _selection.insert(inserted.ptr()); + + update(true); + _commit(_("Add node")); +} + + +static void +add_or_replace_if_extremum(std::vector< std::pair<NodeList::iterator, double> > &vec, + double & extrvalue, double testvalue, NodeList::iterator const& node, double t) +{ + if (testvalue > extrvalue) { + // replace all extreme nodes with the new one + vec.clear(); + vec.emplace_back( node, t ); + extrvalue = testvalue; + } else if ( Geom::are_near(testvalue, extrvalue) ) { + // very rare but: extremum node at the same extreme value!!! so add it to the list + vec.emplace_back( node, t ); + } +} + +/** Insert a new node at the extremum of the selected segments. */ +void PathManipulator::insertNodeAtExtremum(ExtremumType extremum) +{ + if (_selection.size() < 2) return; + + double sign = (extremum == EXTR_MIN_X || extremum == EXTR_MIN_Y) ? -1. : 1.; + Geom::Dim2 dim = (extremum == EXTR_MIN_X || extremum == EXTR_MAX_X) ? Geom::X : Geom::Y; + + for (auto & _subpath : _subpaths) { + Geom::Coord extrvalue = - Geom::infinity(); + std::vector< std::pair<NodeList::iterator, double> > extremum_vector; + + for (NodeList::iterator first = _subpath->begin(); first != _subpath->end(); ++first) { + NodeList::iterator second = first.next(); + if (second && first->selected() && second->selected()) { + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * first->position()[dim], first, 0.); + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * second->position()[dim], first, 1.); + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // a line segment has is extrema at the start and end, no node should be added + continue; + } else { + // build 1D cubic bezier curve + Geom::Bezier temp1d(first->position()[dim], first->front()->position()[dim], + second->back()->position()[dim], second->position()[dim]); + // and determine extremum + Geom::Bezier deriv1d = derivative(temp1d); + std::vector<double> rs = deriv1d.roots(); + for (double & r : rs) { + add_or_replace_if_extremum(extremum_vector, extrvalue, sign * temp1d.valueAt(r), first, r); + } + } + } + } + + for (auto & i : extremum_vector) { + // don't insert node at the start or end of a segment, i.e. round values for extr_t + double t = i.second; + if ( !Geom::are_near(t - std::floor(t+0.5),0.) ) // std::floor(t+0.5) is another way of writing round(t) + { + _selection.insert( subdivideSegment(i.first, t).ptr() ); + } + } + } +} + + +/** Insert new nodes exactly at the positions of selected nodes while preserving shape. + * This is equivalent to breaking, except that it doesn't split into subpaths. */ +void PathManipulator::duplicateNodes() +{ + if (_selection.empty()) return; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + NodeList::iterator k = j.next(); + Node *n = new Node(_multi_path_manipulator._path_data.node_data, *j); + + if (k) { + // Move the new node to the bottom of the Z-order. This way you can drag all + // nodes that were selected before this operation without deselecting + // everything because there is a new node above. + n->sink(); + } + + n->front()->setPosition(*j->front()); + j->front()->retract(); + j->setType(NODE_CUSP, false); + _subpath->insert(k, n); + + if (k) { + // We need to manually call the selection change callback to refresh + // the handle display correctly. + // This call changes num_selected, but we call this once for a selected node + // and once for an unselected node, so in the end the number stays correct. + _selectionChanged(j.ptr(), true); + _selectionChanged(n, false); + } else { + // select the new end node instead of the node just before it + _selection.erase(j.ptr()); + _selection.insert(n); + break; // this was the end node, nothing more to do + } + } + } + } +} + +/** Replace contiguous selections of nodes in each subpath with one node. */ +void PathManipulator::weldNodes(NodeList::iterator preserve_pos) +{ + if (_selection.size() < 2) return; + hideDragPoint(); + + bool pos_valid = preserve_pos; + for (auto sp : _subpaths) { + unsigned num_selected = 0, num_unselected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected < 2) continue; + if (num_unselected == 0) { + // if all nodes in a subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + bool use_pos = false; + Geom::Point back_pos, front_pos; + back_pos = *sel_beg->back(); + + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + front_pos = *sel_end->front(); + if (pos_valid && sel_end == preserve_pos) use_pos = true; + } + if (num_points > 1) { + Geom::Point joined_pos; + if (use_pos) { + joined_pos = preserve_pos->position(); + pos_valid = false; + } else { + joined_pos = Geom::middle_point(back_pos, front_pos); + } + sel_beg->setType(NODE_CUSP, false); + sel_beg->move(joined_pos); + // do not move handles if they aren't degenerate + if (!sel_beg->back()->isDegenerate()) { + sel_beg->back()->setPosition(back_pos); + } + if (!sel_end.prev()->front()->isDegenerate()) { + sel_beg->front()->setPosition(front_pos); + } + sel_beg = sel_beg.next(); + while (sel_beg != sel_end) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + --num_selected; + } + } + --num_selected; // for the joined node or single selected node + } + } +} + +/** Remove nodes in the middle of selected segments. */ +void PathManipulator::weldSegments() +{ + if (_selection.size() < 2) return; + hideDragPoint(); + + for (auto sp : _subpaths) { + unsigned num_selected = 0, num_unselected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + + // if 2 or fewer nodes are selected, there can't be any middle points to remove. + if (num_selected <= 2) continue; + + if (num_unselected == 0 && sp->closed()) { + // if all nodes in a closed subpath are selected, the operation doesn't make much sense + continue; + } + + // Start from unselected node in closed paths, so that we don't start in the middle + // of a selection + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + + // Work loop + while (num_selected > 0) { + // Find selected node + while (sel_beg && !sel_beg->selected()) sel_beg = sel_beg.next(); + if (!sel_beg) throw std::logic_error("Join nodes: end of open path reached, " + "but there are still nodes to process!"); + + // note: this is initialized to zero, because the loop below counts sel_beg as well + // the loop conditions are simpler that way + unsigned num_points = 0; + + // find the end of selected segment + for (sel_end = sel_beg; sel_end && sel_end->selected(); sel_end = sel_end.next()) { + ++num_points; + } + if (num_points > 2) { + // remove nodes in the middle + // TODO: fit bezier to the former shape + sel_beg = sel_beg.next(); + while (sel_beg != sel_end.prev()) { + NodeList::iterator next = sel_beg.next(); + sp->erase(sel_beg); + sel_beg = next; + } + } + sel_beg = sel_end; + // decrease num_selected by the number of points processed + num_selected -= num_points; + } + } +} + +/** Break the subpath at selected nodes. It also works for single node closed paths. */ +void PathManipulator::breakNodes() +{ + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) { + SubpathPtr sp = *i; + NodeList::iterator cur = sp->begin(), end = sp->end(); + if (!sp->closed()) { + // Each open path must have at least two nodes so no checks are required. + // For 2-node open paths, cur == end + ++cur; + --end; + } + for (; cur != end; ++cur) { + if (!cur->selected()) continue; + SubpathPtr ins; + bool becomes_open = false; + + if (sp->closed()) { + // Move the node to break at to the beginning of path + if (cur != sp->begin()) + sp->splice(sp->begin(), *sp, cur, sp->end()); + sp->setClosed(false); + ins = sp; + becomes_open = true; + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), cur); + _subpaths.insert(i, new_sp); + ins = new_sp; + } + + Node *n = new Node(_multi_path_manipulator._path_data.node_data, cur->position()); + ins->insert(ins->end(), n); + cur->setType(NODE_CUSP, false); + n->back()->setRelativePos(cur->back()->relativePos()); + cur->back()->retract(); + n->sink(); + + if (becomes_open) { + cur = sp->begin(); // this will be increased to ++sp->begin() + end = --sp->end(); + } + } + } +} + +/** Delete selected nodes in the path, optionally substituting deleted segments with bezier curves + * in a way that attempts to preserve the original shape of the curve. */ +void PathManipulator::deleteNodes(bool keep_shape) +{ + if (_selection.empty()) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + + // If there are less than 2 unselected nodes in an open subpath or no unselected nodes + // in a closed one, delete entire subpath. + unsigned num_unselected = 0, num_selected = 0; + for (auto & j : *sp) { + if (j.selected()) ++num_selected; + else ++num_unselected; + } + if (num_selected == 0) { + ++i; + continue; + } + if (sp->closed() ? (num_unselected < 1) : (num_unselected < 2)) { + _subpaths.erase(i++); + continue; + } + + // In closed paths, start from an unselected node - otherwise we might start in the middle + // of a selected stretch and the resulting bezier fit would be suboptimal + NodeList::iterator sel_beg = sp->begin(), sel_end; + if (sp->closed()) { + while (sel_beg->selected()) ++sel_beg; + } + sel_end = sel_beg; + + while (num_selected > 0) { + while (sel_beg && !sel_beg->selected()) { + sel_beg = sel_beg.next(); + } + sel_end = sel_beg; + + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + } + + num_selected -= _deleteStretch(sel_beg, sel_end, keep_shape); + sel_beg = sel_end; + } + ++i; + } +} + +/** + * Delete nodes between the two iterators. + * The given range can cross the beginning of the subpath in closed subpaths. + * @param start Beginning of the range to delete + * @param end End of the range + * @param keep_shape Whether to fit the handles at surrounding nodes to approximate + * the shape before deletion + * @return Number of deleted nodes + */ +unsigned PathManipulator::_deleteStretch(NodeList::iterator start, NodeList::iterator end, bool keep_shape) +{ + unsigned const samples_per_segment = 10; + double const t_step = 1.0 / samples_per_segment; + + unsigned del_len = 0; + for (NodeList::iterator i = start; i != end; i = i.next()) { + ++del_len; + } + if (del_len == 0) return 0; + + // set surrounding node types to cusp if: + // 1. keep_shape is on, or + // 2. we are deleting at the end or beginning of an open path + if ((keep_shape || !end) && start.prev()) start.prev()->setType(NODE_CUSP, false); + if ((keep_shape || !start.prev()) && end) end->setType(NODE_CUSP, false); + + if (keep_shape && start.prev() && end) { + unsigned num_samples = (del_len + 1) * samples_per_segment + 1; + Geom::Point *bezier_data = new Geom::Point[num_samples]; + Geom::Point result[4]; + unsigned seg = 0; + + for (NodeList::iterator cur = start.prev(); cur != end; cur = cur.next()) { + Geom::CubicBezier bc(*cur, *cur->front(), *cur.next(), *cur.next()->back()); + for (unsigned s = 0; s < samples_per_segment; ++s) { + bezier_data[seg * samples_per_segment + s] = bc.pointAt(t_step * s); + } + ++seg; + } + // Fill last point + bezier_data[num_samples - 1] = end->position(); + // Compute replacement bezier curve + // TODO the fitting algorithm sucks - rewrite it to be awesome + bezier_fit_cubic(result, bezier_data, num_samples, 0.5); + delete[] bezier_data; + + start.prev()->front()->setPosition(result[1]); + end->back()->setPosition(result[2]); + } + + // We can't use nl->erase(start, end), because it would break when the stretch + // crosses the beginning of a closed subpath + NodeList &nl = start->nodeList(); + while (start != end) { + NodeList::iterator next = start.next(); + nl.erase(start); + start = next; + } + // if we are removing, we readjust the handlers + if(_isBSpline()){ + if(start.prev()){ + double bspline_weight = _bsplineHandlePosition(start.prev()->back(), false); + start.prev()->front()->setPosition(_bsplineHandleReposition(start.prev()->front(), bspline_weight)); + } + if(end){ + double bspline_weight = _bsplineHandlePosition(end->front(), false); + end->back()->setPosition(_bsplineHandleReposition(end->back(),bspline_weight)); + } + } + + return del_len; +} + +/** Removes selected segments */ +void PathManipulator::deleteSegments() +{ + if (_selection.empty()) return; + hideDragPoint(); + + for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end();) { + SubpathPtr sp = *i; + bool has_unselected = false; + unsigned num_selected = 0; + for (auto & j : *sp) { + if (j.selected()) { + ++num_selected; + } else { + has_unselected = true; + } + } + if (!has_unselected) { + _subpaths.erase(i++); + continue; + } + + NodeList::iterator sel_beg = sp->begin(); + if (sp->closed()) { + while (sel_beg && sel_beg->selected()) ++sel_beg; + } + while (num_selected > 0) { + if (!sel_beg->selected()) { + sel_beg = sel_beg.next(); + continue; + } + NodeList::iterator sel_end = sel_beg; + unsigned num_points = 0; + while (sel_end && sel_end->selected()) { + sel_end = sel_end.next(); + ++num_points; + } + if (num_points >= 2) { + // Retract end handles + sel_end.prev()->setType(NODE_CUSP, false); + sel_end.prev()->back()->retract(); + sel_beg->setType(NODE_CUSP, false); + sel_beg->front()->retract(); + if (sp->closed()) { + // In closed paths, relocate the beginning of the path to the last selected + // node and then unclose it. Remove the nodes from the first selected node + // to the new end of path. + if (sel_end.prev() != sp->begin()) + sp->splice(sp->begin(), *sp, sel_end.prev(), sp->end()); + sp->setClosed(false); + sp->erase(sel_beg.next(), sp->end()); + } else { + // for open paths: + // 1. At end or beginning, delete including the node on the end or beginning + // 2. In the middle, delete only inner nodes + if (sel_beg == sp->begin()) { + sp->erase(sp->begin(), sel_end.prev()); + } else if (sel_end == sp->end()) { + sp->erase(sel_beg.next(), sp->end()); + } else { + SubpathPtr new_sp(new NodeList(_subpaths)); + new_sp->splice(new_sp->end(), *sp, sp->begin(), sel_beg.next()); + _subpaths.insert(i, new_sp); + if (sel_end.prev()) + sp->erase(sp->begin(), sel_end.prev()); + } + } + } + sel_beg = sel_end; + num_selected -= num_points; + } + ++i; + } +} + +/** Reverse subpaths of the path. + * @param selected_only If true, only paths that have at least one selected node + * will be reversed. Otherwise all subpaths will be reversed. */ +void PathManipulator::reverseSubpaths(bool selected_only) +{ + for (auto & _subpath : _subpaths) { + if (selected_only) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (j->selected()) { + _subpath->reverse(); + break; // continue with the next subpath + } + } + } else { + _subpath->reverse(); + } + } +} + +/** Make selected segments curves / lines. */ +void PathManipulator::setSegmentType(SegmentType type) +{ + if (_selection.empty()) return; + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + NodeList::iterator k = j.next(); + if (!(k && j->selected() && k->selected())) continue; + switch (type) { + case SEGMENT_STRAIGHT: + if (j->front()->isDegenerate() && k->back()->isDegenerate()) + break; + j->front()->move(*j); + k->back()->move(*k); + break; + case SEGMENT_CUBIC_BEZIER: + if (!j->front()->isDegenerate() || !k->back()->isDegenerate()) + break; + // move both handles to 1/3 of the line + j->front()->move(j->position() + (k->position() - j->position()) / 3); + k->back()->move(k->position() + (j->position() - k->position()) / 3); + break; + } + } + } +} + +void PathManipulator::scaleHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() == NODE_SYMMETRIC || n->type() == NODE_AUTO) { + n->setType(NODE_SMOOTH); + } + Handle *h = _chooseHandle(n, which); + double length_change; + + if (pixel) { + length_change = 1.0 / _desktop->current_zoom() * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + length_change = prefs->getDoubleLimited("/options/defaultscale/value", 2, 1, 1000, "px"); + length_change *= dir; + } + + Geom::Point relpos; + if (h->isDegenerate()) { + if (dir < 0) return; + Node *nh = n->nodeToward(h); + if (!nh) return; + relpos = Geom::unit_vector(nh->position() - n->position()) * length_change; + } else { + relpos = h->relativePos(); + double rellen = relpos.length(); + relpos *= ((rellen + length_change) / rellen); + } + h->setRelativePos(relpos); + update(); + gchar const *key = which < 0 ? "handle:scale:left" : "handle:scale:right"; + _commit(_("Scale handle"), key); +} + +void PathManipulator::rotateHandle(Node *n, int which, int dir, bool pixel) +{ + if (n->type() != NODE_CUSP) { + n->setType(NODE_CUSP); + } + Handle *h = _chooseHandle(n, which); + if (h->isDegenerate()) return; + + double angle; + if (pixel) { + // Rotate by "one pixel" + angle = atan2(1.0 / _desktop->current_zoom(), h->length()) * dir; + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + angle = M_PI * dir / snaps; + } + + h->setRelativePos(h->relativePos() * Geom::Rotate(angle)); + update(); + gchar const *key = which < 0 ? "handle:rotate:left" : "handle:rotate:right"; + _commit(_("Rotate handle"), key); +} + +Handle *PathManipulator::_chooseHandle(Node *n, int which) +{ + NodeList::iterator i = NodeList::get_iterator(n); + Node *prev = i.prev().ptr(); + Node *next = i.next().ptr(); + + // on an endnode, the remaining handle automatically wins + if (!next) return n->back(); + if (!prev) return n->front(); + + // compare X coord offline segments + Geom::Point npos = next->position(); + Geom::Point ppos = prev->position(); + if (which < 0) { + // pick left handle. + // we just swap the handles and pick the right handle below. + std::swap(npos, ppos); + } + + if (npos[Geom::X] >= ppos[Geom::X]) { + return n->front(); + } else { + return n->back(); + } +} + +/** Set the visibility of handles. */ +void PathManipulator::showHandles(bool show) +{ + if (show == _show_handles) return; + if (show) { + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (!j->selected()) continue; + j->showHandles(true); + if (j.prev()) j.prev()->showHandles(true); + if (j.next()) j.next()->showHandles(true); + } + } + } else { + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.showHandles(false); + } + } + } + _show_handles = show; +} + +/** Set the visibility of outline. */ +void PathManipulator::showOutline(bool show) +{ + if (show == _show_outline) return; + _show_outline = show; + _updateOutline(); +} + +void PathManipulator::showPathDirection(bool show) +{ + if (show == _show_path_direction) return; + _show_path_direction = show; + _updateOutline(); +} + +void PathManipulator::setLiveOutline(bool set) +{ + _live_outline = set; +} + +void PathManipulator::setLiveObjects(bool set) +{ + _live_objects = set; +} + +void PathManipulator::updateHandles() +{ + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.updateHandles(); + } + } +} + +void PathManipulator::setControlsTransform(Geom::Affine const &tnew) +{ + Geom::Affine delta = _i2d_transform.inverse() * _edit_transform.inverse() * tnew * _i2d_transform; + _edit_transform = tnew; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.transform(delta); + } + } + _createGeometryFromControlPoints(); +} + +/** Hide the curve drag point until the next motion event. + * This should be called at the beginning of every method that can delete nodes. + * Otherwise the invalidated iterator in the dragpoint can cause crashes. */ +void PathManipulator::hideDragPoint() +{ + _dragpoint->setVisible(false); + _dragpoint->setIterator(NodeList::iterator()); +} + +/** Insert a node in the segment beginning with the supplied iterator, + * at the given time value */ +NodeList::iterator PathManipulator::subdivideSegment(NodeList::iterator first, double t) +{ + if (!first) throw std::invalid_argument("Subdivide after invalid iterator"); + NodeList &list = NodeList::get(first); + NodeList::iterator second = first.next(); + if (!second) throw std::invalid_argument("Subdivide after last node in open path"); + if (first->type() == NODE_SYMMETRIC) + first->setType(NODE_SMOOTH, false); + if (second->type() == NODE_SYMMETRIC) + second->setType(NODE_SMOOTH, false); + + // We need to insert the segment after 'first'. We can't simply use 'second' + // as the point of insertion, because when 'first' is the last node of closed path, + // the new node will be inserted as the first node instead. + NodeList::iterator insert_at = first; + ++insert_at; + + NodeList::iterator inserted; + if (first->front()->isDegenerate() && second->back()->isDegenerate()) { + // for a line segment, insert a cusp node + Node *n = new Node(_multi_path_manipulator._path_data.node_data, + Geom::lerp(t, first->position(), second->position())); + n->setType(NODE_CUSP, false); + inserted = list.insert(insert_at, n); + } else { + // build bezier curve and subdivide + Geom::CubicBezier temp(first->position(), first->front()->position(), + second->back()->position(), second->position()); + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = temp.subdivide(t); + std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + + // set new handle positions + Node *n = new Node(_multi_path_manipulator._path_data.node_data, seg2[0]); + if(!_isBSpline()){ + n->back()->setPosition(seg1[2]); + n->front()->setPosition(seg2[1]); + n->setType(NODE_SMOOTH, false); + } else { + Geom::D2< Geom::SBasis > sbasis_inside_nodes; + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + if(second->back()->isDegenerate()){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(second->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + Geom::Point next = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER); + next = Geom::Point(next[Geom::X] + HANDLE_CUBIC_GAP,next[Geom::Y] + HANDLE_CUBIC_GAP); + line_inside_nodes->reset(); + n->front()->setPosition(next); + }else{ + n->front()->setPosition(seg2[1]); + } + if(first->front()->isDegenerate()){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(first->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + Geom::Point previous = sbasis_inside_nodes.valueAt(DEFAULT_START_POWER); + previous = Geom::Point(previous[Geom::X] + HANDLE_CUBIC_GAP,previous[Geom::Y] + HANDLE_CUBIC_GAP); + n->back()->setPosition(previous); + }else{ + n->back()->setPosition(seg1[2]); + } + n->setType(NODE_CUSP, false); + } + inserted = list.insert(insert_at, n); + + first->front()->move(seg1[1]); + second->back()->move(seg2[2]); + } + return inserted; +} + +/** Find the node that is closest/farthest from the origin + * @param origin Point of reference + * @param search_selected Consider selected nodes + * @param search_unselected Consider unselected nodes + * @param closest If true, return closest node, if false, return farthest + * @return The matching node, or an empty iterator if none found + */ +NodeList::iterator PathManipulator::extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest) +{ + NodeList::iterator match; + double extr_dist = closest ? HUGE_VAL : -HUGE_VAL; + if (_selection.empty() && !search_unselected) return match; + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if(j->selected()) { + if (!search_selected) continue; + } else { + if (!search_unselected) continue; + } + double dist = Geom::distance(*j, *origin); + bool cond = closest ? (dist < extr_dist) : (dist > extr_dist); + if (cond) { + match = j; + extr_dist = dist; + } + } + } + return match; +} + +/** Called by the XML observer when something else than us modifies the path. */ +void PathManipulator::_externalChange(unsigned type) +{ + hideDragPoint(); + + switch (type) { + case PATH_CHANGE_D: { + _getGeometry(); + + // ugly: stored offsets of selected nodes in a vector + // vector<bool> should be specialized so that it takes only 1 bit per value + std::vector<bool> selpos; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + selpos.push_back(j.selected()); + } + } + unsigned size = selpos.size(), curpos = 0; + + _createControlPointsFromGeometry(); + + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + if (curpos >= size) goto end_restore; + if (selpos[curpos]) _selection.insert(j.ptr()); + ++curpos; + } + } + end_restore: + + _updateOutline(); + } break; + case PATH_CHANGE_TRANSFORM: { + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path) { + Geom::Affine i2d_change = _d2i_transform; + _i2d_transform = path->i2dt_affine(); + _d2i_transform = _i2d_transform.inverse(); + i2d_change *= _i2d_transform; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + j.transform(i2d_change); + } + } + _updateOutline(); + } + } break; + default: break; + } +} + +/** Create nodes and handles based on the XML of the edited path. */ +void PathManipulator::_createControlPointsFromGeometry() +{ + clear(); + + // sanitize pathvector and store it in SPCurve, + // so that _updateDragPoint doesn't crash on paths with naked movetos + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector()); + for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) { + // NOTE: this utilizes the fact that Geom::PathVector is an std::vector. + // When we erase an element, the next one slides into position, + // so we do not increment the iterator even though it is theoretically invalidated. + if (i->empty()) { + i = pathv.erase(i); + } else { + ++i; + } + } + if (pathv.empty()) { + return; + } + _spcurve->set_pathvector(pathv); + + pathv *= (_edit_transform * _i2d_transform); + + // in this loop, we know that there are no zero-segment subpaths + for (auto & pit : pathv) { + // prepare new subpath + SubpathPtr subpath(new NodeList(_subpaths)); + _subpaths.push_back(subpath); + + Node *previous_node = new Node(_multi_path_manipulator._path_data.node_data, pit.initialPoint()); + subpath->push_back(previous_node); + + bool closed = pit.closed(); + + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end(); ++cit) { + Geom::Point pos = cit->finalPoint(); + Node *current_node; + // if the closing segment is degenerate and the path is closed, we need to move + // the handle of the first node instead of creating a new one + if (closed && cit == --(pit.end())) { + current_node = subpath->begin().get_pointer(); + } else { + /* regardless of segment type, create a new node at the end + * of this segment (unless this is the last segment of a closed path + * with a degenerate closing segment */ + current_node = new Node(_multi_path_manipulator._path_data.node_data, pos); + subpath->push_back(current_node); + } + // if this is a bezier segment, move handles appropriately + // TODO: I don't know why the dynamic cast below doesn't want to work + // when I replace BezierCurve with CubicBezier. Might be a bug + // somewhere in pathv_to_linear_and_cubic_beziers + Geom::BezierCurve const *bezier = dynamic_cast<Geom::BezierCurve const*>(&*cit); + if (bezier && bezier->order() == 3) + { + previous_node->front()->setPosition((*bezier)[1]); + current_node ->back() ->setPosition((*bezier)[2]); + } + previous_node = current_node; + } + // If the path is closed, make the list cyclic + if (pit.closed()) subpath->setClosed(true); + } + + // we need to set the nodetypes after all the handles are in place, + // so that pickBestType works correctly + // TODO maybe migrate to inkscape:node-types? + // TODO move this into SPPath - do not manipulate directly + + //XML Tree being used here directly while it shouldn't be. + gchar const *nts_raw = _path ? _path->getRepr()->attribute(_nodetypesKey().data()) : nullptr; + /* Calculate the needed length of the nodetype string. + * For closed paths, the entry is duplicated for the starting node, + * so we can just use the count of segments including the closing one + * to include the extra end node. */ + /* pad the string to required length with a bogus value. + * 'b' and any other letter not recognized by the parser causes the best fit to be set + * as the node type */ + auto const *tsi = nts_raw ? nts_raw : ""; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + char nodetype = (*tsi) ? (*tsi++) : 'b'; + j.setType(Node::parse_nodetype(nodetype), false); + } + if (_subpath->closed() && *tsi) { + // STUPIDITY ALERT: it seems we need to use the duplicate type symbol instead of + // the first one to remain backward compatible. + _subpath->begin()->setType(Node::parse_nodetype(*tsi++), false); + } + } +} + +//determines if the trace has a bspline effect and the number of steps that it takes +int PathManipulator::_bsplineGetSteps() const { + + LivePathEffect::LPEBSpline const *lpe_bsp = nullptr; + + SPLPEItem * path = dynamic_cast<SPLPEItem *>(_path); + if (path){ + if(path->hasPathEffect()){ + Inkscape::LivePathEffect::Effect const *this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(this_effect){ + lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline const*>(this_effect->getLPEObj()->get_lpe()); + } + } + } + int steps = 0; + if(lpe_bsp){ + steps = lpe_bsp->steps+1; + } + return steps; +} + +// determines if the trace has bspline effect +void PathManipulator::_recalculateIsBSpline(){ + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path && path->hasPathEffect()) { + Inkscape::LivePathEffect::Effect const *this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(this_effect){ + _is_bspline = true; + return; + } + } + _is_bspline = false; +} + +bool PathManipulator::_isBSpline() const { + return _is_bspline; +} + +// returns the corresponding strength to the position of the handlers +double PathManipulator::_bsplineHandlePosition(Handle *h, bool check_other) +{ + using Geom::X; + using Geom::Y; + double pos = NO_POWER; + Node *n = h->parent(); + Node * next_node = nullptr; + next_node = n->nodeToward(h); + if(next_node){ + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(next_node->position()); + if(!are_near(h->position(), n->position())){ + pos = Geom::nearest_time(Geom::Point(h->position()[X] - HANDLE_CUBIC_GAP, h->position()[Y] - HANDLE_CUBIC_GAP), *line_inside_nodes->first_segment()); + } + } + if (pos == NO_POWER && check_other){ + return _bsplineHandlePosition(h->other(), false); + } + return pos; +} + +// give the location for the handler in the corresponding position +Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h, bool check_other) +{ + double pos = this->_bsplineHandlePosition(h, check_other); + return _bsplineHandleReposition(h,pos); +} + +// give the location for the handler to the specified position +Geom::Point PathManipulator::_bsplineHandleReposition(Handle *h,double pos){ + using Geom::X; + using Geom::Y; + Geom::Point ret = h->position(); + Node *n = h->parent(); + Geom::D2< Geom::SBasis > sbasis_inside_nodes; + std::unique_ptr<SPCurve> line_inside_nodes(new SPCurve()); + Node * next_node = nullptr; + next_node = n->nodeToward(h); + if(next_node && pos != NO_POWER){ + line_inside_nodes->moveto(n->position()); + line_inside_nodes->lineto(next_node->position()); + sbasis_inside_nodes = line_inside_nodes->first_segment()->toSBasis(); + ret = sbasis_inside_nodes.valueAt(pos); + ret = Geom::Point(ret[X] + HANDLE_CUBIC_GAP, ret[Y] + HANDLE_CUBIC_GAP); + }else{ + if(pos == NO_POWER){ + ret = n->position(); + } + } + return ret; +} + +/** Construct the geometric representation of nodes and handles, update the outline + * and display + * \param alert_LPE if true, first the LPE is warned what the new path is going to be before updating it + */ +void PathManipulator::_createGeometryFromControlPoints(bool alert_LPE) +{ + Geom::PathBuilder builder; + //Refresh if is bspline some times -think on path change selection, this value get lost + _recalculateIsBSpline(); + for (std::list<SubpathPtr>::iterator spi = _subpaths.begin(); spi != _subpaths.end(); ) { + SubpathPtr subpath = *spi; + if (subpath->empty()) { + _subpaths.erase(spi++); + continue; + } + NodeList::iterator prev = subpath->begin(); + builder.moveTo(prev->position()); + for (NodeList::iterator i = ++subpath->begin(); i != subpath->end(); ++i) { + build_segment(builder, prev.ptr(), i.ptr()); + prev = i; + } + if (subpath->closed()) { + // Here we link the last and first node if the path is closed. + // If the last segment is Bezier, we add it. + if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) { + build_segment(builder, prev.ptr(), subpath->begin().ptr()); + } + // if that segment is linear, we just call closePath(). + builder.closePath(); + } + ++spi; + } + builder.flush(); + Geom::PathVector pathv = builder.peek() * (_edit_transform * _i2d_transform).inverse(); + for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) { + // NOTE: this utilizes the fact that Geom::PathVector is an std::vector. + // When we erase an element, the next one slides into position, + // so we do not increment the iterator even though it is theoretically invalidated. + if (i->empty()) { + i = pathv.erase(i); + } else { + ++i; + } + } + if (pathv.empty()) { + return; + } + + if (_spcurve->get_pathvector() == pathv) { + return; + } + _spcurve->set_pathvector(pathv); + if (alert_LPE) { + /// \todo note that _path can be an Inkscape::LivePathEffect::Effect* too, kind of confusing, rework member naming? + SPPath *path = dynamic_cast<SPPath *>(_path); + if (path && path->hasPathEffect()) { + Inkscape::LivePathEffect::Effect* this_effect = path->getPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE); + if(this_effect){ + LivePathEffect::LPEPowerStroke *lpe_pwr = dynamic_cast<LivePathEffect::LPEPowerStroke*>(this_effect->getLPEObj()->get_lpe()); + if (lpe_pwr) { + lpe_pwr->adjustForNewPath(pathv); + } + } + } + } + if (_live_outline) { + _updateOutline(); + } + if (_live_objects) { + _setGeometry(); + } +} + +/** Build one segment of the geometric representation. + * @relates PathManipulator */ +void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node) +{ + if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate()) + { + // NOTE: It seems like the renderer cannot correctly handle vline / hline segments, + // and trying to display a path using them results in funny artifacts. + builder.lineTo(cur_node->position()); + } else { + // this is a bezier segment + builder.curveTo( + prev_node->front()->position(), + cur_node->back()->position(), + cur_node->position()); + } +} + +/** Construct a node type string to store in the sodipodi:nodetypes attribute. */ +std::string PathManipulator::_createTypeString() +{ + // precondition: no single-node subpaths + std::stringstream tstr; + for (auto & _subpath : _subpaths) { + for (auto & j : *_subpath) { + tstr << j.type(); + } + // nodestring format peculiarity: first node is counted twice for closed paths + if (_subpath->closed()) tstr << _subpath->begin()->type(); + } + return tstr.str(); +} + +/** Update the path outline. */ +void PathManipulator::_updateOutline() +{ + if (!_show_outline) { + sp_canvas_item_hide(_outline); + return; + } + + Geom::PathVector pv = _spcurve->get_pathvector(); + pv *= (_edit_transform * _i2d_transform); + // This SPCurve thing has to be killed with extreme prejudice + SPCurve *_hc = new SPCurve(); + if (_show_path_direction) { + // To show the direction, we append additional subpaths which consist of a single + // linear segment that starts at the time value of 0.5 and extends for 10 pixels + // at an angle 150 degrees from the unit tangent. This creates the appearance + // of little 'harpoons' that show the direction of the subpaths. + auto rot_scale_w2d = Geom::Rotate(210.0 / 180.0 * M_PI) * Geom::Scale(10.0) * _desktop->w2d(); + Geom::PathVector arrows; + for (auto & path : pv) { + for (Geom::Path::iterator j = path.begin(); j != path.end_default(); ++j) { + Geom::Point at = j->pointAt(0.5); + Geom::Point ut = j->unitTangentAt(0.5); + Geom::Point arrow_end = at + (Geom::unit_vector(_desktop->d2w(ut)) * rot_scale_w2d); + + Geom::Path arrow(at); + arrow.appendNew<Geom::LineSegment>(arrow_end); + arrows.push_back(arrow); + } + } + pv.insert(pv.end(), arrows.begin(), arrows.end()); + } + _hc->set_pathvector(pv); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(_outline), _hc); + sp_canvas_item_show(_outline); + _hc->unref(); +} + +/** Retrieve the geometry of the edited object from the object tree */ +void PathManipulator::_getGeometry() +{ + using namespace Inkscape::LivePathEffect; + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *path = dynamic_cast<SPPath *>(_path); + if (lpeobj) { + Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + _spcurve->unref(); + _spcurve = new SPCurve(pathparam->get_pathvector()); + } + } else if (path) { + _spcurve->unref(); + _spcurve = path->getCurveForEdit(); + // never allow NULL to sneak in here! + if (_spcurve == nullptr) { + _spcurve = new SPCurve(); + } + } +} + +/** Set the geometry of the edited object in the object tree, but do not commit to XML */ +void PathManipulator::_setGeometry() +{ + using namespace Inkscape::LivePathEffect; + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + SPPath *path = dynamic_cast<SPPath *>(_path); + if (lpeobj) { + // copied from nodepath.cpp + // NOTE: if we are editing an LPE param, _path is not actually an SPPath, it is + // a LivePathEffectObject. (mad laughter) + Effect *lpe = lpeobj->get_lpe(); + if (lpe) { + PathParam *pathparam = dynamic_cast<PathParam *>(lpe->getParameter(_lpe_key.data())); + if (pathparam->get_pathvector() == _spcurve->get_pathvector()) { + return; //False we dont update LPE + } + pathparam->set_new_value(_spcurve->get_pathvector(), false); + lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + } else if (path) { + // return true to leave the decision on empty to the caller. + // Maybe the path become empty and we want to update to empty + if (empty()) return; + if (path->getCurveBeforeLPE(true)) { + if (!_spcurve->is_equal(path->getCurveBeforeLPE(true))) { + path->setCurveBeforeLPE(_spcurve); + sp_lpe_item_update_patheffect(path, true, false); + } + } else if(!_spcurve->is_equal(path->getCurve(true))) { + path->setCurve(_spcurve); + } + } +} + +/** Figure out in what attribute to store the nodetype string. */ +Glib::ustring PathManipulator::_nodetypesKey() +{ + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + if (!lpeobj) { + return "sodipodi:nodetypes"; + } else { + return _lpe_key + "-nodetypes"; + } +} + +/** Return the XML node we are editing. + * This method is wrong but necessary at the moment. */ +Inkscape::XML::Node *PathManipulator::_getXMLNode() +{ + //XML Tree being used here directly while it shouldn't be. + LivePathEffectObject *lpeobj = dynamic_cast<LivePathEffectObject *>(_path); + if (!lpeobj) + return _path->getRepr(); + //XML Tree being used here directly while it shouldn't be. + return lpeobj->getRepr(); +} + +bool PathManipulator::_nodeClicked(Node *n, GdkEventButton *event) +{ + if (event->button != 1) return false; + if (held_alt(*event) && held_control(*event)) { + // Ctrl+Alt+click: delete nodes + hideDragPoint(); + NodeList::iterator iter = NodeList::get_iterator(n); + NodeList &nl = iter->nodeList(); + + if (nl.size() <= 1 || (nl.size() <= 2 && !nl.closed())) { + // Removing last node of closed path - delete it + nl.kill(); + } else { + // In other cases, delete the node under cursor + _deleteStretch(iter, iter.next(), true); + } + + if (!empty()) { + update(true); + } + + // We need to call MPM's method because it could have been our last node + _multi_path_manipulator._doneWithCleanup(_("Delete node")); + + return true; + } else if (held_control(*event)) { + // Ctrl+click: cycle between node types + if (!n->isEndNode()) { + n->setType(static_cast<NodeType>((n->type() + 1) % NODE_LAST_REAL_TYPE)); + update(); + _commit(_("Cycle node type")); + } + return true; + } + return false; +} + +void PathManipulator::_handleGrabbed() +{ + _selection.hideTransformHandles(); +} + +void PathManipulator::_handleUngrabbed() +{ + _selection.restoreTransformHandles(); + _commit(_("Drag handle")); +} + +bool PathManipulator::_handleClicked(Handle *h, GdkEventButton *event) +{ + // retracting by Ctrl+click + if (event->button == 1 && held_control(*event)) { + h->move(h->parent()->position()); + update(); + _commit(_("Retract handle")); + return true; + } + return false; +} + +void PathManipulator::_selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected) { + for (auto & n : pvec) { + _selectionChanged(n, selected); + } +} + +void PathManipulator::_selectionChanged(SelectableControlPoint *p, bool selected) +{ + // don't do anything if we do not show handles + if (!_show_handles) return; + + // only do something if a node changed selection state + Node *node = dynamic_cast<Node*>(p); + if (!node) return; + + // update handle display + NodeList::iterator iters[5]; + iters[2] = NodeList::get_iterator(node); + iters[1] = iters[2].prev(); + iters[3] = iters[2].next(); + if (selected) { + // selection - show handles on this node and adjacent ones + node->showHandles(true); + if (iters[1]) iters[1]->showHandles(true); + if (iters[3]) iters[3]->showHandles(true); + } else { + /* Deselection is more complex. + * The change might affect 3 nodes - this one and two adjacent. + * If the node and both its neighbors are deselected, hide handles. + * Otherwise, leave as is. */ + if (iters[1]) iters[0] = iters[1].prev(); + if (iters[3]) iters[4] = iters[3].next(); + bool nodesel[5]; + for (int i = 0; i < 5; ++i) { + nodesel[i] = iters[i] && iters[i]->selected(); + } + for (int i = 1; i < 4; ++i) { + if (iters[i] && !nodesel[i-1] && !nodesel[i] && !nodesel[i+1]) { + iters[i]->showHandles(false); + } + } + } +} + +/** Removes all nodes belonging to this manipulator from the control point selection */ +void PathManipulator::_removeNodesFromSelection() +{ + // remove this manipulator's nodes from selection + for (auto & _subpath : _subpaths) { + for (NodeList::iterator j = _subpath->begin(); j != _subpath->end(); ++j) { + _selection.erase(j.get_pointer()); + } + } +} + +/** Update the XML representation and put the specified annotation on the undo stack */ +void PathManipulator::_commit(Glib::ustring const &annotation) +{ + writeXML(); + if (_desktop) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_NODE, annotation.data()); + } +} + +void PathManipulator::_commit(Glib::ustring const &annotation, gchar const *key) +{ + writeXML(); + DocumentUndo::maybeDone(_desktop->getDocument(), key, SP_VERB_CONTEXT_NODE, + annotation.data()); +} + +/** Update the position of the curve drag point such that it is over the nearest + * point of the path. */ +Geom::Coord PathManipulator::_updateDragPoint(Geom::Point const &evp) +{ + Geom::Coord dist = HUGE_VAL; + + Geom::Affine to_desktop = _edit_transform * _i2d_transform; + Geom::PathVector pv = _spcurve->get_pathvector(); + boost::optional<Geom::PathVectorTime> pvp = + pv.nearestTime(_desktop->w2d(evp) * to_desktop.inverse()); + if (!pvp) return dist; + Geom::Point nearest_pt = _desktop->d2w(pv.pointAt(*pvp) * to_desktop); + + double fracpart = pvp->t; + std::list<SubpathPtr>::iterator spi = _subpaths.begin(); + for (unsigned i = 0; i < pvp->path_index; ++i, ++spi) {} + NodeList::iterator first = (*spi)->before(pvp->asPathTime()); + + dist = Geom::distance(evp, nearest_pt); + + double stroke_tolerance = _getStrokeTolerance(); + if (first && first.next() && + fracpart != 0.0 && + fracpart != 1.0 && + dist < stroke_tolerance) + { + _dragpoint->setVisible(true); + _dragpoint->setPosition(_desktop->w2d(nearest_pt)); + _dragpoint->setSize(2 * stroke_tolerance); + _dragpoint->setTimeValue(fracpart); + _dragpoint->setIterator(first); + } else { + _dragpoint->setVisible(false); + } + + return dist; +} + +/// This is called on zoom change to update the direction arrows +void PathManipulator::_updateOutlineOnZoomChange() +{ + if (_show_path_direction) _updateOutline(); +} + +/** Compute the radius from the edge of the path where clicks should initiate a curve drag + * or segment selection, in window coordinates. */ +double PathManipulator::_getStrokeTolerance() +{ + /* Stroke event tolerance is equal to half the stroke's width plus the global + * drag tolerance setting. */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double ret = prefs->getIntLimited("/options/dragtolerance/value", 2, 0, 100); + if (_path && _path->style && !_path->style->stroke.isNone()) { + ret += _path->style->stroke_width.computed * 0.5 + * (_edit_transform * _i2d_transform).descrim() // scale to desktop coords + * _desktop->current_zoom(); // == _d2w.descrim() - scale to window coords + } + return ret; +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/path-manipulator.h b/src/ui/tool/path-manipulator.h new file mode 100644 index 0000000..3d7fbd5 --- /dev/null +++ b/src/ui/tool/path-manipulator.h @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path manipulator - a component that edits a single path on-canvas + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_PATH_MANIPULATOR_H +#define SEEN_UI_TOOL_PATH_MANIPULATOR_H + +#include <string> +#include <memory> +#include <2geom/pathvector.h> +#include <2geom/affine.h> +#include "ui/tool/node.h" +#include "ui/tool/manipulator.h" +#include "live_effects/lpe-bspline.h" + +struct SPCanvasItem; +class SPCurve; +class SPPath; + +namespace Inkscape { +namespace XML { class Node; } + +namespace UI { + +class PathManipulator; +class ControlPointSelection; +class PathManipulatorObserver; +class CurveDragPoint; +class PathCanvasGroups; +class MultiPathManipulator; +class Node; +class Handle; + +struct PathSharedData { + NodeSharedData node_data; + SPCanvasGroup *outline_group; + SPCanvasGroup *dragpoint_group; +}; + +/** + * Manipulator that edits a single path using nodes with handles. + * Currently only cubic bezier and linear segments are supported, but this might change + * some time in the future. + */ +class PathManipulator : public PointManipulator { +public: + typedef SPPath *ItemType; + + PathManipulator(MultiPathManipulator &mpm, SPObject *path, Geom::Affine const &edit_trans, + guint32 outline_color, Glib::ustring lpe_key); + ~PathManipulator() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + bool empty(); + void writeXML(); + void update(bool alert_LPE = false); // update display, but don't commit + void clear(); // remove all nodes from manipulator + SPObject *item() { return _path; } + + void selectSubpaths(); + void invertSelectionInSubpaths(); + + void insertNodeAtExtremum(ExtremumType extremum); + void insertNodes(); + void insertNode(Geom::Point); + void insertNode(NodeList::iterator first, double t, bool take_selection); + void duplicateNodes(); + void weldNodes(NodeList::iterator preserve_pos = NodeList::iterator()); + void weldSegments(); + void breakNodes(); + void deleteNodes(bool keep_shape = true); + void deleteSegments(); + void reverseSubpaths(bool selected_only); + void setSegmentType(SegmentType); + + void scaleHandle(Node *n, int which, int dir, bool pixel); + void rotateHandle(Node *n, int which, int dir, bool pixel); + + void showOutline(bool show); + void showHandles(bool show); + void showPathDirection(bool show); + void setLiveOutline(bool set); + void setLiveObjects(bool set); + void updateHandles(); + void setControlsTransform(Geom::Affine const &); + void hideDragPoint(); + MultiPathManipulator &mpm() { return _multi_path_manipulator; } + + NodeList::iterator subdivideSegment(NodeList::iterator after, double t); + NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected, + bool search_unselected, bool closest); + + int _bsplineGetSteps() const; + // this is necessary for Tab-selection in MultiPathManipulator + SubpathList &subpathList() { return _subpaths; } + + static bool is_item_type(void *item); +private: + typedef NodeList Subpath; + typedef std::shared_ptr<NodeList> SubpathPtr; + + void _createControlPointsFromGeometry(); + + void _recalculateIsBSpline(); + bool _isBSpline() const; + double _bsplineHandlePosition(Handle *h, bool check_other = true); + Geom::Point _bsplineHandleReposition(Handle *h, bool check_other = true); + Geom::Point _bsplineHandleReposition(Handle *h, double pos); + void _createGeometryFromControlPoints(bool alert_LPE = false); + unsigned _deleteStretch(NodeList::iterator first, NodeList::iterator last, bool keep_shape); + std::string _createTypeString(); + void _updateOutline(); + //void _setOutline(Geom::PathVector const &); + void _getGeometry(); + void _setGeometry(); + Glib::ustring _nodetypesKey(); + Inkscape::XML::Node *_getXMLNode(); + + void _selectionChangedM(std::vector<SelectableControlPoint *> pvec, bool selected); + void _selectionChanged(SelectableControlPoint * p, bool selected); + bool _nodeClicked(Node *, GdkEventButton *); + void _handleGrabbed(); + bool _handleClicked(Handle *, GdkEventButton *); + void _handleUngrabbed(); + + void _externalChange(unsigned type); + void _removeNodesFromSelection(); + void _commit(Glib::ustring const &annotation); + void _commit(Glib::ustring const &annotation, gchar const *key); + Geom::Coord _updateDragPoint(Geom::Point const &); + void _updateOutlineOnZoomChange(); + double _getStrokeTolerance(); + Handle *_chooseHandle(Node *n, int which); + + SubpathList _subpaths; + MultiPathManipulator &_multi_path_manipulator; + SPObject *_path; ///< can be an SPPath or an Inkscape::LivePathEffect::Effect !!! + SPCurve *_spcurve; // in item coordinates + SPCanvasItem *_outline; + CurveDragPoint *_dragpoint; // an invisible control point hovering over curve + PathManipulatorObserver *_observer; + Geom::Affine _d2i_transform; ///< desktop-to-item transform + Geom::Affine _i2d_transform; ///< item-to-desktop transform, inverse of _d2i_transform + Geom::Affine _edit_transform; ///< additional transform to apply to editing controls + bool _show_handles; + bool _show_outline; + bool _show_path_direction; + bool _live_outline; + bool _live_objects; + bool _is_bspline; + Glib::ustring _lpe_key; + + friend class PathManipulatorObserver; + friend class CurveDragPoint; + friend class Node; + friend class Handle; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selectable-control-point.cpp b/src/ui/tool/selectable-control-point.cpp new file mode 100644 index 0000000..02480ec --- /dev/null +++ b/src/ui/tool/selectable-control-point.cpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" + +namespace Inkscape { +namespace UI { + +ControlPoint::ColorSet SelectableControlPoint::_default_scp_color_set = { + {0xffffff00, 0x01000000}, // normal fill, stroke + {0xff0000ff, 0x01000000}, // mouseover fill, stroke + {0x0000ffff, 0x01000000}, // clicked fill, stroke + // + {0x0000ffff, 0x000000ff}, // normal fill, stroke when selected + {0xff000000, 0x000000ff}, // mouseover fill, stroke when selected + {0xff000000, 0x000000ff} // clicked fill, stroke when selected +}; + +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::ControlType type, + ControlPointSelection &sel, + ColorSet const &cset, SPCanvasGroup *group) : + ControlPoint(d, initial_pos, anchor, type, cset, group), + _selection(sel) +{ + _selection.allPoints().insert(this); +} + +SelectableControlPoint::SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, + ColorSet const &cset, SPCanvasGroup *group) : + ControlPoint(d, initial_pos, anchor, pixbuf, cset, group), + _selection (sel) +{ + _selection.allPoints().insert(this); +} + +SelectableControlPoint::~SelectableControlPoint() +{ + _selection.erase(this); + _selection.allPoints().erase(this); +} + +bool SelectableControlPoint::grabbed(GdkEventMotion *) +{ + // if a point is dragged while not selected, it should select itself + if (!selected()) { + _takeSelection(); + } + _selection._pointGrabbed(this); + return false; +} + +void SelectableControlPoint::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + _selection._pointDragged(new_pos, event); +} + +void SelectableControlPoint::ungrabbed(GdkEventButton *) +{ + _selection._pointUngrabbed(); +} + +bool SelectableControlPoint::clicked(GdkEventButton *event) +{ + if (_selection._pointClicked(this, event)) + return true; + + if (event->button != 1) return false; + if (held_shift(*event)) { + if (selected()) { + _selection.erase(this); + } else { + _selection.insert(this); + } + } else { + _takeSelection(); + } + return true; +} + +void SelectableControlPoint::select(bool toselect) +{ + if (toselect) { + _selection.insert(this); + } else { + _selection.erase(this); + } +} + +void SelectableControlPoint::_takeSelection() +{ + _selection.clear(); + _selection.insert(this); +} + +bool SelectableControlPoint::selected() const +{ + SelectableControlPoint *p = const_cast<SelectableControlPoint*>(this); + return _selection.find(p) != _selection.end(); +} + +void SelectableControlPoint::_setState(State state) +{ + if (!selected()) { + ControlPoint::_setState(state); + } else { + ColorEntry current = {0, 0}; + ColorSet const &activeCset = (_isLurking()) ? invisible_cset : _cset; + switch (state) { + case STATE_NORMAL: + current = activeCset.selected_normal; + break; + case STATE_MOUSEOVER: + current = activeCset.selected_mouseover; + break; + case STATE_CLICKED: + current = activeCset.selected_clicked; + break; + } + _setColors(current); + _state = state; + } +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selectable-control-point.h b/src/ui/tool/selectable-control-point.h new file mode 100644 index 0000000..919988a --- /dev/null +++ b/src/ui/tool/selectable-control-point.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H +#define SEEN_UI_TOOL_SELECTABLE_CONTROL_POINT_H + +#include "ui/tool/control-point.h" + +namespace Inkscape { +namespace UI { + +class ControlPointSelection; + +/** + * Desktop-bound selectable control object. + */ +class SelectableControlPoint : public ControlPoint { +public: + + ~SelectableControlPoint() override; + bool selected() const; + void updateState() { _setState(_state); } + virtual Geom::Rect bounds() const { + return Geom::Rect(position(), position()); + } + virtual void select(bool toselect); + friend class NodeList; + + +protected: + + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Inkscape::ControlType type, + ControlPointSelection &sel, + ColorSet const &cset = _default_scp_color_set, SPCanvasGroup *group = nullptr); + + SelectableControlPoint(SPDesktop *d, Geom::Point const &initial_pos, SPAnchorType anchor, + Glib::RefPtr<Gdk::Pixbuf> pixbuf, + ControlPointSelection &sel, + ColorSet const &cset = _default_scp_color_set, SPCanvasGroup *group = nullptr); + + void _setState(State state) override; + + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + bool grabbed(GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *event) override; + bool clicked(GdkEventButton *event) override; + + ControlPointSelection &_selection; + +private: + + void _takeSelection(); + + static ColorSet _default_scp_color_set; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selector.cpp b/src/ui/tool/selector.cpp new file mode 100644 index 0000000..934d623 --- /dev/null +++ b/src/ui/tool/selector.cpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "control-point.h" +#include "desktop.h" + +#include "display/sodipodi-ctrlrect.h" +#include "ui/tools/tool-base.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/selector.h" + +#include <gdk/gdkkeysyms.h> + +namespace Inkscape { +namespace UI { + +/** A hidden control point used for rubberbanding and selection. + * It uses a clever hack: the canvas item is hidden and only receives events when they + * are passed to it using Selector's event() function. When left mouse button + * is pressed, it grabs events and handles drags and clicks in the usual way. */ +class SelectorPoint : public ControlPoint { +public: + SelectorPoint(SPDesktop *d, SPCanvasGroup *group, Selector *s) : + ControlPoint(d, Geom::Point(0,0), SP_ANCHOR_CENTER, + CTRL_TYPE_INVISIPOINT, + invisible_cset, group), + _selector(s), + _cancel(false) + { + setVisible(false); + _rubber = static_cast<CtrlRect*>(sp_canvas_item_new(_desktop->getControls(), + SP_TYPE_CTRLRECT, nullptr)); + _rubber->setColor(0x8080ffff, false, 0x0); + _rubber->setInvert(true); + sp_canvas_item_hide(_rubber); + } + + ~SelectorPoint() override { + sp_canvas_item_destroy(_rubber); + } + + SPDesktop *desktop() { return _desktop; } + + bool event(Inkscape::UI::Tools::ToolBase *ec, GdkEvent *e) { + return _eventHandler(ec, e); + } + +protected: + bool _eventHandler(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) override { + if (event->type == GDK_KEY_PRESS && shortcut_key(event->key) == GDK_KEY_Escape && + sp_canvas_item_is_visible(_rubber)) + { + _cancel = true; + sp_canvas_item_hide(_rubber); + return true; + } + return ControlPoint::_eventHandler(event_context, event); + } + +private: + bool grabbed(GdkEventMotion *) override { + _cancel = false; + _start = position(); + sp_canvas_item_show(_rubber); + return false; + } + + void dragged(Geom::Point &new_pos, GdkEventMotion *) override { + if (_cancel) return; + Geom::Rect sel(_start, new_pos); + _rubber->setRectangle(sel); + } + + void ungrabbed(GdkEventButton *event) override { + if (_cancel) return; + sp_canvas_item_hide(_rubber); + Geom::Rect sel(_start, position()); + _selector->signal_area.emit(sel, event); + } + + bool clicked(GdkEventButton *event) override { + if (event->button != 1) return false; + _selector->signal_point.emit(position(), event); + return true; + } + + CtrlRect *_rubber; + Selector *_selector; + Geom::Point _start; + bool _cancel; +}; + + +Selector::Selector(SPDesktop *d) + : Manipulator(d) + , _dragger(new SelectorPoint(d, d->getControls(), this)) +{ + _dragger->setVisible(false); +} + +Selector::~Selector() +{ + delete _dragger; +} + +bool Selector::event(Inkscape::UI::Tools::ToolBase *event_context, GdkEvent *event) +{ + // The hidden control point will capture all events after it obtains the grab, + // but it relies on this function to initiate it. If we pass only first button + // press events here, it won't interfere with any other event handling. + switch (event->type) { + case GDK_BUTTON_PRESS: + // Do not pass button presses other than left button to the control point. + // This way middle click and right click can be handled in ToolBase. + if (event->button.button == 1 && !event_context->space_panning) { + _dragger->setPosition(_desktop->w2d(event_point(event->motion))); + return _dragger->event(event_context, event); + } + break; + default: break; + } + return false; +} + +bool Selector::doubleClicked() { + return _dragger->doubleClicked(); +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/selector.h b/src/ui/tool/selector.h new file mode 100644 index 0000000..6881a63 --- /dev/null +++ b/src/ui/tool/selector.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Selector component (click and rubberband) + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SELECTOR_H +#define SEEN_UI_TOOL_SELECTOR_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/rect.h> +#include "ui/tool/manipulator.h" + +class SPDesktop; +class CtrlRect; + +namespace Inkscape { +namespace UI { + +class SelectorPoint; + +class Selector : public Manipulator { +public: + Selector(SPDesktop *d); + ~Selector() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + virtual bool doubleClicked(); + + sigc::signal<void, Geom::Rect const &, GdkEventButton*> signal_area; + sigc::signal<void, Geom::Point const &, GdkEventButton*> signal_point; +private: + SelectorPoint *_dragger; + Geom::Point _start; + CtrlRect *_rubber; + gulong _connection; + bool _cancel; + friend class SelectorPoint; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/shape-record.h b/src/ui/tool/shape-record.h new file mode 100644 index 0000000..d46a1c0 --- /dev/null +++ b/src/ui/tool/shape-record.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Structures that store data needed for shape editing which are not contained + * directly in the XML node + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_SHAPE_RECORD_H +#define SEEN_UI_TOOL_SHAPE_RECORD_H + +#include <glibmm/ustring.h> +#include <boost/operators.hpp> +#include <2geom/affine.h> + +class SPItem; +namespace Inkscape { +namespace UI { + +/** Role of the shape in the drawing - affects outline display and color */ +enum ShapeRole { + SHAPE_ROLE_NORMAL, + SHAPE_ROLE_CLIPPING_PATH, + SHAPE_ROLE_MASK, + SHAPE_ROLE_LPE_PARAM // implies edit_original set to true in ShapeRecord +}; + +struct ShapeRecord : + public boost::totally_ordered<ShapeRecord> +{ + SPObject *object; // SP node for the edited shape could be a lpeoject invisible so we use a spobject + Geom::Affine edit_transform; // how to transform controls - used for clipping paths and masks + ShapeRole role; + Glib::ustring lpe_key; // name of LPE shape param being edited + + inline bool operator==(ShapeRecord const &o) const { + return object == o.object && lpe_key == o.lpe_key; + } + inline bool operator<(ShapeRecord const &o) const { + return object == o.object ? (lpe_key < o.lpe_key) : (object < o.object); + } +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/transform-handle-set.cpp b/src/ui/tool/transform-handle-set.cpp new file mode 100644 index 0000000..c1d6692 --- /dev/null +++ b/src/ui/tool/transform-handle-set.cpp @@ -0,0 +1,867 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <algorithm> + +#include <glib/gi18n.h> + +#include <2geom/transforms.h> + +#include "control-point.h" +#include "desktop.h" +#include "pure-transform.h" +#include "seltrans.h" +#include "snap.h" + +#include "display/sodipodi-ctrlrect.h" + +#include "object/sp-namedview.h" + +#include "ui/tool/commit-events.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/node.h" +#include "ui/tool/selectable-control-point.h" +#include "ui/tool/transform-handle-set.h" +#include "ui/tools/node-tool.h" + + +// FIXME BRAIN DAMAGE WARNING: this is a global variable in select-context.cpp +// It should be moved to a header +extern GdkPixbuf *handles[]; +GType sp_select_context_get_type(); + +namespace Inkscape { +namespace UI { + +namespace { + +SPAnchorType corner_to_anchor(unsigned c) { + switch (c % 4) { + case 0: return SP_ANCHOR_NE; + case 1: return SP_ANCHOR_NW; + case 2: return SP_ANCHOR_SW; + default: return SP_ANCHOR_SE; + } +} + +SPAnchorType side_to_anchor(unsigned s) { + switch (s % 4) { + case 0: return SP_ANCHOR_N; + case 1: return SP_ANCHOR_W; + case 2: return SP_ANCHOR_S; + default: return SP_ANCHOR_E; + } +} + +// TODO move those two functions into a common place +double snap_angle(double a) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + double unit_angle = M_PI / snaps; + return CLAMP(unit_angle * round(a / unit_angle), -M_PI, M_PI); +} + +double snap_increment_degrees() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int snaps = prefs->getIntLimited("/options/rotationsnapsperpi/value", 12, 1, 1000); + return 180.0 / snaps; +} + +} // anonymous namespace + +ControlPoint::ColorSet TransformHandle::thandle_cset = { + {0x000000ff, 0x000000ff}, + {0x00ff6600, 0x000000ff}, + {0x00ff6600, 0x000000ff}, + // + {0x000000ff, 0x000000ff}, + {0x00ff6600, 0x000000ff}, + {0x00ff6600, 0x000000ff} +}; + +TransformHandle::TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb) : + ControlPoint(th._desktop, Geom::Point(), anchor, + pb, + thandle_cset, th._transform_handle_group), + _th(th) +{ + setVisible(false); +} + +// TODO: This code is duplicated in seltrans.cpp; fix this! +void TransformHandle::getNextClosestPoint(bool reverse) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + if (!_all_snap_sources_sorted.empty()) { + if (reverse) { // Shift-tab will find a closer point + if (_all_snap_sources_iter == _all_snap_sources_sorted.begin()) { + _all_snap_sources_iter = _all_snap_sources_sorted.end(); + } + --_all_snap_sources_iter; + } else { // Tab will find a point further away + ++_all_snap_sources_iter; + if (_all_snap_sources_iter == _all_snap_sources_sorted.end()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + } + } + + _snap_points.clear(); + _snap_points.push_back(*_all_snap_sources_iter); + + // Show the updated snap source now; otherwise it won't be shown until the selection is being moved again + SnapManager &m = _desktop->namedview->snap_manager; + m.setup(_desktop); + m.displaySnapsource(*_all_snap_sources_iter); + m.unSetup(); + } + } +} + +bool TransformHandle::grabbed(GdkEventMotion *) +{ + _origin = position(); + _last_transform.setIdentity(); + startTransform(); + + _th._setActiveHandle(this); + _setLurking(true); + _setState(_state); + + // Collect the snap-candidates, one for each selected node. These will be stored in the _snap_points vector. + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context); + //ControlPointSelection *selection = nt->_selected_nodes.get(); + ControlPointSelection* selection = nt->_selected_nodes; + + selection->setOriginalPoints(); + selection->getOriginalPoints(_snap_points); + selection->getUnselectedPoints(_unselected_points); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/snapclosestonly/value", false)) { + // Find the closest snap source candidate + _all_snap_sources_sorted = _snap_points; + + // Calculate and store the distance to the reference point for each snap candidate point + for(auto & i : _all_snap_sources_sorted) { + i.setDistance(Geom::L2(i.getPoint() - _origin)); + } + + // Sort them ascending, using the distance calculated above as the single criteria + std::sort(_all_snap_sources_sorted.begin(), _all_snap_sources_sorted.end()); + + // Now get the closest snap source + _snap_points.clear(); + if (!_all_snap_sources_sorted.empty()) { + _all_snap_sources_iter = _all_snap_sources_sorted.begin(); + _snap_points.push_back(_all_snap_sources_sorted.front()); + } + } + + return false; +} + +void TransformHandle::dragged(Geom::Point &new_pos, GdkEventMotion *event) +{ + Geom::Affine t = computeTransform(new_pos, event); + // protect against degeneracies + if (t.isSingular()) return; + Geom::Affine incr = _last_transform.inverse() * t; + if (incr.isSingular()) return; + _th.signal_transform.emit(incr); + _last_transform = t; +} + +void TransformHandle::ungrabbed(GdkEventButton *) +{ + _snap_points.clear(); + _th._clearActiveHandle(); + _setLurking(false); + _setState(_state); + endTransform(); + _th.signal_commit.emit(getCommitEvent()); + + //updates the positions of the nodes + Inkscape::UI::Tools::NodeTool *nt = INK_NODE_TOOL(_th._desktop->event_context); + ControlPointSelection* selection = nt->_selected_nodes; + selection->setOriginalPoints(); +} + + +class ScaleHandle : public TransformHandle { +public: + ScaleHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb) + : TransformHandle(th, anchor, pb) + {} +protected: + Glib::ustring _getTip(unsigned state) const override { + if (state_held_control(state)) { + if (state_held_shift(state)) { + return C_("Transform handle tip", + "<b>Shift+Ctrl</b>: scale uniformly about the rotation center"); + } + return C_("Transform handle tip", "<b>Ctrl:</b> scale uniformly"); + } + if (state_held_shift(state)) { + if (state_held_alt(state)) { + return C_("Transform handle tip", + "<b>Shift+Alt</b>: scale using an integer ratio about the rotation center"); + } + return C_("Transform handle tip", "<b>Shift</b>: scale from the rotation center"); + } + if (state_held_alt(state)) { + return C_("Transform handle tip", "<b>Alt</b>: scale using an integer ratio"); + } + return C_("Transform handle tip", "<b>Scale handle</b>: drag to scale the selection"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + return format_tip(C_("Transform handle tip", + "Scale by %.2f%% x %.2f%%"), _last_scale_x * 100, _last_scale_y * 100); + } + + bool _hasDragTips() const override { return true; } + + static double _last_scale_x, _last_scale_y; +}; +double ScaleHandle::_last_scale_x = 1.0; +double ScaleHandle::_last_scale_y = 1.0; + +/** + * Corner scaling handle for node transforms. + */ +class ScaleCornerHandle : public ScaleHandle { +public: + + ScaleCornerHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) : + ScaleHandle(th, corner_to_anchor(d_corner), _corner_to_pixbuf(d_corner)), + _corner(corner) + {} + +protected: + void startTransform() override { + _sc_center = _th.rotationCenter(); + _sc_opposite = _th.bounds().corner(_corner + 2); + _last_scale_x = _last_scale_y = 1.0; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vold = _origin - scc, vnew = new_pos - scc; + // avoid exploding the selection + if (Geom::are_near(vold[Geom::X], 0) || Geom::are_near(vold[Geom::Y], 0)) + return Geom::identity(); + + Geom::Scale scale = Geom::Scale(vnew[Geom::X] / vold[Geom::X], vnew[Geom::Y] / vold[Geom::Y]); + + if (held_alt(*event)) { + for (unsigned i = 0; i < 2; ++i) { + if (fabs(scale[i]) >= 1.0) { + scale[i] = round(scale[i]); + } else { + scale[i] = 1.0 / round(1.0 / MIN(scale[i],10)); + } + } + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + Inkscape::PureScale *ptr; + if (held_control(*event)) { + scale[0] = scale[1] = std::min(scale[0], scale[1]); + ptr = new Inkscape::PureScaleConstrained(Geom::Scale(scale[0], scale[1]), scc); + } else { + ptr = new Inkscape::PureScale(Geom::Scale(scale[0], scale[1]), scc, false); + } + m.snapTransformed(_snap_points, _origin, (*ptr)); + m.unSetup(); + if (ptr->best_snapped_point.getSnapped()) { + scale = ptr->getScaleSnapped(); + } + + delete ptr; + } + + _last_scale_x = scale[0]; + _last_scale_y = scale[1]; + Geom::Affine t = Geom::Translate(-scc) + * Geom::Scale(scale[0], scale[1]) + * Geom::Translate(scc); + return t; + } + + CommitEvent getCommitEvent() override { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } + +private: + + static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) { + //sp_select_context_get_type(); + switch (c % 2) { + case 0: + return Glib::wrap(handles[1], true); + break; + default: + return Glib::wrap(handles[0], true); + break; + } + } + + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _corner; +}; + +/** + * Side scaling handle for node transforms. + */ +class ScaleSideHandle : public ScaleHandle { +public: + ScaleSideHandle(TransformHandleSet &th, unsigned side, unsigned d_side) + : ScaleHandle(th, side_to_anchor(d_side), _side_to_pixbuf(side)) + , _side(side) + {} +protected: + void startTransform() override { + _sc_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _sc_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_scale_x = _last_scale_y = 1.0; + } + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override { + Geom::Point scc = held_shift(*event) ? _sc_center : _sc_opposite; + Geom::Point vs; + Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2); + + // avoid exploding the selection + if (Geom::are_near(scc[d1], _origin[d1])) + return Geom::identity(); + + vs[d1] = (new_pos - scc)[d1] / (_origin - scc)[d1]; + if (held_alt(*event)) { + if (fabs(vs[d1]) >= 1.0) { + vs[d1] = round(vs[d1]); + } else { + vs[d1] = 1.0 / round(1.0 / MIN(vs[d1],10)); + } + vs[d2] = 1.0; + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + bool uniform = held_control(*event); + Inkscape::PureStretchConstrained psc = Inkscape::PureStretchConstrained(vs[d1], scc, d1, uniform); + m.snapTransformed(_snap_points, _origin, psc); + m.unSetup(); + + if (psc.best_snapped_point.getSnapped()) { + Geom::Point result = psc.getStretchSnapped().vector(); //best_snapped_point.getTransformation(); + vs[d1] = result[d1]; + vs[d2] = result[d2]; + } else { + // on ctrl, apply uniform scaling instead of stretching + // Preserve aspect ratio, but never flip in the dimension not being edited (by using fabs()) + vs[d2] = uniform ? fabs(vs[d1]) : 1.0; + } + } + + _last_scale_x = vs[Geom::X]; + _last_scale_y = vs[Geom::Y]; + Geom::Affine t = Geom::Translate(-scc) + * Geom::Scale(vs) + * Geom::Translate(scc); + return t; + } + CommitEvent getCommitEvent() override { + return _last_transform.isUniformScale() + ? COMMIT_MOUSE_SCALE_UNIFORM + : COMMIT_MOUSE_SCALE; + } +private: + static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned c) { + //sp_select_context_get_type(); + switch (c % 2) { + case 0: return Glib::wrap(handles[3], true); + default: return Glib::wrap(handles[2], true); + } + } + Geom::Point _sc_center; + Geom::Point _sc_opposite; + unsigned _side; +}; + +/** + * Rotation handle for node transforms. + */ +class RotateHandle : public TransformHandle { +public: + RotateHandle(TransformHandleSet &th, unsigned corner, unsigned d_corner) + : TransformHandle(th, corner_to_anchor(d_corner), _corner_to_pixbuf(d_corner)) + , _corner(corner) + {} +protected: + + void startTransform() override { + _rot_center = _th.rotationCenter(); + _rot_opposite = _th.bounds().corner(_corner + 2); + _last_angle = 0; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override + { + Geom::Point rotc = held_shift(*event) ? _rot_opposite : _rot_center; + double angle = Geom::angle_between(_origin - rotc, new_pos - rotc); + if (held_control(*event)) { + angle = snap_angle(angle); + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + Inkscape::PureRotateConstrained prc = Inkscape::PureRotateConstrained(angle, rotc); + m.snapTransformed(_snap_points, _origin, prc); + m.unSetup(); + + if (prc.best_snapped_point.getSnapped()) { + angle = prc.getAngleSnapped(); //best_snapped_point.getTransformation()[0]; + } + } + + _last_angle = angle; + Geom::Affine t = Geom::Translate(-rotc) + * Geom::Rotate(angle) + * Geom::Translate(rotc); + return t; + } + + CommitEvent getCommitEvent() override { return COMMIT_MOUSE_ROTATE; } + + Glib::ustring _getTip(unsigned state) const override { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl</b>: rotate around the opposite corner and snap " + "angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift</b>: rotate around the opposite corner"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl</b>: snap angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Rotation handle</b>: drag to rotate " + "the selection around the rotation center"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + return format_tip(C_("Transform handle tip", "Rotate by %.2f°"), + _last_angle * 180.0 / M_PI); + } + + bool _hasDragTips() const override { return true; } + +private: + static Glib::RefPtr<Gdk::Pixbuf> _corner_to_pixbuf(unsigned c) { + //sp_select_context_get_type(); + switch (c % 4) { + case 0: return Glib::wrap(handles[7], true); + case 1: return Glib::wrap(handles[6], true); + case 2: return Glib::wrap(handles[5], true); + default: return Glib::wrap(handles[4], true); + } + } + Geom::Point _rot_center; + Geom::Point _rot_opposite; + unsigned _corner; + static double _last_angle; +}; +double RotateHandle::_last_angle = 0; + +class SkewHandle : public TransformHandle { +public: + SkewHandle(TransformHandleSet &th, unsigned side, unsigned d_side) + : TransformHandle(th, side_to_anchor(d_side), _side_to_pixbuf(side)) + , _side(side) + {} + +protected: + + void startTransform() override { + _skew_center = _th.rotationCenter(); + Geom::Rect b = _th.bounds(); + _skew_opposite = Geom::middle_point(b.corner(_side + 2), b.corner(_side + 3)); + _last_angle = 0; + _last_horizontal = _side % 2; + } + + Geom::Affine computeTransform(Geom::Point const &new_pos, GdkEventMotion *event) override + { + Geom::Point scc = held_shift(*event) ? _skew_center : _skew_opposite; + Geom::Dim2 d1 = static_cast<Geom::Dim2>((_side + 1) % 2); + Geom::Dim2 d2 = static_cast<Geom::Dim2>(_side % 2); + + Geom::Point const initial_delta = _origin - scc; + + if (fabs(initial_delta[d1]) < 1e-15) { + return Geom::Affine(); + } + + // Calculate the scale factors, which can be either visual or geometric + // depending on which type of bbox is currently being used (see preferences -> selector tool) + Geom::Scale scale = calcScaleFactors(_origin, new_pos, scc, false); + Geom::Scale skew = calcScaleFactors(_origin, new_pos, scc, true); + scale[d2] = 1; + skew[d2] = 1; + + // Skew handles allow scaling up to integer multiples of the original size + // in the second direction; prevent explosions + + if (fabs(scale[d1]) < 1) { + // Prevent shrinking of the selected object, while allowing mirroring + scale[d1] = copysign(1.0, scale[d1]); + } else { + // Allow expanding of the selected object by integer multiples + scale[d1] = floor(scale[d1] + 0.5); + } + + double angle = atan(skew[d1] / scale[d1]); + + if (held_control(*event)) { + angle = snap_angle(angle); + skew[d1] = tan(angle) * scale[d1]; + } else { + SnapManager &m = _th._desktop->namedview->snap_manager; + m.setupIgnoreSelection(_th._desktop, true, &_unselected_points); + + Inkscape::PureSkewConstrained psc = Inkscape::PureSkewConstrained(skew[d1], scale[d1], scc, d2); + m.snapTransformed(_snap_points, _origin, psc); + m.unSetup(); + + if (psc.best_snapped_point.getSnapped()) { + skew[d1] = psc.getSkewSnapped(); //best_snapped_point.getTransformation()[0]; + } + } + + _last_angle = angle; + + // Update the handle position + Geom::Point new_new_pos; + new_new_pos[d2] = initial_delta[d1] * skew[d1] + _origin[d2]; + new_new_pos[d1] = initial_delta[d1] * scale[d1] + scc[d1]; + + // Calculate the relative affine + Geom::Affine relative_affine = Geom::identity(); + relative_affine[2*d1 + d1] = (new_new_pos[d1] - scc[d1]) / initial_delta[d1]; + relative_affine[2*d1 + (d2)] = (new_new_pos[d2] - _origin[d2]) / initial_delta[d1]; + relative_affine[2*(d2) + (d1)] = 0; + relative_affine[2*(d2) + (d2)] = 1; + + for (int i = 0; i < 2; i++) { + if (fabs(relative_affine[3*i]) < 1e-15) { + relative_affine[3*i] = 1e-15; + } + } + + Geom::Affine t = Geom::Translate(-scc) + * relative_affine + * Geom::Translate(scc); + + return t; + } + + CommitEvent getCommitEvent() override { + return (_side % 2) + ? COMMIT_MOUSE_SKEW_Y + : COMMIT_MOUSE_SKEW_X; + } + + Glib::ustring _getTip(unsigned state) const override { + if (state_held_shift(state)) { + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Shift+Ctrl</b>: skew about the rotation center with snapping " + "to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", "<b>Shift</b>: skew about the rotation center"); + } + if (state_held_control(state)) { + return format_tip(C_("Transform handle tip", + "<b>Ctrl</b>: snap skew angle to %f° increments"), snap_increment_degrees()); + } + return C_("Transform handle tip", + "<b>Skew handle</b>: drag to skew (shear) selection about " + "the opposite handle"); + } + + Glib::ustring _getDragTip(GdkEventMotion */*event*/) const override { + if (_last_horizontal) { + return format_tip(C_("Transform handle tip", "Skew horizontally by %.2f°"), + _last_angle * 360.0); + } else { + return format_tip(C_("Transform handle tip", "Skew vertically by %.2f°"), + _last_angle * 360.0); + } + } + + bool _hasDragTips() const override { return true; } + +private: + + static Glib::RefPtr<Gdk::Pixbuf> _side_to_pixbuf(unsigned s) { + //sp_select_context_get_type(); + switch (s % 4) { + case 0: return Glib::wrap(handles[10], true); + case 1: return Glib::wrap(handles[9], true); + case 2: return Glib::wrap(handles[8], true); + default: return Glib::wrap(handles[11], true); + } + } + Geom::Point _skew_center; + Geom::Point _skew_opposite; + unsigned _side; + static bool _last_horizontal; + static double _last_angle; +}; +bool SkewHandle::_last_horizontal = false; +double SkewHandle::_last_angle = 0; + +class RotationCenter : public ControlPoint { + +public: + RotationCenter(TransformHandleSet &th) : + ControlPoint(th._desktop, Geom::Point(), SP_ANCHOR_CENTER, + _get_pixbuf(), + _center_cset, th._transform_handle_group), + _th(th) + { + setVisible(false); + } + +protected: + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override { + SnapManager &sm = _th._desktop->namedview->snap_manager; + sm.setup(_th._desktop); + bool snap = !held_shift(*event) && sm.someSnapperMightSnap(); + if (held_control(*event)) { + // constrain to axes + Geom::Point origin = _last_drag_origin(); + std::vector<Inkscape::Snapper::SnapConstraint> constraints; + constraints.emplace_back(origin, Geom::Point(1, 0)); + constraints.emplace_back(origin, Geom::Point(0, 1)); + new_pos = sm.multipleConstrainedSnaps(Inkscape::SnapCandidatePoint(new_pos, + SNAPSOURCE_ROTATION_CENTER), constraints, held_shift(*event)).getPoint(); + } else if (snap) { + sm.freeSnapReturnByRef(new_pos, SNAPSOURCE_ROTATION_CENTER); + } + sm.unSetup(); + } + Glib::ustring _getTip(unsigned /*state*/) const override { + return C_("Transform handle tip", + "<b>Rotation center</b>: drag to change the origin of transforms"); + } + +private: + + static Glib::RefPtr<Gdk::Pixbuf> _get_pixbuf() { + //sp_select_context_get_type(); + return Glib::wrap(handles[12], true); + } + + static ColorSet _center_cset; + TransformHandleSet &_th; +}; + +ControlPoint::ColorSet RotationCenter::_center_cset = { + {0x00000000, 0x000000ff}, + {0x00000000, 0xff0000b0}, + {0x00000000, 0xff0000b0}, + // + {0x00000000, 0x000000ff}, + {0x00000000, 0xff0000b0}, + {0x00000000, 0xff0000b0} +}; + + +TransformHandleSet::TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group) + : Manipulator(d) + , _active(nullptr) + , _transform_handle_group(th_group) + , _mode(MODE_SCALE) + , _in_transform(false) + , _visible(true) +{ + _trans_outline = static_cast<CtrlRect*>(sp_canvas_item_new(_desktop->getControls(), + SP_TYPE_CTRLRECT, nullptr)); + sp_canvas_item_hide(_trans_outline); + _trans_outline->setDashed(true); + + bool y_inverted = !d->is_yaxisdown(); + for (unsigned i = 0; i < 4; ++i) { + unsigned d_c = y_inverted ? i : 3 - i; + unsigned d_s = y_inverted ? i : 6 - i; + _scale_corners[i] = new ScaleCornerHandle(*this, i, d_c); + _scale_sides[i] = new ScaleSideHandle(*this, i, d_s); + _rot_corners[i] = new RotateHandle(*this, i, d_c); + _skew_sides[i] = new SkewHandle(*this, i, d_s); + } + _center = new RotationCenter(*this); + // when transforming, update rotation center position + signal_transform.connect(sigc::mem_fun(*_center, &RotationCenter::transform)); +} + +TransformHandleSet::~TransformHandleSet() +{ + for (auto & _handle : _handles) { + delete _handle; + } +} + +void TransformHandleSet::setMode(Mode m) +{ + _mode = m; + _updateVisibility(_visible); +} + +Geom::Rect TransformHandleSet::bounds() const +{ + return Geom::Rect(*_scale_corners[0], *_scale_corners[2]); +} + +ControlPoint const &TransformHandleSet::rotationCenter() const +{ + return *_center; +} + +ControlPoint &TransformHandleSet::rotationCenter() +{ + return *_center; +} + +void TransformHandleSet::setVisible(bool v) +{ + if (_visible != v) { + _visible = v; + _updateVisibility(_visible); + } +} + +void TransformHandleSet::setBounds(Geom::Rect const &r, bool preserve_center) +{ + if (_in_transform) { + _trans_outline->setRectangle(r); + } else { + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->move(r.corner(i)); + _scale_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + _rot_corners[i]->move(r.corner(i)); + _skew_sides[i]->move(Geom::middle_point(r.corner(i), r.corner(i+1))); + } + if (!preserve_center) _center->move(r.midpoint()); + if (_visible) _updateVisibility(true); + } +} + +bool TransformHandleSet::event(Inkscape::UI::Tools::ToolBase *, GdkEvent*) +{ + return false; +} + +void TransformHandleSet::_emitTransform(Geom::Affine const &t) +{ + signal_transform.emit(t); + _center->transform(t); +} + +void TransformHandleSet::_setActiveHandle(ControlPoint *th) +{ + _active = th; + if (_in_transform) + throw std::logic_error("Transform initiated when another transform in progress"); + _in_transform = true; + // hide all handles except the active one + _updateVisibility(false); + sp_canvas_item_show(_trans_outline); +} + +void TransformHandleSet::_clearActiveHandle() +{ + // This can only be called from handles, so they had to be visible before _setActiveHandle + sp_canvas_item_hide(_trans_outline); + _active = nullptr; + _in_transform = false; + _updateVisibility(_visible); +} + +void TransformHandleSet::_updateVisibility(bool v) +{ + if (v) { + Geom::Rect b = bounds(); + Geom::Point handle_size( + gdk_pixbuf_get_width(handles[0]) / _desktop->current_zoom(), + gdk_pixbuf_get_height(handles[0]) / _desktop->current_zoom()); + Geom::Point bp = b.dimensions(); + + // do not scale when the bounding rectangle has zero width or height + bool show_scale = (_mode == MODE_SCALE) && !Geom::are_near(b.minExtent(), 0); + // do not rotate if the bounding rectangle is degenerate + bool show_rotate = (_mode == MODE_ROTATE_SKEW) && !Geom::are_near(b.maxExtent(), 0); + bool show_scale_side[2], show_skew[2]; + + // show sides if: + // a) there is enough space between corner handles, or + // b) corner handles are not shown, but side handles make sense + // this affects horizontal and vertical scale handles; skew handles never + // make sense if rotate handles are not shown + for (unsigned i = 0; i < 2; ++i) { + Geom::Dim2 d = static_cast<Geom::Dim2>(i); + Geom::Dim2 otherd = static_cast<Geom::Dim2>((i+1)%2); + show_scale_side[i] = (_mode == MODE_SCALE); + show_scale_side[i] &= (show_scale ? bp[d] >= handle_size[d] + : !Geom::are_near(bp[otherd], 0)); + show_skew[i] = (show_rotate && bp[d] >= handle_size[d] + && !Geom::are_near(bp[otherd], 0)); + } + for (unsigned i = 0; i < 4; ++i) { + _scale_corners[i]->setVisible(show_scale); + _rot_corners[i]->setVisible(show_rotate); + _scale_sides[i]->setVisible(show_scale_side[i%2]); + _skew_sides[i]->setVisible(show_skew[i%2]); + } + // show rotation center if there is enough space (?) + _center->setVisible(show_rotate /*&& bp[Geom::X] > handle_size[Geom::X] + && bp[Geom::Y] > handle_size[Geom::Y]*/); + } else { + for (auto & _handle : _handles) { + if (_handle != _active) + _handle->setVisible(false); + } + } + +} + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tool/transform-handle-set.h b/src/ui/tool/transform-handle-set.h new file mode 100644 index 0000000..803e27c --- /dev/null +++ b/src/ui/tool/transform-handle-set.h @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Affine transform handles component + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H +#define SEEN_UI_TOOL_TRANSFORM_HANDLE_SET_H + +#include <memory> +#include <gdk/gdk.h> +#include <2geom/forward.h> +#include "ui/tool/commit-events.h" +#include "ui/tool/manipulator.h" +#include "enums.h" + +class SPDesktop; +class CtrlRect; +namespace Inkscape { +namespace UI { + +class RotateHandle; +class SkewHandle; +class ScaleCornerHandle; +class ScaleSideHandle; +class RotationCenter; + +class TransformHandleSet : public Manipulator { +public: + + enum Mode { + MODE_SCALE, + MODE_ROTATE_SKEW + }; + + TransformHandleSet(SPDesktop *d, SPCanvasGroup *th_group); + ~TransformHandleSet() override; + bool event(Inkscape::UI::Tools::ToolBase *, GdkEvent *) override; + + bool visible() const { return _visible; } + Mode mode() const { return _mode; } + Geom::Rect bounds() const; + void setVisible(bool v); + + /** Sets the mode of transform handles (scale or rotate). */ + void setMode(Mode m); + + void setBounds(Geom::Rect const &, bool preserve_center = false); + + bool transforming() { return _in_transform; } + + ControlPoint const &rotationCenter() const; + ControlPoint &rotationCenter(); + + sigc::signal<void, Geom::Affine const &> signal_transform; + sigc::signal<void, CommitEvent> signal_commit; + +private: + + void _emitTransform(Geom::Affine const &); + void _setActiveHandle(ControlPoint *h); + void _clearActiveHandle(); + + /** Update the visibility of transformation handles according to settings and the dimensions + * of the bounding box. It hides the handles that would have no effect or lead to + * discontinuities. Additionally, side handles for which there is no space are not shown. + */ + void _updateVisibility(bool v); + + // TODO unions must GO AWAY: + union { + ControlPoint *_handles[17]; + struct { + ScaleCornerHandle *_scale_corners[4]; + ScaleSideHandle *_scale_sides[4]; + RotateHandle *_rot_corners[4]; + SkewHandle *_skew_sides[4]; + RotationCenter *_center; + }; + }; + + ControlPoint *_active; + SPCanvasGroup *_transform_handle_group; + CtrlRect *_trans_outline; + Mode _mode; + bool _in_transform; + bool _visible; + bool _rot_center_visible; + friend class TransformHandle; + friend class RotationCenter; +}; + +/** Base class for node transform handles to simplify implementation. */ +class TransformHandle : public ControlPoint +{ +public: + TransformHandle(TransformHandleSet &th, SPAnchorType anchor, Glib::RefPtr<Gdk::Pixbuf> pb); + void getNextClosestPoint(bool reverse); + +protected: + virtual void startTransform() {} + virtual void endTransform() {} + virtual Geom::Affine computeTransform(Geom::Point const &pos, GdkEventMotion *event) = 0; + virtual CommitEvent getCommitEvent() = 0; + + Geom::Affine _last_transform; + Geom::Point _origin; + TransformHandleSet &_th; + std::vector<Inkscape::SnapCandidatePoint> _snap_points; + std::vector<Inkscape::SnapCandidatePoint> _unselected_points; + std::vector<Inkscape::SnapCandidatePoint> _all_snap_sources_sorted; + std::vector<Inkscape::SnapCandidatePoint>::iterator _all_snap_sources_iter; + +private: + bool grabbed(GdkEventMotion *) override; + void dragged(Geom::Point &new_pos, GdkEventMotion *event) override; + void ungrabbed(GdkEventButton *) override; + + static ColorSet thandle_cset; +}; + +} // namespace UI +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/arc-toolbar.cpp b/src/ui/toolbar/arc-toolbar.cpp new file mode 100644 index 0000000..6c17ab1 --- /dev/null +++ b/src/ui/toolbar/arc-toolbar.cpp @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Arc aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "arc-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "mod360.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-ellipse.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/pref-pusher.h" +#include "ui/tools/arc-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/spinbutton-events.h" +#include "widgets/widget-sizes.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; + + +static Inkscape::XML::NodeEventVector arc_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::ArcToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +ArcToolbar::ArcToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _repr(nullptr) +{ + _tracker->setActiveUnit(desktop->getNamedView()->display_units); + auto prefs = Inkscape::Preferences::get(); + + { + _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))); + _mode_item->set_use_markup(true); + add(*_mode_item); + } + + /* Radius X */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto rx_val = prefs->getDouble("/tools/shapes/arc/rx", 0); + _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-rx", _("Rx:"), _rx_adj)); + _rx_item->set_tooltip_text(_("Horizontal radius of the circle, ellipse, or arc")); + _rx_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_rx_adj->gobj()); + _rx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed), + _rx_adj, "rx")); + _rx_item->set_sensitive(false); + add(*_rx_item); + } + + /* Radius Y */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto ry_val = prefs->getDouble("/tools/shapes/arc/ry", 0); + _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-ry", _("Ry:"), _ry_adj)); + _ry_item->set_tooltip_text(_("Vertical radius of the circle, ellipse, or arc")); + _ry_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_ry_adj->gobj()); + _ry_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::value_changed), + _ry_adj, "ry")); + _ry_item->set_sensitive(false); + add(*_ry_item); + } + + // add the units menu + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("") ); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Start */ + { + auto start_val = prefs->getDouble("/tools/shapes/arc/start", 0.0); + _start_adj = Gtk::Adjustment::create(start_val, -360.0, 360.0, 1.0, 10.0); + auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-start", _("Start:"), _start_adj)); + eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's start point")); + eact->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + add(*eact); + } + + /* End */ + { + auto end_val = prefs->getDouble("/tools/shapes/arc/end", 0.0); + _end_adj = Gtk::Adjustment::create(end_val, -360.0, 360.0, 1.0, 10.0); + auto eact = Gtk::manage(new UI::Widget::SpinButtonToolItem("arc-end", _("End:"), _end_adj)); + eact->set_tooltip_text(_("The angle (in degrees) from the horizontal to the arc's end point")); + eact->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + add(*eact); + } + _start_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed), + _start_adj, "start", _end_adj)); + _end_adj->signal_value_changed().connect( sigc::bind(sigc::mem_fun(*this, &ArcToolbar::startend_value_changed), + _end_adj, "end", _start_adj)); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Arc: Slice, Arc, Chord */ + { + Gtk::RadioToolButton::Group type_group; + + auto slice_btn = Gtk::manage(new Gtk::RadioToolButton(_("Slice"))); + slice_btn->set_tooltip_text(_("Switch to slice (closed shape with two radii)")); + slice_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-segment")); + _type_buttons.push_back(slice_btn); + + auto arc_btn = Gtk::manage(new Gtk::RadioToolButton(_("Arc (Open)"))); + arc_btn->set_tooltip_text(_("Switch to arc (unclosed shape)")); + arc_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-arc")); + _type_buttons.push_back(arc_btn); + + auto chord_btn = Gtk::manage(new Gtk::RadioToolButton(_("Chord"))); + chord_btn->set_tooltip_text(_("Switch to chord (closed shape)")); + chord_btn->set_icon_name(INKSCAPE_ICON("draw-ellipse-chord")); + _type_buttons.push_back(chord_btn); + + slice_btn->set_group(type_group); + arc_btn->set_group(type_group); + chord_btn->set_group(type_group); + + gint type = prefs->getInt("/tools/shapes/arc/arc_type", 0); + _type_buttons[type]->set_active(); + + int btn_index = 0; + for (auto btn : _type_buttons) + { + btn->set_sensitive(); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &ArcToolbar::type_changed), btn_index++)); + add(*btn); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Make Whole */ + { + _make_whole = Gtk::manage(new Gtk::ToolButton(_("Make whole"))); + _make_whole->set_tooltip_text(_("Make the shape a whole ellipse, not arc or segment")); + _make_whole->set_icon_name(INKSCAPE_ICON("draw-ellipse-whole")); + _make_whole->signal_clicked().connect(sigc::mem_fun(*this, &ArcToolbar::defaults)); + add(*_make_whole); + _make_whole->set_sensitive(true); + } + + _single = true; + // sensitivize make whole and open checkbox + { + sensitivize( _start_adj->get_value(), _end_adj->get_value() ); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &ArcToolbar::check_ec)); + + show_all(); +} + +ArcToolbar::~ArcToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } +} + +GtkWidget * +ArcToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new ArcToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +ArcToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name) +{ + // Per SVG spec "a [radius] value of zero disables rendering of the element". + // However our implementation does not allow a setting of zero in the UI (not even in the XML editor) + // and ugly things happen if it's forced here, so better leave the properties untouched. + if (!adj->get_value()) { + return; + } + + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + SPDocument* document = _desktop->getDocument(); + Geom::Scale scale = document->getDocumentScale(); + + if (DocumentUndo::getUndoSensitive(document)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name, + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + + SPGenericEllipse *ge = SP_GENERICELLIPSE(item); + + if (!strcmp(value_name, "rx")) { + ge->setVisibleRx(Quantity::convert(adj->get_value(), unit, "px")); + } else { + ge->setVisibleRy(Quantity::convert(adj->get_value(), unit, "px")); + } + + ge->normalize(); + (SP_OBJECT(ge))->updateRepr(); + (SP_OBJECT(ge))->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_ARC, + _("Ellipse: Change radius")); + } + + _freeze = false; +} + +void +ArcToolbar::startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + Glib::RefPtr<Gtk::Adjustment>& other_adj) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/arc/") + value_name, adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + gchar* namespaced_name = g_strconcat("sodipodi:", value_name, NULL); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + + SPGenericEllipse *ge = SP_GENERICELLIPSE(item); + + if (!strcmp(value_name, "start")) { + ge->start = (adj->get_value() * M_PI)/ 180; + } else { + ge->end = (adj->get_value() * M_PI)/ 180; + } + + ge->normalize(); + (SP_OBJECT(ge))->updateRepr(); + (SP_OBJECT(ge))->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + + modmade = true; + } + } + + g_free(namespaced_name); + + sensitivize( adj->get_value(), other_adj->get_value() ); + + if (modmade) { + DocumentUndo::maybeDone(_desktop->getDocument(), value_name, SP_VERB_CONTEXT_ARC, + _("Arc: Change start/end")); + } + + _freeze = false; +} + +void +ArcToolbar::type_changed( int type ) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/shapes/arc/arc_type", type); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + Glib::ustring arc_type = "slice"; + bool open = false; + switch (type) { + case 0: + arc_type = "slice"; + open = false; + break; + case 1: + arc_type = "arc"; + open = true; + break; + case 2: + arc_type = "chord"; + open = true; // For backward compat, not truly open but chord most like arc. + break; + default: + std::cerr << "sp_arctb_type_changed: bad arc type: " << type << std::endl; + } + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_GENERICELLIPSE(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttribute("sodipodi:open", (open?"true":nullptr) ); + repr->setAttribute("sodipodi:arc-type", arc_type); + item->updateRepr(); + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_ARC, + _("Arc: Changed arc type")); + } + + _freeze = false; +} + +void +ArcToolbar::defaults() +{ + _start_adj->set_value(0.0); + _end_adj->set_value(0.0); + + if(_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(_desktop->canvas)); +} + +void +ArcToolbar::sensitivize( double v1, double v2 ) +{ + if (v1 == 0 && v2 == 0) { + if (_single) { // only for a single selected ellipse (for now) + for (auto btn : _type_buttons) btn->set_sensitive(false); + _make_whole->set_sensitive(false); + } + } else { + for (auto btn : _type_buttons) btn->set_sensitive(true); + _make_whole->set_sensitive(true); + } +} + +void +ArcToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_ARC_CONTEXT(ec)) { + _changed = _desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ArcToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) { + _changed.disconnect(); + if(_repr) { + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + } + } +} + +void +ArcToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if ( _repr ) { + _item = nullptr; + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + SPItem *item = nullptr; + + for(auto i : selection->items()){ + if (SP_IS_GENERICELLIPSE(i)) { + n_selected++; + item = i; + repr = item->getRepr(); + } + } + + _single = false; + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _single = true; + _mode_item->set_markup(_("<b>Change:</b>")); + _rx_item->set_sensitive(true); + _ry_item->set_sensitive(true); + + if (repr) { + _repr = repr; + _item = item; + Inkscape::GC::anchor(_repr); + _repr->addListener(&arc_tb_repr_events, this); + _repr->synthesizeEvents(&arc_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + sensitivize( 1, 0 ); + } +} + +void +ArcToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const * /*name*/, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer data) +{ + auto toolbar = reinterpret_cast<ArcToolbar *>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + if (toolbar->_item && SP_IS_GENERICELLIPSE(toolbar->_item)) { + SPGenericEllipse *ge = SP_GENERICELLIPSE(toolbar->_item); + + Unit const *unit = toolbar->_tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + gdouble rx = ge->getVisibleRx(); + gdouble ry = ge->getVisibleRy(); + toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit)); + toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit)); + } + + gdouble start = 0.; + gdouble end = 0.; + sp_repr_get_double(repr, "sodipodi:start", &start); + sp_repr_get_double(repr, "sodipodi:end", &end); + + toolbar->_start_adj->set_value(mod360((start * 180)/M_PI)); + toolbar->_end_adj->set_value(mod360((end * 180)/M_PI)); + + toolbar->sensitivize(toolbar->_start_adj->get_value(), toolbar->_end_adj->get_value()); + + char const *arctypestr = nullptr; + arctypestr = repr->attribute("sodipodi:arc-type"); + if (!arctypestr) { // For old files. + char const *openstr = nullptr; + openstr = repr->attribute("sodipodi:open"); + arctypestr = (openstr ? "arc" : "slice"); + } + + if (!strcmp(arctypestr,"slice")) { + toolbar->_type_buttons[0]->set_active(); + } else if (!strcmp(arctypestr,"arc")) { + toolbar->_type_buttons[1]->set_active(); + } else { + toolbar->_type_buttons[2]->set_active(); + } + + toolbar->_freeze = false; +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/arc-toolbar.h b/src/ui/toolbar/arc-toolbar.h new file mode 100644 index 0000000..b0b0450 --- /dev/null +++ b/src/ui/toolbar/arc-toolbar.h @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ARC_TOOLBAR_H +#define SEEN_ARC_TOOLBAR_H + +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPItem; + +namespace Gtk { +class RadioToolButton; +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class ArcToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + + UI::Widget::SpinButtonToolItem *_rx_item; + UI::Widget::SpinButtonToolItem *_ry_item; + + UI::Widget::LabelToolItem *_mode_item; + + std::vector<Gtk::RadioToolButton *> _type_buttons; + Gtk::ToolButton *_make_whole; + + Glib::RefPtr<Gtk::Adjustment> _rx_adj; + Glib::RefPtr<Gtk::Adjustment> _ry_adj; + Glib::RefPtr<Gtk::Adjustment> _start_adj; + Glib::RefPtr<Gtk::Adjustment> _end_adj; + + bool _freeze; + bool _single; + + XML::Node *_repr; + SPItem *_item; + + void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name); + void startend_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + Glib::RefPtr<Gtk::Adjustment>& other_adj); + void type_changed( int type ); + void defaults(); + void sensitivize( double v1, double v2 ); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + + sigc::connection _changed; + +protected: + ArcToolbar(SPDesktop *desktop); + ~ArcToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); +}; + +} +} +} + +#endif /* !SEEN_ARC_TOOLBAR_H */ diff --git a/src/ui/toolbar/box3d-toolbar.cpp b/src/ui/toolbar/box3d-toolbar.cpp new file mode 100644 index 0000000..8f14277 --- /dev/null +++ b/src/ui/toolbar/box3d-toolbar.cpp @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "box3d-toolbar.h" + +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/box3d.h" +#include "object/persp3d.h" + +#include "ui/icon-names.h" +#include "ui/pref-pusher.h" +#include "ui/tools/box3d-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector box3d_persp_tb_repr_events = +{ + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::Box3DToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +Box3DToolbar::Box3DToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _repr(nullptr), + _freeze(false) +{ + auto prefs = Inkscape::Preferences::get(); + auto document = desktop->getDocument(); + auto persp_impl = document->getCurrentPersp3DImpl(); + + /* Angle X */ + { + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + auto angle_x_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_x", 30); + _angle_x_adj = Gtk::Adjustment::create(angle_x_val, -360.0, 360.0, 1.0, 10.0); + _angle_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-x", _("Angle X:"), _angle_x_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_x_item->set_tooltip_text(_("Angle of PLs in X direction")); + _angle_x_item->set_custom_numeric_menu_data(values); + _angle_x_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _angle_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_x_adj, Proj::X)); + add(*_angle_x_item); + } + + if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::X)) { + _angle_x_item->set_sensitive(true); + } else { + _angle_x_item->set_sensitive(false); + } + + /* VP X state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_x_state_item = add_toggle_button(_("State of VP in X direction"), + _("Toggle VP in X direction between 'finite' and 'infinite' (=parallel)")); + _vp_x_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_x_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::X)); + _angle_x_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) ); + _vp_x_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_x_state", true) ); + } + + /* Angle Y */ + { + auto angle_y_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_y", 30); + _angle_y_adj = Gtk::Adjustment::create(angle_y_val, -360.0, 360.0, 1.0, 10.0); + _angle_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-y", _("Angle Y:"), _angle_y_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_y_item->set_tooltip_text(_("Angle of PLs in Y direction")); + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + _angle_y_item->set_custom_numeric_menu_data(values); + _angle_y_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _angle_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_y_adj, Proj::Y)); + add(*_angle_y_item); + } + + if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::Y)) { + _angle_y_item->set_sensitive(true); + } else { + _angle_y_item->set_sensitive(false); + } + + /* VP Y state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_y_state_item = add_toggle_button(_("State of VP in Y direction"), + _("Toggle VP in Y direction between 'finite' and 'infinite' (=parallel)")); + _vp_y_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_y_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Y)); + _angle_y_item->set_sensitive( !prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) ); + _vp_y_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_y_state", true) ); + } + + /* Angle Z */ + { + auto angle_z_val = prefs->getDouble("/tools/shapes/3dbox/box3d_angle_z", 30); + _angle_z_adj = Gtk::Adjustment::create(angle_z_val, -360.0, 360.0, 1.0, 10.0); + _angle_z_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("box3d-angle-z", _("Angle Z:"), _angle_z_adj)); + // TRANSLATORS: PL is short for 'perspective line' + _angle_z_item->set_tooltip_text(_("Angle of PLs in Z direction")); + std::vector<double> values = {-90, -60, -30, 0, 30, 60, 90}; + _angle_z_item->set_custom_numeric_menu_data(values); + _angle_z_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _angle_z_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::angle_value_changed), + _angle_z_adj, Proj::Z)); + add(*_angle_z_item); + } + + if (!persp_impl || !persp3d_VP_is_finite(persp_impl, Proj::Z)) { + _angle_z_item->set_sensitive(true); + } else { + _angle_z_item->set_sensitive(false); + } + + /* VP Z state */ + { + // TRANSLATORS: VP is short for 'vanishing point' + _vp_z_state_item = add_toggle_button(_("State of VP in Z direction"), + _("Toggle VP in Z direction between 'finite' and 'infinite' (=parallel)")); + _vp_z_state_item->set_icon_name(INKSCAPE_ICON("perspective-parallel")); + _vp_z_state_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &Box3DToolbar::vp_state_changed), Proj::Z)); + _angle_z_item->set_sensitive(!prefs->getBool("/tools/shapes/3dbox/vp_z_state", true)); + _vp_z_state_item->set_active( prefs->getBool("/tools/shapes/3dbox/vp_z_state", true) ); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &Box3DToolbar::check_ec)); + + show_all(); +} + +GtkWidget * +Box3DToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new Box3DToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +Box3DToolbar::angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Proj::Axis axis) +{ + SPDocument *document = _desktop->getDocument(); + + // quit if run by the attr_changed or selection changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + std::list<Persp3D *> sel_persps = _desktop->getSelection()->perspList(); + if (sel_persps.empty()) { + // this can happen when the document is created; we silently ignore it + return; + } + Persp3D *persp = sel_persps.front(); + + persp->perspective_impl->tmat.set_infinite_direction (axis, + adj->get_value()); + persp->updateRepr(); + + // TODO: use the correct axis here, too + DocumentUndo::maybeDone(document, "perspangle", SP_VERB_CONTEXT_3DBOX, _("3D Box: Change perspective (angle of infinite axis)")); + + _freeze = false; +} + +void +Box3DToolbar::vp_state_changed(Proj::Axis axis) +{ + // TODO: Take all selected perspectives into account + auto sel_persps = SP_ACTIVE_DESKTOP->getSelection()->perspList(); + if (sel_persps.empty()) { + // this can happen when the document is created; we silently ignore it + return; + } + Persp3D *persp = sel_persps.front(); + + Gtk::ToggleToolButton *btn = nullptr; + + switch(axis) { + case Proj::X: + btn = _vp_x_state_item; + break; + case Proj::Y: + btn = _vp_y_state_item; + break; + case Proj::Z: + btn = _vp_z_state_item; + break; + default: + return; + } + + bool set_infinite = btn->get_active(); + persp3d_set_VP_state (persp, axis, set_infinite ? Proj::VP_INFINITE : Proj::VP_FINITE); +} + +void +Box3DToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_BOX3D_CONTEXT(ec)) { + _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &Box3DToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) + _changed.disconnect(); + } +} + +Box3DToolbar::~Box3DToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } +} + +/** + * \param selection Should not be NULL. + */ +// FIXME: This should rather be put into persp3d-reference.cpp or something similar so that it reacts upon each +// Change of the perspective, and not of the current selection (but how to refer to the toolbar then?) +void +Box3DToolbar::selection_changed(Inkscape::Selection *selection) +{ + // Here the following should be done: If all selected boxes have finite VPs in a certain direction, + // disable the angle entry fields for this direction (otherwise entering a value in them should only + // update the perspectives with infinite VPs and leave the other ones untouched). + + Inkscape::XML::Node *persp_repr = nullptr; + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + SPItem *item = selection->singleItem(); + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + // FIXME: Also deal with multiple selected boxes + Persp3D *persp = box3d_get_perspective(box); + persp_repr = persp->getRepr(); + if (persp_repr) { + _repr = persp_repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&box3d_persp_tb_repr_events, this); + _repr->synthesizeEvents(&box3d_persp_tb_repr_events, this); + + SP_ACTIVE_DOCUMENT->setCurrentPersp3D(persp3d_get_from_repr(_repr)); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/shapes/3dbox/persp", _repr->attribute("id")); + + _freeze = true; + resync_toolbar(_repr); + _freeze = false; + } + } +} + +void +Box3DToolbar::resync_toolbar(Inkscape::XML::Node *persp_repr) +{ + if (!persp_repr) { + g_print ("No perspective given to box3d_resync_toolbar().\n"); + return; + } + + Persp3D *persp = persp3d_get_from_repr(persp_repr); + if (!persp) { + // Hmm, is it an error if this happens? + return; + } + set_button_and_adjustment(persp, Proj::X, + _angle_x_adj, + _angle_x_item, + _vp_x_state_item); + set_button_and_adjustment(persp, Proj::Y, + _angle_y_adj, + _angle_y_item, + _vp_y_state_item); + set_button_and_adjustment(persp, Proj::Z, + _angle_z_adj, + _angle_z_item, + _vp_z_state_item); +} + +void +Box3DToolbar::set_button_and_adjustment(Persp3D *persp, + Proj::Axis axis, + Glib::RefPtr<Gtk::Adjustment>& adj, + UI::Widget::SpinButtonToolItem *spin_btn, + Gtk::ToggleToolButton *toggle_btn) +{ + // TODO: Take all selected perspectives into account but don't touch the state button if not all of them + // have the same state (otherwise a call to box3d_vp_z_state_changed() is triggered and the states + // are reset). + bool is_infinite = !persp3d_VP_is_finite(persp->perspective_impl, axis); + + if (is_infinite) { + toggle_btn->set_active(true); + spin_btn->set_sensitive(true); + + double angle = persp3d_get_infinite_angle(persp, axis); + if (angle != Geom::infinity()) { // FIXME: We should catch this error earlier (don't show the spinbutton at all) + adj->set_value(normalize_angle(angle)); + } + } else { + toggle_btn->set_active(false); + spin_btn->set_sensitive(false); + } +} + +void +Box3DToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const * /*name*/, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<Box3DToolbar*>(data); + + // quit if run by the attr_changed or selection changed listener + if (toolbar->_freeze) { + return; + } + + // set freeze so that it can be caught in box3d_angle_z_value_changed() (to avoid calling + // SPDocumentUndo::maybeDone() when the document is undo insensitive) + toolbar->_freeze = true; + + // TODO: Only update the appropriate part of the toolbar +// if (!strcmp(name, "inkscape:vp_z")) { + toolbar->resync_toolbar(repr); +// } + + Persp3D *persp = persp3d_get_from_repr(repr); + persp3d_update_box_reprs(persp); + + toolbar->_freeze = false; +} + +/** + * \brief normalize angle so that it lies in the interval [0,360] + * + * TODO: Isn't there something in 2Geom or cmath that does this? + */ +double +Box3DToolbar::normalize_angle(double a) { + double angle = a + ((int) (a/360.0))*360; + if (angle < 0) { + angle += 360.0; + } + return angle; +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/box3d-toolbar.h b/src/ui/toolbar/box3d-toolbar.h new file mode 100644 index 0000000..dc74664 --- /dev/null +++ b/src/ui/toolbar/box3d-toolbar.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_BOX3D_TOOLBAR_H +#define SEEN_BOX3D_TOOLBAR_H + +/** + * @file + * 3d box aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +#include "axis-manip.h" + +class Persp3D; +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class SpinButtonToolItem; +} + +namespace Tools { +class ToolBase; +} + +namespace Toolbar { +class Box3DToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_angle_x_item; + UI::Widget::SpinButtonToolItem *_angle_y_item; + UI::Widget::SpinButtonToolItem *_angle_z_item; + + Glib::RefPtr<Gtk::Adjustment> _angle_x_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_y_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_z_adj; + + Gtk::ToggleToolButton *_vp_x_state_item; + Gtk::ToggleToolButton *_vp_y_state_item; + Gtk::ToggleToolButton *_vp_z_state_item; + + XML::Node *_repr; + bool _freeze; + + void angle_value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Proj::Axis axis); + void vp_state_changed(Proj::Axis axis); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + void resync_toolbar(Inkscape::XML::Node *persp_repr); + void set_button_and_adjustment(Persp3D *persp, + Proj::Axis axis, + Glib::RefPtr<Gtk::Adjustment>& adj, + UI::Widget::SpinButtonToolItem *spin_btn, + Gtk::ToggleToolButton *toggle_btn); + double normalize_angle(double a); + + sigc::connection _changed; + +protected: + Box3DToolbar(SPDesktop *desktop); + ~Box3DToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); + +}; +} +} +} +#endif /* !SEEN_BOX3D_TOOLBAR_H */ diff --git a/src/ui/toolbar/calligraphy-toolbar.cpp b/src/ui/toolbar/calligraphy-toolbar.cpp new file mode 100644 index 0000000..34e7a89 --- /dev/null +++ b/src/ui/toolbar/calligraphy-toolbar.cpp @@ -0,0 +1,599 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Calligraphy aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "calligraphy-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/comboboxtext.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/uxmanager.h" +#include "ui/dialog/calligraphic-profile-rename.h" +#include "ui/widget/spin-button-tool-item.h" + +using Inkscape::DocumentUndo; + +std::vector<Glib::ustring> get_presets_list() { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + std::vector<Glib::ustring> presets = prefs->getAllDirs("/tools/calligraphic/preset"); + + return presets; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +CalligraphyToolbar::CalligraphyToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _presets_blocked(false) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /*calligraphic profile */ + { + _profile_selector_combo = Gtk::manage(new Gtk::ComboBoxText()); + _profile_selector_combo->set_tooltip_text(_("Choose a preset")); + + build_presets_list(); + + auto profile_selector_ti = Gtk::manage(new Gtk::ToolItem()); + profile_selector_ti->add(*_profile_selector_combo); + add(*profile_selector_ti); + + _profile_selector_combo->signal_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::change_profile)); + } + + /*calligraphic profile editor */ + { + auto profile_edit_item = Gtk::manage(new Gtk::ToolButton(_("Add/Edit Profile"))); + profile_edit_item->set_tooltip_text(_("Add or edit calligraphic profile")); + profile_edit_item->set_icon_name(INKSCAPE_ICON("document-properties")); + profile_edit_item->signal_clicked().connect(sigc::mem_fun(*this, &CalligraphyToolbar::edit_profile)); + add(*profile_edit_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Width */ + std::vector<Glib::ustring> labels = {_("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/calligraphic/width", 15); + _width_adj = Gtk::Adjustment::create(width_val, 1, 100, 1.0, 10.0); + auto width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-width", _("Width:"), _width_adj, 1, 0)); + width_item->set_tooltip_text(_("The width of the calligraphic pen (relative to the visible canvas area)")); + width_item->set_custom_numeric_menu_data(values, labels); + width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::width_value_changed)); + _widget_map["width"] = G_OBJECT(_width_adj->gobj()); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*width_item); + width_item->set_sensitive(true); + } + + /* Use Pressure button */ + { + _usepressure = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the width of the pen")); + _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _widget_map["usepressure"] = G_OBJECT(_usepressure->gobj()); + _usepressure_pusher.reset(new SimplePrefPusher(_usepressure, "/tools/calligraphic/usepressure")); + _usepressure->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled), + _usepressure, + "/tools/calligraphic/usepressure")); + } + + /* Trace Background button */ + { + _tracebackground = add_toggle_button(_("Trace Background"), + _("Trace the lightness of the background by the width of the pen (white - minimum width, black - maximum width)")); + _tracebackground->set_icon_name(INKSCAPE_ICON("draw-trace-background")); + _widget_map["tracebackground"] = G_OBJECT(_tracebackground->gobj()); + _tracebackground_pusher.reset(new SimplePrefPusher(_tracebackground, "/tools/calligraphic/tracebackground")); + _tracebackground->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &CalligraphyToolbar::on_pref_toggled), + _tracebackground, + "/tools/calligraphic/tracebackground")); + } + + { + /* Thinning */ + std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")}; + std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100}; + auto thinning_val = prefs->getDouble("/tools/calligraphic/thinning", 10); + _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0); + auto thinning_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-thinning", _("Thinning:"), _thinning_adj, 1, 0)); + thinning_item->set_tooltip_text(("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)")); + thinning_item->set_custom_numeric_menu_data(values, labels); + thinning_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::velthin_value_changed)); + _widget_map["thinning"] = G_OBJECT(_thinning_adj->gobj()); + add(*thinning_item); + thinning_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Angle */ + std::vector<Glib::ustring> labels = {_("(left edge up)"), "", "", _("(horizontal)"), _("(default)"), "", _("(right edge up)")}; + std::vector<double> values = { -90, -60, -30, 0, 30, 60, 90}; + auto angle_val = prefs->getDouble("/tools/calligraphic/angle", 30); + _angle_adj = Gtk::Adjustment::create(angle_val, -90.0, 90.0, 1.0, 10.0); + _angle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-angle", _("Angle:"), _angle_adj, 1, 0)); + _angle_item->set_tooltip_text(_("The angle of the pen's nib (in degrees; 0 = horizontal; has no effect if fixation = 0)")); + _angle_item->set_custom_numeric_menu_data(values, labels); + _angle_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _angle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::angle_value_changed)); + _widget_map["angle"] = G_OBJECT(_angle_adj->gobj()); + add(*_angle_item); + _angle_item->set_sensitive(true); + } + + /* Use Tilt button */ + { + _usetilt = add_toggle_button(_("Tilt"), + _("Use the tilt of the input device to alter the angle of the pen's nib")); + _usetilt->set_icon_name(INKSCAPE_ICON("draw-use-tilt")); + _widget_map["usetilt"] = G_OBJECT(_usetilt->gobj()); + _usetilt_pusher.reset(new SimplePrefPusher(_usetilt, "/tools/calligraphic/usetilt")); + _usetilt->signal_toggled().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tilt_state_changed)); + _angle_item->set_sensitive(!prefs->getBool("/tools/calligraphic/usetilt", true)); + _usetilt->set_active(prefs->getBool("/tools/calligraphic/usetilt", true)); + } + + { + /* Fixation */ + std::vector<Glib::ustring> labels = {_("(perpendicular to stroke, \"brush\")"), "", "", "", _("(almost fixed, default)"), _("(fixed by Angle, \"pen\")")}; + std::vector<double> values = { 0, 20, 40, 60, 90, 100}; + auto flatness_val = prefs->getDouble("/tools/calligraphic/flatness", 90); + _fixation_adj = Gtk::Adjustment::create(flatness_val, 0.0, 100, 1.0, 10.0); + auto flatness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-fixation", _("Fixation:"), _fixation_adj, 1, 0)); + flatness_item->set_tooltip_text(_("Angle behavior (0 = nib always perpendicular to stroke direction, 100 = fixed angle)")); + flatness_item->set_custom_numeric_menu_data(values, labels); + flatness_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _fixation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::flatness_value_changed)); + _widget_map["flatness"] = G_OBJECT(_fixation_adj->gobj()); + add(*flatness_item); + flatness_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Cap Rounding */ + std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")}; + std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0}; + auto cap_rounding_val = prefs->getDouble("/tools/calligraphic/cap_rounding", 0.0); + _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1); + auto cap_rounding_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2)); + + // TRANSLATORS: "cap" means "end" (both start and finish) here + cap_rounding_item->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)")); + cap_rounding_item->set_custom_numeric_menu_data(values, labels); + cap_rounding_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::cap_rounding_value_changed)); + _widget_map["cap_rounding"] = G_OBJECT(_cap_rounding_adj->gobj()); + add(*cap_rounding_item); + cap_rounding_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Tremor */ + std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")}; + std::vector<double> values = { 0, 10, 20, 40, 60, 100}; + auto tremor_val = prefs->getDouble("/tools/calligraphic/tremor", 0.0); + _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0); + auto tremor_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-tremor", _("Tremor:"), _tremor_adj, 1, 0)); + tremor_item->set_tooltip_text(_("Increase to make strokes rugged and trembling")); + tremor_item->set_custom_numeric_menu_data(values, labels); + tremor_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::tremor_value_changed)); + _widget_map["tremor"] = G_OBJECT(_tremor_adj->gobj()); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*tremor_item); + tremor_item->set_sensitive(true); + } + + { + /* Wiggle */ + std::vector<Glib::ustring> labels = {_("(no wiggle)"), _("(slight deviation)"), "", "", _("(wild waves and curls)")}; + std::vector<double> values = { 0, 20, 40, 60, 100}; + auto wiggle_val = prefs->getDouble("/tools/calligraphic/wiggle", 0.0); + _wiggle_adj = Gtk::Adjustment::create(wiggle_val, 0.0, 100, 1, 10.0); + auto wiggle_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-wiggle", _("Wiggle:"), _wiggle_adj, 1, 0)); + wiggle_item->set_tooltip_text(_("Increase to make the pen waver and wiggle")); + wiggle_item->set_custom_numeric_menu_data(values, labels); + wiggle_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _wiggle_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::wiggle_value_changed)); + _widget_map["wiggle"] = G_OBJECT(_wiggle_adj->gobj()); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*wiggle_item); + wiggle_item->set_sensitive(true); + } + + { + /* Mass */ + std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")}; + std::vector<double> values = { 0.0, 2, 10, 20, 50, 100}; + auto mass_val = prefs->getDouble("/tools/calligraphic/mass", 2.0); + _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0); + auto mass_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("calligraphy-mass", _("Mass:"), _mass_adj, 1, 0)); + mass_item->set_tooltip_text(_("Increase to make the pen drag behind, as if slowed by inertia")); + mass_item->set_custom_numeric_menu_data(values, labels); + mass_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &CalligraphyToolbar::mass_value_changed)); + _widget_map["mass"] = G_OBJECT(_mass_adj->gobj()); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*mass_item); + mass_item->set_sensitive(true); + } + + show_all(); +} + +GtkWidget * +CalligraphyToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new CalligraphyToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +CalligraphyToolbar::width_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/width", _width_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::velthin_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/calligraphic/thinning", _thinning_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::angle_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/angle", _angle_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::flatness_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/flatness", _fixation_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::cap_rounding_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/cap_rounding", _cap_rounding_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::tremor_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/tremor", _tremor_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::wiggle_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/wiggle", _wiggle_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::mass_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/calligraphic/mass", _mass_adj->get_value() ); + update_presets_list(); +} + +void +CalligraphyToolbar::on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(path, item->get_active()); + update_presets_list(); +} + +void +CalligraphyToolbar::update_presets_list() +{ + if (_presets_blocked) { + return; + } + + auto prefs = Inkscape::Preferences::get(); + auto presets = get_presets_list(); + + int index = 1; // 0 is for no preset. + for (auto i = presets.begin(); i != presets.end(); ++i, ++index) { + bool match = true; + + auto preset = prefs->getAllEntries(*i); + for (auto & j : preset) { + Glib::ustring entry_name = j.getEntryName(); + if (entry_name == "id" || entry_name == "name") { + continue; + } + + void *widget = _widget_map[entry_name.data()]; + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + double v = j.getDouble(); + GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget); + //std::cout << "compared adj " << attr_name << gtk_adjustment_get_value(adj) << " to " << v << "\n"; + if (fabs(gtk_adjustment_get_value(adj) - v) > 1e-6) { + match = false; + break; + } + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + bool v = j.getBool(); + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + //std::cout << "compared toggle " << attr_name << gtk_toggle_action_get_active(toggle) << " to " << v << "\n"; + if ( static_cast<bool>(gtk_toggle_tool_button_get_active(toggle)) != v ) { + match = false; + break; + } + } + } + } + + if (match) { + // newly added item is at the same index as the + // save command, so we need to change twice for it to take effect + _profile_selector_combo->set_active(0); + _profile_selector_combo->set_active(index); + return; + } + } + + // no match found + _profile_selector_combo->set_active(0); +} + +void +CalligraphyToolbar::tilt_state_changed() +{ + _angle_item->set_sensitive(!_usetilt->get_active()); + on_pref_toggled(_usetilt, "/tools/calligraphic/usetilt"); +} + +void +CalligraphyToolbar::build_presets_list() +{ + _presets_blocked = true; + + _profile_selector_combo->remove_all(); + _profile_selector_combo->append(_("No preset")); + + // iterate over all presets to populate the list + auto prefs = Inkscape::Preferences::get(); + auto presets = get_presets_list(); + + for (auto & preset : presets) { + GtkTreeIter iter; + Glib::ustring preset_name = prefs->getString(preset + "/name"); + + if (!preset_name.empty()) { + _profile_selector_combo->append(_(preset_name.data())); + } + } + + _presets_blocked = false; + + update_presets_list(); +} + +void +CalligraphyToolbar::change_profile() +{ + auto mode = _profile_selector_combo->get_active_row_number(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (_presets_blocked) { + return; + } + + // mode is one-based so we subtract 1 + std::vector<Glib::ustring> presets = get_presets_list(); + + Glib::ustring preset_path = ""; + if (mode - 1 < presets.size()) { + preset_path = presets.at(mode - 1); + } + + if (!preset_path.empty()) { + _presets_blocked = true; //temporarily block the selector so no one will updadte it while we're reading it + + std::vector<Inkscape::Preferences::Entry> preset = prefs->getAllEntries(preset_path); + + // Shouldn't this be std::map? + for (auto & i : preset) { + Glib::ustring entry_name = i.getEntryName(); + if (entry_name == "id" || entry_name == "name") { + continue; + } + void *widget = _widget_map[entry_name.data()]; + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + GtkAdjustment* adj = static_cast<GtkAdjustment *>(widget); + gtk_adjustment_set_value(adj, i.getDouble()); + //std::cout << "set adj " << attr_name << " to " << v << "\n"; + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + gtk_toggle_tool_button_set_active(toggle, i.getBool()); + //std::cout << "set toggle " << attr_name << " to " << v << "\n"; + } else { + g_warning("Unknown widget type for preset: %s\n", entry_name.data()); + } + } else { + g_warning("Bad key found in a preset record: %s\n", entry_name.data()); + } + } + _presets_blocked = false; + } +} + +void +CalligraphyToolbar::edit_profile() +{ + save_profile(nullptr); +} + +void +CalligraphyToolbar::save_profile(GtkWidget * /*widget*/) +{ + using Inkscape::UI::Dialog::CalligraphicProfileRename; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (! _desktop) { + return; + } + + if (_presets_blocked) { + return; + } + + Glib::ustring current_profile_name = _profile_selector_combo->get_active_text(); + + if (current_profile_name == _("No preset")) { + current_profile_name = ""; + } + + CalligraphicProfileRename::show(_desktop, current_profile_name); + if ( !CalligraphicProfileRename::applied()) { + // dialog cancelled + update_presets_list(); + return; + } + Glib::ustring new_profile_name = CalligraphicProfileRename::getProfileName(); + + if (new_profile_name.empty()) { + // empty name entered + update_presets_list (); + return; + } + + _presets_blocked = true; + + // If there's a preset with the given name, find it and set save_path appropriately + auto presets = get_presets_list(); + int total_presets = presets.size(); + int new_index = -1; + Glib::ustring save_path; // profile pref path without a trailing slash + + int temp_index = 0; + for (std::vector<Glib::ustring>::iterator i = presets.begin(); i != presets.end(); ++i, ++temp_index) { + Glib::ustring name = prefs->getString(*i + "/name"); + if (!name.empty() && (new_profile_name == name || current_profile_name == name)) { + new_index = temp_index; + save_path = *i; + break; + } + } + + if ( CalligraphicProfileRename::deleted() && new_index != -1) { + prefs->remove(save_path); + _presets_blocked = false; + build_presets_list(); + return; + } + + if (new_index == -1) { + // no preset with this name, create + new_index = total_presets + 1; + gchar *profile_id = g_strdup_printf("/dcc%d", new_index); + save_path = Glib::ustring("/tools/calligraphic/preset") + profile_id; + g_free(profile_id); + } + + for (auto map_item : _widget_map) { + auto widget_name = map_item.first; + auto widget = map_item.second; + + if (widget) { + if (GTK_IS_ADJUSTMENT(widget)) { + GtkAdjustment* adj = GTK_ADJUSTMENT(widget); + prefs->setDouble(save_path + "/" + widget_name, gtk_adjustment_get_value(adj)); + //std::cout << "wrote adj " << widget_name << ": " << v << "\n"; + } else if (GTK_IS_TOGGLE_TOOL_BUTTON(widget)) { + auto toggle = GTK_TOGGLE_TOOL_BUTTON(widget); + prefs->setBool(save_path + "/" + widget_name, gtk_toggle_tool_button_get_active(toggle)); + //std::cout << "wrote tog " << widget_name << ": " << v << "\n"; + } else { + g_warning("Unknown widget type for preset: %s\n", widget_name.c_str()); + } + } else { + g_warning("Bad key when writing preset: %s\n", widget_name.c_str()); + } + } + prefs->setString(save_path + "/name", new_profile_name); + + _presets_blocked = true; + build_presets_list(); +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/calligraphy-toolbar.h b/src/ui/toolbar/calligraphy-toolbar.h new file mode 100644 index 0000000..d216888 --- /dev/null +++ b/src/ui/toolbar/calligraphy-toolbar.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CALLIGRAPHY_TOOLBAR_H +#define SEEN_CALLIGRAPHY_TOOLBAR_H + +/** + * @file + * Calligraphy aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ComboBoxText; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Widget { +class SpinButtonToolItem; +} + +namespace Toolbar { + +class CalligraphyToolbar : public Toolbar { +private: + bool _presets_blocked; + + UI::Widget::SpinButtonToolItem *_angle_item; + Gtk::ComboBoxText *_profile_selector_combo; + + std::map<Glib::ustring, GObject *> _widget_map; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mass_adj; + Glib::RefPtr<Gtk::Adjustment> _wiggle_adj; + Glib::RefPtr<Gtk::Adjustment> _angle_adj; + Glib::RefPtr<Gtk::Adjustment> _thinning_adj; + Glib::RefPtr<Gtk::Adjustment> _tremor_adj; + Glib::RefPtr<Gtk::Adjustment> _fixation_adj; + Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj; + Gtk::ToggleToolButton *_usepressure; + Gtk::ToggleToolButton *_tracebackground; + Gtk::ToggleToolButton *_usetilt; + + std::unique_ptr<SimplePrefPusher> _tracebackground_pusher; + std::unique_ptr<SimplePrefPusher> _usepressure_pusher; + std::unique_ptr<SimplePrefPusher> _usetilt_pusher; + + void width_value_changed(); + void velthin_value_changed(); + void angle_value_changed(); + void flatness_value_changed(); + void cap_rounding_value_changed(); + void tremor_value_changed(); + void wiggle_value_changed(); + void mass_value_changed(); + void build_presets_list(); + void change_profile(); + void save_profile(GtkWidget *widget); + void edit_profile(); + void update_presets_list(); + void tilt_state_changed(); + void on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path); + +protected: + CalligraphyToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_CALLIGRAPHY_TOOLBAR_H */ diff --git a/src/ui/toolbar/connector-toolbar.cpp b/src/ui/toolbar/connector-toolbar.cpp new file mode 100644 index 0000000..0220a0a --- /dev/null +++ b/src/ui/toolbar/connector-toolbar.cpp @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Connector aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "connector-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "conn-avoid-ref.h" + +#include "desktop.h" +#include "document-undo.h" +#include "enums.h" +#include "graphlayout.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-namedview.h" +#include "object/sp-path.h" + +#include "ui/icon-names.h" +#include "ui/tools/connector-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "widgets/spinbutton-events.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector connector_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::ConnectorToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +ConnectorToolbar::ConnectorToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _freeze(false), + _repr(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + { + auto avoid_item = Gtk::manage(new Gtk::ToolButton(_("Avoid"))); + avoid_item->set_tooltip_text(_("Make connectors avoid selected objects")); + avoid_item->set_icon_name(INKSCAPE_ICON("connector-avoid")); + avoid_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_avoid)); + add(*avoid_item); + } + + { + auto ignore_item = Gtk::manage(new Gtk::ToolButton(_("Ignore"))); + ignore_item->set_tooltip_text(_("Make connectors ignore selected objects")); + ignore_item->set_icon_name(INKSCAPE_ICON("connector-ignore")); + ignore_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::path_set_ignore)); + add(*ignore_item); + } + + // Orthogonal connectors toggle button + { + _orthogonal = add_toggle_button(_("Orthogonal"), + _("Make connector orthogonal or polyline")); + _orthogonal->set_icon_name(INKSCAPE_ICON("connector-orthogonal")); + + bool tbuttonstate = prefs->getBool("/tools/connector/orthogonal"); + _orthogonal->set_active(( tbuttonstate ? TRUE : FALSE )); + _orthogonal->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::orthogonal_toggled)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // Curvature spinbox + auto curvature_val = prefs->getDouble("/tools/connector/curvature", defaultConnCurvature); + _curvature_adj = Gtk::Adjustment::create(curvature_val, 0, 100, 1.0, 10.0); + auto curvature_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-curvature", _("Curvature:"), _curvature_adj, 1, 0)); + curvature_item->set_tooltip_text(_("The amount of connectors curvature")); + curvature_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _curvature_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::curvature_changed)); + add(*curvature_item); + + // Spacing spinbox + auto spacing_val = prefs->getDouble("/tools/connector/spacing", defaultConnSpacing); + _spacing_adj = Gtk::Adjustment::create(spacing_val, 0, 100, 1.0, 10.0); + auto spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-spacing", _("Spacing:"), _spacing_adj, 1, 0)); + spacing_item->set_tooltip_text(_("The amount of space left around objects by auto-routing connectors")); + spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::spacing_changed)); + add(*spacing_item); + + // Graph (connector network) layout + { + auto graph_item = Gtk::manage(new Gtk::ToolButton(_("Graph"))); + graph_item->set_tooltip_text(_("Nicely arrange selected connector network")); + graph_item->set_icon_name(INKSCAPE_ICON("distribute-graph")); + graph_item->signal_clicked().connect(sigc::mem_fun(*this, &ConnectorToolbar::graph_layout)); + add(*graph_item); + } + + // Default connector length spinbox + auto length_val = prefs->getDouble("/tools/connector/length", 100); + _length_adj = Gtk::Adjustment::create(length_val, 10, 1000, 10.0, 100.0); + auto length_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:connector-length", _("Length:"), _length_adj, 1, 0)); + length_item->set_tooltip_text(_("Ideal length for connectors when layout is applied")); + length_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _length_adj->signal_value_changed().connect(sigc::mem_fun(*this, &ConnectorToolbar::length_changed)); + add(*length_item); + + // Directed edges toggle button + { + _directed_item = add_toggle_button(_("Downwards"), + _("Make connectors with end-markers (arrows) point downwards")); + _directed_item->set_icon_name(INKSCAPE_ICON("distribute-graph-directed")); + + bool tbuttonstate = prefs->getBool("/tools/connector/directedlayout"); + _directed_item->set_active(tbuttonstate ? TRUE : FALSE); + + _directed_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::directed_graph_layout_toggled)); + desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &ConnectorToolbar::selection_changed)); + } + + // Avoid overlaps toggle button + { + _overlap_item = add_toggle_button(_("Remove overlaps"), + _("Do not allow overlapping shapes")); + _overlap_item->set_icon_name(INKSCAPE_ICON("distribute-remove-overlaps")); + + bool tbuttonstate = prefs->getBool("/tools/connector/avoidoverlaplayout"); + _overlap_item->set_active(tbuttonstate ? TRUE : FALSE); + + _overlap_item->signal_toggled().connect(sigc::mem_fun(*this, &ConnectorToolbar::nooverlaps_graph_layout_toggled)); + } + + // Code to watch for changes to the connector-spacing attribute in + // the XML. + Inkscape::XML::Node *repr = desktop->namedview->getRepr(); + g_assert(repr != nullptr); + + if(_repr) { + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&connector_tb_repr_events, this); + _repr->synthesizeEvents(&connector_tb_repr_events, this); + } + + show_all(); +} + +GtkWidget * +ConnectorToolbar::create( SPDesktop *desktop) +{ + auto toolbar = new ConnectorToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} // end of ConnectorToolbar::prep() + +void +ConnectorToolbar::path_set_avoid() +{ + Inkscape::UI::Tools::cc_selection_set_avoid(true); +} + +void +ConnectorToolbar::path_set_ignore() +{ + Inkscape::UI::Tools::cc_selection_set_avoid(false); +} + +void +ConnectorToolbar::orthogonal_toggled() +{ + auto doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + // in turn, prevent callbacks from responding + _freeze = true; + + bool is_orthog = _orthogonal->get_active(); + gchar orthog_str[] = "orthogonal"; + gchar polyline_str[] = "polyline"; + gchar *value = is_orthog ? orthog_str : polyline_str ; + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + item->setAttribute( "inkscape:connector-type", + value, nullptr); + item->getAvoidRef().handleSettingChange(); + modmade = true; + } + } + + if (!modmade) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/orthogonal", is_orthog); + } else { + + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, + is_orthog ? _("Set connector type: orthogonal"): _("Set connector type: polyline")); + } + + _freeze = false; +} + +void +ConnectorToolbar::curvature_changed() +{ + SPDocument *doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + // in turn, prevent callbacks from responding + _freeze = true; + + auto newValue = _curvature_adj->get_value(); + gchar value[G_ASCII_DTOSTR_BUF_SIZE]; + g_ascii_dtostr(value, G_ASCII_DTOSTR_BUF_SIZE, newValue); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + item->setAttribute( "inkscape:connector-curvature", + value, nullptr); + item->getAvoidRef().handleSettingChange(); + modmade = true; + } + } + + if (!modmade) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/connector/curvature"), newValue); + } + else { + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, + _("Change connector curvature")); + } + + _freeze = false; +} + +void +ConnectorToolbar::spacing_changed() +{ + SPDocument *doc = _desktop->getDocument(); + + if (!DocumentUndo::getUndoSensitive(doc)) { + return; + } + + Inkscape::XML::Node *repr = _desktop->namedview->getRepr(); + + if ( !repr->attribute("inkscape:connector-spacing") && + ( _spacing_adj->get_value() == defaultConnSpacing )) { + // Don't need to update the repr if the attribute doesn't + // exist and it is being set to the default value -- as will + // happen at startup. + return; + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + sp_repr_set_css_double(repr, "inkscape:connector-spacing", _spacing_adj->get_value()); + _desktop->namedview->updateRepr(); + bool modmade = false; + + std::vector<SPItem *> items; + items = get_avoided_items(items, _desktop->currentRoot(), _desktop); + for (auto item : items) { + Geom::Affine m = Geom::identity(); + avoid_item_move(&m, item); + modmade = true; + } + + if(modmade) { + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, + _("Change connector spacing")); + } + _freeze = false; +} + +void +ConnectorToolbar::graph_layout() +{ + if (!SP_ACTIVE_DESKTOP) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // hack for clones, see comment in align-and-distribute.cpp + int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED); + + auto tmp = SP_ACTIVE_DESKTOP->getSelection()->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + graphlayout(vec); + + prefs->setInt("/options/clonecompensation/value", saved_compensation); + + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_DIALOG_ALIGN_DISTRIBUTE, _("Arrange connector network")); +} + +void +ConnectorToolbar::length_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/connector/length", _length_adj->get_value()); +} + +void +ConnectorToolbar::directed_graph_layout_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/directedlayout", _directed_item->get_active()); +} + +void +ConnectorToolbar::selection_changed(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + if (SP_IS_PATH(item)) + { + gdouble curvature = SP_PATH(item)->connEndPair.getCurvature(); + bool is_orthog = SP_PATH(item)->connEndPair.isOrthogonal(); + _orthogonal->set_active(is_orthog); + _curvature_adj->set_value(curvature); + } + +} + +void +ConnectorToolbar::nooverlaps_graph_layout_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/connector/avoidoverlaplayout", + _overlap_item->get_active()); +} + +void +ConnectorToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<ConnectorToolbar *>(data); + + if ( !toolbar->_freeze + && (strcmp(name, "inkscape:connector-spacing") == 0) ) { + gdouble spacing = defaultConnSpacing; + sp_repr_get_double(repr, "inkscape:connector-spacing", &spacing); + + toolbar->_spacing_adj->set_value(spacing); + + if(toolbar->_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(toolbar->_desktop->canvas)); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/connector-toolbar.h b/src/ui/toolbar/connector-toolbar.h new file mode 100644 index 0000000..66df79e --- /dev/null +++ b/src/ui/toolbar/connector-toolbar.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONNECTOR_TOOLBAR_H +#define SEEN_CONNECTOR_TOOLBAR_H + +/** + * @file + * Connector aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Toolbar { +class ConnectorToolbar : public Toolbar { +private: + Gtk::ToggleToolButton *_orthogonal; + Gtk::ToggleToolButton *_directed_item; + Gtk::ToggleToolButton *_overlap_item; + + Glib::RefPtr<Gtk::Adjustment> _curvature_adj; + Glib::RefPtr<Gtk::Adjustment> _spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _length_adj; + + bool _freeze; + + Inkscape::XML::Node *_repr; + + void path_set_avoid(); + void path_set_ignore(); + void orthogonal_toggled(); + void graph_layout(); + void directed_graph_layout_toggled(); + void nooverlaps_graph_layout_toggled(); + void curvature_changed(); + void spacing_changed(); + void length_changed(); + void selection_changed(Inkscape::Selection *selection); + +protected: + ConnectorToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data); +}; + +} +} +} + +#endif /* !SEEN_CONNECTOR_TOOLBAR_H */ diff --git a/src/ui/toolbar/dropper-toolbar.cpp b/src/ui/toolbar/dropper-toolbar.cpp new file mode 100644 index 0000000..3d6c4f9 --- /dev/null +++ b/src/ui/toolbar/dropper-toolbar.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Dropper aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "dropper-toolbar.h" +#include "document-undo.h" +#include "preferences.h" +#include "widgets/spinbutton-events.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +void DropperToolbar::on_pick_alpha_button_toggled() +{ + auto active = _pick_alpha_button->get_active(); + + auto prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/dropper/pick", active ); + + _set_alpha_button->set_sensitive(active); + + spinbutton_defocus(GTK_WIDGET(gobj())); +} + +void DropperToolbar::on_set_alpha_button_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool( "/tools/dropper/setalpha", _set_alpha_button->get_active( ) ); + spinbutton_defocus(GTK_WIDGET(gobj())); +} + +/* + * TODO: Would like to add swatch of current color. + * TODO: Add queue of last 5 or so colors selected with new swatches so that + * can drag and drop places. Will provide a nice mixing palette. + */ +DropperToolbar::DropperToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + // Add widgets to toolbar + add_label(_("Opacity:")); + _pick_alpha_button = add_toggle_button(_("Pick"), + _("Pick both the color and the alpha (transparency) under cursor; " + "otherwise, pick only the visible color premultiplied by alpha")); + _set_alpha_button = add_toggle_button(_("Assign"), + _("If alpha was picked, assign it to selection " + "as fill or stroke transparency")); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Set initial state of widgets + auto pickAlpha = prefs->getInt( "/tools/dropper/pick", 1 ); + auto setAlpha = prefs->getBool( "/tools/dropper/setalpha", true); + + _pick_alpha_button->set_active(pickAlpha); + _set_alpha_button->set_active(setAlpha); + + // Make sure the set-alpha button is disabled if we're not picking alpha + _set_alpha_button->set_sensitive(pickAlpha); + + // Connect signal handlers + auto pick_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_pick_alpha_button_toggled); + auto set_alpha_button_toggled_cb = sigc::mem_fun(*this, &DropperToolbar::on_set_alpha_button_toggled); + + _pick_alpha_button->signal_toggled().connect(pick_alpha_button_toggled_cb); + _set_alpha_button->signal_toggled().connect(set_alpha_button_toggled_cb); + + show_all(); +} + +GtkWidget * +DropperToolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new DropperToolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/dropper-toolbar.h b/src/ui/toolbar/dropper-toolbar.h new file mode 100644 index 0000000..c8aa42f --- /dev/null +++ b/src/ui/toolbar/dropper-toolbar.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_DROPPER_TOOLBAR_H +#define SEEN_DROPPER_TOOLBAR_H + +/** + * @file + * Dropper aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +/** + * \brief A toolbar for controlling the dropper tool + */ +class DropperToolbar : public Toolbar { +private: + // Tool widgets + Gtk::ToggleToolButton *_pick_alpha_button; ///< Control whether to pick opacity + Gtk::ToggleToolButton *_set_alpha_button; ///< Control whether to set opacity + + // Event handlers + void on_pick_alpha_button_toggled(); + void on_set_alpha_button_toggled(); + +protected: + DropperToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} +#endif /* !SEEN_DROPPER_TOOLBAR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/eraser-toolbar.cpp b/src/ui/toolbar/eraser-toolbar.cpp new file mode 100644 index 0000000..279b82d --- /dev/null +++ b/src/ui/toolbar/eraser-toolbar.cpp @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Erasor aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "eraser-toolbar.h" + +#include <array> + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tools/eraser-tool.h" + +#include "ui/widget/spin-button-tool-item.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +EraserToolbar::EraserToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _freeze(false) +{ + gint eraser_mode = ERASER_MODE_DELETE; + auto prefs = Inkscape::Preferences::get(); + + // Mode + { + add_label(_("Mode:")); + + Gtk::RadioToolButton::Group mode_group; + + std::vector<Gtk::RadioToolButton *> mode_buttons; + + auto delete_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete"))); + delete_btn->set_tooltip_text(_("Delete objects touched by eraser")); + delete_btn->set_icon_name(INKSCAPE_ICON("draw-eraser-delete-objects")); + mode_buttons.push_back(delete_btn); + + auto cut_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Cut"))); + cut_btn->set_tooltip_text(_("Cut out from paths and shapes")); + cut_btn->set_icon_name(INKSCAPE_ICON("path-difference")); + mode_buttons.push_back(cut_btn); + + auto clip_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Clip"))); + clip_btn->set_tooltip_text(_("Clip from objects")); + clip_btn->set_icon_name(INKSCAPE_ICON("path-intersection")); + mode_buttons.push_back(clip_btn); + + eraser_mode = prefs->getInt("/tools/eraser/mode", ERASER_MODE_CLIP); // Used at end + + mode_buttons[eraser_mode]->set_active(); + + int btn_index = 0; + + for (auto btn : mode_buttons) + { + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &EraserToolbar::mode_changed), btn_index++)); + } + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Width */ + { + std::vector<Glib::ustring> labels = {_("(no width)"), _("(hairline)"), "", "", "", _("(default)"), "", "", "", "", _("(broad stroke)")}; + std::vector<double> values = { 0, 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/eraser/width", 15); + _width_adj = Gtk::Adjustment::create(width_val, 0, 100, 1.0, 10.0); + _width = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-width", _("Width:"), _width_adj, 1, 0)); + _width->set_tooltip_text(_("The width of the eraser pen (relative to the visible canvas area)")); + _width->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _width->set_custom_numeric_menu_data(values, labels); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::width_value_changed)); + // TODO: Allow SpinButtonToolItem to display as a slider + // ege_adjustment_action_set_appearance( toolbar->_width, TOOLBAR_SLIDER_HINT ); + add(*_width); + _width->set_sensitive(true); + } + + /* Use Pressure button */ + { + _usepressure = add_toggle_button(_("Eraser Pressure"), + _("Use the pressure of the input device to alter the width of the pen")); + _usepressure->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _pressure_pusher.reset(new UI::SimplePrefPusher(_usepressure, "/tools/eraser/usepressure")); + _usepressure->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::usepressure_toggled)); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Thinning */ + { + std::vector<Glib::ustring> labels = {_("(speed blows up stroke)"), "", "", _("(slight widening)"), _("(constant width)"), _("(slight thinning, default)"), "", "", _("(speed deflates stroke)")}; + std::vector<double> values = { -100, -40, -20, -10, 0, 10, 20, 40, 100}; + auto thinning_val = prefs->getDouble("/tools/eraser/thinning", 10); + _thinning_adj = Gtk::Adjustment::create(thinning_val, -100, 100, 1, 10.0); + _thinning = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-thinning", _("Thinning:"), _thinning_adj, 1, 0)); + _thinning->set_tooltip_text(_("How much velocity thins the stroke (> 0 makes fast strokes thinner, < 0 makes them broader, 0 makes width independent of velocity)")); + _thinning->set_custom_numeric_menu_data(values, labels); + _thinning->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _thinning_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::velthin_value_changed)); + add(*_thinning); + _thinning->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Cap Rounding */ + { + std::vector<Glib::ustring> labels = {_("(blunt caps, default)"), _("(slightly bulging)"), "", "", _("(approximately round)"), _("(long protruding caps)")}; + std::vector<double> values = { 0, 0.3, 0.5, 1.0, 1.4, 5.0}; + auto cap_rounding_val = prefs->getDouble("/tools/eraser/cap_rounding", 0.0); + _cap_rounding_adj = Gtk::Adjustment::create(cap_rounding_val, 0.0, 5.0, 0.01, 0.1); + // TRANSLATORS: "cap" means "end" (both start and finish) here + _cap_rounding = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-cap-rounding", _("Caps:"), _cap_rounding_adj, 0.01, 2)); + _cap_rounding->set_tooltip_text(_("Increase to make caps at the ends of strokes protrude more (0 = no caps, 1 = round caps)")); + _cap_rounding->set_custom_numeric_menu_data(values, labels); + _cap_rounding->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _cap_rounding_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::cap_rounding_value_changed)); + add(*_cap_rounding); + _cap_rounding->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Tremor */ + { + std::vector<Glib::ustring> labels = {_("(smooth line)"), _("(slight tremor)"), _("(noticeable tremor)"), "", "", _("(maximum tremor)")}; + std::vector<double> values = { 0, 10, 20, 40, 60, 100}; + auto tremor_val = prefs->getDouble("/tools/eraser/tremor", 0.0); + _tremor_adj = Gtk::Adjustment::create(tremor_val, 0.0, 100, 1, 10.0); + _tremor = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-tremor", _("Tremor:"), _tremor_adj, 1, 0)); + _tremor->set_tooltip_text(_("Increase to make strokes rugged and trembling")); + _tremor->set_custom_numeric_menu_data(values, labels); + _tremor->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _tremor_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::tremor_value_changed)); + + // TODO: Allow slider appearance + //ege_adjustment_action_set_appearance( toolbar->_tremor, TOOLBAR_SLIDER_HINT ); + add(*_tremor); + _tremor->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Mass */ + { + std::vector<Glib::ustring> labels = {_("(no inertia)"), _("(slight smoothing, default)"), _("(noticeable lagging)"), "", "", _("(maximum inertia)")}; + std::vector<double> values = { 0.0, 2, 10, 20, 50, 100}; + auto mass_val = prefs->getDouble("/tools/eraser/mass", 10.0); + _mass_adj = Gtk::Adjustment::create(mass_val, 0.0, 100, 1, 10.0); + _mass = Gtk::manage(new UI::Widget::SpinButtonToolItem("eraser-mass", _("Mass:"), _mass_adj, 1, 0)); + _mass->set_tooltip_text(_("Increase to make the eraser drag behind, as if slowed by inertia")); + _mass->set_custom_numeric_menu_data(values, labels); + _mass->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _mass_adj->signal_value_changed().connect(sigc::mem_fun(*this, &EraserToolbar::mass_value_changed)); + // TODO: Allow slider appearance + //ege_adjustment_action_set_appearance( toolbar->_mass, TOOLBAR_SLIDER_HINT ); + add(*_mass); + _mass->set_sensitive(true); + } + + _separators.push_back(Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_separators.back()); + + /* Overlap */ + { + _split = add_toggle_button(_("Break apart cut items"), + _("Break apart cut items")); + _split->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _split->set_active( prefs->getBool("/tools/eraser/break_apart", false) ); + _split->signal_toggled().connect(sigc::mem_fun(*this, &EraserToolbar::toggle_break_apart)); + } + + show_all(); + + set_eraser_mode_visibility(eraser_mode); +} + +GtkWidget * +EraserToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new EraserToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +EraserToolbar::mode_changed(int mode) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/eraser/mode", mode ); + } + + set_eraser_mode_visibility(mode); + + // only take action if run by the attr_changed listener + if (!_freeze) { + // in turn, prevent listener from responding + _freeze = true; + + /* + if ( eraser_mode != ERASER_MODE_DELETE ) { + } else { + } + */ + // TODO finish implementation + + _freeze = false; + } +} + +void +EraserToolbar::set_eraser_mode_visibility(const guint eraser_mode) +{ + _split->set_visible((eraser_mode == ERASER_MODE_CUT)); + + const gboolean visibility = (eraser_mode != ERASER_MODE_DELETE); + + const std::array<Gtk::Widget *, 6> arr = {_cap_rounding, + _mass, + _thinning, + _tremor, + _usepressure, + _width}; + for (auto widget : arr) { + widget->set_visible(visibility); + } + + for (auto separator : _separators) { + separator->set_visible(visibility); + } +} + +void +EraserToolbar::width_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/width", _width_adj->get_value() ); +} + +void +EraserToolbar::mass_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/mass", _mass_adj->get_value() ); +} + +void +EraserToolbar::velthin_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/eraser/thinning", _thinning_adj->get_value() ); +} + +void +EraserToolbar::cap_rounding_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/cap_rounding", _cap_rounding_adj->get_value() ); +} + +void +EraserToolbar::tremor_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/eraser/tremor", _tremor_adj->get_value() ); +} + +void +EraserToolbar::toggle_break_apart() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _split->get_active(); + prefs->setBool("/tools/eraser/break_apart", active); +} + +void +EraserToolbar::usepressure_toggled() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/eraser/usepressure", _usepressure->get_active()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/eraser-toolbar.h b/src/ui/toolbar/eraser-toolbar.h new file mode 100644 index 0000000..4ab94b7 --- /dev/null +++ b/src/ui/toolbar/eraser-toolbar.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ERASOR_TOOLBAR_H +#define SEEN_ERASOR_TOOLBAR_H + +/** + * @file + * Erasor aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class SeparatorToolItem; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Widget { +class SpinButtonToolItem; +} + +namespace Toolbar { +class EraserToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_width; + UI::Widget::SpinButtonToolItem *_mass; + UI::Widget::SpinButtonToolItem *_thinning; + UI::Widget::SpinButtonToolItem *_cap_rounding; + UI::Widget::SpinButtonToolItem *_tremor; + + Gtk::ToggleToolButton *_usepressure; + Gtk::ToggleToolButton *_split; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mass_adj; + Glib::RefPtr<Gtk::Adjustment> _thinning_adj; + Glib::RefPtr<Gtk::Adjustment> _cap_rounding_adj; + Glib::RefPtr<Gtk::Adjustment> _tremor_adj; + + std::unique_ptr<SimplePrefPusher> _pressure_pusher; + + std::vector<Gtk::SeparatorToolItem *> _separators; + + bool _freeze; + + void mode_changed(int mode); + void set_eraser_mode_visibility(const guint eraser_mode); + void width_value_changed(); + void mass_value_changed(); + void velthin_value_changed(); + void cap_rounding_value_changed(); + void tremor_value_changed(); + static void update_presets_list(gpointer data); + void toggle_break_apart(); + void usepressure_toggled(); + +protected: + EraserToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_ERASOR_TOOLBAR_H */ diff --git a/src/ui/toolbar/gradient-toolbar.cpp b/src/ui/toolbar/gradient-toolbar.cpp new file mode 100644 index 0000000..e49dab4 --- /dev/null +++ b/src/ui/toolbar/gradient-toolbar.cpp @@ -0,0 +1,1178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include <gtkmm/comboboxtext.h> +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "gradient-toolbar.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "ui/icon-names.h" +#include "ui/tools/gradient-tool.h" +#include "ui/util.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "widgets/gradient-image.h" +#include "widgets/gradient-vector.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::ToolBase; + +static bool blocked = false; + +void gr_apply_gradient_to_item( SPItem *item, SPGradient *gr, SPGradientType initialType, Inkscape::PaintTarget initialMode, Inkscape::PaintTarget mode ) +{ + SPStyle *style = item->style; + bool isFill = (mode == Inkscape::FOR_FILL); + if (style + && (isFill ? style->fill.isPaintserver() : style->stroke.isPaintserver()) + //&& SP_IS_GRADIENT(isFill ? style->getFillPaintServer() : style->getStrokePaintServer()) ) { + && (isFill ? SP_IS_GRADIENT(style->getFillPaintServer()) : SP_IS_GRADIENT(style->getStrokePaintServer())) ) { + SPPaintServer *server = isFill ? style->getFillPaintServer() : style->getStrokePaintServer(); + if ( SP_IS_LINEARGRADIENT(server) ) { + sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_LINEAR, mode); + } else if ( SP_IS_RADIALGRADIENT(server) ) { + sp_item_set_gradient(item, gr, SP_GRADIENT_TYPE_RADIAL, mode); + } + } + else if (initialMode == mode) + { + sp_item_set_gradient(item, gr, initialType, mode); + } +} + +/** +Applies gradient vector gr to the gradients attached to the selected dragger of drag, or if none, +to all objects in selection. If there was no previous gradient on an item, uses gradient type and +fill/stroke setting from preferences to create new default (linear: left/right; radial: centered) +gradient. +*/ +void gr_apply_gradient(Inkscape::Selection *selection, GrDrag *drag, SPGradient *gr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPGradientType initialType = static_cast<SPGradientType>(prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR)); + Inkscape::PaintTarget initialMode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + // GRADIENTFIXME: make this work for multiple selected draggers. + + // First try selected dragger + if (drag && !drag->selected.empty()) { + GrDragger *dragger = *(drag->selected.begin()); + for(auto draggable : dragger->draggables) { //for all draggables of dragger + gr_apply_gradient_to_item(draggable->item, gr, initialType, initialMode, draggable->fill_or_stroke); + } + return; + } + + // If no drag or no dragger selected, act on selection + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + gr_apply_gradient_to_item(*i, gr, initialType, initialMode, initialMode); + } +} + +int gr_vector_list(Glib::RefPtr<Gtk::ListStore> store, SPDesktop *desktop, + bool selection_empty, SPGradient *gr_selected, bool gr_multi) +{ + int selected = -1; + + if (!blocked) { + std::cerr << "gr_vector_list: should be blocked!" << std::endl; + } + + // Get list of gradients in document. + SPDocument *document = desktop->getDocument(); + std::vector<SPObject *> gl; + std::vector<SPObject *> gradients = document->getResourceList( "gradient" ); + for (auto gradient : gradients) { + SPGradient *grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && !grad->isSolid() ) { + gl.push_back(gradient); + } + } + + store->clear(); + + Inkscape::UI::Widget::ComboToolItemColumns columns; + Gtk::TreeModel::Row row; + + if (gl.empty()) { + // The document has no gradients + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else if (selection_empty) { + // Document has gradients, but nothing is currently selected. + + row = *(store->append()); + row[columns.col_label ] = _("Nothing Selected"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else { + + if (gr_selected == nullptr) { + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + } + + if (gr_multi) { + row = *(store->append()); + row[columns.col_label ] = _("Multiple gradients"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + } + + int idx = 0; + for (auto it : gl) { + SPGradient *gradient = SP_GRADIENT(it); + + Glib::ustring label = gr_prepare_label(gradient); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradient_to_pixbuf_ref(gradient, 64, 16); + + row = *(store->append()); + row[columns.col_label ] = label; + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_pixbuf ] = pixbuf; + row[columns.col_data ] = gradient; + row[columns.col_sensitive] = true; + + if (gradient == gr_selected) { + selected = idx; + } + idx ++; + } + + if (gr_multi) { + selected = 0; // This will show "Multiple Gradients" + } + } + + return selected; +} + +/* + * Get the gradient of the selected desktop item + * This is gradient containing the repeat settings, not the underlying "getVector" href linked gradient. + */ +void gr_get_dt_selected_gradient(Inkscape::Selection *selection, SPGradient *&gr_selected) +{ + SPGradient *gradient = nullptr; + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i;// get the items gradient, not the getVector() version + SPStyle *style = item->style; + SPPaintServer *server = nullptr; + + if (style && (style->fill.isPaintserver())) { + server = item->style->getFillPaintServer(); + } + if (style && (style->stroke.isPaintserver())) { + server = item->style->getStrokePaintServer(); + } + + if ( SP_IS_GRADIENT(server) ) { + gradient = SP_GRADIENT(server); + } + } + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient) { + gr_selected = gradient; + } +} + +/* + * Get the current selection and dragger status from the desktop + */ +void gr_read_selection( Inkscape::Selection *selection, + GrDrag *drag, + SPGradient *&gr_selected, + bool &gr_multi, + SPGradientSpread &spr_selected, + bool &spr_multi ) +{ + if (drag && !drag->selected.empty()) { + // GRADIENTFIXME: make this work for more than one selected dragger? + GrDragger *dragger = *(drag->selected.begin()); + for(auto draggable : dragger->draggables) { //for all draggables of dragger + SPGradient *gradient = sp_item_gradient_get_vector(draggable->item, draggable->fill_or_stroke); + SPGradientSpread spread = sp_item_gradient_get_spread(draggable->item, draggable->fill_or_stroke); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + return; + } + + // If no selected dragger, read desktop selection + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + SPStyle *style = item->style; + + if (style && (style->fill.isPaintserver())) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient *gradient = SP_GRADIENT(server)->getVector(); + SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread(); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + } + if (style && (style->stroke.isPaintserver())) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( SP_IS_GRADIENT(server) ) { + SPGradient *gradient = SP_GRADIENT(server)->getVector(); + SPGradientSpread spread = SP_GRADIENT(server)->fetchSpread(); + + if (gradient && gradient->isSolid()) { + gradient = nullptr; + } + + if (gradient && (gradient != gr_selected)) { + if (gr_selected) { + gr_multi = true; + } else { + gr_selected = gradient; + } + } + if (spread != spr_selected) { + if (spr_selected != SP_GRADIENT_SPREAD_UNDEFINED) { + spr_multi = true; + } else { + spr_selected = spread; + } + } + } + } + } + } + +namespace Inkscape { +namespace UI { +namespace Toolbar { +GradientToolbar::GradientToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + auto prefs = Inkscape::Preferences::get(); + + /* New gradient linear or radial */ + { + add_label(_("New:")); + + Gtk::RadioToolButton::Group new_type_group; + + auto linear_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("linear"))); + linear_button->set_tooltip_text(_("Create linear gradient")); + linear_button->set_icon_name(INKSCAPE_ICON("paint-gradient-linear")); + _new_type_buttons.push_back(linear_button); + + auto radial_button = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("radial"))); + radial_button->set_tooltip_text(_("Create radial (elliptic or circular) gradient")); + radial_button->set_icon_name(INKSCAPE_ICON("paint-gradient-radial")); + _new_type_buttons.push_back(radial_button); + + gint mode = prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR); + _new_type_buttons[ mode == SP_GRADIENT_TYPE_LINEAR ? 0 : 1 ]->set_active(); // linear == 1, radial == 2 + + int btn_index = 0; + for (auto btn : _new_type_buttons) + { + btn->set_sensitive(true); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_type_changed), btn_index++)); + add(*btn); + } + } + + /* New gradient on fill or stroke*/ + { + Gtk::RadioToolButton::Group new_fillstroke_group; + + auto fill_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill"))); + fill_btn->set_tooltip_text(_("Create gradient in the fill")); + fill_btn->set_icon_name(INKSCAPE_ICON("object-fill")); + _new_fillstroke_buttons.push_back(fill_btn); + + auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke"))); + stroke_btn->set_tooltip_text(_("Create gradient in the stroke")); + stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke")); + _new_fillstroke_buttons.push_back(stroke_btn); + + auto fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + _new_fillstroke_buttons[ fsmode == Inkscape::FOR_FILL ? 0 : 1 ]->set_active(); + + auto btn_index = 0; + for (auto btn : _new_fillstroke_buttons) + { + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &GradientToolbar::new_fillstroke_changed), btn_index++)); + btn->set_sensitive(); + add(*btn); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Gradient Select list*/ + { + UI::Widget::ComboToolItemColumns columns; + + auto store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + _select_cb = UI::Widget::ComboToolItem::create(_("Select"), // Label + "", // Tooltip + "Not Used", // Icon + store ); // Tree store + + _select_cb->use_icon( false ); + _select_cb->use_pixbuf( true ); + _select_cb->use_group_label( true ); + _select_cb->set_active( 0 ); + _select_cb->set_sensitive( false ); + + add(*_select_cb); + _select_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::gradient_changed)); + } + + // Gradients Linked toggle + { + _linked_item = add_toggle_button(_("Link gradients"), + _("Link gradients to change all related gradients")); + _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked")); + _linked_item->signal_toggled().connect(sigc::mem_fun(*this, &GradientToolbar::linked_changed)); + + bool linkedmode = prefs->getBool("/options/forkgradientvectors/value", true); + _linked_item->set_active(!linkedmode); + } + + /* Reverse */ + { + _stops_reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse"))); + _stops_reverse_item->set_tooltip_text(_("Reverse the direction of the gradient")); + _stops_reverse_item->set_icon_name(INKSCAPE_ICON("object-flip-horizontal")); + _stops_reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::reverse)); + add(*_stops_reverse_item); + _stops_reverse_item->set_sensitive(false); + } + + // Gradient Spread type (how a gradient is drawn outside its nominal area) + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> spread_dropdown_items_list = { + const_cast<gchar *>(C_("Gradient repeat type", "None")), + _("Reflected"), + _("Direct") + }; + + for (auto item: spread_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _spread_cb = Gtk::manage(UI::Widget::ComboToolItem::create(_("Repeat: "), + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/pservers.html#LinearGradientSpreadMethodAttribute + _("Whether to fill with flat color beyond the ends of the gradient vector " + "(spreadMethod=\"pad\"), or repeat the gradient in the same direction " + "(spreadMethod=\"repeat\"), or repeat the gradient in alternating opposite " + "directions (spreadMethod=\"reflect\")"), + "Not Used", store)); + _spread_cb->use_group_label(true); + + _spread_cb->set_active(0); + _spread_cb->set_sensitive(false); + + _spread_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::spread_changed)); + add(*_spread_cb); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Gradient Stop list */ + { + UI::Widget::ComboToolItemColumns columns; + + auto store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("No stops"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + _stop_cb = + UI::Widget::ComboToolItem::create(_("Stops" ), // Label + "", // Tooltip + "Not Used", // Icon + store ); // Tree store + + _stop_cb->use_icon( false ); + _stop_cb->use_pixbuf( true ); + _stop_cb->use_group_label( true ); + _stop_cb->set_active( 0 ); + _stop_cb->set_sensitive( false ); + + add(*_stop_cb); + _stop_cb->signal_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_changed)); + } + + /* Offset */ + { + auto offset_val = prefs->getDouble("/tools/gradient/stopoffset", 0); + _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 1.0, 0.01, 0.1); + _offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("gradient-stopoffset", C_("Gradient", "Offset:"), _offset_adj, 0.01, 2)); + _offset_item->set_tooltip_text(_("Offset of selected stop")); + _offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &GradientToolbar::stop_offset_adjustment_changed)); + add(*_offset_item); + _offset_item->set_sensitive(false); + } + + /* Add stop */ + { + _stops_add_item = Gtk::manage(new Gtk::ToolButton(_("Insert new stop"))); + _stops_add_item->set_tooltip_text(_("Insert new stop")); + _stops_add_item->set_icon_name(INKSCAPE_ICON("node-add")); + _stops_add_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::add_stop)); + add(*_stops_add_item); + _stops_add_item->set_sensitive(false); + } + + /* Delete stop */ + { + _stops_delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete stop"))); + _stops_delete_item->set_tooltip_text(_("Delete stop")); + _stops_delete_item->set_icon_name(INKSCAPE_ICON("node-delete")); + _stops_delete_item->signal_clicked().connect(sigc::mem_fun(*this, &GradientToolbar::remove_stop)); + add(*_stops_delete_item); + _stops_delete_item->set_sensitive(false); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &GradientToolbar::check_ec)); + + show_all(); +} + +/** + * Gradient auxiliary toolbar construction and setup. + * + */ +GtkWidget * +GradientToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new GradientToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +GradientToolbar::new_type_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/gradient/newgradient", + mode == 0 ? SP_GRADIENT_TYPE_LINEAR : SP_GRADIENT_TYPE_RADIAL); +} + +void +GradientToolbar::new_fillstroke_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::PaintTarget fsmode = (mode == 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + prefs->setInt("/tools/gradient/newfillorstroke", (fsmode == Inkscape::FOR_FILL) ? 1 : 0); +} + +/* + * User selected a gradient from the combobox + */ +void +GradientToolbar::gradient_changed(int active) +{ + if (blocked) { + return; + } + + if (active < 0) { + return; + } + + blocked = true; + + SPGradient *gr = get_selected_gradient(); + + if (gr) { + gr = sp_gradient_ensure_vector_normalized(gr); + + Inkscape::Selection *selection = _desktop->getSelection(); + ToolBase *ev = _desktop->getEventContext(); + + gr_apply_gradient(selection, ev ? ev->get_drag() : nullptr, gr); + + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Assign gradient to object")); + } + + blocked = false; +} + +/** + * \brief Return gradient selected in menu + */ +SPGradient * +GradientToolbar::get_selected_gradient() +{ + int active = _select_cb->get_active(); + + auto store = _select_cb->get_store(); + auto row = store->children()[active]; + UI::Widget::ComboToolItemColumns columns; + + void* pointer = row[columns.col_data]; + SPGradient *gr = static_cast<SPGradient *>(pointer); + + return gr; +} + +/** + * \brief User selected a spread method from the combobox + */ +void +GradientToolbar::spread_changed(int active) +{ + if (blocked) { + return; + } + + blocked = true; + + Inkscape::Selection *selection = _desktop->getSelection(); + SPGradient *gradient = nullptr; + gr_get_dt_selected_gradient(selection, gradient); + + if (gradient) { + SPGradientSpread spread = (SPGradientSpread) active; + gradient->setSpread(spread); + gradient->updateRepr(); + + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Set gradient repeat")); + } + + blocked = false; +} + +/** + * \brief User selected a stop from the combobox + */ +void +GradientToolbar::stop_changed(int active) +{ + if (blocked) { + return; + } + + blocked = true; + + ToolBase *ev = _desktop->getEventContext(); + SPGradient *gr = get_selected_gradient(); + + select_dragger_by_stop(gr, ev); + + blocked = false; +} + +void +GradientToolbar::select_dragger_by_stop(SPGradient *gradient, + ToolBase *ev) +{ + if (!blocked) { + std::cerr << "select_dragger_by_stop: should be blocked!" << std::endl; + } + + if (!ev || !gradient) { + return; + } + + GrDrag *drag = ev->get_drag(); + if (!drag) { + return; + } + + SPStop *stop = get_selected_stop(); + + drag->selectByStop(stop, false, true); + + stop_set_offset(); +} + +/** + * \brief Get stop selected by menu + */ +SPStop * +GradientToolbar::get_selected_stop() +{ + int active = _stop_cb->get_active(); + + auto store = _stop_cb->get_store(); + auto row = store->children()[active]; + UI::Widget::ComboToolItemColumns columns; + void* pointer = row[columns.col_data]; + SPStop *stop = static_cast<SPStop *>(pointer); + + return stop; +} + +/** + * Change desktop dragger selection to this stop + * + * Set the offset widget value (based on which stop is selected) + */ +void +GradientToolbar::stop_set_offset() +{ + if (!blocked) { + std::cerr << "gr_stop_set_offset: should be blocked!" << std::endl; + } + + SPStop *stop = get_selected_stop(); + if (!stop) { + // std::cerr << "gr_stop_set_offset: no stop!" << std::endl; + return; + } + + if (!_offset_item) { + return; + } + bool isEndStop = false; + + SPStop *prev = nullptr; + prev = stop->getPrevStop(); + if (prev != nullptr ) { + _offset_adj->set_lower(prev->offset); + } else { + isEndStop = true; + _offset_adj->set_lower(0); + } + + SPStop *next = nullptr; + next = stop->getNextStop(); + if (next != nullptr ) { + _offset_adj->set_upper(next->offset); + } else { + isEndStop = true; + _offset_adj->set_upper(1.0); + } + + _offset_adj->set_value(stop->offset); + _offset_item->set_sensitive( !isEndStop ); +} + +/** + * \brief User changed the offset + */ +void +GradientToolbar::stop_offset_adjustment_changed() +{ + if (blocked) { + return; + } + + blocked = true; + + SPStop *stop = get_selected_stop(); + if (stop) { + stop->offset = _offset_adj->get_value(); + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", SP_VERB_CONTEXT_GRADIENT, + _("Change gradient stop offset")); + + } + + blocked = false; +} + +/** + * \brief Add stop to gradient + */ +void +GradientToolbar::add_stop() +{ + if (!_desktop) { + return; + } + + auto selection = _desktop->getSelection(); + if (!selection) { + return; + } + + auto ev = _desktop->getEventContext(); + auto rc = SP_GRADIENT_CONTEXT(ev); + + if (rc) { + sp_gradient_context_add_stops_between_selected_stops(rc); + } +} + +/** + * \brief Remove stop from vector + */ +void +GradientToolbar::remove_stop() +{ + if (!_desktop) { + return; + } + + auto selection = _desktop->getSelection(); // take from desktop, not from args + if (!selection) { + return; + } + + auto ev = _desktop->getEventContext(); + GrDrag *drag = nullptr; + if (ev) { + drag = ev->get_drag(); + } + + if (drag) { + drag->deleteSelected(); + } +} + +/** + * \brief Reverse vector + */ +void +GradientToolbar::reverse() +{ + sp_gradient_reverse_selected_gradients(_desktop); +} + +/** + * \brief Lock or unlock links + */ +void +GradientToolbar::linked_changed() +{ + bool active = _linked_item->get_active(); + if ( active ) { + _linked_item->set_icon_name(INKSCAPE_ICON("object-locked")); + } else { + _linked_item->set_icon_name(INKSCAPE_ICON("object-unlocked")); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/forkgradientvectors/value", !active); +} + +// lp:1327267 +/** + * Checks the current tool and connects gradient aux toolbox signals if it happens to be the gradient tool. + * Called every time the current tool changes by signal emission. + */ +void +GradientToolbar::check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_GRADIENT_CONTEXT(ec)) { + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + // connect to selection modified and changed signals + _connection_changed = selection->connectChanged(sigc::mem_fun(*this, &GradientToolbar::selection_changed)); + _connection_modified = selection->connectModified(sigc::mem_fun(*this, &GradientToolbar::selection_modified)); + _connection_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &GradientToolbar::drag_selection_changed)); + + // Is this necessary? Couldn't hurt. + selection_changed(selection); + + // connect to release and modified signals of the defs (i.e. when someone changes gradient) + _connection_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &GradientToolbar::defs_release)); + _connection_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &GradientToolbar::defs_modified)); + } else { + if (_connection_changed) + _connection_changed.disconnect(); + if (_connection_modified) + _connection_modified.disconnect(); + if (_connection_subselection_changed) + _connection_subselection_changed.disconnect(); + if (_connection_defs_release) + _connection_defs_release.disconnect(); + if (_connection_defs_modified) + _connection_defs_modified.disconnect(); + } +} + +/** + * Core function, setup all the widgets whenever something changes on the desktop + */ +void +GradientToolbar::selection_changed(Inkscape::Selection * /*selection*/) +{ + if (blocked) + return; + + blocked = true; + + if (!_desktop) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args + if (selection) { + + ToolBase *ev = _desktop->getEventContext(); + GrDrag *drag = nullptr; + if (ev) { + drag = ev->get_drag(); + } + + SPGradient *gr_selected = nullptr; + SPGradientSpread spr_selected = SP_GRADIENT_SPREAD_UNDEFINED; + bool gr_multi = false; + bool spr_multi = false; + + gr_read_selection(selection, drag, gr_selected, gr_multi, spr_selected, spr_multi); + + // Gradient selection menu + auto store = _select_cb->get_store(); + int gradient = gr_vector_list (store, _desktop, selection->isEmpty(), gr_selected, gr_multi); + + if (gradient < 0) { + // No selection or no gradients + _select_cb->set_active( 0 ); + _select_cb->set_sensitive (false); + } else { + // Single gradient or multiple gradients + _select_cb->set_active( gradient ); + _select_cb->set_sensitive (true); + } + + // Spread menu + _spread_cb->set_sensitive( gr_selected && !gr_multi ); + _spread_cb->set_active( gr_selected ? (int)spr_selected : 0 ); + + _stops_add_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty())); + _stops_delete_item->set_sensitive((gr_selected && !gr_multi && drag && !drag->selected.empty())); + _stops_reverse_item->set_sensitive((gr_selected!= nullptr)); + + _stop_cb->set_sensitive( gr_selected && !gr_multi); + + update_stop_list (gr_selected, nullptr, gr_multi); + select_stop_by_draggers(gr_selected, ev); + } + + blocked = false; +} + +/** + * \brief Construct stop list + */ +int +GradientToolbar::update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi) +{ + if (!blocked) { + std::cerr << "update_stop_list should be blocked!" << std::endl; + } + + int selected = -1; + + auto store = _stop_cb->get_store(); + + if (!store) { + return selected; + } + + store->clear(); + + UI::Widget::ComboToolItemColumns columns; + Gtk::TreeModel::Row row; + + if (!SP_IS_GRADIENT(gradient)) { + // No valid gradient + + row = *(store->append()); + row[columns.col_label ] = _("No gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else if (!gradient->hasStops()) { + // Has gradient but it has no stops + + row = *(store->append()); + row[columns.col_label ] = _("No stops in gradient"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_data ] = nullptr; + row[columns.col_sensitive] = true; + + } else { + // Gradient has stops + + // Get list of stops + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + + SPStop *stop = SP_STOP(&ochild); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = sp_gradstop_to_pixbuf_ref (stop, 32, 16); + + Inkscape::XML::Node *repr = reinterpret_cast<SPItem *>(&ochild)->getRepr(); + Glib::ustring label = gr_ellipsize_text(repr->attribute("id"), 25); + + row = *(store->append()); + row[columns.col_label ] = label; + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_pixbuf ] = pixbuf; + row[columns.col_data ] = stop; + row[columns.col_sensitive] = true; + } + } + } + + if (new_stop != nullptr) { + selected = select_stop_in_list (gradient, new_stop); + } + + return selected; +} + +/** + * \brief Find position of new_stop in menu. + */ +int +GradientToolbar::select_stop_in_list(SPGradient *gradient, SPStop *new_stop) +{ + int i = 0; + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + if (&ochild == new_stop) { + return i; + } + i++; + } + } + return -1; +} + +/** + * \brief Set stop in menu to match stops selected by draggers + */ +void +GradientToolbar::select_stop_by_draggers(SPGradient *gradient, ToolBase *ev) +{ + if (!blocked) { + std::cerr << "select_stop_by_draggers should be blocked!" << std::endl; + } + + if (!ev || !gradient) + return; + + SPGradient *vector = gradient->getVector(); + if (!vector) + return; + + GrDrag *drag = ev->get_drag(); + + if (!drag || drag->selected.empty()) { + _stop_cb->set_active(0); + stop_set_offset(); + return; + } + + gint n = 0; + SPStop *stop = nullptr; + int selected = -1; + + // For all selected draggers + for(auto dragger : drag->selected) { + + // For all draggables of dragger + for(auto draggable : dragger->draggables) { + + if (draggable->point_type != POINT_RG_FOCUS) { + n++; + if (n > 1) break; + } + + stop = vector->getFirstStop(); + + switch (draggable->point_type) { + case POINT_LG_MID: + case POINT_RG_MID1: + case POINT_RG_MID2: + stop = sp_get_stop_i(vector, draggable->point_i); + break; + case POINT_LG_END: + case POINT_RG_R1: + case POINT_RG_R2: + stop = sp_last_stop(vector); + break; + default: + break; + } + } + if (n > 1) break; + } + + if (n > 1) { + // Multiple stops selected + if (_offset_item) { + _offset_item->set_sensitive(false); + } + + // Stop list always updated first... reinsert "Multiple stops" as first entry. + UI::Widget::ComboToolItemColumns columns; + auto store = _stop_cb->get_store(); + + auto row = *(store->prepend()); + row[columns.col_label ] = _("Multiple stops"); + row[columns.col_tooltip ] = ""; + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + selected = 0; + + } else { + selected = select_stop_in_list(gradient, stop); + } + + if (selected < 0) { + _stop_cb->set_active (0); + _stop_cb->set_sensitive (false); + } else { + _stop_cb->set_active (selected); + _stop_cb->set_sensitive (true); + stop_set_offset(); + } +} + +void +GradientToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + selection_changed(selection); +} + +void +GradientToolbar::drag_selection_changed(gpointer /*dragger*/) +{ + selection_changed(nullptr); +} + +void +GradientToolbar::defs_release(SPObject * /*defs*/) +{ + selection_changed(nullptr); +} + +void +GradientToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/) +{ + selection_changed(nullptr); +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/toolbar/gradient-toolbar.h b/src/ui/toolbar/gradient-toolbar.h new file mode 100644 index 0000000..d5ea1b0 --- /dev/null +++ b/src/ui/toolbar/gradient-toolbar.h @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_TOOLBAR_H +#define SEEN_GRADIENT_TOOLBAR_H + +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPGradient; + +namespace Gtk { +class ComboBoxText; +class ToolButton; +class ToolItem; +} + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class GradientToolbar : public Toolbar { +private: + std::vector<Gtk::RadioToolButton *> _new_type_buttons; + std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons; + UI::Widget::ComboToolItem *_select_cb; + UI::Widget::ComboToolItem *_spread_cb; + UI::Widget::ComboToolItem *_stop_cb; + + Gtk::ToolButton *_stops_add_item; + Gtk::ToolButton *_stops_delete_item; + Gtk::ToolButton *_stops_reverse_item; + Gtk::ToggleToolButton *_linked_item; + + UI::Widget::SpinButtonToolItem *_offset_item; + + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + void new_type_changed(int mode); + void new_fillstroke_changed(int mode); + void gradient_changed(int active); + SPGradient * get_selected_gradient(); + void spread_changed(int active); + void stop_changed(int active); + void select_dragger_by_stop(SPGradient *gradient, + UI::Tools::ToolBase *ev); + SPStop * get_selected_stop(); + void stop_set_offset(); + void stop_offset_adjustment_changed(); + void add_stop(); + void remove_stop(); + void reverse(); + void linked_changed(); + void check_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + int update_stop_list( SPGradient *gradient, SPStop *new_stop, bool gr_multi); + int select_stop_in_list(SPGradient *gradient, SPStop *new_stop); + void select_stop_by_draggers(SPGradient *gradient, UI::Tools::ToolBase *ev); + void selection_modified(Inkscape::Selection *selection, guint flags); + void drag_selection_changed(gpointer dragger); + void defs_release(SPObject * defs); + void defs_modified(SPObject *defs, guint flags); + + sigc::connection _connection_changed; + sigc::connection _connection_modified; + sigc::connection _connection_subselection_changed; + sigc::connection _connection_defs_release; + sigc::connection _connection_defs_modified; + +protected: + GradientToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_GRADIENT_TOOLBAR_H */ diff --git a/src/ui/toolbar/lpe-toolbar.cpp b/src/ui/toolbar/lpe-toolbar.cpp new file mode 100644 index 0000000..ad08f6a --- /dev/null +++ b/src/ui/toolbar/lpe-toolbar.cpp @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-toolbar.h" + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "live_effects/lpe-line_segment.h" + +#include "helper/action-context.h" +#include "helper/action.h" + +#include "ui/icon-names.h" +#include "ui/tools-switch.h" +#include "ui/tools/lpe-tool.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::ToolBase; +using Inkscape::UI::Tools::LpeTool; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +LPEToolbar::LPEToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _currentlpe(nullptr), + _currentlpeitem(nullptr) +{ + _tracker->setActiveUnit(_desktop->getNamedView()->display_units); + + auto unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + auto prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/lpetool/unit", unit->abbr); + + /* Automatically create a list of LPEs that get added to the toolbar **/ + { + Gtk::RadioToolButton::Group mode_group; + + // The first toggle button represents the state that no subtool is active. + auto inactive_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("All inactive"))); + inactive_mode_btn->set_tooltip_text(_("No geometric tool is active")); + inactive_mode_btn->set_icon_name(INKSCAPE_ICON("draw-geometry-inactive")); + _mode_buttons.push_back(inactive_mode_btn); + + Inkscape::LivePathEffect::EffectType type; + for (int i = 1; i < num_subtools; ++i) { // i == 0 ia INVALIDE_LPE. + + type = lpesubtools[i].type; + + auto btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, Inkscape::LivePathEffect::LPETypeConverter.get_label(type))); + btn->set_tooltip_text(_(Inkscape::LivePathEffect::LPETypeConverter.get_label(type).c_str())); + btn->set_icon_name(lpesubtools[i].icon_name); + _mode_buttons.push_back(btn); + } + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &LPEToolbar::mode_changed), btn_idx++)); + } + + int mode = prefs->getInt("/tools/lpetool/mode", 0); + _mode_buttons[mode]->set_active(); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Show limiting bounding box */ + { + _show_bbox_item = add_toggle_button(_("Show limiting bounding box"), + _("Show bounding box (used to cut infinite lines)")); + _show_bbox_item->set_icon_name(INKSCAPE_ICON("show-bounding-box")); + _show_bbox_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_bbox)); + _show_bbox_item->set_active(prefs->getBool( "/tools/lpetool/show_bbox", true )); + } + + /* Set limiting bounding box to bbox of current selection */ + { + // TODO: Shouldn't this just be a button (not toggle button)? + _bbox_from_selection_item = add_toggle_button(_("Get limiting bounding box from selection"), + _("Set limiting bounding box (used to cut infinite lines) to the bounding box of current selection")); + _bbox_from_selection_item->set_icon_name(INKSCAPE_ICON("draw-geometry-set-bounding-box")); + _bbox_from_selection_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_set_bbox)); + _bbox_from_selection_item->set_active(false); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Combo box to choose line segment type */ + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> line_segment_dropdown_items_list = { + _("Closed"), + _("Open start"), + _("Open end"), + _("Open both") + }; + + for (auto item: line_segment_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _line_segment_combo = Gtk::manage(UI::Widget::ComboToolItem::create(_("Line Type"), _("Choose a line segment type"), "Not Used", store)); + _line_segment_combo->use_group_label(false); + + _line_segment_combo->set_active(0); + + _line_segment_combo->signal_changed().connect(sigc::mem_fun(*this, &LPEToolbar::change_line_segment_type)); + add(*_line_segment_combo); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Display measuring info for selected items */ + { + _measuring_item = add_toggle_button(_("Display measuring info"), + _("Display measuring info for selected items")); + _measuring_item->set_icon_name(INKSCAPE_ICON("draw-geometry-show-measuring-info")); + _measuring_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::toggle_show_measuring_info)); + _measuring_item->set_active( prefs->getBool( "/tools/lpetool/show_measuring_info", true ) ); + } + + // Add the units menu + { + _units_item = _tracker->create_tool_item(_("Units"), ("") ); + add(*_units_item); + _units_item->signal_changed_after().connect(sigc::mem_fun(*this, &LPEToolbar::unit_changed)); + _units_item->set_sensitive( prefs->getBool("/tools/lpetool/show_measuring_info", true)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Open LPE dialog (to adapt parameters numerically) */ + { + // TODO: Shouldn't this be a regular Gtk::ToolButton (not toggle)? + _open_lpe_dialog_item = add_toggle_button(_("Open LPE dialog"), + _("Open LPE dialog (to adapt parameters numerically)")); + _open_lpe_dialog_item->set_icon_name(INKSCAPE_ICON("dialog-geometry")); + _open_lpe_dialog_item->signal_toggled().connect(sigc::mem_fun(*this, &LPEToolbar::open_lpe_dialog)); + _open_lpe_dialog_item->set_active(false); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &LPEToolbar::watch_ec)); + + show_all(); +} + +void +LPEToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +GtkWidget * +LPEToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new LPEToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +// this is called when the mode is changed via the toolbar (i.e., one of the subtool buttons is pressed) +void +LPEToolbar::mode_changed(int mode) +{ + using namespace Inkscape::LivePathEffect; + + ToolBase *ec = _desktop->event_context; + if (!SP_IS_LPETOOL_CONTEXT(ec)) { + return; + } + + // only take action if run by the attr_changed listener + if (!_freeze) { + // in turn, prevent listener from responding + _freeze = true; + + EffectType type = lpesubtools[mode].type; + + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + bool success = lpetool_try_construction(lc, type); + if (success) { + // since the construction was already performed, we set the state back to inactive + _mode_buttons[0]->set_active(); + mode = 0; + } else { + // switch to the chosen subtool + SP_LPETOOL_CONTEXT(_desktop->event_context)->mode = type; + } + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt( "/tools/lpetool/mode", mode ); + } + + _freeze = false; + } +} + +void +LPEToolbar::toggle_show_bbox() { + auto prefs = Inkscape::Preferences::get(); + + bool show = _show_bbox_item->get_active(); + prefs->setBool("/tools/lpetool/show_bbox", show); + + if (tools_isactive(_desktop, TOOLS_LPETOOL)) { + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + lpetool_context_reset_limiting_bbox(lc); + } +} + +void +LPEToolbar::toggle_set_bbox() +{ + auto selection = _desktop->selection; + + auto bbox = selection->visualBounds(); + + if (bbox) { + Geom::Point A(bbox->min()); + Geom::Point B(bbox->max()); + + A *= _desktop->doc2dt(); + B *= _desktop->doc2dt(); + + // TODO: should we provide a way to store points in prefs? + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/lpetool/bbox_upperleftx", A[Geom::X]); + prefs->setDouble("/tools/lpetool/bbox_upperlefty", A[Geom::Y]); + prefs->setDouble("/tools/lpetool/bbox_lowerrightx", B[Geom::X]); + prefs->setDouble("/tools/lpetool/bbox_lowerrighty", B[Geom::Y]); + + lpetool_context_reset_limiting_bbox(SP_LPETOOL_CONTEXT(_desktop->event_context)); + } + + _bbox_from_selection_item->set_active(false); +} + +void +LPEToolbar::change_line_segment_type(int mode) +{ + using namespace Inkscape::LivePathEffect; + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + auto line_seg = dynamic_cast<LPELineSegment *>(_currentlpe); + + if (_currentlpeitem && line_seg) { + line_seg->end_type.param_set_value(static_cast<Inkscape::LivePathEffect::EndType>(mode)); + sp_lpe_item_update_patheffect(_currentlpeitem, true, true); + } + + _freeze = false; +} + +void +LPEToolbar::toggle_show_measuring_info() +{ + if (!tools_isactive(_desktop, TOOLS_LPETOOL)) { + return; + } + + bool show = _measuring_item->get_active(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/lpetool/show_measuring_info", show); + + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + lpetool_show_measuring_info(lc, show); + + _units_item->set_sensitive( show ); +} + +void +LPEToolbar::unit_changed(int /* NotUsed */) +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/lpetool/unit", unit->abbr); + + if (SP_IS_LPETOOL_CONTEXT(_desktop->event_context)) { + LpeTool *lc = SP_LPETOOL_CONTEXT(_desktop->event_context); + lpetool_delete_measuring_items(lc); + lpetool_create_measuring_items(lc); + } +} + +void +LPEToolbar::open_lpe_dialog() +{ + if (tools_isactive(_desktop, TOOLS_LPETOOL)) { + sp_action_perform(Inkscape::Verb::get(SP_VERB_DIALOG_LIVE_PATH_EFFECT)->get_action(Inkscape::ActionContext(_desktop)), nullptr); + } + _open_lpe_dialog_item->set_active(false); +} + +void +LPEToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_LPETOOL_CONTEXT(ec)) { + // Watch selection + c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &LPEToolbar::sel_modified)); + c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &LPEToolbar::sel_changed)); + sel_changed(desktop->getSelection()); + } else { + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_selection_changed) + c_selection_changed.disconnect(); + } +} + +void +LPEToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + ToolBase *ec = selection->desktop()->event_context; + if (SP_IS_LPETOOL_CONTEXT(ec)) { + lpetool_update_measuring_items(SP_LPETOOL_CONTEXT(ec)); + } +} + +void +LPEToolbar::sel_changed(Inkscape::Selection *selection) +{ + using namespace Inkscape::LivePathEffect; + ToolBase *ec = selection->desktop()->event_context; + if (!SP_IS_LPETOOL_CONTEXT(ec)) { + return; + } + LpeTool *lc = SP_LPETOOL_CONTEXT(ec); + + lpetool_delete_measuring_items(lc); + lpetool_create_measuring_items(lc, selection); + + // activate line segment combo box if a single item with LPELineSegment is selected + SPItem *item = selection->singleItem(); + if (item && SP_IS_LPE_ITEM(item) && lpetool_item_has_construction(lc, item)) { + + SPLPEItem *lpeitem = SP_LPE_ITEM(item); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe && lpe->effectType() == LINE_SEGMENT) { + LPELineSegment *lpels = static_cast<LPELineSegment*>(lpe); + _currentlpe = lpe; + _currentlpeitem = lpeitem; + _line_segment_combo->set_sensitive(true); + _line_segment_combo->set_active( lpels->end_type.get_value() ); + } else { + _currentlpe = nullptr; + _currentlpeitem = nullptr; + _line_segment_combo->set_sensitive(false); + } + + } else { + _currentlpe = nullptr; + _currentlpeitem = nullptr; + _line_segment_combo->set_sensitive(false); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/toolbar/lpe-toolbar.h b/src/ui/toolbar/lpe-toolbar.h new file mode 100644 index 0000000..903d9da --- /dev/null +++ b/src/ui/toolbar/lpe-toolbar.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPE_TOOLBAR_H +#define SEEN_LPE_TOOLBAR_H + +/** + * @file + * LPE aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; +class SPLPEItem; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +class Selection; + +namespace LivePathEffect { +class Effect; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class UnitTracker; +} + +namespace Toolbar { +class LPEToolbar : public Toolbar { +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + std::vector<Gtk::RadioToolButton *> _mode_buttons; + Gtk::ToggleToolButton *_show_bbox_item; + Gtk::ToggleToolButton *_bbox_from_selection_item; + Gtk::ToggleToolButton *_measuring_item; + Gtk::ToggleToolButton *_open_lpe_dialog_item; + UI::Widget::ComboToolItem *_line_segment_combo; + UI::Widget::ComboToolItem *_units_item; + + bool _freeze; + + LivePathEffect::Effect *_currentlpe; + SPLPEItem *_currentlpeitem; + + sigc::connection c_selection_modified; + sigc::connection c_selection_changed; + + void mode_changed(int mode); + void unit_changed(int not_used); + void sel_modified(Inkscape::Selection *selection, guint flags); + void sel_changed(Inkscape::Selection *selection); + void change_line_segment_type(int mode); + void watch_ec(SPDesktop* desktop, UI::Tools::ToolBase* ec); + + void toggle_show_bbox(); + void toggle_set_bbox(); + void toggle_show_measuring_info(); + void open_lpe_dialog(); + +protected: + LPEToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + void set_mode(int mode); +}; + +} +} +} + +#endif /* !SEEN_LPE_TOOLBAR_H */ diff --git a/src/ui/toolbar/measure-toolbar.cpp b/src/ui/toolbar/measure-toolbar.cpp new file mode 100644 index 0000000..811a47c --- /dev/null +++ b/src/ui/toolbar/measure-toolbar.cpp @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Measure aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "measure-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "message-stack.h" + +#include "ui/icon-names.h" +#include "ui/tools/measure-tool.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::MeasureTool; + +/** Temporary hack: Returns the node tool in the active desktop. + * Will go away during tool refactoring. */ +static MeasureTool *get_measure_tool() +{ + MeasureTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (SP_IS_MEASURE_CONTEXT(ec)) { + tool = static_cast<MeasureTool*>(ec); + } + } + return tool; +} + + + +namespace Inkscape { +namespace UI { +namespace Toolbar { +MeasureToolbar::MeasureToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) +{ + auto prefs = Inkscape::Preferences::get(); + _tracker->setActiveUnitByAbbr(prefs->getString("/tools/measure/unit").c_str()); + + /* Font Size */ + { + auto font_size_val = prefs->getDouble("/tools/measure/fontsize", 10.0); + _font_size_adj = Gtk::Adjustment::create(font_size_val, 1.0, 36.0, 1.0, 4.0); + auto font_size_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-fontsize", _("Font Size:"), _font_size_adj, 0, 2)); + font_size_item->set_tooltip_text(_("The font size to be used in the measurement labels")); + font_size_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _font_size_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::fontsize_value_changed)); + add(*font_size_item); + } + + /* Precision */ + { + auto precision_val = prefs->getDouble("/tools/measure/precision", 2); + _precision_adj = Gtk::Adjustment::create(precision_val, 0, 10, 1, 0); + auto precision_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-precision", _("Precision:"), _precision_adj, 0, 0)); + precision_item->set_tooltip_text(_("Decimal precision of measure")); + precision_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _precision_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::precision_value_changed)); + add(*precision_item); + } + + /* Scale */ + { + auto scale_val = prefs->getDouble("/tools/measure/scale", 100.0); + _scale_adj = Gtk::Adjustment::create(scale_val, 0.0, 90000.0, 1.0, 4.0); + auto scale_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-scale", _("Scale %:"), _scale_adj, 0, 3)); + scale_item->set_tooltip_text(_("Scale the results")); + scale_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::scale_value_changed)); + add(*scale_item); + } + + /* units label */ + { + auto unit_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Units:"))); + unit_label->set_tooltip_text(_("The units to be used for the measurements")); + unit_label->set_use_markup(true); + add(*unit_label); + } + + /* units menu */ + { + auto ti = _tracker->create_tool_item(_("Units:"), _("The units to be used for the measurements") ); + ti->signal_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::unit_changed)); + add(*ti); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* measure only selected */ + { + _only_selected_item = add_toggle_button(_("Measure only selected"), + _("Measure only selected")); + _only_selected_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center")); + _only_selected_item->set_active(prefs->getBool("/tools/measure/only_selected", false)); + _only_selected_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_only_selected)); + } + + /* ignore_1st_and_last */ + { + _ignore_1st_and_last_item = add_toggle_button(_("Ignore first and last"), + _("Ignore first and last")); + _ignore_1st_and_last_item->set_icon_name(INKSCAPE_ICON("draw-geometry-line-segment")); + _ignore_1st_and_last_item->set_active(prefs->getBool("/tools/measure/ignore_1st_and_last", true)); + _ignore_1st_and_last_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_ignore_1st_and_last)); + } + + /* measure in betweens */ + { + _inbetween_item = add_toggle_button(_("Show measures between items"), + _("Show measures between items")); + _inbetween_item->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _inbetween_item->set_active(prefs->getBool("/tools/measure/show_in_between", true)); + _inbetween_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_in_between)); + } + + /* only visible */ + { + _show_hidden_item = add_toggle_button(_("Show hidden intersections"), + _("Show hidden intersections")); + _show_hidden_item->set_icon_name(INKSCAPE_ICON("object-hidden")); + _show_hidden_item->set_active(prefs->getBool("/tools/measure/show_hidden", true)); + _show_hidden_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_show_hidden)) ; + } + + /* measure only current layer */ + { + _all_layers_item = add_toggle_button(_("Measure all layers"), + _("Measure all layers")); + _all_layers_item->set_icon_name(INKSCAPE_ICON("dialog-layers")); + _all_layers_item->set_active(prefs->getBool("/tools/measure/all_layers", true)); + _all_layers_item->signal_toggled().connect(sigc::mem_fun(*this, &MeasureToolbar::toggle_all_layers)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* toggle start end */ + { + _reverse_item = Gtk::manage(new Gtk::ToolButton(_("Reverse measure"))); + _reverse_item->set_tooltip_text(_("Reverse measure")); + _reverse_item->set_icon_name(INKSCAPE_ICON("draw-geometry-mirror")); + _reverse_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::reverse_knots)); + add(*_reverse_item); + } + + /* phantom measure */ + { + _to_phantom_item = Gtk::manage(new Gtk::ToolButton(_("Phantom measure"))); + _to_phantom_item->set_tooltip_text(_("Phantom measure")); + _to_phantom_item->set_icon_name(INKSCAPE_ICON("selection-make-bitmap-copy")); + _to_phantom_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_phantom)); + add(*_to_phantom_item); + } + + /* to guides */ + { + _to_guides_item = Gtk::manage(new Gtk::ToolButton(_("To guides"))); + _to_guides_item->set_tooltip_text(_("To guides")); + _to_guides_item->set_icon_name(INKSCAPE_ICON("guides")); + _to_guides_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_guides)); + add(*_to_guides_item); + } + + /* to item */ + { + _to_item_item = Gtk::manage(new Gtk::ToolButton(_("Convert to item"))); + _to_item_item->set_tooltip_text(_("Convert to item")); + _to_item_item->set_icon_name(INKSCAPE_ICON("path-reverse")); + _to_item_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_item)); + add(*_to_item_item); + } + + /* to mark dimensions */ + { + _mark_dimension_item = Gtk::manage(new Gtk::ToolButton(_("Mark Dimension"))); + _mark_dimension_item->set_tooltip_text(_("Mark Dimension")); + _mark_dimension_item->set_icon_name(INKSCAPE_ICON("tool-pointer")); + _mark_dimension_item->signal_clicked().connect(sigc::mem_fun(*this, &MeasureToolbar::to_mark_dimension)); + add(*_mark_dimension_item); + } + + /* Offset */ + { + auto offset_val = prefs->getDouble("/tools/measure/offset", 5.0); + _offset_adj = Gtk::Adjustment::create(offset_val, 0.0, 90000.0, 1.0, 4.0); + auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("measure-offset", _("Offset:"), _offset_adj, 0, 2)); + offset_item->set_tooltip_text(_("Mark dimension offset")); + offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeasureToolbar::offset_value_changed)); + add(*offset_item); + } + + show_all(); +} + +GtkWidget * +MeasureToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new MeasureToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} // MeasureToolbar::prep() + +void +MeasureToolbar::fontsize_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/fontsize"), + _font_size_adj->get_value()); + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::unit_changed(int /* notUsed */) +{ + Glib::ustring const unit = _tracker->getActiveUnit()->abbr; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/tools/measure/unit", unit); + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::precision_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(Glib::ustring("/tools/measure/precision"), + _precision_adj->get_value()); + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::scale_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/scale"), + _scale_adj->get_value()); + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::offset_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/measure/offset"), + _offset_adj->get_value()); + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } + } +} + +void +MeasureToolbar::toggle_only_selected() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _only_selected_item->get_active(); + prefs->setBool("/tools/measure/only_selected", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measures only selected.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Measure all.")); + } + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_ignore_1st_and_last() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _ignore_1st_and_last_item->get_active(); + prefs->setBool("/tools/measure/ignore_1st_and_last", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures inactive.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Start and end measures active.")); + } + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_show_in_between() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _inbetween_item->get_active(); + prefs->setBool("/tools/measure/show_in_between", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute all elements.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Compute max length.")); + } + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_show_hidden() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _show_hidden_item->get_active(); + prefs->setBool("/tools/measure/show_hidden", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show all crossings.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Show visible crossings.")); + } + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::toggle_all_layers() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _all_layers_item->get_active(); + prefs->setBool("/tools/measure/all_layers", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use all layers in the measure.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Use current layer in the measure.")); + } + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->showCanvasItems(); + } +} + +void +MeasureToolbar::reverse_knots() +{ + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->reverseKnots(); + } +} + +void +MeasureToolbar::to_phantom() +{ + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->toPhantom(); + } +} + +void +MeasureToolbar::to_guides() +{ + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->toGuides(); + } +} + +void +MeasureToolbar::to_item() +{ + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->toItem(); + } +} + +void +MeasureToolbar::to_mark_dimension() +{ + MeasureTool *mt = get_measure_tool(); + if (mt) { + mt->toMarkDimension(); + } +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/measure-toolbar.h b/src/ui/toolbar/measure-toolbar.h new file mode 100644 index 0000000..a922fa1 --- /dev/null +++ b/src/ui/toolbar/measure-toolbar.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_MEASURE_TOOLBAR_H +#define SEEN_MEASURE_TOOLBAR_H + +/** + * @file + * Measure aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Widget { +class UnitTracker; +} + +namespace Toolbar { +class MeasureToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + Glib::RefPtr<Gtk::Adjustment> _font_size_adj; + Glib::RefPtr<Gtk::Adjustment> _precision_adj; + Glib::RefPtr<Gtk::Adjustment> _scale_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + Gtk::ToggleToolButton *_only_selected_item; + Gtk::ToggleToolButton *_ignore_1st_and_last_item; + Gtk::ToggleToolButton *_inbetween_item; + Gtk::ToggleToolButton *_show_hidden_item; + Gtk::ToggleToolButton *_all_layers_item; + + Gtk::ToolButton *_reverse_item; + Gtk::ToolButton *_to_phantom_item; + Gtk::ToolButton *_to_guides_item; + Gtk::ToolButton *_to_item_item; + Gtk::ToolButton *_mark_dimension_item; + + void fontsize_value_changed(); + void unit_changed(int notUsed); + void precision_value_changed(); + void scale_value_changed(); + void offset_value_changed(); + void toggle_only_selected(); + void toggle_ignore_1st_and_last(); + void toggle_show_hidden(); + void toggle_show_in_between(); + void toggle_all_layers(); + void reverse_knots(); + void to_phantom(); + void to_guides(); + void to_item(); + void to_mark_dimension(); + +protected: + MeasureToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_MEASURE_TOOLBAR_H */ diff --git a/src/ui/toolbar/mesh-toolbar.cpp b/src/ui/toolbar/mesh-toolbar.cpp new file mode 100644 index 0000000..c1ff08a --- /dev/null +++ b/src/ui/toolbar/mesh-toolbar.cpp @@ -0,0 +1,621 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * Tavmjong Bah <tavjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "mesh-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/comboboxtext.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/mesh-tool.h" +#include "ui/widget/color-preview.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "widgets/gradient-image.h" +#include "widgets/spinbutton-events.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Tools::MeshTool; + +static bool blocked = false; + +// Get a list of selected meshes taking into account fill/stroke toggles +std::vector<SPMeshGradient *> ms_get_dt_selected_gradients(Inkscape::Selection *selection) +{ + std::vector<SPMeshGradient *> ms_selected; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool edit_fill = prefs->getBool("/tools/mesh/edit_fill", true); + bool edit_stroke = prefs->getBool("/tools/mesh/edit_stroke", true); + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i;// get the items gradient, not the getVector() version + SPStyle *style = item->style; + + if (style) { + + + if (edit_fill && style->fill.isPaintserver()) { + SPPaintServer *server = item->style->getFillPaintServer(); + SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server); + if (mesh) { + ms_selected.push_back(mesh); + } + } + + if (edit_stroke && style->stroke.isPaintserver()) { + SPPaintServer *server = item->style->getStrokePaintServer(); + SPMeshGradient *mesh = dynamic_cast<SPMeshGradient *>(server); + if (mesh) { + ms_selected.push_back(mesh); + } + } + } + + } + return ms_selected; +} + + +/* + * Get the current selection status from the desktop + */ +void ms_read_selection( Inkscape::Selection *selection, + SPMeshGradient *&ms_selected, + bool &ms_selected_multi, + SPMeshType &ms_type, + bool &ms_type_multi ) +{ + ms_selected = nullptr; + ms_selected_multi = false; + ms_type = SP_MESH_TYPE_COONS; + ms_type_multi = false; + + bool first = true; + + // Read desktop selection, taking into account fill/stroke toggles + std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients( selection ); + for (auto & meshe : meshes) { + if (first) { + ms_selected = meshe; + ms_type = meshe->type; + first = false; + } else { + if (ms_selected != meshe) { + ms_selected_multi = true; + } + if (ms_type != meshe->type) { + ms_type_multi = true; + } + } + } +} + + +/* + * Callback functions for user actions + */ + + +/** Temporary hack: Returns the mesh tool in the active desktop. + * Will go away during tool refactoring. */ +static MeshTool *get_mesh_tool() +{ + MeshTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (SP_IS_MESH_CONTEXT(ec)) { + tool = static_cast<MeshTool*>(ec); + } + } + return tool; +} + + +static void mesh_toolbox_watch_ec(SPDesktop* dt, Inkscape::UI::Tools::ToolBase* ec, GObject* holder); + +namespace Inkscape { +namespace UI { +namespace Toolbar { +MeshToolbar::MeshToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _edit_fill_pusher(nullptr) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* New mesh: normal or conical */ + { + add_label(_("New:")); + + Gtk::RadioToolButton::Group new_type_group; + + auto normal_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("normal"))); + normal_type_btn->set_tooltip_text(_("Create mesh gradient")); + normal_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-mesh")); + _new_type_buttons.push_back(normal_type_btn); + + auto conical_type_btn = Gtk::manage(new Gtk::RadioToolButton(new_type_group, _("conical"))); + conical_type_btn->set_tooltip_text(_("Create conical gradient")); + conical_type_btn->set_icon_name(INKSCAPE_ICON("paint-gradient-conical")); + _new_type_buttons.push_back(conical_type_btn); + + int btn_idx = 0; + for (auto btn : _new_type_buttons) { + add(*btn); + btn->set_sensitive(); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_geometry_changed), btn_idx++)); + } + + gint mode = prefs->getInt("/tools/mesh/mesh_geometry", SP_MESH_GEOMETRY_NORMAL); + _new_type_buttons[mode]->set_active(); + } + + /* New gradient on fill or stroke*/ + { + Gtk::RadioToolButton::Group new_fillstroke_group; + + auto fill_button = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("fill"))); + fill_button->set_tooltip_text(_("Create gradient in the fill")); + fill_button->set_icon_name(INKSCAPE_ICON("object-fill")); + _new_fillstroke_buttons.push_back(fill_button); + + auto stroke_btn = Gtk::manage(new Gtk::RadioToolButton(new_fillstroke_group, _("stroke"))); + stroke_btn->set_tooltip_text(_("Create gradient in the stroke")); + stroke_btn->set_icon_name(INKSCAPE_ICON("object-stroke")); + _new_fillstroke_buttons.push_back(stroke_btn); + + int btn_idx = 0; + for(auto btn : _new_fillstroke_buttons) { + add(*btn); + btn->set_sensitive(true); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MeshToolbar::new_fillstroke_changed), btn_idx++)); + } + + gint mode = prefs->getInt("/tools/mesh/newfillorstroke"); + _new_fillstroke_buttons[mode]->set_active(); + } + + /* Number of mesh rows */ + { + std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto rows_val = prefs->getDouble("/tools/mesh/mesh_rows", 1); + _row_adj = Gtk::Adjustment::create(rows_val, 1, 20, 1, 1); + auto row_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-row", _("Rows:"), _row_adj, 1.0, 0)); + row_item->set_tooltip_text(_("Number of rows in new mesh")); + row_item->set_custom_numeric_menu_data(values); + row_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _row_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::row_changed)); + add(*row_item); + row_item->set_sensitive(true); + } + + /* Number of mesh columns */ + { + std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto col_val = prefs->getDouble("/tools/mesh/mesh_cols", 1); + _col_adj = Gtk::Adjustment::create(col_val, 1, 20, 1, 1); + auto col_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("mesh-col", _("Columns:"), _col_adj, 1.0, 0)); + col_item->set_tooltip_text(_("Number of columns in new mesh")); + col_item->set_custom_numeric_menu_data(values); + col_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _col_adj->signal_value_changed().connect(sigc::mem_fun(*this, &MeshToolbar::col_changed)); + add(*col_item); + col_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // TODO: These were disabled in the UI file. Either activate or delete +#if 0 + /* Edit fill mesh */ + { + _edit_fill_item = add_toggle_button(_("Edit Fill"), + _("Edit fill mesh")); + _edit_fill_item->set_icon_name(INKSCAPE_ICON("object-fill")); + _edit_fill_pusher.reset(new UI::SimplePrefPusher(_edit_fill_item, "/tools/mesh/edit_fill")); + _edit_fill_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke)); + } + + /* Edit stroke mesh */ + { + _edit_stroke_item = add_toggle_button(_("Edit Stroke"), + _("Edit stroke mesh")); + _edit_stroke_item->set_icon_name(INKSCAPE_ICON("object-stroke")); + _edit_stroke_pusher.reset(new UI::SimplePrefPusher(_edit_stroke_item, "/tools/mesh/edit_stroke")); + _edit_stroke_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_fill_stroke)); + } + + /* Show/hide side and tensor handles */ + { + auto show_handles_item = add_toggle_button(_("Show Handles"), + _("Show handles")); + show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles")); + _show_handles_pusher.reset(new UI::SimplePrefPusher(show_handles_item, "/tools/mesh/show_handles")); + show_handles_item->signal_toggled().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_handles)); + } +#endif + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &MeshToolbar::watch_ec)); + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Toggle Sides"))); + btn->set_tooltip_text(_("Toggle selected sides between Beziers and lines.")); + btn->set_icon_name(INKSCAPE_ICON("node-segment-line")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::toggle_sides)); + add(*btn); + } + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Make elliptical"))); + btn->set_tooltip_text(_("Make selected sides elliptical by changing length of handles. Works best if handles already approximate ellipse.")); + btn->set_icon_name(INKSCAPE_ICON("node-segment-curve")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::make_elliptical)); + add(*btn); + } + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Pick colors:"))); + btn->set_tooltip_text(_("Pick colors for selected corner nodes from underneath mesh.")); + btn->set_icon_name(INKSCAPE_ICON("color-picker")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::pick_colors)); + add(*btn); + } + + + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("Scale mesh to bounding box:"))); + btn->set_tooltip_text(_("Scale mesh to fit inside bounding box.")); + btn->set_icon_name(INKSCAPE_ICON("mesh-gradient-fit")); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::fit_mesh)); + add(*btn); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Warning */ + { + auto btn = Gtk::manage(new Gtk::ToolButton(_("WARNING: Mesh SVG Syntax Subject to Change"))); + btn->set_tooltip_text(_("WARNING: Mesh SVG Syntax Subject to Change")); + btn->set_icon_name(INKSCAPE_ICON("dialog-warning")); + add(*btn); + btn->signal_clicked().connect(sigc::mem_fun(*this, &MeshToolbar::warning_popup)); + btn->set_sensitive(true); + } + + /* Type */ + { + UI::Widget::ComboToolItemColumns columns; + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = C_("Type", "Coons"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Bicubic"); + row[columns.col_sensitive] = true; + + _select_type_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Smoothing:"), + // TRANSLATORS: Type of Smoothing. See https://en.wikipedia.org/wiki/Coons_patch + _("Coons: no smoothing. Bicubic: smoothing across patch boundaries."), + "Not Used", store)); + _select_type_item->use_group_label(true); + + _select_type_item->set_active(0); + + _select_type_item->signal_changed().connect(sigc::mem_fun(*this, &MeshToolbar::type_changed)); + add(*_select_type_item); + } + + show_all(); +} + +/** + * Mesh auxiliary toolbar construction and setup. + * Don't forget to add to XML in widgets/toolbox.cpp! + * + */ +GtkWidget * +MeshToolbar::create(SPDesktop * desktop) +{ + auto toolbar = new MeshToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +MeshToolbar::new_geometry_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/mesh/mesh_geometry", mode); +} + +void +MeshToolbar::new_fillstroke_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/mesh/newfillorstroke", mode); +} + +void +MeshToolbar::row_changed() +{ + if (blocked) { + return; + } + + blocked = TRUE; + + int rows = _row_adj->get_value(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/tools/mesh/mesh_rows", rows); + + blocked = FALSE; +} + +void +MeshToolbar::col_changed() +{ + if (blocked) { + return; + } + + blocked = TRUE; + + int cols = _col_adj->get_value(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + prefs->setInt("/tools/mesh/mesh_cols", cols); + + blocked = FALSE; +} + +void +MeshToolbar::toggle_fill_stroke() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("tools/mesh/edit_fill", _edit_fill_item->get_active()); + prefs->setBool("tools/mesh/edit_stroke", _edit_stroke_item->get_active()); + + MeshTool *mt = get_mesh_tool(); + if (mt) { + GrDrag *drag = mt->_grdrag; + drag->updateDraggers(); + drag->updateLines(); + drag->updateLevels(); + selection_changed(nullptr); // Need to update Type widget + } +} + +void +MeshToolbar::toggle_handles() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + GrDrag *drag = mt->_grdrag; + drag->refreshDraggers(); + } +} + +void +MeshToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (SP_IS_MESH_CONTEXT(ec)) { + // connect to selection modified and changed signals + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + c_selection_changed = selection->connectChanged(sigc::mem_fun(*this, &MeshToolbar::selection_changed)); + c_selection_modified = selection->connectModified(sigc::mem_fun(*this, &MeshToolbar::selection_modified)); + c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &MeshToolbar::drag_selection_changed)); + + c_defs_release = document->getDefs()->connectRelease(sigc::mem_fun(*this, &MeshToolbar::defs_release)); + c_defs_modified = document->getDefs()->connectModified(sigc::mem_fun(*this, &MeshToolbar::defs_modified)); + selection_changed(selection); + } else { + if (c_selection_changed) + c_selection_changed.disconnect(); + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_subselection_changed) + c_subselection_changed.disconnect(); + if (c_defs_release) + c_defs_release.disconnect(); + if (c_defs_modified) + c_defs_modified.disconnect(); + } +} + +void +MeshToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + selection_changed(selection); +} + +void +MeshToolbar::drag_selection_changed(gpointer /*dragger*/) +{ + selection_changed(nullptr); +} + +void +MeshToolbar::defs_release(SPObject * /*defs*/) +{ + selection_changed(nullptr); +} + +void +MeshToolbar::defs_modified(SPObject * /*defs*/, guint /*flags*/) +{ + selection_changed(nullptr); +} + +/* + * Core function, setup all the widgets whenever something changes on the desktop + */ +void +MeshToolbar::selection_changed(Inkscape::Selection * /* selection */) +{ + // std::cout << "ms_tb_selection_changed" << std::endl; + + if (blocked) + return; + + if (!_desktop) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); // take from desktop, not from args + if (selection) { + // ToolBase *ev = sp_desktop_event_context(desktop); + // GrDrag *drag = NULL; + // if (ev) { + // drag = ev->get_drag(); + // // Hide/show handles? + // } + + SPMeshGradient *ms_selected = nullptr; + SPMeshType ms_type = SP_MESH_TYPE_COONS; + bool ms_selected_multi = false; + bool ms_type_multi = false; + ms_read_selection( selection, ms_selected, ms_selected_multi, ms_type, ms_type_multi ); + // std::cout << " type: " << ms_type << std::endl; + + if (_select_type_item) { + _select_type_item->set_sensitive(!ms_type_multi); + blocked = TRUE; + _select_type_item->set_active(ms_type); + blocked = FALSE; + } + } +} + +void +MeshToolbar::warning_popup() +{ + char *msg = _("Mesh gradients are part of SVG 2:\n" + "* Syntax may change.\n" + "* Web browser implementation is not guaranteed.\n" + "\n" + "For web: convert to bitmap (Edit->Make bitmap copy).\n" + "For print: export to PDF."); + Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_WARNING, + Gtk::BUTTONS_OK, true); + dialog.run(); +} + +/** + * Sets mesh type: Coons, Bicubic + */ +void +MeshToolbar::type_changed(int mode) +{ + if (blocked) { + return; + } + + Inkscape::Selection *selection = _desktop->getSelection(); + std::vector<SPMeshGradient *> meshes = ms_get_dt_selected_gradients(selection); + + SPMeshType type = (SPMeshType) mode; + for (auto & meshe : meshes) { + meshe->type = type; + meshe->type_set = true; + meshe->updateRepr(); + } + if (!meshes.empty() ) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_MESH,_("Set mesh type")); + } +} + +void +MeshToolbar::toggle_sides() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + sp_mesh_context_corner_operation( mt, MG_CORNER_SIDE_TOGGLE ); + } +} + +void +MeshToolbar::make_elliptical() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + sp_mesh_context_corner_operation( mt, MG_CORNER_SIDE_ARC ); + } +} + +void +MeshToolbar::pick_colors() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + sp_mesh_context_corner_operation( mt, MG_CORNER_COLOR_PICK ); + } +} + +void +MeshToolbar::fit_mesh() +{ + MeshTool *mt = get_mesh_tool(); + if (mt) { + sp_mesh_context_fit_mesh_in_bbox( mt ); + } +} + + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/toolbar/mesh-toolbar.h b/src/ui/toolbar/mesh-toolbar.h new file mode 100644 index 0000000..2df4411 --- /dev/null +++ b/src/ui/toolbar/mesh-toolbar.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_MESH_TOOLBAR_H +#define SEEN_MESH_TOOLBAR_H + +/* + * Mesh aux toolbar + * + * Authors: + * bulia byak <bulia@dr.com> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 authors + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPObject; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +class Selection; + +namespace UI { +class SimplePrefPusher; + +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class MeshToolbar : public Toolbar { +private: + std::vector<Gtk::RadioToolButton *> _new_type_buttons; + std::vector<Gtk::RadioToolButton *> _new_fillstroke_buttons; + UI::Widget::ComboToolItem *_select_type_item; + + Gtk::ToggleToolButton *_edit_fill_item; + Gtk::ToggleToolButton *_edit_stroke_item; + + Glib::RefPtr<Gtk::Adjustment> _row_adj; + Glib::RefPtr<Gtk::Adjustment> _col_adj; + + std::unique_ptr<UI::SimplePrefPusher> _edit_fill_pusher; + std::unique_ptr<UI::SimplePrefPusher> _edit_stroke_pusher; + std::unique_ptr<UI::SimplePrefPusher> _show_handles_pusher; + + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_subselection_changed; + sigc::connection c_defs_release; + sigc::connection c_defs_modified; + + void new_geometry_changed(int mode); + void new_fillstroke_changed(int mode); + void row_changed(); + void col_changed(); + void toggle_fill_stroke(); + void selection_changed(Inkscape::Selection *selection); + void toggle_handles(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_modified(Inkscape::Selection *selection, guint flags); + void drag_selection_changed(gpointer dragger); + void defs_release(SPObject *defs); + void defs_modified(SPObject *defs, guint flags); + void warning_popup(); + void type_changed(int mode); + void toggle_sides(); + void make_elliptical(); + void pick_colors(); + void fit_mesh(); + +protected: + MeshToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_MESH_TOOLBAR_H */ diff --git a/src/ui/toolbar/node-toolbar.cpp b/src/ui/toolbar/node-toolbar.cpp new file mode 100644 index 0000000..9aa8328 --- /dev/null +++ b/src/ui/toolbar/node-toolbar.cpp @@ -0,0 +1,651 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Node aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "node-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/image.h> +#include <gtkmm/menutoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "helper/action.h" + +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; +using Inkscape::UI::Tools::NodeTool; + +/** Temporary hack: Returns the node tool in the active desktop. + * Will go away during tool refactoring. */ +static NodeTool *get_node_tool() +{ + NodeTool *tool = nullptr; + if (SP_ACTIVE_DESKTOP ) { + Inkscape::UI::Tools::ToolBase *ec = SP_ACTIVE_DESKTOP->event_context; + if (INK_IS_NODE_TOOL(ec)) { + tool = static_cast<NodeTool*>(ec); + } + } + return tool; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +NodeToolbar::NodeToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false) +{ + auto prefs = Inkscape::Preferences::get(); + + Unit doc_units = *desktop->getNamedView()->display_units; + _tracker->setActiveUnit(&doc_units); + + { + auto insert_node_item = Gtk::manage(new Gtk::MenuToolButton()); + insert_node_item->set_icon_name(INKSCAPE_ICON("node-add")); + insert_node_item->set_label(_("Insert node")); + insert_node_item->set_tooltip_text(_("Insert new nodes into selected segments")); + insert_node_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add)); + + auto insert_node_menu = Gtk::manage(new Gtk::Menu()); + + { + // TODO: Consider moving back to icons in menu? + //auto insert_min_x_icon = Gtk::manage(new Gtk::Image()); + //insert_min_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_x"), Gtk::ICON_SIZE_MENU); + //auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(*insert_min_x_icon)); + auto insert_min_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min X"))); + insert_min_x_item->set_tooltip_text(_("Insert new nodes at min X into selected segments")); + insert_min_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_x)); + insert_node_menu->append(*insert_min_x_item); + } + { + //auto insert_max_x_icon = Gtk::manage(new Gtk::Image()); + //insert_max_x_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_x"), Gtk::ICON_SIZE_MENU); + //auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(*insert_max_x_icon)); + auto insert_max_x_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max X"))); + insert_max_x_item->set_tooltip_text(_("Insert new nodes at max X into selected segments")); + insert_max_x_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_x)); + insert_node_menu->append(*insert_max_x_item); + } + { + //auto insert_min_y_icon = Gtk::manage(new Gtk::Image()); + //insert_min_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_min_y"), Gtk::ICON_SIZE_MENU); + //auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(*insert_min_y_icon)); + auto insert_min_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at min Y"))); + insert_min_y_item->set_tooltip_text(_("Insert new nodes at min Y into selected segments")); + insert_min_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_min_y)); + insert_node_menu->append(*insert_min_y_item); + } + { + //auto insert_max_y_icon = Gtk::manage(new Gtk::Image()); + //insert_max_y_icon->set_from_icon_name(INKSCAPE_ICON("node_insert_max_y"), Gtk::ICON_SIZE_MENU); + //auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(*insert_max_y_icon)); + auto insert_max_y_item = Gtk::manage(new Gtk::MenuItem(_("Insert node at max Y"))); + insert_max_y_item->set_tooltip_text(_("Insert new nodes at max Y into selected segments")); + insert_max_y_item->signal_activate().connect(sigc::mem_fun(*this, &NodeToolbar::edit_add_max_y)); + insert_node_menu->append(*insert_max_y_item); + } + + insert_node_menu->show_all(); + insert_node_item->set_menu(*insert_node_menu); + add(*insert_node_item); + } + + { + auto delete_item = Gtk::manage(new Gtk::ToolButton(_("Delete node"))); + delete_item->set_tooltip_text(_("Delete selected nodes")); + delete_item->set_icon_name(INKSCAPE_ICON("node-delete")); + delete_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete)); + add(*delete_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto join_item = Gtk::manage(new Gtk::ToolButton(_("Join nodes"))); + join_item->set_tooltip_text(_("Join selected nodes")); + join_item->set_icon_name(INKSCAPE_ICON("node-join")); + join_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join)); + add(*join_item); + } + + { + auto break_item = Gtk::manage(new Gtk::ToolButton(_("Break nodes"))); + break_item->set_tooltip_text(_("Break path at selected nodes")); + break_item->set_icon_name(INKSCAPE_ICON("node-break")); + break_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_break)); + add(*break_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto join_segment_item = Gtk::manage(new Gtk::ToolButton(_("Join with segment"))); + join_segment_item->set_tooltip_text(_("Join selected endnodes with a new segment")); + join_segment_item->set_icon_name(INKSCAPE_ICON("node-join-segment")); + join_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_join_segment)); + add(*join_segment_item); + } + + { + auto delete_segment_item = Gtk::manage(new Gtk::ToolButton(_("Delete segment"))); + delete_segment_item->set_tooltip_text(_("Delete segment between two non-endpoint nodes")); + delete_segment_item->set_icon_name(INKSCAPE_ICON("node-delete-segment")); + delete_segment_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_delete_segment)); + add(*delete_segment_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto cusp_item = Gtk::manage(new Gtk::ToolButton(_("Node Cusp"))); + cusp_item->set_tooltip_text(_("Make selected nodes corner")); + cusp_item->set_icon_name(INKSCAPE_ICON("node-type-cusp")); + cusp_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_cusp)); + add(*cusp_item); + } + + { + auto smooth_item = Gtk::manage(new Gtk::ToolButton(_("Node Smooth"))); + smooth_item->set_tooltip_text(_("Make selected nodes smooth")); + smooth_item->set_icon_name(INKSCAPE_ICON("node-type-smooth")); + smooth_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_smooth)); + add(*smooth_item); + } + + { + auto symmetric_item = Gtk::manage(new Gtk::ToolButton(_("Node Symmetric"))); + symmetric_item->set_tooltip_text(_("Make selected nodes symmetric")); + symmetric_item->set_icon_name(INKSCAPE_ICON("node-type-symmetric")); + symmetric_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_symmetrical)); + add(*symmetric_item); + } + + { + auto auto_item = Gtk::manage(new Gtk::ToolButton(_("Node Auto"))); + auto_item->set_tooltip_text(_("Make selected nodes auto-smooth")); + auto_item->set_icon_name(INKSCAPE_ICON("node-type-auto-smooth")); + auto_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_auto)); + add(*auto_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + auto line_item = Gtk::manage(new Gtk::ToolButton(_("Node Line"))); + line_item->set_tooltip_text(_("Make selected segments lines")); + line_item->set_icon_name(INKSCAPE_ICON("node-segment-line")); + line_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_toline)); + add(*line_item); + } + + { + auto curve_item = Gtk::manage(new Gtk::ToolButton(_("Node Curve"))); + curve_item->set_tooltip_text(_("Make selected segments curves")); + curve_item->set_icon_name(INKSCAPE_ICON("node-segment-curve")); + curve_item->signal_clicked().connect(sigc::mem_fun(*this, &NodeToolbar::edit_tocurve)); + add(*curve_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + auto context = Inkscape::ActionContext(_desktop); + + { + auto object_to_path_item = SPAction::create_toolbutton_for_verb(SP_VERB_OBJECT_TO_CURVE, context); + add(*object_to_path_item); + } + + { + auto stroke_to_path_item = SPAction::create_toolbutton_for_verb(SP_VERB_SELECTION_OUTLINE, context); + add(*stroke_to_path_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* X coord of selected node(s) */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto nodes_x_val = prefs->getDouble("/tools/nodes/Xcoord", 0); + _nodes_x_adj = Gtk::Adjustment::create(nodes_x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _nodes_x_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-x", _("X:"), _nodes_x_adj)); + _nodes_x_item->set_tooltip_text(_("X coordinate of selected node(s)")); + _nodes_x_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_nodes_x_adj->gobj()); + _nodes_x_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _nodes_x_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::X)); + _nodes_x_item->set_sensitive(false); + add(*_nodes_x_item); + } + + /* Y coord of selected node(s) */ + { + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + auto nodes_y_val = prefs->getDouble("/tools/nodes/Ycoord", 0); + _nodes_y_adj = Gtk::Adjustment::create(nodes_y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _nodes_y_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("node-y", _("Y:"), _nodes_y_adj)); + _nodes_y_item->set_tooltip_text(_("Y coordinate of selected node(s)")); + _nodes_y_item->set_custom_numeric_menu_data(values); + _tracker->addAdjustment(_nodes_y_adj->gobj()); + _nodes_y_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _nodes_y_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::value_changed), Geom::Y)); + _nodes_y_item->set_sensitive(false); + add(*_nodes_y_item); + } + + // add the units menu + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _object_edit_clip_path_item = add_toggle_button(_("Edit clipping paths"), + _("Show clipping path(s) of selected object(s)")); + _object_edit_clip_path_item->set_icon_name(INKSCAPE_ICON("path-clip-edit")); + _pusher_edit_clipping_paths.reset(new SimplePrefPusher(_object_edit_clip_path_item, "/tools/nodes/edit_clipping_paths")); + _object_edit_clip_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _object_edit_clip_path_item, + "/tools/nodes/edit_clipping_paths")); + } + + { + _object_edit_mask_path_item = add_toggle_button(_("Edit masks"), + _("Show mask(s) of selected object(s)")); + _object_edit_mask_path_item->set_icon_name(INKSCAPE_ICON("path-mask-edit")); + _pusher_edit_masks.reset(new SimplePrefPusher(_object_edit_mask_path_item, "/tools/nodes/edit_masks")); + _object_edit_mask_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _object_edit_mask_path_item, + "/tools/nodes/edit_masks")); + } + + { + _nodes_lpeedit_item = SPAction::create_toolbutton_for_verb(SP_VERB_EDIT_NEXT_PATHEFFECT_PARAMETER, context); + add(*_nodes_lpeedit_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _show_transform_handles_item = add_toggle_button(_("Show Transform Handles"), + _("Show transformation handles for selected nodes")); + _show_transform_handles_item->set_icon_name(INKSCAPE_ICON("node-transform")); + _pusher_show_transform_handles.reset(new UI::SimplePrefPusher(_show_transform_handles_item, "/tools/nodes/show_transform_handles")); + _show_transform_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_transform_handles_item, + "/tools/nodes/show_transform_handles")); + } + + { + _show_handles_item = add_toggle_button(_("Show Handles"), + _("Show Bezier handles of selected nodes")); + _show_handles_item->set_icon_name(INKSCAPE_ICON("show-node-handles")); + _pusher_show_handles.reset(new UI::SimplePrefPusher(_show_handles_item, "/tools/nodes/show_handles")); + _show_handles_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_handles_item, + "/tools/nodes/show_handles")); + } + + { + _show_helper_path_item = add_toggle_button(_("Show Outline"), + _("Show path outline (without path effects)")); + _show_helper_path_item->set_icon_name(INKSCAPE_ICON("show-path-outline")); + _pusher_show_outline.reset(new UI::SimplePrefPusher(_show_helper_path_item, "/tools/nodes/show_outline")); + _show_helper_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &NodeToolbar::on_pref_toggled), + _show_helper_path_item, + "/tools/nodes/show_outline")); + } + + sel_changed(desktop->getSelection()); + desktop->connectEventContextChanged(sigc::mem_fun(*this, &NodeToolbar::watch_ec)); + + show_all(); +} + +GtkWidget * +NodeToolbar::create(SPDesktop *desktop) +{ + auto holder = new NodeToolbar(desktop); + return GTK_WIDGET(holder->gobj()); +} // NodeToolbar::prep() + +void +NodeToolbar::value_changed(Geom::Dim2 d) +{ + auto adj = (d == Geom::X) ? _nodes_x_adj : _nodes_y_adj; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (!_tracker) { + return; + } + + Unit const *unit = _tracker->getActiveUnit(); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + prefs->setDouble(Glib::ustring("/tools/nodes/") + (d == Geom::X ? "x" : "y"), + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + NodeTool *nt = get_node_tool(); + if (nt && !nt->_selected_nodes->empty()) { + double val = Quantity::convert(adj->get_value(), unit, "px"); + double oldval = nt->_selected_nodes->pointwiseBounds()->midpoint()[d]; + Geom::Point delta(0,0); + delta[d] = val - oldval; + nt->_multipath->move(delta); + } + + _freeze = false; +} + +void +NodeToolbar::sel_changed(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + if (item && SP_IS_LPE_ITEM(item)) { + if (SP_LPE_ITEM(item)->hasPathEffect()) { + _nodes_lpeedit_item->set_sensitive(true); + } else { + _nodes_lpeedit_item->set_sensitive(false); + } + } else { + _nodes_lpeedit_item->set_sensitive(false); + } +} + +void +NodeToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (INK_IS_NODE_TOOL(ec)) { + // watch selection + c_selection_changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &NodeToolbar::sel_changed)); + c_selection_modified = desktop->getSelection()->connectModified(sigc::mem_fun(*this, &NodeToolbar::sel_modified)); + c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &NodeToolbar::coord_changed)); + + sel_changed(desktop->getSelection()); + } else { + if (c_selection_changed) + c_selection_changed.disconnect(); + if (c_selection_modified) + c_selection_modified.disconnect(); + if (c_subselection_changed) + c_subselection_changed.disconnect(); + } +} + +void +NodeToolbar::sel_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + sel_changed(selection); +} + +/* is called when the node selection is modified */ +void +NodeToolbar::coord_changed(gpointer /*shape_editor*/) +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + if (!_tracker) { + return; + } + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + NodeTool *nt = get_node_tool(); + if (!nt || !(nt->_selected_nodes) ||nt->_selected_nodes->empty()) { + // no path selected + _nodes_x_item->set_sensitive(false); + _nodes_y_item->set_sensitive(false); + } else { + _nodes_x_item->set_sensitive(true); + _nodes_y_item->set_sensitive(true); + Geom::Coord oldx = Quantity::convert(_nodes_x_adj->get_value(), unit, "px"); + Geom::Coord oldy = Quantity::convert(_nodes_y_adj->get_value(), unit, "px"); + Geom::Point mid = nt->_selected_nodes->pointwiseBounds()->midpoint(); + + if (oldx != mid[Geom::X]) { + _nodes_x_adj->set_value(Quantity::convert(mid[Geom::X], "px", unit)); + } + if (oldy != mid[Geom::Y]) { + _nodes_y_adj->set_value(Quantity::convert(mid[Geom::Y], "px", unit)); + } + } + + _freeze = false; +} + +void +NodeToolbar::edit_add() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodes(); + } +} + +void +NodeToolbar::edit_add_min_x() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_X); + } +} + +void +NodeToolbar::edit_add_max_x() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_X); + } +} + +void +NodeToolbar::edit_add_min_y() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MIN_Y); + } +} + +void +NodeToolbar::edit_add_max_y() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->insertNodesAtExtrema(Inkscape::UI::PointManipulator::EXTR_MAX_Y); + } +} + +void +NodeToolbar::edit_delete() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + nt->_multipath->deleteNodes(prefs->getBool("/tools/nodes/delete_preserves_shape", true)); + } +} + +void +NodeToolbar::edit_join() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->joinNodes(); + } +} + +void +NodeToolbar::edit_break() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->breakNodes(); + } +} + +void +NodeToolbar::edit_delete_segment() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->deleteSegments(); + } +} + +void +NodeToolbar::edit_join_segment() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->joinSegments(); + } +} + +void +NodeToolbar::edit_cusp() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_CUSP); + } +} + +void +NodeToolbar::edit_smooth() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_SMOOTH); + } +} + +void +NodeToolbar::edit_symmetrical() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_SYMMETRIC); + } +} + +void +NodeToolbar::edit_auto() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setNodeType(Inkscape::UI::NODE_AUTO); + } +} + +void +NodeToolbar::edit_toline() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_STRAIGHT); + } +} + +void +NodeToolbar::edit_tocurve() +{ + NodeTool *nt = get_node_tool(); + if (nt) { + nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_CUBIC_BEZIER); + } +} + +void +NodeToolbar::on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(path, item->get_active()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/node-toolbar.h b/src/ui/toolbar/node-toolbar.h new file mode 100644 index 0000000..fc603cb --- /dev/null +++ b/src/ui/toolbar/node-toolbar.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_NODE_TOOLBAR_H +#define SEEN_NODE_TOOLBAR_H + +/** + * @file + * Node aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" +#include "2geom/coord.h" + +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace UI { +class SimplePrefPusher; + +namespace Tools { +class ToolBase; +} + +namespace Widget { +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class NodeToolbar : public Toolbar { +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_transform_handles; + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_handles; + std::unique_ptr<UI::SimplePrefPusher> _pusher_show_outline; + std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_clipping_paths; + std::unique_ptr<UI::SimplePrefPusher> _pusher_edit_masks; + + Gtk::ToggleToolButton *_object_edit_clip_path_item; + Gtk::ToggleToolButton *_object_edit_mask_path_item; + Gtk::ToggleToolButton *_show_transform_handles_item; + Gtk::ToggleToolButton *_show_handles_item; + Gtk::ToggleToolButton *_show_helper_path_item; + + Gtk::ToolButton *_nodes_lpeedit_item; + + UI::Widget::SpinButtonToolItem *_nodes_x_item; + UI::Widget::SpinButtonToolItem *_nodes_y_item; + + Glib::RefPtr<Gtk::Adjustment> _nodes_x_adj; + Glib::RefPtr<Gtk::Adjustment> _nodes_y_adj; + + bool _freeze; + + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_subselection_changed; + + void value_changed(Geom::Dim2 d); + void sel_changed(Inkscape::Selection *selection); + void sel_modified(Inkscape::Selection *selection, guint /*flags*/); + void coord_changed(gpointer shape_editor); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void edit_add(); + void edit_add_min_x(); + void edit_add_max_x(); + void edit_add_min_y(); + void edit_add_max_y(); + void edit_delete(); + void edit_join(); + void edit_break(); + void edit_join_segment(); + void edit_delete_segment(); + void edit_cusp(); + void edit_smooth(); + void edit_symmetrical(); + void edit_auto(); + void edit_toline(); + void edit_tocurve(); + void on_pref_toggled(Gtk::ToggleToolButton *item, + const Glib::ustring& path); + +protected: + NodeToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/paintbucket-toolbar.cpp b/src/ui/toolbar/paintbucket-toolbar.cpp new file mode 100644 index 0000000..fc0b394 --- /dev/null +++ b/src/ui/toolbar/paintbucket-toolbar.cpp @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Paint bucket aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "paintbucket-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" + +#include "ui/icon-names.h" +#include "ui/tools/flood-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +PaintbucketToolbar::PaintbucketToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) +{ + auto prefs = Inkscape::Preferences::get(); + + // Channel + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + for (auto item: Inkscape::UI::Tools::FloodTool::channel_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _channels_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Fill by:"), Glib::ustring(), "Not Used", store)); + _channels_item->use_group_label(true); + + int channels = prefs->getInt("/tools/paintbucket/channels", 0); + _channels_item->set_active(channels); + + _channels_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::channels_changed)); + add(*_channels_item); + } + + // Spacing spinbox + { + auto threshold_val = prefs->getDouble("/tools/paintbucket/threshold", 5); + _threshold_adj = Gtk::Adjustment::create(threshold_val, 0, 100.0, 1.0, 10.0); + auto threshold_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-threshold", _("Threshold:"), _threshold_adj, 1, 0)); + threshold_item->set_tooltip_text(_("The maximum allowed difference between the clicked pixel and the neighboring pixels to be counted in the fill")); + threshold_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _threshold_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::threshold_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*threshold_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + // Create the units menu. + Glib::ustring stored_unit = prefs->getString("/tools/paintbucket/offsetunits"); + if (!stored_unit.empty()) { + Unit const *u = unit_table.getUnit(stored_unit); + _tracker->setActiveUnit(u); + } + + // Offset spinbox + { + auto offset_val = prefs->getDouble("/tools/paintbucket/offset", 0); + _offset_adj = Gtk::Adjustment::create(offset_val, -1e4, 1e4, 0.1, 0.5); + auto offset_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("inkscape:paintbucket-offset", _("Grow/shrink by:"), _offset_adj, 1, 2)); + offset_item->set_tooltip_text(_("The amount to grow (positive) or shrink (negative) the created fill path")); + _tracker->addAdjustment(_offset_adj->gobj()); + offset_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::offset_changed)); + add(*offset_item); + } + + { + auto unit_menu = _tracker->create_tool_item(_("Units"), ("")); + add(*unit_menu); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Auto Gap */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + for (auto item: Inkscape::UI::Tools::FloodTool::gap_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _autogap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Close gaps:"), Glib::ustring(), "Not Used", store)); + _autogap_item->use_group_label(true); + + int autogap = prefs->getInt("/tools/paintbucket/autogap", 0); + _autogap_item->set_active(autogap); + + _autogap_item->signal_changed().connect(sigc::mem_fun(*this, &PaintbucketToolbar::autogap_changed)); + add(*_autogap_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + auto reset_button = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + reset_button->set_tooltip_text(_("Reset paint bucket parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + reset_button->set_icon_name(INKSCAPE_ICON("edit-clear")); + reset_button->signal_clicked().connect(sigc::mem_fun(*this, &PaintbucketToolbar::defaults)); + add(*reset_button); + reset_button->set_sensitive(true); + } + + show_all(); +} + +GtkWidget * +PaintbucketToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new PaintbucketToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +PaintbucketToolbar::channels_changed(int channels) +{ + Inkscape::UI::Tools::FloodTool::set_channels(channels); +} + +void +PaintbucketToolbar::threshold_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/threshold", (gint)_threshold_adj->get_value()); +} + +void +PaintbucketToolbar::offset_changed() +{ + Unit const *unit = _tracker->getActiveUnit(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Don't adjust the offset value because we're saving the + // unit and it'll be correctly handled on load. + prefs->setDouble("/tools/paintbucket/offset", (gdouble)_offset_adj->get_value()); + + g_return_if_fail(unit != nullptr); + prefs->setString("/tools/paintbucket/offsetunits", unit->abbr); +} + +void +PaintbucketToolbar::autogap_changed(int autogap) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/autogap", autogap); +} + +void +PaintbucketToolbar::defaults() +{ + // FIXME: make defaults settable via Inkscape Options + _threshold_adj->set_value(15); + _offset_adj->set_value(0.0); + + _channels_item->set_active(Inkscape::UI::Tools::FLOOD_CHANNELS_RGB); + _autogap_item->set_active(0); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/paintbucket-toolbar.h b/src/ui/toolbar/paintbucket-toolbar.h new file mode 100644 index 0000000..d1b1a77 --- /dev/null +++ b/src/ui/toolbar/paintbucket-toolbar.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PAINTBUCKET_TOOLBAR_H +#define SEEN_PAINTBUCKET_TOOLBAR_H + +/** + * @file + * Paintbucket aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Widget { +class UnitTracker; +class ComboToolItem; +} + +namespace Toolbar { +class PaintbucketToolbar : public Toolbar { +private: + UI::Widget::ComboToolItem *_channels_item; + UI::Widget::ComboToolItem *_autogap_item; + + Glib::RefPtr<Gtk::Adjustment> _threshold_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + + UI::Widget::UnitTracker *_tracker; + + void channels_changed(int channels); + void threshold_changed(); + void offset_changed(); + void autogap_changed(int autogap); + void defaults(); + +protected: + PaintbucketToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} + +#endif /* !SEEN_PAINTBUCKET_TOOLBAR_H */ diff --git a/src/ui/toolbar/pencil-toolbar.cpp b/src/ui/toolbar/pencil-toolbar.cpp new file mode 100644 index 0000000..2b7fdb2 --- /dev/null +++ b/src/ui/toolbar/pencil-toolbar.cpp @@ -0,0 +1,622 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Pencil aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include "pencil-toolbar.h" + +#include "desktop.h" +#include "selection.h" + +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" + +#include "display/curve.h" + +#include "object/sp-shape.h" + +#include "ui/icon-names.h" +#include "ui/tools-switch.h" +#include "ui/tools/pen-tool.h" + +#include "ui/widget/label-tool-item.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "ui/uxmanager.h" + +#include "widgets/spinbutton-events.h" + +using Inkscape::UI::UXManager; + +/* +class PencilToleranceObserver : public Inkscape::Preferences::Observer { +public: + PencilToleranceObserver(Glib::ustring const &path, GObject *x) : Observer(path), _obj(x) + { + g_object_set_data(_obj, "prefobserver", this); + } + virtual ~PencilToleranceObserver() { + if (g_object_get_data(_obj, "prefobserver") == this) { + g_object_set_data(_obj, "prefobserver", NULL); + } + } + virtual void notify(Inkscape::Preferences::Entry const &val) { + GObject* tbl = _obj; + if (g_object_get_data( tbl, "freeze" )) { + return; + } + g_object_set_data( tbl, "freeze", GINT_TO_POINTER(TRUE) ); + + GtkAdjustment * adj = GTK_ADJUSTMENT(g_object_get_data(tbl, "tolerance")); + + double v = val.getDouble(adj->value); + gtk_adjustment_set_value(adj, v); + g_object_set_data( tbl, "freeze", GINT_TO_POINTER(FALSE) ); + } +private: + GObject *_obj; +}; +*/ + +namespace Inkscape { +namespace UI { +namespace Toolbar { +PencilToolbar::PencilToolbar(SPDesktop *desktop, + bool pencil_mode) + : Toolbar(desktop), + _repr(nullptr), + _freeze(false), + _flatten_simplify(nullptr), + _simplify(nullptr) +{ + auto prefs = Inkscape::Preferences::get(); + + add_freehand_mode_toggle(pencil_mode); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + if (pencil_mode) { + /* Use pressure */ + { + _pressure_item = add_toggle_button(_("Use pressure input"), _("Use pressure input")); + _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + bool pressure = prefs->getBool(freehand_tool_name() + "/pressure", false); + _pressure_item->set_active(pressure); + _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::use_pencil_pressure)); + } + /* min pressure */ + { + auto minpressure_val = prefs->getDouble("/tools/freehand/pencil/minpressure", 0); + _minpressure_adj = Gtk::Adjustment::create(minpressure_val, 0, 100, 1, 0); + _minpressure = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-minpressure", _("Min:"), _minpressure_adj, 0, 0)); + _minpressure->set_tooltip_text(_("Min percent of pressure")); + _minpressure->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _minpressure_adj->signal_value_changed().connect( + sigc::mem_fun(*this, &PencilToolbar::minpressure_value_changed)); + add(*_minpressure); + } + /* max pressure */ + { + auto maxpressure_val = prefs->getDouble("/tools/freehand/pencil/maxpressure", 30); + _maxpressure_adj = Gtk::Adjustment::create(maxpressure_val, 0, 100, 1, 0); + _maxpressure = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-maxpressure", _("Max:"), _maxpressure_adj, 0, 0)); + _maxpressure->set_tooltip_text(_("Max percent of pressure")); + _maxpressure->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _maxpressure_adj->signal_value_changed().connect( + sigc::mem_fun(*this, &PencilToolbar::maxpressure_value_changed)); + add(*_maxpressure); + } + + /* powerstoke */ + add_powerstroke_cap(pencil_mode); + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Tolerance */ + { + std::vector<Glib::ustring> labels = { _("(many nodes, rough)"), _("(default)"), "", "", "", "", + _("(few nodes, smooth)") }; + std::vector<double> values = { 1, 10, 20, 30, 50, 75, 100 }; + auto tolerance_val = prefs->getDouble("/tools/freehand/pencil/tolerance", 3.0); + _tolerance_adj = Gtk::Adjustment::create(tolerance_val, 0, 100.0, 0.5, 1.0); + auto tolerance_item = + Gtk::manage(new UI::Widget::SpinButtonToolItem("pencil-tolerance", _("Smoothing:"), _tolerance_adj, 1, 2)); + tolerance_item->set_tooltip_text(_("How much smoothing (simplifying) is applied to the line")); + tolerance_item->set_custom_numeric_menu_data(values, labels); + tolerance_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _tolerance_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PencilToolbar::tolerance_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*tolerance_item); + } + + /* LPE simplify based tolerance */ + { + _simplify = add_toggle_button(_("LPE based interactive simplify"), _("LPE based interactive simplify")); + _simplify->set_icon_name(INKSCAPE_ICON("interactive_simplify")); + _simplify->set_active(prefs->getInt("/tools/freehand/pencil/simplify", 0)); + _simplify->signal_toggled().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_lpe)); + } + + /* LPE simplify flatten */ + { + _flatten_simplify = Gtk::manage(new Gtk::ToolButton(_("LPE simplify flatten"))); + _flatten_simplify->set_tooltip_text(_("LPE simplify flatten")); + _flatten_simplify->set_icon_name(INKSCAPE_ICON("flatten")); + _flatten_simplify->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::simplify_flatten)); + add(*_flatten_simplify); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + } + + /* advanced shape options */ + add_advanced_shape_options(pencil_mode); + + show_all(); + + // Elements must be hidden after show_all() is called + guint freehandMode = prefs->getInt(( pencil_mode ? + "/tools/freehand/pencil/freehand-mode" : + "/tools/freehand/pen/freehand-mode" ), 0); + if (freehandMode != 1 && freehandMode != 2) { + _flatten_spiro_bspline->set_visible(false); + } + if (pencil_mode) { + use_pencil_pressure(); + } +} + +GtkWidget * +PencilToolbar::create_pencil(SPDesktop *desktop) +{ + auto toolbar = new PencilToolbar(desktop, true); + return GTK_WIDGET(toolbar->gobj()); +} + +PencilToolbar::~PencilToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } +} + +void +PencilToolbar::mode_changed(int mode) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(freehand_tool_name() + "/freehand-mode", mode); + + if (mode == 1 || mode == 2) { + _flatten_spiro_bspline->set_visible(true); + } else { + _flatten_spiro_bspline->set_visible(false); + } + + bool visible = (mode != 2); + + if (_simplify) { + _simplify->set_visible(visible); + if (_flatten_simplify) { + _flatten_simplify->set_visible(visible && _simplify->get_active()); + } + } + if (tools_isactive(_desktop, TOOLS_FREEHAND_PEN)) { + SP_PEN_CONTEXT(_desktop->event_context)->setPolylineMode(); + } +} + +/* This is used in generic functions below to share large portions of code between pen and pencil tool */ +Glib::ustring const +PencilToolbar::freehand_tool_name() +{ + return ( tools_isactive(_desktop, TOOLS_FREEHAND_PEN) + ? "/tools/freehand/pen" + : "/tools/freehand/pencil" ); +} + +void +PencilToolbar::add_freehand_mode_toggle(bool tool_is_pencil) +{ + auto label = Gtk::manage(new UI::Widget::LabelToolItem(_("Mode:"))); + label->set_tooltip_text(_("Mode of new lines drawn by this tool")); + add(*label); + /* Freehand mode toggle buttons */ + Gtk::RadioToolButton::Group mode_group; + auto bezier_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Bezier"))); + bezier_mode_btn->set_tooltip_text(_("Create regular Bezier path")); + bezier_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bezier")); + _mode_buttons.push_back(bezier_mode_btn); + + auto spiro_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spiro"))); + spiro_mode_btn->set_tooltip_text(_("Create Spiro path")); + spiro_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-spiro")); + _mode_buttons.push_back(spiro_mode_btn); + + auto bspline_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("BSpline"))); + bspline_mode_btn->set_tooltip_text(_("Create BSpline path")); + bspline_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-bspline")); + _mode_buttons.push_back(bspline_mode_btn); + + if (!tool_is_pencil) { + auto zigzag_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Zigzag"))); + zigzag_mode_btn->set_tooltip_text(_("Create a sequence of straight line segments")); + zigzag_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline")); + _mode_buttons.push_back(zigzag_mode_btn); + + auto paraxial_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Paraxial"))); + paraxial_mode_btn->set_tooltip_text(_("Create a sequence of paraxial line segments")); + paraxial_mode_btn->set_icon_name(INKSCAPE_ICON("path-mode-polyline-paraxial")); + _mode_buttons.push_back(paraxial_mode_btn); + } + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &PencilToolbar::mode_changed), btn_idx++)); + } + + auto prefs = Inkscape::Preferences::get(); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* LPE bspline spiro flatten */ + _flatten_spiro_bspline = Gtk::manage(new Gtk::ToolButton(_("LPE spiro or bspline flatten"))); + _flatten_spiro_bspline->set_tooltip_text(_("LPE spiro or bspline flatten")); + _flatten_spiro_bspline->set_icon_name(INKSCAPE_ICON("flatten")); + _flatten_spiro_bspline->signal_clicked().connect(sigc::mem_fun(*this, &PencilToolbar::flatten_spiro_bspline)); + add(*_flatten_spiro_bspline); + + guint freehandMode = prefs->getInt(( tool_is_pencil ? + "/tools/freehand/pencil/freehand-mode" : + "/tools/freehand/pen/freehand-mode" ), 0); + // freehandMode range is (0,5] for the pen tool, (0,3] for the pencil tool + // freehandMode = 3 is an old way of signifying pressure, set it to 0. + _mode_buttons[(freehandMode < _mode_buttons.size()) ? freehandMode : 0]->set_active(); +} + +void +PencilToolbar::minpressure_value_changed() +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/freehand/pencil/minpressure", _minpressure_adj->get_value()); +} + +void +PencilToolbar::maxpressure_value_changed() +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/freehand/pencil/maxpressure", _maxpressure_adj->get_value()); +} + +void +PencilToolbar::use_pencil_pressure() { + // assumes called by pencil toolbar (and all these widgets exist) + bool pressure = _pressure_item->get_active(); + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(freehand_tool_name() + "/pressure", pressure); + if (pressure) { + _minpressure->set_visible(true); + _maxpressure->set_visible(true); + _cap_item->set_visible(true); + _shape_item->set_visible(false); + _simplify->set_visible(false); + _flatten_spiro_bspline->set_visible(false); + _flatten_simplify->set_visible(false); + for (auto button : _mode_buttons) { + button->set_sensitive(false); + } + } else { + guint freehandMode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + + _minpressure->set_visible(false); + _maxpressure->set_visible(false); + _cap_item->set_visible(false); + _shape_item->set_visible(true); + bool simplify_visible = freehandMode != 2; + _simplify->set_visible(simplify_visible); + _flatten_simplify->set_visible(simplify_visible && _simplify->get_active()); + if (freehandMode == 1 || freehandMode == 2) { + _flatten_spiro_bspline->set_visible(true); + } + for (auto button : _mode_buttons) { + button->set_sensitive(true); + } + } +} + +void +PencilToolbar::add_advanced_shape_options(bool tool_is_pencil) +{ + /*advanced shape options */ + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar*> freehand_shape_dropdown_items_list = { + const_cast<gchar *>(C_("Freehand shape", "None")), + _("Triangle in"), + _("Triangle out"), + _("Ellipse"), + _("From clipboard"), + _("Bend from clipboard"), + _("Last applied") + }; + + for (auto item:freehand_shape_dropdown_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label ] = item; + row[columns.col_sensitive] = true; + } + + _shape_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Shape:"), _("Shape of new paths drawn by this tool"), "Not Used", store)); + _shape_item->use_group_label(true); + + auto prefs = Inkscape::Preferences::get(); + int shape = prefs->getInt((tool_is_pencil ? + "/tools/freehand/pencil/shape" : + "/tools/freehand/pen/shape" ), 0); + _shape_item->set_active(shape); + + _shape_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_shape)); + add(*_shape_item); +} + +void +PencilToolbar::change_shape(int shape) { + auto prefs = Inkscape::Preferences::get(); + prefs->setInt(freehand_tool_name() + "/shape", shape); +} + +void PencilToolbar::add_powerstroke_cap(bool tool_is_pencil) +{ + /* Powerstroke cap */ + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + std::vector<gchar *> powerstroke_cap_items_list = { const_cast<gchar *>(C_("Cap", "Butt")), _("Square"), _("Round"), + _("Peak"), _("Zero width") }; + for (auto item : powerstroke_cap_items_list) { + Gtk::TreeModel::Row row = *(store->append()); + row[columns.col_label] = item; + row[columns.col_sensitive] = true; + } + + _cap_item = Gtk::manage(UI::Widget::ComboToolItem::create(_("Caps:"), _("Cap for powerstroke pressure"), "Not Used", store)); + + auto prefs = Inkscape::Preferences::get(); + + int cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2); + _cap_item->set_active(cap); + _cap_item->use_group_label(true); + + _cap_item->signal_changed().connect(sigc::mem_fun(*this, &PencilToolbar::change_cap)); + + add(*_cap_item); +} + +void PencilToolbar::change_cap(int cap) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/live_effects/powerstroke/powerpencilcap", cap); +} + +void +PencilToolbar::simplify_lpe() +{ + bool simplify = _simplify->get_active(); + auto prefs = Inkscape::Preferences::get(); + prefs->setBool(freehand_tool_name() + "/simplify", simplify); + _flatten_simplify->set_visible(simplify); +} + +void +PencilToolbar::simplify_flatten() +{ + auto selected = _desktop->getSelection()->items(); + SPLPEItem* lpeitem = nullptr; + for (auto it(selected.begin()); it != selected.end(); ++it){ + lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + PathEffectList lpelist = lpeitem->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe)) { + SPShape * shape = dynamic_cast<SPShape *>(lpeitem); + if(shape){ + SPCurve * c = shape->getCurveForEdit(); + lpe->doEffect(c); + lpeitem->setCurrentPathEffect(*i); + if (lpelist.size() > 1){ + lpeitem->removeCurrentPathEffect(true); + shape->setCurveBeforeLPE(c); + } else { + lpeitem->removeCurrentPathEffect(false); + shape->setCurve(c, false); + } + break; + } + } + } + } + } + } + if (lpeitem) { + _desktop->getSelection()->remove(lpeitem->getRepr()); + _desktop->getSelection()->add(lpeitem->getRepr()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } +} + +void +PencilToolbar::flatten_spiro_bspline() +{ + auto selected = _desktop->getSelection()->items(); + SPLPEItem* lpeitem = nullptr; + + for (auto it(selected.begin()); it != selected.end(); ++it){ + lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + PathEffectList lpelist = lpeitem->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe) || + dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe)) + { + SPShape * shape = dynamic_cast<SPShape *>(lpeitem); + if(shape){ + SPCurve * c = shape->getCurveForEdit(); + lpe->doEffect(c); + lpeitem->setCurrentPathEffect(*i); + if (lpelist.size() > 1){ + lpeitem->removeCurrentPathEffect(true); + shape->setCurveBeforeLPE(c); + } else { + lpeitem->removeCurrentPathEffect(false); + shape->setCurve(c, false); + } + break; + } + } + } + } + } + } + if (lpeitem) { + _desktop->getSelection()->remove(lpeitem->getRepr()); + _desktop->getSelection()->add(lpeitem->getRepr()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } +} + +GtkWidget * +PencilToolbar::create_pen(SPDesktop *desktop) +{ + auto toolbar = new PencilToolbar(desktop, false); + return GTK_WIDGET(toolbar->gobj()); +} + +void +PencilToolbar::tolerance_value_changed() +{ + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _freeze = true; + prefs->setDouble("/tools/freehand/pencil/tolerance", + _tolerance_adj->get_value()); + _freeze = false; + auto selected = _desktop->getSelection()->items(); + for (auto it(selected.begin()); it != selected.end(); ++it){ + SPLPEItem* lpeitem = dynamic_cast<SPLPEItem*>(*it); + if (lpeitem && lpeitem->hasPathEffect()){ + Inkscape::LivePathEffect::Effect* simplify = lpeitem->getPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY); + if(simplify){ + Inkscape::LivePathEffect::LPESimplify *lpe_simplify = dynamic_cast<Inkscape::LivePathEffect::LPESimplify*>(simplify->getLPEObj()->get_lpe()); + if (lpe_simplify) { + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0); + tol = tol/(100.0*(102.0-tol)); + std::ostringstream ss; + ss << tol; + Inkscape::LivePathEffect::Effect* powerstroke = lpeitem->getPathEffectOfType(Inkscape::LivePathEffect::POWERSTROKE); + bool simplified = false; + if(powerstroke){ + Inkscape::LivePathEffect::LPEPowerStroke *lpe_powerstroke = dynamic_cast<Inkscape::LivePathEffect::LPEPowerStroke*>(powerstroke->getLPEObj()->get_lpe()); + if(lpe_powerstroke){ + lpe_powerstroke->getRepr()->setAttribute("is_visible", "false"); + sp_lpe_item_update_patheffect(lpeitem, false, false); + SPShape *sp_shape = dynamic_cast<SPShape *>(lpeitem); + if (sp_shape) { + guint previous_curve_length = sp_shape->getCurve(true)->get_segment_count(); + lpe_simplify->getRepr()->setAttribute("threshold", ss.str()); + sp_lpe_item_update_patheffect(lpeitem, false, false); + simplified = true; + guint curve_length = sp_shape->getCurve(true)->get_segment_count(); + std::vector<Geom::Point> ts = lpe_powerstroke->offset_points.data(); + double factor = (double)curve_length/ (double)previous_curve_length; + for (auto & t : ts) { + t[Geom::X] = t[Geom::X] * factor; + } + lpe_powerstroke->offset_points.param_setValue(ts); + } + lpe_powerstroke->getRepr()->setAttribute("is_visible", "true"); + sp_lpe_item_update_patheffect(lpeitem, false, false); + } + } + if(!simplified){ + lpe_simplify->getRepr()->setAttribute("threshold", ss.str()); + } + } + } + } + } +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/pencil-toolbar.h b/src/ui/toolbar/pencil-toolbar.h new file mode 100644 index 0000000..2b2f676 --- /dev/null +++ b/src/ui/toolbar/pencil-toolbar.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_PENCIL_TOOLBAR_H +#define SEEN_PENCIL_TOOLBAR_H + +/** + * @file + * Pencil aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class SpinButtonToolItem; +class ComboToolItem; +} + +namespace Toolbar { +class PencilToolbar : public Toolbar { +private: + std::vector<Gtk::RadioToolButton *> _mode_buttons; + + Gtk::ToggleToolButton *_pressure_item; + UI::Widget::SpinButtonToolItem *_minpressure; + UI::Widget::SpinButtonToolItem *_maxpressure; + + XML::Node *_repr; + Gtk::ToolButton *_flatten_spiro_bspline; + Gtk::ToolButton *_flatten_simplify; + + UI::Widget::ComboToolItem *_shape_item; + UI::Widget::ComboToolItem *_cap_item; + + Gtk::ToggleToolButton *_simplify; + + bool _freeze; + + Glib::RefPtr<Gtk::Adjustment> _minpressure_adj; + Glib::RefPtr<Gtk::Adjustment> _maxpressure_adj; + Glib::RefPtr<Gtk::Adjustment> _tolerance_adj; + + void add_freehand_mode_toggle(bool tool_is_pencil); + void mode_changed(int mode); + Glib::ustring const freehand_tool_name(); + void minpressure_value_changed(); + void maxpressure_value_changed(); + void use_pencil_pressure(); + void tolerance_value_changed(); + void add_advanced_shape_options(bool tool_is_pencil); + void add_powerstroke_cap(bool tool_is_pencil); + void change_shape(int shape); + void change_cap(int cap); + void simplify_lpe(); + void simplify_flatten(); + void flatten_spiro_bspline(); + +protected: + PencilToolbar(SPDesktop *desktop, bool pencil_mode); + ~PencilToolbar() override; + +public: + static GtkWidget * create_pencil(SPDesktop *desktop); + static GtkWidget * create_pen(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_PENCIL_TOOLBAR_H */ diff --git a/src/ui/toolbar/rect-toolbar.cpp b/src/ui/toolbar/rect-toolbar.cpp new file mode 100644 index 0000000..c938bb9 --- /dev/null +++ b/src/ui/toolbar/rect-toolbar.cpp @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Rect aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "rect-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toolbutton.h> + +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-namedview.h" +#include "object/sp-rect.h" + +#include "ui/icon-names.h" +#include "ui/tools/rect-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; + +static Inkscape::XML::NodeEventVector rect_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::RectToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +RectToolbar::RectToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _freeze(false), + _single(true), + _repr(nullptr), + _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // rx/ry units menu: create + //tracker->addUnit( SP_UNIT_PERCENT, 0 ); + // fixme: add % meaning per cent of the width/height + _tracker->setActiveUnit(desktop->getNamedView()->display_units); + _mode_item->set_use_markup(true); + + /* W */ + { + auto width_val = prefs->getDouble("/tools/shapes/rect/width", 0); + _width_adj = Gtk::Adjustment::create(width_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-width", _("W:"), _width_adj)); + _width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + _width_item->set_all_tooltip_text(_("Width of rectangle")); + + _width_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _width_adj, + "width", + &SPRect::setVisibleWidth)); + _tracker->addAdjustment(_width_adj->gobj()); + _width_item->set_sensitive(false); + + std::vector<double> values = {1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + _width_item->set_custom_numeric_menu_data(values); + } + + /* H */ + { + auto height_val = prefs->getDouble("/tools/shapes/rect/height", 0); + + _height_adj = Gtk::Adjustment::create(height_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _height_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _height_adj, + "height", + &SPRect::setVisibleHeight)); + _tracker->addAdjustment(_height_adj->gobj()); + + std::vector<double> values = { 1, 2, 3, 5, 10, 20, 50, 100, 200, 500}; + _height_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-height", _("H:"), _height_adj)); + _height_item->set_custom_numeric_menu_data(values); + _height_item->set_all_tooltip_text(_("Height of rectangle")); + _height_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _height_item->set_sensitive(false); + } + + /* rx */ + { + std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto rx_val = prefs->getDouble("/tools/shapes/rect/rx", 0); + _rx_adj = Gtk::Adjustment::create(rx_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _rx_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _rx_adj, + "rx", + &SPRect::setVisibleRx)); + _tracker->addAdjustment(_rx_adj->gobj()); + _rx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-rx", _("Rx:"), _rx_adj)); + _rx_item->set_all_tooltip_text(_("Horizontal radius of rounded corners")); + _rx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + _rx_item->set_custom_numeric_menu_data(values, labels); + } + + /* ry */ + { + std::vector<Glib::ustring> labels = {_("not rounded"), "", "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto ry_val = prefs->getDouble("/tools/shapes/rect/ry", 0); + _ry_adj = Gtk::Adjustment::create(ry_val, 0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _ry_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &RectToolbar::value_changed), + _ry_adj, + "ry", + &SPRect::setVisibleRy)); + _tracker->addAdjustment(_ry_adj->gobj()); + _ry_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("rect-ry", _("Ry:"), _ry_adj)); + _ry_item->set_all_tooltip_text(_("Vertical radius of rounded corners")); + _ry_item->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + _ry_item->set_custom_numeric_menu_data(values, labels); + } + + // add the units menu + auto unit_menu_ti = _tracker->create_tool_item(_("Units"), ("")); + + /* Reset */ + { + _not_rounded = Gtk::manage(new Gtk::ToolButton(_("Not rounded"))); + _not_rounded->set_tooltip_text(_("Make corners sharp")); + _not_rounded->set_icon_name(INKSCAPE_ICON("rectangle-make-corners-sharp")); + _not_rounded->signal_clicked().connect(sigc::mem_fun(*this, &RectToolbar::defaults)); + _not_rounded->set_sensitive(true); + } + + add(*_mode_item); + add(*_width_item); + add(*_height_item); + add(*_rx_item); + add(*_ry_item); + add(*unit_menu_ti); + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + add(*_not_rounded); + show_all(); + + sensitivize(); + + _desktop->connectEventContextChanged(sigc::mem_fun(*this, &RectToolbar::watch_ec)); +} + +RectToolbar::~RectToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } +} + +GtkWidget * +RectToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new RectToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +RectToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + void (SPRect::*setter)(gdouble)) +{ + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(Glib::ustring("/tools/shapes/rect/") + value_name, + Quantity::convert(adj->get_value(), unit, "px")); + } + + // quit if run by the attr_changed listener + if (_freeze || _tracker->isUpdating()) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + if (SP_IS_RECT(*i)) { + if (adj->get_value() != 0) { + (SP_RECT(*i)->*setter)(Quantity::convert(adj->get_value(), unit, "px")); + } else { + (*i)->removeAttribute(value_name); + } + modmade = true; + } + } + + sensitivize(); + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_RECT, + _("Change rectangle")); + } + + _freeze = false; +} + +void +RectToolbar::sensitivize() +{ + if (_rx_adj->get_value() == 0 && _ry_adj->get_value() == 0 && _single) { // only for a single selected rect (for now) + _not_rounded->set_sensitive(false); + } else { + _not_rounded->set_sensitive(true); + } +} + +void +RectToolbar::defaults() +{ + _rx_adj->set_value(0.0); + _ry_adj->set_value(0.0); + + sensitivize(); +} + +void +RectToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + static sigc::connection changed; + + // use of dynamic_cast<> seems wrong here -- we just need to check the current tool + + if (dynamic_cast<Inkscape::UI::Tools::RectTool *>(ec)) { + Inkscape::Selection *sel = desktop->getSelection(); + + changed = sel->connectChanged(sigc::mem_fun(*this, &RectToolbar::selection_changed)); + + // Synthesize an emission to trigger the update + selection_changed(sel); + } else { + if (changed) { + changed.disconnect(); + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + } + } +} + +/** + * \param selection should not be NULL. + */ +void +RectToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + SPItem *item = nullptr; + + if (_repr) { // remove old listener + _item = nullptr; + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + if (SP_IS_RECT(*i)) { + n_selected++; + item = *i; + repr = item->getRepr(); + } + } + + _single = false; + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + _width_item->set_sensitive(false); + _height_item->set_sensitive(false); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + _single = true; + _width_item->set_sensitive(true); + _height_item->set_sensitive(true); + + if (repr) { + _repr = repr; + _item = item; + Inkscape::GC::anchor(_repr); + _repr->addListener(&rect_tb_repr_events, this); + _repr->synthesizeEvents(&rect_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + sensitivize(); + } +} + +void RectToolbar::event_attr_changed(Inkscape::XML::Node * /*repr*/, gchar const * /*name*/, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer data) +{ + auto toolbar = reinterpret_cast<RectToolbar*>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + Unit const *unit = toolbar->_tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (toolbar->_item && SP_IS_RECT(toolbar->_item)) { + { + gdouble rx = SP_RECT(toolbar->_item)->getVisibleRx(); + toolbar->_rx_adj->set_value(Quantity::convert(rx, "px", unit)); + } + + { + gdouble ry = SP_RECT(toolbar->_item)->getVisibleRy(); + toolbar->_ry_adj->set_value(Quantity::convert(ry, "px", unit)); + } + + { + gdouble width = SP_RECT(toolbar->_item)->getVisibleWidth(); + toolbar->_width_adj->set_value(Quantity::convert(width, "px", unit)); + } + + { + gdouble height = SP_RECT(toolbar->_item)->getVisibleHeight(); + toolbar->_height_adj->set_value(Quantity::convert(height, "px", unit)); + } + } + + toolbar->sensitivize(); + toolbar->_freeze = false; +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/toolbar/rect-toolbar.h b/src/ui/toolbar/rect-toolbar.h new file mode 100644 index 0000000..58d4b2c --- /dev/null +++ b/src/ui/toolbar/rect-toolbar.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_RECT_TOOLBAR_H +#define SEEN_RECT_TOOLBAR_H + +/** + * @file + * Rect aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; +class SPItem; +class SPRect; + +namespace Gtk { +class Toolbutton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class RectToolbar : public Toolbar { +private: + UI::Widget::UnitTracker *_tracker; + + XML::Node *_repr; + SPItem *_item; + + UI::Widget::LabelToolItem *_mode_item; + UI::Widget::SpinButtonToolItem *_width_item; + UI::Widget::SpinButtonToolItem *_height_item; + UI::Widget::SpinButtonToolItem *_rx_item; + UI::Widget::SpinButtonToolItem *_ry_item; + Gtk::ToolButton *_not_rounded; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _height_adj; + Glib::RefPtr<Gtk::Adjustment> _rx_adj; + Glib::RefPtr<Gtk::Adjustment> _ry_adj; + + bool _freeze; + bool _single; + + void value_changed(Glib::RefPtr<Gtk::Adjustment>& adj, + gchar const *value_name, + void (SPRect::*setter)(gdouble)); + + void sensitivize(); + void defaults(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + +protected: + RectToolbar(SPDesktop *desktop); + ~RectToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); + +}; + +} +} +} + +#endif /* !SEEN_RECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp new file mode 100644 index 0000000..87682b1 --- /dev/null +++ b/src/ui/toolbar/select-toolbar.cpp @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Selector aux toolbar + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2003-2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "select-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/adjustment.h> +#include <gtkmm/separatortoolitem.h> + +#include <2geom/rect.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "display/sp-canvas.h" + +#include "object/sp-item-transform.h" +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/widget-sizes.h" + +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +SelectToolbar::SelectToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)), + _update(false), + _lock_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_stroke_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_corners_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_gradient_btn(Gtk::manage(new Gtk::ToggleToolButton())), + _transform_pattern_btn(Gtk::manage(new Gtk::ToggleToolButton())) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + add_toolbutton_for_verb(SP_VERB_EDIT_SELECT_ALL); + add_toolbutton_for_verb(SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS); + auto deselect_button = add_toolbutton_for_verb(SP_VERB_EDIT_DESELECT); + _context_items.push_back(deselect_button); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + auto object_rotate_90_ccw_button = add_toolbutton_for_verb(SP_VERB_OBJECT_ROTATE_90_CCW); + _context_items.push_back(object_rotate_90_ccw_button); + + auto object_rotate_90_cw_button = add_toolbutton_for_verb(SP_VERB_OBJECT_ROTATE_90_CW); + _context_items.push_back(object_rotate_90_cw_button); + + auto object_flip_horizontal_button = add_toolbutton_for_verb(SP_VERB_OBJECT_FLIP_HORIZONTAL); + _context_items.push_back(object_flip_horizontal_button); + + auto object_flip_vertical_button = add_toolbutton_for_verb(SP_VERB_OBJECT_FLIP_VERTICAL); + _context_items.push_back(object_flip_vertical_button); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + auto selection_to_back_button = add_toolbutton_for_verb(SP_VERB_SELECTION_TO_BACK); + _context_items.push_back(selection_to_back_button); + + auto selection_lower_button = add_toolbutton_for_verb(SP_VERB_SELECTION_LOWER); + _context_items.push_back(selection_lower_button); + + auto selection_raise_button = add_toolbutton_for_verb(SP_VERB_SELECTION_RAISE); + _context_items.push_back(selection_raise_button); + + auto selection_to_front_button = add_toolbutton_for_verb(SP_VERB_SELECTION_TO_FRONT); + _context_items.push_back(selection_to_front_button); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + _tracker->addUnit(unit_table.getUnit("%")); + _tracker->setActiveUnit( desktop->getNamedView()->display_units ); + + // x-value control + auto x_val = prefs->getDouble("/tools/select/X", 0.0); + _adj_x = Gtk::Adjustment::create(x_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_x->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_x)); + _tracker->addAdjustment(_adj_x->gobj()); + + auto x_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-x", + C_("Select toolbar", "X:"), + _adj_x, + SPIN_STEP, 3)); + x_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + x_btn->set_all_tooltip_text(C_("Select toolbar", "Horizontal coordinate of selection")); + _context_items.push_back(x_btn); + add(*x_btn); + + // y-value control + auto y_val = prefs->getDouble("/tools/select/Y", 0.0); + _adj_y = Gtk::Adjustment::create(y_val, -1e6, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_y->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_y)); + _tracker->addAdjustment(_adj_y->gobj()); + + auto y_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-y", + C_("Select toolbar", "Y:"), + _adj_y, + SPIN_STEP, 3)); + y_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + y_btn->set_all_tooltip_text(C_("Select toolbar", "Vertical coordinate of selection")); + _context_items.push_back(y_btn); + add(*y_btn); + + // width-value control + auto w_val = prefs->getDouble("/tools/select/width", 0.0); + _adj_w = Gtk::Adjustment::create(w_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_w->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_w)); + _tracker->addAdjustment(_adj_w->gobj()); + + auto w_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-width", + C_("Select toolbar", "W:"), + _adj_w, + SPIN_STEP, 3)); + w_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + w_btn->set_all_tooltip_text(C_("Select toolbar", "Width of selection")); + _context_items.push_back(w_btn); + add(*w_btn); + + // lock toggle + _lock_btn->set_label(_("Lock width and height")); + _lock_btn->set_tooltip_text(_("When locked, change both width and height by the same proportion")); + _lock_btn->set_icon_name(INKSCAPE_ICON("object-unlocked")); + _lock_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_lock)); + set_data("lock", _lock_btn->gobj()); + add(*_lock_btn); + + // height-value control + auto h_val = prefs->getDouble("/tools/select/height", 0.0); + _adj_h = Gtk::Adjustment::create(h_val, 0.0, 1e6, SPIN_STEP, SPIN_PAGE_STEP); + _adj_h->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SelectToolbar::any_value_changed), _adj_h)); + _tracker->addAdjustment(_adj_h->gobj()); + + auto h_btn = Gtk::manage(new UI::Widget::SpinButtonToolItem("select-height", + C_("Select toolbar", "H:"), + _adj_h, + SPIN_STEP, 3)); + h_btn->set_focus_widget(Glib::wrap(GTK_WIDGET(_desktop->canvas))); + h_btn->set_all_tooltip_text(C_("Select toolbar", "Height of selection")); + _context_items.push_back(h_btn); + add(*h_btn); + + // units menu + auto unit_menu = _tracker->create_tool_item(_("Units"), ("") ); + add(*unit_menu); + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + _transform_stroke_btn->set_label(_("Scale stroke width")); + _transform_stroke_btn->set_tooltip_text(_("When scaling objects, scale the stroke width by the same proportion")); + _transform_stroke_btn->set_icon_name(INKSCAPE_ICON("transform-affect-stroke")); + _transform_stroke_btn->set_active(prefs->getBool("/options/transform/stroke", true)); + _transform_stroke_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_stroke)); + add(*_transform_stroke_btn); + + _transform_corners_btn->set_label(_("Scale rounded corners")); + _transform_corners_btn->set_tooltip_text(_("When scaling rectangles, scale the radii of rounded corners")); + _transform_corners_btn->set_icon_name(INKSCAPE_ICON("transform-affect-rounded-corners")); + _transform_corners_btn->set_active(prefs->getBool("/options/transform/rectcorners", true)); + _transform_corners_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_corners)); + add(*_transform_corners_btn); + + _transform_gradient_btn->set_label(_("Move gradients")); + _transform_gradient_btn->set_tooltip_text(_("Move gradients (in fill or stroke) along with the objects")); + _transform_gradient_btn->set_icon_name(INKSCAPE_ICON("transform-affect-gradient")); + _transform_gradient_btn->set_active(prefs->getBool("/options/transform/gradient", true)); + _transform_gradient_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_gradient)); + add(*_transform_gradient_btn); + + _transform_pattern_btn->set_label(_("Move patterns")); + _transform_pattern_btn->set_tooltip_text(_("Move patterns (in fill or stroke) along with the objects")); + _transform_pattern_btn->set_icon_name(INKSCAPE_ICON("transform-affect-pattern")); + _transform_pattern_btn->set_active(prefs->getBool("/options/transform/pattern", true)); + _transform_pattern_btn->signal_toggled().connect(sigc::mem_fun(*this, &SelectToolbar::toggle_pattern)); + add(*_transform_pattern_btn); + + // Force update when selection changes. + INKSCAPE.signal_selection_modified.connect(sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_modified)); + INKSCAPE.signal_selection_changed.connect (sigc::mem_fun(*this, &SelectToolbar::on_inkscape_selection_changed)); + + // Update now. + layout_widget_update(SP_ACTIVE_DESKTOP ? SP_ACTIVE_DESKTOP->getSelection() : nullptr); + + for (auto item : _context_items) { + if ( item->is_sensitive() ) { + item->set_sensitive(false); + } + } + + show_all(); +} + +GtkWidget * +SelectToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SelectToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SelectToolbar::any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj) +{ + if (_update) { + return; + } + + if ( !_tracker || _tracker->isUpdating() ) { + /* + * When only units are being changed, don't treat changes + * to adjuster values as object changes. + */ + return; + } + _update = true; + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + document->ensureUpToDate (); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Geom::OptRect bbox_vis = selection->visualBounds(); + Geom::OptRect bbox_geom = selection->geometricBounds(); + + int prefs_bbox = prefs->getInt("/tools/bounding_box"); + SPItem::BBoxType bbox_type = (prefs_bbox == 0)? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + Geom::OptRect bbox_user = selection->bounds(bbox_type); + + if ( !bbox_user ) { + _update = false; + return; + } + + gdouble x0 = 0; + gdouble y0 = 0; + gdouble x1 = 0; + gdouble y1 = 0; + gdouble xrel = 0; + gdouble yrel = 0; + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + x0 = Quantity::convert(_adj_x->get_value(), unit, "px"); + y0 = Quantity::convert(_adj_y->get_value(), unit, "px"); + x1 = x0 + Quantity::convert(_adj_w->get_value(), unit, "px"); + xrel = Quantity::convert(_adj_w->get_value(), unit, "px") / bbox_user->dimensions()[Geom::X]; + y1 = y0 + Quantity::convert(_adj_h->get_value(), unit, "px");; + yrel = Quantity::convert(_adj_h->get_value(), unit, "px") / bbox_user->dimensions()[Geom::Y]; + } else { + double const x0_propn = _adj_x->get_value() / 100 / unit->factor; + x0 = bbox_user->min()[Geom::X] * x0_propn; + double const y0_propn = _adj_y->get_value() / 100 / unit->factor; + y0 = y0_propn * bbox_user->min()[Geom::Y]; + xrel = _adj_w->get_value() / (100 / unit->factor); + x1 = x0 + xrel * bbox_user->dimensions()[Geom::X]; + yrel = _adj_h->get_value() / (100 / unit->factor); + y1 = y0 + yrel * bbox_user->dimensions()[Geom::Y]; + } + + // Keep proportions if lock is on + if ( _lock_btn->get_active() ) { + if (adj == _adj_h) { + x1 = x0 + yrel * bbox_user->dimensions()[Geom::X]; + } else if (adj == _adj_w) { + y1 = y0 + xrel * bbox_user->dimensions()[Geom::Y]; + } + } + + // scales and moves, in px + double mh = fabs(x0 - bbox_user->min()[Geom::X]); + double sh = fabs(x1 - bbox_user->max()[Geom::X]); + double mv = fabs(y0 - bbox_user->min()[Geom::Y]); + double sv = fabs(y1 - bbox_user->max()[Geom::Y]); + + // unless the unit is %, convert the scales and moves to the unit + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + mh = Quantity::convert(mh, "px", unit); + sh = Quantity::convert(sh, "px", unit); + mv = Quantity::convert(mv, "px", unit); + sv = Quantity::convert(sv, "px", unit); + } + + // do the action only if one of the scales/moves is greater than half the last significant + // digit in the spinbox (currently spinboxes have 3 fractional digits, so that makes 0.0005). If + // the value was changed by the user, the difference will be at least that much; otherwise it's + // just rounding difference between the spinbox value and actual value, so no action is + // performed + char const * const actionkey = ( mh > 5e-4 ? "selector:toolbar:move:horizontal" : + sh > 5e-4 ? "selector:toolbar:scale:horizontal" : + mv > 5e-4 ? "selector:toolbar:move:vertical" : + sv > 5e-4 ? "selector:toolbar:scale:vertical" : nullptr ); + + if (actionkey != nullptr) { + + // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed + desktop->getCanvas()->forceFullRedrawAfterInterruptions(0); + + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool preserve = prefs->getBool("/options/preservetransform/value", false); + + Geom::Affine scaler; + if (bbox_type == SPItem::VISUAL_BBOX) { + scaler = get_scale_transform_for_variable_stroke (*bbox_vis, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1); + } else { + // 1) We could have use the newer get_scale_transform_for_variable_stroke() here, but to avoid regressions + // we'll just use the old get_scale_transform_for_uniform_stroke() for now. + // 2) get_scale_transform_for_uniform_stroke() is intended for visual bounding boxes, not geometrical ones! + // we'll trick it into using a geometric bounding box though, by setting the stroke width to zero + scaler = get_scale_transform_for_uniform_stroke (*bbox_geom, 0, 0, false, false, x0, y0, x1, y1); + } + + selection->applyAffine(scaler); + DocumentUndo::maybeDone(document, actionkey, SP_VERB_CONTEXT_SELECT, + _("Transform by toolbar")); + + // resume interruptibility + desktop->getCanvas()->endForcedFullRedraws(); + } + + _update = false; +} + +void +SelectToolbar::layout_widget_update(Inkscape::Selection *sel) +{ + if (_update) { + return; + } + + _update = true; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + using Geom::X; + using Geom::Y; + if ( sel && !sel->isEmpty() ) { + int prefs_bbox = prefs->getInt("/tools/bounding_box", 0); + SPItem::BBoxType bbox_type = (prefs_bbox ==0)? + SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + Geom::OptRect const bbox(sel->bounds(bbox_type)); + if ( bbox ) { + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + + struct { char const *key; double val; } const keyval[] = { + { "X", bbox->min()[X] }, + { "Y", bbox->min()[Y] }, + { "width", bbox->dimensions()[X] }, + { "height", bbox->dimensions()[Y] } + }; + + if (unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + double const val = unit->factor * 100; + _adj_x->set_value(val); + _adj_y->set_value(val); + _adj_w->set_value(val); + _adj_h->set_value(val); + _tracker->setFullVal( _adj_x->gobj(), keyval[0].val ); + _tracker->setFullVal( _adj_y->gobj(), keyval[1].val ); + _tracker->setFullVal( _adj_w->gobj(), keyval[2].val ); + _tracker->setFullVal( _adj_h->gobj(), keyval[3].val ); + } else { + _adj_x->set_value(Quantity::convert(keyval[0].val, "px", unit)); + _adj_y->set_value(Quantity::convert(keyval[1].val, "px", unit)); + _adj_w->set_value(Quantity::convert(keyval[2].val, "px", unit)); + _adj_h->set_value(Quantity::convert(keyval[3].val, "px", unit)); + } + } + } + + _update = false; +} + +void +SelectToolbar::on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags) +{ + if ((_desktop->getSelection() == selection) // only respond to changes in our desktop + && (flags & (SP_OBJECT_MODIFIED_FLAG | + SP_OBJECT_PARENT_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG ))) + { + layout_widget_update(selection); + } +} + +void +SelectToolbar::on_inkscape_selection_changed(Inkscape::Selection *selection) +{ + if (_desktop->getSelection() == selection) { // only respond to changes in our desktop + bool setActive = (selection && !selection->isEmpty()); + + for (auto item : _context_items) { + if ( setActive != item->get_sensitive() ) { + item->set_sensitive(setActive); + } + } + + layout_widget_update(selection); + } +} + +void +SelectToolbar::toggle_lock() { + if ( _lock_btn->get_active() ) { + _lock_btn->set_icon_name(INKSCAPE_ICON("object-locked")); + } else { + _lock_btn->set_icon_name(INKSCAPE_ICON("object-unlocked")); + } +} + +void +SelectToolbar::toggle_stroke() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_stroke_btn->get_active(); + prefs->setBool("/options/transform/stroke", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>scaled</b> when objects are scaled.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>stroke width</b> is <b>not scaled</b> when objects are scaled.")); + } +} + +void +SelectToolbar::toggle_corners() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_corners_btn->get_active(); + prefs->setBool("/options/transform/rectcorners", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>scaled</b> when rectangles are scaled.")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>rounded rectangle corners</b> are <b>not scaled</b> when rectangles are scaled.")); + } +} + +void +SelectToolbar::toggle_gradient() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_gradient_btn->get_active(); + prefs->setBool("/options/transform/gradient", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed).")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>gradients</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed).")); + } +} + +void +SelectToolbar::toggle_pattern() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool active = _transform_pattern_btn->get_active(); + prefs->setInt("/options/transform/pattern", active); + if ( active ) { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> are <b>transformed</b> along with their objects when those are transformed (moved, scaled, rotated, or skewed).")); + } else { + _desktop->messageStack()->flash(Inkscape::INFORMATION_MESSAGE, _("Now <b>patterns</b> remain <b>fixed</b> when objects are transformed (moved, scaled, rotated, or skewed).")); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/toolbar/select-toolbar.h b/src/ui/toolbar/select-toolbar.h new file mode 100644 index 0000000..79c3310 --- /dev/null +++ b/src/ui/toolbar/select-toolbar.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SELECT_TOOLBAR_H +#define SEEN_SELECT_TOOLBAR_H + +/** \file + * Selector aux toolbar + */ +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; + +namespace Inkscape { +class Selection; + +namespace UI { + +namespace Widget { +class UnitTracker; +} + +namespace Toolbar { + +class SelectToolbar : public Toolbar { +private: + std::unique_ptr<UI::Widget::UnitTracker> _tracker; + + Glib::RefPtr<Gtk::Adjustment> _adj_x; + Glib::RefPtr<Gtk::Adjustment> _adj_y; + Glib::RefPtr<Gtk::Adjustment> _adj_w; + Glib::RefPtr<Gtk::Adjustment> _adj_h; + Gtk::ToggleToolButton *_lock_btn; + Gtk::ToggleToolButton *_transform_stroke_btn; + Gtk::ToggleToolButton *_transform_corners_btn; + Gtk::ToggleToolButton *_transform_gradient_btn; + Gtk::ToggleToolButton *_transform_pattern_btn; + + std::vector<Gtk::ToolItem *> _context_items; + + bool _update; + + void any_value_changed(Glib::RefPtr<Gtk::Adjustment>& adj); + void layout_widget_update(Inkscape::Selection *sel); + void on_inkscape_selection_modified(Inkscape::Selection *selection, guint flags); + void on_inkscape_selection_changed(Inkscape::Selection *selection); + void toggle_lock(); + void toggle_stroke(); + void toggle_corners(); + void toggle_gradient(); + void toggle_pattern(); + +protected: + SelectToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; + +} +} +} +#endif /* !SEEN_SELECT_TOOLBAR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/snap-toolbar.cpp b/src/ui/toolbar/snap-toolbar.cpp new file mode 100644 index 0000000..483faf3 --- /dev/null +++ b/src/ui/toolbar/snap-toolbar.cpp @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Inkscape Snap toolbar + * + * @authors Inkscape Authors + * Copyright (C) 1999-2019 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "snap-toolbar.h" + +#include <glibmm/i18n.h> + +#include "attributes.h" +#include "desktop.h" +#include "verbs.h" + +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +SnapToolbar::SnapToolbar(SPDesktop *desktop) + : Toolbar(desktop), + _freeze(false) +{ + // Global snapping control + { + auto snap_global_verb = Inkscape::Verb::get(SP_VERB_TOGGLE_SNAPPING); + _snap_global_item = add_toggle_button(snap_global_verb->get_name(), + snap_global_verb->get_tip()); + _snap_global_item->set_icon_name(INKSCAPE_ICON("snap")); + _snap_global_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_GLOBAL)); + } + + add_separator(); + + // Snapping to bounding boxes + { + _snap_from_bbox_corner_item = add_toggle_button(_("Bounding box"), + _("Snap bounding boxes")); + _snap_from_bbox_corner_item->set_icon_name(INKSCAPE_ICON("snap")); + _snap_from_bbox_corner_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_BBOX)); + } + + { + _snap_to_bbox_path_item = add_toggle_button(_("Bounding box edges"), + _("Snap to edges of a bounding box")); + _snap_to_bbox_path_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-edges")); + _snap_to_bbox_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE)); + } + + { + _snap_to_bbox_node_item = add_toggle_button(_("Bounding box corners"), + _("Snap bounding box corners")); + _snap_to_bbox_node_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-corners")); + _snap_to_bbox_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_BBOX_CORNER)); + } + + { + _snap_to_from_bbox_edge_midpoints_item = add_toggle_button(_("BBox Edge Midpoints"), + _("Snap midpoints of bounding box edges")); + _snap_to_from_bbox_edge_midpoints_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-midpoints")); + _snap_to_from_bbox_edge_midpoints_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE_MIDPOINT)); + } + + { + _snap_to_from_bbox_edge_centers_item = add_toggle_button(_("BBox Centers"), + _("Snapping centers of bounding boxes")); + _snap_to_from_bbox_edge_centers_item->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center")); + _snap_to_from_bbox_edge_centers_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_BBOX_MIDPOINT)); + } + + add_separator(); + + // Snapping to nodes, paths & handles + { + _snap_from_node_item = add_toggle_button(_("Nodes"), + _("Snap nodes, paths, and handles")); + _snap_from_node_item->set_icon_name(INKSCAPE_ICON("snap")); + _snap_from_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_NODE)); + } + + { + _snap_to_item_path_item = add_toggle_button(_("Paths"), + _("Snap to paths")); + _snap_to_item_path_item->set_icon_name(INKSCAPE_ICON("snap-nodes-path")); + _snap_to_item_path_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_PATH)); + } + + { + _snap_to_path_intersections_item = add_toggle_button(_("Path intersections"), + _("Snap to path intersections")); + _snap_to_path_intersections_item->set_icon_name(INKSCAPE_ICON("snap-nodes-intersection")); + _snap_to_path_intersections_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_PATH_INTERSECTION)); + } + + { + _snap_to_item_node_item = add_toggle_button(_("To nodes"), + _("Snap to cusp nodes, incl. rectangle corners")); + _snap_to_item_node_item->set_icon_name(INKSCAPE_ICON("snap-nodes-cusp")); + _snap_to_item_node_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_NODE_CUSP)); + } + + { + _snap_to_smooth_nodes_item = add_toggle_button(_("Smooth nodes"), + _("Snap smooth nodes, incl. quadrant points of ellipses")); + _snap_to_smooth_nodes_item->set_icon_name(INKSCAPE_ICON("snap-nodes-smooth")); + _snap_to_smooth_nodes_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_NODE_SMOOTH)); + } + + { + _snap_to_from_line_midpoints_item = add_toggle_button(_("Line Midpoints"), + _("Snap midpoints of line segments")); + _snap_to_from_line_midpoints_item->set_icon_name(INKSCAPE_ICON("snap-nodes-midpoint")); + _snap_to_from_line_midpoints_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_LINE_MIDPOINT)); + } + + add_separator(); + + { + _snap_from_others_item = add_toggle_button(_("Others"), + _("Snap other points (centers, guide origins, gradient handles, etc.)")); + _snap_from_others_item->set_icon_name(INKSCAPE_ICON("snap")); + _snap_from_others_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_OTHERS)); + } + + { + _snap_to_from_object_centers_item = add_toggle_button(_("Object Centers"), + _("Snap centers of objects")); + _snap_to_from_object_centers_item->set_icon_name(INKSCAPE_ICON("snap-nodes-center")); + _snap_to_from_object_centers_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_OBJECT_MIDPOINT)); + } + + { + _snap_to_from_rotation_center_item = add_toggle_button(_("Rotation Centers"), + _("Snap an item's rotation center")); + _snap_to_from_rotation_center_item->set_icon_name(INKSCAPE_ICON("snap-nodes-rotation-center")); + _snap_to_from_rotation_center_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_ROTATION_CENTER)); + } + + { + _snap_to_from_text_baseline_item = add_toggle_button(_("Text baseline"), + _("Snap text anchors and baselines")); + _snap_to_from_text_baseline_item->set_icon_name(INKSCAPE_ICON("snap-text-baseline")); + _snap_to_from_text_baseline_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_TEXT_BASELINE)); + } + + add_separator(); + + { + _snap_to_page_border_item = add_toggle_button(_("Page border"), + _("Snap to the page border")); + _snap_to_page_border_item->set_icon_name(INKSCAPE_ICON("snap-page")); + _snap_to_page_border_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_PAGE_BORDER)); + } + + { + _snap_to_grids_item = add_toggle_button(_("Grids"), + _("Snap to grids")); + _snap_to_grids_item->set_icon_name(INKSCAPE_ICON("grid-rectangular")); + _snap_to_grids_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_GRID)); + } + + { + _snap_to_guides_item = add_toggle_button(_("Guides"), + _("Snap guides")); + _snap_to_guides_item->set_icon_name(INKSCAPE_ICON("guides")); + _snap_to_guides_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SnapToolbar::on_snap_toggled), + SP_ATTR_INKSCAPE_SNAP_GUIDE)); + } + + show_all(); +} + +GtkWidget * +SnapToolbar::create(SPDesktop *desktop) +{ + auto tb = Gtk::manage(new SnapToolbar(desktop)); + return GTK_WIDGET(tb->gobj()); +} + +void +SnapToolbar::update(SnapToolbar *tb) +{ + auto nv = tb->_desktop->getNamedView(); + + if (nv == nullptr) { + g_warning("Namedview cannot be retrieved (in updateSnapToolbox)!"); + return; + } + + // The ..._set_active calls below will toggle the buttons, but this shouldn't lead to + // changes in our document because we're only updating the UI; + // Setting the "freeze" parameter to true will block the code in toggle_snap_callback() + tb->_freeze = true; + + bool const c1 = nv->snap_manager.snapprefs.getSnapEnabledGlobally(); + tb->_snap_global_item->set_active(c1); + + bool const c2 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_BBOX_CATEGORY); + tb->_snap_from_bbox_corner_item->set_active(c2); + tb->_snap_from_bbox_corner_item->set_sensitive(c1); + + tb->_snap_to_bbox_path_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_EDGE)); + tb->_snap_to_bbox_path_item->set_sensitive(c1 && c2); + + tb->_snap_to_bbox_node_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_CORNER)); + tb->_snap_to_bbox_node_item->set_sensitive(c1 && c2); + + tb->_snap_to_from_bbox_edge_midpoints_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_EDGE_MIDPOINT)); + tb->_snap_to_from_bbox_edge_midpoints_item->set_sensitive(c1 && c2); + tb->_snap_to_from_bbox_edge_centers_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(SNAPTARGET_BBOX_MIDPOINT)); + tb->_snap_to_from_bbox_edge_centers_item->set_sensitive(c1 && c2); + + bool const c3 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_NODE_CATEGORY); + tb->_snap_from_node_item->set_active(c3); + tb->_snap_from_node_item->set_sensitive(c1); + + tb->_snap_to_item_path_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH)); + tb->_snap_to_item_path_item->set_sensitive(c1 && c3); + tb->_snap_to_path_intersections_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_INTERSECTION)); + tb->_snap_to_path_intersections_item->set_sensitive(c1 && c3); + tb->_snap_to_item_node_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_CUSP)); + tb->_snap_to_item_node_item->set_sensitive(c1 && c3); + tb->_snap_to_smooth_nodes_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_SMOOTH)); + tb->_snap_to_smooth_nodes_item->set_sensitive(c1 && c3); + tb->_snap_to_from_line_midpoints_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_LINE_MIDPOINT)); + tb->_snap_to_from_line_midpoints_item->set_sensitive(c1 && c3); + + bool const c5 = nv->snap_manager.snapprefs.isTargetSnappable(SNAPTARGET_OTHERS_CATEGORY); + tb->_snap_from_others_item->set_active(c5); + tb->_snap_from_others_item->set_sensitive(c1); + tb->_snap_to_from_object_centers_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)); + tb->_snap_to_from_object_centers_item->set_sensitive(c1 && c5); + tb->_snap_to_from_rotation_center_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_ROTATION_CENTER)); + tb->_snap_to_from_rotation_center_item->set_sensitive(c1 && c5); + tb->_snap_to_from_text_baseline_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_TEXT_BASELINE)); + tb->_snap_to_from_text_baseline_item->set_sensitive(c1 && c5); + tb->_snap_to_page_border_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PAGE_BORDER)); + tb->_snap_to_page_border_item->set_sensitive(c1); + tb->_snap_to_grids_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GRID)); + tb->_snap_to_grids_item->set_sensitive(c1); + tb->_snap_to_guides_item->set_active(nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GUIDE)); + tb->_snap_to_guides_item->set_sensitive(c1); + + tb->_freeze = false; +} + +void +SnapToolbar::on_snap_toggled(SPAttributeEnum attr) +{ + if(_freeze) return; + + auto dt = _desktop; + auto nv = dt->getNamedView(); + + if(!nv) { + g_warning("No namedview specified in toggle-snap callback"); + return; + } + + auto doc = nv->document; + auto repr = nv->getRepr(); + + if(!repr) { + g_warning("This namedview doesn't have an XML representation attached!"); + return; + } + + DocumentUndo::ScopedInsensitive _no_undo(doc); + + bool v = false; + + switch (attr) { + case SP_ATTR_INKSCAPE_SNAP_GLOBAL: + dt->toggleSnapGlobal(); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_BBOX_CATEGORY); + sp_repr_set_boolean(repr, "inkscape:snap-bbox", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_EDGE); + sp_repr_set_boolean(repr, "inkscape:bbox-paths", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_CORNER: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_CORNER); + sp_repr_set_boolean(repr, "inkscape:bbox-nodes", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_NODE_CATEGORY); + sp_repr_set_boolean(repr, "inkscape:snap-nodes", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH); + sp_repr_set_boolean(repr, "inkscape:object-paths", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_CLIP: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_CLIP); + sp_repr_set_boolean(repr, "inkscape:snap-path-clip", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_MASK: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_MASK); + sp_repr_set_boolean(repr, "inkscape:snap-path-mask", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE_CUSP: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_CUSP); + sp_repr_set_boolean(repr, "inkscape:object-nodes", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_NODE_SMOOTH: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_NODE_SMOOTH); + sp_repr_set_boolean(repr, "inkscape:snap-smooth-nodes", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_PATH_INTERSECTION: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PATH_INTERSECTION); + sp_repr_set_boolean(repr, "inkscape:snap-intersection-paths", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_OTHERS: + v = nv->snap_manager.snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_OTHERS_CATEGORY); + sp_repr_set_boolean(repr, "inkscape:snap-others", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_ROTATION_CENTER: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_ROTATION_CENTER); + sp_repr_set_boolean(repr, "inkscape:snap-center", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_GRID: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GRID); + sp_repr_set_boolean(repr, "inkscape:snap-grids", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_GUIDE: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_GUIDE); + sp_repr_set_boolean(repr, "inkscape:snap-to-guides", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_PAGE_BORDER: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_PAGE_BORDER); + sp_repr_set_boolean(repr, "inkscape:snap-page", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_LINE_MIDPOINT: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_LINE_MIDPOINT); + sp_repr_set_boolean(repr, "inkscape:snap-midpoints", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_OBJECT_MIDPOINT: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_OBJECT_MIDPOINT); + sp_repr_set_boolean(repr, "inkscape:snap-object-midpoints", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_TEXT_BASELINE: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_TEXT_BASELINE); + sp_repr_set_boolean(repr, "inkscape:snap-text-baseline", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_EDGE_MIDPOINT: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_EDGE_MIDPOINT); + sp_repr_set_boolean(repr, "inkscape:snap-bbox-edge-midpoints", !v); + break; + case SP_ATTR_INKSCAPE_SNAP_BBOX_MIDPOINT: + v = nv->snap_manager.snapprefs.isSnapButtonEnabled(Inkscape::SNAPTARGET_BBOX_MIDPOINT); + sp_repr_set_boolean(repr, "inkscape:snap-bbox-midpoints", !v); + break; + default: + g_warning("toggle_snap_callback has been called with an ID for which no action has been defined"); + break; + } + + doc->setModifiedSinceSave(); +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/toolbar/snap-toolbar.h b/src/ui/toolbar/snap-toolbar.h new file mode 100644 index 0000000..7f8528c --- /dev/null +++ b/src/ui/toolbar/snap-toolbar.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SNAP_TOOLBAR_H +#define SEEN_SNAP_TOOLBAR_H + +/** + * @file + * Snapping toolbar + * + * @authors Inkscape authors, 2004-2019 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +enum SPAttributeEnum : unsigned; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +class SnapToolbar : public Toolbar { +private: + bool _freeze; + + // Toolbar widgets + Gtk::ToggleToolButton *_snap_global_item; + Gtk::ToggleToolButton *_snap_from_bbox_corner_item; + Gtk::ToggleToolButton *_snap_to_bbox_path_item; + Gtk::ToggleToolButton *_snap_to_bbox_node_item; + Gtk::ToggleToolButton *_snap_to_from_bbox_edge_midpoints_item; + Gtk::ToggleToolButton *_snap_to_from_bbox_edge_centers_item; + Gtk::ToggleToolButton *_snap_from_node_item; + Gtk::ToggleToolButton *_snap_to_item_path_item; + Gtk::ToggleToolButton *_snap_to_path_intersections_item; + Gtk::ToggleToolButton *_snap_to_item_node_item; + Gtk::ToggleToolButton *_snap_to_smooth_nodes_item; + Gtk::ToggleToolButton *_snap_to_from_line_midpoints_item; + Gtk::ToggleToolButton *_snap_from_others_item; + Gtk::ToggleToolButton *_snap_to_from_object_centers_item; + Gtk::ToggleToolButton *_snap_to_from_rotation_center_item; + Gtk::ToggleToolButton *_snap_to_from_text_baseline_item; + Gtk::ToggleToolButton *_snap_to_page_border_item; + Gtk::ToggleToolButton *_snap_to_grids_item; + Gtk::ToggleToolButton *_snap_to_guides_item; + + void on_snap_toggled(SPAttributeEnum attr); + +protected: + SnapToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + static void update(SnapToolbar *tb); +}; + +} +} +} +#endif /* !SEEN_SNAP_TOOLBAR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/spiral-toolbar.cpp b/src/ui/toolbar/spiral-toolbar.cpp new file mode 100644 index 0000000..35981c1 --- /dev/null +++ b/src/ui/toolbar/spiral-toolbar.cpp @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spiral aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spiral-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toolbutton.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-spiral.h" + +#include "ui/icon-names.h" +#include "ui/uxmanager.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "widgets/spinbutton-events.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector spiral_tb_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::SpiralToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +SpiralToolbar::SpiralToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _freeze(false), + _repr(nullptr) +{ + auto prefs = Inkscape::Preferences::get(); + + { + _mode_item = Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>"))); + _mode_item->set_use_markup(true); + add(*_mode_item); + } + + /* Revolution */ + { + std::vector<Glib::ustring> labels = {_("just a curve"), "", _("one full revolution"), "", "", "", "", "", "", ""}; + std::vector<double> values = { 0.01, 0.5, 1, 2, 3, 5, 10, 20, 50, 100}; + auto revolution_val = prefs->getDouble("/tools/shapes/spiral/revolution", 3.0); + _revolution_adj = Gtk::Adjustment::create(revolution_val, 0.01, 1024.0, 0.1, 1.0); + _revolution_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-revolutions", _("Turns:"), _revolution_adj, 1, 2)); + _revolution_item->set_tooltip_text(_("Number of revolutions")); + _revolution_item->set_custom_numeric_menu_data(values, labels); + _revolution_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _revolution_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _revolution_adj, "revolution")); + add(*_revolution_item); + } + + /* Expansion */ + { + std::vector<Glib::ustring> labels = {_("circle"), _("edge is much denser"), _("edge is denser"), _("even"), _("center is denser"), _("center is much denser"), ""}; + std::vector<double> values = { 0, 0.1, 0.5, 1, 1.5, 5, 20}; + auto expansion_val = prefs->getDouble("/tools/shapes/spiral/expansion", 1.0); + _expansion_adj = Gtk::Adjustment::create(expansion_val, 0.0, 1000.0, 0.01, 1.0); + + _expansion_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-expansion", _("Divergence:"), _expansion_adj)); + _expansion_item->set_tooltip_text(_("How much denser/sparser are outer revolutions; 1 = uniform")); + _expansion_item->set_custom_numeric_menu_data(values, labels); + _expansion_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _expansion_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _expansion_adj, "expansion")); + add(*_expansion_item); + } + + /* T0 */ + { + std::vector<Glib::ustring> labels = {_("starts from center"), _("starts mid-way"), _("starts near edge")}; + std::vector<double> values = { 0, 0.5, 0.9}; + auto t0_val = prefs->getDouble("/tools/shapes/spiral/t0", 0.0); + _t0_adj = Gtk::Adjustment::create(t0_val, 0.0, 0.999, 0.01, 1.0); + _t0_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spiral-t0", _("Inner radius:"), _t0_adj)); + _t0_item->set_tooltip_text(_("Radius of the innermost revolution (relative to the spiral size)")); + _t0_item->set_custom_numeric_menu_data(values, labels); + _t0_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _t0_adj->signal_value_changed().connect(sigc::bind(sigc::mem_fun(*this, &SpiralToolbar::value_changed), + _t0_adj, "t0")); + add(*_t0_item); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear")); + _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &SpiralToolbar::defaults)); + add(*_reset_item); + } + + _connection.reset(new sigc::connection( + desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &SpiralToolbar::selection_changed)))); + + show_all(); +} + +SpiralToolbar::~SpiralToolbar() +{ + if(_repr) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + if(_connection) { + _connection->disconnect(); + } +} + +GtkWidget * +SpiralToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SpiralToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SpiralToolbar::value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &value_name) +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/spiral/" + value_name, + adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + gchar* namespaced_name = g_strconcat("sodipodi:", value_name.data(), NULL); + + bool modmade = false; + auto itemlist= _desktop->getSelection()->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i){ + SPItem *item = *i; + if (SP_IS_SPIRAL(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + sp_repr_set_svg_double( repr, namespaced_name, + adj->get_value() ); + item->updateRepr(); + modmade = true; + } + } + + g_free(namespaced_name); + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_SPIRAL, + _("Change spiral")); + } + + _freeze = false; +} + +void +SpiralToolbar::defaults() +{ + // fixme: make settable + gdouble rev = 3; + gdouble exp = 1.0; + gdouble t0 = 0.0; + + _revolution_adj->set_value(rev); + _expansion_adj->set_value(exp); + _t0_adj->set_value(t0); + + if(_desktop->canvas) gtk_widget_grab_focus(GTK_WIDGET(_desktop->canvas)); +} + +void +SpiralToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if ( _repr ) { + _repr->removeListenerByData(this); + GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end(); ++i){ + SPItem *item = *i; + if (SP_IS_SPIRAL(item)) { + n_selected++; + repr = item->getRepr(); + } + } + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&spiral_tb_repr_events, this); + _repr->synthesizeEvents(&spiral_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + _mode_item->set_markup(_("<b>Change:</b>")); + } +} + +void +SpiralToolbar::event_attr_changed(Inkscape::XML::Node *repr, + gchar const * /*name*/, + gchar const * /*old_value*/, + gchar const * /*new_value*/, + bool /*is_interactive*/, + gpointer data) +{ + auto toolbar = reinterpret_cast<SpiralToolbar *>(data); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + double revolution = 3.0; + sp_repr_get_double(repr, "sodipodi:revolution", &revolution); + toolbar->_revolution_adj->set_value(revolution); + + double expansion = 1.0; + sp_repr_get_double(repr, "sodipodi:expansion", &expansion); + toolbar->_expansion_adj->set_value(expansion); + + double t0 = 0.0; + sp_repr_get_double(repr, "sodipodi:t0", &t0); + toolbar->_t0_adj->set_value(t0); + + toolbar->_freeze = false; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/spiral-toolbar.h b/src/ui/toolbar/spiral-toolbar.h new file mode 100644 index 0000000..9c27eb5 --- /dev/null +++ b/src/ui/toolbar/spiral-toolbar.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPIRAL_TOOLBAR_H +#define SEEN_SPIRAL_TOOLBAR_H + +/** + * @file + * Spiral aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class SpiralToolbar : public Toolbar { +private: + UI::Widget::LabelToolItem *_mode_item; + + UI::Widget::SpinButtonToolItem *_revolution_item; + UI::Widget::SpinButtonToolItem *_expansion_item; + UI::Widget::SpinButtonToolItem *_t0_item; + + Gtk::ToolButton *_reset_item; + + Glib::RefPtr<Gtk::Adjustment> _revolution_adj; + Glib::RefPtr<Gtk::Adjustment> _expansion_adj; + Glib::RefPtr<Gtk::Adjustment> _t0_adj; + + bool _freeze; + + XML::Node *_repr; + + void value_changed(Glib::RefPtr<Gtk::Adjustment> &adj, + Glib::ustring const &value_name); + void defaults(); + void selection_changed(Inkscape::Selection *selection); + + std::unique_ptr<sigc::connection> _connection; + +protected: + SpiralToolbar(SPDesktop *desktop); + ~SpiralToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer data); +}; +} +} +} + +#endif /* !SEEN_SPIRAL_TOOLBAR_H */ diff --git a/src/ui/toolbar/spray-toolbar.cpp b/src/ui/toolbar/spray-toolbar.cpp new file mode 100644 index 0000000..9e7c4e8 --- /dev/null +++ b/src/ui/toolbar/spray-toolbar.cpp @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Spray aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spray-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "inkscape.h" + +#include "ui/icon-names.h" +#include "ui/simple-pref-pusher.h" + +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/panel-dialog.h" + +#include "ui/widget/spin-button-tool-item.h" + +// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object +// with the mode: spray object in single path) +// Please enable again when working on 1.0 +#define ENABLE_SPRAY_MODE_SINGLE_PATH + +Inkscape::UI::Dialog::CloneTiler *get_clone_tiler_panel(SPDesktop *desktop) +{ + if (Inkscape::UI::Dialog::PanelDialogBase *panel_dialog = + dynamic_cast<Inkscape::UI::Dialog::PanelDialogBase *>(desktop->_dlg_mgr->getDialog("CloneTiler"))) { + try { + Inkscape::UI::Dialog::CloneTiler &clone_tiler = + dynamic_cast<Inkscape::UI::Dialog::CloneTiler &>(panel_dialog->getPanel()); + return &clone_tiler; + } catch (std::exception &e) { } + } + + return nullptr; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { +SprayToolbar::SprayToolbar(SPDesktop *desktop) : + Toolbar(desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Mode */ + { + add_label(_("Mode:")); + + Gtk::RadioToolButton::Group mode_group; + + auto copy_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with copies"))); + copy_mode_btn->set_tooltip_text(_("Spray copies of the initial selection")); + copy_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-copy")); + _mode_buttons.push_back(copy_mode_btn); + + auto clone_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray with clones"))); + clone_mode_btn->set_tooltip_text(_("Spray clones of the initial selection")); + clone_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-clone")); + _mode_buttons.push_back(clone_mode_btn); + +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + auto union_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Spray single path"))); + union_mode_btn->set_tooltip_text(_("Spray objects in a single path")); + union_mode_btn->set_icon_name(INKSCAPE_ICON("spray-mode-union")); + _mode_buttons.push_back(union_mode_btn); +#endif + + auto eraser_mode_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Delete sprayed items"))); + eraser_mode_btn->set_tooltip_text(_("Delete sprayed items from selection")); + eraser_mode_btn->set_icon_name(INKSCAPE_ICON("draw-eraser")); + _mode_buttons.push_back(eraser_mode_btn); + + int btn_idx = 0; + for (auto btn : _mode_buttons) { + btn->set_sensitive(true); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::mode_changed), btn_idx++)); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Width */ + std::vector<Glib::ustring> labels = {_("(narrow spray)"), "", "", "", _("(default)"), "", "", "", "", _("(broad spray)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + auto width_val = prefs->getDouble("/tools/spray/width", 15); + _width_adj = Gtk::Adjustment::create(width_val, 1, 100, 1.0, 10.0); + auto width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-width", _("Width:"), _width_adj, 1, 0)); + width_item->set_tooltip_text(_("The width of the spray area (relative to the visible canvas area)")); + width_item->set_custom_numeric_menu_data(values, labels); + width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::width_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*width_item); + width_item->set_sensitive(true); + } + + /* Use Pressure Width button */ + { + auto pressure_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the width of spray area")); + pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurewidth_pusher.reset(new UI::SimplePrefPusher(pressure_item, "/tools/spray/usepressurewidth")); + pressure_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + pressure_item, + "/tools/spray/usepressurewidth")); + } + + { /* Population */ + std::vector<Glib::ustring> labels = {_("(low population)"), "", "", "", _("(default)"), "", _("(high population)")}; + std::vector<double> values = { 5, 20, 35, 50, 70, 85, 100}; + auto population_val = prefs->getDouble("/tools/spray/population", 70); + _population_adj = Gtk::Adjustment::create(population_val, 1, 100, 1.0, 10.0); + _spray_population = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-population", _("Amount:"), _population_adj, 1, 0)); + _spray_population->set_tooltip_text(_("Adjusts the number of items sprayed per click")); + _spray_population->set_custom_numeric_menu_data(values, labels); + _spray_population->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _population_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::population_value_changed)); + //ege_adjustment_action_set_appearance( holder->_spray_population, TOOLBAR_SLIDER_HINT ); + add(*_spray_population); + _spray_population->set_sensitive(true); + } + + /* Use Pressure Population button */ + { + auto pressure_population_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the amount of sprayed objects")); + pressure_population_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurepopulation_pusher.reset(new UI::SimplePrefPusher(pressure_population_item, "/tools/spray/usepressurepopulation")); + pressure_population_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + pressure_population_item, + "/tools/spray/usepressurepopulation")); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { /* Rotation */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high rotation variation)")}; + std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100}; + auto rotation_val = prefs->getDouble("/tools/spray/rotation_variation", 0); + _rotation_adj = Gtk::Adjustment::create(rotation_val, 0, 100, 1.0, 10.0); + _spray_rotation = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-rotation", _("Rotation:"), _rotation_adj, 1, 0)); + // xgettext:no-c-format + _spray_rotation->set_tooltip_text(_("Variation of the rotation of the sprayed objects; 0% for the same rotation than the original object")); + _spray_rotation->set_custom_numeric_menu_data(values, labels); + _spray_rotation->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::rotation_value_changed)); + // ege_adjustment_action_set_appearance(holder->_spray_rotation, TOOLBAR_SLIDER_HINT ); + add(*_spray_rotation); + _spray_rotation->set_sensitive(); + } + + { /* Scale */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(high scale variation)")}; + std::vector<double> values = { 0, 10, 25, 35, 50, 60, 80, 100}; + auto scale_val = prefs->getDouble("/tools/spray/scale_variation", 0); + _scale_adj = Gtk::Adjustment::create(scale_val, 0, 100, 1.0, 10.0); + _spray_scale = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-scale", C_("Spray tool", "Scale:"), _scale_adj, 1, 0)); + // xgettext:no-c-format + _spray_scale->set_tooltip_text(_("Variation in the scale of the sprayed objects; 0% for the same scale than the original object")); + _spray_scale->set_custom_numeric_menu_data(values, labels); + _spray_scale->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _scale_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::scale_value_changed)); + // ege_adjustment_action_set_appearance( holder->_spray_scale, TOOLBAR_SLIDER_HINT ); + add(*_spray_scale); + _spray_scale->set_sensitive(true); + } + + /* Use Pressure Scale button */ + { + _usepressurescale = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the scale of new items")); + _usepressurescale->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _usepressurescale->set_active(prefs->getBool("/tools/spray/usepressurescale", false)); + _usepressurescale->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_pressure_scale)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + /* Standard_deviation */ + std::vector<Glib::ustring> labels = {_("(minimum scatter)"), "", "", "", "", "", _("(default)"), _("(maximum scatter)")}; + std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100}; + auto sd_val = prefs->getDouble("/tools/spray/standard_deviation", 70); + _sd_adj = Gtk::Adjustment::create(sd_val, 1, 100, 1.0, 10.0); + auto sd_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-standard-deviation", C_("Spray tool", "Scatter:"), _sd_adj, 1, 0)); + sd_item->set_tooltip_text(_("Increase to scatter sprayed objects")); + sd_item->set_custom_numeric_menu_data(values, labels); + sd_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _sd_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::standard_deviation_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*sd_item); + sd_item->set_sensitive(true); + } + + { + /* Mean */ + std::vector<Glib::ustring> labels = {_("(default)"), "", "", "", "", "", "", _("(maximum mean)")}; + std::vector<double> values = { 0, 5, 10, 20, 30, 50, 70, 100}; + auto mean_val = prefs->getDouble("/tools/spray/mean", 0); + _mean_adj = Gtk::Adjustment::create(mean_val, 0, 100, 1.0, 10.0); + auto mean_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-mean", _("Focus:"), _mean_adj, 1, 0)); + mean_item->set_tooltip_text(_("0 to spray a spot; increase to enlarge the ring radius")); + mean_item->set_custom_numeric_menu_data(values, labels); + mean_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _mean_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::mean_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*mean_item); + mean_item->set_sensitive(true); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Over No Transparent */ + { + _over_no_transparent = add_toggle_button(_("Apply over no transparent areas"), + _("Apply over no transparent areas")); + _over_no_transparent->set_icon_name(INKSCAPE_ICON("object-visible")); + _over_no_transparent->set_active(prefs->getBool("/tools/spray/over_no_transparent", true)); + _over_no_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _over_no_transparent, + "/tools/spray/over_no_transparent")); + } + + /* Over Transparent */ + { + _over_transparent = add_toggle_button(_("Apply over transparent areas"), + _("Apply over transparent areas")); + _over_transparent->set_icon_name(INKSCAPE_ICON("object-hidden")); + _over_transparent->set_active(prefs->getBool("/tools/spray/over_transparent", true)); + _over_transparent->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _over_transparent, + "/tools/spray/over_transparent")); + } + + /* Pick No Overlap */ + { + _pick_no_overlap = add_toggle_button(_("No overlap between colors"), + _("No overlap between colors")); + _pick_no_overlap->set_icon_name(INKSCAPE_ICON("symbol-bigger")); + _pick_no_overlap->set_active(prefs->getBool("/tools/spray/pick_no_overlap", false)); + _pick_no_overlap->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_no_overlap, + "/tools/spray/pick_no_overlap")); + } + + /* Overlap */ + { + _no_overlap = add_toggle_button(_("Prevent overlapping objects"), + _("Prevent overlapping objects")); + _no_overlap->set_icon_name(INKSCAPE_ICON("distribute-randomize")); + _no_overlap->set_active(prefs->getBool("/tools/spray/no_overlap", false)); + _no_overlap->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_no_overlap)); + } + + /* Offset */ + { + std::vector<Glib::ustring> labels = {_("(minimum offset)"), "", "", "", _("(default)"), "", "", _("(maximum offset)")}; + std::vector<double> values = { 0, 25, 50, 75, 100, 150, 200, 1000}; + auto offset_val = prefs->getDouble("/tools/spray/offset", 100); + _offset_adj = Gtk::Adjustment::create(offset_val, 0, 1000, 1, 4); + _offset = Gtk::manage(new UI::Widget::SpinButtonToolItem("spray-offset", _("Offset %:"), _offset_adj, 0, 0)); + _offset->set_tooltip_text(_("Increase to segregate objects more (value in percent)")); + _offset->set_custom_numeric_menu_data(values, labels); + _offset->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _offset_adj->signal_value_changed().connect(sigc::mem_fun(*this, &SprayToolbar::offset_value_changed)); + add(*_offset); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Picker */ + { + _picker = add_toggle_button(_("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset."), + _("Pick color from the drawing. You can use clonetiler trace dialog for advanced effects. In clone mode original fill or stroke colors must be unset.")); + _picker->set_icon_name(INKSCAPE_ICON("color-picker")); + _picker->set_active(prefs->getBool("/tools/spray/picker", false)); + _picker->signal_toggled().connect(sigc::mem_fun(*this, &SprayToolbar::toggle_picker)); + } + + /* Pick Fill */ + { + _pick_fill = add_toggle_button(_("Apply picked color to fill"), + _("Apply picked color to fill")); + _pick_fill->set_icon_name(INKSCAPE_ICON("paint-solid")); + _pick_fill->set_active(prefs->getBool("/tools/spray/pick_fill", false)); + _pick_fill->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_fill, + "/tools/spray/pick_fill")); + } + + /* Pick Stroke */ + { + _pick_stroke = add_toggle_button(_("Apply picked color to stroke"), + _("Apply picked color to stroke")); + _pick_stroke->set_icon_name(INKSCAPE_ICON("no-marker")); + _pick_stroke->set_active(prefs->getBool("/tools/spray/pick_stroke", false)); + _pick_stroke->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_stroke, + "/tools/spray/pick_stroke")); + } + + /* Inverse Value Size */ + { + _pick_inverse_value = add_toggle_button(_("Inverted pick value, retaining color in advanced trace mode"), + _("Inverted pick value, retaining color in advanced trace mode")); + _pick_inverse_value->set_icon_name(INKSCAPE_ICON("object-tweak-shrink")); + _pick_inverse_value->set_active(prefs->getBool("/tools/spray/pick_inverse_value", false)); + _pick_inverse_value->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_inverse_value, + "/tools/spray/pick_inverse_value")); + } + + /* Pick from center */ + { + _pick_center = add_toggle_button(_("Pick from center instead of average area."), + _("Pick from center instead of average area.")); + _pick_center->set_icon_name(INKSCAPE_ICON("snap-bounding-box-center")); + _pick_center->set_active(prefs->getBool("/tools/spray/pick_center", true)); + _pick_center->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &SprayToolbar::on_pref_toggled), + _pick_center, + "/tools/spray/pick_center")); + } + + gint mode = prefs->getInt("/tools/spray/mode", 1); + _mode_buttons[mode]->set_active(); + show_all(); + init(); +} + +GtkWidget * +SprayToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new SprayToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +SprayToolbar::width_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/width", + _width_adj->get_value()); +} + +void +SprayToolbar::mean_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/mean", + _mean_adj->get_value()); +} + +void +SprayToolbar::standard_deviation_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/standard_deviation", + _sd_adj->get_value()); +} + +void +SprayToolbar::mode_changed(int mode) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/spray/mode", mode); + init(); +} + +void +SprayToolbar::init(){ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int mode = prefs->getInt("/tools/spray/mode", 0); + + bool show = true; + if(mode == 3 || mode == 2){ + show = false; + } + _no_overlap->set_visible(show); + _over_no_transparent->set_visible(show); + _over_transparent->set_visible(show); + _pick_no_overlap->set_visible(show); + _pick_stroke->set_visible(show); + _pick_fill->set_visible(show); + _pick_inverse_value->set_visible(show); + _pick_center->set_visible(show); + _picker->set_visible(show); + _offset->set_visible(show); + _pick_fill->set_visible(show); + _pick_stroke->set_visible(show); + _pick_inverse_value->set_visible(show); + _pick_center->set_visible(show); + if(mode == 2){ + show = true; + } + _spray_rotation->set_visible(show); + update_widgets(); +} + +void +SprayToolbar::population_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/population", + _population_adj->get_value()); +} + +void +SprayToolbar::rotation_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/rotation_variation", + _rotation_adj->get_value()); +} + +void +SprayToolbar::update_widgets() +{ + _offset_adj->set_value(100.0); + + bool no_overlap_is_active = _no_overlap->get_active() && _no_overlap->get_visible(); + _offset->set_visible(no_overlap_is_active); + if (_usepressurescale->get_active()) { + _scale_adj->set_value(0.0); + _spray_scale->set_sensitive(false); + } else { + _spray_scale->set_sensitive(true); + } + + bool picker_is_active = _picker->get_active() && _picker->get_visible(); + _pick_fill->set_visible(picker_is_active); + _pick_stroke->set_visible(picker_is_active); + _pick_inverse_value->set_visible(picker_is_active); + _pick_center->set_visible(picker_is_active); +} + +void +SprayToolbar::toggle_no_overlap() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _no_overlap->get_active(); + prefs->setBool("/tools/spray/no_overlap", active); + update_widgets(); +} + +void +SprayToolbar::scale_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/scale_variation", + _scale_adj->get_value()); +} + +void +SprayToolbar::offset_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/spray/offset", + _offset_adj->get_value()); +} + +void +SprayToolbar::toggle_pressure_scale() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _usepressurescale->get_active(); + prefs->setBool("/tools/spray/usepressurescale", active); + if(active){ + prefs->setDouble("/tools/spray/scale_variation", 0); + } + update_widgets(); +} + +void +SprayToolbar::toggle_picker() +{ + auto prefs = Inkscape::Preferences::get(); + bool active = _picker->get_active(); + prefs->setBool("/tools/spray/picker", active); + if(active){ + prefs->setBool("/dialogs/clonetiler/dotrace", false); + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (Inkscape::UI::Dialog::CloneTiler *ct = get_clone_tiler_panel(dt)){ + dt->_dlg_mgr->showDialog("CloneTiler"); + ct->show_page_trace(); + } + } + update_widgets(); +} + +void +SprayToolbar::on_pref_toggled(Gtk::ToggleToolButton *btn, + const Glib::ustring& path) +{ + auto prefs = Inkscape::Preferences::get(); + bool active = btn->get_active(); + prefs->setBool(path, active); +} + +void +SprayToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/spray-toolbar.h b/src/ui/toolbar/spray-toolbar.h new file mode 100644 index 0000000..4587cf0 --- /dev/null +++ b/src/ui/toolbar/spray-toolbar.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPRAY_TOOLBAR_H +#define SEEN_SPRAY_TOOLBAR_H + +/** + * @file + * Spray aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +namespace UI { +class SimplePrefPusher; + +namespace Widget { +class SpinButtonToolItem; +} + +namespace Toolbar { +class SprayToolbar : public Toolbar { +private: + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _mean_adj; + Glib::RefPtr<Gtk::Adjustment> _sd_adj; + Glib::RefPtr<Gtk::Adjustment> _population_adj; + Glib::RefPtr<Gtk::Adjustment> _rotation_adj; + Glib::RefPtr<Gtk::Adjustment> _offset_adj; + Glib::RefPtr<Gtk::Adjustment> _scale_adj; + + std::unique_ptr<SimplePrefPusher> _usepressurewidth_pusher; + std::unique_ptr<SimplePrefPusher> _usepressurepopulation_pusher; + + std::vector<Gtk::RadioToolButton *> _mode_buttons; + UI::Widget::SpinButtonToolItem *_spray_population; + UI::Widget::SpinButtonToolItem *_spray_rotation; + UI::Widget::SpinButtonToolItem *_spray_scale; + Gtk::ToggleToolButton *_usepressurescale; + Gtk::ToggleToolButton *_picker; + Gtk::ToggleToolButton *_pick_center; + Gtk::ToggleToolButton *_pick_inverse_value; + Gtk::ToggleToolButton *_pick_fill; + Gtk::ToggleToolButton *_pick_stroke; + Gtk::ToggleToolButton *_pick_no_overlap; + Gtk::ToggleToolButton *_over_transparent; + Gtk::ToggleToolButton *_over_no_transparent; + Gtk::ToggleToolButton *_no_overlap; + UI::Widget::SpinButtonToolItem *_offset; + + void width_value_changed(); + void mean_value_changed(); + void standard_deviation_value_changed(); + void mode_changed(int mode); + void init(); + void population_value_changed(); + void rotation_value_changed(); + void update_widgets(); + void scale_value_changed(); + void offset_value_changed(); + void on_pref_toggled(Gtk::ToggleToolButton *btn, + const Glib::ustring& path); + void toggle_no_overlap(); + void toggle_pressure_scale(); + void toggle_picker(); + +protected: + SprayToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + void set_mode(int mode); +}; +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/star-toolbar.cpp b/src/ui/toolbar/star-toolbar.cpp new file mode 100644 index 0000000..2c020cf --- /dev/null +++ b/src/ui/toolbar/star-toolbar.cpp @@ -0,0 +1,564 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Star aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "star-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-star.h" + +#include "ui/icon-names.h" +#include "ui/tools/star-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" + +#include "xml/node-event-vector.h" + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; + +static Inkscape::XML::NodeEventVector star_tb_repr_events = +{ + nullptr, /* child_added */ + nullptr, /* child_removed */ + Inkscape::UI::Toolbar::StarToolbar::event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +namespace Inkscape { +namespace UI { +namespace Toolbar { +StarToolbar::StarToolbar(SPDesktop *desktop) : + Toolbar(desktop), + _mode_item(Gtk::manage(new UI::Widget::LabelToolItem(_("<b>New:</b>")))), + _repr(nullptr), + _freeze(false) +{ + _mode_item->set_use_markup(true); + add(*_mode_item); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false); + + /* Flatsided checkbox */ + { + Gtk::RadioToolButton::Group flat_item_group; + + auto flat_polygon_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Polygon"))); + flat_polygon_button->set_tooltip_text(_("Regular polygon (with one handle) instead of a star")); + flat_polygon_button->set_icon_name(INKSCAPE_ICON("draw-polygon")); + _flat_item_buttons.push_back(flat_polygon_button); + + auto flat_star_button = Gtk::manage(new Gtk::RadioToolButton(flat_item_group, _("Star"))); + flat_star_button->set_tooltip_text(_("Star instead of a regular polygon (with one handle)")); + flat_star_button->set_icon_name(INKSCAPE_ICON("draw-star")); + _flat_item_buttons.push_back(flat_star_button); + + _flat_item_buttons[ isFlatSided ? 0 : 1 ]->set_active(); + + int btn_index = 0; + + for (auto btn : _flat_item_buttons) + { + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &StarToolbar::side_mode_changed), btn_index++)); + } + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Magnitude */ + { + std::vector<Glib::ustring> labels = {_("triangle/tri-star"), _("square/quad-star"), _("pentagon/five-pointed star"), _("hexagon/six-pointed star"), "", "", "", "", ""}; + std::vector<double> values = { 3, 4, 5, 6, 7, 8, 10, 12, 20}; + auto magnitude_val = prefs->getDouble("/tools/shapes/star/magnitude", 3); + _magnitude_adj = Gtk::Adjustment::create(magnitude_val, 3, 1024, 1, 5); + _magnitude_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-magnitude", _("Corners:"), _magnitude_adj, 1.0, 0)); + _magnitude_item->set_tooltip_text(_("Number of corners of a polygon or star")); + _magnitude_item->set_custom_numeric_menu_data(values, labels); + _magnitude_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _magnitude_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::magnitude_value_changed)); + _magnitude_item->set_sensitive(true); + add(*_magnitude_item); + } + + /* Spoke ratio */ + { + std::vector<Glib::ustring> labels = {_("thin-ray star"), "", _("pentagram"), _("hexagram"), _("heptagram"), _("octagram"), _("regular polygon")}; + std::vector<double> values = { 0.01, 0.2, 0.382, 0.577, 0.692, 0.765, 1}; + auto prop_val = prefs->getDouble("/tools/shapes/star/proportion", 0.5); + _spoke_adj = Gtk::Adjustment::create(prop_val, 0.01, 1.0, 0.01, 0.1); + _spoke_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-spoke", _("Spoke ratio:"), _spoke_adj)); + // TRANSLATORS: Tip radius of a star is the distance from the center to the farthest handle. + // Base radius is the same for the closest handle. + _spoke_item->set_tooltip_text(_("Base radius to tip radius ratio")); + _spoke_item->set_custom_numeric_menu_data(values, labels); + _spoke_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _spoke_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::proportion_value_changed)); + + add(*_spoke_item); + } + + /* Roundedness */ + { + std::vector<Glib::ustring> labels = {_("stretched"), _("twisted"), _("slightly pinched"), _("NOT rounded"), _("slightly rounded"), + _("visibly rounded"), _("well rounded"), _("amply rounded"), "", _("stretched"), _("blown up")}; + std::vector<double> values = {-1, -0.2, -0.03, 0, 0.05, 0.1, 0.2, 0.3, 0.5, 1, 10}; + auto roundedness_val = prefs->getDouble("/tools/shapes/star/rounded", 0.0); + _roundedness_adj = Gtk::Adjustment::create(roundedness_val, -10.0, 10.0, 0.01, 0.1); + _roundedness_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-roundedness", _("Rounded:"), _roundedness_adj)); + _roundedness_item->set_tooltip_text(_("How rounded are the corners (0 for sharp)")); + _roundedness_item->set_custom_numeric_menu_data(values, labels); + _roundedness_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _roundedness_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::rounded_value_changed)); + _roundedness_item->set_sensitive(true); + add(*_roundedness_item); + } + + /* Randomization */ + { + std::vector<Glib::ustring> labels = {_("NOT randomized"), _("slightly irregular"), _("visibly randomized"), _("strongly randomized"), _("blown up")}; + std::vector<double> values = { 0, 0.01, 0.1, 0.5, 10}; + auto randomized_val = prefs->getDouble("/tools/shapes/star/randomized", 0.0); + _randomization_adj = Gtk::Adjustment::create(randomized_val, -10.0, 10.0, 0.001, 0.01); + _randomization_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("star-randomized", _("Randomized:"), _randomization_adj, 0.1, 3)); + _randomization_item->set_tooltip_text(_("Scatter randomly the corners and angles")); + _randomization_item->set_custom_numeric_menu_data(values, labels); + _randomization_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _randomization_adj->signal_value_changed().connect(sigc::mem_fun(*this, &StarToolbar::randomized_value_changed)); + _randomization_item->set_sensitive(true); + add(*_randomization_item); + } + + add(*Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Reset */ + { + _reset_item = Gtk::manage(new Gtk::ToolButton(_("Defaults"))); + _reset_item->set_icon_name(INKSCAPE_ICON("edit-clear")); + _reset_item->set_tooltip_text(_("Reset shape parameters to defaults (use Inkscape Preferences > Tools to change defaults)")); + _reset_item->signal_clicked().connect(sigc::mem_fun(*this, &StarToolbar::defaults)); + _reset_item->set_sensitive(true); + add(*_reset_item); + } + + desktop->connectEventContextChanged(sigc::mem_fun(*this, &StarToolbar::watch_ec)); + + show_all(); + _spoke_item->set_visible(!isFlatSided); +} + +StarToolbar::~StarToolbar() +{ + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } +} + +GtkWidget * +StarToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new StarToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +StarToolbar::side_mode_changed(int mode) +{ + bool flat = (mode == 0); + + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool( "/tools/shapes/star/isflatsided", flat ); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + Inkscape::Selection *selection = _desktop->getSelection(); + bool modmade = false; + + if (_spoke_item) { + _spoke_item->set_visible(!flat); + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + repr->setAttribute("inkscape:flatsided", flat ? "true" : "false" ); + item->updateRepr(); + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR, + flat ? _("Make polygon") : _("Make star")); + } + + _freeze = false; +} + +void +StarToolbar::magnitude_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + // do not remember prefs if this call is initiated by an undo change, because undoing object + // creation sets bogus values to its attributes before it is deleted + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/shapes/star/magnitude", + (gint)_magnitude_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + sp_repr_set_int(repr,"sodipodi:sides", + (gint)_magnitude_adj->get_value()); + double arg1 = 0.5; + sp_repr_get_double(repr, "sodipodi:arg1", &arg1); + sp_repr_set_svg_double(repr, "sodipodi:arg2", + (arg1 + M_PI / (gint)_magnitude_adj->get_value())); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR, + _("Star: Change number of corners")); + } + + _freeze = false; +} + +void +StarToolbar::proportion_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + if (!std::isnan(_spoke_adj->get_value())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/proportion", + _spoke_adj->get_value()); + } + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + + gdouble r1 = 1.0; + gdouble r2 = 1.0; + sp_repr_get_double(repr, "sodipodi:r1", &r1); + sp_repr_get_double(repr, "sodipodi:r2", &r2); + if (r2 < r1) { + sp_repr_set_svg_double(repr, "sodipodi:r2", + r1*_spoke_adj->get_value()); + } else { + sp_repr_set_svg_double(repr, "sodipodi:r1", + r2*_spoke_adj->get_value()); + } + + item->updateRepr(); + modmade = true; + } + } + + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR, + _("Star: Change spoke ratio")); + } + + _freeze = false; +} + +void +StarToolbar::rounded_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/rounded", (gdouble) _roundedness_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + sp_repr_set_svg_double(repr, "inkscape:rounded", + (gdouble) _roundedness_adj->get_value()); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR, + _("Star: Change rounding")); + } + + _freeze = false; +} + +void +StarToolbar::randomized_value_changed() +{ + if (DocumentUndo::getUndoSensitive(_desktop->getDocument())) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/tools/shapes/star/randomized", + (gdouble) _randomization_adj->get_value()); + } + + // quit if run by the attr_changed listener + if (_freeze) { + return; + } + + // in turn, prevent listener from responding + _freeze = true; + + bool modmade = false; + + Inkscape::Selection *selection = _desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + Inkscape::XML::Node *repr = item->getRepr(); + sp_repr_set_svg_double(repr, "inkscape:randomized", + (gdouble) _randomization_adj->get_value()); + item->updateRepr(); + modmade = true; + } + } + if (modmade) { + DocumentUndo::done(_desktop->getDocument(), SP_VERB_CONTEXT_STAR, + _("Star: Change randomization")); + } + + _freeze = false; +} + +void +StarToolbar::defaults() +{ + + // FIXME: in this and all other _default functions, set some flag telling the value_changed + // callbacks to lump all the changes for all selected objects in one undo step + + // fixme: make settable in prefs! + gint mag = 5; + gdouble prop = 0.5; + gboolean flat = FALSE; + gdouble randomized = 0; + gdouble rounded = 0; + + _flat_item_buttons[ flat ? 0 : 1 ]->set_active(); + + _spoke_item->set_visible(!flat); + + _magnitude_adj->set_value(mag); + _spoke_adj->set_value(prop); + _roundedness_adj->set_value(rounded); + _randomization_adj->set_value(randomized); +} + +void +StarToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) +{ + if (dynamic_cast<Inkscape::UI::Tools::StarTool const*>(ec) != nullptr) { + _changed = desktop->getSelection()->connectChanged(sigc::mem_fun(*this, &StarToolbar::selection_changed)); + selection_changed(desktop->getSelection()); + } else { + if (_changed) + _changed.disconnect(); + } +} + +/** + * \param selection Should not be NULL. + */ +void +StarToolbar::selection_changed(Inkscape::Selection *selection) +{ + int n_selected = 0; + Inkscape::XML::Node *repr = nullptr; + + if (_repr) { // remove old listener + _repr->removeListenerByData(this); + Inkscape::GC::release(_repr); + _repr = nullptr; + } + + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (SP_IS_STAR(item)) { + n_selected++; + repr = item->getRepr(); + } + } + + if (n_selected == 0) { + _mode_item->set_markup(_("<b>New:</b>")); + } else if (n_selected == 1) { + _mode_item->set_markup(_("<b>Change:</b>")); + + if (repr) { + _repr = repr; + Inkscape::GC::anchor(_repr); + _repr->addListener(&star_tb_repr_events, this); + _repr->synthesizeEvents(&star_tb_repr_events, this); + } + } else { + // FIXME: implement averaging of all parameters for multiple selected stars + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Average:</b>")); + //gtk_label_set_markup(GTK_LABEL(l), _("<b>Change:</b>")); + } +} + +void +StarToolbar::event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const * /*old_value*/, gchar const * /*new_value*/, + bool /*is_interactive*/, gpointer dataPointer) +{ + auto toolbar = reinterpret_cast<StarToolbar *>(dataPointer); + + // quit if run by the _changed callbacks + if (toolbar->_freeze) { + return; + } + + // in turn, prevent callbacks from responding + toolbar->_freeze = true; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool isFlatSided = prefs->getBool("/tools/shapes/star/isflatsided", false); + + if (!strcmp(name, "inkscape:randomized")) { + double randomized = 0.0; + sp_repr_get_double(repr, "inkscape:randomized", &randomized); + toolbar->_randomization_adj->set_value(randomized); + } else if (!strcmp(name, "inkscape:rounded")) { + double rounded = 0.0; + sp_repr_get_double(repr, "inkscape:rounded", &rounded); + toolbar->_roundedness_adj->set_value(rounded); + } else if (!strcmp(name, "inkscape:flatsided")) { + char const *flatsides = repr->attribute("inkscape:flatsided"); + if ( flatsides && !strcmp(flatsides,"false") ) { + toolbar->_flat_item_buttons[1]->set_active(); + toolbar->_spoke_item->set_visible(true); + } else { + toolbar->_flat_item_buttons[0]->set_active(); + toolbar->_spoke_item->set_visible(false); + } + } else if ((!strcmp(name, "sodipodi:r1") || !strcmp(name, "sodipodi:r2")) && (!isFlatSided) ) { + gdouble r1 = 1.0; + gdouble r2 = 1.0; + sp_repr_get_double(repr, "sodipodi:r1", &r1); + sp_repr_get_double(repr, "sodipodi:r2", &r2); + if (r2 < r1) { + toolbar->_spoke_adj->set_value(r2/r1); + } else { + toolbar->_spoke_adj->set_value(r1/r2); + } + } else if (!strcmp(name, "sodipodi:sides")) { + int sides = 0; + sp_repr_get_int(repr, "sodipodi:sides", &sides); + toolbar->_magnitude_adj->set_value(sides); + } + + toolbar->_freeze = false; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/toolbar/star-toolbar.h b/src/ui/toolbar/star-toolbar.h new file mode 100644 index 0000000..c44caab --- /dev/null +++ b/src/ui/toolbar/star-toolbar.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_STAR_TOOLBAR_H +#define SEEN_STAR_TOOLBAR_H + +/** + * @file + * Star aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +#include <gtkmm/adjustment.h> + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +class ToolButton; +} + +namespace Inkscape { +class Selection; + +namespace XML { +class Node; +} + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class StarToolbar : public Toolbar { +private: + UI::Widget::LabelToolItem *_mode_item; + std::vector<Gtk::RadioToolButton *> _flat_item_buttons; + UI::Widget::SpinButtonToolItem *_magnitude_item; + UI::Widget::SpinButtonToolItem *_spoke_item; + UI::Widget::SpinButtonToolItem *_roundedness_item; + UI::Widget::SpinButtonToolItem *_randomization_item; + Gtk::ToolButton *_reset_item; + + XML::Node *_repr; + + Glib::RefPtr<Gtk::Adjustment> _magnitude_adj; + Glib::RefPtr<Gtk::Adjustment> _spoke_adj; + Glib::RefPtr<Gtk::Adjustment> _roundedness_adj; + Glib::RefPtr<Gtk::Adjustment> _randomization_adj; + + bool _freeze; + sigc::connection _changed; + + void side_mode_changed(int mode); + void magnitude_value_changed(); + void proportion_value_changed(); + void rounded_value_changed(); + void randomized_value_changed(); + void defaults(); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void selection_changed(Inkscape::Selection *selection); + +protected: + StarToolbar(SPDesktop *desktop); + ~StarToolbar() override; + +public: + static GtkWidget * create(SPDesktop *desktop); + + static void event_attr_changed(Inkscape::XML::Node *repr, + gchar const *name, + gchar const *old_value, + gchar const *new_value, + bool is_interactive, + gpointer dataPointer); +}; + +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/text-toolbar.cpp b/src/ui/toolbar/text-toolbar.cpp new file mode 100644 index 0000000..3b9a152 --- /dev/null +++ b/src/ui/toolbar/text-toolbar.cpp @@ -0,0 +1,2540 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Text aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 1999-2013 authors + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "text-toolbar.h" + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "libnrtype/font-lister.h" + +#include "display/sp-canvas.h" +#include "object/sp-flowdiv.h" +#include "object/sp-flowtext.h" +#include "object/sp-root.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "object/sp-string.h" + +#include "svg/css-ostringstream.h" +#include "ui/icon-names.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/widget/combo-box-entry-tool-item.h" +#include "ui/widget/combo-tool-item.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" +#include "util/units.h" + +#include "widgets/style-utils.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::Unit; +using Inkscape::Util::Quantity; +using Inkscape::Util::unit_table; +using Inkscape::UI::Widget::UnitTracker; + +//#define DEBUG_TEXT + +//######################## +//## Text Toolbox ## +//######################## + +// Functions for debugging: +#ifdef DEBUG_TEXT +static void sp_print_font(SPStyle *query) +{ + + + bool family_set = query->font_family.set; + bool style_set = query->font_style.set; + bool fontspec_set = query->font_specification.set; + + std::cout << " Family set? " << family_set + << " Style set? " << style_set + << " FontSpec set? " << fontspec_set + << std::endl; +} + +static void sp_print_fontweight( SPStyle *query ) { + const gchar* names[] = {"100", "200", "300", "400", "500", "600", "700", "800", "900", + "NORMAL", "BOLD", "LIGHTER", "BOLDER", "Out of range"}; + // Missing book = 380 + int index = query->font_weight.computed; + if (index < 0 || index > 13) + index = 13; + std::cout << " Weight: " << names[ index ] + << " (" << query->font_weight.computed << ")" << std::endl; +} + +static void sp_print_fontstyle( SPStyle *query ) { + + const gchar* names[] = {"NORMAL", "ITALIC", "OBLIQUE", "Out of range"}; + int index = query->font_style.computed; + if( index < 0 || index > 3 ) index = 3; + std::cout << " Style: " << names[ index ] << std::endl; + +} +#endif + +static bool is_relative( Unit const *unit ) { + return (unit->abbr == "" || unit->abbr == "em" || unit->abbr == "ex" || unit->abbr == "%"); +} + +static bool is_relative(SPCSSUnit const unit) +{ + return (unit == SP_CSS_UNIT_NONE || unit == SP_CSS_UNIT_EM || unit == SP_CSS_UNIT_EX || + unit == SP_CSS_UNIT_PERCENT); +} + +// Set property for object, but unset all descendents +// Should probably be moved to desktop_style.cpp +static void recursively_set_properties(SPObject *object, SPCSSAttr *css, bool unset_descendents = true) +{ + object->changeCSS (css, "style"); + + SPCSSAttr *css_unset = sp_repr_css_attr_unset_all( css ); + std::vector<SPObject *> children = object->childList(false); + for (auto i: children) { + recursively_set_properties(i, unset_descendents ? css_unset : css); + } + sp_repr_css_attr_unref (css_unset); +} + +/* + * Set the default list of font sizes, scaled to the users preferred unit + */ +static void sp_text_set_sizes(GtkListStore* model_size, int unit) +{ + gtk_list_store_clear(model_size); + + // List of font sizes for dropchange-down menu + int sizes[] = { + 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28, + 32, 36, 40, 48, 56, 64, 72, 144 + }; + + // Array must be same length as SPCSSUnit in style.h + float ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16}; + + for(int i : sizes) { + GtkTreeIter iter; + Glib::ustring size = Glib::ustring::format(i / (float)ratios[unit]); + gtk_list_store_append( model_size, &iter ); + gtk_list_store_set( model_size, &iter, 0, size.c_str(), -1 ); + } +} + + +// TODO: possibly share with font-selector by moving most code to font-lister (passing family name) +static void sp_text_toolbox_select_cb( GtkEntry* entry, GtkEntryIconPosition /*position*/, GdkEvent /*event*/, gpointer /*data*/ ) { + + Glib::ustring family = gtk_entry_get_text ( entry ); + //std::cout << "text_toolbox_missing_font_cb: selecting: " << family << std::endl; + + // Get all items with matching font-family set (not inherited!). + std::vector<SPItem*> selectList; + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *document = desktop->getDocument(); + std::vector<SPItem*> x,y; + std::vector<SPItem*> allList = get_all_items(x, document->getRoot(), desktop, false, false, true, y); + for(std::vector<SPItem*>::const_reverse_iterator i=allList.rbegin();i!=allList.rend(); ++i){ + SPItem *item = *i; + SPStyle *style = item->style; + + if (style) { + + Glib::ustring family_style; + if (style->font_family.set) { + family_style = style->font_family.value(); + //std::cout << " family style from font_family: " << family_style << std::endl; + } + else if (style->font_specification.set) { + family_style = style->font_specification.value(); + //std::cout << " family style from font_spec: " << family_style << std::endl; + } + + if (family_style.compare( family ) == 0 ) { + //std::cout << " found: " << item->getId() << std::endl; + selectList.push_back(item); + } + } + } + + // Update selection + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + //std::cout << " list length: " << g_slist_length ( selectList ) << std::endl; + selection->setList(selectList); +} + +static void text_toolbox_watch_ec(SPDesktop* dt, Inkscape::UI::Tools::ToolBase* ec, GObject* holder); + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +TextToolbar::TextToolbar(SPDesktop *desktop) + : Toolbar(desktop) + , _freeze(false) + , _text_style_from_prefs(false) + , _outer(true) + , _updating(false) + , _tracker(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) + , _tracker_fs(new UnitTracker(Inkscape::Util::UNIT_TYPE_LINEAR)) + , _cusor_numbers(0) +{ + /* Line height unit tracker */ + _tracker->prependUnit(unit_table.getUnit("")); // Ratio + _tracker->addUnit(unit_table.getUnit("%")); + _tracker->addUnit(unit_table.getUnit("em")); + _tracker->addUnit(unit_table.getUnit("ex")); + _tracker->setActiveUnit(unit_table.getUnit("")); + // We change only the display value + _tracker->changeLabel("lines", 0, true); + _tracker_fs->setActiveUnit(unit_table.getUnit("mm")); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Font family */ + { + // Font list + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + fontlister->update_font_list( SP_ACTIVE_DESKTOP->getDocument()); + Glib::RefPtr<Gtk::ListStore> store = fontlister->get_font_list(); + GtkListStore* model = store->gobj(); + + _font_family_item = + Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontFamilyAction", + _("Font Family"), + _("Select Font Family (Alt-X to access)"), + GTK_TREE_MODEL(model), + -1, // Entry width + 50, // Extra list width + (gpointer)font_lister_cell_data_func2, // Cell layout + (gpointer)font_lister_separator_func2, + GTK_WIDGET(desktop->canvas))); // Focus widget + _font_family_item->popup_enable(); // Enable entry completion + gchar *const info = _("Select all text with this font-family"); + _font_family_item->set_info( info ); // Show selection icon + _font_family_item->set_info_cb( (gpointer)sp_text_toolbox_select_cb ); + + gchar *const warning = _("Font not found on system"); + _font_family_item->set_warning( warning ); // Show icon w/ tooltip if font missing + _font_family_item->set_warning_cb( (gpointer)sp_text_toolbox_select_cb ); + + //ink_comboboxentry_action_set_warning_callback( act, sp_text_fontfamily_select_all ); + _font_family_item->set_altx_name( "altx-text" ); // Set Alt-X keyboard shortcut + _font_family_item->signal_changed().connect( sigc::mem_fun(*this, &TextToolbar::fontfamily_value_changed) ); + add(*_font_family_item); + + // Change style of drop-down from menu to list + auto css_provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(css_provider, + "#TextFontFamilyAction_combobox {\n" + " -GtkComboBox-appears-as-list: true;\n" + "}\n", + -1, nullptr); + + auto screen = gdk_screen_get_default(); + _font_family_item->focus_on_click(false); + gtk_style_context_add_provider_for_screen(screen, + GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + } + + /* Font styles */ + { + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + Glib::RefPtr<Gtk::ListStore> store = fontlister->get_style_list(); + GtkListStore* model_style = store->gobj(); + + _font_style_item = Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontStyleAction", + _("Font Style"), + _("Font style"), + GTK_TREE_MODEL(model_style), + 12, // Width in characters + 0, // Extra list width + nullptr, // Cell layout + nullptr, // Separator + GTK_WIDGET(desktop->canvas))); // Focus widget + + _font_style_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontstyle_value_changed)); + _font_style_item->focus_on_click(false); + add(*_font_style_item); + } + + add_separator(); + + /* Font size */ + { + // List of font sizes for drop-down menu + GtkListStore* model_size = gtk_list_store_new( 1, G_TYPE_STRING ); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + + sp_text_set_sizes(model_size, unit); + + auto unit_str = sp_style_get_css_unit_string(unit); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")"); + + _font_size_item = Gtk::manage(new UI::Widget::ComboBoxEntryToolItem( "TextFontSizeAction", + _("Font Size"), + tooltip, + GTK_TREE_MODEL(model_size), + 8, // Width in characters + 0, // Extra list width + nullptr, // Cell layout + nullptr, // Separator + GTK_WIDGET(desktop->canvas))); // Focus widget + + _font_size_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::fontsize_value_changed)); + _font_size_item->focus_on_click(false); + add(*_font_size_item); + } + /* Font_ size units */ + { + _font_size_units_item = _tracker_fs->create_tool_item(_("Units"), ("")); + _font_size_units_item->signal_changed_after().connect( + sigc::mem_fun(*this, &TextToolbar::fontsize_unit_changed)); + _font_size_units_item->focus_on_click(false); + add(*_font_size_units_item); + } + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Smaller spacing"), "", "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", _("Larger spacing")}; + std::vector<double> values = { 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 2.0}; + + auto line_height_val = 1.25; + _line_height_adj = Gtk::Adjustment::create(line_height_val, 0.0, 1000.0, 0.1, 1.0); + _line_height_item = + Gtk::manage(new UI::Widget::SpinButtonToolItem("text-line-height", "", _line_height_adj, 0.1, 2)); + _line_height_item->set_tooltip_text(_("Spacing between baselines")); + _line_height_item->set_custom_numeric_menu_data(values, labels); + _line_height_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _line_height_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_value_changed)); + //_tracker->addAdjustment(_line_height_adj->gobj()); // (Alex V) Why is this commented out? + _line_height_item->set_sensitive(true); + _line_height_item->set_icon(INKSCAPE_ICON("text_line_spacing")); + add(*_line_height_item); + } + /* Line height units */ + { + _line_height_units_item = _tracker->create_tool_item( _("Units"), ("")); + _line_height_units_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::lineheight_unit_changed)); + _line_height_units_item->focus_on_click(false); + add(*_line_height_units_item); + } + + Gtk::SeparatorToolItem *separator = Gtk::manage(new Gtk::SeparatorToolItem()); + /* Alignment */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Align left"); + row[columns.col_tooltip ] = _("Align left"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-left"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Align center"); + row[columns.col_tooltip ] = _("Align center"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-center"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Align right"); + row[columns.col_tooltip ] = _("Align right"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-right"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Justify"); + row[columns.col_tooltip ] = _("Justify (only flowed text)"); + row[columns.col_icon ] = INKSCAPE_ICON("format-justify-fill"); + row[columns.col_sensitive] = false; + + _align_item = + UI::Widget::ComboToolItem::create(_("Alignment"), // Label + _("Text alignment"), // Tooltip + "Not Used", // Icon + store ); // Tree store + _align_item->use_icon( true ); + _align_item->use_label( false ); + gint mode = prefs->getInt("/tools/text/align_mode", 0); + _align_item->set_active( mode ); + + add(*_align_item); + _align_item->focus_on_click(false); + _align_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::align_mode_changed)); + } + + /* Style - Superscript */ + { + _superscript_item = Gtk::manage(new Gtk::ToggleToolButton()); + _superscript_item->set_label(_("Toggle superscript")); + _superscript_item->set_tooltip_text(_("Toggle superscript")); + _superscript_item->set_icon_name(INKSCAPE_ICON("text_superscript")); + _superscript_item->set_name("text-superscript"); + add(*_superscript_item); + _superscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _superscript_item)); + _superscript_item->set_active(prefs->getBool("/tools/text/super", false)); + } + + /* Style - Subscript */ + { + _subscript_item = Gtk::manage(new Gtk::ToggleToolButton()); + _subscript_item->set_label(_("Toggle subscript")); + _subscript_item->set_tooltip_text(_("Toggle subscript")); + _subscript_item->set_icon_name(INKSCAPE_ICON("text_subscript")); + _subscript_item->set_name("text-subscript"); + add(*_subscript_item); + _subscript_item->signal_toggled().connect(sigc::bind(sigc::mem_fun(*this, &TextToolbar::script_changed), _subscript_item)); + _subscript_item->set_active(prefs->getBool("/tools/text/sub", false)); + } + + /* Letter spacing */ + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")}; + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}; + auto letter_spacing_val = prefs->getDouble("/tools/text/letterspacing", 0.0); + _letter_spacing_adj = Gtk::Adjustment::create(letter_spacing_val, -100.0, 100.0, 0.01, 0.10); + _letter_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-letter-spacing", _("Letter:"), _letter_spacing_adj, 0.1, 2)); + _letter_spacing_item->set_tooltip_text(_("Spacing between letters (px)")); + _letter_spacing_item->set_custom_numeric_menu_data(values, labels); + _letter_spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _letter_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::letterspacing_value_changed)); + add(*_letter_spacing_item); + + _letter_spacing_item->set_sensitive(true); + _letter_spacing_item->set_icon(INKSCAPE_ICON("text_letter_spacing")); + } + + /* Word spacing */ + { + // Drop down menu + std::vector<Glib::ustring> labels = {_("Negative spacing"), "", "", "", C_("Text tool", "Normal"), "", "", "", "", "", "", "", _("Positive spacing")}; + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}; + auto word_spacing_val = prefs->getDouble("/tools/text/wordspacing", 0.0); + _word_spacing_adj = Gtk::Adjustment::create(word_spacing_val, -100.0, 100.0, 0.01, 0.10); + _word_spacing_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-word-spacing", _("Word:"), _word_spacing_adj, 0.1, 2)); + _word_spacing_item->set_tooltip_text(_("Spacing between words (px)")); + _word_spacing_item->set_custom_numeric_menu_data(values, labels); + _word_spacing_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _word_spacing_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::wordspacing_value_changed)); + + add(*_word_spacing_item); + _word_spacing_item->set_sensitive(true); + _word_spacing_item->set_icon(INKSCAPE_ICON("text_word_spacing")); + } + + /* Character kerning (horizontal shift) */ + { + // Drop down menu + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 }; + auto dx_val = prefs->getDouble("/tools/text/dx", 0.0); + _dx_adj = Gtk::Adjustment::create(dx_val, -100.0, 100.0, 0.01, 0.1); + _dx_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dx", _("Kern:"), _dx_adj, 0.1, 2)); + _dx_item->set_custom_numeric_menu_data(values); + _dx_item->set_tooltip_text(_("Horizontal kerning (px)")); + _dx_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _dx_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dx_value_changed)); + add(*_dx_item); + _dx_item->set_sensitive(true); + _dx_item->set_icon(INKSCAPE_ICON("text_horz_kern")); + } + + /* Character vertical shift */ + { + // Drop down menu + std::vector<double> values = { -2.0, -1.5, -1.0, -0.5, 0, 0.5, 1.0, 1.5, 2.0, 2.5 }; + auto dy_val = prefs->getDouble("/tools/text/dy", 0.0); + _dy_adj = Gtk::Adjustment::create(dy_val, -100.0, 100.0, 0.01, 0.1); + _dy_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-dy", _("Vert:"), _dy_adj, 0.1, 2)); + _dy_item->set_tooltip_text(_("Vertical kerning (px)")); + _dy_item->set_custom_numeric_menu_data(values); + _dy_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _dy_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::dy_value_changed)); + _dy_item->set_sensitive(true); + _dy_item->set_icon(INKSCAPE_ICON("text_vert_kern")); + add(*_dy_item); + } + + /* Character rotation */ + { + std::vector<double> values = { -90, -45, -30, -15, 0, 15, 30, 45, 90, 180 }; + auto rotation_val = prefs->getDouble("/tools/text/rotation", 0.0); + _rotation_adj = Gtk::Adjustment::create(rotation_val, -180.0, 180.0, 0.1, 1.0); + _rotation_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("text-rotation", _("Rot:"), _rotation_adj, 0.1, 2)); + _rotation_item->set_tooltip_text(_("Character rotation (degrees)")); + _rotation_item->set_custom_numeric_menu_data(values); + _rotation_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _rotation_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TextToolbar::rotation_value_changed)); + _rotation_item->set_sensitive(); + _rotation_item->set_icon(INKSCAPE_ICON("text_rotation")); + add(*_rotation_item); + } + + + /* Writing mode (Horizontal, Vertical-LR, Vertical-RL) */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Horizontal"); + row[columns.col_tooltip ] = _("Horizontal text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Vertical — RL"); + row[columns.col_tooltip ] = _("Vertical text — lines: right to left"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Vertical — LR"); + row[columns.col_tooltip ] = _("Vertical text — lines: left to right"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-vertical-lr"); + row[columns.col_sensitive] = true; + + _writing_mode_item = + UI::Widget::ComboToolItem::create( _("Writing mode"), // Label + _("Block progression"), // Tooltip + "Not Used", // Icon + store ); // Tree store + _writing_mode_item->use_icon(true); + _writing_mode_item->use_label( false ); + gint mode = prefs->getInt("/tools/text/writing_mode", 0); + _writing_mode_item->set_active( mode ); + add(*_writing_mode_item); + _writing_mode_item->focus_on_click(false); + _writing_mode_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::writing_mode_changed)); + } + + + /* Text (glyph) orientation (Auto (mixed), Upright, Sideways) */ + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("Auto"); + row[columns.col_tooltip ] = _("Auto glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-auto"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Upright"); + row[columns.col_tooltip ] = _("Upright glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-upright"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("Sideways"); + row[columns.col_tooltip ] = _("Sideways glyph orientation"); + row[columns.col_icon ] = INKSCAPE_ICON("text-orientation-sideways"); + row[columns.col_sensitive] = true; + + _orientation_item = + UI::Widget::ComboToolItem::create(_("Text orientation"), // Label + _("Text (glyph) orientation in vertical text."), // Tooltip + "Not Used", // Icon + store ); // List store + _orientation_item->use_icon(true); + _orientation_item->use_label(false); + gint mode = prefs->getInt("/tools/text/text_orientation", 0); + _orientation_item->set_active( mode ); + _orientation_item->focus_on_click(false); + add(*_orientation_item); + + _orientation_item->signal_changed().connect(sigc::mem_fun(*this, &TextToolbar::orientation_changed)); + } + + // Text direction (predominant direction of horizontal text). + { + UI::Widget::ComboToolItemColumns columns; + + Glib::RefPtr<Gtk::ListStore> store = Gtk::ListStore::create(columns); + + Gtk::TreeModel::Row row; + + row = *(store->append()); + row[columns.col_label ] = _("LTR"); + row[columns.col_tooltip ] = _("Left to right text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-horizontal"); + row[columns.col_sensitive] = true; + + row = *(store->append()); + row[columns.col_label ] = _("RTL"); + row[columns.col_tooltip ] = _("Right to left text"); + row[columns.col_icon ] = INKSCAPE_ICON("frmt-text-direction-r2l"); + row[columns.col_sensitive] = true; + + _direction_item = + UI::Widget::ComboToolItem::create( _("Text direction"), // Label + _("Text direction for normally horizontal text."), // Tooltip + "Not Used", // Icon + store ); // List store + _direction_item->use_icon(true); + _direction_item->use_label(false); + gint mode = prefs->getInt("/tools/text/text_direction", 0); + _direction_item->set_active( mode ); + _direction_item->focus_on_click(false); + add(*_direction_item); + + _direction_item->signal_changed_after().connect(sigc::mem_fun(*this, &TextToolbar::direction_changed)); + } + + show_all(); + + // we emit a selection change on tool switch to text + desktop->connectEventContextChanged(sigc::mem_fun(*this, &TextToolbar::watch_ec)); +} + +/* + * Set the style, depending on the inner or outer text being selected + */ +void TextToolbar::text_outer_set_style(SPCSSAttr *css) +{ + // Calling sp_desktop_set_style will result in a call to TextTool::_styleSet() which + // will set the style on selected text inside the <text> element. If we want to set + // the style on the outer <text> objects we need to bypass this call. + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(_outer) { + // Apply css to parent text objects directly. + for (auto i : desktop->getSelection()->items()) { + SPItem *item = dynamic_cast<SPItem *>(i); + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + // Scale by inverse of accumulated parent transform + SPCSSAttr *css_set = sp_repr_css_attr_new(); + sp_repr_css_merge(css_set, css); + Geom::Affine const local(item->i2doc_affine()); + double const ex(local.descrim()); + if ((ex != 0.0) && (ex != 1.0)) { + sp_css_attr_scale(css_set, 1 / ex); + } + recursively_set_properties(item, css_set); + sp_repr_css_attr_unref(css_set); + } + } + } else { + // Apply css to selected inner objects. + sp_desktop_set_style (desktop, css, true, false); + } +} + +void +TextToolbar::fontfamily_value_changed() +{ +#ifdef DEBUG_TEXT + std::cout << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl; + std::cout << "sp_text_fontfamily_value_changed: " << std::endl; +#endif + + // quit if run by the _changed callbacks + if (_freeze) { +#ifdef DEBUG_TEXT + std::cout << "sp_text_fontfamily_value_changed: frozen... return" << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n" << std::endl; +#endif + return; + } + _freeze = true; + + gchar *temp_family = _font_family_item->get_active_text(); + Glib::ustring new_family(temp_family); + g_free(temp_family); + css_font_family_unquote( new_family ); // Remove quotes around font family names. + + // TODO: Think about how to handle handle multiple selections. While + // the font-family may be the same for all, the styles might be different. + // See: TextEdit::onApply() for example of looping over selected items. + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); +#ifdef DEBUG_TEXT + std::cout << " Old family: " << fontlister->get_font_family() << std::endl; + std::cout << " New family: " << new_family << std::endl; + std::cout << " Old active: " << fontlister->get_font_family_row() << std::endl; + // std::cout << " New active: " << act->active << std::endl; +#endif + if( new_family.compare( fontlister->get_font_family() ) != 0 ) { + // Changed font-family + + if( _font_family_item->get_active() == -1 ) { + // New font-family, not in document, not on system (could be fallback list) + fontlister->insert_font_family( new_family ); + + // This just sets a variable in the ComboBoxEntryAction object... + // shouldn't we also set the actual active row in the combobox? + _font_family_item->set_active(0); // New family is always at top of list. + } + + fontlister->set_font_family( _font_family_item->get_active() ); + // active text set in sp_text_toolbox_selection_changed() + + SPCSSAttr *css = sp_repr_css_attr_new (); + fontlister->fill_css( css ); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if( desktop->getSelection()->isEmpty() ) { + // Update default + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // If there is a selection, update + sp_desktop_set_style (desktop, css, true, true); // Results in selection change called twice. + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change font family")); + } + sp_repr_css_attr_unref (css); + } + + // unfreeze + _freeze = false; + +#ifdef DEBUG_TEXT + std::cout << "sp_text_toolbox_fontfamily_changes: exit" << std::endl; + std::cout << "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM" << std::endl; + std::cout << std::endl; +#endif +} + +GtkWidget * +TextToolbar::create(SPDesktop *desktop) +{ + auto tb = Gtk::manage(new TextToolbar(desktop)); + return GTK_WIDGET(tb->gobj()); +} + +void +TextToolbar::fontsize_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gchar *text = _font_size_item->get_active_text(); + gchar *endptr; + gdouble size = g_strtod( text, &endptr ); + if (endptr == text) { // Conversion failed, non-numeric input. + g_warning( "Conversion of size text to double failed, input: %s\n", text ); + g_free( text ); + _freeze = false; + return; + } + g_free( text ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000); // somewhat arbitrary, but text&font preview freezes with too huge fontsizes + + if (size > max_size) + size = max_size; + + // Set css font size. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + if (prefs->getBool("/options/font/textOutputPx", true)) { + osfs << sp_style_css_size_units_to_px(size, unit) << sp_style_get_css_unit_string(SP_CSS_UNIT_PX); + } else { + osfs << size << sp_style_get_css_unit_string(unit); + } + sp_repr_css_set_property (css, "font-size", osfs.str().c_str()); + double factor = size / selection_fontsize; + + // Apply font size to selected objects. + text_outer_set_style(css); + + Unit const *unit_lh = _tracker->getActiveUnit(); + g_return_if_fail(unit_lh != nullptr); + if (!is_relative(unit_lh) && _outer) { + double lineheight = _line_height_adj->get_value(); + _freeze = false; + _line_height_adj->set_value(lineheight * factor); + _freeze = true; + } + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + sp_desktop_set_style(_desktop, css, true, true); + // Save for undo + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:size", SP_VERB_NONE, + _("Text: Change font size")); + } + + sp_repr_css_attr_unref(css); + + _freeze = false; +} + +void +TextToolbar::fontstyle_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + Glib::ustring new_style = _font_style_item->get_active_text(); + + Inkscape::FontLister* fontlister = Inkscape::FontLister::get_instance(); + + if( new_style.compare( fontlister->get_font_style() ) != 0 ) { + + fontlister->set_font_style( new_style ); + // active text set in sp_text_toolbox_seletion_changed() + + SPCSSAttr *css = sp_repr_css_attr_new (); + fontlister->fill_css( css ); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + sp_desktop_set_style (desktop, css, true, true); + + + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_style = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + if (result_style == QUERY_STYLE_NOTHING) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // Save for undo + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change font style")); + } + + sp_repr_css_attr_unref (css); + + } + + _freeze = false; +} + +// Handles both Superscripts and Subscripts +void +TextToolbar::script_changed(Gtk::ToggleToolButton *btn) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + + _freeze = true; + + // Called by Superscript or Subscript button? + auto name = btn->get_name(); + gint prop = (btn == _superscript_item) ? 0 : 1; + +#ifdef DEBUG_TEXT + std::cout << "TextToolbar::script_changed: " << prop << std::endl; +#endif + + // Query baseline + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_baseline = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_BASELINES); + + bool setSuper = false; + bool setSub = false; + + if (Inkscape::is_query_style_updateable(result_baseline)) { + // If not set or mixed, turn on superscript or subscript + if( prop == 0 ) { + setSuper = true; + } else { + setSub = true; + } + } else { + // Superscript + gboolean superscriptSet = (query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER ); + + // Subscript + gboolean subscriptSet = (query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB ); + + setSuper = !superscriptSet && prop == 0; + setSub = !subscriptSet && prop == 1; + } + + // Set css properties + SPCSSAttr *css = sp_repr_css_attr_new (); + if( setSuper || setSub ) { + // Openoffice 2.3 and Adobe use 58%, Microsoft Word 2002 uses 65%, LaTex about 70%. + // 58% looks too small to me, especially if a superscript is placed on a superscript. + // If you make a change here, consider making a change to baseline-shift amount + // in style.cpp. + sp_repr_css_set_property (css, "font-size", "65%"); + } else { + sp_repr_css_set_property (css, "font-size", ""); + } + if( setSuper ) { + sp_repr_css_set_property (css, "baseline-shift", "super"); + } else if( setSub ) { + sp_repr_css_set_property (css, "baseline-shift", "sub"); + } else { + sp_repr_css_set_property (css, "baseline-shift", "baseline"); + } + + // Apply css to selected objects. + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + sp_desktop_set_style (desktop, css, true, false); + + // Save for undo + if(result_baseline != QUERY_STYLE_NOTHING) { + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:script", SP_VERB_NONE, + _("Text: Change superscript or subscript")); + } + _freeze = false; +} + +void +TextToolbar::align_mode_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/text/align_mode", mode); + + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + // move the x of all texts to preserve the same bbox + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist= selection->items(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text) { + SPItem *item = i; + + unsigned writing_mode = item->style->writing_mode.value; + // below, variable names suggest horizontal move, but we check the writing direction + // and move in the corresponding axis + Geom::Dim2 axis; + if (writing_mode == SP_CSS_WRITING_MODE_LR_TB || writing_mode == SP_CSS_WRITING_MODE_RL_TB) { + axis = Geom::X; + } else { + axis = Geom::Y; + } + + Geom::OptRect bbox = item->geometricBounds(); + if (!bbox) + continue; + double width = bbox->dimensions()[axis]; + // If you want to align within some frame, other than the text's own bbox, calculate + // the left and right (or top and bottom for tb text) slacks of the text inside that + // frame (currently unused) + double left_slack = 0; + double right_slack = 0; + unsigned old_align = item->style->text_align.value; + double move = 0; + if (old_align == SP_CSS_TEXT_ALIGN_START || old_align == SP_CSS_TEXT_ALIGN_LEFT) { + switch (mode) { + case 0: + move = -left_slack; + break; + case 1: + move = width/2 + (right_slack - left_slack)/2; + break; + case 2: + move = width + right_slack; + break; + } + } else if (old_align == SP_CSS_TEXT_ALIGN_CENTER) { + switch (mode) { + case 0: + move = -width/2 - left_slack; + break; + case 1: + move = (right_slack - left_slack)/2; + break; + case 2: + move = width/2 + right_slack; + break; + } + } else if (old_align == SP_CSS_TEXT_ALIGN_END || old_align == SP_CSS_TEXT_ALIGN_RIGHT) { + switch (mode) { + case 0: + move = -width - left_slack; + break; + case 1: + move = -width/2 + (right_slack - left_slack)/2; + break; + case 2: + move = right_slack; + break; + } + } + Geom::Point XY = SP_TEXT(item)->attributes.firstXY(); + if (axis == Geom::X) { + XY = XY + Geom::Point (move, 0); + } else { + XY = XY + Geom::Point (0, move); + } + SP_TEXT(item)->attributes.setFirstXY(XY); + item->updateRepr(); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "text-anchor", "start"); + sp_repr_css_set_property (css, "text-align", "start"); + break; + } + case 1: + { + sp_repr_css_set_property (css, "text-anchor", "middle"); + sp_repr_css_set_property (css, "text-align", "center"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "text-anchor", "end"); + sp_repr_css_set_property (css, "text-align", "end"); + break; + } + + case 3: + { + sp_repr_css_set_property (css, "text-anchor", "start"); + sp_repr_css_set_property (css, "text-align", "justify"); + break; + } + } + + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (desktop, css, true, true); + if (result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change alignment")); + } + sp_repr_css_attr_unref (css); + + gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas)); + + _freeze = false; +} + +void +TextToolbar::writing_mode_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "writing-mode", "lr-tb"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "writing-mode", "tb-rl"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "writing-mode", "vertical-lr"); + break; + } + } + + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change writing mode")); + } + sp_repr_css_attr_unref (css); + + gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas)); + + _freeze = false; +} + +void +TextToolbar::orientation_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "text-orientation", "auto"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "text-orientation", "upright"); + break; + } + + case 2: + { + sp_repr_css_set_property (css, "text-orientation", "sideways"); + break; + } + } + + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change orientation")); + } + sp_repr_css_attr_unref (css); + + gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas)); + + _freeze = false; +} + +void +TextToolbar::direction_changed(int mode) +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + switch (mode) + { + case 0: + { + sp_repr_css_set_property (css, "direction", "ltr"); + break; + } + + case 1: + { + sp_repr_css_set_property (css, "direction", "rtl"); + break; + } + } + + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // If querying returned nothing, update default style. + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_desktop_set_style (SP_ACTIVE_DESKTOP, css, true, true); + if(result_numbers != QUERY_STYLE_NOTHING) + { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Text: Change direction")); + } + sp_repr_css_attr_unref (css); + + gtk_widget_grab_focus (GTK_WIDGET(SP_ACTIVE_DESKTOP->canvas)); + + _freeze = false; +} + +void +TextToolbar::lineheight_value_changed() +{ + // quit if run by the _changed callbacks or is not text tool + if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) { + return; + } + + _freeze = true; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + // Get user selected unit and save as preference + Unit const *unit = _tracker->getActiveUnit(); + // @Tav same disabled unit + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit so + // we can save it (allows us to adjust line height value when unit changes). + + // Set css line height. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + if ( is_relative(unit) ) { + osfs << _line_height_adj->get_value() << unit->abbr; + } else { + // Inside SVG file, always use "px" for absolute units. + osfs << Quantity::convert(_line_height_adj->get_value(), unit, "px") << "px"; + } + + sp_repr_css_set_property (css, "line-height", osfs.str().c_str()); + + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist = selection->items(); + if (_outer) { + // Special else makes this different from other uses of text_outer_set_style + text_outer_set_style(css); + } else { + SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin()); + SPStyle *parent_style = parent->style; + SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET); + Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25"); + SPCSSAttr *cssfit = sp_repr_css_attr_new(); + sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str()); + double minheight = 0; + if (parent_style) { + minheight = parent_style->line_height.computed; + } + if (minheight) { + for (auto i : parent->childList(false)) { + SPItem *child = dynamic_cast<SPItem *>(i); + if (!child) { + continue; + } + recursively_set_properties(child, cssfit); + } + } + sp_repr_css_set_property(cssfit, "line-height", "0"); + parent->changeCSS(cssfit, "style"); + subselection_wrap_toggle(true); + sp_desktop_set_style(desktop, css, true, true); + subselection_wrap_toggle(false); + sp_repr_css_attr_unref(cssfit); + } + // Only need to save for undo if a text item has been changed. + itemlist = selection->items(); + bool modmade = false; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + modmade = true; + break; + } + } + + // Save for undo + if (modmade) { + // Call ensureUpToDate() causes rebuild of text layout (with all proper style + // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly + // save new <tspan> 'x' and 'y' attribute values by calling updateRepr(). + // Partial fix for bug #1590141. + + desktop->getDocument()->ensureUpToDate(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + (i)->updateRepr(); + } + } + if (!_outer) { + prepare_inner(); + } + DocumentUndo::maybeDone(desktop->getDocument(), "ttb:line-height", SP_VERB_NONE, _("Text: Change line-height")); + } + + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::lineheight_unit_changed(int /* Not Used */) +{ + // quit if run by the _changed callbacks or is not text tool + if (_freeze || !SP_IS_TEXT_CONTEXT(_desktop->event_context)) { + return; + } + _freeze = true; + + // Get old saved unit + int old_unit = _lineheight_unit; + + // Get user selected unit and save as preference + Unit const *unit = _tracker->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit. + SPILength temp_length; + Inkscape::CSSOStringStream temp_stream; + temp_stream << 1 << unit->abbr; + temp_length.read(temp_stream.str().c_str()); + prefs->setInt("/tools/text/lineheight/display_unit", temp_length.unit); + if (old_unit == temp_length.unit) { + _freeze = false; + return; + } + + // Read current line height value + double line_height = _line_height_adj->get_value(); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = desktop->getSelection(); + auto itemlist = selection->items(); + + // Convert between units + double font_size = 0; + double doc_scale = 1; + int count = 0; + bool has_flow = false; + + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + doc_scale = Geom::Affine(i->i2dt_affine()).descrim(); + font_size += i->style->font_size.computed * doc_scale; + ++count; + } + if (flowtext) { + has_flow = true; + } + } + if (count > 0) { + font_size /= count; + } else { + font_size = 20; + } + if ((unit->abbr == "" || unit->abbr == "em") && (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_EM)) { + // Do nothing + } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_EX) { + line_height *= 0.5; + } else if ((unit->abbr) == "ex" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) { + line_height *= 2.0; + } else if ((unit->abbr == "" || unit->abbr == "em") && old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 100.0; + } else if ((unit->abbr) == "%" && (old_unit == SP_CSS_UNIT_EM || old_unit == SP_CSS_UNIT_NONE)) { + line_height *= 100; + } else if ((unit->abbr) == "ex" && old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 50.0; + } else if ((unit->abbr) == "%" && old_unit == SP_CSS_UNIT_EX) { + line_height *= 50; + } else if (is_relative(unit)) { + // Convert absolute to relative... for the moment use average font-size + if (old_unit == SP_CSS_UNIT_NONE) old_unit = SP_CSS_UNIT_EM; + line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), "px"); + + if (font_size > 0) { + line_height /= font_size; + } + if ((unit->abbr) == "%") { + line_height *= 100; + } else if ((unit->abbr) == "ex") { + line_height *= 2; + } + } else if (old_unit == SP_CSS_UNIT_NONE || old_unit == SP_CSS_UNIT_PERCENT || old_unit == SP_CSS_UNIT_EM || + old_unit == SP_CSS_UNIT_EX) { + // Convert relative to absolute... for the moment use average font-size + if (old_unit == SP_CSS_UNIT_PERCENT) { + line_height /= 100.0; + } else if (old_unit == SP_CSS_UNIT_EX) { + line_height /= 2.0; + } + line_height *= font_size; + line_height = Quantity::convert(line_height, "px", unit); + } else { + // Convert between different absolute units (only used in GUI) + line_height = Quantity::convert(line_height, sp_style_get_css_unit_string(old_unit), unit); + } + // Set css line height. + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + // Set css line height. + if ( is_relative(unit) ) { + osfs << line_height << unit->abbr; + } else { + osfs << Quantity::convert(line_height, unit, "px") << "px"; + } + sp_repr_css_set_property (css, "line-height", osfs.str().c_str()); + + // Update GUI with line_height value. + _line_height_adj->set_value(line_height); + // Update "climb rate" The custom action has a step property but no way to set it. + if (unit->abbr == "%") { + _line_height_adj->set_step_increment(1.0); + _line_height_adj->set_page_increment(10.0); + } else { + _line_height_adj->set_step_increment(0.1); + _line_height_adj->set_page_increment(1.0); + } + // Internal function to set line-height which is spacing mode dependent. + if (_outer) { + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + if (dynamic_cast<SPText *>(*i) || dynamic_cast<SPFlowtext *>(*i)) { + SPItem *item = *i; + // Scale by inverse of accumulated parent transform + SPCSSAttr *css_set = sp_repr_css_attr_new(); + sp_repr_css_merge(css_set, css); + Geom::Affine const local(item->i2doc_affine()); + double const ex(local.descrim()); + if ((ex != 0.0) && (ex != 1.0)) { + sp_css_attr_scale(css_set, 1 / ex); + } + recursively_set_properties(item, css_set); + sp_repr_css_attr_unref(css_set); + } + } + } else { + SPItem *parent = dynamic_cast<SPItem *>(*itemlist.begin()); + SPStyle *parent_style = parent->style; + SPCSSAttr *parent_cssatr = sp_css_attr_from_style(parent_style, SP_STYLE_FLAG_IFSET); + Glib::ustring parent_lineheight = sp_repr_css_property(parent_cssatr, "line-height", "1.25"); + SPCSSAttr *cssfit = sp_repr_css_attr_new(); + sp_repr_css_set_property(cssfit, "line-height", parent_lineheight.c_str()); + double minheight = 0; + if (parent_style) { + minheight = parent_style->line_height.computed; + } + if (minheight) { + for (auto i : parent->childList(false)) { + SPItem *child = dynamic_cast<SPItem *>(i); + if (!child) { + continue; + } + recursively_set_properties(child, cssfit); + } + } + sp_repr_css_set_property(cssfit, "line-height", "0"); + parent->changeCSS(cssfit, "style"); + subselection_wrap_toggle(true); + sp_desktop_set_style(desktop, css, true, true); + subselection_wrap_toggle(false); + sp_repr_css_attr_unref(cssfit); + } + itemlist= selection->items(); + // Only need to save for undo if a text item has been changed. + bool modmade = false; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + modmade = true; + break; + } + } + // Save for undo + if(modmade) { + // Call ensureUpToDate() causes rebuild of text layout (with all proper style + // cascading, etc.). For multi-line text with sodipodi::role="line", we must explicitly + // save new <tspan> 'x' and 'y' attribute values by calling updateRepr(). + // Partial fix for bug #1590141. + + desktop->getDocument()->ensureUpToDate(); + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + (i)->updateRepr(); + } + } + if (_outer) { + prepare_inner(); + } + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:line-height", SP_VERB_NONE, + _("Text: Change line-height unit")); + } + + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void TextToolbar::fontsize_unit_changed(int /* Not Used */) +{ + // quit if run by the _changed callbacks + Unit const *unit = _tracker_fs->getActiveUnit(); + g_return_if_fail(unit != nullptr); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // This nonsense is to get SP_CSS_UNIT_xx value corresponding to unit. + SPILength temp_size; + Inkscape::CSSOStringStream temp_size_stream; + temp_size_stream << 1 << unit->abbr; + temp_size.read(temp_size_stream.str().c_str()); + prefs->setInt("/options/font/unitType", temp_size.unit); + selection_changed(SP_ACTIVE_DESKTOP->selection); +} + +void +TextToolbar::wordspacing_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + // At the moment this handles only numerical values (i.e. no em unit). + // Set css word-spacing + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + osfs << _word_spacing_adj->get_value() << "px"; // For now always use px + sp_repr_css_set_property (css, "word-spacing", osfs.str().c_str()); + text_outer_set_style(css); + + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } else { + // Save for undo + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:word-spacing", SP_VERB_NONE, + _("Text: Change word-spacing")); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::letterspacing_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + // At the moment this handles only numerical values (i.e. no em unit). + // Set css letter-spacing + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream osfs; + osfs << _letter_spacing_adj->get_value() << "px"; // For now always use px + sp_repr_css_set_property (css, "letter-spacing", osfs.str().c_str()); + text_outer_set_style(css); + + // If no selected objects, set default. + SPStyle query(SP_ACTIVE_DOCUMENT); + int result_numbers = + sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + if (result_numbers == QUERY_STYLE_NOTHING) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->mergeStyle("/tools/text/style", css); + } + else + { + // Save for undo + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:letter-spacing", SP_VERB_NONE, + _("Text: Change letter-spacing")); + } + + sp_repr_css_attr_unref (css); + + _freeze = false; +} + +void +TextToolbar::dx_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_dx = _dx_adj->get_value(); + bool modmade = false; + + if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_dx = attributes->getDx( char_index ); + double delta_dx = new_dx - old_dx; + sp_te_adjust_dx( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_dx ); + modmade = true; + } + } + } + + if(modmade) { + // Save for undo + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:dx", SP_VERB_NONE, + _("Text: Change dx (kern)")); + } + _freeze = false; +} + +void +TextToolbar::dy_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_dy = _dy_adj->get_value(); + bool modmade = false; + + if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_dy = attributes->getDy( char_index ); + double delta_dy = new_dy - old_dy; + sp_te_adjust_dy( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_dy ); + modmade = true; + } + } + } + + if(modmade) { + // Save for undo + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:dy", SP_VERB_NONE, + _("Text: Change dy")); + } + + _freeze = false; +} + +void +TextToolbar::rotation_value_changed() +{ + // quit if run by the _changed callbacks + if (_freeze) { + return; + } + _freeze = true; + + gdouble new_degrees = _rotation_adj->get_value(); + + bool modmade = false; + if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + double old_degrees = attributes->getRotate( char_index ); + double delta_deg = new_degrees - old_degrees; + sp_te_adjust_rotation( tc->text, tc->text_sel_start, tc->text_sel_end, SP_ACTIVE_DESKTOP, delta_deg ); + modmade = true; + } + } + } + + // Save for undo + if(modmade) { + DocumentUndo::maybeDone(SP_ACTIVE_DESKTOP->getDocument(), "ttb:rotate", SP_VERB_NONE, + _("Text: Change rotate")); + } + + _freeze = false; +} + +void TextToolbar::selection_modified_select_tool(Inkscape::Selection *selection, guint flags) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double factor = prefs->getDouble("/options/font/scaleLineHeightFromFontSIze", 1.0); + if (factor != 1.0) { + Unit const *unit_lh = _tracker->getActiveUnit(); + g_return_if_fail(unit_lh != nullptr); + if (!is_relative(unit_lh) && _outer) { + double lineheight = _line_height_adj->get_value(); + bool is_freeze = _freeze; + _freeze = false; + _line_height_adj->set_value(lineheight * factor); + _freeze = is_freeze; + } + prefs->setDouble("/options/font/scaleLineHeightFromFontSIze", 1.0); + } +} + +void TextToolbar::selection_changed(Inkscape::Selection *selection) // don't bother to update font list if subsel + // changed +{ +#ifdef DEBUG_TEXT + static int count = 0; + ++count; + std::cout << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << "sp_text_toolbox_selection_changed: start " << count << std::endl; +#endif + + // quit if run by the _changed callbacks + if (_freeze) { + +#ifdef DEBUG_TEXT + std::cout << " Frozen, returning" << std::endl; + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + return; + } + _freeze = true; + + // selection defined as argument but not used, argh!!! + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *document = SP_ACTIVE_DOCUMENT; + selection = desktop->getSelection(); + auto itemlist = selection->items(); + +#ifdef DEBUG_TEXT + for(auto i : itemlist) { + const gchar* id = i->getId(); + std::cout << " " << id << std::endl; + } + Glib::ustring selected_text = sp_text_get_selected_text((SP_ACTIVE_DESKTOP)->event_context); + std::cout << " Selected text: |" << selected_text << "|" << std::endl; +#endif + + // Only flowed text can be justified, only normal text can be kerned... + // Find out if we have flowed text now so we can use it several places + gboolean isFlow = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::vector<SPItem *> to_work; + for (auto i : itemlist) { + SPText *text = dynamic_cast<SPText *>(i); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(i); + if (text || flowtext) { + to_work.push_back(i); + } + if (flowtext || + (text && text->style && text->style->shape_inside.set)) { + isFlow = true; + } + } + bool outside = false; + if (selection && to_work.size() == 0) { + outside = true; + } + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->selection_update(); + // Update font list, but only if widget already created. + if (_font_family_item->get_combobox() != nullptr) { + _font_family_item->set_active_text(fontlister->get_font_family().c_str(), fontlister->get_font_family_row()); + _font_style_item->set_active_text(fontlister->get_font_style().c_str()); + } + + /* + * Query from current selection: + * Font family (font-family) + * Style (font-weight, font-style, font-stretch, font-variant, font-align) + * Numbers (font-size, letter-spacing, word-spacing, line-height, text-anchor, writing-mode) + * Font specification (Inkscape private attribute) + */ + SPStyle query(document); + SPStyle query_fallback(document); + int result_family = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTFAMILY); + int result_style = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + int result_baseline = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_BASELINES); + int result_wmode = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_WRITINGMODES); + + // Calling sp_desktop_query_style will result in a call to TextTool::_styleQueried(). + // This returns the style of the selected text inside the <text> element... which + // is often the style of one or more <tspan>s. If we want the style of the outer + // <text> objects then we need to bypass the call to TextTool::_styleQueried(). + // The desktop selection never includes the elements inside the <text> element. + int result_numbers = 0; + int result_numbers_fallback = 0; + if (!outside) { + if (_outer && this->_sub_active_item) { + std::vector<SPItem *> qactive{ this->_sub_active_item }; + SPItem *parent = dynamic_cast<SPItem *>(this->_sub_active_item->parent); + std::vector<SPItem *> qparent{ parent }; + result_numbers = + sp_desktop_query_style_from_list(qactive, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + result_numbers_fallback = + sp_desktop_query_style_from_list(qparent, &query_fallback, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } else if (_outer) { + result_numbers = sp_desktop_query_style_from_list(to_work, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } else { + result_numbers = sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } + } else { + result_numbers = + sp_desktop_query_style(desktop, &query, QUERY_STYLE_PROPERTY_FONTNUMBERS); + } + + /* + * If no text in selection (querying returned nothing), read the style from + * the /tools/text preferences (default style for new texts). Return if + * tool bar already set to these preferences. + */ + if (result_family == QUERY_STYLE_NOTHING || + result_style == QUERY_STYLE_NOTHING || + result_numbers == QUERY_STYLE_NOTHING || + result_wmode == QUERY_STYLE_NOTHING ) { + // There are no texts in selection, read from preferences. + query.readFromPrefs("/tools/text"); +#ifdef DEBUG_TEXT + std::cout << " read style from prefs:" << std::endl; + sp_print_font( &query ); +#endif + if (_text_style_from_prefs) { + // Do not reset the toolbar style from prefs if we already did it last time + _freeze = false; +#ifdef DEBUG_TEXT + std::cout << " text_style_from_prefs: toolbar already set" << std:: endl; + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + return; + } + + // To ensure the value of the combobox is properly set on start-up, only mark + // the prefs set if the combobox has already been constructed. + if( _font_family_item->get_combobox() != nullptr ) { + _text_style_from_prefs = true; + } + } else { + _text_style_from_prefs = false; + } + + // If we have valid query data for text (font-family, font-specification) set toolbar accordingly. + { + // Size (average of text selected) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + double size = 0; + if (!size && _cusor_numbers != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(_query_cursor.font_size.computed, unit); + } + if (!size && result_numbers != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(query.font_size.computed, unit); + } + if (!size && result_numbers_fallback != QUERY_STYLE_NOTHING) { + size = sp_style_css_size_px_to_units(query_fallback.font_size.computed, unit); + } + + auto unit_str = sp_style_get_css_unit_string(unit); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", unit_str, ")"); + + _font_size_item->set_tooltip(tooltip.c_str()); + + Inkscape::CSSOStringStream os; + // We dot want to parse values just show + + _tracker_fs->setActiveUnitByAbbr(sp_style_get_css_unit_string(unit)); + int rounded_size = std::round(size); + if (std::abs((size - rounded_size)/size) < 0.0001) { + // We use rounded_size to avoid rounding errors when, say, converting stored 'px' values to displayed 'pt' values. + os << rounded_size; + selection_fontsize = rounded_size; + } else { + os << size; + selection_fontsize = size; + } + + // Freeze to ignore callbacks. + //g_object_freeze_notify( G_OBJECT( fontSizeAction->combobox ) ); + sp_text_set_sizes(GTK_LIST_STORE(_font_size_item->get_model()), unit); + //g_object_thaw_notify( G_OBJECT( fontSizeAction->combobox ) ); + + _font_size_item->set_active_text( os.str().c_str() ); + + // Superscript + gboolean superscriptSet = + ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) && + query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUPER ); + + _superscript_item->set_active(superscriptSet); + + // Subscript + gboolean subscriptSet = + ((result_baseline == QUERY_STYLE_SINGLE || result_baseline == QUERY_STYLE_MULTIPLE_SAME ) && + query.baseline_shift.set && + query.baseline_shift.type == SP_BASELINE_SHIFT_LITERAL && + query.baseline_shift.literal == SP_CSS_BASELINE_SHIFT_SUB ); + + _subscript_item->set_active(subscriptSet); + + // Alignment + + // Note: SVG 1.1 doesn't include text-align, SVG 1.2 Tiny doesn't include text-align="justify" + // text-align="justify" was a draft SVG 1.2 item (along with flowed text). + // Only flowed text can be left and right justified at the same time. + // Disable button if we don't have flowed text. + + Glib::RefPtr<Gtk::ListStore> store = _align_item->get_store(); + Gtk::TreeModel::Row row = *(store->get_iter("3")); // Justify entry + UI::Widget::ComboToolItemColumns columns; + row[columns.col_sensitive] = isFlow; + + int activeButton = 0; + if (query.text_align.computed == SP_CSS_TEXT_ALIGN_JUSTIFY) + { + activeButton = 3; + } else { + // This should take 'direction' into account + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_START) activeButton = 0; + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_MIDDLE) activeButton = 1; + if (query.text_anchor.computed == SP_CSS_TEXT_ANCHOR_END) activeButton = 2; + } + _align_item->set_active( activeButton ); + + double height = 0; + gint line_height_unit = 0; + + if (!height && _cusor_numbers != QUERY_STYLE_NOTHING) { + height = _query_cursor.line_height.value; + line_height_unit = _query_cursor.line_height.unit; + } + + if (!height && result_numbers != QUERY_STYLE_NOTHING) { + height = query.line_height.value; + line_height_unit = query.line_height.unit; + } + + if (!height && result_numbers_fallback != QUERY_STYLE_NOTHING) { + height = query_fallback.line_height.value; + line_height_unit = query_fallback.line_height.unit; + } + + if (line_height_unit == SP_CSS_UNIT_PERCENT) { + height *= 100.0; // Inkscape store % as fraction in .value + } + + // We dot want to parse values just show + if (!is_relative(SPCSSUnit(line_height_unit))) { + gint curunit = prefs->getInt("/tools/text/lineheight/display_unit", 1); + // For backwards comaptibility + if (is_relative(SPCSSUnit(curunit))) { + prefs->setInt("/tools/text/lineheight/display_unit", 1); + curunit = 1; + } + height = Quantity::convert(height, "px", sp_style_get_css_unit_string(curunit)); + line_height_unit = curunit; + } + _line_height_adj->set_value(height); + + + // Update "climb rate" + if (line_height_unit == SP_CSS_UNIT_PERCENT) { + _line_height_adj->set_step_increment(1.0); + _line_height_adj->set_page_increment(10.0); + } else { + _line_height_adj->set_step_increment(0.1); + _line_height_adj->set_page_increment(1.0); + } + + if( line_height_unit == SP_CSS_UNIT_NONE ) { + // Function 'sp_style_get_css_unit_string' returns 'px' for unit none. + // We need to avoid this. + _tracker->setActiveUnitByAbbr(""); + } else { + _tracker->setActiveUnitByAbbr(sp_style_get_css_unit_string(line_height_unit)); + } + + // Save unit so we can do conversions between new/old units. + _lineheight_unit = line_height_unit; + // Word spacing + double wordSpacing; + if (query.word_spacing.normal) wordSpacing = 0.0; + else wordSpacing = query.word_spacing.computed; // Assume no units (change in desktop-style.cpp) + + _word_spacing_adj->set_value(wordSpacing); + + // Letter spacing + double letterSpacing; + if (query.letter_spacing.normal) letterSpacing = 0.0; + else letterSpacing = query.letter_spacing.computed; // Assume no units (change in desktop-style.cpp) + + _letter_spacing_adj->set_value(letterSpacing); + + // Writing mode + int activeButton2 = 0; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_LR_TB) activeButton2 = 0; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_RL) activeButton2 = 1; + if (query.writing_mode.computed == SP_CSS_WRITING_MODE_TB_LR) activeButton2 = 2; + + _writing_mode_item->set_active( activeButton2 ); + + // Orientation + int activeButton3 = 0; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_MIXED ) activeButton3 = 0; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_UPRIGHT ) activeButton3 = 1; + if (query.text_orientation.computed == SP_CSS_TEXT_ORIENTATION_SIDEWAYS) activeButton3 = 2; + + _orientation_item->set_active( activeButton3 ); + + // Disable text orientation for horizontal text... + _orientation_item->set_sensitive( activeButton2 != 0 ); + + // Direction + int activeButton4 = 0; + if (query.direction.computed == SP_CSS_DIRECTION_LTR ) activeButton4 = 0; + if (query.direction.computed == SP_CSS_DIRECTION_RTL ) activeButton4 = 1; + _direction_item->set_active( activeButton4 ); + } + +#ifdef DEBUG_TEXT + std::cout << " GUI: fontfamily.value: " << query.font_family.value() << std::endl; + std::cout << " GUI: font_size.computed: " << query.font_size.computed << std::endl; + std::cout << " GUI: font_weight.computed: " << query.font_weight.computed << std::endl; + std::cout << " GUI: font_style.computed: " << query.font_style.computed << std::endl; + std::cout << " GUI: text_anchor.computed: " << query.text_anchor.computed << std::endl; + std::cout << " GUI: text_align.computed: " << query.text_align.computed << std::endl; + std::cout << " GUI: line_height.computed: " << query.line_height.computed + << " line_height.value: " << query.line_height.value + << " line_height.unit: " << query.line_height.unit << std::endl; + std::cout << " GUI: word_spacing.computed: " << query.word_spacing.computed + << " word_spacing.value: " << query.word_spacing.value + << " word_spacing.unit: " << query.word_spacing.unit << std::endl; + std::cout << " GUI: letter_spacing.computed: " << query.letter_spacing.computed + << " letter_spacing.value: " << query.letter_spacing.value + << " letter_spacing.unit: " << query.letter_spacing.unit << std::endl; + std::cout << " GUI: writing_mode.computed: " << query.writing_mode.computed << std::endl; +#endif + + // Kerning (xshift), yshift, rotation. NB: These are not CSS attributes. + if( SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context) ) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if( tc ) { + unsigned char_index = -1; + TextTagAttributes *attributes = + text_tag_attributes_at_position( tc->text, std::min(tc->text_sel_start, tc->text_sel_end), &char_index ); + if( attributes ) { + + // Dx + double dx = attributes->getDx( char_index ); + _dx_adj->set_value(dx); + + // Dy + double dy = attributes->getDy( char_index ); + _dy_adj->set_value(dy); + + // Rotation + double rotation = attributes->getRotate( char_index ); + /* SVG value is between 0 and 360 but we're using -180 to 180 in widget */ + if( rotation > 180.0 ) rotation -= 360.0; + _rotation_adj->set_value(rotation); + +#ifdef DEBUG_TEXT + std::cout << " GUI: Dx: " << dx << std::endl; + std::cout << " GUI: Dy: " << dy << std::endl; + std::cout << " GUI: Rotation: " << rotation << std::endl; +#endif + } + } + } + + { + // Set these here as we don't always have kerning/rotating attributes + _dx_item->set_sensitive(!isFlow); + _dy_item->set_sensitive(!isFlow); + _rotation_item->set_sensitive(!isFlow); + } + +#ifdef DEBUG_TEXT + std::cout << "sp_text_toolbox_selection_changed: exit " << count << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif + + _freeze = false; +} + +void +TextToolbar::watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec) { + bool is_text_toolbar = SP_IS_TEXT_CONTEXT(ec); + bool is_select_toolbar = !is_text_toolbar && SP_IS_SELECT_CONTEXT(ec); + if (is_text_toolbar) { + // Watch selection + // Ensure FontLister is updated here first.................. + c_selection_changed = + desktop->getSelection()->connectChangedFirst(sigc::mem_fun(*this, &TextToolbar::selection_changed)); + c_selection_modified = desktop->getSelection()->connectModifiedFirst(sigc::mem_fun(*this, &TextToolbar::selection_modified)); + c_subselection_changed = desktop->connectToolSubselectionChanged(sigc::mem_fun(*this, &TextToolbar::subselection_changed)); + this->_sub_active_item = nullptr; + selection_changed(desktop->getSelection()); + } else if (is_select_toolbar) { + c_selection_modified_select_tool = desktop->getSelection()->connectModifiedFirst( + sigc::mem_fun(*this, &TextToolbar::selection_modified_select_tool)); + } + + + if (!is_text_toolbar) { + c_selection_changed.disconnect(); + c_selection_modified.disconnect(); + c_subselection_changed.disconnect(); + } + + if (!is_select_toolbar) { + c_selection_modified_select_tool.disconnect(); + } +} + +void +TextToolbar::selection_modified(Inkscape::Selection *selection, guint /*flags*/) +{ + this->_sub_active_item = nullptr; + selection_changed(selection); + +} + +void TextToolbar::subselection_wrap_toggle(bool start) +{ + if (SP_IS_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context)) { + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if (tc) { + _updating = true; + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { + Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start; + Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end; + tc->text_sel_start = wrap_start; + tc->text_sel_end = wrap_end; + wrap_start = start_selection; + wrap_end = end_selection; + } + _updating = start; + } + } +} + +/* +* This function parse the just created line height in one or more lines of a text subselection +* can recibe 2 kinds of input because when we store a text element we apply a fallback that change +* structure. This visualy is not reflected but user maybe want to change a part of this subselection +* once the falback is created, so we need more complex here to fill the gap. +* Basicaly we have a line height changed in the new wraper element/s between wrap_start and wrap_end +* this variables store starting iterator of first char in line and last char in line in a subselection +* this elements are styled well but we can have orphands text nodes before and after the subselection. +* this normaly 3 elements are inside a container as direct child of a text element, so we need to apply +* the container style to the optional previous and last text nodes warping into a new element that get the +* container style (this are not part to the subsselection). After wrap we unindent all child of the container and +* remove the container +* +*/ +void TextToolbar::prepare_inner() +{ + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT((SP_ACTIVE_DESKTOP)->event_context); + if (tc) { + Inkscape::Text::Layout *layout = const_cast<Inkscape::Text::Layout *>(te_get_layout(tc->text)); + if (layout) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *spobject = dynamic_cast<SPObject *>(tc->text); + SPItem *spitem = dynamic_cast<SPItem *>(tc->text); + SPText *text = dynamic_cast<SPText *>(tc->text); + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + if (!spobject) { + return; + } + + // We check for external files with text nodes direct children of text elemet + // and wrap it into a tspan elements as inkscape do. + if (text) { + std::vector<SPObject *> childs = spitem->childList(false); + for (auto child : childs) { + SPString *spstring = dynamic_cast<SPString *>(child); + if (spstring) { + Glib::ustring content = spstring->string; + if (content != "\n") { + Inkscape::XML::Node *rstring = xml_doc->createTextNode(content.c_str()); + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + Inkscape::XML::Node *rnl = xml_doc->createTextNode("\n"); + rtspan->setAttribute("sodipodi:role", "line"); + rtspan->addChild(rstring, nullptr); + text->getRepr()->addChild(rtspan, child->getRepr()); + Inkscape::GC::release(rstring); + Inkscape::GC::release(rtspan); + text->getRepr()->removeChild(spstring->getRepr()); + } + } + } + } + + // Here we remove temporary the shape to allow layout calculate where are the warp_end and warpo_start + // position if one of this are hiden because the previous line height changed + if (text) { + text->hide_shape_inside(); + } + if (flowtext) { + flowtext->fix_overflow_flowregion(false); + } + SPObject *rawptr_start = nullptr; + SPObject *rawptr_end = nullptr; + layout->getSourceOfCharacter(wrap_start, &rawptr_start); + layout->getSourceOfCharacter(wrap_end, &rawptr_end); + if (text) { + text->show_shape_inside(); + } + if (flowtext) { + flowtext->fix_overflow_flowregion(true); + } + if (!rawptr_start || !rawptr_end || !SP_IS_OBJECT(rawptr_start)|| !SP_IS_OBJECT(rawptr_end)) { + return; + } + SPObject *startobj = reinterpret_cast<SPObject *>(rawptr_start); + SPObject *endobj = reinterpret_cast<SPObject *>(rawptr_end); + // here we try to slect the elements where we need to work + // looping parent while text element in start and end + // and geting this and the inbetween elements + SPObject *start = startobj; + SPObject *end = endobj; + while (start->parent != spobject) + { + start = start->parent; + } + while (end->parent != spobject) + { + end = end->parent; + } + std::vector<SPObject *> containers; + while (start && start != end) { + containers.push_back(start); + start = start->getNext(); + } + if (start) { + containers.push_back(start); + } + for (auto container : containers) { + //we store the parent style to apply to the childs unselected + const gchar * style = container->getRepr()->attribute("style"); + Inkscape::XML::Node *prevchild = container->getRepr(); + std::vector<SPObject*> childs = container->childList(false); + for (auto child : childs) { + SPString *spstring = dynamic_cast<SPString *>(child); + SPFlowtspan *flowtspan = dynamic_cast<SPFlowtspan *>(child); + SPTSpan *tspan = dynamic_cast<SPTSpan *>(child); + // we need to upper all flowtspans to container level + // to this we need to change the element from flowspan to flowpara + if (flowtspan) { + Inkscape::XML::Node *flowpara = xml_doc->createElement("svg:flowPara"); + std::vector<SPObject*> fts_childs = flowtspan->childList(false); + bool hascontent = false; + // we need to move the contents to the new created element + // mayve we can move directly but the safer for me is duplicate + // inject into the new element and delete original + for (auto fts_child : fts_childs) { + // is this check necesary? + if (fts_child) { + Inkscape::XML::Node *fts_child_node = fts_child->getRepr()->duplicate(xml_doc); + flowtspan->getRepr()->removeChild(fts_child->getRepr()); + flowpara->addChild(fts_child_node, nullptr); + Inkscape::GC::release(fts_child_node); + hascontent = true; + } + } + // if no contents we dont want to add + if (hascontent) { + flowpara->setAttribute("style", flowtspan->getRepr()->attribute("style")); + spobject->getRepr()->addChild(flowpara, prevchild); + Inkscape::GC::release(flowpara); + prevchild = flowpara; + } + container->getRepr()->removeChild(flowtspan->getRepr()); + } else if (tspan) { + if (child->childList(false).size()) { + child->getRepr()->setAttribute("sodipodi:role", "line"); + // maybe we need to move unindent function here + // to be the same as other here + prevchild = unindent_node(child->getRepr(), prevchild); + } else { + // if no contents we dont want to add + container->getRepr()->removeChild(child->getRepr()); + } + } else if (spstring) { + // we are on a text node, we act diferent if in a text or flowtext. + // wrap a duplicate of the element and unindent after the prevchild + // and finaly delete original + Inkscape::XML::Node *string_node = xml_doc->createTextNode(spstring->string.c_str()); + if (text) { + Inkscape::XML::Node *tspan_node = xml_doc->createElement("svg:tspan"); + tspan_node->setAttribute("style", container->getRepr()->attribute("style")); + tspan_node->addChild(string_node, nullptr); + tspan_node->setAttribute("sodipodi:role", "line"); + text->getRepr()->addChild(tspan_node, prevchild); + Inkscape::GC::release(string_node); + Inkscape::GC::release(tspan_node); + prevchild = tspan_node; + } else if (flowtext) { + Inkscape::XML::Node *flowpara_node = xml_doc->createElement("svg:flowPara"); + flowpara_node->setAttribute("style", container->getRepr()->attribute("style")); + flowpara_node->addChild(string_node, nullptr); + flowtext->getRepr()->addChild(flowpara_node, prevchild); + Inkscape::GC::release(string_node); + Inkscape::GC::release(flowpara_node); + prevchild = flowpara_node; + } + container->getRepr()->removeChild(spstring->getRepr()); + } + } + tc->text->getRepr()->removeChild(container->getRepr()); + } + } + } +} + +Inkscape::XML::Node *TextToolbar::unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *prevchild) +{ + g_assert(repr != nullptr); + + Inkscape::XML::Node *parent = repr->parent(); + if (parent) { + Inkscape::XML::Node *grandparent = parent->parent(); + if (grandparent) { + SPDocument *doc = SP_ACTIVE_DOCUMENT; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *newrepr = repr->duplicate(xml_doc); + parent->removeChild(repr); + grandparent->addChild(newrepr, prevchild); + Inkscape::GC::release(newrepr); + newrepr->setAttribute("sodipodi:role", "line"); + return newrepr; + } + } + std::cout << "error on TextToolbar.cpp::2433" << std::endl; + return repr; +} + +void TextToolbar::subselection_changed(gpointer texttool) +{ +#ifdef DEBUG_TEXT + std::cout << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << "subselection_changed: start " << std::endl; +#endif + // quit if run by the _changed callbacks + this->_sub_active_item = nullptr; + if (_updating) { + return; + } + Inkscape::UI::Tools::TextTool *const tc = SP_TEXT_CONTEXT(SP_EVENT_CONTEXT(texttool)); + if (tc) { + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { + Inkscape::Text::Layout::iterator start = layout->begin(); + Inkscape::Text::Layout::iterator end = layout->end(); + Inkscape::Text::Layout::iterator start_selection = tc->text_sel_start; + Inkscape::Text::Layout::iterator end_selection = tc->text_sel_end; +#ifdef DEBUG_TEXT + std::cout << " GUI: Start of text: " << layout->iteratorToCharIndex(start) << std::endl; + std::cout << " GUI: End of text: " << layout->iteratorToCharIndex(end) << std::endl; + std::cout << " GUI: Start of selection: " << layout->iteratorToCharIndex(start_selection) << std::endl; + std::cout << " GUI: End of selection: " << layout->iteratorToCharIndex(end_selection) << std::endl; + std::cout << " GUI: Loop Subelements: " << std::endl; + std::cout << " ::::::::::::::::::::::::::::::::::::::::::::: " << std::endl; +#endif + gint startline = layout->paragraphIndex(start_selection); + gint endline = layout->paragraphIndex(end_selection); + if (start_selection == end_selection) { + this->_outer = true; + gint counter = 0; + for (auto child : tc->text->childList(false)) { + SPItem *item = dynamic_cast<SPItem *>(child); + if (item && counter == startline) { + this->_sub_active_item = item; + int origin_selection = layout->iteratorToCharIndex(start_selection); + Inkscape::Text::Layout::iterator next = layout->charIndexToIterator(origin_selection + 1); + Inkscape::Text::Layout::iterator prev = layout->charIndexToIterator(origin_selection - 1); + //TODO: find a better way to init + _updating = true; + SPStyle query(SP_ACTIVE_DOCUMENT); + _query_cursor = query; + Inkscape::Text::Layout::iterator start_line = tc->text_sel_start; + start_line.thisStartOfLine(); + if (tc->text_sel_start == start_line) { + tc->text_sel_start = next; + } else { + tc->text_sel_start = prev; + } + _cusor_numbers = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &_query_cursor, QUERY_STYLE_PROPERTY_FONTNUMBERS); + tc->text_sel_start = start_selection; + _updating = false; + break; + } + ++counter; + } + selection_changed(nullptr); + } else if ((start_selection == start && end_selection == end)) { + // full subselection + _cusor_numbers = 0; + this->_outer = true; + selection_changed(nullptr); + } else { + _cusor_numbers = 0; + this->_outer = false; + wrap_start = tc->text_sel_start; + wrap_end = tc->text_sel_end; + if (tc->text_sel_start > tc->text_sel_end) { + wrap_start.thisEndOfLine(); + wrap_end.thisStartOfLine(); + } else { + wrap_start.thisStartOfLine(); + wrap_end.thisEndOfLine(); + } + selection_changed(nullptr); + } + } + } +#ifdef DEBUG_TEXT + std::cout << "subselection_changed: exit " << std::endl; + std::cout << "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&" << std::endl; + std::cout << std::endl; +#endif +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/toolbar/text-toolbar.h b/src/ui/toolbar/text-toolbar.h new file mode 100644 index 0000000..6108dd6 --- /dev/null +++ b/src/ui/toolbar/text-toolbar.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TEXT_TOOLBAR_H +#define SEEN_TEXT_TOOLBAR_H + +/** + * @file + * Text aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/sp-item.h" +#include "object/sp-object.h" +#include "toolbar.h" +#include "text-editing.h" +#include "style.h" +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/popover.h> +#include <gtkmm/separatortoolitem.h> +#include <sigc++/connection.h> + +class SPDesktop; + +namespace Gtk { +class ComboBoxText; +class ToggleToolButton; +} + +namespace Inkscape { +class Selection; + +namespace UI { +namespace Tools { +class ToolBase; +} + +namespace Widget { +class ComboBoxEntryToolItem; +class ComboToolItem; +class SpinButtonToolItem; +class UnitTracker; +} + +namespace Toolbar { +class TextToolbar : public Toolbar { +private: + bool _freeze; + bool _text_style_from_prefs; + UI::Widget::UnitTracker *_tracker; + UI::Widget::UnitTracker *_tracker_fs; + + UI::Widget::ComboBoxEntryToolItem *_font_family_item; + UI::Widget::ComboBoxEntryToolItem *_font_size_item; + UI::Widget::ComboToolItem *_font_size_units_item; + UI::Widget::ComboBoxEntryToolItem *_font_style_item; + UI::Widget::ComboToolItem *_line_height_units_item; + UI::Widget::SpinButtonToolItem *_line_height_item; + Gtk::ToggleToolButton *_superscript_item; + Gtk::ToggleToolButton *_subscript_item; + + UI::Widget::ComboToolItem *_align_item; + UI::Widget::ComboToolItem *_writing_mode_item; + UI::Widget::ComboToolItem *_orientation_item; + UI::Widget::ComboToolItem *_direction_item; + + UI::Widget::SpinButtonToolItem *_word_spacing_item; + UI::Widget::SpinButtonToolItem *_letter_spacing_item; + UI::Widget::SpinButtonToolItem *_dx_item; + UI::Widget::SpinButtonToolItem *_dy_item; + UI::Widget::SpinButtonToolItem *_rotation_item; + + Glib::RefPtr<Gtk::Adjustment> _line_height_adj; + Glib::RefPtr<Gtk::Adjustment> _word_spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _letter_spacing_adj; + Glib::RefPtr<Gtk::Adjustment> _dx_adj; + Glib::RefPtr<Gtk::Adjustment> _dy_adj; + Glib::RefPtr<Gtk::Adjustment> _rotation_adj; + bool _outer; + SPItem *_sub_active_item; + int _lineheight_unit; + Inkscape::Text::Layout::iterator wrap_start; + Inkscape::Text::Layout::iterator wrap_end; + bool _updating; + int _cusor_numbers; + SPStyle _query_cursor; + double selection_fontsize; + sigc::connection c_selection_changed; + sigc::connection c_selection_modified; + sigc::connection c_selection_modified_select_tool; + sigc::connection c_subselection_changed; + void text_outer_set_style(SPCSSAttr *css); + void fontfamily_value_changed(); + void fontsize_value_changed(); + void subselection_wrap_toggle(bool start); + void fontstyle_value_changed(); + void script_changed(Gtk::ToggleToolButton *btn); + void align_mode_changed(int mode); + void writing_mode_changed(int mode); + void orientation_changed(int mode); + void direction_changed(int mode); + void lineheight_value_changed(); + void lineheight_unit_changed(int not_used); + void wordspacing_value_changed(); + void letterspacing_value_changed(); + void dx_value_changed(); + void dy_value_changed(); + void prepare_inner(); + void focus_text(); + void rotation_value_changed(); + void fontsize_unit_changed(int not_used); + void selection_changed(Inkscape::Selection *selection); + void selection_modified(Inkscape::Selection *selection, guint flags); + void selection_modified_select_tool(Inkscape::Selection *selection, guint flags); + void subselection_changed(gpointer texttool); + void watch_ec(SPDesktop* desktop, Inkscape::UI::Tools::ToolBase* ec); + void set_sizes(int unit); + Inkscape::XML::Node *unindent_node(Inkscape::XML::Node *repr, Inkscape::XML::Node *before); + + protected: + TextToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_TEXT_TOOLBAR_H */ diff --git a/src/ui/toolbar/toolbar.cpp b/src/ui/toolbar/toolbar.cpp new file mode 100644 index 0000000..445f5b7 --- /dev/null +++ b/src/ui/toolbar/toolbar.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "toolbar.h" + +#include <gtkmm/label.h> +#include <gtkmm/separatortoolitem.h> +#include <gtkmm/toggletoolbutton.h> + +#include "desktop.h" + +#include "helper/action.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +Gtk::ToolItem * +Toolbar::add_label(const Glib::ustring &label_text) +{ + auto ti = Gtk::manage(new Gtk::ToolItem()); + + // For now, we always enable mnemonic + auto label = Gtk::manage(new Gtk::Label(label_text, true)); + + ti->add(*label); + add(*ti); + + return ti; +} + +/** + * \brief Add a toggle toolbutton to the toolbar + * + * \param[in] label_text The text to display in the toolbar + * \param[in] tooltip_text The tooltip text for the toolitem + * + * \returns The toggle button + */ +Gtk::ToggleToolButton * +Toolbar::add_toggle_button(const Glib::ustring &label_text, + const Glib::ustring &tooltip_text) +{ + auto btn = Gtk::manage(new Gtk::ToggleToolButton(label_text)); + btn->set_tooltip_text(tooltip_text); + add(*btn); + return btn; +} + +/** + * \brief Add a toolbutton that performs a given verb + * + * \param[in] verb_code The code for the verb (e.g., SP_VERB_EDIT_SELECT_ALL) + * + * \returns a pointer to the toolbutton + */ +Gtk::ToolButton * +Toolbar::add_toolbutton_for_verb(unsigned int verb_code) +{ + auto context = Inkscape::ActionContext(_desktop); + auto button = SPAction::create_toolbutton_for_verb(verb_code, context); + add(*button); + return button; +} + +/** + * \brief Add a separator line to the toolbar + * + * \details This is just a convenience wrapper for the + * standard GtkMM functionality + */ +void +Toolbar::add_separator() +{ + add(* Gtk::manage(new Gtk::SeparatorToolItem())); +} + +GtkWidget * +Toolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new Toolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/toolbar.h b/src/ui/toolbar/toolbar.h new file mode 100644 index 0000000..18c0510 --- /dev/null +++ b/src/ui/toolbar/toolbar.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_TOOLBAR_H +#define SEEN_TOOLBAR_H + +#include <gtkmm/toolbar.h> + +class SPDesktop; + +namespace Gtk { + class Label; + class ToggleToolButton; +} + +namespace Inkscape { +namespace UI { +namespace Toolbar { +/** + * \brief An abstract definition for a toolbar within Inkscape + * + * \detail This is basically the same as a Gtk::Toolbar but contains a + * few convenience functions. All toolbars must define a "create" + * function that adds all the required tool-items and returns the + * toolbar as a GtkWidget + */ +class Toolbar : public Gtk::Toolbar { +protected: + SPDesktop *_desktop; + + /** + * \brief A default constructor that just assigns the desktop + */ + Toolbar(SPDesktop *desktop) + : _desktop(desktop) + {} + + Gtk::ToolItem * add_label(const Glib::ustring &label_text); + Gtk::ToggleToolButton * add_toggle_button(const Glib::ustring &label_text, + const Glib::ustring &tooltip_text); + Gtk::ToolButton * add_toolbutton_for_verb(unsigned int verb_code); + void add_separator(); + +protected: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/tweak-toolbar.cpp b/src/ui/toolbar/tweak-toolbar.cpp new file mode 100644 index 0000000..dc5352c --- /dev/null +++ b/src/ui/toolbar/tweak-toolbar.cpp @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Tweak aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tweak-toolbar.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/radiotoolbutton.h> +#include <gtkmm/separatortoolitem.h> + +#include "desktop.h" +#include "document-undo.h" + +#include "ui/icon-names.h" +#include "ui/tools/tweak-tool.h" +#include "ui/widget/label-tool-item.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-button-tool-item.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +TweakToolbar::TweakToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /* Width */ + { + std::vector<Glib::ustring> labels = {_("(pinch tweak)"), "", "", "", _("(default)"), "", "", "", "", _("(broad tweak)")}; + std::vector<double> values = { 1, 3, 5, 10, 15, 20, 30, 50, 75, 100}; + + auto width_val = prefs->getDouble("/tools/tweak/width", 15); + _width_adj = Gtk::Adjustment::create(width_val * 100, 1, 100, 1.0, 10.0); + _width_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-width", _("Width:"), _width_adj, 0.01, 0)); + _width_item->set_tooltip_text(_("The width of the tweak area (relative to the visible canvas area)")); + _width_item->set_custom_numeric_menu_data(values, labels); + _width_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _width_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::width_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*_width_item); + _width_item->set_sensitive(true); + } + + // Force + { + std::vector<Glib::ustring> labels = {_("(minimum force)"), "", "", _("(default)"), "", "", "", _("(maximum force)")}; + std::vector<double> values = { 1, 5, 10, 20, 30, 50, 70, 100}; + auto force_val = prefs->getDouble("/tools/tweak/force", 20); + _force_adj = Gtk::Adjustment::create(force_val * 100, 1, 100, 1.0, 10.0); + _force_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-force", _("Force:"), _force_adj, 0.01, 0)); + _force_item->set_tooltip_text(_("The force of the tweak action")); + _force_item->set_custom_numeric_menu_data(values, labels); + _force_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _force_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::force_value_changed)); + // ege_adjustment_action_set_appearance( eact, TOOLBAR_SLIDER_HINT ); + add(*_force_item); + _force_item->set_sensitive(true); + } + + /* Use Pressure button */ + { + _pressure_item = add_toggle_button(_("Pressure"), + _("Use the pressure of the input device to alter the force of tweak action")); + _pressure_item->set_icon_name(INKSCAPE_ICON("draw-use-pressure")); + _pressure_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::pressure_state_changed)); + _pressure_item->set_active(prefs->getBool("/tools/tweak/usepressure", true)); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + /* Mode */ + { + add_label(_("Mode:")); + Gtk::RadioToolButton::Group mode_group; + + auto mode_move_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move mode"))); + mode_move_btn->set_tooltip_text(_("Move objects in any direction")); + mode_move_btn->set_icon_name(INKSCAPE_ICON("object-tweak-push")); + _mode_buttons.push_back(mode_move_btn); + + auto mode_inout_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move in/out mode"))); + mode_inout_btn->set_tooltip_text(_("Move objects towards cursor; with Shift from cursor")); + mode_inout_btn->set_icon_name(INKSCAPE_ICON("object-tweak-attract")); + _mode_buttons.push_back(mode_inout_btn); + + auto mode_jitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Move jitter mode"))); + mode_jitter_btn->set_tooltip_text(_("Move objects in random directions")); + mode_jitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-randomize")); + _mode_buttons.push_back(mode_jitter_btn); + + auto mode_scale_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Scale mode"))); + mode_scale_btn->set_tooltip_text(_("Shrink objects, with Shift enlarge")); + mode_scale_btn->set_icon_name(INKSCAPE_ICON("object-tweak-shrink")); + _mode_buttons.push_back(mode_scale_btn); + + auto mode_rotate_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Rotate mode"))); + mode_rotate_btn->set_tooltip_text(_("Rotate objects, with Shift counterclockwise")); + mode_rotate_btn->set_icon_name(INKSCAPE_ICON("object-tweak-rotate")); + _mode_buttons.push_back(mode_rotate_btn); + + auto mode_dupdel_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Duplicate/delete mode"))); + mode_dupdel_btn->set_tooltip_text(_("Duplicate objects, with Shift delete")); + mode_dupdel_btn->set_icon_name(INKSCAPE_ICON("object-tweak-duplicate")); + _mode_buttons.push_back(mode_dupdel_btn); + + auto mode_push_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Push mode"))); + mode_push_btn->set_tooltip_text(_("Push parts of paths in any direction")); + mode_push_btn->set_icon_name(INKSCAPE_ICON("path-tweak-push")); + _mode_buttons.push_back(mode_push_btn); + + auto mode_shrinkgrow_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Shrink/grow mode"))); + mode_shrinkgrow_btn->set_tooltip_text(_("Shrink (inset) parts of paths; with Shift grow (outset)")); + mode_shrinkgrow_btn->set_icon_name(INKSCAPE_ICON("path-tweak-shrink")); + _mode_buttons.push_back(mode_shrinkgrow_btn); + + auto mode_attrep_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Attract/repel mode"))); + mode_attrep_btn->set_tooltip_text(_("Attract parts of paths towards cursor; with Shift from cursor")); + mode_attrep_btn->set_icon_name(INKSCAPE_ICON("path-tweak-attract")); + _mode_buttons.push_back(mode_attrep_btn); + + auto mode_roughen_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Roughen mode"))); + mode_roughen_btn->set_tooltip_text(_("Roughen parts of paths")); + mode_roughen_btn->set_icon_name(INKSCAPE_ICON("path-tweak-roughen")); + _mode_buttons.push_back(mode_roughen_btn); + + auto mode_colpaint_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color paint mode"))); + mode_colpaint_btn->set_tooltip_text(_("Paint the tool's color upon selected objects")); + mode_colpaint_btn->set_icon_name(INKSCAPE_ICON("object-tweak-paint")); + _mode_buttons.push_back(mode_colpaint_btn); + + auto mode_coljitter_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Color jitter mode"))); + mode_coljitter_btn->set_tooltip_text(_("Jitter the colors of selected objects")); + mode_coljitter_btn->set_icon_name(INKSCAPE_ICON("object-tweak-jitter-color")); + _mode_buttons.push_back(mode_coljitter_btn); + + auto mode_blur_btn = Gtk::manage(new Gtk::RadioToolButton(mode_group, _("Blur mode"))); + mode_blur_btn->set_tooltip_text(_("Blur selected objects more; with Shift, blur less")); + mode_blur_btn->set_icon_name(INKSCAPE_ICON("object-tweak-blur")); + _mode_buttons.push_back(mode_blur_btn); + + int btn_idx = 0; + + for (auto btn : _mode_buttons) { + btn->set_sensitive(); + add(*btn); + btn->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &TweakToolbar::mode_changed), btn_idx++)); + } + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + guint mode = prefs->getInt("/tools/tweak/mode", 0); + + /* Fidelity */ + { + std::vector<Glib::ustring> labels = {_("(rough, simplified)"), "", "", _("(default)"), "", "", _("(fine, but many nodes)")}; + std::vector<double> values = { 10, 25, 35, 50, 60, 80, 100}; + + auto fidelity_val = prefs->getDouble("/tools/tweak/fidelity", 50); + _fidelity_adj = Gtk::Adjustment::create(fidelity_val * 100, 1, 100, 1.0, 10.0); + _fidelity_item = Gtk::manage(new UI::Widget::SpinButtonToolItem("tweak-fidelity", _("Fidelity:"), _fidelity_adj, 0.01, 0)); + _fidelity_item->set_tooltip_text(_("Low fidelity simplifies paths; high fidelity preserves path features but may generate a lot of new nodes")); + _fidelity_item->set_custom_numeric_menu_data(values, labels); + _fidelity_item->set_focus_widget(Glib::wrap(GTK_WIDGET(desktop->canvas))); + _fidelity_adj->signal_value_changed().connect(sigc::mem_fun(*this, &TweakToolbar::fidelity_value_changed)); + add(*_fidelity_item); + } + + add(* Gtk::manage(new Gtk::SeparatorToolItem())); + + { + _channels_label = Gtk::manage(new UI::Widget::LabelToolItem(_("Channels:"))); + _channels_label->set_use_markup(true); + add(*_channels_label); + } + + { + //TRANSLATORS: "H" here stands for hue + _doh_item = add_toggle_button(C_("Hue", "H"), + _("In color mode, act on object's hue")); + _doh_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doh)); + _doh_item->set_active(prefs->getBool("/tools/tweak/doh", true)); + } + { + //TRANSLATORS: "S" here stands for saturation + _dos_item = add_toggle_button(C_("Saturation", "S"), + _("In color mode, act on object's saturation")); + _dos_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dos)); + _dos_item->set_active(prefs->getBool("/tools/tweak/dos", true)); + } + { + //TRANSLATORS: "S" here stands for saturation + _dol_item = add_toggle_button(C_("Lightness", "L"), + _("In color mode, act on object's lightness")); + _dol_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_dol)); + _dol_item->set_active(prefs->getBool("/tools/tweak/dol", true)); + } + { + //TRANSLATORS: "O" here stands for opacity + _doo_item = add_toggle_button(C_("Opacity", "O"), + _("In color mode, act on object's opacity")); + _doo_item->signal_toggled().connect(sigc::mem_fun(*this, &TweakToolbar::toggle_doo)); + _doo_item->set_active(prefs->getBool("/tools/tweak/doo", true)); + } + + _mode_buttons[mode]->set_active(); + show_all(); + + // Elements must be hidden after show_all() is called + if (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT || mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER) { + _fidelity_item->set_visible(false); + } else { + _channels_label->set_visible(false); + _doh_item->set_visible(false); + _dos_item->set_visible(false); + _dol_item->set_visible(false); + _doo_item->set_visible(false); + } +} + +void +TweakToolbar::set_mode(int mode) +{ + _mode_buttons[mode]->set_active(); +} + +GtkWidget * +TweakToolbar::create(SPDesktop *desktop) +{ + auto toolbar = new TweakToolbar(desktop); + return GTK_WIDGET(toolbar->gobj()); +} + +void +TweakToolbar::width_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/width", + _width_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::force_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/force", + _force_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::mode_changed(int mode) +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/tweak/mode", mode); + + bool flag = ((mode == Inkscape::UI::Tools::TWEAK_MODE_COLORPAINT) || + (mode == Inkscape::UI::Tools::TWEAK_MODE_COLORJITTER)); + + _doh_item->set_visible(flag); + _dos_item->set_visible(flag); + _dol_item->set_visible(flag); + _doo_item->set_visible(flag); + _channels_label->set_visible(flag); + + if (_fidelity_item) { + _fidelity_item->set_visible(!flag); + } +} + +void +TweakToolbar::fidelity_value_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setDouble( "/tools/tweak/fidelity", + _fidelity_adj->get_value() * 0.01 ); +} + +void +TweakToolbar::pressure_state_changed() +{ + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/usepressure", _pressure_item->get_active()); +} + +void +TweakToolbar::toggle_doh() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/doh", _doh_item->get_active()); +} + +void +TweakToolbar::toggle_dos() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/dos", _dos_item->get_active()); +} + +void +TweakToolbar::toggle_dol() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/dol", _dol_item->get_active()); +} + +void +TweakToolbar::toggle_doo() { + auto prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/tweak/doo", _doo_item->get_active()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/tweak-toolbar.h b/src/ui/toolbar/tweak-toolbar.h new file mode 100644 index 0000000..cd1c7d0 --- /dev/null +++ b/src/ui/toolbar/tweak-toolbar.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TWEAK_TOOLBAR_H +#define SEEN_TWEAK_TOOLBAR_H + +/** + * @file + * Tweak aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +class SPDesktop; + +namespace Gtk { +class RadioToolButton; +} + +namespace Inkscape { +namespace UI { +namespace Widget { +class LabelToolItem; +class SpinButtonToolItem; +} + +namespace Toolbar { +class TweakToolbar : public Toolbar { +private: + UI::Widget::SpinButtonToolItem *_width_item; + UI::Widget::SpinButtonToolItem *_force_item; + UI::Widget::SpinButtonToolItem *_fidelity_item; + + Gtk::ToggleToolButton *_pressure_item; + + Glib::RefPtr<Gtk::Adjustment> _width_adj; + Glib::RefPtr<Gtk::Adjustment> _force_adj; + Glib::RefPtr<Gtk::Adjustment> _fidelity_adj; + + std::vector<Gtk::RadioToolButton *> _mode_buttons; + + UI::Widget::LabelToolItem *_channels_label; + Gtk::ToggleToolButton *_doh_item; + Gtk::ToggleToolButton *_dos_item; + Gtk::ToggleToolButton *_dol_item; + Gtk::ToggleToolButton *_doo_item; + + void width_value_changed(); + void force_value_changed(); + void mode_changed(int mode); + void fidelity_value_changed(); + void pressure_state_changed(); + void toggle_doh(); + void toggle_dos(); + void toggle_dol(); + void toggle_doo(); + +protected: + TweakToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); + + void set_mode(int mode); +}; +} +} +} + +#endif /* !SEEN_SELECT_TOOLBAR_H */ diff --git a/src/ui/toolbar/zoom-toolbar.cpp b/src/ui/toolbar/zoom-toolbar.cpp new file mode 100644 index 0000000..3b4d3d6 --- /dev/null +++ b/src/ui/toolbar/zoom-toolbar.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Zoom aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "zoom-toolbar.h" + +#include "desktop.h" +#include "verbs.h" + +#include "helper/action.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +ZoomToolbar::ZoomToolbar(SPDesktop *desktop) + : Toolbar(desktop) +{ + add_toolbutton_for_verb(SP_VERB_ZOOM_IN); + add_toolbutton_for_verb(SP_VERB_ZOOM_OUT); + + add_separator(); + + add_toolbutton_for_verb(SP_VERB_ZOOM_1_1); + add_toolbutton_for_verb(SP_VERB_ZOOM_1_2); + add_toolbutton_for_verb(SP_VERB_ZOOM_2_1); + + add_separator(); + + add_toolbutton_for_verb(SP_VERB_ZOOM_SELECTION); + add_toolbutton_for_verb(SP_VERB_ZOOM_DRAWING); + add_toolbutton_for_verb(SP_VERB_ZOOM_PAGE); + add_toolbutton_for_verb(SP_VERB_ZOOM_PAGE_WIDTH); + add_toolbutton_for_verb(SP_VERB_ZOOM_CENTER_PAGE); + + add_separator(); + + add_toolbutton_for_verb(SP_VERB_ZOOM_PREV); + add_toolbutton_for_verb(SP_VERB_ZOOM_NEXT); + + show_all(); +} + +GtkWidget * +ZoomToolbar::create(SPDesktop *desktop) +{ + auto toolbar = Gtk::manage(new ZoomToolbar(desktop)); + return GTK_WIDGET(toolbar->gobj()); +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/toolbar/zoom-toolbar.h b/src/ui/toolbar/zoom-toolbar.h new file mode 100644 index 0000000..b5d34de --- /dev/null +++ b/src/ui/toolbar/zoom-toolbar.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ZOOM_TOOLBAR_H +#define SEEN_ZOOM_TOOLBAR_H + +/** + * @file + * Zoom aux toolbar + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2011 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "toolbar.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { + +/** + * \brief A toolbar for controlling the zoom + */ +class ZoomToolbar : public Toolbar { +protected: + ZoomToolbar(SPDesktop *desktop); + +public: + static GtkWidget * create(SPDesktop *desktop); +}; +} +} +} + +#endif /* !SEEN_ZOOM_TOOLBAR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools-switch.cpp b/src/ui/tools-switch.cpp new file mode 100644 index 0000000..46695cf --- /dev/null +++ b/src/ui/tools-switch.cpp @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Utility functions for switching tools (= contexts) + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Josh Andler <scislac@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003-2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> // prevents deprecation warnings + +#include "inkscape.h" +#include "desktop.h" + +#include <glibmm/i18n.h> + +#include "ui/tools-switch.h" + +#include "object/sp-rect.h" +#include "object/sp-ellipse.h" +#include "object/sp-flowtext.h" +#include "object/sp-offset.h" +#include "object/sp-path.h" +#include "object/sp-star.h" +#include "object/sp-spiral.h" +#include "object/sp-text.h" + +// TODO: How many of these are actually needed? +#include "ui/tools/arc-tool.h" +#include "ui/tools/box3d-tool.h" +#include "ui/tools/calligraphic-tool.h" +#include "ui/tools/connector-tool.h" +#include "ui/tools/dropper-tool.h" +#include "ui/tools/eraser-tool.h" +#include "ui/tools/flood-tool.h" +#include "ui/tools/gradient-tool.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/measure-tool.h" +#include "ui/tools/mesh-tool.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/pencil-tool.h" +#include "ui/tools/rect-tool.h" +#include "ui/tools/select-tool.h" +#include "ui/tools/spiral-tool.h" +#include "ui/tools/spray-tool.h" +#include "ui/tools/text-tool.h" +#include "ui/tools/tweak-tool.h" +#include "ui/tools/zoom-tool.h" + +#include "message-context.h" + +using Inkscape::UI::Tools::ToolBase; + +static char const *const tool_names[] = { + nullptr, + "/tools/select", + "/tools/nodes", + "/tools/tweak", + "/tools/spray", + "/tools/shapes/rect", + "/tools/shapes/3dbox", + "/tools/shapes/arc", + "/tools/shapes/star", + "/tools/shapes/spiral", + "/tools/freehand/pencil", + "/tools/freehand/pen", + "/tools/calligraphic", + "/tools/text", + "/tools/gradient", + "/tools/mesh", + "/tools/zoom", + "/tools/measure", + "/tools/dropper", + "/tools/connector", + "/tools/paintbucket", + "/tools/eraser", + "/tools/lpetool", + nullptr +}; + +// TODO: HEY! these belong to the tools themselves! +static char const *const tool_msg[] = { + nullptr, + N_("<b>Click</b> to Select and Transform objects, <b>Drag</b> to select many objects."), + N_("Modify selected path points (nodes) directly."), + N_("To tweak a path by pushing, select it and drag over it."), + N_("<b>Drag</b>, <b>click</b> or <b>click and scroll</b> to spray the selected objects."), + N_("<b>Drag</b> to create a rectangle. <b>Drag controls</b> to round corners and resize. <b>Click</b> to select."), + N_("<b>Drag</b> to create a 3D box. <b>Drag controls</b> to resize in perspective. <b>Click</b> to select (with <b>Ctrl+Alt</b> for single faces)."), + N_("<b>Drag</b> to create an ellipse. <b>Drag controls</b> to make an arc or segment. <b>Click</b> to select."), + N_("<b>Drag</b> to create a star. <b>Drag controls</b> to edit the star shape. <b>Click</b> to select."), + N_("<b>Drag</b> to create a spiral. <b>Drag controls</b> to edit the spiral shape. <b>Click</b> to select."), + N_("<b>Drag</b> to create a freehand line. <b>Shift</b> appends to selected path, <b>Alt</b> activates sketch mode."), + N_("<b>Click</b> or <b>click and drag</b> to start a path; with <b>Shift</b> to append to selected path. <b>Ctrl+click</b> to create single dots (straight line modes only)."), + N_("<b>Drag</b> to draw a calligraphic stroke; with <b>Ctrl</b> to track a guide path. <b>Arrow keys</b> adjust width (left/right) and angle (up/down)."), + N_("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type."), + N_("<b>Drag</b> or <b>double click</b> to create a gradient on selected objects, <b>drag handles</b> to adjust gradients."), + N_("<b>Drag</b> or <b>double click</b> to create a mesh on selected objects, <b>drag handles</b> to adjust meshes."), + N_("<b>Click</b> or <b>drag around an area</b> to zoom in, <b>Shift+click</b> to zoom out."), + N_("<b>Drag</b> to measure the dimensions of objects."), + N_("<b>Click</b> to set fill, <b>Shift+click</b> to set stroke; <b>drag</b> to average color in area; with <b>Alt</b> to pick inverse color; <b>Ctrl+C</b> to copy the color under mouse to clipboard"), + N_("<b>Click and drag</b> between shapes to create a connector."), + N_("<b>Click</b> to paint a bounded area, <b>Shift+click</b> to union the new fill with the current selection, <b>Ctrl+click</b> to change the clicked object's fill and stroke to the current setting."), + N_("<b>Drag</b> to erase."), + N_("Choose a subtool from the toolbar"), +}; + +static int +tools_prefpath2num(char const *id) +{ + int i = 1; + while (tool_names[i]) { + if (strcmp(tool_names[i], id) == 0) + return i; + else i++; + } + g_assert( 0 == TOOLS_INVALID ); + return 0; //nothing found +} + +int +tools_isactive(SPDesktop *dt, unsigned num) +{ + g_assert( num < G_N_ELEMENTS(tool_names) ); + if (dynamic_cast<ToolBase *>(dt->event_context)) { + return dt->event_context->pref_observer->observed_path == tool_names[num]; + } else { + return FALSE; + } +} + +int +tools_active(SPDesktop *dt) +{ + return tools_prefpath2num(dt->event_context->pref_observer->observed_path.data()); +} + +void +tools_switch(SPDesktop *dt, int num) +{ + dt->tipsMessageContext()->set(Inkscape::NORMAL_MESSAGE, gettext( tool_msg[num] ) ); + if (dt) { + // This event may change the above message + dt->_tool_changed.emit(num); + } + + dt->setEventContext(tool_names[num]); + /* fixme: This is really ugly hack. We should bind and unbind class methods */ + /* First 4 tools use guides, first is undefined but we don't care */ + dt->activate_guides(num < 5); + INKSCAPE.eventcontext_set(dt->getEventContext()); +} + +void tools_switch_by_item(SPDesktop *dt, SPItem *item, Geom::Point const p) +{ + if (dynamic_cast<SPRect *>(item)) { + tools_switch(dt, TOOLS_SHAPES_RECT); + } else if (dynamic_cast<SPBox3D *>(item)) { + tools_switch(dt, TOOLS_SHAPES_3DBOX); + } else if (dynamic_cast<SPGenericEllipse *>(item)) { + tools_switch(dt, TOOLS_SHAPES_ARC); + } else if (dynamic_cast<SPStar *>(item)) { + tools_switch(dt, TOOLS_SHAPES_STAR); + } else if (dynamic_cast<SPSpiral *>(item)) { + tools_switch(dt, TOOLS_SHAPES_SPIRAL); + } else if (dynamic_cast<SPPath *>(item)) { + if (Inkscape::UI::Tools::cc_item_is_connector(item)) { + tools_switch(dt, TOOLS_CONNECTOR); + } + else { + tools_switch(dt, TOOLS_NODES); + } + } else if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + tools_switch(dt, TOOLS_TEXT); + sp_text_context_place_cursor_at (SP_TEXT_CONTEXT(dt->event_context), item, p); + } else if (dynamic_cast<SPOffset *>(item)) { + tools_switch(dt, TOOLS_NODES); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools-switch.h b/src/ui/tools-switch.h new file mode 100644 index 0000000..ad2a731 --- /dev/null +++ b/src/ui/tools-switch.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Utility functions for switching tools (= contexts) + * + * Authors: + * bulia byak <bulia@dr.com> + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_TOOLS_SWITCH_H +#define SEEN_TOOLS_SWITCH_H + +class SPDesktop; +class SPItem; +namespace Geom { +class Point; +} + + +enum { + TOOLS_INVALID, + TOOLS_SELECT, + TOOLS_NODES, + TOOLS_TWEAK, + TOOLS_SPRAY, + TOOLS_SHAPES_RECT, + TOOLS_SHAPES_3DBOX, + TOOLS_SHAPES_ARC, + TOOLS_SHAPES_STAR, + TOOLS_SHAPES_SPIRAL, + TOOLS_FREEHAND_PENCIL, + TOOLS_FREEHAND_PEN, + TOOLS_CALLIGRAPHIC, + TOOLS_TEXT, + TOOLS_GRADIENT, + TOOLS_MESH, + TOOLS_ZOOM, + TOOLS_MEASURE, + TOOLS_DROPPER, + TOOLS_CONNECTOR, + TOOLS_PAINTBUCKET, + TOOLS_ERASER, + TOOLS_LPETOOL +}; + +int tools_isactive(SPDesktop *dt, unsigned num); +int tools_active(SPDesktop *dt); +void tools_switch(SPDesktop *dt, int num); +void tools_switch_by_item (SPDesktop *dt, SPItem *item, Geom::Point const p); + +#endif // !SEEN_TOOLS_SWITCH_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp new file mode 100644 index 0000000..8356817 --- /dev/null +++ b/src/ui/tools/arc-tool.cpp @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Ellipse drawing context. + */ +/* Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <johan@shouraizou.nl> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000-2006 Authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm/i18n.h> +#include <gdk/gdkkeysyms.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "preferences.h" +#include "selection.h" +#include "snap.h" +#include "verbs.h" + +#include "display/sp-canvas.h" +#include "display/sp-canvas-item.h" + +#include "include/macros.h" + +#include "object/sp-ellipse.h" +#include "object/sp-namedview.h" + +#include "ui/pixmaps/cursor-ellipse.xpm" + +#include "ui/tools/arc-tool.h" +#include "ui/shape-editor.h" +#include "ui/tools/tool-base.h" + +#include "xml/repr.h" +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& ArcTool::getPrefsPath() { + return ArcTool::prefsPath; +} + +const std::string ArcTool::prefsPath = "/tools/shapes/arc"; + + +ArcTool::ArcTool() + : ToolBase(cursor_ellipse_xpm) + , arc(nullptr) +{ +} + +void ArcTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +ArcTool::~ArcTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->arc) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void ArcTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void ArcTool::setup() { + ToolBase::setup(); + + Inkscape::Selection *selection = this->desktop->getSelection(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = selection->connectChanged( + sigc::mem_fun(this, &ArcTool::selection_changed) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +bool ArcTool::item_handler(SPItem* item, GdkEvent* event) { + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, event); + } + break; + // motion and release are always on root (why?) + default: + break; + } + + return ToolBase::item_handler(item, event); +} + +bool ArcTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + bool handled = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + dragging = true; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + /* Snap center */ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + nullptr, event->button.time); + handled = true; + m.unSetup(); + } + break; + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + handled = true; + } else if (!this->sp_event_context_knot_mouseover()){ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the arc + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + handled = true; + } + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + if (!dragging) { + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: make circle or integer-ratio ellipse, snap arc/segment angle"), + _("<b>Shift</b>: draw around the starting point"), + nullptr); + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("arc-rx"); + handled = true; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + handled = true; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the arc + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + handled = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (event->key.keyval) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!handled) { + handled = ToolBase::root_handler(event); + } + + return handled; +} + +void ArcTool::drag(Geom::Point pt, guint state) { + if (!this->arc) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "arc"); + + // Set style + sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/arc", false); + + this->arc = SP_GENERICELLIPSE(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->arc->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->arc->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + bool ctrl_save = false; + + if ((state & GDK_MOD1_MASK) && (state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) { + // if Alt is pressed without Shift in addition to Control, temporarily drop the CONTROL mask + // so that the ellipse is not constrained to integer ratios + ctrl_save = true; + state = state ^ GDK_CONTROL_MASK; + } + + Geom::Rect r = Inkscape::snap_rectangular_box(desktop, this->arc, pt, this->center, state); + + if (ctrl_save) { + state = state ^ GDK_CONTROL_MASK; + } + + Geom::Point dir = r.dimensions() / 2; + + if (state & GDK_MOD1_MASK) { + /* With Alt let the ellipse pass through the mouse pointer */ + Geom::Point c = r.midpoint(); + + if (!ctrl_save) { + if (fabs(dir[Geom::X]) > 1E-6 && fabs(dir[Geom::Y]) > 1E-6) { + Geom::Affine const i2d ( (this->arc)->i2dt_affine() ); + Geom::Point new_dir = pt * i2d - c; + new_dir[Geom::X] *= dir[Geom::Y] / dir[Geom::X]; + double lambda = new_dir.length() / dir[Geom::Y]; + r = Geom::Rect (c - lambda*dir, c + lambda*dir); + } + } else { + /* with Alt+Ctrl (without Shift) we generate a perfect circle + with diameter click point <--> mouse pointer */ + double l = dir.length(); + Geom::Point d (l, l); + r = Geom::Rect (c - d, c + d); + } + } + + this->arc->position_set( + r.midpoint()[Geom::X], r.midpoint()[Geom::Y], + r.dimensions()[Geom::X] / 2, r.dimensions()[Geom::Y] / 2); + + double rdimx = r.dimensions()[Geom::X]; + double rdimy = r.dimensions()[Geom::Y]; + + Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px"); + Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px"); + Glib::ustring xs = rdimx_q.string(desktop->namedview->display_units); + Glib::ustring ys = rdimy_q.string(desktop->namedview->display_units); + + if (state & GDK_CONTROL_MASK) { + int ratio_x, ratio_y; + bool is_golden_ratio = false; + + if (fabs (rdimx) > fabs (rdimy)) { + if (fabs(rdimx / rdimy - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = (int) rint (rdimx / rdimy); + ratio_y = 1; + } else { + if (fabs(rdimy / rdimx - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = 1; + ratio_y = (int) rint (rdimy / rdimx); + } + + if (!is_golden_ratio) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Ellipse</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Ellipse</b>: %s × %s; with <b>Ctrl</b> to make circle, integer-ratio, or golden-ratio ellipse; with <b>Shift</b> to draw around the starting point"), xs.c_str(), ys.c_str()); + } +} + +void ArcTool::finishItem() { + this->message_context->clear(); + + if (this->arc != nullptr) { + if (this->arc->rx.computed == 0 || this->arc->ry.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized arc, for example when the start and and point snap to the snap grid point + return; + } + + this->arc->updateRepr(); + this->arc->doWriteTransform(this->arc->transform, nullptr, true); + + desktop->canvas->endForcedFullRedraws(); + + desktop->getSelection()->set(this->arc); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_ARC, _("Create ellipse")); + + this->arc = nullptr; + } +} + +void ArcTool::cancel() { + desktop->getSelection()->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + if (this->arc != nullptr) { + this->arc->deleteObject(); + this->arc = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(desktop->getDocument()); +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/arc-tool.h b/src/ui/tools/arc-tool.h new file mode 100644 index 0000000..6af99e0 --- /dev/null +++ b/src/ui/tools/arc-tool.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_ARC_CONTEXT_H +#define SEEN_ARC_CONTEXT_H + +/* + * Ellipse drawing context + * + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPGenericEllipse; + +namespace Inkscape { + class Selection; +} + +#define SP_ARC_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ArcTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_ARC_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ArcTool*>(obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ArcTool : public ToolBase { +public: + ArcTool(); + ~ArcTool() override; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPGenericEllipse *arc; + + Geom::Point center; + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(Geom::Point pt, guint state); + void finishItem(); + void cancel(); +}; + +} +} +} + +#endif /* !SEEN_ARC_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp new file mode 100644 index 0000000..4dc866e --- /dev/null +++ b/src/ui/tools/box3d-tool.cpp @@ -0,0 +1,614 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 3D box drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de> + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "perspective-line.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "verbs.h" + +#include "display/sp-canvas-item.h" +#include "display/sp-canvas.h" + +#include "include/macros.h" + +#include "ui/pixmaps/cursor-3dbox.xpm" + +#include "object/box3d-side.h" +#include "object/box3d.h" +#include "object/sp-defs.h" +#include "object/sp-namedview.h" + +#include "ui/shape-editor.h" +#include "ui/tools/box3d-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& Box3dTool::getPrefsPath() { + return Box3dTool::prefsPath; +} + +const std::string Box3dTool::prefsPath = "/tools/shapes/3dbox"; + +Box3dTool::Box3dTool() + : ToolBase(cursor_3dbox_xpm) + , _vpdrag(nullptr) + , box3d(nullptr) + , ctrl_dragged(false) + , extruded(false) +{ +} + +void Box3dTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + + +Box3dTool::~Box3dTool() { + this->enableGrDrag(false); + + delete (this->_vpdrag); + this->_vpdrag = nullptr; + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->box3d) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void Box3dTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); + + if (selection->perspList().size() == 1) { + // selecting a single box changes the current perspective + this->desktop->doc()->setCurrentPersp3D(selection->perspList().front()); + } +} + +/* Create a default perspective in document defs if none is present (which can happen, among other + * circumstances, after 'vacuum defs' or when a pre-0.46 file is opened). + */ +static void sp_box3d_context_ensure_persp_in_defs(SPDocument *document) { + SPDefs *defs = document->getDefs(); + + bool has_persp = false; + for (auto& child: defs->children) { + if (SP_IS_PERSP3D(&child)) { + has_persp = true; + break; + } + } + + if (!has_persp) { + document->setCurrentPersp3D(persp3d_create_xml_element (document)); + } +} + +void Box3dTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &Box3dTool::selection_changed) + ); + + this->_vpdrag = new Box3D::VPDrag(this->desktop->getDocument()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +bool Box3dTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, event); + //ret = TRUE; + } + break; + // motion and release are always on root (why?) + default: + break; + } + +// if (((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler) { +// ret = ((ToolBaseClass *) sp_box3d_context_parent_class)->item_handler(event_context, item, event); +// } + // CPPIFY: ret is always overwritten... + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool Box3dTool::root_handler(GdkEvent* event) { + static bool dragging; + + SPDocument *document = desktop->getDocument(); + auto const y_dir = desktop->yaxisdir(); + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + Persp3D *cur_persp = document->getCurrentPersp3D(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning) { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point button_dt(desktop->w2d(button_w)); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + // remember clicked box3d, *not* disregarding groups (since a 3D box is a group), honoring Alt + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, event->button.state & GDK_CONTROL_MASK); + + dragging = true; + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, this->box3d); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + this->center = button_dt; + + this->drag_origin = button_dt; + this->drag_ptB = button_dt; + this->drag_ptC = button_dt; + + // This can happen after saving when the last remaining perspective was purged and must be recreated. + if (!cur_persp) { + sp_box3d_context_ensure_persp_in_defs(document); + cur_persp = document->getCurrentPersp3D(); + } + + /* Projective preimages of clicked point under current perspective */ + this->drag_origin_proj = cur_persp->perspective_impl->tmat.preimage (button_dt, 0, Proj::Z); + this->drag_ptB_proj = this->drag_origin_proj; + this->drag_ptC_proj = this->drag_origin_proj; + this->drag_ptC_proj.normalize(); + this->drag_ptC_proj[Proj::Z] = 0.25; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + nullptr, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, this->box3d); + m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + this->ctrl_dragged = event->motion.state & GDK_CONTROL_MASK; + + if ((event->motion.state & GDK_SHIFT_MASK) && !this->extruded && this->box3d) { + // once shift is pressed, set this->extruded + this->extruded = true; + } + + if (!this->extruded) { + this->drag_ptB = motion_dt; + this->drag_ptC = motion_dt; + + this->drag_ptB_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, 0, Proj::Z); + this->drag_ptC_proj = this->drag_ptB_proj; + this->drag_ptC_proj.normalize(); + this->drag_ptC_proj[Proj::Z] = 0.25; + } else { + // Without Ctrl, motion of the extruded corner is constrained to the + // perspective line from drag_ptB to vanishing point Y. + if (!this->ctrl_dragged) { + /* snapping */ + Box3D::PerspectiveLine pline (this->drag_ptB, Proj::Z, document->getCurrentPersp3D()); + this->drag_ptC = pline.closest_to (motion_dt); + + this->drag_ptB_proj.normalize(); + this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (this->drag_ptC, this->drag_ptB_proj[Proj::X], Proj::X); + } else { + this->drag_ptC = motion_dt; + + this->drag_ptB_proj.normalize(); + this->drag_ptC_proj = cur_persp->perspective_impl->tmat.preimage (motion_dt, this->drag_ptB_proj[Proj::X], Proj::X); + } + + m.freeSnapReturnByRef(this->drag_ptC, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + + m.unSetup(); + + this->drag(event->motion.state); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if (event->button.button == 1 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the box + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked box3d if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = TRUE; + break; + + case GDK_KEY_bracketright: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_bracketleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::X, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_parenright: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_parenleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Y, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_braceright: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, 180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + case GDK_KEY_braceleft: + persp3d_rotate_VP (document->getCurrentPersp3D(), Proj::Z, -180 / snaps * y_dir, MOD__ALT(event)); + DocumentUndo::done(document, SP_VERB_CONTEXT_3DBOX, + _("Change perspective (angle of PLs)")); + ret = true; + break; + + /* FOR DEBUGGING PURPOSES + case GDK_O: + if (MOD__CTRL(event) && MOD__SHIFT(event)) { + Box3D::create_canvas_point(persp3d_get_VP(document()->getCurrentPersp3D(), Proj::W).affine(), 7, 0xff00ff00); + } + ret = true; + break; + */ + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + desktop->selection->toGuides(); + ret = true; + } + break; + + case GDK_KEY_p: + case GDK_KEY_P: + if (MOD__SHIFT_ONLY(event)) { + if (document->getCurrentPersp3D()) { + persp3d_print_debugging_info (document->getCurrentPersp3D()); + } + ret = true; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("box3d-angle-x"); + ret = TRUE; + } + if (MOD__SHIFT_ONLY(event)) { + persp3d_toggle_VPs(selection->perspList(), Proj::X); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_y: + case GDK_KEY_Y: + if (MOD__SHIFT_ONLY(event)) { + persp3d_toggle_VPs(selection->perspList(), Proj::Y); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__SHIFT_ONLY(event)) { + persp3d_toggle_VPs(selection->perspList(), Proj::Z); + this->_vpdrag->updateLines(); // FIXME: Shouldn't this be done automatically? + ret = true; + } + break; + + case GDK_KEY_Escape: + desktop->getSelection()->clear(); + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + if (!this->within_tolerance) { + // we've been dragging, finish the box + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void Box3dTool::drag(guint /*state*/) { + if (!this->box3d) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + SPBox3D *box3d = SPBox3D::createBox3D((SPItem*)desktop->currentLayer()); + + // Set style + desktop->applyCurrentOrToolStyle(box3d, "/tools/shapes/3dbox", false); + + this->box3d = box3d; + + // TODO: Incorporate this in box3d-side.cpp! + for (int i = 0; i < 6; ++i) { + Box3DSide *side = Box3DSide::createBox3DSide(box3d); + + guint desc = Box3D::int_to_face(i); + + Box3D::Axis plane = (Box3D::Axis) (desc & 0x7); + plane = (Box3D::is_plane(plane) ? plane : Box3D::orth_plane_or_axis(plane)); + side->dir1 = Box3D::extract_first_axis_direction(plane); + side->dir2 = Box3D::extract_second_axis_direction(plane); + side->front_or_rear = (Box3D::FrontOrRear) (desc & 0x8); + + // Set style + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring descr = "/desktop/"; + descr += box3d_side_axes_string(side); + descr += "/style"; + + Glib::ustring cur_style = prefs->getString(descr); + + bool use_current = prefs->getBool("/tools/shapes/3dbox/usecurrent", false); + + if (use_current && !cur_style.empty()) { + // use last used style + side->setAttribute("style", cur_style); + } else { + // use default style + Glib::ustring tool_path = Glib::ustring::compose("/tools/shapes/3dbox/%1", + box3d_side_axes_string(side)); + desktop->applyCurrentOrToolStyle (side, tool_path, false); + } + + side->updateRepr(); // calls box3d_side_write() and updates, e.g., the axes string description + } + + box3d_set_z_orders(this->box3d); + this->box3d->updateRepr(); + + // TODO: It would be nice to show the VPs during dragging, but since there is no selection + // at this point (only after finishing the box), we must do this "manually" + /* this._vpdrag->updateDraggers(); */ + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + g_assert(this->box3d); + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + box3d_check_for_swapped_coords(this->box3d); + + /* we need to call this from here (instead of from box3d_position_set(), for example) + because z-order setting must not interfere with display updates during undo/redo */ + box3d_set_z_orders (this->box3d); + + box3d_position_set(this->box3d); + + // status text + this->message_context->setF(Inkscape::NORMAL_MESSAGE, "%s", _("<b>3D Box</b>; with <b>Shift</b> to extrude along the Z axis")); +} + +void Box3dTool::finishItem() { + this->message_context->clear(); + this->ctrl_dragged = false; + this->extruded = false; + + if (this->box3d != nullptr) { + SPDocument *doc = this->desktop->getDocument(); + + if (!doc || !doc->getCurrentPersp3D()) { + return; + } + + this->box3d->orig_corner0 = this->drag_origin_proj; + this->box3d->orig_corner7 = this->drag_ptC_proj; + + this->box3d->updateRepr(); + + box3d_relabel_corners(this->box3d); + + desktop->canvas->endForcedFullRedraws(); + + desktop->getSelection()->set(this->box3d); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_3DBOX, + _("Create 3D box")); + + this->box3d = nullptr; + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/box3d-tool.h b/src/ui/tools/box3d-tool.h new file mode 100644 index 0000000..1ae63a0 --- /dev/null +++ b/src/ui/tools/box3d-tool.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_BOX3D_CONTEXT_H__ +#define __SP_BOX3D_CONTEXT_H__ + +/* + * 3D box drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2007 Maximilian Albert <Anhalter42@gmx.de> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "proj_pt.h" +#include "vanishing-point.h" + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPBox3D; + +namespace Box3D { + struct VPDrag; +} + +namespace Inkscape { + class Selection; +} + +#define SP_BOX3D_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::Box3dTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_BOX3D_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::Box3dTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class Box3dTool : public ToolBase { +public: + Box3dTool(); + ~Box3dTool() override; + + Box3D::VPDrag * _vpdrag; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPBox3D* box3d; + Geom::Point center; + + /** + * save three corners while dragging: + * 1) the starting point (already done by the event_context) + * 2) drag_ptB --> the opposite corner of the front face (before pressing shift) + * 3) drag_ptC --> the "extruded corner" (which coincides with the mouse pointer location + * if we are ctrl-dragging but is constrained to the perspective line from drag_ptC + * to the vanishing point Y otherwise) + */ + Geom::Point drag_origin; + Geom::Point drag_ptB; + Geom::Point drag_ptC; + + Proj::Pt3 drag_origin_proj; + Proj::Pt3 drag_ptB_proj; + Proj::Pt3 drag_ptC_proj; + + bool ctrl_dragged; /* whether we are ctrl-dragging */ + bool extruded; /* whether shift-dragging already occurred (i.e. the box is already extruded) */ + + sigc::connection sel_changed_connection; + + void selection_changed(Inkscape::Selection* selection); + + void drag(guint state); + void finishItem(); +}; + +} +} +} + +#endif /* __SP_BOX3D_CONTEXT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp new file mode 100644 index 0000000..aa5d44d --- /dev/null +++ b/src/ui/tools/calligraphic-tool.cpp @@ -0,0 +1,1203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2005-2007 bulia byak + * Copyright (C) 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noDYNA_DRAW_VERBOSE + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> +#include <string> +#include <cstring> +#include <numeric> + +#include <2geom/pathvector.h> +#include <2geom/bezier-utils.h> +#include <2geom/circle.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-context.h" +#include "selection.h" +#include "splivarot.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/canvas-arena.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "include/macros.h" + +#include "livarot/Shape.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/pixmaps/cursor-calligraphy.xpm" + +#include "svg/svg.h" + + +#include "ui/tools/calligraphic-tool.h" +#include "ui/tools/freehand-base.h" + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff + +#define TOLERANCE_CALLIGRAPHIC 0.1 + +#define DYNA_EPSILON 0.5e-6 +#define DYNA_EPSILON_START 0.5e-2 +#define DYNA_VEL_START 1e-5 + +#define DYNA_MIN_WIDTH 1.0e-6 + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void add_cap(SPCurve *curve, Geom::Point const &from, Geom::Point const &to, double rounding); + +const std::string& CalligraphicTool::getPrefsPath() { + return CalligraphicTool::prefsPath; +} + + +const std::string CalligraphicTool::prefsPath = "/tools/calligraphic"; + +CalligraphicTool::CalligraphicTool() + : DynamicBase(cursor_calligraphy_xpm) + , keep_selected(true) + , hatch_spacing(0) + , hatch_spacing_step(0) + , hatch_item(nullptr) + , hatch_livarot_path(nullptr) + , hatch_last_nearest(Geom::Point(0,0)) + , hatch_last_pointer(Geom::Point(0,0)) + , hatch_escaped(false) + , hatch_area(nullptr) + , just_started_drawing(false) + , trace_bg(false) +{ + this->vel_thin = 0.1; + this->flatness = 0.9; + this->cap_rounding = 0.0; + this->abs_width = false; +} + +CalligraphicTool::~CalligraphicTool() { + if (this->hatch_area) { + sp_canvas_item_destroy(this->hatch_area); + this->hatch_area = nullptr; + } +} + +void CalligraphicTool::setup() { + DynamicBase::setup(); + + this->accumulated = new SPCurve(); + this->currentcurve = new SPCurve(); + + this->cal1 = new SPCurve(); + this->cal2 = new SPCurve(); + + this->currentshape = sp_canvas_item_new(this->desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), DDC_RED_RGBA, SP_WIND_RULE_EVENODD); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), this->desktop); + + { + /* TODO: have a look at DropperTool::setup where the same is done.. generalize? */ + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); + + SPCurve *c = new SPCurve(path); + + this->hatch_area = sp_canvas_bpath_new(this->desktop->getControls(), c, true); + + c->unref(); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->hatch_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->hatch_area); + } + + sp_event_context_read(this, "mass"); + sp_event_context_read(this, "wiggle"); + sp_event_context_read(this, "angle"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "thinning"); + sp_event_context_read(this, "tremor"); + sp_event_context_read(this, "flatness"); + sp_event_context_read(this, "tracebackground"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "usetilt"); + sp_event_context_read(this, "abs_width"); + sp_event_context_read(this, "keep_selected"); + sp_event_context_read(this, "cap_rounding"); + + this->is_drawing = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/calligraphic/selcue")) { + this->enableSelectionCue(); + } +} + +void CalligraphicTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "tracebackground") { + this->trace_bg = val.getBool(); + } else if (path == "keep_selected") { + this->keep_selected = val.getBool(); + } else { + //pass on up to parent class to handle common attributes. + DynamicBase::set(val); + } + + //g_print("DDC: %g %g %g %g\n", ddc->mass, ddc->drag, ddc->angle, ddc->width); +} + +static double +flerp(double f0, double f1, double p) +{ + return f0 + ( f1 - f0 ) * p; +} + +///* Get normalized point */ +//Geom::Point CalligraphicTool::getNormalizedPoint(Geom::Point v) const { +// Geom::Rect drect = desktop->get_display_area(); +// +// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); +// +// return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max); +//} +// +///* Get view point */ +//Geom::Point CalligraphicTool::getViewPoint(Geom::Point n) const { +// Geom::Rect drect = desktop->get_display_area(); +// +// double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); +// +// return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]); +//} + +void CalligraphicTool::reset(Geom::Point p) { + this->last = this->cur = this->getNormalizedPoint(p); + + this->vel = Geom::Point(0,0); + this->vel_max = 0; + this->acc = Geom::Point(0,0); + this->ang = Geom::Point(0,0); + this->del = Geom::Point(0,0); +} + +void CalligraphicTool::extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) { + this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE); + } else { + this->pressure = DDC_DEFAULT_PRESSURE; + } + + if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt)) { + this->xtilt = CLAMP (this->xtilt, DDC_MIN_TILT, DDC_MAX_TILT); + } else { + this->xtilt = DDC_DEFAULT_TILT; + } + + if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt)) { + this->ytilt = CLAMP (this->ytilt, DDC_MIN_TILT, DDC_MAX_TILT); + } else { + this->ytilt = DDC_DEFAULT_TILT; + } +} + + +bool CalligraphicTool::apply(Geom::Point p) { + Geom::Point n = this->getNormalizedPoint(p); + + /* Calculate mass and drag */ + double const mass = flerp(1.0, 160.0, this->mass); + double const drag = flerp(0.0, 0.5, this->drag * this->drag); + + /* Calculate force and acceleration */ + Geom::Point force = n - this->cur; + + // If force is below the absolute threshold DYNA_EPSILON, + // or we haven't yet reached DYNA_VEL_START (i.e. at the beginning of stroke) + // _and_ the force is below the (higher) DYNA_EPSILON_START threshold, + // discard this move. + // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen, + // especially bothersome at the start of the stroke where we don't yet have the inertia to + // smooth them out. + if ( Geom::L2(force) < DYNA_EPSILON || (this->vel_max < DYNA_VEL_START && Geom::L2(force) < DYNA_EPSILON_START)) { + return FALSE; + } + + this->acc = force / mass; + + /* Calculate new velocity */ + this->vel += this->acc; + + if (Geom::L2(this->vel) > this->vel_max) + this->vel_max = Geom::L2(this->vel); + + /* Calculate angle of drawing tool */ + + double a1; + if (this->usetilt) { + // 1a. calculate nib angle from input device tilt: + if (this->xtilt == 0 && this->ytilt == 0) { + // to be sure that atan2 in the computation below + // would not crash or return NaN. + a1 = 0; + } else { + Geom::Point dir(-this->xtilt, this->ytilt); + a1 = atan2(dir); + } + } + else { + // 1b. fixed dc->angle (absolutely flat nib): + a1 = ( this->angle / 180.0 ) * M_PI; + } + a1 *= -this->desktop->yaxisdir(); + a1 = fmod(a1, M_PI); + if (a1 > 0.5*M_PI) { + a1 -= M_PI; + } else if (a1 <= -0.5*M_PI) { + a1 += M_PI; + } + + // 2. perpendicular to dc->vel (absolutely non-flat nib): + gdouble const mag_vel = Geom::L2(this->vel); + if ( mag_vel < DYNA_EPSILON ) { + return FALSE; + } + Geom::Point ang2 = Geom::rot90(this->vel) / mag_vel; + + // 3. Average them using flatness parameter: + // calculate angles + double a2 = atan2(ang2); + // flip a2 to force it to be in the same half-circle as a1 + bool flipped = false; + if (fabs (a2-a1) > 0.5*M_PI) { + a2 += M_PI; + flipped = true; + } + // normalize a2 + if (a2 > M_PI) + a2 -= 2*M_PI; + if (a2 < -M_PI) + a2 += 2*M_PI; + // find the flatness-weighted bisector angle, unflip if a2 was flipped + // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this? + double new_ang = a1 + (1 - this->flatness) * (a2 - a1) - (flipped? M_PI : 0); + + // Try to detect a sudden flip when the new angle differs too much from the previous for the + // current velocity; in that case discard this move + double angle_delta = Geom::L2(Geom::Point (cos (new_ang), sin (new_ang)) - this->ang); + if ( angle_delta / Geom::L2(this->vel) > 4000 ) { + return FALSE; + } + + // convert to point + this->ang = Geom::Point (cos (new_ang), sin (new_ang)); + +// g_print ("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(dc->acc), dc->vel_max, Geom::L2(dc->vel), a1, a2, new_ang); + + /* Apply drag */ + this->vel *= 1.0 - drag; + + /* Update position */ + this->last = this->cur; + this->cur += this->vel; + + return TRUE; +} + +void CalligraphicTool::brush() { + g_assert( this->npoints >= 0 && this->npoints < SAMPLING_SIZE ); + + // How much velocity thins strokestyle + double vel_thin = flerp (0, 160, this->vel_thin); + + // Influence of pressure on thickness + double pressure_thick = (this->usepressure ? this->pressure : 1.0); + + // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass + // drag) + Geom::Point brush = this->getViewPoint(this->cur); + Geom::Point brush_w = SP_EVENT_CONTEXT(this)->desktop->d2w(brush); + + double trace_thick = 1; + if (this->trace_bg) { + // pick single pixel + double R, G, B, A; + Geom::IntRect area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1)); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(this->desktop->getDrawing()), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + double max = MAX (MAX (R, G), B); + double min = MIN (MIN (R, G), B); + double L = A * (max + min)/2 + (1 - A); // blend with white bg + trace_thick = 1 - L; + //g_print ("L %g thick %g\n", L, trace_thick); + } + + double width = (pressure_thick * trace_thick - vel_thin * Geom::L2(this->vel)) * this->width; + + double tremble_left = 0, tremble_right = 0; + if (this->tremor > 0) { + // obtain two normally distributed random variables, using polar Box-Muller transform + double x1, x2, w, y1, y2; + do { + x1 = 2.0 * g_random_double_range(0,1) - 1.0; + x2 = 2.0 * g_random_double_range(0,1) - 1.0; + w = x1 * x1 + x2 * x2; + } while ( w >= 1.0 ); + w = sqrt( (-2.0 * log( w ) ) / w ); + y1 = x1 * w; + y2 = x2 * w; + + // deflect both left and right edges randomly and independently, so that: + // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve; + // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths; + // (3) deflection somewhat depends on speed, to prevent fast strokes looking + // comparatively smooth and slow ones excessively jittery + tremble_left = (y1)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + tremble_right = (y2)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + } + + if ( width < 0.02 * this->width ) { + width = 0.02 * this->width; + } + + double dezoomify_factor = 0.05 * 1000; + if (!this->abs_width) { + dezoomify_factor /= SP_EVENT_CONTEXT(this)->desktop->current_zoom(); + } + + Geom::Point del_left = dezoomify_factor * (width + tremble_left) * this->ang; + Geom::Point del_right = dezoomify_factor * (width + tremble_right) * this->ang; + + this->point1[this->npoints] = brush + del_left; + this->point2[this->npoints] = brush - del_right; + + this->del = 0.5*(del_left + del_right); + + this->npoints++; +} + +static void +sp_ddc_update_toolbox (SPDesktop *desktop, const gchar *id, double value) +{ + desktop->setToolboxAdjustmentValue (id, value); +} + +void CalligraphicTool::cancel() { + this->dragging = false; + this->is_drawing = false; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + /* Remove all temporary line segments */ + for (auto i:this->segments) + sp_canvas_item_destroy(SP_CANVAS_ITEM(i)); + this->segments.clear(); + + /* reset accumulated curve */ + this->accumulated->reset(); + this->clear_current(); + + if (this->repr) { + this->repr = nullptr; + } +} + +bool CalligraphicTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return TRUE; + } + + this->accumulated->reset(); + + if (this->repr) { + this->repr = nullptr; + } + + /* initialize first point */ + this->npoints = 0; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK ), + nullptr, + event->button.time); + + ret = TRUE; + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + set_high_motion_precision(); + this->is_drawing = true; + this->just_started_drawing = true; + } + break; + case GDK_MOTION_NOTIFY: + { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + this->extinput(event); + + this->message_context->clear(); + + // for hatching: + double hatch_dist = 0; + Geom::Point hatch_unit_vector(0,0); + Geom::Point nearest(0,0); + Geom::Point pointer(0,0); + Geom::Affine motion_to_curve(Geom::identity()); + + if (event->motion.state & GDK_CONTROL_MASK) { // hatching - sense the item + + SPItem *selected = desktop->getSelection()->singleItem(); + if (selected && (SP_IS_SHAPE(selected) || SP_IS_TEXT(selected))) { + // One item selected, and it's a path; + // let's try to track it as a guide + + if (selected != this->hatch_item) { + this->hatch_item = selected; + if (this->hatch_livarot_path) + delete this->hatch_livarot_path; + this->hatch_livarot_path = Path_for_item (this->hatch_item, true, true); + this->hatch_livarot_path->ConvertWithBackData(0.01); + } + + // calculate pointer point in the guide item's coords + motion_to_curve = selected->dt2i_affine() * selected->i2doc_affine(); + pointer = motion_dt * motion_to_curve; + + // calculate the nearest point on the guide path + boost::optional<Path::cut_position> position = get_nearest_position_on_Path(this->hatch_livarot_path, pointer); + nearest = get_point_on_Path(this->hatch_livarot_path, position->piece, position->t); + + + // distance from pointer to nearest + hatch_dist = Geom::L2(pointer - nearest); + // unit-length vector + hatch_unit_vector = (pointer - nearest)/hatch_dist; + + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Guide path selected</b>; start drawing along the guide with <b>Ctrl</b>")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Select a guide path</b> to track with <b>Ctrl</b>")); + } + } + + if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + this->dragging = TRUE; + + if (event->motion.state & GDK_CONTROL_MASK && this->hatch_item) { // hatching + +#define HATCH_VECTOR_ELEMENTS 12 +#define INERTIA_ELEMENTS 24 +#define SPEED_ELEMENTS 12 +#define SPEED_MIN 0.3 +#define SPEED_NORMAL 0.35 +#define INERTIA_FORCE 0.5 + + // speed is the movement of the nearest point along the guide path, divided by + // the movement of the pointer at the same period; it is averaged for the last + // SPEED_ELEMENTS motion events. Normally, as you track the guide path, speed + // is about 1, i.e. the nearest point on the path is moved by about the same + // distance as the pointer. If the speed starts to decrease, we are losing + // contact with the guide; if it drops below SPEED_MIN, we are on our own and + // not attracted to guide anymore. Most often this happens when you have + // tracked to the end of a guide calligraphic stroke and keep moving + // further. We try to handle this situation gracefully: not stick with the + // guide forever but let go of it smoothly and without sharp jerks (non-zero + // mass recommended; with zero mass, jerks are still quite noticeable). + + double speed = 1; + if (Geom::L2(this->hatch_last_nearest) != 0) { + // the distance nearest moved since the last motion event + double nearest_moved = Geom::L2(nearest - this->hatch_last_nearest); + // the distance pointer moved since the last motion event + double pointer_moved = Geom::L2(pointer - this->hatch_last_pointer); + // store them in stacks limited to SPEED_ELEMENTS + this->hatch_nearest_past.push_front(nearest_moved); + if (this->hatch_nearest_past.size() > SPEED_ELEMENTS) + this->hatch_nearest_past.pop_back(); + this->hatch_pointer_past.push_front(pointer_moved); + if (this->hatch_pointer_past.size() > SPEED_ELEMENTS) + this->hatch_pointer_past.pop_back(); + + // If the stacks are full, + if (this->hatch_nearest_past.size() == SPEED_ELEMENTS) { + // calculate the sums of all stored movements + double nearest_sum = std::accumulate (this->hatch_nearest_past.begin(), this->hatch_nearest_past.end(), 0.0); + double pointer_sum = std::accumulate (this->hatch_pointer_past.begin(), this->hatch_pointer_past.end(), 0.0); + // and divide to get the speed + speed = nearest_sum/pointer_sum; + //g_print ("nearest sum %g pointer_sum %g speed %g\n", nearest_sum, pointer_sum, speed); + } + } + + if ( this->hatch_escaped // already escaped, do not reattach + || (speed < SPEED_MIN) // stuck; most likely reached end of traced stroke + || (this->hatch_spacing > 0 && hatch_dist > 50 * this->hatch_spacing) // went too far from the guide + ) { + // We are NOT attracted to the guide! + + //g_print ("\nlast_nearest %g %g nearest %g %g pointer %g %g pos %d %g\n", dc->last_nearest[Geom::X], dc->last_nearest[Geom::Y], nearest[Geom::X], nearest[Geom::Y], pointer[Geom::X], pointer[Geom::Y], position->piece, position->t); + + // Remember hatch_escaped so we don't get + // attracted again until the end of this stroke + this->hatch_escaped = true; + + if (this->inertia_vectors.size() >= INERTIA_ELEMENTS/2) { // move by inertia + Geom::Point moved_past_escape = motion_dt - this->inertia_vectors.front(); + Geom::Point inertia = + this->inertia_vectors.front() - this->inertia_vectors.back(); + + double dot = Geom::dot (moved_past_escape, inertia); + dot /= Geom::L2(moved_past_escape) * Geom::L2(inertia); + + if (dot > 0) { // mouse is still moving in approx the same direction + Geom::Point should_have_moved = + (inertia) * (1/Geom::L2(inertia)) * Geom::L2(moved_past_escape); + motion_dt = this->inertia_vectors.front() + + (INERTIA_FORCE * should_have_moved + (1 - INERTIA_FORCE) * moved_past_escape); + } + } + + } else { + + // Calculate angle cosine of this vector-to-guide and all past vectors + // summed, to detect if we accidentally flipped to the other side of the + // guide + Geom::Point hatch_vector_accumulated = std::accumulate + (this->hatch_vectors.begin(), this->hatch_vectors.end(), Geom::Point(0,0)); + double dot = Geom::dot (pointer - nearest, hatch_vector_accumulated); + dot /= Geom::L2(pointer - nearest) * Geom::L2(hatch_vector_accumulated); + + if (this->hatch_spacing != 0) { // spacing was already set + double target; + if (speed > SPEED_NORMAL) { + // all ok, strictly obey the spacing + target = this->hatch_spacing; + } else { + // looks like we're starting to lose speed, + // so _gradually_ let go attraction to prevent jerks + target = (this->hatch_spacing * speed + hatch_dist * (SPEED_NORMAL - speed))/SPEED_NORMAL; + } + if (!std::isnan(dot) && dot < -0.5) {// flip + target = -target; + } + + // This is the track pointer that we will use instead of the real one + Geom::Point new_pointer = nearest + target * hatch_unit_vector; + + // some limited feedback: allow persistent pulling to slightly change + // the spacing + this->hatch_spacing += (hatch_dist - this->hatch_spacing)/3500; + + // return it to the desktop coords + motion_dt = new_pointer * motion_to_curve.inverse(); + + if (speed >= SPEED_NORMAL) { + this->inertia_vectors.push_front(motion_dt); + if (this->inertia_vectors.size() > INERTIA_ELEMENTS) + this->inertia_vectors.pop_back(); + } + + } else { + // this is the first motion event, set the dist + this->hatch_spacing = hatch_dist; + } + + // remember last points + this->hatch_last_pointer = pointer; + this->hatch_last_nearest = nearest; + + this->hatch_vectors.push_front(pointer - nearest); + if (this->hatch_vectors.size() > HATCH_VECTOR_ELEMENTS) + this->hatch_vectors.pop_back(); + } + + this->message_context->set(Inkscape::NORMAL_MESSAGE, this->hatch_escaped? _("Tracking: <b>connection to guide path lost!</b>") : _("<b>Tracking</b> a guide path")); + + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> a calligraphic stroke")); + } + + if (this->just_started_drawing) { + this->just_started_drawing = false; + this->reset(motion_dt); + } + + if (!this->apply(motion_dt)) { + ret = TRUE; + break; + } + + if ( this->cur != this->last ) { + this->brush(); + g_assert( this->npoints > 0 ); + this->fit_and_split(false); + } + ret = TRUE; + } + + // Draw the hatching circle if necessary + if (event->motion.state & GDK_CONTROL_MASK) { + if (this->hatch_spacing == 0 && hatch_dist != 0) { + // Haven't set spacing yet: gray, center free, update radius live + Geom::Point c = desktop->w2d(motion_w); + Geom::Affine const sm (Geom::Scale(hatch_dist, hatch_dist) * Geom::Translate(c)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } else if (this->dragging && !this->hatch_escaped) { + // Tracking: green, center snapped, fixed radius + Geom::Point c = motion_dt; + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x00FF00ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } else if (this->dragging && this->hatch_escaped) { + // Tracking escaped: red, center free, fixed radius + Geom::Point c = motion_dt; + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0xFF0000ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } else { + // Not drawing but spacing set: gray, center snapped, fixed radius + Geom::Point c = (nearest + this->hatch_spacing * hatch_unit_vector) * motion_to_curve.inverse(); + if (!std::isnan(c[Geom::X]) && !std::isnan(c[Geom::Y])) { + Geom::Affine const sm (Geom::Scale(this->hatch_spacing, this->hatch_spacing) * Geom::Translate(c)); + sp_canvas_item_affine_absolute(this->hatch_area, sm); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->hatch_area), 0x7f7f7fff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->hatch_area); + } + } + } else { + sp_canvas_item_hide(this->hatch_area); + } + } + break; + + + case GDK_BUTTON_RELEASE: + { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + desktop->canvas->endForcedFullRedraws(); + set_high_motion_precision(false); + this->is_drawing = false; + + if (this->dragging && event->button.button == 1 && !this->space_panning) { + this->dragging = FALSE; + + this->apply(motion_dt); + + /* Remove all temporary line segments */ + for (auto i:this->segments) + sp_canvas_item_destroy(SP_CANVAS_ITEM(i)); + this->segments.clear(); + + /* Create object */ + this->fit_and_split(true); + if (this->accumulate()) + this->set_to_accumulated(event->button.state & GDK_SHIFT_MASK, event->button.state & GDK_MOD1_MASK); // performs document_done + else + g_warning ("Failed to create path: invalid data in dc->cal1 or dc->cal2"); + + /* reset accumulated curve */ + this->accumulated->reset(); + + this->clear_current(); + if (this->repr) { + this->repr = nullptr; + } + + if (!this->hatch_pointer_past.empty()) this->hatch_pointer_past.clear(); + if (!this->hatch_nearest_past.empty()) this->hatch_nearest_past.clear(); + if (!this->inertia_vectors.empty()) this->inertia_vectors.clear(); + if (!this->hatch_vectors.empty()) this->hatch_vectors.clear(); + this->hatch_last_nearest = Geom::Point(0,0); + this->hatch_last_pointer = Geom::Point(0,0); + this->hatch_escaped = false; + this->hatch_item = nullptr; + this->hatch_livarot_path = nullptr; + this->just_started_drawing = false; + + if (this->hatch_spacing != 0 && !this->keep_selected) { + // we do not select the newly drawn path, so increase spacing by step + if (this->hatch_spacing_step == 0) { + this->hatch_spacing_step = this->hatch_spacing; + } + this->hatch_spacing += this->hatch_spacing_step; + } + + this->message_context->clear(); + ret = TRUE; + } else if (!this->dragging && event->button.button == 1 && !this->space_panning){ + spdc_create_single_dot(this, this->desktop->w2d(motion_w), "/tools/calligraphic", event->button.state); + } + break; + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->angle += 5.0; + if (this->angle > 90.0) + this->angle = 90.0; + sp_ddc_update_toolbox (desktop, "calligraphy-angle", this->angle); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->angle -= 5.0; + if (this->angle < -90.0) + this->angle = -90.0; + sp_ddc_update_toolbox (desktop, "calligraphy-angle", this->angle); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + if (this->width > 1.0) + this->width = 1.0; + sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100); // the same spinbutton is for alt+x + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + if (this->width < 0.01) + this->width = 0.01; + sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + sp_ddc_update_toolbox (desktop, "calligraphy-width", this->width * 100); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("calligraphy-width"); + ret = TRUE; + } + break; + case GDK_KEY_Escape: + if (this->is_drawing) { + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__CTRL_ONLY(event) && this->is_drawing) { + // if drawing, cancel, otherwise pass it up for undo + this->cancel(); + ret = TRUE; + } + break; + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->message_context->clear(); + this->hatch_spacing = 0; + this->hatch_spacing_step = 0; + break; + default: + break; + } + break; + + default: + break; + } + + if (!ret) { +// if ((SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler) { +// ret = (SP_EVENT_CONTEXT_CLASS(sp_dyna_draw_context_parent_class))->root_handler(event_context, event); +// } + ret = DynamicBase::root_handler(event); + } + + return ret; +} + + +void CalligraphicTool::clear_current() { + /* reset bpath */ + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), nullptr); + /* reset curve */ + this->currentcurve->reset(); + this->cal1->reset(); + this->cal2->reset(); + /* reset points */ + this->npoints = 0; +} + +void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) { + if (!this->accumulated->is_empty()) { + if (!this->repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool (desktop, repr, "/tools/calligraphic", false); + + this->repr = repr; + + SPItem *item=SP_ITEM(desktop->currentLayer()->appendChildRepr(this->repr)); + Inkscape::GC::release(this->repr); + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + item->updateRepr(); + } + + Geom::PathVector pathv = this->accumulated->get_pathvector() * desktop->dt2doc(); + gchar *str = sp_svg_write_path(pathv); + g_assert( str != nullptr ); + this->repr->setAttribute("d", str); + g_free(str); + + if (unionize) { + desktop->getSelection()->add(this->repr); + desktop->getSelection()->pathUnion(true); + } else if (subtract) { + desktop->getSelection()->add(this->repr); + desktop->getSelection()->pathDiff(true); + } else { + if (this->keep_selected) { + desktop->getSelection()->set(this->repr); + } + } + + // Now we need to write the transform information. + // First, find out whether our repr is still linked to a valid object. In this case, + // we need to write the transform data only for this element. + // Either there was no boolean op or it failed. + SPItem *result = SP_ITEM(desktop->doc()->getObjectByRepr(this->repr)); + + if (result == nullptr) { + // The boolean operation succeeded. + // Now we fetch the single item, that has been set as selected by the boolean op. + // This is its result. + result = desktop->getSelection()->singleItem(); + } + result->doWriteTransform(result->transform, nullptr, true); + } else { + if (this->repr) { + sp_repr_unparent(this->repr); + } + + this->repr = nullptr; + } + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_CALLIGRAPHIC, + _("Draw calligraphic stroke")); +} + +static void +add_cap(SPCurve *curve, + Geom::Point const &from, + Geom::Point const &to, + double rounding) +{ + if (Geom::L2( to - from ) > DYNA_EPSILON) { + Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0); + double mag = Geom::L2(vel); + + Geom::Point v = mag * Geom::rot90( to - from ) / Geom::L2( to - from ); + curve->curveto(from + v, to + v, to); + } +} + +bool CalligraphicTool::accumulate() { + if ( + this->cal1->is_empty() || + this->cal2->is_empty() || + (this->cal1->get_segment_count() <= 0) || + this->cal1->first_path()->closed() + ) { + + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + SPCurve *rev_cal2 = this->cal2->create_reverse(); + + if ((rev_cal2->get_segment_count() <= 0) || rev_cal2->first_path()->closed()) { + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + + return false; // failure + } + + Geom::Curve const * dc_cal1_firstseg = this->cal1->first_segment(); + Geom::Curve const * rev_cal2_firstseg = rev_cal2->first_segment(); + Geom::Curve const * dc_cal1_lastseg = this->cal1->last_segment(); + Geom::Curve const * rev_cal2_lastseg = rev_cal2->last_segment(); + + this->accumulated->reset(); /* Is this required ?? */ + + this->accumulated->append(this->cal1, false); + + add_cap(this->accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), this->cap_rounding); + + this->accumulated->append(rev_cal2, true); + + add_cap(this->accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), this->cap_rounding); + + this->accumulated->closepath(); + + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + + return true; // success +} + +static double square(double const x) +{ + return x * x; +} + +void CalligraphicTool::fit_and_split(bool release) { + double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC ); + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:R=%c]", release?'T':'F'); +#endif + + if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) { + return; // just clicked + } + + if ( this->npoints == SAMPLING_SIZE - 1 || release ) { +#define BEZIER_SIZE 4 +#define BEZIER_MAX_BEZIERS 8 +#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS ) + +#ifdef DYNA_DRAW_VERBOSE + g_print("[F&S:#] dc->npoints:%d, release:%s\n", + this->npoints, release ? "TRUE" : "FALSE"); +#endif + + /* Current calligraphic */ + if ( this->cal1->is_empty() || this->cal2->is_empty() ) { + /* dc->npoints > 0 */ + /* g_print("calligraphics(1|2) reset\n"); */ + this->cal1->reset(); + this->cal2->reset(); + + this->cal1->moveto(this->point1[0]); + this->cal2->moveto(this->point2[0]); + } + + Geom::Point b1[BEZIER_MAX_LENGTH]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) ); + + Geom::Point b2[BEZIER_MAX_LENGTH]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, + tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) ); + + if ( nb1 != -1 && nb2 != -1 ) { + /* Fit and draw and reset state */ +#ifdef DYNA_DRAW_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + /* CanvasShape */ + if (! release) { + this->currentcurve->reset(); + this->currentcurve->moveto(b1[0]); + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]); + for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) { + this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + // FIXME: dc->segments is always NULL at this point?? + if (this->segments.empty()) { // first segment + add_cap(this->currentcurve, b2[0], b1[0], this->cap_rounding); + } + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true); + } + + /* Current calligraphic */ + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) { + this->cal2->curveto(bp2[1], bp2[2], bp2[3]); + } + } else { + /* fixme: ??? */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[fit_and_split] failed to fit-cubic.\n"); +#endif + this->draw_temporary_box(); + + for (gint i = 1; i < this->npoints; i++) { + this->cal1->lineto(this->point1[i]); + } + for (gint i = 1; i < this->npoints; i++) { + this->cal2->lineto(this->point2[i]); + } + } + + /* Fit and draw and copy last point */ +#ifdef DYNA_DRAW_VERBOSE + g_print("[%d]Yup\n", this->npoints); +#endif + if (!release) { + g_assert(!this->currentcurve->is_empty()); + + SPCanvasItem *cbp = sp_canvas_item_new(desktop->getSketch(), + SP_TYPE_CANVAS_BPATH, + nullptr); + SPCurve *curve = this->currentcurve->copy(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve, true); + curve->unref(); + + guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", true); + //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/calligraphic", false); + double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/calligraphic"); + double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", true); + //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/calligraphic", false); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD); + //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing + //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + + this->segments.push_back(cbp); + } + + this->point1[0] = this->point1[this->npoints - 1]; + this->point2[0] = this->point2[this->npoints - 1]; + this->npoints = 1; + } else { + this->draw_temporary_box(); + } +} + +void CalligraphicTool::draw_temporary_box() { + this->currentcurve->reset(); + + this->currentcurve->moveto(this->point2[this->npoints-1]); + + for (gint i = this->npoints-2; i >= 0; i--) { + this->currentcurve->lineto(this->point2[i]); + } + + for (gint i = 0; i < this->npoints; i++) { + this->currentcurve->lineto(this->point1[i]); + } + + if (this->npoints >= 2) { + add_cap(this->currentcurve, this->point1[this->npoints-1], this->point2[this->npoints-1], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true); +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/calligraphic-tool.h b/src/ui/tools/calligraphic-tool.h new file mode 100644 index 0000000..924d018 --- /dev/null +++ b/src/ui/tools/calligraphic-tool.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_DYNA_DRAW_CONTEXT_H_SEEN +#define SP_DYNA_DRAW_CONTEXT_H_SEEN + +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> +#include <string> + +#include <2geom/point.h> + +#include "ui/tools/dynamic-base.h" + +class SPItem; +class Path; +struct SPCanvasItem; + +#define DDC_MIN_PRESSURE 0.0 +#define DDC_MAX_PRESSURE 1.0 +#define DDC_DEFAULT_PRESSURE 1.0 + +#define DDC_MIN_TILT -1.0 +#define DDC_MAX_TILT 1.0 +#define DDC_DEFAULT_TILT 0.0 + +namespace Inkscape { +namespace UI { +namespace Tools { + +class CalligraphicTool : public DynamicBase { +public: + CalligraphicTool(); + ~CalligraphicTool() override; + + static const std::string prefsPath; + + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + /** newly created object remain selected */ + bool keep_selected; + + double hatch_spacing; + double hatch_spacing_step; + SPItem *hatch_item; + Path *hatch_livarot_path; + std::list<double> hatch_nearest_past; + std::list<double> hatch_pointer_past; + std::list<Geom::Point> inertia_vectors; + Geom::Point hatch_last_nearest, hatch_last_pointer; + std::list<Geom::Point> hatch_vectors; + bool hatch_escaped; + SPCanvasItem *hatch_area; + bool just_started_drawing; + bool trace_bg; + + void clear_current(); + void set_to_accumulated(bool unionize, bool subtract); + bool accumulate(); + void fit_and_split(bool release); + void draw_temporary_box(); + void cancel(); + void brush(); + bool apply(Geom::Point p); + void extinput(GdkEvent *event); + void reset(Geom::Point p); +}; + +} +} +} + +#endif // SP_DYNA_DRAW_CONTEXT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/connector-tool.cpp b/src/ui/tools/connector-tool.cpp new file mode 100644 index 0000000..d3425a2 --- /dev/null +++ b/src/ui/tools/connector-tool.cpp @@ -0,0 +1,1393 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Connector creation tool + * + * Authors: + * Michael Wybrow <mjwybrow@users.sourceforge.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Martin Owens <doctormo@gmail.com> + * + * Copyright (C) 2005-2008 Michael Wybrow + * Copyright (C) 2009 Monash University + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * TODO: + * o Show a visual indicator for objects with the 'avoid' property set. + * o Allow user to change a object between a path and connector through + * the interface. + * o Create an interface for setting markers (arrow heads). + * o Better distinguish between paths and connectors to prevent problems + * in the node tool and paths accidentally being turned into connectors + * in the connector tool. Perhaps have a way to convert between. + * o Only call libavoid's updateEndPoint as required. Currently we do it + * for both endpoints, even if only one is moving. + * o Deal sanely with connectors with both endpoints attached to the + * same connection point, and drawing of connectors attaching + * overlapping shapes (currently tries to adjust connector to be + * outside both bounding boxes). + * o Fix many special cases related to connectors updating, + * e.g., copying a couple of shapes and a connector that are + * attached to each other. + * e.g., detach connector when it is moved or transformed in + * one of the other contexts. + * o Cope with shapes whose ids change when they have attached + * connectors. + * o During dragging motion, gobble up to and use the final motion event. + * Gobbling away all duplicates after the current can occasionally result + * in the path lagging behind the mouse cursor if it is no longer being + * dragged. + * o Fix up libavoid's representation after undo actions. It doesn't see + * any transform signals and hence doesn't know shapes have moved back to + * there earlier positions. + * + * ---------------------------------------------------------------------------- + * + * Notes: + * + * Much of the way connectors work for user-defined points has been + * changed so that it no longer defines special attributes to record + * the points. Instead it uses single node paths to define points + * who are then separate objects that can be fixed on the canvas, + * grouped into objects and take full advantage of all transform, snap + * and align functionality of all other objects. + * + * I think that the style change between polyline and orthogonal + * would be much clearer with two buttons (radio behaviour -- just + * one is true). + * + * The other tools show a label change from "New:" to "Change:" + * depending on whether an object is selected. We could consider + * this but there may not be space. + * + * Likewise for the avoid/ignore shapes buttons. These should be + * inactive when a shape is not selected in the connector context. + * + */ + +#include <string> +#include <cstring> + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> +#include <gdk/gdkkeysyms.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" +#include "snap.h" +#include "verbs.h" + +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "3rdparty/adaptagrams/libavoid/router.h" + +#include "object/sp-conn-end.h" +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-text.h" + +#include "ui/pixmaps/cursor-connector.xpm" + +#include "svg/svg.h" + +#include "ui/tools/connector-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void cc_clear_active_knots(SPKnotList k); + +static void shape_event_attr_deleted(Inkscape::XML::Node *repr, + Inkscape::XML::Node *child, Inkscape::XML::Node *ref, gpointer data); + +static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const *old_value, gchar const *new_value, bool is_interactive, + gpointer data); + +static void cc_select_handle(SPKnot* knot); +static void cc_deselect_handle(SPKnot* knot); +static bool cc_item_is_shape(SPItem *item); + +/*static Geom::Point connector_drag_origin_w(0, 0); +static bool connector_within_tolerance = false;*/ + +static Inkscape::XML::NodeEventVector shape_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_added */ + shape_event_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +static Inkscape::XML::NodeEventVector layer_repr_events = { + nullptr, /* child_added */ + shape_event_attr_deleted, + nullptr, /* child_added */ + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +std::string const& ConnectorTool::getPrefsPath() +{ + return ConnectorTool::prefsPath; +} + +std::string const ConnectorTool::prefsPath = "/tools/connector"; + +ConnectorTool::ConnectorTool() + : ToolBase(cursor_connector_xpm) + , selection(nullptr) + , npoints(0) + , state(SP_CONNECTOR_CONTEXT_IDLE) + , red_bpath(nullptr) + , red_curve(nullptr) + , red_color(0xff00007f) + , green_curve(nullptr) + , newconn(nullptr) + , newConnRef(nullptr) + , curvature(0.0) + , isOrthogonal(false) + , active_shape(nullptr) + , active_shape_repr(nullptr) + , active_shape_layer_repr(nullptr) + , active_conn(nullptr) + , active_conn_repr(nullptr) + , active_handle(nullptr) + , selected_handle(nullptr) + , clickeditem(nullptr) + , clickedhandle(nullptr) + , shref(nullptr) + , ehref(nullptr) + , c0(nullptr) + , c1(nullptr) + , cl0(nullptr) + , cl1(nullptr) +{ + for (int i = 0; i < 2; ++i) { + this->endpt_handle[i] = nullptr; + this->endpt_handler_id[i] = 0; + } +} + +ConnectorTool::~ConnectorTool() +{ + this->sel_changed_connection.disconnect(); + + for (auto & i : this->endpt_handle) { + if (this->endpt_handle[1]) { + //g_object_unref(this->endpt_handle[i]); + knot_unref(i); + i = nullptr; + } + } + + if (this->shref) { + g_free(this->shref); + this->shref = nullptr; + } + + if (this->ehref) { + g_free(this->shref); + this->shref = nullptr; + } + + g_assert( this->newConnRef == nullptr ); +} + +void ConnectorTool::setup() +{ + ToolBase::setup(); + + this->selection = this->desktop->getSelection(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->selection->connectChanged( + sigc::mem_fun(this, &ConnectorTool::_selectionChanged) + ); + + /* Create red bpath */ + this->red_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, + 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->red_bpath), 0x00000000, + SP_WIND_RULE_NONZERO); + /* Create red curve */ + this->red_curve = new SPCurve(); + + /* Create green curve */ + this->green_curve = new SPCurve(); + + // Notice the initial selection. + //cc_selection_changed(this->selection, (gpointer) this); + this->_selectionChanged(this->selection); + + this->within_tolerance = false; + + sp_event_context_read(this, "curvature"); + sp_event_context_read(this, "orthogonal"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/connector/selcue", false)) { + this->enableSelectionCue(); + } + + // Make sure we see all enter events for canvas items, + // even if a mouse button is depressed. + this->desktop->canvas->_gen_all_enter_events = true; +} + +void ConnectorTool::set(const Inkscape::Preferences::Entry& val) +{ + /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like + * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */ + Glib::ustring name = val.getEntryName(); + + if (name == "curvature") { + this->curvature = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up + } else if (name == "orthogonal") { + this->isOrthogonal = val.getBool(); + } +} + +void ConnectorTool::finish() +{ + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + + ToolBase::finish(); + + if (this->selection) { + this->selection = nullptr; + } + + this->cc_clear_active_shape(); + this->cc_clear_active_conn(); + + // Restore the default event generating behaviour. + this->desktop->canvas->_gen_all_enter_events = false; +} + +//----------------------------------------------------------------------------- + + +void ConnectorTool::cc_clear_active_shape() +{ + if (this->active_shape == nullptr) { + return; + } + g_assert( this->active_shape_repr ); + g_assert( this->active_shape_layer_repr ); + + this->active_shape = nullptr; + + if (this->active_shape_repr) { + sp_repr_remove_listener_by_data(this->active_shape_repr, this); + Inkscape::GC::release(this->active_shape_repr); + this->active_shape_repr = nullptr; + + sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this); + Inkscape::GC::release(this->active_shape_layer_repr); + this->active_shape_layer_repr = nullptr; + } + + cc_clear_active_knots(this->knots); +} + +static void cc_clear_active_knots(SPKnotList k) +{ + // Hide the connection points if they exist. + if (k.size()) { + for (auto & it : k) { + it.first->hide(); + } + } +} + +void ConnectorTool::cc_clear_active_conn() +{ + if (this->active_conn == nullptr) { + return; + } + g_assert( this->active_conn_repr ); + + this->active_conn = nullptr; + + if (this->active_conn_repr) { + sp_repr_remove_listener_by_data(this->active_conn_repr, this); + Inkscape::GC::release(this->active_conn_repr); + this->active_conn_repr = nullptr; + } + + // Hide the endpoint handles. + for (auto & i : this->endpt_handle) { + if (i) { + i->hide(); + } + } +} + + +bool ConnectorTool::_ptHandleTest(Geom::Point& p, gchar **href) +{ + if (this->active_handle && (this->knots.find(this->active_handle) != this->knots.end())) { + p = this->active_handle->pos; + *href = g_strdup_printf("#%s", this->active_handle->owner->getId()); + return true; + } + *href = nullptr; + return false; +} + +static void cc_select_handle(SPKnot* knot) +{ + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(10); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0x0000ffff, 0x0000ffff, 0x0000ffff, 0x0000ffff); + knot->updateCtrl(); +} + +static void cc_deselect_handle(SPKnot* knot) +{ + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(8); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + knot->updateCtrl(); +} + +bool ConnectorTool::item_handler(SPItem* item, GdkEvent* event) +{ + bool ret = false; + + Geom::Point p(event->button.x, event->button.y); + + switch (event->type) { + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + if ((this->state == SP_CONNECTOR_CONTEXT_DRAGGING) && this->within_tolerance) { + this->_resetColors(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + } + + if (this->state != SP_CONNECTOR_CONTEXT_IDLE) { + // Doing something else like rerouting. + break; + } + + // find out clicked item, honoring Alt + SPItem *item = sp_event_context_find_item(desktop, p, event->button.state & GDK_MOD1_MASK, FALSE); + + if (event->button.state & GDK_SHIFT_MASK) { + this->selection->toggle(item); + } else { + this->selection->set(item); + /* When selecting a new item, do not allow showing + connection points on connectors. (yet?) + */ + + if (item != this->active_shape && !cc_item_is_connector(item)) { + this->_setActiveShape(item); + } + } + + ret = true; + } + break; + + case GDK_ENTER_NOTIFY: + if (!this->selected_handle) { + if (cc_item_is_shape(item)) { + this->_setActiveShape(item); + } + + ret = true; + } + break; + + default: + break; + } + + return ret; +} + +bool ConnectorTool::root_handler(GdkEvent* event) +{ + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(get_latin_keyval (&event->key)); + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + + +bool ConnectorTool::_handleButtonPress(GdkEventButton const &bevent) +{ + Geom::Point const event_w(bevent.x, bevent.y); + /* Find desktop coordinates */ + Geom::Point p = this->desktop->w2d(event_w); + + bool ret = false; + + if ( bevent.button == 1 && !this->space_panning ) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return true; + } + + Geom::Point const event_w(bevent.x, bevent.y); + + this->xp = bevent.x; + this->yp = bevent.y; + this->within_tolerance = true; + + Geom::Point const event_dt = this->desktop->w2d(event_w); + + SnapManager &m = this->desktop->namedview->snap_manager; + + switch (this->state) { + case SP_CONNECTOR_CONTEXT_STOP: + /* This is allowed, if we just canceled curve */ + case SP_CONNECTOR_CONTEXT_IDLE: + { + if ( this->npoints == 0 ) { + this->cc_clear_active_conn(); + + this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new connector")); + + /* Set start anchor */ + /* Create green anchor */ + Geom::Point p = event_dt; + + // Test whether we clicked on a connection point + bool found = this->_ptHandleTest(p, &this->shref); + + if (!found) { + // This is the first point, so just snap it to the grid + // as there's no other points to go off. + m.setup(this->desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + } + this->_setInitialPoint(p); + + } + this->state = SP_CONNECTOR_CONTEXT_DRAGGING; + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + // This is the second click of a connector creation. + m.setup(this->desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + this->_setSubsequentPoint(p); + this->_finishSegment(p); + + this->_ptHandleTest(p, &this->ehref); + if (this->npoints != 0) { + this->_finish(); + } + this->cc_set_active_conn(this->newconn); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_CLOSE: + { + g_warning("Button down in CLOSE state"); + break; + } + default: + break; + } + } else if (bevent.button == 3) { + if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) { + // A context menu is going to be triggered here, + // so end the rerouting operation. + this->_reroutingFinish(&p); + + this->state = SP_CONNECTOR_CONTEXT_IDLE; + + // Don't set ret to TRUE, so we drop through to the + // parent handler which will open the context menu. + } else if (this->npoints != 0) { + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + } + } + return ret; +} + +bool ConnectorTool::_handleMotionNotify(GdkEventMotion const &mevent) +{ + bool ret = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (this->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) { + // allow middle-button scrolling + return false; + } + + Geom::Point const event_w(mevent.x, mevent.y); + + if (this->within_tolerance) { + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( ( abs( (gint) mevent.x - this->xp ) < this->tolerance ) && + ( abs( (gint) mevent.y - this->yp ) < this->tolerance ) ) { + return false; // Do not drag if we're within tolerance from origin. + } + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process + // the motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + /* Find desktop coordinates */ + Geom::Point p = desktop->w2d(event_w); + + SnapManager &m = desktop->namedview->snap_manager; + + switch (this->state) { + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + gobble_motion_events(mevent.state); + // This is movement during a connector creation. + if ( this->npoints > 0 ) { + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + this->selection->clear(); + this->_setSubsequentPoint(p); + ret = true; + } + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + gobble_motion_events(GDK_BUTTON1_MASK); + g_assert( SP_IS_PATH(this->clickeditem)); + + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + // Update the hidden path + Geom::Affine i2d ( (this->clickeditem)->i2dt_affine() ); + Geom::Affine d2i = i2d.inverse(); + SPPath *path = SP_PATH(this->clickeditem); + SPCurve *curve = path->getCurve(true); + if (this->clickedhandle == this->endpt_handle[0]) { + Geom::Point o = this->endpt_handle[1]->pos; + curve->stretch_endpoints(p * d2i, o * d2i); + } else { + Geom::Point o = this->endpt_handle[0]->pos; + curve->stretch_endpoints(o * d2i, p * d2i); + } + sp_conn_reroute_path_immediate(path); + + // Copy this to the temporary visible path + this->red_curve = path->getCurveForEdit(); + this->red_curve->transform(i2d); + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); + ret = true; + break; + } + case SP_CONNECTOR_CONTEXT_STOP: + /* This is perfectly valid */ + break; + default: + if (!this->sp_event_context_knot_mouseover()) { + m.setup(desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +bool ConnectorTool::_handleButtonRelease(GdkEventButton const &revent) +{ + bool ret = false; + + if ( revent.button == 1 && !this->space_panning ) { + SPDocument *doc = desktop->getDocument(); + SnapManager &m = desktop->namedview->snap_manager; + + Geom::Point const event_w(revent.x, revent.y); + + /* Find desktop coordinates */ + Geom::Point p = this->desktop->w2d(event_w); + + switch (this->state) { + //case SP_CONNECTOR_CONTEXT_POINT: + case SP_CONNECTOR_CONTEXT_DRAGGING: + { + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + + if (this->within_tolerance) { + this->_finishSegment(p); + return true; + } + // Connector has been created via a drag, end it now. + this->_setSubsequentPoint(p); + this->_finishSegment(p); + // Test whether we clicked on a connection point + this->_ptHandleTest(p, &this->ehref); + if (this->npoints != 0) { + this->_finish(); + } + this->cc_set_active_conn(this->newconn); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + break; + } + case SP_CONNECTOR_CONTEXT_REROUTING: + { + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.unSetup(); + this->_reroutingFinish(&p); + + doc->ensureUpToDate(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + return true; + break; + } + case SP_CONNECTOR_CONTEXT_STOP: + /* This is allowed, if we just cancelled curve */ + break; + default: + break; + } + ret = true; + } + return ret; +} + +bool ConnectorTool::_handleKeyPress(guint const keyval) +{ + bool ret = false; + + switch (keyval) { + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (this->npoints != 0) { + this->_finish(); + this->state = SP_CONNECTOR_CONTEXT_IDLE; + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->state == SP_CONNECTOR_CONTEXT_REROUTING) { + SPDocument *doc = desktop->getDocument(); + + this->_reroutingFinish(nullptr); + + DocumentUndo::undo(doc); + + this->state = SP_CONNECTOR_CONTEXT_IDLE; + desktop->messageStack()->flash( Inkscape::NORMAL_MESSAGE, + _("Connector endpoint drag cancelled.")); + ret = true; + } else if (this->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + this->state = SP_CONNECTOR_CONTEXT_STOP; + this->_resetColors(); + ret = true; + } + break; + default: + break; + } + return ret; +} + +void ConnectorTool::_reroutingFinish(Geom::Point *const p) +{ + SPDocument *doc = desktop->getDocument(); + + // Clear the temporary path: + this->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); + + if (p != nullptr) { + // Test whether we clicked on a connection point + gchar *shape_label; + bool found = this->_ptHandleTest(*p, &shape_label); + + if (found) { + if (this->clickedhandle == this->endpt_handle[0]) { + this->clickeditem->setAttribute("inkscape:connection-start", shape_label); + } else { + this->clickeditem->setAttribute("inkscape:connection-end", shape_label); + } + g_free(shape_label); + } + } + this->clickeditem->setHidden(false); + sp_conn_reroute_path_immediate(SP_PATH(this->clickeditem)); + this->clickeditem->updateRepr(); + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, _("Reroute connector")); + this->cc_set_active_conn(this->clickeditem); +} + + +void ConnectorTool::_resetColors() +{ + /* Red */ + this->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); + + this->green_curve->reset(); + this->npoints = 0; +} + +void ConnectorTool::_setInitialPoint(Geom::Point const p) +{ + g_assert( this->npoints == 0 ); + + this->p[0] = p; + this->p[1] = p; + this->npoints = 2; + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); +} + +void ConnectorTool::_setSubsequentPoint(Geom::Point const p) +{ + g_assert( this->npoints != 0 ); + + Geom::Point o = desktop->dt2doc(this->p[0]); + Geom::Point d = desktop->dt2doc(p); + Avoid::Point src(o[Geom::X], o[Geom::Y]); + Avoid::Point dst(d[Geom::X], d[Geom::Y]); + + if (!this->newConnRef) { + Avoid::Router *router = desktop->getDocument()->getRouter(); + this->newConnRef = new Avoid::ConnRef(router); + this->newConnRef->setEndpoint(Avoid::VertID::src, src); + if (this->isOrthogonal) + this->newConnRef->setRoutingType(Avoid::ConnType_Orthogonal); + else + this->newConnRef->setRoutingType(Avoid::ConnType_PolyLine); + } + // Set new endpoint. + this->newConnRef->setEndpoint(Avoid::VertID::tar, dst); + // Immediately generate new routes for connector. + this->newConnRef->makePathInvalid(); + this->newConnRef->router()->processTransaction(); + // Recreate curve from libavoid route. + recreateCurve( this->red_curve, this->newConnRef, this->curvature ); + this->red_curve->transform(desktop->doc2dt()); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); +} + + +/** + * Concats red, blue and green. + * If any anchors are defined, process these, optionally removing curves from white list + * Invoke _flush_white to write result back to object. + */ +void ConnectorTool::_concatColorsAndFlush() +{ + SPCurve *c = this->green_curve; + this->green_curve = new SPCurve(); + + this->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); + + if (c->is_empty()) { + c->unref(); + return; + } + + this->_flushWhite(c); + + c->unref(); +} + + +/* + * Flushes white curve(s) and additional curve into object + * + * No cleaning of colored curves - this has to be done by caller + * No rereading of white data, so if you cannot rely on ::modified, do it in caller + * + */ + +void ConnectorTool::_flushWhite(SPCurve *gc) +{ + SPCurve *c; + + if (gc) { + c = gc; + c->ref(); + } else { + return; + } + + /* Now we have to go back to item coordinates at last */ + c->transform(this->desktop->dt2doc()); + + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if ( c && !c->is_empty() ) { + /* We actually have something to write */ + + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool(desktop, repr, "/tools/connector", false); + + gchar *str = sp_svg_write_path( c->get_pathvector() ); + g_assert( str != nullptr ); + repr->setAttribute("d", str); + g_free(str); + + /* Attach repr */ + this->newconn = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + this->newconn->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + + bool connection = false; + this->newconn->setAttribute( "inkscape:connector-type", + this->isOrthogonal ? "orthogonal" : "polyline", nullptr ); + this->newconn->setAttribute( "inkscape:connector-curvature", + Glib::Ascii::dtostr(this->curvature).c_str(), nullptr ); + if (this->shref) { + this->newconn->setAttribute( "inkscape:connection-start", this->shref); + connection = true; + } + + if (this->ehref) { + this->newconn->setAttribute( "inkscape:connection-end", this->ehref); + connection = true; + } + // Process pending updates. + this->newconn->updateRepr(); + doc->ensureUpToDate(); + + if (connection) { + // Adjust endpoints to shape edge. + sp_conn_reroute_path_immediate(SP_PATH(this->newconn)); + this->newconn->updateRepr(); + } + + this->newconn->doWriteTransform(this->newconn->transform, nullptr, true); + + // Only set the selection after we are finished with creating the attributes of + // the connector. Otherwise, the selection change may alter the defaults for + // values like curvature in the connector context, preventing subsequent lookup + // of their original values. + this->selection->set(repr); + Inkscape::GC::release(repr); + } + + c->unref(); + + DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, _("Create connector")); +} + + +void ConnectorTool::_finishSegment(Geom::Point const /*p*/) +{ + if (!this->red_curve->is_empty()) { + this->green_curve->append_continuous(this->red_curve, 0.0625); + + this->p[0] = this->p[3]; + this->p[1] = this->p[4]; + this->npoints = 2; + + this->red_curve->reset(); + } +} + +void ConnectorTool::_finish() +{ + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing connector")); + + this->red_curve->reset(); + this->_concatColorsAndFlush(); + + this->npoints = 0; + + if (this->newConnRef) { + this->newConnRef->router()->deleteConnector(this->newConnRef); + this->newConnRef = nullptr; + } +} + + +static gboolean cc_generic_knot_handler(SPCanvasItem *, GdkEvent *event, SPKnot *knot) +{ + g_assert (knot != nullptr); + + //g_object_ref(knot); + knot_ref(knot); + + ConnectorTool *cc = SP_CONNECTOR_CONTEXT( + knot->desktop->event_context); + + gboolean consumed = FALSE; + + gchar const *knot_tip = "Click to join at this point"; + switch (event->type) { + case GDK_ENTER_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, TRUE); + + cc->active_handle = knot; + if (knot_tip) { + knot->desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, knot_tip); + } + + consumed = TRUE; + break; + case GDK_LEAVE_NOTIFY: + knot->setFlag(SP_KNOT_MOUSEOVER, FALSE); + + /* FIXME: the following test is a workaround for LP Bug #1273510. + * It seems that a signal is not correctly disconnected, maybe + * something missing in cc_clear_active_conn()? */ + if (cc) { + cc->active_handle = nullptr; + } + + if (knot_tip) { + knot->desktop->event_context->defaultMessageContext()->clear(); + } + + consumed = TRUE; + break; + default: + break; + } + + //g_object_unref(knot); + knot_unref(knot); + + return consumed; +} + + +static gboolean endpt_handler(SPKnot */*knot*/, GdkEvent *event, ConnectorTool *cc) +{ + //g_assert( SP_IS_CONNECTOR_CONTEXT(cc) ); + + gboolean consumed = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + g_assert( (cc->active_handle == cc->endpt_handle[0]) || + (cc->active_handle == cc->endpt_handle[1]) ); + if (cc->state == SP_CONNECTOR_CONTEXT_IDLE) { + cc->clickeditem = cc->active_conn; + cc->clickedhandle = cc->active_handle; + cc->cc_clear_active_conn(); + cc->state = SP_CONNECTOR_CONTEXT_REROUTING; + + // Disconnect from attached shape + unsigned ind = (cc->active_handle == cc->endpt_handle[0]) ? 0 : 1; + sp_conn_end_detach(cc->clickeditem, ind); + + Geom::Point origin; + if (cc->clickedhandle == cc->endpt_handle[0]) { + origin = cc->endpt_handle[1]->pos; + } else { + origin = cc->endpt_handle[0]->pos; + } + + // Show the red path for dragging. + cc->red_curve = SP_PATH(cc->clickeditem)->getCurveForEdit(); + Geom::Affine i2d = (cc->clickeditem)->i2dt_affine(); + cc->red_curve->transform(i2d); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(cc->red_bpath), cc->red_curve, true); + + cc->clickeditem->setHidden(true); + + // The rest of the interaction rerouting the connector is + // handled by the context root handler. + consumed = TRUE; + } + break; + default: + break; + } + + return consumed; +} + +void ConnectorTool::_activeShapeAddKnot(SPItem* item) +{ + SPKnot *knot = new SPKnot(desktop, nullptr); + + knot->owner = item; + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(8); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + knot->updateCtrl(); + + // We don't want to use the standard knot handler. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + + knot->_event_handler_id = 0; + + g_signal_connect(G_OBJECT(knot->item), "event", + G_CALLBACK(cc_generic_knot_handler), knot); + + knot->setPosition(item->getAvoidRef().getConnectionPointPos() * desktop->doc2dt(), 0); + knot->show(); + this->knots[knot] = 1; +} + +void ConnectorTool::_setActiveShape(SPItem *item) +{ + g_assert(item != nullptr ); + + if (this->active_shape != item) { + // The active shape has changed + // Rebuild everything + this->active_shape = item; + // Remove existing active shape listeners + if (this->active_shape_repr) { + sp_repr_remove_listener_by_data(this->active_shape_repr, this); + Inkscape::GC::release(this->active_shape_repr); + + sp_repr_remove_listener_by_data(this->active_shape_layer_repr, this); + Inkscape::GC::release(this->active_shape_layer_repr); + } + + // Listen in case the active shape changes + this->active_shape_repr = item->getRepr(); + if (this->active_shape_repr) { + Inkscape::GC::anchor(this->active_shape_repr); + sp_repr_add_listener(this->active_shape_repr, &shape_repr_events, this); + + this->active_shape_layer_repr = this->active_shape_repr->parent(); + Inkscape::GC::anchor(this->active_shape_layer_repr); + sp_repr_add_listener(this->active_shape_layer_repr, &layer_repr_events, this); + } + + cc_clear_active_knots(this->knots); + + // The idea here is to try and add a group's children to solidify + // connection handling. We react to path objects with only one node. + for (auto& child: item->children) { + if (SP_IS_PATH(&child) && SP_PATH(&child)->nodesInPath() == 1) { + this->_activeShapeAddKnot((SPItem *) &child); + } + } + this->_activeShapeAddKnot(item); + + } else { + // Ensure the item's connection_points map + // has been updated + item->document->ensureUpToDate(); + } +} + +void ConnectorTool::cc_set_active_conn(SPItem *item) +{ + g_assert( SP_IS_PATH(item) ); + + const SPCurve *curve = SP_PATH(item)->getCurveForEdit(true); + Geom::Affine i2dt = item->i2dt_affine(); + + if (this->active_conn == item) { + if (curve->is_empty()) { + // Connector is invisible because it is clipped to the boundary of + // two overlapping shapes. + this->endpt_handle[0]->hide(); + this->endpt_handle[1]->hide(); + } else { + // Just adjust handle positions. + Geom::Point startpt = *(curve->first_point()) * i2dt; + this->endpt_handle[0]->setPosition(startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + this->endpt_handle[1]->setPosition(endpt, 0); + } + + return; + } + + this->active_conn = item; + + // Remove existing active conn listeners + if (this->active_conn_repr) { + sp_repr_remove_listener_by_data(this->active_conn_repr, this); + Inkscape::GC::release(this->active_conn_repr); + this->active_conn_repr = nullptr; + } + + // Listen in case the active conn changes + this->active_conn_repr = item->getRepr(); + if (this->active_conn_repr) { + Inkscape::GC::anchor(this->active_conn_repr); + sp_repr_add_listener(this->active_conn_repr, &shape_repr_events, this); + } + + for (int i = 0; i < 2; ++i) { + // Create the handle if it doesn't exist + if ( this->endpt_handle[i] == nullptr ) { + SPKnot *knot = new SPKnot(this->desktop, + _("<b>Connector endpoint</b>: drag to reroute or connect to new shapes")); + + knot->setShape(SP_KNOT_SHAPE_SQUARE); + knot->setSize(7); + knot->setAnchor(SP_ANCHOR_CENTER); + knot->setFill(0xffffff00, 0xff0000ff, 0xff0000ff, 0xff0000ff); + knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff); + knot->updateCtrl(); + + // We don't want to use the standard knot handler, + // since we don't want this knot to be draggable. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + + knot->_event_handler_id = 0; + + g_signal_connect(G_OBJECT(knot->item), "event", + G_CALLBACK(cc_generic_knot_handler), knot); + + this->endpt_handle[i] = knot; + } + + // Remove any existing handlers + if (this->endpt_handler_id[i]) { + g_signal_handlers_disconnect_by_func( + G_OBJECT(this->endpt_handle[i]->item), + (void*)G_CALLBACK(endpt_handler), (gpointer) this ); + + this->endpt_handler_id[i] = 0; + } + + // Setup handlers for connector endpoints, this is + // is as 'after' so that cc_generic_knot_handler is + // triggered first for any endpoint. + this->endpt_handler_id[i] = g_signal_connect_after( + G_OBJECT(this->endpt_handle[i]->item), "event", + G_CALLBACK(endpt_handler), this); + } + + if (curve->is_empty()) { + // Connector is invisible because it is clipped to the boundary + // of two overlpapping shapes. So, it doesn't need endpoints. + return; + } + + Geom::Point startpt = *(curve->first_point()) * i2dt; + this->endpt_handle[0]->setPosition(startpt, 0); + + Geom::Point endpt = *(curve->last_point()) * i2dt; + this->endpt_handle[1]->setPosition(endpt, 0); + + this->endpt_handle[0]->show(); + this->endpt_handle[1]->show(); +} + +void cc_create_connection_point(ConnectorTool* cc) +{ + if (cc->active_shape && cc->state == SP_CONNECTOR_CONTEXT_IDLE) { + if (cc->selected_handle) { + cc_deselect_handle( cc->selected_handle ); + } + + SPKnot *knot = new SPKnot(cc->desktop, nullptr); + + // We do not process events on this knot. + g_signal_handler_disconnect(G_OBJECT(knot->item), + knot->_event_handler_id); + + knot->_event_handler_id = 0; + + cc_select_handle( knot ); + cc->selected_handle = knot; + cc->selected_handle->show(); + cc->state = SP_CONNECTOR_CONTEXT_NEWCONNPOINT; + } +} + +static bool cc_item_is_shape(SPItem *item) +{ + if (SP_IS_PATH(item)) { + const SPCurve * curve = (SP_SHAPE(item))->_curve; + if ( curve && !(curve->is_closed()) ) { + // Open paths are connectors. + return false; + } + } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/connector/ignoretext", true)) { + // Don't count text as a shape we can connect connector to. + return false; + } + } + return true; +} + + +bool cc_item_is_connector(SPItem *item) +{ + if (SP_IS_PATH(item)) { + bool closed = SP_PATH(item)->getCurveForEdit(true)->is_closed(); + if (SP_PATH(item)->connEndPair.isAutoRoutingConn() && !closed) { + // To be considered a connector, an object must be a non-closed + // path that is marked with a "inkscape:connector-type" attribute. + return true; + } + } + return false; +} + + +void cc_selection_set_avoid(bool const set_avoid) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop == nullptr) { + return; + } + + SPDocument *document = desktop->getDocument(); + + Inkscape::Selection *selection = desktop->getSelection(); + + + int changes = 0; + + for (SPItem *item: selection->items()) { + char const *value = (set_avoid) ? "true" : nullptr; + + if (cc_item_is_shape(item)) { + item->setAttribute("inkscape:connector-avoid", value); + item->getAvoidRef().handleSettingChange(); + changes++; + } + } + + if (changes == 0) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, + _("Select <b>at least one non-connector object</b>.")); + return; + } + + char *event_desc = (set_avoid) ? + _("Make connectors avoid selected objects") : + _("Make connectors ignore selected objects"); + DocumentUndo::done(document, SP_VERB_CONTEXT_CONNECTOR, event_desc); +} + +void ConnectorTool::_selectionChanged(Inkscape::Selection *selection) +{ + SPItem *item = selection->singleItem(); + + if (this->active_conn == item) { + // Nothing to change. + return; + } + + if (item == nullptr) { + this->cc_clear_active_conn(); + return; + } + + if (cc_item_is_connector(item)) { + this->cc_set_active_conn(item); + } +} + +static void shape_event_attr_deleted(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node *child, + Inkscape::XML::Node */*ref*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + if (child == cc->active_shape_repr) { + // The active shape has been deleted. Clear active shape. + cc->cc_clear_active_shape(); + } +} + + +static void shape_event_attr_changed(Inkscape::XML::Node *repr, gchar const *name, + gchar const */*old_value*/, gchar const */*new_value*/, bool /*is_interactive*/, gpointer data) +{ + g_assert(data); + ConnectorTool *cc = SP_CONNECTOR_CONTEXT(data); + + // Look for changes that result in onscreen movement. + if (!strcmp(name, "d") || !strcmp(name, "x") || !strcmp(name, "y") || + !strcmp(name, "width") || !strcmp(name, "height") || + !strcmp(name, "transform")) { + if (repr == cc->active_shape_repr) { + // Active shape has moved. Clear active shape. + cc->cc_clear_active_shape(); + } else if (repr == cc->active_conn_repr) { + // The active conn has been moved. + // Set it again, which just sets new handle positions. + cc->cc_set_active_conn(cc->active_conn); + } + } +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/connector-tool.h b/src/ui/tools/connector-tool.h new file mode 100644 index 0000000..c71e154 --- /dev/null +++ b/src/ui/tools/connector-tool.h @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CONNECTOR_CONTEXT_H +#define SEEN_CONNECTOR_CONTEXT_H + +/* + * Connector creation tool + * + * Authors: + * Michael Wybrow <mjwybrow@users.sourceforge.net> + * + * Copyright (C) 2005 Michael Wybrow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <map> +#include <string> + +#include <2geom/point.h> +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +class SPItem; +class SPCurve; +class SPKnot; +struct SPCanvasItem; + +namespace Avoid { + class ConnRef; +} + +namespace Inkscape { + class Selection; + + namespace XML { + class Node; + } +} + +#define SP_CONNECTOR_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ConnectorTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +//#define SP_IS_CONNECTOR_CONTEXT(obj) (dynamic_cast<const ConnectorTool*>((const ToolBase*)obj) != NULL) + +enum { + SP_CONNECTOR_CONTEXT_IDLE, + SP_CONNECTOR_CONTEXT_DRAGGING, + SP_CONNECTOR_CONTEXT_CLOSE, + SP_CONNECTOR_CONTEXT_STOP, + SP_CONNECTOR_CONTEXT_REROUTING, + SP_CONNECTOR_CONTEXT_NEWCONNPOINT +}; + +typedef std::map<SPKnot *, int> SPKnotList; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ConnectorTool : public ToolBase { +public: + ConnectorTool(); + ~ConnectorTool() override; + + Inkscape::Selection *selection; + Geom::Point p[5]; + + /** \invar npoints in {0, 2}. */ + gint npoints; + unsigned int state : 4; + + // Red curve + SPCanvasItem *red_bpath; + SPCurve *red_curve; + guint32 red_color; + + // Green curve + SPCurve *green_curve; + + // The new connector + SPItem *newconn; + Avoid::ConnRef *newConnRef; + gdouble curvature; + bool isOrthogonal; + + // The active shape + SPItem *active_shape; + Inkscape::XML::Node *active_shape_repr; + Inkscape::XML::Node *active_shape_layer_repr; + + // Same as above, but for the active connector + SPItem *active_conn; + Inkscape::XML::Node *active_conn_repr; + sigc::connection sel_changed_connection; + + // The activehandle + SPKnot *active_handle; + + // The selected handle, used in editing mode + SPKnot *selected_handle; + + SPItem *clickeditem; + SPKnot *clickedhandle; + + SPKnotList knots; + SPKnot *endpt_handle[2]; + guint endpt_handler_id[2]; + gchar *shref; + gchar *ehref; + SPCanvasItem *c0, *c1, *cl0, *cl1; + + static std::string const prefsPath; + + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + std::string const& getPrefsPath() override; + + void cc_clear_active_shape(); + void cc_set_active_conn(SPItem *item); + void cc_clear_active_conn(); + +private: + void _selectionChanged(Inkscape::Selection *selection); + + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handleKeyPress(guint const keyval); + + void _setInitialPoint(Geom::Point const p); + void _setSubsequentPoint(Geom::Point const p); + void _finishSegment(Geom::Point p); + void _resetColors(); + void _finish(); + void _concatColorsAndFlush(); + void _flushWhite(SPCurve *gc); + + void _activeShapeAddKnot(SPItem* item); + void _setActiveShape(SPItem *item); + bool _ptHandleTest(Geom::Point& p, gchar **href); + + void _reroutingFinish(Geom::Point *const p); +}; + +void cc_selection_set_avoid(bool const set_ignore); +void cc_create_connection_point(ConnectorTool* cc); +void cc_remove_connection_point(ConnectorTool* cc); +bool cc_item_is_connector(SPItem *item); + +} +} +} + +#endif /* !SEEN_CONNECTOR_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp new file mode 100644 index 0000000..2552944 --- /dev/null +++ b/src/ui/tools/dropper-tool.cpp @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Tool for picking colors from drawing + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> + +#include <2geom/transforms.h> +#include <2geom/circle.h> + +#include "color-rgba.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "message-context.h" +#include "preferences.h" +#include "selection.h" +#include "sp-cursor.h" +#include "verbs.h" + +#include "display/canvas-bpath.h" +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "display/cairo-utils.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" + +#include "ui/pixmaps/cursor-dropper-f.xpm" +#include "ui/pixmaps/cursor-dropper-s.xpm" +#include "ui/pixmaps/cursor-dropping-f.xpm" +#include "ui/pixmaps/cursor-dropping-s.xpm" + +#include "svg/svg-color.h" + +#include "ui/tools/dropper-tool.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& DropperTool::getPrefsPath() { + return DropperTool::prefsPath; +} + +const std::string DropperTool::prefsPath = "/tools/dropper"; + +DropperTool::DropperTool() + : ToolBase(cursor_dropper_f_xpm) + , R(0) + , G(0) + , B(0) + , alpha(0) + , radius(0) + , invert(false) + , stroke(false) + , dropping(false) + , dragging(false) + , grabbed(nullptr) + , area(nullptr) + , centre(0, 0) +{ +} + +DropperTool::~DropperTool() = default; + +void DropperTool::setup() { + ToolBase::setup(); + + /* TODO: have a look at CalligraphicTool::setup where the same is done.. generalize? */ + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); + + SPCurve *c = new SPCurve(path); + + this->area = sp_canvas_bpath_new(this->desktop->getControls(), c); + + c->unref(); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->area), 0x0000007f, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->area); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/dropper/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/dropper/gradientdrag")) { + this->enableGrDrag(); + } +} + +void DropperTool::finish() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + if (this->area) { + sp_canvas_item_destroy(this->area); + this->area = nullptr; + } + + ToolBase::finish(); +} + +/** + * Returns the current dropper context color. + */ +guint32 DropperTool::get_color(bool invert) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE); + bool setalpha = prefs->getBool("/tools/dropper/setalpha", true); + + return SP_RGBA32_F_COMPOSE( + fabs(invert - this->R), + fabs(invert - this->G), + fabs(invert - this->B), + (pick == SP_DROPPER_PICK_ACTUAL && setalpha) ? this->alpha : 1.0); +} + +bool DropperTool::root_handler(GdkEvent* event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + int ret = FALSE; + int pick = prefs->getInt("/tools/dropper/pick", SP_DROPPER_PICK_VISIBLE); + + // Decide first what kind of 'mode' we're in. + if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) { + switch (event->key.keyval) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->stroke = event->type == GDK_KEY_PRESS; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->dropping = event->type == GDK_KEY_PRESS; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + this->invert = event->type == GDK_KEY_PRESS; + break; + } + } + + // Get color from selected object instead. + if(this->dropping) { + Inkscape::Selection *selection = desktop->getSelection(); + g_assert(selection); + guint32 apply_color; + bool apply_set = false; + for (auto& obj: selection->objects()) { + if(obj->style) { + double opacity = 1.0; + if(!this->stroke && obj->style->fill.set) { + if(obj->style->fill_opacity.set) { + opacity = obj->style->fill_opacity.value; + } + apply_color = obj->style->fill.value.color.toRGBA32(opacity); + apply_set = true; + } else if(this->stroke && obj->style->stroke.set) { + if(obj->style->stroke_opacity.set) { + opacity = obj->style->stroke_opacity.value; + } + apply_color = obj->style->stroke.value.color.toRGBA32(opacity); + apply_set = true; + } + } + } + if(apply_set) { + this->R = SP_RGBA32_R_F(apply_color); + this->G = SP_RGBA32_G_F(apply_color); + this->B = SP_RGBA32_B_F(apply_color); + this->alpha = SP_RGBA32_A_F(apply_color); + } else { + // This means that having no selection or some other error + // we will default back to normal dropper mode. + this->dropping = false; + } + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + this->centre = Geom::Point(event->button.x, event->button.y); + this->dragging = true; + ret = TRUE; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + nullptr, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + + break; + case GDK_MOTION_NOTIFY: + if (event->motion.state & GDK_BUTTON2_MASK || event->motion.state & GDK_BUTTON3_MASK) { + // pass on middle and right drag + ret = FALSE; + break; + } else if (!this->space_panning) { + // otherwise, constantly calculate color no matter is any button pressed or not + double rw = 0.0; + double R(0), G(0), B(0), A(0); + + if (this->dragging) { + // calculate average + + // radius + rw = std::min(Geom::L2(Geom::Point(event->button.x, event->button.y) - this->centre), 400.0); + if (rw == 0) { // happens sometimes, little idea why... + break; + } + this->radius = rw; + + Geom::Point const cd = desktop->w2d(this->centre); + Geom::Affine const w2dt = desktop->w2d(); + const double scale = rw * w2dt.descrim(); + Geom::Affine const sm( Geom::Scale(scale, scale) * Geom::Translate(cd) ); + sp_canvas_item_affine_absolute(this->area, sm); + sp_canvas_item_show(this->area); + + /* Get buffer */ + Geom::Rect r(this->centre, this->centre); + r.expandBy(rw); + if (!r.hasZeroArea()) { + Geom::IntRect area = r.roundOutwards(); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area.width(), area.height()); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + } + } else { + // pick single pixel + Geom::IntRect area = Geom::IntRect::from_xywh(floor(event->button.x), floor(event->button.y), 1, 1); + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area); + ink_cairo_surface_average_color_premul(s, R, G, B, A); + cairo_surface_destroy(s); + } + + if (pick == SP_DROPPER_PICK_VISIBLE) { + // compose with page color + guint32 bg = desktop->getNamedView()->pagecolor; + R = R + (SP_RGBA32_R_F(bg)) * (1 - A); + G = G + (SP_RGBA32_G_F(bg)) * (1 - A); + B = B + (SP_RGBA32_B_F(bg)) * (1 - A); + A = 1.0; + } else { + // un-premultiply color channels + if (A > 0) { + R /= A; + G /= A; + B /= A; + } + } + + if (fabs(A) < 1e-4) { + A = 0; // suppress exponentials, CSS does not allow that + } + + // remember color + if(!this->dropping && (R != this->R || G != this->G || B != this->B || A != this->alpha)) { + this->R = R; + this->G = G; + this->B = B; + this->alpha = A; + } + ret = TRUE; + } + break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + sp_canvas_item_hide(this->area); + this->dragging = false; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + Inkscape::Selection *selection = desktop->getSelection(); + g_assert(selection); + std::vector<SPItem *> old_selection(selection->items().begin(), selection->items().end()); + if(this->dropping) { + Geom::Point const button_w(event->button.x, event->button.y); + // remember clicked item, disregarding groups, honoring Alt + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + + // Change selected object to object under cursor + if (this->item_to_select) { + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + selection->set(this->item_to_select); + } + } else { + if (prefs->getBool("/tools/dropper/onetimepick", false)) { + // "One time" pick from Fill/Stroke dialog stroke page, always apply fill or stroke (ignore <Shift> key) + stroke = (prefs->getInt("/dialogs/fillstroke/page", 0) == 0) ? false : true; + } + } + + // do the actual color setting + sp_desktop_set_color(desktop, ColorRGBA(this->get_color(this->invert)), false, !this->stroke); + + // REJON: set aux. toolbar input to hex color! + if (!(desktop->getSelection()->isEmpty())) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_DROPPER, + _("Set picked color")); + } + if(this->dropping) { + selection->setList(old_selection); + } + + if (prefs->getBool("/tools/dropper/onetimepick", false)) { + prefs->setBool("/tools/dropper/onetimepick", false); + sp_toggle_dropper(desktop); + + // sp_toggle_dropper will delete ourselves. + // Thus, make sure we return immediately. + return true; + } + + ret = TRUE; + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) { + ret = TRUE; + } + break; + case GDK_KEY_Escape: + desktop->getSelection()->clear(); + break; + } + break; + } + + // set the status message to the right text. + gchar c[64]; + sp_svg_write_color(c, sizeof(c), this->get_color(this->invert)); + + // alpha of color under cursor, to show in the statusbar + // locale-sensitive printf is OK, since this goes to the UI, not into SVG + gchar *alpha = g_strdup_printf(_(" alpha %.3g"), this->alpha); + // where the color is picked, to show in the statusbar + gchar *where = this->dragging ? g_strdup_printf(_(", averaged with radius %d"), (int) this->radius) : g_strdup_printf("%s", _(" under cursor")); + // message, to show in the statusbar + const gchar *message = this->dragging ? _("<b>Release mouse</b> to set color.") : _("<b>Click</b> to set fill, <b>Shift+click</b> to set stroke; <b>drag</b> to average color in area; with <b>Alt</b> to pick inverse color; <b>Ctrl+C</b> to copy the color under mouse to clipboard"); + + this->defaultMessageContext()->setF( + Inkscape::NORMAL_MESSAGE, + "<b>%s%s</b>%s. %s", c, + (pick == SP_DROPPER_PICK_VISIBLE) ? "" : alpha, where, message); + + g_free(where); + g_free(alpha); + + // Set the right cursor for the mode and apply the special Fill color + auto xpm = (this->dropping ? (this->stroke ? cursor_dropping_s_xpm : cursor_dropping_f_xpm) : + (this->stroke ? cursor_dropper_s_xpm : cursor_dropper_f_xpm)); + GdkCursor *cursor = sp_cursor_from_xpm(xpm, this->get_color(this->invert)); + GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(desktop->getCanvas())); + gdk_window_set_cursor(window, cursor); + g_object_unref(cursor); + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/dropper-tool.h b/src/ui/tools/dropper-tool.h new file mode 100644 index 0000000..d52049c --- /dev/null +++ b/src/ui/tools/dropper-tool.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_DROPPER_CONTEXT_H__ +#define __SP_DROPPER_CONTEXT_H__ + +/* + * Tool for picking colors from drawing + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "ui/tools/tool-base.h" + +struct SPCanvasItem; + +#define SP_DROPPER_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::DropperTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_DROPPER_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::DropperTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +enum { + SP_DROPPER_PICK_VISIBLE, + SP_DROPPER_PICK_ACTUAL +}; +enum { + DONT_REDRAW_CURSOR, + DRAW_FILL_CURSOR, + DRAW_STROKE_CURSOR +}; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class DropperTool : public ToolBase { +public: + DropperTool(); + ~DropperTool() override; + + static const std::string prefsPath; + + const std::string& getPrefsPath() override; + + guint32 get_color(bool invert=false); + +protected: + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; + +private: + double R; + double G; + double B; + double alpha; + + double radius; + bool invert; + bool stroke; + bool dropping; + bool dragging; + + SPCanvasItem* grabbed; + SPCanvasItem* area; + Geom::Point centre; +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/dynamic-base.cpp b/src/ui/tools/dynamic-base.cpp new file mode 100644 index 0000000..92eba3f --- /dev/null +++ b/src/ui/tools/dynamic-base.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/dynamic-base.h" + +#include "message-context.h" +#include "display/sp-canvas-item.h" +#include "desktop.h" +#include "display/curve.h" + +#define MIN_PRESSURE 0.0 +#define MAX_PRESSURE 1.0 +#define DEFAULT_PRESSURE 1.0 + +#define DRAG_MIN 0.0 +#define DRAG_DEFAULT 1.0 +#define DRAG_MAX 1.0 + +namespace Inkscape { +namespace UI { +namespace Tools { + +DynamicBase::DynamicBase(gchar const *const *cursor_shape) + : ToolBase(cursor_shape) + , accumulated(nullptr) + , currentshape(nullptr) + , currentcurve(nullptr) + , cal1(nullptr) + , cal2(nullptr) + , point1() + , point2() + , npoints(0) + , repr(nullptr) + , cur(0, 0) + , vel(0, 0) + , vel_max(0) + , acc(0, 0) + , ang(0, 0) + , last(0, 0) + , del(0, 0) + , pressure(DEFAULT_PRESSURE) + , xtilt(0) + , ytilt(0) + , dragging(false) + , usepressure(false) + , usetilt(false) + , mass(0.3) + , drag(DRAG_DEFAULT) + , angle(30.0) + , width(0.2) + , vel_thin(0.1) + , flatness(0.9) + , tremor(0) + , cap_rounding(0) + , is_drawing(false) + , abs_width(false) +{ +} + +DynamicBase::~DynamicBase() { + if (this->accumulated) { + this->accumulated = this->accumulated->unref(); + this->accumulated = nullptr; + } + + for (auto i:segments) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(i)); + } + segments.clear(); + + if (this->currentcurve) { + this->currentcurve = this->currentcurve->unref(); + this->currentcurve = nullptr; + } + + if (this->cal1) { + this->cal1 = this->cal1->unref(); + this->cal1 = nullptr; + } + + if (this->cal2) { + this->cal2 = this->cal2->unref(); + this->cal2 = nullptr; + } + + if (this->currentshape) { + sp_canvas_item_destroy(this->currentshape); + this->currentshape = nullptr; + } +} + +void DynamicBase::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring path = value.getEntryName(); + + // ignore preset modifications + static Glib::ustring const presets_path = this->pref_observer->observed_path + "/preset"; + Glib::ustring const &full_path = value.getPath(); + + if (full_path.compare(0, presets_path.size(), presets_path) == 0) { + return; + } + + if (path == "mass") { + this->mass = 0.01 * CLAMP(value.getInt(10), 0, 100); + } else if (path == "wiggle") { + this->drag = CLAMP((1 - 0.01 * value.getInt()), DRAG_MIN, DRAG_MAX); // drag is inverse to wiggle + } else if (path == "angle") { + this->angle = CLAMP(value.getDouble(), -90, 90); + } else if (path == "width") { + this->width = 0.01 * CLAMP(value.getInt(10), 1, 100); + } else if (path == "thinning") { + this->vel_thin = 0.01 * CLAMP(value.getInt(10), -100, 100); + } else if (path == "tremor") { + this->tremor = 0.01 * CLAMP(value.getInt(), 0, 100); + } else if (path == "flatness") { + this->flatness = 0.01 * CLAMP(value.getInt(), 0, 100); + } else if (path == "usepressure") { + this->usepressure = value.getBool(); + } else if (path == "usetilt") { + this->usetilt = value.getBool(); + } else if (path == "abs_width") { + this->abs_width = value.getBool(); + } else if (path == "cap_rounding") { + this->cap_rounding = value.getDouble(); + } +} + +/* Get normalized point */ +Geom::Point DynamicBase::getNormalizedPoint(Geom::Point v) const { + Geom::Rect drect = this->desktop->get_display_area(); + + double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); + + return Geom::Point(( v[Geom::X] - drect.min()[Geom::X] ) / max, ( v[Geom::Y] - drect.min()[Geom::Y] ) / max); +} + +/* Get view point */ +Geom::Point DynamicBase::getViewPoint(Geom::Point n) const { + Geom::Rect drect = this->desktop->get_display_area(); + + double const max = MAX ( drect.dimensions()[Geom::X], drect.dimensions()[Geom::Y] ); + + return Geom::Point(n[Geom::X] * max + drect.min()[Geom::X], n[Geom::Y] * max + drect.min()[Geom::Y]); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/dynamic-base.h b/src/ui/tools/dynamic-base.h new file mode 100644 index 0000000..1a36f30 --- /dev/null +++ b/src/ui/tools/dynamic-base.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef COMMON_CONTEXT_H_SEEN +#define COMMON_CONTEXT_H_SEEN + +/* + * Common drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" +#include "display/sp-canvas-item.h" + +struct SPCanvasItem; +class SPCurve; + +namespace Inkscape { + namespace XML { + class Node; + } +} + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +namespace Inkscape { +namespace UI { +namespace Tools { + +class DynamicBase : public ToolBase { +public: + DynamicBase(gchar const *const *cursor_shape); + ~DynamicBase() override; + + void set(const Inkscape::Preferences::Entry& val) override; + +protected: + /** accumulated shape which ultimately goes in svg:path */ + SPCurve *accumulated; + + /** canvas items for "committed" segments */ + std::vector<SPCanvasItem*> segments; + + /** canvas item for red "leading" segment */ + SPCanvasItem *currentshape; + /** shape of red "leading" segment */ + SPCurve *currentcurve; + + /** left edge of the stroke; combined to get accumulated */ + SPCurve *cal1; + + /** right edge of the stroke; combined to get accumulated */ + SPCurve *cal2; + + /** left edge points for this segment */ + Geom::Point point1[SAMPLING_SIZE]; + + /** right edge points for this segment */ + Geom::Point point2[SAMPLING_SIZE]; + + /** number of edge points for this segment */ + gint npoints; + + /* repr */ + Inkscape::XML::Node *repr; + + /* common */ + Geom::Point cur; + Geom::Point vel; + double vel_max; + Geom::Point acc; + Geom::Point ang; + Geom::Point last; + Geom::Point del; + + /* extended input data */ + gdouble pressure; + gdouble xtilt; + gdouble ytilt; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressure; + bool usetilt; + double mass, drag; + double angle; + double width; + + double vel_thin; + double flatness; + double tremor; + double cap_rounding; + + bool is_drawing; + + /** uses absolute width independent of zoom */ + bool abs_width; + + Geom::Point getViewPoint(Geom::Point n) const; + Geom::Point getNormalizedPoint(Geom::Point v) const; +}; + +} +} +} + +#endif // COMMON_CONTEXT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp new file mode 100644 index 0000000..8a98350 --- /dev/null +++ b/src/ui/tools/eraser-tool.cpp @@ -0,0 +1,1119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Eraser drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2005-2007 bulia byak + * Copyright (C) 2006 MenTaLguY + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noERASER_VERBOSE + +#include <string> +#include <cstring> +#include <numeric> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/bezier-utils.h> +#include <2geom/pathvector.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "layer-manager.h" +#include "layer-model.h" +#include "message-context.h" +#include "path-chemistry.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "splivarot.h" +#include "verbs.h" + +#include "display/sp-canvas.h" +#include "display/canvas-arena.h" +#include "display/canvas-bpath.h" +#include "display/curve.h" + +#include "include/macros.h" + +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-rect.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "style.h" + +#include "ui/pixmaps/cursor-eraser.xpm" + +#include "svg/svg.h" + +#include "ui/tools/eraser-tool.h" + +using Inkscape::DocumentUndo; + +#define ERC_RED_RGBA 0xff0000ff + +#define TOLERANCE_ERASER 0.1 + +#define ERASER_EPSILON 0.5e-6 +#define ERASER_EPSILON_START 0.5e-2 +#define ERASER_VEL_START 1e-5 + +#define DRAG_MIN 0.0 +#define DRAG_DEFAULT 1.0 +#define DRAG_MAX 1.0 + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& EraserTool::getPrefsPath() { + return EraserTool::prefsPath; +} + +const std::string EraserTool::prefsPath = "/tools/eraser"; + +EraserTool::EraserTool() + : DynamicBase(cursor_eraser_xpm) + , nowidth(false) +{ +} + +EraserTool::~EraserTool() = default; + +void EraserTool::setup() { + DynamicBase::setup(); + + this->accumulated = new SPCurve(); + this->currentcurve = new SPCurve(); + + this->cal1 = new SPCurve(); + this->cal2 = new SPCurve(); + + this->currentshape = sp_canvas_item_new(desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr); + + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->currentshape), ERC_RED_RGBA, SP_WIND_RULE_EVENODD); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->currentshape), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(this->currentshape), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + +/* +static ProfileFloatElement f_profile[PROFILE_FLOAT_SIZE] = { + {"mass",0.02, 0.0, 1.0}, + {"wiggle",0.0, 0.0, 1.0}, + {"angle",30.0, -90.0, 90.0}, + {"thinning",0.1, -1.0, 1.0}, + {"tremor",0.0, 0.0, 1.0}, + {"flatness",0.9, 0.0, 1.0}, + {"cap_rounding",0.0, 0.0, 5.0} +}; +*/ + + sp_event_context_read(this, "mass"); + sp_event_context_read(this, "wiggle"); + sp_event_context_read(this, "angle"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "thinning"); + sp_event_context_read(this, "tremor"); + sp_event_context_read(this, "flatness"); + sp_event_context_read(this, "tracebackground"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "usetilt"); + sp_event_context_read(this, "abs_width"); + sp_event_context_read(this, "cap_rounding"); + + this->is_drawing = false; + //TODO not sure why get 0.01 if slider width == 0, maybe a double/int problem + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/eraser/selcue", false) != 0) { + this->enableSelectionCue(); + } + + // TODO temp force: + this->enableSelectionCue(); +} + +static double +flerp(double f0, double f1, double p) +{ + return f0 + ( f1 - f0 ) * p; +} + +void EraserTool::reset(Geom::Point p) { + this->last = this->cur = getNormalizedPoint(p); + this->vel = Geom::Point(0,0); + this->vel_max = 0; + this->acc = Geom::Point(0,0); + this->ang = Geom::Point(0,0); + this->del = Geom::Point(0,0); +} + +void EraserTool::extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) + this->pressure = CLAMP (this->pressure, ERC_MIN_PRESSURE, ERC_MAX_PRESSURE); + else + this->pressure = ERC_DEFAULT_PRESSURE; + + if (gdk_event_get_axis (event, GDK_AXIS_XTILT, &this->xtilt)) + this->xtilt = CLAMP (this->xtilt, ERC_MIN_TILT, ERC_MAX_TILT); + else + this->xtilt = ERC_DEFAULT_TILT; + + if (gdk_event_get_axis (event, GDK_AXIS_YTILT, &this->ytilt)) + this->ytilt = CLAMP (this->ytilt, ERC_MIN_TILT, ERC_MAX_TILT); + else + this->ytilt = ERC_DEFAULT_TILT; +} + + +bool EraserTool::apply(Geom::Point p) { + Geom::Point n = getNormalizedPoint(p); + + /* Calculate mass and drag */ + double const mass = flerp(1.0, 160.0, this->mass); + double const drag = flerp(0.0, 0.5, this->drag * this->drag); + + /* Calculate force and acceleration */ + Geom::Point force = n - this->cur; + + // If force is below the absolute threshold ERASER_EPSILON, + // or we haven't yet reached ERASER_VEL_START (i.e. at the beginning of stroke) + // _and_ the force is below the (higher) ERASER_EPSILON_START threshold, + // discard this move. + // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen, + // especially bothersome at the start of the stroke where we don't yet have the inertia to + // smooth them out. + if ( Geom::L2(force) < ERASER_EPSILON || (this->vel_max < ERASER_VEL_START && Geom::L2(force) < ERASER_EPSILON_START)) { + return FALSE; + } + + this->acc = force / mass; + + /* Calculate new velocity */ + this->vel += this->acc; + + if (Geom::L2(this->vel) > this->vel_max) + this->vel_max = Geom::L2(this->vel); + + /* Calculate angle of drawing tool */ + + double a1; + if (this->usetilt) { + // 1a. calculate nib angle from input device tilt: + gdouble length = std::sqrt(this->xtilt*this->xtilt + this->ytilt*this->ytilt);; + + if (length > 0) { + Geom::Point ang1 = Geom::Point(this->ytilt/length, this->xtilt/length); + a1 = atan2(ang1); + } + else + a1 = 0.0; + } + else { + // 1b. fixed dc->angle (absolutely flat nib): + double const radians = ( (this->angle - 90) / 180.0 ) * M_PI; + Geom::Point ang1 = Geom::Point(-sin(radians), cos(radians)); + a1 = atan2(ang1); + } + + // 2. perpendicular to dc->vel (absolutely non-flat nib): + gdouble const mag_vel = Geom::L2(this->vel); + if ( mag_vel < ERASER_EPSILON ) { + return FALSE; + } + Geom::Point ang2 = Geom::rot90(this->vel) / mag_vel; + + // 3. Average them using flatness parameter: + // calculate angles + double a2 = atan2(ang2); + // flip a2 to force it to be in the same half-circle as a1 + bool flipped = false; + if (fabs (a2-a1) > 0.5*M_PI) { + a2 += M_PI; + flipped = true; + } + // normalize a2 + if (a2 > M_PI) + a2 -= 2*M_PI; + if (a2 < -M_PI) + a2 += 2*M_PI; + // find the flatness-weighted bisector angle, unflip if a2 was flipped + // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this? + double new_ang = a1 + (1 - this->flatness) * (a2 - a1) - (flipped? M_PI : 0); + + // Try to detect a sudden flip when the new angle differs too much from the previous for the + // current velocity; in that case discard this move + double angle_delta = Geom::L2(Geom::Point (cos (new_ang), sin (new_ang)) - this->ang); + if ( angle_delta / Geom::L2(this->vel) > 4000 ) { + return FALSE; + } + + // convert to point + this->ang = Geom::Point (cos (new_ang), sin (new_ang)); + +// g_print ("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(dc->acc), dc->vel_max, Geom::L2(dc->vel), a1, a2, new_ang); + + /* Apply drag */ + this->vel *= 1.0 - drag; + + /* Update position */ + this->last = this->cur; + this->cur += this->vel; + + return TRUE; +} + +void EraserTool::brush() { + g_assert( this->npoints >= 0 && this->npoints < SAMPLING_SIZE ); + + // How much velocity thins strokestyle + double vel_thin = flerp (0, 160, this->vel_thin); + + // Influence of pressure on thickness + double pressure_thick = (this->usepressure ? this->pressure : 1.0); + + // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass + // drag) + Geom::Point brush = getViewPoint(this->cur); + //Geom::Point brush_w = SP_EVENT_CONTEXT(dc)->desktop->d2w(brush); + + double trace_thick = 1; + + double width = (pressure_thick * trace_thick - vel_thin * Geom::L2(this->vel)) * this->width; + + double tremble_left = 0, tremble_right = 0; + if (this->tremor > 0) { + // obtain two normally distributed random variables, using polar Box-Muller transform + double x1, x2, w, y1, y2; + do { + x1 = 2.0 * g_random_double_range(0,1) - 1.0; + x2 = 2.0 * g_random_double_range(0,1) - 1.0; + w = x1 * x1 + x2 * x2; + } while ( w >= 1.0 ); + w = sqrt( (-2.0 * log( w ) ) / w ); + y1 = x1 * w; + y2 = x2 * w; + + // deflect both left and right edges randomly and independently, so that: + // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve; + // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths; + // (3) deflection somewhat depends on speed, to prevent fast strokes looking + // comparatively smooth and slow ones excessively jittery + tremble_left = (y1)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + tremble_right = (y2)*this->tremor * (0.15 + 0.8*width) * (0.35 + 14*Geom::L2(this->vel)); + } + + if ( width < 0.02 * this->width ) { + width = 0.02 * this->width; + } + + double dezoomify_factor = 0.05 * 1000; + if (!this->abs_width) { + dezoomify_factor /= SP_EVENT_CONTEXT(this)->desktop->current_zoom(); + } + + Geom::Point del_left = dezoomify_factor * (width + tremble_left) * this->ang; + Geom::Point del_right = dezoomify_factor * (width + tremble_right) * this->ang; + + this->point1[this->npoints] = brush + del_left; + this->point2[this->npoints] = brush - del_right; + + if (this->nowidth) { + this->point1[this->npoints] = Geom::middle_point(this->point1[this->npoints],this->point2[this->npoints]); + } + this->del = 0.5*(del_left + del_right); + + this->npoints++; +} + +static void +sp_erc_update_toolbox (SPDesktop *desktop, const gchar *id, double value) +{ + desktop->setToolboxAdjustmentValue (id, value); +} + +void EraserTool::cancel() { + SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop; + this->dragging = FALSE; + this->is_drawing = false; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + /* Remove all temporary line segments */ + for (auto i : this->segments) + sp_canvas_item_destroy(SP_CANVAS_ITEM(i)); + this->segments.clear(); + /* reset accumulated curve */ + this->accumulated->reset(); + this->clear_current(); + if (this->repr) { + this->repr = nullptr; + } +} + +bool EraserTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint eraser_mode = prefs->getInt("/tools/eraser/mode", 2); + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return TRUE; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + + this->reset(button_dt); + this->extinput(event); + this->apply(button_dt); + + this->accumulated->reset(); + + if (this->repr) { + this->repr = nullptr; + } + if ( eraser_mode == ERASER_MODE_DELETE ) { + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + /* initialize first point */ + this->npoints = 0; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK ), + nullptr, + event->button.time); + + ret = TRUE; + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_drawing = true; + } + break; + + case GDK_MOTION_NOTIFY: { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w) + ); + this->extinput(event); + + this->message_context->clear(); + + if ( this->is_drawing && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + this->dragging = TRUE; + + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> an eraser stroke")); + + if (!this->apply(motion_dt)) { + ret = TRUE; + break; + } + + if ( this->cur != this->last ) { + this->brush(); + g_assert( this->npoints > 0 ); + this->fit_and_split(false); + } + + ret = TRUE; + } + if ( eraser_mode == ERASER_MODE_DELETE ) { + this->accumulated->reset(); + Inkscape::Rubberband::get(desktop)->move(motion_dt); + } + } + break; + + case GDK_BUTTON_RELEASE: { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->dragging && event->button.button == 1 && !this->space_panning) { + this->dragging = FALSE; + + this->apply(motion_dt); + + /* Remove all temporary line segments */ + for (auto i : this->segments) + sp_canvas_item_destroy(SP_CANVAS_ITEM(i)); + this->segments.clear(); + + /* Create object */ + this->fit_and_split(true); + this->accumulate(); + this->set_to_accumulated(); // performs document_done + + /* reset accumulated curve */ + this->accumulated->reset(); + + this->clear_current(); + if (this->repr) { + this->repr = nullptr; + } + + this->message_context->clear(); + ret = TRUE; + } + + if (eraser_mode == ERASER_MODE_DELETE && Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->stop(); + } + + break; + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { +// case GDK_KEY_Up: +// case GDK_KEY_KP_Up: +// if (!MOD__CTRL_ONLY(event)) { +// this->angle += 5.0; + +// if (this->angle > 90.0) { +// this->angle = 90.0; +// } +// sp_erc_update_toolbox (desktop, "eraser-angle", this->angle); +// ret = TRUE; +// } +// break; + +// case GDK_KEY_Down: +// case GDK_KEY_KP_Down: +// if (!MOD__CTRL_ONLY(event)) { +// this->angle -= 5.0; + +// if (this->angle < -90.0) { +// this->angle = -90.0; +// } + +// sp_erc_update_toolbox (desktop, "eraser-angle", this->angle); +// ret = TRUE; +// } +// break; + + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + + if (this->width > 1.0) { + this->width = 1.0; + } + + sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100); // the same spinbutton is for alt+x + ret = TRUE; + } + break; + + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + + if (this->width < 0.01) { + this->width = 0.01; + } + + sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100); + ret = TRUE; + } + break; + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100); + ret = TRUE; + break; + + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + sp_erc_update_toolbox (desktop, "eraser-width", this->width * 100); + ret = TRUE; + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("eraser-width"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if ( eraser_mode == ERASER_MODE_DELETE ) { + Inkscape::Rubberband::get(desktop)->stop(); + } + if (this->is_drawing) { + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__CTRL_ONLY(event) && this->is_drawing) { + // if drawing, cancel, otherwise pass it up for undo + this->cancel(); + ret = TRUE; + } + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + this->message_context->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = DynamicBase::root_handler(event); + } + + return ret; +} + +void EraserTool::clear_current() { + // reset bpath + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), nullptr); + + // reset curve + this->currentcurve->reset(); + this->cal1->reset(); + this->cal2->reset(); + + // reset points + this->npoints = 0; +} + +void EraserTool::set_to_accumulated() { + bool workDone = false; + SPDocument *document = this->desktop->doc(); + if (!this->accumulated->is_empty()) { + if (!this->repr) { + /* Create object */ + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + /* Set style */ + sp_desktop_apply_style_tool (this->desktop, repr, "/tools/eraser", false); + + this->repr = repr; + } + SPObject * top_layer = desktop->currentRoot(); + SPItem *item_repr = SP_ITEM(top_layer->appendChildRepr(this->repr)); + Inkscape::GC::release(this->repr); + item_repr->updateRepr(); + Geom::PathVector pathv = this->accumulated->get_pathvector() * this->desktop->dt2doc(); + pathv *= item_repr->i2doc_affine().inverse(); + gchar *str = sp_svg_write_path(pathv); + g_assert( str != nullptr ); + this->repr->setAttribute("d", str); + g_free(str); + Geom::OptRect eraserBbox; + if ( this->repr ) { + bool wasSelection = false; + Inkscape::Selection *selection = this->desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint eraser_mode = prefs->getInt("/tools/eraser/mode", ERASER_MODE_CLIP); + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + + SPItem* acid = SP_ITEM(this->desktop->doc()->getObjectByRepr(this->repr)); + eraserBbox = acid->documentVisualBounds(); + std::vector<SPItem*> remainingItems; + std::vector<SPItem*> toWorkOn; + if (selection->isEmpty()) { + if (eraser_mode == ERASER_MODE_CUT || eraser_mode == ERASER_MODE_CLIP) { + toWorkOn = document->getItemsPartiallyInBox(this->desktop->dkey, *eraserBbox, false, false, false, true); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(this->desktop); + toWorkOn = document->getItemsAtPoints(this->desktop->dkey, r->getPoints()); + } + toWorkOn.erase(std::remove(toWorkOn.begin(), toWorkOn.end(), acid), toWorkOn.end()); + } else { + if (eraser_mode == ERASER_MODE_DELETE) { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(this->desktop); + std::vector<SPItem*> touched; + touched = document->getItemsAtPoints(this->desktop->dkey, r->getPoints()); + for (auto i : touched) { + if(selection->includes(i)){ + toWorkOn.push_back(i); + } + } + } else { + toWorkOn.insert(toWorkOn.end(), selection->items().begin(), selection->items().end()); + } + wasSelection = true; + } + + if ( !toWorkOn.empty() ) { + if (eraser_mode == ERASER_MODE_CUT) { + for (auto i : toWorkOn){ + SPItem *item = i; + SPUse *use = dynamic_cast<SPUse *>(item); + if (SP_IS_PATH(item) && SP_PATH(item)->nodesInPath () == 2){ + SPItem *item = i; + item->deleteObject(true); + workDone = true; + } else if (SP_IS_GROUP(item) || use ) { + /*Do nothing*/ + } else { + Geom::OptRect bbox = item->documentVisualBounds(); + if (bbox && bbox->intersects(*eraserBbox)) { + Inkscape::XML::Node* dup = this->repr->duplicate(xml_doc); + this->repr->parent()->appendChild(dup); + Inkscape::GC::release(dup); // parent takes over + Inkscape::ObjectSet w_selection(this->desktop); + w_selection.set(dup); + if (!this->nowidth) { + w_selection.pathUnion(true); + } + w_selection.add(item); + if(item->style->fill_rule.value == SP_WIND_RULE_EVENODD){ + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-rule", "evenodd"); + sp_desktop_set_style(this->desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + } + if (this->nowidth) { + w_selection.pathCut(true); + } else { + w_selection.pathDiff(true); + } + workDone = true; // TODO set this only if something was cut. + bool break_apart = prefs->getBool("/tools/eraser/break_apart", false); + if(!break_apart){ + w_selection.combine(true); + } else { + if(!this->nowidth){ + w_selection.breakApart(true); + } + } + if ( !w_selection.isEmpty() ) { + // If the item was not completely erased, track the new remainder. + std::vector<SPItem*> nowSel(w_selection.items().begin(), w_selection.items().end()); + for (auto i2 : nowSel) { + remainingItems.push_back(i2); + } + } + } else { + remainingItems.push_back(item); + } + } + } + } else if (eraser_mode == ERASER_MODE_CLIP) { + if (!this->nowidth) { + remainingItems.clear(); + for (auto item : toWorkOn){ + Inkscape::ObjectSet w_selection(this->desktop); + Geom::OptRect bbox = item->documentVisualBounds(); + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node* dup = this->repr->duplicate(xml_doc); + this->repr->parent()->appendChild(dup); + Inkscape::GC::release(dup); // parent takes over + w_selection.set(dup); + w_selection.pathUnion(true); + if (bbox && bbox->intersects(*eraserBbox)) { + SPClipPath *clip_path = item->getClipObject(); + if (clip_path) { + std::vector<SPItem*> selected; + selected.push_back(SP_ITEM(clip_path->firstChild())); + std::vector<Inkscape::XML::Node*> to_select; + std::vector<SPItem*> items(selected); + sp_item_list_to_curves(items, selected, to_select); + Inkscape::XML::Node * clip_data = SP_ITEM(clip_path->firstChild())->getRepr(); + if (!clip_data && !to_select.empty()) { + clip_data = *(to_select.begin()); + } + if (clip_data) { + Inkscape::XML::Node *dup_clip = clip_data->duplicate(xml_doc); + if (dup_clip) { + SPItem * dup_clip_obj = SP_ITEM(item_repr->parent->appendChildRepr(dup_clip)); + Inkscape::GC::release(dup_clip); + if (dup_clip_obj) { + dup_clip_obj->transform *= + item->getRelativeTransform(SP_ITEM(item_repr->parent)); + dup_clip_obj->updateRepr(); + clip_path->deleteObject(true); + w_selection.raiseToTop(true); + w_selection.add(dup_clip); + w_selection.pathDiff(true); + //SPItem * clip = SP_ITEM(*(w_selection.items().begin())); + } + } + } + } else { + Inkscape::XML::Node *rect_repr = xml_doc->createElement("svg:rect"); + sp_desktop_apply_style_tool (this->desktop, rect_repr, "/tools/eraser", false); + SPRect * rect = SP_RECT(item_repr->parent->appendChildRepr(rect_repr)); + Inkscape::GC::release(rect_repr); + rect->setPosition (bbox->left(), bbox->top(), bbox->width(), bbox->height()); + rect->transform = SP_ITEM(rect->parent)->i2doc_affine().inverse(); + + rect->updateRepr(); + rect->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + w_selection.raiseToTop(true); + w_selection.add(rect); + w_selection.pathDiff(true); + } + w_selection.raiseToTop(true); + w_selection.add(item); + w_selection.setMask(true, false, true); + } else { + SPItem *erase_clip = w_selection.singleItem(); + if (erase_clip) { + erase_clip->deleteObject(true); + } + } + workDone = true; + if (wasSelection) { + remainingItems.push_back(item); + } + } + } + } else { + for (auto item : toWorkOn) { + item->deleteObject(true); + workDone = true; + } + } + + if (eraser_mode == ERASER_MODE_DELETE) { + selection->deleteItems(); + remainingItems.clear(); + } + + selection->clear(); + if ( wasSelection ) { + if ( !remainingItems.empty() ) { + selection->add(remainingItems.begin(), remainingItems.end()); + } + } + } + // Remove the eraser stroke itself: + sp_repr_unparent( this->repr ); + this->repr = nullptr; + } + } else { + if (this->repr) { + sp_repr_unparent(this->repr); + this->repr = nullptr; + } + } + if ( workDone ) { + DocumentUndo::done(document, SP_VERB_CONTEXT_ERASER, _("Draw eraser stroke")); + } else { + DocumentUndo::cancel(document); + } +} + +static void +add_cap(SPCurve *curve, + Geom::Point const &pre, Geom::Point const &from, + Geom::Point const &to, Geom::Point const &post, + double rounding) +{ + Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0); + double mag = Geom::L2(vel); + + Geom::Point v_in = from - pre; + double mag_in = Geom::L2(v_in); + + if ( mag_in > ERASER_EPSILON ) { + v_in = mag * v_in / mag_in; + } else { + v_in = Geom::Point(0, 0); + } + + Geom::Point v_out = to - post; + double mag_out = Geom::L2(v_out); + + if ( mag_out > ERASER_EPSILON ) { + v_out = mag * v_out / mag_out; + } else { + v_out = Geom::Point(0, 0); + } + + if ( Geom::L2(v_in) > ERASER_EPSILON || Geom::L2(v_out) > ERASER_EPSILON ) { + curve->curveto(from + v_in, to + v_out, to); + } +} + +void EraserTool::accumulate() { + // construct a crude outline of the eraser's path. + // this desperately needs to be rewritten to use the path outliner... + if ( !this->cal1->is_empty() && !this->cal2->is_empty() ) { + this->accumulated->reset(); /* Is this required ?? */ + SPCurve *rev_cal2 = this->cal2->create_reverse(); + + g_assert(this->cal1->get_segment_count() > 0); + g_assert(rev_cal2->get_segment_count() > 0); + g_assert( ! this->cal1->first_path()->closed() ); + g_assert( ! rev_cal2->first_path()->closed() ); + + Geom::BezierCurve const * dc_cal1_firstseg = dynamic_cast<Geom::BezierCurve const *>( this->cal1->first_segment() ); + Geom::BezierCurve const * rev_cal2_firstseg = dynamic_cast<Geom::BezierCurve const *>( rev_cal2->first_segment() ); + Geom::BezierCurve const * dc_cal1_lastseg = dynamic_cast<Geom::BezierCurve const *>( this->cal1->last_segment() ); + Geom::BezierCurve const * rev_cal2_lastseg = dynamic_cast<Geom::BezierCurve const *>( rev_cal2->last_segment() ); + + g_assert( dc_cal1_firstseg ); + g_assert( rev_cal2_firstseg ); + g_assert( dc_cal1_lastseg ); + g_assert( rev_cal2_lastseg ); + + this->accumulated->append(this->cal1, FALSE); + if(!this->nowidth) { + add_cap(this->accumulated, + dc_cal1_lastseg->finalPoint() - dc_cal1_lastseg->unitTangentAt(1), + dc_cal1_lastseg->finalPoint(), + rev_cal2_firstseg->initialPoint(), + rev_cal2_firstseg->initialPoint() + rev_cal2_firstseg->unitTangentAt(0), + this->cap_rounding); + + this->accumulated->append(rev_cal2, TRUE); + + add_cap(this->accumulated, + rev_cal2_lastseg->finalPoint() - rev_cal2_lastseg->unitTangentAt(1), + rev_cal2_lastseg->finalPoint(), + dc_cal1_firstseg->initialPoint(), + dc_cal1_firstseg->initialPoint() + dc_cal1_firstseg->unitTangentAt(0), + this->cap_rounding); + + this->accumulated->closepath(); + } + + rev_cal2->unref(); + + this->cal1->reset(); + this->cal2->reset(); + } +} + +static double square(double const x) +{ + return x * x; +} + +void EraserTool::fit_and_split(bool release) { + double const tolerance_sq = square( desktop->w2d().descrim() * TOLERANCE_ERASER ); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->nowidth = prefs->getDouble( "/tools/eraser/width", 1) == 0; + +#ifdef ERASER_VERBOSE + g_print("[F&S:R=%c]", release?'T':'F'); +#endif + + if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) + return; // just clicked + + if ( this->npoints == SAMPLING_SIZE - 1 || release ) { +#define BEZIER_SIZE 4 +#define BEZIER_MAX_BEZIERS 8 +#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS ) + +#ifdef ERASER_VERBOSE + g_print("[F&S:#] this->npoints:%d, release:%s\n", + dc->npoints, release ? "TRUE" : "FALSE"); +#endif + + /* Current eraser */ + if ( this->cal1->is_empty() || this->cal2->is_empty() ) { + /* dc->npoints > 0 */ + /* g_print("erasers(1|2) reset\n"); */ + this->cal1->reset(); + this->cal2->reset(); + + this->cal1->moveto(this->point1[0]); + this->cal2->moveto(this->point2[0]); + } + + Geom::Point b1[BEZIER_MAX_LENGTH]; + gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) ); + + Geom::Point b2[BEZIER_MAX_LENGTH]; + gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints, tolerance_sq, BEZIER_MAX_BEZIERS); + g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) ); + + if ( nb1 != -1 && nb2 != -1 ) { + /* Fit and draw and reset state */ +#ifdef ERASER_VERBOSE + g_print("nb1:%d nb2:%d\n", nb1, nb2); +#endif + + /* CanvasShape */ + if (! release) { + this->currentcurve->reset(); + this->currentcurve->moveto(b1[0]); + + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->currentcurve->curveto(bp1[1], bp1[2], bp1[3]); + } + + this->currentcurve->lineto(b2[BEZIER_SIZE*(nb2-1) + 3]); + + for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) { + this->currentcurve->curveto(bp2[2], bp2[1], bp2[0]); + } + + // FIXME: this->segments is always NULL at this point?? + if (this->segments.empty()) { // first segment + add_cap(this->currentcurve, b2[1], b2[0], b1[0], b1[1], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true); + } + + /* Current eraser */ + for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) { + this->cal1->curveto(bp1[1], bp1[2], bp1[3]); + } + + for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) { + this->cal2->curveto(bp2[1], bp2[2], bp2[3]); + } + } else { + /* fixme: ??? */ +#ifdef ERASER_VERBOSE + g_print("[fit_and_split] failed to fit-cubic.\n"); +#endif + this->draw_temporary_box(); + + for (gint i = 1; i < this->npoints; i++) { + this->cal1->lineto(this->point1[i]); + } + + for (gint i = 1; i < this->npoints; i++) { + this->cal2->lineto(this->point2[i]); + } + } + + /* Fit and draw and copy last point */ +#ifdef ERASER_VERBOSE + g_print("[%d]Yup\n", this->npoints); +#endif + if (!release) { + gint eraser_mode = prefs->getInt("/tools/eraser/mode",2); + g_assert(!this->currentcurve->is_empty()); + + SPCanvasItem *cbp = sp_canvas_item_new(desktop->getSketch(), SP_TYPE_CANVAS_BPATH, nullptr); + SPCurve *curve = this->currentcurve->copy(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH (cbp), curve, true); + curve->unref(); + + guint32 fillColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", true); + //guint32 strokeColor = sp_desktop_get_color_tool (desktop, "/tools/eraser", false); + double opacity = sp_desktop_get_master_opacity_tool (desktop, "/tools/eraser"); + double fillOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", true); + //double strokeOpacity = sp_desktop_get_opacity_tool (desktop, "/tools/eraser", false); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(cbp), ((fillColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*fillOpacity)), SP_WIND_RULE_EVENODD); + //on second thougtht don't do stroke yet because we don't have stoke-width yet and because stoke appears between segments while drawing + //sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), ((strokeColor & 0xffffff00) | SP_COLOR_F_TO_U(opacity*strokeOpacity)), 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cbp), 0x00000000, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + /* fixme: Cannot we cascade it to root more clearly? */ + g_signal_connect(G_OBJECT(cbp), "event", G_CALLBACK(sp_desktop_root_handler), desktop); + + this->segments.push_back(cbp); + + if (eraser_mode == ERASER_MODE_DELETE) { + sp_canvas_item_hide(cbp); + sp_canvas_item_hide(this->currentshape); + } + } + + this->point1[0] = this->point1[this->npoints - 1]; + this->point2[0] = this->point2[this->npoints - 1]; + this->npoints = 1; + } else { + this->draw_temporary_box(); + } +} + +void EraserTool::draw_temporary_box() { + this->currentcurve->reset(); + + this->currentcurve->moveto(this->point1[this->npoints-1]); + + for (gint i = this->npoints-2; i >= 0; i--) { + this->currentcurve->lineto(this->point1[i]); + } + + for (gint i = 0; i < this->npoints; i++) { + this->currentcurve->lineto(this->point2[i]); + } + + if (this->npoints >= 2) { + add_cap(this->currentcurve, this->point2[this->npoints-2], this->point2[this->npoints-1], this->point1[this->npoints-1], this->point1[this->npoints-2], this->cap_rounding); + } + + this->currentcurve->closepath(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->currentshape), this->currentcurve, true); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/eraser-tool.h b/src/ui/tools/eraser-tool.h new file mode 100644 index 0000000..6da1fa3 --- /dev/null +++ b/src/ui/tools/eraser-tool.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_ERASER_CONTEXT_H_SEEN +#define SP_ERASER_CONTEXT_H_SEEN + +/* + * Handwriting-like drawing mode + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * The original dynadraw code: + * Paul Haeberli <paul@sgi.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "ui/tools/dynamic-base.h" + +#define ERC_MIN_PRESSURE 0.0 +#define ERC_MAX_PRESSURE 1.0 +#define ERC_DEFAULT_PRESSURE 1.0 + +#define ERC_MIN_TILT -1.0 +#define ERC_MAX_TILT 1.0 +#define ERC_DEFAULT_TILT 0.0 + +#define ERASER_MODE_DELETE 0 +#define ERASER_MODE_CUT 1 +#define ERASER_MODE_CLIP 2 + +namespace Inkscape { +namespace UI { +namespace Tools { + +class EraserTool : public DynamicBase { +public: + EraserTool(); + ~EraserTool() override; + + static const std::string prefsPath; + + void setup() override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + void reset(Geom::Point p); + void extinput(GdkEvent *event); + bool apply(Geom::Point p); + void brush(); + void cancel(); + void clear_current(); + void set_to_accumulated(); + void accumulate(); + void fit_and_split(bool release); + void draw_temporary_box(); + bool nowidth; +}; + +} +} +} + +#endif // SP_ERASER_CONTEXT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/flood-tool.cpp b/src/ui/tools/flood-tool.cpp new file mode 100644 index 0000000..0e72142 --- /dev/null +++ b/src/ui/tools/flood-tool.cpp @@ -0,0 +1,1254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Bucket fill drawing context, works by bitmap filling an area on a rendered version + * of the current display and then tracing the result using potrace. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * John Bintz <jcoswell@coswellproductions.org> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "flood-tool.h" + +#include <cmath> +#include <queue> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/pathvector.h> + +#include "color.h" +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection.h" +#include "splivarot.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing-image.h" +#include "display/drawing.h" +#include "display/sp-canvas.h" + +#include "include/macros.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-root.h" + +#include "ui/pixmaps/cursor-paintbucket.xpm" + +#include "svg/svg.h" + +#include "trace/imagemap.h" +#include "trace/potrace/inkscape-potrace.h" + +#include "ui/shape-editor.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +using Inkscape::Display::ExtractARGB32; +using Inkscape::Display::ExtractRGB32; +using Inkscape::Display::AssembleARGB32; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& FloodTool::getPrefsPath() { + return FloodTool::prefsPath; +} + +const std::string FloodTool::prefsPath = "/tools/paintbucket"; + +// TODO: Replace by C++11 initialization +// Must match PaintBucketChannels enum +Glib::ustring ch_init[8] = { + _("Visible Colors"), + _("Red"), + _("Green"), + _("Blue"), + _("Hue"), + _("Saturation"), + _("Lightness"), + _("Alpha"), +}; +const std::vector<Glib::ustring> FloodTool::channel_list( ch_init, ch_init+8 ); + +Glib::ustring gap_init[4] = { + NC_("Flood autogap", "None"), + NC_("Flood autogap", "Small"), + NC_("Flood autogap", "Medium"), + NC_("Flood autogap", "Large") +}; +const std::vector<Glib::ustring> FloodTool::gap_list( gap_init, gap_init+4 ); + +FloodTool::FloodTool() + : ToolBase(cursor_paintbucket_xpm) + , item(nullptr) +{ + // TODO: Why does the flood tool use a hardcoded tolerance instead of a pref? + this->tolerance = 4; +} + +FloodTool::~FloodTool() { + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->item) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void FloodTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void FloodTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &FloodTool::selection_changed) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/paintbucket/selcue")) { + this->enableSelectionCue(); + } +} + + +// Changes from 0.48 -> 0.49 (Cairo) +// 0.49: Ignores alpha in background +// 0.48: RGBA, 0.49 ARGB +// 0.49: premultiplied alpha +inline static guint32 compose_onto(guint32 px, guint32 bg) +{ + guint ap = 0, rp = 0, gp = 0, bp = 0; + guint rb = 0, gb = 0, bb = 0; + ExtractARGB32(px, ap, rp, gp, bp); + ExtractRGB32(bg, rb, gb, bb); + + // guint ao = 255*255 - (255-ap)*(255-bp); ao = (ao + 127) / 255; + // guint ao = (255-ap)*ab + 255*ap; ao = (ao + 127) / 255; + guint ao = 255; // Cairo version doesn't allow background to have alpha != 1. + guint ro = (255-ap)*rb + 255*rp; ro = (ro + 127) / 255; + guint go = (255-ap)*gb + 255*gp; go = (go + 127) / 255; + guint bo = (255-ap)*bb + 255*bp; bo = (bo + 127) / 255; + + guint pxout = AssembleARGB32(ao, ro, go, bo); + return pxout; +} + +/** + * Get the pointer to a pixel in a pixel buffer. + * @param px The pixel buffer. + * @param x The X coordinate. + * @param y The Y coordinate. + * @param stride The rowstride of the pixel buffer. + */ +inline guint32 get_pixel(guchar *px, int x, int y, int stride) { + return *reinterpret_cast<guint32*>(px + y * stride + x * 4); +} + +inline unsigned char * get_trace_pixel(guchar *trace_px, int x, int y, int width) { + return trace_px + (x + y * width); +} + +/** + * \brief Check whether two unsigned integers are close to each other + * + * \param[in] a The 1st unsigned int + * \param[in] b The 2nd unsigned int + * \param[in] d The threshold for comparison + * + * \return true if |a-b| <= d; false otherwise + */ +static bool compare_guint32(guint32 const a, guint32 const b, guint32 const d) +{ + const int difference = std::abs(static_cast<int>(a) - static_cast<int>(b)); + return difference <= d; +} + +/** + * Compare a pixel in a pixel buffer with another pixel to determine if a point should be included in the fill operation. + * @param check The pixel in the pixel buffer to check. + * @param orig The original selected pixel to use as the fill target color. + * @param merged_orig_pixel The original pixel merged with the background. + * @param dtc The desktop background color. + * @param threshold The fill threshold. + * @param method The fill method to use as defined in PaintBucketChannels. + */ +static bool compare_pixels(guint32 check, guint32 orig, guint32 merged_orig_pixel, guint32 dtc, int threshold, PaintBucketChannels method) +{ + float hsl_check[3] = {0,0,0}, hsl_orig[3] = {0,0,0}; + + guint32 ac = 0, rc = 0, gc = 0, bc = 0; + ExtractARGB32(check, ac, rc, gc, bc); + + guint32 ao = 0, ro = 0, go = 0, bo = 0; + ExtractARGB32(orig, ao, ro, go, bo); + + guint32 ad = 0, rd = 0, gd = 0, bd = 0; + ExtractARGB32(dtc, ad, rd, gd, bd); + + guint32 amop = 0, rmop = 0, gmop = 0, bmop = 0; + ExtractARGB32(merged_orig_pixel, amop, rmop, gmop, bmop); + + if ((method == FLOOD_CHANNELS_H) || + (method == FLOOD_CHANNELS_S) || + (method == FLOOD_CHANNELS_L)) { + double dac = ac; + double dao = ao; + SPColor::rgb_to_hsl_floatv(hsl_check, rc / dac, gc / dac, bc / dac); + SPColor::rgb_to_hsl_floatv(hsl_orig, ro / dao, go / dao, bo / dao); + } + + switch (method) { + case FLOOD_CHANNELS_ALPHA: + return compare_guint32(ac, ao, threshold); + case FLOOD_CHANNELS_R: + return compare_guint32(ac ? unpremul_alpha(rc, ac) : 0, + ao ? unpremul_alpha(ro, ao) : 0, + threshold); + case FLOOD_CHANNELS_G: + return compare_guint32(ac ? unpremul_alpha(gc, ac) : 0, + ao ? unpremul_alpha(go, ao) : 0, + threshold); + case FLOOD_CHANNELS_B: + return compare_guint32(ac ? unpremul_alpha(bc, ac) : 0, + ao ? unpremul_alpha(bo, ao) : 0, + threshold); + case FLOOD_CHANNELS_RGB: + { + guint32 amc, rmc, bmc, gmc; + //amc = 255*255 - (255-ac)*(255-ad); amc = (amc + 127) / 255; + //amc = (255-ac)*ad + 255*ac; amc = (amc + 127) / 255; + amc = 255; // Why are we looking at desktop? Cairo version ignores destop alpha + rmc = (255-ac)*rd + 255*rc; rmc = (rmc + 127) / 255; + gmc = (255-ac)*gd + 255*gc; gmc = (gmc + 127) / 255; + bmc = (255-ac)*bd + 255*bc; bmc = (bmc + 127) / 255; + + int diff = 0; // The total difference between each of the 3 color components + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(rmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(rmop, amop) : 0)); + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(gmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(gmop, amop) : 0)); + diff += std::abs(static_cast<int>(amc ? unpremul_alpha(bmc, amc) : 0) - static_cast<int>(amop ? unpremul_alpha(bmop, amop) : 0)); + return ((diff / 3) <= ((threshold * 3) / 4)); + } + case FLOOD_CHANNELS_H: + return ((int)(fabs(hsl_check[0] - hsl_orig[0]) * 100.0) <= threshold); + case FLOOD_CHANNELS_S: + return ((int)(fabs(hsl_check[1] - hsl_orig[1]) * 100.0) <= threshold); + case FLOOD_CHANNELS_L: + return ((int)(fabs(hsl_check[2] - hsl_orig[2]) * 100.0) <= threshold); + } + + return false; +} + +enum { + PIXEL_CHECKED = 1, + PIXEL_QUEUED = 2, + PIXEL_PAINTABLE = 4, + PIXEL_NOT_PAINTABLE = 8, + PIXEL_COLORED = 16 +}; + +static inline bool is_pixel_checked(unsigned char *t) { return (*t & PIXEL_CHECKED) == PIXEL_CHECKED; } +static inline bool is_pixel_queued(unsigned char *t) { return (*t & PIXEL_QUEUED) == PIXEL_QUEUED; } +static inline bool is_pixel_paintability_checked(unsigned char *t) { + return !((*t & PIXEL_PAINTABLE) == 0) && ((*t & PIXEL_NOT_PAINTABLE) == 0); +} +static inline bool is_pixel_paintable(unsigned char *t) { return (*t & PIXEL_PAINTABLE) == PIXEL_PAINTABLE; } +static inline bool is_pixel_colored(unsigned char *t) { return (*t & PIXEL_COLORED) == PIXEL_COLORED; } + +static inline void mark_pixel_checked(unsigned char *t) { *t |= PIXEL_CHECKED; } +static inline void mark_pixel_unchecked(unsigned char *t) { *t ^= PIXEL_CHECKED; } +static inline void mark_pixel_queued(unsigned char *t) { *t |= PIXEL_QUEUED; } +static inline void mark_pixel_paintable(unsigned char *t) { *t |= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; } +static inline void mark_pixel_not_paintable(unsigned char *t) { *t |= PIXEL_NOT_PAINTABLE; *t ^= PIXEL_PAINTABLE; } +static inline void mark_pixel_colored(unsigned char *t) { *t |= PIXEL_COLORED; } + +static inline void clear_pixel_paintability(unsigned char *t) { *t ^= PIXEL_PAINTABLE; *t ^= PIXEL_NOT_PAINTABLE; } + +struct bitmap_coords_info { + bool is_left; + unsigned int x; + unsigned int y; + int y_limit; + unsigned int width; + unsigned int height; + unsigned int stride; + unsigned int threshold; + unsigned int radius; + PaintBucketChannels method; + guint32 dtc; + guint32 merged_orig_pixel; + Geom::Rect bbox; + Geom::Rect screen; + unsigned int max_queue_size; + unsigned int current_step; +}; + +/** + * Check if a pixel can be included in the fill. + * @param px The rendered pixel buffer to check. + * @param trace_t The pixel in the trace pixel buffer to check or mark. + * @param x The X coordinate. + * @param y The y coordinate. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + */ +inline static bool check_if_pixel_is_paintable(guchar *px, unsigned char *trace_t, int x, int y, guint32 orig_color, bitmap_coords_info bci) { + if (is_pixel_paintability_checked(trace_t)) { + return is_pixel_paintable(trace_t); + } else { + guint32 pixel = get_pixel(px, x, y, bci.stride); + if (compare_pixels(pixel, orig_color, bci.merged_orig_pixel, bci.dtc, bci.threshold, bci.method)) { + mark_pixel_paintable(trace_t); + return true; + } else { + mark_pixel_not_paintable(trace_t); + return false; + } + } +} + +/** + * Perform the bitmap-to-vector tracing and place the traced path onto the document. + * @param px The trace pixel buffer to trace to SVG. + * @param desktop The desktop on which to place the final SVG path. + * @param transform The transform to apply to the final SVG path. + * @param union_with_selection If true, merge the final SVG path with the current selection. + */ +static void do_trace(bitmap_coords_info bci, guchar *trace_px, SPDesktop *desktop, Geom::Affine transform, unsigned int min_x, unsigned int max_x, unsigned int min_y, unsigned int max_y, bool union_with_selection) { + SPDocument *document = desktop->getDocument(); + + unsigned char *trace_t; + + GrayMap *gray_map = GrayMapCreate((max_x - min_x + 1), (max_y - min_y + 1)); + unsigned int gray_map_y = 0; + for (unsigned int y = min_y; y <= max_y; y++) { + unsigned long *gray_map_t = gray_map->rows[gray_map_y]; + + trace_t = get_trace_pixel(trace_px, min_x, y, bci.width); + for (unsigned int x = min_x; x <= max_x; x++) { + *gray_map_t = is_pixel_colored(trace_t) ? GRAYMAP_BLACK : GRAYMAP_WHITE; + gray_map_t++; + trace_t++; + } + gray_map_y++; + } + + Inkscape::Trace::Potrace::PotraceTracingEngine pte; + pte.keepGoing = 1; + std::vector<Inkscape::Trace::TracingEngineResult> results = pte.traceGrayMap(gray_map); + gray_map->destroy(gray_map); + + //XML Tree being used here directly while it shouldn't be...." + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + + long totalNodeCount = 0L; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double offset = prefs->getDouble("/tools/paintbucket/offset", 0.0); + + for (auto result : results) { + totalNodeCount += result.getNodeCount(); + + Inkscape::XML::Node *pathRepr = xml_doc->createElement("svg:path"); + /* Set style */ + sp_desktop_apply_style_tool (desktop, pathRepr, "/tools/paintbucket", false); + + Geom::PathVector pathv = sp_svg_read_pathv(result.getPathData().c_str()); + Path *path = new Path; + path->LoadPathVector(pathv); + + if (offset != 0) { + + Shape *path_shape = new Shape(); + + path->ConvertWithBackData(0.03); + path->Fill(path_shape, 0); + delete path; + + Shape *expanded_path_shape = new Shape(); + + expanded_path_shape->ConvertToShape(path_shape, fill_nonZero); + path_shape->MakeOffset(expanded_path_shape, offset * desktop->current_zoom(), join_round, 4); + expanded_path_shape->ConvertToShape(path_shape, fill_positive); + + Path *expanded_path = new Path(); + + expanded_path->Reset(); + expanded_path_shape->ConvertToForme(expanded_path); + expanded_path->ConvertEvenLines(1.0); + expanded_path->Simplify(1.0); + + delete path_shape; + delete expanded_path_shape; + + gchar *str = expanded_path->svg_dump_path(); + if (str && *str) { + pathRepr->setAttribute("d", str); + g_free(str); + } else { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Too much inset</b>, the result is empty.")); + Inkscape::GC::release(pathRepr); + g_free(str); + return; + } + + delete expanded_path; + + } else { + gchar *str = path->svg_dump_path(); + delete path; + pathRepr->setAttribute("d", str); + g_free(str); + } + + desktop->currentLayer()->addChild(pathRepr,nullptr); + + SPObject *reprobj = document->getObjectByRepr(pathRepr); + if (reprobj) { + SP_ITEM(reprobj)->doWriteTransform(transform); + + // premultiply the item transform by the accumulated parent transform in the paste layer + Geom::Affine local (SP_GROUP(desktop->currentLayer())->i2doc_affine()); + if (!local.isIdentity()) { + gchar const *t_str = pathRepr->attribute("transform"); + Geom::Affine item_t (Geom::identity()); + if (t_str) + sp_svg_transform_read(t_str, &item_t); + item_t *= local.inverse(); + // (we're dealing with unattached repr, so we write to its attr instead of using sp_item_set_transform) + gchar *affinestr=sp_svg_transform_write(item_t); + pathRepr->setAttribute("transform", affinestr); + g_free(affinestr); + } + + Inkscape::Selection *selection = desktop->getSelection(); + + pathRepr->setPosition(-1); + + if (union_with_selection) { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created and unioned with selection.","Area filled, path with <b>%d</b> nodes created and unioned with selection.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->add(reprobj); + selection->pathUnion(true); + } else { + desktop->messageStack()->flashF( Inkscape::WARNING_MESSAGE, + ngettext("Area filled, path with <b>%d</b> node created.","Area filled, path with <b>%d</b> nodes created.", + SP_PATH(reprobj)->nodesInPath()), SP_PATH(reprobj)->nodesInPath() ); + selection->set(reprobj); + } + + } + + Inkscape::GC::release(pathRepr); + + } +} + +/** + * The possible return states of perform_bitmap_scanline_check(). + */ +enum ScanlineCheckResult { + SCANLINE_CHECK_OK, + SCANLINE_CHECK_ABORTED, + SCANLINE_CHECK_BOUNDARY +}; + +/** + * Determine if the provided coordinates are within the pixel buffer limits. + * @param x The X coordinate. + * @param y The Y coordinate. + * @param bci The bitmap_coords_info structure. + */ +inline static bool coords_in_range(unsigned int x, unsigned int y, bitmap_coords_info bci) { + return (x < bci.width) && + (y < bci.height); +} + +#define PAINT_DIRECTION_LEFT 1 +#define PAINT_DIRECTION_RIGHT 2 +#define PAINT_DIRECTION_UP 4 +#define PAINT_DIRECTION_DOWN 8 +#define PAINT_DIRECTION_ALL 15 + +/** + * Paint a pixel or a square (if autogap is enabled) on the trace pixel buffer. + * @param px The rendered pixel buffer to check. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + * @param original_point_trace_t The original pixel in the trace pixel buffer to check. + */ +inline static unsigned int paint_pixel(guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned char *original_point_trace_t) { + if (bci.radius == 0) { + mark_pixel_colored(original_point_trace_t); + return PAINT_DIRECTION_ALL; + } else { + unsigned char *trace_t; + + bool can_paint_up = true; + bool can_paint_down = true; + bool can_paint_left = true; + bool can_paint_right = true; + + for (unsigned int ty = bci.y - bci.radius; ty <= bci.y + bci.radius; ty++) { + for (unsigned int tx = bci.x - bci.radius; tx <= bci.x + bci.radius; tx++) { + if (coords_in_range(tx, ty, bci)) { + trace_t = get_trace_pixel(trace_px, tx, ty, bci.width); + if (!is_pixel_colored(trace_t)) { + if (check_if_pixel_is_paintable(px, trace_t, tx, ty, orig_color, bci)) { + mark_pixel_colored(trace_t); + } else { + if (tx < bci.x) { can_paint_left = false; } + if (tx > bci.x) { can_paint_right = false; } + if (ty < bci.y) { can_paint_up = false; } + if (ty > bci.y) { can_paint_down = false; } + } + } + } + } + } + + unsigned int paint_directions = 0; + if (can_paint_left) { paint_directions += PAINT_DIRECTION_LEFT; } + if (can_paint_right) { paint_directions += PAINT_DIRECTION_RIGHT; } + if (can_paint_up) { paint_directions += PAINT_DIRECTION_UP; } + if (can_paint_down) { paint_directions += PAINT_DIRECTION_DOWN; } + + return paint_directions; + } +} + +/** + * Push a point to be checked onto the bottom of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void push_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_back(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Shift a point to be checked onto the top of the rendered pixel buffer check queue. + * @param fill_queue The fill queue to add the point to. + * @param max_queue_size The maximum size of the fill queue. + * @param trace_t The trace pixel buffer pixel. + * @param x The X coordinate. + * @param y The Y coordinate. + */ +static void shift_point_onto_queue(std::deque<Geom::Point> *fill_queue, unsigned int max_queue_size, unsigned char *trace_t, unsigned int x, unsigned int y) { + if (!is_pixel_queued(trace_t)) { + if ((fill_queue->size() < max_queue_size)) { + fill_queue->push_front(Geom::Point(x, y)); + mark_pixel_queued(trace_t); + } + } +} + +/** + * Scan a row in the rendered pixel buffer and add points to the fill queue as necessary. + * @param fill_queue The fill queue to add the point to. + * @param px The rendered pixel buffer. + * @param trace_px The trace pixel buffer. + * @param orig_color The original selected pixel to use as the fill target color. + * @param bci The bitmap_coords_info structure. + */ +static ScanlineCheckResult perform_bitmap_scanline_check(std::deque<Geom::Point> *fill_queue, guchar *px, guchar *trace_px, guint32 orig_color, bitmap_coords_info bci, unsigned int *min_x, unsigned int *max_x) { + bool aborted = false; + bool reached_screen_boundary = false; + bool ok; + + bool keep_tracing; + bool initial_paint = true; + + unsigned char *current_trace_t = get_trace_pixel(trace_px, bci.x, bci.y, bci.width); + unsigned int paint_directions; + + bool currently_painting_top = false; + bool currently_painting_bottom = false; + + unsigned int top_ty = (bci.y > 0) ? bci.y - 1 : 0; + unsigned int bottom_ty = bci.y + 1; + + bool can_paint_top = (top_ty > 0); + bool can_paint_bottom = (bottom_ty < bci.height); + + Geom::Point front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front(); + + do { + ok = false; + if (bci.is_left) { + keep_tracing = (bci.x != 0); + } else { + keep_tracing = (bci.x < bci.width); + } + + *min_x = MIN(*min_x, bci.x); + *max_x = MAX(*max_x, bci.x); + + if (keep_tracing) { + if (check_if_pixel_is_paintable(px, current_trace_t, bci.x, bci.y, orig_color, bci)) { + paint_directions = paint_pixel(px, trace_px, orig_color, bci, current_trace_t); + if (bci.radius == 0) { + mark_pixel_checked(current_trace_t); + if ((!fill_queue->empty()) && + (front_of_queue[Geom::X] == bci.x) && + (front_of_queue[Geom::Y] == bci.y)) { + fill_queue->pop_front(); + front_of_queue = fill_queue->empty() ? Geom::Point() : fill_queue->front(); + } + } + + if (can_paint_top) { + if (paint_directions & PAINT_DIRECTION_UP) { + unsigned char *trace_t = current_trace_t - bci.width; + if (!is_pixel_queued(trace_t)) { + bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, top_ty, orig_color, bci); + + if (initial_paint) { currently_painting_top = !ok_to_paint; } + + if (ok_to_paint && (!currently_painting_top)) { + currently_painting_top = true; + push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, top_ty); + } + if ((!ok_to_paint) && currently_painting_top) { + currently_painting_top = false; + } + } + } + } + + if (can_paint_bottom) { + if (paint_directions & PAINT_DIRECTION_DOWN) { + unsigned char *trace_t = current_trace_t + bci.width; + if (!is_pixel_queued(trace_t)) { + bool ok_to_paint = check_if_pixel_is_paintable(px, trace_t, bci.x, bottom_ty, orig_color, bci); + + if (initial_paint) { currently_painting_bottom = !ok_to_paint; } + + if (ok_to_paint && (!currently_painting_bottom)) { + currently_painting_bottom = true; + push_point_onto_queue(fill_queue, bci.max_queue_size, trace_t, bci.x, bottom_ty); + } + if ((!ok_to_paint) && currently_painting_bottom) { + currently_painting_bottom = false; + } + } + } + } + + if (bci.is_left) { + if (paint_directions & PAINT_DIRECTION_LEFT) { + bci.x--; current_trace_t--; + ok = true; + } + } else { + if (paint_directions & PAINT_DIRECTION_RIGHT) { + bci.x++; current_trace_t++; + ok = true; + } + } + + initial_paint = false; + } + } else { + if (bci.bbox.min()[Geom::X] > bci.screen.min()[Geom::X]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + } while (ok); + + if (aborted) { return SCANLINE_CHECK_ABORTED; } + if (reached_screen_boundary) { return SCANLINE_CHECK_BOUNDARY; } + return SCANLINE_CHECK_OK; +} + +/** + * Sort the rendered pixel buffer check queue vertically. + */ +static bool sort_fill_queue_vertical(Geom::Point a, Geom::Point b) { + return a[Geom::Y] > b[Geom::Y]; +} + +/** + * Sort the rendered pixel buffer check queue horizontally. + */ +static bool sort_fill_queue_horizontal(Geom::Point a, Geom::Point b) { + return a[Geom::X] > b[Geom::X]; +} + +/** + * Perform a flood fill operation. + * @param event_context The event context for this tool. + * @param event The details of this event. + * @param union_with_selection If true, union the new fill with the current selection. + * @param is_point_fill If false, use the Rubberband "touch selection" to get the initial points for the fill. + * @param is_touch_fill If true, use only the initial contact point in the Rubberband "touch selection" as the fill target color. + */ +static void sp_flood_do_flood_fill(ToolBase *event_context, GdkEvent *event, bool union_with_selection, bool is_point_fill, bool is_touch_fill) { + SPDesktop *desktop = event_context->desktop; + SPDocument *document = desktop->getDocument(); + + document->ensureUpToDate(); + + Geom::OptRect bbox = document->getRoot()->visualBounds(); + + if (!bbox) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill.")); + return; + } + + // Render 160% of the physical display to the render pixel buffer, so that available + // fill areas off the screen can be included in the fill. + double padding = 1.6; + + Geom::Rect screen = desktop->get_display_area(); + + // image space is world space with an offset + Geom::Rect const screen_world = screen * desktop->d2w(); + Geom::IntPoint const img_dims = (screen_world.dimensions() * padding).ceil(); + Geom::Affine const world2img = Geom::Translate((img_dims - screen_world.dimensions()) / 2.0 - screen_world.min()); + Geom::Affine const doc2img = desktop->doc2dt() * desktop->d2w() * world2img; + + auto const width = img_dims.x(); + auto const height = img_dims.y(); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + guchar *px = g_new(guchar, stride * height); + guint32 bgcolor, dtc; + + // Draw image into data block px + { // this block limits the lifetime of Drawing and DrawingContext + /* Create DrawingItems and set transform */ + unsigned dkey = SPItem::display_key_new(1); + Inkscape::Drawing drawing; + Inkscape::DrawingItem *root = document->getRoot()->invoke_show( drawing, dkey, SP_ITEM_SHOW_DISPLAY); + root->setTransform(doc2img); + drawing.setRoot(root); + + Geom::IntRect final_bbox = Geom::IntRect::from_xywh(0, 0, width, height); + drawing.update(final_bbox); + + cairo_surface_t *s = cairo_image_surface_create_for_data( + px, CAIRO_FORMAT_ARGB32, width, height, stride); + Inkscape::DrawingContext dc(s, Geom::Point(0,0)); + // cairo_translate not necessary here - surface origin is at 0,0 + + SPNamedView *nv = desktop->getNamedView(); + bgcolor = nv->pagecolor; + // bgcolor is 0xrrggbbaa, we need 0xaarrggbb + dtc = (bgcolor >> 8) | (bgcolor << 24); + + dc.setSource(bgcolor); + dc.setOperator(CAIRO_OPERATOR_SOURCE); + dc.paint(); + dc.setOperator(CAIRO_OPERATOR_OVER); + + drawing.render(dc, final_bbox); + + //cairo_surface_write_to_png( s, "cairo.png" ); + + cairo_surface_flush(s); + cairo_surface_destroy(s); + + // Hide items + document->getRoot()->invoke_hide(dkey); + } + + // { + // // Dump data to png + // cairo_surface_t *s = cairo_image_surface_create_for_data( + // px, CAIRO_FORMAT_ARGB32, width, height, stride); + // cairo_surface_write_to_png( s, "cairo2.png" ); + // std::cout << " Wrote cairo2.png" << std::endl; + // } + + guchar *trace_px = g_new(guchar, width * height); + memset(trace_px, 0x00, width * height); + + std::deque<Geom::Point> fill_queue; + std::queue<Geom::Point> color_queue; + + std::vector<Geom::Point> fill_points; + + bool aborted = false; + int y_limit = height - 1; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + PaintBucketChannels method = (PaintBucketChannels) prefs->getInt("/tools/paintbucket/channels", 0); + int threshold = prefs->getIntLimited("/tools/paintbucket/threshold", 1, 0, 100); + + switch(method) { + case FLOOD_CHANNELS_ALPHA: + case FLOOD_CHANNELS_RGB: + case FLOOD_CHANNELS_R: + case FLOOD_CHANNELS_G: + case FLOOD_CHANNELS_B: + threshold = (255 * threshold) / 100; + break; + case FLOOD_CHANNELS_H: + case FLOOD_CHANNELS_S: + case FLOOD_CHANNELS_L: + break; + } + + bitmap_coords_info bci; + + bci.y_limit = y_limit; + bci.width = width; + bci.height = height; + bci.stride = stride; + bci.threshold = threshold; + bci.method = method; + bci.bbox = *bbox; + bci.screen = screen; + bci.dtc = dtc; + bci.radius = prefs->getIntLimited("/tools/paintbucket/autogap", 0, 0, 3); + bci.max_queue_size = (width * height) / 4; + bci.current_step = 0; + + if (is_point_fill) { + fill_points.emplace_back(event->button.x, event->button.y); + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + fill_points = r->getPoints(); + } + + auto const img_max_indices = Geom::Rect::from_xywh(0, 0, width - 1, height - 1); + + for (unsigned int i = 0; i < fill_points.size(); i++) { + Geom::Point pw = fill_points[i] * world2img; + + pw = img_max_indices.clamp(pw); + + if (is_touch_fill) { + if (i == 0) { + color_queue.push(pw); + } else { + unsigned char *trace_t = get_trace_pixel(trace_px, (int)pw[Geom::X], (int)pw[Geom::Y], width); + push_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, (int)pw[Geom::X], (int)pw[Geom::Y]); + } + } else { + color_queue.push(pw); + } + } + + bool reached_screen_boundary = false; + + bool first_run = true; + + unsigned long sort_size_threshold = 5; + + unsigned int min_y = height; + unsigned int max_y = 0; + unsigned int min_x = width; + unsigned int max_x = 0; + + while (!color_queue.empty() && !aborted) { + Geom::Point color_point = color_queue.front(); + color_queue.pop(); + + int cx = (int)color_point[Geom::X]; + int cy = (int)color_point[Geom::Y]; + + guint32 orig_color = get_pixel(px, cx, cy, stride); + bci.merged_orig_pixel = compose_onto(orig_color, dtc); + + unsigned char *trace_t = get_trace_pixel(trace_px, cx, cy, width); + if (!is_pixel_checked(trace_t) && !is_pixel_colored(trace_t)) { + if (check_if_pixel_is_paintable(px, trace_px, cx, cy, orig_color, bci)) { + shift_point_onto_queue(&fill_queue, bci.max_queue_size, trace_t, cx, cy); + + if (!first_run) { + for (unsigned int y = 0; y < height; y++) { + trace_t = get_trace_pixel(trace_px, 0, y, width); + for (unsigned int x = 0; x < width; x++) { + clear_pixel_paintability(trace_t); + trace_t++; + } + } + } + first_run = false; + } + } + + unsigned long old_fill_queue_size = fill_queue.size(); + + while (!fill_queue.empty() && !aborted) { + Geom::Point cp = fill_queue.front(); + + if (bci.radius == 0) { + unsigned long new_fill_queue_size = fill_queue.size(); + + /* + * To reduce the number of points in the fill queue, periodically + * resort all of the points in the queue so that scanline checks + * can complete more quickly. A point cannot be checked twice + * in a normal scanline checks, so forcing scanline checks to start + * from one corner of the rendered area as often as possible + * will reduce the number of points that need to be checked and queued. + */ + if (new_fill_queue_size > sort_size_threshold) { + if (new_fill_queue_size > old_fill_queue_size) { + std::sort(fill_queue.begin(), fill_queue.end(), sort_fill_queue_vertical); + + std::deque<Geom::Point>::iterator start_sort = fill_queue.begin(); + std::deque<Geom::Point>::iterator end_sort = fill_queue.begin(); + unsigned int sort_y = (unsigned int)cp[Geom::Y]; + unsigned int current_y; + + for (std::deque<Geom::Point>::iterator i = fill_queue.begin(); i != fill_queue.end(); ++i) { + Geom::Point current = *i; + current_y = (unsigned int)current[Geom::Y]; + if (current_y != sort_y) { + if (start_sort != end_sort) { + std::sort(start_sort, end_sort, sort_fill_queue_horizontal); + } + sort_y = current_y; + start_sort = i; + } + end_sort = i; + } + if (start_sort != end_sort) { + std::sort(start_sort, end_sort, sort_fill_queue_horizontal); + } + + cp = fill_queue.front(); + } + } + + old_fill_queue_size = new_fill_queue_size; + } + + fill_queue.pop_front(); + + int x = (int)cp[Geom::X]; + int y = (int)cp[Geom::Y]; + + min_y = MIN((unsigned int)y, min_y); + max_y = MAX((unsigned int)y, max_y); + + unsigned char *trace_t = get_trace_pixel(trace_px, x, y, width); + if (!is_pixel_checked(trace_t)) { + mark_pixel_checked(trace_t); + + if (y == 0) { + if (bbox->min()[Geom::Y] > screen.min()[Geom::Y]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + + if (y == y_limit) { + if (bbox->max()[Geom::Y] < screen.max()[Geom::Y]) { + aborted = true; break; + } else { + reached_screen_boundary = true; + } + } + + bci.is_left = true; + bci.x = x; + bci.y = y; + + ScanlineCheckResult result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x); + + switch (result) { + case SCANLINE_CHECK_ABORTED: + aborted = true; + break; + case SCANLINE_CHECK_BOUNDARY: + reached_screen_boundary = true; + break; + default: + break; + } + + if (bci.x < width) { + trace_t++; + if (!is_pixel_checked(trace_t) && !is_pixel_queued(trace_t)) { + mark_pixel_checked(trace_t); + bci.is_left = false; + bci.x = x + 1; + + result = perform_bitmap_scanline_check(&fill_queue, px, trace_px, orig_color, bci, &min_x, &max_x); + + switch (result) { + case SCANLINE_CHECK_ABORTED: + aborted = true; + break; + case SCANLINE_CHECK_BOUNDARY: + reached_screen_boundary = true; + break; + default: + break; + } + } + } + } + + bci.current_step++; + + if (bci.current_step > bci.max_queue_size) { + aborted = true; + } + } + } + + g_free(px); + + if (aborted) { + g_free(trace_px); + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Area is not bounded</b>, cannot fill.")); + return; + } + + if (reached_screen_boundary) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("<b>Only the visible part of the bounded area was filled.</b> If you want to fill all of the area, undo, zoom out, and fill again.")); + } + + unsigned int trace_padding = bci.radius + 1; + if (min_y > trace_padding) { min_y -= trace_padding; } + if (max_y < (y_limit - trace_padding)) { max_y += trace_padding; } + if (min_x > trace_padding) { min_x -= trace_padding; } + if (max_x < (width - 1 - trace_padding)) { max_x += trace_padding; } + + Geom::Affine inverted_affine = Geom::Translate(min_x, min_y) * doc2img.inverse(); + + do_trace(bci, trace_px, desktop, inverted_affine, min_x, max_x, min_y, max_y, union_with_selection); + + g_free(trace_px); + + DocumentUndo::done(document, SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area")); +} + +bool FloodTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ((event->button.state & GDK_CONTROL_MASK) && event->button.button == 1 && !this->space_panning) { + Geom::Point const button_w(event->button.x, event->button.y); + + SPItem *item = sp_event_context_find_item (desktop, button_w, TRUE, TRUE); + + // Set style + desktop->applyCurrentOrToolStyle(item, "/tools/paintbucket", false); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_PAINTBUCKET, _("Set style on object")); + // Dead assignment: Value stored to 'ret' is never read + //ret = TRUE; + } + break; + + default: + break; + } + +// if (((ToolBaseClass *) sp_flood_context_parent_class)->item_handler) { +// ret = ((ToolBaseClass *) sp_flood_context_parent_class)->item_handler(event_context, item, event); +// } + // CPPIFY: ret is overwritten... + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool FloodTool::root_handler(GdkEvent* event) { + static bool dragging; + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (!(event->button.state & GDK_CONTROL_MASK)) { + Geom::Point const button_w(event->button.x, event->button.y); + + if (Inkscape::have_viable_layer(desktop, this->defaultMessageContext())) { + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point const p(desktop->w2d(button_w)); + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + Inkscape::Rubberband::get(desktop)->start(desktop, p); + } + } + } + + case GDK_MOTION_NOTIFY: + if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + + this->within_tolerance = false; + + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point const p(desktop->w2d(motion_pt)); + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(p); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> areas to add to fill, hold <b>Alt</b> for touch fill")); + gobble_motion_events(GDK_BUTTON1_MASK); + } + } + break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started()) { + // set "busy" cursor + desktop->setWaitingCursor(); + + if (SP_IS_EVENT_CONTEXT(this)) { + // Since setWaitingCursor runs main loop iterations, we may have already left this tool! + // So check if the tool is valid before doing anything + dragging = false; + + bool is_point_fill = this->within_tolerance; + bool is_touch_fill = event->button.state & GDK_MOD1_MASK; + + sp_flood_do_flood_fill(this, event, event->button.state & GDK_SHIFT_MASK, is_point_fill, is_touch_fill); + + desktop->clearWaitingCursor(); + // restore cursor when done; note that it may already be different if e.g. user + // switched to another tool during interruptible tracing or drawing, in which case do nothing + + ret = TRUE; + } + + r->stop(); + + //if (SP_IS_EVENT_CONTEXT(this)) { + this->defaultMessageContext()->clear(); + //} + } + } + break; + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = TRUE; + break; + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void FloodTool::finishItem() { + this->message_context->clear(); + + if (this->item != nullptr) { + this->item->updateRepr(); + + desktop->canvas->endForcedFullRedraws(); + + desktop->getSelection()->set(this->item); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_PAINTBUCKET, _("Fill bounded area")); + + this->item = nullptr; + } +} + +void FloodTool::set_channels(gint channels) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/tools/paintbucket/channels", channels); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/flood-tool.h b/src/ui/tools/flood-tool.h new file mode 100644 index 0000000..877db98 --- /dev/null +++ b/src/ui/tools/flood-tool.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_FLOOD_CONTEXT_H__ +#define __SP_FLOOD_CONTEXT_H__ + +/* + * Flood fill drawing context + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * John Bintz <jcoswell@coswellproductions.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" + +#define SP_FLOOD_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FloodTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_FLOOD_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FloodTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace Tools { + +class FloodTool : public ToolBase { +public: + FloodTool(); + ~FloodTool() override; + + SPItem *item; + + sigc::connection sel_changed_connection; + + static const std::string prefsPath; + + void setup() override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + + static void set_channels(gint channels); + static const std::vector<Glib::ustring> channel_list; + static const std::vector<Glib::ustring> gap_list; + +private: + void selection_changed(Inkscape::Selection* selection); + void finishItem(); +}; + +enum PaintBucketChannels { + FLOOD_CHANNELS_RGB, + FLOOD_CHANNELS_R, + FLOOD_CHANNELS_G, + FLOOD_CHANNELS_B, + FLOOD_CHANNELS_H, + FLOOD_CHANNELS_S, + FLOOD_CHANNELS_L, + FLOOD_CHANNELS_ALPHA +}; + +} +} +} + +#endif diff --git a/src/ui/tools/freehand-base.cpp b/src/ui/tools/freehand-base.cpp new file mode 100644 index 0000000..34faf81 --- /dev/null +++ b/src/ui/tools/freehand-base.cpp @@ -0,0 +1,1112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2012 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define DRAW_VERBOSE + +#include "desktop-style.h" +#include "message-stack.h" +#include "selection-chemistry.h" + +#include "display/canvas-bpath.h" +#include "display/curve.h" + +#include "include/macros.h" + +#include "live_effects/lpe-bendpath.h" +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-powerstroke.h" + +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "id-clash.h" +#include "object/sp-item-group.h" +#include "object/sp-path.h" +#include "object/sp-rect.h" +#include "object/sp-use.h" +#include "style.h" + +#include "ui/clipboard.h" +#include "ui/control-manager.h" +#include "ui/draw-anchor.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tools/pen-tool.h" +#include "ui/tools/pencil-tool.h" + +#define MIN_PRESSURE 0.0 +#define MAX_PRESSURE 1.0 +#define DEFAULT_PRESSURE 1.0 + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc); +static void spdc_selection_modified(Inkscape::Selection *sel, guint flags, FreehandBase *dc); + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection *sel); + +/** + * Flushes white curve(s) and additional curve into object. + * + * No cleaning of colored curves - this has to be done by caller + * No rereading of white data, so if you cannot rely on ::modified, do it in caller + */ +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc); + +static void spdc_reset_white(FreehandBase *dc); +static void spdc_free_colors(FreehandBase *dc); + +FreehandBase::FreehandBase(gchar const *const *cursor_shape) + : ToolBase(cursor_shape) + , selection(nullptr) + , grab(nullptr) + , attach(false) + , red_color(0xff00007f) + , blue_color(0x0000ff7f) + , green_color(0x00ff007f) + , highlight_color(0x0000007f) + , red_bpath(nullptr) + , red_curve(nullptr) + , blue_bpath(nullptr) + , blue_curve(nullptr) + , green_curve(nullptr) + , green_anchor(nullptr) + , green_closed(false) + , white_item(nullptr) + , sa_overwrited(nullptr) + , sa(nullptr) + , ea(nullptr) + , waiting_LPE_type(Inkscape::LivePathEffect::INVALID_LPE) + , red_curve_is_valid(false) + , anchor_statusbar(false) + , tablet_enabled(false) + , is_tablet(false) + , pressure(DEFAULT_PRESSURE) +{ +} + +FreehandBase::~FreehandBase() { + if (this->grab) { + sp_canvas_item_ungrab(this->grab); + this->grab = nullptr; + } + + if (this->selection) { + this->selection = nullptr; + } + + spdc_free_colors(this); +} + +void FreehandBase::setup() { + ToolBase::setup(); + + this->selection = desktop->getSelection(); + + // Connect signals to track selection changes + this->sel_changed_connection = this->selection->connectChanged( + sigc::bind(sigc::ptr_fun(&spdc_selection_changed), this) + ); + this->sel_modified_connection = this->selection->connectModified( + sigc::bind(sigc::ptr_fun(&spdc_selection_modified), this) + ); + + // Create red bpath + this->red_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + // Create red curve + this->red_curve = new SPCurve(); + + // Create blue bpath + this->blue_bpath = sp_canvas_bpath_new(this->desktop->getSketch(), nullptr); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->blue_bpath), this->blue_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + // Create blue curve + this->blue_curve = new SPCurve(); + + // Create green curve + this->green_curve = new SPCurve(); + + // No green anchor by default + this->green_anchor = nullptr; + this->green_closed = FALSE; + + // Create start anchor alternative curve + this->sa_overwrited = new SPCurve(); + + this->attach = TRUE; + spdc_attach_selection(this, this->selection); +} + +void FreehandBase::finish() { + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + if (this->grab) { + sp_canvas_item_ungrab(this->grab); + } + + if (this->selection) { + this->selection = nullptr; + } + + spdc_free_colors(this); + + ToolBase::finish(); +} + +void FreehandBase::set(const Inkscape::Preferences::Entry& /*value*/) { +} + +bool FreehandBase::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) { + ret = TRUE; + } + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +static Glib::ustring const tool_name(FreehandBase *dc) +{ + return ( SP_IS_PEN_CONTEXT(dc) + ? "/tools/freehand/pen" + : "/tools/freehand/pencil" ); +} + +static void spdc_paste_curve_as_freehand_shape(Geom::PathVector const &newpath, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + + // TODO: Don't paste path if nothing is on the clipboard + SPDocument *document = dc->desktop->doc(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + Effect::createAndApply(PATTERN_ALONG_PATH, dc->desktop->doc(), item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + static_cast<LPEPatternAlongPath*>(lpe)->pattern.set_new_value(newpath,true); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/live_effects/pap/width", 1); + if (!scale) { + scale = 1 / document->getDocumentScale()[0]; + } + Inkscape::SVGOStringStream os; + os << scale; + lpe->getRepr()->setAttribute("prop_scale", os.str()); + DocumentUndo::setUndoSensitive(document, saved); +} + +void spdc_apply_style(SPObject *obj) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + if (obj->style) { + if (obj->style->stroke.isPaintserver()) { + SPPaintServer *server = obj->style->getStrokePaintServer(); + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "fill", str.c_str()); + } + } else if (obj->style->stroke.isColor()) { + gchar c[64]; + sp_svg_write_color( + c, sizeof(c), + obj->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(obj->style->stroke_opacity.value))); + sp_repr_css_set_property(css, "fill", c); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + } else { + sp_repr_css_unset_property(css, "fill"); + } + + sp_repr_css_set_property(css, "fill-rule", "nonzero"); + sp_repr_css_set_property(css, "stroke", "none"); + + sp_desktop_apply_css_recursive(obj, css, true); + sp_repr_css_attr_unref(css); +} +static void spdc_apply_powerstroke_shape(std::vector<Geom::Point> points, FreehandBase *dc, SPItem *item, + gint maxrecursion = 0) +{ + using namespace Inkscape::LivePathEffect; + SPDocument *document = SP_ACTIVE_DOCUMENT; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!document || !desktop) { + return; + } + if (SP_IS_PENCIL_CONTEXT(dc)) { + PencilTool *pt = SP_PENCIL_CONTEXT(dc); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (dc->tablet_enabled) { + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById("power_stroke_preview"))) { + elemref->getRepr()->removeAttribute("style"); + SPItem *successor = dynamic_cast<SPItem *>(elemref); + sp_desktop_apply_style_tool(desktop, successor->getRepr(), + Glib::ustring("/tools/freehand/pencil").data(), false); + spdc_apply_style(successor); + item->deleteObject(true); + item = successor; + dc->selection->set(item); + item->setLocked(false); + dc->white_item = item; + rename_id(SP_OBJECT(item), "path-1"); + } + return; + } + } + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + Effect::createAndApply(POWERSTROKE, dc->desktop->doc(), item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + + static_cast<LPEPowerStroke*>(lpe)->offset_points.param_set_and_write_new_value(points); + + // write powerstroke parameters: + lpe->getRepr()->setAttribute("start_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("end_linecap_type", "zerowidth"); + lpe->getRepr()->setAttribute("sort_points", "true"); + lpe->getRepr()->setAttribute("interpolator_type", "CubicBezierJohan"); + lpe->getRepr()->setAttribute("interpolator_beta", "0.2"); + lpe->getRepr()->setAttribute("miter_limit", "4"); + lpe->getRepr()->setAttribute("scale_width", "1"); + lpe->getRepr()->setAttribute("linejoin_type", "extrp_arc"); + DocumentUndo::setUndoSensitive(document, saved); +} + +static void spdc_apply_bend_shape(gchar const *svgd, FreehandBase *dc, SPItem *item) +{ + using namespace Inkscape::LivePathEffect; + SPUse *use = dynamic_cast<SPUse *>(item); + if ( use ) { + return; + } + SPDocument *document = SP_ACTIVE_DOCUMENT; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!document || !desktop) { + return; + } + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + if(!SP_IS_LPE_ITEM(item) || !SP_LPE_ITEM(item)->hasPathEffectOfType(BEND_PATH)){ + Effect::createAndApply(BEND_PATH, dc->desktop->doc(), item); + } + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + + // write bend parameters: + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/live_effects/bend/width", 1); + if (!scale) { + scale = 1; + } + Inkscape::SVGOStringStream os; + os << scale; + lpe->getRepr()->setAttribute("prop_scale", os.str()); + lpe->getRepr()->setAttribute("scale_y_rel", "false"); + lpe->getRepr()->setAttribute("vertical", "false"); + static_cast<LPEBendPath*>(lpe)->bend_path.paste_param_path(svgd); + DocumentUndo::setUndoSensitive(document, saved); +} + +static void spdc_apply_simplify(std::string threshold, FreehandBase *dc, SPItem *item) +{ + SPDocument *document = SP_ACTIVE_DOCUMENT; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!document || !desktop) { + return; + } + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + using namespace Inkscape::LivePathEffect; + + Effect::createAndApply(SIMPLIFY, document, item); + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + // write simplify parameters: + lpe->getRepr()->setAttribute("steps", "1"); + lpe->getRepr()->setAttributeOrRemoveIfEmpty("threshold", threshold); + lpe->getRepr()->setAttribute("smooth_angles", "360"); + lpe->getRepr()->setAttribute("helper_size", "0"); + lpe->getRepr()->setAttribute("simplify_individual_paths", "false"); + lpe->getRepr()->setAttribute("simplify_just_coalesce", "false"); + DocumentUndo::setUndoSensitive(document, saved); +} + +enum shapeType { NONE, TRIANGLE_IN, TRIANGLE_OUT, ELLIPSE, CLIPBOARD, BEND_CLIPBOARD, LAST_APPLIED }; +static shapeType previous_shape_type = NONE; + +static void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item, SPCurve *curve, bool is_bend) +{ + using namespace Inkscape::LivePathEffect; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (item && SP_IS_LPE_ITEM(item)) { + //Store the clipboard path to apply in the future without the use of clipboard + static Geom::PathVector previous_shape_pathv; + static SPItem *bend_item; + shapeType shape = (shapeType)prefs->getInt(tool_name(dc) + "/shape", 0); + if (previous_shape_type == NONE) { + previous_shape_type = shape; + } + if(shape == LAST_APPLIED){ + shape = previous_shape_type; + if(shape == CLIPBOARD || shape == BEND_CLIPBOARD){ + shape = LAST_APPLIED; + } + } + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if (is_bend && + (shape == BEND_CLIPBOARD || (shape == LAST_APPLIED && previous_shape_type != CLIPBOARD)) && + cm->paste(SP_ACTIVE_DESKTOP,true)) + { + bend_item = dc->selection->singleItem(); + if(!bend_item || (!SP_IS_SHAPE(bend_item) && !SP_IS_GROUP(bend_item))){ + previous_shape_type = NONE; + return; + } + } else if(is_bend) { + return; + } + if (!is_bend && previous_shape_type == BEND_CLIPBOARD && shape == BEND_CLIPBOARD) { + return; + } + bool shape_applied = false; + bool simplify = prefs->getInt(tool_name(dc) + "/simplify", 0); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + if(simplify && mode != 2){ + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0); + tol = tol/(100.0*(102.0-tol)); + std::ostringstream ss; + ss << tol; + spdc_apply_simplify(ss.str(), dc, item); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + } + if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1) { + Effect::createAndApply(SPIRO, dc->desktop->doc(), item); + } + + if (prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2) { + Effect::createAndApply(BSPLINE, dc->desktop->doc(), item); + } + SPShape *sp_shape = dynamic_cast<SPShape *>(item); + if (sp_shape) { + curve = sp_shape->getCurve(); + } + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *cstroke = sp_repr_css_property(css_item, "stroke", "none"); + const char *cfill = sp_repr_css_property(css_item, "fill", "none"); + const char *stroke_width = sp_repr_css_property(css_item, "stroke-width", "0"); + double swidth; + sp_svg_number_read_d(stroke_width, &swidth); + swidth = prefs->getDouble("/live_effects/powerstroke/width", swidth/2); + if (!swidth) { + swidth = swidth/2; + } + swidth = std::abs(swidth); + if (SP_IS_PENCIL_CONTEXT(dc)) { + if (dc->tablet_enabled) { + std::vector<Geom::Point> points; + spdc_apply_powerstroke_shape(points, dc, item); + shape_applied = true; + shape = NONE; + previous_shape_type = NONE; + } + } + +#define SHAPE_LENGTH 10 +#define SHAPE_HEIGHT 10 + + switch (shape) { + case NONE: + // don't apply any shape + break; + case TRIANGLE_IN: + { + // "triangle in" + std::vector<Geom::Point> points(1); + + points[0] = Geom::Point(0., swidth); + //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse(); + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = true; + break; + } + case TRIANGLE_OUT: + { + // "triangle out" + guint curve_length = curve->get_segment_count(); + std::vector<Geom::Point> points(1); + points[0] = Geom::Point(0, swidth); + //points[0] *= i2anc_affine(static_cast<SPItem *>(item->parent), NULL).inverse(); + points[0][Geom::X] = (double)curve_length; + spdc_apply_powerstroke_shape(points, dc, item); + + shape_applied = true; + break; + } + case ELLIPSE: + { + // "ellipse" + SPCurve *c = new SPCurve(); + const double C1 = 0.552; + c->moveto(0, SHAPE_HEIGHT/2); + c->curveto(0, (1 - C1) * SHAPE_HEIGHT/2, (1 - C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH/2, 0); + c->curveto((1 + C1) * SHAPE_LENGTH/2, 0, SHAPE_LENGTH, (1 - C1) * SHAPE_HEIGHT/2, SHAPE_LENGTH, SHAPE_HEIGHT/2); + c->curveto(SHAPE_LENGTH, (1 + C1) * SHAPE_HEIGHT/2, (1 + C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, SHAPE_LENGTH/2, SHAPE_HEIGHT); + c->curveto((1 - C1) * SHAPE_LENGTH/2, SHAPE_HEIGHT, 0, (1 + C1) * SHAPE_HEIGHT/2, 0, SHAPE_HEIGHT/2); + c->closepath(); + spdc_paste_curve_as_freehand_shape(c->get_pathvector(), dc, item); + c->unref(); + + shape_applied = true; + break; + } + case CLIPBOARD: + { + // take shape from clipboard; + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + if(cm->paste(SP_ACTIVE_DESKTOP,true)){ + SPItem * pasted_clipboard = dc->selection->singleItem(); + dc->selection->toCurves(); + pasted_clipboard = dc->selection->singleItem(); + if(pasted_clipboard){ + Inkscape::XML::Node *pasted_clipboard_root = pasted_clipboard->getRepr(); + Inkscape::XML::Node *path = sp_repr_lookup_name(pasted_clipboard_root, "svg:path", -1); // unlimited search depth + if ( path != nullptr ) { + gchar const *svgd = path->attribute("d"); + dc->selection->remove(SP_OBJECT(pasted_clipboard)); + previous_shape_pathv = sp_svg_read_pathv(svgd); + previous_shape_pathv *= pasted_clipboard->transform; + spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item); + + shape = CLIPBOARD; + shape_applied = true; + pasted_clipboard->deleteObject(); + } else { + shape = NONE; + } + } else { + shape = NONE; + } + } else { + shape = NONE; + } + break; + } + case BEND_CLIPBOARD: + { + gchar const *svgd = item->getRepr()->attribute("d"); + if(bend_item && (SP_IS_SHAPE(bend_item) || SP_IS_GROUP(bend_item))){ + // If item is a SPRect, convert it to path first: + if ( dynamic_cast<SPRect *>(bend_item) ) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::Selection *sel = desktop->getSelection(); + if ( sel && !sel->isEmpty() ) { + sel->clear(); + sel->add(bend_item); + sel->toCurves(); + bend_item = sel->singleItem(); + } + } + } + bend_item->moveTo(item,false); + bend_item->transform.setTranslation(Geom::Point()); + spdc_apply_bend_shape(svgd, dc, bend_item); + dc->selection->add(SP_OBJECT(bend_item)); + + shape = BEND_CLIPBOARD; + } else { + bend_item = nullptr; + shape = NONE; + } + break; + } + case LAST_APPLIED: + { + if(previous_shape_type == CLIPBOARD){ + if(previous_shape_pathv.size() != 0){ + spdc_paste_curve_as_freehand_shape(previous_shape_pathv, dc, item); + shape_applied = true; + shape = CLIPBOARD; + } else{ + shape = NONE; + } + } else { + if(bend_item != nullptr && bend_item->getRepr() != nullptr){ + gchar const *svgd = item->getRepr()->attribute("d"); + dc->selection->add(SP_OBJECT(bend_item)); + dc->selection->duplicate(); + dc->selection->remove(SP_OBJECT(bend_item)); + bend_item = dc->selection->singleItem(); + if(bend_item){ + bend_item->moveTo(item,false); + Geom::Coord expansion_X = bend_item->transform.expansionX(); + Geom::Coord expansion_Y = bend_item->transform.expansionY(); + bend_item->transform = Geom::Affine(1,0,0,1,0,0); + bend_item->transform.setExpansionX(expansion_X); + bend_item->transform.setExpansionY(expansion_Y); + spdc_apply_bend_shape(svgd, dc, bend_item); + dc->selection->add(SP_OBJECT(bend_item)); + + shape = BEND_CLIPBOARD; + } else { + shape = NONE; + } + } else { + shape = NONE; + } + } + break; + } + default: + break; + } + previous_shape_type = shape; + + if (shape_applied) { + // apply original stroke color as fill and unset stroke; then return + SPCSSAttr *css = sp_repr_css_attr_new(); + if (!strcmp(cfill, "none")) { + sp_repr_css_set_property (css, "fill", cstroke); + } else { + sp_repr_css_set_property (css, "fill", cfill); + } + sp_repr_css_set_property (css, "stroke", "none"); + sp_desktop_apply_css_recursive(dc->white_item, css, true); + sp_repr_css_attr_unref(css); + return; + } + if (dc->waiting_LPE_type != INVALID_LPE) { + Effect::createAndApply(dc->waiting_LPE_type, dc->desktop->doc(), item); + dc->waiting_LPE_type = INVALID_LPE; + + if (SP_IS_LPETOOL_CONTEXT(dc)) { + // since a geometric LPE was applied, we switch back to "inactive" mode + lpetool_context_switch_mode(SP_LPETOOL_CONTEXT(dc), INVALID_LPE); + } + } + if (SP_IS_PEN_CONTEXT(dc)) { + SP_PEN_CONTEXT(dc)->setPolylineMode(); + } + } +} + +/* + * Selection handlers + */ + +static void spdc_selection_changed(Inkscape::Selection *sel, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +/* fixme: We have to ensure this is not delayed (Lauris) */ + +static void spdc_selection_modified(Inkscape::Selection *sel, guint /*flags*/, FreehandBase *dc) +{ + if (dc->attach) { + spdc_attach_selection(dc, sel); + } +} + +static void spdc_attach_selection(FreehandBase *dc, Inkscape::Selection */*sel*/) +{ + // We reset white and forget white/start/end anchors + spdc_reset_white(dc); + dc->sa = nullptr; + dc->ea = nullptr; + + SPItem *item = dc->selection ? dc->selection->singleItem() : nullptr; + + if ( item && SP_IS_PATH(item) ) { + // Create new white data + // Item + dc->white_item = item; + + // Curve list + // We keep it in desktop coordinates to eliminate calculation errors + SPCurve *norm = SP_PATH(item)->getCurveForEdit(); + norm->transform((dc->white_item)->i2dt_affine()); + g_return_if_fail( norm != nullptr ); + dc->white_curves = norm->split(); + norm->unref(); + + // Anchor list + for (auto c:dc->white_curves) { + g_return_if_fail( c->get_segment_count() > 0 ); + if ( !c->is_closed() ) { + SPDrawAnchor *a; + a = sp_draw_anchor_new(dc, c, TRUE, *(c->first_point())); + if (a) + dc->white_anchors.push_back(a); + a = sp_draw_anchor_new(dc, c, FALSE, *(c->last_point())); + if (a) + dc->white_anchors.push_back(a); + } + } + // fixme: recalculate active anchor? + } +} + + +void spdc_endpoint_snap_rotation(ToolBase const *const ec, Geom::Point &p, Geom::Point const &o, + guint state) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + unsigned const snaps = abs(prefs->getInt("/options/rotationsnapsperpi/value", 12)); + + SnapManager &m = ec->desktop->namedview->snap_manager; + m.setup(ec->desktop); + + bool snap_enabled = m.snapprefs.getSnapEnabledGlobally(); + if (state & GDK_SHIFT_MASK) { + // SHIFT disables all snapping, except the angular snapping. After all, the user explicitly asked for angular + // snapping by pressing CTRL, otherwise we wouldn't have arrived here. But although we temporarily disable + // the snapping here, we must still call for a constrained snap in order to apply the constraints (i.e. round + // to the nearest angle increment) + m.snapprefs.setSnapEnabledGlobally(false); + } + + Inkscape::SnappedPoint dummy = m.constrainedAngularSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE), boost::optional<Geom::Point>(), o, snaps); + p = dummy.getPoint(); + + if (state & GDK_SHIFT_MASK) { + m.snapprefs.setSnapEnabledGlobally(snap_enabled); // restore the original setting + } + + m.unSetup(); +} + + +void spdc_endpoint_snap_free(ToolBase const * const ec, Geom::Point& p, boost::optional<Geom::Point> &start_of_line, guint const /*state*/) +{ + SPDesktop *dt = ec->desktop; + SnapManager &m = dt->namedview->snap_manager; + Inkscape::Selection *selection = dt->getSelection(); + + // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping) + // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment + + m.setup(dt, true, selection->singleItem()); + Inkscape::SnapCandidatePoint scp(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + if (start_of_line) { + scp.addOrigin(*start_of_line); + } + + Inkscape::SnappedPoint sp = m.freeSnap(scp); + p = sp.getPoint(); + + m.unSetup(); +} + +static SPCurve *reverse_then_unref(SPCurve *orig) +{ + SPCurve *ret = orig->create_reverse(); + orig->unref(); + return ret; +} + +void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed) +{ + // Concat RBG + SPCurve *c = dc->green_curve; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Green + dc->green_curve = new SPCurve(); + for (auto i : dc->green_bpaths) + sp_canvas_item_destroy(i); + dc->green_bpaths.clear(); + + // Blue + c->append_continuous(dc->blue_curve, 0.0625); + dc->blue_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->blue_bpath), nullptr); + + // Red + if (dc->red_curve_is_valid) { + c->append_continuous(dc->red_curve, 0.0625); + } + dc->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(dc->red_bpath), nullptr); + + if (c->is_empty()) { + c->unref(); + return; + } + + // Step A - test, whether we ended on green anchor + if ( (forceclosed && + (!dc->sa || (dc->sa && dc->sa->curve->is_empty()))) || + ( dc->green_anchor && dc->green_anchor->active)) + { + // We hit green anchor, closing Green-Blue-Red + dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed.")); + c->closepath_current(); + // Closed path, just flush + spdc_flush_white(dc, c); + c->unref(); + return; + } + + // Step B - both start and end anchored to same curve + if ( dc->sa && dc->ea + && ( dc->sa->curve == dc->ea->curve ) + && ( ( dc->sa != dc->ea ) + || dc->sa->curve->is_closed() ) ) + { + // We hit bot start and end of single curve, closing paths + dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Closing path.")); + dc->sa_overwrited->append_continuous(c, 0.0625); + c->unref(); + dc->sa_overwrited->closepath_current(); + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve)); + } + dc->white_curves.push_back(dc->sa_overwrited); + spdc_flush_white(dc, nullptr); + return; + } + // Step C - test start + if (dc->sa) { + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), dc->sa->curve)); + } + SPCurve *s = dc->sa_overwrited; + s->append_continuous(c, 0.0625); + c->unref(); + c = s->ref(); + } else /* Step D - test end */ if (dc->ea) { + SPCurve *e = dc->ea->curve; + if (!dc->white_curves.empty()) { + dc->white_curves.erase(std::find(dc->white_curves.begin(),dc->white_curves.end(), e)); + } + if (!dc->ea->start) { + e = reverse_then_unref(e); + } + if(prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 1 || + prefs->getInt(tool_name(dc) + "/freehand-mode", 0) == 2){ + e = reverse_then_unref(e); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*e->last_segment()); + SPCurve *lastSeg = new SPCurve(); + if(cubic){ + lastSeg->moveto((*cubic)[0]); + lastSeg->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + if( e->get_segment_count() == 1){ + e = lastSeg; + }else{ + //we eliminate the last segment + e->backspace(); + //and we add it again with the recreation + e->append_continuous(lastSeg, 0.0625); + } + } + e = reverse_then_unref(e); + } + c->append_continuous(e, 0.0625); + e->unref(); + } + if (forceclosed) + { + dc->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Path is closed.")); + c->closepath_current(); + } + spdc_flush_white(dc, c); + + c->unref(); +} + +static void spdc_flush_white(FreehandBase *dc, SPCurve *gc) +{ + SPCurve *c; + if (! dc->white_curves.empty()) { + g_assert(dc->white_item); + c = SPCurve::concat(dc->white_curves); + dc->white_curves.clear(); + if (gc) { + c->append(gc, FALSE); + } + } else if (gc) { + c = gc; + c->ref(); + } else { + return; + } + + // Now we have to go back to item coordinates at last + c->transform( dc->white_item + ? (dc->white_item)->dt2i_affine() + : dc->desktop->dt2doc() ); + + SPDesktop *desktop = dc->desktop; + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + if ( c && !c->is_empty() ) { + // We actually have something to write + + bool has_lpe = false; + Inkscape::XML::Node *repr; + + if (dc->white_item) { + repr = dc->white_item->getRepr(); + has_lpe = SP_LPE_ITEM(dc->white_item)->hasPathEffectRecursive(); + } else { + repr = xml_doc->createElement("svg:path"); + // Set style + sp_desktop_apply_style_tool(desktop, repr, tool_name(dc).data(), false); + } + + gchar *str = sp_svg_write_path( c->get_pathvector() ); + g_assert( str != nullptr ); + if (has_lpe) + repr->setAttribute("inkscape:original-d", str); + else + repr->setAttribute("d", str); + g_free(str); + + if (SP_IS_PENCIL_CONTEXT(dc) && dc->tablet_enabled) { + if (!dc->white_item) { + dc->white_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + } + spdc_check_for_and_apply_waiting_LPE(dc, dc->white_item, c, false); + } + if (!dc->white_item) { + // Attach repr + SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + dc->white_item = item; + //Bend needs the transforms applied after, Other effects best before + spdc_check_for_and_apply_waiting_LPE(dc, item, c, true); + Inkscape::GC::release(repr); + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + item->updateRepr(); + item->doWriteTransform(item->transform, nullptr, true); + spdc_check_for_and_apply_waiting_LPE(dc, item, c, false); + dc->selection->set(repr); + if(previous_shape_type == BEND_CLIPBOARD){ + repr->parent()->removeChild(repr); + } + } + DocumentUndo::done(doc, SP_IS_PEN_CONTEXT(dc)? SP_VERB_CONTEXT_PEN : SP_VERB_CONTEXT_PENCIL, + _("Draw path")); + + // When quickly drawing several subpaths with Shift, the next subpath may be finished and + // flushed before the selection_modified signal is fired by the previous change, which + // results in the tool losing all of the selected path's curve except that last subpath. To + // fix this, we force the selection_modified callback now, to make sure the tool's curve is + // in sync immediately. + spdc_selection_modified(desktop->getSelection(), 0, dc); + } + + c->unref(); + + // Flush pending updates + doc->ensureUpToDate(); +} + +SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p) +{ + SPDrawAnchor *active = nullptr; + + // Test green anchor + if (dc->green_anchor) { + active = sp_draw_anchor_test(dc->green_anchor, p, TRUE); + } + + for (auto i:dc->white_anchors) { + SPDrawAnchor *na = sp_draw_anchor_test(i, p, !active); + if ( !active && na ) { + active = na; + } + } + return active; +} + +static void spdc_reset_white(FreehandBase *dc) +{ + if (dc->white_item) { + // We do not hold refcount + dc->white_item = nullptr; + } + for (auto i: dc->white_curves) + i->unref(); + dc->white_curves.clear(); + for (auto i:dc->white_anchors) + sp_draw_anchor_destroy(i); + dc->white_anchors.clear(); +} + +static void spdc_free_colors(FreehandBase *dc) +{ + // Red + if (dc->red_bpath) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->red_bpath)); + dc->red_bpath = nullptr; + } + if (dc->red_curve) { + dc->red_curve = dc->red_curve->unref(); + } + + // Blue + if (dc->blue_bpath) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(dc->blue_bpath)); + dc->blue_bpath = nullptr; + } + if (dc->blue_curve) { + dc->blue_curve = dc->blue_curve->unref(); + } + + // Overwrite start anchor curve + if (dc->sa_overwrited) { + dc->sa_overwrited = dc->sa_overwrited->unref(); + } + // Green + for (auto i : dc->green_bpaths) + sp_canvas_item_destroy(i); + dc->green_bpaths.clear(); + if (dc->green_curve) { + dc->green_curve = dc->green_curve->unref(); + } + if (dc->green_anchor) { + dc->green_anchor = sp_draw_anchor_destroy(dc->green_anchor); + } + + // White + if (dc->white_item) { + // We do not hold refcount + dc->white_item = nullptr; + } + for (auto i: dc->white_curves) + i->unref(); + dc->white_curves.clear(); + for (auto i:dc->white_anchors) + sp_draw_anchor_destroy(i); + dc->white_anchors.clear(); +} + +void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state) { + g_return_if_fail(!strcmp(tool, "/tools/freehand/pen") || !strcmp(tool, "/tools/freehand/pencil") + || !strcmp(tool, "/tools/calligraphic") ); + Glib::ustring tool_path = tool; + + SPDesktop *desktop = ec->desktop; + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "arc"); + SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + item->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + Inkscape::GC::release(repr); + + // apply the tool's current style + sp_desktop_apply_style_tool(desktop, repr, tool, false); + + // find out stroke width (TODO: is there an easier way??) + double stroke_width = 3.0; + gchar const *style_str = repr->attribute("style"); + if (style_str) { + SPStyle style(SP_ACTIVE_DOCUMENT); + style.mergeString(style_str); + stroke_width = style.stroke_width.computed; + } + + // unset stroke and set fill color to former stroke color + gchar * str; + str = strcmp(tool, "/tools/calligraphic") ? g_strdup_printf("fill:#%06x;stroke:none;", sp_desktop_get_color_tool(desktop, tool, false) >> 8) + : g_strdup_printf("fill:#%06x;stroke:#%06x;", sp_desktop_get_color_tool(desktop, tool, true) >> 8, sp_desktop_get_color_tool(desktop, tool, false) >> 8); + repr->setAttribute("style", str); + g_free(str); + + // put the circle where the mouse click occurred and set the diameter to the + // current stroke width, multiplied by the amount specified in the preferences + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Geom::Affine const i2d (item->i2dt_affine ()); + Geom::Point pp = pt * i2d.inverse(); + + double rad = 0.5 * prefs->getDouble(tool_path + "/dot-size", 3.0); + if (!strcmp(tool, "/tools/calligraphic")) + rad = 0.0333 * prefs->getDouble(tool_path + "/width", 3.0) / desktop->current_zoom() / desktop->getDocument()->getDocumentScale()[Geom::X]; + if (event_state & GDK_MOD1_MASK) { + // TODO: We vary the dot size between 0.5*rad and 1.5*rad, where rad is the dot size + // as specified in prefs. Very simple, but it might be sufficient in practice. If not, + // we need to devise something more sophisticated. + double s = g_random_double_range(-0.5, 0.5); + rad *= (1 + s); + } + if (event_state & GDK_SHIFT_MASK) { + // double the point size + rad *= 2; + } + + sp_repr_set_svg_double (repr, "sodipodi:cx", pp[Geom::X]); + sp_repr_set_svg_double (repr, "sodipodi:cy", pp[Geom::Y]); + sp_repr_set_svg_double (repr, "sodipodi:rx", rad * stroke_width); + sp_repr_set_svg_double (repr, "sodipodi:ry", rad * stroke_width); + item->updateRepr(); + item->doWriteTransform(item->transform, nullptr, true); + + desktop->getSelection()->set(item); + + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating single dot")); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, _("Create single dot")); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/freehand-base.h b/src/ui/tools/freehand-base.h new file mode 100644 index 0000000..7dc960a --- /dev/null +++ b/src/ui/tools/freehand-base.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DRAW_CONTEXT_H +#define SEEN_SP_DRAW_CONTEXT_H + +/* + * Generic drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" +#include "live_effects/effect-enum.h" + +struct SPCanvasItem; +class SPCurve; +struct SPDrawAnchor; + +namespace Inkscape { + class Selection; +} + +namespace boost { + template<class T> + class optional; +} + +/* Freehand context */ + +#define SP_DRAW_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::FreehandBase*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_DRAW_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::FreehandBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + + +namespace Inkscape { +namespace UI { +namespace Tools { + +class FreehandBase : public ToolBase { +public: + FreehandBase(gchar const *const *cursor_shape); + ~FreehandBase() override; + + Inkscape::Selection *selection; + SPCanvasItem *grab; + + bool attach; + + guint32 red_color; + guint32 blue_color; + guint32 green_color; + guint32 highlight_color; + + // Red + SPCanvasItem *red_bpath; + SPCurve *red_curve; + + // Blue + SPCanvasItem *blue_bpath; + SPCurve *blue_curve; + + // Green + std::vector<SPCanvasItem*> green_bpaths; + SPCurve *green_curve; + SPDrawAnchor *green_anchor; + gboolean green_closed; // a flag meaning we hit the green anchor, so close the path on itself + + // White + SPItem *white_item; + std::list<SPCurve *> white_curves; + std::vector<SPDrawAnchor*> white_anchors; + + // Temporary modified curve when start anchor + SPCurve *sa_overwrited; + + // Start anchor + SPDrawAnchor *sa; + + // End anchor + SPDrawAnchor *ea; + + + /* type of the LPE that is to be applied automatically to a finished path (if any) */ + Inkscape::LivePathEffect::EffectType waiting_LPE_type; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + + bool red_curve_is_valid; + + bool anchor_statusbar; + + bool tablet_enabled; + + bool is_tablet; + + gdouble pressure; + void set(const Inkscape::Preferences::Entry& val) override; + +protected: + + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; +}; + +/** + * Returns FIRST active anchor (the activated one). + */ +SPDrawAnchor *spdc_test_inside(FreehandBase *dc, Geom::Point p); + +/** + * Concats red, blue and green. + * If any anchors are defined, process these, optionally removing curves from white list + * Invoke _flush_white to write result back to object. + */ +void spdc_concat_colors_and_flush(FreehandBase *dc, gboolean forceclosed); + +/** + * Snaps node or handle to PI/rotationsnapsperpi degree increments. + * + * @param dc draw context. + * @param p cursor point (to be changed by snapping). + * @param o origin point. + * @param state keyboard state to check if ctrl or shift was pressed. + */ +void spdc_endpoint_snap_rotation(ToolBase const *const ec, Geom::Point &p, Geom::Point const &o, guint state); + +void spdc_endpoint_snap_free(ToolBase const *ec, Geom::Point &p, boost::optional<Geom::Point> &start_of_line, guint state); + +/** + * If we have an item and a waiting LPE, apply the effect to the item + * (spiro spline mode is treated separately). + */ +void spdc_check_for_and_apply_waiting_LPE(FreehandBase *dc, SPItem *item); + +/** + * Create a single dot represented by a circle. + */ +void spdc_create_single_dot(ToolBase *ec, Geom::Point const &pt, char const *tool, guint event_state); + +} +} +} + +#endif // SEEN_SP_DRAW_CONTEXT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/gradient-tool.cpp b/src/ui/tools/gradient-tool.cpp new file mode 100644 index 0000000..6cb00fc --- /dev/null +++ b/src/ui/tools/gradient-tool.cpp @@ -0,0 +1,932 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gdk/gdkkeysyms.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-drag.h" +#include "include/macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" +#include "verbs.h" + +#include "object/sp-namedview.h" +#include "object/sp-stop.h" + +#include "display/sp-ctrlline.h" + +#include "ui/pixmaps/cursor-gradient-add.xpm" +#include "ui/pixmaps/cursor-gradient.xpm" + +#include "svg/css-ostringstream.h" + +#include "ui/tools/gradient-tool.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint state, guint32 etime); + +const std::string& GradientTool::getPrefsPath() { + return GradientTool::prefsPath; +} + +const std::string GradientTool::prefsPath = "/tools/gradient"; + + +GradientTool::GradientTool() + : ToolBase(cursor_gradient_xpm) + , cursor_addnode(false) + , node_added(false) +// TODO: Why are these connections stored as pointers? + , selcon(nullptr) + , subselcon(nullptr) +{ + // TODO: This value is overwritten in the root handler + this->tolerance = 6; +} + +GradientTool::~GradientTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +// This must match GrPointType enum sp-gradient.h +// We should move this to a shared header (can't simply move to gradient.h since that would require +// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!). +const gchar *gr_handle_descr [] = { + N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN + N_("Linear gradient <b>end</b>"), + N_("Linear gradient <b>mid stop</b>"), + N_("Radial gradient <b>center</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS + N_("Radial gradient <b>mid stop</b>"), + N_("Radial gradient <b>mid stop</b>"), + N_("Mesh gradient <b>corner</b>"), + N_("Mesh gradient <b>handle</b>"), + N_("Mesh gradient <b>tensor</b>") +}; + +void GradientTool::selection_changed(Inkscape::Selection*) { + GradientTool *rc = (GradientTool *) this; + + GrDrag *drag = rc->_grdrag; + Inkscape::Selection *selection = this->desktop->getSelection(); + if (selection == nullptr) { + return; + } + guint n_obj = (guint) boost::distance(selection->items()); + + if (!drag->isNonEmpty() || selection->isEmpty()) + return; + guint n_tot = drag->numDraggers(); + guint n_sel = drag->numSelected(); + + //The use of ngettext in the following code is intentional even if the English singular form would never be used + if (n_sel == 1) { + if (drag->singleSelectedDraggerNumDraggables() == 1) { + gchar * message = g_strconcat( + //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message + _("%s selected"), + //TRANSLATORS: Mind the space in front. This is part of a compound message + ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + rc->message_context->setF(Inkscape::NORMAL_MESSAGE, + message,_(gr_handle_descr[drag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj); + } else { + gchar * message = g_strconcat( + //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count) + ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected", + "One handle merging %d stops (drag with <b>Shift</b> to separate) selected",drag->singleSelectedDraggerNumDraggables()), + ngettext(" out of %d gradient handle"," out of %d gradient handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + rc->message_context->setF(Inkscape::NORMAL_MESSAGE,message,drag->singleSelectedDraggerNumDraggables(), n_tot, n_obj); + } + } else if (n_sel > 1) { + //TRANSLATORS: The plural refers to number of selected gradient handles. This is part of a compound message (part two indicates selected object count) + gchar * message = g_strconcat(ngettext("<b>%d</b> gradient handle selected out of %d","<b>%d</b> gradient handles selected out of %d",n_sel), + //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + rc->message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj); + } else if (n_sel == 0) { + rc->message_context->setF(Inkscape::NORMAL_MESSAGE, + //TRANSLATORS: The plural refers to number of selected objects + ngettext("<b>No</b> gradient handles selected out of %d on %d selected object", + "<b>No</b> gradient handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj); + } +} + +void GradientTool::setup() { + ToolBase::setup(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/gradient/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = this->desktop->getSelection(); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &GradientTool::selection_changed) + )); + + this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged( + sigc::hide(sigc::bind( + sigc::mem_fun(this, &GradientTool::selection_changed), + (Inkscape::Selection*)nullptr + )) + )); + + this->selection_changed(selection); +} + +void +sp_gradient_context_select_next (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_next(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +void +sp_gradient_context_select_prev (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_prev(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +static bool +sp_gradient_context_is_over_line (GradientTool *rc, SPItem *item, Geom::Point event_p) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + //Translate mouse point into proper coord system + rc->mousepoint_doc = desktop->w2d(event_p); + + if (SP_IS_CTRLLINE(item)) { + SPCtrlLine* line = SP_CTRLLINE(item); + + Geom::LineSegment ls(line->s, line->e); + Geom::Point nearest = ls.pointAt(ls.nearestTime(rc->mousepoint_doc)); + double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom(); + + double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance; + + bool close = (dist_screen < tolerance); + + return close; + } + return false; +} + +static std::vector<Geom::Point> +sp_gradient_context_get_stop_intervals (GrDrag *drag, std::vector<SPStop *> &these_stops, std::vector<SPStop *> &next_stops) +{ + std::vector<Geom::Point> coords; + + // for all selected draggers + for (std::set<GrDragger *>::const_iterator i = drag->selected.begin(); i != drag->selected.end() ; ++i ) { + GrDragger *dragger = *i; + // remember the coord of the dragger to reselect it later + coords.push_back(dragger->point); + // for all draggables of dragger + for (std::vector<GrDraggable *>::const_iterator j = dragger->draggables.begin(); j != dragger->draggables.end(); ++j) { + GrDraggable *d = *j; + + // find the gradient + SPGradient *gradient = getGradient(d->item, d->fill_or_stroke); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + + // these draggable types cannot have a next draggabe to insert a stop between them + if (d->point_type == POINT_LG_END || + d->point_type == POINT_RG_FOCUS || + d->point_type == POINT_RG_R1 || + d->point_type == POINT_RG_R2) { + continue; + } + + // from draggables to stops + SPStop *this_stop = sp_get_stop_i (vector, d->point_i); + SPStop *next_stop = this_stop->getNextStop(); + SPStop *last_stop = sp_last_stop (vector); + + Inkscape::PaintTarget fs = d->fill_or_stroke; + SPItem *item = d->item; + gint type = d->point_type; + gint p_i = d->point_i; + + // if there's a next stop, + if (next_stop) { + GrDragger *dnext = nullptr; + // find its dragger + // (complex because it may have different types, and because in radial, + // more than one dragger may correspond to a stop, so we must distinguish) + if (type == POINT_LG_BEGIN || type == POINT_LG_MID) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_LG_END, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_LG_MID, p_i+1, fs); + } + } else { // radial + if (type == POINT_RG_CENTER || type == POINT_RG_MID1) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_RG_R1, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_RG_MID1, p_i+1, fs); + } + } + if ((type == POINT_RG_MID2) || + (type == POINT_RG_CENTER && dnext && !dnext->isSelected())) { + if (next_stop == last_stop) { + dnext = drag->getDraggerFor(item, POINT_RG_R2, p_i+1, fs); + } else { + dnext = drag->getDraggerFor(item, POINT_RG_MID2, p_i+1, fs); + } + } + } + + // if both adjacent draggers selected, + if ((std::find(these_stops.begin(),these_stops.end(),this_stop)==these_stops.end()) && dnext && dnext->isSelected()) { + + // remember the coords of the future dragger to select it + coords.push_back(0.5*(dragger->point + dnext->point)); + + // do not insert a stop now, it will confuse the loop; + // just remember the stops + these_stops.push_back(this_stop); + next_stops.push_back(next_stop); + } + } + } + } + return coords; +} + +void +sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc) +{ + SPDocument *doc = nullptr; + GrDrag *drag = rc->_grdrag; + + std::vector<SPStop *> these_stops; + std::vector<SPStop *> next_stops; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops); + + if (these_stops.empty() && drag->numSelected() == 1) { + // if a single stop is selected, add between that stop and the next one + GrDragger *dragger = *(drag->selected.begin()); + for (auto d : dragger->draggables) { + if (d->point_type == POINT_RG_FOCUS) { + /* + * There are 2 draggables at the center (start) of a radial gradient + * To avoid creating 2 separate stops, ignore this draggable point type + */ + continue; + } + SPGradient *gradient = getGradient(d->item, d->fill_or_stroke); + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (gradient, false); + SPStop *this_stop = sp_get_stop_i (vector, d->point_i); + if (this_stop) { + SPStop *next_stop = this_stop->getNextStop(); + if (next_stop) { + these_stops.push_back(this_stop); + next_stops.push_back(next_stop); + } + } + } + } + + // now actually create the new stops + auto i = these_stops.rbegin(); + auto j = next_stops.rbegin(); + std::vector<SPStop *> new_stops; + + for (;i != these_stops.rend() && j != next_stops.rend(); ++i, ++j ) { + SPStop *this_stop = *i; + SPStop *next_stop = *j; + gfloat offset = 0.5*(this_stop->offset + next_stop->offset); + SPObject *parent = this_stop->parent; + if (SP_IS_GRADIENT (parent)) { + doc = parent->document; + SPStop *new_stop = sp_vector_add_stop (SP_GRADIENT (parent), this_stop, next_stop, offset); + new_stops.push_back(new_stop); + SP_GRADIENT(parent)->ensureVector(); + } + } + + if (!these_stops.empty() && doc) { + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Add gradient stop")); + drag->updateDraggers(); + // so that it does not automatically update draggers in idle loop, as this would deselect + drag->local_change = true; + + // select the newly created stops + for (auto i:new_stops) { + drag->selectByStop(i); + } + } +} + +static double sqr(double x) {return x*x;} + +/** + * Remove unnecessary stops in the adjacent currently selected stops + * + * For selected stops that are adjacent to each other, remove + * stops that don't change the gradient visually, within a range of tolerance. + * + * @param rc GradientTool used to extract selected stops + * @param tolerance maximum difference between stop and expected color at that position + */ +static void +sp_gradient_simplify(GradientTool *rc, double tolerance) +{ + SPDocument *doc = nullptr; + GrDrag *drag = rc->_grdrag; + + std::vector<SPStop *> these_stops; + std::vector<SPStop *> next_stops; + + std::vector<Geom::Point> coords = sp_gradient_context_get_stop_intervals (drag, these_stops, next_stops); + + std::set<SPStop *> todel; + + auto i = these_stops.begin(); + auto j = next_stops.begin(); + for (; i != these_stops.end() && j != next_stops.end(); ++i, ++j) { + SPStop *stop0 = *i; + SPStop *stop1 = *j; + + // find the next adjacent stop if it exists and is in selection + auto i1 = std::find(these_stops.begin(), these_stops.end(), stop1); + if (i1 != these_stops.end()) { + if (next_stops.size()>(i1-these_stops.begin())) { + SPStop *stop2 = *(next_stops.begin() + (i1-these_stops.begin())); + + if (todel.find(stop0)!=todel.end() || todel.find(stop2) != todel.end()) + continue; + + // compare color of stop1 to the average color of stop0 and stop2 + guint32 const c0 = stop0->get_rgba32(); + guint32 const c2 = stop2->get_rgba32(); + guint32 const c1r = stop1->get_rgba32(); + guint32 c1 = average_color (c0, c2, + (stop1->offset - stop0->offset) / (stop2->offset - stop0->offset)); + + double diff = + sqr(SP_RGBA32_R_F(c1) - SP_RGBA32_R_F(c1r)) + + sqr(SP_RGBA32_G_F(c1) - SP_RGBA32_G_F(c1r)) + + sqr(SP_RGBA32_B_F(c1) - SP_RGBA32_B_F(c1r)) + + sqr(SP_RGBA32_A_F(c1) - SP_RGBA32_A_F(c1r)); + + if (diff < tolerance) + todel.insert(stop1); + } + } + } + + for (auto stop : todel) { + doc = stop->document; + Inkscape::XML::Node * parent = stop->getRepr()->parent(); + parent->removeChild( stop->getRepr() ); + } + + if (!todel.empty()) { + DocumentUndo::done(doc, SP_VERB_CONTEXT_GRADIENT, _("Simplify gradient")); + drag->local_change = true; + drag->updateDraggers(); + drag->selectByCoords(coords); + } +} + + +static void +sp_gradient_context_add_stop_near_point (GradientTool *rc, SPItem *item, Geom::Point mouse_p, guint32 /*etime*/) +{ + // item is the selected item. mouse_p the location in doc coordinates of where to add the stop + + ToolBase *ec = SP_EVENT_CONTEXT(rc); + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + double tolerance = (double) ec->tolerance; + + SPStop *newstop = ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom()); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); + + ec->get_drag()->updateDraggers(); + ec->get_drag()->local_change = true; + ec->get_drag()->selectByStop(newstop); +} + +bool GradientTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + GrDrag *drag = this->_grdrag; + g_assert (drag); + + gint ret = FALSE; + + auto move_handle = [&](int x_dir, int y_dir) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__SHIFT(event)) { + mul *= 10; + } + + y_dir *= -desktop->yaxisdir(); + + if (MOD__ALT(event)) { + drag->selected_move_screen(mul * x_dir, mul * y_dir); + } else { + mul *= nudge; + drag->selected_move(mul * x_dir, mul * y_dir); + } + }; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if ( event->button.button == 1 ) { + bool over_line = false; + SPCtrlLine *line = nullptr; + + if (!drag->lines.empty()) { + for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end() && (!over_line); ++l) { + line = *l; + over_line |= sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (over_line) { + // we take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + sp_gradient_context_add_stop_near_point(this, SP_ITEM(selection->items().front()), this->mousepoint_doc, event->button.time); + } else { + auto items= selection->items(); + for (auto i = items.begin();i!=items.end();++i) { + SPItem *item = *i; + SPGradientType new_type = (SPGradientType) prefs->getInt("/tools/gradient/newgradient", SP_GRADIENT_TYPE_LINEAR); + Inkscape::PaintTarget fsmode = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + SPGradient *vector = sp_gradient_vector_for_object(desktop->getDocument(), desktop, item, fsmode); + + SPGradient *priv = sp_item_set_gradient(item, vector, new_type, fsmode); + sp_gradient_reset_to_userspace(priv, item); + } + desktop->redrawDesktop();; + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_GRADIENT, + _("Create default gradient")); + } + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning ) { + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = desktop->w2d(button_w); + if (event->button.state & GDK_SHIFT_MASK) { + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + } else { + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + } + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point const motion_dt = this->desktop->w2d(motion_w); + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(motion_dt); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them")); + } else { + sp_gradient_drag(*this, motion_dt, event->motion.state, event->motion.time); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else { + if (!drag->mouseOver() && !selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt = this->desktop->w2d(motion_w); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + + bool over_line = false; + + if (!drag->lines.empty()) { + for (auto line : drag->lines) { + over_line |= sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if (this->cursor_addnode && !over_line) { + this->cursor_shape = cursor_gradient_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && over_line) { + this->cursor_shape = cursor_gradient_add_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if ( event->button.button == 1 && !this->space_panning ) { + bool over_line = false; + SPCtrlLine *line = nullptr; + + if (!drag->lines.empty()) { + for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end() && (!over_line); ++l) { + line = *l; + over_line = sp_gradient_context_is_over_line (this, (SPItem*) line, Geom::Point(event->motion.x, event->motion.y)); + } + } + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (over_line && line) { + sp_gradient_context_add_stop_near_point(this, line->item, this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + // we've been dragging, either do nothing (grdrag handles that), + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + drag->selectRect(*b); + } + } + } else if (this->item_to_select) { + if (over_line && line) { + // Clicked on an existing gradient line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + drag->deselectAll(); + selection->set(this->item_to_select); + } + } + } else { + // click in an empty space; do the same as Esc + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + selection->clear(); + } + } + + this->item_to_select = nullptr; + ret = TRUE; + } + + Inkscape::Rubberband::get(desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap gradient angle"), + _("<b>Shift</b>: draw gradient around the starting point"), + nullptr); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-grad"); + ret = TRUE; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && drag->isNonEmpty()) { + drag->selectAll(); + ret = TRUE; + } + break; + + case GDK_KEY_L: + case GDK_KEY_l: + if (MOD__CTRL_ONLY(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_gradient_simplify(this, 1e-4); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + Inkscape::SelectionHelper::selectNone(desktop); + } + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_Left: // move handle left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(-1, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move handle up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(0, 1); + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move handle right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(1, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move handle down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(0, -1); + ret = TRUE; + } + break; + + case GDK_KEY_r: + case GDK_KEY_R: + if (MOD__SHIFT_ONLY(event)) { + sp_gradient_reverse_selected_gradients(desktop); + ret = TRUE; + } + break; + + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // with any modifiers: + sp_gradient_context_add_stops_between_selected_stops (this); + ret = TRUE; + break; + + case GDK_KEY_i: + case GDK_KEY_I: + if (MOD__SHIFT_ONLY(event)) { + // Shift+I - insert stops (alternate keybinding for keyboards + // that don't have the Insert key) + sp_gradient_context_add_stops_between_selected_stops (this); + ret = TRUE; + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +// Creates a new linear or radial gradient. +static void sp_gradient_drag(GradientTool &rc, Geom::Point const pt, guint /*state*/, guint32 etime) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop; + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + ToolBase *ec = SP_EVENT_CONTEXT(&rc); + + if (!selection->isEmpty()) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int type = prefs->getInt("/tools/gradient/newgradient", 1); + Inkscape::PaintTarget fill_or_stroke = (prefs->getInt("/tools/gradient/newfillorstroke", 1) != 0) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + + SPGradient *vector; + if (ec->item_to_select) { + // pick color from the object where drag started + vector = sp_gradient_vector_for_object(document, desktop, ec->item_to_select, fill_or_stroke); + } else { + // Starting from empty space: + // Sort items so that the topmost comes last + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + sort(items.begin(),items.end(),sp_item_repr_compare_position_bool); + // take topmost + vector = sp_gradient_vector_for_object(document, desktop, SP_ITEM(items.back()), fill_or_stroke); + } + + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + + auto itemlist = selection->items(); + for (auto i = itemlist.begin();i!=itemlist.end();++i) { + + //FIXME: see above + sp_repr_css_change_recursive((*i)->getRepr(), css, "style"); + + sp_item_set_gradient(*i, vector, (SPGradientType) type, fill_or_stroke); + + if (type == SP_GRADIENT_TYPE_LINEAR) { + sp_item_gradient_set_coords (*i, POINT_LG_BEGIN, 0, rc.origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (*i, POINT_LG_END, 0, pt, fill_or_stroke, true, false); + } else if (type == SP_GRADIENT_TYPE_RADIAL) { + sp_item_gradient_set_coords (*i, POINT_RG_CENTER, 0, rc.origin, fill_or_stroke, true, false); + sp_item_gradient_set_coords (*i, POINT_RG_R1, 0, pt, fill_or_stroke, true, false); + } + (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG); + } + if (ec->_grdrag) { + ec->_grdrag->updateDraggers(); + // prevent regenerating draggers by selection modified signal, which sometimes + // comes too late and thus destroys the knot which we will now grab: + ec->_grdrag->local_change = true; + // give the grab out-of-bounds values of xp/yp because we're already dragging + // and therefore are already out of tolerance + ec->_grdrag->grabKnot (selection->items().front(), + type == SP_GRADIENT_TYPE_LINEAR? POINT_LG_END : POINT_RG_R1, + -1, // ignore number (though it is always 1) + fill_or_stroke, 99999, 99999, etime); + } + // We did an undoable action, but SPDocumentUndo::done will be called by the knot when released + + // status text; we do not track coords because this branch is run once, not all the time + // during drag + int n_objects = (int) boost::distance(selection->items()); + rc.message_context->setF(Inkscape::NORMAL_MESSAGE, + ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle", + "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects), + n_objects); + } else { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient.")); + } +} + +} +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/gradient-tool.h b/src/ui/tools/gradient-tool.h new file mode 100644 index 0000000..5de281a --- /dev/null +++ b/src/ui/tools/gradient-tool.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_GRADIENT_CONTEXT_H__ +#define __SP_GRADIENT_CONTEXT_H__ + +/* + * Gradient drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org. + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005,2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" + +#define SP_GRADIENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::GradientTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_GRADIENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::GradientTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class GradientTool : public ToolBase { +public: + GradientTool(); + ~GradientTool() override; + + Geom::Point origin; + + bool cursor_addnode; + + bool node_added; + + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + + sigc::connection *selcon; + sigc::connection *subselcon; + + static const std::string prefsPath; + + void setup() override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + void selection_changed(Inkscape::Selection*); +}; + +void sp_gradient_context_select_next (ToolBase *event_context); +void sp_gradient_context_select_prev (ToolBase *event_context); +void sp_gradient_context_add_stops_between_selected_stops (GradientTool *rc); + +} +} +} + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/lpe-tool.cpp b/src/ui/tools/lpe-tool.cpp new file mode 100644 index 0000000..d1e7e18 --- /dev/null +++ b/src/ui/tools/lpe-tool.cpp @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs + * + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Lauris Kaplinski <lauris@kaplinski.com> + * Abhishek Sharma + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtk/gtk.h> + +#include <2geom/sbasis-geometric.h> + +#include "desktop.h" +#include "document.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection.h" + +#include "display/curve.h" +#include "display/canvas-bpath.h" +#include "display/canvas-text.h" + +#include "object/sp-path.h" + +#include "ui/pixmaps/cursor-crosshairs.xpm" + +#include "util/units.h" + +#include "ui/toolbar/lpe-toolbar.h" +#include "ui/tools/lpe-tool.h" +#include "ui/shape-editor.h" + +using Inkscape::Util::unit_table; +using Inkscape::UI::Tools::PenTool; + +const int num_subtools = 8; + +SubtoolEntry lpesubtools[] = { + // this must be here to account for the "all inactive" action + {Inkscape::LivePathEffect::INVALID_LPE, "draw-geometry-inactive"}, + {Inkscape::LivePathEffect::LINE_SEGMENT, "draw-geometry-line-segment"}, + {Inkscape::LivePathEffect::CIRCLE_3PTS, "draw-geometry-circle-from-three-points"}, + {Inkscape::LivePathEffect::CIRCLE_WITH_RADIUS, "draw-geometry-circle-from-radius"}, + {Inkscape::LivePathEffect::PARALLEL, "draw-geometry-line-parallel"}, + {Inkscape::LivePathEffect::PERP_BISECTOR, "draw-geometry-line-perpendicular"}, + {Inkscape::LivePathEffect::ANGLE_BISECTOR, "draw-geometry-angle-bisector"}, + {Inkscape::LivePathEffect::MIRROR_SYMMETRY, "draw-geometry-mirror"} +}; + +namespace Inkscape { +namespace UI { +namespace Tools { + +void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data); + +const std::string& LpeTool::getPrefsPath() { + return LpeTool::prefsPath; +} + +const std::string LpeTool::prefsPath = "/tools/lpetool"; + +LpeTool::LpeTool() + : PenTool(cursor_crosshairs_xpm) + , shape_editor(nullptr) + , canvas_bbox(nullptr) + , mode(Inkscape::LivePathEffect::BEND_PATH) +// TODO: pointer? + , measuring_items(new std::map<SPPath *, SPCanvasItem*>) +{ +} + +LpeTool::~LpeTool() { + delete this->shape_editor; + this->shape_editor = nullptr; + + if (this->canvas_bbox) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(this->canvas_bbox)); + this->canvas_bbox = nullptr; + } + + lpetool_delete_measuring_items(this); + delete this->measuring_items; + this->measuring_items = nullptr; + + this->sel_changed_connection.disconnect(); +} + +void LpeTool::setup() { + PenTool::setup(); + + Inkscape::Selection *selection = this->desktop->getSelection(); + SPItem *item = selection->singleItem(); + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = + selection->connectChanged(sigc::bind(sigc::ptr_fun(&sp_lpetool_context_selection_changed), (gpointer)this)); + + this->shape_editor = new ShapeEditor(this->desktop); + + lpetool_context_switch_mode(this, Inkscape::LivePathEffect::INVALID_LPE); + lpetool_context_reset_limiting_bbox(this); + lpetool_create_measuring_items(this); + +// TODO temp force: + this->enableSelectionCue(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (item) { + this->shape_editor->set_item(item); + } + + if (prefs->getBool("/tools/lpetool/selcue")) { + this->enableSelectionCue(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new nodepath and reassigns listeners to the new selected item's repr. + */ +void sp_lpetool_context_selection_changed(Inkscape::Selection *selection, gpointer data) +{ + LpeTool *lc = SP_LPETOOL_CONTEXT(data); + + lc->shape_editor->unset_item(); + SPItem *item = selection->singleItem(); + lc->shape_editor->set_item(item); +} + +void LpeTool::set(const Inkscape::Preferences::Entry& val) { + if (val.getEntryName() == "mode") { + Inkscape::Preferences::get()->setString("/tools/geometric/mode", "drag"); + SP_PEN_CONTEXT(this)->mode = PenTool::MODE_DRAG; + } +} + +bool LpeTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + { + // select the clicked item but do nothing else + Inkscape::Selection * const selection = this->desktop->getSelection(); + selection->clear(); + selection->add(item); + ret = TRUE; + break; + } + case GDK_BUTTON_RELEASE: + // TODO: do we need to catch this or can we pass it on to the parent handler? + ret = TRUE; + break; + default: + break; + } + + if (!ret) { + ret = PenTool::item_handler(item, event); + } + + return ret; +} + +bool LpeTool::root_handler(GdkEvent* event) { + Inkscape::Selection *selection = desktop->getSelection(); + + bool ret = false; + + if (this->hasWaitingLPE()) { + // quit when we are waiting for a LPE to be applied + //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event); + return PenTool::root_handler(event); + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (this->mode == Inkscape::LivePathEffect::INVALID_LPE) { + // don't do anything for now if we are inactive (except clearing the selection + // since this was a click into empty space) + selection->clear(); + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Choose a construction tool from the toolbar.")); + ret = true; + break; + } + + // save drag origin + this->xp = (gint) event->button.x; + this->yp = (gint) event->button.y; + this->within_tolerance = true; + + using namespace Inkscape::LivePathEffect; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int mode = prefs->getInt("/tools/lpetool/mode"); + EffectType type = lpesubtools[mode].type; + + //bool over_stroke = lc->shape_editor->is_over_stroke(Geom::Point(event->button.x, event->button.y), true); + + this->waitForLPEMouseClicks(type, Inkscape::LivePathEffect::Effect::acceptsNumClicks(type)); + + // we pass the mouse click on to pen tool as the first click which it should collect + //ret = ((ToolBaseClass *) sp_lpetool_context_parent_class)->root_handler(event_context, event); + ret = PenTool::root_handler(event); + } + break; + + + case GDK_BUTTON_RELEASE: + { + /** + break; + **/ + } + + case GDK_KEY_PRESS: + /** + switch (get_latin_keyval (&event->key)) { + } + break; + **/ + + case GDK_KEY_RELEASE: + /** + switch (get_latin_keyval(&event->key)) { + case GDK_Control_L: + case GDK_Control_R: + dc->_message_context->clear(); + break; + default: + break; + } + **/ + + default: + break; + } + + if (!ret) { + ret = PenTool::root_handler(event); + } + + return ret; +} + +/* + * Finds the index in the list of geometric subtools corresponding to the given LPE type. + * Returns -1 if no subtool is found. + */ +int +lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type) { + for (int i = 0; i < num_subtools; ++i) { + if (lpesubtools[i].type == type) { + return i; + } + } + return -1; +} + +/* + * Checks whether an item has a construction applied as LPE and if so returns the index in + * lpesubtools of this construction + */ +int lpetool_item_has_construction(LpeTool */*lc*/, SPItem *item) +{ + if (!SP_IS_LPE_ITEM(item)) { + return -1; + } + + Inkscape::LivePathEffect::Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + if (!lpe) { + return -1; + } + return lpetool_mode_to_index(lpe->effectType()); +} + +/* + * Attempts to perform the construction of the given type (i.e., to apply the corresponding LPE) to + * a single selected item. Returns whether we succeeded. + */ +bool +lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type) +{ + Inkscape::Selection *selection = lc->desktop->getSelection(); + SPItem *item = selection->singleItem(); + + // TODO: should we check whether type represents a valid geometric construction? + if (item && SP_IS_LPE_ITEM(item) && Inkscape::LivePathEffect::Effect::acceptsNumClicks(type) == 0) { + Inkscape::LivePathEffect::Effect::createAndApply(type, lc->desktop->getDocument(), item); + return true; + } + return false; +} + +void +lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type) +{ + int index = lpetool_mode_to_index(type); + if (index != -1) { + lc->mode = type; + auto tb = dynamic_cast<UI::Toolbar::LPEToolbar*>(lc->desktop->get_toolbar_by_name("LPEToolToolbar")); + + if(tb) { + tb->set_mode(index); + } else { + std::cerr << "Could not access LPE toolbar" << std::endl; + } + } else { + g_warning ("Invalid mode selected: %d", type); + return; + } +} + +void +lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B) { + Geom::Coord w = document->getWidth().value("px"); + Geom::Coord h = document->getHeight().value("px"); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + double ulx = prefs->getDouble("/tools/lpetool/bbox_upperleftx", 0); + double uly = prefs->getDouble("/tools/lpetool/bbox_upperlefty", 0); + double lrx = prefs->getDouble("/tools/lpetool/bbox_lowerrightx", w); + double lry = prefs->getDouble("/tools/lpetool/bbox_lowerrighty", h); + + A = Geom::Point(ulx, uly); + B = Geom::Point(lrx, lry); +} + +/* + * Reads the limiting bounding box from preferences and draws it on the screen + */ +// TODO: Note that currently the bbox is not user-settable; we simply use the page borders +void +lpetool_context_reset_limiting_bbox(LpeTool *lc) +{ + if (lc->canvas_bbox) { + sp_canvas_item_destroy(lc->canvas_bbox); + lc->canvas_bbox = nullptr; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (!prefs->getBool("/tools/lpetool/show_bbox", true)) + return; + + SPDocument *document = lc->desktop->getDocument(); + + Geom::Point A, B; + lpetool_get_limiting_bbox_corners(document, A, B); + Geom::Affine doc2dt(lc->desktop->doc2dt()); + A *= doc2dt; + B *= doc2dt; + + Geom::Rect rect(A, B); + SPCurve *curve = SPCurve::new_from_rect(rect); + + lc->canvas_bbox = sp_canvas_bpath_new (lc->desktop->getControls(), curve); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(lc->canvas_bbox), 0x0000ffff, 0.8, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT, 5, 5); +} + +static void +set_pos_and_anchor(SPCanvasText *canvas_text, const Geom::Piecewise<Geom::D2<Geom::SBasis> > &pwd2, + const double t, const double length, bool /*use_curvature*/ = false) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > pwd2_reparam = arc_length_parametrization(pwd2, 2 , 0.1); + double t_reparam = pwd2_reparam.cuts.back() * t; + Point pos = pwd2_reparam.valueAt(t_reparam); + Point dir = unit_vector(derivative(pwd2_reparam).valueAt(t_reparam)); + Point n = -rot90(dir); + double angle = Geom::angle_between(dir, Point(1,0)); + + sp_canvastext_set_coords(canvas_text, pos + n * length); + sp_canvastext_set_anchor_manually(canvas_text, std::sin(angle), -std::cos(angle)); +} + +void +lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection) +{ + if (!selection) { + selection = lc->desktop->getSelection(); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show = prefs->getBool("/tools/lpetool/show_measuring_info", true); + + SPPath *path; + SPCurve *curve; + SPCanvasText *canvas_text; + SPCanvasGroup *tmpgrp = lc->desktop->getTempGroup(); + gchar *arc_length; + double lengthval; + auto items= selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + if (SP_IS_PATH(*i)) { + path = SP_PATH(*i); + curve = path->getCurve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = paths_to_pw(curve->get_pathvector()); + canvas_text = (SPCanvasText *) sp_canvastext_new(tmpgrp, lc->desktop, Geom::Point(0,0), ""); + if (!show) + sp_canvas_item_hide(SP_CANVAS_ITEM(canvas_text)); + + Inkscape::Util::Unit const * unit = nullptr; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + + lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str()); + sp_canvastext_set_text (canvas_text, arc_length); + set_pos_and_anchor(canvas_text, pwd2, 0.5, 10); + // TODO: must we free arc_length? + (*lc->measuring_items)[path] = SP_CANVAS_ITEM(canvas_text); + } + } +} + +void +lpetool_delete_measuring_items(LpeTool *lc) +{ + std::map<SPPath *, SPCanvasItem*>::iterator i; + for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) { + sp_canvas_item_destroy(i->second); + } + lc->measuring_items->clear(); +} + +void +lpetool_update_measuring_items(LpeTool *lc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for ( std::map<SPPath *, SPCanvasItem*>::iterator i = lc->measuring_items->begin(); + i != lc->measuring_items->end(); + ++i ) + { + SPPath *path = i->first; + SPCurve *curve = path->getCurve(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = Geom::paths_to_pw(curve->get_pathvector()); + Inkscape::Util::Unit const * unit = nullptr; + if (prefs->getString("/tools/lpetool/unit").compare("")) { + unit = unit_table.getUnit(prefs->getString("/tools/lpetool/unit")); + } else { + unit = unit_table.getUnit("px"); + } + double lengthval = Geom::length(pwd2); + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit); + gchar *arc_length = g_strdup_printf("%.2f %s", lengthval, unit->abbr.c_str()); + sp_canvastext_set_text (SP_CANVASTEXT(i->second), arc_length); + set_pos_and_anchor(SP_CANVASTEXT(i->second), pwd2, 0.5, 10); + // TODO: must we free arc_length? + } +} + +void +lpetool_show_measuring_info(LpeTool *lc, bool show) +{ + std::map<SPPath *, SPCanvasItem*>::iterator i; + for (i = lc->measuring_items->begin(); i != lc->measuring_items->end(); ++i) { + if (show) { + sp_canvas_item_show(i->second); + } else { + sp_canvas_item_hide(i->second); + } + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/lpe-tool.h b/src/ui/tools/lpe-tool.h new file mode 100644 index 0000000..789aaf3 --- /dev/null +++ b/src/ui/tools/lpe-tool.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SP_LPETOOL_CONTEXT_H_SEEN +#define SP_LPETOOL_CONTEXT_H_SEEN + +/* + * LPEToolContext: a context for a generic tool composed of subtools that are given by LPEs + * + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 1998 The Free Software Foundation + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2008 Maximilian Albert + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/pen-tool.h" + +#define SP_LPETOOL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::LpeTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_LPETOOL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::LpeTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +/* This is the list of subtools from which the toolbar of the LPETool is built automatically */ +extern const int num_subtools; + +struct SubtoolEntry { + Inkscape::LivePathEffect::EffectType type; + gchar const *icon_name; +}; + +extern SubtoolEntry lpesubtools[]; + +enum LPEToolState { + LPETOOL_STATE_PEN, + LPETOOL_STATE_NODE +}; + +namespace Inkscape { +class Selection; +} + +class ShapeEditor; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class LpeTool : public PenTool { +public: + LpeTool(); + ~LpeTool() override; + + ShapeEditor* shape_editor; + SPCanvasItem *canvas_bbox; + Inkscape::LivePathEffect::EffectType mode; + + std::map<SPPath *, SPCanvasItem*> *measuring_items; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + + static const std::string prefsPath; + + const std::string& getPrefsPath() override; + +protected: + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; +}; + +int lpetool_mode_to_index(Inkscape::LivePathEffect::EffectType const type); +int lpetool_item_has_construction(LpeTool *lc, SPItem *item); +bool lpetool_try_construction(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type); +void lpetool_context_switch_mode(LpeTool *lc, Inkscape::LivePathEffect::EffectType const type); +void lpetool_get_limiting_bbox_corners(SPDocument *document, Geom::Point &A, Geom::Point &B); +void lpetool_context_reset_limiting_bbox(LpeTool *lc); +void lpetool_create_measuring_items(LpeTool *lc, Inkscape::Selection *selection = nullptr); +void lpetool_delete_measuring_items(LpeTool *lc); +void lpetool_update_measuring_items(LpeTool *lc); +void lpetool_show_measuring_info(LpeTool *lc, bool show = true); + +} +} +} + +#endif // SP_LPETOOL_CONTEXT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/measure-tool.cpp b/src/ui/tools/measure-tool.cpp new file mode 100644 index 0000000..caf9e3c --- /dev/null +++ b/src/ui/tools/measure-tool.cpp @@ -0,0 +1,1466 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Our nice measuring tool + * + * Authors: + * Felipe Correa da Silva Sanches <juca@members.fsf.org> + * Jon A. Cruz <jon@joncruz.org> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <boost/none_t.hpp> + +#include <2geom/line.h> +#include <2geom/path-intersection.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "path-chemistry.h" +#include "rubberband.h" +#include "text-editing.h" +#include "verbs.h" + +#include "display/curve.h" +#include "display/sodipodi-ctrl.h" +#include "display/sp-canvas-util.h" +#include "display/sp-canvas.h" +#include "display/sp-ctrlcurve.h" +#include "display/sp-ctrlline.h" + +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/pixmaps/cursor-measure.xpm" + +#include "svg/stringstream.h" +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "ui/dialog/knot-properties.h" +#include "ui/tools/freehand-base.h" +#include "ui/tools/measure-tool.h" + +#include "util/units.h" + +using Inkscape::ControlManager; +using Inkscape::CTLINE_SECONDARY; +using Inkscape::Util::unit_table; +using Inkscape::DocumentUndo; + +#define MT_KNOT_COLOR_NORMAL 0xffffff00 +#define MT_KNOT_COLOR_MOUSEOVER 0xff000000 + + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& MeasureTool::getPrefsPath() +{ + return MeasureTool::prefsPath; +} + +const std::string MeasureTool::prefsPath = "/tools/measure"; + +namespace { + +/** + * Simple class to use for removing label overlap. + */ +class LabelPlacement { +public: + + double lengthVal; + double offset; + Geom::Point start; + Geom::Point end; +}; + +bool SortLabelPlacement(LabelPlacement const &first, LabelPlacement const &second) +{ + if (first.end[Geom::Y] == second.end[Geom::Y]) { + return first.end[Geom::X] < second.end[Geom::X]; + } else { + return first.end[Geom::Y] < second.end[Geom::Y]; + } +} + +//precision is for give the number of decimal positions +//of the label to calculate label width +void repositionOverlappingLabels(std::vector<LabelPlacement> &placements, SPDesktop *desktop, Geom::Point const &normal, double fontsize, int precision) +{ + std::sort(placements.begin(), placements.end(), SortLabelPlacement); + + double border = 3; + Geom::Rect box; + { + Geom::Point tmp(fontsize * (6 + precision) + (border * 2), fontsize + (border * 2)); + tmp = desktop->w2d(tmp); + box = Geom::Rect(-tmp[Geom::X] / 2, -tmp[Geom::Y] / 2, tmp[Geom::X] / 2, tmp[Geom::Y] / 2); + } + + // Using index since vector may be re-ordered as we go. + // Starting at one, since the first item can't overlap itself + for (size_t i = 1; i < placements.size(); i++) { + LabelPlacement &place = placements[i]; + + bool changed = false; + do { + Geom::Rect current(box + place.end); + + changed = false; + bool overlaps = false; + for (size_t j = i; (j > 0) && !overlaps; --j) { + LabelPlacement &otherPlace = placements[j - 1]; + Geom::Rect target(box + otherPlace.end); + if (current.intersects(target)) { + overlaps = true; + } + } + if (overlaps) { + place.offset += (fontsize + border); + place.end = place.start - desktop->w2d(normal * place.offset); + changed = true; + } + } while (changed); + + std::sort(placements.begin(), placements.begin() + i + 1, SortLabelPlacement); + } +} + +/** + * Calculates where to place the anchor for the display text and arc. + * + * @param desktop the desktop that is being used. + * @param angle the angle to be displaying. + * @param baseAngle the angle of the initial baseline. + * @param startPoint the point that is the vertex of the selected angle. + * @param endPoint the point that is the end the user is manipulating for measurement. + * @param fontsize the size to display the text label at. + */ +Geom::Point calcAngleDisplayAnchor(SPDesktop *desktop, double angle, double baseAngle, + Geom::Point const &startPoint, Geom::Point const &endPoint, + double fontsize) +{ + // Time for the trick work of figuring out where things should go, and how. + double lengthVal = (endPoint - startPoint).length(); + double effective = baseAngle + (angle / 2); + Geom::Point where(lengthVal, 0); + where *= Geom::Affine(Geom::Rotate(effective)) * Geom::Affine(Geom::Translate(startPoint)); + + // When the angle is tight, the label would end up under the cursor and/or lines. Bump it + double scaledFontsize = std::abs(fontsize * desktop->w2d(Geom::Point(0, 1.0))[Geom::Y]); + if (std::abs((where - endPoint).length()) < scaledFontsize) { + where[Geom::Y] += scaledFontsize * 2; + } + + // We now have the ideal position, but need to see if it will fit/work. + + Geom::Rect visibleArea = desktop->get_display_area(); + // Bring it in to "title safe" for the anchor point + Geom::Point textBox = desktop->w2d(Geom::Point(fontsize * 3, fontsize / 2)); + textBox[Geom::Y] = std::abs(textBox[Geom::Y]); + + visibleArea = Geom::Rect(visibleArea.min()[Geom::X] + textBox[Geom::X], + visibleArea.min()[Geom::Y] + textBox[Geom::Y], + visibleArea.max()[Geom::X] - textBox[Geom::X], + visibleArea.max()[Geom::Y] - textBox[Geom::Y]); + + where[Geom::X] = std::min(where[Geom::X], visibleArea.max()[Geom::X]); + where[Geom::X] = std::max(where[Geom::X], visibleArea.min()[Geom::X]); + where[Geom::Y] = std::min(where[Geom::Y], visibleArea.max()[Geom::Y]); + where[Geom::Y] = std::max(where[Geom::Y], visibleArea.min()[Geom::Y]); + + return where; +} + +/** + * Create a measure item in current document. + * + * @param pathv the path to create. + * @param markers if the path results get markers. + * @param color of the stroke. + * @param measure_repr container element. + */ +void setMeasureItem(Geom::PathVector pathv, bool is_curve, bool markers, guint32 color, Inkscape::XML::Node *measure_repr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop) { + return; + } + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr; + repr = xml_doc->createElement("svg:path"); + gchar *str = sp_svg_write_path(pathv); + SPCSSAttr *css = sp_repr_css_attr_new(); + Geom::Coord strokewidth = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse().expansionX(); + std::stringstream stroke_width; + stroke_width.imbue(std::locale::classic()); + if(measure_repr) { + stroke_width << strokewidth / desktop->current_zoom(); + } else { + stroke_width << strokewidth; + } + sp_repr_css_set_property (css, "stroke-width", stroke_width.str().c_str()); + sp_repr_css_set_property (css, "fill", "none"); + if(color) { + gchar color_line[64]; + sp_svg_write_color (color_line, sizeof(color_line), color); + sp_repr_css_set_property (css, "stroke", color_line); + } else { + sp_repr_css_set_property (css, "stroke", "#ff0000"); + } + char const * stroke_linecap = is_curve ? "butt" : "square"; + sp_repr_css_set_property (css, "stroke-linecap", stroke_linecap); + sp_repr_css_set_property (css, "stroke-linejoin", "miter"); + sp_repr_css_set_property (css, "stroke-miterlimit", "4"); + sp_repr_css_set_property (css, "stroke-dasharray", "none"); + if(measure_repr) { + sp_repr_css_set_property (css, "stroke-opacity", "0.5"); + } else { + sp_repr_css_set_property (css, "stroke-opacity", "1"); + } + if(markers) { + sp_repr_css_set_property (css, "marker-start", "url(#Arrow2Sstart)"); + sp_repr_css_set_property (css, "marker-end", "url(#Arrow2Send)"); + } + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + repr->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + g_assert( str != nullptr ); + repr->setAttribute("d", str); + g_free(str); + if(measure_repr) { + measure_repr->addChild(repr, nullptr); + Inkscape::GC::release(repr); + } else { + SPItem *item = SP_ITEM(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + item->updateRepr(); + desktop->getSelection()->clear(); + desktop->getSelection()->add(item); + } +} + +/** + * Given an angle, the arc center and edge point, draw an arc segment centered around that edge point. + * + * @param desktop the desktop that is being used. + * @param center the center point for the arc. + * @param end the point that ends at the edge of the arc segment. + * @param anchor the anchor point for displaying the text label. + * @param angle the angle of the arc segment to draw. + * @param measure_rpr the container of the curve if converted to items. + */ +void createAngleDisplayCurve(SPDesktop *desktop, Geom::Point const ¢er, Geom::Point const &end, Geom::Point const &anchor, double angle, bool to_phantom, std::vector<SPCanvasItem *> &measure_phantom_items , std::vector<SPCanvasItem *> &measure_tmp_items , Inkscape::XML::Node *measure_repr = nullptr) +{ + // Given that we have a point on the arc's edge and the angle of the arc, we need to get the two endpoints. + + double textLen = std::abs((anchor - center).length()); + double sideLen = std::abs((end - center).length()); + if (sideLen > 0.0) { + double factor = std::min(1.0, textLen / sideLen); + + // arc start + Geom::Point p1 = end * (Geom::Affine(Geom::Translate(-center)) + * Geom::Affine(Geom::Scale(factor)) + * Geom::Affine(Geom::Translate(center))); + + // arc end + Geom::Point p4 = p1 * (Geom::Affine(Geom::Translate(-center)) + * Geom::Affine(Geom::Rotate(-angle)) + * Geom::Affine(Geom::Translate(center))); + + // from Riskus + double xc = center[Geom::X]; + double yc = center[Geom::Y]; + double ax = p1[Geom::X] - xc; + double ay = p1[Geom::Y] - yc; + double bx = p4[Geom::X] - xc; + double by = p4[Geom::Y] - yc; + double q1 = (ax * ax) + (ay * ay); + double q2 = q1 + (ax * bx) + (ay * by); + + double k2 = (4.0 / 3.0) * (std::sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx)); + + Geom::Point p2(xc + ax - (k2 * ay), + yc + ay + (k2 * ax)); + Geom::Point p3(xc + bx + (k2 * by), + yc + by - (k2 * bx)); + SPCtrlCurve *curve = ControlManager::getManager().createControlCurve(desktop->getTempGroup(), p1, p2, p3, p4, CTLINE_SECONDARY); + if(to_phantom){ + curve->rgba = 0x8888887f; + measure_phantom_items.push_back(SP_CANVAS_ITEM(curve)); + } else { + measure_tmp_items.push_back(SP_CANVAS_ITEM(curve)); + } + sp_canvas_item_move_to_z(SP_CANVAS_ITEM(curve), 0); + sp_canvas_item_show(SP_CANVAS_ITEM(curve)); + if(measure_repr) { + Geom::PathVector pathv; + Geom::Path path; + path.start(desktop->doc2dt(p1)); + path.appendNew<Geom::CubicBezier>(desktop->doc2dt(p2),desktop->doc2dt(p3),desktop->doc2dt(p4)); + pathv.push_back(path); + pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + if(!pathv.empty()) { + setMeasureItem(pathv, true, false, 0xff00007f, measure_repr); + } + } + } +} + +} // namespace + +boost::optional<Geom::Point> explicit_base_tmp = boost::none; + +MeasureTool::MeasureTool() + : ToolBase(cursor_measure_xpm) + , grabbed(nullptr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + start_p = readMeasurePoint(true); + end_p = readMeasurePoint(false); + dimension_offset = 35; + last_pos = Geom::Point(0,0); + // create the knots + this->knot_start = new SPKnot(desktop, _("Measure start, <b>Shift+Click</b> for position dialog")); + this->knot_start->setMode(SP_KNOT_MODE_XOR); + this->knot_start->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER); + this->knot_start->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f); + this->knot_start->setShape(SP_KNOT_SHAPE_CIRCLE); + this->knot_start->updateCtrl(); + this->knot_end = new SPKnot(desktop, _("Measure end, <b>Shift+Click</b> for position dialog")); + this->knot_end->setMode(SP_KNOT_MODE_XOR); + this->knot_end->setFill(MT_KNOT_COLOR_NORMAL, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER, MT_KNOT_COLOR_MOUSEOVER); + this->knot_end->setStroke(0x0000007f, 0x0000007f, 0x0000007f, 0x0000007f); + this->knot_end->setShape(SP_KNOT_SHAPE_CIRCLE); + this->knot_end->updateCtrl(); + Geom::Rect display_area = desktop->get_display_area(); + if(display_area.interiorContains(start_p) && display_area.interiorContains(end_p) && end_p != Geom::Point()) { + this->knot_start->moveto(start_p); + this->knot_start->show(); + this->knot_end->moveto(end_p); + this->knot_end->show(); + showCanvasItems(); + } else { + start_p = Geom::Point(0,0); + end_p = Geom::Point(0,0); + writeMeasurePoint(start_p, true); + writeMeasurePoint(end_p, false); + } + this->_knot_start_moved_connection = this->knot_start->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotStartMovedHandler)); + this->_knot_start_click_connection = this->knot_start->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler)); + this->_knot_start_ungrabbed_connection = this->knot_start->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler)); + this->_knot_end_moved_connection = this->knot_end->moved_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotEndMovedHandler)); + this->_knot_end_click_connection = this->knot_end->click_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotClickHandler)); + this->_knot_end_ungrabbed_connection = this->knot_end->ungrabbed_signal.connect(sigc::mem_fun(*this, &MeasureTool::knotUngrabbedHandler)); + +} + +MeasureTool::~MeasureTool() +{ + this->_knot_start_moved_connection.disconnect(); + this->_knot_start_ungrabbed_connection.disconnect(); + this->_knot_end_moved_connection.disconnect(); + this->_knot_end_ungrabbed_connection.disconnect(); + + /* unref should call destroy */ + knot_unref(this->knot_start); + knot_unref(this->knot_end); + for (auto & measure_tmp_item : measure_tmp_items) { + sp_canvas_item_destroy(measure_tmp_item); + } + measure_tmp_items.clear(); + for (auto & idx : measure_item) { + sp_canvas_item_destroy(idx); + } + measure_item.clear(); + for (auto & measure_phantom_item : measure_phantom_items) { + sp_canvas_item_destroy(measure_phantom_item); + } + measure_phantom_items.clear(); +} + +Geom::Point MeasureTool::readMeasurePoint(bool is_start) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end"; + return prefs->getPoint(measure_point, Geom::Point(Geom::infinity(),Geom::infinity())); +} + +void MeasureTool::writeMeasurePoint(Geom::Point point, bool is_start) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring measure_point = is_start ? "/tools/measure/measure-start" : "/tools/measure/measure-end"; + prefs->setPoint(measure_point, point); +} + +//This function is used to reverse the Measure, I do it in two steps because when +//we move the knot the start_ or the end_p are overwritten so I need the original values. +void MeasureTool::reverseKnots() +{ + Geom::Point start = start_p; + Geom::Point end = end_p; + this->knot_start->moveto(end); + this->knot_start->show(); + this->knot_end->moveto(start); + this->knot_end->show(); + start_p = end; + end_p = start; + this->showCanvasItems(); +} + +void MeasureTool::knotClickHandler(SPKnot *knot, guint state) +{ + if (state & GDK_SHIFT_MASK) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring const unit_name = prefs->getString("/tools/measure/unit"); + explicit_base = explicit_base_tmp; + Inkscape::UI::Dialogs::KnotPropertiesDialog::showDialog(desktop, knot, unit_name); + } +} + +void MeasureTool::knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state) +{ + Geom::Point point = this->knot_start->position(); + if (state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, point, end_p, state); + } else if (!(state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(this->knot_end->position()); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + point = sp.getPoint(); + snap_manager.unSetup(); + } + if(start_p != point) { + start_p = point; + this->knot_start->moveto(start_p); + } + showCanvasItems(); +} + +void MeasureTool::knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state) +{ + Geom::Point point = this->knot_end->position(); + if (state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, point, start_p, state); + } else if (!(state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + Inkscape::SnapCandidatePoint scp(point, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(this->knot_start->position()); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + point = sp.getPoint(); + snap_manager.unSetup(); + } + if(end_p != point) { + end_p = point; + this->knot_end->moveto(end_p); + } + showCanvasItems(); +} + +void MeasureTool::knotUngrabbedHandler(SPKnot */*knot*/, unsigned int state) +{ + this->knot_start->moveto(start_p); + this->knot_end->moveto(end_p); + showCanvasItems(); +} + + +//todo: we need this function? +void MeasureTool::finish() +{ + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + ToolBase::finish(); +} + +static void calculate_intersections(SPDesktop * /*desktop*/, SPItem* item, Geom::PathVector const &lineseg, SPCurve *curve, std::vector<double> &intersections) +{ + curve->transform(item->i2doc_affine()); + // Find all intersections of the control-line with this shape + Geom::CrossingSet cs = Geom::crossings(lineseg, curve->get_pathvector()); + Geom::delete_duplicates(cs[0]); + + // Reconstruct and store the points of intersection + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_hidden = prefs->getBool("/tools/measure/show_hidden", true); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + for (const auto & m : cs[0]) { + if (!show_hidden) { + double eps = 0.0001; + if ((m.ta > eps && + item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta - eps))), true, nullptr)) || + (m.ta + eps < 1 && + item == desktop->getItemAtPoint(desktop->d2w(desktop->dt2doc(lineseg[0].pointAt(m.ta + eps))), true, nullptr))) { + intersections.push_back(m.ta); + } + } else { + intersections.push_back(m.ta); + } + } +} + +bool MeasureTool::root_handler(GdkEvent* event) +{ + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: { + this->knot_start->hide(); + this->knot_end->hide(); + Geom::Point const button_w(event->button.x, event->button.y); + explicit_base = boost::none; + explicit_base_tmp = boost::none; + last_end = boost::none; + + if (event->button.button == 1 && !this->space_panning) { + // save drag origin + start_p = desktop->w2d(Geom::Point(event->button.x, event->button.y)); + within_tolerance = true; + + ret = TRUE; + } + + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + snap_manager.freeSnapReturnByRef(start_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + snap_manager.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + nullptr, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + break; + } + case GDK_KEY_PRESS: { + if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) { + explicit_base_tmp = explicit_base; + explicit_base = end_p; + showInfoBox(last_pos, true); + } + break; + } + case GDK_KEY_RELEASE: { + if ((event->key.keyval == GDK_KEY_Control_L) || (event->key.keyval == GDK_KEY_Control_R)) { + showInfoBox(last_pos, false); + } + break; + } + case GDK_MOTION_NOTIFY: { + if (!(event->motion.state & GDK_BUTTON1_MASK)) { + if(!(event->motion.state & GDK_SHIFT_MASK)) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + + snap_manager.preSnap(scp); + snap_manager.unSetup(); + } + last_pos = Geom::Point(event->motion.x, event->motion.y); + if (event->motion.state & GDK_CONTROL_MASK) { + showInfoBox(last_pos, true); + } else { + showInfoBox(last_pos, false); + } + } else { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + //Inkscape::Util::Unit const * unit = desktop->getNamedView()->getDisplayUnit(); + for (auto & idx : measure_item) { + sp_canvas_item_destroy(idx); + } + measure_item.clear(); + ret = TRUE; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + Geom::Point const motion_w(event->motion.x, event->motion.y); + if ( within_tolerance) { + if ( Geom::LInfty( motion_w - start_p ) < tolerance) { + return FALSE; // Do not drag if we're within tolerance from origin. + } + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + if(event->motion.time == 0 || !last_end || Geom::LInfty( motion_w - *last_end ) > (tolerance/4.0)) { + Geom::Point const motion_dt(desktop->w2d(motion_w)); + end_p = motion_dt; + + if (event->motion.state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state); + } else if (!(event->motion.state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + end_p = sp.getPoint(); + snap_manager.unSetup(); + } + showCanvasItems(); + last_end = motion_w ; + } + gobble_motion_events(GDK_BUTTON1_MASK); + } + break; + } + case GDK_BUTTON_RELEASE: { + this->knot_start->moveto(start_p); + this->knot_start->show(); + if(last_end) { + end_p = desktop->w2d(*last_end); + if (event->button.state & GDK_CONTROL_MASK) { + spdc_endpoint_snap_rotation(this, end_p, start_p, event->motion.state); + } else if (!(event->button.state & GDK_SHIFT_MASK)) { + SnapManager &snap_manager = desktop->namedview->snap_manager; + snap_manager.setup(desktop); + Inkscape::SnapCandidatePoint scp(end_p, Inkscape::SNAPSOURCE_OTHER_HANDLE); + scp.addOrigin(start_p); + Inkscape::SnappedPoint sp = snap_manager.freeSnap(scp); + end_p = sp.getPoint(); + snap_manager.unSetup(); + } + } + this->knot_end->moveto(end_p); + this->knot_end->show(); + showCanvasItems(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + break; + } + default: + break; + } + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void MeasureTool::setMarkers() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *doc = desktop->getDocument(); + SPObject *arrowStart = doc->getObjectById("Arrow2Sstart"); + SPObject *arrowEnd = doc->getObjectById("Arrow2Send"); + if (!arrowStart) { + setMarker(true); + } + if(!arrowEnd) { + setMarker(false); + } +} +void MeasureTool::setMarker(bool isStart) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *doc = desktop->getDocument(); + SPDefs *defs = doc->getDefs(); + Inkscape::XML::Node *rmarker; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + rmarker = xml_doc->createElement("svg:marker"); + rmarker->setAttribute("id", isStart ? "Arrow2Sstart" : "Arrow2Send"); + rmarker->setAttribute("inkscape:isstock", "true"); + rmarker->setAttribute("inkscape:stockid", isStart ? "Arrow2Sstart" : "Arrow2Send"); + rmarker->setAttribute("orient", "auto"); + rmarker->setAttribute("refX", "0.0"); + rmarker->setAttribute("refY", "0.0"); + rmarker->setAttribute("style", "overflow:visible;"); + SPItem *marker = SP_ITEM(defs->appendChildRepr(rmarker)); + Inkscape::GC::release(rmarker); + marker->updateRepr(); + Inkscape::XML::Node *rpath; + rpath = xml_doc->createElement("svg:path"); + rpath->setAttribute("d", "M 8.72,4.03 L -2.21,0.02 L 8.72,-4.00 C 6.97,-1.63 6.98,1.62 8.72,4.03 z"); + rpath->setAttribute("id", isStart ? "Arrow2SstartPath" : "Arrow2SendPath"); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property (css, "stroke", "none"); + sp_repr_css_set_property (css, "fill", "#000000"); + sp_repr_css_set_property (css, "fill-opacity", "1"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rpath->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + rpath->setAttribute("transform", isStart ? "scale(0.3) translate(-2.3,0)" : "scale(0.3) rotate(180) translate(-2.3,0)"); + SPItem *path = SP_ITEM(marker->appendChildRepr(rpath)); + Inkscape::GC::release(rpath); + path->updateRepr(); +} + +void MeasureTool::toGuides() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = desktop->getDocument(); + Geom::Point start = desktop->doc2dt(start_p) * desktop->doc2dt(); + Geom::Point end = desktop->doc2dt(end_p) * desktop->doc2dt(); + Geom::Ray ray(start,end); + SPNamedView *namedview = desktop->namedview; + if(!namedview) { + return; + } + setGuide(start,ray.angle(), _("Measure")); + if(explicit_base) { + explicit_base = *explicit_base * SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + ray.setPoints(start, *explicit_base); + if(ray.angle() != 0) { + setGuide(start,ray.angle(), _("Base")); + } + } + setGuide(start,0,""); + setGuide(start,Geom::rad_from_deg(90),_("Start")); + setGuide(end,0,_("End")); + setGuide(end,Geom::rad_from_deg(90),""); + showCanvasItems(true); + doc->ensureUpToDate(); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Add guides from measure tool")); +} + +void MeasureTool::toPhantom() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = desktop->getDocument(); + for (auto & measure_phantom_item : measure_phantom_items) { + sp_canvas_item_destroy(measure_phantom_item); + } + measure_phantom_items.clear(); + for (auto & measure_tmp_item : measure_tmp_items) { + sp_canvas_item_destroy(measure_tmp_item); + } + measure_tmp_items.clear(); + showCanvasItems(false, false, true); + doc->ensureUpToDate(); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Keep last measure on the canvas, for reference")); +} + +void MeasureTool::toItem() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = desktop->getDocument(); + Geom::Ray ray(start_p,end_p); + guint32 line_color_primary = 0x0000ff7f; + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g"); + showCanvasItems(false, true, false, rgroup); + setLine(start_p,end_p, false, line_color_primary, rgroup); + SPItem *measure_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(rgroup)); + Inkscape::GC::release(rgroup); + measure_item->updateRepr(); + doc->ensureUpToDate(); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Convert measure to items")); + reset(); +} + +void MeasureTool::toMarkDimension() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + SPDocument *doc = desktop->getDocument(); + setMarkers(); + Geom::Ray ray(start_p,end_p); + Geom::Point start = start_p + Geom::Point::polar(ray.angle(), 5); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + dimension_offset = prefs->getDouble("/tools/measure/offset", 5.0); + start = start + Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset); + Geom::Point end = end_p + Geom::Point::polar(ray.angle(), -5); + end = end+ Geom::Point::polar(ray.angle() + Geom::rad_from_deg(90), -dimension_offset); + guint32 color = 0x000000ff; + setLine(start, end, true, color); + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + int precision = prefs->getInt("/tools/measure/precision", 2); + std::stringstream precision_str; + precision_str.imbue(std::locale::classic()); + precision_str << "%." << precision << "f %s"; + Geom::Point middle = Geom::middle_point(start, end); + double totallengthval = (end_p - start_p).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + gchar *totallength_str = g_strdup_printf(precision_str.str().c_str(), totallengthval * scale, unit_name.c_str()); + double textangle = Geom::rad_from_deg(180) - ray.angle(); + if (desktop->is_yaxisdown()) { + textangle = ray.angle() - Geom::rad_from_deg(180); + } + setLabelText(totallength_str, middle, fontsize, textangle, color); + g_free(totallength_str); + doc->ensureUpToDate(); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MEASURE,_("Add global measure line")); +} + +void MeasureTool::setGuide(Geom::Point origin, double angle, const char *label) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPDocument *doc = desktop->getDocument(); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + SPRoot const *root = doc->getRoot(); + Geom::Affine affine(Geom::identity()); + if(root) { + affine *= root->c2p.inverse(); + } + SPNamedView *namedview = desktop->namedview; + if(!namedview) { + return; + } + + // <sodipodi:guide> stores inverted y-axis coordinates + if (desktop->is_yaxisdown()) { + origin[Geom::Y] = doc->getHeight().value("px") - origin[Geom::Y]; + angle *= -1.0; + } + + origin *= affine; + //measure angle + Inkscape::XML::Node *guide; + guide = xml_doc->createElement("sodipodi:guide"); + std::stringstream position; + position.imbue(std::locale::classic()); + position << origin[Geom::X] << "," << origin[Geom::Y]; + guide->setAttribute("position", position.str() ); + guide->setAttribute("inkscape:color", "rgb(167,0,255)"); + guide->setAttribute("inkscape:label", label); + Geom::Point unit_vector = Geom::rot90(origin.polar(angle)); + std::stringstream angle_str; + angle_str.imbue(std::locale::classic()); + angle_str << unit_vector[Geom::X] << "," << unit_vector[Geom::Y]; + guide->setAttribute("orientation", angle_str.str()); + namedview->appendChild(guide); + Inkscape::GC::release(guide); +} + +void MeasureTool::setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, Inkscape::XML::Node *measure_repr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite()) { + return; + } + Geom::PathVector pathv; + Geom::Path path; + path.start(desktop->doc2dt(start_point)); + path.appendNew<Geom::LineSegment>(desktop->doc2dt(end_point)); + pathv.push_back(path); + pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + if(!pathv.empty()) { + setMeasureItem(pathv, false, markers, color, measure_repr); + } +} + +void MeasureTool::setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !origin.isFinite()) { + return; + } + char const * svgd; + svgd = "m 0.707,0.707 6.586,6.586 m 0,-6.586 -6.586,6.586"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + Geom::Scale scale = Geom::Scale(desktop->current_zoom()).inverse(); + pathv *= Geom::Translate(Geom::Point(-3.5,-3.5)); + pathv *= scale; + pathv *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5)); + pathv *= Geom::Translate(desktop->doc2dt(origin)); + pathv *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + if (!pathv.empty()) { + guint32 line_color_secondary = 0xff0000ff; + setMeasureItem(pathv, false, false, line_color_secondary, measure_repr); + } +} + +void MeasureTool::setLabelText(const char *value, Geom::Point pos, double fontsize, Geom::Coord angle, guint32 background, Inkscape::XML::Node *measure_repr, CanvasTextAnchorPositionEnum text_anchor) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + /* Create <text> */ + pos = desktop->doc2dt(pos); + Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); + + + /* Set style */ + sp_desktop_apply_style_tool(desktop, rtext, "/tools/text", true); + if(measure_repr) { + sp_repr_set_svg_double(rtext, "x", 2); + sp_repr_set_svg_double(rtext, "y", 2); + } else { + sp_repr_set_svg_double(rtext, "x", 0); + sp_repr_set_svg_double(rtext, "y", 0); + } + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); + SPCSSAttr *css = sp_repr_css_attr_new(); + std::stringstream font_size; + font_size.imbue(std::locale::classic()); + if(measure_repr) { + font_size << fontsize; + } else { + font_size << fontsize << "pt"; + } + sp_repr_css_set_property (css, "font-size", font_size.str().c_str()); + sp_repr_css_set_property (css, "font-style", "normal"); + sp_repr_css_set_property (css, "font-weight", "normal"); + sp_repr_css_set_property (css, "line-height", "125%"); + sp_repr_css_set_property (css, "letter-spacing", "0"); + sp_repr_css_set_property (css, "word-spacing", "0"); + sp_repr_css_set_property (css, "text-align", "center"); + sp_repr_css_set_property (css, "text-anchor", "middle"); + if(measure_repr) { + sp_repr_css_set_property (css, "fill", "#FFFFFF"); + } else { + sp_repr_css_set_property (css, "fill", "#000000"); + } + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_repr_css_set_property (css, "stroke", "none"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rtspan->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + /* Create TEXT */ + Inkscape::XML::Node *rstring = xml_doc->createTextNode(value); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + SPItem *text_item = SP_ITEM(desktop->currentLayer()->appendChildRepr(rtext)); + Inkscape::GC::release(rtext); + text_item->updateRepr(); + Geom::OptRect bbox = text_item->geometricBounds(); + if (!measure_repr && bbox) { + Geom::Point center = bbox->midpoint(); + text_item->transform *= Geom::Translate(center).inverse(); + pos += Geom::Point::polar(angle+ Geom::rad_from_deg(90), -bbox->height()); + } + if(measure_repr) { + /* Create <group> */ + Inkscape::XML::Node *rgroup = xml_doc->createElement("svg:g"); + /* Create <rect> */ + Inkscape::XML::Node *rrect = xml_doc->createElement("svg:rect"); + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar color_line[64]; + sp_svg_write_color (color_line, sizeof(color_line), background); + sp_repr_css_set_property (css, "fill", color_line); + sp_repr_css_set_property (css, "fill-opacity", "0.5"); + sp_repr_css_set_property (css, "stroke-width", "0"); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rrect->setAttribute("style", css_str); + sp_repr_css_attr_unref (css); + sp_repr_set_svg_double(rgroup, "x", 0); + sp_repr_set_svg_double(rgroup, "y", 0); + sp_repr_set_svg_double(rrect, "x", -bbox->width()/2.0); + sp_repr_set_svg_double(rrect, "y", -bbox->height()); + sp_repr_set_svg_double(rrect, "width", bbox->width() + 6); + sp_repr_set_svg_double(rrect, "height", bbox->height() + 6); + Inkscape::XML::Node *rtextitem = text_item->getRepr(); + text_item->deleteObject(); + rgroup->addChild(rtextitem, nullptr); + Inkscape::GC::release(rtextitem); + rgroup->addChild(rrect, nullptr); + Inkscape::GC::release(rrect); + SPItem *text_item_box = SP_ITEM(desktop->currentLayer()->appendChildRepr(rgroup)); + Geom::Scale scale = Geom::Scale(desktop->current_zoom()).inverse(); + if(bbox && text_anchor == TEXT_ANCHOR_CENTER) { + text_item_box->transform *= Geom::Translate(bbox->midpoint() - Geom::Point(1.0,1.0)).inverse(); + } + text_item_box->transform *= scale; + text_item_box->transform *= Geom::Translate(Geom::Point() - (scale.vector() * 0.5)); + text_item_box->transform *= Geom::Translate(pos); + text_item_box->transform *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + text_item_box->updateRepr(); + text_item_box->doWriteTransform(text_item_box->transform, nullptr, true); + Inkscape::XML::Node *rlabel = text_item_box->getRepr(); + text_item_box->deleteObject(); + measure_repr->addChild(rlabel, nullptr); + Inkscape::GC::release(rlabel); + } else { + text_item->transform *= Geom::Rotate(angle); + text_item->transform *= Geom::Translate(pos); + text_item->transform *= SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + text_item->doWriteTransform(text_item->transform, nullptr, true); + } +} + +void MeasureTool::reset() +{ + this->knot_start->hide(); + this->knot_end->hide(); + for (auto & measure_tmp_item : measure_tmp_items) { + sp_canvas_item_destroy(measure_tmp_item); + } + measure_tmp_items.clear(); +} + +void MeasureTool::setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, Glib::ustring unit_name, Geom::Point position, guint32 background, CanvasTextAnchorPositionEnum text_anchor, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + std::stringstream precision_str; + precision_str.imbue(std::locale::classic()); + if(is_angle){ + precision_str << "%." << precision << "f °"; + } else { + precision_str << "%." << precision << "f %s"; + } + gchar *measure_str = g_strdup_printf(precision_str.str().c_str(), amount, unit_name.c_str()); + SPCanvasText *canvas_tooltip = sp_canvastext_new(desktop->getTempGroup(), + desktop, + position, + measure_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->rgba_background = background; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = text_anchor; + if(to_phantom){ + canvas_tooltip->rgba_background = 0x4444447f; + measure_phantom_items.push_back(SP_CANVAS_ITEM(canvas_tooltip)); + sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip)); + } else { + measure_tmp_items.push_back(SP_CANVAS_ITEM(canvas_tooltip)); + sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip)); + } + + if(to_item) { + setLabelText(measure_str, position, fontsize, 0, background, measure_repr); + } + g_free(measure_str); +} + +void MeasureTool::setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + guint32 color = 0xff0000ff; + if(to_phantom){ + color = 0x888888ff; + } + SPCanvasItem * canvasitem = sp_canvas_item_new(desktop->getTempGroup(), + SP_TYPE_CTRL, + "anchor", SP_ANCHOR_CENTER, + "size", 9, + "stroked", TRUE, + "stroke_color", color, + "mode", SP_KNOT_MODE_XOR, + "shape", SP_KNOT_SHAPE_CROSS, + NULL ); + + SP_CTRL(canvasitem)->moveto(position); + if(to_phantom){ + measure_phantom_items.push_back(canvasitem); + } else { + measure_tmp_items.push_back(canvasitem); + } + sp_canvas_item_show(canvasitem); + sp_canvas_item_move_to_z(canvasitem, 0); + + if(to_item) { + setPoint(position, measure_repr); + } +} + +void MeasureTool::setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, Inkscape::CtrlLineType ctrl_line_type, Inkscape::XML::Node *measure_repr){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + gint32 color = ctrl_line_type == CTLINE_PRIMARY ? 0x0000ff7f : 0xff00007f; + if(to_phantom){ + color = ctrl_line_type == CTLINE_PRIMARY ? 0x4444447f : 0x8888887f; + } + SPCtrlLine *control_line = ControlManager::getManager().createControlLine(desktop->getTempGroup(), + start, + end, + ctrl_line_type); + control_line->rgba = color; + if(to_phantom){ + measure_phantom_items.push_back(SP_CANVAS_ITEM(control_line)); + } else { + measure_tmp_items.push_back(SP_CANVAS_ITEM(control_line)); + } + sp_canvas_item_move_to_z(SP_CANVAS_ITEM(control_line), 0); + sp_canvas_item_show(SP_CANVAS_ITEM(control_line)); + if(to_item) { + setLine(start, + end, + false, + color, + measure_repr); + } +} + +void MeasureTool::showItemInfoText(Geom::Point pos, gchar *measure_str, double fontsize) +{ + SPCanvasText *canvas_tooltip = sp_canvastext_new(desktop->getTempGroup(), + desktop, + pos, + measure_str); + sp_canvastext_set_fontsize(canvas_tooltip, fontsize); + canvas_tooltip->rgba = 0xffffffff; + canvas_tooltip->outline = false; + canvas_tooltip->background = true; + canvas_tooltip->anchor_position = TEXT_ANCHOR_LEFT; + canvas_tooltip->rgba_background = 0x00000099; + measure_item.push_back(SP_CANVAS_ITEM(canvas_tooltip)); + sp_canvas_item_show(SP_CANVAS_ITEM(canvas_tooltip)); +} + +void MeasureTool::showInfoBox(Geom::Point cursor, bool into_groups) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Util::Unit const * unit = desktop->getNamedView()->getDisplayUnit(); + for (auto & idx : measure_item) { + sp_canvas_item_destroy(idx); + } + measure_item.clear(); + + SPItem *newover = desktop->getItemAtPoint(cursor, into_groups); + if (newover) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + int precision = prefs->getInt("/tools/measure/precision", 2); + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + bool only_selected = prefs->getBool("/tools/measure/only_selected", false); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + Geom::Scale zoom = Geom::Scale(Inkscape::Util::Quantity::convert(desktop->current_zoom(), "px", unit->abbr)).inverse(); + if(newover != over){ + over = newover; + Preferences *prefs = Preferences::get(); + int prefs_bbox = prefs->getBool("/tools/bounding_box", false); + SPItem::BBoxType bbox_type = !prefs_bbox ? SPItem::VISUAL_BBOX : SPItem::GEOMETRIC_BBOX; + Geom::OptRect bbox = over->bounds(bbox_type); + if (bbox) { + + item_width = Inkscape::Util::Quantity::convert((*bbox).width() * scale, unit->abbr, unit_name); + item_height = Inkscape::Util::Quantity::convert((*bbox).height() * scale, unit->abbr, unit_name); + item_x = Inkscape::Util::Quantity::convert((*bbox).left(), unit->abbr, unit_name); + Geom::Point y_point(0,Inkscape::Util::Quantity::convert((*bbox).bottom() * scale, unit->abbr, "px")); + y_point *= desktop->doc2dt(); + item_y = Inkscape::Util::Quantity::convert(y_point[Geom::Y] * scale, "px", unit_name); + if (SP_IS_SHAPE(over)) { + Geom::PathVector shape = SP_SHAPE(over)->getCurve(true)->get_pathvector(); + item_length = Geom::length(paths_to_pw(shape)); + item_length = Inkscape::Util::Quantity::convert(item_length * scale, unit->abbr, unit_name); + } + } + } + gchar *measure_str = nullptr; + std::stringstream precision_str; + precision_str.imbue(std::locale::classic()); + double origin = Inkscape::Util::Quantity::convert(14, "px", unit->abbr); + Geom::Point rel_position = Geom::Point(origin, origin); + Geom::Point pos = desktop->w2d(cursor); + double gap = Inkscape::Util::Quantity::convert(7 + fontsize, "px", unit->abbr); + if (only_selected) { + if (desktop->getSelection()->includes(over)) { + showItemInfoText(pos + (rel_position * zoom),_("Selected"),fontsize); + } else { + showItemInfoText(pos + (rel_position * zoom),_("Not selected"),fontsize); + } + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + } + if (SP_IS_SHAPE(over)) { + precision_str << _("Length") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_length, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + } else if (SP_IS_GROUP(over)) { + measure_str = _("Press 'CTRL' to measure into group"); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + } + precision_str << "Y: %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_y, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << "X: %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_x, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << _("Height") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_height, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + rel_position = Geom::Point(rel_position[Geom::X], rel_position[Geom::Y] + gap); + + precision_str << _("Width") << ": %." << precision << "f %s"; + measure_str = g_strdup_printf(precision_str.str().c_str(), item_width, unit_name.c_str()); + precision_str.str(""); + showItemInfoText(pos + (rel_position * zoom),measure_str,fontsize); + g_free(measure_str); + } +} + +void MeasureTool::showCanvasItems(bool to_guides, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(!desktop || !start_p.isFinite() || !end_p.isFinite() || start_p == end_p) { + return; + } + writeMeasurePoint(start_p, true); + writeMeasurePoint(end_p, false); + //clear previous canvas items, we'll draw new ones + for (auto & measure_tmp_item : measure_tmp_items) { + sp_canvas_item_destroy(measure_tmp_item); + } + measure_tmp_items.clear(); + //TODO:Calculate the measure area for current length and origin + // and use canvas->requestRedraw. In the calculation need a gap for outside text + // maybe this remove the trash lines on measure use + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool show_in_between = prefs->getBool("/tools/measure/show_in_between", true); + bool all_layers = prefs->getBool("/tools/measure/all_layers", true); + dimension_offset = 70; + Geom::PathVector lineseg; + Geom::Path p; + Geom::Point start_p_doc = start_p * desktop->dt2doc(); + Geom::Point end_p_doc = end_p * desktop->dt2doc(); + p.start(start_p_doc); + p.appendNew<Geom::LineSegment>(end_p_doc); + lineseg.push_back(p); + + double angle = atan2(end_p - start_p); + double baseAngle = 0; + + if (explicit_base) { + baseAngle = atan2(explicit_base.get() - start_p); + angle -= baseAngle; + } + + std::vector<SPItem*> items; + SPDocument *doc = desktop->getDocument(); + Geom::Rect rect(start_p_doc, end_p_doc); + items = doc->getItemsPartiallyInBox(desktop->dkey, rect, false, true, false, true); + Inkscape::LayerModel *layer_model = nullptr; + SPObject *current_layer = nullptr; + if(desktop){ + layer_model = desktop->layers; + current_layer = desktop->currentLayer(); + } + std::vector<double> intersection_times; + bool only_selected = prefs->getBool("/tools/measure/only_selected", false); + for (auto i : items) { + SPItem *item = i; + if (!desktop->getSelection()->includes(i) && only_selected) { + continue; + } + if(all_layers || (layer_model && layer_model->layerForObject(item) == current_layer)){ + if (SP_IS_SHAPE(item)) { + calculate_intersections(desktop, item, lineseg, SP_SHAPE(item)->getCurve(), intersection_times); + } else { + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) { + break; + } + + // get path from iter to iter_next: + SPCurve *curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { + continue; // error converting this glyph + } + if (curve->is_empty()) { // whitespace glyph? + curve->unref(); + continue; + } + + calculate_intersections(desktop, item, lineseg, curve, intersection_times); + if (iter == te_get_layout(item)->end()) { + break; + } + } while (true); + } + } + } + } + Glib::ustring unit_name = prefs->getString("/tools/measure/unit"); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + // Normal will be used for lines and text + Geom::Point windowNormal = Geom::unit_vector(Geom::rot90(desktop->d2w(end_p - start_p))); + Geom::Point normal = desktop->w2d(windowNormal); + + std::vector<Geom::Point> intersections; + std::sort(intersection_times.begin(), intersection_times.end()); + for (double & intersection_time : intersection_times) { + intersections.push_back(lineseg[0].pointAt(intersection_time)); + } + + if(!show_in_between && intersection_times.size() > 1) { + Geom::Point start = lineseg[0].pointAt(intersection_times[0]); + Geom::Point end = lineseg[0].pointAt(intersection_times[intersection_times.size()-1]); + intersections.clear(); + intersections.push_back(start); + intersections.push_back(end); + } + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) { + intersections.insert(intersections.begin(),lineseg[0].pointAt(0)); + intersections.push_back(lineseg[0].pointAt(1)); + } + std::vector<LabelPlacement> placements; + for (size_t idx = 1; idx < intersections.size(); ++idx) { + LabelPlacement placement; + placement.lengthVal = (intersections[idx] - intersections[idx - 1]).length(); + placement.lengthVal = Inkscape::Util::Quantity::convert(placement.lengthVal, "px", unit_name); + placement.offset = dimension_offset / 2; + placement.start = desktop->doc2dt( (intersections[idx - 1] + intersections[idx]) / 2 ); + placement.end = placement.start - (normal * placement.offset); + + placements.push_back(placement); + } + int precision = prefs->getInt("/tools/measure/precision", 2); + // Adjust positions + repositionOverlappingLabels(placements, desktop, windowNormal, fontsize, precision); + for (auto & place : placements) { + setMeasureCanvasText(false, precision, place.lengthVal * scale, fontsize, unit_name, place.end, 0x0000007f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr); + } + Geom::Point angleDisplayPt = calcAngleDisplayAnchor(desktop, angle, baseAngle, + start_p, end_p, + fontsize); + { + setMeasureCanvasText(true, precision, Geom::deg_from_rad(angle), fontsize, unit_name, angleDisplayPt, 0x337f337f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr); + } + + { + double totallengthval = (end_p - start_p).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + Geom::Point origin = end_p + desktop->w2d(Geom::Point(3*fontsize, -fontsize)); + setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x3333337f, TEXT_ANCHOR_LEFT, to_item, to_phantom, measure_repr); + } + + if (intersections.size() > 2) { + double totallengthval = (intersections[intersections.size()-1] - intersections[0]).length(); + totallengthval = Inkscape::Util::Quantity::convert(totallengthval, "px", unit_name); + Geom::Point origin = desktop->doc2dt((intersections[0] + intersections[intersections.size()-1])/2) + normal * dimension_offset; + setMeasureCanvasText(false, precision, totallengthval * scale, fontsize, unit_name, origin, 0x33337f7f, TEXT_ANCHOR_CENTER, to_item, to_phantom, measure_repr); + } + + // Initial point + { + setMeasureCanvasItem(start_p, false, to_phantom, measure_repr); + } + + // Now that text has been added, we can add lines and controls so that they go underneath + for (size_t idx = 0; idx < intersections.size(); ++idx) { + setMeasureCanvasItem(desktop->doc2dt(intersections[idx]), to_item, to_phantom, measure_repr); + if(to_guides) { + gchar *cross_number; + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true)) { + cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx)); + } else { + cross_number= g_strdup_printf(_("Crossing %lu"), static_cast<unsigned long>(idx + 1)); + } + if (!prefs->getBool("/tools/measure/ignore_1st_and_last", true) && idx == 0) { + setGuide(desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), ""); + } else { + setGuide(desktop->doc2dt(intersections[idx]), angle + Geom::rad_from_deg(90), cross_number); + } + g_free(cross_number); + } + } + // Since adding goes to the bottom, do all lines last. + + // draw main control line + { + setMeasureCanvasControlLine(start_p, end_p, false, to_phantom, CTLINE_PRIMARY, measure_repr); + double length = std::abs((end_p - start_p).length()); + Geom::Point anchorEnd = start_p; + anchorEnd[Geom::X] += length; + if (explicit_base) { + anchorEnd *= (Geom::Affine(Geom::Translate(-start_p)) + * Geom::Affine(Geom::Rotate(baseAngle)) + * Geom::Affine(Geom::Translate(start_p))); + } + setMeasureCanvasControlLine(start_p, anchorEnd, to_item, to_phantom, CTLINE_SECONDARY, measure_repr); + createAngleDisplayCurve(desktop, start_p, end_p, angleDisplayPt, angle, to_phantom, measure_phantom_items, measure_tmp_items, measure_repr); + } + + if (intersections.size() > 2) { + setMeasureCanvasControlLine(desktop->doc2dt(intersections[0]) + normal * dimension_offset, desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr); + + setMeasureCanvasControlLine(desktop->doc2dt(intersections[0]), desktop->doc2dt(intersections[0]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr); + + setMeasureCanvasControlLine(desktop->doc2dt(intersections[intersections.size() - 1]), desktop->doc2dt(intersections[intersections.size() - 1]) + normal * dimension_offset, to_item, to_phantom, CTLINE_PRIMARY , measure_repr); + } + + // call-out lines + for (auto & place : placements) { + setMeasureCanvasControlLine(place.start, place.end, to_item, to_phantom, CTLINE_SECONDARY, measure_repr); + } + + { + for (size_t idx = 1; idx < intersections.size(); ++idx) { + Geom::Point measure_text_pos = (intersections[idx - 1] + intersections[idx]) / 2; + setMeasureCanvasControlLine(desktop->doc2dt(measure_text_pos), desktop->doc2dt(measure_text_pos) - (normal * dimension_offset / 2), to_item, to_phantom, CTLINE_SECONDARY, measure_repr); + } + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/measure-tool.h b/src/ui/tools/measure-tool.h new file mode 100644 index 0000000..a6ca39e --- /dev/null +++ b/src/ui/tools/measure-tool.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MEASURING_CONTEXT_H +#define SEEN_SP_MEASURING_CONTEXT_H + +/* + * Our fine measuring tool + * + * Authors: + * Felipe Correa da Silva Sanches <juca@members.fsf.org> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * Copyright (C) 2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include "display/canvas-text.h" +#include "display/canvas-temporary-item.h" +#include "ui/control-manager.h" +#include <boost/optional.hpp> + +#define SP_MEASURE_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeasureTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_MEASURE_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeasureTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +class SPKnot; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class MeasureTool : public ToolBase { +public: + MeasureTool(); + ~MeasureTool() override; + + static const std::string prefsPath; + + void finish() override; + bool root_handler(GdkEvent* event) override; + virtual void showCanvasItems(bool to_guides = false, bool to_item = false, bool to_phantom = false, Inkscape::XML::Node *measure_repr = nullptr); + virtual void reverseKnots(); + virtual void toGuides(); + virtual void toPhantom(); + virtual void toMarkDimension(); + virtual void toItem(); + virtual void reset(); + virtual void setMarkers(); + virtual void setMarker(bool isStart); + const std::string& getPrefsPath() override; + Geom::Point readMeasurePoint(bool is_start); + void showInfoBox(Geom::Point cursor, bool into_groups); + void showItemInfoText(Geom::Point pos, gchar *measure_str, double fontsize); + void writeMeasurePoint(Geom::Point point, bool is_start); + void setGuide(Geom::Point origin, double angle, const char *label); + void setPoint(Geom::Point origin, Inkscape::XML::Node *measure_repr); + void setLine(Geom::Point start_point,Geom::Point end_point, bool markers, guint32 color, Inkscape::XML::Node *measure_repr = nullptr); + void setMeasureCanvasText(bool is_angle, double precision, double amount, double fontsize, Glib::ustring unit_name, Geom::Point position, guint32 background, CanvasTextAnchorPositionEnum text_anchor, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr); + void setMeasureCanvasItem(Geom::Point position, bool to_item, bool to_phantom, Inkscape::XML::Node *measure_repr); + void setMeasureCanvasControlLine(Geom::Point start, Geom::Point end, bool to_item, bool to_phantom, Inkscape::CtrlLineType ctrl_line_type, Inkscape::XML::Node *measure_repr); + void setLabelText(const char *value, Geom::Point pos, double fontsize, Geom::Coord angle, guint32 background , Inkscape::XML::Node *measure_repr = nullptr, CanvasTextAnchorPositionEnum text_anchor = TEXT_ANCHOR_CENTER ); + void knotStartMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state); + void knotEndMovedHandler(SPKnot */*knot*/, Geom::Point const &ppointer, guint state); + void knotClickHandler(SPKnot *knot, guint state); + void knotUngrabbedHandler(SPKnot */*knot*/, unsigned int /*state*/); +private: + SPCanvasItem* grabbed; + boost::optional<Geom::Point> explicit_base; + boost::optional<Geom::Point> last_end; + SPKnot *knot_start; + SPKnot *knot_end; + gint dimension_offset; + Geom::Point start_p; + Geom::Point end_p; + Geom::Point last_pos; + std::vector<SPCanvasItem *> measure_tmp_items; + std::vector<SPCanvasItem *> measure_phantom_items; + std::vector<SPCanvasItem *> measure_item; + double item_width; + double item_height; + double item_x; + double item_y; + double item_length; + SPItem *over; + sigc::connection _knot_start_moved_connection; + sigc::connection _knot_start_ungrabbed_connection; + sigc::connection _knot_start_click_connection; + sigc::connection _knot_end_moved_connection; + sigc::connection _knot_end_click_connection; + sigc::connection _knot_end_ungrabbed_connection; +}; + +} +} +} + +#endif // SEEN_SP_MEASURING_CONTEXT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/mesh-tool.cpp b/src/ui/tools/mesh-tool.cpp new file mode 100644 index 0000000..e7f2444 --- /dev/null +++ b/src/ui/tools/mesh-tool.cpp @@ -0,0 +1,1098 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Mesh drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Abhishek Sharma + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +//#define DEBUG_MESH + + +// Libraries +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +// General +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "gradient-drag.h" +#include "gradient-chemistry.h" +#include "include/macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection.h" +#include "snap.h" +#include "verbs.h" + +#include "display/sp-ctrlcurve.h" +#include "display/curve.h" + +#include "object/sp-defs.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-namedview.h" +#include "object/sp-text.h" +#include "style.h" + +#include "ui/pixmaps/cursor-gradient.xpm" +#include "ui/pixmaps/cursor-gradient-add.xpm" + +#include "ui/control-manager.h" +#include "ui/tools/mesh-tool.h" + + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_mesh_new_default(MeshTool &rc); + +const std::string& MeshTool::getPrefsPath() { + return MeshTool::prefsPath; +} + +const std::string MeshTool::prefsPath = "/tools/mesh"; + +// TODO: The gradient tool class looks like a 1:1 copy. + +MeshTool::MeshTool() + : ToolBase(cursor_gradient_xpm) +// TODO: Why are these connections stored as pointers? + , selcon(nullptr) + , subselcon(nullptr) + , cursor_addnode(false) + , node_added(false) + , show_handles(true) + , edit_fill(true) + , edit_stroke(true) +{ + // TODO: This value is overwritten in the root handler + this->tolerance = 6; +} + +MeshTool::~MeshTool() { + this->enableGrDrag(false); + + this->selcon->disconnect(); + delete this->selcon; + + this->subselcon->disconnect(); + delete this->subselcon; +} + +// This must match GrPointType enum sp-gradient.h +// We should move this to a shared header (can't simply move to gradient.h since that would require +// including <glibmm/i18n.h> which messes up "N_" in extensions... argh!). +const gchar *ms_handle_descr [] = { + N_("Linear gradient <b>start</b>"), //POINT_LG_BEGIN + N_("Linear gradient <b>end</b>"), + N_("Linear gradient <b>mid stop</b>"), + N_("Radial gradient <b>center</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>radius</b>"), + N_("Radial gradient <b>focus</b>"), // POINT_RG_FOCUS + N_("Radial gradient <b>mid stop</b>"), + N_("Radial gradient <b>mid stop</b>"), + N_("Mesh gradient <b>corner</b>"), + N_("Mesh gradient <b>handle</b>"), + N_("Mesh gradient <b>tensor</b>") +}; + +void MeshTool::selection_changed(Inkscape::Selection* /*sel*/) { + GrDrag *drag = this->_grdrag; + Inkscape::Selection *selection = this->desktop->getSelection(); + + if (selection == nullptr) { + return; + } + + guint n_obj = (guint) boost::distance(selection->items()); + + if (!drag->isNonEmpty() || selection->isEmpty()) { + return; + } + + guint n_tot = drag->numDraggers(); + guint n_sel = drag->numSelected(); + + //The use of ngettext in the following code is intentional even if the English singular form would never be used + if (n_sel == 1) { + if (drag->singleSelectedDraggerNumDraggables() == 1) { + gchar * message = g_strconcat( + //TRANSLATORS: %s will be substituted with the point name (see previous messages); This is part of a compound message + _("%s selected"), + //TRANSLATORS: Mind the space in front. This is part of a compound message + ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + this->message_context->setF(Inkscape::NORMAL_MESSAGE, + message,_(ms_handle_descr[drag->singleSelectedDraggerSingleDraggableType()]), n_tot, n_obj); + } else { + gchar * message = + g_strconcat( + //TRANSLATORS: This is a part of a compound message (out of two more indicating: grandint handle count & object count) + ngettext("One handle merging %d stop (drag with <b>Shift</b> to separate) selected", + "One handle merging %d stops (drag with <b>Shift</b> to separate) selected", + drag->singleSelectedDraggerNumDraggables()), + ngettext(" out of %d mesh handle"," out of %d mesh handles",n_tot), + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + this->message_context->setF(Inkscape::NORMAL_MESSAGE,message,drag->singleSelectedDraggerNumDraggables(), n_tot, n_obj); + } + } else if (n_sel > 1) { + //TRANSLATORS: The plural refers to number of selected mesh handles. This is part of a compound message (part two indicates selected object count) + gchar * message = + g_strconcat(ngettext("<b>%d</b> mesh handle selected out of %d","<b>%d</b> mesh handles selected out of %d",n_sel), + //TRANSLATORS: Mind the space in front. (Refers to gradient handles selected). This is part of a compound message + ngettext(" on %d selected object"," on %d selected objects",n_obj),NULL); + this->message_context->setF(Inkscape::NORMAL_MESSAGE,message, n_sel, n_tot, n_obj); + } else if (n_sel == 0) { + this->message_context->setF(Inkscape::NORMAL_MESSAGE, + //TRANSLATORS: The plural refers to number of selected objects + ngettext("<b>No</b> mesh handles selected out of %d on %d selected object", + "<b>No</b> mesh handles selected out of %d on %d selected objects",n_obj), n_tot, n_obj); + } + + // FIXME + // We need to update mesh gradient handles. + // Get gradient this drag belongs too.. +} + +void MeshTool::setup() { + ToolBase::setup(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/mesh/selcue", true)) { + this->enableSelectionCue(); + } + + this->enableGrDrag(); + Inkscape::Selection *selection = this->desktop->getSelection(); + + this->selcon = new sigc::connection(selection->connectChanged( + sigc::mem_fun(this, &MeshTool::selection_changed) + )); + + this->subselcon = new sigc::connection(this->desktop->connectToolSubselectionChanged( + sigc::hide(sigc::bind( + sigc::mem_fun(*this, &MeshTool::selection_changed), + (Inkscape::Selection*)nullptr) + ) + )); + + sp_event_context_read(this, "show_handles"); + sp_event_context_read(this, "edit_fill"); + sp_event_context_read(this, "edit_stroke"); + + this->selection_changed(selection); + +} + +void MeshTool::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring entry_name = value.getEntryName(); + if (entry_name == "show_handles") { + this->show_handles = value.getBool(true); + } else if (entry_name == "edit_fill") { + this->edit_fill = value.getBool(true); + } else if (entry_name == "edit_stroke") { + this->edit_stroke = value.getBool(true); + } else { + ToolBase::set(value); + } +} + +void +sp_mesh_context_select_next (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_next(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +void +sp_mesh_context_select_prev (ToolBase *event_context) +{ + GrDrag *drag = event_context->_grdrag; + g_assert (drag); + + GrDragger *d = drag->select_prev(); + + event_context->desktop->scroll_to_point(d->point, 1.0); +} + +/** +Returns vector of control lines mouse is over. Returns only first if 'first' is true. +*/ +static std::vector<SPCtrlCurve *> +sp_mesh_context_over_line (MeshTool *rc, Geom::Point event_p, bool first = true) +{ + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + //Translate mouse point into proper coord system + rc->mousepoint_doc = desktop->w2d(event_p); + + double tolerance = (double) SP_EVENT_CONTEXT(rc)->tolerance; + + GrDrag *drag = rc->_grdrag; + + std::vector<SPCtrlCurve *> selected; + + for (std::vector<SPCtrlLine *>::const_iterator l = drag->lines.begin(); l != drag->lines.end(); ++l) { + if (!SP_IS_CTRLCURVE(*l)) continue; + + SPCtrlCurve *curve = SP_CTRLCURVE(*l); + Geom::BezierCurveN<3> b( curve->p0, curve->p1, curve->p2, curve->p3 ); + Geom::Coord coord = b.nearestTime( rc->mousepoint_doc ); // Coord == double + Geom::Point nearest = b( coord ); + + double dist_screen = Geom::L2 (rc->mousepoint_doc - nearest) * desktop->current_zoom(); + if (dist_screen < tolerance) { + selected.push_back(curve); + if (first) { + break; + } + } + } + return selected; +} + + +/** +Split row/column near the mouse point. +*/ +static void sp_mesh_context_split_near_point(MeshTool *rc, SPItem *item, Geom::Point mouse_p, guint32 /*etime*/) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_split_near_point: entrance: " << mouse_p << std::endl; +#endif + + // item is the selected item. mouse_p the location in doc coordinates of where to add the stop + + ToolBase *ec = SP_EVENT_CONTEXT(rc); + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + double tolerance = (double) ec->tolerance; + + ec->get_drag()->addStopNearPoint (item, mouse_p, tolerance/desktop->current_zoom()); + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH, + _("Split mesh row/column")); + + ec->get_drag()->updateDraggers(); +} + +/** +Wrapper for various mesh operations that require a list of selected corner nodes. + */ +void +sp_mesh_context_corner_operation (MeshTool *rc, MeshCornerOperation operation ) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_corner_operation: entrance: " << operation << std::endl; +#endif + + SPDocument *doc = nullptr; + GrDrag *drag = rc->_grdrag; + + std::map<SPMeshGradient*, std::vector<guint> > points; + std::map<SPMeshGradient*, SPItem*> items; + std::map<SPMeshGradient*, Inkscape::PaintTarget> fill_or_stroke; + + // Get list of selected draggers for each mesh. + // For all selected draggers (a dragger may include draggerables from different meshes). + for (auto dragger : drag->selected) { + // For all draggables of dragger (a draggable corresponds to a unique mesh). + for (auto d : dragger->draggables) { + // Only mesh corners + if( d->point_type != POINT_MG_CORNER ) continue; + + // Find the gradient + SPMeshGradient *gradient = SP_MESHGRADIENT( getGradient (d->item, d->fill_or_stroke) ); + + // Collect points together for same gradient + points[gradient].push_back( d->point_i ); + items[gradient] = d->item; + fill_or_stroke[gradient] = d->fill_or_stroke ? Inkscape::FOR_FILL: Inkscape::FOR_STROKE; + } + } + + // Loop over meshes. + for( std::map<SPMeshGradient*, std::vector<guint> >::const_iterator iter = points.begin(); iter != points.end(); ++iter) { + SPMeshGradient *mg = SP_MESHGRADIENT( iter->first ); + if( iter->second.size() > 0 ) { + guint noperation = 0; + switch (operation) { + + case MG_CORNER_SIDE_TOGGLE: + // std::cout << "SIDE_TOGGLE" << std::endl; + noperation += mg->array.side_toggle( iter->second ); + break; + + case MG_CORNER_SIDE_ARC: + // std::cout << "SIDE_ARC" << std::endl; + noperation += mg->array.side_arc( iter->second ); + break; + + case MG_CORNER_TENSOR_TOGGLE: + // std::cout << "TENSOR_TOGGLE" << std::endl; + noperation += mg->array.tensor_toggle( iter->second ); + break; + + case MG_CORNER_COLOR_SMOOTH: + // std::cout << "COLOR_SMOOTH" << std::endl; + noperation += mg->array.color_smooth( iter->second ); + break; + + case MG_CORNER_COLOR_PICK: + // std::cout << "COLOR_PICK" << std::endl; + noperation += mg->array.color_pick( iter->second, items[iter->first] ); + break; + + case MG_CORNER_INSERT: + // std::cout << "INSERT" << std::endl; + noperation += mg->array.insert( iter->second ); + break; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + + if( noperation > 0 ) { + mg->array.write( mg ); + mg->requestModified(SP_OBJECT_MODIFIED_FLAG); + doc = mg->document; + + switch (operation) { + + case MG_CORNER_SIDE_TOGGLE: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh path type.")); + drag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_SIDE_ARC: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Approximated arc for mesh side.")); + drag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_TENSOR_TOGGLE: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Toggled mesh tensors.")); + drag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_COLOR_SMOOTH: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Smoothed mesh corner color.")); + drag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_COLOR_PICK: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Picked mesh corner color.")); + drag->local_change = true; // Don't create new draggers. + break; + + case MG_CORNER_INSERT: + DocumentUndo::done(doc, SP_VERB_CONTEXT_MESH, _("Inserted new row or column.")); + break; + + default: + std::cout << "sp_mesh_corner_operation: unknown operation" << std::endl; + } + } + } + } + + // Not needed. Update is done via gr_drag_sel_modified(). + // drag->updateDraggers(); + +} + + +/** + * Scale mesh to just fit into bbox of selected items. + */ +void +sp_mesh_context_fit_mesh_in_bbox (MeshTool *rc) +{ + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_fit_mesh_in_bbox: entrance: Entrance"<< std::endl; +#endif + + SPDesktop *desktop = SP_EVENT_CONTEXT (rc)->desktop; + + Inkscape::Selection *selection = desktop->getSelection(); + if (selection == nullptr) { + return; + } + + bool changed = false; + auto itemlist = selection->items(); + for (auto i=itemlist.begin(); i!=itemlist.end(); ++i) { + + SPItem *item = *i; + SPStyle *style = item->style; + + if (style) { + + if (style->fill.isPaintserver()) { + SPPaintServer *server = item->style->getFillPaintServer(); + if ( SP_IS_MESHGRADIENT(server) ) { + + Geom::OptRect item_bbox = item->geometricBounds(); + SPMeshGradient *gradient = SP_MESHGRADIENT(server); + if (gradient->array.fill_box( item_bbox )) { + changed = true; + } + } + } + + if (style->stroke.isPaintserver()) { + SPPaintServer *server = item->style->getStrokePaintServer(); + if ( SP_IS_MESHGRADIENT(server) ) { + + Geom::OptRect item_bbox = item->visualBounds(); + SPMeshGradient *gradient = SP_MESHGRADIENT(server); + if (gradient->array.fill_box( item_bbox )) { + changed = true; + } + } + } + + } + } + if (changed) { + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH, + _("Fit mesh inside bounding box.")); + } +} + + +/** +Handles all keyboard and mouse input for meshs. +Note: node/handle events are take care of elsewhere. +*/ +bool MeshTool::root_handler(GdkEvent* event) { + static bool dragging; + + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + // Get value of fill or stroke preference + Inkscape::PaintTarget fill_or_stroke_pref = + static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke")); + + GrDrag *drag = this->_grdrag; + g_assert (drag); + + gint ret = FALSE; + + auto move_handle = [&](int x_dir, int y_dir) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__SHIFT(event)) { + mul *= 10; + } + + y_dir *= -desktop->yaxisdir(); + + if (MOD__ALT(event)) { + drag->selected_move_screen(mul * x_dir, mul * y_dir); + } else { + mul *= nudge; + drag->selected_move(mul * x_dir, mul * y_dir); + } + }; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_2BUTTON_PRESS" << std::endl; +#endif + + // Double click: + // If over a mesh line, divide mesh row/column + // If not over a line and no mesh, create new mesh for top selected object. + + if ( event->button.button == 1 ) { + + // Are we over a mesh line? + std::vector<SPCtrlCurve *> over_line = + sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y)); + + if (!over_line.empty()) { + // We take the first item in selection, because with doubleclick, the first click + // always resets selection to the single object under cursor + sp_mesh_context_split_near_point(this, selection->items().front(), this->mousepoint_doc, event->button.time); + } else { + // Create a new gradient with default coordinates. + + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (!has_mesh) { + sp_mesh_new_default(*this); + } + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_PRESS" << std::endl; +#endif + + // Button down + // If mesh already exists, do rubber band selection. + // Else set origin for drag which will create a new gradient. + if ( event->button.button == 1 && !this->space_panning ) { + + // Are we over a mesh line? + std::vector<SPCtrlCurve *> over_line = + sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y), false); + + if (!over_line.empty()) { + for (auto it : over_line) { + SPItem *item = it->item; + Inkscape::PaintTarget fill_or_stroke = + it->is_fill ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE; + GrDragger* dragger0 = drag->getDraggerFor(item, POINT_MG_CORNER, it->corner0, fill_or_stroke); + GrDragger* dragger1 = drag->getDraggerFor(item, POINT_MG_CORNER, it->corner1, fill_or_stroke); + bool add = (event->button.state & GDK_SHIFT_MASK); + bool toggle = (event->button.state & GDK_CONTROL_MASK); + if ( !add && !toggle ) { + drag->deselectAll(); + } + drag->setSelected( dragger0, true, !toggle ); + drag->setSelected( dragger1, true, !toggle ); + } + ret = true; + break; // To avoid putting the following code in an else block. + } + + Geom::Point button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + dragging = true; + + Geom::Point button_dt = desktop->w2d(button_w); + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (has_mesh) { + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + } + + // remember clicked item, disregarding groups, honoring Alt; do nothing with Crtl to + // enable Ctrl+doubleclick of exactly the selected item(s) + if (!(event->button.state & GDK_CONTROL_MASK)) { + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + } + + if (!selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + + this->origin = button_dt; + + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + // Mouse move + if ( dragging && ( event->motion.state & GDK_BUTTON1_MASK ) && !this->space_panning ) { + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_MOTION_NOTIFY: Dragging" << std::endl; +#endif + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point const motion_dt = this->desktop->w2d(motion_w); + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(motion_dt); + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw around</b> handles to select them")); + } else { + // Do nothing. For a linear/radial gradient we follow the drag, updating the + // gradient as the end node is dragged. For a mesh gradient, the gradient is always + // created to fill the object when the drag ends. + } + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else { + // Not dragging + + // Do snapping + if (!drag->mouseOver() && !selection->isEmpty()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt = this->desktop->w2d(motion_w); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + + // Highlight corner node corresponding to side or tensor node + if( drag->mouseOver() ) { + // MESH FIXME: Light up corresponding corner node corresponding to node we are over. + // See "pathflash" in ui/tools/node-tool.cpp for ideas. + // Use desktop->add_temporary_canvasitem( SPCanvasItem, milliseconds ); + } + + // Change cursor shape if over line + std::vector<SPCtrlCurve *> over_line = + sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y)); + + if (this->cursor_addnode && over_line.empty()) { + this->cursor_shape = cursor_gradient_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = false; + } else if (!this->cursor_addnode && !over_line.empty()) { + this->cursor_shape = cursor_gradient_add_xpm; + this->sp_event_context_update_cursor(); + this->cursor_addnode = true; + } + } + break; + + case GDK_BUTTON_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_BUTTON_RELEASE" << std::endl; +#endif + + this->xp = this->yp = 0; + + if ( event->button.button == 1 && !this->space_panning ) { + + // Check if over line + std::vector<SPCtrlCurve *> over_line = + sp_mesh_context_over_line(this, Geom::Point(event->motion.x, event->motion.y)); + + if ( (event->button.state & GDK_CONTROL_MASK) && (event->button.state & GDK_MOD1_MASK ) ) { + if (!over_line.empty()) { + sp_mesh_context_split_near_point(this, over_line[0]->item, + this->mousepoint_doc, 0); + ret = TRUE; + } + } else { + dragging = false; + + // unless clicked with Ctrl (to enable Ctrl+doubleclick). + if (event->button.state & GDK_CONTROL_MASK) { + ret = TRUE; + break; + } + + if (!this->within_tolerance) { + + // Check if object already has mesh... if it does, + // don't create new mesh with click-drag. + bool has_mesh = false; + if (!selection->isEmpty()) { + SPStyle *style = selection->items().front()->style; + if (style) { + SPPaintServer *server = + (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + style->getFillPaintServer(): + style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + } + + if (!has_mesh) { + sp_mesh_new_default(*this); + } else { + + // we've been dragging, either create a new gradient + // or rubberband-select if we have rubberband + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !this->within_tolerance) { + // this was a rubberband drag + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + if (!(event->button.state & GDK_SHIFT_MASK)) { + drag->deselectAll(); + } + drag->selectRect(*b); + } + } + } + + } else if (this->item_to_select) { + if (!over_line.empty()) { + // Clicked on an existing mesh line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + drag->deselectAll(); + selection->set(this->item_to_select); + } + } + } else { + if (!over_line.empty()) { + // Clicked on an existing mesh line, don't change selection. This stops + // possible change in selection during a double click with overlapping objects + } else { + // click in an empty space; do the same as Esc + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + selection->clear(); + } + } + } + + this->item_to_select = nullptr; + ret = TRUE; + } + + Inkscape::Rubberband::get(desktop)->stop(); + } + break; + + case GDK_KEY_PRESS: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_KEY_PRESS" << std::endl; +#endif + + // FIXME: tip + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + + // sp_event_show_modifier_tip (this->defaultMessageContext(), event, + // _("FIXME<b>Ctrl</b>: snap mesh angle"), + // _("FIXME<b>Shift</b>: draw mesh around the starting point"), + // NULL); + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && drag->isNonEmpty()) { + drag->selectAll(); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!drag->selected.empty()) { + drag->deselectAll(); + } else { + selection->clear(); + } + + ret = TRUE; + //TODO: make dragging escapable by Esc + break; + + case GDK_KEY_Left: // move handle left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(-1, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move handle up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(0, 1); + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move handle right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(1, 0); + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move handle down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (!MOD__CTRL(event)) { // not ctrl + move_handle(0, -1); + ret = TRUE; + } + break; + + // Mesh Operations -------------------------------------------- + + case GDK_KEY_Insert: + case GDK_KEY_KP_Insert: + // with any modifiers: + sp_mesh_context_corner_operation ( this, MG_CORNER_INSERT ); + ret = TRUE; + break; + + case GDK_KEY_i: + case GDK_KEY_I: + if (MOD__SHIFT_ONLY(event)) { + // Shift+I - insert corners (alternate keybinding for keyboards + // that don't have the Insert key) + sp_mesh_context_corner_operation ( this, MG_CORNER_INSERT ); + ret = TRUE; + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if ( !drag->selected.empty() ) { + ret = TRUE; + } + break; + + case GDK_KEY_b: // Toggle mesh side between lineto and curveto. + case GDK_KEY_B: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_SIDE_TOGGLE ); + ret = TRUE; + } + break; + + case GDK_KEY_c: // Convert mesh side from generic Bezier to Bezier approximating arc, + case GDK_KEY_C: // preserving handle direction. + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_SIDE_ARC ); + ret = TRUE; + } + break; + + case GDK_KEY_g: // Toggle mesh tensor points on/off + case GDK_KEY_G: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_TENSOR_TOGGLE ); + ret = TRUE; + } + break; + + case GDK_KEY_j: // Smooth corner color + case GDK_KEY_J: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_SMOOTH ); + ret = TRUE; + } + break; + + case GDK_KEY_k: // Pick corner color + case GDK_KEY_K: + if (MOD__ALT(event) && drag->isNonEmpty() && drag->hasSelection()) { + sp_mesh_context_corner_operation ( this, MG_CORNER_COLOR_PICK ); + ret = TRUE; + } + break; + + default: + break; + } + + break; + + case GDK_KEY_RELEASE: + +#ifdef DEBUG_MESH + std::cout << "sp_mesh_context_root_handler: GDK_KEY_RELEASE" << std::endl; +#endif + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +// Creates a new mesh gradient. +static void sp_mesh_new_default(MeshTool &rc) { + SPDesktop *desktop = SP_EVENT_CONTEXT(&rc)->desktop; + Inkscape::Selection *selection = desktop->getSelection(); + SPDocument *document = desktop->getDocument(); + + if (!selection->isEmpty()) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::PaintTarget fill_or_stroke_pref = + static_cast<Inkscape::PaintTarget>(prefs->getInt("/tools/mesh/newfillorstroke")); + + // Ensure mesh is immediately editable. + // Editing both fill and stroke at same time doesn't work well so avoid. + if (fill_or_stroke_pref == Inkscape::FOR_FILL) { + prefs->setBool("/tools/mesh/edit_fill", true ); + prefs->setBool("/tools/mesh/edit_stroke", false); + } else { + prefs->setBool("/tools/mesh/edit_fill", false); + prefs->setBool("/tools/mesh/edit_stroke", true ); + } + +// HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPDefs *defs = document->getDefs(); + + auto items= selection->items(); + for(auto i=items.begin();i!=items.end();++i){ + + //FIXME: see above + sp_repr_css_change_recursive((*i)->getRepr(), css, "style"); + + // Create mesh element + Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr)); + mg->array.create(mg, *i, (fill_or_stroke_pref == Inkscape::FOR_FILL) ? + (*i)->geometricBounds() : (*i)->visualBounds()); + + bool isText = SP_IS_TEXT(*i); + sp_style_set_property_url(*i, + ((fill_or_stroke_pref == Inkscape::FOR_FILL) ? "fill":"stroke"), + mg, isText); + + (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_MESH, _("Create mesh")); + + // status text; we do not track coords because this branch is run once, not all the time + // during drag + int n_objects = (int) boost::distance(selection->items()); + rc.message_context->setF(Inkscape::NORMAL_MESSAGE, + ngettext("<b>Gradient</b> for %d object; with <b>Ctrl</b> to snap angle", + "<b>Gradient</b> for %d objects; with <b>Ctrl</b> to snap angle", n_objects), + n_objects); + } else { + desktop->getMessageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>objects</b> on which to create gradient.")); + } + +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/mesh-tool.h b/src/ui/tools/mesh-tool.h new file mode 100644 index 0000000..4964a02 --- /dev/null +++ b/src/ui/tools/mesh-tool.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MESH_CONTEXT_H +#define SEEN_SP_MESH_CONTEXT_H + +/* + * Mesh drawing and editing tool + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org. + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2012 Tavmjong Bah + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2005,2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include "ui/tools/tool-base.h" + +#include "object/sp-mesh-array.h" + +#define SP_MESH_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::MeshTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_MESH_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::MeshTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class MeshTool : public ToolBase { +public: + MeshTool(); + ~MeshTool() override; + + Geom::Point origin; + + Geom::Point mousepoint_doc; // stores mousepoint when over_line in doc coords + + sigc::connection *selcon; + sigc::connection *subselcon; + + static const std::string prefsPath; + + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + void selection_changed(Inkscape::Selection* sel); + + bool cursor_addnode; + bool node_added; + bool show_handles; + bool edit_fill; + bool edit_stroke; + + +}; + +void sp_mesh_context_select_next(ToolBase *event_context); +void sp_mesh_context_select_prev(ToolBase *event_context); +void sp_mesh_context_corner_operation(MeshTool *event_context, MeshCornerOperation operation ); +void sp_mesh_context_fit_mesh_in_bbox(MeshTool *event_context); + +} +} +} + +#endif // SEEN_SP_MESH_CONTEXT_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp new file mode 100644 index 0000000..bc77f91 --- /dev/null +++ b/src/ui/tools/node-tool.cpp @@ -0,0 +1,848 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * New node tool - implementation. + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iomanip> + +#include <glibmm/ustring.h> +#include <glib/gi18n.h> +#include <gdk/gdkkeysyms.h> + + + +#include "desktop.h" +#include "document.h" +#include "message-context.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" + +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sp-canvas-group.h" +#include "display/sp-canvas.h" + +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" + +#include "include/macros.h" + +#include "object/sp-clippath.h" +#include "object/sp-item-group.h" +#include "object/sp-mask.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "ui/pixmaps/cursor-node-d.xpm" +#include "ui/pixmaps/cursor-node.xpm" + +#include "ui/control-manager.h" +#include "ui/shape-editor.h" // temporary! +#include "ui/tool/control-point-selection.h" +#include "ui/tool/curve-drag-point.h" +#include "ui/tool/event-utils.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tool/selector.h" +#include "ui/tools-switch.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/tool-base.h" + + +/** @struct NodeTool + * + * Node tool event context. + * + * @par Architectural overview of the tool + * @par + * Here's a breakdown of what each object does. + * - Handle: shows a handle and keeps the node type constraint (smooth / symmetric) by updating + * the other handle's position when dragged. Its move() method cannot violate the constraints. + * - Node: keeps node type constraints for auto nodes and smooth nodes at ends of linear segments. + * Its move() method cannot violate constraints. Handles linear grow and dispatches spatial grow + * to MultiPathManipulator. Keeps a reference to its NodeList. + * - NodeList: exposes an iterator-based interface to nodes. It is possible to obtain an iterator + * to a node from the node. Keeps a reference to its SubpathList. + * - SubpathList: list of NodeLists that represents an editable pathvector. Keeps a reference + * to its PathManipulator. + * - PathManipulator: performs most of the single-path actions like reverse subpaths, + * delete segment, shift selection, etc. Keeps a reference to MultiPathManipulator. + * - MultiPathManipulator: performs additional operations for actions that are not per-path, + * for example node joins and segment joins. Tracks the control transforms for PMs that edit + * clipping paths and masks. It is more or less equivalent to ShapeEditor and in the future + * it might handle all shapes. Handles XML commit of actions that affect all paths or + * the node selection and removes PathManipulators that have no nodes left after e.g. node + * deletes. + * - ControlPointSelection: keeps track of node selection and a set of nodes that can potentially + * be selected. There can be more than one selection. Performs actions that require no + * knowledge about the path, only about the nodes, like dragging and transforms. It is not + * specific to nodes and can accommodate any control point derived from SelectableControlPoint. + * Transforms nodes in response to transform handle events. + * - TransformHandleSet: displays nodeset transform handles and emits transform events. The aim + * is to eventually use a common class for object and control point transforms. + * - SelectableControlPoint: base for any type of selectable point. It can belong to only one + * selection. + * + * @par Functionality that resides in weird places + * @par + * + * This list is probably incomplete. + * - Curve dragging: CurveDragPoint, controlled by PathManipulator + * - Single handle shortcuts: MultiPathManipulator::event(), ModifierTracker + * - Linear and spatial grow: Node, spatial grow routed to ControlPointSelection + * - Committing handle actions performed with the mouse: PathManipulator + * - Sculpting: ControlPointSelection + * + * @par Plans for the future + * @par + * - MultiPathManipulator should become a generic shape editor that manages all active manipulator, + * more or less like the old ShapeEditor. + * - Knotholder should be rewritten into one manipulator class per shape, using the control point + * classes. Interesting features like dragging rectangle sides could be added along the way. + * - Better handling of clip and mask editing, particularly in response to undo. + * - High level refactoring of the event context hierarchy. All aspects of tools, like toolbox + * controls, icons, event handling should be collected in one class, though each aspect + * of a tool might be in an separate class for better modularity. The long term goal is to allow + * tools to be defined in extensions or shared library plugins. + */ + +using Inkscape::ControlManager; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& NodeTool::getPrefsPath() { + return NodeTool::prefsPath; +} + +const std::string NodeTool::prefsPath = "/tools/nodes"; + +SPCanvasGroup *create_control_group(SPDesktop *d); + +NodeTool::NodeTool() + : ToolBase(cursor_node_xpm) + , _selected_nodes(nullptr) + , _multipath(nullptr) + , edit_clipping_paths(false) + , edit_masks(false) + , flashed_item(nullptr) + , flash_tempitem(nullptr) + , _selector(nullptr) + , _path_data(nullptr) + , _transform_handle_group(nullptr) + , _last_over(nullptr) + , cursor_drag(false) + , show_handles(false) + , show_outline(false) + , live_outline(false) + , live_objects(false) + , show_path_direction(false) + , show_transform_handles(false) + , single_node_transform_handles(false) +{ +} + +SPCanvasGroup *create_control_group(SPDesktop *d) +{ + return reinterpret_cast<SPCanvasGroup*>(sp_canvas_item_new( + d->getControls(), SP_TYPE_CANVAS_GROUP, nullptr)); +} + +void destroy_group(SPCanvasGroup *g) +{ + sp_canvas_item_destroy(SP_CANVAS_ITEM(g)); +} + +NodeTool::~NodeTool() { + this->enableGrDrag(false); + + if (this->flash_tempitem) { + this->desktop->remove_temporary_canvasitem(this->flash_tempitem); + } + for (auto hp : this->_helperpath_tmpitem) { + this->desktop->remove_temporary_canvasitem(hp); + } + this->_selection_changed_connection.disconnect(); + //this->_selection_modified_connection.disconnect(); + this->_mouseover_changed_connection.disconnect(); + this->_sizeUpdatedConn.disconnect(); + + delete this->_multipath; + delete this->_selected_nodes; + delete this->_selector; + + Inkscape::UI::PathSharedData &data = *this->_path_data; + destroy_group(data.node_data.node_group); + destroy_group(data.node_data.handle_group); + destroy_group(data.node_data.handle_line_group); + destroy_group(data.outline_group); + destroy_group(data.dragpoint_group); + destroy_group(this->_transform_handle_group); + this->desktop->canvas->endForcedFullRedraws(); +} + +void NodeTool::setup() { + ToolBase::setup(); + + this->_path_data = new Inkscape::UI::PathSharedData(); + + Inkscape::UI::PathSharedData &data = *this->_path_data; + data.node_data.desktop = this->desktop; + + // selector has to be created here, so that its hidden control point is on the bottom + this->_selector = new Inkscape::UI::Selector(this->desktop); + + // Prepare canvas groups for controls. This guarantees correct z-order, so that + // for example a dragpoint won't obscure a node + data.outline_group = create_control_group(this->desktop); + data.node_data.handle_line_group = create_control_group(this->desktop); + data.dragpoint_group = create_control_group(this->desktop); + this->_transform_handle_group = create_control_group(this->desktop); + data.node_data.node_group = create_control_group(this->desktop); + data.node_data.handle_group = create_control_group(this->desktop); + + Inkscape::Selection *selection = this->desktop->getSelection(); + + this->_selection_changed_connection.disconnect(); + this->_selection_changed_connection = + selection->connectChanged(sigc::mem_fun(this, &NodeTool::selection_changed)); + + this->_mouseover_changed_connection.disconnect(); + this->_mouseover_changed_connection = + Inkscape::UI::ControlPoint::signal_mouseover_change.connect(sigc::mem_fun(this, &NodeTool::mouseover_changed)); + + this->_sizeUpdatedConn = ControlManager::getManager().connectCtrlSizeChanged( + sigc::mem_fun(this, &NodeTool::handleControlUiStyleChange) + ); + if (this->_transform_handle_group) { + this->_selected_nodes = new Inkscape::UI::ControlPointSelection(this->desktop, this->_transform_handle_group); + } + data.node_data.selection = this->_selected_nodes; + + this->_multipath = new Inkscape::UI::MultiPathManipulator(data, this->_selection_changed_connection); + + this->_selector->signal_point.connect(sigc::mem_fun(this, &NodeTool::select_point)); + this->_selector->signal_area.connect(sigc::mem_fun(this, &NodeTool::select_area)); + + this->_multipath->signal_coords_changed.connect( + sigc::bind( + sigc::mem_fun(*this->desktop, &SPDesktop::emitToolSubselectionChanged), + (void*)nullptr + ) + ); + + this->_selected_nodes->signal_selection_changed.connect( + // Hide both signal parameters and bind the function parameter to 0 + // sigc::signal<void, SelectableControlPoint *, bool> + // <=> + // void update_tip(GdkEvent *event) + sigc::hide(sigc::hide(sigc::bind( + sigc::mem_fun(this, &NodeTool::update_tip), + (GdkEvent*)nullptr + ))) + ); + + this->cursor_drag = false; + this->show_transform_handles = true; + this->single_node_transform_handles = false; + this->flash_tempitem = nullptr; + this->flashed_item = nullptr; + this->_last_over = nullptr; + + // read prefs before adding items to selection to prevent momentarily showing the outline + sp_event_context_read(this, "show_handles"); + sp_event_context_read(this, "show_outline"); + sp_event_context_read(this, "live_outline"); + sp_event_context_read(this, "live_objects"); + sp_event_context_read(this, "show_path_direction"); + sp_event_context_read(this, "show_transform_handles"); + sp_event_context_read(this, "single_node_transform_handles"); + sp_event_context_read(this, "edit_clipping_paths"); + sp_event_context_read(this, "edit_masks"); + + this->selection_changed(selection); + this->update_tip(nullptr); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/nodes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/nodes/gradientdrag")) { + this->enableGrDrag(); + } + + this->desktop->emitToolSubselectionChanged(nullptr); // sets the coord entry fields to inactive + sp_update_helperpath(); +} + +// Clean selection on tool change +void NodeTool::finish() +{ + this->_selected_nodes->clear(); + ToolBase::finish(); +} + +// show helper paths of the applied LPE, if any +void sp_update_helperpath() { + SPDesktop * desktop = SP_ACTIVE_DESKTOP; + if (!desktop || !tools_isactive(desktop, TOOLS_NODES)) { + return; + } + Inkscape::UI::Tools::NodeTool *nt = static_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context); + Inkscape::Selection *selection = desktop->getSelection(); + for (auto hp : nt->_helperpath_tmpitem) { + desktop->remove_temporary_canvasitem(hp); + } + nt->_helperpath_tmpitem.clear(); + std::vector<SPItem *> vec(selection->items().begin(), selection->items().end()); + std::vector<std::pair<Geom::PathVector, Geom::Affine>> cs; + for (auto item : vec) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + Inkscape::LivePathEffect::Effect *lpe = SP_LPE_ITEM(lpeitem)->getCurrentLPE(); + if (lpe && lpe->isVisible()/* && lpe->showOrigPath()*/) { + std::vector<Geom::Point> selectedNodesPositions; + if (nt->_selected_nodes) { + Inkscape::UI::ControlPointSelection *selectionNodes = nt->_selected_nodes; + for (auto selectionNode : *selectionNodes) { + Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selectionNode); + selectedNodesPositions.push_back(n->position()); + } + } + lpe->setSelectedNodePoints(selectedNodesPositions); + lpe->setCurrentZoom(desktop->current_zoom()); + SPCurve *c = new SPCurve(); + SPCurve *cc = new SPCurve(); + std::vector<Geom::PathVector> cs = lpe->getCanvasIndicators(lpeitem); + for (auto &p : cs) { + cc->set_pathvector(p); + c->append(cc, false); + cc->reset(); + } + if (!c->is_empty()) { + SPCanvasItem *helperpath = sp_canvas_bpath_new(desktop->getTempGroup(), c, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(helperpath), 0x0000ff9A, 1.0, SP_STROKE_LINEJOIN_MITER, + SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(helperpath), 0, SP_WIND_RULE_NONZERO); + sp_canvas_item_affine_absolute(helperpath, lpeitem->i2dt_affine()); + nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath, 0)); + SPCanvasItem *helperpath_back = sp_canvas_bpath_new(desktop->getTempGroup(), c, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(helperpath_back), 0xFFFFFF33, 3.0, SP_STROKE_LINEJOIN_MITER, + SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(helperpath_back), 0, SP_WIND_RULE_NONZERO); + sp_canvas_item_affine_absolute(helperpath_back, lpeitem->i2dt_affine()); + nt->_helperpath_tmpitem.emplace_back(desktop->add_temporary_canvasitem(helperpath_back, 0)); + } + c->unref(); + cc->unref(); + } + } + } +} + +void NodeTool::set(const Inkscape::Preferences::Entry& value) { + Glib::ustring entry_name = value.getEntryName(); + + if (entry_name == "show_handles") { + this->show_handles = value.getBool(true); + this->_multipath->showHandles(this->show_handles); + } else if (entry_name == "show_outline") { + this->show_outline = value.getBool(); + this->_multipath->showOutline(this->show_outline); + } else if (entry_name == "live_outline") { + this->live_outline = value.getBool(); + this->_multipath->setLiveOutline(this->live_outline); + } else if (entry_name == "live_objects") { + this->live_objects = value.getBool(); + this->_multipath->setLiveObjects(this->live_objects); + } else if (entry_name == "show_path_direction") { + this->show_path_direction = value.getBool(); + this->_multipath->showPathDirection(this->show_path_direction); + } else if (entry_name == "show_transform_handles") { + this->show_transform_handles = value.getBool(true); + this->_selected_nodes->showTransformHandles( + this->show_transform_handles, this->single_node_transform_handles); + } else if (entry_name == "single_node_transform_handles") { + this->single_node_transform_handles = value.getBool(); + this->_selected_nodes->showTransformHandles( + this->show_transform_handles, this->single_node_transform_handles); + } else if (entry_name == "edit_clipping_paths") { + this->edit_clipping_paths = value.getBool(); + this->selection_changed(this->desktop->selection); + } else if (entry_name == "edit_masks") { + this->edit_masks = value.getBool(); + this->selection_changed(this->desktop->selection); + } else { + ToolBase::set(value); + } +} + +/** Recursively collect ShapeRecords */ +static +void gather_items(NodeTool *nt, SPItem *base, SPObject *obj, Inkscape::UI::ShapeRole role, + std::set<Inkscape::UI::ShapeRecord> &s) +{ + using namespace Inkscape::UI; + + if (!obj) { + return; + } + + //XML Tree being used directly here while it shouldn't be. + if (role != SHAPE_ROLE_NORMAL && (SP_IS_GROUP(obj) || SP_IS_OBJECTGROUP(obj))) { + for (auto& c: obj->children) { + gather_items(nt, base, &c, role, s); + } + } else if (SP_IS_ITEM(obj)) { + SPObject *object = obj; + SPItem *item = dynamic_cast<SPItem *>(obj); + ShapeRecord r; + r.object = object; + // TODO add support for objectBoundingBox + r.edit_transform = base ? base->i2doc_affine() : Geom::identity(); + r.role = role; + + if (s.insert(r).second) { + // this item was encountered the first time + if (nt->edit_clipping_paths) { + gather_items(nt, item, item->getClipObject(), SHAPE_ROLE_CLIPPING_PATH, s); + } + + if (nt->edit_masks) { + gather_items(nt, item, item->getMaskObject(), SHAPE_ROLE_MASK, s); + } + } + } +} + +void NodeTool::selection_changed(Inkscape::Selection *sel) { + using namespace Inkscape::UI; + + std::set<ShapeRecord> shapes; + + auto items= sel->items(); + for(auto i=items.begin();i!=items.end();++i){ + SPItem *item = *i; + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + sp_lpe_item_update_patheffect(lpeitem, true, true); + } + if (item) { + gather_items(this, nullptr, item, SHAPE_ROLE_NORMAL, shapes); + } + } + + // use multiple ShapeEditors for now, to allow editing many shapes at once + // needs to be rethought + for (boost::ptr_map<SPItem*, ShapeEditor>::iterator i = this->_shape_editors.begin(); + i != this->_shape_editors.end(); ) + { + ShapeRecord s; + s.object = dynamic_cast<SPObject *>(i->first); + + if (shapes.find(s) == shapes.end()) { + this->_shape_editors.erase(i++); + } else { + ++i; + } + } + + for (const auto & r : shapes) { + if ((SP_IS_SHAPE(r.object) || SP_IS_TEXT(r.object) || SP_IS_GROUP(r.object) || SP_IS_OBJECTGROUP(r.object)) && + this->_shape_editors.find(SP_ITEM(r.object)) == this->_shape_editors.end()) { + ShapeEditor *si = new ShapeEditor(this->desktop, r.edit_transform); + SPItem *item = SP_ITEM(r.object); + si->set_item(item); + this->_shape_editors.insert(item, si); + } + } + + std::vector<SPItem *> vec(sel->items().begin(), sel->items().end()); + _previous_selection = _current_selection; + _current_selection = vec; + this->_multipath->setItems(shapes); + this->update_tip(nullptr); + sp_update_helperpath(); + // This not need to be called canvas is updated on selection change on setItems + // this->desktop->updateNow(); +} + +bool NodeTool::root_handler(GdkEvent* event) { + /* things to handle here: + * 1. selection of items + * 2. passing events to manipulators + * 3. some keybindings + */ + using namespace Inkscape::UI; // pull in event helpers + + desktop->getCanvas()->forceFullRedrawAfterInterruptions(5, false); + + Inkscape::Selection *selection = desktop->selection; + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (this->_multipath->event(this, event)) { + return true; + } + + if (this->_selector->event(this, event)) { + return true; + } + + if (this->_selected_nodes->event(this, event)) { + return true; + } + switch (event->type) + { + case GDK_MOTION_NOTIFY: { + sp_update_helperpath(); + SPItem *over_item = nullptr; + combine_motion_events(desktop->canvas, event->motion, 0); + over_item = sp_event_context_find_item(desktop, event_point(event->button), FALSE, TRUE); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(this->desktop->w2d(motion_w)); + + SnapManager &m = this->desktop->namedview->snap_manager; + + // We will show a pre-snap indication for when the user adds a node through double-clicking + // Adding a node will only work when a path has been selected; if that's not the case then snapping is useless + if (!this->desktop->selection->isEmpty()) { + if (!(event->motion.state & GDK_SHIFT_MASK)) { + m.setup(this->desktop); + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + m.preSnap(scp, true); + m.unSetup(); + } + } + + if (over_item && over_item != this->_last_over) { + this->_last_over = over_item; + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + } + // create pathflash outline + if (prefs->getBool("/tools/nodes/pathflash_enabled")) { + // We want to reset flashed item to can highligh again previous one + if (!over_item && this->flashed_item) { + this->flashed_item = nullptr; + break; + } + if (!over_item || over_item == this->flashed_item) { + break; + } + + if (!prefs->getBool("/tools/nodes/pathflash_selected") && over_item && selection->includes(over_item)) { + break; + } + + if (this->flash_tempitem) { + desktop->remove_temporary_canvasitem(this->flash_tempitem); + this->flash_tempitem = nullptr; + this->flashed_item = nullptr; + } + + if (!SP_IS_SHAPE(over_item)) { + break; // for now, handle only shapes + } + + this->flashed_item = over_item; + SPCurve *c = SP_SHAPE(over_item)->getCurveForEdit(); + + if (!c) { + break; // break out when curve doesn't exist + } + + c->transform(over_item->i2dt_affine()); + SPCanvasItem *flash = sp_canvas_bpath_new(desktop->getTempGroup(), c, true); + + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(flash), + //prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff), 1.0, + over_item->highlight_color(), 1.0, + SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(flash), 0, SP_WIND_RULE_NONZERO); + this->flash_tempitem = desktop->add_temporary_canvasitem(flash, + prefs->getInt("/tools/nodes/pathflash_timeout", 500)); + + c->unref(); + } + } break; // do not return true, because we need to pass this event to the parent context + // otherwise some features cease to work + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) + { + case GDK_KEY_Escape: // deselect everything + if (this->_selected_nodes->empty()) { + Inkscape::SelectionHelper::selectNone(desktop); + } else { + this->_selected_nodes->clear(); + } + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + return TRUE; + + case GDK_KEY_a: + case GDK_KEY_A: + if (held_control(event->key) && held_alt(event->key)) { + this->_selected_nodes->selectAll(); + // Ctrl+A is handled in selection-chemistry.cpp via verb + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + return TRUE; + } + break; + + case GDK_KEY_h: + case GDK_KEY_H: + if (held_only_control(event->key)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/nodes/show_handles", !this->show_handles); + return TRUE; + } + break; + + default: + break; + } + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + break; + + case GDK_KEY_RELEASE: + //ink_node_tool_update_tip(nt, event); + this->update_tip(event); + break; + + case GDK_BUTTON_RELEASE: + if (this->_selector->doubleClicked()) { + // If the selector received the doubleclick event, then we're at some distance from + // the path; otherwise, the doubleclick event would have been received by + // CurveDragPoint; we will insert nodes into the path anyway but only if we can snap + // to the path. Otherwise the position would not be very well defined. + if (!(event->motion.state & GDK_SHIFT_MASK)) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(this->desktop->w2d(motion_w)); + + SnapManager &m = this->desktop->namedview->snap_manager; + m.setup(this->desktop); + Inkscape::SnapCandidatePoint scp(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE); + Inkscape::SnappedPoint sp = m.freeSnap(scp, Geom::OptRect(), true); + m.unSetup(); + + if (sp.getSnapped()) { + // The first click of the double click will have cleared the path selection, because + // we clicked aside of the path. We need to undo this on double click + Inkscape::Selection *selection = desktop->getSelection(); + selection->addList(_previous_selection); + + // The selection has been restored, and the signal selection_changed has been emitted, + // which has again forced a restore of the _mmap variable of the MultiPathManipulator (this->_multipath) + // Now we can insert the new nodes as if nothing has happened! + this->_multipath->insertNode(this->desktop->d2w(sp.getPoint())); + } + } + } + break; + + default: + break; + } + // we realy dont want to stop any node operation we want to success all even the time consume it + + return ToolBase::root_handler(event); +} + +void NodeTool::update_tip(GdkEvent *event) { + using namespace Inkscape::UI; + if (event && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) { + unsigned new_state = state_after_event(event); + + if (new_state == event->key.state) { + return; + } + + if (state_held_shift(new_state)) { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection, " + "click to toggle object selection")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, + C_("Node tool tip", "<b>Shift</b>: drag to add nodes to the selection")); + } + + return; + } + } + + unsigned sz = this->_selected_nodes->size(); + unsigned total = this->_selected_nodes->allPoints().size(); + + if (sz != 0) { + // TODO: Use Glib::ustring::compose and remove the useless copy after string freeze + char *nodestring_temp = g_strdup_printf( + ngettext("<b>%u of %u</b> node selected.", "<b>%u of %u</b> nodes selected.", total), + sz, total); + Glib::ustring nodestring(nodestring_temp); + g_free(nodestring_temp); + + if (sz == 2) { + // if there are only two nodes selected, display the angle + // of a line going through them relative to the X axis. + Inkscape::UI::ControlPointSelection::Set &selection_nodes = this->_selected_nodes->allPoints(); + std::vector<Geom::Point> positions; + for (auto selection_node : selection_nodes) { + if (selection_node->selected()) { + Inkscape::UI::Node *n = dynamic_cast<Inkscape::UI::Node *>(selection_node); + positions.push_back(n->position()); + } + } + g_assert(positions.size() == 2); + const double angle = Geom::deg_from_rad(Geom::Line(positions[0], positions[1]).angle()); + nodestring += " "; + nodestring += Glib::ustring::compose(_("Angle: %1°."), + Glib::ustring::format(std::fixed, std::setprecision(2), angle)); + } + + if (this->_last_over) { + // TRANSLATORS: The %s below is where the "%u of %u nodes selected" sentence gets put + char *dyntip = g_strdup_printf(C_("Node tool tip", + "%s Drag to select nodes, click to edit only this object (more: Shift)"), + nodestring.c_str()); + this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } else { + char *dyntip = g_strdup_printf(C_("Node tool tip", + "%s Drag to select nodes, click clear the selection"), + nodestring.c_str()); + this->message_context->set(Inkscape::NORMAL_MESSAGE, dyntip); + g_free(dyntip); + } + } else if (!this->_multipath->empty()) { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to edit only this object")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select nodes, click to clear the selection")); + } + } else { + if (this->_last_over) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit, click to edit this object (more: Shift)")); + } else { + this->message_context->set(Inkscape::NORMAL_MESSAGE, C_("Node tool tip", + "Drag to select objects to edit")); + } + } +} + +/** + * @param sel Area in desktop coordinates + */ +void NodeTool::select_area(Geom::Rect const &sel, GdkEventButton *event) { + using namespace Inkscape::UI; + + if (this->_multipath->empty()) { + // if multipath is empty, select rubberbanded items rather than nodes + Inkscape::Selection *selection = this->desktop->selection; + auto sel_doc = desktop->dt2doc() * sel; + std::vector<SPItem*> items = this->desktop->getDocument()->getItemsInBox(this->desktop->dkey, sel_doc); + selection->setList(items); + } else { + if (!held_shift(*event)) { + this->_selected_nodes->clear(); + } + + this->_selected_nodes->selectArea(sel); + } +} + +void NodeTool::select_point(Geom::Point const &/*sel*/, GdkEventButton *event) { + using namespace Inkscape::UI; // pull in event helpers + + if (!event) { + return; + } + + if (event->button != 1) { + return; + } + + Inkscape::Selection *selection = this->desktop->selection; + + SPItem *item_clicked = sp_event_context_find_item (this->desktop, event_point(*event), + (event->state & GDK_MOD1_MASK) && !(event->state & GDK_CONTROL_MASK), TRUE); + + if (item_clicked == nullptr) { // nothing under cursor + // if no Shift, deselect + // if there are nodes selected, the first click should deselect the nodes + // and the second should deselect the items + if (!state_held_shift(event->state)) { + if (this->_selected_nodes->empty()) { + selection->clear(); + } else { + this->_selected_nodes->clear(); + } + } + } else { + if (held_shift(*event)) { + selection->toggle(item_clicked); + } else { + selection->set(item_clicked); + } + // This not need to be called canvas is updated on selection change + // this->desktop->updateNow(); + } +} + +void NodeTool::mouseover_changed(Inkscape::UI::ControlPoint *p) { + using Inkscape::UI::CurveDragPoint; + + CurveDragPoint *cdp = dynamic_cast<CurveDragPoint*>(p); + + if (cdp && !this->cursor_drag) { + this->cursor_shape = cursor_node_d_xpm; + this->sp_event_context_update_cursor(); + this->cursor_drag = true; + } else if (!cdp && this->cursor_drag) { + this->cursor_shape = cursor_node_xpm; + this->sp_event_context_update_cursor(); + this->cursor_drag = false; + } +} + +void NodeTool::handleControlUiStyleChange() { + this->_multipath->updateHandles(); +} + +} +} +} + +//} // anonymous namespace + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/node-tool.h b/src/ui/tools/node-tool.h new file mode 100644 index 0000000..2fcc8d0 --- /dev/null +++ b/src/ui/tools/node-tool.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief New node tool with support for multiple path editing + */ +/* Authors: + * Krzysztof KosiÅ„ski <tweenk@gmail.com> + * + * Copyright (C) 2009 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UI_TOOL_NODE_TOOL_H +#define SEEN_UI_TOOL_NODE_TOOL_H + +#include <boost/ptr_container/ptr_map.hpp> +#include <glib.h> +#include "ui/tools/tool-base.h" + +// we need it to call it from Live Effect +#include "selection.h" + +namespace Inkscape { + namespace Display { + class TemporaryItem; + } + + namespace UI { + class MultiPathManipulator; + class ControlPointSelection; + class Selector; + class ControlPoint; + + struct PathSharedData; + } +} + +struct SPCanvasGroup; + +#define INK_NODE_TOOL(obj) (dynamic_cast<Inkscape::UI::Tools::NodeTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define INK_IS_NODE_TOOL(obj) (dynamic_cast<const Inkscape::UI::Tools::NodeTool*>((const Inkscape::UI::Tools::ToolBase*)obj)) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class NodeTool : public ToolBase { +public: + NodeTool(); + ~NodeTool() override; + + Inkscape::UI::ControlPointSelection* _selected_nodes; + Inkscape::UI::MultiPathManipulator* _multipath; + std::vector<Inkscape::Display::TemporaryItem *> _helperpath_tmpitem; + + bool edit_clipping_paths; + bool edit_masks; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + boost::ptr_map<SPItem*, ShapeEditor> _shape_editors; + +private: + sigc::connection _selection_changed_connection; + sigc::connection _mouseover_changed_connection; + sigc::connection _sizeUpdatedConn; + + SPItem *flashed_item; + + Inkscape::Display::TemporaryItem *flash_tempitem; + Inkscape::UI::Selector* _selector; + Inkscape::UI::PathSharedData* _path_data; + SPCanvasGroup *_transform_handle_group; + SPItem *_last_over; + + bool cursor_drag; + bool show_handles; + bool show_outline; + bool live_outline; + bool live_objects; + bool show_path_direction; + bool show_transform_handles; + bool single_node_transform_handles; + + std::vector<SPItem*> _current_selection; + std::vector<SPItem*> _previous_selection; + + void selection_changed(Inkscape::Selection *sel); + + void select_area(Geom::Rect const &sel, GdkEventButton *event); + void select_point(Geom::Point const &sel, GdkEventButton *event); + void mouseover_changed(Inkscape::UI::ControlPoint *p); + void update_tip(GdkEvent *event); + void handleControlUiStyleChange(); +}; + void sp_update_helperpath(); +} + +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp new file mode 100644 index 0000000..a6d7858 --- /dev/null +++ b/src/ui/tools/pen-tool.cpp @@ -0,0 +1,2104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Pen event context implementation. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/curves.h> + +#include "context-fns.h" +#include "desktop.h" +#include "include/macros.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "shortcuts.h" +#include "verbs.h" + +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sodipodi-ctrl.h" +#include "display/sp-canvas.h" +#include "display/sp-ctrlline.h" + +#include "object/sp-path.h" + +#include "ui/pixmaps/cursor-pen.xpm" + +#include "ui/control-manager.h" +#include "ui/draw-anchor.h" +#include "ui/tools-switch.h" +#include "ui/tools/pen-tool.h" + +// we include the necessary files for BSpline & Spiro +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/parameter/path.h" + +#define INKSCAPE_LPE_SPIRO_C +#include "live_effects/lpe-spiro.h" + +#include "helper/geom-nodetype.h" + +// For handling un-continuous paths: +#include "inkscape.h" + +#include "live_effects/spiro.h" + +#define INKSCAPE_LPE_BSPLINE_C +#include "live_effects/lpe-bspline.h" + + +using Inkscape::ControlManager; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static Geom::Point pen_drag_origin_w(0, 0); +static bool pen_within_tolerance = false; +const double HANDLE_CUBIC_GAP = 0.001; + +const std::string& PenTool::getPrefsPath() { + return PenTool::prefsPath; +} + +const std::string PenTool::prefsPath = "/tools/freehand/pen"; + +PenTool::PenTool() + : FreehandBase(cursor_pen_xpm) + , p() + , previous(Geom::Point(0,0)) + , npoints(0) + , mode(MODE_CLICK) + , state(POINT) + , polylines_only(false) + , polylines_paraxial(false) + , paraxial_angle(Geom::Point(0,0)) + , num_clicks(0) + , expecting_clicks_for_LPE(0) + , waiting_LPE(nullptr) + , waiting_item(nullptr) + , c0(nullptr) + , c1(nullptr) + , cl0(nullptr) + , cl1(nullptr) + , events_disabled(false) +{ + tablet_enabled = false; +} + +PenTool::PenTool(gchar const *const *cursor_shape) + : FreehandBase(cursor_shape) + , p() + , previous(Geom::Point(0,0)) + , npoints(0) + , mode(MODE_CLICK) + , state(POINT) + , polylines_only(false) + , polylines_paraxial(false) + , num_clicks(0) + , expecting_clicks_for_LPE(0) + , waiting_LPE(nullptr) + , waiting_item(nullptr) + , c0(nullptr) + , c1(nullptr) + , cl0(nullptr) + , cl1(nullptr) + , events_disabled(false) +{ +} + +PenTool::~PenTool() { + if (this->c0) { + sp_canvas_item_destroy(this->c0); + this->c0 = nullptr; + } + if (this->c1) { + sp_canvas_item_destroy(this->c1); + this->c1 = nullptr; + } + if (this->cl0) { + sp_canvas_item_destroy(this->cl0); + this->cl0 = nullptr; + } + if (this->cl1) { + sp_canvas_item_destroy(this->cl1); + this->cl1 = nullptr; + } + + if (this->waiting_item && this->expecting_clicks_for_LPE > 0) { + // we received too few clicks to sanely set the parameter path so we remove the LPE from the item + this->waiting_item->removeCurrentPathEffect(false); + } +} + +void PenTool::setPolylineMode() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pen/freehand-mode", 0); + // change the nodes to make space for bspline mode + this->polylines_only = (mode == 3 || mode == 4); + this->polylines_paraxial = (mode == 4); + this->spiro = (mode == 1); + this->bspline = (mode == 2); + this->_bsplineSpiroColor(); + if (!this->green_bpaths.empty()) + this->_redrawAll(); +} + +/** + * Callback to initialize PenTool object. + */ +void PenTool::setup() { + FreehandBase::setup(); + ControlManager &mgr = ControlManager::getManager(); + + // Pen indicators + this->c0 = mgr.createControl(this->desktop->getControls(), Inkscape::CTRL_TYPE_ADJ_HANDLE); + mgr.track(this->c0); + + this->c1 = mgr.createControl(this->desktop->getControls(), Inkscape::CTRL_TYPE_ADJ_HANDLE); + mgr.track(this->c1); + + this->cl0 = mgr.createControlLine(this->desktop->getControls()); + this->cl1 = mgr.createControlLine(this->desktop->getControls()); + + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl0); + sp_canvas_item_hide(this->cl1); + + sp_event_context_read(this, "mode"); + + this->anchor_statusbar = false; + + this->setPolylineMode(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pen/selcue")) { + this->enableSelectionCue(); + } +} + +void PenTool::_cancel() { + this->num_clicks = 0; + this->state = PenTool::STOP; + this->_resetColors(); + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl0); + sp_canvas_item_hide(this->cl1); + this->message_context->clear(); + this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); + + this->desktop->canvas->endForcedFullRedraws(); +} + +/** + * Finalization callback. + */ +void PenTool::finish() { + sp_event_context_discard_delayed_snap_event(this); + + if (this->npoints != 0) { + // switching context - finish path + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + this->_finish(false); + } + + FreehandBase::finish(); +} + +/** + * Callback that sets key to value in pen context. + */ +void PenTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring name = val.getEntryName(); + + if (name == "mode") { + if ( val.getString() == "drag" ) { + this->mode = MODE_DRAG; + } else { + this->mode = MODE_CLICK; + } + } +} + +bool PenTool::hasWaitingLPE() { + // note: waiting_LPE_type is defined in SPDrawContext + return (this->waiting_LPE != nullptr || + this->waiting_LPE_type != Inkscape::LivePathEffect::INVALID_LPE); +} + +/** + * Snaps new node relative to the previous node. + */ +void PenTool::_endpointSnap(Geom::Point &p, guint const state) const { + // Paraxial kicks in after first line has set the angle (before then it's a free line) + bool poly = this->polylines_paraxial && !this->green_curve->is_unset(); + + if ((state & GDK_CONTROL_MASK) && !poly) { //CTRL enables angular snapping + if (this->npoints > 0) { + spdc_endpoint_snap_rotation(this, p, this->p[0], state); + } else { + boost::optional<Geom::Point> origin = boost::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); + } + } else { + // We cannot use shift here to disable snapping because the shift-key is already used + // to toggle the paraxial direction; if the user wants to disable snapping (s)he will + // have to use the %-key, the menu, or the snap toolbar + if ((this->npoints > 0) && poly) { + // snap constrained + this->_setToNearestHorizVert(p, state); + } else { + // snap freely + boost::optional<Geom::Point> origin = this->npoints > 0 ? this->p[0] : boost::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); // pass the origin, to allow for perpendicular / tangential snapping + } + } +} + +/** + * Snaps new node's handle relative to the new node. + */ +void PenTool::_endpointSnapHandle(Geom::Point &p, guint const state) const { + g_return_if_fail(( this->npoints == 2 || + this->npoints == 5 )); + + if ((state & GDK_CONTROL_MASK)) { //CTRL enables angular snapping + spdc_endpoint_snap_rotation(this, p, this->p[this->npoints - 2], state); + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + boost::optional<Geom::Point> origin = this->p[this->npoints - 2]; + spdc_endpoint_snap_free(this, p, origin, state); + } + } +} + +bool PenTool::item_handler(SPItem* item, GdkEvent* event) { + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + default: + break; + } + + if (!ret) { + ret = FreehandBase::item_handler(item, event); + } + + return ret; +} + +/** + * Callback to handle all pen events. + */ +bool PenTool::root_handler(GdkEvent* event) { + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_2BUTTON_PRESS: + ret = this->_handle2ButtonPress(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(event); + break; + + default: + break; + } + + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +/** + * Handle mouse button press event. + */ +bool PenTool::_handleButtonPress(GdkEventButton const &bevent) { + if (this->events_disabled) { + // skip event processing if events are disabled + return false; + } + + Geom::Point const event_w(bevent.x, bevent.y); + Geom::Point event_dt(desktop->w2d(event_w)); + //Test whether we hit any anchor. + SPDrawAnchor * const anchor = spdc_test_inside(this, event_w); + + //with this we avoid creating a new point over the existing one + if(bevent.button != 3 && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){ + if( anchor && anchor == this->sa && this->green_curve->is_unset()){ + //remove the following line to avoid having one node on top of another + _finishSegment(event_dt, bevent.state); + _finish(true); + return true; + } + return false; + } + + bool ret = false; + if (bevent.button == 1 && !this->space_panning + // make sure this is not the last click for a waiting LPE (otherwise we want to finish the path) + && this->expecting_clicks_for_LPE != 1) { + + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return true; + } + + if (!this->grab ) { + // Grab mouse, so release will not pass unnoticed + this->grab = SP_CANVAS_ITEM(desktop->acetate); + sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + nullptr, bevent.time); + } + + pen_drag_origin_w = event_w; + pen_within_tolerance = true; + + switch (this->mode) { + + case PenTool::MODE_CLICK: + // In click mode we add point on release + switch (this->state) { + case PenTool::POINT: + case PenTool::CONTROL: + case PenTool::CLOSE: + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + this->state = PenTool::POINT; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::STOP: + // This is allowed, if we just canceled curve + case PenTool::POINT: + if (this->npoints == 0) { + this->_bsplineSpiroColor(); + Geom::Point p; + if ((bevent.state & GDK_CONTROL_MASK) && (this->polylines_only || this->polylines_paraxial)) { + p = event_dt; + if (!(bevent.state & GDK_SHIFT_MASK)) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + } + spdc_create_single_dot(this, p, "/tools/freehand/pen", bevent.state); + ret = true; + break; + } + + // TODO: Perhaps it would be nicer to rearrange the following case + // distinction so that the case of a waiting LPE is treated separately + + // Set start anchor + + this->sa = anchor; + if (anchor) { + //Put the start overwrite curve always on the same direction + if (anchor->start) { + this->sa_overwrited = this->sa->curve->create_reverse(); + } else { + this->sa_overwrited = this->sa->curve->copy(); + } + this->_bsplineSpiroStartAnchor(bevent.state & GDK_SHIFT_MASK); + } + if (anchor && (!this->hasWaitingLPE()|| this->bspline || this->spiro)) { + // Adjust point to anchor if needed; if we have a waiting LPE, we need + // a fresh path to be created so don't continue an existing one + p = anchor->dp; + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path")); + } else { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + Inkscape::Selection * const selection = desktop->getSelection(); + if (!(bevent.state & GDK_SHIFT_MASK) || this->hasWaitingLPE()) { + // if we have a waiting LPE, we need a fresh path to be created + // so don't append to an existing one + selection->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + } + + // Create green anchor + p = event_dt; + this->_endpointSnap(p, bevent.state); + this->green_anchor = sp_draw_anchor_new(this, this->green_curve, true, p); + } + this->_setInitialPoint(p); + } else { + // Set end anchor + this->ea = anchor; + Geom::Point p; + if (anchor) { + p = anchor->dp; + // we hit an anchor, will finish the curve (either with or without closing) + // in release handler + this->state = PenTool::CLOSE; + + if (this->green_anchor && this->green_anchor->active) { + // we clicked on the current curve start, so close it even if + // we drag a handle away from it + this->green_closed = true; + } + ret = true; + break; + + } else { + p = event_dt; + this->_endpointSnap(p, bevent.state); // Snap node only if not hitting anchor. + this->_setSubsequentPoint(p, true); + } + } + // avoid the creation of a control point so a node is created in the release event + this->state = (this->spiro || this->bspline || this->polylines_only) ? PenTool::POINT : PenTool::CONTROL; + ret = true; + break; + case PenTool::CONTROL: + g_warning("Button down in CONTROL state"); + break; + case PenTool::CLOSE: + g_warning("Button down in CLOSE state"); + break; + default: + break; + } + break; + default: + break; + } + } else if (this->expecting_clicks_for_LPE == 1 && this->npoints != 0) { + // when the last click for a waiting LPE occurs we want to finish the path + this->_finishSegment(event_dt, bevent.state); + if (this->green_closed) { + // finishing at the start anchor, close curve + this->_finish(true); + } else { + // finishing at some other anchor, finish curve but not close + this->_finish(false); + } + + ret = true; + } else if (bevent.button == 3 && this->npoints != 0) { + // right click - finish path + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + this->_finish(false); + ret = true; + } + + if (this->expecting_clicks_for_LPE > 0) { + --this->expecting_clicks_for_LPE; + } + + return ret; +} + +/** + * Handle motion_notify event. + */ +bool PenTool::_handleMotionNotify(GdkEventMotion const &mevent) { + bool ret = false; + + if (this->space_panning || mevent.state & GDK_BUTTON2_MASK || mevent.state & GDK_BUTTON3_MASK) { + // allow scrolling + return false; + } + + if (this->events_disabled) { + // skip motion events if pen events are disabled + return false; + } + + Geom::Point const event_w(mevent.x, mevent.y); + + //we take out the function the const "tolerance" because we need it later + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if (pen_within_tolerance) { + if ( Geom::LInfty( event_w - pen_drag_origin_w ) < tolerance ) { + return false; // Do not drag if we're within tolerance from origin. + } + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + pen_within_tolerance = false; + + // Find desktop coordinates + Geom::Point p = desktop->w2d(event_w); + + // Test, whether we hit any anchor + SPDrawAnchor *anchor = spdc_test_inside(this, event_w); + + switch (this->mode) { + case PenTool::MODE_CLICK: + switch (this->state) { + case PenTool::POINT: + if ( this->npoints != 0 ) { + // Only set point, if we are already appending + this->_endpointSnap(p, mevent.state); + this->_setSubsequentPoint(p, true); + ret = true; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case PenTool::CONTROL: + case PenTool::CLOSE: + // Placing controls is last operation in CLOSE state + this->_endpointSnap(p, mevent.state); + this->_setCtrl(p, mevent.state); + ret = true; + break; + case PenTool::STOP: + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::POINT: + if ( this->npoints > 0 ) { + // Only set point, if we are already appending + + if (!anchor) { // Snap node only if not hitting anchor + this->_endpointSnap(p, mevent.state); + this->_setSubsequentPoint(p, true, mevent.state); + } else { + this->_setSubsequentPoint(anchor->dp, false, mevent.state); + } + + if (anchor && !this->anchor_statusbar) { + if(!this->spiro && !this->bspline){ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path.")); + }else{ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to close and finish the path. Shift+Click make a cusp node")); + } + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + } + + ret = true; + } else { + if (anchor && !this->anchor_statusbar) { + if(!this->spiro && !this->bspline){ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point.")); + }else{ + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> or <b>click and drag</b> to continue the path from this point. Shift+Click make a cusp node")); + } + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + + } + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + } + break; + case PenTool::CONTROL: + case PenTool::CLOSE: + // Placing controls is last operation in CLOSE state + + // snap the handle + + this->_endpointSnapHandle(p, mevent.state); + + if (!this->polylines_only) { + this->_setCtrl(p, mevent.state); + } else { + this->_setCtrl(this->p[1], mevent.state); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + ret = true; + break; + case PenTool::STOP: + // Don't break; fall through to default to do preSnapping + default: + if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + break; + default: + break; + } + // calls the function "bspline_spiro_motion" when the mouse starts or stops moving + if(this->bspline){ + this->_bsplineSpiroMotion(mevent.state); + }else{ + if ( Geom::LInfty( event_w - pen_drag_origin_w ) > (tolerance/2) || mevent.time == 0) { + this->_bsplineSpiroMotion(mevent.state); + pen_drag_origin_w = event_w; + } + } + + return ret; +} + +/** + * Handle mouse button release event. + */ +bool PenTool::_handleButtonRelease(GdkEventButton const &revent) { + if (this->events_disabled) { + // skip event processing if events are disabled + return false; + } + + bool ret = false; + + if (revent.button == 1 && !this->space_panning) { + Geom::Point const event_w(revent.x, revent.y); + + // Find desktop coordinates + Geom::Point p = this->desktop->w2d(event_w); + + // Test whether we hit any anchor. + + SPDrawAnchor *anchor = spdc_test_inside(this, event_w); + // if we try to create a node in the same place as another node, we skip + if((!anchor || anchor == this->sa) && (this->spiro || this->bspline) && this->npoints > 0 && this->p[0] == this->p[3]){ + return true; + } + + switch (this->mode) { + case PenTool::MODE_CLICK: + switch (this->state) { + case PenTool::POINT: + this->ea = anchor; + if (anchor) { + p = anchor->dp; + } + this->state = PenTool::CONTROL; + break; + case PenTool::CONTROL: + // End current segment + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + this->state = PenTool::POINT; + break; + case PenTool::CLOSE: + // End current segment + if (!anchor) { // Snap node only if not hitting anchor + this->_endpointSnap(p, revent.state); + } + this->_finishSegment(p, revent.state); + // hude the guide of the penultimate node when closing the curve + if(this->spiro){ + sp_canvas_item_hide(this->c1); + } + this->_finish(true); + this->state = PenTool::POINT; + break; + case PenTool::STOP: + // This is allowed, if we just canceled curve + this->state = PenTool::POINT; + break; + default: + break; + } + break; + case PenTool::MODE_DRAG: + switch (this->state) { + case PenTool::POINT: + case PenTool::CONTROL: + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + break; + case PenTool::CLOSE: + this->_endpointSnap(p, revent.state); + this->_finishSegment(p, revent.state); + // hide the penultimate node guide when closing the curve + if(this->spiro){ + sp_canvas_item_hide(this->c1); + } + if (this->green_closed) { + // finishing at the start anchor, close curve + this->_finish(true); + } else { + // finishing at some other anchor, finish curve but not close + this->_finish(false); + } + break; + case PenTool::STOP: + // This is allowed, if we just cancelled curve + break; + default: + break; + } + this->state = PenTool::POINT; + break; + default: + break; + } + if (this->grab) { + // Release grab now + sp_canvas_item_ungrab(this->grab); + this->grab = nullptr; + } + + ret = true; + + this->green_closed = false; + } + + // TODO: can we be sure that the path was created correctly? + // TODO: should we offer an option to collect the clicks in a list? + if (this->expecting_clicks_for_LPE == 0 && this->hasWaitingLPE()) { + this->setPolylineMode(); + + Inkscape::Selection *selection = this->desktop->getSelection(); + + if (this->waiting_LPE) { + // we have an already created LPE waiting for a path + this->waiting_LPE->acceptParamPath(SP_PATH(selection->singleItem())); + selection->add(this->waiting_item); + this->waiting_LPE = nullptr; + } else { + // the case that we need to create a new LPE and apply it to the just-drawn path is + // handled in spdc_check_for_and_apply_waiting_LPE() in draw-context.cpp + } + } + + return ret; +} + +bool PenTool::_handle2ButtonPress(GdkEventButton const &bevent) { + bool ret = false; + // only end on LMB double click. Otherwise horizontal scrolling causes ending of the path + if (this->npoints != 0 && bevent.button == 1 && this->state != PenTool::CLOSE) { + this->_finish(false); + ret = true; + } + return ret; +} + +void PenTool::_redrawAll() { + // green + if (! this->green_bpaths.empty()) { + // remove old piecewise green canvasitems + for (auto i : this->green_bpaths){ + sp_canvas_item_destroy(i); + } + this->green_bpaths.clear(); + // one canvas bpath for all of green_curve + SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO); + + this->green_bpaths.push_back(canvas_shape); + } + if (this->green_anchor) { + SP_CTRL(this->green_anchor->ctrl)->moveto(this->green_anchor->dp); + } + + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + this->red_curve->curveto(this->p[1], this->p[2], this->p[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); + + // handles + // hide the handlers in bspline and spiro modes + if (this->p[0] != this->p[1] && !this->spiro && !this->bspline) { + SP_CTRL(this->c1)->moveto(this->p[1]); + this->cl1->setCoords(this->p[0], this->p[1]); + sp_canvas_item_show(this->c1); + sp_canvas_item_show(this->cl1); + } else { + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl1); + } + + Geom::Curve const * last_seg = this->green_curve->last_segment(); + if (last_seg) { + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( last_seg ); + // hide the handlers in bspline and spiro modes + if ( cubic && + (*cubic)[2] != this->p[0] && !this->spiro && !this->bspline ) + { + Geom::Point p2 = (*cubic)[2]; + SP_CTRL(this->c0)->moveto(p2); + this->cl0->setCoords(p2, this->p[0]); + sp_canvas_item_show(this->c0); + sp_canvas_item_show(this->cl0); + } else { + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->cl0); + } + } + + // simply redraw the spiro. because its a redrawing, we don't call the global function, + // but we call the redrawing at the ending. + this->_bsplineSpiroBuild(); +} + +void PenTool::_lastpointMove(gdouble x, gdouble y) { + if (this->npoints != 5) + return; + + y *= -this->desktop->yaxisdir(); + + // green + if (!this->green_curve->is_unset()) { + this->green_curve->last_point_additive_move( Geom::Point(x,y) ); + } else { + // start anchor too + if (this->green_anchor) { + this->green_anchor->dp += Geom::Point(x, y); + } + } + + // red + + this->p[0] += Geom::Point(x, y); + this->p[1] += Geom::Point(x, y); + this->_redrawAll(); +} + +void PenTool::_lastpointMoveScreen(gdouble x, gdouble y) { + this->_lastpointMove(x / this->desktop->current_zoom(), y / this->desktop->current_zoom()); +} + +void PenTool::_lastpointToCurve() { + // avoid that if the "red_curve" contains only two points ( rect ), it doesn't stop here. + if (this->npoints != 5 && !this->spiro && !this->bspline) + return; + + this->p[1] = this->red_curve->last_segment()->initialPoint() + (1./3.)*(*this->red_curve->last_point() - this->red_curve->last_segment()->initialPoint()); + //modificate the last segment of the green curve so it creates the type of node we need + if (this->spiro||this->bspline) { + if (!this->green_curve->is_unset()) { + Geom::Point A(0,0); + Geom::Point B(0,0); + Geom::Point C(0,0); + Geom::Point D(0,0); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() ); + //We obtain the last segment 4 points in the previous curve + if ( cubic ){ + A = (*cubic)[0]; + B = (*cubic)[1]; + if (this->spiro) { + C = this->p[0] + (this->p[0] - this->p[1]); + } else { + C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point()); + } + D = (*cubic)[3]; + } else { + A = this->green_curve->last_segment()->initialPoint(); + B = this->green_curve->last_segment()->initialPoint(); + if (this->spiro) { + C = this->p[0] + (this->p[0] - this->p[1]); + } else { + C = *this->green_curve->last_point() + (1./3.)*(this->green_curve->last_segment()->initialPoint() - *this->green_curve->last_point()); + } + D = *this->green_curve->last_point(); + } + SPCurve *previous = new SPCurve(); + previous->moveto(A); + previous->curveto(B, C, D); + if ( this->green_curve->get_segment_count() == 1) { + this->green_curve = previous; + } else { + //we eliminate the last segment + this->green_curve->backspace(); + //and we add it again with the recreation + this->green_curve->append_continuous(previous, 0.0625); + } + } + //if the last node is an union with another curve + if (this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()) { + this->_bsplineSpiroStartAnchor(false); + } + } + + this->_redrawAll(); +} + + +void PenTool::_lastpointToLine() { + // avoid that if the "red_curve" contains only two points ( rect) it doesn't stop here. + if (this->npoints != 5 && !this->bspline) + return; + + // modify the last segment of the green curve so the type of node we want is created. + if(this->spiro || this->bspline){ + if(!this->green_curve->is_unset()){ + Geom::Point A(0,0); + Geom::Point B(0,0); + Geom::Point C(0,0); + Geom::Point D(0,0); + SPCurve * previous = new SPCurve(); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>( this->green_curve->last_segment() ); + if ( cubic ) { + A = this->green_curve->last_segment()->initialPoint(); + B = (*cubic)[1]; + C = *this->green_curve->last_point(); + D = C; + } else { + //We obtain the last segment 4 points in the previous curve + A = this->green_curve->last_segment()->initialPoint(); + B = A; + C = *this->green_curve->last_point(); + D = C; + } + previous->moveto(A); + previous->curveto(B, C, D); + if( this->green_curve->get_segment_count() == 1){ + this->green_curve = previous; + }else{ + //we eliminate the last segment + this->green_curve->backspace(); + //and we add it again with the recreation + this->green_curve->append_continuous(previous, 0.0625); + } + } + // if the last node is an union with another curve + if(this->green_curve->is_unset() && this->sa && !this->sa->curve->is_unset()){ + this->_bsplineSpiroStartAnchor(true); + } + } + + this->p[1] = this->p[0]; + this->_redrawAll(); +} + + +bool PenTool::_handleKeyPress(GdkEvent *event) { + bool ret = false; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + + // Check for undo if we have started drawing a path. + if (this->npoints > 0) { + unsigned int shortcut = sp_shortcut_get_for_event((GdkEventKey*)event); + Inkscape::Verb* verb = sp_shortcut_get_verb(shortcut); + if (verb) { + unsigned int vcode = verb->get_code(); + if (vcode == SP_VERB_EDIT_UNDO) + return _undoLastPoint(); + } + } + + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Left: // move last point left + case GDK_KEY_KP_Left: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(-10, 0); // shift + } + else { + this->_lastpointMoveScreen(-1, 0); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(-10*nudge, 0); // shift + } + else { + this->_lastpointMove(-nudge, 0); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Up: // move last point up + case GDK_KEY_KP_Up: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(0, 10); // shift + } + else { + this->_lastpointMoveScreen(0, 1); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(0, 10*nudge); // shift + } + else { + this->_lastpointMove(0, nudge); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Right: // move last point right + case GDK_KEY_KP_Right: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(10, 0); // shift + } + else { + this->_lastpointMoveScreen(1, 0); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(10*nudge, 0); // shift + } + else { + this->_lastpointMove(nudge, 0); // no shift + } + } + ret = true; + } + break; + case GDK_KEY_Down: // move last point down + case GDK_KEY_KP_Down: + if (!MOD__CTRL(event)) { // not ctrl + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + this->_lastpointMoveScreen(0, -10); // shift + } + else { + this->_lastpointMoveScreen(0, -1); // no shift + } + } + else { // no alt + if (MOD__SHIFT(event)) { + this->_lastpointMove(0, -10*nudge); // shift + } + else { + this->_lastpointMove(0, -nudge); // no shift + } + } + ret = true; + } + break; + +/*TODO: this is not yet enabled?? looks like some traces of the Geometry tool + case GDK_KEY_P: + case GDK_KEY_p: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PARALLEL, 2); + ret = true; + } + break; + + case GDK_KEY_C: + case GDK_KEY_c: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::CIRCLE_3PTS, 3); + ret = true; + } + break; + + case GDK_KEY_B: + case GDK_KEY_b: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::PERP_BISECTOR, 2); + ret = true; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__SHIFT_ONLY(event)) { + sp_pen_context_wait_for_LPE_mouse_clicks(pc, Inkscape::LivePathEffect::ANGLE_BISECTOR, 3); + ret = true; + } + break; +*/ + + case GDK_KEY_U: + case GDK_KEY_u: + if (MOD__SHIFT_ONLY(event)) { + this->_lastpointToCurve(); + ret = true; + } + break; + case GDK_KEY_L: + case GDK_KEY_l: + if (MOD__SHIFT_ONLY(event)) { + this->_lastpointToLine(); + ret = true; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + if (this->npoints != 0) { + this->ea = nullptr; // unset end anchor if set (otherwise crashes) + if(MOD__SHIFT_ONLY(event)) { + // All this is needed to stop the last control + // point dispeating and stop making an n-1 shape. + Geom::Point const p(0, 0); + if(this->red_curve->is_unset()) { + this->red_curve->moveto(p); + } + this->_finishSegment(p, 0); + this->_finish(true); + } else { + this->_finish(false); + } + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + this->_cancel (); + ret = true; + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + this->desktop->selection->toGuides(); + ret = true; + } + break; + case GDK_KEY_BackSpace: + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + ret = _undoLastPoint(); + break; + default: + break; + } + return ret; +} + +void PenTool::_resetColors() { + // Red + this->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr, true); + // Blue + this->blue_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->blue_bpath), nullptr, true); + // Green + for (auto i:this->green_bpaths) { + sp_canvas_item_destroy(i); + } + this->green_bpaths.clear(); + this->green_curve->reset(); + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + this->sa = nullptr; + this->ea = nullptr; + this->sa_overwrited->reset(); + + this->npoints = 0; + this->red_curve_is_valid = false; +} + + +void PenTool::_setInitialPoint(Geom::Point const p) { + g_assert( this->npoints == 0 ); + + this->p[0] = p; + this->p[1] = p; + this->npoints = 2; + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr, true); + + this->desktop->canvas->forceFullRedrawAfterInterruptions(5); +} + +/** + * Show the status message for the current line/curve segment. + * This type of message always shows angle/distance as the last + * two parameters ("angle %3.2f°, distance %s"). + */ +void PenTool::_setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message) { + g_assert((pc_point_to_compare == 0) || (pc_point_to_compare == 3)); // exclude control handles + g_assert(message != nullptr); + + Geom::Point rel = p - this->p[pc_point_to_compare]; + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(Geom::L2(rel), "px"); + Glib::ustring dist = q.string(desktop->namedview->display_units); + double angle = atan2(rel[Geom::Y], rel[Geom::X]) * 180 / M_PI; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/compassangledisplay/value", false) != 0) { + angle = 90 - angle; + + if (desktop->is_yaxisdown()) { + angle = 180 - angle; + } + + if (angle < 0) { + angle += 360; + } + } + + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, message, angle, dist.c_str()); +} + +// this function changes the colors red, green and blue making them transparent or not, depending on if spiro is being used. +void PenTool::_bsplineSpiroColor() +{ + static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if(this->spiro){ + this->red_color = 0xff000000; + this->green_color = 0x00ff0000; + }else if(this->bspline){ + this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color(); + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0xff00007f; + this->red_color = 0xff00007f; + } else { + this->green_color = this->highlight_color; + this->red_color = this->highlight_color; + } + }else{ + this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color(); + this->red_color = 0xff00007f; + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0x00ff007f; + } else { + this->green_color = this->highlight_color; + } + sp_canvas_item_hide(this->blue_bpath); + } + //We erase all the "green_bpaths" to recreate them after with the colour + //transparency recently modified + if (!this->green_bpaths.empty()) { + // remove old piecewise green canvasitems + for (auto i:this->green_bpaths) { + sp_canvas_item_destroy(i); + } + this->green_bpaths.clear(); + // one canvas bpath for all of green_curve + SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + } + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->red_bpath), this->red_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); +} + + +void PenTool::_bsplineSpiro(bool shift) +{ + if(!this->spiro && !this->bspline){ + return; + } + + shift?this->_bsplineSpiroOff():this->_bsplineSpiroOn(); + this->_bsplineSpiroBuild(); +} + +void PenTool::_bsplineSpiroOn() +{ + if(!this->red_curve->is_unset()){ + using Geom::X; + using Geom::Y; + this->npoints = 5; + this->p[0] = *this->red_curve->first_point(); + this->p[3] = this->red_curve->first_segment()->finalPoint(); + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + } +} + +void PenTool::_bsplineSpiroOff() +{ + if(!this->red_curve->is_unset()){ + this->npoints = 5; + this->p[0] = *this->red_curve->first_point(); + this->p[3] = this->red_curve->first_segment()->finalPoint(); + this->p[2] = this->p[3]; + } +} + +void PenTool::_bsplineSpiroStartAnchor(bool shift) +{ + if(this->sa->curve->is_unset()){ + return; + } + + LivePathEffect::LPEBSpline *lpe_bsp = nullptr; + + if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){ + Inkscape::LivePathEffect::Effect* thisEffect = SP_LPE_ITEM(this->white_item)->getPathEffectOfType(Inkscape::LivePathEffect::BSPLINE); + if(thisEffect){ + lpe_bsp = dynamic_cast<LivePathEffect::LPEBSpline*>(thisEffect->getLPEObj()->get_lpe()); + } + } + if(lpe_bsp){ + this->bspline = true; + }else{ + this->bspline = false; + } + LivePathEffect::LPESpiro *lpe_spi = nullptr; + + if (SP_IS_LPE_ITEM(this->white_item) && SP_LPE_ITEM(this->white_item)->hasPathEffect()){ + Inkscape::LivePathEffect::Effect* thisEffect = SP_LPE_ITEM(this->white_item)->getPathEffectOfType(Inkscape::LivePathEffect::SPIRO); + if(thisEffect){ + lpe_spi = dynamic_cast<LivePathEffect::LPESpiro*>(thisEffect->getLPEObj()->get_lpe()); + } + } + if(lpe_spi){ + this->spiro = true; + }else{ + this->spiro = false; + } + if(!this->spiro && !this->bspline){ + _bsplineSpiroColor(); + return; + } + if(shift){ + this->_bsplineSpiroStartAnchorOff(); + } else { + this->_bsplineSpiroStartAnchorOn(); + } +} + +void PenTool::_bsplineSpiroStartAnchorOn() +{ + using Geom::X; + using Geom::Y; + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited ->last_segment()); + SPCurve *last_segment = new SPCurve(); + Geom::Point point_a = this->sa_overwrited->last_segment()->initialPoint(); + Geom::Point point_d = *this->sa_overwrited->last_point(); + Geom::Point point_c = point_d + (1./3)*(point_a - point_d); + point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP); + if(cubic){ + last_segment->moveto(point_a); + last_segment->curveto((*cubic)[1],point_c,point_d); + }else{ + last_segment->moveto(point_a); + last_segment->curveto(point_a,point_c,point_d); + } + if( this->sa_overwrited->get_segment_count() == 1){ + this->sa_overwrited = last_segment->copy(); + }else{ + //we eliminate the last segment + this->sa_overwrited->backspace(); + //and we add it again with the recreation + this->sa_overwrited->append_continuous(last_segment, 0.0625); + } + last_segment->unref(); +} + +void PenTool::_bsplineSpiroStartAnchorOff() +{ + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->sa_overwrited->last_segment()); + if(cubic){ + SPCurve *last_segment = new SPCurve(); + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + if( this->sa_overwrited->get_segment_count() == 1){ + this->sa_overwrited = last_segment->copy(); + }else{ + //we eliminate the last segment + this->sa_overwrited->backspace(); + //and we add it again with the recreation + this->sa_overwrited->append_continuous(last_segment, 0.0625); + } + last_segment->unref(); + } +} + +void PenTool::_bsplineSpiroMotion(guint const state){ + bool shift = state & GDK_SHIFT_MASK; + if(!this->spiro && !this->bspline){ + return; + } + using Geom::X; + using Geom::Y; + if(this->red_curve->is_unset()) return; + this->npoints = 5; + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + if (this->green_curve->is_unset() && !this->sa) { + this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]); + this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP, this->p[1][Y] + HANDLE_CUBIC_GAP); + if(shift){ + this->p[2] = this->p[3]; + } + } else if (!this->green_curve->is_unset()){ + tmp_curve.reset(this->green_curve->copy()); + } else { + tmp_curve.reset(this->sa_overwrited->copy()); + } + if ((state & GDK_MOD1_MASK ) && previous != Geom::Point(0,0)) { //ALT drag + this->p[0] = this->p[0] + (this->p[3] - previous); + } + if(!tmp_curve ->is_unset()){ + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if ((state & GDK_MOD1_MASK ) && + !Geom::are_near(*tmp_curve ->last_point(), this->p[0], 0.1)) + { + SPCurve * previous_weight_power = new SPCurve(); + Geom::D2< Geom::SBasis > SBasisweight_power; + previous_weight_power->moveto(tmp_curve ->last_segment()->initialPoint()); + previous_weight_power->lineto(this->p[0]); + SBasisweight_power = previous_weight_power->first_segment()->toSBasis(); + previous_weight_power->reset(); + previous_weight_power->unref(); + if( tmp_curve ->get_segment_count() == 1){ + Geom::Point initial = tmp_curve ->last_segment()->initialPoint(); + tmp_curve->reset(); + tmp_curve->moveto(initial); + }else{ + tmp_curve->backspace(); + } + if(this->bspline && cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])){ + tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), SBasisweight_power.valueAt(0.66667), this->p[0]); + } else if(this->bspline && cubic) { + tmp_curve->curveto(SBasisweight_power.valueAt(0.33334), this->p[0], this->p[0]); + } else if (cubic && !Geom::are_near((*cubic)[2],(*cubic)[3])) { + tmp_curve->curveto((*cubic)[1], (*cubic)[2] + (this->p[3] - previous), this->p[0]); + } else if (cubic){ + tmp_curve->curveto((*cubic)[1], this->p[0], this->p[0]); + } else { + tmp_curve->lineto(this->p[0]); + } + cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if (this->sa && this->green_curve->is_unset()) { + this->sa_overwrited = tmp_curve->copy(); + } + this->green_curve = tmp_curve->copy(); + } + if (cubic) { + if (this->bspline) { + SPCurve * weight_power = new SPCurve(); + Geom::D2< Geom::SBasis > SBasisweight_power; + weight_power->moveto(this->red_curve->last_segment()->initialPoint()); + weight_power->lineto(*this->red_curve->last_point()); + SBasisweight_power = weight_power->first_segment()->toSBasis(); + weight_power->reset(); + weight_power->unref(); + this->p[1] = SBasisweight_power.valueAt(0.33334); + if(!Geom::are_near(this->p[1],this->p[0])){ + this->p[1] = Geom::Point(this->p[1][X] + HANDLE_CUBIC_GAP,this->p[1][Y] + HANDLE_CUBIC_GAP); + } else { + this->p[1] = this->p[0]; + } + if (shift) { + this->p[2] = this->p[3]; + } + if(Geom::are_near((*cubic)[3], (*cubic)[2])) { + this->p[1] = this->p[0]; + } + } else { + this->p[1] = (*cubic)[3] + ((*cubic)[3] - (*cubic)[2] ); + } + } else { + this->p[1] = this->p[0]; + if (shift) { + this->p[2] = this->p[3]; + } + } + previous = *this->red_curve->last_point(); + SPCurve * red = new SPCurve(); + red->moveto(this->p[0]); + red->curveto(this->p[1],this->p[2],this->p[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), red, true); + red->reset(); + red->unref(); + } + + if(this->anchor_statusbar && !this->red_curve->is_unset()){ + if(shift){ + this->_bsplineSpiroEndAnchorOff(); + }else{ + this->_bsplineSpiroEndAnchorOn(); + } + } + if (!this->green_bpaths.empty()) { + // remove old piecewise green canvasitems + for (auto i: this->green_bpaths) { + sp_canvas_item_destroy(i); + } + this->green_bpaths.clear(); + } + // one canvas bpath for all of green_curve + SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), this->green_curve, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(canvas_shape), 0, SP_WIND_RULE_NONZERO); + this->green_bpaths.push_back(canvas_shape); + this->_bsplineSpiroBuild(); +} + +void PenTool::_bsplineSpiroEndAnchorOn() +{ + + using Geom::X; + using Geom::Y; + this->p[2] = this->p[3] + (1./3)*(this->p[0] - this->p[3]); + this->p[2] = Geom::Point(this->p[2][X] + HANDLE_CUBIC_GAP,this->p[2][Y] + HANDLE_CUBIC_GAP); + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + std::unique_ptr<SPCurve> last_segment(new SPCurve()); + Geom::Point point_c(0,0); + if( this->green_anchor && this->green_anchor->active ){ + tmp_curve.reset(this->green_curve->create_reverse()); + if(this->green_curve->get_segment_count()==0){ + return; + } + } else if(this->sa){ + tmp_curve.reset(this->sa_overwrited->copy()->create_reverse()); + }else{ + return; + } + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if(this->bspline){ + point_c = *tmp_curve ->last_point() + (1./3)*(tmp_curve ->last_segment()->initialPoint() - *tmp_curve ->last_point()); + point_c = Geom::Point(point_c[X] + HANDLE_CUBIC_GAP, point_c[Y] + HANDLE_CUBIC_GAP); + }else{ + point_c = this->p[3] + this->p[3] - this->p[2]; + } + if(cubic){ + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],point_c,(*cubic)[3]); + }else{ + last_segment->moveto(tmp_curve ->last_segment()->initialPoint()); + last_segment->lineto(*tmp_curve ->last_point()); + } + if( tmp_curve ->get_segment_count() == 1){ + tmp_curve = std::move(last_segment); + }else{ + //we eliminate the last segment + tmp_curve ->backspace(); + //and we add it again with the recreation + tmp_curve ->append_continuous(last_segment.get(), 0.0625); + } + tmp_curve.reset(tmp_curve ->create_reverse()); + if( this->green_anchor && this->green_anchor->active ) + { + this->green_curve->reset(); + this->green_curve = tmp_curve->copy(); + }else{ + this->sa_overwrited->reset(); + this->sa_overwrited = tmp_curve->copy(); + } +} + +void PenTool::_bsplineSpiroEndAnchorOff() +{ + + std::unique_ptr<SPCurve> tmp_curve(new SPCurve()); + std::unique_ptr<SPCurve> last_segment(new SPCurve()); + this->p[2] = this->p[3]; + if( this->green_anchor && this->green_anchor->active ){ + tmp_curve.reset(this->green_curve->create_reverse()); + if(this->green_curve->get_segment_count()==0){ + return; + } + } else if(this->sa){ + tmp_curve.reset(this->sa_overwrited->copy()->create_reverse()); + }else{ + return; + } + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*tmp_curve ->last_segment()); + if(cubic){ + last_segment->moveto((*cubic)[0]); + last_segment->curveto((*cubic)[1],(*cubic)[3],(*cubic)[3]); + }else{ + last_segment->moveto(tmp_curve ->last_segment()->initialPoint()); + last_segment->lineto(*tmp_curve ->last_point()); + } + if( tmp_curve ->get_segment_count() == 1){ + tmp_curve = std::move(last_segment); + }else{ + //we eliminate the last segment + tmp_curve ->backspace(); + //and we add it again with the recreation + tmp_curve ->append_continuous(last_segment.get(), 0.0625); + } + tmp_curve.reset(tmp_curve ->create_reverse()); + + if( this->green_anchor && this->green_anchor->active ) + { + this->green_curve->reset(); + this->green_curve = tmp_curve->copy(); + }else{ + this->sa_overwrited->reset(); + this->sa_overwrited = tmp_curve->copy(); + } +} + +//prepares the curves for its transformation into BSpline curve. +void PenTool::_bsplineSpiroBuild() +{ + if(!this->spiro && !this->bspline){ + return; + } + + //We create the base curve + SPCurve *curve = new SPCurve(); + //If we continuate the existing curve we add it at the start + if(this->sa && !this->sa->curve->is_unset()){ + delete curve; + curve = this->sa_overwrited->copy(); + } + + if (!this->green_curve->is_unset()){ + curve->append_continuous(this->green_curve, 0.0625); + } + + //and the red one + if (!this->red_curve->is_unset()){ + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + if(this->anchor_statusbar && !this->sa && !(this->green_anchor && this->green_anchor->active)){ + this->red_curve->curveto(this->p[1],this->p[3],this->p[3]); + }else{ + this->red_curve->curveto(this->p[1],this->p[2],this->p[3]); + } + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); + curve->append_continuous(this->red_curve, 0.0625); + } + previous = *this->red_curve->last_point(); + if(!curve->is_unset()){ + // close the curve if the final points of the curve are close enough + if(Geom::are_near(curve->first_path()->initialPoint(), curve->last_path()->finalPoint())){ + curve->closepath_current(); + } + //TODO: CALL TO CLONED FUNCTION SPIRO::doEffect IN lpe-spiro.cpp + //For example + //using namespace Inkscape::LivePathEffect; + //LivePathEffectObject *lpeobj = static_cast<LivePathEffectObject*> (curve); + //Effect *spr = static_cast<Effect*> ( new LPEbspline(lpeobj) ); + //spr->doEffect(curve); + if (this->bspline) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(curve, 0, hp); + } else { + LivePathEffect::sp_spiro_do_effect(curve); + } + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->blue_bpath), curve, true); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->blue_bpath), this->blue_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_show(this->blue_bpath); + curve->unref(); + this->blue_curve->reset(); + //We hide the holders that doesn't contribute anything + if(this->spiro){ + sp_canvas_item_show(this->c1); + SP_CTRL(this->c1)->moveto(this->p[0]); + }else + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl1); + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->cl0); + }else{ + //if the curve is empty + sp_canvas_item_hide(this->blue_bpath); + + } +} + +void PenTool::_setSubsequentPoint(Geom::Point const p, bool statusbar, guint status) { + g_assert( this->npoints != 0 ); + + // todo: Check callers to see whether 2 <= npoints is guaranteed. + + this->p[2] = p; + this->p[3] = p; + this->p[4] = p; + this->npoints = 5; + this->red_curve->reset(); + bool is_curve; + this->red_curve->moveto(this->p[0]); + if (this->polylines_paraxial && !statusbar) { + // we are drawing horizontal/vertical lines and hit an anchor; + Geom::Point const origin = this->p[0]; + // if the previous point and the anchor are not aligned either horizontally or vertically... + if ((std::abs(p[Geom::X] - origin[Geom::X]) > 1e-9) && (std::abs(p[Geom::Y] - origin[Geom::Y]) > 1e-9)) { + // ...then we should draw an L-shaped path, consisting of two paraxial segments + Geom::Point intermed = p; + this->_setToNearestHorizVert(intermed, status); + this->red_curve->lineto(intermed); + } + this->red_curve->lineto(p); + is_curve = false; + } else { + // one of the 'regular' modes + if (this->p[1] != this->p[0] || this->spiro) { + this->red_curve->curveto(this->p[1], p, p); + is_curve = true; + } else { + this->red_curve->lineto(p); + is_curve = false; + } + } + + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); + + if (statusbar) { + gchar *message; + if(this->spiro || this->bspline){ + message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°; with <b>Shift+Click</b> cusp node,<b>ALT</b> move previous, <b>Enter</b> or <b>Shift+Enter</b> to finish" ): + _("<b>Line segment</b>: angle %3.2f°; with <b>Shift+Click</b> cusp node,<b>ALT</b> move previous, <b>Enter</b> or <b>Shift+Enter</b> to finish"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } else { + message = is_curve ? + _("<b>Curve segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path" ): + _("<b>Line segment</b>: angle %3.2f°, distance %s; with <b>Ctrl</b> to snap angle, <b>Enter</b> or <b>Shift+Enter</b> to finish the path"); + this->_setAngleDistanceStatusMessage(p, 0, message); + } + + } +} + + +void PenTool::_setCtrl(Geom::Point const p, guint const state) { + sp_canvas_item_show(this->c1); + sp_canvas_item_show(this->cl1); + + if ( this->npoints == 2 ) { + this->p[1] = p; + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->cl0); + SP_CTRL(this->c1)->moveto(this->p[1]); + this->cl1->setCoords(this->p[0], this->p[1]); + this->_setAngleDistanceStatusMessage(p, 0, _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle")); + } else if ( this->npoints == 5 ) { + this->p[4] = p; + sp_canvas_item_show(this->c0); + sp_canvas_item_show(this->cl0); + bool is_symm = false; + if ( ( ( this->mode == PenTool::MODE_CLICK ) && ( state & GDK_CONTROL_MASK ) ) || + ( ( this->mode == PenTool::MODE_DRAG ) && !( state & GDK_SHIFT_MASK ) ) ) { + Geom::Point delta = p - this->p[3]; + this->p[2] = this->p[3] - delta; + is_symm = true; + this->red_curve->reset(); + this->red_curve->moveto(this->p[0]); + this->red_curve->curveto(this->p[1], this->p[2], this->p[3]); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve, true); + } + SP_CTRL(this->c0)->moveto(this->p[2]); + this->cl0 ->setCoords(this->p[3], this->p[2]); + SP_CTRL(this->c1)->moveto(this->p[4]); + this->cl1->setCoords(this->p[3], this->p[4]); + + + + gchar *message = is_symm ? + _("<b>Curve handle, symmetric</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only") : + _("<b>Curve handle</b>: angle %3.2f°, length %s; with <b>Ctrl</b> to snap angle, with <b>Shift</b> to move this handle only"); + this->_setAngleDistanceStatusMessage(p, 3, message); + } else { + g_warning("Something bad happened - npoints is %d", this->npoints); + } +} + +void PenTool::_finishSegment(Geom::Point const p, guint const state) { + if (this->polylines_paraxial) { + this->nextParaxialDirection(p, this->p[0], state); + } + + ++num_clicks; + + + if (!this->red_curve->is_unset()) { + this->_bsplineSpiro(state & GDK_SHIFT_MASK); + if(!this->green_curve->is_unset() && + !Geom::are_near(*this->green_curve->last_point(),this->p[0])) + { + std::unique_ptr<SPCurve> lsegment(new SPCurve()); + Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const*>(&*this->green_curve->last_segment()); + if (cubic) { + lsegment->moveto((*cubic)[0]); + lsegment->curveto((*cubic)[1], this->p[0] - ((*cubic)[2] - (*cubic)[3]), *this->red_curve->first_point()); + this->green_curve->backspace(); + this->green_curve->append_continuous(lsegment.get(), 0.0625); + } + } + this->green_curve->append_continuous(this->red_curve, 0.0625); + SPCurve *curve = this->red_curve->copy(); + + /// \todo fixme: + SPCanvasItem *canvas_shape = sp_canvas_bpath_new(this->desktop->getSketch(), curve, true); + curve->unref(); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(canvas_shape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + this->green_bpaths.push_back(canvas_shape); + + this->p[0] = this->p[3]; + this->p[1] = this->p[4]; + this->npoints = 2; + + this->red_curve->reset(); + } +} + +// Partial fix for https://bugs.launchpad.net/inkscape/+bug/171990 +// TODO: implement the redo feature +bool PenTool::_undoLastPoint() { + bool ret = false; + + if ( this->green_curve->is_unset() || (this->green_curve->last_segment() == nullptr) ) { + if (!this->red_curve->is_unset()) { + this->_cancel (); + ret = true; + } else { + // do nothing; this event should be handled upstream + } + } else { + // Reset red curve + this->red_curve->reset(); + // Get last segment + if ( this->green_curve->is_unset() ) { + g_warning("pen_handle_key_press, case GDK_KP_Delete: Green curve is empty"); + return false; + } + // The code below assumes that this->green_curve has only ONE path ! + Geom::Curve const * crv = this->green_curve->last_segment(); + this->p[0] = crv->initialPoint(); + if ( Geom::CubicBezier const * cubic = dynamic_cast<Geom::CubicBezier const *>(crv)) { + this->p[1] = (*cubic)[1]; + + } else { + this->p[1] = this->p[0]; + } + + // assign the value in a third of the distance of the last segment. + if (this->bspline){ + this->p[1] = this->p[0] + (1./3)*(this->p[3] - this->p[0]); + } + + Geom::Point const pt( (this->npoints < 4) ? crv->finalPoint() : this->p[3] ); + + this->npoints = 2; + // delete the last segment of the green curve and green bpath + if (this->green_curve->get_segment_count() == 1) { + this->npoints = 5; + if (!this->green_bpaths.empty()) { + sp_canvas_item_destroy(this->green_bpaths.back()); + this->green_bpaths.pop_back(); + } + this->green_curve->reset(); + } else { + this->green_curve->backspace(); + if (this->green_bpaths.size() > 1) { + sp_canvas_item_destroy(this->green_bpaths.back()); + this->green_bpaths.pop_back(); + } else if (this->green_bpaths.size() == 1) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(green_bpaths.back()), this->green_curve, true); + } + } + + // assign the value of this->p[1] to the opposite of the green line last segment + if (this->spiro){ + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(this->green_curve->last_segment()); + if ( cubic ) { + this->p[1] = (*cubic)[3] + (*cubic)[3] - (*cubic)[2]; + SP_CTRL(this->c1)->moveto(this->p[0]); + } else { + this->p[1] = this->p[0]; + } + } + + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl0); + sp_canvas_item_hide(this->cl1); + this->state = PenTool::POINT; + + if(this->polylines_paraxial) { + // We compare the point we're removing with the nearest horiz/vert to + // see if the line was added with SHIFT or not. + Geom::Point compare(pt); + this->_setToNearestHorizVert(compare, 0); + if ((std::abs(compare[Geom::X] - pt[Geom::X]) > 1e-9) + || (std::abs(compare[Geom::Y] - pt[Geom::Y]) > 1e-9)) { + this->paraxial_angle = this->paraxial_angle.cw(); + } + } + this->_setSubsequentPoint(pt, true); + + //redraw + this->_bsplineSpiroBuild(); + ret = true; + } + + return ret; +} + +void PenTool::_finish(gboolean const closed) { + if (this->expecting_clicks_for_LPE > 1) { + // don't let the path be finished before we have collected the required number of mouse clicks + return; + } + + + this->num_clicks = 0; + + this->_disableEvents(); + + this->message_context->clear(); + + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Drawing finished")); + + // cancelate line without a created segment + this->red_curve->reset(); + spdc_concat_colors_and_flush(this, closed); + this->sa = nullptr; + this->ea = nullptr; + + this->npoints = 0; + this->state = PenTool::POINT; + + sp_canvas_item_hide(this->c0); + sp_canvas_item_hide(this->c1); + sp_canvas_item_hide(this->cl0); + sp_canvas_item_hide(this->cl1); + + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + + this->desktop->canvas->endForcedFullRedraws(); + + this->_enableEvents(); +} + +void PenTool::_disableEvents() { + this->events_disabled = true; +} + +void PenTool::_enableEvents() { + g_return_if_fail(this->events_disabled != 0); + + this->events_disabled = false; +} + +void PenTool::waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines) { + if (effect_type == Inkscape::LivePathEffect::INVALID_LPE) + return; + + this->waiting_LPE_type = effect_type; + this->expecting_clicks_for_LPE = num_clicks; + this->polylines_only = use_polylines; + this->polylines_paraxial = false; // TODO: think if this is correct for all cases +} + +void PenTool::nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state) { + // + // after the first mouse click we determine whether the mouse pointer is closest to a + // horizontal or vertical segment; for all subsequent mouse clicks, we use the direction + // orthogonal to the last one; pressing Shift toggles the direction + // + // num_clicks is not reliable because spdc_pen_finish_segment is sometimes called too early + // (on first mouse release), in which case num_clicks immediately becomes 1. + // if (this->num_clicks == 0) { + + if (this->green_curve->is_unset()) { + // first mouse click + double h = pt[Geom::X] - origin[Geom::X]; + double v = pt[Geom::Y] - origin[Geom::Y]; + this->paraxial_angle = Geom::Point(h, v).ccw(); + } + if(!(state & GDK_SHIFT_MASK)) { + this->paraxial_angle = this->paraxial_angle.ccw(); + } +} + +void PenTool::_setToNearestHorizVert(Geom::Point &pt, guint const state) const { + Geom::Point const origin = this->p[0]; + Geom::Point const target = (state & GDK_SHIFT_MASK) ? this->paraxial_angle : this->paraxial_angle.ccw(); + + // Create a horizontal or vertical constraint line + Inkscape::Snapper::SnapConstraint cl(origin, target); + + // Snap along the constraint line; if we didn't snap then still the constraint will be applied + SnapManager &m = this->desktop->namedview->snap_manager; + + Inkscape::Selection *selection = this->desktop->getSelection(); + // selection->singleItem() is the item that is currently being drawn. This item will not be snapped to (to avoid self-snapping) + // TODO: Allow snapping to the stationary parts of the item, and only ignore the last segment + + m.setup(this->desktop, true, selection->singleItem()); + m.constrainedSnapReturnByRef(pt, Inkscape::SNAPSOURCE_NODE_HANDLE, cl); + m.unSetup(); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/pen-tool.h b/src/ui/tools/pen-tool.h new file mode 100644 index 0000000..388bb3d --- /dev/null +++ b/src/ui/tools/pen-tool.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * PenTool: a context for pen tool events. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_PEN_CONTEXT_H +#define SEEN_PEN_CONTEXT_H + + + +#include "ui/tools/freehand-base.h" +#include "live_effects/effect.h" + +#define SP_PEN_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PenTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_PEN_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PenTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +struct SPCtrlLine; + +namespace Inkscape { +namespace UI { +namespace Tools { + +/** + * PenTool: a context for pen tool events. + */ +class PenTool : public FreehandBase { +public: + PenTool(); + PenTool(gchar const *const *cursor_shape); + ~PenTool() override; + + enum Mode { + MODE_CLICK, + MODE_DRAG + }; + + enum State { + POINT, + CONTROL, + CLOSE, + STOP + }; + + Geom::Point p[5]; + Geom::Point previous; + /** \invar npoints in {0, 2, 5}. */ + // npoints somehow determines the type of the node (what does it mean, exactly? the number of Bezier handles?) + gint npoints; + + Mode mode; + State state; + bool polylines_only; + bool polylines_paraxial; + Geom::Point paraxial_angle; + + // propiety which saves if Spiro mode is active or not + bool spiro; + bool bspline; + int num_clicks; + + unsigned int expecting_clicks_for_LPE; // if positive, finish the path after this many clicks + Inkscape::LivePathEffect::Effect *waiting_LPE; // if NULL, waiting_LPE_type in SPDrawContext is taken into account + SPLPEItem *waiting_item; + + SPCanvasItem *c0; + SPCanvasItem *c1; + + SPCtrlLine *cl0; + SPCtrlLine *cl1; + + bool events_disabled; + + static const std::string prefsPath; + + const std::string& getPrefsPath() override; + + void nextParaxialDirection(Geom::Point const &pt, Geom::Point const &origin, guint state); + void setPolylineMode(); + bool hasWaitingLPE(); + void waitForLPEMouseClicks(Inkscape::LivePathEffect::EffectType effect_type, unsigned int num_clicks, bool use_polylines = true); + +protected: + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + +private: + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handle2ButtonPress(GdkEventButton const &bevent); + bool _handleKeyPress(GdkEvent *event); + //this function changes the colors red, green and blue making them transparent or not depending on if the function uses spiro + void _bsplineSpiroColor(); + //creates a node in bspline or spiro modes + void _bsplineSpiro(bool shift); + //creates a node in bspline or spiro modes + void _bsplineSpiroOn(); + //creates a CUSP node + void _bsplineSpiroOff(); + //continues the existing curve in bspline or spiro mode + void _bsplineSpiroStartAnchor(bool shift); + //continues the existing curve with the union node in bspline or spiro modes + void _bsplineSpiroStartAnchorOn(); + //continues an existing curve with the union node in CUSP mode + void _bsplineSpiroStartAnchorOff(); + //modifies the "red_curve" when it detects movement + void _bsplineSpiroMotion(guint const state); + //closes the curve with the last node in bspline or spiro mode + void _bsplineSpiroEndAnchorOn(); + //closes the curve with the last node in CUSP mode + void _bsplineSpiroEndAnchorOff(); + //apply the effect + void _bsplineSpiroBuild(); + + void _setInitialPoint(Geom::Point const p); + void _setSubsequentPoint(Geom::Point const p, bool statusbar, guint status = 0); + void _setCtrl(Geom::Point const p, guint state); + void _finishSegment(Geom::Point p, guint state); + bool _undoLastPoint(); + + void _finish(gboolean closed); + + void _resetColors(); + + void _disableEvents(); + void _enableEvents(); + + void _setToNearestHorizVert(Geom::Point &pt, guint const state) const; + + void _setAngleDistanceStatusMessage(Geom::Point const p, int pc_point_to_compare, gchar const *message); + + void _lastpointToLine(); + void _lastpointToCurve(); + void _lastpointMoveScreen(gdouble x, gdouble y); + void _lastpointMove(gdouble x, gdouble y); + void _redrawAll(); + + void _endpointSnapHandle(Geom::Point &p, guint const state) const; + void _endpointSnap(Geom::Point &p, guint const state) const; + + void _cancel(); +}; + +} +} +} + +#endif /* !SEEN_PEN_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/pencil-tool.cpp b/src/ui/tools/pencil-tool.cpp new file mode 100644 index 0000000..7bb0275 --- /dev/null +++ b/src/ui/tools/pencil-tool.cpp @@ -0,0 +1,1239 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Pencil event context implementation. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2004 Monash University + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> + +#include "ui/tools/pencil-tool.h" +#include <2geom/bezier-utils.h> +#include <2geom/circle.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/svg-path-parser.h> + +#include "desktop.h" +#include "inkscape.h" + +#include "context-fns.h" +#include "desktop-style.h" +#include "message-context.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "snap.h" + +#include "display/canvas-bpath.h" +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "livarot/Path.h" // Simplify paths + +#include "live_effects/lpe-powerstroke-interpolators.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpeobject.h" + +#include "object/sp-lpe-item.h" +#include "object/sp-path.h" +#include "splivarot.h" +#include "style.h" + +#include "ui/pixmaps/cursor-pencil.xpm" + +#include "svg/svg.h" + +#include "ui/draw-anchor.h" +#include "ui/tool/event-utils.h" + +#include "xml/node.h" +#include "xml/sp-css-attr.h" +#include <glibmm/i18n.h> +// #include <thread> +// #include <chrono> + +namespace Inkscape { +namespace UI { +namespace Tools { + +static Geom::Point pencil_drag_origin_w(0, 0); +static bool pencil_within_tolerance = false; + +static bool in_svg_plane(Geom::Point const &p) { return Geom::LInfty(p) < 1e18; } +const double HANDLE_CUBIC_GAP = 0.01; + +const std::string& PencilTool::getPrefsPath() { + return PencilTool::prefsPath; +} + +const std::string PencilTool::prefsPath = "/tools/freehand/pencil"; + +PencilTool::PencilTool() + : FreehandBase(cursor_pencil_xpm) + , p() + , _npoints(0) + , _state(SP_PENCIL_CONTEXT_IDLE) + , _req_tangent(0, 0) + , _is_drawing(false) + , sketch_n(0) + , _curve(nullptr) + , _pressure_curve(nullptr) +{ +} + +void PencilTool::setup() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/freehand/pencil/selcue")) { + this->enableSelectionCue(); + } + this->_curve = new SPCurve(); + this->_pressure_curve = new SPCurve(); + + FreehandBase::setup(); + + this->_is_drawing = false; + this->anchor_statusbar = false; +} + + + +PencilTool::~PencilTool() { + if (this->_curve) { + this->_curve->unref(); + } + if (this->_pressure_curve) { + this->_pressure_curve->unref(); + } +} + +void PencilTool::_extinput(GdkEvent *event) { + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &this->pressure)) { + this->pressure = CLAMP (this->pressure, DDC_MIN_PRESSURE, DDC_MAX_PRESSURE); + is_tablet = true; + } else { + this->pressure = DDC_DEFAULT_PRESSURE; + is_tablet = false; + } +} + +/** Snaps new node relative to the previous node. */ +void PencilTool::_endpointSnap(Geom::Point &p, guint const state) { + if ((state & GDK_CONTROL_MASK)) { //CTRL enables constrained snapping + if (this->_npoints > 0) { + spdc_endpoint_snap_rotation(this, p, this->p[0], state); + } + } else { + if (!(state & GDK_SHIFT_MASK)) { //SHIFT disables all snapping, except the angular snapping above + //After all, the user explicitly asked for angular snapping by + //pressing CTRL + boost::optional<Geom::Point> origin = this->_npoints > 0 ? this->p[0] : boost::optional<Geom::Point>(); + spdc_endpoint_snap_free(this, p, origin, state); + } + } +} + +/** + * Callback for handling all pencil context events. + */ +bool PencilTool::root_handler(GdkEvent* event) { + bool ret = false; + this->_extinput(event); + switch (event->type) { + case GDK_BUTTON_PRESS: + ret = this->_handleButtonPress(event->button); + break; + + case GDK_MOTION_NOTIFY: + ret = this->_handleMotionNotify(event->motion); + break; + + case GDK_BUTTON_RELEASE: + ret = this->_handleButtonRelease(event->button); + break; + + case GDK_KEY_PRESS: + ret = this->_handleKeyPress(event->key); + break; + + case GDK_KEY_RELEASE: + ret = this->_handleKeyRelease(event->key); + break; + + default: + break; + } + if (!ret) { + ret = FreehandBase::root_handler(event); + } + + return ret; +} + +bool PencilTool::_handleButtonPress(GdkEventButton const &bevent) { + bool ret = false; + if ( bevent.button == 1 && !this->space_panning) { + Inkscape::Selection *selection = desktop->getSelection(); + + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return true; + } + + if (!this->grab) { + /* Grab mouse, so release will not pass unnoticed */ + this->grab = SP_CANVAS_ITEM(desktop->acetate); + sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + nullptr, bevent.time); + } + + Geom::Point const button_w(bevent.x, bevent.y); + + /* Find desktop coordinates */ + Geom::Point p = this->desktop->w2d(button_w); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, button_w); + if (tablet_enabled) { + anchor = nullptr; + } + pencil_drag_origin_w = Geom::Point(bevent.x,bevent.y); + pencil_within_tolerance = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tablet_enabled = prefs->getBool("/tools/freehand/pencil/pressure", false); + switch (this->_state) { + case SP_PENCIL_CONTEXT_ADDLINE: + /* Current segment will be finished with release */ + ret = true; + break; + default: + /* Set first point of sequence */ + SnapManager &m = desktop->namedview->snap_manager; + if (bevent.state & GDK_CONTROL_MASK) { + m.setup(desktop, true); + if (!(bevent.state & GDK_SHIFT_MASK)) { + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + spdc_create_single_dot(this, p, "/tools/freehand/pencil", bevent.state); + m.unSetup(); + ret = true; + break; + } + if (anchor) { + p = anchor->dp; + //Put the start overwrite curve always on the same direction + if (anchor->start) { + this->sa_overwrited = anchor->curve->create_reverse(); + } else { + this->sa_overwrited = anchor->curve->copy(); + } + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Continuing selected path")); + } else { + m.setup(desktop, true); + if (tablet_enabled) { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + selection->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + } else if (!(bevent.state & GDK_SHIFT_MASK)) { + // This is the first click of a new curve; deselect item so that + // this curve is not combined with it (unless it is drawn from its + // anchor, which is handled by the sibling branch above) + selection->clear(); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Creating new path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } else if (selection->singleItem() && SP_IS_PATH(selection->singleItem())) { + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Appending to selected path")); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + } + m.unSetup(); + } + if (!tablet_enabled) { + this->sa = anchor; + } + this->_setStartpoint(p); + ret = true; + break; + } + + set_high_motion_precision(); + this->_is_drawing = true; + } + return ret; +} + +bool PencilTool::_handleMotionNotify(GdkEventMotion const &mevent) { + if ((mevent.state & GDK_CONTROL_MASK) && (mevent.state & GDK_BUTTON1_MASK)) { + // mouse was accidentally moved during Ctrl+click; + // ignore the motion and create a single point + this->_is_drawing = false; + return true; + } + bool ret = false; + + if (this->space_panning || (mevent.state & GDK_BUTTON2_MASK) || (mevent.state & GDK_BUTTON3_MASK)) { + // allow scrolling + return ret; + } + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, pencil_drag_origin_w); + if (this->pressure == 0.0 && tablet_enabled && !anchor) { + // tablet event was accidentally fired without press; + return ret; + } + + if ( ( mevent.state & GDK_BUTTON1_MASK ) && !this->grab && this->_is_drawing) { + /* Grab mouse, so release will not pass unnoticed */ + this->grab = SP_CANVAS_ITEM(desktop->acetate); + sp_canvas_item_grab(this->grab, ( GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK ), + nullptr, mevent.time); + } + + /* Find desktop coordinates */ + Geom::Point p = desktop->w2d(Geom::Point(mevent.x, mevent.y)); + + + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (pencil_within_tolerance) { + gint const tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( Geom::LInfty( Geom::Point(mevent.x,mevent.y) - pencil_drag_origin_w ) < tolerance ) { + return false; // Do not drag if we're within tolerance from origin. + } + } + + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + pencil_within_tolerance = false; + + anchor = spdc_test_inside(this, Geom::Point(mevent.x,mevent.y)); + + switch (this->_state) { + case SP_PENCIL_CONTEXT_ADDLINE: + if (is_tablet) { + this->_state = SP_PENCIL_CONTEXT_FREEHAND; + return false; + } + /* Set red endpoint */ + if (anchor) { + p = anchor->dp; + } else { + Geom::Point ptnr(p); + this->_endpointSnap(ptnr, mevent.state); + p = ptnr; + } + this->_setEndpoint(p); + ret = true; + break; + default: + /* We may be idle or already freehand */ + if ( (mevent.state & GDK_BUTTON1_MASK) && this->_is_drawing ) { + if (this->_state == SP_PENCIL_CONTEXT_IDLE) { + sp_event_context_discard_delayed_snap_event(this); + } + this->_state = SP_PENCIL_CONTEXT_FREEHAND; + + if ( !this->sa && !this->green_anchor ) { + /* Create green anchor */ + this->green_anchor = sp_draw_anchor_new(this, this->green_curve, TRUE, this->p[0]); + } + if (anchor) { + p = anchor->dp; + } + if ( this->_npoints != 0) { // buttonpress may have happened before we entered draw context! + if (this->ps.empty()) { + // Only in freehand mode we have to add the first point also to this->ps (apparently) + // - We cannot add this point in spdc_set_startpoint, because we only need it for freehand + // - We cannot do this in the button press handler because at that point we don't know yet + // whether we're going into freehand mode or not + this->ps.push_back(this->p[0]); + if (tablet_enabled) { + this->_wps.emplace_back(0, 0); + } + } + this->_addFreehandPoint(p, mevent.state, false); + ret = true; + } + if (anchor && !this->anchor_statusbar) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Release</b> here to close and finish the path.")); + this->anchor_statusbar = true; + this->ea = anchor; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + this->ea = nullptr; + } else if (!anchor) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Drawing a freehand path")); + this->ea = nullptr; + } + + } else { + if (anchor && !this->anchor_statusbar) { + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag</b> to continue the path from this point.")); + this->anchor_statusbar = true; + } else if (!anchor && this->anchor_statusbar) { + this->message_context->clear(); + this->anchor_statusbar = false; + } + } + + // Show the pre-snap indicator to communicate to the user where we would snap to if he/she were to + // a) press the mousebutton to start a freehand drawing, or + // b) release the mousebutton to finish a freehand drawing + if (!tablet_enabled && !this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true); + m.preSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + } + return ret; +} + +bool PencilTool::_handleButtonRelease(GdkEventButton const &revent) { + bool ret = false; + + set_high_motion_precision(false); + + if ( revent.button == 1 && this->_is_drawing && !this->space_panning) { + this->_is_drawing = false; + + /* Find desktop coordinates */ + Geom::Point p = desktop->w2d(Geom::Point(revent.x, revent.y)); + + /* Test whether we hit any anchor. */ + SPDrawAnchor *anchor = spdc_test_inside(this, Geom::Point(revent.x, revent.y)); + + switch (this->_state) { + case SP_PENCIL_CONTEXT_IDLE: + /* Releasing button in idle mode means single click */ + /* We have already set up start point/anchor in button_press */ + if (!(revent.state & GDK_CONTROL_MASK) && !is_tablet) { + // Ctrl+click creates a single point so only set context in ADDLINE mode when Ctrl isn't pressed + this->_state = SP_PENCIL_CONTEXT_ADDLINE; + } + /*Or select the down item if we are in tablet mode*/ + if (is_tablet) { + using namespace Inkscape::LivePathEffect; + SPItem * item = sp_event_context_find_item (desktop, Geom::Point(revent.x, revent.y), FALSE, FALSE); + if (item && (!this->white_item || item != white_item)) { + Effect* lpe = SP_LPE_ITEM(item)->getCurrentLPE(); + if (lpe) { + LPEPowerStroke* ps = static_cast<LPEPowerStroke*>(lpe); + if (ps) { + desktop->selection->clear(); + desktop->selection->add(item); + } + } + } + } + break; + case SP_PENCIL_CONTEXT_ADDLINE: + /* Finish segment now */ + if (anchor) { + p = anchor->dp; + } else { + this->_endpointSnap(p, revent.state); + } + this->ea = anchor; + this->_setEndpoint(p); + this->_finishEndpoint(); + this->_state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(this); + break; + case SP_PENCIL_CONTEXT_FREEHAND: + if (revent.state & GDK_MOD1_MASK && !tablet_enabled) { + /* sketch mode: interpolate the sketched path and improve the current output path with the new interpolation. don't finish sketch */ + this->_sketchInterpolate(); + + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + + this->_state = SP_PENCIL_CONTEXT_SKETCH; + } else { + /* Finish segment now */ + /// \todo fixme: Clean up what follows (Lauris) + if (anchor) { + p = anchor->dp; + } else { + Geom::Point p_end = p; + if (tablet_enabled) { + this->_addFreehandPoint(p_end, revent.state, true); + this->_pressure_curve->reset(); + } else { + this->_endpointSnap(p_end, revent.state); + if (p_end != p) { + // then we must have snapped! + this->_addFreehandPoint(p_end, revent.state, true); + } + } + } + + this->ea = anchor; + /* Write curves to object */ + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand")); + this->_interpolate(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (tablet_enabled) { + gint shapetype = prefs->getInt("/tools/freehand/pencil/shape", 0); + gint simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + gint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + prefs->setInt("/tools/freehand/pencil/shape", 0); + prefs->setInt("/tools/freehand/pencil/simplify", 0); + prefs->setInt("/tools/freehand/pencil/freehand-mode", 0); + spdc_concat_colors_and_flush(this, FALSE); + prefs->setInt("/tools/freehand/pencil/freehand-mode", mode); + prefs->setInt("/tools/freehand/pencil/simplify", simplify); + prefs->setInt("/tools/freehand/pencil/shape", shapetype); + } else { + spdc_concat_colors_and_flush(this, FALSE); + } + this->points.clear(); + this->sa = nullptr; + this->ea = nullptr; + this->ps.clear(); + this->_wps.clear(); + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + this->_state = SP_PENCIL_CONTEXT_IDLE; + // reset sketch mode too + this->sketch_n = 0; + } + break; + case SP_PENCIL_CONTEXT_SKETCH: + default: + break; + } + + if (this->grab) { + /* Release grab now */ + sp_canvas_item_ungrab(this->grab); + this->grab = nullptr; + } + + ret = true; + } + return ret; +} + +void PencilTool::_cancel() { + if (this->grab) { + /* Release grab now */ + sp_canvas_item_ungrab(this->grab); + this->grab = nullptr; + } + + this->_is_drawing = false; + this->_state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(this); + + this->red_curve->reset(); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); + for (auto i:this->green_bpaths) { + sp_canvas_item_destroy(i); + } + this->green_bpaths.clear(); + this->green_curve->reset(); + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + + this->message_context->clear(); + this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); + + this->desktop->canvas->endForcedFullRedraws(); +} + +bool PencilTool::_handleKeyPress(GdkEventKey const &event) { + bool ret = false; + + switch (get_latin_keyval(&event)) { + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // Prevent the zoom field from activation. + if (!Inkscape::UI::held_only_control(event)) { + ret = true; + } + break; + case GDK_KEY_Escape: + if (this->_npoints != 0) { + // if drawing, cancel, otherwise pass it up for deselecting + if (this->_state != SP_PENCIL_CONTEXT_IDLE) { + this->_cancel(); + ret = true; + } + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (Inkscape::UI::held_only_control(event) && this->_npoints != 0) { + // if drawing, cancel, otherwise pass it up for undo + if (this->_state != SP_PENCIL_CONTEXT_IDLE) { + this->_cancel(); + ret = true; + } + } + break; + case GDK_KEY_g: + case GDK_KEY_G: + if (Inkscape::UI::held_only_shift(event)) { + this->desktop->selection->toGuides(); + ret = true; + } + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (this->_state == SP_PENCIL_CONTEXT_IDLE) { + this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("<b>Sketch mode</b>: holding <b>Alt</b> interpolates between sketched paths. Release <b>Alt</b> to finalize.")); + } + break; + default: + break; + } + return ret; +} + +bool PencilTool::_handleKeyRelease(GdkEventKey const &event) { + bool ret = false; + + switch (get_latin_keyval(&event)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Meta_L: + case GDK_KEY_Meta_R: + if (this->_state == SP_PENCIL_CONTEXT_SKETCH) { + spdc_concat_colors_and_flush(this, FALSE); + this->sketch_n = 0; + this->sa = nullptr; + this->ea = nullptr; + if (this->green_anchor) { + this->green_anchor = sp_draw_anchor_destroy(this->green_anchor); + } + this->_state = SP_PENCIL_CONTEXT_IDLE; + sp_event_context_discard_delayed_snap_event(this); + this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Finishing freehand sketch")); + ret = true; + } + break; + default: + break; + } + return ret; +} + +/** + * Reset points and set new starting point. + */ +void PencilTool::_setStartpoint(Geom::Point const &p) { + this->_npoints = 0; + this->red_curve_is_valid = false; + if (in_svg_plane(p)) { + this->p[this->_npoints++] = p; + } +} + +/** + * Change moving endpoint position. + * <ul> + * <li>Ctrl constrains to moving to H/V direction, snapping in given direction. + * <li>Otherwise we snap freely to whatever attractors are available. + * </ul> + * + * Number of points is (re)set to 2 always, 2nd point is modified. + * We change RED curve. + */ +void PencilTool::_setEndpoint(Geom::Point const &p) { + if (this->_npoints == 0) { + return; + /* May occur if first point wasn't in SVG plane (e.g. weird w2d transform, perhaps from bad + * zoom setting). + */ + } + g_return_if_fail( this->_npoints > 0 ); + + this->red_curve->reset(); + if ( ( p == this->p[0] ) + || !in_svg_plane(p) ) + { + this->_npoints = 1; + } else { + this->p[1] = p; + this->_npoints = 2; + + this->red_curve->moveto(this->p[0]); + this->red_curve->lineto(this->p[1]); + this->red_curve_is_valid = true; + if (!tablet_enabled) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve); + } + } +} + +/** + * Finalize addline. + * + * \todo + * fixme: I'd like remove red reset from concat colors (lauris). + * Still not sure, how it will make most sense. + */ +void PencilTool::_finishEndpoint() { + if (this->red_curve->is_unset() || + this->red_curve->first_point() == this->red_curve->second_point()) + { + this->red_curve->reset(); + if (!tablet_enabled) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), nullptr); + } + } else { + /* Write curves to object. */ + spdc_concat_colors_and_flush(this, FALSE); + this->sa = nullptr; + this->ea = nullptr; + } +} + +static inline double square(double const x) { return x * x; } + + + +void PencilTool::addPowerStrokePencil() +{ + if (this->_curve) { + SPDocument *document = SP_ACTIVE_DOCUMENT; + if (!document) { + return; + } + using namespace Inkscape::LivePathEffect; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4; + double tolerance_sq = 0.02 * square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + int n_points = this->ps.size(); + // worst case gives us a segment per point + int max_segs = 4 * n_points; + std::vector<Geom::Point> b(max_segs); + SPCurve *curvepressure = new SPCurve(); + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + if (n_segs > 0) { + /* Fit and draw and reset state */ + curvepressure->moveto(b[0]); + for (int c = 0; c < n_segs; c++) { + curvepressure->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } + } + Geom::Affine transform_coordinate = SP_ITEM(SP_ACTIVE_DESKTOP->currentLayer())->i2dt_affine().inverse(); + curvepressure->transform(transform_coordinate); + Geom::Path path = curvepressure->get_pathvector()[0]; + if (!path.empty()) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *pp = nullptr; + pp = xml_doc->createElement("svg:path"); + gchar *pvector_str = sp_svg_write_path(path); + if (pvector_str) { + pp->setAttribute("d", pvector_str); + g_free(pvector_str); + } + pp->setAttribute("id", "power_stroke_preview"); + Inkscape::GC::release(pp); + + SPShape *powerpreview = SP_SHAPE(SP_ITEM(SP_ACTIVE_DESKTOP->currentLayer())->appendChildRepr(pp)); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(powerpreview); + if (!lpeitem) { + return; + } + Inkscape::DocumentUndo::ScopedInsensitive no_undo(document); + tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 0.0, 100.0) + 30; + if (tol > 30) { + tol = tol / (130.0 * (132.0 - tol)); + Inkscape::SVGOStringStream threshold; + threshold << tol; + Effect::createAndApply(SIMPLIFY, desktop->doc(), SP_ITEM(lpeitem)); + Effect *lpe = lpeitem->getCurrentLPE(); + Inkscape::LivePathEffect::LPESimplify *simplify = + static_cast<Inkscape::LivePathEffect::LPESimplify *>(lpe); + if (simplify) { + sp_lpe_item_enable_path_effects(lpeitem, false); + Glib::ustring pref_path = "/live_effects/simplify/smooth_angles"; + bool valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("smooth_angles", "0"); + } + pref_path = "/live_effects/simplify/helper_size"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("helper_size", "0"); + } + pref_path = "/live_effects/simplify/step"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + lpe->getRepr()->setAttribute("step", "1"); + } + lpe->getRepr()->setAttribute("threshold", threshold.str()); + lpe->getRepr()->setAttribute("simplify_individual_paths", "false"); + lpe->getRepr()->setAttribute("simplify_just_coalesce", "false"); + sp_lpe_item_enable_path_effects(lpeitem, true); + } + sp_lpe_item_update_patheffect(lpeitem, false, true); + curvepressure = powerpreview->getCurve(); + if (curvepressure->is_empty()) { + return; + } + path = curvepressure->get_pathvector()[0]; + } + powerStrokeInterpolate(path); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil"; + prefs->setBool(pref_path_pp, true); + Effect::createAndApply(POWERSTROKE, SP_ACTIVE_DESKTOP->doc(), lpeitem); + Effect *lpe = lpeitem->getCurrentLPE(); + Inkscape::LivePathEffect::LPEPowerStroke *pspreview = static_cast<LPEPowerStroke *>(lpe); + if (pspreview) { + sp_lpe_item_enable_path_effects(lpeitem, false); + Glib::ustring pref_path = "/live_effects/powerstroke/interpolator_type"; + bool valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("interpolator_type", "CentripetalCatmullRom"); + } + pref_path = "/live_effects/powerstroke/linejoin_type"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("linejoin_type", "spiro"); + } + pref_path = "/live_effects/powerstroke/interpolator_beta"; + valid = prefs->getEntry(pref_path).isValid(); + if (!valid) { + pspreview->getRepr()->setAttribute("interpolator_beta", "0.75"); + } + gint cap = prefs->getInt("/live_effects/powerstroke/powerpencilcap", 2); + pspreview->getRepr()->setAttribute("start_linecap_type", LineCapTypeConverter.get_key(cap)); + pspreview->getRepr()->setAttribute("end_linecap_type", LineCapTypeConverter.get_key(cap)); + pspreview->getRepr()->setAttribute("sort_points", "true"); + if (!this->points.size()) { + Geom::Point default_point((path.size()/2.0), 0.5); + this->points.push_back(default_point); + } + pspreview->offset_points.param_set_and_write_new_value(this->points); + sp_lpe_item_enable_path_effects(lpeitem, true); + sp_lpe_item_update_patheffect(lpeitem, false, true); + pp->setAttribute("style", "fill:#888888;opacity:1;fill-rule:nonzero;stroke:none;"); + } + if (curvepressure) { + curvepressure->unref(); + } + prefs->setBool(pref_path_pp, false); + } + } +} + +/** + * Add a virtual point to the future pencil path. + * + * @param p the point to add. + * @param state event state + * @param last the point is the last of the user stroke. + */ +void PencilTool::_addFreehandPoint(Geom::Point const &p, guint /*state*/, bool last) +{ + g_assert( this->_npoints > 0 ); + g_return_if_fail(unsigned(this->_npoints) < G_N_ELEMENTS(this->p)); + + double distance = 0; + if ( ( p != this->p[ this->_npoints - 1 ] ) + && in_svg_plane(p) ) + { + this->p[this->_npoints++] = p; + this->_fitAndSplit(); + if (tablet_enabled) { + distance = Geom::distance(p, this->ps.back()) + this->_wps.back()[Geom::X]; + } + this->ps.push_back(p); + } + if (tablet_enabled && in_svg_plane(p)) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double min = prefs->getIntLimited("/tools/freehand/pencil/minpressure", 0, 0, 100) / 100.0; + double max = prefs->getIntLimited("/tools/freehand/pencil/maxpressure", 30, 0, 100) / 100.0; + if (min > max) { + min = max; + } + double dezoomify_factor = 0.05 * 1000 / SP_EVENT_CONTEXT(this)->desktop->current_zoom(); + double pressure_shrunk = (((this->pressure - 0.25) * 1.25) * (max - min)) + min; + double pressure_computed = pressure_shrunk * dezoomify_factor; + double pressure_computed_scaled = std::abs(pressure_computed * SP_ACTIVE_DOCUMENT->getDocumentScale().inverse()[Geom::X]); + if (p != this->p[this->_npoints - 1]) { + this->_wps.emplace_back(distance, pressure_computed_scaled); + } + pressure_computed = std::abs(pressure_computed); + if (pressure_computed) { + Geom::Circle pressure_dot(p, pressure_computed); + Geom::Piecewise<Geom::D2<Geom::SBasis>> pressure_piecewise; + pressure_piecewise.push_cut(0); + pressure_piecewise.push(pressure_dot.toSBasis(), 1); + Geom::PathVector pressure_path = Geom::path_from_piecewise(pressure_piecewise, 0.1); + Geom::PathVector previous_presure = this->_pressure_curve->get_pathvector(); + if (!pressure_path.empty() && !previous_presure.empty()) { + pressure_path = sp_pathvector_boolop(pressure_path, previous_presure, bool_op_union, fill_nonZero, fill_nonZero); + } + this->_pressure_curve->set_pathvector(pressure_path); + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->_pressure_curve); + } + if (last) { + this->addPowerStrokePencil(); + } + } +} + +void PencilTool::powerStrokeInterpolate(Geom::Path const path) +{ + size_t ps_size = this->ps.size(); + if ( ps_size <= 1 ) { + return; + } + + using Geom::X; + using Geom::Y; + gint path_size = path.size(); + std::vector<Geom::Point> tmp_points; + Geom::Point previous = Geom::Point(Geom::infinity(), 0); + bool increase = false; + size_t i = 0; + double dezoomify_factor = 0.05 * 1000 / SP_EVENT_CONTEXT(this)->desktop->current_zoom(); + double limit = 6 * dezoomify_factor; + double max = + std::max(this->_wps.back()[Geom::X] - (this->_wps.back()[Geom::X] / 10), this->_wps.back()[Geom::X] - limit); + double min = std::min(this->_wps.back()[Geom::X] / 10, limit); + double original_lenght = this->_wps.back()[Geom::X]; + double max10 = 0; + double min10 = 0; + for (auto wps : this->_wps) { + i++; + Geom::Coord pressure = wps[Geom::Y]; + max10 = max10 > pressure ? max10 : pressure; + min10 = min10 <= pressure ? min10 : pressure; + + if (!original_lenght || wps[Geom::X] > max) { + break; + } + if (wps[Geom::Y] == 0 || wps[Geom::X] < min) { + continue; + } + if (previous[Geom::Y] < (max10 + min10) / 2.0) { + if (increase && tmp_points.size() > 1) { + tmp_points.pop_back(); + } + wps[Geom::Y] = max10; + tmp_points.push_back(wps); + increase = true; + } else { + if (!increase && tmp_points.size() > 1) { + tmp_points.pop_back(); + } + wps[Geom::Y] = min10; + tmp_points.push_back(wps); + increase = false; + } + previous = wps; + max10 = 0; + min10 = 999999999; + } + this->points.clear(); + double prev_pressure = 0; + for (auto point : tmp_points) { + point[Geom::X] /= (double)original_lenght; + point[Geom::X] *= path_size; + if (std::abs(point[Geom::Y] - prev_pressure) > point[Geom::Y] / 10.0) { + this->points.push_back(point); + prev_pressure = point[Geom::Y]; + } + } + tmp_points.clear(); +} + +void PencilTool::_interpolate() { + size_t ps_size = this->ps.size(); + if ( ps_size <= 1 ) { + return; + } + using Geom::X; + using Geom::Y; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4; + bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + if(simplify){ + double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 0.0, 100.0) * 0.4; + tol = std::min(tol,tol2); + } + this->green_curve->reset(); + this->red_curve->reset(); + this->red_curve_is_valid = false; + double tolerance_sq = square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + + g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent)); + + int n_points = this->ps.size(); + + // worst case gives us a segment per point + int max_segs = 4 * n_points; + + std::vector<Geom::Point> b(max_segs); + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + if (n_segs > 0) { + /* Fit and draw and reset state */ + this->green_curve->moveto(b[0]); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + for (int c = 0; c < n_segs; c++) { + // if we are in BSpline we modify the trace to create adhoc nodes + if (mode == 2) { + Geom::Point point_at1 = b[4 * c + 0] + (1./3) * (b[4 * c + 3] - b[4 * c + 0]); + point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + Geom::Point point_at2 = b[4 * c + 3] + (1./3) * (b[4 * c + 0] - b[4 * c + 3]); + point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + this->green_curve->curveto(point_at1,point_at2,b[4*c+3]); + } else { + if (!tablet_enabled || c != n_segs - 1) { + this->green_curve->curveto(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } else { + boost::optional<Geom::Point> finalp = this->green_curve->last_point(); + if (this->green_curve->nodes_in_path() > 4 && Geom::are_near(*finalp, b[4 * c + 3], 10.0)) { + this->green_curve->backspace(); + this->green_curve->curveto(*finalp, b[4 * c + 3], b[4 * c + 3]); + } else { + this->green_curve->curveto(b[4 * c + 1], b[4 * c + 3], b[4 * c + 3]); + } + } + } + } + if (!tablet_enabled) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->green_curve); + } + /* Fit and draw and copy last point */ + g_assert(!this->green_curve->is_empty()); + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } +} + + +/* interpolates the sketched curve and tweaks the current sketch interpolation*/ +void PencilTool::_sketchInterpolate() { + if ( this->ps.size() <= 1 ) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double tol = prefs->getDoubleLimited("/tools/freehand/pencil/tolerance", 10.0, 1.0, 100.0) * 0.4; + bool simplify = prefs->getInt("/tools/freehand/pencil/simplify", 0); + if(simplify){ + double tol2 = prefs->getDoubleLimited("/tools/freehand/pencil/base-simplify", 25.0, 1.0, 100.0) * 0.4; + tol = std::min(tol,tol2); + } + double tolerance_sq = 0.02 * square(this->desktop->w2d().descrim() * tol) * exp(0.2 * tol - 2); + + bool average_all_sketches = prefs->getBool("/tools/freehand/pencil/average_all_sketches", true); + + g_assert(is_zero(this->_req_tangent) || is_unit_vector(this->_req_tangent)); + + this->red_curve->reset(); + this->red_curve_is_valid = false; + + int n_points = this->ps.size(); + + // worst case gives us a segment per point + int max_segs = 4 * n_points; + + std::vector<Geom::Point> b(max_segs); + + int const n_segs = Geom::bezier_fit_cubic_r(b.data(), this->ps.data(), n_points, tolerance_sq, max_segs); + + if (n_segs > 0) { + Geom::Path fit(b[0]); + + for (int c = 0; c < n_segs; c++) { + fit.appendNew<Geom::CubicBezier>(b[4 * c + 1], b[4 * c + 2], b[4 * c + 3]); + } + + Geom::Piecewise<Geom::D2<Geom::SBasis> > fit_pwd2 = fit.toPwSb(); + + if (this->sketch_n > 0) { + double t; + + if (average_all_sketches) { + // Average = (sum of all) / n + // = (sum of all + new one) / n+1 + // = ((old average)*n + new one) / n+1 + t = this->sketch_n / (this->sketch_n + 1.); + } else { + t = 0.5; + } + + this->sketch_interpolation = Geom::lerp(t, fit_pwd2, this->sketch_interpolation); + + // simplify path, to eliminate small segments + Path path; + path.LoadPathVector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01)); + path.Simplify(0.5); + + Geom::PathVector *pathv = path.MakePathVector(); + this->sketch_interpolation = (*pathv)[0].toPwSb(); + delete pathv; + } else { + this->sketch_interpolation = fit_pwd2; + } + + this->sketch_n++; + + this->green_curve->reset(); + this->green_curve->set_pathvector(Geom::path_from_piecewise(this->sketch_interpolation, 0.01)); + if (!tablet_enabled) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->green_curve); + } + /* Fit and draw and copy last point */ + g_assert(!this->green_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->green_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + } + + this->ps.clear(); + this->points.clear(); + this->_wps.clear(); +} + +void PencilTool::_fitAndSplit() { + g_assert( this->_npoints > 1 ); + + double const tolerance_sq = 0; + + Geom::Point b[4]; + g_assert(is_zero(this->_req_tangent) + || is_unit_vector(this->_req_tangent)); + Geom::Point const tHatEnd(0, 0); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const n_segs = Geom::bezier_fit_cubic_full(b, nullptr, this->p, this->_npoints, + this->_req_tangent, tHatEnd, + tolerance_sq, 1); + if ( n_segs > 0 + && unsigned(this->_npoints) < G_N_ELEMENTS(this->p) ) + { + /* Fit and draw and reset state */ + + this->red_curve->reset(); + this->red_curve->moveto(b[0]); + using Geom::X; + using Geom::Y; + // if we are in BSpline we modify the trace to create adhoc nodes + guint mode = prefs->getInt("/tools/freehand/pencil/freehand-mode", 0); + if(mode == 2){ + Geom::Point point_at1 = b[0] + (1./3)*(b[3] - b[0]); + point_at1 = Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + Geom::Point point_at2 = b[3] + (1./3)*(b[0] - b[3]); + point_at2 = Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + this->red_curve->curveto(point_at1,point_at2,b[3]); + }else{ + this->red_curve->curveto(b[1], b[2], b[3]); + } + if (!tablet_enabled) { + sp_canvas_bpath_set_bpath(SP_CANVAS_BPATH(this->red_bpath), this->red_curve); + } + this->red_curve_is_valid = true; + } else { + /* Fit and draw and copy last point */ + + g_assert(!this->red_curve->is_empty()); + + /* Set up direction of next curve. */ + { + Geom::Curve const * last_seg = this->red_curve->last_segment(); + g_assert( last_seg ); // Relevance: validity of (*last_seg) + this->p[0] = last_seg->finalPoint(); + this->_npoints = 1; + Geom::Curve *last_seg_reverse = last_seg->reverse(); + Geom::Point const req_vec( -last_seg_reverse->unitTangentAt(0) ); + delete last_seg_reverse; + this->_req_tangent = ( ( Geom::is_zero(req_vec) || !in_svg_plane(req_vec) ) + ? Geom::Point(0, 0) + : Geom::unit_vector(req_vec) ); + } + + + this->green_curve->append_continuous(this->red_curve, 0.0625); + SPCurve *curve = this->red_curve->copy(); + + /// \todo fixme: + SPCanvasItem *cshape = sp_canvas_bpath_new(this->desktop->getSketch(), curve, true); + curve->unref(); + + this->highlight_color = SP_ITEM(this->desktop->currentLayer())->highlight_color(); + if((unsigned int)prefs->getInt("/tools/nodes/highlight_color", 0xff0000ff) == this->highlight_color){ + this->green_color = 0x00ff007f; + } else { + this->green_color = this->highlight_color; + } + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(cshape), this->green_color, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + + this->green_bpaths.push_back(cshape); + + this->red_curve_is_valid = false; + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/pencil-tool.h b/src/ui/tools/pencil-tool.h new file mode 100644 index 0000000..41cb71a --- /dev/null +++ b/src/ui/tools/pencil-tool.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * PencilTool: a context for pencil tool events + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_PENCIL_CONTEXT_H +#define SEEN_PENCIL_CONTEXT_H + + +#include "ui/tools/freehand-base.h" + +#include <2geom/piecewise.h> +#include <2geom/d2.h> +#include <2geom/sbasis.h> +#include <2geom/pathvector.h> +// #include <future> + +class SPShape; + +#define DDC_MIN_PRESSURE 0.0 +#define DDC_MAX_PRESSURE 1.0 +#define DDC_DEFAULT_PRESSURE 1.0 +#define SP_PENCIL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::PencilTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_PENCIL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::PencilTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum PencilState { + SP_PENCIL_CONTEXT_IDLE, + SP_PENCIL_CONTEXT_ADDLINE, + SP_PENCIL_CONTEXT_FREEHAND, + SP_PENCIL_CONTEXT_SKETCH +}; + +/** + * PencilTool: a context for pencil tool events + */ +class PencilTool : public FreehandBase { +public: + PencilTool(); + ~PencilTool() override; + Geom::Point p[16]; + std::vector<Geom::Point> ps; + std::vector<Geom::Point> points; + void addPowerStrokePencil(); + void powerStrokeInterpolate(Geom::Path const path); + Geom::Piecewise<Geom::D2<Geom::SBasis> > sketch_interpolation; // the current proposal from the sketched paths + unsigned sketch_n; // number of sketches done + static const std::string prefsPath; + const std::string& getPrefsPath() override; + +protected: + + void setup() override; + bool root_handler(GdkEvent* event) override; + +private: + bool _handleButtonPress(GdkEventButton const &bevent); + bool _handleMotionNotify(GdkEventMotion const &mevent); + bool _handleButtonRelease(GdkEventButton const &revent); + bool _handleKeyPress(GdkEventKey const &event); + bool _handleKeyRelease(GdkEventKey const &event); + void _setStartpoint(Geom::Point const &p); + void _setEndpoint(Geom::Point const &p); + void _finishEndpoint(); + void _addFreehandPoint(Geom::Point const &p, guint state, bool last); + void _fitAndSplit(); + void _interpolate(); + void _sketchInterpolate(); + void _extinput(GdkEvent *event); + void _cancel(); + void _endpointSnap(Geom::Point &p, guint const state); + std::vector<Geom::Point> _wps; + SPCurve * _curve; + SPCurve *_pressure_curve; + Geom::Point _req_tangent; + bool _is_drawing; + PencilState _state; + gint _npoints; + // std::future<bool> future; +}; + +} +} +} + +#endif /* !SEEN_PENCIL_CONTEXT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/rect-tool.cpp b/src/ui/tools/rect-tool.cpp new file mode 100644 index 0000000..2cd10a2 --- /dev/null +++ b/src/ui/tools/rect-tool.cpp @@ -0,0 +1,503 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Rectangle drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2000-2005 authors + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "message-context.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "verbs.h" + +#include "display/sp-canvas-item.h" +#include "display/sp-canvas.h" + +#include "object/sp-rect.h" +#include "object/sp-namedview.h" + +#include "ui/pixmaps/cursor-rect.xpm" + +#include "ui/shape-editor.h" +#include "ui/tools/rect-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& RectTool::getPrefsPath() { + return RectTool::prefsPath; +} + +const std::string RectTool::prefsPath = "/tools/shapes/rect"; + +RectTool::RectTool() + : ToolBase(cursor_rect_xpm) + , rect(nullptr) + , rx(0) + , ry(0) +{ +} + +void RectTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate)); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +RectTool::~RectTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->rect) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void RectTool::selection_changed(Inkscape::Selection* selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void RectTool::setup() { + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection.disconnect(); + this->sel_changed_connection = this->desktop->getSelection()->connectChanged( + sigc::mem_fun(this, &RectTool::selection_changed) + ); + + sp_event_context_read(this, "rx"); + sp_event_context_read(this, "ry"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +void RectTool::set(const Inkscape::Preferences::Entry& val) { + /* fixme: Proper error handling for non-numeric data. Use a locale-independent function like + * g_ascii_strtod (or a thin wrapper that does the right thing for invalid values inf/nan). */ + Glib::ustring name = val.getEntryName(); + + if ( name == "rx" ) { + this->rx = val.getDoubleLimited(); // prevents NaN and +/-Inf from messing up + } else if ( name == "ry" ) { + this->ry = val.getDoubleLimited(); + } +} + +bool RectTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if ( event->button.button == 1 && !this->space_panning) { + Inkscape::setup_for_drag_start(desktop, this, event); + } + break; + // motion and release are always on root (why?) + default: + break; + } + + ret = ToolBase::item_handler(item, event); + + return ret; +} + +bool RectTool::root_handler(GdkEvent* event) { + static bool dragging; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + Geom::Point const button_w(event->button.x, event->button.y); + + // save drag origin + this->xp = (gint) button_w[Geom::X]; + this->yp = (gint) button_w[Geom::Y]; + this->within_tolerance = true; + + // remember clicked item, disregarding groups, honoring Alt + this->item_to_select = sp_event_context_find_item (desktop, button_w, event->button.state & GDK_MOD1_MASK, TRUE); + + dragging = true; + + /* Position center */ + Geom::Point button_dt(desktop->w2d(button_w)); + this->center = button_dt; + + /* Snap center */ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + this->center = button_dt; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + nullptr, event->button.time); + + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + if ( dragging + && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) + { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); // this will also handle the snapping + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1 && !this->space_panning) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the rect + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + } + break; + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + if (!dragging){ + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: make square or integer-ratio rect, lock a rounded corner circular"), + _("<b>Shift</b>: draw around the starting point"), + nullptr); + } + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo("rect-width"); + ret = TRUE; + } + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + desktop->selection->toGuides(); + ret = true; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the rect + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void RectTool::drag(Geom::Point const pt, guint state) { + SPDesktop *desktop = this->desktop; + + if (!this->rect) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:rect"); + + // Set style + sp_desktop_apply_style_tool (desktop, repr, "/tools/shapes/rect", false); + + this->rect = SP_RECT(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + + this->rect->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->rect->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + Geom::Rect const r = Inkscape::snap_rectangular_box(desktop, this->rect, pt, this->center, state); + + this->rect->setPosition(r.min()[Geom::X], r.min()[Geom::Y], r.dimensions()[Geom::X], r.dimensions()[Geom::Y]); + + if (this->rx != 0.0) { + this->rect->setRx(true, this->rx); + } + + if (this->ry != 0.0) { + if (this->rx == 0.0) + this->rect->setRy(true, CLAMP(this->ry, 0, MIN(r.dimensions()[Geom::X], r.dimensions()[Geom::Y])/2)); + else + this->rect->setRy(true, CLAMP(this->ry, 0, r.dimensions()[Geom::Y])); + } + + // status text + double rdimx = r.dimensions()[Geom::X]; + double rdimy = r.dimensions()[Geom::Y]; + + Inkscape::Util::Quantity rdimx_q = Inkscape::Util::Quantity(rdimx, "px"); + Inkscape::Util::Quantity rdimy_q = Inkscape::Util::Quantity(rdimy, "px"); + Glib::ustring xs = rdimx_q.string(desktop->namedview->display_units); + Glib::ustring ys = rdimy_q.string(desktop->namedview->display_units); + + if (state & GDK_CONTROL_MASK) { + int ratio_x, ratio_y; + bool is_golden_ratio = false; + + if (fabs (rdimx) > fabs (rdimy)) { + if (fabs(rdimx / rdimy - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = (int) rint (rdimx / rdimy); + ratio_y = 1; + } else { + if (fabs(rdimy / rdimx - goldenratio) < 1e-6) { + is_golden_ratio = true; + } + + ratio_x = 1; + ratio_y = (int) rint (rdimy / rdimx); + } + + if (!is_golden_ratio) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to ratio %d:%d); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str(), ratio_x, ratio_y); + } else { + if (ratio_y == 1) { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1.618 : 1); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s (constrained to golden ratio 1 : 1.618); with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } + } + } else { + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Rectangle</b>: %s × %s; with <b>Ctrl</b> to make square, integer-ratio, or golden-ratio rectangle; with <b>Shift</b> to draw around the starting point"), + xs.c_str(), ys.c_str()); + } +} + +void RectTool::finishItem() { + this->message_context->clear(); + + if (this->rect != nullptr) { + if (this->rect->width.computed == 0 || this->rect->height.computed == 0) { + this->cancel(); // Don't allow the creating of zero sized rectangle, for example when the start and and point snap to the snap grid point + return; + } + + this->rect->updateRepr(); + this->rect->doWriteTransform(this->rect->transform, nullptr, true); + + this->desktop->canvas->endForcedFullRedraws(); + + this->desktop->getSelection()->set(this->rect); + + DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_RECT, _("Create rectangle")); + + this->rect = nullptr; + } +} + +void RectTool::cancel(){ + this->desktop->getSelection()->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate)); + + if (this->rect != nullptr) { + this->rect->deleteObject(); + this->rect = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + this->desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(this->desktop->getDocument()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/rect-tool.h b/src/ui/tools/rect-tool.h new file mode 100644 index 0000000..703b7c2 --- /dev/null +++ b/src/ui/tools/rect-tool.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_RECT_CONTEXT_H__ +#define __SP_RECT_CONTEXT_H__ + +/* + * Rectangle drawing context + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPRect; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class RectTool : public ToolBase { +public: + RectTool(); + ~RectTool() override; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPRect *rect; + Geom::Point center; + + gdouble rx; /* roundness radius (x direction) */ + gdouble ry; /* roundness radius (y direction) */ + + sigc::connection sel_changed_connection; + + void drag(Geom::Point const pt, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp new file mode 100644 index 0000000..c3a7b63 --- /dev/null +++ b/src/ui/tools/select-tool.cpp @@ -0,0 +1,1171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Selection and transformation context + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 authors + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 1999-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstring> +#include <string> + +#include <gtkmm/widget.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection-describer.h" +#include "selection.h" +#include "seltrans.h" +#include "sp-cursor.h" + +#include "display/drawing-item.h" +#include "display/sp-canvas.h" +#include "display/sp-canvas-item.h" + +#include "object/box3d.h" +#include "style.h" + +#include "ui/pixmaps/cursor-select-d.xpm" +#include "ui/pixmaps/cursor-select-m.xpm" +#include "ui/pixmaps/handles.xpm" + +#include "ui/tools-switch.h" +#include "ui/tools/select-tool.h" + +#ifdef WITH_DBUS +#include "extension/dbus/document-interface.h" +#endif + + +using Inkscape::DocumentUndo; + +GdkPixbuf *handles[23]; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static GdkCursor *CursorSelectMouseover = nullptr; +static GdkCursor *CursorSelectDragging = nullptr; + +static gint rb_escaped = 0; // if non-zero, rubberband was canceled by esc, so the next button release should not deselect +static gint drag_escaped = 0; // if non-zero, drag was canceled by esc + +const std::string& SelectTool::getPrefsPath() { + return SelectTool::prefsPath; +} + +const std::string SelectTool::prefsPath = "/tools/select"; + + +//Creates rotated variations for handles +static void +sp_load_handles(int start, int count, char const **xpm) { + handles[start] = gdk_pixbuf_new_from_xpm_data((gchar const **)xpm); + for(int i = start + 1; i < start + count; i++) { + // We use either the original at *start or previous loop item to rotate + handles[i] = gdk_pixbuf_rotate_simple(handles[i-1], GDK_PIXBUF_ROTATE_CLOCKWISE); + } +} + +SelectTool::SelectTool() + // Don't load a default cursor + : ToolBase(nullptr) + , dragging(false) + , moved(false) + , button_press_shift(false) + , button_press_ctrl(false) + , button_press_alt(false) + , cycling_wrap(true) + , item(nullptr) + , grabbed(nullptr) + , _seltrans(nullptr) + , _describer(nullptr) +{ + // cursors in select context + CursorSelectMouseover = sp_cursor_from_xpm(cursor_select_m_xpm); + CursorSelectDragging = sp_cursor_from_xpm(cursor_select_d_xpm); + + // selection handles + sp_load_handles(0, 2, handle_scale_xpm); + sp_load_handles(2, 2, handle_stretch_xpm); + sp_load_handles(4, 4, handle_rotate_xpm); + sp_load_handles(8, 4, handle_skew_xpm); + sp_load_handles(12, 1, handle_center_xpm); + sp_load_handles(13, 4, handle_align_xpm); + sp_load_handles(17, 1, handle_align_center_xpm); + sp_load_handles(18, 4, handle_align_corner_xpm); +} + +//static gint xp = 0, yp = 0; // where drag started +//static gint tolerance = 0; +//static bool within_tolerance = false; +static bool is_cycling = false; + + +SelectTool::~SelectTool() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + delete this->_seltrans; + this->_seltrans = nullptr; + + delete this->_describer; + this->_describer = nullptr; + + if (CursorSelectDragging) { + g_object_unref(CursorSelectDragging); + CursorSelectDragging = nullptr; + } + + if (CursorSelectMouseover) { + g_object_unref(CursorSelectMouseover); + CursorSelectMouseover = nullptr; + } + this->desktop->canvas->endForcedFullRedraws(); +} + +void SelectTool::setup() { + ToolBase::setup(); + + this->_describer = new Inkscape::SelectionDescriber( + desktop->selection, + desktop->messageStack(), + _("Click selection to toggle scale/rotation handles (or Shift+s)"), + _("No objects selected. Click, Shift+click, Alt+scroll mouse on top of objects, or drag around objects to select.") + ); + + this->_seltrans = new Inkscape::SelTrans(desktop); + + sp_event_context_read(this, "show"); + sp_event_context_read(this, "transform"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/select/gradientdrag")) { + this->enableGrDrag(); + } +} + +void SelectTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "show") { + if (val.getString() == "outline") { + this->_seltrans->setShow(Inkscape::SelTrans::SHOW_OUTLINE); + } else { + this->_seltrans->setShow(Inkscape::SelTrans::SHOW_CONTENT); + } + } +} + +bool SelectTool::sp_select_context_abort() { + Inkscape::SelTrans *seltrans = this->_seltrans; + + if (this->dragging) { + if (this->moved) { // cancel dragging an object + seltrans->ungrab(); + this->moved = FALSE; + this->dragging = FALSE; + sp_event_context_discard_delayed_snap_event(this); + drag_escaped = 1; + + if (this->item) { + // only undo if the item is still valid + if (this->item->document) { + DocumentUndo::undo(desktop->getDocument()); + } + + sp_object_unref( this->item, nullptr); + } else if (this->button_press_ctrl) { + // NOTE: This is a workaround to a bug. + // When the ctrl key is held, sc->item is not defined + // so in this case (only), we skip the object doc check + DocumentUndo::undo(desktop->getDocument()); + } + this->item = nullptr; + + SP_EVENT_CONTEXT(this)->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Move canceled.")); + return true; + } + } else { + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->stop(); + rb_escaped = 1; + SP_EVENT_CONTEXT(this)->defaultMessageContext()->clear(); + SP_EVENT_CONTEXT(this)->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selection canceled.")); + return true; + } + } + return false; +} + +static bool +key_is_a_modifier (guint key) { + return (key == GDK_KEY_Alt_L || + key == GDK_KEY_Alt_R || + key == GDK_KEY_Control_L || + key == GDK_KEY_Control_R || + key == GDK_KEY_Shift_L || + key == GDK_KEY_Shift_R || + key == GDK_KEY_Meta_L || // Meta is when you press Shift+Alt (at least on my machine) + key == GDK_KEY_Meta_R); +} + +static void +sp_select_context_up_one_layer(SPDesktop *desktop) +{ + /* Click in empty place, go up one level -- but don't leave a layer to root. + * + * (Rationale: we don't usually allow users to go to the root, since that + * detracts from the layer metaphor: objects at the root level can in front + * of or behind layers. Whereas it's fine to go to the root if editing + * a document that has no layers (e.g. a non-Inkscape document).) + * + * Once we support editing SVG "islands" (e.g. <svg> embedded in an xhtml + * document), we might consider further restricting the below to disallow + * leaving a layer to go to a non-layer. + */ + SPObject *const current_layer = desktop->currentLayer(); + if (current_layer) { + SPObject *const parent = current_layer->parent; + SPGroup *current_group = dynamic_cast<SPGroup *>(current_layer); + if ( parent + && ( parent->parent + || !( current_group + && ( SPGroup::LAYER == current_group->layerMode() ) ) ) ) + { + desktop->setCurrentLayer(parent); + if (current_group && (SPGroup::LAYER != current_group->layerMode())) { + desktop->getSelection()->set(current_layer); + } + } + } +} + +bool SelectTool::item_handler(SPItem* item, GdkEvent* event) { + gint ret = FALSE; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + // make sure we still have valid objects to move around + if (this->item && this->item->document == nullptr) { + this->sp_select_context_abort(); + } + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + /* Left mousebutton */ + + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + // remember what modifiers were on before button press + this->button_press_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false; + this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false; + this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false; + + if (event->button.state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)) { + // if shift or ctrl was pressed, do not move objects; + // pass the event to root handler which will perform rubberband, shift-click, ctrl-click, ctrl-drag + } else { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + + this->dragging = TRUE; + this->moved = FALSE; + + gdk_window_set_cursor(window, CursorSelectDragging); + + + // remember the clicked item in this->item: + if (this->item) { + sp_object_unref(this->item, nullptr); + this->item = nullptr; + } + + this->item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE); + sp_object_ref(this->item, nullptr); + + rb_escaped = drag_escaped = 0; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->drawing), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, + nullptr, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->drawing); + + ret = TRUE; + } + } else if (event->button.button == 3 && !this->dragging) { + // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband + this->sp_select_context_abort(); + } + break; + + + case GDK_ENTER_NOTIFY: { + if (!desktop->isWaitingCursor() && !this->dragging) { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + + gdk_window_set_cursor(window, CursorSelectMouseover); + } + break; + } + case GDK_LEAVE_NOTIFY: + if (!desktop->isWaitingCursor() && !this->dragging) { + Glib::RefPtr<Gdk::Window> window = Glib::wrap(GTK_WIDGET(desktop->getCanvas()))->get_window(); + + window->set_cursor(this->cursor); + } + break; + + case GDK_KEY_PRESS: + if (get_latin_keyval (&event->key) == GDK_KEY_space) { + if (this->dragging && this->grabbed) { + /* stamping mode: show content mode moving */ + _seltrans->stamp(); + ret = TRUE; + } + } else if (get_latin_keyval (&event->key) == GDK_KEY_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(false); + ret = TRUE; + } + } else if (get_latin_keyval (&event->key) == GDK_KEY_ISO_Left_Tab) { + if (this->dragging && this->grabbed) { + _seltrans->getNextClosestPoint(true); + ret = TRUE; + } + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::item_handler(item, event); + } + + return ret; +} + +void SelectTool::sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event, bool shift_pressed) { + if ( this->cycling_items.empty() ) + return; + + Inkscape::DrawingItem *arenaitem; + + if(cycling_cur_item) { + arenaitem = cycling_cur_item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(0.3); + } + + // Find next item and activate it + + + std::vector<SPItem *>::iterator next = cycling_items.end(); + + if ((scroll_event->direction == GDK_SCROLL_UP) || + (scroll_event->direction == GDK_SCROLL_SMOOTH && scroll_event->delta_y < 0)) { + if (! cycling_cur_item) { + next = cycling_items.begin(); + } else { + next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item ); + g_assert (next != cycling_items.end()); + next++; + if (next == cycling_items.end()) { + if ( cycling_wrap ) { + next = cycling_items.begin(); + } else { + next--; + } + } + } + } else { + if (! cycling_cur_item) { + next = cycling_items.end(); + next--; + } else { + next = std::find( cycling_items.begin(), cycling_items.end(), cycling_cur_item ); + g_assert (next != cycling_items.end()); + if (next == cycling_items.begin()){ + if ( cycling_wrap ) { + next = cycling_items.end(); + next--; + } + } else { + next--; + } + } + } + + this->cycling_cur_item = *next; + g_assert(next != cycling_items.end()); + g_assert(cycling_cur_item != nullptr); + + arenaitem = cycling_cur_item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(1.0); + + if (shift_pressed) { + selection->add(cycling_cur_item); + } else { + selection->set(cycling_cur_item); + } +} + +void SelectTool::sp_select_context_reset_opacities() { + for (auto item : this->cycling_items_cmp) { + if (item) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(SP_SCALE24_TO_FLOAT(item->style->opacity.value)); + } else { + g_assert_not_reached(); + } + } + + this->cycling_items_cmp.clear(); + this->cycling_cur_item = nullptr; +} + +bool SelectTool::root_handler(GdkEvent* event) { + SPItem *item = nullptr; + SPItem *item_at_point = nullptr, *group_at_point = nullptr, *item_in_group = nullptr; + gint ret = FALSE; + + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // make sure we still have valid objects to move around + if (this->item && this->item->document == nullptr) { + this->sp_select_context_abort(); + } + desktop->canvas->forceFullRedrawAfterInterruptions(5, false); + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (event->button.button == 1) { + if (!selection->isEmpty()) { + SPItem *clicked_item = selection->items().front(); + + if (dynamic_cast<SPGroup *>(clicked_item) && !dynamic_cast<SPBox3D *>(clicked_item)) { // enter group if it's not a 3D box + desktop->setCurrentLayer(clicked_item); + desktop->getSelection()->clear(); + this->dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + + } else { // switch tool + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point const p(desktop->w2d(button_pt)); + tools_switch_by_item (desktop, clicked_item, p); + } + } else { + sp_select_context_up_one_layer(desktop); + } + + ret = TRUE; + } + break; + + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point const p(desktop->w2d(button_pt)); + + if (event->button.state & GDK_MOD1_MASK) { + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + + Inkscape::Rubberband::get(desktop)->start(desktop, p); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK, + nullptr, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + + // remember what modifiers were on before button press + this->button_press_shift = (event->button.state & GDK_SHIFT_MASK) ? true : false; + this->button_press_ctrl = (event->button.state & GDK_CONTROL_MASK) ? true : false; + this->button_press_alt = (event->button.state & GDK_MOD1_MASK) ? true : false; + + this->moved = FALSE; + + rb_escaped = drag_escaped = 0; + + ret = TRUE; + } else if (event->button.button == 3) { + // right click; do not eat it so that right-click menu can appear, but cancel dragging & rubberband + this->sp_select_context_abort(); + } + break; + + case GDK_MOTION_NOTIFY: + { + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point const p(desktop->w2d(motion_pt)); + if ( within_tolerance + && ( abs( (gint) event->motion.x - xp ) < tolerance ) + && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (this->button_press_ctrl || (this->button_press_alt && !this->button_press_shift && !selection->isEmpty())) { + // if it's not click and ctrl or alt was pressed (the latter with some selection + // but not with shift) we want to drag rather than rubberband + this->dragging = TRUE; + + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + + gdk_window_set_cursor(window, CursorSelectDragging); + } + + if (this->dragging) { + /* User has dragged fast, so we get events on root (lauris)*/ + // not only that; we will end up here when ctrl-dragging as well + // and also when we started within tolerance, but trespassed tolerance outside of item + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->stop(); + } + this->defaultMessageContext()->clear(); + + item_at_point = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), FALSE); + + if (!item_at_point) { // if no item at this point, try at the click point (bug 1012200) + item_at_point = desktop->getItemAtPoint(Geom::Point(xp, yp), FALSE); + } + + if (item_at_point || this->moved || this->button_press_alt) { + // drag only if starting from an item, or if something is already grabbed, or if alt-dragging + if (!this->moved) { + item_in_group = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + group_at_point = desktop->getGroupAtPoint(Geom::Point(event->button.x, event->button.y)); + + { + SPGroup *selGroup = dynamic_cast<SPGroup *>(selection->single()); + if (selGroup && (selGroup->layerMode() == SPGroup::LAYER)) { + group_at_point = selGroup; + } + } + + // group-at-point is meant to be topmost item if it's a group, + // not topmost group of all items at point + if (group_at_point != item_in_group && + !(group_at_point && item_at_point && + group_at_point->isAncestorOf(item_at_point))) { + group_at_point = nullptr; + } + + // if neither a group nor an item (possibly in a group) at point are selected, set selection to the item at point + if ((!item_in_group || !selection->includes(item_in_group)) && + (!group_at_point || !selection->includes(group_at_point)) + && !this->button_press_alt) { + // select what is under cursor + if (!_seltrans->isEmpty()) { + _seltrans->resetState(); + } + + // when simply ctrl-dragging, we don't want to go into groups + if (item_at_point && !selection->includes(item_at_point)) { + selection->set(item_at_point); + } + } // otherwise, do not change selection so that dragging selected-within-group items, as well as alt-dragging, is possible + + _seltrans->grab(p, -1, -1, FALSE, TRUE); + this->moved = TRUE; + } + + if (!_seltrans->isEmpty()) { + sp_event_context_discard_delayed_snap_event(this); + _seltrans->moveTo(p, event->button.state); + } + + desktop->scroll_to_point(p); + gobble_motion_events(GDK_BUTTON1_MASK); + ret = TRUE; + } else { + this->dragging = FALSE; + sp_event_context_discard_delayed_snap_event(this); + + } + + } else { + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(p); + + if (Inkscape::Rubberband::get(desktop)->getMode() == RUBBERBAND_MODE_TOUCHPATH) { + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Draw over</b> objects to select them; release <b>Alt</b> to switch to rubberband selection")); + } else { + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("<b>Drag around</b> objects to select them; press <b>Alt</b> to switch to touch selection")); + } + + gobble_motion_events(GDK_BUTTON1_MASK); + } + } + } + break; + } + case GDK_BUTTON_RELEASE: + xp = yp = 0; + + if ((event->button.button == 1) && (this->grabbed) && !this->space_panning) { + if (this->dragging) { + GdkWindow* window; + + if (this->moved) { + // item has been moved + _seltrans->ungrab(); + this->moved = FALSE; +#ifdef WITH_DBUS + dbus_send_ping(desktop, this->item); +#endif + } else if (this->item && !drag_escaped) { + // item has not been moved -> simply a click, do selecting + if (!selection->isEmpty()) { + if (event->button.state & GDK_SHIFT_MASK) { + // with shift, toggle selection + _seltrans->resetState(); + selection->toggle(this->item); + } else { + SPObject* single = selection->single(); + SPGroup *singleGroup = dynamic_cast<SPGroup *>(single); + // without shift, increase state (i.e. toggle scale/rotation handles) + if (selection->includes(this->item)) { + _seltrans->increaseState(); + } else if (singleGroup && (singleGroup->layerMode() == SPGroup::LAYER) && single->isAncestorOf(this->item)) { + _seltrans->increaseState(); + } else { + _seltrans->resetState(); + selection->set(this->item); + } + } + } else { // simple or shift click, no previous selection + _seltrans->resetState(); + selection->set(this->item); + } + } + + this->dragging = FALSE; + window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + + gdk_window_set_cursor(window, CursorSelectMouseover); + sp_event_context_discard_delayed_snap_event(this); + + if (this->item) { + sp_object_unref( this->item, nullptr); + } + + this->item = nullptr; + } else { + Inkscape::Rubberband *r = Inkscape::Rubberband::get(desktop); + + if (r->is_started() && !within_tolerance) { + // this was a rubberband drag + std::vector<SPItem*> items; + + if (r->getMode() == RUBBERBAND_MODE_RECT) { + Geom::OptRect const b = r->getRectangle(); + items = desktop->getDocument()->getItemsInBox(desktop->dkey, (*b) * desktop->dt2doc()); + } else if (r->getMode() == RUBBERBAND_MODE_TOUCHPATH) { + items = desktop->getDocument()->getItemsAtPoints(desktop->dkey, r->getPoints()); + } + + _seltrans->resetState(); + r->stop(); + this->defaultMessageContext()->clear(); + + if (event->button.state & GDK_SHIFT_MASK) { + // with shift, add to selection + selection->addList (items); + } else { + // without shift, simply select anew + selection->setList (items); + } + + } else { // it was just a click, or a too small rubberband + r->stop(); + + if (this->button_press_shift && !rb_escaped && !drag_escaped) { + // this was a shift+click or alt+shift+click, select what was clicked upon + this->button_press_shift = false; + + if (this->button_press_ctrl) { + // go into groups, honoring Alt + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, TRUE); + this->button_press_ctrl = FALSE; + } else { + // don't go into groups, honoring Alt + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), event->button.state & GDK_MOD1_MASK, FALSE); + } + + if (item) { + selection->toggle(item); + item = nullptr; + } + + } else if ((this->button_press_ctrl || this->button_press_alt) && !rb_escaped && !drag_escaped) { // ctrl+click, alt+click + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), this->button_press_alt, this->button_press_ctrl); + + this->button_press_ctrl = FALSE; + this->button_press_alt = FALSE; + + if (item) { + if (selection->includes(item)) { + _seltrans->increaseState(); + } else { + _seltrans->resetState(); + selection->set(item); + } + + item = nullptr; + } + } else { // click without shift, simply deselect, unless with Alt or something was cancelled + if (!selection->isEmpty()) { + if (!(rb_escaped) && !(drag_escaped) && !(event->button.state & GDK_MOD1_MASK)) { + selection->clear(); + } + + rb_escaped = 0; + } + } + } + + ret = TRUE; + } + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + // Think is not necesary now + // desktop->updateNow(); + } + + if (event->button.button == 1) { + Inkscape::Rubberband::get(desktop)->stop(); // might have been started in another tool! + } + + this->button_press_shift = false; + this->button_press_ctrl = false; + this->button_press_alt = false; + break; + + case GDK_SCROLL: { + + GdkEventScroll *scroll_event = (GdkEventScroll*) event; + + if ( ! (scroll_event->state & GDK_MOD1_MASK)) // do nothing specific if alt was not pressed + break; + + bool shift_pressed = scroll_event->state & GDK_SHIFT_MASK; + is_cycling = true; + + /* Rebuild list of items underneath the mouse pointer */ + Geom::Point p = desktop->d2w(desktop->point()); + SPItem *item = desktop->getItemAtPoint(p, true, nullptr); + this->cycling_items.clear(); + + SPItem *tmp = nullptr; + while(item != nullptr) { + this->cycling_items.push_back(item); + item = desktop->getItemAtPoint(p, true, item); + if (selection->includes(item)) tmp = item; + } + + /* Compare current item list with item list during previous scroll ... */ + bool item_lists_differ = this->cycling_items != this->cycling_items_cmp; + + if(item_lists_differ) { + this->sp_select_context_reset_opacities(); + for (auto l : this->cycling_items_cmp) + selection->remove(l); // deselects the previous content of the cycling loop + this->cycling_items_cmp = (this->cycling_items); + + // set opacities in new stack + for(auto item : this->cycling_items) { + if (item) { + Inkscape::DrawingItem *arenaitem = item->get_arenaitem(desktop->dkey); + arenaitem->setOpacity(0.3); + } + } + } + if(!cycling_cur_item) cycling_cur_item = tmp; + + this->cycling_wrap = prefs->getBool("/options/selection/cycleWrap", true); + + // Cycle through the items underneath the mouse pointer, one-by-one + this->sp_select_context_cycle_through_items(selection, scroll_event, shift_pressed); + + ret = TRUE; + + GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel( GTK_WIDGET(desktop->canvas) )); + if (w) { + gtk_window_present(w); + gtk_widget_grab_focus (GTK_WIDGET(desktop->canvas)); + } + break; + } + + case GDK_KEY_PRESS: // keybindings for select context + { + { + guint keyval = get_latin_keyval(&event->key); + + bool alt = ( MOD__ALT(event) + || (keyval == GDK_KEY_Alt_L) + || (keyval == GDK_KEY_Alt_R) + || (keyval == GDK_KEY_Meta_L) + || (keyval == GDK_KEY_Meta_R)); + + if (!key_is_a_modifier (keyval)) { + this->defaultMessageContext()->clear(); + } else if (this->grabbed || _seltrans->isGrabbed()) { + if (Inkscape::Rubberband::get(desktop)->is_started()) { + // if Alt then change cursor to moving cursor: + if (alt) { + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_TOUCHPATH); + } + } else { + // do not change the statusbar text when mousekey is down to move or transform the object, + // because the statusbar text is already updated somewhere else. + break; + } + } else { + sp_event_show_modifier_tip (this->defaultMessageContext(), event, + _("<b>Ctrl</b>: click to select in groups; drag to move hor/vert"), + _("<b>Shift</b>: click to toggle select; drag for rubberband selection"), + _("<b>Alt</b>: click to select under; scroll mouse-wheel to cycle-select; drag to move selected or select by touch")); + + // if Alt and nonempty selection, show moving cursor ("move selected"): + if (alt && !selection->isEmpty() && !desktop->isWaitingCursor()) { + GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + + gdk_window_set_cursor(window, CursorSelectDragging); + } + //*/ + break; + } + } + + gdouble const nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); // in px + gdouble const offset = prefs->getDoubleLimited("/options/defaultscale/value", 2, 0, 1000, "px"); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + auto const y_dir = desktop->yaxisdir(); + + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Left: // move selection left + case GDK_KEY_KP_Left: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events( get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->moveScreen(mul*-10, 0); // shift + } else { + desktop->getSelection()->moveScreen(mul*-1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->move(mul*-10*nudge, 0); // shift + } else { + desktop->getSelection()->move(mul*-nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Up: // move selection up + case GDK_KEY_KP_Up: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + mul *= -y_dir; + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->moveScreen(0, mul*10); // shift + } else { + desktop->getSelection()->moveScreen(0, mul*1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->move(0, mul*10*nudge); // shift + } else { + desktop->getSelection()->move(0, mul*nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Right: // move selection right + case GDK_KEY_KP_Right: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->moveScreen(mul*10, 0); // shift + } else { + desktop->getSelection()->moveScreen(mul*1, 0); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->move(mul*10*nudge, 0); // shift + } else { + desktop->getSelection()->move(mul*nudge, 0); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Down: // move selection down + case GDK_KEY_KP_Down: + if (!MOD__CTRL(event)) { // not ctrl + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + mul *= -y_dir; + + if (MOD__ALT(event)) { // alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->moveScreen(0, mul*-10); // shift + } else { + desktop->getSelection()->moveScreen(0, mul*-1); // no shift + } + } else { // no alt + if (MOD__SHIFT(event)) { + desktop->getSelection()->move(0, mul*-10*nudge); // shift + } else { + desktop->getSelection()->move(0, mul*-nudge); // no shift + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (!this->sp_select_context_abort()) { + selection->clear(); + } + + ret = TRUE; + break; + + case GDK_KEY_a: + case GDK_KEY_A: + if (MOD__CTRL_ONLY(event)) { + sp_edit_select_all(desktop); + ret = TRUE; + } + break; + + case GDK_KEY_space: + /* stamping mode: show outline mode moving */ + /* FIXME: Is next condition ok? (lauris) */ + if (this->dragging && this->grabbed) { + _seltrans->stamp(); + ret = TRUE; + } + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("select-x"); + ret = TRUE; + } + break; + + case GDK_KEY_bracketleft: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + selection->rotateScreen(-mul * y_dir); + } else if (MOD__CTRL(event)) { + selection->rotate(-90 * y_dir); + } else if (snaps) { + selection->rotate(-180.0/snaps * y_dir); + } + + ret = TRUE; + break; + + case GDK_KEY_bracketright: + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events(get_latin_keyval(&event->key), 0); // with any mask + selection->rotateScreen(mul * y_dir); + } else if (MOD__CTRL(event)) { + selection->rotate(90 * y_dir); + } else if (snaps) { + selection->rotate(180.0/snaps * y_dir); + } + + ret = TRUE; + break; + + case GDK_KEY_Return: + if (MOD__CTRL_ONLY(event)) { + if (selection->singleItem()) { + SPItem *clicked_item = selection->singleItem(); + SPGroup *clickedGroup = dynamic_cast<SPGroup *>(clicked_item); + if ( (clickedGroup && (clickedGroup->layerMode() != SPGroup::LAYER)) || dynamic_cast<SPBox3D *>(clicked_item)) { // enter group or a 3D box + desktop->setCurrentLayer(clicked_item); + desktop->getSelection()->clear(); + } else { + this->desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Selected object is not a group. Cannot enter.")); + } + } + + ret = TRUE; + } + break; + + case GDK_KEY_BackSpace: + if (MOD__CTRL_ONLY(event)) { + sp_select_context_up_one_layer(desktop); + ret = TRUE; + } + break; + + case GDK_KEY_s: + case GDK_KEY_S: + if (MOD__SHIFT_ONLY(event)) { + if (!selection->isEmpty()) { + _seltrans->increaseState(); + } + + ret = TRUE; + } + break; + + case GDK_KEY_g: + case GDK_KEY_G: + if (MOD__SHIFT_ONLY(event)) { + desktop->selection->toGuides(); + ret = true; + } + break; + + default: + break; + } + break; + } + case GDK_KEY_RELEASE: { + guint keyval = get_latin_keyval(&event->key); + if (key_is_a_modifier (keyval)) { + this->defaultMessageContext()->clear(); + } + + bool alt = ( MOD__ALT(event) + || (keyval == GDK_KEY_Alt_L) + || (keyval == GDK_KEY_Alt_R) + || (keyval == GDK_KEY_Meta_L) + || (keyval == GDK_KEY_Meta_R)); + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + // if Alt then change cursor to moving cursor: + if (alt) { + Inkscape::Rubberband::get(desktop)->setMode(RUBBERBAND_MODE_RECT); + } + } else { + if (alt) { + // quit cycle-selection and reset opacities + if (is_cycling) { + this->sp_select_context_reset_opacities(); + is_cycling = false; + } + } + } + + // set cursor to default. + if (!desktop->isWaitingCursor()) { + // Do we need to reset the cursor here on key release ? + //GdkWindow* window = gtk_widget_get_window (GTK_WIDGET (desktop->getCanvas())); + //gdk_window_set_cursor(window, event_context->cursor); + } + break; + } + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h new file mode 100644 index 0000000..c721771 --- /dev/null +++ b/src/ui/tools/select-tool.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SELECT_CONTEXT_H__ +#define __SP_SELECT_CONTEXT_H__ + +/* + * Select tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" + +#define SP_SELECT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SelectTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SELECT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SelectTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +struct SPCanvasItem; + +namespace Inkscape { + class SelTrans; + class SelectionDescriber; +} + +namespace Inkscape { +namespace UI { +namespace Tools { + +class SelectTool : public ToolBase { +public: + SelectTool(); + ~SelectTool() override; + + bool dragging; + bool moved; + bool button_press_shift; + bool button_press_ctrl; + bool button_press_alt; + + std::vector<SPItem *> cycling_items; + std::vector<SPItem *> cycling_items_cmp; + SPItem *cycling_cur_item; + bool cycling_wrap; + + SPItem *item; + SPCanvasItem *grabbed; + Inkscape::SelTrans *_seltrans; + Inkscape::SelectionDescriber *_describer; + + static const std::string prefsPath; + + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + bool sp_select_context_abort(); + void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event, bool shift_pressed); + void sp_select_context_reset_opacities(); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp new file mode 100644 index 0000000..aa65c2e --- /dev/null +++ b/src/ui/tools/spiral-tool.cpp @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Spiral drawing context + * + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "selection.h" +#include "verbs.h" + +#include "display/sp-canvas-item.h" +#include "display/sp-canvas.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" +#include "object/sp-spiral.h" + +#include "ui/pixmaps/cursor-spiral.xpm" +#include "ui/shape-editor.h" +#include "ui/tools/spiral-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& SpiralTool::getPrefsPath() { + return SpiralTool::prefsPath; +} + +const std::string SpiralTool::prefsPath = "/tools/shapes/spiral"; + +SpiralTool::SpiralTool() + : ToolBase(cursor_spiral_xpm) + , spiral(nullptr) + , revo(3) + , exp(1) + , t0(0) +{ +} + +void SpiralTool::finish() { + SPDesktop *desktop = this->desktop; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +SpiralTool::~SpiralTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->spiral) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + */ +void SpiralTool::selection_changed(Inkscape::Selection *selection) { + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void SpiralTool::setup() { + ToolBase::setup(); + + sp_event_context_read(this, "expansion"); + sp_event_context_read(this, "revolution"); + sp_event_context_read(this, "t0"); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + Inkscape::Selection *selection = this->desktop->getSelection(); + this->sel_changed_connection.disconnect(); + + this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &SpiralTool::selection_changed)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +void SpiralTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring name = val.getEntryName(); + + if (name == "expansion") { + this->exp = CLAMP(val.getDouble(), 0.0, 1000.0); + } else if (name == "revolution") { + this->revo = CLAMP(val.getDouble(3.0), 0.05, 40.0); + } else if (name == "t0") { + this->t0 = CLAMP(val.getDouble(), 0.0, 0.999); + } +} + +bool SpiralTool::root_handler(GdkEvent* event) { + static gboolean dragging; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = desktop->getSelection(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + dragging = TRUE; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + ( GDK_KEY_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK ), + nullptr, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(this->desktop->w2d(motion_w)); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, this->spiral); + m.freeSnapReturnByRef(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + if (event->button.button == 1 && !this->space_panning) { + dragging = FALSE; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap angle"), + nullptr, + _("<b>Alt</b>: lock spiral radius")); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("spiral-revolutions"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the spiral + this->finish(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void SpiralTool::drag(Geom::Point const &p, guint state) { + SPDesktop *desktop = SP_EVENT_CONTEXT(this)->desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->spiral) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "spiral"); + + // Set style + sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/spiral", false); + + this->spiral = SP_SPIRAL(desktop->currentLayer()->appendChildRepr(repr)); + Inkscape::GC::release(repr); + this->spiral->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->spiral->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true, this->spiral); + Geom::Point pt2g = p; + m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + Geom::Point const p0 = desktop->dt2doc(this->center); + Geom::Point const p1 = desktop->dt2doc(pt2g); + + Geom::Point const delta = p1 - p0; + gdouble const rad = Geom::L2(delta); + + // Start angle calculated from end angle and number of revolutions. + gdouble arg = Geom::atan2(delta) - 2.0*M_PI * spiral->revo; + + if (state & GDK_CONTROL_MASK) { + /* Snap start angle */ + double snaps_radian = M_PI/snaps; + arg = std::round(arg/snaps_radian) * snaps_radian; + } + + /* Fixme: these parameters should be got from dialog box */ + this->spiral->setPosition(p0[Geom::X], p0[Geom::Y], + /*expansion*/ this->exp, + /*revolution*/ this->revo, + rad, arg, + /*t0*/ this->t0); + + /* status text */ + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(rad, "px"); + Glib::ustring rads = q.string(desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + _("<b>Spiral</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle"), + rads.c_str(), arg * 180/M_PI + 360*spiral->revo); +} + +void SpiralTool::finishItem() { + this->message_context->clear(); + + if (this->spiral != nullptr) { + if (this->spiral->rad == 0) { + this->cancel(); // Don't allow the creating of zero sized spiral, for example when the start and and point snap to the snap grid point + return; + } + + spiral->set_shape(); + spiral->updateRepr(SP_OBJECT_WRITE_EXT); + spiral->doWriteTransform(spiral->transform, nullptr, true); + + this->desktop->canvas->endForcedFullRedraws(); + + this->desktop->getSelection()->set(this->spiral); + + DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_SPIRAL, _("Create spiral")); + + this->spiral = nullptr; + } +} + +void SpiralTool::cancel() { + this->desktop->getSelection()->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(this->desktop->acetate)); + + if (this->spiral != nullptr) { + this->spiral->deleteObject(); + this->spiral = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + this->desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(this->desktop->getDocument()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/spiral-tool.h b/src/ui/tools/spiral-tool.h new file mode 100644 index 0000000..03c3ae2 --- /dev/null +++ b/src/ui/tools/spiral-tool.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SPIRAL_CONTEXT_H__ +#define __SP_SPIRAL_CONTEXT_H__ + +/** \file + * Spiral drawing context + */ +/* + * Authors: + * Mitsuru Oka + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2001 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/connection.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#define SP_SPIRAL_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SpiralTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SPIRAL_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SpiralTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +class SPSpiral; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class SpiralTool : public ToolBase { +public: + SpiralTool(); + ~SpiralTool() override; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPSpiral * spiral; + Geom::Point center; + gdouble revo; + gdouble exp; + gdouble t0; + + sigc::connection sel_changed_connection; + + void drag(Geom::Point const &p, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection *selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp new file mode 100644 index 0000000..0c95d68 --- /dev/null +++ b/src/ui/tools/spray-tool.cpp @@ -0,0 +1,1533 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Spray Tool + * + * Authors: + * Pierre-Antoine MARC + * Pierre CACLIN + * Aurel-Aimé MARMION + * Julien LERAY + * Benoît LAVORATA + * Vincent MONTAGNE + * Pierre BARBRY-BLOT + * Steren GIANNINI (steren.giannini@gmail.com) + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * Adrian Boguszewski + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <numeric> +#include <vector> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/circle.h> + + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "include/macros.h" +#include "message-context.h" +#include "path-chemistry.h" +#include "selection.h" +#include "splivarot.h" +#include "verbs.h" + +#include "display/cairo-utils.h" +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "display/drawing-context.h" +#include "display/drawing.h" +#include "display/sp-canvas.h" + +#include "helper/action.h" + +#include "object/box3d.h" +#include "object/sp-item-transform.h" + +#include "ui/pixmaps/cursor-spray.xpm" + +#include "svg/svg.h" +#include "svg/svg-color.h" + +#include "ui/toolbar/spray-toolbar.h" +#include "ui/tools/spray-tool.h" +#include "ui/dialog/dialog-manager.h" + + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff +#define DYNA_MIN_WIDTH 1.0e-6 + +// Disabled in 0.91 because of Bug #1274831 (crash, spraying an object +// with the mode: spray object in single path) +// Please enable again when working on 1.0 +#define ENABLE_SPRAY_MODE_SINGLE_PATH + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum { + PICK_COLOR, + PICK_OPACITY, + PICK_R, + PICK_G, + PICK_B, + PICK_H, + PICK_S, + PICK_L +}; + +const std::string& SprayTool::getPrefsPath() { + return SprayTool::prefsPath; +} + +const std::string SprayTool::prefsPath = "/tools/spray"; + +/** + * This function returns pseudo-random numbers from a normal distribution + * @param mu : mean + * @param sigma : standard deviation ( > 0 ) + */ +inline double NormalDistribution(double mu, double sigma) +{ + // use Box Muller's algorithm + return mu + sigma * sqrt( -2.0 * log(g_random_double_range(0, 1)) ) * cos( 2.0*M_PI*g_random_double_range(0, 1) ); +} + +/* Method to rotate items */ +static void sp_spray_rotate_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Rotate const &rotation) +{ + Geom::Translate const s(c); + Geom::Affine affine = s.inverse() * rotation * s; + // Rotate item. + item->set_i2d_affine(item->i2dt_affine() * affine); + // Use each item's own transform writer, consistent with sp_selection_apply_affine() + item->doWriteTransform(item->transform); + // Restore the center position (it's changed because the bbox center changed) + if (item->isCenterSet()) { + item->setCenter(c); + item->updateRepr(); + } +} + +/* Method to scale items */ +static void sp_spray_scale_rel(Geom::Point c, SPDesktop */*desktop*/, SPItem *item, Geom::Scale const &scale) +{ + Geom::Translate const s(c); + item->set_i2d_affine(item->i2dt_affine() * s.inverse() * scale * s); + item->doWriteTransform(item->transform); +} + +SprayTool::SprayTool() + : ToolBase(cursor_spray_xpm, false) + , pressure(TC_DEFAULT_PRESSURE) + , dragging(false) + , usepressurewidth(false) + , usepressurepopulation(false) + , usepressurescale(false) + , usetilt(false) + , usetext(false) + , width(0.2) + , ratio(0) + , tilt(0) + , rotation_variation(0) + , population(0) + , scale_variation(1) + , scale(1) + , mean(0.2) + , standard_deviation(0.2) + , distrib(1) + , mode(0) + , is_drawing(false) + , is_dilating(false) + , has_dilated(false) + , dilate_area(nullptr) + , no_overlap(false) + , picker(false) + , pick_center(true) + , pick_inverse_value(false) + , pick_fill(false) + , pick_stroke(false) + , pick_no_overlap(false) + , over_transparent(true) + , over_no_transparent(true) + , offset(0) + , pick(0) + , do_trace(false) + , pick_to_size(false) + , pick_to_presence(false) + , pick_to_color(false) + , pick_to_opacity(false) + , invert_picked(false) + , gamma_picked(0) + , rand_picked(0) +{ +} + +SprayTool::~SprayTool() { + object_set.clear(); + this->enableGrDrag(false); + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + sp_canvas_item_destroy(this->dilate_area); + this->dilate_area = nullptr; + } +} + +void SprayTool::update_cursor(bool /*with_shift*/) { + guint num = 0; + gchar *sel_message = nullptr; + + if (!desktop->selection->isEmpty()) { + num = (guint) boost::distance(desktop->selection->items()); + sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num); + } else { + sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected")); + } + + switch (this->mode) { + case SPRAY_MODE_COPY: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>copies</b> of the initial selection."), sel_message); + break; + case SPRAY_MODE_CLONE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray <b>clones</b> of the initial selection."), sel_message); + break; + case SPRAY_MODE_SINGLE_PATH: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag, click or click and scroll to spray in a <b>single path</b> of the initial selection."), sel_message); + break; + default: + break; + } + + this->sp_event_context_update_cursor(); + g_free(sel_message); +} + +void SprayTool::setup() { + ToolBase::setup(); + + { + /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */ + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); + + SPCurve *c = new SPCurve(path); + + this->dilate_area = sp_canvas_bpath_new(this->desktop->getControls(), c); + c->unref(); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->dilate_area); + } + + this->is_drawing = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/dialogs/clonetiler/dotrace", false); + if (prefs->getBool("/tools/spray/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/spray/gradientdrag")) { + this->enableGrDrag(); + } + + sp_event_context_read(this, "distrib"); + sp_event_context_read(this, "width"); + sp_event_context_read(this, "ratio"); + sp_event_context_read(this, "tilt"); + sp_event_context_read(this, "rotation_variation"); + sp_event_context_read(this, "scale_variation"); + sp_event_context_read(this, "mode"); + sp_event_context_read(this, "population"); + sp_event_context_read(this, "mean"); + sp_event_context_read(this, "standard_deviation"); + sp_event_context_read(this, "usepressurewidth"); + sp_event_context_read(this, "usepressurepopulation"); + sp_event_context_read(this, "usepressurescale"); + sp_event_context_read(this, "Scale"); + sp_event_context_read(this, "offset"); + sp_event_context_read(this, "picker"); + sp_event_context_read(this, "pick_center"); + sp_event_context_read(this, "pick_inverse_value"); + sp_event_context_read(this, "pick_fill"); + sp_event_context_read(this, "pick_stroke"); + sp_event_context_read(this, "pick_no_overlap"); + sp_event_context_read(this, "over_no_transparent"); + sp_event_context_read(this, "over_transparent"); + sp_event_context_read(this, "no_overlap"); +} + +void SprayTool::setCloneTilerPrefs() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->do_trace = prefs->getBool("/dialogs/clonetiler/dotrace", false); + this->pick = prefs->getInt("/dialogs/clonetiler/pick"); + this->pick_to_size = prefs->getBool("/dialogs/clonetiler/pick_to_size", false); + this->pick_to_presence = prefs->getBool("/dialogs/clonetiler/pick_to_presence", false); + this->pick_to_color = prefs->getBool("/dialogs/clonetiler/pick_to_color", false); + this->pick_to_opacity = prefs->getBool("/dialogs/clonetiler/pick_to_opacity", false); + this->rand_picked = 0.01 * prefs->getDoubleLimited("/dialogs/clonetiler/rand_picked", 0, 0, 100); + this->invert_picked = prefs->getBool("/dialogs/clonetiler/invert_picked", false); + this->gamma_picked = prefs->getDoubleLimited("/dialogs/clonetiler/gamma_picked", 0, -10, 10); +} + +void SprayTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "mode") { + this->mode = val.getInt(); + this->update_cursor(false); + } else if (path == "width") { + this->width = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "usepressurewidth") { + this->usepressurewidth = val.getBool(); + } else if (path == "usepressurepopulation") { + this->usepressurepopulation = val.getBool(); + } else if (path == "usepressurescale") { + this->usepressurescale = val.getBool(); + } else if (path == "population") { + this->population = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "rotation_variation") { + this->rotation_variation = CLAMP(val.getDouble(0.0), 0, 100.0); + } else if (path == "scale_variation") { + this->scale_variation = CLAMP(val.getDouble(1.0), 0, 100.0); + } else if (path == "standard_deviation") { + this->standard_deviation = 0.01 * CLAMP(val.getInt(10), 1, 100); + } else if (path == "mean") { + this->mean = 0.01 * CLAMP(val.getInt(10), 1, 100); +// Not implemented in the toolbar and preferences yet + } else if (path == "distribution") { + this->distrib = val.getInt(1); + } else if (path == "tilt") { + this->tilt = CLAMP(val.getDouble(0.1), 0, 1000.0); + } else if (path == "ratio") { + this->ratio = CLAMP(val.getDouble(), 0.0, 0.9); + } else if (path == "offset") { + this->offset = val.getDoubleLimited(100.0, 0, 1000.0); + } else if (path == "pick_center") { + this->pick_center = val.getBool(true); + } else if (path == "pick_inverse_value") { + this->pick_inverse_value = val.getBool(false); + } else if (path == "pick_fill") { + this->pick_fill = val.getBool(false); + } else if (path == "pick_stroke") { + this->pick_stroke = val.getBool(false); + } else if (path == "pick_no_overlap") { + this->pick_no_overlap = val.getBool(false); + } else if (path == "over_no_transparent") { + this->over_no_transparent = val.getBool(true); + } else if (path == "over_transparent") { + this->over_transparent = val.getBool(true); + } else if (path == "no_overlap") { + this->no_overlap = val.getBool(false); + } else if (path == "picker") { + this->picker = val.getBool(false); + } +} + +static void sp_spray_extinput(SprayTool *tc, GdkEvent *event) +{ + if (gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &tc->pressure)) { + tc->pressure = CLAMP(tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE); + } else { + tc->pressure = TC_DEFAULT_PRESSURE; + } +} + +static double get_width(SprayTool *tc) +{ + double pressure = (tc->usepressurewidth? tc->pressure / TC_DEFAULT_PRESSURE : 1); + return pressure * tc->width; +} + +static double get_dilate_radius(SprayTool *tc) +{ + return 250 * get_width(tc)/SP_EVENT_CONTEXT(tc)->desktop->current_zoom(); +} + +static double get_path_mean(SprayTool *tc) +{ + return tc->mean; +} + +static double get_path_standard_deviation(SprayTool *tc) +{ + return tc->standard_deviation; +} + +static double get_population(SprayTool *tc) +{ + double pressure = (tc->usepressurepopulation? tc->pressure / TC_DEFAULT_PRESSURE : 1); + return pressure * tc->population; +} + +static double get_pressure(SprayTool *tc) +{ + double pressure = tc->pressure / TC_DEFAULT_PRESSURE; + return pressure; +} + +static double get_move_mean(SprayTool *tc) +{ + return tc->mean; +} + +static double get_move_standard_deviation(SprayTool *tc) +{ + return tc->standard_deviation; +} + +/** + * Method to handle the distribution of the items + * @param[out] radius : radius of the position of the sprayed object + * @param[out] angle : angle of the position of the sprayed object + * @param[in] a : mean + * @param[in] s : standard deviation + * @param[in] choice : + + */ +static void random_position(double &radius, double &angle, double &a, double &s, int /*choice*/) +{ + // angle is taken from an uniform distribution + angle = g_random_double_range(0, M_PI*2.0); + + // radius is taken from a Normal Distribution + double radius_temp =-1; + while(!((radius_temp >= 0) && (radius_temp <=1 ))) + { + radius_temp = NormalDistribution(a, s); + } + // Because we are in polar coordinates, a special treatment has to be done to the radius. + // Otherwise, positions taken from an uniform repartition on radius and angle will not seam to + // be uniformily distributed on the disk (more at the center and less at the boundary). + // We counter this effect with a 0.5 exponent. This is empiric. + radius = pow(radius_temp, 0.5); + +} + +static void sp_spray_transform_path(SPItem * item, Geom::Path &path, Geom::Affine affine, Geom::Point center){ + path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr).inverse(); + path *= item->transform.inverse(); + Geom::Affine dt2p; + if (item->parent) { + dt2p = static_cast<SPItem *>(item->parent)->i2dt_affine().inverse(); + } else { + dt2p = item->document->dt2doc(); + } + Geom::Affine i2dt = item->i2dt_affine() * Geom::Translate(center).inverse() * affine * Geom::Translate(center); + path *= i2dt * dt2p; + path *= i2anc_affine(static_cast<SPItem *>(item->parent), nullptr); +} + +/** +Randomizes \a val by \a rand, with 0 < val < 1 and all values (including 0, 1) having the same +probability of being displaced. + */ +double randomize01(double val, double rand) +{ + double base = MIN (val - rand, 1 - 2*rand); + if (base < 0) { + base = 0; + } + val = base + g_random_double_range (0, MIN (2 * rand, 1 - base)); + return CLAMP(val, 0, 1); // this should be unnecessary with the above provisions, but just in case... +} + +guint32 getPickerData(Geom::IntRect area){ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + double R = 0, G = 0, B = 0, A = 0; + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, area.width(), area.height()); + sp_canvas_arena_render_surface(SP_CANVAS_ARENA(desktop->getDrawing()), s, area); + ink_cairo_surface_average_color(s, R, G, B, A); + cairo_surface_destroy(s); + //this can fix the bug #1511998 if confirmed + if( A == 0 || A < 1e-6){ + R = 1; + G = 1; + B = 1; + } + return SP_RGBA32_F_COMPOSE(R, G, B, A); +} + +static void showHidden(std::vector<SPItem *> items_down){ + for (auto item_hidden : items_down) { + item_hidden->setHidden(false); + item_hidden->updateRepr(); + } +} +//todo: maybe move same parameter to preferences +static bool fit_item(SPDesktop *desktop, + SPItem *item, + Geom::OptRect bbox, + Geom::Point &move, + Geom::Point center, + gint mode, + double angle, + double &_scale, + double scale, + bool picker, + bool pick_center, + bool pick_inverse_value, + bool pick_fill, + bool pick_stroke, + bool pick_no_overlap, + bool over_no_transparent, + bool over_transparent, + bool no_overlap, + double offset, + SPCSSAttr *css, + bool trace_scale, + int pick, + bool do_trace, + bool pick_to_size, + bool pick_to_presence, + bool pick_to_color, + bool pick_to_opacity, + bool invert_picked, + double gamma_picked , + double rand_picked) +{ + SPDocument *doc = item->document; + double width = bbox->width(); + double height = bbox->height(); + double offset_width = (offset * width)/100.0 - (width); + if(offset_width < 0 ){ + offset_width = 0; + } + double offset_height = (offset * height)/100.0 - (height); + if(offset_height < 0 ){ + offset_height = 0; + } + if(picker && pick_to_size && !trace_scale && do_trace){ + _scale = 0.1; + } + Geom::OptRect bbox_procesed = Geom::Rect(Geom::Point(bbox->left() - offset_width, bbox->top() - offset_height),Geom::Point(bbox->right() + offset_width, bbox->bottom() + offset_height)); + Geom::Path path; + path.start(Geom::Point(bbox_procesed->left(), bbox_procesed->top())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->top())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->right(), bbox_procesed->bottom())); + path.appendNew<Geom::LineSegment>(Geom::Point(bbox_procesed->left(), bbox_procesed->bottom())); + path.close(true); + sp_spray_transform_path(item, path, Geom::Scale(_scale), center); + sp_spray_transform_path(item, path, Geom::Scale(scale), center); + sp_spray_transform_path(item, path, Geom::Rotate(angle), center); + path *= Geom::Translate(move); + bbox_procesed = path.boundsFast(); + double bbox_left_main = bbox_procesed->left(); + double bbox_right_main = bbox_procesed->right(); + double bbox_top_main = bbox_procesed->top(); + double bbox_bottom_main = bbox_procesed->bottom(); + double width_transformed = bbox_procesed->width(); + double height_transformed = bbox_procesed->height(); + Geom::Point mid_point = desktop->d2w(bbox_procesed->midpoint() * desktop->doc2dt()); + Geom::IntRect area = Geom::IntRect::from_xywh(floor(mid_point[Geom::X]), floor(mid_point[Geom::Y]), 1, 1); + guint32 rgba = getPickerData(area); + guint32 rgba2 = 0xffffff00; + Geom::Rect rect_sprayed(desktop->d2w(Geom::Point(bbox_left_main,bbox_top_main)), desktop->d2w(Geom::Point(bbox_right_main,bbox_bottom_main))); + if (!rect_sprayed.hasZeroArea()) { + rgba2 = getPickerData(rect_sprayed.roundOutwards()); + } + if(pick_no_overlap) { + if(rgba != rgba2) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + } + if(!pick_center) { + rgba = rgba2; + } + if(!over_transparent && (SP_RGBA32_A_F(rgba) == 0 || SP_RGBA32_A_F(rgba) < 1e-6)) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + if(!over_no_transparent && SP_RGBA32_A_F(rgba) > 0) { + if(mode != SPRAY_MODE_ERASER) { + return false; + } + } + if(offset < 100 ) { + offset_width = ((99.0 - offset) * width_transformed)/100.0 - width_transformed; + offset_height = ((99.0 - offset) * height_transformed)/100.0 - height_transformed; + } else { + offset_width = 0; + offset_height = 0; + } + std::vector<SPItem*> items_down = desktop->getDocument()->getItemsPartiallyInBox(desktop->dkey, *bbox_procesed); + Inkscape::Selection *selection = desktop->getSelection(); + if (selection->isEmpty()) { + return false; + } + std::vector<SPItem*> const items_selected(selection->items().begin(), selection->items().end()); + std::vector<SPItem*> items_down_erased; + for (std::vector<SPItem*>::const_iterator i=items_down.begin(); i!=items_down.end(); ++i) { + SPItem *item_down = *i; + Geom::OptRect bbox_down = item_down->documentVisualBounds(); + double bbox_left = bbox_down->left(); + double bbox_top = bbox_down->top(); + gchar const * item_down_sharp = g_strdup_printf("#%s", item_down->getId()); + items_down_erased.push_back(item_down); + for (auto item_selected : items_selected) { + gchar const * spray_origin; + if(!item_selected->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item_selected->getId()); + } else { + spray_origin = item_selected->getAttribute("inkscape:spray-origin"); + } + if(strcmp(item_down_sharp, spray_origin) == 0 || + (item_down->getAttribute("inkscape:spray-origin") && + strcmp(item_down->getAttribute("inkscape:spray-origin"),spray_origin) == 0 )) + { + if(mode == SPRAY_MODE_ERASER) { + if(strcmp(item_down_sharp, spray_origin) != 0 && !selection->includes(item_down) ){ + item_down->deleteObject(); + items_down_erased.pop_back(); + break; + } + } else if(no_overlap) { + if(!(offset_width < 0 && offset_height < 0 && std::abs(bbox_left - bbox_left_main) > std::abs(offset_width) && + std::abs(bbox_top - bbox_top_main) > std::abs(offset_height))){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } else if(picker || over_transparent || over_no_transparent) { + item_down->setHidden(true); + item_down->updateRepr(); + } + } + } + } + if(mode == SPRAY_MODE_ERASER){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down_erased); + } + return false; + } + if(picker || over_transparent || over_no_transparent){ + if(!no_overlap){ + doc->ensureUpToDate(); + rgba = getPickerData(area); + if (!rect_sprayed.hasZeroArea()) { + rgba2 = getPickerData(rect_sprayed.roundOutwards()); + } + } + if(pick_no_overlap){ + if(rgba != rgba2){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + if(!pick_center){ + rgba = rgba2; + } + double opacity = 1.0; + gchar color_string[32]; *color_string = 0; + float r = SP_RGBA32_R_F(rgba); + float g = SP_RGBA32_G_F(rgba); + float b = SP_RGBA32_B_F(rgba); + float a = SP_RGBA32_A_F(rgba); + if(!over_transparent && (a == 0 || a < 1e-6)){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + if(!over_no_transparent && a > 0){ + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + + if(picker && do_trace){ + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, r, g, b); + + gdouble val = 0; + switch (pick) { + case PICK_COLOR: + val = 1 - hsl[2]; // inverse lightness; to match other picks where black = max + break; + case PICK_OPACITY: + val = a; + break; + case PICK_R: + val = r; + break; + case PICK_G: + val = g; + break; + case PICK_B: + val = b; + break; + case PICK_H: + val = hsl[0]; + break; + case PICK_S: + val = hsl[1]; + break; + case PICK_L: + val = 1 - hsl[2]; + break; + default: + break; + } + + if (rand_picked > 0) { + val = randomize01 (val, rand_picked); + r = randomize01 (r, rand_picked); + g = randomize01 (g, rand_picked); + b = randomize01 (b, rand_picked); + } + + if (gamma_picked != 0) { + double power; + if (gamma_picked > 0) + power = 1/(1 + fabs(gamma_picked)); + else + power = 1 + fabs(gamma_picked); + + val = pow (val, power); + r = pow ((double)r, (double)power); + g = pow ((double)g, (double)power); + b = pow ((double)b, (double)power); + } + + if (invert_picked) { + val = 1 - val; + r = 1 - r; + g = 1 - g; + b = 1 - b; + } + + val = CLAMP (val, 0, 1); + r = CLAMP (r, 0, 1); + g = CLAMP (g, 0, 1); + b = CLAMP (b, 0, 1); + + // recompose tweaked color + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + if (pick_to_size) { + if(!trace_scale){ + if(pick_inverse_value) { + _scale = 1.0 - val; + } else { + _scale = val; + } + if(_scale == 0.0) { + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + if(!fit_item(desktop + , item + , bbox + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , true + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked) + ) + { + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + } + + if (pick_to_opacity) { + if(pick_inverse_value) { + opacity *= 1.0 - val; + } else { + opacity *= val; + } + std::stringstream opacity_str; + opacity_str.imbue(std::locale::classic()); + opacity_str << opacity; + sp_repr_css_set_property(css, "opacity", opacity_str.str().c_str()); + } + if (pick_to_presence) { + if (g_random_double_range (0, 1) > val) { + //Hiding the element is a way to retain original + //behaviour of tiled clones for presence option. + sp_repr_css_set_property(css, "opacity", "0"); + } + } + if (pick_to_color) { + sp_svg_write_color(color_string, sizeof(color_string), rgba); + if(pick_fill){ + sp_repr_css_set_property(css, "fill", color_string); + } + if(pick_stroke){ + sp_repr_css_set_property(css, "stroke", color_string); + } + } + if (opacity < 1e-6) { // invisibly transparent, skip + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + return false; + } + } + if(!do_trace){ + if(!pick_center){ + rgba = rgba2; + } + if (pick_inverse_value) { + r = 1 - SP_RGBA32_R_F(rgba); + g = 1 - SP_RGBA32_G_F(rgba); + b = 1 - SP_RGBA32_B_F(rgba); + } else { + r = SP_RGBA32_R_F(rgba); + g = SP_RGBA32_G_F(rgba); + b = SP_RGBA32_B_F(rgba); + } + rgba = SP_RGBA32_F_COMPOSE(r, g, b, a); + sp_svg_write_color(color_string, sizeof(color_string), rgba); + if(pick_fill){ + sp_repr_css_set_property(css, "fill", color_string); + } + if(pick_stroke){ + sp_repr_css_set_property(css, "stroke", color_string); + } + } + if(!no_overlap && (picker || over_transparent || over_no_transparent)){ + showHidden(items_down); + } + } + return true; +} + +static bool sp_spray_recursive(SPDesktop *desktop, + Inkscape::ObjectSet *set, + SPItem *item, + Geom::Point p, + Geom::Point /*vector*/, + gint mode, + double radius, + double population, + double &scale, + double scale_variation, + bool /*reverse*/, + double mean, + double standard_deviation, + double ratio, + double tilt, + double rotation_variation, + gint _distrib, + bool no_overlap, + bool picker, + bool pick_center, + bool pick_inverse_value, + bool pick_fill, + bool pick_stroke, + bool pick_no_overlap, + bool over_no_transparent, + bool over_transparent, + double offset, + bool usepressurescale, + double pressure, + int pick, + bool do_trace, + bool pick_to_size, + bool pick_to_presence, + bool pick_to_color, + bool pick_to_opacity, + bool invert_picked, + double gamma_picked , + double rand_picked) +{ + bool did = false; + + { + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + // convert 3D boxes to ordinary groups before spraying their shapes + item = box3d_convert_to_group(box); + set->add(item); + } + } + + double _fid = g_random_double_range(0, 1); + double angle = g_random_double_range( - rotation_variation / 100.0 * M_PI , rotation_variation / 100.0 * M_PI ); + double _scale = g_random_double_range( 1.0 - scale_variation / 100.0, 1.0 + scale_variation / 100.0 ); + if(usepressurescale){ + _scale = pressure; + } + double dr; double dp; + random_position( dr, dp, mean, standard_deviation, _distrib ); + dr=dr*radius; + + if (mode == SPRAY_MODE_COPY || mode == SPRAY_MODE_ERASER) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if(_fid <= population) + { + SPDocument *doc = item->document; + gchar const * spray_origin; + if(!item->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item->getId()); + } else { + spray_origin = item->getAttribute("inkscape:spray-origin"); + } + Geom::Point center = item->getCenter(); + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + SPCSSAttr *css = sp_repr_css_attr_new(); + if(mode == SPRAY_MODE_ERASER || no_overlap || picker || !over_transparent || !over_no_transparent){ + if(!fit_item(desktop + , item + , a + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , false + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked)){ + return false; + } + } + SPItem *item_copied; + // Duplicate + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + if(!copy->attribute("inkscape:spray-origin")){ + copy->setAttribute("inkscape:spray-origin", spray_origin); + } + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + item_copied = dynamic_cast<SPItem *>(new_obj); // Conversion object->item + sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(_scale)); + sp_spray_scale_rel(center,desktop, item_copied, Geom::Scale(scale)); + sp_spray_rotate_rel(center,desktop,item_copied, Geom::Rotate(angle)); + // Move the cursor p + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + Inkscape::GC::release(copy); + if(picker){ + sp_desktop_apply_css_recursive(item_copied, css, true); + } + did = true; + } + } +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + } else if (mode == SPRAY_MODE_SINGLE_PATH) { + long setSize = boost::distance(set->items()); + SPItem *parent_item = setSize > 0 ? set->items().front() : nullptr; // Initial object + SPItem *unionResult = setSize > 1 ? *(++set->items().begin()) : nullptr; // Previous union + SPItem *item_copied = nullptr; // Projected object + + if (parent_item) { + SPDocument *doc = parent_item->document; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = parent_item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + + Geom::OptRect a = parent_item->documentVisualBounds(); + if (a) { + if (_fid <= population) { // Rules the population of objects sprayed + // Duplicates the parent item + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + gchar const * spray_origin; + if(!copy->attribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", old_repr->attribute("id")); + copy->setAttribute("inkscape:spray-origin", spray_origin); + } else { + spray_origin = copy->attribute("inkscape:spray-origin"); + } + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + item_copied = dynamic_cast<SPItem *>(new_obj); + + // Move around the cursor + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + + Geom::Point center = parent_item->getCenter(); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); + sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + + // Union and duplication + set->clear(); + set->add(item_copied); + if (unionResult) { // No need to add the very first item (initialized with NULL). + set->add(unionResult); + } + set->pathUnion(true); + set->add(parent_item); + Inkscape::GC::release(copy); + did = true; + } + } + } +#endif + } else if (mode == SPRAY_MODE_CLONE) { + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + if(_fid <= population) { + SPDocument *doc = item->document; + gchar const * spray_origin; + if(!item->getAttribute("inkscape:spray-origin")){ + spray_origin = g_strdup_printf("#%s", item->getId()); + } else { + spray_origin = item->getAttribute("inkscape:spray-origin"); + } + Geom::Point center=item->getCenter(); + Geom::Point move = (Geom::Point(cos(tilt)*cos(dp)*dr/(1-ratio)+sin(tilt)*sin(dp)*dr/(1+ratio), -sin(tilt)*cos(dp)*dr/(1-ratio)+cos(tilt)*sin(dp)*dr/(1+ratio)))+(p-a->midpoint()); + SPCSSAttr *css = sp_repr_css_attr_new(); + if(mode == SPRAY_MODE_ERASER || no_overlap || picker || !over_transparent || !over_no_transparent){ + if(!fit_item(desktop + , item + , a + , move + , center + , mode + , angle + , _scale + , scale + , picker + , pick_center + , pick_inverse_value + , pick_fill + , pick_stroke + , pick_no_overlap + , over_no_transparent + , over_transparent + , no_overlap + , offset + , css + , true + , pick + , do_trace + , pick_to_size + , pick_to_presence + , pick_to_color + , pick_to_opacity + , invert_picked + , gamma_picked + , rand_picked)) + { + return false; + } + } + SPItem *item_copied; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + Inkscape::XML::Node *parent = old_repr->parent(); + + // Creation of the clone + Inkscape::XML::Node *clone = xml_doc->createElement("svg:use"); + // Ad the clone to the list of the parent's children + parent->appendChild(clone); + // Generates the link between parent and child attributes + if(!clone->attribute("inkscape:spray-origin")){ + clone->setAttribute("inkscape:spray-origin", spray_origin); + } + gchar *href_str = g_strdup_printf("#%s", old_repr->attribute("id")); + clone->setAttribute("xlink:href", href_str); + g_free(href_str); + + SPObject *clone_object = doc->getObjectByRepr(clone); + // Conversion object->item + item_copied = dynamic_cast<SPItem *>(clone_object); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(_scale, _scale)); + sp_spray_scale_rel(center, desktop, item_copied, Geom::Scale(scale, scale)); + sp_spray_rotate_rel(center, desktop, item_copied, Geom::Rotate(angle)); + item_copied->move_rel(Geom::Translate(move * desktop->doc2dt().withoutTranslation())); + if(picker){ + sp_desktop_apply_css_recursive(item_copied, css, true); + } + Inkscape::GC::release(clone); + did = true; + } + } + } + + return did; +} + +static bool sp_spray_dilate(SprayTool *tc, Geom::Point /*event_p*/, Geom::Point p, Geom::Point vector, bool reverse) +{ + SPDesktop *desktop = tc->desktop; + Inkscape::ObjectSet *set = tc->objectSet(); + if (set->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + double population = get_population(tc); + if (radius == 0 || population == 0) { + return false; + } + double path_mean = get_path_mean(tc); + if (radius == 0 || path_mean == 0) { + return false; + } + double path_standard_deviation = get_path_standard_deviation(tc); + if (radius == 0 || path_standard_deviation == 0) { + return false; + } + double move_mean = get_move_mean(tc); + double move_standard_deviation = get_move_standard_deviation(tc); + + { + std::vector<SPItem*> const items(set->items().begin(), set->items().end()); + + for(auto item : items){ + g_assert(item != nullptr); + sp_object_ref(item); + } + + for(auto item : items){ + g_assert(item != nullptr); + if (sp_spray_recursive(desktop + , set + , item + , p, vector + , tc->mode + , radius + , population + , tc->scale + , tc->scale_variation + , reverse + , move_mean + , move_standard_deviation + , tc->ratio + , tc->tilt + , tc->rotation_variation + , tc->distrib + , tc->no_overlap + , tc->picker + , tc->pick_center + , tc->pick_inverse_value + , tc->pick_fill + , tc->pick_stroke + , tc->pick_no_overlap + , tc->over_no_transparent + , tc->over_transparent + , tc->offset + , tc->usepressurescale + , get_pressure(tc) + , tc->pick + , tc->do_trace + , tc->pick_to_size + , tc->pick_to_presence + , tc->pick_to_color + , tc->pick_to_opacity + , tc->invert_picked + , tc->gamma_picked + , tc->rand_picked)) { + did = true; + } + } + + for(auto item : items){ + g_assert(item != nullptr); + sp_object_unref(item); + } + } + + return did; +} + +static void sp_spray_update_area(SprayTool *tc) +{ + double radius = get_dilate_radius(tc); + Geom::Affine const sm ( Geom::Scale(radius/(1-tc->ratio), radius/(1+tc->ratio)) ); + sp_canvas_item_affine_absolute(tc->dilate_area, (sm* Geom::Rotate(tc->tilt))* Geom::Translate(SP_EVENT_CONTEXT(tc)->desktop->point())); + sp_canvas_item_show(tc->dilate_area); +} + +static void sp_spray_switch_mode(SprayTool *tc, gint mode, bool with_shift) +{ + // Select the button mode + auto tb = dynamic_cast<UI::Toolbar::SprayToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("SprayToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Spray toolbar" << std::endl; + } + + // Need to set explicitly, because the prefs may not have changed by the previous + tc->mode = mode; + tc->update_cursor(with_shift); +} + +bool SprayTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + sp_canvas_item_show(this->dilate_area); + break; + case GDK_LEAVE_NOTIFY: + sp_canvas_item_hide(this->dilate_area); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return TRUE; + } + this->setCloneTilerPrefs(); + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + this->last_push = desktop->dt2doc(motion_dt); + + sp_spray_extinput(this, event); + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + set_high_motion_precision(); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + + object_set = *desktop->getSelection(); + if (mode == SPRAY_MODE_SINGLE_PATH) { + desktop->getSelection()->clear(); + } + + sp_spray_dilate(this, motion_w, this->last_push, Geom::Point(0,0), MOD__SHIFT(event)); + + this->has_dilated = true; + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + Geom::Point motion_doc(desktop->dt2doc(motion_dt)); + sp_spray_extinput(this, event); + + // Draw the dilating cursor + double radius = get_dilate_radius(this); + Geom::Affine const sm (Geom::Scale(radius/(1-this->ratio), radius/(1+this->ratio)) ); + sp_canvas_item_affine_absolute(this->dilate_area, (sm*Geom::Rotate(this->tilt))*Geom::Translate(desktop->w2d(motion_w))); + sp_canvas_item_show(this->dilate_area); + + guint num = 0; + if (!desktop->selection->isEmpty()) { + num = (guint) boost::distance(desktop->selection->items()); + } + if (num == 0) { + this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to spray.")); + } + + // Dilating: + if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) { + sp_spray_dilate(this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false); + //this->last_push = motion_doc; + this->has_dilated = true; + + // It's slow, so prevent clogging up with events + gobble_motion_events(GDK_BUTTON1_MASK); + return TRUE; + } + } + break; + /* Spray with the scroll */ + case GDK_SCROLL: { + if (event->scroll.state & GDK_BUTTON1_MASK) { + double temp ; + temp = this->population; + this->population = 1.0; + desktop->setToolboxAdjustmentValue("population", this->population * 100); + Geom::Point const scroll_w(event->button.x, event->button.y); + Geom::Point const scroll_dt = desktop->point();; + + switch (event->scroll.direction) { + case GDK_SCROLL_DOWN: + case GDK_SCROLL_UP: + case GDK_SCROLL_SMOOTH: { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return TRUE; + } + this->last_push = desktop->dt2doc(scroll_dt); + sp_spray_extinput(this, event); + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + if(this->is_dilating && !this->space_panning) { + sp_spray_dilate(this, scroll_w, desktop->dt2doc(scroll_dt), Geom::Point(0,0), false); + } + this->has_dilated = true; + + this->population = temp; + desktop->setToolboxAdjustmentValue("population", this->population * 100); + + ret = TRUE; + } + break; + case GDK_SCROLL_RIGHT: + {} break; + case GDK_SCROLL_LEFT: + {} break; + } + } + break; + } + + case GDK_BUTTON_RELEASE: { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + desktop->canvas->endForcedFullRedraws(); + set_high_motion_precision(false); + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1 && !this->space_panning) { + if (!this->has_dilated) { + // If we did not rub, do a light tap + this->pressure = 0.03; + sp_spray_dilate(this, motion_w, desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event)); + } + this->is_dilating = false; + this->has_dilated = false; + switch (this->mode) { + case SPRAY_MODE_COPY: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_SPRAY, _("Spray with copies")); + break; + case SPRAY_MODE_CLONE: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_SPRAY, _("Spray with clones")); + break; + case SPRAY_MODE_SINGLE_PATH: + object_set.pathUnion(true); + desktop->getSelection()->add(object_set.objects().begin(), object_set.objects().end()); + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_SPRAY, _("Spray in single path")); + break; + } + } + object_set.clear(); + break; + } + + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_j: + case GDK_KEY_J: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_COPY, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_k: + case GDK_KEY_K: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_CLONE, MOD__SHIFT(event)); + ret = TRUE; + } + break; +#ifdef ENABLE_SPRAY_MODE_SINGLE_PATH + case GDK_KEY_l: + case GDK_KEY_L: + if (MOD__SHIFT_ONLY(event)) { + sp_spray_switch_mode(this, SPRAY_MODE_SINGLE_PATH, MOD__SHIFT(event)); + ret = TRUE; + } + break; +#endif + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->population += 0.01; + if (this->population > 1.0) { + this->population = 1.0; + } + desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->population -= 0.01; + if (this->population < 0.0) { + this->population = 0.0; + } + desktop->setToolboxAdjustmentValue("spray-population", this->population * 100); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + if (this->width > 1.0) { + this->width = 1.0; + } + // The same spinbutton is for alt+x + desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + if (this->width < 0.01) { + this->width = 0.01; + } + desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + desktop->setToolboxAdjustmentValue("spray-width", this->width * 100); + sp_spray_update_area(this); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo("spray-width"); + ret = TRUE; + } + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(true); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(false); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); + this->message_context->clear(); + break; + default: + sp_spray_switch_mode (this, prefs->getInt("/tools/spray/mode"), MOD__SHIFT(event)); + break; + } + } + + default: + break; + } + + if (!ret) { +// if ((SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler) { +// ret = (SP_EVENT_CONTEXT_CLASS(sp_spray_context_parent_class))->root_handler(event_context, event); +// } + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/tools/spray-tool.h b/src/ui/tools/spray-tool.h new file mode 100644 index 0000000..a4d5d92 --- /dev/null +++ b/src/ui/tools/spray-tool.h @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_SPRAY_CONTEXT_H__ +#define __SP_SPRAY_CONTEXT_H__ + +/* + * Spray Tool + * + * Authors: + * Pierre-Antoine MARC + * Pierre CACLIN + * Aurel-Aimé MARMION + * Julien LERAY + * Benoît LAVORATA + * Vincent MONTAGNE + * Pierre BARBRY-BLOT + * Jabiertxo ARRAIZA + * Adrian Boguszewski + * + * Copyright (C) 2009 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +#define SP_SPRAY_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::SprayTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_SPRAY_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::SprayTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { + namespace UI { + namespace Dialog { + class Dialog; + } + } +} + + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +#define TC_MIN_PRESSURE 0.0 +#define TC_MAX_PRESSURE 1.0 +#define TC_DEFAULT_PRESSURE 0.35 + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum { + SPRAY_MODE_COPY, + SPRAY_MODE_CLONE, + SPRAY_MODE_SINGLE_PATH, + SPRAY_MODE_ERASER, + SPRAY_OPTION, +}; + +class SprayTool : public ToolBase { +public: + SprayTool(); + ~SprayTool() override; + + //ToolBase event_context; + //Inkscape::UI::Dialog::Dialog *dialog_option;//Attribut de type SprayOptionClass, localisé dans scr/ui/dialog + /* extended input data */ + gdouble pressure; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressurewidth; + bool usepressurepopulation; + bool usepressurescale; + bool usetilt; + bool usetext; + + double width; + double ratio; + double tilt; + double rotation_variation; + double population; + double scale_variation; + double scale; + double mean; + double standard_deviation; + + gint distrib; + + gint mode; + + bool is_drawing; + + bool is_dilating; + bool has_dilated; + Geom::Point last_push; + SPCanvasItem *dilate_area; + bool no_overlap; + bool picker; + bool pick_center; + bool pick_inverse_value; + bool pick_fill; + bool pick_stroke; + bool pick_no_overlap; + bool over_transparent; + bool over_no_transparent; + double offset; + int pick; + bool do_trace; + bool pick_to_size; + bool pick_to_presence; + bool pick_to_color; + bool pick_to_opacity; + bool invert_picked; + double gamma_picked; + double rand_picked; + sigc::connection style_set_connection; + + static const std::string prefsPath; + + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + virtual void setCloneTilerPrefs(); + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + + void update_cursor(bool /*with_shift*/); + + ObjectSet* objectSet() { + return &object_set; + } + +private: + ObjectSet object_set; +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/tools/star-tool.cpp b/src/ui/tools/star-tool.cpp new file mode 100644 index 0000000..8b916ae --- /dev/null +++ b/src/ui/tools/star-tool.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Star drawing context + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "message-context.h" +#include "selection.h" +#include "verbs.h" + +#include "display/sp-canvas.h" +#include "display/sp-canvas-item.h" + +#include "include/macros.h" + +#include "object/sp-namedview.h" +#include "object/sp-star.h" + +#include "ui/pixmaps/cursor-star.xpm" + +#include "ui/shape-editor.h" +#include "ui/tools/star-tool.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& StarTool::getPrefsPath() { + return StarTool::prefsPath; +} + +const std::string StarTool::prefsPath = "/tools/shapes/star"; + +StarTool::StarTool() + : ToolBase(cursor_star_xpm) + , star(nullptr) + , magnitude(5) + , proportion(0.5) + , isflatsided(false) + , rounded(0) + , randomized(0) +{ +} + +void StarTool::finish() { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + this->finishItem(); + this->sel_changed_connection.disconnect(); + + ToolBase::finish(); +} + +StarTool::~StarTool() { + this->enableGrDrag(false); + + this->sel_changed_connection.disconnect(); + + delete this->shape_editor; + this->shape_editor = nullptr; + + /* fixme: This is necessary because we do not grab */ + if (this->star) { + this->finishItem(); + } +} + +/** + * Callback that processes the "changed" signal on the selection; + * destroys old and creates new knotholder. + * + * @param selection Should not be NULL. + */ +void StarTool::selection_changed(Inkscape::Selection* selection) { + g_assert (selection != nullptr); + + this->shape_editor->unset_item(); + this->shape_editor->set_item(selection->singleItem()); +} + +void StarTool::setup() { + ToolBase::setup(); + + sp_event_context_read(this, "magnitude"); + sp_event_context_read(this, "proportion"); + sp_event_context_read(this, "isflatsided"); + sp_event_context_read(this, "rounded"); + sp_event_context_read(this, "randomized"); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item) { + this->shape_editor->set_item(item); + } + + Inkscape::Selection *selection = this->desktop->getSelection(); + + this->sel_changed_connection.disconnect(); + + this->sel_changed_connection = selection->connectChanged(sigc::mem_fun(this, &StarTool::selection_changed)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/shapes/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/shapes/gradientdrag")) { + this->enableGrDrag(); + } +} + +void StarTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "magnitude") { + this->magnitude = CLAMP(val.getInt(5), 3, 1024); + } else if (path == "proportion") { + this->proportion = CLAMP(val.getDouble(0.5), 0.01, 2.0); + } else if (path == "isflatsided") { + this->isflatsided = val.getBool(); + } else if (path == "rounded") { + this->rounded = val.getDouble(); + } else if (path == "randomized") { + this->randomized = val.getDouble(); + } +} + +bool StarTool::root_handler(GdkEvent* event) { + static bool dragging; + + SPDesktop *desktop = this->desktop; + Inkscape::Selection *selection = desktop->getSelection(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + gint ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + dragging = true; + + this->center = Inkscape::setup_for_drag_start(desktop, this, event); + + /* Snap center */ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop, true); + m.freeSnapReturnByRef(this->center, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK | + GDK_BUTTON_PRESS_MASK, + nullptr, event->button.time); + ret = TRUE; + } + break; + + case GDK_MOTION_NOTIFY: + if (dragging && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + this->drag(motion_dt, event->motion.state); + + gobble_motion_events(GDK_BUTTON1_MASK); + + ret = TRUE; + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_NODE_HANDLE)); + m.unSetup(); + } + break; + case GDK_BUTTON_RELEASE: + this->xp = this->yp = 0; + + if (event->button.button == 1 && !this->space_panning) { + dragging = false; + + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the star + this->finishItem(); + } else if (this->item_to_select) { + // no dragging, select clicked item if any + if (event->button.state & GDK_SHIFT_MASK) { + selection->toggle(this->item_to_select); + } else { + selection->set(this->item_to_select); + } + } else { + // click in an empty space + selection->clear(); + } + + this->item_to_select = nullptr; + ret = TRUE; + sp_canvas_item_ungrab(SP_CANVAS_ITEM (desktop->acetate)); + } + break; + + case GDK_KEY_PRESS: + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt (at least on my machine) + case GDK_KEY_Meta_R: + sp_event_show_modifier_tip(this->defaultMessageContext(), event, + _("<b>Ctrl</b>: snap angle; keep rays radial"), + nullptr, + nullptr); + break; + + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-star"); + ret = TRUE; + } + break; + + case GDK_KEY_Escape: + if (dragging) { + dragging = false; + sp_event_context_discard_delayed_snap_event(this); + // if drawing, cancel, otherwise pass it up for deselecting + this->cancel(); + ret = TRUE; + } + break; + + case GDK_KEY_space: + if (dragging) { + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + dragging = false; + + sp_event_context_discard_delayed_snap_event(this); + + if (!this->within_tolerance) { + // we've been dragging, finish the star + this->finishItem(); + } + // do not return true, so that space would work switching to selector + } + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + case GDK_KEY_Meta_L: // Meta is when you press Shift+Alt + case GDK_KEY_Meta_R: + this->defaultMessageContext()->clear(); + break; + + default: + break; + } + break; + + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +void StarTool::drag(Geom::Point p, guint state) +{ + SPDesktop *desktop = this->desktop; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int const snaps = prefs->getInt("/options/rotationsnapsperpi/value", 12); + + if (!this->star) { + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return; + } + + // Create object + Inkscape::XML::Document *xml_doc = this->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + repr->setAttribute("sodipodi:type", "star"); + + // Set style + sp_desktop_apply_style_tool(desktop, repr, "/tools/shapes/star", false); + + this->star = SP_STAR(desktop->currentLayer()->appendChildRepr(repr)); + + Inkscape::GC::release(repr); + this->star->transform = SP_ITEM(desktop->currentLayer())->i2doc_affine().inverse(); + this->star->updateRepr(); + + desktop->canvas->forceFullRedrawAfterInterruptions(5); + } + + /* Snap corner point with no constraints */ + SnapManager &m = desktop->namedview->snap_manager; + + m.setup(desktop, true, this->star); + Geom::Point pt2g = p; + m.freeSnapReturnByRef(pt2g, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + Geom::Point const p0 = desktop->dt2doc(this->center); + Geom::Point const p1 = desktop->dt2doc(pt2g); + + double const sides = (gdouble) this->magnitude; + Geom::Point const d = p1 - p0; + Geom::Coord const r1 = Geom::L2(d); + double arg1 = atan2(d); + + if (state & GDK_CONTROL_MASK) { + /* Snap angle */ + double snaps_radian = M_PI/snaps; + arg1 = std::round(arg1/snaps_radian) * snaps_radian; + } + + sp_star_position_set(this->star, this->magnitude, p0, r1, r1 * this->proportion, + arg1, arg1 + M_PI / sides, this->isflatsided, this->rounded, this->randomized); + + /* status text */ + Inkscape::Util::Quantity q = Inkscape::Util::Quantity(r1, "px"); + Glib::ustring rads = q.string(desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, + ( this->isflatsided? + _("<b>Polygon</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle") : + _("<b>Star</b>: radius %s, angle %.2f°; with <b>Ctrl</b> to snap angle") ), + rads.c_str(), arg1 * 180 / M_PI); +} + +void StarTool::finishItem() { + this->message_context->clear(); + + if (this->star != nullptr) { + if (this->star->r[1] == 0) { + // Don't allow the creating of zero sized arc, for example + // when the start and and point snap to the snap grid point + this->cancel(); + return; + } + + // Set transform center, so that odd stars rotate correctly + // LP #462157 + this->star->setCenter(this->center); + this->star->set_shape(); + this->star->updateRepr(SP_OBJECT_WRITE_EXT); + this->star->doWriteTransform(this->star->transform, nullptr, true); + desktop->canvas->endForcedFullRedraws(); + + desktop->getSelection()->set(this->star); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_STAR, + _("Create star")); + + this->star = nullptr; + } +} + +void StarTool::cancel() { + desktop->getSelection()->clear(); + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + if (this->star != nullptr) { + this->star->deleteObject(); + this->star = nullptr; + } + + this->within_tolerance = false; + this->xp = 0; + this->yp = 0; + this->item_to_select = nullptr; + + desktop->canvas->endForcedFullRedraws(); + + DocumentUndo::cancel(desktop->getDocument()); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/star-tool.h b/src/ui/tools/star-tool.h new file mode 100644 index 0000000..08f0d9d --- /dev/null +++ b/src/ui/tools/star-tool.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_STAR_CONTEXT_H__ +#define __SP_STAR_CONTEXT_H__ + +/* + * Star drawing context + * + * Authors: + * Mitsuru Oka <oka326@parkcity.ne.jp> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2001-2002 Mitsuru Oka + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" + +class SPStar; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class StarTool : public ToolBase { +public: + StarTool(); + ~StarTool() override; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPStar* star; + + Geom::Point center; + + /* Number of corners */ + gint magnitude; + + /* Outer/inner radius ratio */ + gdouble proportion; + + /* flat sides or not? */ + bool isflatsided; + + /* rounded corners ratio */ + gdouble rounded; + + // randomization + gdouble randomized; + + sigc::connection sel_changed_connection; + + void drag(Geom::Point p, guint state); + void finishItem(); + void cancel(); + void selection_changed(Inkscape::Selection* selection); +}; + +} +} +} + +#endif diff --git a/src/ui/tools/text-tool.cpp b/src/ui/tools/text-tool.cpp new file mode 100644 index 0000000..5c2e2ca --- /dev/null +++ b/src/ui/tools/text-tool.cpp @@ -0,0 +1,1897 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * TextTool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> +#include <gtkmm/clipboard.h> +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include <display/sp-canvas.h> +#include <display/sp-ctrlline.h> +#include <display/sodipodi-ctrlrect.h> +#include <display/sp-ctrlquadr.h> + +#include "context-fns.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "include/macros.h" +#include "inkscape.h" +#include "message-context.h" +#include "message-stack.h" +#include "rubberband.h" +#include "selection-chemistry.h" +#include "selection.h" +#include "text-editing.h" +#include "verbs.h" + +#include "object/sp-flowtext.h" +#include "object/sp-namedview.h" +#include "object/sp-text.h" +#include "object/sp-rect.h" +#include "object/sp-shape.h" +#include "object/sp-ellipse.h" + +#include "style.h" + +#include "ui/pixmaps/cursor-text-insert.xpm" +#include "ui/pixmaps/cursor-text.xpm" + +#include "ui/control-manager.h" +#include "ui/shape-editor.h" +#include "ui/tools/text-tool.h" + +#include "xml/attribute-record.h" +#include "xml/node-event-vector.h" +#include "xml/sp-css-attr.h" + +using Inkscape::ControlManager; +using Inkscape::DocumentUndo; + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void sp_text_context_validate_cursor_iterators(TextTool *tc); +static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see = true); +static void sp_text_context_update_text_selection(TextTool *tc); +static gint sp_text_context_timeout(TextTool *tc); +static void sp_text_context_forget_text(TextTool *tc); + +static gint sptc_focus_in(GtkWidget *widget, GdkEventFocus *event, TextTool *tc); +static gint sptc_focus_out(GtkWidget *widget, GdkEventFocus *event, TextTool *tc); +static void sptc_commit(GtkIMContext *imc, gchar *string, TextTool *tc); + +const std::string& TextTool::getPrefsPath() { + return TextTool::prefsPath; +} + +const std::string TextTool::prefsPath = "/tools/text"; + + +TextTool::TextTool() + : ToolBase(cursor_text_xpm) + , imc(nullptr) + , text(nullptr) + , pdoc(0, 0) + , unimode(false) + , unipos(0) + , cursor(nullptr) + , indicator(nullptr) + , frame(nullptr) + , timeout(0) + , show(false) + , phase(false) + , nascent_object(false) + , over_text(false) + , dragging(0) + , creating(false) + , grabbed(nullptr) + , preedit_string(nullptr) +{ +} + +TextTool::~TextTool() { + delete this->shape_editor; + this->shape_editor = nullptr; + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + Inkscape::Rubberband::get(this->desktop)->stop(); +} + +void TextTool::setup() { + GtkSettings* settings = gtk_settings_get_default(); + gint timeout = 0; + g_object_get( settings, "gtk-cursor-blink-time", &timeout, NULL ); + + if (timeout < 0) { + timeout = 200; + } else { + timeout /= 2; + } + + this->cursor = ControlManager::getManager().createControlLine(desktop->getControls(), Geom::Point(100, 0), Geom::Point(100, 100)); + this->cursor->setRgba32(0x000000ff); + sp_canvas_item_hide(this->cursor); + + // The rectangle box tightly wrapping text object when selected or under cursor. + this->indicator = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRLRECT, nullptr); + SP_CTRLRECT(this->indicator)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100))); + SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0); + SP_CTRLRECT(this->indicator)->setShadow(1, 0xffffff7f); + sp_canvas_item_hide(this->indicator); + + // The rectangle box outlining wrapping the shape for text in a shape. + this->frame = sp_canvas_item_new(desktop->getControls(), SP_TYPE_CTRLRECT, nullptr); + SP_CTRLRECT(this->frame)->setRectangle(Geom::Rect(Geom::Point(0, 0), Geom::Point(100, 100))); + SP_CTRLRECT(this->frame)->setColor(0x0000ff7f, false, 0); + sp_canvas_item_hide(this->frame); + + this->timeout = g_timeout_add(timeout, (GSourceFunc) sp_text_context_timeout, this); + + this->imc = gtk_im_multicontext_new(); + if (this->imc) { + GtkWidget *canvas = GTK_WIDGET(desktop->getCanvas()); + + /* im preedit handling is very broken in inkscape for + * multi-byte characters. See bug 1086769. + * We need to let the IM handle the preediting, and + * just take in the characters when they're finished being + * entered. + */ + gtk_im_context_set_use_preedit(this->imc, FALSE); + gtk_im_context_set_client_window(this->imc, + gtk_widget_get_window (canvas)); + + g_signal_connect(G_OBJECT(canvas), "focus_in_event", G_CALLBACK(sptc_focus_in), this); + g_signal_connect(G_OBJECT(canvas), "focus_out_event", G_CALLBACK(sptc_focus_out), this); + g_signal_connect(G_OBJECT(this->imc), "commit", G_CALLBACK(sptc_commit), this); + + if (gtk_widget_has_focus(canvas)) { + sptc_focus_in(canvas, nullptr, this); + } + } + + ToolBase::setup(); + + this->shape_editor = new ShapeEditor(this->desktop); + + SPItem *item = this->desktop->getSelection()->singleItem(); + if (item && ( + (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) || + (SP_IS_TEXT(item) && !SP_TEXT(item)->has_shape_inside()) ) + ) { + this->shape_editor->set_item(item); + } + + this->sel_changed_connection = desktop->getSelection()->connectChangedFirst( + sigc::mem_fun(*this, &TextTool::_selectionChanged) + ); + this->sel_modified_connection = desktop->getSelection()->connectModifiedFirst( + sigc::mem_fun(*this, &TextTool::_selectionModified) + ); + this->style_set_connection = desktop->connectSetStyle( + sigc::mem_fun(*this, &TextTool::_styleSet) + ); + this->style_query_connection = desktop->connectQueryStyle( + sigc::mem_fun(*this, &TextTool::_styleQueried) + ); + + _selectionChanged(desktop->getSelection()); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/text/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/text/gradientdrag")) { + this->enableGrDrag(); + } +} + +void TextTool::finish() { + if (this->desktop) { + sp_signal_disconnect_by_data(this->desktop->getCanvas(), this); + } + + this->enableGrDrag(false); + + this->style_set_connection.disconnect(); + this->style_query_connection.disconnect(); + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + sp_text_context_forget_text(SP_TEXT_CONTEXT(this)); + + if (this->imc) { + g_object_unref(G_OBJECT(this->imc)); + this->imc = nullptr; + } + + if (this->timeout) { + g_source_remove(this->timeout); + this->timeout = 0; + } + + if (this->cursor) { + sp_canvas_item_destroy(this->cursor); + this->cursor = nullptr; + } + + if (this->indicator) { + sp_canvas_item_destroy(this->indicator); + this->indicator = nullptr; + } + + if (this->frame) { + sp_canvas_item_destroy(this->frame); + this->frame = nullptr; + } + + for (auto & text_selection_quad : this->text_selection_quads) { + sp_canvas_item_hide(text_selection_quad); + sp_canvas_item_destroy(text_selection_quad); + } + + this->text_selection_quads.clear(); + + ToolBase::finish(); +} + +bool TextTool::item_handler(SPItem* item, GdkEvent* event) { + SPItem *item_ungrouped; + + gint ret = FALSE; + sp_text_context_validate_cursor_iterators(this); + Inkscape::Text::Layout::iterator old_start = this->text_sel_start; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + // this var allow too much lees subbselection queries + // reducing it to cursor iteracion, mouseup and down + // find out clicked item, disregarding groups + item_ungrouped = desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + desktop->getSelection()->set(item_ungrouped); + if (this->text) { + // find out click point in document coordinates + Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // set the cursor closest to that point + if (event->button.state & GDK_SHIFT_MASK) { + this->text_sel_start = old_start; + this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + } else { + this->text_sel_start = this->text_sel_end = sp_te_get_position_by_coords(this->text, p); + } + // update display + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 1; + } + ret = TRUE; + } + } + break; + case GDK_2BUTTON_PRESS: + if (event->button.button == 1 && this->text && this->dragging) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) { + if (!layout->isStartOfWord(this->text_sel_start)) + this->text_sel_start.prevStartOfWord(); + if (!layout->isEndOfWord(this->text_sel_end)) + this->text_sel_end.nextEndOfWord(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 2; + ret = TRUE; + } + } + break; + case GDK_3BUTTON_PRESS: + if (event->button.button == 1 && this->text && this->dragging) { + this->text_sel_start.thisStartOfLine(); + this->text_sel_end.thisEndOfLine(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + this->dragging = 3; + ret = TRUE; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && this->dragging && !this->space_panning) { + this->dragging = 0; + sp_event_context_discard_delayed_snap_event(this); + ret = TRUE; + desktop->emitToolSubselectionChanged((gpointer)this); + } + break; + case GDK_MOTION_NOTIFY: + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::item_handler(item, event); + } + + return ret; +} + +static void sp_text_context_setup_text(TextTool *tc) +{ + ToolBase *ec = SP_EVENT_CONTEXT(tc); + + /* Create <text> */ + Inkscape::XML::Document *xml_doc = ec->desktop->doc()->getReprDoc(); + Inkscape::XML::Node *rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); // we preserve spaces in the text objects we create + + /* Set style */ + sp_desktop_apply_style_tool(ec->desktop, rtext, "/tools/text", true); + + sp_repr_set_svg_double(rtext, "x", tc->pdoc[Geom::X]); + sp_repr_set_svg_double(rtext, "y", tc->pdoc[Geom::Y]); + + /* Create <tspan> */ + Inkscape::XML::Node *rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); // otherwise, why bother creating the tspan? + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + + /* Create TEXT */ + Inkscape::XML::Node *rstring = xml_doc->createTextNode(""); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + SPItem *text_item = SP_ITEM(ec->desktop->currentLayer()->appendChildRepr(rtext)); + /* fixme: Is selection::changed really immediate? */ + /* yes, it's immediate .. why does it matter? */ + ec->desktop->getSelection()->set(text_item); + Inkscape::GC::release(rtext); + text_item->transform = SP_ITEM(ec->desktop->currentLayer())->i2doc_affine().inverse(); + + text_item->updateRepr(); + text_item->doWriteTransform(text_item->transform, nullptr, true); + DocumentUndo::done(ec->desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Create text")); +} + +/** + * Insert the character indicated by tc.uni to replace the current selection, + * and reset tc.uni/tc.unipos to empty string. + * + * \pre tc.uni/tc.unipos non-empty. + */ +static void insert_uni_char(TextTool *const tc) +{ + g_return_if_fail(tc->unipos + && tc->unipos < sizeof(tc->uni) + && tc->uni[tc->unipos] == '\0'); + unsigned int uv; + std::stringstream ss; + ss << std::hex << tc->uni; + ss >> uv; + tc->unipos = 0; + tc->uni[tc->unipos] = '\0'; + + if ( !g_unichar_isprint(static_cast<gunichar>(uv)) + && !(g_unichar_validate(static_cast<gunichar>(uv)) && (g_unichar_type(static_cast<gunichar>(uv)) == G_UNICODE_PRIVATE_USE) ) ) { + // This may be due to bad input, so it goes to statusbar. + tc->desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, + _("Non-printable character")); + } else { + if (!tc->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + gchar u[10]; + guint const len = g_unichar_to_utf8(uv, u); + u[len] = '\0'; + + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, u); + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + DocumentUndo::done(tc->desktop->getDocument(), SP_VERB_DIALOG_TRANSFORM, + _("Insert Unicode character")); + } +} + +static void hex_to_printable_utf8_buf(char const *const ehex, char *utf8) +{ + unsigned int uv; + std::stringstream ss; + ss << std::hex << ehex; + ss >> uv; + if (!g_unichar_isprint((gunichar) uv)) { + uv = 0xfffd; + } + guint const len = g_unichar_to_utf8(uv, utf8); + utf8[len] = '\0'; +} + +static void show_curr_uni_char(TextTool *const tc) +{ + g_return_if_fail(tc->unipos < sizeof(tc->uni) + && tc->uni[tc->unipos] == '\0'); + if (tc->unipos) { + char utf8[10]; + hex_to_printable_utf8_buf(tc->uni, utf8); + + /* Status bar messages are in pango markup, so we need xml escaping. */ + if (utf8[1] == '\0') { + switch(utf8[0]) { + case '<': strcpy(utf8, "<"); break; + case '>': strcpy(utf8, ">"); break; + case '&': strcpy(utf8, "&"); break; + default: break; + } + } + tc->defaultMessageContext()->setF(Inkscape::NORMAL_MESSAGE, + _("Unicode (<b>Enter</b> to finish): %s: %s"), tc->uni, utf8); + } else { + tc->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): ")); + } +} + +bool TextTool::root_handler(GdkEvent* event) { + sp_canvas_item_hide(this->indicator); + + sp_text_context_validate_cursor_iterators(this); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + + if (Inkscape::have_viable_layer(desktop, desktop->getMessageStack()) == false) { + return TRUE; + } + + // save drag origin + this->xp = (gint) event->button.x; + this->yp = (gint) event->button.y; + this->within_tolerance = true; + + Geom::Point const button_pt(event->button.x, event->button.y); + Geom::Point button_dt(desktop->w2d(button_pt)); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(button_dt, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + this->p0 = button_dt; + Inkscape::Rubberband::get(desktop)->start(desktop, this->p0); + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_PRESS_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, + nullptr, event->button.time); + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + this->creating = true; + + /* Processed */ + return TRUE; + } + break; + case GDK_MOTION_NOTIFY: { + if (this->creating && (event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + if ( this->within_tolerance + && ( abs( (gint) event->motion.x - this->xp ) < this->tolerance ) + && ( abs( (gint) event->motion.y - this->yp ) < this->tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to draw, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + this->within_tolerance = false; + + Geom::Point const motion_pt(event->motion.x, event->motion.y); + Geom::Point p = desktop->w2d(motion_pt); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(p, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + Inkscape::Rubberband::get(desktop)->move(p); + gobble_motion_events(GDK_BUTTON1_MASK); + + // status text + Inkscape::Util::Quantity x_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::X]), "px"); + Inkscape::Util::Quantity y_q = Inkscape::Util::Quantity(fabs((p - this->p0)[Geom::Y]), "px"); + Glib::ustring xs = x_q.string(desktop->namedview->display_units); + Glib::ustring ys = y_q.string(desktop->namedview->display_units); + this->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("<b>Flowed text frame</b>: %s × %s"), xs.c_str(), ys.c_str()); + } else if (!this->sp_event_context_knot_mouseover()) { + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + m.preSnap(Inkscape::SnapCandidatePoint(motion_dt, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + } + if ((event->motion.state & GDK_BUTTON1_MASK) && this->dragging && !this->space_panning) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (!layout) + break; + // find out click point in document coordinates + Geom::Point p = desktop->w2d(Geom::Point(event->button.x, event->button.y)); + // set the cursor closest to that point + Inkscape::Text::Layout::iterator new_end = sp_te_get_position_by_coords(this->text, p); + if (this->dragging == 2) { + // double-click dragging: go by word + if (new_end < this->text_sel_start) { + if (!layout->isStartOfWord(new_end)) + new_end.prevStartOfWord(); + } else if (!layout->isEndOfWord(new_end)) + new_end.nextEndOfWord(); + } else if (this->dragging == 3) { + // triple-click dragging: go by line + if (new_end < this->text_sel_start) + new_end.thisStartOfLine(); + else + new_end.thisEndOfLine(); + } + // update display + if (this->text_sel_end != new_end) { + this->text_sel_end = new_end; + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + } + gobble_motion_events(GDK_BUTTON1_MASK); + break; + } + // find out item under mouse, disregarding groups + SPItem *item_ungrouped = + desktop->getItemAtPoint(Geom::Point(event->button.x, event->button.y), TRUE, nullptr); + if (SP_IS_TEXT(item_ungrouped) || SP_IS_FLOWTEXT(item_ungrouped)) { + Inkscape::Text::Layout const *layout = te_get_layout(item_ungrouped); + if (layout->inputTruncated()) { + SP_CTRLRECT(this->indicator)->setColor(0xff0000ff, false, 0); + } else { + SP_CTRLRECT(this->indicator)->setColor(0x0000ff7f, false, 0); + } + Geom::OptRect ibbox = item_ungrouped->desktopVisualBounds(); + if (ibbox) { + SP_CTRLRECT(this->indicator)->setRectangle(*ibbox); + } + sp_canvas_item_show(this->indicator); + + this->cursor_shape = cursor_text_insert_xpm; + this->sp_event_context_update_cursor(); + sp_text_context_update_text_selection(this); + if (SP_IS_TEXT(item_ungrouped)) { + desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, + _("<b>Click</b> to edit the text, <b>drag</b> to select part of the text.")); + } else { + desktop->event_context->defaultMessageContext()->set( + Inkscape::NORMAL_MESSAGE, + _("<b>Click</b> to edit the flowed text, <b>drag</b> to select part of the text.")); + } + this->over_text = true; + } else { + this->over_text = false; + // update cursor and statusbar: we are not over a text object now + this->cursor_shape = cursor_text_xpm; + this->sp_event_context_update_cursor(); + desktop->event_context->defaultMessageContext()->clear(); + } + } break; + + case GDK_BUTTON_RELEASE: + if (event->button.button == 1 && !this->space_panning) { + sp_event_context_discard_delayed_snap_event(this); + + Geom::Point p1 = desktop->w2d(Geom::Point(event->button.x, event->button.y)); + + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + m.freeSnapReturnByRef(p1, Inkscape::SNAPSOURCE_NODE_HANDLE); + m.unSetup(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + Inkscape::Rubberband::get(desktop)->stop(); + + if (this->creating && this->within_tolerance) { + /* Button 1, set X & Y & new item */ + desktop->getSelection()->clear(); + this->pdoc = desktop->dt2doc(p1); + this->show = TRUE; + this->phase = true; + this->nascent_object = true; // new object was just created + + /* Cursor */ + sp_canvas_item_show(this->cursor); + // Cursor height is defined by the new text object's font size; it needs to be set + // artificially here, for the text object does not exist yet: + double cursor_height = sp_desktop_get_font_size_tool(desktop); + auto const y_dir = desktop->yaxisdir(); + this->cursor->setCoords(p1, p1 - Geom::Point(0, y_dir * cursor_height)); + if (this->imc) { + GdkRectangle im_cursor; + Geom::Point const top_left = SP_EVENT_CONTEXT(this)->desktop->get_display_area().corner(3); + Geom::Point const cursor_size(0, cursor_height); + Geom::Point const im_position = SP_EVENT_CONTEXT(this)->desktop->d2w(p1 + cursor_size - top_left); + im_cursor.x = (int) floor(im_position[Geom::X]); + im_cursor.y = (int) floor(im_position[Geom::Y]); + im_cursor.width = 0; + im_cursor.height = (int) -floor(SP_EVENT_CONTEXT(this)->desktop->d2w(cursor_size)[Geom::Y]); + gtk_im_context_set_cursor_location(this->imc, &im_cursor); + } + this->message_context->set(Inkscape::NORMAL_MESSAGE, _("Type text; <b>Enter</b> to start new line.")); // FIXME:: this is a copy of a string from _update_cursor below, do not desync + + this->within_tolerance = false; + } else if (this->creating) { + double cursor_height = sp_desktop_get_font_size_tool(desktop); + if (fabs(p1[Geom::Y] - this->p0[Geom::Y]) > cursor_height) { + // otherwise even one line won't fit; most probably a slip of hand (even if bigger than tolerance) + + if (prefs->getBool("/tools/text/use_svg2", true)) { + // SVG 2 text + + SPItem *text = create_text_with_rectangle (desktop, this->p0, p1); + + desktop->getSelection()->set(text); + SPCSSAttr *css = sp_repr_css_attr(text->getRepr(), "style" ); + sp_repr_css_attr_unref(css); + + } else { + // SVG 1.2 text + + SPItem *ft = create_flowtext_with_internal_frame (desktop, this->p0, p1); + + /* Set style */ + sp_desktop_apply_style_tool(desktop, ft->getRepr(), "/tools/text", true); + SPCSSAttr *css = sp_repr_css_attr(ft->getRepr(), "style" ); + Geom::Affine const local(ft->i2doc_affine()); + double const ex(local.descrim()); + if ( (ex != 0.0) && (ex != 1.0) ) { + sp_css_attr_scale(css, 1/ex); + } + ft->setCSS(css,"style"); + sp_repr_css_attr_unref(css); + ft->updateRepr(); + + desktop->getSelection()->set(ft); + } + + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Flowed text is created.")); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Create flowed text")); + + } else { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("The frame is <b>too small</b> for the current font size. Flowed text not created.")); + } + } + this->creating = false; + desktop->emitToolSubselectionChanged((gpointer)this); + return TRUE; + } + break; + case GDK_KEY_PRESS: { + guint const group0_keyval = get_latin_keyval(&event->key); + + if (group0_keyval == GDK_KEY_KP_Add || + group0_keyval == GDK_KEY_KP_Subtract) { + if (!(event->key.state & GDK_MOD2_MASK)) // mod2 is NumLock; if on, type +/- keys + break; // otherwise pass on keypad +/- so they can zoom + } + + if ((this->text) || (this->nascent_object)) { + // there is an active text object in this context, or a new object was just created + + if (this->unimode || !this->imc + || (MOD__CTRL(event) && MOD__SHIFT(event)) // input methods tend to steal this for unimode, + // but we have our own so make sure they don't swallow it + || !gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { + //IM did not consume the key, or we're in unimode + + if (!MOD__CTRL_ONLY(event) && this->unimode) { + /* TODO: ISO 14755 (section 3 Definitions) says that we should also + accept the first 6 characters of alphabets other than the latin + alphabet "if the Latin alphabet is not used". The below is also + reasonable (viz. hope that the user's keyboard includes latin + characters and force latin interpretation -- just as we do for our + keyboard shortcuts), but differs from the ISO 14755 + recommendation. */ + switch (group0_keyval) { + case GDK_KEY_space: + case GDK_KEY_KP_Space: { + if (this->unipos) { + insert_uni_char(this); + } + /* Stay in unimode. */ + show_curr_uni_char(this); + return TRUE; + } + + case GDK_KEY_BackSpace: { + g_return_val_if_fail(this->unipos < sizeof(this->uni), TRUE); + if (this->unipos) { + this->uni[--this->unipos] = '\0'; + } + show_curr_uni_char(this); + return TRUE; + } + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: { + if (this->unipos) { + insert_uni_char(this); + } + /* Exit unimode. */ + this->unimode = false; + this->defaultMessageContext()->clear(); + return TRUE; + } + + case GDK_KEY_Escape: { + // Cancel unimode. + this->unimode = false; + gtk_im_context_reset(this->imc); + this->defaultMessageContext()->clear(); + return TRUE; + } + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + break; + + default: { + if (g_ascii_isxdigit(group0_keyval)) { + g_return_val_if_fail(this->unipos < sizeof(this->uni) - 1, TRUE); + this->uni[this->unipos++] = group0_keyval; + this->uni[this->unipos] = '\0'; + if (this->unipos == 8) { + /* This behaviour is partly to allow us to continue to + use a fixed-length buffer for tc->uni. Reason for + choosing the number 8 is that it's the length of + ``canonical form'' mentioned in the ISO 14755 spec. + An advantage over choosing 6 is that it allows using + backspace for typos & misremembering when entering a + 6-digit number. */ + insert_uni_char(this); + } + show_curr_uni_char(this); + return TRUE; + } else { + /* The intent is to ignore but consume characters that could be + typos for hex digits. Gtk seems to ignore & consume all + non-hex-digits, and we do similar here. Though note that some + shortcuts (like keypad +/- for zoom) get processed before + reaching this code. */ + return TRUE; + } + } + } + } + + Inkscape::Text::Layout::iterator old_start = this->text_sel_start; + Inkscape::Text::Layout::iterator old_end = this->text_sel_end; + bool cursor_moved = false; + int screenlines = 1; + if (this->text) { + double spacing = sp_te_get_average_linespacing(this->text); + Geom::Rect const d = desktop->get_display_area(); + screenlines = (int) floor(fabs(d.min()[Geom::Y] - d.max()[Geom::Y])/spacing) - 1; + if (screenlines <= 0) + screenlines = 1; + } + + /* Neither unimode nor IM consumed key; process text tool shortcuts */ + switch (group0_keyval) { + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-text"); + return TRUE; + } + break; + case GDK_KEY_space: + if (MOD__CTRL_ONLY(event)) { + /* No-break space */ + if (!this->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(this); + this->nascent_object = false; // we don't need it anymore, having created a real <text> + } + this->text_sel_start = this->text_sel_end = sp_te_replace(this->text, this->text_sel_start, this->text_sel_end, "\302\240"); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("No-break space")); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Insert no-break space")); + return TRUE; + } + break; + case GDK_KEY_U: + case GDK_KEY_u: + if (MOD__CTRL_ONLY(event) || (MOD__CTRL(event) && MOD__SHIFT(event))) { + if (this->unimode) { + this->unimode = false; + this->defaultMessageContext()->clear(); + } else { + this->unimode = true; + this->unipos = 0; + this->defaultMessageContext()->set(Inkscape::NORMAL_MESSAGE, _("Unicode (<b>Enter</b> to finish): ")); + } + if (this->imc) { + gtk_im_context_reset(this->imc); + } + return TRUE; + } + break; + case GDK_KEY_B: + case GDK_KEY_b: + if (MOD__CTRL_ONLY(event) && this->text) { + SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end)); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (style->font_weight.computed == SP_CSS_FONT_WEIGHT_NORMAL + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_100 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_200 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_300 + || style->font_weight.computed == SP_CSS_FONT_WEIGHT_400) + sp_repr_css_set_property(css, "font-weight", "bold"); + else + sp_repr_css_set_property(css, "font-weight", "normal"); + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Make bold")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + break; + case GDK_KEY_I: + case GDK_KEY_i: + if (MOD__CTRL_ONLY(event) && this->text) { + SPStyle const *style = sp_te_style_at_position(this->text, std::min(this->text_sel_start, this->text_sel_end)); + SPCSSAttr *css = sp_repr_css_attr_new(); + if (style->font_style.computed != SP_CSS_FONT_STYLE_NORMAL) + sp_repr_css_set_property(css, "font-style", "normal"); + else + sp_repr_css_set_property(css, "font-style", "italic"); + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Make italic")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + break; + + case GDK_KEY_A: + case GDK_KEY_a: + if (MOD__CTRL_ONLY(event) && this->text) { + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) { + this->text_sel_start = layout->begin(); + this->text_sel_end = layout->end(); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + if (!this->text) { // printable key; create text if none (i.e. if nascent_object) + sp_text_context_setup_text(this); + this->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + SPText* text_element = dynamic_cast<SPText*>(text); + if (text_element && (text_element->has_shape_inside() || text_element->has_inline_size())) { + // Handle new line like any other character. + this->text_sel_start = this->text_sel_end = sp_te_insert(this->text, this->text_sel_start, "\n"); + } else { + // Replace new line by either <tspan sodipodi:role="line" or <flowPara>. + iterator_pair enter_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, enter_pair); + (void)success; // TODO cleanup + this->text_sel_start = this->text_sel_end = enter_pair.first; + this->text_sel_start = this->text_sel_end = sp_te_insert_line(this->text, this->text_sel_start); + } + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("New line")); + return TRUE; + } + case GDK_KEY_BackSpace: + if (this->text) { // if nascent_object, do nothing, but return TRUE; same for all other delete and move keys + + bool noSelection = false; + + if (MOD__CTRL(event)) { + this->text_sel_start = this->text_sel_end; + } + + if (this->text_sel_start == this->text_sel_end) { + if (MOD__CTRL(event)) { + this->text_sel_start.prevStartOfWord(); + } else { + this->text_sel_start.prevCursorPosition(); + } + noSelection = true; + } + + iterator_pair bspace_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, bspace_pair); + + if (noSelection) { + if (success) { + this->text_sel_start = this->text_sel_end = bspace_pair.first; + } else { // nothing deleted + this->text_sel_start = this->text_sel_end = bspace_pair.second; + } + } else { + if (success) { + this->text_sel_start = this->text_sel_end = bspace_pair.first; + } else { // nothing deleted + this->text_sel_start = bspace_pair.first; + this->text_sel_end = bspace_pair.second; + } + } + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Backspace")); + } + return TRUE; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + if (this->text) { + bool noSelection = false; + + if (MOD__CTRL(event)) { + this->text_sel_start = this->text_sel_end; + } + + if (this->text_sel_start == this->text_sel_end) { + if (MOD__CTRL(event)) { + this->text_sel_end.nextEndOfWord(); + } else { + this->text_sel_end.nextCursorPosition(); + } + noSelection = true; + } + + iterator_pair del_pair; + bool success = sp_te_delete(this->text, this->text_sel_start, this->text_sel_end, del_pair); + + if (noSelection) { + this->text_sel_start = this->text_sel_end = del_pair.first; + } else { + if (success) { + this->text_sel_start = this->text_sel_end = del_pair.first; + } else { // nothing deleted + this->text_sel_start = del_pair.first; + this->text_sel_end = del_pair.second; + } + } + + + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::done(desktop->getDocument(), SP_VERB_CONTEXT_TEXT, _("Delete")); + } + return TRUE; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*-10, 0)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*-1, 0)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(desktop->getDocument(), "kern:left", SP_VERB_CONTEXT_TEXT, _("Kern to the left")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorLeftWithControl(); + else + this->text_sel_end.cursorLeft(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*10, 0)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(mul*1, 0)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(desktop->getDocument(), "kern:right", SP_VERB_CONTEXT_TEXT, _("Kern to the right")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorRightWithControl(); + else + this->text_sel_end.cursorRight(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*-10)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*-1)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(desktop->getDocument(), "kern:up", SP_VERB_CONTEXT_TEXT, _("Kern up")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorUpWithControl(); + else + this->text_sel_end.cursorUp(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (this->text) { + if (MOD__ALT(event)) { + gint mul = 1 + gobble_key_events( + get_latin_keyval(&event->key), 0); // with any mask + if (MOD__SHIFT(event)) + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*10)); + else + sp_te_adjust_kerning_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, Geom::Point(0, mul*1)); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + DocumentUndo::maybeDone(desktop->getDocument(), "kern:down", SP_VERB_CONTEXT_TEXT, _("Kern down")); + } else { + if (MOD__CTRL(event)) + this->text_sel_end.cursorDownWithControl(); + else + this->text_sel_end.cursorDown(); + cursor_moved = true; + break; + } + } + return TRUE; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + if (this->text) { + if (MOD__CTRL(event)) + this->text_sel_end.thisStartOfShape(); + else + this->text_sel_end.thisStartOfLine(); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_End: + case GDK_KEY_KP_End: + if (this->text) { + if (MOD__CTRL(event)) + this->text_sel_end.nextStartOfShape(); + else + this->text_sel_end.thisEndOfLine(); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + if (this->text) { + this->text_sel_end.cursorDown(screenlines); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + if (this->text) { + this->text_sel_end.cursorUp(screenlines); + cursor_moved = true; + break; + } + return TRUE; + case GDK_KEY_Escape: + if (this->creating) { + this->creating = false; + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + Inkscape::Rubberband::get(desktop)->stop(); + } else { + desktop->getSelection()->clear(); + } + this->nascent_object = FALSE; + return TRUE; + case GDK_KEY_bracketleft: + if (this->text) { + if (MOD__ALT(event) || MOD__CTRL(event)) { + if (MOD__ALT(event)) { + if (MOD__SHIFT(event)) { + // FIXME: alt+shift+[] does not work, don't know why + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10); + } else { + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1); + } + } else { + sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, desktop, -90); + } + DocumentUndo::maybeDone(desktop->getDocument(), "textrot:ccw", SP_VERB_CONTEXT_TEXT, _("Rotate counterclockwise")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_bracketright: + if (this->text) { + if (MOD__ALT(event) || MOD__CTRL(event)) { + if (MOD__ALT(event)) { + if (MOD__SHIFT(event)) { + // FIXME: alt+shift+[] does not work, don't know why + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10); + } else { + sp_te_adjust_rotation_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1); + } + } else { + sp_te_adjust_rotation(this->text, this->text_sel_start, this->text_sel_end, desktop, 90); + } + DocumentUndo::maybeDone(desktop->getDocument(), "textrot:cw", SP_VERB_CONTEXT_TEXT, _("Rotate clockwise")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_less: + case GDK_KEY_comma: + if (this->text) { + if (MOD__ALT(event)) { + if (MOD__CTRL(event)) { + if (MOD__SHIFT(event)) + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10); + else + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1); + DocumentUndo::maybeDone(desktop->getDocument(), "linespacing:dec", SP_VERB_CONTEXT_TEXT, _("Contract line spacing")); + } else { + if (MOD__SHIFT(event)) + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -10); + else + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, -1); + DocumentUndo::maybeDone(desktop->getDocument(), "letterspacing:dec", SP_VERB_CONTEXT_TEXT, _("Contract letter spacing")); + } + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + case GDK_KEY_greater: + case GDK_KEY_period: + if (this->text) { + if (MOD__ALT(event)) { + if (MOD__CTRL(event)) { + if (MOD__SHIFT(event)) + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10); + else + sp_te_adjust_linespacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1); + DocumentUndo::maybeDone(desktop->getDocument(), "linespacing:inc", SP_VERB_CONTEXT_TEXT, _("Expand line spacing")); + } else { + if (MOD__SHIFT(event)) + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 10); + else + sp_te_adjust_tspan_letterspacing_screen(this->text, this->text_sel_start, this->text_sel_end, desktop, 1); + DocumentUndo::maybeDone(desktop->getDocument(), "letterspacing:inc", SP_VERB_CONTEXT_TEXT, _("Expand letter spacing"));\ + } + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return TRUE; + } + } + break; + default: + break; + } + + if (cursor_moved) { + if (!MOD__SHIFT(event)) + this->text_sel_start = this->text_sel_end; + if (old_start != this->text_sel_start || old_end != this->text_sel_end) { + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + } + return TRUE; + } + + } else return TRUE; // return the "I took care of it" value if it was consumed by the IM + } else { // do nothing if there's no object to type in - the key will be sent to parent context, + // except up/down that are swallowed to prevent the zoom field from activation + if ((group0_keyval == GDK_KEY_Up || + group0_keyval == GDK_KEY_Down || + group0_keyval == GDK_KEY_KP_Up || + group0_keyval == GDK_KEY_KP_Down ) + && !MOD__CTRL_ONLY(event)) { + return TRUE; + } else if (group0_keyval == GDK_KEY_Escape) { // cancel rubberband + if (this->creating) { + this->creating = false; + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + Inkscape::Rubberband::get(desktop)->stop(); + } + } else if ((group0_keyval == GDK_KEY_x || group0_keyval == GDK_KEY_X) && MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("altx-text"); + return TRUE; + } + } + break; + } + + case GDK_KEY_RELEASE: + if (!this->unimode && this->imc && gtk_im_context_filter_keypress(this->imc, (GdkEventKey*) event)) { + return TRUE; + } + break; + default: + break; + } + + // if nobody consumed it so far +// if ((SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler) { // and there's a handler in parent context, +// return (SP_EVENT_CONTEXT_CLASS(sp_text_context_parent_class))->root_handler(event_context, event); // send event to parent +// } else { +// return FALSE; // return "I did nothing" value so that global shortcuts can be activated +// } + return ToolBase::root_handler(event); + +} + +/** + Attempts to paste system clipboard into the currently edited text, returns true on success + */ +bool sp_text_paste_inline(ToolBase *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return false; + TextTool *tc = SP_TEXT_CONTEXT(ec); + + if ((tc->text) || (tc->nascent_object)) { + // there is an active text object in this context, or a new object was just created + + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const clip_text = refClipboard->wait_for_text(); + + if (!clip_text.empty()) { + + bool is_svg2 = false; + SPText *textitem = dynamic_cast<SPText *>(tc->text); + if (textitem) { + is_svg2 = textitem->has_shape_inside() /*|| textitem->has_inline_size()*/; // Do now since hiding messes this up. + textitem->hide_shape_inside(); + } + + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(tc->text); + if (flowtext) { + flowtext->fix_overflow_flowregion(false); + } + + // Fix for 244940 + // The XML standard defines the following as valid characters + // (Extensible Markup Language (XML) 1.0 (Fourth Edition) paragraph 2.2) + // char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + // Since what comes in off the paste buffer will go right into XML, clean + // the text here. + Glib::ustring text(clip_text); + Glib::ustring::iterator itr = text.begin(); + gunichar paste_string_uchar; + + while(itr != text.end()) + { + paste_string_uchar = *itr; + + // Make sure we don't have a control character. We should really check + // for the whole range above... Add the rest of the invalid cases from + // above if we find additional issues + if(paste_string_uchar >= 0x00000020 || + paste_string_uchar == 0x00000009 || + paste_string_uchar == 0x0000000A || + paste_string_uchar == 0x0000000D) { + ++itr; + } else { + itr = text.erase(itr); + } + } + + if (!tc->text) { // create text if none (i.e. if nascent_object) + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + // using indices is slow in ustrings. Whatever. + Glib::ustring::size_type begin = 0; + for ( ; ; ) { + Glib::ustring::size_type end = text.find('\n', begin); + + if (end == Glib::ustring::npos || is_svg2) { + // Paste everything + if (begin != text.length()) + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin).c_str()); + break; + } + + // Paste up to new line, add line, repeat. + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, text.substr(begin, end - begin).c_str()); + tc->text_sel_start = tc->text_sel_end = sp_te_insert_line(tc->text, tc->text_sel_start); + begin = end + 1; + } + if (textitem) { + textitem->show_shape_inside(); + } + if (flowtext) { + flowtext->fix_overflow_flowregion(true); + } + DocumentUndo::done(ec->desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Paste text")); + + return true; + } + + } // FIXME: else create and select a new object under cursor! + + return false; +} + +/** + Gets the raw characters that comprise the currently selected text, converting line + breaks into lf characters. +*/ +Glib::ustring sp_text_get_selected_text(ToolBase const *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return ""; + TextTool const *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return ""; + + return sp_te_get_string_multiline(tc->text, tc->text_sel_start, tc->text_sel_end); +} + +SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return nullptr; + TextTool const *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return nullptr; + + SPObject const *obj = sp_te_object_at_position(tc->text, tc->text_sel_end); + + if (obj) { + return take_style_from_item(const_cast<SPObject*>(obj)); + } + + return nullptr; +} +// this two functions are commented because are used on clipboard +// and because slow the text pastinbg and usage a lot +// and couldent get it working properly we miss font size font style or never work +// and user usualy want paste as plain text and get the position context +// style. Anyway I retain for further usage. + +/* static bool css_attrs_are_equal(SPCSSAttr const *first, SPCSSAttr const *second) +{ + Inkscape::Util::List<Inkscape::XML::AttributeRecord const> attrs = first->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = second->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + attrs = second->attributeList(); + for ( ; attrs ; attrs++) { + gchar const *other_attr = first->attribute(g_quark_to_string(attrs->key)); + if (other_attr == nullptr || strcmp(attrs->value, other_attr)) + return false; + } + return true; +} + +std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned> +*positions) +{ + std::vector<SPCSSAttr*> vec; + SPCSSAttr *css, *css_new; + TextTool *tc = SP_TEXT_CONTEXT(ec); + Inkscape::Text::Layout::iterator i = std::min(tc->text_sel_start, tc->text_sel_end); + SPObject const *obj = sp_te_object_at_position(tc->text, i); + if (obj) { + css = take_style_from_item(const_cast<SPObject*>(obj)); + } + vec.push_back(css); + positions->push_back(0); + i.nextCharacter(); + *k = 1; + *b = 1; + while (i != std::max(tc->text_sel_start, tc->text_sel_end)) + { + obj = sp_te_object_at_position(tc->text, i); + if (obj) { + css_new = take_style_from_item(const_cast<SPObject*>(obj)); + } + if(!css_attrs_are_equal(css, css_new)) + { + vec.push_back(css_new); + css = sp_repr_css_attr_new(); + sp_repr_css_merge(css, css_new); + positions->push_back(*k); + (*b)++; + } + i.nextCharacter(); + (*k)++; + } + positions->push_back(*k); + return vec; +} + */ + +/** + Deletes the currently selected characters. Returns false if there is no + text selection currently. +*/ +bool sp_text_delete_selection(ToolBase *ec) +{ + if (!SP_IS_TEXT_CONTEXT(ec)) + return false; + TextTool *tc = SP_TEXT_CONTEXT(ec); + if (tc->text == nullptr) + return false; + + if (tc->text_sel_start == tc->text_sel_end) + return false; + + iterator_pair pair; + bool success = sp_te_delete(tc->text, tc->text_sel_start, tc->text_sel_end, pair); + + + if (success) { + tc->text_sel_start = tc->text_sel_end = pair.first; + } else { // nothing deleted + tc->text_sel_start = pair.first; + tc->text_sel_end = pair.second; + } + + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + + return true; +} + +/** + * \param selection Should not be NULL. + */ +void TextTool::_selectionChanged(Inkscape::Selection *selection) +{ + g_assert(selection != nullptr); + + ToolBase *ec = SP_EVENT_CONTEXT(this); + + ec->shape_editor->unset_item(); + SPItem *item = selection->singleItem(); + if (item && ( + (SP_IS_FLOWTEXT(item) && SP_FLOWTEXT(item)->has_internal_frame()) || + (SP_IS_TEXT(item) && + !(SP_TEXT(item)->has_shape_inside() && !SP_TEXT(item)->get_first_rectangle())) + )) { + ec->shape_editor->set_item(item); + } + + if (this->text && (item != this->text)) { + sp_text_context_forget_text(this); + } + this->text = nullptr; + + if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + this->text = item; + Inkscape::Text::Layout const *layout = te_get_layout(this->text); + if (layout) + this->text_sel_start = this->text_sel_end = layout->end(); + } else { + this->text = nullptr; + } + + // we update cursor without scrolling, because this position may not be final; + // item_handler moves cusros to the point of click immediately + sp_text_context_update_cursor(this, false); + sp_text_context_update_text_selection(this); +} + +void TextTool::_selectionModified(Inkscape::Selection */*selection*/, guint /*flags*/) +{ + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); +} + +bool TextTool::_styleSet(SPCSSAttr const *css) +{ + if (this->text == nullptr) + return false; + if (this->text_sel_start == this->text_sel_end) + return false; // will get picked up by the parent and applied to the whole text object + + sp_te_apply_style(this->text, this->text_sel_start, this->text_sel_end, css); + + // This is a bandaid fix... whenever a style is changed it might cause the text layout to + // change which requires rewriting the 'x' and 'y' attributes of the tpsans for Inkscape + // multi-line text (with sodipodi:role="line"). We need to rewrite the repr after this is + // done. rebuldLayout() will be called a second time unnecessarily. + SPText* sptext = dynamic_cast<SPText*>(text); + if (sptext) { + sptext->rebuildLayout(); + sptext->updateRepr(); + } + + DocumentUndo::done(this->desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Set text style")); + sp_text_context_update_cursor(this); + sp_text_context_update_text_selection(this); + return true; +} + +int TextTool::_styleQueried(SPStyle *style, int property) +{ + if (this->text == nullptr) { + return QUERY_STYLE_NOTHING; + } + const Inkscape::Text::Layout *layout = te_get_layout(this->text); + if (layout == nullptr) { + return QUERY_STYLE_NOTHING; + } + sp_text_context_validate_cursor_iterators(this); + + std::vector<SPItem*> styles_list; + + Inkscape::Text::Layout::iterator begin_it, end_it; + if (this->text_sel_start < this->text_sel_end) { + begin_it = this->text_sel_start; + end_it = this->text_sel_end; + } else { + begin_it = this->text_sel_end; + end_it = this->text_sel_start; + } + if (begin_it == end_it) { + if (!begin_it.prevCharacter()) { + end_it.nextCharacter(); + } + } + for (Inkscape::Text::Layout::iterator it = begin_it ; it < end_it ; it.nextStartOfSpan()) { + SPObject *pos_obj = nullptr; + layout->getSourceOfCharacter(it, &pos_obj); + if (!pos_obj) { + continue; + } + if (! pos_obj->parent) // the string is not in the document anymore (deleted) + return 0; + + if ( SP_IS_STRING(pos_obj) ) { + pos_obj = pos_obj->parent; // SPStrings don't have style + } + styles_list.insert(styles_list.begin(),(SPItem*)pos_obj); + } + + int result = sp_desktop_query_style_from_list (styles_list, style, property); + + return result; +} + +static void sp_text_context_validate_cursor_iterators(TextTool *tc) +{ + if (tc->text == nullptr) + return; + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + if (layout) { // undo can change the text length without us knowing it + layout->validateIterator(&tc->text_sel_start); + layout->validateIterator(&tc->text_sel_end); + } +} + +static void sp_text_context_update_cursor(TextTool *tc, bool scroll_to_see) +{ + // due to interruptible display, tc may already be destroyed during a display update before + // the cursor update (can't do both atomically, alas) + if (!tc->desktop) return; + + if (tc->text) { + Geom::Point p0, p1; + sp_te_get_cursor_coords(tc->text, tc->text_sel_end, p0, p1); + Geom::Point const d0 = p0 * tc->text->i2dt_affine(); + Geom::Point const d1 = p1 * tc->text->i2dt_affine(); + + // scroll to show cursor + if (scroll_to_see) { + + // We don't want to scroll outside the text box area (i.e. when there is hidden text) + // or we could end up in Timbuktu. + bool scroll = true; + if (SP_IS_TEXT(tc->text)) { + Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame(); + if (opt_frame && (!opt_frame->contains(p0))) { + scroll = false; + } + } + + if (scroll) { + Geom::Point const center = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().midpoint(); + if (Geom::L2(d0 - center) > Geom::L2(d1 - center)) + // unlike mouse moves, here we must scroll all the way at first shot, so we override the autoscrollspeed + SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d0, 1.0); + else + SP_EVENT_CONTEXT(tc)->desktop->scroll_to_point(d1, 1.0); + } + } + + sp_canvas_item_show(tc->cursor); + tc->cursor->setCoords(d0, d1); + + /* fixme: ... need another transformation to get canvas widget coordinate space? */ + if (tc->imc) { + GdkRectangle im_cursor = { 0, 0, 1, 1 }; + Geom::Point const top_left = SP_EVENT_CONTEXT(tc)->desktop->get_display_area().corner(3); + Geom::Point const im_d0 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d0 - top_left); + Geom::Point const im_d1 = SP_EVENT_CONTEXT(tc)->desktop->d2w(d1 - top_left); + im_cursor.x = (int) floor(im_d0[Geom::X]); + im_cursor.y = (int) floor(im_d1[Geom::Y]); + im_cursor.width = (int) floor(im_d1[Geom::X]) - im_cursor.x; + im_cursor.height = (int) floor(im_d0[Geom::Y]) - im_cursor.y; + gtk_im_context_set_cursor_location(tc->imc, &im_cursor); + } + + tc->show = TRUE; + tc->phase = true; + + Inkscape::Text::Layout const *layout = te_get_layout(tc->text); + int const nChars = layout->iteratorToCharIndex(layout->end()); + char const *trunc = ""; + bool truncated = false; + if (layout->inputTruncated()) { + truncated = true; + trunc = _(" [truncated]"); + } + + if (truncated) { + SP_CTRLRECT(tc->frame)->setColor(0xff0000ff, false, 0); + } else { + SP_CTRLRECT(tc->frame)->setColor(0x0000ff7f, false, 0); + } + + // Frame around text + if (SP_IS_FLOWTEXT(tc->text)) { + SPItem *frame = SP_FLOWTEXT(tc->text)->get_frame (nullptr); // first frame only + if (frame) { + sp_canvas_item_show(tc->frame); + Geom::OptRect frame_bbox = frame->desktopVisualBounds(); + if (frame_bbox) { + SP_CTRLRECT(tc->frame)->setRectangle(*frame_bbox); + sp_canvas_item_show(tc->frame); + } + } + + SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_MESSAGE, ngettext("Type or edit flowed text (%d character%s); <b>Enter</b> to start new paragraph.", "Type or edit flowed text (%d characters%s); <b>Enter</b> to start new paragraph.", nChars), nChars, trunc); + + } else if (SP_IS_TEXT(tc->text)) { + + Geom::OptRect opt_frame = SP_TEXT(tc->text)->get_frame(); + + if (opt_frame) { + // User units to screen pixels + Geom::Rect frame = *opt_frame; + frame *= tc->text->i2dt_affine(); + + SP_CTRLRECT(tc->frame)->setRectangle(frame); + sp_canvas_item_show(tc->frame); + } else { + sp_canvas_item_hide(tc->frame); + } + + } else { + + SP_EVENT_CONTEXT(tc)->message_context->setF(Inkscape::NORMAL_MESSAGE, ngettext("Type or edit text (%d character%s); <b>Enter</b> to start new line.", "Type or edit text (%d characters%s); <b>Enter</b> to start new line.", nChars), nChars, trunc); + } + + } else { + sp_canvas_item_hide(tc->cursor); + sp_canvas_item_hide(tc->frame); + tc->show = FALSE; + if (!tc->nascent_object) { + SP_EVENT_CONTEXT(tc)->message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Click</b> to select or create text, <b>drag</b> to create flowed text; then type.")); // FIXME: this is a copy of string from tools-switch, do not desync + } + } + + SP_EVENT_CONTEXT(tc)->desktop->emitToolSubselectionChanged((gpointer)tc); +} + +static void sp_text_context_update_text_selection(TextTool *tc) +{ + // due to interruptible display, tc may already be destroyed during a display update before + // the selection update (can't do both atomically, alas) + if (!tc->desktop) return; + + for (auto & text_selection_quad : tc->text_selection_quads) { + sp_canvas_item_hide(text_selection_quad); + sp_canvas_item_destroy(text_selection_quad); + } + tc->text_selection_quads.clear(); + + std::vector<Geom::Point> quads; + if (tc->text != nullptr) + quads = sp_te_create_selection_quads(tc->text, tc->text_sel_start, tc->text_sel_end, (tc->text)->i2dt_affine()); + for (unsigned i = 0 ; i < quads.size() ; i += 4) { + SPCanvasItem *quad_canvasitem; + quad_canvasitem = sp_canvas_item_new(tc->desktop->getControls(), SP_TYPE_CTRLQUADR, nullptr); + // FIXME: make the color settable in prefs + // for now, use semitrasparent blue, as cairo cannot do inversion :( + sp_ctrlquadr_set_rgba32(SP_CTRLQUADR(quad_canvasitem), 0x00777777); + sp_ctrlquadr_set_coords(SP_CTRLQUADR(quad_canvasitem), quads[i], quads[i+1], quads[i+2], quads[i+3]); + sp_canvas_item_show(quad_canvasitem); + tc->text_selection_quads.push_back(quad_canvasitem); + } + + if (tc->shape_editor != nullptr) { + if (tc->shape_editor->knotholder) { + tc->shape_editor->knotholder->update_knots(); + } + } +} + +static gint sp_text_context_timeout(TextTool *tc) +{ + if (tc->show) { + sp_canvas_item_show(tc->cursor); + if (tc->phase) { + tc->phase = false; + tc->cursor->setRgba32(0x000000ff); + } else { + tc->phase = true; + tc->cursor->setRgba32(0xffffffff); + } + } + + return TRUE; +} + +static void sp_text_context_forget_text(TextTool *tc) +{ + if (! tc->text) return; + SPItem *ti = tc->text; + (void)ti; + /* We have to set it to zero, + * or selection changed signal messes everything up */ + tc->text = nullptr; + +/* FIXME: this automatic deletion when nothing is inputted crashes the XML edittor and also crashes when duplicating an empty flowtext. + So don't create an empty flowtext in the first place? Create it when first character is typed. + */ +/* + if ((SP_IS_TEXT(ti) || SP_IS_FLOWTEXT(ti)) && sp_te_input_is_empty(ti)) { + Inkscape::XML::Node *text_repr = ti->getRepr(); + // the repr may already have been unparented + // if we were called e.g. as the result of + // an undo or the element being removed from + // the XML editor + if ( text_repr && text_repr->parent() ) { + sp_repr_unparent(text_repr); + SPDocumentUndo::done(tc->desktop->getDocument(), SP_VERB_CONTEXT_TEXT, + _("Remove empty text")); + } + } +*/ +} + +gint sptc_focus_in(GtkWidget *widget, GdkEventFocus */*event*/, TextTool *tc) +{ + gtk_im_context_focus_in(tc->imc); + return FALSE; +} + +gint sptc_focus_out(GtkWidget */*widget*/, GdkEventFocus */*event*/, TextTool *tc) +{ + gtk_im_context_focus_out(tc->imc); + return FALSE; +} + +static void sptc_commit(GtkIMContext */*imc*/, gchar *string, TextTool *tc) +{ + if (!tc->text) { + sp_text_context_setup_text(tc); + tc->nascent_object = false; // we don't need it anymore, having created a real <text> + } + + tc->text_sel_start = tc->text_sel_end = sp_te_replace(tc->text, tc->text_sel_start, tc->text_sel_end, string); + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); + + DocumentUndo::done(tc->text->document, SP_VERB_CONTEXT_TEXT, + _("Type text")); +} + +void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where) +{ + tc->desktop->selection->set (text); + tc->text_sel_start = tc->text_sel_end = where; + sp_text_context_update_cursor(tc); + sp_text_context_update_text_selection(tc); +} + +void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p) +{ + tc->desktop->selection->set (text); + sp_text_context_place_cursor (tc, text, sp_te_get_position_by_coords(tc->text, p)); +} + +Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text) +{ + if (text != tc->text) + return nullptr; + return &(tc->text_sel_end); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/text-tool.h b/src/ui/tools/text-tool.h new file mode 100644 index 0000000..9ea353a --- /dev/null +++ b/src/ui/tools/text-tool.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TEXT_CONTEXT_H__ +#define __SP_TEXT_CONTEXT_H__ + +/* + * TextTool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/connection.h> + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> +#include "libnrtype/Layout-TNG.h" + +#define SP_TEXT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::TextTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_TEXT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::TextTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +typedef struct _GtkIMContext GtkIMContext; + +struct SPCtrlLine; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class TextTool : public ToolBase { +public: + TextTool(); + ~TextTool() override; + + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; + sigc::connection style_set_connection; + sigc::connection style_query_connection; + + GtkIMContext *imc; + + SPItem *text; // the text we're editing, or NULL if none selected + + /* Text item position in root coordinates */ + Geom::Point pdoc; + /* Insertion point position */ + Inkscape::Text::Layout::iterator text_sel_start; + Inkscape::Text::Layout::iterator text_sel_end; + + gchar uni[9]; + bool unimode; + guint unipos; + + SPCtrlLine *cursor; + SPCanvasItem *indicator; + SPCanvasItem *frame; // hiliting the first frame of flowtext; FIXME: make this a list to accommodate arbitrarily many chained shapes + std::vector<SPCanvasItem*> text_selection_quads; + gint timeout; + bool show; + bool phase; + bool nascent_object; // true if we're clicked on canvas to put cursor, but no text typed yet so ->text is still NULL + + bool over_text; // true if cursor is over a text object + + guint dragging : 2; // dragging selection over text + bool creating; // dragging rubberband to create flowtext + SPCanvasItem *grabbed; // we grab while we are creating, to get events even if the mouse goes out of the window + Geom::Point p0; // initial point if the flowtext rect + + /* Preedit String */ + gchar* preedit_string; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + void _selectionChanged(Inkscape::Selection *selection); + void _selectionModified(Inkscape::Selection *selection, guint flags); + bool _styleSet(SPCSSAttr const *css); + int _styleQueried(SPStyle *style, int property); +}; + +bool sp_text_paste_inline(ToolBase *ec); +Glib::ustring sp_text_get_selected_text(ToolBase const *ec); +SPCSSAttr *sp_text_get_style_at_cursor(ToolBase const *ec); +// std::vector<SPCSSAttr*> sp_text_get_selected_style(ToolBase const *ec, unsigned *k, int *b, std::vector<unsigned> +// *positions); +bool sp_text_delete_selection(ToolBase *ec); +void sp_text_context_place_cursor (TextTool *tc, SPObject *text, Inkscape::Text::Layout::iterator where); +void sp_text_context_place_cursor_at (TextTool *tc, SPObject *text, Geom::Point const p); +Inkscape::Text::Layout::iterator *sp_text_context_get_cursor_position(TextTool *tc, SPObject *text); + +} +} +} + +#endif diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp new file mode 100644 index 0000000..6dcbe4c --- /dev/null +++ b/src/ui/tools/tool-base.cpp @@ -0,0 +1,1630 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Main event handling, and related helper functions. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 1999-2012 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> +#include <glibmm/i18n.h> + +#include "shortcuts.h" +#include "file.h" + + + +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "gradient-drag.h" +#include "knot-ptr.h" +#include "include/macros.h" +#include "message-context.h" +#include "rubberband.h" +#include "selcue.h" +#include "selection.h" +#include "sp-cursor.h" + +#include "display/sp-canvas.h" +#include "display/sp-canvas-group.h" +#include "display/canvas-rotate.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-guide.h" + +#include "ui/contextmenu.h" +#include "ui/interface.h" +#include "ui/event-debug.h" +#include "ui/tool/control-point.h" +#include "ui/shape-editor.h" +#include "ui/tools/tool-base.h" +#include "ui/tools-switch.h" +#include "ui/tools/lpe-tool.h" +#include "ui/tool/commit-events.h" +#include "ui/tool/event-utils.h" +#include "ui/tools/node-tool.h" +#include "ui/tool/shape-record.h" + +#include "widgets/desktop-widget.h" + +#include "xml/node-event-vector.h" + +// globals for temporary switching to selector by space +static bool selector_toggled = FALSE; +static int switch_selector_to = 0; + +// globals for temporary switching to dropper by 'D' +static bool dropper_toggled = FALSE; +static int switch_dropper_to = 0; + +// globals for keeping track of keyboard scroll events in order to accelerate +static guint32 scroll_event_time = 0; +static gdouble scroll_multiply = 1; +static guint scroll_keyval = 0; + +// globals for key processing +static bool latin_keys_group_valid = FALSE; +static gint latin_keys_group; + + +namespace Inkscape { +namespace UI { +namespace Tools { + +static void set_event_location(SPDesktop * desktop, GdkEvent * event); + + +void ToolBase::set(const Inkscape::Preferences::Entry& /*val*/) { +} + +void ToolBase::finish() { + this->desktop->canvas->endForcedFullRedraws(); + this->enableSelectionCue(false); +} + +SPDesktop const& ToolBase::getDesktop() const { + return *desktop; +} + +ToolBase::ToolBase(gchar const *const *cursor_shape, bool uses_snap) + : pref_observer(nullptr) + , cursor(nullptr) + , xp(0) + , yp(0) + , tolerance(0) + , within_tolerance(false) + , item_to_select(nullptr) + , message_context(nullptr) + , _selcue(nullptr) + , _grdrag(nullptr) + , shape_editor(nullptr) + , space_panning(false) + , _delayed_snap_event(nullptr) + , _dse_callback_in_process(false) + , desktop(nullptr) + , _uses_snap(uses_snap) + , cursor_shape(cursor_shape) + , _button1on(false) + , _button3on(false) +{ +} + +ToolBase::~ToolBase() { + this->message_context = nullptr; + + if (this->desktop) { + this->desktop = nullptr; + } + + if (this->pref_observer) { + delete this->pref_observer; + } + + if (this->_delayed_snap_event) { + delete this->_delayed_snap_event; + } +} + + +/** + * Set the cursor to a standard GDK cursor + */ +void ToolBase::sp_event_context_set_cursor(GdkCursorType cursor_type) { + + GtkWidget *w = GTK_WIDGET(this->desktop->getCanvas()); + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, cursor_type); + + if (cursor) { + gdk_window_set_cursor (gtk_widget_get_window (w), cursor); + g_object_unref (cursor); + } +} + +/** + * Recreates and draws cursor on desktop related to ToolBase. + */ +void ToolBase::sp_event_context_update_cursor() { + Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas())); + if (w->get_window()) { + if (this->cursor_shape) { + bool fillHasColor=false, strokeHasColor=false; + guint32 fillColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), true, &fillHasColor); + guint32 strokeColor = sp_desktop_get_color_tool(this->desktop, this->getPrefsPath(), false, &strokeHasColor); + double fillOpacity = fillHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), true) : 1.0; + double strokeOpacity = strokeHasColor ? sp_desktop_get_opacity_tool(this->desktop, this->getPrefsPath(), false) : 1.0; + + this->cursor = Glib::wrap(sp_cursor_from_xpm( + this->cursor_shape, + SP_RGBA32_C_COMPOSE(fillColor, fillOpacity), + SP_RGBA32_C_COMPOSE(strokeColor, strokeOpacity) + )); + } + w->get_window()->set_cursor(cursor); + w->get_display()->flush(); + } + this->desktop->waiting_cursor = false; +} + +/** + * Callback that gets called on initialization of ToolBase object. + * Redraws mouse cursor, at the moment. + * + * When you override it, call this method first. + */ +void ToolBase::setup() { + this->pref_observer = new ToolPrefObserver(this->getPrefsPath(), this); + Inkscape::Preferences::get()->addObserver(*(this->pref_observer)); + this->sp_event_context_update_cursor(); +} + +/** + * Gobbles next key events on the queue with the same keyval and mask. Returns the number of events consumed. + */ +gint gobble_key_events(guint keyval, gint mask) { + GdkEvent *event_next; + gint i = 0; + + event_next = gdk_event_get(); + // while the next event is also a key notify with the same keyval and mask, + while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type + == GDK_KEY_RELEASE) && event_next->key.keyval == keyval && (!mask + || (event_next->key.state & mask))) { + if (event_next->type == GDK_KEY_PRESS) + i++; + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); + + return i; +} + +/** + * Gobbles next motion notify events on the queue with the same mask. Returns the number of events consumed. + */ +gint gobble_motion_events(gint mask) { + GdkEvent *event_next; + gint i = 0; + + event_next = gdk_event_get(); + // while the next event is also a key notify with the same keyval and mask, + while (event_next && event_next->type == GDK_MOTION_NOTIFY + && (event_next->motion.state & mask)) { + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + i++; + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); + + return i; +} + +/** + * Toggles current tool between active tool and selector tool. + * Subroutine of sp_event_context_private_root_handler(). + */ +static void sp_toggle_selector(SPDesktop *dt) { + if (!dt->event_context) + return; + + if (tools_isactive(dt, TOOLS_SELECT)) { + if (selector_toggled) { + if (switch_selector_to) + tools_switch(dt, switch_selector_to); + selector_toggled = FALSE; + } else + return; + } else { + selector_toggled = TRUE; + switch_selector_to = tools_active(dt); + tools_switch(dt, TOOLS_SELECT); + } +} + +/** + * Toggles current tool between active tool and dropper tool. + * Subroutine of sp_event_context_private_root_handler(). + */ +void sp_toggle_dropper(SPDesktop *dt) { + if (!dt->event_context) + return; + + if (tools_isactive(dt, TOOLS_DROPPER)) { + if (dropper_toggled) { + if (switch_dropper_to) + tools_switch(dt, switch_dropper_to); + dropper_toggled = FALSE; + } else + return; + } else { + dropper_toggled = TRUE; + switch_dropper_to = tools_active(dt); + tools_switch(dt, TOOLS_DROPPER); + } +} + +/** + * Calculates and keeps track of scroll acceleration. + * Subroutine of sp_event_context_private_root_handler(). + */ +static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration, + SPCanvas */*canvas*/) { + guint32 time_diff = ((GdkEventKey *) event)->time - scroll_event_time; + + /* key pressed within 500ms ? (1/2 second) */ + if (time_diff > 500 || event->key.keyval != scroll_keyval) { + scroll_multiply = 1; // abort acceleration + } else { + scroll_multiply += acceleration; // continue acceleration + } + + scroll_event_time = ((GdkEventKey *) event)->time; + scroll_keyval = event->key.keyval; + + return scroll_multiply; +} + +/** Moves the selected points along the supplied unit vector according to + * the modifier state of the supplied event. */ +bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) +{ + if (held_control(event)) return false; + unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + Geom::Point delta = dir * num; + if (held_shift(event)) delta *= 10; + if (held_alt(event)) { + delta /= desktop->current_zoom(); + } else { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double nudge = prefs->getDoubleLimited("/options/nudgedistance/value", 2, 0, 1000, "px"); + delta *= nudge; + } + if (shape_editor && shape_editor->has_knotholder()) { + KnotHolder * knotholder = shape_editor->knotholder; + if (knotholder) { + knotholder->transform_selected(Geom::Translate(delta)); + } + } else if (tools_isactive(desktop, TOOLS_NODES)) { + Inkscape::UI::Tools::NodeTool *nt = static_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context); + if (nt) { + for(auto i=nt->_shape_editors.begin();i!=nt->_shape_editors.end();++i){ + ShapeEditor * shape_editor = i->second; + if (shape_editor && shape_editor->has_knotholder()) { + KnotHolder * knotholder = shape_editor->knotholder; + if (knotholder) { + knotholder->transform_selected(Geom::Translate(delta)); + } + } + } + } + } + return true; +} + + +bool ToolBase::root_handler(GdkEvent* event) { + + // ui_dump_event (event, "ToolBase::root_handler"); + static Geom::Point button_w; + static unsigned int panning = 0; + static unsigned int panning_cursor = 0; + static unsigned int zoom_rb = 0; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + /// @todo REmove redundant /value in preference keys + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + bool allow_panning = prefs->getBool("/options/spacebarpans/value"); + gint ret = FALSE; + + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (panning) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + ret = TRUE; + } else { + /* sp_desktop_dialog(); */ + } + break; + + case GDK_BUTTON_PRESS: + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + button_w = Geom::Point(event->button.x, event->button.y); + + switch (event->button.button) { + case 1: + if (this->space_panning) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + sp_event_context_discard_delayed_snap_event(this); + } + panning = 1; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, nullptr, + event->button.time - 1); + + ret = TRUE; + } + break; + + case 2: + if ((event->button.state & GDK_CONTROL_MASK) && !desktop->get_rotation_lock()) { + // On screen canvas rotation preview + + // Grab background before doing anything else + sp_canvas_rotate_start (SP_CANVAS_ROTATE(desktop->canvas_rotate), + desktop->canvas->_backing_store); + sp_canvas_item_ungrab (desktop->acetate); + sp_canvas_item_show (desktop->canvas_rotate); + sp_canvas_item_grab (desktop->canvas_rotate, + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK, + nullptr, event->button.time ); + // sp_canvas_item_hide (desktop->drawing); + + } else if (event->button.state & GDK_SHIFT_MASK) { + zoom_rb = 2; + } else { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + sp_event_context_discard_delayed_snap_event(this); + } + panning = 2; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | + GDK_POINTER_MOTION_HINT_MASK, + nullptr, event->button.time - 1); + + } + + ret = TRUE; + break; + + case 3: + if ((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK)) { + // When starting panning, make sure there are no snap events pending because these might disable the panning again + if (_uses_snap) { + sp_event_context_discard_delayed_snap_event(this); + } + panning = 3; + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, nullptr, + event->button.time); + + ret = TRUE; + } else { + sp_event_root_menu_popup(desktop, nullptr, event); + } + break; + + default: + break; + } + break; + + case GDK_MOTION_NOTIFY: + if (panning) { + if (panning == 4 && !xp && !yp ) { + // <Space> + mouse panning started, save location and grab canvas + xp = event->motion.x; + yp = event->motion.y; + button_w = Geom::Point(event->motion.x, event->motion.y); + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_RELEASE_MASK | GDK_BUTTON_RELEASE_MASK + | GDK_POINTER_MOTION_MASK + | GDK_POINTER_MOTION_HINT_MASK, nullptr, + event->motion.time - 1); + } + + if ((panning == 2 && !(event->motion.state & GDK_BUTTON2_MASK)) + || (panning == 1 && !(event->motion.state & GDK_BUTTON1_MASK)) + || (panning == 3 && !(event->motion.state & GDK_BUTTON3_MASK))) { + /* Gdk seems to lose button release for us sometimes :-( */ + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + ret = TRUE; + } else { + // To fix https://bugs.launchpad.net/inkscape/+bug/1458200 + // we increase the tolerance because no sensible data for panning + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance * 3) && (abs((gint) event->motion.y - yp) + < tolerance * 3)) { + // do not drag if we're within tolerance from origin + break; + } + + // Once the user has moved farther than tolerance from + // the original location (indicating they intend to move + // the object, not click), then always process the motion + // notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + // gobble subsequent motion events to prevent "sticking" + // when scrolling is slow + gobble_motion_events(panning == 2 ? GDK_BUTTON2_MASK : (panning + == 1 ? GDK_BUTTON1_MASK : GDK_BUTTON3_MASK)); + + if (panning_cursor == 0) { + panning_cursor = 1; + this->sp_event_context_set_cursor(GDK_FLEUR); + } + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const moved_w(motion_w - button_w); + this->desktop->scroll_relative(moved_w, true); // we're still scrolling, do not redraw + ret = TRUE; + } + } else if (zoom_rb) { + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + if (within_tolerance && (abs((gint) event->motion.x - xp) + < tolerance) && (abs((gint) event->motion.y - yp) + < tolerance)) { + break; // do not drag if we're within tolerance from origin + } + + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + if (Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::Rubberband::get(desktop)->move(motion_dt); + } else { + Inkscape::Rubberband::get(desktop)->start(desktop, motion_dt); + } + + if (zoom_rb == 2) { + gobble_motion_events(GDK_BUTTON2_MASK); + } + } + break; + + case GDK_BUTTON_RELEASE: { + bool middle_mouse_zoom = prefs->getBool("/options/middlemousezoom/value"); + + xp = yp = 0; + + if (panning_cursor == 1) { + panning_cursor = 0; + Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas())); + w->get_window()->set_cursor(cursor); + } + + if (middle_mouse_zoom && within_tolerance && (panning || zoom_rb)) { + zoom_rb = 0; + + if (panning) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + } + + Geom::Point const event_w(event->button.x, event->button.y); + Geom::Point const event_dt(desktop->w2d(event_w)); + + double const zoom_inc = prefs->getDoubleLimited( + "/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + desktop->zoom_relative_keep_point(event_dt, (event->button.state + & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc); + + desktop->updateNow(); + ret = TRUE; + } else if (panning == event->button.button) { + panning = 0; + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + // in slow complex drawings, some of the motion events are lost; + // to make up for this, we scroll it once again to the button-up event coordinates + // (i.e. canvas will always get scrolled all the way to the mouse release point, + // even if few intermediate steps were visible) + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const moved_w(motion_w - button_w); + + this->desktop->scroll_relative(moved_w); + desktop->updateNow(); + ret = TRUE; + } else if (zoom_rb == event->button.button) { + zoom_rb = 0; + + Geom::OptRect const b = Inkscape::Rubberband::get(desktop)->getRectangle(); + Inkscape::Rubberband::get(desktop)->stop(); + + if (b && !within_tolerance) { + desktop->set_display_area(*b, 10); + } + + ret = TRUE; + } + } + break; + + case GDK_KEY_PRESS: { + double const acceleration = prefs->getDoubleLimited( + "/options/scrollingacceleration/value", 0, 0, 6); + int const key_scroll = prefs->getIntLimited("/options/keyscroll/value", + 10, 0, 1000); + + switch (get_latin_keyval(&event->key)) { + // GDK insists on stealing these keys (F1 for no idea what, tab for cycling widgets + // in the editing window). So we resteal them back and run our regular shortcut + // invoker on them. + unsigned int shortcut; + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + case GDK_KEY_F1: + shortcut = sp_shortcut_get_for_event((GdkEventKey*)event); + ret = sp_shortcut_invoke(shortcut, desktop); + break; + + case GDK_KEY_Q: + case GDK_KEY_q: + if (desktop->quick_zoomed()) { + ret = TRUE; + } + if (!MOD__SHIFT(event) && !MOD__CTRL(event) && !MOD__ALT(event)) { + desktop->zoom_quick(true); + ret = TRUE; + } + break; + + case GDK_KEY_W: + case GDK_KEY_w: + case GDK_KEY_F4: + /* Close view */ + if (MOD__CTRL_ONLY(event)) { + sp_ui_close_view(nullptr); + ret = TRUE; + } + break; + + case GDK_KEY_Left: // Ctrl Left + case GDK_KEY_KP_Left: + case GDK_KEY_KP_4: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, desktop->getCanvas())); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_relative(Geom::Point(i, 0)); + ret = TRUE; + } else { + ret = _keyboardMove(event->key, Geom::Point(-1, 0)); + } + break; + + case GDK_KEY_Up: // Ctrl Up + case GDK_KEY_KP_Up: + case GDK_KEY_KP_8: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, desktop->getCanvas())); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_relative(Geom::Point(0, i)); + ret = TRUE; + } else { + ret = _keyboardMove(event->key, Geom::Point(0, -desktop->yaxisdir())); + } + break; + + case GDK_KEY_Right: // Ctrl Right + case GDK_KEY_KP_Right: + case GDK_KEY_KP_6: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, desktop->getCanvas())); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_relative(Geom::Point(-i, 0)); + ret = TRUE; + } else { + ret = _keyboardMove(event->key, Geom::Point(1, 0)); + } + break; + + case GDK_KEY_Down: // Ctrl Down + case GDK_KEY_KP_Down: + case GDK_KEY_KP_2: + if (MOD__CTRL_ONLY(event)) { + int i = (int) floor(key_scroll * accelerate_scroll(event, + acceleration, desktop->getCanvas())); + + gobble_key_events(get_latin_keyval(&event->key), GDK_CONTROL_MASK); + this->desktop->scroll_relative(Geom::Point(0, -i)); + ret = TRUE; + } else { + ret = _keyboardMove(event->key, Geom::Point(0, desktop->yaxisdir())); + } + break; + + case GDK_KEY_Menu: + sp_event_root_menu_popup(desktop, nullptr, event); + ret = TRUE; + break; + + case GDK_KEY_F10: + if (MOD__SHIFT_ONLY(event)) { + sp_event_root_menu_popup(desktop, nullptr, event); + ret = TRUE; + } + break; + + case GDK_KEY_space: + within_tolerance = true; + xp = yp = 0; + if (!allow_panning) break; + panning = 4; + this->space_panning = true; + this->message_context->set(Inkscape::INFORMATION_MESSAGE, + _("<b>Space+mouse move</b> to pan canvas")); + + ret = TRUE; + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + if (MOD__ALT_ONLY(event)) { + desktop->zoom_grab_focus(); + ret = TRUE; + } + break; + + default: + break; + } + } + break; + + case GDK_KEY_RELEASE: + // Stop panning on any key release + if (this->space_panning) { + this->space_panning = false; + this->message_context->clear(); + } + + if (panning) { + panning = 0; + xp = yp = 0; + + sp_canvas_item_ungrab(SP_CANVAS_ITEM(desktop->acetate)); + + desktop->updateNow(); + } + + if (panning_cursor == 1) { + panning_cursor = 0; + Gtk::Widget* w = Glib::wrap(GTK_WIDGET(desktop->getCanvas())); + w->get_window()->set_cursor(cursor); + } + + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_space: + if (within_tolerance) { + // Space was pressed, but not panned + sp_toggle_selector(desktop); + + // Be careful, sp_toggle_selector will delete ourselves. + // Thus, make sure we return immediately. + return true; + } + + break; + + case GDK_KEY_Q: + case GDK_KEY_q: + if (desktop->quick_zoomed()) { + desktop->zoom_quick(false); + ret = TRUE; + } + break; + + default: + break; + } + break; + + case GDK_SCROLL: { + bool ctrl = (event->scroll.state & GDK_CONTROL_MASK); + bool shift = (event->scroll.state & GDK_SHIFT_MASK); + bool wheelzooms = prefs->getBool("/options/wheelzooms/value"); + + int constexpr WHEEL_SCROLL_DEFAULT = 40; + int const wheel_scroll = prefs->getIntLimited( + "/options/wheelscroll/value", WHEEL_SCROLL_DEFAULT, 0, 1000); + + // Size of smooth-scrolls (only used in GTK+ 3) + gdouble delta_x = 0; + gdouble delta_y = 0; + + if ((ctrl & shift) && !desktop->get_rotation_lock()) { + /* ctrl + shift, rotate */ + + double rotate_inc = prefs->getDoubleLimited( + "/options/rotateincrement/value", 15, 1, 90, "°" ); + rotate_inc *= M_PI/180.0; + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + // Do nothing + break; + + case GDK_SCROLL_DOWN: + rotate_inc = -rotate_inc; + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + double delta_y_clamped = CLAMP(delta_y, -1.0, 1.0); // values > 1 result in excessive rotating + rotate_inc = rotate_inc * -delta_y_clamped; + break; + } + + default: + rotate_inc = 0.0; + break; + } + + if (rotate_inc != 0.0) { + Geom::Point const scroll_dt = desktop->point(); + desktop->rotate_relative_keep_point(scroll_dt, rotate_inc); + } + + } else if (shift && !ctrl) { + /* shift + wheel, pan left--right */ + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + case GDK_SCROLL_LEFT: + desktop->scroll_relative(Geom::Point(wheel_scroll, 0)); + break; + + case GDK_SCROLL_DOWN: + case GDK_SCROLL_RIGHT: + desktop->scroll_relative(Geom::Point(-wheel_scroll, 0)); + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + desktop->scroll_relative(Geom::Point(wheel_scroll * -delta_y, 0)); + break; + } + + default: + break; + } + + } else if ((ctrl && !wheelzooms) || (!ctrl && wheelzooms)) { + /* ctrl + wheel, zoom in--out */ + double rel_zoom; + double const zoom_inc = prefs->getDoubleLimited( + "/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + rel_zoom = zoom_inc; + break; + + case GDK_SCROLL_DOWN: + rel_zoom = 1 / zoom_inc; + break; + + case GDK_SCROLL_SMOOTH: { + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + double delta_y_clamped = CLAMP(std::abs(delta_y), 0.0, 1.0); // values > 1 result in excessive zooming + double zoom_inc_scaled = (zoom_inc-1) * delta_y_clamped + 1; + if (delta_y < 0) { + rel_zoom = zoom_inc_scaled; + } else { + rel_zoom = 1 / zoom_inc_scaled; + } + break; + } + + default: + rel_zoom = 0.0; + break; + } + + if (rel_zoom != 0.0) { + Geom::Point const scroll_dt = desktop->point(); + desktop->zoom_relative_keep_point(scroll_dt, rel_zoom); + } + + /* no modifier, pan up--down (left--right on multiwheel mice?) */ + } else { + switch (event->scroll.direction) { + case GDK_SCROLL_UP: + desktop->scroll_relative(Geom::Point(0, wheel_scroll)); + break; + + case GDK_SCROLL_DOWN: + desktop->scroll_relative(Geom::Point(0, -wheel_scroll)); + break; + + case GDK_SCROLL_LEFT: + desktop->scroll_relative(Geom::Point(wheel_scroll, 0)); + break; + + case GDK_SCROLL_RIGHT: + desktop->scroll_relative(Geom::Point(-wheel_scroll, 0)); + break; + + case GDK_SCROLL_SMOOTH: + gdk_event_get_scroll_deltas(event, &delta_x, &delta_y); +#ifdef GDK_WINDOWING_QUARTZ + // MacBook trackpad scroll event gives pixel delta + delta_x /= WHEEL_SCROLL_DEFAULT; + delta_y /= WHEEL_SCROLL_DEFAULT; +#endif + desktop->scroll_relative(Geom::Point(-wheel_scroll*delta_x, -wheel_scroll*delta_y)); + break; + } + } + break; + } + + default: + break; + } + + return ret; +} + +/** + * This function allow to handle global tool events if not _pre function is full overrided. + */ + +bool ToolBase::block_button(GdkEvent *event) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + switch (event->button.button) { + case 1: + this->_button1on = true; + break; + case 2: + this->_button2on = true; + break; + case 3: + this->_button3on = true; + break; + } + break; + case GDK_BUTTON_RELEASE: + switch (event->button.button) { + case 1: + this->_button1on = false; + break; + case 2: + this->_button2on = false; + break; + case 3: + this->_button3on = false; + break; + } + break; + case GDK_MOTION_NOTIFY: + if (event->motion.state & Gdk::ModifierType::BUTTON1_MASK) { + this->_button1on = true; + } else { + this->_button1on = false; + } + if (event->motion.state & Gdk::ModifierType::BUTTON2_MASK) { + this->_button2on = true; + } else { + this->_button2on = false; + } + if (event->motion.state & Gdk::ModifierType::BUTTON3_MASK) { + this->_button3on = true; + } else { + this->_button3on = false; + } + } + if (this->_button1on == true && this->_button3on == true) { + return true; + } + return false; +} + +/** + * Handles item specific events. Gets called from Gdk. + * + * Only reacts to right mouse button at the moment. + * \todo Fixme: do context sensitive popup menu on items. + */ +bool ToolBase::item_handler(SPItem* item, GdkEvent* event) { + int ret = FALSE; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 3 && + !((event->button.state & GDK_SHIFT_MASK) || (event->button.state & GDK_CONTROL_MASK))) { + sp_event_root_menu_popup(this->desktop, item, event); + ret = TRUE; + } + break; + + default: + break; + } + + return ret; +} + +/** + * Returns true if we're hovering above a knot (needed because we don't want to pre-snap in that case). + */ +bool ToolBase::sp_event_context_knot_mouseover() const { + if (this->shape_editor) { + return this->shape_editor->knot_mouseover(); + } + + return false; +} + +/** + * Enables/disables the ToolBase's SelCue. + */ +void ToolBase::enableSelectionCue(bool enable) { + if (enable) { + if (!_selcue) { + _selcue = new Inkscape::SelCue(desktop); + } + } else { + delete _selcue; + _selcue = nullptr; + } +} + +/** + * Enables/disables the ToolBase's GrDrag. + */ +void ToolBase::enableGrDrag(bool enable) { + if (enable) { + if (!_grdrag) { + _grdrag = new GrDrag(desktop); + } + } else { + if (_grdrag) { + delete _grdrag; + _grdrag = nullptr; + } + } +} + +/** + * Delete a selected GrDrag point + */ +bool ToolBase::deleteSelectedDrag(bool just_one) { + + if (_grdrag && !_grdrag->selected.empty()) { + _grdrag->deleteSelected(just_one); + return TRUE; + } + + return FALSE; +} + +/** Enable (or disable) high precision for motion events + * + * This is intended to be used by drawing tools, that need to process motion events with high accuracy + * and high update rate (for example free hand tools) + * + * With standard accuracy some intermediate motion events might be discarded + * + * Call this function when an operation that requires high accuracy is started (e.g. mouse button is pressed + * to draw a line). Make sure to call it again and restore standard precision afterwards. **/ +void ToolBase::set_high_motion_precision(bool high_precision) { + Glib::RefPtr<Gdk::Window> window = desktop->getToplevel()->get_window(); + + if (window) { + window->set_event_compression(!high_precision); + } +} + +/** + * Calls virtual set() function of ToolBase. + */ +void sp_event_context_read(ToolBase *ec, gchar const *key) { + g_return_if_fail(ec != nullptr); + g_return_if_fail(SP_IS_EVENT_CONTEXT(ec)); + g_return_if_fail(key != nullptr); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Inkscape::Preferences::Entry val = prefs->getEntry(ec->pref_observer->observed_path + '/' + key); + ec->set(val); +} + +/** + * Calls virtual root_handler(), the main event handling function. + */ +gint sp_event_context_root_handler(ToolBase * event_context, + GdkEvent * event) +{ + + if (!event_context->_uses_snap) { + return sp_event_context_virtual_root_handler(event_context, event); + } + + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(event_context, nullptr, nullptr, + (GdkEventMotion *) event, + DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (event_context && event_context->_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback( + event_context->_delayed_snap_event); + } + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + // Snapping will be on hold if we're moving the mouse at high speeds. When starting + // drawing a new shape we really should snap though. + event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + break; + default: + break; + } + + return sp_event_context_virtual_root_handler(event_context, event); +} + +gint sp_event_context_virtual_root_handler(ToolBase * event_context, GdkEvent * event) { + gint ret = false; + + if (event_context) { + + if (event_context->block_button(event)) { + return false; + } + SPDesktop* desktop = event_context->desktop; + ret = event_context->root_handler(event); + + set_event_location(desktop, event); + } + + return ret; +} + +/** + * Calls virtual item_handler(), the item event handling function. + */ +gint sp_event_context_item_handler(ToolBase * event_context, + SPItem * item, GdkEvent * event) +{ + if (!event_context->_uses_snap) { + return sp_event_context_virtual_item_handler(event_context, item, event); + } + + switch (event->type) { + case GDK_MOTION_NOTIFY: + sp_event_context_snap_delay_handler(event_context, (gpointer) item, nullptr, (GdkEventMotion *) event, DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER); + break; + case GDK_BUTTON_RELEASE: + if (event_context && event_context->_delayed_snap_event) { + // If we have any pending snapping action, then invoke it now + sp_event_context_snap_watchdog_callback(event_context->_delayed_snap_event); + } + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + // Snapping will be on hold if we're moving the mouse at high speeds. When starting + // drawing a new shape we really should snap though. + event_context->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + break; + default: + break; + } + + return sp_event_context_virtual_item_handler(event_context, item, event); +} + +gint sp_event_context_virtual_item_handler(ToolBase * event_context, SPItem * item, GdkEvent * event) { + gint ret = false; + if (event_context) { // If no event-context is available then do nothing, otherwise Inkscape would crash + // (see the comment in SPDesktop::set_event_context, and bug LP #622350) + if (event_context->block_button(event)) { + return false; + } + // et = (SP_EVENT_CONTEXT_CLASS(G_OBJECT_GET_CLASS(event_context)))->item_handler(event_context, item, event); + ret = event_context->item_handler(item, event); + + if (!ret) { + ret = sp_event_context_virtual_root_handler(event_context, event); + } else { + set_event_location(event_context->desktop, event); + } + } + + return ret; +} + +/** + * Shows coordinates on status bar. + */ +static void set_event_location(SPDesktop *desktop, GdkEvent *event) { + if (event->type != GDK_MOTION_NOTIFY) { + return; + } + + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + desktop->set_coordinate_status(button_dt); +} + +//------------------------------------------------------------------- +/** + * Create popup menu and tell Gtk to show it. + */ +void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event) { + + // It seems the param item is the SPItem at the bottom of the z-order + // Using the same function call used on left click in sp_select_context_item_handler() to get top of z-order + // fixme: sp_canvas_arena should set the top z-order object as arena->active + item = sp_event_context_find_item (desktop, + Geom::Point(event->button.x, event->button.y), FALSE, FALSE); + + if (event->type == GDK_KEY_PRESS && !desktop->getSelection()->isEmpty()) { + item = desktop->getSelection()->items().front(); + } + + ContextMenu* CM = new ContextMenu(desktop, item); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (window) { + if (window->get_style_context()->has_class("dark")) { + CM->get_style_context()->add_class("dark"); + } else { + CM->get_style_context()->add_class("bright"); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + CM->get_style_context()->add_class("symbolic"); + } else { + CM->get_style_context()->add_class("regular"); + } + } + CM->show(); + + + switch (event->type) { + case GDK_BUTTON_PRESS: + case GDK_KEY_PRESS: + CM->popup_at_pointer(event); + break; + default: + break; + } +} + +/** + * Show tool context specific modifier tip. + */ +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, + GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip, + gchar const *alt_tip) { + guint keyval = get_latin_keyval(&event->key); + + bool ctrl = ctrl_tip && (MOD__CTRL(event) || (keyval == GDK_KEY_Control_L) || (keyval + == GDK_KEY_Control_R)); + bool shift = shift_tip && (MOD__SHIFT(event) || (keyval == GDK_KEY_Shift_L) || (keyval + == GDK_KEY_Shift_R)); + bool alt = alt_tip && (MOD__ALT(event) || (keyval == GDK_KEY_Alt_L) || (keyval + == GDK_KEY_Alt_R) || (keyval == GDK_KEY_Meta_L) || (keyval == GDK_KEY_Meta_R)); + + gchar *tip = g_strdup_printf("%s%s%s%s%s", (ctrl ? ctrl_tip : ""), (ctrl + && (shift || alt) ? "; " : ""), (shift ? shift_tip : ""), ((ctrl + || shift) && alt ? "; " : ""), (alt ? alt_tip : "")); + + if (strlen(tip) > 0) { + message_context->flash(Inkscape::INFORMATION_MESSAGE, tip); + } + + g_free(tip); +} + +/** + * Try to determine the keys group of Latin layout. + * Check available keymap entries for Latin 'a' key and find the minimal integer value. + */ +static void update_latin_keys_group() { + GdkKeymapKey* keys; + gint n_keys; + + latin_keys_group_valid = FALSE; + if (gdk_keymap_get_entries_for_keyval(Gdk::Display::get_default()->get_keymap(), GDK_KEY_a, &keys, &n_keys)) { + for (gint i = 0; i < n_keys; i++) { + if (!latin_keys_group_valid || keys[i].group < latin_keys_group) { + latin_keys_group = keys[i].group; + latin_keys_group_valid = TRUE; + } + } + g_free(keys); + } +} + +/** + * Initialize Latin keys group handling. + */ +void init_latin_keys_group() { + g_signal_connect(G_OBJECT(Gdk::Display::get_default()->get_keymap()), + "keys-changed", G_CALLBACK(update_latin_keys_group), NULL); + update_latin_keys_group(); +} + +/** + * Return the keyval corresponding to the key event in Latin group. + * + * Use this instead of simply event->keyval, so that your keyboard shortcuts + * work regardless of layouts (e.g., in Cyrillic). + */ +guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers /*= NULL*/) { + guint keyval = 0; + GdkModifierType modifiers; + gint group = latin_keys_group_valid ? latin_keys_group : event->group; + + gdk_keymap_translate_keyboard_state( + Gdk::Display::get_default()->get_keymap(), + event->hardware_keycode, (GdkModifierType) event->state, group, + &keyval, nullptr, nullptr, &modifiers); + + if (consumed_modifiers) { +#ifndef GDK_WINDOWING_QUARTZ + *consumed_modifiers = modifiers; +#else + // gdk_quartz_keymap_translate_keyboard_state fills the `consumed_modifiers` + // incorrectly, e.g. assigns 0xB instead of 0x0 when no modifiers pressed. + + *consumed_modifiers = 0; + + for (unsigned mod = 1, statemask = (event->state & GDK_MODIFIER_MASK); mod <= statemask; mod <<= 1) { + if ((mod & statemask)) { + guint keyval_no_mod = 0; + gdk_keymap_translate_keyboard_state(Gdk::Display::get_default()->get_keymap(), event->hardware_keycode, + (GdkModifierType)(event->state & ~mod), group, &keyval_no_mod, + nullptr, nullptr, nullptr); + if (keyval_no_mod != keyval) { + *consumed_modifiers |= mod; + } + } + } +#endif + } + return keyval; +} + +/** + * Returns item at point p in desktop. + * + * If state includes alt key mask, cyclically selects under; honors + * into_groups. + */ +SPItem *sp_event_context_find_item(SPDesktop *desktop, Geom::Point const &p, + bool select_under, bool into_groups) +{ + SPItem *item = nullptr; + + if (select_under) { + auto tmp = desktop->selection->items(); + std::vector<SPItem *> vec(tmp.begin(), tmp.end()); + SPItem *selected_at_point = desktop->getItemFromListAtPointBottom(vec, p); + item = desktop->getItemAtPoint(p, into_groups, selected_at_point); + if (item == nullptr) { // we may have reached bottom, flip over to the top + item = desktop->getItemAtPoint(p, into_groups, nullptr); + } + } else { + item = desktop->getItemAtPoint(p, into_groups, nullptr); + } + + return item; +} + +/** + * Returns item if it is under point p in desktop, at any depth; otherwise returns NULL. + * + * Honors into_groups. + */ +SPItem * +sp_event_context_over_item(SPDesktop *desktop, SPItem *item, + Geom::Point const &p) { + std::vector<SPItem*> temp; + temp.push_back(item); + SPItem *item_at_point = desktop->getItemFromListAtPointBottom(temp, p); + return item_at_point; +} + +ShapeEditor * +sp_event_context_get_shape_editor(ToolBase *ec) { + return ec->shape_editor; +} + + +/** + * Analyses the current event, calculates the mouse speed, turns snapping off (temporarily) if the + * mouse speed is above a threshold, and stores the current event such that it can be re-triggered when needed + * (re-triggering is controlled by a watchdog timer). + * + * @param ec Pointer to the event context. + * @param dse_item Pointer that store a reference to a canvas or to an item. + * @param dse_item2 Another pointer, storing a reference to a knot or controlpoint. + * @param event Pointer to the motion event. + * @param origin Identifier (enum) specifying where the delay (and the call to this method) were initiated. + */ +void sp_event_context_snap_delay_handler(ToolBase *ec, + gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, + DelayedSnapEvent::DelayedSnapEventOrigin origin) +{ + static guint32 prev_time; + static boost::optional<Geom::Point> prev_pos; + + if (!ec->_uses_snap || ec->_dse_callback_in_process) { + return; + } + + // Snapping occurs when dragging with the left mouse button down, or when hovering e.g. in the pen tool with left mouse button up + bool const c1 = event->state & GDK_BUTTON2_MASK; // We shouldn't hold back any events when other mouse buttons have been + bool const c2 = event->state & GDK_BUTTON3_MASK; // pressed, e.g. when scrolling with the middle mouse button; if we do then + // Inkscape will get stuck in an unresponsive state + bool const c3 = tools_isactive(ec->desktop, TOOLS_CALLIGRAPHIC); + // The snap delay will repeat the last motion event, which will lead to + // erroneous points in the calligraphy context. And because we don't snap + // in this context, we might just as well disable the snap delay all together + bool const c4 = ec->space_panning; // Don't snap while panning with the spacebar + + if (c1 || c2 || c3 || c4) { + // Make sure that we don't send any pending snap events to a context if we know in advance + // that we're not going to snap any way (e.g. while scrolling with middle mouse button) + // Any motion event might affect the state of the context, leading to unexpected behavior + sp_event_context_discard_delayed_snap_event(ec); + } else if (ec->desktop + && ec->desktop->namedview->snap_manager.snapprefs.getSnapEnabledGlobally()) { + // Snap when speed drops below e.g. 0.02 px/msec, or when no motion events have occurred for some period. + // i.e. snap when we're at stand still. A speed threshold enforces snapping for tablets, which might never + // be fully at stand still and might keep spitting out motion events. + ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(true); // put snapping on hold + + Geom::Point event_pos(event->x, event->y); + guint32 event_t = gdk_event_get_time((GdkEvent *) event); + + if (prev_pos) { + Geom::Coord dist = Geom::L2(event_pos - *prev_pos); + guint32 delta_t = event_t - prev_time; + gdouble speed = delta_t > 0 ? dist / delta_t : 1000; + //std::cout << "Mouse speed = " << speed << " px/msec " << std::endl; + if (speed > 0.02) { // Jitter threshold, might be needed for tablets + // We're moving fast, so postpone any snapping until the next GDK_MOTION_NOTIFY event. We + // will keep on postponing the snapping as long as the speed is high. + // We must snap at some point in time though, so set a watchdog timer at some time from + // now, just in case there's no future motion event that drops under the speed limit (when + // stopping abruptly) + delete ec->_delayed_snap_event; + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); // watchdog is reset, i.e. pushed forward in time + // If the watchdog expires before a new motion event is received, we will snap (as explained + // above). This means however that when the timer is too short, we will always snap and that the + // speed threshold is ineffective. In the extreme case the delay is set to zero, and snapping will + // be immediate, as it used to be in the old days ;-). + } else { // Speed is very low, so we're virtually at stand still + // But if we're really standing still, then we should snap now. We could use some low-pass filtering, + // otherwise snapping occurs for each jitter movement. For this filtering we'll leave the watchdog to expire, + // snap, and set a new watchdog again. + if (ec->_delayed_snap_event == nullptr) { // no watchdog has been set + // it might have already expired, so we'll set a new one; the snapping frequency will be limited this way + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); + } // else: watchdog has been set before and we'll wait for it to expire + } + } else { + // This is the first GDK_MOTION_NOTIFY event, so postpone snapping and set the watchdog + g_assert(ec->_delayed_snap_event == nullptr); + ec->_delayed_snap_event = new DelayedSnapEvent(ec, dse_item, dse_item2, event, origin); + } + + prev_pos = event_pos; + prev_time = event_t; + } +} + +/** + * When the snap delay watchdog timer barks, this method will be called and will re-inject the last motion + * event in an appropriate place, with snapping being turned on again. + */ +gboolean sp_event_context_snap_watchdog_callback(gpointer data) { + // Snap NOW! For this the "postponed" flag will be reset and the last motion event will be repeated + DelayedSnapEvent *dse = reinterpret_cast<DelayedSnapEvent*> (data); + + if (dse == nullptr) { + // This might occur when this method is called directly, i.e. not through the timer + // E.g. on GDK_BUTTON_RELEASE in sp_event_context_root_handler() + return FALSE; + } + + ToolBase *ec = dse->getEventContext(); + if (ec == nullptr) { + delete dse; + return false; + } + if (ec->desktop == nullptr) { + ec->_delayed_snap_event = nullptr; + delete dse; + return false; + } + + ec->_dse_callback_in_process = true; + + SPDesktop *dt = ec->desktop; + dt->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); + + // Depending on where the delayed snap event originated from, we will inject it back at it's origin + // The switch below takes care of that and prepares the relevant parameters + switch (dse->getOrigin()) { + case DelayedSnapEvent::EVENTCONTEXT_ROOT_HANDLER: + sp_event_context_virtual_root_handler(ec, dse->getEvent()); + break; + case DelayedSnapEvent::EVENTCONTEXT_ITEM_HANDLER: { + gpointer item = dse->getItem(); + if (item && SP_IS_ITEM(item)) { + sp_event_context_virtual_item_handler(ec, SP_ITEM(item), dse->getEvent()); + } + } + break; + case DelayedSnapEvent::KNOT_HANDLER: { + gpointer knot = dse->getItem2(); + check_if_knot_deleted(knot); + if (knot && SP_IS_KNOT(knot)) { + sp_knot_handler_request_position(dse->getEvent(), SP_KNOT(knot)); + } + } + break; + case DelayedSnapEvent::CONTROL_POINT_HANDLER: { + using Inkscape::UI::ControlPoint; + gpointer pitem2 = dse->getItem2(); + if (!pitem2) + { + ec->_delayed_snap_event = nullptr; + delete dse; + return false; + } + ControlPoint *point = reinterpret_cast<ControlPoint*> (pitem2); + if (point) { + if (point->position().isFinite() && (dt == point->_desktop)) { + point->_eventHandler(ec, dse->getEvent()); + } + else { + //workaround: + //[Bug 781893] Crash after moving a Bezier node after Knot path effect? + // --> at some time, some point with X = 0 and Y = nan (not a number) is created ... + // even so, the desktop pointer is invalid and equal to 0xff + g_warning ("encountered non finite point when evaluating snapping callback"); + } + } + } + break; + case DelayedSnapEvent::GUIDE_HANDLER: { + gpointer item = dse->getItem(); + gpointer item2 = dse->getItem2(); + if (item && item2) { + g_assert(SP_IS_CANVAS_ITEM(item)); + g_assert(SP_IS_GUIDE(item2)); + sp_dt_guide_event(SP_CANVAS_ITEM(item), dse->getEvent(), item2); + } + } + break; + case DelayedSnapEvent::GUIDE_HRULER: + case DelayedSnapEvent::GUIDE_VRULER: { + gpointer item = dse->getItem(); + gpointer item2 = dse->getItem2(); + if (item && item2) { + g_assert(GTK_IS_WIDGET(item)); + g_assert(SP_IS_DESKTOP_WIDGET(item2)); + if (dse->getOrigin() == DelayedSnapEvent::GUIDE_HRULER) { + SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), true); + } else { + SPDesktopWidget::ruler_event(GTK_WIDGET(item), dse->getEvent(), SP_DESKTOP_WIDGET(item2), false); + } + } + } + break; + default: + g_warning("Origin of snap-delay event has not been defined!;"); + break; + } + + ec->_delayed_snap_event = nullptr; + delete dse; + + ec->_dse_callback_in_process = false; + + return FALSE; //Kills the timer and stops it from executing this callback over and over again. +} + +void sp_event_context_discard_delayed_snap_event(ToolBase *ec) { + delete ec->_delayed_snap_event; + ec->_delayed_snap_event = nullptr; + ec->desktop->namedview->snap_manager.snapprefs.setSnapPostponedGlobally(false); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h new file mode 100644 index 0000000..d3f1000 --- /dev/null +++ b/src/ui/tools/tool-base.h @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_EVENT_CONTEXT_H +#define SEEN_SP_EVENT_CONTEXT_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstddef> +#include <string> +#include <memory> +#include "knot.h" +#include "knotholder.h" +#include <2geom/point.h> +#include <gdk/gdk.h> +#include <gdkmm/cursor.h> +#include <glib-object.h> +#include <sigc++/trackable.h> + +#include "preferences.h" + +namespace Glib { + class ustring; +} + +class GrDrag; +class SPDesktop; +class SPItem; +class KnotHolder; +namespace Inkscape { + class MessageContext; + class SelCue; +} + +#define SP_EVENT_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ToolBase*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_EVENT_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ToolBase*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { + + +class ShapeEditor; + +namespace Tools { + +class ToolBase; + +gboolean sp_event_context_snap_watchdog_callback(gpointer data); +void sp_event_context_discard_delayed_snap_event(ToolBase *ec); + +class DelayedSnapEvent { +public: + enum DelayedSnapEventOrigin { + UNDEFINED_HANDLER = 0, + EVENTCONTEXT_ROOT_HANDLER, + EVENTCONTEXT_ITEM_HANDLER, + KNOT_HANDLER, + CONTROL_POINT_HANDLER, + GUIDE_HANDLER, + GUIDE_HRULER, + GUIDE_VRULER + }; + + DelayedSnapEvent(ToolBase *event_context, gpointer const dse_item, gpointer dse_item2, GdkEventMotion const *event, DelayedSnapEvent::DelayedSnapEventOrigin const origin) + : _timer_id(0) + , _event(nullptr) + , _item(dse_item) + , _item2(dse_item2) + , _origin(origin) + , _event_context(event_context) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited("/options/snapdelay/value", 0, 0, 1000); + + // We used to have this specified in milliseconds; this has changed to seconds now for consistency's sake + if (value > 1) { // Apparently we have an old preference file, this value must have been in milliseconds; + value = value / 1000.0; // now convert this value to seconds + } + + _timer_id = g_timeout_add(value*1000.0, &sp_event_context_snap_watchdog_callback, this); + _event = gdk_event_copy((GdkEvent*) event); + + ((GdkEventMotion *)_event)->time = GDK_CURRENT_TIME; + } + + ~DelayedSnapEvent() { + if (_timer_id > 0) g_source_remove(_timer_id); // Kill the watchdog + if (_event != nullptr) gdk_event_free(_event); // Remove the copy of the original event + } + + ToolBase* getEventContext() { + return _event_context; + } + + DelayedSnapEventOrigin getOrigin() { + return _origin; + } + + GdkEvent* getEvent() { + return _event; + } + + gpointer getItem() { + return _item; + } + + gpointer getItem2() { + return _item2; + } + +private: + guint _timer_id; + GdkEvent* _event; + gpointer _item; + gpointer _item2; + DelayedSnapEventOrigin _origin; + ToolBase* _event_context; +}; + +void sp_event_context_snap_delay_handler(ToolBase *ec, gpointer const dse_item, gpointer const dse_item2, GdkEventMotion *event, DelayedSnapEvent::DelayedSnapEventOrigin origin); + + +/** + * Base class for Event processors. + * + * This is per desktop object, which (its derivatives) implements + * different actions bound to mouse events. + * + * ToolBase is an abstract base class of all tools. As the name + * indicates, event context implementations process UI events (mouse + * movements and keypresses) and take actions (like creating or modifying + * objects). There is one event context implementation for each tool, + * plus few abstract base classes. Writing a new tool involves + * subclassing ToolBase. + */ +class ToolBase + : public sigc::trackable +{ +public: + void enableSelectionCue (bool enable=true); + void enableGrDrag (bool enable=true); + bool deleteSelectedDrag(bool just_one); + + ToolBase(gchar const *const *cursor_shape, bool uses_snap=true); + + virtual ~ToolBase(); + + ToolBase(const ToolBase&) = delete; + ToolBase& operator=(const ToolBase&) = delete; + + Inkscape::Preferences::Observer *pref_observer; + Glib::RefPtr<Gdk::Cursor> cursor; + + gint xp, yp; ///< where drag started + gint tolerance; + bool _button1on; + bool _button2on; + bool _button3on; + bool within_tolerance; ///< are we still within tolerance of origin + + SPItem *item_to_select; ///< the item where mouse_press occurred, to + ///< be selected if this is a click not drag + + Inkscape::MessageContext *defaultMessageContext() const { + return message_context.get(); + } + + std::unique_ptr<Inkscape::MessageContext> message_context; + + Inkscape::SelCue *_selcue; + + GrDrag *_grdrag; + + GrDrag *get_drag () { + return _grdrag; + } + + ShapeEditor* shape_editor; + + bool space_panning; + bool rotating_mode; + + DelayedSnapEvent *_delayed_snap_event; + bool _dse_callback_in_process; + + virtual void setup(); + virtual void finish(); + + // Is called by our pref_observer if a preference has been changed. + virtual void set(const Inkscape::Preferences::Entry& val); + virtual bool root_handler(GdkEvent *event); + virtual bool item_handler(SPItem *item, GdkEvent *event); + bool block_button(GdkEvent *event); + + virtual const std::string& getPrefsPath() = 0; + + /** + * An observer that relays pref changes to the derived classes. + */ + class ToolPrefObserver: public Inkscape::Preferences::Observer { + public: + ToolPrefObserver(Glib::ustring const &path, ToolBase *ec) + : Inkscape::Preferences::Observer(path) + , ec(ec) + { + } + + void notify(Inkscape::Preferences::Entry const &val) override { + ec->set(val); + } + + private: + ToolBase * const ec; + }; + + SPDesktop const& getDesktop() const; + + +//protected: + void sp_event_context_update_cursor(); + + SPDesktop *desktop; + bool _uses_snap; // TODO: make protected or private +protected: + /// An xpm containing the shape of the tool's cursor. + gchar const *const *cursor_shape; + bool sp_event_context_knot_mouseover() const; + + void set_high_motion_precision(bool high_precision = true); + +private: + bool _keyboardMove(GdkEventKey const &event, Geom::Point const &dir); + void sp_event_context_set_cursor(GdkCursorType cursor_type); +}; + +void sp_event_context_read(ToolBase *ec, gchar const *key); + +gint sp_event_context_root_handler(ToolBase *ec, GdkEvent *event); +gint sp_event_context_virtual_root_handler(ToolBase *ec, GdkEvent *event); +gint sp_event_context_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event); +gint sp_event_context_virtual_item_handler(ToolBase *ec, SPItem *item, GdkEvent *event); + +void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); + +gint gobble_key_events(guint keyval, gint mask); +gint gobble_motion_events(gint mask); + +void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, + gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip); + +void init_latin_keys_group(); +guint get_latin_keyval(GdkEventKey const *event, guint *consumed_modifiers = nullptr); + +SPItem *sp_event_context_find_item (SPDesktop *desktop, Geom::Point const &p, bool select_under, bool into_groups); +SPItem *sp_event_context_over_item (SPDesktop *desktop, SPItem *item, Geom::Point const &p); + +void sp_toggle_dropper(SPDesktop *dt); + +bool sp_event_context_knot_mouseover(ToolBase *ec); + +} // namespace Tools +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SP_EVENT_CONTEXT_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp new file mode 100644 index 0000000..0c5e83b --- /dev/null +++ b/src/ui/tools/tweak-tool.cpp @@ -0,0 +1,1535 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * tweaking paths without node editing + * + * Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <numeric> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glibmm/i18n.h> + +#include <2geom/circle.h> + +#include "context-fns.h" +#include "desktop-events.h" +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "filter-chemistry.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "include/macros.h" +#include "message-context.h" +#include "path-chemistry.h" +#include "selection.h" +#include "splivarot.h" +#include "verbs.h" + +#include "display/canvas-arena.h" +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "livarot/Shape.h" + +#include "object/box3d.h" +#include "object/filters/gaussian-blur.h" +#include "object/sp-flowtext.h" +#include "object/sp-item-transform.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-path.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" +#include "style.h" + +#include "ui/pixmaps/cursor-tweak-attract.xpm" +#include "ui/pixmaps/cursor-tweak-color.xpm" +#include "ui/pixmaps/cursor-tweak-less.xpm" +#include "ui/pixmaps/cursor-tweak-more.xpm" +#include "ui/pixmaps/cursor-tweak-move-in.xpm" +#include "ui/pixmaps/cursor-tweak-move-jitter.xpm" +#include "ui/pixmaps/cursor-tweak-move-out.xpm" +#include "ui/pixmaps/cursor-tweak-move.xpm" +#include "ui/pixmaps/cursor-tweak-push.xpm" +#include "ui/pixmaps/cursor-tweak-repel.xpm" +#include "ui/pixmaps/cursor-tweak-rotate-clockwise.xpm" +#include "ui/pixmaps/cursor-tweak-rotate-counterclockwise.xpm" +#include "ui/pixmaps/cursor-tweak-roughen.xpm" +#include "ui/pixmaps/cursor-tweak-scale-down.xpm" +#include "ui/pixmaps/cursor-tweak-scale-up.xpm" +#include "ui/pixmaps/cursor-tweak-thicken.xpm" +#include "ui/pixmaps/cursor-tweak-thin.xpm" + +#include "svg/svg.h" + +#include "ui/toolbar/tweak-toolbar.h" + +#include "ui/tools/tweak-tool.h" + +using Inkscape::DocumentUndo; + +#define DDC_RED_RGBA 0xff0000ff + +#define DYNA_MIN_WIDTH 1.0e-6 + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& TweakTool::getPrefsPath() { + return TweakTool::prefsPath; +} + +const std::string TweakTool::prefsPath = "/tools/tweak"; + +TweakTool::TweakTool() + : ToolBase(cursor_push_xpm) + , pressure(TC_DEFAULT_PRESSURE) + , dragging(false) + , usepressure(false) + , usetilt(false) + , width(0.2) + , force(0.2) + , fidelity(0) + , mode(0) + , is_drawing(false) + , is_dilating(false) + , has_dilated(false) + , dilate_area(nullptr) + , do_h(true) + , do_s(true) + , do_l(true) + , do_o(false) +{ +} + +TweakTool::~TweakTool() { + this->enableGrDrag(false); + + this->style_set_connection.disconnect(); + + if (this->dilate_area) { + sp_canvas_item_destroy(this->dilate_area); + this->dilate_area = nullptr; + } +} + +static bool is_transform_mode (gint mode) +{ + return (mode == TWEAK_MODE_MOVE || + mode == TWEAK_MODE_MOVE_IN_OUT || + mode == TWEAK_MODE_MOVE_JITTER || + mode == TWEAK_MODE_SCALE || + mode == TWEAK_MODE_ROTATE || + mode == TWEAK_MODE_MORELESS); +} + +static bool is_color_mode (gint mode) +{ + return (mode == TWEAK_MODE_COLORPAINT || mode == TWEAK_MODE_COLORJITTER || mode == TWEAK_MODE_BLUR); +} + +void TweakTool::update_cursor (bool with_shift) { + guint num = 0; + gchar *sel_message = nullptr; + + if (!desktop->selection->isEmpty()) { + num = (guint) boost::distance(desktop->selection->items()); + sel_message = g_strdup_printf(ngettext("<b>%i</b> object selected","<b>%i</b> objects selected",num), num); + } else { + sel_message = g_strdup_printf("%s", _("<b>Nothing</b> selected")); + } + + switch (this->mode) { + case TWEAK_MODE_MOVE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>move</b>."), sel_message); + this->cursor_shape = cursor_tweak_move_xpm; + break; + case TWEAK_MODE_MOVE_IN_OUT: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move in</b>; with Shift to <b>move out</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_tweak_move_out_xpm; + } else { + this->cursor_shape = cursor_tweak_move_in_xpm; + } + break; + case TWEAK_MODE_MOVE_JITTER: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>move randomly</b>."), sel_message); + this->cursor_shape = cursor_tweak_move_jitter_xpm; + break; + case TWEAK_MODE_SCALE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>scale down</b>; with Shift to <b>scale up</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_tweak_scale_up_xpm; + } else { + this->cursor_shape = cursor_tweak_scale_down_xpm; + } + break; + case TWEAK_MODE_ROTATE: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>rotate clockwise</b>; with Shift, <b>counterclockwise</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_tweak_rotate_counterclockwise_xpm; + } else { + this->cursor_shape = cursor_tweak_rotate_clockwise_xpm; + } + break; + case TWEAK_MODE_MORELESS: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>duplicate</b>; with Shift, <b>delete</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_tweak_less_xpm; + } else { + this->cursor_shape = cursor_tweak_more_xpm; + } + break; + case TWEAK_MODE_PUSH: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag to <b>push paths</b>."), sel_message); + this->cursor_shape = cursor_push_xpm; + break; + case TWEAK_MODE_SHRINK_GROW: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>inset paths</b>; with Shift to <b>outset</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_thicken_xpm; + } else { + this->cursor_shape = cursor_thin_xpm; + } + break; + case TWEAK_MODE_ATTRACT_REPEL: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>attract paths</b>; with Shift to <b>repel</b>."), sel_message); + if (with_shift) { + this->cursor_shape = cursor_repel_xpm; + } else { + this->cursor_shape = cursor_attract_xpm; + } + break; + case TWEAK_MODE_ROUGHEN: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>roughen paths</b>."), sel_message); + this->cursor_shape = cursor_roughen_xpm; + break; + case TWEAK_MODE_COLORPAINT: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>paint objects</b> with color."), sel_message); + this->cursor_shape = cursor_color_xpm; + break; + case TWEAK_MODE_COLORJITTER: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>randomize colors</b>."), sel_message); + this->cursor_shape = cursor_color_xpm; + break; + case TWEAK_MODE_BLUR: + this->message_context->setF(Inkscape::NORMAL_MESSAGE, _("%s. Drag or click to <b>increase blur</b>; with Shift to <b>decrease</b>."), sel_message); + this->cursor_shape = cursor_color_xpm; + break; + } + + this->sp_event_context_update_cursor(); + g_free(sel_message); +} + +bool TweakTool::set_style(const SPCSSAttr* css) { + if (this->mode == TWEAK_MODE_COLORPAINT) { // intercept color setting only in this mode + // we cannot store properties with uris + css = sp_css_attr_unset_uris(const_cast<SPCSSAttr *>(css)); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setStyle("/tools/tweak/style", const_cast<SPCSSAttr *>(css)); + return true; + } + + return false; +} + +void TweakTool::setup() { + ToolBase::setup(); + + { + /* TODO: have a look at sp_dyna_draw_context_setup where the same is done.. generalize? at least make it an arcto! */ + Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); + + SPCurve *c = new SPCurve(path); + + this->dilate_area = sp_canvas_bpath_new(this->desktop->getControls(), c); + c->unref(); + sp_canvas_bpath_set_fill(SP_CANVAS_BPATH(this->dilate_area), 0x00000000,(SPWindRule)0); + sp_canvas_bpath_set_stroke(SP_CANVAS_BPATH(this->dilate_area), 0xff9900ff, 1.0, SP_STROKE_LINEJOIN_MITER, SP_STROKE_LINECAP_BUTT); + sp_canvas_item_hide(this->dilate_area); + } + + this->is_drawing = false; + + sp_event_context_read(this, "width"); + sp_event_context_read(this, "mode"); + sp_event_context_read(this, "fidelity"); + sp_event_context_read(this, "force"); + sp_event_context_read(this, "usepressure"); + sp_event_context_read(this, "doh"); + sp_event_context_read(this, "dol"); + sp_event_context_read(this, "dos"); + sp_event_context_read(this, "doo"); + + this->style_set_connection = this->desktop->connectSetStyle( // catch style-setting signal in this tool + //sigc::bind(sigc::ptr_fun(&sp_tweak_context_style_set), this) + sigc::mem_fun(this, &TweakTool::set_style) + ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/tools/tweak/selcue")) { + this->enableSelectionCue(); + } + if (prefs->getBool("/tools/tweak/gradientdrag")) { + this->enableGrDrag(); + } +} + +void TweakTool::set(const Inkscape::Preferences::Entry& val) { + Glib::ustring path = val.getEntryName(); + + if (path == "width") { + this->width = CLAMP(val.getDouble(0.1), -1000.0, 1000.0); + } else if (path == "mode") { + this->mode = val.getInt(); + this->update_cursor(false); + } else if (path == "fidelity") { + this->fidelity = CLAMP(val.getDouble(), 0.0, 1.0); + } else if (path == "force") { + this->force = CLAMP(val.getDouble(1.0), 0, 1.0); + } else if (path == "usepressure") { + this->usepressure = val.getBool(); + } else if (path == "doh") { + this->do_h = val.getBool(); + } else if (path == "dos") { + this->do_s = val.getBool(); + } else if (path == "dol") { + this->do_l = val.getBool(); + } else if (path == "doo") { + this->do_o = val.getBool(); + } +} + +static void +sp_tweak_extinput(TweakTool *tc, GdkEvent *event) +{ + if (gdk_event_get_axis (event, GDK_AXIS_PRESSURE, &tc->pressure)) { + tc->pressure = CLAMP (tc->pressure, TC_MIN_PRESSURE, TC_MAX_PRESSURE); + } else { + tc->pressure = TC_DEFAULT_PRESSURE; + } +} + +static double +get_dilate_radius (TweakTool *tc) +{ + // 10 times the pen width: + return 500 * tc->width/SP_EVENT_CONTEXT(tc)->desktop->current_zoom(); +} + +static double +get_path_force (TweakTool *tc) +{ + double force = 8 * (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE) + /sqrt(SP_EVENT_CONTEXT(tc)->desktop->current_zoom()); + if (force > 3) { + force += 4 * (force - 3); + } + return force * tc->force; +} + +static double +get_move_force (TweakTool *tc) +{ + double force = (tc->usepressure? tc->pressure : TC_DEFAULT_PRESSURE); + return force * tc->force; +} + +static bool +sp_tweak_dilate_recursive (Inkscape::Selection *selection, SPItem *item, Geom::Point p, Geom::Point vector, gint mode, double radius, double force, double fidelity, bool reverse) +{ + bool did = false; + + { + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box && !is_transform_mode(mode) && !is_color_mode(mode)) { + // convert 3D boxes to ordinary groups before tweaking their shapes + item = box3d_convert_to_group(box); + selection->add(item); + } + } + + if (dynamic_cast<SPText *>(item) || dynamic_cast<SPFlowtext *>(item)) { + std::vector<SPItem*> items; + items.push_back(item); + std::vector<SPItem*> selected; + std::vector<Inkscape::XML::Node*> to_select; + SPDocument *doc = item->document; + sp_item_list_to_curves (items, selected, to_select); + SPObject* newObj = doc->getObjectByRepr(to_select[0]); + item = dynamic_cast<SPItem *>(newObj); + g_assert(item != nullptr); + selection->add(item); + } + + if (dynamic_cast<SPGroup *>(item) && !dynamic_cast<SPBox3D *>(item)) { + std::vector<SPItem *> children; + for (auto& child: item->children) { + if (dynamic_cast<SPItem *>(&child)) { + children.push_back(dynamic_cast<SPItem *>(&child)); + } + } + + for (auto i = children.rbegin(); i!= children.rend(); ++i) { + SPItem *child = *i; + g_assert(child != nullptr); + if (sp_tweak_dilate_recursive (selection, child, p, vector, mode, radius, force, fidelity, reverse)) { + did = true; + } + } + } else { + if (mode == TWEAK_MODE_MOVE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * vector; + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MOVE_IN_OUT) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * + (reverse? (a->midpoint() - p) : (p - a->midpoint())); + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MOVE_JITTER) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double dp = g_random_double_range(0, M_PI*2); + double dr = g_random_double_range(0, radius); + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + Geom::Point move = force * 0.5 * (cos(M_PI * x) + 1) * Geom::Point(cos(dp)*dr, sin(dp)*dr); + item->move_rel(Geom::Translate(move * selection->desktop()->doc2dt().withoutTranslation())); + did = true; + } + } + + } else if (mode == TWEAK_MODE_SCALE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double scale = 1 + (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1); + item->scale_rel(Geom::Scale(scale, scale)); + did = true; + } + } + + } else if (mode == TWEAK_MODE_ROTATE) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double angle = (reverse? force : -force) * 0.05 * (cos(M_PI * x) + 1) * M_PI; + angle *= -selection->desktop()->yaxisdir(); + item->rotate_rel(Geom::Rotate(angle)); + did = true; + } + } + + } else if (mode == TWEAK_MODE_MORELESS) { + + Geom::OptRect a = item->documentVisualBounds(); + if (a) { + double x = Geom::L2(a->midpoint() - p)/radius; + if (a->contains(p)) x = 0; + if (x < 1) { + double prob = force * 0.5 * (cos(M_PI * x) + 1); + double chance = g_random_double_range(0, 1); + if (chance <= prob) { + if (reverse) { // delete + item->deleteObject(true, true); + } else { // duplicate + SPDocument *doc = item->document; + Inkscape::XML::Document* xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *old_repr = item->getRepr(); + SPObject *old_obj = doc->getObjectByRepr(old_repr); + Inkscape::XML::Node *parent = old_repr->parent(); + Inkscape::XML::Node *copy = old_repr->duplicate(xml_doc); + parent->appendChild(copy); + SPObject *new_obj = doc->getObjectByRepr(copy); + if (selection->includes(old_obj)) { + selection->add(new_obj); + } + Inkscape::GC::release(copy); + } + did = true; + } + } + } + + } else if (dynamic_cast<SPPath *>(item) || dynamic_cast<SPShape *>(item)) { + + Inkscape::XML::Node *newrepr = nullptr; + gint pos = 0; + Inkscape::XML::Node *parent = nullptr; + char const *id = nullptr; + if (!dynamic_cast<SPPath *>(item)) { + newrepr = sp_selected_item_to_curved_repr(item, 0); + if (!newrepr) { + return false; + } + + // remember the position of the item + pos = item->getRepr()->position(); + // remember parent + parent = item->getRepr()->parent(); + // remember id + id = item->getRepr()->attribute("id"); + } + + // skip those paths whose bboxes are entirely out of reach with our radius + Geom::OptRect bbox = item->documentVisualBounds(); + if (bbox) { + bbox->expandBy(radius); + if (!bbox->contains(p)) { + return false; + } + } + + Path *orig = Path_for_item(item, false); + if (orig == nullptr) { + return false; + } + + Path *res = new Path; + res->SetBackData(false); + + Shape *theShape = new Shape; + Shape *theRes = new Shape; + Geom::Affine i2doc(item->i2doc_affine()); + + orig->ConvertWithBackData((0.08 - (0.07 * fidelity)) / i2doc.descrim()); // default 0.059 + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) { + theRes->ConvertToShape(theShape, fill_nonZero); + } else if (val && strcmp(val, "evenodd") == 0) { + theRes->ConvertToShape(theShape, fill_oddEven); + } else { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + if (Geom::L2(vector) != 0) { + vector = 1/Geom::L2(vector) * vector; + } + + bool did_this = false; + if (mode == TWEAK_MODE_SHRINK_GROW) { + if (theShape->MakeTweak(tweak_mode_grow, theRes, + reverse? force : -force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) // 0 means the shape was actually changed + did_this = true; + } else if (mode == TWEAK_MODE_ATTRACT_REPEL) { + if (theShape->MakeTweak(tweak_mode_repel, theRes, + reverse? force : -force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) + did_this = true; + } else if (mode == TWEAK_MODE_PUSH) { + if (theShape->MakeTweak(tweak_mode_push, theRes, + 1.0, + join_straight, 4.0, + true, p, force*2*vector, radius, &i2doc) == 0) + did_this = true; + } else if (mode == TWEAK_MODE_ROUGHEN) { + if (theShape->MakeTweak(tweak_mode_roughen, theRes, + force, + join_straight, 4.0, + true, p, Geom::Point(0,0), radius, &i2doc) == 0) + did_this = true; + } + + // the rest only makes sense if we actually changed the path + if (did_this) { + theRes->ConvertToShape(theShape, fill_positive); + + res->Reset(); + theRes->ConvertToForme(res); + + double th_max = (0.6 - 0.59*sqrt(fidelity)) / i2doc.descrim(); + double threshold = MAX(th_max, th_max*force); + res->ConvertEvenLines(threshold); + res->Simplify(threshold / (selection->desktop()->current_zoom())); + + if (newrepr) { // converting to path, need to replace the repr + bool is_selected = selection->includes(item); + if (is_selected) { + selection->remove(item); + } + + // It's going to resurrect, so we delete without notifying listeners. + item->deleteObject(false); + + // restore id + newrepr->setAttribute("id", id); + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(newrepr, pos); + + if (is_selected) + selection->add(newrepr); + } + + if (res->descr_cmd.size() > 1) { + gchar *str = res->svg_dump_path(); + if (newrepr) { + newrepr->setAttribute("d", str); + } else { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem && lpeitem->hasPathEffectRecursive()) { + item->setAttribute("inkscape:original-d", str); + } else { + item->setAttribute("d", str); + } + } + g_free(str); + } else { + // TODO: if there's 0 or 1 node left, delete this path altogether + } + + if (newrepr) { + Inkscape::GC::release(newrepr); + newrepr = nullptr; + } + } + + delete theShape; + delete theRes; + delete orig; + delete res; + + if (did_this) { + did = true; + } + } + + } + + return did; +} + + static void +tweak_colorpaint (float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +{ + float rgb_g[3]; + + if (!do_h || !do_s || !do_l) { + float hsl_g[3]; + SPColor::rgb_to_hsl_floatv (hsl_g, SP_RGBA32_R_F(goal), SP_RGBA32_G_F(goal), SP_RGBA32_B_F(goal)); + float hsl_c[3]; + SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); + if (!do_h) { + hsl_g[0] = hsl_c[0]; + } + if (!do_s) { + hsl_g[1] = hsl_c[1]; + } + if (!do_l) { + hsl_g[2] = hsl_c[2]; + } + SPColor::hsl_to_rgb_floatv (rgb_g, hsl_g[0], hsl_g[1], hsl_g[2]); + } else { + rgb_g[0] = SP_RGBA32_R_F(goal); + rgb_g[1] = SP_RGBA32_G_F(goal); + rgb_g[2] = SP_RGBA32_B_F(goal); + } + + for (int i = 0; i < 3; i++) { + double d = rgb_g[i] - color[i]; + color[i] += d * force; + } +} + + static void +tweak_colorjitter (float *color, double force, bool do_h, bool do_s, bool do_l) +{ + float hsl_c[3]; + SPColor::rgb_to_hsl_floatv (hsl_c, color[0], color[1], color[2]); + + if (do_h) { + hsl_c[0] += g_random_double_range(-0.5, 0.5) * force; + if (hsl_c[0] > 1) { + hsl_c[0] -= 1; + } + if (hsl_c[0] < 0) { + hsl_c[0] += 1; + } + } + if (do_s) { + hsl_c[1] += g_random_double_range(-hsl_c[1], 1 - hsl_c[1]) * force; + } + if (do_l) { + hsl_c[2] += g_random_double_range(-hsl_c[2], 1 - hsl_c[2]) * force; + } + + SPColor::hsl_to_rgb_floatv (color, hsl_c[0], hsl_c[1], hsl_c[2]); +} + + static void +tweak_color (guint mode, float *color, guint32 goal, double force, bool do_h, bool do_s, bool do_l) +{ + if (mode == TWEAK_MODE_COLORPAINT) { + tweak_colorpaint (color, goal, force, do_h, do_s, do_l); + } else if (mode == TWEAK_MODE_COLORJITTER) { + tweak_colorjitter (color, force, do_h, do_s, do_l); + } +} + + static void +tweak_opacity (guint mode, SPIScale24 *style_opacity, double opacity_goal, double force) +{ + double opacity = SP_SCALE24_TO_FLOAT (style_opacity->value); + + if (mode == TWEAK_MODE_COLORPAINT) { + double d = opacity_goal - opacity; + opacity += d * force; + } else if (mode == TWEAK_MODE_COLORJITTER) { + opacity += g_random_double_range(-opacity, 1 - opacity) * force; + } + + style_opacity->value = SP_SCALE24_FROM_FLOAT(opacity); +} + + + static double +tweak_profile (double dist, double radius) +{ + if (radius == 0) { + return 0; + } + double x = dist / radius; + double alpha = 1; + if (x >= 1) { + return 0; + } else if (x <= 0) { + return 1; + } else { + return (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5); + } +} + +static void tweak_colors_in_gradient(SPItem *item, Inkscape::PaintTarget fill_or_stroke, + guint32 const rgb_goal, Geom::Point p_w, double radius, double force, guint mode, + bool do_h, bool do_s, bool do_l, bool /*do_o*/) +{ + SPGradient *gradient = getGradient(item, fill_or_stroke); + + if (!gradient || !dynamic_cast<SPGradient *>(gradient)) { + return; + } + + Geom::Affine i2d (item->i2doc_affine ()); + Geom::Point p = p_w * i2d.inverse(); + p *= (gradient->gradientTransform).inverse(); + // now p is in gradient's original coordinates + + SPLinearGradient *lg = dynamic_cast<SPLinearGradient *>(gradient); + SPRadialGradient *rg = dynamic_cast<SPRadialGradient *>(gradient); + if (lg || rg) { + + double pos = 0; + double r = 0; + + if (lg) { + Geom::Point p1(lg->x1.computed, lg->y1.computed); + Geom::Point p2(lg->x2.computed, lg->y2.computed); + Geom::Point pdiff(p2 - p1); + double vl = Geom::L2(pdiff); + + // This is the matrix which moves and rotates the gradient line + // so it's oriented along the X axis: + Geom::Affine norm = Geom::Affine(Geom::Translate(-p1)) * + Geom::Affine(Geom::Rotate(-atan2(pdiff[Geom::Y], pdiff[Geom::X]))); + + // Transform the mouse point by it to find out its projection onto the gradient line: + Geom::Point pnorm = p * norm; + + // Scale its X coordinate to match the length of the gradient line: + pos = pnorm[Geom::X] / vl; + // Calculate radius in length-of-gradient-line units + r = radius / vl; + + } + if (rg) { + Geom::Point c (rg->cx.computed, rg->cy.computed); + pos = Geom::L2(p - c) / rg->r.computed; + r = radius / rg->r.computed; + } + + // Normalize pos to 0..1, taking into account gradient spread: + double pos_e = pos; + if (gradient->getSpread() == SP_GRADIENT_SPREAD_PAD) { + if (pos > 1) { + pos_e = 1; + } + if (pos < 0) { + pos_e = 0; + } + } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REPEAT) { + if (pos > 1 || pos < 0) { + pos_e = pos - floor(pos); + } + } else if (gradient->getSpread() == SP_GRADIENT_SPREAD_REFLECT) { + if (pos > 1 || pos < 0) { + bool odd = ((int)(floor(pos)) % 2 == 1); + pos_e = pos - floor(pos); + if (odd) { + pos_e = 1 - pos_e; + } + } + } + + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary(gradient, false); + + double offset_l = 0; + double offset_h = 0; + SPObject *child_prev = nullptr; + for (auto& child: vector->children) { + SPStop *stop = dynamic_cast<SPStop *>(&child); + if (!stop) { + continue; + } + + offset_h = stop->offset; + + if (child_prev) { + SPStop *prevStop = dynamic_cast<SPStop *>(child_prev); + g_assert(prevStop != nullptr); + + if (offset_h - offset_l > r && pos_e >= offset_l && pos_e <= offset_h) { + // the summit falls in this interstop, and the radius is small, + // so it only affects the ends of this interstop; + // distribute the force between the two endstops so that they + // get all the painting even if they are not touched by the brush + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * (pos_e - offset_l) / (offset_h - offset_l), + do_h, do_s, do_l); + tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + force * (offset_h - pos_e) / (offset_h - offset_l), + do_h, do_s, do_l); + stop->updateRepr(); + child_prev->updateRepr(); + break; + } else { + // wide brush, may affect more than 2 stops, + // paint each stop by the force from the profile curve + if (offset_l <= pos_e && offset_l > pos_e - r) { + tweak_color(mode, prevStop->getColor().v.c, rgb_goal, + force * tweak_profile (fabs (pos_e - offset_l), r), + do_h, do_s, do_l); + child_prev->updateRepr(); + } + + if (offset_h >= pos_e && offset_h < pos_e + r) { + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * tweak_profile (fabs (pos_e - offset_h), r), + do_h, do_s, do_l); + stop->updateRepr(); + } + } + } + + offset_l = offset_h; + child_prev = &child; + } + } else { + // Mesh + SPMeshGradient *mg = dynamic_cast<SPMeshGradient *>(gradient); + if (mg) { + SPMeshGradient *mg_array = dynamic_cast<SPMeshGradient *>(mg->getArray()); + SPMeshNodeArray *array = &(mg_array->array); + // Every third node is a corner node + for( unsigned i=0; i < array->nodes.size(); i+=3 ) { + for( unsigned j=0; j < array->nodes[i].size(); j+=3 ) { + SPStop *stop = array->nodes[i][j]->stop; + double distance = Geom::L2(Geom::Point(p - array->nodes[i][j]->p)); + tweak_color (mode, stop->getColor().v.c, rgb_goal, + force * tweak_profile (distance, radius), do_h, do_s, do_l); + stop->updateRepr(); + } + } + } + } +} + + static bool +sp_tweak_color_recursive (guint mode, SPItem *item, SPItem *item_at_point, + guint32 fill_goal, bool do_fill, + guint32 stroke_goal, bool do_stroke, + float opacity_goal, bool do_opacity, + bool do_blur, bool reverse, + Geom::Point p, double radius, double force, + bool do_h, bool do_s, bool do_l, bool do_o) +{ + bool did = false; + + if (dynamic_cast<SPGroup *>(item)) { + for (auto& child: item->children) { + SPItem *childItem = dynamic_cast<SPItem *>(&child); + if (childItem) { + if (sp_tweak_color_recursive (mode, childItem, item_at_point, + fill_goal, do_fill, + stroke_goal, do_stroke, + opacity_goal, do_opacity, + do_blur, reverse, + p, radius, force, do_h, do_s, do_l, do_o)) { + did = true; + } + } + } + + } else { + SPStyle *style = item->style; + if (!style) { + return false; + } + Geom::OptRect bbox = item->documentGeometricBounds(); + if (!bbox) { + return false; + } + + Geom::Rect brush(p - Geom::Point(radius, radius), p + Geom::Point(radius, radius)); + + Geom::Point center = bbox->midpoint(); + double this_force; + + // if item == item_at_point, use max force + if (item == item_at_point) { + this_force = force; + // else if no overlap of bbox and brush box, skip: + } else if (!bbox->intersects(brush)) { + return false; + //TODO: + // else if object > 1.5 brush: test 4/8/16 points in the brush on hitting the object, choose max + //} else if (bbox->maxExtent() > 3 * radius) { + //} + // else if object > 0.5 brush: test 4 corners of bbox and center on being in the brush, choose max + // else if still smaller, then check only the object center: + } else { + this_force = force * tweak_profile (Geom::L2 (p - center), radius); + } + + if (this_force > 0.002) { + + if (do_blur) { + Geom::OptRect bbox = item->documentGeometricBounds(); + if (!bbox) { + return did; + } + + double blur_now = 0; + Geom::Affine i2dt = item->i2dt_affine (); + if (style->filter.set && style->getFilter()) { + //cycle through filter primitives + for (auto& primitive_obj: style->getFilter()->children) { + SPFilterPrimitive *primitive = dynamic_cast<SPFilterPrimitive *>(&primitive_obj); + if (primitive) { + //if primitive is gaussianblur + SPGaussianBlur * spblur = dynamic_cast<SPGaussianBlur *>(primitive); + if (spblur) { + float num = spblur->stdDeviation.getNumber(); + blur_now += num * i2dt.descrim(); // sum all blurs in the filter + } + } + } + } + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; + blur_now = blur_now / perimeter; + + double blur_new; + if (reverse) { + blur_new = blur_now - 0.06 * force; + } else { + blur_new = blur_now + 0.06 * force; + } + if (blur_new < 0.0005 && blur_new < blur_now) { + blur_new = 0; + } + if (blur_new == 0) { + remove_filter(item, false); + } else { + double radius = blur_new * perimeter; + SPFilter *filter = modify_filter_gaussian_blur_from_item(item->document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } + return true; // do not do colors, blur is a separate mode + } + + if (do_fill) { + if (style->fill.isPaintserver()) { + tweak_colors_in_gradient(item, Inkscape::FOR_FILL, fill_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + did = true; + } else if (style->fill.isColor()) { + tweak_color (mode, style->fill.value.color.v.c, fill_goal, this_force, do_h, do_s, do_l); + item->updateRepr(); + did = true; + } + } + if (do_stroke) { + if (style->stroke.isPaintserver()) { + tweak_colors_in_gradient(item, Inkscape::FOR_STROKE, stroke_goal, p, radius, this_force, mode, do_h, do_s, do_l, do_o); + did = true; + } else if (style->stroke.isColor()) { + tweak_color (mode, style->stroke.value.color.v.c, stroke_goal, this_force, do_h, do_s, do_l); + item->updateRepr(); + did = true; + } + } + if (do_opacity && do_o) { + tweak_opacity (mode, &style->opacity, opacity_goal, this_force); + } + } +} + +return did; +} + + + static bool +sp_tweak_dilate (TweakTool *tc, Geom::Point event_p, Geom::Point p, Geom::Point vector, bool reverse) +{ + Inkscape::Selection *selection = tc->desktop->getSelection(); + SPDesktop *desktop = SP_EVENT_CONTEXT(tc)->desktop; + + if (selection->isEmpty()) { + return false; + } + + bool did = false; + double radius = get_dilate_radius(tc); + + SPItem *item_at_point = SP_EVENT_CONTEXT(tc)->desktop->getItemAtPoint(event_p, TRUE); + + bool do_fill = false, do_stroke = false, do_opacity = false; + guint32 fill_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", true, &do_fill); + guint32 stroke_goal = sp_desktop_get_color_tool(desktop, "/tools/tweak", false, &do_stroke); + double opacity_goal = sp_desktop_get_master_opacity_tool(desktop, "/tools/tweak", &do_opacity); + if (reverse) { +#if 0 + // HSL inversion + float hsv[3]; + float rgb[3]; + SPColor::rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(fill_goal), + SP_RGBA32_G_F(fill_goal), + SP_RGBA32_B_F(fill_goal)); + SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); + fill_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); + SPColor::rgb_to_hsv_floatv (hsv, + SP_RGBA32_R_F(stroke_goal), + SP_RGBA32_G_F(stroke_goal), + SP_RGBA32_B_F(stroke_goal)); + SPColor::hsv_to_rgb_floatv (rgb, hsv[0]<.5? hsv[0]+.5 : hsv[0]-.5, 1 - hsv[1], 1 - hsv[2]); + stroke_goal = SP_RGBA32_F_COMPOSE(rgb[0], rgb[1], rgb[2], 1); +#else + // RGB inversion + fill_goal = SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(fill_goal)), + (255 - SP_RGBA32_G_U(fill_goal)), + (255 - SP_RGBA32_B_U(fill_goal)), + (255 - SP_RGBA32_A_U(fill_goal))); + stroke_goal = SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(stroke_goal)), + (255 - SP_RGBA32_G_U(stroke_goal)), + (255 - SP_RGBA32_B_U(stroke_goal)), + (255 - SP_RGBA32_A_U(stroke_goal))); +#endif + opacity_goal = 1 - opacity_goal; + } + + double path_force = get_path_force(tc); + if (radius == 0 || path_force == 0) { + return false; + } + double move_force = get_move_force(tc); + double color_force = MIN(sqrt(path_force)/20.0, 1); + + // auto items= selection->items(); + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + for(auto item : items){ + if (is_color_mode (tc->mode)) { + if (do_fill || do_stroke || do_opacity) { + if (sp_tweak_color_recursive (tc->mode, item, item_at_point, + fill_goal, do_fill, + stroke_goal, do_stroke, + opacity_goal, do_opacity, + tc->mode == TWEAK_MODE_BLUR, reverse, + p, radius, color_force, tc->do_h, tc->do_s, tc->do_l, tc->do_o)) { + did = true; + } + } + } else if (is_transform_mode(tc->mode)) { + if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, move_force, tc->fidelity, reverse)) { + did = true; + } + } else { + if (sp_tweak_dilate_recursive (selection, item, p, vector, tc->mode, radius, path_force, tc->fidelity, reverse)) { + did = true; + } + } + } + + return did; +} + + static void +sp_tweak_update_area (TweakTool *tc) +{ + double radius = get_dilate_radius(tc); + Geom::Affine const sm (Geom::Scale(radius, radius) * Geom::Translate(SP_EVENT_CONTEXT(tc)->desktop->point())); + sp_canvas_item_affine_absolute(tc->dilate_area, sm); + sp_canvas_item_show(tc->dilate_area); +} + + static void +sp_tweak_switch_mode (TweakTool *tc, gint mode, bool with_shift) +{ + auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("TweakToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Tweak toolbar" << std::endl; + } + + // need to set explicitly, because the prefs may not have changed by the previous + tc->mode = mode; + tc->update_cursor(with_shift); +} + + static void +sp_tweak_switch_mode_temporarily (TweakTool *tc, gint mode, bool with_shift) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Juggling about so that prefs have the old value but tc->mode and the button show new mode: + gint now_mode = prefs->getInt("/tools/tweak/mode", 0); + + auto tb = dynamic_cast<UI::Toolbar::TweakToolbar*>(SP_EVENT_CONTEXT(tc)->desktop->get_toolbar_by_name("TweakToolbar")); + + if(tb) { + tb->set_mode(mode); + } else { + std::cerr << "Could not access Tweak toolbar" << std::endl; + } + + // button has changed prefs, restore + prefs->setInt("/tools/tweak/mode", now_mode); + // changing prefs changed tc->mode, restore back : + tc->mode = mode; + tc->update_cursor(with_shift); +} + +bool TweakTool::root_handler(GdkEvent* event) { + gint ret = FALSE; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + sp_canvas_item_show(this->dilate_area); + break; + case GDK_LEAVE_NOTIFY: + sp_canvas_item_hide(this->dilate_area); + break; + case GDK_BUTTON_PRESS: + if (event->button.button == 1 && !this->space_panning) { + + if (Inkscape::have_viable_layer(desktop, defaultMessageContext()) == false) { + return TRUE; + } + + Geom::Point const button_w(event->button.x, + event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + this->last_push = desktop->dt2doc(button_dt); + + sp_tweak_extinput(this, event); + + desktop->canvas->forceFullRedrawAfterInterruptions(3); + this->is_drawing = true; + this->is_dilating = true; + this->has_dilated = false; + + ret = TRUE; + } + break; + case GDK_MOTION_NOTIFY: + { + Geom::Point const motion_w(event->motion.x, + event->motion.y); + Geom::Point motion_dt(desktop->w2d(motion_w)); + Geom::Point motion_doc(desktop->dt2doc(motion_dt)); + sp_tweak_extinput(this, event); + + // draw the dilating cursor + double radius = get_dilate_radius(this); + Geom::Affine const sm (Geom::Scale(radius, radius) * Geom::Translate(desktop->w2d(motion_w))); + sp_canvas_item_affine_absolute(this->dilate_area, sm); + sp_canvas_item_show(this->dilate_area); + + guint num = 0; + if (!desktop->selection->isEmpty()) { + num = (guint) boost::distance(desktop->selection->items()); + } + if (num == 0) { + this->message_context->flash(Inkscape::ERROR_MESSAGE, _("<b>Nothing selected!</b> Select objects to tweak.")); + } + + // dilating: + if (this->is_drawing && ( event->motion.state & GDK_BUTTON1_MASK )) { + sp_tweak_dilate (this, motion_w, motion_doc, motion_doc - this->last_push, event->button.state & GDK_SHIFT_MASK? true : false); + //this->last_push = motion_doc; + this->has_dilated = true; + // it's slow, so prevent clogging up with events + gobble_motion_events(GDK_BUTTON1_MASK); + return TRUE; + } + + } + break; + case GDK_BUTTON_RELEASE: + { + Geom::Point const motion_w(event->button.x, event->button.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + + desktop->canvas->endForcedFullRedraws(); + this->is_drawing = false; + + if (this->is_dilating && event->button.button == 1 && !this->space_panning) { + if (!this->has_dilated) { + // if we did not rub, do a light tap + this->pressure = 0.03; + sp_tweak_dilate (this, motion_w, desktop->dt2doc(motion_dt), Geom::Point(0,0), MOD__SHIFT(event)); + } + this->is_dilating = false; + this->has_dilated = false; + switch (this->mode) { + case TWEAK_MODE_MOVE: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Move tweak")); + break; + case TWEAK_MODE_MOVE_IN_OUT: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Move in/out tweak")); + break; + case TWEAK_MODE_MOVE_JITTER: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Move jitter tweak")); + break; + case TWEAK_MODE_SCALE: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Scale tweak")); + break; + case TWEAK_MODE_ROTATE: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Rotate tweak")); + break; + case TWEAK_MODE_MORELESS: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Duplicate/delete tweak")); + break; + case TWEAK_MODE_PUSH: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Push path tweak")); + break; + case TWEAK_MODE_SHRINK_GROW: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Shrink/grow path tweak")); + break; + case TWEAK_MODE_ATTRACT_REPEL: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Attract/repel path tweak")); + break; + case TWEAK_MODE_ROUGHEN: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Roughen path tweak")); + break; + case TWEAK_MODE_COLORPAINT: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Color paint tweak")); + break; + case TWEAK_MODE_COLORJITTER: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Color jitter tweak")); + break; + case TWEAK_MODE_BLUR: + DocumentUndo::done(this->desktop->getDocument(), + SP_VERB_CONTEXT_TWEAK, _("Blur tweak")); + break; + } + } + break; + } + case GDK_KEY_PRESS: + { + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_m: + case GDK_KEY_M: + case GDK_KEY_0: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_i: + case GDK_KEY_I: + case GDK_KEY_1: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_IN_OUT, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_z: + case GDK_KEY_Z: + case GDK_KEY_2: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MOVE_JITTER, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_less: + case GDK_KEY_comma: + case GDK_KEY_greater: + case GDK_KEY_period: + case GDK_KEY_3: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_SCALE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_bracketright: + case GDK_KEY_bracketleft: + case GDK_KEY_4: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ROTATE, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_d: + case GDK_KEY_D: + case GDK_KEY_5: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_MORELESS, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_p: + case GDK_KEY_P: + case GDK_KEY_6: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_PUSH, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_s: + case GDK_KEY_S: + case GDK_KEY_7: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_a: + case GDK_KEY_A: + case GDK_KEY_8: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ATTRACT_REPEL, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_r: + case GDK_KEY_R: + case GDK_KEY_9: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_ROUGHEN, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_c: + case GDK_KEY_C: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_COLORPAINT, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_j: + case GDK_KEY_J: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_COLORJITTER, MOD__SHIFT(event)); + ret = TRUE; + } + break; + case GDK_KEY_b: + case GDK_KEY_B: + if (MOD__SHIFT_ONLY(event)) { + sp_tweak_switch_mode(this, TWEAK_MODE_BLUR, MOD__SHIFT(event)); + ret = TRUE; + } + break; + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (!MOD__CTRL_ONLY(event)) { + this->force += 0.05; + if (this->force > 1.0) { + this->force = 1.0; + } + desktop->setToolboxAdjustmentValue ("tweak-force", this->force * 100); + ret = TRUE; + } + break; + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (!MOD__CTRL_ONLY(event)) { + this->force -= 0.05; + if (this->force < 0.0) { + this->force = 0.0; + } + desktop->setToolboxAdjustmentValue ("tweak-force", this->force * 100); + ret = TRUE; + } + break; + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (!MOD__CTRL_ONLY(event)) { + this->width += 0.01; + if (this->width > 1.0) { + this->width = 1.0; + } + desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); // the same spinbutton is for alt+x + sp_tweak_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (!MOD__CTRL_ONLY(event)) { + this->width -= 0.01; + if (this->width < 0.01) { + this->width = 0.01; + } + desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + } + break; + case GDK_KEY_Home: + case GDK_KEY_KP_Home: + this->width = 0.01; + desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + break; + case GDK_KEY_End: + case GDK_KEY_KP_End: + this->width = 1.0; + desktop->setToolboxAdjustmentValue ("tweak-width", this->width * 100); + sp_tweak_update_area(this); + ret = TRUE; + break; + case GDK_KEY_x: + case GDK_KEY_X: + if (MOD__ALT_ONLY(event)) { + desktop->setToolboxFocusTo ("tweak-width"); + ret = TRUE; + } + break; + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(true); + break; + + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_tweak_switch_mode_temporarily(this, TWEAK_MODE_SHRINK_GROW, MOD__SHIFT(event)); + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + } + case GDK_KEY_RELEASE: { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (get_latin_keyval(&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->update_cursor(false); + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event)); + this->message_context->clear(); + break; + default: + sp_tweak_switch_mode (this, prefs->getInt("/tools/tweak/mode"), MOD__SHIFT(event)); + break; + } + } + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/tweak-tool.h b/src/ui/tools/tweak-tool.h new file mode 100644 index 0000000..677cc0b --- /dev/null +++ b/src/ui/tools/tweak-tool.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_TWEAK_CONTEXT_H__ +#define __SP_TWEAK_CONTEXT_H__ + +/* + * tweaking paths without node editing + * + * Authors: + * bulia byak + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" +#include <2geom/point.h> + +#define SAMPLING_SIZE 8 /* fixme: ?? */ + +#define TC_MIN_PRESSURE 0.0 +#define TC_MAX_PRESSURE 1.0 +#define TC_DEFAULT_PRESSURE 0.35 + +namespace Inkscape { +namespace UI { +namespace Tools { + +enum { + TWEAK_MODE_MOVE, + TWEAK_MODE_MOVE_IN_OUT, + TWEAK_MODE_MOVE_JITTER, + TWEAK_MODE_SCALE, + TWEAK_MODE_ROTATE, + TWEAK_MODE_MORELESS, + TWEAK_MODE_PUSH, + TWEAK_MODE_SHRINK_GROW, + TWEAK_MODE_ATTRACT_REPEL, + TWEAK_MODE_ROUGHEN, + TWEAK_MODE_COLORPAINT, + TWEAK_MODE_COLORJITTER, + TWEAK_MODE_BLUR +}; + +class TweakTool : public ToolBase { +public: + TweakTool(); + ~TweakTool() override; + + /* extended input data */ + gdouble pressure; + + /* attributes */ + bool dragging; /* mouse state: mouse is dragging */ + bool usepressure; + bool usetilt; + + double width; + double force; + double fidelity; + + gint mode; + + bool is_drawing; + + bool is_dilating; + bool has_dilated; + Geom::Point last_push; + SPCanvasItem *dilate_area; + + bool do_h; + bool do_s; + bool do_l; + bool do_o; + + sigc::connection style_set_connection; + + static const std::string prefsPath; + + void setup() override; + void set(const Inkscape::Preferences::Entry& val) override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + + void update_cursor(bool with_shift); + +private: + bool set_style(const SPCSSAttr* css); +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/tools/zoom-tool.cpp b/src/ui/tools/zoom-tool.cpp new file mode 100644 index 0000000..0372e8d --- /dev/null +++ b/src/ui/tools/zoom-tool.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Handy zooming tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <gdk/gdkkeysyms.h> + +#include "include/macros.h" +#include "rubberband.h" +#include "display/sp-canvas-item.h" +#include "display/sp-canvas-util.h" +#include "desktop.h" +#include "ui/pixmaps/cursor-zoom.xpm" +#include "ui/pixmaps/cursor-zoom-out.xpm" +#include "selection-chemistry.h" + +#include "ui/tools/zoom-tool.h" + +namespace Inkscape { +namespace UI { +namespace Tools { + +const std::string& ZoomTool::getPrefsPath() { + return ZoomTool::prefsPath; +} + +const std::string ZoomTool::prefsPath = "/tools/zoom"; + +ZoomTool::ZoomTool() + : ToolBase(cursor_zoom_xpm) + , grabbed(nullptr) + , escaped(false) +{ +} + +ZoomTool::~ZoomTool() = default; + +void ZoomTool::finish() { + this->enableGrDrag(false); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + ToolBase::finish(); +} + +void ZoomTool::setup() { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/tools/zoom/selcue")) { + this->enableSelectionCue(); + } + + if (prefs->getBool("/tools/zoom/gradientdrag")) { + this->enableGrDrag(); + } + + ToolBase::setup(); +} + +bool ZoomTool::root_handler(GdkEvent* event) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + double const zoom_inc = prefs->getDoubleLimited("/options/zoomincrement/value", M_SQRT2, 1.01, 10); + + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + + if (event->button.button == 1 && !this->space_panning) { + // save drag origin + xp = (gint) event->button.x; + yp = (gint) event->button.y; + within_tolerance = true; + + Inkscape::Rubberband::get(desktop)->start(desktop, button_dt); + + escaped = false; + + ret = true; + } else if (event->button.button == 3) { + double const zoom_rel( (event->button.state & GDK_SHIFT_MASK) + ? zoom_inc + : 1 / zoom_inc ); + + desktop->zoom_relative_keep_point(button_dt, zoom_rel); + ret = true; + } + + sp_canvas_item_grab(SP_CANVAS_ITEM(desktop->acetate), + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK, + nullptr, event->button.time); + + this->grabbed = SP_CANVAS_ITEM(desktop->acetate); + break; + } + + case GDK_MOTION_NOTIFY: + if ((event->motion.state & GDK_BUTTON1_MASK) && !this->space_panning) { + ret = true; + + if ( within_tolerance + && ( abs( (gint) event->motion.x - xp ) < tolerance ) + && ( abs( (gint) event->motion.y - yp ) < tolerance ) ) { + break; // do not drag if we're within tolerance from origin + } + // Once the user has moved farther than tolerance from the original location + // (indicating they intend to move the object, not click), then always process the + // motion notify coordinates as given (no snapping back to origin) + within_tolerance = false; + + Geom::Point const motion_w(event->motion.x, event->motion.y); + Geom::Point const motion_dt(desktop->w2d(motion_w)); + Inkscape::Rubberband::get(desktop)->move(motion_dt); + gobble_motion_events(GDK_BUTTON1_MASK); + } + break; + + case GDK_BUTTON_RELEASE: + { + Geom::Point const button_w(event->button.x, event->button.y); + Geom::Point const button_dt(desktop->w2d(button_w)); + + if ( event->button.button == 1 && !this->space_panning) { + Geom::OptRect const b = Inkscape::Rubberband::get(desktop)->getRectangle(); + + if (b && !within_tolerance && !(GDK_SHIFT_MASK & event->button.state) ) { + desktop->set_display_area(*b, 10); + } else if (!escaped) { + double const zoom_rel( (event->button.state & GDK_SHIFT_MASK) + ? 1 / zoom_inc + : zoom_inc ); + + desktop->zoom_relative_keep_point(button_dt, zoom_rel); + } + + ret = true; + } + + Inkscape::Rubberband::get(desktop)->stop(); + + if (this->grabbed) { + sp_canvas_item_ungrab(this->grabbed); + this->grabbed = nullptr; + } + + xp = yp = 0; + escaped = false; + break; + } + case GDK_KEY_PRESS: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Escape: + if (!Inkscape::Rubberband::get(desktop)->is_started()) { + Inkscape::SelectionHelper::selectNone(desktop); + } + + Inkscape::Rubberband::get(desktop)->stop(); + xp = yp = 0; + escaped = true; + ret = true; + break; + + case GDK_KEY_Up: + case GDK_KEY_Down: + case GDK_KEY_KP_Up: + case GDK_KEY_KP_Down: + // prevent the zoom field from activation + if (!MOD__CTRL_ONLY(event)) + ret = true; + break; + + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->cursor_shape = cursor_zoom_out_xpm; + this->sp_event_context_update_cursor(); + break; + + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + ret = this->deleteSelectedDrag(MOD__CTRL_ONLY(event)); + break; + + default: + break; + } + break; + case GDK_KEY_RELEASE: + switch (get_latin_keyval (&event->key)) { + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + this->cursor_shape = cursor_zoom_xpm; + this->sp_event_context_update_cursor(); + break; + default: + break; + } + break; + default: + break; + } + + if (!ret) { + ret = ToolBase::root_handler(event); + } + + return ret; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/tools/zoom-tool.h b/src/ui/tools/zoom-tool.h new file mode 100644 index 0000000..003b6be --- /dev/null +++ b/src/ui/tools/zoom-tool.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_ZOOM_CONTEXT_H__ +#define __SP_ZOOM_CONTEXT_H__ + +/* + * Handy zooming tool + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * + * Copyright (C) 1999-2002 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/tools/tool-base.h" + +#define SP_ZOOM_CONTEXT(obj) (dynamic_cast<Inkscape::UI::Tools::ZoomTool*>((Inkscape::UI::Tools::ToolBase*)obj)) +#define SP_IS_ZOOM_CONTEXT(obj) (dynamic_cast<const Inkscape::UI::Tools::ZoomTool*>((const Inkscape::UI::Tools::ToolBase*)obj) != NULL) + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ZoomTool : public ToolBase { +public: + ZoomTool(); + ~ZoomTool() override; + + static const std::string prefsPath; + + void setup() override; + void finish() override; + bool root_handler(GdkEvent* event) override; + + const std::string& getPrefsPath() override; + +private: + SPCanvasItem *grabbed; + bool escaped; +}; + +} +} +} + +#endif diff --git a/src/ui/util.cpp b/src/ui/util.cpp new file mode 100644 index 0000000..1832cf7 --- /dev/null +++ b/src/ui/util.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Utility functions for UI + * + * Authors: + * Tavmjong Bah + * John Smith + * + * Copyright (C) 2004, 2013, 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "util.h" + +/* + * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text" + * Text should be > length 8 or just return the original text + */ +Glib::ustring ink_ellipsize_text(Glib::ustring const &src, size_t maxlen) +{ + if (src.length() > maxlen && maxlen > 8) { + size_t p1 = (size_t) maxlen / 2; + size_t p2 = (size_t) src.length() - (maxlen - p1 - 1); + return src.substr(0, p1) + "…" + src.substr(p2); + } + return src; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/util.h b/src/ui/util.h new file mode 100644 index 0000000..9480371 --- /dev/null +++ b/src/ui/util.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Utility functions for UI + * + * Authors: + * Tavmjong Bah + * John Smith + * + * Copyright (C) 2013, 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef UI_UTIL_SEEN +#define UI_UTIL_SEEN + +#include <glibmm/ustring.h> + +Glib::ustring ink_ellipsize_text (Glib::ustring const &src, size_t maxlen); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/uxmanager.cpp b/src/ui/uxmanager.cpp new file mode 100644 index 0000000..1f62ece --- /dev/null +++ b/src/ui/uxmanager.cpp @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Desktop widget implementation. + */ +/* Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "widgets/desktop-widget.h" + +#include "uxmanager.h" +#include "desktop.h" +#include "ui/monitor.h" +#include "util/ege-tags.h" +#include "widgets/toolbox.h" + +class TrackItem +{ +public: + TrackItem() : + destroyConn(), + boxes() + {} + + sigc::connection destroyConn; + std::vector<GtkWidget*> boxes; +}; + +static std::vector<SPDesktop*> desktops; +static std::vector<SPDesktopWidget*> dtws; +static std::map<SPDesktop*, TrackItem> trackedBoxes; + + +namespace { + +void desktopDestructHandler(SPDesktop *desktop) +{ + std::map<SPDesktop*, TrackItem>::iterator it = trackedBoxes.find(desktop); + if (it != trackedBoxes.end()) + { + trackedBoxes.erase(it); + } +} + + +// TODO unify this later: +static Glib::ustring getLayoutPrefPath( Inkscape::UI::View::View *view ) +{ + Glib::ustring prefPath; + + if (reinterpret_cast<SPDesktop*>(view)->is_focusMode()) { + prefPath = "/focus/"; + } else if (reinterpret_cast<SPDesktop*>(view)->is_fullscreen()) { + prefPath = "/fullscreen/"; + } else { + prefPath = "/window/"; + } + + return prefPath; +} + +} + +namespace Inkscape { +namespace UI { + +UXManager* instance = nullptr; + +class UXManagerImpl : public UXManager +{ +public: + UXManagerImpl(); + ~UXManagerImpl() override; + + void addTrack( SPDesktopWidget* dtw ) override; + void delTrack( SPDesktopWidget* dtw ) override; + + void connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) override; + + gint getDefaultTask( SPDesktop *desktop ) override; + void setTask(SPDesktop* dt, gint val) override; + + bool isWidescreen() const override; + +private: + bool _widescreen; +}; + +UXManager* UXManager::getInstance() +{ + if (!instance) { + instance = new UXManagerImpl(); + } + return instance; +} + + +UXManager::UXManager() += default; + +UXManager::~UXManager() += default; + +UXManagerImpl::UXManagerImpl() : + _widescreen(false) +{ + ege::TagSet tags; + tags.setLang("en"); + + tags.addTag(ege::Tag("General")); + tags.addTag(ege::Tag("Icons")); + + // Figure out if we're on a widescreen display + Gdk::Rectangle monitor_geometry = Inkscape::UI::get_monitor_geometry_primary(); + int const width = monitor_geometry.get_width(); + int const height = monitor_geometry.get_height(); + + if (width && height) { + gdouble aspect = static_cast<gdouble>(width) / static_cast<gdouble>(height); + if (aspect > 1.65) { + _widescreen = true; + } + } +} + +UXManagerImpl::~UXManagerImpl() += default; + +bool UXManagerImpl::isWidescreen() const +{ + return _widescreen; +} + +gint UXManagerImpl::getDefaultTask( SPDesktop *desktop ) +{ + gint taskNum = isWidescreen() ? 2 : 0; + + Glib::ustring prefPath = getLayoutPrefPath( desktop ); + taskNum = Inkscape::Preferences::get()->getInt( prefPath + "task/taskset", taskNum ); + taskNum = (taskNum < 0) ? 0 : (taskNum > 2) ? 2 : taskNum; + + return taskNum; +} + +void UXManagerImpl::setTask(SPDesktop* dt, gint val) +{ + for (auto dtw : dtws) { + gboolean notDone = Inkscape::Preferences::get()->getBool("/options/workarounds/dynamicnotdone", false); + + if (dtw->desktop == dt) { + int taskNum = val; + switch (val) { + default: + case 0: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_TOP); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP); + } + dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT); + taskNum = val; // in case it was out of range + break; + case 1: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_TOP); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_TOP); + } + dtw->setToolboxPosition("SnapToolbar", GTK_POS_TOP); + break; + case 2: + dtw->setToolboxPosition("ToolToolbar", GTK_POS_LEFT); + dtw->setToolboxPosition("CommandsToolbar", GTK_POS_RIGHT); + dtw->setToolboxPosition("SnapToolbar", GTK_POS_RIGHT); + if (notDone) { + dtw->setToolboxPosition("AuxToolbar", GTK_POS_RIGHT); + } + } + Glib::ustring prefPath = getLayoutPrefPath( dtw->desktop ); + Inkscape::Preferences::get()->setInt( prefPath + "task/taskset", taskNum ); + } + } +} + + +void UXManagerImpl::addTrack( SPDesktopWidget* dtw ) +{ + if (std::find(dtws.begin(), dtws.end(), dtw) == dtws.end()) { + dtws.push_back(dtw); + } +} + +void UXManagerImpl::delTrack( SPDesktopWidget* dtw ) +{ + std::vector<SPDesktopWidget*>::iterator iter = std::find(dtws.begin(), dtws.end(), dtw); + if (iter != dtws.end()) { + dtws.erase(iter); + } +} + +void UXManagerImpl::connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) +{ + if (!desktop) + { + return; + } + TrackItem &tracker = trackedBoxes[desktop]; + std::vector<GtkWidget*>& tracked = tracker.boxes; + tracker.destroyConn = desktop->connectDestroy(&desktopDestructHandler); + + for (auto toolbox : toolboxes) { + ToolboxFactory::setToolboxDesktop( toolbox, desktop ); + if (find(tracked.begin(), tracked.end(), toolbox) == tracked.end()) { + tracked.push_back(toolbox); + } + } + + if (std::find(desktops.begin(), desktops.end(), desktop) == desktops.end()) { + desktops.push_back(desktop); + } + + gint taskNum = getDefaultTask( desktop ); + + // note: this will change once more options are in the task set support: + Inkscape::UI::UXManager::getInstance()->setTask( desktop, taskNum ); +} + + +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/uxmanager.h b/src/ui/uxmanager.h new file mode 100644 index 0000000..5cb799c --- /dev/null +++ b/src/ui/uxmanager.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_UXMANAGER_H +#define SEEN_UI_UXMANAGER_H +/* + * A simple interface for previewing representations. + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include <glib.h> + +extern "C" typedef struct _GtkWidget GtkWidget; +class SPDesktop; +struct SPDesktopWidget; + +namespace Inkscape { +namespace UI { + +class UXManager +{ +public: + static UXManager* getInstance(); + virtual ~UXManager(); + + virtual void addTrack( SPDesktopWidget* dtw ) = 0; + virtual void delTrack( SPDesktopWidget* dtw ) = 0; + + virtual void connectToDesktop( std::vector<GtkWidget *> const & toolboxes, SPDesktop *desktop ) = 0; + + virtual gint getDefaultTask( SPDesktop *desktop ) = 0; + virtual void setTask( SPDesktop* dt, gint val ) = 0; + + virtual bool isWidescreen() const = 0; + + UXManager( UXManager const & ) = delete; + UXManager & operator=( UXManager const & ) = delete; + +protected: + UXManager(); +}; + +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_UI_UXMANAGER_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/view/README b/src/ui/view/README new file mode 100644 index 0000000..9316f8b --- /dev/null +++ b/src/ui/view/README @@ -0,0 +1,51 @@ + +This directory contains the class Inkscape::UI::View::View and related items. + +View is an abstract base class for all UI document views. Documents +can be displayed by more than one window, each having its own view +(e.g. zoom level, selection, etc.). + +View is the base class for: + +* SPDesktop +* SVGView REMOVED + +SPViewWidget is the base for: + +* SPDocumentWidget +* SPSVGViewWidget REMOVED + +SPSVGViewWidget has been replaced by SVGViewWidget, see below. + + +SPViewWidget: + Contains a GtkEventBox and holds a View. + +SPDesktopWidget: + Contains: + VBox + HBox + GtkGrid + GtkPaned + GtkGrid + SPCanvas + Plus lots of other junk. + + +SVGViewWidget: + Used many places as a convenient way to show an SVG (file dialog, Inkview). + Derived, rather uselessly, from Gtk::Scrollbar. + It no longer is dependent on View (and really doesn't belong here anymore). + + It contains: SPCanvas + +To do: + + +* Convert everything to C++. +* Evaluate moving SPDesktopWidget down the widget stack. + It doesn't use the EventBox of SPViewWidget! + +A DesktopViewWidget should contain: + DesktopView (aka SPDesktop) + SPCanvas diff --git a/src/ui/view/edit-widget-interface.h b/src/ui/view/edit-widget-interface.h new file mode 100644 index 0000000..eb3e33b --- /dev/null +++ b/src/ui/view/edit-widget-interface.h @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * John Bintz <jcoswell@coswellproductions.org> + * + * Copyright (C) 2006 John Bintz + * Copyright (C) 2005 Ralf Stephan + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_H +#define INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_H + +#include "message.h" +#include <2geom/point.h> + +namespace Gtk { + class Toolbar; + class Window; +} + +namespace Glib { +class ustring; +} + +namespace Inkscape { namespace UI { namespace Widget { class Dock; } } } + +namespace Inkscape { +namespace UI { +namespace View { + +/** + * Abstract base class for all EditWidget implementations. + */ +struct EditWidgetInterface +{ + EditWidgetInterface() = default; + virtual ~EditWidgetInterface() = default; + + /// Returns pointer to window UI object as void* + virtual Gtk::Window *getWindow() = 0; + + /// Set the widget's title + virtual void setTitle (gchar const*) = 0; + + /// Show all parts of widget the user wants to see. + virtual void layout() = 0; + + /// Present widget to user + virtual void present() = 0; + + /// Returns geometry of widget + virtual void getGeometry (gint &x, gint &y, gint &w, gint &h) = 0; + + /// Change the widget's size + virtual void setSize (gint w, gint h) = 0; + + /// Move widget to specified position + virtual void setPosition (Geom::Point p) = 0; + + /// Transientize widget + virtual void setTransient (void*, int) = 0; + + /// Return mouse position in widget + virtual Geom::Point getPointer() = 0; + + /// Make widget iconified + virtual void setIconified() = 0; + + /// Make widget maximized on screen + virtual void setMaximized() = 0; + + /// Make widget fill screen and show it if possible. + virtual void setFullscreen() = 0; + + /// Shuts down the desktop object for the view being closed. It checks + /// to see if the document has been edited, and if so prompts the user + /// to save, discard, or cancel. Returns TRUE if the shutdown operation + /// is cancelled or if the save is cancelled or fails, FALSE otherwise. + virtual bool shutdown() = 0; + + /// Destroy and delete widget. + virtual void destroy() = 0; + + + /// Store window position to prefs + virtual void storeDesktopPosition() = 0; + + /// Queue a redraw request with the canvas + virtual void requestCanvasUpdate() = 0; + + /// Force a redraw of the canvas + virtual void requestCanvasUpdateAndWait() = 0; + + /// Enable interaction on this desktop + virtual void enableInteraction() = 0; + + /// Disable interaction on this desktop + virtual void disableInteraction() = 0; + + /// Update the "active desktop" indicator + virtual void activateDesktop() = 0; + + /// Update the "inactive desktop" indicator + virtual void deactivateDesktop() = 0; + + /// Update rulers from current values + virtual void updateRulers() = 0; + + /// Update scrollbars from current values + virtual void updateScrollbars (double scale) = 0; + + /// Toggle rulers on/off and set preference value accordingly + virtual void toggleRulers() = 0; + + /// Toggle scrollbars on/off and set preference value accordingly + virtual void toggleScrollbars() = 0; + + /// Toggle CMS on/off and set preference value accordingly + virtual void toggleColorProfAdjust() = 0; + + /// Is CMS on/off + virtual bool colorProfAdjustEnabled() = 0; + + /// Temporarily block signals and update zoom display + virtual void updateZoom() = 0; + + /// The zoom display will get the keyboard focus. + virtual void letZoomGrabFocus() = 0; + + /// Temporarily block signals and update rotation display + virtual void updateRotation() = 0; + + virtual Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring&) = 0; + + /// In auxiliary toolbox, set focus to widget having specific id + virtual void setToolboxFocusTo (const gchar *) = 0; + + /// In auxiliary toolbox, set value of adjustment with specific id + virtual void setToolboxAdjustmentValue (const gchar *, double) = 0; + + /// In auxiliary toolbox, return true if specific togglebutton is active + virtual bool isToolboxButtonActive (gchar const*) = 0; + + /// Set the coordinate display + virtual void setCoordinateStatus (Geom::Point p) = 0; + + /// Message widget will get no content + virtual void setMessage (Inkscape::MessageType type, gchar const* msg) = 0; + + + /** Show an info dialog with the given message */ + virtual bool showInfoDialog( Glib::ustring const &message ) = 0; + + /// Open yes/no dialog with warning text and confirmation question. + virtual bool warnDialog (Glib::ustring const &) = 0; + + virtual Inkscape::UI::Widget::Dock* getDock () = 0; +}; + +} // namespace View +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_VIEW_EDIT_WIDGET_IFACE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/view/svg-view-widget.cpp b/src/ui/view/svg-view-widget.cpp new file mode 100644 index 0000000..c349b5a --- /dev/null +++ b/src/ui/view/svg-view-widget.cpp @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A light-weight widget containing an SPCanvas for rendering an SVG. + */ +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Includes code moved from svg-view.cpp authored by: + * MenTaLGuy + * Ralf Stephan <ralf@ark.in-berlin.de> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#include <iostream> + +#include "svg-view-widget.h" + +#include "document.h" + +#include "2geom/transforms.h" + +#include "display/sp-canvas.h" +#include "display/sp-canvas-group.h" +#include "display/sp-canvas-item.h" +#include "display/canvas-arena.h" + +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { +namespace UI { +namespace View { + +/** + * Callback connected with arena_event. + */ +// This hasn't worked since at least 0.48. It should result in a cursor change over <a></a> links. +// There should be a better way of doing this. See note in canvas-arena.cpp. +static gint arena_handler(SPCanvasArena */*arena*/, Inkscape::DrawingItem *ai, + GdkEvent *event, SVGViewWidget *svgview) +{ + static gdouble x, y; + static gboolean active = FALSE; + SPEvent spev; + + SPItem *spitem = (ai) ? ai->getItem() : nullptr; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 1) { + active = TRUE; + x = event->button.x; + y = event->button.y; + } + break; + case GDK_BUTTON_RELEASE: + if (event->button.button == 1) { + if (active && (event->button.x == x) && + (event->button.y == y)) { + spev.type = SPEvent::ACTIVATE; + if ( spitem != nullptr ) + { + spitem->emitEvent (spev); + } + } + } + active = FALSE; + break; + case GDK_MOTION_NOTIFY: + active = FALSE; + break; + case GDK_ENTER_NOTIFY: + spev.type = SPEvent::MOUSEOVER; + spev.view = svgview; + if ( spitem != nullptr ) + { + spitem->emitEvent (spev); + } + break; + case GDK_LEAVE_NOTIFY: + spev.type = SPEvent::MOUSEOUT; + spev.view = svgview; + if ( spitem != nullptr ) + { + spitem->emitEvent (spev); + } + break; + default: + break; + } + + return TRUE; +} + + +/** + * A light-weight widget containing an SPCanvas for rendering an SVG. + * It's derived from a Gtk::ScrolledWindow like the previous C version, but that doesn't seem to be + * too useful. + */ +SVGViewWidget::SVGViewWidget(SPDocument* document) + : _document(nullptr) + , _dkey(0) + , _parent(nullptr) + , _drawing(nullptr) + , _hscale(1.0) + , _vscale(1.0) + , _rescale(false) + , _keepaspect(false) + , _width(0.0) + , _height(0.0) +{ + _canvas = SPCanvas::createAA(); + add(*Glib::wrap(_canvas)); + + SPCanvasItem* item = + sp_canvas_item_new(SP_CANVAS(_canvas)->getRoot(), SP_TYPE_CANVAS_GROUP, nullptr); + _parent = SP_CANVAS_GROUP(item); + + _drawing = sp_canvas_item_new (_parent, SP_TYPE_CANVAS_ARENA, nullptr); + g_signal_connect (G_OBJECT (_drawing), "arena_event", G_CALLBACK (arena_handler), this); + + setDocument(document); + + signal_size_allocate().connect(sigc::mem_fun(*this, &SVGViewWidget::size_allocate)); +} + +SVGViewWidget::~SVGViewWidget() +{ + if (_document) { + _document = nullptr; + } +} + +void +SVGViewWidget::setDocument(SPDocument* document) +{ + // Clear old document + if (_document) { + _document->getRoot()->invoke_hide(_dkey); // Removed from display tree + } + + // Add new document + if (document) { + _document = document; + + Inkscape::DrawingItem *ai = document->getRoot()->invoke_show( + SP_CANVAS_ARENA (_drawing)->drawing, + _dkey, + SP_ITEM_SHOW_DISPLAY); + + if (ai) { + SP_CANVAS_ARENA (_drawing)->drawing.root()->prependChild(ai); + } + + doRescale (); + } +} + +void +SVGViewWidget::setResize(int width, int height) +{ + // Triggers size_allocation which calls SVGViewWidget::size_allocate. + set_size_request(width, height); + queue_resize(); +} + +void +SVGViewWidget::size_allocate(Gtk::Allocation& allocation) +{ + double width = allocation.get_width(); + double height = allocation.get_height(); + + if (width < 0.0 || height < 0.0) { + std::cerr << "SVGViewWidget::size_allocate: negative dimensions!" << std::endl; + return; + } + + _rescale = true; + _keepaspect = true; + _width = width; + _height = height; + + doRescale (); +} + +void +SVGViewWidget::doRescale() +{ + if (!_document) { + std::cerr << "SVGViewWidget::doRescale: No document!" << std::endl; + return; + } + + if (_document->getWidth().value("px") < 1e-9) { + std::cerr << "SVGViewWidget::doRescale: Width too small!" << std::endl; + return; + } + + if (_document->getHeight().value("px") < 1e-9) { + std::cerr << "SVGViewWidget::doRescale: Height too small!" << std::endl; + return; + } + + double x_offset = 0.0; + double y_offset = 0.0; + if (_rescale) { + _hscale = _width / _document->getWidth().value("px"); + _vscale = _height / _document->getHeight().value("px"); + if (_keepaspect) { + if (_hscale > _vscale) { + _hscale = _vscale; + x_offset = (_width - _document->getWidth().value("px") * _vscale)/2.0; + } else { + _vscale = _hscale; + y_offset = (_height - _document->getHeight().value("px") * _hscale)/2.0; + } + } + } + + if (_drawing) { + sp_canvas_item_affine_absolute (_drawing, Geom::Scale(_hscale, _vscale) * Geom::Translate(x_offset, y_offset)); + } +} + +void +SVGViewWidget::mouseover() +{ + GdkDisplay *display = gdk_display_get_default(); + GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_HAND2); + GdkWindow *window = gtk_widget_get_window (GTK_WIDGET(SP_CANVAS_ITEM(_drawing)->canvas)); + gdk_window_set_cursor(window, cursor); + g_object_unref(cursor); +} + +void +SVGViewWidget::mouseout() +{ + GdkWindow *window = gtk_widget_get_window (GTK_WIDGET(SP_CANVAS_ITEM(_drawing)->canvas)); + gdk_window_set_cursor(window, nullptr); +} + +} // Namespace View +} // Namespace UI +} // Namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/view/svg-view-widget.h b/src/ui/view/svg-view-widget.h new file mode 100644 index 0000000..644c879 --- /dev/null +++ b/src/ui/view/svg-view-widget.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A light-weight widget containing an SPCanvas with for rendering an SVG. + */ +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Authors + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * Read the file 'COPYING' for more information. + * + */ + +#ifndef INKSCAPE_UI_SVG_VIEW_WIDGET_VARIATIONS_H +#define INKSCAPE_UI_SVG_VIEW_WIDGET_VARIATIONS_H + + +#include <gtkmm.h> + +class SPDocument; +class SPCanvasGroup; +class SPCanvasItem; + +namespace Inkscape { +namespace UI { +namespace View { + +/** + * A light-weight widget containing an SPCanvas for rendering an SVG. + */ +class SVGViewWidget : public Gtk::ScrolledWindow { + +public: + SVGViewWidget(SPDocument* document); + ~SVGViewWidget() override; + void setDocument( SPDocument* document); + void setResize( int width, int height); + +private: + void size_allocate(Gtk::Allocation& allocation); + + GtkWidget* _canvas; + +// From SVGView --------------------------------- + +public: + SPDocument* _document; + unsigned int _dkey; + SPCanvasGroup *_parent; + SPCanvasItem *_drawing; + double _hscale; ///< horizontal scale + double _vscale; ///< vertical scale + bool _rescale; ///< whether to rescale automatically + bool _keepaspect; + double _width; + double _height; + + /** + * Helper function that sets rescale ratio. + */ + void doRescale(); + + /** + * Change cursor (used for links). + */ + void mouseover(); + void mouseout(); + +}; + +} // Namespace View +} // Namespace UI +} // Namespace Inkscape + +#endif // INKSCAPE_UI_SVG_VIEW_WIDGET + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/view/view-widget.cpp b/src/ui/view/view-widget.cpp new file mode 100644 index 0000000..ce9745d --- /dev/null +++ b/src/ui/view/view-widget.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "view.h" +#include "view-widget.h" + +//using namespace Inkscape::UI::View; + +// SPViewWidget +static void sp_view_widget_dispose(GObject *object); + +G_DEFINE_TYPE(SPViewWidget, sp_view_widget, GTK_TYPE_EVENT_BOX); + +/** + * Callback to initialize the SPViewWidget vtable. + */ +static void sp_view_widget_class_init(SPViewWidgetClass *vwc) +{ + GObjectClass *object_class = G_OBJECT_CLASS(vwc); + + object_class->dispose = sp_view_widget_dispose; +} + +/** + * Callback to initialize the SPViewWidget. + */ +static void sp_view_widget_init(SPViewWidget *vw) +{ + vw->view = nullptr; +} + +/** + * Callback to disconnect from view and destroy SPViewWidget. + * + * Apparently, this gets only called when a desktop is closed, but then twice! + */ +static void sp_view_widget_dispose(GObject *object) +{ + SPViewWidget *vw = SP_VIEW_WIDGET(object); + + if (vw->view) { + vw->view->close(); + Inkscape::GC::release(vw->view); + vw->view = nullptr; + } + + if (G_OBJECT_CLASS(sp_view_widget_parent_class)->dispose) { + G_OBJECT_CLASS(sp_view_widget_parent_class)->dispose(object); + } + + Inkscape::GC::request_early_collection(); +} + +void sp_view_widget_set_view(SPViewWidget *vw, Inkscape::UI::View::View *view) +{ + g_return_if_fail(vw != nullptr); + g_return_if_fail(SP_IS_VIEW_WIDGET(vw)); + g_return_if_fail(view != nullptr); + + g_return_if_fail(vw->view == nullptr); + + vw->view = view; + Inkscape::GC::anchor(view); + + if (((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->set_view) { + ((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->set_view(vw, view); + } +} + +bool sp_view_widget_shutdown(SPViewWidget *vw) +{ + g_return_val_if_fail(vw != nullptr, TRUE); + g_return_val_if_fail(SP_IS_VIEW_WIDGET(vw), TRUE); + + if (((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->shutdown) { + return ((SPViewWidgetClass *) G_OBJECT_GET_CLASS(vw))->shutdown(vw); + } + + return FALSE; +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/view/view-widget.h b/src/ui/view/view-widget.h new file mode 100644 index 0000000..0296ac6 --- /dev/null +++ b/src/ui/view/view-widget.h @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_VIEW_VIEWWIDGET_H +#define INKSCAPE_UI_VIEW_VIEWWIDGET_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> + + +namespace Inkscape { +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI +} // namespace Inkscape + +class SPViewWidget; +class SPNamedView; + +#define SP_TYPE_VIEW_WIDGET (sp_view_widget_get_type ()) +#define SP_VIEW_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SP_TYPE_VIEW_WIDGET, SPViewWidget)) +#define SP_VIEW_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SP_TYPE_VIEW_WIDGET, SPViewWidgetClass)) +#define SP_IS_VIEW_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SP_TYPE_VIEW_WIDGET)) +#define SP_IS_VIEW_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SP_TYPE_VIEW_WIDGET)) +#define SP_VIEW_WIDGET_VIEW(w) (SP_VIEW_WIDGET (w)->view) +#define SP_VIEW_WIDGET_DOCUMENT(w) (SP_VIEW_WIDGET (w)->view ? ((SPViewWidget *) (w))->view->doc : NULL) + +/** + * Registers the SPViewWidget class with Glib and returns its type number. + */ +GType sp_view_widget_get_type(); + +/** + * Connects widget to view's 'resized' signal and calls virtual set_view() + * function. + */ +void sp_view_widget_set_view(SPViewWidget *vw, Inkscape::UI::View::View *view); + +/** + * Allows presenting 'save changes' dialog, FALSE - continue, TRUE - cancel. + * Calls the virtual shutdown() function of the SPViewWidget. + */ +bool sp_view_widget_shutdown(SPViewWidget *vw); + +/** + * SPViewWidget is a GUI widget that contain a single View. It is also + * an abstract base class with little functionality of its own. + */ +class SPViewWidget { + public: + GtkEventBox eventbox; // NOT USED! + + Inkscape::UI::View::View *view; + + // C++ Wrappers + GType getType() const { + return sp_view_widget_get_type(); + } + + void setView(Inkscape::UI::View::View *view) { + sp_view_widget_set_view(this, view); + } + + gboolean shutdown() { + return sp_view_widget_shutdown(this); + } + +// void resized (double x, double y) = 0; +}; + +/** + * The Glib-style vtable for the SPViewWidget class. + */ +class SPViewWidgetClass { + public: + GtkEventBoxClass parent_class; + + /* Virtual method to set/change/remove view */ + void (* set_view) (SPViewWidget *vw, Inkscape::UI::View::View *view); + /// Virtual method about view size change + void (* view_resized) (SPViewWidget *vw, Inkscape::UI::View::View *view, gdouble width, gdouble height); + + gboolean (* shutdown) (SPViewWidget *vw); +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/view/view.cpp b/src/ui/view/view.cpp new file mode 100644 index 0000000..3b2b7f2 --- /dev/null +++ b/src/ui/view/view.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <memory> +#include "document.h" +#include "view.h" +#include "message-stack.h" +#include "message-context.h" +#include "verbs.h" +#include "inkscape.h" + +namespace Inkscape { +namespace UI { +namespace View { + +static void +_onResized (double x, double y, View* v) +{ + v->onResized (x,y); +} + +static void +_onRedrawRequested (View* v) +{ + v->onRedrawRequested(); +} + +static void +_onStatusMessage (Inkscape::MessageType type, gchar const *message, View* v) +{ + v->onStatusMessage (type, message); +} + +static void +_onDocumentURISet (gchar const* uri, View* v) +{ + v->onDocumentURISet (uri); +} + +static void +_onDocumentResized (double x, double y, View* v) +{ + v->onDocumentResized (x,y); +} + +//-------------------------------------------------------------------- +View::View() +: _doc(nullptr) +{ + _message_stack = std::make_shared<Inkscape::MessageStack>(); + _tips_message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(_message_stack)); + + _resized_connection = _resized_signal.connect (sigc::bind (sigc::ptr_fun (&_onResized), this)); + _redraw_requested_connection = _redraw_requested_signal.connect (sigc::bind (sigc::ptr_fun (&_onRedrawRequested), this)); + + _message_changed_connection = _message_stack->connectChanged (sigc::bind (sigc::ptr_fun (&_onStatusMessage), this)); +} + +View::~View() +{ + _close(); +} + +void View::_close() { + _message_changed_connection.disconnect(); + + _tips_message_context = nullptr; + + _message_stack = nullptr; + + if (_doc) { + _document_uri_set_connection.disconnect(); + _document_resized_connection.disconnect(); + if (INKSCAPE.remove_document(_doc)) { + // this was the last view of this document, so delete it + // delete _doc; Delete now handled in Inkscape::Application + } + _doc = nullptr; + } + + Inkscape::Verb::delete_all_view (this); +} + +void View::emitResized (double width, double height) +{ + _resized_signal.emit (width, height); +} + +void View::requestRedraw() +{ + _redraw_requested_signal.emit(); +} + +void View::setDocument(SPDocument *doc) { + g_return_if_fail(doc != nullptr); + + if (_doc) { + _document_uri_set_connection.disconnect(); + _document_resized_connection.disconnect(); + if (INKSCAPE.remove_document(_doc)) { + // this was the last view of this document, so delete it + // delete _doc; Delete now handled in Inkscape::Application + } + } + + INKSCAPE.add_document(doc); + + _doc = doc; + _document_uri_set_connection = + _doc->connectURISet(sigc::bind(sigc::ptr_fun(&_onDocumentURISet), this)); + _document_resized_connection = + _doc->connectResized(sigc::bind(sigc::ptr_fun(&_onDocumentResized), this)); + _document_uri_set_signal.emit( _doc->getDocumentURI() ); +} + +}}} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/view/view.h b/src/ui/view/view.h new file mode 100644 index 0000000..3acfd2c --- /dev/null +++ b/src/ui/view/view.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_UI_VIEW_VIEW_H +#define INKSCAPE_UI_VIEW_VIEW_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdk.h> +#include <cstddef> +#include <memory> +#include <sigc++/connection.h> +#include "message.h" +#include "inkgc/gc-managed.h" +#include "gc-finalized.h" +#include "gc-anchored.h" +#include <2geom/forward.h> + +/** + * Iterates until true or returns false. + * When used as signal accumulator, stops emission if one slot returns true. + */ +struct StopOnTrue { + typedef bool result_type; + + template<typename T_iterator> + result_type operator()(T_iterator first, T_iterator last) const{ + for (; first != last; ++first) + if (*first) return true; + return false; + } +}; + +/** + * Iterates until nonzero or returns 0. + * When used as signal accumulator, stops emission if one slot returns nonzero. + */ +struct StopOnNonZero { + typedef int result_type; + + template<typename T_iterator> + result_type operator()(T_iterator first, T_iterator last) const{ + for (; first != last; ++first) + if (*first) return *first; + return 0; + } +}; + +class SPDocument; + +namespace Inkscape { + class MessageContext; + class MessageStack; + namespace UI { + namespace View { + +/** + * View is an abstract base class of all UI document views. This + * includes both the editing window and the SVG preview, but does not + * include the non-UI RGBA buffer-based Inkscape::Drawing nor the XML editor or + * similar views. The View base class has very little functionality of + * its own. + */ +class View : public GC::Managed<>, + public GC::Finalized, + public GC::Anchored +{ +public: + + View(); + + /** + * Deletes and nulls all View message stacks and disconnects it from signals. + */ + ~View() override; + + void close() { _close(); } + + /// Returns a pointer to the view's document. + SPDocument *doc() const + { return _doc; } + /// Returns a pointer to the view's message stack. + std::shared_ptr<Inkscape::MessageStack> messageStack() const + { return _message_stack; } + /// Returns a pointer to the view's tipsMessageContext. + Inkscape::MessageContext *tipsMessageContext() const + { return _tips_message_context.get(); } + + void emitResized(gdouble width, gdouble height); + void requestRedraw(); + + virtual void onResized (double, double) {}; + virtual void onRedrawRequested() {}; + virtual void onStatusMessage (Inkscape::MessageType type, gchar const *message) {}; + virtual void onDocumentURISet (gchar const* uri) {}; + virtual void onDocumentResized (double, double) {}; + virtual bool shutdown() { return false; }; + +protected: + SPDocument *_doc; + std::shared_ptr<Inkscape::MessageStack> _message_stack; + std::unique_ptr<Inkscape::MessageContext> _tips_message_context; + + virtual void _close(); + + /** + * Disconnects the view from the document signals, connects the view + * to a new one, and emits the _document_set_signal on the view. + * + * This is code common to all subclasses and called from their + * setDocument() methods after they are done. + * + * @param doc The new document to connect the view to. + */ + virtual void setDocument(SPDocument *doc); + + sigc::signal<void,double,double> _resized_signal; + sigc::signal<void,gchar const*> _document_uri_set_signal; + sigc::signal<void> _redraw_requested_signal; + +private: + sigc::connection _resized_connection; + sigc::connection _redraw_requested_connection; + sigc::connection _message_changed_connection; // foreign + sigc::connection _document_uri_set_connection; // foreign + sigc::connection _document_resized_connection; // foreign +}; + +}}} + +#endif // INKSCAPE_UI_VIEW_VIEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/alignment-selector.cpp b/src/ui/widget/alignment-selector.cpp new file mode 100644 index 0000000..e5ac17a --- /dev/null +++ b/src/ui/widget/alignment-selector.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.cpp + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/alignment-selector.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void AlignmentSelector::setupButton(const Glib::ustring& icon, Gtk::Button& button) { + Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + buttonIcon->show(); + + button.set_relief(Gtk::RELIEF_NONE); + button.show(); + button.add(*buttonIcon); + button.set_can_focus(false); +} + +AlignmentSelector::AlignmentSelector() + : _container() +{ + set_halign(Gtk::ALIGN_CENTER); + setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]); + setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]); + setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]); + setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]); + setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]); + setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]); + setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]); + + _container.set_row_homogeneous(); + _container.set_column_homogeneous(true); + + for(int i = 0; i < 9; ++i) { + _buttons[i].signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &AlignmentSelector::btn_activated), i)); + + _container.attach(_buttons[i], i % 3, i / 3, 1, 1); + } + + this->add(_container); +} + +AlignmentSelector::~AlignmentSelector() +{ + // TODO Auto-generated destructor stub +} + +void AlignmentSelector::btn_activated(int index) +{ + _alignmentClicked.emit(index); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/alignment-selector.h b/src/ui/widget/alignment-selector.h new file mode 100644 index 0000000..5bcf0fa --- /dev/null +++ b/src/ui/widget/alignment-selector.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.h + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef ANCHOR_SELECTOR_H_ +#define ANCHOR_SELECTOR_H_ + +#include <gtkmm/bin.h> +#include <gtkmm/button.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AlignmentSelector : public Gtk::Bin +{ +private: + Gtk::Button _buttons[9]; + Gtk::Grid _container; + + sigc::signal<void, int> _alignmentClicked; + + void setupButton(const Glib::ustring &icon, Gtk::Button &button); + void btn_activated(int index); + +public: + + sigc::signal<void, int> &on_alignmentClicked() { return _alignmentClicked; } + + AlignmentSelector(); + ~AlignmentSelector() override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* ANCHOR_SELECTOR_H_ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/anchor-selector.cpp b/src/ui/widget/anchor-selector.cpp new file mode 100644 index 0000000..b151a81 --- /dev/null +++ b/src/ui/widget/anchor-selector.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.cpp + * + * Created on: Mar 22, 2012 + * Author: denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "ui/widget/anchor-selector.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void AnchorSelector::setupButton(const Glib::ustring& icon, Gtk::ToggleButton& button) { + Gtk::Image *buttonIcon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + buttonIcon->show(); + + button.set_relief(Gtk::RELIEF_NONE); + button.show(); + button.add(*buttonIcon); + button.set_can_focus(false); +} + +AnchorSelector::AnchorSelector() + : _container() +{ + set_halign(Gtk::ALIGN_CENTER); + setupButton(INKSCAPE_ICON("boundingbox_top_left"), _buttons[0]); + setupButton(INKSCAPE_ICON("boundingbox_top"), _buttons[1]); + setupButton(INKSCAPE_ICON("boundingbox_top_right"), _buttons[2]); + setupButton(INKSCAPE_ICON("boundingbox_left"), _buttons[3]); + setupButton(INKSCAPE_ICON("boundingbox_center"), _buttons[4]); + setupButton(INKSCAPE_ICON("boundingbox_right"), _buttons[5]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_left"), _buttons[6]); + setupButton(INKSCAPE_ICON("boundingbox_bottom"), _buttons[7]); + setupButton(INKSCAPE_ICON("boundingbox_bottom_right"), _buttons[8]); + + _container.set_row_homogeneous(); + _container.set_column_homogeneous(true); + + for (int i = 0; i < 9; ++i) { + _buttons[i].signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &AnchorSelector::btn_activated), i)); + + _container.attach(_buttons[i], i % 3, i / 3, 1, 1); + } + _selection = 4; + _buttons[4].set_active(); + + this->add(_container); +} + +AnchorSelector::~AnchorSelector() +{ + // TODO Auto-generated destructor stub +} + +void AnchorSelector::btn_activated(int index) +{ + if (_selection == index && _buttons[index].get_active() == false) { + _buttons[index].set_active(true); + } + else if (_selection != index && _buttons[index].get_active()) { + int old_selection = _selection; + _selection = index; + _buttons[old_selection].set_active(false); + _selectionChanged.emit(); + } +} + +void AnchorSelector::setAlignment(int horizontal, int vertical) +{ + int index = 3 * vertical + horizontal; + if (index >= 0 && index < 9) { + _buttons[index].set_active(!_buttons[index].get_active()); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/anchor-selector.h b/src/ui/widget/anchor-selector.h new file mode 100644 index 0000000..49ce0b2 --- /dev/null +++ b/src/ui/widget/anchor-selector.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * anchor-selector.h + * + * Created on: Mar 22, 2012 + * Author: denis + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef ANCHOR_SELECTOR_H_ +#define ANCHOR_SELECTOR_H_ + +#include <gtkmm/bin.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class AnchorSelector : public Gtk::Bin +{ +private: + Gtk::ToggleButton _buttons[9]; + int _selection; + Gtk::Grid _container; + + sigc::signal<void> _selectionChanged; + + void setupButton(const Glib::ustring &icon, Gtk::ToggleButton &button); + void btn_activated(int index); + +public: + + int getHorizontalAlignment() { return _selection % 3; } + int getVerticalAlignment() { return _selection / 3; } + + sigc::signal<void> &on_selectionChanged() { return _selectionChanged; } + + void setAlignment(int horizontal, int vertical); + + AnchorSelector(); + ~AnchorSelector() override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* ANCHOR_SELECTOR_H_ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/attr-widget.h b/src/ui/widget/attr-widget.h new file mode 100644 index 0000000..014a540 --- /dev/null +++ b/src/ui/widget/attr-widget.h @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Rodrigo Kumpera <kumpera@gmail.com> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ATTR_WIDGET_H +#define INKSCAPE_UI_WIDGET_ATTR_WIDGET_H + +#include "attributes.h" +#include "object/sp-object.h" +#include "xml/node.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +enum DefaultValueType +{ + T_NONE, + T_DOUBLE, + T_VECT_DOUBLE, + T_BOOL, + T_UINT, + T_CHARPTR +}; + +/** + * Very basic interface for classes that control attributes. + */ +class DefaultValueHolder +{ + DefaultValueType type; + union { + double d_val; + std::vector<double>* vt_val; + bool b_val; + unsigned int uint_val; + char* cptr_val; + } value; + + //FIXME remove copy ctor and assignment operator as private to avoid double free of the vector +public: + DefaultValueHolder () { + type = T_NONE; + } + + DefaultValueHolder (double d) { + type = T_DOUBLE; + value.d_val = d; + } + + DefaultValueHolder (std::vector<double>* d) { + type = T_VECT_DOUBLE; + value.vt_val = d; + } + + DefaultValueHolder (char* c) { + type = T_CHARPTR; + value.cptr_val = c; + } + + DefaultValueHolder (bool d) { + type = T_BOOL; + value.b_val = d; + } + + DefaultValueHolder (unsigned int ui) { + type = T_UINT; + value.uint_val = ui; + } + + ~DefaultValueHolder() { + if (type == T_VECT_DOUBLE) + delete value.vt_val; + } + + unsigned int as_uint() { + g_assert (type == T_UINT); + return value.uint_val; + } + + bool as_bool() { + g_assert (type == T_BOOL); + return value.b_val; + } + + double as_double() { + g_assert (type == T_DOUBLE); + return value.d_val; + } + + std::vector<double>* as_vector() { + g_assert (type == T_VECT_DOUBLE); + return value.vt_val; + } + + char* as_charptr() { + g_assert (type == T_CHARPTR); + return value.cptr_val; + } +}; + +class AttrWidget +{ +public: + AttrWidget(const SPAttributeEnum a, unsigned int value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttributeEnum a, double value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttributeEnum a, bool value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttributeEnum a, char* value) + : _attr(a), + _default(value) + {} + + AttrWidget(const SPAttributeEnum a) + : _attr(a), + _default() + {} + + virtual ~AttrWidget() + = default; + + virtual Glib::ustring get_as_attribute() const = 0; + virtual void set_from_attribute(SPObject*) = 0; + + SPAttributeEnum get_attribute() const + { + return _attr; + } + + sigc::signal<void>& signal_attr_changed() + { + return _signal; + } +protected: + DefaultValueHolder* get_default() { return &_default; } + const gchar* attribute_value(SPObject* o) const + { + const gchar* name = (const gchar*)sp_attribute_name(_attr); + if(name && o) { + const gchar* val = o->getRepr()->attribute(name); + return val; + } + return nullptr; + } + +private: + const SPAttributeEnum _attr; + DefaultValueHolder _default; + sigc::signal<void> _signal; +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/button.cpp b/src/ui/widget/button.cpp new file mode 100644 index 0000000..f119c06 --- /dev/null +++ b/src/ui/widget/button.cpp @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic button widget + *//* + * Authors: + * see git history + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> + +#include "button.h" +#include "helper/action-context.h" +#include "helper/action.h" +#include "shortcuts.h" +#include "ui/icon-loader.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +Button::~Button() +{ + if (_action) { + _c_set_active.disconnect(); + _c_set_sensitive.disconnect(); + g_object_unref(_action); + } + + if (_doubleclick_action) { + set_doubleclick_action(nullptr); + } +} + +void +Button::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const +{ + auto child = get_child(); + + if (child) { + child->get_preferred_width(minimal_width, natural_width); + } else { + minimal_width = 0; + natural_width = 0; + } + + auto context = get_style_context(); + + auto padding = context->get_padding(context->get_state()); + auto border = context->get_border(context->get_state()); + + minimal_width += MAX(2, padding.get_left() + padding.get_right() + border.get_left() + border.get_right()); + natural_width += MAX(2, padding.get_left() + padding.get_right() + border.get_left() + border.get_right()); +} + +void +Button::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const +{ + auto child = get_child(); + + if (child) { + child->get_preferred_height(minimal_height, natural_height); + } else { + minimal_height = 0; + natural_height = 0; + } + + auto context = get_style_context(); + + auto padding = context->get_padding(context->get_state()); + auto border = context->get_border(context->get_state()); + + minimal_height += MAX(2, padding.get_top() + padding.get_bottom() + border.get_top() + border.get_bottom()); + natural_height += MAX(2, padding.get_top() + padding.get_bottom() + border.get_top() + border.get_bottom()); +} + +void +Button::on_clicked() +{ + if (_type == BUTTON_TYPE_TOGGLE) { + Gtk::Button::on_clicked(); + } +} + +bool +Button::process_event(GdkEvent *event) +{ + switch (event->type) { + case GDK_2BUTTON_PRESS: + if (_doubleclick_action) { + sp_action_perform(_doubleclick_action, nullptr); + } + return true; + break; + default: + break; + } + + return false; +} + +void +Button::perform_action() +{ + if (_action) { + sp_action_perform(_action, nullptr); + } +} + +Button::Button(GtkIconSize size, + ButtonType type, + SPAction *action, + SPAction *doubleclick_action) + : + _action(nullptr), + _doubleclick_action(nullptr), + _type(type), + _lsize(CLAMP(size, GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_DIALOG)) +{ + set_border_width(0); + + set_can_focus(false); + set_can_default(false); + + _on_clicked = signal_clicked().connect(sigc::mem_fun(*this, &Button::perform_action)); + + signal_event().connect(sigc::mem_fun(*this, &Button::process_event)); + + set_action(action); + + if (doubleclick_action) { + set_doubleclick_action(doubleclick_action); + } + + // The Inkscape style is no-relief buttons + set_relief(Gtk::RELIEF_NONE); +} + +void +Button::toggle_set_down(bool down) +{ + _on_clicked.block(); + set_active(down); + _on_clicked.unblock(); +} + +void +Button::set_doubleclick_action(SPAction *action) +{ + if (_doubleclick_action) { + g_object_unref(_doubleclick_action); + } + _doubleclick_action = action; + if (action) { + g_object_ref(action); + } +} + +void +Button::set_action(SPAction *action) +{ + Gtk::Widget *child; + + if (_action) { + _c_set_active.disconnect(); + _c_set_sensitive.disconnect(); + child = get_child(); + if (child) { + remove(); + } + g_object_unref(_action); + } + + _action = action; + if (action) { + g_object_ref(action); + _c_set_active = action->signal_set_active.connect( + sigc::mem_fun(*this, &Button::action_set_active)); + + _c_set_sensitive = action->signal_set_sensitive.connect( + sigc::mem_fun(*this, &Gtk::Widget::set_sensitive)); + + if (action->image) { + child = Glib::wrap(sp_get_icon_image(action->image, _lsize)); + child->show(); + add(*child); + } + } + + set_composed_tooltip(action); +} + +void +Button::action_set_active(bool active) +{ + if (_type != BUTTON_TYPE_TOGGLE) { + return; + } + + /* temporarily lobotomized until SPActions are per-view */ + if (false && !active != !get_active()) { + toggle_set_down(active); + } +} + +void +Button::set_composed_tooltip(SPAction *action) +{ + if (action) { + unsigned int shortcut = sp_shortcut_get_primary(action->verb); + if (shortcut != GDK_KEY_VoidSymbol) { + // there's both action and shortcut + + gchar *key = sp_shortcut_get_label(shortcut); + + gchar *tip = g_strdup_printf("%s (%s)", action->tip, key); + set_tooltip_text(tip); + g_free(tip); + g_free(key); + } else { + // action has no shortcut + set_tooltip_text(action->tip); + } + } else { + // no action + set_tooltip_text(nullptr); + } +} + +Button::Button(GtkIconSize size, + ButtonType type, + Inkscape::UI::View::View *view, + const gchar *name, + const gchar *tip) + : + _action(nullptr), + _doubleclick_action(nullptr), + _type(type), + _lsize(CLAMP(size, GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_DIALOG)) +{ + set_border_width(0); + + set_can_focus(false); + set_can_default(false); + + _on_clicked = signal_clicked().connect(sigc::mem_fun(*this, &Button::perform_action)); + signal_event().connect(sigc::mem_fun(*this, &Button::process_event)); + + auto action = sp_action_new(Inkscape::ActionContext(view), name, name, tip, name, nullptr); + set_action(action); + g_object_unref(action); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/button.h b/src/ui/widget/button.h new file mode 100644 index 0000000..0b1bfc2 --- /dev/null +++ b/src/ui/widget/button.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic button widget + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_BUTTON_H +#define SEEN_SP_BUTTON_H + +#include <gtkmm/togglebutton.h> +#include <sigc++/connection.h> + +struct SPAction; + +namespace Inkscape { +namespace UI { +namespace View { +class View; +} + +namespace Widget { + +enum ButtonType { + BUTTON_TYPE_NORMAL, + BUTTON_TYPE_TOGGLE +}; + +class Button : public Gtk::ToggleButton{ +private: + ButtonType _type; + GtkIconSize _lsize; + unsigned int _psize; + SPAction *_action; + SPAction *_doubleclick_action; + + sigc::connection _c_set_active; + sigc::connection _c_set_sensitive; + + void set_action(SPAction *action); + void set_doubleclick_action(SPAction *action); + void set_composed_tooltip(SPAction *action); + void action_set_active(bool active); + void perform_action(); + bool process_event(GdkEvent *event); + + sigc::connection _on_clicked; + +protected: + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void on_clicked() override; + +public: + Button(GtkIconSize size, + ButtonType type, + SPAction *action, + SPAction *doubleclick_action); + + Button(GtkIconSize size, + ButtonType type, + Inkscape::UI::View::View *view, + const gchar *name, + const gchar *tip); + + ~Button() override; + + void toggle_set_down(bool down); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape +#endif // !SEEN_SP_BUTTON_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/clipmaskicon.cpp b/src/ui/widget/clipmaskicon.cpp new file mode 100644 index 0000000..1093c1f --- /dev/null +++ b/src/ui/widget/clipmaskicon.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/clipmaskicon.h" + +#include "layertypeicon.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "widgets/toolbox.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ClipMaskIcon::ClipMaskIcon() : + Glib::ObjectBase(typeid(ClipMaskIcon)), + Gtk::CellRendererPixbuf(), + _pixClipName(INKSCAPE_ICON("path-cut")), + _pixMaskName(INKSCAPE_ICON("path-difference")), + _pixBothName(INKSCAPE_ICON("bitmap-trace")), + _property_active(*this, "active", 0), + _property_pixbuf_clip(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_mask(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_both(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) +{ + + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + + _property_pixbuf_clip = sp_get_icon_pixbuf(_pixClipName, GTK_ICON_SIZE_MENU); + _property_pixbuf_mask = sp_get_icon_pixbuf(_pixMaskName, GTK_ICON_SIZE_MENU); + _property_pixbuf_both = sp_get_icon_pixbuf(_pixBothName, GTK_ICON_SIZE_MENU); + + property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr); +} + +void ClipMaskIcon::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void ClipMaskIcon::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void ClipMaskIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + switch (_property_active.get_value()) + { + case 1: + property_pixbuf() = _property_pixbuf_clip; + break; + case 2: + property_pixbuf() = _property_pixbuf_mask; + break; + case 3: + property_pixbuf() = _property_pixbuf_both; + break; + default: + property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr); + break; + } + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool ClipMaskIcon::activate_vfunc(GdkEvent* /*event*/, + Gtk::Widget& /*widget*/, + const Glib::ustring& /*path*/, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + return false; +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/widget/clipmaskicon.h b/src/ui/widget/clipmaskicon.h new file mode 100644 index 0000000..d8bbe52 --- /dev/null +++ b/src/ui/widget/clipmaskicon.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_CLIPMASKICON_H__ +#define __UI_DIALOG_CLIPMASKICON_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ClipMaskIcon : public Gtk::CellRendererPixbuf { +public: + ClipMaskIcon(); + ~ClipMaskIcon() override = default;; + + Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + +protected: + + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + +private: + int phys; + + Glib::ustring _pixClipName; + Glib::ustring _pixMaskName; + Glib::ustring _pixBothName; + + Glib::Property<int> _property_active; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_clip; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_mask; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_both; + +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-entry.cpp b/src/ui/widget/color-entry.cpp new file mode 100644 index 0000000..804350c --- /dev/null +++ b/src/ui/widget/color-entry.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Entry widget for typing color value in css form + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm.h> +#include <glibmm/i18n.h> +#include <iomanip> + +#include "color-entry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorEntry::ColorEntry(SelectedColor &color) + : _color(color) + , _updating(false) + , _updatingrgba(false) + , _prevpos(0) + , _lastcolor(0) +{ + _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + signal_activate().connect(sigc::mem_fun(this, &ColorEntry::_onColorChanged)); + get_buffer()->signal_inserted_text().connect(sigc::mem_fun(this, &ColorEntry::_inputCheck)); + _onColorChanged(); + + // add extra character for pasting a hash, '#11223344' + set_max_length(9); + set_width_chars(8); + set_tooltip_text(_("Hexadecimal RGBA value of the color")); +} + +ColorEntry::~ColorEntry() +{ + _color_changed_connection.disconnect(); + _color_dragged_connection.disconnect(); +} + +void ColorEntry::_inputCheck(guint pos, const gchar * /*chars*/, guint n_chars) +{ + // remember position of last character, so we can remove it. + // we only overflow by 1 character at most. + _prevpos = pos + n_chars - 1; +} + +void ColorEntry::on_changed() +{ + if (_updating) { + return; + } + if (_updatingrgba) { + return; // Typing text into entry box + } + + Glib::ustring text = get_text(); + bool changed = false; + + // Coerce the value format to hexadecimal + for (auto it = text.begin(); it != text.end(); /*++it*/) { + if (!g_ascii_isxdigit(*it)) { + text.erase(it); + changed = true; + } else { + ++it; + } + } + + if (text.size() > 8) { + text.erase(_prevpos, 1); + changed = true; + } + + // autofill rules + gchar *str = g_strdup(text.c_str()); + gchar *end = nullptr; + guint64 rgba = g_ascii_strtoull(str, &end, 16); + ptrdiff_t len = end - str; + if (len < 8) { + if (len == 0) { + rgba = _lastcolor; + } else if (len <= 2) { + if (len == 1) { + rgba *= 17; + } + rgba = (rgba << 24) + (rgba << 16) + (rgba << 8); + } else if (len <= 4) { + // display as rrggbbaa + rgba = rgba << (4 * (4 - len)); + guint64 r = rgba & 0xf000; + guint64 g = rgba & 0x0f00; + guint64 b = rgba & 0x00f0; + guint64 a = rgba & 0x000f; + rgba = 17 * ((r << 12) + (g << 8) + (b << 4) + a); + } else { + rgba = rgba << (4 * (8 - len)); + } + + if (len == 7) { + rgba = (rgba & 0xfffffff0) + (_lastcolor & 0x00f); + } else if (len == 5) { + rgba = (rgba & 0xfffff000) + (_lastcolor & 0xfff); + } else if (len != 4 && len != 8) { + rgba = (rgba & 0xffffff00) + (_lastcolor & 0x0ff); + } + } + + _updatingrgba = true; + if (changed) { + set_text(str); + } + SPColor color(rgba); + _color.setColorAlpha(color, SP_RGBA32_A_F(rgba)); + _updatingrgba = false; + + g_free(str); +} + + +void ColorEntry::_onColorChanged() +{ + if (_updatingrgba) { + return; + } + + SPColor color = _color.color(); + gdouble alpha = _color.alpha(); + + _lastcolor = color.toRGBA32(alpha); + Glib::ustring text = Glib::ustring::format(std::hex, std::setw(8), std::setfill(L'0'), _lastcolor); + + Glib::ustring old_text = get_text(); + if (old_text != text) { + _updating = true; + set_text(text); + _updating = false; + } +} +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-entry.h b/src/ui/widget/color-entry.h new file mode 100644 index 0000000..4df80de --- /dev/null +++ b/src/ui/widget/color-entry.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Entry widget for typing color value in css form + *//* + * Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLOR_ENTRY_H +#define SEEN_COLOR_ENTRY_H + +#include <gtkmm/entry.h> +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorEntry : public Gtk::Entry +{ +public: + ColorEntry(SelectedColor &color); + ~ColorEntry() override; + +protected: + void on_changed() override; + +private: + void _onColorChanged(); + void _inputCheck(guint pos, const gchar * /*chars*/, guint /*n_chars*/); + + SelectedColor &_color; + sigc::connection _color_changed_connection; + sigc::connection _color_dragged_connection; + bool _updating; + bool _updatingrgba; + guint32 _lastcolor; + int _prevpos; +}; + +} +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-icc-selector.cpp b/src/ui/widget/color-icc-selector.cpp new file mode 100644 index 0000000..3100605 --- /dev/null +++ b/src/ui/widget/color-icc-selector.cpp @@ -0,0 +1,1074 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <set> +#include <utility> + +#include <gtkmm/adjustment.h> +#include <glibmm/i18n.h> + +#include "colorspace.h" +#include "document.h" +#include "inkscape.h" +#include "profile-manager.h" + +#include "svg/svg-icc-color.h" + +#include "ui/dialog-events.h" +#include "ui/util.h" +#include "ui/widget/color-icc-selector.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" + +#define noDEBUG_LCMS + +#if defined(HAVE_LIBLCMS2) +#include "object/color-profile.h" +#include "cms-system.h" +#include "color-profile-cms-fns.h" + +#ifdef DEBUG_LCMS +#include "preferences.h" +#endif // DEBUG_LCMS +#endif // defined(HAVE_LIBLCMS2) + +#ifdef DEBUG_LCMS +extern guint update_in_progress; +#define DEBUG_MESSAGE(key, ...) \ + { \ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); \ + bool dump = prefs->getBool("/options/scislac/" #key); \ + bool dumpD = prefs->getBool("/options/scislac/" #key "D"); \ + bool dumpD2 = prefs->getBool("/options/scislac/" #key "D2"); \ + dumpD && = ((update_in_progress == 0) || dumpD2); \ + if (dump) { \ + g_message(__VA_ARGS__); \ + } \ + if (dumpD) { \ + GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, \ + GTK_BUTTONS_OK, __VA_ARGS__); \ + g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog); \ + gtk_widget_show_all(dialog); \ + } \ + } +#endif // DEBUG_LCMS + + +#define XPAD 4 +#define YPAD 1 + +namespace { + +size_t maxColorspaceComponentCount = 0; + +#if defined(HAVE_LIBLCMS2) + +/** + * Internal variable to track all known colorspaces. + */ +std::set<cmsUInt32Number> knownColorspaces; + +#endif + +/** + * Helper function to handle GTK2/GTK3 attachment #ifdef code. + */ +void attachToGridOrTable(GtkWidget *parent, GtkWidget *child, guint left, guint top, guint width, guint height, + bool hexpand = false, bool centered = false, guint xpadding = XPAD, guint ypadding = YPAD) +{ + gtk_widget_set_margin_start(child, xpadding); + gtk_widget_set_margin_end(child, xpadding); + gtk_widget_set_margin_top(child, ypadding); + gtk_widget_set_margin_bottom(child, ypadding); + + if (hexpand) { + gtk_widget_set_hexpand(child, TRUE); + } + + if (centered) { + gtk_widget_set_halign(child, GTK_ALIGN_CENTER); + gtk_widget_set_valign(child, GTK_ALIGN_CENTER); + } + + gtk_grid_attach(GTK_GRID(parent), child, left, top, width, height); +} + +} // namespace + +/* +icSigRgbData +icSigCmykData +icSigCmyData +*/ +#define SPACE_ID_RGB 0 +#define SPACE_ID_CMY 1 +#define SPACE_ID_CMYK 2 + + +colorspace::Component::Component() + : name() + , tip() + , scale(1) +{ +} + +colorspace::Component::Component(std::string name, std::string tip, guint scale) + : name(std::move(name)) + , tip(std::move(tip)) + , scale(scale) +{ +} + +#if defined(HAVE_LIBLCMS2) +static cmsUInt16Number *getScratch() +{ + // bytes per pixel * input channels * width + static cmsUInt16Number *scritch = static_cast<cmsUInt16Number *>(g_new(cmsUInt16Number, 4 * 1024)); + + return scritch; +} + +std::vector<colorspace::Component> colorspace::getColorSpaceInfo(uint32_t space) +{ + static std::map<cmsUInt32Number, std::vector<Component> > sets; + if (sets.empty()) { + sets[cmsSigXYZData].push_back(Component("_X", "X", 2)); // TYPE_XYZ_16 + sets[cmsSigXYZData].push_back(Component("_Y", "Y", 1)); + sets[cmsSigXYZData].push_back(Component("_Z", "Z", 2)); + + sets[cmsSigLabData].push_back(Component("_L", "L", 100)); // TYPE_Lab_16 + sets[cmsSigLabData].push_back(Component("_a", "a", 256)); + sets[cmsSigLabData].push_back(Component("_b", "b", 256)); + + // cmsSigLuvData + + sets[cmsSigYCbCrData].push_back(Component("_Y", "Y", 1)); // TYPE_YCbCr_16 + sets[cmsSigYCbCrData].push_back(Component("C_b", "Cb", 1)); + sets[cmsSigYCbCrData].push_back(Component("C_r", "Cr", 1)); + + sets[cmsSigYxyData].push_back(Component("_Y", "Y", 1)); // TYPE_Yxy_16 + sets[cmsSigYxyData].push_back(Component("_x", "x", 1)); + sets[cmsSigYxyData].push_back(Component("y", "y", 1)); + + sets[cmsSigRgbData].push_back(Component(_("_R:"), _("Red"), 1)); // TYPE_RGB_16 + sets[cmsSigRgbData].push_back(Component(_("_G:"), _("Green"), 1)); + sets[cmsSigRgbData].push_back(Component(_("_B:"), _("Blue"), 1)); + + sets[cmsSigGrayData].push_back(Component(_("G:"), _("Gray"), 1)); // TYPE_GRAY_16 + + sets[cmsSigHsvData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HSV_16 + sets[cmsSigHsvData].push_back(Component(_("_S:"), _("Saturation"), 1)); + sets[cmsSigHsvData].push_back(Component("_V:", "Value", 1)); + + sets[cmsSigHlsData].push_back(Component(_("_H:"), _("Hue"), 360)); // TYPE_HLS_16 + sets[cmsSigHlsData].push_back(Component(_("_L:"), _("Lightness"), 1)); + sets[cmsSigHlsData].push_back(Component(_("_S:"), _("Saturation"), 1)); + + sets[cmsSigCmykData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMYK_16 + sets[cmsSigCmykData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + sets[cmsSigCmykData].push_back(Component(_("_K:"), _("Black"), 1)); + + sets[cmsSigCmyData].push_back(Component(_("_C:"), _("Cyan"), 1)); // TYPE_CMY_16 + sets[cmsSigCmyData].push_back(Component(_("_M:"), _("Magenta"), 1)); + sets[cmsSigCmyData].push_back(Component(_("_Y:"), _("Yellow"), 1)); + + for (auto & set : sets) { + knownColorspaces.insert(set.first); + maxColorspaceComponentCount = std::max(maxColorspaceComponentCount, set.second.size()); + } + } + + std::vector<Component> target; + + if (sets.find(space) != sets.end()) { + target = sets[space]; + } + return target; +} + + +std::vector<colorspace::Component> colorspace::getColorSpaceInfo(Inkscape::ColorProfile *prof) +{ + return getColorSpaceInfo(asICColorSpaceSig(prof->getColorSpace())); +} + +#endif // defined(HAVE_LIBLCMS2) + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Class containing the parts for a single color component's UI presence. + */ +class ComponentUI { + public: + ComponentUI() + : _component() + , _adj(nullptr) + , _slider(nullptr) + , _btn(nullptr) + , _label(nullptr) + , _map(nullptr) + { + } + + ComponentUI(colorspace::Component component) + : _component(std::move(component)) + , _adj(nullptr) + , _slider(nullptr) + , _btn(nullptr) + , _label(nullptr) + , _map(nullptr) + { + } + + colorspace::Component _component; + GtkAdjustment *_adj; // Component adjustment + Inkscape::UI::Widget::ColorSlider *_slider; + GtkWidget *_btn; // spinbutton + GtkWidget *_label; // Label + guchar *_map; +}; + +/** + * Class that implements the internals of the selector. + */ +class ColorICCSelectorImpl { + public: + ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color); + + ~ColorICCSelectorImpl(); + + static void _adjustmentChanged(GtkAdjustment *adjustment, ColorICCSelectorImpl *cs); + + void _sliderGrabbed(); + void _sliderReleased(); + void _sliderChanged(); + + static void _profileSelected(GtkWidget *src, gpointer data); + static void _fixupHit(GtkWidget *src, gpointer data); + +#if defined(HAVE_LIBLCMS2) + void _setProfile(SVGICCColor *profile); + void _switchToProfile(gchar const *name); +#endif + void _updateSliders(gint ignore); + void _profilesChanged(std::string const &name); + + ColorICCSelector *_owner; + SelectedColor &_color; + + gboolean _updating : 1; + gboolean _dragging : 1; + + guint32 _fixupNeeded; + GtkWidget *_fixupBtn; + GtkWidget *_profileSel; + + std::vector<ComponentUI> _compUI; + + GtkAdjustment *_adj; // Channel adjustment + Inkscape::UI::Widget::ColorSlider *_slider; + GtkWidget *_sbtn; // Spinbutton + GtkWidget *_label; // Label + +#if defined(HAVE_LIBLCMS2) + std::string _profileName; + Inkscape::ColorProfile *_prof; + guint _profChannelCount; + gulong _profChangedID; +#endif // defined(HAVE_LIBLCMS2) +}; + + + +const gchar *ColorICCSelector::MODE_NAME = N_("CMS"); + +ColorICCSelector::ColorICCSelector(SelectedColor &color) + : _impl(nullptr) +{ + _impl = new ColorICCSelectorImpl(this, color); + init(); + color.signal_changed.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged)); + // color.signal_dragged.connect(sigc::mem_fun(this, &ColorICCSelector::_colorChanged)); +} + +ColorICCSelector::~ColorICCSelector() +{ + if (_impl) { + delete _impl; + _impl = nullptr; + } +} + + + +ColorICCSelectorImpl::ColorICCSelectorImpl(ColorICCSelector *owner, SelectedColor &color) + : _owner(owner) + , _color(color) + , _updating(FALSE) + , _dragging(FALSE) + , _fixupNeeded(0) + , _fixupBtn(nullptr) + , _profileSel(nullptr) + , _compUI() + , _adj(nullptr) + , _slider(nullptr) + , _sbtn(nullptr) + , _label(nullptr) +#if defined(HAVE_LIBLCMS2) + , _profileName() + , _prof(nullptr) + , _profChannelCount(0) + , _profChangedID(0) +#endif // defined(HAVE_LIBLCMS2) +{ +} + +ColorICCSelectorImpl::~ColorICCSelectorImpl() +{ + _adj = nullptr; + _sbtn = nullptr; + _label = nullptr; +} + +void ColorICCSelector::init() +{ + gint row = 0; + + _impl->_updating = FALSE; + _impl->_dragging = FALSE; + + GtkWidget *t = GTK_WIDGET(gobj()); + + _impl->_compUI.clear(); + + // Create components + row = 0; + + + _impl->_fixupBtn = gtk_button_new_with_label(_("Fix")); + g_signal_connect(G_OBJECT(_impl->_fixupBtn), "clicked", G_CALLBACK(ColorICCSelectorImpl::_fixupHit), + (gpointer)_impl); + gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); + gtk_widget_set_tooltip_text(_impl->_fixupBtn, _("Fix RGB fallback to match icc-color() value.")); + gtk_widget_show(_impl->_fixupBtn); + + attachToGridOrTable(t, _impl->_fixupBtn, 0, row, 1, 1); + + // Combobox and store with 2 columns : label (0) and full name (1) + GtkListStore *store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + _impl->_profileSel = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store)); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, TRUE); + gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(_impl->_profileSel), renderer, "text", 0, NULL); + + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1); + + gtk_widget_show(_impl->_profileSel); + gtk_combo_box_set_active(GTK_COMBO_BOX(_impl->_profileSel), 0); + + attachToGridOrTable(t, _impl->_profileSel, 1, row, 1, 1); + +#if defined(HAVE_LIBLCMS2) + _impl->_profChangedID = g_signal_connect(G_OBJECT(_impl->_profileSel), "changed", + G_CALLBACK(ColorICCSelectorImpl::_profileSelected), (gpointer)_impl); +#else + gtk_widget_set_sensitive(_impl->_profileSel, false); +#endif // defined(HAVE_LIBLCMS2) + + + row++; + +// populate the data for colorspaces and channels: +#if defined(HAVE_LIBLCMS2) + std::vector<colorspace::Component> things = colorspace::getColorSpaceInfo(cmsSigRgbData); +#endif // defined(HAVE_LIBLCMS2) + + for (size_t i = 0; i < maxColorspaceComponentCount; i++) { +#if defined(HAVE_LIBLCMS2) + if (i < things.size()) { + _impl->_compUI.emplace_back(things[i]); + } + else { + _impl->_compUI.emplace_back(); + } + + std::string labelStr = (i < things.size()) ? things[i].name.c_str() : ""; +#else + _impl->_compUI.push_back(ComponentUI()); + + std::string labelStr = "."; +#endif + + _impl->_compUI[i]._label = gtk_label_new_with_mnemonic(labelStr.c_str()); + + gtk_widget_set_halign(_impl->_compUI[i]._label, GTK_ALIGN_END); + gtk_widget_show(_impl->_compUI[i]._label); + gtk_widget_set_no_show_all(_impl->_compUI[i]._label, TRUE); + + attachToGridOrTable(t, _impl->_compUI[i]._label, 0, row, 1, 1); + + // Adjustment + guint scaleValue = _impl->_compUI[i]._component.scale; + gdouble step = static_cast<gdouble>(scaleValue) / 100.0; + gdouble page = static_cast<gdouble>(scaleValue) / 10.0; + gint digits = (step > 0.9) ? 0 : 2; + _impl->_compUI[i]._adj = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, scaleValue, step, page, page)); + + // Slider + _impl->_compUI[i]._slider = + Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_impl->_compUI[i]._adj, true))); +#if defined(HAVE_LIBLCMS2) + _impl->_compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); +#else + _impl->_compUI[i]._slider->set_tooltip_text("."); +#endif // defined(HAVE_LIBLCMS2) + _impl->_compUI[i]._slider->show(); + _impl->_compUI[i]._slider->set_no_show_all(); + + attachToGridOrTable(t, _impl->_compUI[i]._slider->gobj(), 1, row, 1, 1, true); + + _impl->_compUI[i]._btn = gtk_spin_button_new(_impl->_compUI[i]._adj, step, digits); +#if defined(HAVE_LIBLCMS2) + gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); +#else + gtk_widget_set_tooltip_text(_impl->_compUI[i]._btn, "."); +#endif // defined(HAVE_LIBLCMS2) + sp_dialog_defocus_on_enter(_impl->_compUI[i]._btn); + gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_compUI[i]._label), _impl->_compUI[i]._btn); + gtk_widget_show(_impl->_compUI[i]._btn); + gtk_widget_set_no_show_all(_impl->_compUI[i]._btn, TRUE); + + attachToGridOrTable(t, _impl->_compUI[i]._btn, 2, row, 1, 1, false, true); + + _impl->_compUI[i]._map = g_new(guchar, 4 * 1024); + memset(_impl->_compUI[i]._map, 0x0ff, 1024 * 4); + + + // Signals + g_signal_connect(G_OBJECT(_impl->_compUI[i]._adj), "value_changed", + G_CALLBACK(ColorICCSelectorImpl::_adjustmentChanged), _impl); + + _impl->_compUI[i]._slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed)); + _impl->_compUI[i]._slider->signal_released.connect( + sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased)); + _impl->_compUI[i]._slider->signal_value_changed.connect( + sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged)); + + row++; + } + + // Label + _impl->_label = gtk_label_new_with_mnemonic(_("_A:")); + + gtk_widget_set_halign(_impl->_label, GTK_ALIGN_END); + gtk_widget_show(_impl->_label); + + attachToGridOrTable(t, _impl->_label, 0, row, 1, 1); + + // Adjustment + _impl->_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 100.0, 1.0, 10.0, 10.0)); + + // Slider + _impl->_slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_impl->_adj, true))); + _impl->_slider->set_tooltip_text(_("Alpha (opacity)")); + _impl->_slider->show(); + + attachToGridOrTable(t, _impl->_slider->gobj(), 1, row, 1, 1, true); + + _impl->_slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5), + SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0)); + + + // Spinbutton + _impl->_sbtn = gtk_spin_button_new(GTK_ADJUSTMENT(_impl->_adj), 1.0, 0); + gtk_widget_set_tooltip_text(_impl->_sbtn, _("Alpha (opacity)")); + sp_dialog_defocus_on_enter(_impl->_sbtn); + gtk_label_set_mnemonic_widget(GTK_LABEL(_impl->_label), _impl->_sbtn); + gtk_widget_show(_impl->_sbtn); + + attachToGridOrTable(t, _impl->_sbtn, 2, row, 1, 1, false, true); + + // Signals + g_signal_connect(G_OBJECT(_impl->_adj), "value_changed", G_CALLBACK(ColorICCSelectorImpl::_adjustmentChanged), + _impl); + + _impl->_slider->signal_grabbed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderGrabbed)); + _impl->_slider->signal_released.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderReleased)); + _impl->_slider->signal_value_changed.connect(sigc::mem_fun(_impl, &ColorICCSelectorImpl::_sliderChanged)); + + gtk_widget_show(t); +} + +void ColorICCSelectorImpl::_fixupHit(GtkWidget * /*src*/, gpointer data) +{ + ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data); + gtk_widget_set_sensitive(self->_fixupBtn, FALSE); + self->_adjustmentChanged(self->_compUI[0]._adj, self); +} + +#if defined(HAVE_LIBLCMS2) +void ColorICCSelectorImpl::_profileSelected(GtkWidget * /*src*/, gpointer data) +{ + ColorICCSelectorImpl *self = reinterpret_cast<ColorICCSelectorImpl *>(data); + + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(self->_profileSel), &iter)) { + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(self->_profileSel)); + gchar *name = nullptr; + + gtk_tree_model_get(store, &iter, 1, &name, -1); + self->_switchToProfile(name); + gtk_widget_set_tooltip_text(self->_profileSel, name); + + g_free(name); + } +} +#endif // defined(HAVE_LIBLCMS2) + +#if defined(HAVE_LIBLCMS2) +void ColorICCSelectorImpl::_switchToProfile(gchar const *name) +{ + bool dirty = false; + SPColor tmp(_color.color()); + + if (name) { + if (tmp.icc && tmp.icc->colorProfile == name) { +#ifdef DEBUG_LCMS + g_message("Already at name [%s]", name); +#endif // DEBUG_LCMS + } + else { +#ifdef DEBUG_LCMS + g_message("Need to switch to profile [%s]", name); +#endif // DEBUG_LCMS + if (tmp.icc) { + tmp.icc->colors.clear(); + } + else { + tmp.icc = new SVGICCColor(); + } + tmp.icc->colorProfile = name; + Inkscape::ColorProfile *newProf = SP_ACTIVE_DOCUMENT->getProfileManager()->find(name); + if (newProf) { + cmsHTRANSFORM trans = newProf->getTransfFromSRGB8(); + if (trans) { + guint32 val = _color.color().toRGBA32(0); + guchar pre[4] = { + static_cast<guchar>(SP_RGBA32_R_U(val)), + static_cast<guchar>(SP_RGBA32_G_U(val)), + static_cast<guchar>(SP_RGBA32_B_U(val)), + 255}; +#ifdef DEBUG_LCMS + g_message("Shoving in [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); +#endif // DEBUG_LCMS + cmsUInt16Number post[4] = { 0, 0, 0, 0 }; + cmsDoTransform(trans, pre, post, 1); +#ifdef DEBUG_LCMS + g_message("got on out [%04x] [%04x] [%04x] [%04x]", post[0], post[1], post[2], post[3]); +#endif // DEBUG_LCMS +#if HAVE_LIBLCMS2 + guint count = cmsChannelsOf(asICColorSpaceSig(newProf->getColorSpace())); +#endif + + std::vector<colorspace::Component> things = + colorspace::getColorSpaceInfo(asICColorSpaceSig(newProf->getColorSpace())); + + for (guint i = 0; i < count; i++) { + gdouble val = + (((gdouble)post[i]) / 65535.0) * (gdouble)((i < things.size()) ? things[i].scale : 1); +#ifdef DEBUG_LCMS + g_message(" scaled %d by %d to be %f", i, ((i < things.size()) ? things[i].scale : 1), val); +#endif // DEBUG_LCMS + tmp.icc->colors.push_back(val); + } + cmsHTRANSFORM retrans = newProf->getTransfToSRGB8(); + if (retrans) { + cmsDoTransform(retrans, post, pre, 1); +#ifdef DEBUG_LCMS + g_message(" back out [%02x] [%02x] [%02x]", pre[0], pre[1], pre[2]); +#endif // DEBUG_LCMS + tmp.set(SP_RGBA32_U_COMPOSE(pre[0], pre[1], pre[2], 0xff)); + } + + dirty = true; + } + } + } + } + else { +#ifdef DEBUG_LCMS + g_message("NUKE THE ICC"); +#endif // DEBUG_LCMS + if (tmp.icc) { + delete tmp.icc; + tmp.icc = nullptr; + dirty = true; + _fixupHit(nullptr, this); + } + else { +#ifdef DEBUG_LCMS + g_message("No icc to nuke"); +#endif // DEBUG_LCMS + } + } + + if (dirty) { +#ifdef DEBUG_LCMS + g_message("+----------------"); + g_message("+ new color is [%s]", tmp.toString().c_str()); +#endif // DEBUG_LCMS + _setProfile(tmp.icc); + //_adjustmentChanged( _compUI[0]._adj, SP_COLOR_ICC_SELECTOR(_csel) ); + _color.setColor(tmp); +#ifdef DEBUG_LCMS + g_message("+_________________"); +#endif // DEBUG_LCMS + } +} +#endif // defined(HAVE_LIBLCMS2) + +#if defined(HAVE_LIBLCMS2) +struct _cmp { + bool operator()(const SPObject * const & a, const SPObject * const & b) + { + const Inkscape::ColorProfile &a_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*a); + const Inkscape::ColorProfile &b_prof = reinterpret_cast<const Inkscape::ColorProfile &>(*b); + gchar *a_name_casefold = g_utf8_casefold(a_prof.name, -1 ); + gchar *b_name_casefold = g_utf8_casefold(b_prof.name, -1 ); + int result = g_strcmp0(a_name_casefold, b_name_casefold); + g_free(a_name_casefold); + g_free(b_name_casefold); + return result < 0; + } +}; + +template <typename From, typename To> +struct static_caster { To * operator () (From * value) const { return static_cast<To *>(value); } }; + +void ColorICCSelectorImpl::_profilesChanged(std::string const &name) +{ + GtkComboBox *combo = GTK_COMBO_BOX(_profileSel); + + g_signal_handler_block(G_OBJECT(_profileSel), _profChangedID); + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo)); + gtk_list_store_clear(store); + + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, _("<none>"), 1, _("<none>"), -1); + + gtk_combo_box_set_active(combo, 0); + + int index = 1; + std::vector<SPObject *> current = SP_ACTIVE_DOCUMENT->getResourceList("iccprofile"); + + std::set<Inkscape::ColorProfile *, Inkscape::ColorProfile::pointerComparator> _current; + std::transform(current.begin(), + current.end(), + std::inserter(_current, _current.begin()), + static_caster<SPObject, Inkscape::ColorProfile>()); + + for (auto &it: _current) { + Inkscape::ColorProfile *prof = it; + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, ink_ellipsize_text(prof->name, 25).c_str(), 1, prof->name, -1); + + if (name == prof->name) { + gtk_combo_box_set_active(combo, index); + gtk_widget_set_tooltip_text(_profileSel, prof->name); + } + + index++; + } + + g_signal_handler_unblock(G_OBJECT(_profileSel), _profChangedID); +} +#else +void ColorICCSelectorImpl::_profilesChanged(std::string const & /*name*/) {} +#endif // defined(HAVE_LIBLCMS2) + +void ColorICCSelector::on_show() +{ + Gtk::Grid::on_show(); + _colorChanged(); +} + +// Helpers for setting color value + +void ColorICCSelector::_colorChanged() +{ + _impl->_updating = TRUE; +// sp_color_icc_set_color( SP_COLOR_ICC( _icc ), &color ); + +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_colorChanged(%08x:%s)", this, _impl->_color.color().toRGBA32(_impl->_color.alpha()), + ((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + +#ifdef DEBUG_LCMS + g_message("FLIPPIES!!!! %p '%s'", _impl->_color.color().icc, + (_impl->_color.color().icc ? _impl->_color.color().icc->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + + _impl->_profilesChanged((_impl->_color.color().icc) ? _impl->_color.color().icc->colorProfile : std::string("")); + ColorScales::setScaled(_impl->_adj, _impl->_color.alpha()); + +#if defined(HAVE_LIBLCMS2) + _impl->_setProfile(_impl->_color.color().icc); + _impl->_fixupNeeded = 0; + gtk_widget_set_sensitive(_impl->_fixupBtn, FALSE); + + if (_impl->_prof) { + if (_impl->_prof->getTransfToSRGB8()) { + cmsUInt16Number tmp[4]; + for (guint i = 0; i < _impl->_profChannelCount; i++) { + gdouble val = 0.0; + if (_impl->_color.color().icc->colors.size() > i) { + if (_impl->_compUI[i]._component.scale == 256) { + val = (_impl->_color.color().icc->colors[i] + 128.0) / + static_cast<gdouble>(_impl->_compUI[i]._component.scale); + } + else { + val = _impl->_color.color().icc->colors[i] / + static_cast<gdouble>(_impl->_compUI[i]._component.scale); + } + } + tmp[i] = val * 0x0ffff; + } + guchar post[4] = { 0, 0, 0, 0 }; + cmsHTRANSFORM trans = _impl->_prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, tmp, post, 1); + guint32 other = SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255); + if (other != _impl->_color.color().toRGBA32(255)) { + _impl->_fixupNeeded = other; + gtk_widget_set_sensitive(_impl->_fixupBtn, TRUE); +#ifdef DEBUG_LCMS + g_message("Color needs to change 0x%06x to 0x%06x", _color.toRGBA32(255) >> 8, other >> 8); +#endif // DEBUG_LCMS + } + } + } + } +#else +//(void)color; +#endif // defined(HAVE_LIBLCMS2) + _impl->_updateSliders(-1); + + + _impl->_updating = FALSE; +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_colorChanged()", this); +#endif // DEBUG_LCMS +} + +#if defined(HAVE_LIBLCMS2) +void ColorICCSelectorImpl::_setProfile(SVGICCColor *profile) +{ +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_setProfile(%s)", this, ((profile) ? profile->colorProfile.c_str() : "<null>")); +#endif // DEBUG_LCMS + bool profChanged = false; + if (_prof && (!profile || (_profileName != profile->colorProfile))) { + // Need to clear out the prior one + profChanged = true; + _profileName.clear(); + _prof = nullptr; + _profChannelCount = 0; + } + else if (profile && !_prof) { + profChanged = true; + } + + for (auto & i : _compUI) { + gtk_widget_hide(i._label); + i._slider->hide(); + gtk_widget_hide(i._btn); + } + + if (profile) { + _prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(profile->colorProfile.c_str()); + if (_prof && (asICColorProfileClassSig(_prof->getProfileClass()) != cmsSigNamedColorClass)) { +#if HAVE_LIBLCMS2 + _profChannelCount = cmsChannelsOf(asICColorSpaceSig(_prof->getColorSpace())); +#endif + + if (profChanged) { + std::vector<colorspace::Component> things = + colorspace::getColorSpaceInfo(asICColorSpaceSig(_prof->getColorSpace())); + for (size_t i = 0; (i < things.size()) && (i < _profChannelCount); ++i) { + _compUI[i]._component = things[i]; + } + + for (guint i = 0; i < _profChannelCount; i++) { + gtk_label_set_text_with_mnemonic(GTK_LABEL(_compUI[i]._label), + (i < things.size()) ? things[i].name.c_str() : ""); + + _compUI[i]._slider->set_tooltip_text((i < things.size()) ? things[i].tip.c_str() : ""); + gtk_widget_set_tooltip_text(_compUI[i]._btn, (i < things.size()) ? things[i].tip.c_str() : ""); + + _compUI[i]._slider->setColors(SPColor(0.0, 0.0, 0.0).toRGBA32(0xff), + SPColor(0.5, 0.5, 0.5).toRGBA32(0xff), + SPColor(1.0, 1.0, 1.0).toRGBA32(0xff)); + /* + _compUI[i]._adj = GTK_ADJUSTMENT( gtk_adjustment_new( val, 0.0, _fooScales[i], + step, page, page ) ); + g_signal_connect( G_OBJECT( _compUI[i]._adj ), "value_changed", G_CALLBACK( + _adjustmentChanged ), _csel ); + + sp_color_slider_set_adjustment( SP_COLOR_SLIDER(_compUI[i]._slider), + _compUI[i]._adj ); + gtk_spin_button_set_adjustment( GTK_SPIN_BUTTON(_compUI[i]._btn), + _compUI[i]._adj ); + gtk_spin_button_set_digits( GTK_SPIN_BUTTON(_compUI[i]._btn), digits ); + */ + gtk_widget_show(_compUI[i]._label); + _compUI[i]._slider->show(); + gtk_widget_show(_compUI[i]._btn); + // gtk_adjustment_set_value( _compUI[i]._adj, 0.0 ); + // gtk_adjustment_set_value( _compUI[i]._adj, val ); + } + for (size_t i = _profChannelCount; i < _compUI.size(); i++) { + gtk_widget_hide(_compUI[i]._label); + _compUI[i]._slider->hide(); + gtk_widget_hide(_compUI[i]._btn); + } + } + } + else { + // Give up for now on named colors + _prof = nullptr; + } + } + +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_setProfile()", this); +#endif // DEBUG_LCMS +} +#endif // defined(HAVE_LIBLCMS2) + +void ColorICCSelectorImpl::_updateSliders(gint ignore) +{ +#if defined(HAVE_LIBLCMS2) + if (_color.color().icc) { + for (guint i = 0; i < _profChannelCount; i++) { + gdouble val = 0.0; + if (_color.color().icc->colors.size() > i) { + if (_compUI[i]._component.scale == 256) { + val = (_color.color().icc->colors[i] + 128.0) / static_cast<gdouble>(_compUI[i]._component.scale); + } + else { + val = _color.color().icc->colors[i] / static_cast<gdouble>(_compUI[i]._component.scale); + } + } + gtk_adjustment_set_value(_compUI[i]._adj, val); + } + + if (_prof) { + if (_prof->getTransfToSRGB8()) { + for (guint i = 0; i < _profChannelCount; i++) { + if (static_cast<gint>(i) != ignore) { + cmsUInt16Number *scratch = getScratch(); + cmsUInt16Number filler[4] = { 0, 0, 0, 0 }; + for (guint j = 0; j < _profChannelCount; j++) { + filler[j] = 0x0ffff * ColorScales::getScaled(_compUI[j]._adj); + } + + cmsUInt16Number *p = scratch; + for (guint x = 0; x < 1024; x++) { + for (guint j = 0; j < _profChannelCount; j++) { + if (j == i) { + *p++ = x * 0x0ffff / 1024; + } + else { + *p++ = filler[j]; + } + } + } + + cmsHTRANSFORM trans = _prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, scratch, _compUI[i]._map, 1024); + if (_compUI[i]._slider) + { + _compUI[i]._slider->setMap(_compUI[i]._map); + } + } + } + } + } + } + } +#else + (void)ignore; +#endif // defined(HAVE_LIBLCMS2) + + guint32 start = _color.color().toRGBA32(0x00); + guint32 mid = _color.color().toRGBA32(0x7f); + guint32 end = _color.color().toRGBA32(0xff); + + _slider->setColors(start, mid, end); +} + + +void ColorICCSelectorImpl::_adjustmentChanged(GtkAdjustment *adjustment, ColorICCSelectorImpl *cs) +{ +#ifdef DEBUG_LCMS + g_message("/^^^^^^^^^ %p::_adjustmentChanged()", cs); +#endif // DEBUG_LCMS + + ColorICCSelector *iccSelector = cs->_owner; + if (iccSelector->_impl->_updating) { + return; + } + + iccSelector->_impl->_updating = TRUE; + + gint match = -1; + + SPColor newColor(iccSelector->_impl->_color.color()); + gfloat scaled = ColorScales::getScaled(iccSelector->_impl->_adj); + if (iccSelector->_impl->_adj == adjustment) { +#ifdef DEBUG_LCMS + g_message("ALPHA"); +#endif // DEBUG_LCMS + } + else { +#if defined(HAVE_LIBLCMS2) + for (size_t i = 0; i < iccSelector->_impl->_compUI.size(); i++) { + if (iccSelector->_impl->_compUI[i]._adj == adjustment) { + match = i; + break; + } + } + if (match >= 0) { +#ifdef DEBUG_LCMS + g_message(" channel %d", match); +#endif // DEBUG_LCMS + } + + + cmsUInt16Number tmp[4]; + for (guint i = 0; i < 4; i++) { + tmp[i] = ColorScales::getScaled(iccSelector->_impl->_compUI[i]._adj) * 0x0ffff; + } + guchar post[4] = { 0, 0, 0, 0 }; + + cmsHTRANSFORM trans = iccSelector->_impl->_prof->getTransfToSRGB8(); + if (trans) { + cmsDoTransform(trans, tmp, post, 1); + } + + SPColor other(SP_RGBA32_U_COMPOSE(post[0], post[1], post[2], 255)); + other.icc = new SVGICCColor(); + if (iccSelector->_impl->_color.color().icc) { + other.icc->colorProfile = iccSelector->_impl->_color.color().icc->colorProfile; + } + + guint32 prior = iccSelector->_impl->_color.color().toRGBA32(255); + guint32 newer = other.toRGBA32(255); + + if (prior != newer) { +#ifdef DEBUG_LCMS + g_message("Transformed color from 0x%08x to 0x%08x", prior, newer); + g_message(" ~~~~ FLIP"); +#endif // DEBUG_LCMS + newColor = other; + newColor.icc->colors.clear(); + for (guint i = 0; i < iccSelector->_impl->_profChannelCount; i++) { + gdouble val = ColorScales::getScaled(iccSelector->_impl->_compUI[i]._adj); + val *= iccSelector->_impl->_compUI[i]._component.scale; + if (iccSelector->_impl->_compUI[i]._component.scale == 256) { + val -= 128; + } + newColor.icc->colors.push_back(val); + } + } +#endif // defined(HAVE_LIBLCMS2) + } + iccSelector->_impl->_color.setColorAlpha(newColor, scaled); + // iccSelector->_updateInternals( newColor, scaled, iccSelector->_impl->_dragging ); + iccSelector->_impl->_updateSliders(match); + + iccSelector->_impl->_updating = FALSE; +#ifdef DEBUG_LCMS + g_message("\\_________ %p::_adjustmentChanged()", cs); +#endif // DEBUG_LCMS +} + +void ColorICCSelectorImpl::_sliderGrabbed() +{ + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + // if (!iccSelector->_dragging) { + // iccSelector->_dragging = TRUE; + // iccSelector->_grabbed(); + // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_impl->_adj ), + // iccSelector->_dragging ); + // } +} + +void ColorICCSelectorImpl::_sliderReleased() +{ + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + // if (iccSelector->_dragging) { + // iccSelector->_dragging = FALSE; + // iccSelector->_released(); + // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_adj ), + // iccSelector->_dragging ); + // } +} + +#ifdef DEBUG_LCMS +void ColorICCSelectorImpl::_sliderChanged(SPColorSlider *slider, SPColorICCSelector *cs) +#else +void ColorICCSelectorImpl::_sliderChanged() +#endif // DEBUG_LCMS +{ +#ifdef DEBUG_LCMS + g_message("Changed %p and %p", slider, cs); +#endif // DEBUG_LCMS + // ColorICCSelector* iccSelector = dynamic_cast<ColorICCSelector*>(SP_COLOR_SELECTOR(cs)->base); + + // iccSelector->_updateInternals( iccSelector->_color, ColorScales::getScaled( iccSelector->_adj ), + // iccSelector->_dragging ); +} + +Gtk::Widget *ColorICCSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const +{ + Gtk::Widget *w = Gtk::manage(new ColorICCSelector(color)); + return w; +} + +Glib::ustring ColorICCSelectorFactory::modeName() const { return gettext(ColorICCSelector::MODE_NAME); } +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-icc-selector.h b/src/ui/widget/color-icc-selector.h new file mode 100644 index 0000000..2c5ec41 --- /dev/null +++ b/src/ui/widget/color-icc-selector.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_ICC_SELECTOR_H +#define SEEN_SP_COLOR_ICC_SELECTOR_H + +#include <gtkmm/widget.h> +#include <gtkmm/grid.h> + +#include "ui/selected-color.h" + +namespace Inkscape { + +class ColorProfile; + +namespace UI { +namespace Widget { + +class ColorICCSelectorImpl; + +class ColorICCSelector + : public Gtk::Grid + { + public: + static const gchar *MODE_NAME; + + ColorICCSelector(SelectedColor &color); + ~ColorICCSelector() override; + + virtual void init(); + + protected: + void on_show() override; + + virtual void _colorChanged(); + + void _recalcColor(gboolean changing); + + private: + friend class ColorICCSelectorImpl; + + // By default, disallow copy constructor and assignment operator + ColorICCSelector(const ColorICCSelector &obj); + ColorICCSelector &operator=(const ColorICCSelector &obj); + + ColorICCSelectorImpl *_impl; +}; + + +class ColorICCSelectorFactory : public ColorSelectorFactory { + public: + Gtk::Widget *createWidget(SelectedColor &color) const override; + Glib::ustring modeName() const override; +}; +} +} +} +#endif // SEEN_SP_COLOR_ICC_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-notebook.cpp b/src/ui/widget/color-notebook.cpp new file mode 100644 index 0000000..474b4d2 --- /dev/null +++ b/src/ui/widget/color-notebook.cpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages + *//* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification) + * + * Copyright (C) 2001-2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#undef SPCS_PREVIEW +#define noDUMP_CHANGE_INFO + +#include <glibmm/i18n.h> +#include <gtkmm/label.h> +#include <gtkmm/notebook.h> +#include <gtkmm/radiobutton.h> + +#include "cms-system.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" +#include "profile-manager.h" + +#include "object/color-profile.h" +#include "ui/icon-loader.h" + +#include "svg/svg-icc-color.h" + +#include "ui/dialog-events.h" +#include "ui/tools-switch.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/color-entry.h" +#include "ui/widget/color-icc-selector.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-wheel-selector.h" + +#include "widgets/spw-utilities.h" + +using Inkscape::CMSSystem; + +#define XPAD 4 +#define YPAD 1 + +namespace Inkscape { +namespace UI { +namespace Widget { + + +ColorNotebook::ColorNotebook(SelectedColor &color) + : Gtk::Grid() + , _selected_color(color) +{ + set_name("ColorNotebook"); + + Page *page; + + page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_RGB), true); + _available_pages.push_back(page); + page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_HSL), true); + _available_pages.push_back(page); + page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_HSV), true); + _available_pages.push_back(page); + page = new Page(new ColorScalesFactory(SP_COLOR_SCALES_MODE_CMYK), true); + _available_pages.push_back(page); + page = new Page(new ColorWheelSelectorFactory, true); + _available_pages.push_back(page); +#if defined(HAVE_LIBLCMS2) + page = new Page(new ColorICCSelectorFactory, true); + _available_pages.push_back(page); +#endif + + _initUI(); + + _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorNotebook::_onSelectedColorChanged)); +} + +ColorNotebook::~ColorNotebook() +{ + if (_buttons) { + delete[] _buttons; + _buttons = nullptr; + } +} + +ColorNotebook::Page::Page(Inkscape::UI::ColorSelectorFactory *selector_factory, bool enabled_full) + : selector_factory(selector_factory) + , enabled_full(enabled_full) +{ +} + + +void ColorNotebook::_initUI() +{ + guint row = 0; + + Gtk::Notebook *notebook = Gtk::manage(new Gtk::Notebook); + notebook->show(); + notebook->set_show_border(false); + notebook->set_show_tabs(false); + _book = GTK_WIDGET(notebook->gobj()); + + _buttonbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2); + gtk_box_set_homogeneous(GTK_BOX(_buttonbox), TRUE); + + gtk_widget_show(_buttonbox); + _buttons = new GtkWidget *[_available_pages.size()]; + + for (int i = 0; static_cast<size_t>(i) < _available_pages.size(); i++) { + _addPage(_available_pages[i]); + } + + gtk_widget_set_margin_start(_buttonbox, XPAD); + gtk_widget_set_margin_end(_buttonbox, XPAD); + gtk_widget_set_margin_top(_buttonbox, YPAD); + gtk_widget_set_margin_bottom(_buttonbox, YPAD); + gtk_widget_set_hexpand(_buttonbox, TRUE); + gtk_widget_set_valign(_buttonbox, GTK_ALIGN_CENTER); + attach(*Glib::wrap(_buttonbox), 0, row, 2, 1); + + row++; + + gtk_widget_set_margin_start(_book, XPAD * 2); + gtk_widget_set_margin_end(_book, XPAD * 2); + gtk_widget_set_margin_top(_book, YPAD); + gtk_widget_set_margin_bottom(_book, YPAD); + gtk_widget_set_hexpand(_book, TRUE); + gtk_widget_set_vexpand(_book, TRUE); + attach(*notebook, 0, row, 2, 1); + + // restore the last active page + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _setCurrentPage(prefs->getInt("/colorselector/page", 0)); + row++; + + GtkWidget *rgbabox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + +#if defined(HAVE_LIBLCMS2) + /* Create color management icons */ + _box_colormanaged = gtk_event_box_new(); + GtkWidget *colormanaged = sp_get_icon_image("color-management", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_colormanaged), colormanaged); + gtk_widget_set_tooltip_text(_box_colormanaged, _("Color Managed")); + gtk_widget_set_sensitive(_box_colormanaged, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_colormanaged, FALSE, FALSE, 2); + + _box_outofgamut = gtk_event_box_new(); + GtkWidget *outofgamut = sp_get_icon_image("out-of-gamut-icon", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_outofgamut), outofgamut); + gtk_widget_set_tooltip_text(_box_outofgamut, _("Out of gamut!")); + gtk_widget_set_sensitive(_box_outofgamut, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_outofgamut, FALSE, FALSE, 2); + + _box_toomuchink = gtk_event_box_new(); + GtkWidget *toomuchink = sp_get_icon_image("too-much-ink-icon", GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_container_add(GTK_CONTAINER(_box_toomuchink), toomuchink); + gtk_widget_set_tooltip_text(_box_toomuchink, _("Too much ink!")); + gtk_widget_set_sensitive(_box_toomuchink, false); + gtk_box_pack_start(GTK_BOX(rgbabox), _box_toomuchink, FALSE, FALSE, 2); +#endif // defined(HAVE_LIBLCMS2) + + + /* Color picker */ + GtkWidget *picker = sp_get_icon_image("color-picker", GTK_ICON_SIZE_SMALL_TOOLBAR); + _btn_picker = gtk_button_new(); + gtk_button_set_relief(GTK_BUTTON(_btn_picker), GTK_RELIEF_NONE); + gtk_container_add(GTK_CONTAINER(_btn_picker), picker); + gtk_widget_set_tooltip_text(_btn_picker, _("Pick colors from image")); + gtk_box_pack_start(GTK_BOX(rgbabox), _btn_picker, FALSE, FALSE, 2); + g_signal_connect(G_OBJECT(_btn_picker), "clicked", G_CALLBACK(ColorNotebook::_onPickerClicked), this); + + /* Create RGBA entry and color preview */ + _rgbal = gtk_label_new_with_mnemonic(_("RGBA_:")); + gtk_widget_set_halign(_rgbal, GTK_ALIGN_END); + gtk_box_pack_start(GTK_BOX(rgbabox), _rgbal, TRUE, TRUE, 2); + + ColorEntry *rgba_entry = Gtk::manage(new ColorEntry(_selected_color)); + sp_dialog_defocus_on_enter(GTK_WIDGET(rgba_entry->gobj())); + gtk_box_pack_start(GTK_BOX(rgbabox), GTK_WIDGET(rgba_entry->gobj()), FALSE, FALSE, 0); + gtk_label_set_mnemonic_widget(GTK_LABEL(_rgbal), GTK_WIDGET(rgba_entry->gobj())); + + gtk_widget_show_all(rgbabox); + +#if defined(HAVE_LIBLCMS2) + // the "too much ink" icon is initially hidden + gtk_widget_hide(GTK_WIDGET(_box_toomuchink)); +#endif // defined(HAVE_LIBLCMS2) + + gtk_widget_set_margin_start(rgbabox, XPAD); + gtk_widget_set_margin_end(rgbabox, XPAD); + gtk_widget_set_margin_top(rgbabox, YPAD); + gtk_widget_set_margin_bottom(rgbabox, YPAD); + attach(*Glib::wrap(rgbabox), 0, row, 2, 1); + +#ifdef SPCS_PREVIEW + _p = sp_color_preview_new(0xffffffff); + gtk_widget_show(_p); + attach(*Glib::wrap(_p), 2, 3, row, row + 1, Gtk::FILL, Gtk::FILL, XPAD, YPAD); +#endif + + g_signal_connect(G_OBJECT(_book), "switch-page", G_CALLBACK(ColorNotebook::_onPageSwitched), this); +} + +void ColorNotebook::_onPickerClicked(GtkWidget * /*widget*/, ColorNotebook * /*colorbook*/) +{ + // Set the dropper into a "one click" mode, so it reverts to the previous tool after a click + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/tools/dropper/onetimepick", true); + Inkscape::UI::Tools::sp_toggle_dropper(SP_ACTIVE_DESKTOP); +} + +void ColorNotebook::_onButtonClicked(GtkWidget *widget, ColorNotebook *nb) +{ + if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) { + return; + } + + for (gint i = 0; i < gtk_notebook_get_n_pages(GTK_NOTEBOOK(nb->_book)); i++) { + if (nb->_buttons[i] == widget) { + gtk_notebook_set_current_page(GTK_NOTEBOOK(nb->_book), i); + } + } +} + +void ColorNotebook::_onSelectedColorChanged() { _updateICCButtons(); } + +void ColorNotebook::_onPageSwitched(GtkNotebook *notebook, GtkWidget *page, guint page_num, ColorNotebook *colorbook) +{ + if (colorbook->get_visible()) { + // remember the page we switched to + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt("/colorselector/page", page_num); + } +} + + +// TODO pass in param so as to avoid the need for SP_ACTIVE_DOCUMENT +void ColorNotebook::_updateICCButtons() +{ + SPColor color = _selected_color.color(); + gfloat alpha = _selected_color.alpha(); + + g_return_if_fail((0.0 <= alpha) && (alpha <= 1.0)); + +#if defined(HAVE_LIBLCMS2) + /* update color management icon*/ + gtk_widget_set_sensitive(_box_colormanaged, color.icc != nullptr); + + /* update out-of-gamut icon */ + gtk_widget_set_sensitive(_box_outofgamut, false); + if (color.icc) { + Inkscape::ColorProfile *target_profile = + SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str()); + if (target_profile) + gtk_widget_set_sensitive(_box_outofgamut, target_profile->GamutCheck(color)); + } + + /* update too-much-ink icon */ + gtk_widget_set_sensitive(_box_toomuchink, false); + if (color.icc) { + Inkscape::ColorProfile *prof = SP_ACTIVE_DOCUMENT->getProfileManager()->find(color.icc->colorProfile.c_str()); + if (prof && CMSSystem::isPrintColorSpace(prof)) { + gtk_widget_show(GTK_WIDGET(_box_toomuchink)); + double ink_sum = 0; + for (double i : color.icc->colors) { + ink_sum += i; + } + + /* Some literature states that when the sum of paint values exceed 320%, it is considered to be a satured + color, + which means the paper can get too wet due to an excessive amount of ink. This may lead to several + issues + such as misalignment and poor quality of printing in general.*/ + if (ink_sum > 3.2) + gtk_widget_set_sensitive(_box_toomuchink, true); + } + else { + gtk_widget_hide(GTK_WIDGET(_box_toomuchink)); + } + } +#endif // defined(HAVE_LIBLCMS2) +} + +void ColorNotebook::_setCurrentPage(int i) +{ + gtk_notebook_set_current_page(GTK_NOTEBOOK(_book), i); + + if (_buttons && (static_cast<size_t>(i) < _available_pages.size())) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_buttons[i]), TRUE); + } +} + +void ColorNotebook::_addPage(Page &page) +{ + Gtk::Widget *selector_widget; + + selector_widget = page.selector_factory->createWidget(_selected_color); + if (selector_widget) { + selector_widget->show(); + + Glib::ustring mode_name = page.selector_factory->modeName(); + Gtk::Widget *tab_label = Gtk::manage(new Gtk::Label(mode_name)); + tab_label->set_name("ColorModeLabel"); + gint page_num = gtk_notebook_append_page(GTK_NOTEBOOK(_book), selector_widget->gobj(), tab_label->gobj()); + + _buttons[page_num] = gtk_radio_button_new_with_label(nullptr, mode_name.c_str()); + gtk_widget_set_name(_buttons[page_num], "ColorModeButton"); + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(_buttons[page_num]), FALSE); + if (page_num > 0) { + auto g = Glib::wrap(GTK_RADIO_BUTTON(_buttons[0]))->get_group(); + Glib::wrap(GTK_RADIO_BUTTON(_buttons[page_num]))->set_group(g); + } + gtk_widget_show(_buttons[page_num]); + gtk_box_pack_start(GTK_BOX(_buttonbox), _buttons[page_num], TRUE, TRUE, 0); + + g_signal_connect(G_OBJECT(_buttons[page_num]), "clicked", G_CALLBACK(_onButtonClicked), this); + } +} +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-notebook.h b/src/ui/widget/color-notebook.h new file mode 100644 index 0000000..c7bc7b5 --- /dev/null +++ b/src/ui/widget/color-notebook.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A notebook with RGB, CMYK, CMS, HSL, and Wheel pages + *//* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification) + * + * Copyright (C) 2001-2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_NOTEBOOK_H +#define SEEN_SP_COLOR_NOTEBOOK_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <boost/ptr_container/ptr_vector.hpp> +#include <gtkmm/grid.h> +#include <glib.h> + +#include "color.h" +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorNotebook + : public Gtk::Grid +{ +public: + ColorNotebook(SelectedColor &color); + ~ColorNotebook() override; + +protected: + struct Page { + Page(Inkscape::UI::ColorSelectorFactory *selector_factory, bool enabled_full); + + Inkscape::UI::ColorSelectorFactory *selector_factory; + bool enabled_full; + }; + + virtual void _initUI(); + void _addPage(Page &page); + + static void _onButtonClicked(GtkWidget *widget, ColorNotebook *colorbook); + static void _onPickerClicked(GtkWidget *widget, ColorNotebook *colorbook); + static void _onPageSwitched(GtkNotebook *notebook, GtkWidget *page, guint page_num, ColorNotebook *colorbook); + virtual void _onSelectedColorChanged(); + + void _updateICCButtons(); + void _setCurrentPage(int i); + + Inkscape::UI::SelectedColor &_selected_color; + gulong _entryId; + GtkWidget *_book; + GtkWidget *_buttonbox; + GtkWidget **_buttons; + GtkWidget *_rgbal; /* RGBA entry */ +#if defined(HAVE_LIBLCMS2) + GtkWidget *_box_outofgamut, *_box_colormanaged, *_box_toomuchink; +#endif // defined(HAVE_LIBLCMS2) + GtkWidget *_btn_picker; + GtkWidget *_p; /* Color preview */ + boost::ptr_vector<Page> _available_pages; + +private: + // By default, disallow copy constructor and assignment operator + ColorNotebook(const ColorNotebook &obj) = delete; + ColorNotebook &operator=(const ColorNotebook &obj) = delete; +}; +} +} +} +#endif // SEEN_SP_COLOR_NOTEBOOK_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/widget/color-picker.cpp b/src/ui/widget/color-picker.cpp new file mode 100644 index 0000000..5beb090 --- /dev/null +++ b/src/ui/widget/color-picker.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Ralf Stephan <ralf@ark.in-berlin.de> + * Abhishek Sharma + * + * Copyright (C) Authors 2000-2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "color-picker.h" +#include "inkscape.h" +#include "desktop.h" +#include "document.h" +#include "document-undo.h" +#include "ui/dialog-events.h" + +#include "ui/widget/color-notebook.h" +#include "verbs.h" + + +static bool _in_use = false; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorPicker::ColorPicker (const Glib::ustring& title, const Glib::ustring& tip, + guint32 rgba, bool undo) + : _preview(rgba), _title(title), _rgba(rgba), _undo(undo), + _colorSelectorDialog("dialogs.colorpickerwindow") +{ + setupDialog(title); + _preview.show(); + add (_preview); + set_tooltip_text (tip); + _selected_color.signal_changed.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); + _selected_color.signal_released.connect(sigc::mem_fun(this, &ColorPicker::_onSelectedColorChanged)); +} + +ColorPicker::~ColorPicker() +{ + closeWindow(); +} + +void ColorPicker::setupDialog(const Glib::ustring &title) +{ + GtkWidget *dlg = GTK_WIDGET(_colorSelectorDialog.gobj()); + sp_transientize(dlg); + + _colorSelectorDialog.hide(); + _colorSelectorDialog.set_title (title); + _colorSelectorDialog.set_border_width (4); + + _color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + _colorSelectorDialog.get_content_area()->pack_start ( + *_color_selector, true, true, 0); + _color_selector->show(); +} + +void ColorPicker::setSensitive(bool sensitive) { set_sensitive(sensitive); } + +void ColorPicker::setRgba32 (guint32 rgba) +{ + if (_in_use) return; + + _preview.setRgba32 (rgba); + _rgba = rgba; + if (_color_selector) + { + _updating = true; + _selected_color.setValue(rgba); + _updating = false; + } +} + +void ColorPicker::closeWindow() +{ + _colorSelectorDialog.hide(); +} + +void ColorPicker::on_clicked() +{ + if (_color_selector) + { + _updating = true; + _selected_color.setValue(_rgba); + _updating = false; + } + _colorSelectorDialog.show(); + Glib::RefPtr<Gdk::Window> window = _colorSelectorDialog.get_parent_window(); + if (window) { + window->focus(1); + } +} + +void ColorPicker::on_changed (guint32) +{ +} + +void ColorPicker::_onSelectedColorChanged() { + if (_updating) { + return; + } + + if (_in_use) { + return; + } else { + _in_use = true; + } + + guint32 rgba = _selected_color.value(); + _preview.setRgba32(rgba); + + if (_undo && SP_ACTIVE_DESKTOP) { + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_NONE, + /* TODO: annotate */ "color-picker.cpp:130"); + } + + on_changed(rgba); + _in_use = false; + _changed_signal.emit(rgba); + _rgba = rgba; +} + +}//namespace Widget +}//namespace UI +}//namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-picker.h b/src/ui/widget/color-picker.h new file mode 100644 index 0000000..b98f832 --- /dev/null +++ b/src/ui/widget/color-picker.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Color picker button and window. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) Authors 2000-2005 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __COLOR_PICKER_H__ +#define __COLOR_PICKER_H__ + +#include "labelled.h" + +#include <cstddef> + +#include "ui/selected-color.h" +#include "ui/widget/color-preview.h" +#include <gtkmm/button.h> +#include <gtkmm/dialog.h> +#include <gtkmm/window.h> +#include <sigc++/sigc++.h> + +struct SPColorSelector; + +namespace Inkscape +{ +namespace UI +{ +namespace Widget +{ + + +class ColorPicker : public Gtk::Button { +public: + + ColorPicker (const Glib::ustring& title, + const Glib::ustring& tip, + const guint32 rgba, + bool undo); + + ~ColorPicker() override; + + void setRgba32 (guint32 rgba); + void setSensitive(bool sensitive); + void closeWindow(); + sigc::connection connectChanged (const sigc::slot<void,guint>& slot) + { return _changed_signal.connect (slot); } + +protected: + + void _onSelectedColorChanged(); + void on_clicked() override; + virtual void on_changed (guint32); + + ColorPreview _preview; + + /*const*/ Glib::ustring _title; + sigc::signal<void,guint32> _changed_signal; + guint32 _rgba; + bool _undo; + bool _updating; + + //Dialog + void setupDialog(const Glib::ustring &title); + //Inkscape::UI::Dialog::Dialog _colorSelectorDialog; + Gtk::Dialog _colorSelectorDialog; + SelectedColor _selected_color; + Gtk::Widget *_color_selector; +}; + + +class LabelledColorPicker : public Labelled { +public: + + LabelledColorPicker (const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const guint32 rgba, + bool undo) : Labelled(label, tip, new ColorPicker(title, tip, rgba, undo)) {} + + ~LabelledColorPicker() override + { static_cast<ColorPicker*>(_widget)->~ColorPicker(); } + + void setRgba32 (guint32 rgba) + { static_cast<ColorPicker*>(_widget)->setRgba32 (rgba); } + + void closeWindow() + { static_cast<ColorPicker*>(_widget)->closeWindow (); } + + sigc::connection connectChanged (const sigc::slot<void,guint>& slot) + { return static_cast<ColorPicker*>(_widget)->connectChanged(slot); } +}; + +}//namespace Widget +}//namespace UI +}//namespace Inkscape + +#endif /* !__COLOR_PICKER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-preview.cpp b/src/ui/widget/color-preview.cpp new file mode 100644 index 0000000..ac8fc57 --- /dev/null +++ b/src/ui/widget/color-preview.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2005 Authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/color-preview.h" +#include "display/cairo-utils.h" +#include <cairo.h> + +#define SPCP_DEFAULT_WIDTH 32 +#define SPCP_DEFAULT_HEIGHT 12 + +namespace Inkscape { + namespace UI { + namespace Widget { + +ColorPreview::ColorPreview (guint32 rgba) +{ + _rgba = rgba; + set_has_window(false); + set_name("ColorPreview"); +} + +void +ColorPreview::on_size_allocate (Gtk::Allocation &all) +{ + set_allocation (all); + if (get_is_drawable()) + queue_draw(); +} + +void +ColorPreview::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const +{ + minimum_height = natural_height = SPCP_DEFAULT_HEIGHT; +} + +void +ColorPreview::get_preferred_height_for_width_vfunc(int /* width */, int& minimum_height, int& natural_height) const +{ + minimum_height = natural_height = SPCP_DEFAULT_HEIGHT; +} + +void +ColorPreview::get_preferred_width_vfunc(int& minimum_width, int& natural_width) const +{ + minimum_width = natural_width = SPCP_DEFAULT_WIDTH; +} + +void +ColorPreview::get_preferred_width_for_height_vfunc(int /* height */, int& minimum_width, int& natural_width) const +{ + minimum_width = natural_width = SPCP_DEFAULT_WIDTH; +} + +void +ColorPreview::setRgba32 (guint32 rgba) +{ + _rgba = rgba; + + if (get_is_drawable()) + queue_draw(); +} + +bool +ColorPreview::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) +{ + double x, y, width, height; + const Gtk::Allocation& allocation = get_allocation(); + x = 0; + y = 0; + width = allocation.get_width()/2.0; + height = allocation.get_height(); + + double radius = height / 7.5; + double degrees = M_PI / 180.0; + cairo_new_sub_path (cr->cobj()); + cairo_line_to(cr->cobj(), width, 0); + cairo_line_to(cr->cobj(), width, height); + cairo_arc (cr->cobj(), x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees); + cairo_arc (cr->cobj(), x + radius, y + radius, radius, 180 * degrees, 270 * degrees); + cairo_close_path (cr->cobj()); + + /* Transparent area */ + + cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); + + cairo_set_source(cr->cobj(), checkers); + cr->fill_preserve(); + ink_cairo_set_source_rgba32(cr->cobj(), _rgba); + cr->fill(); + cairo_pattern_destroy(checkers); + + /* Solid area */ + + x = width; + + cairo_new_sub_path (cr->cobj()); + cairo_arc (cr->cobj(), x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees); + cairo_arc (cr->cobj(), x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees); + cairo_line_to(cr->cobj(), x, height); + cairo_line_to(cr->cobj(), x, y); + cairo_close_path (cr->cobj()); + ink_cairo_set_source_rgba32(cr->cobj(), _rgba | 0xff); + cr->fill(); + + return true; +} + +GdkPixbuf* +ColorPreview::toPixbuf (int width, int height) +{ + GdkRectangle carea; + gint w2; + w2 = width / 2; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + /* Transparent area */ + carea.x = 0; + carea.y = 0; + carea.width = w2; + carea.height = height; + + cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); + + cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height); + cairo_set_source(ct, checkers); + cairo_fill_preserve(ct); + ink_cairo_set_source_rgba32(ct, _rgba); + cairo_fill(ct); + + cairo_pattern_destroy(checkers); + + /* Solid area */ + carea.x = w2; + carea.y = 0; + carea.width = width - w2; + carea.height = height; + + cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height); + ink_cairo_set_source_rgba32(ct, _rgba | 0xff); + cairo_fill(ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +}}} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/color-preview.h b/src/ui/widget/color-preview.h new file mode 100644 index 0000000..b789579 --- /dev/null +++ b/src/ui/widget/color-preview.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_COLOR_PREVIEW_H +#define SEEN_COLOR_PREVIEW_H +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2001-2005 Authors + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/widget.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A simple color preview widget, mainly used within a picker button. + */ +class ColorPreview : public Gtk::Widget { +public: + ColorPreview (guint32 rgba); + void setRgba32 (guint32 rgba); + GdkPixbuf* toPixbuf (int width, int height); + +protected: + void on_size_allocate (Gtk::Allocation &all) override; + + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int& minimum_height, int& natural_height) const override; + void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int& minimum_width, int& natural_width) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + + guint32 _rgba; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_COLOR_PREVIEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/color-scales.cpp b/src/ui/widget/color-scales.cpp new file mode 100644 index 0000000..a93ab2a --- /dev/null +++ b/src/ui/widget/color-scales.cpp @@ -0,0 +1,736 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/adjustment.h> +#include <glibmm/i18n.h> + +#include "ui/dialog-events.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" + +#define CSC_CHANNEL_R (1 << 0) +#define CSC_CHANNEL_G (1 << 1) +#define CSC_CHANNEL_B (1 << 2) +#define CSC_CHANNEL_A (1 << 3) +#define CSC_CHANNEL_H (1 << 0) +#define CSC_CHANNEL_S (1 << 1) +#define CSC_CHANNEL_V (1 << 2) +#define CSC_CHANNEL_C (1 << 0) +#define CSC_CHANNEL_M (1 << 1) +#define CSC_CHANNEL_Y (1 << 2) +#define CSC_CHANNEL_K (1 << 3) +#define CSC_CHANNEL_CMYKA (1 << 4) + +#define CSC_CHANNELS_ALL 0 + +#define XPAD 4 +#define YPAD 1 + +#define noDUMP_CHANGE_INFO 1 + +namespace Inkscape { +namespace UI { +namespace Widget { + + +static const gchar *sp_color_scales_hue_map(); + +const gchar *ColorScales::SUBMODE_NAMES[] = { N_("None"), N_("RGB"), N_("HSL"), N_("CMYK"), N_("HSV") }; + +ColorScales::ColorScales(SelectedColor &color, SPColorScalesMode mode) + : Gtk::Grid() + , _color(color) + , _rangeLimit(255.0) + , _updating(FALSE) + , _dragging(FALSE) + , _mode(SP_COLOR_SCALES_MODE_NONE) +{ + for (gint i = 0; i < 5; i++) { + _l[i] = nullptr; + _a[i] = nullptr; + _s[i] = nullptr; + _b[i] = nullptr; + } + + _initUI(mode); + + _color.signal_changed.connect(sigc::mem_fun(this, &ColorScales::_onColorChanged)); + _color.signal_dragged.connect(sigc::mem_fun(this, &ColorScales::_onColorChanged)); +} + +ColorScales::~ColorScales() +{ + for (gint i = 0; i < 5; i++) { + _l[i] = nullptr; + _a[i] = nullptr; + _s[i] = nullptr; + _b[i] = nullptr; + } +} + +void ColorScales::_initUI(SPColorScalesMode mode) +{ + gint i; + + _updating = FALSE; + _dragging = FALSE; + + GtkWidget *t = GTK_WIDGET(gobj()); + + /* Create components */ + for (i = 0; i < static_cast<gint>(G_N_ELEMENTS(_a)); i++) { + /* Label */ + _l[i] = gtk_label_new(""); + + gtk_widget_set_halign(_l[i], GTK_ALIGN_START); + gtk_widget_show(_l[i]); + + gtk_widget_set_margin_start(_l[i], XPAD); + gtk_widget_set_margin_end(_l[i], XPAD); + gtk_widget_set_margin_top(_l[i], YPAD); + gtk_widget_set_margin_bottom(_l[i], YPAD); + gtk_grid_attach(GTK_GRID(t), _l[i], 0, i, 1, 1); + + /* Adjustment */ + _a[i] = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, _rangeLimit, 1.0, 10.0, 10.0)); + /* Slider */ + _s[i] = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(Glib::wrap(_a[i], true))); + _s[i]->show(); + + _s[i]->set_margin_start(XPAD); + _s[i]->set_margin_end(XPAD); + _s[i]->set_margin_top(YPAD); + _s[i]->set_margin_bottom(YPAD); + _s[i]->set_hexpand(true); + gtk_grid_attach(GTK_GRID(t), _s[i]->gobj(), 1, i, 1, 1); + + /* Spinbutton */ + _b[i] = gtk_spin_button_new(GTK_ADJUSTMENT(_a[i]), 1.0, 0); + sp_dialog_defocus_on_enter(_b[i]); + gtk_label_set_mnemonic_widget(GTK_LABEL(_l[i]), _b[i]); + gtk_widget_show(_b[i]); + + gtk_widget_set_margin_start(_b[i], XPAD); + gtk_widget_set_margin_end(_b[i], XPAD); + gtk_widget_set_margin_top(_b[i], YPAD); + gtk_widget_set_margin_bottom(_b[i], YPAD); + gtk_widget_set_halign(_b[i], GTK_ALIGN_END); + gtk_widget_set_valign(_b[i], GTK_ALIGN_CENTER); + gtk_grid_attach(GTK_GRID(t), _b[i], 2, i, 1, 1); + + /* Attach channel value to adjustment */ + g_object_set_data(G_OBJECT(_a[i]), "channel", GINT_TO_POINTER(i)); + /* Signals */ + g_signal_connect(G_OBJECT(_a[i]), "value_changed", G_CALLBACK(_adjustmentAnyChanged), this); + _s[i]->signal_grabbed.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyGrabbed)); + _s[i]->signal_released.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyReleased)); + _s[i]->signal_value_changed.connect(sigc::mem_fun(this, &ColorScales::_sliderAnyChanged)); + } + + //Prevent 5th bar from being shown by PanelDialog::show_all_children + gtk_widget_set_no_show_all(_l[4], TRUE); + _s[4]->set_no_show_all(true); + gtk_widget_set_no_show_all(_b[4], TRUE); + + /* Initial mode is none, so it works */ + setMode(mode); +} + +void ColorScales::_recalcColor() +{ + SPColor color; + gfloat alpha = 1.0; + gfloat c[5]; + + switch (_mode) { + case SP_COLOR_SCALES_MODE_RGB: + case SP_COLOR_SCALES_MODE_HSL: + case SP_COLOR_SCALES_MODE_HSV: + _getRgbaFloatv(c); + color.set(c[0], c[1], c[2]); + alpha = c[3]; + break; + case SP_COLOR_SCALES_MODE_CMYK: { + _getCmykaFloatv(c); + + float rgb[3]; + SPColor::cmyk_to_rgb_floatv(rgb, c[0], c[1], c[2], c[3]); + color.set(rgb[0], rgb[1], rgb[2]); + alpha = c[4]; + break; + } + default: + g_warning("file %s: line %d: Illegal color selector mode %d", __FILE__, __LINE__, _mode); + break; + } + + _color.preserveICC(); + _color.setColorAlpha(color, alpha); +} + +void ColorScales::_updateDisplay() +{ +#ifdef DUMP_CHANGE_INFO + g_message("ColorScales::_onColorChanged( this=%p, %f, %f, %f, %f)", this, _color.color().v.c[0], + _color.color().v.c[1], _color.color().v.c[2], _color.alpha()); +#endif + gfloat tmp[3]; + gfloat c[5] = { 0.0, 0.0, 0.0, 0.0 }; + + SPColor color = _color.color(); + + switch (_mode) { + case SP_COLOR_SCALES_MODE_RGB: + color.get_rgb_floatv(c); + c[3] = _color.alpha(); + c[4] = 0.0; + break; + case SP_COLOR_SCALES_MODE_HSL: + color.get_rgb_floatv(tmp); + SPColor::rgb_to_hsl_floatv(c, tmp[0], tmp[1], tmp[2]); + c[3] = _color.alpha(); + c[4] = 0.0; + break; + case SP_COLOR_SCALES_MODE_HSV: + color.get_rgb_floatv(tmp); + SPColor::rgb_to_hsv_floatv(c, tmp[0], tmp[1], tmp[2]); + c[3] = _color.alpha(); + c[4] = 0.0; + break; + case SP_COLOR_SCALES_MODE_CMYK: + color.get_cmyk_floatv(c); + c[4] = _color.alpha(); + break; + default: + g_warning("file %s: line %d: Illegal color selector mode %d", __FILE__, __LINE__, _mode); + break; + } + + _updating = TRUE; + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], c[3]); + setScaled(_a[4], c[4]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = FALSE; +} + +/* Helpers for setting color value */ +gfloat ColorScales::getScaled(const GtkAdjustment *a) +{ + gfloat val = gtk_adjustment_get_value(const_cast<GtkAdjustment *>(a)) / + gtk_adjustment_get_upper(const_cast<GtkAdjustment *>(a)); + return val; +} + +void ColorScales::setScaled(GtkAdjustment *a, gfloat v, bool constrained) +{ + gdouble upper = gtk_adjustment_get_upper(a); + gfloat val = v * upper; + if (constrained) { + // TODO: do we want preferences for these? + if (upper == 255) { + val = round(val/16) * 16; + } else { + val = round(val/10) * 10; + } + } + gtk_adjustment_set_value(a, val); +} + +void ColorScales::_setRangeLimit(gdouble upper) +{ + _rangeLimit = upper; + for (auto & i : _a) { + gtk_adjustment_set_upper(i, upper); + } +} + +void ColorScales::_onColorChanged() +{ + if (!get_visible()) { + return; + } + _updateDisplay(); +} + +void ColorScales::on_show() +{ + Gtk::Grid::on_show(); + _updateDisplay(); +} + +void ColorScales::_getRgbaFloatv(gfloat *rgba) +{ + g_return_if_fail(rgba != nullptr); + + switch (_mode) { + case SP_COLOR_SCALES_MODE_RGB: + rgba[0] = getScaled(_a[0]); + rgba[1] = getScaled(_a[1]); + rgba[2] = getScaled(_a[2]); + rgba[3] = getScaled(_a[3]); + break; + case SP_COLOR_SCALES_MODE_HSL: + SPColor::hsl_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + rgba[3] = getScaled(_a[3]); + break; + case SP_COLOR_SCALES_MODE_HSV: + SPColor::hsv_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + rgba[3] = getScaled(_a[3]); + break; + case SP_COLOR_SCALES_MODE_CMYK: + SPColor::cmyk_to_rgb_floatv(rgba, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3])); + rgba[3] = getScaled(_a[4]); + break; + default: + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + break; + } +} + +void ColorScales::_getCmykaFloatv(gfloat *cmyka) +{ + gfloat rgb[3]; + + g_return_if_fail(cmyka != nullptr); + + switch (_mode) { + case SP_COLOR_SCALES_MODE_RGB: + SPColor::rgb_to_cmyk_floatv(cmyka, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + cmyka[4] = getScaled(_a[3]); + break; + case SP_COLOR_SCALES_MODE_HSL: + SPColor::hsl_to_rgb_floatv(rgb, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + SPColor::rgb_to_cmyk_floatv(cmyka, rgb[0], rgb[1], rgb[2]); + cmyka[4] = getScaled(_a[3]); + break; + case SP_COLOR_SCALES_MODE_CMYK: + cmyka[0] = getScaled(_a[0]); + cmyka[1] = getScaled(_a[1]); + cmyka[2] = getScaled(_a[2]); + cmyka[3] = getScaled(_a[3]); + cmyka[4] = getScaled(_a[4]); + break; + default: + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + break; + } +} + +guint32 ColorScales::_getRgba32() +{ + gfloat c[4]; + guint32 rgba; + + _getRgbaFloatv(c); + + rgba = SP_RGBA32_F_COMPOSE(c[0], c[1], c[2], c[3]); + + return rgba; +} + +void ColorScales::setMode(SPColorScalesMode mode) +{ + gfloat rgba[4]; + gfloat c[4]; + + if (_mode == mode) + return; + + if ((_mode == SP_COLOR_SCALES_MODE_RGB) || (_mode == SP_COLOR_SCALES_MODE_HSL) || + (_mode == SP_COLOR_SCALES_MODE_CMYK) || (_mode == SP_COLOR_SCALES_MODE_HSV)) { + _getRgbaFloatv(rgba); + } + else { + rgba[0] = rgba[1] = rgba[2] = rgba[3] = 1.0; + } + + _mode = mode; + + switch (mode) { + case SP_COLOR_SCALES_MODE_RGB: + _setRangeLimit(255.0); + gtk_adjustment_set_upper(_a[3], 100.0); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_R:")); + _s[0]->set_tooltip_text(_("Red")); + gtk_widget_set_tooltip_text(_b[0], _("Red")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_G:")); + _s[1]->set_tooltip_text(_("Green")); + gtk_widget_set_tooltip_text(_b[1], _("Green")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_B:")); + _s[2]->set_tooltip_text(_("Blue")); + gtk_widget_set_tooltip_text(_b[2], _("Blue")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)")); + _s[0]->setMap(nullptr); + gtk_widget_hide(_l[4]); + _s[4]->hide(); + gtk_widget_hide(_b[4]); + _updating = TRUE; + setScaled(_a[0], rgba[0]); + setScaled(_a[1], rgba[1]); + setScaled(_a[2], rgba[2]); + setScaled(_a[3], rgba[3]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = FALSE; + break; + case SP_COLOR_SCALES_MODE_HSL: + _setRangeLimit(100.0); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_H:")); + _s[0]->set_tooltip_text(_("Hue")); + gtk_widget_set_tooltip_text(_b[0], _("Hue")); + gtk_adjustment_set_upper(_a[0], 360.0); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_S:")); + _s[1]->set_tooltip_text(_("Saturation")); + gtk_widget_set_tooltip_text(_b[1], _("Saturation")); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_L:")); + _s[2]->set_tooltip_text(_("Lightness")); + gtk_widget_set_tooltip_text(_b[2], _("Lightness")); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)")); + _s[0]->setMap((guchar *)(sp_color_scales_hue_map())); + gtk_widget_hide(_l[4]); + _s[4]->hide(); + gtk_widget_hide(_b[4]); + _updating = TRUE; + c[0] = 0.0; + + SPColor::rgb_to_hsl_floatv(c, rgba[0], rgba[1], rgba[2]); + + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], rgba[3]); + + _updateSliders(CSC_CHANNELS_ALL); + _updating = FALSE; + break; + case SP_COLOR_SCALES_MODE_HSV: + _setRangeLimit(100.0); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_H:")); + _s[0]->set_tooltip_text(_("Hue")); + gtk_widget_set_tooltip_text(_b[0], _("Hue")); + gtk_adjustment_set_upper(_a[0], 360.0); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_S:")); + _s[1]->set_tooltip_text(_("Saturation")); + gtk_widget_set_tooltip_text(_b[1], _("Saturation")); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_V:")); + _s[2]->set_tooltip_text(_("Value")); + gtk_widget_set_tooltip_text(_b[2], _("Value")); + + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_A:")); + _s[3]->set_tooltip_text(_("Alpha (opacity)")); + gtk_widget_set_tooltip_text(_b[3], _("Alpha (opacity)")); + _s[0]->setMap((guchar *)(sp_color_scales_hue_map())); + gtk_widget_hide(_l[4]); + _s[4]->hide(); + gtk_widget_hide(_b[4]); + _updating = TRUE; + c[0] = 0.0; + + SPColor::rgb_to_hsv_floatv(c, rgba[0], rgba[1], rgba[2]); + + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], rgba[3]); + + _updateSliders(CSC_CHANNELS_ALL); + _updating = FALSE; + break; + case SP_COLOR_SCALES_MODE_CMYK: + _setRangeLimit(100.0); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[0]), _("_C:")); + _s[0]->set_tooltip_text(_("Cyan")); + gtk_widget_set_tooltip_text(_b[0], _("Cyan")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[1]), _("_M:")); + _s[1]->set_tooltip_text(_("Magenta")); + gtk_widget_set_tooltip_text(_b[1], _("Magenta")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[2]), _("_Y:")); + _s[2]->set_tooltip_text(_("Yellow")); + gtk_widget_set_tooltip_text(_b[2], _("Yellow")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[3]), _("_K:")); + _s[3]->set_tooltip_text(_("Black")); + gtk_widget_set_tooltip_text(_b[3], _("Black")); + gtk_label_set_markup_with_mnemonic(GTK_LABEL(_l[4]), _("_A:")); + _s[4]->set_tooltip_text(_("Alpha (opacity)")); + gtk_widget_set_tooltip_text(_b[4], _("Alpha (opacity)")); + _s[0]->setMap(nullptr); + gtk_widget_show(_l[4]); + _s[4]->show(); + gtk_widget_show(_b[4]); + _updating = TRUE; + + SPColor::rgb_to_cmyk_floatv(c, rgba[0], rgba[1], rgba[2]); + setScaled(_a[0], c[0]); + setScaled(_a[1], c[1]); + setScaled(_a[2], c[2]); + setScaled(_a[3], c[3]); + + setScaled(_a[4], rgba[3]); + _updateSliders(CSC_CHANNELS_ALL); + _updating = FALSE; + break; + default: + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + break; + } +} + +SPColorScalesMode ColorScales::getMode() const { return _mode; } + +void ColorScales::_adjustmentAnyChanged(GtkAdjustment *adjustment, ColorScales *cs) +{ + gint channel = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(adjustment), "channel")); + + _adjustmentChanged(cs, channel); +} + +void ColorScales::_sliderAnyGrabbed() +{ + if (_updating) { + return; + } + if (!_dragging) { + _dragging = TRUE; + _color.setHeld(true); + } +} + +void ColorScales::_sliderAnyReleased() +{ + if (_updating) { + return; + } + if (_dragging) { + _dragging = FALSE; + _color.setHeld(false); + } +} + +void ColorScales::_sliderAnyChanged() +{ + if (_updating) { + return; + } + _recalcColor(); +} + +void ColorScales::_adjustmentChanged(ColorScales *scales, guint channel) +{ + if (scales->_updating) { + return; + } + + scales->_updateSliders((1 << channel)); + scales->_recalcColor(); +} + +void ColorScales::_updateSliders(guint channels) +{ + gfloat rgb0[3], rgbm[3], rgb1[3]; +#ifdef SPCS_PREVIEW + guint32 rgba; +#endif + switch (_mode) { + case SP_COLOR_SCALES_MODE_RGB: + if ((channels != CSC_CHANNEL_R) && (channels != CSC_CHANNEL_A)) { + /* Update red */ + _s[0]->setColors(SP_RGBA32_F_COMPOSE(0.0, getScaled(_a[1]), getScaled(_a[2]), 1.0), + SP_RGBA32_F_COMPOSE(0.5, getScaled(_a[1]), getScaled(_a[2]), 1.0), + SP_RGBA32_F_COMPOSE(1.0, getScaled(_a[1]), getScaled(_a[2]), 1.0)); + } + if ((channels != CSC_CHANNEL_G) && (channels != CSC_CHANNEL_A)) { + /* Update green */ + _s[1]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 0.0, getScaled(_a[2]), 1.0), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 0.5, getScaled(_a[2]), 1.0), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), 1.0, getScaled(_a[2]), 1.0)); + } + if ((channels != CSC_CHANNEL_B) && (channels != CSC_CHANNEL_A)) { + /* Update blue */ + _s[2]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 0.0, 1.0), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 0.5, 1.0), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), 1.0, 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + _s[3]->setColors(SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.5), + SP_RGBA32_F_COMPOSE(getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 1.0)); + } + break; + case SP_COLOR_SCALES_MODE_HSL: + /* Hue is never updated */ + if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + /* Update saturation */ + SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2])); + SPColor::hsl_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2])); + SPColor::hsl_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2])); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + /* Update value */ + SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0); + SPColor::hsl_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5); + SPColor::hsl_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + SPColor::hsl_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + break; + case SP_COLOR_SCALES_MODE_HSV: + /* Hue is never updated */ + if ((channels != CSC_CHANNEL_S) && (channels != CSC_CHANNEL_A)) { + /* Update saturation */ + SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2])); + SPColor::hsv_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2])); + SPColor::hsv_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2])); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_V) && (channels != CSC_CHANNEL_A)) { + /* Update value */ + SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0); + SPColor::hsv_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5); + SPColor::hsv_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_A) { + /* Update alpha */ + SPColor::hsv_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2])); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + break; + case SP_COLOR_SCALES_MODE_CMYK: + if ((channels != CSC_CHANNEL_C) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update C */ + SPColor::cmyk_to_rgb_floatv(rgb0, 0.0, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgbm, 0.5, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgb1, 1.0, getScaled(_a[1]), getScaled(_a[2]), getScaled(_a[3])); + _s[0]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_M) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update M */ + SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), 0.0, getScaled(_a[2]), getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), 0.5, getScaled(_a[2]), getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), 1.0, getScaled(_a[2]), getScaled(_a[3])); + _s[1]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_Y) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update Y */ + SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), 0.0, getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), 0.5, getScaled(_a[3])); + SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), 1.0, getScaled(_a[3])); + _s[2]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if ((channels != CSC_CHANNEL_K) && (channels != CSC_CHANNEL_CMYKA)) { + /* Update K */ + SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.0); + SPColor::cmyk_to_rgb_floatv(rgbm, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 0.5); + SPColor::cmyk_to_rgb_floatv(rgb1, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), 1.0); + _s[3]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0), + SP_RGBA32_F_COMPOSE(rgbm[0], rgbm[1], rgbm[2], 1.0), + SP_RGBA32_F_COMPOSE(rgb1[0], rgb1[1], rgb1[2], 1.0)); + } + if (channels != CSC_CHANNEL_CMYKA) { + /* Update alpha */ + SPColor::cmyk_to_rgb_floatv(rgb0, getScaled(_a[0]), getScaled(_a[1]), getScaled(_a[2]), + getScaled(_a[3])); + _s[4]->setColors(SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.0), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 0.5), + SP_RGBA32_F_COMPOSE(rgb0[0], rgb0[1], rgb0[2], 1.0)); + } + break; + default: + g_warning("file %s: line %d: Illegal color selector mode", __FILE__, __LINE__); + break; + } + +#ifdef SPCS_PREVIEW + rgba = sp_color_scales_get_rgba32(cs); + sp_color_preview_set_rgba32(SP_COLOR_PREVIEW(_p), rgba); +#endif +} + +static const gchar *sp_color_scales_hue_map() +{ + static gchar *map = nullptr; + + if (!map) { + gchar *p; + gint h; + map = g_new(gchar, 4 * 1024); + p = map; + for (h = 0; h < 1024; h++) { + gfloat rgb[3]; + SPColor::hsl_to_rgb_floatv(rgb, h / 1024.0, 1.0, 0.5); + *p++ = SP_COLOR_F_TO_U(rgb[0]); + *p++ = SP_COLOR_F_TO_U(rgb[1]); + *p++ = SP_COLOR_F_TO_U(rgb[2]); + *p++ = 0xFF; + } + } + + return map; +} + +ColorScalesFactory::ColorScalesFactory(SPColorScalesMode submode) + : _submode(submode) +{ +} + +ColorScalesFactory::~ColorScalesFactory() = default; + +Gtk::Widget *ColorScalesFactory::createWidget(Inkscape::UI::SelectedColor &color) const +{ + Gtk::Widget *w = Gtk::manage(new ColorScales(color, _submode)); + return w; +} + +Glib::ustring ColorScalesFactory::modeName() const { + return gettext(ColorScales::SUBMODE_NAMES[_submode]); +} + +} +} +} diff --git a/src/ui/widget/color-scales.h b/src/ui/widget/color-scales.h new file mode 100644 index 0000000..f3007fd --- /dev/null +++ b/src/ui/widget/color-scales.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_SCALES_H +#define SEEN_SP_COLOR_SCALES_H + +#include <gtkmm/grid.h> + +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorSlider; + +enum SPColorScalesMode { + SP_COLOR_SCALES_MODE_NONE = 0, + SP_COLOR_SCALES_MODE_RGB = 1, + SP_COLOR_SCALES_MODE_HSL = 2, + SP_COLOR_SCALES_MODE_CMYK = 3, + SP_COLOR_SCALES_MODE_HSV = 4 +}; + +class ColorScales + : public Gtk::Grid +{ +public: + static const gchar *SUBMODE_NAMES[]; + + static gfloat getScaled(const GtkAdjustment *a); + static void setScaled(GtkAdjustment *a, gfloat v, bool constrained = false); + + ColorScales(SelectedColor &color, SPColorScalesMode mode); + ~ColorScales() override; + + virtual void _initUI(SPColorScalesMode mode); + + void setMode(SPColorScalesMode mode); + SPColorScalesMode getMode() const; + +protected: + void _onColorChanged(); + void on_show() override; + + static void _adjustmentAnyChanged(GtkAdjustment *adjustment, ColorScales *cs); + void _sliderAnyGrabbed(); + void _sliderAnyReleased(); + void _sliderAnyChanged(); + static void _adjustmentChanged(ColorScales *cs, guint channel); + + void _getRgbaFloatv(gfloat *rgba); + void _getCmykaFloatv(gfloat *cmyka); + guint32 _getRgba32(); + void _updateSliders(guint channels); + void _recalcColor(); + void _updateDisplay(); + + void _setRangeLimit(gdouble upper); + + SelectedColor &_color; + SPColorScalesMode _mode; + gdouble _rangeLimit; + gboolean _updating : 1; + gboolean _dragging : 1; + GtkAdjustment *_a[5]; /* Channel adjustments */ + Inkscape::UI::Widget::ColorSlider *_s[5]; /* Channel sliders */ + GtkWidget *_b[5]; /* Spinbuttons */ + GtkWidget *_l[5]; /* Labels */ + +private: + // By default, disallow copy constructor and assignment operator + ColorScales(ColorScales const &obj) = delete; + ColorScales &operator=(ColorScales const &obj) = delete; +}; + +class ColorScalesFactory : public Inkscape::UI::ColorSelectorFactory +{ +public: + ColorScalesFactory(SPColorScalesMode submode); + ~ColorScalesFactory() override; + + Gtk::Widget *createWidget(Inkscape::UI::SelectedColor &color) const override; + Glib::ustring modeName() const override; + +private: + SPColorScalesMode _submode; +}; + +} +} +} + +#endif /* !SEEN_SP_COLOR_SCALES_H */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/color-slider.cpp b/src/ui/widget/color-slider.cpp new file mode 100644 index 0000000..2d19055 --- /dev/null +++ b/src/ui/widget/color-slider.cpp @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A slider with colored background - implementation. + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdkmm/cursor.h> +#include <gdkmm/general.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/stylecontext.h> + +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" +#include "preferences.h" + +static const gint SLIDER_WIDTH = 96; +static const gint SLIDER_HEIGHT = 8; +static const gint ARROW_SIZE = 7; + +static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[], + guint b0, guint b1, guint mask); +static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start, + gint step, guint b0, guint b1, guint mask); + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorSlider::ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment) + : _dragging(false) + , _value(0.0) + , _oldvalue(0.0) + , _mapsize(0) + , _map(nullptr) +{ + _c0[0] = 0x00; + _c0[1] = 0x00; + _c0[2] = 0x00; + _c0[3] = 0xff; + + _cm[0] = 0xff; + _cm[1] = 0x00; + _cm[2] = 0x00; + _cm[3] = 0xff; + + _c0[0] = 0xff; + _c0[1] = 0xff; + _c0[2] = 0xff; + _c0[3] = 0xff; + + _b0 = 0x5f; + _b1 = 0xa0; + _bmask = 0x08; + + setAdjustment(adjustment); +} + +ColorSlider::~ColorSlider() +{ + if (_adjustment) { + _adjustment_changed_connection.disconnect(); + _adjustment_value_changed_connection.disconnect(); + _adjustment.reset(); + } +} + +void ColorSlider::on_realize() +{ + set_realized(); + + if (!_gdk_window) { + GdkWindowAttr attributes; + gint attributes_mask; + Gtk::Allocation allocation = get_allocation(); + + memset(&attributes, 0, sizeof(attributes)); + attributes.x = allocation.get_x(); + attributes.y = allocation.get_y(); + attributes.width = allocation.get_width(); + attributes.height = allocation.get_height(); + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.visual = gdk_screen_get_system_visual(gdk_screen_get_default()); + attributes.event_mask = get_events(); + attributes.event_mask |= (Gdk::EXPOSURE_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL; + + _gdk_window = Gdk::Window::create(get_parent_window(), &attributes, attributes_mask); + set_window(_gdk_window); + _gdk_window->set_user_data(gobj()); + } +} + +void ColorSlider::on_unrealize() +{ + _gdk_window.reset(); + + Gtk::Widget::on_unrealize(); +} + +void ColorSlider::on_size_allocate(Gtk::Allocation &allocation) +{ + set_allocation(allocation); + + if (get_realized()) { + _gdk_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), + allocation.get_height()); + } +} + +void ColorSlider::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border padding = style_context->get_padding(get_state_flags()); + int width = SLIDER_WIDTH + padding.get_left() + padding.get_right(); + minimum_width = natural_width = width; +} + +void ColorSlider::get_preferred_width_for_height_vfunc(int /*height*/, int &minimum_width, int &natural_width) const +{ + get_preferred_width(minimum_width, natural_width); +} + +void ColorSlider::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border padding = style_context->get_padding(get_state_flags()); + int height = SLIDER_HEIGHT + padding.get_top() + padding.get_bottom(); + minimum_height = natural_height = height; +} + +void ColorSlider::get_preferred_height_for_width_vfunc(int /*width*/, int &minimum_height, int &natural_height) const +{ + get_preferred_height(minimum_height, natural_height); +} + +bool ColorSlider::on_button_press_event(GdkEventButton *event) +{ + if (event->button == 1) { + Gtk::Allocation allocation = get_allocation(); + gint cx, cw; + cx = get_style_context()->get_padding(get_state_flags()).get_left(); + cw = allocation.get_width() - 2 * cx; + signal_grabbed.emit(); + _dragging = true; + _oldvalue = _value; + gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0); + bool constrained = event->state & GDK_CONTROL_MASK; + ColorScales::setScaled(_adjustment->gobj(), value, constrained); + signal_dragged.emit(); + + auto window = _gdk_window->gobj(); + + auto seat = gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event)); + gdk_seat_grab(seat, + window, + GDK_SEAT_CAPABILITY_ALL_POINTING, + FALSE, + nullptr, + reinterpret_cast<GdkEvent *>(event), + nullptr, + nullptr); + } + + return false; +} + +bool ColorSlider::on_button_release_event(GdkEventButton *event) +{ + if (event->button == 1) { + gdk_seat_ungrab(gdk_event_get_seat(reinterpret_cast<GdkEvent *>(event))); + _dragging = false; + signal_released.emit(); + if (_value != _oldvalue) { + signal_value_changed.emit(); + } + } + + return false; +} + +bool ColorSlider::on_motion_notify_event(GdkEventMotion *event) +{ + if (_dragging) { + gint cx, cw; + Gtk::Allocation allocation = get_allocation(); + cx = get_style_context()->get_padding(get_state_flags()).get_left(); + cw = allocation.get_width() - 2 * cx; + gfloat value = CLAMP((gfloat)(event->x - cx) / cw, 0.0, 1.0); + bool constrained = event->state & GDK_CONTROL_MASK; + ColorScales::setScaled(_adjustment->gobj(), value, constrained); + signal_dragged.emit(); + } + + return false; +} + +void ColorSlider::setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment) +{ + if (!adjustment) { + _adjustment = Gtk::Adjustment::create(0.0, 0.0, 1.0, 0.01, 0.0, 0.0); + } + else { + adjustment->set_page_increment(0.0); + adjustment->set_page_size(0.0); + } + + if (_adjustment != adjustment) { + if (_adjustment) { + _adjustment_changed_connection.disconnect(); + _adjustment_value_changed_connection.disconnect(); + } + + _adjustment = adjustment; + _adjustment_changed_connection = + _adjustment->signal_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentChanged)); + _adjustment_value_changed_connection = + _adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorSlider::_onAdjustmentValueChanged)); + + _value = ColorScales::getScaled(_adjustment->gobj()); + + _onAdjustmentChanged(); + } +} + +void ColorSlider::_onAdjustmentChanged() { queue_draw(); } + +void ColorSlider::_onAdjustmentValueChanged() +{ + if (_value != ColorScales::getScaled(_adjustment->gobj())) { + gint cx, cy, cw, ch; + auto style_context = get_style_context(); + auto allocation = get_allocation(); + auto padding = style_context->get_padding(get_state_flags()); + cx = padding.get_left(); + cy = padding.get_top(); + cw = allocation.get_width() - 2 * cx; + ch = allocation.get_height() - 2 * cy; + if ((gint)(ColorScales::getScaled(_adjustment->gobj()) * cw) != (gint)(_value * cw)) { + gint ax, ay; + gfloat value; + value = _value; + _value = ColorScales::getScaled(_adjustment->gobj()); + ax = (int)(cx + value * cw - ARROW_SIZE / 2 - 2); + ay = cy; + queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); + ax = (int)(cx + _value * cw - ARROW_SIZE / 2 - 2); + ay = cy; + queue_draw_area(ax, ay, ARROW_SIZE + 4, ch); + } + else { + _value = ColorScales::getScaled(_adjustment->gobj()); + } + } +} + +void ColorSlider::setColors(guint32 start, guint32 mid, guint32 end) +{ + // Remove any map, if set + _map = nullptr; + + _c0[0] = start >> 24; + _c0[1] = (start >> 16) & 0xff; + _c0[2] = (start >> 8) & 0xff; + _c0[3] = start & 0xff; + + _cm[0] = mid >> 24; + _cm[1] = (mid >> 16) & 0xff; + _cm[2] = (mid >> 8) & 0xff; + _cm[3] = mid & 0xff; + + _c1[0] = end >> 24; + _c1[1] = (end >> 16) & 0xff; + _c1[2] = (end >> 8) & 0xff; + _c1[3] = end & 0xff; + + queue_draw(); +} + +void ColorSlider::setMap(const guchar *map) +{ + _map = const_cast<guchar *>(map); + + queue_draw(); +} + +void ColorSlider::setBackground(guint dark, guint light, guint size) +{ + _b0 = dark; + _b1 = light; + _bmask = size; + + queue_draw(); +} + +bool ColorSlider::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) +{ + gboolean colorsOnTop = Inkscape::Preferences::get()->getBool("/options/workarounds/colorsontop", false); + + auto allocation = get_allocation(); + auto style_context = get_style_context(); + + // Draw shadow + if (colorsOnTop) { + style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height()); + } + + /* Paintable part of color gradient area */ + Gdk::Rectangle carea; + Gtk::Border padding; + + padding = style_context->get_padding(get_state_flags()); + + carea.set_x(padding.get_left()); + carea.set_y(padding.get_top()); + + carea.set_width(allocation.get_width() - 2 * carea.get_x()); + carea.set_height(allocation.get_height() - 2 * carea.get_y()); + + if (_map) { + /* Render map pixelstore */ + gint d = (1024 << 16) / carea.get_width(); + gint s = 0; + + const guchar *b = + sp_color_slider_render_map(0, 0, carea.get_width(), carea.get_height(), _map, s, d, _b0, _b1, _bmask); + + if (b != nullptr && carea.get_width() > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = Gdk::Pixbuf::create_from_data( + b, Gdk::COLORSPACE_RGB, false, 8, carea.get_width(), carea.get_height(), carea.get_width() * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y()); + cr->paint(); + } + } + else { + gint c[4], dc[4]; + + /* Render gradient */ + + // part 1: from c0 to cm + if (carea.get_width() > 0) { + for (gint i = 0; i < 4; i++) { + c[i] = _c0[i] << 16; + dc[i] = ((_cm[i] << 16) - c[i]) / (carea.get_width() / 2); + } + guint wi = carea.get_width() / 2; + const guchar *b = sp_color_slider_render_gradient(0, 0, wi, carea.get_height(), c, dc, _b0, _b1, _bmask); + + /* Draw pixelstore 1 */ + if (b != nullptr && wi > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = + Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_x(), carea.get_y()); + cr->paint(); + } + } + + // part 2: from cm to c1 + if (carea.get_width() > 0) { + for (gint i = 0; i < 4; i++) { + c[i] = _cm[i] << 16; + dc[i] = ((_c1[i] << 16) - c[i]) / (carea.get_width() / 2); + } + guint wi = carea.get_width() / 2; + const guchar *b = sp_color_slider_render_gradient(carea.get_width() / 2, 0, wi, carea.get_height(), c, dc, + _b0, _b1, _bmask); + + /* Draw pixelstore 2 */ + if (b != nullptr && wi > 0) { + Glib::RefPtr<Gdk::Pixbuf> pb = + Gdk::Pixbuf::create_from_data(b, Gdk::COLORSPACE_RGB, false, 8, wi, carea.get_height(), wi * 3); + + Gdk::Cairo::set_source_pixbuf(cr, pb, carea.get_width() / 2 + carea.get_x(), carea.get_y()); + cr->paint(); + } + } + } + + /* Draw shadow */ + if (!colorsOnTop) { + style_context->render_frame(cr, 0, 0, allocation.get_width(), allocation.get_height()); + } + + /* Draw arrow */ + gint x = (int)(_value * (carea.get_width() - 1) - ARROW_SIZE / 2 + carea.get_x()); + gint y1 = carea.get_y(); + gint y2 = carea.get_y() + carea.get_height() - 1; + cr->set_line_width(1.0); + + // Define top arrow + cr->move_to(x - 0.5, y1 + 0.5); + cr->line_to(x + ARROW_SIZE - 0.5, y1 + 0.5); + cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y1 + ARROW_SIZE / 2.0 + 0.5); + cr->line_to(x - 0.5, y1 + 0.5); + + // Define bottom arrow + cr->move_to(x - 0.5, y2 + 0.5); + cr->line_to(x + ARROW_SIZE - 0.5, y2 + 0.5); + cr->line_to(x + (ARROW_SIZE - 1) / 2.0, y2 - ARROW_SIZE / 2.0 + 0.5); + cr->line_to(x - 0.5, y2 + 0.5); + + // Render both arrows + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->stroke_preserve(); + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->fill(); + + return false; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* Colors are << 16 */ + +static const guchar *sp_color_slider_render_gradient(gint x0, gint y0, gint width, gint height, gint c[], gint dc[], + guint b0, guint b1, guint mask) +{ + static guchar *buf = nullptr; + static gint bs = 0; + guchar *dp; + gint x, y; + guint r, g, b, a; + + if (buf && (bs < width * height)) { + g_free(buf); + buf = nullptr; + } + if (!buf) { + buf = g_new(guchar, width * height * 3); + bs = width * height; + } + + dp = buf; + r = c[0]; + g = c[1]; + b = c[2]; + a = c[3]; + for (x = x0; x < x0 + width; x++) { + gint cr, cg, cb, ca; + guchar *d; + cr = r >> 16; + cg = g >> 16; + cb = b >> 16; + ca = a >> 16; + d = dp; + for (y = y0; y < y0 + height; y++) { + guint bg, fc; + /* Background value */ + bg = ((x & mask) ^ (y & mask)) ? b0 : b1; + fc = (cr - bg) * ca; + d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cg - bg) * ca; + d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cb - bg) * ca; + d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + d += 3 * width; + } + r += dc[0]; + g += dc[1]; + b += dc[2]; + a += dc[3]; + dp += 3; + } + + return buf; +} + +/* Positions are << 16 */ + +static const guchar *sp_color_slider_render_map(gint x0, gint y0, gint width, gint height, guchar *map, gint start, + gint step, guint b0, guint b1, guint mask) +{ + static guchar *buf = nullptr; + static gint bs = 0; + guchar *dp; + gint x, y; + + if (buf && (bs < width * height)) { + g_free(buf); + buf = nullptr; + } + if (!buf) { + buf = g_new(guchar, width * height * 3); + bs = width * height; + } + + dp = buf; + for (x = x0; x < x0 + width; x++) { + gint cr, cg, cb, ca; + guchar *d = dp; + guchar *sp = map + 4 * (start >> 16); + cr = *sp++; + cg = *sp++; + cb = *sp++; + ca = *sp++; + for (y = y0; y < y0 + height; y++) { + guint bg, fc; + /* Background value */ + bg = ((x & mask) ^ (y & mask)) ? b0 : b1; + fc = (cr - bg) * ca; + d[0] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cg - bg) * ca; + d[1] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + fc = (cb - bg) * ca; + d[2] = bg + ((fc + (fc >> 8) + 0x80) >> 8); + d += 3 * width; + } + dp += 3; + start += step; + } + + return buf; +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/ui/widget/color-slider.h b/src/ui/widget/color-slider.h new file mode 100644 index 0000000..6a0834e --- /dev/null +++ b/src/ui/widget/color-slider.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history +* Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_COLOR_SLIDER_H +#define SEEN_COLOR_SLIDER_H + +#include <gtkmm/widget.h> +#include <sigc++/signal.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/* + * A slider with colored background + */ +class ColorSlider : public Gtk::Widget { +public: + ColorSlider(Glib::RefPtr<Gtk::Adjustment> adjustment); + ~ColorSlider() override; + + void setAdjustment(Glib::RefPtr<Gtk::Adjustment> adjustment); + + void setColors(guint32 start, guint32 mid, guint32 end); + + void setMap(const guchar *map); + + void setBackground(guint dark, guint light, guint size); + + sigc::signal<void> signal_grabbed; + sigc::signal<void> signal_dragged; + sigc::signal<void> signal_released; + sigc::signal<void> signal_value_changed; + +protected: + void on_size_allocate(Gtk::Allocation &allocation) override; + void on_realize() override; + void on_unrealize() override; + bool on_button_press_event(GdkEventButton *event) override; + bool on_button_release_event(GdkEventButton *event) override; + bool on_motion_notify_event(GdkEventMotion *event) override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + +private: + void _onAdjustmentChanged(); + void _onAdjustmentValueChanged(); + + bool _dragging; + + Glib::RefPtr<Gtk::Adjustment> _adjustment; + sigc::connection _adjustment_changed_connection; + sigc::connection _adjustment_value_changed_connection; + + gfloat _value; + gfloat _oldvalue; + guchar _c0[4], _cm[4], _c1[4]; + guchar _b0, _b1; + guchar _bmask; + + gint _mapsize; + guchar *_map; + + Glib::RefPtr<Gdk::Window> _gdk_window; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-wheel-selector.cpp b/src/ui/widget/color-wheel-selector.cpp new file mode 100644 index 0000000..9695303 --- /dev/null +++ b/src/ui/widget/color-wheel-selector.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "color-wheel-selector.h" + +#include <glibmm/i18n.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> +#include "ui/dialog-events.h" +#include "ui/widget/color-scales.h" +#include "ui/widget/color-slider.h" +#include "ui/widget/ink-color-wheel.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +#define XPAD 4 +#define YPAD 1 + + +const gchar *ColorWheelSelector::MODE_NAME = N_("Wheel"); + +ColorWheelSelector::ColorWheelSelector(SelectedColor &color) + : Gtk::Grid() + , _color(color) + , _updating(false) + , _wheel(nullptr) + , _slider(nullptr) +{ + set_name("ColorWheelSelector"); + + _initUI(); + _color_changed_connection = color.signal_changed.connect(sigc::mem_fun(this, &ColorWheelSelector::_colorChanged)); + _color_dragged_connection = color.signal_dragged.connect(sigc::mem_fun(this, &ColorWheelSelector::_colorChanged)); +} + +ColorWheelSelector::~ColorWheelSelector() +{ + _color_changed_connection.disconnect(); + _color_dragged_connection.disconnect(); +} + +void ColorWheelSelector::_initUI() +{ + /* Create components */ + gint row = 0; + + _wheel = Gtk::manage(new Inkscape::UI::Widget::ColorWheel()); + _wheel->set_halign(Gtk::ALIGN_FILL); + _wheel->set_valign(Gtk::ALIGN_FILL); + _wheel->set_hexpand(true); + _wheel->set_vexpand(true); + attach(*_wheel, 0, row, 3, 1); + + row++; + + /* Label */ + Gtk::Label *label = Gtk::manage(new Gtk::Label(_("_A:"), true)); + label->set_halign(Gtk::ALIGN_END); + label->set_valign(Gtk::ALIGN_CENTER); + + label->set_margin_start(XPAD); + label->set_margin_end(XPAD); + label->set_margin_top(YPAD); + label->set_margin_bottom(YPAD); + label->set_halign(Gtk::ALIGN_FILL); + label->set_valign(Gtk::ALIGN_FILL); + attach(*label, 0, row, 1, 1); + + /* Adjustment */ + _alpha_adjustment = Gtk::Adjustment::create(0.0, 0.0, 100.0, 1.0, 10.0, 10.0); + + /* Slider */ + _slider = Gtk::manage(new Inkscape::UI::Widget::ColorSlider(_alpha_adjustment)); + _slider->set_tooltip_text(_("Alpha (opacity)")); + + _slider->set_margin_start(XPAD); + _slider->set_margin_end(XPAD); + _slider->set_margin_top(YPAD); + _slider->set_margin_bottom(YPAD); + _slider->set_hexpand(true); + _slider->set_halign(Gtk::ALIGN_FILL); + _slider->set_valign(Gtk::ALIGN_FILL); + attach(*_slider, 1, row, 1, 1); + + _slider->setColors(SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.0), SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 0.5), + SP_RGBA32_F_COMPOSE(1.0, 1.0, 1.0, 1.0)); + + /* Spinbutton */ + auto spin_button = Gtk::manage(new Gtk::SpinButton(_alpha_adjustment, 1.0, 0)); + spin_button->set_tooltip_text(_("Alpha (opacity)")); + sp_dialog_defocus_on_enter(GTK_WIDGET(spin_button->gobj())); + label->set_mnemonic_widget(*spin_button); + + spin_button->set_margin_start(XPAD); + spin_button->set_margin_end(XPAD); + spin_button->set_margin_top(YPAD); + spin_button->set_margin_bottom(YPAD); + spin_button->set_halign(Gtk::ALIGN_CENTER); + spin_button->set_valign(Gtk::ALIGN_CENTER); + attach(*spin_button, 2, row, 1, 1); + + /* Signals */ + _alpha_adjustment->signal_value_changed().connect(sigc::mem_fun(this, &ColorWheelSelector::_adjustmentChanged)); + _slider->signal_grabbed.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderGrabbed)); + _slider->signal_released.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderReleased)); + _slider->signal_value_changed.connect(sigc::mem_fun(*this, &ColorWheelSelector::_sliderChanged)); + _wheel->signal_color_changed().connect(sigc::mem_fun(*this, &ColorWheelSelector::_wheelChanged)); + + show_all(); +} + +void ColorWheelSelector::on_show() +{ + Gtk::Grid::on_show(); + _updateDisplay(); +} + +void ColorWheelSelector::_colorChanged() +{ + _updateDisplay(); +} + +void ColorWheelSelector::_adjustmentChanged() +{ + if (_updating) { + return; + } + + _color.preserveICC(); + _color.setAlpha(ColorScales::getScaled(_alpha_adjustment->gobj())); +} + +void ColorWheelSelector::_sliderGrabbed() +{ + _color.preserveICC(); + _color.setHeld(true); +} + +void ColorWheelSelector::_sliderReleased() +{ + _color.preserveICC(); + _color.setHeld(false); +} + +void ColorWheelSelector::_sliderChanged() +{ + if (_updating) { + return; + } + + _color.preserveICC(); + _color.setAlpha(ColorScales::getScaled(_alpha_adjustment->gobj())); +} + +void ColorWheelSelector::_wheelChanged() +{ + if (_updating) { + return; + } + + double rgb[3] = { 0, 0, 0 }; + _wheel->get_rgb(rgb[0], rgb[1], rgb[2]); + + SPColor color(rgb[0], rgb[1], rgb[2]); + + guint32 start = color.toRGBA32(0x00); + guint32 mid = color.toRGBA32(0x7f); + guint32 end = color.toRGBA32(0xff); + + _updating = true; + _slider->setColors(start, mid, end); + _color.preserveICC(); + + _color.setHeld(_wheel->is_adjusting()); + _color.setColor(color); + _updating = false; +} + +void ColorWheelSelector::_updateDisplay() +{ + if(_updating) { return; } + +#ifdef DUMP_CHANGE_INFO + g_message("ColorWheelSelector::_colorChanged( this=%p, %f, %f, %f, %f)", this, _color.color().v.c[0], + _color.color().v.c[1], _color.color().v.c[2], alpha); +#endif + + _updating = true; + { + float hsv[3] = { 0, 0, 0 }; + SPColor::rgb_to_hsv_floatv(hsv, _color.color().v.c[0], _color.color().v.c[1], _color.color().v.c[2]); + _wheel->set_rgb(_color.color().v.c[0], _color.color().v.c[1], _color.color().v.c[2]); + } + + guint32 start = _color.color().toRGBA32(0x00); + guint32 mid = _color.color().toRGBA32(0x7f); + guint32 end = _color.color().toRGBA32(0xff); + + _slider->setColors(start, mid, end); + + ColorScales::setScaled(_alpha_adjustment->gobj(), _color.alpha()); + + _updating = false; +} + + +Gtk::Widget *ColorWheelSelectorFactory::createWidget(Inkscape::UI::SelectedColor &color) const +{ + Gtk::Widget *w = Gtk::manage(new ColorWheelSelector(color)); + return w; +} + +Glib::ustring ColorWheelSelectorFactory::modeName() const { return gettext(ColorWheelSelector::MODE_NAME); } +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/color-wheel-selector.h b/src/ui/widget/color-wheel-selector.h new file mode 100644 index 0000000..59cf6b6 --- /dev/null +++ b/src/ui/widget/color-wheel-selector.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Color selector widget containing GIMP color wheel and slider + */ +/* Authors: + * Tomasz Boczkowski <penginsbacon@gmail.com> (c++-sification) + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_WHEEL_SELECTOR_H +#define SEEN_SP_COLOR_WHEEL_SELECTOR_H + +#include <gtkmm/grid.h> + +#include "ui/selected-color.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorSlider; +class ColorWheel; +class ColorWheelSelector + : public Gtk::Grid +{ +public: + static const gchar *MODE_NAME; + + ColorWheelSelector(SelectedColor &color); + ~ColorWheelSelector() override; + +protected: + void _initUI(); + + void on_show() override; + + void _colorChanged(); + void _adjustmentChanged(); + void _sliderGrabbed(); + void _sliderReleased(); + void _sliderChanged(); + void _wheelChanged(); + + void _updateDisplay(); + + SelectedColor &_color; + bool _updating; + Glib::RefPtr<Gtk::Adjustment> _alpha_adjustment; + Inkscape::UI::Widget::ColorWheel *_wheel; + Inkscape::UI::Widget::ColorSlider *_slider; + +private: + // By default, disallow copy constructor and assignment operator + ColorWheelSelector(const ColorWheelSelector &obj) = delete; + ColorWheelSelector &operator=(const ColorWheelSelector &obj) = delete; + + sigc::connection _color_changed_connection; + sigc::connection _color_dragged_connection; +}; + +class ColorWheelSelectorFactory : public ColorSelectorFactory { +public: + Gtk::Widget *createWidget(SelectedColor &color) const override; + Glib::ustring modeName() const override; +}; +} +} +} + +#endif // SEEN_SP_COLOR_WHEEL_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-box-entry-tool-item.cpp b/src/ui/widget/combo-box-entry-tool-item.cpp new file mode 100644 index 0000000..9adcca1 --- /dev/null +++ b/src/ui/widget/combo-box-entry-tool-item.cpp @@ -0,0 +1,691 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry. + * Features: + * Setting GtkEntryBox width in characters. + * Passing a function for formatting cells. + * Displaying a warning if entry text isn't in list. + * Check comma separated values in text against list. (Useful for font-family fallbacks.) + * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry) + * to allow setting resources. + * + * Author(s): + * Tavmjong Bah + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * We must provide for both a toolbar item and a menu item. + * As we don't know which widgets are used (or even constructed), + * we must keep track of things like active entry ourselves. + */ + +#include "combo-box-entry-tool-item.h" + +#include <iostream> +#include <cstring> +#include <glibmm/ustring.h> + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <gdkmm/display.h> + +#include "ui/icon-names.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +ComboBoxEntryToolItem::ComboBoxEntryToolItem(Glib::ustring name, + Glib::ustring label, + Glib::ustring tooltip, + GtkTreeModel *model, + gint entry_width, + gint extra_width, + void *cell_data_func, + void *separator_func, + GtkWidget *focusWidget) + : _label(std::move(label)), + _tooltip(std::move(tooltip)), + _model(model), + _entry_width(entry_width), + _extra_width(extra_width), + _cell_data_func(cell_data_func), + _separator_func(separator_func), + _focusWidget(focusWidget), + _active(-1), + _text(strdup("")), + _entry_completion(nullptr), + _indicator(nullptr), + _popup(false), + _info(nullptr), + _info_cb(nullptr), + _info_cb_id(0), + _info_cb_blocked(false), + _warning(nullptr), + _warning_cb(nullptr), + _warning_cb_id(0), + _warning_cb_blocked(false), + _altx_name(nullptr) +{ + set_name(name); + + gchar *action_name = g_strdup( get_name().c_str() ); + gchar *combobox_name = g_strjoin( nullptr, action_name, "_combobox", NULL ); + gchar *entry_name = g_strjoin( nullptr, action_name, "_entry", NULL ); + g_free( action_name ); + + GtkWidget* comboBoxEntry = gtk_combo_box_new_with_model_and_entry (_model); + gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (comboBoxEntry), 0); + + // Name it so we can muck with it using an RC file + gtk_widget_set_name( comboBoxEntry, combobox_name ); + g_free( combobox_name ); + + { + gtk_widget_set_halign(comboBoxEntry, GTK_ALIGN_START); + gtk_widget_set_hexpand(comboBoxEntry, FALSE); + gtk_widget_set_vexpand(comboBoxEntry, FALSE); + add(*Glib::wrap(comboBoxEntry)); + } + + _combobox = GTK_COMBO_BOX (comboBoxEntry); + + //gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), ink_comboboxentry_action->active ); + gtk_combo_box_set_active( GTK_COMBO_BOX( comboBoxEntry ), 0 ); + + g_signal_connect( G_OBJECT(comboBoxEntry), "changed", G_CALLBACK(combo_box_changed_cb), this ); + + // Optionally add separator function... + if( _separator_func != nullptr ) { + gtk_combo_box_set_row_separator_func( _combobox, + GtkTreeViewRowSeparatorFunc (_separator_func), + nullptr, nullptr ); + } + + // FIXME: once gtk3 migration is done this can be removed + // https://bugzilla.gnome.org/show_bug.cgi?id=734915 + gtk_widget_show_all (comboBoxEntry); + + // Optionally add formatting... + if( _cell_data_func != nullptr ) { + GtkCellRenderer *cell = gtk_cell_renderer_text_new(); + gtk_cell_layout_clear( GTK_CELL_LAYOUT( comboBoxEntry ) ); + gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( comboBoxEntry ), cell, true ); + gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT( comboBoxEntry ), cell, + GtkCellLayoutDataFunc (_cell_data_func), + nullptr, nullptr ); + } + + // Optionally widen the combobox width... which widens the drop-down list in list mode. + if( _extra_width > 0 ) { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr); + gtk_widget_set_size_request( GTK_WIDGET( _combobox ), + req.width + _extra_width, -1 ); + } + + // Get reference to GtkEntry and fiddle a bit with it. + GtkWidget *child = gtk_bin_get_child( GTK_BIN(comboBoxEntry) ); + + // Name it so we can muck with it using an RC file + gtk_widget_set_name( child, entry_name ); + g_free( entry_name ); + + if( child && GTK_IS_ENTRY( child ) ) { + + _entry = GTK_ENTRY(child); + + // Change width + if( _entry_width > 0 ) { + gtk_entry_set_width_chars (GTK_ENTRY (child), _entry_width ); + } + + // Add pop-up entry completion if required + if( _popup ) { + popup_enable(); + } + + // Add altx_name if required + if( _altx_name ) { + g_object_set_data( G_OBJECT( child ), _altx_name, _entry ); + } + + // Add signal for GtkEntry to check if finished typing. + g_signal_connect( G_OBJECT(child), "activate", G_CALLBACK(entry_activate_cb), this ); + g_signal_connect( G_OBJECT(child), "key-press-event", G_CALLBACK(keypress_cb), this ); + } + + set_tooltip(_tooltip.c_str()); + + show_all(); +} + +// Setters/Getters --------------------------------------------------- + +gchar* +ComboBoxEntryToolItem::get_active_text() +{ + gchar* text = g_strdup( _text ); + return text; +} + +/* + * For the font-family list we need to handle two cases: + * Text is in list store: + * In this case we use row number as the font-family list can have duplicate + * entries, one in the document font part and one in the system font part. In + * order that scrolling through the list works properly we must distinguish + * between the two. + * Text is not in the list store (i.e. default font-family is not on system): + * In this case we have a row number of -1, and the text must be set by hand. + */ +gboolean +ComboBoxEntryToolItem::set_active_text(const gchar* text, int row) +{ + if( strcmp( _text, text ) != 0 ) { + g_free( _text ); + _text = g_strdup( text ); + } + + // Get active row or -1 if none + if( row < 0 ) { + row = get_active_row_from_text(this, _text); + } + _active = row; + + // Set active row, check that combobox has been created. + if( _combobox ) { + gtk_combo_box_set_active( GTK_COMBO_BOX( _combobox ), _active ); + } + + // Fiddle with entry + if( _entry ) { + + // Explicitly set text in GtkEntry box (won't be set if text not in list). + gtk_entry_set_text( _entry, text ); + + // Show or hide warning -- this might be better moved to text-toolbox.cpp + if( _info_cb_id != 0 && + !_info_cb_blocked ) { + g_signal_handler_block (G_OBJECT(_entry), + _info_cb_id ); + _info_cb_blocked = true; + } + if( _warning_cb_id != 0 && + !_warning_cb_blocked ) { + g_signal_handler_block (G_OBJECT(_entry), + _warning_cb_id ); + _warning_cb_blocked = true; + } + + bool set = false; + if( _warning != nullptr ) { + Glib::ustring missing = check_comma_separated_text(); + if( !missing.empty() ) { + gtk_entry_set_icon_from_icon_name( _entry, + GTK_ENTRY_ICON_SECONDARY, + INKSCAPE_ICON("dialog-warning") ); + // Can't add tooltip until icon set + Glib::ustring warning = _warning; + warning += ": "; + warning += missing; + gtk_entry_set_icon_tooltip_text( _entry, + GTK_ENTRY_ICON_SECONDARY, + warning.c_str() ); + + if( _warning_cb ) { + + // Add callback if we haven't already + if( _warning_cb_id == 0 ) { + _warning_cb_id = + g_signal_connect( G_OBJECT(_entry), + "icon-press", + G_CALLBACK(_warning_cb), + this); + } + // Unblock signal + if( _warning_cb_blocked ) { + g_signal_handler_unblock (G_OBJECT(_entry), + _warning_cb_id ); + _warning_cb_blocked = false; + } + } + set = true; + } + } + + if( !set && _info != nullptr ) { + gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + INKSCAPE_ICON("edit-select-all") ); + gtk_entry_set_icon_tooltip_text( _entry, + GTK_ENTRY_ICON_SECONDARY, + _info ); + + if( _info_cb ) { + // Add callback if we haven't already + if( _info_cb_id == 0 ) { + _info_cb_id = + g_signal_connect( G_OBJECT(_entry), + "icon-press", + G_CALLBACK(_info_cb), + this); + } + // Unblock signal + if( _info_cb_blocked ) { + g_signal_handler_unblock (G_OBJECT(_entry), + _info_cb_id ); + _info_cb_blocked = false; + } + } + set = true; + } + + if( !set ) { + gtk_entry_set_icon_from_icon_name( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + nullptr ); + } + } + + // Return if active text in list + gboolean found = ( _active != -1 ); + return found; +} + +void +ComboBoxEntryToolItem::set_entry_width(gint entry_width) +{ + _entry_width = entry_width; + + // Clamp to limits + if(entry_width < -1) entry_width = -1; + if(entry_width > 100) entry_width = 100; + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_width_chars( GTK_ENTRY(_entry), entry_width ); + } +} + +void +ComboBoxEntryToolItem::set_extra_width( gint extra_width ) +{ + _extra_width = extra_width; + + // Clamp to limits + if(extra_width < -1) extra_width = -1; + if(extra_width > 500) extra_width = 500; + + // Widget may not have been created.... + if( _combobox ) { + GtkRequisition req; + gtk_widget_get_preferred_size(GTK_WIDGET(_combobox), &req, nullptr); + gtk_widget_set_size_request( GTK_WIDGET( _combobox ), req.width + _extra_width, -1 ); + } +} + +void +ComboBoxEntryToolItem::focus_on_click( bool focus_on_click ) +{ + if (_combobox) { + gtk_widget_set_focus_on_click(GTK_WIDGET(_combobox), focus_on_click); + } +} + +void +ComboBoxEntryToolItem::popup_enable() +{ + _popup = true; + + // Widget may not have been created.... + if( _entry ) { + + // Check we don't already have a GtkEntryCompletion + if( _entry_completion ) return; + + _entry_completion = gtk_entry_completion_new(); + + gtk_entry_set_completion( _entry, _entry_completion ); + gtk_entry_completion_set_model( _entry_completion, _model ); + gtk_entry_completion_set_text_column( _entry_completion, 0 ); + gtk_entry_completion_set_popup_completion( _entry_completion, true ); + gtk_entry_completion_set_inline_completion( _entry_completion, false ); + gtk_entry_completion_set_inline_selection( _entry_completion, true ); + + g_signal_connect (G_OBJECT (_entry_completion), "match-selected", G_CALLBACK (match_selected_cb), this); + } +} + +void +ComboBoxEntryToolItem::popup_disable() +{ + _popup = false; + + if( _entry_completion ) { + gtk_widget_destroy(GTK_WIDGET(_entry_completion)); + _entry_completion = nullptr; + } +} + +void +ComboBoxEntryToolItem::set_tooltip(const gchar* tooltip) +{ + set_tooltip_text(tooltip); + gtk_widget_set_tooltip_text ( GTK_WIDGET(_combobox), tooltip); + + // Widget may not have been created.... + if( _entry ) { + gtk_widget_set_tooltip_text ( GTK_WIDGET(_entry), tooltip); + } +} + +void +ComboBoxEntryToolItem::set_info(const gchar* info) +{ + g_free( _info ); + _info = g_strdup( info ); + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + _info ); + } +} + +void +ComboBoxEntryToolItem::set_info_cb(gpointer info_cb) +{ + _info_cb = info_cb; +} + +void +ComboBoxEntryToolItem::set_warning(const gchar* warning) +{ + g_free( _warning ); + _warning = g_strdup( warning ); + + // Widget may not have been created.... + if( _entry ) { + gtk_entry_set_icon_tooltip_text( GTK_ENTRY(_entry), + GTK_ENTRY_ICON_SECONDARY, + _warning ); + } +} + +void +ComboBoxEntryToolItem::set_warning_cb(gpointer warning_cb) +{ + _warning_cb = warning_cb; +} + +void +ComboBoxEntryToolItem::set_altx_name(const gchar* altx_name) +{ + g_free(_altx_name); + _altx_name = g_strdup( altx_name ); + + // Widget may not have been created.... + if(_entry) { + g_object_set_data( G_OBJECT(_entry), _altx_name, _entry ); + } +} + +// Internal --------------------------------------------------- + +// Return row of active text or -1 if not found. If exclude is true, +// use 3d column if available to exclude row from checking (useful to +// skip rows added for font-families included in doc and not on +// system) +gint +ComboBoxEntryToolItem::get_active_row_from_text(ComboBoxEntryToolItem *action, + const gchar *target_text, + gboolean exclude, + gboolean ignore_case ) +{ + // Check if text in list + gint row = 0; + gboolean found = false; + GtkTreeIter iter; + gboolean valid = gtk_tree_model_get_iter_first( action->_model, &iter ); + while ( valid ) { + + // See if we should exclude a row + gboolean check = true; // If true, font-family is on system. + if( exclude && gtk_tree_model_get_n_columns( action->_model ) > 2 ) { + gtk_tree_model_get( action->_model, &iter, 2, &check, -1 ); + } + + if( check ) { + // Get text from list entry + gchar* text = nullptr; + gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); // Column 0 + + if( !ignore_case ) { + // Case sensitive compare + if( strcmp( target_text, text ) == 0 ){ + found = true; + g_free(text); + break; + } + } else { + // Case insensitive compare + gchar* target_text_casefolded = g_utf8_casefold( target_text, -1 ); + gchar* text_casefolded = g_utf8_casefold( text, -1 ); + gboolean equal = (strcmp( target_text_casefolded, text_casefolded ) == 0 ); + g_free( text_casefolded ); + g_free( target_text_casefolded ); + if( equal ) { + found = true; + g_free(text); + break; + } + } + g_free(text); + } + + ++row; + valid = gtk_tree_model_iter_next( action->_model, &iter ); + } + + if( !found ) row = -1; + + return row; +} + +// Checks if all comma separated text fragments are in the list and +// returns a ustring with a list of missing fragments. +// This is useful for checking if all fonts in a font-family fallback +// list are available on the system. +// +// This routine could also create a Pango Markup string to show which +// fragments are invalid in the entry box itself. See: +// http://developer.gnome.org/pango/stable/PangoMarkupFormat.html +// However... it appears that while one can retrieve the PangoLayout +// for a GtkEntry box, it is only a copy and changing it has no effect. +// PangoLayout * pl = gtk_entry_get_layout( entry ); +// pango_layout_set_markup( pl, "NEW STRING", -1 ); // DOESN'T WORK +Glib::ustring +ComboBoxEntryToolItem::check_comma_separated_text() +{ + Glib::ustring missing; + + // Parse fallback_list using a comma as deliminator + gchar** tokens = g_strsplit( _text, ",", 0 ); + + gint i = 0; + while( tokens[i] != nullptr ) { + + // Remove any surrounding white space. + g_strstrip( tokens[i] ); + + if( get_active_row_from_text( this, tokens[i], true, true ) == -1 ) { + missing += tokens[i]; + missing += ", "; + } + ++i; + } + g_strfreev( tokens ); + + // Remove extra comma and space from end. + if( missing.size() >= 2 ) { + missing.resize( missing.size()-2 ); + } + return missing; +} + +// Callbacks --------------------------------------------------- + +void +ComboBoxEntryToolItem::combo_box_changed_cb( GtkComboBox* widget, gpointer data ) +{ + // Two things can happen to get here: + // An item is selected in the drop-down menu. + // Text is typed. + // We only react here if an item is selected. + + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem *>( data ); + + // Check if item selected: + gint newActive = gtk_combo_box_get_active(widget); + if( newActive >= 0 && newActive != action->_active ) { + + action->_active = newActive; + + GtkTreeIter iter; + if( gtk_combo_box_get_active_iter( GTK_COMBO_BOX( action->_combobox ), &iter ) ) { + + gchar* text = nullptr; + gtk_tree_model_get( action->_model, &iter, 0, &text, -1 ); + gtk_entry_set_text( action->_entry, text ); + + g_free( action->_text ); + action->_text = text; + } + + // Now let the world know + action->_signal_changed.emit(); + } +} + +void +ComboBoxEntryToolItem::entry_activate_cb( GtkEntry *widget, + gpointer data ) +{ + // Get text from entry box.. check if it matches a menu entry. + + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem*>( data ); + + // Get text + g_free( action->_text ); + action->_text = g_strdup( gtk_entry_get_text( widget ) ); + + // Get row + action->_active = + get_active_row_from_text( action, action->_text ); + + // Set active row + gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active ); + + // Now let the world know + action->_signal_changed.emit(); +} + +gboolean +ComboBoxEntryToolItem::match_selected_cb( GtkEntryCompletion* /*widget*/, GtkTreeModel* model, GtkTreeIter* iter, gpointer data ) +{ + // Get action + auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data); + GtkEntry *entry = action->_entry; + + if( entry) { + gchar *family = nullptr; + gtk_tree_model_get(model, iter, 0, &family, -1); + + // Set text in GtkEntry + gtk_entry_set_text (GTK_ENTRY (entry), family ); + + // Set text in ToolItem + g_free( action->_text ); + action->_text = family; + + // Get row + action->_active = + get_active_row_from_text( action, action->_text ); + + // Set active row + gtk_combo_box_set_active( GTK_COMBO_BOX( action->_combobox), action->_active ); + + // Now let the world know + action->_signal_changed.emit(); + + return true; + } + return false; +} + +void +ComboBoxEntryToolItem::defocus() +{ + if ( _focusWidget ) { + gtk_widget_grab_focus( _focusWidget ); + } +} + +gboolean +ComboBoxEntryToolItem::keypress_cb( GtkWidget * /*widget*/, GdkEventKey *event, gpointer data ) +{ + gboolean wasConsumed = FALSE; /* default to report event not consumed */ + guint key = 0; + auto action = reinterpret_cast<ComboBoxEntryToolItem*>(data); + gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(), + event->hardware_keycode, (GdkModifierType)event->state, + 0, &key, nullptr, nullptr, nullptr ); + + switch ( key ) { + + // TODO Add bindings for Tab/LeftTab + case GDK_KEY_Escape: + { + //gtk_spin_button_set_value( GTK_SPIN_BUTTON(widget), action->private_data->lastVal ); + action->defocus(); + wasConsumed = TRUE; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + action->defocus(); + //wasConsumed = TRUE; + } + break; + + + } + + return wasConsumed; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-box-entry-tool-item.h b/src/ui/widget/combo-box-entry-tool-item.h new file mode 100644 index 0000000..3d6440a --- /dev/null +++ b/src/ui/widget/combo-box-entry-tool-item.h @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A class derived from Gtk::ToolItem that wraps a GtkComboBoxEntry. + * Features: + * Setting GtkEntryBox width in characters. + * Passing a function for formatting cells. + * Displaying a warning if entry text isn't in list. + * Check comma separated values in text against list. (Useful for font-family fallbacks.) + * Setting names for GtkComboBoxEntry and GtkEntry (actionName_combobox, actionName_entry) + * to allow setting resources. + * + * Author(s): + * Tavmjong Bah + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_COMBOBOXENTRY_ACTION +#define SEEN_INK_COMBOBOXENTRY_ACTION + +#include <gtkmm/toolitem.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Creates a Gtk::ToolItem subclass that wraps a Gtk::ComboBox object. + */ +class ComboBoxEntryToolItem : public Gtk::ToolItem { +private: + Glib::ustring _tooltip; + Glib::ustring _label; + GtkTreeModel *_model; ///< Tree Model + GtkComboBox *_combobox; + GtkEntry *_entry; + gint _entry_width;// Width of GtkEntry in characters. + gint _extra_width;// Extra Width of GtkComboBox.. to widen drop-down list in list mode. + gpointer _cell_data_func; // drop-down menu format + gpointer _separator_func; + gboolean _popup; // Do we pop-up an entry-completion dialog? + GtkEntryCompletion *_entry_completion; + GtkWidget *_focusWidget; ///< The widget to return focus to + + GtkWidget *_indicator; + gint _active; // Index of active menu item (-1 if not in list). + gchar *_text; // Text of active menu item or entry box. + gchar *_info; // Text for tooltip info about entry. + gpointer _info_cb; // Callback for clicking info icon. + gint _info_cb_id; + gboolean _info_cb_blocked; + gchar *_warning; // Text for tooltip warning that entry isn't in list. + gpointer _warning_cb; // Callback for clicking warning icon. + gint _warning_cb_id; + gboolean _warning_cb_blocked; + gchar *_altx_name; // Target for Alt-X keyboard shortcut. + + // Signals + sigc::signal<void> _signal_changed; + + void (*changed) (ComboBoxEntryToolItem* action); + void (*activated) (ComboBoxEntryToolItem* action); + + static gint get_active_row_from_text(ComboBoxEntryToolItem *action, + const gchar *target_text, + gboolean exclude = false, + gboolean ignore_case = false); + void defocus(); + + static void combo_box_changed_cb( GtkComboBox* widget, gpointer data ); + static void entry_activate_cb( GtkEntry *widget, + gpointer data ); + static gboolean match_selected_cb( GtkEntryCompletion *widget, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data); + static gboolean keypress_cb( GtkWidget *widget, + GdkEventKey *event, + gpointer data ); + + Glib::ustring check_comma_separated_text(); + +public: + ComboBoxEntryToolItem(const Glib::ustring name, + const Glib::ustring label, + const Glib::ustring tooltip, + GtkTreeModel *model, + gint entry_width = -1, + gint extra_width = -1, + gpointer cell_data_func = nullptr, + gpointer separator_func = nullptr, + GtkWidget* focusWidget = nullptr); + + gchar* get_active_text(); + gboolean set_active_text(const gchar* text, int row=-1); + + void set_entry_width(gint entry_width); + void set_extra_width(gint extra_width); + + void popup_enable(); + void popup_disable(); + void focus_on_click( bool focus_on_click ); + + void set_info( const gchar* info ); + void set_info_cb( gpointer info_cb ); + void set_warning( const gchar* warning_cb ); + void set_warning_cb(gpointer warning ); + void set_tooltip( const gchar* tooltip ); + + void set_altx_name( const gchar* altx_name ); + + // Accessor methods + decltype(_model) get_model() const {return _model;} + decltype(_combobox) get_combobox() const {return _combobox;} + decltype(_entry) get_entry() const {return _entry;} + decltype(_entry_width) get_entry_width() const {return _entry_width;} + decltype(_extra_width) get_extra_width() const {return _extra_width;} + decltype(_cell_data_func) get_cell_data_func() const {return _cell_data_func;} + decltype(_separator_func) get_separator_func() const {return _separator_func;} + decltype(_popup) get_popup() const {return _popup;} + decltype(_focusWidget) get_focus_widget() const {return _focusWidget;} + + decltype(_active) get_active() const {return _active;} + + decltype(_signal_changed) signal_changed() {return _signal_changed;} + + // Mutator methods + void set_model (decltype(_model) model) {_model = model;} + void set_combobox (decltype(_combobox) combobox) {_combobox = combobox;} + void set_entry (decltype(_entry) entry) {_entry = entry;} + void set_cell_data_func(decltype(_cell_data_func) cell_data_func) {_cell_data_func = cell_data_func;} + void set_separator_func(decltype(_separator_func) separator_func) {_separator_func = separator_func;} + void set_popup (decltype(_popup) popup) {_popup = popup;} + void set_focus_widget (decltype(_focusWidget) focus_widget) {_focusWidget = focus_widget;} + + // This doesn't seem right... surely we should set the active row in the Combobox too? + void set_active (decltype(_active) active) {_active = active;} +}; + +} +} +} +#endif /* SEEN_INK_COMBOBOXENTRY_ACTION */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-enums.h b/src/ui/widget/combo-enums.h new file mode 100644 index 0000000..d574106 --- /dev/null +++ b/src/ui/widget/combo-enums.h @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_COMBO_ENUMS_H +#define INKSCAPE_UI_WIDGET_COMBO_ENUMS_H + +#include "ui/widget/labelled.h" +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include "attr-widget.h" +#include "util/enums.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class ComboBoxEnum : public Gtk::ComboBox, public AttrWidget +{ +private: + int on_sort_compare( const Gtk::TreeModel::iterator & a, const Gtk::TreeModel::iterator & b) + { + Glib::ustring an=(*a)[_columns.label]; + Glib::ustring bn=(*b)[_columns.label]; + return an.compare(bn); + } + + bool _sort; + +public: + ComboBoxEnum(E default_value, const Util::EnumDataConverter<E>& c, const SPAttributeEnum a = SP_ATTR_INVALID, bool sort = true) + : AttrWidget(a, (unsigned int)default_value), setProgrammatically(false), _converter(c) + { + _sort = sort; + + signal_changed().connect(signal_attr_changed().make_slot()); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event)); + _model = Gtk::ListStore::create(_columns); + set_model(_model); + + pack_start(_columns.label); + + // Initialize list + for(int i = 0; i < static_cast<int>(_converter._length); ++i) { + Gtk::TreeModel::Row row = *_model->append(); + const Util::EnumData<E>* data = &_converter.data(i); + row[_columns.data] = data; + row[_columns.label] = _( _converter.get_label(data->id).c_str() ); + } + set_active_by_id(default_value); + + // Sort the list + if (sort) { + _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare)); + _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING); + } + } + + ComboBoxEnum(const Util::EnumDataConverter<E>& c, const SPAttributeEnum a = SP_ATTR_INVALID, bool sort = true) + : AttrWidget(a, (unsigned int) 0), setProgrammatically(false), _converter(c) + { + _sort = sort; + + signal_changed().connect(signal_attr_changed().make_slot()); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_scroll_event)); + + _model = Gtk::ListStore::create(_columns); + set_model(_model); + + pack_start(_columns.label); + + // Initialize list + for(unsigned int i = 0; i < _converter._length; ++i) { + Gtk::TreeModel::Row row = *_model->append(); + const Util::EnumData<E>* data = &_converter.data(i); + row[_columns.data] = data; + row[_columns.label] = _( _converter.get_label(data->id).c_str() ); + } + set_active(0); + + // Sort the list + if (_sort) { + _model->set_default_sort_func(sigc::mem_fun(*this, &ComboBoxEnum<E>::on_sort_compare)); + _model->set_sort_column(_columns.label, Gtk::SORT_ASCENDING); + } + } + + Glib::ustring get_as_attribute() const override + { + return get_active_data()->key; + } + + void set_from_attribute(SPObject* o) override + { + setProgrammatically = true; + const gchar* val = attribute_value(o); + if(val) + set_active_by_id(_converter.get_id_from_key(val)); + else + set_active(get_default()->as_uint()); + } + + const Util::EnumData<E>* get_active_data() const + { + Gtk::TreeModel::iterator i = this->get_active(); + if(i) + return (*i)[_columns.data]; + return nullptr; + } + + void add_row(const Glib::ustring& s) + { + Gtk::TreeModel::Row row = *_model->append(); + row[_columns.data] = 0; + row[_columns.label] = s; + } + + void remove_row(E id) { + Gtk::TreeModel::iterator i; + + for(i = _model->children().begin(); i != _model->children().end(); ++i) { + const Util::EnumData<E>* data = (*i)[_columns.data]; + + if(data->id == id) + break; + } + + if(i != _model->children().end()) + _model->erase(i); + } + + void set_active_by_id(E id) { + setProgrammatically = true; + for(Gtk::TreeModel::iterator i = _model->children().begin(); + i != _model->children().end(); ++i) + { + const Util::EnumData<E>* data = (*i)[_columns.data]; + if(data->id == id) { + set_active(i); + break; + } + } + }; + + bool on_scroll_event(GdkEventScroll *event) override { return false; } + + void set_active_by_key(const Glib::ustring& key) { + setProgrammatically = true; + set_active_by_id( _converter.get_id_from_key(key) ); + }; + + bool setProgrammatically; + +private: + class Columns : public Gtk::TreeModel::ColumnRecord + { + public: + Columns() + { + add(data); + add(label); + } + + Gtk::TreeModelColumn<const Util::EnumData<E>*> data; + Gtk::TreeModelColumn<Glib::ustring> label; + }; + + Columns _columns; + Glib::RefPtr<Gtk::ListStore> _model; + const Util::EnumDataConverter<E>& _converter; +}; + + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class LabelledComboBoxEnum : public Labelled +{ +public: + LabelledComboBoxEnum( Glib::ustring const &label, + Glib::ustring const &tooltip, + const Util::EnumDataConverter<E>& c, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true, + bool sorted = true) + : Labelled(label, tooltip, new ComboBoxEnum<E>(c, SP_ATTR_INVALID, sorted), suffix, icon, mnemonic) + { + } + + ComboBoxEnum<E>* getCombobox() { + return static_cast< ComboBoxEnum<E>* > (_widget); + } +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-tool-item.cpp b/src/ui/widget/combo-tool-item.cpp new file mode 100644 index 0000000..ffc7e75 --- /dev/null +++ b/src/ui/widget/combo-tool-item.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +/** \file + A combobox that can be displayed in a toolbar. +*/ + +#include "combo-tool-item.h" +#include "preferences.h" +#include <iostream> +#include <utility> +#include <gtkmm/toolitem.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/combobox.h> +#include <gtkmm/menu.h> +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +ComboToolItem* +ComboToolItem::create(const Glib::ustring &group_label, + const Glib::ustring &tooltip, + const Glib::ustring &stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry) +{ + return new ComboToolItem(group_label, tooltip, stock_id, store, has_entry); +} + +ComboToolItem::ComboToolItem(Glib::ustring group_label, + Glib::ustring tooltip, + Glib::ustring stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry) : + _active(-1), + _group_label(std::move( group_label )), + _tooltip(std::move( tooltip )), + _stock_id(std::move( stock_id )), + _store (std::move(store)), + _use_label (true), + _use_icon (false), + _use_pixbuf (true), + _icon_size ( Gtk::ICON_SIZE_LARGE_TOOLBAR ), + _combobox (nullptr), + _group_label_widget(nullptr), + _container(Gtk::manage(new Gtk::Box())), + _menuitem (nullptr) +{ + add(*_container); + _container->set_spacing(3); + + // ": " is added to the group label later + if (!_group_label.empty()) { + // we don't expect trailing spaces + // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ' '); + + // strip space (note: raw() indexing is much cheaper on Glib::ustring) + if (_group_label.raw()[_group_label.raw().size() - 1] == ' ') { + _group_label.resize(_group_label.size() - 1); + } + } + if (!_group_label.empty()) { + // we don't expect a trailing colon + // g_assert(_group_label.raw()[_group_label.raw().size() - 1] != ':'); + + // strip colon (note: raw() indexing is much cheaper on Glib::ustring) + if (_group_label.raw()[_group_label.raw().size() - 1] == ':') { + _group_label.resize(_group_label.size() - 1); + } + } + + + // Create combobox + _combobox = Gtk::manage (new Gtk::ComboBox(has_entry)); + _combobox->set_model(_store); + + populate_combobox(); + + _combobox->signal_changed().connect( + sigc::mem_fun(*this, &ComboToolItem::on_changed_combobox)); + _container->pack_start(*_combobox); + + show_all(); +} + +void +ComboToolItem::focus_on_click( bool focus_on_click ) +{ + _combobox->set_focus_on_click(focus_on_click); +} + + +void +ComboToolItem::use_label(bool use_label) +{ + _use_label = use_label; + populate_combobox(); +} + +void +ComboToolItem::use_icon(bool use_icon) +{ + _use_icon = use_icon; + populate_combobox(); +} + +void +ComboToolItem::use_pixbuf(bool use_pixbuf) +{ + _use_pixbuf = use_pixbuf; + populate_combobox(); +} + +void +ComboToolItem::use_group_label(bool use_group_label) +{ + if (use_group_label == (_group_label_widget != nullptr)) { + return; + } + if (use_group_label) { + _container->remove(*_combobox); + _group_label_widget = Gtk::manage(new Gtk::Label(_group_label + ": ")); + _container->pack_start(*_group_label_widget); + _container->pack_start(*_combobox); + } else { + _container->remove(*_group_label_widget); + delete _group_label_widget; + _group_label_widget = nullptr; + } +} + +void +ComboToolItem::populate_combobox() +{ + _combobox->clear(); + + ComboToolItemColumns columns; + if (_use_icon) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/theme/symbolicIcons", false)) { + auto children = _store->children(); + for (auto row : children) { + Glib::ustring icon = row[columns.col_icon]; + gint pos = icon.find("-symbolic"); + if (pos == std::string::npos) { + icon += "-symbolic"; + } + row[columns.col_icon] = icon; + } + } + Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf; + renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _combobox->pack_start (*Gtk::manage(renderer), false); + _combobox->add_attribute (*renderer, "icon_name", columns.col_icon ); + } else if (_use_pixbuf) { + Gtk::CellRendererPixbuf *renderer = new Gtk::CellRendererPixbuf; + //renderer->set_property ("stock_size", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _combobox->pack_start (*Gtk::manage(renderer), false); + _combobox->add_attribute (*renderer, "pixbuf", columns.col_pixbuf ); + } + + if (_use_label) { + _combobox->pack_start(columns.col_label); + } + + std::vector<Gtk::CellRenderer*> cells = _combobox->get_cells(); + for (auto & cell : cells) { + _combobox->add_attribute (*cell, "sensitive", columns.col_sensitive); + } + + set_tooltip_text(_tooltip); + _combobox->set_tooltip_text(_tooltip); + _combobox->set_active (_active); +} + +void +ComboToolItem::set_active (gint active) { + if (_active != active) { + + _active = active; + + if (_combobox) { + _combobox->set_active (active); + } + + if (active < _radiomenuitems.size()) { + _radiomenuitems[ active ]->set_active(); + } + } +} + +Glib::ustring +ComboToolItem::get_active_text () { + Gtk::TreeModel::Row row = _store->children()[_active]; + ComboToolItemColumns columns; + Glib::ustring label = row[columns.col_label]; + return label; +} + +bool +ComboToolItem::on_create_menu_proxy() +{ + if (_menuitem == nullptr) { + + _menuitem = Gtk::manage (new Gtk::MenuItem(_group_label)); + Gtk::Menu *menu = Gtk::manage (new Gtk::Menu); + + Gtk::RadioButton::Group group; + int index = 0; + auto children = _store->children(); + for (auto row : children) { + ComboToolItemColumns columns; + Glib::ustring label = row[columns.col_label ]; + Glib::ustring icon = row[columns.col_icon ]; + Glib::ustring tooltip = row[columns.col_tooltip ]; + bool sensitive = row[columns.col_sensitive ]; + + Gtk::RadioMenuItem* button = Gtk::manage(new Gtk::RadioMenuItem(group)); + button->set_label (label); + button->set_tooltip_text( tooltip ); + button->set_sensitive( sensitive ); + + button->signal_toggled().connect( sigc::bind<0>( + sigc::mem_fun(*this, &ComboToolItem::on_toggled_radiomenu), index++) + ); + + menu->add (*button); + + _radiomenuitems.push_back( button ); + } + + if ( _active < _radiomenuitems.size()) { + _radiomenuitems[ _active ]->set_active(); + } + + _menuitem->set_submenu (*menu); + _menuitem->show_all(); + } + + set_proxy_menu_item(_group_label, *_menuitem); + return true; +} + +void +ComboToolItem::on_changed_combobox() { + + int row = _combobox->get_active_row_number(); + set_active( row ); + _changed.emit (_active); + _changed_after.emit (_active); +} + +void +ComboToolItem::on_toggled_radiomenu(int n) { + + // toggled emitted twice, first for button toggled off, second for button toggled on. + // We want to react only to the button turned on. + if ( n < _radiomenuitems.size() &&_radiomenuitems[ n ]->get_active()) { + set_active ( n ); + _changed.emit (_active); + _changed_after.emit (_active); + } +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/combo-tool-item.h b/src/ui/widget/combo-tool-item.h new file mode 100644 index 0000000..1fc8b00 --- /dev/null +++ b/src/ui/widget/combo-tool-item.h @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_COMBO_TOOL_ITEM +#define SEEN_COMBO_TOOL_ITEM + +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A combobox that can be displayed in a toolbar +*/ + +#include <gtkmm/toolitem.h> +#include <gtkmm/liststore.h> +#include <sigc++/sigc++.h> +#include <vector> + +namespace Gtk { +class Box; +class ComboBox; +class Label; +class MenuItem; +class RadioMenuItem; +} + +namespace Inkscape { +namespace UI { +namespace Widget { +class ComboToolItemColumns : public Gtk::TreeModel::ColumnRecord { +public: + ComboToolItemColumns() { + add (col_label); + add (col_value); + add (col_icon); + add (col_pixbuf); + add (col_data); // Used to store a pointer + add (col_tooltip); + add (col_sensitive); + } + Gtk::TreeModelColumn<Glib::ustring> col_label; + Gtk::TreeModelColumn<Glib::ustring> col_value; + Gtk::TreeModelColumn<Glib::ustring> col_icon; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > col_pixbuf; + Gtk::TreeModelColumn<void *> col_data; + Gtk::TreeModelColumn<Glib::ustring> col_tooltip; + Gtk::TreeModelColumn<bool> col_sensitive; +}; + + +class ComboToolItem : public Gtk::ToolItem { + +public: + static ComboToolItem* create(const Glib::ustring &label, + const Glib::ustring &tooltip, + const Glib::ustring &stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry = false); + + /* Style of combobox */ + void use_label( bool use_label ); + void use_icon( bool use_icon ); + void focus_on_click( bool focus_on_click ); + void use_pixbuf( bool use_pixbuf ); + void use_group_label( bool use_group_label ); // Applies to tool item only + + gint get_active() { return _active; } + Glib::ustring get_active_text(); + void set_active( gint active ); + void set_icon_size( Gtk::BuiltinIconSize size ) { _icon_size = size; } + + Glib::RefPtr<Gtk::ListStore> get_store() { return _store; } + + sigc::signal<void, int> signal_changed() { return _changed; } + sigc::signal<void, int> signal_changed_after() { return _changed_after; } + +protected: + bool on_create_menu_proxy() override; + void populate_combobox(); + + /* Signals */ + sigc::signal<void, int> _changed; + sigc::signal<void, int> _changed_after; // Needed for unit tracker which eats _changed. + +private: + + Glib::ustring _group_label; + Glib::ustring _tooltip; + Glib::ustring _stock_id; + Glib::RefPtr<Gtk::ListStore> _store; + + gint _active; /* Active menu item/button */ + + /* Style */ + bool _use_label; + bool _use_icon; // Applies to menu item only + bool _use_pixbuf; + Gtk::BuiltinIconSize _icon_size; + + /* Combobox in tool */ + Gtk::ComboBox* _combobox; + Gtk::Label* _group_label_widget; + Gtk::Box* _container; + + Gtk::MenuItem* _menuitem; + std::vector<Gtk::RadioMenuItem*> _radiomenuitems; + + /* Internal Callbacks */ + void on_changed_combobox(); + void on_toggled_radiomenu(int n); + + ComboToolItem(Glib::ustring group_label, + Glib::ustring tooltip, + Glib::ustring stock_id, + Glib::RefPtr<Gtk::ListStore> store, + bool has_entry = false); +}; +} +} +} +#endif /* SEEN_COMBO_TOOL_ITEM */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dash-selector.cpp b/src/ui/widget/dash-selector.cpp new file mode 100644 index 0000000..897b964 --- /dev/null +++ b/src/ui/widget/dash-selector.cpp @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Combobox for selecting dash patterns - implementation. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "dash-selector.h" + +#include <cstring> + +#include <glibmm/i18n.h> + +#include <2geom/coord.h> + +#include "preferences.h" + +#include "display/cairo-utils.h" + +#include "style.h" + +#include "ui/dialog-events.h" +#include "ui/widget/spinbutton.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +gchar const *const DashSelector::_prefs_path = "/palette/dashes"; + +static double dash_0[] = {-1.0}; +static double dash_1_1[] = {1.0, 1.0, -1.0}; +static double dash_2_1[] = {2.0, 1.0, -1.0}; +static double dash_4_1[] = {4.0, 1.0, -1.0}; +static double dash_1_2[] = {1.0, 2.0, -1.0}; +static double dash_1_4[] = {1.0, 4.0, -1.0}; + +static size_t BD_LEN = 7; // must correspond to the number of entries in the next line +static double *builtin_dashes[] = {dash_0, dash_1_1, dash_2_1, dash_4_1, dash_1_2, dash_1_4, nullptr}; + +static double **dashes = nullptr; + +DashSelector::DashSelector() + : preview_width(80), + preview_height(16), + preview_lineheight(2) +{ + set_spacing(4); + + // TODO: find something more sensible here!! + init_dashes(); + + dash_store = Gtk::ListStore::create(dash_columns); + dash_combo.set_model(dash_store); + dash_combo.pack_start(image_renderer); + dash_combo.set_cell_data_func(image_renderer, sigc::mem_fun(*this, &DashSelector::prepareImageRenderer)); + dash_combo.set_tooltip_text(_("Dash pattern")); + dash_combo.get_style_context()->add_class("combobright"); + dash_combo.show(); + dash_combo.signal_changed().connect( sigc::mem_fun(*this, &DashSelector::on_selection) ); + + this->pack_start(dash_combo, true, true, 0); + + offset = Gtk::Adjustment::create(0.0, 0.0, 10.0, 0.1, 1.0, 0.0); + offset->signal_value_changed().connect(sigc::mem_fun(*this, &DashSelector::offset_value_changed)); + auto sb = new Inkscape::UI::Widget::SpinButton(offset, 0.1, 2); + sb->set_tooltip_text(_("Pattern offset")); + sp_dialog_defocus_on_enter_cpp(sb); + sb->show(); + + this->pack_start(*sb, false, false, 0); + + int np=0; + while (dashes[np]){ np++;} + for (int i = 0; i<np-1; i++) { // all but the custom one go this way + // Add the dashes to the combobox + Gtk::TreeModel::Row row = *(dash_store->append()); + row[dash_columns.dash] = dashes[i]; + row[dash_columns.pixbuf] = Glib::wrap(sp_dash_to_pixbuf(dashes[i])); + } + // add the custom one + Gtk::TreeModel::Row row = *(dash_store->append()); + row[dash_columns.dash] = dashes[np-1]; + row[dash_columns.pixbuf] = Glib::wrap(sp_text_to_pixbuf((char *)"Custom")); + + this->set_data("pattern", dashes[0]); +} + +DashSelector::~DashSelector() { + // FIXME: for some reason this doesn't get called; does the call to manage() in + // sp_stroke_style_line_widget_new() not processed correctly? +} + +void DashSelector::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) { + + Glib::RefPtr<Gdk::Pixbuf> pixbuf = (*row)[dash_columns.pixbuf]; + image_renderer.property_pixbuf() = pixbuf; +} + +void DashSelector::init_dashes() { + + if (!dashes) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::vector<Glib::ustring> dash_prefs = prefs->getAllDirs(_prefs_path); + + int pos = 0; + if (!dash_prefs.empty()) { + SPStyle style; + dashes = g_new (double *, dash_prefs.size() + 2); // +1 for custom slot, +1 for terminator slot + + for (auto & dash_pref : dash_prefs) { + style.readFromPrefs( dash_pref ); + + if (!style.stroke_dasharray.values.empty()) { + dashes[pos] = g_new (double, style.stroke_dasharray.values.size() + 1); + double *d = dashes[pos]; + unsigned i = 0; + for (; i < style.stroke_dasharray.values.size(); i++) { + d[i] = style.stroke_dasharray.values[i].value; + } + d[i] = -1; + } else { + dashes[pos] = dash_0; + } + pos += 1; + } + } else { // This code may never execute - a new preferences.xml is created for a new user. Maybe if the user deletes dashes from preferences.xml? + dashes = g_new (double *, BD_LEN + 2); // +1 for custom slot, +1 for terminator slot + unsigned i; + for(i=0;i<BD_LEN;i++) { + dashes[i] = builtin_dashes[i]; + } + pos = BD_LEN; + } + // make a place to hold the custom dashes, up to 15 positions long (+ terminator) + dashes[pos] = g_new (double, 16); + double *d = dashes[pos]; + int i=0; + for(i=0;i<15;i++){ d[i]=i; } // have to put something in there, this is a pattern hopefully nobody would choose + d[15]=-1.0; + // final terminator + dashes[++pos] = nullptr; + } +} + +void DashSelector::set_dash (int ndash, double *dash, double o) +{ + int pos = -1; // Allows custom patterns to remain unscathed by this. + int count = 0; // will hold the NULL terminator at the end of the dashes list + if (ndash > 0) { + double delta = 0.0; + for (int i = 0; i < ndash; i++) + delta += dash[i]; + delta /= 1000.0; + + for (int i = 0; dashes[i]; i++,count++) { + double *pattern = dashes[i]; + int np = 0; + while (pattern[np] >= 0.0) + np += 1; + if (np == ndash) { + int j; + for (j = 0; j < ndash; j++) { + if (!Geom::are_near(dash[j], pattern[j], delta)) { + break; + } + } + if (j == ndash) { + pos = i; + break; + } + } + } + } + else if(ndash==0) { + pos = 0; + } + if(pos>=0){ + this->set_data("pattern", dashes[pos]); + this->dash_combo.set_active(pos); + this->offset->set_value(o); + if(pos == 10) { + this->offset->set_value(10.0); + } + } + else { // Hit a custom pattern in the SVG, write it into the combobox. + count--; // the one slot for custom patterns + double *d = dashes[count]; + int i=0; + for(i=0;i< (ndash > 15 ? 15 : ndash) ;i++) { + d[i]=dash[i]; + } // store the custom pattern + d[ndash]=-1.0; //terminate it + this->set_data("pattern", dashes[count]); + this->dash_combo.set_active(count); + this->offset->set_value(o); // what does this do???? + } +} + +void DashSelector::get_dash(int *ndash, double **dash, double *off) +{ + double *pattern = (double*) this->get_data("pattern"); + + int nd = 0; + while (pattern[nd] >= 0.0) + nd += 1; + + if (nd > 0) { + if (ndash) + *ndash = nd; + if (dash) { + *dash = g_new (double, nd); + memcpy (*dash, pattern, nd * sizeof (double)); + } + if (off) + *off = offset->get_value(); + } else { + if (ndash) + *ndash = 0; + if (dash) + *dash = nullptr; + if (off) + *off = 0.0; + } +} + +/** + * Fill a pixbuf with the dash pattern using standard cairo drawing + */ +GdkPixbuf* DashSelector::sp_dash_to_pixbuf(double *pattern) +{ + int n_dashes; + for (n_dashes = 0; pattern[n_dashes] >= 0.0; n_dashes ++) ; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, preview_width, preview_height); + cairo_t *ct = cairo_create(s); + + cairo_set_line_width (ct, preview_lineheight); + cairo_scale (ct, preview_lineheight, 1); + //cairo_set_source_rgb (ct, 0, 0, 0); + cairo_move_to (ct, 0, preview_height/2); + cairo_line_to (ct, preview_width, preview_height/2); + cairo_set_dash(ct, pattern, n_dashes, 0); + cairo_stroke (ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +/** + * Fill a pixbuf with a text label using standard cairo drawing + */ +GdkPixbuf* DashSelector::sp_text_to_pixbuf(char *text) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, preview_width, preview_height); + cairo_t *ct = cairo_create(s); + + cairo_select_font_face (ct, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); + cairo_set_font_size (ct, 12.0); + cairo_set_source_rgb (ct, 0.0, 0.0, 0.0); + cairo_move_to (ct, 16.0, 13.0); + cairo_show_text (ct, text); + + cairo_stroke (ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + +void DashSelector::on_selection () +{ + double *pattern = dash_combo.get_active()->get_value(dash_columns.dash); + this->set_data ("pattern", pattern); + + changed_signal.emit(); +} + +void DashSelector::offset_value_changed() +{ + changed_signal.emit(); +} +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dash-selector.h b/src/ui/widget/dash-selector.h new file mode 100644 index 0000000..449392a --- /dev/null +++ b/src/ui/widget/dash-selector.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DASH_SELECTOR_NEW_H +#define SEEN_SP_DASH_SELECTOR_NEW_H + +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert> (gtkmm-ification) + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> + +#include <sigc++/signal.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Class that wraps a combobox and spinbutton for selecting dash patterns. + */ +class DashSelector : public Gtk::HBox { +public: + DashSelector(); + ~DashSelector() override; + + /** + * Get and set methods for dashes + */ + void set_dash(int ndash, double *dash, double offset); + void get_dash(int *ndash, double **dash, double *offset); + + sigc::signal<void> changed_signal; + +private: + + /** + * Initialize dashes list from preferences + */ + static void init_dashes(); + + /** + * Fill a pixbuf with the dash pattern using standard cairo drawing + */ + GdkPixbuf* sp_dash_to_pixbuf(double *pattern); + + /** + * Fill a pixbuf with text standard cairo drawing + */ + GdkPixbuf* sp_text_to_pixbuf(char *text); + + /** + * Callback for combobox image renderer + */ + void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ); + + /** + * Callback for offset adjustment changing + */ + void offset_value_changed(); + + /** + * Callback for combobox selection changing + */ + void on_selection(); + + /** + * Combobox columns + */ + class DashColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<double *> dash; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > pixbuf; + + DashColumns() { + add(dash); add(pixbuf); + } + }; + DashColumns dash_columns; + Glib::RefPtr<Gtk::ListStore> dash_store; + Gtk::ComboBox dash_combo; + Gtk::CellRendererPixbuf image_renderer; + Glib::RefPtr<Gtk::Adjustment> offset; + + static gchar const *const _prefs_path; + int preview_width; + int preview_height; + int preview_lineheight; + +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_SP_DASH_SELECTOR_NEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dock-item.cpp b/src/ui/widget/dock-item.cpp new file mode 100644 index 0000000..01786d5 --- /dev/null +++ b/src/ui/widget/dock-item.cpp @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/dock.h" + +#include "desktop.h" +#include "inkscape.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include <glibmm/exceptionhandler.h> +#include <gtkmm/icontheme.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +DockItem::DockItem(Dock& dock, const Glib::ustring& name, const Glib::ustring& long_name, + const Glib::ustring& icon_name, State state, GdlDockPlacement placement) : + _dock(dock), + _prev_state(state), + _prev_position(0), + _window(nullptr), + _x(0), + _y(0), + _grab_focus_on_realize(false), + _gdl_dock_item(nullptr) +{ + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + GdlDockItemBehavior gdl_dock_behavior = + (prefs->getBool("/options/dock/cancenterdock", true) ? + GDL_DOCK_ITEM_BEH_NORMAL : + GDL_DOCK_ITEM_BEH_CANT_DOCK_CENTER); + + + if (!icon_name.empty()) { + _icon_pixbuf = sp_get_icon_pixbuf(icon_name, "/toolbox/secondary"); + } + + if ( _icon_pixbuf ) { + _gdl_dock_item = gdl_dock_item_new_with_pixbuf_icon( name.c_str(), long_name.c_str(), + _icon_pixbuf->gobj(), gdl_dock_behavior ); + } else { + _gdl_dock_item = gdl_dock_item_new(name.c_str(), long_name.c_str(), gdl_dock_behavior); + } + + _frame.set_shadow_type(Gtk::SHADOW_IN); + gtk_container_add (GTK_CONTAINER (_gdl_dock_item), GTK_WIDGET (_frame.gobj())); + _frame.add(_dock_item_box); + _dock_item_box.set_border_width(3); + + signal_drag_begin().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDragBegin)); + signal_drag_end().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDragEnd)); + signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onHide), false); + signal_show().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onShow), false); + signal_state_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onStateChanged)); + signal_delete_event().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onDeleteEvent)); + signal_realize().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onRealize)); + + _dock.addItem(*this, ( _prev_state == FLOATING_STATE || _prev_state == ICONIFIED_FLOATING_STATE ) ? GDL_DOCK_FLOATING : placement); + + if (_prev_state == ICONIFIED_FLOATING_STATE || _prev_state == ICONIFIED_DOCKED_STATE) { + iconify(); + } + + show_all(); + +} + +DockItem::~DockItem() +{ + g_free(_gdl_dock_item); +} + +Gtk::Widget& +DockItem::getWidget() +{ + return *Glib::wrap(GTK_WIDGET(_gdl_dock_item)); +} + +GtkWidget * +DockItem::gobj() +{ + return _gdl_dock_item; +} + +Gtk::VBox * +DockItem::get_vbox() +{ + return &_dock_item_box; +} + + +void +DockItem::get_position(int& x, int& y) +{ + if (getWindow()) { + getWindow()->get_position(x, y); + } else { + x = _x; + y = _y; + } +} + +void +DockItem::get_size(int& width, int& height) +{ + if (getWindow()) { + getWindow()->get_size(width, height); + } else { + width = get_vbox()->get_width(); + height = get_vbox()->get_height(); + } +} + + +void +DockItem::resize(int width, int height) +{ + if (_window) + _window->resize(width, height); +} + + +void +DockItem::move(int x, int y) +{ + if (_window) + _window->move(x, y); +} + +void +DockItem::set_position(Gtk::WindowPosition position) +{ + if (_window) + _window->set_position(position); +} + +void +DockItem::set_size_request(int width, int height) +{ + getWidget().set_size_request(width, height); +} + +void DockItem::size_request(Gtk::Requisition& requisition) +{ + Gtk::Requisition req_natural; + getWidget().get_preferred_size(req_natural, requisition); +} + +void +DockItem::set_title(Glib::ustring title) +{ + g_object_set (_gdl_dock_item, + "long-name", title.c_str(), + NULL); + + gdl_dock_item_set_tablabel(GDL_DOCK_ITEM(_gdl_dock_item), + gtk_label_new (title.c_str())); +} + +bool +DockItem::isAttached() const +{ + return GDL_DOCK_OBJECT_ATTACHED (_gdl_dock_item); +} + + +bool +DockItem::isFloating() const +{ + return (GTK_WIDGET(gdl_dock_object_get_toplevel(GDL_DOCK_OBJECT (_gdl_dock_item))) != + _dock.getGdlWidget()); +} + +bool +DockItem::isIconified() const +{ + return GDL_DOCK_ITEM_ICONIFIED (_gdl_dock_item); +} + +DockItem::State +DockItem::getState() const +{ + if (isIconified() && _prev_state == FLOATING_STATE) { + return ICONIFIED_FLOATING_STATE; + } else if (isIconified()) { + return ICONIFIED_DOCKED_STATE; + } else if (isFloating() && isAttached()) { + return FLOATING_STATE; + } else if (isAttached()) { + return DOCKED_STATE; + } + + return UNATTACHED; +} + +DockItem::State +DockItem::getPrevState() const +{ + return _prev_state; +} + +GdlDockPlacement +DockItem::getPlacement() const +{ + GdlDockPlacement placement = GDL_DOCK_TOP; + GdlDockObject *parent = gdl_dock_object_get_parent_object (GDL_DOCK_OBJECT(_gdl_dock_item)); + if (parent) { + gdl_dock_object_child_placement(parent, GDL_DOCK_OBJECT(_gdl_dock_item), &placement); + } + + return placement; +} + +void +DockItem::hide() +{ + gdl_dock_item_hide_item (GDL_DOCK_ITEM(_gdl_dock_item)); +} + +void +DockItem::show() +{ + gdl_dock_item_show_item (GDL_DOCK_ITEM(_gdl_dock_item)); +} + +void +DockItem::iconify() +{ + gdl_dock_item_iconify_item (GDL_DOCK_ITEM(_gdl_dock_item)); +} + +void +DockItem::show_all() +{ + gtk_widget_show_all(_gdl_dock_item); +} + +void +DockItem::present() +{ + gdl_dock_object_present(GDL_DOCK_OBJECT(_gdl_dock_item), nullptr); + + // always grab focus, even if we're already present + grab_focus(); + + if (!isFloating() && getWidget().get_realized()) + _dock.scrollToItem(*this); +} + + +void +DockItem::grab_focus() +{ + if (gtk_widget_get_realized (_gdl_dock_item)) { + + // make sure the window we're in is present + Gtk::Widget *toplevel = getWidget().get_toplevel(); + if (Gtk::Window *window = dynamic_cast<Gtk::Window *>(toplevel)) { + window->present(); + } + + gtk_widget_grab_focus (_gdl_dock_item); + + } else { + _grab_focus_on_realize = true; + } +} + + +/* Signal wrappers */ + +Glib::SignalProxy0<void> +DockItem::signal_show() +{ + return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_show_proxy); +} + +Glib::SignalProxy0<void> +DockItem::signal_hide() +{ + return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_hide_proxy); +} + +Glib::SignalProxy1<bool, GdkEventAny *> +DockItem::signal_delete_event() +{ + return Glib::SignalProxy1<bool, GdkEventAny *>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_delete_event_proxy); +} + +Glib::SignalProxy0<void> +DockItem::signal_drag_begin() +{ + return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_drag_begin_proxy); +} + +Glib::SignalProxy1<void, bool> +DockItem::signal_drag_end() +{ + return Glib::SignalProxy1<void, bool>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_drag_end_proxy); +} + +Glib::SignalProxy0<void> +DockItem::signal_realize() +{ + return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock_item)), + &_signal_realize_proxy); +} + +sigc::signal<void, DockItem::State, DockItem::State> +DockItem::signal_state_changed() +{ + return _signal_state_changed; +} + +void +DockItem::_onHideWindow() +{ + if (_window) + _window->get_position(_x, _y); +} + +void +DockItem::_onHide() +{ + if (_prev_state == ICONIFIED_DOCKED_STATE) + _prev_state = DOCKED_STATE; + else if (_prev_state == ICONIFIED_FLOATING_STATE) + _prev_state = FLOATING_STATE; + + _signal_state_changed.emit(UNATTACHED, getState()); +} + +void +DockItem::_onShow() +{ + _signal_state_changed.emit(UNATTACHED, getState()); +} + +void +DockItem::_onDragBegin() +{ + _prev_state = getState(); + if (_prev_state == FLOATING_STATE) + _dock.toggleDockable(getWidget().get_width(), getWidget().get_height()); +} + +void +DockItem::_onDragEnd(bool) +{ + State state = getState(); + + if (state != _prev_state) + _signal_state_changed.emit(_prev_state, state); + + if (state == FLOATING_STATE) { + if (_prev_state == FLOATING_STATE) + _dock.toggleDockable(); + } + + _prev_state = state; +} + +void +DockItem::_onRealize() +{ + if (_grab_focus_on_realize) { + _grab_focus_on_realize = false; + grab_focus(); + } +} + +bool +DockItem::_onKeyPress(GdkEventKey *event) +{ + gboolean return_value; + g_signal_emit_by_name (_gdl_dock_item, "key_press_event", event, &return_value); + return return_value; +} + +void +DockItem::_onStateChanged(State /*prev_state*/, State new_state) +{ + _window = getWindow(); + if(_window) + _window->set_type_hint(Gdk::WINDOW_TYPE_HINT_NORMAL); + + if (new_state == FLOATING_STATE && _window) { + _window->signal_hide().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onHideWindow)); + _signal_key_press_event_connection = + _window->signal_key_press_event().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::DockItem::_onKeyPress)); + } +} + + +bool +DockItem::_onDeleteEvent(GdkEventAny */*event*/) +{ + hide(); + return false; +} + + +Gtk::Window * +DockItem::getWindow() +{ + g_return_val_if_fail(_gdl_dock_item, 0); + Gtk::Container *parent = getWidget().get_parent(); + parent = (parent ? parent->get_parent() : nullptr); + return (parent ? dynamic_cast<Gtk::Window *>(parent) : nullptr); +} + +const Glib::SignalProxyInfo +DockItem::_signal_show_proxy = +{ + "show", + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback, + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback +}; + +const Glib::SignalProxyInfo +DockItem::_signal_hide_proxy = +{ + "hide", + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback, + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback +}; + + +const Glib::SignalProxyInfo +DockItem::_signal_delete_event_proxy = +{ + "delete_event", + (GCallback) &_signal_delete_event_callback, + (GCallback) &_signal_delete_event_callback +}; + + +const Glib::SignalProxyInfo +DockItem::_signal_drag_begin_proxy = +{ + "dock-drag-begin", + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback, + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback +}; + + +const Glib::SignalProxyInfo +DockItem::_signal_drag_end_proxy = +{ + "dock_drag_end", + (GCallback) &_signal_drag_end_callback, + (GCallback) &_signal_drag_end_callback +}; + + +const Glib::SignalProxyInfo +DockItem::_signal_realize_proxy = +{ + "realize", + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback, + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback +}; + + +gboolean +DockItem::_signal_delete_event_callback(GtkWidget *self, GdkEventAny *event, void *data) +{ + using namespace Gtk; + typedef sigc::slot<bool, GdkEventAny *> SlotType; + + if (Glib::ObjectBase::_get_current_wrapper((GObject *) self)) { + try { + if(sigc::slot_base *const slot = Glib::SignalProxyNormal::data_to_slot(data)) + return static_cast<int>( (*static_cast<SlotType*>(slot))(event) ); + } catch(...) { + Glib::exception_handlers_invoke(); + } + } + + typedef gboolean RType; + return RType(); +} + +void +DockItem::_signal_drag_end_callback(GtkWidget *self, gboolean cancelled, void *data) +{ + using namespace Gtk; + typedef sigc::slot<void, bool> SlotType; + + if (Glib::ObjectBase::_get_current_wrapper((GObject *) self)) { + try { + if(sigc::slot_base *const slot = Glib::SignalProxyNormal::data_to_slot(data)) + (*static_cast<SlotType *>(slot))(cancelled); + } catch(...) { + Glib::exception_handlers_invoke(); + } + } +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dock-item.h b/src/ui/widget/dock-item.h new file mode 100644 index 0000000..be7ac77 --- /dev/null +++ b/src/ui/widget/dock-item.h @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_UI_WIGET_DOCK_ITEM_H +#define INKSCAPE_UI_WIGET_DOCK_ITEM_H + +#include <gtkmm/box.h> +#include <gtkmm/frame.h> +#include <gtkmm/window.h> + +#include <gdl/gdl.h> + +namespace Gtk { + class HButtonBox; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Dock; + +/** + * A custom wrapper around gdl-dock-item. + */ +class DockItem { + +public: + + enum State { UNATTACHED, // item not bound to the dock (a temporary state) + FLOATING_STATE, // item not in its dock (but can be docked in other, + // e.g. floating, docks) + DOCKED_STATE, // item in its assigned dock + ICONIFIED_DOCKED_STATE, // item iconified in its assigned dock from dock + ICONIFIED_FLOATING_STATE}; // item iconified in its assigned dock from float + + DockItem(Dock& dock, const Glib::ustring& name, const Glib::ustring& long_name, + const Glib::ustring& icon_name, State state, GdlDockPlacement placement); + + ~DockItem(); + + Gtk::Widget& getWidget(); + GtkWidget *gobj(); + + Gtk::VBox *get_vbox(); + + void get_position(int& x, int& y); + void get_size(int& width, int& height); + + void resize(int width, int height); + void move(int x, int y); + void set_position(Gtk::WindowPosition); + void set_size_request(int width, int height); + void size_request(Gtk::Requisition& requisition); + void set_title(Glib::ustring title); + + bool isAttached() const; + bool isFloating() const; + bool isIconified() const; + State getState() const; + State getPrevState() const; + GdlDockPlacement getPlacement() const; + + Gtk::Window *getWindow(); //< gives the parent window, if the dock item has one (i.e. it's floating) + + void hide(); + void show(); + void iconify(); + void show_all(); + + void present(); + + void grab_focus(); + + Glib::SignalProxy0<void> signal_show(); + Glib::SignalProxy0<void> signal_hide(); + Glib::SignalProxy1<bool, GdkEventAny *> signal_delete_event(); + Glib::SignalProxy0<void> signal_drag_begin(); + Glib::SignalProxy1<void, bool> signal_drag_end(); + Glib::SignalProxy0<void> signal_realize(); + + sigc::signal<void, State, State> signal_state_changed(); + +private: + Dock &_dock; //< parent dock + + State _prev_state; //< last known state + + int _prev_position; + + Gtk::Window *_window; //< reference to floating window, if any + int _x, _y; //< last known position of window, if floating + + bool _grab_focus_on_realize; //< if the dock item should grab focus on the next realize + + GtkWidget *_gdl_dock_item; + Glib::RefPtr<Gdk::Pixbuf> _icon_pixbuf; + + /** Interface widgets, will be packed like + * gdl_dock_item -> _frame -> _dock_item_box + */ + Gtk::Frame _frame; + Gtk::VBox _dock_item_box; + + /** Internal signal handlers */ + void _onHide(); + void _onHideWindow(); + void _onShow(); + void _onDragBegin(); + void _onDragEnd(bool cancelled); + void _onRealize(); + + bool _onKeyPress(GdkEventKey *event); + void _onStateChanged(State prev_state, State new_state); + bool _onDeleteEvent(GdkEventAny *event); + + sigc::connection _signal_key_press_event_connection; + + /** GdlDockItem signal proxy structures */ + static const Glib::SignalProxyInfo _signal_show_proxy; + static const Glib::SignalProxyInfo _signal_hide_proxy; + static const Glib::SignalProxyInfo _signal_delete_event_proxy; + + static const Glib::SignalProxyInfo _signal_drag_begin_proxy; + static const Glib::SignalProxyInfo _signal_drag_end_proxy; + static const Glib::SignalProxyInfo _signal_realize_proxy; + + static gboolean _signal_delete_event_callback(GtkWidget *self, GdkEventAny *event, void *data); + static void _signal_drag_end_callback(GtkWidget* self, gboolean p0, void* data); + + sigc::signal<void, State, State> _signal_state_changed; + + DockItem() = delete; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIGET_DOCK_ITEM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dock.cpp b/src/ui/widget/dock.cpp new file mode 100644 index 0000000..9720296 --- /dev/null +++ b/src/ui/widget/dock.cpp @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * A desktop dock pane to dock dialogs. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "dock.h" +#include "inkscape.h" +#include "preferences.h" +#include "desktop.h" + +#include <gtkmm/adjustment.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +namespace { + +void hideCallback(GObject * /*object*/, gpointer dock_ptr) +{ + g_return_if_fail( dock_ptr != nullptr ); + + Dock *dock = static_cast<Dock *>(dock_ptr); + dock->hide(); +} + +void unhideCallback(GObject * /*object*/, gpointer dock_ptr) +{ + g_return_if_fail( dock_ptr != nullptr ); + + Dock *dock = static_cast<Dock *>(dock_ptr); + dock->show(); +} + +} + +const int Dock::_default_empty_width = 0; +const int Dock::_default_dock_bar_width = 36; + + +Dock::Dock(Gtk::Orientation orientation) + : _gdl_dock(gdl_dock_new()), +#if WITH_GDL_3_6 + _gdl_dock_bar(GDL_DOCK_BAR(gdl_dock_bar_new(G_OBJECT(_gdl_dock)))), +#else + _gdl_dock_bar(GDL_DOCK_BAR(gdl_dock_bar_new(GDL_DOCK(_gdl_dock)))), +#endif + _scrolled_window (Gtk::manage(new Gtk::ScrolledWindow)) +{ + gtk_widget_set_name(_gdl_dock, "GdlDock"); + +#if WITH_GDL_3_6 + gtk_orientable_set_orientation(GTK_ORIENTABLE(_gdl_dock_bar), + static_cast<GtkOrientation>(orientation)); +#else + gdl_dock_bar_set_orientation(_gdl_dock_bar, + static_cast<GtkOrientation>(orientation)); +#endif + + _filler.set_name("DockBoxFiller"); + + _paned = Gtk::manage(new Gtk::Paned(orientation)); + _paned->set_name("DockBoxPane"); + _paned->pack1(*Glib::wrap(GTK_WIDGET(_gdl_dock)), false, false); + _paned->pack2(_filler, true, false); + // resize, shrink + + _dock_box = Gtk::manage(new Gtk::Box(orientation == Gtk::ORIENTATION_HORIZONTAL ? + Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL)); + _dock_box->set_name("DockBox"); + _dock_box->pack_start(*_paned, Gtk::PACK_EXPAND_WIDGET); + _dock_box->pack_end(*Gtk::manage(Glib::wrap(GTK_WIDGET(_gdl_dock_bar))), Gtk::PACK_SHRINK); + + _scrolled_window->set_name("DockScrolledWindow"); + _scrolled_window->add(*_dock_box); + _scrolled_window->set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + _scrolled_window->set_size_request(0); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + GdlSwitcherStyle gdl_switcher_style = + static_cast<GdlSwitcherStyle>(prefs->getIntLimited("/options/dock/switcherstyle", + GDL_SWITCHER_STYLE_BOTH, 0, 4)); + + GdlDockMaster* master = nullptr; + + g_object_get(GDL_DOCK_OBJECT(_gdl_dock), + "master", &master, + NULL); + + g_object_set(master, + "switcher-style", gdl_switcher_style, + NULL); + + GdlDockBarStyle gdl_dock_bar_style = + static_cast<GdlDockBarStyle>(prefs->getIntLimited("/options/dock/dockbarstyle", + GDL_DOCK_BAR_BOTH, 0, 3)); + + gdl_dock_bar_set_style(_gdl_dock_bar, gdl_dock_bar_style); + + + INKSCAPE.signal_dialogs_hide.connect(sigc::mem_fun(*this, &Dock::hide)); + INKSCAPE.signal_dialogs_unhide.connect(sigc::mem_fun(*this, &Dock::show)); + + g_signal_connect(_paned->gobj(), "button-press-event", G_CALLBACK(_on_paned_button_event), (void *)this); + g_signal_connect(_paned->gobj(), "button-release-event", G_CALLBACK(_on_paned_button_event), (void *)this); + + signal_layout_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Widget::Dock::_onLayoutChanged)); +} + +Dock::~Dock() +{ + g_free(_gdl_dock); + g_free(_gdl_dock_bar); +} + +void Dock::addItem(DockItem& item, GdlDockPlacement placement) +{ + _dock_items.push_back(&item); + + gdl_dock_add_item(GDL_DOCK(_gdl_dock), + GDL_DOCK_ITEM(item.gobj()), + placement); +} + +Gtk::Widget &Dock::getWidget() +{ + return *_scrolled_window; +} + +Gtk::Paned *Dock::getParentPaned() +{ + g_return_val_if_fail(_dock_box, 0); + Gtk::Container *parent = getWidget().get_parent(); + return (parent != nullptr ? dynamic_cast<Gtk::Paned *>(parent) : nullptr); +} + + +Gtk::Paned *Dock::getPaned() +{ + return _paned; +} + +GtkWidget *Dock::getGdlWidget() +{ + return GTK_WIDGET(_gdl_dock); +} + +bool Dock::isEmpty() const +{ + std::list<const DockItem *>::const_iterator + i = _dock_items.begin(), + e = _dock_items.end(); + + for (; i != e; ++i) { + if ((*i)->getState() == DockItem::DOCKED_STATE) { + return false; + } + } + + return true; +} + +bool Dock::hasIconifiedItems() const +{ + std::list<const DockItem *>::const_iterator + i = _dock_items.begin(), + e = _dock_items.end(); + + for (; i != e; ++i) { + if ((*i)->isIconified()) { + return true; + } + } + + return false; +} + +void Dock::hide() +{ + getWidget().hide(); +} + +void Dock::show() +{ + getWidget().show(); +} + +void Dock::toggleDockable(int width, int height) +{ + static int prev_horizontal_position, prev_vertical_position; + + Gtk::Paned *parent_paned = getParentPaned(); + + if (width > 0 && height > 0) { + prev_horizontal_position = parent_paned->get_position(); + prev_vertical_position = _paned->get_position(); + + if (getWidget().get_width() < width) + parent_paned->set_position(parent_paned->get_width() - width); + + if (_paned->get_position() < height) + _paned->set_position(height); + + } else { + parent_paned->set_position(prev_horizontal_position); + _paned->set_position(prev_vertical_position); + } +} + +void Dock::scrollToItem(DockItem& item) +{ + int item_x, item_y; + item.getWidget().translate_coordinates(getWidget(), 0, 0, item_x, item_y); + + int dock_height = getWidget().get_height(), item_height = item.getWidget().get_height(); + double vadjustment = _scrolled_window->get_vadjustment()->get_value(); + + if (item_y < 0) + _scrolled_window->get_vadjustment()->set_value(vadjustment + item_y); + else if (item_y + item_height > dock_height) + _scrolled_window->get_vadjustment()->set_value( + vadjustment + ((item_y + item_height) - dock_height)); +} + +Glib::SignalProxy0<void> +Dock::signal_layout_changed() +{ + return Glib::SignalProxy0<void>(Glib::wrap(GTK_WIDGET(_gdl_dock)), + &_signal_layout_changed_proxy); +} + +void Dock::_onLayoutChanged() +{ + if (isEmpty()) { + if (hasIconifiedItems()) { + _paned->get_child1()->set_size_request(-1, -1); + _scrolled_window->set_size_request(_default_dock_bar_width); + } else { + _paned->get_child1()->set_size_request(-1, -1); + _scrolled_window->set_size_request(_default_empty_width); + } + getParentPaned()->set_position(10000); + + } else { + // unset any forced size requests + _paned->get_child1()->set_size_request(-1, -1); + _scrolled_window->set_size_request(-1); + } +} + +void +Dock::_onPanedButtonEvent(GdkEventButton *event) +{ + if (event->button == 1 && event->type == GDK_BUTTON_PRESS) + /* unset size request when starting a drag */ + _paned->get_child1()->set_size_request(-1, -1); +} + +gboolean +Dock::_on_paned_button_event(GtkWidget */*widget*/, GdkEventButton *event, gpointer user_data) +{ + if (Dock *dock = static_cast<Dock *>(user_data)) + dock->_onPanedButtonEvent(event); + + return FALSE; +} + +const Glib::SignalProxyInfo +Dock::_signal_layout_changed_proxy = +{ + "layout-changed", + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback, + (GCallback) &Glib::SignalProxyNormal::slot0_void_callback +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/dock.h b/src/ui/widget/dock.h new file mode 100644 index 0000000..f061f59 --- /dev/null +++ b/src/ui/widget/dock.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief A desktop dock pane to dock dialogs, a custom wrapper around gdl-dock. + */ +/* Author: + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_DOCK_H +#define INKSCAPE_UI_WIDGET_DOCK_H + +#include <gtkmm/box.h> +#include <list> +#include "ui/widget/dock-item.h" + +struct _GdlDock; +typedef _GdlDock GdlDock; +struct _GdlDockBar; +typedef _GdlDockBar GdlDockBar; + +namespace Gtk { +class Paned; +class ScrolledWindow; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Dock { + +public: + + Dock(Gtk::Orientation orientation=Gtk::ORIENTATION_VERTICAL); + ~Dock(); + + void addItem(DockItem& item, GdlDockPlacement placement); + + Gtk::Widget& getWidget(); //< return the top widget + Gtk::Paned *getParentPaned(); + Gtk::Paned *getPaned(); + + GtkWidget* getGdlWidget(); //< return the top gdl widget + + bool isEmpty() const; //< true iff none of the dock's items are in a docked state + bool hasIconifiedItems() const; + + Glib::SignalProxy0<void> signal_layout_changed(); + + void hide(); + void show(); + + /** Toggle size of dock between the previous dimensions and the ones sent as parameters */ + void toggleDockable(int width=0, int height=0); + + /** Scrolls the scrolled window container to make the provided dock item visible, if needed */ + void scrollToItem(DockItem& item); + +protected: + + std::list<const DockItem *> _dock_items; //< added dock items + + /** Interface widgets, will be packed like + * _scrolled_window -> (_dock_box -> (_paned -> (_dock -> _filler) | _dock_bar)) + */ + Gtk::Box *_dock_box; + Gtk::Paned *_paned; + GtkWidget *_gdl_dock; + GdlDockBar *_gdl_dock_bar; + Gtk::Box _filler; + Gtk::ScrolledWindow *_scrolled_window; + + /** Internal signal handlers */ + void _onLayoutChanged(); + void _onPanedButtonEvent(GdkEventButton *event); + + static gboolean _on_paned_button_event(GtkWidget *widget, GdkEventButton *event, + gpointer user_data); + + /** GdlDock signal proxy structures */ + static const Glib::SignalProxyInfo _signal_layout_changed_proxy; + + /** Standard widths */ + static const int _default_empty_width; + static const int _default_dock_bar_width; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_DIALOG_BEHAVIOUR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/ui/widget/entity-entry.cpp b/src/ui/widget/entity-entry.cpp new file mode 100644 index 0000000..6e87459 --- /dev/null +++ b/src/ui/widget/entity-entry.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2000 - 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "entity-entry.h" + +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/entry.h> + +#include "document-undo.h" +#include "inkscape.h" +#include "preferences.h" +#include "rdf.h" +#include "verbs.h" + +#include "object/sp-root.h" + +#include "ui/widget/registry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + +EntityEntry* +EntityEntry::create (rdf_work_entity_t* ent, Registry& wr) +{ + g_assert (ent); + EntityEntry* obj = nullptr; + switch (ent->format) + { + case RDF_FORMAT_LINE: + obj = new EntityLineEntry (ent, wr); + break; + case RDF_FORMAT_MULTILINE: + obj = new EntityMultiLineEntry (ent, wr); + break; + default: + g_warning ("An unknown RDF format was requested."); + } + + g_assert (obj); + obj->_label.show(); + return obj; +} + +EntityEntry::EntityEntry (rdf_work_entity_t* ent, Registry& wr) + : _label(Glib::ustring(_(ent->title)), Gtk::ALIGN_END), + _packable(nullptr), + _entity(ent), _wr(&wr) +{ +} + +EntityEntry::~EntityEntry() +{ + _changed_connection.disconnect(); +} + +void EntityEntry::save_to_preferences(SPDocument *doc) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + const gchar *text = rdf_get_work_entity (doc, _entity); + prefs->setString(PREFS_METADATA + Glib::ustring(_entity->name), Glib::ustring(text ? text : "")); +} + +EntityLineEntry::EntityLineEntry (rdf_work_entity_t* ent, Registry& wr) +: EntityEntry (ent, wr) +{ + Gtk::Entry *e = new Gtk::Entry; + e->set_tooltip_text (_(ent->tip)); + _packable = e; + _changed_connection = e->signal_changed().connect (sigc::mem_fun (*this, &EntityLineEntry::on_changed)); +} + +EntityLineEntry::~EntityLineEntry() +{ + delete static_cast<Gtk::Entry*>(_packable); +} + +void EntityLineEntry::update(SPDocument *doc) +{ + const char *text = rdf_get_work_entity (doc, _entity); + // If RDF title is not set, get the document's <title> and set the RDF: + if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) { + text = doc->getRoot()->title(); + rdf_set_work_entity(doc, _entity, text); + } + static_cast<Gtk::Entry*>(_packable)->set_text (text ? text : ""); +} + + +void EntityLineEntry::load_from_preferences() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name)); + if (text.length() > 0) { + static_cast<Gtk::Entry*>(_packable)->set_text (text.c_str()); + } +} + +void +EntityLineEntry::on_changed() +{ + if (_wr->isUpdating()) return; + + _wr->setUpdating (true); + SPDocument *doc = SP_ACTIVE_DOCUMENT; + Glib::ustring text = static_cast<Gtk::Entry*>(_packable)->get_text(); + if (rdf_set_work_entity (doc, _entity, text.c_str())) { + if (doc->isSensitive()) { + DocumentUndo::done(doc, SP_VERB_NONE, "Document metadata updated"); + } + } + _wr->setUpdating (false); +} + +EntityMultiLineEntry::EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr) +: EntityEntry (ent, wr) +{ + Gtk::ScrolledWindow *s = new Gtk::ScrolledWindow; + s->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + s->set_shadow_type (Gtk::SHADOW_IN); + _packable = s; + _v.set_size_request (-1, 35); + _v.set_wrap_mode (Gtk::WRAP_WORD); + _v.set_accepts_tab (false); + s->add (_v); + _v.set_tooltip_text (_(ent->tip)); + _changed_connection = _v.get_buffer()->signal_changed().connect (sigc::mem_fun (*this, &EntityMultiLineEntry::on_changed)); +} + +EntityMultiLineEntry::~EntityMultiLineEntry() +{ + delete static_cast<Gtk::ScrolledWindow*>(_packable); +} + +void EntityMultiLineEntry::update(SPDocument *doc) +{ + const char *text = rdf_get_work_entity (doc, _entity); + // If RDF title is not set, get the document's <title> and set the RDF: + if ( !text && !strcmp(_entity->name, "title") && doc->getRoot() ) { + text = doc->getRoot()->title(); + rdf_set_work_entity(doc, _entity, text); + } + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + tv->get_buffer()->set_text (text ? text : ""); +} + + +void EntityMultiLineEntry::load_from_preferences() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring text = prefs->getString(PREFS_METADATA + Glib::ustring(_entity->name)); + if (text.length() > 0) { + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + tv->get_buffer()->set_text (text.c_str()); + } +} + + +void +EntityMultiLineEntry::on_changed() +{ + if (_wr->isUpdating()) return; + + _wr->setUpdating (true); + SPDocument *doc = SP_ACTIVE_DOCUMENT; + Gtk::ScrolledWindow *s = static_cast<Gtk::ScrolledWindow*>(_packable); + Gtk::TextView *tv = static_cast<Gtk::TextView*>(s->get_child()); + Glib::ustring text = tv->get_buffer()->get_text(); + if (rdf_set_work_entity (doc, _entity, text.c_str())) { + DocumentUndo::done(doc, SP_VERB_NONE, "Document metadata updated"); + } + _wr->setUpdating (false); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/entity-entry.h b/src/ui/widget/entity-entry.h new file mode 100644 index 0000000..3168e4c --- /dev/null +++ b/src/ui/widget/entity-entry.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H +#define INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H + +#include <gtkmm/textview.h> + +struct rdf_work_entity_t; +class SPDocument; + +namespace Gtk { +class TextBuffer; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +class EntityEntry { +public: + static EntityEntry* create (rdf_work_entity_t* ent, Registry& wr); + virtual ~EntityEntry() = 0; + virtual void update (SPDocument *doc) = 0; + virtual void on_changed() = 0; + virtual void load_from_preferences() = 0; + void save_to_preferences(SPDocument *doc); + Gtk::Label _label; + Gtk::Widget *_packable; + +protected: + EntityEntry (rdf_work_entity_t* ent, Registry& wr); + sigc::connection _changed_connection; + rdf_work_entity_t *_entity; + Registry *_wr; +}; + +class EntityLineEntry : public EntityEntry { +public: + EntityLineEntry (rdf_work_entity_t* ent, Registry& wr); + ~EntityLineEntry() override; + void update (SPDocument *doc) override; + void load_from_preferences() override; + +protected: + void on_changed() override; +}; + +class EntityMultiLineEntry : public EntityEntry { +public: + EntityMultiLineEntry (rdf_work_entity_t* ent, Registry& wr); + ~EntityMultiLineEntry() override; + void update (SPDocument *doc) override; + void load_from_preferences() override; + +protected: + void on_changed() override; + Gtk::TextView _v; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_ENTITY_ENTRY__H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/entry.cpp b/src/ui/widget/entry.cpp new file mode 100644 index 0000000..e9a63c5 --- /dev/null +++ b/src/ui/widget/entry.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "entry.h" + +#include <gtkmm/entry.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Entry::Entry( Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic) +{ +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + diff --git a/src/ui/widget/entry.h b/src/ui/widget/entry.h new file mode 100644 index 0000000..3674d51 --- /dev/null +++ b/src/ui/widget/entry.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_ENTRY__H +#define INKSCAPE_UI_WIDGET_ENTRY__H + +#include "labelled.h" + +namespace Gtk { +class Entry; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Helperclass for Gtk::Entry widgets. + */ +class Entry : public Labelled +{ +public: + Entry( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + // TO DO: add methods to access Gtk::Entry widget + + Gtk::Entry* getEntry() {return (Gtk::Entry*)(_widget);}; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_ENTRY__H diff --git a/src/ui/widget/filter-effect-chooser.cpp b/src/ui/widget/filter-effect-chooser.cpp new file mode 100644 index 0000000..d933408 --- /dev/null +++ b/src/ui/widget/filter-effect-chooser.cpp @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Filter effect selection selection widget + * + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Tavmjong Bah + * + * Copyright (C) 2007, 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter-effect-chooser.h" + +#include "document.h" + +namespace Inkscape { + +const EnumData<SPBlendMode> SPBlendModeData[SP_CSS_BLEND_ENDMODE] = { + { SP_CSS_BLEND_NORMAL, _("Normal"), "normal" }, + { SP_CSS_BLEND_MULTIPLY, _("Multiply"), "multiply" }, + { SP_CSS_BLEND_SCREEN, _("Screen"), "screen" }, + { SP_CSS_BLEND_DARKEN, _("Darken"), "darken" }, + { SP_CSS_BLEND_LIGHTEN, _("Lighten"), "lighten" }, + // New in Compositing and Blending Level 1 + { SP_CSS_BLEND_OVERLAY, _("Overlay"), "overlay" }, + { SP_CSS_BLEND_COLORDODGE, _("Color Dodge"), "color-dodge" }, + { SP_CSS_BLEND_COLORBURN, _("Color Burn"), "color-burn" }, + { SP_CSS_BLEND_HARDLIGHT, _("Hard Light"), "hard-light" }, + { SP_CSS_BLEND_SOFTLIGHT, _("Soft Light"), "soft-light" }, + { SP_CSS_BLEND_DIFFERENCE, _("Difference"), "difference" }, + { SP_CSS_BLEND_EXCLUSION, _("Exclusion"), "exclusion" }, + { SP_CSS_BLEND_HUE, _("Hue"), "hue" }, + { SP_CSS_BLEND_SATURATION, _("Saturation"), "saturation" }, + { SP_CSS_BLEND_COLOR, _("Color"), "color" }, + { SP_CSS_BLEND_LUMINOSITY, _("Luminosity"), "luminosity" } +}; +const EnumDataConverter<SPBlendMode> SPBlendModeConverter(SPBlendModeData, SP_CSS_BLEND_ENDMODE); + + +namespace UI { +namespace Widget { + +SimpleFilterModifier::SimpleFilterModifier(int flags) + : _flags(flags) + , _lb_blend(_("Blend mode:")) + , _lb_isolation("Isolate") // Translate for 1.1 + , _blend(SPBlendModeConverter, SP_ATTR_INVALID, false) + , _blur(_("Blur (%)"), 0, 0, 100, 1, 0.1, 1) + , _opacity(_("Opacity (%)"), 0, 0, 100, 1, 0.1, 1) + , _notify(true) +{ + set_name("SimpleFilterModifier"); + + _flags = flags; + + if (flags & BLEND) { + add(_hb_blend); + _lb_blend.set_use_underline(); + _hb_blend.set_halign(Gtk::ALIGN_END); + _hb_blend.set_valign(Gtk::ALIGN_CENTER); + _hb_blend.set_margin_top(3); + _hb_blend.set_margin_end(5); + _lb_blend.set_mnemonic_widget(_blend); + _hb_blend.pack_start(_lb_blend, false, false, 5); + _hb_blend.pack_start(_blend, false, false, 5); + /* + * For best fit inkscape-browsers with no GUI to isolation we need all groups, + * clones and symbols with isolation == isolate to not show to the user of + * Inkscape a "strange" behabiour from the designer point of view. + * Is strange because only happends when object not has clip, mask, + * filter, blending or opacity . + * Anyway the feature is a no-gui feature and render as spected. + */ + /* if (flags & ISOLATION) { + _isolation.property_active() = false; + _hb_blend.pack_start(_isolation, false, false, 5); + _hb_blend.pack_start(_lb_isolation, false, false, 5); + _isolation.set_tooltip_text("Don't blend childrens with objects behind"); + _lb_isolation.set_tooltip_text("Don't blend childrens with objects behind"); + } */ + Gtk::Separator *separator = Gtk::manage(new Gtk::Separator()); + separator->set_margin_top(8); + separator->set_margin_bottom(8); + add(*separator); + } + + if (flags & BLUR) { + add(_blur); + } + + if (flags & OPACITY) { + add(_opacity); + } + show_all_children(); + + _blend.signal_changed().connect(signal_blend_changed()); + _blur.signal_value_changed().connect(signal_blur_changed()); + _opacity.signal_value_changed().connect(signal_opacity_changed()); + _isolation.signal_toggled().connect(signal_isolation_changed()); +} + +sigc::signal<void> &SimpleFilterModifier::signal_isolation_changed() +{ + if (_notify) { + return _signal_isolation_changed; + } + _notify = true; + return _signal_null; +} + +sigc::signal<void>& SimpleFilterModifier::signal_blend_changed() +{ + if (_notify) { + return _signal_blend_changed; + } + _notify = true; + return _signal_null; +} + +sigc::signal<void>& SimpleFilterModifier::signal_blur_changed() +{ + // we dont use notifi to block use aberaje for multiple + return _signal_blur_changed; +} + +sigc::signal<void>& SimpleFilterModifier::signal_opacity_changed() +{ + // we dont use notifi to block use averaje for multiple + return _signal_opacity_changed; +} + +SPIsolation SimpleFilterModifier::get_isolation_mode() +{ + return _isolation.get_active() ? SP_CSS_ISOLATION_ISOLATE : SP_CSS_ISOLATION_AUTO; +} + +void SimpleFilterModifier::set_isolation_mode(const SPIsolation val, bool notify) +{ + _notify = notify; + _isolation.set_active(val == SP_CSS_ISOLATION_ISOLATE); +} + +SPBlendMode SimpleFilterModifier::get_blend_mode() +{ + const Util::EnumData<SPBlendMode> *d = _blend.get_active_data(); + if (d) { + return _blend.get_active_data()->id; + } else { + return SP_CSS_BLEND_NORMAL; + } +} + +void SimpleFilterModifier::set_blend_mode(const SPBlendMode val, bool notify) +{ + _notify = notify; + _blend.set_active(val); +} + +double SimpleFilterModifier::get_blur_value() const +{ + return _blur.get_value(); +} + +void SimpleFilterModifier::set_blur_value(const double val) +{ + _blur.set_value(val); +} + +double SimpleFilterModifier::get_opacity_value() const +{ + return _opacity.get_value(); +} + +void SimpleFilterModifier::set_opacity_value(const double val) +{ + _opacity.set_value(val); +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/filter-effect-chooser.h b/src/ui/widget/filter-effect-chooser.h new file mode 100644 index 0000000..cbbe2b5 --- /dev/null +++ b/src/ui/widget/filter-effect-chooser.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __FILTER_EFFECT_CHOOSER_H__ +#define __FILTER_EFFECT_CHOOSER_H__ + +/* + * Filter effect selection selection widget + * + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Tavmjong Bah + * + * Copyright (C) 2007, 2017 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/separator.h> + +#include "combo-enums.h" +#include "spin-scale.h" +#include "style-enums.h" + +using Inkscape::Util::EnumData; +using Inkscape::Util::EnumDataConverter; + +namespace Inkscape { +extern const Util::EnumDataConverter<SPBlendMode> SPBlendModeConverter; +namespace UI { +namespace Widget { + +/* Allows basic control over feBlend and feGaussianBlur effects as well as opacity. + * Common for Object, Layers, and Fill and Stroke dialogs. +*/ +class SimpleFilterModifier : public Gtk::VBox +{ +public: + enum Flags { NONE = 0, BLUR = 1, OPACITY = 2, BLEND = 4, ISOLATION = 16 }; + + SimpleFilterModifier(int flags); + + sigc::signal<void> &signal_blend_changed(); + sigc::signal<void> &signal_blur_changed(); + sigc::signal<void> &signal_opacity_changed(); + sigc::signal<void> &signal_isolation_changed(); + + SPIsolation get_isolation_mode(); + void set_isolation_mode(const SPIsolation, bool notify); + + SPBlendMode get_blend_mode(); + void set_blend_mode(const SPBlendMode, bool notify); + + double get_blur_value() const; + void set_blur_value(const double); + + double get_opacity_value() const; + void set_opacity_value(const double); + +private: + int _flags; + bool _notify; + + Gtk::HBox _hb_blend; + Gtk::Label _lb_blend; + Gtk::Label _lb_isolation; + ComboBoxEnum<SPBlendMode> _blend; + SpinScale _blur; + SpinScale _opacity; + Gtk::CheckButton _isolation; + + sigc::signal<void> _signal_null; + sigc::signal<void> _signal_blend_changed; + sigc::signal<void> _signal_blur_changed; + sigc::signal<void> _signal_opacity_changed; + sigc::signal<void> _signal_isolation_changed; +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/font-button.cpp b/src/ui/widget/font-button.cpp new file mode 100644 index 0000000..e0a140a --- /dev/null +++ b/src/ui/widget/font-button.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "font-button.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/fontbutton.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontButton::FontButton(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::FontButton("Sans 10"), suffix, icon, mnemonic) +{ +} + +Glib::ustring FontButton::getValue() const +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::FontButton*>(_widget)->get_font_name(); +} + + +void FontButton::setValue (Glib::ustring fontspec) +{ + g_assert(_widget != nullptr); + static_cast<Gtk::FontButton*>(_widget)->set_font_name(fontspec); +} + +Glib::SignalProxy0<void> FontButton::signal_font_value_changed() +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::FontButton*>(_widget)->signal_font_set(); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/font-button.h b/src/ui/widget/font-button.h new file mode 100644 index 0000000..a53b7d6 --- /dev/null +++ b/src/ui/widget/font-button.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Copyright (C) 2007 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_BUTTON_H +#define INKSCAPE_UI_WIDGET_FONT_BUTTON_H + +#include "labelled.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled font button for entering font values + */ +class FontButton : public Labelled +{ +public: + /** + * Construct a FontButton Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + FontButton( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + Glib::ustring getValue() const; + void setValue (Glib::ustring fontspec); + /** + * Signal raised when the font button's value changes. + */ + Glib::SignalProxy0<void> signal_font_value_changed(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RANDOM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/font-selector-toolbar.cpp b/src/ui/widget/font-selector-toolbar.cpp new file mode 100644 index 0000000..ea53d90 --- /dev/null +++ b/src/ui/widget/font-selector-toolbar.cpp @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> +#include <gdkmm/display.h> + +#include "font-selector-toolbar.h" + +#include "libnrtype/font-lister.h" +#include "libnrtype/font-instance.h" + +#include "ui/icon-names.h" + +// For updating from selection +#include "inkscape.h" +#include "desktop.h" +#include "object/sp-text.h" + +// TEMP TEMP TEMP +#include "ui/toolbar/text-toolbar.h" + +/* To do: + * Fix altx. Need to store + */ + +void family_cell_data_func(const Gtk::TreeModel::const_iterator iter, Gtk::CellRendererText* cell ) { + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Glib::ustring markup = font_lister->get_font_family_markup(iter); + // std::cout << "Markup: " << markup << std::endl; + + cell->set_property ("markup", markup); +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontSelectorToolbar::FontSelectorToolbar () + : Gtk::Grid () + , family_combo (true) // true => with text entry. + , style_combo (true) + , signal_block (false) +{ + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + // Font family + family_combo.set_model (font_lister->get_font_list()); + family_combo.set_entry_text_column (0); + family_combo.set_name ("FontSelectorToolBar: Family"); + family_combo.set_row_separator_func (&font_lister_separator_func); + + family_combo.clear(); // Clears all CellRenderer mappings. + family_combo.set_cell_data_func (family_cell, + sigc::bind(sigc::ptr_fun(family_cell_data_func), &family_cell)); + family_combo.pack_start (family_cell); + + + Gtk::Entry* entry = family_combo.get_entry(); + entry->signal_icon_press().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_icon_pressed)); + entry->signal_key_press_event().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_key_press_event), false); // false => connect first + entry->set_data (Glib::Quark("altx-text"), entry); // Desktop will set focus to entry with Alt-x. + + + Glib::RefPtr<Gtk::EntryCompletion> completion = Gtk::EntryCompletion::create(); + completion->set_model (font_lister->get_font_list()); + completion->set_text_column (0); + completion->set_popup_completion (); + completion->set_inline_completion (false); + completion->set_inline_selection (); + // completion->signal_match_selected().connect(sigc::mem_fun(*this, &FontSelectorToolbar::on_match_selected), false); // false => connect before default handler. + entry->set_completion (completion); + + // Style + style_combo.set_model (font_lister->get_style_list()); + style_combo.set_name ("FontSelectorToolbar: Style"); + + // Grid + set_name ("FontSelectorToolbar: Grid"); + attach (family_combo, 0, 0, 1, 1); + attach (style_combo, 1, 0, 1, 1); + + // Add signals + family_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_family_changed)); + style_combo.signal_changed().connect (sigc::mem_fun(*this, &FontSelectorToolbar::on_style_changed)); + + show_all_children(); + + // Initialize font family lists. (May already be done.) Should be done on document change. + font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument()); + + // When FontLister is changed, update family and style shown in GUI. + font_lister->connectUpdate(sigc::mem_fun(*this, &FontSelectorToolbar::update_font)); +} + + +// Update GUI based on font-selector values. +void +FontSelectorToolbar::update_font () +{ + if (signal_block) return; + + signal_block = true; + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreeModel::Row row; + + // Set font family. + try { + row = font_lister->get_row_for_font (); + family_combo.set_active (row); + } catch (...) { + std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for family: " + << font_lister->get_font_family() << std::endl; + } + + // Set style. + try { + row = font_lister->get_row_for_style (); + style_combo.set_active (row); + } catch (...) { + std::cerr << "FontSelectorToolbar::update_font: Couldn't find row for style: " + << font_lister->get_font_style() << std::endl; + } + + // Check for missing fonts. + Glib::ustring missing_fonts = get_missing_fonts(); + + // Add an icon to end of entry. + Gtk::Entry* entry = family_combo.get_entry(); + if (missing_fonts.empty()) { + // If no missing fonts, add icon for selecting all objects with this font-family. + entry->set_icon_from_icon_name (INKSCAPE_ICON("edit-select-all"), Gtk::ENTRY_ICON_SECONDARY); + entry->set_icon_tooltip_text (_("Select all text with this text family"), Gtk::ENTRY_ICON_SECONDARY); + } else { + // If missing fonts, add warning icon. + Glib::ustring warning = _("Font not found on system: ") + missing_fonts; + entry->set_icon_from_icon_name (INKSCAPE_ICON("dialog-warning"), Gtk::ENTRY_ICON_SECONDARY); + entry->set_icon_tooltip_text (warning, Gtk::ENTRY_ICON_SECONDARY); + } + + signal_block = false; +} + +// Get comma separated list of fonts in font-family that are not on system. +// To do, move to font-lister. +Glib::ustring +FontSelectorToolbar::get_missing_fonts () +{ + // Get font list in text entry which may be a font stack (with fallbacks). + Glib::ustring font_list = family_combo.get_entry_text(); + Glib::ustring missing_font_list; + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + std::vector<Glib::ustring> tokens = Glib::Regex::split_simple("\\s*,\\s*", font_list); + + for (auto token: tokens) { + bool found = false; + Gtk::TreeModel::Children children = font_lister->get_font_list()->children(); + for (auto iter2: children) { + Gtk::TreeModel::Row row2 = *iter2; + Glib::ustring family2 = row2[font_lister->FontList.family]; + bool onSystem2 = row2[font_lister->FontList.onSystem]; + // CSS dictates that font family names are case insensitive. + // This should really implement full Unicode case unfolding. + if (onSystem2 && token.casefold().compare(family2.casefold()) == 0) { + found = true; + break; + } + } + + if (!found) { + missing_font_list += token; + missing_font_list += ", "; + } + } + + // Remove extra comma and space from end. + if (missing_font_list.size() >= 2) { + missing_font_list.resize(missing_font_list.size() - 2); + } + + return missing_font_list; +} + + +// Callbacks + +// Need to update style list +void +FontSelectorToolbar::on_family_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::ustring family = family_combo.get_entry_text(); + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_font_family (family); + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelectorToolbar::on_style_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::ustring style = style_combo.get_entry_text(); + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->set_font_style (style); + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelectorToolbar::on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event) { + std::cout << "FontSelectorToolbar::on_entry_icon_pressed" << std::endl; + std::cout << " .... Should select all items with same font-family. FIXME" << std::endl; + // Call equivalent of sp_text_toolbox_select_cb() in text-toolbar.cpp + // Should be action! (Maybe: select_all_fontfamily( Glib::ustring font_family );). + // Check how Find dialog works. +} + +// bool +// FontSelectorToolbar::on_match_selected (const Gtk::TreeModel::iterator& iter) +// { +// std::cout << "on_match_selected" << std::endl; +// std::cout << " FIXME" << std::endl; +// Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); +// Glib::ustring family = (*iter)[font_lister->FontList.family]; +// std::cout << " family: " << family << std::endl; +// return false; // Leave it to default handler to set entry text. +// } + +// Return focus to canvas. +bool +FontSelectorToolbar::on_key_press_event (GdkEventKey* key_event) +{ + bool consumed = false; + + unsigned int key = 0; + gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr ); + + switch ( key ) { + + case GDK_KEY_Escape: + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + // Defocus + std::cerr << "FontSelectorToolbar::on_key_press_event: Defocus: FIXME" << std::endl; + consumed = true; + } + break; + } + + return consumed; // Leave it to default handler if false. +} + +void +FontSelectorToolbar::changed_emit() { + signal_block = true; + changed_signal.emit (); + signal_block = false; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-selector-toolbar.h b/src/ui/widget/font-selector-toolbar.h new file mode 100644 index 0000000..53cdcea --- /dev/null +++ b/src/ui/widget/font-selector-toolbar.h @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + * The routines here create and manage a font selector widget with two parts, + * one each for font-family and font-style. + * + * This is essentially a toolbar version of the 'FontSelector' widget. Someday + * this may be merged with it. + * + * The main functions are: + * Create the font-selector toolbar widget. + * Update the lists when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Update the on-screen text. + * Provide the currently selected values. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H +#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_TOOLBAR_H + +#include <gtkmm/grid.h> +#include <gtkmm/treeview.h> +#include <gtkmm/comboboxtext.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container of widgets for selecting font faces. + * + * It is used by Text tool toolbar. The FontSelectorToolbar class utilizes the + * FontLister class to obtain a list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts + * are kept track of by their "fontspecs" which are the same as the strings that Pango generates. + * + * The main functions are: + * Create the font-selector widget. + * Update the child widgets when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made to a child widget. + */ +class FontSelectorToolbar : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontSelectorToolbar (); + +protected: + + // Font family + Gtk::ComboBox family_combo; + Gtk::CellRendererText family_cell; + + // Font style + Gtk::ComboBoxText style_combo; + Gtk::CellRendererText style_cell; + +private: + + // Make a list of missing fonts for tooltip and for warning icon. + Glib::ustring get_missing_fonts (); + + // Signal handlers + void on_family_changed(); + void on_style_changed(); + void on_icon_pressed (Gtk::EntryIconPosition icon_position, const GdkEventButton* event); + // bool on_match_selected (const Gtk::TreeModel::iterator& iter); + bool on_key_press_event (GdkEventKey* key_event) override; + + // Signals + sigc::signal<void> changed_signal; + void changed_emit(); + bool signal_block; + +public: + + /** + * Update GUI based on font-selector values. + */ + void update_font (); + + /** + * Let others know that user has changed GUI settings. + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return changed_signal.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_TOOLBAR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-selector.cpp b/src/ui/widget/font-selector.cpp new file mode 100644 index 0000000..df13fa3 --- /dev/null +++ b/src/ui/widget/font-selector.cpp @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/markup.h> + +#include "font-selector.h" + +#include "libnrtype/font-lister.h" +#include "libnrtype/font-instance.h" + +// For updating from selection +#include "inkscape.h" +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontSelector::FontSelector (bool with_size, bool with_variations) + : Gtk::Grid () + , family_frame (_("Font family")) + , style_frame (C_("Font selector", "Style")) + , size_label (_("Font size")) + , size_combobox (true) // With entry + , signal_block (false) + , font_size (18) +{ + + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + + // Font family + family_treecolumn.pack_start (family_cell, false); + family_treecolumn.set_fixed_width (200); + family_treecolumn.add_attribute (family_cell, "text", 0); + family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func); + + family_treeview.set_row_separator_func (&font_lister_separator_func); + family_treeview.set_model (font_lister->get_font_list()); + family_treeview.set_name ("FontSelector: Family"); + family_treeview.set_headers_visible (false); + family_treeview.append_column (family_treecolumn); + + family_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + family_scroll.add (family_treeview); + + family_frame.set_hexpand (true); + family_frame.set_vexpand (true); + family_frame.add (family_scroll); + + // Style + style_treecolumn.pack_start (style_cell, false); + style_treecolumn.add_attribute (style_cell, "text", 0); + style_treecolumn.set_cell_data_func (style_cell, sigc::mem_fun(*this, &FontSelector::style_cell_data_func)); + style_treecolumn.set_title ("Face"); + style_treecolumn.set_resizable (true); + + style_treeview.set_model (font_lister->get_style_list()); + style_treeview.set_name ("FontSelectorStyle"); + style_treeview.append_column ("CSS", font_lister->FontStyleList.cssStyle); + style_treeview.append_column (style_treecolumn); + + style_treeview.get_column(0)->set_resizable (true); + + style_scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + style_scroll.add (style_treeview); + + style_frame.set_hexpand (true); + style_frame.set_vexpand (true); + style_frame.add (style_scroll); + + // Size + size_combobox.set_name ("FontSelectorSize"); + set_sizes(); + size_combobox.set_active_text( "18" ); + + // Font Variations + font_variations.set_vexpand (true); + font_variations_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + font_variations_scroll.add (font_variations); + + // Grid + set_name ("FontSelectorGrid"); + set_row_spacing(4); + set_column_spacing(4); + // Add extra columns to the "family frame" to change space distribution + // by prioritizing font family over styles + const int extra = 4; + attach (family_frame, 0, 0, 1 + extra, 2); + attach (style_frame, 1 + extra, 0, 2, 1); + if (with_size) { // Glyph panel does not use size. + attach (size_label, 1 + extra, 1, 1, 1); + attach (size_combobox, 2 + extra, 1, 1, 1); + } + if (with_variations) { // Glyphs panel does not use variations. + attach (font_variations_scroll, 0, 2, 3 + extra, 1); + } + + // Add signals + family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed)); + style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed)); + size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed)); + font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed)); + + show_all_children(); + + // Initialize font family lists. (May already be done.) Should be done on document change. + font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument()); +} + +void +FontSelector::set_sizes () +{ + size_combobox.remove_all(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + + int sizes[] = { + 4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28, + 32, 36, 40, 48, 56, 64, 72, 144 + }; + + // Array must be same length as SPCSSUnit in style-internal.h + // PX PT PC MM CM IN EM EX % + double ratios[] = {1, 1, 1, 10, 4, 40, 100, 16, 8, 0.16}; + + for (int i : sizes) + { + double size = i/ratios[unit]; + size_combobox.append( Glib::ustring::format(size) ); + } +} + +void +FontSelector::set_fontsize_tooltip() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT); + Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")"); + size_combobox.set_tooltip_text (tooltip); +} + +// Update GUI. +// We keep a private copy of the style list as the font-family in widget is only temporary +// until the "Apply" button is set so the style list can be different from that in +// FontLister. +void +FontSelector::update_font () +{ + signal_block = true; + + Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreePath path; + Glib::ustring family = font_lister->get_font_family(); + Glib::ustring style = font_lister->get_font_style(); + + // Set font family + try { + path = font_lister->get_row_for_font (family); + } catch (...) { + std::cerr << "FontSelector::update_font: Couldn't find row for font-family: " + << family << std::endl; + path.clear(); + path.push_back(0); + } + + Gtk::TreePath currentPath; + Gtk::TreeViewColumn *currentColumn; + family_treeview.get_cursor(currentPath, currentColumn); + if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) { + family_treeview.set_cursor (path); + family_treeview.scroll_to_row (path); + } + + // Get font-lister style list for selected family + Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path)); + GList *styles; + row.get_value(1, styles); + + // Copy font-lister style list to private list store, searching for match. + Gtk::TreeModel::iterator match; + FontLister::FontStyleListClass FontStyleList; + Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList); + for ( ; styles; styles = styles->next ) { + Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName; + if (style == ((StyleNames*)styles->data)->CssName) { + match = treeModelIter; + } + } + + // Attach store to tree view and select row. + style_treeview.set_model (local_style_list_store); + if (match) { + style_treeview.get_selection()->select (match); + } + + Glib::ustring fontspec = font_lister->get_fontspec(); + update_variations(fontspec); + + signal_block = false; +} + +void +FontSelector::update_size (double size) +{ + signal_block = true; + + // Set font size + std::stringstream ss; + ss << size; + size_combobox.get_entry()->set_text( ss.str() ); + font_size = size; // Store value + set_fontsize_tooltip(); + + signal_block = false; +} + + +// If use_variations is true (default), we get variation values from variations widget otherwise we +// get values from CSS widget (we need to be able to keep the two widgets synchronized both ways). +Glib::ustring +FontSelector::get_fontspec(bool use_variations) { + + // Build new fontspec from GUI settings + Glib::ustring family = "Sans"; // Default...family list may not have been constructed. + Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(); + if (iter) { + (*iter).get_value(0, family); + } + + Glib::ustring style = "Normal"; + iter = style_treeview.get_selection()->get_selected(); + if (iter) { + (*iter).get_value(0, style); + } + + if (family.empty()) { + std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl; + } + + if (style.empty()) { + std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl; + } + + Glib::ustring fontspec = family + ", "; + + if (use_variations) { + // Clip any font_variation data in 'style' as we'll replace it. + auto pos = style.find('@'); + if (pos != Glib::ustring::npos) { + style.erase (pos, style.length()-1); + } + + Glib::ustring variations = font_variations.get_pango_string(); + + if (variations.empty()) { + fontspec += style; + } else { + fontspec += variations; + } + } else { + fontspec += style; + } + + return fontspec; +} + +void +FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ + Glib::ustring family = "Sans"; // Default...family list may not have been constructed. + Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected(); + if (iter_family) { + (*iter_family).get_value(0, family); + } + + Glib::ustring style = "Normal"; + (*iter).get_value(1, style); + + Glib::ustring style_escaped = Glib::Markup::escape_text( style ); + Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style ); + Glib::ustring markup; + + markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>"; + + // std::cout << " markup: " << markup << " (" << name << ")" << std::endl; + + renderer->set_property("markup", markup); +} + + +// Callbacks + +// Need to update style list +void +FontSelector::on_family_changed() { + + if (signal_block) return; + signal_block = true; + + Glib::RefPtr<Gtk::TreeModel> model; + Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model); + + if (!iter) { + // This can happen just after the family list is recreated. + signal_block = false; + return; + } + + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + fontlister->ensureRowStyles(model, iter); + + Gtk::TreeModel::Row row = *iter; + + // Get family name + Glib::ustring family; + row.get_value(0, family); + + // Get style list (TO DO: Get rid of GList) + GList *styles; + row.get_value(1, styles); + + // Find best style match for selected family with current style (e.g. of selected text). + Glib::ustring style = fontlister->get_font_style(); + Glib::ustring best = fontlister->get_best_style_match (family, style); + + // Create are own store of styles for selected font-family (the font-family selected + // in the dialog may not be the same as stored in the font-lister class until the + // "Apply" button is triggered). + Gtk::TreeModel::iterator it_best; + FontLister::FontStyleListClass FontStyleList; + Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList); + + // Build list and find best match. + for ( ; styles; styles = styles->next ) { + Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName; + if (best == ((StyleNames*)styles->data)->CssName) { + it_best = treeModelIter; + } + } + + // Attach store to tree view and select row. + style_treeview.set_model (local_style_list_store); + if (it_best) { + style_treeview.get_selection()->select (it_best); + } + + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelector::on_style_changed() { + if (signal_block) return; + + // Update variations widget if new style selected from style widget. + signal_block = true; + Glib::ustring fontspec = get_fontspec( false ); + update_variations(fontspec); + signal_block = false; + + // Let world know + changed_emit(); +} + +void +FontSelector::on_size_changed() { + + if (signal_block) return; + + double size; + Glib::ustring input = size_combobox.get_active_text(); + try { + size = std::stod (input); + } + catch (std::invalid_argument) { + std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl; + size = -1; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // Arbitrary: Text and Font preview freezes with huge font sizes. + int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000); + + if (size <= 0) { + return; + } + if (size > max_size) + size = max_size; + + if (fabs(font_size - size) > 0.001) { + font_size = size; + // Let world know + changed_emit(); + } +} + +void +FontSelector::on_variations_changed() { + + if (signal_block) return; + + // Let world know + changed_emit(); +} + +void +FontSelector::changed_emit() { + signal_block = true; + signal_changed.emit (get_fontspec()); + signal_block = false; +} + +void FontSelector::update_variations(const Glib::ustring& fontspec) { + font_variations.update(fontspec); + + // Check if there are any variations available; if not, don't expand font_variations_scroll + bool hasContent = font_variations.variations_present(); + font_variations_scroll.set_vexpand(hasContent); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-selector.h b/src/ui/widget/font-selector.h new file mode 100644 index 0000000..137d411 --- /dev/null +++ b/src/ui/widget/font-selector.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + * The routines here create and manage a font selector widget with three parts, + * one each for font-family, font-style, and font-size. + * + * It is used by the TextEdit and Glyphs panel dialogs. The FontLister class is used + * to access the list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text + * toolbar. Fonts are kept track of by their "fontspecs" which are the same as the + * strings that Pango generates. + * + * The main functions are: + * Create the font-seletor widget. + * Update the lists when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made so that the Text Preview can be updated. + * Provide the currently selected values. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_SELECTOR_H +#define INKSCAPE_UI_WIDGET_FONT_SELECTOR_H + +#include <gtkmm/grid.h> +#include <gtkmm/frame.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treeview.h> +#include <gtkmm/label.h> +#include <gtkmm/comboboxtext.h> + +#include "ui/widget/font-variations.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container of widgets for selecting font faces. + * + * It is used by the TextEdit and Glyphs panel dialogs. The FontSelector class utilizes the + * FontLister class to obtain a list of font-families and their associated styles for fonts either + * on the system or in the document. The FontLister class is also used by the Text toolbar. Fonts + * are kept track of by their "fontspecs" which are the same as the strings that Pango generates. + * + * The main functions are: + * Create the font-selector widget. + * Update the child widgets when a new text selection is made. + * Update the Style list when a new font-family is selected, highlighting the + * best match to the original font style (as not all fonts have the same style options). + * Emit a signal when any change is made to a child widget. + */ +class FontSelector : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontSelector (bool with_size = true, bool with_variations = true); + +protected: + + // Font family + Gtk::Frame family_frame; + Gtk::ScrolledWindow family_scroll; + Gtk::TreeView family_treeview; + Gtk::TreeViewColumn family_treecolumn; + Gtk::CellRendererText family_cell; + + // Font style + Gtk::Frame style_frame; + Gtk::ScrolledWindow style_scroll; + Gtk::TreeView style_treeview; + Gtk::TreeViewColumn style_treecolumn; + Gtk::CellRendererText style_cell; + + // Font size + Gtk::Label size_label; + Gtk::ComboBoxText size_combobox; + + // Font variations + Gtk::ScrolledWindow font_variations_scroll; + FontVariations font_variations; + +private: + + // Set sizes in font size combobox. + void set_sizes(); + void set_fontsize_tooltip(); + + // Use font style when listing style names. + void style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + + // Signal handlers + void on_family_changed(); + void on_style_changed(); + void on_size_changed(); + void on_variations_changed(); + + // Signals + sigc::signal<void, Glib::ustring> signal_changed; + void changed_emit(); + bool signal_block; + + // Variables + double font_size; + + // control font variations update and UI element size + void update_variations(const Glib::ustring& fontspec); + +public: + + /** + * Update GUI based on fontspec + */ + void update_font (); + void update_size (double size); + + /** + * Get fontspec based on current settings. (Does not handle size, yet.) + */ + Glib::ustring get_fontspec(bool use_variations = true); + + /** + * Get font size. Could be merged with fontspec. + */ + double get_fontsize() { return font_size; }; + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void, Glib::ustring> slot) { + return signal_changed.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_SETTINGS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variants.cpp b/src/ui/widget/font-variants.cpp new file mode 100644 index 0000000..1087431 --- /dev/null +++ b/src/ui/widget/font-variants.cpp @@ -0,0 +1,1461 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2015, 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <libnrtype/font-instance.h> + +#include "font-variants.h" + +// For updating from selection +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + // A simple class to handle UI for one feature. We could of derived this from Gtk::HBox but by + // attaching widgets directly to Gtk::Grid, we keep columns lined up (which may or may not be a + // good thing). + class Feature + { + public: + Feature( const Glib::ustring& name, OTSubstitution& glyphs, int options, Glib::ustring family, Gtk::Grid& grid, int &row, FontVariants* parent) + : _name (name) + , _options (options) + { + Gtk::Label* table_name = Gtk::manage (new Gtk::Label()); + table_name->set_markup ("\"" + name + "\" "); + + grid.attach (*table_name, 0, row, 1, 1); + + Gtk::FlowBox* flow_box = nullptr; + Gtk::ScrolledWindow* scrolled_window = nullptr; + if (options > 2) { + // If there are more than 2 option, pack them into a flowbox instead of directly putting them in the grid. + // Some fonts might have a table with many options (Bungee Hairline table 'ornm' has 113 entries). + flow_box = Gtk::manage (new Gtk::FlowBox()); + flow_box->set_selection_mode(); // Turn off selection + flow_box->set_homogeneous(); + flow_box->set_max_children_per_line (100); // Override default value + flow_box->set_min_children_per_line (10); // Override default value + + // We pack this into a scrollbar... otherwise the minimum height is set to what is required to fit all + // flow box children into the flow box when the flow box has minimum width. (Crazy if you ask me!) + scrolled_window = Gtk::manage (new Gtk::ScrolledWindow()); + scrolled_window->set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); + scrolled_window->add(*flow_box); + } + + Gtk::RadioButton::Group group; + for (int i = 0; i < options; ++i) { + + // Create radio button and create or add to button group. + Gtk::RadioButton* button = Gtk::manage (new Gtk::RadioButton()); + if (i == 0) { + group = button->get_group(); + } else { + button->set_group (group); + } + button->signal_clicked().connect ( sigc::mem_fun(*parent, &FontVariants::feature_callback) ); + buttons.push_back (button); + + // Create label. + Gtk::Label* label = Gtk::manage (new Gtk::Label()); + + // Restrict label width (some fonts have lots of alternatives). + label->set_line_wrap( true ); + label->set_line_wrap_mode( Pango::WRAP_WORD_CHAR ); + label->set_ellipsize( Pango::ELLIPSIZE_END ); + label->set_lines(3); + label->set_hexpand(); + + Glib::ustring markup; + markup += "<span font_family='"; + markup += family; + markup += "' font_features='"; + markup += name; + markup += " "; + markup += std::to_string (i); + markup += "'>"; + markup += Glib::Markup::escape_text (glyphs.input); + markup += "</span>"; + label->set_markup (markup); + + // Add button and label to widget + if (!flow_box) { + // Attach directly to grid (keeps things aligned row-to-row). + grid.attach (*button, 2*i+1, row, 1, 1); + grid.attach (*label, 2*i+2, row, 1, 1); + } else { + // Pack into FlowBox + + // Pack button and label into a box so they stay together. + Gtk::Box* box = Gtk::manage (new Gtk::Box()); + box->add(*button); + box->add(*label); + + flow_box->add(*box); + } + } + + if (scrolled_window) { + grid.attach (*scrolled_window, 1, row, 4, 1); + } + } + + Glib::ustring + get_css() + { + int i = 0; + for (auto b: buttons) { + if (b->get_active()) { + if (i == 0) { + // Features are always off by default (for those handled here). + return ""; + } else if (i == 1) { + // Feature without value has implied value of 1. + return ("\"" + _name + "\", "); + } else { + // Feature with value greater than 1 must be explicitly set. + return ("\"" + _name + "\" " + std::to_string (i) + ", "); + } + } + ++i; + } + return ""; + } + + void + set_active(int i) + { + if (i < buttons.size()) { + buttons[i]->set_active(); + } + } + + private: + Glib::ustring _name; + int _options; + std::vector <Gtk::RadioButton*> buttons; + }; + + FontVariants::FontVariants () : + Gtk::VBox (), + _ligatures_frame ( Glib::ustring(C_("Font feature", "Ligatures" )) ), + _ligatures_common ( Glib::ustring(C_("Font feature", "Common" )) ), + _ligatures_discretionary ( Glib::ustring(C_("Font feature", "Discretionary")) ), + _ligatures_historical ( Glib::ustring(C_("Font feature", "Historical" )) ), + _ligatures_contextual ( Glib::ustring(C_("Font feature", "Contextual" )) ), + + _position_frame ( Glib::ustring(C_("Font feature", "Position" )) ), + _position_normal ( Glib::ustring(C_("Font feature", "Normal" )) ), + _position_sub ( Glib::ustring(C_("Font feature", "Subscript" )) ), + _position_super ( Glib::ustring(C_("Font feature", "Superscript" )) ), + + _caps_frame ( Glib::ustring(C_("Font feature", "Capitals" )) ), + _caps_normal ( Glib::ustring(C_("Font feature", "Normal" )) ), + _caps_small ( Glib::ustring(C_("Font feature", "Small" )) ), + _caps_all_small ( Glib::ustring(C_("Font feature", "All small" )) ), + _caps_petite ( Glib::ustring(C_("Font feature", "Petite" )) ), + _caps_all_petite ( Glib::ustring(C_("Font feature", "All petite" )) ), + _caps_unicase ( Glib::ustring(C_("Font feature", "Unicase" )) ), + _caps_titling ( Glib::ustring(C_("Font feature", "Titling" )) ), + + _numeric_frame ( Glib::ustring(C_("Font feature", "Numeric" )) ), + _numeric_lining ( Glib::ustring(C_("Font feature", "Lining" )) ), + _numeric_old_style ( Glib::ustring(C_("Font feature", "Old Style" )) ), + _numeric_default_style ( Glib::ustring(C_("Font feature", "Default Style")) ), + _numeric_proportional ( Glib::ustring(C_("Font feature", "Proportional" )) ), + _numeric_tabular ( Glib::ustring(C_("Font feature", "Tabular" )) ), + _numeric_default_width ( Glib::ustring(C_("Font feature", "Default Width")) ), + _numeric_diagonal ( Glib::ustring(C_("Font feature", "Diagonal" )) ), + _numeric_stacked ( Glib::ustring(C_("Font feature", "Stacked" )) ), + _numeric_default_fractions( Glib::ustring(C_("Font feature", "Default Fractions")) ), + _numeric_ordinal ( Glib::ustring(C_("Font feature", "Ordinal" )) ), + _numeric_slashed_zero ( Glib::ustring(C_("Font feature", "Slashed Zero" )) ), + + _asian_frame ( Glib::ustring(C_("Font feature", "East Asian" )) ), + _asian_default_variant ( Glib::ustring(C_("Font feature", "Default" )) ), + _asian_jis78 ( Glib::ustring(C_("Font feature", "JIS78" )) ), + _asian_jis83 ( Glib::ustring(C_("Font feature", "JIS83" )) ), + _asian_jis90 ( Glib::ustring(C_("Font feature", "JIS90" )) ), + _asian_jis04 ( Glib::ustring(C_("Font feature", "JIS04" )) ), + _asian_simplified ( Glib::ustring(C_("Font feature", "Simplified" )) ), + _asian_traditional ( Glib::ustring(C_("Font feature", "Traditional" )) ), + _asian_default_width ( Glib::ustring(C_("Font feature", "Default" )) ), + _asian_full_width ( Glib::ustring(C_("Font feature", "Full Width" )) ), + _asian_proportional_width ( Glib::ustring(C_("Font feature", "Proportional" )) ), + _asian_ruby ( Glib::ustring(C_("Font feature", "Ruby" )) ), + + _feature_frame ( Glib::ustring(C_("Font feature", "Feature Settings")) ), + _feature_label ( Glib::ustring(C_("Font feature", "Selection has different Feature Settings!")) ), + + _ligatures_changed( false ), + _position_changed( false ), + _caps_changed( false ), + _numeric_changed( false ), + _asian_changed( false ) + + { + + set_name ( "FontVariants" ); + + // Ligatures -------------------------- + + // Add tooltips + _ligatures_common.set_tooltip_text( + _("Common ligatures. On by default. OpenType tables: 'liga', 'clig'")); + _ligatures_discretionary.set_tooltip_text( + _("Discretionary ligatures. Off by default. OpenType table: 'dlig'")); + _ligatures_historical.set_tooltip_text( + _("Historical ligatures. Off by default. OpenType table: 'hlig'")); + _ligatures_contextual.set_tooltip_text( + _("Contextual forms. On by default. OpenType table: 'calt'")); + + // Add signals + _ligatures_common.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_discretionary.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_historical.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + _ligatures_contextual.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::ligatures_callback) ); + + // Restrict label widths (some fonts have lots of ligatures). Must also set ellipsize mode. + _ligatures_label_common.set_max_width_chars( 60 ); + _ligatures_label_discretionary.set_max_width_chars( 60 ); + _ligatures_label_historical.set_max_width_chars( 60 ); + _ligatures_label_contextual.set_max_width_chars( 60 ); + + _ligatures_label_common.set_ellipsize( Pango::ELLIPSIZE_END ); + _ligatures_label_discretionary.set_ellipsize( Pango::ELLIPSIZE_END ); + _ligatures_label_historical.set_ellipsize( Pango::ELLIPSIZE_END ); + _ligatures_label_contextual.set_ellipsize( Pango::ELLIPSIZE_END ); + + _ligatures_label_common.set_lines( 5 ); + _ligatures_label_discretionary.set_lines( 5 ); + _ligatures_label_historical.set_lines( 5 ); + _ligatures_label_contextual.set_lines( 5 ); + + // Allow user to select characters. Not useful as this selects the ligatures. + // _ligatures_label_common.set_selectable( true ); + // _ligatures_label_discretionary.set_selectable( true ); + // _ligatures_label_historical.set_selectable( true ); + // _ligatures_label_contextual.set_selectable( true ); + + // Add to frame + _ligatures_grid.attach( _ligatures_common, 0, 0, 1, 1); + _ligatures_grid.attach( _ligatures_discretionary, 0, 1, 1, 1); + _ligatures_grid.attach( _ligatures_historical, 0, 2, 1, 1); + _ligatures_grid.attach( _ligatures_contextual, 0, 3, 1, 1); + _ligatures_grid.attach( _ligatures_label_common, 1, 0, 1, 1); + _ligatures_grid.attach( _ligatures_label_discretionary, 1, 1, 1, 1); + _ligatures_grid.attach( _ligatures_label_historical, 1, 2, 1, 1); + _ligatures_grid.attach( _ligatures_label_contextual, 1, 3, 1, 1); + + _ligatures_grid.set_margin_start(15); + _ligatures_grid.set_margin_end(15); + + _ligatures_frame.add( _ligatures_grid ); + pack_start( _ligatures_frame, Gtk::PACK_SHRINK ); + + ligatures_init(); + + // Position ---------------------------------- + + // Add tooltips + _position_normal.set_tooltip_text( _("Normal position.")); + _position_sub.set_tooltip_text( _("Subscript. OpenType table: 'subs'") ); + _position_super.set_tooltip_text( _("Superscript. OpenType table: 'sups'") ); + + // Group buttons + Gtk::RadioButton::Group position_group = _position_normal.get_group(); + _position_sub.set_group(position_group); + _position_super.set_group(position_group); + + // Add signals + _position_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + _position_sub.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + _position_super.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::position_callback) ); + + // Add to frame + _position_grid.attach( _position_normal, 0, 0, 1, 1); + _position_grid.attach( _position_sub, 1, 0, 1, 1); + _position_grid.attach( _position_super, 2, 0, 1, 1); + + _position_grid.set_margin_start(15); + _position_grid.set_margin_end(15); + + _position_frame.add( _position_grid ); + pack_start( _position_frame, Gtk::PACK_SHRINK ); + + position_init(); + + // Caps ---------------------------------- + + // Add tooltips + _caps_normal.set_tooltip_text( _("Normal capitalization.")); + _caps_small.set_tooltip_text( _("Small-caps (lowercase). OpenType table: 'smcp'")); + _caps_all_small.set_tooltip_text( _("All small-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'smcp'")); + _caps_petite.set_tooltip_text( _("Petite-caps (lowercase). OpenType table: 'pcap'")); + _caps_all_petite.set_tooltip_text( _("All petite-caps (uppercase and lowercase). OpenType tables: 'c2sc' and 'pcap'")); + _caps_unicase.set_tooltip_text( _("Unicase (small caps for uppercase, normal for lowercase). OpenType table: 'unic'")); + _caps_titling.set_tooltip_text( _("Titling caps (lighter-weight uppercase for use in titles). OpenType table: 'titl'")); + + // Group buttons + Gtk::RadioButton::Group caps_group = _caps_normal.get_group(); + _caps_small.set_group(caps_group); + _caps_all_small.set_group(caps_group); + _caps_petite.set_group(caps_group); + _caps_all_petite.set_group(caps_group); + _caps_unicase.set_group(caps_group); + _caps_titling.set_group(caps_group); + + // Add signals + _caps_normal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_all_small.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_all_petite.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_unicase.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + _caps_titling.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::caps_callback) ); + + // Add to frame + _caps_grid.attach( _caps_normal, 0, 0, 1, 1); + _caps_grid.attach( _caps_unicase, 1, 0, 1, 1); + _caps_grid.attach( _caps_titling, 2, 0, 1, 1); + _caps_grid.attach( _caps_small, 0, 1, 1, 1); + _caps_grid.attach( _caps_all_small, 1, 1, 1, 1); + _caps_grid.attach( _caps_petite, 2, 1, 1, 1); + _caps_grid.attach( _caps_all_petite, 3, 1, 1, 1); + + _caps_grid.set_margin_start(15); + _caps_grid.set_margin_end(15); + + _caps_frame.add( _caps_grid ); + pack_start( _caps_frame, Gtk::PACK_SHRINK ); + + caps_init(); + + // Numeric ------------------------------ + + // Add tooltips + _numeric_default_style.set_tooltip_text( _("Normal style.")); + _numeric_lining.set_tooltip_text( _("Lining numerals. OpenType table: 'lnum'")); + _numeric_old_style.set_tooltip_text( _("Old style numerals. OpenType table: 'onum'")); + _numeric_default_width.set_tooltip_text( _("Normal widths.")); + _numeric_proportional.set_tooltip_text( _("Proportional width numerals. OpenType table: 'pnum'")); + _numeric_tabular.set_tooltip_text( _("Same width numerals. OpenType table: 'tnum'")); + _numeric_default_fractions.set_tooltip_text( _("Normal fractions.")); + _numeric_diagonal.set_tooltip_text( _("Diagonal fractions. OpenType table: 'frac'")); + _numeric_stacked.set_tooltip_text( _("Stacked fractions. OpenType table: 'afrc'")); + _numeric_ordinal.set_tooltip_text( _("Ordinals (raised 'th', etc.). OpenType table: 'ordn'")); + _numeric_slashed_zero.set_tooltip_text( _("Slashed zeros. OpenType table: 'zero'")); + + // Group buttons + Gtk::RadioButton::Group style_group = _numeric_default_style.get_group(); + _numeric_lining.set_group(style_group); + _numeric_old_style.set_group(style_group); + + Gtk::RadioButton::Group width_group = _numeric_default_width.get_group(); + _numeric_proportional.set_group(width_group); + _numeric_tabular.set_group(width_group); + + Gtk::RadioButton::Group fraction_group = _numeric_default_fractions.get_group(); + _numeric_diagonal.set_group(fraction_group); + _numeric_stacked.set_group(fraction_group); + + // Add signals + _numeric_default_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_lining.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_old_style.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_proportional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_tabular.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_default_fractions.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_diagonal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_stacked.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_ordinal.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + _numeric_slashed_zero.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::numeric_callback) ); + + // Add to frame + _numeric_grid.attach (_numeric_default_style, 0, 0, 1, 1); + _numeric_grid.attach (_numeric_lining, 1, 0, 1, 1); + _numeric_grid.attach (_numeric_lining_label, 2, 0, 1, 1); + _numeric_grid.attach (_numeric_old_style, 3, 0, 1, 1); + _numeric_grid.attach (_numeric_old_style_label, 4, 0, 1, 1); + + _numeric_grid.attach (_numeric_default_width, 0, 1, 1, 1); + _numeric_grid.attach (_numeric_proportional, 1, 1, 1, 1); + _numeric_grid.attach (_numeric_proportional_label, 2, 1, 1, 1); + _numeric_grid.attach (_numeric_tabular, 3, 1, 1, 1); + _numeric_grid.attach (_numeric_tabular_label, 4, 1, 1, 1); + + _numeric_grid.attach (_numeric_default_fractions, 0, 2, 1, 1); + _numeric_grid.attach (_numeric_diagonal, 1, 2, 1, 1); + _numeric_grid.attach (_numeric_diagonal_label, 2, 2, 1, 1); + _numeric_grid.attach (_numeric_stacked, 3, 2, 1, 1); + _numeric_grid.attach (_numeric_stacked_label, 4, 2, 1, 1); + + _numeric_grid.attach (_numeric_ordinal, 0, 3, 1, 1); + _numeric_grid.attach (_numeric_ordinal_label, 1, 3, 4, 1); + + _numeric_grid.attach (_numeric_slashed_zero, 0, 4, 1, 1); + _numeric_grid.attach (_numeric_slashed_zero_label, 1, 4, 1, 1); + + _numeric_grid.set_margin_start(15); + _numeric_grid.set_margin_end(15); + + _numeric_frame.add( _numeric_grid ); + pack_start( _numeric_frame, Gtk::PACK_SHRINK ); + + // East Asian + + // Add tooltips + _asian_default_variant.set_tooltip_text ( _("Default variant.")); + _asian_jis78.set_tooltip_text( _("JIS78 forms. OpenType table: 'jp78'.")); + _asian_jis83.set_tooltip_text( _("JIS83 forms. OpenType table: 'jp83'.")); + _asian_jis90.set_tooltip_text( _("JIS90 forms. OpenType table: 'jp90'.")); + _asian_jis04.set_tooltip_text( _("JIS2004 forms. OpenType table: 'jp04'.")); + _asian_simplified.set_tooltip_text( _("Simplified forms. OpenType table: 'smpl'.")); + _asian_traditional.set_tooltip_text( _("Traditional forms. OpenType table: 'trad'.")); + _asian_default_width.set_tooltip_text ( _("Default width.")); + _asian_full_width.set_tooltip_text( _("Full width variants. OpenType table: 'fwid'.")); + _asian_proportional_width.set_tooltip_text(_("Proportional width variants. OpenType table: 'pwid'.")); + _asian_ruby.set_tooltip_text( _("Ruby variants. OpenType table: 'ruby'.")); + + // Add signals + _asian_default_variant.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis78.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis83.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis90.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_jis04.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_simplified.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_traditional.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_default_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_full_width.signal_clicked().connect ( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_proportional_width.signal_clicked().connect (sigc::mem_fun(*this, &FontVariants::asian_callback) ); + _asian_ruby.signal_clicked().connect( sigc::mem_fun(*this, &FontVariants::asian_callback) ); + + // Add to frame + _asian_grid.attach (_asian_default_variant, 0, 0, 1, 1); + _asian_grid.attach (_asian_jis78, 1, 0, 1, 1); + _asian_grid.attach (_asian_jis83, 2, 0, 1, 1); + _asian_grid.attach (_asian_jis90, 1, 1, 1, 1); + _asian_grid.attach (_asian_jis04, 2, 1, 1, 1); + _asian_grid.attach (_asian_simplified, 1, 2, 1, 1); + _asian_grid.attach (_asian_traditional, 2, 2, 1, 1); + _asian_grid.attach (_asian_default_width, 0, 3, 1, 1); + _asian_grid.attach (_asian_full_width, 1, 3, 1, 1); + _asian_grid.attach (_asian_proportional_width, 2, 3, 1, 1); + _asian_grid.attach (_asian_ruby, 0, 4, 1, 1); + + _asian_grid.set_margin_start(15); + _asian_grid.set_margin_end(15); + + _asian_frame.add( _asian_grid ); + pack_start( _asian_frame, Gtk::PACK_SHRINK ); + + // Group Buttons + Gtk::RadioButton::Group asian_variant_group = _asian_default_variant.get_group(); + _asian_jis78.set_group(asian_variant_group); + _asian_jis83.set_group(asian_variant_group); + _asian_jis90.set_group(asian_variant_group); + _asian_jis04.set_group(asian_variant_group); + _asian_simplified.set_group(asian_variant_group); + _asian_traditional.set_group(asian_variant_group); + + Gtk::RadioButton::Group asian_width_group = _asian_default_width.get_group(); + _asian_full_width.set_group (asian_width_group); + _asian_proportional_width.set_group (asian_width_group); + + // Feature settings --------------------- + + // Add tooltips + _feature_entry.set_tooltip_text( _("Feature settings in CSS form (e.g. \"wxyz\" or \"wxyz\" 3).")); + + _feature_substitutions.set_justify( Gtk::JUSTIFY_LEFT ); + _feature_substitutions.set_line_wrap( true ); + _feature_substitutions.set_line_wrap_mode( Pango::WRAP_WORD_CHAR ); + + _feature_list.set_justify( Gtk::JUSTIFY_LEFT ); + _feature_list.set_line_wrap( true ); + + // Add to frame + _feature_vbox.pack_start( _feature_grid, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_entry, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_label, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_substitutions, Gtk::PACK_SHRINK ); + _feature_vbox.pack_start( _feature_list, Gtk::PACK_SHRINK ); + + _feature_vbox.set_margin_start(15); + _feature_vbox.set_margin_end(15); + + _feature_frame.add( _feature_vbox ); + pack_start( _feature_frame, Gtk::PACK_SHRINK ); + + // Add signals + //_feature_entry.signal_key_press_event().connect ( sigc::mem_fun(*this, &FontVariants::feature_callback) ); + _feature_entry.signal_changed().connect( sigc::mem_fun(*this, &FontVariants::feature_callback) ); + + show_all_children(); + + } + + void + FontVariants::ligatures_init() { + // std::cout << "FontVariants::ligatures_init()" << std::endl; + } + + void + FontVariants::ligatures_callback() { + // std::cout << "FontVariants::ligatures_callback()" << std::endl; + _ligatures_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::position_init() { + // std::cout << "FontVariants::position_init()" << std::endl; + } + + void + FontVariants::position_callback() { + // std::cout << "FontVariants::position_callback()" << std::endl; + _position_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::caps_init() { + // std::cout << "FontVariants::caps_init()" << std::endl; + } + + void + FontVariants::caps_callback() { + // std::cout << "FontVariants::caps_callback()" << std::endl; + _caps_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::numeric_init() { + // std::cout << "FontVariants::numeric_init()" << std::endl; + } + + void + FontVariants::numeric_callback() { + // std::cout << "FontVariants::numeric_callback()" << std::endl; + _numeric_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::asian_init() { + // std::cout << "FontVariants::asian_init()" << std::endl; + } + + void + FontVariants::asian_callback() { + // std::cout << "FontVariants::asian_callback()" << std::endl; + _asian_changed = true; + _changed_signal.emit(); + } + + void + FontVariants::feature_init() { + // std::cout << "FontVariants::feature_init()" << std::endl; + } + + void + FontVariants::feature_callback() { + // std::cout << "FontVariants::feature_callback()" << std::endl; + _feature_changed = true; + _changed_signal.emit(); + } + + // Update GUI based on query. + void + FontVariants::update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ) { + + update_opentype( font_spec ); + + _ligatures_all = query->font_variant_ligatures.computed; + _ligatures_mix = query->font_variant_ligatures.value; + + _ligatures_common.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_COMMON ); + _ligatures_discretionary.set_active(_ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY ); + _ligatures_historical.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ); + _ligatures_contextual.set_active( _ligatures_all & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL ); + + _ligatures_common.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_COMMON ); + _ligatures_discretionary.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_DISCRETIONARY ); + _ligatures_historical.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_HISTORICAL ); + _ligatures_contextual.set_inconsistent( _ligatures_mix & SP_CSS_FONT_VARIANT_LIGATURES_CONTEXTUAL ); + + _position_all = query->font_variant_position.computed; + _position_mix = query->font_variant_position.value; + + _position_normal.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_NORMAL ); + _position_sub.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUB ); + _position_super.set_active( _position_all & SP_CSS_FONT_VARIANT_POSITION_SUPER ); + + _position_normal.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_NORMAL ); + _position_sub.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUB ); + _position_super.set_inconsistent( _position_mix & SP_CSS_FONT_VARIANT_POSITION_SUPER ); + + _caps_all = query->font_variant_caps.computed; + _caps_mix = query->font_variant_caps.value; + + _caps_normal.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_NORMAL ); + _caps_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_SMALL ); + _caps_all_small.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL ); + _caps_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_PETITE ); + _caps_all_petite.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE ); + _caps_unicase.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_UNICASE ); + _caps_titling.set_active( _caps_all & SP_CSS_FONT_VARIANT_CAPS_TITLING ); + + _caps_normal.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_NORMAL ); + _caps_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_SMALL ); + _caps_all_small.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL ); + _caps_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_PETITE ); + _caps_all_petite.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE ); + _caps_unicase.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_UNICASE ); + _caps_titling.set_inconsistent( _caps_mix & SP_CSS_FONT_VARIANT_CAPS_TITLING ); + + _numeric_all = query->font_variant_numeric.computed; + _numeric_mix = query->font_variant_numeric.value; + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS) { + _numeric_lining.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS) { + _numeric_old_style.set_active(); + } else { + _numeric_default_style.set_active(); + } + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS) { + _numeric_proportional.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS) { + _numeric_tabular.set_active(); + } else { + _numeric_default_width.set_active(); + } + + if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS) { + _numeric_diagonal.set_active(); + } else if (_numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS) { + _numeric_stacked.set_active(); + } else { + _numeric_default_fractions.set_active(); + } + + _numeric_ordinal.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL ); + _numeric_slashed_zero.set_active( _numeric_all & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO ); + + + _numeric_lining.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_LINING_NUMS ); + _numeric_old_style.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_OLDSTYLE_NUMS ); + _numeric_proportional.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_PROPORTIONAL_NUMS ); + _numeric_tabular.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_TABULAR_NUMS ); + _numeric_diagonal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS ); + _numeric_stacked.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS ); + _numeric_ordinal.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_ORDINAL ); + _numeric_slashed_zero.set_inconsistent( _numeric_mix & SP_CSS_FONT_VARIANT_NUMERIC_SLASHED_ZERO ); + + _asian_all = query->font_variant_east_asian.computed; + _asian_mix = query->font_variant_east_asian.value; + + if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78) { + _asian_jis78.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83) { + _asian_jis83.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90) { + _asian_jis90.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04) { + _asian_jis04.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED) { + _asian_simplified.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL) { + _asian_traditional.set_active(); + } else { + _asian_default_variant.set_active(); + } + + if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH) { + _asian_full_width.set_active(); + } else if (_asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH) { + _asian_proportional_width.set_active(); + } else { + _asian_default_width.set_active(); + } + + _asian_ruby.set_active ( _asian_all & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY ); + + _asian_jis78.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS78); + _asian_jis83.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS83); + _asian_jis90.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS90); + _asian_jis04.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_JIS04); + _asian_simplified.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED); + _asian_traditional.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL); + _asian_full_width.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH); + _asian_proportional_width.set_inconsistent(_asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_PROPORTIONAL_WIDTH); + _asian_ruby.set_inconsistent( _asian_mix & SP_CSS_FONT_VARIANT_EAST_ASIAN_RUBY); + + // Fix me: Should match a space if second part matches. ---, + // : Add boundary to 'on' and 'off'. v + Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("\"(\\w{4})\"\\s*([0-9]+|on|off|)"); + Glib::MatchInfo matchInfo; + std::string setting; + + // Set feature radiobutton (if it exists) or add to _feature_entry string. + char const *val = query->font_feature_settings.value(); + if (val) { + + std::vector<Glib::ustring> tokens = + Glib::Regex::split_simple("\\s*,\\s*", val); + + for (auto token: tokens) { + regex->match(token, matchInfo); + if (matchInfo.matches()) { + Glib::ustring table = matchInfo.fetch(1); + Glib::ustring value = matchInfo.fetch(2); + + if (_features.find(table) != _features.end()) { + int v = 0; + if (value == "0" || value == "off") v = 0; + else if (value == "1" || value == "on" || value.empty() ) v = 1; + else v = std::stoi(value); + _features[table]->set_active(v); + } else { + setting += token + ", "; + } + } + } + } + + // Remove final ", " + if (setting.length() > 1) { + setting.pop_back(); + setting.pop_back(); + } + + // Tables without radiobuttons. + _feature_entry.set_text( setting ); + + if( different_features ) { + _feature_label.show(); + } else { + _feature_label.hide(); + } + } + + // Update GUI based on OpenType tables of selected font (which may be changed in font selector tab). + void + FontVariants::update_opentype (Glib::ustring& font_spec) { + + // Disable/Enable based on available OpenType tables. + font_instance* res = font_factory::Default()->FaceFromFontSpecification( font_spec.c_str() ); + if( res ) { + + std::map<Glib::ustring, OTSubstitution>::iterator it; + + if((it = res->openTypeTables.find("liga"))!= res->openTypeTables.end() || + (it = res->openTypeTables.find("clig"))!= res->openTypeTables.end()) { + _ligatures_common.set_sensitive(); + } else { + _ligatures_common.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("dlig"))!= res->openTypeTables.end()) { + _ligatures_discretionary.set_sensitive(); + } else { + _ligatures_discretionary.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("hlig"))!= res->openTypeTables.end()) { + _ligatures_historical.set_sensitive(); + } else { + _ligatures_historical.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("calt"))!= res->openTypeTables.end()) { + _ligatures_contextual.set_sensitive(); + } else { + _ligatures_contextual.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("subs"))!= res->openTypeTables.end()) { + _position_sub.set_sensitive(); + } else { + _position_sub.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("sups"))!= res->openTypeTables.end()) { + _position_super.set_sensitive(); + } else { + _position_super.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) { + _caps_small.set_sensitive(); + } else { + _caps_small.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() && + (it = res->openTypeTables.find("smcp"))!= res->openTypeTables.end()) { + _caps_all_small.set_sensitive(); + } else { + _caps_all_small.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) { + _caps_petite.set_sensitive(); + } else { + _caps_petite.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("c2sc"))!= res->openTypeTables.end() && + (it = res->openTypeTables.find("pcap"))!= res->openTypeTables.end()) { + _caps_all_petite.set_sensitive(); + } else { + _caps_all_petite.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("unic"))!= res->openTypeTables.end()) { + _caps_unicase.set_sensitive(); + } else { + _caps_unicase.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("titl"))!= res->openTypeTables.end()) { + _caps_titling.set_sensitive(); + } else { + _caps_titling.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("lnum"))!= res->openTypeTables.end()) { + _numeric_lining.set_sensitive(); + } else { + _numeric_lining.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("onum"))!= res->openTypeTables.end()) { + _numeric_old_style.set_sensitive(); + } else { + _numeric_old_style.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pnum"))!= res->openTypeTables.end()) { + _numeric_proportional.set_sensitive(); + } else { + _numeric_proportional.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("tnum"))!= res->openTypeTables.end()) { + _numeric_tabular.set_sensitive(); + } else { + _numeric_tabular.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("frac"))!= res->openTypeTables.end()) { + _numeric_diagonal.set_sensitive(); + } else { + _numeric_diagonal.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("afrac"))!= res->openTypeTables.end()) { + _numeric_stacked.set_sensitive(); + } else { + _numeric_stacked.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("ordn"))!= res->openTypeTables.end()) { + _numeric_ordinal.set_sensitive(); + } else { + _numeric_ordinal.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("zero"))!= res->openTypeTables.end()) { + _numeric_slashed_zero.set_sensitive(); + } else { + _numeric_slashed_zero.set_sensitive( false ); + } + + // East-Asian + if((it = res->openTypeTables.find("jp78"))!= res->openTypeTables.end()) { + _asian_jis78.set_sensitive(); + } else { + _asian_jis78.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp83"))!= res->openTypeTables.end()) { + _asian_jis83.set_sensitive(); + } else { + _asian_jis83.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp90"))!= res->openTypeTables.end()) { + _asian_jis90.set_sensitive(); + } else { + _asian_jis90.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("jp04"))!= res->openTypeTables.end()) { + _asian_jis04.set_sensitive(); + } else { + _asian_jis04.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("smpl"))!= res->openTypeTables.end()) { + _asian_simplified.set_sensitive(); + } else { + _asian_simplified.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("trad"))!= res->openTypeTables.end()) { + _asian_traditional.set_sensitive(); + } else { + _asian_traditional.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("fwid"))!= res->openTypeTables.end()) { + _asian_full_width.set_sensitive(); + } else { + _asian_full_width.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("pwid"))!= res->openTypeTables.end()) { + _asian_proportional_width.set_sensitive(); + } else { + _asian_proportional_width.set_sensitive( false ); + } + + if((it = res->openTypeTables.find("ruby"))!= res->openTypeTables.end()) { + _asian_ruby.set_sensitive(); + } else { + _asian_ruby.set_sensitive( false ); + } + + // List available ligatures + Glib::ustring markup_liga; + Glib::ustring markup_dlig; + Glib::ustring markup_hlig; + Glib::ustring markup_calt; + + for (auto table: res->openTypeTables) { + + if (table.first == "liga" || + table.first == "clig" || + table.first == "dlig" || + table.first == "hgli" || + table.first == "calt") { + + Glib::ustring markup; + markup += "<span font_family='"; + markup += sp_font_description_get_family(res->descr); + markup += "'>"; + markup += Glib::Markup::escape_text(table.second.output); + markup += "</span>"; + + if (table.first == "liga") markup_liga += markup; + if (table.first == "clig") markup_liga += markup; + if (table.first == "dlig") markup_dlig += markup; + if (table.first == "hlig") markup_hlig += markup; + if (table.first == "calt") markup_calt += markup; + } + } + + _ligatures_label_common.set_markup ( markup_liga.c_str() ); + _ligatures_label_discretionary.set_markup ( markup_dlig.c_str() ); + _ligatures_label_historical.set_markup ( markup_hlig.c_str() ); + _ligatures_label_contextual.set_markup ( markup_calt.c_str() ); + + // List available numeric variants + Glib::ustring markup_lnum; + Glib::ustring markup_onum; + Glib::ustring markup_pnum; + Glib::ustring markup_tnum; + Glib::ustring markup_frac; + Glib::ustring markup_afrc; + Glib::ustring markup_ordn; + Glib::ustring markup_zero; + + for (auto table: res->openTypeTables) { + + Glib::ustring markup; + markup += "<span font_family='"; + markup += sp_font_description_get_family(res->descr); + markup += "' font_features='"; + markup += table.first; + markup += "'>"; + if (table.first == "lnum" || + table.first == "onum" || + table.first == "pnum" || + table.first == "tnum") markup += "0123456789"; + if (table.first == "zero") markup += "0"; + if (table.first == "ordn") markup += "[" + table.second.before + "]" + table.second.output; + if (table.first == "frac" || + table.first == "afrc" ) markup += "1/2 2/3 3/4 4/5 5/6"; // Can we do better? + markup += "</span>"; + + if (table.first == "lnum") markup_lnum += markup; + if (table.first == "onum") markup_onum += markup; + if (table.first == "pnum") markup_pnum += markup; + if (table.first == "tnum") markup_tnum += markup; + if (table.first == "frac") markup_frac += markup; + if (table.first == "afrc") markup_afrc += markup; + if (table.first == "ordn") markup_ordn += markup; + if (table.first == "zero") markup_zero += markup; + } + + _numeric_lining_label.set_markup ( markup_lnum.c_str() ); + _numeric_old_style_label.set_markup ( markup_onum.c_str() ); + _numeric_proportional_label.set_markup ( markup_pnum.c_str() ); + _numeric_tabular_label.set_markup ( markup_tnum.c_str() ); + _numeric_diagonal_label.set_markup ( markup_frac.c_str() ); + _numeric_stacked_label.set_markup ( markup_afrc.c_str() ); + _numeric_ordinal_label.set_markup ( markup_ordn.c_str() ); + _numeric_slashed_zero_label.set_markup ( markup_zero.c_str() ); + + // Make list of tables not handled above. + std::map<Glib::ustring, OTSubstitution> table_copy = res->openTypeTables; + if( (it = table_copy.find("liga")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("clig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("hlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("calt")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("subs")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("sups")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("smcp")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("c2sc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pcap")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("c2pc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("unic")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("titl")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("lnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("onum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("tnum")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("frac")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("afrc")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ordn")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("zero")) != table_copy.end() ) table_copy.erase( it ); + + if( (it = table_copy.find("jp78")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp83")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp90")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("jp04")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("smpl")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("trad")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("fwid")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pwid")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ruby")) != table_copy.end() ) table_copy.erase( it ); + + // An incomplete list of tables that should not be exposed to the user: + if( (it = table_copy.find("abvf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("abvs")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("akhn")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("blwf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("blws")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ccmp")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("cjct")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dnom")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("dtls")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("fina")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("half")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("haln")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("init")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("isol")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("locl")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("medi")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("nukt")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("numr")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pref")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pres")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("pstf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("psts")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rlig")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rkrf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rphf")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("rtlm")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("ssty")) != table_copy.end() ) table_copy.erase( it ); + if( (it = table_copy.find("vatu")) != table_copy.end() ) table_copy.erase( it ); + + // Clear out old features + auto children = _feature_grid.get_children(); + for (auto child: children) { + _feature_grid.remove (*child); + } + _features.clear(); + + std::string markup; + int grid_row = 0; + + // GSUB lookup type 1 (1 to 1 mapping). + for (auto table: res->openTypeTables) { + if (table.first == "case" || + table.first == "hist" || + (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't'))) { + + if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it ); + + _features[table.first] = new Feature (table.first, table.second, 2, + sp_font_description_get_family(res->descr), + _feature_grid, grid_row, this); + grid_row++; + } + } + + // GSUB lookup type 3 (1 to many mapping). Optionally type 1. + for (auto table: res->openTypeTables) { + if (table.first == "salt" || + table.first == "swsh" || + table.first == "cwsh" || + table.first == "ornm" || + table.first == "nalt" || + (table.first[0] == 'c' && table.first[1] == 'v')) { + + if (table.second.input.length() == 0) { + // This can happen if a table is not in the 'DFLT' script and 'dflt' language. + // We should be using the 'lang' attribute to find the correct tables. + // std::cerr << "FontVariants::open_type_update: " + // << table.first << " has no entries!" << std::endl; + continue; + } + + if( (it = table_copy.find(table.first)) != table_copy.end() ) table_copy.erase( it ); + + // Our lame attempt at determining number of alternative glyphs for one glyph: + int number = table.second.output.length() / table.second.input.length(); + if (number < 1) { + number = 1; // Must have at least on/off, see comment above about 'lang' attribute. + // std::cout << table.first << " " + // << table.second.output.length() << "/" + // << table.second.input.length() << "=" + // << number << std::endl; + } + + _features[table.first] = new Feature (table.first, table.second, number+1, + sp_font_description_get_family(res->descr), + _feature_grid, grid_row, this); + grid_row++; + } + } + + _feature_grid.show_all(); + + _feature_substitutions.set_markup ( markup.c_str() ); + + std::string ott_list = "OpenType tables not included above: "; + for(it = table_copy.begin(); it != table_copy.end(); ++it) { + ott_list += it->first; + ott_list += ", "; + } + + if (table_copy.size() > 0) { + ott_list.pop_back(); + ott_list.pop_back(); + _feature_list.set_text( ott_list.c_str() ); + } else { + _feature_list.set_text( "" ); + } + + } else { + std::cerr << "FontVariants::update(): Couldn't find font_instance for: " + << font_spec << std::endl; + } + + _ligatures_changed = false; + _position_changed = false; + _caps_changed = false; + _numeric_changed = false; + _feature_changed = false; + } + + void + FontVariants::fill_css( SPCSSAttr *css ) { + + // Ligatures + bool common = _ligatures_common.get_active(); + bool discretionary = _ligatures_discretionary.get_active(); + bool historical = _ligatures_historical.get_active(); + bool contextual = _ligatures_contextual.get_active(); + + if( !common && !discretionary && !historical && !contextual ) { + sp_repr_css_set_property(css, "font-variant-ligatures", "none" ); + } else if ( common && !discretionary && !historical && contextual ) { + sp_repr_css_set_property(css, "font-variant-ligatures", "normal" ); + } else { + Glib::ustring css_string; + if ( !common ) + css_string += "no-common-ligatures "; + if ( discretionary ) + css_string += "discretionary-ligatures "; + if ( historical ) + css_string += "historical-ligatures "; + if ( !contextual ) + css_string += "no-contextual "; + sp_repr_css_set_property(css, "font-variant-ligatures", css_string.c_str() ); + } + + // Position + { + unsigned position_new = SP_CSS_FONT_VARIANT_POSITION_NORMAL; + Glib::ustring css_string; + if( _position_normal.get_active() ) { + css_string = "normal"; + } else if( _position_sub.get_active() ) { + css_string = "sub"; + position_new = SP_CSS_FONT_VARIANT_POSITION_SUB; + } else if( _position_super.get_active() ) { + css_string = "super"; + position_new = SP_CSS_FONT_VARIANT_POSITION_SUPER; + } + + // 'if' may not be necessary... need to test. + if( (_position_all != position_new) || ((_position_mix != 0) && _position_changed) ) { + sp_repr_css_set_property(css, "font-variant-position", css_string.c_str() ); + } + } + + // Caps + { + //unsigned caps_new; + Glib::ustring css_string; + if( _caps_normal.get_active() ) { + css_string = "normal"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL; + } else if( _caps_small.get_active() ) { + css_string = "small-caps"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_SMALL; + } else if( _caps_all_small.get_active() ) { + css_string = "all-small-caps"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_SMALL; + } else if( _caps_petite.get_active() ) { + css_string = "petite"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_PETITE; + } else if( _caps_all_petite.get_active() ) { + css_string = "all-petite"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_ALL_PETITE; + } else if( _caps_unicase.get_active() ) { + css_string = "unicase"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_UNICASE; + } else if( _caps_titling.get_active() ) { + css_string = "titling"; + // caps_new = SP_CSS_FONT_VARIANT_CAPS_TITLING; + //} else { + // caps_new = SP_CSS_FONT_VARIANT_CAPS_NORMAL; + } + + // May not be necessary... need to test. + //if( (_caps_all != caps_new) || ((_caps_mix != 0) && _caps_changed) ) { + sp_repr_css_set_property(css, "font-variant-caps", css_string.c_str() ); + //} + } + + // Numeric + bool default_style = _numeric_default_style.get_active(); + bool lining = _numeric_lining.get_active(); + bool old_style = _numeric_old_style.get_active(); + + bool default_width = _numeric_default_width.get_active(); + bool proportional = _numeric_proportional.get_active(); + bool tabular = _numeric_tabular.get_active(); + + bool default_fractions = _numeric_default_fractions.get_active(); + bool diagonal = _numeric_diagonal.get_active(); + bool stacked = _numeric_stacked.get_active(); + + bool ordinal = _numeric_ordinal.get_active(); + bool slashed_zero = _numeric_slashed_zero.get_active(); + + if (default_style & default_width & default_fractions & !ordinal & !slashed_zero) { + sp_repr_css_set_property(css, "font-variant-numeric", "normal"); + } else { + Glib::ustring css_string; + if ( lining ) + css_string += "lining-nums "; + if ( old_style ) + css_string += "oldstyle-nums "; + if ( proportional ) + css_string += "proportional-nums "; + if ( tabular ) + css_string += "tabular-nums "; + if ( diagonal ) + css_string += "diagonal-fractions "; + if ( stacked ) + css_string += "stacked-fractions "; + if ( ordinal ) + css_string += "ordinal "; + if ( slashed_zero ) + css_string += "slashed-zero "; + sp_repr_css_set_property(css, "font-variant-numeric", css_string.c_str() ); + } + + // East Asian + bool default_variant = _asian_default_variant.get_active(); + bool jis78 = _asian_jis78.get_active(); + bool jis83 = _asian_jis83.get_active(); + bool jis90 = _asian_jis90.get_active(); + bool jis04 = _asian_jis04.get_active(); + bool simplified = _asian_simplified.get_active(); + bool traditional = _asian_traditional.get_active(); + bool asian_width = _asian_default_width.get_active(); + bool fwid = _asian_full_width.get_active(); + bool pwid = _asian_proportional_width.get_active(); + bool ruby = _asian_ruby.get_active(); + + if (default_style & asian_width & !ruby) { + sp_repr_css_set_property(css, "font-variant-east-asian", "normal"); + } else { + Glib::ustring css_string; + if (jis78) css_string += "jis78 "; + if (jis83) css_string += "jis83 "; + if (jis90) css_string += "jis90 "; + if (jis04) css_string += "jis04 "; + if (simplified) css_string += "simplfied "; + if (traditional) css_string += "traditional "; + + if (fwid) css_string += "fwid "; + if (pwid) css_string += "pwid "; + + if (ruby) css_string += "ruby "; + + sp_repr_css_set_property(css, "font-variant-east-asian", css_string.c_str() ); + } + + // Feature settings + Glib::ustring feature_string; + for (auto i: _features) { + feature_string += i.second->get_css(); + } + + feature_string += _feature_entry.get_text(); + // std::cout << "feature_string: " << feature_string << std::endl; + + if (!feature_string.empty()) { + sp_repr_css_set_property(css, "font-feature-settings", feature_string.c_str()); + } else { + sp_repr_css_unset_property(css, "font-feature-settings"); + } + } + + Glib::ustring + FontVariants::get_markup() { + + Glib::ustring markup; + + // Ligatures + bool common = _ligatures_common.get_active(); + bool discretionary = _ligatures_discretionary.get_active(); + bool historical = _ligatures_historical.get_active(); + bool contextual = _ligatures_contextual.get_active(); + + if (!common) markup += "liga=0,clig=0,"; // On by default. + if (discretionary) markup += "dlig=1,"; + if (historical) markup += "hlig=1,"; + if (contextual) markup += "calt=1,"; + + // Position + if ( _position_sub.get_active() ) markup += "subs=1,"; + else if ( _position_super.get_active() ) markup += "sups=1,"; + + // Caps + if ( _caps_small.get_active() ) markup += "smcp=1,"; + else if ( _caps_all_small.get_active() ) markup += "c2sc=1,smcp=1,"; + else if ( _caps_petite.get_active() ) markup += "pcap=1,"; + else if ( _caps_all_petite.get_active() ) markup += "c2pc=1,pcap=1,"; + else if ( _caps_unicase.get_active() ) markup += "unic=1,"; + else if ( _caps_titling.get_active() ) markup += "titl=1,"; + + // Numeric + bool default_style = _numeric_default_style.get_active(); + bool lining = _numeric_lining.get_active(); + bool old_style = _numeric_old_style.get_active(); + + bool default_width = _numeric_default_width.get_active(); + bool proportional = _numeric_proportional.get_active(); + bool tabular = _numeric_tabular.get_active(); + + bool default_fractions = _numeric_default_fractions.get_active(); + bool diagonal = _numeric_diagonal.get_active(); + bool stacked = _numeric_stacked.get_active(); + + bool ordinal = _numeric_ordinal.get_active(); + bool slashed_zero = _numeric_slashed_zero.get_active(); + + if (lining) markup += "lnum=1,"; + if (old_style) markup += "onum=1,"; + if (proportional) markup += "pnum=1,"; + if (tabular) markup += "tnum=1,"; + if (diagonal) markup += "frac=1,"; + if (stacked) markup += "afrc=1,"; + if (ordinal) markup += "ordn=1,"; + if (slashed_zero) markup += "zero=1,"; + + // East Asian + bool default_variant = _asian_default_variant.get_active(); + bool jis78 = _asian_jis78.get_active(); + bool jis83 = _asian_jis83.get_active(); + bool jis90 = _asian_jis90.get_active(); + bool jis04 = _asian_jis04.get_active(); + bool simplified = _asian_simplified.get_active(); + bool traditional = _asian_traditional.get_active(); + bool asian_width = _asian_default_width.get_active(); + bool fwid = _asian_full_width.get_active(); + bool pwid = _asian_proportional_width.get_active(); + bool ruby = _asian_ruby.get_active(); + + if (jis78 ) markup += "jp78=1,"; + if (jis83 ) markup += "jp83=1,"; + if (jis90 ) markup += "jp90=1,"; + if (jis04 ) markup += "jp04=1,"; + if (simplified ) markup += "smpl=1,"; + if (traditional ) markup += "trad=1,"; + + if (fwid ) markup += "fwid=1,"; + if (pwid ) markup += "pwid=1,"; + + if (ruby ) markup += "ruby=1,"; + + // Feature settings + Glib::ustring feature_string; + for (auto i: _features) { + feature_string += i.second->get_css(); + } + + feature_string += _feature_entry.get_text(); + if (!feature_string.empty()) { + markup += feature_string; + } + + // std::cout << "|" << markup << "|" << std::endl; + return markup; + } + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variants.h b/src/ui/widget/font-variants.h new file mode 100644 index 0000000..83c9fa9 --- /dev/null +++ b/src/ui/widget/font-variants.h @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2015, 2018 Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_VARIANT_H +#define INKSCAPE_UI_WIDGET_FONT_VARIANT_H + +#include <gtkmm/expander.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> + +class SPDesktop; +class SPObject; +class SPStyle; +class SPCSSAttr; + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Feature; + +/** + * A container for selecting font variants (OpenType Features). + */ +class FontVariants : public Gtk::VBox +{ + +public: + + /** + * Constructor + */ + FontVariants(); + +protected: + // Ligatures: To start, use four check buttons. + Gtk::Expander _ligatures_frame; + Gtk::Grid _ligatures_grid; + Gtk::CheckButton _ligatures_common; + Gtk::CheckButton _ligatures_discretionary; + Gtk::CheckButton _ligatures_historical; + Gtk::CheckButton _ligatures_contextual; + Gtk::Label _ligatures_label_common; + Gtk::Label _ligatures_label_discretionary; + Gtk::Label _ligatures_label_historical; + Gtk::Label _ligatures_label_contextual; + + // Position: Exclusive options + Gtk::Expander _position_frame; + Gtk::Grid _position_grid; + Gtk::RadioButton _position_normal; + Gtk::RadioButton _position_sub; + Gtk::RadioButton _position_super; + + // Caps: Exclusive options (maybe a dropdown menu to save space?) + Gtk::Expander _caps_frame; + Gtk::Grid _caps_grid; + Gtk::RadioButton _caps_normal; + Gtk::RadioButton _caps_small; + Gtk::RadioButton _caps_all_small; + Gtk::RadioButton _caps_petite; + Gtk::RadioButton _caps_all_petite; + Gtk::RadioButton _caps_unicase; + Gtk::RadioButton _caps_titling; + + // Numeric: Complicated! + Gtk::Expander _numeric_frame; + Gtk::Grid _numeric_grid; + + Gtk::RadioButton _numeric_default_style; + Gtk::RadioButton _numeric_lining; + Gtk::Label _numeric_lining_label; + Gtk::RadioButton _numeric_old_style; + Gtk::Label _numeric_old_style_label; + + Gtk::RadioButton _numeric_default_width; + Gtk::RadioButton _numeric_proportional; + Gtk::Label _numeric_proportional_label; + Gtk::RadioButton _numeric_tabular; + Gtk::Label _numeric_tabular_label; + + Gtk::RadioButton _numeric_default_fractions; + Gtk::RadioButton _numeric_diagonal; + Gtk::Label _numeric_diagonal_label; + Gtk::RadioButton _numeric_stacked; + Gtk::Label _numeric_stacked_label; + + Gtk::CheckButton _numeric_ordinal; + Gtk::Label _numeric_ordinal_label; + + Gtk::CheckButton _numeric_slashed_zero; + Gtk::Label _numeric_slashed_zero_label; + + // East Asian: Complicated! + Gtk::Expander _asian_frame; + Gtk::Grid _asian_grid; + + Gtk::RadioButton _asian_default_variant; + Gtk::RadioButton _asian_jis78; + Gtk::RadioButton _asian_jis83; + Gtk::RadioButton _asian_jis90; + Gtk::RadioButton _asian_jis04; + Gtk::RadioButton _asian_simplified; + Gtk::RadioButton _asian_traditional; + + Gtk::RadioButton _asian_default_width; + Gtk::RadioButton _asian_full_width; + Gtk::RadioButton _asian_proportional_width; + + Gtk::CheckButton _asian_ruby; + + // ----- + Gtk::Expander _feature_frame; + Gtk::Grid _feature_grid; + Gtk::VBox _feature_vbox; + Gtk::Entry _feature_entry; + Gtk::Label _feature_label; + Gtk::Label _feature_list; + Gtk::Label _feature_substitutions; + +private: + void ligatures_init(); + void ligatures_callback(); + + void position_init(); + void position_callback(); + + void caps_init(); + void caps_callback(); + + void numeric_init(); + void numeric_callback(); + + void asian_init(); + void asian_callback(); + + void feature_init(); +public: + void feature_callback(); + +private: + // To determine if we need to write out property (may not be necessary) + unsigned _ligatures_all; + unsigned _position_all; + unsigned _caps_all; + unsigned _numeric_all; + unsigned _asian_all; + + unsigned _ligatures_mix; + unsigned _position_mix; + unsigned _caps_mix; + unsigned _numeric_mix; + unsigned _asian_mix; + + bool _ligatures_changed; + bool _position_changed; + bool _caps_changed; + bool _numeric_changed; + bool _feature_changed; + bool _asian_changed; + + std::map<std::string, Feature*> _features; + + sigc::signal<void> _changed_signal; + +public: + + /** + * Update GUI based on query results. + */ + void update( SPStyle const *query, bool different_features, Glib::ustring& font_spec ); + + /** + * Update GUI based on OpenType features of selected font. + */ + void update_opentype( Glib::ustring& font_spec ); + + /** + * Fill SPCSSAttr based on settings of buttons. + */ + void fill_css( SPCSSAttr* css ); + + /** + * Get CSS string for markup. + */ + Glib::ustring get_markup(); + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return _changed_signal.connect(slot); + } +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_VARIANT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variations.cpp b/src/ui/widget/font-variations.cpp new file mode 100644 index 0000000..fae5cc3 --- /dev/null +++ b/src/ui/widget/font-variations.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iostream> +#include <iomanip> + +#include <gtkmm.h> +#include <glibmm/i18n.h> + +#include <libnrtype/font-instance.h> + +#include "font-variations.h" + +// For updating from selection +#include "desktop.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +FontVariationAxis::FontVariationAxis (Glib::ustring name, OTVarAxis& axis) + : name (name) +{ + + // std::cout << "FontVariationAxis::FontVariationAxis:: " + // << " name: " << name + // << " min: " << axis.minimum + // << " max: " << axis.maximum + // << " val: " << axis.set_val << std::endl; + + label = Gtk::manage( new Gtk::Label( name ) ); + add( *label ); + + precision = 2 - int( log10(axis.maximum - axis.minimum)); + if (precision < 0) precision = 0; + + scale = Gtk::manage( new Gtk::Scale() ); + scale->set_range (axis.minimum, axis.maximum); + scale->set_value (axis.set_val); + scale->set_digits (precision); + scale->set_hexpand(true); + add( *scale ); +} + + +// ------------------------------------------------------------- // + +FontVariations::FontVariations () : + Gtk::Grid () +{ + // std::cout << "FontVariations::FontVariations" << std::endl; + set_orientation( Gtk::ORIENTATION_VERTICAL ); + set_name ("FontVariations"); + size_group = Gtk::SizeGroup::create(Gtk::SIZE_GROUP_HORIZONTAL); + show_all_children(); +} + + +// Update GUI based on query. +void +FontVariations::update (const Glib::ustring& font_spec) { + + font_instance* res = font_factory::Default()->FaceFromFontSpecification (font_spec.c_str()); + + auto children = get_children(); + for (auto child: children) { + remove ( *child ); + } + axes.clear(); + + for (auto a: res->openTypeVarAxes) { + // std::cout << "Creating axis: " << a.first << std::endl; + FontVariationAxis* axis = Gtk::manage( new FontVariationAxis( a.first, a.second )); + axes.push_back( axis ); + add( *axis ); + size_group->add_widget( *(axis->get_label()) ); // Keep labels the same width + axis->get_scale()->signal_value_changed().connect( + sigc::mem_fun(*this, &FontVariations::on_variations_change) + ); + } + + show_all_children(); +} + +void +FontVariations::fill_css( SPCSSAttr *css ) { + + // Eventually will want to favor using 'font-weight', etc. but at the moment these + // can't handle "fractional" values. See CSS Fonts Module Level 4. + sp_repr_css_set_property(css, "font-variation-settings", get_css_string().c_str()); +} + +Glib::ustring +FontVariations::get_css_string() { + + Glib::ustring css_string; + + for (auto axis: axes) { + Glib::ustring name = axis->get_name(); + + // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.) + if (name == "Width") name = "wdth"; // 'font-stretch' + if (name == "Weight") name = "wght"; // 'font-weight' + if (name == "Optical size") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution. + if (name == "Slant") name = "slnt"; // 'font-style' + if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic. + + std::stringstream value; + value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value(); + css_string += "'" + name + "' " + value.str() + "', "; + } + + return css_string; +} + +Glib::ustring +FontVariations::get_pango_string() { + + Glib::ustring pango_string; + + if (!axes.empty()) { + + pango_string += "@"; + + for (auto axis: axes) { + if (axis->get_value() == 0) continue; // TEMP: Should check against default value. + Glib::ustring name = axis->get_name(); + + // Translate the "named" axes. (Additional names in 'stat' table, may need to handle them.) + if (name == "Width") name = "wdth"; // 'font-stretch' + if (name == "Weight") name = "wght"; // 'font-weight' + if (name == "Optical size") name = "opsz"; // 'font-optical-sizing' Can trigger glyph substitution. + if (name == "Slant") name = "slnt"; // 'font-style' + if (name == "Italic") name = "ital"; // 'font-style' Toggles from Roman to Italic. + + std::stringstream value; + value << std::fixed << std::setprecision(axis->get_precision()) << axis->get_value(); + pango_string += name + "=" + value.str() + ","; + } + + pango_string.erase (pango_string.size() - 1); // Erase last ',' + } + + return pango_string; +} + +void +FontVariations::on_variations_change() { + // std::cout << "FontVariations::on_variations_change: " << get_css_string() << std::endl;; + signal_changed.emit (); +} + +bool FontVariations::variations_present() const { + return !axes.empty(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/font-variations.h b/src/ui/widget/font-variations.h new file mode 100644 index 0000000..a3d3896 --- /dev/null +++ b/src/ui/widget/font-variations.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Felipe Corrêa da Silva Sanches <juca@members.fsf.org> + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2018 Felipe Corrêa da Silva Sanches, Tavmong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H +#define INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H + +#include <gtkmm/grid.h> +#include <gtkmm/sizegroup.h> +#include <gtkmm/label.h> +#include <gtkmm/scale.h> + +#include "libnrtype/OpenTypeUtil.h" + +#include "style.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +/** + * A widget for a single axis: Label and Slider + */ +class FontVariationAxis : public Gtk::Grid +{ +public: + FontVariationAxis(Glib::ustring name, OTVarAxis& axis); + Glib::ustring get_name() { return name; } + Gtk::Label* get_label() { return label; } + double get_value() { return scale->get_value(); } + int get_precision() { return precision; } + Gtk::Scale* get_scale() { return scale; } + +private: + + // Widgets + Glib::ustring name; + Gtk::Label* label; + Gtk::Scale* scale; + + int precision; + + // Signals + sigc::signal<void> signal_changed; +}; + +/** + * A widget for selecting font variations (OpenType Variations). + */ +class FontVariations : public Gtk::Grid +{ + +public: + + /** + * Constructor + */ + FontVariations(); + +protected: + +public: + + /** + * Update GUI. + */ + void update(const Glib::ustring& font_spec); + + /** + * Fill SPCSSAttr based on settings of buttons. + */ + void fill_css( SPCSSAttr* css ); + + /** + * Get CSS String + */ + Glib::ustring get_css_string(); + + Glib::ustring get_pango_string(); + + void on_variations_change(); + + /** + * Let others know that user has changed GUI settings. + * (Used to enable 'Apply' and 'Default' buttons.) + */ + sigc::connection connectChanged(sigc::slot<void> slot) { + return signal_changed.connect(slot); + } + + // return true if there are some variations present + bool variations_present() const; + +private: + + std::vector<FontVariationAxis*> axes; + Glib::RefPtr<Gtk::SizeGroup> size_group; + + sigc::signal<void> signal_changed; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FONT_VARIATIONS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/frame.cpp b/src/ui/widget/frame.cpp new file mode 100644 index 0000000..eac4e22 --- /dev/null +++ b/src/ui/widget/frame.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Murray C + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "frame.h" + + +// Inkscape::UI::Widget::Frame + +namespace Inkscape { +namespace UI { +namespace Widget { + +Frame::Frame(Glib::ustring const &label_text /*= ""*/, gboolean label_bold /*= TRUE*/ ) + : _label(label_text, Gtk::ALIGN_END, Gtk::ALIGN_CENTER, true) +{ + set_shadow_type(Gtk::SHADOW_NONE); + + set_label_widget(_label); + set_label(label_text, label_bold); +} + +void +Frame::add(Widget& widget) +{ + Gtk::Frame::add(widget); + set_padding(4, 0, 8, 0); + show_all_children(); +} + +void +Frame::set_label(const Glib::ustring &label_text, gboolean label_bold /*= TRUE*/) +{ + if (label_bold) { + _label.set_markup(Glib::ustring("<b>") + label_text + "</b>"); + } else { + _label.set_text(label_text); + } +} + +void +Frame::set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right) +{ + auto child = get_child(); + + if(child) + { + child->set_margin_top(padding_top); + child->set_margin_bottom(padding_bottom); + child->set_margin_start(padding_left); + child->set_margin_end(padding_right); + } +} + +Gtk::Label const * +Frame::get_label_widget() const +{ + return &_label; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/frame.h b/src/ui/widget/frame.h new file mode 100644 index 0000000..b2934b6 --- /dev/null +++ b/src/ui/widget/frame.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Murray C + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_FRAME_H +#define INKSCAPE_UI_WIDGET_FRAME_H + +#include <gtkmm/frame.h> +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Creates a Gnome HIG style indented frame with bold label + * See http://developer.gnome.org/hig-book/stable/controls-frames.html.en + */ +class Frame : public Gtk::Frame +{ +public: + + /** + * Construct a Frame Widget. + * + * @param label The frame text. + */ + Frame(Glib::ustring const &label = "", gboolean label_bold = TRUE); + + /** + * Return the label widget + */ + Gtk::Label const *get_label_widget() const; + + /** + * Add a widget to this frame + */ + void add(Widget& widget) override; + + /** + * Set the frame label text and if bold or not + */ + void set_label(const Glib::ustring &label, gboolean label_bold = TRUE); + + /** + * Set the frame padding + */ + void set_padding (guint padding_top, guint padding_bottom, guint padding_left, guint padding_right); + +protected: + Gtk::Label _label; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_FRAME_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/highlight-picker.cpp b/src/ui/widget/highlight-picker.cpp new file mode 100644 index 0000000..f35e405 --- /dev/null +++ b/src/ui/widget/highlight-picker.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm.h> +#include <gtkmm/icontheme.h> + +#include "display/cairo-utils.h" + +#include "highlight-picker.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +HighlightPicker::HighlightPicker() : + Glib::ObjectBase(typeid(HighlightPicker)), + Gtk::CellRendererPixbuf(), + _property_active(*this, "active", 0) +{ + + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; +} + +HighlightPicker::~HighlightPicker() += default; + +void HighlightPicker::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void HighlightPicker::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void HighlightPicker::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + GdkRectangle carea; + + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 10, 20); + cairo_t *ct = cairo_create(s); + + /* Transparent area */ + carea.x = 0; + carea.y = 0; + carea.width = 10; + carea.height = 20; + + cairo_pattern_t *checkers = ink_cairo_pattern_create_checkerboard(); + + cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height / 2); + cairo_set_source(ct, checkers); + cairo_fill_preserve(ct); + ink_cairo_set_source_rgba32(ct, _property_active.get_value()); + cairo_fill(ct); + + cairo_pattern_destroy(checkers); + + cairo_rectangle(ct, carea.x, carea.y + carea.height / 2, carea.width, carea.height / 2); + ink_cairo_set_source_rgba32(ct, _property_active.get_value() | 0x000000ff); + cairo_fill(ct); + + cairo_rectangle(ct, carea.x, carea.y, carea.width, carea.height); + ink_cairo_set_source_rgba32(ct, 0x333333ff); + cairo_set_line_width(ct, 2); + cairo_stroke(ct); + + cairo_destroy(ct); + cairo_surface_flush(s); + + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_data( cairo_image_surface_get_data(s), + GDK_COLORSPACE_RGB, TRUE, 8, + 10, 20, cairo_image_surface_get_stride(s), + ink_cairo_pixbuf_cleanup, s); + convert_pixbuf_argb32_to_normal(pixbuf); + + property_pixbuf() = Glib::wrap(pixbuf); + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool HighlightPicker::activate_vfunc(GdkEvent* /*event*/, + Gtk::Widget& /*widget*/, + const Glib::ustring& /*path*/, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + return false; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +//should be okay to put this here +/** + * Converts GdkPixbuf's data to premultiplied ARGB. + * This function will convert a GdkPixbuf in place into Cairo's native pixel format. + * Note that this is a hack intended to save memory. When the pixbuf is in Cairo's format, + * using it with GTK will result in corrupted drawings. + */ +void +convert_pixbuf_normal_to_argb32(GdkPixbuf *pb) +{ + convert_pixels_pixbuf_to_argb32( + gdk_pixbuf_get_pixels(pb), + gdk_pixbuf_get_width(pb), + gdk_pixbuf_get_height(pb), + gdk_pixbuf_get_rowstride(pb)); +} + +/** + * Converts GdkPixbuf's data back to its native format. + * Once this is done, the pixbuf can be used with GTK again. + */ +void +convert_pixbuf_argb32_to_normal(GdkPixbuf *pb) +{ + convert_pixels_argb32_to_pixbuf( + gdk_pixbuf_get_pixels(pb), + gdk_pixbuf_get_width(pb), + gdk_pixbuf_get_height(pb), + gdk_pixbuf_get_rowstride(pb)); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/widget/highlight-picker.h b/src/ui/widget/highlight-picker.h new file mode 100644 index 0000000..0b77a23 --- /dev/null +++ b/src/ui/widget/highlight-picker.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_HIGHLIGHT_PICKER_H__ +#define __UI_DIALOG_HIGHLIGHT_PICKER_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class HighlightPicker : public Gtk::CellRendererPixbuf { +public: + HighlightPicker(); + ~HighlightPicker() override; + + Glib::PropertyProxy<guint32> property_active() { return _property_active.get_proxy(); } + +protected: + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + +private: + + Glib::Property<guint32> _property_active; +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/iconrenderer.cpp b/src/ui/widget/iconrenderer.cpp new file mode 100644 index 0000000..4ca250e --- /dev/null +++ b/src/ui/widget/iconrenderer.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/iconrenderer.h" + +#include "layertypeicon.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "widgets/toolbox.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +IconRenderer::IconRenderer() : + Glib::ObjectBase(typeid(IconRenderer)), + Gtk::CellRendererPixbuf(), + _property_icon(*this, "icon", 0) +{ + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + set_pixbuf(); +} + +/* + * Called when an icon is clicked. + */ +IconRenderer::type_signal_activated IconRenderer::signal_activated() +{ + return m_signal_activated; +} + +void IconRenderer::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void IconRenderer::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void IconRenderer::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + set_pixbuf(); + + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool IconRenderer::activate_vfunc(GdkEvent* /*event*/, + Gtk::Widget& /*widget*/, + const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + m_signal_activated.emit(path); + return true; +} + +void IconRenderer::add_icon(Glib::ustring name) +{ + _icons.push_back(sp_get_icon_pixbuf(name.c_str(), GTK_ICON_SIZE_BUTTON)); +} + +void IconRenderer::set_pixbuf() +{ + int icon_index = property_icon().get_value(); + if(icon_index >= 0 && icon_index < _icons.size()) { + property_pixbuf() = _icons[icon_index]; + } else { + property_pixbuf() = sp_get_icon_pixbuf("image-missing", GTK_ICON_SIZE_BUTTON); + } +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/iconrenderer.h b/src/ui/widget/iconrenderer.h new file mode 100644 index 0000000..662ce5b --- /dev/null +++ b/src/ui/widget/iconrenderer.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_ADDTOICON_H__ +#define __UI_DIALOG_ADDTOICON_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * Martin Owens 2018 <doctormo@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class IconRenderer : public Gtk::CellRendererPixbuf { +public: + IconRenderer(); + ~IconRenderer() override = default;; + + Glib::PropertyProxy<int> property_icon() { return _property_icon.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + + void add_icon(Glib::ustring name); + + typedef sigc::signal<void, Glib::ustring> type_signal_activated; + type_signal_activated signal_activated(); +protected: + type_signal_activated m_signal_activated; + + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + +private: + + Glib::Property<int> _property_icon; + std::vector<Glib::RefPtr<Gdk::Pixbuf>> _icons; + void set_pixbuf(); +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/imagetoggler.cpp b/src/ui/widget/imagetoggler.cpp new file mode 100644 index 0000000..a1d258e --- /dev/null +++ b/src/ui/widget/imagetoggler.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jon A. Cruz + * Johan B. C. Engelen + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "ui/widget/imagetoggler.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "widgets/toolbox.h" + +#include <iostream> + +namespace Inkscape { +namespace UI { +namespace Widget { + +ImageToggler::ImageToggler( char const* on, char const* off) : + Glib::ObjectBase(typeid(ImageToggler)), + Gtk::CellRendererPixbuf(), + _pixOnName(on), + _pixOffName(off), + _property_active(*this, "active", false), + _property_activatable(*this, "activatable", true), + _property_pixbuf_on(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_off(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) +{ + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + + _property_pixbuf_on = sp_get_icon_pixbuf(_pixOnName, GTK_ICON_SIZE_MENU); + _property_pixbuf_off = sp_get_icon_pixbuf(_pixOffName, GTK_ICON_SIZE_MENU); + + property_pixbuf() = _property_pixbuf_off.get_value(); +} + +void ImageToggler::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void ImageToggler::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void ImageToggler::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + property_pixbuf() = _property_active.get_value() ? _property_pixbuf_on : _property_pixbuf_off; + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool +ImageToggler::activate_vfunc(GdkEvent* event, + Gtk::Widget& /*widget*/, + const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + _signal_pre_toggle.emit(event); + _signal_toggled.emit(path); + + return false; +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/widget/imagetoggler.h b/src/ui/widget/imagetoggler.h new file mode 100644 index 0000000..a03ee37 --- /dev/null +++ b/src/ui/widget/imagetoggler.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_IMAGETOGGLER_H__ +#define __UI_DIALOG_IMAGETOGGLER_H__ +/* + * Authors: + * Jon A. Cruz + * Johan B. C. Engelen + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ImageToggler : public Gtk::CellRendererPixbuf { +public: + ImageToggler( char const *on, char const *off); + ~ImageToggler() override = default;; + + sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;} + sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; } + + Glib::PropertyProxy<bool> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy<bool> property_activatable() { return _property_activatable.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + +protected: + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + +private: + Glib::ustring _pixOnName; + Glib::ustring _pixOffName; + + Glib::Property<bool> _property_active; + Glib::Property<bool> _property_activatable; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_on; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_off; + + sigc::signal<void, const Glib::ustring&> _signal_toggled; + sigc::signal<void, GdkEvent const *> _signal_pre_toggle; +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-color-wheel.cpp b/src/ui/widget/ink-color-wheel.cpp new file mode 100644 index 0000000..7e56ee5 --- /dev/null +++ b/src/ui/widget/ink-color-wheel.cpp @@ -0,0 +1,726 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Color wheel widget. Outer ring for Hue. Inner triangle for Saturation and Value. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include "ink-color-wheel.h" + +// A point with a color value. +class color_point { +public: + color_point() : x(0), y(0), r(0), g(0), b(0) {}; + color_point(double x, double y, double r, double g, double b) : x(x), y(y), r(r), g(g), b(b) {}; + color_point(double x, double y, guint32 color) : x(x), y(y), + r(((color & 0xff0000) >> 16)/255.0), + g(((color & 0xff00) >> 8)/255.0), + b(((color & 0xff) )/255.0) {}; + guint32 get_color() { return (int(r*255) << 16 | int(g*255) << 8 | int(b*255)); }; + double x; + double y; + double r; + double g; + double b; +}; + +inline double lerp(const double& v0, const double& v1, const double& t0, const double&t1, const double& t) { + double s = 0; + if (t0 != t1) { + s = (t - t0)/(t1 - t0); + } + return (1.0 - s) * v0 + s * v1; +} + +inline color_point lerp(const color_point& v0, const color_point& v1, const double &t0, const double &t1, const double& t) { + double x = lerp(v0.x, v1.x, t0, t1, t); + double y = lerp(v0.y, v1.y, t0, t1, t); + double r = lerp(v0.r, v1.r, t0, t1, t); + double g = lerp(v0.g, v1.g, t0, t1, t); + double b = lerp(v0.b, v1.b, t0, t1, t); + return (color_point(x, y, r, g, b)); +} + +inline double clamp(const double& value, const double& min, const double& max) { + if (value < min) return min; + if (value > max) return max; + return value; +} + +// h, s, and v in range 0 to 1. Returns rgb value useful for use in Cairo. +guint32 hsv_to_rgb(double h, double s, double v) { + + if (h < 0.0 || h > 1.0 || + s < 0.0 || s > 1.0 || + v < 0.0 || v > 1.0) { + std::cerr << "ColorWheel: hsv_to_rgb: input out of bounds: (0-1)" + << " h: " << h << " s: " << s << " v: " << v << std::endl; + return 0x0; + } + + double r = v; + double g = v; + double b = v; + + if (s != 0.0) { + double c = s * v; + if (h == 1.0) h = 0.0; + h *= 6.0; + + double f = h - (int)h; + double p = v * (1.0 - s); + double q = v * (1.0 - s * f); + double t = v * (1.0 - s * (1.0 - f)); + + switch ((int)h) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + default: g_assert_not_reached(); + } + } + guint32 rgb = (((int)floor (r*255 + 0.5) << 16) | + ((int)floor (g*255 + 0.5) << 8) | + ((int)floor (b*255 + 0.5) )); + return rgb; +} + +double luminance(guint32 color) +{ + double r(((color & 0xff0000) >> 16)/255.0); + double g(((color & 0xff00) >> 8)/255.0); + double b(((color & 0xff) )/255.0); + return (r * 0.2125 + g * 0.7154 + b * 0.0721); +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +ColorWheel::ColorWheel() + : _hue(0.0) + , _saturation(1.0) + , _value(1.0) + , _ring_width(0.2) + , _mode(DRAG_NONE) + , _focus_on_ring(true) +{ + set_name("ColorWheel"); + add_events(Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::BUTTON_MOTION_MASK | + Gdk::KEY_PRESS_MASK ); + set_can_focus(); +} + +void +ColorWheel::set_rgb(const double& r, const double&g, const double&b, bool override_hue) +{ + double Min = std::min({r, g, b}); + double Max = std::max({r, g, b}); + _value = Max; + if (Min == Max) { + if (override_hue) { + _hue = 0.0; + } + } else { + if (Max == r) { + _hue = ((g-b)/(Max-Min) )/6.0; + } else if (Max == g) { + _hue = ((b-r)/(Max-Min) + 2)/6.0; + } else { + _hue = ((r-g)/(Max-Min) + 4)/6.0; + } + if (_hue < 0.0) { + _hue += 1.0; + } + } + if (Max == 0) { + _saturation = 0; + } else { + _saturation = (Max - Min)/Max; + } +} + +void +ColorWheel::get_rgb(double& r, double& g, double& b) +{ + guint32 color = get_rgb(); + r = ((color & 0xff0000) >> 16)/255.0; + g = ((color & 0x00ff00) >> 8)/255.0; + b = ((color & 0x0000ff) )/255.0; +} + +guint32 +ColorWheel::get_rgb() +{ + return hsv_to_rgb(_hue, _saturation, _value); +} + +/* Pad triangle vertically if necessary */ +void +draw_vertical_padding(color_point p0, color_point p1, int padding, bool pad_upwards, + guint32 *buffer, int height, int stride); + +bool +ColorWheel::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) { + Gtk::Allocation allocation = get_allocation(); + const int width = allocation.get_width(); + const int height = allocation.get_height(); + + const int stride = Cairo::ImageSurface::format_stride_for_width(Cairo::FORMAT_RGB24, width); + + int cx = width/2; + int cy = height/2; + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + // Paint ring + guint32* buffer_ring = g_new (guint32, height * stride / 4); + double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + double r2_max = (r_max+1) * (r_max+1); // Must expand a bit to avoid edge effects. + double r2_min = (r_min-1) * (r_min-1); // Must shrink a bit to avoid edge effects. + + for (int i = 0; i < height; ++i) { + guint32* p = buffer_ring + i * width; + double dy = (cy - i); + for (int j = 0; j < width; ++j) { + double dx = (j - cx); + double r2 = dx * dx + dy * dy; + if (r2 < r2_min || r2 > r2_max) { + *p++ = 0; // Save calculation time. + } else { + double angle = atan2 (dy, dx); + if (angle < 0.0) { + angle += 2.0 * M_PI; + } + double hue = angle/(2.0 * M_PI); + + *p++ = hsv_to_rgb(hue, 1.0, 1.0); + } + } + } + + Cairo::RefPtr<::Cairo::ImageSurface> source_ring = + ::Cairo::ImageSurface::create((unsigned char *)buffer_ring, + Cairo::FORMAT_RGB24, + width, height, stride); + + cr->set_antialias(Cairo::ANTIALIAS_SUBPIXEL); + + // Paint line on ring in source (so it gets clipped by stroke). + double l = 0.0; + guint32 color_on_ring = hsv_to_rgb(_hue, 1.0, 1.0); + if (luminance(color_on_ring) < 0.5) l = 1.0; + + Cairo::RefPtr<::Cairo::Context> cr_source_ring = ::Cairo::Context::create(source_ring); + cr_source_ring->set_source_rgb(l, l, l); + + cr_source_ring->move_to (cx, cy); + cr_source_ring->line_to (cx + cos(_hue * M_PI * 2.0) * r_max+1, + cy - sin(_hue * M_PI * 2.0) * r_max+1); + cr_source_ring->stroke(); + + // Paint with ring surface, clipping to ring. + cr->save(); + cr->set_source(source_ring, 0, 0); + cr->set_line_width (r_max - r_min); + cr->begin_new_path(); + cr->arc(cx, cy, (r_max + r_min)/2.0, 0, 2.0 * M_PI); + cr->stroke(); + cr->restore(); + + g_free(buffer_ring); + + // Draw focus + if (has_focus() && _focus_on_ring) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + style_context->render_focus(cr, 0, 0, width, height); + } + + // Paint triangle. + /* The triangle is painted by first finding color points on the + * edges of the triangle at the same y value via linearly + * interpolating between corner values, and then interpolating along + * x between the those edge points. The interpolation is in sRGB + * space which leads to a complicated mapping between x/y and + * saturation/value. This was probably done to remove the need to + * convert between HSV and RGB for each pixel. + * Black corner: v = 0, s = 1 + * White corner: v = 1, s = 0 + * Color corner; v = 1, s = 1 + */ + const int padding = 3; // Avoid edge artifacts. + double x0, y0, x1, y1, x2, y2; + triangle_corners(x0, y0, x1, y1, x2, y2); + guint32 color0 = hsv_to_rgb(_hue, 1.0, 1.0); + guint32 color1 = hsv_to_rgb(_hue, 1.0, 0.0); + guint32 color2 = hsv_to_rgb(_hue, 0.0, 1.0); + + color_point p0 (x0, y0, color0); + color_point p1 (x1, y1, color1); + color_point p2 (x2, y2, color2); + + // Reorder so we paint from top down. + if (p1.y > p2.y) { + std::swap(p1, p2); + } + + if (p0.y > p2.y) { + std::swap(p0, p2); + } + + if (p0.y > p1.y) { + std::swap(p0, p1); + } + + guint32* buffer_triangle = g_new (guint32, height * stride / 4); + + for (int y = 0; y < height; ++y) { + guint32 *p = buffer_triangle + y * (stride / 4); + + if (p0.y <= y+padding && y-padding < p2.y) { + + // Get values on side at position y. + color_point side0; + double y_inter = clamp(y, p0.y, p2.y); + if (y < p1.y) { + side0 = lerp(p0, p1, p0.y, p1.y, y_inter); + } else { + side0 = lerp(p1, p2, p1.y, p2.y, y_inter); + } + color_point side1 = lerp(p0, p2, p0.y, p2.y, y_inter); + + // side0 should be on left + if (side0.x > side1.x) { + std::swap (side0, side1); + } + + int x_start = std::max(0, int(side0.x)); + int x_end = std::min(int(side1.x), width); + + for (int x = 0; x < width; ++x) { + if (x <= x_start) { + *p++ = side0.get_color(); + } else if (x < x_end) { + *p++ = lerp(side0, side1, side0.x, side1.x, x).get_color(); + } else { + *p++ = side1.get_color(); + } + } + } + } + + // add vertical padding to each side separately + color_point temp_point = lerp(p0, p1, p0.x, p1.x, (p0.x + p1.x) / 2.0); + bool pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p0, p1, padding, pad_upwards, buffer_triangle, height, stride / 4); + + temp_point = lerp(p0, p2, p0.x, p2.x, (p0.x + p2.x) / 2.0); + pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p0, p2, padding, pad_upwards, buffer_triangle, height, stride / 4); + + temp_point = lerp(p1, p2, p1.x, p2.x, (p1.x + p2.x) / 2.0); + pad_upwards = is_in_triangle(temp_point.x, temp_point.y + 1); + draw_vertical_padding(p1, p2, padding, pad_upwards, buffer_triangle, height, stride / 4); + + Cairo::RefPtr<::Cairo::ImageSurface> source_triangle = + ::Cairo::ImageSurface::create((unsigned char *)buffer_triangle, + Cairo::FORMAT_RGB24, + width, height, stride); + + // Paint with triangle surface, clipping to triangle. + cr->save(); + cr->set_source(source_triangle, 0, 0); + cr->move_to(p0.x, p0.y); + cr->line_to(p1.x, p1.y); + cr->line_to(p2.x, p2.y); + cr->close_path(); + cr->fill(); + cr->restore(); + + g_free(buffer_triangle); + + // Draw marker + double mx = x1 + (x2-x1) * _value + (x0-x2) * _saturation * _value; + double my = y1 + (y2-y1) * _value + (y0-y2) * _saturation * _value; + + double a = 0.0; + guint32 color_at_marker = get_rgb(); + if (luminance(color_at_marker) < 0.5) a = 1.0; + + cr->set_source_rgb(a, a, a); + cr->begin_new_path(); + cr->arc(mx, my, 4, 0, 2 * M_PI); + cr->stroke(); + + // Draw focus + if (has_focus() && !_focus_on_ring) { + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + style_context->render_focus(cr, mx-4, my-4, 8, 8); // This doesn't seem to work. + cr->set_line_width(0.5); + cr->set_source_rgb(1-a, 1-a, 1-a); + cr->begin_new_path(); + cr->arc(mx, my, 7, 0, 2 * M_PI); + cr->stroke(); + } + + return true; +} + +void +draw_vertical_padding(color_point p0, color_point p1, int padding, bool pad_upwards, + guint32 *buffer, int height, int stride) +{ + // skip if horizontal padding is more accurate + double gradient = (p1.y - p0.y) / (p1.x - p0.x); + if (std::abs(gradient) > 1.0) { + return; + } + + double min_y = std::min(p0.y, p1.y); + double max_y = std::max(p0.y, p1.y); + + double min_x = std::min(p0.x, p1.x); + double max_x = std::max(p0.x, p1.x); + + for (int y = min_y; y <= max_y; ++y) { + double start_x = lerp(p0, p1, p0.y, p1.y, clamp(y, min_y, max_y)).x; + double end_x = lerp(p0, p1, p0.y, p1.y, clamp(y + 1, min_y, max_y)).x; + if (start_x > end_x) { + std::swap(start_x, end_x); + } + + guint32 *p = buffer + y * stride; + p += static_cast<int>(start_x); + for (int x = start_x; x <= end_x; ++x) { + color_point point = lerp(p0, p1, p0.x, p1.x, clamp(x, min_x, max_x)); + for (int offset = 0; offset <= padding; ++offset) { + if (pad_upwards && (point.y - offset) >= 0) { + *(p - (offset * stride)) = point.get_color(); + } else if (!pad_upwards && (point.y + offset) < height) { + *(p + (offset * stride)) = point.get_color(); + } + } + ++p; + } + } +} + +// Find triangle corners given hue and radius. +void +ColorWheel::triangle_corners(double &x0, double &y0, + double &x1, double &y1, + double &x2, double &y2) +{ + Gtk::Allocation allocation = get_allocation(); + const int width = allocation.get_width(); + const int height = allocation.get_height(); + + int cx = width/2; + int cy = height/2; + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + + double angle = _hue * 2.0 * M_PI; + + x0 = cx + cos(angle) * r_min; + y0 = cy - sin(angle) * r_min; + x1 = cx + cos(angle + 2.0 * M_PI / 3.0) * r_min; + y1 = cy - sin(angle + 2.0 * M_PI / 3.0) * r_min; + x2 = cx + cos(angle + 4.0 * M_PI / 3.0) * r_min; + y2 = cy - sin(angle + 4.0 * M_PI / 3.0) * r_min; +} + +void +ColorWheel::set_from_xy(const double& x, const double& y) +{ + Gtk::Allocation allocation = get_allocation(); + const int width = allocation.get_width(); + const int height = allocation.get_height(); + double cx = width/2.0; + double cy = height/2.0; + double r = std::min(cx, cy) * (1 - _ring_width); + + // We calculate RGB value under the cursor by rotating the cursor + // and triangle by the hue value and looking at position in the + // now right pointing triangle. + double angle = _hue * 2 * M_PI; + double Sin = sin(angle); + double Cos = cos(angle); + double xp = ((x-cx) * Cos - (y-cy) * Sin) / r; + double yp = ((x-cx) * Sin + (y-cy) * Cos) / r; + + double xt = lerp(0.0, 1.0, -0.5, 1.0, xp); + xt = clamp(xt, 0, 1); + + double dy = (1-xt) * cos(M_PI/6.0); + double yt = lerp(0.0, 1.0, -dy, dy, yp); + yt = clamp(yt, 0, 1); + + color_point c0(0, 0, yt, yt, yt); // Grey point along base. + color_point c1(0, 0, hsv_to_rgb(_hue, 1, 1)); // Hue point at apex + color_point c = lerp(c0, c1, 0, 1, xt); + + set_rgb(c.r, c.g, c.b, false); // Don't override previous hue. +} + +bool +ColorWheel::is_in_ring(const double& x, const double& y) +{ + Gtk::Allocation allocation = get_allocation(); + const int width = allocation.get_width(); + const int height = allocation.get_height(); + + int cx = width/2; + int cy = height/2; + + int focus_line_width; + int focus_padding; + get_style_property("focus-line-width", focus_line_width); + get_style_property("focus-padding", focus_padding); + + double r_max = std::min( width, height)/2.0 - 2 * (focus_line_width + focus_padding); + double r_min = r_max * (1.0 - _ring_width); + double r2_max = r_max * r_max; + double r2_min = r_min * r_min; + + double dx = x - cx; + double dy = y - cy; + double r2 = dx * dx + dy * dy; + + return (r2_min < r2 && r2 < r2_max); +} + +bool +ColorWheel::is_in_triangle(const double& x, const double& y) +{ + double x0, y0, x1, y1, x2, y2; + triangle_corners(x0, y0, x1, y1, x2, y2); + + double det = (x2-x1) * (y0-y1) - (y2-y1) * (x0-x1); + double s = ((x -x1) * (y0-y1) - (y -y1) * (x0-x1)) / det; + double t = ((x2-x1) * (y -y1) - (y2-y1) * (x -x1)) / det; + + return (s >= 0.0 && t >= 0.0 && s+t <= 1.0); +} + +bool +ColorWheel::on_focus(Gtk::DirectionType direction) +{ + // In forward direction, focus passes from no focus to ring focus to triangle focus to no focus. + if (!has_focus()) { + _focus_on_ring = (direction == Gtk::DIR_TAB_FORWARD); + grab_focus(); + return true; + } + + // Already have focus + bool keep_focus = false; + + switch (direction) { + case Gtk::DIR_UP: + case Gtk::DIR_LEFT: + case Gtk::DIR_TAB_BACKWARD: + if (!_focus_on_ring) { + _focus_on_ring = true; + keep_focus = true; + } + break; + + case Gtk::DIR_DOWN: + case Gtk::DIR_RIGHT: + case Gtk::DIR_TAB_FORWARD: + if (_focus_on_ring) { + _focus_on_ring = false; + keep_focus = true; + } + break; + } + + queue_draw(); // Update focus indicators. + + return keep_focus; +} + +bool +ColorWheel::on_button_press_event(GdkEventButton* event) +{ + // Seat is automatically grabbed. + double x = event->x; + double y = event->y; + + if (is_in_ring(x, y) ) { + _mode = DRAG_H; + grab_focus(); + _focus_on_ring = true; + return true; + } + + if (is_in_triangle(x, y)) { + _mode = DRAG_SV; + grab_focus(); + _focus_on_ring = false; + return true; + } + + return false; +} + +bool +ColorWheel::on_button_release_event(GdkEventButton* event) +{ + _mode = DRAG_NONE; + return true; +} + +bool +ColorWheel::on_motion_notify_event(GdkEventMotion* event) +{ + double x = event->x; + double y = event->y; + + Gtk::Allocation allocation = get_allocation(); + const int width = allocation.get_width(); + const int height = allocation.get_height(); + double cx = width/2.0; + double cy = height/2.0; + double r = std::min(cx, cy) * (1 - _ring_width); + + if (_mode == DRAG_H) { + + double angle = -atan2(y-cy, x-cx); + if (angle < 0) angle += 2.0 * M_PI; + _hue = angle / (2.0 * M_PI); + + queue_draw(); + _signal_color_changed.emit(); + return true; + } + + if (_mode == DRAG_SV) { + + set_from_xy(x, y); + _signal_color_changed.emit(); + queue_draw(); + return true; + } + + return false; +} + +bool +ColorWheel::on_key_press_event(GdkEventKey* key_event) +{ + bool consumed = false; + + unsigned int key = 0; + gdk_keymap_translate_keyboard_state( Gdk::Display::get_default()->get_keymap(), + key_event->hardware_keycode, + (GdkModifierType)key_event->state, + 0, &key, nullptr, nullptr, nullptr ); + + double x0, y0, x1, y1, x2, y2; + triangle_corners(x0, y0, x1, y1, x2, y2); + + // Marker position + double mx = x1 + (x2-x1) * _value + (x0-x2) * _saturation * _value; + double my = y1 + (y2-y1) * _value + (y0-y2) * _saturation * _value; + + + const double delta_hue = 2.0/360.0; + + switch (key) { + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + if (_focus_on_ring) { + _hue += delta_hue; + } else { + my -= 1.0; + set_from_xy(mx, my); + } + consumed = true; + break; + + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + if (_focus_on_ring) { + _hue -= delta_hue; + } else { + my += 1.0; + set_from_xy(mx, my); + } + consumed = true; + break; + + case GDK_KEY_Left: + case GDK_KEY_KP_Left: + if (_focus_on_ring) { + _hue += delta_hue; + } else { + mx -= 1.0; + set_from_xy(mx, my); + } + consumed = true; + break; + + case GDK_KEY_Right: + case GDK_KEY_KP_Right: + if (_focus_on_ring) { + _hue -= delta_hue; + } else { + mx += 1.0; + set_from_xy(mx, my); + } + consumed = true; + break; + + } + + if (consumed) { + if (_hue >= 1.0) _hue -= 1.0; + if (_hue < 0.0) _hue += 1.0; + _signal_color_changed.emit(); + queue_draw(); + } + + return consumed; +} + +sigc::signal<void> +ColorWheel::signal_color_changed() +{ + return _signal_color_changed; +} + +} // Namespace Inkscape +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-color-wheel.h b/src/ui/widget/ink-color-wheel.h new file mode 100644 index 0000000..c6333b7 --- /dev/null +++ b/src/ui/widget/ink-color-wheel.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Color wheel widget. Outer ring for Hue. Inner triangle for Saturation and Value. + * + * Copyright (C) 2018 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INK_COLORWHEEL_H +#define INK_COLORWHEEL_H + +/* Rewrite of the C Gimp ColorWheel which came originally from GTK2. */ + +#include <gtkmm.h> +#include <iostream> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class ColorWheel : public Gtk::DrawingArea +{ +public: + ColorWheel(); + void set_rgb(const double& r, const double& g, const double& b, bool override_hue = true); + void get_rgb(double& r, double& g, double& b); + guint32 get_rgb(); + bool is_adjusting() {return _mode != DRAG_NONE;} + +protected: + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + +private: + void triangle_corners(double& x0, double& y0, + double& x1, double& y1, + double& x2, double& y2); + void set_from_xy(const double& x, const double& y); + bool is_in_ring( const double& x, const double& y); + bool is_in_triangle(const double& x, const double& y); + + enum DragMode { + DRAG_NONE, + DRAG_H, + DRAG_SV + }; + + double _hue; // Range [0,1) + double _saturation; + double _value; + double _ring_width; + DragMode _mode; + bool _focus_on_ring; + + // Callbacks + bool on_focus(Gtk::DirectionType direction) override; + bool on_button_press_event(GdkEventButton* event) override; + bool on_button_release_event(GdkEventButton* event) override; + bool on_motion_notify_event(GdkEventMotion* event) override; + bool on_key_press_event(GdkEventKey* key_event) override; + + // Signals +public: + sigc::signal<void> signal_color_changed(); + +protected: + sigc::signal<void> _signal_color_changed; + +}; + +} // Namespace Inkscape +} +} +#endif // INK_COLOR_WHEEL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-flow-box.cpp b/src/ui/widget/ink-flow-box.cpp new file mode 100644 index 0000000..8485dd9 --- /dev/null +++ b/src/ui/widget/ink-flow-box.cpp @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkflow-box widget. + * This widget allow pack widgets in a flowbox with a controller to show-hide + * + * Author: + * Jabier Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2018 Jabier Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "preferences.h" +#include "ui/icon-loader.h" +#include "ui/widget/ink-flow-box.h" +#include <gtkmm/adjustment.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +InkFlowBox::InkFlowBox(const gchar *name) +{ + set_name(name); + this->pack_start(_controller, false, false, 0); + this->pack_start(_flowbox, true, true, 0); + _flowbox.set_activate_on_single_click(true); + Gtk::ToggleButton *tbutton = new Gtk::ToggleButton("", false); + tbutton->set_always_show_image(true); + _flowbox.set_selection_mode(Gtk::SelectionMode::SELECTION_NONE); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), false); + tbutton->set_active(prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true)); + Glib::ustring iconname = "object-unlocked"; + if (tbutton->get_active()) { + iconname = "object-locked"; + } + tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU)); + tbutton->signal_toggled().connect( + sigc::bind<Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_global_toggle), tbutton)); + _controller.pack_start(*tbutton); + tbutton->hide(); + tbutton->set_no_show_all(true); + showing = 0; + sensitive = true; +} + +InkFlowBox::~InkFlowBox() = default; + +Glib::ustring InkFlowBox::getPrefsPath(gint pos) +{ + return Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/index_") + std::to_string(pos); +} + +bool InkFlowBox::on_filter(Gtk::FlowBoxChild *child) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool(getPrefsPath(child->get_index()), true)) { + showing++; + return true; + } + return false; +} + +void InkFlowBox::on_toggle(gint pos, Gtk::ToggleButton *tbutton) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool global = prefs->getBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), true); + if (global && sensitive) { + sensitive = false; + bool active = true; + for (auto child : tbutton->get_parent()->get_children()) { + if (tbutton != child) { + dynamic_cast<Gtk::ToggleButton *>(child)->set_active(active); + active = false; + } + } + prefs->setBool(getPrefsPath(pos), true); + tbutton->set_active(true); + sensitive = true; + } else { + prefs->setBool(getPrefsPath(pos), tbutton->get_active()); + } + showing = 0; + _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter)); + _flowbox.set_max_children_per_line(showing); +} + +void InkFlowBox::on_global_toggle(Gtk::ToggleButton *tbutton) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(Glib::ustring("/dialogs/") + get_name() + Glib::ustring("/flowbox/lock"), tbutton->get_active()); + sensitive = true; + if (tbutton->get_active()) { + sensitive = false; + bool active = true; + for (auto child : tbutton->get_parent()->get_children()) { + if (tbutton != child) { + dynamic_cast<Gtk::ToggleButton *>(child)->set_active(active); + active = false; + } + } + } + Glib::ustring iconname = "object-unlocked"; + if (tbutton->get_active()) { + iconname = "object-locked"; + } + tbutton->set_image(*sp_get_icon_image(iconname, Gtk::ICON_SIZE_MENU)); + sensitive = true; +} + +void InkFlowBox::insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::ToggleButton *tbutton = new Gtk::ToggleButton(label, true); + tbutton->set_active(prefs->getBool(getPrefsPath(pos), active)); + tbutton->signal_toggled().connect( + sigc::bind<gint, Gtk::ToggleButton *>(sigc::mem_fun(*this, &InkFlowBox::on_toggle), pos, tbutton)); + _controller.pack_start(*tbutton); + tbutton->show(); + prefs->setBool(getPrefsPath(pos), prefs->getBool(getPrefsPath(pos), active)); + widget->set_size_request(minwidth, -1); + _flowbox.insert(*widget, pos); + showing = 0; + _flowbox.set_filter_func(sigc::mem_fun(*this, &InkFlowBox::on_filter)); + _flowbox.set_max_children_per_line(showing); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-flow-box.h b/src/ui/widget/ink-flow-box.h new file mode 100644 index 0000000..be84ee9 --- /dev/null +++ b/src/ui/widget/ink-flow-box.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkflow-box widget. + * This widget allow pack widgets in a flowbox with a controller to show-hide + * + * Author: + * Jabier Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2018 Jabier Arraiza + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_INK_FLOW_BOX_H +#define INKSCAPE_INK_FLOW_BOX_H + +#include <gtkmm/actionbar.h> +#include <gtkmm/box.h> +#include <gtkmm/flowbox.h> +#include <gtkmm/flowboxchild.h> +#include <gtkmm/togglebutton.h> +#include <sigc++/signal.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A flowbox widget with filter controller for dialogs. + */ + +class InkFlowBox : public Gtk::VBox { + public: + InkFlowBox(const gchar *name); + ~InkFlowBox() override; + void insert(Gtk::Widget *widget, Glib::ustring label, gint pos, bool active, int minwidth); + void on_toggle(gint pos, Gtk::ToggleButton *tbutton); + void on_global_toggle(Gtk::ToggleButton *tbutton); + void set_visible(gint pos, bool visible); + bool on_filter(Gtk::FlowBoxChild *child); + Glib::ustring getPrefsPath(gint pos); + /** + * Construct a InkFlowBox. + */ + + private: + Gtk::FlowBox _flowbox; + Gtk::ActionBar _controller; + gint showing; + bool sensitive; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_INK_FLOW_BOX_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-ruler.cpp b/src/ui/widget/ink-ruler.cpp new file mode 100644 index 0000000..3bb117a --- /dev/null +++ b/src/ui/widget/ink-ruler.cpp @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget. + * + * Copyright (C) 2019 Tavmjong Bah + * + * Rewrite of the 'C' ruler code which came originally from Gimp. + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#include "ink-ruler.h" + +#include <iostream> +#include <cmath> + +#include "util/units.h" + +struct SPRulerMetric +{ + gdouble ruler_scale[16]; + gint subdivide[5]; +}; + +// Ruler metric for general use. +static SPRulerMetric const ruler_metric_general = { + { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000 }, + { 1, 5, 10, 50, 100 } +}; + +// Ruler metric for inch scales. +static SPRulerMetric const ruler_metric_inches = { + { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 }, + { 1, 2, 4, 8, 16 } +}; + +// Half width of pointer triangle. +static double half_width = 5.0; + +namespace Inkscape { +namespace UI { +namespace Widget { + +Ruler::Ruler(Gtk::Orientation orientation) + : _orientation(orientation) + , _backing_store(nullptr) + , _lower(0) + , _upper(1000) + , _max_size(1000) + , _unit(nullptr) + , _backing_store_valid(false) + , _rect() + , _position(0) +{ + set_name("InkRuler"); + + set_events(Gdk::POINTER_MOTION_MASK); + + signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback)); +} + +// Set display unit for ruler. +void +Ruler::set_unit(Inkscape::Util::Unit const *unit) +{ + if (_unit != unit) { + _unit = unit; + + _backing_store_valid = false; + queue_draw(); + } +} + +// Set range for ruler, update ticks. +void +Ruler::set_range(const double& lower, const double& upper) +{ + if (_lower != lower || _upper != upper) { + + _lower = lower; + _upper = upper; + _max_size = _upper - _lower; + if (_max_size == 0) { + _max_size = 1; + } + + _backing_store_valid = false; + queue_draw(); + } +} + +// Add a widget (i.e. canvas) to monitor. Note, we don't worry about removing this signal as +// our ruler is tied tightly to the canvas, if one is destroyed, so is the other. +void +Ruler::add_track_widget(Gtk::Widget& widget) +{ + widget.signal_motion_notify_event().connect(sigc::mem_fun(*this, &Ruler::draw_marker_callback), false); // false => connect first +} + + +// Draws marker in response to motion events from canvas. Position is defined in ruler pixel +// coordinates. The routine assumes that the ruler is the same width (height) as the canvas. If +// not, one could use Gtk::Widget::translate_coordinates() to convert the coordinates. +bool +Ruler::draw_marker_callback(GdkEventMotion* motion_event) +{ + double position = 0; + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + position = motion_event->x; + } else { + position = motion_event->y; + } + + if (position != _position) { + + _position = position; + + // Find region to repaint (old and new marker positions). + Cairo::RectangleInt new_rect = marker_rect(); + Cairo::RefPtr<Cairo::Region> region = Cairo::Region::create(new_rect); + region->do_union(_rect); + + // Queue repaint + queue_draw_region(region); + + _rect = new_rect; + } + + return false; +} + + +// Find smallest dimension of ruler based on font size. +void +Ruler::size_request (Gtk::Requisition& requisition) const +{ + // Get border size + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + + // Get font size + Pango::FontDescription font = style_context->get_font(get_state_flags()); + int font_size = font.get_size(); + if (!font.get_size_is_absolute()) { + font_size /= Pango::SCALE; + } + + int size = 2 + font_size * 2.0; // Room for labels and ticks + + int width = border.get_left() + border.get_right(); + int height = border.get_top() + border.get_bottom(); + + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + width += 1; + height += size; + } else { + width += size; + height += 1; + } + + // Only valid for orientation in question (smallest dimension)! + requisition.width = width; + requisition.height = height; +} + +void +Ruler::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const +{ + Gtk::Requisition requisition; + size_request(requisition); + minimum_width = natural_width = requisition.width; +} + +void +Ruler::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const +{ + Gtk::Requisition requisition; + size_request(requisition); + minimum_height = natural_height = requisition.height; +} + +// Update backing store when scale changes. +// Note: in principle, there should not be a border (ruler ends should match canvas ends). If there +// is a border, we calculate tick position ignoring border width at ends of ruler but move the +// ticks and position marker inside the border. +bool +Ruler::draw_scale(const::Cairo::RefPtr<::Cairo::Context>& cr_in) +{ + + // Get style information + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + Gdk::RGBA foreground = style_context->get_color(get_state_flags()); + + Pango::FontDescription font = style_context->get_font(get_state_flags()); + int font_size = font.get_size(); + if (!font.get_size_is_absolute()) { + font_size /= Pango::SCALE; + } + + Gtk::Allocation allocation = get_allocation(); + int awidth = allocation.get_width(); + int aheight = allocation.get_height(); + + // if (allocation.get_x() != 0 || allocation.get_y() != 0) { + // std::cerr << "Ruler::draw_scale: maybe we do have to handle allocation x and y! " + // << " x: " << allocation.get_x() << " y: " << allocation.get_y() << std::endl; + // } + + // Create backing store (need surface_in to get scale factor correct). + Cairo::RefPtr<Cairo::Surface> surface_in = cr_in->get_target(); + _backing_store = Cairo::Surface::create(surface_in, Cairo::CONTENT_COLOR_ALPHA, awidth, aheight); + + // Get context + Cairo::RefPtr<::Cairo::Context> cr = ::Cairo::Context::create(_backing_store); + style_context->render_background(cr, 0, 0, awidth, aheight); + + cr->set_line_width(1.0); + Gdk::Cairo::set_source_rgba(cr, foreground); + + // Ruler size (only smallest dimension used later). + int rwidth = awidth - (border.get_left() + border.get_right()); + int rheight = aheight - (border.get_top() + border.get_bottom()); + + // Draw bottom/right line of ruler + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + cr->rectangle( 0, aheight - border.get_bottom() - 1, awidth, 1); + } else { + cr->rectangle( awidth - border.get_left() - 1, 0, 1, aheight); + std::swap(awidth, aheight); + std::swap(rwidth, rheight); + } + cr->fill(); + + // From here on, awidth is the longest dimension of the ruler, rheight is the shortest. + + // Figure out scale. Largest ticks must be far enough apart to fit largest text in vertical ruler. + // We actually require twice the distance. + unsigned int scale = std::ceil (_max_size); // Largest number + Glib::ustring scale_text = std::to_string(scale); + unsigned int digits = scale_text.length() + 1; // Add one for negative sign. + unsigned int minimum = digits * font_size * 2; + + double pixels_per_unit = awidth/_max_size; // pixel per distance + + SPRulerMetric ruler_metric = ruler_metric_general; + if (_unit == Inkscape::Util::unit_table.getUnit("in")) { + ruler_metric = ruler_metric_inches; + } + + unsigned scale_index; + for (scale_index = 0; scale_index < G_N_ELEMENTS (ruler_metric.ruler_scale)-1; ++scale_index) { + if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) > minimum) break; + } + + // Now we find out what is the subdivide index for the closest ticks we can draw + unsigned divide_index; + for (divide_index = 0; divide_index < G_N_ELEMENTS (ruler_metric.subdivide)-1; ++divide_index) { + if (ruler_metric.ruler_scale[scale_index] * std::abs (pixels_per_unit) < 5 * ruler_metric.subdivide[divide_index+1]) break; + } + + // We'll loop over all ticks. + double pixels_per_tick = pixels_per_unit * + ruler_metric.ruler_scale[scale_index] / ruler_metric.subdivide[divide_index]; + + double units_per_tick = pixels_per_tick/pixels_per_unit; + double ticks_per_unit = 1.0/units_per_tick; + + // Find first and last ticks + int start = 0; + int end = 0; + if (_lower < _upper) { + start = std::floor (_lower * ticks_per_unit); + end = std::ceil (_upper * ticks_per_unit); + } else { + start = std::floor (_upper * ticks_per_unit); + end = std::ceil (_lower * ticks_per_unit); + } + + // std::cout << " start: " << start + // << " end: " << end + // << " pixels_per_unit: " << pixels_per_unit + // << " pixels_per_tick: " << pixels_per_tick + // << std::endl; + + // Loop over all ticks + for (int i = start; i < end+1; ++i) { + + // Position of tick (add 0.5 to center tick on pixel). + double position = std::floor(i*pixels_per_tick - _lower*pixels_per_unit) + 0.5; + + // Height of tick + int height = rheight; + for (int j = divide_index; j > 0; --j) { + if (i%ruler_metric.subdivide[j] == 0) break; + height = height/2 + 1; + } + + // Draw text for major ticks. + if (i%ruler_metric.subdivide[divide_index] == 0) { + + int label_value = std::round(i*units_per_tick); + Glib::ustring label = std::to_string(label_value); + + Glib::RefPtr<Pango::Layout> layout = create_pango_layout(""); + layout->set_font_description(font); + + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + layout->set_text(label); + cr->move_to (position+2, border.get_top()); + layout->show_in_cairo_context(cr); + } else { + cr->move_to (border.get_left(), position); + int n = 0; + for (char const &c : label) { + std::string s(1, c); + layout->set_text(s); + int text_width; + int text_height; + layout->get_pixel_size(text_width, text_height); + cr->move_to(border.get_left() + (aheight-text_width)/2.0 - 1, + position + n*0.7*text_height - 1); + layout->show_in_cairo_context(cr); + ++n; + } + // Glyphs are not centered in vertical text... should specify fixed width numbers. + // Glib::RefPtr<Pango::Context> context = layout->get_context(); + // if (_orientation == Gtk::ORIENTATION_VERTICAL) { + // context->set_base_gravity(Pango::GRAVITY_EAST); + // context->set_gravity_hint(Pango::GRAVITY_HINT_STRONG); + // cr->move_to(...) + // cr->save(); + // cr->rotate(M_PI_2); + // layout->show_in_cairo_context(cr); + // cr->restore(); + // } + } + } + + // Draw ticks + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + cr->move_to(position, rheight + border.get_top() - height); + cr->line_to(position, rheight + border.get_top()); + } else { + cr->move_to(rheight + border.get_left() - height, position); + cr->line_to(rheight + border.get_left(), position); + } + cr->stroke(); + } + + _backing_store_valid = true; + + return true; +} + +// Draw position marker, we use doubles here. +void +Ruler::draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr) +{ + + // Get style information + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + Gdk::RGBA foreground = style_context->get_color(get_state_flags()); + + Gtk::Allocation allocation = get_allocation(); + const int awidth = allocation.get_width(); + const int aheight = allocation.get_height(); + + // Temp (to verify our redraw rectangle encloses position marker). + // Cairo::RectangleInt rect = marker_rect(); + // cr->set_source_rgb(0, 1.0, 0); + // cr->rectangle (rect.x, rect.y, rect.width, rect.height); + // cr->fill(); + + Gdk::Cairo::set_source_rgba(cr, foreground); + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + double offset = aheight - border.get_bottom(); + cr->move_to(_position, offset); + cr->line_to(_position - half_width, offset - half_width); + cr->line_to(_position + half_width, offset - half_width); + cr->close_path(); + } else { + double offset = awidth - border.get_right(); + cr->move_to(offset, _position); + cr->line_to(offset - half_width, _position - half_width); + cr->line_to(offset - half_width, _position + half_width); + cr->close_path(); + } + cr->fill(); +} + +// This is a pixel aligned integer rectangle that encloses the position marker. Used to define the +// redraw area. +Cairo::RectangleInt +Ruler::marker_rect() +{ + // Get border size + Glib::RefPtr<Gtk::StyleContext> style_context = get_style_context(); + Gtk::Border border = style_context->get_border(get_state_flags()); + + Gtk::Allocation allocation = get_allocation(); + const int awidth = allocation.get_width(); + const int aheight = allocation.get_height(); + + int rwidth = awidth - border.get_left() - border.get_right(); + int rheight = aheight - border.get_top() - border.get_bottom(); + + Cairo::RectangleInt rect; + rect.x = 0; + rect.y = 0; + rect.width = 0; + rect.height = 0; + + // Find size of rectangle to enclose triangle. + if (_orientation == Gtk::ORIENTATION_HORIZONTAL) { + rect.x = std::floor(_position - half_width); + rect.y = std::floor(border.get_top() + rheight - half_width); + rect.width = std::ceil(half_width * 2.0 + 1); + rect.height = std::ceil(half_width); + } else { + rect.x = std::floor(border.get_left() + rwidth - half_width); + rect.y = std::floor(_position - half_width); + rect.width = std::ceil(half_width); + rect.height = std::ceil(half_width * 2.0 + 1); + } + + return rect; +} + +// Draw the ruler using the tick backing store. +bool +Ruler::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) { + + if (!_backing_store_valid) { + draw_scale (cr); + } + + cr->set_source (_backing_store, 0, 0); + cr->paint(); + + draw_marker (cr); + + return true; +} + +// Update ruler on style change (font-size, etc.) +void +Ruler::on_style_updated() { + + Gtk::DrawingArea::on_style_updated(); + + _backing_store_valid = false; // If font-size changed we need to regenerate store. + + queue_resize(); + queue_draw(); +} + +} // Namespace Inkscape +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-ruler.h b/src/ui/widget/ink-ruler.h new file mode 100644 index 0000000..0b9f9af --- /dev/null +++ b/src/ui/widget/ink-ruler.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Ruler widget. Indicates horizontal or vertical position of a cursor in a specified widget. + * + * Copyright (C) 2019 Tavmjong Bah + * + * The contents of this file may be used under the GNU General Public License Version 2 or later. + * + */ + +#ifndef INK_RULER_H +#define INK_RULER_H + +/* Rewrite of the C Ruler. */ + +#include <gtkmm.h> + +namespace Inkscape { +namespace Util { +class Unit; +} +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Ruler : public Gtk::DrawingArea +{ +public: + Ruler(Gtk::Orientation orientation); + + void set_unit(Inkscape::Util::Unit const *unit); + void set_range(const double& lower, const double& upper); + + void add_track_widget(Gtk::Widget& widget); + bool draw_marker_callback(GdkEventMotion* motion_event); + + void size_request(Gtk::Requisition& requisition) const; + void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override; + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + +protected: + bool draw_scale(const Cairo::RefPtr<::Cairo::Context>& cr); + void draw_marker(const Cairo::RefPtr<::Cairo::Context>& cr); + Cairo::RectangleInt marker_rect(); + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + void on_style_updated() override; + +private: + Gtk::Orientation _orientation; + + Inkscape::Util::Unit const* _unit; + double _lower; + double _upper; + double _position; + double _max_size; + double _font_scale; + + bool _backing_store_valid; + + Cairo::RefPtr<::Cairo::Surface> _backing_store; + Cairo::RectangleInt _rect; +}; + +} // Namespace Inkscape +} +} +#endif // INK_RULER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/ink-spinscale.cpp b/src/ui/widget/ink-spinscale.cpp new file mode 100644 index 0000000..7c546c2 --- /dev/null +++ b/src/ui/widget/ink-spinscale.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** \file + A widget that allows entering a numerical value either by + clicking/dragging on a custom Gtk::Scale or by using a + Gtk::SpinButton. The custom Gtk::Scale differs from the stock + Gtk::Scale in that it includes a label to save space and has a + "slow dragging" mode triggered by the Alt key. +*/ + +#include "ink-spinscale.h" +#include <gdkmm/general.h> +#include <gdkmm/cursor.h> +#include <gdkmm/event.h> + +#include <gtkmm/spinbutton.h> + +#include <gdk/gdk.h> + +#include <iostream> +#include <utility> + +InkScale::InkScale(Glib::RefPtr<Gtk::Adjustment> adjustment, Gtk::SpinButton* spinbutton) + : Glib::ObjectBase("InkScale") + , Gtk::Scale(adjustment) + , _spinbutton(spinbutton) + , _dragging(false) + , _drag_start(0) + , _drag_offset(0) +{ + set_name("InkScale"); + // std::cout << "GType name: " << G_OBJECT_TYPE_NAME(gobj()) << std::endl; +} + +void +InkScale::set_label(Glib::ustring label) { + _label = label; +} + +bool +InkScale::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) { + + Gtk::Range::on_draw(cr); + + // Get SpinButton style info... + auto style_spin = _spinbutton->get_style_context(); + auto state_spin = style_spin->get_state(); + Gdk::RGBA text_color = style_spin->get_color( state_spin ); + + // Create Pango layout. + auto layout_label = create_pango_layout(_label); + layout_label->set_ellipsize( Pango::ELLIPSIZE_END ); + layout_label->set_width(PANGO_SCALE * get_width()); + + // Get y location of SpinButton text (to match vertical position of SpinButton text). + int x, y; + _spinbutton->get_layout_offsets(x, y); + + // Fill widget proportional to value. + double fraction = get_fraction(); + + // Get trough rectangle and clipping point for text. + Gdk::Rectangle slider_area = get_range_rect(); + double clip_text_x = slider_area.get_x() + slider_area.get_width() * fraction; + + // Render text in normal text color. + cr->save(); + cr->rectangle(clip_text_x, 0, get_width(), get_height()); + cr->clip(); + Gdk::Cairo::set_source_rgba(cr, text_color); + //cr->set_source_rgba(0, 0, 0, 1); + cr->move_to(5, y ); + layout_label->show_in_cairo_context(cr); + cr->restore(); + + // Render text, clipped, in white over bar (TODO: use same color as SpinButton progress bar). + cr->save(); + cr->rectangle(0, 0, clip_text_x, get_height()); + cr->clip(); + cr->set_source_rgba(1, 1, 1, 1); + cr->move_to(5, y); + layout_label->show_in_cairo_context(cr); + cr->restore(); + + return true; +} + +bool +InkScale::on_button_press_event(GdkEventButton* button_event) { + + if (! (button_event->state & GDK_MOD1_MASK) ) { + bool constrained = button_event->state & GDK_CONTROL_MASK; + set_adjustment_value(button_event->x, constrained); + } + + // Dragging must be initialized after any adjustment due to button press. + _dragging = true; + _drag_start = button_event->x; + _drag_offset = get_width() * get_fraction(); + + return true; +} + +bool +InkScale::on_button_release_event(GdkEventButton* button_event) { + + _dragging = false; + return true; +} + +bool +InkScale::on_motion_notify_event(GdkEventMotion* motion_event) { + + double x = motion_event->x; + double y = motion_event->y; + + if (_dragging) { + + if (! (motion_event->state & GDK_MOD1_MASK) ) { + // Absolute change + bool constrained = motion_event->state & GDK_CONTROL_MASK; + set_adjustment_value(x, constrained); + } else { + // Relative change + double xx = (_drag_offset + (x - _drag_start) * 0.1); + set_adjustment_value(xx); + } + return true; + } + + if (! (motion_event->state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK))) { + + auto display = get_display(); + auto cursor = Gdk::Cursor::create(display, Gdk::SB_UP_ARROW); + // Get Gdk::window (not Gtk::window).. set cursor for entire window. + // Would need to unset with leave event. + // get_window()->set_cursor( cursor ); + + // Can't see how to do this the C++ way since GdkEventMotion + // is a structure with a C window member. There is a gdkmm + // wrapping function for Gdk::EventMotion but only in unstable. + + // If the cursor theme doesn't have the `sb_up_arrow` cursor then the pointer will be NULL + if (cursor) + gdk_window_set_cursor( motion_event->window, cursor->gobj() ); + } + + return false; +} + +double +InkScale::get_fraction() { + + Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment(); + double upper = adjustment->get_upper(); + double lower = adjustment->get_lower(); + double value = adjustment->get_value(); + double fraction = (value - lower)/(upper - lower); + + return fraction; +} + +void +InkScale::set_adjustment_value(double x, bool constrained) { + + Glib::RefPtr<Gtk::Adjustment> adjustment = get_adjustment(); + double upper = adjustment->get_upper(); + double lower = adjustment->get_lower(); + double range = upper-lower; + + Gdk::Rectangle slider_area = get_range_rect(); + double fraction = (x - slider_area.get_x()) / (double)slider_area.get_width(); + double value = fraction * range + lower; + + if (constrained) { + // TODO: do we want preferences for (any of) these? + if (fmod(range+1,16) == 0) { + value = round(value/16) * 16; + } else if (range >= 1000 && fmod(upper,100) == 0) { + value = round(value/100) * 100; + } else if (range >= 100 && fmod(upper,10) == 0) { + value = round(value/10) * 10; + } else if (range > 20 && fmod(upper,5) == 0) { + value = round(value/5) * 5; + } else if (range > 2) { + value = round(value); + } else if (range <= 2) { + value = round(value*10) / 10; + } + } + + adjustment->set_value( value ); +} + +/*******************************************************************/ + +InkSpinScale::InkSpinScale(double value, double lower, + double upper, double step_increment, + double page_increment, double page_size) +{ + set_name("InkSpinScale"); + + g_assert (upper - lower > 0); + + _adjustment = Gtk::Adjustment::create(value, + lower, + upper, + step_increment, + page_increment, + page_size); + + _spinbutton = Gtk::manage(new Gtk::SpinButton(_adjustment)); + _spinbutton->set_numeric(); + _spinbutton->signal_key_release_event().connect(sigc::mem_fun(*this,&InkSpinScale::on_key_release_event),false); + + _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton)); + _scale->set_draw_value(false); + + pack_end( *_spinbutton, Gtk::PACK_SHRINK ); + pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET ); +} + +InkSpinScale::InkSpinScale(Glib::RefPtr<Gtk::Adjustment> adjustment) + : _adjustment(std::move(adjustment)) +{ + set_name("InkSpinScale"); + + g_assert (_adjustment->get_upper() - _adjustment->get_lower() > 0); + + _spinbutton = Gtk::manage(new Gtk::SpinButton(_adjustment)); + _spinbutton->set_numeric(); + + _scale = Gtk::manage(new InkScale(_adjustment, _spinbutton)); + _scale->set_draw_value(false); + + pack_end( *_spinbutton, Gtk::PACK_SHRINK ); + pack_end( *_scale, Gtk::PACK_EXPAND_WIDGET ); +} + +void +InkSpinScale::set_label(Glib::ustring label) { + _scale->set_label(label); +} + +void +InkSpinScale::set_digits(int digits) { + _spinbutton->set_digits(digits); +} + +int +InkSpinScale::get_digits() const { + return _spinbutton->get_digits(); +} + +void +InkSpinScale::set_focus_widget(GtkWidget * focus_widget) { + _focus_widget = focus_widget; +} + +// Return focus to canvas. +bool +InkSpinScale::on_key_release_event(GdkEventKey* key_event) { + + switch (key_event->keyval) { + case GDK_KEY_Escape: + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + if (_focus_widget) { + gtk_widget_grab_focus( _focus_widget ); + } + } + break; + } + + return false; +} diff --git a/src/ui/widget/ink-spinscale.h b/src/ui/widget/ink-spinscale.h new file mode 100644 index 0000000..ada7efd --- /dev/null +++ b/src/ui/widget/ink-spinscale.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_SPINSCALE_H +#define INK_SPINSCALE_H + +/* + * Authors: + * Tavmjong Bah <tavmjong@free.fr> + * + * Copyright (C) 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/** + A widget that allows entering a numerical value either by + clicking/dragging on a custom Gtk::Scale or by using a + Gtk::SpinButton. The custom Gtk::Scale differs from the stock + Gtk::Scale in that it includes a label to save space and has a + "slow-dragging" mode triggered by the Alt key. +*/ + +#include <glibmm/ustring.h> + +#include <gtkmm/box.h> +#include <gtkmm/scale.h> + +namespace Gtk { + class SpinButton; +} + +class InkScale : public Gtk::Scale +{ + public: + InkScale(Glib::RefPtr<Gtk::Adjustment>, Gtk::SpinButton* spinbutton); + ~InkScale() override = default;; + + void set_label(Glib::ustring label); + + bool on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) override; + + protected: + + bool on_button_press_event(GdkEventButton* button_event) override; + bool on_button_release_event(GdkEventButton* button_event) override; + bool on_motion_notify_event(GdkEventMotion* motion_event) override; + + private: + + double get_fraction(); + void set_adjustment_value(double x, bool constrained = false); + + Gtk::SpinButton * _spinbutton; // Needed to get placement/text color. + Glib::ustring _label; + + bool _dragging; + double _drag_start; + double _drag_offset; +}; + +class InkSpinScale : public Gtk::Box +{ + public: + + // Create an InkSpinScale with a new adjustment. + InkSpinScale(double value, + double lower, + double upper, + double step_increment = 1, + double page_increment = 10, + double page_size = 0); + + // Create an InkSpinScale with a preexisting adjustment. + InkSpinScale(Glib::RefPtr<Gtk::Adjustment>); + + ~InkSpinScale() override = default;; + + void set_label(Glib::ustring label); + void set_digits(int digits); + int get_digits() const; + void set_focus_widget(GtkWidget *focus_widget); + Glib::RefPtr<Gtk::Adjustment> get_adjustment() { return _adjustment; }; + + protected: + + InkScale* _scale; + Gtk::SpinButton* _spinbutton; + Glib::RefPtr<Gtk::Adjustment> _adjustment; + GtkWidget* _focus_widget = nullptr; + + bool on_key_release_event(GdkEventKey* key_event) override; + + private: + +}; + +#endif // INK_SPINSCALE_H diff --git a/src/ui/widget/insertordericon.cpp b/src/ui/widget/insertordericon.cpp new file mode 100644 index 0000000..0a21f3c --- /dev/null +++ b/src/ui/widget/insertordericon.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/insertordericon.h" + +#include "layertypeicon.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "widgets/toolbox.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +InsertOrderIcon::InsertOrderIcon() : + Glib::ObjectBase(typeid(InsertOrderIcon)), + Gtk::CellRendererPixbuf(), + _pixTopName(INKSCAPE_ICON("insert-top")), + _pixBottomName(INKSCAPE_ICON("insert-bottom")), + _property_active(*this, "active", 0), + _property_pixbuf_top(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_bottom(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) +{ + + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + + _property_pixbuf_top = sp_get_icon_pixbuf(_pixTopName, GTK_ICON_SIZE_MENU); + _property_pixbuf_bottom = sp_get_icon_pixbuf(_pixBottomName, GTK_ICON_SIZE_MENU); + + property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr); +} + + +void InsertOrderIcon::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void InsertOrderIcon::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void InsertOrderIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + switch (_property_active.get_value()) + { + case 1: + property_pixbuf() = _property_pixbuf_top; + break; + case 2: + property_pixbuf() = _property_pixbuf_bottom; + break; + default: + property_pixbuf() = Glib::RefPtr<Gdk::Pixbuf>(nullptr); + break; + } + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool InsertOrderIcon::activate_vfunc(GdkEvent* /*event*/, + Gtk::Widget& /*widget*/, + const Glib::ustring& /*path*/, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + return false; +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/widget/insertordericon.h b/src/ui/widget/insertordericon.h new file mode 100644 index 0000000..7ca1cef --- /dev/null +++ b/src/ui/widget/insertordericon.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_INSERTORDERICON_H__ +#define __UI_DIALOG_INSERTORDERICON_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> + +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class InsertOrderIcon : public Gtk::CellRendererPixbuf { +public: + InsertOrderIcon(); + ~InsertOrderIcon() override = default;; + + Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + +protected: + + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + +private: + int phys; + + Glib::ustring _pixTopName; + Glib::ustring _pixBottomName; + + Glib::Property<int> _property_active; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_top; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_bottom; + +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/label-tool-item.cpp b/src/ui/widget/label-tool-item.cpp new file mode 100644 index 0000000..979cfa2 --- /dev/null +++ b/src/ui/widget/label-tool-item.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A label that can be added to a toolbar + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "label-tool-item.h" + +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief Create a tool-item containing a label + * + * \param[in] label The text to display in the label + * \param[in] mnemonic True if text should use a mnemonic + */ +LabelToolItem::LabelToolItem(const Glib::ustring& label, bool mnemonic) + : _label(Gtk::manage(new Gtk::Label(label, mnemonic))) +{ + add(*_label); + show_all(); +} + +/** + * \brief Set the markup text in the label + * + * \param[in] str The markup text + */ +void +LabelToolItem::set_markup(const Glib::ustring& str) +{ + _label->set_markup(str); +} + +/** + * \brief Sets whether label uses Pango markup + * + * \param[in] setting true if the label text should be parsed for markup + */ +void +LabelToolItem::set_use_markup(bool setting) +{ + _label->set_use_markup(setting); +} + +} +} +} +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/label-tool-item.h b/src/ui/widget/label-tool-item.h new file mode 100644 index 0000000..1fe6892 --- /dev/null +++ b/src/ui/widget/label-tool-item.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * A label that can be added to a toolbar + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_LABEL_TOOL_ITEM_H +#define SEEN_LABEL_TOOL_ITEM_H + +#include <gtkmm/toolitem.h> + +namespace Gtk { +class Label; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief A label that can be added to a toolbar + */ +class LabelToolItem : public Gtk::ToolItem { +private: + Gtk::Label *_label; + +public: + LabelToolItem(const Glib::ustring& label, bool mnemonic = false); + + void set_markup(const Glib::ustring& str); + void set_use_markup(bool setting = true); +}; +} +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/labelled.cpp b/src/ui/widget/labelled.cpp new file mode 100644 index 0000000..b320b70 --- /dev/null +++ b/src/ui/widget/labelled.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "labelled.h" +#include "ui/icon-loader.h" +#include <gtkmm/image.h> +#include <gtkmm/label.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Labelled::Labelled(Glib::ustring const &label, Glib::ustring const &tooltip, + Gtk::Widget *widget, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : _widget(widget), + _label(new Gtk::Label(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, mnemonic)), + _suffix(new Gtk::Label(suffix, Gtk::ALIGN_START)) +{ + g_assert(g_utf8_validate(icon.c_str(), -1, nullptr)); + if (icon != "") { + _icon = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR)); + pack_start(*_icon, Gtk::PACK_SHRINK); + } + + set_spacing(6); + // Setting margins separately allows for more control over them + set_margin_start(6); + set_margin_end(6); + pack_start(*Gtk::manage(_label), Gtk::PACK_SHRINK); + pack_start(*Gtk::manage(_widget), Gtk::PACK_SHRINK); + if (mnemonic) { + _label->set_mnemonic_widget(*_widget); + } + widget->set_tooltip_text(tooltip); +} + + +void Labelled::setWidgetSizeRequest(int width, int height) +{ + if (_widget) + _widget->set_size_request(width, height); + + +} + +Gtk::Widget const * +Labelled::getWidget() const +{ + return _widget; +} + +Gtk::Label const * +Labelled::getLabel() const +{ + return _label; +} + +void +Labelled::setLabelText(const Glib::ustring &str) +{ + _label->set_text(str); +} + +void +Labelled::setTooltipText(const Glib::ustring &tooltip) +{ + _label->set_tooltip_text(tooltip); + _widget->set_tooltip_text(tooltip); +} + +bool Labelled::on_mnemonic_activate ( bool group_cycling ) +{ + return _widget->mnemonic_activate ( group_cycling ); +} + +void +Labelled::set_hexpand(bool expand) +{ + // should only have 2 children, but second child may not be _widget + child_property_pack_type(*get_children().back()) = expand ? Gtk::PACK_END + : Gtk::PACK_START; + + Gtk::HBox::set_hexpand(expand); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/labelled.h b/src/ui/widget/labelled.h new file mode 100644 index 0000000..4620b1a --- /dev/null +++ b/src/ui/widget/labelled.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_LABELLED_H +#define INKSCAPE_UI_WIDGET_LABELLED_H + +#include <gtkmm/box.h> + +namespace Gtk { +class Image; +class Label; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Adds a label with optional icon or suffix to another widget. + */ +class Labelled : public Gtk::HBox +{ +public: + + /** + * Construct a Labelled Widget. + * + * @param label Label. + * @param widget Widget to label; should be allocated with new, as it will + * be passed to Gtk::manage(). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the text + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Labelled(Glib::ustring const &label, Glib::ustring const &tooltip, + Gtk::Widget *widget, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Allow the setting of the width of the labelled widget + */ + void setWidgetSizeRequest(int width, int height); + Gtk::Widget const *getWidget() const; + Gtk::Label const *getLabel() const; + + void setLabelText(const Glib::ustring &str); + void setTooltipText(const Glib::ustring &tooltip); + + void set_hexpand(bool expand = true); + +private: + bool on_mnemonic_activate( bool group_cycling ) override; + +protected: + + Gtk::Widget *_widget; + Gtk::Label *_label; + Gtk::Label *_suffix; + Gtk::Image *_icon; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_LABELLED_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/layer-selector.cpp b/src/ui/widget/layer-selector.cpp new file mode 100644 index 0000000..779542c --- /dev/null +++ b/src/ui/widget/layer-selector.cpp @@ -0,0 +1,616 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Widgets::LayerSelector - layer selector widget + * + * Authors: + * MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include "ui/dialog/layer-properties.h" +#include "ui/icon-loader.h" +#include <boost/range/adaptor/filtered.hpp> +#include <boost/range/adaptor/reversed.hpp> +#include <glibmm/i18n.h> + +#include "desktop.h" + +#include "document.h" +#include "document-undo.h" +#include "layer-manager.h" +#include "ui/icon-names.h" +#include "ui/util.h" +#include "util/reverse-list.h" +#include "verbs.h" +#include "xml/node-event-vector.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +namespace { + +class AlternateIcons : public Gtk::HBox { +public: + AlternateIcons(Gtk::BuiltinIconSize size, Glib::ustring const &a, Glib::ustring const &b) + : _a(nullptr), _b(nullptr) + { + set_name("AlternateIcons"); + if (!a.empty()) { + _a = Gtk::manage(sp_get_icon_image(a, size)); + _a->set_no_show_all(true); + add(*_a); + } + if (!b.empty()) { + _b = Gtk::manage(sp_get_icon_image(b, size)); + _b->set_no_show_all(true); + add(*_b); + } + setState(false); + } + + bool state() const { return _state; } + void setState(bool state) { + _state = state; + if (_state) { + if (_a) { + _a->hide(); + } + if (_b) { + _b->show(); + } + } else { + if (_a) { + _a->show(); + } + if (_b) { + _b->hide(); + } + } + } + +private: + Gtk::Image *_a; + Gtk::Image *_b; + bool _state; +}; + +} + +/** LayerSelector constructor. Creates lock and hide buttons, + * initializes the layer dropdown selector with a label renderer, + * and hooks up signal for setting the desktop layer when the + * selector is changed. + */ +LayerSelector::LayerSelector(SPDesktop *desktop) +: _desktop(nullptr), _layer(nullptr) +{ + set_name("LayerSelector"); + AlternateIcons *label; + + label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, + INKSCAPE_ICON("object-visible"), INKSCAPE_ICON("object-hidden"))); + _visibility_toggle.add(*label); + _visibility_toggle.signal_toggled().connect( + sigc::compose( + sigc::mem_fun(*label, &AlternateIcons::setState), + sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active) + ) + ); + _visibility_toggled_connection = _visibility_toggle.signal_toggled().connect( + sigc::compose( + sigc::mem_fun(*this, &LayerSelector::_hideLayer), + sigc::mem_fun(_visibility_toggle, &Gtk::ToggleButton::get_active) + ) + ); + + _visibility_toggle.set_relief(Gtk::RELIEF_NONE); + _visibility_toggle.set_tooltip_text(_("Toggle current layer visibility")); + pack_start(_visibility_toggle, Gtk::PACK_EXPAND_PADDING); + + label = Gtk::manage(new AlternateIcons(Gtk::ICON_SIZE_MENU, + INKSCAPE_ICON("object-unlocked"), INKSCAPE_ICON("object-locked"))); + _lock_toggle.add(*label); + _lock_toggle.signal_toggled().connect( + sigc::compose( + sigc::mem_fun(*label, &AlternateIcons::setState), + sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active) + ) + ); + _lock_toggled_connection = _lock_toggle.signal_toggled().connect( + sigc::compose( + sigc::mem_fun(*this, &LayerSelector::_lockLayer), + sigc::mem_fun(_lock_toggle, &Gtk::ToggleButton::get_active) + ) + ); + + _lock_toggle.set_relief(Gtk::RELIEF_NONE); + _lock_toggle.set_tooltip_text(_("Lock or unlock current layer")); + pack_start(_lock_toggle, Gtk::PACK_EXPAND_PADDING); + + _selector.set_tooltip_text(_("Current layer")); + pack_start(_selector, Gtk::PACK_EXPAND_WIDGET); + + _layer_model = Gtk::ListStore::create(_model_columns); + _selector.set_model(_layer_model); + _selector.pack_start(_label_renderer); + _selector.set_cell_data_func( + _label_renderer, + sigc::mem_fun(*this, &LayerSelector::_prepareLabelRenderer) + ); + + _selection_changed_connection = _selector.signal_changed().connect( + sigc::mem_fun(*this, &LayerSelector::_setDesktopLayer) + ); + setDesktop(desktop); +} + +/** Destructor - disconnects signal handler + */ +LayerSelector::~LayerSelector() { + setDesktop(nullptr); + _selection_changed_connection.disconnect(); +} + +/** Sets the desktop for the widget. First disconnects signals + * for the current desktop, then stores the pointer to the + * given \a desktop, and attaches its signals to this one. + * Then it selects the current layer for the desktop. + */ +void LayerSelector::setDesktop(SPDesktop *desktop) { + if ( desktop == _desktop ) { + return; + } + + if (_desktop) { +// _desktop_shutdown_connection.disconnect(); + if (_current_layer_changed_connection) + _current_layer_changed_connection.disconnect(); + if (_layers_changed_connection) + _layers_changed_connection.disconnect(); +// g_signal_handlers_disconnect_by_func(_desktop, (gpointer)&detach, this); + } + _desktop = desktop; + if (_desktop) { + // TODO we need a different signal for this, really..s +// _desktop_shutdown_connection = _desktop->connectShutdown( +// sigc::bind (sigc::ptr_fun (detach), this)); +// g_signal_connect_after(_desktop, "shutdown", GCallback(detach), this); + + LayerManager *mgr = _desktop->layer_manager; + if ( mgr ) { + _current_layer_changed_connection = mgr->connectCurrentLayerChanged( sigc::mem_fun(*this, &LayerSelector::_selectLayer) ); + //_layerUpdatedConnection = mgr->connectLayerDetailsChanged( sigc::mem_fun(*this, &LayerSelector::_updateLayer) ); + _layers_changed_connection = mgr->connectChanged( sigc::mem_fun(*this, &LayerSelector::_layersChanged) ); + } + + _selectLayer(_desktop->currentLayer()); + } +} + +namespace { + +class is_layer { +public: + is_layer(SPDesktop *desktop) : _desktop(desktop) {} + bool operator()(SPObject &object) const { + return _desktop->isLayer(&object); + } +private: + SPDesktop *_desktop; +}; + +class column_matches_object { +public: + column_matches_object(Gtk::TreeModelColumn<SPObject *> const &column, + SPObject &object) + : _column(column), _object(object) {} + bool operator()(Gtk::TreeModel::const_iterator const &iter) const { + SPObject *current=(*iter)[_column]; + return current == &_object; + } +private: + Gtk::TreeModelColumn<SPObject *> const &_column; + SPObject &_object; +}; + +} + +void LayerSelector::_layersChanged() +{ + if (_desktop) { + /* + * This code fixes #166691 but causes issues #1066543 and #1080378. + * Comment out until solution found. + */ + //_selectLayer(_desktop->currentLayer()); + } +} + +/** Selects the given layer in the dropdown selector. + */ +void LayerSelector::_selectLayer(SPObject *layer) { + using Inkscape::Util::List; + using Inkscape::Util::cons; + using Inkscape::Util::reverse_list; + + _selection_changed_connection.block(); + _visibility_toggled_connection.block(); + _lock_toggled_connection.block(); + + while (!_layer_model->children().empty()) { + Gtk::ListStore::iterator first_row(_layer_model->children().begin()); + _destroyEntry(first_row); + _layer_model->erase(first_row); + } + + SPObject *root=_desktop->currentRoot(); + + if (_layer) { + sp_object_unref(_layer, nullptr); + _layer = nullptr; + } + + if (layer) { + List<SPObject &> hierarchy=reverse_list<SPObject::ParentIterator>(layer, root); + if ( layer == root ) { + _buildEntries(0, cons(*root, hierarchy)); + } else if (hierarchy) { + _buildSiblingEntries(0, *root, hierarchy); + } + + Gtk::TreeIter row( + std::find_if( + _layer_model->children().begin(), + _layer_model->children().end(), + column_matches_object(_model_columns.object, *layer) + ) + ); + if ( row != _layer_model->children().end() ) { + _selector.set_active(row); + } + + _layer = layer; + sp_object_ref(_layer, nullptr); + } + + if ( !layer || layer == root ) { + _visibility_toggle.set_sensitive(false); + _visibility_toggle.set_active(false); + _lock_toggle.set_sensitive(false); + _lock_toggle.set_active(false); + } else { + _visibility_toggle.set_sensitive(true); + _visibility_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false )); + _lock_toggle.set_sensitive(true); + _lock_toggle.set_active(( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false )); + } + + _lock_toggled_connection.unblock(); + _visibility_toggled_connection.unblock(); + _selection_changed_connection.unblock(); +} + +/** Sets the current desktop layer to the actively selected layer. + */ +void LayerSelector::_setDesktopLayer() { + Gtk::ListStore::iterator selected(_selector.get_active()); + SPObject *layer=_selector.get_active()->get_value(_model_columns.object); + if ( _desktop && layer ) { + _current_layer_changed_connection.block(); + _layers_changed_connection.block(); + + _desktop->layer_manager->setCurrentLayer(layer); + + _current_layer_changed_connection.unblock(); + _layers_changed_connection.unblock(); + + _selectLayer(_desktop->currentLayer()); + } + if (_desktop && _desktop->canvas) { + gtk_widget_grab_focus (GTK_WIDGET(_desktop->canvas)); + } +} + +/** Creates rows in the _layer_model data structure for each item + * in \a hierarchy, to a given \a depth. + */ +void LayerSelector::_buildEntries(unsigned depth, + Inkscape::Util::List<SPObject &> hierarchy) +{ + using Inkscape::Util::List; + using Inkscape::Util::rest; + + _buildEntry(depth, *hierarchy); + + List<SPObject &> remainder=rest(hierarchy); + if (remainder) { + _buildEntries(depth+1, remainder); + } else { + _buildSiblingEntries(depth+1, *hierarchy, remainder); + } +} + +/** Creates entries in the _layer_model data structure for + * all siblings of the first child in \a parent. + */ +void LayerSelector::_buildSiblingEntries( + unsigned depth, SPObject &parent, + Inkscape::Util::List<SPObject &> hierarchy +) { + using Inkscape::Util::rest; + + auto siblings = parent.children | boost::adaptors::filtered(is_layer(_desktop)) | boost::adaptors::reversed; + + SPObject *layer( hierarchy ? &*hierarchy : nullptr ); + + for (auto& sib: siblings) { + _buildEntry(depth, sib); + if ( &sib == layer ) { + _buildSiblingEntries(depth+1, *layer, rest(hierarchy)); + } + } +} + +namespace { + +struct Callbacks { + sigc::slot<void> update_row; + sigc::slot<void> update_list; +}; + +void attribute_changed(Inkscape::XML::Node */*repr*/, gchar const *name, + gchar const */*old_value*/, gchar const */*new_value*/, + bool /*is_interactive*/, void *data) +{ + if ( !std::strcmp(name, "inkscape:groupmode") ) { + reinterpret_cast<Callbacks *>(data)->update_list(); + } else { + reinterpret_cast<Callbacks *>(data)->update_row(); + } +} + +void node_added(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) { + gchar const *mode=child->attribute("inkscape:groupmode"); + if ( mode && !std::strcmp(mode, "layer") ) { + reinterpret_cast<Callbacks *>(data)->update_list(); + } +} + +void node_removed(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, Inkscape::XML::Node */*ref*/, void *data) { + gchar const *mode=child->attribute("inkscape:groupmode"); + if ( mode && !std::strcmp(mode, "layer") ) { + reinterpret_cast<Callbacks *>(data)->update_list(); + } +} + +void node_reordered(Inkscape::XML::Node */*parent*/, Inkscape::XML::Node *child, + Inkscape::XML::Node */*old_ref*/, Inkscape::XML::Node */*new_ref*/, + void *data) +{ + gchar const *mode=child->attribute("inkscape:groupmode"); + if ( mode && !std::strcmp(mode, "layer") ) { + reinterpret_cast<Callbacks *>(data)->update_list(); + } +} + +void update_row_for_object(SPObject *object, + Gtk::TreeModelColumn<SPObject *> const &column, + Glib::RefPtr<Gtk::ListStore> const &model) +{ + Gtk::TreeIter row( + std::find_if( + model->children().begin(), + model->children().end(), + column_matches_object(column, *object) + ) + ); + if ( row != model->children().end() ) { + model->row_changed(model->get_path(row), row); + } +} + +void rebuild_all_rows(sigc::slot<void, SPObject *> rebuild, SPDesktop *desktop) +{ + rebuild(desktop->currentLayer()); +} + +} + +void LayerSelector::_protectUpdate(sigc::slot<void> slot) { + bool visibility_blocked=_visibility_toggled_connection.blocked(); + bool lock_blocked=_lock_toggled_connection.blocked(); + _visibility_toggled_connection.block(true); + _lock_toggled_connection.block(true); + slot(); + + SPObject *layer = _desktop ? _desktop->currentLayer() : nullptr; + if ( layer ) { + bool wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isLocked() : false ); + if ( _lock_toggle.get_active() != wantedValue ) { + _lock_toggle.set_active( wantedValue ); + } + wantedValue = ( SP_IS_ITEM(layer) ? SP_ITEM(layer)->isHidden() : false ); + if ( _visibility_toggle.get_active() != wantedValue ) { + _visibility_toggle.set_active( wantedValue ); + } + } + _visibility_toggled_connection.block(visibility_blocked); + _lock_toggled_connection.block(lock_blocked); +} + +/** Builds and appends a row in the layer model object. + */ +void LayerSelector::_buildEntry(unsigned depth, SPObject &object) { + Inkscape::XML::NodeEventVector *vector; + + Callbacks *callbacks=new Callbacks(); + + callbacks->update_row = sigc::bind( + sigc::mem_fun(*this, &LayerSelector::_protectUpdate), + sigc::bind( + sigc::ptr_fun(&update_row_for_object), + &object, _model_columns.object, _layer_model + ) + ); + + SPObject *layer=_desktop->currentLayer(); + if ( (&object == layer) || (&object == layer->parent) ) { + callbacks->update_list = sigc::bind( + sigc::mem_fun(*this, &LayerSelector::_protectUpdate), + sigc::bind( + sigc::ptr_fun(&rebuild_all_rows), + sigc::mem_fun(*this, &LayerSelector::_selectLayer), + _desktop + ) + ); + + Inkscape::XML::NodeEventVector events = { + &node_added, + &node_removed, + &attribute_changed, + nullptr, + &node_reordered + }; + + vector = new Inkscape::XML::NodeEventVector(events); + } else { + Inkscape::XML::NodeEventVector events = { + nullptr, + nullptr, + &attribute_changed, + nullptr, + nullptr + }; + + vector = new Inkscape::XML::NodeEventVector(events); + } + + Gtk::ListStore::iterator row(_layer_model->append()); + + row->set_value(_model_columns.depth, depth); + + sp_object_ref(&object, nullptr); + row->set_value(_model_columns.object, &object); + + Inkscape::GC::anchor(object.getRepr()); + row->set_value(_model_columns.repr, object.getRepr()); + + row->set_value(_model_columns.callbacks, reinterpret_cast<void *>(callbacks)); + + sp_repr_add_listener(object.getRepr(), vector, callbacks); +} + +/** Removes a row from the _model_columns object, disconnecting listeners + * on the slot. + */ +void LayerSelector::_destroyEntry(Gtk::ListStore::iterator const &row) { + Callbacks *callbacks=reinterpret_cast<Callbacks *>(row->get_value(_model_columns.callbacks)); + SPObject *object=row->get_value(_model_columns.object); + if (object) { + sp_object_unref(object, nullptr); + } + Inkscape::XML::Node *repr=row->get_value(_model_columns.repr); + if (repr) { + sp_repr_remove_listener_by_data(repr, callbacks); + Inkscape::GC::release(repr); + } + delete callbacks; +} + +/** Formats the label for a given layer row + */ +void LayerSelector::_prepareLabelRenderer( + Gtk::TreeModel::const_iterator const &row +) { + unsigned depth=(*row)[_model_columns.depth]; + SPObject *object=(*row)[_model_columns.object]; + bool label_defaulted(false); + + // TODO: when the currently selected row is removed, + // (or before one has been selected) something appears to + // "invent" an iterator with null data and try to render it; + // where does it come from, and how can we avoid it? + if ( object && object->getRepr() ) { + SPObject *layer=( _desktop ? _desktop->currentLayer() : nullptr ); + SPObject *root=( _desktop ? _desktop->currentRoot() : nullptr ); + + bool isancestor = !( (layer && (object->parent == layer->parent)) || ((layer == root) && (object->parent == root))); + + bool iscurrent = ( (object == layer) && (object != root) ); + + gchar *format = g_strdup_printf ( + "<span size=\"smaller\" %s><tt>%*s%s</tt>%s%s%s%%s%s%s%s</span>", + ( _desktop && _desktop->itemIsHidden (SP_ITEM(object)) ? "foreground=\"gray50\"" : "" ), + depth, "", ( iscurrent ? "•" : " " ), + ( iscurrent ? "<b>" : "" ), + ( SP_ITEM(object)->isLocked() ? "[" : "" ), + ( isancestor ? "<small>" : "" ), + ( isancestor ? "</small>" : "" ), + ( SP_ITEM(object)->isLocked() ? "]" : "" ), + ( iscurrent ? "</b>" : "" ) + ); + + gchar const *label; + if ( object != root ) { + label = object->label(); + if (!object->label()) { + label = object->defaultLabel(); + label_defaulted = true; + } + } else { + label = _("(root)"); + } + + gchar *text = g_markup_printf_escaped(format, ink_ellipsize_text (label, 50).c_str()); + _label_renderer.property_markup() = text; + g_free(text); + g_free(format); + } else { + _label_renderer.property_markup() = "<small> </small>"; + } + + _label_renderer.property_ypad() = 1; + _label_renderer.property_style() = ( label_defaulted ? + Pango::STYLE_ITALIC : + Pango::STYLE_NORMAL ); + +} + +void LayerSelector::_lockLayer(bool lock) { + if ( _layer && SP_IS_ITEM(_layer) ) { + SP_ITEM(_layer)->setLocked(lock); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE, + lock? _("Lock layer") : _("Unlock layer")); + } +} + +void LayerSelector::_hideLayer(bool hide) { + if ( _layer && SP_IS_ITEM(_layer) ) { + SP_ITEM(_layer)->setHidden(hide); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_NONE, + hide? _("Hide layer") : _("Unhide layer")); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/layer-selector.h b/src/ui/widget/layer-selector.h new file mode 100644 index 0000000..eadfce2 --- /dev/null +++ b/src/ui/widget/layer-selector.h @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::LayerSelector - layer selector widget + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR +#define SEEN_INKSCAPE_WIDGETS_LAYER_SELECTOR + +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/togglebutton.h> +#include <gtkmm/cellrenderertext.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/liststore.h> +#include <sigc++/slot.h> +#include "util/list.h" + +class SPDesktop; +class SPDocument; +class SPObject; +namespace Inkscape { +namespace XML { +class Node; +} +} + + +namespace Inkscape { +namespace UI { +namespace Widget { + +class DocumentTreeModel; + +class LayerSelector : public Gtk::HBox { +public: + LayerSelector(SPDesktop *desktop = nullptr); + ~LayerSelector() override; + + SPDesktop *desktop() { return _desktop; } + void setDesktop(SPDesktop *desktop); + +private: + class LayerModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<unsigned> depth; + Gtk::TreeModelColumn<SPObject *> object; + Gtk::TreeModelColumn<Inkscape::XML::Node *> repr; + Gtk::TreeModelColumn<void *> callbacks; + + LayerModelColumns() { + add(depth); add(object); add(repr); add(callbacks); + } + }; + + SPDesktop *_desktop; + + Gtk::ComboBox _selector; + Gtk::ToggleButton _visibility_toggle; + Gtk::ToggleButton _lock_toggle; + + LayerModelColumns _model_columns; + Gtk::CellRendererText _label_renderer; + Glib::RefPtr<Gtk::ListStore> _layer_model; + +// sigc::connection _desktop_shutdown_connection; + sigc::connection _layers_changed_connection; + sigc::connection _current_layer_changed_connection; + sigc::connection _selection_changed_connection; + sigc::connection _visibility_toggled_connection; + sigc::connection _lock_toggled_connection; + + SPObject *_layer; + + void _selectLayer(SPObject *layer); + void _layersChanged(); + + void _setDesktopLayer(); + + void _buildEntry(unsigned depth, SPObject &object); + void _buildEntries(unsigned depth, + Inkscape::Util::List<SPObject &> hierarchy); + void _buildSiblingEntries(unsigned depth, + SPObject &parent, + Inkscape::Util::List<SPObject &> hierarchy); + void _protectUpdate(sigc::slot<void> slot); + void _destroyEntry(Gtk::ListStore::iterator const &row); + void _hideLayer(bool hide); + void _lockLayer(bool lock); + + void _prepareLabelRenderer(Gtk::TreeModel::const_iterator const &row); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/layertypeicon.cpp b/src/ui/widget/layertypeicon.cpp new file mode 100644 index 0000000..d8b1378 --- /dev/null +++ b/src/ui/widget/layertypeicon.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/layertypeicon.h" + +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "widgets/toolbox.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +LayerTypeIcon::LayerTypeIcon() : + Glib::ObjectBase(typeid(LayerTypeIcon)), + Gtk::CellRendererPixbuf(), + _pixLayerName(INKSCAPE_ICON("dialog-layers")), + _pixGroupName(INKSCAPE_ICON("layer-duplicate")), + _pixPathName(INKSCAPE_ICON("layer-rename")), + _property_active(*this, "active", false), + _property_activatable(*this, "activatable", true), + _property_pixbuf_layer(*this, "pixbuf_on", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_group(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)), + _property_pixbuf_path(*this, "pixbuf_off", Glib::RefPtr<Gdk::Pixbuf>(nullptr)) +{ + + property_mode() = Gtk::CELL_RENDERER_MODE_ACTIVATABLE; + + _property_pixbuf_layer = sp_get_icon_pixbuf(_pixLayerName, GTK_ICON_SIZE_MENU); + _property_pixbuf_group = sp_get_icon_pixbuf(_pixGroupName, GTK_ICON_SIZE_MENU); + _property_pixbuf_path = sp_get_icon_pixbuf(_pixPathName, GTK_ICON_SIZE_MENU); + + property_pixbuf() = _property_pixbuf_path.get_value(); +} + +void LayerTypeIcon::get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const +{ + Gtk::CellRendererPixbuf::get_preferred_height_vfunc(widget, min_h, nat_h); + + if (min_h) { + min_h += (min_h) >> 1; + } + + if (nat_h) { + nat_h += (nat_h) >> 1; + } +} + +void LayerTypeIcon::get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const +{ + Gtk::CellRendererPixbuf::get_preferred_width_vfunc(widget, min_w, nat_w); + + if (min_w) { + min_w += (min_w) >> 1; + } + + if (nat_w) { + nat_w += (nat_w) >> 1; + } +} + +void LayerTypeIcon::render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) +{ + property_pixbuf() = _property_active.get_value() == 1 ? _property_pixbuf_group : (_property_active.get_value() == 2 ? _property_pixbuf_layer : _property_pixbuf_path); + Gtk::CellRendererPixbuf::render_vfunc( cr, widget, background_area, cell_area, flags ); +} + +bool +LayerTypeIcon::activate_vfunc(GdkEvent* event, + Gtk::Widget& /*widget*/, + const Glib::ustring& path, + const Gdk::Rectangle& /*background_area*/, + const Gdk::Rectangle& /*cell_area*/, + Gtk::CellRendererState /*flags*/) +{ + _signal_pre_toggle.emit(event); + _signal_toggled.emit(path); + + return false; +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + + diff --git a/src/ui/widget/layertypeicon.h b/src/ui/widget/layertypeicon.h new file mode 100644 index 0000000..7dccf4c --- /dev/null +++ b/src/ui/widget/layertypeicon.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __UI_DIALOG_LAYERTYPEICON_H__ +#define __UI_DIALOG_LAYERTYPEICON_H__ +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/cellrendererpixbuf.h> +#include <gtkmm/widget.h> +#include <glibmm/property.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class LayerTypeIcon : public Gtk::CellRendererPixbuf { +public: + LayerTypeIcon(); + ~LayerTypeIcon() override = default;; + + sigc::signal<void, const Glib::ustring&> signal_toggled() { return _signal_toggled;} + sigc::signal<void, GdkEvent const *> signal_pre_toggle() { return _signal_pre_toggle; } + + Glib::PropertyProxy<int> property_active() { return _property_active.get_proxy(); } + Glib::PropertyProxy<int> property_activatable() { return _property_activatable.get_proxy(); } + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_on(); + Glib::PropertyProxy< Glib::RefPtr<Gdk::Pixbuf> > property_pixbuf_off(); + +protected: + void render_vfunc( const Cairo::RefPtr<Cairo::Context>& cr, + Gtk::Widget& widget, + const Gdk::Rectangle& background_area, + const Gdk::Rectangle& cell_area, + Gtk::CellRendererState flags ) override; + + void get_preferred_width_vfunc(Gtk::Widget& widget, + int& min_w, + int& nat_w) const override; + + void get_preferred_height_vfunc(Gtk::Widget& widget, + int& min_h, + int& nat_h) const override; + + bool activate_vfunc(GdkEvent *event, + Gtk::Widget &widget, + const Glib::ustring &path, + const Gdk::Rectangle &background_area, + const Gdk::Rectangle &cell_area, + Gtk::CellRendererState flags) override; + + +private: + Glib::ustring _pixLayerName; + Glib::ustring _pixGroupName; + Glib::ustring _pixPathName; + + Glib::Property<int> _property_active; + Glib::Property<int> _property_activatable; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_layer; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_group; + Glib::Property< Glib::RefPtr<Gdk::Pixbuf> > _property_pixbuf_path; + + sigc::signal<void, const Glib::ustring&> _signal_toggled; + sigc::signal<void, GdkEvent const *> _signal_pre_toggle; + +}; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif /* __UI_DIALOG_IMAGETOGGLER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/licensor.cpp b/src/ui/widget/licensor.cpp new file mode 100644 index 0000000..2ad811f --- /dev/null +++ b/src/ui/widget/licensor.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Abhishek Sharma + * + * Copyright (C) 2000 - 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "licensor.h" + +#include <gtkmm/entry.h> +#include <gtkmm/radiobutton.h> + +#include "ui/widget/entity-entry.h" +#include "ui/widget/registry.h" +#include "rdf.h" +#include "inkscape.h" +#include "document-undo.h" +#include "verbs.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +const struct rdf_license_t _proprietary_license = + {_("Proprietary"), "", nullptr}; + +const struct rdf_license_t _other_license = + {Q_("MetadataLicence|Other"), "", nullptr}; + +class LicenseItem : public Gtk::RadioButton { +public: + LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group); +protected: + void on_toggled() override; + struct rdf_license_t const *_lic; + EntityEntry *_eep; + Registry &_wr; +}; + +LicenseItem::LicenseItem (struct rdf_license_t const* license, EntityEntry* entity, Registry &wr, Gtk::RadioButtonGroup *group) +: Gtk::RadioButton(_(license->name)), _lic(license), _eep(entity), _wr(wr) +{ + if (group) { + set_group (*group); + } +} + +/// \pre it is assumed that the license URI entry is a Gtk::Entry +void LicenseItem::on_toggled() +{ + if (_wr.isUpdating()) return; + + _wr.setUpdating (true); + SPDocument *doc = SP_ACTIVE_DOCUMENT; + rdf_set_license (doc, _lic->details ? _lic : nullptr); + if (doc->isSensitive()) { + DocumentUndo::done(doc, SP_VERB_NONE, _("Document license updated")); + } + _wr.setUpdating (false); + static_cast<Gtk::Entry*>(_eep->_packable)->set_text (_lic->uri); + _eep->on_changed(); +} + +//--------------------------------------------------- + +Licensor::Licensor() +: Gtk::VBox(false,4), + _eentry (nullptr) +{ +} + +Licensor::~Licensor() +{ + if (_eentry) delete _eentry; +} + +void Licensor::init (Registry& wr) +{ + /* add license-specific metadata entry areas */ + rdf_work_entity_t* entity = rdf_find_entity ( "license_uri" ); + _eentry = EntityEntry::create (entity, wr); + + LicenseItem *i; + wr.setUpdating (true); + i = Gtk::manage (new LicenseItem (&_proprietary_license, _eentry, wr, nullptr)); + Gtk::RadioButtonGroup group = i->get_group(); + add (*i); + LicenseItem *pd = i; + + for (struct rdf_license_t * license = rdf_licenses; + license && license->name; + license++) { + i = Gtk::manage (new LicenseItem (license, _eentry, wr, &group)); + add(*i); + } + // add Other at the end before the URI field for the confused ppl. + LicenseItem *io = Gtk::manage (new LicenseItem (&_other_license, _eentry, wr, &group)); + add (*io); + + pd->set_active(); + wr.setUpdating (false); + + Gtk::HBox *box = Gtk::manage (new Gtk::HBox); + pack_start (*box, true, true, 0); + + box->pack_start (_eentry->_label, false, false, 5); + box->pack_start (*_eentry->_packable, true, true, 0); + + show_all_children(); +} + +void Licensor::update (SPDocument *doc) +{ + /* identify the license info */ + struct rdf_license_t * license = rdf_get_license (doc); + + if (license) { + int i; + for (i=0; rdf_licenses[i].name; i++) + if (license == &rdf_licenses[i]) + break; + static_cast<LicenseItem*>(get_children()[i+1])->set_active(); + } + else { + static_cast<LicenseItem*>(get_children()[0])->set_active(); + } + + /* update the URI */ + _eentry->update (doc); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/licensor.h b/src/ui/widget/licensor.h new file mode 100644 index 0000000..3e1f0da --- /dev/null +++ b/src/ui/widget/licensor.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_LICENSOR_H +#define INKSCAPE_UI_WIDGET_LICENSOR_H + +#include <gtkmm/box.h> + +class SPDocument; + +namespace Inkscape { + namespace UI { + namespace Widget { + +class EntityEntry; +class Registry; + + +/** + * Widget for specifying a document's license; part of document + * preferences dialog. + */ +class Licensor : public Gtk::VBox { +public: + Licensor(); + ~Licensor() override; + void init (Registry&); + void update (SPDocument *doc); + +protected: + EntityEntry *_eentry; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_LICENSOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/notebook-page.cpp b/src/ui/widget/notebook-page.cpp new file mode 100644 index 0000000..a189d78 --- /dev/null +++ b/src/ui/widget/notebook-page.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Notebook page widget. + * + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "notebook-page.h" + +# include <gtkmm/grid.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +NotebookPage::NotebookPage(int n_rows, int n_columns, bool expand, bool fill, guint padding) + :_table(Gtk::manage(new Gtk::Grid())) +{ + set_name("NotebookPage"); + set_border_width(4); + set_spacing(4); + + _table->set_row_spacing(4); + _table->set_column_spacing(4); + + pack_start(*_table, expand, fill, padding); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/notebook-page.h b/src/ui/widget/notebook-page.h new file mode 100644 index 0000000..cc11d30 --- /dev/null +++ b/src/ui/widget/notebook-page.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H +#define INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H + +#include <gtkmm/box.h> + +namespace Gtk { +class Grid; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A tabbed notebook page for dialogs. + */ +class NotebookPage : public Gtk::VBox +{ +public: + + NotebookPage(); + + /** + * Construct a NotebookPage. + */ + NotebookPage(int n_rows, int n_columns, bool expand=false, bool fill=false, guint padding=0); + + Gtk::Grid& table() { return *_table; } + +protected: + Gtk::Grid *_table; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_NOTEBOOK_PAGE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/object-composite-settings.cpp b/src/ui/widget/object-composite-settings.cpp new file mode 100644 index 0000000..db2e91c --- /dev/null +++ b/src/ui/widget/object-composite-settings.cpp @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A widget for controlling object compositing (filter, opacity, etc.) + * + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * Niko Kiirala <niko@kiirala.com> + * Abhishek Sharma + * + * Copyright (C) 2004--2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/object-composite-settings.h" + +#include "desktop.h" + +#include "desktop-style.h" +#include "document.h" +#include "document-undo.h" +#include "filter-chemistry.h" +#include "inkscape.h" +#include "style.h" +#include "svg/css-ostringstream.h" +#include "verbs.h" +#include "display/sp-canvas.h" +#include "object/filters/blend.h" +#include "ui/widget/style-subject.h" + +constexpr double BLUR_MULTIPLIER = 4.0; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ObjectCompositeSettings::ObjectCompositeSettings(unsigned int verb_code, char const *history_prefix, int flags) +: _verb_code(verb_code), + _blend_tag(Glib::ustring(history_prefix) + ":blend"), + _blur_tag(Glib::ustring(history_prefix) + ":blur"), + _opacity_tag(Glib::ustring(history_prefix) + ":opacity"), + _isolation_tag(Glib::ustring(history_prefix) + ":isolation"), + _filter_modifier(flags), + _blocked(false) +{ + set_name( "ObjectCompositeSettings"); + + // Filter Effects + pack_start(_filter_modifier, false, false, 2); + + _filter_modifier.signal_blend_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged)); + _filter_modifier.signal_blur_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_blendBlurValueChanged)); + _filter_modifier.signal_opacity_changed().connect(sigc::mem_fun(*this, &ObjectCompositeSettings::_opacityValueChanged)); + _filter_modifier.signal_isolation_changed().connect( + sigc::mem_fun(*this, &ObjectCompositeSettings::_isolationValueChanged)); + + show_all_children(); +} + +ObjectCompositeSettings::~ObjectCompositeSettings() { + setSubject(nullptr); +} + +void ObjectCompositeSettings::setSubject(StyleSubject *subject) { + _subject_changed.disconnect(); + if (subject) { + _subject = subject; + _subject_changed = _subject->connectChanged(sigc::mem_fun(*this, &ObjectCompositeSettings::_subjectChanged)); + _subject->setDesktop(SP_ACTIVE_DESKTOP); + } +} + +// We get away with sharing one callback for blend and blur as this is used by +// * the Layers dialog where only one layer can be selected at a time, +// * the Fill and Stroke dialog where only blur is used. +// If both blend and blur are used in a dialog where more than one object can +// be selected then this should be split into separate functions for blend and +// blur (like in the Objects dialog). +void +ObjectCompositeSettings::_blendBlurValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + SPDocument *document = desktop->getDocument(); + + if (_blocked) + return; + _blocked = true; + + // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in crash 1580903 + //sp_canvas_force_full_redraw_after_interruptions(desktop->getCanvas(), 0); + + Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX); + double radius; + if (bbox) { + double perimeter = bbox->dimensions()[Geom::X] + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + double blur_value = _filter_modifier.get_blur_value() / 100.0; + radius = blur_value * blur_value * perimeter / BLUR_MULTIPLIER; + } else { + radius = 0; + } + + //apply created filter to every selected item + std::vector<SPObject*> sel = _subject->list(); + for (auto i : sel) { + if (!SP_IS_ITEM(i)) { + continue; + } + SPItem * item = SP_ITEM(i); + SPStyle *style = item->style; + g_assert(style != nullptr); + bool change_blend = (item->style->mix_blend_mode.set ? item->style->mix_blend_mode.value : SP_CSS_BLEND_NORMAL) != _filter_modifier.get_blend_mode(); + // < 1.0 filter based blend removal + if (!item->style->mix_blend_mode.set && item->style->filter.set && item->style->getFilter()) { + remove_filter_legacy_blend(item); + } + item->style->mix_blend_mode.set = TRUE; + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + } else { + item->style->mix_blend_mode.value = _filter_modifier.get_blend_mode(); + } + + if (radius == 0 && item->style->filter.set + && filter_is_single_gaussian_blur(SP_FILTER(item->style->getFilter()))) { + remove_filter(item, false); + } else if (radius != 0) { + SPFilter *filter = modify_filter_gaussian_blur_from_item(document, item, radius); + sp_style_set_property_url(item, "filter", filter, false); + } + if (change_blend) { //we do blend so we need update display style + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } else { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + } + } + + DocumentUndo::maybeDone(document, _blur_tag.c_str(), _verb_code, + _("Change blur/blend filter")); + + // resume interruptibility + //sp_canvas_end_forced_full_redraws(desktop->getCanvas()); + + _blocked = false; +} + +void +ObjectCompositeSettings::_opacityValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + + SPCSSAttr *css = sp_repr_css_attr_new (); + + Inkscape::CSSOStringStream os; + os << CLAMP (_filter_modifier.get_opacity_value() / 100, 0.0, 1.0); + sp_repr_css_set_property (css, "opacity", os.str().c_str()); + + _subject->setCSS(css); + + sp_repr_css_attr_unref (css); + + DocumentUndo::maybeDone(desktop->getDocument(), _opacity_tag.c_str(), _verb_code, + _("Change opacity")); + + // resume interruptibility + //sp_canvas_end_forced_full_redraws(desktop->getCanvas()); + + _blocked = false; +} + +void ObjectCompositeSettings::_isolationValueChanged() +{ + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + + for (auto item : _subject->list()) { + item->style->isolation.set = TRUE; + item->style->isolation.value = _filter_modifier.get_isolation_mode(); + if (item->style->isolation.value == SP_CSS_ISOLATION_ISOLATE) { + item->style->mix_blend_mode.set = TRUE; + item->style->mix_blend_mode.value = SP_CSS_BLEND_NORMAL; + } + item->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + + DocumentUndo::maybeDone(desktop->getDocument(), _isolation_tag.c_str(), _verb_code, _("Change isolation")); + + // resume interruptibility + // sp_canvas_end_forced_full_redraws(desktop->getCanvas()); + + _blocked = false; +} + +void +ObjectCompositeSettings::_subjectChanged() { + if (!_subject) { + return; + } + + SPDesktop *desktop = _subject->getDesktop(); + if (!desktop) { + return; + } + + if (_blocked) + return; + _blocked = true; + SPStyle query(desktop->getDocument()); + int result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_MASTEROPACITY); + + switch (result) { + case QUERY_STYLE_NOTHING: + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_opacity_value(100 * SP_SCALE24_TO_FLOAT(query.opacity.value)); + break; + } + + //query now for current filter mode and average blurring of selection + const int isolation_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_ISOLATION); + switch (isolation_result) { + case QUERY_STYLE_NOTHING: + _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_isolation_mode(query.isolation.value, true); // here dont work mix_blend_mode.set + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + _filter_modifier.set_isolation_mode(SP_CSS_ISOLATION_AUTO, false); + // TODO: set text + break; + } + + // query now for current filter mode and average blurring of selection + const int blend_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLEND); + switch(blend_result) { + case QUERY_STYLE_NOTHING: + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_SAME: + _filter_modifier.set_blend_mode(query.mix_blend_mode.value, true); // here dont work mix_blend_mode.set + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + _filter_modifier.set_blend_mode(SP_CSS_BLEND_NORMAL, false); + break; + } + + int blur_result = _subject->queryStyle(&query, QUERY_STYLE_PROPERTY_BLUR); + switch (blur_result) { + case QUERY_STYLE_NOTHING: // no blurring + _filter_modifier.set_blur_value(0); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + Geom::OptRect bbox = _subject->getBounds(SPItem::GEOMETRIC_BBOX); + if (bbox) { + double perimeter = + bbox->dimensions()[Geom::X] + + bbox->dimensions()[Geom::Y]; // fixme: this is only half the perimeter, is that correct? + // update blur widget value + float radius = query.filter_gaussianBlur_deviation.value; + float percent = std::sqrt(radius * BLUR_MULTIPLIER / perimeter) * 100; + _filter_modifier.set_blur_value(percent); + } + break; + } + + // If we have nothing selected, disable dialog. + if (result == QUERY_STYLE_NOTHING && + blend_result == QUERY_STYLE_NOTHING ) { + _filter_modifier.set_sensitive( false ); + } else { + _filter_modifier.set_sensitive( true ); + } + + _blocked = false; +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/object-composite-settings.h b/src/ui/widget/object-composite-settings.h new file mode 100644 index 0000000..9650118 --- /dev/null +++ b/src/ui/widget/object-composite-settings.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H +#define SEEN_UI_WIDGET_OBJECT_COMPOSITE_SETTINGS_H + +/* + * Authors: + * Bryce W. Harrington <bryce@bryceharrington.org> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004--2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/label.h> +#include <gtkmm/scale.h> +#include <glibmm/ustring.h> + +#include "ui/widget/filter-effect-chooser.h" + +class SPDesktop; +struct InkscapeApplication; + +namespace Inkscape { + +namespace UI { +namespace Widget { + +class StyleSubject; + +/* + * A widget for controlling object compositing (filter, opacity, etc.) + */ +class ObjectCompositeSettings : public Gtk::VBox { +public: + ObjectCompositeSettings(unsigned int verb_code, char const *history_prefix, int flags); + ~ObjectCompositeSettings() override; + + void setSubject(StyleSubject *subject); + +private: + unsigned int _verb_code; + Glib::ustring _blend_tag; + Glib::ustring _blur_tag; + Glib::ustring _opacity_tag; + Glib::ustring _isolation_tag; + + StyleSubject *_subject; + + SimpleFilterModifier _filter_modifier; + + bool _blocked; + gulong _desktop_activated; + sigc::connection _subject_changed; + + static void _on_desktop_activate(SPDesktop *desktop, ObjectCompositeSettings *w); + static void _on_desktop_deactivate(SPDesktop *desktop, ObjectCompositeSettings *w); + void _subjectChanged(); + void _blendBlurValueChanged(); + void _opacityValueChanged(); + void _isolationValueChanged(); +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/page-sizer.cpp b/src/ui/widget/page-sizer.cpp new file mode 100644 index 0000000..d869a1c --- /dev/null +++ b/src/ui/widget/page-sizer.cpp @@ -0,0 +1,781 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * + * Paper-size widget and helper functions + */ +/* + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Bob Jamison <ishmal@users.sf.net> + * Abhishek Sharma + * + * Copyright (C) 2000 - 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "page-sizer.h" +#include "pages-skeleton.h" +#include <glib.h> +#include <glibmm/i18n.h> +#include "verbs.h" +#include "helper/action.h" +#include "object/sp-root.h" +#include "io/resource.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +//######################################################################## +//# P A G E S I Z E R +//######################################################################## + +/** + * Constructor + */ +PageSizer::PageSizer(Registry & _wr) + : Gtk::VBox(false,4), + _dimensionUnits( _("U_nits:"), "units", _wr ), + _dimensionWidth( _("_Width:"), _("Width of paper"), "width", _dimensionUnits, _wr ), + _dimensionHeight( _("_Height:"), _("Height of paper"), "height", _dimensionUnits, _wr ), + _marginLock( _("Loc_k margins"), _("Lock margins"), "lock-margins", _wr, false, nullptr, nullptr), + _lock_icon(), + _marginTop( _("T_op:"), _("Top margin"), "fit-margin-top", _wr ), + _marginLeft( _("L_eft:"), _("Left margin"), "fit-margin-left", _wr), + _marginRight( _("Ri_ght:"), _("Right margin"), "fit-margin-right", _wr), + _marginBottom( _("Botto_m:"), _("Bottom margin"), "fit-margin-bottom", _wr), + _lockMarginUpdate(false), + _scaleX(_("Scale _x:"), _("Scale X"), "scale-x", _wr), + _scaleY(_("Scale _y:"), _("While SVG allows non-uniform scaling it is recommended to use only uniform scaling in Inkscape. To set a non-uniform scaling, set the 'viewBox' directly."), "scale-y", _wr), + _lockScaleUpdate(false), + _viewboxX(_("X:"), _("X"), "viewbox-x", _wr), + _viewboxY(_("Y:"), _("Y"), "viewbox-y", _wr), + _viewboxW(_("Width:"), _("Width"), "viewbox-width", _wr), + _viewboxH(_("Height:"), _("Height"), "viewbox-height", _wr), + _lockViewboxUpdate(false), + _widgetRegistry(&_wr) +{ + // set precision of scalar entry boxes + _wr.setUpdating (true); + _dimensionWidth.setDigits(5); + _dimensionHeight.setDigits(5); + _marginTop.setDigits(5); + _marginLeft.setDigits(5); + _marginRight.setDigits(5); + _marginBottom.setDigits(5); + _scaleX.setDigits(5); + _scaleY.setDigits(5); + _viewboxX.setDigits(5); + _viewboxY.setDigits(5); + _viewboxW.setDigits(5); + _viewboxH.setDigits(5); + _dimensionWidth.setRange( 0.00001, 10000000 ); + _dimensionHeight.setRange( 0.00001, 10000000 ); + _scaleX.setRange( 0.00001, 100000 ); + _scaleY.setRange( 0.00001, 100000 ); + _viewboxX.setRange( -10000000, 10000000 ); + _viewboxY.setRange( -10000000, 10000000 ); + _viewboxW.setRange( 0.00001, 10000000 ); + _viewboxH.setRange( 0.00001, 10000000 ); + + _scaleY.set_sensitive (false); // We only want to display Y scale. + + _wr.setUpdating (false); + + //# Set up the Paper Size combo box + _paperSizeListStore = Gtk::ListStore::create(_paperSizeListColumns); + _paperSizeList.set_model(_paperSizeListStore); + _paperSizeList.append_column(_("Name"), + _paperSizeListColumns.nameColumn); + _paperSizeList.append_column(_("Description"), + _paperSizeListColumns.descColumn); + _paperSizeList.set_headers_visible(false); + _paperSizeListSelection = _paperSizeList.get_selection(); + _paper_size_list_connection = + _paperSizeListSelection->signal_changed().connect ( + sigc::mem_fun (*this, &PageSizer::on_paper_size_list_changed)); + _paperSizeListScroller.add(_paperSizeList); + _paperSizeListScroller.set_shadow_type(Gtk::SHADOW_IN); + _paperSizeListScroller.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + _paperSizeListScroller.set_size_request(-1, 130); + + + char *path = Inkscape::IO::Resource::profile_path("pages.csv"); + if (!g_file_test(path, G_FILE_TEST_EXISTS)) { + if (!g_file_set_contents(path, pages_skeleton, -1, nullptr)) { + g_warning("%s", _("Failed to create the page file.")); + } + } + + gchar *content = nullptr; + if (g_file_get_contents(path, &content, nullptr, nullptr)) { + + gchar **lines = g_strsplit_set(content, "\n", 0); + + for (int i = 0; lines && lines[i]; ++i) { + gchar **line = g_strsplit_set(lines[i], ",", 5); + if (!line[0] || !line[1] || !line[2] || !line[3] || line[0][0]=='#') + continue; + //name, width, height, unit + double width = g_ascii_strtod(line[1], nullptr); + double height = g_ascii_strtod(line[2], nullptr); + g_strstrip(line[0]); + g_strstrip(line[3]); + Glib::ustring name = line[0]; + char formatBuf[80]; + snprintf(formatBuf, 79, "%0.1f x %0.1f", width, height); + Glib::ustring desc = formatBuf; + desc.append(" " + std::string(line[3])); + PaperSize paper(name, width, height, Inkscape::Util::unit_table.getUnit(line[3])); + _paperSizeTable[name] = paper; + Gtk::TreeModel::Row row = *(_paperSizeListStore->append()); + row[_paperSizeListColumns.nameColumn] = name; + row[_paperSizeListColumns.descColumn] = desc; + g_strfreev(line); + } + g_strfreev(lines); + g_free(content); + } + g_free(path); + + pack_start (_paperSizeListScroller, true, true, 0); + + //## Set up orientation radio buttons + pack_start (_orientationBox, false, false, 0); + _orientationLabel.set_label(_("Orientation:")); + _orientationBox.pack_start(_orientationLabel, false, false, 0); + _landscapeButton.set_use_underline(); + _landscapeButton.set_label(_("_Landscape")); + _landscapeButton.set_active(true); + Gtk::RadioButton::Group group = _landscapeButton.get_group(); + _orientationBox.pack_end (_landscapeButton, false, false, 5); + _portraitButton.set_use_underline(); + _portraitButton.set_label(_("_Portrait")); + _portraitButton.set_active(true); + _orientationBox.pack_end (_portraitButton, false, false, 5); + _portraitButton.set_group (group); + _portraitButton.set_active (true); + + // Setting default custom unit to document unit + SPDesktop *dt = SP_ACTIVE_DESKTOP; + SPNamedView *nv = dt->getNamedView(); + _wr.setUpdating (true); + if (nv->page_size_units) { + _dimensionUnits.setUnit(nv->page_size_units->abbr); + } else if (nv->display_units) { + _dimensionUnits.setUnit(nv->display_units->abbr); + } + _wr.setUpdating (false); + + + //## Set up custom size frame + _customFrame.set_label(_("Custom size")); + pack_start (_customFrame, false, false, 0); + _customFrame.add(_customDimTable); + + _customDimTable.set_border_width(4); + _customDimTable.set_row_spacing(4); + _customDimTable.set_column_spacing(4); + + _dimensionHeight.set_halign(Gtk::ALIGN_CENTER); + _dimensionUnits.set_halign(Gtk::ALIGN_END); + _customDimTable.attach(_dimensionWidth, 0, 0, 1, 1); + _customDimTable.attach(_dimensionHeight, 1, 0, 1, 1); + _customDimTable.attach(_dimensionUnits, 2, 0, 1, 1); + + _customDimTable.attach(_fitPageMarginExpander, 0, 1, 3, 1); + + //## Set up fit page expander + _fitPageMarginExpander.set_use_underline(); + _fitPageMarginExpander.set_label(_("Resi_ze page to content...")); + _fitPageMarginExpander.add(_marginTable); + + _marginTable.set_border_width(4); + _marginTable.set_row_spacing(4); + _marginTable.set_column_spacing(4); + + //### margin label and lock button + _marginLabel.set_markup(Glib::ustring("<b><i>") + _("Margins") + "</i></b>"); + _marginLabel.set_halign(Gtk::ALIGN_CENTER); + + _lock_icon.set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_LARGE_TOOLBAR); + _lock_icon.show(); + _marginLock.set_active(false); + _marginLock.add(_lock_icon); + + _marginBox.set_spacing(4); + _marginBox.add(_marginLabel); + _marginBox.add(_marginLock); + _marginBox.set_halign(Gtk::ALIGN_CENTER); + _marginTable.attach(_marginBox, 1, 1, 1, 1); + + //### margins + _marginTop.set_halign(Gtk::ALIGN_CENTER); + _marginLeft.set_halign(Gtk::ALIGN_START); + _marginRight.set_halign(Gtk::ALIGN_END); + _marginBottom.set_halign(Gtk::ALIGN_CENTER); + + _marginTable.attach(_marginTop, 0, 0, 3, 1); + _marginTable.attach(_marginLeft, 0, 1, 1, 1); + _marginTable.attach(_marginRight, 2, 1, 1, 1); + _marginTable.attach(_marginBottom, 0, 2, 3, 1); + + //### fit page to drawing button + _fitPageButton.set_use_underline(); + _fitPageButton.set_label(_("_Resize page to drawing or selection (Ctrl+Shift+R)")); + _fitPageButton.set_tooltip_text(_("Resize the page to fit the current selection, or the entire drawing if there is no selection")); + + _fitPageButton.set_hexpand(); + _fitPageButton.set_halign(Gtk::ALIGN_CENTER); + _marginTable.attach(_fitPageButton, 0, 3, 3, 1); + + + //## Set up scale frame + _scaleFrame.set_label(_("Scale")); + pack_start (_scaleFrame, false, false, 0); + _scaleFrame.add(_scaleTable); + + _scaleTable.set_border_width(4); + _scaleTable.set_row_spacing(4); + _scaleTable.set_column_spacing(4); + + _scaleTable.attach(_scaleX, 0, 0, 1, 1); + _scaleTable.attach(_scaleY, 1, 0, 1, 1); + _scaleTable.attach(_scaleLabel, 2, 0, 1, 1); + + _viewboxExpander.set_hexpand(); + _scaleTable.attach(_viewboxExpander, 0, 2, 3, 1); + + _viewboxExpander.set_use_underline(); + _viewboxExpander.set_label(_("_Viewbox...")); + _viewboxExpander.add(_viewboxTable); + + _viewboxTable.set_border_width(4); + _viewboxTable.set_row_spacing(4); + _viewboxTable.set_column_spacing(4); + + _viewboxX.set_halign(Gtk::ALIGN_END); + _viewboxY.set_halign(Gtk::ALIGN_END); + _viewboxW.set_halign(Gtk::ALIGN_END); + _viewboxH.set_halign(Gtk::ALIGN_END); + _viewboxSpacer.set_hexpand(); + _viewboxTable.attach(_viewboxX, 0, 0, 1, 1); + _viewboxTable.attach(_viewboxY, 1, 0, 1, 1); + _viewboxTable.attach(_viewboxW, 0, 1, 1, 1); + _viewboxTable.attach(_viewboxH, 1, 1, 1, 1); + _viewboxTable.attach(_viewboxSpacer, 2, 0, 3, 1); + + _wr.setUpdating (true); + updateScaleUI(); + _wr.setUpdating (false); +} + + +/** + * Destructor + */ +PageSizer::~PageSizer() += default; + + + +/** + * Initialize or reset this widget + */ +void +PageSizer::init () +{ + _landscape_connection = _landscapeButton.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_landscape)); + _portrait_connection = _portraitButton.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_portrait)); + _changedw_connection = _dimensionWidth.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_value_changed)); + _changedh_connection = _dimensionHeight.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_value_changed)); + _changedu_connection = _dimensionUnits.getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &PageSizer::on_units_changed)); + _fitPageButton.signal_clicked().connect(sigc::mem_fun(*this, &PageSizer::fire_fit_canvas_to_selection_or_drawing)); + _changeds_connection = _scaleX.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_scale_changed)); + _changedvx_connection = _viewboxX.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed)); + _changedvy_connection = _viewboxY.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed)); + _changedvw_connection = _viewboxW.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed)); + _changedvh_connection = _viewboxH.signal_value_changed().connect (sigc::mem_fun (*this, &PageSizer::on_viewbox_changed)); + _changedlk_connection = _marginLock.signal_toggled().connect (sigc::mem_fun (*this, &PageSizer::on_margin_lock_changed)); + _changedmt_connection = _marginTop.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginTop)); + _changedmb_connection = _marginBottom.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginBottom)); + _changedml_connection = _marginLeft.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginLeft)); + _changedmr_connection = _marginRight.signal_value_changed().connect (sigc::bind<RegisteredScalar*>(sigc::mem_fun (*this, &PageSizer::on_margin_changed), &_marginRight)); + show_all_children(); +} + + +/** + * Set document dimensions (if not called by Doc prop's update()) and + * set the PageSizer's widgets and text entries accordingly. If + * 'changeList' is true, then adjust the paperSizeList to show the closest + * standard page size. + * + * \param w, h + * \param changeList whether to modify the paper size list + */ +void +PageSizer::setDim (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h, bool changeList, bool changeSize) +{ + static bool _called = false; + if (_called) { + return; + } + + _called = true; + + _paper_size_list_connection.block(); + _landscape_connection.block(); + _portrait_connection.block(); + _changedw_connection.block(); + _changedh_connection.block(); + + _unit = w.unit->abbr; + + if (SP_ACTIVE_DESKTOP && !_widgetRegistry->isUpdating()) { + SPDocument *doc = SP_ACTIVE_DESKTOP->getDocument(); + Inkscape::Util::Quantity const old_height = doc->getHeight(); + doc->setWidthAndHeight (w, h, changeSize); + // The origin for the user is in the lower left corner; this point should remain stationary when + // changing the page size. The SVG's origin however is in the upper left corner, so we must compensate for this + if (changeSize && !doc->is_yaxisdown()) { + Geom::Translate const vert_offset(Geom::Point(0, (old_height.value("px") - h.value("px")))); + doc->getRoot()->translateChildItems(vert_offset); + } + DocumentUndo::done(doc, SP_VERB_NONE, _("Set page size")); + } + + if ( w != h ) { + _landscapeButton.set_sensitive(true); + _portraitButton.set_sensitive (true); + _landscape = ( w > h ); + _landscapeButton.set_active(_landscape ? true : false); + _portraitButton.set_active (_landscape ? false : true); + } else { + _landscapeButton.set_sensitive(false); + _portraitButton.set_sensitive (false); + } + + if (changeList) + { + Gtk::TreeModel::Row row = (*find_paper_size(w, h)); + if (row) + _paperSizeListSelection->select(row); + } + + _dimensionWidth.setUnit(w.unit->abbr); + _dimensionWidth.setValue (w.quantity); + _dimensionHeight.setUnit(h.unit->abbr); + _dimensionHeight.setValue (h.quantity); + + + _paper_size_list_connection.unblock(); + _landscape_connection.unblock(); + _portrait_connection.unblock(); + _changedw_connection.unblock(); + _changedh_connection.unblock(); + + _called = false; +} + +/** + * Updates the scalar widgets for the fit margins. (Just changes the value + * of the ui widgets to match the xml). + */ +void +PageSizer::updateFitMarginsUI(Inkscape::XML::Node *nv_repr) +{ + if (!_lockMarginUpdate) { + double value = 0.0; + if (sp_repr_get_double(nv_repr, "fit-margin-top", &value)) { + _marginTop.setValue(value); + } + if (sp_repr_get_double(nv_repr, "fit-margin-left", &value)) { + _marginLeft.setValue(value); + } + if (sp_repr_get_double(nv_repr, "fit-margin-right", &value)) { + _marginRight.setValue(value); + } + if (sp_repr_get_double(nv_repr, "fit-margin-bottom", &value)) { + _marginBottom.setValue(value); + } + } +} + + +/** + * Returns an iterator pointing to a row in paperSizeListStore which + * contains a paper of the specified size, or + * paperSizeListStore->children().end() if no such paper exists. + * + * The code is not tested for the case where w and h have different units. + */ +Gtk::ListStore::iterator +PageSizer::find_paper_size (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h) const +{ + // The code below assumes that w < h, so make sure that's the case: + if ( h < w ) { + std::swap(h,w); + } + + std::map<Glib::ustring, PaperSize>::const_iterator iter; + for (iter = _paperSizeTable.begin() ; + iter != _paperSizeTable.end() ; ++iter) { + PaperSize paper = iter->second; + Inkscape::Util::Quantity smallX (paper.smaller, paper.unit); + Inkscape::Util::Quantity largeX (paper.larger, paper.unit); + + // account for landscape formats (e.g. business cards) + if (largeX < smallX) { + std::swap(largeX, smallX); + } + + if ( are_near(w, smallX, 0.1) && are_near(h, largeX, 0.1) ) { + Gtk::ListStore::iterator p = _paperSizeListStore->children().begin(); + Gtk::ListStore::iterator pend = _paperSizeListStore->children().end(); + // We need to search paperSizeListStore explicitly for the + // specified paper size because it is sorted in a different + // way than paperSizeTable (which is sorted alphabetically) + for ( ; p != pend; ++p) { + if ((*p)[_paperSizeListColumns.nameColumn] == paper.name) { + return p; + } + } + } + } + return _paperSizeListStore->children().end(); +} + + + +/** + * Tell the desktop to fit the page size to the selection or drawing. + */ +void +PageSizer::fire_fit_canvas_to_selection_or_drawing() +{ + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (!dt) { + return; + } + SPDocument *doc; + SPNamedView *nv; + Inkscape::XML::Node *nv_repr; + + if ((doc = SP_ACTIVE_DESKTOP->getDocument()) + && (nv = sp_document_namedview(doc, nullptr)) + && (nv_repr = nv->getRepr())) { + _lockMarginUpdate = true; + sp_repr_set_svg_double(nv_repr, "fit-margin-top", _marginTop.getValue()); + sp_repr_set_svg_double(nv_repr, "fit-margin-left", _marginLeft.getValue()); + sp_repr_set_svg_double(nv_repr, "fit-margin-right", _marginRight.getValue()); + sp_repr_set_svg_double(nv_repr, "fit-margin-bottom", _marginBottom.getValue()); + _lockMarginUpdate = false; + } + + Verb *verb = Verb::get( SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING ); + if (verb) { + SPAction *action = verb->get_action(Inkscape::ActionContext(dt)); + if (action) { + sp_action_perform(action, nullptr); + } + } +} + + + +/** + * Paper Size list callback for when a user changes the selection + */ +void +PageSizer::on_paper_size_list_changed() +{ + //Glib::ustring name = _paperSizeList.get_active_text(); + Gtk::TreeModel::iterator miter = _paperSizeListSelection->get_selected(); + if(!miter) + { + //error? + return; + } + Gtk::TreeModel::Row row = *miter; + Glib::ustring name = row[_paperSizeListColumns.nameColumn]; + std::map<Glib::ustring, PaperSize>::const_iterator piter = + _paperSizeTable.find(name); + if (piter == _paperSizeTable.end()) { + g_warning("paper size '%s' not found in table", name.c_str()); + return; + } + PaperSize paper = piter->second; + Inkscape::Util::Quantity w = Inkscape::Util::Quantity(paper.smaller, paper.unit); + Inkscape::Util::Quantity h = Inkscape::Util::Quantity(paper.larger, paper.unit); + + if ( w > h ) { + // enforce landscape mode if this is desired for the given page format + _landscape = true; + } else { + // otherwise we keep the current mode + _landscape = _landscapeButton.get_active(); + } + + if ((_landscape && (w < h)) || (!_landscape && (w > h))) + setDim (h, w, false); + else + setDim (w, h, false); + +} + + +/** + * Portrait button callback + */ +void +PageSizer::on_portrait() +{ + if (!_portraitButton.get_active()) + return; + Inkscape::Util::Quantity w = Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionWidth.getUnit()); + Inkscape::Util::Quantity h = Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionHeight.getUnit()); + if (h < w) { + setDim (h, w); + } +} + + +/** + * Landscape button callback + */ +void +PageSizer::on_landscape() +{ + if (!_landscapeButton.get_active()) + return; + Inkscape::Util::Quantity w = Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionWidth.getUnit()); + Inkscape::Util::Quantity h = Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionHeight.getUnit()); + if (w < h) { + setDim (h, w); + } +} + + +/** + * Update scale widgets + */ +void +PageSizer::updateScaleUI() +{ + + static bool _called = false; + if (_called) { + return; + } + + _called = true; + + _changeds_connection.block(); + _changedvx_connection.block(); + _changedvy_connection.block(); + _changedvw_connection.block(); + _changedvh_connection.block(); + + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (dt) { + SPDocument *doc = dt->getDocument(); + + // Update scale + Geom::Scale scale = doc->getDocumentScale(); + SPNamedView *nv = dt->getNamedView(); + + std::stringstream ss; + ss << _("User units per ") << nv->display_units->abbr << "." ; + _scaleLabel.set_text( ss.str() ); + + if( !_lockScaleUpdate ) { + + double scaleX_inv = + Inkscape::Util::Quantity::convert( scale[Geom::X], "px", nv->display_units ); + if( scaleX_inv > 0 ) { + _scaleX.setValue(1.0/scaleX_inv); + } else { + // Should never happen + std::cerr << "PageSizer::updateScaleUI(): Invalid scale value: " << scaleX_inv << std::endl; + _scaleX.setValue(1.0); + } + } + + { // Don't need to lock as scaleY widget not linked to callback. + double scaleY_inv = + Inkscape::Util::Quantity::convert( scale[Geom::Y], "px", nv->display_units ); + if( scaleY_inv > 0 ) { + _scaleY.setValue(1.0/scaleY_inv); + } else { + // Should never happen + std::cerr << "PageSizer::updateScaleUI(): Invalid scale value: " << scaleY_inv << std::endl; + _scaleY.setValue(1.0); + } + } + + if( !_lockViewboxUpdate ) { + Geom::Rect viewBox = doc->getViewBox(); + _viewboxX.setValue( viewBox.min()[Geom::X] ); + _viewboxY.setValue( viewBox.min()[Geom::Y] ); + _viewboxW.setValue( viewBox.width() ); + _viewboxH.setValue( viewBox.height() ); + } + + } else { + // Should never happen + std::cerr << "PageSizer::updateScaleUI(): No active desktop." << std::endl; + _scaleLabel.set_text( "Unknown scale" ); + } + + _changeds_connection.unblock(); + _changedvx_connection.unblock(); + _changedvy_connection.unblock(); + _changedvw_connection.unblock(); + _changedvh_connection.unblock(); + + _called = false; +} + + +/** + * Callback for the dimension widgets + */ +void +PageSizer::on_value_changed() +{ + if (_widgetRegistry->isUpdating()) return; + if (_unit != _dimensionUnits.getUnit()->abbr) return; + setDim (Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionUnits.getUnit()), + Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionUnits.getUnit())); +} + +void +PageSizer::on_units_changed() +{ + if (_widgetRegistry->isUpdating()) return; + _unit = _dimensionUnits.getUnit()->abbr; + setDim (Inkscape::Util::Quantity(_dimensionWidth.getValue(""), _dimensionUnits.getUnit()), + Inkscape::Util::Quantity(_dimensionHeight.getValue(""), _dimensionUnits.getUnit()), + true, false); +} + +/** + * Callback for scale widgets + */ +void +PageSizer::on_scale_changed() +{ + if (_widgetRegistry->isUpdating()) return; + + double value = _scaleX.getValue(); + if( value > 0 ) { + + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (dt) { + SPDocument *doc = dt->getDocument(); + SPNamedView *nv = dt->getNamedView(); + + double scaleX_inv = Inkscape::Util::Quantity(1.0/value, nv->display_units ).value("px"); + + _lockScaleUpdate = true; + doc->setDocumentScale( 1.0/scaleX_inv ); + updateScaleUI(); + _lockScaleUpdate = false; + DocumentUndo::done(doc, SP_VERB_NONE, _("Set page scale")); + } + } +} + +/** + * Callback for viewbox widgets + */ +void +PageSizer::on_viewbox_changed() +{ + if (_widgetRegistry->isUpdating()) return; + + double viewboxX = _viewboxX.getValue(); + double viewboxY = _viewboxY.getValue(); + double viewboxW = _viewboxW.getValue(); + double viewboxH = _viewboxH.getValue(); + + if( viewboxW > 0 && viewboxH > 0) { + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (dt) { + SPDocument *doc = dt->getDocument(); + _lockViewboxUpdate = true; + doc->setViewBox( Geom::Rect::from_xywh( viewboxX, viewboxY, viewboxW, viewboxH ) ); + updateScaleUI(); + _lockViewboxUpdate = false; + DocumentUndo::done(doc, SP_VERB_NONE, _("Set 'viewBox'")); + } + } else { + std::cerr + << "PageSizer::on_viewbox_changed(): width and height must both be greater than zero." + << std::endl; + } +} + +void +PageSizer::on_margin_lock_changed() +{ + if (_marginLock.get_active()) { + _lock_icon.set_from_icon_name("object-locked", Gtk::ICON_SIZE_LARGE_TOOLBAR); + double left = _marginLeft.getValue(); + double right = _marginRight.getValue(); + double top = _marginTop.getValue(); + //double bottom = _marginBottom.getValue(); + if (Geom::are_near(left,right)) { + if (Geom::are_near(left, top)) { + on_margin_changed(&_marginBottom); + } else { + on_margin_changed(&_marginTop); + } + } else { + if (Geom::are_near(left, top)) { + on_margin_changed(&_marginRight); + } else { + on_margin_changed(&_marginLeft); + } + } + } else { + _lock_icon.set_from_icon_name("object-unlocked", Gtk::ICON_SIZE_LARGE_TOOLBAR); + } +} + +void +PageSizer::on_margin_changed(RegisteredScalar* widg) +{ + double value = widg->getValue(); + if (_widgetRegistry->isUpdating()) return; + if (_marginLock.get_active() && !_lockMarginUpdate) { + _lockMarginUpdate = true; + _marginLeft.setValue(value); + _marginRight.setValue(value); + _marginTop.setValue(value); + _marginBottom.setValue(value); + _lockMarginUpdate = false; + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/page-sizer.h b/src/ui/widget/page-sizer.h new file mode 100644 index 0000000..b399835 --- /dev/null +++ b/src/ui/widget/page-sizer.h @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_PAGE_SIZER_H +#define INKSCAPE_UI_WIDGET_PAGE_SIZER_H + +#include <cstddef> +#include "ui/widget/registered-widget.h" +#include <sigc++/sigc++.h> + +#include "util/units.h" + +#include <gtkmm/expander.h> +#include <gtkmm/frame.h> +#include <gtkmm/grid.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/radiobutton.h> + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace UI { +namespace Widget { + +class Registry; + +/** + * Data class used to store common paper dimensions. Used to make + * PageSizer's _paperSizeTable. + */ +class PaperSize +{ +public: + + /** + * Default constructor + */ + PaperSize() + { init(); } + + /** + * Main constructor. Use this one. + */ + PaperSize(const Glib::ustring &nameArg, + double smallerArg, + double largerArg, + Inkscape::Util::Unit const *unitArg) + { + name = nameArg; + smaller = smallerArg; + larger = largerArg; + unit = unitArg; + } + + /** + * Copy constructor + */ + PaperSize(const PaperSize &other) + { assign(other); } + + /** + * Assignment operator + */ + PaperSize &operator=(const PaperSize &other) + { assign(other); return *this; } + + /** + * Destructor + */ + virtual ~PaperSize() + = default; + + /** + * Name of this paper specification + */ + Glib::ustring name; + + /** + * The lesser of the two dimensions + */ + double smaller; + + /** + * The greater of the two dimensions + */ + double larger; + + /** + * The units (px, pt, mm, etc) of this specification + */ + Inkscape::Util::Unit const *unit; /// pointer to object in UnitTable, do not delete + +private: + + void init() + { + name = ""; + smaller = 0.0; + larger = 0.0; + unit = unit_table.getUnit("px"); + } + + void assign(const PaperSize &other) + { + name = other.name; + smaller = other.smaller; + larger = other.larger; + unit = other.unit; + } + +}; + + + + + +/** + * A compound widget that allows the user to select the desired + * page size. This widget is used in DocumentPreferences + */ +class PageSizer : public Gtk::VBox +{ +public: + + /** + * Constructor + */ + PageSizer(Registry & _wr); + + /** + * Destructor + */ + ~PageSizer() override; + + /** + * Set up or reset this widget + */ + void init (); + + /** + * Set the page size to the given dimensions. If 'changeList' is + * true, then reset the paper size list to the closest match + */ + void setDim (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h, bool changeList=true, bool changeSize=true); + + /** + * Updates the scalar widgets for the fit margins. (Just changes the value + * of the ui widgets to match the xml). + */ + void updateFitMarginsUI(Inkscape::XML::Node *nv_repr); + + /** + * Updates the margin widgets. If lock widget is active + */ + void on_margin_changed(RegisteredScalar* widg); + + void on_margin_lock_changed(); + + /** + * Updates the scale widgets. (Just changes the values of the ui widgets.) + */ + void updateScaleUI(); + +protected: + + /** + * Our handy table of all 'standard' paper sizes. + */ + std::map<Glib::ustring, PaperSize> _paperSizeTable; + + /** + * Find the closest standard paper size in the table, to the + */ + Gtk::ListStore::iterator find_paper_size (Inkscape::Util::Quantity w, Inkscape::Util::Quantity h) const; + + void fire_fit_canvas_to_selection_or_drawing(); + + //### The Paper Size selection list + Gtk::HBox _paperSizeListBox; + Gtk::Label _paperSizeListLabel; + class PaperSizeColumns : public Gtk::TreeModel::ColumnRecord + { + public: + PaperSizeColumns() + { add(nameColumn); add(descColumn); } + Gtk::TreeModelColumn<Glib::ustring> nameColumn; + Gtk::TreeModelColumn<Glib::ustring> descColumn; + }; + + PaperSizeColumns _paperSizeListColumns; + Glib::RefPtr<Gtk::ListStore> _paperSizeListStore; + Gtk::TreeView _paperSizeList; + Glib::RefPtr<Gtk::TreeSelection> _paperSizeListSelection; + Gtk::ScrolledWindow _paperSizeListScroller; + //callback + void on_paper_size_list_changed(); + sigc::connection _paper_size_list_connection; + + //### Portrait or landscape orientation + Gtk::HBox _orientationBox; + Gtk::Label _orientationLabel; + Gtk::RadioButton _portraitButton; + Gtk::RadioButton _landscapeButton; + //callbacks + void on_portrait(); + void on_landscape(); + sigc::connection _portrait_connection; + sigc::connection _landscape_connection; + + //### Custom size frame + Gtk::Frame _customFrame; + Gtk::Grid _customDimTable; + + RegisteredUnitMenu _dimensionUnits; + RegisteredScalarUnit _dimensionWidth; + RegisteredScalarUnit _dimensionHeight; + + //### Fit Page options + Gtk::Expander _fitPageMarginExpander; + + Gtk::Grid _marginTable; + Gtk::Box _marginBox; + Gtk::Label _marginLabel; + RegisteredToggleButton _marginLock; + Gtk::Image _lock_icon; + RegisteredScalar _marginTop; + RegisteredScalar _marginLeft; + RegisteredScalar _marginRight; + RegisteredScalar _marginBottom; + Gtk::Button _fitPageButton; + bool _lockMarginUpdate; + + // Document scale + Gtk::Frame _scaleFrame; + Gtk::Grid _scaleTable; + + Gtk::Label _scaleLabel; + RegisteredScalar _scaleX; + RegisteredScalar _scaleY; + bool _lockScaleUpdate; + + // Viewbox + Gtk::Expander _viewboxExpander; + Gtk::Grid _viewboxTable; + + RegisteredScalar _viewboxX; + RegisteredScalar _viewboxY; + RegisteredScalar _viewboxW; + RegisteredScalar _viewboxH; + Gtk::Box _viewboxSpacer; + bool _lockViewboxUpdate; + + //callback + void on_value_changed(); + void on_units_changed(); + void on_scale_changed(); + void on_viewbox_changed(); + sigc::connection _changedw_connection; + sigc::connection _changedh_connection; + sigc::connection _changedu_connection; + sigc::connection _changeds_connection; + sigc::connection _changedvx_connection; + sigc::connection _changedvy_connection; + sigc::connection _changedvw_connection; + sigc::connection _changedvh_connection; + sigc::connection _changedlk_connection; + sigc::connection _changedmt_connection; + sigc::connection _changedmb_connection; + sigc::connection _changedml_connection; + sigc::connection _changedmr_connection; + + Registry *_widgetRegistry; + + //### state - whether we are currently landscape or portrait + bool _landscape; + + Glib::ustring _unit; + +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + + +#endif // INKSCAPE_UI_WIDGET_PAGE_SIZER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/pages-skeleton.h b/src/ui/widget/pages-skeleton.h new file mode 100644 index 0000000..c62e03e --- /dev/null +++ b/src/ui/widget/pages-skeleton.h @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * List of paper sizes + */ +/* + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Bob Jamison <ishmal@users.sf.net> + * Abhishek Sharma + * + see git history + * + * Copyright (C) 2000 - 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PAGES_SKELETON_H +#define SEEN_PAGES_SKELETON_H + + + /** \note + * The ISO page sizes in the table below differ from ghostscript's idea of page sizes (by + * less than 1pt). Being off by <1pt should be OK for most purposes, but may cause fuzziness + * (antialiasing) problems when printing to 72dpi or 144dpi printers or bitmap files due to + * postscript's different coordinate system (y=0 meaning bottom of page in postscript and top + * of page in SVG). I haven't looked into whether this does in fact cause fuzziness, I merely + * note the possibility. Rounding done by extension/internal/ps.cpp (e.g. floor/ceil calls) + * will also affect whether fuzziness occurs. + * + * The remainder of this comment discusses the origin of the numbers used for ISO page sizes in + * this table and in ghostscript. + * + * The versions here, in mm, are the official sizes according to + * <a href="http://en.wikipedia.org/wiki/Paper_sizes">http://en.wikipedia.org/wiki/Paper_sizes</a> + * at 2005-01-25. (The ISO entries in the below table + * were produced mechanically from the table on that page.) + * + * (The rule seems to be that A0, B0, ..., D0. sizes are rounded to the nearest number of mm + * from the "theoretical size" (i.e. 1000 * sqrt(2) or pow(2.0, .25) or the like), whereas + * going from e.g. A0 to A1 always take the floor of halving -- which by chance coincides + * exactly with flooring the "theoretical size" for n != 0 instead of the rounding to nearest + * done for n==0.) + * + * Ghostscript paper sizes are given in gs_statd.ps according to gs(1). gs_statd.ps always + * uses an integer number ofpt: sometimes gs_statd.ps rounds to nearest (e.g. a1), sometimes + * floors (e.g. a10), sometimes ceils (e.g. a8). + * + * I'm not sure how ghostscript's gs_statd.ps was calculated: it isn't just rounding the + * "theoretical size" of each page topt (see a0), nor is it rounding the a0 size times an + * appropriate power of two (see a1). Possibly it was prepared manually, with a human applying + * inconsistent rounding rules when converting from mm to pt. + */ + /** \todo + * Should we include the JIS B series (used in Japan) + * (JIS B0 is sometimes called JB0, and similarly for JB1 etc)? + * Should we exclude B7--B10 and A7--10 to make the list smaller ? + * Should we include any of the ISO C, D and E series (see below) ? + */ + + + + /* See http://www.hbp.com/content/PCR_envelopes.cfm for a much larger list of US envelope + sizes. */ + /* Note that `Folio' (used in QPrinter/KPrinter) is deliberately absent from this list, as it + means different sizes to different people: different people may expect the width to be + either 8, 8.25 or 8.5 inches, and the height to be either 13 or 13.5 inches, even + restricting our interpretation to foolscap folio. If you wish to introduce a folio-like + page size to the list, then please consider using a name more specific than just `Folio' or + `Foolscap Folio'. */ + +static char const pages_skeleton[] = R"(#Inkscape page sizes +#NAME, WIDTH, HEIGHT, UNIT +A4, 210, 297, mm +US Letter, 8.5, 11, in +US Legal, 8.5, 14, in +US Executive, 7.25, 10.5, in +A0, 841, 1189, mm +A1, 594, 841, mm +A2, 420, 594, mm +A3, 297, 420, mm +A5, 148, 210, mm +A6, 105, 148, mm +A7, 74, 105, mm +A8, 52, 74, mm +A9, 37, 52, mm +A10, 26, 37, mm +B0, 1000, 1414, mm +B1, 707, 1000, mm +B2, 500, 707, mm +B3, 353, 500, mm +B4, 250, 353, mm +B5, 176, 250, mm +B6, 125, 176, mm +B7, 88, 125, mm +B8, 62, 88, mm +B9, 44, 62, mm +B10, 31, 44, mm +C0, 917, 1297, mm +C1, 648, 917, mm +C2, 458, 648, mm +C3, 324, 458, mm +C4, 229, 324, mm +C5, 162, 229, mm +C6, 114, 162, mm +C7, 81, 114, mm +C8, 57, 81, mm +C9, 40, 57, mm +C10, 28, 40, mm +D1, 545, 771, mm +D2, 385, 545, mm +D3, 272, 385, mm +D4, 192, 272, mm +D5, 136, 192, mm +D6, 96, 136, mm +D7, 68, 96, mm +E3, 400, 560, mm +E4, 280, 400, mm +E5, 200, 280, mm +E6, 140, 200, mm +CSE, 462, 649, pt +US #10 Envelope, 9.5, 4.125, in +DL Envelope, 220, 110, mm +Ledger/Tabloid, 11, 17, in +Banner 468x60, 468, 60, px +Icon 16x16, 16, 16, px +Icon 32x32, 32, 32, px +Icon 48x48, 48, 48, px +ID Card (ISO 7810), 85.60, 53.98, mm +Business Card (US), 3.5, 2, in +Business Card (Europe), 85, 55, mm +Business Card (Aus/NZ), 90, 55, mm +Arch A, 9, 12, in +Arch B, 12, 18, in +Arch C, 18, 24, in +Arch D, 24, 36, in +Arch E, 36, 48, in +Arch E1, 30, 42, in +Video SD / PAL, 768, 576, px +Video SD-Widescreen / PAL, 1024, 576, px +Video SD / NTSC, 544, 480, px +Video SD-Widescreen / NTSC, 872, 486, px +Video HD 720p, 1280, 720, px +Video HD 1080p, 1920, 1080, px +Video DCI 2k (Full Frame), 2048, 1080, px +Video UHD 4k, 3840, 2160, px +Video DCI 4k (Full Frame), 4096, 2160, px +Video UHD 8k, 7680, 4320, px +)"; + +#endif diff --git a/src/ui/widget/panel.cpp b/src/ui/widget/panel.cpp new file mode 100644 index 0000000..8ca08a0 --- /dev/null +++ b/src/ui/widget/panel.cpp @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Jon A. Cruz <jon@joncruz.org> + * Gustav Broberg <broberg@kth.se> + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2005 Jon A. Cruz + * Copyright (C) 2007 Gustav Broberg + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/dialog.h> // for Gtk::RESPONSE_* + +#include <glibmm/i18n.h> + +#include "panel.h" +#include "desktop.h" + +#include "inkscape.h" +#include "preview.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +void Panel::prep() { + GtkIconSize sizes[] = { + GTK_ICON_SIZE_MENU, + GTK_ICON_SIZE_MENU, + GTK_ICON_SIZE_SMALL_TOOLBAR, + GTK_ICON_SIZE_BUTTON, + GTK_ICON_SIZE_DND, // Not used by options, but included to make the last size larger + GTK_ICON_SIZE_DIALOG + }; + Preview::set_size_mappings( G_N_ELEMENTS(sizes), sizes ); +} + +Panel::Panel(gchar const *prefs_path, int verb_num) : + _prefs_path(prefs_path), + _desktop(SP_ACTIVE_DESKTOP), + _verb_num(verb_num), + _action_area(nullptr) +{ + set_name("InkscapePanel"); + set_orientation(Gtk::ORIENTATION_VERTICAL); + + signalResponse().connect(sigc::mem_fun(*this, &Panel::_handleResponse)); + signalActivateDesktop().connect(sigc::mem_fun(*this, &Panel::setDesktop)); + + pack_start(_contents, true, true); + + show_all_children(); +} + +Panel::~Panel() += default; + +void Panel::present() +{ + _signal_present.emit(); +} + +sigc::signal<void, int> &Panel::signalResponse() +{ + return _signal_response; +} + +sigc::signal<void> &Panel::signalPresent() +{ + return _signal_present; +} + +gchar const *Panel::getPrefsPath() const +{ + return _prefs_path.data(); +} + +int const &Panel::getVerb() const +{ + return _verb_num; +} + +void Panel::setDesktop(SPDesktop *desktop) +{ + _desktop = desktop; +} + +void Panel::_apply() +{ + g_warning("Apply button clicked for panel [Panel::_apply()]"); +} + +Gtk::Button *Panel::addResponseButton(const Glib::ustring &button_text, int response_id, bool pack_start) +{ + // Create a button box for the response buttons if it's the first button to be added + if (!_action_area) { + _action_area = new Gtk::ButtonBox(); + _action_area->set_layout(Gtk::BUTTONBOX_END); + _action_area->set_spacing(6); + _action_area->set_border_width(4); + pack_end(*_action_area, Gtk::PACK_SHRINK, 0); + } + + Gtk::Button *button = new Gtk::Button(button_text, true); + + _action_area->pack_end(*button); + + if (pack_start) { + _action_area->set_child_secondary(*button , true); + } + + if (response_id != 0) { + // Re-emit clicked signals as response signals + button->signal_clicked().connect(sigc::bind(_signal_response.make_slot(), response_id)); + _response_map[response_id] = button; + } + + return button; +} + +void Panel::setResponseSensitive(int response_id, bool setting) +{ + if (_response_map[response_id]) + _response_map[response_id]->set_sensitive(setting); +} + +sigc::signal<void, SPDesktop *, SPDocument *> & +Panel::signalDocumentReplaced() +{ + return _signal_document_replaced; +} + +sigc::signal<void, SPDesktop *> & +Panel::signalActivateDesktop() +{ + return _signal_activate_desktop; +} + +sigc::signal<void, SPDesktop *> & +Panel::signalDeactiveDesktop() +{ + return _signal_deactive_desktop; +} + +void Panel::_handleResponse(int response_id) +{ + switch (response_id) { + case Gtk::RESPONSE_APPLY: { + _apply(); + break; + } + } +} + +Inkscape::Selection *Panel::_getSelection() +{ + return _desktop->getSelection(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/panel.h b/src/ui/widget/panel.h new file mode 100644 index 0000000..3f08218 --- /dev/null +++ b/src/ui/widget/panel.h @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2004 Bryce Harrington + * Copyright (C) 2005 Jon A. Cruz + * Copyright (C) 2012 Kris De Gussem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UI_WIDGET_PANEL_H +#define SEEN_INKSCAPE_UI_WIDGET_PANEL_H + +#include <gtkmm/box.h> +#include <map> + +class SPDesktop; +class SPDocument; + +namespace Gtk { + class Button; + class ButtonBox; +} + +struct InkscapeApplication; + +namespace Inkscape { + +class Selection; + +namespace UI { + +namespace Widget { + +/** + * A generic dockable container. + * + * Inkscape::UI::Widget::Panel is a base class from which dockable dialogs + * are created. A new dockable dialog is created by deriving a class from panel. + * Child widgets are private data members of Panel (no need to use pointers and + * new). + * + * @see UI::Dialog::DesktopTracker to handle desktop change, selection change and selected object modifications. + * @see UI::Dialog::DialogManager manages the dialogs within inkscape. + */ +class Panel : public Gtk::Box { +public: + static void prep(); + + /** + * Construct a Panel. + * + * @param prefs_path characteristic path to load/save dialog position. + * @param verb_num the dialog verb. + */ + Panel(gchar const *prefs_path = nullptr, int verb_num = 0); + ~Panel() override; + + gchar const *getPrefsPath() const; + + int const &getVerb() const; + + virtual void present(); //< request to be present + + void restorePanelPrefs(); + + virtual void setDesktop(SPDesktop *desktop); + SPDesktop *getDesktop() { return _desktop; } + + /* Signal accessors */ + virtual sigc::signal<void, int> &signalResponse(); + virtual sigc::signal<void> &signalPresent(); + + /* Methods providing a Gtk::Dialog like interface for adding buttons that emit Gtk::RESPONSE + * signals on click. */ + Gtk::Button* addResponseButton (const Glib::ustring &button_text, int response_id, bool pack_start=false); + void setResponseSensitive(int response_id, bool setting); + + /* Return signals. Signals emitted by PanelDialog. */ + virtual sigc::signal<void, SPDesktop *, SPDocument *> &signalDocumentReplaced(); + virtual sigc::signal<void, SPDesktop *> &signalActivateDesktop(); + virtual sigc::signal<void, SPDesktop *> &signalDeactiveDesktop(); + +protected: + /** + * Returns a pointer to a Gtk::Box containing the child widgets. + */ + Gtk::Box *_getContents() { return &_contents; } + virtual void _apply(); + + virtual void _handleResponse(int response_id); + + /* Helper methods */ + Inkscape::Selection *_getSelection(); + + /** + * Stores characteristic path for loading/saving the dialog position. + */ + Glib::ustring const _prefs_path; + + /* Signals */ + sigc::signal<void, int> _signal_response; + sigc::signal<void> _signal_present; + sigc::signal<void, SPDesktop *, SPDocument *> _signal_document_replaced; + sigc::signal<void, SPDesktop *> _signal_activate_desktop; + sigc::signal<void, SPDesktop *> _signal_deactive_desktop; + +private: + SPDesktop *_desktop; + + int _verb_num; + + Gtk::VBox _contents; + Gtk::ButtonBox *_action_area; //< stores response buttons + + /* A map to store which widget that emits a certain response signal */ + typedef std::map<int, Gtk::Widget *> ResponseMap; + ResponseMap _response_map; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_UI_WIDGET_PANEL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/point.cpp b/src/ui/widget/point.cpp new file mode 100644 index 0000000..e0d6eed --- /dev/null +++ b/src/ui/widget/point.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2007 Authors + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/point.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic), + xwidget("X:",""), + ywidget("Y:","") +{ + static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::VBox*>(_widget)->show_all_children(); +} + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic), + xwidget("X:","", digits), + ywidget("Y:","", digits) +{ + static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::VBox*>(_widget)->show_all_children(); +} + +Point::Point(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::VBox(), suffix, icon, mnemonic), + xwidget("X:","", adjust, digits), + ywidget("Y:","", adjust, digits) +{ + static_cast<Gtk::VBox*>(_widget)->pack_start(xwidget, true, true); + static_cast<Gtk::VBox*>(_widget)->pack_start(ywidget, true, true); + static_cast<Gtk::VBox*>(_widget)->show_all_children(); +} + +unsigned Point::getDigits() const +{ + return xwidget.getDigits(); +} + +double Point::getStep() const +{ + return xwidget.getStep(); +} + +double Point::getPage() const +{ + return xwidget.getPage(); +} + +double Point::getRangeMin() const +{ + return xwidget.getRangeMin(); +} + +double Point::getRangeMax() const +{ + return xwidget.getRangeMax(); +} + +double Point::getXValue() const +{ + return xwidget.getValue(); +} + +double Point::getYValue() const +{ + return ywidget.getValue(); +} + +Geom::Point Point::getValue() const +{ + return Geom::Point( getXValue() , getYValue() ); +} + +int Point::getXValueAsInt() const +{ + return xwidget.getValueAsInt(); +} + +int Point::getYValueAsInt() const +{ + return ywidget.getValueAsInt(); +} + + +void Point::setDigits(unsigned digits) +{ + xwidget.setDigits(digits); + ywidget.setDigits(digits); +} + +void Point::setIncrements(double step, double page) +{ + xwidget.setIncrements(step, page); + ywidget.setIncrements(step, page); +} + +void Point::setRange(double min, double max) +{ + xwidget.setRange(min, max); + ywidget.setRange(min, max); +} + +void Point::setValue(Geom::Point const & p) +{ + xwidget.setValue(p[0]); + ywidget.setValue(p[1]); +} + +void Point::update() +{ + xwidget.update(); + ywidget.update(); +} + +bool Point::setProgrammatically() +{ + return (xwidget.setProgrammatically || ywidget.setProgrammatically); +} + +void Point::clearProgrammatically() +{ + xwidget.setProgrammatically = false; + ywidget.setProgrammatically = false; +} + + +Glib::SignalProxy0<void> Point::signal_x_value_changed() +{ + return xwidget.signal_value_changed(); +} + +Glib::SignalProxy0<void> Point::signal_y_value_changed() +{ + return ywidget.signal_value_changed(); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/point.h b/src/ui/widget/point.h new file mode 100644 index 0000000..018be5b --- /dev/null +++ b/src/ui/widget/point.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2007 Authors + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_POINT_H +#define INKSCAPE_UI_WIDGET_POINT_H + +#include "ui/widget/labelled.h" +#include <2geom/point.h> +#include "ui/widget/scalar.h" + +namespace Gtk { +class Adjustment; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional icon or suffix, for + * entering arbitrary coordinate values. + */ +class Point : public Labelled +{ +public: + + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Point Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Point( Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Fetches the precision of the spin button. + */ + unsigned getDigits() const; + + /** + * Gets the current step increment used by the spin button. + */ + double getStep() const; + + /** + * Gets the current page increment used by the spin button. + */ + double getPage() const; + + /** + * Gets the minimum range value allowed for the spin button. + */ + double getRangeMin() const; + + /** + * Gets the maximum range value allowed for the spin button. + */ + double getRangeMax() const; + + bool getSnapToTicks() const; + + /** + * Get the value in the spin_button. + */ + double getXValue() const; + + double getYValue() const; + + Geom::Point getValue() const; + + /** + * Get the value spin_button represented as an integer. + */ + int getXValueAsInt() const; + + int getYValueAsInt() const; + + /** + * Sets the precision to be displayed by the spin button. + */ + void setDigits(unsigned digits); + + /** + * Sets the step and page increments for the spin button. + */ + void setIncrements(double step, double page); + + /** + * Sets the minimum and maximum range allowed for the spin button. + */ + void setRange(double min, double max); + + /** + * Sets the value of the spin button. + */ + void setValue(Geom::Point const & p); + + /** + * Manually forces an update of the spin button. + */ + void update(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_x_value_changed(); + + Glib::SignalProxy0<void> signal_y_value_changed(); + + /** + * Check 'setProgrammatically' of both scalar widgets. False if value is changed by user by clicking the widget. + * true if the value was set by setValue, not changed by the user; + * if a callback checks it, it must reset it back to false. + */ + bool setProgrammatically(); + + void clearProgrammatically(); + +protected: + Scalar xwidget; + Scalar ywidget; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_POINT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp new file mode 100644 index 0000000..92c101b --- /dev/null +++ b/src/ui/widget/preferences-widget.cpp @@ -0,0 +1,1023 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Preferences dialog. + * + * Authors: + * Marco Scholten + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004, 2006, 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/convert.h> +#include <glibmm/regex.h> + +#include <gtkmm/box.h> +#include <gtkmm/frame.h> +#include <gtkmm/scale.h> +#include <gtkmm/table.h> + + +#include "desktop.h" +#include "inkscape.h" +#include "message-stack.h" +#include "preferences.h" +#include "selcue.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "io/sys.h" + +#include "ui/dialog/filedialog.h" +#include "ui/icon-loader.h" +#include "ui/widget/preferences-widget.h" + + +#ifdef _WIN32 +#include <windows.h> +#endif + +using namespace Inkscape::UI::Widget; + +namespace Inkscape { +namespace UI { +namespace Widget { + +DialogPage::DialogPage() +{ + set_border_width(12); + + set_orientation(Gtk::ORIENTATION_VERTICAL); + set_column_spacing(12); + set_row_spacing(6); +} + +/** + * Add a widget to the bottom row of the dialog page + * + * \param[in] indent Whether the widget should be indented by one column + * \param[in] label The label text for the widget + * \param[in] widget The widget to add to the page + * \param[in] suffix Text for an optional label at the right of the widget + * \param[in] tip Tooltip text for the widget + * \param[in] expand_widget Whether to expand the widget horizontally + * \param[in] other_widget An optional additional widget to display at the right of the first one + */ +void DialogPage::add_line(bool indent, + Glib::ustring const &label, + Gtk::Widget &widget, + Glib::ustring const &suffix, + const Glib::ustring &tip, + bool expand_widget, + Gtk::Widget *other_widget) +{ + if (tip != "") + widget.set_tooltip_text (tip); + + auto hb = Gtk::manage(new Gtk::Box()); + hb->set_spacing(12); + hb->set_hexpand(true); + hb->pack_start(widget, expand_widget, expand_widget); + + // Pack an additional widget into a box with the widget if desired + if (other_widget) + hb->pack_start(*other_widget, expand_widget, expand_widget); + + hb->set_valign(Gtk::ALIGN_CENTER); + + // Add a label in the first column if provided + if (label != "") + { + Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START, + Gtk::ALIGN_CENTER, true)); + label_widget->set_mnemonic_widget(widget); + label_widget->set_markup(label_widget->get_text()); + + if (indent) { + label_widget->set_margin_start(12); + } + + label_widget->set_valign(Gtk::ALIGN_CENTER); + add(*label_widget); + attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); + } + + // Now add the widget to the bottom of the dialog + if (label == "") + { + if (indent) { + hb->set_margin_start(12); + } + + add(*hb); + + GValue width = G_VALUE_INIT; + g_value_init(&width, G_TYPE_INT); + g_value_set_int(&width, 2); + gtk_container_child_set_property(GTK_CONTAINER(gobj()), GTK_WIDGET(hb->gobj()), "width", &width); + } + + // Add a label on the right of the widget if desired + if (suffix != "") + { + Gtk::Label* suffix_widget = Gtk::manage(new Gtk::Label(suffix , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true)); + suffix_widget->set_markup(suffix_widget->get_text()); + hb->pack_start(*suffix_widget,false,false); + } + +} + +void DialogPage::add_group_header(Glib::ustring name) +{ + if (name != "") + { + Gtk::Label* label_widget = Gtk::manage(new Gtk::Label(Glib::ustring(/*"<span size='large'>*/"<b>") + name + + Glib::ustring("</b>"/*</span>"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true)); + + label_widget->set_use_markup(true); + label_widget->set_valign(Gtk::ALIGN_CENTER); + add(*label_widget); + } +} + +void DialogPage::set_tip(Gtk::Widget& widget, Glib::ustring const &tip) +{ + widget.set_tooltip_text (tip); +} + +void PrefCheckButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + bool default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->set_label(label); + this->set_active( prefs->getBool(_prefs_path, default_value) ); +} + +void PrefCheckButton::on_toggled() +{ + this->changed_signal.emit(this->get_active()); + if (this->get_visible()) //only take action if the user toggled it + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(_prefs_path, this->get_active()); + } +} + +void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member) +{ + _prefs_path = prefs_path; + _value_type = VAL_STRING; + _string_value = string_value; + (void)default_value; + this->set_label(label); + if (group_member) + { + Gtk::RadioButtonGroup rbg = group_member->get_group(); + this->set_group(rbg); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring val = prefs->getString(_prefs_path); + if ( !val.empty() ) + this->set_active(val == _string_value); + else + this->set_active( false ); +} + +void PrefRadioButton::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + int int_value, bool default_value, PrefRadioButton* group_member) +{ + _prefs_path = prefs_path; + _value_type = VAL_INT; + _int_value = int_value; + this->set_label(label); + if (group_member) + { + Gtk::RadioButtonGroup rbg = group_member->get_group(); + this->set_group(rbg); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (default_value) + this->set_active( prefs->getInt(_prefs_path, int_value) == _int_value ); + else + this->set_active( prefs->getInt(_prefs_path, int_value + 1) == _int_value ); +} + +void PrefRadioButton::on_toggled() +{ + this->changed_signal.emit(this->get_active()); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (this->get_visible() && this->get_active() ) //only take action if toggled by user (to active) + { + if ( _value_type == VAL_STRING ) + prefs->setString(_prefs_path, _string_value); + else if ( _value_type == VAL_INT ) + prefs->setInt(_prefs_path, _int_value); + } +} + +void PrefSpinButton::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double /*page_increment*/, + double default_value, bool is_int, bool is_percent) +{ + _prefs_path = prefs_path; + _is_int = is_int; + _is_percent = is_percent; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value; + if (is_int) { + if (is_percent) { + value = 100 * prefs->getDoubleLimited(prefs_path, default_value, lower/100.0, upper/100.0); + } else { + value = (double) prefs->getIntLimited(prefs_path, (int) default_value, (int) lower, (int) upper); + } + } else { + value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + } + + this->set_range (lower, upper); + this->set_increments (step_increment, 0); + this->set_value (value); + this->set_width_chars(6); + if (is_int) + this->set_digits(0); + else if (step_increment < 0.1) + this->set_digits(4); + else + this->set_digits(2); + +} + +void PrefSpinButton::on_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (this->get_visible()) //only take action if user changed value + { + if (_is_int) { + if (_is_percent) { + prefs->setDouble(_prefs_path, this->get_value()/100.0); + } else { + prefs->setInt(_prefs_path, (int) this->get_value()); + } + } else { + prefs->setDouble(_prefs_path, this->get_value()); + } + } +} + +void PrefSpinUnit::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, + double default_value, UnitType unit_type, Glib::ustring const &default_unit) +{ + _prefs_path = prefs_path; + _is_percent = (unit_type == UNIT_TYPE_DIMENSIONLESS); + + resetUnitType(unit_type); + setUnit(default_unit); + setRange (lower, upper); /// @fixme this disregards changes of units + setIncrements (step_increment, 0); + if (step_increment < 0.1) { + setDigits(4); + } else { + setDigits(2); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + Glib::ustring unitstr = prefs->getUnit(prefs_path); + if (unitstr.length() == 0) { + unitstr = default_unit; + // write the assumed unit to preferences: + prefs->setDoubleUnit(_prefs_path, value, unitstr); + } + setValue(value, unitstr); + + signal_value_changed().connect_notify(sigc::mem_fun(*this, &PrefSpinUnit::on_my_value_changed)); +} + +void PrefSpinUnit::on_my_value_changed() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (getWidget()->get_visible()) //only take action if user changed value + { + prefs->setDoubleUnit(_prefs_path, getValue(getUnit()->abbr), getUnit()->abbr); + } +} + +const double ZoomCorrRuler::textsize = 7; +const double ZoomCorrRuler::textpadding = 5; + +ZoomCorrRuler::ZoomCorrRuler(int width, int height) : + _unitconv(1.0), + _border(5) +{ + set_size(width, height); +} + +void ZoomCorrRuler::set_size(int x, int y) +{ + _min_width = x; + _height = y; + set_size_request(x + _border*2, y + _border*2); +} + +// The following two functions are borrowed from 2geom's toy-framework-2; if they are useful in +// other locations, we should perhaps make them (or adapted versions of them) publicly available +static void +draw_text(cairo_t *cr, Geom::Point loc, const char* txt, bool bottom = false, + double fontsize = ZoomCorrRuler::textsize, std::string fontdesc = "Sans") { + PangoLayout* layout = pango_cairo_create_layout (cr); + pango_layout_set_text(layout, txt, -1); + + // set font and size + std::ostringstream sizestr; + sizestr << fontsize; + fontdesc = fontdesc + " " + sizestr.str(); + PangoFontDescription *font_desc = pango_font_description_from_string(fontdesc.c_str()); + pango_layout_set_font_description(layout, font_desc); + pango_font_description_free (font_desc); + + PangoRectangle logical_extent; + pango_layout_get_pixel_extents(layout, nullptr, &logical_extent); + cairo_move_to(cr, loc[Geom::X], loc[Geom::Y] - (bottom ? logical_extent.height : 0)); + pango_cairo_show_layout(cr, layout); +} + +static void +draw_number(cairo_t *cr, Geom::Point pos, double num) { + std::ostringstream number; + number << num; + draw_text(cr, pos, number.str().c_str(), true); +} + +/* + * \arg dist The distance between consecutive minor marks + * \arg major_interval Number of marks after which to draw a major mark + */ +void +ZoomCorrRuler::draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + const double zoomcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0); + double mark = 0; + int i = 0; + while (mark <= _drawing_width) { + cr->move_to(mark, _height); + if ((i % major_interval) == 0) { + // major mark + cr->line_to(mark, 0); + Geom::Point textpos(mark + 3, ZoomCorrRuler::textsize + ZoomCorrRuler::textpadding); + draw_number(cr->cobj(), textpos, dist * i); + } else { + // minor mark + cr->line_to(mark, ZoomCorrRuler::textsize + 2 * ZoomCorrRuler::textpadding); + } + mark += dist * zoomcorr / _unitconv; + ++i; + } +} + +bool +ZoomCorrRuler::on_draw(const Cairo::RefPtr<Cairo::Context>& cr) { + Glib::RefPtr<Gdk::Window> window = get_window(); + + int w = window->get_width(); + _drawing_width = w - _border * 2; + + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->set_fill_rule(Cairo::FILL_RULE_WINDING); + cr->rectangle(0, 0, w, _height + _border*2); + cr->fill(); + + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->set_line_width(0.5); + + cr->translate(_border, _border); // so that we have a small white border around the ruler + cr->move_to (0, _height); + cr->line_to (_drawing_width, _height); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring abbr = prefs->getString("/options/zoomcorrection/unit"); + if (abbr == "cm") { + draw_marks(cr, 0.1, 10); + } else if (abbr == "in") { + draw_marks(cr, 0.25, 4); + } else if (abbr == "mm") { + draw_marks(cr, 10, 10); + } else if (abbr == "pc") { + draw_marks(cr, 1, 10); + } else if (abbr == "pt") { + draw_marks(cr, 10, 10); + } else if (abbr == "px") { + draw_marks(cr, 10, 10); + } else { + draw_marks(cr, 1, 1); + } + cr->stroke(); + + return true; +} + + +void +ZoomCorrRulerSlider::on_slider_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/options/zoomcorrection/value", _slider->get_value() / 100.0); + _sb.set_value(_slider->get_value()); + _ruler.queue_draw(); + freeze = false; + } +} + +void +ZoomCorrRulerSlider::on_spinbutton_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/options/zoomcorrection/value", _sb.get_value() / 100.0); + _slider->set_value(_sb.get_value()); + _ruler.queue_draw(); + freeze = false; + } +} + +void +ZoomCorrRulerSlider::on_unit_changed() { + if (GPOINTER_TO_INT(_unit.get_data("sensitive")) == 0) { + // when the unit menu is initialized, the unit is set to the default but + // it needs to be reset later so we don't perform the change in this case + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString("/options/zoomcorrection/unit", _unit.getUnitAbbr()); + double conv = _unit.getConversion(_unit.getUnitAbbr(), "px"); + _ruler.set_unit_conversion(conv); + if (_ruler.get_visible()) { + _ruler.queue_draw(); + } +} + +bool ZoomCorrRulerSlider::on_mnemonic_activate ( bool group_cycling ) +{ + return _sb.mnemonic_activate ( group_cycling ); +} + + +void +ZoomCorrRulerSlider::init(int ruler_width, int ruler_height, double lower, double upper, + double step_increment, double page_increment, double default_value) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited("/options/zoomcorrection/value", default_value, lower, upper) * 100.0; + + freeze = false; + + _ruler.set_size(ruler_width, ruler_height); + + _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + + _slider->set_size_request(_ruler.width(), -1); + _slider->set_range (lower, upper); + _slider->set_increments (step_increment, page_increment); + _slider->set_value (value); + _slider->set_digits(2); + + _slider->signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_slider_value_changed)); + _sb.signal_value_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_spinbutton_value_changed)); + _unit.signal_changed().connect(sigc::mem_fun(*this, &ZoomCorrRulerSlider::on_unit_changed)); + + _sb.set_range (lower, upper); + _sb.set_increments (step_increment, 0); + _sb.set_value (value); + _sb.set_digits(2); + _sb.set_halign(Gtk::ALIGN_CENTER); + _sb.set_valign(Gtk::ALIGN_END); + + _unit.set_data("sensitive", GINT_TO_POINTER(0)); + _unit.setUnitType(UNIT_TYPE_LINEAR); + _unit.set_data("sensitive", GINT_TO_POINTER(1)); + _unit.setUnit(prefs->getString("/options/zoomcorrection/unit")); + _unit.set_halign(Gtk::ALIGN_CENTER); + _unit.set_valign(Gtk::ALIGN_END); + + auto table = Gtk::manage(new Gtk::Grid()); + table->attach(*_slider, 0, 0, 1, 1); + table->attach(_sb, 1, 0, 1, 1); + table->attach(_ruler, 0, 1, 1, 1); + table->attach(_unit, 1, 1, 1, 1); + + pack_start(*table, Gtk::PACK_SHRINK); +} + +void +PrefSlider::on_slider_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(_prefs_path, _slider->get_value()); + _sb.set_value(_slider->get_value()); + freeze = false; + } +} + +void +PrefSlider::on_spinbutton_value_changed() +{ + if (this->get_visible() || freeze) //only take action if user changed value + { + freeze = true; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(_prefs_path, _sb.get_value()); + _slider->set_value(_sb.get_value()); + freeze = false; + } +} + +bool PrefSlider::on_mnemonic_activate ( bool group_cycling ) +{ + return _sb.mnemonic_activate ( group_cycling ); +} + +void +PrefSlider::init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, double default_value, int digits) +{ + _prefs_path = prefs_path; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double value = prefs->getDoubleLimited(prefs_path, default_value, lower, upper); + + freeze = false; + + _slider = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + + _slider->set_range (lower, upper); + _slider->set_increments (step_increment, page_increment); + _slider->set_value (value); + _slider->set_digits(digits); + _slider->signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_slider_value_changed)); + + _sb.signal_value_changed().connect(sigc::mem_fun(*this, &PrefSlider::on_spinbutton_value_changed)); + _sb.set_range (lower, upper); + _sb.set_increments (step_increment, 0); + _sb.set_value (value); + _sb.set_digits(digits); + _sb.set_halign(Gtk::ALIGN_CENTER); + _sb.set_valign(Gtk::ALIGN_END); + + auto table = Gtk::manage(new Gtk::Grid()); + _slider->set_hexpand(); + table->attach(*_slider, 0, 0, 1, 1); + table->attach(_sb, 1, 0, 1, 1); + + this->pack_start(*table, Gtk::PACK_EXPAND_WIDGET); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, + Glib::ustring labels[], int values[], int num_items, int default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + int value = prefs->getInt(_prefs_path, default_value); + + for (int i = 0 ; i < num_items; ++i) + { + this->append(labels[i]); + _values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, + Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + Glib::ustring value = prefs->getString(_prefs_path); + if(value.empty()) + { + value = default_value; + } + + for (int i = 0 ; i < num_items; ++i) + { + this->append(labels[i]); + _ustr_values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values, + int default_value) +{ + size_t labels_size = labels.size(); + size_t values_size = values.size(); + if (values_size != labels_size) { + std::cout << "PrefCombo::" + << "Different number of values/labels in " << prefs_path << std::endl; + return; + } + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + int value = prefs->getInt(_prefs_path, default_value); + + for (int i = 0; i < labels_size; ++i) { + this->append(labels[i]); + _values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, + std::vector<Glib::ustring> values, Glib::ustring default_value) +{ + size_t labels_size = labels.size(); + size_t values_size = values.size(); + if (values_size != labels_size) { + std::cout << "PrefCombo::" + << "Different number of values/labels in " << prefs_path << std::endl; + return; + } + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int row = 0; + Glib::ustring value = prefs->getString(_prefs_path); + if (value.empty()) { + value = default_value; + } + + for (int i = 0; i < labels_size; ++i) { + this->append(labels[i]); + _ustr_values.push_back(values[i]); + if (value == values[i]) + row = i; + } + this->set_active(row); +} + +void PrefCombo::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if(!_values.empty()) + { + prefs->setInt(_prefs_path, _values[this->get_active_row_number()]); + } + else + { + prefs->setString(_prefs_path, _ustr_values[this->get_active_row_number()]); + } + } +} + +void PrefEntryButtonHBox::init(Glib::ustring const &prefs_path, + bool visibility, Glib::ustring const &default_string) +{ + _prefs_path = prefs_path; + _default_string = default_string; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + relatedEntry = new Gtk::Entry(); + relatedButton = new Gtk::Button(_("Reset")); + relatedEntry->set_invisible_char('*'); + relatedEntry->set_visibility(visibility); + relatedEntry->set_text(prefs->getString(_prefs_path)); + this->pack_start(*relatedEntry); + this->pack_start(*relatedButton); + relatedButton->signal_clicked().connect( + sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedButtonClickedCallback)); + relatedEntry->signal_changed().connect( + sigc::mem_fun(*this, &PrefEntryButtonHBox::onRelatedEntryChangedCallback)); +} + +void PrefEntryButtonHBox::onRelatedEntryChangedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, relatedEntry->get_text()); + } +} + +void PrefEntryButtonHBox::onRelatedButtonClickedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, _default_string); + relatedEntry->set_text(_default_string); + } +} + +bool PrefEntryButtonHBox::on_mnemonic_activate ( bool group_cycling ) +{ + return relatedEntry->mnemonic_activate ( group_cycling ); +} + +void PrefEntryFileButtonHBox::init(Glib::ustring const &prefs_path, + bool visibility) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + relatedEntry = new Gtk::Entry(); + relatedEntry->set_invisible_char('*'); + relatedEntry->set_visibility(visibility); + relatedEntry->set_text(prefs->getString(_prefs_path)); + + relatedButton = new Gtk::Button(); + Gtk::HBox* pixlabel = new Gtk::HBox(false, 3); + Gtk::Image *im = sp_get_icon_image("applications-graphics", Gtk::ICON_SIZE_BUTTON); + pixlabel->pack_start(*im); + Gtk::Label *l = new Gtk::Label(); + l->set_markup_with_mnemonic(_("_Browse...")); + pixlabel->pack_start(*l); + relatedButton->add(*pixlabel); + + this->pack_end(*relatedButton, false, false, 4); + this->pack_start(*relatedEntry, true, true, 0); + + relatedButton->signal_clicked().connect( + sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedButtonClickedCallback)); + relatedEntry->signal_changed().connect( + sigc::mem_fun(*this, &PrefEntryFileButtonHBox::onRelatedEntryChangedCallback)); +} + +void PrefEntryFileButtonHBox::onRelatedEntryChangedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, relatedEntry->get_text()); + } +} + +static Inkscape::UI::Dialog::FileOpenDialog * selectPrefsFileInstance = nullptr; + +void PrefEntryFileButtonHBox::onRelatedButtonClickedCallback() +{ + if (this->get_visible()) //only take action if user changed value + { + //# Get the current directory for finding files + static Glib::ustring open_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + + Glib::ustring attr = prefs->getString(_prefs_path); + if (!attr.empty()) open_path = attr; + + //# Test if the open_path directory exists + if (!Inkscape::IO::file_test(open_path.c_str(), + (GFileTest)(G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR))) + open_path = ""; + +#ifdef _WIN32 + //# If no open path, default to our win32 documents folder + if (open_path.empty()) + { + // The path to the My Documents folder is read from the + // value "HKEY_CURRENT_USER\Software\Windows\CurrentVersion\Explorer\Shell Folders\Personal" + HKEY key = NULL; + if(RegOpenKeyExA(HKEY_CURRENT_USER, + "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", + 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) + { + WCHAR utf16path[_MAX_PATH]; + DWORD value_type; + DWORD data_size = sizeof(utf16path); + if(RegQueryValueExW(key, L"Personal", NULL, &value_type, + (BYTE*)utf16path, &data_size) == ERROR_SUCCESS) + { + g_assert(value_type == REG_SZ); + gchar *utf8path = g_utf16_to_utf8( + (const gunichar2*)utf16path, -1, NULL, NULL, NULL); + if(utf8path) + { + open_path = Glib::ustring(utf8path); + g_free(utf8path); + } + } + } + } +#endif + + //# If no open path, default to our home directory + if (open_path.empty()) + { + open_path = g_get_home_dir(); + open_path.append(G_DIR_SEPARATOR_S); + } + + //# Create a dialog + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!selectPrefsFileInstance) { + selectPrefsFileInstance = + Inkscape::UI::Dialog::FileOpenDialog::create( + *desktop->getToplevel(), + open_path, + Inkscape::UI::Dialog::EXE_TYPES, + _("Select a bitmap editor")); + } + + //# Show the dialog + bool const success = selectPrefsFileInstance->show(); + + if (!success) { + return; + } + + //# User selected something. Get name and type + Glib::ustring fileName = selectPrefsFileInstance->getFilename(); + + if (!fileName.empty()) + { + Glib::ustring newFileName = Glib::filename_to_utf8(fileName); + + if ( newFileName.size() > 0) + open_path = newFileName; + else + g_warning( "ERROR CONVERTING OPEN FILENAME TO UTF-8" ); + + prefs->setString(_prefs_path, open_path); + } + + relatedEntry->set_text(fileName); + } +} + +bool PrefEntryFileButtonHBox::on_mnemonic_activate ( bool group_cycling ) +{ + return relatedEntry->mnemonic_activate ( group_cycling ); +} + +void PrefOpenFolder::init(Glib::ustring const &entry_string, Glib::ustring const &tooltip) +{ + relatedEntry = new Gtk::Entry(); + relatedButton = new Gtk::Button(); + Gtk::HBox *pixlabel = new Gtk::HBox(false, 3); + Gtk::Image *im = sp_get_icon_image("document-open", Gtk::ICON_SIZE_BUTTON); + pixlabel->pack_start(*im); + Gtk::Label *l = new Gtk::Label(); + l->set_markup_with_mnemonic(_("Open")); + pixlabel->pack_start(*l); + relatedButton->add(*pixlabel); + relatedButton->set_tooltip_text(tooltip); + relatedEntry->set_text(entry_string); + relatedEntry->set_sensitive(false); + this->pack_end(*relatedButton, false, false, 4); + this->pack_start(*relatedEntry, true, true, 0); + relatedButton->signal_clicked().connect(sigc::mem_fun(*this, &PrefOpenFolder::onRelatedButtonClickedCallback)); +} + +void PrefOpenFolder::onRelatedButtonClickedCallback() +{ + g_mkdir_with_parents(relatedEntry->get_text().c_str(), 0700); + // https://stackoverflow.com/questions/42442189/how-to-open-spawn-a-file-with-glib-gtkmm-in-windows +#ifdef _WIN32 + ShellExecute(NULL, "open", relatedEntry->get_text().c_str(), NULL, NULL, SW_SHOWDEFAULT); +#elif defined(__APPLE__) + std::vector<std::string> argv = { "open", relatedEntry->get_text().raw() }; + Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH); +#else + gchar *path = g_filename_to_uri(relatedEntry->get_text().c_str(), NULL, NULL); + Glib::ustring xgd = "xdg-open "; + xgd += path; + system((xgd).c_str()); + g_free(path); +#endif +} + +void PrefFileButton::init(Glib::ustring const &prefs_path) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + select_filename(Glib::filename_from_utf8(prefs->getString(_prefs_path))); + + signal_selection_changed().connect(sigc::mem_fun(*this, &PrefFileButton::onFileChanged)); +} + +void PrefFileButton::onFileChanged() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, Glib::filename_to_utf8(get_filename())); +} + +void PrefEntry::init(Glib::ustring const &prefs_path, bool visibility) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->set_invisible_char('*'); + this->set_visibility(visibility); + this->set_text(prefs->getString(_prefs_path)); +} + +void PrefEntry::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, this->get_text()); + } +} + +void PrefMultiEntry::init(Glib::ustring const &prefs_path, int height) +{ + // TODO: Figure out if there's a way to specify height in lines instead of px + // and how to obtain a reasonable default width if 'expand_widget' is not used + set_size_request(100, height); + set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + set_shadow_type(Gtk::SHADOW_IN); + + add(_text); + + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value = prefs->getString(_prefs_path); + value = Glib::Regex::create("\\|")->replace_literal(value, 0, "\n", (Glib::RegexMatchFlags)0); + _text.get_buffer()->set_text(value); + _text.get_buffer()->signal_changed().connect(sigc::mem_fun(*this, &PrefMultiEntry::on_changed)); +} + +void PrefMultiEntry::on_changed() +{ + if (get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring value = _text.get_buffer()->get_text(); + value = Glib::Regex::create("\\n")->replace_literal(value, 0, "|", (Glib::RegexMatchFlags)0); + prefs->setString(_prefs_path, value); + } +} + +void PrefColorPicker::init(Glib::ustring const &label, Glib::ustring const &prefs_path, + guint32 default_rgba) +{ + _prefs_path = prefs_path; + _title = label; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->setRgba32( prefs->getInt(_prefs_path, (int)default_rgba) ); +} + +void PrefColorPicker::on_changed (guint32 rgba) +{ + if (this->get_visible()) //only take action if the user toggled it + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(_prefs_path, (int) rgba); + } +} + +void PrefUnit::init(Glib::ustring const &prefs_path) +{ + _prefs_path = prefs_path; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + setUnitType(UNIT_TYPE_LINEAR); + setUnit(prefs->getString(_prefs_path)); +} + +void PrefUnit::on_changed() +{ + if (this->get_visible()) //only take action if user changed value + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(_prefs_path, getUnitAbbr()); + } +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h new file mode 100644 index 0000000..3e132c0 --- /dev/null +++ b/src/ui/widget/preferences-widget.h @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widgets for Inkscape Preferences dialog. + */ +/* + * Authors: + * Marco Scholten + * Bruno Dilly <bruno.dilly@gmail.com> + * + * Copyright (C) 2004, 2006, 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H +#define INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H + +#include <iostream> +#include <vector> + +#include <gtkmm/filechooserbutton.h> +#include "ui/widget/spinbutton.h" +#include <cstddef> +#include <sigc++/sigc++.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/grid.h> + +#include "ui/widget/color-picker.h" +#include "ui/widget/unit-menu.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/scalar-unit.h" + +namespace Gtk { +class Scale; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class PrefCheckButton : public Gtk::CheckButton +{ +public: + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + bool default_value); + sigc::signal<void, bool> changed_signal; +protected: + Glib::ustring _prefs_path; + void on_toggled() override; +}; + +class PrefRadioButton : public Gtk::RadioButton +{ +public: + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + int int_value, bool default_value, PrefRadioButton* group_member); + void init(Glib::ustring const &label, Glib::ustring const &prefs_path, + Glib::ustring const &string_value, bool default_value, PrefRadioButton* group_member); + sigc::signal<void, bool> changed_signal; +protected: + Glib::ustring _prefs_path; + Glib::ustring _string_value; + int _value_type; + enum + { + VAL_INT, + VAL_STRING + }; + int _int_value; + void on_toggled() override; +}; + +class PrefSpinButton : public SpinButton +{ +public: + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, + double default_value, bool is_int, bool is_percent); +protected: + Glib::ustring _prefs_path; + bool _is_int; + bool _is_percent; + void on_value_changed() override; +}; + +class PrefSpinUnit : public ScalarUnit +{ +public: + PrefSpinUnit() : ScalarUnit("", "") {}; + + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, + double default_value, + UnitType unit_type, Glib::ustring const &default_unit); +protected: + Glib::ustring _prefs_path; + bool _is_percent; + void on_my_value_changed(); +}; + +class ZoomCorrRuler : public Gtk::DrawingArea { +public: + ZoomCorrRuler(int width = 100, int height = 20); + void set_size(int x, int y); + void set_unit_conversion(double conv) { _unitconv = conv; } + + int width() { return _min_width + _border*2; } + + static const double textsize; + static const double textpadding; + +private: + bool on_draw(const Cairo::RefPtr<Cairo::Context>& cr) override; + + void draw_marks(Cairo::RefPtr<Cairo::Context> cr, double dist, int major_interval); + + double _unitconv; + int _min_width; + int _height; + int _border; + int _drawing_width; +}; + +class ZoomCorrRulerSlider : public Gtk::VBox +{ +public: + void init(int ruler_width, int ruler_height, double lower, double upper, + double step_increment, double page_increment, double default_value); + +private: + void on_slider_value_changed(); + void on_spinbutton_value_changed(); + void on_unit_changed(); + bool on_mnemonic_activate( bool group_cycling ) override; + + Inkscape::UI::Widget::SpinButton _sb; + UnitMenu _unit; + Gtk::Scale* _slider; + ZoomCorrRuler _ruler; + bool freeze; // used to block recursive updates of slider and spinbutton +}; + +class PrefSlider : public Gtk::HBox +{ +public: + void init(Glib::ustring const &prefs_path, + double lower, double upper, double step_increment, double page_increment, double default_value, int digits); + +private: + void on_slider_value_changed(); + void on_spinbutton_value_changed(); + bool on_mnemonic_activate( bool group_cycling ) override; + + Glib::ustring _prefs_path; + Inkscape::UI::Widget::SpinButton _sb; + + Gtk::Scale* _slider; + + bool freeze; // used to block recursive updates of slider and spinbutton +}; + + +class PrefCombo : public Gtk::ComboBoxText +{ +public: + void init(Glib::ustring const &prefs_path, + Glib::ustring labels[], int values[], int num_items, int default_value); + + /** + * Initialize a combo box. + * second form uses strings as key values. + */ + void init(Glib::ustring const &prefs_path, + Glib::ustring labels[], Glib::ustring values[], int num_items, Glib::ustring default_value); + /** + * Initialize a combo box. + * with vectors. + */ + void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<int> values, + int default_value); + + void init(Glib::ustring const &prefs_path, std::vector<Glib::ustring> labels, std::vector<Glib::ustring> values, + Glib::ustring default_value); + + protected: + Glib::ustring _prefs_path; + std::vector<int> _values; + std::vector<Glib::ustring> _ustr_values; ///< string key values used optionally instead of numeric _values + void on_changed() override; +}; + +class PrefEntry : public Gtk::Entry +{ +public: + void init(Glib::ustring const &prefs_path, bool mask); +protected: + Glib::ustring _prefs_path; + void on_changed() override; +}; + +class PrefMultiEntry : public Gtk::ScrolledWindow +{ +public: + void init(Glib::ustring const &prefs_path, int height); +protected: + Glib::ustring _prefs_path; + Gtk::TextView _text; + void on_changed(); +}; + +class PrefEntryButtonHBox : public Gtk::HBox +{ +public: + void init(Glib::ustring const &prefs_path, + bool mask, Glib::ustring const &default_string); + +protected: + Glib::ustring _prefs_path; + Glib::ustring _default_string; + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedEntryChangedCallback(); + void onRelatedButtonClickedCallback(); + bool on_mnemonic_activate( bool group_cycling ) override; +}; + +class PrefEntryFileButtonHBox : public Gtk::HBox +{ +public: + void init(Glib::ustring const &prefs_path, + bool mask); +protected: + Glib::ustring _prefs_path; + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedEntryChangedCallback(); + void onRelatedButtonClickedCallback(); + bool on_mnemonic_activate( bool group_cycling ) override; +}; + +class PrefOpenFolder : public Gtk::HBox { + public: + void init(Glib::ustring const &entry_string, Glib::ustring const &tooltip); + + protected: + Gtk::Button *relatedButton; + Gtk::Entry *relatedEntry; + void onRelatedButtonClickedCallback(); +}; + +class PrefFileButton : public Gtk::FileChooserButton +{ +public: + void init(Glib::ustring const &prefs_path); + +protected: + Glib::ustring _prefs_path; + void onFileChanged(); +}; + +class PrefColorPicker : public ColorPicker +{ +public: + PrefColorPicker() : ColorPicker("", "", 0, false) {}; + ~PrefColorPicker() override = default;; + + void init(Glib::ustring const &abel, Glib::ustring const &prefs_path, + guint32 default_rgba); + +protected: + Glib::ustring _prefs_path; + void on_changed (guint32 rgba) override; +}; + +class PrefUnit : public UnitMenu +{ +public: + void init(Glib::ustring const &prefs_path); +protected: + Glib::ustring _prefs_path; + void on_changed() override; +}; + +class DialogPage : public Gtk::Grid +{ +public: + DialogPage(); + void add_line(bool indent, Glib::ustring const &label, Gtk::Widget& widget, Glib::ustring const &suffix, Glib::ustring const &tip, bool expand = true, Gtk::Widget *other_widget = nullptr); + void add_group_header(Glib::ustring name); + void set_tip(Gtk::Widget &widget, Glib::ustring const &tip); +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif //INKSCAPE_UI_WIDGET_INKSCAPE_PREFERENCES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/preview.cpp b/src/ui/widget/preview.cpp new file mode 100644 index 0000000..a56639c --- /dev/null +++ b/src/ui/widget/preview.cpp @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Preview Stuffs. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2005 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <algorithm> +#include <gdkmm/general.h> +#include "preview.h" +#include "preferences.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +#define PRIME_BUTTON_MAGIC_NUMBER 1 + +/* Keep in sync with last value in eek-preview.h */ +#define PREVIEW_SIZE_LAST PREVIEW_SIZE_HUGE +#define PREVIEW_SIZE_NEXTFREE (PREVIEW_SIZE_HUGE + 1) + +#define PREVIEW_MAX_RATIO 500 + +void +Preview::set_color(int r, int g, int b ) +{ + _r = r; + _g = g; + _b = b; + + queue_draw(); +} + + +void +Preview::set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf) +{ + _previewPixbuf = pixbuf; + + queue_draw(); + + if (_scaled) + { + _scaled.reset(); + } + + _scaledW = _previewPixbuf->get_width(); + _scaledH = _previewPixbuf->get_height(); +} + +static gboolean setupDone = FALSE; +static GtkRequisition sizeThings[PREVIEW_SIZE_NEXTFREE]; + +void +Preview::set_size_mappings( guint count, GtkIconSize const* sizes ) +{ + gint width = 0; + gint height = 0; + gint smallest = 512; + gint largest = 0; + guint i = 0; + guint delta = 0; + + for ( i = 0; i < count; ++i ) { + gboolean worked = gtk_icon_size_lookup( sizes[i], &width, &height ); + if ( worked ) { + if ( width < smallest ) { + smallest = width; + } + if ( width > largest ) { + largest = width; + } + } + } + + smallest = (smallest * 3) / 4; + + delta = largest - smallest; + + for ( i = 0; i < G_N_ELEMENTS(sizeThings); ++i ) { + guint val = smallest + ( (i * delta) / (G_N_ELEMENTS(sizeThings) - 1) ); + sizeThings[i].width = val; + sizeThings[i].height = val; + } + + setupDone = TRUE; +} + +void +Preview::size_request(GtkRequisition* req) const +{ + int width = 0; + int height = 0; + + if ( !setupDone ) { + GtkIconSize sizes[] = { + GTK_ICON_SIZE_MENU, + GTK_ICON_SIZE_SMALL_TOOLBAR, + GTK_ICON_SIZE_LARGE_TOOLBAR, + GTK_ICON_SIZE_BUTTON, + GTK_ICON_SIZE_DIALOG + }; + set_size_mappings( G_N_ELEMENTS(sizes), sizes ); + } + + width = sizeThings[_size].width; + height = sizeThings[_size].height; + + if ( _view == VIEW_TYPE_LIST ) { + width *= 3; + } + + if ( _ratio != 100 ) { + width = (width * _ratio) / 100; + if ( width < 0 ) { + width = 1; + } + } + + req->width = width; + req->height = height; +} + +void +Preview::get_preferred_width_vfunc(int &minimal_width, int &natural_width) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_width = natural_width = requisition.width; +} + +void +Preview::get_preferred_height_vfunc(int &minimal_height, int &natural_height) const +{ + GtkRequisition requisition; + size_request(&requisition); + minimal_height = natural_height = requisition.height; +} + +bool +Preview::on_draw(const Cairo::RefPtr<Cairo::Context> &cr) +{ + auto allocation = get_allocation(); + + gint insetTop = 0, insetBottom = 0; + gint insetLeft = 0, insetRight = 0; + + if (_border == BORDER_SOLID) { + insetTop = 1; + insetLeft = 1; + } + if (_border == BORDER_SOLID_LAST_ROW) { + insetTop = insetBottom = 1; + insetLeft = 1; + } + if (_border == BORDER_WIDE) { + insetTop = insetBottom = 1; + insetLeft = insetRight = 1; + } + + auto context = get_style_context(); + + context->render_frame(cr, + 0, 0, + allocation.get_width(), allocation.get_height()); + + context->render_background(cr, + 0, 0, + allocation.get_width(), allocation.get_height()); + + // Border + if (_border != BORDER_NONE) { + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->rectangle(0, 0, allocation.get_width(), allocation.get_height()); + cr->fill(); + } + + cr->set_source_rgb(_r/65535.0, _g/65535.0, _b/65535.0 ); + cr->rectangle(insetLeft, insetTop, allocation.get_width() - (insetLeft + insetRight), allocation.get_height() - (insetTop + insetBottom)); + cr->fill(); + + if (_previewPixbuf ) + { + if ((allocation.get_width() != _scaledW) || (allocation.get_height() != _scaledH)) { + if (_scaled) + { + _scaled.reset(); + } + + _scaledW = allocation.get_width() - (insetLeft + insetRight); + _scaledH = allocation.get_height() - (insetTop + insetBottom); + + _scaled = _previewPixbuf->scale_simple(_scaledW, + _scaledH, + Gdk::INTERP_BILINEAR); + } + + Glib::RefPtr<Gdk::Pixbuf> pix = (_scaled) ? _scaled : _previewPixbuf; + + // Border + if (_border != BORDER_NONE) { + cr->set_source_rgb(0.0, 0.0, 0.0); + cr->rectangle(0, 0, allocation.get_width(), allocation.get_height()); + cr->fill(); + } + + Gdk::Cairo::set_source_pixbuf(cr, pix, insetLeft, insetTop); + cr->paint(); + } + + if (_linked) + { + /* Draw arrow */ + GdkRectangle possible = {insetLeft, + insetTop, + (allocation.get_width() - (insetLeft + insetRight)), + (allocation.get_height() - (insetTop + insetBottom)) + }; + + GdkRectangle area = {possible.x, + possible.y, + possible.width / 2, + possible.height / 2 }; + + /* Make it square */ + if ( area.width > area.height ) + area.width = area.height; + if ( area.height > area.width ) + area.height = area.width; + + /* Center it horizontally */ + if ( area.width < possible.width ) { + int diff = (possible.width - area.width) / 2; + area.x += diff; + } + + if (_linked & PREVIEW_LINK_IN) + { + context->render_arrow(cr, + G_PI, // Down-pointing arrow + area.x, area.y, + std::min(area.width, area.height) + ); + } + + if (_linked & PREVIEW_LINK_OUT) + { + GdkRectangle otherArea = {area.x, area.y, area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height); + } + + context->render_arrow(cr, + G_PI, // Down-pointing arrow + otherArea.x, otherArea.y, + std::min(otherArea.width, otherArea.height) + ); + } + + if (_linked & PREVIEW_LINK_OTHER) + { + GdkRectangle otherArea = {insetLeft, area.y, area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + + context->render_arrow(cr, + 1.5*G_PI, // Left-pointing arrow + otherArea.x, otherArea.y, + std::min(otherArea.width, otherArea.height) + ); + } + + + if (_linked & PREVIEW_FILL) + { + GdkRectangle otherArea = {possible.x + ((possible.width / 4) - (area.width / 2)), + area.y, + area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + context->render_check(cr, + otherArea.x, otherArea.y, + otherArea.width, otherArea.height ); + } + + if (_linked & PREVIEW_STROKE) + { + GdkRectangle otherArea = {possible.x + (((possible.width * 3) / 4) - (area.width / 2)), + area.y, + area.width, area.height}; + if ( otherArea.height < possible.height ) { + otherArea.y = possible.y + (possible.height - otherArea.height) / 2; + } + // This should be a diamond too? + context->render_check(cr, + otherArea.x, otherArea.y, + otherArea.width, otherArea.height ); + } + } + + + if ( has_focus() ) { + allocation = get_allocation(); + + context->render_focus(cr, + 0 + 1, 0 + 1, + allocation.get_width() - 2, allocation.get_height() - 2 ); + } + + return false; +} + + +bool +Preview::on_enter_notify_event(GdkEventCrossing* event ) +{ + _within = true; + set_state_flags(_hot ? Gtk::STATE_FLAG_ACTIVE : Gtk::STATE_FLAG_PRELIGHT, false); + + return false; +} + +bool +Preview::on_leave_notify_event(GdkEventCrossing* event) +{ + _within = false; + set_state_flags(Gtk::STATE_FLAG_NORMAL, false); + + return false; +} + +bool +Preview::on_button_press_event(GdkEventButton *event) +{ + if (_takesFocus && !has_focus() ) + { + grab_focus(); + } + + if ( event->button == PRIME_BUTTON_MAGIC_NUMBER || + event->button == 2 ) + { + _hot = true; + + if ( _within ) + { + set_state_flags(Gtk::STATE_FLAG_ACTIVE, false); + } + } + + return false; +} + +bool +Preview::on_button_release_event(GdkEventButton* event) +{ + _hot = false; + set_state_flags(Gtk::STATE_FLAG_NORMAL, false); + + if (_within && + (event->button == PRIME_BUTTON_MAGIC_NUMBER || + event->button == 2)) + { + gboolean isAlt = ( ((event->state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK) || + (event->button == 2)); + + if ( isAlt ) + { + _signal_alt_clicked(2); + } + else + { + _signal_clicked.emit(); + } + } + + return false; +} + +void +Preview::set_linked(LinkType link) +{ + link = (LinkType)(link & PREVIEW_LINK_ALL); + + if (link != _linked) + { + _linked = link; + + queue_draw(); + } +} + +LinkType +Preview::get_linked() const +{ + return (LinkType)_linked; +} + +void +Preview::set_details(ViewType view, + PreviewSize size, + guint ratio, + guint border) +{ + _view = view; + + if ( size > PREVIEW_SIZE_LAST ) + { + size = PREVIEW_SIZE_LAST; + } + + _size = size; + + if ( ratio > PREVIEW_MAX_RATIO ) + { + ratio = PREVIEW_MAX_RATIO; + } + + _ratio = ratio; + _border = border; + + queue_draw(); +} + +Preview::Preview() + : _r(0x80), + _g(0x80), + _b(0xcc), + _scaledW(0), + _scaledH(0), + _hot(false), + _within(false), + _takesFocus(false), + _view(VIEW_TYPE_LIST), + _size(PREVIEW_SIZE_SMALL), + _ratio(100), + _border(BORDER_NONE), + _previewPixbuf(nullptr), + _scaled(nullptr), + _linked(PREVIEW_LINK_NONE) +{ + set_can_focus(true); + set_receives_default(true); + + set_sensitive(true); + + add_events(Gdk::BUTTON_PRESS_MASK + |Gdk::BUTTON_RELEASE_MASK + |Gdk::KEY_PRESS_MASK + |Gdk::KEY_RELEASE_MASK + |Gdk::FOCUS_CHANGE_MASK + |Gdk::ENTER_NOTIFY_MASK + |Gdk::LEAVE_NOTIFY_MASK ); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/preview.h b/src/ui/widget/preview.h new file mode 100644 index 0000000..b455367 --- /dev/null +++ b/src/ui/widget/preview.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Preview Stuffs. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2005-2008 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef SEEN_EEK_PREVIEW_H +#define SEEN_EEK_PREVIEW_H + +#include <gtkmm/drawingarea.h> + +/** + * @file + * Generic implementation of an object that can be shown by a preview. + */ + +namespace Inkscape { +namespace UI { +namespace Widget { + +enum PreviewStyle { + PREVIEW_STYLE_ICON = 0, + PREVIEW_STYLE_PREVIEW, + PREVIEW_STYLE_NAME, + PREVIEW_STYLE_BLURB, + PREVIEW_STYLE_ICON_NAME, + PREVIEW_STYLE_ICON_BLURB, + PREVIEW_STYLE_PREVIEW_NAME, + PREVIEW_STYLE_PREVIEW_BLURB +}; + +enum ViewType { + VIEW_TYPE_LIST = 0, + VIEW_TYPE_GRID +}; + +enum PreviewSize { + PREVIEW_SIZE_TINY = 0, + PREVIEW_SIZE_SMALL, + PREVIEW_SIZE_MEDIUM, + PREVIEW_SIZE_BIG, + PREVIEW_SIZE_BIGGER, + PREVIEW_SIZE_HUGE +}; + +enum LinkType { + PREVIEW_LINK_NONE = 0, + PREVIEW_LINK_IN = 1, + PREVIEW_LINK_OUT = 2, + PREVIEW_LINK_OTHER = 4, + PREVIEW_FILL = 8, + PREVIEW_STROKE = 16, + PREVIEW_LINK_ALL = 31 +}; + +enum BorderStyle { + BORDER_NONE = 0, + BORDER_SOLID, + BORDER_WIDE, + BORDER_SOLID_LAST_ROW, +}; + +class Preview : public Gtk::DrawingArea { +private: + int _scaledW; + int _scaledH; + + int _r; + int _g; + int _b; + + bool _hot; + bool _within; + bool _takesFocus; ///< flag to grab focus when clicked + ViewType _view; + PreviewSize _size; + unsigned int _ratio; + LinkType _linked; + unsigned int _border; + + Glib::RefPtr<Gdk::Pixbuf> _previewPixbuf; + Glib::RefPtr<Gdk::Pixbuf> _scaled; + + // signals + sigc::signal<void> _signal_clicked; + sigc::signal<void, int> _signal_alt_clicked; + + void size_request(GtkRequisition *req) const; + +protected: + void get_preferred_width_vfunc(int &minimal_width, int &natural_width) const override; + void get_preferred_height_vfunc(int &minimal_height, int &natural_height) const override; + bool on_draw(const Cairo::RefPtr<Cairo::Context> &cr) override; + bool on_button_press_event(GdkEventButton *button_event) override; + bool on_button_release_event(GdkEventButton *button_event) override; + bool on_enter_notify_event(GdkEventCrossing* event ) override; + bool on_leave_notify_event(GdkEventCrossing* event ) override; + +public: + Preview(); + bool get_focus_on_click() const {return _takesFocus;} + void set_focus_on_click(bool focus_on_click) {_takesFocus = focus_on_click;} + LinkType get_linked() const; + void set_linked(LinkType link); + void set_details(ViewType view, + PreviewSize size, + guint ratio, + guint border); + void set_color(int r, int g, int b); + void set_pixbuf(const Glib::RefPtr<Gdk::Pixbuf> &pixbuf); + static void set_size_mappings(guint count, GtkIconSize const* sizes); + + decltype(_signal_clicked) signal_clicked() {return _signal_clicked;} + decltype(_signal_alt_clicked) signal_alt_clicked() {return _signal_alt_clicked;} +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif /* SEEN_EEK_PREVIEW_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/random.cpp b/src/ui/widget/random.cpp new file mode 100644 index 0000000..495a778 --- /dev/null +++ b/src/ui/widget/random.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "random.h" +#include "ui/icon-loader.h" +#include <glibmm/i18n.h> + +#include <gtkmm/button.h> +#include <gtkmm/image.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, digits, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +Random::Random(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, adjust, digits, suffix, icon, mnemonic) +{ + startseed = 0; + addReseedButton(); +} + +long Random::getStartSeed() const +{ + return startseed; +} + +void Random::setStartSeed(long newseed) +{ + startseed = newseed; +} + +void Random::addReseedButton() +{ + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("randomize", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button * pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &Random::onReseedButtonClick)); + pButton->set_tooltip_text(_("Reseed the random number generator; this creates a different sequence of random numbers.")); + + pack_start(*pButton, Gtk::PACK_SHRINK, 0); +} + +void +Random::onReseedButtonClick() +{ + startseed = g_random_int(); + signal_reseeded.emit(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/random.h b/src/ui/widget/random.h new file mode 100644 index 0000000..2648cb2 --- /dev/null +++ b/src/ui/widget/random.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_RANDOM_H +#define INKSCAPE_UI_WIDGET_RANDOM_H + +#include "scalar.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional + * icon or suffix, for entering arbitrary number values. It adds an extra + * number called "startseed", that is not UI edittable, but should be put in SVG. + * This does NOT generate a random number, but provides merely the saving of + * the startseed value. + */ +class Random : public Scalar +{ +public: + + /** + * Construct a Random scalar Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Random Scalar Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Random Scalar Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Random(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Gets the startseed. + */ + long getStartSeed() const; + + /** + * Sets the startseed number. + */ + void setStartSeed(long newseed); + + sigc::signal <void> signal_reseeded; + +protected: + long startseed; + +private: + + /** + * Add reseed button to the widget. + */ + void addReseedButton(); + + void onReseedButtonClick(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RANDOM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registered-enums.h b/src/ui/widget/registered-enums.h new file mode 100644 index 0000000..b0cc199 --- /dev/null +++ b/src/ui/widget/registered-enums.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H +#define INKSCAPE_UI_WIDGET_REGISTERED_ENUMS_H + +#include "ui/widget/combo-enums.h" +#include "ui/widget/registered-widget.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Simplified management of enumerations in the UI as combobox. + */ +template<typename E> class RegisteredEnum : public RegisteredWidget< LabelledComboBoxEnum<E> > +{ +public: + ~RegisteredEnum() override { + _changed_connection.disconnect(); + } + + RegisteredEnum ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const Util::EnumDataConverter<E>& c, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr, + bool sorted = true ) + : RegisteredWidget< LabelledComboBoxEnum<E> >(label, tip, c, (const Glib::ustring &)"", (const Glib::ustring &)"", true, sorted) + { + RegisteredWidget< LabelledComboBoxEnum<E> >::init_parent(key, wr, repr_in, doc_in); + _changed_connection = combobox()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredEnum::on_changed)); + } + + void set_active_by_id (E id) { + combobox()->set_active_by_id(id); + }; + + void set_active_by_key (const Glib::ustring& key) { + combobox()->set_active_by_key(key); + } + + inline const Util::EnumData<E>* get_active_data() { + combobox()->get_active_data(); + } + + ComboBoxEnum<E> * combobox() { + return LabelledComboBoxEnum<E>::getCombobox(); + } + + sigc::connection _changed_connection; + +protected: + void on_changed() { + if (combobox()->setProgrammatically) { + combobox()->setProgrammatically = false; + return; + } + + if (RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->isUpdating()) + return; + + RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (true); + + const Util::EnumData<E>* data = combobox()->get_active_data(); + if (data) { + RegisteredWidget< LabelledComboBoxEnum<E> >::write_to_xml(data->key.c_str()); + } + + RegisteredWidget< LabelledComboBoxEnum<E> >::_wr->setUpdating (false); + } +}; + +} +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registered-widget.cpp b/src/ui/widget/registered-widget.cpp new file mode 100644 index 0000000..bd62b73 --- /dev/null +++ b/src/ui/widget/registered-widget.cpp @@ -0,0 +1,845 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * bulia byak <buliabyak@users.sf.net> + * Bryce W. Harrington <bryce@bryceharrington.org> + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon Phillips <jon@rejon.org> + * Ralf Stephan <ralf@ark.in-berlin.de> (Gtkmm) + * Abhishek Sharma + * + * Copyright (C) 2000 - 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "registered-widget.h" + +#include <gtkmm/radiobutton.h> + +#include "verbs.h" + +#include "object/sp-root.h" + +#include "svg/svg-color.h" +#include "svg/stringstream.h" + +#include "widgets/spinbutton-events.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +/*######################################### + * Registered CHECKBUTTON + */ + +RegisteredCheckButton::~RegisteredCheckButton() +{ + _toggled_connection.disconnect(); +} + +RegisteredCheckButton::RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *active_str, char const *inactive_str) + : RegisteredWidget<Gtk::CheckButton>() + , _active_str(active_str) + , _inactive_str(inactive_str) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + set_tooltip_text (tip); + Gtk::Label *l = new Gtk::Label(); + l->set_markup(label); + l->set_use_underline (true); + add (*manage (l)); + + if(right) set_halign(Gtk::ALIGN_END); + else set_halign(Gtk::ALIGN_START); + + set_valign(Gtk::ALIGN_CENTER); + _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredCheckButton::on_toggled)); +} + +void +RegisteredCheckButton::setActive (bool b) +{ + setProgrammatically = true; + set_active (b); + //The slave button is greyed out if the master button is unchecked + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(b); + } + setProgrammatically = false; +} + +void +RegisteredCheckButton::on_toggled() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + _wr->setUpdating (true); + + write_to_xml(get_active() ? _active_str : _inactive_str); + //The slave button is greyed out if the master button is unchecked + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(get_active()); + } + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TOGGLEBUTTON + */ + +RegisteredToggleButton::~RegisteredToggleButton() +{ + _toggled_connection.disconnect(); +} + +RegisteredToggleButton::RegisteredToggleButton (const Glib::ustring& /*label*/, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right, Inkscape::XML::Node* repr_in, SPDocument *doc_in, char const *icon_active, char const *icon_inactive) + : RegisteredWidget<Gtk::ToggleButton>() +{ + init_parent(key, wr, repr_in, doc_in); + setProgrammatically = false; + set_tooltip_text (tip); + + if(right) set_halign(Gtk::ALIGN_END); + else set_halign(Gtk::ALIGN_START); + + set_valign(Gtk::ALIGN_CENTER); + _toggled_connection = signal_toggled().connect (sigc::mem_fun (*this, &RegisteredToggleButton::on_toggled)); +} + +void +RegisteredToggleButton::setActive (bool b) +{ + setProgrammatically = true; + set_active (b); + //The slave button is greyed out if the master button is untoggled + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(b); + } + setProgrammatically = false; +} + +void +RegisteredToggleButton::on_toggled() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + _wr->setUpdating (true); + + write_to_xml(get_active() ? "true" : "false"); + //The slave button is greyed out if the master button is untoggled + for (std::list<Gtk::Widget*>::const_iterator i = _slavewidgets.begin(); i != _slavewidgets.end(); ++i) { + (*i)->set_sensitive(get_active()); + } + + _wr->setUpdating (false); +} + +/*######################################### + * Registered UNITMENU + */ + +RegisteredUnitMenu::~RegisteredUnitMenu() +{ + _changed_connection.disconnect(); +} + +RegisteredUnitMenu::RegisteredUnitMenu (const Glib::ustring& label, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Labelled> (label, "" /*tooltip*/, new UnitMenu()) +{ + init_parent(key, wr, repr_in, doc_in); + + getUnitMenu()->setUnitType (UNIT_TYPE_LINEAR); + _changed_connection = getUnitMenu()->signal_changed().connect (sigc::mem_fun (*this, &RegisteredUnitMenu::on_changed)); +} + +void +RegisteredUnitMenu::setUnit (Glib::ustring unit) +{ + getUnitMenu()->setUnit(unit); +} + +void +RegisteredUnitMenu::on_changed() +{ + if (_wr->isUpdating()) + return; + + Inkscape::SVGOStringStream os; + os << getUnitMenu()->getUnitAbbr(); + + _wr->setUpdating (true); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SCALARUNIT + */ + +RegisteredScalarUnit::~RegisteredScalarUnit() +{ + _value_changed_connection.disconnect(); +} + +RegisteredScalarUnit::RegisteredScalarUnit (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, const RegisteredUnitMenu &rum, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in, RSU_UserUnits user_units) + : RegisteredWidget<ScalarUnit>(label, tip, UNIT_TYPE_LINEAR, "", "", rum.getUnitMenu()), + _um(nullptr) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + initScalar (-1e6, 1e6); + setUnit (rum.getUnitMenu()->getUnitAbbr()); + setDigits (2); + _um = rum.getUnitMenu(); + _user_units = user_units; + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalarUnit::on_value_changed)); +} + + +void +RegisteredScalarUnit::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + if (_user_units != RSU_none) { + // Output length in 'user units', taking into account scale in 'x' or 'y'. + double scale = 1.0; + if (doc) { + SPRoot *root = doc->getRoot(); + if (root->viewBox_set) { + // check to see if scaling is uniform + if(Geom::are_near((root->viewBox.width() * root->height.computed) / (root->width.computed * root->viewBox.height()), 1.0, Geom::EPSILON)) { + scale = (root->viewBox.width() / root->width.computed + root->viewBox.height() / root->height.computed)/2.0; + } else if (_user_units == RSU_x) { + scale = root->viewBox.width() / root->width.computed; + } else { + scale = root->viewBox.height() / root->height.computed; + } + } + } + os << getValue("px") * scale; + } else { + // Output using unit identifiers. + os << getValue(""); + if (_um) + os << _um->getUnitAbbr(); + } + + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SCALAR + */ + +RegisteredScalar::~RegisteredScalar() +{ + _value_changed_connection.disconnect(); +} + +RegisteredScalar::RegisteredScalar ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Scalar>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredScalar::on_value_changed)); +} + +void +RegisteredScalar::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + //Force exact 0 if decimals over to 6 + double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue(); + os << val; + //TODO: Test is ok remove this sensitives + //also removed in registered text and in registered random + //set_sensitive(false); + write_to_xml(os.str().c_str()); + //set_sensitive(true); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered TEXT + */ + +RegisteredText::~RegisteredText() +{ + _activate_connection.disconnect(); +} + +RegisteredText::RegisteredText ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Text>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + _activate_connection = signal_activate().connect (sigc::mem_fun (*this, &RegisteredText::on_activate)); +} + +void +RegisteredText::on_activate() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + Glib::ustring str(getText()); + Inkscape::SVGOStringStream os; + os << str; + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + + +/*######################################### + * Registered COLORPICKER + */ + +RegisteredColorPicker::RegisteredColorPicker(const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const Glib::ustring& ckey, + const Glib::ustring& akey, + Registry& wr, + Inkscape::XML::Node* repr_in, + SPDocument *doc_in) + : RegisteredWidget<LabelledColorPicker> (label, title, tip, 0, true) +{ + init_parent("", wr, repr_in, doc_in); + + _ckey = ckey; + _akey = akey; + _changed_connection = connectChanged (sigc::mem_fun (*this, &RegisteredColorPicker::on_changed)); +} + +RegisteredColorPicker::~RegisteredColorPicker() +{ + _changed_connection.disconnect(); +} + +void +RegisteredColorPicker::setRgba32 (guint32 rgba) +{ + LabelledColorPicker::setRgba32 (rgba); +} + +void +RegisteredColorPicker::closeWindow() +{ + LabelledColorPicker::closeWindow(); +} + +void +RegisteredColorPicker::on_changed (guint32 rgba) +{ + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + // Use local repr here. When repr is specified, use that one, but + // if repr==NULL, get the repr of namedview of active desktop. + Inkscape::XML::Node *local_repr = repr; + SPDocument *local_doc = doc; + if (!local_repr) { + // no repr specified, use active desktop's namedview's repr + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (!dt) + return; + local_repr = dt->getNamedView()->getRepr(); + local_doc = dt->getDocument(); + } + gchar c[32]; + if (_akey == _ckey + "_opacity_LPE") { //For LPE parameter we want stored with alpha + sprintf(c, "#%08x", rgba); + } else { + sp_svg_write_color(c, sizeof(c), rgba); + } + bool saved = DocumentUndo::getUndoSensitive(local_doc); + DocumentUndo::setUndoSensitive(local_doc, false); + local_repr->setAttribute(_ckey, c); + sp_repr_set_css_double(local_repr, _akey.c_str(), (rgba & 0xff) / 255.0); + DocumentUndo::setUndoSensitive(local_doc, saved); + + local_doc->setModifiedSinceSave(); + DocumentUndo::done(local_doc, SP_VERB_NONE, + /* TODO: annotate */ "registered-widget.cpp: RegisteredColorPicker::on_changed"); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered SUFFIXEDINTEGER + */ + +RegisteredSuffixedInteger::~RegisteredSuffixedInteger() +{ + _changed_connection.disconnect(); +} + +RegisteredSuffixedInteger::RegisteredSuffixedInteger (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& suffix, const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Scalar>(label, tip, 0, suffix), + setProgrammatically(false) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (0, 1e6); + setDigits (0); + setIncrements(1, 10); + + _changed_connection = signal_value_changed().connect (sigc::mem_fun(*this, &RegisteredSuffixedInteger::on_value_changed)); +} + +void +RegisteredSuffixedInteger::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered RADIOBUTTONPAIR + */ + +RegisteredRadioButtonPair::~RegisteredRadioButtonPair() +{ + _changed_connection.disconnect(); +} + +RegisteredRadioButtonPair::RegisteredRadioButtonPair (const Glib::ustring& label, + const Glib::ustring& label1, const Glib::ustring& label2, + const Glib::ustring& tip1, const Glib::ustring& tip2, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + : RegisteredWidget<Gtk::HBox>(), + _rb1(nullptr), + _rb2(nullptr) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + + add(*Gtk::manage(new Gtk::Label(label))); + _rb1 = Gtk::manage(new Gtk::RadioButton(label1, true)); + add (*_rb1); + Gtk::RadioButtonGroup group = _rb1->get_group(); + _rb2 = Gtk::manage(new Gtk::RadioButton(group, label2, true)); + add (*_rb2); + _rb2->set_active(); + _rb1->set_tooltip_text(tip1); + _rb2->set_tooltip_text(tip2); + _changed_connection = _rb1->signal_toggled().connect (sigc::mem_fun (*this, &RegisteredRadioButtonPair::on_value_changed)); +} + +void +RegisteredRadioButtonPair::setValue (bool second) +{ + if (!_rb1 || !_rb2) + return; + + setProgrammatically = true; + if (second) { + _rb2->set_active(); + } else { + _rb1->set_active(); + } +} + +void +RegisteredRadioButtonPair::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + bool second = _rb2->get_active(); + write_to_xml(second ? "true" : "false"); + + _wr->setUpdating (false); +} + + +/*######################################### + * Registered POINT + */ + +RegisteredPoint::~RegisteredPoint() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredPoint::RegisteredPoint ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredPoint::on_value_changed)); +} + +void +RegisteredPoint::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getXValue() << "," << getYValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TRANSFORMEDPOINT + */ + +RegisteredTransformedPoint::~RegisteredTransformedPoint() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredTransformedPoint::RegisteredTransformedPoint ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip), + to_svg(Geom::identity()) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredTransformedPoint::on_value_changed)); +} + +void +RegisteredTransformedPoint::setValue(Geom::Point const & p) +{ + Geom::Point new_p = p * to_svg.inverse(); + Point::setValue(new_p); // the Point widget should display things in canvas coordinates +} + +void +RegisteredTransformedPoint::setTransform(Geom::Affine const & canvas_to_svg) +{ + // check if matrix is singular / has inverse + if ( ! canvas_to_svg.isSingular() ) { + to_svg = canvas_to_svg; + } else { + // set back to default + to_svg = Geom::identity(); + } +} + +void +RegisteredTransformedPoint::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Geom::Point pos = getValue() * to_svg; + + Inkscape::SVGOStringStream os; + os << pos; + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered TRANSFORMEDPOINT + */ + +RegisteredVector::~RegisteredVector() +{ + _value_x_changed_connection.disconnect(); + _value_y_changed_connection.disconnect(); +} + +RegisteredVector::RegisteredVector ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<Point> (label, tip), + _polar_coords(false) +{ + init_parent(key, wr, repr_in, doc_in); + + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_x_changed_connection = signal_x_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); + _value_y_changed_connection = signal_y_value_changed().connect (sigc::mem_fun (*this, &RegisteredVector::on_value_changed)); +} + +void +RegisteredVector::setValue(Geom::Point const & p) +{ + if (!_polar_coords) { + Point::setValue(p); + } else { + Geom::Point polar; + polar[Geom::X] = atan2(p) *180/M_PI; + polar[Geom::Y] = p.length(); + Point::setValue(polar); + } +} + +void +RegisteredVector::setValue(Geom::Point const & p, Geom::Point const & origin) +{ + RegisteredVector::setValue(p); + _origin = origin; +} + +void RegisteredVector::setPolarCoords(bool polar_coords) +{ + _polar_coords = polar_coords; + if (polar_coords) { + xwidget.setLabelText("Angle:"); + ywidget.setLabelText("Distance:"); + } else { + xwidget.setLabelText("X:"); + ywidget.setLabelText("Y:"); + } +} + +void +RegisteredVector::on_value_changed() +{ + if (setProgrammatically()) { + clearProgrammatically(); + return; + } + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Geom::Point origin = _origin; + Geom::Point vector = getValue(); + if (_polar_coords) { + vector = Geom::Point::polar(vector[Geom::X]*M_PI/180, vector[Geom::Y]); + } + + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +/*######################################### + * Registered RANDOM + */ + +RegisteredRandom::~RegisteredRandom() +{ + _value_changed_connection.disconnect(); + _reseeded_connection.disconnect(); +} + +RegisteredRandom::RegisteredRandom ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument * doc_in ) + : RegisteredWidget<Random> (label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + + setProgrammatically = false; + setRange (-1e6, 1e6); + setDigits (2); + setIncrements(0.1, 1.0); + _value_changed_connection = signal_value_changed().connect (sigc::mem_fun (*this, &RegisteredRandom::on_value_changed)); + _reseeded_connection = signal_reseeded.connect(sigc::mem_fun(*this, &RegisteredRandom::on_value_changed)); +} + +void +RegisteredRandom::setValue (double val, long startseed) +{ + Scalar::setValue (val); + setStartSeed(startseed); +} + +void +RegisteredRandom::on_value_changed() +{ + if (setProgrammatically) { + setProgrammatically = false; + return; + } + + if (_wr->isUpdating()) { + return; + } + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + //Force exact 0 if decimals over to 6 + double val = getValue() < 1e-6 && getValue() > -1e-6?0.0:getValue(); + os << val << ';' << getStartSeed(); + write_to_xml(os.str().c_str()); + _wr->setUpdating (false); +} + +/*######################################### + * Registered FONT-BUTTON + */ + +RegisteredFontButton::~RegisteredFontButton() +{ + _signal_font_set.disconnect(); +} + +RegisteredFontButton::RegisteredFontButton ( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, + SPDocument* doc_in ) + : RegisteredWidget<FontButton>(label, tip) +{ + init_parent(key, wr, repr_in, doc_in); + _signal_font_set = signal_font_value_changed().connect (sigc::mem_fun (*this, &RegisteredFontButton::on_value_changed)); +} + +void +RegisteredFontButton::setValue (Glib::ustring fontspec) +{ + FontButton::setValue(fontspec); +} + +void +RegisteredFontButton::on_value_changed() +{ + + if (_wr->isUpdating()) + return; + + _wr->setUpdating (true); + + Inkscape::SVGOStringStream os; + os << getValue(); + + write_to_xml(os.str().c_str()); + + _wr->setUpdating (false); +} + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registered-widget.h b/src/ui/widget/registered-widget.h new file mode 100644 index 0000000..d0b728a --- /dev/null +++ b/src/ui/widget/registered-widget.h @@ -0,0 +1,458 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ +#define INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ + +#include <2geom/affine.h> +#include "xml/node.h" +#include "registry.h" + +#include "ui/widget/scalar.h" +#include "ui/widget/scalar-unit.h" +#include "ui/widget/point.h" +#include "ui/widget/text.h" +#include "ui/widget/random.h" +#include "ui/widget/unit-menu.h" +#include "ui/widget/font-button.h" +#include "ui/widget/color-picker.h" +#include "inkscape.h" + +#include "document.h" +#include "document-undo.h" +#include "desktop.h" +#include "object/sp-namedview.h" + +#include <gtkmm/checkbutton.h> + +class SPDocument; + +namespace Gtk { + class HScale; + class RadioButton; + class SpinButton; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +template <class W> +class RegisteredWidget : public W { +public: + void set_undo_parameters(const unsigned int _event_type, Glib::ustring _event_description) + { + event_type = _event_type; + event_description = _event_description; + write_undo = true; + } + void set_xml_target(Inkscape::XML::Node *xml_node, SPDocument *document) + { + repr = xml_node; + doc = document; + } + + bool is_updating() {if (_wr) return _wr->isUpdating(); else return false;} + +protected: + RegisteredWidget() : W() { construct(); } + template< typename A > + explicit RegisteredWidget( A& a ): W( a ) { construct(); } + template< typename A, typename B > + RegisteredWidget( A& a, B& b ): W( a, b ) { construct(); } + template< typename A, typename B, typename C > + RegisteredWidget( A& a, B& b, C* c ): W( a, b, c ) { construct(); } + template< typename A, typename B, typename C > + RegisteredWidget( A& a, B& b, C& c ): W( a, b, c ) { construct(); } + template< typename A, typename B, typename C, typename D > + RegisteredWidget( A& a, B& b, C c, D d ): W( a, b, c, d ) { construct(); } + template< typename A, typename B, typename C, typename D, typename E > + RegisteredWidget( A& a, B& b, C& c, D d, E e ): W( a, b, c, d, e ) { construct(); } + template< typename A, typename B, typename C, typename D, typename E , typename F> + RegisteredWidget( A& a, B& b, C c, D& d, E& e, F* f): W( a, b, c, d, e, f) { construct(); } + template< typename A, typename B, typename C, typename D, typename E , typename F, typename G> + RegisteredWidget( A& a, B& b, C& c, D& d, E& e, F f, G& g): W( a, b, c, d, e, f, g) { construct(); } + + ~RegisteredWidget() override = default;; + + void init_parent(const Glib::ustring& key, Registry& wr, Inkscape::XML::Node* repr_in, SPDocument *doc_in) + { + _wr = ≀ + _key = key; + repr = repr_in; + doc = doc_in; + if (repr && !doc) // doc cannot be NULL when repr is not NULL + g_warning("Initialization of registered widget using defined repr but with doc==NULL"); + } + + void write_to_xml(const char * svgstr) + { + // Use local repr here. When repr is specified, use that one, but + // if repr==NULL, get the repr of namedview of active desktop. + Inkscape::XML::Node *local_repr = repr; + SPDocument *local_doc = doc; + if (!local_repr) { + // no repr specified, use active desktop's namedview's repr + SPDesktop* dt = SP_ACTIVE_DESKTOP; + local_repr = reinterpret_cast<SPObject *>(dt->getNamedView())->getRepr(); + local_doc = dt->getDocument(); + } + + bool saved = DocumentUndo::getUndoSensitive(local_doc); + DocumentUndo::setUndoSensitive(local_doc, false); + const char * svgstr_old = local_repr->attribute(_key.c_str()); + if (!write_undo) { + local_repr->setAttribute(_key, svgstr); + } + DocumentUndo::setUndoSensitive(local_doc, saved); + if (svgstr_old && svgstr && strcmp(svgstr_old,svgstr)) { + local_doc->setModifiedSinceSave(); + } + + if (write_undo) { + local_repr->setAttribute(_key, svgstr); + DocumentUndo::done(local_doc, event_type, event_description); + } + } + + Registry * _wr; + Glib::ustring _key; + Inkscape::XML::Node * repr; + SPDocument * doc; + unsigned int event_type; + Glib::ustring event_description; + bool write_undo; + +private: + void construct() { + _wr = nullptr; + repr = nullptr; + doc = nullptr; + write_undo = false; + event_type = 0; //SP_VERB_INVALID + } +}; + +//####################################################### + +class RegisteredCheckButton : public RegisteredWidget<Gtk::CheckButton> { +public: + ~RegisteredCheckButton() override; + RegisteredCheckButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=false, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *active_str = "true", char const *inactive_str = "false"); + + void setActive (bool); + + std::list<Gtk::Widget*> _slavewidgets; + + // a slave button is only sensitive when the master button is active + // i.e. a slave button is greyed-out when the master button is not checked + + void setSlaveWidgets(std::list<Gtk::Widget*> btns) { + _slavewidgets = btns; + } + + bool setProgrammatically; // true if the value was set by setActive, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + char const *_active_str, *_inactive_str; + sigc::connection _toggled_connection; + void on_toggled() override; +}; + +class RegisteredToggleButton : public RegisteredWidget<Gtk::ToggleButton> { +public: + ~RegisteredToggleButton() override; + RegisteredToggleButton (const Glib::ustring& label, const Glib::ustring& tip, const Glib::ustring& key, Registry& wr, bool right=true, Inkscape::XML::Node* repr_in=nullptr, SPDocument *doc_in=nullptr, char const *icon_active = "true", char const *icon_inactive = "false"); + + void setActive (bool); + + std::list<Gtk::Widget*> _slavewidgets; + + // a slave button is only sensitive when the master button is active + // i.e. a slave button is greyed-out when the master button is not checked + + void setSlaveWidgets(std::list<Gtk::Widget*> btns) { + _slavewidgets = btns; + } + + bool setProgrammatically; // true if the value was set by setActive, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + sigc::connection _toggled_connection; + void on_toggled() override; +}; + +class RegisteredUnitMenu : public RegisteredWidget<Labelled> { +public: + ~RegisteredUnitMenu() override; + RegisteredUnitMenu ( const Glib::ustring& label, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + void setUnit (const Glib::ustring); + Unit const * getUnit() const { return static_cast<UnitMenu*>(_widget)->getUnit(); }; + UnitMenu* getUnitMenu() const { return static_cast<UnitMenu*>(_widget); }; + sigc::connection _changed_connection; + +protected: + void on_changed(); +}; + +// Allow RegisteredScalarUnit to output lengths in 'user units' (which may have direction dependent +// scale factors). +enum RSU_UserUnits { + RSU_none, + RSU_x, + RSU_y +}; + +class RegisteredScalarUnit : public RegisteredWidget<ScalarUnit> { +public: + ~RegisteredScalarUnit() override; + RegisteredScalarUnit ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const RegisteredUnitMenu &rum, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr, + RSU_UserUnits _user_units = RSU_none ); + +protected: + sigc::connection _value_changed_connection; + UnitMenu *_um; + void on_value_changed(); + RSU_UserUnits _user_units; +}; + +class RegisteredScalar : public RegisteredWidget<Scalar> { +public: + ~RegisteredScalar() override; + RegisteredScalar (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); +protected: + sigc::connection _value_changed_connection; + void on_value_changed(); +}; + +class RegisteredText : public RegisteredWidget<Text> { +public: + ~RegisteredText() override; + RegisteredText (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + +protected: + sigc::connection _activate_connection; + void on_activate(); +}; + +class RegisteredColorPicker : public RegisteredWidget<LabelledColorPicker> { +public: + ~RegisteredColorPicker() override; + + RegisteredColorPicker (const Glib::ustring& label, + const Glib::ustring& title, + const Glib::ustring& tip, + const Glib::ustring& ckey, + const Glib::ustring& akey, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setRgba32 (guint32); + void closeWindow(); + +protected: + Glib::ustring _ckey, _akey; + void on_changed (guint32); + sigc::connection _changed_connection; +}; + +class RegisteredSuffixedInteger : public RegisteredWidget<Scalar> { +public: + ~RegisteredSuffixedInteger() override; + RegisteredSuffixedInteger ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& suffix, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false + +protected: + sigc::connection _changed_connection; + void on_value_changed(); +}; + +class RegisteredRadioButtonPair : public RegisteredWidget<Gtk::HBox> { +public: + ~RegisteredRadioButtonPair() override; + RegisteredRadioButtonPair ( const Glib::ustring& label, + const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + void setValue (bool second); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false +protected: + Gtk::RadioButton *_rb1, *_rb2; + sigc::connection _changed_connection; + void on_value_changed(); +}; + +class RegisteredPoint : public RegisteredWidget<Point> { +public: + ~RegisteredPoint() override; + RegisteredPoint ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); +}; + + +class RegisteredTransformedPoint : public RegisteredWidget<Point> { +public: + ~RegisteredTransformedPoint() override; + RegisteredTransformedPoint ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + // redefine setValue, because transform must be applied + void setValue(Geom::Point const & p); + + void setTransform(Geom::Affine const & canvas_to_svg); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); + + Geom::Affine to_svg; +}; + + +class RegisteredVector : public RegisteredWidget<Point> { +public: + ~RegisteredVector() override; + RegisteredVector (const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr ); + + // redefine setValue, because transform must be applied + void setValue(Geom::Point const & p); + void setValue(Geom::Point const & p, Geom::Point const & origin); + + /** + * Changes the widgets text to polar coordinates. The SVG output will still be a normal carthesian vector. + * Careful: when calling getValue(), the return value's X-coord will be the angle, Y-value will be the distance/length. + * After changing the coords type (polar/non-polar), the value has to be reset (setValue). + */ + void setPolarCoords(bool polar_coords = true); + +protected: + sigc::connection _value_x_changed_connection; + sigc::connection _value_y_changed_connection; + void on_value_changed(); + + Geom::Point _origin; + bool _polar_coords; +}; + + +class RegisteredRandom : public RegisteredWidget<Random> { +public: + ~RegisteredRandom() override; + RegisteredRandom ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setValue (double val, long startseed); + +protected: + sigc::connection _value_changed_connection; + sigc::connection _reseeded_connection; + void on_value_changed(); +}; + +class RegisteredFontButton : public RegisteredWidget<FontButton> { +public: + ~RegisteredFontButton() override; + RegisteredFontButton ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Registry& wr, + Inkscape::XML::Node* repr_in = nullptr, + SPDocument *doc_in = nullptr); + + void setValue (Glib::ustring fontspec); + +protected: + sigc::connection _signal_font_set; + void on_value_changed(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_REGISTERED_WIDGET__H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registry.cpp b/src/ui/widget/registry.cpp new file mode 100644 index 0000000..2834007 --- /dev/null +++ b/src/ui/widget/registry.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "registry.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + +Registry::Registry() : _updating(false) {} + +Registry::~Registry() = default; + +bool +Registry::isUpdating() +{ + return _updating; +} + +void +Registry::setUpdating (bool upd) +{ + _updating = upd; +} + +//==================================================== + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/registry.h b/src/ui/widget/registry.h new file mode 100644 index 0000000..190aaac --- /dev/null +++ b/src/ui/widget/registry.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UI_WIDGET_REGISTRY__H +#define INKSCAPE_UI_WIDGET_REGISTRY__H + +namespace Gtk { + class Object; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry { +public: + Registry(); + ~Registry(); + + bool isUpdating(); + void setUpdating (bool); + +protected: + bool _updating; +}; + +} // namespace Dialog +} // namespace UI +} // namespace Widget + +#endif // INKSCAPE_UI_WIDGET_REGISTRY__H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/rendering-options.cpp b/src/ui/widget/rendering-options.cpp new file mode 100644 index 0000000..549f494 --- /dev/null +++ b/src/ui/widget/rendering-options.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "preferences.h" +#include "rendering-options.h" +#include "util/units.h" +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +void RenderingOptions::_toggled() +{ + _frame_bitmap.set_sensitive(as_bitmap()); +} + +RenderingOptions::RenderingOptions () : + Gtk::VBox (), + _frame_backends ( Glib::ustring(_("Backend")) ), + _radio_vector ( Glib::ustring(_("Vector")) ), + _radio_bitmap ( Glib::ustring(_("Bitmap")) ), + _frame_bitmap ( Glib::ustring(_("Bitmap options")) ), + _dpi( _("DPI"), + Glib::ustring(_("Preferred resolution of rendering, " + "in dots per inch.")), + 1, + Glib::ustring(""), Glib::ustring(""), + false) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + // set up tooltips + _radio_vector.set_tooltip_text( + _("Render using Cairo vector operations. " + "The resulting image is usually smaller in file " + "size and can be arbitrarily scaled, but some " + "filter effects will not be correctly rendered.")); + _radio_bitmap.set_tooltip_text( + _("Render everything as bitmap. The resulting image " + "is usually larger in file size and cannot be " + "arbitrarily scaled without quality loss, but all " + "objects will be rendered exactly as displayed.")); + + set_border_width(2); + + Gtk::RadioButtonGroup group = _radio_vector.get_group (); + _radio_bitmap.set_group (group); + _radio_bitmap.signal_toggled().connect(sigc::mem_fun(*this, &RenderingOptions::_toggled)); + + // default to vector operations + if (prefs->getBool("/dialogs/printing/asbitmap", false)) { + _radio_bitmap.set_active(); + } else { + _radio_vector.set_active(); + } + + // configure default DPI + _dpi.setRange(Inkscape::Util::Quantity::convert(1, "in", "pt"),2400.0); + _dpi.setValue(prefs->getDouble("/dialogs/printing/dpi", + Inkscape::Util::Quantity::convert(1, "in", "pt"))); + _dpi.setIncrements(1.0,10.0); + _dpi.setDigits(0); + _dpi.update(); + + // fill frames + Gtk::VBox *box_vector = Gtk::manage( new Gtk::VBox () ); + box_vector->set_border_width (2); + box_vector->add (_radio_vector); + box_vector->add (_radio_bitmap); + _frame_backends.add (*box_vector); + + Gtk::HBox *box_bitmap = Gtk::manage( new Gtk::HBox () ); + box_bitmap->set_border_width (2); + box_bitmap->add (_dpi); + _frame_bitmap.add (*box_bitmap); + + // fill up container + add (_frame_backends); + add (_frame_bitmap); + + // initialize states + _toggled(); + + show_all_children (); +} + +bool +RenderingOptions::as_bitmap () +{ + return _radio_bitmap.get_active(); +} + +double +RenderingOptions::bitmap_dpi () +{ + return _dpi.getValue(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/rendering-options.h b/src/ui/widget/rendering-options.h new file mode 100644 index 0000000..2e10ff3 --- /dev/null +++ b/src/ui/widget/rendering-options.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Kees Cook <kees@outflux.net> + * + * Copyright (C) 2007 Kees Cook + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H +#define INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H + +#include "scalar.h" + +#include <gtkmm/frame.h> +#include <gtkmm/radiobutton.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A container for selecting rendering options. + */ +class RenderingOptions : public Gtk::VBox +{ +public: + + /** + * Construct a Rendering Options widget. + */ + RenderingOptions(); + + bool as_bitmap(); // should we render as a bitmap? + double bitmap_dpi(); // at what DPI should we render the bitmap? + +protected: + // Radio buttons to select desired rendering + Gtk::Frame _frame_backends; + Gtk::RadioButton _radio_vector; + Gtk::RadioButton _radio_bitmap; + + // Bitmap options + Gtk::Frame _frame_bitmap; + Scalar _dpi; // DPI of bitmap to render + + // callback for bitmap button + void _toggled(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_RENDERING_OPTIONS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp new file mode 100644 index 0000000..639f8d1 --- /dev/null +++ b/src/ui/widget/rotateable.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <2geom/point.h> +#include "ui/tools/tool-base.h" +#include "rotateable.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +Rotateable::Rotateable(): + axis(-M_PI/4), + maxdecl(M_PI/4) +{ + dragging = false; + working = false; + scrolling = false; + modifier = 0; + current_axis = axis; + + signal_button_press_event().connect(sigc::mem_fun(*this, &Rotateable::on_click)); + signal_motion_notify_event().connect(sigc::mem_fun(*this, &Rotateable::on_motion)); + signal_button_release_event().connect(sigc::mem_fun(*this, &Rotateable::on_release)); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &Rotateable::on_scroll)); + +} + +bool Rotateable::on_click(GdkEventButton *event) { + if (event->button == 1) { + drag_started_x = event->x; + drag_started_y = event->y; + modifier = get_single_modifier(modifier, event->state); + dragging = true; + working = false; + current_axis = axis; + return true; + } + return false; +} + +guint Rotateable::get_single_modifier(guint old, guint state) { + + if (old == 0 || old == 3) { + if (state & GDK_CONTROL_MASK) + return 1; // ctrl + if (state & GDK_SHIFT_MASK) + return 2; // shift + if (state & GDK_MOD1_MASK) + return 3; // alt + return 0; + } else { + if (!(state & GDK_CONTROL_MASK) && !(state & GDK_SHIFT_MASK)) { + if (state & GDK_MOD1_MASK) + return 3; // alt + else + return 0; // none + } + if (old == 1) { + if (state & GDK_SHIFT_MASK && !(state & GDK_CONTROL_MASK)) + return 2; // shift + if (state & GDK_MOD1_MASK && !(state & GDK_CONTROL_MASK)) + return 3; // alt + return 1; + } + if (old == 2) { + if (state & GDK_CONTROL_MASK && !(state & GDK_SHIFT_MASK)) + return 1; // ctrl + if (state & GDK_MOD1_MASK && !(state & GDK_SHIFT_MASK)) + return 3; // alt + return 2; + } + return old; + } +} + + +bool Rotateable::on_motion(GdkEventMotion *event) { + if (dragging) { + double dist = Geom::L2(Geom::Point(event->x, event->y) - Geom::Point(drag_started_x, drag_started_y)); + double angle = atan2(event->y - drag_started_y, event->x - drag_started_x); + if (dist > 20) { + working = true; + double force = CLAMP (-(angle - current_axis)/maxdecl, -1, 1); + if (fabs(force) < 0.002) + force = 0; // snap to zero + if (modifier != get_single_modifier(modifier, event->state)) { + // user has switched modifiers in mid drag, close past drag and start a new + // one, redefining axis temporarily + do_release(force, modifier); + current_axis = angle; + modifier = get_single_modifier(modifier, event->state); + } else { + do_motion(force, modifier); + } + } + Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); + return true; + } + return false; +} + + +bool Rotateable::on_release(GdkEventButton *event) { + if (dragging && working) { + double angle = atan2(event->y - drag_started_y, event->x - drag_started_x); + double force = CLAMP(-(angle - current_axis) / maxdecl, -1, 1); + if (fabs(force) < 0.002) + force = 0; // snap to zero + do_release(force, modifier); + current_axis = axis; + dragging = false; + working = false; + return true; + } + dragging = false; + working = false; + return false; +} + +bool Rotateable::on_scroll(GdkEventScroll* event) +{ + double change = 0.0; + + if (event->direction == GDK_SCROLL_UP) { + change = 1.0; + } else if (event->direction == GDK_SCROLL_DOWN) { + change = -1.0; + } else if (event->direction == GDK_SCROLL_SMOOTH) { + double delta_y_clamped = CLAMP(event->delta_y, -1.0, 1.0); // values > 1 result in excessive changes + change = 1.0 * -delta_y_clamped; + } else { + return FALSE; + } + + drag_started_x = event->x; + drag_started_y = event->y; + modifier = get_single_modifier(modifier, event->state); + dragging = false; + working = false; + scrolling = true; + current_axis = axis; + + do_scroll(change, modifier); + + dragging = false; + working = false; + scrolling = false; + + return TRUE; +} + +Rotateable::~Rotateable() = default; + + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/rotateable.h b/src/ui/widget/rotateable.h new file mode 100644 index 0000000..c174a09 --- /dev/null +++ b/src/ui/widget/rotateable.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_ROTATEABLE_H +#define INKSCAPE_UI_ROTATEABLE_H + +#include <gtkmm/box.h> +#include <gtkmm/eventbox.h> +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Widget adjustable by dragging it to rotate away from a zero-change axis. + */ +class Rotateable: public Gtk::EventBox +{ +public: + Rotateable(); + + ~Rotateable() override; + + bool on_click(GdkEventButton *event); + bool on_motion(GdkEventMotion *event); + bool on_release(GdkEventButton *event); + bool on_scroll(GdkEventScroll* event); + + double axis; + double current_axis; + double maxdecl; + bool scrolling; + +private: + double drag_started_x; + double drag_started_y; + guint modifier; + bool dragging; + bool working; + + guint get_single_modifier(guint old, guint state); + + virtual void do_motion (double /*by*/, guint /*state*/) {} + virtual void do_release (double /*by*/, guint /*state*/) {} + virtual void do_scroll (double /*by*/, guint /*state*/) {} +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_ROTATEABLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scalar-unit.cpp b/src/ui/widget/scalar-unit.cpp new file mode 100644 index 0000000..2b6d001 --- /dev/null +++ b/src/ui/widget/scalar-unit.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Derek P. Moore <derekm@hackunix.org> + * buliabyak@gmail.com + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "scalar-unit.h" +#include "spinbutton.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + UnitType unit_type, + Glib::ustring const &suffix, + Glib::ustring const &icon, + UnitMenu *unit_menu, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic), + _unit_menu(unit_menu), + _hundred_percent(0), + _absolute_is_increment(false), + _percentage_is_increment(false) +{ + if (_unit_menu == nullptr) { + _unit_menu = new UnitMenu(); + g_assert(_unit_menu); + _unit_menu->setUnitType(unit_type); + + remove(*_widget); + Gtk::Box *widget_holder = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 6); + widget_holder->pack_start(*_widget, Gtk::PACK_SHRINK); + widget_holder->pack_start(*Gtk::manage(_unit_menu), Gtk::PACK_SHRINK); + pack_start(*Gtk::manage(widget_holder), Gtk::PACK_SHRINK); + } + _unit_menu->signal_changed() + .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed)); + + static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu); + + lastUnits = _unit_menu->getUnitAbbr(); +} + +ScalarUnit::ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + ScalarUnit &take_unitmenu, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Scalar(label, tooltip, suffix, icon, mnemonic), + _unit_menu(take_unitmenu._unit_menu), + _hundred_percent(0), + _absolute_is_increment(false), + _percentage_is_increment(false) +{ + _unit_menu->signal_changed() + .connect_notify(sigc::mem_fun(*this, &ScalarUnit::on_unit_changed)); + + static_cast<SpinButton*>(_widget)->setUnitMenu(_unit_menu); + + lastUnits = _unit_menu->getUnitAbbr(); +} + + +void ScalarUnit::initScalar(double min_value, double max_value) +{ + g_assert(_unit_menu != nullptr); + Scalar::setDigits(_unit_menu->getDefaultDigits()); + Scalar::setIncrements(_unit_menu->getDefaultStep(), + _unit_menu->getDefaultPage()); + Scalar::setRange(min_value, max_value); +} + +bool ScalarUnit::setUnit(Glib::ustring const &unit) +{ + g_assert(_unit_menu != nullptr); + // First set the unit + if (!_unit_menu->setUnit(unit)) { + return false; + } + lastUnits = unit; + return true; +} + +void ScalarUnit::setUnitType(UnitType unit_type) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->setUnitType(unit_type); + lastUnits = _unit_menu->getUnitAbbr(); +} + +void ScalarUnit::resetUnitType(UnitType unit_type) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->resetUnitType(unit_type); + lastUnits = _unit_menu->getUnitAbbr(); +} + +Unit const * ScalarUnit::getUnit() const +{ + g_assert(_unit_menu != nullptr); + return _unit_menu->getUnit(); +} + +UnitType ScalarUnit::getUnitType() const +{ + g_assert(_unit_menu); + return _unit_menu->getUnitType(); +} + +void ScalarUnit::setValue(double number, Glib::ustring const &units) +{ + g_assert(_unit_menu != nullptr); + _unit_menu->setUnit(units); + Scalar::setValue(number); +} + +void ScalarUnit::setValueKeepUnit(double number, Glib::ustring const &units) +{ + g_assert(_unit_menu != nullptr); + if (units == "") { + // set the value in the default units + Scalar::setValue(number); + } else { + double conversion = _unit_menu->getConversion(units); + Scalar::setValue(number / conversion); + } +} + +void ScalarUnit::setValue(double number) +{ + Scalar::setValue(number); +} + +double ScalarUnit::getValue(Glib::ustring const &unit_name) const +{ + g_assert(_unit_menu != nullptr); + if (unit_name == "") { + // Return the value in the default units + return Scalar::getValue(); + } else { + double conversion = _unit_menu->getConversion(unit_name); + return conversion * Scalar::getValue(); + } +} + +void ScalarUnit::grabFocusAndSelectEntry() +{ + _widget->grab_focus(); + static_cast<SpinButton*>(_widget)->select_region(0, 20); +} + + +void ScalarUnit::setHundredPercent(double number) +{ + _hundred_percent = number; +} + +void ScalarUnit::setAbsoluteIsIncrement(bool value) +{ + _absolute_is_increment = value; +} + +void ScalarUnit::setPercentageIsIncrement(bool value) +{ + _percentage_is_increment = value; +} + +double ScalarUnit::PercentageToAbsolute(double value) +{ + // convert from percent to absolute + double convertedVal = 0; + double hundred_converted = _hundred_percent / _unit_menu->getConversion("px"); // _hundred_percent is in px + if (_percentage_is_increment) + value += 100; + convertedVal = 0.01 * hundred_converted * value; + if (_absolute_is_increment) + convertedVal -= hundred_converted; + + return convertedVal; +} + +double ScalarUnit::AbsoluteToPercentage(double value) +{ + double convertedVal = 0; + // convert from absolute to percent + if (_hundred_percent == 0) { + if (_percentage_is_increment) + convertedVal = 0; + else + convertedVal = 100; + } else { + double hundred_converted = _hundred_percent / _unit_menu->getConversion("px", lastUnits); // _hundred_percent is in px + if (_absolute_is_increment) + value += hundred_converted; + convertedVal = 100 * value / hundred_converted; + if (_percentage_is_increment) + convertedVal -= 100; + } + + return convertedVal; +} + +double ScalarUnit::getAsPercentage() +{ + double convertedVal = AbsoluteToPercentage(Scalar::getValue()); + return convertedVal; +} + + +void ScalarUnit::setFromPercentage(double value) +{ + double absolute = PercentageToAbsolute(value); + Scalar::setValue(absolute); +} + + +void ScalarUnit::on_unit_changed() +{ + g_assert(_unit_menu != nullptr); + + Glib::ustring abbr = _unit_menu->getUnitAbbr(); + _suffix->set_label(abbr); + + Inkscape::Util::Unit const *new_unit = unit_table.getUnit(abbr); + Inkscape::Util::Unit const *old_unit = unit_table.getUnit(lastUnits); + + double convertedVal = 0; + if (old_unit->type == UNIT_TYPE_DIMENSIONLESS && new_unit->type == UNIT_TYPE_LINEAR) { + convertedVal = PercentageToAbsolute(Scalar::getValue()); + } else if (old_unit->type == UNIT_TYPE_LINEAR && new_unit->type == UNIT_TYPE_DIMENSIONLESS) { + convertedVal = AbsoluteToPercentage(Scalar::getValue()); + } else { + double conversion = _unit_menu->getConversion(lastUnits); + convertedVal = Scalar::getValue() / conversion; + } + Scalar::setValue(convertedVal); + + lastUnits = abbr; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scalar-unit.h b/src/ui/widget/scalar-unit.h new file mode 100644 index 0000000..e82c41d --- /dev/null +++ b/src/ui/widget/scalar-unit.h @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Bryce Harrington <bryce@bryceharrington.org> + * Derek P. Moore <derekm@hackunix.org> + * buliabyak@gmail.com + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SCALAR_UNIT_H +#define INKSCAPE_UI_WIDGET_SCALAR_UNIT_H + +#include "scalar.h" +#include "unit-menu.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional icon or suffix, for + * entering the values of various unit types. + * + * A ScalarUnit is a control for entering, viewing, or manipulating + * numbers with units. This differs from ordinary numbers like 2 or + * 3.14 because the number portion of a scalar *only* has meaning + * when considered with its unit type. For instance, 12 m and 12 in + * have very different actual values, but 1 m and 100 cm have the same + * value. The ScalarUnit allows us to abstract the presentation of + * the scalar to the user from the internal representations used by + * the program. + */ +class ScalarUnit : public Scalar +{ +public: + /** + * Construct a ScalarUnit. + * + * @param label Label. + * @param unit_type Unit type (defaults to UNIT_TYPE_LINEAR). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param unit_menu UnitMenu drop down; if not specified, one will be created + * and displayed after the widget (defaults to NULL). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + UnitType unit_type = UNIT_TYPE_LINEAR, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + UnitMenu *unit_menu = nullptr, + bool mnemonic = true); + + /** + * Construct a ScalarUnit. + * + * @param label Label. + * @param tooltip Tooltip text. + * @param take_unitmenu Use the unitmenu from this parameter. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + ScalarUnit(Glib::ustring const &label, Glib::ustring const &tooltip, + ScalarUnit &take_unitmenu, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Initializes the scalar based on the settings in _unit_menu. + * Requires that _unit_menu has already been initialized. + */ + void initScalar(double min_value, double max_value); + + /** + * Gets the object for the currently selected unit. + */ + Unit const * getUnit() const; + + /** + * Gets the UnitType ID for the unit. + */ + UnitType getUnitType() const; + + /** + * Returns the value in the given unit system. + */ + double getValue(Glib::ustring const &units) const; + + /** + * Sets the unit for the ScalarUnit widget. + */ + bool setUnit(Glib::ustring const &units); + + /** + * Adds the unit type to the ScalarUnit widget. + */ + void setUnitType(UnitType unit_type); + + /** + * Resets the unit type for the ScalarUnit widget. + */ + void resetUnitType(UnitType unit_type); + + /** + * Sets the number and unit system. + */ + void setValue(double number, Glib::ustring const &units); + + /** + * Convert and sets the number only and keeps the current unit. + */ + void setValueKeepUnit(double number, Glib::ustring const &units); + + /** + * Sets the number only. + */ + void setValue(double number); + + /** + * Grab focus, and select the text that is in the entry field. + */ + void grabFocusAndSelectEntry(); + + void setHundredPercent(double number); + + void setAbsoluteIsIncrement(bool value); + + void setPercentageIsIncrement(bool value); + + /** + * Convert value from % to absolute, using _hundred_percent and *_is_increment flags. + */ + double PercentageToAbsolute(double value); + + /** + * Convert value from absolute to %, using _hundred_percent and *_is_increment flags. + */ + double AbsoluteToPercentage(double value); + + /** + * Assuming the current unit is absolute, get the corresponding % value. + */ + double getAsPercentage(); + + /** + * Assuming the current unit is absolute, set the value corresponding to a given %. + */ + void setFromPercentage(double value); + + /** + * Signal handler for updating the value and suffix label when unit is changed. + */ + void on_unit_changed(); + +protected: + UnitMenu *_unit_menu; + + double _hundred_percent; // the length that corresponds to 100%, in px, for %-to/from-absolute conversions + + bool _absolute_is_increment; // if true, 120% with _hundred_percent=100px gets converted to/from 20px; otherwise, to/from 120px + bool _percentage_is_increment; // if true, 120px with _hundred_percent=100px gets converted to/from 20%; otherwise, to/from 120% + // if both are true, 20px is converted to/from 20% if _hundred_percent=100px + + Glib::ustring lastUnits; // previously selected unit, for conversions +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SCALAR_UNIT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scalar.cpp b/src/ui/widget/scalar.cpp new file mode 100644 index 0000000..471de49 --- /dev/null +++ b/src/ui/widget/scalar.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2004-2011 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "scalar.h" +#include "spinbutton.h" +#include <gtkmm/scale.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(0.0, digits), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Scalar::Scalar(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new SpinButton(adjust, 0.0, digits), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +unsigned Scalar::getDigits() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_digits(); +} + +double Scalar::getStep() const +{ + g_assert(_widget != nullptr); + double step, page; + static_cast<SpinButton*>(_widget)->get_increments(step, page); + return step; +} + +double Scalar::getPage() const +{ + g_assert(_widget != nullptr); + double step, page; + static_cast<SpinButton*>(_widget)->get_increments(step, page); + return page; +} + +double Scalar::getRangeMin() const +{ + g_assert(_widget != nullptr); + double min, max; + static_cast<SpinButton*>(_widget)->get_range(min, max); + return min; +} + +double Scalar::getRangeMax() const +{ + g_assert(_widget != nullptr); + double min, max; + static_cast<SpinButton*>(_widget)->get_range(min, max); + return max; +} + +double Scalar::getValue() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_value(); +} + +int Scalar::getValueAsInt() const +{ + g_assert(_widget != nullptr); + return static_cast<SpinButton*>(_widget)->get_value_as_int(); +} + + +void Scalar::setDigits(unsigned digits) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_digits(digits); +} + +void Scalar::setIncrements(double step, double /*page*/) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_increments(step, 0); +} + +void Scalar::setRange(double min, double max) +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->set_range(min, max); +} + +void Scalar::setValue(double value, bool setProg) +{ + g_assert(_widget != nullptr); + if (setProg) { + setProgrammatically = true; // callback is supposed to reset back, if it cares + } + static_cast<SpinButton*>(_widget)->set_value(value); +} + +void Scalar::setWidthChars(unsigned chars) +{ + g_assert(_widget != NULL); + static_cast<SpinButton*>(_widget)->set_width_chars(chars); +} + +void Scalar::update() +{ + g_assert(_widget != nullptr); + static_cast<SpinButton*>(_widget)->update(); +} + +void Scalar::addSlider() +{ + auto scale = new Gtk::Scale(static_cast<SpinButton*>(_widget)->get_adjustment()); + scale->set_draw_value(false); + add (*manage (scale)); +} + +Glib::SignalProxy0<void> Scalar::signal_value_changed() +{ + return static_cast<SpinButton*>(_widget)->signal_value_changed(); +} + +Glib::SignalProxy1<bool, GdkEventButton*> Scalar::signal_button_release_event() +{ + return static_cast<SpinButton*>(_widget)->signal_button_release_event(); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/scalar.h b/src/ui/widget/scalar.h new file mode 100644 index 0000000..29a14d1 --- /dev/null +++ b/src/ui/widget/scalar.h @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Derek P. Moore <derekm@hackunix.org> + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SCALAR_H +#define INKSCAPE_UI_WIDGET_SCALAR_H + +#include "labelled.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with spin buttons and optional + * icon or suffix, for entering arbitrary number values. + */ +class Scalar : public Labelled +{ +public: + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param digits Number of decimal digits to display. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + unsigned digits, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Construct a Scalar Widget. + * + * @param label Label. + * @param adjust Adjustment to use for the SpinButton. + * @param digits Number of decimal digits to display (defaults to 0). + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to true). + */ + Scalar(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::RefPtr<Gtk::Adjustment> &adjust, + unsigned digits = 0, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Fetches the precision of the spin button. + */ + unsigned getDigits() const; + + /** + * Gets the current step increment used by the spin button. + */ + double getStep() const; + + /** + * Gets the current page increment used by the spin button. + */ + double getPage() const; + + /** + * Gets the minimum range value allowed for the spin button. + */ + double getRangeMin() const; + + /** + * Gets the maximum range value allowed for the spin button. + */ + double getRangeMax() const; + + bool getSnapToTicks() const; + + /** + * Get the value in the spin_button. + */ + double getValue() const; + + /** + * Get the value spin_button represented as an integer. + */ + int getValueAsInt() const; + + /** + * Sets the precision to be displayed by the spin button. + */ + void setDigits(unsigned digits); + + /** + * Sets the step and page increments for the spin button. + * @todo Remove the second parameter - deprecated + */ + void setIncrements(double step, double page); + + /** + * Sets the minimum and maximum range allowed for the spin button. + */ + void setRange(double min, double max); + + /** + * Sets the value of the spin button. + */ + void setValue(double value, bool setProg = true); + + /** + * Sets the width of the spin button by number of characters. + */ + void setWidthChars(unsigned chars); + + /** + * Manually forces an update of the spin button. + */ + void update(); + + /** + * Adds a slider (HScale) to the left of the spinbox. + */ + void addSlider(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_value_changed(); + + /** + * Signal raised when the spin button's pressed. + */ + Glib::SignalProxy1<bool, GdkEventButton*> signal_button_release_event(); + + /** + * true if the value was set by setValue, not changed by the user; + * if a callback checks it, it must reset it back to false. + */ + bool setProgrammatically; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SCALAR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp new file mode 100644 index 0000000..9417a9f --- /dev/null +++ b/src/ui/widget/selected-style.cpp @@ -0,0 +1,1488 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * buliabyak@gmail.com + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005 author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "selected-style.h" + +#include <gtkmm/separatormenuitem.h> + + +#include "desktop-style.h" +#include "document-undo.h" +#include "gradient-chemistry.h" +#include "message-context.h" +#include "selection.h" +#include "sp-cursor.h" + +#include "display/sp-canvas.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-namedview.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "style.h" + +#include "ui/pixmaps/cursor-adj-a.xpm" +#include "ui/pixmaps/cursor-adj-h.xpm" +#include "ui/pixmaps/cursor-adj-l.xpm" +#include "ui/pixmaps/cursor-adj-s.xpm" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/fill-and-stroke.h" +#include "ui/dialog/panel-dialog.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/color-preview.h" + +#include "widgets/ege-paint-def.h" +#include "widgets/gradient-image.h" +#include "widgets/spinbutton-events.h" +#include "widgets/spw-utilities.h" +#include "widgets/widget-sizes.h" + +using Inkscape::Util::unit_table; + +static gdouble const _sw_presets[] = { 32 , 16 , 10 , 8 , 6 , 4 , 3 , 2 , 1.5 , 1 , 0.75 , 0.5 , 0.25 , 0.1 }; +static gchar const *const _sw_presets_str[] = {"32", "16", "10", "8", "6", "4", "3", "2", "1.5", "1", "0.75", "0.5", "0.25", "0.1"}; + +static void +ss_selection_changed (Inkscape::Selection *, gpointer data) +{ + Inkscape::UI::Widget::SelectedStyle *ss = (Inkscape::UI::Widget::SelectedStyle *) data; + ss->update(); +} + +static void +ss_selection_modified( Inkscape::Selection *selection, guint flags, gpointer data ) +{ + // Don't update the style when dragging or doing non-style related changes + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + ss_selection_changed (selection, data); + } +} + +static void +ss_subselection_changed( gpointer /*dragger*/, gpointer data ) +{ + ss_selection_changed (nullptr, data); +} + +namespace { + +void clearTooltip( Gtk::Widget &widget ) +{ + widget.set_tooltip_text(""); + widget.set_has_tooltip(false); +} + +} // namespace + +namespace Inkscape { +namespace UI { +namespace Widget { + + +struct DropTracker { + SelectedStyle* parent; + int item; +}; + +/* Drag and Drop */ +enum ui_drop_target_info { + APP_OSWB_COLOR +}; + +//TODO: warning: deprecated conversion from string constant to ‘gchar*’ +// +//Turn out to be warnings that we should probably leave in place. The +// pointers/types used need to be read-only. So until we correct the using +// code, those warnings are actually desired. They say "Hey! Fix this". We +// definitely don't want to hide/ignore them. --JonCruz +static const GtkTargetEntry ui_drop_target_entries [] = { + {g_strdup("application/x-oswb-color"), 0, APP_OSWB_COLOR} +}; + +static guint nui_drop_target_entries = G_N_ELEMENTS(ui_drop_target_entries); + +/* convenience function */ +static Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop); + +SelectedStyle::SelectedStyle(bool /*layout*/) + : current_stroke_width(0) + , _sw_unit(nullptr) + , _desktop(nullptr) + , _table() + , _fill_label(_("Fill:")) + , _stroke_label(_("Stroke:")) + , _opacity_label(_("O:")) + , _fill_place(this, SS_FILL) + , _stroke_place(this, SS_STROKE) + , _fill_flag_place() + , _stroke_flag_place() + , _opacity_place() + , _opacity_adjustment(Gtk::Adjustment::create(100, 0.0, 100, 1.0, 10.0)) + , _opacity_sb(0.02, 0) + , _fill() + , _stroke() + , _stroke_width_place(this) + , _stroke_width("") + , _fill_empty_space("") + , _opacity_blocked(false) +{ + set_name("SelectedStyle"); + _drop[0] = _drop[1] = nullptr; + _dropEnabled[0] = _dropEnabled[1] = false; + + _fill_label.set_halign(Gtk::ALIGN_START); + _fill_label.set_valign(Gtk::ALIGN_CENTER); + _fill_label.set_margin_top(0); + _fill_label.set_margin_bottom(0); + _stroke_label.set_halign(Gtk::ALIGN_START); + _stroke_label.set_valign(Gtk::ALIGN_CENTER); + _stroke_label.set_margin_top(0); + _stroke_label.set_margin_bottom(0); + _opacity_label.set_halign(Gtk::ALIGN_START); + _opacity_label.set_valign(Gtk::ALIGN_CENTER); + _opacity_label.set_margin_top(0); + _opacity_label.set_margin_bottom(0); + _stroke_width.set_name("monoStrokeWidth"); + _fill_empty_space.set_name("fillEmptySpace"); + + _fill_label.set_margin_start(0); + _fill_label.set_margin_end(0); + _stroke_label.set_margin_start(0); + _stroke_label.set_margin_end(0); + _opacity_label.set_margin_start(0); + _opacity_label.set_margin_end(0); + + _table.set_column_spacing(2); + _table.set_row_spacing(0); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + + _na[i].set_markup (_("N/A")); + _na[i].show_all(); + __na[i] = (_("Nothing selected")); + + if (i == SS_FILL) { + _none[i].set_markup (C_("Fill", "<i>None</i>")); + } else { + _none[i].set_markup (C_("Stroke", "<i>None</i>")); + } + _none[i].show_all(); + __none[i] = (i == SS_FILL)? (C_("Fill and stroke", "No fill, middle-click for black fill")) : (C_("Fill and stroke", "No stroke, middle-click for black stroke")); + + _pattern[i].set_markup (_("Pattern")); + _pattern[i].show_all(); + __pattern[i] = (i == SS_FILL)? (_("Pattern fill")) : (_("Pattern stroke")); + + _hatch[i].set_markup(_("Hatch")); + _hatch[i].show_all(); + __hatch[i] = (i == SS_FILL) ? (_("Hatch fill")) : (_("Hatch stroke")); + + _lgradient[i].set_markup (_("<b>L</b>")); + _lgradient[i].show_all(); + __lgradient[i] = (i == SS_FILL)? (_("Linear gradient fill")) : (_("Linear gradient stroke")); + + _gradient_preview_l[i] = GTK_WIDGET(sp_gradient_image_new (nullptr)); + _gradient_box_l[i].pack_start(_lgradient[i]); + _gradient_box_l[i].pack_start(*(Glib::wrap(_gradient_preview_l[i]))); + _gradient_box_l[i].show_all(); + + _rgradient[i].set_markup (_("<b>R</b>")); + _rgradient[i].show_all(); + __rgradient[i] = (i == SS_FILL)? (_("Radial gradient fill")) : (_("Radial gradient stroke")); + + _gradient_preview_r[i] = GTK_WIDGET(sp_gradient_image_new (nullptr)); + _gradient_box_r[i].pack_start(_rgradient[i]); + _gradient_box_r[i].pack_start(*(Glib::wrap(_gradient_preview_r[i]))); + _gradient_box_r[i].show_all(); + +#ifdef WITH_MESH + _mgradient[i].set_markup (_("<b>M</b>")); + _mgradient[i].show_all(); + __mgradient[i] = (i == SS_FILL)? (_("Mesh gradient fill")) : (_("Mesh gradient stroke")); + + _gradient_preview_m[i] = GTK_WIDGET(sp_gradient_image_new (nullptr)); + _gradient_box_m[i].pack_start(_mgradient[i]); + _gradient_box_m[i].pack_start(*(Glib::wrap(_gradient_preview_m[i]))); + _gradient_box_m[i].show_all(); +#endif + + _many[i].set_markup (_("Different")); + _many[i].show_all(); + __many[i] = (i == SS_FILL)? (_("Different fills")) : (_("Different strokes")); + + _unset[i].set_markup (_("<b>Unset</b>")); + _unset[i].show_all(); + __unset[i] = (i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke")); + + _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0); + __color[i] = (i == SS_FILL)? (_("Flat color fill")) : (_("Flat color stroke")); + + // TRANSLATORS: A means "Averaged" + _averaged[i].set_markup (_("<b>a</b>")); + _averaged[i].show_all(); + __averaged[i] = (i == SS_FILL)? (_("Fill is averaged over selected objects")) : (_("Stroke is averaged over selected objects")); + + // TRANSLATORS: M means "Multiple" + _multiple[i].set_markup (_("<b>m</b>")); + _multiple[i].show_all(); + __multiple[i] = (i == SS_FILL)? (_("Multiple selected objects have the same fill")) : (_("Multiple selected objects have the same stroke")); + + _popup_edit[i].add(*(new Gtk::Label((i == SS_FILL)? _("Edit fill...") : _("Edit stroke..."), Gtk::ALIGN_START))); + _popup_edit[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_edit : &SelectedStyle::on_stroke_edit )); + + _popup_lastused[i].add(*(new Gtk::Label(_("Last set color"), Gtk::ALIGN_START))); + _popup_lastused[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_lastused : &SelectedStyle::on_stroke_lastused )); + + _popup_lastselected[i].add(*(new Gtk::Label(_("Last selected color"), Gtk::ALIGN_START))); + _popup_lastselected[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_lastselected : &SelectedStyle::on_stroke_lastselected )); + + _popup_invert[i].add(*(new Gtk::Label(_("Invert"), Gtk::ALIGN_START))); + _popup_invert[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_invert : &SelectedStyle::on_stroke_invert )); + + _popup_white[i].add(*(new Gtk::Label(_("White"), Gtk::ALIGN_START))); + _popup_white[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_white : &SelectedStyle::on_stroke_white )); + + _popup_black[i].add(*(new Gtk::Label(_("Black"), Gtk::ALIGN_START))); + _popup_black[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_black : &SelectedStyle::on_stroke_black )); + + _popup_copy[i].add(*(new Gtk::Label(_("Copy color"), Gtk::ALIGN_START))); + _popup_copy[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_copy : &SelectedStyle::on_stroke_copy )); + + _popup_paste[i].add(*(new Gtk::Label(_("Paste color"), Gtk::ALIGN_START))); + _popup_paste[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_paste : &SelectedStyle::on_stroke_paste )); + + _popup_swap[i].add(*(new Gtk::Label(_("Swap fill and stroke"), Gtk::ALIGN_START))); + _popup_swap[i].signal_activate().connect(sigc::mem_fun(*this, + &SelectedStyle::on_fillstroke_swap)); + + _popup_opaque[i].add(*(new Gtk::Label((i == SS_FILL)? _("Make fill opaque") : _("Make stroke opaque"), Gtk::ALIGN_START))); + _popup_opaque[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_opaque : &SelectedStyle::on_stroke_opaque )); + + //TRANSLATORS COMMENT: unset is a verb here + _popup_unset[i].add(*(new Gtk::Label((i == SS_FILL)? _("Unset fill") : _("Unset stroke"), Gtk::ALIGN_START))); + _popup_unset[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_unset : &SelectedStyle::on_stroke_unset )); + + _popup_remove[i].add(*(new Gtk::Label((i == SS_FILL)? _("Remove fill") : _("Remove stroke"), Gtk::ALIGN_START))); + _popup_remove[i].signal_activate().connect(sigc::mem_fun(*this, + (i == SS_FILL)? &SelectedStyle::on_fill_remove : &SelectedStyle::on_stroke_remove )); + + _popup[i].attach(_popup_edit[i], 0,1, 0,1); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 1,2); + _popup[i].attach(_popup_lastused[i], 0,1, 2,3); + _popup[i].attach(_popup_lastselected[i], 0,1, 3,4); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 4,5); + _popup[i].attach(_popup_invert[i], 0,1, 5,6); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 6,7); + _popup[i].attach(_popup_white[i], 0,1, 7,8); + _popup[i].attach(_popup_black[i], 0,1, 8,9); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 9,10); + _popup[i].attach(_popup_copy[i], 0,1, 10,11); + _popup_copy[i].set_sensitive(false); + _popup[i].attach(_popup_paste[i], 0,1, 11,12); + _popup[i].attach(_popup_swap[i], 0,1, 12,13); + _popup[i].attach(*(new Gtk::SeparatorMenuItem()), 0,1, 13,14); + _popup[i].attach(_popup_opaque[i], 0,1, 14,15); + _popup[i].attach(_popup_unset[i], 0,1, 15,16); + _popup[i].attach(_popup_remove[i], 0,1, 16,17); + _popup[i].show_all(); + + _mode[i] = SS_NA; + } + + { + int row = 0; + + Inkscape::Util::UnitTable::UnitMap m = unit_table.units(Inkscape::Util::UNIT_TYPE_LINEAR); + Inkscape::Util::UnitTable::UnitMap::iterator iter = m.begin(); + while(iter != m.end()) { + Gtk::RadioMenuItem *mi = Gtk::manage(new Gtk::RadioMenuItem(_sw_group)); + mi->add(*(new Gtk::Label(iter->first, Gtk::ALIGN_START))); + _unit_mis.push_back(mi); + Inkscape::Util::Unit const *u = unit_table.getUnit(iter->first); + mi->signal_activate().connect(sigc::bind<Inkscape::Util::Unit const *>(sigc::mem_fun(*this, &SelectedStyle::on_popup_units), u)); + _popup_sw.attach(*mi, 0,1, row, row+1); + row++; + ++iter; + } + + _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1); + row++; + + for (guint i = 0; i < G_N_ELEMENTS(_sw_presets_str); ++i) { + Gtk::MenuItem *mi = Gtk::manage(new Gtk::MenuItem()); + mi->add(*(new Gtk::Label(_sw_presets_str[i], Gtk::ALIGN_START))); + mi->signal_activate().connect(sigc::bind<int>(sigc::mem_fun(*this, &SelectedStyle::on_popup_preset), i)); + _popup_sw.attach(*mi, 0,1, row, row+1); + row++; + } + + _popup_sw.attach(*(new Gtk::SeparatorMenuItem()), 0,1, row, row+1); + row++; + + _popup_sw_remove.add(*(new Gtk::Label(_("Remove"), Gtk::ALIGN_START))); + _popup_sw_remove.signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_remove)); + _popup_sw.attach(_popup_sw_remove, 0,1, row, row+1); + row++; + + _popup_sw.show_all(); + } + + _fill_place.add(_na[SS_FILL]); + _fill_place.set_tooltip_text(__na[SS_FILL]); + _fill.pack_start(_fill_place, Gtk::PACK_SHRINK); + _fill.pack_start(_fill_empty_space, Gtk::PACK_SHRINK); + + _stroke_place.add(_na[SS_STROKE]); + _stroke_place.set_tooltip_text(__na[SS_STROKE]); + + _stroke.pack_start(_stroke_place); + _stroke_width_place.add(_stroke_width); + _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK); + + _opacity_sb.set_adjustment(_opacity_adjustment); + _opacity_sb.set_size_request (SELECTED_STYLE_SB_WIDTH, -1); + _opacity_sb.set_sensitive (false); + + _table.attach(_fill_label, 0, 0, 1, 1); + _table.attach(_stroke_label, 0, 1, 1, 1); + + _table.attach(_fill_flag_place, 1, 0, 1, 1); + _table.attach(_stroke_flag_place, 1, 1, 1, 1); + + _table.attach(_fill, 2, 0, 1, 1); + _table.attach(_stroke, 2, 1, 1, 1); + + _opacity_place.add(_opacity_label); + + _table.attach(_opacity_place, 4, 0, 1, 2); + _table.attach(_opacity_sb, 5, 0, 1, 2); + + pack_start(_table, true, true, 2); + + set_size_request (SELECTED_STYLE_WIDTH, -1); + + _drop[SS_FILL] = new DropTracker(); + ((DropTracker*)_drop[SS_FILL])->parent = this; + ((DropTracker*)_drop[SS_FILL])->item = SS_FILL; + + _drop[SS_STROKE] = new DropTracker(); + ((DropTracker*)_drop[SS_STROKE])->parent = this; + ((DropTracker*)_drop[SS_STROKE])->item = SS_STROKE; + + g_signal_connect(_stroke_place.gobj(), + "drag_data_received", + G_CALLBACK(dragDataReceived), + _drop[SS_STROKE]); + + g_signal_connect(_fill_place.gobj(), + "drag_data_received", + G_CALLBACK(dragDataReceived), + _drop[SS_FILL]); + + _fill_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_fill_click)); + _stroke_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_stroke_click)); + _opacity_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_click)); + _stroke_width_place.signal_button_press_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click)); + _stroke_width_place.signal_button_release_event().connect(sigc::mem_fun(*this, &SelectedStyle::on_sw_click)); + _opacity_sb.signal_populate_popup().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_menu)); + _opacity_sb.signal_value_changed().connect(sigc::mem_fun(*this, &SelectedStyle::on_opacity_changed)); + // Connect to key-press to ensure focus is consistent with other spin buttons when using the keys vs mouse-click + g_signal_connect (G_OBJECT (_opacity_sb.gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), _opacity_sb.gobj()); + g_signal_connect (G_OBJECT (_opacity_sb.gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), _opacity_sb.gobj()); +} + +SelectedStyle::~SelectedStyle() +{ + selection_changed_connection->disconnect(); + delete selection_changed_connection; + selection_modified_connection->disconnect(); + delete selection_modified_connection; + subselection_changed_connection->disconnect(); + delete subselection_changed_connection; + _unit_mis.clear(); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + delete _color_preview[i]; + // FIXME: do we need this? the destroy methods are not exported + //sp_gradient_image_destroy(GTK_OBJECT(_gradient_preview_l[i])); + //sp_gradient_image_destroy(GTK_OBJECT(_gradient_preview_r[i])); + } + + delete (DropTracker*)_drop[SS_FILL]; + delete (DropTracker*)_drop[SS_STROKE]; +} + +void +SelectedStyle::setDesktop(SPDesktop *desktop) +{ + _desktop = desktop; + g_object_set_data (G_OBJECT(_opacity_sb.gobj()), "dtw", _desktop->canvas); + + Inkscape::Selection *selection = desktop->getSelection(); + + selection_changed_connection = new sigc::connection (selection->connectChanged( + sigc::bind ( + sigc::ptr_fun(&ss_selection_changed), + this ) + )); + selection_modified_connection = new sigc::connection (selection->connectModified( + sigc::bind ( + sigc::ptr_fun(&ss_selection_modified), + this ) + )); + subselection_changed_connection = new sigc::connection (desktop->connectToolSubselectionChanged( + sigc::bind ( + sigc::ptr_fun(&ss_subselection_changed), + this ) + )); + + _sw_unit = desktop->getNamedView()->display_units; + + // Set the doc default unit active in the units list + for ( auto mi:_unit_mis ) { + if (mi && mi->get_label() == _sw_unit->abbr) { + mi->set_active(); + break; + } + } +} + +void SelectedStyle::dragDataReceived( GtkWidget */*widget*/, + GdkDragContext */*drag_context*/, + gint /*x*/, gint /*y*/, + GtkSelectionData *data, + guint /*info*/, + guint /*event_time*/, + gpointer user_data ) +{ + DropTracker* tracker = (DropTracker*)user_data; + + // copied from drag-and-drop.cpp, case APP_OSWB_COLOR + bool worked = false; + Glib::ustring colorspec; + if (gtk_selection_data_get_format(data) == 8) { + ege::PaintDef color; + worked = color.fromMIMEData("application/x-oswb-color", + reinterpret_cast<char const *>(gtk_selection_data_get_data(data)), + gtk_selection_data_get_length(data), + gtk_selection_data_get_format(data)); + if (worked) { + if (color.getType() == ege::PaintDef::CLEAR) { + colorspec = ""; // TODO check if this is sufficient + } else if (color.getType() == ege::PaintDef::NONE) { + colorspec = "none"; + } else { + unsigned int r = color.getR(); + unsigned int g = color.getG(); + unsigned int b = color.getB(); + + gchar* tmp = g_strdup_printf("#%02x%02x%02x", r, g, b); + colorspec = tmp; + g_free(tmp); + } + } + } + if (worked) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, (tracker->item == SS_FILL) ? "fill":"stroke", colorspec.c_str()); + + sp_desktop_set_style(tracker->parent->_desktop, css); + sp_repr_css_attr_unref(css); + DocumentUndo::done(tracker->parent->_desktop->getDocument(), SP_VERB_NONE, _("Drop color")); + } +} + +void SelectedStyle::on_fill_remove() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill", "none"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Remove fill")); +} + +void SelectedStyle::on_stroke_remove() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", "none"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Remove stroke")); +} + +void SelectedStyle::on_fill_unset() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_unset_property (css, "fill"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Unset fill")); +} + +void SelectedStyle::on_stroke_unset() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_unset_property (css, "stroke"); + sp_repr_css_unset_property (css, "stroke-opacity"); + sp_repr_css_unset_property (css, "stroke-width"); + sp_repr_css_unset_property (css, "stroke-miterlimit"); + sp_repr_css_unset_property (css, "stroke-linejoin"); + sp_repr_css_unset_property (css, "stroke-linecap"); + sp_repr_css_unset_property (css, "stroke-dashoffset"); + sp_repr_css_unset_property (css, "stroke-dasharray"); + sp_desktop_set_style (_desktop, css, true, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Unset stroke")); +} + +void SelectedStyle::on_fill_opaque() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Make fill opaque")); +} + +void SelectedStyle::on_stroke_opaque() { + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke-opacity", "1"); + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Make fill opaque")); +} + +void SelectedStyle::on_fill_lastused() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = sp_desktop_get_color(_desktop, true); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Apply last set color to fill")); +} + +void SelectedStyle::on_stroke_lastused() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = sp_desktop_get_color(_desktop, false); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), color); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Apply last set color to stroke")); +} + +void SelectedStyle::on_fill_lastselected() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _lastselected[SS_FILL]); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Apply last selected color to fill")); +} + +void SelectedStyle::on_stroke_lastselected() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _lastselected[SS_STROKE]); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Apply last selected color to stroke")); +} + +void SelectedStyle::on_fill_invert() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = _thisselected[SS_FILL]; + gchar c[64]; + if (_mode[SS_FILL] == SS_LGRADIENT || _mode[SS_FILL] == SS_RGRADIENT) { + sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_FILL); + return; + + } + + if (_mode[SS_FILL] != SS_COLOR) return; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(color)), + (255 - SP_RGBA32_G_U(color)), + (255 - SP_RGBA32_B_U(color)), + SP_RGBA32_A_U(color) + ) + ); + sp_repr_css_set_property (css, "fill", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Invert fill")); +} + +void SelectedStyle::on_stroke_invert() { + SPCSSAttr *css = sp_repr_css_attr_new (); + guint32 color = _thisselected[SS_STROKE]; + gchar c[64]; + if (_mode[SS_STROKE] == SS_LGRADIENT || _mode[SS_STROKE] == SS_RGRADIENT) { + sp_gradient_invert_selected_gradients(_desktop, Inkscape::FOR_STROKE); + return; + } + if (_mode[SS_STROKE] != SS_COLOR) return; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (255 - SP_RGBA32_R_U(color)), + (255 - SP_RGBA32_G_U(color)), + (255 - SP_RGBA32_B_U(color)), + SP_RGBA32_A_U(color) + ) + ); + sp_repr_css_set_property (css, "stroke", c); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Invert stroke")); +} + +void SelectedStyle::on_fill_white() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0xffffffff); + sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill-opacity", "1"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("White fill")); +} + +void SelectedStyle::on_stroke_white() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0xffffffff); + sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke-opacity", "1"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("White stroke")); +} + +void SelectedStyle::on_fill_black() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0x000000ff); + sp_repr_css_set_property (css, "fill", c); + sp_repr_css_set_property (css, "fill-opacity", "1.0"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Black fill")); +} + +void SelectedStyle::on_stroke_black() { + SPCSSAttr *css = sp_repr_css_attr_new (); + gchar c[64]; + sp_svg_write_color (c, sizeof(c), 0x000000ff); + sp_repr_css_set_property (css, "stroke", c); + sp_repr_css_set_property (css, "stroke-opacity", "1.0"); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Black stroke")); +} + +void SelectedStyle::on_fill_copy() { + if (_mode[SS_FILL] == SS_COLOR) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _thisselected[SS_FILL]); + Glib::ustring text; + text += c; + if (!text.empty()) { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + refClipboard->set_text(text); + } + } +} + +void SelectedStyle::on_stroke_copy() { + if (_mode[SS_STROKE] == SS_COLOR) { + gchar c[64]; + sp_svg_write_color (c, sizeof(c), _thisselected[SS_STROKE]); + Glib::ustring text; + text += c; + if (!text.empty()) { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + refClipboard->set_text(text); + } + } +} + +void SelectedStyle::on_fill_paste() { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const text = refClipboard->wait_for_text(); + + if (!text.empty()) { + guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity + if (color == 0x000000ff) // failed to parse color string + return; + + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "fill", text.c_str()); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Paste fill")); + } +} + +void SelectedStyle::on_stroke_paste() { + Glib::RefPtr<Gtk::Clipboard> refClipboard = Gtk::Clipboard::get(); + Glib::ustring const text = refClipboard->wait_for_text(); + + if (!text.empty()) { + guint32 color = sp_svg_read_color(text.c_str(), 0x000000ff); // impossible value, as SVG color cannot have opacity + if (color == 0x000000ff) // failed to parse color string + return; + + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", text.c_str()); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Paste stroke")); + } +} + +void SelectedStyle::on_fillstroke_swap() { + _desktop->getSelection()->swapFillStroke(); +} + +void SelectedStyle::on_fill_edit() { + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageFill(); +} + +void SelectedStyle::on_stroke_edit() { + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokePaint(); +} + +bool +SelectedStyle::on_fill_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageFill(); + + } else if (event->button == 3) { // right-click, popup menu + _popup[SS_FILL].popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastcolor + if (_mode[SS_FILL] == SS_NONE) { + on_fill_lastused(); + } else { + on_fill_remove(); + } + } + return true; +} + +bool +SelectedStyle::on_stroke_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokePaint(); + } else if (event->button == 3) { // right-click, popup menu + _popup[SS_STROKE].popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastcolor + if (_mode[SS_STROKE] == SS_NONE) { + on_stroke_lastused(); + } else { + on_stroke_remove(); + } + } + return true; +} + +bool +SelectedStyle::on_sw_click(GdkEventButton *event) +{ + if (event->button == 1) { // click, open fill&stroke + if (Dialog::FillAndStroke *fs = get_fill_and_stroke_panel(_desktop)) + fs->showPageStrokeStyle(); + } else if (event->button == 3) { // right-click, popup menu + _popup_sw.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } else if (event->button == 2) { // middle click, toggle none/lastwidth? + // + } + return true; +} + +bool +SelectedStyle::on_opacity_click(GdkEventButton *event) +{ + if (event->button == 2) { // middle click + const char* opacity = _opacity_sb.get_value() < 50? "0.5" : (_opacity_sb.get_value() == 100? "0" : "1"); + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "opacity", opacity); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, + _("Change opacity")); + return true; + } + + return false; +} + +void SelectedStyle::on_popup_units(Inkscape::Util::Unit const *unit) { + _sw_unit = unit; + update(); +} + +void SelectedStyle::on_popup_preset(int i) { + SPCSSAttr *css = sp_repr_css_attr_new (); + gdouble w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(_sw_presets[i], _sw_unit, "px"); + } else { + w = _sw_presets[i]; + } + Inkscape::CSSOStringStream os; + os << w; + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + // FIXME: update dash patterns! + sp_desktop_set_style (_desktop, css, true); + sp_repr_css_attr_unref (css); + DocumentUndo::done(_desktop->getDocument(), SP_VERB_DIALOG_SWATCHES, + _("Change stroke width")); +} + +void +SelectedStyle::update() +{ + if (_desktop == nullptr) + return; + + // create temporary style + SPStyle query(_desktop->getDocument()); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + Gtk::EventBox *place = (i == SS_FILL)? &_fill_place : &_stroke_place; + Gtk::EventBox *flag_place = (i == SS_FILL)? &_fill_flag_place : &_stroke_flag_place; + + place->remove(); + flag_place->remove(); + + clearTooltip(*place); + clearTooltip(*flag_place); + + _mode[i] = SS_NA; + _paintserver_id[i].clear(); + + _popup_copy[i].set_sensitive(false); + + // query style from desktop. This returns a result flag and fills query with the style of subselection, if any, or selection + int result = sp_desktop_query_style (_desktop, &query, + (i == SS_FILL)? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE); + switch (result) { + case QUERY_STYLE_NOTHING: + place->add(_na[i]); + place->set_tooltip_text(__na[i]); + _mode[i] = SS_NA; + if ( _dropEnabled[i] ) { + gtk_drag_dest_unset( GTK_WIDGET((i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()) ); + _dropEnabled[i] = false; + } + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + if ( !_dropEnabled[i] ) { + gtk_drag_dest_set( GTK_WIDGET( (i==SS_FILL) ? _fill_place.gobj():_stroke_place.gobj()), + GTK_DEST_DEFAULT_ALL, + ui_drop_target_entries, + nui_drop_target_entries, + GdkDragAction(GDK_ACTION_COPY | GDK_ACTION_MOVE) ); + _dropEnabled[i] = true; + } + SPIPaint *paint; + if (i == SS_FILL) { + paint = &(query.fill); + } else { + paint = &(query.stroke); + } + if (paint->set && paint->isPaintserver()) { + SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (&query) : SP_STYLE_STROKE_SERVER (&query); + if ( server ) { + Inkscape::XML::Node *srepr = server->getRepr(); + _paintserver_id[i] += "url(#"; + _paintserver_id[i] += srepr->attribute("id"); + _paintserver_id[i] += ")"; + + if (SP_IS_LINEARGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_l[i]), vector); + place->add(_gradient_box_l[i]); + place->set_tooltip_text(__lgradient[i]); + _mode[i] = SS_LGRADIENT; + } else if (SP_IS_RADIALGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_r[i]), vector); + place->add(_gradient_box_r[i]); + place->set_tooltip_text(__rgradient[i]); + _mode[i] = SS_RGRADIENT; +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + SPGradient *array = SP_GRADIENT(server)->getArray(); + sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(_gradient_preview_m[i]), array); + place->add(_gradient_box_m[i]); + place->set_tooltip_text(__mgradient[i]); + _mode[i] = SS_MGRADIENT; +#endif + } else if (SP_IS_PATTERN(server)) { + place->add(_pattern[i]); + place->set_tooltip_text(__pattern[i]); + _mode[i] = SS_PATTERN; + } else if (SP_IS_HATCH(server)) { + place->add(_hatch[i]); + place->set_tooltip_text(__hatch[i]); + _mode[i] = SS_HATCH; + } + } else { + g_warning ("file %s: line %d: Unknown paint server", __FILE__, __LINE__); + } + } else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32( + SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query.fill_opacity.value : query.stroke_opacity.value)); + _lastselected[i] = _thisselected[i]; + _thisselected[i] = color; // include opacity + ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color); + _color_preview[i]->show_all(); + place->add(*_color_preview[i]); + gchar c_string[64]; + g_snprintf (c_string, 64, "%06x/%.3g", color >> 8, SP_RGBA32_A_F(color)); + place->set_tooltip_text(__color[i] + ": " + c_string + _(", drag to adjust, middle-click to remove")); + _mode[i] = SS_COLOR; + _popup_copy[i].set_sensitive(true); + + } else if (paint->set && paint->isNone()) { + place->add(_none[i]); + place->set_tooltip_text(__none[i]); + _mode[i] = SS_NONE; + } else if (!paint->set) { + place->add(_unset[i]); + place->set_tooltip_text(__unset[i]); + _mode[i] = SS_UNSET; + } + if (result == QUERY_STYLE_MULTIPLE_AVERAGED) { + flag_place->add(_averaged[i]); + flag_place->set_tooltip_text(__averaged[i]); + } else if (result == QUERY_STYLE_MULTIPLE_SAME) { + flag_place->add(_multiple[i]); + flag_place->set_tooltip_text(__multiple[i]); + } + break; + case QUERY_STYLE_MULTIPLE_DIFFERENT: + place->add(_many[i]); + place->set_tooltip_text(__many[i]); + _mode[i] = SS_MANY; + break; + default: + break; + } + } + +// Now query opacity + clearTooltip(_opacity_place); + clearTooltip(_opacity_sb); + + int result = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_MASTEROPACITY); + + switch (result) { + case QUERY_STYLE_NOTHING: + _opacity_place.set_tooltip_text(_("Nothing selected")); + _opacity_sb.set_tooltip_text(_("Nothing selected")); + _opacity_sb.set_sensitive(false); + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + _opacity_place.set_tooltip_text(_("Opacity (%)")); + _opacity_sb.set_tooltip_text(_("Opacity (%)")); + if (_opacity_blocked) break; + _opacity_blocked = true; + _opacity_sb.set_sensitive(true); + _opacity_adjustment->set_value(SP_SCALE24_TO_FLOAT(query.opacity.value) * 100); + _opacity_blocked = false; + break; + } + +// Now query stroke_width + int result_sw = sp_desktop_query_style (_desktop, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH); + switch (result_sw) { + case QUERY_STYLE_NOTHING: + _stroke_width.set_markup(""); + current_stroke_width = 0; + break; + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: + case QUERY_STYLE_MULTIPLE_SAME: + { + double w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", _sw_unit); + } else { + w = query.stroke_width.computed; + } + current_stroke_width = w; + + { + gchar *str = g_strdup_printf(" %#.3g", w); + if (str[strlen(str) - 1] == ',' || str[strlen(str) - 1] == '.') { + str[strlen(str)-1] = '\0'; + } + _stroke_width.set_markup(str); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Stroke width: %.5g%s%s"), + w, + _sw_unit? _sw_unit->abbr.c_str() : "px", + (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED)? + _(" (averaged)") : ""); + _stroke_width_place.set_tooltip_text(str); + g_free (str); + } + break; + } + default: + break; + } +} + +void SelectedStyle::opacity_0() {_opacity_sb.set_value(0);} +void SelectedStyle::opacity_025() {_opacity_sb.set_value(25);} +void SelectedStyle::opacity_05() {_opacity_sb.set_value(50);} +void SelectedStyle::opacity_075() {_opacity_sb.set_value(75);} +void SelectedStyle::opacity_1() {_opacity_sb.set_value(100);} + +void SelectedStyle::on_opacity_menu (Gtk::Menu *menu) { + + Glib::ListHandle<Gtk::Widget *> children = menu->get_children(); + for (auto iter : children) { + menu->remove(*iter); + } + + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label(_("0 (transparent)"), Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_0 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("25%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_025 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("50%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_05 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label("75%", Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_075 )); + menu->add(*item); + } + { + Gtk::MenuItem *item = new Gtk::MenuItem; + item->add(*(new Gtk::Label(_("100% (opaque)"), Gtk::ALIGN_START, Gtk::ALIGN_START))); + item->signal_activate().connect(sigc::mem_fun(*this, &SelectedStyle::opacity_1 )); + menu->add(*item); + } + + menu->show_all(); +} + +void SelectedStyle::on_opacity_changed () +{ + g_return_if_fail(_desktop); // TODO this shouldn't happen! + if (_opacity_blocked) + return; + _opacity_blocked = true; + SPCSSAttr *css = sp_repr_css_attr_new (); + Inkscape::CSSOStringStream os; + os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0); + sp_repr_css_set_property (css, "opacity", os.str().c_str()); + // FIXME: workaround for GTK breakage: display interruptibility sometimes results in GTK + // sending multiple value-changed events. As if when Inkscape interrupts redraw for main loop + // iterations, GTK discovers that this callback hasn't finished yet, and for some weird reason + // decides to add yet another value-changed event to the queue. Totally braindead if you ask + // me. As a result, scrolling the spinbutton once results in runaway change until it hits 1.0 + // or 0.0. (And no, this is not a race with ::update, I checked that.) + // Sigh. So we disable interruptibility while we're setting the new value. + _desktop->getCanvas()->forceFullRedrawAfterInterruptions(0); + sp_desktop_set_style (_desktop, css); + sp_repr_css_attr_unref (css); + DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", SP_VERB_DIALOG_FILL_STROKE, + _("Change opacity")); + // resume interruptibility + _desktop->getCanvas()->endForcedFullRedraws(); + // spinbutton_defocus(GTK_WIDGET(_opacity_sb.gobj())); + _opacity_blocked = false; +} + +/* ============================================= RotateableSwatch */ + +RotateableSwatch::RotateableSwatch(SelectedStyle *parent, guint mode) : + fillstroke(mode), + parent(parent), + startcolor(0), + startcolor_set(false), + undokey("ssrot1"), + cr(nullptr), + cr_set(false) + +{ +} + +RotateableSwatch::~RotateableSwatch() = default; + +double +RotateableSwatch::color_adjust(float *hsla, double by, guint32 cc, guint modifier) +{ + SPColor::rgb_to_hsl_floatv (hsla, SP_RGBA32_R_F(cc), SP_RGBA32_G_F(cc), SP_RGBA32_B_F(cc)); + hsla[3] = SP_RGBA32_A_F(cc); + double diff = 0; + if (modifier == 2) { // saturation + double old = hsla[1]; + if (by > 0) { + hsla[1] += by * (1 - hsla[1]); + } else { + hsla[1] += by * (hsla[1]); + } + diff = hsla[1] - old; + } else if (modifier == 1) { // lightness + double old = hsla[2]; + if (by > 0) { + hsla[2] += by * (1 - hsla[2]); + } else { + hsla[2] += by * (hsla[2]); + } + diff = hsla[2] - old; + } else if (modifier == 3) { // alpha + double old = hsla[3]; + hsla[3] += by/2; + if (hsla[3] < 0) { + hsla[3] = 0; + } else if (hsla[3] > 1) { + hsla[3] = 1; + } + diff = hsla[3] - old; + } else { // hue + double old = hsla[0]; + hsla[0] += by/2; + while (hsla[0] < 0) + hsla[0] += 1; + while (hsla[0] > 1) + hsla[0] -= 1; + diff = hsla[0] - old; + } + + float rgb[3]; + SPColor::hsl_to_rgb_floatv (rgb, hsla[0], hsla[1], hsla[2]); + + gchar c[64]; + sp_svg_write_color (c, sizeof(c), + SP_RGBA32_U_COMPOSE( + (SP_COLOR_F_TO_U(rgb[0])), + (SP_COLOR_F_TO_U(rgb[1])), + (SP_COLOR_F_TO_U(rgb[2])), + 0xff + ) + ); + + SPCSSAttr *css = sp_repr_css_attr_new (); + + if (modifier == 3) { // alpha + Inkscape::CSSOStringStream osalpha; + osalpha << hsla[3]; + sp_repr_css_set_property(css, (fillstroke == SS_FILL) ? "fill-opacity" : "stroke-opacity", osalpha.str().c_str()); + } else { + sp_repr_css_set_property (css, (fillstroke == SS_FILL) ? "fill" : "stroke", c); + } + sp_desktop_set_style (parent->getDesktop(), css); + sp_repr_css_attr_unref (css); + return diff; +} + +void +RotateableSwatch::do_motion(double by, guint modifier) { + if (parent->_mode[fillstroke] != SS_COLOR) + return; + + if (!scrolling && !cr_set) { + GtkWidget *w = GTK_WIDGET(gobj()); + GdkPixbuf *pixbuf = nullptr; + + if (modifier == 2) { // saturation + pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_s_xpm); + } else if (modifier == 1) { // lightness + pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_l_xpm); + } else if (modifier == 3) { // alpha + pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_a_xpm); + } else { // hue + pixbuf = gdk_pixbuf_new_from_xpm_data((const gchar **)cursor_adj_h_xpm); + } + + if (pixbuf != nullptr) { + cr = gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, 16, 16); + g_object_unref(pixbuf); + gdk_window_set_cursor(gtk_widget_get_window(w), cr); + g_object_unref(cr); + cr = nullptr; + cr_set = true; + } + } + + guint32 cc; + if (!startcolor_set) { + cc = startcolor = parent->_thisselected[fillstroke]; + startcolor_set = true; + } else { + cc = startcolor; + } + + float hsla[4]; + double diff = 0; + + diff = color_adjust(hsla, by, cc, modifier); + + if (modifier == 3) { // alpha + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust alpha"))); + double ch = hsla[3]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>alpha</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Shift</b> to adjust saturation, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else if (modifier == 2) { // saturation + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust saturation"))); + double ch = hsla[1]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>saturation</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Ctrl</b> to adjust lightness, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else if (modifier == 1) { // lightness + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust lightness"))); + double ch = hsla[2]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>lightness</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, without modifiers to adjust hue"), ch - diff, ch, diff); + + } else { // hue + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust hue"))); + double ch = hsla[0]; + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>hue</b>: was %.3g, now <b>%.3g</b> (diff %.3g); with <b>Shift</b> to adjust saturation, with <b>Alt</b> to adjust alpha, with <b>Ctrl</b> to adjust lightness"), ch - diff, ch, diff); + } +} + + +void +RotateableSwatch::do_scroll(double by, guint modifier) { + do_motion(by/30.0, modifier); + do_release(by/30.0, modifier); +} + +void +RotateableSwatch::do_release(double by, guint modifier) { + if (parent->_mode[fillstroke] != SS_COLOR) + return; + + float hsla[4]; + color_adjust(hsla, by, startcolor, modifier); + + if (cr_set) { + GtkWidget *w = GTK_WIDGET(gobj()); + gdk_window_set_cursor(gtk_widget_get_window(w), nullptr); + if (cr) { + g_object_unref(cr); + cr = nullptr; + } + cr_set = false; + } + + if (modifier == 3) { // alpha + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, ("Adjust alpha")); + } else if (modifier == 2) { // saturation + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, ("Adjust saturation")); + + } else if (modifier == 1) { // lightness + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, ("Adjust lightness")); + + } else { // hue + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, ("Adjust hue")); + } + + if (!strcmp(undokey, "ssrot1")) { + undokey = "ssrot2"; + } else { + undokey = "ssrot1"; + } + + parent->getDesktop()->event_context->message_context->clear(); + startcolor_set = false; +} + +/* ============================================= RotateableStrokeWidth */ + +RotateableStrokeWidth::RotateableStrokeWidth(SelectedStyle *parent) : + parent(parent), + startvalue(0), + startvalue_set(false), + undokey("swrot1") +{ +} + +RotateableStrokeWidth::~RotateableStrokeWidth() = default; + +double +RotateableStrokeWidth::value_adjust(double current, double by, guint /*modifier*/, bool final) +{ + double newval; + // by is -1..1 + double max_f = 50; // maximum width is (current * max_f), minimum - zero + newval = current * (std::exp(std::log(max_f-1) * (by+1)) - 1) / (max_f-2); + + SPCSSAttr *css = sp_repr_css_attr_new (); + if (final && newval < 1e-6) { + // if dragged into zero and this is the final adjust on mouse release, delete stroke; + // if it's not final, leave it a chance to increase again (which is not possible with "none") + sp_repr_css_set_property (css, "stroke", "none"); + } else { + newval = Inkscape::Util::Quantity::convert(newval, parent->_sw_unit, "px"); + Inkscape::CSSOStringStream os; + os << newval; + sp_repr_css_set_property (css, "stroke-width", os.str().c_str()); + } + + sp_desktop_set_style (parent->getDesktop(), css); + sp_repr_css_attr_unref (css); + return newval - current; +} + +void +RotateableStrokeWidth::do_motion(double by, guint modifier) { + + // if this is the first motion after a mouse grab, remember the current width + if (!startvalue_set) { + startvalue = parent->current_stroke_width; + // if it's 0, adjusting (which uses multiplication) will not be able to change it, so we + // cheat and provide a non-zero value + if (startvalue == 0) + startvalue = 1; + startvalue_set = true; + } + + if (modifier == 3) { // Alt, do nothing + } else { + double diff = value_adjust(startvalue, by, modifier, false); + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust stroke width"))); + parent->getDesktop()->event_context->message_context->setF(Inkscape::IMMEDIATE_MESSAGE, _("Adjusting <b>stroke width</b>: was %.3g, now <b>%.3g</b> (diff %.3g)"), startvalue, startvalue + diff, diff); + } +} + +void +RotateableStrokeWidth::do_release(double by, guint modifier) { + + if (modifier == 3) { // do nothing + + } else { + value_adjust(startvalue, by, modifier, true); + startvalue_set = false; + DocumentUndo::maybeDone(parent->getDesktop()->getDocument(), undokey, + SP_VERB_DIALOG_FILL_STROKE, (_("Adjust stroke width"))); + } + + if (!strcmp(undokey, "swrot1")) { + undokey = "swrot2"; + } else { + undokey = "swrot1"; + } + parent->getDesktop()->event_context->message_context->clear(); +} + +void +RotateableStrokeWidth::do_scroll(double by, guint modifier) { + do_motion(by/10.0, modifier); + startvalue_set = false; +} + +Dialog::FillAndStroke *get_fill_and_stroke_panel(SPDesktop *desktop) +{ + if (Dialog::PanelDialogBase *panel_dialog = + dynamic_cast<Dialog::PanelDialogBase *>(desktop->_dlg_mgr->getDialog("FillAndStroke"))) { + try { + Dialog::FillAndStroke &fill_and_stroke = + dynamic_cast<Dialog::FillAndStroke &>(panel_dialog->getPanel()); + return &fill_and_stroke; + } catch (std::exception &e) { } + } + + return nullptr; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/selected-style.h b/src/ui/widget/selected-style.h new file mode 100644 index 0000000..388f802 --- /dev/null +++ b/src/ui/widget/selected-style.h @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * buliabyak@gmail.com + * scislac@users.sf.net + * + * Copyright (C) 2005 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_CURRENT_STYLE_H +#define INKSCAPE_UI_CURRENT_STYLE_H + +#include <gtkmm/box.h> +#include <gtkmm/grid.h> + +#include <gtkmm/label.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/enums.h> +#include <gtkmm/menu.h> +#include <gtkmm/menuitem.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/radiobuttongroup.h> +#include <gtkmm/radiomenuitem.h> +#include "ui/widget/spinbutton.h" + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "rotateable.h" + +class SPDesktop; + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace UI { +namespace Widget { + +enum { + SS_NA, + SS_NONE, + SS_UNSET, + SS_PATTERN, + SS_LGRADIENT, + SS_RGRADIENT, +#ifdef WITH_MESH + SS_MGRADIENT, +#endif + SS_MANY, + SS_COLOR, + SS_HATCH +}; + +enum { + SS_FILL, + SS_STROKE +}; + +class SelectedStyle; + +class RotateableSwatch : public Rotateable { + public: + RotateableSwatch(SelectedStyle *parent, guint mode); + ~RotateableSwatch() override; + + double color_adjust (float *hsl, double by, guint32 cc, guint state); + + void do_motion (double by, guint state) override; + void do_release (double by, guint state) override; + void do_scroll (double by, guint state) override; + +private: + guint fillstroke; + + SelectedStyle *parent; + + guint32 startcolor; + bool startcolor_set; + + gchar const *undokey; + + GdkCursor *cr; + bool cr_set; +}; + +class RotateableStrokeWidth : public Rotateable { + public: + RotateableStrokeWidth(SelectedStyle *parent); + ~RotateableStrokeWidth() override; + + double value_adjust(double current, double by, guint modifier, bool final); + void do_motion (double by, guint state) override; + void do_release (double by, guint state) override; + void do_scroll (double by, guint state) override; + +private: + SelectedStyle *parent; + + double startvalue; + bool startvalue_set; + + gchar const *undokey; +}; + +/** + * Selected style indicator (fill, stroke, opacity). + */ +class SelectedStyle : public Gtk::HBox +{ +public: + SelectedStyle(bool layout = true); + + ~SelectedStyle() override; + + void setDesktop(SPDesktop *desktop); + SPDesktop *getDesktop() {return _desktop;} + void update(); + + guint32 _lastselected[2]; + guint32 _thisselected[2]; + + guint _mode[2]; + + double current_stroke_width; + Inkscape::Util::Unit const *_sw_unit; // points to object in UnitTable, do not delete + +protected: + SPDesktop *_desktop; + + Gtk::Grid _table; + + Gtk::Label _fill_label; + Gtk::Label _stroke_label; + Gtk::Label _opacity_label; + + RotateableSwatch _fill_place; + RotateableSwatch _stroke_place; + + Gtk::EventBox _fill_flag_place; + Gtk::EventBox _stroke_flag_place; + + Gtk::EventBox _opacity_place; + Glib::RefPtr<Gtk::Adjustment> _opacity_adjustment; + Inkscape::UI::Widget::SpinButton _opacity_sb; + + Gtk::Label _na[2]; + Glib::ustring __na[2]; + + Gtk::Label _none[2]; + Glib::ustring __none[2]; + + Gtk::Label _pattern[2]; + Glib::ustring __pattern[2]; + + Gtk::Label _hatch[2]; + Glib::ustring __hatch[2]; + + Gtk::Label _lgradient[2]; + Glib::ustring __lgradient[2]; + + GtkWidget *_gradient_preview_l[2]; + Gtk::HBox _gradient_box_l[2]; + + Gtk::Label _rgradient[2]; + Glib::ustring __rgradient[2]; + + GtkWidget *_gradient_preview_r[2]; + Gtk::HBox _gradient_box_r[2]; + +#ifdef WITH_MESH + Gtk::Label _mgradient[2]; + Glib::ustring __mgradient[2]; + + GtkWidget *_gradient_preview_m[2]; + Gtk::HBox _gradient_box_m[2]; +#endif + + Gtk::Label _many[2]; + Glib::ustring __many[2]; + + Gtk::Label _unset[2]; + Glib::ustring __unset[2]; + + Gtk::Widget *_color_preview[2]; + Glib::ustring __color[2]; + + Gtk::Label _averaged[2]; + Glib::ustring __averaged[2]; + Gtk::Label _multiple[2]; + Glib::ustring __multiple[2]; + + Gtk::HBox _fill; + Gtk::HBox _stroke; + RotateableStrokeWidth _stroke_width_place; + Gtk::Label _stroke_width; + Gtk::Label _fill_empty_space; + + Glib::ustring _paintserver_id[2]; + + sigc::connection *selection_changed_connection; + sigc::connection *selection_modified_connection; + sigc::connection *subselection_changed_connection; + + static void dragDataReceived( GtkWidget *widget, + GdkDragContext *drag_context, + gint x, gint y, + GtkSelectionData *data, + guint info, + guint event_time, + gpointer user_data ); + + bool on_fill_click(GdkEventButton *event); + bool on_stroke_click(GdkEventButton *event); + bool on_opacity_click(GdkEventButton *event); + bool on_sw_click(GdkEventButton *event); + + bool _opacity_blocked; + void on_opacity_changed(); + void on_opacity_menu(Gtk::Menu *menu); + void opacity_0(); + void opacity_025(); + void opacity_05(); + void opacity_075(); + void opacity_1(); + + void on_fill_remove(); + void on_stroke_remove(); + void on_fill_lastused(); + void on_stroke_lastused(); + void on_fill_lastselected(); + void on_stroke_lastselected(); + void on_fill_unset(); + void on_stroke_unset(); + void on_fill_edit(); + void on_stroke_edit(); + void on_fillstroke_swap(); + void on_fill_invert(); + void on_stroke_invert(); + void on_fill_white(); + void on_stroke_white(); + void on_fill_black(); + void on_stroke_black(); + void on_fill_copy(); + void on_stroke_copy(); + void on_fill_paste(); + void on_stroke_paste(); + void on_fill_opaque(); + void on_stroke_opaque(); + + Gtk::Menu _popup[2]; + Gtk::MenuItem _popup_edit[2]; + Gtk::MenuItem _popup_lastused[2]; + Gtk::MenuItem _popup_lastselected[2]; + Gtk::MenuItem _popup_invert[2]; + Gtk::MenuItem _popup_white[2]; + Gtk::MenuItem _popup_black[2]; + Gtk::MenuItem _popup_copy[2]; + Gtk::MenuItem _popup_paste[2]; + Gtk::MenuItem _popup_swap[2]; + Gtk::MenuItem _popup_opaque[2]; + Gtk::MenuItem _popup_unset[2]; + Gtk::MenuItem _popup_remove[2]; + + Gtk::Menu _popup_sw; + Gtk::RadioButtonGroup _sw_group; + std::vector<Gtk::RadioMenuItem*> _unit_mis; + void on_popup_units(Inkscape::Util::Unit const *u); + void on_popup_preset(int i); + Gtk::MenuItem _popup_sw_remove; + + void *_drop[2]; + bool _dropEnabled[2]; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_BUTTON_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-button-tool-item.cpp b/src/ui/widget/spin-button-tool-item.cpp new file mode 100644 index 0000000..b283939 --- /dev/null +++ b/src/ui/widget/spin-button-tool-item.cpp @@ -0,0 +1,532 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "spin-button-tool-item.h" + +#include <gtkmm/box.h> +#include <gtkmm/image.h> +#include <gtkmm/radiomenuitem.h> +#include <gtkmm/toolbar.h> + +#include <utility> + +#include "spinbutton.h" +#include "ui/icon-loader.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * \brief Handler for the button's "focus-in-event" signal + * + * \param focus_event The event that triggered the signal + * + * \detail This just logs the current value of the spin-button + * and sets the _transfer_focus flag + */ +bool +SpinButtonToolItem::on_btn_focus_in_event(GdkEventFocus * /* focus_event */) +{ + _last_val = _btn->get_value(); + _transfer_focus = true; + + return false; // Event not consumed +} + +/** + * \brief Handler for the button's "focus-out-event" signal + * + * \param focus_event The event that triggered the signal + * + * \detail This just unsets the _transfer_focus flag + */ +bool +SpinButtonToolItem::on_btn_focus_out_event(GdkEventFocus * /* focus_event */) +{ + _transfer_focus = false; + + return false; // Event not consumed +} + +/** + * \brief Handler for the button's "key-press-event" signal + * + * \param key_event The event that triggered the signal + * + * \detail If the ESC key was pressed, restore the last value and defocus. + * If the Enter key was pressed, just defocus. + */ +bool +SpinButtonToolItem::on_btn_key_press_event(GdkEventKey *key_event) +{ + bool was_consumed = false; // Whether event has been consumed or not + auto display = Gdk::Display::get_default(); + auto keymap = display->get_keymap(); + guint key = 0; + gdk_keymap_translate_keyboard_state(keymap, key_event->hardware_keycode, + static_cast<GdkModifierType>(key_event->state), + 0, &key, 0, 0, 0); + + auto val = _btn->get_value(); + + switch(key) { + case GDK_KEY_Escape: + { + _transfer_focus = true; + _btn->set_value(_last_val); + defocus(); + was_consumed = true; + } + break; + + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + { + _transfer_focus = true; + defocus(); + was_consumed = true; + } + break; + + case GDK_KEY_Tab: + { + _transfer_focus = false; + was_consumed = process_tab(1); + } + break; + + case GDK_KEY_ISO_Left_Tab: + { + _transfer_focus = false; + was_consumed = process_tab(-1); + } + break; + + // TODO: Enable variable step-size if this is ever used + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + { + _transfer_focus = false; + _btn->set_value(val+1); + was_consumed=true; + } + break; + + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + { + _transfer_focus = false; + _btn->set_value(val-1); + was_consumed=true; + } + break; + + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + { + _transfer_focus = false; + _btn->set_value(val+10); + was_consumed=true; + } + break; + + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + { + _transfer_focus = false; + _btn->set_value(val-10); + was_consumed=true; + } + break; + + case GDK_KEY_z: + case GDK_KEY_Z: + { + _transfer_focus = false; + _btn->set_value(_last_val); + was_consumed = true; + } + break; + } + + return was_consumed; +} + +/** + * \brief Shift focus to a different widget + * + * \details This only has an effect if the _transfer_focus flag and the _focus_widget are set + */ +void +SpinButtonToolItem::defocus() +{ + if(_transfer_focus && _focus_widget) { + _focus_widget->grab_focus(); + } +} + +/** + * \brief Move focus to another spinbutton in the toolbar + * + * \param increment[in] The number of places to shift within the toolbar + */ +bool +SpinButtonToolItem::process_tab(int increment) +{ + // If the increment is zero, do nothing + if(increment == 0) return true; + + // Here, we're working through the widget hierarchy: + // Toolbar + // |- ToolItem (*this) + // |-> Box + // |-> SpinButton (*_btn) + // + // Our aim is to find the next/previous spin-button within a toolitem in our toolbar + + bool handled = false; + + // We only bother doing this if the current item is actually in a toolbar! + auto toolbar = dynamic_cast<Gtk::Toolbar *>(get_parent()); + + if (toolbar) { + // Get the index of the current item within the toolbar and the total number of items + auto my_index = toolbar->get_item_index(*this); + auto n_items = toolbar->get_n_items(); + + auto test_index = my_index + increment; // The index of the item we want to check + + // Loop through tool items as long as we're within the bounds of the toolbar and + // we haven't yet found our new item to focus on + while(test_index > 0 && test_index <= n_items && !handled) { + + auto tool_item = toolbar->get_nth_item(test_index); + + if(tool_item) { + // There are now two options that we support: + if(dynamic_cast<SpinButtonToolItem *>(tool_item)) { + // (1) The tool item is a SpinButtonToolItem, in which case, we just pass + // focus to its spin-button + dynamic_cast<SpinButtonToolItem *>(tool_item)->grab_button_focus(); + handled = true; + } + else if(dynamic_cast<Gtk::SpinButton *>(tool_item->get_child())) { + // (2) The tool item contains a plain Gtk::SpinButton, in which case we + // pass focus directly to it + tool_item->get_child()->grab_focus(); + } + } + + test_index += increment; + } + } + + return handled; +} + +/** + * \brief Handler for toggle events on numeric menu items + * + * \details Sets the adjustment to the desired value + */ +void +SpinButtonToolItem::on_numeric_menu_item_toggled(double value) +{ + auto adj = _btn->get_adjustment(); + adj->set_value(value); +} + +Gtk::RadioMenuItem * +SpinButtonToolItem::create_numeric_menu_item(Gtk::RadioButtonGroup *group, + double value, + const Glib::ustring& label) +{ + // Represent the value as a string + std::ostringstream ss; + ss << value; + + // Append the label if specified + if (!label.empty()) { + ss << ": " << label; + } + + auto numeric_option = Gtk::manage(new Gtk::RadioMenuItem(*group, ss.str())); + + // Set the adjustment value in response to changes in the selected item + auto toggled_handler = sigc::bind(sigc::mem_fun(*this, &SpinButtonToolItem::on_numeric_menu_item_toggled), value); + numeric_option->signal_toggled().connect(toggled_handler); + + return numeric_option; +} + +/** + * \brief Create a menu containing fixed numeric options for the adjustment + * + * \details Each of these values represents a snap-point for the adjustment's value + */ +Gtk::Menu * +SpinButtonToolItem::create_numeric_menu() +{ + auto numeric_menu = Gtk::manage(new Gtk::Menu()); + + Gtk::RadioMenuItem::Group group; + + // Get values for the adjustment + auto adj = _btn->get_adjustment(); + auto adj_value = adj->get_value(); + auto lower = adj->get_lower(); + auto upper = adj->get_upper(); + auto step = adj->get_step_increment(); + auto page = adj->get_page_increment(); + + auto digits = _btn->get_digits(); + + // A number a little smaller than the smallest increment that can be + // displayed in the spinbutton entry. + // + // For example, if digits = 0, we are displaying integers only and + // epsilon = 0.9 * 10^-0 = 0.9 + // + // For digits = 1, we get epsilon = 0.9 * 10^-1 = 0.09 + // For digits = 2, we get epsilon = 0.9 * 10^-2 = 0.009 etc... + auto epsilon = 0.9 * pow(10.0, -float(digits)); + + // Start by setting some fixed values based on the adjustment's + // parameters. + NumericMenuData values; + values.push_back(std::make_pair(upper, "")); + values.push_back(std::make_pair(adj_value + page, "")); + values.push_back(std::make_pair(adj_value + step, "")); + values.push_back(std::make_pair(adj_value, "")); + values.push_back(std::make_pair(adj_value - step, "")); + values.push_back(std::make_pair(adj_value - page, "")); + values.push_back(std::make_pair(lower, "")); + + // Now add any custom items + for (auto custom_data : _custom_menu_data) { + values.push_back(custom_data); + } + + // Sort the numeric menu items into reverse numerical order (largest at top of menu) + std::sort (begin(values), end(values)); + std::reverse(begin(values), end(values)); + + for (auto value : values) + { + auto numeric_menu_item = create_numeric_menu_item(&group, value.first, value.second); + numeric_menu->append(*numeric_menu_item); + + if (fabs(adj_value - value.first) < epsilon) { + // If the adjustment value is very close to the value of this menu item, + // make this menu item active + numeric_menu_item->set_active(); + } + } + + return numeric_menu; +} + +/** + * \brief Create a menu-item in response to the "create-menu-proxy" signal + * + * \detail This is an override for the default Gtk::ToolItem handler so + * we don't need to explicitly connect this to the signal. It + * runs if the toolitem is unable to fit on the toolbar, and + * must be represented by a menu item instead. + */ +bool +SpinButtonToolItem::on_create_menu_proxy() +{ + // The main menu-item. It just contains the label that normally appears + // next to the spin-button, and an indicator for a sub-menu. + auto menu_item = Gtk::manage(new Gtk::MenuItem(_label_text)); + auto numeric_menu = create_numeric_menu(); + menu_item->set_submenu(*numeric_menu); + + set_proxy_menu_item(_name, *menu_item); + + return true; // Finished handling the event +} + +/** + * \brief Create a new SpinButtonToolItem + * + * \param[in] name A unique ID for this tool-item (not translatable) + * \param[in] label_text The text to display in the toolbar + * \param[in] adjustment The Gtk::Adjustment to attach to the spinbutton + * \param[in] climb_rate The climb rate for the spin button (default = 0) + * \param[in] digits Number of decimal places to display + */ +SpinButtonToolItem::SpinButtonToolItem(const Glib::ustring name, + const Glib::ustring& label_text, + Glib::RefPtr<Gtk::Adjustment>& adjustment, + double climb_rate, + int digits) + : _btn(Gtk::manage(new SpinButton(adjustment, climb_rate, digits))), + _name(std::move(name)), + _label_text(label_text), + _last_val(0.0), + _transfer_focus(false), + _focus_widget(nullptr) +{ + set_margin_start(3); + set_margin_end(3); + set_name(_name); + + // Handle popup menu + _btn->signal_popup_menu().connect(sigc::mem_fun(*this, &SpinButtonToolItem::on_popup_menu), false); + + // Handle button events + auto btn_focus_in_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_in_event); + _btn->signal_focus_in_event().connect(btn_focus_in_event_cb, false); + + auto btn_focus_out_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_focus_out_event); + _btn->signal_focus_out_event().connect(btn_focus_out_event_cb, false); + + auto btn_key_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_key_press_event); + _btn->signal_key_press_event().connect(btn_key_press_event_cb, false); + + auto btn_button_press_event_cb = sigc::mem_fun(*this, &SpinButtonToolItem::on_btn_button_press_event); + _btn->signal_button_press_event().connect(btn_button_press_event_cb, false); + + _btn->add_events(Gdk::KEY_PRESS_MASK); + + // Create a label + _label = Gtk::manage(new Gtk::Label(label_text)); + + // Arrange the widgets in a horizontal box + _hbox = Gtk::manage(new Gtk::Box()); + _hbox->set_spacing(3); + _hbox->pack_start(*_label); + _hbox->pack_start(*_btn); + add(*_hbox); + show_all(); +} + +void +SpinButtonToolItem::set_icon(const Glib::ustring& icon_name) +{ + _hbox->remove(*_label); + _icon = Gtk::manage(sp_get_icon_image(icon_name, Gtk::ICON_SIZE_SMALL_TOOLBAR)); + + if(_icon) { + _hbox->pack_start(*_icon); + _hbox->reorder_child(*_icon, 0); + } + + show_all(); +} + +bool +SpinButtonToolItem::on_btn_button_press_event(const GdkEventButton *button_event) +{ + if (gdk_event_triggers_context_menu(reinterpret_cast<const GdkEvent *>(button_event)) && + button_event->type == GDK_BUTTON_PRESS) { + do_popup_menu(button_event); + return true; + } + + return false; +} + +void +SpinButtonToolItem::do_popup_menu(const GdkEventButton *button_event) +{ + auto menu = create_numeric_menu(); + menu->attach_to_widget(*_btn); + menu->show_all(); + menu->popup_at_pointer(reinterpret_cast<const GdkEvent *>(button_event)); +} + +/** + * \brief Create a popup menu + */ +bool +SpinButtonToolItem::on_popup_menu() +{ + do_popup_menu(nullptr); + return true; +} + +/** + * \brief Transfers focus to the child spinbutton by default + */ +void +SpinButtonToolItem::on_grab_focus() +{ + grab_button_focus(); +} + +/** + * \brief Set the tooltip to display on this (and all child widgets) + * + * \param[in] text The tooltip to display + */ +void +SpinButtonToolItem::set_all_tooltip_text(const Glib::ustring& text) +{ + set_tooltip_text(text); + _btn->set_tooltip_text(text); +} + +/** + * \brief Set the widget that focus moves to when this one loses focus + * + * \param widget The widget that will gain focus + */ +void +SpinButtonToolItem::set_focus_widget(Gtk::Widget *widget) +{ + _focus_widget = widget; +} + +/** + * \brief Grab focus on the spin-button widget + */ +void +SpinButtonToolItem::grab_button_focus() +{ + _btn->grab_focus(); +} + +void +SpinButtonToolItem::set_custom_numeric_menu_data(std::vector<double>& values, + const std::vector<Glib::ustring>& labels) +{ + if(values.size() != labels.size() && !labels.empty()) { + g_warning("Cannot add custom menu items. Value and label arrays are different sizes"); + return; + } + + _custom_menu_data.clear(); + + int i = 0; + + for (auto value : values) { + if(labels.empty()) { + _custom_menu_data.push_back(std::make_pair(value, "")); + } + else { + _custom_menu_data.push_back(std::make_pair(value, labels[i++])); + } + } +} + +Glib::RefPtr<Gtk::Adjustment> +SpinButtonToolItem::get_adjustment() +{ + return _btn->get_adjustment(); +} +} // namespace Widget +} // namespace UI +} // namespace Inkscape +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-button-tool-item.h b/src/ui/widget/spin-button-tool-item.h new file mode 100644 index 0000000..c073f56 --- /dev/null +++ b/src/ui/widget/spin-button-tool-item.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SPIN_BUTTON_TOOL_ITEM_H +#define SEEN_SPIN_BUTTON_TOOL_ITEM_H + +#include <gtkmm/toolitem.h> + +namespace Gtk { +class Box; +class RadioButtonGroup; +class RadioMenuItem; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class SpinButton; + +/** + * \brief A spin-button with a label that can be added to a toolbar + */ +class SpinButtonToolItem : public Gtk::ToolItem +{ +private: + typedef std::vector< std::pair<double, Glib::ustring> > NumericMenuData; + + Glib::ustring _name; ///< A unique ID for the widget (NOT translatable) + SpinButton *_btn; ///< The spin-button within the widget + Glib::ustring _label_text; ///< A string to use in labels for the widget (translatable) + double _last_val; ///< The last value of the adjustment + bool _transfer_focus; ///< Whether or not to transfer focus + + Gtk::Box *_hbox; ///< Horizontal box, to store widgets + Gtk::Widget *_label; ///< A text label to describe the setting + Gtk::Widget *_icon; ///< An icon to describe the setting + + /** A widget that grabs focus when this one loses it */ + Gtk::Widget * _focus_widget; + + // Custom values and labels to add to the numeric popup-menu + NumericMenuData _custom_menu_data; + + // Event handlers + bool on_btn_focus_in_event(GdkEventFocus *focus_event); + bool on_btn_focus_out_event(GdkEventFocus *focus_event); + bool on_btn_key_press_event(GdkEventKey *key_event); + bool on_btn_button_press_event(const GdkEventButton *button_event); + bool on_popup_menu(); + void do_popup_menu(const GdkEventButton *button_event); + + void defocus(); + bool process_tab(int direction); + + void on_numeric_menu_item_toggled(double value); + + Gtk::Menu * create_numeric_menu(); + + Gtk::RadioMenuItem * create_numeric_menu_item(Gtk::RadioButtonGroup *group, + double value, + const Glib::ustring& label = ""); + +protected: + bool on_create_menu_proxy() override; + void on_grab_focus() override; + +public: + SpinButtonToolItem(const Glib::ustring name, + const Glib::ustring& label_text, + Glib::RefPtr<Gtk::Adjustment>& adjustment, + double climb_rate = 0.1, + int digits = 3); + + void set_all_tooltip_text(const Glib::ustring& text); + void set_focus_widget(Gtk::Widget *widget); + void grab_button_focus(); + + void set_custom_numeric_menu_data(std::vector<double>& values, + const std::vector<Glib::ustring>& labels = std::vector<Glib::ustring>()); + Glib::RefPtr<Gtk::Adjustment> get_adjustment(); + void set_icon(const Glib::ustring& icon_name); +}; +} // namespace Widget +} // namespace UI +} // namespace Inkscape +#endif // SEEN_SPIN_BUTTON_TOOL_ITEM_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-scale.cpp b/src/ui/widget/spin-scale.cpp new file mode 100644 index 0000000..5e3a2a2 --- /dev/null +++ b/src/ui/widget/spin-scale.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * + * Copyright (C) 2012 Author + * 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spin-scale.h" + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +SpinScale::SpinScale(const Glib::ustring label, double value, + double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttributeEnum a, const Glib::ustring tip_text) + : AttrWidget(a, value) + , _inkspinscale(value, lower, upper, step_increment, page_increment, 0) +{ + set_name("SpinScale"); + + _inkspinscale.set_label (label); + _inkspinscale.set_digits (digits); + _inkspinscale.set_tooltip_text (tip_text); + + _adjustment = _inkspinscale.get_adjustment(); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + + pack_start(_inkspinscale); + + show_all_children(); +} + +SpinScale::SpinScale(const Glib::ustring label, + Glib::RefPtr<Gtk::Adjustment> adjustment, int digits, + const SPAttributeEnum a, const Glib::ustring tip_text) + : AttrWidget(a, 0.0) + , _inkspinscale(adjustment) +{ + set_name("SpinScale"); + + _inkspinscale.set_label (label); + _inkspinscale.set_digits (digits); + _inkspinscale.set_tooltip_text (tip_text); + + _adjustment = _inkspinscale.get_adjustment(); + + signal_value_changed().connect(signal_attr_changed().make_slot()); + + pack_start(_inkspinscale); + + show_all_children(); +} + +Glib::ustring SpinScale::get_as_attribute() const +{ + const double val = _adjustment->get_value(); + + if( _inkspinscale.get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); +} + +void SpinScale::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if (val) + _adjustment->set_value(Glib::Ascii::strtod(val)); + else + _adjustment->set_value(get_default()->as_double()); +} + +Glib::SignalProxy0<void> SpinScale::signal_value_changed() +{ + return _adjustment->signal_value_changed(); +} + +double SpinScale::get_value() const +{ + return _adjustment->get_value(); +} + +void SpinScale::set_value(const double val) +{ + _adjustment->set_value(val); +} + +void SpinScale::set_focuswidget(GtkWidget *widget) +{ + _inkspinscale.set_focus_widget(widget); +} + +const decltype(SpinScale::_adjustment) SpinScale::get_adjustment() const +{ + return _adjustment; +} + +decltype(SpinScale::_adjustment) SpinScale::get_adjustment() +{ + return _adjustment; +} + + +DualSpinScale::DualSpinScale(const Glib::ustring label1, const Glib::ustring label2, + double value, double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttributeEnum a, + const Glib::ustring tip_text1, const Glib::ustring tip_text2) + : AttrWidget(a), + _s1(label1, value, lower, upper, step_increment, page_increment, digits, SP_ATTR_INVALID, tip_text1), + _s2(label2, value, lower, upper, step_increment, page_increment, digits, SP_ATTR_INVALID, tip_text2), + //TRANSLATORS: "Link" means to _link_ two sliders together + _link(C_("Sliders", "Link")) +{ + set_name("DualSpinScale"); + signal_value_changed().connect(signal_attr_changed().make_slot()); + + _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinScale::update_linked)); + + _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinScale::link_toggled)); + + Gtk::Box* vb = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vb->add(_s1); + vb->add(_s2); + pack_start(*vb); + pack_start(_link, false, false); + _link.set_active(true); + + show_all(); +} + +Glib::ustring DualSpinScale::get_as_attribute() const +{ + if(_link.get_active()) + return _s1.get_as_attribute(); + else + return _s1.get_as_attribute() + " " + _s2.get_as_attribute(); +} + +void DualSpinScale::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if(val) { + // Split val into parts + gchar** toks = g_strsplit(val, " ", 2); + + if(toks) { + double v1 = 0.0, v2 = 0.0; + if(toks[0]) + v1 = v2 = Glib::Ascii::strtod(toks[0]); + if(toks[1]) + v2 = Glib::Ascii::strtod(toks[1]); + + _link.set_active(toks[1] == nullptr); + + _s1.get_adjustment()->set_value(v1); + _s2.get_adjustment()->set_value(v2); + + g_strfreev(toks); + } + } +} + +sigc::signal<void>& DualSpinScale::signal_value_changed() +{ + return _signal_value_changed; +} + +const SpinScale& DualSpinScale::get_SpinScale1() const +{ + return _s1; +} + +SpinScale& DualSpinScale::get_SpinScale1() +{ + return _s1; +} + +const SpinScale& DualSpinScale::get_SpinScale2() const +{ + return _s2; +} + +SpinScale& DualSpinScale::get_SpinScale2() +{ + return _s2; +} + +void DualSpinScale::link_toggled() +{ + _s2.set_sensitive(!_link.get_active()); + update_linked(); +} + +void DualSpinScale::update_linked() +{ + if(_link.get_active()) + _s2.set_value(_s1.get_value()); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-scale.h b/src/ui/widget/spin-scale.h new file mode 100644 index 0000000..b154cb3 --- /dev/null +++ b/src/ui/widget/spin-scale.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * + * Copyright (C) 2012 Author + * 2017 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SPIN_SCALE_H +#define INKSCAPE_UI_WIDGET_SPIN_SCALE_H + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/togglebutton.h> +#include "attr-widget.h" +#include "ink-spinscale.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Wrap the InkSpinScale class and attach an attribute. + * A combo widget with label, scale slider, spinbutton, and adjustment; + */ +class SpinScale : public Gtk::Box, public AttrWidget +{ + +public: + SpinScale(const Glib::ustring label, double value, + double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttributeEnum a = SP_ATTR_INVALID, const Glib::ustring tip_text = ""); + + // Used by extensions + SpinScale(const Glib::ustring label, + Glib::RefPtr<Gtk::Adjustment> adjustment, int digits, + const SPAttributeEnum a = SP_ATTR_INVALID, const Glib::ustring tip_text = ""); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + // Shortcuts to _adjustment + Glib::SignalProxy0<void> signal_value_changed(); + double get_value() const; + void set_value(const double); + void set_focuswidget(GtkWidget *widget); + +private: + Glib::RefPtr<Gtk::Adjustment> _adjustment; + InkSpinScale _inkspinscale; + +public: + const decltype(_adjustment) get_adjustment() const; + decltype(_adjustment) get_adjustment(); +}; + + +/** + * Contains two SpinScales for controlling number-opt-number attributes. + * + * @see SpinScale + */ +class DualSpinScale : public Gtk::Box, public AttrWidget +{ +public: + DualSpinScale(const Glib::ustring label1, const Glib::ustring label2, + double value, double lower, double upper, + double step_increment, double page_increment, int digits, + const SPAttributeEnum a, + const Glib::ustring tip_text1, const Glib::ustring tip_text2); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + sigc::signal<void>& signal_value_changed(); + + const SpinScale& get_SpinScale1() const; + SpinScale& get_SpinScale1(); + + const SpinScale& get_SpinScale2() const; + SpinScale& get_SpinScale2(); + + //void remove_scale(); +private: + void link_toggled(); + void update_linked(); + sigc::signal<void> _signal_value_changed; + SpinScale _s1, _s2; + Gtk::ToggleButton _link; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SPIN_SCALE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-slider.cpp b/src/ui/widget/spin-slider.cpp new file mode 100644 index 0000000..e4cd0c6 --- /dev/null +++ b/src/ui/widget/spin-slider.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Felipe C. da S. Sanches <juca@members.fsf.org> + * + * Copyright (C) 2007 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spin-slider.h" + +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +SpinSlider::SpinSlider(double value, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, const char* tip_text) + : AttrWidget(a, value), + _adjustment(Gtk::Adjustment::create(value, lower, upper, step_inc)), + _scale(_adjustment), _spin(_adjustment, climb_rate, digits) +{ + set_name("SpinSlider"); + signal_value_changed().connect(signal_attr_changed().make_slot()); + + pack_start(_scale); + pack_start(_spin, false, false); + if (tip_text){ + _scale.set_tooltip_text(tip_text); + _spin.set_tooltip_text(tip_text); + } + + _scale.set_draw_value(false); + + show_all_children(); +} + +Glib::ustring SpinSlider::get_as_attribute() const +{ + const auto val = _adjustment->get_value(); + + if(_spin.get_digits() == 0) + return Glib::Ascii::dtostr((int)val); + else + return Glib::Ascii::dtostr(val); +} + +void SpinSlider::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if(val) + _adjustment->set_value(Glib::Ascii::strtod(val)); + else + _adjustment->set_value(get_default()->as_double()); +} + +Glib::SignalProxy0<void> SpinSlider::signal_value_changed() +{ + return _adjustment->signal_value_changed(); +} + +double SpinSlider::get_value() const +{ + return _adjustment->get_value(); +} + +void SpinSlider::set_value(const double val) +{ + _adjustment->set_value(val); +} + +const decltype(SpinSlider::_adjustment) SpinSlider::get_adjustment() const +{ + return _adjustment; +} + +decltype(SpinSlider::_adjustment) SpinSlider::get_adjustment() +{ + return _adjustment; +} + +const Gtk::Scale& SpinSlider::get_scale() const +{ + return _scale; +} + +Gtk::Scale& SpinSlider::get_scale() +{ + return _scale; +} + +const Inkscape::UI::Widget::SpinButton& SpinSlider::get_spin_button() const +{ + return _spin; +} +Inkscape::UI::Widget::SpinButton& SpinSlider::get_spin_button() +{ + return _spin; +} + +void SpinSlider::remove_scale() +{ + remove(_scale); +} + +DualSpinSlider::DualSpinSlider(double value, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a, char* tip_text1, char* tip_text2) + : AttrWidget(a), + _s1(value, lower, upper, step_inc, climb_rate, digits, SP_ATTR_INVALID, tip_text1), + _s2(value, lower, upper, step_inc, climb_rate, digits, SP_ATTR_INVALID, tip_text2), + //TRANSLATORS: "Link" means to _link_ two sliders together + _link(C_("Sliders", "Link")) +{ + signal_value_changed().connect(signal_attr_changed().make_slot()); + + _s1.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s2.get_adjustment()->signal_value_changed().connect(_signal_value_changed.make_slot()); + _s1.get_adjustment()->signal_value_changed().connect(sigc::mem_fun(*this, &DualSpinSlider::update_linked)); + _link.signal_toggled().connect(sigc::mem_fun(*this, &DualSpinSlider::link_toggled)); + + Gtk::VBox* vb = Gtk::manage(new Gtk::VBox); + vb->add(_s1); + vb->add(_s2); + pack_start(*vb); + pack_start(_link, false, false); + _link.set_active(true); + + show_all(); +} + +Glib::ustring DualSpinSlider::get_as_attribute() const +{ + if(_link.get_active()) + return _s1.get_as_attribute(); + else + return _s1.get_as_attribute() + " " + _s2.get_as_attribute(); +} + +void DualSpinSlider::set_from_attribute(SPObject* o) +{ + const gchar* val = attribute_value(o); + if(val) { + // Split val into parts + gchar** toks = g_strsplit(val, " ", 2); + + if(toks) { + double v1 = 0.0, v2 = 0.0; + if(toks[0]) + v1 = v2 = Glib::Ascii::strtod(toks[0]); + if(toks[1]) + v2 = Glib::Ascii::strtod(toks[1]); + + _link.set_active(toks[1] == nullptr); + + _s1.get_adjustment()->set_value(v1); + _s2.get_adjustment()->set_value(v2); + + g_strfreev(toks); + } + } +} + +sigc::signal<void>& DualSpinSlider::signal_value_changed() +{ + return _signal_value_changed; +} + +const SpinSlider& DualSpinSlider::get_spinslider1() const +{ + return _s1; +} + +SpinSlider& DualSpinSlider::get_spinslider1() +{ + return _s1; +} + +const SpinSlider& DualSpinSlider::get_spinslider2() const +{ + return _s2; +} + +SpinSlider& DualSpinSlider::get_spinslider2() +{ + return _s2; +} + +void DualSpinSlider::remove_scale() +{ + _s1.remove_scale(); + _s2.remove_scale(); +} + +void DualSpinSlider::link_toggled() +{ + _s2.set_sensitive(!_link.get_active()); + update_linked(); +} + +void DualSpinSlider::update_linked() +{ + if(_link.get_active()) + _s2.set_value(_s1.get_value()); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spin-slider.h b/src/ui/widget/spin-slider.h new file mode 100644 index 0000000..24a18a0 --- /dev/null +++ b/src/ui/widget/spin-slider.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Nicholas Bishop <nicholasbishop@gmail.com> + * + * Copyright (C) 2007 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SPIN_SLIDER_H +#define INKSCAPE_UI_WIDGET_SPIN_SLIDER_H + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/scale.h> +#include <gtkmm/togglebutton.h> +#include "spinbutton.h" +#include "attr-widget.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Groups an HScale and a SpinButton together using the same Adjustment. + */ +class SpinSlider : public Gtk::HBox, public AttrWidget +{ +public: + SpinSlider(double value, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum a = SP_ATTR_INVALID, const char* tip_text = nullptr); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + // Shortcuts to _adjustment + Glib::SignalProxy0<void> signal_value_changed(); + double get_value() const; + void set_value(const double); + + const Gtk::Scale& get_scale() const; + Gtk::Scale& get_scale(); + + const Inkscape::UI::Widget::SpinButton& get_spin_button() const; + Inkscape::UI::Widget::SpinButton& get_spin_button(); + + // Change the SpinSlider into a SpinButton with AttrWidget support) + void remove_scale(); +private: + Glib::RefPtr<Gtk::Adjustment> _adjustment; + Gtk::Scale _scale; + Inkscape::UI::Widget::SpinButton _spin; + +public: + const decltype(_adjustment) get_adjustment() const; + decltype(_adjustment) get_adjustment(); +}; + +/** + * Contains two SpinSliders for controlling number-opt-number attributes. + * + * @see SpinSlider + */ +class DualSpinSlider : public Gtk::HBox, public AttrWidget +{ +public: + DualSpinSlider(double value, double lower, double upper, double step_inc, + double climb_rate, int digits, const SPAttributeEnum, char* tip_text1, char* tip_text2); + + Glib::ustring get_as_attribute() const override; + void set_from_attribute(SPObject*) override; + + sigc::signal<void>& signal_value_changed(); + + const SpinSlider& get_spinslider1() const; + SpinSlider& get_spinslider1(); + + const SpinSlider& get_spinslider2() const; + SpinSlider& get_spinslider2(); + + void remove_scale(); +private: + void link_toggled(); + void update_linked(); + sigc::signal<void> _signal_value_changed; + SpinSlider _s1, _s2; + Gtk::ToggleButton _link; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SPIN_SLIDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spinbutton.cpp b/src/ui/widget/spinbutton.cpp new file mode 100644 index 0000000..c633035 --- /dev/null +++ b/src/ui/widget/spinbutton.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Johan B. C. Engelen + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spinbutton.h" +#include "unit-menu.h" +#include "unit-tracker.h" +#include "util/expression-evaluator.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + + +void +SpinButton::connect_signals() { + signal_input().connect(sigc::mem_fun(*this, &SpinButton::on_input)); + signal_focus_in_event().connect(sigc::mem_fun(*this, &SpinButton::on_my_focus_in_event)); + signal_key_press_event().connect(sigc::mem_fun(*this, &SpinButton::on_my_key_press_event)); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &SpinButton::on_scroll_event)); + set_focus_on_click(true); +}; + +int SpinButton::on_input(double* newvalue) +{ + try { + Inkscape::Util::EvaluatorQuantity result; + if (_unit_menu || _unit_tracker) { + Unit const *unit = nullptr; + if (_unit_menu) { + unit = _unit_menu->getUnit(); + } else { + unit = _unit_tracker->getActiveUnit(); + } + Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), unit); + result = eval.evaluate(); + // check if output dimension corresponds to input unit + if (result.dimension != (unit->isAbsolute() ? 1 : 0) ) { + throw Inkscape::Util::EvaluatorException("Input dimensions do not match with parameter dimensions.",""); + } + } else { + Inkscape::Util::ExpressionEvaluator eval = Inkscape::Util::ExpressionEvaluator(get_text().c_str(), nullptr); + result = eval.evaluate(); + } + *newvalue = result.value; + } + catch(Inkscape::Util::EvaluatorException &e) { + g_message ("%s", e.what()); + + return false; + } + + return true; +} + +bool SpinButton::on_my_focus_in_event(GdkEventFocus* /*event*/) +{ + _on_focus_in_value = get_value(); + return false; // do not consume the event +} + + + +bool SpinButton::on_scroll_event(GdkEventScroll *event) +{ + if (!is_focus()) { + return false; + } + double step, page; + get_increments(step, page); + if (event->state & GDK_CONTROL_MASK) { + step = page; + } + double change = 0.0; + if (event->direction == GDK_SCROLL_UP) { + change = step; + } else if (event->direction == GDK_SCROLL_DOWN) { + change = -step; + } else if (event->direction == GDK_SCROLL_SMOOTH) { + double delta_y_clamped = CLAMP(event->delta_y, -1, 1); // values > 1 result in excessive changes + change = step * -delta_y_clamped; + } else { + return false; + } + set_value(get_value() + change); + return true; +} + +bool SpinButton::on_my_key_press_event(GdkEventKey* event) +{ + switch (Inkscape::UI::Tools::get_latin_keyval (event)) { + case GDK_KEY_Escape: + undo(); + return true; // I consumed the event + break; + case GDK_KEY_z: + case GDK_KEY_Z: + if (event->state & GDK_CONTROL_MASK) { + undo(); + return true; // I consumed the event + } + break; + default: + break; + } + + return false; // do not consume the event +} + +void SpinButton::undo() +{ + set_value(_on_focus_in_value); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/spinbutton.h b/src/ui/widget/spinbutton.h new file mode 100644 index 0000000..710b511 --- /dev/null +++ b/src/ui/widget/spinbutton.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Johan B. C. Engelen + * + * Copyright (C) 2011 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_SPINBUTTON_H +#define INKSCAPE_UI_WIDGET_SPINBUTTON_H + +#include <gtkmm/spinbutton.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +class UnitMenu; +class UnitTracker; + +/** + * SpinButton widget, that allows entry of simple math expressions (also units, when linked with UnitMenu), + * and allows entry of both '.' and ',' for the decimal, even when in numeric mode. + * + * Calling "set_numeric()" effectively disables the expression parsing. If no unit menu is linked, all unitlike characters are ignored. + */ +class SpinButton : public Gtk::SpinButton +{ +public: + SpinButton(double climb_rate = 0.0, guint digits = 0) + : Gtk::SpinButton(climb_rate, digits), + _unit_menu(nullptr), + _unit_tracker(nullptr), + _on_focus_in_value(0.) + { + connect_signals(); + }; + explicit SpinButton(Glib::RefPtr<Gtk::Adjustment>& adjustment, double climb_rate = 0.0, guint digits = 0) + : Gtk::SpinButton(adjustment, climb_rate, digits), + _unit_menu(nullptr), + _unit_tracker(nullptr), + _on_focus_in_value(0.) + { + connect_signals(); + }; + + ~SpinButton() override = default; + + // noncopyable + SpinButton(const SpinButton&) = delete; + SpinButton& operator=(const SpinButton&) = delete; + + void setUnitMenu(UnitMenu* unit_menu) { _unit_menu = unit_menu; }; + + void addUnitTracker(UnitTracker* ut) { _unit_tracker = ut; }; + +protected: + UnitMenu *_unit_menu; /// Linked unit menu for unit conversion in entered expressions. + UnitTracker *_unit_tracker; // Linked unit tracker for unit conversion in entered expressions. + double _on_focus_in_value; + + void connect_signals(); + + /** + * This callback function should try to convert the entered text to a number and write it to newvalue. + * It calls a method to evaluate the (potential) mathematical expression. + * + * @retval false No conversion done, continue with default handler. + * @retval true Conversion successful, don't call default handler. + */ + int on_input(double* newvalue) override; + + /** + * When focus is obtained, save the value to enable undo later. + * @retval false continue with default handler. + * @retval true don't call default handler. + */ + bool on_my_focus_in_event(GdkEventFocus* event); + + /** + * When scroll is done. + * @retval false continue with default handler. + * @retval true don't call default handler. + */ + bool on_scroll_event(GdkEventScroll *event) override; + /** + * Handle specific keypress events, like Ctrl+Z. + * + * @retval false continue with default handler. + * @retval true don't call default handler. + */ + bool on_my_key_press_event(GdkEventKey* event); + + /** + * Undo the editing, by resetting the value upon when the spinbutton got focus. + */ + void undo(); +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_SPINBUTTON_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/style-subject.cpp b/src/ui/widget/style-subject.cpp new file mode 100644 index 0000000..9c30a42 --- /dev/null +++ b/src/ui/widget/style-subject.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style-subject.h" + +#include "desktop.h" +#include "desktop-style.h" +#include "selection.h" + +#include "xml/sp-css-attr.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +StyleSubject::StyleSubject() : _desktop(nullptr) { +} + +StyleSubject::~StyleSubject() { + setDesktop(nullptr); +} + +void StyleSubject::setDesktop(SPDesktop *desktop) { + if (desktop != _desktop) { + if (desktop) { + GC::anchor(desktop); + } + if (_desktop) { + GC::release(_desktop); + } + _desktop = desktop; + _afterDesktopSwitch(desktop); + _emitChanged(); + } +} + +StyleSubject::Selection::Selection() = default; + +StyleSubject::Selection::~Selection() = default; + +Inkscape::Selection *StyleSubject::Selection::_getSelection() const { + SPDesktop *desktop = getDesktop(); + if (desktop) { + return desktop->getSelection(); + } else { + return nullptr; + } +} + +std::vector<SPObject*> StyleSubject::Selection::list() { + Inkscape::Selection *selection = _getSelection(); + if(selection) { + return std::vector<SPObject *>(selection->objects().begin(), selection->objects().end()); + } + + return std::vector<SPObject*>(); +} + +Geom::OptRect StyleSubject::Selection::getBounds(SPItem::BBoxType type) { + Inkscape::Selection *selection = _getSelection(); + if (selection) { + return selection->bounds(type); + } else { + return Geom::OptRect(); + } +} + +int StyleSubject::Selection::queryStyle(SPStyle *query, int property) { + SPDesktop *desktop = getDesktop(); + if (desktop) { + return sp_desktop_query_style(desktop, query, property); + } else { + return QUERY_STYLE_NOTHING; + } +} + +void StyleSubject::Selection::_afterDesktopSwitch(SPDesktop *desktop) { + _sel_changed.disconnect(); + _subsel_changed.disconnect(); + _sel_modified.disconnect(); + if (desktop) { + _subsel_changed = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged))); + Inkscape::Selection *selection = desktop->getSelection(); + if (selection) { + _sel_changed = selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &Selection::_emitChanged))); + _sel_modified = selection->connectModified(sigc::mem_fun(*this, &Selection::_emitModified)); + } + } +} + +void StyleSubject::Selection::setCSS(SPCSSAttr *css) { + SPDesktop *desktop = getDesktop(); + if (desktop) { + sp_desktop_set_style(desktop, css); + } +} + +StyleSubject::CurrentLayer::CurrentLayer() { + _element = nullptr; +} + +StyleSubject::CurrentLayer::~CurrentLayer() = default; + +void StyleSubject::CurrentLayer::_setLayer(SPObject *layer) { + _layer_release.disconnect(); + _layer_modified.disconnect(); + if (_element) { + sp_object_unref(_element, nullptr); + } + _element = layer; + if (layer) { + sp_object_ref(layer, nullptr); + _layer_release = layer->connectRelease(sigc::hide(sigc::bind(sigc::mem_fun(*this, &CurrentLayer::_setLayer), (SPObject *)nullptr))); + _layer_modified = layer->connectModified(sigc::hide(sigc::hide(sigc::mem_fun(*this, &CurrentLayer::_emitChanged)))); + } + _emitChanged(); +} + +SPObject *StyleSubject::CurrentLayer::_getLayer() const { + return _element; +} + +SPObject *StyleSubject::CurrentLayer::_getLayerSList() const { + return _element; + +} + +std::vector<SPObject*> StyleSubject::CurrentLayer::list(){ + std::vector<SPObject*> list; + list.push_back(_element); + return list; +} + +Geom::OptRect StyleSubject::CurrentLayer::getBounds(SPItem::BBoxType type) { + SPObject *layer = _getLayer(); + if (layer && SP_IS_ITEM(layer)) { + return SP_ITEM(layer)->desktopBounds(type); + } else { + return Geom::OptRect(); + } +} + +int StyleSubject::CurrentLayer::queryStyle(SPStyle *query, int property) { + std::vector<SPItem*> list; + SPObject* i=_getLayerSList(); + if (i) { + list.push_back((SPItem*)i); + return sp_desktop_query_style_from_list(list, query, property); + } else { + return QUERY_STYLE_NOTHING; + } +} + +void StyleSubject::CurrentLayer::setCSS(SPCSSAttr *css) { + SPObject *layer = _getLayer(); + if (layer) { + sp_desktop_apply_css_recursive(layer, css, true); + } +} + +void StyleSubject::CurrentLayer::_afterDesktopSwitch(SPDesktop *desktop) { + _layer_switched.disconnect(); + if (desktop) { + _layer_switched = desktop->connectCurrentLayerChanged(sigc::mem_fun(*this, &CurrentLayer::_setLayer)); + _setLayer(desktop->currentLayer()); + } else { + _setLayer(nullptr); + } +} + +} +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/style-subject.h b/src/ui/widget/style-subject.h new file mode 100644 index 0000000..c2f2b3f --- /dev/null +++ b/src/ui/widget/style-subject.h @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Abstraction for different style widget operands. + */ +/* + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H +#define SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H + +#include <boost/optional.hpp> +#include <2geom/rect.h> +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "object/sp-item.h" +#include "object/sp-tag.h" +#include "object/sp-tag-use.h" +#include "object/sp-tag-use-reference.h" + +class SPDesktop; +class SPObject; +class SPCSSAttr; +class SPStyle; + +namespace Inkscape { +class Selection; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class StyleSubject { +public: + class Selection; + class CurrentLayer; + + + StyleSubject(); + virtual ~StyleSubject(); + + void setDesktop(SPDesktop *desktop); + SPDesktop *getDesktop() const { return _desktop; } + + virtual Geom::OptRect getBounds(SPItem::BBoxType type) = 0; + virtual int queryStyle(SPStyle *query, int property) = 0; + virtual void setCSS(SPCSSAttr *css) = 0; + virtual std::vector<SPObject*> list(){return std::vector<SPObject*>();}; + + sigc::connection connectChanged(sigc::signal<void>::slot_type slot) { + return _changed_signal.connect(slot); + } + +protected: + virtual void _afterDesktopSwitch(SPDesktop */*desktop*/) {} + void _emitChanged() { _changed_signal.emit(); } + void _emitModified(Inkscape::Selection* selection, guint flags) { + // Do not say this object has styles unless it's style has been modified + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + _emitChanged(); + } + } + +private: + sigc::signal<void> _changed_signal; + SPDesktop *_desktop; +}; + +class StyleSubject::Selection : public StyleSubject { +public: + Selection(); + ~Selection() override; + + Geom::OptRect getBounds(SPItem::BBoxType type) override; + int queryStyle(SPStyle *query, int property) override; + void setCSS(SPCSSAttr *css) override; + std::vector<SPObject*> list() override; + +protected: + void _afterDesktopSwitch(SPDesktop *desktop) override; + +private: + Inkscape::Selection *_getSelection() const; + + sigc::connection _sel_changed; + sigc::connection _subsel_changed; + sigc::connection _sel_modified; +}; + +class StyleSubject::CurrentLayer : public StyleSubject { +public: + CurrentLayer(); + ~CurrentLayer() override; + + Geom::OptRect getBounds(SPItem::BBoxType type) override; + int queryStyle(SPStyle *query, int property) override; + void setCSS(SPCSSAttr *css) override; + std::vector<SPObject*> list() override; + +protected: + void _afterDesktopSwitch(SPDesktop *desktop) override; + +private: + SPObject *_getLayer() const; + void _setLayer(SPObject *layer); + SPObject *_getLayerSList() const; + + sigc::connection _layer_switched; + sigc::connection _layer_release; + sigc::connection _layer_modified; + mutable SPObject* _element; +}; + +} +} +} + +#endif // SEEN_INKSCAPE_UI_WIDGET_STYLE_SUBJECT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/style-swatch.cpp b/src/ui/widget/style-swatch.cpp new file mode 100644 index 0000000..734f092 --- /dev/null +++ b/src/ui/widget/style-swatch.cpp @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Static style swatch (fill, stroke, opacity). + */ +/* Authors: + * buliabyak@gmail.com + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "style-swatch.h" + +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> + +#include "inkscape.h" +#include "verbs.h" + +#include "object/sp-linear-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "style.h" + +#include "helper/action.h" + +#include "ui/widget/color-preview.h" +#include "util/units.h" + +#include "widgets/spw-utilities.h" +#include "widgets/widget-sizes.h" + +#include "xml/sp-css-attr.h" + +enum { + SS_FILL, + SS_STROKE +}; + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * Watches whether the tool uses the current style. + */ +class StyleSwatch::ToolObserver : public Inkscape::Preferences::Observer { +public: + ToolObserver(Glib::ustring const &path, StyleSwatch &ss) : + Observer(path), + _style_swatch(ss) + {} + void notify(Inkscape::Preferences::Entry const &val) override; +private: + StyleSwatch &_style_swatch; +}; + +/** + * Watches for changes in the observed style pref. + */ +class StyleSwatch::StyleObserver : public Inkscape::Preferences::Observer { +public: + StyleObserver(Glib::ustring const &path, StyleSwatch &ss) : + Observer(path), + _style_swatch(ss) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + this->notify(prefs->getEntry(path)); + } + void notify(Inkscape::Preferences::Entry const &val) override { + SPCSSAttr *css = val.getInheritedStyle(); + _style_swatch.setStyle(css); + sp_repr_css_attr_unref(css); + } +private: + StyleSwatch &_style_swatch; +}; + +void StyleSwatch::ToolObserver::notify(Inkscape::Preferences::Entry const &val) +{ + bool usecurrent = val.getBool(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_style_swatch._style_obs) delete _style_swatch._style_obs; + + if (usecurrent) { + _style_swatch._style_obs = new StyleObserver("/desktop/style", _style_swatch); + + // If desktop's last-set style is empty, a tool uses its own fixed style even if set to use + // last-set (so long as it's empty). To correctly show this, we get the tool's style + // if the desktop's style is empty. + SPCSSAttr *css = prefs->getStyle("/desktop/style"); + if (!css->attributeList()) { + SPCSSAttr *css2 = prefs->getInheritedStyle(_style_swatch._tool_path + "/style"); + _style_swatch.setStyle(css2); + sp_repr_css_attr_unref(css2); + } + sp_repr_css_attr_unref(css); + } else { + _style_swatch._style_obs = new StyleObserver(_style_swatch._tool_path + "/style", _style_swatch); + } + prefs->addObserver(*_style_swatch._style_obs); +} + +StyleSwatch::StyleSwatch(SPCSSAttr *css, gchar const *main_tip) + : + _desktop(nullptr), + _verb_t(0), + _css(nullptr), + _tool_obs(nullptr), + _style_obs(nullptr), + _table(Gtk::manage(new Gtk::Grid())), + _sw_unit(nullptr) +{ + set_name("StyleSwatch"); + + _label[SS_FILL].set_markup(_("Fill:")); + _label[SS_STROKE].set_markup(_("Stroke:")); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + _label[i].set_halign(Gtk::ALIGN_START); + _label[i].set_valign(Gtk::ALIGN_CENTER); + _label[i].set_margin_top(0); + _label[i].set_margin_bottom(0); + _label[i].set_margin_start(0); + _label[i].set_margin_end(0); + + _color_preview[i] = new Inkscape::UI::Widget::ColorPreview (0); + } + + _opacity_value.set_halign(Gtk::ALIGN_START); + _opacity_value.set_valign(Gtk::ALIGN_CENTER); + _opacity_value.set_margin_top(0); + _opacity_value.set_margin_bottom(0); + _opacity_value.set_margin_start(0); + _opacity_value.set_margin_end(0); + + _table->set_column_spacing(2); + _table->set_row_spacing(0); + + _stroke.pack_start(_place[SS_STROKE]); + _stroke_width_place.add(_stroke_width); + _stroke.pack_start(_stroke_width_place, Gtk::PACK_SHRINK); + + _opacity_place.add(_opacity_value); + + _table->attach(_label[SS_FILL], 0, 0, 1, 1); + _table->attach(_label[SS_STROKE], 0, 1, 1, 1); + _table->attach(_place[SS_FILL], 1, 0, 1, 1); + _table->attach(_stroke, 1, 1, 1, 1); + _table->attach(_opacity_place, 2, 0, 1, 2); + + _swatch.add(*_table); + pack_start(_swatch, true, true, 0); + + set_size_request (STYLE_SWATCH_WIDTH, -1); + + setStyle (css); + + _swatch.signal_button_press_event().connect(sigc::mem_fun(*this, &StyleSwatch::on_click)); + + if (main_tip) + { + _swatch.set_tooltip_text(main_tip); + } +} + +void StyleSwatch::setClickVerb(sp_verb_t verb_t) { + _verb_t = verb_t; +} + +void StyleSwatch::setDesktop(SPDesktop *desktop) { + _desktop = desktop; +} + +bool +StyleSwatch::on_click(GdkEventButton */*event*/) +{ + if (this->_desktop && this->_verb_t != SP_VERB_NONE) { + Inkscape::Verb *verb = Inkscape::Verb::get(this->_verb_t); + SPAction *action = verb->get_action(Inkscape::ActionContext((Inkscape::UI::View::View *) this->_desktop)); + sp_action_perform (action, nullptr); + return true; + } + return false; +} + +StyleSwatch::~StyleSwatch() +{ + if (_css) + sp_repr_css_attr_unref (_css); + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + delete _color_preview[i]; + } + + if (_style_obs) delete _style_obs; + if (_tool_obs) delete _tool_obs; +} + +void +StyleSwatch::setWatchedTool(const char *path, bool synthesize) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (_tool_obs) { + delete _tool_obs; + _tool_obs = nullptr; + } + + if (path) { + _tool_path = path; + _tool_obs = new ToolObserver(_tool_path + "/usecurrent", *this); + prefs->addObserver(*_tool_obs); + } else { + _tool_path = ""; + } + + // hack until there is a real synthesize events function for prefs, + // which shouldn't be hard to write once there is sufficient need for it + if (synthesize && _tool_obs) { + _tool_obs->notify(prefs->getEntry(_tool_path + "/usecurrent")); + } +} + + +void StyleSwatch::setStyle(SPCSSAttr *css) +{ + if (_css) + sp_repr_css_attr_unref (_css); + + if (!css) + return; + + _css = sp_repr_css_attr_new(); + sp_repr_css_merge(_css, css); + + Glib::ustring css_string; + sp_repr_css_write_string (_css, css_string); + + SPStyle style(SP_ACTIVE_DOCUMENT); + if (!css_string.empty()) { + style.mergeString(css_string.c_str()); + } + setStyle (&style); +} + +void StyleSwatch::setStyle(SPStyle *query) +{ + _place[SS_FILL].remove(); + _place[SS_STROKE].remove(); + + bool has_stroke = true; + + for (int i = SS_FILL; i <= SS_STROKE; i++) { + Gtk::EventBox *place = &(_place[i]); + + SPIPaint *paint; + if (i == SS_FILL) { + paint = &(query->fill); + } else { + paint = &(query->stroke); + } + + if (paint->set && paint->isPaintserver()) { + SPPaintServer *server = (i == SS_FILL)? SP_STYLE_FILL_SERVER (query) : SP_STYLE_STROKE_SERVER (query); + + if (SP_IS_LINEARGRADIENT (server)) { + _value[i].set_markup(_("L Gradient")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Linear gradient fill")) : (_("Linear gradient stroke"))); + } else if (SP_IS_RADIALGRADIENT (server)) { + _value[i].set_markup(_("R Gradient")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Radial gradient fill")) : (_("Radial gradient stroke"))); + } else if (SP_IS_PATTERN (server)) { + _value[i].set_markup(_("Pattern")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Pattern fill")) : (_("Pattern stroke"))); + } + + } else if (paint->set && paint->isColor()) { + guint32 color = paint->value.color.toRGBA32( SP_SCALE24_TO_FLOAT ((i == SS_FILL)? query->fill_opacity.value : query->stroke_opacity.value) ); + ((Inkscape::UI::Widget::ColorPreview*)_color_preview[i])->setRgba32 (color); + _color_preview[i]->show_all(); + place->add(*_color_preview[i]); + gchar *tip; + if (i == SS_FILL) { + tip = g_strdup_printf (_("Fill: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + } else { + tip = g_strdup_printf (_("Stroke: %06x/%.3g"), color >> 8, SP_RGBA32_A_F(color)); + } + place->set_tooltip_text(tip); + g_free (tip); + } else if (paint->set && paint->isNone()) { + _value[i].set_markup(C_("Fill and stroke", "<i>None</i>")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (C_("Fill and stroke", "No fill")) : (C_("Fill and stroke", "No stroke"))); + if (i == SS_STROKE) has_stroke = false; + } else if (!paint->set) { + _value[i].set_markup(_("<b>Unset</b>")); + place->add(_value[i]); + place->set_tooltip_text((i == SS_FILL)? (_("Unset fill")) : (_("Unset stroke"))); + if (i == SS_STROKE) has_stroke = false; + } + } + +// Now query stroke_width + if (has_stroke) { + double w; + if (_sw_unit) { + w = Inkscape::Util::Quantity::convert(query->stroke_width.computed, "px", _sw_unit); + } else { + w = query->stroke_width.computed; + } + + { + gchar *str = g_strdup_printf(" %.3g", w); + _stroke_width.set_markup(str); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Stroke width: %.5g%s"), + w, + _sw_unit? _sw_unit->abbr.c_str() : "px"); + _stroke_width_place.set_tooltip_text(str); + g_free (str); + } + } else { + _stroke_width_place.set_tooltip_text(""); + _stroke_width.set_markup(""); + _stroke_width.set_has_tooltip(false); + } + + gdouble op = SP_SCALE24_TO_FLOAT(query->opacity.value); + if (op != 1) { + { + gchar *str; + str = g_strdup_printf(_("O: %2.0f"), (op*100.0)); + _opacity_value.set_markup (str); + g_free (str); + } + { + gchar *str = g_strdup_printf(_("Opacity: %2.1f %%"), (op*100.0)); + _opacity_place.set_tooltip_text(str); + g_free (str); + } + } else { + _opacity_place.set_tooltip_text(""); + _opacity_value.set_markup(""); + _opacity_value.set_has_tooltip(false); + } + + show_all(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/style-swatch.h b/src/ui/widget/style-swatch.h new file mode 100644 index 0000000..4c7dc51 --- /dev/null +++ b/src/ui/widget/style-swatch.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Static style swatch (fill, stroke, opacity) + */ +/* Authors: + * buliabyak@gmail.com + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_CURRENT_STYLE_H +#define INKSCAPE_UI_CURRENT_STYLE_H + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/enums.h> + +#include "desktop.h" +#include "preferences.h" + +class SPStyle; +class SPCSSAttr; + +namespace Gtk { +class Grid; +} + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace UI { +namespace Widget { + +class StyleSwatch : public Gtk::HBox +{ +public: + StyleSwatch (SPCSSAttr *attr, gchar const *main_tip); + + ~StyleSwatch() override; + + void setStyle(SPStyle *style); + void setStyle(SPCSSAttr *attr); + SPCSSAttr *getStyle(); + + void setWatchedTool (const char *path, bool synthesize); + + void setClickVerb(sp_verb_t verb_t); + void setDesktop(SPDesktop *desktop); + bool on_click(GdkEventButton *event); + +private: + class ToolObserver; + class StyleObserver; + + SPDesktop *_desktop; + sp_verb_t _verb_t; + SPCSSAttr *_css; + ToolObserver *_tool_obs; + StyleObserver *_style_obs; + Glib::ustring _tool_path; + + Gtk::EventBox _swatch; + + Gtk::Grid *_table; + + Gtk::Label _label[2]; + Gtk::EventBox _place[2]; + Gtk::EventBox _opacity_place; + Gtk::Label _value[2]; + Gtk::Label _opacity_value; + Gtk::Widget *_color_preview[2]; + Glib::ustring __color[2]; + Gtk::HBox _stroke; + Gtk::EventBox _stroke_width_place; + Gtk::Label _stroke_width; + + Inkscape::Util::Unit *_sw_unit; + +friend class ToolObserver; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_BUTTON_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/text.cpp b/src/ui/widget/text.cpp new file mode 100644 index 0000000..656ec45 --- /dev/null +++ b/src/ui/widget/text.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "text.h" +#include <gtkmm/entry.h> + +namespace Inkscape { +namespace UI { +namespace Widget { + +Text::Text(Glib::ustring const &label, Glib::ustring const &tooltip, + Glib::ustring const &suffix, + Glib::ustring const &icon, + bool mnemonic) + : Labelled(label, tooltip, new Gtk::Entry(), suffix, icon, mnemonic), + setProgrammatically(false) +{ +} + +Glib::ustring const Text::getText() const +{ + g_assert(_widget != nullptr); + return static_cast<Gtk::Entry*>(_widget)->get_text(); +} + +void Text::setText(Glib::ustring const text) +{ + g_assert(_widget != nullptr); + setProgrammatically = true; // callback is supposed to reset back, if it cares + static_cast<Gtk::Entry*>(_widget)->set_text(text); // FIXME: set correctly +} + +Glib::SignalProxy0<void> Text::signal_activate() +{ + return static_cast<Gtk::Entry*>(_widget)->signal_activate(); +} + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/text.h b/src/ui/widget/text.h new file mode 100644 index 0000000..87c9357 --- /dev/null +++ b/src/ui/widget/text.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Carl Hetherington <inkscape@carlh.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2004 Carl Hetherington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_TEXT_H +#define INKSCAPE_UI_WIDGET_TEXT_H + +#include "labelled.h" + + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A labelled text box, with optional icon or suffix, for entering arbitrary number values. + */ +class Text : public Labelled +{ +public: + + /** + * Construct a Text Widget. + * + * @param label Label. + * @param suffix Suffix, placed after the widget (defaults to ""). + * @param icon Icon filename, placed before the label (defaults to ""). + * @param mnemonic Mnemonic toggle; if true, an underscore (_) in the label + * indicates the next character should be used for the + * mnemonic accelerator key (defaults to false). + */ + Text(Glib::ustring const &label, + Glib::ustring const &tooltip, + Glib::ustring const &suffix = "", + Glib::ustring const &icon = "", + bool mnemonic = true); + + /** + * Get the text in the entry. + */ + Glib::ustring const getText() const; + + /** + * Sets the text of the text entry. + */ + void setText(Glib::ustring const text); + + void update(); + + /** + * Signal raised when the spin button's value changes. + */ + Glib::SignalProxy0<void> signal_activate(); + + bool setProgrammatically; // true if the value was set by setValue, not changed by the user; + // if a callback checks it, it must reset it back to false +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_TEXT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/tolerance-slider.cpp b/src/ui/widget/tolerance-slider.cpp new file mode 100644 index 0000000..b1b28a7 --- /dev/null +++ b/src/ui/widget/tolerance-slider.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * Abhishek Sharma + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "tolerance-slider.h" + +#include "registry.h" + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scale.h> + +#include "inkscape.h" +#include "document.h" +#include "document-undo.h" +#include "desktop.h" + +#include "object/sp-namedview.h" + +#include "svg/stringstream.h" + +#include "xml/repr.h" + +namespace Inkscape { +namespace UI { +namespace Widget { + +//=================================================== + +//--------------------------------------------------- + + + +//==================================================== + +ToleranceSlider::ToleranceSlider(const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr) +: _vbox(nullptr) +{ + init(label1, label2, label3, tip1, tip2, tip3, key, wr); +} + +ToleranceSlider::~ToleranceSlider() +{ + if (_vbox) delete _vbox; + _scale_changed_connection.disconnect(); +} + +void ToleranceSlider::init (const Glib::ustring& label1, const Glib::ustring& label2, const Glib::ustring& label3, const Glib::ustring& tip1, const Glib::ustring& tip2, const Glib::ustring& tip3, const Glib::ustring& key, Registry& wr) +{ + // hbox = label + slider + // + // e.g. + // + // snap distance |-------X---| 37 + + // vbox = checkbutton + // + + // hbox + + _vbox = new Gtk::VBox; + _hbox = Gtk::manage(new Gtk::HBox); + + Gtk::Label *theLabel1 = Gtk::manage(new Gtk::Label(label1)); + theLabel1->set_use_underline(); + theLabel1->set_halign(Gtk::ALIGN_START); + theLabel1->set_valign(Gtk::ALIGN_CENTER); + // align the label with the checkbox text above by indenting 22 px. + _hbox->pack_start(*theLabel1, Gtk::PACK_EXPAND_WIDGET, 22); + + _hscale = Gtk::manage(new Gtk::Scale(Gtk::ORIENTATION_HORIZONTAL)); + _hscale->set_range(1.0, 51.0); + + theLabel1->set_mnemonic_widget (*_hscale); + _hscale->set_draw_value (true); + _hscale->set_value_pos (Gtk::POS_RIGHT); + _hscale->set_size_request (100, -1); + _old_val = 10; + _hscale->set_value (_old_val); + _hscale->set_tooltip_text (tip1); + _hbox->add (*_hscale); + + + Gtk::Label *theLabel2 = Gtk::manage(new Gtk::Label(label2)); + theLabel2->set_use_underline(); + Gtk::Label *theLabel3 = Gtk::manage(new Gtk::Label(label3)); + theLabel3->set_use_underline(); + _button1 = Gtk::manage(new Gtk::RadioButton); + _radio_button_group = _button1->get_group(); + _button2 = Gtk::manage(new Gtk::RadioButton); + _button2->set_group(_radio_button_group); + _button1->set_tooltip_text (tip2); + _button2->set_tooltip_text (tip3); + _button1->add (*theLabel3); + _button1->set_halign(Gtk::ALIGN_START); + _button1->set_valign(Gtk::ALIGN_CENTER); + _button2->add (*theLabel2); + _button2->set_halign(Gtk::ALIGN_START); + _button2->set_valign(Gtk::ALIGN_CENTER); + + _vbox->add (*_button1); + _vbox->add (*_button2); + // Here we need some extra pixels to get the vertical spacing right. Why? + _vbox->pack_end(*_hbox, true, true, 3); // add 3 px. + _key = key; + _scale_changed_connection = _hscale->signal_value_changed().connect (sigc::mem_fun (*this, &ToleranceSlider::on_scale_changed)); + _btn_toggled_connection = _button2->signal_toggled().connect (sigc::mem_fun (*this, &ToleranceSlider::on_toggled)); + _wr = ≀ + _vbox->show_all_children(); +} + +void ToleranceSlider::setValue (double val) +{ + auto adj = _hscale->get_adjustment(); + + adj->set_lower (1.0); + adj->set_upper (51.0); + adj->set_step_increment (1.0); + + if (val > 9999.9) // magic value 10000.0 + { + _button1->set_active (true); + _button2->set_active (false); + _hbox->set_sensitive (false); + val = 50.0; + } + else + { + _button1->set_active (false); + _button2->set_active (true); + _hbox->set_sensitive (true); + } + _hscale->set_value (val); + _hbox->show_all(); +} + +void ToleranceSlider::setLimits (double theMin, double theMax) +{ + _hscale->set_range (theMin, theMax); + _hscale->get_adjustment()->set_step_increment (1); +} + +void ToleranceSlider::on_scale_changed() +{ + update (_hscale->get_value()); +} + +void ToleranceSlider::on_toggled() +{ + if (!_button2->get_active()) + { + _old_val = _hscale->get_value(); + _hbox->set_sensitive (false); + _hbox->show_all(); + setValue (10000.0); + update (10000.0); + } + else + { + _hbox->set_sensitive (true); + _hbox->show_all(); + setValue (_old_val); + update (_old_val); + } +} + +void ToleranceSlider::update (double val) +{ + if (_wr->isUpdating()) + return; + + SPDesktop *dt = SP_ACTIVE_DESKTOP; + if (!dt) + return; + + Inkscape::SVGOStringStream os; + os << val; + + _wr->setUpdating (true); + + SPDocument *doc = dt->getDocument(); + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); + Inkscape::XML::Node *repr = dt->getNamedView()->getRepr(); + repr->setAttribute(_key, os.str()); + DocumentUndo::setUndoSensitive(doc, saved); + + doc->setModifiedSinceSave(); + + _wr->setUpdating (false); +} + + +} // namespace Dialog +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/tolerance-slider.h b/src/ui/widget/tolerance-slider.h new file mode 100644 index 0000000..1c4af1d --- /dev/null +++ b/src/ui/widget/tolerance-slider.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ralf Stephan <ralf@ark.in-berlin.de> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_ +#define INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_ + +#include <gtkmm/radiobuttongroup.h> + +namespace Gtk { +class RadioButton; +class Scale; +class VBox; +class HBox; +} + +namespace Inkscape { +namespace UI { +namespace Widget { + +class Registry; + +/** + * Implementation of tolerance slider widget. + * This widget is part of the Document properties dialog. + */ +class ToleranceSlider { +public: + ToleranceSlider(const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& label3, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& tip3, + const Glib::ustring& key, + Registry& wr); + ~ToleranceSlider(); + void setValue (double); + void setLimits (double, double); + Gtk::VBox* _vbox; +private: + void init (const Glib::ustring& label1, + const Glib::ustring& label2, + const Glib::ustring& label3, + const Glib::ustring& tip1, + const Glib::ustring& tip2, + const Glib::ustring& tip3, + const Glib::ustring& key, + Registry& wr); + +protected: + void on_scale_changed(); + void on_toggled(); + void update (double val); + Gtk::HBox *_hbox; + Gtk::Scale *_hscale; + Gtk::RadioButtonGroup _radio_button_group; + Gtk::RadioButton *_button1; + Gtk::RadioButton *_button2; + Registry *_wr; + Glib::ustring _key; + sigc::connection _scale_changed_connection; + sigc::connection _btn_toggled_connection; + double _old_val; +}; + + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_TOLERANCE_SLIDER__H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/unit-menu.cpp b/src/ui/widget/unit-menu.cpp new file mode 100644 index 0000000..aaf565f --- /dev/null +++ b/src/ui/widget/unit-menu.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> + +#include "unit-menu.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +UnitMenu::UnitMenu() : _type(UNIT_TYPE_NONE) +{ + set_active(0); + gtk_widget_add_events(GTK_WIDGET(gobj()), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + signal_scroll_event().connect(sigc::mem_fun(*this, &UnitMenu::on_scroll_event)); +} + +UnitMenu::~UnitMenu() = default; + +bool UnitMenu::setUnitType(UnitType unit_type) +{ + // Expand the unit widget with unit entries from the unit table + UnitTable::UnitMap m = unit_table.units(unit_type); + + for (auto & i : m) { + append(i.first); + } + _type = unit_type; + set_active_text(unit_table.primary(unit_type)); + + return true; +} + +bool UnitMenu::resetUnitType(UnitType unit_type) +{ + remove_all(); + + return setUnitType(unit_type); +} + +void UnitMenu::addUnit(Unit const& u) +{ + unit_table.addUnit(u, false); + append(u.abbr); +} + +Unit const * UnitMenu::getUnit() const +{ + if (get_active_text() == "") { + g_assert(_type != UNIT_TYPE_NONE); + return unit_table.getUnit(unit_table.primary(_type)); + } + return unit_table.getUnit(get_active_text()); +} + +bool UnitMenu::setUnit(Glib::ustring const & unit) +{ + // TODO: Determine if 'unit' is available in the dropdown. + // If not, return false + + set_active_text(unit); + return true; +} + +Glib::ustring UnitMenu::getUnitAbbr() const +{ + if (get_active_text() == "") { + return ""; + } + return getUnit()->abbr; +} + +UnitType UnitMenu::getUnitType() const +{ + return getUnit()->type; +} + +double UnitMenu::getUnitFactor() const +{ + return getUnit()->factor; +} + +int UnitMenu::getDefaultDigits() const +{ + return getUnit()->defaultDigits(); +} + +double UnitMenu::getDefaultStep() const +{ + int factor_digits = -1*int(log10(getUnit()->factor)); + return pow(10.0, factor_digits); +} + +double UnitMenu::getDefaultPage() const +{ + return 10 * getDefaultStep(); +} + +double UnitMenu::getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr) const +{ + double old_factor = getUnit()->factor; + if (old_unit_abbr != "no_unit") { + old_factor = unit_table.getUnit(old_unit_abbr)->factor; + } + Unit const * new_unit = unit_table.getUnit(new_unit_abbr); + + // Catch the case of zero or negative unit factors (error!) + if (old_factor < 0.0000001 || + new_unit->factor < 0.0000001) { + // TODO: Should we assert here? + return 0.00; + } + + return old_factor / new_unit->factor; +} + +bool UnitMenu::isAbsolute() const +{ + return getUnitType() != UNIT_TYPE_DIMENSIONLESS; +} + +bool UnitMenu::isRadial() const +{ + return getUnitType() == UNIT_TYPE_RADIAL; +} + +bool UnitMenu::on_scroll_event(GdkEventScroll *event) { return false; } + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/unit-menu.h b/src/ui/widget/unit-menu.h new file mode 100644 index 0000000..b8e3ab7 --- /dev/null +++ b/src/ui/widget/unit-menu.h @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Bryce Harrington <bryce@bryceharrington.org> + * + * Copyright (C) 2004 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_UNIT_H +#define INKSCAPE_UI_WIDGET_UNIT_H + +#include <gtkmm/comboboxtext.h> +#include "util/units.h" + +using namespace Inkscape::Util; + +namespace Inkscape { +namespace UI { +namespace Widget { + +/** + * A drop down menu for choosing unit types. + */ +class UnitMenu : public Gtk::ComboBoxText +{ +public: + + /** + * Construct a UnitMenu + */ + UnitMenu(); + + ~UnitMenu() override; + + /** + * Adds the unit type to the widget. This extracts the corresponding + * units from the unit map matching the given type, and appends them + * to the dropdown widget. It causes the primary unit for the given + * unit_type to be selected. + */ + bool setUnitType(UnitType unit_type); + + /** + * Removes all unit entries, then adds the unit type to the widget. + * This extracts the corresponding + * units from the unit map matching the given type, and appends them + * to the dropdown widget. It causes the primary unit for the given + * unit_type to be selected. + */ + bool resetUnitType(UnitType unit_type); + + /** + * Adds a unit, possibly user-defined, to the menu. + */ + void addUnit(Unit const& u); + + /** + * Sets the dropdown widget to the given unit abbreviation. + * Returns true if the unit was selectable, false if not + * (i.e., if the unit was not present in the widget). + */ + bool setUnit(Glib::ustring const &unit); + + /** + * Returns the Unit object corresponding to the current selection + * in the dropdown widget. + */ + Unit const * getUnit() const; + + /** + * Returns the abbreviated unit name of the selected unit. + */ + Glib::ustring getUnitAbbr() const; + + /** + * Returns the UnitType of the selected unit. + */ + UnitType getUnitType() const; + + /** + * Returns the unit factor for the selected unit. + */ + double getUnitFactor() const; + + /** + * Returns the recommended number of digits for displaying + * numbers of this unit type. + */ + int getDefaultDigits() const; + + /** + * Returns the recommended step size in spin buttons + * displaying units of this type. + */ + double getDefaultStep() const; + + /** + * Returns the recommended page size (when hitting pgup/pgdn) + * in spin buttons displaying units of this type. + */ + double getDefaultPage() const; + + /** + * Returns the conversion factor required to convert values + * of the currently selected unit into units of type + * new_unit_abbr. + */ + double getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr = "no_unit") const; + + /** + * Returns true if the selected unit is not dimensionless + * (false for %, true for px, pt, cm, etc). + */ + bool isAbsolute() const; + + /** + * Returns true if the selected unit is radial (deg or rad). + */ + bool isRadial() const; + +protected: + UnitType _type; + /** + * block scroll from widget if is inside a scrolled window. + */ + bool on_scroll_event(GdkEventScroll *event) override; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_UNIT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/ui/widget/unit-tracker.cpp b/src/ui/widget/unit-tracker.cpp new file mode 100644 index 0000000..40d5ccf --- /dev/null +++ b/src/ui/widget/unit-tracker.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::UnitTracker + * Simple mediator to synchronize changes to unit menus + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2007 Jon A. Cruz + * Copyright (C) 2013 Matthew Petroff + * Copyright (C) 2018 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <iostream> + +#include "unit-tracker.h" + +#include "combo-tool-item.h" + +#define COLUMN_STRING 0 + +using Inkscape::Util::UnitTable; +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace UI { +namespace Widget { + +UnitTracker::UnitTracker(UnitType unit_type) : + _active(0), + _isUpdating(false), + _activeUnit(nullptr), + _activeUnitInitialized(false), + _store(nullptr), + _priorValues() +{ + UnitTable::UnitMap m = unit_table.units(unit_type); + + ComboToolItemColumns columns; + _store = Gtk::ListStore::create(columns); + Gtk::TreeModel::Row row; + + for (auto & m_iter : m) { + + Glib::ustring unit = m_iter.first; + + row = *(_store->append()); + row[columns.col_label ] = unit; + row[columns.col_value ] = unit; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + } + + // Why? + gint count = _store->children().size(); + if ((count > 0) && (_active > count)) { + _setActive(--count); + } else { + _setActive(_active); + } +} + +UnitTracker::~UnitTracker() +{ + _combo_list.clear(); + + // Unhook weak references to GtkAdjustments + for (auto i : _adjList) { + g_object_weak_unref(G_OBJECT(i), _adjustmentFinalizedCB, this); + } + _adjList.clear(); +} + +bool UnitTracker::isUpdating() const +{ + return _isUpdating; +} + +Inkscape::Util::Unit const * UnitTracker::getActiveUnit() const +{ + return _activeUnit; +} + +void UnitTracker::changeLabel(Glib::ustring new_label, gint pos, bool onlylabel) +{ + ComboToolItemColumns columns; + _store->children()[pos][columns.col_label] = new_label; + if (!onlylabel) { + _store->children()[pos][columns.col_value] = new_label; + } +} + +void UnitTracker::setActiveUnit(Inkscape::Util::Unit const *unit) +{ + if (unit) { + + ComboToolItemColumns columns; + int index = 0; + for (auto& row: _store->children() ) { + Glib::ustring storedUnit = row[columns.col_value]; + if (!unit->abbr.compare (storedUnit)) { + _setActive (index); + break; + } + index++; + } + } +} + +void UnitTracker::setActiveUnitByAbbr(gchar const *abbr) +{ + Inkscape::Util::Unit const *u = unit_table.getUnit(abbr); + setActiveUnit(u); +} + +void UnitTracker::addAdjustment(GtkAdjustment *adj) +{ + if (std::find(_adjList.begin(),_adjList.end(),adj) == _adjList.end()) { + g_object_weak_ref(G_OBJECT(adj), _adjustmentFinalizedCB, this); + _adjList.push_back(adj); + } else { + std::cerr << "UnitTracker::addAjustment: Adjustment already added!" << std::endl; + } +} + +void UnitTracker::addUnit(Inkscape::Util::Unit const *u) +{ + ComboToolItemColumns columns; + + Gtk::TreeModel::Row row; + row = *(_store->append()); + row[columns.col_label ] = u ? u->abbr.c_str() : ""; + row[columns.col_value ] = u ? u->abbr.c_str() : ""; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; +} + +void UnitTracker::prependUnit(Inkscape::Util::Unit const *u) +{ + ComboToolItemColumns columns; + + Gtk::TreeModel::Row row; + row = *(_store->prepend()); + row[columns.col_label ] = u ? u->abbr.c_str() : ""; + row[columns.col_value ] = u ? u->abbr.c_str() : ""; + row[columns.col_tooltip ] = (""); + row[columns.col_icon ] = "NotUsed"; + row[columns.col_sensitive] = true; + + /* Re-shuffle our default selection here (_active gets out of sync) */ + setActiveUnit(_activeUnit); + +} + +void UnitTracker::setFullVal(GtkAdjustment *adj, gdouble val) +{ + _priorValues[adj] = val; +} + +ComboToolItem * +UnitTracker::create_tool_item(Glib::ustring const &label, + Glib::ustring const &tooltip) +{ + auto combo = ComboToolItem::create(label, tooltip, "NotUsed", _store); + combo->set_active(_active); + combo->signal_changed().connect(sigc::mem_fun(*this, &UnitTracker::_unitChangedCB)); + combo->set_data("unit-tracker", this); + _combo_list.push_back(combo); + return combo; +} + +void UnitTracker::_unitChangedCB(int active) +{ + _setActive(active); +} + +void UnitTracker::_adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was) +{ + if (data && where_the_object_was) { + UnitTracker *self = reinterpret_cast<UnitTracker *>(data); + self->_adjustmentFinalized(where_the_object_was); + } +} + +void UnitTracker::_adjustmentFinalized(GObject *where_the_object_was) +{ + GtkAdjustment* adj = (GtkAdjustment*)(where_the_object_was); + auto it = std::find(_adjList.begin(),_adjList.end(), adj); + if (it != _adjList.end()) { + _adjList.erase(it); + } else { + g_warning("Received a finalization callback for unknown object %p", where_the_object_was); + } +} + +void UnitTracker::_setActive(gint active) +{ + if ( active != _active || !_activeUnitInitialized ) { + gint oldActive = _active; + + if (_store) { + + // Find old and new units + ComboToolItemColumns columns; + int index = 0; + Glib::ustring oldAbbr( "NotFound" ); + Glib::ustring newAbbr( "NotFound" ); + for (auto& row: _store->children() ) { + if (index == _active) { + oldAbbr = row[columns.col_value]; + } + if (index == active) { + newAbbr = row[columns.col_value]; + } + if (newAbbr != "NotFound" && oldAbbr != "NotFound") break; + ++index; + } + + if (oldAbbr != "NotFound") { + + if (newAbbr != "NotFound") { + Inkscape::Util::Unit const *oldUnit = unit_table.getUnit(oldAbbr); + Inkscape::Util::Unit const *newUnit = unit_table.getUnit(newAbbr); + _activeUnit = newUnit; + + if (!_adjList.empty()) { + _fixupAdjustments(oldUnit, newUnit); + } + } else { + std::cerr << "UnitTracker::_setActive: Did not find new unit: " << active << std::endl; + } + + } else { + std::cerr << "UnitTracker::_setActive: Did not find old unit: " << oldActive + << " new: " << active << std::endl; + } + } + _active = active; + + for (auto combo : _combo_list) { + if(combo) combo->set_active(active); + } + + _activeUnitInitialized = true; + } +} + +void UnitTracker::_fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit) +{ + _isUpdating = true; + for ( auto adj : _adjList ) { + gdouble oldVal = gtk_adjustment_get_value(adj); + gdouble val = oldVal; + + if ( (oldUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) + && (newUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) ) + { + val = newUnit->factor * 100; + _priorValues[adj] = Inkscape::Util::Quantity::convert(oldVal, oldUnit, "px"); + } else if ( (oldUnit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) + && (newUnit->type != Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) ) + { + if (_priorValues.find(adj) != _priorValues.end()) { + val = Inkscape::Util::Quantity::convert(_priorValues[adj], "px", newUnit); + } + } else { + val = Inkscape::Util::Quantity::convert(oldVal, oldUnit, newUnit); + } + + gtk_adjustment_set_value(adj, val); + } + _isUpdating = false; +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/ui/widget/unit-tracker.h b/src/ui/widget/unit-tracker.h new file mode 100644 index 0000000..b85da06 --- /dev/null +++ b/src/ui/widget/unit-tracker.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::UI::Widget::UnitTracker + * Simple mediator to synchronize changes to unit menus + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2007 Jon A. Cruz + * Copyright (C) 2013 Matthew Petroff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UI_WIDGET_UNIT_TRACKER_H +#define INKSCAPE_UI_WIDGET_UNIT_TRACKER_H + +#include <map> +#include <vector> + +#include <gtkmm/liststore.h> + +#include "util/units.h" + +using Inkscape::Util::Unit; +using Inkscape::Util::UnitType; + +typedef struct _GObject GObject; +typedef struct _GtkAdjustment GtkAdjustment; +typedef struct _GtkListStore GtkListStore; + +namespace Inkscape { +namespace UI { +namespace Widget { +class ComboToolItem; + +class UnitTracker { +public: + UnitTracker(UnitType unit_type); + virtual ~UnitTracker(); + + bool isUpdating() const; + + void setActiveUnit(Inkscape::Util::Unit const *unit); + void setActiveUnitByAbbr(gchar const *abbr); + Inkscape::Util::Unit const * getActiveUnit() const; + + void addUnit(Inkscape::Util::Unit const *u); + void addAdjustment(GtkAdjustment *adj); + void prependUnit(Inkscape::Util::Unit const *u); + void setFullVal(GtkAdjustment *adj, gdouble val); + void changeLabel(Glib::ustring new_label, gint pos, bool onlylabel = false); + + ComboToolItem *create_tool_item(Glib::ustring const &label, + Glib::ustring const &tooltip); + +protected: + UnitType _type; + +private: + // Callbacks + void _unitChangedCB(int active); + static void _adjustmentFinalizedCB(gpointer data, GObject *where_the_object_was); + + void _setActive(gint index); + void _fixupAdjustments(Inkscape::Util::Unit const *oldUnit, Inkscape::Util::Unit const *newUnit); + + // Cleanup + void _adjustmentFinalized(GObject *where_the_object_was); + + gint _active; + bool _isUpdating; + Inkscape::Util::Unit const *_activeUnit; + bool _activeUnitInitialized; + + Glib::RefPtr<Gtk::ListStore> _store; + std::vector<ComboToolItem *> _combo_list; + std::vector<GtkAdjustment*> _adjList; + std::map <GtkAdjustment *, gdouble> _priorValues; +}; + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#endif // INKSCAPE_UI_WIDGET_UNIT_TRACKER_H diff --git a/src/unclump.cpp b/src/unclump.cpp new file mode 100644 index 0000000..6b134c0 --- /dev/null +++ b/src/unclump.cpp @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Unclumping objects. + */ +/* Authors: + * bulia byak + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <map> + +#include <2geom/transforms.h> + +#include "unclump.h" + +#include "object/sp-item.h" + + +// Taking bbox of an item is an expensive operation, and we need to do it many times, so here we +// cache the centers, widths, and heights of items + +//FIXME: make a class with these cashes as members instead of globals +std::map<const gchar *, Geom::Point> c_cache; +std::map<const gchar *, Geom::Point> wh_cache; + +/** +Center of bbox of item +*/ +static Geom::Point +unclump_center (SPItem *item) +{ + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(item->getId()); + if ( i != c_cache.end() ) { + return i->second; + } + + Geom::OptRect r = item->desktopVisualBounds(); + if (r) { + Geom::Point const c = r->midpoint(); + c_cache[item->getId()] = c; + return c; + } else { + // FIXME + return Geom::Point(0, 0); + } +} + +static Geom::Point +unclump_wh (SPItem *item) +{ + Geom::Point wh; + std::map<const gchar *, Geom::Point>::iterator i = wh_cache.find(item->getId()); + if ( i != wh_cache.end() ) { + wh = i->second; + } else { + Geom::OptRect r = item->desktopVisualBounds(); + if (r) { + wh = r->dimensions(); + wh_cache[item->getId()] = wh; + } else { + wh = Geom::Point(0, 0); + } + } + + return wh; +} + +/** +Distance between "edges" of item1 and item2. An item is considered to be an ellipse inscribed into its w/h, +so its radius (distance from center to edge) depends on the w/h and the angle towards the other item. +May be negative if the edge of item1 is between the center and the edge of item2. +*/ +static double +unclump_dist (SPItem *item1, SPItem *item2) +{ + Geom::Point c1 = unclump_center (item1); + Geom::Point c2 = unclump_center (item2); + + Geom::Point wh1 = unclump_wh (item1); + Geom::Point wh2 = unclump_wh (item2); + + // angle from each item's center to the other's, unsqueezed by its w/h, normalized to 0..pi/2 + double a1 = atan2 ((c2 - c1)[Geom::Y], (c2 - c1)[Geom::X] * wh1[Geom::Y]/wh1[Geom::X]); + a1 = fabs (a1); + if (a1 > M_PI/2) a1 = M_PI - a1; + + double a2 = atan2 ((c1 - c2)[Geom::Y], (c1 - c2)[Geom::X] * wh2[Geom::Y]/wh2[Geom::X]); + a2 = fabs (a2); + if (a2 > M_PI/2) a2 = M_PI - a2; + + // get the radius of each item for the given angle + double r1 = 0.5 * (wh1[Geom::X] + (wh1[Geom::Y] - wh1[Geom::X]) * (a1/(M_PI/2))); + double r2 = 0.5 * (wh2[Geom::X] + (wh2[Geom::Y] - wh2[Geom::X]) * (a2/(M_PI/2))); + + // dist between centers minus angle-adjusted radii + double dist_r = (Geom::L2 (c2 - c1) - r1 - r2); + + double stretch1 = wh1[Geom::Y]/wh1[Geom::X]; + double stretch2 = wh2[Geom::Y]/wh2[Geom::X]; + + if ((stretch1 > 1.5 || stretch1 < 0.66) && (stretch2 > 1.5 || stretch2 < 0.66)) { + + std::vector<double> dists; + dists.push_back (dist_r); + + // If both objects are not circle-like, find dists between four corners + std::vector<Geom::Point> c1_points(2); + { + double y_closest; + if (c2[Geom::Y] > c1[Geom::Y] + wh1[Geom::Y]/2) { + y_closest = c1[Geom::Y] + wh1[Geom::Y]/2; + } else if (c2[Geom::Y] < c1[Geom::Y] - wh1[Geom::Y]/2) { + y_closest = c1[Geom::Y] - wh1[Geom::Y]/2; + } else { + y_closest = c2[Geom::Y]; + } + c1_points[0] = Geom::Point (c1[Geom::X], y_closest); + double x_closest; + if (c2[Geom::X] > c1[Geom::X] + wh1[Geom::X]/2) { + x_closest = c1[Geom::X] + wh1[Geom::X]/2; + } else if (c2[Geom::X] < c1[Geom::X] - wh1[Geom::X]/2) { + x_closest = c1[Geom::X] - wh1[Geom::X]/2; + } else { + x_closest = c2[Geom::X]; + } + c1_points[1] = Geom::Point (x_closest, c1[Geom::Y]); + } + + + std::vector<Geom::Point> c2_points(2); + { + double y_closest; + if (c1[Geom::Y] > c2[Geom::Y] + wh2[Geom::Y]/2) { + y_closest = c2[Geom::Y] + wh2[Geom::Y]/2; + } else if (c1[Geom::Y] < c2[Geom::Y] - wh2[Geom::Y]/2) { + y_closest = c2[Geom::Y] - wh2[Geom::Y]/2; + } else { + y_closest = c1[Geom::Y]; + } + c2_points[0] = Geom::Point (c2[Geom::X], y_closest); + double x_closest; + if (c1[Geom::X] > c2[Geom::X] + wh2[Geom::X]/2) { + x_closest = c2[Geom::X] + wh2[Geom::X]/2; + } else if (c1[Geom::X] < c2[Geom::X] - wh2[Geom::X]/2) { + x_closest = c2[Geom::X] - wh2[Geom::X]/2; + } else { + x_closest = c1[Geom::X]; + } + c2_points[1] = Geom::Point (x_closest, c2[Geom::Y]); + } + + for (int i = 0; i < 2; i ++) { + for (int j = 0; j < 2; j ++) { + dists.push_back (Geom::L2 (c1_points[i] - c2_points[j])); + } + } + + // return the minimum of all dists + return *std::min_element(dists.begin(), dists.end()); + } else { + return dist_r; + } +} + +/** +Average unclump_dist from item to others +*/ +static double unclump_average (SPItem *item, std::list<SPItem*> &others) +{ + int n = 0; + double sum = 0; + for (std::list<SPItem*>::const_iterator i = others.begin(); i != others.end();++i) { + SPItem *other = *i; + + if (other == item) + continue; + + n++; + sum += unclump_dist (item, other); + } + + if (n != 0) + return sum/n; + else + return 0; +} + +/** +Closest to item among others + */ +static SPItem *unclump_closest (SPItem *item, std::list<SPItem*> &others) +{ + double min = HUGE_VAL; + SPItem *closest = nullptr; + + for (std::list<SPItem*>::const_iterator i = others.begin(); i != others.end();++i) { + SPItem *other = *i; + + if (other == item) + continue; + + double dist = unclump_dist (item, other); + if (dist < min && fabs (dist) < 1e6) { + min = dist; + closest = other; + } + } + + return closest; +} + +/** +Most distant from item among others + */ +static SPItem *unclump_farest (SPItem *item, std::list<SPItem*> &others) +{ + double max = -HUGE_VAL; + SPItem *farest = nullptr; + for (std::list<SPItem*>::const_iterator i = others.begin(); i != others.end();++i) { + SPItem *other = *i; + + if (other == item) + continue; + + double dist = unclump_dist (item, other); + if (dist > max && fabs (dist) < 1e6) { + max = dist; + farest = other; + } + } + + return farest; +} + +/** +Removes from the \a rest list those items that are "behind" \a closest as seen from \a item, +i.e. those on the other side of the line through \a closest perpendicular to the direction from \a +item to \a closest. Returns a newly created list which must be freed. + */ +static std::vector<SPItem*> +unclump_remove_behind (SPItem *item, SPItem *closest, std::list<SPItem*> &rest) +{ + Geom::Point it = unclump_center (item); + Geom::Point p1 = unclump_center (closest); + + // perpendicular through closest to the direction to item: + Geom::Point perp = Geom::rot90(it - p1); + Geom::Point p2 = p1 + perp; + + // get the standard Ax + By + C = 0 form for p1-p2: + double A = p1[Geom::Y] - p2[Geom::Y]; + double B = p2[Geom::X] - p1[Geom::X]; + double C = p2[Geom::Y] * p1[Geom::X] - p1[Geom::Y] * p2[Geom::X]; + + // substitute the item into it: + double val_item = A * it[Geom::X] + B * it[Geom::Y] + C; + + std::vector<SPItem*> out; + for (std::list<SPItem*>::const_reverse_iterator i = rest.rbegin(); i != rest.rend();++i) { + SPItem *other = *i; + + if (other == item) + continue; + + Geom::Point o = unclump_center (other); + double val_other = A * o[Geom::X] + B * o[Geom::Y] + C; + + if (val_item * val_other <= 1e-6) { + // different signs, which means item and other are on the different sides of p1-p2 line; skip + } else { + out.push_back(other); + } + } + + return out; +} + +/** +Moves \a what away from \a from by \a dist + */ +static void +unclump_push (SPItem *from, SPItem *what, double dist) +{ + Geom::Point it = unclump_center (what); + Geom::Point p = unclump_center (from); + Geom::Point by = dist * Geom::unit_vector (- (p - it)); + + Geom::Affine move = Geom::Translate (by); + + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(what->getId()); + if ( i != c_cache.end() ) { + i->second *= move; + } + + //g_print ("push %s at %g,%g from %g,%g by %g,%g, dist %g\n", what->getId(), it[Geom::X],it[Geom::Y], p[Geom::X],p[Geom::Y], by[Geom::X],by[Geom::Y], dist); + + what->set_i2d_affine(what->i2dt_affine() * move); + what->doWriteTransform(what->transform); +} + +/** +Moves \a what towards \a to by \a dist + */ +static void +unclump_pull (SPItem *to, SPItem *what, double dist) +{ + Geom::Point it = unclump_center (what); + Geom::Point p = unclump_center (to); + Geom::Point by = dist * Geom::unit_vector (p - it); + + Geom::Affine move = Geom::Translate (by); + + std::map<const gchar *, Geom::Point>::iterator i = c_cache.find(what->getId()); + if ( i != c_cache.end() ) { + i->second *= move; + } + + //g_print ("pull %s at %g,%g to %g,%g by %g,%g, dist %g\n", what->getId(), it[Geom::X],it[Geom::Y], p[Geom::X],p[Geom::Y], by[Geom::X],by[Geom::Y], dist); + + what->set_i2d_affine(what->i2dt_affine() * move); + what->doWriteTransform(what->transform); +} + + +/** +Unclumps the items in \a items, reducing local unevenness in their distribution. Produces an effect +similar to "engraver dots". The only distribution which is unchanged by unclumping is a hexagonal +grid. May be called repeatedly for stronger effect. + */ +void +unclump (std::vector<SPItem*> &items) +{ + c_cache.clear(); + wh_cache.clear(); + + for (std::vector<SPItem*>::const_iterator i = items.begin(); i != items.end();++i) { // for each original/clone x: + SPItem *item = *i; + + std::list<SPItem*> nei; + + std::list<SPItem*> rest; + for (int i=0; i < static_cast<int>(items.size()); i++) { + rest.push_front(items[items.size() - i - 1]); + } + rest.remove(item); + + while (!rest.empty()) { + SPItem *closest = unclump_closest (item, rest); + if (closest) { + nei.push_front(closest); + rest.remove(closest); + std::vector<SPItem*> new_rest = unclump_remove_behind (item, closest, rest); + rest.clear(); + for (int i=0; i < static_cast<int>(new_rest.size()); i++) { + rest.push_front(new_rest[new_rest.size() - i - 1]); + } + } else { + break; + } + } + + if ( (nei.size()) >= 2) { + double ave = unclump_average (item, nei); + + SPItem *closest = unclump_closest (item, nei); + SPItem *farest = unclump_farest (item, nei); + + double dist_closest = unclump_dist (closest, item); + double dist_farest = unclump_dist (farest, item); + + //g_print ("NEI %d for item %s closest %s at %g farest %s at %g ave %g\n", g_slist_length(nei), item->getId(), closest->getId(), dist_closest, farest->getId(), dist_farest, ave); + + if (fabs (ave) < 1e6 && fabs (dist_closest) < 1e6 && fabs (dist_farest) < 1e6) { // otherwise the items are bogus + // increase these coefficients to make unclumping more aggressive and less stable + // the pull coefficient is a bit bigger to counteract the long-term expansion trend + unclump_push (closest, item, 0.3 * (ave - dist_closest)); + unclump_pull (farest, item, 0.35 * (dist_farest - ave)); + } + } + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/unclump.h b/src/unclump.h new file mode 100644 index 0000000..823a83e --- /dev/null +++ b/src/unclump.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Unclumping objects + */ +/* Authors: + * bulia byak + * + * Copyright (C) 2005 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_UNCLUMP_H +#define SEEN_DIALOGS_UNCLUMP_H + +#include <vector> + +class SPItem; + +void unclump(std::vector<SPItem*> &items); + +#endif /* !UNCLUMP_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/undo-stack-observer.h b/src/undo-stack-observer.h new file mode 100644 index 0000000..0d27780 --- /dev/null +++ b/src/undo-stack-observer.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * David Yip <yipdw@rose-hulman.edu> + * + * Copyright (c) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_UNDO_COMMIT_OBSERVER_H +#define SEEN_UNDO_COMMIT_OBSERVER_H + +#include "inkgc/gc-managed.h" + +namespace Inkscape { + +struct Event; + +/** + * Observes changes made to the undo and redo stacks. + * + * More specifically, an UndoStackObserver is a class that receives notifications when + * any of the following events occur: + * <ul> + * <li>A change is committed to the undo stack.</li> + * <li>An undo action is made.</li> + * <li>A redo action is made.</li> + * </ul> + * + * UndoStackObservers should not be used on their own. Instead, they should be registered + * with a CompositeUndoStackObserver. + */ +class UndoStackObserver : public GC::Managed<> { +public: + UndoStackObserver() = default; + virtual ~UndoStackObserver() = default; + + /** + * Triggered when the user issues an undo command. + * + * \param log Pointer to an Event describing the undone event. + */ + virtual void notifyUndoEvent(Event* log) = 0; + + /** + * Triggered when the user issues a redo command. + * + * \param log Pointer to an Event describing the redone event. + */ + virtual void notifyRedoEvent(Event* log) = 0; + + /** + * Triggered when a set of transactions is committed to the undo log. + * + * \param log Pointer to an Event describing the committed events. + */ + virtual void notifyUndoCommitEvent(Event* log) = 0; + + /** + * Triggered when the undo log is cleared. + */ + virtual void notifyClearUndoEvent() = 0; + + /** + * Triggered when the redo log is cleared. + */ + virtual void notifyClearRedoEvent() = 0; + +}; + +} + +#endif // SEEN_UNDO_COMMIT_OBSERVER_H diff --git a/src/unicoderange.cpp b/src/unicoderange.cpp new file mode 100644 index 0000000..ca1a3f6 --- /dev/null +++ b/src/unicoderange.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "unicoderange.h" + +#include <cstdlib> +#include <cstring> + +static unsigned int hex2int(char* s){ + int res=0; + int i=0, mul=1; + while(s[i+1]!='\0') i++; + + while(i>=0){ + if (s[i] >= 'A' && s[i] <= 'F') res += mul * (s[i]-'A'+10); + if (s[i] >= 'a' && s[i] <= 'f') res += mul * (s[i]-'a'+10); + if (s[i] >= '0' && s[i] <= '9') res += mul * (s[i]-'0'); + i--; + mul*=16; + } + return res; +} + +UnicodeRange::UnicodeRange(const gchar* value){ + if (!value) return; + gchar* val = (gchar*) value; + while(val[0] != '\0'){ + if (val[0]=='U' && val[1]=='+'){ + val += add_range(val+2); + } else { + this->unichars.push_back(g_utf8_get_char(&val[0])); + val++; + } + //skip spaces or commas + while(val[0]==' ' || val[0]==',') val++; + } +} + +int UnicodeRange::add_range(gchar* val){ + Urange r; + int i=0, count=0; + while(val[i]!='\0' && val[i]!='-' && val[i]!=' ' && val[i]!=','){ + i++; + } + r.start = (gchar*) malloc((i+1)*sizeof(gchar)); + strncpy(r.start, val, i); + r.start[i] = '\0'; + val+=i; + count+=i; + i=0; + if (val[0]=='-'){ + val++; + while(val[i]!='\0' && val[i]!='-' && val[i]!=' ' && val[i]!=',') i++; + r.end = (gchar*) malloc((i+1)*sizeof(gchar)); + strncpy(r.end, val, i); + r.end[i] = '\0'; + // val+=i; + count+=i; + } else { + r.end=nullptr; + } + this->range.push_back(r); + return count+1; +} + +bool UnicodeRange::contains(gchar unicode){ + for(unsigned int unichar : this->unichars){ + if (static_cast<gunichar>(unicode) == unichar){ + return true; + } + } + + unsigned int unival; + unival = g_utf8_get_char (&unicode); + char uni[9] = "00000000"; + uni[8]= '\0'; + for (unsigned int i=7; unival>0; i--){ + unsigned char val = unival & 0xf; + unival = unival >> 4; + if (val < 10) uni[i] = '0' + val; + else uni[i] = 'A'+ val - 10; + } + + bool found; + for(auto r : this->range){ + if (r.end){ + if (unival >= hex2int(r.start) && unival <= hex2int(r.end)) return true; + } else { + found = true; + + int p=0; + while (r.start[p]!='\0') p++; + p--; + + for (int pos=8;p>=0;pos--,p--){ + if (uni[pos]!='?' && uni[pos]!=r.start[p]) found = false; + } + if (found) return true; + } + } + return false; +} + +Glib::ustring UnicodeRange::attribute_string(){ + Glib::ustring result; + unsigned int i; + for(i=0; i<this->unichars.size(); i++){ + result += this->unichars[i]; + if (i!=this->unichars.size()-1) result += ","; + } + + for(i=0; i<this->range.size(); i++){ + result += "U+" + Glib::ustring(this->range[i].start); + if (this->range[i].end) result += "-" + Glib::ustring(this->range[i].end); + if (i!=this->range.size()-1) result += ", "; + } + + return result; +} + +gunichar UnicodeRange::sample_glyph(){ + //This could be better + if (!unichars.empty()) + return unichars[0]; + if (!range.empty()) + return hex2int(range[0].start); + return (gunichar) ' '; +} + diff --git a/src/unicoderange.h b/src/unicoderange.h new file mode 100644 index 0000000..8a75631 --- /dev/null +++ b/src/unicoderange.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glibmm/ustring.h> +#include <vector> + +// A type which can hold any UTF-32 or UCS-4 character code. +typedef unsigned int gunichar; + +struct Urange{ + char* start; + char* end; +}; + +class UnicodeRange{ +public: +UnicodeRange(const char* val); +int add_range(char* val); +bool contains(char unicode); +Glib::ustring attribute_string(); +gunichar sample_glyph(); + +private: +std::vector<Urange> range; +std::vector<gunichar> unichars; +}; + diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt new file mode 100644 index 0000000..4815590 --- /dev/null +++ b/src/util/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(util_SRC + ege-appear-time-tracker.cpp + ege-tags.cpp + expression-evaluator.cpp + share.cpp + units.cpp + ziptool.cpp + + + # ------- + # Headers + const_char_ptr.h + copy.h + ege-appear-time-tracker.h + ege-tags.h + enums.h + expression-evaluator.h + find-if-before.h + find-last-if.h + fixed_point.h + format.h + forward-pointer-iterator.h + list-container-test.h + list-container.h + list-copy.h + list.h + longest-common-suffix.h + reference.h + reverse-list.h + share.h + signal-blocker.h + ucompose.hpp + units.h + ziptool.h +) + +add_inkscape_lib(util_LIB "${util_SRC}") +# add_inkscape_source("${util_SRC}") diff --git a/src/util/README b/src/util/README new file mode 100644 index 0000000..254e3fa --- /dev/null +++ b/src/util/README @@ -0,0 +1,9 @@ + + +This directory contains a variety of utility code. + +To do: + +* Merge with 'helper' into this directory. +* Move individual files to more appropriate directories. +* Split into three sub-directories: numeric, color, svg. diff --git a/src/util/const_char_ptr.h b/src/util/const_char_ptr.h new file mode 100644 index 0000000..872ca67 --- /dev/null +++ b/src/util/const_char_ptr.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Provides `const_char_ptr` + */ +/* + * Authors: + * Sergei Izmailov <sergei.a.izmailov@gmail.com> + * + * Copyright (C) 2020 Sergei Izmailov + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_UTIL_CONST_CHAR_PTR_H +#define SEEN_INKSCAPE_UTIL_CONST_CHAR_PTR_H +#include <glibmm/ustring.h> +#include <string> +#include "share.h" + +namespace Inkscape { +namespace Util { + +/** + * Non-owning reference to 'const char*' + * Main-purpose: avoid overloads of type `f(char*, str&)`, `f(str&, char*)`, `f(char*, char*)`, ... + */ +class const_char_ptr{ +public: + const_char_ptr() noexcept: m_data(nullptr){}; + const_char_ptr(std::nullptr_t): const_char_ptr() {}; + const_char_ptr(const char* const data) noexcept: m_data(data) {}; + const_char_ptr(const Glib::ustring& str) noexcept: const_char_ptr(str.c_str()) {}; + const_char_ptr(const std::string& str) noexcept: const_char_ptr(str.c_str()) {}; + const_char_ptr(const ptr_shared& shared) : const_char_ptr(static_cast<const char* const>(shared)) {}; + + const char * data() const noexcept { return m_data; } +private: + const char * const m_data = nullptr; +}; +} +} +#endif // SEEN_INKSCAPE_UTIL_CONST_CHAR_PTR_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: \ No newline at end of file diff --git a/src/util/copy.h b/src/util/copy.h new file mode 100644 index 0000000..6068132 --- /dev/null +++ b/src/util/copy.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Traits::Copy - traits class to determine types to use when copying + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_TRAITS_COPY_H +#define SEEN_INKSCAPE_TRAITS_COPY_H + +namespace Inkscape { + +namespace Traits { + +template <typename T> +struct Copy { + typedef T Type; +}; + +template <typename T> +struct Copy<T const> { + typedef T Type; +}; + +template <typename T> +struct Copy<T &> { + typedef T &Type; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ege-appear-time-tracker.cpp b/src/util/ege-appear-time-tracker.cpp new file mode 100644 index 0000000..d3c0899 --- /dev/null +++ b/src/util/ege-appear-time-tracker.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Appear Time Tracker. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +#include "ege-appear-time-tracker.h" +#include <glib-object.h> +#include <gtk/gtk.h> + + +namespace ege +{ + +namespace { + +void unhookHandler( gulong &id, GtkWidget *obj ) +{ + if ( id ) { + if ( obj ) { + g_signal_handler_disconnect( G_OBJECT(obj), id ); + } + id = 0; + } +} + +} // namespace + + +AppearTimeTracker::AppearTimeTracker(GTimer *timer, GtkWidget *widget, gchar const* name) : + _name(name ? name : ""), + _timer(timer), + _widget(widget), + _topMost(widget), + _autodelete(false), + _mapId(0), + _realizeId(0), + _hierarchyId(0) + +{ + while (gtk_widget_get_parent(_topMost)) { + _topMost = gtk_widget_get_parent(_topMost); + } + _mapId = g_signal_connect( G_OBJECT(_topMost), "map-event", G_CALLBACK(mapCB), this ); + _realizeId = g_signal_connect( G_OBJECT(_topMost), "realize", G_CALLBACK(realizeCB), this ); + _hierarchyId = g_signal_connect( G_OBJECT(_widget), "hierarchy-changed", G_CALLBACK(hierarchyCB), this ); +} + +AppearTimeTracker::~AppearTimeTracker() +{ + if ( _timer ) { + g_timer_destroy(_timer); + _timer = nullptr; + } + + unhookHandler( _mapId, _topMost ); + unhookHandler( _realizeId, _topMost ); + unhookHandler( _hierarchyId, _widget ); +} + +void AppearTimeTracker::stop() { + if (_timer) { + g_timer_stop(_timer); + } +} + +void AppearTimeTracker::setAutodelete(bool autodelete) +{ + if ( autodelete != _autodelete ) { + _autodelete = autodelete; + } +} + +void AppearTimeTracker::report(gchar const* msg) +{ + gulong msCount = 0; + gdouble secs = g_timer_elapsed( _timer, &msCount ); + g_message("Time ended at %2.3f with [%s] on [%s]", secs, msg, _name.c_str()); +} + +void AppearTimeTracker::handleHierarchyChange( GtkWidget * /*prevTop*/ ) +{ + GtkWidget *newTop = _widget; + while (gtk_widget_get_parent(newTop)) { + newTop = gtk_widget_get_parent(newTop); + } + + if ( newTop != _topMost ) { + unhookHandler( _mapId, _topMost ); + unhookHandler( _realizeId, _topMost ); + + _topMost = newTop; + _mapId = g_signal_connect( G_OBJECT(_topMost), "map-event", G_CALLBACK(mapCB), this ); + _realizeId = g_signal_connect( G_OBJECT(_topMost), "realize", G_CALLBACK(realizeCB), this ); + } +} + +gboolean AppearTimeTracker::mapCB(GtkWidget * /*widget*/, GdkEvent * /*event*/, gpointer userData) +{ + AppearTimeTracker *tracker = reinterpret_cast<AppearTimeTracker*>(userData); + tracker->report("MAP"); + if ( tracker->_autodelete ) { + delete tracker; + } + return FALSE; +} + +void AppearTimeTracker::realizeCB(GtkWidget * /*widget*/, gpointer userData) +{ + AppearTimeTracker *tracker = reinterpret_cast<AppearTimeTracker*>(userData); + tracker->report("REALIZE"); +} + +void AppearTimeTracker::hierarchyCB(GtkWidget * /*widget*/, GtkWidget *prevTop, gpointer userData) +{ + AppearTimeTracker *tracker = reinterpret_cast<AppearTimeTracker*>(userData); + tracker->handleHierarchyChange( prevTop ); +} + +} // namespace ege + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ege-appear-time-tracker.h b/src/util/ege-appear-time-tracker.h new file mode 100644 index 0000000..4318e62 --- /dev/null +++ b/src/util/ege-appear-time-tracker.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +#ifndef SEEN_APPEAR_TIME_TRACKER_H +#define SEEN_APPEAR_TIME_TRACKER_H + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Appear Time Tracker. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <glib.h> +#include <glibmm/ustring.h> + +typedef union _GdkEvent GdkEvent; +typedef struct _GtkWidget GtkWidget; + +namespace ege +{ + +class AppearTimeTracker { +public: + AppearTimeTracker(GTimer *timer, GtkWidget *widget, gchar const* name); + ~AppearTimeTracker(); + + void stop(); + + bool isAutodelete() const { return _autodelete; } + void setAutodelete(bool autodelete); + +private: + Glib::ustring _name; + GTimer *_timer; + GtkWidget *_widget; + GtkWidget *_topMost; + bool _autodelete; + gulong _mapId; + gulong _realizeId; + gulong _hierarchyId; + + static gboolean mapCB(GtkWidget *widget, GdkEvent *event, gpointer userData); + static void realizeCB(GtkWidget *widget, gpointer userData); + static void hierarchyCB(GtkWidget *widget, GtkWidget *prevTop, gpointer userData); + + void report(gchar const* msg); + void handleHierarchyChange( GtkWidget *prevTop ); +}; + +} // namespace ege + +#endif // SEEN_APPEAR_TIME_TRACKER_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ege-tags.cpp b/src/util/ege-tags.cpp new file mode 100644 index 0000000..f8acab8 --- /dev/null +++ b/src/util/ege-tags.cpp @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is EGE Tagging Support. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <libintl.h> + +#if !defined(_) +#define _(s) gettext(s) +#endif // !defined(_) + +#include <set> +#include <algorithm> +#include <functional> +#include <utility> + +#include "ege-tags.h" + +#include <glib.h> + +namespace ege +{ + +Label::Label(std::string lang, std::string value) : + lang(std::move(lang)), + value(std::move(value)) +{ +} + +Label::~Label() += default; + +// ========================================================================= + +Tag::~Tag() += default; + +Tag::Tag(std::string key) : + key(std::move(key)) +{ +} + +// ========================================================================= + +TagSet::TagSet() : + lang(), + tags(), + counts() +{ +} + +TagSet::~TagSet() += default; + +void TagSet::setLang(std::string const& lang) +{ + if (lang != this->lang) { + this->lang = lang; + } +} + + +struct sameLang : public std::binary_function<Label, Label, bool> { + bool operator()(Label const& x, Label const& y) const { return (x.lang == y.lang); } +}; + + +bool TagSet::addTag(Tag const& tag) +{ + bool present = false; + + for ( std::vector<Tag>::iterator it = tags.begin(); (it != tags.end()) && !present; ++it ) { + if (tag.key == it->key) { + present = true; + + for (const auto & label : tag.labels) { + std::vector<Label>::iterator itOld = std::find_if( it->labels.begin(), it->labels.end(), std::bind2nd(sameLang(), label) ); + if (itOld != it->labels.end()) { + itOld->value = label.value; + } else { + it->labels.push_back(label); + } + } + } + } + + if (!present) { + tags.push_back(tag); + counts[tag.key] = 0; + } + + return present; +} + + +std::vector<Tag> const& TagSet::getTags() +{ + return tags; +} + +int TagSet::getCount( std::string const& key ) +{ + int count = 0; + if ( counts.find(key) != counts.end() ) { + count = counts[key]; + } + return count; +} + +void TagSet::increment( std::string const& key ) +{ + if ( counts.find(key) != counts.end() ) { + counts[key]++; + } else { + Tag tag(key); + tags.push_back(tag); + counts[key] = 1; + } +} + +void TagSet::decrement( std::string const& key ) +{ + if ( counts.find(key) != counts.end() ) { + counts[key]--; + } +} + +} // namespace ege + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ege-tags.h b/src/util/ege-tags.h new file mode 100644 index 0000000..dfa584d --- /dev/null +++ b/src/util/ege-tags.h @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +#ifndef SEEN_EGE_TAGS_H +#define SEEN_EGE_TAGS_H + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is EGE Tagging Support. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <string> +#include <vector> +#include <map> + +/* + * Implements base tagging of http://create.freedesktop.org/wiki/ResourceTagging . + */ + +// Note that this API is preliminary and subject to frequent change: + +namespace ege +{ + +class Label +{ +public: + Label(); + Label(std::string lang, std::string value); + ~Label(); + + std::string lang; + std::string value; +}; + +class Tag +{ +public: + Tag(); + Tag(std::string key); + ~Tag(); + + std::string key; + std::vector<Label> labels; +}; + + +/** + * Contains a set of tags with unique keys, and with locale support. + * + */ +class TagSet +{ +public: + TagSet(); + ~TagSet(); + + std::string const & getLang() const; + void setLang(std::string const& lang); + + /** + * Adds or updates a tag. + * + * @return true if a tag was updated, false if it was added. + */ + bool addTag(Tag const& tag); + std::vector<Tag> const& getTags(); + + int getCount( std::string const& key ); + void increment( std::string const& key ); + void decrement( std::string const& key ); + +private: + + std::string lang; + std::vector<Tag> tags; + std::map<std::string, int> counts; +}; + +} // namespace ege + + +#endif // SEEN_EGE_TAGS_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/enums.h b/src/util/enums.h new file mode 100644 index 0000000..46c4d5e --- /dev/null +++ b/src/util/enums.h @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Nicholas Bishop <nicholasbishop@gmail.com> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_UTIL_ENUMS_H +#define INKSCAPE_UTIL_ENUMS_H + +#include <glibmm/ustring.h> + +namespace Inkscape { +namespace Util { + +/** + * Simplified management of enumerations of svg items with UI labels. + * IMPORTANT: + * When initializing the EnumData struct, you cannot use _(...) to translate strings. + * Instead, one must use N_(...) and do the translation every time the string is retrieved. + */ +template<typename E> +struct EnumData +{ + E id; + const Glib::ustring label; + const Glib::ustring key; +}; + +const Glib::ustring empty_string(""); + +/** + * Simplified management of enumerations of svg items with UI labels. + * + * @note that get_id_from_key and get_id_from_label return 0 if it cannot find an entry for that key string. + * @note that get_label and get_key return an empty string when the requested id is not in the list. + */ +template<typename E> class EnumDataConverter +{ +public: + typedef EnumData<E> Data; + + EnumDataConverter(const EnumData<E>* cd, const unsigned int length) + : _length(length), _data(cd) + {} + + E get_id_from_label(const Glib::ustring& label) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].label == label) + return _data[i].id; + } + + return (E)0; + } + + E get_id_from_key(const Glib::ustring& key) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].key == key) + return _data[i].id; + } + + return (E)0; + } + + bool is_valid_key(const Glib::ustring& key) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].key == key) + return true; + } + + return false; + } + + bool is_valid_id(const E id) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].id == id) + return true; + } + return false; + } + + const Glib::ustring& get_label(const E id) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].id == id) + return _data[i].label; + } + + return empty_string; + } + + const Glib::ustring& get_key(const E id) const + { + for(unsigned int i = 0; i < _length; ++i) { + if(_data[i].id == id) + return _data[i].key; + } + + return empty_string; + } + + const EnumData<E>& data(const unsigned int i) const + { + return _data[i]; + } + + const unsigned int _length; +private: + const EnumData<E>* _data; +}; + + +} +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/expression-evaluator.cpp b/src/util/expression-evaluator.cpp new file mode 100644 index 0000000..24a56b7 --- /dev/null +++ b/src/util/expression-evaluator.cpp @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +/** @file + * TODO: insert short description here + */ +/* LIBGIMP - The GIMP Library + * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball + * + * Original file from libgimpwidgets: gimpeevl.c + * Copyright (C) 2008 Fredrik Alstromer <roe@excu.se> + * Copyright (C) 2008 Martin Nordholts <martinn@svn.gnome.org> + * Modified for Inkscape by Johan Engelen + * Copyright (C) 2011 Johan Engelen + * Copyright (C) 2013 Matthew Petroff + * + * This library is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * <http://www.gnu.org/licenses/>. + */ + +#include "util/expression-evaluator.h" +#include "util/units.h" + +#include <glib/gconvert.h> + +#include <cmath> +#include <cstring> + +using Inkscape::Util::unit_table; + +namespace Inkscape { +namespace Util { + +EvaluatorQuantity::EvaluatorQuantity(double value, unsigned int dimension) : + value(value), + dimension(dimension) +{ +} + +EvaluatorToken::EvaluatorToken() +{ + type = 0; + value.fl = 0; +} + +ExpressionEvaluator::ExpressionEvaluator(const char *string, Unit const *unit) : + string(g_locale_to_utf8(string,-1,nullptr,nullptr,nullptr)), + unit(unit) +{ + current_token.type = TOKEN_END; + + // Preload symbol + parseNextToken(); +} + +/** + * Evaluates the given arithmetic expression, along with an optional dimension + * analysis, and basic unit conversions. + * + * All units conversions factors are relative to some implicit + * base-unit. This is also the unit of the returned value. + * + * Returns: An EvaluatorQuantity with a value given in the base unit along with + * the order of the dimension (e.g. if the base unit is inches, a dimension + * order of two means in^2). + * + * @return Result of evaluation. + * @throws Inkscape::Util::EvaluatorException There was a parse error. + **/ +EvaluatorQuantity ExpressionEvaluator::evaluate() +{ + if (!g_utf8_validate(string, -1, nullptr)) { + throw EvaluatorException("Invalid UTF8 string", nullptr); + } + + EvaluatorQuantity result = EvaluatorQuantity(); + EvaluatorQuantity default_unit_factor; + + // Empty expression evaluates to 0 + if (acceptToken(TOKEN_END, nullptr)) { + return result; + } + + result = evaluateExpression(); + + // There should be nothing left to parse by now + isExpected(TOKEN_END, nullptr); + + resolveUnit(nullptr, &default_unit_factor, unit); + + // Entire expression is dimensionless, apply default unit if applicable + if ( result.dimension == 0 && default_unit_factor.dimension != 0 ) { + result.value /= default_unit_factor.value; + result.dimension = default_unit_factor.dimension; + } + return result; +} + +EvaluatorQuantity ExpressionEvaluator::evaluateExpression() +{ + bool subtract; + EvaluatorQuantity evaluated_terms; + + evaluated_terms = evaluateTerm(); + + // Continue evaluating terms, chained with + or -. + for (subtract = FALSE; + acceptToken('+', nullptr) || (subtract = acceptToken('-', nullptr)); + subtract = FALSE) + { + EvaluatorQuantity new_term = evaluateTerm(); + + // If dimensions mismatch, attempt default unit assignment + if ( new_term.dimension != evaluated_terms.dimension ) { + EvaluatorQuantity default_unit_factor; + + resolveUnit(nullptr, &default_unit_factor, unit); + + if ( new_term.dimension == 0 + && evaluated_terms.dimension == default_unit_factor.dimension ) + { + new_term.value /= default_unit_factor.value; + new_term.dimension = default_unit_factor.dimension; + } else if ( evaluated_terms.dimension == 0 + && new_term.dimension == default_unit_factor.dimension ) + { + evaluated_terms.value /= default_unit_factor.value; + evaluated_terms.dimension = default_unit_factor.dimension; + } else { + throwError("Dimension mismatch during addition"); + } + } + + evaluated_terms.value += (subtract ? -new_term.value : new_term.value); + } + + return evaluated_terms; +} + +EvaluatorQuantity ExpressionEvaluator::evaluateTerm() +{ + bool division; + EvaluatorQuantity evaluated_exp_terms = evaluateExpTerm(); + + for ( division = false; + acceptToken('*', nullptr) || (division = acceptToken('/', nullptr)); + division = false ) + { + EvaluatorQuantity new_exp_term = evaluateExpTerm(); + + if (division) { + evaluated_exp_terms.value /= new_exp_term.value; + evaluated_exp_terms.dimension -= new_exp_term.dimension; + } else { + evaluated_exp_terms.value *= new_exp_term.value; + evaluated_exp_terms.dimension += new_exp_term.dimension; + } + } + + return evaluated_exp_terms; +} + +EvaluatorQuantity ExpressionEvaluator::evaluateExpTerm() +{ + EvaluatorQuantity evaluated_signed_factors = evaluateSignedFactor(); + + while(acceptToken('^', nullptr)) { + EvaluatorQuantity new_signed_factor = evaluateSignedFactor(); + + if (new_signed_factor.dimension == 0) { + evaluated_signed_factors.value = pow(evaluated_signed_factors.value, + new_signed_factor.value); + evaluated_signed_factors.dimension *= new_signed_factor.value; + } else { + throwError("Unit in exponent"); + } + } + + return evaluated_signed_factors; +} + +EvaluatorQuantity ExpressionEvaluator::evaluateSignedFactor() +{ + EvaluatorQuantity result; + bool negate = FALSE; + + if (!acceptToken('+', nullptr)) { + negate = acceptToken ('-', nullptr); + } + + result = evaluateFactor(); + + if (negate) { + result.value = -result.value; + } + + return result; +} + +EvaluatorQuantity ExpressionEvaluator::evaluateFactor() +{ + EvaluatorQuantity evaluated_factor = EvaluatorQuantity(); + EvaluatorToken consumed_token = EvaluatorToken(); + + if (acceptToken(TOKEN_END, &consumed_token)) { + return evaluated_factor; + } + else if (acceptToken(TOKEN_NUM, &consumed_token)) { + evaluated_factor.value = consumed_token.value.fl; + } else if (acceptToken('(', nullptr)) { + evaluated_factor = evaluateExpression(); + isExpected(')', nullptr); + } else { + throwError("Expected number or '('"); + } + + if ( current_token.type == TOKEN_IDENTIFIER ) { + char *identifier; + EvaluatorQuantity result; + + acceptToken(TOKEN_ANY, &consumed_token); + + identifier = g_newa(char, consumed_token.value.size + 1); + + strncpy(identifier, consumed_token.value.c, consumed_token.value.size); + identifier[consumed_token.value.size] = '\0'; + + if (resolveUnit(identifier, &result, unit)) { + evaluated_factor.value /= result.value; + evaluated_factor.dimension += result.dimension; + } else { + throwError("Unit was not resolved"); + } + } + + return evaluated_factor; +} + +bool ExpressionEvaluator::acceptToken(TokenType token_type, + EvaluatorToken *consumed_token) +{ + bool existed = FALSE; + + if ( token_type == current_token.type || token_type == TOKEN_ANY ) { + existed = TRUE; + + if (consumed_token) { + *consumed_token = current_token; + } + + // Parse next token + parseNextToken(); + } + + return existed; +} + +void ExpressionEvaluator::parseNextToken() +{ + const char *s; + + movePastWhiteSpace(); + s = string; + start_of_current_token = s; + + if ( !s || s[0] == '\0' ) { + // We're all done + current_token.type = TOKEN_END; + } else if ( s[0] == '+' || s[0] == '-' ) { + // Snatch these before the g_strtod() does, otherwise they might + // be used in a numeric conversion. + acceptTokenCount(1, s[0]); + } else { + // Attempt to parse a numeric value + char *endptr = nullptr; + gdouble value = g_strtod(s, &endptr); + + if ( endptr && endptr != s ) { + // A numeric could be parsed, use it + current_token.value.fl = value; + + current_token.type = TOKEN_NUM; + string = endptr; + } else if (isUnitIdentifierStart(s[0])) { + // Unit identifier + current_token.value.c = s; + current_token.value.size = getIdentifierSize(s, 0); + + acceptTokenCount(current_token.value.size, TOKEN_IDENTIFIER); + } else { + // Everything else is a single character token + acceptTokenCount(1, s[0]); + } + } +} + +void ExpressionEvaluator::acceptTokenCount (int count, TokenType token_type) +{ + current_token.type = token_type; + string += count; +} + +void ExpressionEvaluator::isExpected(TokenType token_type, + EvaluatorToken *value) +{ + if (!acceptToken(token_type, value)) { + throwError("Unexpected token"); + } +} + +void ExpressionEvaluator::movePastWhiteSpace() +{ + if (!string) { + return; + } + + while (g_ascii_isspace(*string)) { + string++; + } +} + +bool ExpressionEvaluator::isUnitIdentifierStart(gunichar c) +{ + return (g_unichar_isalpha (c) + || c == (gunichar) '%' + || c == (gunichar) '\''); +} + +/** + * getIdentifierSize: + * @s: + * @start: + * + * Returns: Size of identifier in bytes (not including NULL + * terminator). + **/ +int ExpressionEvaluator::getIdentifierSize(const char *string, int start_offset) +{ + const char *start = g_utf8_offset_to_pointer(string, start_offset); + const char *s = start; + gunichar c = g_utf8_get_char(s); + int length = 0; + + if (isUnitIdentifierStart(c)) { + s = g_utf8_next_char (s); + c = g_utf8_get_char (s); + length++; + + while ( isUnitIdentifierStart (c) || g_unichar_isdigit (c) ) { + s = g_utf8_next_char(s); + c = g_utf8_get_char(s); + length++; + } + } + + return g_utf8_offset_to_pointer(start, length) - start; +} + +bool ExpressionEvaluator::resolveUnit (const char* identifier, + EvaluatorQuantity *result, + Unit const* unit) +{ + if (!unit) { + result->value = 1; + result->dimension = 1; + return true; + }else if (!identifier) { + result->value = 1; + result->dimension = unit->isAbsolute() ? 1 : 0; + return true; + } else if (unit_table.hasUnit(identifier)) { + Unit const *identifier_unit = unit_table.getUnit(identifier); + result->value = Quantity::convert(1, unit, identifier_unit); + result->dimension = identifier_unit->isAbsolute() ? 1 : 0; + return true; + } else { + return false; + } +} + +void ExpressionEvaluator::throwError(const char *msg) +{ + throw EvaluatorException(msg, start_of_current_token); +} + +} // namespace Util +} // namespace Inkscape diff --git a/src/util/expression-evaluator.h b/src/util/expression-evaluator.h new file mode 100644 index 0000000..a45ad5a --- /dev/null +++ b/src/util/expression-evaluator.h @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +/** @file + * TODO: insert short description here + */ +/* LIBGIMP - The GIMP Library + * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball + * + * Original file from libgimpwidgets: gimpeevl.h + * Copyright (C) 2008-2009 Fredrik Alstromer <roe@excu.se> + * Copyright (C) 2008-2009 Martin Nordholts <martinn@svn.gnome.org> + * Modified for Inkscape by Johan Engelen + * Copyright (C) 2011 Johan Engelen + * Copyright (C) 2013 Matthew Petroff + * + * This library is free software: you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see + * <http://www.gnu.org/licenses/>. + */ + +#ifndef INKSCAPE_UTIL_EXPRESSION_EVALUATOR_H +#define INKSCAPE_UTIL_EXPRESSION_EVALUATOR_H + +#include "util/units.h" + +#include <exception> +#include <sstream> +#include <string> + +/** + * @file + * Expression evaluator: A straightforward recursive + * descent parser, no fuss, no new dependencies. The lexer is hand + * coded, tedious, not extremely fast but works. It evaluates the + * expression as it goes along, and does not create a parse tree or + * anything, and will not optimize anything. It uses doubles for + * precision, with the given use case, that's enough to combat any + * rounding errors (as opposed to optimizing the evaluation). + * + * It relies on external unit resolving through a callback and does + * elementary dimensionality constraint check (e.g. "2 mm + 3 px * 4 + * in" is an error, as L + L^2 is a mismatch). It uses g_strtod() for numeric + * conversions and it's non-destructive in terms of the parameters, and + * it's reentrant. + * + * EBNF: + * + * expression ::= term { ('+' | '-') term }* | + * <empty string> ; + * + * term ::= exponent { ( '*' | '/' ) exponent }* ; + * + * exponent ::= signed factor { '^' signed factor }* ; + * + * signed factor ::= ( '+' | '-' )? factor ; + * + * unit factor ::= factor unit? ; + * + * factor ::= number | '(' expression ')' ; + * + * number ::= ? what g_strtod() consumes ? ; + * + * unit ::= ? what not g_strtod() consumes and not whitespace ? ; + * + * The code should match the EBNF rather closely (except for the + * non-terminal unit factor, which is inlined into factor) for + * maintainability reasons. + * + * It will allow 1++1 and 1+-1 (resulting in 2 and 0, respectively), + * but I figured one might want that, and I don't think it's going to + * throw anyone off. + */ + +namespace Inkscape { +namespace Util { + +class Unit; + +/** + * EvaluatorQuantity: + * @param value In reference units. + * @param dimension mm has a dimension of 1, mm^2 has a dimension of 2, etc. + */ +class EvaluatorQuantity +{ +public: + EvaluatorQuantity(double value = 0, unsigned int dimension = 0); + + double value; + unsigned int dimension; +}; + +/** + * TokenType + */ +enum { + TOKEN_NUM = 30000, + TOKEN_IDENTIFIER = 30001, + TOKEN_ANY = 40000, + TOKEN_END = 50000 +}; +typedef int TokenType; + +/** + * EvaluatorToken + */ +class EvaluatorToken +{ +public: + EvaluatorToken(); + + TokenType type; + + union { + double fl; + struct { + const char *c; + int size; + }; + } value; +}; + +/** + * ExpressionEvaluator + * @param string NULL terminated input string to evaluate + * @param unit Unit output should be in + */ +class ExpressionEvaluator +{ +public: + ExpressionEvaluator(const char *string, Unit const *unit = nullptr); + + EvaluatorQuantity evaluate(); + +private: + const char *string; + Unit const *unit; + + EvaluatorToken current_token; + const char *start_of_current_token; + + EvaluatorQuantity evaluateExpression(); + EvaluatorQuantity evaluateTerm(); + EvaluatorQuantity evaluateExpTerm(); + EvaluatorQuantity evaluateSignedFactor(); + EvaluatorQuantity evaluateFactor(); + + bool acceptToken(TokenType token_type, EvaluatorToken *consumed_token); + void parseNextToken(); + void acceptTokenCount(int count, TokenType token_type); + void isExpected(TokenType token_type, EvaluatorToken *value); + + void movePastWhiteSpace(); + + static bool isUnitIdentifierStart(gunichar c); + static int getIdentifierSize(const char *s, int start); + + static bool resolveUnit(const char *identifier, EvaluatorQuantity *result, Unit const *unit); + + void throwError(const char *msg); +}; + +/** + * Special exception class for the expression evaluator. + */ +class EvaluatorException : public std::exception { +public: + EvaluatorException(const char *message, const char *at_position) { + std::ostringstream os; + const char *token = at_position ? at_position : "<End of input>"; + os << "Expression evaluator error: " << message << " at '" << token << "'"; + msgstr = os.str(); + } + + ~EvaluatorException() noexcept override = default; // necessary to destroy the string object!!! + + const char *what() const noexcept override { + return msgstr.c_str(); + } +protected: + std::string msgstr; +}; + +} +} + +#endif // INKSCAPE_UTIL_EXPRESSION_EVALUATOR_H diff --git a/src/util/find-if-before.h b/src/util/find-if-before.h new file mode 100644 index 0000000..6d15fdf --- /dev/null +++ b/src/util/find-if-before.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Algorithms::find_if_before - finds the position before + * the first value that satisfies + * the predicate + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2005 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ALGORITHMS_FIND_IF_BEFORE_H +#define SEEN_INKSCAPE_ALGORITHMS_FIND_IF_BEFORE_H + +#include <algorithm> + +namespace Inkscape { + +namespace Algorithms { + +template <typename ForwardIterator, typename UnaryPredicate> +inline ForwardIterator find_if_before(ForwardIterator start, + ForwardIterator end, + UnaryPredicate pred) +{ + ForwardIterator before=end; + while ( start != end && !pred(*start) ) { + before = start; + ++start; + } + return ( start != end ) ? before : end; +} + +} + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/util/find-last-if.h b/src/util/find-last-if.h new file mode 100644 index 0000000..1402de7 --- /dev/null +++ b/src/util/find-last-if.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Algorithms::find_last_if + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ALGORITHMS_FIND_LAST_IF_H +#define SEEN_INKSCAPE_ALGORITHMS_FIND_LAST_IF_H + +#include <algorithm> + +namespace Inkscape { + +namespace Algorithms { + +template <typename ForwardIterator, typename UnaryPredicate> +inline ForwardIterator find_last_if(ForwardIterator start, ForwardIterator end, + UnaryPredicate pred) +{ + ForwardIterator last_found(end); + while ( start != end ) { + start = std::find_if(start, end, pred); + if ( start != end ) { + last_found = start; + ++start; + } + } + return last_found; +} + +} + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/util/fixed_point.h b/src/util/fixed_point.h new file mode 100644 index 0000000..5bb0dde --- /dev/null +++ b/src/util/fixed_point.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::FixedPoint - fixed point type + * + * Authors: + * Jasper van de Gronde <th.v.d.gronde@hccnet.net> + * + * Copyright (C) 2006 Jasper van de Gronde + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_FIXED_POINT_H +#define SEEN_INKSCAPE_UTIL_FIXED_POINT_H + +#include "util/reference.h" +#include <cmath> +#include <algorithm> +#include <limits> + +namespace Inkscape { + +namespace Util { + +template <typename T, unsigned int precision> +class FixedPoint { +public: + FixedPoint() = default; + FixedPoint(const FixedPoint& value) : v(value.v) {} + FixedPoint(char value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(unsigned char value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(short value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(unsigned short value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(int value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(unsigned int value) : v(static_cast<T>(value)<<precision) {} + FixedPoint(double value) : v(static_cast<T>(floor(value*(1<<precision)))) {} + + FixedPoint& operator+=(FixedPoint val) { v += val.v; return *this; } + FixedPoint& operator-=(FixedPoint val) { v -= val.v; return *this; } + FixedPoint& operator*=(FixedPoint val) { + const unsigned int half_size = 8*sizeof(T)/2; + const T al = v&((1<<half_size)-1), bl = val.v&((1<<half_size)-1); + const T ah = v>>half_size, bh = val.v>>half_size; + v = static_cast<unsigned int>(al*bl)>>precision; + if ( half_size >= precision ) { + v += ((al*bh)+(ah*bl)+((ah*bh)<<half_size))<<(half_size-precision); + } else { + v += ((al*bh)+(ah*bl))>>(precision-half_size); + v += (ah*bh)<<(2*half_size-precision); + } + return *this; + } + + FixedPoint& operator*=(char val) { v *= val; return *this; } + FixedPoint& operator*=(unsigned char val) { v *= val; return *this; } + FixedPoint& operator*=(short val) { v *= val; return *this; } + FixedPoint& operator*=(unsigned short val) { v *= val; return *this; } + FixedPoint& operator*=(int val) { v *= val; return *this; } + FixedPoint& operator*=(unsigned int val) { v *= val; return *this; } + + FixedPoint operator+(FixedPoint val) const { FixedPoint r(*this); return r+=val; } + FixedPoint operator-(FixedPoint val) const { FixedPoint r(*this); return r-=val; } + FixedPoint operator*(FixedPoint val) const { FixedPoint r(*this); return r*=val; } + + FixedPoint operator*(char val) const { FixedPoint r(*this); return r*=val; } + FixedPoint operator*(unsigned char val) const { FixedPoint r(*this); return r*=val; } + FixedPoint operator*(short val) const { FixedPoint r(*this); return r*=val; } + FixedPoint operator*(unsigned short val) const { FixedPoint r(*this); return r*=val; } + FixedPoint operator*(int val) const { FixedPoint r(*this); return r*=val; } + FixedPoint operator*(unsigned int val) const { FixedPoint r(*this); return r*=val; } + + float operator*(float val) const { return static_cast<float>(*this)*val; } + double operator*(double val) const { return static_cast<double>(*this)*val; } + + operator char() const { return v>>precision; } + operator unsigned char() const { return v>>precision; } + operator short() const { return v>>precision; } + operator unsigned short() const { return v>>precision; } + operator int() const { return v>>precision; } + operator unsigned int() const { return v>>precision; } + + operator float() const { return ldexpf(v,-precision); } + operator double() const { return ldexp(v,-precision); } +private: + T v; +}; + +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(char a, FixedPoint<T,precision> b) { return b*=a; } +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(unsigned char a, FixedPoint<T,precision> b) { return b*=a; } +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(short a, FixedPoint<T,precision> b) { return b*=a; } +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(unsigned short a, FixedPoint<T,precision> b) { return b*=a; } +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(int a, FixedPoint<T,precision> b) { return b*=a; } +template<typename T, unsigned int precision> FixedPoint<T,precision> operator *(unsigned int a, FixedPoint<T,precision> b) { return b*=a; } + +template<typename T, unsigned int precision> float operator *(float a, FixedPoint<T,precision> b) { return b*a; } +template<typename T, unsigned int precision> double operator *(double a, FixedPoint<T,precision> b) { return b*a; } + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/format.h b/src/util/format.h new file mode 100644 index 0000000..b38f3a6 --- /dev/null +++ b/src/util/format.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::format - g_strdup_printf wrapper producing shared strings + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_FORMAT_H +#define SEEN_INKSCAPE_UTIL_FORMAT_H + +#include <cstdarg> +#include <glib.h> +#include "util/share.h" + +namespace Inkscape { + +namespace Util { + +inline ptr_shared vformat(char const *format, va_list args) { + char *temp=g_strdup_vprintf(format, args); + ptr_shared result=share_string(temp); + g_free(temp); + return result; +} + + // needed since G_GNUC_PRINTF can only be used on a declaration + ptr_shared format(char const *format, ...) G_GNUC_PRINTF(1,2); +inline ptr_shared format(char const *format, ...) { + va_list args; + + va_start(args, format); + ptr_shared result=vformat(format, args); + va_end(args); + + return result; +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/forward-pointer-iterator.h b/src/util/forward-pointer-iterator.h new file mode 100644 index 0000000..9fe3bb8 --- /dev/null +++ b/src/util/forward-pointer-iterator.h @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::ForwardPointerIterator - wraps a simple pointer + * with various strategies + * to determine sequence + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_FORWARD_POINTER_ITERATOR_H +#define SEEN_INKSCAPE_UTIL_FORWARD_POINTER_ITERATOR_H + +#include <iterator> +#include <cstddef> +#include "util/reference.h" + +namespace Inkscape { + +namespace Util { + +template <typename BaseType, typename Strategy> +class ForwardPointerIterator; + +template <typename BaseType, typename Strategy> +class ForwardPointerIterator<BaseType const, Strategy> { +public: + typedef std::forward_iterator_tag iterator_category; + typedef typename Traits::Reference<BaseType const>::LValue value_type; + typedef std::ptrdiff_t difference_type; + typedef typename Traits::Reference<BaseType const>::LValue reference; + typedef typename Traits::Reference<BaseType const>::RValue const_reference; + typedef typename Traits::Reference<BaseType const>::Pointer pointer; + + typedef ForwardPointerIterator<BaseType const, Strategy> Self; + + ForwardPointerIterator() = default; + ForwardPointerIterator(pointer p) : _p(p) {} + + operator pointer() const { return _p; } + reference operator*() const { return *_p; } + pointer operator->() const { return _p; } + + bool operator==(Self const &other) const { + return _p == other._p; + } + bool operator!=(Self const &other) const { + return _p != other._p; + } + + Self &operator++() { + _p = Strategy::next(_p); + return *this; + } + Self operator++(int) { + Self old(*this); + operator++(); + return old; + } + + operator bool() const { return _p != nullptr; } + +private: + pointer _p; +}; + +template <typename BaseType, typename Strategy> +class ForwardPointerIterator +: public ForwardPointerIterator<BaseType const, Strategy> +{ +public: + typedef typename Traits::Reference<BaseType>::LValue value_type; + typedef typename Traits::Reference<BaseType>::LValue reference; + typedef typename Traits::Reference<BaseType>::RValue const_reference; + typedef typename Traits::Reference<BaseType>::Pointer pointer; + + typedef ForwardPointerIterator<BaseType const, Strategy> Ancestor; + typedef ForwardPointerIterator<BaseType, Strategy> Self; + + ForwardPointerIterator() : Ancestor() {} + ForwardPointerIterator(pointer p) : Ancestor(p) {} + + operator pointer() const { + return const_cast<pointer>(Ancestor::operator->()); + } + reference operator*() const { + return const_cast<reference>(Ancestor::operator*()); + } + pointer operator->() const { + return const_cast<pointer>(Ancestor::operator->()); + } + + Self &operator++() { + Ancestor::operator++(); + return *this; + } + Self operator++(int) { + Self old(*this); + operator++(); + return old; + } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/list-container-test.h b/src/util/list-container-test.h new file mode 100644 index 0000000..b46ed7b --- /dev/null +++ b/src/util/list-container-test.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> + +#include <stdarg.h> +#include "util/list-container.h" + +#define ARRAY_RANGE(array) (array), (array)+sizeof((array))/sizeof((array)[0]) + +static bool check_values(Inkscape::Util::ListContainer<int> const &c, unsigned n_values, ...) { + bool ret = true; + va_list args; + va_start(args, n_values); + Inkscape::Util::ListContainer<int>::const_iterator iter(c.begin()); + while ( n_values && iter != c.end() ) { + int const value = va_arg(args, int); + if ( value != *iter ) { + ret = false; + } + if ( n_values == 1 && &c.back() != &*iter ) { + ret = false; + } + n_values--; + ++iter; + } + va_end(args); + return ret && n_values == 0 && iter == c.end(); +} + +class ListContainerTest : public CxxTest::TestSuite { +public: + ListContainerTest() + { + Inkscape::GC::init(); + } + virtual ~ListContainerTest() {} + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static ListContainerTest *createSuite() { return new ListContainerTest(); } + static void destroySuite( ListContainerTest *suite ) { delete suite; } + + void testRangeConstructor() + { + int const values[]={1,2,3,4}; + int const * const values_end=values+4; + Inkscape::Util::ListContainer<int> container(values, values_end); + + Inkscape::Util::ListContainer<int>::iterator container_iter=container.begin(); + int const * values_iter=values; + + while ( values_iter != values_end && container_iter != container.end() ) { + TS_ASSERT_EQUALS(*values_iter , *container_iter); + ++values_iter; + ++container_iter; + } + + TS_ASSERT_EQUALS(values_iter , values_end); + TS_ASSERT_EQUALS(container_iter , container.end()); + } + + void testEqualityTests() + { + int const a[] = { 1, 2, 3, 4 }; + int const b[] = { 1, 2, 3, 4 }; + int const c[] = { 1, 2, 3 }; + int const d[] = { 1, 2, 3, 5 }; + Inkscape::Util::ListContainer<int> c_a(ARRAY_RANGE(a)); + Inkscape::Util::ListContainer<int> c_b(ARRAY_RANGE(b)); + Inkscape::Util::ListContainer<int> c_c(ARRAY_RANGE(c)); + Inkscape::Util::ListContainer<int> c_d(ARRAY_RANGE(d)); + + TS_ASSERT(c_a == c_b); + TS_ASSERT(!( c_a != c_b )); + TS_ASSERT(!( c_a == c_c )); + TS_ASSERT(c_a != c_c); + TS_ASSERT(!( c_a == c_d )); + TS_ASSERT(c_a != c_d); + } + + void testLessThan() + { + int const a[] = { 1, 2, 3, 4 }; + int const b[] = { 1, 2, 2, 4 }; + int const c[] = { 1, 2, 4, 4 }; + int const d[] = { 1, 2, 3 }; + Inkscape::Util::ListContainer<int> c_a(ARRAY_RANGE(a)); + Inkscape::Util::ListContainer<int> c_b(ARRAY_RANGE(b)); + Inkscape::Util::ListContainer<int> c_c(ARRAY_RANGE(c)); + Inkscape::Util::ListContainer<int> c_d(ARRAY_RANGE(d)); + TS_ASSERT(c_a >= c_b); + TS_ASSERT(!( c_a < c_b )); + TS_ASSERT(!( c_a >= c_c )); + TS_ASSERT(c_a < c_c); + TS_ASSERT(!( c_a < c_d )); + TS_ASSERT(c_a >= c_d); + TS_ASSERT(c_d < c_a); + } + + void testAssignmentOperator() + { + int const a[] = { 1, 2, 3, 4 }; + Inkscape::Util::ListContainer<int> c_a(ARRAY_RANGE(a)); + Inkscape::Util::ListContainer<int> c_c; + TS_ASSERT(c_a != c_c); + c_c = c_a; + TS_ASSERT(c_a == c_c); + c_c = c_a; + TS_ASSERT(c_a == c_c); + } + + void testFillConstructor() + { + Inkscape::Util::ListContainer<int> filled((std::size_t)3, 2); + TS_ASSERT(check_values(filled, 3, 2, 2, 2)); + } + + void testContainerSize() + { + // max_size() and size() return ListContainer<>::size_type which is unsigned int + Inkscape::Util::ListContainer<int> empty; + TS_ASSERT(empty.empty()); + TS_ASSERT_EQUALS(empty.size(), 0u); + int const a[] = { 1, 2, 3 }; + Inkscape::Util::ListContainer<int> c_a(ARRAY_RANGE(a)); + TS_ASSERT(!c_a.empty()); + TS_ASSERT_EQUALS(c_a.size(), 3u); + + TS_ASSERT_LESS_THAN(0u, empty.max_size()); + } + + void testAppending() + { + Inkscape::Util::ListContainer<int> c; + c.push_back(1); + TS_ASSERT(check_values(c, 1, 1)); + c.push_back(2); + TS_ASSERT(check_values(c, 2, 1, 2)); + c.push_back(3); + TS_ASSERT(check_values(c, 3, 1, 2, 3)); + } + + void testBulkAppending() + { + int const a[] = { 1, 2, 3, 4 }; + int const b[] = { 5, 6, 7 }; + Inkscape::Util::ListContainer<int> c_a(ARRAY_RANGE(a)); + Inkscape::Util::ListContainer<int> c_b(ARRAY_RANGE(b)); + c_a.insert(c_a.end(), c_b.begin(), c_b.end()); + TS_ASSERT(check_values(c_a, 7, 1, 2, 3, 4, 5, 6, 7)); + } + + void testPrepending() + { + Inkscape::Util::ListContainer<int> c; + c.push_front(1); + TS_ASSERT(check_values(c, 1, 1)); + c.push_front(2); + TS_ASSERT(check_values(c, 2, 2, 1)); + c.push_front(3); + TS_ASSERT(check_values(c, 3, 3, 2, 1)); + } + + void testSingleValueInsertion() + { + Inkscape::Util::ListContainer<int> c; + + c.insert(c.begin(), 1); + TS_ASSERT(check_values(c, 1, 1)); + + c.insert(c.end(), 2); + TS_ASSERT(check_values(c, 2, 1, 2)); + + c.insert(c.begin(), 3); + TS_ASSERT(check_values(c, 3, 3, 1, 2)); + + Inkscape::Util::ListContainer<int>::iterator pos=c.begin(); + ++pos; + c.insert(pos, 4); + TS_ASSERT(check_values(c, 4, 3, 4, 1, 2)); + } + + void testSingleValueErasure() + { + int const values[] = { 1, 2, 3, 4 }; + Inkscape::Util::ListContainer<int> c(ARRAY_RANGE(values)); + + c.erase(c.begin()); + TS_ASSERT(check_values(c, 3, 2, 3, 4)); + + Inkscape::Util::ListContainer<int>::iterator pos=c.begin(); + ++pos; + c.erase(pos); + TS_ASSERT(check_values(c, 2, 2, 4)); + + pos=c.begin(); + ++pos; + c.erase(pos); + TS_ASSERT(check_values(c, 1, 2)); + + c.erase(c.begin()); + TS_ASSERT(check_values(c, 0)); + } + + void testPopFront() + { + int const full_ary[] = { 1, 2, 3 }; + Inkscape::Util::ListContainer<int> t(ARRAY_RANGE(full_ary)); + TS_ASSERT(check_values(t, 3, 1, 2, 3)); + TS_ASSERT_EQUALS(t.back() , 3); + t.pop_front(); + TS_ASSERT(check_values(t, 2, 2, 3)); + TS_ASSERT_EQUALS(t.back() , 3); + t.push_back(23); + TS_ASSERT(check_values(t, 3, 2, 3, 23)); + TS_ASSERT_EQUALS(t.back() , 23); + t.pop_front(); + TS_ASSERT(check_values(t, 2, 3, 23)); + TS_ASSERT_EQUALS(t.back() , 23); + t.pop_front(); + TS_ASSERT(check_values(t, 1, 23)); + TS_ASSERT_EQUALS(t.back() , 23); + t.pop_front(); + TS_ASSERT(check_values(t, 0)); + t.push_back(42); + TS_ASSERT(check_values(t, 1, 42)); + TS_ASSERT_EQUALS(t.back() , 42); + } + + void testEraseAfter() + { + int const full_ary[] = { 1, 2, 3, 4 }; + int const exp_ary[] = { 1, 3, 4 }; + Inkscape::Util::ListContainer<int> full_list(ARRAY_RANGE(full_ary)); + Inkscape::Util::ListContainer<int> exp_list(ARRAY_RANGE(exp_ary)); + TS_ASSERT(full_list != exp_list); + full_list.erase_after(full_list.begin()); + TS_ASSERT(full_list == exp_list); + } +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/list-container.h b/src/util/list-container.h new file mode 100644 index 0000000..7002387 --- /dev/null +++ b/src/util/list-container.h @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::ListContainer - encapsulates lists as STL containers, + * providing fast appending + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2005 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_LIST_CONTAINER_H +#define SEEN_INKSCAPE_UTIL_LIST_CONTAINER_H + +#include <limits> +#include "util/list.h" + +namespace Inkscape { + +namespace Util { + +template <typename T> +class ListContainer { +public: + /* default constructible */ + ListContainer() = default; + + /* assignable */ + ListContainer(ListContainer const &other) { + *this = other; + } + ListContainer &operator=(ListContainer const &other) { + clear(); + for ( const_iterator iter = other.begin() ; iter ; ++iter ) { + push_back(*iter); + } + return *this; + } + void swap(ListContainer<T> &other) { + std::swap(other._head, _head); + std::swap(other._tail, _tail); + } + + /* equality comparable */ + bool operator==(ListContainer const &other) const { + const_iterator iter = _head; + const_iterator other_iter = other._head; + while ( iter && other_iter ) { + if (!( *iter == *other_iter )) { + return false; + } + ++iter; + ++other_iter; + } + return !iter && !other_iter; + } + bool operator!=(ListContainer const &other) const { + return !operator==(other); + } + + /* lessthan comparable */ + bool operator<(ListContainer const &other) const { + const_iterator iter = _head; + const_iterator other_iter = other._head; + while ( iter && other_iter ) { + if ( *iter < *other_iter ) { + return true; + } else if ( *other_iter < *iter ) { + return false; + } + ++iter; + ++other_iter; + } + return bool(other_iter); + } + bool operator>=(ListContainer const &other) const { + return !operator<(other); + } + + /* container */ + typedef typename List<T>::value_type value_type; + typedef List<T> iterator; + typedef List<T const> const_iterator; + typedef typename List<T>::reference reference; + typedef typename List<T>::const_reference const_reference; + typedef typename List<T>::pointer pointer; + typedef typename List<T>::difference_type difference_type; + typedef std::size_t size_type; + + iterator begin() { return _head; } + const_iterator begin() const { return _head; } + iterator end() { return iterator(); } + const_iterator end() const { return const_iterator(); } + size_type size() const { + size_type size=0; + for ( iterator iter = _head ; iter ; ++iter ) { + size++; + } + return size; + } + size_type max_size() const { + return std::numeric_limits<std::size_t>::max(); + } + bool empty() const { return !_head; } + + /* sequence */ + ListContainer(size_type count, const_reference value) { + for ( ; count ; --count ) { + push_back(value); + } + } + ListContainer(size_type count) { + value_type default_value; + for ( ; count ; --count ) { + push_back(default_value); + } + } + template <typename ForwardIterator> + ListContainer(ForwardIterator i, ForwardIterator j) { + for ( ; i != j ; ++i ) { + push_back(*i); + } + } + + reference front() { return *_head; } + const_reference front() const { return *_head; } + + iterator insert(const_iterator position, const_reference value) { + if (position) { + if ( position != _head ) { + MutableList<T> added(value); + MutableList<T> before=_before(position); + set_rest(added, rest(before)); + set_rest(before, added); + return added; + } else { + push_front(value); + return _head; + } + } else { + push_back(value); + return _tail; + } + } + void insert(const_iterator position, size_type count, const_reference value) + { + _insert_from_temp(position, ListContainer(count, value)); + } + template <typename ForwardIterator> + void insert(const_iterator position, ForwardIterator i, ForwardIterator j) { + _insert_from_temp(position, ListContainer(i, j)); + } + void erase(const_iterator position) { + erase(position, rest(position)); + } + void erase(const_iterator i, const_iterator j) { + if ( i == _head ) { + _head = static_cast<MutableList<T> &>(j); + if ( !j || !rest(j) ) { + _tail = _head; + } + } else { + MutableList<T> before=_before(i); + if (j) { + set_rest(before, static_cast<MutableList<T> &>(j)); + } else { + set_rest(before, MutableList<T>()); + _tail = before; + } + } + } + void clear() { + _head = _tail = MutableList<T>(); + } + void resize(size_type size, const_reference fill) { + MutableList<T> before; + MutableList<T> iter; + for ( iter = _head ; iter && size ; ++iter ) { + before = iter; + size--; + } + if (size) { + ListContainer temp(size, fill); + if (empty()) { + _head = temp._head; + _tail = temp._tail; + } else { + set_rest(_tail, temp._head); + _tail = temp._tail; + } + } else if (iter) { + if (before) { + set_rest(before, MutableList<T>()); + _tail = before; + } else { + _head = _tail = MutableList<T>(); + } + } + } + void resize(size_type size) { + resize(size, value_type()); + } + + /* front insertion sequence */ + void push_front(const_reference value) { + if (_head) { + _head = cons(value, _head); + } else { + _head = _tail = MutableList<T>(value); + } + } + void pop_front() { + if (_head) { + _head = rest(_head); + if (!_head) { + _tail = _head; + } + } + } + + /* back insertion sequence */ + reference back() { return *_tail; } + const_reference back() const { return *_tail; } + void push_back(const_reference value) { + if (_tail) { + MutableList<T> added(value); + set_rest(_tail, added); + _tail = added; + } else { + _head = _tail = MutableList<T>(value); + } + } + // we're not required to provide pop_back if we can't + // implement it efficiently + + /* additional */ + MutableList<T> detatchList() { + MutableList<T> list=_head; + _head = _tail = MutableList<T>(); + return list; + } + iterator insert_after(const_iterator pos, const_reference value) { + MutableList<T> added(value); + if (pos) { + MutableList<T> before=static_cast<MutableList<T> &>(pos); + set_rest(added, rest(before)); + set_rest(before, added); + if ( _tail == before ) { + _tail = added; + } + } else { + push_front(value); + } + } + void insert_after(const_iterator position, size_type count, + const_reference value) + { + _insert_after_from_temp(position, ListContainer(count, value)); + } + template <typename ForwardIterator> + void insert_after(const_iterator position, + ForwardIterator i, ForwardIterator j) + { + _insert_after_from_temp(position, ListContainer(i, j)); + } + void erase_after(const_iterator position) { + if (!position) { + pop_front(); + } else { + MutableList<T> before=static_cast<MutableList<T> &>(position); + MutableList<T> removed=rest(before); + set_rest(before, rest(removed)); + if ( removed == _tail ) { + _tail = before; + } + } + } + +private: + MutableList<T> _head; + MutableList<T> _tail; + + MutableList<T> _before(const_iterator position) { + for ( MutableList<T> iter = _head ; iter ; ++iter ) { + if ( rest(iter) == position ) { + return iter; + } + } + return MutableList<T>(); + } + void _insert_from_temp(const_iterator pos, ListContainer const &temp) { + if (temp.empty()) { + return; + } + if (empty()) { /* if empty, just take the whole thing */ + _head = temp._head; + _tail = temp._tail; + } else if (pos) { + if ( pos == _head ) { /* prepend */ + set_rest(temp._tail, _head); + _head = temp._head; + } else { /* insert somewhere in the middle */ + MutableList<T> before=_before(pos); + set_rest(temp._tail, static_cast<MutableList<T> &>(pos)); + set_rest(before, temp._head); + } + } else { /* append */ + set_rest(_tail, temp._head); + _tail = temp._tail; + } + } + void _insert_after_from_temp(const_iterator pos, + ListContainer const &temp) + { + if (temp.empty()) { + return; + } + if (empty()) { /* if empty, just take the whole thing */ + _head = temp._head; + _tail = temp._tail; + } else if (pos) { + if ( pos == _tail ) { /* append */ + set_rest(_tail, temp._head); + _tail = temp._tail; + } else { /* insert somewhere in the middle */ + MutableList<T> before=static_cast<MutableList<T> &>(pos); + set_rest(temp._tail, rest(before)); + set_rest(before, temp._head); + } + } else { /* prepend */ + set_rest(temp._tail, _head); + _head = temp._head; + } + } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/list-copy.h b/src/util/list-copy.h new file mode 100644 index 0000000..51e6af4 --- /dev/null +++ b/src/util/list-copy.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Traits::ListCopy - helper traits class for copying lists + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_TRAITS_LIST_COPY_H +#define SEEN_INKSCAPE_TRAITS_LIST_COPY_H + +#include <iterator> +#include "util/copy.h" +#include "util/list.h" + +namespace Inkscape { + +namespace Traits { + +template <typename InputIterator> +struct ListCopy { + typedef typename Copy< + typename std::iterator_traits<InputIterator>::value_type + >::Type ResultValue; + typedef typename Util::MutableList<ResultValue> ResultList; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/list.h b/src/util/list.h new file mode 100644 index 0000000..68f410b --- /dev/null +++ b/src/util/list.h @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_LIST_H +#define SEEN_INKSCAPE_UTIL_LIST_H + +#include <cstddef> +#include <iterator> +#include "inkgc/gc-managed.h" +#include "util/reference.h" + +namespace Inkscape { + +namespace Util { + +/// Generic ListCell for Inkscape::Util::List. +template <typename T> +struct ListCell : public GC::Managed<> { + ListCell() = default; + ListCell(typename Traits::Reference<T>::RValue v, ListCell *n) + : value(v), next(n) {} + + T value; + ListCell *next; +}; + +template <typename T> class List; +template <typename T> class MutableList; + +template <typename T> +bool is_empty(List<T> const &list); + +template <typename T> +typename List<T>::reference first(List<T> const &list); + +template <typename T> +List<T> const &rest(List<T> const &list); + +template <typename T> +MutableList<T> &rest(MutableList<T> const &list); + +template <typename T> +MutableList<T> const &set_rest(MutableList<T> const &list, + MutableList<T> const &rest); + +/// Helper template. +template <typename T> +class List<T const> { +public: + typedef std::forward_iterator_tag iterator_category; + typedef T const value_type; + typedef std::ptrdiff_t difference_type; + typedef typename Traits::Reference<value_type>::LValue reference; + typedef typename Traits::Reference<value_type>::RValue const_reference; + typedef typename Traits::Reference<value_type>::Pointer pointer; + + List() : _cell(nullptr) {} + explicit List(const_reference value, List const &next=List()) + : _cell(new ListCell<T>(value, next._cell)) {} + + operator bool() const { return this->_cell; } + + reference operator*() const { return this->_cell->value; } + pointer operator->() const { return &this->_cell->value; } + + bool operator==(List const &other) const { + return this->_cell == other._cell; + } + bool operator!=(List const &other) const { + return this->_cell != other._cell; + } + + List &operator++() { + this->_cell = this->_cell->next; + return *this; + } + List operator++(int) { + List old(*this); + this->_cell = this->_cell->next; + return old; + } + + friend reference first<>(List const &); + friend List const &rest<>(List const &); + friend bool is_empty<>(List const &); + +protected: + ListCell<T> *_cell; +}; + +/** + * Generic linked list. + * + * These lists are designed to store simple values like pointers, + * references, and scalar values. While they can be used to directly + * store more complex objects, destructors for those objects will not + * be called unless those objects derive from Inkscape::GC::Finalized. + * + * In general it's better to use lists to store pointers or references + * to objects requiring finalization and manage object lifetimes separately. + * + * @see Inkscape::GC::Finalized + * + * cons() is synonymous with List<T>(first, rest), except that the + * compiler will usually be able to infer T from the type of \a rest. + * + * If you need to create an empty list (which can, for example, be used + * as an 'end' value with STL algorithms), call the List<> constructor + * with no arguments, like so: + * + * <code> List<int>() </code> + */ +template <typename T> +class List : public List<T const> { +public: + typedef T value_type; + typedef typename Traits::Reference<value_type>::LValue reference; + typedef typename Traits::Reference<value_type>::RValue const_reference; + typedef typename Traits::Reference<value_type>::Pointer pointer; + + List() : List<T const>() {} + explicit List(const_reference value, List const &next=List()) + : List<T const>(value, next) {} + + reference operator*() const { return this->_cell->value; } + pointer operator->() const { return &this->_cell->value; } + + List &operator++() { + this->_cell = this->_cell->next; + return *this; + } + List operator++(int) { + List old(*this); + this->_cell = this->_cell->next; + return old; + } + + friend reference first<>(List const &); + friend List const &rest<>(List const &); + friend bool is_empty<>(List const &); +}; + +/// Helper template. +template <typename T> +class List<T &> { +public: + typedef std::forward_iterator_tag iterator_category; + typedef T &value_type; + typedef std::ptrdiff_t difference_type; + typedef typename Traits::Reference<value_type>::LValue reference; + typedef typename Traits::Reference<value_type>::RValue const_reference; + typedef typename Traits::Reference<value_type>::Pointer pointer; + + List() : _cell(nullptr) {} + List(const_reference value, List const &next=List()) + : _cell(new ListCell<T &>(value, next._cell)) {} + + operator bool() const { return this->_cell; } + + reference operator*() const { return this->_cell->value; } + pointer operator->() const { return &this->_cell->value; } + + bool operator==(List const &other) const { + return this->_cell == other._cell; + } + bool operator!=(List const &other) const { + return this->_cell != other._cell; + } + + List &operator++() { + this->_cell = this->_cell->next; + return *this; + } + List operator++(int) { + List old(*this); + this->_cell = this->_cell->next; + return old; + } + + friend reference first<>(List const &); + friend List const &rest<>(List const &); + friend bool is_empty<>(List const &); + +protected: + ListCell<T &> *_cell; +}; + +/** + * Generic MutableList. + * + * Like a linked list, but one whose tail can be exchanged for + * another later by using set_rest() or assignment through rest() + * as an lvalue. It's otherwise identical to the "non-mutable" form. + * + * As with List, you can create an empty list like so: + * + * <code> MutableList<int>() </code> + */ +template <typename T> +class MutableList : public List<T> { +public: + MutableList() = default; + explicit MutableList(typename List<T>::const_reference value, + MutableList const &next=MutableList()) + : List<T>(value, next) {} + + MutableList &operator++() { + this->_cell = this->_cell->next; + return *this; + } + MutableList operator++(int) { + MutableList old(*this); + this->_cell = this->_cell->next; + return old; + } + + friend MutableList &rest<>(MutableList const &); + friend MutableList const &set_rest<>(MutableList const &, + MutableList const &); +}; + +/** + * Creates a (non-empty) linked list. + * + * Creates a new linked list with a copy of the given value (\a first) + * in its first element; the remainder of the list will be the list + * provided as \a rest. + * + * The remainder of the list -- the "tail" -- is incorporated by + * reference rather than being copied. + * + * The returned value can also be treated as an STL forward iterator. + * + * @param first the value for the first element of the list + * @param rest the rest of the list; may be an empty list + * + * @return a new list + * + * @see List<> + * @see is_empty<> + * + */ +template <typename T> +inline List<T> cons(typename Traits::Reference<T>::RValue first, + List<T> const &rest) +{ + return List<T>(first, rest); +} + +/** + * Creates a (non-empty) linked list whose tail can be exchanged + * for another. + * + * Creates a new linked list, but one whose tail can be exchanged for + * another later by using set_rest() or assignment through rest() + * as an lvalue. It's otherwise identical to the "non-mutable" form. + * + * This form of cons() is synonymous with MutableList<T>(first, rest), + * except that the compiler can usually infer T from the type of \a rest. + * + * As with List<>, you can create an empty list like so: + * + * MutableList<int>() + * + * @see MutableList<> + * @see is_empty<> + * + * @param first the value for the first element of the list + * @param rest the rest of the list; may be an empty list + * + * @return a new list + */ +template <typename T> +inline MutableList<T> cons(typename Traits::Reference<T>::RValue first, + MutableList<T> const &rest) +{ + return MutableList<T>(first, rest); +} + +/** + * Returns true if the given list is empty. + * + * Returns true if the given list is empty. This is equivalent + * to !list. + * + * @param list the list + * + * @return true if the list is empty, false otherwise. + */ +template <typename T> +inline bool is_empty(List<T> const &list) { return !list._cell; } + +/** + * Returns the first value in a linked list. + * + * Returns a reference to the first value in the list. This + * corresponds to the value of the first argument passed to cons(). + * + * If the list holds mutable values (or references to them), first() + * can be used as an lvalue. + * + * For example: + * + * first(list) = value; + * + * The results of calling this on an empty list are undefined. + * + * @see cons<> + * @see is_empty<> + * + * @param list the list; cannot be empty + * + * @return a reference to the first value in the list + */ +template <typename T> +inline typename List<T>::reference first(List<T> const &list) { + return list._cell->value; +} + +/** + * Returns the remainder of a linked list after the first element. + * + * Returns the remainder of the list after the first element (its "tail"). + * + * This will be the same as the second argument passed to cons(). + * + * The results of calling this on an empty list are undefined. + * + * @see cons<> + * @see is_empty<> + * + * @param list the list; cannot be empty + * + * @return the remainder of the list + */ +template <typename T> +inline List<T> const &rest(List<T> const &list) { + return reinterpret_cast<List<T> const &>(list._cell->next); +} + +/** + * Returns a reference to the remainder of a linked list after + * the first element. + * + * Returns a reference to the remainder of the list after the first + * element (its "tail"). For MutableList<>, rest() can be used as + * an lvalue, to set a new tail. + * + * For example: + * + * rest(list) = other; + * + * Results of calling this on an empty list are undefined. + * + * @see cons<> + * @see is_empty<> + * + * @param list the list; cannot be empty + * + * @return a reference to the remainder of the list + */ +template <typename T> +inline MutableList<T> &rest(MutableList<T> const &list) { + return reinterpret_cast<MutableList<T> &>(list._cell->next); +} + +/** + * Sets a new tail for an existing linked list. + * + * Sets the tail of the given MutableList<>, corresponding to the + * second argument of cons(). + * + * Results of calling this on an empty list are undefined. + * + * @see rest<> + * @see cons<> + * @see is_empty<> + * + * @param list the list; cannot be empty + * @param rest the new tail; corresponds to the second argument of cons() + * + * @return the new tail + */ +template <typename T> +inline MutableList<T> const &set_rest(MutableList<T> const &list, + MutableList<T> const &rest) +{ + list._cell->next = rest._cell; + return reinterpret_cast<MutableList<T> &>(list._cell->next); +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/longest-common-suffix.h b/src/util/longest-common-suffix.h new file mode 100644 index 0000000..bc72e6f --- /dev/null +++ b/src/util/longest-common-suffix.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Algorithms::longest_common_suffix + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ALGORITHMS_LONGEST_COMMON_SUFFIX_H +#define SEEN_INKSCAPE_ALGORITHMS_LONGEST_COMMON_SUFFIX_H + +#include <iterator> +#include <functional> +#include "util/list.h" + +namespace Inkscape { + +namespace Algorithms { + +/** + * Time costs: + * + * The case of sharing a common successor is handled in O(1) time. + * + * If \a a is the longest common suffix, then runs in O(len(rest of b)) time. + * + * Otherwise, runs in O(len(a) + len(b)) time. + */ + +template <typename ForwardIterator> +ForwardIterator longest_common_suffix(ForwardIterator a, ForwardIterator b, + ForwardIterator end) +{ + typedef typename std::iterator_traits<ForwardIterator>::value_type value_type; + return longest_common_suffix(a, b, end, std::equal_to<value_type>()); +} + +template <typename ForwardIterator, typename BinaryPredicate> +ForwardIterator longest_common_suffix(ForwardIterator a, ForwardIterator b, + ForwardIterator end, BinaryPredicate pred) +{ + if ( a == end || b == end ) { + return end; + } + + /* Handle in O(1) time the common cases of identical lists or tails. */ + { + /* identical lists? */ + if ( a == b ) { + return a; + } + + /* identical tails? */ + ForwardIterator tail_a(a); + ForwardIterator tail_b(b); + if ( ++tail_a == ++tail_b ) { + return tail_a; + } + } + + /* Build parallel lists of suffixes, ordered by increasing length. */ + + using Inkscape::Util::List; + using Inkscape::Util::cons; + ForwardIterator lists[2] = { a, b }; + List<ForwardIterator> suffixes[2]; + + for ( int i=0 ; i < 2 ; i++ ) { + for ( ForwardIterator iter(lists[i]) ; iter != end ; ++iter ) { + if ( iter == lists[1-i] ) { + // the other list is a suffix of this one + return lists[1-i]; + } + + suffixes[i] = cons(iter, suffixes[i]); + } + } + + /* Iterate in parallel through the lists of suffix lists from shortest to + * longest, stopping before the first pair of suffixes that differs + */ + + ForwardIterator longest_common(end); + + while ( suffixes[0] && suffixes[1] && + pred(**suffixes[0], **suffixes[1]) ) + { + longest_common = *suffixes[0]; + ++suffixes[0]; + ++suffixes[1]; + } + + return longest_common; +} + +} + +} + +#endif /* !SEEN_INKSCAPE_ALGORITHMS_LONGEST_COMMON_SUFFIX_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/util/reference.h b/src/util/reference.h new file mode 100644 index 0000000..b4a123b --- /dev/null +++ b/src/util/reference.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Traits::Reference - traits class for dealing with reference types + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_TRAITS_REFERENCE_H +#define SEEN_INKSCAPE_TRAITS_REFERENCE_H + +namespace Inkscape { + +namespace Traits { + +template <typename T> +struct Reference { + typedef T const &RValue; + typedef T &LValue; + typedef T *Pointer; + typedef T const *ConstPointer; +}; + +template <typename T> +struct Reference<T &> { + typedef T &RValue; + typedef T &LValue; + typedef T *Pointer; + typedef T const *ConstPointer; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/reverse-list.h b/src/util/reverse-list.h new file mode 100644 index 0000000..c016408 --- /dev/null +++ b/src/util/reverse-list.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::reverse_list - generate a reversed list from iterator range + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_REVERSE_LIST_H +#define SEEN_INKSCAPE_UTIL_REVERSE_LIST_H + +#include "util/list.h" +#include "util/list-copy.h" + +namespace Inkscape { + +namespace Util { + +template <typename InputIterator> +inline typename Traits::ListCopy<InputIterator>::ResultList +reverse_list(InputIterator start, InputIterator end) { + typename Traits::ListCopy<InputIterator>::ResultList head; + while ( start != end ) { + head = cons(*start, head); + ++start; + } + return head; +} + +template <typename T> +inline typename Traits::ListCopy<List<T> >::ResultList +reverse_list(List<T> const &list) { + return reverse_list(list, List<T>()); +} + +template <typename T> +inline MutableList<T> +reverse_list_in_place(MutableList<T> start, + MutableList<T> end=MutableList<T>()) +{ + MutableList<T> reversed(end); + while ( start != end ) { + MutableList<T> temp(start); + ++start; + set_rest(temp, reversed); + reversed = temp; + } + return reversed; +} + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/share.cpp b/src/util/share.cpp new file mode 100644 index 0000000..6c436dc --- /dev/null +++ b/src/util/share.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::ptr_shared<T> - like T const *, but stronger. + * Used to hold c-style strings for objects that are managed by the gc. + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "util/share.h" +#include <glib.h> + +namespace Inkscape { +namespace Util { + +ptr_shared share_string(char const *string) { + g_return_val_if_fail(string != nullptr, share_unsafe(nullptr)); + return share_string(string, std::strlen(string)); +} + +ptr_shared share_string(char const *string, std::size_t length) { + g_return_val_if_fail(string != nullptr, share_unsafe(nullptr)); + char *new_string=new (GC::ATOMIC) char[length+1]; + std::memcpy(new_string, string, length); + new_string[length] = 0; + return share_unsafe(new_string); +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/share.h b/src/util/share.h new file mode 100644 index 0000000..e8b9fb1 --- /dev/null +++ b/src/util/share.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Util::ptr_shared<T> - like T const *, but stronger. + * Used to hold c-style strings for objects that are managed by the gc. + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2006 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_SHARE_H +#define SEEN_INKSCAPE_UTIL_SHARE_H + +#include "inkgc/gc-core.h" +#include <cstring> +#include <cstddef> + +namespace Inkscape { +namespace Util { + +class ptr_shared { +public: + + ptr_shared() : _string(nullptr) {} + ptr_shared(ptr_shared const &other) = default; + + operator char const *() const { return _string; } + operator bool() const { return _string; } + + char const *pointer() const { return _string; } + char const &operator[](int i) const { return _string[i]; } + + ptr_shared operator+(int i) const { + return share_unsafe(_string+i); + } + ptr_shared operator-(int i) const { + return share_unsafe(_string-i); + } + //WARNING: No bounds checking in += and -= functions. Moving the pointer + //past the end of the string and then back could probably cause the garbage + //collector to deallocate the string inbetween, as there's temporary no + //valid reference pointing into the allocated space. + ptr_shared &operator+=(int i) { + _string += i; + return *this; + } + ptr_shared &operator-=(int i) { + _string -= i; + return *this; + } + std::ptrdiff_t operator-(ptr_shared const &other) { + return _string - other._string; + } + + ptr_shared &operator=(ptr_shared const &other) = default; + + bool operator==(ptr_shared const &other) const { + return _string == other._string; + } + bool operator!=(ptr_shared const &other) const { + return _string != other._string; + } + bool operator>(ptr_shared const &other) const { + return _string > other._string; + } + bool operator<(ptr_shared const &other) const { + return _string < other._string; + } + + friend ptr_shared share_unsafe(char const *string); + +private: + ptr_shared(char const *string) : _string(string) {} + static ptr_shared share_unsafe(char const *string) { + return ptr_shared(string); + } + + //This class (and code using it) assumes that it never has to free this + //pointer, and that the memory it points to will not be freed as long as a + //ptr_shared pointing to it exists. + char const *_string; +}; + +ptr_shared share_string(char const *string); +ptr_shared share_string(char const *string, std::size_t length); + +inline ptr_shared share_unsafe(char const *string) { + return ptr_shared::share_unsafe(string); +} + +//TODO: Do we need this function? +inline ptr_shared share_static_string(char const *string) { + return share_unsafe(string); +} + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/signal-blocker.h b/src/util/signal-blocker.h new file mode 100644 index 0000000..8fb6256 --- /dev/null +++ b/src/util/signal-blocker.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Base RAII blocker for sgic++ signals. + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_SIGNAL_BLOCKER_H +#define SEEN_INKSCAPE_UTIL_SIGNAL_BLOCKER_H + +#include <string> +#include <sigc++/connection.h> + +/** + * Base RAII blocker for sgic++ signals. + */ +class SignalBlocker +{ +public: + /** + * Creates a new instance that if the signal is currently unblocked will block + * it until this instance is destructed and then will unblock it. + */ + SignalBlocker( sigc::connection *connection ) : + _connection(connection), + _wasBlocked(_connection->blocked()) + { + if (!_wasBlocked) + { + _connection->block(); + } + } + + /** + * Destructor that will unblock the signal if it was blocked initially by this + * instance. + */ + ~SignalBlocker() + { + if (!_wasBlocked) + { + _connection->block(false); + } + } + +private: + // noncopyable, nonassignable + SignalBlocker(SignalBlocker const &other) = delete; + SignalBlocker& operator=(SignalBlocker const &other) = delete; + + sigc::connection *_connection; + bool _wasBlocked; +}; + +#endif // SEEN_INKSCAPE_UTIL_SIGNAL_BLOCKER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ucompose.hpp b/src/util/ucompose.hpp new file mode 100644 index 0000000..e0d2fc2 --- /dev/null +++ b/src/util/ucompose.hpp @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2006 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* Defines String::ucompose(fmt, arg...) for easy, i18n-friendly + * composition of strings with Gtkmm >= 1.3.* (see www.gtkmm.org). + * Uses Glib::ustring instead of std::string which doesn't work with + * Gtkmm due to character encoding troubles with stringstreams. + * + * Version 1.0.4. + * + * Copyright (c) 2002, 03, 04 Ole Laursen <olau@hardworking.dk>. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA. + */ + +// +// Basic usage is like +// +// String::ucompose("This is a %1x%2 matrix.", rows, cols); +// +// See http://www.cs.aau.dk/~olau/compose/ or the included +// README.compose for more details. +// + +#ifndef STRING_UCOMPOSE_HPP +#define STRING_UCOMPOSE_HPP + +#include <glibmm/ustring.h> +#include <glibmm/convert.h> + +#include <stdexcept> +#include <sstream> +#include <string> +#include <list> +#include <map> // for multimap + +namespace UStringPrivate +{ + // the actual composition class - using String::ucompose is cleaner, so we + // hide it here + class Composition + { + public: + // initialize and prepare format string on the form "text %1 text %2 etc." + explicit Composition(std::string fmt); + + // supply an replacement argument starting from %1 + template <typename T> + Composition &arg(const T &obj); + + // compose and return string + Glib::ustring str() const; + + private: + + //This is standard, not GCC-specific like wostringstream + std::basic_ostringstream<wchar_t> os; + + int arg_no; + + // we store the output as a list - when the output string is requested, the + // list is concatenated to a string; this way we can keep iterators into + // the list instead of into a string where they're possibly invalidated + // when inserting a specification string + typedef std::list<std::string> output_list; + output_list output; + + // the initial parse of the format string fills in the specification map + // with positions for each of the various %?s + typedef std::multimap<int, output_list::iterator> specification_map; + specification_map specs; + + template <typename T> + std::string stringify(T obj); + }; + + // helper for converting spec string numbers + inline int char_to_int(char c) + { + switch (c) { + case '0': return 0; + case '1': return 1; + case '2': return 2; + case '3': return 3; + case '4': return 4; + case '5': return 5; + case '6': return 6; + case '7': return 7; + case '8': return 8; + case '9': return 9; + default: return -1000; + } + } + + inline bool is_number(int n) + { + switch (n) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + + default: + return false; + } + } + + template <typename T> + inline std::string Composition::stringify(T obj) + { + os << obj; + + std::wstring str = os.str(); + + return Glib::convert(std::string(reinterpret_cast<const char *>(str.data()), + str.size() * sizeof(wchar_t)), + "UTF-8", "WCHAR_T"); + } + + // specialisations for the common string types + template <> + inline std::string + Composition::stringify<std::string>(std::string obj) + { + return obj; + } + + template <> + inline std::string + Composition::stringify<Glib::ustring>(Glib::ustring obj) + { + return obj; + } + + template <> + inline std::string + Composition::stringify<const char *>(const char *obj) + { + return obj; + } + + // implementation of class Composition + template <typename T> + inline Composition &Composition::arg(const T &obj) + { + Glib::ustring rep = stringify(obj); + + if (!rep.empty()) { // manipulators don't produce output + for (specification_map::const_iterator i = specs.lower_bound(arg_no), + end = specs.upper_bound(arg_no); i != end; ++i) { + output_list::iterator pos = i->second; + ++pos; + + output.insert(pos, rep); + } + + os.str(std::wstring()); + //os.clear(); + ++arg_no; + } + + return *this; + } + + inline Composition::Composition(std::string fmt) + : arg_no(1) + { +#if __GNUC__ >= 3 + try { + os.imbue(std::locale("")); // use the user's locale for the stream + } + catch (std::runtime_error& e) { // fallback to classic if it failed + os.imbue(std::locale::classic()); + } +#endif + std::string::size_type b = 0, i = 0; + + // fill in output with the strings between the %1 %2 %3 etc. and + // fill in specs with the positions + while (i < fmt.length()) { + if (fmt[i] == '%' && i + 1 < fmt.length()) { + if (fmt[i + 1] == '%') { // catch %% + fmt.replace(i, 2, "%"); + ++i; + } + else if (is_number(fmt[i + 1])) { // aha! a spec! + // save string + output.push_back(fmt.substr(b, i - b)); + + int n = 1; // number of digits + int spec_no = 0; + + do { + spec_no += char_to_int(fmt[i + n]); + spec_no *= 10; + ++n; + } while (i + n < fmt.length() && is_number(fmt[i + n])); + + spec_no /= 10; + output_list::iterator pos = output.end(); + --pos; // safe since we have just inserted a string + + specs.insert(specification_map::value_type(spec_no, pos)); + + // jump over spec string + i += n; + b = i; + } + else + ++i; + } + else + ++i; + } + + if (i - b > 0) // add the rest of the string + output.push_back(fmt.substr(b, i - b)); + } + + inline Glib::ustring Composition::str() const + { + // assemble string + std::string str; + + for (const auto & i : output) + str += i; + + return str; + } +} + + +namespace String +{ + // a series of functions which accept a format string on the form "text %1 + // more %2 less %3" and a number of templated parameters and spits out the + // composited string + template <typename T1> + inline Glib::ustring ucompose(const Glib::ustring &fmt, const T1 &o1) + { + UStringPrivate::Composition c(fmt); + c.arg(o1); + return c.str(); + } + + template <typename T1, typename T2> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2); + return c.str(); + } + + template <typename T1, typename T2, typename T3> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10, + typename T11> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10, const T11 &o11) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10).arg(o11); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10, + typename T11, typename T12> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10, const T11 &o11, const T12 &o12) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10).arg(o11).arg(o12); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10, + typename T11, typename T12, typename T13> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10, const T11 &o11, const T12 &o12, + const T13 &o13) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10).arg(o11).arg(o12).arg(o13); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10, + typename T11, typename T12, typename T13, typename T14> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10, const T11 &o11, const T12 &o12, + const T13 &o13, const T14 &o14) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10).arg(o11).arg(o12).arg(o13).arg(o14); + return c.str(); + } + + template <typename T1, typename T2, typename T3, typename T4, typename T5, + typename T6, typename T7, typename T8, typename T9, typename T10, + typename T11, typename T12, typename T13, typename T14, + typename T15> + inline Glib::ustring ucompose(const Glib::ustring &fmt, + const T1 &o1, const T2 &o2, const T3 &o3, + const T4 &o4, const T5 &o5, const T6 &o6, + const T7 &o7, const T8 &o8, const T9 &o9, + const T10 &o10, const T11 &o11, const T12 &o12, + const T13 &o13, const T14 &o14, const T15 &o15) + { + UStringPrivate::Composition c(fmt); + c.arg(o1).arg(o2).arg(o3).arg(o4).arg(o5).arg(o6).arg(o7).arg(o8).arg(o9) + .arg(o10).arg(o11).arg(o12).arg(o13).arg(o14).arg(o15); + return c.str(); + } +} + + +#endif // STRING_UCOMPOSE_HPP diff --git a/src/util/units.cpp b/src/util/units.cpp new file mode 100644 index 0000000..d24ec3c --- /dev/null +++ b/src/util/units.cpp @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Units + * + * Authors: + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2013 Matthew Petroff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <cerrno> +#include <iomanip> +#include <iostream> +#include <utility> +#include <unordered_map> +#include <glib.h> +#include <glibmm/regex.h> +#include <glibmm/fileutils.h> +#include <glibmm/markup.h> + +#include <2geom/coord.h> + +#include "util/units.h" +#include "path-prefix.h" +#include "streq.h" + +using Inkscape::Util::UNIT_TYPE_DIMENSIONLESS; +using Inkscape::Util::UNIT_TYPE_LINEAR; +using Inkscape::Util::UNIT_TYPE_RADIAL; +using Inkscape::Util::UNIT_TYPE_FONT_HEIGHT; + +namespace +{ + +#define MAKE_UNIT_CODE(a, b) \ + ((((unsigned)(a) & 0xdf) << 8) | ((unsigned)(b) & 0xdf)) + +enum UnitCode { + UNIT_CODE_PX = MAKE_UNIT_CODE('p','x'), + UNIT_CODE_PT = MAKE_UNIT_CODE('p','t'), + UNIT_CODE_PC = MAKE_UNIT_CODE('p','c'), + UNIT_CODE_MM = MAKE_UNIT_CODE('m','m'), + UNIT_CODE_CM = MAKE_UNIT_CODE('c','m'), + UNIT_CODE_IN = MAKE_UNIT_CODE('i','n'), + UNIT_CODE_EM = MAKE_UNIT_CODE('e','m'), + UNIT_CODE_EX = MAKE_UNIT_CODE('e','x'), + UNIT_CODE_PERCENT = MAKE_UNIT_CODE('%',0) +}; + +// TODO: convert to constexpr in C++11, so that the above constants can be eliminated +inline unsigned make_unit_code(char a, char b) { + // this should work without the casts, but let's be 100% sure + // also ensure that the codes are in lowercase + return MAKE_UNIT_CODE(a,b); +} +inline unsigned make_unit_code(char const *str) { + if (!str || str[0] == 0) return 0; + return MAKE_UNIT_CODE(str[0], str[1]); +} + + +// This must match SVGLength::Unit +unsigned const svg_length_lookup[] = { + 0, + UNIT_CODE_PX, + UNIT_CODE_PT, + UNIT_CODE_PC, + UNIT_CODE_MM, + UNIT_CODE_CM, + UNIT_CODE_IN, + UNIT_CODE_EM, + UNIT_CODE_EX, + UNIT_CODE_PERCENT +}; + + + +// maps unit codes obtained from their abbreviations to their SVGLength unit indexes +typedef std::unordered_map<unsigned, SVGLength::Unit> UnitCodeLookup; + +UnitCodeLookup make_unit_code_lookup() +{ + UnitCodeLookup umap; + for (unsigned i = 1; i < G_N_ELEMENTS(svg_length_lookup); ++i) { + umap[svg_length_lookup[i]] = static_cast<SVGLength::Unit>(i); + } + return umap; +} + +UnitCodeLookup const unit_code_lookup = make_unit_code_lookup(); + + + +typedef std::unordered_map<Glib::ustring, Inkscape::Util::UnitType> TypeMap; + +/** A std::map that gives the data type value for the string version. + * @todo consider hiding map behind hasFoo() and getFoo() type functions. */ +TypeMap make_type_map() +{ + TypeMap tmap; + tmap["DIMENSIONLESS"] = UNIT_TYPE_DIMENSIONLESS; + tmap["LINEAR"] = UNIT_TYPE_LINEAR; + tmap["RADIAL"] = UNIT_TYPE_RADIAL; + tmap["FONT_HEIGHT"] = UNIT_TYPE_FONT_HEIGHT; + // Note that code was not yet handling LINEAR_SCALED, TIME, QTY and NONE + + return tmap; +} + +TypeMap const type_map = make_type_map(); + +} // namespace + +namespace Inkscape { +namespace Util { + +class UnitParser : public Glib::Markup::Parser +{ +public: + typedef Glib::Markup::Parser::AttributeMap AttrMap; + typedef Glib::Markup::ParseContext Ctx; + + UnitParser(UnitTable *table); + ~UnitParser() override = default; + +protected: + void on_start_element(Ctx &ctx, Glib::ustring const &name, AttrMap const &attrs) override; + void on_end_element(Ctx &ctx, Glib::ustring const &name) override; + void on_text(Ctx &ctx, Glib::ustring const &text) override; + +public: + UnitTable *tbl; + bool primary; + bool skip; + Unit unit; +}; + +UnitParser::UnitParser(UnitTable *table) : + tbl(table), + primary(false), + skip(false) +{ +} + +#define BUFSIZE (255) + +Unit::Unit() : + type(UNIT_TYPE_DIMENSIONLESS), // should this or NONE be the default? + factor(1.0), + name(), + name_plural(), + abbr(), + description() +{ +} + +Unit::Unit(UnitType type, + double factor, + Glib::ustring name, + Glib::ustring name_plural, + Glib::ustring abbr, + Glib::ustring description) + : type(type) + , factor(factor) + , name(std::move(name)) + , name_plural(std::move(name_plural)) + , abbr(std::move(abbr)) + , description(std::move(description)) +{ + g_return_if_fail(factor <= 0); +} + +void Unit::clear() +{ + *this = Unit(); +} + +int Unit::defaultDigits() const +{ + int factor_digits = int(log10(factor)); + if (factor_digits < 0) { + g_warning("factor = %f, factor_digits = %d", factor, factor_digits); + g_warning("factor_digits < 0 - returning 0"); + factor_digits = 0; + } + return factor_digits; +} + +bool Unit::compatibleWith(Unit const *u) const +{ + // Percentages + if (type == UNIT_TYPE_DIMENSIONLESS || u->type == UNIT_TYPE_DIMENSIONLESS) { + return true; + } + + // Other units with same type + if (type == u->type) { + return true; + } + + // Different, incompatible types + return false; +} +bool Unit::compatibleWith(Glib::ustring const &u) const +{ + return compatibleWith(unit_table.getUnit(u)); +} + +bool Unit::operator==(Unit const &other) const +{ + return (type == other.type && name.compare(other.name) == 0); +} + +int Unit::svgUnit() const +{ + char const *astr = abbr.c_str(); + unsigned code = make_unit_code(astr); + + UnitCodeLookup::const_iterator u = unit_code_lookup.find(code); + if (u != unit_code_lookup.end()) { + return u->second; + } + return 0; +} + +double Unit::convert(double from_dist, Unit const *to) const +{ + // Percentage + if (to->type == UNIT_TYPE_DIMENSIONLESS) { + return from_dist * to->factor; + } + + // Incompatible units + if (type != to->type) { + return -1; + } + + // Compatible units + return from_dist * factor / to->factor; +} +double Unit::convert(double from_dist, Glib::ustring const &to) const +{ + return convert(from_dist, unit_table.getUnit(to)); +} +double Unit::convert(double from_dist, char const *to) const +{ + return convert(from_dist, unit_table.getUnit(to)); +} + + + +Unit UnitTable::_empty_unit; + +UnitTable::UnitTable() +{ + gchar *filename = g_build_filename(INKSCAPE_UIDIR, "units.xml", NULL); + load(filename); + g_free(filename); +} + +UnitTable::~UnitTable() +{ + for (auto & iter : _unit_map) + { + delete iter.second; + } +} + +void UnitTable::addUnit(Unit const &u, bool primary) +{ + _unit_map[make_unit_code(u.abbr.c_str())] = new Unit(u); + if (primary) { + _primary_unit[u.type] = u.abbr; + } +} + +Unit const *UnitTable::getUnit(char const *abbr) const +{ + UnitCodeMap::const_iterator f = _unit_map.find(make_unit_code(abbr)); + if (f != _unit_map.end()) { + return &(*f->second); + } + return &_empty_unit; +} + +Unit const *UnitTable::getUnit(Glib::ustring const &unit_abbr) const +{ + return getUnit(unit_abbr.c_str()); +} +Unit const *UnitTable::getUnit(SVGLength::Unit u) const +{ + if (u == 0 || u > SVGLength::LAST_UNIT) { + return &_empty_unit; + } + + UnitCodeMap::const_iterator f = _unit_map.find(svg_length_lookup[u]); + if (f != _unit_map.end()) { + return &(*f->second); + } + return &_empty_unit; +} + +Unit const *UnitTable::findUnit(double factor, UnitType type) const +{ + const double eps = factor * 0.01; // allow for 1% deviation + + UnitCodeMap::const_iterator cit = _unit_map.begin(); + while (cit != _unit_map.end()) { + if (cit->second->type == type) { + if (Geom::are_near(cit->second->factor, factor, eps)) { + // unit found! + break; + } + } + ++cit; + } + + if (cit != _unit_map.end()) { + return cit->second; + } else { + return getUnit(_primary_unit[type]); + } +} + +Quantity UnitTable::parseQuantity(Glib::ustring const &q) const +{ + Glib::MatchInfo match_info; + + // Extract value + double value = 0; + Glib::RefPtr<Glib::Regex> value_regex = Glib::Regex::create("[-+]*[\\d+]*[\\.,]*[\\d+]*[eE]*[-+]*\\d+"); + if (value_regex->match(q, match_info)) { + std::istringstream tmp_v(match_info.fetch(0)); + tmp_v >> value; + } + int start_pos, end_pos; + match_info.fetch_pos(0, end_pos, start_pos); + end_pos = q.size() - start_pos; + Glib::ustring u = q.substr(start_pos, end_pos); + + // Extract unit abbreviation + Glib::ustring abbr; + Glib::RefPtr<Glib::Regex> unit_regex = Glib::Regex::create("[A-z%]+"); + if (unit_regex->match(u, match_info)) { + abbr = match_info.fetch(0); + } + + Quantity qty(value, abbr); + return qty; +} + +/* UNSAFE while passing around pointers to the Unit objects in this table +bool UnitTable::deleteUnit(Unit const &u) +{ + bool deleted = false; + // Cannot delete the primary unit type since it's + // used for conversions + if (u.abbr != _primary_unit[u.type]) { + UnitCodeMap::iterator iter = _unit_map.find(make_unit_code(u.abbr.c_str())); + if (iter != _unit_map.end()) { + delete (*iter).second; + _unit_map.erase(iter); + deleted = true; + } + } + return deleted; +} +*/ + +bool UnitTable::hasUnit(Glib::ustring const &unit) const +{ + UnitCodeMap::const_iterator iter = _unit_map.find(make_unit_code(unit.c_str())); + return (iter != _unit_map.end()); +} + +UnitTable::UnitMap UnitTable::units(UnitType type) const +{ + UnitMap submap; + for (auto iter : _unit_map) { + if (iter.second->type == type) { + submap.insert(UnitMap::value_type(iter.second->abbr, *iter.second)); + } + } + + return submap; +} + +Glib::ustring UnitTable::primary(UnitType type) const +{ + return _primary_unit[type]; +} + +bool UnitTable::load(std::string const &filename) { + UnitParser uparser(this); + Glib::Markup::ParseContext ctx(uparser); + + try { + Glib::ustring unitfile = Glib::file_get_contents(filename); + ctx.parse(unitfile); + ctx.end_parse(); + } catch (Glib::FileError const &e) { + g_warning("Units file %s is missing: %s\n", filename.c_str(), e.what().c_str()); + return false; + } catch (Glib::MarkupError const &e) { + g_warning("Problem loading units file '%s': %s\n", filename.c_str(), e.what().c_str()); + return false; + } + return true; +} + +/* +bool UnitTable::save(std::string const &filename) { + g_warning("UnitTable::save(): not implemented"); + + return false; +} +*/ + +Inkscape::Util::UnitTable unit_table; + +void UnitParser::on_start_element(Ctx &/*ctx*/, Glib::ustring const &name, AttrMap const &attrs) +{ + if (name == "unit") { + // reset for next use + unit.clear(); + primary = false; + skip = false; + + AttrMap::const_iterator f; + if ((f = attrs.find("type")) != attrs.end()) { + Glib::ustring type = f->second; + TypeMap::const_iterator tf = type_map.find(type); + if (tf != type_map.end()) { + unit.type = tf->second; + } else { + g_warning("Skipping unknown unit type '%s'.\n", type.c_str()); + skip = true; + } + } + if ((f = attrs.find("pri")) != attrs.end()) { + primary = (f->second[0] == 'y' || f->second[0] == 'Y'); + } + } +} + +void UnitParser::on_text(Ctx &ctx, Glib::ustring const &text) +{ + Glib::ustring element = ctx.get_element(); + if (element == "name") { + unit.name = text; + } else if (element == "plural") { + unit.name_plural = text; + } else if (element == "abbr") { + unit.abbr = text; + } else if (element == "factor") { + // TODO make sure we use the right conversion + unit.factor = g_ascii_strtod(text.c_str(), nullptr); + } else if (element == "description") { + unit.description = text; + } +} + +void UnitParser::on_end_element(Ctx &/*ctx*/, Glib::ustring const &name) +{ + if (name == "unit" && !skip) { + tbl->addUnit(unit, primary); + } +} + +Quantity::Quantity(double q, Unit const *u) + : unit(u) + , quantity(q) +{ +} +Quantity::Quantity(double q, Glib::ustring const &u) + : unit(unit_table.getUnit(u.c_str())) + , quantity(q) +{ +} +Quantity::Quantity(double q, char const *u) + : unit(unit_table.getUnit(u)) + , quantity(q) +{ +} + +bool Quantity::compatibleWith(Unit const *u) const +{ + return unit->compatibleWith(u); +} +bool Quantity::compatibleWith(Glib::ustring const &u) const +{ + return compatibleWith(u.c_str()); +} +bool Quantity::compatibleWith(char const *u) const +{ + return compatibleWith(unit_table.getUnit(u)); +} + +double Quantity::value(Unit const *u) const +{ + return convert(quantity, unit, u); +} +double Quantity::value(Glib::ustring const &u) const +{ + return value(u.c_str()); +} +double Quantity::value(char const *u) const +{ + return value(unit_table.getUnit(u)); +} + +Glib::ustring Quantity::string(Unit const *u) const { + return Glib::ustring::format(std::fixed, std::setprecision(2), value(u)) + " " + u->abbr; +} +Glib::ustring Quantity::string(Glib::ustring const &u) const { + return string(unit_table.getUnit(u.c_str())); +} +Glib::ustring Quantity::string() const { + return string(unit); +} + +double Quantity::convert(double from_dist, Unit const *from, Unit const *to) +{ + return from->convert(from_dist, to); +} +double Quantity::convert(double from_dist, Glib::ustring const &from, Unit const *to) +{ + return convert(from_dist, unit_table.getUnit(from.c_str()), to); +} +double Quantity::convert(double from_dist, Unit const *from, Glib::ustring const &to) +{ + return convert(from_dist, from, unit_table.getUnit(to.c_str())); +} +double Quantity::convert(double from_dist, Glib::ustring const &from, Glib::ustring const &to) +{ + return convert(from_dist, unit_table.getUnit(from.c_str()), unit_table.getUnit(to.c_str())); +} +double Quantity::convert(double from_dist, char const *from, char const *to) +{ + return convert(from_dist, unit_table.getUnit(from), unit_table.getUnit(to)); +} + +bool Quantity::operator<(Quantity const &rhs) const +{ + if (unit->type != rhs.unit->type) { + g_warning("Incompatible units"); + return false; + } + return quantity < rhs.value(unit); +} +bool Quantity::operator==(Quantity const &other) const +{ + /** \fixme This is overly strict. I think we should change this to: + if (unit->type != other.unit->type) { + g_warning("Incompatible units"); + return false; + } + return are_near(quantity, other.value(unit)); + */ + return (*unit == *other.unit) && (quantity == other.quantity); +} + +} // namespace Util +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/units.h b/src/util/units.h new file mode 100644 index 0000000..48478c7 --- /dev/null +++ b/src/util/units.h @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Units + * These classes are used for defining different unit systems. + * + * Authors: + * Matthew Petroff <matthew@mpetroff.net> + * + * Copyright (C) 2013 Matthew Petroff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_UTIL_UNITS_H +#define INKSCAPE_UTIL_UNITS_H + +#include <unordered_map> +#include <boost/operators.hpp> +#include <glibmm/ustring.h> +#include <2geom/coord.h> +#include "svg/svg-length.h" + +#ifndef DOXYGEN_SHOULD_SKIP_THIS + +#define DEFAULT_UNIT_NAME "mm"; + +namespace std { +template <> +struct hash<Glib::ustring> : public std::unary_function<Glib::ustring, std::size_t> { + std::size_t operator()(Glib::ustring const &s) const { + return hash<std::string>()(s.raw()); + } +}; +} // namespace std + +#endif + +namespace Inkscape { +namespace Util { + +enum UnitType { + UNIT_TYPE_DIMENSIONLESS, /* Percentage */ + UNIT_TYPE_LINEAR, + UNIT_TYPE_LINEAR_SCALED, + UNIT_TYPE_RADIAL, + UNIT_TYPE_TIME, + UNIT_TYPE_FONT_HEIGHT, + UNIT_TYPE_QTY, + UNIT_TYPE_NONE = -1 +}; + +const char DEG[] = "°"; + +class Unit + : boost::equality_comparable<Unit> +{ +public: + Unit(); + Unit(UnitType type, + double factor, + Glib::ustring name, + Glib::ustring name_plural, + Glib::ustring abbr, + Glib::ustring description); + + void clear(); + + bool isAbsolute() const { return type != UNIT_TYPE_DIMENSIONLESS; } + + /** + * Returns the suggested precision to use for displaying numbers + * of this unit. + */ + int defaultDigits() const; + + /** Checks if a unit is compatible with the specified unit. */ + bool compatibleWith(Unit const *u) const; + bool compatibleWith(Glib::ustring const &) const; + bool compatibleWith(char const *) const; + + UnitType type; + double factor; + Glib::ustring name; + Glib::ustring name_plural; + Glib::ustring abbr; + Glib::ustring description; + + /** Check if units are equal. */ + bool operator==(Unit const &other) const; + + /** Get SVG unit code. */ + int svgUnit() const; + + /** Convert value from this unit **/ + double convert(double from_dist, Unit const *to) const; + double convert(double from_dist, Glib::ustring const &to) const; + double convert(double from_dist, char const *to) const; +}; + +class Quantity + : boost::totally_ordered<Quantity> +{ +public: + Unit const *unit; + double quantity; + + /** Initialize a quantity. */ + Quantity(double q, Unit const *u); + Quantity(double q, Glib::ustring const &u); + Quantity(double q, char const *u); + + /** Checks if a quantity is compatible with the specified unit. */ + bool compatibleWith(Unit const *u) const; + bool compatibleWith(Glib::ustring const &u) const; + bool compatibleWith(char const *u) const; + + /** Return the quantity's value in the specified unit. */ + double value(Unit const *u) const; + double value(Glib::ustring const &u) const; + double value(char const *u) const; + + /** Return a printable string of the value in the specified unit. */ + Glib::ustring string(Unit const *u) const; + Glib::ustring string(Glib::ustring const &u) const; + Glib::ustring string() const; + + /** Convert distances. + no NULL check is performed on the passed pointers to Unit objects! */ + static double convert(double from_dist, Unit const *from, Unit const *to); + static double convert(double from_dist, Glib::ustring const &from, Unit const *to); + static double convert(double from_dist, Unit const *from, Glib::ustring const &to); + static double convert(double from_dist, Glib::ustring const &from, Glib::ustring const &to); + static double convert(double from_dist, char const *from, char const *to); + + /** Comparison operators. */ + bool operator<(Quantity const &rhs) const; + bool operator==(Quantity const &other) const; +}; + +inline bool are_near(Quantity const &a, Quantity const &b, double eps=Geom::EPSILON) +{ + return Geom::are_near(a.quantity, b.value(a.unit), eps); +} + +class UnitTable { +public: + /** + * Initializes the unit tables and identifies the primary unit types. + * + * The primary unit's conversion factor is required to be 1.00 + */ + UnitTable(); + virtual ~UnitTable(); + + typedef std::unordered_map<Glib::ustring, Unit> UnitMap; + typedef std::unordered_map<unsigned, Unit*> UnitCodeMap; + + /** Add a new unit to the table */ + void addUnit(Unit const &u, bool primary); + + /** Retrieve a given unit based on its string identifier */ + Unit const *getUnit(Glib::ustring const &name) const; + Unit const *getUnit(char const *name) const; + + /** Try to find a unit based on its conversion factor to the primary */ + Unit const *findUnit(double factor, UnitType type) const; + + /** Retrieve a given unit based on its SVGLength unit */ + Unit const *getUnit(SVGLength::Unit u) const; + + /** Retrieve a quantity based on its string identifier */ + Quantity parseQuantity(Glib::ustring const &q) const; + + /** Remove a unit definition from the given unit type table * / + * DISABLED, unsafe with the current passing around pointers to Unit objects in this table */ + //bool deleteUnit(Unit const &u); + + /** Returns true if the given string 'name' is a valid unit in the table */ + bool hasUnit(Glib::ustring const &name) const; + + /** Provides an iteratable list of items in the given unit table */ + UnitMap units(UnitType type) const; + + /** Returns the default unit abbr for the given type */ + Glib::ustring primary(UnitType type) const; + + double getScale() const; + + void setScale(); + + /** Load units from an XML file. + * + * Loads and merges the contents of the given file into the UnitTable, + * possibly overwriting existing unit definitions. + * + * @param filename file to be loaded + */ + bool load(std::string const &filename); + + /* * Saves the current UnitTable to the given file. */ + //bool save(std::string const &filename); + +protected: + UnitCodeMap _unit_map; + Glib::ustring _primary_unit[UNIT_TYPE_QTY]; + + double _linear_scale; + static Unit _empty_unit; + +private: + UnitTable(UnitTable const &t); + UnitTable operator=(UnitTable const &t); + +}; + +extern UnitTable unit_table; + +} // namespace Util +} // namespace Inkscape + +#endif // define INKSCAPE_UTIL_UNITS_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/util/ziptool.cpp b/src/util/ziptool.cpp new file mode 100644 index 0000000..8eb5254 --- /dev/null +++ b/src/util/ziptool.cpp @@ -0,0 +1,3039 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Bob Jamison + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +/* + * This is intended to be a standalone, reduced capability + * implementation of Gzip and Zip functionality. Its + * targeted use case is for archiving and retrieving single files + * which use these encoding types. Being memory based and + * non-optimized, it is not useful in cases where very large + * archives are needed or where high performance is desired. + * However, it should hopefully work very well for smaller, + * one-at-a-time tasks. What you get in return is the ability + * to drop these files into your project and remove the dependencies + * on ZLib and Info-Zip. Enjoy. + */ + + +#include <cstdio> +#include <cstdarg> +#include <ctime> + +#include <string> +#include <utility> + +#include "ziptool.h" + + + + + + +//######################################################################## +//# A D L E R 3 2 +//######################################################################## + +/** + * Constructor + */ +Adler32::Adler32() +{ + reset(); +} + +/** + * Destructor + */ +Adler32::~Adler32() += default; + +/** + * Reset Adler-32 checksum to initial value. + */ +void Adler32::reset() +{ + value = 1; +} + +// ADLER32_BASE is the largest prime number smaller than 65536 +#define ADLER32_BASE 65521 + +void Adler32::update(unsigned char b) +{ + unsigned long s1 = value & 0xffff; + unsigned long s2 = (value >> 16) & 0xffff; + s1 += b & 0xff; + s2 += s1; + value = ((s2 % ADLER32_BASE) << 16) | (s1 % ADLER32_BASE); +} + +void Adler32::update(char *str) +{ + if (str) + while (*str) + update((unsigned char)*str++); +} + + +/** + * Returns current checksum value. + */ +unsigned long Adler32::getValue() +{ + return value & 0xffffffffL; +} + + + +//######################################################################## +//# C R C 3 2 +//######################################################################## + +/** + * Constructor + */ +Crc32::Crc32() +{ + reset(); +} + +/** + * Destructor + */ +Crc32::~Crc32() += default; + +static bool crc_table_ready = false; +static unsigned long crc_table[256]; + +/** + * make the table for a fast CRC. + */ +static void makeCrcTable() +{ + if (crc_table_ready) + return; + for (int n = 0; n < 256; n++) + { + unsigned long c = n; + for (int k = 8; --k >= 0; ) + { + if ((c & 1) != 0) + c = 0xedb88320 ^ (c >> 1); + else + c >>= 1; + } + crc_table[n] = c; + } + crc_table_ready = true; +} + + +/** + * Reset CRC-32 checksum to initial value. + */ +void Crc32::reset() +{ + value = 0; + makeCrcTable(); +} + +void Crc32::update(unsigned char b) +{ + unsigned long c = ~value; + + c &= 0xffffffff; + c = crc_table[(c ^ b) & 0xff] ^ (c >> 8); + value = ~c; +} + + +void Crc32::update(char *str) +{ + if (str) + while (*str) + update((unsigned char)*str++); +} + +void Crc32::update(const std::vector<unsigned char> &buf) +{ + std::vector<unsigned char>::const_iterator iter; + for (iter=buf.begin() ; iter!=buf.end() ; ++iter) + { + unsigned char ch = *iter; + update(ch); + } +} + + +/** + * Returns current checksum value. + */ +unsigned long Crc32::getValue() +{ + return value & 0xffffffffL; +} + +//######################################################################## +//# I N F L A T E R +//######################################################################## + + +/** + * + */ +struct Huffman +{ + int *count; // number of symbols of each length + int *symbol; // canonically ordered symbols +}; + +/** + * + */ +class Inflater +{ +public: + + Inflater(); + + virtual ~Inflater(); + + static const int MAXBITS = 15; // max bits in a code + static const int MAXLCODES = 286; // max number of literal/length codes + static const int MAXDCODES = 30; // max number of distance codes + static const int MAXCODES = 316; // max codes lengths to read + static const int FIXLCODES = 288; // number of fixed literal/length codes + + /** + * + */ + bool inflate(std::vector<unsigned char> &destination, + std::vector<unsigned char> &source); + +private: + + /** + * + */ + void error(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + /** + * + */ + void trace(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + /** + * + */ + void dump(); + + /** + * + */ + int buildHuffman(Huffman *h, int *length, int n); + + /** + * + */ + bool getBits(int need, int *oval); + + /** + * + */ + int doDecode(Huffman *h); + + /** + * + */ + bool doCodes(Huffman *lencode, Huffman *distcode); + + /** + * + */ + bool doStored(); + + /** + * + */ + bool doFixed(); + + /** + * + */ + bool doDynamic(); + + + std::vector<unsigned char>dest; + + std::vector<unsigned char>src; + unsigned long srcPos; //current read position + int bitBuf; + int bitCnt; + +}; + + +/** + * + */ +Inflater::Inflater() : + dest(), + src(), + srcPos(0), + bitBuf(0), + bitCnt(0) +{ +} + +/** + * + */ +Inflater::~Inflater() += default; + +/** + * + */ +void Inflater::error(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "Inflater error:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +/** + * + */ +void Inflater::trace(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "Inflater:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + + +/** + * + */ +void Inflater::dump() +{ + for (unsigned char i : dest) + { + fputc(i, stdout); + } +} + +/** + * + */ +int Inflater::buildHuffman(Huffman *h, int *length, int n) +{ + // count number of codes of each length + for (int len = 0; len <= MAXBITS; len++) + h->count[len] = 0; + for (int symbol = 0; symbol < n; symbol++) + (h->count[length[symbol]])++; // assumes lengths are within bounds + if (h->count[0] == n) // no codes! + { + error("huffman tree will result in failed decode"); + return -1; + } + + // check for an over-subscribed or incomplete set of lengths + int left = 1; // number of possible codes left of current length + for (int len = 1; len <= MAXBITS; len++) + { + left <<= 1; // one more bit, double codes left + left -= h->count[len]; // deduct count from possible codes + if (left < 0) + { + error("huffman over subscribed"); + return -1; + } + } + + // generate offsets into symbol table for each length for sorting + int offs[MAXBITS+1]; //offsets in symbol table for each length + offs[1] = 0; + for (int len = 1; len < MAXBITS; len++) + offs[len + 1] = offs[len] + h->count[len]; + + /* + * put symbols in table sorted by length, by symbol order within each + * length + */ + for (int symbol = 0; symbol < n; symbol++) + if (length[symbol] != 0) + h->symbol[offs[length[symbol]]++] = symbol; + + // return zero for complete set, positive for incomplete set + return left; +} + + +/** + * + */ +bool Inflater::getBits(int requiredBits, int *oval) +{ + long val = bitBuf; + + //add more bytes if needed + while (bitCnt < requiredBits) + { + if (srcPos >= src.size()) + { + error("premature end of input"); + return false; + } + val |= ((long)(src[srcPos++])) << bitCnt; + bitCnt += 8; + } + + //update the buffer and return the data + bitBuf = (int)(val >> requiredBits); + bitCnt -= requiredBits; + *oval = (int)(val & ((1L << requiredBits) - 1)); + + return true; +} + + +/** + * + */ +int Inflater::doDecode(Huffman *h) +{ + int bitTmp = bitBuf; + int left = bitCnt; + int code = 0; + int first = 0; + int index = 0; + int len = 1; + int *next = h->count + 1; + while (true) + { + while (left--) + { + code |= bitTmp & 1; + bitTmp >>= 1; + int count = *next++; + if (code < first + count) + { /* if length len, return symbol */ + bitBuf = bitTmp; + bitCnt = (bitCnt - len) & 7; + return h->symbol[index + (code - first)]; + } + index += count; + first += count; + first <<= 1; + code <<= 1; + len++; + } + left = (MAXBITS+1) - len; + if (left == 0) + break; + if (srcPos >= src.size()) + { + error("premature end of input"); + dump(); + return -1; + } + bitTmp = src[srcPos++]; + if (left > 8) + left = 8; + } + + error("no end of block found"); + return -1; +} + +/** + * + */ +bool Inflater::doCodes(Huffman *lencode, Huffman *distcode) +{ + static const int lens[29] = { // Size base for length codes 257..285 + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; + static const int lext[29] = { // Extra bits for length codes 257..285 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const int dists[30] = { // Offset base for distance codes 0..29 + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577}; + static const int dext[30] = { // Extra bits for distance codes 0..29 + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13}; + + //decode literals and length/distance pairs + while (true) + { + int symbol = doDecode(lencode); + if (symbol == 256) + break; + if (symbol < 0) + { + return false; + } + if (symbol < 256) //literal + { + dest.push_back(symbol); + } + else if (symbol > 256)//length + { + symbol -= 257; + if (symbol >= 29) + { + error("invalid fixed code"); + return false; + } + int ret; + if (!getBits(lext[symbol], &ret)) + return false; + int len = lens[symbol] + ret; + + symbol = doDecode(distcode);//distance + if (symbol < 0) + { + return false; + } + + if (!getBits(dext[symbol], &ret)) + return false; + unsigned int dist = dists[symbol] + ret; + if (dist > dest.size()) + { + error("distance too far back %d/%d", dist, dest.size()); + dump(); + //printf("pos:%d\n", srcPos); + return false; + } + + // copy length bytes from distance bytes back + //dest.push_back('{'); + while (len--) + { + dest.push_back(dest[dest.size() - dist]); + } + //dest.push_back('}'); + + } + } + + return true; +} + +/** + */ +bool Inflater::doStored() +{ + //trace("### stored ###"); + + // clear bits from current byte + bitBuf = 0; + bitCnt = 0; + + // length + if (srcPos + 4 > src.size()) + { + error("not enough input"); + return false; + } + + int len = src[srcPos++]; + len |= src[srcPos++] << 8; + //trace("### len:%d", len); + // check complement + if (src[srcPos++] != (~len & 0xff) || + src[srcPos++] != ((~len >> 8) & 0xff)) + { + error("twos complement for storage size do not match"); + return false; + } + + // copy data + if (srcPos + len > src.size()) + { + error("Not enough input for stored block"); + return false; + } + while (len--) + dest.push_back(src[srcPos++]); + + return true; +} + +/** + */ +bool Inflater::doFixed() +{ + //trace("### fixed ###"); + + static bool firstTime = true; + static int lencnt[MAXBITS+1], lensym[FIXLCODES]; + static int distcnt[MAXBITS+1], distsym[MAXDCODES]; + static Huffman lencode = {lencnt, lensym}; + static Huffman distcode = {distcnt, distsym}; + + if (firstTime) + { + firstTime = false; + + int lengths[FIXLCODES]; + + // literal/length table + int symbol = 0; + for ( ; symbol < 144; symbol++) + lengths[symbol] = 8; + for ( ; symbol < 256; symbol++) + lengths[symbol] = 9; + for ( ; symbol < 280; symbol++) + lengths[symbol] = 7; + for ( ; symbol < FIXLCODES; symbol++) + lengths[symbol] = 8; + buildHuffman(&lencode, lengths, FIXLCODES); + + // distance table + for (int symbol = 0; symbol < MAXDCODES; symbol++) + lengths[symbol] = 5; + buildHuffman(&distcode, lengths, MAXDCODES); + } + + // decode data until end-of-block code + bool ret = doCodes(&lencode, &distcode); + return ret; +} + +/** + */ +bool Inflater::doDynamic() +{ + //trace("### dynamic ###"); + int lengths[MAXCODES]; // descriptor code lengths + int lencnt[MAXBITS+1], lensym[MAXLCODES]; // lencode memory + int distcnt[MAXBITS+1], distsym[MAXDCODES]; // distcode memory + Huffman lencode = {lencnt, lensym}; // length code + Huffman distcode = {distcnt, distsym}; // distance code + static const int order[19] = // permutation of code length codes + {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + + // get number of lengths in each table, check lengths + int ret; + if (!getBits(5, &ret)) + return false; + int nlen = ret + 257; + if (!getBits(5, &ret)) + return false; + int ndist = ret + 1; + if (!getBits(4, &ret)) + return false; + int ncode = ret + 4; + if (nlen > MAXLCODES || ndist > MAXDCODES) + { + error("Bad codes"); + return false; + } + + // get code length code lengths + int index = 0; + for ( ; index < ncode; index++) + { + if (!getBits(3, &ret)) + return false; + lengths[order[index]] = ret; + } + for ( ; index < 19; index++) + lengths[order[index]] = 0; + + // build huffman table for code lengths codes + if (buildHuffman(&lencode, lengths, 19) != 0) + return false; + + // read length/literal and distance code length tables + index = 0; + while (index < nlen + ndist) + { + int symbol = doDecode(&lencode); + if (symbol < 16) // length in 0..15 + lengths[index++] = symbol; + else + { // repeat instruction + int len = 0; // assume repeating zeros + if (symbol == 16) + { // repeat last length 3..6 times + if (index == 0) + { + error("no last length"); + return false; + } + len = lengths[index - 1];// last length + if (!getBits(2, &ret)) + return false; + symbol = 3 + ret; + } + else if (symbol == 17) // repeat zero 3..10 times + { + if (!getBits(3, &ret)) + return false; + symbol = 3 + ret; + } + else // == 18, repeat zero 11..138 times + { + if (!getBits(7, &ret)) + return false; + symbol = 11 + ret; + } + if (index + symbol > nlen + ndist) + { + error("too many lengths"); + return false; + } + while (symbol--) // repeat last or zero symbol times + lengths[index++] = len; + } + } + + // build huffman table for literal/length codes + int err = buildHuffman(&lencode, lengths, nlen); + if (err < 0 || (err > 0 && nlen - lencode.count[0] != 1)) + { + error("incomplete length codes"); + //return false; + } + // build huffman table for distance codes + err = buildHuffman(&distcode, lengths + nlen, ndist); + if (err < 0 || (err > 0 && nlen - lencode.count[0] != 1)) + { + error("incomplete dist codes"); + return false; + } + + // decode data until end-of-block code + bool retn = doCodes(&lencode, &distcode); + return retn; +} + +/** + */ +bool Inflater::inflate(std::vector<unsigned char> &destination, + std::vector<unsigned char> &source) +{ + dest.clear(); + src = source; + srcPos = 0; + bitBuf = 0; + bitCnt = 0; + + while (true) + { + int last; // one if last block + if (!getBits(1, &last)) + return false; + int type; // block type 0..3 + if (!getBits(2, &type)) + return false; + switch (type) + { + case 0: + if (!doStored()) + return false; + break; + case 1: + if (!doFixed()) + return false; + break; + case 2: + if (!doDynamic()) + return false; + break; + default: + error("Unknown block type %d", type); + return false; + } + if (last) + break; + } + + destination = dest; + + return true; +} + + + + + + +//######################################################################## +//# D E F L A T E R +//######################################################################## + + +#define DEFLATER_BUF_SIZE 32768 +class Deflater +{ +public: + + /** + * + */ + Deflater(); + + /** + * + */ + virtual ~Deflater(); + + /** + * + */ + virtual void reset(); + + /** + * + */ + virtual bool update(int ch); + + /** + * + */ + virtual bool finish(); + + /** + * + */ + virtual std::vector<unsigned char> &getCompressed(); + + /** + * + */ + bool deflate(std::vector<unsigned char> &dest, + const std::vector<unsigned char> &src); + + void encodeDistStatic(unsigned int len, unsigned int dist); + +private: + + //debug messages + void error(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + void trace(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + bool compressWindow(); + + bool compress(); + + std::vector<unsigned char> compressed; + + std::vector<unsigned char> uncompressed; + + std::vector<unsigned char> window; + + unsigned int windowPos; + + //#### Output + unsigned int outputBitBuf; + unsigned int outputNrBits; + + void put(int ch); + + void putWord(int ch); + + void putFlush(); + + void putBits(unsigned int ch, unsigned int bitsWanted); + + void putBitsR(unsigned int ch, unsigned int bitsWanted); + + //#### Huffman Encode + void encodeLiteralStatic(unsigned int ch); + + unsigned char windowBuf[DEFLATER_BUF_SIZE]; + //assume 32-bit ints + unsigned int windowHashBuf[DEFLATER_BUF_SIZE]; +}; + + +//######################################################################## +//# A P I +//######################################################################## + + +/** + * + */ +Deflater::Deflater() +{ + reset(); +} + +/** + * + */ +Deflater::~Deflater() += default; + +/** + * + */ +void Deflater::reset() +{ + compressed.clear(); + uncompressed.clear(); + window.clear(); + windowPos = 0; + outputBitBuf = 0; + outputNrBits = 0; + for (int k=0; k<DEFLATER_BUF_SIZE; k++) + { + windowBuf[k]=0; + windowHashBuf[k]=0; + } +} + +/** + * + */ +bool Deflater::update(int ch) +{ + uncompressed.push_back((unsigned char)(ch & 0xff)); + return true; +} + +/** + * + */ +bool Deflater::finish() +{ + return compress(); +} + +/** + * + */ +std::vector<unsigned char> &Deflater::getCompressed() +{ + return compressed; +} + + +/** + * + */ +bool Deflater::deflate(std::vector<unsigned char> &dest, + const std::vector<unsigned char> &src) +{ + reset(); + uncompressed = src; + if (!compress()) + return false; + dest = compressed; + return true; +} + + + + + + + +//######################################################################## +//# W O R K I N G C O D E +//######################################################################## + + +//############################# +//# M E S S A G E S +//############################# + +/** + * Print error messages + */ +void Deflater::error(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "Deflater error:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +/** + * Print trace messages + */ +void Deflater::trace(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "Deflater:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + + + + +//############################# +//# O U T P U T +//############################# + +/** + * + */ +void Deflater::put(int ch) +{ + compressed.push_back(ch); + outputBitBuf = 0; + outputNrBits = 0; +} + +/** + * + */ +void Deflater::putWord(int ch) +{ + int lo = (ch ) & 0xff; + int hi = (ch>>8) & 0xff; + put(lo); + put(hi); +} + +/** + * + */ +void Deflater::putFlush() +{ + if (outputNrBits > 0) + { + put(outputBitBuf & 0xff); + } + outputBitBuf = 0; + outputNrBits = 0; +} + +/** + * + */ +void Deflater::putBits(unsigned int ch, unsigned int bitsWanted) +{ + //trace("n:%4u, %d\n", ch, bitsWanted); + + while (bitsWanted--) + { + //add bits to position 7. shift right + outputBitBuf = (outputBitBuf>>1) + (ch<<7 & 0x80); + ch >>= 1; + outputNrBits++; + if (outputNrBits >= 8) + { + unsigned char b = outputBitBuf & 0xff; + //printf("b:%02x\n", b); + put(b); + } + } +} + +static unsigned int bitReverse(unsigned int code, unsigned int nrBits) +{ + unsigned int outb = 0; + while (nrBits--) + { + outb = (outb << 1) | (code & 0x01); + code >>= 1; + } + return outb; +} + + +/** + * + */ +void Deflater::putBitsR(unsigned int ch, unsigned int bitsWanted) +{ + //trace("r:%4u, %d", ch, bitsWanted); + + unsigned int rcode = bitReverse(ch, bitsWanted); + + putBits(rcode, bitsWanted); + +} + + +//############################# +//# E N C O D E +//############################# + + + +void Deflater::encodeLiteralStatic(unsigned int ch) +{ + //trace("c: %d", ch); + + if (ch < 144) + { + putBitsR(ch + 0x0030 , 8); // 00110000 + } + else if (ch < 256) + { + putBitsR(ch - 144 + 0x0190 , 9); // 110010000 + } + else if (ch < 280) + { + putBitsR(ch - 256 + 0x0000 , 7); // 0000000 + } + else if (ch < 288) + { + putBitsR(ch - 280 + 0x00c0 , 8); // 11000000 + } + else //out of range + { + error("Literal out of range: %d", ch); + } + +} + + +struct LenBase +{ + unsigned int base; + unsigned int range; + unsigned int bits; +}; + +LenBase lenBases[] = +{ + { 3, 1, 0 }, + { 4, 1, 0 }, + { 5, 1, 0 }, + { 6, 1, 0 }, + { 7, 1, 0 }, + { 8, 1, 0 }, + { 9, 1, 0 }, + { 10, 1, 0 }, + { 11, 2, 1 }, + { 13, 2, 1 }, + { 15, 2, 1 }, + { 17, 2, 1 }, + { 19, 4, 2 }, + { 23, 4, 2 }, + { 27, 4, 2 }, + { 31, 4, 2 }, + { 35, 8, 3 }, + { 43, 8, 3 }, + { 51, 8, 3 }, + { 59, 8, 3 }, + { 67, 16, 4 }, + { 83, 16, 4 }, + { 99, 16, 4 }, + { 115, 16, 4 }, + { 131, 32, 5 }, + { 163, 32, 5 }, + { 195, 32, 5 }, + { 227, 32, 5 }, + { 258, 1, 0 } +}; + +struct DistBase +{ + unsigned int base; + unsigned int range; + unsigned int bits; +}; + +DistBase distBases[] = +{ + { 1, 1, 0 }, + { 2, 1, 0 }, + { 3, 1, 0 }, + { 4, 1, 0 }, + { 5, 2, 1 }, + { 7, 2, 1 }, + { 9, 4, 2 }, + { 13, 4, 2 }, + { 17, 8, 3 }, + { 25, 8, 3 }, + { 33, 16, 4 }, + { 49, 16, 4 }, + { 65, 32, 5 }, + { 97, 32, 5 }, + { 129, 64, 6 }, + { 193, 64, 6 }, + { 257, 128, 7 }, + { 385, 128, 7 }, + { 513, 256, 8 }, + { 769, 256, 8 }, + { 1025, 512, 9 }, + { 1537, 512, 9 }, + { 2049, 1024, 10 }, + { 3073, 1024, 10 }, + { 4097, 2048, 11 }, + { 6145, 2048, 11 }, + { 8193, 4096, 12 }, + { 12289, 4096, 12 }, + { 16385, 8192, 13 }, + { 24577, 8192, 13 } +}; + +void Deflater::encodeDistStatic(unsigned int len, unsigned int dist) +{ + + //## Output length + + if (len < 3 || len > 258) + { + error("Length out of range:%d", len); + return; + } + + bool found = false; + for (int i=0 ; i<29 ; i++) + { + unsigned int base = lenBases[i].base; + unsigned int range = lenBases[i].range; + if (base + range > len) + { + unsigned int lenCode = 257 + i; + unsigned int length = len - base; + //trace("--- %d %d %d %d", len, base, range, length); + encodeLiteralStatic(lenCode); + putBits(length, lenBases[i].bits); + found = true; + break; + } + } + if (!found) + { + error("Length not found in table:%d", len); + return; + } + + //## Output distance + + if (dist < 4 || dist > 32768) + { + error("Distance out of range:%d", dist); + return; + } + + found = false; + for (int i=0 ; i<30 ; i++) + { + unsigned int base = distBases[i].base; + unsigned int range = distBases[i].range; + if (base + range > dist) + { + unsigned int distCode = i; + unsigned int distance = dist - base; + //error("--- %d %d %d %d", dist, base, range, distance); + putBitsR(distCode, 5); + putBits(distance, distBases[i].bits); + found = true; + break; + } + } + if (!found) + { + error("Distance not found in table:%d", dist); + return; + } +} + + +//############################# +//# C O M P R E S S +//############################# + + +/** + * This method does the dirty work of dictionary + * compression. Basically it looks for redundant + * strings and has the current duplicate refer back + * to the previous one. + */ +bool Deflater::compressWindow() +{ + windowPos = 0; + unsigned int windowSize = window.size(); + //### Compress as much of the window as possible + + unsigned int hash = 0; + //Have each value be a long with the byte at this position, + //plus the 3 bytes after it in the window + for (int i=windowSize-1 ; i>=0 ; i--) + { + unsigned char ch = window[i]; + windowBuf[i] = ch; + hash = ((hash<<8) & 0xffffff00) | ch; + windowHashBuf[i] = hash; + } + + while (windowPos < windowSize - 3) + { + //### Find best match, if any + unsigned int bestMatchLen = 0; + unsigned int bestMatchDist = 0; + if (windowPos >= 4) + { + for (unsigned int lookBack=0 ; lookBack<windowPos-4 ; lookBack++) + { + //Check 4-char hashes first, before continuing with string + if (windowHashBuf[lookBack] == windowHashBuf[windowPos]) + { + unsigned int lookAhead=4; + unsigned int lookAheadMax = windowSize - 4 - windowPos; + if (lookBack + lookAheadMax >= windowPos -4 ) + lookAheadMax = windowPos - 4 - lookBack; + if (lookAheadMax > 258) + lookAheadMax = 258; + unsigned char *wp = &(windowBuf[windowPos+4]); + unsigned char *lb = &(windowBuf[lookBack+4]); + while (lookAhead<lookAheadMax) + { + if (*lb++ != *wp++) + break; + lookAhead++; + } + if (lookAhead > bestMatchLen) + { + bestMatchLen = lookAhead; + bestMatchDist = windowPos - lookBack; + } + } + } + } + if (bestMatchLen > 3) + { + //Distance encode + //trace("### distance"); + /* + printf("### 1 '"); + for (int i=0 ; i < bestMatchLen ; i++) + fputc(window[windowPos+i], stdout); + printf("'\n### 2 '"); + for (int i=0 ; i < bestMatchLen ; i++) + fputc(window[windowPos-bestMatchDist+i], stdout); + printf("'\n"); + */ + encodeDistStatic(bestMatchLen, bestMatchDist); + windowPos += bestMatchLen; + } + else + { + //Literal encode + //trace("### literal"); + encodeLiteralStatic(windowBuf[windowPos]); + windowPos++; + } + } + + while (windowPos < windowSize) + encodeLiteralStatic(windowBuf[windowPos++]); + + encodeLiteralStatic(256); + return true; +} + + +/** + * + */ +bool Deflater::compress() +{ + //trace("compress"); + unsigned long total = 0L; + windowPos = 0; + std::vector<unsigned char>::iterator iter; + for (iter = uncompressed.begin(); iter != uncompressed.end() ; ) + { + total += windowPos; + trace("total:%ld", total); + if (windowPos > window.size()) + windowPos = window.size(); + window.erase(window.begin() , window.begin()+windowPos); + while (window.size() < 32768 && iter != uncompressed.end()) + { + window.push_back(*iter); + ++iter; + } + if (window.size() >= 32768) + putBits(0x00, 1); //0 -- more blocks + else + putBits(0x01, 1); //1 -- last block + putBits(0x01, 2); //01 -- static trees + if (!compressWindow()) + return false; + } + putFlush(); + return true; +} + + + + + +//######################################################################## +//# G Z I P F I L E +//######################################################################## + +/** + * Constructor + */ +GzipFile::GzipFile() : + data(), + fileName(), + fileBuf(), + fileBufPos(0), + compressionMethod(0) +{ +} + +/** + * Destructor + */ +GzipFile::~GzipFile() += default; + +/** + * Print error messages + */ +void GzipFile::error(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "GzipFile error:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +/** + * Print trace messages + */ +void GzipFile::trace(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "GzipFile:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +/** + * + */ +void GzipFile::put(unsigned char ch) +{ + data.push_back(ch); +} + +/** + * + */ +void GzipFile::setData(const std::vector<unsigned char> &str) +{ + data = str; +} + +/** + * + */ +void GzipFile::clearData() +{ + data.clear(); +} + +/** + * + */ +std::vector<unsigned char> &GzipFile::getData() +{ + return data; +} + +/** + * + */ +std::string &GzipFile::getFileName() +{ + return fileName; +} + +/** + * + */ +void GzipFile::setFileName(const std::string &val) +{ + fileName = val; +} + + + +//##################################### +//# U T I L I T Y +//##################################### + +/** + * Loads a new file into an existing GzipFile + */ +bool GzipFile::loadFile(const std::string &fName) +{ + FILE *f = fopen(fName.c_str() , "rb"); + if (!f) + { + error("Cannot open file %s", fName.c_str()); + return false; + } + while (true) + { + int ch = fgetc(f); + if (ch < 0) + break; + data.push_back(ch); + } + fclose(f); + setFileName(fName); + return true; +} + + + +//##################################### +//# W R I T E +//##################################### + +/** + * + */ +bool GzipFile::putByte(unsigned char ch) +{ + fileBuf.push_back(ch); + return true; +} + + + +/** + * + */ +bool GzipFile::putLong(unsigned long val) +{ + fileBuf.push_back( (unsigned char)((val ) & 0xff)); + fileBuf.push_back( (unsigned char)((val>> 8) & 0xff)); + fileBuf.push_back( (unsigned char)((val>>16) & 0xff)); + fileBuf.push_back( (unsigned char)((val>>24) & 0xff)); + return true; +} + + + +/** + * + */ +bool GzipFile::write() +{ + fileBuf.clear(); + + putByte(0x1f); //magic + putByte(0x8b); //magic + putByte( 8); //compression method + putByte(0x08); //flags. say we have a crc and file name + + unsigned long ltime = (unsigned long) time(nullptr); + putLong(ltime); + + //xfl + putByte(0); + //OS + putByte(0); + + //file name + for (char i : fileName) + putByte(i); + putByte(0); + + + //compress + std::vector<unsigned char> compBuf; + Deflater deflater; + if (!deflater.deflate(compBuf, data)) + { + return false; + } + + std::vector<unsigned char>::iterator iter; + for (iter=compBuf.begin() ; iter!=compBuf.end() ; ++iter) + { + unsigned char ch = *iter; + putByte(ch); + } + + Crc32 crcEngine; + crcEngine.update(data); + unsigned long crc = crcEngine.getValue(); + putLong(crc); + + putLong(data.size()); + + return true; +} + + +/** + * + */ +bool GzipFile::writeBuffer(std::vector<unsigned char> &outBuf) +{ + if (!write()) + return false; + outBuf.clear(); + outBuf = fileBuf; + return true; +} + + +/** + * + */ +bool GzipFile::writeFile(const std::string &fileName) +{ + if (!write()) + return false; + FILE *f = fopen(fileName.c_str(), "wb"); + if (!f) + return false; + std::vector<unsigned char>::iterator iter; + for (iter=fileBuf.begin() ; iter!=fileBuf.end() ; ++iter) + { + unsigned char ch = *iter; + fputc(ch, f); + } + fclose(f); + return true; +} + + +//##################################### +//# R E A D +//##################################### + +bool GzipFile::getByte(unsigned char *ch) +{ + if (fileBufPos >= fileBuf.size()) + { + error("unexpected end of data"); + return false; + } + *ch = fileBuf[fileBufPos++]; + return true; +} + +/** + * + */ +bool GzipFile::getLong(unsigned long *val) +{ + if (fileBuf.size() - fileBufPos < 4) + return false; + int ch1 = fileBuf[fileBufPos++]; + int ch2 = fileBuf[fileBufPos++]; + int ch3 = fileBuf[fileBufPos++]; + int ch4 = fileBuf[fileBufPos++]; + *val = ((ch4<<24) & 0xff000000L) | + ((ch3<<16) & 0x00ff0000L) | + ((ch2<< 8) & 0x0000ff00L) | + ((ch1 ) & 0x000000ffL); + return true; +} + +bool GzipFile::read() +{ + fileBufPos = 0; + + unsigned char ch; + + //magic cookie + if (!getByte(&ch)) + return false; + if (ch != 0x1f) + { + error("bad gzip header"); + return false; + } + if (!getByte(&ch)) + return false; + if (ch != 0x8b) + { + error("bad gzip header"); + return false; + } + + //## compression method + if (!getByte(&ch)) + return false; + compressionMethod = ch & 0xff; + + //## flags + if (!getByte(&ch)) + return false; + //bool ftext = ch & 0x01; + bool fhcrc = ch & 0x02; + bool fextra = ch & 0x04; + bool fname = ch & 0x08; + bool fcomment = ch & 0x10; + + //trace("cm:%d ftext:%d fhcrc:%d fextra:%d fname:%d fcomment:%d", + // cm, ftext, fhcrc, fextra, fname, fcomment); + + //## file time + unsigned long ltime; + if (!getLong(<ime)) + return false; + //time_t mtime = (time_t)ltime; + + //## XFL + if (!getByte(&ch)) + return false; + //int xfl = ch; + + //## OS + if (!getByte(&ch)) + return false; + //int os = ch; + + //std::string timestr = ctime(&mtime); + //trace("xfl:%d os:%d mtime:%s", xfl, os, timestr.c_str()); + + if (fextra) + { + if (!getByte(&ch)) + return false; + long xlen = ch; + if (!getByte(&ch)) + return false; + xlen = (xlen << 8) + ch; + for (long l=0 ; l<xlen ; l++) + { + if (!getByte(&ch)) + return false; + } + } + + if (fname) + { + fileName = ""; + while (true) + { + if (!getByte(&ch)) + return false; + if (ch==0) + break; + fileName.push_back(ch); + } + } + + if (fcomment) + { + while (true) + { + if (!getByte(&ch)) + return false; + if (ch==0) + break; + } + } + + if (fhcrc) + { + if (!getByte(&ch)) + return false; + if (!getByte(&ch)) + return false; + } + + //read remainder of stream + //compressed data runs up until 8 bytes before end of buffer + std::vector<unsigned char> compBuf; + while (fileBufPos < fileBuf.size() - 8) + { + if (!getByte(&ch)) + return false; + compBuf.push_back(ch); + } + //uncompress + data.clear(); + Inflater inflater; + if (!inflater.inflate(data, compBuf)) + { + return false; + } + + //Get the CRC and compare + Crc32 crcEngine; + crcEngine.update(data); + unsigned long calcCrc = crcEngine.getValue(); + unsigned long givenCrc; + if (!getLong(&givenCrc)) + return false; + if (givenCrc != calcCrc) + { + error("Specified crc, %ud, not what received: %ud", + givenCrc, calcCrc); + return false; + } + + //Get the file size and compare + unsigned long givenFileSize; + if (!getLong(&givenFileSize)) + return false; + if (givenFileSize != data.size()) + { + error("Specified data size, %ld, not what received: %ld", + givenFileSize, data.size()); + return false; + } + + return true; +} + + + +/** + * + */ +bool GzipFile::readBuffer(const std::vector<unsigned char> &inbuf) +{ + fileBuf = inbuf; + if (!read()) + return false; + return true; +} + + +/** + * + */ +bool GzipFile::readFile(const std::string &fileName) +{ + fileBuf.clear(); + FILE *f = fopen(fileName.c_str(), "rb"); + if (!f) + return false; + while (true) + { + int ch = fgetc(f); + if (ch < 0) + break; + fileBuf.push_back(ch); + } + fclose(f); + if (!read()) + return false; + return true; +} + + + + + + + + +//######################################################################## +//# Z I P F I L E +//######################################################################## + +/** + * Constructor + */ +ZipEntry::ZipEntry() : + crc (0L), + fileName (), + comment (), + compressionMethod (8), + compressedData (), + uncompressedData (), + position (0) +{ +} + +/** + * + */ +ZipEntry::ZipEntry(std::string fileNameArg, + std::string commentArg) : + crc (0L), + fileName (std::move(fileNameArg)), + comment (std::move(commentArg)), + compressionMethod (8), + compressedData (), + uncompressedData (), + position (0) +{ +} + +/** + * Destructor + */ +ZipEntry::~ZipEntry() += default; + + +/** + * + */ +std::string ZipEntry::getFileName() +{ + return fileName; +} + +/** + * + */ +void ZipEntry::setFileName(const std::string &val) +{ + fileName = val; +} + +/** + * + */ +std::string ZipEntry::getComment() +{ + return comment; +} + +/** + * + */ +void ZipEntry::setComment(const std::string &val) +{ + comment = val; +} + +/** + * + */ +unsigned long ZipEntry::getCompressedSize() +{ + return (unsigned long)compressedData.size(); +} + +/** + * + */ +int ZipEntry::getCompressionMethod() +{ + return compressionMethod; +} + +/** + * + */ +void ZipEntry::setCompressionMethod(int val) +{ + compressionMethod = val; +} + +/** + * + */ +std::vector<unsigned char> &ZipEntry::getCompressedData() +{ + return compressedData; +} + +/** + * + */ +void ZipEntry::setCompressedData(const std::vector<unsigned char> &val) +{ + compressedData = val; +} + +/** + * + */ +unsigned long ZipEntry::getUncompressedSize() +{ + return (unsigned long)uncompressedData.size(); +} + +/** + * + */ +std::vector<unsigned char> &ZipEntry::getUncompressedData() +{ + return uncompressedData; +} + +/** + * + */ +void ZipEntry::setUncompressedData(const std::vector<unsigned char> &val) +{ + uncompressedData = val; +} + +void ZipEntry::setUncompressedData(const std::string &s) +{ + uncompressedData.clear(); + uncompressedData.reserve(s.size()); + uncompressedData.insert(uncompressedData.begin(), s.begin(), s.end()); +} + +/** + * + */ +unsigned long ZipEntry::getCrc() +{ + return crc; +} + +/** + * + */ +void ZipEntry::setCrc(unsigned long val) +{ + crc = val; +} + +/** + * + */ +void ZipEntry::write(unsigned char ch) +{ + uncompressedData.push_back(ch); +} + +/** + * + */ +void ZipEntry::finish() +{ + Crc32 c32; + std::vector<unsigned char>::iterator iter; + for (iter = uncompressedData.begin() ; + iter!= uncompressedData.end() ; ++iter) + { + unsigned char ch = *iter; + c32.update(ch); + } + crc = c32.getValue(); + switch (compressionMethod) + { + case 0: //none + { + for (iter = uncompressedData.begin() ; + iter!= uncompressedData.end() ; ++iter) + { + unsigned char ch = *iter; + compressedData.push_back(ch); + } + break; + } + case 8: //deflate + { + Deflater deflater; + if (!deflater.deflate(compressedData, uncompressedData)) + { + //some error + } + break; + } + default: + { + printf("error: unknown compression method %d\n", + compressionMethod); + } + } +} + + + + +/** + * + */ +bool ZipEntry::readFile(const std::string &fileNameArg, + const std::string &commentArg) +{ + crc = 0L; + uncompressedData.clear(); + fileName = fileNameArg; + comment = commentArg; + FILE *f = fopen(fileName.c_str(), "rb"); + if (!f) + { + return false; + } + while (true) + { + int ch = fgetc(f); + if (ch < 0) + break; + uncompressedData.push_back((unsigned char)ch); + } + fclose(f); + finish(); + return true; +} + + +/** + * + */ +void ZipEntry::setPosition(unsigned long val) +{ + position = val; +} + +/** + * + */ +unsigned long ZipEntry::getPosition() +{ + return position; +} + + + + + + + +/** + * Constructor + */ +ZipFile::ZipFile() : + entries(), + fileBuf(), + fileBufPos(0), + comment() +{ +} + +/** + * Destructor + */ +ZipFile::~ZipFile() +{ + std::vector<ZipEntry *>::iterator iter; + for (iter=entries.begin() ; iter!=entries.end() ; ++iter) + { + ZipEntry *entry = *iter; + delete entry; + } + entries.clear(); +} + +/** + * + */ +void ZipFile::setComment(const std::string &val) +{ + comment = val; +} + +/** + * + */ +std::string ZipFile::getComment() +{ + return comment; +} + + +/** + * + */ +std::vector<ZipEntry *> &ZipFile::getEntries() +{ + return entries; +} + + + +//##################################### +//# M E S S A G E S +//##################################### + +void ZipFile::error(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "ZipFile error:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +void ZipFile::trace(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fprintf(stdout, "ZipFile:"); + vfprintf(stdout, fmt, args); + fprintf(stdout, "\n"); + va_end(args); +} + +//##################################### +//# U T I L I T Y +//##################################### + +/** + * + */ +ZipEntry *ZipFile::addFile(const std::string &fileName, + const std::string &comment) +{ + ZipEntry *ze = new ZipEntry(); + if (!ze->readFile(fileName, comment)) + { + delete ze; + return nullptr; + } + entries.push_back(ze); + return ze; +} + + +/** + * + */ +ZipEntry *ZipFile::newEntry(const std::string &fileName, + const std::string &comment) +{ + ZipEntry *ze = new ZipEntry(fileName, comment); + entries.push_back(ze); + return ze; +} + + +//##################################### +//# W R I T E +//##################################### + +/** + * + */ +bool ZipFile::putLong(unsigned long val) +{ + fileBuf.push_back( ((int)(val )) & 0xff); + fileBuf.push_back( ((int)(val>> 8)) & 0xff); + fileBuf.push_back( ((int)(val>>16)) & 0xff); + fileBuf.push_back( ((int)(val>>24)) & 0xff); + return true; +} + + +/** + * + */ +bool ZipFile::putInt(unsigned int val) +{ + fileBuf.push_back( (val ) & 0xff); + fileBuf.push_back( (val>> 8) & 0xff); + return true; +} + +/** + * + */ +bool ZipFile::putByte(unsigned char val) +{ + fileBuf.push_back(val); + return true; +} + +/** + * + */ +bool ZipFile::writeFileData() +{ + std::vector<ZipEntry *>::iterator iter; + for (iter = entries.begin() ; iter != entries.end() ; ++iter) + { + ZipEntry *entry = *iter; + entry->setPosition(fileBuf.size()); + //##### HEADER + std::string fname = entry->getFileName(); + putLong(0x04034b50L); + putInt(20); //versionNeeded + putInt(0); //gpBitFlag + //putInt(0); //compression method + putInt(entry->getCompressionMethod()); //compression method + putInt(0); //mod time + putInt(0); //mod date + putLong(entry->getCrc()); //crc32 + putLong(entry->getCompressedSize()); + putLong(entry->getUncompressedSize()); + putInt(fname.size());//fileName length + putInt(8);//extra field length + //file name + for (char i : fname) + putByte((unsigned char)i); + //extra field + putInt(0x7855); + putInt(4); + putInt(100); + putInt(100); + + //##### DATA + std::vector<unsigned char> &buf = entry->getCompressedData(); + std::vector<unsigned char>::iterator iter; + for (iter = buf.begin() ; iter != buf.end() ; ++iter) + { + unsigned char ch = (unsigned char) *iter; + putByte(ch); + } + } + return true; +} + +/** + * + */ +bool ZipFile::writeCentralDirectory() +{ + unsigned long cdPosition = fileBuf.size(); + std::vector<ZipEntry *>::iterator iter; + for (iter = entries.begin() ; iter != entries.end() ; ++iter) + { + ZipEntry *entry = *iter; + std::string fname = entry->getFileName(); + std::string ecomment = entry->getComment(); + putLong(0x02014b50L); //magic cookie + putInt(2386); //versionMadeBy + putInt(20); //versionNeeded + putInt(0); //gpBitFlag + putInt(entry->getCompressionMethod()); //compression method + putInt(0); //mod time + putInt(0); //mod date + putLong(entry->getCrc()); //crc32 + putLong(entry->getCompressedSize()); + putLong(entry->getUncompressedSize()); + putInt(fname.size());//fileName length + putInt(4);//extra field length + putInt(ecomment.size());//comment length + putInt(0); //disk number start + putInt(0); //internal attributes + putLong(0); //external attributes + putLong(entry->getPosition()); + + //file name + for (char i : fname) + putByte((unsigned char)i); + //extra field + putInt(0x7855); + putInt(0); + //comment + for (char i : ecomment) + putByte((unsigned char)i); + } + unsigned long cdSize = fileBuf.size() - cdPosition; + + putLong(0x06054b50L); + putInt(0);//number of this disk + putInt(0);//nr of disk with central dir + putInt(entries.size()); //number of entries on this disk + putInt(entries.size()); //number of entries total + putLong(cdSize); //size of central dir + putLong(cdPosition); //position of central dir + putInt(comment.size());//comment size + for (char i : comment) + putByte(i); + return true; +} + + + +/** + * + */ +bool ZipFile::write() +{ + fileBuf.clear(); + if (!writeFileData()) + return false; + if (!writeCentralDirectory()) + return false; + return true; +} + + +/** + * + */ +bool ZipFile::writeBuffer(std::vector<unsigned char> &outBuf) +{ + if (!write()) + return false; + outBuf.clear(); + outBuf = fileBuf; + return true; +} + + +/** + * + */ +bool ZipFile::writeFile(const std::string &fileName) +{ + if (!write()) + return false; + FILE *f = fopen(fileName.c_str(), "wb"); + if (!f) + return false; + std::vector<unsigned char>::iterator iter; + for (iter=fileBuf.begin() ; iter!=fileBuf.end() ; ++iter) + { + unsigned char ch = *iter; + fputc(ch, f); + } + fclose(f); + return true; +} + +//##################################### +//# R E A D +//##################################### + +/** + * + */ +bool ZipFile::getLong(unsigned long *val) +{ + if (fileBuf.size() - fileBufPos < 4) + return false; + int ch1 = fileBuf[fileBufPos++]; + int ch2 = fileBuf[fileBufPos++]; + int ch3 = fileBuf[fileBufPos++]; + int ch4 = fileBuf[fileBufPos++]; + *val = ((ch4<<24) & 0xff000000L) | + ((ch3<<16) & 0x00ff0000L) | + ((ch2<< 8) & 0x0000ff00L) | + ((ch1 ) & 0x000000ffL); + return true; +} + +/** + * + */ +bool ZipFile::getInt(unsigned int *val) +{ + if (fileBuf.size() - fileBufPos < 2) + return false; + int ch1 = fileBuf[fileBufPos++]; + int ch2 = fileBuf[fileBufPos++]; + *val = ((ch2<< 8) & 0xff00) | + ((ch1 ) & 0x00ff); + return true; +} + + +/** + * + */ +bool ZipFile::getByte(unsigned char *val) +{ + if (fileBuf.size() <= fileBufPos) + return false; + *val = fileBuf[fileBufPos++]; + return true; +} + + +/** + * + */ +bool ZipFile::readFileData() +{ + //printf("#################################################\n"); + //printf("###D A T A\n"); + //printf("#################################################\n"); + while (true) + { + unsigned long magicCookie; + if (!getLong(&magicCookie)) + { + error("magic cookie not found"); + break; + } + trace("###Cookie:%lx", magicCookie); + if (magicCookie == 0x02014b50L) //central directory + break; + if (magicCookie != 0x04034b50L) + { + error("file header not found"); + return false; + } + unsigned int versionNeeded; + if (!getInt(&versionNeeded)) + { + error("bad version needed found"); + return false; + } + unsigned int gpBitFlag; + if (!getInt(&gpBitFlag)) + { + error("bad bit flag found"); + return false; + } + unsigned int compressionMethod; + if (!getInt(&compressionMethod)) + { + error("bad compressionMethod found"); + return false; + } + unsigned int modTime; + if (!getInt(&modTime)) + { + error("bad modTime found"); + return false; + } + unsigned int modDate; + if (!getInt(&modDate)) + { + error("bad modDate found"); + return false; + } + unsigned long crc32; + if (!getLong(&crc32)) + { + error("bad crc32 found"); + return false; + } + unsigned long compressedSize; + if (!getLong(&compressedSize)) + { + error("bad compressedSize found"); + return false; + } + unsigned long uncompressedSize; + if (!getLong(&uncompressedSize)) + { + error("bad uncompressedSize found"); + return false; + } + unsigned int fileNameLength; + if (!getInt(&fileNameLength)) + { + error("bad fileNameLength found"); + return false; + } + unsigned int extraFieldLength; + if (!getInt(&extraFieldLength)) + { + error("bad extraFieldLength found"); + return false; + } + std::string fileName; + for (unsigned int i=0 ; i<fileNameLength ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + fileName.push_back(ch); + } + std::string extraField; + for (unsigned int i=0 ; i<extraFieldLength ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + extraField.push_back(ch); + } + trace("######################### DATA"); + trace("FileName :%d:%s" , fileName.size(), fileName.c_str()); + trace("Extra field :%d:%s" , extraField.size(), extraField.c_str()); + trace("Version needed :%d" , versionNeeded); + trace("Bitflag :%d" , gpBitFlag); + trace("Compression Method :%d" , compressionMethod); + trace("Mod time :%d" , modTime); + trace("Mod date :%d" , modDate); + trace("CRC :%lx", crc32); + trace("Compressed size :%ld", compressedSize); + trace("Uncompressed size :%ld", uncompressedSize); + + //#### Uncompress the data + std::vector<unsigned char> compBuf; + if (gpBitFlag & 0x8)//bit 3 was set. means we don't know compressed size + { + unsigned char c1, c2, c3, c4; + c2 = c3 = c4 = 0; + while (true) + { + unsigned char ch; + if (!getByte(&ch)) + { + error("premature end of data"); + break; + } + compBuf.push_back(ch); + c1 = c2; c2 = c3; c3 = c4; c4 = ch; + if (c1 == 0x50 && c2 == 0x4b && c3 == 0x07 && c4 == 0x08) + { + trace("found end of compressed data"); + //remove the cookie + compBuf.erase(compBuf.end() -4, compBuf.end()); + break; + } + } + } + else + { + for (unsigned long bnr = 0 ; bnr < compressedSize ; bnr++) + { + unsigned char ch; + if (!getByte(&ch)) + { + error("premature end of data"); + break; + } + compBuf.push_back(ch); + } + } + + printf("### data: "); + for (int i=0 ; i<10 ; i++) + printf("%02x ", compBuf[i] & 0xff); + printf("\n"); + + if (gpBitFlag & 0x8)//only if bit 3 set + { + /* this cookie was read in the loop above + unsigned long dataDescriptorSignature ; + if (!getLong(&dataDescriptorSignature)) + break; + if (dataDescriptorSignature != 0x08074b50L) + { + error("bad dataDescriptorSignature found"); + return false; + } + */ + unsigned long crc32; + if (!getLong(&crc32)) + { + error("bad crc32 found"); + return false; + } + unsigned long compressedSize; + if (!getLong(&compressedSize)) + { + error("bad compressedSize found"); + return false; + } + unsigned long uncompressedSize; + if (!getLong(&uncompressedSize)) + { + error("bad uncompressedSize found"); + return false; + } + }//bit 3 was set + //break; + + std::vector<unsigned char> uncompBuf; + switch (compressionMethod) + { + case 8: //deflate + { + Inflater inflater; + if (!inflater.inflate(uncompBuf, compBuf)) + { + return false; + } + break; + } + default: + { + error("Unimplemented compression method %d", compressionMethod); + return false; + } + } + + if (uncompressedSize != uncompBuf.size()) + { + error("Size mismatch. Expected %ld, received %ld", + uncompressedSize, uncompBuf.size()); + return false; + } + + Crc32 crcEngine; + crcEngine.update(uncompBuf); + unsigned long crc = crcEngine.getValue(); + if (crc != crc32) + { + error("Crc mismatch. Calculated %08ux, received %08ux", crc, crc32); + return false; + } + + ZipEntry *ze = new ZipEntry(fileName, comment); + ze->setCompressionMethod(compressionMethod); + ze->setCompressedData(compBuf); + ze->setUncompressedData(uncompBuf); + ze->setCrc(crc); + entries.push_back(ze); + + + } + return true; +} + + +/** + * + */ +bool ZipFile::readCentralDirectory() +{ + //printf("#################################################\n"); + //printf("###D I R E C T O R Y\n"); + //printf("#################################################\n"); + while (true) + { + //We start with a central directory cookie already + //Check at the bottom of the loop. + unsigned int version; + if (!getInt(&version)) + { + error("bad version found"); + return false; + } + unsigned int versionNeeded; + if (!getInt(&versionNeeded)) + { + error("bad version found"); + return false; + } + unsigned int gpBitFlag; + if (!getInt(&gpBitFlag)) + { + error("bad bit flag found"); + return false; + } + unsigned int compressionMethod; + if (!getInt(&compressionMethod)) + { + error("bad compressionMethod found"); + return false; + } + unsigned int modTime; + if (!getInt(&modTime)) + { + error("bad modTime found"); + return false; + } + unsigned int modDate; + if (!getInt(&modDate)) + { + error("bad modDate found"); + return false; + } + unsigned long crc32; + if (!getLong(&crc32)) + { + error("bad crc32 found"); + return false; + } + unsigned long compressedSize; + if (!getLong(&compressedSize)) + { + error("bad compressedSize found"); + return false; + } + unsigned long uncompressedSize; + if (!getLong(&uncompressedSize)) + { + error("bad uncompressedSize found"); + return false; + } + unsigned int fileNameLength; + if (!getInt(&fileNameLength)) + { + error("bad fileNameLength found"); + return false; + } + unsigned int extraFieldLength; + if (!getInt(&extraFieldLength)) + { + error("bad extraFieldLength found"); + return false; + } + unsigned int fileCommentLength; + if (!getInt(&fileCommentLength)) + { + error("bad fileCommentLength found"); + return false; + } + unsigned int diskNumberStart; + if (!getInt(&diskNumberStart)) + { + error("bad diskNumberStart found"); + return false; + } + unsigned int internalFileAttributes; + if (!getInt(&internalFileAttributes)) + { + error("bad internalFileAttributes found"); + return false; + } + unsigned long externalFileAttributes; + if (!getLong(&externalFileAttributes)) + { + error("bad externalFileAttributes found"); + return false; + } + unsigned long localHeaderOffset; + if (!getLong(&localHeaderOffset)) + { + error("bad localHeaderOffset found"); + return false; + } + std::string fileName; + for (unsigned int i=0 ; i<fileNameLength ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + fileName.push_back(ch); + } + std::string extraField; + for (unsigned int i=0 ; i<extraFieldLength ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + extraField.push_back(ch); + } + std::string fileComment; + for (unsigned int i=0 ; i<fileCommentLength ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + fileComment.push_back(ch); + } + trace("######################### ENTRY"); + trace("FileName :%s" , fileName.c_str()); + trace("Extra field :%s" , extraField.c_str()); + trace("File comment :%s" , fileComment.c_str()); + trace("Version :%d" , version); + trace("Version needed :%d" , versionNeeded); + trace("Bitflag :%d" , gpBitFlag); + trace("Compression Method :%d" , compressionMethod); + trace("Mod time :%d" , modTime); + trace("Mod date :%d" , modDate); + trace("CRC :%lx", crc32); + trace("Compressed size :%ld", compressedSize); + trace("Uncompressed size :%ld", uncompressedSize); + trace("Disk nr start :%ld", diskNumberStart); + trace("Header offset :%ld", localHeaderOffset); + + + unsigned long magicCookie; + if (!getLong(&magicCookie)) + { + error("magic cookie not found"); + return false; + } + trace("###Cookie:%lx", magicCookie); + if (magicCookie == 0x06054b50L) //end of central directory + break; + else if (magicCookie == 0x05054b50L) //signature + { + //## Digital Signature + unsigned int signatureSize; + if (!getInt(&signatureSize)) + { + error("bad signatureSize found"); + return false; + } + std::string digitalSignature; + for (unsigned int i=0 ; i<signatureSize ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + digitalSignature.push_back(ch); + } + trace("######## SIGNATURE :'%s'" , digitalSignature.c_str()); + } + else if (magicCookie != 0x02014b50L) //central directory + { + error("directory file header not found"); + return false; + } + } + + unsigned int diskNr; + if (!getInt(&diskNr)) + { + error("bad diskNr found"); + return false; + } + unsigned int diskWithCd; + if (!getInt(&diskWithCd)) + { + error("bad diskWithCd found"); + return false; + } + unsigned int nrEntriesDisk; + if (!getInt(&nrEntriesDisk)) + { + error("bad nrEntriesDisk found"); + return false; + } + unsigned int nrEntriesTotal; + if (!getInt(&nrEntriesTotal)) + { + error("bad nrEntriesTotal found"); + return false; + } + unsigned long cdSize; + if (!getLong(&cdSize)) + { + error("bad cdSize found"); + return false; + } + unsigned long cdPos; + if (!getLong(&cdPos)) + { + error("bad cdPos found"); + return false; + } + unsigned int commentSize; + if (!getInt(&commentSize)) + { + error("bad commentSize found"); + return false; + } + comment = ""; + for (unsigned int i=0 ; i<commentSize ; i++) + { + unsigned char ch; + if (!getByte(&ch)) + break; + comment.push_back(ch); + } + trace("######## Zip Comment :'%s'" , comment.c_str()); + + return true; +} + + +/** + * + */ +bool ZipFile::read() +{ + fileBufPos = 0; + if (!readFileData()) + { + return false; + } + if (!readCentralDirectory()) + { + return false; + } + return true; +} + +/** + * + */ +bool ZipFile::readBuffer(const std::vector<unsigned char> &inbuf) +{ + fileBuf = inbuf; + if (!read()) + return false; + return true; +} + + +/** + * + */ +bool ZipFile::readFile(const std::string &fileName) +{ + fileBuf.clear(); + FILE *f = fopen(fileName.c_str(), "rb"); + if (!f) + return false; + while (true) + { + int ch = fgetc(f); + if (ch < 0) + break; + fileBuf.push_back(ch); + } + fclose(f); + if (!read()) + return false; + return true; +} + + + + + + + + + +//######################################################################## +//# E N D O F F I L E +//######################################################################## + + diff --git a/src/util/ziptool.h b/src/util/ziptool.h new file mode 100644 index 0000000..26aaac8 --- /dev/null +++ b/src/util/ziptool.h @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * Bob Jamison + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef SEEN_ZIPTOOL_H +#define SEEN_ZIPTOOL_H +/** + * This is intended to be a standalone, reduced capability + * implementation of Gzip and Zip functionality. Its + * targeted use case is for archiving and retrieving single files + * which use these encoding types. Being memory based and + * non-optimized, it is not useful in cases where very large + * archives are needed or where high performance is desired. + * However, it should hopefully work well for smaller, + * one-at-a-time tasks. What you get in return is the ability + * to drop these files into your project and remove the dependencies + * on ZLib and Info-Zip. Enjoy. + */ + + + +#include <vector> +#include <string> + + +//######################################################################## +//# A D L E R 3 2 +//######################################################################## + +class Adler32 +{ +public: + + Adler32(); + + virtual ~Adler32(); + + void reset(); + + void update(unsigned char b); + + void update(char *str); + + unsigned long getValue(); + +private: + + unsigned long value; + +}; + + +//######################################################################## +//# C R C 3 2 +//######################################################################## + +class Crc32 +{ +public: + + Crc32(); + + virtual ~Crc32(); + + void reset(); + + void update(unsigned char b); + + void update(char *str); + + void update(const std::vector<unsigned char> &buf); + + unsigned long getValue(); + +private: + + unsigned long value; + +}; + + + + + + +//######################################################################## +//# G Z I P S T R E A M S +//######################################################################## + +class GzipFile +{ +public: + + /** + * + */ + GzipFile(); + + /** + * + */ + virtual ~GzipFile(); + + /** + * + */ + virtual void put(unsigned char ch); + + /** + * + */ + virtual void setData(const std::vector<unsigned char> &str); + + /** + * + */ + virtual void clearData(); + + /** + * + */ + virtual std::vector<unsigned char> &getData(); + + /** + * + */ + virtual std::string &getFileName(); + + /** + * + */ + virtual void setFileName(const std::string &val); + + + //###################### + //# U T I L I T Y + //###################### + + /** + * + */ + virtual bool readFile(const std::string &fName); + + //###################### + //# W R I T E + //###################### + + /** + * + */ + virtual bool write(); + + /** + * + */ + virtual bool writeBuffer(std::vector<unsigned char> &outbuf); + + /** + * + */ + virtual bool writeFile(const std::string &fileName); + + + //###################### + //# R E A D + //###################### + + + /** + * + */ + virtual bool read(); + + /** + * + */ + virtual bool readBuffer(const std::vector<unsigned char> &inbuf); + + /** + * + */ + virtual bool loadFile(const std::string &fileName); + + + +private: + + std::vector<unsigned char> data; + std::string fileName; + + //debug messages + void error(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + void trace(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + std::vector<unsigned char> fileBuf; + unsigned long fileBufPos; + + bool getByte(unsigned char *ch); + bool getLong(unsigned long *val); + + bool putByte(unsigned char ch); + bool putLong(unsigned long val); + + int compressionMethod; +}; + + + + +//######################################################################## +//# Z I P F I L E +//######################################################################## + + +/** + * + */ +class ZipEntry +{ +public: + + /** + * + */ + ZipEntry(); + + /** + * + */ + ZipEntry(std::string fileName, + std::string comment); + + /** + * + */ + virtual ~ZipEntry(); + + /** + * + */ + virtual std::string getFileName(); + + /** + * + */ + virtual void setFileName(const std::string &val); + + /** + * + */ + virtual std::string getComment(); + + /** + * + */ + virtual void setComment(const std::string &val); + + /** + * + */ + virtual unsigned long getCompressedSize(); + + /** + * + */ + virtual int getCompressionMethod(); + + /** + * + */ + virtual void setCompressionMethod(int val); + + /** + * + */ + virtual std::vector<unsigned char> &getCompressedData(); + + /** + * + */ + virtual void setCompressedData(const std::vector<unsigned char> &val); + + /** + * + */ + virtual unsigned long getUncompressedSize(); + + /** + * + */ + virtual std::vector<unsigned char> &getUncompressedData(); + + /** + * + */ + virtual void setUncompressedData(const std::vector<unsigned char> &val); + virtual void setUncompressedData(const std::string &val); + + /** + * + */ + virtual void write(unsigned char ch); + + /** + * + */ + virtual void finish(); + + /** + * + */ + virtual unsigned long getCrc(); + + /** + * + */ + virtual void setCrc(unsigned long crc); + + /** + * + */ + virtual bool readFile(const std::string &fileNameArg, + const std::string &commentArg); + + /** + * + */ + virtual void setPosition(unsigned long val); + + /** + * + */ + virtual unsigned long getPosition(); + +private: + + unsigned long crc; + + std::string fileName; + std::string comment; + + int compressionMethod; + + std::vector<unsigned char> compressedData; + std::vector<unsigned char> uncompressedData; + + unsigned long position; +}; + + + + + + + + + +/** + * This class sits over the zlib and gzip code to + * implement a PKWare or Info-Zip .zip file reader and + * writer + */ +class ZipFile +{ +public: + + /** + * + */ + ZipFile(); + + /** + * + */ + virtual ~ZipFile(); + + //###################### + //# V A R I A B L E S + //###################### + + /** + * + */ + virtual void setComment(const std::string &val); + + /** + * + */ + virtual std::string getComment(); + + /** + * Return the list of entries currently in this file + */ + std::vector<ZipEntry *> &getEntries(); + + + //###################### + //# U T I L I T Y + //###################### + + /** + * + */ + virtual ZipEntry *addFile(const std::string &fileNameArg, + const std::string &commentArg); + + /** + * + */ + virtual ZipEntry *newEntry(const std::string &fileNameArg, + const std::string &commentArg); + + //###################### + //# W R I T E + //###################### + + /** + * + */ + virtual bool write(); + + /** + * + */ + virtual bool writeBuffer(std::vector<unsigned char> &outbuf); + + /** + * + */ + virtual bool writeFile(const std::string &fileName); + + + //###################### + //# R E A D + //###################### + + + /** + * + */ + virtual bool read(); + + /** + * + */ + virtual bool readBuffer(const std::vector<unsigned char> &inbuf); + + /** + * + */ + virtual bool readFile(const std::string &fileName); + + +private: + + //debug messages + void error(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + void trace(char const *fmt, ...) + #ifdef G_GNUC_PRINTF + G_GNUC_PRINTF(2, 3) + #endif + ; + + //# Private writing methods + + /** + * + */ + bool putLong(unsigned long val); + + /** + * + */ + bool putInt(unsigned int val); + + + /** + * + */ + bool putByte(unsigned char val); + + /** + * + */ + bool writeFileData(); + + /** + * + */ + bool writeCentralDirectory(); + + + //# Private reading methods + + /** + * + */ + bool getLong(unsigned long *val); + + /** + * + */ + bool getInt(unsigned int *val); + + /** + * + */ + bool getByte(unsigned char *val); + + /** + * + */ + bool readFileData(); + + /** + * + */ + bool readCentralDirectory(); + + + std::vector<ZipEntry *> entries; + + std::vector<unsigned char> fileBuf; + unsigned long fileBufPos; + + std::string comment; +}; + + + + + + +#endif // SEEN_ZIPTOOL_H + + +//######################################################################## +//# E N D O F F I L E +//######################################################################## + diff --git a/src/vanishing-point.cpp b/src/vanishing-point.cpp new file mode 100644 index 0000000..1b7d806 --- /dev/null +++ b/src/vanishing-point.cpp @@ -0,0 +1,773 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Vanishing point for 3D perspectives + * + * Authors: + * bulia byak <buliabyak@users.sf.net> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Maximilian Albert <Anhalter42@gmx.de> + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005-2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "vanishing-point.h" + +#include "desktop.h" +#include "document-undo.h" +#include "perspective-line.h" +#include "snap.h" +#include "verbs.h" + +#include "display/sp-canvas-item.h" +#include "display/sp-ctrlline.h" + +#include "object/sp-namedview.h" + +#include "ui/shape-editor.h" +#include "ui/tools/tool-base.h" + +using Inkscape::CTLINE_PRIMARY; +using Inkscape::CTLINE_SECONDARY; +using Inkscape::CTLINE_TERTIARY; +using Inkscape::CTRL_TYPE_ANCHOR; +using Inkscape::ControlManager; +using Inkscape::CtrlLineType; +using Inkscape::DocumentUndo; + +namespace Box3D { + +#define VP_KNOT_COLOR_NORMAL 0xffffff00 +#define VP_KNOT_COLOR_SELECTED 0x0000ff00 + +// screen pixels between knots when they snap: +#define SNAP_DIST 5 + +// absolute distance between gradient points for them to become a single dragger when the drag is created: +#define MERGE_DIST 0.1 + +// knot shapes corresponding to GrPointType enum +SPKnotShapeType vp_knot_shapes[] = { + SP_KNOT_SHAPE_SQUARE, // VP_FINITE + SP_KNOT_SHAPE_CIRCLE // VP_INFINITE +}; + +static void vp_drag_sel_changed(Inkscape::Selection * /*selection*/, gpointer data) +{ + VPDrag *drag = (VPDrag *)data; + drag->updateDraggers(); + drag->updateLines(); + drag->updateBoxReprs(); +} + +static void vp_drag_sel_modified(Inkscape::Selection * /*selection*/, guint /*flags*/, gpointer data) +{ + VPDrag *drag = (VPDrag *)data; + drag->updateLines(); + // drag->updateBoxReprs(); + drag->updateBoxHandles(); // FIXME: Only update the handles of boxes on this dragger (not on all) + drag->updateDraggers(); +} + +static bool have_VPs_of_same_perspective(VPDragger *dr1, VPDragger *dr2) +{ + for (auto & vp : dr1->vps) { + if (dr2->hasPerspective(vp.get_perspective())) { + return true; + } + } + return false; +} + +static void vp_knot_moved_handler(SPKnot *knot, Geom::Point const &ppointer, guint state, gpointer data) +{ + VPDragger *dragger = (VPDragger *)data; + VPDrag *drag = dragger->parent; + + Geom::Point p = ppointer; + + // FIXME: take from prefs + double snap_dist = SNAP_DIST / SP_ACTIVE_DESKTOP->current_zoom(); + + /* + * We use dragging_started to indicate if we have already checked for the need to split Draggers up. + * This only has the purpose of avoiding costly checks in the routine below. + */ + if (!dragger->dragging_started && (state & GDK_SHIFT_MASK)) { + /* with Shift; if there is more than one box linked to this VP + we need to split it and create a new perspective */ + if (dragger->numberOfBoxes() > 1) { // FIXME: Don't do anything if *all* boxes of a VP are selected + std::set<VanishingPoint *, less_ptr> sel_vps = dragger->VPsOfSelectedBoxes(); + + std::list<SPBox3D *> sel_boxes; + for (auto sel_vp : sel_vps) { + // for each VP that has selected boxes: + Persp3D *old_persp = sel_vp->get_perspective(); + sel_boxes = sel_vp->selectedBoxes(SP_ACTIVE_DESKTOP->getSelection()); + + // we create a new perspective ... + Persp3D *new_persp = persp3d_create_xml_element(dragger->parent->document, old_persp->perspective_impl); + + /* ... unlink the boxes from the old one and + FIXME: We need to unlink the _un_selected boxes of each VP so that + the correct boxes are kept with the VP being moved */ + std::list<SPBox3D *> bx_lst = persp3d_list_of_boxes(old_persp); + for (auto & i : bx_lst) { + if (std::find(sel_boxes.begin(), sel_boxes.end(), i) == sel_boxes.end()) { + /* if a box in the VP is unselected, move it to the + newly created perspective so that it doesn't get dragged **/ + box3d_switch_perspectives(i, old_persp, new_persp); + } + } + } + // FIXME: Do we need to create a new dragger as well? + dragger->updateZOrders(); + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_3DBOX, _("Split vanishing points")); + return; + } + } + + if (!(state & GDK_SHIFT_MASK)) { + // without Shift; see if we need to snap to another dragger + for (std::vector<VPDragger *>::const_iterator di = dragger->parent->draggers.begin(); + di != dragger->parent->draggers.end(); ++di) { + VPDragger *d_new = *di; + if ((d_new != dragger) && (Geom::L2(d_new->point - p) < snap_dist)) { + if (have_VPs_of_same_perspective(dragger, d_new)) { + // this would result in degenerate boxes, which we disallow for the time being + continue; + } + + // update positions ... (this is needed so that the perspectives are detected as identical) + // FIXME: This is called a bit too often, isn't it? + for (auto & vp : dragger->vps) { + vp.set_pos(d_new->point); + } + + // ... join lists of VPs ... + d_new->vps.merge(dragger->vps); + + // ... delete old dragger ... + drag->draggers.erase(std::remove(drag->draggers.begin(), drag->draggers.end(), dragger), + drag->draggers.end()); + delete dragger; + dragger = nullptr; + + // ... and merge any duplicate perspectives + d_new->mergePerspectives(); + + // TODO: Update the new merged dragger + d_new->updateTip(); + + d_new->parent->updateBoxDisplays(); // FIXME: Only update boxes in current dragger! + d_new->updateZOrders(); + + drag->updateLines(); + + // TODO: Undo machinery; this doesn't work yet because perspectives must be created and + // deleted according to changes in the svg representation, not based on any user input + // as is currently the case. + + DocumentUndo::done(SP_ACTIVE_DESKTOP->getDocument(), SP_VERB_CONTEXT_3DBOX, + _("Merge vanishing points")); + + return; + } + } + } + + // We didn't hit the return statement above, so we didn't snap to another dragger. Therefore we'll now try a regular + // snap + // Regardless of the status of the SHIFT key, we will try to snap; Here SHIFT does not disable snapping, as the + // shift key + // has a different purpose in this context (see above) + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + Inkscape::SnappedPoint s = m.freeSnap(Inkscape::SnapCandidatePoint(p, Inkscape::SNAPSOURCE_OTHER_HANDLE)); + m.unSetup(); + if (s.getSnapped()) { + p = s.getPoint(); + knot->moveto(p); + } + + dragger->point = p; // FIXME: Is dragger->point being used at all? + + dragger->updateVPs(p); + dragger->updateBoxDisplays(); + dragger->parent->updateBoxHandles(); // FIXME: Only update the handles of boxes on this dragger (not on all) + dragger->updateZOrders(); + + drag->updateLines(); + + dragger->dragging_started = true; +} + +static void vp_knot_grabbed_handler(SPKnot * /*knot*/, unsigned int /*state*/, gpointer data) +{ + VPDragger *dragger = (VPDragger *)data; + VPDrag *drag = dragger->parent; + + drag->dragging = true; +} + +static void vp_knot_ungrabbed_handler(SPKnot *knot, guint /*state*/, gpointer data) +{ + VPDragger *dragger = (VPDragger *)data; + + dragger->point_original = dragger->point = knot->pos; + + dragger->dragging_started = false; + + for (auto & vp : dragger->vps) { + vp.set_pos(knot->pos); + vp.updateBoxReprs(); + vp.updatePerspRepr(); + } + + dragger->parent->updateDraggers(); + dragger->parent->updateLines(); + dragger->parent->updateBoxHandles(); + + // TODO: Update box's paths and svg representation + + dragger->parent->dragging = false; + + // TODO: Undo machinery!! + g_return_if_fail(dragger->parent); + g_return_if_fail(dragger->parent->document); + DocumentUndo::done(dragger->parent->document, SP_VERB_CONTEXT_3DBOX, _("3D box: Move vanishing point")); +} + +unsigned int VanishingPoint::global_counter = 0; + +// FIXME: Rename to something more meaningful! +void VanishingPoint::set_pos(Proj::Pt2 const &pt) +{ + g_return_if_fail(_persp); + _persp->perspective_impl->tmat.set_image_pt(_axis, pt); +} + +std::list<SPBox3D *> VanishingPoint::selectedBoxes(Inkscape::Selection *sel) +{ + std::list<SPBox3D *> sel_boxes; + auto itemlist = sel->items(); + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + SPItem *item = *i; + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box && this->hasBox(box)) { + sel_boxes.push_back(box); + } + } + return sel_boxes; +} + +VPDragger::VPDragger(VPDrag *parent, Geom::Point p, VanishingPoint &vp) + : parent(parent) + , knot(nullptr) + , point(p) + , point_original(p) + , dragging_started(false) + , vps() +{ + if (vp.is_finite()) { + // create the knot + this->knot = new SPKnot(SP_ACTIVE_DESKTOP, nullptr); + this->knot->setMode(SP_KNOT_MODE_XOR); + this->knot->setFill(VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL, VP_KNOT_COLOR_NORMAL); + this->knot->setStroke(0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff); + this->knot->updateCtrl(); + knot->item->ctrlType = CTRL_TYPE_ANCHOR; + ControlManager::getManager().track(knot->item); + + // move knot to the given point + this->knot->setPosition(this->point, SP_KNOT_STATE_NORMAL); + this->knot->show(); + + // connect knot's signals + this->_moved_connection = + this->knot->moved_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_moved_handler), this)); + this->_grabbed_connection = + this->knot->grabbed_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_grabbed_handler), this)); + this->_ungrabbed_connection = + this->knot->ungrabbed_signal.connect(sigc::bind(sigc::ptr_fun(vp_knot_ungrabbed_handler), this)); + + // add the initial VP (which may be NULL!) + this->addVP(vp); + } +} + +VPDragger::~VPDragger() +{ + // disconnect signals + this->_moved_connection.disconnect(); + this->_grabbed_connection.disconnect(); + this->_ungrabbed_connection.disconnect(); + + /* unref should call destroy */ + knot_unref(this->knot); +} + +/** +Updates the statusbar tip of the dragger knot, based on its draggables + */ +void VPDragger::updateTip() +{ + if (this->knot && this->knot->tip) { + g_free(this->knot->tip); + this->knot->tip = nullptr; + } + + guint num = this->numberOfBoxes(); + if (this->vps.size() == 1) { + if (this->vps.front().is_finite()) { + this->knot->tip = g_strdup_printf(ngettext("<b>Finite</b> vanishing point shared by <b>%d</b> box", + "<b>Finite</b> vanishing point shared by <b>%d</b> boxes; drag " + "with <b>Shift</b> to separate selected box(es)", + num), + num); + } + else { + // This won't make sense any more when infinite VPs are not shown on the canvas, + // but currently we update the status message anyway + this->knot->tip = g_strdup_printf(ngettext("<b>Infinite</b> vanishing point shared by <b>%d</b> box", + "<b>Infinite</b> vanishing point shared by <b>%d</b> boxes; " + "drag with <b>Shift</b> to separate selected box(es)", + num), + num); + } + } + else { + int length = this->vps.size(); + char *desc1 = g_strdup_printf("Collection of <b>%d</b> vanishing points ", length); + char *desc2 = g_strdup_printf( + ngettext("shared by <b>%d</b> box; drag with <b>Shift</b> to separate selected box(es)", + "shared by <b>%d</b> boxes; drag with <b>Shift</b> to separate selected box(es)", num), + num); + this->knot->tip = g_strconcat(desc1, desc2, NULL); + g_free(desc1); + g_free(desc2); + } +} + +/** + * Adds a vanishing point to the dragger (also updates the position if necessary); + * the perspective is stored separately, too, for efficiency in updating boxes. + */ +void VPDragger::addVP(VanishingPoint &vp, bool update_pos) +{ + if (!vp.is_finite() || std::find(vps.begin(), vps.end(), vp) != vps.end()) { + // don't add infinite VPs; don't add the same VP twice + return; + } + + if (update_pos) { + vp.set_pos(this->point); + } + this->vps.push_front(vp); + + this->updateTip(); +} + +void VPDragger::removeVP(VanishingPoint const &vp) +{ + std::list<VanishingPoint>::iterator i = std::find(this->vps.begin(), this->vps.end(), vp); + if (i != this->vps.end()) { + this->vps.erase(i); + } + this->updateTip(); +} + +VanishingPoint *VPDragger::findVPWithBox(SPBox3D *box) +{ + for (auto & vp : vps) { + if (vp.hasBox(box)) { + return &vp; + } + } + return nullptr; +} + +std::set<VanishingPoint *, less_ptr> VPDragger::VPsOfSelectedBoxes() +{ + std::set<VanishingPoint *, less_ptr> sel_vps; + VanishingPoint *vp; + // FIXME: Should we take the selection from the parent VPDrag? I guess it shouldn't make a difference. + Inkscape::Selection *sel = SP_ACTIVE_DESKTOP->getSelection(); + auto itemlist = sel->items(); + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + SPItem *item = *i; + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + vp = this->findVPWithBox(box); + if (vp) { + sel_vps.insert(vp); + } + } + } + return sel_vps; +} + +guint VPDragger::numberOfBoxes() +{ + guint num = 0; + for (auto & vp : vps) { + num += vp.numberOfBoxes(); + } + return num; +} + +bool VPDragger::hasPerspective(const Persp3D *persp) +{ + for (auto & vp : vps) { + if (persp3d_perspectives_coincide(persp, vp.get_perspective())) { + return true; + } + } + return false; +} + +void VPDragger::mergePerspectives() +{ + Persp3D *persp1, *persp2; + for (std::list<VanishingPoint>::iterator i = vps.begin(); i != vps.end(); ++i) { + persp1 = (*i).get_perspective(); + for (std::list<VanishingPoint>::iterator j = i; j != vps.end(); ++j) { + persp2 = (*j).get_perspective(); + if (persp1 == persp2) { + /* don't merge a perspective with itself */ + continue; + } + if (persp3d_perspectives_coincide(persp1, persp2)) { + /* if perspectives coincide but are not the same, merge them */ + persp3d_absorb(persp1, persp2); + + this->parent->swap_perspectives_of_VPs(persp2, persp1); + + SP_OBJECT(persp2)->deleteObject(false); + } + } + } +} + +void VPDragger::updateBoxDisplays() +{ + for (auto & vp : this->vps) { + vp.updateBoxDisplays(); + } +} + +void VPDragger::updateVPs(Geom::Point const &pt) +{ + for (auto & vp : this->vps) { + vp.set_pos(pt); + } +} + +void VPDragger::updateZOrders() +{ + for (auto & vp : this->vps) { + persp3d_update_z_orders(vp.get_perspective()); + } +} + +void VPDragger::printVPs() +{ + g_print("VPDragger at position (%f, %f):\n", point[Geom::X], point[Geom::Y]); + for (auto & vp : this->vps) { + g_print(" VP %s\n", vp.axisString()); + } +} + +VPDrag::VPDrag(SPDocument *document) +{ + this->document = document; + this->selection = SP_ACTIVE_DESKTOP->getSelection(); + + this->show_lines = true; + this->front_or_rear_lines = 0x1; + + this->dragging = false; + + this->sel_changed_connection = + this->selection->connectChanged(sigc::bind(sigc::ptr_fun(&vp_drag_sel_changed), (gpointer) this) + + ); + this->sel_modified_connection = + this->selection->connectModified(sigc::bind(sigc::ptr_fun(&vp_drag_sel_modified), (gpointer) this)); + + this->updateDraggers(); + this->updateLines(); +} + +VPDrag::~VPDrag() +{ + this->sel_changed_connection.disconnect(); + this->sel_modified_connection.disconnect(); + + for (auto dragger : this->draggers) { + delete dragger; + } + this->draggers.clear(); + + for (std::vector<SPCtrlLine *>::const_iterator i = this->lines.begin(); i != this->lines.end(); ++i) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(*i)); + } + this->lines.clear(); +} + +/** + * Select the dragger that has the given VP. + */ +VPDragger *VPDrag::getDraggerFor(VanishingPoint const &vp) +{ + for (auto dragger : this->draggers) { + for (std::list<VanishingPoint>::iterator j = dragger->vps.begin(); j != dragger->vps.end(); ++j) { + // TODO: Should we compare the pointers or the VPs themselves!?!?!?! + if (*j == vp) { + return (dragger); + } + } + } + return nullptr; +} + +void VPDrag::printDraggers() +{ + g_print("=== VPDrag info: =================================\n"); + for (auto dragger : this->draggers) { + dragger->printVPs(); + g_print("========\n"); + } + g_print("=================================================\n"); +} + +/** + * Regenerates the draggers list from the current selection; is called when selection is changed or modified + */ +void VPDrag::updateDraggers() +{ + if (this->dragging) + return; + // delete old draggers + for (auto dragger : this->draggers) { + delete dragger; + } + this->draggers.clear(); + + g_return_if_fail(this->selection != nullptr); + + auto itemlist = this->selection->items(); + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + SPItem *item = *i; + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + VanishingPoint vp; + for (int i = 0; i < 3; ++i) { + vp.set(box3d_get_perspective(box), Proj::axes[i]); + addDragger(vp); + } + } + } +} + +/** +Regenerates the lines list from the current selection; is called on each move +of a dragger, so that lines are always in sync with the actual perspective +*/ +void VPDrag::updateLines() +{ + // delete old lines + for (std::vector<SPCtrlLine *>::const_iterator i = this->lines.begin(); i != this->lines.end(); ++i) { + sp_canvas_item_destroy(SP_CANVAS_ITEM(*i)); + } + this->lines.clear(); + + // do nothing if perspective lines are currently disabled + if (this->show_lines == 0) + return; + + g_return_if_fail(this->selection != nullptr); + + auto itemlist = this->selection->items(); + for (auto i = itemlist.begin(); i != itemlist.end(); ++i) { + SPItem *item = *i; + SPBox3D *box = dynamic_cast<SPBox3D *>(item); + if (box) { + this->drawLinesForFace(box, Proj::X); + this->drawLinesForFace(box, Proj::Y); + this->drawLinesForFace(box, Proj::Z); + } + } +} + +void VPDrag::updateBoxHandles() +{ + // FIXME: Is there a way to update the knots without accessing the + // (previously) statically linked function KnotHolder::update_knots? + + auto sel = selection->items(); + if (sel.empty()) + return; // no selection + + if (boost::distance(sel) > 1) { + // Currently we only show handles if a single box is selected + return; + } + + Inkscape::UI::Tools::ToolBase *ec = INKSCAPE.active_event_context(); + g_assert(ec != nullptr); + if (ec->shape_editor != nullptr) { + ec->shape_editor->update_knotholder(); + } +} + +void VPDrag::updateBoxReprs() +{ + for (auto dragger : this->draggers) { + for (auto & vp : dragger->vps) { + vp.updateBoxReprs(); + } + } +} + +void VPDrag::updateBoxDisplays() +{ + for (auto dragger : this->draggers) { + for (auto & vp : dragger->vps) { + vp.updateBoxDisplays(); + } + } +} + + +/** + * Depending on the value of all_lines, draw the front and/or rear perspective lines starting from the given corners. + */ +void VPDrag::drawLinesForFace(const SPBox3D *box, + Proj::Axis axis) //, guint corner1, guint corner2, guint corner3, guint corner4) +{ + CtrlLineType type = CTLINE_PRIMARY; + switch (axis) { + // TODO: Make color selectable by user + case Proj::X: + type = CTLINE_SECONDARY; + break; + case Proj::Y: + type = CTLINE_PRIMARY; + break; + case Proj::Z: + type = CTLINE_TERTIARY; + break; + default: + g_assert_not_reached(); + } + + const size_t NUM_CORNERS = 4; + Geom::Point corners[NUM_CORNERS]; + box3d_corners_for_PLs(box, axis, corners[0], corners[1], corners[2], corners[3]); + + g_return_if_fail(box3d_get_perspective(box)); + Proj::Pt2 vp = persp3d_get_VP(box3d_get_perspective(box), axis); + if (vp.is_finite()) { + // draw perspective lines for finite VPs + Geom::Point pt = vp.affine(); + if (this->front_or_rear_lines & 0x1) { + // draw 'front' perspective lines + this->addLine(corners[0], pt, type); + this->addLine(corners[1], pt, type); + } + if (this->front_or_rear_lines & 0x2) { + // draw 'rear' perspective lines + this->addLine(corners[2], pt, type); + this->addLine(corners[3], pt, type); + } + } + else { + // draw perspective lines for infinite VPs + boost::optional<Geom::Point> pts[NUM_CORNERS]; + Persp3D *persp = box3d_get_perspective(box); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; // FIXME: Store the desktop in VPDrag + + for (size_t i = 0; i < NUM_CORNERS; i++) { + Box3D::PerspectiveLine pl(corners[i], axis, persp); + if (!(pts[i] = pl.intersection_with_viewbox(desktop))) { + // some perspective lines are outside the canvas; currently we don't draw any of them + return; + } + } + if (this->front_or_rear_lines & 0x1) { + // draw 'front' perspective lines + this->addLine(corners[0], *pts[0], type); + this->addLine(corners[1], *pts[1], type); + } + if (this->front_or_rear_lines & 0x2) { + // draw 'rear' perspective lines + this->addLine(corners[2], *pts[2], type); + this->addLine(corners[3], *pts[3], type); + } + } +} + +/** + * If there already exists a dragger within MERGE_DIST of p, add the VP to it; + * otherwise create new dragger and add it to draggers list + * We also store the corresponding perspective in case it is not already present. + */ +void VPDrag::addDragger(VanishingPoint &vp) +{ + if (!vp.is_finite()) { + // don't create draggers for infinite vanishing points + return; + } + Geom::Point p = vp.get_pos(); + + for (auto dragger : this->draggers) { + if (Geom::L2(dragger->point - p) < MERGE_DIST) { + // distance is small, merge this draggable into dragger, no need to create new dragger + dragger->addVP(vp); + return; + } + } + + VPDragger *new_dragger = new VPDragger(this, p, vp); + // fixme: draggers should be added AFTER the last one: this way tabbing through them will be from begin to end. + this->draggers.push_back(new_dragger); +} + +void VPDrag::swap_perspectives_of_VPs(Persp3D *persp2, Persp3D *persp1) +{ + // iterate over all VP in all draggers and replace persp2 with persp1 + for (auto dragger : this->draggers) { + for (auto & vp : dragger->vps) { + if (vp.get_perspective() == persp2) { + vp.set_perspective(persp1); + } + } + } +} + +void VPDrag::addLine(Geom::Point const &p1, Geom::Point const &p2, Inkscape::CtrlLineType type) +{ + SPCtrlLine *line = ControlManager::getManager().createControlLine(SP_ACTIVE_DESKTOP->getControls(), p1, p2, type); + sp_canvas_item_show(line); + this->lines.push_back(line); +} + +} // namespace Box3D + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/vanishing-point.h b/src/vanishing-point.h new file mode 100644 index 0000000..6901cf6 --- /dev/null +++ b/src/vanishing-point.h @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Vanishing point for 3D perspectives + * + * Authors: + * Maximilian Albert <Anhalter42@gmx.de> + * + * Copyright (C) 2007 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_VANISHING_POINT_H +#define SEEN_VANISHING_POINT_H + +#include <2geom/point.h> +#include <list> +#include <set> + +#include "knot.h" +#include "selection.h" + +#include "object/persp3d.h" +#include "object/box3d.h" + +#include "ui/control-manager.h" // TODO break enums out separately + +class SPBox3D; + +namespace Box3D { + +enum VPState { + VP_FINITE = 0, // perspective lines meet in the VP + VP_INFINITE // perspective lines are parallel +}; + +/* VanishingPoint is a simple wrapper class to easily extract VP data from perspectives. + * A VanishingPoint represents a VP in a certain direction (X, Y, Z) of a single perspective. + * In particular, it can potentially have more than one box linked to it (although in facth they + * are rather linked to the parent perspective). + */ +// FIXME: Don't store the box in the VP but rather the perspective (and link the box to it)!! +class VanishingPoint { +public: + VanishingPoint() : my_counter(VanishingPoint::global_counter++), _persp(nullptr), _axis(Proj::NONE) {} + VanishingPoint(Persp3D *persp, Proj::Axis axis) : my_counter(VanishingPoint::global_counter++), _persp(persp), _axis(axis) {} + VanishingPoint(const VanishingPoint &other) : my_counter(VanishingPoint::global_counter++), _persp(other._persp), _axis(other._axis) {} + + inline VanishingPoint &operator=(VanishingPoint const &rhs) = default; + inline bool operator==(VanishingPoint const &rhs) const { + /* vanishing points coincide if they belong to the same perspective */ + return (_persp == rhs._persp && _axis == rhs._axis); + } + + inline bool operator<(VanishingPoint const &rhs) const { + return my_counter < rhs.my_counter; + } + + inline void set(Persp3D *persp, Proj::Axis axis) { + _persp = persp; + _axis = axis; + } + void set_pos(Proj::Pt2 const &pt); + inline bool is_finite() const { + g_return_val_if_fail (_persp, false); + return persp3d_get_VP (_persp, _axis).is_finite(); + } + inline Geom::Point get_pos() const { + g_return_val_if_fail (_persp, Geom::Point (Geom::infinity(), Geom::infinity())); + return persp3d_get_VP (_persp,_axis).affine(); + } + inline Persp3D * get_perspective() const { + return _persp; + } + inline Persp3D * set_perspective(Persp3D *persp) { + return _persp = persp; + } + + inline bool hasBox (SPBox3D *box) { + return persp3d_has_box(_persp, box); + } + inline unsigned int numberOfBoxes() const { + return persp3d_num_boxes(_persp); + } + + /* returns all selected boxes sharing this perspective */ + std::list<SPBox3D *> selectedBoxes(Inkscape::Selection *sel); + + inline void updateBoxDisplays() const { + g_return_if_fail (_persp); + persp3d_update_box_displays(_persp); + } + inline void updateBoxReprs() const { + g_return_if_fail (_persp); + persp3d_update_box_reprs(_persp); + } + inline void updatePerspRepr() const { + g_return_if_fail (_persp); + SP_OBJECT(_persp)->updateRepr(SP_OBJECT_WRITE_EXT); + } + inline void printPt() const { + g_return_if_fail (_persp); + persp3d_get_VP (_persp, _axis).print(""); + } + inline char const *axisString () { return Proj::string_from_axis(_axis); } + + unsigned int my_counter; + static unsigned int global_counter; // FIXME: Only to implement operator< so that we can merge lists. Do this in a better way!! +private: + Persp3D *_persp; + Proj::Axis _axis; +}; + +struct VPDrag; + +struct less_ptr : public std::binary_function<VanishingPoint *, VanishingPoint *, bool> { + bool operator()(VanishingPoint *vp1, VanishingPoint *vp2) { + return GPOINTER_TO_INT(vp1) < GPOINTER_TO_INT(vp2); + } +}; + +struct VPDragger { +public: + VPDragger(VPDrag *parent, Geom::Point p, VanishingPoint &vp); + ~VPDragger(); + + VPDrag *parent; + SPKnot *knot; + + // position of the knot, desktop coords + Geom::Point point; + // position of the knot before it began to drag; updated when released + Geom::Point point_original; + + bool dragging_started; + + std::list<VanishingPoint> vps; + + void addVP(VanishingPoint &vp, bool update_pos = false); + void removeVP(const VanishingPoint &vp); + + void updateTip(); + + unsigned int numberOfBoxes(); // the number of boxes linked to all VPs of the dragger + VanishingPoint *findVPWithBox(SPBox3D *box); + std::set<VanishingPoint*, less_ptr> VPsOfSelectedBoxes(); + + bool hasPerspective(const Persp3D *persp); + void mergePerspectives(); // remove duplicate perspectives + + void updateBoxDisplays(); + void updateVPs(Geom::Point const &pt); + void updateZOrders(); + + void printVPs(); + +private: + sigc::connection _moved_connection; + sigc::connection _grabbed_connection; + sigc::connection _ungrabbed_connection; +}; + +struct VPDrag { +public: + VPDrag(SPDocument *document); + ~VPDrag(); + + VPDragger *getDraggerFor (VanishingPoint const &vp); + + bool dragging; + + SPDocument *document; + std::vector<VPDragger *> draggers; + std::vector<SPCtrlLine *> lines; + + void printDraggers(); // convenience for debugging + /* + * FIXME: Should the following functions be merged? + * Also, they should make use of the info in a VanishingPoint structure (regarding boxes + * and perspectives) rather than each time iterating over the whole list of selected items? + */ + void updateDraggers (); + void updateLines (); + void updateBoxHandles (); + void updateBoxReprs (); + void updateBoxDisplays (); + void drawLinesForFace (const SPBox3D *box, Proj::Axis axis); //, guint corner1, guint corner2, guint corner3, guint corner4); + bool show_lines; /* whether perspective lines are drawn at all */ + unsigned int front_or_rear_lines; /* whether we draw perspective lines from all corners or only the + front/rear corners (indicated by the first/second bit, respectively */ + + + inline bool hasEmptySelection() { return this->selection->isEmpty(); } + bool allBoxesAreSelected (VPDragger *dragger); + + // FIXME: Should this be private? (It's the case with the corresponding function in gradient-drag.h) + // But vp_knot_grabbed_handler + void addDragger (VanishingPoint &vp); + + void swap_perspectives_of_VPs(Persp3D *persp2, Persp3D *persp1); + +private: + //void deselect_all(); + + /** + * Create a line from p1 to p2 and add it to the lines list. + */ + void addLine(Geom::Point const &p1, Geom::Point const &p2, Inkscape::CtrlLineType type); + + Inkscape::Selection *selection; + sigc::connection sel_changed_connection; + sigc::connection sel_modified_connection; +}; + +} // namespace Box3D + + +#endif /* !SEEN_VANISHING_POINT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/verbs.cpp b/src/verbs.cpp new file mode 100644 index 0000000..10dac25 --- /dev/null +++ b/src/verbs.cpp @@ -0,0 +1,3490 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Actions for inkscape. + * + * + * This file implements routines necessary to deal with verbs. A verb + * is a numeric identifier used to retrieve standard SPActions for particular + * views. + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * MenTaLguY <mental@rydia.net> + * David Turner <novalis@gnu.org> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * 2006 Johan Engelen <johan@shouraizou.nl> + * 2012 Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +// Note that gtkmm headers must be included before gtk+ C headers +// in all files. The same applies for glibmm/glib etc. +// If this is not done, then errors will be generate relating to Glib::Threads being undefined + +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/messagedialog.h> + +#include "desktop.h" + +#include "document.h" +#include "file.h" +#include "gradient-drag.h" +#include "help.h" +#include "inkscape.h" +#include "layer-fns.h" +#include "layer-manager.h" +#include "message-stack.h" +#include "path-chemistry.h" +#include "selection-chemistry.h" +#include "seltrans.h" +#include "shortcuts.h" +#include "splivarot.h" +#include "text-chemistry.h" + +#include "display/curve.h" +#include "display/sp-canvas.h" + +#include "extension/effect.h" + +#include "helper/action.h" + +#include "live_effects/effect.h" +#include "live_effects/lpe-powerclip.h" +#include "live_effects/lpe-powermask.h" + +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-guide.h" +#include "object/sp-namedview.h" + +#include "ui/dialog/align-and-distribute.h" +#include "ui/dialog/clonetiler.h" +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/document-properties.h" +#include "ui/dialog/extensions.h" +#include "ui/dialog/glyphs.h" +#include "ui/dialog/icon-preview.h" +#include "ui/dialog/inkscape-preferences.h" +#include "ui/dialog/layer-properties.h" +#include "ui/dialog/layers.h" +#include "ui/dialog/new-from-template.h" +#include "ui/dialog/object-properties.h" +#include "ui/dialog/paint-servers.h" +#include "ui/dialog/save-template-dialog.h" +#include "ui/dialog/swatches.h" +#include "ui/dialog/symbols.h" +#include "ui/icon-names.h" +#include "ui/interface.h" +#include "ui/shape-editor.h" +#include "ui/tools-switch.h" +#include "ui/tools/freehand-base.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/select-tool.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::Dialog::ActionAlign; + +/** + * Return the name without underscores and ellipsis, for use in dialog + * titles, etc. Allocated memory must be freed by caller. + */ +gchar *sp_action_get_title(SPAction const *action) +{ + char const *src = action->name; + size_t const len = strlen(src); + gchar *ret = g_new(gchar, len + 1); + unsigned ri = 0; + + for (unsigned si = 0 ; ; si++) { + int const c = src[si]; + // Ignore Unicode Character "…" (U+2026) + if ( c == '\xE2' && si + 2 < len && src[si+1] == '\x80' && src[si+2] == '\xA6' ) { + si += 2; + } else if ( c != '_' && c != '.' ) { + ret[ri] = c; + ri++; + if (c == '\0') { + return ret; + } + } + } + +} // end of sp_action_get_title() + +namespace Inkscape { + +/** + * A class to encompass all of the verbs which deal with file operations. + */ +class FileVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + FileVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("File")) + { } +}; // FileVerb class + +/** + * A class to encompass all of the verbs which deal with edit operations. + */ +class EditVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + EditVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Edit")) + { } +}; // EditVerb class + +/** + * A class to encompass all of the verbs which deal with selection operations. + */ +class SelectionVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + SelectionVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Selection")) + { } +}; // SelectionVerb class + +/** + * A class to encompass all of the verbs which deal with layer operations. + */ +class LayerVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + LayerVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Layer")) + { } +}; // LayerVerb class + +/** + * A class to encompass all of the verbs which deal with operations related to objects. + */ +class ObjectVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + ObjectVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Object")) + { } +}; // ObjectVerb class + +/** + * A class to encompass all of the verbs which deal with operations related to tags. + */ +class TagVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + TagVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Tag")) + { } +}; // TagVerb class + +/** + * A class to encompass all of the verbs which deal with operations relative to context. + */ +class ContextVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + ContextVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Context")) + { } +}; // ContextVerb class + +/** + * A class to encompass all of the verbs which deal with zoom operations. + */ +class ZoomVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + ZoomVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("View")) + { } +}; // ZoomVerb class + + +/** + * A class to encompass all of the verbs which deal with dialog operations. + */ +class DialogVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + DialogVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Dialog")) + { } +}; // DialogVerb class + +/** + * A class to encompass all of the verbs which deal with help operations. + */ +class HelpVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + HelpVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Help")) + { } +}; // HelpVerb class + +/** + * A class to encompass all of the verbs which open an URL. + */ +class HelpUrlVerb : public HelpVerb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the HelpVerb initializer with the same parameters. */ + HelpUrlVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + HelpVerb(code, id, name, tip, image) + { } +}; // HelpUrlVerb class + +/** + * A class to encompass all of the verbs which deal with tutorial operations. + */ +class TutorialVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + TutorialVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Help")) + { } +}; // TutorialVerb class + +/** + * A class to encompass all of the verbs which deal with text operations. + */ +class TextVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + TextVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Text")) + { } +}; //TextVerb : public Verb + +Verb::VerbTable Verb::_verbs; +Verb::VerbIDTable Verb::_verb_ids; + +/** + * Create a verb without a code. + * + * This function calls the other constructor for all of the parameters, + * but generates the code. It is important to READ THE OTHER DOCUMENTATION + * it has important details in it. To generate the code a static is + * used which starts at the last static value: \c SP_VERB_LAST. For + * each call it is incremented. The list of allocated verbs is kept + * in the \c _verbs hashtable which is indexed by the \c code. + */ +Verb::Verb(gchar const *id, gchar const *name, gchar const *tip, gchar const *image, gchar const *group) : + _actions(nullptr), + _id(id), + _name(name), + _tip(tip), + _full_tip(nullptr), + _shortcut(0), + _image(image), + _code(0), + _group(group), + _default_sensitive(false) +{ + static int count = SP_VERB_LAST; + + count++; + _code = count; + _verbs.insert(VerbTable::value_type(count, this)); + _verb_ids.insert(VerbIDTable::value_type(_id, this)); +} + +/** + * Destroy a verb. + * + * The only allocated variable is the _actions variable. If it has + * been allocated it is deleted. + */ +Verb::~Verb() +{ + /// \todo all the actions need to be cleaned up first. + delete _actions; + + if (_full_tip) { + g_free(_full_tip); + _full_tip = nullptr; + } +} + +/** + * Verbs are no good without actions. This is a place holder + * for a function that every subclass should write. Most + * can be written using \c make_action_helper. + * + * @param context Which context the action should be created for. + * @return NULL to represent error (this function shouldn't ever be called) + */ +SPAction *Verb::make_action(Inkscape::ActionContext const & /*context*/) +{ + //std::cout << "make_action" << std::endl; + return nullptr; +} + +/** + * Create an action for a \c FileVerb. + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *FileVerb::make_action(Inkscape::ActionContext const & context) +{ + //std::cout << "fileverb: make_action: " << &perform << std::endl; + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c EditVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *EditVerb::make_action(Inkscape::ActionContext const & context) +{ + //std::cout << "editverb: make_action: " << &perform << std::endl; + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c SelectionVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *SelectionVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c LayerVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *LayerVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c ObjectVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *ObjectVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c TagVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param view Which view the action should be created for. + * @return The built action. + */ +SPAction *TagVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c ContextVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *ContextVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c ZoomVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *ZoomVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c DialogVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *DialogVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c HelpVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *HelpVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c HelpUrlVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *HelpUrlVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c TutorialVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *TutorialVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Create an action for a \c TextVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *TextVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * A quick little convenience function to make building actions + * a little bit easier. + * + * This function does a couple of things. The most obvious is that + * it allocates and creates the action. When it does this it + * translates the \c _name and \c _tip variables. This allows them + * to be statically allocated easily, and get translated in the end. Then, + * if the action gets created, a listener is added to the action with + * the vector that is passed in. + * + * @param context Which context the action should be created for. + * @param vector The function vector for the verb. + * @return The created action. + */ +SPAction *Verb::make_action_helper(Inkscape::ActionContext const & context, void (*perform_fun)(SPAction *, void *), void *in_pntr) +{ + SPAction *action; + //std::cout << "Adding action: " << _code << std::endl; + action = sp_action_new(context, _id, _(_name), + _tip ? _(_tip) : nullptr, _image, this); + + if (action == nullptr) return nullptr; + + action->signal_perform.connect( + sigc::bind( + sigc::bind( + sigc::ptr_fun(perform_fun), + in_pntr ? in_pntr : reinterpret_cast<void*>(_code)), + action)); + + return action; +} + +/** + * A function to get an action if it exists, or otherwise to build it. + * + * This function will get the action for a given view for this verb. It + * will create the verb if it can't be found in the ActionTable. Also, + * if the \c ActionTable has not been created, it gets created by this + * function. + * + * If the action is created, it's sensitivity must be determined. The + * default for a new action is that it is sensitive. If the value in + * \c _default_sensitive is \c false, then the sensitivity must be + * removed. Also, if the view being created is based on the same + * document as a view already created, the sensitivity should be the + * same as views on that document. A view with the same document is + * looked for, and the sensitivity is matched. Unfortunately, this is + * currently a linear search. + * + * @param context The action context which this action relates to. + * @return The action, or NULL if there is an error. + */ +SPAction *Verb::get_action(Inkscape::ActionContext const & context) +{ + SPAction *action = nullptr; + + if ( _actions == nullptr ) { + _actions = new ActionTable; + } + ActionTable::iterator action_found = _actions->find(context.getView()); + + if (action_found != _actions->end()) { + action = action_found->second; + } else { + action = this->make_action(context); + + if (action == nullptr) printf("Hmm, NULL in %s\n", _name); + if (!_default_sensitive) { + sp_action_set_sensitive(action, 0); + } else { + for (ActionTable::iterator cur_action = _actions->begin(); + cur_action != _actions->end() && context.getView() != nullptr; + ++cur_action) { + if (cur_action->first != nullptr && cur_action->first->doc() == context.getDocument()) { + sp_action_set_sensitive(action, cur_action->second->sensitive); + break; + } + } + } + + _actions->insert(ActionTable::value_type(context.getView(), action)); + } + + return action; +} + +/* static */ +bool Verb::ensure_desktop_valid(SPAction *action) +{ + if (sp_action_get_desktop(action) != nullptr) { + return true; + } + g_printerr("WARNING: ignoring verb %s - GUI required for this verb.\n", action->id); + return false; +} + +void Verb::sensitive(SPDocument *in_doc, bool in_sensitive) +{ + // printf("Setting sensitivity of \"%s\" to %d\n", _name, in_sensitive); + if (_actions != nullptr) { + for (auto & _action : *_actions) { + if (in_doc == nullptr || (_action.first != nullptr && _action.first->doc() == in_doc)) { + sp_action_set_sensitive(_action.second, in_sensitive ? 1 : 0); + } + } + } + + if (in_doc == nullptr) { + _default_sensitive = in_sensitive; + } + + return; +} + +/** + * Accessor to get the tooltip for verb as localised string. + */ +gchar const *Verb::get_tip() +{ + gchar const *result = nullptr; + if (_tip) { + unsigned int shortcut = sp_shortcut_get_primary(this); + if ( (shortcut != _shortcut) || !_full_tip) { + if (_full_tip) { + g_free(_full_tip); + _full_tip = nullptr; + } + _shortcut = shortcut; + gchar* shortcutString = sp_shortcut_get_label(shortcut); + if (shortcutString) { + _full_tip = g_strdup_printf("%s (%s)", _(_tip), shortcutString); + g_free(shortcutString); + shortcutString = nullptr; + } else { + _full_tip = g_strdup(_(_tip)); + } + } + result = _full_tip; + } + + return result; +} + +void +Verb::name(SPDocument *in_doc, Glib::ustring in_name) +{ + if (_actions != nullptr) { + for (auto & _action : *_actions) { + if (in_doc == nullptr || (_action.first != nullptr && _action.first->doc() == in_doc)) { + sp_action_set_name(_action.second, in_name); + } + } + } +} + +/** + * A function to remove the action associated with a view. + * + * This function looks for the action in \c _actions. If it is + * found then it is unreferenced and the entry in the action + * table is erased. + * + * @param view Which view's actions should be removed. + * @return None + */ +void Verb::delete_view(Inkscape::UI::View::View *view) +{ + if (_actions == nullptr) return; + if (_actions->empty()) return; + +#if 0 + static int count = 0; + std::cout << count++ << std::endl; +#endif + + ActionTable::iterator action_found = _actions->find(view); + + if (action_found != _actions->end()) { + SPAction *action = action_found->second; + _actions->erase(action_found); + g_object_unref(action); + } + + return; +} + +/** + * A function to delete a view from all verbs. + * + * This function first looks through _base_verbs and deteles + * the view from all of those views. If \c _verbs is not empty + * then all of the entries in that table have all of the views + * deleted also. + * + * @param view Which view's actions should be removed. + * @return None + */ +void Verb::delete_all_view(Inkscape::UI::View::View *view) +{ + for (int i = 0; i <= SP_VERB_LAST; i++) { + if (_base_verbs[i]) + _base_verbs[i]->delete_view(view); + } + + if (!_verbs.empty()) { + for (auto & _verb : _verbs) { + Inkscape::Verb *verbpntr = _verb.second; + // std::cout << "Delete In Verb: " << verbpntr->_name << std::endl; + verbpntr->delete_view(view); + } + } + + return; +} + +/** + * A function to turn a \c code into a Verb for dynamically created Verbs. + * + * This function basically just looks through the \c _verbs hash + * table. STL does all the work. + * + * @param code What code is being looked for. + * @return The found Verb of NULL if none is found. + */ +Verb *Verb::get_search(unsigned int code) +{ + Verb *verb = nullptr; + VerbTable::iterator verb_found = _verbs.find(code); + + if (verb_found != _verbs.end()) { + verb = verb_found->second; + } + + return verb; +} + +/** + * Find a Verb using it's ID. + * + * This function uses the \c _verb_ids has table to find the + * verb by it's id. Should be much faster than previous + * implementations. + * + * @param id Which id to search for. + */ +Verb *Verb::getbyid(gchar const *id, bool verbose) +{ + Verb *verb = nullptr; + VerbIDTable::iterator verb_found = _verb_ids.find(id); + + if (verb_found != _verb_ids.end()) { + verb = verb_found->second; + } + + if (verb == nullptr +#ifndef HAVE_ASPELL + && strcmp(id, "DialogSpellcheck") != 0 +#endif + ) { + if (verbose) + printf("Unable to find: %s\n", id); + } + + return verb; +} + +/** + * Decode the verb code and take appropriate action. + */ +void FileVerb::perform(SPAction *action, void *data) +{ + // Convert verb impls to use this where possible, to reduce static cling + // to macros like SP_ACTIVE_DOCUMENT, which end up enforcing GUI-mode operation + SPDocument *doc = sp_action_get_document(action); + + // We can vacuum defs, or exit, without needing a desktop! + bool handled = true; + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_FILE_VACUUM: + sp_file_vacuum(doc); + break; + case SP_VERB_FILE_QUIT: + sp_file_exit(); + break; + default: + handled = false; + break; + } + if (handled) { + return; + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *desktop = sp_action_get_desktop(action); + + Gtk::Window *parent = desktop->getToplevel(); + g_assert(parent != nullptr); + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_FILE_NEW: + sp_file_new_default(); + break; + case SP_VERB_FILE_OPEN: + sp_file_open_dialog(*parent, nullptr, nullptr); + break; + case SP_VERB_FILE_REVERT: + sp_file_revert_dialog(); + break; + case SP_VERB_FILE_SAVE: + sp_file_save(*parent, nullptr, nullptr); + break; + case SP_VERB_FILE_SAVE_AS: + sp_file_save_as(*parent, nullptr, nullptr); + break; + case SP_VERB_FILE_SAVE_A_COPY: + sp_file_save_a_copy(*parent, nullptr, nullptr); + break; + case SP_VERB_FILE_SAVE_TEMPLATE: + Inkscape::UI::Dialog::SaveTemplate::save_document_as_template(*parent); + break; + case SP_VERB_FILE_PRINT: + sp_file_print(*parent); + break; + case SP_VERB_FILE_IMPORT: + prefs->setBool("/options/onimport",true); + sp_file_import(*parent); + prefs->setBool("/options/onimport",false); + break; +// case SP_VERB_FILE_EXPORT: +// sp_file_export_dialog(*parent); +// break; + case SP_VERB_FILE_NEXT_DESKTOP: + INKSCAPE.switch_desktops_next(); + break; + case SP_VERB_FILE_PREV_DESKTOP: + INKSCAPE.switch_desktops_prev(); + break; + case SP_VERB_FILE_CLOSE_VIEW: + sp_ui_close_view(nullptr); + break; + case SP_VERB_FILE_TEMPLATES: + Inkscape::UI::NewFromTemplate::load_new_from_template(); + break; + default: + break; + } + + +} // end of sp_verb_action_file_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void EditVerb::perform(SPAction *action, void *data) +{ + // We can clear all without a desktop + bool handled = true; + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_EDIT_CLEAR_ALL: + sp_edit_clear_all(sp_action_get_selection(action)); + break; + default: + handled = false; + break; + } + if (handled) { + return; + } + + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_EDIT_UNDO: + sp_undo(dt, dt->getDocument()); + break; + case SP_VERB_EDIT_REDO: + sp_redo(dt, dt->getDocument()); + break; + case SP_VERB_EDIT_CUT: + dt->selection->cut(); + break; + case SP_VERB_EDIT_COPY: + dt->selection->copy(); + break; + case SP_VERB_EDIT_PASTE: + sp_selection_paste(dt, false); + break; + case SP_VERB_EDIT_PASTE_STYLE: + dt->selection->pasteStyle(); + break; + case SP_VERB_EDIT_PASTE_SIZE: + dt->selection->pasteSize(true,true); + break; + case SP_VERB_EDIT_PASTE_SIZE_X: + dt->selection->pasteSize(true, false); + break; + case SP_VERB_EDIT_PASTE_SIZE_Y: + dt->selection->pasteSize(false, true); + break; + case SP_VERB_EDIT_PASTE_SIZE_SEPARATELY: + dt->selection->pasteSizeSeparately(true, true); + break; + case SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_X: + dt->selection->pasteSizeSeparately(true, false); + break; + case SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_Y: + dt->selection->pasteSizeSeparately(false, true); + break; + case SP_VERB_EDIT_PASTE_IN_PLACE: + sp_selection_paste(dt, true); + break; + case SP_VERB_EDIT_PASTE_LIVEPATHEFFECT: + dt->selection->pastePathEffect(); + break; + case SP_VERB_EDIT_REMOVE_LIVEPATHEFFECT: + dt->selection->removeLPE(); + break; + case SP_VERB_EDIT_REMOVE_FILTER: + dt->selection->removeFilter(); + break; + case SP_VERB_EDIT_DELETE: + dt->selection->deleteItems(); + break; + case SP_VERB_EDIT_DUPLICATE: + dt->selection->duplicate(); + break; + case SP_VERB_EDIT_CLONE: + dt->selection->clone(); + break; + case SP_VERB_EDIT_UNLINK_CLONE: + dt->selection->unlink(); + break; + case SP_VERB_EDIT_UNLINK_CLONE_RECURSIVE: + dt->selection->unlinkRecursive(false, true); + break; + case SP_VERB_EDIT_RELINK_CLONE: + dt->selection->relink(); + break; + case SP_VERB_EDIT_CLONE_SELECT_ORIGINAL: + dt->selection->cloneOriginal(); + break; + case SP_VERB_EDIT_CLONE_ORIGINAL_PATH_LPE: + dt->selection->cloneOriginalPathLPE(); + break; + case SP_VERB_EDIT_SELECTION_2_MARKER: + dt->selection->toMarker(); + break; + case SP_VERB_EDIT_SELECTION_2_GUIDES: + dt->selection->toGuides(); + break; + case SP_VERB_EDIT_TILE: + dt->selection->tile(); + break; + case SP_VERB_EDIT_UNTILE: + dt->selection->untile(); + break; + case SP_VERB_EDIT_SYMBOL: + dt->selection->toSymbol(); + break; + case SP_VERB_EDIT_UNSYMBOL: + dt->selection->unSymbol(); + break; + case SP_VERB_EDIT_SELECT_ALL: + SelectionHelper::selectAll(dt); + break; + case SP_VERB_EDIT_SELECT_SAME_FILL_STROKE: + SelectionHelper::selectSameFillStroke(dt); + break; + case SP_VERB_EDIT_SELECT_SAME_FILL_COLOR: + SelectionHelper::selectSameFillColor(dt); + break; + case SP_VERB_EDIT_SELECT_SAME_STROKE_COLOR: + SelectionHelper::selectSameStrokeColor(dt); + break; + case SP_VERB_EDIT_SELECT_SAME_STROKE_STYLE: + SelectionHelper::selectSameStrokeStyle(dt); + break; + case SP_VERB_EDIT_SELECT_SAME_OBJECT_TYPE: + SelectionHelper::selectSameObjectType(dt); + break; + case SP_VERB_EDIT_INVERT: + SelectionHelper::invert(dt); + break; + case SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS: + SelectionHelper::selectAllInAll(dt); + break; + case SP_VERB_EDIT_INVERT_IN_ALL_LAYERS: + SelectionHelper::invertAllInAll(dt); + break; + case SP_VERB_EDIT_SELECT_NEXT: + SelectionHelper::selectNext(dt); + break; + case SP_VERB_EDIT_SELECT_PREV: + SelectionHelper::selectPrev(dt); + break; + case SP_VERB_EDIT_DESELECT: + SelectionHelper::selectNone(dt); + break; + case SP_VERB_EDIT_DELETE_ALL_GUIDES: + sp_guide_delete_all_guides(dt); + break; + case SP_VERB_EDIT_GUIDES_TOGGLE_LOCK: + dt->toggleGuidesLock(); + break; + case SP_VERB_EDIT_GUIDES_AROUND_PAGE: + sp_guide_create_guides_around_page(dt); + break; + case SP_VERB_EDIT_NEXT_PATHEFFECT_PARAMETER: + sp_selection_next_patheffect_param(dt); + break; + case SP_VERB_EDIT_SWAP_FILL_STROKE: + dt->selection->swapFillStroke(); + break; + case SP_VERB_EDIT_LINK_COLOR_PROFILE: + break; + case SP_VERB_EDIT_REMOVE_COLOR_PROFILE: + break; + default: + break; + } + +} // end of sp_verb_action_edit_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void SelectionVerb::perform(SPAction *action, void *data) +{ + Inkscape::Selection *selection = sp_action_get_selection(action); + SPDesktop *dt = sp_action_get_desktop(action); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Some of these operations have been modified so they work in command-line mode! + // In this case, all we need is a selection + if (!selection) { + return; + } + + bool handled = true; + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_SELECTION_UNION: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathUnion(); + break; + case SP_VERB_SELECTION_INTERSECT: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathIntersect(); + break; + case SP_VERB_SELECTION_DIFF: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathDiff(); + break; + case SP_VERB_SELECTION_SYMDIFF: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathSymDiff(); + break; + case SP_VERB_SELECTION_CUT: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathCut(); + break; + case SP_VERB_SELECTION_SLICE: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + selection->pathSlice(); + break; + case SP_VERB_SELECTION_GROW: + { + // FIXME these and the other grow/shrink they should use gobble_key_events. + // the problem is how to get access to which key, if any, to gobble. + selection->scale(prefs->getDoubleLimited("/options/defaultscale/value", 2, 0, 1000)); + break; + } + case SP_VERB_SELECTION_GROW_SCREEN: + { + selection->scaleScreen(2); + break; + } + case SP_VERB_SELECTION_GROW_DOUBLE: + { + selection->scaleTimes(2); + break; + } + case SP_VERB_SELECTION_SHRINK: + { + selection->scale(-prefs->getDoubleLimited("/options/defaultscale/value", 2, 0, 1000)); + break; + } + case SP_VERB_SELECTION_SHRINK_SCREEN: + { + selection->scaleScreen(-2); + break; + } + case SP_VERB_SELECTION_SHRINK_HALVE: + { + selection->scaleTimes(0.5); + break; + } + case SP_VERB_SELECTION_TO_FRONT: + selection->raiseToTop(); + break; + case SP_VERB_SELECTION_TO_BACK: + selection->lowerToBottom(); + break; + case SP_VERB_SELECTION_RAISE: + selection->raise(); + break; + case SP_VERB_SELECTION_LOWER: + selection->lower(); + break; + case SP_VERB_SELECTION_STACK_UP: + selection->stackUp(); + break; + case SP_VERB_SELECTION_STACK_DOWN: + selection->stackDown(); + break; + case SP_VERB_SELECTION_GROUP: + selection->group(); + break; + case SP_VERB_SELECTION_UNGROUP: + selection->ungroup(); + break; + case SP_VERB_SELECTION_UNGROUP_POP_SELECTION: + selection->popFromGroup(); + break; + default: + handled = false; + break; + } + + if (handled) { + return; + } + + // The remaining operations require a desktop + g_return_if_fail(ensure_desktop_valid(action)); + + g_assert(dt->_dlg_mgr != nullptr); + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_SELECTION_TEXTTOPATH: + text_put_on_path(); + break; + case SP_VERB_SELECTION_TEXTFROMPATH: + text_remove_from_path(); + break; + case SP_VERB_SELECTION_REMOVE_KERNS: + text_remove_all_kerns(); + break; + + case SP_VERB_SELECTION_OFFSET: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_offset(dt); + break; + case SP_VERB_SELECTION_OFFSET_SCREEN: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_offset_screen(dt, 1); + break; + case SP_VERB_SELECTION_OFFSET_SCREEN_10: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_offset_screen(dt, 10); + break; + case SP_VERB_SELECTION_INSET: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_inset(dt); + break; + case SP_VERB_SELECTION_INSET_SCREEN: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_inset_screen(dt, 1); + break; + case SP_VERB_SELECTION_INSET_SCREEN_10: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_inset_screen(dt, 10); + break; + case SP_VERB_SELECTION_DYNAMIC_OFFSET: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_create_offset_object_zero(dt); + tools_switch(dt, TOOLS_NODES); + break; + case SP_VERB_SELECTION_LINKED_OFFSET: + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + sp_selected_path_create_updating_offset_object_zero(dt); + tools_switch(dt, TOOLS_NODES); + break; + case SP_VERB_SELECTION_OUTLINE: + sp_selected_path_outline(dt); + break; + case SP_VERB_SELECTION_OUTLINE_LEGACY: + sp_selected_path_outline(dt, true); + break; + case SP_VERB_SELECTION_SIMPLIFY: + selection->toCurves(true); + sp_selected_path_simplify(dt); + break; + case SP_VERB_SELECTION_REVERSE: + SelectionHelper::reverse(dt); + break; + case SP_VERB_SELECTION_TRACE: + INKSCAPE.dialogs_unhide(); + dt->_dlg_mgr->showDialog("Trace"); + break; + case SP_VERB_SELECTION_CREATE_BITMAP: + dt->selection->createBitmapCopy(); + break; + + case SP_VERB_SELECTION_COMBINE: + selection->unlinkRecursive(true); + selection->combine(); + break; + case SP_VERB_SELECTION_BREAK_APART: + selection->breakApart(); + break; + case SP_VERB_SELECTION_ARRANGE: + INKSCAPE.dialogs_unhide(); + dt->_dlg_mgr->showDialog("TileDialog"); //FIXME: denis: What's this string (to be changed) + break; + default: + break; + } + +} // end of sp_verb_action_selection_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void LayerVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + size_t verb = reinterpret_cast<std::size_t>(data); + + if ( !dt->currentLayer() ) { + return; + } + + switch (verb) { + case SP_VERB_LAYER_NEW: { + Inkscape::UI::Dialogs::LayerPropertiesDialog::showCreate(dt, dt->currentLayer()); + break; + } + case SP_VERB_LAYER_RENAME: { + Inkscape::UI::Dialogs::LayerPropertiesDialog::showRename(dt, dt->currentLayer()); + break; + } + case SP_VERB_LAYER_NEXT: { + SPObject *next=Inkscape::next_layer(dt->currentRoot(), dt->currentLayer()); + if (next) { + dt->setCurrentLayer(next); + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_NEXT, + _("Switch to next layer")); + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Switched to next layer.")); + } else { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Cannot go past last layer.")); + } + break; + } + case SP_VERB_LAYER_PREV: { + SPObject *prev=Inkscape::previous_layer(dt->currentRoot(), dt->currentLayer()); + if (prev) { + dt->setCurrentLayer(prev); + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_PREV, + _("Switch to previous layer")); + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Switched to previous layer.")); + } else { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Cannot go before first layer.")); + } + break; + } + case SP_VERB_LAYER_MOVE_TO_NEXT: { + dt->selection->toNextLayer(); + break; + } + case SP_VERB_LAYER_MOVE_TO_PREV: { + dt->selection->toPrevLayer(); + break; + } + case SP_VERB_LAYER_MOVE_TO: { + Inkscape::UI::Dialogs::LayerPropertiesDialog::showMove(dt, dt->currentLayer()); + break; + } + case SP_VERB_LAYER_TO_TOP: + case SP_VERB_LAYER_TO_BOTTOM: + case SP_VERB_LAYER_RAISE: + case SP_VERB_LAYER_LOWER: { + if ( dt->currentLayer() == dt->currentRoot() ) { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + return; + } + + SPItem *layer=SP_ITEM(dt->currentLayer()); + g_return_if_fail(layer != nullptr); + + SPObject *old_pos = layer->getNext(); + + switch (verb) { + case SP_VERB_LAYER_TO_TOP: + layer->raiseToTop(); + break; + case SP_VERB_LAYER_TO_BOTTOM: + layer->lowerToBottom(); + break; + case SP_VERB_LAYER_RAISE: + layer->raiseOne(); + break; + case SP_VERB_LAYER_LOWER: + layer->lowerOne(); + break; + } + + if ( layer->getNext() != old_pos ) { + char const *message = nullptr; + Glib::ustring description = ""; + switch (verb) { + case SP_VERB_LAYER_TO_TOP: + message = g_strdup_printf(_("Raised layer <b>%s</b>."), layer->defaultLabel()); + description = _("Layer to top"); + break; + case SP_VERB_LAYER_RAISE: + message = g_strdup_printf(_("Raised layer <b>%s</b>."), layer->defaultLabel()); + description = _("Raise layer"); + break; + case SP_VERB_LAYER_TO_BOTTOM: + message = g_strdup_printf(_("Lowered layer <b>%s</b>."), layer->defaultLabel()); + description = _("Layer to bottom"); + break; + case SP_VERB_LAYER_LOWER: + message = g_strdup_printf(_("Lowered layer <b>%s</b>."), layer->defaultLabel()); + description = _("Lower layer"); + break; + }; + DocumentUndo::done(dt->getDocument(), verb, description); + if (message) { + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, message); + g_free((void *) message); + } + } else { + dt->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Cannot move layer any further.")); + } + + break; + } + case SP_VERB_LAYER_DUPLICATE: { + if ( dt->currentLayer() != dt->currentRoot() ) { + + dt->selection->duplicate(true, true); + + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_DUPLICATE, + _("Duplicate layer")); + + // TRANSLATORS: this means "The layer has been duplicated." + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Duplicated layer.")); + } else { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + } + break; + } + case SP_VERB_LAYER_DELETE: { + if ( dt->currentLayer() != dt->currentRoot() ) { + dt->getSelection()->clear(); + SPObject *old_layer = dt->currentLayer(); + SPObject *old_parent = old_layer->parent; + SPObject *old_parent_parent = (old_parent != nullptr) ? old_parent->parent : nullptr; + + SPObject *survivor = Inkscape::previous_layer(dt->currentRoot(), old_layer); + if (survivor != nullptr && survivor->parent == old_layer) { + while (survivor != nullptr && + survivor->parent != old_parent && + survivor->parent != old_parent_parent) + { + survivor = Inkscape::previous_layer(dt->currentRoot(), survivor); + } + } + + if (survivor == nullptr || (survivor->parent != old_parent && survivor->parent != old_layer)) { + survivor = Inkscape::next_layer(dt->currentRoot(), old_layer); + while (survivor != nullptr && + survivor != old_parent && + survivor->parent != old_parent) + { + survivor = Inkscape::next_layer(dt->currentRoot(), survivor); + } + } + + // Deleting the old layer before switching layers is a hack to trigger the + // listeners of the deletion event (as happens when old_layer is deleted using the + // xml editor). See + // http://sourceforge.net/tracker/index.php?func=detail&aid=1339397&group_id=93438&atid=604306 + // + old_layer->deleteObject(); + + if (survivor) { + dt->setCurrentLayer(survivor); + } + + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_DELETE, + _("Delete layer")); + + // TRANSLATORS: this means "The layer has been deleted." + dt->messageStack()->flash(Inkscape::NORMAL_MESSAGE, _("Deleted layer.")); + } else { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + } + break; + } + case SP_VERB_LAYER_SOLO: { + if ( dt->currentLayer() == dt->currentRoot() ) { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + } else { + dt->toggleLayerSolo( dt->currentLayer() ); + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_SOLO, _("Toggle layer solo")); + } + break; + } + case SP_VERB_LAYER_SHOW_ALL: { + dt->toggleHideAllLayers( false ); + DocumentUndo::maybeDone(dt->getDocument(), "layer:showall", SP_VERB_LAYER_SHOW_ALL, _("Show all layers")); + break; + } + case SP_VERB_LAYER_HIDE_ALL: { + dt->toggleHideAllLayers( true ); + DocumentUndo::maybeDone(dt->getDocument(), "layer:hideall", SP_VERB_LAYER_HIDE_ALL, _("Hide all layers")); + break; + } + case SP_VERB_LAYER_LOCK_ALL: { + dt->toggleLockAllLayers( true ); + DocumentUndo::maybeDone(dt->getDocument(), "layer:lockall", SP_VERB_LAYER_LOCK_ALL, _("Lock all layers")); + break; + } + case SP_VERB_LAYER_LOCK_OTHERS: { + if ( dt->currentLayer() == dt->currentRoot() ) { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + } else { + dt->toggleLockOtherLayers( dt->currentLayer() ); + DocumentUndo::done(dt->getDocument(), SP_VERB_LAYER_LOCK_OTHERS, _("Lock other layers")); + } + break; + } + case SP_VERB_LAYER_UNLOCK_ALL: { + dt->toggleLockAllLayers( false ); + DocumentUndo::maybeDone(dt->getDocument(), "layer:unlockall", SP_VERB_LAYER_UNLOCK_ALL, _("Unlock all layers")); + break; + } + case SP_VERB_LAYER_TOGGLE_LOCK: + case SP_VERB_LAYER_TOGGLE_HIDE: { + if ( dt->currentLayer() == dt->currentRoot() ) { + dt->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("No current layer.")); + } else { + if ( verb == SP_VERB_LAYER_TOGGLE_HIDE ){ + SP_ITEM(dt->currentLayer())->setHidden(!SP_ITEM(dt->currentLayer())->isHidden()); + } else { + SP_ITEM(dt->currentLayer())->setLocked(!SP_ITEM(dt->currentLayer())->isLocked()); + } + + } + } + } + + return; +} // end of sp_verb_action_layer_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void ObjectVerb::perform( SPAction *action, void *data) +{ + SPDesktop *dt = sp_action_get_desktop(action); + Inkscape::Selection *sel = sp_action_get_selection(action); + + // We can perform some actions without a desktop + bool handled = true; + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_OBJECT_TO_CURVE: + sel->toCurves(); + break; + default: + handled = false; + break; + } + if (handled) { + return; + } + + g_return_if_fail(ensure_desktop_valid(action)); + + Inkscape::UI::Tools::ToolBase *ec = dt->event_context; + + if (sel->isEmpty()) + return; + + Geom::OptRect bbox = sel->visualBounds(); + if (!bbox) { + return; + } + // If the rotation center of the selection is visible, choose it as reference point + // for horizontal and vertical flips. Otherwise, take the center of the bounding box. + Geom::Point center; + if (tools_isactive(dt, TOOLS_SELECT) && sel->center() && SP_SELECT_CONTEXT(ec)->_seltrans->centerIsVisible()) + center = *sel->center(); + else + center = bbox->midpoint(); + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_OBJECT_ROTATE_90_CW: + sel->rotate90(false); + break; + case SP_VERB_OBJECT_ROTATE_90_CCW: + sel->rotate90(true); + break; + case SP_VERB_OBJECT_FLATTEN: + sel->removeTransform(); + break; + case SP_VERB_OBJECT_FLOW_TEXT: + text_flow_into_shape(); + break; + case SP_VERB_OBJECT_UNFLOW_TEXT: + text_unflow(); + break; + case SP_VERB_OBJECT_FLOWTEXT_TO_TEXT: + flowtext_to_text(); + break; + case SP_VERB_OBJECT_FLIP_HORIZONTAL: + sel->setScaleRelative(center, Geom::Scale(-1.0, 1.0)); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_FLIP_HORIZONTAL, + _("Flip horizontally")); + break; + case SP_VERB_OBJECT_FLIP_VERTICAL: + sel->setScaleRelative(center, Geom::Scale(1.0, -1.0)); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_FLIP_VERTICAL, + _("Flip vertically")); + break; + case SP_VERB_OBJECT_SET_MASK: + sel->setMask(false, false); + break; + case SP_VERB_OBJECT_SET_INVERSE_MASK: + sel->setMask(false, false); + Inkscape::LivePathEffect::sp_inverse_powermask(sp_action_get_selection(action)); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_SET_INVERSE_MASK, _("_Set Inverse (LPE)")); + break; + case SP_VERB_OBJECT_EDIT_MASK: + sel->editMask(false); + break; + case SP_VERB_OBJECT_UNSET_MASK: + Inkscape::LivePathEffect::sp_remove_powermask(sp_action_get_selection(action)); + sel->unsetMask(false); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_UNSET_MASK, _("Release mask")); + break; + case SP_VERB_OBJECT_SET_CLIPPATH: + sel->setMask(true, false); + break; + case SP_VERB_OBJECT_SET_INVERSE_CLIPPATH: + sel->setMask(true, false); + Inkscape::LivePathEffect::sp_inverse_powerclip(sp_action_get_selection(action)); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, _("_Set Inverse (LPE)")); + break; + case SP_VERB_OBJECT_CREATE_CLIP_GROUP: + sel->setClipGroup(); + break; + case SP_VERB_OBJECT_EDIT_CLIPPATH: + sel->editMask(true); + break; + case SP_VERB_OBJECT_UNSET_CLIPPATH: + Inkscape::LivePathEffect::sp_remove_powerclip(sp_action_get_selection(action)); + sel->unsetMask(true); + DocumentUndo::done(dt->getDocument(), SP_VERB_OBJECT_UNSET_CLIPPATH, _("Release clipping path")); + + break; + default: + break; + } + +} // end of sp_verb_action_object_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void TagVerb::perform( SPAction *action, void *data) +{ + SPDesktop *dt = static_cast<SPDesktop*>(sp_action_get_view(action)); + if (!dt) + return; + + Inkscape::XML::Document * doc; + Inkscape::XML::Node * repr; + gchar *id; + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_TAG_NEW: + static int tag_suffix=1; + id=nullptr; + do { + g_free(id); + id = g_strdup_printf(_("Set %d"), tag_suffix++); + } while (dt->doc()->getObjectById(id)); + + doc = dt->doc()->getReprDoc(); + repr = doc->createElement("inkscape:tag"); + repr->setAttribute("id", id); + g_free(id); + + dt->doc()->getDefs()->addChild(repr, nullptr); + Inkscape::DocumentUndo::done(dt->doc(), SP_VERB_DIALOG_TAGS, _("Create new selection set")); + break; + default: + break; + } + +} // end of sp_verb_action_tag_perform() + + +/** + * Decode the verb code and take appropriate action. + */ +void ContextVerb::perform(SPAction *action, void *data) +{ + SPDesktop *dt; + sp_verb_t verb; + int vidx; + + g_return_if_fail(ensure_desktop_valid(action)); + dt = sp_action_get_desktop(action); + + verb = (sp_verb_t)GPOINTER_TO_INT((gpointer)data); + + /** \todo !!! hopefully this can go away soon and actions can look after + * themselves + */ + for (vidx = SP_VERB_CONTEXT_SELECT; vidx <= SP_VERB_CONTEXT_LPETOOL_PREFS; vidx++) + { + SPAction *tool_action= get((sp_verb_t)vidx)->get_action(action->context); + if (tool_action) { + sp_action_set_active(tool_action, vidx == (int)verb); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (verb) { + case SP_VERB_CONTEXT_SELECT: + tools_switch(dt, TOOLS_SELECT); + break; + case SP_VERB_CONTEXT_NODE: + tools_switch(dt, TOOLS_NODES); + break; + case SP_VERB_CONTEXT_TWEAK: + tools_switch(dt, TOOLS_TWEAK); + break; + case SP_VERB_CONTEXT_SPRAY: + tools_switch(dt, TOOLS_SPRAY); + break; + case SP_VERB_CONTEXT_RECT: + tools_switch(dt, TOOLS_SHAPES_RECT); + break; + case SP_VERB_CONTEXT_3DBOX: + tools_switch(dt, TOOLS_SHAPES_3DBOX); + break; + case SP_VERB_CONTEXT_ARC: + tools_switch(dt, TOOLS_SHAPES_ARC); + break; + case SP_VERB_CONTEXT_STAR: + tools_switch(dt, TOOLS_SHAPES_STAR); + break; + case SP_VERB_CONTEXT_SPIRAL: + tools_switch(dt, TOOLS_SHAPES_SPIRAL); + break; + case SP_VERB_CONTEXT_PENCIL: + tools_switch(dt, TOOLS_FREEHAND_PENCIL); + break; + case SP_VERB_CONTEXT_PEN: + tools_switch(dt, TOOLS_FREEHAND_PEN); + break; + case SP_VERB_CONTEXT_CALLIGRAPHIC: + tools_switch(dt, TOOLS_CALLIGRAPHIC); + break; + case SP_VERB_CONTEXT_TEXT: + tools_switch(dt, TOOLS_TEXT); + break; + case SP_VERB_CONTEXT_GRADIENT: + tools_switch(dt, TOOLS_GRADIENT); + break; + case SP_VERB_CONTEXT_MESH: + tools_switch(dt, TOOLS_MESH); + break; + case SP_VERB_CONTEXT_ZOOM: + tools_switch(dt, TOOLS_ZOOM); + break; + case SP_VERB_CONTEXT_MEASURE: + tools_switch(dt, TOOLS_MEASURE); + break; + case SP_VERB_CONTEXT_DROPPER: + Inkscape::UI::Tools::sp_toggle_dropper(dt); // Functionality defined in event-context.cpp + break; + case SP_VERB_CONTEXT_CONNECTOR: + tools_switch(dt, TOOLS_CONNECTOR); + break; + case SP_VERB_CONTEXT_PAINTBUCKET: + tools_switch(dt, TOOLS_PAINTBUCKET); + break; + case SP_VERB_CONTEXT_ERASER: + tools_switch(dt, TOOLS_ERASER); + break; + case SP_VERB_CONTEXT_LPETOOL: + tools_switch(dt, TOOLS_LPETOOL); + break; + + case SP_VERB_CONTEXT_SELECT_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SELECTOR); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_NODE_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_NODE); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_TWEAK_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_TWEAK); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_SPRAY_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SPRAY); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_RECT_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SHAPES_RECT); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_3DBOX_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SHAPES_3DBOX); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_ARC_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SHAPES_ELLIPSE); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_STAR_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SHAPES_STAR); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_SPIRAL_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_SHAPES_SPIRAL); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_PENCIL_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_PENCIL); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_PEN_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_PEN); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_CALLIGRAPHY); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_TEXT_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_TEXT); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_GRADIENT_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_GRADIENT); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_MESH_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_GRADIENT); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_ZOOM_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_ZOOM); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_MEASURE_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_MEASURE); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_DROPPER_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_DROPPER); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_CONNECTOR_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_CONNECTOR); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_PAINTBUCKET_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_PAINTBUCKET); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_ERASER_PREFS: + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_ERASER); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_CONTEXT_LPETOOL_PREFS: + g_print ("TODO: Create preferences page for LPETool\n"); + prefs->setInt("/dialogs/preferences/page", PREFS_PAGE_TOOLS_LPETOOL); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR: + case SP_VERB_ALIGN_HORIZONTAL_LEFT: + case SP_VERB_ALIGN_HORIZONTAL_CENTER: + case SP_VERB_ALIGN_HORIZONTAL_RIGHT: + case SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR: + case SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR: + case SP_VERB_ALIGN_VERTICAL_TOP: + case SP_VERB_ALIGN_VERTICAL_CENTER: + case SP_VERB_ALIGN_VERTICAL_BOTTOM: + case SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR: + case SP_VERB_ALIGN_BOTH_TOP_LEFT: + case SP_VERB_ALIGN_BOTH_TOP_RIGHT: + case SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT: + case SP_VERB_ALIGN_BOTH_BOTTOM_LEFT: + case SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR: + case SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR: + case SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR: + case SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR: + case SP_VERB_ALIGN_BOTH_CENTER: + ActionAlign::do_verb_action(dt, verb); + break; + + default: + break; + } + +} // end of sp_verb_action_ctx_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void TextVerb::perform(SPAction *action, void */*data*/) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + + SPDocument *doc = dt->getDocument(); + (void)doc; + Inkscape::XML::Node *repr = dt->namedview->getRepr(); + (void)repr; +} + +/** + * Decode the verb code and take appropriate action. + */ +void ZoomVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + Inkscape::UI::Tools::ToolBase *ec = dt->event_context; + + SPDocument *doc = dt->getDocument(); + + Inkscape::XML::Node *repr = dt->namedview->getRepr(); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gdouble zoom_inc = + prefs->getDoubleLimited( "/options/zoomincrement/value", M_SQRT2, 1.01, 10 ); + gdouble rotate_inc = + prefs->getDoubleLimited( "/options/rotateincrement/value", 15, 1, 90, "°" ); + rotate_inc *= M_PI/180.0; + + double zcorr = prefs->getDouble("/options/zoomcorrection/value", 1.0); + + //Geom::Rect const d = dt->get_display_area(); + + Geom::Rect const d_canvas = dt->getCanvas()->getViewbox(); // Not SVG 'viewBox' + Geom::Point midpoint = dt->w2d(d_canvas.midpoint()); // Midpoint of drawing on canvas. + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_ZOOM_IN: + { + gint mul = 1 + Inkscape::UI::Tools::gobble_key_events( + GDK_KEY_KP_Add, 0); // with any mask + // FIXME what if zoom out is bound to something other than subtract? + // While drawing with the pen/pencil tool, zoom towards the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const zoom_to (*rc->last_point()); + dt->zoom_relative_keep_point(zoom_to, mul*zoom_inc); + break; + } + } + + dt->zoom_relative_center_point( midpoint, mul*zoom_inc); + break; + } + case SP_VERB_ZOOM_OUT: + { + gint mul = 1 + Inkscape::UI::Tools::gobble_key_events( + GDK_KEY_KP_Subtract, 0); // with any mask + // While drawing with the pen/pencil tool, zoom away from the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const zoom_to (*rc->last_point()); + dt->zoom_relative_keep_point(zoom_to, 1 / (mul*zoom_inc)); + break; + } + } + + dt->zoom_relative_center_point( midpoint, 1 / (mul*zoom_inc) ); + break; + } + case SP_VERB_ZOOM_1_1: + dt->zoom_absolute_center_point( midpoint, 1.0 * zcorr ); + break; + case SP_VERB_ZOOM_1_2: + dt->zoom_absolute_center_point( midpoint, 0.5 * zcorr ); + break; + case SP_VERB_ZOOM_2_1: + dt->zoom_absolute_center_point( midpoint, 2.0 * zcorr ); + break; + case SP_VERB_ZOOM_PAGE: + dt->zoom_page(); + break; + case SP_VERB_ZOOM_PAGE_WIDTH: + dt->zoom_page_width(); + break; + case SP_VERB_ZOOM_DRAWING: + dt->zoom_drawing(); + break; + case SP_VERB_ZOOM_SELECTION: + dt->zoom_selection(); + break; + case SP_VERB_ZOOM_NEXT: + dt->next_transform(); + break; + case SP_VERB_ZOOM_PREV: + dt->prev_transform(); + break; + case SP_VERB_ZOOM_CENTER_PAGE: + dt->zoom_center_page(); + break; + case SP_VERB_TOGGLE_ROTATION_LOCK: + dt->toggle_rotation_lock(); + break; + case SP_VERB_ROTATE_CW: + { + gint mul = 1 + Inkscape::UI::Tools::gobble_key_events( GDK_KEY_parenleft, 0); + // While drawing with the pen/pencil tool, rotate towards the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const rotate_to (*rc->last_point()); + dt->rotate_relative_keep_point(rotate_to, mul * rotate_inc); + break; + } + } + + dt->rotate_relative_center_point(midpoint, mul * rotate_inc); + break; + } + case SP_VERB_ROTATE_CCW: + { + gint mul = 1 + Inkscape::UI::Tools::gobble_key_events( GDK_KEY_parenright, 0); + // While drawing with the pen/pencil tool, rotate towards the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const rotate_to (*rc->last_point()); + dt->rotate_relative_keep_point(rotate_to, -mul * rotate_inc); + break; + } + } + + dt->rotate_relative_center_point(midpoint, -mul * rotate_inc); + break; + } + case SP_VERB_ROTATE_ZERO: + dt->rotate_absolute_center_point(midpoint, 0.0); + break; + case SP_VERB_FLIP_HORIZONTAL: + { + // While drawing with the pen/pencil tool, flip towards the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const flip_to (*rc->last_point()); + dt->flip_relative_keep_point(flip_to, SPDesktop::FLIP_HORIZONTAL); + break; + } + } + + dt->flip_relative_center_point( midpoint, SPDesktop::FLIP_HORIZONTAL); + break; + } + case SP_VERB_FLIP_VERTICAL: + { + /* gint mul = 1 + */ Inkscape::UI::Tools::gobble_key_events( GDK_KEY_parenright, 0); + // While drawing with the pen/pencil tool, flip towards the end of the unfinished path + if (tools_isactive(dt, TOOLS_FREEHAND_PENCIL) || tools_isactive(dt, TOOLS_FREEHAND_PEN)) { + SPCurve *rc = SP_DRAW_CONTEXT(ec)->red_curve; + if (!rc->is_empty()) { + Geom::Point const flip_to (*rc->last_point()); + dt->flip_relative_keep_point(flip_to, SPDesktop::FLIP_VERTICAL); + break; + } + } + + dt->flip_relative_center_point( midpoint, SPDesktop::FLIP_VERTICAL); + break; + } + case SP_VERB_FLIP_NONE: + dt->flip_absolute_center_point( midpoint, SPDesktop::FLIP_NONE); + break; + case SP_VERB_TOGGLE_RULERS: + dt->toggleRulers(); + break; + case SP_VERB_TOGGLE_SCROLLBARS: + dt->toggleScrollbars(); + break; + case SP_VERB_TOGGLE_COMMANDS_TOOLBAR: + dt->toggleToolbar("commands", SP_VERB_TOGGLE_COMMANDS_TOOLBAR); + break; + case SP_VERB_TOGGLE_SNAP_TOOLBAR: + dt->toggleToolbar("snaptoolbox", SP_VERB_TOGGLE_SNAP_TOOLBAR); + break; + case SP_VERB_TOGGLE_TOOL_TOOLBAR: + dt->toggleToolbar("toppanel", SP_VERB_TOGGLE_TOOL_TOOLBAR); + break; + case SP_VERB_TOGGLE_TOOLBOX: + dt->toggleToolbar("toolbox", SP_VERB_TOGGLE_TOOLBOX); + break; + case SP_VERB_TOGGLE_PALETTE: + dt->toggleToolbar("panels", SP_VERB_TOGGLE_PALETTE); + break; + case SP_VERB_TOGGLE_STATUSBAR: + dt->toggleToolbar("statusbar", SP_VERB_TOGGLE_STATUSBAR); + break; + case SP_VERB_TOGGLE_GUIDES: + sp_namedview_toggle_guides(doc, dt->namedview); + break; + case SP_VERB_TOGGLE_SNAPPING: + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + dt->toggleSnapGlobal(); + break; + } + case SP_VERB_TOGGLE_GRID: + dt->toggleGrids(); + break; + case SP_VERB_FULLSCREEN: + dt->fullscreen(); + break; + case SP_VERB_FULLSCREENFOCUS: + dt->fullscreen(); + dt->focusMode(!dt->is_fullscreen()); + break; + case SP_VERB_FOCUSTOGGLE: + dt->focusMode(!dt->is_focusMode()); + break; + case SP_VERB_VIEW_NEW: + sp_ui_new_view(); + break; + case SP_VERB_VIEW_MODE_NORMAL: + dt->setDisplayModeNormal(); + break; + case SP_VERB_VIEW_MODE_NO_FILTERS: + dt->setDisplayModeNoFilters(); + break; + case SP_VERB_VIEW_MODE_OUTLINE: + dt->setDisplayModeOutline(); + break; + case SP_VERB_VIEW_MODE_VISIBLE_HAIRLINES: + dt->setDisplayModeVisibleHairlines(); + break; + case SP_VERB_VIEW_MODE_TOGGLE: + dt->displayModeToggle(); + break; + case SP_VERB_VIEW_COLOR_MODE_NORMAL: + dt->setDisplayColorModeNormal(); + break; + case SP_VERB_VIEW_COLOR_MODE_GRAYSCALE: + dt->setDisplayColorModeGrayscale(); + break; +// case SP_VERB_VIEW_COLOR_MODE_PRINT_COLORS_PREVIEW: +// dt->setDisplayColorModePrintColorsPreview(); +// break; + case SP_VERB_VIEW_COLOR_MODE_TOGGLE: + dt->displayColorModeToggle(); + break; + case SP_VERB_VIEW_TOGGLE_SPLIT: + dt->toggleSplitMode(); + break; + case SP_VERB_VIEW_TOGGLE_XRAY: + dt->toggleXRay(); + break; + case SP_VERB_VIEW_CMS_TOGGLE: + dt->toggleColorProfAdjust(); + break; + case SP_VERB_VIEW_ICON_PREVIEW: + INKSCAPE.dialogs_unhide(); + dt->_dlg_mgr->showDialog("IconPreviewPanel"); + break; + + default: + break; + } + // this is not needed canvas is updated correctly in all + // dt->updateNow(); + +} // end of sp_verb_action_zoom_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void DialogVerb::perform(SPAction *action, void *data) +{ + if (reinterpret_cast<std::size_t>(data) != SP_VERB_DIALOG_TOGGLE) { + // unhide all when opening a new dialog + INKSCAPE.dialogs_unhide(); + } + + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + g_assert(dt->_dlg_mgr != nullptr); + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_DIALOG_PROTOTYPE: + dt->_dlg_mgr->showDialog("Prototype"); + break; + case SP_VERB_DIALOG_DISPLAY: + //sp_display_dialog(); + dt->_dlg_mgr->showDialog("InkscapePreferences"); + break; + case SP_VERB_DIALOG_METADATA: + // sp_desktop_dialog(); + dt->_dlg_mgr->showDialog("DocumentMetadata"); + break; + case SP_VERB_DIALOG_NAMEDVIEW: + // sp_desktop_dialog(); + dt->_dlg_mgr->showDialog("DocumentProperties"); + break; + case SP_VERB_DIALOG_FILL_STROKE: + dt->_dlg_mgr->showDialog("FillAndStroke"); + break; + case SP_VERB_DIALOG_GLYPHS: + dt->_dlg_mgr->showDialog("Glyphs"); + break; + case SP_VERB_DIALOG_SWATCHES: + dt->_dlg_mgr->showDialog("Swatches"); + break; + case SP_VERB_DIALOG_SYMBOLS: + dt->_dlg_mgr->showDialog("Symbols"); + break; + case SP_VERB_DIALOG_PAINT: + dt->_dlg_mgr->showDialog("PaintServers"); + break; + case SP_VERB_DIALOG_TRANSFORM: + dt->_dlg_mgr->showDialog("Transformation"); + break; + case SP_VERB_DIALOG_ALIGN_DISTRIBUTE: + dt->_dlg_mgr->showDialog("AlignAndDistribute"); + break; + case SP_VERB_DIALOG_SPRAY_OPTION: + dt->_dlg_mgr->showDialog("SprayOptionClass"); + break; + case SP_VERB_DIALOG_TEXT: + dt->_dlg_mgr->showDialog("TextFont"); + break; + case SP_VERB_DIALOG_XML_EDITOR: + dt->_dlg_mgr->showDialog("XmlTree"); + break; + case SP_VERB_DIALOG_SELECTORS: + dt->_dlg_mgr->showDialog("Selectors"); + break; + case SP_VERB_DIALOG_FIND: + dt->_dlg_mgr->showDialog("Find"); + break; +#if HAVE_ASPELL + case SP_VERB_DIALOG_SPELLCHECK: + dt->_dlg_mgr->showDialog("SpellCheck"); + break; +#endif + case SP_VERB_DIALOG_DEBUG: + dt->_dlg_mgr->showDialog("Messages"); + break; + case SP_VERB_DIALOG_UNDO_HISTORY: + dt->_dlg_mgr->showDialog("UndoHistory"); + break; + case SP_VERB_DIALOG_TOGGLE: + INKSCAPE.dialogs_toggle(); + break; + case SP_VERB_DIALOG_CLONETILER: + //clonetiler_dialog(); + dt->_dlg_mgr->showDialog("CloneTiler"); + break; + case SP_VERB_DIALOG_ATTR: + //sp_item_dialog(); + dt->_dlg_mgr->showDialog("ObjectAttributes"); + break; + case SP_VERB_DIALOG_ITEM: + //sp_item_dialog(); + dt->_dlg_mgr->showDialog("ObjectProperties"); + break; + case SP_VERB_DIALOG_INPUT: + dt->_dlg_mgr->showDialog("InputDevices"); + break; + case SP_VERB_DIALOG_EXPORT: + dt->_dlg_mgr->showDialog("Export"); + break; + case SP_VERB_DIALOG_EXTENSIONEDITOR: + dt->_dlg_mgr->showDialog("ExtensionEditor"); + break; + case SP_VERB_DIALOG_LAYERS: + dt->_dlg_mgr->showDialog("LayersPanel"); + break; + case SP_VERB_DIALOG_OBJECTS: + dt->_dlg_mgr->showDialog("ObjectsPanel"); + break; + case SP_VERB_DIALOG_TAGS: + dt->_dlg_mgr->showDialog("TagsPanel"); + break; + case SP_VERB_DIALOG_LIVE_PATH_EFFECT: + dt->_dlg_mgr->showDialog("LivePathEffect"); + break; + case SP_VERB_DIALOG_FILTER_EFFECTS: + dt->_dlg_mgr->showDialog("FilterEffectsDialog"); + break; + case SP_VERB_DIALOG_SVG_FONTS: + dt->_dlg_mgr->showDialog("SvgFontsDialog"); + break; + case SP_VERB_DIALOG_PRINT_COLORS_PREVIEW: + dt->_dlg_mgr->showDialog("PrintColorsPreviewDialog"); + break; + case SP_VERB_DIALOG_STYLE: + dt->_dlg_mgr->showDialog("StyleDialog"); + break; + default: + break; + } +} // end of sp_verb_action_dialog_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void HelpVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + g_assert(dt->_dlg_mgr != nullptr); + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_HELP_ABOUT: + sp_help_about(); + break; + case SP_VERB_HELP_ABOUT_EXTENSIONS: + // Inkscape::UI::Dialogs::ExtensionsPanel *panel = new Inkscape::UI::Dialogs::ExtensionsPanel(); + // panel->set_full(true); + // show_panel( *panel, "dialogs.aboutextensions", SP_VERB_HELP_ABOUT_EXTENSIONS ); + break; + case SP_VERB_HELP_MEMORY: + INKSCAPE.dialogs_unhide(); + dt->_dlg_mgr->showDialog("Memory"); + break; + default: + break; + } +} // end of sp_verb_action_help_perform() + +/** + * Decode the verb code and take appropriate action. + */ +void HelpUrlVerb::perform(SPAction *action, void *data) +{ + // get current window + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *desktop = sp_action_get_desktop(action); + Gtk::Window *window = desktop->getToplevel(); + + // get URL + Glib::ustring url; + + static const char *lang = _("en"); // TODO: strip /en/ for English version? + static const char *version = "100"; // TODO: make this auto-updating? + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_HELP_URL_ASK_QUESTION: + url = Glib::ustring::compose("https://inkscape.org/%1/community/", lang, version); + break; + case SP_VERB_HELP_URL_MAN: + url = Glib::ustring::compose("https://inkscape.org/%1/doc/inkscape-man%2.html", lang, version); + break; + case SP_VERB_HELP_URL_FAQ: + url = Glib::ustring::compose("https://inkscape.org/%1/learn/faq/", lang); + break; + case SP_VERB_HELP_URL_KEYS: + url = Glib::ustring::compose("https://inkscape.org/%1/doc/keys%2.html", lang, version); + break; + case SP_VERB_HELP_URL_RELEASE_NOTES: + url = Glib::ustring::compose("https://inkscape.org/%1/release/inkscape-1.0", lang, version); + break; + case SP_VERB_HELP_URL_REPORT_BUG: + url = Glib::ustring::compose("https://inkscape.org/%1/contribute/report-bugs/", lang); + break; + case SP_VERB_HELP_URL_MANUAL: + url = "http://tavmjong.free.fr/INKSCAPE/MANUAL/html/index.php"; + break; + case SP_VERB_HELP_URL_SVG11_SPEC: + url = "http://www.w3.org/TR/SVG11/"; + break; + case SP_VERB_HELP_URL_SVG2_SPEC: + url = "http://www.w3.org/TR/SVG2/"; + break; + default: + g_assert_not_reached(); + return; + } + + // open URL for current window + sp_help_open_url(url, window); +} + +/** + * Decode the verb code and take appropriate action. + */ +void TutorialVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_TUTORIAL_BASIC: + sp_help_open_tutorial("tutorial-basic"); + break; + case SP_VERB_TUTORIAL_SHAPES: + sp_help_open_tutorial("tutorial-shapes"); + break; + case SP_VERB_TUTORIAL_ADVANCED: + sp_help_open_tutorial("tutorial-advanced"); + break; + case SP_VERB_TUTORIAL_TRACING: + sp_help_open_tutorial("tutorial-tracing"); + break; + case SP_VERB_TUTORIAL_TRACING_PIXELART: + sp_help_open_tutorial("tutorial-tracing-pixelart"); + break; + case SP_VERB_TUTORIAL_CALLIGRAPHY: + sp_help_open_tutorial("tutorial-calligraphy"); + break; + case SP_VERB_TUTORIAL_INTERPOLATE: + sp_help_open_tutorial("tutorial-interpolate"); + break; + case SP_VERB_TUTORIAL_DESIGN: + sp_help_open_tutorial("tutorial-elements"); + break; + case SP_VERB_TUTORIAL_TIPS: + sp_help_open_tutorial("tutorial-tips"); + break; + default: + break; + } +} // end of sp_verb_action_tutorial_perform() + +// *********** Effect Last ********** + +/** + * A class to represent the last effect issued. + */ +class EffectLastVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + EffectLastVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Extensions")) + { + set_default_sensitive(false); + } +}; // EffectLastVerb class + +/** + * Create an action for a \c EffectLastVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *EffectLastVerb::make_action(Inkscape::ActionContext const & context) +{ + return make_action_helper(context, &perform); +} + +/** + * Decode the verb code and take appropriate action. + */ +void EffectLastVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + Inkscape::UI::View::View *current_view = sp_action_get_view(action); + + Inkscape::Extension::Effect *effect = Inkscape::Extension::Effect::get_last_effect(); + + if (effect == nullptr) return; + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_EFFECT_LAST_PREF: + effect->prefs(current_view); + break; + case SP_VERB_EFFECT_LAST: + effect->effect(current_view); + break; + default: + return; + } + + return; +} +// *********** End Effect Last ********** + +// *********** Fit Canvas ********** + +/** + * A class to represent the canvas fitting verbs. + */ +class FitCanvasVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + FitCanvasVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("View")) + { } +}; // FitCanvasVerb class + +/** + * Create an action for a \c FitCanvasVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *FitCanvasVerb::make_action(Inkscape::ActionContext const & context) +{ + SPAction *action = make_action_helper(context, &perform); + return action; +} + +/** + * Decode the verb code and take appropriate action. + */ +void FitCanvasVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + SPDocument *doc = dt->getDocument(); + if (!doc) return; + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_FIT_CANVAS_TO_SELECTION: + dt->selection->fitCanvas(true); + break; + case SP_VERB_FIT_CANVAS_TO_DRAWING: + verb_fit_canvas_to_drawing(dt); + break; + case SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING: + fit_canvas_to_selection_or_drawing(dt); + break; + default: + return; + } + + return; +} +// *********** End Fit Canvas ********** + + +// *********** Lock'N'Hide ********** + +/** + * A class to represent the object unlocking and unhiding verbs. + */ +class LockAndHideVerb : public Verb { +private: + static void perform(SPAction *action, void *mydata); +protected: + SPAction *make_action(Inkscape::ActionContext const & context) override; +public: + /** Use the Verb initializer with the same parameters. */ + LockAndHideVerb(unsigned int const code, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image) : + Verb(code, id, name, tip, image, _("Layer")) + { + set_default_sensitive(true); + } +}; // LockAndHideVerb class + +/** + * Create an action for a \c LockAndHideVerb. + * + * Calls \c make_action_helper with the \c vector. + * + * @param context Which context the action should be created for. + * @return The built action. + */ +SPAction *LockAndHideVerb::make_action(Inkscape::ActionContext const & context) +{ + SPAction *action = make_action_helper(context, &perform); + return action; +} + +/** + * Decode the verb code and take appropriate action. + */ +void LockAndHideVerb::perform(SPAction *action, void *data) +{ + g_return_if_fail(ensure_desktop_valid(action)); + SPDesktop *dt = sp_action_get_desktop(action); + SPDocument *doc = dt->getDocument(); + if (!doc) return; + + switch (reinterpret_cast<std::size_t>(data)) { + case SP_VERB_UNLOCK_ALL: + unlock_all(dt); + DocumentUndo::done(doc, SP_VERB_UNLOCK_ALL, _("Unlock all objects in the current layer")); + break; + case SP_VERB_UNLOCK_ALL_IN_ALL_LAYERS: + unlock_all_in_all_layers(dt); + DocumentUndo::done(doc, SP_VERB_UNLOCK_ALL_IN_ALL_LAYERS, _("Unlock all objects in all layers")); + break; + case SP_VERB_UNHIDE_ALL: + unhide_all(dt); + DocumentUndo::done(doc, SP_VERB_UNHIDE_ALL, _("Unhide all objects in the current layer")); + break; + case SP_VERB_UNHIDE_ALL_IN_ALL_LAYERS: + unhide_all_in_all_layers(dt); + DocumentUndo::done(doc, SP_VERB_UNHIDE_ALL_IN_ALL_LAYERS, _("Unhide all objects in all layers")); + break; + default: + return; + } + + return; +} +// *********** End Lock'N'Hide ********** + + +// these must be in the same order as the SP_VERB_* enum in "verbs.h" +Verb *Verb::_base_verbs[] = { + // Header + new Verb(SP_VERB_INVALID, nullptr, nullptr, nullptr, nullptr, nullptr), + new Verb(SP_VERB_NONE, "None", NC_("Verb", "None"), N_("Does nothing"), nullptr, nullptr), + + // File + new FileVerb(SP_VERB_FILE_NEW, "FileNew", N_("_New"), N_("Create new document from the default template"), + INKSCAPE_ICON("document-new")), + new FileVerb(SP_VERB_FILE_OPEN, "FileOpen", N_("_Open..."), N_("Open an existing document"), + INKSCAPE_ICON("document-open")), + new FileVerb(SP_VERB_FILE_REVERT, "FileRevert", N_("Re_vert"), + N_("Revert to the last saved version of document (changes will be lost)"), + INKSCAPE_ICON("document-revert")), + new FileVerb(SP_VERB_FILE_SAVE, "FileSave", N_("_Save"), N_("Save document"), INKSCAPE_ICON("document-save")), + new FileVerb(SP_VERB_FILE_SAVE_AS, "FileSaveAs", N_("Save _As..."), N_("Save document under a new name"), + INKSCAPE_ICON("document-save-as")), + new FileVerb(SP_VERB_FILE_SAVE_A_COPY, "FileSaveACopy", N_("Save a Cop_y..."), + N_("Save a copy of the document under a new name"), nullptr), + new FileVerb(SP_VERB_FILE_SAVE_TEMPLATE, "FileSaveTemplate", N_("Save Template..."), + N_("Save a copy of the document as template"), nullptr), + new FileVerb(SP_VERB_FILE_PRINT, "FilePrint", N_("_Print..."), N_("Print document"), + INKSCAPE_ICON("document-print")), + // TRANSLATORS: "Vacuum Defs" means "Clean up defs" (so as to remove unused definitions) + new FileVerb( + SP_VERB_FILE_VACUUM, "FileVacuum", N_("Clean _Up Document"), + N_("Remove unused definitions (such as gradients or clipping paths) from the <defs> of the document"), + INKSCAPE_ICON("document-cleanup")), + new FileVerb(SP_VERB_FILE_IMPORT, "FileImport", N_("_Import..."), + N_("Import a bitmap or SVG image into this document"), INKSCAPE_ICON("document-import")), + // new FileVerb(SP_VERB_FILE_EXPORT, "FileExport", N_("_Export Bitmap..."), N_("Export this document or a + // selection as a bitmap image"), INKSCAPE_ICON("document-export")), + new FileVerb(SP_VERB_FILE_NEXT_DESKTOP, "NextWindow", N_("N_ext Window"), N_("Switch to the next document window"), + INKSCAPE_ICON("window-next")), + new FileVerb(SP_VERB_FILE_PREV_DESKTOP, "PrevWindow", N_("P_revious Window"), + N_("Switch to the previous document window"), INKSCAPE_ICON("window-previous")), + new FileVerb(SP_VERB_FILE_CLOSE_VIEW, "FileClose", N_("_Close"), N_("Close this document window"), + INKSCAPE_ICON("window-close")), + new FileVerb(SP_VERB_FILE_QUIT, "FileQuit", N_("_Quit"), N_("Quit Inkscape"), INKSCAPE_ICON("application-exit")), + new FileVerb(SP_VERB_FILE_TEMPLATES, "FileTemplates", N_("New from _Template..."), + N_("Create new project from template"), INKSCAPE_ICON("dialog-templates")), + + // Edit + new EditVerb(SP_VERB_EDIT_UNDO, "EditUndo", N_("_Undo"), N_("Undo last action"), INKSCAPE_ICON("edit-undo")), + new EditVerb(SP_VERB_EDIT_REDO, "EditRedo", N_("_Redo"), N_("Do again the last undone action"), + INKSCAPE_ICON("edit-redo")), + new EditVerb(SP_VERB_EDIT_CUT, "EditCut", N_("Cu_t"), N_("Cut selection to clipboard"), INKSCAPE_ICON("edit-cut")), + new EditVerb(SP_VERB_EDIT_COPY, "EditCopy", N_("_Copy"), N_("Copy selection to clipboard"), + INKSCAPE_ICON("edit-copy")), + new EditVerb(SP_VERB_EDIT_PASTE, "EditPaste", N_("_Paste"), + N_("Paste objects from clipboard to mouse point, or paste text"), INKSCAPE_ICON("edit-paste")), + new EditVerb(SP_VERB_EDIT_PASTE_STYLE, "EditPasteStyle", N_("Paste _Style"), + N_("Apply the style of the copied object to selection"), INKSCAPE_ICON("edit-paste-style")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE, "EditPasteSize", N_("Paste Si_ze"), + N_("Scale selection to match the size of the copied object"), INKSCAPE_ICON("edit-paste-size")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE_X, "EditPasteWidth", N_("Paste _Width"), + N_("Scale selection horizontally to match the width of the copied object"), INKSCAPE_ICON("edit-paste-width")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE_Y, "EditPasteHeight", N_("Paste _Height"), + N_("Scale selection vertically to match the height of the copied object"), INKSCAPE_ICON("edit-paste-height")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE_SEPARATELY, "EditPasteSizeSeparately", N_("Paste Size Separately"), + N_("Scale each selected object to match the size of the copied object"), INKSCAPE_ICON("edit-paste-size-separately")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_X, "EditPasteWidthSeparately", N_("Paste Width Separately"), + N_("Scale each selected object horizontally to match the width of the copied object"), INKSCAPE_ICON("edit-paste-width-separately")), + new EditVerb(SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_Y, "EditPasteHeightSeparately", N_("Paste Height Separately"), + N_("Scale each selected object vertically to match the height of the copied object"), INKSCAPE_ICON("edit-paste-height-separately")), + new EditVerb(SP_VERB_EDIT_PASTE_IN_PLACE, "EditPasteInPlace", N_("Paste _In Place"), + N_("Paste objects from clipboard to the original location"), INKSCAPE_ICON("edit-paste-in-place")), + new EditVerb(SP_VERB_EDIT_PASTE_LIVEPATHEFFECT, "PasteLivePathEffect", N_("Paste Path _Effect"), + N_("Apply the path effect of the copied object to selection"), nullptr), + new EditVerb(SP_VERB_EDIT_REMOVE_LIVEPATHEFFECT, "RemoveLivePathEffect", N_("Remove Path _Effect"), + N_("Remove any path effects from selected objects"), nullptr), + new EditVerb(SP_VERB_EDIT_REMOVE_FILTER, "RemoveFilter", N_("_Remove Filters"), + N_("Remove any filters from selected objects"), nullptr), + new EditVerb(SP_VERB_EDIT_DELETE, "EditDelete", N_("_Delete"), N_("Delete selection"), + INKSCAPE_ICON("edit-delete")), + new EditVerb(SP_VERB_EDIT_DUPLICATE, "EditDuplicate", N_("Duplic_ate"), N_("Duplicate Selected Objects"), + INKSCAPE_ICON("edit-duplicate")), + new EditVerb(SP_VERB_EDIT_CLONE, "EditClone", N_("Create Clo_ne"), + N_("Create a clone (a copy linked to the original) of selected object"), INKSCAPE_ICON("edit-clone")), + new EditVerb(SP_VERB_EDIT_UNLINK_CLONE, "EditUnlinkClone", N_("Unlin_k Clone"), + N_("Cut the selected clones' links to the originals, turning them into standalone objects"), + INKSCAPE_ICON("edit-clone-unlink")), + new EditVerb(SP_VERB_EDIT_UNLINK_CLONE_RECURSIVE, "EditUnlinkCloneRecursive", N_("Unlink Clones _recursively"), + N_("Unlink all clones in the selection, even if they are in groups."), + INKSCAPE_ICON("edit-clone-unlink")), + new EditVerb(SP_VERB_EDIT_RELINK_CLONE, "EditRelinkClone", N_("Relink to Copied"), + N_("Relink the selected clones to the object currently on the clipboard"), INKSCAPE_ICON("edit-clone-link")), + new EditVerb(SP_VERB_EDIT_CLONE_SELECT_ORIGINAL, "EditCloneSelectOriginal", N_("Select _Original"), + N_("Select the object to which the selected clone is linked"), INKSCAPE_ICON("edit-select-original")), + new EditVerb(SP_VERB_EDIT_CLONE_ORIGINAL_PATH_LPE, "EditCloneOriginalPathLPE", N_("Clone original path (LPE)"), + N_("Creates a new path, applies the Clone original LPE, and refers it to the selected path"), INKSCAPE_ICON("edit-clone-link-lpe")), + new EditVerb(SP_VERB_EDIT_SELECTION_2_MARKER, "ObjectsToMarker", N_("Objects to _Marker"), + N_("Convert selection to a line marker"), nullptr), + new EditVerb(SP_VERB_EDIT_SELECTION_2_GUIDES, "ObjectsToGuides", N_("Objects to Gu_ides"), + N_("Convert selected objects to a collection of guidelines aligned with their edges"), nullptr), + new EditVerb(SP_VERB_EDIT_TILE, "ObjectsToPattern", N_("Objects to Patter_n"), + N_("Convert selection to a rectangle with tiled pattern fill"), nullptr), + new EditVerb(SP_VERB_EDIT_UNTILE, "ObjectsFromPattern", N_("Pattern to _Objects"), + N_("Extract objects from a tiled pattern fill"), nullptr), + new EditVerb(SP_VERB_EDIT_SYMBOL, "ObjectsToSymbol", N_("Group to Symbol"), N_("Convert group to a symbol"), + nullptr), + new EditVerb(SP_VERB_EDIT_UNSYMBOL, "ObjectsFromSymbol", N_("Symbol to Group"), N_("Extract group from a symbol"), + nullptr), + new EditVerb(SP_VERB_EDIT_CLEAR_ALL, "EditClearAll", N_("Clea_r All"), N_("Delete all objects from document"), + nullptr), + new EditVerb(SP_VERB_EDIT_SELECT_ALL, "EditSelectAll", N_("Select Al_l"), N_("Select all objects or all nodes"), + INKSCAPE_ICON("edit-select-all")), + new EditVerb(SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS, "EditSelectAllInAllLayers", N_("Select All in All La_yers"), + N_("Select all objects in all visible and unlocked layers"), INKSCAPE_ICON("edit-select-all-layers")), + new EditVerb(SP_VERB_EDIT_SELECT_SAME_FILL_STROKE, "EditSelectSameFillStroke", N_("Fill _and Stroke"), + N_("Select all objects with the same fill and stroke as the selected objects"), + INKSCAPE_ICON("edit-select-same-fill-and-stroke")), + new EditVerb(SP_VERB_EDIT_SELECT_SAME_FILL_COLOR, "EditSelectSameFillColor", N_("_Fill Color"), + N_("Select all objects with the same fill as the selected objects"), INKSCAPE_ICON("edit-select-same-fill")), + new EditVerb(SP_VERB_EDIT_SELECT_SAME_STROKE_COLOR, "EditSelectSameStrokeColor", N_("_Stroke Color"), + N_("Select all objects with the same stroke as the selected objects"), + INKSCAPE_ICON("edit-select-same-stroke-color")), + new EditVerb(SP_VERB_EDIT_SELECT_SAME_STROKE_STYLE, "EditSelectSameStrokeStyle", N_("Stroke St_yle"), + N_("Select all objects with the same stroke style (width, dash, markers) as the selected objects"), + INKSCAPE_ICON("edit-select-same-stroke-style")), + new EditVerb( + SP_VERB_EDIT_SELECT_SAME_OBJECT_TYPE, "EditSelectSameObjectType", N_("_Object Type"), + N_("Select all objects with the same object type (rect, arc, text, path, bitmap etc) as the selected objects"), + INKSCAPE_ICON("edit-select-same-object-type")), + new EditVerb(SP_VERB_EDIT_INVERT, "EditInvert", N_("In_vert Selection"), + N_("Invert selection (unselect what is selected and select everything else)"), + INKSCAPE_ICON("edit-select-invert")), + new EditVerb(SP_VERB_EDIT_INVERT_IN_ALL_LAYERS, "EditInvertInAllLayers", N_("Invert in All Layers"), + N_("Invert selection in all visible and unlocked layers"), nullptr), + new EditVerb(SP_VERB_EDIT_SELECT_NEXT, "EditSelectNext", N_("Select Next"), N_("Select next object or node"), + nullptr), + new EditVerb(SP_VERB_EDIT_SELECT_PREV, "EditSelectPrev", N_("Select Previous"), + N_("Select previous object or node"), nullptr), + new EditVerb(SP_VERB_EDIT_DESELECT, "EditDeselect", N_("D_eselect"), N_("Deselect any selected objects or nodes"), + INKSCAPE_ICON("edit-select-none")), + new EditVerb(SP_VERB_EDIT_DELETE_ALL_GUIDES, "EditRemoveAllGuides", N_("Delete All Guides"), + N_("Delete all the guides in the document"), nullptr), + new EditVerb(SP_VERB_EDIT_GUIDES_TOGGLE_LOCK, "EditGuidesToggleLock", N_("Lock All Guides"), + N_("Toggle lock of all guides in the document"), nullptr), + new EditVerb(SP_VERB_EDIT_GUIDES_AROUND_PAGE, "EditGuidesAroundPage", N_("Create _Guides Around the Page"), + N_("Create four guides aligned with the page borders"), nullptr), + new EditVerb(SP_VERB_EDIT_NEXT_PATHEFFECT_PARAMETER, "EditNextPathEffectParameter", + N_("Next path effect parameter"), N_("Show next editable path effect parameter"), + INKSCAPE_ICON("path-effect-parameter-next")), + new EditVerb(SP_VERB_EDIT_SWAP_FILL_STROKE, "EditSwapFillStroke", N_("Swap fill and stroke"), + N_("Swap fill and stroke of an object"), nullptr), + + // Selection + new SelectionVerb(SP_VERB_SELECTION_TO_FRONT, "SelectionToFront", N_("Raise to _Top"), N_("Raise selection to top"), + INKSCAPE_ICON("selection-top")), + new SelectionVerb(SP_VERB_SELECTION_TO_BACK, "SelectionToBack", N_("Lower to _Bottom"), + N_("Lower selection to bottom"), INKSCAPE_ICON("selection-bottom")), + new SelectionVerb(SP_VERB_SELECTION_RAISE, "SelectionRaise", N_("_Raise"), N_("Raise selection one step"), + INKSCAPE_ICON("selection-raise")), + new SelectionVerb(SP_VERB_SELECTION_LOWER, "SelectionLower", N_("_Lower"), N_("Lower selection one step"), + INKSCAPE_ICON("selection-lower")), + + + new SelectionVerb(SP_VERB_SELECTION_STACK_UP, "SelectionStackUp", N_("_Stack up"), + N_("Stack selection one step up"), INKSCAPE_ICON("layer-raise")), + new SelectionVerb(SP_VERB_SELECTION_STACK_DOWN, "SelectionStackDown", N_("_Stack down"), + N_("Stack selection one step down"), INKSCAPE_ICON("layer-lower")), + + + new SelectionVerb(SP_VERB_SELECTION_GROUP, "SelectionGroup", N_("_Group"), N_("Group selected objects"), + INKSCAPE_ICON("object-group")), + new SelectionVerb(SP_VERB_SELECTION_UNGROUP, "SelectionUnGroup", N_("_Ungroup"), N_("Ungroup selected groups"), + INKSCAPE_ICON("object-ungroup")), + new SelectionVerb(SP_VERB_SELECTION_UNGROUP_POP_SELECTION, "SelectionUnGroupPopSelection", + N_("_Pop Selected Objects out of Group"), N_("Pop selected objects out of group"), + INKSCAPE_ICON("object-ungroup-pop-selection")), + + new SelectionVerb(SP_VERB_SELECTION_TEXTTOPATH, "SelectionTextToPath", N_("_Put on Path"), N_("Put text on path"), + INKSCAPE_ICON("text-put-on-path")), + new SelectionVerb(SP_VERB_SELECTION_TEXTFROMPATH, "SelectionTextFromPath", N_("_Remove from Path"), + N_("Remove text from path"), INKSCAPE_ICON("text-remove-from-path")), + new SelectionVerb(SP_VERB_SELECTION_REMOVE_KERNS, "SelectionTextRemoveKerns", N_("Remove Manual _Kerns"), + // TRANSLATORS: "glyph": An image used in the visual representation of characters; + // roughly speaking, how a character looks. A font is a set of glyphs. + N_("Remove all manual kerns and glyph rotations from a text object"), + INKSCAPE_ICON("text-unkern")), + + new SelectionVerb(SP_VERB_SELECTION_UNION, "SelectionUnion", N_("_Union"), N_("Create union of selected paths"), + INKSCAPE_ICON("path-union")), + new SelectionVerb(SP_VERB_SELECTION_INTERSECT, "SelectionIntersect", N_("_Intersection"), + N_("Create intersection of selected paths"), INKSCAPE_ICON("path-intersection")), + new SelectionVerb(SP_VERB_SELECTION_DIFF, "SelectionDiff", N_("_Difference"), + N_("Create difference of selected paths (bottom minus top)"), INKSCAPE_ICON("path-difference")), + new SelectionVerb(SP_VERB_SELECTION_SYMDIFF, "SelectionSymDiff", N_("E_xclusion"), + N_("Create exclusive OR of selected paths (those parts that belong to only one path)"), + INKSCAPE_ICON("path-exclusion")), + new SelectionVerb(SP_VERB_SELECTION_CUT, "SelectionDivide", N_("Di_vision"), N_("Cut the bottom path into pieces"), + INKSCAPE_ICON("path-division")), + // TRANSLATORS: "to cut a path" is not the same as "to break a path apart" - see the + // Advanced tutorial for more info + new SelectionVerb(SP_VERB_SELECTION_SLICE, "SelectionCutPath", N_("Cut _Path"), + N_("Cut the bottom path's stroke into pieces, removing fill"), INKSCAPE_ICON("path-cut")), + new SelectionVerb(SP_VERB_SELECTION_GROW, "SelectionGrow", N_("_Grow"), N_("Make selected objects bigger"), + INKSCAPE_ICON("selection-grow")), + new SelectionVerb(SP_VERB_SELECTION_GROW_SCREEN, "SelectionGrowScreen", N_("_Grow on screen"), + N_("Make selected objects bigger relative to screen"), INKSCAPE_ICON("selection-grow-screen")), + new SelectionVerb(SP_VERB_SELECTION_GROW_DOUBLE, "SelectionGrowDouble", N_("_Double size"), + N_("Double the size of selected objects"), INKSCAPE_ICON("selection-grow-double")), + new SelectionVerb(SP_VERB_SELECTION_SHRINK, "SelectionShrink", N_("_Shrink"), N_("Make selected objects smaller"), + INKSCAPE_ICON("selection-shrink")), + new SelectionVerb(SP_VERB_SELECTION_SHRINK_SCREEN, "SelectionShrinkScreen", N_("_Shrink on screen"), + N_("Make selected objects smaller relative to screen"), INKSCAPE_ICON("selection-shrink-screen")), + new SelectionVerb(SP_VERB_SELECTION_SHRINK_HALVE, "SelectionShrinkHalve", N_("_Halve size"), + N_("Halve the size of selected objects"), INKSCAPE_ICON("selection-shrink-halve")), + // TRANSLATORS: "outset": expand a shape by offsetting the object's path, + // i.e. by displacing it perpendicular to the path in each point. + // See also the Advanced Tutorial for explanation. + new SelectionVerb(SP_VERB_SELECTION_OFFSET, "SelectionOffset", N_("Outs_et"), N_("Outset selected paths"), + INKSCAPE_ICON("path-outset")), + new SelectionVerb(SP_VERB_SELECTION_OFFSET_SCREEN, "SelectionOffsetScreen", N_("O_utset Path by 1 px"), + N_("Outset selected paths by 1 px"), nullptr), + new SelectionVerb(SP_VERB_SELECTION_OFFSET_SCREEN_10, "SelectionOffsetScreen10", N_("O_utset Path by 10 px"), + N_("Outset selected paths by 10 px"), nullptr), + // TRANSLATORS: "inset": contract a shape by offsetting the object's path, + // i.e. by displacing it perpendicular to the path in each point. + // See also the Advanced Tutorial for explanation. + new SelectionVerb(SP_VERB_SELECTION_INSET, "SelectionInset", N_("I_nset"), N_("Inset selected paths"), + INKSCAPE_ICON("path-inset")), + new SelectionVerb(SP_VERB_SELECTION_INSET_SCREEN, "SelectionInsetScreen", N_("I_nset Path by 1 px"), + N_("Inset selected paths by 1 px"), nullptr), + new SelectionVerb(SP_VERB_SELECTION_INSET_SCREEN_10, "SelectionInsetScreen10", N_("I_nset Path by 10 px"), + N_("Inset selected paths by 10 px"), nullptr), + new SelectionVerb(SP_VERB_SELECTION_DYNAMIC_OFFSET, "SelectionDynOffset", N_("D_ynamic Offset"), + N_("Create a dynamic offset object"), INKSCAPE_ICON("path-offset-dynamic")), + new SelectionVerb(SP_VERB_SELECTION_LINKED_OFFSET, "SelectionLinkedOffset", N_("_Linked Offset"), + N_("Create a dynamic offset object linked to the original path"), + INKSCAPE_ICON("path-offset-linked")), + new SelectionVerb(SP_VERB_SELECTION_OUTLINE, "StrokeToPath", N_("_Stroke to Path"), + N_("Convert selected object's stroke to paths"), INKSCAPE_ICON("stroke-to-path")), + new SelectionVerb(SP_VERB_SELECTION_OUTLINE_LEGACY, "StrokeToPathLegacy", N_("_Stroke to Path Legacy"), + N_("Convert selected object's stroke to paths legacy mode"), INKSCAPE_ICON("stroke-to-path")), + new SelectionVerb(SP_VERB_SELECTION_SIMPLIFY, "SelectionSimplify", N_("Si_mplify"), + N_("Simplify selected paths (remove extra nodes)"), INKSCAPE_ICON("path-simplify")), + new SelectionVerb(SP_VERB_SELECTION_REVERSE, "SelectionReverse", N_("_Reverse"), + N_("Reverse the direction of selected paths (useful for flipping markers)"), + INKSCAPE_ICON("path-reverse")), + // TRANSLATORS: "to trace" means "to convert a bitmap to vector graphics" (to vectorize) + new SelectionVerb(SP_VERB_SELECTION_TRACE, "SelectionTrace", N_("_Trace Bitmap..."), + N_("Create one or more paths from a bitmap by tracing it"), INKSCAPE_ICON("bitmap-trace")), + new SelectionVerb(SP_VERB_SELECTION_CREATE_BITMAP, "SelectionCreateBitmap", N_("Make a _Bitmap Copy"), + N_("Export selection to a bitmap and insert it into document"), + INKSCAPE_ICON("selection-make-bitmap-copy")), + new SelectionVerb(SP_VERB_SELECTION_COMBINE, "SelectionCombine", N_("_Combine"), + N_("Combine several paths into one"), INKSCAPE_ICON("path-combine")), + // TRANSLATORS: "to cut a path" is not the same as "to break a path apart" - see the + // Advanced tutorial for more info + new SelectionVerb(SP_VERB_SELECTION_BREAK_APART, "SelectionBreakApart", N_("Break _Apart"), + N_("Break selected paths into subpaths"), INKSCAPE_ICON("path-break-apart")), + new SelectionVerb(SP_VERB_SELECTION_ARRANGE, "DialogArrange", N_("_Arrange..."), + N_("Arrange selected objects in a table or circle"), INKSCAPE_ICON("dialog-rows-and-columns")), + // Layer + new LayerVerb(SP_VERB_LAYER_NEW, "LayerNew", N_("_Add Layer..."), N_("Create a new layer"), + INKSCAPE_ICON("layer-new")), + new LayerVerb(SP_VERB_LAYER_RENAME, "LayerRename", N_("Re_name Layer..."), N_("Rename the current layer"), + INKSCAPE_ICON("layer-rename")), + new LayerVerb(SP_VERB_LAYER_NEXT, "LayerNext", N_("Switch to Layer Abov_e"), + N_("Switch to the layer above the current"), INKSCAPE_ICON("layer-previous")), + new LayerVerb(SP_VERB_LAYER_PREV, "LayerPrev", N_("Switch to Layer Belo_w"), + N_("Switch to the layer below the current"), INKSCAPE_ICON("layer-next")), + new LayerVerb(SP_VERB_LAYER_MOVE_TO_NEXT, "LayerMoveToNext", N_("Move Selection to Layer Abo_ve"), + N_("Move selection to the layer above the current"), INKSCAPE_ICON("selection-move-to-layer-above")), + new LayerVerb(SP_VERB_LAYER_MOVE_TO_PREV, "LayerMoveToPrev", N_("Move Selection to Layer Bel_ow"), + N_("Move selection to the layer below the current"), INKSCAPE_ICON("selection-move-to-layer-below")), + new LayerVerb(SP_VERB_LAYER_MOVE_TO, "LayerMoveTo", N_("Move Selection to Layer..."), N_("Move selection to layer"), + INKSCAPE_ICON("selection-move-to-layer")), + new LayerVerb(SP_VERB_LAYER_TO_TOP, "LayerToTop", N_("Layer to _Top"), N_("Raise the current layer to the top"), + INKSCAPE_ICON("layer-top")), + new LayerVerb(SP_VERB_LAYER_TO_BOTTOM, "LayerToBottom", N_("Layer to _Bottom"), + N_("Lower the current layer to the bottom"), INKSCAPE_ICON("layer-bottom")), + new LayerVerb(SP_VERB_LAYER_RAISE, "LayerRaise", N_("_Raise Layer"), N_("Raise the current layer"), + INKSCAPE_ICON("layer-raise")), + new LayerVerb(SP_VERB_LAYER_LOWER, "LayerLower", N_("_Lower Layer"), N_("Lower the current layer"), + INKSCAPE_ICON("layer-lower")), + new LayerVerb(SP_VERB_LAYER_DUPLICATE, "LayerDuplicate", N_("D_uplicate Current Layer"), + N_("Duplicate an existing layer"), INKSCAPE_ICON("layer-duplicate")), + new LayerVerb(SP_VERB_LAYER_DELETE, "LayerDelete", N_("_Delete Current Layer"), N_("Delete the current layer"), + INKSCAPE_ICON("layer-delete")), + new LayerVerb(SP_VERB_LAYER_SOLO, "LayerSolo", N_("_Show/hide other layers"), N_("Solo the current layer"), + nullptr), + new LayerVerb(SP_VERB_LAYER_SHOW_ALL, "LayerShowAll", N_("_Show all layers"), N_("Show all the layers"), nullptr), + new LayerVerb(SP_VERB_LAYER_HIDE_ALL, "LayerHideAll", N_("_Hide all layers"), N_("Hide all the layers"), nullptr), + new LayerVerb(SP_VERB_LAYER_LOCK_ALL, "LayerLockAll", N_("_Lock all layers"), N_("Lock all the layers"), nullptr), + new LayerVerb(SP_VERB_LAYER_LOCK_OTHERS, "LayerLockOthers", N_("Lock/Unlock _other layers"), + N_("Lock all the other layers"), nullptr), + new LayerVerb(SP_VERB_LAYER_UNLOCK_ALL, "LayerUnlockAll", N_("_Unlock all layers"), N_("Unlock all the layers"), + nullptr), + new LayerVerb(SP_VERB_LAYER_TOGGLE_LOCK, "LayerToggleLock", N_("_Lock/Unlock Current Layer"), + N_("Toggle lock on current layer"), nullptr), + new LayerVerb(SP_VERB_LAYER_TOGGLE_HIDE, "LayerToggleHide", N_("_Show/Hide Current Layer"), + N_("Toggle visibility of current layer"), nullptr), + + // Object + new ObjectVerb(SP_VERB_OBJECT_ROTATE_90_CW, "ObjectRotate90", N_("Rotate _90\xc2\xb0 CW"), + // This is shared between tooltips and statusbar, so they + // must use UTF-8, not HTML entities for special characters. + N_("Rotate selection 90\xc2\xb0 clockwise"), INKSCAPE_ICON("object-rotate-right")), + new ObjectVerb(SP_VERB_OBJECT_ROTATE_90_CCW, "ObjectRotate90CCW", N_("Rotate 9_0\xc2\xb0 CCW"), + // This is shared between tooltips and statusbar, so they + // must use UTF-8, not HTML entities for special characters. + N_("Rotate selection 90\xc2\xb0 counter-clockwise"), INKSCAPE_ICON("object-rotate-left")), + new ObjectVerb(SP_VERB_OBJECT_FLATTEN, "ObjectRemoveTransform", N_("Remove _Transformations"), + N_("Remove transformations from object"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_TO_CURVE, "ObjectToPath", N_("_Object to Path"), + N_("Convert selected object to path"), INKSCAPE_ICON("object-to-path")), + new ObjectVerb(SP_VERB_OBJECT_FLOW_TEXT, "ObjectFlowText", N_("_Flow into Frame"), + N_("Put text into a frame (path or shape), creating a flowed text linked to the frame object"), + "text-flow-into-frame"), + new ObjectVerb(SP_VERB_OBJECT_UNFLOW_TEXT, "ObjectUnFlowText", N_("_Unflow"), + N_("Remove text from frame (creates a single-line text object)"), INKSCAPE_ICON("text-unflow")), + new ObjectVerb(SP_VERB_OBJECT_FLOWTEXT_TO_TEXT, "ObjectFlowtextToText", N_("_Convert to Text"), + N_("Convert flowed text to regular text object (preserves appearance)"), + INKSCAPE_ICON("text-convert-to-regular")), + new ObjectVerb(SP_VERB_OBJECT_FLIP_HORIZONTAL, "ObjectFlipHorizontally", N_("Flip _Horizontal"), + N_("Flip selected objects horizontally"), INKSCAPE_ICON("object-flip-horizontal")), + new ObjectVerb(SP_VERB_OBJECT_FLIP_VERTICAL, "ObjectFlipVertically", N_("Flip _Vertical"), + N_("Flip selected objects vertically"), INKSCAPE_ICON("object-flip-vertical")), + new ObjectVerb(SP_VERB_OBJECT_SET_MASK, "ObjectSetMask", N_("_Set"), + N_("Apply mask to selection (using the topmost object as mask)"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_SET_INVERSE_MASK, "ObjectSetInverseMask", N_("_Set Inverse (LPE)"), + N_("Apply inverse mask to selection (using the topmost object as mask)"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_EDIT_MASK, "ObjectEditMask", N_("_Edit"), N_("Edit mask"), + INKSCAPE_ICON("path-mask-edit")), + new ObjectVerb(SP_VERB_OBJECT_UNSET_MASK, "ObjectUnSetMask", N_("_Release"), N_("Remove mask from selection"), + nullptr), + new ObjectVerb(SP_VERB_OBJECT_SET_CLIPPATH, "ObjectSetClipPath", N_("_Set"), + N_("Apply clipping path to selection (using the topmost object as clipping path)"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, "ObjectSetInverseClipPath", N_("_Set Inverse (LPE)"), + N_("Apply inverse clipping path to selection (using the topmost object as clipping path)"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_CREATE_CLIP_GROUP, "ObjectCreateClipGroup", N_("Create Cl_ip Group"), + N_("Creates a clip group using the selected objects as a base"), nullptr), + new ObjectVerb(SP_VERB_OBJECT_EDIT_CLIPPATH, "ObjectEditClipPath", N_("_Edit"), N_("Edit clipping path"), + INKSCAPE_ICON("path-clip-edit")), + new ObjectVerb(SP_VERB_OBJECT_UNSET_CLIPPATH, "ObjectUnSetClipPath", N_("_Release"), + N_("Remove clipping path from selection"), nullptr), + // Tag + new TagVerb(SP_VERB_TAG_NEW, "TagNew", N_("_New"), N_("Create new selection set"), nullptr), + // Tools + new ContextVerb(SP_VERB_CONTEXT_SELECT, "ToolSelector", NC_("ContextVerb", "Select"), + N_("Select and transform objects"), INKSCAPE_ICON("tool-pointer")), + new ContextVerb(SP_VERB_CONTEXT_NODE, "ToolNode", NC_("ContextVerb", "Node Edit"), N_("Edit paths by nodes"), + INKSCAPE_ICON("tool-node-editor")), + new ContextVerb(SP_VERB_CONTEXT_TWEAK, "ToolTweak", NC_("ContextVerb", "Tweak"), + N_("Tweak objects by sculpting or painting"), INKSCAPE_ICON("tool-tweak")), + new ContextVerb(SP_VERB_CONTEXT_SPRAY, "ToolSpray", NC_("ContextVerb", "Spray"), + N_("Spray objects by sculpting or painting"), INKSCAPE_ICON("tool-spray")), + new ContextVerb(SP_VERB_CONTEXT_RECT, "ToolRect", NC_("ContextVerb", "Rectangle"), + N_("Create rectangles and squares"), INKSCAPE_ICON("draw-rectangle")), + new ContextVerb(SP_VERB_CONTEXT_3DBOX, "Tool3DBox", NC_("ContextVerb", "3D Box"), N_("Create 3D boxes"), + INKSCAPE_ICON("draw-cuboid")), + new ContextVerb(SP_VERB_CONTEXT_ARC, "ToolArc", NC_("ContextVerb", "Ellipse"), + N_("Create circles, ellipses, and arcs"), INKSCAPE_ICON("draw-ellipse")), + new ContextVerb(SP_VERB_CONTEXT_STAR, "ToolStar", NC_("ContextVerb", "Star"), N_("Create stars and polygons"), + INKSCAPE_ICON("draw-polygon-star")), + new ContextVerb(SP_VERB_CONTEXT_SPIRAL, "ToolSpiral", NC_("ContextVerb", "Spiral"), N_("Create spirals"), + INKSCAPE_ICON("draw-spiral")), + new ContextVerb(SP_VERB_CONTEXT_PENCIL, "ToolPencil", NC_("ContextVerb", "Pencil"), N_("Draw freehand lines"), + INKSCAPE_ICON("draw-freehand")), + new ContextVerb(SP_VERB_CONTEXT_PEN, "ToolPen", NC_("ContextVerb", "Pen"), + N_("Draw Bezier curves and straight lines"), INKSCAPE_ICON("draw-path")), + new ContextVerb(SP_VERB_CONTEXT_CALLIGRAPHIC, "ToolCalligraphic", NC_("ContextVerb", "Calligraphy"), + N_("Draw calligraphic or brush strokes"), INKSCAPE_ICON("draw-calligraphic")), + new ContextVerb(SP_VERB_CONTEXT_TEXT, "ToolText", NC_("ContextVerb", "Text"), N_("Create and edit text objects"), + INKSCAPE_ICON("draw-text")), + new ContextVerb(SP_VERB_CONTEXT_GRADIENT, "ToolGradient", NC_("ContextVerb", "Gradient"), + N_("Create and edit gradients"), INKSCAPE_ICON("color-gradient")), + new ContextVerb(SP_VERB_CONTEXT_MESH, "ToolMesh", NC_("ContextVerb", "Mesh"), N_("Create and edit meshes"), + INKSCAPE_ICON("mesh-gradient")), + new ContextVerb(SP_VERB_CONTEXT_ZOOM, "ToolZoom", NC_("ContextVerb", "Zoom"), N_("Zoom in or out"), + INKSCAPE_ICON("zoom")), + new ContextVerb(SP_VERB_CONTEXT_MEASURE, "ToolMeasure", NC_("ContextVerb", "Measure"), N_("Measurement tool"), + INKSCAPE_ICON("tool-measure")), + new ContextVerb(SP_VERB_CONTEXT_DROPPER, "ToolDropper", NC_("ContextVerb", "Dropper"), N_("Pick colors from image"), + INKSCAPE_ICON("color-picker")), + new ContextVerb(SP_VERB_CONTEXT_CONNECTOR, "ToolConnector", NC_("ContextVerb", "Connector"), + N_("Create diagram connectors"), INKSCAPE_ICON("draw-connector")), + new ContextVerb(SP_VERB_CONTEXT_PAINTBUCKET, "ToolPaintBucket", NC_("ContextVerb", "Paint Bucket"), + N_("Fill bounded areas"), INKSCAPE_ICON("color-fill")), + new ContextVerb(SP_VERB_CONTEXT_LPE, "ToolLPE", NC_("ContextVerb", "LPE Edit"), N_("Edit Path Effect parameters"), + nullptr), + new ContextVerb(SP_VERB_CONTEXT_ERASER, "ToolEraser", NC_("ContextVerb", "Eraser"), N_("Erase existing paths"), + INKSCAPE_ICON("draw-eraser")), + new ContextVerb(SP_VERB_CONTEXT_LPETOOL, "ToolLPETool", NC_("ContextVerb", "LPE Tool"), + N_("Do geometric constructions"), "draw-geometry"), + // Tool prefs + new ContextVerb(SP_VERB_CONTEXT_SELECT_PREFS, "SelectPrefs", N_("Selector Preferences"), + N_("Open Preferences for the Selector tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_NODE_PREFS, "NodePrefs", N_("Node Tool Preferences"), + N_("Open Preferences for the Node tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_TWEAK_PREFS, "TweakPrefs", N_("Tweak Tool Preferences"), + N_("Open Preferences for the Tweak tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_SPRAY_PREFS, "SprayPrefs", N_("Spray Tool Preferences"), + N_("Open Preferences for the Spray tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_RECT_PREFS, "RectPrefs", N_("Rectangle Preferences"), + N_("Open Preferences for the Rectangle tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_3DBOX_PREFS, "3DBoxPrefs", N_("3D Box Preferences"), + N_("Open Preferences for the 3D Box tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_ARC_PREFS, "ArcPrefs", N_("Ellipse Preferences"), + N_("Open Preferences for the Ellipse tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_STAR_PREFS, "StarPrefs", N_("Star Preferences"), + N_("Open Preferences for the Star tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_SPIRAL_PREFS, "SpiralPrefs", N_("Spiral Preferences"), + N_("Open Preferences for the Spiral tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_PENCIL_PREFS, "PencilPrefs", N_("Pencil Preferences"), + N_("Open Preferences for the Pencil tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_PEN_PREFS, "PenPrefs", N_("Pen Preferences"), + N_("Open Preferences for the Pen tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS, "CalligraphicPrefs", N_("Calligraphic Preferences"), + N_("Open Preferences for the Calligraphy tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_TEXT_PREFS, "TextPrefs", N_("Text Preferences"), + N_("Open Preferences for the Text tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_GRADIENT_PREFS, "GradientPrefs", N_("Gradient Preferences"), + N_("Open Preferences for the Gradient tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_MESH_PREFS, "Mesh_Prefs", N_("Mesh Preferences"), + N_("Open Preferences for the Mesh tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_ZOOM_PREFS, "ZoomPrefs", N_("Zoom Preferences"), + N_("Open Preferences for the Zoom tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_MEASURE_PREFS, "MeasurePrefs", N_("Measure Preferences"), + N_("Open Preferences for the Measure tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_DROPPER_PREFS, "DropperPrefs", N_("Dropper Preferences"), + N_("Open Preferences for the Dropper tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_CONNECTOR_PREFS, "ConnectorPrefs", N_("Connector Preferences"), + N_("Open Preferences for the Connector tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_PAINTBUCKET_PREFS, "PaintBucketPrefs", N_("Paint Bucket Preferences"), + N_("Open Preferences for the Paint Bucket tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_ERASER_PREFS, "EraserPrefs", N_("Eraser Preferences"), + N_("Open Preferences for the Eraser tool"), nullptr), + new ContextVerb(SP_VERB_CONTEXT_LPETOOL_PREFS, "LPEToolPrefs", N_("LPE Tool Preferences"), + N_("Open Preferences for the LPETool tool"), nullptr), + + // Zoom + new ZoomVerb(SP_VERB_ZOOM_IN, "ZoomIn", N_("Zoom In"), N_("Zoom in"), INKSCAPE_ICON("zoom-in")), + new ZoomVerb(SP_VERB_ZOOM_OUT, "ZoomOut", N_("Zoom Out"), N_("Zoom out"), INKSCAPE_ICON("zoom-out")), + new ZoomVerb(SP_VERB_ZOOM_NEXT, "ZoomNext", N_("Nex_t Zoom"), N_("Next zoom (from the history of zooms)"), + INKSCAPE_ICON("zoom-next")), + new ZoomVerb(SP_VERB_ZOOM_PREV, "ZoomPrev", N_("Pre_vious Zoom"), N_("Previous zoom (from the history of zooms)"), + INKSCAPE_ICON("zoom-previous")), + new ZoomVerb(SP_VERB_ZOOM_1_1, "Zoom1:0", N_("Zoom 1:_1"), N_("Zoom to 1:1"), INKSCAPE_ICON("zoom-original")), + new ZoomVerb(SP_VERB_ZOOM_1_2, "Zoom1:2", N_("Zoom 1:_2"), N_("Zoom to 1:2"), INKSCAPE_ICON("zoom-half-size")), + new ZoomVerb(SP_VERB_ZOOM_2_1, "Zoom2:1", N_("_Zoom 2:1"), N_("Zoom to 2:1"), INKSCAPE_ICON("zoom-double-size")), + new ZoomVerb(SP_VERB_ZOOM_PAGE, "ZoomPage", N_("_Page"), N_("Zoom to fit page in window"), + INKSCAPE_ICON("zoom-fit-page")), + new ZoomVerb(SP_VERB_ZOOM_PAGE_WIDTH, "ZoomPageWidth", N_("Page _Width"), N_("Zoom to fit page width in window"), + INKSCAPE_ICON("zoom-fit-width")), + new ZoomVerb(SP_VERB_ZOOM_DRAWING, "ZoomDrawing", N_("_Drawing"), N_("Zoom to fit drawing in window"), + INKSCAPE_ICON("zoom-fit-drawing")), + new ZoomVerb(SP_VERB_ZOOM_SELECTION, "ZoomSelection", N_("_Selection"), N_("Zoom to fit selection in window"), + INKSCAPE_ICON("zoom-fit-selection")), + new ZoomVerb(SP_VERB_ZOOM_CENTER_PAGE, "ZoomCenterPage", N_("_Center Page"), N_("Center page in window"), + INKSCAPE_ICON("zoom-center-page")), + + new ZoomVerb(SP_VERB_ROTATE_CW, "RotateClockwise", N_("Rotate Clockwise"), N_("Rotate canvas clockwise"), nullptr), + new ZoomVerb(SP_VERB_ROTATE_CCW, "RotateCounterClockwise", N_("Rotate Counter-Clockwise"), + N_("Rotate canvas counter-clockwise"), nullptr), + new ZoomVerb(SP_VERB_ROTATE_ZERO, "RotateZero", N_("Reset Rotation"), N_("Reset canvas rotation to zero"), nullptr), + + new ZoomVerb(SP_VERB_FLIP_HORIZONTAL, "FlipHorizontal", N_("Flip Horizontally"), N_("Flip canvas horizontally"), + INKSCAPE_ICON("object-flip-horizontal")), + new ZoomVerb(SP_VERB_FLIP_VERTICAL, "FlipVertical", N_("Flip Vertically"), N_("Flip canvas vertically"), + INKSCAPE_ICON("object-flip-vertical")), + new ZoomVerb(SP_VERB_FLIP_NONE, "FlipNone", N_("Reset Flip"), N_("Undo any flip"), nullptr), + + + // WHY ARE THE FOLLOWING ZoomVerbs??? + + // View + new ZoomVerb(SP_VERB_TOGGLE_RULERS, "ToggleRulers", N_("_Rulers"), N_("Show or hide the canvas rulers"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_SCROLLBARS, "ToggleScrollbars", N_("Scroll_bars"), + N_("Show or hide the canvas scrollbars"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_GRID, "ToggleGrid", N_("Page _Grid"), N_("Show or hide the page grid"), + INKSCAPE_ICON("show-grid")), + new ZoomVerb(SP_VERB_TOGGLE_GUIDES, "ToggleGuides", N_("G_uides"), + N_("Show or hide guides (drag from a ruler to create a guide)"), INKSCAPE_ICON("show-guides")), + new ZoomVerb(SP_VERB_TOGGLE_ROTATION_LOCK, "ToggleRotationLock", N_("Lock Rotation"), + N_("Lock canvas rotation"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_SNAPPING, "ToggleSnapGlobal", N_("Snap"), N_("Enable snapping"), INKSCAPE_ICON("snap")), + new ZoomVerb(SP_VERB_TOGGLE_COMMANDS_TOOLBAR, "ToggleCommandsToolbar", N_("_Commands Bar"), + N_("Show or hide the Commands bar (under the menu)"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_SNAP_TOOLBAR, "ToggleSnapToolbar", N_("Sn_ap Controls Bar"), + N_("Show or hide the snapping controls"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_TOOL_TOOLBAR, "ToggleToolToolbar", N_("T_ool Controls Bar"), + N_("Show or hide the Tool Controls bar"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_TOOLBOX, "ToggleToolbox", N_("_Toolbox"), + N_("Show or hide the main toolbox (on the left)"), nullptr), + new ZoomVerb(SP_VERB_TOGGLE_PALETTE, "TogglePalette", N_("_Palette"), N_("Show or hide the color palette"), + nullptr), + new ZoomVerb(SP_VERB_TOGGLE_STATUSBAR, "ToggleStatusbar", N_("_Statusbar"), + N_("Show or hide the statusbar (at the bottom of the window)"), nullptr), + + new ZoomVerb(SP_VERB_FULLSCREEN, "FullScreen", N_("_Fullscreen"), N_("Stretch this document window to full screen"), + INKSCAPE_ICON("view-fullscreen")), + new ZoomVerb(SP_VERB_FULLSCREENFOCUS, "FullScreenFocus", N_("Fullscreen & Focus Mode"), + N_("Stretch this document window to full screen"), INKSCAPE_ICON("view-fullscreen")), + new ZoomVerb(SP_VERB_FOCUSTOGGLE, "FocusToggle", N_("Toggle _Focus Mode"), + N_("Remove excess toolbars to focus on drawing"), nullptr), + new ZoomVerb(SP_VERB_VIEW_NEW, "ViewNew", N_("Duplic_ate Window"), N_("Open a new window with the same document"), + INKSCAPE_ICON("window-new")), + + new ZoomVerb(SP_VERB_VIEW_MODE_NORMAL, "ViewModeNormal", N_("_Normal"), N_("Switch to normal display mode"), + nullptr), + new ZoomVerb(SP_VERB_VIEW_MODE_NO_FILTERS, "ViewModeNoFilters", N_("No _Filters"), + N_("Switch to normal display without filters"), nullptr), + new ZoomVerb(SP_VERB_VIEW_MODE_OUTLINE, "ViewModeOutline", N_("_Outline"), + N_("Switch to outline (wireframe) display mode"), nullptr), + new ZoomVerb(SP_VERB_VIEW_MODE_VISIBLE_HAIRLINES, "ViewModeVisibleHairlines", N_("Visible _Hairlines"), + N_("Make sure hairlines are always drawn thick enough to see"), nullptr), + new ZoomVerb(SP_VERB_VIEW_MODE_TOGGLE, "ViewModeToggle", N_("_Toggle"), + N_("Toggle between normal and outline display modes"), nullptr), + new ZoomVerb(SP_VERB_VIEW_COLOR_MODE_NORMAL, "ViewColorModeNormal", N_("_Normal"), + N_("Switch to normal color display mode"), nullptr), + new ZoomVerb(SP_VERB_VIEW_COLOR_MODE_GRAYSCALE, "ViewColorModeGrayscale", N_("_Grayscale"), + N_("Switch to grayscale display mode"), nullptr), + // new ZoomVerb(SP_VERB_VIEW_COLOR_MODE_PRINT_COLORS_PREVIEW, "ViewColorModePrintColorsPreview", N_("_Print + // Colors Preview"), + // N_("Switch to print colors preview mode"), NULL), + new ZoomVerb(SP_VERB_VIEW_COLOR_MODE_TOGGLE, "ViewColorModeToggle", N_("_Toggle"), + N_("Toggle between normal and grayscale color display modes"), nullptr), + + new ZoomVerb(SP_VERB_VIEW_TOGGLE_SPLIT, "ViewSplitModeToggle", N_("_Split View Mode"), + N_("Split canvas in 2 to show outline"), nullptr), + + new ZoomVerb(SP_VERB_VIEW_TOGGLE_XRAY, "ViewXRayToggle", N_("_XRay Mode"), N_("XRay around cursor"), nullptr), + + new ZoomVerb(SP_VERB_VIEW_CMS_TOGGLE, "ViewCmsToggle", N_("Color-Managed View"), + N_("Toggle color-managed display for this document window"), INKSCAPE_ICON("color-management")), + + new ZoomVerb(SP_VERB_VIEW_ICON_PREVIEW, "ViewIconPreview", N_("Ico_n Preview..."), + N_("Open a window to preview objects at different icon resolutions"), + INKSCAPE_ICON("dialog-icon-preview")), + + // Dialogs + new DialogVerb(SP_VERB_DIALOG_PROTOTYPE, "DialogPrototype", N_("Prototype..."), N_("Prototype Dialog"), + INKSCAPE_ICON("preferences-system")), + new DialogVerb(SP_VERB_DIALOG_DISPLAY, "DialogPreferences", N_("P_references..."), + N_("Edit global Inkscape preferences"), INKSCAPE_ICON("preferences-system")), + new DialogVerb(SP_VERB_DIALOG_NAMEDVIEW, "DialogDocumentProperties", N_("_Document Properties..."), + N_("Edit properties of this document (to be saved with the document)"), + INKSCAPE_ICON("document-properties")), + new DialogVerb(SP_VERB_DIALOG_METADATA, "DialogMetadata", N_("Document _Metadata..."), + N_("Edit document metadata (to be saved with the document)"), INKSCAPE_ICON("document-metadata")), + new DialogVerb(SP_VERB_DIALOG_FILL_STROKE, "DialogFillStroke", N_("_Fill and Stroke..."), + N_("Edit objects' colors, gradients, arrowheads, and other fill and stroke properties..."), + INKSCAPE_ICON("dialog-fill-and-stroke")), + // FIXME: Probably better to either use something from the icon naming spec or ship our own "select-font" icon + // Technically what we show are unicode code points and not glyphs. The actual glyphs shown are determined by the + // shaping engines. + new DialogVerb(SP_VERB_DIALOG_GLYPHS, "DialogGlyphs", N_("_Unicode Characters..."), + N_("Select Unicode characters from a palette"), INKSCAPE_ICON("accessories-character-map")), + // FIXME: Probably better to either use something from the icon naming spec or ship our own "select-color" icon + // TRANSLATORS: "Swatches" means: color samples + new DialogVerb(SP_VERB_DIALOG_SWATCHES, "DialogSwatches", N_("S_watches..."), + N_("Select colors from a swatches palette"), INKSCAPE_ICON("swatches")), + new DialogVerb(SP_VERB_DIALOG_SYMBOLS, "DialogSymbols", N_("S_ymbols..."), + N_("Select symbol from a symbols palette"), INKSCAPE_ICON("symbols")), + new DialogVerb(SP_VERB_DIALOG_PAINT, "DialogPaintServers", N_("_Paint Servers..."), + // FIXME missing Inkscape Paint Server Icon + N_("Select paint server from a collection"), INKSCAPE_ICON("symbols")), + new DialogVerb(SP_VERB_DIALOG_TRANSFORM, "DialogTransform", N_("Transfor_m..."), + N_("Precisely control objects' transformations"), INKSCAPE_ICON("dialog-transform")), + new DialogVerb(SP_VERB_DIALOG_ALIGN_DISTRIBUTE, "DialogAlignDistribute", N_("_Align and Distribute..."), + N_("Align and distribute objects"), INKSCAPE_ICON("dialog-align-and-distribute")), + new DialogVerb(SP_VERB_DIALOG_SPRAY_OPTION, "DialogSprayOption", N_("_Spray options..."), + N_("Some options for the spray"), INKSCAPE_ICON("dialog-spray-options")), + new DialogVerb(SP_VERB_DIALOG_UNDO_HISTORY, "DialogUndoHistory", N_("Undo _History..."), N_("Undo History"), + INKSCAPE_ICON("edit-undo-history")), + new DialogVerb(SP_VERB_DIALOG_TEXT, "DialogText", N_("_Text and Font..."), + N_("View and select font family, font size and other text properties"), + INKSCAPE_ICON("dialog-text-and-font")), + new DialogVerb(SP_VERB_DIALOG_XML_EDITOR, "DialogXMLEditor", N_("_XML Editor..."), + N_("View and edit the XML tree of the document"), INKSCAPE_ICON("dialog-xml-editor")), + new DialogVerb(SP_VERB_DIALOG_SELECTORS, "DialogSelectors", N_("_Selectors and CSS..."), + N_("View and edit CSS selectors and styles"), INKSCAPE_ICON("dialog-selectors")), + new DialogVerb(SP_VERB_DIALOG_FIND, "DialogFind", N_("_Find/Replace..."), N_("Find objects in document"), + INKSCAPE_ICON("edit-find")), +#if HAVE_ASPELL + new DialogVerb(SP_VERB_DIALOG_SPELLCHECK, "DialogSpellcheck", N_("Check Spellin_g..."), + N_("Check spelling of text in document"), INKSCAPE_ICON("tools-check-spelling")), +#endif + new DialogVerb(SP_VERB_DIALOG_DEBUG, "DialogDebug", N_("_Messages..."), N_("View debug messages"), + INKSCAPE_ICON("dialog-messages")), + new DialogVerb(SP_VERB_DIALOG_TOGGLE, "DialogsToggle", N_("Show/Hide D_ialogs"), + N_("Show or hide all open dialogs"), INKSCAPE_ICON("show-dialogs")), + new DialogVerb(SP_VERB_DIALOG_CLONETILER, "DialogClonetiler", N_("Create Tiled Clones..."), + N_("Create multiple clones of selected object, arranging them into a pattern or scattering"), + INKSCAPE_ICON("dialog-tile-clones")), + new DialogVerb(SP_VERB_DIALOG_ATTR, "DialogObjectAttributes", N_("_Object attributes..."), + N_("Edit the object attributes..."), INKSCAPE_ICON("dialog-object-properties")), + new DialogVerb(SP_VERB_DIALOG_ITEM, "DialogObjectProperties", N_("_Object Properties..."), + N_("Edit the ID, locked and visible status, and other object properties"), + INKSCAPE_ICON("dialog-object-properties")), + new DialogVerb(SP_VERB_DIALOG_INPUT, "DialogInput", N_("_Input Devices..."), + N_("Configure extended input devices, such as a graphics tablet"), + INKSCAPE_ICON("dialog-input-devices")), + new DialogVerb(SP_VERB_DIALOG_EXTENSIONEDITOR, "org.inkscape.dialogs.extensioneditor", N_("_Extensions..."), + N_("Query information about extensions"), nullptr), + new DialogVerb(SP_VERB_DIALOG_LAYERS, "DialogLayers", N_("Layer_s..."), N_("View Layers"), + INKSCAPE_ICON("dialog-layers")), + new DialogVerb(SP_VERB_DIALOG_OBJECTS, "DialogObjects", N_("Object_s..."), N_("View Objects"), + INKSCAPE_ICON("dialog-objects")), + new DialogVerb(SP_VERB_DIALOG_TAGS, "DialogTags", N_("Selection Se_ts..."), N_("View Tags"), + INKSCAPE_ICON("edit-select-all-layers")), + new DialogVerb(SP_VERB_DIALOG_STYLE, "DialogStyle", N_("Style Dialog..."), N_("View Style Dialog"), nullptr), + new DialogVerb(SP_VERB_DIALOG_LIVE_PATH_EFFECT, "DialogLivePathEffect", N_("Path E_ffects..."), + N_("Manage, edit, and apply path effects"), INKSCAPE_ICON("dialog-path-effects")), + new DialogVerb(SP_VERB_DIALOG_FILTER_EFFECTS, "DialogFilterEffects", N_("Filter _Editor..."), + N_("Manage, edit, and apply SVG filters"), INKSCAPE_ICON("dialog-filters")), + new DialogVerb(SP_VERB_DIALOG_SVG_FONTS, "DialogSVGFonts", N_("SVG Font Editor..."), N_("Edit SVG fonts"), nullptr), + new DialogVerb(SP_VERB_DIALOG_PRINT_COLORS_PREVIEW, "DialogPrintColorsPreview", N_("Print Colors..."), + N_("Select which color separations to render in Print Colors Preview rendermode"), nullptr), + new DialogVerb(SP_VERB_DIALOG_EXPORT, "DialogExport", N_("_Export PNG Image..."), + N_("Export this document or a selection as a PNG image"), INKSCAPE_ICON("document-export")), + // Help + new HelpVerb(SP_VERB_HELP_ABOUT_EXTENSIONS, "HelpAboutExtensions", N_("About E_xtensions"), + N_("Information on Inkscape extensions"), nullptr), + new HelpVerb(SP_VERB_HELP_MEMORY, "HelpAboutMemory", N_("About _Memory"), N_("Memory usage information"), + INKSCAPE_ICON("dialog-memory")), + new HelpVerb(SP_VERB_HELP_ABOUT, "HelpAbout", N_("_About Inkscape"), N_("Inkscape version, authors, license"), + INKSCAPE_ICON("inkscape-logo")), + // new HelpVerb(SP_VERB_SHOW_LICENSE, "ShowLicense", N_("_License"), + // N_("Distribution terms"), /*"show_license"*/"inkscape_options"), + + // Help URLs + // TODO: Better tooltips + new HelpUrlVerb(SP_VERB_HELP_URL_ASK_QUESTION, "HelpUrlAskQuestion", + N_("Ask Us a Question"), N_("Ask Us a Question"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_MAN, "HelpUrlMan", + N_("Command Line Options"), N_("Command Line Options"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_FAQ, "HelpUrlFAQ", + N_("FAQ"), N_("FAQ"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_KEYS, "HelpUrlKeys", + N_("Keys and Mouse Reference"), N_("Keys and Mouse Reference"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_RELEASE_NOTES, "HelpUrlReleaseNotes", + N_("New in This Version"), N_("New in This Version"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_REPORT_BUG, "HelpUrlReportBug", + N_("Report a Bug"), N_("Report a Bug"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_MANUAL, "HelpUrlManual", + N_("Inkscape Manual"), N_("Inkscape Manual"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_SVG11_SPEC, "HelpUrlSvg11Spec", + N_("SVG 1.1 Specification"), N_("SVG 1.1 Specification"), nullptr), + new HelpUrlVerb(SP_VERB_HELP_URL_SVG2_SPEC, "HelpUrlSvg2Spec", + N_("SVG 2 Specification"), N_("SVG 2 Specification"), nullptr), + + // Tutorials + new TutorialVerb(SP_VERB_TUTORIAL_BASIC, "TutorialsBasic", N_("Inkscape: _Basic"), + N_("Getting started with Inkscape"), nullptr /*"tutorial_basic"*/), + new TutorialVerb(SP_VERB_TUTORIAL_SHAPES, "TutorialsShapes", N_("Inkscape: _Shapes"), + N_("Using shape tools to create and edit shapes"), nullptr), + new TutorialVerb(SP_VERB_TUTORIAL_ADVANCED, "TutorialsAdvanced", N_("Inkscape: _Advanced"), + N_("Advanced Inkscape topics"), nullptr /*"tutorial_advanced"*/), + // TRANSLATORS: "to trace" means "to convert a bitmap to vector graphics" (to vectorize) + new TutorialVerb(SP_VERB_TUTORIAL_TRACING, "TutorialsTracing", N_("Inkscape: T_racing"), N_("Using bitmap tracing"), + nullptr /*"tutorial_tracing"*/), + new TutorialVerb(SP_VERB_TUTORIAL_TRACING_PIXELART, "TutorialsTracingPixelArt", N_("Inkscape: Tracing Pixel Art"), + N_("Using Trace Pixel Art dialog"), nullptr), + new TutorialVerb(SP_VERB_TUTORIAL_CALLIGRAPHY, "TutorialsCalligraphy", N_("Inkscape: _Calligraphy"), + N_("Using the Calligraphy pen tool"), nullptr), + new TutorialVerb(SP_VERB_TUTORIAL_INTERPOLATE, "TutorialsInterpolate", N_("Inkscape: _Interpolate"), + N_("Using the interpolate extension"), nullptr /*"tutorial_interpolate"*/), + new TutorialVerb(SP_VERB_TUTORIAL_DESIGN, "TutorialsDesign", N_("_Elements of Design"), + N_("Principles of design in the tutorial form"), nullptr /*"tutorial_design"*/), + new TutorialVerb(SP_VERB_TUTORIAL_TIPS, "TutorialsTips", N_("_Tips and Tricks"), + N_("Miscellaneous tips and tricks"), nullptr /*"tutorial_tips"*/), + + // Effect -- renamed Extension + new EffectLastVerb(SP_VERB_EFFECT_LAST, "EffectLast", N_("Previous Exte_nsion"), + N_("Repeat the last extension with the same settings"), nullptr), + new EffectLastVerb(SP_VERB_EFFECT_LAST_PREF, "EffectLastPref", N_("_Previous Extension Settings..."), + N_("Repeat the last extension with new settings"), nullptr), + + // Fit Page + new FitCanvasVerb(SP_VERB_FIT_CANVAS_TO_SELECTION, "FitCanvasToSelection", N_("Fit Page to Selection"), + N_("Fit the page to the current selection"), nullptr), + new FitCanvasVerb(SP_VERB_FIT_CANVAS_TO_DRAWING, "FitCanvasToDrawing", N_("Fit Page to Drawing"), + N_("Fit the page to the drawing"), nullptr), + new FitCanvasVerb(SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING, "FitCanvasToSelectionOrDrawing", + N_("_Resize Page to Selection"), + N_("Fit the page to the current selection or the drawing if there is no selection"), nullptr), + // LockAndHide + new LockAndHideVerb(SP_VERB_UNLOCK_ALL, "UnlockAll", N_("Unlock All"), + N_("Unlock all objects in the current layer"), nullptr), + new LockAndHideVerb(SP_VERB_UNLOCK_ALL_IN_ALL_LAYERS, "UnlockAllInAllLayers", N_("Unlock All in All Layers"), + N_("Unlock all objects in all layers"), nullptr), + new LockAndHideVerb(SP_VERB_UNHIDE_ALL, "UnhideAll", N_("Unhide All"), + N_("Unhide all objects in the current layer"), nullptr), + new LockAndHideVerb(SP_VERB_UNHIDE_ALL_IN_ALL_LAYERS, "UnhideAllInAllLayers", N_("Unhide All in All Layers"), + N_("Unhide all objects in all layers"), nullptr), + // Color Management + new EditVerb(SP_VERB_EDIT_LINK_COLOR_PROFILE, "LinkColorProfile", N_("Link Color Profile"), + N_("Link an ICC color profile"), nullptr), + new EditVerb(SP_VERB_EDIT_REMOVE_COLOR_PROFILE, "RemoveColorProfile", N_("Remove Color Profile"), + N_("Remove a linked ICC color profile"), nullptr), + // Scripting + new ContextVerb(SP_VERB_EDIT_ADD_EXTERNAL_SCRIPT, "AddExternalScript", N_("Add External Script"), + N_("Add an external script"), nullptr), + new ContextVerb(SP_VERB_EDIT_ADD_EMBEDDED_SCRIPT, "AddEmbeddedScript", N_("Add Embedded Script"), + N_("Add an embedded script"), nullptr), + new ContextVerb(SP_VERB_EDIT_EMBEDDED_SCRIPT, "EditEmbeddedScript", N_("Edit Embedded Script"), + N_("Edit an embedded script"), nullptr), + new ContextVerb(SP_VERB_EDIT_REMOVE_EXTERNAL_SCRIPT, "RemoveExternalScript", N_("Remove External Script"), + N_("Remove an external script"), nullptr), + new ContextVerb(SP_VERB_EDIT_REMOVE_EMBEDDED_SCRIPT, "RemoveEmbeddedScript", N_("Remove Embedded Script"), + N_("Remove an embedded script"), nullptr), + // Align + new ContextVerb(SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR, "AlignHorizontalRightToAnchor", + N_("Align right edges of objects to the left edge of the anchor"), + N_("Align right edges of objects to the left edge of the anchor"), + INKSCAPE_ICON("align-horizontal-right-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_HORIZONTAL_LEFT, "AlignHorizontalLeft", N_("Align left edges"), + N_("Align left edges"), INKSCAPE_ICON("align-horizontal-left")), + new ContextVerb(SP_VERB_ALIGN_HORIZONTAL_CENTER, "AlignHorizontalCenter", N_("Center on vertical axis"), + N_("Center on vertical axis"), INKSCAPE_ICON("align-horizontal-center")), + new ContextVerb(SP_VERB_ALIGN_HORIZONTAL_RIGHT, "AlignHorizontalRight", N_("Align right sides"), + N_("Align right sides"), INKSCAPE_ICON("align-horizontal-right")), + new ContextVerb(SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR, "AlignHorizontalLeftToAnchor", + N_("Align left edges of objects to the right edge of the anchor"), + N_("Align left edges of objects to the right edge of the anchor"), + INKSCAPE_ICON("align-horizontal-left-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR, "AlignVerticalBottomToAnchor", + N_("Align bottom edges of objects to the top edge of the anchor"), + N_("Align bottom edges of objects to the top edge of the anchor"), + INKSCAPE_ICON("align-vertical-bottom-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_VERTICAL_TOP, "AlignVerticalTop", N_("Align top edges"), N_("Align top edges"), + INKSCAPE_ICON("align-vertical-top")), + new ContextVerb(SP_VERB_ALIGN_VERTICAL_CENTER, "AlignVerticalCenter", N_("Center on horizontal axis"), + N_("Center on horizontal axis"), INKSCAPE_ICON("align-vertical-center")), + new ContextVerb(SP_VERB_ALIGN_VERTICAL_BOTTOM, "AlignVerticalBottom", N_("Align bottom edges"), + N_("Align bottom edges"), INKSCAPE_ICON("align-vertical-bottom")), + new ContextVerb(SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR, "AlignVerticalTopToAnchor", + N_("Align top edges of objects to the bottom edge of the anchor"), + N_("Align top edges of objects to the bottom edge of the anchor"), + INKSCAPE_ICON("align-vertical-top")), + new ContextVerb(SP_VERB_ALIGN_BOTH_TOP_LEFT, "AlignBothTopLeft", + N_("Align edges of objects to the top-left corner of the anchor"), + N_("Align edges of objects to the top-left corner of the anchor"), + INKSCAPE_ICON("align-vertical-top-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_TOP_RIGHT, "AlignBothTopRight", + N_("Align edges of objects to the top-right corner of the anchor"), + N_("Align edges of objects to the top-right corner of the anchor"), + INKSCAPE_ICON("align-vertical-top-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT, "AlignBothBottomRight", + N_("Align edges of objects to the bottom-right corner of the anchor"), + N_("Align edges of objects to the bottom-right corner of the anchor"), + INKSCAPE_ICON("align-vertical-bottom-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_BOTTOM_LEFT, "AlignBothBottomLeft", + N_("Align edges of objects to the bottom-left corner of the anchor"), + N_("Align edges of objects to the bottom-left corner of the anchor"), + INKSCAPE_ICON("align-vertical-bottom-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR, "AlignBothTopLeftToAnchor", + N_("Align edges of objects to the top-left corner of the anchor"), + N_("Align edges of objects to the top-left corner of the anchor"), + INKSCAPE_ICON("align-vertical-top-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR, "AlignBothTopRightToAnchor", + N_("Align edges of objects to the top-right corner of the anchor"), + N_("Align edges of objects to the top-right corner of the anchor"), + INKSCAPE_ICON("align-vertical-top-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR, "AlignBothBottomRightToAnchor", + N_("Align edges of objects to the bottom-right corner of the anchor"), + N_("Align edges of objects to the bottom-right corner of the anchor"), + INKSCAPE_ICON("align-vertical-bottom-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR, "AlignBothBottomLeftToAnchor", + N_("Align edges of objects to the bottom-left corner of the anchor"), + N_("Align edges of objects to the bottom-left corner of the anchor"), + INKSCAPE_ICON("align-vertical-bottom-to-anchor")), + new ContextVerb(SP_VERB_ALIGN_BOTH_CENTER, "AlignVerticalHorizontalCenter", + N_("Center on horizontal and vertical axis"), N_("Center on horizontal and vertical axis"), + INKSCAPE_ICON("align-vertical-center")), + + + // Footer + new Verb(SP_VERB_LAST, " '\"invalid id", nullptr, nullptr, nullptr, nullptr) +}; + +std::vector<Inkscape::Verb *> +Verb::getList () { + + std::vector<Verb *> verbs; + // Go through the dynamic verb table + for (auto & _verb : _verbs) { + Verb * verb = _verb.second; + if (verb->get_code() == SP_VERB_INVALID || + verb->get_code() == SP_VERB_NONE || + verb->get_code() == SP_VERB_LAST) { + continue; + } + + verbs.push_back(verb); + } + + return verbs; +}; + +void +Verb::list () { + // Go through the dynamic verb table + for (auto & _verb : _verbs) { + Verb * verb = _verb.second; + if (verb->get_code() == SP_VERB_INVALID || + verb->get_code() == SP_VERB_NONE || + verb->get_code() == SP_VERB_LAST) { + continue; + } + + printf("%s: %s\n", verb->get_id(), verb->get_tip()? verb->get_tip() : verb->get_name()); + } + + return; +}; + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/verbs.h b/src/verbs.h new file mode 100644 index 0000000..706dee4 --- /dev/null +++ b/src/verbs.h @@ -0,0 +1,665 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_VERBS_H +#define SEEN_SP_VERBS_H +/* + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * David Yip <yipdw@rose-hulman.edu> + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) (date unspecified) Authors + + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstring> +#include <string> +#include <glibmm/ustring.h> + +struct SPAction; +class SPDocument; + +namespace Inkscape { + +class ActionContext; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI +} // namespace Inkscape + +/** + * This anonymous enum is used to provide a list of the Verbs + * which are defined statically in the verb files. There may be + * other verbs which are defined dynamically also. + */ +enum { + /* Header */ + SP_VERB_INVALID, /**< A dummy verb to represent doing something wrong. */ + SP_VERB_NONE, /**< A dummy verb to represent not having a verb. */ + /* File */ + SP_VERB_FILE_NEW, /**< A new file in a new window. */ + SP_VERB_FILE_OPEN, /**< Open a file. */ + SP_VERB_FILE_REVERT, /**< Revert this file to its original state. */ + SP_VERB_FILE_SAVE, /**< Save the current file with its saved filename */ + SP_VERB_FILE_SAVE_AS, /**< Save the current file with a new filename */ + SP_VERB_FILE_SAVE_A_COPY, /**< Save a copy of the current file */ + SP_VERB_FILE_SAVE_TEMPLATE, /**< Save the ciurrent document as template */ + SP_VERB_FILE_PRINT, + SP_VERB_FILE_VACUUM, + SP_VERB_FILE_IMPORT, + // SP_VERB_FILE_EXPORT, + SP_VERB_FILE_NEXT_DESKTOP, + SP_VERB_FILE_PREV_DESKTOP, + SP_VERB_FILE_CLOSE_VIEW, + SP_VERB_FILE_QUIT, + SP_VERB_FILE_TEMPLATES, + /* Edit */ + SP_VERB_EDIT_UNDO, + SP_VERB_EDIT_REDO, + SP_VERB_EDIT_CUT, + SP_VERB_EDIT_COPY, + SP_VERB_EDIT_PASTE, + SP_VERB_EDIT_PASTE_STYLE, + SP_VERB_EDIT_PASTE_SIZE, + SP_VERB_EDIT_PASTE_SIZE_X, + SP_VERB_EDIT_PASTE_SIZE_Y, + SP_VERB_EDIT_PASTE_SIZE_SEPARATELY, + SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_X, + SP_VERB_EDIT_PASTE_SIZE_SEPARATELY_Y, + SP_VERB_EDIT_PASTE_IN_PLACE, + SP_VERB_EDIT_PASTE_LIVEPATHEFFECT, + SP_VERB_EDIT_REMOVE_LIVEPATHEFFECT, + SP_VERB_EDIT_REMOVE_FILTER, + SP_VERB_EDIT_DELETE, + SP_VERB_EDIT_DUPLICATE, + SP_VERB_EDIT_CLONE, + SP_VERB_EDIT_UNLINK_CLONE, + SP_VERB_EDIT_UNLINK_CLONE_RECURSIVE, + SP_VERB_EDIT_RELINK_CLONE, + SP_VERB_EDIT_CLONE_SELECT_ORIGINAL, + SP_VERB_EDIT_CLONE_ORIGINAL_PATH_LPE, + SP_VERB_EDIT_SELECTION_2_MARKER, + SP_VERB_EDIT_SELECTION_2_GUIDES, + SP_VERB_EDIT_TILE, + SP_VERB_EDIT_UNTILE, + SP_VERB_EDIT_SYMBOL, + SP_VERB_EDIT_UNSYMBOL, + SP_VERB_EDIT_CLEAR_ALL, + SP_VERB_EDIT_SELECT_ALL, + SP_VERB_EDIT_SELECT_ALL_IN_ALL_LAYERS, + SP_VERB_EDIT_SELECT_SAME_FILL_STROKE, + SP_VERB_EDIT_SELECT_SAME_FILL_COLOR, + SP_VERB_EDIT_SELECT_SAME_STROKE_COLOR, + SP_VERB_EDIT_SELECT_SAME_STROKE_STYLE, + SP_VERB_EDIT_SELECT_SAME_OBJECT_TYPE, + SP_VERB_EDIT_INVERT, + SP_VERB_EDIT_INVERT_IN_ALL_LAYERS, + SP_VERB_EDIT_SELECT_NEXT, + SP_VERB_EDIT_SELECT_PREV, + SP_VERB_EDIT_DESELECT, + SP_VERB_EDIT_DELETE_ALL_GUIDES, + SP_VERB_EDIT_GUIDES_TOGGLE_LOCK, + SP_VERB_EDIT_GUIDES_AROUND_PAGE, + SP_VERB_EDIT_NEXT_PATHEFFECT_PARAMETER, + SP_VERB_EDIT_SWAP_FILL_STROKE, + /* Selection */ + SP_VERB_SELECTION_TO_FRONT, + SP_VERB_SELECTION_TO_BACK, + SP_VERB_SELECTION_RAISE, + SP_VERB_SELECTION_LOWER, + SP_VERB_SELECTION_STACK_UP, + SP_VERB_SELECTION_STACK_DOWN, + SP_VERB_SELECTION_GROUP, + SP_VERB_SELECTION_UNGROUP, + SP_VERB_SELECTION_UNGROUP_POP_SELECTION, + SP_VERB_SELECTION_TEXTTOPATH, + SP_VERB_SELECTION_TEXTFROMPATH, + SP_VERB_SELECTION_REMOVE_KERNS, + SP_VERB_SELECTION_UNION, + SP_VERB_SELECTION_INTERSECT, + SP_VERB_SELECTION_DIFF, + SP_VERB_SELECTION_SYMDIFF, + SP_VERB_SELECTION_CUT, + SP_VERB_SELECTION_SLICE, + SP_VERB_SELECTION_GROW, + SP_VERB_SELECTION_GROW_SCREEN, + SP_VERB_SELECTION_GROW_DOUBLE, + SP_VERB_SELECTION_SHRINK, + SP_VERB_SELECTION_SHRINK_SCREEN, + SP_VERB_SELECTION_SHRINK_HALVE, + SP_VERB_SELECTION_OFFSET, + SP_VERB_SELECTION_OFFSET_SCREEN, + SP_VERB_SELECTION_OFFSET_SCREEN_10, + SP_VERB_SELECTION_INSET, + SP_VERB_SELECTION_INSET_SCREEN, + SP_VERB_SELECTION_INSET_SCREEN_10, + SP_VERB_SELECTION_DYNAMIC_OFFSET, + SP_VERB_SELECTION_LINKED_OFFSET, + SP_VERB_SELECTION_OUTLINE, + SP_VERB_SELECTION_OUTLINE_LEGACY, + SP_VERB_SELECTION_SIMPLIFY, + SP_VERB_SELECTION_REVERSE, + SP_VERB_SELECTION_TRACE, + SP_VERB_SELECTION_CREATE_BITMAP, + SP_VERB_SELECTION_COMBINE, + SP_VERB_SELECTION_BREAK_APART, + SP_VERB_SELECTION_ARRANGE, // Former SP_VERB_SELECTION_GRIDTILE + /* Layer */ + SP_VERB_LAYER_NEW, + SP_VERB_LAYER_RENAME, + SP_VERB_LAYER_NEXT, + SP_VERB_LAYER_PREV, + SP_VERB_LAYER_MOVE_TO_NEXT, + SP_VERB_LAYER_MOVE_TO_PREV, + SP_VERB_LAYER_MOVE_TO, + SP_VERB_LAYER_TO_TOP, + SP_VERB_LAYER_TO_BOTTOM, + SP_VERB_LAYER_RAISE, + SP_VERB_LAYER_LOWER, + SP_VERB_LAYER_DUPLICATE, + SP_VERB_LAYER_DELETE, + SP_VERB_LAYER_SOLO, + SP_VERB_LAYER_SHOW_ALL, + SP_VERB_LAYER_HIDE_ALL, + SP_VERB_LAYER_LOCK_ALL, + SP_VERB_LAYER_LOCK_OTHERS, + SP_VERB_LAYER_UNLOCK_ALL, + SP_VERB_LAYER_TOGGLE_LOCK, + SP_VERB_LAYER_TOGGLE_HIDE, + /* Object */ + SP_VERB_OBJECT_ROTATE_90_CW, + SP_VERB_OBJECT_ROTATE_90_CCW, + SP_VERB_OBJECT_FLATTEN, + SP_VERB_OBJECT_TO_CURVE, + SP_VERB_OBJECT_FLOW_TEXT, + SP_VERB_OBJECT_UNFLOW_TEXT, + SP_VERB_OBJECT_FLOWTEXT_TO_TEXT, + SP_VERB_OBJECT_FLIP_HORIZONTAL, + SP_VERB_OBJECT_FLIP_VERTICAL, + SP_VERB_OBJECT_SET_MASK, + SP_VERB_OBJECT_SET_INVERSE_MASK, + SP_VERB_OBJECT_EDIT_MASK, + SP_VERB_OBJECT_UNSET_MASK, + SP_VERB_OBJECT_SET_CLIPPATH, + SP_VERB_OBJECT_SET_INVERSE_CLIPPATH, + SP_VERB_OBJECT_CREATE_CLIP_GROUP, + SP_VERB_OBJECT_EDIT_CLIPPATH, + SP_VERB_OBJECT_UNSET_CLIPPATH, + /* Tag */ + SP_VERB_TAG_NEW, + /* Tools */ + SP_VERB_CONTEXT_SELECT, + SP_VERB_CONTEXT_NODE, + SP_VERB_CONTEXT_TWEAK, + SP_VERB_CONTEXT_SPRAY, + SP_VERB_CONTEXT_RECT, + SP_VERB_CONTEXT_3DBOX, + SP_VERB_CONTEXT_ARC, + SP_VERB_CONTEXT_STAR, + SP_VERB_CONTEXT_SPIRAL, + SP_VERB_CONTEXT_PENCIL, + SP_VERB_CONTEXT_PEN, + SP_VERB_CONTEXT_CALLIGRAPHIC, + SP_VERB_CONTEXT_TEXT, + SP_VERB_CONTEXT_GRADIENT, + SP_VERB_CONTEXT_MESH, + SP_VERB_CONTEXT_ZOOM, + SP_VERB_CONTEXT_MEASURE, + SP_VERB_CONTEXT_DROPPER, + SP_VERB_CONTEXT_CONNECTOR, + SP_VERB_CONTEXT_PAINTBUCKET, + SP_VERB_CONTEXT_LPE, /* not really a tool but used for editing LPE parameters on-canvas for example */ + SP_VERB_CONTEXT_ERASER, + SP_VERB_CONTEXT_LPETOOL, /* note that this is very different from SP_VERB_CONTEXT_LPE above! */ + /* Tool preferences */ + SP_VERB_CONTEXT_SELECT_PREFS, + SP_VERB_CONTEXT_NODE_PREFS, + SP_VERB_CONTEXT_TWEAK_PREFS, + SP_VERB_CONTEXT_SPRAY_PREFS, + SP_VERB_CONTEXT_RECT_PREFS, + SP_VERB_CONTEXT_3DBOX_PREFS, + SP_VERB_CONTEXT_ARC_PREFS, + SP_VERB_CONTEXT_STAR_PREFS, + SP_VERB_CONTEXT_SPIRAL_PREFS, + SP_VERB_CONTEXT_PENCIL_PREFS, + SP_VERB_CONTEXT_PEN_PREFS, + SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS, + SP_VERB_CONTEXT_TEXT_PREFS, + SP_VERB_CONTEXT_GRADIENT_PREFS, + SP_VERB_CONTEXT_MESH_PREFS, + SP_VERB_CONTEXT_ZOOM_PREFS, + SP_VERB_CONTEXT_MEASURE_PREFS, + SP_VERB_CONTEXT_DROPPER_PREFS, + SP_VERB_CONTEXT_CONNECTOR_PREFS, + SP_VERB_CONTEXT_PAINTBUCKET_PREFS, + SP_VERB_CONTEXT_ERASER_PREFS, + SP_VERB_CONTEXT_LPETOOL_PREFS, + + /* Zooming */ + SP_VERB_ZOOM_IN, + SP_VERB_ZOOM_OUT, + SP_VERB_ZOOM_NEXT, + SP_VERB_ZOOM_PREV, + SP_VERB_ZOOM_1_1, + SP_VERB_ZOOM_1_2, + SP_VERB_ZOOM_2_1, + SP_VERB_ZOOM_PAGE, + SP_VERB_ZOOM_PAGE_WIDTH, + SP_VERB_ZOOM_DRAWING, + SP_VERB_ZOOM_SELECTION, + SP_VERB_ZOOM_CENTER_PAGE, + + /* Canvas Rotation */ + SP_VERB_ROTATE_CW, + SP_VERB_ROTATE_CCW, + SP_VERB_ROTATE_ZERO, + + /* Canvas Flip */ + SP_VERB_FLIP_HORIZONTAL, + SP_VERB_FLIP_VERTICAL, + SP_VERB_FLIP_NONE, + + /* Desktop settings */ + SP_VERB_TOGGLE_RULERS, + SP_VERB_TOGGLE_SCROLLBARS, + SP_VERB_TOGGLE_GRID, + SP_VERB_TOGGLE_GUIDES, + SP_VERB_TOGGLE_ROTATION_LOCK, + SP_VERB_TOGGLE_SNAPPING, + SP_VERB_TOGGLE_COMMANDS_TOOLBAR, + SP_VERB_TOGGLE_SNAP_TOOLBAR, + SP_VERB_TOGGLE_TOOL_TOOLBAR, + SP_VERB_TOGGLE_TOOLBOX, + SP_VERB_TOGGLE_PALETTE, + SP_VERB_TOGGLE_STATUSBAR, + SP_VERB_FULLSCREEN, + SP_VERB_FULLSCREENFOCUS, + SP_VERB_FOCUSTOGGLE, + SP_VERB_VIEW_NEW, + SP_VERB_VIEW_MODE_NORMAL, + SP_VERB_VIEW_MODE_NO_FILTERS, + SP_VERB_VIEW_MODE_OUTLINE, + SP_VERB_VIEW_MODE_VISIBLE_HAIRLINES, + SP_VERB_VIEW_MODE_TOGGLE, + SP_VERB_VIEW_COLOR_MODE_NORMAL, + SP_VERB_VIEW_COLOR_MODE_GRAYSCALE, + + // SP_VERB_VIEW_COLOR_MODE_PRINT_COLORS_PREVIEW, + SP_VERB_VIEW_COLOR_MODE_TOGGLE, + SP_VERB_VIEW_TOGGLE_SPLIT, + SP_VERB_VIEW_TOGGLE_XRAY, + SP_VERB_VIEW_CMS_TOGGLE, + SP_VERB_VIEW_ICON_PREVIEW, + + /* Dialogs */ + SP_VERB_DIALOG_PROTOTYPE, + SP_VERB_DIALOG_DISPLAY, + SP_VERB_DIALOG_NAMEDVIEW, + SP_VERB_DIALOG_METADATA, + SP_VERB_DIALOG_FILL_STROKE, + SP_VERB_DIALOG_GLYPHS, + SP_VERB_DIALOG_SWATCHES, + SP_VERB_DIALOG_SYMBOLS, + SP_VERB_DIALOG_PAINT, + SP_VERB_DIALOG_TRANSFORM, + SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + SP_VERB_DIALOG_SPRAY_OPTION, + SP_VERB_DIALOG_UNDO_HISTORY, + SP_VERB_DIALOG_TEXT, + SP_VERB_DIALOG_XML_EDITOR, + SP_VERB_DIALOG_SELECTORS, + SP_VERB_DIALOG_FIND, + +#if HAVE_ASPELL + SP_VERB_DIALOG_SPELLCHECK, +#endif + + SP_VERB_DIALOG_DEBUG, + SP_VERB_DIALOG_TOGGLE, + SP_VERB_DIALOG_CLONETILER, + SP_VERB_DIALOG_ATTR, + SP_VERB_DIALOG_ITEM, + SP_VERB_DIALOG_INPUT, + SP_VERB_DIALOG_EXTENSIONEDITOR, + SP_VERB_DIALOG_LAYERS, + SP_VERB_DIALOG_OBJECTS, + SP_VERB_DIALOG_TAGS, + SP_VERB_DIALOG_STYLE, + SP_VERB_DIALOG_LIVE_PATH_EFFECT, + SP_VERB_DIALOG_FILTER_EFFECTS, + SP_VERB_DIALOG_SVG_FONTS, + SP_VERB_DIALOG_PRINT_COLORS_PREVIEW, + SP_VERB_DIALOG_EXPORT, + /* Help */ + SP_VERB_HELP_ABOUT_EXTENSIONS, + SP_VERB_HELP_MEMORY, + SP_VERB_HELP_ABOUT, + // SP_VERB_SHOW_LICENSE, + + /* Help URLs */ + SP_VERB_HELP_URL_ASK_QUESTION, + SP_VERB_HELP_URL_MAN, + SP_VERB_HELP_URL_FAQ, + SP_VERB_HELP_URL_KEYS, + SP_VERB_HELP_URL_RELEASE_NOTES, + SP_VERB_HELP_URL_REPORT_BUG, + SP_VERB_HELP_URL_MANUAL, + SP_VERB_HELP_URL_SVG11_SPEC, + SP_VERB_HELP_URL_SVG2_SPEC, + + /* Tutorials */ + SP_VERB_TUTORIAL_BASIC, + SP_VERB_TUTORIAL_SHAPES, + SP_VERB_TUTORIAL_ADVANCED, + SP_VERB_TUTORIAL_TRACING, + SP_VERB_TUTORIAL_TRACING_PIXELART, + SP_VERB_TUTORIAL_CALLIGRAPHY, + SP_VERB_TUTORIAL_INTERPOLATE, + SP_VERB_TUTORIAL_DESIGN, + SP_VERB_TUTORIAL_TIPS, + /* Effects */ + SP_VERB_EFFECT_LAST, + SP_VERB_EFFECT_LAST_PREF, + /* Fit Canvas */ + SP_VERB_FIT_CANVAS_TO_SELECTION, + SP_VERB_FIT_CANVAS_TO_DRAWING, + SP_VERB_FIT_CANVAS_TO_SELECTION_OR_DRAWING, + /* LockAndHide */ + SP_VERB_UNLOCK_ALL, + SP_VERB_UNLOCK_ALL_IN_ALL_LAYERS, + SP_VERB_UNHIDE_ALL, + SP_VERB_UNHIDE_ALL_IN_ALL_LAYERS, + /* Color management */ + SP_VERB_EDIT_LINK_COLOR_PROFILE, + SP_VERB_EDIT_REMOVE_COLOR_PROFILE, + /*Scripting*/ + SP_VERB_EDIT_ADD_EXTERNAL_SCRIPT, + SP_VERB_EDIT_ADD_EMBEDDED_SCRIPT, + SP_VERB_EDIT_EMBEDDED_SCRIPT, + SP_VERB_EDIT_REMOVE_EXTERNAL_SCRIPT, + SP_VERB_EDIT_REMOVE_EMBEDDED_SCRIPT, + /* Alignment */ + SP_VERB_ALIGN_HORIZONTAL_RIGHT_TO_ANCHOR, + SP_VERB_ALIGN_HORIZONTAL_LEFT, + SP_VERB_ALIGN_HORIZONTAL_CENTER, + SP_VERB_ALIGN_HORIZONTAL_RIGHT, + SP_VERB_ALIGN_HORIZONTAL_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_VERTICAL_BOTTOM_TO_ANCHOR, + SP_VERB_ALIGN_VERTICAL_TOP, + SP_VERB_ALIGN_VERTICAL_CENTER, + SP_VERB_ALIGN_VERTICAL_BOTTOM, + SP_VERB_ALIGN_VERTICAL_TOP_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_TOP_LEFT, + SP_VERB_ALIGN_BOTH_TOP_RIGHT, + SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT, + SP_VERB_ALIGN_BOTH_BOTTOM_LEFT, + SP_VERB_ALIGN_BOTH_TOP_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_TOP_RIGHT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_BOTTOM_RIGHT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_BOTTOM_LEFT_TO_ANCHOR, + SP_VERB_ALIGN_BOTH_CENTER, + + /* Footer */ + SP_VERB_LAST +}; + +char *sp_action_get_title (const SPAction *action); + +#include <map> +#include <vector> + +namespace Inkscape { + +/** + * A class to represent things the user can do. In many ways + * these are 'action factories' as they are used to create + * individual actions that are based on a given view. + */ +class Verb { +private: + /** An easy to use definition of the table of verbs by code. */ + typedef std::map<unsigned int, Inkscape::Verb *> VerbTable; + + /** A table of all the dynamically created verbs. */ + static VerbTable _verbs; + + /** The table of statically created verbs which are mostly + 'base verbs'. */ + static Verb * _base_verbs[SP_VERB_LAST + 1]; + /* Plus one because there is an entry for SP_VERB_LAST */ + + /** A string comparison function to be used in the Verb ID lookup + to find the different verbs in the hash map. */ + struct ltstr { + bool operator()(const char* s1, const char* s2) const { + if ( (s1 == nullptr) && (s2 != nullptr) ) { + return true; + } else if (s1 == nullptr || s2 == nullptr) { + return false; + } else { + return strcmp(s1, s2) < 0; + } + } + }; + + /** An easy to use definition of the table of verbs by ID. */ + typedef std::map<gchar const *, Verb *, ltstr> VerbIDTable; + + /** Quick lookup of verbs by ID */ + static VerbIDTable _verb_ids; + + /** A simple typedef to make using the action table easier. */ + typedef std::map<Inkscape::UI::View::View *, SPAction *> ActionTable; + /** A list of all the actions that have been created for this + verb. It is referenced by the view that they are created for. */ + ActionTable * _actions; + + /** A unique textual ID for the verb. */ + char const * _id; + + /** The full name of the verb. (shown on menu entries) */ + char const * _name; + + /** Tooltip for the verb. */ + char const * _tip; + + char * _full_tip; // includes shortcut + + unsigned int _shortcut; + + /** Name of the image that represents the verb. */ + char const * _image; + + /** + * Unique numerical representation of the verb. In most cases + * it is a value from the anonymous enum at the top of this + * file. + */ + unsigned int _code; + + /** Name of the group the verb belongs to. */ + char const * _group; + + /** + * Whether this verb is set to default to sensitive or + * insensitive when new actions are created. + */ + bool _default_sensitive; + +protected: + + /** + * Allows for preliminary setting of the \c _default_sensitive + * value without effecting existing actions. + * This function is mostly used at initialization where there are + * not actions to effect. I can't think of another case where it + * should be used. + * + * @param in_val New value. + */ + bool set_default_sensitive (bool in_val) { return _default_sensitive = in_val; } + +public: + + /** Accessor to get the \c _default_sensitive value. */ + bool get_default_sensitive () { return _default_sensitive; } + + /** Accessor to get the internal variable. */ + unsigned int get_code () { return _code; } + + /** Accessor to get the internal variable. */ + char const * get_id () { return _id; } + + /** Accessor to get the internal variable. */ + char const * get_name () { return _name; } + + /** Accessor to get the internal variable. */ + char const * get_short_tip () { return _tip; }; + + /** Accessor to get the internal variable. */ + char const * get_tip () ; + + /** Accessor to get the internal variable. */ + char const * get_image () { return _image; } + + /** Get the verbs group */ + char const * get_group () { return _group; } + + /** Set the name after initialization. */ + char const * set_name (char const * name) { _name = name; return _name; } + + /** Set the tooltip after initialization. */ + char const * set_tip (char const * tip) { _tip = tip; return _tip; } + + +protected: + SPAction *make_action_helper (Inkscape::ActionContext const & context, void (*perform_fun)(SPAction *, void *), void *in_pntr = nullptr); + virtual SPAction *make_action (Inkscape::ActionContext const & context); + +public: + + /** + * Inititalizes the Verb with the parameters. + * + * This function also sets \c _actions to NULL. + * + * @warning NO DATA IS COPIED BY CALLING THIS FUNCTION. + * + * In many respects this is very bad object oriented design, but it + * is done for a reason. All verbs today are of two types: 1) static + * or 2) created for extension. In the static case all of the + * strings are constants in the code, and thus don't really need to + * be copied. In the extensions case the strings are identical to + * the ones already created in the extension object, copying them + * would be a waste of memory. + * + * @param code Goes to \c _code. + * @param id Goes to \c _id. + * @param name Goes to \c _name. + * @param tip Goes to \c _tip. + * @param image Goes to \c _image. + */ + Verb(const unsigned int code, + char const * id, + char const * name, + char const * tip, + char const * image, + char const * group) : + _actions(nullptr), + _id(id), + _name(name), + _tip(tip), + _full_tip(nullptr), + _shortcut(0), + _image(image), + _code(code), + _group(group), + _default_sensitive(true) + { + _verbs.insert(VerbTable::value_type(_code, this)); + _verb_ids.insert(VerbIDTable::value_type(_id, this)); + } + Verb (char const * id, char const * name, char const * tip, char const * image, char const * group); + virtual ~Verb (); + + SPAction * get_action(Inkscape::ActionContext const & context); + +private: + static Verb * get_search (unsigned int code); +public: + + /** + * A function to turn a code into a verb. + * + * This is an inline function to translate the codes which are + * static quickly. This should optimize into very quick code + * everywhere which hard coded \c codes are used. In the case + * where the \c code is not static the \c get_search function + * is used. + * + * @param code The code to be translated + * @return A pointer to a verb object or a NULL if not found. + */ + static Verb * get (unsigned int code) { + if (code <= SP_VERB_LAST) { + return _base_verbs[code]; + } else { + return get_search(code); + } + } + static Verb * getbyid (gchar const * id, bool verbose = true); + + /** + * Print a message to stderr indicating that this verb needs a GUI to run + */ + static bool ensure_desktop_valid(SPAction *action); + + static void delete_all_view (Inkscape::UI::View::View * view); + void delete_view (Inkscape::UI::View::View * view); + + void sensitive (SPDocument * in_doc = nullptr, bool in_sensitive = true); + void name (SPDocument * in_doc = nullptr, Glib::ustring in_name = ""); + +// Yes, multiple public, protected and private sections are bad. We'll clean that up later +protected: + + /** + * Returns the size of the internal base verb array. + * + * This is an inline function intended for testing. This should normally not be used. + * For testing, a subclass that returns this value can be created to verify that the + * length matches the enum values, etc. + * + * @return The size in elements of the internal base array. + */ + static int _getBaseListSize() {return G_N_ELEMENTS(_base_verbs);} + +public: + static void list (); + static std::vector<Inkscape::Verb *>getList (); + +}; /* Verb class */ + + +} /* Inkscape namespace */ + +#endif // SEEN_SP_VERBS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/version.cpp b/src/version.cpp new file mode 100644 index 0000000..7133ca3 --- /dev/null +++ b/src/version.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Versions + * + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 2012 Kris De Gussem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <sstream> +#include "version.h" + +bool sp_version_from_string(const char *string, Inkscape::Version *version) +{ + if (!string) { + return false; + } + + try + { + std::stringstream ss; + + // Throw exception if error. + ss.exceptions(std::ios::failbit | std::ios::badbit); + ss << string; + ss >> version->_major; + char tmp=0; + ss >> tmp; + ss >>version->_minor; + + // Don't throw exception if failbit gets set (empty string OK). + ss.exceptions(std::ios::goodbit); + getline(ss, version->_tail); + return true; + } + catch(...) + { + version->_major = 0; + version->_minor = 0; + version->_tail.clear(); + return false; + } +} + +char *sp_version_to_string(Inkscape::Version version) +{ + return g_strdup_printf("%u.%u%s", version._major, version._minor, version._tail.c_str()); +} + +bool sp_version_inside_range(Inkscape::Version version, + unsigned major_min, unsigned minor_min, + unsigned major_max, unsigned minor_max) +{ + if ( version._major < major_min || version._major > major_max ) { + return false; + } else if ( version._major == major_min && + version._minor <= minor_min ) + { + return false; + } else if ( version._major == major_max && + version._minor >= minor_max ) + { + return false; + } else { + return true; + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/version.h b/src/version.h new file mode 100644 index 0000000..ecf10b1 --- /dev/null +++ b/src/version.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2003 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_VERSION_H +#define SEEN_INKSCAPE_VERSION_H + +#define SVG_VERSION "1.1" + +#include <string> + +namespace Inkscape { + +class Version { +public: + + Version() : _major(0), _minor(0) {} + + // Note: somebody pollutes our namespace with major() and minor() + Version(unsigned mj, unsigned mn) : _major(mj), _minor(mn) {} + + bool operator>(Version const &other) const { + return _major > other._major || + ( _major == other._major && _minor > other._minor ); + } + + bool operator==(Version const &other) const { + return _major == other._major && _minor == other._minor; + } + + bool operator!=(Version const &other) const { + return _major != other._major || _minor != other._minor; + } + + bool operator<(Version const &other) const { + return _major < other._major || + ( _major == other._major && _minor < other._minor ); + } + + unsigned int _major; + unsigned int _minor; + std::string _tail; // Development version +}; + +} + +bool sp_version_from_string(const char *string, Inkscape::Version *version); + +char *sp_version_to_string(Inkscape::Version version); + +bool sp_version_inside_range(Inkscape::Version version, + unsigned major_min, unsigned minor_min, + unsigned major_max, unsigned minor_max); + +#endif // SEEN_INKSCAPE_VERSION_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:75 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt new file mode 100644 index 0000000..3ed2a37 --- /dev/null +++ b/src/widgets/CMakeLists.txt @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(widgets_SRC + desktop-widget.cpp + ege-paint-def.cpp + fill-style.cpp + gradient-image.cpp + gradient-selector.cpp + gradient-vector.cpp + ink-action.cpp + paint-selector.cpp + sp-attribute-widget.cpp + sp-color-selector.cpp + sp-xmlview-tree.cpp + spinbutton-events.cpp + spw-utilities.cpp + stroke-marker-selector.cpp + stroke-style.cpp + swatch-selector.cpp + toolbox.cpp + + # ------- + # Headers + desktop-widget.h + ege-paint-def.h + fill-n-stroke-factory.h + fill-style.h + gradient-image.h + gradient-selector.h + gradient-vector.h + ink-action.h + paint-selector.h + sp-attribute-widget.h + sp-color-selector.h + sp-xmlview-tree.h + spinbutton-events.h + spw-utilities.h + stroke-marker-selector.h + stroke-style.h + swatch-selector.h + toolbox.h + widget-sizes.h +) + +add_inkscape_source("${widgets_SRC}") diff --git a/src/widgets/README b/src/widgets/README new file mode 100644 index 0000000..b1c905a --- /dev/null +++ b/src/widgets/README @@ -0,0 +1,10 @@ + + +This directory contains widgets written in 'C'. + +To do: + +* Replace 'C' widgets by 'C++' widgets. +* Temporary move to 'ui/widgets/legacy'. + + diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp new file mode 100644 index 0000000..491ce41 --- /dev/null +++ b/src/widgets/desktop-widget.cpp @@ -0,0 +1,2570 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Desktop widget implementation + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * bulia byak <buliabyak@users.sf.net> + * Ralf Stephan <ralf@ark.in-berlin.de> + * John Bintz <jcoswell@coswellproductions.org> + * Johan Engelen <j.b.c.engelen@ewi.utwente.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2007 Johan Engelen + * Copyright (C) 2006 John Bintz + * Copyright (C) 2004 MenTaLguY + * Copyright (C) 1999-2002 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <2geom/rect.h> + +#include "attributes.h" +#include "cms-system.h" +#include "conn-avoid-ref.h" +#include "desktop-events.h" +#include "desktop-widget.h" +#include "desktop.h" +#include "document-undo.h" +#include "ege-color-prof-tracker.h" +#include "file.h" +#include "inkscape-version.h" +#include "verbs.h" + +#include "display/canvas-arena.h" +#include "display/canvas-axonomgrid.h" +#include "display/guideline.h" +#include "display/sp-canvas.h" + +#include "extension/db.h" + +#include "helper/action.h" + +#include "object/sp-image.h" +#include "object/sp-namedview.h" +#include "object/sp-root.h" + +#include "ui/dialog/dialog-manager.h" +#include "ui/dialog/swatches.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/tools/box3d-tool.h" +#include "ui/uxmanager.h" +#include "ui/widget/button.h" +#include "ui/widget/dock.h" +#include "ui/widget/ink-ruler.h" +#include "ui/widget/layer-selector.h" +#include "ui/widget/selected-style.h" +#include "ui/widget/spin-button-tool-item.h" +#include "ui/widget/unit-tracker.h" + +// TEMP +#include "ui/desktop/menubar.h" + +#include "util/ege-appear-time-tracker.h" +#include "util/units.h" + +// We're in the "widgets" directory, so no need to explicitly prefix these: +#include "spinbutton-events.h" +#include "spw-utilities.h" +#include "toolbox.h" +#include "widget-sizes.h" + +#ifdef GDK_WINDOWING_QUARTZ +#include <gtkosxapplication.h> +#endif + +using Inkscape::DocumentUndo; +using Inkscape::UI::Widget::UnitTracker; +using Inkscape::UI::UXManager; +using Inkscape::UI::ToolboxFactory; +using ege::AppearTimeTracker; +using Inkscape::Util::unit_table; + + +//--------------------------------------------------------------------- +/* SPDesktopWidget */ + +static void sp_desktop_widget_class_init (SPDesktopWidgetClass *klass); + +static void sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation); +static void sp_desktop_widget_realize (GtkWidget *widget); + +static void sp_desktop_widget_adjustment_value_changed (GtkAdjustment *adj, SPDesktopWidget *dtw); + +static gdouble sp_dtw_zoom_value_to_display (gdouble value); +static gdouble sp_dtw_zoom_display_to_value (gdouble value); +static void sp_dtw_zoom_menu_handler (SPDesktop *dt, gdouble factor); + +SPViewWidgetClass *dtw_parent_class; + +class CMSPrefWatcher { +public: + CMSPrefWatcher() : + _dpw(*this), + _spw(*this), + _tracker(ege_color_prof_tracker_new(nullptr)) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + g_signal_connect( G_OBJECT(_tracker), "modified", G_CALLBACK(hook), this ); + prefs->addObserver(_dpw); + prefs->addObserver(_spw); + } + virtual ~CMSPrefWatcher() = default; + + //virtual void notify(PrefValue &); + void add( SPDesktopWidget* dtw ) { + _widget_list.push_back(dtw); + } + void remove( SPDesktopWidget* dtw ) { + _widget_list.remove(dtw); + } + +private: + static void hook(EgeColorProfTracker *tracker, gint b, CMSPrefWatcher *watcher); + + class DisplayProfileWatcher : public Inkscape::Preferences::Observer { + public: + DisplayProfileWatcher(CMSPrefWatcher &pw) : Observer("/options/displayprofile"), _pw(pw) {} + void notify(Inkscape::Preferences::Entry const &/*val*/) override { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _pw._setCmsSensitive(!prefs->getString("/options/displayprofile/uri").empty()); + _pw._refreshAll(); + } + private: + CMSPrefWatcher &_pw; + }; + + DisplayProfileWatcher _dpw; + + class SoftProofWatcher : public Inkscape::Preferences::Observer { + public: + SoftProofWatcher(CMSPrefWatcher &pw) : Observer("/options/softproof"), _pw(pw) {} + void notify(Inkscape::Preferences::Entry const &) override { + _pw._refreshAll(); + } + private: + CMSPrefWatcher &_pw; + }; + + SoftProofWatcher _spw; + + void _refreshAll(); + void _setCmsSensitive(bool value); + + std::list<SPDesktopWidget*> _widget_list; + EgeColorProfTracker *_tracker; + + friend class DisplayProfileWatcher; + friend class SoftproofWatcher; +}; + +#if defined(HAVE_LIBLCMS2) +void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint monitor, CMSPrefWatcher * /*watcher*/) +{ + unsigned char* buf = nullptr; + guint len = 0; + + ege_color_prof_tracker_get_profile_for( monitor, reinterpret_cast<gpointer*>(&buf), &len ); + Glib::ustring id = Inkscape::CMSSystem::setDisplayPer( buf, len, monitor ); +} +#else +void CMSPrefWatcher::hook(EgeColorProfTracker * /*tracker*/, gint /*monitor*/, CMSPrefWatcher * /*watcher*/) +{ +} +#endif // defined(HAVE_LIBLCMS2) + +/// @todo Use conditional compilation in saner places. The whole PrefWatcher +/// object is unnecessary if defined(HAVE_LIBLCMS2) is not defined. +void CMSPrefWatcher::_refreshAll() +{ +#if defined(HAVE_LIBLCMS2) + for (auto & it : _widget_list) { + it->requestCanvasUpdate(); + } +#endif // defined(HAVE_LIBLCMS2) +} + +void CMSPrefWatcher::_setCmsSensitive(bool enabled) +{ +#if defined(HAVE_LIBLCMS2) + for ( auto dtw : _widget_list ) { + auto cms_adj = dtw->get_cms_adjust(); + if ( cms_adj->get_sensitive() != enabled ) { + dtw->cms_adjust_set_sensitive(enabled); + } + } +#else + (void) enabled; +#endif // defined(HAVE_LIBLCMS2) +} + +static CMSPrefWatcher* watcher = nullptr; + +void +SPDesktopWidget::setMessage (Inkscape::MessageType type, const gchar *message) +{ + _select_status->set_markup(message ? message : ""); + + // make sure the important messages are displayed immediately! + if (type == Inkscape::IMMEDIATE_MESSAGE && _select_status->get_is_drawable()) { + _select_status->queue_draw(); + } + + _select_status->set_tooltip_text(_select_status->get_text()); +} + +Geom::Point +SPDesktopWidget::window_get_pointer() +{ + int x, y; + auto window = Glib::wrap(GTK_WIDGET(_canvas))->get_window(); + auto display = window->get_display(); + auto seat = display->get_default_seat(); + auto device = seat->get_pointer(); + Gdk::ModifierType m; + window->get_device_position(device, x, y, m); + + return Geom::Point(x, y); +} + +static GTimer *overallTimer = nullptr; + +/** + * Registers SPDesktopWidget class and returns its type number. + */ +GType SPDesktopWidget::getType() +{ + static GType type = 0; + if (!type) { + GTypeInfo info = { + sizeof(SPDesktopWidgetClass), + nullptr, // base_init + nullptr, // base_finalize + (GClassInitFunc)sp_desktop_widget_class_init, + nullptr, // class_finalize + nullptr, // class_data + sizeof(SPDesktopWidget), + 0, // n_preallocs + (GInstanceInitFunc)SPDesktopWidget::init, + nullptr // value_table + }; + type = g_type_register_static(SP_TYPE_VIEW_WIDGET, "SPDesktopWidget", &info, static_cast<GTypeFlags>(0)); + // Begin a timer to watch for the first desktop to appear on-screen + overallTimer = g_timer_new(); + } + return type; +} + +/** + * SPDesktopWidget vtable initialization + */ +static void +sp_desktop_widget_class_init (SPDesktopWidgetClass *klass) +{ + dtw_parent_class = SP_VIEW_WIDGET_CLASS(g_type_class_peek_parent(klass)); + + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + object_class->dispose = SPDesktopWidget::dispose; + + widget_class->size_allocate = sp_desktop_widget_size_allocate; + widget_class->realize = sp_desktop_widget_realize; +} + +/** + * Callback for changes in size of the canvas table (i.e. the container for + * the canvas, the rulers etc). + * + * This adjusts the range of the rulers when the dock container is adjusted + * (fixes lp:950552) + * + * This fix was causing the rulers to be completely redrawn when not needed. + * Added check to see if allocation really changed. + * + *(Question, why is the callback being called when allocation not changed?) + */ +void +SPDesktopWidget::canvas_tbl_size_allocate(Gtk::Allocation& allocation) +{ + if (_allocation == allocation) { + return; + } + + _allocation = allocation; + update_rulers(); +} + +/** + * Callback for SPDesktopWidget object initialization. + */ +void SPDesktopWidget::init( SPDesktopWidget *dtw ) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + new (&dtw->modified_connection) sigc::connection(); + + dtw->_ruler_clicked = false; + dtw->_ruler_dragged = false; + dtw->_active_guide = nullptr; + dtw->_xp = 0; + dtw->_yp = 0; + dtw->window = nullptr; + dtw->desktop = nullptr; + dtw->_interaction_disabled_counter = 0; + + /* Main table */ + dtw->_vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + dtw->_vbox->set_name("DesktopMainTable"); + gtk_container_add( GTK_CONTAINER(dtw), GTK_WIDGET(dtw->_vbox->gobj()) ); + + /* Status bar */ + dtw->_statusbar = Gtk::manage(new Gtk::Box()); + dtw->_statusbar->set_name("DesktopStatusBar"); + dtw->_vbox->pack_end(*dtw->_statusbar, false, true); + + /* Swatches panel */ + { + dtw->_panels = new Inkscape::UI::Dialog::SwatchesPanel("/embedded/swatches"); + dtw->_panels->set_vexpand(false); + dtw->_vbox->pack_end(*dtw->_panels, false, true); + } + + /* DesktopHBox (Vertical toolboxes, canvas) */ + dtw->_hbox = Gtk::manage(new Gtk::Box()); + dtw->_hbox->set_name("DesktopHbox"); + dtw->_vbox->pack_end(*dtw->_hbox, true, true); + + /* Toolboxes */ + dtw->aux_toolbox = ToolboxFactory::createAuxToolbox(); + dtw->_vbox->pack_end(*Glib::wrap(dtw->aux_toolbox), false, true); + + dtw->snap_toolbox = ToolboxFactory::createSnapToolbox(); + ToolboxFactory::setOrientation( dtw->snap_toolbox, GTK_ORIENTATION_VERTICAL ); + dtw->_hbox->pack_end(*Glib::wrap(dtw->snap_toolbox), false, true); + + dtw->commands_toolbox = ToolboxFactory::createCommandsToolbox(); + dtw->_vbox->pack_end(*Glib::wrap(dtw->commands_toolbox), false, true); + + dtw->tool_toolbox = ToolboxFactory::createToolToolbox(); + ToolboxFactory::setOrientation( dtw->tool_toolbox, GTK_ORIENTATION_VERTICAL ); + dtw->_hbox->pack_start(*Glib::wrap(dtw->tool_toolbox), false, true); + + /* Canvas table wrapper */ + auto tbl_wrapper = Gtk::manage(new Gtk::Grid()); // Is this widget really needed? No! + tbl_wrapper->set_name("CanvasTableWrapper"); + dtw->_hbox->pack_start(*tbl_wrapper, true, true, 1); + + /* Canvas table */ + dtw->_canvas_tbl = Gtk::manage(new Gtk::Grid()); + dtw->_canvas_tbl->set_name("CanvasTable"); + // Added to table wrapper later either directly or via paned window shared with dock. + + + + // Lock guides button + dtw->_guides_lock = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("object-locked"), + _("Toggle lock of all guides in the document"))); + + auto guides_lock_style_provider = Gtk::CssProvider::create(); + guides_lock_style_provider->load_from_data("GtkWidget { padding-left: 0; padding-right: 0; padding-top: 0; padding-bottom: 0; }"); + dtw->_guides_lock->set_name("LockGuides"); + auto context = dtw->_guides_lock->get_style_context(); + context->add_provider(guides_lock_style_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + dtw->_guides_lock->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::update_guides_lock)); + dtw->_canvas_tbl->attach(*dtw->_guides_lock, 0, 0, 1, 1); + + /* Rulers */ + Inkscape::Util::Unit const *pt = unit_table.getUnit("pt"); + + /* Horizontal ruler */ + dtw->_hruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_HORIZONTAL)); + dtw->_hruler->set_unit(pt); + + // We should probably get rid of this and attach the signals directly rulers. + dtw->_hruler_box = Gtk::manage(new Gtk::EventBox()); + dtw->_hruler_box->set_tooltip_text(gettext(pt->name_plural.c_str())); + dtw->_hruler_box->add(*dtw->_hruler); + dtw->_hruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_hruler_box, true)); + dtw->_hruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_hruler_box, true)); + dtw->_hruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_hruler_box, true)); + + dtw->_canvas_tbl->attach(*dtw->_hruler_box, 1, 0, 1, 1); + + /* Vertical ruler */ + dtw->_vruler = Gtk::manage(new Inkscape::UI::Widget::Ruler(Gtk::ORIENTATION_VERTICAL)); + dtw->_vruler->set_unit(pt); + + dtw->_vruler_box = Gtk::manage(new Gtk::EventBox()); + dtw->_vruler_box->set_tooltip_text(gettext(pt->name_plural.c_str())); + dtw->_vruler_box->add(*dtw->_vruler); + dtw->_vruler_box->signal_button_press_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_press_event), dtw->_vruler_box, false)); + dtw->_vruler_box->signal_button_release_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_button_release_event), dtw->_vruler_box, false)); + dtw->_vruler_box->signal_motion_notify_event().connect(sigc::bind(sigc::mem_fun(*dtw, &SPDesktopWidget::on_ruler_box_motion_notify_event), dtw->_vruler_box, false)); + + dtw->_canvas_tbl->attach(*dtw->_vruler_box, 0, 1, 1, 1); + + // Horizontal scrollbar + dtw->_hadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + dtw->_hscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_hadj)); + dtw->_hscrollbar->set_name("HorizontalScrollbar"); + dtw->_canvas_tbl->attach(*dtw->_hscrollbar, 1, 2, 1, 1); + + // By packing the sticky zoom button and vertical scrollbar in a box it allows the canvas to + // expand fully to the top if the rulers are hidden. + // (Otherwise, the canvas is pushed down by the height of the sticky zoom button.) + + // Vertical Scrollbar box + dtw->_vscrollbar_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + dtw->_canvas_tbl->attach(*dtw->_vscrollbar_box, 2, 0, 1, 2); + + // Sticky zoom button + dtw->_sticky_zoom = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("zoom-original"), + _("Zoom drawing if window size changes"))); + dtw->_sticky_zoom->set_name("StickyZoom"); + dtw->_sticky_zoom->set_active(prefs->getBool("/options/stickyzoom/value")); + dtw->_sticky_zoom->signal_toggled().connect(sigc::mem_fun(dtw, &SPDesktopWidget::sticky_zoom_toggled)); + dtw->_vscrollbar_box->pack_start(*dtw->_sticky_zoom, false, false); + + // Vertical scrollbar + dtw->_vadj = Gtk::Adjustment::create(0.0, -4000.0, 4000.0, 10.0, 100.0, 4.0); + dtw->_vscrollbar = Gtk::manage(new Gtk::Scrollbar(dtw->_vadj, Gtk::ORIENTATION_VERTICAL)); + dtw->_vscrollbar->set_name("VerticalScrollbar"); + dtw->_vscrollbar_box->pack_start(*dtw->_vscrollbar, true, true, 0); + + gchar const* tip = ""; + Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE ); + if ( verb ) { + SPAction *act = verb->get_action( Inkscape::ActionContext( dtw->viewwidget.view ) ); + if ( act && act->tip ) { + tip = act->tip; + } + } + dtw->_cms_adjust = Gtk::manage(new Inkscape::UI::Widget::Button(GTK_ICON_SIZE_MENU, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + nullptr, + INKSCAPE_ICON("color-management"), + tip )); + dtw->_cms_adjust->set_name("CMS_Adjust"); + +#if defined(HAVE_LIBLCMS2) + { + Glib::ustring current = prefs->getString("/options/displayprofile/uri"); + bool enabled = current.length() > 0; + dtw->cms_adjust_set_sensitive(enabled ); + if ( enabled ) { + bool active = prefs->getBool("/options/displayprofile/enable"); + if ( active ) { + dtw->_cms_adjust->toggle_set_down(true); + } + } + } + g_signal_connect_after( G_OBJECT(dtw->_cms_adjust->gobj()), "clicked", G_CALLBACK(SPDesktopWidget::cms_adjust_toggled), dtw ); +#else + dtw->cms_adjust_set_sensitive(false); +#endif // defined(HAVE_LIBLCMS2) + + dtw->_canvas_tbl->attach(*dtw->_cms_adjust, 2, 2, 1, 1); + { + if (!watcher) { + watcher = new CMSPrefWatcher(); + } + watcher->add(dtw); + } + /* Canvas */ + dtw->_canvas = SP_CANVAS(SPCanvas::createAA()); +#if defined(HAVE_LIBLCMS2) + dtw->_canvas->_enable_cms_display_adj = prefs->getBool("/options/displayprofile/enable"); +#endif // defined(HAVE_LIBLCMS2) + gtk_widget_set_can_focus (GTK_WIDGET (dtw->_canvas), TRUE); + + dtw->_hruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas))); + dtw->_vruler->add_track_widget(*Glib::wrap(GTK_WIDGET(dtw->_canvas))); + + auto css_provider = gtk_css_provider_new(); + auto style_context = gtk_widget_get_style_context(GTK_WIDGET(dtw->_canvas)); + + gtk_css_provider_load_from_data(css_provider, + "SPCanvas {\n" + " background-color: white;\n" + "}\n", + -1, nullptr); + + gtk_style_context_add_provider(style_context, + GTK_STYLE_PROVIDER(css_provider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + g_signal_connect(G_OBJECT(dtw->_canvas), "event", G_CALLBACK(SPDesktopWidget::event), dtw); + + gtk_widget_set_hexpand(GTK_WIDGET(dtw->_canvas), TRUE); + gtk_widget_set_vexpand(GTK_WIDGET(dtw->_canvas), TRUE); + dtw->_canvas_tbl->attach(*Glib::wrap(GTK_WIDGET(dtw->_canvas)), 1, 1, 1, 1); + + /* Dock */ + bool create_dock = + prefs->getIntLimited("/options/dialogtype/value", Inkscape::UI::Dialog::FLOATING, 0, 1) == + Inkscape::UI::Dialog::DOCK; + + if (create_dock) { + dtw->_dock = new Inkscape::UI::Widget::Dock(); + auto paned = new Gtk::Paned(); + paned->set_name("Canvas_and_Dock"); + + paned->pack1(*dtw->_canvas_tbl); + paned->pack2(dtw->_dock->getWidget(), Gtk::FILL); + + /* Prevent the paned from catching F6 and F8 by unsetting the default callbacks */ + if (GtkPanedClass *paned_class = GTK_PANED_CLASS (G_OBJECT_GET_CLASS (paned->gobj()))) { + paned_class->cycle_child_focus = nullptr; + paned_class->cycle_handle_focus = nullptr; + } + + paned->set_hexpand(true); + paned->set_vexpand(true); + tbl_wrapper->attach(*paned, 1, 1, 1, 1); + } else { + dtw->_canvas_tbl->set_hexpand(true); + dtw->_canvas_tbl->set_vexpand(true); + tbl_wrapper->attach(*(dtw->_canvas_tbl), 1, 1, 1, 1); + } + + // connect scrollbar signals + dtw->_hadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed)); + dtw->_vadj->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::on_adjustment_value_changed)); + + // --------------- Status Tool Bar ------------------// + + // Selected Style (Fill/Stroke/Opacity) + dtw->_selected_style = new Inkscape::UI::Widget::SelectedStyle(true); + dtw->_statusbar->pack_start(*dtw->_selected_style, false, false); + + // Separator + dtw->_statusbar->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), + false, false); + + // Layer Selector + dtw->layer_selector = new Inkscape::UI::Widget::LayerSelector(nullptr); + // FIXME: need to unreference on container destruction to avoid leak + dtw->layer_selector->reference(); + dtw->_statusbar->pack_start(*dtw->layer_selector, false, false, 1); + + // Select Status + dtw->_select_status = Gtk::manage(new Gtk::Label()); + dtw->_select_status->set_name("SelectStatus"); + dtw->_select_status->set_ellipsize(Pango::ELLIPSIZE_END); + dtw->_select_status->set_line_wrap(true); + dtw->_select_status->set_lines(2); + dtw->_select_status->set_halign(Gtk::ALIGN_START); + dtw->_select_status->set_size_request(1, -1); + + // Display the initial welcome message in the statusbar + dtw->_select_status->set_markup(_("<b>Welcome to Inkscape!</b> Use shape or freehand tools to create objects; use selector (arrow) to move or transform them.")); + + dtw->_statusbar->pack_start(*dtw->_select_status, true, true); + + + // Zoom status spinbutton --------------- + auto zoom_adj = Gtk::Adjustment::create(100.0, log(SP_DESKTOP_ZOOM_MIN)/log(2), log(SP_DESKTOP_ZOOM_MAX)/log(2), 0.1); + dtw->_zoom_status = Gtk::manage(new Gtk::SpinButton(zoom_adj)); + + dtw->_zoom_status->set_data("dtw", dtw->_canvas); + dtw->_zoom_status->set_tooltip_text(_("Zoom")); + dtw->_zoom_status->set_size_request(STATUS_ZOOM_WIDTH, -1); + dtw->_zoom_status->set_width_chars(6); + dtw->_zoom_status->set_numeric(false); + dtw->_zoom_status->set_update_policy(Gtk::UPDATE_ALWAYS); + + // Callbacks + dtw->_zoom_status_input_connection = dtw->_zoom_status->signal_input().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_input)); + dtw->_zoom_status_output_connection = dtw->_zoom_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_output)); + g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_zoom_status->gobj()); + g_signal_connect (G_OBJECT (dtw->_zoom_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_zoom_status->gobj()); + dtw->_zoom_status_value_changed_connection = dtw->_zoom_status->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_value_changed)); + dtw->_zoom_status_populate_popup_connection = dtw->_zoom_status->signal_populate_popup().connect(sigc::mem_fun(dtw, &SPDesktopWidget::zoom_populate_popup)); + + // Style + auto css_provider_spinbutton = Gtk::CssProvider::create(); + css_provider_spinbutton->load_from_data("* { padding-left: 2px; padding-right: 2px; padding-top: 0px; padding-bottom: 0px;}"); // Shouldn't this be in a style sheet? Used also by rotate. + + dtw->_zoom_status->set_name("ZoomStatus"); + auto context_zoom = dtw->_zoom_status->get_style_context(); + context_zoom->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + // Rotate status spinbutton --------------- + auto rotation_adj = Gtk::Adjustment::create(0, -360.0, 360.0, 1.0); + dtw->_rotation_status = Gtk::manage(new Gtk::SpinButton(rotation_adj)); + dtw->_rotation_status->set_data("dtw", dtw->_canvas); + dtw->_rotation_status->set_tooltip_text(_("Rotation. (Also Ctrl+Shift+Scroll)")); + dtw->_rotation_status->set_size_request(STATUS_ROTATION_WIDTH, -1); + dtw->_rotation_status->set_width_chars(7); + dtw->_rotation_status->set_numeric(false); + dtw->_rotation_status->set_digits(2); + dtw->_rotation_status->set_increments(1.0, 15.0); + dtw->_rotation_status->set_update_policy(Gtk::UPDATE_ALWAYS); + + // Callbacks + dtw->_rotation_status_input_connection = dtw->_rotation_status->signal_input().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_input)); + dtw->_rotation_status_output_connection = dtw->_rotation_status->signal_output().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_output)); + g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "focus-in-event", G_CALLBACK (spinbutton_focus_in), dtw->_rotation_status->gobj()); + g_signal_connect (G_OBJECT (dtw->_rotation_status->gobj()), "key-press-event", G_CALLBACK (spinbutton_keypress), dtw->_rotation_status->gobj()); + dtw->_rotation_status_value_changed_connection = dtw->_rotation_status->signal_value_changed().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_value_changed)); + dtw->_rotation_status_populate_popup_connection = dtw->_rotation_status->signal_populate_popup().connect(sigc::mem_fun(dtw, &SPDesktopWidget::rotation_populate_popup)); + + // Style + dtw->_rotation_status->set_name("RotationStatus"); + auto context_rotation = dtw->_rotation_status->get_style_context(); + context_rotation->add_provider(css_provider_spinbutton, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + + // Cursor coordinates + dtw->_coord_status = Gtk::manage(new Gtk::Grid()); + dtw->_coord_status->set_name("CoordinateAndZStatus"); + dtw->_coord_status->set_row_spacing(0); + dtw->_coord_status->set_column_spacing(10); + dtw->_coord_status->set_margin_end(10); + auto sep = Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)); + sep->set_name("CoordinateSeparator"); + dtw->_coord_status->attach(*sep, 0, 0, 1, 2); + + dtw->_coord_status->set_tooltip_text(_("Cursor coordinates")); + auto label_x = Gtk::manage(new Gtk::Label(_("X:"))); + auto label_y = Gtk::manage(new Gtk::Label(_("Y:"))); + label_x->set_halign(Gtk::ALIGN_START); + label_y->set_halign(Gtk::ALIGN_START); + dtw->_coord_status->attach(*label_x, 1, 0, 1, 1); + dtw->_coord_status->attach(*label_y, 1, 1, 1, 1); + dtw->_coord_status_x = Gtk::manage(new Gtk::Label()); + dtw->_coord_status_y = Gtk::manage(new Gtk::Label()); + dtw->_coord_status_x->set_name("CoordinateStatusX"); + dtw->_coord_status_y->set_name("CoordinateStatusY"); + dtw->_coord_status_x->set_markup(" 0.00 "); + dtw->_coord_status_y->set_markup(" 0.00 "); + + auto label_z = Gtk::manage(new Gtk::Label(_("Z:"))); + label_z->set_name("ZLabel"); + auto label_r = Gtk::manage(new Gtk::Label(_("R:"))); + label_r->set_name("RLabel"); + + dtw->_coord_status_x->set_halign(Gtk::ALIGN_END); + dtw->_coord_status_y->set_halign(Gtk::ALIGN_END); + dtw->_coord_status->attach(*dtw->_coord_status_x, 2, 0, 1, 1); + dtw->_coord_status->attach(*dtw->_coord_status_y, 2, 1, 1, 1); + + dtw->_coord_status->attach(*label_z, 3, 0, 1, 2); + dtw->_coord_status->attach(*dtw->_zoom_status, 4, 0, 1, 2); + + dtw->_coord_status->attach(*label_r, 5, 0, 1, 2); + dtw->_coord_status->attach(*dtw->_rotation_status, 6, 0, 1, 2); + + dtw->_statusbar->pack_end(*dtw->_coord_status, false, false); + + // --------------- Color Management ---------------- // + dtw->_tracker = ege_color_prof_tracker_new(GTK_WIDGET(dtw->layer_selector->gobj())); +#if defined(HAVE_LIBLCMS2) + bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); + if ( fromDisplay ) { + Glib::ustring id = Inkscape::CMSSystem::getDisplayId( 0 ); + + bool enabled = false; + dtw->_canvas->_cms_key = id; + enabled = !dtw->_canvas->_cms_key.empty(); + dtw->cms_adjust_set_sensitive(enabled); + } + g_signal_connect( G_OBJECT(dtw->_tracker), "changed", G_CALLBACK(SPDesktopWidget::color_profile_event), dtw ); +#endif // defined(HAVE_LIBLCMS2) + + // ------------------ Finish Up -------------------- // + dtw->_vbox->show_all(); + + gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas)); + + // If this is the first desktop created, report the time it takes to show up + if ( overallTimer ) { + if ( prefs->getBool("/dialogs/debug/trackAppear", false) ) { + // Time tracker takes ownership of the timer. + AppearTimeTracker *tracker = new AppearTimeTracker(overallTimer, GTK_WIDGET(dtw), "first SPDesktopWidget"); + tracker->setAutodelete(true); + } else { + g_timer_destroy(overallTimer); + } + overallTimer = nullptr; + } + // Ensure that ruler ranges are updated correctly whenever the canvas table + // is resized + dtw->_canvas_tbl_size_allocate_connection = dtw->_canvas_tbl->signal_size_allocate().connect(sigc::mem_fun(dtw, &SPDesktopWidget::canvas_tbl_size_allocate)); +} + +/** + * Called before SPDesktopWidget destruction. + */ +void +SPDesktopWidget::dispose(GObject *object) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (object); + + if (dtw == nullptr) { + return; + } + + UXManager::getInstance()->delTrack(dtw); + + if (dtw->desktop) { + if ( watcher ) { + watcher->remove(dtw); + } + + // Zoom + dtw->_zoom_status_input_connection.disconnect(); + dtw->_zoom_status_output_connection.disconnect(); + g_signal_handlers_disconnect_matched (G_OBJECT (dtw->_zoom_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_zoom_status); + dtw->_zoom_status_value_changed_connection.disconnect(); + dtw->_zoom_status_populate_popup_connection.disconnect(); + + // Rotation + dtw->_rotation_status_input_connection.disconnect(); + dtw->_rotation_status_output_connection.disconnect(); + g_signal_handlers_disconnect_matched (G_OBJECT (dtw->_rotation_status->gobj()), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, dtw->_rotation_status); + dtw->_rotation_status_value_changed_connection.disconnect(); + dtw->_rotation_status_populate_popup_connection.disconnect(); + + // Canvas + g_signal_handlers_disconnect_by_func (G_OBJECT (dtw->_canvas), (gpointer) G_CALLBACK (SPDesktopWidget::event), dtw); + dtw->_canvas_tbl_size_allocate_connection.disconnect(); + + dtw->layer_selector->setDesktop(nullptr); + dtw->layer_selector->unreference(); + INKSCAPE.remove_desktop(dtw->desktop); // clears selection and event_context + dtw->modified_connection.disconnect(); + dtw->desktop->destroy(); + Inkscape::GC::release (dtw->desktop); + dtw->desktop = nullptr; + } + + dtw->modified_connection.~connection(); + + if (G_OBJECT_CLASS (dtw_parent_class)->dispose) { + (* G_OBJECT_CLASS (dtw_parent_class)->dispose) (object); + } +} + +/** + * Set the title in the desktop-window (if desktop has an own window). + * + * The title has form file name: desktop number - Inkscape. + * The desktop number is only shown if it's 2 or higher, + */ +void +SPDesktopWidget::updateTitle(gchar const* uri) +{ + if (window) { + + SPDocument *doc = this->desktop->doc(); + + std::string Name; + if (doc->isModifiedSinceSave()) { + Name += "*"; + } + + Name += uri; + + if (desktop->number > 1) { + Name += ": "; + Name += std::to_string(desktop->number); + } + Name += " ("; + + if (desktop->getMode() == Inkscape::RENDERMODE_OUTLINE) { + Name += N_("outline"); + } else if (desktop->getMode() == Inkscape::RENDERMODE_NO_FILTERS) { + Name += N_("no filters"); + } else if (desktop->getMode() == Inkscape::RENDERMODE_VISIBLE_HAIRLINES) { + Name += N_("visible hairlines"); + } + + if (desktop->getColorMode() != Inkscape::COLORMODE_NORMAL && + desktop->getMode() != Inkscape::RENDERMODE_NORMAL) { + Name += ", "; + } + + if (desktop->getColorMode() == Inkscape::COLORMODE_GRAYSCALE) { + Name += N_("grayscale"); + } else if (desktop->getColorMode() == Inkscape::COLORMODE_PRINT_COLORS_PREVIEW) { + Name += N_("print colors preview"); + } + + if (*Name.rbegin() == '(') { // Can not use C++11 .back() or .pop_back() with ustring! + Name.erase(Name.size() - 2); + } else { + Name += ")"; + } + + Name += " - Inkscape"; + + // Name += " ("; + // Name += Inkscape::version_string; + // Name += ")"; + + window->set_title (Name); + } +} + +Inkscape::UI::Widget::Dock* +SPDesktopWidget::getDock() +{ + return _dock; +} + +/** + * Callback to allocate space for desktop widget. + */ +static void +sp_desktop_widget_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget); + GtkAllocation widg_allocation; + gtk_widget_get_allocation(widget, &widg_allocation); + + if ((allocation->x == widg_allocation.x) && + (allocation->y == widg_allocation.y) && + (allocation->width == widg_allocation.width) && + (allocation->height == widg_allocation.height)) { + if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate) + GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation); + return; + } + + if (gtk_widget_get_realized (widget)) { + Geom::Rect const area = dtw->desktop->get_display_area(); + Geom::Rect const d_canvas = dtw->desktop->getCanvas()->getViewbox(); + Geom::Point midpoint = dtw->desktop->w2d(d_canvas.midpoint()); + + double zoom = dtw->desktop->current_zoom(); + + if (GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate) { + GTK_WIDGET_CLASS(dtw_parent_class)->size_allocate (widget, allocation); + } + + if (dtw->get_sticky_zoom_active()) { + /* Find new visible area */ + Geom::Rect newarea = dtw->desktop->get_display_area(); + /* Calculate adjusted zoom */ + double oldshortside = MIN( area.width(), area.height()); + double newshortside = MIN(newarea.width(), newarea.height()); + zoom *= newshortside / oldshortside; + } + dtw->desktop->zoom_absolute_center_point (midpoint, zoom); + + // TODO - Should call show_dialogs() from sp_namedview_window_from_document only. + // But delaying the call to here solves dock sizing issues on OS X, (see #171579) + dtw->desktop->show_dialogs(); + + } else { + if (GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate) { + GTK_WIDGET_CLASS (dtw_parent_class)->size_allocate (widget, allocation); + } +// this->size_allocate (widget, allocation); + } +} + +#ifdef GDK_WINDOWING_QUARTZ +static GtkMenuItem *_get_help_menu(GtkMenuShell *menu) +{ + // Assume "Help" is the last child in menu + GtkMenuItem *last = nullptr; + auto callback = [](GtkWidget *widget, gpointer data) { + *static_cast<GtkMenuItem **>(data) = GTK_MENU_ITEM(widget); + }; + gtk_container_foreach(GTK_CONTAINER(menu), callback, &last); + return last; +} +#endif + +/** + * Callback to realize desktop widget. + */ +static void +sp_desktop_widget_realize (GtkWidget *widget) +{ + + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET (widget); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (GTK_WIDGET_CLASS (dtw_parent_class)->realize) + (* GTK_WIDGET_CLASS (dtw_parent_class)->realize) (widget); + + Geom::Rect d = Geom::Rect::from_xywh(Geom::Point(0,0), (dtw->desktop->doc())->getDimensions()); + + if (d.width() < 1.0 || d.height() < 1.0) return; + + dtw->desktop->set_display_area (d, 10); + + dtw->updateNamedview(); + gchar *gtkThemeName; + gboolean gtkApplicationPreferDarkTheme; + GtkSettings *settings = gtk_settings_get_default(); + Gtk::Window *window = SP_ACTIVE_DESKTOP->getToplevel(); + if (settings && window) { + g_object_get(settings, "gtk-theme-name", >kThemeName, NULL); + g_object_get(settings, "gtk-application-prefer-dark-theme", >kApplicationPreferDarkTheme, NULL); + bool dark = Glib::ustring(gtkThemeName).find(":dark") != std::string::npos; + if (!dark) { + Glib::RefPtr<Gtk::StyleContext> stylecontext = window->get_style_context(); + Gdk::RGBA rgba; + bool background_set = stylecontext->lookup_color("theme_bg_color", rgba); + if (background_set && (0.299 * rgba.get_red() + 0.587 * rgba.get_green() + 0.114 * rgba.get_blue()) < 0.5) { + dark = true; + } + } + if (dark) { + window->get_style_context()->add_class("dark"); + window->get_style_context()->remove_class("bright"); + } else { + window->get_style_context()->add_class("bright"); + window->get_style_context()->remove_class("dark"); + } + if (prefs->getBool("/theme/symbolicIcons", false)) { + window->get_style_context()->add_class("symbolic"); + window->get_style_context()->remove_class("regular"); + } else { + window->get_style_context()->add_class("regular"); + window->get_style_context()->remove_class("symbolic"); + } + INKSCAPE.signal_change_theme.emit(); + } + +#ifdef GDK_WINDOWING_QUARTZ + // native macOS menu + auto osxapp = gtkosx_application_get(); + auto menushell = static_cast<Gtk::MenuShell *>(dtw->menubar()); + if (osxapp && menushell && window) { + menushell->set_parent(*window); + gtkosx_application_set_menu_bar(osxapp, menushell->gobj()); + // using quartz accelerators gives menu shortcuts priority over everything else, + // messes up text input because Inkscape has single key shortcuts (e.g. 1-6). + gtkosx_application_set_use_quartz_accelerators(osxapp, false); + gtkosx_application_set_help_menu(osxapp, _get_help_menu(menushell->gobj())); + + // Window menu disabled because hidden windows which are brought back + // from the menu are non-functional. + // https://gitlab.com/inkscape/inkscape/-/issues/1105 +#if 0 + gtkosx_application_set_window_menu(osxapp, nullptr); +#endif + + // move some items to "Inkscape" menu + unsigned app_menu_verbs[] = { + SP_VERB_NONE, + SP_VERB_DIALOG_INPUT, + SP_VERB_DIALOG_DISPLAY, + SP_VERB_NONE, + SP_VERB_HELP_ABOUT, + }; + for (auto verb : app_menu_verbs) { + GtkWidget *menuitem = nullptr; + if (verb == SP_VERB_NONE) { + menuitem = gtk_separator_menu_item_new(); + } else if (auto item = get_menu_item_for_verb(verb, dtw->desktop)) { + menuitem = static_cast<Gtk::Widget *>(item)->gobj(); + } else { + continue; + } + // Don't use index 0 because it appends the app name. Index 1 + // seems to work perfectly with inserting items in reverse order. + gtkosx_application_insert_app_menu_item(osxapp, menuitem, 1); + } + } +#endif +} + +/* This is just to provide access to common functionality from sp_desktop_widget_realize() above + as well as from SPDesktop::change_document() */ +void SPDesktopWidget::updateNamedview() +{ + // Listen on namedview modification + // originally (prior to the sigc++ conversion) the signal was simply + // connected twice rather than disconnecting the first connection + modified_connection.disconnect(); + + modified_connection = desktop->namedview->connectModified(sigc::mem_fun(*this, &SPDesktopWidget::namedviewModified)); + namedviewModified(desktop->namedview, SP_OBJECT_MODIFIED_FLAG); + + updateTitle( desktop->doc()->getDocumentName() ); +} + +/** + * Callback to handle desktop widget event. + */ +gint +SPDesktopWidget::event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw) +{ + if (event->type == GDK_BUTTON_PRESS) { + // defocus any spinbuttons + gtk_widget_grab_focus (GTK_WIDGET(dtw->_canvas)); + } + + if ((event->type == GDK_BUTTON_PRESS) && (event->button.button == 3)) { + if (event->button.state & GDK_SHIFT_MASK) { + sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), TRUE); + } else { + sp_canvas_arena_set_sticky (SP_CANVAS_ARENA (dtw->desktop->drawing), FALSE); + } + } + + if (GTK_WIDGET_CLASS (dtw_parent_class)->event) { + return (* GTK_WIDGET_CLASS (dtw_parent_class)->event) (widget, event); + } else { + // The key press/release events need to be passed to desktop handler explicitly, + // because otherwise the event contexts only receive key events when the mouse cursor + // is over the canvas. This redirection is only done for key events and only if there's no + // current item on the canvas, because item events and all mouse events are caught + // and passed on by the canvas acetate (I think). --bb + + if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) + && !dtw->_canvas->_current_item) { + return sp_desktop_root_handler (nullptr, event, dtw->desktop); + } + } + + return FALSE; +} + +#if defined(HAVE_LIBLCMS2) +void +SPDesktopWidget::color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget *dtw) +{ + // Handle profile changes + GdkScreen* screen = gtk_widget_get_screen(GTK_WIDGET(dtw)); + GdkWindow *window = gtk_widget_get_window(gtk_widget_get_toplevel(GTK_WIDGET(dtw))); + + // Figure out the ID for the monitor + auto display = gdk_display_get_default(); + auto monitor = gdk_display_get_monitor_at_window(display, window); + + int n_monitors = gdk_display_get_n_monitors(display); + + int monitorNum = -1; + + // Now loop through the set of monitors and figure out whether this monitor matches + for (int i_monitor = 0; i_monitor < n_monitors; ++i_monitor) { + auto monitor_at_index = gdk_display_get_monitor(display, i_monitor); + if(monitor_at_index == monitor) monitorNum = i_monitor; + } + + Glib::ustring id = Inkscape::CMSSystem::getDisplayId( monitorNum ); + bool enabled = false; + dtw->_canvas->_cms_key = id; + dtw->requestCanvasUpdate(); + enabled = !dtw->_canvas->_cms_key.empty(); + dtw->cms_adjust_set_sensitive(enabled); +} +#else // defined(HAVE_LIBLCMS2) +void sp_dtw_color_profile_event(EgeColorProfTracker */*tracker*/, SPDesktopWidget * /*dtw*/) +{ +} +#endif // defined(HAVE_LIBLCMS2) + +void +SPDesktopWidget::update_guides_lock() +{ + bool down = _guides_lock->get_active(); + + auto doc = desktop->getDocument(); + auto nv = desktop->getNamedView(); + auto repr = nv->getRepr(); + + if ( down != nv->lockguides ) { + nv->lockguides = down; + sp_namedview_guides_toggle_lock(doc, nv); + if (down) { + setMessage (Inkscape::NORMAL_MESSAGE, _("Locked all guides")); + } else { + setMessage (Inkscape::NORMAL_MESSAGE, _("Unlocked all guides")); + } + } +} + +#if defined(HAVE_LIBLCMS2) +void +SPDesktopWidget::cms_adjust_toggled( GtkWidget */*button*/, gpointer data ) +{ + SPDesktopWidget *dtw = SP_DESKTOP_WIDGET(data); + + bool down = dtw->_cms_adjust->get_active(); + if ( down != dtw->_canvas->_enable_cms_display_adj ) { + dtw->_canvas->_enable_cms_display_adj = down; + dtw->desktop->redrawDesktop(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/displayprofile/enable", down); + if (down) { + dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>enabled</b> in this window")); + } else { + dtw->setMessage (Inkscape::NORMAL_MESSAGE, _("Color-managed display is <b>disabled</b> in this window")); + } + } +} +#endif // defined(HAVE_LIBLCMS2) + +void +SPDesktopWidget::cms_adjust_set_sensitive(bool enabled) +{ + Inkscape::Verb* verb = Inkscape::Verb::get( SP_VERB_VIEW_CMS_TOGGLE ); + if ( verb ) { + SPAction *act = verb->get_action( Inkscape::ActionContext(viewwidget.view) ); + if ( act ) { + sp_action_set_sensitive( act, enabled ); + } + } + _cms_adjust->set_sensitive(enabled); +} + +void +sp_dtw_desktop_activate (SPDesktopWidget */*dtw*/) +{ + /* update active desktop indicator */ +} + +void +sp_dtw_desktop_deactivate (SPDesktopWidget */*dtw*/) +{ + /* update inactive desktop indicator */ +} + +/** + * Shuts down the desktop object for the view being closed. It checks + * to see if the document has been edited, and if so prompts the user + * to save, discard, or cancel. Returns TRUE if the shutdown operation + * is cancelled or if the save is cancelled or fails, FALSE otherwise. + */ +bool +SPDesktopWidget::shutdown() +{ + g_assert(desktop != nullptr); + + if (INKSCAPE.sole_desktop_for_document(*desktop)) { + SPDocument *doc = desktop->doc(); + if (doc->isModifiedSinceSave()) { + Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + Glib::ustring message = g_markup_printf_escaped( + _("<span weight=\"bold\" size=\"larger\">Save changes to document \"%s\" before closing?</span>\n\n" + "If you close without saving, your changes will be discarded."), + doc->getDocumentName()); + Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE); + dialog.property_destroy_with_parent() = true; + + // fix for bug lp:168809 + Gtk::Container *ma = dialog.get_message_area(); + std::vector<Gtk::Widget*> ma_labels = ma->get_children(); + ma_labels[0]->set_can_focus(false); + + Gtk::Button close_button(_("Close _without saving"), true); + close_button.show(); + dialog.add_action_widget(close_button, Gtk::RESPONSE_NO); + + dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + dialog.add_button(_("_Save"), Gtk::RESPONSE_YES); + dialog.set_default_response(Gtk::RESPONSE_YES); + + gint response = dialog.run(); + + switch (response) { + case GTK_RESPONSE_YES: + { + doc->doRef(); + sp_namedview_document_from_window(desktop); + if (sp_file_save_document(*window, doc)) { + doc->doUnref(); + } else { // save dialog cancelled or save failed + doc->doUnref(); + return TRUE; + } + + break; + } + case GTK_RESPONSE_NO: + break; + default: // cancel pressed, or dialog was closed + return TRUE; + break; + } + } + /* Code to check data loss */ + bool allow_data_loss = FALSE; + while (doc->getReprRoot()->attribute("inkscape:dataloss") != nullptr && allow_data_loss == FALSE) { + Gtk::Window *toplevel_window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + Glib::ustring message = g_markup_printf_escaped( + _("<span weight=\"bold\" size=\"larger\">The file \"%s\" was saved with a format that may cause data loss!</span>\n\n" + "Do you want to save this file as Inkscape SVG?"), + doc->getDocumentName() ? doc->getDocumentName() : "Unnamed"); + Gtk::MessageDialog dialog = Gtk::MessageDialog(*toplevel_window, message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE); + dialog.property_destroy_with_parent() = true; + + // fix for bug lp:168809 + Gtk::Container *ma = dialog.get_message_area(); + std::vector<Gtk::Widget*> ma_labels = ma->get_children(); + ma_labels[0]->set_can_focus(false); + + Gtk::Button close_button(_("Close _without saving"), true); + close_button.show(); + dialog.add_action_widget(close_button, Gtk::RESPONSE_NO); + + dialog.add_button(_("_Cancel"), Gtk::RESPONSE_CANCEL); + + Gtk::Button save_button(_("_Save as Inkscape SVG"), true); + save_button.set_can_default(true); + save_button.show(); + dialog.add_action_widget(save_button, Gtk::RESPONSE_YES); + dialog.set_default_response(Gtk::RESPONSE_YES); + + gint response = dialog.run(); + + switch (response) { + case GTK_RESPONSE_YES: + { + doc->doRef(); + + if (sp_file_save_dialog(*window, doc, Inkscape::Extension::FILE_SAVE_METHOD_INKSCAPE_SVG)) { + doc->doUnref(); + } else { // save dialog cancelled or save failed + doc->doUnref(); + return TRUE; + } + + break; + } + case GTK_RESPONSE_NO: + allow_data_loss = TRUE; + break; + default: // cancel pressed, or dialog was closed + return TRUE; + break; + } + } + } + + /* Save window geometry to prefs for use as a default. + * Use depends on setting of "options.savewindowgeometry". + * But we save the info here regardless of the setting. + */ + storeDesktopPosition(); + + return FALSE; +} + +/** + * \store dessktop position + */ +void SPDesktopWidget::storeDesktopPosition() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool maxed = desktop->is_maximized(); + bool full = desktop->is_fullscreen(); + prefs->setBool("/desktop/geometry/fullscreen", full); + prefs->setBool("/desktop/geometry/maximized", maxed); + gint w, h, x, y; + desktop->getWindowGeometry(x, y, w, h); + // Don't save geom for maximized windows. It + // just tells you the current maximized size, which is not + // as useful as whatever value it had previously. + if (!maxed && !full) { + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } +} + +/** + * \pre this->desktop->main != 0 + */ +void +SPDesktopWidget::requestCanvasUpdate() { + // ^^ also this->desktop != 0 + g_return_if_fail(this->desktop != nullptr); + g_return_if_fail(this->desktop->main != nullptr); + gtk_widget_queue_draw (GTK_WIDGET (SP_CANVAS_ITEM (this->desktop->main)->canvas)); +} + +void +SPDesktopWidget::requestCanvasUpdateAndWait() { + requestCanvasUpdate(); + + while (gtk_events_pending()) + gtk_main_iteration_do(FALSE); + +} + +void +SPDesktopWidget::enableInteraction() +{ + g_return_if_fail(_interaction_disabled_counter > 0); + + _interaction_disabled_counter--; + + if (_interaction_disabled_counter == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(this), TRUE); + } +} + +void +SPDesktopWidget::disableInteraction() +{ + if (_interaction_disabled_counter == 0) { + gtk_widget_set_sensitive(GTK_WIDGET(this), FALSE); + } + + _interaction_disabled_counter++; +} + +void +SPDesktopWidget::setCoordinateStatus(Geom::Point p) +{ + gchar *cstr; + cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::X]); + _coord_status_x->set_markup(cstr); + g_free(cstr); + + cstr = g_strdup_printf("%7.2f", _dt2r * p[Geom::Y]); + _coord_status_y->set_markup(cstr); + g_free(cstr); +} + +void +SPDesktopWidget::letZoomGrabFocus() +{ + if (_zoom_status) _zoom_status->grab_focus(); +} + +void +SPDesktopWidget::getWindowGeometry (gint &x, gint &y, gint &w, gint &h) +{ + if (window) + { + window->get_size (w, h); + window->get_position (x, y); + } +} + +void +SPDesktopWidget::setWindowPosition (Geom::Point p) +{ + if (window) + { + window->move (gint(round(p[Geom::X])), gint(round(p[Geom::Y]))); + } +} + +void +SPDesktopWidget::setWindowSize (gint w, gint h) +{ + if (window) + { + window->set_default_size (w, h); + window->resize (w, h); + } +} + +#ifdef __APPLE__ +/** + * This should be a no-op, but on macOS it raises the given transient window to the + * top of other transient windows from the same parent. + */ +static gboolean transient_focus_in_callback(GtkWindow *window, GdkEvent *, gpointer) +{ + gtk_window_set_transient_for(window, gtk_window_get_transient_for(window)); + return FALSE; +} +#endif + +/** + * \note transientizing does not work on windows; when you minimize a document + * and then open it back, only its transient emerges and you cannot access + * the document window. The document window must be restored by rightclicking + * the taskbar button and pressing "Restore" + */ +void +SPDesktopWidget::setWindowTransient (void *p, int transient_policy) +{ + if (window) + { + GtkWindow *w = GTK_WINDOW(window->gobj()); + +#ifdef __APPLE__ + // Workaround for https://gitlab.gnome.org/GNOME/gtk/issues/2436 + // The first time this window is made transient, connect the focus-in event to + // re-transientize the window in order to raise it above other transient windows. + if (gtk_window_get_transient_for(GTK_WINDOW(p)) == nullptr) { + g_signal_connect(GTK_WIDGET(p), "focus-in-event", G_CALLBACK(transient_focus_in_callback), nullptr); + } +#endif + + gtk_window_set_transient_for (GTK_WINDOW(p), w); + + /* + * This enables "aggressive" transientization, + * i.e. dialogs always emerging on top when you switch documents. Note + * however that this breaks "click to raise" policy of a window + * manager because the switched-to document will be raised at once + * (so that its transients also could raise) + */ + if (transient_policy == 2) + // without this, a transient window not always emerges on top + gtk_window_present (w); + } +} + +void +SPDesktopWidget::presentWindow() +{ + GtkWindow *w =GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this))); + if (w) + gtk_window_present (w); +} + +bool SPDesktopWidget::showInfoDialog( Glib::ustring const &message ) +{ + bool result = false; + Gtk::Window *window = Glib::wrap(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(this)))); + if (window) + { + Gtk::MessageDialog dialog(*window, message, false, Gtk::MESSAGE_INFO, Gtk::BUTTONS_OK); + dialog.property_destroy_with_parent() = true; + dialog.set_name("InfoDialog"); + dialog.set_title(_("Note:")); // probably want to take this as a parameter. + dialog.run(); + } + return result; +} + +bool SPDesktopWidget::warnDialog (Glib::ustring const &text) +{ + Gtk::MessageDialog dialog (*window, text, false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK_CANCEL); + gint response = dialog.run(); + if (response == Gtk::RESPONSE_OK) + return true; + else + return false; +} + +void +SPDesktopWidget::iconify() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_iconified()) { + gtk_window_deiconify(topw); + } else { + gtk_window_iconify(topw); + } + } +} + +void +SPDesktopWidget::maximize() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_maximized()) { + gtk_window_unmaximize(topw); + } else { + // Save geometry to prefs before maximizing so that + // something useful is stored there, because GTK doesn't maintain + // a separate non-maximized size. + if (!desktop->is_iconified() && !desktop->is_fullscreen()) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint w = -1; + gint h, x, y; + getWindowGeometry(x, y, w, h); + g_assert(w != -1); + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } + gtk_window_maximize(topw); + } + } +} + +void +SPDesktopWidget::fullscreen() +{ + GtkWindow *topw = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(_canvas))); + if (GTK_IS_WINDOW(topw)) { + if (desktop->is_fullscreen()) { + gtk_window_unfullscreen(topw); + // widget layout is triggered by the resulting window_state_event + } else { + // Save geometry to prefs before maximizing so that + // something useful is stored there, because GTK doesn't maintain + // a separate non-maximized size. + if (!desktop->is_iconified() && !desktop->is_maximized()) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint w, h, x, y; + getWindowGeometry(x, y, w, h); + prefs->setInt("/desktop/geometry/width", w); + prefs->setInt("/desktop/geometry/height", h); + prefs->setInt("/desktop/geometry/x", x); + prefs->setInt("/desktop/geometry/y", y); + } + gtk_window_fullscreen(topw); + // widget layout is triggered by the resulting window_state_event + } + } +} + +/** + * Hide whatever the user does not want to see in the window + */ +void SPDesktopWidget::layoutWidgets() +{ + SPDesktopWidget *dtw = this; + Glib::ustring pref_root; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (dtw->desktop->is_focusMode()) { + pref_root = "/focus/"; + } else if (dtw->desktop->is_fullscreen()) { + pref_root = "/fullscreen/"; + } else { + pref_root = "/window/"; + } + + if (!prefs->getBool(pref_root + "commands/state", true)) { + gtk_widget_hide (dtw->commands_toolbox); + } else { + gtk_widget_show_all (dtw->commands_toolbox); + } + + if (!prefs->getBool(pref_root + "snaptoolbox/state", true)) { + gtk_widget_hide (dtw->snap_toolbox); + } else { + gtk_widget_show_all (dtw->snap_toolbox); + } + + if (!prefs->getBool(pref_root + "toppanel/state", true)) { + gtk_widget_hide (dtw->aux_toolbox); + } else { + // we cannot just show_all because that will show all tools' panels; + // this is a function from toolbox.cpp that shows only the current tool's panel + ToolboxFactory::showAuxToolbox(dtw->aux_toolbox); + } + + if (!prefs->getBool(pref_root + "toolbox/state", true)) { + gtk_widget_hide (dtw->tool_toolbox); + } else { + gtk_widget_show_all (dtw->tool_toolbox); + } + + if (!prefs->getBool(pref_root + "statusbar/state", true)) { + dtw->_statusbar->hide(); + } else { + dtw->_statusbar->show_all(); + } + + if (!prefs->getBool(pref_root + "panels/state", true)) { + dtw->_panels->hide(); + } else { + dtw->_panels->show_all(); + } + + if (!prefs->getBool(pref_root + "scrollbars/state", true)) { + dtw->_hscrollbar->hide(); + dtw->_vscrollbar_box->hide(); + dtw->_cms_adjust->hide(); + } else { + dtw->_hscrollbar->show_all(); + dtw->_vscrollbar_box->show_all(); + dtw->_cms_adjust->show_all(); + } + + if (!prefs->getBool(pref_root + "rulers/state", true)) { + dtw->_guides_lock->hide(); + dtw->_hruler->hide(); + dtw->_vruler->hide(); + } else { + dtw->_guides_lock->show_all(); + dtw->_hruler->show_all(); + dtw->_vruler->show_all(); + } +} + +Gtk::Toolbar * +SPDesktopWidget::get_toolbar_by_name(const Glib::ustring& name) +{ + // The name is actually attached to the GtkGrid that contains + // the toolbar, so we need to get the grid first + auto widget = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), name); + auto grid = dynamic_cast<Gtk::Grid*>(widget); + + if (!grid) return nullptr; + + auto child = grid->get_child_at(0,0); + auto tb = dynamic_cast<Gtk::Toolbar*>(child); + + return tb; +} + +void +SPDesktopWidget::setToolboxFocusTo (const gchar* label) +{ + // First try looking for a named widget + auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), label); + + // Fallback to looking for a named data member (deprecated) + if (!hb) { + hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer) label))); + } + + if (hb) + { + hb->grab_focus(); + } +} + +void +SPDesktopWidget::setToolboxAdjustmentValue (gchar const *id, double value) +{ + // First try looking for a named widget + auto hb = sp_search_by_name_recursive(Glib::wrap(aux_toolbox), id); + + // Fallback to looking for a named data member (deprecated) + if (!hb) { + hb = Glib::wrap(GTK_WIDGET(sp_search_by_data_recursive(aux_toolbox, (gpointer)id))); + } + + if (hb) { + auto sb = dynamic_cast<Inkscape::UI::Widget::SpinButtonToolItem *>(hb); + auto a = sb->get_adjustment(); + + if(a) a->set_value(value); + } + + else g_warning ("Could not find GtkAdjustment for %s\n", id); +} + + +bool +SPDesktopWidget::isToolboxButtonActive (const gchar* id) +{ + bool isActive = false; + gpointer thing = sp_search_by_data_recursive(aux_toolbox, (gpointer) id); + if ( !thing ) { + //g_message( "Unable to locate item for {%s}", id ); + } else if ( GTK_IS_TOGGLE_BUTTON(thing) ) { + GtkToggleButton *b = GTK_TOGGLE_BUTTON(thing); + isActive = gtk_toggle_button_get_active( b ) != 0; + } else if ( GTK_IS_TOGGLE_ACTION(thing) ) { + GtkToggleAction* act = GTK_TOGGLE_ACTION(thing); + isActive = gtk_toggle_action_get_active( act ) != 0; + } else if ( GTK_IS_TOGGLE_TOOL_BUTTON(thing) ) { + GtkToggleToolButton *b = GTK_TOGGLE_TOOL_BUTTON(thing); + isActive = gtk_toggle_tool_button_get_active( b ) != 0; + } else { + //g_message( "Item for {%s} is of an unsupported type", id ); + } + + return isActive; +} + +void SPDesktopWidget::setToolboxPosition(Glib::ustring const& id, GtkPositionType pos) +{ + // Note - later on these won't be individual member variables. + GtkWidget* toolbox = nullptr; + if (id == "ToolToolbar") { + toolbox = tool_toolbox; + } else if (id == "AuxToolbar") { + toolbox = aux_toolbox; + } else if (id == "CommandsToolbar") { + toolbox = commands_toolbox; + } else if (id == "SnapToolbar") { + toolbox = snap_toolbox; + } + + + if (toolbox) { + switch(pos) { + case GTK_POS_TOP: + case GTK_POS_BOTTOM: + if ( gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) { + // Removing a widget can reduce ref count to zero + g_object_ref(G_OBJECT(toolbox)); + _hbox->remove(*Glib::wrap(toolbox)); + _vbox->add(*Glib::wrap(toolbox)); + g_object_unref(G_OBJECT(toolbox)); + + // Function doesn't seem to be in Gtkmm wrapper yet + gtk_box_set_child_packing(_vbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START); + } + ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_HORIZONTAL); + break; + case GTK_POS_LEFT: + case GTK_POS_RIGHT: + if ( !gtk_widget_is_ancestor(toolbox, GTK_WIDGET(_hbox->gobj())) ) { + g_object_ref(G_OBJECT(toolbox)); + _vbox->remove(*Glib::wrap(toolbox)); + _hbox->add(*Glib::wrap(toolbox)); + g_object_unref(G_OBJECT(toolbox)); + + // Function doesn't seem to be in Gtkmm wrapper yet + gtk_box_set_child_packing(_hbox->gobj(), toolbox, FALSE, TRUE, 0, GTK_PACK_START); + if (pos == GTK_POS_LEFT) { + _hbox->reorder_child(*Glib::wrap(toolbox), 0 ); + } + } + ToolboxFactory::setOrientation(toolbox, GTK_ORIENTATION_VERTICAL); + break; + } + } +} + + +SPDesktopWidget *sp_desktop_widget_new(SPDocument *document) +{ + SPDesktopWidget* dtw = SPDesktopWidget::createInstance(document); + return dtw; +} + +SPDesktopWidget* SPDesktopWidget::createInstance(SPDocument *document) +{ + SPDesktopWidget *dtw = static_cast<SPDesktopWidget*>(g_object_new(SP_TYPE_DESKTOP_WIDGET, nullptr)); + + SPNamedView *namedview = sp_document_namedview(document, nullptr); + + dtw->_dt2r = 1. / namedview->display_units->factor; + + dtw->_ruler_origin = Geom::Point(0,0); //namedview->gridorigin; Why was the grid origin used here? + + dtw->desktop = new SPDesktop(); + dtw->stub = new SPDesktopWidget::WidgetStub (dtw); + dtw->desktop->init (namedview, dtw->_canvas, dtw->stub); + INKSCAPE.add_desktop (dtw->desktop); + + // Add the shape geometry to libavoid for autorouting connectors. + // This needs desktop set for its spacing preferences. + init_avoided_shape_geometry(dtw->desktop); + + dtw->_selected_style->setDesktop(dtw->desktop); + + /* Once desktop is set, we can update rulers */ + dtw->update_rulers(); + + sp_view_widget_set_view (SP_VIEW_WIDGET (dtw), dtw->desktop); + + /* Listen on namedview modification */ + dtw->modified_connection = namedview->connectModified(sigc::mem_fun(*dtw, &SPDesktopWidget::namedviewModified)); + + dtw->layer_selector->setDesktop(dtw->desktop); + + // TEMP + dtw->_menubar = build_menubar(dtw->desktop); + dtw->_menubar->set_name("MenuBar"); + dtw->_menubar->show_all(); + +#ifdef GDK_WINDOWING_QUARTZ + // native macOS menu: do this later because we don't have the window handle yet +#else + dtw->_vbox->pack_start(*dtw->_menubar, false, false); +#endif + + dtw->layoutWidgets(); + + std::vector<GtkWidget *> toolboxes; + toolboxes.push_back(dtw->tool_toolbox); + toolboxes.push_back(dtw->aux_toolbox); + toolboxes.push_back(dtw->commands_toolbox); + toolboxes.push_back(dtw->snap_toolbox); + + dtw->_panels->setDesktop( dtw->desktop ); + + UXManager::getInstance()->addTrack(dtw); + UXManager::getInstance()->connectToDesktop( toolboxes, dtw->desktop ); + + return dtw; +} + + +void +SPDesktopWidget::update_rulers() +{ + Geom::Rect viewbox = desktop->get_display_area(true); + // "true" means: Use integer values of the canvas for calculating the display area, similar + // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(), + // where ix and iy are rounded integer values; these values are stored in SPCanvasBuf->rect, + // and used for drawing the grid). By using the integer values here too, the ruler ticks + // will be perfectly aligned to the grid + + double lower_x = _dt2r * (viewbox.left() - _ruler_origin[Geom::X]); + double upper_x = _dt2r * (viewbox.right() - _ruler_origin[Geom::X]); + _hruler->set_range(lower_x, upper_x); + + double lower_y = _dt2r * (viewbox.bottom() - _ruler_origin[Geom::Y]); + double upper_y = _dt2r * (viewbox.top() - _ruler_origin[Geom::Y]); + if (desktop->is_yaxisdown()) { + std::swap(lower_y, upper_y); + } + _vruler->set_range(lower_y, upper_y); +} + + +void SPDesktopWidget::namedviewModified(SPObject *obj, guint flags) +{ + SPNamedView *nv=SP_NAMEDVIEW(obj); + + if (flags & SP_OBJECT_MODIFIED_FLAG) { + _dt2r = 1. / nv->display_units->factor; + _ruler_origin = Geom::Point(0,0); //nv->gridorigin; Why was the grid origin used here? + + _vruler->set_unit(nv->getDisplayUnit()); + _hruler->set_unit(nv->getDisplayUnit()); + + /* This loops through all the grandchildren of aux toolbox, + * and for each that it finds, it performs an sp_search_by_data_recursive(), + * looking for widgets that hold some "tracker" data (this is used by + * all toolboxes to refer to the unit selector). The default document units + * is then selected within these unit selectors. + * + * Of course it would be nice to be able to refer to the toolbox and the + * unit selector directly by name, but I don't yet see a way to do that. + * + * This should solve: https://bugs.launchpad.net/inkscape/+bug/362995 + */ + if (GTK_IS_CONTAINER(aux_toolbox)) { + std::vector<Gtk::Widget*> ch = Glib::wrap(GTK_CONTAINER(aux_toolbox))->get_children(); + for (auto i:ch) { + if (GTK_IS_CONTAINER(i->gobj())) { + std::vector<Gtk::Widget*> grch = dynamic_cast<Gtk::Container*>(i)->get_children(); + for (auto j:grch) { + + if (!GTK_IS_WIDGET(j->gobj())) // wasn't a widget + continue; + + // Don't apply to text toolbar. We want to be able to + // use different units for text. (Bug 1562217) + const Glib::ustring name = j->get_name(); + if ( name == "TextToolbar" || name == "MeasureToolbar") + continue; + + gpointer t = sp_search_by_data_recursive(GTK_WIDGET(j->gobj()), (gpointer) "unit-tracker"); + if (t == nullptr) // didn't find any tracker data + continue; + + UnitTracker *tracker = reinterpret_cast<UnitTracker*>( t ); + if (tracker == nullptr) // it's null when inkscape is first opened + continue; + + tracker->setActiveUnit( nv->display_units ); + } // grandchildren + } // if child is a container + } // children + } // if aux_toolbox is a container + + _hruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + _vruler_box->set_tooltip_text(gettext(nv->display_units->name_plural.c_str())); + + update_rulers(); + ToolboxFactory::updateSnapToolbox(this->desktop, nullptr, this->snap_toolbox); + } +} + +void +SPDesktopWidget::on_adjustment_value_changed() +{ + if (update) + return; + + update = 1; + + // Do not call canvas->scrollTo directly... messes up 'offset'. + desktop->scroll_absolute( Geom::Point(_hadj->get_value(), + _vadj->get_value()), false); + + update = 0; +} + +/* we make the desktop window with focus active, signal is connected in interface.c */ +bool SPDesktopWidget::onFocusInEvent(GdkEventFocus*) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/bitmapautoreload/value", true)) { + std::vector<SPObject *> imageList = (desktop->doc())->getResourceList("image"); + for (auto it : imageList) { + SPImage* image = SP_IMAGE(it); + image->refresh_if_outdated(); + } + } + + INKSCAPE.activate_desktop (desktop); + + return false; +} + +// ------------------------ Zoom ------------------------ +static gdouble +sp_dtw_zoom_value_to_display (gdouble value) +{ + return floor (10 * (pow (2, value) * 100.0 + 0.05)) / 10; +} + +static gdouble +sp_dtw_zoom_display_to_value (gdouble value) +{ + return log (value / 100.0) / log (2); +} + +int +SPDesktopWidget::zoom_input(double *new_val) +{ + gchar *b = g_strdup(_zoom_status->get_text().c_str()); + + gchar *comma = g_strstr_len (b, -1, ","); + if (comma) { + *comma = '.'; + } + + char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + gdouble new_typed = atof (b); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + g_free (b); + + *new_val = sp_dtw_zoom_display_to_value (new_typed); + return TRUE; +} + +bool +SPDesktopWidget::zoom_output() +{ + gchar b[64]; + double val = sp_dtw_zoom_value_to_display (_zoom_status->get_value()); + if (val < 10) { + g_snprintf (b, 64, "%4.1f%%", val); + } else { + g_snprintf (b, 64, "%4.0f%%", val); + } + _zoom_status->set_text(b); + return true; +} + +void +SPDesktopWidget::zoom_value_changed() +{ + double const zoom_factor = pow (2, _zoom_status->get_value()); + + // Zoom around center of window + Geom::Rect const d_canvas = desktop->getCanvas()->getViewbox(); + Geom::Point midpoint = desktop->w2d(d_canvas.midpoint()); + _zoom_status_value_changed_connection.block(); + desktop->zoom_absolute_center_point (midpoint, zoom_factor); + _zoom_status_value_changed_connection.unblock(); + + spinbutton_defocus(GTK_WIDGET(_zoom_status->gobj())); +} + +void +SPDesktopWidget::zoom_menu_handler(double factor) +{ + Geom::Rect const d = desktop->get_display_area(); + desktop->zoom_absolute_center_point (d.midpoint(), factor); +} + +void +SPDesktopWidget::zoom_populate_popup(Gtk::Menu *menu) +{ + for ( auto iter : menu->get_children()) { + menu->remove(*iter); + } + + auto item_1000 = Gtk::manage(new Gtk::MenuItem("1000%")); + auto item_500 = Gtk::manage(new Gtk::MenuItem("500%")); + auto item_200 = Gtk::manage(new Gtk::MenuItem("200%")); + auto item_100 = Gtk::manage(new Gtk::MenuItem("100%")); + auto item_50 = Gtk::manage(new Gtk::MenuItem( "50%")); + auto item_25 = Gtk::manage(new Gtk::MenuItem( "25%")); + auto item_10 = Gtk::manage(new Gtk::MenuItem( "10%")); + + item_1000->signal_activate().connect(sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 10.00)); + item_500->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 5.00)); + item_200->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 2.00)); + item_100->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 1.00)); + item_50->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.50)); + item_25->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.25)); + item_10->signal_activate().connect( sigc::bind(sigc::mem_fun(this, &SPDesktopWidget::zoom_menu_handler), 0.10)); + + menu->append(*item_1000); + menu->append(*item_500); + menu->append(*item_200); + menu->append(*item_100); + menu->append(*item_50); + menu->append(*item_25); + menu->append(*item_10); + + auto sep = Gtk::manage(new Gtk::SeparatorMenuItem()); + menu->append(*sep); + + auto item_page = Gtk::manage(new Gtk::MenuItem(_("Page"))); + item_page->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_page)); + menu->append(*item_page); + + auto item_drawing = Gtk::manage(new Gtk::MenuItem(_("Drawing"))); + item_drawing->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_drawing)); + menu->append(*item_drawing); + + auto item_selection = Gtk::manage(new Gtk::MenuItem(_("Selection"))); + item_selection->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_selection)); + menu->append(*item_selection); + + auto item_center_page = Gtk::manage(new Gtk::MenuItem(_("Centre Page"))); + item_center_page->signal_activate().connect(sigc::mem_fun(desktop, &SPDesktop::zoom_center_page)); + menu->append(*item_center_page); + + menu->show_all(); +} + + +void +SPDesktopWidget::sticky_zoom_toggled() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/stickyzoom/value", _sticky_zoom->get_active()); +} + + +void +SPDesktopWidget::update_zoom() +{ + _zoom_status_value_changed_connection.block(); + _zoom_status->set_value(log(desktop->current_zoom()) / log(2)); + _zoom_status->queue_draw(); + _zoom_status_value_changed_connection.unblock(); +} + + +// ---------------------- Rotation ------------------------ +int +SPDesktopWidget::rotation_input(double *new_val) +{ + auto *b = g_strdup(_rotation_status->get_text().c_str()); + + gchar *comma = g_strstr_len (b, -1, ","); + if (comma) { + *comma = '.'; + } + + char *oldlocale = g_strdup (setlocale(LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + gdouble new_value = atof (b); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + g_free (b); + + *new_val = new_value; + return true; +} + +bool +SPDesktopWidget::rotation_output() +{ + gchar b[64]; + double val = _rotation_status->get_value(); + + if (val < -180) val += 360; + if (val > 180) val -= 360; + + g_snprintf (b, 64, "%7.2f°", val); + + _rotation_status->set_text(b); + return true; +} + +void +SPDesktopWidget::rotation_value_changed() +{ + double const rotate_factor = M_PI / 180.0 * _rotation_status->get_value(); + // std::cout << "SPDesktopWidget::rotation_value_changed: " + // << _rotation_status->get_value() + // << " (" << rotate_factor << ")" <<std::endl; + + // Rotate around center of window + Geom::Rect const d_canvas = desktop->getCanvas()->getViewbox(); + _rotation_status_value_changed_connection.block(); + Geom::Point midpoint = desktop->w2d(d_canvas.midpoint()); + desktop->rotate_absolute_center_point (midpoint, rotate_factor); + _rotation_status_value_changed_connection.unblock(); + + spinbutton_defocus(GTK_WIDGET(_rotation_status->gobj())); +} + +void +SPDesktopWidget::rotation_populate_popup(Gtk::Menu *menu) +{ + for ( auto iter : menu->get_children()) { + menu->remove(*iter); + } + + auto item_m135 = Gtk::manage(new Gtk::MenuItem("-135°")); + auto item_m90 = Gtk::manage(new Gtk::MenuItem( "-90°")); + auto item_m45 = Gtk::manage(new Gtk::MenuItem( "-45°")); + auto item_0 = Gtk::manage(new Gtk::MenuItem( "0°")); + auto item_p45 = Gtk::manage(new Gtk::MenuItem( "45°")); + auto item_p90 = Gtk::manage(new Gtk::MenuItem( "90°")); + auto item_p135 = Gtk::manage(new Gtk::MenuItem( "135°")); + auto item_p180 = Gtk::manage(new Gtk::MenuItem( "180°")); + + item_m135->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -135)); + item_m90->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -90)); + item_m45->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), -45)); + item_0->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 0)); + item_p45->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 45)); + item_p90->signal_activate().connect( sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 90)); + item_p135->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 135)); + item_p180->signal_activate().connect(sigc::bind(sigc::mem_fun(_rotation_status, &Gtk::SpinButton::set_value), 180)); + + menu->append(*item_m135); + menu->append(*item_m90); + menu->append(*item_m45); + menu->append(*item_0); + menu->append(*item_p45); + menu->append(*item_p90); + menu->append(*item_p135); + menu->append(*item_p180); + + menu->show_all(); +} + + +void +SPDesktopWidget::update_rotation() +{ + _rotation_status_value_changed_connection.block(); + _rotation_status->set_value(desktop->current_rotation() / M_PI * 180.0); + _rotation_status->queue_draw(); + _rotation_status_value_changed_connection.unblock(); + +} + + +// --------------- Rulers/Scrollbars/Etc. ----------------- +void +SPDesktopWidget::toggle_rulers() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_guides_lock->get_visible()) { + _guides_lock->hide(); + _hruler->hide(); + _vruler->hide(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", false); + } else { + _guides_lock->show_all(); + _hruler->show_all(); + _vruler->show_all(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/rulers/state" : "/window/rulers/state", true); + } +} + +void +SPDesktopWidget::toggle_scrollbars() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (_hscrollbar->get_visible()) { + _hscrollbar->hide(); + _vscrollbar_box->hide(); + _cms_adjust->hide(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", false); + } else { + _hscrollbar->show_all(); + _vscrollbar_box->show_all(); + _cms_adjust->show_all(); + prefs->setBool(desktop->is_fullscreen() ? "/fullscreen/scrollbars/state" : "/window/scrollbars/state", true); + } +} + +bool +SPDesktopWidget::get_color_prof_adj_enabled() const +{ + return _cms_adjust->get_sensitive() && _cms_adjust->get_active(); +} + +void +SPDesktopWidget::toggle_color_prof_adj() +{ + if (_cms_adjust->get_sensitive()) { + if (_cms_adjust->get_active()) { + _cms_adjust->toggle_set_down(false); + } else { + _cms_adjust->toggle_set_down(true); + } + } +} + +static void +set_adjustment (Glib::RefPtr<Gtk::Adjustment> &adj, double l, double u, double ps, double si, double pi) +{ + if ((l != adj->get_lower()) || + (u != adj->get_upper()) || + (ps != adj->get_page_size()) || + (si != adj->get_step_increment()) || + (pi != adj->get_page_increment())) { + adj->set_lower(l); + adj->set_upper(u); + adj->set_page_size(ps); + adj->set_step_increment(si); + adj->set_page_increment(pi); + } +} + +void +SPDesktopWidget::update_scrollbars(double scale) +{ + if (update) return; + update = 1; + + /* The desktop region we always show unconditionally */ + SPDocument *doc = desktop->doc(); + Geom::Rect darea ( Geom::Point(-doc->getWidth().value("px"), -doc->getHeight().value("px")), + Geom::Point(2 * doc->getWidth().value("px"), 2 * doc->getHeight().value("px")) ); + + Geom::OptRect deskarea; + if (Inkscape::Preferences::get()->getInt("/tools/bounding_box") == 0) { + deskarea = darea | doc->getRoot()->desktopVisualBounds(); + } else { + deskarea = darea | doc->getRoot()->desktopGeometricBounds(); + } + + /* Canvas region we always show unconditionally */ + double const y_dir = desktop->yaxisdir(); + Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), + Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); + + Geom::Rect viewbox = _canvas->getViewbox(); + + /* Viewbox is always included into scrollable region */ + carea = Geom::unify(carea, viewbox); + + set_adjustment(_hadj, carea.min()[Geom::X], carea.max()[Geom::X], + viewbox.dimensions()[Geom::X], + 0.1 * viewbox.dimensions()[Geom::X], + viewbox.dimensions()[Geom::X]); + _hadj->set_value(viewbox.min()[Geom::X]); + + set_adjustment(_vadj, carea.min()[Geom::Y], carea.max()[Geom::Y], + viewbox.dimensions()[Geom::Y], + 0.1 * viewbox.dimensions()[Geom::Y], + viewbox.dimensions()[Geom::Y]); + _vadj->set_value(viewbox.min()[Geom::Y]); + + update = 0; +} + +bool +SPDesktopWidget::get_sticky_zoom_active() const +{ + return _sticky_zoom->get_active(); +} + +double +SPDesktopWidget::get_hruler_thickness() const +{ + auto allocation = _hruler->get_allocation(); + return allocation.get_height(); +} + +double +SPDesktopWidget::get_vruler_thickness() const +{ + auto allocation = _vruler->get_allocation(); + return allocation.get_width(); +} + +gint +SPDesktopWidget::ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz) +{ + switch (event->type) { + case GDK_BUTTON_PRESS: + dtw->on_ruler_box_button_press_event(&event->button, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + case GDK_MOTION_NOTIFY: + dtw->on_ruler_box_motion_notify_event(&event->motion, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + case GDK_BUTTON_RELEASE: + dtw->on_ruler_box_button_release_event(&event->button, Glib::wrap(GTK_EVENT_BOX(widget)), horiz); + break; + default: + break; + } + + return FALSE; +} + +bool +SPDesktopWidget::on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz) +{ + if (horiz) { + sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_HRULER); + } + else { + sp_event_context_snap_delay_handler(desktop->event_context, (gpointer) widget->gobj(), (gpointer) this, event, Inkscape::UI::Tools::DelayedSnapEvent::GUIDE_VRULER); + } + + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (_ruler_clicked) { + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point event_dt(desktop->w2d(event_w)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint tolerance = prefs->getIntLimited("/options/dragtolerance/value", 0, 0, 100); + if ( ( abs( (gint) event->x - _xp ) < tolerance ) + && ( abs( (gint) event->y - _yp ) < tolerance ) ) { + return false; + } + + _ruler_dragged = true; + + // explicitly show guidelines; if I draw a guide, I want them on + if ((horiz ? wy : wx) >= 0) { + desktop->namedview->setGuides(true); + } + + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal); + } + sp_guideline_set_normal(SP_GUIDELINE(_active_guide), _normal); + sp_guideline_set_position(SP_GUIDELINE(_active_guide), event_dt); + + desktop->set_coordinate_status(event_dt); + } + + return false; +} + +bool +SPDesktopWidget::on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz) +{ + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (_ruler_clicked && event->button == 1) { + sp_event_context_discard_delayed_snap_event(desktop->event_context); + + auto seat = gdk_device_get_seat(event->device); + gdk_seat_ungrab(seat); + + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point event_dt(desktop->w2d(event_w)); + + if (!(event->state & GDK_SHIFT_MASK)) { + ruler_snap_new_guide(desktop, _active_guide, event_dt, _normal); + } + + sp_canvas_item_destroy(_active_guide); + _active_guide = nullptr; + if ((horiz ? wy : wx) >= 0) { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("sodipodi:guide"); + + // If root viewBox set, interpret guides in terms of viewBox (90/96) + double newx = event_dt.x(); + double newy = event_dt.y(); + + // <sodipodi:guide> stores inverted y-axis coordinates + if (desktop->is_yaxisdown()) { + newy = desktop->doc()->getHeight().value("px") - newy; + _normal[Geom::Y] *= -1.0; + } + + SPRoot *root = desktop->doc()->getRoot(); + if( root->viewBox_set ) { + newx = newx * root->viewBox.width() / root->width.computed; + newy = newy * root->viewBox.height() / root->height.computed; + } + sp_repr_set_point(repr, "position", Geom::Point( newx, newy )); + sp_repr_set_point(repr, "orientation", _normal); + desktop->namedview->appendChild(repr); + Inkscape::GC::release(repr); + DocumentUndo::done(desktop->getDocument(), SP_VERB_NONE, + _("Create guide")); + } + desktop->set_coordinate_status(event_dt); + + if (!_ruler_dragged) { + // Ruler click (without drag) toggle the guide visibility on and off + Inkscape::XML::Node *repr = desktop->namedview->getRepr(); + sp_namedview_toggle_guides(desktop->getDocument(), desktop->namedview); + } + + _ruler_clicked = false; + _ruler_dragged = false; + } + + return false; +} + +bool +SPDesktopWidget::on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz) +{ + if (_ruler_clicked) // event triggerred on a double click: do no process the click + return false; + + int wx, wy; + + GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(_canvas)); + + gint width, height; + + gdk_window_get_device_position(window, event->device, &wx, &wy, nullptr); + gdk_window_get_geometry(window, nullptr /*x*/, nullptr /*y*/, &width, &height); + + Geom::Point const event_win(wx, wy); + + if (event->button == 1) { + _ruler_clicked = true; + _ruler_dragged = false; + // save click origin + _xp = (gint) event->x; + _yp = (gint) event->y; + + Geom::Point const event_w(sp_canvas_window_to_world(_canvas, event_win)); + Geom::Point const event_dt(desktop->w2d(event_w)); + + // calculate the normal of the guidelines when dragged from the edges of rulers. + auto const y_dir = desktop->yaxisdir(); + Geom::Point normal_bl_to_tr(1., y_dir); //bottomleft to topright + Geom::Point normal_tr_to_bl(-1., y_dir); //topright to bottomleft + normal_bl_to_tr.normalize(); + normal_tr_to_bl.normalize(); + Inkscape::CanvasGrid * grid = sp_namedview_get_first_enabled_grid(desktop->namedview); + if (grid){ + if (grid->getGridType() == Inkscape::GRID_AXONOMETRIC ) { + Inkscape::CanvasAxonomGrid *axonomgrid = dynamic_cast<Inkscape::CanvasAxonomGrid *>(grid); + if (event->state & GDK_CONTROL_MASK) { + // guidelines normal to gridlines + normal_bl_to_tr = Geom::Point::polar(-axonomgrid->angle_rad[0], 1.0); + normal_tr_to_bl = Geom::Point::polar(axonomgrid->angle_rad[2], 1.0); + } else { + normal_bl_to_tr = rot90(Geom::Point::polar(axonomgrid->angle_rad[2], 1.0)); + normal_tr_to_bl = rot90(Geom::Point::polar(-axonomgrid->angle_rad[0], 1.0)); + } + } + } + if (horiz) { + if (wx < 50) { + _normal = normal_bl_to_tr; + } else if (wx > width - 50) { + _normal = normal_tr_to_bl; + } else { + _normal = Geom::Point(0.,1.); + } + } else { + if (wy < 50) { + _normal = normal_bl_to_tr; + } else if (wy > height - 50) { + _normal = normal_tr_to_bl; + } else { + _normal = Geom::Point(1.,0.); + } + } + + _active_guide = sp_guideline_new(desktop->guides, nullptr, event_dt, _normal); + sp_guideline_set_color(SP_GUIDELINE(_active_guide), desktop->namedview->guidehicolor); + + auto window = widget->get_window()->gobj(); + + auto seat = gdk_device_get_seat(event->device); + gdk_seat_grab(seat, + window, + GDK_SEAT_CAPABILITY_ALL_POINTING, + FALSE, + nullptr, + (GdkEvent*)event, + nullptr, + nullptr); + } + + return false; +} + +GtkAllocation +SPDesktopWidget::get_canvas_allocation() const +{ + GtkAllocation allocation; + gtk_widget_get_allocation(GTK_WIDGET(_canvas), &allocation); + return allocation; +} + +void +SPDesktopWidget::ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem * /*guide*/, Geom::Point &event_dt, Geom::Point &normal) +{ + SnapManager &m = desktop->namedview->snap_manager; + m.setup(desktop); + // We're dragging a brand new guide, just pulled of the rulers seconds ago. When snapping to a + // path this guide will change it slope to become either tangential or perpendicular to that path. It's + // therefore not useful to try tangential or perpendicular snapping, so this will be disabled temporarily + bool pref_perp = m.snapprefs.getSnapPerp(); + bool pref_tang = m.snapprefs.getSnapTang(); + m.snapprefs.setSnapPerp(false); + m.snapprefs.setSnapTang(false); + // We only have a temporary guide which is not stored in our document yet. + // Because the guide snapper only looks in the document for guides to snap to, + // we don't have to worry about a guide snapping to itself here + Geom::Point normal_orig = normal; + m.guideFreeSnap(event_dt, normal, false, false); + // After snapping, both event_dt and normal have been modified accordingly; we'll take the normal (of the + // curve we snapped to) to set the normal the guide. And rotate it by 90 deg. if needed + if (pref_perp) { // Perpendicular snapping to paths is requested by the user, so let's do that + if (normal != normal_orig) { + normal = Geom::rot90(normal); + } + } + if (!(pref_tang || pref_perp)) { // if we don't want to snap either perpendicularly or tangentially, then + normal = normal_orig; // we must restore the normal to it's original state + } + // Restore the preferences + m.snapprefs.setSnapPerp(pref_perp); + m.snapprefs.setSnapTang(pref_tang); + m.unSetup(); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/widgets/desktop-widget.h b/src/widgets/desktop-widget.h new file mode 100644 index 0000000..e61cdd5 --- /dev/null +++ b/src/widgets/desktop-widget.h @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_DESKTOP_WIDGET_H +#define SEEN_SP_DESKTOP_WIDGET_H + +/** \file + * SPDesktopWidget: handling Gtk events on a desktop. + * + * Authors: + * Jon A. Cruz <jon@joncruz.org> (c) 2010 + * John Bintz <jcoswell@coswellproductions.org> (c) 2006 + * Ralf Stephan <ralf@ark.in-berlin.de> (c) 2005 + * Abhishek Sharma + * ? -2004 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <gtkmm.h> + +#include "message.h" +#include "ui/view/view-widget.h" +#include "ui/view/edit-widget-interface.h" + +#include <cstddef> +#include <sigc++/connection.h> +#include <2geom/point.h> + +// forward declaration +typedef struct _EgeColorProfTracker EgeColorProfTracker; +struct SPCanvas; +struct SPCanvasItem; +class SPDocument; +class SPDesktop; +struct SPDesktopWidget; +class SPObject; + +namespace Inkscape { +namespace UI { +namespace Dialog { +class SwatchesPanel; +} // namespace Dialog + +namespace Widget { +class Button; +class LayerSelector; +class SelectedStyle; +class Ruler; +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +#define SP_TYPE_DESKTOP_WIDGET SPDesktopWidget::getType() +#define SP_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidget)) +#define SP_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_DESKTOP_WIDGET, SPDesktopWidgetClass)) +#define SP_IS_DESKTOP_WIDGET(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_DESKTOP_WIDGET)) +#define SP_IS_DESKTOP_WIDGET_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_DESKTOP_WIDGET)) + +/** + * Create a new SPDesktopWidget + */ +SPDesktopWidget *sp_desktop_widget_new(SPDocument* document); + +void sp_desktop_widget_show_decorations(SPDesktopWidget *dtw, gboolean show); +void sp_desktop_widget_update_hruler (SPDesktopWidget *dtw); +void sp_desktop_widget_update_vruler (SPDesktopWidget *dtw); + +/* Show/hide rulers & scrollbars */ +void sp_desktop_widget_update_scrollbars (SPDesktopWidget *dtw, double scale); + +void sp_dtw_desktop_activate (SPDesktopWidget *dtw); +void sp_dtw_desktop_deactivate (SPDesktopWidget *dtw); + +/// A GtkEventBox on an SPDesktop. +struct SPDesktopWidget { + SPViewWidget viewwidget; + + unsigned int update : 1; + + sigc::connection modified_connection; + + SPDesktop *desktop; + + Gtk::Window *window; + + static void dispose(GObject *object); + +private: + // Flags for ruler event handling + bool _ruler_clicked; ///< True if the ruler has been clicked + bool _ruler_dragged; ///< True if a drag on the ruler is occurring + + SPCanvasItem *_active_guide; ///< The guide currently being handled during a ruler event + Geom::Point _normal; ///< Normal to the guide currently being handled during ruler event + int _xp; ///< x coordinate for start of drag + int _yp; ///< y coordinate for start of drag + + // The root vbox of the window layout. + Gtk::Box *_vbox; + + Gtk::Box *_hbox; + + Gtk::MenuBar *_menubar; // TEMP + Gtk::Box *_statusbar; + + Inkscape::UI::Dialog::SwatchesPanel *_panels; + + Glib::RefPtr<Gtk::Adjustment> _hadj; + Glib::RefPtr<Gtk::Adjustment> _vadj; + + Gtk::ToggleButton *_guides_lock; + + Inkscape::UI::Widget::Button *_cms_adjust; + Gtk::ToggleButton *_sticky_zoom; + Gtk::Grid *_coord_status; + + Gtk::Label *_coord_status_x; + Gtk::Label *_coord_status_y; + Gtk::SpinButton *_zoom_status; + sigc::connection _zoom_status_input_connection; + sigc::connection _zoom_status_output_connection; + sigc::connection _zoom_status_value_changed_connection; + sigc::connection _zoom_status_populate_popup_connection; + Gtk::Label *_select_status; + Gtk::SpinButton *_rotation_status; + + sigc::connection _rotation_status_input_connection; + sigc::connection _rotation_status_output_connection; + sigc::connection _rotation_status_value_changed_connection; + sigc::connection _rotation_status_populate_popup_connection; + + Inkscape::UI::Widget::Dock *_dock; + + Gtk::Scrollbar *_hscrollbar; + Gtk::Scrollbar *_vscrollbar; + Gtk::Box *_vscrollbar_box; + + Inkscape::UI::Widget::SelectedStyle *_selected_style; + + /** A table for displaying the canvas, rulers etc */ + Gtk::Grid *_canvas_tbl; + sigc::connection _canvas_tbl_size_allocate_connection; + + Gtk::EventBox *_hruler_box; + Gtk::EventBox *_vruler_box; // eventboxes for setting tooltips + + /* Rulers */ + Inkscape::UI::Widget::Ruler *_hruler; + Inkscape::UI::Widget::Ruler *_vruler; + Gtk::Allocation _allocation; + + unsigned int _interaction_disabled_counter; + + Geom::Point _ruler_origin; + double _dt2r; + + SPCanvas *_canvas; + +public: + Inkscape::UI::Widget::LayerSelector *layer_selector; + + EgeColorProfTracker* _tracker; + + struct WidgetStub : public Inkscape::UI::View::EditWidgetInterface { + SPDesktopWidget *_dtw; + WidgetStub (SPDesktopWidget* dtw) : _dtw(dtw) {} + + void setTitle (gchar const *uri) override + { _dtw->updateTitle (uri); } + Gtk::Window* getWindow() override + { return _dtw->window; } + + void layout() override { + _dtw->layoutWidgets(); + } + + void present() override + { _dtw->presentWindow(); } + void getGeometry (gint &x, gint &y, gint &w, gint &h) override + { _dtw->getWindowGeometry (x, y, w, h); } + void setSize (gint w, gint h) override + { _dtw->setWindowSize (w, h); } + void setPosition (Geom::Point p) override + { _dtw->setWindowPosition (p); } + void setTransient (void* p, int transient_policy) override + { _dtw->setWindowTransient (p, transient_policy); } + Geom::Point getPointer() override + { return _dtw->window_get_pointer(); } + void setIconified() override + { _dtw->iconify(); } + void setMaximized() override + { _dtw->maximize(); } + void setFullscreen() override + { _dtw->fullscreen(); } + bool shutdown() override + { return _dtw->shutdown(); } + void destroy() override + { + if(_dtw->window != nullptr) + delete _dtw->window; + _dtw->window = nullptr; + } + + void storeDesktopPosition() override { _dtw->storeDesktopPosition(); } + void requestCanvasUpdate() override { _dtw->requestCanvasUpdate(); } + void requestCanvasUpdateAndWait() override { _dtw->requestCanvasUpdateAndWait(); } + void enableInteraction() override { _dtw->enableInteraction(); } + void disableInteraction() override { _dtw->disableInteraction(); } + void activateDesktop() override { sp_dtw_desktop_activate(_dtw); } + void deactivateDesktop() override { sp_dtw_desktop_deactivate(_dtw); } + void updateRulers() override { _dtw->update_rulers(); } + void updateScrollbars(double scale) override { _dtw->update_scrollbars(scale); } + void toggleRulers() override { _dtw->toggle_rulers(); } + void toggleScrollbars() override { _dtw->toggle_scrollbars(); } + void toggleColorProfAdjust() override { _dtw->toggle_color_prof_adj(); } + bool colorProfAdjustEnabled() override { return _dtw->get_color_prof_adj_enabled(); } + void updateZoom() override { _dtw->update_zoom(); } + void letZoomGrabFocus() override { _dtw->letZoomGrabFocus(); } + void updateRotation() override { _dtw->update_rotation(); } + Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name) override {return _dtw->get_toolbar_by_name(name);} + void setToolboxFocusTo(const gchar *id) override { _dtw->setToolboxFocusTo(id); } + void setToolboxAdjustmentValue(const gchar *id, double val) override + { _dtw->setToolboxAdjustmentValue (id, val); } + bool isToolboxButtonActive (gchar const* id) override + { return _dtw->isToolboxButtonActive (id); } + void setCoordinateStatus (Geom::Point p) override + { _dtw->setCoordinateStatus (p); } + void setMessage (Inkscape::MessageType type, gchar const* msg) override + { _dtw->setMessage (type, msg); } + + bool showInfoDialog( Glib::ustring const &message ) override { + return _dtw->showInfoDialog( message ); + } + + bool warnDialog (Glib::ustring const &text) override + { return _dtw->warnDialog (text); } + + Inkscape::UI::Widget::Dock* getDock () override + { return _dtw->getDock(); } + }; + + WidgetStub *stub; + + void setMessage(Inkscape::MessageType type, gchar const *message); + Geom::Point window_get_pointer(); + bool shutdown(); + void viewSetPosition (Geom::Point p); + void letZoomGrabFocus(); + void getWindowGeometry (gint &x, gint &y, gint &w, gint &h); + void setWindowPosition (Geom::Point p); + void setWindowSize (gint w, gint h); + void setWindowTransient (void *p, int transient_policy); + void presentWindow(); + bool showInfoDialog( Glib::ustring const &message ); + bool warnDialog (Glib::ustring const &text); + Gtk::Toolbar* get_toolbar_by_name(const Glib::ustring& name); + void setToolboxFocusTo (gchar const *); + void setToolboxAdjustmentValue (gchar const * id, double value); + bool isToolboxButtonActive (gchar const *id); + void setToolboxPosition(Glib::ustring const& id, GtkPositionType pos); + void setCoordinateStatus(Geom::Point p); + void storeDesktopPosition(); + void requestCanvasUpdate(); + void requestCanvasUpdateAndWait(); + void enableInteraction(); + void disableInteraction(); + void updateTitle(gchar const *uri); + bool onFocusInEvent(GdkEventFocus*); + + Gtk::MenuBar *menubar() { return _menubar; } + + Inkscape::UI::Widget::Dock* getDock(); + + static GType getType(); + static SPDesktopWidget* createInstance(SPDocument *document); + + void updateNamedview(); + void update_guides_lock(); + + /// Get the CMS adjustment button widget + decltype(_cms_adjust) get_cms_adjust() const {return _cms_adjust;} + + void cms_adjust_set_sensitive(bool enabled); + bool get_color_prof_adj_enabled() const; + void toggle_color_prof_adj(); + bool get_sticky_zoom_active() const; + void update_zoom(); + void update_rotation(); + void update_rulers(); + double get_hruler_thickness() const; + double get_vruler_thickness() const; + GtkAllocation get_canvas_allocation() const; + void iconify(); + void maximize(); + void fullscreen(); + static gint ruler_event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw, bool horiz); + + private: + GtkWidget *tool_toolbox; + GtkWidget *aux_toolbox; + GtkWidget *commands_toolbox; + GtkWidget *snap_toolbox; + + static void init(SPDesktopWidget *widget); + void layoutWidgets(); + + void namedviewModified(SPObject *obj, guint flags); + void on_adjustment_value_changed(); + void toggle_scrollbars(); + void update_scrollbars(double scale); + void toggle_rulers(); + void sticky_zoom_toggled(); + int zoom_input(double *new_val); + bool zoom_output(); + void zoom_value_changed(); + void zoom_menu_handler(double factor); + void zoom_populate_popup(Gtk::Menu *menu); + int rotation_input(double *new_val); + bool rotation_output(); + void rotation_value_changed(); + void rotation_populate_popup(Gtk::Menu *menu); + void canvas_tbl_size_allocate(Gtk::Allocation &allocation); + +#if defined(HAVE_LIBLCMS2) + static void cms_adjust_toggled( GtkWidget *button, gpointer data ); + static void color_profile_event(EgeColorProfTracker *tracker, SPDesktopWidget *dtw); +#endif + static void ruler_snap_new_guide(SPDesktop *desktop, SPCanvasItem *guide, Geom::Point &event_dt, Geom::Point &normal); + static gint event(GtkWidget *widget, GdkEvent *event, SPDesktopWidget *dtw); + bool on_ruler_box_button_press_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz); + bool on_ruler_box_button_release_event(GdkEventButton *event, Gtk::EventBox *widget, bool horiz); + bool on_ruler_box_motion_notify_event(GdkEventMotion *event, Gtk::EventBox *widget, bool horiz); +}; + +/// The SPDesktopWidget vtable +struct SPDesktopWidgetClass { + SPViewWidgetClass parent_class; +}; + +#endif /* !SEEN_SP_DESKTOP_WIDGET_H */ + +/* + Local Variables: +mode:c++ +c-file-style:"stroustrup" +c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) +indent-tabs-mode:nil +fill-column:99 +End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/widgets/ege-paint-def.cpp b/src/widgets/ege-paint-def.cpp new file mode 100644 index 0000000..fa654cb --- /dev/null +++ b/src/widgets/ege-paint-def.cpp @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Color Definition. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#include <libintl.h> + +#include <cstdint> +#include <string> +#include <iostream> +#include <sstream> +#include <cstring> +#include <cstdio> +#include <utility> +#include <glibmm/i18n.h> +#include <glibmm/stringutils.h> + +#if !defined(_) +#define _(s) gettext(s) +#endif // !defined(_) + +#include "ege-paint-def.h" + +namespace ege +{ + +static std::string mimeTEXT("text/plain"); +static std::string mimeX_COLOR("application/x-color"); +static std::string mimeOSWB_COLOR("application/x-oswb-color"); + +PaintDef::PaintDef() : + descr(_("none")), + type(NONE), + r(0), + g(0), + b(0), + editable(false), + _listeners() +{ +} + +PaintDef::PaintDef( ColorType type ) : + descr(), + type(type), + r(0), + g(0), + b(0), + editable(false), + _listeners() +{ + switch (type) { + case CLEAR: + descr = _("remove"); + break; + case NONE: + descr = _("none"); + break; + case RGB: + descr = ""; + break; + } +} + +PaintDef::PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description ) : + descr(std::move(description)), + type(RGB), + r(r), + g(g), + b(b), + editable(false), + _listeners() +{ +} + +PaintDef::~PaintDef() += default; + +PaintDef::PaintDef( PaintDef const &other ) +{ + if ( this != &other ) { + *this = other; + } +} + +PaintDef& PaintDef::operator=( PaintDef const &other ) +{ + if ( this != & other ) + { + type = other.type; + r = other.r; + g = other.g; + b = other.b; + descr = other.descr; + editable = other.editable; + //TODO: _listeners should be assigned a value + } + return *this; +} + +class PaintDef::HookData { +public: + HookData( ColorCallback cb, void* data ) {_cb = cb; _data = data;} + ColorCallback _cb; + void* _data; +}; + + +std::vector<std::string> PaintDef::getMIMETypes() +{ + std::vector<std::string> listing; + listing.push_back(mimeOSWB_COLOR); + listing.push_back(mimeX_COLOR); + listing.push_back(mimeTEXT); + return listing; +} + +void PaintDef::getMIMEData(std::string const & type, char*& dest, int& len, int& format) +{ + if ( type == mimeTEXT ) { + dest = new char[8]; + snprintf( dest, 8, "#%02x%02x%02x", getR(), getG(), getB() ); + dest[7] = 0; + len = 8; + format = 8; + } else if ( type == mimeX_COLOR ) { + uint16_t* tmp = new uint16_t[4]; + tmp[0] = (getR() << 8) | getR(); + tmp[1] = (getG() << 8) | getG(); + tmp[2] = (getB() << 8) | getB(); + tmp[3] = 0xffff; + dest = reinterpret_cast<char*>(tmp); + len = 8; + format = 16; + } else if ( type == mimeOSWB_COLOR ) { + std::string tmp("<paint>"); + switch ( getType() ) { + case ege::PaintDef::NONE: + { + tmp += "<nocolor/>"; + } + break; + case ege::PaintDef::CLEAR: + { + tmp += "<clear/>"; + } + break; + default: + { + tmp += std::string("<color name=\"") + descr + "\">"; + tmp += "<sRGB r=\""; + tmp += Glib::Ascii::dtostr(getR()/255.0); + tmp += "\" g=\""; + tmp += Glib::Ascii::dtostr(getG()/255.0); + tmp += "\" b=\""; + tmp += Glib::Ascii::dtostr(getB()/255.0); + tmp += "\"/>"; + tmp += "</color>"; + } + } + tmp += "</paint>"; + len = tmp.size(); + dest = new char[len]; + // Note that this is not null-terminated: + memcpy(dest, tmp.c_str(), len); + format = 8; + } else { + // nothing + dest = nullptr; + len = 0; + } +} + +bool PaintDef::fromMIMEData(std::string const & type, char const * data, int len, int /*format*/) +{ + bool worked = false; + bool changed = false; + if ( type == mimeTEXT ) { + } else if ( type == mimeX_COLOR ) { + } else if ( type == mimeOSWB_COLOR ) { + std::string xml(data, len); + if ( xml.find("<nocolor/>") != std::string::npos ) { + if ( (this->type != ege::PaintDef::NONE) + || (this->r != 0) + || (this->g != 0) + || (this->b != 0) ) { + this->type = ege::PaintDef::NONE; + this->r = 0; + this->g = 0; + this->b = 0; + changed = true; + } + worked = true; + } else { + size_t pos = xml.find("<sRGB"); + if ( pos != std::string::npos ) { + size_t endPos = xml.find(">", pos); + std::string srgb = xml.substr(pos, endPos); + this->type = ege::PaintDef::RGB; + size_t numPos = srgb.find("r="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->r = static_cast<int>(255 * dbl); + } + numPos = srgb.find("g="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->g = static_cast<int>(255 * dbl); + } + numPos = srgb.find("b="); + if (numPos != std::string::npos) { + double dbl = Glib::Ascii::strtod(srgb.substr(numPos + 3)); + this->b = static_cast<int>(255 * dbl); + } + + size_t pos = xml.find("<color "); + if ( pos != std::string::npos ) { + size_t endPos = xml.find(">", pos); + std::string colorTag = xml.substr(pos, endPos); + + size_t namePos = colorTag.find("name="); + if (namePos != std::string::npos) { + char quote = colorTag[namePos + 5]; + endPos = colorTag.find(quote, namePos + 6); + descr = colorTag.substr(namePos + 6, endPos - (namePos + 6)); + } + } + changed = true; + worked = true; + } + } + } + if ( changed ) { + // beware of callbacks changing things + for (auto & _listener : _listeners) + { + if ( _listener->_cb ) + { + _listener->_cb( _listener->_data ); + } + } + } + return worked; +} + +void PaintDef::setRGB( unsigned int r, unsigned int g, unsigned int b ) +{ + if ( r != this->r || g != this->g || b != this->b ) { + this->r = r; + this->g = g; + this->b = b; + + // beware of callbacks changing things + for (auto & _listener : _listeners) + { + if ( _listener->_cb ) + { + _listener->_cb( _listener->_data ); + } + } + } +} + +void PaintDef::addCallback( ColorCallback cb, void* data ) +{ + _listeners.push_back( new HookData(cb, data) ); +} + +void PaintDef::removeCallback( ColorCallback /*cb*/, void* /*data*/ ) +{ +} + + +} // namespace ege + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/ege-paint-def.h b/src/widgets/ege-paint-def.h new file mode 100644 index 0000000..63643c5 --- /dev/null +++ b/src/widgets/ege-paint-def.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later OR MPL-1.1 OR LGPL-2.1-or-later +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Eek Color Definition. + * + * The Initial Developer of the Original Code is + * Jon A. Cruz. + * Portions created by the Initial Developer are Copyright (C) 2006 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +#ifndef SEEN_EGE_PAINT_DEF_H +#define SEEN_EGE_PAINT_DEF_H + +#include <string> +#include <vector> + +namespace ege +{ + +typedef void (*ColorCallback)( void* data ); + + +/** + * Pure data representation of a color definition. + */ +class PaintDef +{ +public: + enum ColorType{CLEAR, NONE, RGB}; + + PaintDef(); + PaintDef(ColorType type); + PaintDef( unsigned int r, unsigned int g, unsigned int b, std::string description ); + virtual ~PaintDef(); + + PaintDef( PaintDef const &other ); + virtual PaintDef& operator=( PaintDef const &other ); + + ColorType getType() const { return type; } + + std::vector<std::string> getMIMETypes(); + void getMIMEData(std::string const & type, char*& dest, int& len, int& format); + bool fromMIMEData(std::string const & type, char const * data, int len, int format); + + void setRGB( unsigned int r, unsigned int g, unsigned int b ); + unsigned int getR() const { return r; } + unsigned int getG() const { return g; } + unsigned int getB() const { return b; } + + void addCallback( ColorCallback cb, void* data ); + void removeCallback( ColorCallback cb, void* data ); + + bool isEditable() const { return editable; } + void setEditable( bool edit ) { editable = edit; } + + std::string descr; + +protected: + ColorType type; + unsigned int r; + unsigned int g; + unsigned int b; + bool editable; + +private: + class HookData; + + std::vector<HookData*> _listeners; +}; + + +} // namespace ege + +#endif // SEEN_EGE_PAINT_DEF_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/fill-n-stroke-factory.h b/src/widgets/fill-n-stroke-factory.h new file mode 100644 index 0000000..5ca9daa --- /dev/null +++ b/src/widgets/fill-n-stroke-factory.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_FILL_N_STROKE_FACTORY_H +#define SEEN_FILL_N_STROKE_FACTORY_H +/* Authors: + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "fill-or-stroke.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace Widgets { + +Gtk::Widget *createStyleWidget( FillOrStroke kind ); +Gtk::Widget *createStrokeStyleWidget( ); + +} // namespace Widgets +} // namespace Inkscape + +#endif // !SEEN_FILL_N_STROKE_FACTORY_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/fill-style.cpp b/src/widgets/fill-style.cpp new file mode 100644 index 0000000..f35a148 --- /dev/null +++ b/src/widgets/fill-style.cpp @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill style widget. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2005 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_FS_VERBOSE + +#include <gtkmm/box.h> +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-n-stroke-factory.h" +#include "fill-style.h" +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "selection.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-text.h" +#include "style.h" + +#include "display/sp-canvas.h" + +#include "widgets/paint-selector.h" + + +// These can be deleted once we sort out the libart dependence. + +#define ART_WIND_RULE_NONZERO 0 + +/* Fill */ + + +Gtk::Widget *sp_fill_style_widget_new() +{ + return Inkscape::Widgets::createStyleWidget( FILL ); +} + + +namespace Inkscape { + +class FillNStroke : public Gtk::VBox +{ +public: + FillNStroke( FillOrStroke k ); + ~FillNStroke() override; + + void setFillrule( SPPaintSelector::FillRule mode ); + + void setDesktop(SPDesktop *desktop); + +private: + static void paintModeChangeCB(SPPaintSelector *psel, SPPaintSelector::Mode mode, FillNStroke *self); + static void paintChangedCB(SPPaintSelector *psel, FillNStroke *self); + static void paintDraggedCB(SPPaintSelector *psel, FillNStroke *self); + static gboolean dragDelayCB(gpointer data); + + static void fillruleChangedCB( SPPaintSelector *psel, SPPaintSelector::FillRule mode, FillNStroke *self ); + + void selectionModifiedCB(guint flags); + void eventContextCB(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext); + + void dragFromPaint(); + void updateFromPaint(); + + void performUpdate(); + + FillOrStroke kind; + SPDesktop *desktop; + SPPaintSelector *psel; + guint32 lastDrag; + guint dragId; + bool update; + sigc::connection selectChangedConn; + sigc::connection subselChangedConn; + sigc::connection selectModifiedConn; + sigc::connection eventContextConn; +}; + +} // namespace Inkscape + +void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop) +{ + Inkscape::FillNStroke *fs = dynamic_cast<Inkscape::FillNStroke*>(widget); + if (fs) { + fs->setDesktop(desktop); + } +} + +namespace Inkscape { + +/** + * Create the fill or stroke style widget, and hook up all the signals. + */ +Gtk::Widget *Inkscape::Widgets::createStyleWidget( FillOrStroke kind ) +{ + FillNStroke *filler = new FillNStroke(kind); + + return filler; +} + +FillNStroke::FillNStroke( FillOrStroke k ) : + Gtk::VBox(), + kind(k), + desktop(nullptr), + psel(nullptr), + lastDrag(0), + dragId(0), + update(false), + selectChangedConn(), + subselChangedConn(), + selectModifiedConn(), + eventContextConn() +{ + // Add and connect up the paint selector widget: + psel = sp_paint_selector_new(kind); + gtk_widget_show(GTK_WIDGET(psel)); + gtk_container_add(GTK_CONTAINER(gobj()), GTK_WIDGET(psel)); + g_signal_connect( G_OBJECT(psel), "mode_changed", + G_CALLBACK(paintModeChangeCB), + this ); + + g_signal_connect( G_OBJECT(psel), "dragged", + G_CALLBACK(paintDraggedCB), + this ); + + g_signal_connect( G_OBJECT(psel), "changed", + G_CALLBACK(paintChangedCB), + this ); + if (kind == FILL) { + g_signal_connect( G_OBJECT(psel), "fillrule_changed", + G_CALLBACK(fillruleChangedCB), + this ); + } + + performUpdate(); +} + +FillNStroke::~FillNStroke() +{ + if (dragId) { + g_source_remove(dragId); + dragId = 0; + } + psel = nullptr; + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + eventContextConn.disconnect(); +} + +/** + * On signal modified, invokes an update of the fill or stroke style paint object. + */ +void FillNStroke::selectionModifiedCB( guint flags ) +{ + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { +#ifdef SP_FS_VERBOSE + g_message("selectionModifiedCB(%d) on %p", flags, this); +#endif + performUpdate(); + } +} + +void FillNStroke::setDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + if (dragId) { + g_source_remove(dragId); + dragId = 0; + } + if (this->desktop) { + selectModifiedConn.disconnect(); + subselChangedConn.disconnect(); + selectChangedConn.disconnect(); + eventContextConn.disconnect(); + } + this->desktop = desktop; + if (desktop && desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); + subselChangedConn = desktop->connectToolSubselectionChanged(sigc::hide(sigc::mem_fun(*this, &FillNStroke::performUpdate))); + eventContextConn = desktop->connectEventContextChanged(sigc::hide(sigc::bind(sigc::mem_fun(*this, &FillNStroke::eventContextCB), (Inkscape::UI::Tools::ToolBase *)nullptr))); + + // Must check flags, so can't call performUpdate() directly. + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &FillNStroke::selectionModifiedCB))); + } + performUpdate(); + } +} + +/** + * Listen to this "change in tool" event, in case a subselection tool (such as Gradient or Node) selection + * is changed back to a selection tool - especially needed for selected gradient stops. + */ +void FillNStroke::eventContextCB(SPDesktop * /*desktop*/, Inkscape::UI::Tools::ToolBase * /*eventcontext*/) +{ + performUpdate(); +} + + +/** + * Gets the active fill or stroke style property, then sets the appropriate + * color, alpha, gradient, pattern, etc. for the paint-selector. + * + * @param sel Selection to use, or NULL. + */ +void FillNStroke::performUpdate() +{ + if ( update || !desktop ) { + return; + } + + if ( dragId ) { + // local change; do nothing, but reset the flag + g_source_remove(dragId); + dragId = 0; + return; + } + + update = true; + + // create temporary style + SPStyle query(desktop->doc()); + + // query style from desktop into it. This returns a result flag and fills query with the style of subselection, if any, or selection + int result = sp_desktop_query_style(desktop, &query, (kind == FILL) ? QUERY_STYLE_PROPERTY_FILL : QUERY_STYLE_PROPERTY_STROKE); + + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPIScale24 &targOpacity = *(kind == FILL ? query.fill_opacity.upcast() : query.stroke_opacity.upcast()); + + switch (result) { + case QUERY_STYLE_NOTHING: + { + /* No paint at all */ + psel->setMode(SPPaintSelector::MODE_EMPTY); + break; + } + + case QUERY_STYLE_SINGLE: + case QUERY_STYLE_MULTIPLE_AVERAGED: // TODO: treat this slightly differently, e.g. display "averaged" somewhere in paint selector + case QUERY_STYLE_MULTIPLE_SAME: + { + SPPaintSelector::Mode pselmode = SPPaintSelector::getModeForStyle(query, kind); + psel->setMode(pselmode); + + if (kind == FILL) { + psel->setFillrule(query.fill_rule.computed == ART_WIND_RULE_NONZERO? + SPPaintSelector::FILLRULE_NONZERO : SPPaintSelector::FILLRULE_EVENODD); + } + + if (targPaint.set && targPaint.isColor()) { + psel->setColorAlpha(targPaint.value.color, SP_SCALE24_TO_FLOAT(targOpacity.value)); + } else if (targPaint.set && targPaint.isPaintserver()) { + + SPPaintServer *server = (kind == FILL) ? query.getFillPaintServer() : query.getStrokePaintServer(); + + if (server) { + if (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setSwatch( vector ); + } else if (SP_IS_LINEARGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setGradientLinear( vector ); + + SPLinearGradient *lg = SP_LINEARGRADIENT(server); + psel->setGradientProperties( lg->getUnits(), + lg->getSpread() ); + } else if (SP_IS_RADIALGRADIENT(server)) { + SPGradient *vector = SP_GRADIENT(server)->getVector(); + psel->setGradientRadial( vector ); + + SPRadialGradient *rg = SP_RADIALGRADIENT(server); + psel->setGradientProperties( rg->getUnits(), + rg->getSpread() ); +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + SPGradient *array = SP_GRADIENT(server)->getArray(); + psel->setGradientMesh( SP_MESHGRADIENT(array) ); + SPMeshGradient *mg = SP_MESHGRADIENT(server); + psel->updateMeshList( SP_MESHGRADIENT( array )); +#endif + } else if (SP_IS_PATTERN(server)) { + SPPattern *pat = SP_PATTERN(server)->rootPattern(); + psel->updatePatternList( pat ); + } + } + } + break; + } + + case QUERY_STYLE_MULTIPLE_DIFFERENT: + { + psel->setMode(SPPaintSelector::MODE_MULTIPLE); + break; + } + } + + update = false; +} + +/** + * When the mode is changed, invoke a regular changed handler. + */ +void FillNStroke::paintModeChangeCB( SPPaintSelector * /*psel*/, + SPPaintSelector::Mode /*mode*/, + FillNStroke *self ) +{ +#ifdef SP_FS_VERBOSE + g_message("paintModeChangeCB(psel, mode, self:%p)", self); +#endif + if (self && !self->update) { + self->updateFromPaint(); + } +} + +void FillNStroke::fillruleChangedCB( SPPaintSelector * /*psel*/, + SPPaintSelector::FillRule mode, + FillNStroke *self ) +{ + if (self) { + self->setFillrule(mode); + } +} + +void FillNStroke::setFillrule( SPPaintSelector::FillRule mode ) +{ + if (!update && desktop) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-rule", (mode == SPPaintSelector::FILLRULE_EVENODD) ? "evenodd":"nonzero"); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(desktop->doc(), SP_VERB_DIALOG_FILL_STROKE, + _("Change fill rule")); + } +} + +static gchar const *undo_F_label_1 = "fill:flatcolor:1"; +static gchar const *undo_F_label_2 = "fill:flatcolor:2"; + +static gchar const *undo_S_label_1 = "stroke:flatcolor:1"; +static gchar const *undo_S_label_2 = "stroke:flatcolor:2"; + +static gchar const *undo_F_label = undo_F_label_1; +static gchar const *undo_S_label = undo_S_label_1; + + +void FillNStroke::paintDraggedCB(SPPaintSelector * /*psel*/, FillNStroke *self) +{ +#ifdef SP_FS_VERBOSE + g_message("paintDraggedCB(psel, spw:%p)", self); +#endif + if (self && !self->update) { + self->dragFromPaint(); + } +} + + +gboolean FillNStroke::dragDelayCB(gpointer data) +{ + gboolean keepGoing = TRUE; + if (data) { + FillNStroke *self = reinterpret_cast<FillNStroke*>(data); + if (!self->update) { + if (self->dragId) { + g_source_remove(self->dragId); + self->dragId = 0; + + self->dragFromPaint(); + self->performUpdate(); + } + keepGoing = FALSE; + } + } else { + keepGoing = FALSE; + } + return keepGoing; +} + +/** + * This is called repeatedly while you are dragging a color slider, only for flat color + * modes. Previously it set the color in style but did not update the repr for efficiency, however + * this was flakey and didn't buy us almost anything. So now it does the same as _changed, except + * lumps all its changes for undo. + */ +void FillNStroke::dragFromPaint() +{ + if (!desktop || update) { + return; + } + + guint32 when = gtk_get_current_event_time(); + + // Don't attempt too many updates per second. + // Assume a base 15.625ms resolution on the timer. + if (!dragId && lastDrag && when && ((when - lastDrag) < 32)) { + // local change, do not update from selection + dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 33, dragDelayCB, this, nullptr); + } + + if (dragId) { + // previous local flag not cleared yet; + // this means dragged events come too fast, so we better skip this one to speed up display + // (it's safe to do this in any case) + return; + } + lastDrag = when; + + update = true; + + switch (psel->mode) { + case SPPaintSelector::MODE_SOLID_COLOR: + { + // local change, do not update from selection + dragId = g_timeout_add_full(G_PRIORITY_DEFAULT, 100, dragDelayCB, this, nullptr); + psel->setFlatColor( desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); + DocumentUndo::maybeDone(desktop->doc(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set fill color") : _("Set stroke color")); + break; + } + + default: + g_warning( "file %s: line %d: Paint %d should not emit 'dragged'", + __FILE__, __LINE__, psel->mode ); + break; + } + update = false; +} + +/** +This is called (at least) when: +1 paint selector mode is switched (e.g. flat color -> gradient) +2 you finished dragging a gradient node and released mouse +3 you changed a gradient selector parameter (e.g. spread) +Must update repr. + */ +void FillNStroke::paintChangedCB( SPPaintSelector * /*psel*/, FillNStroke *self ) +{ +#ifdef SP_FS_VERBOSE + g_message("paintChangedCB(psel, spw:%p)", self); +#endif + if (self && !self->update) { + self->updateFromPaint(); + } +} + +void FillNStroke::updateFromPaint() +{ + if (!desktop) { + return; + } + update = true; + + SPDocument *document = desktop->getDocument(); + Inkscape::Selection *selection = desktop->getSelection(); + + std::vector<SPItem*> const items(selection->items().begin(), selection->items().end()); + + switch (psel->mode) { + case SPPaintSelector::MODE_EMPTY: + // This should not happen. + g_warning( "file %s: line %d: Paint %d should not emit 'changed'", + __FILE__, __LINE__, psel->mode); + break; + case SPPaintSelector::MODE_MULTIPLE: + // This happens when you switch multiple objects with different gradients to flat color; + // nothing to do here. + break; + + case SPPaintSelector::MODE_NONE: + { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", "none"); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Remove fill") : _("Remove stroke")); + break; + } + + case SPPaintSelector::MODE_SOLID_COLOR: + { + if (kind == FILL) { + // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in losing release events + desktop->getCanvas()->forceFullRedrawAfterInterruptions(0); + } + + psel->setFlatColor( desktop, + (kind == FILL) ? "fill" : "stroke", + (kind == FILL) ? "fill-opacity" : "stroke-opacity" ); + DocumentUndo::maybeDone(desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set fill color") : _("Set stroke color")); + + if (kind == FILL) { + // resume interruptibility + desktop->getCanvas()->endForcedFullRedraws(); + } + + // on release, toggle undo_label so that the next drag will not be lumped with this one + if (undo_F_label == undo_F_label_1) { + undo_F_label = undo_F_label_2; + undo_S_label = undo_S_label_2; + } else { + undo_F_label = undo_F_label_1; + undo_S_label = undo_S_label_1; + } + + break; + } + + case SPPaintSelector::MODE_GRADIENT_LINEAR: + case SPPaintSelector::MODE_GRADIENT_RADIAL: + case SPPaintSelector::MODE_SWATCH: + if (!items.empty()) { + SPGradientType const gradient_type = ( psel->mode != SPPaintSelector::MODE_GRADIENT_RADIAL + ? SP_GRADIENT_TYPE_LINEAR + : SP_GRADIENT_TYPE_RADIAL ); + bool createSwatch = (psel->mode == SPPaintSelector::MODE_SWATCH); + + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + SPGradient *vector = psel->getGradientVector(); + if (!vector) { + /* No vector in paint selector should mean that we just changed mode */ + + SPStyle query(desktop->doc()); + int result = objects_query_fillstroke(items, &query, kind == FILL); + if (result == QUERY_STYLE_MULTIPLE_SAME) { + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + SPColor common; + if (!targPaint.isColor()) { + common = sp_desktop_get_color(desktop, kind == FILL); + } else { + common = targPaint.value.color; + } + vector = sp_document_default_gradient_vector( document, common, createSwatch ); + if ( vector && createSwatch ) { + vector->setSwatch(); + } + } + + for(auto item : items){ + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + if (!vector) { + SPGradient *gr = sp_gradient_vector_for_object( document, + desktop, + reinterpret_cast<SPObject*>(item), + (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE, + createSwatch ); + if ( gr && createSwatch ) { + gr->setSwatch(); + } + sp_item_set_gradient(item, + gr, + gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } else { + sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + } + } + } else { + // We have changed from another gradient type, or modified spread/units within + // this gradient type. + vector = sp_gradient_ensure_vector_normalized(vector); + for(auto item : items){ + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + SPGradient *gr = sp_item_set_gradient(item, vector, gradient_type, (kind == FILL) ? Inkscape::FOR_FILL : Inkscape::FOR_STROKE); + psel->pushAttrsToGradient( gr ); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set gradient on fill") : _("Set gradient on stroke")); + } + break; + +#ifdef WITH_MESH + case SPPaintSelector::MODE_GRADIENT_MESH: + + if (!items.empty()) { + SPGradientType const gradient_type = SP_GRADIENT_TYPE_MESH; + + SPCSSAttr *css = nullptr; + if (kind == FILL) { + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPDefs *defs = document->getDefs(); + + SPMeshGradient * mesh = psel->getMeshGradient(); + + for(auto item : items){ + + //FIXME: see above + if (kind == FILL) { + sp_repr_css_change_recursive(item->getRepr(), css, "style"); + } + + // Check if object already has mesh. + bool has_mesh = false; + SPStyle *style = item->style; + if (style) { + SPPaintServer *server = + (kind==FILL) ? style->getFillPaintServer():style->getStrokePaintServer(); + if (server && SP_IS_MESHGRADIENT(server)) + has_mesh = true; + } + + if (!mesh || !has_mesh) { + // No mesh in document or object does not already have mesh -> + // Create new mesh. + + // Create mesh element + Inkscape::XML::Node *repr = xml_doc->createElement("svg:meshgradient"); + + // privates are garbage-collectable + repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(repr); + Inkscape::GC::release(repr); + + // Get corresponding object + SPMeshGradient *mg = static_cast<SPMeshGradient *>(document->getObjectByRepr(repr)); + mg->array.create(mg, item, (kind==FILL) ? + item->geometricBounds() : item->visualBounds()); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), + mg, isText); + + // (*i)->requestModified(SP_OBJECT_MODIFIED_FLAG|SP_OBJECT_STYLE_MODIFIED_FLAG); + + } else { + // Using found mesh + + // Duplicate + Inkscape::XML::Node *mesh_repr = mesh->getRepr(); + Inkscape::XML::Node *copy_repr = mesh_repr->duplicate(xml_doc); + + // privates are garbage-collectable + copy_repr->setAttribute("inkscape:collect", "always"); + + // Attach to document + defs->getRepr()->appendChild(copy_repr); + Inkscape::GC::release(copy_repr); + + // Get corresponding object + SPMeshGradient *mg = + static_cast<SPMeshGradient *>(document->getObjectByRepr(copy_repr)); + // std::cout << " " << (mg->getId()?mg->getId():"null") << std::endl; + mg->array.read(mg); + + Geom::OptRect item_bbox = (kind==FILL) ? + item->geometricBounds() : item->visualBounds(); + mg->array.fill_box( item_bbox ); + + bool isText = SP_IS_TEXT(item); + sp_style_set_property_url (item, ((kind == FILL) ? "fill":"stroke"), + mg, isText); + } + } + + if (css) { + sp_repr_css_attr_unref(css); + css = nullptr; + } + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set mesh on fill") : _("Set mesh on stroke")); + } + break; +#endif + + case SPPaintSelector::MODE_PATTERN: + + if (!items.empty()) { + + SPPattern *pattern = psel->getPattern(); + if (!pattern) { + + /* No Pattern in paint selector should mean that we just + * changed mode - don't do jack. + */ + + } else { + Inkscape::XML::Node *patrepr = pattern->getRepr(); + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar *urltext = g_strdup_printf("url(#%s)", patrepr->attribute("id")); + sp_repr_css_set_property(css, (kind == FILL) ? "fill" : "stroke", urltext); + + // HACK: reset fill-opacity - that 0.75 is annoying; BUT remove this when we have an opacity slider for all tabs + if (kind == FILL) { + sp_repr_css_set_property(css, "fill-opacity", "1.0"); + } + + // cannot just call sp_desktop_set_style, because we don't want to touch those + // objects who already have the same root pattern but through a different href + // chain. FIXME: move this to a sp_item_set_pattern + for(auto item : items){ + Inkscape::XML::Node *selrepr = item->getRepr(); + if ( (kind == STROKE) && !selrepr) { + continue; + } + SPObject *selobj = item; + + SPStyle *style = selobj->style; + if (style && ((kind == FILL) ? style->fill.isPaintserver() : style->stroke.isPaintserver())) { + SPPaintServer *server = (kind == FILL) ? + selobj->style->getFillPaintServer() : + selobj->style->getStrokePaintServer(); + if (SP_IS_PATTERN(server) && SP_PATTERN(server)->rootPattern() == pattern) + // only if this object's pattern is not rooted in our selected pattern, apply + continue; + } + + if (kind == FILL) { + sp_desktop_apply_css_recursive(selobj, css, true); + } else { + sp_repr_css_change_recursive(selrepr, css, "style"); + } + } + + sp_repr_css_attr_unref(css); + css = nullptr; + g_free(urltext); + + } // end if + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Set pattern on fill") : + _("Set pattern on stroke")); + } // end if + + break; + + case SPPaintSelector::MODE_UNSET: + if (!items.empty()) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (kind == FILL) { + sp_repr_css_unset_property(css, "fill"); + } else { + sp_repr_css_unset_property(css, "stroke"); + sp_repr_css_unset_property(css, "stroke-opacity"); + sp_repr_css_unset_property(css, "stroke-width"); + sp_repr_css_unset_property(css, "stroke-miterlimit"); + sp_repr_css_unset_property(css, "stroke-linejoin"); + sp_repr_css_unset_property(css, "stroke-linecap"); + sp_repr_css_unset_property(css, "stroke-dashoffset"); + sp_repr_css_unset_property(css, "stroke-dasharray"); + } + + sp_desktop_set_style(desktop, css); + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + (kind == FILL) ? _("Unset fill") : _("Unset stroke")); + } + break; + + default: + g_warning( "file %s: line %d: Paint selector should not be in " + "mode %d", + __FILE__, __LINE__, + psel->mode ); + break; + } + + update = false; +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/fill-style.h b/src/widgets/fill-style.h new file mode 100644 index 0000000..3a64f2c --- /dev/null +++ b/src/widgets/fill-style.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Fill style configuration + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_SP_FILL_STYLE_H +#define SEEN_DIALOGS_SP_FILL_STYLE_H + +namespace Gtk { +class Widget; +} + +class SPDesktop; + +Gtk::Widget *sp_fill_style_widget_new(); + +void sp_fill_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop); + +#endif // SEEN_DIALOGS_SP_FILL_STYLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/gradient-image.cpp b/src/widgets/gradient-image.cpp new file mode 100644 index 0000000..1a0e253 --- /dev/null +++ b/src/widgets/gradient-image.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include <glibmm/refptr.h> +#include <gdkmm/pixbuf.h> + +#include <cairomm/surface.h> + +#include "gradient-image.h" + +#include "display/cairo-utils.h" + +#include "object/sp-gradient.h" +#include "object/sp-stop.h" + +static void sp_gradient_image_size_request (GtkWidget *widget, GtkRequisition *requisition); + +static void sp_gradient_image_destroy(GtkWidget *object); +static void sp_gradient_image_get_preferred_width(GtkWidget *widget, + gint *minimal_width, + gint *natural_width); + +static void sp_gradient_image_get_preferred_height(GtkWidget *widget, + gint *minimal_height, + gint *natural_height); +static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *cr); +static void sp_gradient_image_gradient_release (SPObject *, SPGradientImage *im); +static void sp_gradient_image_gradient_modified (SPObject *, guint flags, SPGradientImage *im); +static void sp_gradient_image_update (SPGradientImage *img); + +G_DEFINE_TYPE(SPGradientImage, sp_gradient_image, GTK_TYPE_WIDGET); + +static void sp_gradient_image_class_init(SPGradientImageClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + widget_class->get_preferred_width = sp_gradient_image_get_preferred_width; + widget_class->get_preferred_height = sp_gradient_image_get_preferred_height; + widget_class->draw = sp_gradient_image_draw; + widget_class->destroy = sp_gradient_image_destroy; +} + +static void +sp_gradient_image_init (SPGradientImage *image) +{ + gtk_widget_set_has_window (GTK_WIDGET(image), FALSE); + + image->gradient = nullptr; + + new (&image->release_connection) sigc::connection(); + new (&image->modified_connection) sigc::connection(); +} + +static void sp_gradient_image_destroy(GtkWidget *object) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE (object); + + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + image->gradient = nullptr; + } + + image->release_connection.~connection(); + image->modified_connection.~connection(); + + if (GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy) + GTK_WIDGET_CLASS(sp_gradient_image_parent_class)->destroy(object); +} + +static void sp_gradient_image_size_request(GtkWidget * /*widget*/, GtkRequisition *requisition) +{ + requisition->width = 54; + requisition->height = 12; +} + +static void sp_gradient_image_get_preferred_width(GtkWidget *widget, gint *minimal_width, gint *natural_width) +{ + GtkRequisition requisition; + sp_gradient_image_size_request(widget, &requisition); + *minimal_width = *natural_width = requisition.width; +} + +static void sp_gradient_image_get_preferred_height(GtkWidget *widget, gint *minimal_height, gint *natural_height) +{ + GtkRequisition requisition; + sp_gradient_image_size_request(widget, &requisition); + *minimal_height = *natural_height = requisition.height; +} + +static gboolean sp_gradient_image_draw(GtkWidget *widget, cairo_t *ct) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE(widget); + SPGradient *gr = image->gradient; + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(allocation.width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + return TRUE; +} + +GtkWidget * +sp_gradient_image_new (SPGradient *gradient) +{ + SPGradientImage *image = SP_GRADIENT_IMAGE(g_object_new(SP_TYPE_GRADIENT_IMAGE, nullptr)); + + sp_gradient_image_set_gradient (image, gradient); + + return GTK_WIDGET(image); +} + +GdkPixbuf* +sp_gradient_to_pixbuf (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + // no need to free s - the call below takes ownership + GdkPixbuf *pixbuf = ink_pixbuf_create_from_cairo_surface(s); + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_set_source(ct, check); + cairo_paint(ct); + cairo_pattern_destroy(check); + + if (gr) { + cairo_pattern_t *p = gr->create_preview_pattern(width); + cairo_set_source(ct, p); + cairo_paint(ct); + cairo_pattern_destroy(p); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + +Glib::RefPtr<Gdk::Pixbuf> +sp_gradstop_to_pixbuf_ref (SPStop *stop, int width, int height) +{ + cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *ct = cairo_create(s); + + /* Checkerboard background */ + cairo_pattern_t *check = ink_cairo_pattern_create_checkerboard(); + cairo_rectangle(ct, 0, 0, width, height); + cairo_set_source(ct, check); + cairo_fill_preserve(ct); + cairo_pattern_destroy(check); + + if (stop) { + /* Alpha area */ + cairo_rectangle(ct, 0, 0, width/2, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32()); + cairo_fill(ct); + + /* Solid area */ + cairo_rectangle(ct, width/2, 0, width, height); + ink_cairo_set_source_rgba32(ct, stop->get_rgba32() | 0xff); + cairo_fill(ct); + } + + cairo_destroy(ct); + cairo_surface_flush(s); + + Cairo::RefPtr<Cairo::Surface> sref = Cairo::RefPtr<Cairo::Surface>(new Cairo::Surface(s)); + Glib::RefPtr<Gdk::Pixbuf> pixbuf = + Gdk::Pixbuf::create(sref, 0, 0, width, height); + + cairo_surface_destroy(s); + + return pixbuf; +} + + +void +sp_gradient_image_set_gradient (SPGradientImage *image, SPGradient *gradient) +{ + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + } + + image->gradient = gradient; + + if (gradient) { + image->release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_image_gradient_release), image)); + image->modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_image_gradient_modified), image)); + } + + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_gradient_release (SPObject *, SPGradientImage *image) +{ + if (image->gradient) { + image->release_connection.disconnect(); + image->modified_connection.disconnect(); + } + + image->gradient = nullptr; + + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_gradient_modified (SPObject *, guint /*flags*/, SPGradientImage *image) +{ + sp_gradient_image_update (image); +} + +static void +sp_gradient_image_update (SPGradientImage *image) +{ + if (gtk_widget_is_drawable (GTK_WIDGET(image))) { + gtk_widget_queue_draw (GTK_WIDGET (image)); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/widgets/gradient-image.h b/src/widgets/gradient-image.h new file mode 100644 index 0000000..4fcc4ca --- /dev/null +++ b/src/widgets/gradient-image.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_GRADIENT_IMAGE_H +#define SEEN_SP_GRADIENT_IMAGE_H + +/** + * A simple gradient preview + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> +#include <glibmm/refptr.h> + +class SPGradient; +class SPStop; +namespace Gdk { + class Pixbuf; +} + +#include <sigc++/connection.h> + +#define SP_TYPE_GRADIENT_IMAGE (sp_gradient_image_get_type ()) +#define SP_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_IMAGE, SPGradientImage)) +#define SP_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_IMAGE, SPGradientImageClass)) +#define SP_IS_GRADIENT_IMAGE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_IMAGE)) +#define SP_IS_GRADIENT_IMAGE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_IMAGE)) + +struct SPGradientImage { + GtkWidget widget; + SPGradient *gradient; + + sigc::connection release_connection; + sigc::connection modified_connection; +}; + +struct SPGradientImageClass { + GtkWidgetClass parent_class; +}; + +GType sp_gradient_image_get_type (); + +GtkWidget *sp_gradient_image_new (SPGradient *gradient); +GdkPixbuf *sp_gradient_to_pixbuf (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradient_to_pixbuf_ref (SPGradient *gr, int width, int height); +Glib::RefPtr<Gdk::Pixbuf> sp_gradstop_to_pixbuf_ref (SPStop *gr, int width, int height); +void sp_gradient_image_set_gradient (SPGradientImage *gi, SPGradient *gr); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/widgets/gradient-selector.cpp b/src/widgets/gradient-selector.cpp new file mode 100644 index 0000000..387f077 --- /dev/null +++ b/src/widgets/gradient-selector.cpp @@ -0,0 +1,660 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/treeview.h> +#include <vector> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-vector.h" +#include "id-clash.h" +#include "inkscape.h" +#include "paint-selector.h" +#include "preferences.h" +#include "verbs.h" + +#include "object/sp-defs.h" +#include "style.h" + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include "ui/icon-names.h" + +enum { + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + LAST_SIGNAL +}; + + +static void sp_gradient_selector_dispose(GObject *object); + +/* Signal handlers */ +static void sp_gradient_selector_vector_set (SPGradientVectorSelector *gvs, SPGradient *gr, SPGradientSelector *sel); +static void sp_gradient_selector_edit_vector_clicked (GtkWidget *w, SPGradientSelector *sel); +static void sp_gradient_selector_add_vector_clicked (GtkWidget *w, SPGradientSelector *sel); +static void sp_gradient_selector_delete_vector_clicked (GtkWidget *w, SPGradientSelector *sel); + +static guint signals[LAST_SIGNAL] = {0}; + +G_DEFINE_TYPE(SPGradientSelector, sp_gradient_selector, GTK_TYPE_BOX); + +static void sp_gradient_selector_class_init(SPGradientSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + signals[GRABBED] = g_signal_new ("grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[DRAGGED] = g_signal_new ("dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[RELEASED] = g_signal_new ("released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + signals[CHANGED] = g_signal_new ("changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET (SPGradientSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + object_class->dispose = sp_gradient_selector_dispose; +} + +static void gradsel_style_button(GtkWidget *gtkbtn, char const *iconName) +{ + Gtk::Button *btn = Glib::wrap(GTK_BUTTON(gtkbtn)); + GtkWidget *child = sp_get_icon_image(iconName, GTK_ICON_SIZE_SMALL_TOOLBAR); + gtk_widget_show(child); + btn->add(*manage(Glib::wrap(child))); + btn->set_relief(Gtk::RELIEF_NONE); +} + +static void sp_gradient_selector_init(SPGradientSelector *sel) +{ + sel->safelyInit = true; + sel->blocked = false; + + gtk_orientable_set_orientation(GTK_ORIENTABLE(sel), GTK_ORIENTATION_VERTICAL); + + new (&sel->nonsolid) std::vector<GtkWidget*>(); + new (&sel->swatch_widgets) std::vector<GtkWidget*>(); + + sel->mode = SPGradientSelector::MODE_LINEAR; + + sel->gradientUnits = SP_GRADIENT_UNITS_USERSPACEONUSE; + sel->gradientSpread = SP_GRADIENT_SPREAD_PAD; + + /* Vectors */ + sel->vectors = sp_gradient_vector_selector_new (nullptr, nullptr); + SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(sel->vectors); + sel->store = gvs->store; + sel->columns = gvs->columns; + + sel->treeview = Gtk::manage(new Gtk::TreeView()); + sel->treeview->set_model(gvs->store); + sel->treeview->set_headers_clickable (true); + sel->treeview->set_search_column(1); + sel->treeview->set_vexpand(); + sel->icon_renderer = Gtk::manage(new Gtk::CellRendererPixbuf()); + sel->text_renderer = Gtk::manage(new Gtk::CellRendererText()); + + sel->treeview->append_column(_("Gradient"), *sel->icon_renderer); + Gtk::TreeView::Column* icon_column = sel->treeview->get_column(0); + icon_column->add_attribute(sel->icon_renderer->property_pixbuf(), sel->columns->pixbuf); + icon_column->set_sort_column(sel->columns->color); + icon_column->set_clickable(true); + + sel->treeview->append_column(_("Name"), *sel->text_renderer); + Gtk::TreeView::Column* name_column = sel->treeview->get_column(1); + sel->text_renderer->property_editable() = true; + name_column->add_attribute(sel->text_renderer->property_text(), sel->columns->name); + name_column->set_min_width(180); + name_column->set_clickable(true); + name_column->set_resizable(true); + + sel->treeview->append_column("#", sel->columns->refcount); + Gtk::TreeView::Column* count_column = sel->treeview->get_column(2); + count_column->set_clickable(true); + count_column->set_resizable(true); + + sel->treeview->signal_key_press_event().connect(sigc::mem_fun(*sel, &SPGradientSelector::onKeyPressEvent), false); + + sel->treeview->show(); + + icon_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeColorColClick) ); + name_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeNameColClick) ); + count_column->signal_clicked().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeCountColClick) ); + + gvs->tree_select_connection = sel->treeview->get_selection()->signal_changed().connect( sigc::mem_fun(*sel, &SPGradientSelector::onTreeSelection) ); + sel->text_renderer->signal_edited().connect( sigc::mem_fun(*sel, &SPGradientSelector::onGradientRename) ); + + sel->scrolled_window = Gtk::manage(new Gtk::ScrolledWindow()); + sel->scrolled_window->add(*sel->treeview); + sel->scrolled_window->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + sel->scrolled_window->set_shadow_type(Gtk::SHADOW_IN); + sel->scrolled_window->set_size_request(0, 180); + sel->scrolled_window->set_hexpand(); + sel->scrolled_window->show(); + + gtk_box_pack_start (GTK_BOX (sel), GTK_WIDGET(sel->scrolled_window->gobj()), TRUE, TRUE, 4); + + + /* Create box for buttons */ + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + gtk_box_pack_start( GTK_BOX(sel), hb, FALSE, FALSE, 0 ); + + sel->add = gtk_button_new(); + gradsel_style_button(sel->add, INKSCAPE_ICON("list-add")); + + sel->nonsolid.push_back(sel->add); + gtk_box_pack_start (GTK_BOX (hb), sel->add, FALSE, FALSE, 0); + + g_signal_connect (G_OBJECT (sel->add), "clicked", G_CALLBACK (sp_gradient_selector_add_vector_clicked), sel); + gtk_widget_set_sensitive (sel->add, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->add), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->add, _("Create a duplicate gradient")); + + sel->edit = gtk_button_new(); + gradsel_style_button(sel->edit, INKSCAPE_ICON("edit")); + + sel->nonsolid.push_back(sel->edit); + gtk_box_pack_start (GTK_BOX (hb), sel->edit, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (sel->edit), "clicked", G_CALLBACK (sp_gradient_selector_edit_vector_clicked), sel); + gtk_widget_set_sensitive (sel->edit, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->edit), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->edit, _("Edit gradient")); + + sel->del = gtk_button_new (); + gradsel_style_button(sel->del, INKSCAPE_ICON("list-remove")); + + sel->swatch_widgets.push_back(sel->del); + gtk_box_pack_start (GTK_BOX (hb), sel->del, FALSE, FALSE, 0); + g_signal_connect (G_OBJECT (sel->del), "clicked", G_CALLBACK (sp_gradient_selector_delete_vector_clicked), sel); + gtk_widget_set_sensitive (sel->del, FALSE); + gtk_button_set_relief(GTK_BUTTON(sel->del), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text( sel->del, _("Delete swatch")); + + gtk_widget_show_all(hb); +} + +static void sp_gradient_selector_dispose(GObject *object) +{ + SPGradientSelector *sel = SP_GRADIENT_SELECTOR( object ); + + if ( sel->safelyInit ) { + sel->safelyInit = false; + sel->nonsolid.~vector<GtkWidget*>(); + sel->swatch_widgets.~vector<GtkWidget*>(); + } + + if (sel->icon_renderer) { + delete sel->icon_renderer; + sel->icon_renderer = nullptr; + } + if (sel->text_renderer) { + delete sel->text_renderer; + sel->text_renderer = nullptr; + } + + if ((G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose) { + (G_OBJECT_CLASS(sp_gradient_selector_parent_class))->dispose(object); + } +} + +void SPGradientSelector::setSpread(SPGradientSpread spread) +{ + gradientSpread = spread; + //gtk_combo_box_set_active (GTK_COMBO_BOX(this->spread), gradientSpread); +} + + +GtkWidget *sp_gradient_selector_new() +{ + SPGradientSelector *sel = SP_GRADIENT_SELECTOR(g_object_new (SP_TYPE_GRADIENT_SELECTOR, nullptr)); + + return GTK_WIDGET(sel); +} + +void SPGradientSelector::setMode(SelectorMode mode) +{ + if (mode != this->mode) { + this->mode = mode; + if (mode == MODE_SWATCH) { + for (auto & it : nonsolid) + { + gtk_widget_hide(it); + } + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_show_all(swatch_widget); + } + + Gtk::TreeView::Column* icon_column = treeview->get_column(0); + icon_column->set_title(_("Swatch")); + + SPGradientVectorSelector* vs = SP_GRADIENT_VECTOR_SELECTOR(vectors); + vs->setSwatched(); + } else { + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_hide(swatch_widget); + } + Gtk::TreeView::Column* icon_column = treeview->get_column(0); + icon_column->set_title(_("Gradient")); + + } + } +} + +void SPGradientSelector::setUnits(SPGradientUnits units) +{ + gradientUnits = units; +} + +SPGradientUnits SPGradientSelector::getUnits() +{ + return gradientUnits; +} + +SPGradientSpread SPGradientSelector::getSpread() +{ + return gradientSpread; +} + +void SPGradientSelector::onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text) +{ + Gtk::TreePath path(path_string); + Gtk::TreeModel::iterator iter = store->get_iter(path); + + if( iter ) + { + Gtk::TreeModel::Row row = *iter; + if ( row ) { + SPObject* obj = row[columns->data]; + if ( obj ) { + row[columns->name] = gr_prepare_label(obj); + if (!new_text.empty() && new_text != row[columns->name]) { + rename_id(obj, new_text ); + Inkscape::DocumentUndo::done(obj->document, SP_VERB_CONTEXT_GRADIENT, + _("Rename gradient")); + } + } + } + } +} + +void SPGradientSelector::onTreeColorColClick() { + Gtk::TreeView::Column* column = treeview->get_column(0); + column->set_sort_column(columns->color); +} + +void SPGradientSelector::onTreeNameColClick() { + Gtk::TreeView::Column* column = treeview->get_column(1); + column->set_sort_column(columns->name); +} + + +void SPGradientSelector::onTreeCountColClick() { + Gtk::TreeView::Column* column = treeview->get_column(2); + column->set_sort_column(columns->refcount); +} + +void SPGradientSelector::moveSelection(int amount, bool down, bool toEnd) +{ + Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection(); + auto iter = select->get_selected(); + + if (amount < 0) { + down = !down; + amount = -amount; + } + + auto canary = iter; + if (down) { + ++canary; + } else { + --canary; + } + while (canary && (toEnd || amount > 0)) { + --amount; + if (down) { + ++canary; + ++iter; + } else { + --canary; + --iter; + } + } + + select->select(iter); + treeview->scroll_to_row(store->get_path(iter), 0.5); +} + +bool SPGradientSelector::onKeyPressEvent(GdkEventKey *event) +{ + bool consume = false; + auto display = Gdk::Display::get_default(); + auto keymap = display->get_keymap(); + guint key = 0; + gdk_keymap_translate_keyboard_state(keymap, event->hardware_keycode, + static_cast<GdkModifierType>(event->state), + 0, &key, 0, 0, 0); + + switch (key) { + case GDK_KEY_Up: + case GDK_KEY_KP_Up: { + moveSelection(-1); + consume = true; + break; + } + case GDK_KEY_Down: + case GDK_KEY_KP_Down: { + moveSelection(1); + consume = true; + break; + } + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: { + moveSelection(-5); + consume = true; + break; + } + + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: { + moveSelection(5); + consume = true; + break; + } + + case GDK_KEY_End: + case GDK_KEY_KP_End: { + moveSelection(0, true, true); + consume = true; + break; + } + + case GDK_KEY_Home: + case GDK_KEY_KP_Home: { + moveSelection(0, false, true); + consume = true; + break; + } + } + return consume; +} + +void SPGradientSelector::onTreeSelection() +{ + if (!treeview) { + return; + } + + if (blocked) { + return; + } + + if (!treeview->has_focus()) { + /* Workaround for GTK bug on Windows/OS X + * When the treeview initially doesn't have focus and is clicked + * sometimes get_selection()->signal_changed() has the wrong selection + */ + treeview->grab_focus(); + } + + const Glib::RefPtr<Gtk::TreeSelection> sel = treeview->get_selection(); + if (!sel) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = sel->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[columns->data]; + } + + if (obj) { + sp_gradient_selector_vector_set (nullptr, SP_GRADIENT(obj), this); + } +} + +bool SPGradientSelector::_checkForSelected(const Gtk::TreePath &path, const Gtk::TreeIter& iter, SPGradient *vector) +{ + bool found = false; + + Gtk::TreeModel::Row row = *iter; + if ( vector == row[columns->data] ) + { + treeview->scroll_to_row(path, 0.5); + Glib::RefPtr<Gtk::TreeSelection> select = treeview->get_selection(); + bool wasBlocked = blocked; + blocked = true; + select->select(iter); + blocked = wasBlocked; + found = true; + } + + return found; +} + +void SPGradientSelector::selectGradientInTree(SPGradient *vector) +{ + store->foreach( sigc::bind<SPGradient*>(sigc::mem_fun(*this, &SPGradientSelector::_checkForSelected), vector) ); +} + +void SPGradientSelector::setVector(SPDocument *doc, SPGradient *vector) +{ + g_return_if_fail(!vector || SP_IS_GRADIENT(vector)); + g_return_if_fail(!vector || (vector->document == doc)); + + if (vector && !vector->hasStops()) { + return; + } + + sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(vectors), doc, vector); + + selectGradientInTree(vector); + + if (vector) { + if ( (mode == MODE_SWATCH) && vector->isSwatch() ) { + if ( vector->isSolid() ) { + for (auto & it : nonsolid) + { + gtk_widget_hide(it); + } + } else { + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + } + } else if (mode != MODE_SWATCH) { + + for (auto & swatch_widget : swatch_widgets) + { + gtk_widget_hide(swatch_widget); + } + for (auto & it : nonsolid) + { + gtk_widget_show_all(it); + } + + } + + if (edit) { + gtk_widget_set_sensitive(edit, TRUE); + } + if (add) { + gtk_widget_set_sensitive(add, TRUE); + } + if (del) { + gtk_widget_set_sensitive(del, TRUE); + } + } else { + if (edit) { + gtk_widget_set_sensitive(edit, FALSE); + } + if (add) { + gtk_widget_set_sensitive(add, (doc != nullptr)); + } + if (del) { + gtk_widget_set_sensitive(del, FALSE); + } + } +} + +SPGradient *SPGradientSelector::getVector() +{ + /* fixme: */ + return SP_GRADIENT_VECTOR_SELECTOR(vectors)->gr; +} + + +static void sp_gradient_selector_vector_set(SPGradientVectorSelector * /*gvs*/, SPGradient *gr, SPGradientSelector *sel) +{ + + if (!sel->blocked) { + sel->blocked = TRUE; + gr = sp_gradient_ensure_vector_normalized (gr); + sel->setVector((gr) ? gr->document : nullptr, gr); + g_signal_emit (G_OBJECT (sel), signals[CHANGED], 0, gr); + sel->blocked = FALSE; + } +} + + +static void +sp_gradient_selector_delete_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + const Glib::RefPtr<Gtk::TreeSelection> selection = sel->treeview->get_selection(); + if (!selection) { + return; + } + + SPGradient *obj = nullptr; + /* Single selection */ + Gtk::TreeModel::iterator iter = selection->get_selected(); + if ( iter ) { + Gtk::TreeModel::Row row = *iter; + obj = row[sel->columns->data]; + } + + if (obj) { + sp_gradient_unset_swatch(SP_ACTIVE_DESKTOP, obj->getId()); + } + +} + +static void +sp_gradient_selector_edit_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/dialogs/gradienteditor/showlegacy", false)) { + // Legacy gradient dialog + GtkWidget *dialog; + dialog = sp_gradient_vector_editor_new (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)->gr); + gtk_widget_show (dialog); + } else { + // Invoke the gradient tool + Inkscape::Verb *verb = Inkscape::Verb::get( SP_VERB_CONTEXT_GRADIENT ); + if ( verb ) { + SPAction *action = verb->get_action( Inkscape::ActionContext( ( Inkscape::UI::View::View * ) SP_ACTIVE_DESKTOP ) ); + if ( action ) { + sp_action_perform( action, nullptr ); + } + } + } +} + +static void +sp_gradient_selector_add_vector_clicked (GtkWidget */*w*/, SPGradientSelector *sel) +{ + SPDocument *doc = sp_gradient_vector_selector_get_document (SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)); + + if (!doc) + return; + + SPGradient *gr = sp_gradient_vector_selector_get_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors)); + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + Inkscape::XML::Node *repr = nullptr; + + if (gr) { + repr = gr->getRepr()->duplicate(xml_doc); + // Rename the new gradients id to be similar to the cloned gradients + Glib::ustring old_id = gr->getId(); + rename_id(gr, old_id); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + } else { + repr = xml_doc->createElement("svg:linearGradient"); + Inkscape::XML::Node *stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "0"); + stop->setAttribute("style", "stop-color:#000;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + stop = xml_doc->createElement("svg:stop"); + stop->setAttribute("offset", "1"); + stop->setAttribute("style", "stop-color:#fff;stop-opacity:1;"); + repr->appendChild(stop); + Inkscape::GC::release(stop); + doc->getDefs()->getRepr()->addChild(repr, nullptr); + gr = SP_GRADIENT(doc->getObjectByRepr(repr)); + } + + sp_gradient_vector_selector_set_gradient( SP_GRADIENT_VECTOR_SELECTOR (sel->vectors), doc, gr); + + sel->selectGradientInTree(gr); + + Inkscape::GC::release(repr); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/widgets/gradient-selector.h b/src/widgets/gradient-selector.h new file mode 100644 index 0000000..c787552 --- /dev/null +++ b/src/widgets/gradient-selector.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_SELECTOR_H +#define SEEN_GRADIENT_SELECTOR_H + +/* + * Gradient vector and position widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> + +#include <vector> +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" + +class SPDocument; +class SPGradient; + +namespace Gtk { +class CellRendererPixbuf; +class CellRendererText; +class ScrolledWindow; +class TreeView; +} + +#define SP_TYPE_GRADIENT_SELECTOR (sp_gradient_selector_get_type ()) +#define SP_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelector)) +#define SP_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_SELECTOR, SPGradientSelectorClass)) +#define SP_IS_GRADIENT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_SELECTOR)) +#define SP_IS_GRADIENT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_SELECTOR)) + + + +struct SPGradientSelector { + GtkBox vbox; + + enum SelectorMode { + MODE_LINEAR, + MODE_RADIAL, + MODE_SWATCH + }; + + SelectorMode mode; + + SPGradientUnits gradientUnits; + SPGradientSpread gradientSpread; + + /* Vector selector */ + GtkWidget *vectors; + + /* Tree */ + bool _checkForSelected(const Gtk::TreePath& path, const Gtk::TreeIter& iter, SPGradient *vector); + bool onKeyPressEvent(GdkEventKey *event); + void onTreeSelection(); + void onGradientRename( const Glib::ustring& path_string, const Glib::ustring& new_text); + void onTreeNameColClick(); + void onTreeColorColClick(); + void onTreeCountColClick(); + + Gtk::TreeView *treeview; + Gtk::ScrolledWindow *scrolled_window; + class ModelColumns : public Gtk::TreeModel::ColumnRecord + { + public: + ModelColumns() + { + add(name); + add(refcount); + add(color); + add(data); + add(pixbuf); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> name; + Gtk::TreeModelColumn<unsigned long> color; + Gtk::TreeModelColumn<gint> refcount; + Gtk::TreeModelColumn<SPGradient*> data; + Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf> > pixbuf; + + }; + + ModelColumns *columns; + Glib::RefPtr<Gtk::ListStore> store; + Gtk::CellRendererPixbuf* icon_renderer; + Gtk::CellRendererText* text_renderer; + + /* Editing buttons */ + GtkWidget *edit; + GtkWidget *add; + GtkWidget *del; + GtkWidget *merge; + + /* Position widget */ + GtkWidget *position; + + bool safelyInit; + bool blocked; + + std::vector<GtkWidget*> nonsolid; + std::vector<GtkWidget*> swatch_widgets; + + void setMode(SelectorMode mode); + void setUnits(SPGradientUnits units); + void setSpread(SPGradientSpread spread); + void setVector(SPDocument *doc, SPGradient *vector); + void selectGradientInTree(SPGradient *vector); + void moveSelection(int amount, bool down = true, bool toEnd = false); + + SPGradientUnits getUnits(); + SPGradientSpread getSpread(); + SPGradient *getVector(); +}; + +struct SPGradientSelectorClass { + GtkBoxClass parent_class; + + void (* grabbed) (SPGradientSelector *sel); + void (* dragged) (SPGradientSelector *sel); + void (* released) (SPGradientSelector *sel); + void (* changed) (SPGradientSelector *sel); +}; + +GType sp_gradient_selector_get_type(); + +GtkWidget *sp_gradient_selector_new (); + +void sp_gradient_selector_set_bbox (SPGradientSelector *sel, gdouble x0, gdouble y0, gdouble x1, gdouble y1); + +#endif // SEEN_GRADIENT_SELECTOR_H + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/widgets/gradient-vector.cpp b/src/widgets/gradient-vector.cpp new file mode 100644 index 0000000..a2ca973 --- /dev/null +++ b/src/widgets/gradient-vector.cpp @@ -0,0 +1,1314 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Gradient vector selection widget + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * MenTaLguY <mental@rydia.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 Monash University + * Copyright (C) 2004 David Turner + * Copyright (C) 2006 MenTaLguY + * Copyright (C) 2010 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <set> + +#include <glibmm.h> +#include <glibmm/i18n.h> + + + + +#include "gradient-chemistry.h" +#include "inkscape.h" +#include "preferences.h" +#include "desktop.h" +#include "document-undo.h" +#include "gradient-vector.h" +#include "layer-manager.h" +#include "include/macros.h" +#include "selection-chemistry.h" +#include "verbs.h" + +#include "io/resource.h" + +#include "object/sp-defs.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-stop.h" +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/dialog-events.h" +#include "ui/selected-color.h" +#include "ui/widget/color-notebook.h" +#include "ui/widget/color-preview.h" + +#include "widgets/gradient-image.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; +using Inkscape::UI::SelectedColor; + +enum { + VECTOR_SET, + LAST_SIGNAL +}; + +static void sp_gradient_vector_selector_destroy(GtkWidget *object); + +static void sp_gvs_gradient_release(SPObject *obj, SPGradientVectorSelector *gvs); +static void sp_gvs_defs_release(SPObject *defs, SPGradientVectorSelector *gvs); +static void sp_gvs_defs_modified(SPObject *defs, guint flags, SPGradientVectorSelector *gvs); + +static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs); +static SPStop *get_selected_stop( GtkWidget *vb); +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ); +unsigned long sp_gradient_to_hhssll(SPGradient *gr); + +static guint signals[LAST_SIGNAL] = {0}; + +// TODO FIXME kill these globals!!! +static GtkWidget *dlg = nullptr; +static win_data wd; +static gint x = -1000, y = -1000, w = 0, h = 0; // impossible original values to make sure they are read from prefs +static Glib::ustring const prefs_path = "/dialogs/gradienteditor/"; + +G_DEFINE_TYPE(SPGradientVectorSelector, sp_gradient_vector_selector, GTK_TYPE_BOX); + +static void sp_gradient_vector_selector_class_init(SPGradientVectorSelectorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + + signals[VECTOR_SET] = g_signal_new( "vector_set", + G_TYPE_FROM_CLASS(gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(SPGradientVectorSelectorClass, vector_set), + nullptr, nullptr, + g_cclosure_marshal_VOID__POINTER, + G_TYPE_NONE, 1, + G_TYPE_POINTER); + + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + widget_class->destroy = sp_gradient_vector_selector_destroy; +} + +static void sp_gradient_vector_selector_init(SPGradientVectorSelector *gvs) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(gvs), GTK_ORIENTATION_VERTICAL); + + gvs->idlabel = TRUE; + + gvs->swatched = false; + + gvs->doc = nullptr; + gvs->gr = nullptr; + + new (&gvs->gradient_release_connection) sigc::connection(); + new (&gvs->defs_release_connection) sigc::connection(); + new (&gvs->defs_modified_connection) sigc::connection(); + + gvs->columns = new SPGradientSelector::ModelColumns(); + gvs->store = Gtk::ListStore::create(*gvs->columns); + new (&gvs->tree_select_connection) sigc::connection(); + +} + +static void sp_gradient_vector_selector_destroy(GtkWidget *object) +{ + SPGradientVectorSelector *gvs = SP_GRADIENT_VECTOR_SELECTOR(object); + + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->tree_select_connection.disconnect(); + gvs->gr = nullptr; + } + + if (gvs->doc) { + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + gvs->doc = nullptr; + } + + gvs->gradient_release_connection.~connection(); + gvs->defs_release_connection.~connection(); + gvs->defs_modified_connection.~connection(); + gvs->tree_select_connection.~connection(); + + if ((GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy) { + (GTK_WIDGET_CLASS(sp_gradient_vector_selector_parent_class))->destroy(object); + } +} + +GtkWidget *sp_gradient_vector_selector_new(SPDocument *doc, SPGradient *gr) +{ + GtkWidget *gvs; + + g_return_val_if_fail(!gr || SP_IS_GRADIENT(gr), NULL); + g_return_val_if_fail(!gr || (gr->document == doc), NULL); + + gvs = static_cast<GtkWidget*>(g_object_new(SP_TYPE_GRADIENT_VECTOR_SELECTOR, nullptr)); + + if (doc) { + sp_gradient_vector_selector_set_gradient(SP_GRADIENT_VECTOR_SELECTOR(gvs), doc, gr); + } else { + sp_gvs_rebuild_gui_full(SP_GRADIENT_VECTOR_SELECTOR(gvs)); + } + + return gvs; +} + +void sp_gradient_vector_selector_set_gradient(SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr) +{ +// g_message("sp_gradient_vector_selector_set_gradient(%p, %p, %p) [%s] %d %d", gvs, doc, gr, +// (gr ? gr->getId():"N/A"), +// (gr ? gr->isSwatch() : -1), +// (gr ? gr->isSolid() : -1)); + static gboolean suppress = FALSE; + + g_return_if_fail(gvs != nullptr); + g_return_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs)); + g_return_if_fail(!gr || (doc != nullptr)); + g_return_if_fail(!gr || SP_IS_GRADIENT(gr)); + g_return_if_fail(!gr || (gr->document == doc)); + g_return_if_fail(!gr || gr->hasStops()); + + if (doc != gvs->doc) { + /* Disconnect signals */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + if (gvs->doc) { + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + gvs->doc = nullptr; + } + + // Connect signals + if (doc) { + gvs->defs_release_connection = doc->getDefs()->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_defs_release), gvs)); + gvs->defs_modified_connection = doc->getDefs()->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gvs_defs_modified), gvs)); + } + if (gr) { + gvs->gradient_release_connection = gr->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gvs_gradient_release), gvs)); + } + gvs->doc = doc; + gvs->gr = gr; + sp_gvs_rebuild_gui_full(gvs); + if (!suppress) g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr); + } else if (gr != gvs->gr) { + // Harder case - keep document, rebuild list and stuff + // fixme: (Lauris) + suppress = TRUE; + sp_gradient_vector_selector_set_gradient(gvs, nullptr, nullptr); + sp_gradient_vector_selector_set_gradient(gvs, doc, gr); + suppress = FALSE; + g_signal_emit(G_OBJECT(gvs), signals[VECTOR_SET], 0, gr); + } + /* The case of setting NULL -> NULL is not very interesting */ +} + +SPDocument *sp_gradient_vector_selector_get_document(SPGradientVectorSelector *gvs) +{ + g_return_val_if_fail(gvs != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL); + + return gvs->doc; +} + +SPGradient *sp_gradient_vector_selector_get_gradient(SPGradientVectorSelector *gvs) +{ + g_return_val_if_fail(gvs != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT_VECTOR_SELECTOR(gvs), NULL); + + return gvs->gr; +} + +Glib::ustring gr_prepare_label (SPObject *obj) +{ + const gchar *id = obj->label() ? obj->label() : obj->getId(); + if (!id) { + id = obj->getRepr()->name(); + } + + if (strlen(id) > 14 && (!strncmp (id, "linearGradient", 14) || !strncmp (id, "radialGradient", 14))) + return gr_ellipsize_text (g_strdup_printf ("%s", id+14), 35); + return gr_ellipsize_text (id, 35); +} + +/* + * Ellipse text if longer than maxlen, "50% start text + ... + ~50% end text" + * Text should be > length 8 or just return the original text + */ +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen) +{ + if (src.length() > maxlen && maxlen > 8) { + size_t p1 = (size_t) maxlen / 2; + size_t p2 = (size_t) src.length() - (maxlen - p1 - 1); + return src.substr(0, p1) + "…" + src.substr(p2); + } + return src; +} + +static void sp_gvs_rebuild_gui_full(SPGradientVectorSelector *gvs) +{ + + gvs->tree_select_connection.block(); + + /* Clear old list, if there is any */ + gvs->store->clear(); + + /* Pick up all gradients with vectors */ + std::vector<SPGradient *> gl; + if (gvs->gr) { + std::vector<SPObject *> gradients = gvs->gr->document->getResourceList("gradient"); + for (auto gradient : gradients) { + SPGradient* grad = SP_GRADIENT(gradient); + if ( grad->hasStops() && (grad->isSwatch() == gvs->swatched) ) { + gl.push_back(SP_GRADIENT(gradient)); + } + } + } + + /* Get usage count of all the gradients */ + std::map<SPGradient *, gint> usageCount; + gr_get_usage_counts(gvs->doc, &usageCount); + + if (!gvs->doc) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No document selected"); + + } else if (gl.empty()) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No gradients in document"); + + } else if (!gvs->gr) { + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = _("No gradient selected"); + + } else { + for (auto gr:gl) { + unsigned long hhssll = sp_gradient_to_hhssll(gr); + GdkPixbuf *pixb = sp_gradient_to_pixbuf (gr, 64, 18); + Glib::ustring label = gr_prepare_label(gr); + + Gtk::TreeModel::Row row = *(gvs->store->append()); + row[gvs->columns->name] = label.c_str(); + row[gvs->columns->color] = hhssll; + row[gvs->columns->refcount] = usageCount[gr]; + row[gvs->columns->data] = gr; + row[gvs->columns->pixbuf] = Glib::wrap(pixb); + } + } + + gvs->tree_select_connection.unblock(); + +} + +/* + * Return a "HHSSLL" version of the first stop color so we can sort by it + */ +unsigned long sp_gradient_to_hhssll(SPGradient *gr) +{ + SPStop *stop = gr->getFirstStop(); + unsigned long rgba = stop->get_rgba32(); + float hsl[3]; + SPColor::rgb_to_hsl_floatv (hsl, SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba)); + + return ((int)(hsl[0]*100 * 10000)) + ((int)(hsl[1]*100 * 100)) + ((int)(hsl[2]*100 * 1)); +} + +static void get_all_doc_items(std::vector<SPItem*> &list, SPObject *from) +{ + for (auto& child: from->children) { + if (SP_IS_ITEM(&child)) { + list.push_back(SP_ITEM(&child)); + } + get_all_doc_items(list, &child); + } +} + +/* + * Return a SPItem's gradient + */ +static SPGradient * gr_item_get_gradient(SPItem *item, gboolean fillorstroke) +{ + SPIPaint *item_paint = item->style->getFillOrStroke(fillorstroke); + if (item_paint->isPaintserver()) { + + SPPaintServer *item_server = (fillorstroke) ? + item->style->getFillPaintServer() : item->style->getStrokePaintServer(); + + if (SP_IS_LINEARGRADIENT(item_server) || SP_IS_RADIALGRADIENT(item_server) || + (SP_IS_GRADIENT(item_server) && SP_GRADIENT(item_server)->getVector()->isSwatch())) { + + return SP_GRADIENT(item_server)->getVector(); + } + } + + return nullptr; +} + +/* + * Map each gradient to its usage count for both fill and stroke styles + */ +void gr_get_usage_counts(SPDocument *doc, std::map<SPGradient *, gint> *mapUsageCount ) +{ + if (!doc) + return; + + std::vector<SPItem *> all_list; + get_all_doc_items(all_list, doc->getRoot()); + + for (auto item:all_list) { + if (!item->getId()) + continue; + SPGradient *gr = nullptr; + gr = gr_item_get_gradient(item, true); // fill + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + gr = gr_item_get_gradient(item, false); // stroke + if (gr) { + mapUsageCount->count(gr) > 0 ? (*mapUsageCount)[gr] += 1 : (*mapUsageCount)[gr] = 1; + } + } +} + +static void sp_gvs_gradient_release(SPObject */*obj*/, SPGradientVectorSelector *gvs) +{ + /* Disconnect gradient */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + + /* Rebuild GUI */ + sp_gvs_rebuild_gui_full(gvs); +} + +static void sp_gvs_defs_release(SPObject */*defs*/, SPGradientVectorSelector *gvs) +{ + gvs->doc = nullptr; + + gvs->defs_release_connection.disconnect(); + gvs->defs_modified_connection.disconnect(); + + /* Disconnect gradient as well */ + if (gvs->gr) { + gvs->gradient_release_connection.disconnect(); + gvs->gr = nullptr; + } + + /* Rebuild GUI */ + sp_gvs_rebuild_gui_full(gvs); +} + +static void sp_gvs_defs_modified(SPObject */*defs*/, guint /*flags*/, SPGradientVectorSelector *gvs) +{ + /* fixme: We probably have to check some flags here (Lauris) */ + sp_gvs_rebuild_gui_full(gvs); +} + +void SPGradientVectorSelector::setSwatched() +{ + swatched = true; + sp_gvs_rebuild_gui_full(this); +} + +/*################################################################## + ### Vector Editing Widget + ##################################################################*/ + +#include "widgets/widget-sizes.h" +#include "xml/node-event-vector.h" +#include "svg/svg-color.h" + +#define PAD 4 + +static GtkWidget *sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *stop); + +static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient); +static gint sp_gradient_vector_dialog_delete(GtkWidget *widget, GdkEvent *event, GtkWidget *dialog); +static void sp_gradient_vector_dialog_destroy(GtkWidget *object, gpointer data); +static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer data); +static void sp_gradient_vector_gradient_release(SPObject *obj, GtkWidget *widget); +static void sp_gradient_vector_gradient_modified(SPObject *obj, guint flags, GtkWidget *widget); +static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object); +static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object); +static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop); + +static gboolean blocked = FALSE; + +static void grad_edit_dia_stop_added_or_removed(Inkscape::XML::Node */*repr*/, Inkscape::XML::Node */*child*/, Inkscape::XML::Node */*ref*/, gpointer data) +{ + GtkWidget *vb = GTK_WIDGET(data); + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + update_stop_list(vb, gradient, nullptr); +} + +//FIXME!!! We must also listen to attr changes on all children (i.e. stops) too, +//otherwise the dialog does not reflect undoing color or offset change. This is a major +//hassle, unless we have a "one of the descendants changed in some way" signal. +static Inkscape::XML::NodeEventVector grad_edit_dia_repr_events = +{ + grad_edit_dia_stop_added_or_removed, /* child_added */ + grad_edit_dia_stop_added_or_removed, /* child_removed */ + nullptr, /* attr_changed*/ + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + +static void verify_grad(SPGradient *gradient) +{ + int i = 0; + SPStop *stop = nullptr; + /* count stops */ + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + i++; + stop = SP_STOP(&ochild); + } + } + + Inkscape::XML::Document *xml_doc; + xml_doc = gradient->getRepr()->document(); + + if (i < 1) { + Inkscape::CSSOStringStream os; + os << "stop-color: #000000;stop-opacity:" << 1.0 << ";"; + + Inkscape::XML::Node *child; + + child = xml_doc->createElement("svg:stop"); + sp_repr_set_css_double(child, "offset", 0.0); + child->setAttribute("style", os.str()); + gradient->getRepr()->addChild(child, nullptr); + Inkscape::GC::release(child); + + child = xml_doc->createElement("svg:stop"); + sp_repr_set_css_double(child, "offset", 1.0); + child->setAttribute("style", os.str()); + gradient->getRepr()->addChild(child, nullptr); + Inkscape::GC::release(child); + return; + } + if (i < 2) { + sp_repr_set_css_double(stop->getRepr(), "offset", 0.0); + Inkscape::XML::Node *child = stop->getRepr()->duplicate(gradient->getRepr()->document()); + sp_repr_set_css_double(child, "offset", 1.0); + gradient->getRepr()->addChild(child, stop->getRepr()); + Inkscape::GC::release(child); + } +} + +static void select_stop_in_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop) +{ + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + + int i = 0; + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + if (&ochild == new_stop) { + gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , i); + break; + } + i++; + } + } +} + +static void update_stop_list( GtkWidget *vb, SPGradient *gradient, SPStop *new_stop) +{ + + if (!SP_IS_GRADIENT(gradient)) { + return; + } + + blocked = TRUE; + + /* Clear old list, if there is any */ + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + if (!combo_box) { + return; + } + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + if (!store) { + return; + } + gtk_list_store_clear(store); + GtkTreeIter iter; + + /* Populate the combobox store */ + std::vector<SPStop *> sl; + if ( gradient->hasStops() ) { + for (auto& ochild: gradient->children) { + if (SP_IS_STOP(&ochild)) { + sl.push_back(SP_STOP(&ochild)); + } + } + } + if (sl.empty()) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, NULL, 1, _("No stops in gradient"), 2, NULL, -1); + gtk_widget_set_sensitive (combo_box, FALSE); + + } else { + + for (auto stop:sl) { + Inkscape::XML::Node *repr = stop->getRepr(); + Inkscape::UI::Widget::ColorPreview *cpv = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32())); + GdkPixbuf *pb = cpv->toPixbuf(64, 16); + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, pb, 1, repr->attribute("id"), 2, stop, -1); + gtk_widget_set_sensitive (combo_box, FALSE); + } + gtk_widget_set_sensitive(combo_box, TRUE); + } + + /* Set history */ + if (new_stop == nullptr) { + gtk_combo_box_set_active (GTK_COMBO_BOX(combo_box) , 0); + } else { + select_stop_in_list(vb, gradient, new_stop); + } + + blocked = FALSE; +} + + +// user selected existing stop from list +static void sp_grad_edit_combo_box_changed (GtkComboBox * /*widget*/, GtkWidget *tbl) +{ + SPStop *stop = get_selected_stop(tbl); + if (!stop) { + return; + } + + blocked = TRUE; + + SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(tbl), "cselector")); + // set its color, from the stored array + g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(1)); + csel->setColorAlpha(stop->getColor(), stop->getOpacity()); + g_object_set_data(G_OBJECT(tbl), "updating_color", reinterpret_cast<void*>(0)); + GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offspn")); + GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(tbl), "offslide")); + + GtkAdjustment *adj = static_cast<GtkAdjustment*>(g_object_get_data(G_OBJECT(tbl), "offset")); + + bool isEndStop = false; + + SPStop *prev = nullptr; + prev = stop->getPrevStop(); + if (prev != nullptr ) { + gtk_adjustment_set_lower (adj, prev->offset); + } else { + isEndStop = true; + gtk_adjustment_set_lower (adj, 0); + } + + SPStop *next = nullptr; + next = stop->getNextStop(); + if (next != nullptr ) { + gtk_adjustment_set_upper (adj, next->offset); + } else { + isEndStop = true; + gtk_adjustment_set_upper (adj, 1.0); + } + + //fixme: does this work on all possible input gradients? + if (!isEndStop) { + gtk_widget_set_sensitive(offslide, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE); + } else { + gtk_widget_set_sensitive(offslide, FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), FALSE); + } + + gtk_adjustment_set_value(adj, stop->offset); + + blocked = FALSE; +} + +static SPStop *get_selected_stop( GtkWidget *vb) +{ + SPStop *stop = nullptr; + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(vb), "combo_box")); + if (combo_box) { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + gtk_tree_model_get (GTK_TREE_MODEL(store), &iter, 2, &stop, -1); + } + } + return stop; +} + +static void offadjustmentChanged( GtkAdjustment *adjustment, GtkWidget *vb) +{ + if (!blocked) { + blocked = TRUE; + + SPStop *stop = get_selected_stop(vb); + if (stop) { + stop->offset = gtk_adjustment_get_value (adjustment); + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + + DocumentUndo::maybeDone(stop->document, "gradient:stop:offset", SP_VERB_CONTEXT_GRADIENT, + _("Change gradient stop offset")); + + } + + blocked = FALSE; + } +} + +guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p/* = 0.5*/) +{ + guint32 r = (guint32) (SP_RGBA32_R_U(c1) * p + SP_RGBA32_R_U(c2) * (1 - p)); + guint32 g = (guint32) (SP_RGBA32_G_U(c1) * p + SP_RGBA32_G_U(c2) * (1 - p)); + guint32 b = (guint32) (SP_RGBA32_B_U(c1) * p + SP_RGBA32_B_U(c2) * (1 - p)); + guint32 a = (guint32) (SP_RGBA32_A_U(c1) * p + SP_RGBA32_A_U(c2) * (1 - p)); + + return SP_RGBA32_U_COMPOSE(r, g, b, a); +} + + +static void sp_grd_ed_add_stop(GtkWidget */*widget*/, GtkWidget *vb) +{ + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + verify_grad(gradient); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return; + } + + Inkscape::XML::Node *new_stop_repr = nullptr; + + SPStop *next = stop->getNextStop(); + + if (next == nullptr) { + SPStop *prev = stop->getPrevStop(); + if (prev != nullptr) { + next = stop; + stop = prev; + } + } + + if (next != nullptr) { + new_stop_repr = stop->getRepr()->duplicate(gradient->getRepr()->document()); + gradient->getRepr()->addChild(new_stop_repr, stop->getRepr()); + } else { + next = stop; + new_stop_repr = stop->getPrevStop()->getRepr()->duplicate(gradient->getRepr()->document()); + gradient->getRepr()->addChild(new_stop_repr, stop->getPrevStop()->getRepr()); + } + + SPStop *newstop = reinterpret_cast<SPStop *>(gradient->document->getObjectByRepr(new_stop_repr)); + + newstop->offset = (stop->offset + next->offset) * 0.5 ; + + guint32 const c1 = stop->get_rgba32(); + guint32 const c2 = next->get_rgba32(); + guint32 cnew = sp_average_color(c1, c2); + + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color(c, sizeof(c), cnew); + gdouble opacity = static_cast<gdouble>(SP_RGBA32_A_F(cnew)); + os << "stop-color:" << c << ";stop-opacity:" << opacity <<";"; + newstop->setAttribute("style", os.str()); + sp_repr_set_css_double( newstop->getRepr(), "offset", (double)newstop->offset); + + sp_gradient_vector_widget_load_gradient(vb, gradient); + Inkscape::GC::release(new_stop_repr); + update_stop_list(GTK_WIDGET(vb), gradient, newstop); + GtkWidget *offspin = GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offspn")); + GtkWidget *offslide =GTK_WIDGET(g_object_get_data(G_OBJECT(vb), "offslide")); + gtk_widget_set_sensitive(offslide, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(offspin), TRUE); + DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT, + _("Add gradient stop")); +} + +static void sp_grd_ed_del_stop(GtkWidget */*widget*/, GtkWidget *vb) +{ + SPGradient *gradient = static_cast<SPGradient *>(g_object_get_data(G_OBJECT(vb), "gradient")); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return; + } + + if (gradient->vector.stops.size() > 2) { // 2 is the minimum + + // if we delete first or last stop, move the next/previous to the edge + if (stop->offset == 0) { + SPStop *next = stop->getNextStop(); + if (next) { + next->offset = 0; + sp_repr_set_css_double(next->getRepr(), "offset", 0); + } + } else if (stop->offset == 1) { + SPStop *prev = stop->getPrevStop(); + if (prev) { + prev->offset = 1; + sp_repr_set_css_double(prev->getRepr(), "offset", 1); + } + } + + gradient->getRepr()->removeChild(stop->getRepr()); + sp_gradient_vector_widget_load_gradient(vb, gradient); + update_stop_list(GTK_WIDGET(vb), gradient, nullptr); + DocumentUndo::done(gradient->document, SP_VERB_CONTEXT_GRADIENT, + _("Delete gradient stop")); + } + +} + +static GtkWidget * sp_gradient_vector_widget_new(SPGradient *gradient, SPStop *select_stop) +{ + using Inkscape::UI::Widget::ColorNotebook; + + GtkWidget *vb, *w, *f; + + g_return_val_if_fail(gradient != nullptr, NULL); + g_return_val_if_fail(SP_IS_GRADIENT(gradient), NULL); + + vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, PAD); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + g_signal_connect(G_OBJECT(vb), "destroy", G_CALLBACK(sp_gradient_vector_widget_destroy), NULL); + + w = sp_gradient_image_new(gradient); + g_object_set_data(G_OBJECT(vb), "preview", w); + gtk_widget_show(w); + gtk_box_pack_start(GTK_BOX(vb), w, TRUE, TRUE, PAD); + + sp_repr_add_listener(gradient->getRepr(), &grad_edit_dia_repr_events, vb); + + /* ComboBox of stops with 3 columns, + * The color preview, the label and a pointer to the SPStop + */ + GtkListStore *store = gtk_list_store_new (3, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_POINTER); + GtkWidget *combo_box = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + + GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "pixbuf", 0, NULL); + gtk_cell_renderer_set_padding(renderer, 5, 0); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo_box), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo_box), renderer, "text", 1, NULL); + gtk_widget_show(combo_box); + gtk_box_pack_start(GTK_BOX(vb), combo_box, FALSE, FALSE, 0); + g_object_set_data(G_OBJECT(vb), "combo_box", combo_box); + + update_stop_list(GTK_WIDGET(vb), gradient, nullptr); + + g_signal_connect(G_OBJECT(combo_box), "changed", G_CALLBACK(sp_grad_edit_combo_box_changed), vb); + + /* Add and Remove buttons */ + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + // TRANSLATORS: "Stop" means: a "phase" of a gradient + GtkWidget *b = gtk_button_new_with_label(_("Add stop")); + gtk_widget_show(b); + gtk_container_add(GTK_CONTAINER(hb), b); + gtk_widget_set_tooltip_text(b, _("Add another control stop to gradient")); + g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_add_stop), vb); + b = gtk_button_new_with_label(_("Delete stop")); + gtk_widget_show(b); + gtk_container_add(GTK_CONTAINER(hb), b); + gtk_widget_set_tooltip_text(b, _("Delete current control stop from gradient")); + g_signal_connect(G_OBJECT(b), "clicked", G_CALLBACK(sp_grd_ed_del_stop), vb); + + gtk_widget_show(hb); + gtk_box_pack_start(GTK_BOX(vb),hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + /* Offset Slider and stuff */ + hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /* Label */ + GtkWidget *l = gtk_label_new(C_("Gradient","Offset:")); + gtk_widget_set_halign(l, GTK_ALIGN_END); + + gtk_box_pack_start(GTK_BOX(hb),l, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_widget_show(l); + + /* Adjustment */ + GtkAdjustment *Offset_adj = nullptr; + Offset_adj= GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 1.0, 0.01, 0.01, 0.0)); + g_object_set_data(G_OBJECT(vb), "offset", Offset_adj); + + SPStop *stop = get_selected_stop(vb); + if (!stop) { + return nullptr; + } + + gtk_adjustment_set_value(Offset_adj, stop->offset); + + /* Slider */ + auto slider = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL, Offset_adj); + gtk_scale_set_draw_value( GTK_SCALE(slider), FALSE ); + gtk_widget_show(slider); + gtk_box_pack_start(GTK_BOX(hb),slider, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + g_object_set_data(G_OBJECT(vb), "offslide", slider); + + /* Spinbutton */ + GtkWidget *sbtn = gtk_spin_button_new(GTK_ADJUSTMENT(Offset_adj), 0.01, 2); + sp_dialog_defocus_on_enter(sbtn); + gtk_widget_show(sbtn); + gtk_box_pack_start(GTK_BOX(hb),sbtn, FALSE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + g_object_set_data(G_OBJECT(vb), "offspn", sbtn); + + if (stop->offset>0 && stop->offset<1) { + gtk_widget_set_sensitive(slider, TRUE); + gtk_widget_set_sensitive(GTK_WIDGET(sbtn), TRUE); + } else { + gtk_widget_set_sensitive(slider, FALSE); + gtk_widget_set_sensitive(GTK_WIDGET(sbtn), FALSE); + } + + + /* Signals */ + g_signal_connect(G_OBJECT(Offset_adj), "value_changed", + G_CALLBACK(offadjustmentChanged), vb); + + // g_signal_connect(G_OBJECT(slider), "changed", G_CALLBACK(offsliderChanged), vb); + gtk_widget_show(hb); + gtk_box_pack_start(GTK_BOX(vb), hb, FALSE, FALSE, PAD); + + // TRANSLATORS: "Stop" means: a "phase" of a gradient + f = gtk_frame_new(_("Stop Color")); + gtk_widget_show(f); + gtk_box_pack_start(GTK_BOX(vb), f, TRUE, TRUE, PAD); + + Inkscape::UI::SelectedColor *selected_color = new Inkscape::UI::SelectedColor; + g_object_set_data(G_OBJECT(vb), "cselector", selected_color); + g_object_set_data(G_OBJECT(vb), "updating_color", reinterpret_cast<void*>(0)); + selected_color->signal_changed.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb))); + selected_color->signal_dragged.connect(sigc::bind(sigc::ptr_fun(&sp_gradient_vector_color_changed), selected_color, G_OBJECT(vb))); + + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*selected_color)); + color_selector->show(); + gtk_container_add(GTK_CONTAINER(f), color_selector->gobj()); + + /* + gtk_widget_show(csel); + gtk_container_add(GTK_CONTAINER(f), csel); + g_signal_connect(G_OBJECT(csel), "dragged", G_CALLBACK(sp_gradient_vector_color_dragged), vb); + g_signal_connect(G_OBJECT(csel), "changed", G_CALLBACK(sp_gradient_vector_color_changed), vb); + */ + + gtk_widget_show(vb); + + sp_gradient_vector_widget_load_gradient(vb, gradient); + + if (select_stop) { + select_stop_in_list(GTK_WIDGET(vb), gradient, select_stop); + } + + return vb; +} + + + +GtkWidget * sp_gradient_vector_editor_new(SPGradient *gradient, SPStop *stop) +{ + if (dlg == nullptr) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + dlg = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title ((GtkWindow *) dlg, _("Gradient editor")); + gtk_window_set_resizable ((GtkWindow *) dlg, true); + + if (x == -1000 || y == -1000) { + x = prefs->getInt(prefs_path + "x", -1000); + y = prefs->getInt(prefs_path + "y", -1000); + } + if (w ==0 || h == 0) { + w = prefs->getInt(prefs_path + "w", 0); + h = prefs->getInt(prefs_path + "h", 0); + } + + if (x<0) { + x=0; + } + if (y<0) { + y=0; + } + + if (x != 0 || y != 0) { + gtk_window_move(reinterpret_cast<GtkWindow *>(dlg), x, y); + } else { + gtk_window_set_position(GTK_WINDOW(dlg), GTK_WIN_POS_CENTER); + } + if (w && h) { + gtk_window_resize(reinterpret_cast<GtkWindow *>(dlg), w, h); + } + sp_transientize(dlg); + wd.win = dlg; + wd.stop = 0; + + GObject *obj = G_OBJECT(dlg); + sigc::connection *conn = nullptr; + + conn = new sigc::connection(INKSCAPE.signal_activate_desktop.connect(sigc::bind(sigc::ptr_fun(&sp_transientize_callback), &wd))); + g_object_set_data(obj, "desktop-activate-connection", conn); + + g_signal_connect(obj, "event", G_CALLBACK(sp_dialog_event_handler), dlg); + g_signal_connect(obj, "destroy", G_CALLBACK(sp_gradient_vector_dialog_destroy), dlg); + g_signal_connect(obj, "delete_event", G_CALLBACK(sp_gradient_vector_dialog_delete), dlg); + + conn = new sigc::connection(INKSCAPE.signal_shut_down.connect( + sigc::hide_return( + sigc::bind(sigc::ptr_fun(&sp_gradient_vector_dialog_delete), (GtkWidget *) nullptr, (GdkEvent *) nullptr, (GtkWidget *) nullptr) + ))); + g_object_set_data(obj, "shutdown-connection", conn); + + conn = new sigc::connection(INKSCAPE.signal_dialogs_hide.connect(sigc::bind(sigc::ptr_fun(>k_widget_hide), dlg))); + g_object_set_data(obj, "dialog-hide-connection", conn); + + conn = new sigc::connection(INKSCAPE.signal_dialogs_unhide.connect(sigc::bind(sigc::ptr_fun(>k_widget_show), dlg))); + g_object_set_data(obj, "dialog-unhide-connection", conn); + + gtk_container_set_border_width(GTK_CONTAINER(dlg), PAD); + + GtkWidget *wid = static_cast<GtkWidget*>(sp_gradient_vector_widget_new(gradient, stop)); + g_object_set_data(G_OBJECT(dlg), "gradient-vector-widget", wid); + /* Connect signals */ + gtk_widget_show(wid); + gtk_container_add(GTK_CONTAINER(dlg), wid); + } else { + // FIXME: temp fix for 0.38 + // Simply load_gradient into the editor does not work for multi-stop gradients, + // as the stop list and other widgets are in a wrong state and crash readily. + // Instead we just delete the window (by sending the delete signal) + // and call sp_gradient_vector_editor_new again, so it creates the window anew. + + GdkEventAny event; + GtkWidget *widget = static_cast<GtkWidget *>(dlg); + event.type = GDK_DELETE; + event.window = gtk_widget_get_window (widget); + event.send_event = TRUE; + g_object_ref(G_OBJECT(event.window)); + gtk_main_do_event(reinterpret_cast<GdkEvent*>(&event)); + g_object_unref(G_OBJECT(event.window)); + + g_assert(dlg == nullptr); + sp_gradient_vector_editor_new(gradient, stop); + } + + return dlg; +} + +static void sp_gradient_vector_widget_load_gradient(GtkWidget *widget, SPGradient *gradient) +{ + blocked = TRUE; + + SPGradient *old; + + old = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(widget), "gradient")); + + if (old != gradient) { + sigc::connection *release_connection; + sigc::connection *modified_connection; + + release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_release_connection")); + modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(widget), "gradient_modified_connection")); + + if (old) { + g_assert( release_connection != nullptr ); + g_assert( modified_connection != nullptr ); + release_connection->disconnect(); + modified_connection->disconnect(); + sp_signal_disconnect_by_data(old, widget); + } + + if (gradient) { + if (!release_connection) { + release_connection = new sigc::connection(); + } + if (!modified_connection) { + modified_connection = new sigc::connection(); + } + *release_connection = gradient->connectRelease(sigc::bind<1>(sigc::ptr_fun(&sp_gradient_vector_gradient_release), widget)); + *modified_connection = gradient->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_gradient_vector_gradient_modified), widget)); + } else { + if (release_connection) { + delete release_connection; + release_connection = nullptr; + } + if (modified_connection) { + delete modified_connection; + modified_connection = nullptr; + } + } + + g_object_set_data(G_OBJECT(widget), "gradient_release_connection", release_connection); + g_object_set_data(G_OBJECT(widget), "gradient_modified_connection", modified_connection); + } + + g_object_set_data(G_OBJECT(widget), "gradient", gradient); + + if (gradient) { + gtk_widget_set_sensitive(widget, TRUE); + + gradient->ensureVector(); + + SPStop *stop = get_selected_stop(widget); + if (!stop) { + return; + } + + // get the color selector + SelectedColor *csel = static_cast<SelectedColor*>(g_object_get_data(G_OBJECT(widget), "cselector")); + + g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(1)); + csel->setColorAlpha(stop->getColor(), stop->getOpacity()); + g_object_set_data(G_OBJECT(widget), "updating_color", reinterpret_cast<void*>(0)); + + /* Fill preview */ + GtkWidget *w = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(widget), "preview")); + sp_gradient_image_set_gradient(SP_GRADIENT_IMAGE(w), gradient); + + update_stop_list(GTK_WIDGET(widget), gradient, nullptr); + + // Once the user edits a gradient, it stops being auto-collectable + if (gradient->getRepr()->attribute("inkscape:collect")) { + SPDocument *document = gradient->document; + DocumentUndo::ScopedInsensitive _no_undo(document); + gradient->removeAttribute("inkscape:collect"); + } + } else { // no gradient, disable everything + gtk_widget_set_sensitive(widget, FALSE); + } + + blocked = FALSE; +} + +static void sp_gradient_vector_dialog_destroy(GtkWidget * /*object*/, gpointer /*data*/) +{ + GObject *obj = G_OBJECT(dlg); + assert(obj != NULL); + + sigc::connection *conn = static_cast<sigc::connection *>(g_object_get_data(obj, "desktop-activate-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "shutdown-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-hide-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + conn = static_cast<sigc::connection *>(g_object_get_data(obj, "dialog-unhide-connection")); + assert(conn != NULL); + conn->disconnect(); + delete conn; + + wd.win = dlg = nullptr; + wd.stop = 0; +} + +static gboolean sp_gradient_vector_dialog_delete(GtkWidget */*widget*/, GdkEvent */*event*/, GtkWidget */*dialog*/) +{ + gtk_window_get_position(GTK_WINDOW(dlg), &x, &y); + gtk_window_get_size(GTK_WINDOW(dlg), &w, &h); + + if (x<0) { + x=0; + } + if (y<0) { + y=0; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(prefs_path + "x", x); + prefs->setInt(prefs_path + "y", y); + prefs->setInt(prefs_path + "w", w); + prefs->setInt(prefs_path + "h", h); + + return FALSE; // which means, go ahead and destroy it +} + +/* Widget destroy handler */ +static void sp_gradient_vector_widget_destroy(GtkWidget *object, gpointer /*data*/) +{ + SPObject *gradient = SP_OBJECT(g_object_get_data(G_OBJECT(object), "gradient")); + + sigc::connection *release_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_release_connection")); + sigc::connection *modified_connection = static_cast<sigc::connection *>(g_object_get_data(G_OBJECT(object), "gradient_modified_connection")); + + if (gradient) { + g_assert( release_connection != nullptr ); + g_assert( modified_connection != nullptr ); + release_connection->disconnect(); + modified_connection->disconnect(); + sp_signal_disconnect_by_data(gradient, object); + + if (gradient->getRepr()) { + sp_repr_remove_listener_by_data(gradient->getRepr(), object); + } + } + + SelectedColor *selected_color = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector")); + if (selected_color) { + delete selected_color; + g_object_set_data(G_OBJECT(object), "cselector", nullptr); + } +} + +static void sp_gradient_vector_gradient_release(SPObject */*object*/, GtkWidget *widget) +{ + sp_gradient_vector_widget_load_gradient(widget, nullptr); +} + +static void sp_gradient_vector_gradient_modified(SPObject *object, guint /*flags*/, GtkWidget *widget) +{ + SPGradient *gradient=SP_GRADIENT(object); + if (!blocked) { + blocked = TRUE; + sp_gradient_vector_widget_load_gradient(widget, gradient); + blocked = FALSE; + } +} + +static void sp_gradient_vector_color_dragged(Inkscape::UI::SelectedColor *selected_color, GObject *object) +{ + SPGradient *gradient, *ngr; + + if (blocked) { + return; + } + + gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient")); + if (!gradient) { + return; + } + + blocked = TRUE; + + ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr); + } + + ngr->ensureVector(); + + SPStop *stop = get_selected_stop(GTK_WIDGET(object)); + if (!stop) { + return; + } + + SPColor color = stop->getColor(); + gfloat opacity = stop->getOpacity(); + selected_color->colorAlpha(color, opacity); + stop->style->stop_color.currentcolor = false; + + blocked = FALSE; +} + +static void sp_gradient_vector_color_changed(Inkscape::UI::SelectedColor *selected_color, GObject *object) +{ + (void)selected_color; + + void* updating_color = g_object_get_data(G_OBJECT(object), "updating_color"); + if (updating_color) { + return; + } + + if (blocked) { + return; + } + + SPGradient *gradient = static_cast<SPGradient*>(g_object_get_data(G_OBJECT(object), "gradient")); + if (!gradient) { + return; + } + + blocked = TRUE; + + SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + sp_gradient_vector_widget_load_gradient(GTK_WIDGET(object), ngr); + } + + ngr->ensureVector(); + + /* Set start parameters */ + /* We rely on normalized vector, i.e. stops HAVE to exist */ + g_return_if_fail(ngr->getFirstStop() != nullptr); + + SPStop *stop = get_selected_stop(GTK_WIDGET(object)); + if (!stop) { + return; + } + + SelectedColor *csel = static_cast<SelectedColor *>(g_object_get_data(G_OBJECT(object), "cselector")); + SPColor color; + float alpha = 0; + csel->colorAlpha(color, alpha); + + sp_repr_set_css_double(stop->getRepr(), "offset", stop->offset); + Inkscape::CSSOStringStream os; + os << "stop-color:" << color.toString() << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";"; + stop->setAttribute("style", os.str()); + // g_snprintf(c, 256, "stop-color:#%06x;stop-opacity:%g;", rgb >> 8, static_cast<gdouble>(alpha)); + //stop->setAttribute("style", c); + + DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT, + _("Change gradient stop color")); + + blocked = FALSE; + + // Set the color in the selected stop after change + GtkWidget *combo_box = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(object), "combo_box")); + if (combo_box) { + GtkTreeIter iter; + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo_box), &iter)) { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo_box))); + + Inkscape::UI::Widget::ColorPreview *cp = Gtk::manage(new Inkscape::UI::Widget::ColorPreview(stop->get_rgba32())); + GdkPixbuf *pb = cp->toPixbuf(64, 16); + + gtk_list_store_set (store, &iter, 0, pb, /*1, repr->attribute("id"),*/ 2, stop, -1); + } + } + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/gradient-vector.h b/src/widgets/gradient-vector.h new file mode 100644 index 0000000..c30cb32 --- /dev/null +++ b/src/widgets/gradient-vector.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_GRADIENT_VECTOR_H +#define SEEN_GRADIENT_VECTOR_H + +/* + * Gradient vector selection widget + * + * Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001-2002 Lauris Kaplinski + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/liststore.h> +#include <sigc++/connection.h> +#include "gradient-selector.h" + +#define SP_TYPE_GRADIENT_VECTOR_SELECTOR (sp_gradient_vector_selector_get_type ()) +#define SP_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelector)) +#define SP_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR, SPGradientVectorSelectorClass)) +#define SP_IS_GRADIENT_VECTOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_GRADIENT_VECTOR_SELECTOR)) +#define SP_IS_GRADIENT_VECTOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_GRADIENT_VECTOR_SELECTOR)) + +class SPDocument; +class SPObject; +class SPGradient; +class SPStop; + +struct SPGradientVectorSelector { + GtkBox vbox; + + guint idlabel : 1; + + bool swatched; + + SPDocument *doc; + SPGradient *gr; + + /* Gradient vectors store */ + Glib::RefPtr<Gtk::ListStore> store; + SPGradientSelector::ModelColumns *columns; + + sigc::connection gradient_release_connection; + sigc::connection defs_release_connection; + sigc::connection defs_modified_connection; + sigc::connection tree_select_connection; + + void setSwatched(); +}; + +struct SPGradientVectorSelectorClass { + GtkBoxClass parent_class; + + void (* vector_set) (SPGradientVectorSelector *gvs, SPGradient *gr); +}; + +GType sp_gradient_vector_selector_get_type(); + +GtkWidget *sp_gradient_vector_selector_new (SPDocument *doc, SPGradient *gradient); + +void sp_gradient_vector_selector_set_gradient (SPGradientVectorSelector *gvs, SPDocument *doc, SPGradient *gr); + +SPDocument *sp_gradient_vector_selector_get_document (SPGradientVectorSelector *gvs); +SPGradient *sp_gradient_vector_selector_get_gradient (SPGradientVectorSelector *gvs); + +/* fixme: rethink this (Lauris) */ +GtkWidget *sp_gradient_vector_editor_new (SPGradient *gradient, SPStop *stop = nullptr); + +guint32 sp_average_color(guint32 c1, guint32 c2, gdouble p = 0.5); + +Glib::ustring gr_prepare_label (SPObject *obj); +Glib::ustring gr_ellipsize_text(Glib::ustring const &src, size_t maxlen); + +#endif // SEEN_GRADIENT_VECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/ink-action.cpp b/src/widgets/ink-action.cpp new file mode 100644 index 0000000..0696228 --- /dev/null +++ b/src/widgets/ink-action.cpp @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "ink-action.h" +#include "ui/icon-loader.h" +#include <gtk/gtk.h> + +static void ink_action_finalize( GObject* obj ); +static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec ); +static void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec ); + +static GtkWidget* ink_action_create_menu_item( GtkAction* action ); +static GtkWidget* ink_action_create_tool_item( GtkAction* action ); + +typedef struct +{ + gchar* iconId; + GtkIconSize iconSize; +} InkActionPrivate; + +#define INK_ACTION_GET_PRIVATE( o ) \ + reinterpret_cast<InkActionPrivate *>(ink_action_get_instance_private (o)) + +G_DEFINE_TYPE_WITH_PRIVATE(InkAction, ink_action, GTK_TYPE_ACTION); + +enum { + PROP_INK_ID = 1, + PROP_INK_SIZE +}; + +static void ink_action_class_init( InkActionClass* klass ) +{ + if ( klass ) { + GObjectClass * objClass = G_OBJECT_CLASS( klass ); + + objClass->finalize = ink_action_finalize; + objClass->get_property = ink_action_get_property; + objClass->set_property = ink_action_set_property; + + klass->parent_class.create_menu_item = ink_action_create_menu_item; + klass->parent_class.create_tool_item = ink_action_create_tool_item; + /*klass->parent_class.connect_proxy = connect_proxy;*/ + /*klass->parent_class.disconnect_proxy = disconnect_proxy;*/ + + g_object_class_install_property( objClass, + PROP_INK_ID, + g_param_spec_string( "iconId", + "Icon ID", + "The id for the icon", + "", + (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) ); + + g_object_class_install_property( objClass, + PROP_INK_SIZE, + g_param_spec_int( "iconSize", + "Icon Size", + "The size the icon", + (int)GTK_ICON_SIZE_MENU, + (int)GTK_ICON_SIZE_DIALOG, + (int)GTK_ICON_SIZE_SMALL_TOOLBAR, + (GParamFlags)(G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT) ) ); + } +} + +static void ink_action_init( InkAction* action ) +{ + auto priv = INK_ACTION_GET_PRIVATE (action); + priv->iconId = nullptr; + priv->iconSize = GTK_ICON_SIZE_SMALL_TOOLBAR; +} + +static void ink_action_finalize( GObject* obj ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + g_free( priv->iconId ); + g_free( priv ); + +} + +//Any strings passed in should already be localised +InkAction* ink_action_new( const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *inkId, + GtkIconSize size ) +{ + GObject* obj = (GObject*)g_object_new( INK_ACTION_TYPE, + "name", name, + "label", label, + "tooltip", tooltip, + "iconId", inkId, + "iconSize", size, + NULL ); + + InkAction* action = INK_ACTION( obj ); + + return action; +} + +static void ink_action_get_property( GObject* obj, guint propId, GValue* value, GParamSpec * pspec ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + switch ( propId ) { + case PROP_INK_ID: + { + g_value_set_string( value, priv->iconId ); + } + break; + + case PROP_INK_SIZE: + { + g_value_set_int( value, priv->iconSize ); + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec ); + } +} + +void ink_action_set_property( GObject* obj, guint propId, const GValue *value, GParamSpec* pspec ) +{ + InkAction* action = INK_ACTION( obj ); + auto priv = INK_ACTION_GET_PRIVATE (action); + + switch ( propId ) { + case PROP_INK_ID: + { + gchar* tmp = priv->iconId; + priv->iconId = g_value_dup_string( value ); + g_free( tmp ); + } + break; + + case PROP_INK_SIZE: + { + priv->iconSize = (GtkIconSize)g_value_get_int( value ); + } + break; + + default: + { + G_OBJECT_WARN_INVALID_PROPERTY_ID( obj, propId, pspec ); + } + } +} + +static GtkWidget* ink_action_create_menu_item( GtkAction* action ) +{ + InkAction* act = INK_ACTION( action ); + GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_menu_item( action ); + + return item; +} + +static GtkWidget* ink_action_create_tool_item( GtkAction* action ) +{ + InkAction* act = INK_ACTION( action ); + auto priv = INK_ACTION_GET_PRIVATE (act); + GtkWidget* item = GTK_ACTION_CLASS(ink_action_parent_class)->create_tool_item(action); + + if ( priv->iconId ) { + if ( GTK_IS_TOOL_BUTTON(item) ) { + GtkToolButton* button = GTK_TOOL_BUTTON(item); + + GtkWidget *child = sp_get_icon_image(priv->iconId, priv->iconSize); + gtk_tool_button_set_icon_widget( button, child ); + } else { + // For now trigger a warning but don't do anything else + GtkToolButton* button = GTK_TOOL_BUTTON(item); + (void)button; + } + } + + // TODO investigate if needed + gtk_widget_show_all( item ); + + return item; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/ink-action.h b/src/widgets/ink-action.h new file mode 100644 index 0000000..d97afe1 --- /dev/null +++ b/src/widgets/ink-action.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INK_ACTION +#define SEEN_INK_ACTION + +#include <gtk/gtk.h> +#include "attributes.h" + +/* Equivalent to GTK Actions of the same type, but can support Inkscape SVG icons */ + +G_BEGIN_DECLS + +#define INK_ACTION_TYPE ( ink_action_get_type() ) +#define INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_CAST( (obj), INK_ACTION_TYPE, InkAction) ) +#define INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_CAST( (klass), INK_ACTION_TYPE, InkActionClass) ) +#define IS_INK_ACTION( obj ) ( G_TYPE_CHECK_INSTANCE_TYPE( (obj), INK_ACTION_TYPE) ) +#define IS_INK_ACTION_CLASS( klass ) ( G_TYPE_CHECK_CLASS_TYPE( (klass), INK_ACTION_TYPE) ) +#define INK_ACTION_GET_CLASS( obj ) ( G_TYPE_INSTANCE_GET_CLASS( (obj), INK_ACTION_TYPE, InkActionClass) ) + +typedef struct _InkAction InkAction; +typedef struct _InkActionClass InkActionClass; + +struct _InkAction +{ + GtkAction action; +}; + +struct _InkActionClass +{ + GtkActionClass parent_class; +}; + +GType ink_action_get_type( void ); + +InkAction* ink_action_new( const gchar *name, + const gchar *label, + const gchar *tooltip, + const gchar *inkId, + GtkIconSize size ); + + +G_END_DECLS + +#endif /* SEEN_INK_ACTION */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/mappings.xml b/src/widgets/mappings.xml new file mode 100644 index 0000000..3ea9cb3 --- /dev/null +++ b/src/widgets/mappings.xml @@ -0,0 +1,334 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<mappings> + + <!-- file menu --> + <remap id='file_import' newid='document-import'/> + <remap id='file_export' newid='document-export'/> + <remap id='ocal_import' newid='document-import-ocal'/> + <remap id='ocal_export' newid='document-export-ocal'/> + <remap id='document_metadata' newid='document-metadata'/> + <remap id='input_devices' newid='dialog-input-devices'/> + + <!-- edit menu --> + <remap id='edit_duplicate' newid='edit-duplicate'/> + <remap id='edit_clone' newid='edit-clone'/> + <remap id='edit_unlink_clone' newid='edit-clone-unlink'/> + <remap id='edit_select_original' newid='edit-select-original'/> + <remap id='edit_undo_history' newid='edit-undo-history'/> + <remap id='selection_paste_in_place' newid='edit-paste-in-place'/> + <remap id='selection_paste_style' newid='edit-paste-style'/> + <remap id='selection_bitmap' newid='selection-make-bitmap-copy'/> + <remap id='selection_select_all' newid='edit-select-all'/> + <remap id='selection_select_all_in_all_layers' newid='edit-select-all-layers'/> + <remap id='selection_invert' newid='edit-select-invert'/> + <remap id='selection_deselect' newid='edit-select-none'/> + <remap id='xml_editor' newid='dialog-xml-editor'/> + + <!-- view menu --> + <!-- submenu: zoom --> + <remap id='zoom_1_to_1' newid='zoom-original'/> + <remap id='zoom_1_to_2' newid='zoom-half-size'/> + <remap id='zoom_2_to_1' newid='zoom-double-size'/> + <remap id='zoom_select' newid='zoom-fit-selection'/> + <remap id='zoom_draw' newid='zoom-fit-drawing'/> + <remap id='zoom_page' newid='zoom-fit-page'/> + <remap id='zoom_pagewidth' newid='zoom-fit-width'/> + <remap id='zoom_previous' newid='zoom-previous'/> + <remap id='zoom_next' newid='zoom-next'/> + <remap id='zoom_in' newid='zoom-in'/> + <remap id='zoom_out' newid='zoom-out'/> + + <remap id='grid' newid='show-grid'/> + <remap id='guides' newid='show-guides'/> + <remap id='color_management' newid='color-management'/> + <remap id='dialog_toggle' newid='show-dialogs'/> + <remap id='messages' newid='dialog-messages'/> + <remap id='scripts' newid='dialog-scripts'/> + <remap id='window_previous' newid='window-previous'/> + <remap id='window_next' newid='window-next'/> + <remap id='view_icon_preview' newid='dialog-icon-preview'/> + <remap id='view_new' newid='window-new'/> + <remap id='fullscreen' newid='view-fullscreen'/> + + <!-- layers menu --> + <remap id='new_layer' newid='layer-new'/> + <remap id='rename_layer' newid='layer-rename'/> + <remap id='switch_to_layer_above' newid='layer-previous'/> + <remap id='switch_to_layer_below' newid='layer-next'/> + <remap id='move_selection_above' newid='selection-move-to-layer-above'/> + <remap id='move_selection_below' newid='selection-move-to-layer-below'/> + <remap id='raise_layer' newid='layer-raise'/> + <remap id='lower_layer' newid='layer-lower'/> + <remap id='layer_to_top' newid='layer-top'/> + <remap id='layer_to_bottom' newid='layer-bottom'/> + <remap id='delete_layer' newid='layer-delete'/> + <remap id='layers' newid='dialog-layers'/> + + <!-- object menu --> + <remap id='fill_and_stroke' newid='dialog-fill-and-stroke'/> + <remap id='dialog_item_properties' newid='dialog-object-properties'/> + <remap id='selection_group' newid='object-group'/> + <remap id='selection_ungroup' newid='object-ungroup'/> + <remap id='selection_up' newid='selection-raise'/> + <remap id='selection_down' newid='selection-lower'/> + <remap id='selection_top' newid='selection-top'/> + <remap id='selection_bot' newid='selection-bottom'/> + <remap id='object_rotate_90_CCW' newid='object-rotate-left'/> + <remap id='object_rotate_90_CW' newid='object-rotate-right'/> + <remap id='object_flip_hor' newid='object-flip-horizontal'/> + <remap id='object_flip_ver' newid='object-flip-vertical'/> + <remap id='object_trans' newid='dialog-transform'/> + <remap id='object_align' newid='dialog-align-and-distribute'/> + <remap id='grid_arrange' newid='dialog-rows-and-columns'/> + + <!-- path menu --> + <remap id='object_tocurve' newid='object-to-path'/> + <remap id='stroke_tocurve' newid='stroke-to-path'/> + <remap id='selection_trace' newid='bitmap-trace'/> + <remap id='union' newid='path-union'/> + <remap id='difference' newid='path-difference'/> + <remap id='intersection' newid='path-intersection'/> + <remap id='exclusion' newid='path-exclusion'/> + <remap id='division' newid='path-division'/> + <remap id='cut_path' newid='path-cut'/> + <remap id='selection_combine' newid='path-combine'/> + <remap id='selection_break' newid='path-break-apart'/> + <remap id='outset_path' newid='path-outset'/> + <remap id='inset_path' newid='path-inset'/> + <remap id='dynamic_offset' newid='path-offset-dynamic'/> + <remap id='linked_offset' newid='path-offset-linked'/> + <remap id='simplify' newid='path-simplify'/> + <remap id='selection_reverse' newid='path-reverse'/> + + + <!-- text menu --> + <remap id='object_font' newid='dialog-text-and-font'/> + <remap id='put_on_path' newid='text-put-on-path'/> + <remap id='remove_from_path' newid='text-remove-from-path'/> + <remap id='flow_into_frame' newid='text-flow-into-frame'/> + <remap id='unflow' newid='text-unflow'/> + <remap id='convert_to_text' newid='text-convert-to-regular'/> + <remap id='remove_manual_kerns' newid='text-unkern'/> + + <!-- help menu --> + <remap id='help_keys' newid='help-keyboard-shortcuts'/> + <remap id='help_tutorials' newid='help-contents'/> + <remap id='inkscape_options' newid='inkscape-logo'/> + <remap id='about_memory' newid='dialog-memory'/> + + <!-- tools --> + <remap id='draw_select' newid='tool-pointer'/> + <remap id='draw_node' newid='tool-node-editor'/> + <remap id='draw_tweak' newid='tool-tweak'/> + <remap id='draw_zoom' newid='zoom'/> + <remap id='draw_rect' newid='draw-rectangle'/> + <remap id='draw_3dbox' newid='draw-cuboid'/> + <remap id='draw_arc' newid='draw-ellipse'/> + <remap id='draw_star' newid='draw-polygon-star'/> + <remap id='draw_spiral' newid='draw-spiral'/> + <remap id='draw_freehand' newid='draw-freehand'/> + <remap id='draw_pen' newid='draw-path'/> + <remap id='draw_calligraphic' newid='draw-calligraphic'/> + <remap id='draw_erase' newid='draw-eraser'/> + <remap id='draw_paintbucket' newid='color-fill'/> + <remap id='draw_text' newid='draw-text'/> + <remap id='draw_connector' newid='draw-connector'/> + <remap id='draw_gradient' newid='color-gradient'/> + <remap id='draw_dropper' newid='color-picker'/> + + <!-- TOOLBARS --> + <!-- select toolbar --> + <remap id='transform_stroke' newid='transform-affect-stroke'/> + <remap id='transform_corners' newid='transform-affect-rounded-corners'/> + <remap id='transform_gradient' newid='transform-affect-gradient'/> + <remap id='transform_pattern' newid='transform-affect-pattern'/> + + <!-- node editor toolbar --> + <remap id='node_insert' newid='node-add'/> + <remap id='node_delete' newid='node-delete'/> + <remap id='node_join' newid='node-join'/> + <remap id='node_break' newid='node-break'/> + <remap id='node_join_segment' newid='node-join-segment'/> + <remap id='node_delete_segment' newid='node-delete-segment'/> + <remap id='node_cusp' newid='node-type-cusp'/> + <remap id='node_smooth' newid='node-type-smooth'/> + <remap id='node_symmetric' newid='node-type-symmetric'/> + <remap id='node_auto' newid='node-type-auto-smooth'/> + <remap id='node_curve' newid='node-segment-curve'/> + <remap id='node_line' newid='node-segment-line'/> + <remap id='nodes_show_handles' newid='show-node-handles'/> + <remap id='edit_next_parameter' newid='path-effect-parameter-next'/> + <remap id='nodes_show_helperpath' newid='show-path-outline'/> + <remap id='nodeedit-clippath' newid='path-clip-edit'/> + <remap id='nodeedit-mask' newid='path-mask-edit'/> + <remap id='node_cusp' newid='node-type-cusp'/> + + <!-- tweak toolbar --> + <remap id='tweak_move_mode' newid='object-tweak-push'/> + <remap id='tweak_move_mode_inout' newid='object-tweak-attract'/> + <remap id='tweak_move_mode_jitter' newid='object-tweak-randomize'/> + <remap id='tweak_scale_mode' newid='object-tweak-shrink'/> + <remap id='tweak_rotate_mode' newid='object-tweak-rotate'/> + <remap id='tweak_moreless_mode' newid='object-tweak-duplicate'/> + <remap id='tweak_move_mode' newid='object-tweak-push'/> + <remap id='tweak_push_mode' newid='path-tweak-push'/> + <remap id='tweak_shrink_mode' newid='path-tweak-shrink'/> + <remap id='tweak_attract_mode' newid='path-tweak-attract'/> + <remap id='tweak_roughen_mode' newid='path-tweak-roughen'/> + <remap id='tweak_colorpaint_mode' newid='object-tweak-paint'/> + <remap id='tweak_colorjitter_mode' newid='object-tweak-jitter-color'/> + <remap id='tweak_blur_mode' newid='object-tweak-blur'/> + + <!-- rectangle toolbar --> + <remap id='squared_corner' newid='rectangle-make-corners-sharp'/> + + <!-- cuboid toolbar --> + <remap id='toggle_vp_x' newid='perspective-parallel'/> + + <!-- ellipse toolbar --> + <remap id='reset_circle' newid='draw-ellipse-whole'/> + <remap id='circle_closed_arc' newid='draw-ellipse-segment'/> + <remap id='circle_open_arc' newid='draw-ellipse-arc'/> + + <!-- polygon toolbar --> + <remap id='star_flat' newid='draw-polygon'/> + <remap id='star_angled' newid='draw-star'/> + + <!-- bezier toolbar --> + <remap id='bezier_mode' newid='path-mode-bezier'/> + <remap id='spiro_splines_mode' newid='path-mode-spiro'/> + <remap id='bspline_mode' newid='path-mode-bspline'/> + <remap id='polylines_mode' newid='path-mode-polyline'/> + <remap id='paraxial_lines_mode' newid='path-mode-polyline-paraxial'/> + + <!-- calligraphic toolbar --> + <remap id='guse_tilt' newid='draw-use-tilt'/> + <remap id='guse_pressure' newid='draw-use-pressure'/> + <remap id='trace_background' newid='draw-trace-background'/> + + <!-- eraser toolbar --> + <remap id='delete_object' newid='draw-eraser-delete-objects'/> + + <!-- text toolbar --> + <remap id='writing_mode_tb' newid='format-text-direction-vertical'/> + <remap id='writing_mode_lr' newid='format-text-direction-horizontal'/> + + <!-- connector toolbar --> + <remap id='connector_avoid' newid='connector-avoid'/> + <remap id='connector_ignore' newid='connector-ignore'/> + + <!-- gradient toolbar --> + <remap id='controls_fill' newid='object-fill'/> + <remap id='controls_stroke' newid='object-stroke'/> + + <!-- lpe toolbar --> + <remap id='lpetool_show_bbox' newid='show-bounding-box'/> + <remap id='all_inactive_old' newid='draw-geometry-inactive'/> + <remap id='angle_bisector' newid='draw-geometry-angle-bisector'/> + <remap id='circle_3pts' newid='draw-geometry-circle-from-three-points'/> + <remap id='line_segment' newid='draw-geometry-line-segment'/> + <remap id='mirror_symmetry' newid='draw-geometry-mirror'/> + <remap id='parralel' newid='draw-geometry-line-parallel'/> + <remap id='perp_bisector' newid='draw-geometry-line-perpendicular'/> + + <!-- snapping toolbar --> + <remap id='toggle_snap_global' newid='snap'/> + <remap id='toggle_snap_bbox' newid='snap-bounding-box'/> + <remap id='toggle_snap_to_bbox_path' newid='snap-bounding-box-edges'/> + <remap id='toggle_snap_to_bbox_node' newid='snap-bounding-box-corners'/> + <remap id='toggle_snap_to_bbox_edge_midpoints' newid='snap-bounding-box-midpoints'/> + <remap id='toggle_snap_to_bbox_midpoints' newid='snap-bounding-box-center'/> + <remap id='toggle_snap_nodes' newid='snap-nodes'/> + <remap id='toggle_snap_to_paths' newid='snap-nodes-path'/> + <remap id='toggle_snap_to_nodes' newid='snap-nodes-cusp'/> + <remap id='toggle_snap_to_smooth_nodes' newid='snap-nodes-smooth'/> + <remap id='toggle_snap_to_midpoints' newid='snap-nodes-midpoint'/> + <remap id='toggle_snap_to_path_intersections' newid='snap-nodes-intersection'/> + <remap id='toggle_snap_to_bbox_midpoints-3' newid='snap-nodes-center'/> + <remap id='toggle_snap_center' newid='snap-nodes-rotation-center'/> + <remap id='toggle_snap_page_border' newid='snap-page'/> + <remap id='toggle_snap_grid_guide_intersections' newid='snap-grid-guide-intersections'/> + + <!-- DIALOGS --> + <!-- align and distribute dialog --> + <remap id='al_left_out' newid='align-horizontal-right-to-anchor'/> + <remap id='al_left_in' newid='align-horizontal-left'/> + <remap id='al_center_hor' newid='align-horizontal-center'/> + <remap id='al_right_in' newid='align-horizontal-right'/> + <remap id='al_right_out' newid='align-horizontal-left-to-anchor'/> + <remap id='al_baselines_vert' newid='align-horizontal-baseline'/> + + <remap id='al_top_out' newid='align-vertical-bottom-to-anchor'/> + <remap id='al_top_in' newid='align-vertical-top'/> + <remap id='al_center_ver' newid='align-vertical-center'/> + <remap id='al_bottom_in' newid='align-vertical-bottom'/> + <remap id='al_bottom_out' newid='align-vertical-top-to-anchor'/> + <remap id='al_baselines_hor' newid='align-vertical-baseline'/> + + <remap id='distribute_left' newid='distribute-horizontal-left'/> + <remap id='distribute_hcentre' newid='distribute-horizontal-center'/> + <remap id='distribute_right' newid='distribute-horizontal-right'/> + <remap id='distrobute-hdist' newid='distribute-horizontal-gaps'/> + <remap id='distribute_baselines_hor' newid='distribute-horizontal-baseline'/> + + <remap id='distribute_bottom' newid='distribute-vertical-bottom'/> + <remap id='distribute_vcentre' newid='distribute-vertical-center'/> + <remap id='distribute_top' newid='distribute-vertical-top'/> + <remap id='distrobute-vdist' newid='distribute-vertical-gaps'/> + <remap id='distribute_baselines_vert' newid='distribute-vertical-baseline'/> + + <remap id='distribute_randomize' newid='distribute-randomize'/> + <remap id='unclump' newid='distribute-unclump'/> + <remap id='graph_layout' newid='distribute-graph'/> + <remap id='directed_graph' newid='distribute-graph-directed'/> + <remap id='remove_overlaps' newid='distribute-remove-overlaps'/> + + <remap id='node_valign' newid='align-horizontal-node'/> + <remap id='node_halign' newid='align-vertical-node'/> + <remap id='node_vdistribute' newid='distribute-vertical-node'/> + <remap id='node_hdistribute' newid='distribute-horizontal-node'/> + + <!-- XML editor --> + <remap id='add_xml_element_node' newid='xml-element-new'/> + <remap id='add_xml_text_node' newid='xml-text-new'/> + <remap id='delete_xml_node' newid='xml-node-delete'/> + <remap id='duplicate_xml_node' newid='xml-node-duplicate'/> + <remap id='delete_xml_attribute' newid='xml-attribute-delete'/> + + <!-- transform dialog --> + <remap id='arrows_hor' newid='transform-move-horizontal'/> + <remap id='arrows_ver' newid='transform-move-vertical'/> + <remap id='transform_scale_hor' newid='transform-scale-horizontal'/> + <remap id='transform_scale_ver' newid='transform-scale-vertical'/> + <remap id='transform_scew_hor' newid='transform-skew-horizontal'/> + <remap id='transform_scew_ver' newid='transform-skew-vertical'/> + + <!-- fill and stroke dialog --> + <remap id='properties_fill' newid='object-fill'/> + <remap id='properties_stroke_paint' newid='object-stroke'/> + <remap id='properties_stroke' newid='object-stroke-style'/> + <remap id='fill_none' newid='paint-none'/> + <remap id='fill_solid' newid='paint-solid'/> + <remap id='fill_gradient' newid='paint-gradient-linear'/> + <remap id='fill_radial' newid='paint-gradient-radial'/> + <remap id='fill_pattern' newid='paint-pattern'/> + <remap id='fill_unset' newid='paint-unknown'/> + <remap id='fillrule_evenodd' newid='fill-rule-even-odd'/> + <remap id='fillrule_nonzero' newid='fill-rule-nonzero'/> + <remap id='join_miter' newid='stroke-join-miter'/> + <remap id='join_bevel' newid='stroke-join-bevel'/> + <remap id='join_round' newid='stroke-join-round'/> + <remap id='cap_butt' newid='stroke-cap-butt'/> + <remap id='cap_square' newid='stroke-cap-square'/> + <remap id='cap_round' newid='stroke-cap-round'/> + + <!-- MISCELLANEOUS --> + <remap id='guide' newid='guides'/> + <remap id='grid_xy' newid='grid-rectangular'/> + <remap id='grid_axonom' newid='grid-axonometric'/> + <remap id='visible' newid='object-visible'/> + <remap id='hidden' newid='object-hidden'/> + <remap id='lock_unlocked' newid='object-unlocked'/> + <remap id='width_height_lock' newid='object-locked'/> + <remap id='sticky_zoom' newid='zoom'/> +</mappings> diff --git a/src/widgets/paint-selector.cpp b/src/widgets/paint-selector.cpp new file mode 100644 index 0000000..1e1f99e --- /dev/null +++ b/src/widgets/paint-selector.cpp @@ -0,0 +1,1601 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPPaintSelector: Generic paint selector widget. + *//* + * Authors: + * see git history + * Lauris Kaplinski + * bulia byak <buliabyak@users.sf.net> + * John Cliff <simarilius@yahoo.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_PS_VERBOSE + +#include <cstring> +#include <string> +#include <vector> + +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "gradient-selector.h" +#include "inkscape.h" +#include "paint-selector.h" +#include "path-prefix.h" + +#include "helper/stock-items.h" +#include "ui/icon-loader.h" + +#include "style.h" + +#include "io/sys.h" + +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" + +#include "ui/icon-names.h" +#include "ui/widget/color-notebook.h" + +#include "widgets/swatch-selector.h" +#include "widgets/widget-sizes.h" + +#include "xml/repr.h" + +#ifdef SP_PS_VERBOSE +#include "svg/svg-icc-color.h" +#endif // SP_PS_VERBOSE + +using Inkscape::Widgets::SwatchSelector; +using Inkscape::UI::SelectedColor; + +enum { + MODE_CHANGED, + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + FILLRULE_CHANGED, + LAST_SIGNAL +}; + +static void sp_paint_selector_dispose(GObject *object); + +static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel, gchar const *px, SPPaintSelector::Mode mode, gchar const *tip); +static void sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel); +static void sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel); + +static void sp_paint_selector_set_mode_empty(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_multiple(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_none(SPPaintSelector *psel); +static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode); +#ifdef WITH_MESH +static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode); +#endif +static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode); +static void sp_paint_selector_set_mode_unset(SPPaintSelector *psel); + + +static void sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active); + +static guint psel_signals[LAST_SIGNAL] = {0}; + +#ifdef SP_PS_VERBOSE +static gchar const* modeStrings[] = { + "MODE_EMPTY", + "MODE_MULTIPLE", + "MODE_NONE", + "MODE_SOLID_COLOR", + "MODE_GRADIENT_LINEAR", + "MODE_GRADIENT_RADIAL", +#ifdef WITH_MESH + "MODE_GRADIENT_MESH", +#endif + "MODE_PATTERN", + "MODE_SWATCH", + "MODE_UNSET", + ".", + ".", +}; +#endif + + +static bool isPaintModeGradient(SPPaintSelector::Mode mode) +{ + bool isGrad = (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) || + (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) || + (mode == SPPaintSelector::MODE_SWATCH); + + return isGrad; +} + +static SPGradientSelector *getGradientFromData(SPPaintSelector const *psel) +{ + SPGradientSelector *grad = nullptr; + if (psel->mode == SPPaintSelector::MODE_SWATCH) { + SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector")); + if (swatchsel) { + grad = swatchsel->getGradientSelector(); + } + } else { + grad = reinterpret_cast<SPGradientSelector*>(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector")); + } + return grad; +} + +G_DEFINE_TYPE(SPPaintSelector, sp_paint_selector, GTK_TYPE_BOX); + +static void +sp_paint_selector_class_init(SPPaintSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + psel_signals[MODE_CHANGED] = g_signal_new("mode_changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, mode_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + psel_signals[GRABBED] = g_signal_new("grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[DRAGGED] = g_signal_new("dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[RELEASED] = g_signal_new("released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[CHANGED] = g_signal_new("changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + psel_signals[FILLRULE_CHANGED] = g_signal_new("fillrule_changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPPaintSelectorClass, fillrule_changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + object_class->dispose = sp_paint_selector_dispose; +} + +#define XPAD 4 +#define YPAD 1 + +static void +sp_paint_selector_init(SPPaintSelector *psel) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(psel), GTK_ORIENTATION_VERTICAL); + + psel->mode = static_cast<SPPaintSelector::Mode>(-1); // huh? do you mean 0xff? -- I think this means "not in the enum" + + /* Paint style button box */ + psel->style = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(psel->style), FALSE); + gtk_widget_set_name(psel->style,"PaintSelector"); + gtk_widget_show(psel->style); + gtk_container_set_border_width(GTK_CONTAINER(psel->style), 4); + gtk_box_pack_start(GTK_BOX(psel), psel->style, FALSE, FALSE, 0); + + /* Buttons */ + psel->none = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-none"), + SPPaintSelector::MODE_NONE, _("No paint")); + psel->solid = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-solid"), + SPPaintSelector::MODE_SOLID_COLOR, _("Flat color")); + psel->gradient = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-linear"), + SPPaintSelector::MODE_GRADIENT_LINEAR, _("Linear gradient")); + psel->radial = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-radial"), + SPPaintSelector::MODE_GRADIENT_RADIAL, _("Radial gradient")); +#ifdef WITH_MESH + psel->mesh = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-gradient-mesh"), + SPPaintSelector::MODE_GRADIENT_MESH, _("Mesh gradient")); +#endif + psel->pattern = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-pattern"), + SPPaintSelector::MODE_PATTERN, _("Pattern")); + psel->swatch = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-swatch"), + SPPaintSelector::MODE_SWATCH, _("Swatch")); + psel->unset = sp_paint_selector_style_button_add(psel, INKSCAPE_ICON("paint-unknown"), + SPPaintSelector::MODE_UNSET, _("Unset paint (make it undefined so it can be inherited)")); + + /* Fillrule */ + { + psel->fillrulebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(psel->fillrulebox), FALSE); + gtk_box_pack_end(GTK_BOX(psel->style), psel->fillrulebox, FALSE, FALSE, 0); + + GtkWidget *w; + psel->evenodd = gtk_radio_button_new(nullptr); + gtk_button_set_relief(GTK_BUTTON(psel->evenodd), GTK_RELIEF_NONE); + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->evenodd), FALSE); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + gtk_widget_set_tooltip_text(psel->evenodd, _("Any path self-intersections or subpaths create holes in the fill (fill-rule: evenodd)")); + g_object_set_data(G_OBJECT(psel->evenodd), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_EVENODD)); + w = sp_get_icon_image("fill-rule-even-odd", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(psel->evenodd), w); + gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->evenodd, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(psel->evenodd), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel); + + psel->nonzero = gtk_radio_button_new(gtk_radio_button_get_group(GTK_RADIO_BUTTON(psel->evenodd))); + gtk_button_set_relief(GTK_BUTTON(psel->nonzero), GTK_RELIEF_NONE); + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(psel->nonzero), FALSE); + // TRANSLATORS: for info, see http://www.w3.org/TR/2000/CR-SVG-20000802/painting.html#FillRuleProperty + gtk_widget_set_tooltip_text(psel->nonzero, _("Fill is solid unless a subpath is counterdirectional (fill-rule: nonzero)")); + g_object_set_data(G_OBJECT(psel->nonzero), "mode", GUINT_TO_POINTER(SPPaintSelector::FILLRULE_NONZERO)); + w = sp_get_icon_image("fill-rule-nonzero", GTK_ICON_SIZE_MENU); + gtk_container_add(GTK_CONTAINER(psel->nonzero), w); + gtk_box_pack_start(GTK_BOX(psel->fillrulebox), psel->nonzero, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(psel->nonzero), "toggled", G_CALLBACK(sp_paint_selector_fillrule_toggled), psel); + } + + /* Frame */ + psel->label = gtk_label_new(""); + auto lbbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); + gtk_box_set_homogeneous(GTK_BOX(lbbox), FALSE); + gtk_widget_show(psel->label); + gtk_box_pack_start(GTK_BOX(lbbox), psel->label, false, false, 4); + gtk_box_pack_start(GTK_BOX(psel), lbbox, false, false, 4); + + psel->frame = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(psel->frame), FALSE); + gtk_widget_show(psel->frame); + //gtk_container_set_border_width(GTK_CONTAINER(psel->frame), 0); + gtk_box_pack_start(GTK_BOX(psel), psel->frame, TRUE, TRUE, 0); + + + /* Last used color */ + psel->selected_color = new SelectedColor; + psel->updating_color = false; + + psel->selected_color->signal_grabbed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorGrabbed)); + psel->selected_color->signal_dragged.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorDragged)); + psel->selected_color->signal_released.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorReleased)); + psel->selected_color->signal_changed.connect(sigc::mem_fun(psel, &SPPaintSelector::onSelectedColorChanged)); +} + +static void sp_paint_selector_dispose(GObject *object) +{ + SPPaintSelector *psel = SP_PAINT_SELECTOR(object); + + // clean up our long-living pattern menu + g_object_set_data(G_OBJECT(psel),"patternmenu",nullptr); + +#ifdef WITH_MESH + // clean up our long-living mesh menu + g_object_set_data(G_OBJECT(psel),"meshmenu",nullptr); +#endif + + if (psel->selected_color) { + delete psel->selected_color; + psel->selected_color = nullptr; + } + + if ((G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose) + (G_OBJECT_CLASS(sp_paint_selector_parent_class))->dispose(object); +} + +static GtkWidget *sp_paint_selector_style_button_add(SPPaintSelector *psel, + gchar const *pixmap, SPPaintSelector::Mode mode, + gchar const *tip) +{ + GtkWidget *b, *w; + + b = gtk_toggle_button_new(); + gtk_widget_set_tooltip_text(b, tip); + gtk_widget_show(b); + + gtk_container_set_border_width(GTK_CONTAINER(b), 0); + + gtk_button_set_relief(GTK_BUTTON(b), GTK_RELIEF_NONE); + + gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(b), FALSE); + g_object_set_data(G_OBJECT(b), "mode", GUINT_TO_POINTER(mode)); + + w = sp_get_icon_image(pixmap, GTK_ICON_SIZE_BUTTON); + gtk_container_add(GTK_CONTAINER(b), w); + + gtk_box_pack_start(GTK_BOX(psel->style), b, FALSE, FALSE, 0); + g_signal_connect(G_OBJECT(b), "toggled", G_CALLBACK(sp_paint_selector_style_button_toggled), psel); + + return b; +} + +static void +sp_paint_selector_style_button_toggled(GtkToggleButton *tb, SPPaintSelector *psel) +{ + if (!psel->update && gtk_toggle_button_get_active(tb)) { + psel->setMode(static_cast<SPPaintSelector::Mode>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode")))); + } +} + +static void +sp_paint_selector_fillrule_toggled(GtkToggleButton *tb, SPPaintSelector *psel) +{ + if (!psel->update && gtk_toggle_button_get_active(tb)) { + SPPaintSelector::FillRule fr = static_cast<SPPaintSelector::FillRule>(GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(tb), "mode"))); + g_signal_emit(G_OBJECT(psel), psel_signals[FILLRULE_CHANGED], 0, fr); + } +} + +static void +sp_paint_selector_show_fillrule(SPPaintSelector *psel, bool is_fill) +{ + if (psel->fillrulebox) { + if (is_fill) { + gtk_widget_show_all(psel->fillrulebox); + } else { + gtk_widget_destroy(psel->fillrulebox); + psel->fillrulebox = nullptr; + } + } +} + + +SPPaintSelector *sp_paint_selector_new(FillOrStroke kind) +{ + SPPaintSelector *psel = static_cast<SPPaintSelector*>(g_object_new(SP_TYPE_PAINT_SELECTOR, nullptr)); + + psel->setMode(SPPaintSelector::MODE_MULTIPLE); + + // This silliness is here because I don't know how to pass a parameter to the + // GtkObject "constructor" (sp_paint_selector_init). Remove it when paint_selector + // becomes a normal class. + sp_paint_selector_show_fillrule(psel, kind == FILL); + + return psel; +} + +void SPPaintSelector::setMode(Mode mode) +{ + if (this->mode != mode) { + update = TRUE; +#ifdef SP_PS_VERBOSE + g_print("Mode change %d -> %d %s -> %s\n", this->mode, mode, modeStrings[this->mode], modeStrings[mode]); +#endif + switch (mode) { + case MODE_EMPTY: + sp_paint_selector_set_mode_empty(this); + break; + case MODE_MULTIPLE: + sp_paint_selector_set_mode_multiple(this); + break; + case MODE_NONE: + sp_paint_selector_set_mode_none(this); + break; + case MODE_SOLID_COLOR: + sp_paint_selector_set_mode_color(this, mode); + break; + case MODE_GRADIENT_LINEAR: + case MODE_GRADIENT_RADIAL: + sp_paint_selector_set_mode_gradient(this, mode); + break; +#ifdef WITH_MESH + case MODE_GRADIENT_MESH: + sp_paint_selector_set_mode_mesh(this, mode); + break; +#endif + case MODE_PATTERN: + sp_paint_selector_set_mode_pattern(this, mode); + break; + case MODE_HATCH: + sp_paint_selector_set_mode_hatch(this, mode); + break; + case MODE_SWATCH: + sp_paint_selector_set_mode_swatch(this, mode); + break; + case MODE_UNSET: + sp_paint_selector_set_mode_unset(this); + break; + default: + g_warning("file %s: line %d: Unknown paint mode %d", __FILE__, __LINE__, mode); + break; + } + this->mode = mode; + g_signal_emit(G_OBJECT(this), psel_signals[MODE_CHANGED], 0, this->mode); + update = FALSE; + } +} + +void SPPaintSelector::setFillrule(FillRule fillrule) +{ + if (fillrulebox) { + // TODO this flips widgets but does not use a member to store state. Revisit + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(evenodd), (fillrule == FILLRULE_EVENODD)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(nonzero), (fillrule == FILLRULE_NONZERO)); + } +} + +void SPPaintSelector::setColorAlpha(SPColor const &color, float alpha) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); +/* + guint32 rgba = 0; + + if ( sp_color_get_colorspace_type(color) == SP_COLORSPACE_TYPE_CMYK ) + { +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set CMYKA\n"); +#endif + sp_paint_selector_set_mode(psel, MODE_COLOR_CMYK); + } + else +*/ + { +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set RGBA\n"); +#endif + setMode(MODE_SOLID_COLOR); + } + + updating_color = true; + selected_color->setColorAlpha(color, alpha); + updating_color = false; + //rgba = color.toRGBA32( alpha ); +} + +void SPPaintSelector::setSwatch(SPGradient *vector ) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set SWATCH\n"); +#endif + setMode(MODE_SWATCH); + + SwatchSelector *swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(selector), "swatch-selector")); + if (swatchsel) { + swatchsel->setVector( (vector) ? vector->document : nullptr, vector ); + } +} + +void SPPaintSelector::setGradientLinear(SPGradient *vector) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT LINEAR\n"); +#endif + setMode(MODE_GRADIENT_LINEAR); + + SPGradientSelector *gsel = getGradientFromData(this); + + gsel->setMode(SPGradientSelector::MODE_LINEAR); + gsel->setVector((vector) ? vector->document : nullptr, vector); +} + +void SPPaintSelector::setGradientRadial(SPGradient *vector) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT RADIAL\n"); +#endif + setMode(MODE_GRADIENT_RADIAL); + + SPGradientSelector *gsel = getGradientFromData(this); + + gsel->setMode(SPGradientSelector::MODE_RADIAL); + + gsel->setVector((vector) ? vector->document : nullptr, vector); +} + +#ifdef WITH_MESH +void SPPaintSelector::setGradientMesh(SPMeshGradient *array) +{ +#ifdef SP_PS_VERBOSE + g_print("PaintSelector set GRADIENT MESH\n"); +#endif + setMode(MODE_GRADIENT_MESH); + + // SPGradientSelector *gsel = getGradientFromData(this); + + // gsel->setMode(SPGradientSelector::MODE_GRADIENT_MESH); + // gsel->setVector((mesh) ? mesh->document : 0, mesh); +} +#endif + +void SPPaintSelector::setGradientProperties( SPGradientUnits units, SPGradientSpread spread ) +{ + g_return_if_fail(isPaintModeGradient(mode)); + + SPGradientSelector *gsel = getGradientFromData(this); + gsel->setUnits(units); + gsel->setSpread(spread); +} + +void SPPaintSelector::getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread) const +{ + g_return_if_fail(isPaintModeGradient(mode)); + + SPGradientSelector *gsel = getGradientFromData(this); + units = gsel->getUnits(); + spread = gsel->getSpread(); +} + + +/** + * \post (alpha == NULL) || (*alpha in [0.0, 1.0]). + */ +void SPPaintSelector::getColorAlpha(SPColor &color, gfloat &alpha) const +{ + selected_color->colorAlpha(color, alpha); + + g_assert( ( 0.0 <= alpha ) + && ( alpha <= 1.0 ) ); +} + +SPGradient *SPPaintSelector::getGradientVector() +{ + SPGradient* vect = nullptr; + + if (isPaintModeGradient(mode)) { + SPGradientSelector *gsel = getGradientFromData(this); + vect = gsel->getVector(); + } + + return vect; +} + + +void SPPaintSelector::pushAttrsToGradient( SPGradient *gr ) const +{ + SPGradientUnits units = SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX; + SPGradientSpread spread = SP_GRADIENT_SPREAD_PAD; + getGradientProperties( units, spread ); + gr->setUnits(units); + gr->setSpread(spread); + gr->updateRepr(); +} + +static void +sp_paint_selector_clear_frame(SPPaintSelector *psel) +{ + g_return_if_fail( psel != nullptr); + + if (psel->selector) { + + //This is a hack to work around GtkNotebook bug in ColorSelector. Is sends signal switch-page on destroy + //The widget is hidden first so it can recognize that it should not process signals from notebook child + gtk_widget_set_visible(psel->selector, false); + gtk_widget_destroy(psel->selector); + psel->selector = nullptr; + } +} + +static void +sp_paint_selector_set_mode_empty(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, nullptr); + gtk_widget_set_sensitive(psel->style, FALSE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No objects</b>")); +} + +static void +sp_paint_selector_set_mode_multiple(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, nullptr); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Multiple styles</b>")); +} + +static void +sp_paint_selector_set_mode_unset(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, psel->unset); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Paint is undefined</b>")); +} + +static void +sp_paint_selector_set_mode_none(SPPaintSelector *psel) +{ + sp_paint_selector_set_style_buttons(psel, psel->none); + gtk_widget_set_sensitive(psel->style, TRUE); + + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>No paint</b>")); + +} + +/* Color paint */ + +void SPPaintSelector::onSelectedColorGrabbed() { + g_signal_emit(G_OBJECT(this), psel_signals[GRABBED], 0); +} + +void SPPaintSelector::onSelectedColorDragged() { + if (updating_color) { + return; + } + g_signal_emit(G_OBJECT(this), psel_signals[DRAGGED], 0); +} + +void SPPaintSelector::onSelectedColorReleased() { + g_signal_emit(G_OBJECT(this), psel_signals[RELEASED], 0); +} + +void SPPaintSelector::onSelectedColorChanged() { + if (updating_color) { + return; + } + + if (mode == MODE_SOLID_COLOR) { + g_signal_emit(G_OBJECT(this), psel_signals[CHANGED], 0); + } else { + g_warning("SPPaintSelector::onSelectedColorChanged(): selected color changed while not in color selection mode"); + } +} + +static void sp_paint_selector_set_mode_color(SPPaintSelector *psel, SPPaintSelector::Mode /*mode*/) +{ + using Inkscape::UI::Widget::ColorNotebook; + + if ((psel->mode == SPPaintSelector::MODE_SWATCH) + || (psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR) + || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL) ) { + SPGradientSelector *gsel = getGradientFromData(psel); + if (gsel) { + SPGradient *gradient = gsel->getVector(); + + // Gradient can be null if object paint is changed externally (ie. with a color picker tool) + if (gradient) + { + SPColor color = gradient->getFirstStop()->getColor(); + float alpha = gradient->getFirstStop()->getOpacity(); + psel->selected_color->setColorAlpha(color, alpha, false); + } + } + } + + sp_paint_selector_set_style_buttons(psel, psel->solid); + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_SOLID_COLOR) { + /* Already have color selector */ + // Do nothing + } else { + + sp_paint_selector_clear_frame(psel); + /* Create new color selector */ + /* Create vbox */ + auto vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(vb), FALSE); + gtk_widget_show(vb); + + /* Color selector */ + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(*(psel->selected_color))); + color_selector->show(); + gtk_box_pack_start(GTK_BOX(vb), color_selector->gobj(), TRUE, TRUE, 0); + + /* Pack everything to frame */ + gtk_container_add(GTK_CONTAINER(psel->frame), vb); + + psel->selector = vb; + } + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Flat color</b>")); + +#ifdef SP_PS_VERBOSE + g_print("Color req\n"); +#endif +} + +/* Gradient */ + +static void sp_paint_selector_gradient_grabbed(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[GRABBED], 0); +} + +static void sp_paint_selector_gradient_dragged(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[DRAGGED], 0); +} + +static void sp_paint_selector_gradient_released(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[RELEASED], 0); +} + +static void sp_paint_selector_gradient_changed(SPGradientSelector * /*csel*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + +static void sp_paint_selector_set_mode_gradient(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + GtkWidget *gsel; + + /* fixme: We do not need function-wide gsel at all */ + + if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) { + sp_paint_selector_set_style_buttons(psel, psel->gradient); + } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) { + sp_paint_selector_set_style_buttons(psel, psel->radial); + } + gtk_widget_set_sensitive(psel->style, TRUE); + + if ((psel->mode == SPPaintSelector::MODE_GRADIENT_LINEAR) || (psel->mode == SPPaintSelector::MODE_GRADIENT_RADIAL)) { + /* Already have gradient selector */ + gsel = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "gradient-selector")); + } else { + sp_paint_selector_clear_frame(psel); + /* Create new gradient selector */ + gsel = sp_gradient_selector_new(); + gtk_widget_show(gsel); + g_signal_connect(G_OBJECT(gsel), "grabbed", G_CALLBACK(sp_paint_selector_gradient_grabbed), psel); + g_signal_connect(G_OBJECT(gsel), "dragged", G_CALLBACK(sp_paint_selector_gradient_dragged), psel); + g_signal_connect(G_OBJECT(gsel), "released", G_CALLBACK(sp_paint_selector_gradient_released), psel); + g_signal_connect(G_OBJECT(gsel), "changed", G_CALLBACK(sp_paint_selector_gradient_changed), psel); + /* Pack everything to frame */ + gtk_container_add(GTK_CONTAINER(psel->frame), gsel); + psel->selector = gsel; + g_object_set_data(G_OBJECT(psel->selector), "gradient-selector", gsel); + } + + /* Actually we have to set option menu history here */ + if (mode == SPPaintSelector::MODE_GRADIENT_LINEAR) { + SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_LINEAR); + //sp_gradient_selector_set_mode(SP_GRADIENT_SELECTOR(gsel), SP_GRADIENT_SELECTOR_MODE_LINEAR); + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Linear gradient</b>")); + } else if (mode == SPPaintSelector::MODE_GRADIENT_RADIAL) { + SP_GRADIENT_SELECTOR(gsel)->setMode(SPGradientSelector::MODE_RADIAL); + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Radial gradient</b>")); + } + +#ifdef SP_PS_VERBOSE + g_print("Gradient req\n"); +#endif +} + +// ************************* MESH ************************ +#ifdef WITH_MESH +static void sp_psel_mesh_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/) +{ + // drop our reference to the mesh menu widget + g_object_unref( G_OBJECT(widget) ); +} + +static void sp_psel_mesh_change(GtkWidget * /*widget*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + + +/** + * Returns a list of meshes in the defs of the given source document as a vector + */ +static std::vector<SPMeshGradient *> +ink_mesh_list_get (SPDocument *source) +{ + std::vector<SPMeshGradient *> pl; + if (source == nullptr) + return pl; + + + std::vector<SPObject *> meshes = source->getResourceList("gradient"); + for (auto meshe : meshes) { + if (SP_IS_MESHGRADIENT(meshe) && + SP_GRADIENT(meshe) == SP_GRADIENT(meshe)->getArray()) { // only if this is a root mesh + pl.push_back(SP_MESHGRADIENT(meshe)); + } + } + return pl; +} + +/** + * Adds menu items for mesh list. + */ +static void +sp_mesh_menu_build (GtkWidget *combo, std::vector<SPMeshGradient *> &mesh_list, SPDocument */*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i:mesh_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + + gchar const *meshid = repr->attribute("id"); + gchar const *label = meshid; + + // Only relevant if we supply a set of canned meshes. + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_MESH, meshid, COMBO_COL_SEP, FALSE, -1); + + } +} + +/** + * Pick up all meshes from source, except those that are in + * current_doc (if non-NULL), and add items to the mesh menu. + */ +static void sp_mesh_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*mesh_doc*/) +{ + std::vector<SPMeshGradient *> pl = ink_mesh_list_get(source); + sp_mesh_menu_build (combo, pl, source); +} + + +static void +ink_mesh_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *meshes_doc = nullptr; + + // If we ever add a list of canned mesh gradients, uncomment following: + + // find and load meshes.svg + // if (meshes_doc == NULL) { + // char *meshes_source = g_build_filename(INKSCAPE_MESHESDIR, "meshes.svg", NULL); + // if (Inkscape::IO::file_test(meshes_source, G_FILE_TEST_IS_REGULAR)) { + // meshes_doc = SPDocument::createNewDoc(meshes_source, FALSE); + // } + // g_free(meshes_source); + // } + + // suck in from current doc + sp_mesh_list_from_doc ( combo, nullptr, doc, meshes_doc ); + + // add separator + // { + // GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + // GtkTreeIter iter; + // gtk_list_store_append (store, &iter); + // gtk_list_store_set(store, &iter, + // COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, true, -1); + // } + + // suck in from meshes.svg + // if (meshes_doc) { + // doc->ensureUpToDate(); + // sp_mesh_list_from_doc ( combo, doc, meshes_doc, NULL ); + // } + +} + + +static GtkWidget* +ink_mesh_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_MESH, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_mesh_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update mesh list*/ +void SPPaintSelector::updateMeshList( SPMeshGradient *mesh ) +{ + if (update) { + return; + } + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu")); + g_assert( combo != nullptr ); + + /* Clear existing menu if any */ + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_mesh_menu(combo); + + /* Set history */ + + if (mesh && !g_object_get_data(G_OBJECT(combo), "update")) { + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE)); + gchar const *meshname = mesh->getRepr()->attribute("id"); + + // Find this mesh and set it active in the combo_box + GtkTreeIter iter ; + gchar *meshid = nullptr; + bool valid = gtk_tree_model_get_iter_first (store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1); + while (valid && strcmp(meshid, meshname) != 0) { + valid = gtk_tree_model_iter_next (store, &iter); + g_free(meshid); + meshid = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_MESH, &meshid, -1); + } + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE)); + g_free(meshid); + } +} + +static void sp_paint_selector_set_mode_mesh(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_GRADIENT_MESH) { + sp_paint_selector_set_style_buttons(psel, psel->mesh); + } + gtk_widget_set_sensitive(psel->style, TRUE); + + GtkWidget *tbl = nullptr; + + if (psel->mode == SPPaintSelector::MODE_GRADIENT_MESH) { + /* Already have mesh menu */ + tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "mesh-selector")); + } else { + sp_paint_selector_clear_frame(psel); + + /* Create vbox */ + tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE); + gtk_widget_show(tbl); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the mesh, is stockid or not, is a separator or not. + */ + GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL); + + ink_mesh_menu(combo); + g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_mesh_change), psel); + g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_mesh_destroy), psel); + g_object_set_data(G_OBJECT(psel), "meshmenu", combo); + g_object_ref( G_OBJECT(combo)); + + gtk_container_add(GTK_CONTAINER(hb), combo); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref( G_OBJECT(store)); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + auto l = gtk_label_new(nullptr); + gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Mesh tool</b> to modify the mesh.")); + gtk_label_set_line_wrap(GTK_LABEL(l), true); + gtk_widget_set_size_request(l, 180, -1); + gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + } + + gtk_widget_show_all(tbl); + + gtk_container_add(GTK_CONTAINER(psel->frame), tbl); + psel->selector = tbl; + g_object_set_data(G_OBJECT(psel->selector), "mesh-selector", tbl); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Mesh fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Mesh req\n"); +#endif +} + +SPMeshGradient *SPPaintSelector::getMeshGradient() +{ + g_return_val_if_fail((mode == MODE_GRADIENT_MESH) , NULL); + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "meshmenu")); + + /* no mesh menu if we were just selected */ + if ( combo == nullptr ) { + return nullptr; + } + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + + /* Get the selected mesh */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX(combo), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *meshid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1); + // gtk_tree_model_get (store, &iter, COMBO_COL_LABEL, &label, COMBO_COL_STOCK, &stockid, COMBO_COL_MESH, &meshid, -1); + // std::cout << " .. meshid: " << (meshid?meshid:"null") << " label: " << (label?label:"null") << std::endl; + // g_free(label); + if (meshid == nullptr) { + return nullptr; + } + + SPMeshGradient *mesh = nullptr; + if (strcmp(meshid, "none")){ + + gchar *mesh_name; + if (stockid) { + mesh_name = g_strconcat("urn:inkscape:mesh:", meshid, NULL); + } else { + mesh_name = g_strdup(meshid); + } + + SPObject *mesh_obj = get_stock_item(mesh_name); + if (mesh_obj && SP_IS_MESHGRADIENT(mesh_obj)) { + mesh = SP_MESHGRADIENT(mesh_obj); + } + g_free(mesh_name); + } else { + std::cerr << "SPPaintSelector::getMeshGradient: Unexpected meshid value." << std::endl; + } + + g_free(meshid); + + return mesh; +} + +#endif +// ************************ End Mesh ************************ + +static void +sp_paint_selector_set_style_buttons(SPPaintSelector *psel, GtkWidget *active) +{ + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->none), (active == psel->none)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->solid), (active == psel->solid)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->gradient), (active == psel->gradient)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->radial), (active == psel->radial)); +#ifdef WITH_MESH + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->mesh), (active == psel->mesh)); +#endif + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->pattern), (active == psel->pattern)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->swatch), (active == psel->swatch)); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(psel->unset), (active == psel->unset)); +} + +static void sp_psel_pattern_destroy(GtkWidget *widget, SPPaintSelector * /*psel*/) +{ + // drop our reference to the pattern menu widget + g_object_unref( G_OBJECT(widget) ); +} + +static void sp_psel_pattern_change(GtkWidget * /*widget*/, SPPaintSelector *psel) +{ + g_signal_emit(G_OBJECT(psel), psel_signals[CHANGED], 0); +} + + + +/** + * Returns a list of patterns in the defs of the given source document as a vector + */ +static std::vector<SPPattern*> +ink_pattern_list_get (SPDocument *source) +{ + std::vector<SPPattern *> pl; + if (source == nullptr) + return pl; + + std::vector<SPObject *> patterns = source->getResourceList("pattern"); + for (auto pattern : patterns) { + if (SP_PATTERN(pattern) == SP_PATTERN(pattern)->rootPattern()) { // only if this is a root pattern + pl.push_back(SP_PATTERN(pattern)); + } + } + + return pl; +} + +/** + * Adds menu items for pattern list - derived from marker code, left hb etc in to make addition of previews easier at some point. + */ +static void +sp_pattern_menu_build (GtkWidget *combo, std::vector<SPPattern *> &pl, SPDocument */*source*/) +{ + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + for (auto i=pl.rbegin(); i!=pl.rend(); ++i) { + + Inkscape::XML::Node *repr = (*i)->getRepr(); + + // label for combobox + gchar const *label; + if (repr->attribute("inkscape:stockid")) { + label = _(repr->attribute("inkscape:stockid")); + } else { + label = _(repr->attribute("id")); + } + + gchar const *patid = repr->attribute("id"); + + gboolean stockid = false; + if (repr->attribute("inkscape:stockid")) { + stockid = true; + } + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, label, COMBO_COL_STOCK, stockid, COMBO_COL_PATTERN, patid, COMBO_COL_SEP, FALSE, -1); + + } +} + +/** + * Pick up all patterns from source, except those that are in + * current_doc (if non-NULL), and add items to the pattern menu. + */ +static void sp_pattern_list_from_doc(GtkWidget *combo, SPDocument * /*current_doc*/, SPDocument *source, SPDocument * /*pattern_doc*/) +{ + std::vector<SPPattern *> pl = ink_pattern_list_get(source); + sp_pattern_menu_build (combo, pl, source); +} + + +static void +ink_pattern_menu_populate_menu(GtkWidget *combo, SPDocument *doc) +{ + static SPDocument *patterns_doc = nullptr; + + // find and load patterns.svg + if (patterns_doc == nullptr) { + char *patterns_source = g_build_filename(INKSCAPE_PAINTDIR, "patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns_source, G_FILE_TEST_IS_REGULAR)) { + patterns_doc = SPDocument::createNewDoc(patterns_source, FALSE); + } + g_free(patterns_source); + } + + // suck in from current doc + sp_pattern_list_from_doc ( combo, nullptr, doc, patterns_doc ); + + // add separator + { + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + gtk_list_store_append (store, &iter); + gtk_list_store_set(store, &iter, + COMBO_COL_LABEL, "", COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, true, -1); + } + + // suck in from patterns.svg + if (patterns_doc) { + doc->ensureUpToDate(); + sp_pattern_list_from_doc ( combo, doc, patterns_doc, nullptr ); + } + +} + + +static GtkWidget* +ink_pattern_menu(GtkWidget *combo) +{ + SPDocument *doc = SP_ACTIVE_DOCUMENT; + + GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo))); + GtkTreeIter iter; + + if (!doc) { + + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, + COMBO_COL_LABEL, _("No document selected"), COMBO_COL_STOCK, false, COMBO_COL_PATTERN, "", COMBO_COL_SEP, false, -1); + gtk_widget_set_sensitive(combo, FALSE); + + } else { + + ink_pattern_menu_populate_menu(combo, doc); + gtk_widget_set_sensitive(combo, TRUE); + + } + + // Select the first item that is not a separator + if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL(store), &iter)) { + gboolean sep = false; + gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, COMBO_COL_SEP, &sep, -1); + if (sep) { + gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); + } + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + return combo; +} + + +/*update pattern list*/ +void SPPaintSelector::updatePatternList( SPPattern *pattern ) +{ + if (update) { + return; + } + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu")); + g_assert( combo != nullptr ); + + /* Clear existing menu if any */ + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + gtk_list_store_clear(GTK_LIST_STORE(store)); + + ink_pattern_menu(combo); + + /* Set history */ + + if (pattern && !g_object_get_data(G_OBJECT(combo), "update")) { + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(TRUE)); + gchar const *patname = pattern->getRepr()->attribute("id"); + + // Find this pattern and set it active in the combo_box + GtkTreeIter iter ; + gchar *patid = nullptr; + bool valid = gtk_tree_model_get_iter_first (store, &iter); + if (!valid) { + return; + } + gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1); + while (valid && strcmp(patid, patname) != 0) { + valid = gtk_tree_model_iter_next (store, &iter); + g_free(patid); + patid = nullptr; + gtk_tree_model_get (store, &iter, COMBO_COL_PATTERN, &patid, -1); + } + g_free(patid); + + if (valid) { + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(combo), &iter); + } + + g_object_set_data(G_OBJECT(combo), "update", GINT_TO_POINTER(FALSE)); + } +} + +static void sp_paint_selector_set_mode_pattern(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_PATTERN) { + sp_paint_selector_set_style_buttons(psel, psel->pattern); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + GtkWidget *tbl = nullptr; + + if (psel->mode == SPPaintSelector::MODE_PATTERN) { + /* Already have pattern menu */ + tbl = GTK_WIDGET(g_object_get_data(G_OBJECT(psel->selector), "pattern-selector")); + } else { + sp_paint_selector_clear_frame(psel); + + /* Create vbox */ + tbl = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); + gtk_box_set_homogeneous(GTK_BOX(tbl), FALSE); + gtk_widget_show(tbl); + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + + /** + * Create a combo_box and store with 4 columns, + * The label, a pointer to the pattern, is stockid or not, is a separator or not. + */ + GtkListStore *store = gtk_list_store_new (COMBO_N_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkWidget *combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (store)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(combo), SPPaintSelector::isSeparator, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), renderer, "text", COMBO_COL_LABEL, NULL); + + ink_pattern_menu(combo); + g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(sp_psel_pattern_change), psel); + g_signal_connect(G_OBJECT(combo), "destroy", G_CALLBACK(sp_psel_pattern_destroy), psel); + g_object_set_data(G_OBJECT(psel), "patternmenu", combo); + g_object_ref( G_OBJECT(combo)); + + gtk_container_add(GTK_CONTAINER(hb), combo); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + + g_object_unref( G_OBJECT(store)); + } + + { + auto hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_set_homogeneous(GTK_BOX(hb), FALSE); + auto l = gtk_label_new(nullptr); + gtk_label_set_markup(GTK_LABEL(l), _("Use the <b>Node tool</b> to adjust position, scale, and rotation of the pattern on canvas. Use <b>Object > Pattern > Objects to Pattern</b> to create a new pattern from selection.")); + gtk_label_set_line_wrap(GTK_LABEL(l), true); + gtk_widget_set_size_request(l, 180, -1); + gtk_box_pack_start(GTK_BOX(hb), l, TRUE, TRUE, AUX_BETWEEN_BUTTON_GROUPS); + gtk_box_pack_start(GTK_BOX(tbl), hb, FALSE, FALSE, AUX_BETWEEN_BUTTON_GROUPS); + } + + gtk_widget_show_all(tbl); + + gtk_container_add(GTK_CONTAINER(psel->frame), tbl); + psel->selector = tbl; + g_object_set_data(G_OBJECT(psel->selector), "pattern-selector", tbl); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Pattern fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Pattern req\n"); +#endif +} + +static void sp_paint_selector_set_mode_hatch(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_HATCH) { + sp_paint_selector_set_style_buttons(psel, psel->unset); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_HATCH) { + /* Already have hatch menu, for the moment unset */ + } else { + sp_paint_selector_clear_frame(psel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Hatch fill</b>")); + } +#ifdef SP_PS_VERBOSE + g_print("Hatch req\n"); +#endif +} + +gboolean SPPaintSelector::isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) { + + gboolean sep = FALSE; + gtk_tree_model_get(model, iter, COMBO_COL_SEP, &sep, -1); + return sep; +} + +SPPattern *SPPaintSelector::getPattern() +{ + SPPattern *pat = nullptr; + g_return_val_if_fail(mode == MODE_PATTERN, NULL); + + GtkWidget *combo = GTK_WIDGET(g_object_get_data(G_OBJECT(this), "patternmenu")); + + /* no pattern menu if we were just selected */ + if (combo == nullptr) { + return nullptr; + } + + GtkTreeModel *store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); + + /* Get the selected pattern */ + GtkTreeIter iter; + if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter) || + !gtk_list_store_iter_is_valid(GTK_LIST_STORE(store), &iter)) { + return nullptr; + } + + gchar *patid = nullptr; + gboolean stockid = FALSE; + // gchar *label = nullptr; + gtk_tree_model_get(store, &iter, + // COMBO_COL_LABEL, &label, + COMBO_COL_STOCK, &stockid, + COMBO_COL_PATTERN, &patid, -1); + // g_free(label); + if (patid == nullptr) { + return nullptr; + } + + if (strcmp(patid, "none") != 0) { + gchar *paturn; + + if (stockid) { + paturn = g_strconcat("urn:inkscape:pattern:", patid, NULL); + } else { + paturn = g_strdup(patid); + } + SPObject *pat_obj = get_stock_item(paturn); + if (pat_obj) { + pat = SP_PATTERN(pat_obj); + } + g_free(paturn); + } else { + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *pat_obj = doc->getObjectById(patid); + + if (pat_obj && SP_IS_PATTERN(pat_obj)) { + pat = SP_PATTERN(pat_obj)->rootPattern(); + } + } + + g_free(patid); + + return pat; +} + +static void sp_paint_selector_set_mode_swatch(SPPaintSelector *psel, SPPaintSelector::Mode mode) +{ + if (mode == SPPaintSelector::MODE_SWATCH) { + sp_paint_selector_set_style_buttons(psel, psel->swatch); + } + + gtk_widget_set_sensitive(psel->style, TRUE); + + if (psel->mode == SPPaintSelector::MODE_SWATCH){ + // swatchsel = static_cast<SwatchSelector*>(g_object_get_data(G_OBJECT(psel->selector), "swatch-selector")); + } else { + sp_paint_selector_clear_frame(psel); + // Create new gradient selector + SwatchSelector *swatchsel = new SwatchSelector(); + swatchsel->show(); + + swatchsel->connectGrabbedHandler( G_CALLBACK(sp_paint_selector_gradient_grabbed), psel ); + swatchsel->connectDraggedHandler( G_CALLBACK(sp_paint_selector_gradient_dragged), psel ); + swatchsel->connectReleasedHandler( G_CALLBACK(sp_paint_selector_gradient_released), psel ); + swatchsel->connectchangedHandler( G_CALLBACK(sp_paint_selector_gradient_changed), psel ); + + // Pack everything to frame + gtk_container_add(GTK_CONTAINER(psel->frame), GTK_WIDGET(swatchsel->gobj())); + psel->selector = GTK_WIDGET(swatchsel->gobj()); + g_object_set_data(G_OBJECT(psel->selector), "swatch-selector", swatchsel); + + gtk_label_set_markup(GTK_LABEL(psel->label), _("<b>Swatch fill</b>")); + } + +#ifdef SP_PS_VERBOSE + g_print("Swatch req\n"); +#endif +} + +// TODO this seems very bad to be taking in a desktop pointer to muck with. Logic probably belongs elsewhere +void SPPaintSelector::setFlatColor( SPDesktop *desktop, gchar const *color_property, gchar const *opacity_property ) +{ + SPCSSAttr *css = sp_repr_css_attr_new(); + + SPColor color; + gfloat alpha = 0; + getColorAlpha( color, alpha ); + + std::string colorStr = color.toString(); + +#ifdef SP_PS_VERBOSE + guint32 rgba = color.toRGBA32( alpha ); + g_message("sp_paint_selector_set_flat_color() to '%s' from 0x%08x::%s", + colorStr.c_str(), + rgba, + (color.icc ? color.icc->colorProfile.c_str():"<null>") ); +#endif // SP_PS_VERBOSE + + sp_repr_css_set_property(css, color_property, colorStr.c_str()); + Inkscape::CSSOStringStream osalpha; + osalpha << alpha; + sp_repr_css_set_property(css, opacity_property, osalpha.str().c_str()); + + sp_desktop_set_style(desktop, css); + + sp_repr_css_attr_unref(css); +} + +SPPaintSelector::Mode SPPaintSelector::getModeForStyle(SPStyle const & style, FillOrStroke kind) +{ + Mode mode = MODE_UNSET; + SPIPaint const &target = *style.getFillOrStroke(kind == FILL); + + if ( !target.set ) { + mode = MODE_UNSET; + } else if ( target.isPaintserver() ) { + SPPaintServer const *server = (kind == FILL) ? style.getFillPaintServer() : style.getStrokePaintServer(); + +#ifdef SP_PS_VERBOSE + g_message("SPPaintSelector::getModeForStyle(%p, %d)", &style, kind); + g_message("==== server:%p %s grad:%s swatch:%s", server, server->getId(), (SP_IS_GRADIENT(server)?"Y":"n"), (SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()?"Y":"n")); +#endif // SP_PS_VERBOSE + + + if (server && SP_IS_GRADIENT(server) && SP_GRADIENT(server)->getVector()->isSwatch()) { + mode = MODE_SWATCH; + } else if (SP_IS_LINEARGRADIENT(server)) { + mode = MODE_GRADIENT_LINEAR; + } else if (SP_IS_RADIALGRADIENT(server)) { + mode = MODE_GRADIENT_RADIAL; +#ifdef WITH_MESH + } else if (SP_IS_MESHGRADIENT(server)) { + mode = MODE_GRADIENT_MESH; +#endif + } else if (SP_IS_PATTERN(server)) { + mode = MODE_PATTERN; + } else if (SP_IS_HATCH(server)) { + mode = MODE_HATCH; + } else { + g_warning( "file %s: line %d: Unknown paintserver", __FILE__, __LINE__ ); + mode = MODE_NONE; + } + } else if ( target.isColor() ) { + // TODO this is no longer a valid assertion: + mode = MODE_SOLID_COLOR; // so far only rgb can be read from svg + } else if ( target.isNone() ) { + mode = MODE_NONE; + } else { + g_warning( "file %s: line %d: Unknown paint type", __FILE__, __LINE__ ); + mode = MODE_NONE; + } + + return mode; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/paint-selector.h b/src/widgets/paint-selector.h new file mode 100644 index 0000000..7d142f3 --- /dev/null +++ b/src/widgets/paint-selector.h @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Generic paint selector widget + *//* + * Authors: + * Lauris + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_PAINT_SELECTOR_H +#define SEEN_SP_PAINT_SELECTOR_H + +#include <glib.h> +#include <gtk/gtk.h> + +#include "color.h" +#include "fill-or-stroke.h" + +#include "object/sp-gradient-spread.h" +#include "object/sp-gradient-units.h" + +#include "ui/selected-color.h" + +class SPGradient; +#ifdef WITH_MESH +class SPMeshGradient; +#endif +class SPDesktop; +class SPPattern; +class SPStyle; + +#define SP_TYPE_PAINT_SELECTOR (sp_paint_selector_get_type ()) +#define SP_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_PAINT_SELECTOR, SPPaintSelector)) +#define SP_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_PAINT_SELECTOR, SPPaintSelectorClass)) +#define SP_IS_PAINT_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_PAINT_SELECTOR)) +#define SP_IS_PAINT_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_PAINT_SELECTOR)) + +/** + * Generic paint selector widget. + */ +struct SPPaintSelector { + GtkBox vbox; + + enum Mode { + MODE_EMPTY, + MODE_MULTIPLE, + MODE_NONE, + MODE_SOLID_COLOR, + MODE_GRADIENT_LINEAR, + MODE_GRADIENT_RADIAL, +#ifdef WITH_MESH + MODE_GRADIENT_MESH, +#endif + MODE_PATTERN, + MODE_HATCH, + MODE_SWATCH, + MODE_UNSET + } ; + + enum FillRule { + FILLRULE_NONZERO, + FILLRULE_EVENODD + } ; + + guint update : 1; + + Mode mode; + + GtkWidget *style; + GtkWidget *none; + GtkWidget *solid; + GtkWidget *gradient; + GtkWidget *radial; +#ifdef WITH_MESH + GtkWidget *mesh; +#endif + GtkWidget *pattern; + GtkWidget *swatch; + GtkWidget *unset; + + GtkWidget *fillrulebox; + GtkWidget *evenodd, *nonzero; + + GtkWidget *frame, *selector; + GtkWidget *label; + + Inkscape::UI::SelectedColor *selected_color; + bool updating_color; + + static Mode getModeForStyle(SPStyle const & style, FillOrStroke kind); + + void setMode( Mode mode ); + void setFillrule( FillRule fillrule ); + + void setColorAlpha( SPColor const &color, float alpha ); + void getColorAlpha( SPColor &color, gfloat &alpha ) const; + + void setGradientLinear( SPGradient *vector ); + void setGradientRadial( SPGradient *vector ); +#ifdef WITH_MESH + void setGradientMesh(SPMeshGradient *array); +#endif + void setSwatch( SPGradient *vector ); + + void setGradientProperties( SPGradientUnits units, SPGradientSpread spread ); + void getGradientProperties( SPGradientUnits &units, SPGradientSpread &spread ) const; + + void pushAttrsToGradient( SPGradient *gr ) const; + SPGradient *getGradientVector(); + +#ifdef WITH_MESH + SPMeshGradient * getMeshGradient(); + void updateMeshList( SPMeshGradient *pat ); +#endif + + SPPattern * getPattern(); + void updatePatternList( SPPattern *pat ); + + static gboolean isSeparator (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + + // TODO move this elsewhere: + void setFlatColor( SPDesktop *desktop, const gchar *color_property, const gchar *opacity_property ); + + void onSelectedColorGrabbed(); + void onSelectedColorDragged(); + void onSelectedColorReleased(); + void onSelectedColorChanged(); +}; + +enum { + COMBO_COL_LABEL = 0, + COMBO_COL_STOCK = 1, + COMBO_COL_PATTERN = 2, + COMBO_COL_MESH = COMBO_COL_PATTERN, + COMBO_COL_SEP = 3, + COMBO_N_COLS = 4 +}; + +/// The SPPaintSelector vtable +struct SPPaintSelectorClass { + GtkBoxClass parent_class; + + void (* mode_changed) (SPPaintSelector *psel, SPPaintSelector::Mode mode); + + void (* grabbed) (SPPaintSelector *psel); + void (* dragged) (SPPaintSelector *psel); + void (* released) (SPPaintSelector *psel); + void (* changed) (SPPaintSelector *psel); + void (* fillrule_changed) (SPPaintSelector *psel, SPPaintSelector::FillRule fillrule); +}; + +GType sp_paint_selector_get_type (); + +SPPaintSelector *sp_paint_selector_new(FillOrStroke kind); + + + +#endif // SEEN_SP_PAINT_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-attribute-widget.cpp b/src/widgets/sp-attribute-widget.cpp new file mode 100644 index 0000000..a82d06d --- /dev/null +++ b/src/widgets/sp-attribute-widget.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Base widget for user input of object properties. + */ +/* Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2012, authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm/entry.h> +#include <gtkmm/grid.h> + +#include "sp-attribute-widget.h" + +#include "include/macros.h" +#include "document.h" +#include "document-undo.h" +#include "verbs.h" + +#include "include/gtkmm_version.h" + +#include "object/sp-object.h" + +#include "xml/repr.h" + +using Inkscape::DocumentUndo; + +/** + * Callback for user input in one of the entries. + * + * sp_attribute_table_entry_changed set the object property + * to the new value and updates history. It is a callback from + * the entries created by SPAttributeTable. + * + * @param editable pointer to the entry box. + * @param spat pointer to the SPAttributeTable instance. + */ +static void sp_attribute_table_entry_changed (Gtk::Entry *editable, SPAttributeTable *spat); +/** + * Callback for a modification of the selected object (size, color, properties, etc.). + * + * sp_attribute_table_object_modified rereads the object properties + * and shows the values in the entry boxes. It is a callback from a + * connection of the SPObject. + * + * @param object the SPObject to which this instance is referring to. + * @param flags gives the applied modifications + * @param spat pointer to the SPAttributeTable instance. + */ +static void sp_attribute_table_object_modified (SPObject *object, guint flags, SPAttributeTable *spaw); +/** + * Callback for the deletion of the selected object. + * + * sp_attribute_table_object_release invalidates all data of + * SPAttributeTable and disables the widget. + */ +static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat); + +#define XPAD 4 +#define YPAD 0 + + +SPAttributeTable::SPAttributeTable () : + _object(nullptr), + blocked(false), + table(nullptr), + _attributes(), + _entries(), + modified_connection(), + release_connection() +{ +} + +SPAttributeTable::SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent) : + _object(nullptr), + blocked(false), + table(nullptr), + _attributes(), + _entries(), + modified_connection(), + release_connection() +{ + set_object(object, labels, attributes, parent); +} + +SPAttributeTable::~SPAttributeTable () +{ + clear(); +} + +void SPAttributeTable::clear() +{ + if (table) + { + std::vector<Gtk::Widget*> ch = table->get_children(); + for (int i = (ch.size())-1; i >=0 ; i--) + { + Gtk::Widget *w = ch[i]; + ch.pop_back(); + if (w != nullptr) + { + try + { + sp_signal_disconnect_by_data (w->gobj(), this); + delete w; + } + catch(...) + { + } + } + } + ch.clear(); + _attributes.clear(); + _entries.clear(); + + delete table; + table = nullptr; + } + + if (_object) + { + modified_connection.disconnect(); + release_connection.disconnect(); + _object = nullptr; + } +} + +void SPAttributeTable::set_object(SPObject *object, + std::vector<Glib::ustring> &labels, + std::vector<Glib::ustring> &attributes, + GtkWidget* parent) +{ + g_return_if_fail (!object || SP_IS_OBJECT (object)); + g_return_if_fail (!object || !labels.empty() || !attributes.empty()); + g_return_if_fail (labels.size() == attributes.size()); + + clear(); + _object = object; + + if (object) { + blocked = true; + + // Set up object + modified_connection = object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this)); + release_connection = object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this)); + + // Create table + table = new Gtk::Grid(); + + if (!(parent == nullptr)) + gtk_container_add(GTK_CONTAINER(parent), (GtkWidget*)table->gobj()); + + // Fill rows + _attributes = attributes; + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Label *ll = new Gtk::Label (_(labels[i].c_str())); + ll->show(); + ll->set_halign(Gtk::ALIGN_START); + ll->set_valign(Gtk::ALIGN_CENTER); + ll->set_vexpand(); + ll->set_margin_start(XPAD); + ll->set_margin_end(XPAD); + ll->set_margin_top(XPAD); + ll->set_margin_bottom(XPAD); + table->attach(*ll, 0, i, 1, 1); + + Gtk::Entry *ee = new Gtk::Entry(); + ee->show(); + const gchar *val = object->getRepr()->attribute(attributes[i].c_str()); + ee->set_text (val ? val : (const gchar *) ""); + ee->set_hexpand(); + ee->set_vexpand(); + ee->set_margin_start(XPAD); + ee->set_margin_end(XPAD); + ee->set_margin_top(XPAD); + ee->set_margin_bottom(XPAD); + table->attach(*ee, 1, i, 1, 1); + + _entries.push_back(ee); + g_signal_connect ( ee->gobj(), "changed", + G_CALLBACK (sp_attribute_table_entry_changed), + this ); + } + /* Show table */ + table->show (); + blocked = false; + } +} + +void SPAttributeTable::change_object(SPObject *object) +{ + g_return_if_fail (!object || SP_IS_OBJECT (object)); + if (_object) + { + modified_connection.disconnect(); + release_connection.disconnect(); + _object = nullptr; + } + + _object = object; + if (_object) { + blocked = true; + + // Set up object + modified_connection = _object->connectModified(sigc::bind<2>(sigc::ptr_fun(&sp_attribute_table_object_modified), this)); + release_connection = _object->connectRelease (sigc::bind<1>(sigc::ptr_fun(&sp_attribute_table_object_release), this)); + for (guint i = 0; i < (_attributes.size()); i++) { + const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str()); + _entries[i]->set_text(val ? val : ""); + } + + blocked = false; + } + +} + +void SPAttributeTable::reread_properties() +{ + blocked = true; + for (guint i = 0; i < (_attributes.size()); i++) + { + const gchar *val = _object->getRepr()->attribute(_attributes[i].c_str()); + _entries[i]->set_text(val ? val : ""); + } + blocked = false; +} + +static void sp_attribute_table_object_modified ( SPObject */*object*/, + guint flags, + SPAttributeTable *spat ) +{ + if (flags & SP_OBJECT_MODIFIED_FLAG) + { + std::vector<Glib::ustring> attributes = spat->get_attributes(); + std::vector<Gtk::Entry *> entries = spat->get_entries(); + Glib::ustring text=""; + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Entry* e = entries[i]; + const gchar *val = spat->_object->getRepr()->attribute(attributes[i].c_str()); + text = e->get_text (); + if (val || !text.empty()) { + if (text != val) { + // We are different + spat->blocked = true; + e->set_text (val ? val : (const gchar *) ""); + spat->blocked = false; + } + } + } + } + +} // end of sp_attribute_table_object_modified() + +static void sp_attribute_table_entry_changed ( Gtk::Entry *editable, + SPAttributeTable *spat ) +{ + if (!spat->blocked) + { + std::vector<Glib::ustring> attributes = spat->get_attributes(); + std::vector<Gtk::Entry *> entries = spat->get_entries(); + for (guint i = 0; i < (attributes.size()); i++) { + Gtk::Entry *e = entries[i]; + if ((GtkWidget*)editable == (GtkWidget*)e->gobj()) { + spat->blocked = true; + Glib::ustring text = e->get_text (); + if (spat->_object) { + spat->_object->getRepr()->setAttribute(attributes[i], text); + DocumentUndo::done(spat->_object->document, SP_VERB_NONE, + _("Set attribute")); + } + spat->blocked = false; + return; + } + } + g_warning ("file %s: line %d: Entry signalled change, but there is no such entry", __FILE__, __LINE__); + } + +} // end of sp_attribute_table_entry_changed() + +static void sp_attribute_table_object_release (SPObject */*object*/, SPAttributeTable *spat) +{ + std::vector<Glib::ustring> labels; + std::vector<Glib::ustring> attributes; + spat->set_object (nullptr, labels, attributes, nullptr); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-attribute-widget.h b/src/widgets/sp-attribute-widget.h new file mode 100644 index 0000000..f43fe84 --- /dev/null +++ b/src/widgets/sp-attribute-widget.h @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widget that listens and modifies repr attributes. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2002,2011-2012 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H +#define SEEN_DIALOGS_SP_ATTRIBUTE_WIDGET_H + +#include <gtkmm/widget.h> +#include <cstddef> +#include <sigc++/connection.h> + +namespace Gtk { +class Entry; +class Grid; +} + +namespace Inkscape { +namespace XML { +class Node; +} +} + +class SPObject; + +/** + * A base class for dialogs to enter the value of several properties. + * + * SPAttributeTable is used if you want to alter several properties of + * an object. For each property, it creates an entry next to a label and + * positiones these labels and entries one by one below each other. + */ +class SPAttributeTable : public Gtk::Widget { +public: + /** + * Constructor defaulting to no content. + */ + SPAttributeTable (); + + /** + * Constructor referring to a specific object. + * + * This constructor initializes all data fields and creates the necessary widgets. + * set_object is called for this purpose. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttributeTable instance. + * @param labels list of labels to be shown for the different attributes. + * @param attributes list of attributes whose value can be edited. + * @param parent the parent object owning the SPAttributeTable instance. + * + * @see set_object + */ + SPAttributeTable (SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent); + + ~SPAttributeTable () override; + + /** + * Sets class properties and creates child widgets + * + * set_object initializes all data fields, creates links to the + * SPOject item and creates the necessary widgets. For n properties + * n labels and n entries are created and shown in tabular format. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance. + * @param labels list of labels to be shown for the different attributes. + * @param attributes list of attributes whose value can be edited. + * @param parent the parent object owning the SPAttributeTable instance. + */ + void set_object(SPObject *object, std::vector<Glib::ustring> &labels, std::vector<Glib::ustring> &attributes, GtkWidget* parent); + + /** + * Update values in entry boxes on change of object. + * + * change_object updates the values of the entry boxes in case the user + * of Inkscape selects an other object. + * change_object is a subset of set_object and should only be called by + * the parent class (holding the SPAttributeTable instance). This function + * should only be called when the number of properties/entries nor + * the labels do not change. + * + * @param object the SPObject to which this instance is referring to. It should be the object that is currently selected and whose properties are being shown by this SPAttribuTable instance. + */ + void change_object(SPObject *object); + + /** + * Clears data of SPAttributeTable instance, destroys all child widgets and closes connections. + */ + void clear(); + + /** + * Reads the object attributes. + * + * Reads the object attributes and shows the new object attributes in the + * entry boxes. Caution: function should only be used when which there is + * no change in which objects are selected. + */ + void reread_properties(); + + /** + * Gives access to the attributes list. + */ + std::vector<Glib::ustring> get_attributes() {return _attributes;}; + + /** + * Gives access to the Gtk::Entry list. + */ + std::vector<Gtk::Entry *> get_entries() {return _entries;}; + + /** + * Stores pointer to the selected object. + */ + SPObject *_object; + + /** + * Indicates whether SPAttributeTable is processing callbacks and whether it should accept any updating. + */ + bool blocked; + +private: + /** + * Container widget for the dynamically created child widgets (labels and entry boxes). + */ + Gtk::Grid *table; + + /** + * List of attributes. + * + * _attributes stores the attribute names of the selected object that + * are valid and can be modified through this widget. + */ + std::vector<Glib::ustring> _attributes; + /** + * List of pointers to the respective entry boxes. + * + * _entries stores pointers to the dynamically created entry boxes in which + * the user can midify the attributes of the selected object. + */ + std::vector<Gtk::Entry *> _entries; + + /** + * Sets the callback for a modification of the selection. + */ + sigc::connection modified_connection; + + /** + * Sets the callback for the deletion of the selected object. + */ + sigc::connection release_connection; +}; + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-color-selector.cpp b/src/widgets/sp-color-selector.cpp new file mode 100644 index 0000000..c1c6975 --- /dev/null +++ b/src/widgets/sp-color-selector.cpp @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <gtk/gtk.h> +#include <glibmm/i18n.h> +#include "sp-color-selector.h" + +enum { + GRABBED, + DRAGGED, + RELEASED, + CHANGED, + LAST_SIGNAL +}; + +#define noDUMP_CHANGE_INFO +#define FOO_NAME(x) g_type_name( G_TYPE_FROM_INSTANCE(x) ) + +static void sp_color_selector_dispose(GObject *object); + +static void sp_color_selector_show_all( GtkWidget *widget ); +static void sp_color_selector_hide( GtkWidget *widget ); + +static guint csel_signals[LAST_SIGNAL] = {0}; + +double ColorSelector::_epsilon = 1e-4; + +G_DEFINE_TYPE(SPColorSelector, sp_color_selector, GTK_TYPE_BOX); + +void sp_color_selector_class_init( SPColorSelectorClass *klass ) +{ + static const gchar* nameset[] = {N_("Unnamed"), nullptr}; + GObjectClass *object_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class; + widget_class = GTK_WIDGET_CLASS(klass); + + csel_signals[GRABBED] = g_signal_new( "grabbed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, grabbed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[DRAGGED] = g_signal_new( "dragged", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, dragged), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[RELEASED] = g_signal_new( "released", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, released), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + csel_signals[CHANGED] = g_signal_new( "changed", + G_TYPE_FROM_CLASS(object_class), + (GSignalFlags)(G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE), + G_STRUCT_OFFSET(SPColorSelectorClass, changed), + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0 ); + + klass->name = nameset; + klass->submode_count = 1; + + object_class->dispose = sp_color_selector_dispose; + + widget_class->show_all = sp_color_selector_show_all; + widget_class->hide = sp_color_selector_hide; + +} + +void sp_color_selector_init( SPColorSelector *csel ) +{ + gtk_orientable_set_orientation(GTK_ORIENTABLE(csel), GTK_ORIENTATION_VERTICAL); + + if ( csel->base ) + { + csel->base->init(); + } +/* g_signal_connect(G_OBJECT(csel->rgbae), "changed", G_CALLBACK(sp_color_selector_rgba_entry_changed), csel); */ +} + +void sp_color_selector_dispose(GObject *object) +{ + SPColorSelector *csel = SP_COLOR_SELECTOR( object ); + if ( csel->base ) + { + delete csel->base; + csel->base = nullptr; + } + + if ((G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose ) { + (G_OBJECT_CLASS(sp_color_selector_parent_class))->dispose(object); + } +} + +void sp_color_selector_show_all( GtkWidget *widget ) +{ + gtk_widget_show( widget ); +} + +void sp_color_selector_hide(GtkWidget *widget) +{ + gtk_widget_hide( widget ); +} + +GtkWidget *sp_color_selector_new( GType selector_type ) +{ + g_return_val_if_fail( g_type_is_a( selector_type, SP_TYPE_COLOR_SELECTOR ), NULL ); + + SPColorSelector *csel = SP_COLOR_SELECTOR( g_object_new( selector_type, nullptr ) ); + + return GTK_WIDGET( csel ); +} + +void ColorSelector::setSubmode( guint /*submode*/ ) +{ +} + +guint ColorSelector::getSubmode() const +{ + guint mode = 0; + return mode; +} + +ColorSelector::ColorSelector( SPColorSelector* csel ) + : _csel(csel), + _color( 0 ), + _alpha(1.0), + _held(FALSE), + virgin(true) +{ + g_return_if_fail( SP_IS_COLOR_SELECTOR(_csel) ); +} + +ColorSelector::~ColorSelector() += default; + +void ColorSelector::init() +{ + _csel->base = new ColorSelector( _csel ); +} + +void ColorSelector::setColor( const SPColor& color ) +{ + setColorAlpha( color, _alpha ); +} + +SPColor ColorSelector::getColor() const +{ + return _color; +} + +void ColorSelector::setAlpha( gfloat alpha ) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + setColorAlpha( _color, alpha ); +} + +gfloat ColorSelector::getAlpha() const +{ + return _alpha; +} + +/** +Called from the outside to set the color; optionally emits signal (only when called from +downstream, e.g. the RGBA value field, but not from the rest of the program) +*/ +void ColorSelector::setColorAlpha( const SPColor& color, gfloat alpha, bool emit ) +{ +#ifdef DUMP_CHANGE_INFO + g_message("ColorSelector::setColorAlpha( this=%p, %f, %f, %f, %s, %f, %s) in %s", this, color.v.c[0], color.v.c[1], color.v.c[2], (color.icc?color.icc->colorProfile.c_str():"<null>"), alpha, (emit?"YES":"no"), FOO_NAME(_csel)); +#endif + g_return_if_fail( _csel != nullptr ); + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + +#ifdef DUMP_CHANGE_INFO + g_message("---- ColorSelector::setColorAlpha virgin:%s !close:%s alpha is:%s in %s", + (virgin?"YES":"no"), + (!color.isClose( _color, _epsilon )?"YES":"no"), + ((fabs((_alpha) - (alpha)) >= _epsilon )?"YES":"no"), + FOO_NAME(_csel) + ); +#endif + + if ( virgin || !color.isClose( _color, _epsilon ) || + (fabs((_alpha) - (alpha)) >= _epsilon )) { + + virgin = false; + + _color = color; + _alpha = alpha; + _colorChanged(); + + if (emit) { + g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0); + } +#ifdef DUMP_CHANGE_INFO + } else { + g_message("++++ ColorSelector::setColorAlpha color:%08x ==> _color:%08X isClose:%s in %s", color.toRGBA32(alpha), _color.toRGBA32(_alpha), + (color.isClose( _color, _epsilon )?"YES":"no"), FOO_NAME(_csel)); +#endif + } +} + +void ColorSelector::_grabbed() +{ + _held = TRUE; +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__, + "GRABBED", + FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0); +} + +void ColorSelector::_released() +{ + _held = false; +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s in %s", __FILE__, __LINE__, + "RELEASED", + FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0); + g_signal_emit(G_OBJECT(_csel), csel_signals[CHANGED], 0); +} + +// Called from subclasses to update color and broadcast if needed +void ColorSelector::_updateInternals( const SPColor& color, gfloat alpha, gboolean held ) +{ + g_return_if_fail( ( 0.0 <= alpha ) && ( alpha <= 1.0 ) ); + gboolean colorDifferent = ( !color.isClose( _color, _epsilon ) + || ( fabs((_alpha) - (alpha)) >= _epsilon ) ); + + gboolean grabbed = held && !_held; + gboolean released = !held && _held; + + // Store these before emitting any signals + _held = held; + if ( colorDifferent ) + { + _color = color; + _alpha = alpha; + } + + if ( grabbed ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + "GRABBED", + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[GRABBED], 0); + } + else if ( released ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + "RELEASED", + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[RELEASED], 0); + } + + if ( colorDifferent || released ) + { +#ifdef DUMP_CHANGE_INFO + g_message("%s:%d: About to signal %s to color %08x::%s in %s", __FILE__, __LINE__, + (_held ? "CHANGED" : "DRAGGED" ), + color.toRGBA32( alpha ), (color.icc?color.icc->colorProfile.c_str():"<null>"), FOO_NAME(_csel)); +#endif + g_signal_emit(G_OBJECT(_csel), csel_signals[_held ? DRAGGED : CHANGED], 0); + } +} + +/** + * Called once the color actually changes. Allows subclasses to react to changes. + */ +void ColorSelector::_colorChanged() +{ +} + +void ColorSelector::getColorAlpha( SPColor &color, gfloat &alpha ) const +{ + gint i = 0; + + color = _color; + alpha = _alpha; + + // Try to catch uninitialized value usage + if ( color.v.c[0] ) + { + i++; + } + if ( color.v.c[1] ) + { + i++; + } + if ( color.v.c[2] ) + { + i++; + } + if ( alpha ) + { + i++; + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-color-selector.h b/src/widgets/sp-color-selector.h new file mode 100644 index 0000000..eb77250 --- /dev/null +++ b/src/widgets/sp-color-selector.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_COLOR_SELECTOR_H +#define SEEN_SP_COLOR_SELECTOR_H + +#include <gtk/gtk.h> +#include "color.h" + +struct SPColorSelector; + +class ColorSelector +{ +public: + ColorSelector( SPColorSelector* csel ); + virtual ~ColorSelector(); + + virtual void init(); + + void setColor( const SPColor& color ); + SPColor getColor() const; + + void setAlpha( gfloat alpha ); + gfloat getAlpha() const; + + void setColorAlpha( const SPColor& color, gfloat alpha, bool emit = false ); + void getColorAlpha( SPColor &color, gfloat &alpha ) const; + + virtual void setSubmode( guint submode ); + virtual guint getSubmode() const; + +protected: + void _grabbed(); + void _released(); + void _updateInternals( const SPColor& color, gfloat alpha, gboolean held ); + gboolean _isHeld() const { return _held; } + + virtual void _colorChanged(); + + static double _epsilon; + + SPColorSelector* _csel; + SPColor _color; + gfloat _alpha; // guaranteed to be in [0, 1]. + +private: + // By default, disallow copy constructor and assignment operator + ColorSelector( const ColorSelector& obj ) = delete; + ColorSelector& operator=( const ColorSelector& obj ) = delete; + + gboolean _held; + + bool virgin; // if true, no color is set yet +}; + + + +#define SP_TYPE_COLOR_SELECTOR (sp_color_selector_get_type ()) +#define SP_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_COLOR_SELECTOR, SPColorSelector)) +#define SP_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass)) +#define SP_IS_COLOR_SELECTOR(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_COLOR_SELECTOR)) +#define SP_IS_COLOR_SELECTOR_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_COLOR_SELECTOR)) +#define SP_COLOR_SELECTOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SP_TYPE_COLOR_SELECTOR, SPColorSelectorClass)) + +struct SPColorSelector { + GtkBox vbox; + ColorSelector* base; +}; + +struct SPColorSelectorClass { + GtkBoxClass parent_class; + + const gchar **name; + guint submode_count; + + void (* grabbed) (SPColorSelector *rgbsel); + void (* dragged) (SPColorSelector *rgbsel); + void (* released) (SPColorSelector *rgbsel); + void (* changed) (SPColorSelector *rgbsel); +}; + +GType sp_color_selector_get_type(); + +GtkWidget *sp_color_selector_new( GType selector_type ); + + + +#endif // SEEN_SP_COLOR_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/sp-xmlview-tree.cpp b/src/widgets/sp-xmlview-tree.cpp new file mode 100644 index 0000000..6f45e64 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.cpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Specialization of GtkTreeView for the XML tree view + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2002 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> + +#include "xml/node-event-vector.h" +#include "sp-xmlview-tree.h" + +namespace { +struct NodeData { + SPXMLViewTree * tree; + GtkTreeRowReference *rowref; + Inkscape::XML::Node * repr; + bool expanded = false; //< true if tree view has been expanded to this node + bool dragging = false; + + NodeData(SPXMLViewTree *tree, GtkTreeIter *node, Inkscape::XML::Node *repr); + ~NodeData(); +}; + +// currently dragged node +Inkscape::XML::Node *dragging_repr = nullptr; +} // namespace + +enum { STORE_TEXT_COL = 0, STORE_DATA_COL, STORE_N_COLS }; + +static void sp_xmlview_tree_destroy(GtkWidget * object); + +static NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter); + +static void add_node(SPXMLViewTree *tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node *repr); + +static void element_child_added (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data); +static void element_attr_changed (Inkscape::XML::Node * repr, const gchar * key, const gchar * old_value, const gchar * new_value, bool is_interactive, gpointer data); +static void element_child_removed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data); +static void element_order_changed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * oldref, Inkscape::XML::Node * newref, gpointer data); +static void element_name_changed (Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, gpointer data); +static void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data); + +static void text_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); +static void comment_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); +static void pi_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data); + +static gboolean ref_to_sibling (NodeData *node, Inkscape::XML::Node * ref, GtkTreeIter *); +static gboolean repr_to_child (NodeData *node, Inkscape::XML::Node * repr, GtkTreeIter *); +static GtkTreeRowReference *tree_iter_to_ref(SPXMLViewTree *, GtkTreeIter *); +static gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref); + +static gboolean search_equal_func(GtkTreeModel *, gint column, const gchar *key, GtkTreeIter *, gpointer search_data); +static gboolean foreach_func(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); + +static void on_row_changed(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data); +static void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata); +static void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata); +static gboolean do_drag_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint time, gpointer user_data); + +static const Inkscape::XML::NodeEventVector element_repr_events = { + element_child_added, + element_child_removed, + element_attr_changed, + nullptr, /* content_changed */ + element_order_changed, + element_name_changed +}; + +static const Inkscape::XML::NodeEventVector text_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + text_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +static const Inkscape::XML::NodeEventVector comment_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + comment_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +static const Inkscape::XML::NodeEventVector pi_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + nullptr, /* attr_changed */ + pi_content_changed, + nullptr /* order_changed */, + nullptr /* element_name_changed */ +}; + +/** + * Get an iterator to the first child of `data` + * @param data handle which references a row + * @param[out] child_iter On success: valid iterator to first child + * @return False if the node has no children + */ +static bool get_first_child(NodeData *data, GtkTreeIter *child_iter) +{ + GtkTreeIter iter; + return tree_ref_to_iter(data->tree, &iter, data->rowref) && + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), child_iter, &iter); +} + +/** + * @param iter First dummy row on that level + * @pre all rows on the same level are dummies + * @pre iter is valid + * @post iter is invalid + * @post level is empty + */ +static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter) +{ + do { + g_assert(nullptr == sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(store), iter)); + gtk_tree_store_remove(store, iter); + } while (gtk_tree_store_iter_is_valid(store, iter)); +} + +static gboolean on_test_expand_row( // + GtkTreeView *tree_view, // + GtkTreeIter *iter, // + GtkTreePath *path, // + gpointer) +{ + auto tree = SP_XMLVIEW_TREE(tree_view); + auto model = GTK_TREE_MODEL(tree->store); + + GtkTreeIter childiter; + bool has_children = gtk_tree_model_iter_children(model, &childiter, iter); + g_assert(has_children); + + if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + + remove_dummy_rows(tree->store, &childiter); + + // insert real rows + data->expanded = true; + sp_repr_synthesize_events(data->repr, &element_repr_events, data); + } + + return false; +} + +GtkWidget *sp_xmlview_tree_new(Inkscape::XML::Node * repr, void * /*factory*/, void * /*data*/) +{ + SPXMLViewTree *tree = SP_XMLVIEW_TREE(g_object_new (SP_TYPE_XMLVIEW_TREE, nullptr)); + + gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(tree), FALSE); + gtk_tree_view_set_reorderable (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_enable_search (GTK_TREE_VIEW(tree), TRUE); + gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW(tree), search_equal_func, nullptr, nullptr); + + GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); + GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes ("", renderer, "text", STORE_TEXT_COL, NULL); + gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column); + gtk_cell_renderer_set_padding (renderer, 2, 0); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE); + + sp_xmlview_tree_set_repr (tree, repr); + + g_signal_connect(GTK_TREE_VIEW(tree), "drag-begin", G_CALLBACK(on_drag_begin), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-end", G_CALLBACK(on_drag_end), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "drag-motion", G_CALLBACK(do_drag_motion), tree); + g_signal_connect(GTK_TREE_VIEW(tree), "test-expand-row", G_CALLBACK(on_test_expand_row), nullptr); + + return GTK_WIDGET(tree); +} + +G_DEFINE_TYPE(SPXMLViewTree, sp_xmlview_tree, GTK_TYPE_TREE_VIEW); + +void sp_xmlview_tree_class_init(SPXMLViewTreeClass * klass) +{ + auto widget_class = GTK_WIDGET_CLASS(klass); + widget_class->destroy = sp_xmlview_tree_destroy; + + // Signal for when a tree drag and drop has completed + g_signal_new ( "tree_move", + G_TYPE_FROM_CLASS(klass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, + G_TYPE_UINT); +} + +void +sp_xmlview_tree_init (SPXMLViewTree * tree) +{ + tree->repr = nullptr; + tree->blocked = 0; +} + +void sp_xmlview_tree_destroy(GtkWidget * object) +{ + SPXMLViewTree * tree = SP_XMLVIEW_TREE (object); + + sp_xmlview_tree_set_repr (tree, nullptr); + + GTK_WIDGET_CLASS(sp_xmlview_tree_parent_class)->destroy (object); +} + +/* + * Add a new row to the tree + */ +void +add_node (SPXMLViewTree * tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node * repr) +{ + const Inkscape::XML::NodeEventVector * vec; + + g_assert (tree != nullptr); + + if (before && !gtk_tree_store_iter_is_valid(tree->store, before)) { + before = nullptr; + } + + GtkTreeIter iter; + gtk_tree_store_insert_before (tree->store, &iter, parent, before); + + if (!gtk_tree_store_iter_is_valid(tree->store, &iter)) { + return; + } + + if (!repr) { + // no need to store any data + return; + } + + auto data = new NodeData(tree, &iter, repr); + + g_assert (data != nullptr); + + gtk_tree_store_set(tree->store, &iter, STORE_DATA_COL, data, -1); + + if ( repr->type() == Inkscape::XML::TEXT_NODE ) { + vec = &text_repr_events; + } else if ( repr->type() == Inkscape::XML::COMMENT_NODE ) { + vec = &comment_repr_events; + } else if ( repr->type() == Inkscape::XML::PI_NODE ) { + vec = &pi_repr_events; + } else if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) { + vec = &element_repr_events; + } else { + vec = nullptr; + } + + if (vec) { + /* cheat a little to get the text updated on nodes without id */ + if (repr->type() == Inkscape::XML::ELEMENT_NODE && repr->attribute("id") == nullptr) { + element_attr_changed (repr, "id", nullptr, nullptr, false, data); + } + sp_repr_add_listener (repr, vec, data); + sp_repr_synthesize_events (repr, vec, data); + } +} + +static gboolean remove_all_listeners(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + delete data; + return false; +} + +NodeData::NodeData(SPXMLViewTree *tree, GtkTreeIter *iter, Inkscape::XML::Node *repr) + : tree(tree) + , rowref(tree_iter_to_ref(tree, iter)) + , repr(repr) +{ + if (repr) { + Inkscape::GC::anchor(repr); + } +} + +NodeData::~NodeData() +{ + if (repr) { + sp_repr_remove_listener_by_data(repr, this); + Inkscape::GC::release(repr); + } + gtk_tree_row_reference_free(rowref); +} + +void element_child_added (Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + GtkTreeIter before; + + if (data->tree->blocked) return; + + if (!ref_to_sibling (data, ref, &before)) { + return; + } + + GtkTreeIter data_iter; + tree_ref_to_iter(data->tree, &data_iter, data->rowref); + + if (!data->expanded) { + auto model = GTK_TREE_MODEL(data->tree->store); + GtkTreeIter childiter; + if (!gtk_tree_model_iter_children(model, &childiter, &data_iter)) { + // no children yet, add a dummy + child = nullptr; + } else if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) { + // already has a dummy child + return; + } + } + + add_node (data->tree, &data_iter, &before, child); +} + +void element_attr_changed( + Inkscape::XML::Node* repr, gchar const* key, + gchar const* /*old_value*/, gchar const* /*new_value*/, bool /*is_interactive*/, + gpointer ptr) +{ + if (0 != strcmp (key, "id") && 0 != strcmp (key, "inkscape:label")) + return; + element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr)); +} + +void element_name_changed( + Inkscape::XML::Node* repr, + gchar const* /*oldname*/, gchar const* /*newname*/, gpointer ptr) +{ + element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr)); +} + +void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data) +{ + if (data->tree->blocked) { + return; + } + + gchar const* node_name = repr->name(); + gchar const* id_value = repr->attribute("id"); + gchar const* label_value = repr->attribute("inkscape:label"); + gchar* display_text; + + if (id_value && label_value) { + display_text = g_strdup_printf ("<%s id=\"%s\" inkscape:label=\"%s\">", node_name, id_value, label_value); + } else if (id_value) { + display_text = g_strdup_printf ("<%s id=\"%s\">", node_name, id_value); + } else if (label_value) { + display_text = g_strdup_printf ("<%s inkscape:label=\"%s\">", node_name, label_value); + } else { + display_text = g_strdup_printf ("<%s>", node_name); + } + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set (GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, display_text, -1); + } + + g_free(display_text); +} + +void element_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/, + gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + GtkTreeIter iter; + if (repr_to_child(data, child, &iter)) { + delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(data->tree->store), &iter); + gtk_tree_store_remove(data->tree->store, &iter); + } else if (!repr->firstChild() && get_first_child(data, &iter)) { + // remove dummy when all children gone + remove_dummy_rows(data->tree->store, &iter); + } else { + return; + } + +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(data->tree))); +#endif +} + +void element_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * /*oldref*/, Inkscape::XML::Node * newref, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + GtkTreeIter before, node; + + if (data->tree->blocked) return; + + ref_to_sibling (data, newref, &before); + repr_to_child (data, child, &node); + + if (gtk_tree_store_iter_is_valid(data->tree->store, &before)) { + gtk_tree_store_move_before (data->tree->store, &node, &before); + } else { + repr_to_child (data, newref, &before); + gtk_tree_store_move_after (data->tree->store, &node, &before); + } +} + +Glib::ustring sp_remove_newlines_and_tabs(Glib::ustring val) +{ + int pos; + Glib::ustring newlinesign = "â¤"; + Glib::ustring tabsign = "⇥"; + while ((pos = val.find("\r\n")) != std::string::npos) { + val.erase(pos, 2); + val.insert(pos, newlinesign); + } + while ((pos = val.find('\n')) != std::string::npos) { + val.erase(pos, 1); + val.insert(pos, newlinesign); + } + while ((pos = val.find('\t')) != std::string::npos) { + val.erase(pos, 1); + val.insert(pos, tabsign); + } + return val; +} + +void text_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar * new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("\"%s\"", new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + + g_free (label); +} + +void comment_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData*>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("<!--%s-->", new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + g_free (label); +} + +void pi_content_changed(Inkscape::XML::Node *repr, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr) +{ + NodeData *data = static_cast<NodeData *>(ptr); + + if (data->tree->blocked) return; + + gchar *label = g_strdup_printf ("<?%s %s?>", repr->name(), new_content); + Glib::ustring nolinecontent = label; + nolinecontent = sp_remove_newlines_and_tabs(nolinecontent); + + GtkTreeIter iter; + if (tree_ref_to_iter(data->tree, &iter, data->rowref)) { + gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1); + } + g_free (label); +} + +/* + * Save the source path on drag start, will need it in on_row_changed() when moving a row + */ +void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + SPXMLViewTree *tree = static_cast<SPXMLViewTree *>(userdata); + if (!tree) { + return; + } + + GtkTreeModel *model = nullptr; + GtkTreeIter iter; + GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + if (gtk_tree_selection_get_selected(selection, &model, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(model, &iter); + if (data) { + data->dragging = true; + dragging_repr = data->repr; + } + } +} + +/** + * Finalize what happended in `on_row_changed` and clean up what was set up in `on_drag_begin` + */ +void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata) +{ + if (!dragging_repr) + return; + + auto tree = static_cast<SPXMLViewTree *>(userdata); + auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + bool failed = false; + + GtkTreeIter iter; + if (sp_xmlview_tree_get_repr_node(tree, dragging_repr, &iter)) { + NodeData *data = sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(tree->store), &iter); + + if (data && data->dragging) { + // dragging flag was not cleared in `on_row_changed`, this indicates a failed drag + data->dragging = false; + failed = true; + } else { + // Reselect the dragged row + gtk_tree_selection_select_iter(selection, &iter); + } + } else { +#ifndef GTK_ISSUE_2510_IS_FIXED + // https://gitlab.gnome.org/GNOME/gtk/issues/2510 + gtk_tree_selection_unselect_all(selection); +#endif + } + + dragging_repr = nullptr; + + if (!failed) { + // Signal that a drag and drop has completed successfully + g_signal_emit_by_name(G_OBJECT(tree), "tree_move", GUINT_TO_POINTER(1)); + } +} + +/* + * Main drag & drop function + * Get the old and new paths, and change the Inkscape::XML::Node repr's + */ +void on_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data) +{ + NodeData *data = sp_xmlview_tree_node_get_data(tree_model, iter); + + if (!data || !data->dragging) { + return; + } + data->dragging = false; + + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + + gtk_tree_row_reference_free(data->rowref); + data->rowref = tree_iter_to_ref(tree, iter); + + GtkTreeIter new_parent; + if (!gtk_tree_model_iter_parent(tree_model, &new_parent, iter)) { + //No parent of drop location + return; + } + + Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(tree_model, iter); + Inkscape::XML::Node *before_repr = nullptr; + + // Find the sibling node before iter + GtkTreeIter before_iter = *iter; + if (gtk_tree_model_iter_previous(tree_model, &before_iter)) { + before_repr = sp_xmlview_tree_node_get_repr(tree_model, &before_iter); + } + + // Drop onto oneself causes assert in changeOrder() below, ignore + if (repr == before_repr) + return; + + auto repr_old_parent = repr->parent(); + auto repr_new_parent = sp_xmlview_tree_node_get_repr(tree_model, &new_parent); + + tree->blocked++; + + if (repr_old_parent == repr_new_parent) { + repr_old_parent->changeOrder(repr, before_repr); + } else { + repr_old_parent->removeChild(repr); + repr_new_parent->addChild(repr, before_repr); + } + + NodeData *data_new_parent = sp_xmlview_tree_node_get_data(tree_model, &new_parent); + if (data_new_parent && data_new_parent->expanded) { + // Reselect the dragged row in `on_drag_end` instead of here, because of + // https://gitlab.gnome.org/GNOME/gtk/-/issues/2510 + } else { + // convert to dummy node + delete data; + gtk_tree_store_set(tree->store, iter, STORE_DATA_COL, nullptr, -1); + } + + tree->blocked--; +} + +/* + * Set iter to ref or node data's child with the same repr or first child + */ +gboolean ref_to_sibling (NodeData *data, Inkscape::XML::Node *repr, GtkTreeIter *iter) +{ + if (repr) { + if (!repr_to_child (data, repr, iter)) { + return false; + } + gtk_tree_model_iter_next (GTK_TREE_MODEL(data->tree->store), iter); + } else { + GtkTreeIter data_iter; + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), iter, &data_iter); + } + return true; +} + +/* + * Set iter to the node data's child with the same repr + */ +gboolean repr_to_child (NodeData *data, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + GtkTreeIter data_iter; + GtkTreeModel *model = GTK_TREE_MODEL(data->tree->store); + gboolean valid = false; + + if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) { + return false; + } + + /* + * The node we are looking for is likely to be the last one, so check it first. + */ + gint n_children = gtk_tree_model_iter_n_children (model, &data_iter); + if (n_children > 1) { + valid = gtk_tree_model_iter_nth_child (model, iter, &data_iter, n_children-1); + if (valid && sp_xmlview_tree_node_get_repr (model, iter) == repr) { + //g_message("repr_to_child hit %d", n_children); + return valid; + } + } + + valid = gtk_tree_model_iter_children(model, iter, &data_iter); + while (valid && sp_xmlview_tree_node_get_repr (model, iter) != repr) { + valid = gtk_tree_model_iter_next(model, iter); + } + + return valid; +} + +/* + * Get a matching GtkTreeRowReference for a GtkTreeIter + */ +GtkTreeRowReference *tree_iter_to_ref (SPXMLViewTree * tree, GtkTreeIter* iter) +{ + GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), iter); + GtkTreeRowReference *ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(tree->store), path); + gtk_tree_path_free(path); + + return ref; +} + +/* + * Get a matching GtkTreeIter for a GtkTreeRowReference + */ +gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference *ref) +{ + GtkTreePath* path = gtk_tree_row_reference_get_path(ref); + if (!path) { + return false; + } + gboolean const valid = // + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), iter, path); + gtk_tree_path_free(path); + + return valid; +} + +/* + * Disable drag and drop target on : root node and non-element nodes + */ +gboolean do_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data) +{ + GtkTreePath *path = nullptr; + GtkTreeViewDropPosition pos; + gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW(widget), x, y, &path, &pos); + + int action = 0; + + if (!dragging_repr) { + goto finally; + } + + if (path) { + SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data); + GtkTreeIter iter; + gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), &iter, path); + auto repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), &iter); + + bool const drop_into = pos != GTK_TREE_VIEW_DROP_BEFORE && // + pos != GTK_TREE_VIEW_DROP_AFTER; + + // 0. don't drop on self (also handled by on_row_changed but nice to not have drop highlight for it) + if (repr == dragging_repr) { + goto finally; + } + + // 1. only xml elements can have children + if (drop_into && repr->type() != Inkscape::XML::ELEMENT_NODE) { + goto finally; + } + + // 3. elements must be at least children of the root <svg:svg> element + if (gtk_tree_path_get_depth(path) < 2) { + goto finally; + } + + // 4. drag node specific limitations + { + // nodes which can't be re-parented (because the document holds pointers to them which must stay valid) + static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview"); + static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs"); + + bool const no_reparenting = dragging_repr->code() == CODE_sodipodi_namedview || // + dragging_repr->code() == CODE_svg_defs; + + if (no_reparenting && (drop_into || dragging_repr->parent() != repr->parent())) { + goto finally; + } + } + + action = GDK_ACTION_MOVE; + } + +finally: + if (action == 0) { + // remove drop highlight + gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), nullptr, pos /* ignored */); + } + + gtk_tree_path_free(path); + gdk_drag_status (context, (GdkDragAction)action, time); + + return (action == 0); +} + +/* + * Set the tree selection and scroll to the row with the given repr + */ +void +sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr) +{ + if ( tree->repr == repr ) return; + + if (tree->store) { + gtk_tree_view_set_model(GTK_TREE_VIEW(tree), nullptr); + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), remove_all_listeners, nullptr); + g_object_unref(tree->store); + tree->store = nullptr; + } + + if (tree->repr) { + Inkscape::GC::release(tree->repr); + } + tree->repr = repr; + if (repr) { + tree->store = gtk_tree_store_new(STORE_N_COLS, G_TYPE_STRING, G_TYPE_POINTER); + + Inkscape::GC::anchor(repr); + add_node(tree, nullptr, nullptr, repr); + + // Set the tree model here, after all data is inserted + gtk_tree_view_set_model (GTK_TREE_VIEW(tree), GTK_TREE_MODEL(tree->store)); + g_signal_connect(G_OBJECT(tree->store), "row-changed", G_CALLBACK(on_row_changed), tree); + + GtkTreePath *path = gtk_tree_path_new_from_indices(0, -1); + gtk_tree_view_expand_to_path (GTK_TREE_VIEW(tree), path); + gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW(tree), path, nullptr, true, 0.5, 0.0); + gtk_tree_path_free(path); + } +} + +/* + * Return the node data at a given GtkTreeIter position + */ +NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter) +{ + NodeData *data = nullptr; + gtk_tree_model_get(model, iter, STORE_DATA_COL, &data, -1); + return data; +} + +/* + * Return the repr at a given GtkTreeIter position + */ +Inkscape::XML::Node * +sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * iter) +{ + NodeData *data = sp_xmlview_tree_node_get_data(model, iter); + return data ? data->repr : nullptr; +} + +struct IterByReprData { + const Inkscape::XML::Node *repr; //< in + GtkTreeIter *iter; //< out +}; + +/* + * Find a GtkTreeIter position in the tree by repr + * @return True if the node was found + */ +gboolean +sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *iter) +{ + iter->stamp = 0; // invalidate iterator + IterByReprData funcdata = { repr, iter }; + gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), foreach_func, &funcdata); + return iter->stamp != 0; +} + +gboolean foreach_func(GtkTreeModel *model, GtkTreePath * /*path*/, GtkTreeIter *iter, gpointer user_data) +{ + auto funcdata = static_cast<IterByReprData *>(user_data); + if (sp_xmlview_tree_node_get_repr(model, iter) == funcdata->repr) { + *funcdata->iter = *iter; + return TRUE; + } + + return FALSE; +} + +/* + * Callback function for string searches in the tree + * Return a match on any substring + */ +gboolean search_equal_func(GtkTreeModel *model, gint /*column*/, const gchar *key, GtkTreeIter *iter, gpointer /*search_data*/) +{ + gchar *text = nullptr; + gtk_tree_model_get(model, iter, STORE_TEXT_COL, &text, -1); + + gboolean match = (strstr(text, key) != nullptr); + + g_free(text); + + return !match; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=4:softtabstop=4:fileencoding=utf-8 : diff --git a/src/widgets/sp-xmlview-tree.h b/src/widgets/sp-xmlview-tree.h new file mode 100644 index 0000000..8839ee9 --- /dev/null +++ b/src/widgets/sp-xmlview-tree.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2002 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_XMLVIEW_TREE_H +#define SEEN_SP_XMLVIEW_TREE_H + +#include <gtk/gtk.h> +#include <glib.h> + +/** + * Specialization of GtkTreeView for the XML editor + */ + +#define SP_TYPE_XMLVIEW_TREE (sp_xmlview_tree_get_type ()) +#define SP_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SP_TYPE_XMLVIEW_TREE, SPXMLViewTree)) +#define SP_IS_XMLVIEW_TREE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SP_TYPE_XMLVIEW_TREE)) +#define SP_XMLVIEW_TREE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SP_TYPE_XMLVIEW_TREE)) + +struct SPXMLViewTree; +struct SPXMLViewTreeClass; + +struct SPXMLViewTree +{ + GtkTreeView tree; + GtkTreeStore *store; + Inkscape::XML::Node * repr; + gint blocked; +}; + +struct SPXMLViewTreeClass +{ + GtkTreeViewClass parent_class; +}; + +GType sp_xmlview_tree_get_type (); +GtkWidget * sp_xmlview_tree_new (Inkscape::XML::Node * repr, void * factory, void * data); + +#define SP_XMLVIEW_TREE_REPR(tree) (SP_XMLVIEW_TREE (tree)->repr) + +void sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr); + +Inkscape::XML::Node * sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * node); +gboolean sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *node); + + +#endif // !SEEN_SP_XMLVIEW_TREE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/widgets/spinbutton-events.cpp b/src/widgets/spinbutton-events.cpp new file mode 100644 index 0000000..9800cf5 --- /dev/null +++ b/src/widgets/spinbutton-events.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Common callbacks for spinbuttons + * + * Authors: + * bulia byak <bulia@users.sourceforge.net> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2013 authors + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#include "ui/tools/tool-base.h" + +#include "spinbutton-events.h" + +gboolean spinbutton_focus_in(GtkWidget *w, GdkEventKey * /*event*/, gpointer /*data*/) +{ + gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini")); + if (ini) { + g_free(ini); // free the old value if any + } + + // retrieve the value + ini = g_new(gdouble, 1); + *ini = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + + // remember it + g_object_set_data(G_OBJECT(w), "ini", ini); + + return FALSE; // I didn't consume the event +} + +void spinbutton_undo(GtkWidget *w) +{ + gdouble *ini = static_cast<gdouble *>(g_object_get_data(G_OBJECT(w), "ini")); + if (ini) { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), *ini); + } +} + +void spinbutton_defocus(GtkWidget *container) +{ + // defocus spinbuttons by moving focus to the canvas, unless "stay" is on + gboolean stay = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(container), "stay")); + if (stay) { + g_object_set_data(G_OBJECT(container), "stay", GINT_TO_POINTER(FALSE)); + } else { + GtkWidget *canvas = GTK_WIDGET(g_object_get_data(G_OBJECT(container), "dtw")); + if (canvas) { + gtk_widget_grab_focus(GTK_WIDGET(canvas)); + } + } +} + +gboolean spinbutton_keypress(GtkWidget *w, GdkEventKey *event, gpointer /*data*/) +{ + gboolean result = FALSE; // I didn't consume the event + + switch (Inkscape::UI::Tools::get_latin_keyval(event)) { + case GDK_KEY_Escape: // defocus + spinbutton_undo(w); + spinbutton_defocus(w); + result = TRUE; // I consumed the event + break; + case GDK_KEY_Return: // defocus + case GDK_KEY_KP_Enter: + spinbutton_defocus(w); + result = TRUE; // I consumed the event + break; + case GDK_KEY_Tab: + case GDK_KEY_ISO_Left_Tab: + // set the flag meaning "do not leave toolbar when changing value" + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + result = FALSE; // I didn't consume the event + break; + + // The following keys are processed manually because GTK implements them in strange ways + // (increments start with double step value and seem to grow as you press the key continuously) + + case GDK_KEY_Up: + case GDK_KEY_KP_Up: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v += step; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Down: + case GDK_KEY_KP_Down: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v -= step; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Page_Up: + case GDK_KEY_KP_Page_Up: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v += page; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_Page_Down: + case GDK_KEY_KP_Page_Down: + { + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + gdouble v = gtk_spin_button_get_value(GTK_SPIN_BUTTON(w)); + gdouble step = 0; + gdouble page = 0; + gtk_spin_button_get_increments(GTK_SPIN_BUTTON(w), &step, &page); + v -= page; + gtk_spin_button_set_value(GTK_SPIN_BUTTON(w), v); + result = TRUE; // I consumed the event + break; + } + case GDK_KEY_z: + case GDK_KEY_Z: + g_object_set_data(G_OBJECT(w), "stay", GINT_TO_POINTER(TRUE)); + if (event->state & GDK_CONTROL_MASK) { + spinbutton_undo(w); + result = TRUE; // I consumed the event + } + break; + default: + result = FALSE; + break; + } + + return result; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/spinbutton-events.h b/src/widgets/spinbutton-events.h new file mode 100644 index 0000000..e32ef18 --- /dev/null +++ b/src/widgets/spinbutton-events.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Common callbacks for spinbuttons + * + * Authors: + * bulia byak <bulia@users.sourceforge.net> + * + * Copyright (C) 2003 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +typedef struct _GdkEventKey GdkEventKey; +typedef struct _GtkWidget GtkWidget; + +gboolean spinbutton_focus_in (GtkWidget *w, GdkEventKey *event, gpointer data); +void spinbutton_undo (GtkWidget *w); +gboolean spinbutton_keypress (GtkWidget *w, GdkEventKey *event, gpointer data); +void spinbutton_defocus (GtkWidget *container); + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/spw-utilities.cpp b/src/widgets/spw-utilities.cpp new file mode 100644 index 0000000..38759ef --- /dev/null +++ b/src/widgets/spw-utilities.cpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape Widget Utilities + * + * Authors: + * Bryce W. Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2003 Bryce W. Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include "selection.h" + +#include "spw-utilities.h" + +/** + * Creates a label widget with the given text, at the given col, row + * position in the table. + */ +Gtk::Label * spw_label(Gtk::Grid *table, const gchar *label_text, int col, int row, Gtk::Widget* target) +{ + Gtk::Label *label_widget = new Gtk::Label(); + g_assert(label_widget != nullptr); + if (target != nullptr) { + label_widget->set_text_with_mnemonic(label_text); + label_widget->set_mnemonic_widget(*target); + } else { + label_widget->set_text(label_text); + } + label_widget->show(); + + label_widget->set_halign(Gtk::ALIGN_START); + label_widget->set_valign(Gtk::ALIGN_CENTER); + label_widget->set_margin_start(4); + label_widget->set_margin_end(4); + + table->attach(*label_widget, col, row, 1, 1); + + return label_widget; +} + +/** + * Creates a horizontal layout manager with 4-pixel spacing between children + * and space for 'width' columns. + */ +Gtk::HBox * spw_hbox(Gtk::Grid * table, int width, int col, int row) +{ + /* Create a new hbox with a 4-pixel spacing between children */ + Gtk::HBox *hb = new Gtk::HBox(false, 4); + g_assert(hb != nullptr); + hb->show(); + hb->set_hexpand(); + hb->set_halign(Gtk::ALIGN_FILL); + hb->set_valign(Gtk::ALIGN_CENTER); + table->attach(*hb, col, row, width, 1); + + return hb; +} + +/** + * Finds the descendant of w which has the data with the given key and returns the data, or NULL if there's none. + */ +gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer key) +{ + gpointer r = nullptr; + + if (w && G_IS_OBJECT(w)) { + r = g_object_get_data(G_OBJECT(w), (gchar *) key); + } + if (r) return r; + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for (auto i:children) { + r = sp_search_by_data_recursive(GTK_WIDGET(i->gobj()), key); + if (r) return r; + } + } + + return nullptr; +} + +/** + * Returns a named descendent of parent, which has the given name, or nullptr if there's none. + * + * \param[in] parent The widget to search + * \param[in] name The name of the desired child widget + * + * \return The specified child widget, or nullptr if it cannot be found + */ +Gtk::Widget * +sp_search_by_name_recursive(Gtk::Widget *parent, const Glib::ustring& name) +{ + auto parent_bin = dynamic_cast<Gtk::Bin *>(parent); + auto parent_container = dynamic_cast<Gtk::Container *>(parent); + + if (parent && parent->get_name() == name) { + return parent; + } + else if (parent_bin) { + auto child = parent_bin->get_child(); + return sp_search_by_name_recursive(child, name); + } + else if (parent_container) { + auto children = parent_container->get_children(); + + for (auto child : children) { + auto tmp = sp_search_by_name_recursive(child, name); + + if (tmp) { + return tmp; + } + } + } + + return nullptr; +} + +/** + * Returns the descendant of w which has the given key and value pair, or NULL if there's none. + */ +GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value) +{ + gchar *r = nullptr; + + if (w && G_IS_OBJECT(w)) { + r = (gchar *) g_object_get_data(G_OBJECT(w), key); + } + if (r && !strcmp (r, value)) return w; + + if (GTK_IS_CONTAINER(w)) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(w))->get_children(); + for (auto i:children) { + GtkWidget *child = sp_search_by_value_recursive(GTK_WIDGET(i->gobj()), key, value); + if (child) return child; + } + } + + return nullptr; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/spw-utilities.h b/src/widgets/spw-utilities.h new file mode 100644 index 0000000..4751e81 --- /dev/null +++ b/src/widgets/spw-utilities.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SPW_UTILITIES_H__ +#define __SPW_UTILITIES_H__ + +/* + * Inkscape Widget Utilities + * + * Author: + * Bryce W. Harrington <brycehar@bryceharrington.org> + * + * Copyright (C) 2003 Bryce Harrington + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* The following are helper routines for making Inkscape dialog widgets. + All are prefixed with spw_, short for inkscape_widget. This is not to + be confused with SPWidget, an existing datatype associated with Inkscape::XML::Node/ + SPObject, that reacts to modification. +*/ + +namespace Gtk { + class Label; + class Grid; + class HBox; + class Widget; +} + +Gtk::Label * spw_label(Gtk::Grid *table, gchar const *label_text, int col, int row, Gtk::Widget *target); +Gtk::HBox * spw_hbox(Gtk::Grid *table, int width, int col, int row); + +gpointer sp_search_by_data_recursive(GtkWidget *w, gpointer data); +GtkWidget *sp_search_by_value_recursive(GtkWidget *w, gchar *key, gchar *value); + +Gtk::Widget * sp_search_by_name_recursive(Gtk::Widget *parent, + const Glib::ustring& name); +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/stroke-marker-selector.cpp b/src/widgets/stroke-marker-selector.cpp new file mode 100644 index 0000000..337ba16 --- /dev/null +++ b/src/widgets/stroke-marker-selector.cpp @@ -0,0 +1,567 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Combobox for selecting dash patterns - implementation. + */ +/* Author: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "stroke-marker-selector.h" + +#include <glibmm/i18n.h> +#include <gtkmm/icontheme.h> + +#include "desktop-style.h" +#include "path-prefix.h" +#include "stroke-style.h" + +#include "helper/stock-items.h" +#include "ui/icon-loader.h" + +#include "io/sys.h" + +#include "object/sp-defs.h" +#include "object/sp-marker.h" +#include "object/sp-root.h" +#include "style.h" + +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/util.h" +#include "ui/widget/spinbutton.h" + +static Inkscape::UI::Cache::SvgPreview svg_preview_cache; + +MarkerComboBox::MarkerComboBox(gchar const *id, int l) : + Gtk::ComboBox(), + combo_id(id), + loc(l), + updating(false), + markerCount(0) +{ + + marker_store = Gtk::ListStore::create(marker_columns); + set_model(marker_store); + pack_start(image_renderer, false); + set_cell_data_func(image_renderer, sigc::mem_fun(*this, &MarkerComboBox::prepareImageRenderer)); + gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(gobj()), MarkerComboBox::separator_cb, nullptr, nullptr); + empty_image = sp_get_icon_image("no-marker", Gtk::ICON_SIZE_SMALL_TOOLBAR); + + sandbox = ink_markers_preview_doc (); + + init_combo(); + this->get_style_context()->add_class("combobright"); + + show(); +} + +MarkerComboBox::~MarkerComboBox() { + delete combo_id; + delete sandbox; + delete empty_image; + + if (doc) { + modified_connection.disconnect(); + } +} + +void MarkerComboBox::setDocument(SPDocument *document) +{ + if (doc != document) { + + if (doc) { + modified_connection.disconnect(); + } + + doc = document; + + if (doc) { + modified_connection = doc->getDefs()->connectModified( sigc::hide(sigc::hide(sigc::bind(sigc::ptr_fun(&MarkerComboBox::handleDefsModified), this))) ); + } + + refreshHistory(); + } +} + +void +MarkerComboBox::handleDefsModified(MarkerComboBox *self) +{ + self->refreshHistory(); +} + +void +MarkerComboBox::refreshHistory() +{ + if (updating) + return; + + updating = true; + + std::vector<SPMarker *> ml = get_marker_list(doc); + + /* + * Seems to be no way to get notified of changes just to markers, + * so listen to changes in all defs and check if the number of markers has changed here + * to avoid unnecessary refreshes when things like gradients change + */ + if (markerCount != ml.size()) { + const char *active = get_active()->get_value(marker_columns.marker); + sp_marker_list_from_doc(doc, true); + set_selected(active); + markerCount = ml.size(); + } + + updating = false; +} + +/** + * Init the combobox widget to display markers from markers.svg + */ +void +MarkerComboBox::init_combo() +{ + if (updating) + return; + + static SPDocument *markers_doc = nullptr; + + // add separator + Gtk::TreeModel::Row row_sep = *(marker_store->append()); + row_sep[marker_columns.label] = "Separator"; + row_sep[marker_columns.marker] = g_strdup("None"); + row_sep[marker_columns.image] = NULL; + row_sep[marker_columns.stock] = false; + row_sep[marker_columns.history] = false; + row_sep[marker_columns.separator] = true; + + // find and load markers.svg + if (markers_doc == nullptr) { + char *markers_source = g_build_filename(INKSCAPE_MARKERSDIR, "markers.svg", NULL); + if (Inkscape::IO::file_test(markers_source, G_FILE_TEST_IS_REGULAR)) { + markers_doc = SPDocument::createNewDoc(markers_source, FALSE); + } + g_free(markers_source); + } + + // load markers from markers.svg + if (markers_doc) { + sp_marker_list_from_doc(markers_doc, false); + } + + set_sensitive(true); +} + +/** + * Sets the current marker in the marker combobox. + */ +void MarkerComboBox::set_current(SPObject *marker) +{ + updating = true; + + if (marker != nullptr) { + gchar *markname = g_strdup(marker->getRepr()->attribute("id")); + set_selected(markname); + g_free (markname); + } + else { + set_selected(nullptr); + } + + updating = false; + +} +/** + * Return a uri string representing the current selected marker used for setting the marker style in the document + */ +const gchar * MarkerComboBox::get_active_marker_uri() +{ + /* Get Marker */ + const gchar *markid = get_active()->get_value(marker_columns.marker); + if (!markid) + { + return nullptr; + } + + gchar const *marker = ""; + if (strcmp(markid, "none")) { + bool stockid = get_active()->get_value(marker_columns.stock); + + gchar *markurn; + if (stockid) + { + markurn = g_strconcat("urn:inkscape:marker:",markid,NULL); + } + else + { + markurn = g_strdup(markid); + } + SPObject *mark = get_stock_item(markurn, stockid); + g_free(markurn); + if (mark) { + Inkscape::XML::Node *repr = mark->getRepr(); + marker = g_strconcat("url(#", repr->attribute("id"), ")", NULL); + } + } else { + marker = g_strdup(markid); + } + + return marker; +} + +void MarkerComboBox::set_active_history() { + + const gchar *markid = get_active()->get_value(marker_columns.marker); + + // If forked from a stockid, add the stockid + SPObject const *marker = doc->getObjectById(markid); + if (marker && marker->getRepr()->attribute("inkscape:stockid")) { + markid = marker->getRepr()->attribute("inkscape:stockid"); + } + + set_selected(markid); +} + + +void MarkerComboBox::set_selected(const gchar *name, gboolean retry/*=true*/) { + + if (!name) { + set_active(0); + return; + } + + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.marker] && + !strcmp(row[marker_columns.marker], name)) { + set_active(iter); + return; + } + } + + // Didn't find it in the list, try refreshing from the doc + if (retry) { + sp_marker_list_from_doc(doc, true); + set_selected(name, false); + } +} + + +/** + * Pick up all markers from source, except those that are in + * current_doc (if non-NULL), and add items to the combo. + */ +void MarkerComboBox::sp_marker_list_from_doc(SPDocument *source, gboolean history) +{ + std::vector<SPMarker *> ml = get_marker_list(source); + + remove_markers(history); // Seem to need to remove 2x + remove_markers(history); + add_markers(ml, source, history); +} + +/** + * Returns a list of markers in the defs of the given source document as a vector + * Returns NULL if there are no markers in the document. + */ +std::vector<SPMarker *> MarkerComboBox::get_marker_list (SPDocument *source) +{ + std::vector<SPMarker *> ml; + if (source == nullptr) + return ml; + + SPDefs *defs = source->getDefs(); + if (!defs) { + return ml; + } + + for (auto& child: defs->children) + { + if (SP_IS_MARKER(&child)) { + ml.push_back(SP_MARKER(&child)); + } + } + return ml; +} + +/** + * Remove history or non-history markers from the combo + */ +void MarkerComboBox::remove_markers (gboolean history) +{ + // Having the model set causes assertions when erasing rows, temporarily disconnect + unset_model(); + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.history] == history && row[marker_columns.separator] == false) { + marker_store->erase(iter); + iter = marker_store->children().begin(); + } + } + + set_model(marker_store); +} + +/** + * Adds markers in marker_list to the combo + */ +void MarkerComboBox::add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history) +{ + // Do this here, outside of loop, to speed up preview generation: + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + // Find the separator, + Gtk::TreeIter sep_iter; + for(Gtk::TreeIter iter = marker_store->children().begin(); + iter != marker_store->children().end(); ++iter) { + Gtk::TreeModel::Row row = (*iter); + if (row[marker_columns.separator]) { + sep_iter = iter; + } + } + + if (history) { + // add "None" + Gtk::TreeModel::Row row = *(marker_store->prepend()); + row[marker_columns.label] = C_("Marker", "None"); + row[marker_columns.stock] = false; + row[marker_columns.marker] = g_strdup("None"); + row[marker_columns.image] = NULL; + row[marker_columns.history] = true; + row[marker_columns.separator] = false; + } + + for (auto i:marker_list) { + + Inkscape::XML::Node *repr = i->getRepr(); + gchar const *markid = repr->attribute("inkscape:stockid") ? repr->attribute("inkscape:stockid") : repr->attribute("id"); + + // generate preview + Gtk::Image *prv = create_marker_image (24, repr->attribute("id"), source, drawing, visionkey); + prv->show(); + + // Add history before separator, others after + Gtk::TreeModel::Row row; + if (history) + row = *(marker_store->insert(sep_iter)); + else + row = *(marker_store->append()); + + row[marker_columns.label] = ink_ellipsize_text(markid, 20); + // Non "stock" markers can also have "inkscape:stockid" (when using extension ColorMarkers), + // So use !is_history instead to determine is it is "stock" (ie in the markers.svg file) + row[marker_columns.stock] = !history; + row[marker_columns.marker] = repr->attribute("id"); + row[marker_columns.image] = prv; + row[marker_columns.history] = history; + row[marker_columns.separator] = false; + + } + + sandbox->getRoot()->invoke_hide(visionkey); +} + +/* + * Remove from the cache and recreate a marker image + */ +void +MarkerComboBox::update_marker_image(gchar const *mname) +{ + gchar *cache_name = g_strconcat(combo_id, mname, NULL); + Glib::ustring key = svg_preview_cache.cache_key(doc->getDocumentURI(), cache_name, 24); + g_free (cache_name); + svg_preview_cache.remove_preview_from_cache(key); + + Inkscape::Drawing drawing; + unsigned const visionkey = SPItem::display_key_new(1); + drawing.setRoot(sandbox->getRoot()->invoke_show(drawing, visionkey, SP_ITEM_SHOW_DISPLAY)); + Gtk::Image *prv = create_marker_image(24, mname, doc, drawing, visionkey); + if (prv) { + prv->show(); + } + sandbox->getRoot()->invoke_hide(visionkey); + + for(const auto & iter : marker_store->children()) { + Gtk::TreeModel::Row row = iter; + if (row[marker_columns.marker] && row[marker_columns.history] && + !strcmp(row[marker_columns.marker], mname)) { + row[marker_columns.image] = prv; + return; + } + } + +} +/** + * Creates a copy of the marker named mname, determines its visible and renderable + * area in the bounding box, and then renders it. This allows us to fill in + * preview images of each marker in the marker combobox. + */ +Gtk::Image * +MarkerComboBox::create_marker_image(unsigned psize, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/) +{ + // Retrieve the marker named 'mname' from the source SVG document + SPObject const *marker = source->getObjectById(mname); + if (marker == nullptr) { + return nullptr; + } + + // Create a copy repr of the marker with id="sample" + Inkscape::XML::Document *xml_doc = sandbox->getReprDoc(); + Inkscape::XML::Node *mrepr = marker->getRepr()->duplicate(xml_doc); + mrepr->setAttribute("id", "sample"); + + // Replace the old sample in the sandbox by the new one + Inkscape::XML::Node *defsrepr = sandbox->getObjectById("defs")->getRepr(); + SPObject *oldmarker = sandbox->getObjectById("sample"); + if (oldmarker) { + oldmarker->deleteObject(false); + } + + // TODO - This causes a SIGTRAP on windows + defsrepr->appendChild(mrepr); + + Inkscape::GC::release(mrepr); + + // If the marker color is a url link to a pattern or gradient copy that too + SPObject *mk = source->getObjectById(mname); + SPCSSAttr *css_marker = sp_css_attr_from_object(mk->firstChild(), SP_STYLE_FLAG_ALWAYS); + //const char *mfill = sp_repr_css_property(css_marker, "fill", "none"); + const char *mstroke = sp_repr_css_property(css_marker, "fill", "none"); + + if (!strncmp (mstroke, "url(", 4)) { + SPObject *linkObj = getMarkerObj(mstroke, source); + if (linkObj) { + Inkscape::XML::Node *grepr = linkObj->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = sandbox->getObjectById(linkObj->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + + if (SP_IS_GRADIENT(linkObj)) { + SPGradient *vector = sp_gradient_get_forked_vector_if_necessary (SP_GRADIENT(linkObj), false); + if (vector) { + Inkscape::XML::Node *grepr = vector->getRepr()->duplicate(xml_doc); + SPObject *oldmarker = sandbox->getObjectById(vector->getId()); + if (oldmarker) { + oldmarker->deleteObject(false); + } + defsrepr->appendChild(grepr); + Inkscape::GC::release(grepr); + } + } + } + } + +// Uncomment this to get the sandbox documents saved (useful for debugging) + //FILE *fp = fopen (g_strconcat(combo_id, mname, ".svg", NULL), "w"); + //sp_repr_save_stream(sandbox->getReprDoc(), fp); + //fclose (fp); + + // object to render; note that the id is the same as that of the combo we're building + SPObject *object = sandbox->getObjectById(combo_id); + sandbox->getRoot()->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + sandbox->ensureUpToDate(); + + if (object == nullptr || !SP_IS_ITEM(object)) { + return nullptr; // sandbox broken? + } + + SPItem *item = SP_ITEM(object); + // Find object's bbox in document + Geom::OptRect dbox = item->documentVisualBounds(); + + if (!dbox) { + return nullptr; + } + + /* Update to renderable state */ + gchar *cache_name = g_strconcat(combo_id, mname, NULL); + Glib::ustring key = svg_preview_cache.cache_key(source->getDocumentURI(), cache_name, psize); + g_free (cache_name); + GdkPixbuf *pixbuf = svg_preview_cache.get_preview_from_cache(key); // no ref created + + if (!pixbuf) { + pixbuf = render_pixbuf(drawing, 0.8, *dbox, psize); + svg_preview_cache.set_preview_in_cache(key, pixbuf); + g_object_unref(pixbuf); // reference is held by svg_preview_cache + } + + // Create widget + Gtk::Image *pb = Glib::wrap(GTK_IMAGE(gtk_image_new_from_pixbuf(pixbuf))); + return pb; +} + +void MarkerComboBox::prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ) { + + Gtk::Image *image = (*row)[marker_columns.image]; + if (image) + image_renderer.property_pixbuf() = image->get_pixbuf(); + else + image_renderer.property_pixbuf() = empty_image->get_pixbuf(); +} + +gboolean MarkerComboBox::separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) { + + gboolean sep = FALSE; + gtk_tree_model_get(model, iter, 4, &sep, -1); + return sep; +} + +/** + * Returns a new document containing default start, mid, and end markers. + */ +SPDocument *MarkerComboBox::ink_markers_preview_doc () +{ +gchar const *buffer = R"A( + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + id="MarkerSample"> + + <defs id="defs"/> + + <g id="marker-start"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-start:url(#sample)" + d="M 12.5,13 L 25,13"/> + <rect x="0" y="0" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + <g id="marker-mid"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-mid:url(#sample)" + d="M 0,113 L 12.5,113 L 25,113"/> + <rect x="0" y="100" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + <g id="marker-end"> + <path style="fill:gray;stroke:darkgray;stroke-width:1.7;marker-end:url(#sample)" + d="M 0,213 L 12.5,213"/> + <rect x="0" y="200" width="25" height="25" style="fill:none;stroke:none"/> + </g> + + </svg> +)A"; + + return SPDocument::createNewDocFromMem (buffer, strlen(buffer), FALSE); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/stroke-marker-selector.h b/src/widgets/stroke-marker-selector.h new file mode 100644 index 0000000..c9bd2e8 --- /dev/null +++ b/src/widgets/stroke-marker-selector.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_MARKER_SELECTOR_NEW_H +#define SEEN_SP_MARKER_SELECTOR_NEW_H + +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Maximilian Albert <maximilian.albert> (gtkmm-ification) + * + * Copyright (C) 2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <vector> + +#include <gtkmm/box.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> + +#include <sigc++/signal.h> + +#include "document.h" +#include "inkscape.h" + +#include "display/drawing.h" + +class SPMarker; + +namespace Gtk { + +class Container; +class Adjustment; +} + +/** + * ComboBox derived class for selecting stroke markers. + */ + +class MarkerComboBox : public Gtk::ComboBox { +public: + MarkerComboBox(gchar const *id, int loc); + ~MarkerComboBox() override; + + void setDocument(SPDocument *); + + sigc::signal<void> changed_signal; + + void set_current(SPObject *marker); + void set_active_history(); + void set_selected(const gchar *name, gboolean retry=true); + const gchar *get_active_marker_uri(); + bool update() { return updating; }; + gchar const *get_id() { return combo_id; }; + void update_marker_image(gchar const *mname); + int get_loc() { return loc; }; + +private: + + + Glib::RefPtr<Gtk::ListStore> marker_store; + gchar const *combo_id; + int loc; + bool updating; + guint markerCount; + SPDocument *doc = nullptr; + SPDocument *sandbox; + Gtk::Image *empty_image; + Gtk::CellRendererPixbuf image_renderer; + + class MarkerColumns : public Gtk::TreeModel::ColumnRecord { + public: + Gtk::TreeModelColumn<Glib::ustring> label; + Gtk::TreeModelColumn<const gchar *> marker; // ustring doesn't work here on windows due to unicode + Gtk::TreeModelColumn<gboolean> stock; + Gtk::TreeModelColumn<Gtk::Image *> image; + Gtk::TreeModelColumn<gboolean> history; + Gtk::TreeModelColumn<gboolean> separator; + + MarkerColumns() { + add(label); add(stock); add(marker); add(history); add(separator); add(image); + } + }; + MarkerColumns marker_columns; + + void init_combo(); + void set_history(Gtk::TreeModel::Row match_row); + void sp_marker_list_from_doc(SPDocument *source, gboolean history); + std::vector <SPMarker*> get_marker_list (SPDocument *source); + void add_markers (std::vector<SPMarker *> const& marker_list, SPDocument *source, gboolean history); + void remove_markers (gboolean history); + SPDocument *ink_markers_preview_doc (); + Gtk::Image * create_marker_image(unsigned psize, gchar const *mname, + SPDocument *source, Inkscape::Drawing &drawing, unsigned /*visionkey*/); + + /* + * Callbacks for drawing the combo box + */ + void prepareImageRenderer( Gtk::TreeModel::const_iterator const &row ); + static gboolean separator_cb (GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + + static void handleDefsModified(MarkerComboBox *self); + + void refreshHistory(); + + sigc::connection modified_connection; +}; + +#endif // SEEN_SP_MARKER_SELECTOR_NEW_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/stroke-style.cpp b/src/widgets/stroke-style.cpp new file mode 100644 index 0000000..f2fe0e1 --- /dev/null +++ b/src/widgets/stroke-style.cpp @@ -0,0 +1,1346 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Bryce Harrington <brycehar@bryceharrington.org> + * bulia byak <buliabyak@users.sf.net> + * Maximilian Albert <maximilian.albert@gmail.com> + * Josh Andler <scislac@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2001-2005 authors + * Copyright (C) 2001 Ximian, Inc. + * Copyright (C) 2004 John Cliff + * Copyright (C) 2008 Maximilian Albert (gtkmm-ification) + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_SS_VERBOSE + +#include "desktop-widget.h" +#include "stroke-style.h" + +#include "object/sp-marker.h" +#include "object/sp-namedview.h" +#include "object/sp-rect.h" +#include "object/sp-stop.h" +#include "object/sp-text.h" + +#include "svg/svg-color.h" + +#include "ui/icon-loader.h" +#include "ui/widget/dash-selector.h" +#include "ui/widget/unit-menu.h" + +#include "widgets/style-utils.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +/** + * Creates a new widget for the line stroke paint. + */ +Gtk::Widget *sp_stroke_style_paint_widget_new() +{ + return Inkscape::Widgets::createStyleWidget( STROKE ); +} + +/** + * Creates a new widget for the line stroke style. + */ +Gtk::Widget *sp_stroke_style_line_widget_new() +{ + return Inkscape::Widgets::createStrokeStyleWidget(); +} + +void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop) +{ + Inkscape::StrokeStyle *ss = dynamic_cast<Inkscape::StrokeStyle*>(widget); + if (ss) { + ss->setDesktop(desktop); + } +} + + +/** + * Extract the actual name of the link + * e.g. get mTriangle from url(#mTriangle). + * \return Buffer containing the actual name, allocated from GLib; + * the caller should free the buffer when they no longer need it. + */ +SPObject* getMarkerObj(gchar const *n, SPDocument *doc) +{ + gchar const *p = n; + while (*p != '\0' && *p != '#') { + p++; + } + + if (*p == '\0' || p[1] == '\0') { + return nullptr; + } + + p++; + int c = 0; + while (p[c] != '\0' && p[c] != ')') { + c++; + } + + if (p[c] == '\0') { + return nullptr; + } + + gchar* b = g_strdup(p); + b[c] = '\0'; + + // FIXME: get the document from the object and let the caller pass it in + SPObject *marker = doc->getObjectById(b); + + g_free(b); + return marker; +} + +namespace Inkscape { + + +/** + * Construct a stroke-style radio button with a given icon + * + * \param[in] grp The Gtk::RadioButtonGroup to which to add the new button + * \param[in] icon The icon to use for the button + * \param[in] button_type The type of stroke-style radio button (join/cap) + * \param[in] stroke_style The style attribute to associate with the button + */ +StrokeStyle::StrokeStyleButton::StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style) + : + Gtk::RadioButton(grp), + button_type(button_type), + stroke_style(stroke_style) +{ + show(); + set_mode(false); + + auto px = Gtk::manage(sp_get_icon_image(icon, Gtk::ICON_SIZE_LARGE_TOOLBAR)); + g_assert(px != nullptr); + px->show(); + add(*px); +} + +/** + * Create the fill or stroke style widget, and hook up all the signals. + */ +Gtk::Widget *Inkscape::Widgets::createStrokeStyleWidget( ) +{ + StrokeStyle *strokeStyle = new StrokeStyle(); + + return strokeStyle; +} + +StrokeStyle::StrokeStyle() : + Gtk::Box(), + miterLimitSpin(), + widthSpin(), + unitSelector(), + joinMiter(), + joinRound(), + joinBevel(), + capButt(), + capRound(), + capSquare(), + dashSelector(), + update(false), + desktop(nullptr), + selectChangedConn(), + selectModifiedConn(), + startMarkerConn(), + midMarkerConn(), + endMarkerConn(), + _old_unit(nullptr) +{ + table = new Gtk::Grid(); + table->set_border_width(4); + table->set_row_spacing(4); + table->set_hexpand(false); + table->set_halign(Gtk::ALIGN_CENTER); + table->show(); + add(*table); + + Gtk::HBox *hb; + gint i = 0; + + //spw_label(t, C_("Stroke width", "_Width:"), 0, i); + + hb = spw_hbox(table, 3, 1, i); + +// TODO: when this is gtkmmified, use an Inkscape::UI::Widget::ScalarUnit instead of the separate +// spinbutton and unit selector for stroke width. In sp_stroke_style_line_update, use +// setHundredPercent to remember the averaged width corresponding to 100%. Then the +// stroke_width_set_unit will be removed (because ScalarUnit takes care of conversions itself), and +// with it, the two remaining calls of stroke_average_width, allowing us to get rid of that +// function in desktop-style. + widthAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(1.0, 0.0, 1000.0, 0.1, 10.0, 0.0)); + widthSpin = new Inkscape::UI::Widget::SpinButton(*widthAdj, 0.1, 3); + widthSpin->set_tooltip_text(_("Stroke width")); + widthSpin->show(); + spw_label(table, C_("Stroke width", "_Width:"), 0, i, widthSpin); + + sp_dialog_defocus_on_enter_cpp(widthSpin); + + hb->pack_start(*widthSpin, false, false, 0); + unitSelector = new Inkscape::UI::Widget::UnitMenu(); + unitSelector->setUnitType(Inkscape::Util::UNIT_TYPE_LINEAR); + Gtk::Widget *us = Gtk::manage(unitSelector); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + + unitSelector->addUnit(*unit_table.getUnit("%")); + _old_unit = unitSelector->getUnit(); + if (desktop) { + unitSelector->setUnit(desktop->getNamedView()->display_units->abbr); + _old_unit = desktop->getNamedView()->display_units; + } + widthSpin->setUnitMenu(unitSelector); + unitChangedConn = unitSelector->signal_changed().connect(sigc::mem_fun(*this, &StrokeStyle::unitChangedCB)); + + us->show(); + + hb->pack_start(*us, FALSE, FALSE, 0); + (*widthAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::widthChangedCB)); + i++; + + /* Dash */ + spw_label(table, _("Dashes:"), 0, i, nullptr); //no mnemonic for now + //decide what to do: + // implement a set_mnemonic_source function in the + // Inkscape::UI::Widget::DashSelector class, so that we do not have to + // expose any of the underlying widgets? + dashSelector = Gtk::manage(new Inkscape::UI::Widget::DashSelector); + + dashSelector->show(); + dashSelector->set_hexpand(); + dashSelector->set_halign(Gtk::ALIGN_FILL); + dashSelector->set_valign(Gtk::ALIGN_CENTER); + table->attach(*dashSelector, 1, i, 3, 1); + dashSelector->changed_signal.connect(sigc::mem_fun(*this, &StrokeStyle::lineDashChangedCB)); + + i++; + + /* Drop down marker selectors*/ + // TRANSLATORS: Path markers are an SVG feature that allows you to attach arbitrary shapes + // (arrowheads, bullets, faces, whatever) to the start, end, or middle nodes of a path. + + spw_label(table, _("Markers:"), 0, i, nullptr); + + hb = spw_hbox(table, 1, 1, i); + i++; + + startMarkerCombo = Gtk::manage(new MarkerComboBox("marker-start", SP_MARKER_LOC_START)); + startMarkerCombo->set_tooltip_text(_("Start Markers are drawn on the first node of a path or shape")); + startMarkerConn = startMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), startMarkerCombo, this, SP_MARKER_LOC_START)); + startMarkerCombo->show(); + + hb->pack_start(*startMarkerCombo, true, true, 0); + + midMarkerCombo = Gtk::manage(new MarkerComboBox("marker-mid", SP_MARKER_LOC_MID)); + midMarkerCombo->set_tooltip_text(_("Mid Markers are drawn on every node of a path or shape except the first and last nodes")); + midMarkerConn = midMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), midMarkerCombo, this, SP_MARKER_LOC_MID)); + midMarkerCombo->show(); + + hb->pack_start(*midMarkerCombo, true, true, 0); + + endMarkerCombo = Gtk::manage(new MarkerComboBox("marker-end", SP_MARKER_LOC_END)); + endMarkerCombo->set_tooltip_text(_("End Markers are drawn on the last node of a path or shape")); + endMarkerConn = endMarkerCombo->signal_changed().connect( + sigc::bind<MarkerComboBox *, StrokeStyle *, SPMarkerLoc>( + sigc::ptr_fun(&StrokeStyle::markerSelectCB), endMarkerCombo, this, SP_MARKER_LOC_END)); + endMarkerCombo->show(); + + hb->pack_start(*endMarkerCombo, true, true, 0); + + i++; + + /* Join type */ + // TRANSLATORS: The line join style specifies the shape to be used at the + // corners of paths. It can be "miter", "round" or "bevel". + spw_label(table, _("Join:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup joinGrp; + + joinRound = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-round"), + hb, STROKE_STYLE_BUTTON_JOIN, "round"); + + // TRANSLATORS: Round join: joining lines with a rounded corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinRound->set_tooltip_text(_("Round join")); + + joinBevel = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-bevel"), + hb, STROKE_STYLE_BUTTON_JOIN, "bevel"); + + // TRANSLATORS: Bevel join: joining lines with a blunted (flattened) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinBevel->set_tooltip_text(_("Bevel join")); + + joinMiter = makeRadioButton(joinGrp, INKSCAPE_ICON("stroke-join-miter"), + hb, STROKE_STYLE_BUTTON_JOIN, "miter"); + + // TRANSLATORS: Miter join: joining lines with a sharp (pointed) corner. + // For an example, draw a triangle with a large stroke width and modify the + // "Join" option (in the Fill and Stroke dialog). + joinMiter->set_tooltip_text(_("Miter join")); + + /* Miterlimit */ + // TRANSLATORS: Miter limit: only for "miter join", this limits the length + // of the sharp "spike" when the lines connect at too sharp an angle. + // When two line segments meet at a sharp angle, a miter join results in a + // spike that extends well beyond the connection point. The purpose of the + // miter limit is to cut off such spikes (i.e. convert them into bevels) + // when they become too long. + //spw_label(t, _("Miter _limit:"), 0, i); + miterLimitAdj = new Glib::RefPtr<Gtk::Adjustment>(Gtk::Adjustment::create(4.0, 0.0, 100.0, 0.1, 10.0, 0.0)); + miterLimitSpin = new Inkscape::UI::Widget::SpinButton(*miterLimitAdj, 0.1, 2); + miterLimitSpin->set_tooltip_text(_("Maximum length of the miter (in units of stroke width)")); + miterLimitSpin->show(); + sp_dialog_defocus_on_enter_cpp(miterLimitSpin); + + hb->pack_start(*miterLimitSpin, false, false, 0); + (*miterLimitAdj)->signal_value_changed().connect(sigc::mem_fun(*this, &StrokeStyle::miterLimitChangedCB)); + i++; + + /* Cap type */ + // TRANSLATORS: cap type specifies the shape for the ends of lines + //spw_label(t, _("_Cap:"), 0, i); + spw_label(table, _("Cap:"), 0, i, nullptr); + + hb = spw_hbox(table, 3, 1, i); + + Gtk::RadioButtonGroup capGrp; + + capButt = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-butt"), + hb, STROKE_STYLE_BUTTON_CAP, "butt"); + + // TRANSLATORS: Butt cap: the line shape does not extend beyond the end point + // of the line; the ends of the line are square + capButt->set_tooltip_text(_("Butt cap")); + + capRound = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-round"), + hb, STROKE_STYLE_BUTTON_CAP, "round"); + + // TRANSLATORS: Round cap: the line shape extends beyond the end point of the + // line; the ends of the line are rounded + capRound->set_tooltip_text(_("Round cap")); + + capSquare = makeRadioButton(capGrp, INKSCAPE_ICON("stroke-cap-square"), + hb, STROKE_STYLE_BUTTON_CAP, "square"); + + // TRANSLATORS: Square cap: the line shape extends beyond the end point of the + // line; the ends of the line are square + capSquare->set_tooltip_text(_("Square cap")); + + i++; + + /* Paint order */ + // TRANSLATORS: Paint order determines the order the 'fill', 'stroke', and 'markers are painted. + spw_label(table, _("Order:"), 0, i, nullptr); + + hb = spw_hbox(table, 4, 1, i); + + Gtk::RadioButtonGroup paintOrderGrp; + + paintOrderFSM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fsm"), + hb, STROKE_STYLE_BUTTON_ORDER, "normal"); + paintOrderFSM->set_tooltip_text(_("Fill, Stroke, Markers")); + + paintOrderSFM = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-sfm"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke fill markers"); + paintOrderSFM->set_tooltip_text(_("Stroke, Fill, Markers")); + + paintOrderFMS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-fms"), + hb, STROKE_STYLE_BUTTON_ORDER, "fill markers stroke"); + paintOrderFMS->set_tooltip_text(_("Fill, Markers, Stroke")); + + i++; + + hb = spw_hbox(table, 4, 1, i); + + paintOrderMFS = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-mfs"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers fill stroke"); + paintOrderMFS->set_tooltip_text(_("Markers, Fill, Stroke")); + + paintOrderSMF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-smf"), + hb, STROKE_STYLE_BUTTON_ORDER, "stroke markers fill"); + paintOrderSMF->set_tooltip_text(_("Stroke, Markers, Fill")); + + paintOrderMSF = makeRadioButton(paintOrderGrp, INKSCAPE_ICON("paint-order-msf"), + hb, STROKE_STYLE_BUTTON_ORDER, "markers stroke fill"); + paintOrderMSF->set_tooltip_text(_("Markers, Stroke, Fill")); + + i++; +} + +StrokeStyle::~StrokeStyle() +{ + selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); +} + +void StrokeStyle::setDesktop(SPDesktop *desktop) +{ + if (this->desktop != desktop) { + + if (this->desktop) { + selectModifiedConn.disconnect(); + selectChangedConn.disconnect(); + _document_replaced_connection.disconnect(); + } + this->desktop = desktop; + + if (!desktop) { + return; + } + + if (desktop->selection) { + selectChangedConn = desktop->selection->connectChanged(sigc::hide(sigc::mem_fun(*this, &StrokeStyle::selectionChangedCB))); + selectModifiedConn = desktop->selection->connectModified(sigc::hide<0>(sigc::mem_fun(*this, &StrokeStyle::selectionModifiedCB))); + } + + _document_replaced_connection = + desktop->connectDocumentReplaced(sigc::mem_fun(this, &StrokeStyle::_handleDocumentReplaced)); + + _handleDocumentReplaced(nullptr, desktop->getDocument()); + + updateLine(); + } +} + +void StrokeStyle::_handleDocumentReplaced(SPDesktop *, SPDocument *document) +{ + for (MarkerComboBox *combo : { startMarkerCombo, midMarkerCombo, endMarkerCombo }) { + combo->setDocument(document); + } +} + + +/** + * Helper function for creating stroke-style radio buttons. + * + * \param[in] grp The Gtk::RadioButtonGroup in which to add the button + * \param[in] icon The icon for the button + * \param[in] hb The Gtk::Box container in which to add the button + * \param[in] button_type The type (join/cap) for the button + * \param[in] stroke_style The style attribute to associate with the button + * + * \details After instantiating the button, it is added to a container box and + * a handler for the toggle event is connected. + */ +StrokeStyle::StrokeStyleButton * +StrokeStyle::makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::HBox *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style) +{ + g_assert(icon != nullptr); + g_assert(hb != nullptr); + + StrokeStyleButton *tb = new StrokeStyleButton(grp, icon, button_type, stroke_style); + + hb->pack_start(*tb, false, false, 0); + set_data(icon, tb); + + tb->signal_toggled().connect(sigc::bind<StrokeStyleButton *, StrokeStyle *>( + sigc::ptr_fun(&StrokeStyle::buttonToggledCB), tb, this)); + + return tb; +} + +bool StrokeStyle::shouldMarkersBeUpdated() +{ + return startMarkerCombo->update() || midMarkerCombo->update() || + endMarkerCombo->update(); +} + +/** + * Handles when user selects one of the markers from the marker combobox. + * Gets the marker uri string and applies it to all selected + * items in the current desktop. + */ +void StrokeStyle::markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const /*which*/) +{ + if (spw->update || spw->shouldMarkersBeUpdated()) { + return; + } + + spw->update = true; + + SPDocument *document = spw->desktop->getDocument(); + if (!document) { + return; + } + + /* Get Marker */ + gchar const *marker = marker_combo->get_active_marker_uri(); + + + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar const *combo_id = marker_combo->get_id(); + sp_repr_css_set_property(css, combo_id, marker); + + // Also update the marker combobox, so the document's markers + // show up at the top of the combobox +// sp_stroke_style_line_update( SP_WIDGET(spw), desktop ? desktop->getSelection() : NULL); + //spw->updateMarkerHist(which); + + Inkscape::Selection *selection = spw->desktop->getSelection(); + auto itemlist= selection->items(); + for(auto i=itemlist.begin();i!=itemlist.end();++i){ + SPItem *item = *i; + if (!SP_IS_SHAPE(item) || SP_IS_RECT(item)) { // can't set marker to rect, until it's converted to using <path> + continue; + } + Inkscape::XML::Node *selrepr = item->getRepr(); + if (selrepr) { + sp_repr_css_change_recursive(selrepr, css, "style"); + SPObject *markerObj = getMarkerObj(marker, document); + spw->setMarkerColor(markerObj, marker_combo->get_loc(), item); + } + + item->requestModified(SP_OBJECT_MODIFIED_FLAG); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG); + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, _("Set markers")); + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + spw->update = false; +}; + +void StrokeStyle::updateMarkerHist(SPMarkerLoc const which) +{ + switch (which) { + case SP_MARKER_LOC_START: + startMarkerConn.block(); + startMarkerCombo->set_active_history(); + startMarkerConn.unblock(); + break; + + case SP_MARKER_LOC_MID: + midMarkerConn.block(); + midMarkerCombo->set_active_history(); + midMarkerConn.unblock(); + break; + + case SP_MARKER_LOC_END: + endMarkerConn.block(); + endMarkerCombo->set_active_history(); + endMarkerConn.unblock(); + break; + default: + g_assert_not_reached(); + } +} + +/** + * Callback for when UnitMenu widget is modified. + * Triggers update action. + */ +void StrokeStyle::unitChangedCB() +{ + Inkscape::Util::Unit const *new_unit = unitSelector->getUnit(); + if (new_unit->type == Inkscape::Util::UNIT_TYPE_DIMENSIONLESS) { + widthSpin->set_value(100); + } + widthSpin->set_value(Inkscape::Util::Quantity::convert(widthSpin->get_value(), _old_unit, new_unit)); + _old_unit = new_unit; +} + +/** + * Callback for when stroke style widget is modified. + * Triggers update action. + */ +void +StrokeStyle::selectionModifiedCB(guint flags) +{ + // We care deeply about only updating when the style is updated + // if we update on other flags, we slow inkscape down when dragging + if (flags & (SP_OBJECT_STYLE_MODIFIED_FLAG)) { + updateLine(); + } +} + +/** + * Callback for when stroke style widget is changed. + * Triggers update action. + */ +void +StrokeStyle::selectionChangedCB() +{ + updateLine(); +} + +/* + * Fork marker if necessary and set the referencing items url to the new marker + * Return the new marker + */ +SPObject * +StrokeStyle::forkMarker(SPObject *marker, int loc, SPItem *item) +{ + if (!item || !marker) { + return nullptr; + } + + gchar const *marker_id = SPMarkerNames[loc].key; + + /* + * Optimization when all the references to this marker are from this item + * then we can reuse it and don't need to fork + */ + Glib::ustring urlId = Glib::ustring::format("url(#", marker->getRepr()->attribute("id"), ")"); + unsigned int refs = 0; + for (int i = SP_MARKER_LOC_START; i < SP_MARKER_LOC_QTY; i++) { + if (item->style->marker_ptrs[i]->set && + !strcmp(urlId.c_str(), item->style->marker_ptrs[i]->value())) { + refs++; + } + } + if (marker->hrefcount <= refs) { + return marker; + } + + marker = sp_marker_fork_if_necessary(marker); + + // Update the items url to new marker + Inkscape::XML::Node *mark_repr = marker->getRepr(); + SPCSSAttr *css_item = sp_repr_css_attr_new(); + sp_repr_css_set_property(css_item, marker_id, g_strconcat("url(#", mark_repr->attribute("id"), ")", NULL)); + sp_repr_css_change_recursive(item->getRepr(), css_item, "style"); + + sp_repr_css_attr_unref(css_item); + css_item = nullptr; + + return marker; +} + +/** + * Change the color of the marker to match the color of the item. + * Marker stroke color is set to item stroke color. + * Fill color : + * 1. If the item has fill, use that for the marker fill, + * 2. If the marker has same fill and stroke assume its solid, use item stroke for both fill and stroke the line stroke + * 3. If the marker has fill color, use the marker fill color + * + */ +void +StrokeStyle::setMarkerColor(SPObject *marker, int loc, SPItem *item) +{ + + if (!item || !marker) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean colorStock = prefs->getBool("/options/markers/colorStockMarkers", true); + gboolean colorCustom = prefs->getBool("/options/markers/colorCustomMarkers", false); + const gchar *stock = marker->getRepr()->attribute("inkscape:isstock"); + gboolean isStock = (stock && !strcmp(stock,"true")); + + if (isStock ? !colorStock : !colorCustom) { + return; + } + + // Check if we need to fork this marker + marker = forkMarker(marker, loc, item); + + Inkscape::XML::Node *repr = marker->getRepr()->firstChild(); + if (!repr) { + return; + }; + + // Current line style + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *lstroke = getItemColorForMarker(item, FOR_STROKE, loc); + const char *lstroke_opacity = sp_repr_css_property(css_item, "stroke-opacity", "1"); + const char *lfill = getItemColorForMarker(item, FOR_FILL, loc); + const char *lfill_opacity = sp_repr_css_property(css_item, "fill-opacity", "1"); + + // Current marker style + SPCSSAttr *css_marker = sp_css_attr_from_object(marker->firstChild(), SP_STYLE_FLAG_ALWAYS); + const char *mfill = sp_repr_css_property(css_marker, "fill", "none"); + const char *mstroke = sp_repr_css_property(css_marker, "fill", "none"); + + // Create new marker style with the lines stroke + SPCSSAttr *css = sp_repr_css_attr_new(); + + sp_repr_css_set_property(css, "stroke", lstroke); + sp_repr_css_set_property(css, "stroke-opacity", lstroke_opacity); + + if (strcmp(lfill, "none") ) { + // 1. If the line has fill, use that for the marker fill + sp_repr_css_set_property(css, "fill", lfill); + sp_repr_css_set_property(css, "fill-opacity", lfill_opacity); + } + else if (mfill && mstroke && !strcmp(mfill, mstroke) && mfill[0] == '#' && strcmp(mfill, "#ffffff")) { + // 2. If the marker has same fill and stroke assume its solid. use line stroke for both fill and stroke the line stroke + sp_repr_css_set_property(css, "fill", lstroke); + sp_repr_css_set_property(css, "fill-opacity", lstroke_opacity); + } + else if (mfill && mfill[0] == '#' && strcmp(mfill, "#000000")) { + // 3. If the marker has fill color, use the marker fill color + sp_repr_css_set_property(css, "fill", mfill); + //sp_repr_css_set_property(css, "fill-opacity", mfill_opacity); + } + + sp_repr_css_change_recursive(marker->firstChild()->getRepr(), css, "style"); + + // Tell the combos to update its image cache of this marker + gchar const *mid = marker->getRepr()->attribute("id"); + startMarkerCombo->update_marker_image(mid); + midMarkerCombo->update_marker_image(mid); + endMarkerCombo->update_marker_image(mid); + + sp_repr_css_attr_unref(css); + css = nullptr; + + +} + +/* + * Get the fill or stroke color of the item + * If its a gradient, then return first or last stop color + */ +const char * +StrokeStyle::getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc) +{ + SPCSSAttr *css_item = sp_css_attr_from_object(item, SP_STYLE_FLAG_ALWAYS); + const char *color; + if (fill_or_stroke == FOR_FILL) + color = sp_repr_css_property(css_item, "fill", "none"); + else + color = sp_repr_css_property(css_item, "stroke", "none"); + + if (!strncmp (color, "url(", 4)) { + // If the item has a gradient use the first stop color for the marker + + SPGradient *grad = getGradient(item, fill_or_stroke); + if (grad) { + SPGradient *vector = grad->getVector(FALSE); + SPStop *stop = vector->getFirstStop(); + if (loc == SP_MARKER_LOC_END) { + stop = sp_last_stop(vector); + } + if (stop) { + guint32 const c1 = stop->get_rgba32(); + gchar c[64]; + sp_svg_write_color(c, sizeof(c), c1); + color = g_strdup(c); + //lstroke_opacity = Glib::ustring::format(stop->opacity).c_str(); + } + } + } + return color; +} +/** + * Sets selector widgets' dash style from an SPStyle object. + */ +void +StrokeStyle::setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style) +{ + if (!style->stroke_dasharray.values.empty()) { + double d[64]; + size_t len = MIN(style->stroke_dasharray.values.size(), 64); + /* Set dash */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean scale = prefs->getBool("/options/dash/scale", true); + double scaledash = 1.0; + if (scale) { + scaledash = style->stroke_width.computed; + } + for (unsigned i = 0; i < len; i++) { + if (style->stroke_width.computed != 0) + d[i] = style->stroke_dasharray.values[i].value / scaledash; + else + d[i] = style->stroke_dasharray.values[i].value; // is there a better thing to do for stroke_width==0? + } + dsel->set_dash(len, d, + style->stroke_width.computed != 0 ? style->stroke_dashoffset.value / scaledash + : style->stroke_dashoffset.value); + } else { + dsel->set_dash(0, nullptr, 0.0); + } +} + +/** + * Sets the join type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setJoinType (unsigned const jointype) +{ + Gtk::RadioButton *tb = nullptr; + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + tb = joinMiter; + break; + case SP_STROKE_LINEJOIN_ROUND: + tb = joinRound; + break; + case SP_STROKE_LINEJOIN_BEVEL: + tb = joinBevel; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setJoinType(): Invalid value: " << jointype << std::endl; + tb = joinMiter; + break; + } + setJoinButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setCapType (unsigned const captype) +{ + Gtk::RadioButton *tb = nullptr; + switch (captype) { + case SP_STROKE_LINECAP_BUTT: + tb = capButt; + break; + case SP_STROKE_LINECAP_ROUND: + tb = capRound; + break; + case SP_STROKE_LINECAP_SQUARE: + tb = capSquare; + break; + default: + // Should not happen + std::cerr << "StrokeStyle::setCapType(): Invalid value: " << captype << std::endl; + tb = capButt; + break; + } + setCapButtons(tb); +} + +/** + * Sets the cap type for a line, and updates the stroke style widget's buttons + */ +void +StrokeStyle::setPaintOrder (gchar const *paint_order) +{ + Gtk::RadioButton *tb = paintOrderFSM; + + SPIPaintOrder temp; + temp.read( paint_order ); + + if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL) { + + if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderFSM; + } else { + tb = paintOrderFMS; + } + } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) { + tb = paintOrderSFM; + } else { + tb = paintOrderSMF; + } + } else { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + tb = paintOrderMSF; + } else { + tb = paintOrderMFS; + } + } + + } + setPaintOrderButtons(tb); +} + +/** + * Callback for when stroke style widget is updated, including markers, cap type, + * join type, etc. + */ +void +StrokeStyle::updateLine() +{ + if (update) { + return; + } + + update = true; + + Inkscape::Selection *sel = desktop ? desktop->getSelection() : nullptr; + + FillOrStroke kind = GPOINTER_TO_INT(get_data("kind")) ? FILL : STROKE; + + // create temporary style + SPStyle query(SP_ACTIVE_DOCUMENT); + // query into it + int result_sw = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEWIDTH); + int result_ml = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEMITERLIMIT); + int result_cap = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKECAP); + int result_join = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_STROKEJOIN); + + int result_order = sp_desktop_query_style (SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_PAINTORDER); + + SPIPaint &targPaint = *query.getFillOrStroke(kind == FILL); + + if (!sel || sel->isEmpty()) { + // Nothing selected, grey-out all controls in the stroke-style dialog + table->set_sensitive(false); + + update = false; + + return; + } else { + table->set_sensitive(true); + + if (result_sw == QUERY_STYLE_MULTIPLE_AVERAGED) { + unitSelector->setUnit("%"); + } else { + // same width, or only one object; no sense to keep percent, switch to absolute + Inkscape::Util::Unit const *tempunit = unitSelector->getUnit(); + if (tempunit->type != Inkscape::Util::UNIT_TYPE_LINEAR) { + unitSelector->setUnit(SP_ACTIVE_DESKTOP->getNamedView()->display_units->abbr); + } + } + + Inkscape::Util::Unit const *unit = unitSelector->getUnit(); + + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + double avgwidth = Inkscape::Util::Quantity::convert(query.stroke_width.computed, "px", unit); + (*widthAdj)->set_value(avgwidth); + } else { + (*widthAdj)->set_value(100); + } + + // if none of the selected objects has a stroke, than quite some controls should be disabled + // The markers might still be shown though, so these will not be disabled + bool enabled = (result_sw != QUERY_STYLE_NOTHING) && !targPaint.isNoneSet(); + /* No objects stroked, set insensitive */ + joinMiter->set_sensitive(enabled); + joinRound->set_sensitive(enabled); + joinBevel->set_sensitive(enabled); + + miterLimitSpin->set_sensitive(enabled); + + capButt->set_sensitive(enabled); + capRound->set_sensitive(enabled); + capSquare->set_sensitive(enabled); + + dashSelector->set_sensitive(enabled); + } + + if (result_ml != QUERY_STYLE_NOTHING) + (*miterLimitAdj)->set_value(query.stroke_miterlimit.value); // TODO: reflect averagedness? + + using Inkscape::is_query_style_updateable; + if (! is_query_style_updateable(result_join)) { + setJoinType(query.stroke_linejoin.value); + } else { + setJoinButtons(nullptr); + } + + if (! is_query_style_updateable(result_cap)) { + setCapType (query.stroke_linecap.value); + } else { + setCapButtons(nullptr); + } + + if (! is_query_style_updateable(result_order)) { + setPaintOrder (query.paint_order.value); + } else { + setPaintOrder (nullptr); + } + + std::vector<SPItem*> const objects(sel->items().begin(), sel->items().end()); + if (objects.size()) { + SPObject *const object = objects[0]; + SPStyle *const style = object->style; + /* Markers */ + updateAllMarkers(objects, true); // FIXME: make this desktop query too + + /* Dash */ + setDashSelectorFromStyle(dashSelector, style); // FIXME: make this desktop query too + } + table->set_sensitive(true); + + update = false; +} + +/** + * Sets a line's dash properties in a CSS style object. + */ +void +StrokeStyle::setScaledDash(SPCSSAttr *css, + int ndash, double *dash, double offset, + double scale) +{ + if (ndash > 0) { + Inkscape::CSSOStringStream osarray; + for (int i = 0; i < ndash; i++) { + osarray << dash[i] * scale; + if (i < (ndash - 1)) { + osarray << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", osarray.str().c_str()); + + Inkscape::CSSOStringStream osoffset; + osoffset << offset * scale; + sp_repr_css_set_property(css, "stroke-dashoffset", osoffset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +static inline double calcScaleLineWidth(const double width_typed, SPItem *const item, Inkscape::Util::Unit const *const unit) +{ + if (unit->type == Inkscape::Util::UNIT_TYPE_LINEAR) { + return Inkscape::Util::Quantity::convert(width_typed, unit, "px"); + } else { // percentage + const gdouble old_w = item->style->stroke_width.computed; + return old_w * width_typed / 100; + } +} + +/** + * Sets line properties like width, dashes, markers, etc. on all currently selected items. + */ +void +StrokeStyle::scaleLine() +{ + if (!desktop) { + return; + } + + if (update) { + return; + } + + update = true; + + SPDocument *document = desktop->getDocument(); + Inkscape::Selection *selection = desktop->getSelection(); + auto items= selection->items(); + + /* TODO: Create some standardized method */ + SPCSSAttr *css = sp_repr_css_attr_new(); + + if (!items.empty()) { + double width_typed = (*widthAdj)->get_value(); + double const miterlimit = (*miterLimitAdj)->get_value(); + + Inkscape::Util::Unit const *const unit = unitSelector->getUnit(); + + double *dash, offset; + int ndash; + dashSelector->get_dash(&ndash, &dash, &offset); + + for(auto i=items.begin();i!=items.end();++i){ + /* Set stroke width */ + const double width = calcScaleLineWidth(width_typed, (*i), unit); + + { + Inkscape::CSSOStringStream os_width; + os_width << width; + sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str()); + } + + { + Inkscape::CSSOStringStream os_ml; + os_ml << miterlimit; + sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str()); + } + + /* Set dash */ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gboolean scale = prefs->getBool("/options/dash/scale", true); + if (scale) { + setScaledDash(css, ndash, dash, offset, width); + } + else { + setScaledDash(css, ndash, dash, offset, document->getDocumentScale()[0]); + } + sp_desktop_apply_css_recursive ((*i), css, true); + } + + g_free(dash); + + if (unit->type != Inkscape::Util::UNIT_TYPE_LINEAR) { + // reset to 100 percent + (*widthAdj)->set_value(100.0); + } + + } + + // we have already changed the items, so set style without changing selection + // FIXME: move the above stroke-setting stuff, including percentages, to desktop-style + sp_desktop_set_style (desktop, css, false); + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(document, SP_VERB_DIALOG_FILL_STROKE, + _("Set stroke style")); + + update = false; +} + +/** + * Callback for when the stroke style's width changes. + * Causes all line styles to be applied to all selected items. + */ +void +StrokeStyle::widthChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * Callback for when the stroke style's miterlimit changes. + * Causes all line styles to be applied to all selected items. + */ +void +StrokeStyle::miterLimitChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * Callback for when the stroke style's dash changes. + * Causes all line styles to be applied to all selected items. + */ + +void +StrokeStyle::lineDashChangedCB() +{ + if (update) { + return; + } + + scaleLine(); +} + +/** + * This routine handles toggle events for buttons in the stroke style dialog. + * + * When activated, this routine gets the data for the various widgets, and then + * calls the respective routines to update css properties, etc. + * + */ +void StrokeStyle::buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw) +{ + if (spw->update) { + return; + } + + if (tb->get_active()) { + if (tb->get_button_type() == STROKE_STYLE_BUTTON_JOIN) { + spw->miterLimitSpin->set_sensitive(!strcmp(tb->get_stroke_style(), "miter")); + } + + /* TODO: Create some standardized method */ + SPCSSAttr *css = sp_repr_css_attr_new(); + + switch (tb->get_button_type()) { + case STROKE_STYLE_BUTTON_JOIN: + sp_repr_css_set_property(css, "stroke-linejoin", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setJoinButtons(tb); + break; + case STROKE_STYLE_BUTTON_CAP: + sp_repr_css_set_property(css, "stroke-linecap", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + spw->setCapButtons(tb); + break; + case STROKE_STYLE_BUTTON_ORDER: + sp_repr_css_set_property(css, "paint-order", tb->get_stroke_style()); + sp_desktop_set_style (spw->desktop, css); + //spw->setPaintButtons(tb); + } + + sp_repr_css_attr_unref(css); + css = nullptr; + + DocumentUndo::done(spw->desktop->getDocument(), SP_VERB_DIALOG_FILL_STROKE, _("Set stroke style")); + } +} + +/** + * Updates the join style toggle buttons + */ +void +StrokeStyle::setJoinButtons(Gtk::ToggleButton *active) +{ + joinMiter->set_active(active == joinMiter); + miterLimitSpin->set_sensitive(active == joinMiter); + joinRound->set_active(active == joinRound); + joinBevel->set_active(active == joinBevel); +} + +/** + * Updates the cap style toggle buttons + */ +void +StrokeStyle::setCapButtons(Gtk::ToggleButton *active) +{ + capButt->set_active(active == capButt); + capRound->set_active(active == capRound); + capSquare->set_active(active == capSquare); +} + + +/** + * Updates the paint order style toggle buttons + */ +void +StrokeStyle::setPaintOrderButtons(Gtk::ToggleButton *active) +{ + paintOrderFSM->set_active(active == paintOrderFSM); + paintOrderSFM->set_active(active == paintOrderSFM); + paintOrderFMS->set_active(active == paintOrderFMS); + paintOrderMFS->set_active(active == paintOrderMFS); + paintOrderSMF->set_active(active == paintOrderSMF); + paintOrderMSF->set_active(active == paintOrderMSF); +} + + +/** + * Recursively builds a simple list from an arbitrarily complex selection + * of items and grouped items + */ +static void buildGroupedItemList(SPObject *element, std::vector<SPObject*> &simple_list) +{ + if (SP_IS_GROUP(element)) { + for (SPObject *i = element->firstChild(); i; i = i->getNext()) { + buildGroupedItemList(i, simple_list); + } + } else { + simple_list.push_back(element); + } +} + + +/** + * Updates the marker combobox to highlight the appropriate marker and scroll to + * that marker. + */ +void +StrokeStyle::updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo) +{ + struct { MarkerComboBox *key; int loc; } const keyloc[] = { + { startMarkerCombo, SP_MARKER_LOC_START }, + { midMarkerCombo, SP_MARKER_LOC_MID }, + { endMarkerCombo, SP_MARKER_LOC_END } + }; + + bool all_texts = true; + + auto simplified_list = std::vector<SPObject *>(); + for (SPItem *item : objects) { + buildGroupedItemList(item, simplified_list); + } + + for (SPObject *object : simplified_list) { + if (!SP_IS_TEXT(object)) { + all_texts = false; + break; + } + } + + // We show markers of the last object in the list only + // FIXME: use the first in the list that has the marker of each type, if any + + // -1 means prefs haven't been queried yet + int update = -1; + + for (auto const &markertype : keyloc) { + // For all three marker types, + + // find the corresponding combobox item + MarkerComboBox *combo = markertype.key; + + // Quit if we're in update state + if (combo->update()) { + return; + } + + // Per SVG spec, text objects cannot have markers; disable combobox if only texts are selected + combo->set_sensitive(!all_texts); + + SPObject *marker = nullptr; + + if (!all_texts) { + for (SPObject *object : simplified_list) { + char const *value = object->style->marker_ptrs[markertype.loc]->value(); + + // If the object has this type of markers, + if (value == nullptr) + continue; + + // Extract the name of the marker that the object uses + marker = getMarkerObj(value, object->document); + + // Set the marker color + if (update < 0) { + // query prefs (only once) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + update = prefs->getBool("/options/markers/colorUpdateMarkers", true) ? 1 : 0; + } + + if (update > 0) { + setMarkerColor(marker, markertype.loc, SP_ITEM(object)); + + if (!skip_undo) { + SPDocument *document = desktop->getDocument(); + DocumentUndo::maybeDone(document, "UaM", SP_VERB_DIALOG_FILL_STROKE, + _("Set marker color")); + } + } + } + } + + // Scroll the combobox to that marker + combo->set_current(marker); + } + +} + + + +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/stroke-style.h b/src/widgets/stroke-style.h new file mode 100644 index 0000000..d11c1ae --- /dev/null +++ b/src/widgets/stroke-style.h @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Widgets used in the stroke style dialog. + */ +/* Author: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2010 Jon A. Cruz + * Copyright (C) 2001 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +// WHOA! talk about header bloat! + +#ifndef SEEN_DIALOGS_STROKE_STYLE_H +#define SEEN_DIALOGS_STROKE_STYLE_H + +#include <glibmm/i18n.h> +#include <gtkmm/grid.h> +#include <gtkmm/radiobutton.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "fill-n-stroke-factory.h" +#include "fill-style.h" // to get sp_fill_style_widget_set_desktop +#include "gradient-chemistry.h" + +#include "inkscape.h" +#include "io/sys.h" +#include "path-prefix.h" +#include "preferences.h" +#include "selection.h" +#include "verbs.h" + +#include "display/canvas-bpath.h" // for SP_STROKE_LINEJOIN_* +#include "display/drawing.h" + +#include "helper/stock-items.h" + +#include "style.h" + +#include "svg/css-ostringstream.h" + +#include "ui/cache/svg_preview_cache.h" +#include "ui/dialog-events.h" +#include "ui/icon-names.h" +#include "ui/widget/spinbutton.h" + +#include "widgets/paint-selector.h" +#include "widgets/spw-utilities.h" +#include "widgets/stroke-marker-selector.h" + +#include "xml/repr.h" + +namespace Gtk { +class Widget; +class Container; +} + +namespace Inkscape { + namespace Util { + class Unit; + } + namespace UI { + namespace Widget { + class DashSelector; + class UnitMenu; + } + } +} + +struct { gchar const *key; gint value; } const SPMarkerNames[] = { + {"marker-all", SP_MARKER_LOC}, + {"marker-start", SP_MARKER_LOC_START}, + {"marker-mid", SP_MARKER_LOC_MID}, + {"marker-end", SP_MARKER_LOC_END}, + {"", SP_MARKER_LOC_QTY}, + {nullptr, -1} +}; + +/** + * Creates an instance of a paint style widget. + */ +Gtk::Widget *sp_stroke_style_paint_widget_new(); + +/** + * Creates an instance of a line style widget. + */ +Gtk::Widget *sp_stroke_style_line_widget_new(); + +/** + * Switches a line or paint style widget to track the given desktop. + */ +void sp_stroke_style_widget_set_desktop(Gtk::Widget *widget, SPDesktop *desktop); + +SPObject *getMarkerObj(gchar const *n, SPDocument *doc); + +namespace Inkscape { +class StrokeStyleButton; + +class StrokeStyle : public Gtk::Box +{ +public: + StrokeStyle(); + ~StrokeStyle() override; + void setDesktop(SPDesktop *desktop); + +private: + /** List of valid types for the stroke-style radio-button widget */ + enum StrokeStyleButtonType { + STROKE_STYLE_BUTTON_JOIN, ///< A button to set the line-join style + STROKE_STYLE_BUTTON_CAP, ///< A button to set the line-cap style + STROKE_STYLE_BUTTON_ORDER ///< A button to set the paint-order style + }; + + /** + * A custom radio-button for setting the stroke style. It can be configured + * to set either the join or cap style by setting the button_type field. + */ + class StrokeStyleButton : public Gtk::RadioButton { + public: + StrokeStyleButton(Gtk::RadioButtonGroup &grp, + char const *icon, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + /** Get the type (line/cap) of the stroke-style button */ + inline StrokeStyleButtonType get_button_type() {return button_type;} + + /** Get the stroke style attribute associated with the button */ + inline gchar const * get_stroke_style() {return stroke_style;} + + private: + StrokeStyleButtonType button_type; ///< The type (line/cap) of the button + gchar const *stroke_style; ///< The stroke style associated with the button + }; + + void updateLine(); + void updateAllMarkers(std::vector<SPItem*> const &objects, bool skip_undo = false); + void updateMarkerHist(SPMarkerLoc const which); + void setDashSelectorFromStyle(Inkscape::UI::Widget::DashSelector *dsel, SPStyle *style); + void setJoinType (unsigned const jointype); + void setCapType (unsigned const captype); + void setPaintOrder (gchar const *paint_order); + void setJoinButtons(Gtk::ToggleButton *active); + void setCapButtons(Gtk::ToggleButton *active); + void setPaintOrderButtons(Gtk::ToggleButton *active); + void scaleLine(); + void setScaledDash(SPCSSAttr *css, int ndash, double *dash, double offset, double scale); + void setMarkerColor(SPObject *marker, int loc, SPItem *item); + SPObject *forkMarker(SPObject *marker, int loc, SPItem *item); + const char *getItemColorForMarker(SPItem *item, Inkscape::PaintTarget fill_or_stroke, int loc); + + StrokeStyleButton * makeRadioButton(Gtk::RadioButtonGroup &grp, + char const *icon, + Gtk::HBox *hb, + StrokeStyleButtonType button_type, + gchar const *stroke_style); + + // Callback functions + void selectionModifiedCB(guint flags); + void selectionChangedCB(); + void widthChangedCB(); + void miterLimitChangedCB(); + void lineDashChangedCB(); + void unitChangedCB(); + bool shouldMarkersBeUpdated(); + static void markerSelectCB(MarkerComboBox *marker_combo, StrokeStyle *spw, SPMarkerLoc const which); + static void buttonToggledCB(StrokeStyleButton *tb, StrokeStyle *spw); + + + MarkerComboBox *startMarkerCombo; + MarkerComboBox *midMarkerCombo; + MarkerComboBox *endMarkerCombo; + Gtk::Grid *table; + Glib::RefPtr<Gtk::Adjustment> *widthAdj; + Glib::RefPtr<Gtk::Adjustment> *miterLimitAdj; + Inkscape::UI::Widget::SpinButton *miterLimitSpin; + Inkscape::UI::Widget::SpinButton *widthSpin; + Inkscape::UI::Widget::UnitMenu *unitSelector; + StrokeStyleButton *joinMiter; + StrokeStyleButton *joinRound; + StrokeStyleButton *joinBevel; + StrokeStyleButton *capButt; + StrokeStyleButton *capRound; + StrokeStyleButton *capSquare; + StrokeStyleButton *paintOrderFSM; + StrokeStyleButton *paintOrderSFM; + StrokeStyleButton *paintOrderFMS; + StrokeStyleButton *paintOrderMFS; + StrokeStyleButton *paintOrderSMF; + StrokeStyleButton *paintOrderMSF; + Inkscape::UI::Widget::DashSelector *dashSelector; + + gboolean update; + SPDesktop *desktop; + sigc::connection selectChangedConn; + sigc::connection selectModifiedConn; + sigc::connection startMarkerConn; + sigc::connection midMarkerConn; + sigc::connection endMarkerConn; + sigc::connection unitChangedConn; + + Inkscape::Util::Unit const *_old_unit; + + void _handleDocumentReplaced(SPDesktop *, SPDocument *); + sigc::connection _document_replaced_connection; +}; + +} // namespace Inkscape + +#endif // SEEN_DIALOGS_STROKE_STYLE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/style-utils.h b/src/widgets/style-utils.h new file mode 100644 index 0000000..1557218 --- /dev/null +++ b/src/widgets/style-utils.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Common utility functions for manipulating style. + *//* + * Authors: + * see git history + * Shlomi Fish <shlomif@cpan.org> + * + * Copyright (C) 2016 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_DIALOGS_STYLE_UTILS_H +#define SEEN_DIALOGS_STYLE_UTILS_H + +#include "desktop-style.h" + +namespace Inkscape { + inline bool is_query_style_updateable(const int style) { + return (style == QUERY_STYLE_MULTIPLE_DIFFERENT || style == QUERY_STYLE_NOTHING); + } +} // namespace Inkscape + +#endif // SEEN_DIALOGS_STYLE_UTILS_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/swatch-selector.cpp b/src/widgets/swatch-selector.cpp new file mode 100644 index 0000000..aeead1c --- /dev/null +++ b/src/widgets/swatch-selector.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "swatch-selector.h" + +#include <glibmm/i18n.h> + +#include "document-undo.h" +#include "document.h" +#include "gradient-chemistry.h" +#include "gradient-selector.h" +#include "verbs.h" + +#include "object/sp-stop.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "ui/widget/color-notebook.h" + +#include "xml/node.h" + +namespace Inkscape +{ +namespace Widgets +{ + +SwatchSelector::SwatchSelector() : + Gtk::VBox(), + _gsel(nullptr), + _updating_color(false) +{ + using Inkscape::UI::Widget::ColorNotebook; + + GtkWidget *gsel = sp_gradient_selector_new(); + _gsel = SP_GRADIENT_SELECTOR(gsel); + g_object_set_data( G_OBJECT(gobj()), "base", this ); + _gsel->setMode(SPGradientSelector::MODE_SWATCH); + + gtk_widget_show(gsel); + + pack_start(*Gtk::manage(Glib::wrap(gsel))); + + Gtk::Widget *color_selector = Gtk::manage(new ColorNotebook(_selected_color)); + color_selector->show(); + pack_start(*color_selector); + + //_selected_color.signal_grabbed.connect(sigc::mem_fun(this, &SwatchSelector::_grabbedCb)); + _selected_color.signal_dragged.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + _selected_color.signal_released.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); + // signal_changed doesn't get called if updating shape with colour. + //_selected_color.signal_changed.connect(sigc::mem_fun(this, &SwatchSelector::_changedCb)); +} + +SwatchSelector::~SwatchSelector() +{ + _gsel = nullptr; +} + +SPGradientSelector *SwatchSelector::getGradientSelector() +{ + return _gsel; +} + +void SwatchSelector::_changedCb() +{ + if (_updating_color) { + return; + } + // TODO might have to block cycles + + if (_gsel && _gsel->getVector()) { + SPGradient *gradient = _gsel->getVector(); + SPGradient *ngr = sp_gradient_ensure_vector_normalized(gradient); + if (ngr != gradient) { + /* Our master gradient has changed */ + // TODO replace with proper - sp_gradient_vector_widget_load_gradient(GTK_WIDGET(swsel->_gsel), ngr); + } + + ngr->ensureVector(); + + + SPStop* stop = ngr->getFirstStop(); + if (stop) { + SPColor color = _selected_color.color(); + gfloat alpha = _selected_color.alpha(); + guint32 rgb = color.toRGBA32( 0x00 ); + + // TODO replace with generic shared code that also handles icc-color + Inkscape::CSSOStringStream os; + gchar c[64]; + sp_svg_write_color(c, sizeof(c), rgb); + os << "stop-color:" << c << ";stop-opacity:" << static_cast<gdouble>(alpha) <<";"; + stop->setAttribute("style", os.str()); + + DocumentUndo::done(ngr->document, SP_VERB_CONTEXT_GRADIENT, + _("Change swatch color")); + } + } +} + +void SwatchSelector::connectGrabbedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "grabbed", handler, data ); +} + +void SwatchSelector::connectDraggedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "dragged", handler, data ); +} + +void SwatchSelector::connectReleasedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "released", handler, data ); +} + +void SwatchSelector::connectchangedHandler( GCallback handler, void *data ) +{ + GObject* obj = G_OBJECT(_gsel); + g_signal_connect( obj, "changed", handler, data ); +} + +void SwatchSelector::setVector(SPDocument */*doc*/, SPGradient *vector) +{ + //GtkVBox * box = gobj(); + _gsel->setVector((vector) ? vector->document : nullptr, vector); + + if ( vector && vector->isSolid() ) { + SPStop* stop = vector->getFirstStop(); + + guint32 const colorVal = stop->get_rgba32(); + _updating_color = true; + _selected_color.setValue(colorVal); + _updating_color = false; + // gtk_widget_show_all( GTK_WIDGET(_csel) ); + } else { + //gtk_widget_hide( GTK_WIDGET(_csel) ); + } + +/* +*/ +} + +} // namespace Widgets +} // namespace Inkscape + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/swatch-selector.h b/src/widgets/swatch-selector.h new file mode 100644 index 0000000..88e7ad6 --- /dev/null +++ b/src/widgets/swatch-selector.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_SWATCH_SELECTOR_H +#define SEEN_SP_SWATCH_SELECTOR_H + +#include <gtkmm/box.h> +#include "ui/selected-color.h" + +class SPDocument; +class SPGradient; +struct SPColorSelector; +struct SPGradientSelector; + +namespace Inkscape +{ +namespace Widgets +{ + +class SwatchSelector : public Gtk::VBox +{ +public: + SwatchSelector(); + ~SwatchSelector() override; + + void connectGrabbedHandler( GCallback handler, void *data ); + void connectDraggedHandler( GCallback handler, void *data ); + void connectReleasedHandler( GCallback handler, void *data ); + void connectchangedHandler( GCallback handler, void *data ); + + void setVector(SPDocument *doc, SPGradient *vector); + + SPGradientSelector *getGradientSelector(); + +private: + void _grabbedCb(); + void _draggedCb(); + void _releasedCb(); + void _changedCb(); + + SPGradientSelector *_gsel; + Inkscape::UI::SelectedColor _selected_color; + bool _updating_color; +}; + + +} // namespace Widgets +} // namespace Inkscape + +#endif // SEEN_SP_SWATCH_SELECTOR_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/widgets/toolbox.cpp b/src/widgets/toolbox.cpp new file mode 100644 index 0000000..6248dec --- /dev/null +++ b/src/widgets/toolbox.cpp @@ -0,0 +1,842 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Inkscape toolbar definitions and general utility functions. + * Each tool should have its own xxx-toolbar implementation file + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/action.h> +#include <gtkmm/actiongroup.h> +#include <gtkmm/toolitem.h> +#include <glibmm/i18n.h> + +#include "desktop-style.h" +#include "desktop.h" +#include "document-undo.h" +#include "inkscape.h" +#include "verbs.h" + +#include "ink-action.h" + +#include "helper/action.h" +#include "helper/verb-action.h" + +#include "include/gtkmm_version.h" + +#include "io/resource.h" + +#include "object/sp-namedview.h" + +#include "ui/icon-names.h" +#include "ui/tools-switch.h" +#include "ui/uxmanager.h" +#include "ui/widget/button.h" +#include "ui/widget/spinbutton.h" +#include "ui/widget/style-swatch.h" +#include "ui/widget/unit-tracker.h" + +#include "widgets/spinbutton-events.h" +#include "widgets/spw-utilities.h" +#include "widgets/widget-sizes.h" + +#include "xml/attribute-record.h" +#include "xml/node-event-vector.h" + +#include "ui/toolbar/arc-toolbar.h" +#include "ui/toolbar/box3d-toolbar.h" +#include "ui/toolbar/calligraphy-toolbar.h" +#include "ui/toolbar/connector-toolbar.h" +#include "ui/toolbar/dropper-toolbar.h" +#include "ui/toolbar/eraser-toolbar.h" +#include "ui/toolbar/gradient-toolbar.h" +#include "ui/toolbar/lpe-toolbar.h" +#include "ui/toolbar/mesh-toolbar.h" +#include "ui/toolbar/measure-toolbar.h" +#include "ui/toolbar/node-toolbar.h" +#include "ui/toolbar/rect-toolbar.h" +#include "ui/toolbar/paintbucket-toolbar.h" +#include "ui/toolbar/pencil-toolbar.h" +#include "ui/toolbar/select-toolbar.h" +#include "ui/toolbar/snap-toolbar.h" +#include "ui/toolbar/spray-toolbar.h" +#include "ui/toolbar/spiral-toolbar.h" +#include "ui/toolbar/star-toolbar.h" +#include "ui/toolbar/tweak-toolbar.h" +#include "ui/toolbar/text-toolbar.h" +#include "ui/toolbar/zoom-toolbar.h" + +#include "toolbox.h" + +#include "ui/tools/tool-base.h" + +//#define DEBUG_TEXT + +using Inkscape::UI::UXManager; +using Inkscape::DocumentUndo; +using Inkscape::UI::ToolboxFactory; +using Inkscape::UI::Tools::ToolBase; + +using Inkscape::IO::Resource::get_filename; +using Inkscape::IO::Resource::UIS; + +typedef void (*SetupFunction)(GtkWidget *toolbox, SPDesktop *desktop); +typedef void (*UpdateFunction)(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +enum BarId { + BAR_TOOL = 0, + BAR_AUX, + BAR_COMMANDS, + BAR_SNAP, +}; + +#define BAR_ID_KEY "BarIdValue" +#define HANDLE_POS_MARK "x-inkscape-pos" + +GtkIconSize ToolboxFactory::prefToSize( Glib::ustring const &path, int base ) { + static GtkIconSize sizeChoices[] = { + GTK_ICON_SIZE_LARGE_TOOLBAR, + GTK_ICON_SIZE_SMALL_TOOLBAR, + GTK_ICON_SIZE_DND, + GTK_ICON_SIZE_DIALOG + }; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int index = prefs->getIntLimited( path, base, 0, G_N_ELEMENTS(sizeChoices) ); + return sizeChoices[index]; +} + +Gtk::IconSize ToolboxFactory::prefToSize_mm(Glib::ustring const &path, int base) +{ + static Gtk::IconSize sizeChoices[] = { Gtk::ICON_SIZE_LARGE_TOOLBAR, Gtk::ICON_SIZE_SMALL_TOOLBAR, + Gtk::ICON_SIZE_DND, Gtk::ICON_SIZE_DIALOG }; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int index = prefs->getIntLimited(path, base, 0, G_N_ELEMENTS(sizeChoices)); + return sizeChoices[index]; +} + +static struct { + gchar const *type_name; + gchar const *data_name; + sp_verb_t verb; + sp_verb_t doubleclick_verb; +} const tools[] = { + { "/tools/select", "select_tool", SP_VERB_CONTEXT_SELECT, SP_VERB_CONTEXT_SELECT_PREFS}, + { "/tools/nodes", "node_tool", SP_VERB_CONTEXT_NODE, SP_VERB_CONTEXT_NODE_PREFS }, + { "/tools/tweak", "tweak_tool", SP_VERB_CONTEXT_TWEAK, SP_VERB_CONTEXT_TWEAK_PREFS }, + { "/tools/spray", "spray_tool", SP_VERB_CONTEXT_SPRAY, SP_VERB_CONTEXT_SPRAY_PREFS }, + { "/tools/zoom", "zoom_tool", SP_VERB_CONTEXT_ZOOM, SP_VERB_CONTEXT_ZOOM_PREFS }, + { "/tools/measure", "measure_tool", SP_VERB_CONTEXT_MEASURE, SP_VERB_CONTEXT_MEASURE_PREFS }, + { "/tools/shapes/rect", "rect_tool", SP_VERB_CONTEXT_RECT, SP_VERB_CONTEXT_RECT_PREFS }, + { "/tools/shapes/3dbox", "3dbox_tool", SP_VERB_CONTEXT_3DBOX, SP_VERB_CONTEXT_3DBOX_PREFS }, + { "/tools/shapes/arc", "arc_tool", SP_VERB_CONTEXT_ARC, SP_VERB_CONTEXT_ARC_PREFS }, + { "/tools/shapes/star", "star_tool", SP_VERB_CONTEXT_STAR, SP_VERB_CONTEXT_STAR_PREFS }, + { "/tools/shapes/spiral", "spiral_tool", SP_VERB_CONTEXT_SPIRAL, SP_VERB_CONTEXT_SPIRAL_PREFS }, + { "/tools/freehand/pencil", "pencil_tool", SP_VERB_CONTEXT_PENCIL, SP_VERB_CONTEXT_PENCIL_PREFS }, + { "/tools/freehand/pen", "pen_tool", SP_VERB_CONTEXT_PEN, SP_VERB_CONTEXT_PEN_PREFS }, + { "/tools/calligraphic", "dyna_draw_tool", SP_VERB_CONTEXT_CALLIGRAPHIC, SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS }, + { "/tools/lpetool", "lpetool_tool", SP_VERB_CONTEXT_LPETOOL, SP_VERB_CONTEXT_LPETOOL_PREFS }, + { "/tools/eraser", "eraser_tool", SP_VERB_CONTEXT_ERASER, SP_VERB_CONTEXT_ERASER_PREFS }, + { "/tools/paintbucket", "paintbucket_tool", SP_VERB_CONTEXT_PAINTBUCKET, SP_VERB_CONTEXT_PAINTBUCKET_PREFS }, + { "/tools/text", "text_tool", SP_VERB_CONTEXT_TEXT, SP_VERB_CONTEXT_TEXT_PREFS }, + { "/tools/connector","connector_tool", SP_VERB_CONTEXT_CONNECTOR, SP_VERB_CONTEXT_CONNECTOR_PREFS }, + { "/tools/gradient", "gradient_tool", SP_VERB_CONTEXT_GRADIENT, SP_VERB_CONTEXT_GRADIENT_PREFS }, + { "/tools/mesh", "mesh_tool", SP_VERB_CONTEXT_MESH, SP_VERB_CONTEXT_MESH_PREFS }, + { "/tools/dropper", "dropper_tool", SP_VERB_CONTEXT_DROPPER, SP_VERB_CONTEXT_DROPPER_PREFS }, + { nullptr, nullptr, 0, 0 } +}; + +static struct { + gchar const *type_name; + gchar const *data_name; + GtkWidget *(*create_func)(SPDesktop *desktop); + gchar const *ui_name; + gint swatch_verb_id; + gchar const *swatch_tool; + gchar const *swatch_tip; +} const aux_toolboxes[] = { + { "/tools/select", "select_toolbox", Inkscape::UI::Toolbar::SelectToolbar::create, "SelectToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/nodes", "node_toolbox", Inkscape::UI::Toolbar::NodeToolbar::create, "NodeToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/tweak", "tweak_toolbox", Inkscape::UI::Toolbar::TweakToolbar::create, "TweakToolbar", + SP_VERB_CONTEXT_TWEAK_PREFS, "/tools/tweak", N_("Color/opacity used for color tweaking")}, + { "/tools/spray", "spray_toolbox", Inkscape::UI::Toolbar::SprayToolbar::create, "SprayToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/zoom", "zoom_toolbox", Inkscape::UI::Toolbar::ZoomToolbar::create, "ZoomToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + // If you change MeasureToolbar here, change it also in desktop-widget.cpp + { "/tools/measure", "measure_toolbox", Inkscape::UI::Toolbar::MeasureToolbar::create, "MeasureToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/shapes/star", "star_toolbox", Inkscape::UI::Toolbar::StarToolbar::create, "StarToolbar", + SP_VERB_CONTEXT_STAR_PREFS, "/tools/shapes/star", N_("Style of new stars")}, + { "/tools/shapes/rect", "rect_toolbox", Inkscape::UI::Toolbar::RectToolbar::create, "RectToolbar", + SP_VERB_CONTEXT_RECT_PREFS, "/tools/shapes/rect", N_("Style of new rectangles")}, + { "/tools/shapes/3dbox", "3dbox_toolbox", Inkscape::UI::Toolbar::Box3DToolbar::create, "3DBoxToolbar", + SP_VERB_CONTEXT_3DBOX_PREFS, "/tools/shapes/3dbox", N_("Style of new 3D boxes")}, + { "/tools/shapes/arc", "arc_toolbox", Inkscape::UI::Toolbar::ArcToolbar::create, "ArcToolbar", + SP_VERB_CONTEXT_ARC_PREFS, "/tools/shapes/arc", N_("Style of new ellipses")}, + { "/tools/shapes/spiral", "spiral_toolbox", Inkscape::UI::Toolbar::SpiralToolbar::create, "SpiralToolbar", + SP_VERB_CONTEXT_SPIRAL_PREFS, "/tools/shapes/spiral", N_("Style of new spirals")}, + { "/tools/freehand/pencil", "pencil_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pencil, "PencilToolbar", + SP_VERB_CONTEXT_PENCIL_PREFS, "/tools/freehand/pencil", N_("Style of new paths created by Pencil")}, + { "/tools/freehand/pen", "pen_toolbox", Inkscape::UI::Toolbar::PencilToolbar::create_pen, "PenToolbar", + SP_VERB_CONTEXT_PEN_PREFS, "/tools/freehand/pen", N_("Style of new paths created by Pen")}, + { "/tools/calligraphic", "calligraphy_toolbox", Inkscape::UI::Toolbar::CalligraphyToolbar::create, "CalligraphyToolbar", + SP_VERB_CONTEXT_CALLIGRAPHIC_PREFS, "/tools/calligraphic", N_("Style of new calligraphic strokes")}, + { "/tools/eraser", "eraser_toolbox", Inkscape::UI::Toolbar::EraserToolbar::create, "EraserToolbar", + SP_VERB_CONTEXT_ERASER_PREFS, "/tools/eraser", _("TBD")}, + { "/tools/lpetool", "lpetool_toolbox", Inkscape::UI::Toolbar::LPEToolbar::create, "LPEToolToolbar", + SP_VERB_CONTEXT_LPETOOL_PREFS, "/tools/lpetool", _("TBD")}, + // If you change TextToolbar here, change it also in desktop-widget.cpp + { "/tools/text", "text_toolbox", Inkscape::UI::Toolbar::TextToolbar::create, "TextToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/dropper", "dropper_toolbox", Inkscape::UI::Toolbar::DropperToolbar::create, "DropperToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/connector", "connector_toolbox", Inkscape::UI::Toolbar::ConnectorToolbar::create, "ConnectorToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/gradient", "gradient_toolbox", Inkscape::UI::Toolbar::GradientToolbar::create, "GradientToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/mesh", "mesh_toolbox", Inkscape::UI::Toolbar::MeshToolbar::create, "MeshToolbar", + SP_VERB_INVALID, nullptr, nullptr}, + { "/tools/paintbucket", "paintbucket_toolbox", Inkscape::UI::Toolbar::PaintbucketToolbar::create, "PaintbucketToolbar", + SP_VERB_CONTEXT_PAINTBUCKET_PREFS, "/tools/paintbucket", N_("Style of Paint Bucket fill objects")}, + { nullptr, nullptr, nullptr, nullptr, + SP_VERB_INVALID, nullptr, nullptr } +}; + + +static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop ); + +static void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop); + +static void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_tool_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_aux_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop); +static void update_commands_toolbox(SPDesktop *desktop, ToolBase *eventcontext, GtkWidget *toolbox); + +static void trigger_sp_action( GtkAction* /*act*/, gpointer user_data ) +{ + SPAction* targetAction = SP_ACTION(user_data); + if ( targetAction ) { + sp_action_perform( targetAction, nullptr ); + } +} + +static GtkAction* create_action_for_verb( Inkscape::Verb* verb, Inkscape::UI::View::View* view, GtkIconSize size ) +{ + GtkAction* act = nullptr; + + SPAction* targetAction = verb->get_action(Inkscape::ActionContext(view)); + InkAction* inky = ink_action_new( verb->get_id(), _(verb->get_name()), verb->get_tip(), verb->get_image(), size ); + act = GTK_ACTION(inky); + gtk_action_set_sensitive( act, targetAction->sensitive ); + + g_signal_connect( G_OBJECT(inky), "activate", G_CALLBACK(trigger_sp_action), targetAction ); + + // FIXME: memory leak: this is not unrefed anywhere + g_object_ref(G_OBJECT(targetAction)); + g_object_set_data_full(G_OBJECT(inky), "SPAction", (void*) targetAction, (GDestroyNotify) &g_object_unref); + targetAction->signal_set_sensitive.connect( + sigc::bind<0>( + sigc::ptr_fun(>k_action_set_sensitive), + GTK_ACTION(inky))); + + return act; +} + +static std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> > groups; + +static void desktopDestructHandler(SPDesktop *desktop) +{ + std::map<SPDesktop*, Glib::RefPtr<Gtk::ActionGroup> >::iterator it = groups.find(desktop); + if (it != groups.end()) + { + groups.erase(it); + } +} + +static Glib::RefPtr<Gtk::ActionGroup> create_or_fetch_actions( SPDesktop* desktop ) +{ + Inkscape::UI::View::View *view = desktop; + gint verbsToUse[] = { + // disabled until we have icons for them: + //find + //SP_VERB_EDIT_TILE, + //SP_VERB_EDIT_UNTILE, + SP_VERB_DIALOG_ALIGN_DISTRIBUTE, + SP_VERB_DIALOG_DISPLAY, + SP_VERB_DIALOG_FILL_STROKE, + SP_VERB_DIALOG_NAMEDVIEW, + SP_VERB_DIALOG_TEXT, + SP_VERB_DIALOG_XML_EDITOR, + SP_VERB_DIALOG_SELECTORS, + SP_VERB_DIALOG_LAYERS, + SP_VERB_EDIT_CLONE, + SP_VERB_EDIT_COPY, + SP_VERB_EDIT_CUT, + SP_VERB_EDIT_DUPLICATE, + SP_VERB_EDIT_PASTE, + SP_VERB_EDIT_REDO, + SP_VERB_EDIT_UNDO, + SP_VERB_EDIT_UNLINK_CLONE, + //SP_VERB_FILE_EXPORT, + SP_VERB_DIALOG_EXPORT, + SP_VERB_FILE_IMPORT, + SP_VERB_FILE_NEW, + SP_VERB_FILE_OPEN, + SP_VERB_FILE_PRINT, + SP_VERB_FILE_SAVE, + SP_VERB_OBJECT_TO_CURVE, + SP_VERB_SELECTION_GROUP, + SP_VERB_SELECTION_OUTLINE, + SP_VERB_SELECTION_UNGROUP, + SP_VERB_ZOOM_1_1, + SP_VERB_ZOOM_1_2, + SP_VERB_ZOOM_2_1, + SP_VERB_ZOOM_DRAWING, + SP_VERB_ZOOM_IN, + SP_VERB_ZOOM_NEXT, + SP_VERB_ZOOM_OUT, + SP_VERB_ZOOM_PAGE, + SP_VERB_ZOOM_PAGE_WIDTH, + SP_VERB_ZOOM_PREV, + SP_VERB_ZOOM_SELECTION, + SP_VERB_ZOOM_CENTER_PAGE + }; + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small"); + Glib::RefPtr<Gtk::ActionGroup> mainActions; + if (desktop == nullptr) + { + return mainActions; + } + + if ( groups.find(desktop) != groups.end() ) { + mainActions = groups[desktop]; + } + + if ( !mainActions ) { + mainActions = Gtk::ActionGroup::create("main"); + groups[desktop] = mainActions; + desktop->connectDestroy(&desktopDestructHandler); + } + + for (int i : verbsToUse) { + Inkscape::Verb* verb = Inkscape::Verb::get(i); + if ( verb ) { + if (!mainActions->get_action(verb->get_id())) { + GtkAction* act = create_action_for_verb( verb, view, toolboxSize ); + mainActions->add(Glib::wrap(act)); + } + } + } + + if ( !mainActions->get_action("ToolZoom") ) { + for ( guint i = 0; i < G_N_ELEMENTS(tools) && tools[i].type_name; i++ ) { + Glib::RefPtr<VerbAction> va = VerbAction::create(Inkscape::Verb::get(tools[i].verb), Inkscape::Verb::get(tools[i].doubleclick_verb), view); + if ( va ) { + mainActions->add(va); + if ( i == 0 ) { + va->set_active(true); + } + } else { + // This creates a blank action using the data_name, this can replace + // tools that have been disabled by compile time options. + Glib::RefPtr<Gtk::Action> act = Gtk::Action::create(Glib::ustring(tools[i].data_name)); + act->set_sensitive(false); + mainActions->add(act); + } + } + } + + return mainActions; +} + + +static GtkWidget* toolboxNewCommon( GtkWidget* tb, BarId id, GtkPositionType /*handlePos*/ ) +{ + g_object_set_data(G_OBJECT(tb), "desktop", nullptr); + + gtk_widget_set_sensitive(tb, FALSE); + + GtkWidget *hb = gtk_event_box_new(); // A simple, neutral container. + gtk_widget_set_name(hb, "ToolboxCommon"); + + gtk_container_add(GTK_CONTAINER(hb), tb); + gtk_widget_show(GTK_WIDGET(tb)); + + sigc::connection* conn = new sigc::connection; + g_object_set_data(G_OBJECT(hb), "event_context_connection", conn); + + gpointer val = GINT_TO_POINTER(id); + g_object_set_data(G_OBJECT(hb), BAR_ID_KEY, val); + + return hb; +} + +GtkWidget *ToolboxFactory::createToolToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "ToolToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_TOOL, GTK_POS_TOP ); +} + +GtkWidget *ToolboxFactory::createAuxToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "AuxToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_AUX, GTK_POS_LEFT ); +} + +//#################################### +//# Commands Bar +//#################################### + +GtkWidget *ToolboxFactory::createCommandsToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "CommandsToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_COMMANDS, GTK_POS_LEFT ); +} + +GtkWidget *ToolboxFactory::createSnapToolbox() +{ + auto tb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_name(tb, "SnapToolbox"); + gtk_box_set_homogeneous(GTK_BOX(tb), FALSE); + + return toolboxNewCommon( tb, BAR_SNAP, GTK_POS_LEFT ); +} + +static GtkWidget* createCustomSlider( GtkAdjustment *adjustment, gdouble climbRate, guint digits, Inkscape::UI::Widget::UnitTracker *unit_tracker) +{ + auto adj = Glib::wrap(adjustment, true); + auto inkSpinner = new Inkscape::UI::Widget::SpinButton(adj, climbRate, digits); + inkSpinner->addUnitTracker(unit_tracker); + inkSpinner = Gtk::manage( inkSpinner ); + GtkWidget *widget = GTK_WIDGET( inkSpinner->gobj() ); + return widget; +} + +void ToolboxFactory::setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop) +{ + sigc::connection *conn = static_cast<sigc::connection*>(g_object_get_data(G_OBJECT(toolbox), + "event_context_connection")); + + BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) ); + + SetupFunction setup_func = nullptr; + UpdateFunction update_func = nullptr; + + switch (id) { + case BAR_TOOL: + setup_func = setup_tool_toolbox; + update_func = update_tool_toolbox; + break; + + case BAR_AUX: + toolbox = gtk_bin_get_child(GTK_BIN(toolbox)); + setup_func = setup_aux_toolbox; + update_func = update_aux_toolbox; + break; + + case BAR_COMMANDS: + setup_func = setup_commands_toolbox; + update_func = update_commands_toolbox; + break; + + case BAR_SNAP: + setup_func = setup_snap_toolbox; + update_func = updateSnapToolbox; + break; + default: + g_warning("Unexpected toolbox id encountered."); + } + + gpointer ptr = g_object_get_data(G_OBJECT(toolbox), "desktop"); + SPDesktop *old_desktop = static_cast<SPDesktop*>(ptr); + + if (old_desktop) { + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(toolbox))->get_children(); + for ( auto i:children ) { + gtk_container_remove( GTK_CONTAINER(toolbox), i->gobj() ); + } + } + + g_object_set_data(G_OBJECT(toolbox), "desktop", (gpointer)desktop); + + if (desktop && setup_func && update_func) { + gtk_widget_set_sensitive(toolbox, TRUE); + setup_func(toolbox, desktop); + update_func(desktop, desktop->event_context, toolbox); + *conn = desktop->connectEventContextChanged(sigc::bind (sigc::ptr_fun(update_func), toolbox)); + } else { + gtk_widget_set_sensitive(toolbox, FALSE); + } + +} // end of sp_toolbox_set_desktop() + + +static void setupToolboxCommon( GtkWidget *toolbox, + SPDesktop *desktop, + gchar const *ui_file, + gchar const* toolbarName, + gchar const* sizePref ) +{ + Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop ); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + GtkUIManager* mgr = gtk_ui_manager_new(); + GError* err = nullptr; + + GtkOrientation orientation = GTK_ORIENTATION_HORIZONTAL; + + gtk_ui_manager_insert_action_group( mgr, mainActions->gobj(), 0 ); + + Glib::ustring filename = get_filename(UIS, ui_file); + gtk_ui_manager_add_ui_from_file( mgr, filename.c_str(), &err ); + if(err) { + g_warning("Failed to load %s: %s", filename.c_str(), err->message); + g_error_free(err); + return; + } + + GtkWidget* toolBar = gtk_ui_manager_get_widget( mgr, toolbarName ); + if ( prefs->getBool("/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) ); + + GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK ))); + orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL; + gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation); + gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE); + + g_object_set_data(G_OBJECT(toolBar), "desktop", nullptr); + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if ( child ) { + gtk_container_remove( GTK_CONTAINER(toolbox), child ); + } + + gtk_container_add( GTK_CONTAINER(toolbox), toolBar ); +} + +#define noDUMP_DETAILS 1 + +void ToolboxFactory::setOrientation(GtkWidget* toolbox, GtkOrientation orientation) +{ +#if DUMP_DETAILS + g_message("Set orientation for %p to be %d", toolbox, orientation); + GType type = G_OBJECT_TYPE(toolbox); + g_message(" [%s]", g_type_name(type)); + g_message(" %p", g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)); +#endif + + GtkPositionType pos = (orientation == GTK_ORIENTATION_HORIZONTAL) ? GTK_POS_LEFT : GTK_POS_TOP; + + if (GTK_IS_BIN(toolbox)) { +#if DUMP_DETAILS + g_message(" is a BIN"); +#endif // DUMP_DETAILS + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if (child) { +#if DUMP_DETAILS + GType type2 = G_OBJECT_TYPE(child); + g_message(" child [%s]", g_type_name(type2)); +#endif // DUMP_DETAILS + + if (GTK_IS_BOX(child)) { +#if DUMP_DETAILS + g_message(" is a BOX"); +#endif // DUMP_DETAILS + + std::vector<Gtk::Widget*> children = Glib::wrap(GTK_CONTAINER(child))->get_children(); + if (!children.empty()) { + for (auto curr:children) { + GtkWidget* child2 = curr->gobj(); +#if DUMP_DETAILS + GType type3 = G_OBJECT_TYPE(child2); + g_message(" child2 [%s]", g_type_name(type3)); +#endif // DUMP_DETAILS + + if (GTK_IS_CONTAINER(child2)) { + std::vector<Gtk::Widget*> children2 = Glib::wrap(GTK_CONTAINER(child2))->get_children(); + if (!children2.empty()) { + for (auto curr2:children2) { + GtkWidget* child3 = curr2->gobj(); +#if DUMP_DETAILS + GType type4 = G_OBJECT_TYPE(child3); + g_message(" child3 [%s]", g_type_name(type4)); +#endif // DUMP_DETAILS + if (GTK_IS_TOOLBAR(child3)) { + GtkToolbar* childBar = GTK_TOOLBAR(child3); + gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation); + } + } + } + } + + + if (GTK_IS_TOOLBAR(child2)) { + GtkToolbar* childBar = GTK_TOOLBAR(child2); + gtk_orientable_set_orientation(GTK_ORIENTABLE(childBar), orientation); + } else { + g_message("need to add dynamic switch"); + } + } + } else { + // The call is being made before the toolbox proper has been setup. + g_object_set_data(G_OBJECT(toolbox), HANDLE_POS_MARK, GINT_TO_POINTER(pos)); + } + } else if (GTK_IS_TOOLBAR(child)) { + GtkToolbar* toolbar = GTK_TOOLBAR(child); + gtk_orientable_set_orientation( GTK_ORIENTABLE(toolbar), orientation ); + } + } + } +} + +void setup_tool_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + setupToolboxCommon(toolbox, desktop, "toolbar-tool.ui", "/ui/ToolToolbar", "/toolbox/tools/small"); +} + +void update_tool_toolbox( SPDesktop *desktop, ToolBase *eventcontext, GtkWidget * /*toolbox*/ ) +{ + gchar const *const tname = ( eventcontext + ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext)) + : nullptr ); + Glib::RefPtr<Gtk::ActionGroup> mainActions = create_or_fetch_actions( desktop ); + + for (int i = 0 ; tools[i].type_name ; i++ ) { + Glib::RefPtr<Gtk::Action> act = mainActions->get_action( Inkscape::Verb::get(tools[i].verb)->get_id() ); + if ( act ) { + bool setActive = tname && !strcmp(tname, tools[i].type_name); + Glib::RefPtr<VerbAction> verbAct = Glib::RefPtr<VerbAction>::cast_dynamic(act); + if ( verbAct ) { + verbAct->set_active(setActive); + } + } + } +} + +/** + * \brief Generate the auxiliary toolbox + * + * \details This is the one that appears below the main menu, and contains + * tool-specific toolbars. Each toolbar is created here, using + * its "create" method. + * + * The actual method used for each toolbar is specified in the + * "aux_toolboxes" array, defined above. + */ +void setup_aux_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Loop through all the toolboxes and create them using either + // their "create" methods. + for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) { + if (aux_toolboxes[i].create_func) { + GtkWidget *sub_toolbox = aux_toolboxes[i].create_func(desktop); + gtk_widget_set_name( sub_toolbox, "SubToolBox" ); + + auto holder = gtk_grid_new(); + gtk_grid_attach(GTK_GRID(holder), sub_toolbox, 0, 0, 1, 1); + + // This part is just for styling + if ( prefs->getBool( "/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(sub_toolbox), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize("/toolbox/small"); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(sub_toolbox), static_cast<GtkIconSize>(toolboxSize) ); + gtk_widget_set_hexpand(sub_toolbox, TRUE); + + // Add a swatch widget if one was specified + if ( aux_toolboxes[i].swatch_verb_id != SP_VERB_INVALID ) { + auto swatch = new Inkscape::UI::Widget::StyleSwatch( nullptr, _(aux_toolboxes[i].swatch_tip) ); + swatch->setDesktop( desktop ); + swatch->setClickVerb( aux_toolboxes[i].swatch_verb_id ); + swatch->setWatchedTool( aux_toolboxes[i].swatch_tool, true ); + swatch->set_margin_start(AUX_BETWEEN_BUTTON_GROUPS); + swatch->set_margin_end(AUX_BETWEEN_BUTTON_GROUPS); + swatch->set_margin_top(AUX_SPACING); + swatch->set_margin_bottom(AUX_SPACING); + + auto swatch_ = GTK_WIDGET( swatch->gobj() ); + gtk_grid_attach( GTK_GRID(holder), swatch_, 1, 0, 1, 1); + } + + // Add the new toolbar into the toolbox (i.e., make it the visible toolbar) + // and also store a pointer to it inside the toolbox. This allows the + // active toolbar to be changed. + gtk_container_add(GTK_CONTAINER(toolbox), holder); + gtk_widget_set_name( holder, aux_toolboxes[i].ui_name ); + + // TODO: We could make the toolbox a custom subclass of GtkEventBox + // so that we can store a list of toolbars, rather than using + // GObject data + g_object_set_data(G_OBJECT(toolbox), aux_toolboxes[i].data_name, holder); + gtk_widget_show(sub_toolbox); + gtk_widget_show(holder); + } else if (aux_toolboxes[i].swatch_verb_id != SP_VERB_NONE) { + g_warning("Could not create toolbox %s", aux_toolboxes[i].ui_name); + } + } +} + +void update_aux_toolbox(SPDesktop * /*desktop*/, ToolBase *eventcontext, GtkWidget *toolbox) +{ + gchar const *tname = ( eventcontext + ? eventcontext->getPrefsPath().c_str() //g_type_name(G_OBJECT_TYPE(eventcontext)) + : nullptr ); + for (int i = 0 ; aux_toolboxes[i].type_name ; i++ ) { + GtkWidget *sub_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), aux_toolboxes[i].data_name)); + if (tname && !strcmp(tname, aux_toolboxes[i].type_name)) { + gtk_widget_show_now(sub_toolbox); + g_object_set_data(G_OBJECT(toolbox), "shows", sub_toolbox); + } else { + gtk_widget_hide(sub_toolbox); + } + //FIX issue #Inkscape686 + GtkAllocation allocation; + gtk_widget_get_allocation(sub_toolbox, &allocation); + gtk_widget_size_allocate(sub_toolbox, &allocation); + } + //FIX issue #Inkscape125 + GtkAllocation allocation; + gtk_widget_get_allocation(toolbox, &allocation); + gtk_widget_size_allocate(toolbox, &allocation); +} + +void setup_commands_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + setupToolboxCommon(toolbox, desktop, "toolbar-commands.ui", "/ui/CommandsToolbar", "/toolbox/small"); +} + +void update_commands_toolbox(SPDesktop * /*desktop*/, ToolBase * /*eventcontext*/, GtkWidget * /*toolbox*/) +{ +} + +void setup_snap_toolbox(GtkWidget *toolbox, SPDesktop *desktop) +{ + Glib::ustring sizePref("/toolbox/secondary"); + auto toolBar = Inkscape::UI::Toolbar::SnapToolbar::create(desktop); + auto prefs = Inkscape::Preferences::get(); + + if ( prefs->getBool("/toolbox/icononly", true) ) { + gtk_toolbar_set_style( GTK_TOOLBAR(toolBar), GTK_TOOLBAR_ICONS ); + } + + GtkIconSize toolboxSize = ToolboxFactory::prefToSize(sizePref.c_str()); + gtk_toolbar_set_icon_size( GTK_TOOLBAR(toolBar), static_cast<GtkIconSize>(toolboxSize) ); + + GtkPositionType pos = static_cast<GtkPositionType>(GPOINTER_TO_INT(g_object_get_data( G_OBJECT(toolbox), HANDLE_POS_MARK ))); + auto orientation = ((pos == GTK_POS_LEFT) || (pos == GTK_POS_RIGHT)) ? GTK_ORIENTATION_HORIZONTAL : GTK_ORIENTATION_VERTICAL; + gtk_orientable_set_orientation (GTK_ORIENTABLE(toolBar), orientation); + gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolBar), TRUE); + + GtkWidget* child = gtk_bin_get_child(GTK_BIN(toolbox)); + if ( child ) { + gtk_container_remove( GTK_CONTAINER(toolbox), child ); + } + + gtk_container_add( GTK_CONTAINER(toolbox), toolBar ); +} + +Glib::ustring ToolboxFactory::getToolboxName(GtkWidget* toolbox) +{ + Glib::ustring name; + BarId id = static_cast<BarId>( GPOINTER_TO_INT(g_object_get_data(G_OBJECT(toolbox), BAR_ID_KEY)) ); + switch(id) { + case BAR_TOOL: + name = "ToolToolbar"; + break; + case BAR_AUX: + name = "AuxToolbar"; + break; + case BAR_COMMANDS: + name = "CommandsToolbar"; + break; + case BAR_SNAP: + name = "SnapToolbar"; + break; + } + + return name; +} + +void ToolboxFactory::updateSnapToolbox(SPDesktop *desktop, ToolBase * /*eventcontext*/, GtkWidget *toolbox) +{ + auto tb = dynamic_cast<Inkscape::UI::Toolbar::SnapToolbar*>(Glib::wrap(GTK_TOOLBAR(gtk_bin_get_child(GTK_BIN(toolbox))))); + + if (!tb) { + std::cerr << "Can't get snap toolbar" << std::endl; + return; + } + + Inkscape::UI::Toolbar::SnapToolbar::update(tb); +} + +void ToolboxFactory::showAuxToolbox(GtkWidget *toolbox_toplevel) +{ + gtk_widget_show(toolbox_toplevel); + GtkWidget *toolbox = gtk_bin_get_child(GTK_BIN(toolbox_toplevel)); + + GtkWidget *shown_toolbox = GTK_WIDGET(g_object_get_data(G_OBJECT(toolbox), "shows")); + if (!shown_toolbox) { + return; + } + gtk_widget_show(toolbox); +} + +#define MODE_LABEL_WIDTH 70 + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/toolbox.h b/src/widgets/toolbox.h new file mode 100644 index 0000000..3a1bb4c --- /dev/null +++ b/src/widgets/toolbox.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_TOOLBOX_H +#define SEEN_TOOLBOX_H + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> + +#include "preferences.h" + +#define TOOLBAR_SLIDER_HINT "compact" + +class SPDesktop; + +namespace Inkscape { +namespace UI { +namespace Tools { + +class ToolBase; + +} +} +} + +namespace Inkscape { +namespace UI { + +namespace Widget { + class UnitTracker; +} + +/** + * Main toolbox source. + */ +class ToolboxFactory +{ +public: + static void setToolboxDesktop(GtkWidget *toolbox, SPDesktop *desktop); + static void setOrientation(GtkWidget* toolbox, GtkOrientation orientation); + static void showAuxToolbox(GtkWidget* toolbox); + + static GtkWidget *createToolToolbox(); + static GtkWidget *createAuxToolbox(); + static GtkWidget *createCommandsToolbox(); + static GtkWidget *createSnapToolbox(); + + + static Glib::ustring getToolboxName(GtkWidget* toolbox); + + static void updateSnapToolbox(SPDesktop *desktop, Inkscape::UI::Tools::ToolBase *eventcontext, GtkWidget *toolbox); + + static GtkIconSize prefToSize(Glib::ustring const &path, int base = 0 ); + static Gtk::IconSize prefToSize_mm(Glib::ustring const &path, int base = 0); + + ToolboxFactory() = delete; +}; + + + +} // namespace UI +} // namespace Inkscape + +#endif /* !SEEN_TOOLBOX_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/widgets/widget-sizes.h b/src/widgets/widget-sizes.h new file mode 100644 index 0000000..8dd8c6c --- /dev/null +++ b/src/widgets/widget-sizes.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2016 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +// #define TOOL_BUTTON_SIZE 28 + +// GTK uses 24 for icon sizes by default. Spacing adjust to keep the +// toolbar the same as other GTK applications. If we want that, use +// these defines instead: +//#define AUX_BUTTON_SIZE 24 +//#define AUX_SPACING 2 + +// #define AUX_BUTTON_SIZE 20 +#define AUX_SPACING 3 + +#define AUX_BETWEEN_BUTTON_GROUPS 7 +#define AUX_BETWEEN_SPINBUTTONS 0 +#define AUX_SPINBUTTON_WIDTH 62 +#define AUX_SPINBUTTON_WIDTH_SMALL 56 +#define AUX_SPINBUTTON_HEIGHT 20 +#define AUX_OPTION_MENU_WIDTH 55 +#define AUX_OPTION_MENU_HEIGHT 20 +#define AUX_MENU_ITEM_WIDTH 32 +#define AUX_MENU_ITEM_HEIGHT 18 + +#define SPIN_STEP 0.1 +#define SPIN_PAGE_STEP 5.0 + +#define BOTTOM_BAR_HEIGHT 20 +#define BOTTOM_BUTTON_SIZE 14 + +#define STATUS_BAR_FONT_SIZE 10000 + +#define STATUS_ZOOM_WIDTH 57 +#define STATUS_ROTATION_WIDTH 57 + +#define SELECTED_STYLE_SB_WIDTH 48 +#define SELECTED_STYLE_WIDTH 190 +#define STYLE_SWATCH_WIDTH 135 + +#define STATUS_LAYER_FONT_SIZE 7700 + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/CMakeLists.txt b/src/xml/CMakeLists.txt new file mode 100644 index 0000000..b5a4f18 --- /dev/null +++ b/src/xml/CMakeLists.txt @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(xml_SRC + composite-node-observer.cpp + croco-node-iface.cpp + event.cpp + log-builder.cpp + node-fns.cpp + quote.cpp + repr.cpp + repr-css.cpp + repr-io.cpp + repr-sorting.cpp + repr-util.cpp + simple-document.cpp + simple-node.cpp + subtree.cpp + helper-observer.cpp + rebase-hrefs.cpp + + + # ------- + # Headers + attribute-record.h + comment-node.h + composite-node-observer.h + croco-node-iface.h + document.h + element-node.h + event-fns.h + event.h + helper-observer.h + invalid-operation-exception.h + log-builder.h + node-event-vector.h + node-fns.h + node-iterators.h + node-observer.h + node.h + pi-node.h + quote-test.h + quote.h + rebase-hrefs.h + repr-action-test.h + repr-sorting.h + repr.h + simple-document.h + simple-node.h + sp-css-attr.h + subtree.h + text-node.h +) + +# add_inkscape_lib(xml_LIB "${xml_SRC}") +add_inkscape_source("${xml_SRC}") diff --git a/src/xml/README b/src/xml/README new file mode 100644 index 0000000..d46e7e0 --- /dev/null +++ b/src/xml/README @@ -0,0 +1,12 @@ + + +This directory contains code that handles the XML tree. + +Classes to store the parsed XML of an SVG document. Fairly generic, +and doesn't contain significant SVG-specific functionality. The main +distinguishing features (from something like libxml++) are +notifications about XML changes and undo functionality. This subsystem +is garbage-collected. Because XML nodes were formerly C structures +called SPRepr, the XML tree is sometimes called the "repr tree", and +XML nodes "reprs" (short for "representation"). + diff --git a/src/xml/attribute-record.h b/src/xml/attribute-record.h new file mode 100644 index 0000000..6808040 --- /dev/null +++ b/src/xml/attribute-record.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** @file + * @brief Key-value pair representing an attribute + */ + +#ifndef SEEN_XML_SP_REPR_ATTR_H +#define SEEN_XML_SP_REPR_ATTR_H + +#include <glib.h> +#include "inkgc/gc-managed.h" +#include "util/share.h" + +#define SP_REPR_ATTRIBUTE_KEY(a) g_quark_to_string((a)->key) +#define SP_REPR_ATTRIBUTE_VALUE(a) ((a)->value) + +namespace Inkscape { +namespace XML { + +/** + * @brief Key-value pair representing an attribute + * + * Internally, the attributes of each node in the XML tree are + * represented by this structure. + */ +struct AttributeRecord : public Inkscape::GC::Managed<> { + AttributeRecord(GQuark k, Inkscape::Util::ptr_shared v) + : key(k), value(v) {} + + /** @brief GQuark corresponding to the name of the attribute */ + GQuark key; + /** @brief Shared pointer to the value of the attribute */ + Inkscape::Util::ptr_shared value; + + // accept default copy constructor and assignment operator +}; + +} +} + +#endif /* !SEEN_XML_SP_REPR_ATTR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/xml/comment-node.h b/src/xml/comment-node.h new file mode 100644 index 0000000..3298152 --- /dev/null +++ b/src/xml/comment-node.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Comment node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef SEEN_INKSCAPE_XML_COMMENT_NODE_H +#define SEEN_INKSCAPE_XML_COMMENT_NODE_H + +#include <glib.h> +#include "xml/simple-node.h" + +namespace Inkscape { + +namespace XML { + +/** + * @brief Comment node, e.g. <!-- Some comment --> + */ +struct CommentNode : public SimpleNode { + CommentNode(Util::ptr_shared content, Document *doc) + : SimpleNode(g_quark_from_static_string("comment"), doc) + { + setContent(content); + } + + CommentNode(CommentNode const &other, Document *doc) + : SimpleNode(other, doc) {} + + Inkscape::XML::NodeType type() const override { return Inkscape::XML::COMMENT_NODE; } + +protected: + SimpleNode *_duplicate(Document* doc) const override { return new CommentNode(*this, doc); } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/composite-node-observer.cpp b/src/xml/composite-node-observer.cpp new file mode 100644 index 0000000..77a1bd2 --- /dev/null +++ b/src/xml/composite-node-observer.cpp @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * Inkscape::XML::CompositeNodeObserver - combine multiple observers + * + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * See the file COPYING for details. + * + */ + +#include <cstring> +#include <glib.h> + +#include "util/find-if-before.h" +#include "xml/composite-node-observer.h" +#include "xml/node-event-vector.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" + +namespace Inkscape { + +namespace XML { + +void CompositeNodeObserver::notifyChildAdded(Node &node, Node &child, Node *prev) +{ + _startIteration(); + for (auto & iter : _active) + { + if (!iter.marked) { + iter.observer.notifyChildAdded(node, child, prev); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::notifyChildRemoved(Node &node, Node &child, + Node *prev) +{ + _startIteration(); + for (auto & iter : _active) + { + if (!iter.marked) { + iter.observer.notifyChildRemoved(node, child, prev); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::notifyChildOrderChanged(Node &node, Node &child, + Node *old_prev, + Node *new_prev) +{ + _startIteration(); + for (auto & iter : _active) + { + if (!iter.marked) { + iter.observer.notifyChildOrderChanged(node, child, old_prev, new_prev); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::notifyContentChanged( + Node &node, + Util::ptr_shared old_content, Util::ptr_shared new_content +) { + _startIteration(); + for (auto & iter : _active) + { + if (!iter.marked) { + iter.observer.notifyContentChanged(node, old_content, new_content); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::notifyAttributeChanged( + Node &node, GQuark name, + Util::ptr_shared old_value, Util::ptr_shared new_value +) { + _startIteration(); + for (auto & iter : _active) + { + if (!iter.marked) { + iter.observer.notifyAttributeChanged(node, name, old_value, new_value); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) +{ + _startIteration(); + for (auto& iter : _active) { + if (!iter.marked) { + iter.observer.notifyElementNameChanged(node, old_name, new_name); + } + } + _finishIteration(); +} + +void CompositeNodeObserver::add(NodeObserver &observer) { + if (_iterating) { + _pending.push_back(ObserverRecord(observer)); + } else { + _active.push_back(ObserverRecord(observer)); + } +} + +namespace { + +class VectorNodeObserver : public NodeObserver, public GC::Managed<> { +public: + VectorNodeObserver(NodeEventVector const &v, void *d) + : vector(v), data(d) {} + + NodeEventVector const &vector; + void * const data; + + void notifyChildAdded(Node &node, Node &child, Node *prev) override { + if (vector.child_added) { + vector.child_added(&node, &child, prev, data); + } + } + + void notifyChildRemoved(Node &node, Node &child, Node *prev) override { + if (vector.child_removed) { + vector.child_removed(&node, &child, prev, data); + } + } + + void notifyChildOrderChanged(Node &node, Node &child, Node *old_prev, Node *new_prev) override { + if (vector.order_changed) { + vector.order_changed(&node, &child, old_prev, new_prev, data); + } + } + + void notifyContentChanged(Node &node, Util::ptr_shared old_content, Util::ptr_shared new_content) override { + if (vector.content_changed) { + vector.content_changed(&node, old_content, new_content, data); + } + } + + void notifyAttributeChanged(Node &node, GQuark name, Util::ptr_shared old_value, Util::ptr_shared new_value) override { + if (vector.attr_changed) { + vector.attr_changed(&node, g_quark_to_string(name), old_value, new_value, false, data); + } + } + + void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override { + if (vector.element_name_changed) { + vector.element_name_changed(&node, g_quark_to_string(old_name), g_quark_to_string(new_name), data); + } + } +}; + +} + +void CompositeNodeObserver::addListener(NodeEventVector const &vector, + void *data) +{ + Debug::EventTracker<Debug::SimpleEvent<Debug::Event::XML> > tracker("add-listener"); + add(*(new VectorNodeObserver(vector, data))); +} + +namespace { + +typedef CompositeNodeObserver::ObserverRecord ObserverRecord; +typedef CompositeNodeObserver::ObserverRecordList ObserverRecordList; + +template <typename ObserverPredicate> +struct unmarked_record_satisfying { + ObserverPredicate predicate; + unmarked_record_satisfying(ObserverPredicate p) : predicate(p) {} + bool operator()(ObserverRecord const &record) { + return !record.marked && predicate(record.observer); + } +}; + +template <typename Predicate> +bool mark_one(ObserverRecordList &observers, unsigned &/*marked_count*/, + Predicate p) +{ + ObserverRecordList::iterator found=std::find_if( + observers.begin(), observers.end(), + unmarked_record_satisfying<Predicate>(p) + ); + + if ( found != observers.end() ) { + found->marked = true; + return true; + } else { + return false; + } +} + +template <typename Predicate> +bool remove_one(ObserverRecordList &observers, unsigned &/*marked_count*/, + Predicate p) +{ + if (observers.empty()) { + return false; + } + + if (unmarked_record_satisfying<Predicate>(p)(observers.front())) { + observers.pop_front(); + return true; + } + + ObserverRecordList::iterator found=Algorithms::find_if_before( + observers.begin(), observers.end(), + unmarked_record_satisfying<Predicate>(p) + ); + + if ( found != observers.end() ) { + observers.erase_after(found); + return true; + } else { + return false; + } +} + +bool is_marked(ObserverRecord const &record) { return record.marked; } + +void remove_all_marked(ObserverRecordList &observers, unsigned &marked_count) +{ + ObserverRecordList::iterator iter; + + g_assert( !observers.empty() || !marked_count ); + + while ( marked_count && observers.front().marked ) { + observers.pop_front(); + --marked_count; + } + + iter = observers.begin(); + while (marked_count) { + iter = Algorithms::find_if_before(iter, observers.end(), is_marked); + observers.erase_after(iter); + --marked_count; + } +} + +} + +void CompositeNodeObserver::_finishIteration() { + if (!--_iterating) { + remove_all_marked(_active, _active_marked); + remove_all_marked(_pending, _pending_marked); + _active.insert(_active.end(), _pending.begin(), _pending.end()); + _pending.clear(); + } +} + +namespace { + +struct eql_observer { + NodeObserver const &observer; + eql_observer(NodeObserver const &o) : observer(o) {} + bool operator()(NodeObserver const &other) { + return &observer == &other; + } +}; + +} + +void CompositeNodeObserver::remove(NodeObserver &observer) { + eql_observer p(observer); + if (_iterating) { + mark_one(_active, _active_marked, p) || + mark_one(_pending, _pending_marked, p); + } else { + remove_one(_active, _active_marked, p) || + remove_one(_pending, _pending_marked, p); + } +} + +namespace { + +struct vector_data_matches { + void * const data; + vector_data_matches(void *d) : data(d) {} + + bool operator()(NodeObserver const &observer) { + VectorNodeObserver const *vo=dynamic_cast<VectorNodeObserver const *>(&observer); + bool OK = false; + if (vo) { + if (vo && vo->data == data) { + OK = true; + } + } + return OK; + } +}; + +} + +void CompositeNodeObserver::removeListenerByData(void *data) { + Debug::EventTracker<Debug::SimpleEvent<Debug::Event::XML> > tracker("remove-listener-by-data"); + vector_data_matches p(data); + if (_iterating) { + mark_one(_active, _active_marked, p) || + mark_one(_pending, _pending_marked, p); + } else { + remove_one(_active, _active_marked, p) || + remove_one(_pending, _pending_marked, p); + } +} + +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/composite-node-observer.h b/src/xml/composite-node-observer.h new file mode 100644 index 0000000..96f1c91 --- /dev/null +++ b/src/xml/composite-node-observer.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::XML::CompositeNodeObserver - combine multiple observers + *//* + * Authors: see git history + * + * Copyright (C) 2018 Author + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_COMPOSITE_NODE_OBSERVER_H +#define SEEN_INKSCAPE_XML_COMPOSITE_NODE_OBSERVER_H + +#include "inkgc/gc-managed.h" +#include "xml/node-observer.h" +#include "util/list-container.h" + +namespace Inkscape { + +namespace XML { + +struct NodeEventVector; + +/** + * @brief An observer that relays notifications to multiple other observers + * + * This special observer keeps a list of other observer objects and sends + * the notifications it receives to all of them. The implementation of the class + * allows an observer to remove itself from this object during a method call. + * For the documentation of callback methods, see NodeObserver. + */ +class CompositeNodeObserver : public NodeObserver, public GC::Managed<> { +public: + struct ObserverRecord : public GC::Managed<> { + explicit ObserverRecord(NodeObserver &o) : observer(o), marked(false) {} + + NodeObserver &observer; + bool marked; //< if marked for removal + }; + typedef Util::ListContainer<ObserverRecord> ObserverRecordList; + + CompositeNodeObserver() + : _iterating(0), _active_marked(0), _pending_marked(0) {} + + /** + * @brief Add an observer to the list + * @param observer The observer object to add + */ + void add(NodeObserver &observer); + /** + * @brief Remove an observer from the list + * @param observer The observer object to remove + */ + void remove(NodeObserver &observer); + /** + * @brief Add a set of callbacks with associated data + * @deprecated Use add() instead + */ + void addListener(NodeEventVector const &vector, void *data); + /** + * @brief Remove a set of callbacks by its associated data + * @deprecated Use remove() instead + */ + void removeListenerByData(void *data); + + void notifyChildAdded(Node &node, Node &child, Node *prev) override; + + void notifyChildRemoved(Node &node, Node &child, Node *prev) override; + + void notifyChildOrderChanged(Node &node, Node &child, + Node *old_prev, Node *new_prev) override; + + void notifyContentChanged(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content) override; + + void notifyAttributeChanged(Node &node, GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value) override; + + void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override; + +private: + unsigned _iterating; + ObserverRecordList _active; + unsigned _active_marked; + ObserverRecordList _pending; + unsigned _pending_marked; + + void _startIteration() { ++_iterating; } + void _finishIteration(); +}; + +} // namespace XML +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/croco-node-iface.cpp b/src/xml/croco-node-iface.cpp new file mode 100644 index 0000000..390c7b1 --- /dev/null +++ b/src/xml/croco-node-iface.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cstring> +#include <string> +#include <glib.h> + +#include "xml/croco-node-iface.h" +#include "xml/node.h" + +static char const * +local_part(char const *const qname) +{ + char const *ret = std::strrchr(qname, ':'); + if (ret) + return ++ret; + else + return qname; +} + +namespace Inkscape { +namespace XML { + +extern "C" { + +static CRXMLNodePtr get_parent(CRXMLNodePtr n) { return static_cast<Node const *>(n)->parent(); } +static CRXMLNodePtr get_first_child(CRXMLNodePtr n) { return static_cast<Node const *>(n)->firstChild(); } +static CRXMLNodePtr get_next(CRXMLNodePtr n) { return static_cast<Node const *>(n)->next(); } + +static CRXMLNodePtr get_prev(CRXMLNodePtr cn) +{ + Node const *n = static_cast<Node const *>(cn); + unsigned const n_pos = n->position(); + if (n_pos) { + return n->parent()->nthChild(n_pos - 1); + } else { + return nullptr; + } +} + +static char *get_attr(CRXMLNodePtr n, char const *a) +{ + return g_strdup(static_cast<Node const *>(n)->attribute(a)); +} + +static char const *get_local_name(CRXMLNodePtr n) { return local_part(static_cast<Node const *>(n)->name()); } +static gboolean is_element_node(CRXMLNodePtr n) { return static_cast<Node const *>(n)->type() == ELEMENT_NODE; } +} + +/** + * Interface for XML nodes used by libcroco. + * + * This structure defines operations on Inkscape::XML::Node used by the libcroco + * CSS parsing library. + */ +CRNodeIface const croco_node_iface = { + get_parent, + get_first_child, + get_next, + get_prev, + get_local_name, + get_attr, + g_free, + is_element_node +}; + +} +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/croco-node-iface.h b/src/xml/croco-node-iface.h new file mode 100644 index 0000000..2965ade --- /dev/null +++ b/src/xml/croco-node-iface.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SP_REPR_NODE_IFACE_H +#define INKSCAPE_SP_REPR_NODE_IFACE_H + +#include <3rdparty/libcroco/cr-node-iface.h> + +namespace Inkscape { +namespace XML { +extern CRNodeIface const croco_node_iface; +} +} + +#endif /* !INKSCAPE_SP_REPR_NODE_IFACE_H */ diff --git a/src/xml/document.h b/src/xml/document.h new file mode 100644 index 0000000..92878b2 --- /dev/null +++ b/src/xml/document.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Interface for XML documents + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SP_REPR_DOC_H +#define SEEN_INKSCAPE_XML_SP_REPR_DOC_H + +#include "xml/node.h" + +namespace Inkscape { +namespace XML { + +/** + * @brief Interface for XML documents + * + * This class represents a complete document tree. You have to go through this class + * to create new nodes. It also contains transaction support, which forms the base + * of the undo system. + * + * The document is also a node. It usually contains only two child nodes - a processing + * instruction node (PINode) containing the XML prolog, and the root node. You can get + * the root node of the document by calling the root() method. + * + * The name "transaction" can be misleading, because they are not atomic. Their main feature + * is that they provide rollback. After starting a transaction, + * all changes made to the document are stored in an internal event log. At any time + * after starting the transaction, you can call the rollback() method, which restores + * the document to the state it was before starting the transaction. Calling the commit() + * method causes the internal event log to be discarded, and you can establish a new + * "restore point" by calling beginTransaction() again. There can be only one active + * transaction at a time for a given document. + */ +struct Document : virtual public Node { +public: + /** + * @name Document transactions + * @{ + */ + /** + * @brief Checks whether there is an active transaction for this document + * @return true if there's an established transaction for this document, false otherwise + */ + virtual bool inTransaction()=0; + /** + * @brief Begin a transaction and start recording changes + * + * By calling this method you effectively establish a resotre point. + * You can undo all changes made to the document after this call using rollback(). + */ + virtual void beginTransaction()=0; + /** + * @brief Restore the state of the document prior to the transaction + * + * This method applies the inverses of all recorded changes in reverse order, + * restoring the document state from before the transaction. For some implementations, + * this function may do nothing. + */ + virtual void rollback()=0; + /** + * @brief Commit a transaction and discard change data + * + * This method finishes the active transaction and discards the recorded changes. + */ + virtual void commit()=0; + /** + * @brief Commit a transaction and store the events for later use + * + * This method finishes a transaction and returns an event chain + * that describes the changes made to the document. This method may return NULL, + * which means that the document implementation doesn't support event logging, + * or that no changes were made. + * + * @return Event chain describing the changes, or NULL + */ + virtual Event *commitUndoable()=0; + /*@}*/ + + /** + * @name Create new nodes + * @{ + */ + virtual Node *createElement(char const *name)=0; + virtual Node *createTextNode(char const *content)=0; + virtual Node *createTextNode(char const *content, bool is_CData)=0; + virtual Node *createComment(char const *content)=0; + virtual Node *createPI(char const *target, char const *content)=0; + /*@}*/ + + /** + * @brief Get the event logger for this document + * + * This is an implementation detail that should not be used outside of node implementations. + * It should be made non-public in the future. + */ + virtual NodeObserver *logger()=0; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/element-node.h b/src/xml/element-node.h new file mode 100644 index 0000000..376790f --- /dev/null +++ b/src/xml/element-node.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Element node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2004-2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_ELEMENT_NODE_H +#define SEEN_INKSCAPE_XML_ELEMENT_NODE_H + +#include "xml/simple-node.h" + +namespace Inkscape { + +namespace XML { + +/** + * @brief Element node, e.g. <group /> + */ +class ElementNode : public SimpleNode { +public: + ElementNode(int code, Document *doc) + : SimpleNode(code, doc) {} + ElementNode(ElementNode const &other, Document *doc) + : SimpleNode(other, doc) {} + + Inkscape::XML::NodeType type() const override { return Inkscape::XML::ELEMENT_NODE; } + +protected: + SimpleNode *_duplicate(Document* doc) const override { return new ElementNode(*this, doc); } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/event-fns.h b/src/xml/event-fns.h new file mode 100644 index 0000000..f3a6e52 --- /dev/null +++ b/src/xml/event-fns.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_XML_SP_REPR_ACTION_FNS_H +#define SEEN_INKSCAPE_XML_SP_REPR_ACTION_FNS_H + +namespace Inkscape { +namespace XML { + +struct Document; +class Event; +class NodeObserver; + +void replay_log_to_observer(Event const *log, NodeObserver &observer); +void undo_log_to_observer(Event const *log, NodeObserver &observer); + +} +} + +void sp_repr_begin_transaction (Inkscape::XML::Document *doc); +void sp_repr_rollback (Inkscape::XML::Document *doc); +void sp_repr_commit (Inkscape::XML::Document *doc); +Inkscape::XML::Event *sp_repr_commit_undoable (Inkscape::XML::Document *doc); + +void sp_repr_undo_log (Inkscape::XML::Event *log); +void sp_repr_replay_log (Inkscape::XML::Event *log); +Inkscape::XML::Event *sp_repr_coalesce_log (Inkscape::XML::Event *a, Inkscape::XML::Event *b); +void sp_repr_free_log (Inkscape::XML::Event *log); +void sp_repr_debug_print_log(Inkscape::XML::Event const *log); + +#endif diff --git a/src/xml/event.cpp b/src/xml/event.cpp new file mode 100644 index 0000000..86e042a --- /dev/null +++ b/src/xml/event.cpp @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Repr transaction logging + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004-2005 MenTaLguY + * Copyright (C) 1999-2003 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * g++ port Copyright (C) 2003 Nathan Hurst + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> // g_assert() +#include <cstdio> + +#include "event.h" +#include "event-fns.h" +#include "util/reverse-list.h" +#include "xml/document.h" +#include "xml/node-observer.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" + +using Inkscape::Util::List; +using Inkscape::Util::reverse_list; + +int Inkscape::XML::Event::_next_serial=0; + +void +sp_repr_begin_transaction (Inkscape::XML::Document *doc) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("begin-transaction"); + + g_assert(doc != nullptr); + doc->beginTransaction(); +} + +void +sp_repr_rollback (Inkscape::XML::Document *doc) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("rollback"); + + g_assert(doc != nullptr); + doc->rollback(); +} + +void +sp_repr_commit (Inkscape::XML::Document *doc) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("commit"); + + g_assert(doc != nullptr); + doc->commit(); +} + +Inkscape::XML::Event * +sp_repr_commit_undoable (Inkscape::XML::Document *doc) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("commit"); + + g_assert(doc != nullptr); + return doc->commitUndoable(); +} + +namespace { + +class LogPerformer : public Inkscape::XML::NodeObserver { +public: + typedef Inkscape::XML::Node Node; + + static LogPerformer &instance() { + static LogPerformer singleton; + return singleton; + } + + void notifyChildAdded(Node &parent, Node &child, Node *ref) override { + parent.addChild(&child, ref); + } + + void notifyChildRemoved(Node &parent, Node &child, Node */*old_ref*/) override { + parent.removeChild(&child); + } + + void notifyChildOrderChanged(Node &parent, Node &child, + Node */*old_ref*/, Node *new_ref) override + { + parent.changeOrder(&child, new_ref); + } + + void notifyAttributeChanged(Node &node, GQuark name, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared new_value) override + { + node.setAttribute(g_quark_to_string(name), new_value); + } + + void notifyContentChanged(Node &node, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared new_value) override + { + node.setContent(new_value); + } + + void notifyElementNameChanged(Node& node, GQuark /*old_value*/, GQuark new_value) override + { + node.setCodeUnsafe(new_value); + } +}; + +} + +void Inkscape::XML::undo_log_to_observer( + Inkscape::XML::Event const *log, + Inkscape::XML::NodeObserver &observer +) { + for ( Event const *action = log ; action ; action = action->next ) { + action->undoOne(observer); + } +} + +void sp_repr_undo_log (Inkscape::XML::Event *log) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("undo-log"); + + if (log) { + if (log->repr) { + g_assert(!log->repr->document()->inTransaction()); + } + } + + Inkscape::XML::undo_log_to_observer(log, LogPerformer::instance()); +} + +void Inkscape::XML::EventAdd::_undoOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildRemoved(*this->repr, *this->child, this->ref); +} + +void Inkscape::XML::EventDel::_undoOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildAdded(*this->repr, *this->child, this->ref); +} + +void Inkscape::XML::EventChgAttr::_undoOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyAttributeChanged(*this->repr, this->key, this->newval, this->oldval); +} + +void Inkscape::XML::EventChgContent::_undoOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyContentChanged(*this->repr, this->newval, this->oldval); +} + +void Inkscape::XML::EventChgOrder::_undoOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildOrderChanged(*this->repr, *this->child, this->newref, this->oldref); +} + +void Inkscape::XML::EventChgElementName::_undoOne( + Inkscape::XML::NodeObserver& observer +) const { + observer.notifyElementNameChanged(*this->repr, this->new_name, this->old_name); +} + +void Inkscape::XML::replay_log_to_observer( + Inkscape::XML::Event const *log, + Inkscape::XML::NodeObserver &observer +) { + List<Inkscape::XML::Event const &> reversed = + reverse_list<Inkscape::XML::Event::ConstIterator>(log, nullptr); + for ( ; reversed ; ++reversed ) { + reversed->replayOne(observer); + } +} + +void +sp_repr_replay_log (Inkscape::XML::Event *log) +{ + using Inkscape::Debug::SimpleEvent; + using Inkscape::Debug::EventTracker; + using Inkscape::Debug::Event; + + EventTracker<SimpleEvent<Event::XML> > tracker("replay-log"); + + if (log) { + if (log->repr->document()) { + g_assert(!log->repr->document()->inTransaction()); + } + } + + Inkscape::XML::replay_log_to_observer(log, LogPerformer::instance()); +} + +void Inkscape::XML::EventAdd::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildAdded(*this->repr, *this->child, this->ref); +} + +void Inkscape::XML::EventDel::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildRemoved(*this->repr, *this->child, this->ref); +} + +void Inkscape::XML::EventChgAttr::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyAttributeChanged(*this->repr, this->key, this->oldval, this->newval); +} + +void Inkscape::XML::EventChgContent::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyContentChanged(*this->repr, this->oldval, this->newval); +} + +void Inkscape::XML::EventChgOrder::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyChildOrderChanged(*this->repr, *this->child, this->oldref, this->newref); +} + +void Inkscape::XML::EventChgElementName::_replayOne( + Inkscape::XML::NodeObserver &observer +) const { + observer.notifyElementNameChanged(*this->repr, this->new_name, this->old_name); +} + +Inkscape::XML::Event * +sp_repr_coalesce_log (Inkscape::XML::Event *a, Inkscape::XML::Event *b) +{ + Inkscape::XML::Event *action; + Inkscape::XML::Event **prev_ptr; + + if (!b) return a; + if (!a) return b; + + /* find the earliest action in the second log */ + /* (also noting the pointer that references it, so we can + * replace it later) */ + prev_ptr = &b; + for ( action = b ; action->next ; action = action->next ) { + prev_ptr = &action->next; + } + + /* add the first log after it */ + action->next = a; + + /* optimize the result */ + *prev_ptr = action->optimizeOne(); + + return b; +} + +void +sp_repr_free_log (Inkscape::XML::Event *log) +{ + while (log) { + Inkscape::XML::Event *action; + action = log; + log = action->next; + delete action; + } +} + +namespace { + +template <typename T> struct ActionRelations; + +template <> +struct ActionRelations<Inkscape::XML::EventAdd> { + typedef Inkscape::XML::EventDel Opposite; +}; + +template <> +struct ActionRelations<Inkscape::XML::EventDel> { + typedef Inkscape::XML::EventAdd Opposite; +}; + +template <typename A> +Inkscape::XML::Event *cancel_add_or_remove(A *action) { + typedef typename ActionRelations<A>::Opposite Opposite; + Opposite *opposite=dynamic_cast<Opposite *>(action->next); + + bool OK = false; + if (opposite){ + if (opposite->repr == action->repr && + opposite->child == action->child && + opposite->ref == action->ref ) { + OK = true; + } + } + if (OK){ + Inkscape::XML::Event *remaining=opposite->next; + + delete opposite; + delete action; + + return remaining; + } else { + return action; + } +} +} + +Inkscape::XML::Event *Inkscape::XML::EventAdd::_optimizeOne() { + return cancel_add_or_remove(this); +} + +Inkscape::XML::Event *Inkscape::XML::EventDel::_optimizeOne() { + return cancel_add_or_remove(this); +} + +Inkscape::XML::Event *Inkscape::XML::EventChgAttr::_optimizeOne() { + Inkscape::XML::EventChgAttr *chg_attr=dynamic_cast<Inkscape::XML::EventChgAttr *>(this->next); + + /* consecutive chgattrs on the same key can be combined */ + if ( chg_attr) { + if ( chg_attr->repr == this->repr && + chg_attr->key == this->key ) + { + /* replace our oldval with the prior action's */ + this->oldval = chg_attr->oldval; + + /* discard the prior action */ + this->next = chg_attr->next; + delete chg_attr; + } + } + + return this; +} + +Inkscape::XML::Event *Inkscape::XML::EventChgContent::_optimizeOne() { + Inkscape::XML::EventChgContent *chg_content=dynamic_cast<Inkscape::XML::EventChgContent *>(this->next); + + /* consecutive content changes can be combined */ + if (chg_content) { + if (chg_content->repr == this->repr ) { + /* replace our oldval with the prior action's */ + this->oldval = chg_content->oldval; + + /* get rid of the prior action*/ + this->next = chg_content->next; + delete chg_content; + } + } + + return this; +} + +Inkscape::XML::Event *Inkscape::XML::EventChgOrder::_optimizeOne() { + Inkscape::XML::EventChgOrder *chg_order=dynamic_cast<Inkscape::XML::EventChgOrder *>(this->next); + + /* consecutive chgorders for the same child may be combined or + * canceled out */ + bool OK = false; + if (chg_order) { + if (chg_order->repr == this->repr && + chg_order->child == this->child ){ + OK = true; + } + } + if (OK) { + if ( chg_order->oldref == this->newref ) { + /* cancel them out */ + Inkscape::XML::Event *after=chg_order->next; + + delete chg_order; + delete this; + + return after; + } else { + /* combine them */ + this->oldref = chg_order->oldref; + + /* get rid of the other one */ + this->next = chg_order->next; + delete chg_order; + + return this; + } + } else { + return this; + } +} + +Inkscape::XML::Event* Inkscape::XML::EventChgElementName::_optimizeOne() { + auto next_chg_element_name = dynamic_cast<Inkscape::XML::EventChgElementName*>(this->next); + if (next_chg_element_name && next_chg_element_name->repr == this->repr) { + // Combine name changes to the same element. + this->old_name = next_chg_element_name->old_name; + this->next = next_chg_element_name->next; + delete next_chg_element_name; + } + return this; +} + +namespace { + +class LogPrinter : public Inkscape::XML::NodeObserver { +public: + typedef Inkscape::XML::Node Node; + + static LogPrinter &instance() { + static LogPrinter singleton; + return singleton; + } + + static Glib::ustring node_to_string(Node const &node) { + Glib::ustring result; + char const *type_name=nullptr; + switch (node.type()) { + case Inkscape::XML::DOCUMENT_NODE: + type_name = "Document"; + break; + case Inkscape::XML::ELEMENT_NODE: + type_name = "Element"; + break; + case Inkscape::XML::TEXT_NODE: + type_name = "Text"; + break; + case Inkscape::XML::COMMENT_NODE: + type_name = "Comment"; + break; + default: + g_assert_not_reached(); + } + char buffer[40]; + result.append("#<"); + result.append(type_name); + result.append(":"); + snprintf(buffer, 40, "0x%p", &node); + result.append(buffer); + result.append(">"); + + return result; + } + + static Glib::ustring ref_to_string(Node *ref) { + if (ref) { + return node_to_string(*ref); + } else { + return "beginning"; + } + } + + void notifyChildAdded(Node &parent, Node &child, Node *ref) override { + g_warning("Event: Added %s to %s after %s", node_to_string(parent).c_str(), node_to_string(child).c_str(), ref_to_string(ref).c_str()); + } + + void notifyChildRemoved(Node &parent, Node &child, Node */*ref*/) override { + g_warning("Event: Removed %s from %s", node_to_string(parent).c_str(), node_to_string(child).c_str()); + } + + void notifyChildOrderChanged(Node &parent, Node &child, + Node */*old_ref*/, Node *new_ref) override + { + g_warning("Event: Moved %s after %s in %s", node_to_string(child).c_str(), ref_to_string(new_ref).c_str(), node_to_string(parent).c_str()); + } + + void notifyAttributeChanged(Node &node, GQuark name, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared new_value) override + { + if (new_value) { + g_warning("Event: Set attribute %s to \"%s\" on %s", g_quark_to_string(name), new_value.pointer(), node_to_string(node).c_str()); + } else { + g_warning("Event: Unset attribute %s on %s", g_quark_to_string(name), node_to_string(node).c_str()); + } + } + + void notifyContentChanged(Node &node, + Inkscape::Util::ptr_shared /*old_value*/, + Inkscape::Util::ptr_shared new_value) override + { + if (new_value) { + g_warning("Event: Set content of %s to \"%s\"", node_to_string(node).c_str(), new_value.pointer()); + } else { + g_warning("Event: Unset content of %s", node_to_string(node).c_str()); + } + } + + void notifyElementNameChanged(Node& node, GQuark old_value, GQuark new_value) override + { + g_warning("Event: Changed name of %s from %s to %s\n", + node_to_string(node).c_str(), g_quark_to_string(old_value), g_quark_to_string(new_value)); + } +}; + +} + +void sp_repr_debug_print_log(Inkscape::XML::Event const *log) { + Inkscape::XML::replay_log_to_observer(log, LogPrinter::instance()); +} + diff --git a/src/xml/event.h b/src/xml/event.h new file mode 100644 index 0000000..4256eea --- /dev/null +++ b/src/xml/event.h @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Event object representing a change of the XML document + *//* + * Authors: + * Unknown author(s) + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> (documentation) + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SP_REPR_ACTION_H +#define SEEN_INKSCAPE_XML_SP_REPR_ACTION_H + +typedef unsigned int GQuark; +#include <glibmm/ustring.h> + +#include <iterator> +#include "util/share.h" +#include "util/forward-pointer-iterator.h" +#include "inkgc/gc-managed.h" +#include "xml/node.h" + +namespace Inkscape { +namespace XML { + +/** + * @brief Enumeration of all XML event types + */ +// enum EventType { +// EVENT_ADD, ///< Child added +// EVENT_DEL, ///< Child removed +// EVENT_CHG_ATTR, ///< Attribute changed +// EVENT_CHG_CONTENT, ///< Content changed +// EVENT_CHG_ORDER ///< Order of children changed +// }; + +/** + * @brief Generic XML modification event + * + * This is the base class for all other modification events. It is actually a singly-linked + * list of events, called an event chain or an event log. Logs of events that happened + * in a transaction can be obtained from Document::commitUndoable(). Events can be replayed + * to a NodeObserver, or undone (which is equivalent to replaying opposite events in reverse + * order). + * + * Event logs are built by appending to the front, so by walking the list one iterates over + * the events in reverse chronological order. + */ +class Event +: public Inkscape::GC::Managed<Inkscape::GC::SCANNED, Inkscape::GC::MANUAL> +{ +public: + virtual ~Event() = default; + + /** + * @brief Pointer to the next event in the event chain + * + * Note that the event this pointer points to actually happened before this event. + * This is because the event log is built by appending to the front. + */ + Event *next; + /** + * @brief Serial number of the event, not used at the moment + */ + int serial; + /** + * @brief Pointer to the node that was the object of the event + * + * Because the nodes are garbage-collected, this pointer guarantees that the node + * will stay in memory as long as the event does. This simplifies rolling back + * extensive deletions. + */ + Node *repr; + + struct IteratorStrategy { + static Event const *next(Event const *action) { + return action->next; + } + }; + + typedef Inkscape::Util::ForwardPointerIterator<Event, IteratorStrategy> Iterator; + typedef Inkscape::Util::ForwardPointerIterator<Event const, IteratorStrategy> ConstIterator; + + /** + * @brief If possible, combine this event with the next to reduce memory use + * @return Pointer to the optimized event chain, which may have changed + */ + Event *optimizeOne() { return _optimizeOne(); } + /** + * @brief Undo this event to an observer + * + * This method notifies the specified observer of an action opposite to the one that + * is described by this event. + */ + void undoOne(NodeObserver &observer) const { + _undoOne(observer); + } + /** + * @brief Replay this event to an observer + * + * This method notifies the specified event of the same action that it describes. + */ + void replayOne(NodeObserver &observer) const { + _replayOne(observer); + } + +protected: + Event(Node *r, Event *n) + : next(n), serial(_next_serial++), repr(r) {} + + virtual Event *_optimizeOne()=0; + virtual void _undoOne(NodeObserver &) const=0; + virtual void _replayOne(NodeObserver &) const=0; + +private: + static int _next_serial; +}; + +/** + * @brief Object representing child addition + */ +class EventAdd : public Event { +public: + EventAdd(Node *repr, Node *c, Node *rr, Event *next) + : Event(repr, next), child(c), ref(rr) {} + + /// The added child node + Node *child; + /// The node after which the child has been added, or NULL if it was added as first + Node *ref; + +private: + Event *_optimizeOne() override; + void _undoOne(NodeObserver &observer) const override; + void _replayOne(NodeObserver &observer) const override; +}; + +/** + * @brief Object representing child removal + */ +class EventDel : public Event { +public: + EventDel(Node *repr, Node *c, Node *rr, Event *next) + : Event(repr, next), child(c), ref(rr) {} + + /// The child node that was removed + Node *child; + /// The node after which the removed node was in the sibling order, or NULL if it was first + Node *ref; + +private: + Event *_optimizeOne() override; + void _undoOne(NodeObserver &observer) const override; + void _replayOne(NodeObserver &observer) const override; +}; + +/** + * @brief Object representing attribute change + */ +class EventChgAttr : public Event { +public: + EventChgAttr(Node *repr, GQuark k, + Inkscape::Util::ptr_shared ov, + Inkscape::Util::ptr_shared nv, + Event *next) + : Event(repr, next), key(k), + oldval(ov), newval(nv) {} + + /// GQuark corresponding to the changed attribute's name + GQuark key; + /// Value of the attribute before the change + Inkscape::Util::ptr_shared oldval; + /// Value of the attribute after the change + Inkscape::Util::ptr_shared newval; + +private: + Event *_optimizeOne() override; + void _undoOne(NodeObserver &observer) const override; + void _replayOne(NodeObserver &observer) const override; +}; + +/** + * @brief Object representing content change + */ +class EventChgContent : public Event { +public: + EventChgContent(Node *repr, + Inkscape::Util::ptr_shared ov, + Inkscape::Util::ptr_shared nv, + Event *next) + : Event(repr, next), oldval(ov), newval(nv) {} + + /// Content of the node before the change + Inkscape::Util::ptr_shared oldval; + /// Content of the node after the change + Inkscape::Util::ptr_shared newval; + +private: + Event *_optimizeOne() override; + void _undoOne(NodeObserver &observer) const override; + void _replayOne(NodeObserver &observer) const override; +}; + +/** + * @brief Object representing child order change + */ +class EventChgOrder : public Event { +public: + EventChgOrder(Node *repr, Node *c, Node *orr, Node *nrr, Event *next) + : Event(repr, next), child(c), + oldref(orr), newref(nrr) {} + + /// The node that was relocated in sibling order + Node *child; + /// The node after which the relocated node was in the sibling order before the change, or NULL if it was first + Node *oldref; + /// The node after which the relocated node is after the change, or if it's first + Node *newref; + +private: + Event *_optimizeOne() override; + void _undoOne(NodeObserver &observer) const override; + void _replayOne(NodeObserver &observer) const override; +}; + +/** + * @brief Object representing element name change. + */ +class EventChgElementName : public Event { +public: + EventChgElementName(Node* repr, GQuark old_name, GQuark new_name, Event* next) + : Event(repr, next), old_name(old_name), new_name(new_name) {} + + /// GQuark corresponding to the old element name. + GQuark old_name; + /// GQuark corresponding to the new element name. + GQuark new_name; + +private: + Event* _optimizeOne() override; + void _undoOne(NodeObserver& observer) const override; + void _replayOne(NodeObserver& observer) const override; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/helper-observer.cpp b/src/xml/helper-observer.cpp new file mode 100644 index 0000000..05161e9 --- /dev/null +++ b/src/xml/helper-observer.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper-observer.h" + +#include "object/sp-object.h" + +namespace Inkscape { +namespace XML { + +// Very simple observer that just emits a signal if anything happens to a node +SignalObserver::SignalObserver() + : _oldsel(nullptr) +{} + +SignalObserver::~SignalObserver() +{ + set(nullptr); // if _oldsel!=nullptr, remove observer and decrease refcount +} + +// Add this observer to the SPObject and remove it from any previous object +void SignalObserver::set(SPObject* o) +{ + // XML Tree being used directly in this function in the following code + // while it shouldn't be + // Pointer to object is stored, so refcounting should be increased/decreased + if(_oldsel) { + if (_oldsel->getRepr()) { + _oldsel->getRepr()->removeObserver(*this); + } + sp_object_unref(_oldsel); + _oldsel = nullptr; + } + if(o) { + if (o->getRepr()) { + o->getRepr()->addObserver(*this); + sp_object_ref(o); + _oldsel = o; + } + } +} + +void SignalObserver::notifyChildAdded(XML::Node&, XML::Node&, XML::Node*) +{ signal_changed()(); } + +void SignalObserver::notifyChildRemoved(XML::Node&, XML::Node&, XML::Node*) +{ signal_changed()(); } + +void SignalObserver::notifyChildOrderChanged(XML::Node&, XML::Node&, XML::Node*, XML::Node*) +{ signal_changed()(); } + +void SignalObserver::notifyContentChanged(XML::Node&, Util::ptr_shared, Util::ptr_shared) +{} + +void SignalObserver::notifyAttributeChanged(XML::Node&, GQuark, Util::ptr_shared, Util::ptr_shared) +{ signal_changed()(); } + +void SignalObserver::notifyElementNameChanged(Node&, GQuark, GQuark) +{ + signal_changed()(); +} + +sigc::signal<void>& SignalObserver::signal_changed() +{ + return _signal_changed; +} + +} //namespace XML +} //namespace Inkscape + diff --git a/src/xml/helper-observer.h b/src/xml/helper-observer.h new file mode 100644 index 0000000..4bf172b --- /dev/null +++ b/src/xml/helper-observer.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_XML_HELPER_OBSERVER +#define SEEN_XML_HELPER_OBSERVER + +#include <cstddef> +#include <sigc++/sigc++.h> + +#include "node-observer.h" +#include "node.h" + +class SPObject; + +namespace Inkscape { +namespace XML { + +class Node; + +// Very simple observer that just emits a signal if anything happens to a node +class SignalObserver : public NodeObserver { +public: + SignalObserver(); + ~SignalObserver() override; + + // Add this observer to the SPObject and remove it from any previous object + void set(SPObject* o); + void notifyChildAdded(Node&, Node&, Node*) override; + void notifyChildRemoved(Node&, Node&, Node*) override; + void notifyChildOrderChanged(Node&, Node&, Node*, Node*) override; + void notifyContentChanged(Node&, Util::ptr_shared, Util::ptr_shared) override; + void notifyAttributeChanged(Node&, GQuark, Util::ptr_shared, Util::ptr_shared) override; + void notifyElementNameChanged(Node&, GQuark, GQuark) override; + sigc::signal<void>& signal_changed(); +private: + sigc::signal<void> _signal_changed; + SPObject* _oldsel; +}; + +} +} + +#endif //#ifndef __XML_HELPER_OBSERVER__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/xml/invalid-operation-exception.h b/src/xml/invalid-operation-exception.h new file mode 100644 index 0000000..e43529e --- /dev/null +++ b/src/xml/invalid-operation-exception.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::XML::InvalidOperationException - invalid operation for node type + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Copyright 2004-2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_INVALID_OPERATION_EXCEPTION_H +#define SEEN_INKSCAPE_XML_INVALID_OPERATION_EXCEPTION_H + +#include <exception> +#include <stdexcept> + +namespace Inkscape { + +namespace XML { + +class InvalidOperationException : public std::logic_error { +public: + InvalidOperationException(std::string const &message) : + std::logic_error(message) + { } +}; + +} + +} + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/log-builder.cpp b/src/xml/log-builder.cpp new file mode 100644 index 0000000..3fd831a --- /dev/null +++ b/src/xml/log-builder.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Object building an event log. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/log-builder.h" +#include "xml/event.h" +#include "xml/event-fns.h" + +namespace Inkscape { +namespace XML { + +void LogBuilder::discard() { + sp_repr_free_log(_log); + _log = nullptr; +} + +Event *LogBuilder::detach() { + Event *log=_log; + _log = nullptr; + return log; +} + +void LogBuilder::addChild(Node &node, Node &child, Node *prev) { + _log = new Inkscape::XML::EventAdd(&node, &child, prev, _log); + _log = _log->optimizeOne(); +} + +void LogBuilder::removeChild(Node &node, Node &child, Node *prev) { + _log = new Inkscape::XML::EventDel(&node, &child, prev, _log); + _log = _log->optimizeOne(); +} + +void LogBuilder::setChildOrder(Node &node, Node &child, + Node *old_prev, Node *new_prev) +{ + _log = new Inkscape::XML::EventChgOrder(&node, &child, old_prev, new_prev, _log); + _log = _log->optimizeOne(); +} + +void LogBuilder::setContent(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content) +{ + _log = new Inkscape::XML::EventChgContent(&node, old_content, new_content, _log); + _log = _log->optimizeOne(); +} + +void LogBuilder::setAttribute(Node &node, GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value) +{ + _log = new Inkscape::XML::EventChgAttr(&node, name, old_value, new_value, _log); + _log = _log->optimizeOne(); +} + +void LogBuilder::setElementName(Node& node, GQuark old_name, GQuark new_name) +{ + _log = new Inkscape::XML::EventChgElementName(&node, old_name, new_name, _log); + _log = _log->optimizeOne(); +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/log-builder.h b/src/xml/log-builder.h new file mode 100644 index 0000000..bd7628b --- /dev/null +++ b/src/xml/log-builder.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Object building an event log + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_LOG_BUILDER_H +#define SEEN_INKSCAPE_XML_LOG_BUILDER_H + +#include "inkgc/gc-managed.h" +#include "xml/node-observer.h" + +namespace Inkscape { +namespace XML { + +class Event; +class Node; + +/** + * @brief Event log builder + * + * This object records all events sent to it via the public methods in an internal event log. + * Calling detach() then returns the built log. Calling discard() will clear all the events + * recorded so far. + */ +class LogBuilder { +public: + LogBuilder() : _log(nullptr) {} + ~LogBuilder() { discard(); } + + /** @name Manipulate the recorded event log + * @{ */ + /** + * @brief Clear the internal log + */ + void discard(); + /** + * @brief Get the internal event log + * @return The recorded event chain + */ + Event *detach(); + /*@}*/ + + /** @name Record events in the log + * @{ */ + void addChild(Node &node, Node &child, Node *prev); + + void removeChild(Node &node, Node &child, Node *prev); + + void setChildOrder(Node &node, Node &child, + Node *old_prev, Node *new_prev); + + void setContent(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content); + + void setAttribute(Node &node, GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value); + + void setElementName(Node& node, GQuark old_name, GQuark new_name); + /*@}*/ + +private: + Event *_log; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/node-event-vector.h b/src/xml/node-event-vector.h new file mode 100644 index 0000000..263751e --- /dev/null +++ b/src/xml/node-event-vector.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Deprecated structure for a set of callbacks for node state changes + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * + * Copyright (C) 1999-2002 Lauris Kaplinski and Frank Felfe + * Copyright (C) 2000-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SP_REPR_EVENT_VECTOR +#define SEEN_INKSCAPE_XML_SP_REPR_EVENT_VECTOR + +#include "xml/node.h" + +namespace Inkscape { +namespace XML { +struct NodeEventVector; +} +} + +/** + * @brief Generate events corresponding to the node's state + * @deprecated Use Node::synthesizeEvents(NodeObserver &) instead + */ +inline void sp_repr_synthesize_events (Inkscape::XML::Node *repr, const Inkscape::XML::NodeEventVector *vector, void* data) { + repr->synthesizeEvents(vector, data); +} +/** + * @brief Add a set of callbacks for node state changes and its associated data + * @deprecated Use Node::addObserver() instead + */ +inline void sp_repr_add_listener (Inkscape::XML::Node *repr, const Inkscape::XML::NodeEventVector *vector, void* data) { + repr->addListener(vector, data); +} +/** + * @brief Remove a set of callbacks based on associated data + * @deprecated Use Node::removeObserver() instead + */ +inline void sp_repr_remove_listener_by_data (Inkscape::XML::Node *repr, void* data) { + repr->removeListenerByData(data); +} + +namespace Inkscape { +namespace XML { + +/** + * @brief Structure holding callbacks for node state changes + * @deprecated Derive an observer object from the NodeObserver class instead + */ +struct NodeEventVector { + /* Immediate signals */ + void (* child_added) (Node *repr, Node *child, Node *ref, void* data); + void (* child_removed) (Node *repr, Node *child, Node *ref, void* data); + void (* attr_changed) (Node *repr, char const *key, char const *oldval, char const *newval, bool is_interactive, void* data); + void (* content_changed) (Node *repr, char const *oldcontent, char const *newcontent, void * data); + void (* order_changed) (Node *repr, Node *child, Node *oldref, Node *newref, void* data); + void (* element_name_changed) (Node* repr, char const* oldname, char const* newname, void* data); +}; + +} +} + +#endif diff --git a/src/xml/node-fns.cpp b/src/xml/node-fns.cpp new file mode 100644 index 0000000..c996cfd --- /dev/null +++ b/src/xml/node-fns.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifdef HAVE_CONFIG_H +#endif + +#include <map> +#include <cstring> +#include <string> +#include <glib.h> // g_assert() + +#include "xml/node-iterators.h" +#include "util/find-if-before.h" +#include "node-fns.h" + +namespace Inkscape { +namespace XML { + +/* the id_permitted stuff is a temporary-ish hack */ + +namespace { + +bool id_permitted_internal(GQuark qname) { + char const *qname_s=g_quark_to_string(qname); + return !strncmp("svg:", qname_s, 4) || !strncmp("sodipodi:", qname_s, 9) || + !strncmp("inkscape:", qname_s, 9); +} + + +bool id_permitted_internal_memoized(GQuark qname) { + typedef std::map<GQuark, bool> IdPermittedMap; + static IdPermittedMap id_permitted_names; + + IdPermittedMap::iterator found; + found = id_permitted_names.find(qname); + if ( found != id_permitted_names.end() ) { + return found->second; + } else { + bool permitted=id_permitted_internal(qname); + id_permitted_names[qname] = permitted; + return permitted; + } +} + +} + +bool id_permitted(Node const *node) { + g_return_val_if_fail(node != nullptr, false); + + if ( node->type() != ELEMENT_NODE ) { + return false; + } + + return id_permitted_internal_memoized((GQuark)node->code()); +} + +struct node_matches { + node_matches(Node const &n) : node(n) {} + bool operator()(Node const &other) { return &other == &node; } + Node const &node; +}; + +// documentation moved to header +Node *previous_node(Node *node) { + return node->prev(); + using Inkscape::Algorithms::find_if_before; + + if ( !node || !node->parent() ) { + return nullptr; + } + + Node *previous=find_if_before<NodeSiblingIterator>( + node->parent()->firstChild(), nullptr, node_matches(*node) + ); + + g_assert(previous == nullptr + ? node->parent()->firstChild() == node + : previous->next() == node); + + return previous; +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/node-fns.h b/src/xml/node-fns.h new file mode 100644 index 0000000..82724e2 --- /dev/null +++ b/src/xml/node-fns.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Helper functions for XML nodes + *//* + * Authors: + * see git history + * Unknown author + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> (documentation) + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_XML_NODE_FNS_H +#define SEEN_XML_NODE_FNS_H + +#include "xml/node.h" + +namespace Inkscape { +namespace XML { + +bool id_permitted(Node const *node); + +//@{ +/** + * @brief Get the next node in sibling order + * @param node The origin node + * @return The next node in sibling order + * @relates Inkscape::XML::Node + */ +inline Node *next_node(Node *node) { + return ( node ? node->next() : nullptr ); +} +inline Node const *next_node(Node const *node) { + return ( node ? node->next() : nullptr ); +} +//@} + +//@{ +/** + * @brief Get the previous node in sibling order + * + * This method, unlike Node::next(), is a linear search over the children of @c node's parent. + * The return value is NULL when the node has no parent or is first in the sibling order. + * + * @param node The origin node + * @return The previous node in sibling order, or NULL + * @relates Inkscape::XML::Node + */ +Node *previous_node(Node *node); +inline Node const *previous_node(Node const *node) { + return previous_node(const_cast<Node *>(node)); +} +//@} + +//@{ +/** + * @brief Get the node's parent + * @param node The origin node + * @return The node's parent + * @relates Inkscape::XML::Node + */ +inline Node *parent_node(Node *node) { + return ( node ? node->parent() : nullptr ); +} +inline Node const *parent_node(Node const *node) { + return ( node ? node->parent() : nullptr ); +} +//@} + +} +} + +#endif /* !SEEN_XML_NODE_FNS_H */ +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/node-iterators.h b/src/xml/node-iterators.h new file mode 100644 index 0000000..11c9450 --- /dev/null +++ b/src/xml/node-iterators.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Node iterators + * + * Authors: + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 2004 MenTaLguY + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SP_REPR_ITERATORS_H +#define SEEN_INKSCAPE_XML_SP_REPR_ITERATORS_H + +#include "util/forward-pointer-iterator.h" +#include "xml/node.h" + +namespace Inkscape { +namespace XML { + +struct NodeSiblingIteratorStrategy { + static Node const *next(Node const *node) { + return ( node ? node->next() : nullptr ); + } +}; + +struct NodeParentIteratorStrategy { + static Node const *next(Node const *node) { + return ( node ? node->parent() : nullptr ); + } +}; + +typedef Inkscape::Util::ForwardPointerIterator<Node, + NodeSiblingIteratorStrategy> + NodeSiblingIterator; + +typedef Inkscape::Util::ForwardPointerIterator<Node const, + NodeSiblingIteratorStrategy> + NodeConstSiblingIterator; + +typedef Inkscape::Util::ForwardPointerIterator<Node, + NodeParentIteratorStrategy> + NodeParentIterator; + +typedef Inkscape::Util::ForwardPointerIterator<Node const, + NodeParentIteratorStrategy> + NodeConstParentIterator; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/node-observer.h b/src/xml/node-observer.h new file mode 100644 index 0000000..e3173a8 --- /dev/null +++ b/src/xml/node-observer.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Interface for XML node observers + *//* + * Authors: + * MenTaLguY <mental@rydia.net> + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> (documentation) + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** @file + */ + +#ifndef SEEN_INKSCAPE_XML_NODE_OBSERVER_H +#define SEEN_INKSCAPE_XML_NODE_OBSERVER_H + +#include "util/share.h" +typedef unsigned int GQuark; + +#ifndef INK_UNUSED +#define INK_UNUSED(x) ((void)(x)) +#endif // INK_UNUSED + +namespace Inkscape { +namespace XML { + +class Node; + +/** + * @brief Interface for XML node observers + * + * This class defines an interface for objects that can receive + * XML node state change notifications. The observer has to be registered using + * the Node::addObserver() method to be notified of changes of this node only, + * or using Node::addSubtreeObserver() to also receive notifications about its + * descendants. All observer methods are called when the operations in question have + * been completed, just before returning from the modifying methods. + * + * Be careful when e.g. changing an attribute of @c node in notifyAttributeChanged(). + * The method will be called again due to the XML modification performed in it. If you + * don't take special precautions to ignore the second call, it will result in infinite + * recursion. + * + * The virtual methods of this class do nothing by default, so you don't need to provide + * stubs for things you don't use. A good idea is to make the observer register itself + * on construction and unregister itself on destruction. This will ensure there are + * no dangling references. + */ +class NodeObserver { +protected: + /* the constructor is protected to prevent instantiation */ + NodeObserver() = default; +public: + virtual ~NodeObserver() = default; + + // FIXME: somebody needs to learn what "pure virtual" means + + /** + * @brief Child addition callback + * + * This method is called whenever a child is added to the observed node. The @c prev + * parameter is NULL when the newly added child is first in the sibling order. + * + * @param node The changed XML node + * @param child The newly added child node + * @param prev The node after which the new child was inserted into the sibling order, or NULL + */ + virtual void notifyChildAdded(Node &node, Node &child, Node *prev) { + INK_UNUSED(node); + INK_UNUSED(child); + INK_UNUSED(prev); + } + + /** + * @brief Child removal callback + * + * This method is called whenever a child is removed from the observed node. The @c prev + * parameter is NULL when the removed child was first in the sibling order. + * + * @param node The changed XML node + * @param child The removed child node + * @param prev The node that was before the removed node in sibling order, or NULL + */ + virtual void notifyChildRemoved(Node &node, Node &child, Node *prev) { + INK_UNUSED(node); + INK_UNUSED(child); + INK_UNUSED(prev); + } + + /** + * @brief Child order change callback + * + * This method is called whenever the order of a node's children is changed using + * Node::changeOrder(). The @c old_prev parameter is NULL if the relocated node + * was first in the sibling order before the order change, and @c new_prev is NULL + * if it was moved to the first position by this operation. + * + * @param node The changed XML node + * @param child The child node that was relocated in the sibling order + * @param old_prev The node that was before @c child prior to the order change + * @param new_prev The node that is before @c child after the order change + */ + virtual void notifyChildOrderChanged(Node &node, Node &child, + Node *old_prev, Node *new_prev) { + INK_UNUSED(node); + INK_UNUSED(child); + INK_UNUSED(old_prev); + INK_UNUSED(new_prev); + } + + /** + * @brief Content change callback + * + * This method is called whenever a node's content is changed using Node::setContent(), + * e.g. for text or comment nodes. + * + * @param node The changed XML node + * @param old_content Old content of @c node + * @param new_content New content of @c node + */ + virtual void notifyContentChanged(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content) { + INK_UNUSED(node); + INK_UNUSED(old_content); + INK_UNUSED(new_content); + } + + /** + * @brief Attribute change callback + * + * This method is called whenever one of a node's attributes is changed. + * + * @param node The changed XML node + * @param name GQuark corresponding to the attribute's name + * @param old_value Old value of the modified attribute + * @param new_value New value of the modified attribute + */ + virtual void notifyAttributeChanged(Node &node, GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value) { + INK_UNUSED(node); + INK_UNUSED(name); + INK_UNUSED(old_value); + INK_UNUSED(new_value); + } + + /** + * @brief Element name change callback. + * + * This method is called whenever an element node's name is changed. + * + * @param node The changed XML node. + * @param old_name GQuark corresponding to the old element name. + * @param new_name GQuark corresponding to the new element name. + */ + virtual void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) { + INK_UNUSED(node); + INK_UNUSED(old_name); + INK_UNUSED(new_name); + } + +}; + +} // namespace XML +} // namespace Inkscape + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/node.h b/src/xml/node.h new file mode 100644 index 0000000..9891a67 --- /dev/null +++ b/src/xml/node.h @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Interface for XML nodes + * + * Authors: + * MenTaLguY <mental@rydia.net> + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> (documentation) + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_NODE_H +#define SEEN_INKSCAPE_XML_NODE_H + +#include <glibmm/ustring.h> +#include "gc-anchored.h" +#include "util/list.h" +#include "util/const_char_ptr.h" + +namespace Inkscape { +namespace XML { + +struct AttributeRecord; +struct Document; +class Event; +class NodeObserver; +struct NodeEventVector; + +/** + * @brief Enumeration containing all supported node types. + */ +enum NodeType { + DOCUMENT_NODE, ///< Top-level document node. Do not confuse with the root node. + ELEMENT_NODE, ///< Regular element node, e.g. <group />. + TEXT_NODE, ///< Text node, e.g. "Some text" in <group>Some text</group> is represented by a text node. + COMMENT_NODE, ///< Comment node, e.g. <!-- some comment --> + PI_NODE ///< Processing instruction node, e.g. <?xml version="1.0" encoding="utf-8" standalone="no"?> +}; + +// careful; GC::Anchored should only appear once in the inheritance +// hierarchy; otherwise there will be leaks + +/** + * @brief Interface for refcounted XML nodes + * + * This class is an abstract base type for all nodes in an XML document - this includes + * everything except attributes. An XML document is also a node itself. This is the main + * class used for interfacing with Inkscape's documents. Everything that has to be stored + * in the SVG has to go through this class at some point. + * + * Each node unconditionally has to belong to a document. There no "documentless" nodes, + * and it's not possible to move nodes between documents - they have to be duplicated. + * Each node can only refer to the nodes in the same document. Name of the node is immutable, + * it cannot be changed after its creation. Same goes for the type of the node. To simplify + * the use of this class, you can perform all operations on all nodes, but only some of them + * make any sense. For example, only element nodes can have attributes, only element and + * document nodes can have children, and all nodes except element and document nodes can + * have content. Although you can set content for element nodes, it won't make any difference + * in the XML output. + * + * To create new nodes, use the methods of the Inkscape::XML::Document class. You can obtain + * the nodes' document using the document() method. To destroy a node, just unparent it + * by calling sp_repr_unparent() or node->parent->removeChild() and release any references + * to it. The garbage collector will reclaim the memory in the next pass. There are additional + * convenience functions defined in @ref xml/repr.h + * + * In addition to regular DOM manipulations, you can register observer objects that will + * receive notifications about changes made to the node. See the NodeObserver class. + * + * @see Inkscape::XML::Document + * @see Inkscape::XML::NodeObserver + */ +class Node : public Inkscape::GC::Anchored { +public: + Node() = default; + ~Node() override = default; + + /** + * @name Retrieve information about the node + * @{ + */ + + /** + * @brief Get the type of the node + * @return NodeType enumeration member corresponding to the type of the node. + */ + virtual NodeType type() const=0; + + /** + * @brief Get the name of the element node + * + * This method only makes sense for element nodes. Names are stored as + * GQuarks to accelerate conversions. + * + * @return Name for element nodes, NULL for others + */ + virtual char const *name() const=0; + /** + * @brief Get the integer code corresponding to the node's name + * @return GQuark code corresponding to the name + */ + virtual int code() const=0; + + /** + * @brief Get the index of this node in parent's child order + * + * If this method is used on a node that doesn't have a parent, the method will return 0, + * and a warning will be printed on the console. + * + * @return The node's index, or 0 if the node does not have a parent + */ + virtual unsigned position() const=0; + + /** + * @brief Get the number of children of this node + * @return The number of children + */ + virtual unsigned childCount() const=0; + + /** + * @brief Get the content of a text or comment node + * + * This method makes no sense for element nodes. To retrieve the element node's name, + * use the name() method. + * + * @return The node's content + */ + virtual char const *content() const=0; + + /** + * @brief Get the string representation of a node's attribute + * + * If there is no attribute with the given name, the method will return NULL. + * All strings returned by this method are owned by the node and may not be freed. + * The returned pointer will become invalid when the attribute changes. If you need + * to store the return value, use g_strdup(). To parse the string, use methods + * in repr.h + * + * @param key The name of the node's attribute + */ + virtual char const *attribute(char const *key) const=0; + + /** + * @brief Get a list of the node's attributes + * + * The returned list is a functional programming style list rather than a standard one. + * + * @return A list of AttributeRecord structures describing the attributes + * @todo This method should return std::map<Glib::Quark const, gchar const *> + * or something similar with a custom allocator + */ + virtual Inkscape::Util::List<AttributeRecord const> attributeList() const=0; + + /** + * @brief Check whether this node has any attribute that matches a string + * + * This method checks whether this node has any attributes whose names + * have @c partial_name as their substrings. The check is done using + * the strstr() function of the C library. I don't know what would require that + * functionality, because matchAttributeName("id") matches both "identity" and "hidden". + * + * @param partial_name The string to match against all attributes + * @return true if there is such an attribute, false otherwise + */ + virtual bool matchAttributeName(char const *partial_name) const=0; + + /*@}*/ + + /** + * @name Modify the node + * @{ + */ + + /** + * @brief Set the position of this node in parent's child order + * + * To move the node to the end of the parent's child order, pass a negative argument. + * + * @param pos The new position in parent's child order + */ + virtual void setPosition(int pos)=0; + + /** + * @brief Set the content of a text or comment node + * + * This method doesn't make sense for element nodes. + * + * @param value The node's new content + */ + virtual void setContent(char const *value)=0; + + //@{ + /** + * @brief Change an attribute of this node + * + * The strings passed to this method are copied, so you can free them after use. + * + * @param key Name of the attribute to change + * @param value The new value of the attribute + * @param is_interactive Ignored + */ + + void setAttribute(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value, + bool is_interactive=false) { + this->setAttributeImpl(key.data(), value.data(), is_interactive); + } + + /** + * @brief Change an attribute of this node. Empty string deletes the attribute. + * + * @param key Name of the attribute to change + * @param value The new value of the attribute + * + */ + void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key, + Inkscape::Util::const_char_ptr value) { + this->setAttributeImpl(key.data(), + (value.data() == nullptr || value.data()[0]=='\0') ? nullptr : value.data(), false); + } + + + /** + * @brief Remove an attribute of this node + * + * @param key Name of the attribute to delete + * + */ + void removeAttribute(Inkscape::Util::const_char_ptr key) { + this->setAttributeImpl(key.data(), nullptr, false); + } + + //@} + /** + * @brief Set the integer GQuark code for the name of the node. + * + * Do not use this function unless you really have a good reason. + * + * @param code The integer value corresponding to the string to be set as + * the name of this node + */ + virtual void setCodeUnsafe(int code) = 0; + + /*@}*/ + + + /** + * @name Traverse the XML tree + * @{ + */ + + //@{ + /** + * @brief Get the node's associated document + * @return The document to which the node belongs. Never NULL. + */ + virtual Document *document()=0; + virtual Document const *document() const=0; + //@} + + //@{ + /** + * @brief Get the root node of this node's document + * + * This method works on any node that is part of an XML document, and returns + * the root node of the document in which it resides. For detached node hierarchies + * (i.e. nodes that are not descendants of a document node) this method + * returns the highest-level element node. For detached non-element nodes this method + * returns NULL. + * + * @return A pointer to the root element node, or NULL if the node is detached + */ + virtual Node *root()=0; + virtual Node const *root() const=0; + //@} + + //@{ + /** + * @brief Get the parent of this node + * + * This method will return NULL for detached nodes. + * + * @return Pointer to the parent, or NULL + */ + virtual Node *parent()=0; + virtual Node const *parent() const=0; + //@} + + //@{ + /** + * @brief Get the next sibling of this node + * + * This method will return NULL if the node is the last sibling element of the parent. + * The nodes form a singly-linked list, so there is no "prev()" method. Use the provided + * external function for that. + * + * @return Pointer to the next sibling, or NULL + * @see Inkscape::XML::previous_node() + */ + virtual Node *next()=0; + virtual Node const *next() const=0; + virtual Node *prev()=0; + virtual Node const *prev() const=0; + //@} + + //@{ + /** + * @brief Get the first child of this node + * + * For nodes without any children, this method returns NULL. + * + * @return Pointer to the first child, or NULL + */ + virtual Node *firstChild()=0; + virtual Node const *firstChild() const=0; + //@} + + //@{ + /** + * @brief Get the last child of this node + * + * For nodes without any children, this method returns NULL. + * + * @return Pointer to the last child, or NULL + */ + virtual Node *lastChild()=0; + virtual Node const *lastChild() const=0; + //@} + + //@{ + /** + * @brief Get the child of this node with a given index + * + * If there is no child with the specified index number, this method will return NULL. + * + * @param index The zero-based index of the child to retrieve + * @return Pointer to the appropriate child, or NULL + */ + virtual Node *nthChild(unsigned index)=0; + virtual Node const *nthChild(unsigned index) const=0; + //@} + + /*@}*/ + + /** + * @name Manipulate the XML tree + * @{ + */ + + /** + * @brief Create a duplicate of this node + * + * The newly created node has no parent, and a refcount equal 1. + * You need to manually insert it into the document, using e.g. appendChild(). + * Afterwards, call Inkscape::GC::release on it, so that it will be + * automatically collected when the parent is collected. + * + * @param doc The document in which the duplicate should be created + * @return A pointer to the duplicated node + */ + virtual Node *duplicate(Document *doc) const=0; + + /** + * @brief Insert another node as a child of this node + * + * When @c after is NULL, the inserted node will be placed as the first child + * of this node. @c after must be a child of this node. + * + * @param child The node to insert + * @param after The node after which the inserted node should be placed, or NULL + */ + virtual void addChild(Node *child, Node *after)=0; + + /** + * @brief Insert another node as a child of this node + * + * This is more efficient than appendChild() + setPosition(). + * + * @param child The node to insert + * @param pos The position in parent's child order + */ + void addChildAtPos(Node *child, unsigned pos) + { + Node *after = (pos == 0) ? nullptr : nthChild(pos - 1); + addChild(child, after); + } + + /** + * @brief Append a node as the last child of this node + * @param child The node to append + */ + virtual void appendChild(Node *child)=0; + + /** + * @brief Remove a child of this node + * + * Once the pointer to the removed node disappears from the stack, the removed node + * will be collected in the next GC pass, but only as long as its refcount is zero. + * You should keep a refcount of zero for all nodes in the document except for + * the document node itself, because they will be held in memory by the parent. + * + * @param child The child to remove + */ + virtual void removeChild(Node *child)=0; + + /** + * @brief Move a given node in this node's child order + * + * Both @c child and @c after must be children of this node for the method to work. + * + * @param child The node to move in the order + * @param after The sibling node after which the moved node should be placed + */ + virtual void changeOrder(Node *child, Node *after)=0; + + /** + * @brief Remove all elements that not in src node + * @param src The node to check for elements into this node + * @param key The attribute to use as the identity attribute + */ + virtual void cleanOriginal(Node *src, gchar const *key)=0; + + + /** + * @brief Compare 2 nodes equality + * @param other The other node to compare + * @param recursive Recursive mode check + */ + virtual bool equal(Node const *other, bool recursive)=0; + /** + * @brief Merge all children of another node with the current + * + * This method merges two node hierarchies, where @c src takes precedence. + * @c key is the name of the attribute that determines whether two nodes are + * corresponding (it must be the same for both, and all of their ancestors). If there is + * a corresponding node in @c src hierarchy, their attributes and content override the ones + * already present in this node's hierarchy. If there is no corresponding node, + * it is copied from @c src to this node. This method is used when merging the user's + * preferences file with the defaults, and has little use beyond that. + * + * @param src The node to merge into this node + * @param key The attribute to use as the identity attribute + * @param noid If true process noid items + * @param key If clean callback to cleanOriginal + */ + + virtual void mergeFrom(Node const *src, char const *key, bool extension = false, bool clean = false)=0; + + /*@}*/ + + + /** + * @name Notify observers about operations on the node + * @{ + */ + + /** + * @brief Add an object that will be notified of the changes to this node + * + * @c observer must be an object deriving from the NodeObserver class. + * The virtual methods of this object will be called when a corresponding change + * happens to this node. You can also notify the observer of the node's current state + * using synthesizeEvents(NodeObserver &). + * + * @param observer The observer object + */ + virtual void addObserver(NodeObserver &observer)=0; + /** + * @brief Remove an object from the list of observers + * @param observer The object to be removed + */ + virtual void removeObserver(NodeObserver &observer)=0; + /** + * @brief Generate a sequence of events corresponding to the state of this node + * + * This function notifies the specified observer of all the events that would + * recreate the current state of this node; e.g. the observer is notified of + * all the attributes, children and content like they were just created. + * This function can greatly simplify observer logic. + * + * @param observer The node observer to notify of the events + */ + virtual void synthesizeEvents(NodeObserver &observer)=0; + + /** + * @brief Add an object that will be notified of the changes to this node and its descendants + * + * The difference between adding a regular observer and a subtree observer is that + * the subtree observer will also be notified if a change occurs to any of the node's + * descendants, while a regular observer will only be notified of changes to the node + * it was assigned to. + * + * @param observer The observer object + */ + virtual void addSubtreeObserver(NodeObserver &observer)=0; + + /** + * @brief Remove an object from the subtree observers list + * @param observer The object to be removed + */ + virtual void removeSubtreeObserver(NodeObserver &observer)=0; + + /** + * @brief Add a set node change callbacks with an associated data + * @deprecated Use addObserver(NodeObserver &) instead + */ + virtual void addListener(NodeEventVector const *vector, void *data)=0; + /** + * @brief Remove a set of node change callbacks by their associated data + * @deprecated Use removeObserver(NodeObserver &) instead + */ + virtual void removeListenerByData(void *data)=0; + /** + * @brief Generate a sequence of events corresponding to the state of this node + * @deprecated Use synthesizeEvents(NodeObserver &) instead + */ + virtual void synthesizeEvents(NodeEventVector const *vector, void *data)=0; + + virtual void recursivePrintTree(unsigned level)=0; + + /*@}*/ + +protected: + Node(Node const &) : Anchored() {} + + virtual void setAttributeImpl(char const *key, char const *value, bool is_interactive)=0; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/pi-node.h b/src/xml/pi-node.h new file mode 100644 index 0000000..2ec7942 --- /dev/null +++ b/src/xml/pi-node.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Processing instruction node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2004-2005 MenTaLguY <mental@rydia.net> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_PI_NODE_H +#define SEEN_INKSCAPE_XML_PI_NODE_H + +#include "xml/simple-node.h" + +namespace Inkscape { + +namespace XML { + +/** + * @brief Processing instruction node, e.g. <?xml version="1.0" encoding="utf-8" standalone="no"?> + */ +struct PINode : public SimpleNode { + PINode(GQuark target, Util::ptr_shared content, Document *doc) + : SimpleNode(target, doc) + { + setContent(content); + } + PINode(PINode const &other, Document *doc) + : SimpleNode(other, doc) {} + + Inkscape::XML::NodeType type() const override { return Inkscape::XML::PI_NODE; } + +protected: + SimpleNode *_duplicate(Document* doc) const override { return new PINode(*this, doc); } +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/quote-test.h b/src/xml/quote-test.h new file mode 100644 index 0000000..7e08b8d --- /dev/null +++ b/src/xml/quote-test.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * Initial author: Peter Moulder. + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> +#include "streq.h" + + +#include <cstring> +#include <functional> + +#include "quote.h" + +class XmlQuoteTest : public CxxTest::TestSuite +{ +public: + + XmlQuoteTest() + { + } + virtual ~XmlQuoteTest() {} + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static XmlQuoteTest *createSuite() { return new XmlQuoteTest(); } + static void destroySuite( XmlQuoteTest *suite ) { delete suite; } + + void testXmlQuotedStrlen() + { + struct { + char const *s; + size_t len; + } cases[] = { + {"", 0}, + {"x", 1}, + {"Foo", 3}, + {"\"", 6}, + {"&", 5}, + {"<", 4}, + {">", 4}, + {"a\"b", 8}, + {"a\"b<c>d;!@#$%^*(\\)?", 30} + }; + for(size_t i=0; i<G_N_ELEMENTS(cases); i++) { + TS_ASSERT_EQUALS( xml_quoted_strlen(cases[i].s) , cases[i].len ); + } + } + + void testXmlQuoteStrdup() + { + struct { + char const * s1; + char const * s2; + } cases[] = { + {"", ""}, + {"x", "x"}, + {"Foo", "Foo"}, + {"\"", """}, + {"&", "&"}, + {"<", "<"}, + {">", ">"}, + {"a\"b<c>d;!@#$%^*(\\)?", "a"b<c>d;!@#$%^*(\\)?"} + }; + for(size_t i=0; i<G_N_ELEMENTS(cases); i++) { + char* str = xml_quote_strdup(cases[i].s1); + TS_ASSERT_RELATION( streq_rel, cases[i].s2, str ); + g_free(str); + } + } +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/quote.cpp b/src/xml/quote.cpp new file mode 100644 index 0000000..148cb9d --- /dev/null +++ b/src/xml/quote.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief XML quoting routines + *//* + * Authors: + * see git history + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2015 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/quote.h" +#include <cstring> +#include <glib.h> + +/// Returns the length of the string after quoting the characters <code>"&<></code>. +size_t xml_quoted_strlen(char const *val) +{ + if (!val) return 0; + size_t len = 0; + + for (char const *valp = val; *valp; ++valp) { + switch (*valp) { + case '"': + len += 6; // " + break; + case '&': + len += 5; // & + break; + case '<': + case '>': + len += 4; // < or > + break; + default: + ++len; + break; + } + } + return len; +} + +char *xml_quote_strdup(char const *src) +{ + size_t len = xml_quoted_strlen(src); + char *result = static_cast<char*>(g_malloc(len + 1)); + char *resp = result; + + for (char const *srcp = src; *srcp; ++srcp) { + switch(*srcp) { + case '"': + strcpy(resp, """); + resp += 6; + break; + case '&': + strcpy(resp, "&"); + resp += 5; + break; + case '<': + strcpy(resp, "<"); + resp += 4; + break; + case '>': + strcpy(resp, ">"); + resp += 4; + break; + default: + *resp++ = *srcp; + break; + } + } + *resp = 0; + return result; +} + +// quote: ", &, <, > + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/quote.h b/src/xml/quote.h new file mode 100644 index 0000000..a2e5950 --- /dev/null +++ b/src/xml/quote.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_XML_QUOTE_H +#define SEEN_XML_QUOTE_H + +#include <cstddef> + +size_t xml_quoted_strlen(char const *val); +char *xml_quote_strdup(char const *src); + + +#endif /* !SEEN_XML_QUOTE_H */ diff --git a/src/xml/rebase-hrefs.cpp b/src/xml/rebase-hrefs.cpp new file mode 100644 index 0000000..b6f7e55 --- /dev/null +++ b/src/xml/rebase-hrefs.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/convert.h> +#include <glibmm/miscutils.h> +#include <glibmm/uriutils.h> + +#include "../document.h" /* Unfortunately there's a separate xml/document.h. */ +#include "streq.h" + +#include "io/dir-util.h" +#include "io/sys.h" + +#include "object/sp-object.h" +#include "object/uri.h" + +#include "xml/node.h" +#include "xml/rebase-hrefs.h" + +using Inkscape::XML::AttributeRecord; + +/** + * Determine if a href needs rebasing. + */ +static bool href_needs_rebasing(std::string const &href) +{ + bool ret = true; + + if ( href.empty() || (href[0] == '#') ) { + ret = false; + /* False (no change) is the right behaviour even when the base URI differs from the + * document URI: RFC 3986 defines empty string relative URL as referring to the containing + * document, rather than referring to the base URI. */ + } else { + /* Don't change data or http hrefs. */ + std::string scheme = Glib::uri_parse_scheme(href); + if ( !scheme.empty() ) { + /* Assume it shouldn't be changed. This is probably wrong if the scheme is `file' + * (or if the scheme of the new base is non-file, though I believe that never + * happens at the time of writing), but that's rare, and we won't try too hard to + * handle this now: wait until after the freeze, then add liburiparser (or similar) + * as a dependency and do it properly. For now we'll just try to be simple (while + * at least still correctly handling data hrefs). */ + ret = false; + } + } + + return ret; +} + +Inkscape::Util::List<AttributeRecord const> +Inkscape::XML::rebase_href_attrs(gchar const *const old_abs_base, + gchar const *const new_abs_base, + Inkscape::Util::List<AttributeRecord const> attributes) +{ + using Inkscape::Util::List; + using Inkscape::Util::cons; + using Inkscape::Util::ptr_shared; + using Inkscape::Util::share_string; + + + if (old_abs_base == new_abs_base) { + return attributes; + } + + GQuark const href_key = g_quark_from_static_string("xlink:href"); + GQuark const absref_key = g_quark_from_static_string("sodipodi:absref"); + + /* First search attributes for xlink:href and sodipodi:absref, putting the rest in ret. + * + * However, if we find that xlink:href doesn't need rebasing, then return immediately + * with no change to attributes. */ + ptr_shared old_href; + ptr_shared sp_absref; + List<AttributeRecord const> ret; + { + for (List<AttributeRecord const> ai(attributes); ai; ++ai) { + if (ai->key == href_key) { + old_href = ai->value; + if (!href_needs_rebasing(static_cast<char const *>(old_href))) { + return attributes; + } + } else if (ai->key == absref_key) { + sp_absref = ai->value; + } else { + ret = cons(AttributeRecord(ai->key, ai->value), ret); + } + } + } + + if (!old_href) { + return attributes; + /* We could instead return ret in this case, i.e. ensure that sodipodi:absref is cleared if + * no xlink:href attribute. However, retaining it might be more cautious. + * + * (For the usual case of not present, attributes and ret will be the same except + * reversed.) */ + } + + auto uri = URI::from_href_and_basedir(static_cast<char const *>(old_href), old_abs_base); + auto abs_href = uri.toNativeFilename(); + + if (!Inkscape::IO::file_test(abs_href.c_str(), G_FILE_TEST_EXISTS) && + Inkscape::IO::file_test(sp_absref, G_FILE_TEST_EXISTS)) { + uri = URI::from_native_filename(sp_absref); + } + + std::string baseuri; + if (new_abs_base && new_abs_base[0]) { + baseuri = URI::from_dirname(new_abs_base).str(); + } + + auto new_href = uri.str(baseuri.c_str()); + + ret = cons(AttributeRecord(href_key, share_string(new_href.c_str())), ret); // Check if this is safe/copied or if it is only held. + if (sp_absref) { + /* We assume that if there wasn't previously a sodipodi:absref attribute + * then we shouldn't create one. */ + ret = cons(AttributeRecord(absref_key, ( streq(abs_href.c_str(), sp_absref) + ? sp_absref + : share_string(abs_href.c_str()) )), + ret); + } + + return ret; +} + +void Inkscape::XML::rebase_hrefs(SPDocument *const doc, gchar const *const new_base, bool const spns) +{ + using Inkscape::URI; + + std::string old_base_url_str = URI::from_dirname(doc->getDocumentBase()).str(); + std::string new_base_url_str; + + if (new_base) { + new_base_url_str = URI::from_dirname(new_base).str(); + } + + /* TODO: Should handle not just image but also: + * + * a, altGlyph, animElementAttrs, animate, animateColor, animateMotion, animateTransform, + * animation, audio, color-profile, cursor, definition-src, discard, feImage, filter, + * font-face-uri, foreignObject, glyphRef, handler, linearGradient, mpath, pattern, + * prefetch, radialGradient, script, set, textPath, tref, use, video + * + * (taken from the union of the xlink:href elements listed at + * http://www.w3.org/TR/SVG11/attindex.html and + * http://www.w3.org/TR/SVGMobile12/attributeTable.html). + * + * Also possibly some other attributes of type <URI> or <IRI> or list-thereof, or types like + * <paint> that can include an IRI/URI, and stylesheets and style attributes. (xlink:base is a + * special case. xlink:role and xlink:arcrole can be assumed to be already absolute, based on + * http://www.w3.org/TR/SVG11/struct.html#xlinkRefAttrs .) + * + * Note that it may not useful to set sodipodi:absref for anything other than image. + * + * Note also that Inkscape only supports fragment hrefs (href="#pattern257") for many of these + * cases. */ + std::vector<SPObject *> images = doc->getResourceList("image"); + for (auto image : images) { + Inkscape::XML::Node *ir = image->getRepr(); + + auto href_cstr = ir->attribute("xlink:href"); + if (!href_cstr) { + continue; + } + + // skip fragment URLs + if (href_cstr[0] == '#') { + continue; + } + + // make absolute + URI url; + try { + url = URI(href_cstr, old_base_url_str.c_str()); + } catch (...) { + continue; + } + + // skip non-file URLs + if (!url.hasScheme("file")) { + continue; + } + + // if path doesn't exist, use sodipodi:absref + if (!g_file_test(url.toNativeFilename().c_str(), G_FILE_TEST_EXISTS)) { + auto spabsref = ir->attribute("sodipodi:absref"); + if (spabsref && g_file_test(spabsref, G_FILE_TEST_EXISTS)) { + url = URI::from_native_filename(spabsref); + } + } else if (spns) { + ir->setAttributeOrRemoveIfEmpty("sodipodi:absref", url.toNativeFilename()); + } + + if (!spns) { + ir->removeAttribute("sodipodi:absref"); + } + + auto href_str = url.str(new_base_url_str.c_str()); + href_str = Inkscape::uri_to_iri(href_str.c_str()); + + ir->setAttribute("xlink:href", href_str); + } + + doc->setDocumentBase(new_base); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 : diff --git a/src/xml/rebase-hrefs.h b/src/xml/rebase-hrefs.h new file mode 100644 index 0000000..afab3e4 --- /dev/null +++ b/src/xml/rebase-hrefs.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2014 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef REBASE_HREFS_H_SEEN +#define REBASE_HREFS_H_SEEN + +#include "util/list.h" +#include "xml/attribute-record.h" +class SPDocument; + +namespace Inkscape { +namespace XML { + +/** + * Change relative hrefs in doc to be relative to \a new_base instead of doc.base. + * + * (NULL doc base or new_base is interpreted as current working directory.) + * + * @param spns True if doc should contain sodipodi:absref attributes. + */ +void rebase_hrefs(SPDocument *doc, char const *new_base, bool spns); + +/** + * Change relative xlink:href attributes to be relative to \a new_abs_base instead of old_abs_base. + * + * Note that old_abs_base and new_abs_base must each be non-NULL, absolute directory paths. + */ +Inkscape::Util::List<AttributeRecord const> rebase_href_attrs( + char const *old_abs_base, + char const *new_abs_base, + Inkscape::Util::List<AttributeRecord const> attributes); + + +// /** +// * . +// * @return a non-empty replacement href if needed, empty otherwise. +// */ +// std::string rebase_href_attrs( std::string const &oldAbsBase, std::string const &newAbsBase, gchar const *href, gchar const *absref = 0 ); + +} // namespace XML +} // namespace Inkscape + + +#endif /* !REBASE_HREFS_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 : diff --git a/src/xml/repr-action-test.h b/src/xml/repr-action-test.h new file mode 100644 index 0000000..29df246 --- /dev/null +++ b/src/xml/repr-action-test.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <cxxtest/TestSuite.h> + +#include <cstdlib> +#include <glib.h> + +#include "repr.h" +#include "event-fns.h" + +static void * const null_ptr = 0; + +class XmlReprActionTest : public CxxTest::TestSuite +{ + Inkscape::XML::Document *document; + Inkscape::XML::Node *a, *b, *c, *root; + +public: + + XmlReprActionTest() + { + Inkscape::GC::init(); + + document = sp_repr_document_new("test"); + root = document->root(); + + a = document->createElement("a"); + b = document->createElement("b"); + c = document->createElement("c"); + } + virtual ~XmlReprActionTest() {} + +// createSuite and destroySuite get us per-suite setup and teardown +// without us having to worry about static initialization order, etc. + static XmlReprActionTest *createSuite() { return new XmlReprActionTest(); } + static void destroySuite( XmlReprActionTest *suite ) { delete suite; } + + void testRollbackOfNodeAddition() + { + sp_repr_begin_transaction(document); + TS_ASSERT_EQUALS(a->parent() , null_ptr); + + root->appendChild(a); + TS_ASSERT_EQUALS(a->parent() , root); + + sp_repr_rollback(document); + TS_ASSERT_EQUALS(a->parent() , null_ptr); + } + + void testRollbackOfNodeRemoval() + { + root->appendChild(a); + + sp_repr_begin_transaction(document); + TS_ASSERT_EQUALS(a->parent() , root); + + sp_repr_unparent(a); + TS_ASSERT_EQUALS(a->parent() , null_ptr); + + sp_repr_rollback(document); + TS_ASSERT_EQUALS(a->parent() , root); + + sp_repr_unparent(a); + } + + void testRollbackOfNodeReordering() + { + root->appendChild(a); + root->appendChild(b); + root->appendChild(c); + + sp_repr_begin_transaction(document); + TS_ASSERT_EQUALS(a->next() , b); + TS_ASSERT_EQUALS(b->next() , c); + TS_ASSERT_EQUALS(c->next() , null_ptr); + + root->changeOrder(b, c); + TS_ASSERT_EQUALS(a->next() , c); + TS_ASSERT_EQUALS(b->next() , null_ptr); + TS_ASSERT_EQUALS(c->next() , b); + + sp_repr_rollback(document); + TS_ASSERT_EQUALS(a->next() , b); + TS_ASSERT_EQUALS(b->next() , c); + TS_ASSERT_EQUALS(c->next() , null_ptr); + + sp_repr_unparent(a); + sp_repr_unparent(b); + sp_repr_unparent(c); + } + + /* lots more tests needed ... */ +}; + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr-css.cpp b/src/xml/repr-css.cpp new file mode 100644 index 0000000..f1b1e0c --- /dev/null +++ b/src/xml/repr-css.cpp @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * bulia byak <buliabyak@users.sf.net> + * Tavmjong Bah <tavmjong@free.fr> (Documentation) + * + * Functions to manipulate SPCSSAttr which is a class derived from Inkscape::XML::Node See + * sp-css-attr.h and node.h + * + * SPCSSAttr is a special node type where the "attributes" are the properties in an element's style + * attribute. For example, style="fill:blue;stroke:none" is stored in a List (Inkscape::Util:List) + * where the key is the property (e.g. "fill" or "stroke") and the value is the property's value + * (e.g. "blue" or "none"). An element's properties are manipulated by adding, removing, or + * changing an item in the List. Utility functions are provided to go back and forth between the + * two ways of representing properties (by a string or by a list). + * + * Use sp_repr_css_write_string to go from a property list to a style string. + * + */ + +#define SP_REPR_CSS_C + +#include <cstring> +#include <string> +#include <sstream> + +#include <glibmm/ustring.h> + +#include "3rdparty/libcroco/cr-declaration.h" + +#include "svg/css-ostringstream.h" + +#include "xml/repr.h" +#include "xml/simple-document.h" +#include "xml/sp-css-attr.h" + +using Inkscape::Util::List; +using Inkscape::XML::AttributeRecord; +using Inkscape::XML::SimpleNode; +using Inkscape::XML::Node; +using Inkscape::XML::NodeType; +using Inkscape::XML::Document; + +struct SPCSSAttrImpl : public SimpleNode, public SPCSSAttr { +public: + SPCSSAttrImpl(Document *doc) + : SimpleNode(g_quark_from_static_string("css"), doc) {} + SPCSSAttrImpl(SPCSSAttrImpl const &other, Document *doc) + : SimpleNode(other, doc) {} + + NodeType type() const override { return Inkscape::XML::ELEMENT_NODE; } + +protected: + SimpleNode *_duplicate(Document* doc) const override { return new SPCSSAttrImpl(*this, doc); } +}; + +static void sp_repr_css_add_components(SPCSSAttr *css, Node const *repr, gchar const *attr); + +/** + * Creates an empty SPCSSAttr (a class for manipulating CSS style properties). + */ +SPCSSAttr *sp_repr_css_attr_new() +{ + static Inkscape::XML::Document *attr_doc=nullptr; + if (!attr_doc) { + attr_doc = new Inkscape::XML::SimpleDocument(); + } + return new SPCSSAttrImpl(attr_doc); +} + +/** + * Unreferences an SPCSSAttr (will be garbage collected if no references remain). + */ +void sp_repr_css_attr_unref(SPCSSAttr *css) +{ + g_assert(css != nullptr); + Inkscape::GC::release((Node *) css); +} + +/** + * Creates a new SPCSSAttr with one attribute (i.e. style) copied from an existing repr (node). The + * repr attribute data is in the form of a char const * string (e.g. fill:#00ff00;stroke:none). The + * string is parsed by libcroco which returns a CRDeclaration list (a typical C linked list) of + * properties and values. This list is then used to fill the attributes of the new SPCSSAttr. + */ +SPCSSAttr *sp_repr_css_attr(Node const *repr, gchar const *attr) +{ + g_assert(repr != nullptr); + g_assert(attr != nullptr); + + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_add_components(css, repr, attr); + return css; +} + + +/** + * Attempt to parse the passed string as a hexadecimal RGB or RGBA color. + * @param text The Glib::ustring to parse + * @return New CSS style representation if the parsing was successful, NULL otherwise + */ +SPCSSAttr *sp_repr_css_attr_parse_color_to_fill(const Glib::ustring &text) +{ +// TODO reuse existing code instead of replicating here. + Glib::ustring::size_type len = text.bytes(); + char *str = const_cast<char *>(text.data()); + bool attempt_alpha = false; + if ( !str || ( *str == '\0' ) ) { + return nullptr; // this is OK due to boolean short-circuit + } + + // those conditionals guard against parsing e.g. the string "fab" as "fab000" + // (incomplete color) and "45fab71" as "45fab710" (incomplete alpha) + if ( *str == '#' ) { + if ( len < 7 ) { + return nullptr; + } + if ( len >= 9 ) { + attempt_alpha = true; + } + } else { + if ( len < 6 ) { + return nullptr; + } + if ( len >= 8 ) { + attempt_alpha = true; + } + } + + unsigned int color = 0, alpha = 0xff; + + // skip a leading #, if present + if ( *str == '#' ) { + ++str; + } + + // try to parse first 6 digits + int res = sscanf(str, "%6x", &color); + if ( res && ( res != EOF ) ) { + if (attempt_alpha) {// try to parse alpha if there's enough characters + sscanf(str + 6, "%2x", &alpha); + if ( !res || res == EOF ) { + alpha = 0xff; + } + } + + SPCSSAttr *color_css = sp_repr_css_attr_new(); + + // print and set properties + gchar color_str[16]; + g_snprintf(color_str, 16, "#%06x", color); + sp_repr_css_set_property(color_css, "fill", color_str); + + float opacity = static_cast<float>(alpha)/static_cast<float>(0xff); + if (opacity > 1.0) { + opacity = 1.0; // safeguard + } + Inkscape::CSSOStringStream opcss; + opcss << opacity; + sp_repr_css_set_property(color_css, "fill-opacity", opcss.str().data()); + return color_css; + } + return nullptr; +} + + +/** + * Adds an attribute to an existing SPCSAttr with the cascaded value including all parents. + */ +static void sp_repr_css_attr_inherited_recursive(SPCSSAttr *css, Node const *repr, gchar const *attr) +{ + const Node *parent = repr->parent(); + + // read the ancestors from root down, using head recursion, so that children override parents + if (parent) { + sp_repr_css_attr_inherited_recursive(css, parent, attr); + } + sp_repr_css_add_components(css, repr, attr); +} + +/** + * Creates a new SPCSSAttr with one attribute whose value is determined by cascading. + */ +SPCSSAttr *sp_repr_css_attr_inherited(Node const *repr, gchar const *attr) +{ + g_assert(repr != nullptr); + g_assert(attr != nullptr); + + SPCSSAttr *css = sp_repr_css_attr_new(); + + sp_repr_css_attr_inherited_recursive(css, repr, attr); + + return css; +} + +/** + * Adds components (style properties) to an existing SPCSAttr from the specified attribute's data + * (nominally a style attribute). + * + */ +static void sp_repr_css_add_components(SPCSSAttr *css, Node const *repr, gchar const *attr) +{ + g_assert(css != nullptr); + g_assert(repr != nullptr); + g_assert(attr != nullptr); + + char const *data = repr->attribute(attr); + sp_repr_css_attr_add_from_string(css, data); +} + +/** + * Returns a character string of the value of a given style property or a default value if the + * attribute is not found. + */ +char const *sp_repr_css_property(SPCSSAttr *css, gchar const *name, gchar const *defval) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + char const *attr = ((Node *)css)->attribute(name); + return ( attr == nullptr + ? defval + : attr ); +} + +/** + * Returns a character string of the value of a given style property or a default value if the + * attribute is not found. + */ +Glib::ustring sp_repr_css_property(SPCSSAttr *css, Glib::ustring const &name, Glib::ustring const &defval) +{ + g_assert(css != nullptr); + + Glib::ustring retval = defval; + char const *attr = ((Node *)css)->attribute(name.c_str()); + if (attr) { + retval = attr; + } + + return retval; +} + +/** + * Returns true if a style property is present and its value is unset. + */ +bool sp_repr_css_property_is_unset(SPCSSAttr *css, gchar const *name) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + char const *attr = ((Node *)css)->attribute(name); + return (attr && !strcmp(attr, "inkscape:unset")); +} + + +/** + * Set a style property to a new value (e.g. fill to #ffff00). + */ +void sp_repr_css_set_property(SPCSSAttr *css, gchar const *name, gchar const *value) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + ((Node *) css)->setAttribute(name, value); +} + +/** + * Set a style property to "inkscape:unset". + */ +void sp_repr_css_unset_property(SPCSSAttr *css, gchar const *name) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + ((Node *) css)->setAttribute(name, "inkscape:unset"); +} + +/** + * Return the value of a style property if property define, or a default value if not. + */ +double sp_repr_css_double_property(SPCSSAttr *css, gchar const *name, double defval) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + double val = defval; + sp_repr_get_double((Node *) css, name, &val); + return val; +} + +/** + * Write a style attribute string from a list of properties stored in an SPCSAttr object. + */ +void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str) +{ + str.clear(); + for ( List<AttributeRecord const> iter = css->attributeList() ; + iter ; ++iter ) + { + if (iter->value && !strcmp(iter->value, "inkscape:unset")) { + continue; + } + + str.append(g_quark_to_string(iter->key)); + str.push_back(':'); + str.append(iter->value); // Any necessary quoting to be done by calling routine. + + if (rest(iter)) { + str.push_back(';'); + } + } +} + +/** + * Sets an attribute (e.g. style) to a string created from a list of style properties. + */ +void sp_repr_css_set(Node *repr, SPCSSAttr *css, gchar const *attr) +{ + g_assert(repr != nullptr); + g_assert(css != nullptr); + g_assert(attr != nullptr); + + Glib::ustring value; + sp_repr_css_write_string(css, value); + + /* + * If the new value is different from the old value, this will sometimes send a signal via + * CompositeNodeObserver::notiftyAttributeChanged() which results in calling + * SPObject::repr_attr_changed and thus updates the object's SPStyle. This update + * results in another call to repr->setAttribute(). + */ + repr->setAttributeOrRemoveIfEmpty(attr, value); +} + +/** + * Loops through a List of style properties, printing key/value pairs. + */ +void sp_repr_css_print(SPCSSAttr *css) +{ + for ( List<AttributeRecord const> iter = css->attributeList() ; + iter ; ++iter ) + { + gchar const * key = g_quark_to_string(iter->key); + gchar const * val = iter->value; + g_print("%s:\t%s\n",key,val); + } +} + +/** + * Merges two SPCSSAttr's. Properties in src overwrite properties in dst if present in both. + */ +void sp_repr_css_merge(SPCSSAttr *dst, SPCSSAttr *src) +{ + g_assert(dst != nullptr); + g_assert(src != nullptr); + + dst->mergeFrom(src, ""); +} + +/** + * Merges style properties as parsed by libcroco into an existing SPCSSAttr. + * libcroco converts all single quotes to double quotes, which needs to be + * undone as we always use single quotes inside our 'style' strings since + * double quotes are used outside: e.g.: + * style="font-family:'DejaVu Sans'" + */ +static void sp_repr_css_merge_from_decl(SPCSSAttr *css, CRDeclaration const *const decl) +{ + guchar *const str_value_unsigned = cr_term_to_string(decl->value); + + Glib::ustring value( reinterpret_cast<gchar *>(str_value_unsigned ) ); + g_free(str_value_unsigned); + + Glib::ustring::size_type pos = 0; + while( (pos=value.find("\"",pos)) != Glib::ustring::npos) { + value.replace(pos,1,"'"); + ++pos; + } + + Glib::ustring units; + + /* + * Problem with parsing of units em and ex, like font-size "1.2em" and "3.4ex" + * stringstream thinks they are in scientific "e" notation and fails + * Must be a better way using std::fixed, precision etc + * + * HACK for now is to strip off em and ex units and add them back at the end + */ + int le = value.length(); + if (le > 2) { + units = value.substr(le-2, 2); + if ((units == "em") || (units == "ex")) { + value = value.substr(0, le-2); + } + else { + units.clear(); + } + } + + // libcroco uses %.17f for formatting... leading to trailing zeros or small rounding errors. + // CSSOStringStream is used here to write valid CSS (as in sp_style_write_string). This has + // the additional benefit of respecting the numerical precision set in the SVG Output + // preferences. We assume any numerical part comes first (if not, the whole string is copied). + std::stringstream ss( value ); + double number = 0; + std::string characters; + std::string temp; + bool number_valid = !(ss >> number).fail(); + if (!number_valid) { + ss.clear(); + ss.seekg(0); // work-around for a bug in libc++ (see lp:1300271) + } + while( !(ss >> temp).eof() ) { + characters += temp; + characters += " "; + } + characters += temp; + Inkscape::CSSOStringStream os; + if( number_valid ) os << number; + os << characters; + if (!units.empty()) { + os << units; + //g_message("sp_repr_css_merge_from_decl looks like em or ex units %s --> %s", str_value, os.str().c_str()); + } + ((Node *) css)->setAttribute(decl->property->stryng->str, os.str()); +} + +/** + * Merges style properties as parsed by libcroco into an existing SPCSSAttr. + * + * \pre decl_list != NULL + */ +static void sp_repr_css_merge_from_decl_list(SPCSSAttr *css, CRDeclaration const *const decl_list) +{ + // read the decls from start to end, using tail recursion, so that latter declarations override + // (Ref: http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order point 4.) + // because sp_repr_css_merge_from_decl sets properties unconditionally + sp_repr_css_merge_from_decl(css, decl_list); + if (decl_list->next) { + sp_repr_css_merge_from_decl_list(css, decl_list->next); + } +} + +/** + * Use libcroco to parse a string for CSS properties and then merge + * them into an existing SPCSSAttr. + */ +void sp_repr_css_attr_add_from_string(SPCSSAttr *css, gchar const *p) +{ + if (p != nullptr) { + CRDeclaration *const decl_list + = cr_declaration_parse_list_from_buf(reinterpret_cast<guchar const *>(p), CR_UTF_8); + if (decl_list) { + sp_repr_css_merge_from_decl_list(css, decl_list); + cr_declaration_destroy(decl_list); + } + } +} + +/** + * Creates a new SPCSAttr with the values filled from a repr, merges in properties from the given + * SPCSAttr, and then replaces that SPCSAttr with the new one. This is called, for example, for + * each object in turn when a selection's style is updated via sp_desktop_set_style(). + */ +void sp_repr_css_change(Node *repr, SPCSSAttr *css, gchar const *attr) +{ + g_assert(repr != nullptr); + g_assert(css != nullptr); + g_assert(attr != nullptr); + + SPCSSAttr *current = sp_repr_css_attr(repr, attr); + sp_repr_css_merge(current, css); + sp_repr_css_set(repr, current, attr); + + sp_repr_css_attr_unref(current); +} + +void sp_repr_css_change_recursive(Node *repr, SPCSSAttr *css, gchar const *attr) +{ + g_assert(repr != nullptr); + g_assert(css != nullptr); + g_assert(attr != nullptr); + + sp_repr_css_change(repr, css, attr); + + for (Node *child = repr->firstChild(); child != nullptr; child = child->next()) { + sp_repr_css_change_recursive(child, css, attr); + } +} + +/** + * Return a new SPCSSAttr with all the properties found in the input SPCSSAttr unset. + */ +SPCSSAttr* sp_repr_css_attr_unset_all(SPCSSAttr *css) +{ + SPCSSAttr* css_unset = sp_repr_css_attr_new(); + for ( List<AttributeRecord const> iter = css->attributeList() ; iter ; ++iter ) { + sp_repr_css_set_property (css_unset, g_quark_to_string(iter->key), "inkscape:unset"); + } + return css_unset; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr-io.cpp b/src/xml/repr-io.cpp new file mode 100644 index 0000000..ac9794e --- /dev/null +++ b/src/xml/repr-io.cpp @@ -0,0 +1,1065 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Dirty DOM-like tree + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <string> +#include <stdexcept> + +#include <libxml/parser.h> + +#include "xml/repr.h" +#include "xml/attribute-record.h" +#include "xml/rebase-hrefs.h" +#include "xml/simple-document.h" +#include "xml/text-node.h" + +#include "io/sys.h" +#include "io/stream/stringstream.h" +#include "io/stream/gzipstream.h" +#include "io/stream/uristream.h" + +#include "extension/extension.h" + +#include "attribute-rel-util.h" +#include "attribute-sort-util.h" + +#include "preferences.h" + +#include <glibmm/miscutils.h> + +using Inkscape::IO::Writer; +using Inkscape::Util::List; +using Inkscape::Util::cons; +using Inkscape::XML::Document; +using Inkscape::XML::SimpleDocument; +using Inkscape::XML::Node; +using Inkscape::XML::AttributeRecord; +using Inkscape::XML::rebase_href_attrs; + +Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns); +static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map); +static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, std::map<std::string, std::string> &prefix_map); +static void sp_repr_write_stream_root_element(Node *repr, Writer &out, + bool add_whitespace, gchar const *default_ns, + int inlineattrs, int indent, + gchar const *old_href_abs_base, + gchar const *new_href_abs_base); + +static void sp_repr_write_stream_element(Node *repr, Writer &out, + gint indent_level, bool add_whitespace, + Glib::QueryQuark elide_prefix, + List<AttributeRecord const> attributes, + int inlineattrs, int indent, + gchar const *old_href_abs_base, + gchar const *new_href_abs_base); + + +class XmlSource +{ +public: + XmlSource() + : filename(nullptr), + encoding(nullptr), + fp(nullptr), + firstFewLen(0), + LoadEntities(false), + cachedData(), + cachedPos(0), + instr(nullptr), + gzin(nullptr) + { + for (unsigned char & k : firstFew) + { + k=0; + } + } + virtual ~XmlSource() + { + close(); + if ( encoding ) { + g_free(encoding); + encoding = nullptr; + } + } + + int setFile( char const * filename, bool load_entities ); + + xmlDocPtr readXml(); + + static int readCb( void * context, char * buffer, int len ); + static int closeCb( void * context ); + + char const* getEncoding() const { return encoding; } + int read( char * buffer, int len ); + int close(); +private: + const char* filename; + char* encoding; + FILE* fp; + unsigned char firstFew[4]; + int firstFewLen; + bool LoadEntities; // Checks for SYSTEM Entities (requires cached data) + std::string cachedData; + unsigned int cachedPos; + Inkscape::IO::FileInputStream* instr; + Inkscape::IO::GzipInputStream* gzin; +}; + +int XmlSource::setFile(char const *filename, bool load_entities=false) +{ + int retVal = -1; + + this->filename = filename; + + fp = Inkscape::IO::fopen_utf8name(filename, "r"); + if ( fp ) { + // First peek in the file to see what it is + memset( firstFew, 0, sizeof(firstFew) ); + + size_t some = fread( firstFew, 1, 4, fp ); + if ( fp ) { + // first check for compression + if ( (some >= 2) && (firstFew[0] == 0x1f) && (firstFew[1] == 0x8b) ) { + //g_message(" the file being read is gzip'd. extract it"); + fclose(fp); + fp = nullptr; + fp = Inkscape::IO::fopen_utf8name(filename, "r"); + instr = new Inkscape::IO::FileInputStream(fp); + gzin = new Inkscape::IO::GzipInputStream(*instr); + + memset( firstFew, 0, sizeof(firstFew) ); + some = 0; + int single = 0; + while ( some < 4 && single >= 0 ) + { + single = gzin->get(); + if ( single >= 0 ) { + firstFew[some++] = 0x0ff & single; + } else { + break; + } + } + } + + int encSkip = 0; + if ( (some >= 2) &&(firstFew[0] == 0xfe) && (firstFew[1] == 0xff) ) { + encoding = g_strdup("UTF-16BE"); + encSkip = 2; + } else if ( (some >= 2) && (firstFew[0] == 0xff) && (firstFew[1] == 0xfe) ) { + encoding = g_strdup("UTF-16LE"); + encSkip = 2; + } else if ( (some >= 3) && (firstFew[0] == 0xef) && (firstFew[1] == 0xbb) && (firstFew[2] == 0xbf) ) { + encoding = g_strdup("UTF-8"); + encSkip = 3; + } + + if ( encSkip ) { + memmove( firstFew, firstFew + encSkip, (some - encSkip) ); + some -= encSkip; + } + + firstFewLen = some; + retVal = 0; // no error + } + } + if(load_entities) { + this->cachedData = std::string(""); + this->cachedPos = 0; + + // First get data from file in typical way (cache it all) + char *buffer = new char [4096]; + while(true) { + int len = this->read(buffer, 4096); + if(len <= 0) break; + buffer[len] = 0; + this->cachedData += buffer; + } + delete[] buffer; + + // Check for SYSTEM or PUBLIC entities and remove them from the cache + GMatchInfo *info; + gint start, end; + + GRegex *regex = g_regex_new( + "<!ENTITY\\s+[^>\\s]+\\s+(SYSTEM|PUBLIC\\s+\"[^>\"]+\")\\s+\"[^>\"]+\"\\s*>", + G_REGEX_CASELESS, G_REGEX_MATCH_NEWLINE_ANY, nullptr); + + g_regex_match (regex, this->cachedData.c_str(), G_REGEX_MATCH_NEWLINE_ANY, &info); + + while (g_match_info_matches (info)) { + if (g_match_info_fetch_pos (info, 1, &start, &end)) + this->cachedData.erase(start, end - start); + g_match_info_next (info, nullptr); + } + g_match_info_free(info); + g_regex_unref(regex); + } + // Do this after loading cache, so reads don't return cache to fill cache. + this->LoadEntities = load_entities; + return retVal; +} + +xmlDocPtr XmlSource::readXml() +{ + int parse_options = XML_PARSE_HUGE | XML_PARSE_RECOVER; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool allowNetAccess = prefs->getBool("/options/externalresources/xml/allow_net_access", false); + if (!allowNetAccess) parse_options |= XML_PARSE_NONET; + + // Allow NOENT only if we're filtering out SYSTEM and PUBLIC entities + if (LoadEntities) parse_options |= XML_PARSE_NOENT; + + return xmlReadIO( readCb, closeCb, this, + filename, getEncoding(), parse_options); +} + +int XmlSource::readCb( void * context, char * buffer, int len ) +{ + int retVal = -1; + + if ( context ) { + XmlSource* self = static_cast<XmlSource*>(context); + retVal = self->read( buffer, len ); + } + return retVal; +} + +int XmlSource::closeCb(void * context) +{ + if ( context ) { + XmlSource* self = static_cast<XmlSource*>(context); + self->close(); + } + return 0; +} + +int XmlSource::read( char *buffer, int len ) +{ + int retVal = 0; + size_t got = 0; + + if ( LoadEntities ) { + if (cachedPos >= cachedData.length()) { + return -1; + } else { + retVal = cachedData.copy(buffer, len, cachedPos); + cachedPos += retVal; + return retVal; // Do NOT continue. + } + } else if ( firstFewLen > 0 ) { + int some = (len < firstFewLen) ? len : firstFewLen; + memcpy( buffer, firstFew, some ); + if ( len < firstFewLen ) { + memmove( firstFew, firstFew + some, (firstFewLen - some) ); + } + firstFewLen -= some; + got = some; + } else if ( gzin ) { + int single = 0; + while ( (static_cast<int>(got) < len) && (single >= 0) ) + { + single = gzin->get(); + if ( single >= 0 ) { + buffer[got++] = 0x0ff & single; + } else { + break; + } + } + } else { + got = fread( buffer, 1, len, fp ); + } + + if ( feof(fp) ) { + retVal = got; + } else if ( ferror(fp) ) { + retVal = -1; + } else { + retVal = got; + } + + return retVal; +} + +int XmlSource::close() +{ + if ( gzin ) { + gzin->close(); + delete gzin; + gzin = nullptr; + } + if ( instr ) { + instr->close(); + fp = nullptr; + delete instr; + instr = nullptr; + } + if ( fp ) { + fclose(fp); + fp = nullptr; + } + return 0; +} + +/** + * Reads XML from a file, and returns the Document. + * The default namespace can also be specified, if desired. + */ +Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns) +{ + xmlDocPtr doc = nullptr; + Document * rdoc = nullptr; + + xmlSubstituteEntitiesDefault(1); + + g_return_val_if_fail(filename != nullptr, NULL); + if (!Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) { + g_warning("Can't open file: %s (doesn't exist)", filename); + return nullptr; + } + /* fixme: A file can disappear at any time, including between now and when we actually try to + * open it. Get rid of the above test once we're sure that we correctly handle + * non-existence. */ + + // TODO: bulia, please look over + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError* error = nullptr; + // TODO: need to replace with our own fopen and reading + gchar* localFilename = g_filename_from_utf8(filename, -1, &bytesRead, &bytesWritten, &error); + g_return_val_if_fail(localFilename != nullptr, NULL); + + Inkscape::IO::dump_fopen_call(filename, "N"); + + XmlSource src; + + if (src.setFile(filename) == 0) { + doc = src.readXml(); + rdoc = sp_repr_do_read(doc, default_ns); + // For some reason, failed ns loading results in this + // We try a system check version of load with NOENT for adobe + if (rdoc && strcmp(rdoc->root()->name(), "ns:svg") == 0) { + xmlFreeDoc(doc); + src.setFile(filename, true); + doc = src.readXml(); + rdoc = sp_repr_do_read(doc, default_ns); + } + } + + if (doc) { + xmlFreeDoc(doc); + } + + if (localFilename) { + g_free(localFilename); + } + + return rdoc; +} + +/** + * Reads and parses XML from a buffer, returning it as an Document + */ +Document *sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns) +{ + xmlDocPtr doc; + Document * rdoc; + + xmlSubstituteEntitiesDefault(1); + + g_return_val_if_fail (buffer != nullptr, NULL); + + int parser_options = XML_PARSE_HUGE | XML_PARSE_RECOVER; + parser_options |= XML_PARSE_NONET; // TODO: should we allow network access? + // proper solution would be to check the preference "/options/externalresources/xml/allow_net_access" + // as done in XmlSource::readXml which gets called by the analogous sp_repr_read_file() + // but sp_repr_read_mem() seems to be called in locations where Inkscape::Preferences::get() fails badly + doc = xmlReadMemory (const_cast<gchar *>(buffer), length, nullptr, nullptr, parser_options); + + rdoc = sp_repr_do_read (doc, default_ns); + if (doc) { + xmlFreeDoc (doc); + } + return rdoc; +} + +/** + * Reads and parses XML from a buffer, returning it as an Document + */ +Document *sp_repr_read_buf (const Glib::ustring &buf, const gchar *default_ns) +{ + return sp_repr_read_mem(buf.c_str(), buf.size(), default_ns); +} + + +namespace Inkscape { + +struct compare_quark_ids { + bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const { + return a.id() < b.id(); + } +}; + +} + +namespace { + +typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap; + +Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) { + static PrefixMap prefix_map; + PrefixMap::iterator iter = prefix_map.find(qname); + if ( iter != prefix_map.end() ) { + return (*iter).second; + } else { + gchar const *name_string=g_quark_to_string(qname); + gchar const *prefix_end=strchr(name_string, ':'); + if (prefix_end) { + Glib::Quark prefix=Glib::ustring(name_string, prefix_end); + prefix_map.insert(PrefixMap::value_type(qname, prefix)); + return prefix; + } else { + return GQuark(0); + } + } +} + +} + +namespace { + +void promote_to_namespace(Node *repr, const gchar *prefix) { + if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) { + GQuark code = repr->code(); + if (!qname_prefix(code).id()) { + gchar *svg_name = g_strconcat(prefix, ":", g_quark_to_string(code), NULL); + repr->setCodeUnsafe(g_quark_from_string(svg_name)); + g_free(svg_name); + } + for ( Node *child = repr->firstChild() ; child ; child = child->next() ) { + promote_to_namespace(child, prefix); + } + } +} + +} + +/** + * Reads in a XML file to create a Document + */ +Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns) +{ + if (doc == nullptr) { + return nullptr; + } + xmlNodePtr node=xmlDocGetRootElement (doc); + if (node == nullptr) { + return nullptr; + } + + std::map<std::string, std::string> prefix_map; + + Document *rdoc = new Inkscape::XML::SimpleDocument(); + + Node *root=nullptr; + for ( node = doc->children ; node != nullptr ; node = node->next ) { + if (node->type == XML_ELEMENT_NODE) { + Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map); + rdoc->appendChild(repr); + Inkscape::GC::release(repr); + + if (!root) { + root = repr; + } else { + root = nullptr; + break; + } + } else if ( node->type == XML_COMMENT_NODE || node->type == XML_PI_NODE ) { + Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map); + rdoc->appendChild(repr); + Inkscape::GC::release(repr); + } + } + + if (root != nullptr) { + /* promote elements of some XML documents that don't use namespaces + * into their default namespace */ + if ( default_ns && !strchr(root->name(), ':') ) { + if ( !strcmp(default_ns, SP_SVG_NS_URI) ) { + promote_to_namespace(root, "svg"); + } + if ( !strcmp(default_ns, INKSCAPE_EXTENSION_URI) ) { + promote_to_namespace(root, INKSCAPE_EXTENSION_NS_NC); + } + } + + + // Clean unnecessary attributes and style properties from SVG documents. (Controlled by + // preferences.) Note: internal Inkscape svg files will also be cleaned (filters.svg, + // icons.svg). How can one tell if a file is internal? + if ( !strcmp(root->name(), "svg:svg" ) ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool clean = prefs->getBool("/options/svgoutput/check_on_reading"); + if( clean ) { + sp_attribute_clean_tree( root ); + } + } + } + + return rdoc; +} + +gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar */*default_ns*/, std::map<std::string, std::string> &prefix_map) +{ + const xmlChar *prefix; + if (ns){ + if (ns->href ) { + prefix = reinterpret_cast<const xmlChar*>( sp_xml_ns_uri_prefix(reinterpret_cast<const gchar*>(ns->href), + reinterpret_cast<const char*>(ns->prefix)) ); + prefix_map[reinterpret_cast<const char*>(prefix)] = reinterpret_cast<const char*>(ns->href); + } + else { + prefix = nullptr; + } + } + else { + prefix = nullptr; + } + + if (prefix) { + return g_snprintf (p, len, "%s:%s", reinterpret_cast<const gchar*>(prefix), name); + } else { + return g_snprintf (p, len, "%s", name); + } +} + +static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map) +{ + xmlAttrPtr prop; + xmlNodePtr child; + gchar c[256]; + + if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) { + + if (node->content == nullptr || *(node->content) == '\0') { + return nullptr; // empty text node + } + + // Since libxml2 2.9.0, only element nodes are checked, thus check parent. + // Note: this only handles XML's rules for white space. SVG's specific rules + // are handled in sp-string.cpp. + bool preserve = (xmlNodeGetSpacePreserve (node->parent) == 1); + + xmlChar *p; + for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++) + ; // skip all whitespace + + if (!(*p)) { // this is an all-whitespace node, and preserve == default + return nullptr; // we do not preserve all-whitespace nodes unless we are asked to + } + + // We keep track of original node type so that CDATA sections are preserved on output. + return xml_doc->createTextNode(reinterpret_cast<gchar *>(node->content), + node->type == XML_CDATA_SECTION_NODE ); + } + + if (node->type == XML_COMMENT_NODE) { + return xml_doc->createComment(reinterpret_cast<gchar *>(node->content)); + } + + if (node->type == XML_PI_NODE) { + return xml_doc->createPI(reinterpret_cast<const gchar *>(node->name), + reinterpret_cast<const gchar *>(node->content)); + } + + if (node->type == XML_ENTITY_DECL) { + return nullptr; + } + + sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map); + Node *repr = xml_doc->createElement(c); + /* TODO remember node->ns->prefix if node->ns != NULL */ + + for (prop = node->properties; prop != nullptr; prop = prop->next) { + if (prop->children) { + sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map); + repr->setAttribute(c, reinterpret_cast<gchar*>(prop->children->content)); + /* TODO remember prop->ns->prefix if prop->ns != NULL */ + } + } + + if (node->content) { + repr->setContent(reinterpret_cast<gchar*>(node->content)); + } + + for (child = node->xmlChildrenNode; child != nullptr; child = child->next) { + Node *crepr = sp_repr_svg_read_node (xml_doc, child, default_ns, prefix_map); + if (crepr) { + repr->appendChild(crepr); + Inkscape::GC::release(crepr); + } + } + + return repr; +} + + +static void sp_repr_save_writer(Document *doc, Inkscape::IO::Writer *out, + gchar const *default_ns, + gchar const *old_href_abs_base, + gchar const *new_href_abs_base) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool inlineattrs = prefs->getBool("/options/svgoutput/inlineattrs"); + int indent = prefs->getInt("/options/svgoutput/indent", 2); + + /* fixme: do this The Right Way */ + out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" ); + + const gchar *str = static_cast<Node *>(doc)->attribute("doctype"); + if (str) { + out->writeString( str ); + } + + for (Node *repr = sp_repr_document_first_child(doc); + repr; repr = repr->next()) + { + Inkscape::XML::NodeType const node_type = repr->type(); + if ( node_type == Inkscape::XML::ELEMENT_NODE ) { + sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns, inlineattrs, indent, + old_href_abs_base, new_href_abs_base); + } else { + sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0), inlineattrs, indent, + old_href_abs_base, new_href_abs_base); + if ( node_type == Inkscape::XML::COMMENT_NODE ) { + out->writeChar('\n'); + } + } + } +} + + +Glib::ustring sp_repr_save_buf(Document *doc) +{ + Inkscape::IO::StringOutputStream souts; + Inkscape::IO::OutputStreamWriter outs(souts); + + sp_repr_save_writer(doc, &outs, SP_INKSCAPE_NS_URI, nullptr, nullptr); + + outs.close(); + Glib::ustring buf = souts.getString(); + + return buf; +} + + +void sp_repr_save_stream(Document *doc, FILE *fp, gchar const *default_ns, bool compress, + gchar const *const old_href_abs_base, + gchar const *const new_href_abs_base) +{ + Inkscape::IO::FileOutputStream bout(fp); + Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : nullptr; + Inkscape::IO::OutputStreamWriter *out = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout ); + + sp_repr_save_writer(doc, out, default_ns, old_href_abs_base, new_href_abs_base); + + delete out; + delete gout; +} + + + +/** + * Returns true if file successfully saved. + * + * \param filename The actual file to do I/O to, which might be a temp file. + * + * \param for_filename The base URI [actually filename] to assume for purposes of rewriting + * xlink:href attributes. + */ +bool sp_repr_save_rebased_file(Document *doc, gchar const *const filename, gchar const *default_ns, + gchar const *old_base, gchar const *for_filename) +{ + if (!filename) { + return false; + } + + bool compress; + { + size_t const filename_len = strlen(filename); + compress = ( filename_len > 5 + && strcasecmp(".svgz", filename + filename_len - 5) == 0 ); + } + + Inkscape::IO::dump_fopen_call( filename, "B" ); + FILE *file = Inkscape::IO::fopen_utf8name(filename, "w"); + if (file == nullptr) { + return false; + } + + Glib::ustring old_href_abs_base; + Glib::ustring new_href_abs_base; + + if (old_base) { + old_href_abs_base = old_base; + if (!Glib::path_is_absolute(old_href_abs_base)) { + old_href_abs_base = Glib::build_filename(Glib::get_current_dir(), old_href_abs_base); + } + } + + if (for_filename) { + if (Glib::path_is_absolute(for_filename)) { + new_href_abs_base = Glib::path_get_dirname(for_filename); + } else { + Glib::ustring const cwd = Glib::get_current_dir(); + Glib::ustring const for_abs_filename = Glib::build_filename(cwd, for_filename); + new_href_abs_base = Glib::path_get_dirname(for_abs_filename); + } + + /* effic: Once we're confident that we never need (or never want) to resort + * to using sodipodi:absref instead of the xlink:href value, + * then we should do `if streq() { free them and set both to NULL; }'. */ + } + sp_repr_save_stream(doc, file, default_ns, compress, old_href_abs_base.c_str(), new_href_abs_base.c_str()); + + if (fclose (file) != 0) { + return false; + } + + return true; +} + +/** + * Returns true iff file successfully saved. + */ +bool sp_repr_save_file(Document *doc, gchar const *const filename, gchar const *default_ns) +{ + return sp_repr_save_rebased_file(doc, filename, default_ns, nullptr, nullptr); +} + + +/* (No doubt this function already exists elsewhere.) */ +static void repr_quote_write (Writer &out, const gchar * val) +{ + if (val) { + for (; *val != '\0'; val++) { + switch (*val) { + case '"': out.writeString( """ ); break; + case '&': out.writeString( "&" ); break; + case '<': out.writeString( "<" ); break; + case '>': out.writeString( ">" ); break; + default: out.writeChar( *val ); break; + } + } + } +} + +static void repr_write_comment( Writer &out, const gchar * val, bool addWhitespace, gint indentLevel, int indent ) +{ + if ( indentLevel > 16 ) { + indentLevel = 16; + } + if (addWhitespace && indent) { + for (gint i = 0; i < indentLevel; i++) { + for (gint j = 0; j < indent; j++) { + out.writeChar(' '); + } + } + } + + out.printf("<!--%s-->", val); + + if (addWhitespace) { + out.writeChar('\n'); + } +} + +namespace { + +typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap; +typedef std::map<Glib::QueryQuark, Inkscape::Util::ptr_shared, Inkscape::compare_quark_ids> NSMap; + +gchar const *qname_local_name(Glib::QueryQuark qname) { + static LocalNameMap local_name_map; + LocalNameMap::iterator iter = local_name_map.find(qname); + if ( iter != local_name_map.end() ) { + return (*iter).second; + } else { + gchar const *name_string=g_quark_to_string(qname); + gchar const *prefix_end=strchr(name_string, ':'); + if (prefix_end) { + return prefix_end + 1; + } else { + return name_string; + } + } +} + +void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) { + using Inkscape::Util::ptr_shared; + using Inkscape::Util::share_unsafe; + + static const Glib::QueryQuark xml_prefix("xml"); + + NSMap::iterator iter=ns_map.find(prefix); + if ( iter == ns_map.end() ) { + if (prefix.id()) { + gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix)); + if (uri) { + ns_map.insert(NSMap::value_type(prefix, share_unsafe(uri))); + } else if ( prefix != xml_prefix ) { + g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix)); + } + } else { + ns_map.insert(NSMap::value_type(prefix, ptr_shared())); + } + } +} + +void populate_ns_map(NSMap &ns_map, Node &repr) { + if ( repr.type() == Inkscape::XML::ELEMENT_NODE ) { + add_ns_map_entry(ns_map, qname_prefix(repr.code())); + for ( List<AttributeRecord const> iter=repr.attributeList() ; + iter ; ++iter ) + { + Glib::QueryQuark prefix=qname_prefix(iter->key); + if (prefix.id()) { + add_ns_map_entry(ns_map, prefix); + } + } + for ( Node *child=repr.firstChild() ; + child ; child = child->next() ) + { + populate_ns_map(ns_map, *child); + } + } +} + +} + +static void sp_repr_write_stream_root_element(Node *repr, Writer &out, + bool add_whitespace, gchar const *default_ns, + int inlineattrs, int indent, + gchar const *const old_href_base, + gchar const *const new_href_base) +{ + using Inkscape::Util::ptr_shared; + + g_assert(repr != nullptr); + + // Clean unnecessary attributes and stype properties. (Controlled by preferences.) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool clean = prefs->getBool("/options/svgoutput/check_on_writing"); + if (clean) sp_attribute_clean_tree( repr ); + + // Sort attributes in a canonical order (helps with "diffing" SVG files).only if not set disable optimizations + bool sort = !prefs->getBool("/options/svgoutput/disable_optimizations") && prefs->getBool("/options/svgoutput/sort_attributes"); + if (sort) sp_attribute_sort_tree( repr ); + + Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml"); + + NSMap ns_map; + populate_ns_map(ns_map, *repr); + + Glib::QueryQuark elide_prefix=GQuark(0); + if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) { + elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, nullptr)); + } + + List<AttributeRecord const> attributes=repr->attributeList(); + for (auto & iter : ns_map) + { + Glib::QueryQuark prefix=iter.first; + ptr_shared ns_uri=iter.second; + + if (prefix.id()) { + if ( prefix != xml_prefix ) { + if ( elide_prefix == prefix ) { + attributes = cons(AttributeRecord(g_quark_from_static_string("xmlns"), ns_uri), attributes); + } + + Glib::ustring attr_name="xmlns:"; + attr_name.append(g_quark_to_string(prefix)); + GQuark key = g_quark_from_string(attr_name.c_str()); + attributes = cons(AttributeRecord(key, ns_uri), attributes); + } + } else { + // if there are non-namespaced elements, we can't globally + // use a default namespace + elide_prefix = GQuark(0); + } + } + + return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes, + inlineattrs, indent, old_href_base, new_href_base); +} + +void sp_repr_write_stream( Node *repr, Writer &out, gint indent_level, + bool add_whitespace, Glib::QueryQuark elide_prefix, + int inlineattrs, int indent, + gchar const *const old_href_base, + gchar const *const new_href_base) +{ + switch (repr->type()) { + case Inkscape::XML::TEXT_NODE: { + if( dynamic_cast<const Inkscape::XML::TextNode *>(repr)->is_CData() ) { + // Preserve CDATA sections, not converting '&' to &, etc. + out.printf( "<![CDATA[%s]]>", repr->content() ); + } else { + repr_quote_write( out, repr->content() ); + } + break; + } + case Inkscape::XML::COMMENT_NODE: { + repr_write_comment( out, repr->content(), add_whitespace, indent_level, indent ); + break; + } + case Inkscape::XML::PI_NODE: { + out.printf( "<?%s %s?>", repr->name(), repr->content() ); + break; + } + case Inkscape::XML::ELEMENT_NODE: { + sp_repr_write_stream_element( repr, out, indent_level, + add_whitespace, elide_prefix, + repr->attributeList(), + inlineattrs, indent, + old_href_base, new_href_base); + break; + } + case Inkscape::XML::DOCUMENT_NODE: { + g_assert_not_reached(); + break; + } + default: { + g_assert_not_reached(); + } + } +} + + +void sp_repr_write_stream_element( Node * repr, Writer & out, + gint indent_level, bool add_whitespace, + Glib::QueryQuark elide_prefix, + List<AttributeRecord const> attributes, + int inlineattrs, int indent, + gchar const *old_href_base, + gchar const *new_href_base ) +{ + Node *child = nullptr; + bool loose = false; + bool const add_whitespace_parent = add_whitespace; + + g_return_if_fail (repr != nullptr); + + if ( indent_level > 16 ) { + indent_level = 16; + } + + if (add_whitespace && indent) { + for (gint i = 0; i < indent_level; i++) { + for (gint j = 0; j < indent; j++) { + out.writeChar(' '); + } + } + } + + GQuark code = repr->code(); + gchar const *element_name; + if ( elide_prefix == qname_prefix(code) ) { + element_name = qname_local_name(code); + } else { + element_name = g_quark_to_string(code); + } + out.printf( "<%s", element_name ); + + // If this is a <text> element, suppress formatting whitespace + // for its content and children: + if (strcmp(repr->name(), "svg:text") == 0 || + strcmp(repr->name(), "svg:flowRoot") == 0) { + add_whitespace = false; + } else { + // Suppress formatting whitespace for xml:space="preserve" + gchar const *xml_space_attr = repr->attribute("xml:space"); + if (g_strcmp0(xml_space_attr, "preserve") == 0) { + add_whitespace = false; + } else if (g_strcmp0(xml_space_attr, "default") == 0) { + add_whitespace = true; + } + } + + for ( List<AttributeRecord const> iter = rebase_href_attrs(old_href_base, new_href_base, + attributes); + iter ; ++iter ) + { + if (!inlineattrs) { + out.writeChar('\n'); + if (indent) { + for ( gint i = 0 ; i < indent_level + 1 ; i++ ) { + for ( gint j = 0 ; j < indent ; j++ ) { + out.writeChar(' '); + } + } + } + } + out.printf(" %s=\"", g_quark_to_string(iter->key)); + repr_quote_write(out, iter->value); + out.writeChar('"'); + } + + loose = TRUE; + for (child = repr->firstChild() ; child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::TEXT_NODE) { + loose = FALSE; + break; + } + } + + if (repr->firstChild()) { + out.writeChar('>'); + if (loose && add_whitespace) { + out.writeChar('\n'); + } + for (child = repr->firstChild(); child != nullptr; child = child->next()) { + sp_repr_write_stream(child, out, ( loose ? indent_level + 1 : 0 ), + add_whitespace, elide_prefix, inlineattrs, indent, + old_href_base, new_href_base); + } + + if (loose && add_whitespace && indent) { + for (gint i = 0; i < indent_level; i++) { + for ( gint j = 0 ; j < indent ; j++ ) { + out.writeChar(' '); + } + } + } + out.printf( "</%s>", element_name ); + } else { + out.writeString( " />" ); + } + + if (add_whitespace_parent) { + out.writeChar('\n'); + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr-sorting.cpp b/src/xml/repr-sorting.cpp new file mode 100644 index 0000000..325b2a6 --- /dev/null +++ b/src/xml/repr-sorting.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "util/longest-common-suffix.h" +#include "xml/repr.h" +#include "xml/node-iterators.h" +#include "repr-sorting.h" + +static bool same_repr(Inkscape::XML::Node const &a, Inkscape::XML::Node const &b) +{ + /* todo: I'm not certain that it's legal to take the address of a reference. Check the exact wording of the spec on this matter. */ + return &a == &b; +} + +Inkscape::XML::Node const *LCA(Inkscape::XML::Node const *a, Inkscape::XML::Node const *b) +{ + using Inkscape::Algorithms::longest_common_suffix; + Inkscape::XML::Node const *ancestor = longest_common_suffix<Inkscape::XML::NodeConstParentIterator>( + a, b, nullptr, &same_repr); + bool OK = false; + if (ancestor) { + if (ancestor->type() != Inkscape::XML::DOCUMENT_NODE) { + OK = true; + } + } + if ( OK ) { + return ancestor; + } else { + return nullptr; + } +} + +Inkscape::XML::Node *LCA(Inkscape::XML::Node *a, Inkscape::XML::Node *b) +{ + Inkscape::XML::Node const *tmp = LCA(const_cast<Inkscape::XML::Node const *>(a), const_cast<Inkscape::XML::Node const *>(b)); + return const_cast<Inkscape::XML::Node *>(tmp); +} + +Inkscape::XML::Node const *AncetreFils(Inkscape::XML::Node const *descendent, Inkscape::XML::Node const *ancestor) +{ + Inkscape::XML::Node const *result = nullptr; + if ( descendent && ancestor ) { + if (descendent->parent() == ancestor) { + result = descendent; + } else { + result = AncetreFils(descendent->parent(), ancestor); + } + } + return result; +} + +Inkscape::XML::Node *AncetreFils(Inkscape::XML::Node *descendent, Inkscape::XML::Node *ancestor) +{ + Inkscape::XML::Node const * tmp = AncetreFils(const_cast<Inkscape::XML::Node const*>(descendent), const_cast<Inkscape::XML::Node const*>(ancestor)); + return const_cast<Inkscape::XML::Node *>(tmp); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr-sorting.h b/src/xml/repr-sorting.h new file mode 100644 index 0000000..c111623 --- /dev/null +++ b/src/xml/repr-sorting.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/** @file + * @brief Some functions relevant sorting reprs by position within document. + * @todo Functions in this file have non-English names. Determine what they do and rename + * accordingly. + */ + +#ifndef SEEN_XML_REPR_SORTING_H +#define SEEN_XML_REPR_SORTING_H + +namespace Inkscape { +namespace XML { + +class Node; + +} // namespace XML +} // namespace Inkscape + + +Inkscape::XML::Node *LCA(Inkscape::XML::Node *a, Inkscape::XML::Node *b); +Inkscape::XML::Node const *LCA(Inkscape::XML::Node const *a, Inkscape::XML::Node const *b); + +/** + * Returns a child of \a ancestor such that ret is itself an ancestor of \a descendent. + * + * The current version returns NULL if ancestor or descendent is NULL, though future versions may + * call g_log. Please update this comment if you rely on the current behaviour. + */ +Inkscape::XML::Node const *AncetreFils(Inkscape::XML::Node const *descendent, Inkscape::XML::Node const *ancestor); + +Inkscape::XML::Node *AncetreFils(Inkscape::XML::Node *descendent, Inkscape::XML::Node *ancestor); + +#endif // SEEN_XML_REPR_SOTRING_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr-util.cpp b/src/xml/repr-util.cpp new file mode 100644 index 0000000..4ecc4a7 --- /dev/null +++ b/src/xml/repr-util.cpp @@ -0,0 +1,661 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Miscellaneous helpers for reprs. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@ximian.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * g++ port Copyright (C) 2003 Nathan Hurst + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <cstdlib> + + +#include <glib.h> +#include <glibmm.h> + +#include <2geom/point.h> +#include "svg/stringstream.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-length.h" + +#include "xml/repr.h" +#include "xml/repr-sorting.h" + + +#define OSB_NS_URI "http://www.openswatchbook.org/uri/2009/osb" + + +struct SPXMLNs { + SPXMLNs *next; + unsigned int uri, prefix; +}; + +/*##################### +# DEFINITIONS +#####################*/ + +#ifndef FALSE +# define FALSE 0 +#endif + +#ifndef TRUE +# define TRUE (!FALSE) +#endif + +#ifndef MAX +# define MAX(a,b) (((a) < (b)) ? (b) : (a)) +#endif + +/*##################### +# FORWARD DECLARATIONS +#####################*/ + +static void sp_xml_ns_register_defaults(); +static char *sp_xml_ns_auto_prefix(char const *uri); + +/*##################### +# MAIN +#####################*/ + +/** + * SPXMLNs + */ + +static SPXMLNs *namespaces=nullptr; + +/* + * There are the prefixes to use for the XML namespaces defined + * in repr.h + */ +static void sp_xml_ns_register_defaults() +{ + static SPXMLNs defaults[11]; + + defaults[0].uri = g_quark_from_static_string(SP_SODIPODI_NS_URI); + defaults[0].prefix = g_quark_from_static_string("sodipodi"); + defaults[0].next = &defaults[1]; + + defaults[1].uri = g_quark_from_static_string(SP_XLINK_NS_URI); + defaults[1].prefix = g_quark_from_static_string("xlink"); + defaults[1].next = &defaults[2]; + + defaults[2].uri = g_quark_from_static_string(SP_SVG_NS_URI); + defaults[2].prefix = g_quark_from_static_string("svg"); + defaults[2].next = &defaults[3]; + + defaults[3].uri = g_quark_from_static_string(SP_INKSCAPE_NS_URI); + defaults[3].prefix = g_quark_from_static_string("inkscape"); + defaults[3].next = &defaults[4]; + + defaults[4].uri = g_quark_from_static_string(SP_RDF_NS_URI); + defaults[4].prefix = g_quark_from_static_string("rdf"); + defaults[4].next = &defaults[5]; + + defaults[5].uri = g_quark_from_static_string(SP_CC_NS_URI); + defaults[5].prefix = g_quark_from_static_string("cc"); + defaults[5].next = &defaults[6]; + + defaults[6].uri = g_quark_from_static_string(SP_DC_NS_URI); + defaults[6].prefix = g_quark_from_static_string("dc"); + defaults[6].next = &defaults[7]; + + defaults[7].uri = g_quark_from_static_string(OSB_NS_URI); + defaults[7].prefix = g_quark_from_static_string("osb"); + defaults[7].next = &defaults[8]; + + // Inkscape versions prior to 0.44 would write this namespace + // URI instead of the correct sodipodi namespace; by adding this + // entry to the table last (where it gets used for URI -> prefix + // lookups, but not prefix -> URI lookups), we effectively transfer + // elements in this namespace to the correct sodipodi namespace: + + defaults[8].uri = g_quark_from_static_string(SP_BROKEN_SODIPODI_NS_URI); + defaults[8].prefix = g_quark_from_static_string("sodipodi"); + defaults[8].next = &defaults[9]; + + // "Duck prion" + // This URL became widespread due to a bug in versions <= 0.43 + + defaults[9].uri = g_quark_from_static_string("http://inkscape.sourceforge.net/DTD/s odipodi-0.dtd"); + defaults[9].prefix = g_quark_from_static_string("sodipodi"); + defaults[9].next = &defaults[10]; + + // This namespace URI is being phased out by Creative Commons + + defaults[10].uri = g_quark_from_static_string(SP_OLD_CC_NS_URI); + defaults[10].prefix = g_quark_from_static_string("cc"); + defaults[10].next = nullptr; + + namespaces = &defaults[0]; +} + +char *sp_xml_ns_auto_prefix(char const *uri) +{ + char const *start, *end; + char *new_prefix; + start = uri; + while ((end = strpbrk(start, ":/"))) { + start = end + 1; + } + end = start + strspn(start, "abcdefghijklmnopqrstuvwxyz"); + if (end == start) { + start = "ns"; + end = start + 2; + } + new_prefix = g_strndup(start, end - start); + if (sp_xml_ns_prefix_uri(new_prefix)) { + char *temp; + int counter=0; + do { + temp = g_strdup_printf("%s%d", new_prefix, counter++); + } while (sp_xml_ns_prefix_uri(temp)); + g_free(new_prefix); + new_prefix = temp; + } + return new_prefix; +} + +gchar const *sp_xml_ns_uri_prefix(gchar const *uri, gchar const *suggested) +{ + char const *prefix; + + if (!uri) return nullptr; + + if (!namespaces) { + sp_xml_ns_register_defaults(); + } + + GQuark const key = g_quark_from_string(uri); + prefix = nullptr; + for ( SPXMLNs *iter=namespaces ; iter ; iter = iter->next ) { + if ( iter->uri == key ) { + prefix = g_quark_to_string(iter->prefix); + break; + } + } + + if (!prefix) { + char *new_prefix; + SPXMLNs *ns; + if (suggested) { + GQuark const prefix_key=g_quark_from_string(suggested); + + SPXMLNs *found=namespaces; + while (found) { + if (found->prefix != prefix_key) { + found = found->next; + } + else { + break; + } + } + + if (found) { // prefix already used? + new_prefix = sp_xml_ns_auto_prefix(uri); + } else { // safe to use suggested + new_prefix = g_strdup(suggested); + } + } else { + new_prefix = sp_xml_ns_auto_prefix(uri); + } + + ns = g_new(SPXMLNs, 1); + g_assert( ns != nullptr ); + ns->uri = g_quark_from_string(uri); + ns->prefix = g_quark_from_string(new_prefix); + + g_free(new_prefix); + + ns->next = namespaces; + namespaces = ns; + + prefix = g_quark_to_string(ns->prefix); + } + + return prefix; +} + +gchar const *sp_xml_ns_prefix_uri(gchar const *prefix) +{ + SPXMLNs *iter; + char const *uri; + + if (!prefix) return nullptr; + + if (!namespaces) { + sp_xml_ns_register_defaults(); + } + + GQuark const key = g_quark_from_string(prefix); + uri = nullptr; + for ( iter = namespaces ; iter ; iter = iter->next ) { + if ( iter->prefix == key ) { + uri = g_quark_to_string(iter->uri); + break; + } + } + return uri; +} + +/** + * Works for different-parent objects, so long as they have a common ancestor. Return value: + * 0 positions are equivalent + * 1 first object's position is greater than the second + * -1 first object's position is less than the second + * @todo Rewrite this function's description to be understandable + */ +int sp_repr_compare_position(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second) +{ + int p1, p2; + if (first->parent() == second->parent()) { + /* Basic case - first and second have same parent */ + p1 = first->position(); + p2 = second->position(); + } else { + /* Special case - the two objects have different parents. They + could be in different groups or on different layers for + instance. */ + + // Find the lowest common ancestor(LCA) + Inkscape::XML::Node const *ancestor = LCA(first, second); + g_assert(ancestor != nullptr); + + if (ancestor == first) { + return 1; + } else if (ancestor == second) { + return -1; + } else { + Inkscape::XML::Node const *to_first = AncetreFils(first, ancestor); + Inkscape::XML::Node const *to_second = AncetreFils(second, ancestor); + g_assert(to_second->parent() == to_first->parent()); + p1 = to_first->position(); + p2 = to_second->position(); + } + } + + if (p1 > p2) return 1; + if (p1 < p2) return -1; + return 0; + + /* effic: Assuming that the parent--child relationship is consistent + (i.e. that the parent really does contain first and second among + its list of children), it should be equivalent to walk along the + children and see which we encounter first (returning 0 iff first + == second). + + Given that this function is used solely for sorting, we can use a + similar approach to do the sort: gather the things to be sorted, + into an STL vector (to allow random access and faster + traversals). Do a single pass of the parent's children; for each + child, do a pass on whatever items in the vector we haven't yet + encountered. If the child is found, then swap it to the + beginning of the yet-unencountered elements of the vector. + Continue until no more than one remains unencountered. -- + pjrm */ +} + +bool sp_repr_compare_position_bool(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second){ + return sp_repr_compare_position(first, second)<0; +} + + +/** + * Find an element node using an unique attribute. + * + * This function returns the first child of the specified node that has the attribute + * @c key equal to @c value. Note that this function does not recurse. + * + * @param repr The node to start from + * @param key The name of the attribute to use for comparisons + * @param value The value of the attribute to look for + * @relatesalso Inkscape::XML::Node + */ +Inkscape::XML::Node *sp_repr_lookup_child(Inkscape::XML::Node *repr, + gchar const *key, + gchar const *value) +{ + g_return_val_if_fail(repr != nullptr, NULL); + for ( Inkscape::XML::Node *child = repr->firstChild() ; child ; child = child->next() ) { + gchar const *child_value = child->attribute(key); + if ( (child_value == value) || + (value && child_value && !strcmp(child_value, value)) ) + { + return child; + } + } + return nullptr; +} + +/** + * Recursive version of sp_repr_lookup_child(). + */ +Inkscape::XML::Node const *sp_repr_lookup_descendant(Inkscape::XML::Node const *repr, + gchar const *key, + gchar const *value) +{ + Inkscape::XML::Node const *found = nullptr; + g_return_val_if_fail(repr != nullptr, NULL); + gchar const *repr_value = repr->attribute(key); + if ( (repr_value == value) || + (repr_value && value && strcmp(repr_value, value) == 0) ) { + found = repr; + } else { + for (Inkscape::XML::Node const *child = repr->firstChild() ; child && !found; child = child->next() ) { + found = sp_repr_lookup_descendant( child, key, value ); + } + } + return found; +} + + +Inkscape::XML::Node *sp_repr_lookup_descendant(Inkscape::XML::Node *repr, + gchar const *key, + gchar const *value) +{ + Inkscape::XML::Node const *found = sp_repr_lookup_descendant( const_cast<Inkscape::XML::Node const *>(repr), key, value ); + return const_cast<Inkscape::XML::Node *>(found); +} + +Inkscape::XML::Node const *sp_repr_lookup_name( Inkscape::XML::Node const *repr, gchar const *name, gint maxdepth ) +{ + Inkscape::XML::Node const *found = nullptr; + g_return_val_if_fail(repr != nullptr, NULL); + g_return_val_if_fail(name != nullptr, NULL); + + GQuark const quark = g_quark_from_string(name); + + if ( (GQuark)repr->code() == quark ) { + found = repr; + } else if ( maxdepth != 0 ) { + // maxdepth == -1 means unlimited + if ( maxdepth == -1 ) { + maxdepth = 0; + } + + for (Inkscape::XML::Node const *child = repr->firstChild() ; child && !found; child = child->next() ) { + found = sp_repr_lookup_name( child, name, maxdepth - 1 ); + } + } + return found; +} + +Inkscape::XML::Node *sp_repr_lookup_name( Inkscape::XML::Node *repr, gchar const *name, gint maxdepth ) +{ + Inkscape::XML::Node const *found = sp_repr_lookup_name( const_cast<Inkscape::XML::Node const *>(repr), name, maxdepth ); + return const_cast<Inkscape::XML::Node *>(found); +} + +std::vector<Inkscape::XML::Node const *> sp_repr_lookup_name_many( Inkscape::XML::Node const *repr, gchar const *name, gint maxdepth ) +{ + std::vector<Inkscape::XML::Node const *> nodes; + std::vector<Inkscape::XML::Node const *> found; + g_return_val_if_fail(repr != nullptr, nodes); + g_return_val_if_fail(name != nullptr, nodes); + + GQuark const quark = g_quark_from_string(name); + + if ( (GQuark)repr->code() == quark ) { + nodes.push_back(repr); + } + + if ( maxdepth != 0 ) { + // maxdepth == -1 means unlimited + if ( maxdepth == -1 ) { + maxdepth = 0; + } + + for (Inkscape::XML::Node const *child = repr->firstChild() ; child; child = child->next() ) { + found = sp_repr_lookup_name_many( child, name, maxdepth - 1); + nodes.insert(nodes.end(), found.begin(), found.end()); + } + } + + return nodes; +} + +std::vector<Inkscape::XML::Node *> +sp_repr_lookup_property_many( Inkscape::XML::Node *repr, Glib::ustring const& property, + Glib::ustring const &value, int maxdepth ) +{ + std::vector<Inkscape::XML::Node *> nodes; + std::vector<Inkscape::XML::Node *> found; + g_return_val_if_fail(repr != nullptr, nodes); + + SPCSSAttr* css = sp_repr_css_attr (repr, "style"); + if (value == sp_repr_css_property (css, property, "")) { + nodes.push_back(repr); + } + + if ( maxdepth != 0 ) { + // maxdepth == -1 means unlimited + if ( maxdepth == -1 ) { + maxdepth = 0; + } + + for (Inkscape::XML::Node *child = repr->firstChild() ; child; child = child->next() ) { + found = sp_repr_lookup_property_many( child, property, value, maxdepth - 1); + nodes.insert(nodes.end(), found.begin(), found.end()); + } + } + + return nodes; +} + +/** + * Determine if the node is a 'title', 'desc' or 'metadata' element. + */ +bool sp_repr_is_meta_element(const Inkscape::XML::Node *node) +{ + if (node == nullptr) return false; + if (node->type() != Inkscape::XML::ELEMENT_NODE) return false; + gchar const *name = node->name(); + if (name == nullptr) return false; + if (!std::strcmp(name, "svg:title")) return true; + if (!std::strcmp(name, "svg:desc")) return true; + if (!std::strcmp(name, "svg:metadata")) return true; + return false; +} + +/** + * Parses the boolean value of an attribute "key" in repr and sets val accordingly, or to FALSE if + * the attr is not set. + * + * \return TRUE if the attr was set, FALSE otherwise. + */ +unsigned int sp_repr_get_boolean(Inkscape::XML::Node *repr, gchar const *key, unsigned int *val) +{ + gchar const *v; + + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + g_return_val_if_fail(val != nullptr, FALSE); + + v = repr->attribute(key); + + if (v != nullptr) { + if (!g_ascii_strcasecmp(v, "true") || + !g_ascii_strcasecmp(v, "yes" ) || + !g_ascii_strcasecmp(v, "y" ) || + (atoi(v) != 0)) { + *val = TRUE; + } else { + *val = FALSE; + } + return TRUE; + } else { + *val = FALSE; + return FALSE; + } +} + +unsigned int sp_repr_get_int(Inkscape::XML::Node *repr, gchar const *key, int *val) +{ + gchar const *v; + + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + g_return_val_if_fail(val != nullptr, FALSE); + + v = repr->attribute(key); + + if (v != nullptr) { + *val = atoi(v); + return TRUE; + } + + return FALSE; +} + +unsigned int sp_repr_get_double(Inkscape::XML::Node *repr, gchar const *key, double *val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + g_return_val_if_fail(val != nullptr, FALSE); + + gchar const *v = repr->attribute(key); + + if (v != nullptr) { + *val = g_ascii_strtod(v, nullptr); + return TRUE; + } + + return FALSE; +} + +unsigned int sp_repr_set_boolean(Inkscape::XML::Node *repr, gchar const *key, unsigned int val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + + repr->setAttribute(key, (val) ? "true" : "false"); + return true; +} + +unsigned int sp_repr_set_int(Inkscape::XML::Node *repr, gchar const *key, int val) +{ + gchar c[32]; + + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + + g_snprintf(c, 32, "%d", val); + + repr->setAttribute(key, c); + return true; +} + +/** + * Set a property attribute to \a val [slightly rounded], in the format + * required for CSS properties: in particular, it never uses exponent + * notation. + */ +unsigned int sp_repr_set_css_double(Inkscape::XML::Node *repr, gchar const *key, double val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + + Inkscape::CSSOStringStream os; + os << val; + + repr->setAttribute(key, os.str()); + return true; +} + +/** + * For attributes where an exponent is allowed. + * + * Not suitable for property attributes (fill-opacity, font-size etc.). + */ +unsigned int sp_repr_set_svg_double(Inkscape::XML::Node *repr, gchar const *key, double val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + g_return_val_if_fail(val==val, FALSE);//tests for nan + + Inkscape::SVGOStringStream os; + os << val; + + repr->setAttribute(key, os.str()); + return true; +} + +unsigned int sp_repr_set_svg_non_default_double(Inkscape::XML::Node *repr, gchar const *key, double val, double default_value) +{ + if (val==default_value){ + repr->removeAttribute(key); + return true; + } + return sp_repr_set_svg_double(repr, key, val); +} + +/** + * For attributes where an exponent is allowed. + * + * Not suitable for property attributes. + */ +unsigned int sp_repr_set_svg_length(Inkscape::XML::Node *repr, gchar const *key, SVGLength &val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + + repr->setAttribute(key, val.write()); + return true; +} + +unsigned sp_repr_set_point(Inkscape::XML::Node *repr, gchar const *key, Geom::Point const & val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + + Inkscape::SVGOStringStream os; + os << val[Geom::X] << "," << val[Geom::Y]; + + repr->setAttribute(key, os.str()); + return true; +} + +unsigned int sp_repr_get_point(Inkscape::XML::Node *repr, gchar const *key, Geom::Point *val) +{ + g_return_val_if_fail(repr != nullptr, FALSE); + g_return_val_if_fail(key != nullptr, FALSE); + g_return_val_if_fail(val != nullptr, FALSE); + + gchar const *v = repr->attribute(key); + + g_return_val_if_fail(v != nullptr, FALSE); + + gchar ** strarray = g_strsplit(v, ",", 2); + + if (strarray && strarray[0] && strarray[1]) { + double newx, newy; + newx = g_ascii_strtod(strarray[0], nullptr); + newy = g_ascii_strtod(strarray[1], nullptr); + g_strfreev (strarray); + *val = Geom::Point(newx, newy); + return TRUE; + } + + g_strfreev (strarray); + return FALSE; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/repr.cpp b/src/xml/repr.cpp new file mode 100644 index 0000000..8988586 --- /dev/null +++ b/src/xml/repr.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * A few non-inline functions of the C facade to Inkscape::XML::Node. + */ + +/* + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * MenTaLguY <mental@rydia.net> + * + * Copyright (C) 1999-2003 authors + * Copyright (C) 2000-2002 Ximian, Inc. + * g++ port Copyright (C) 2003 Nathan Hurst + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noREPR_VERBOSE + +#include <cstring> + +#include "xml/repr.h" +#include "xml/text-node.h" +#include "xml/element-node.h" +#include "xml/comment-node.h" +#include "xml/simple-document.h" + +using Inkscape::Util::share_string; + +/// Returns new document having as first child a node named rootname. +Inkscape::XML::Document * +sp_repr_document_new(char const *rootname) +{ + Inkscape::XML::Document *doc = new Inkscape::XML::SimpleDocument(); + if (!strcmp(rootname, "svg:svg")) { + doc->setAttribute("version", "1.0"); + doc->setAttribute("standalone", "no"); + Inkscape::XML::Node *comment = doc->createComment(" Created with Inkscape (http://www.inkscape.org/) "); + doc->appendChild(comment); + Inkscape::GC::release(comment); + } + + Inkscape::XML::Node *root = doc->createElement(rootname); + doc->appendChild(root); + Inkscape::GC::release(root); + + return doc; +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/xml/repr.h b/src/xml/repr.h new file mode 100644 index 0000000..68a857b --- /dev/null +++ b/src/xml/repr.h @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief C facade to Inkscape::XML::Node + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 authors + * Copyright (C) 2000-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SP_REPR_H +#define SEEN_SP_REPR_H + +#include <vector> +#include <glibmm/quark.h> + +#include "xml/node.h" +#include "xml/document.h" + +#define SP_SODIPODI_NS_URI "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" +#define SP_BROKEN_SODIPODI_NS_URI "http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" +#define SP_INKSCAPE_NS_URI "http://www.inkscape.org/namespaces/inkscape" +#define SP_XLINK_NS_URI "http://www.w3.org/1999/xlink" +#define SP_SVG_NS_URI "http://www.w3.org/2000/svg" +#define SP_RDF_NS_URI "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define SP_CC_NS_URI "http://creativecommons.org/ns#" +#define SP_OLD_CC_NS_URI "http://web.resource.org/cc/" +#define SP_DC_NS_URI "http://purl.org/dc/elements/1.1/" + +class SPCSSAttr; +class SVGLength; + +namespace Inkscape { +namespace IO { +class Writer; +} // namespace IO +} // namespace Inkscape + +namespace Geom { +class Point; +} + +/* SPXMLNs */ +char const *sp_xml_ns_uri_prefix(char const *uri, char const *suggested); +char const *sp_xml_ns_prefix_uri(char const *prefix); + +Inkscape::XML::Document *sp_repr_document_new(char const *rootname); + +/* IO */ + +Inkscape::XML::Document *sp_repr_read_file(char const *filename, char const *default_ns); +Inkscape::XML::Document *sp_repr_read_mem(char const *buffer, int length, char const *default_ns); +void sp_repr_write_stream(Inkscape::XML::Node *repr, Inkscape::IO::Writer &out, + int indent_level, bool add_whitespace, Glib::QueryQuark elide_prefix, + int inlineattrs, int indent, + char const *old_href_base = nullptr, + char const *new_href_base = nullptr); +Inkscape::XML::Document *sp_repr_read_buf (const Glib::ustring &buf, const char *default_ns); +Glib::ustring sp_repr_save_buf(Inkscape::XML::Document *doc); + +// TODO convert to std::string +void sp_repr_save_stream(Inkscape::XML::Document *doc, FILE *to_file, + char const *default_ns = nullptr, bool compress = false, + char const *old_href_base = nullptr, + char const *new_href_base = nullptr); + +bool sp_repr_save_file(Inkscape::XML::Document *doc, char const *filename, char const *default_ns=nullptr); +bool sp_repr_save_rebased_file(Inkscape::XML::Document *doc, char const *filename_utf8, + char const *default_ns, + char const *old_base, char const *new_base_filename); + + +/* CSS stuff */ + +SPCSSAttr *sp_repr_css_attr_new(); +void sp_repr_css_attr_unref(SPCSSAttr *css); +SPCSSAttr *sp_repr_css_attr(Inkscape::XML::Node const *repr, char const *attr); +SPCSSAttr *sp_repr_css_attr_parse_color_to_fill(const Glib::ustring &text); +SPCSSAttr *sp_repr_css_attr_inherited(Inkscape::XML::Node const *repr, char const *attr); +SPCSSAttr *sp_repr_css_attr_unset_all(SPCSSAttr *css); + +char const *sp_repr_css_property(SPCSSAttr *css, char const *name, char const *defval); +Glib::ustring sp_repr_css_property(SPCSSAttr *css, Glib::ustring const &name, Glib::ustring const &defval); +void sp_repr_css_set_property(SPCSSAttr *css, char const *name, char const *value); +void sp_repr_css_unset_property(SPCSSAttr *css, char const *name); +bool sp_repr_css_property_is_unset(SPCSSAttr *css, char const *name); +double sp_repr_css_double_property(SPCSSAttr *css, char const *name, double defval); + +void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str); +void sp_repr_css_set(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key); +void sp_repr_css_merge(SPCSSAttr *dst, SPCSSAttr *src); +void sp_repr_css_attr_add_from_string(SPCSSAttr *css, const char *data); +void sp_repr_css_change(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key); +void sp_repr_css_change_recursive(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key); +void sp_repr_css_print(SPCSSAttr *css); + +/* Utility finctions */ +/// Remove \a repr from children of its parent node. +inline void sp_repr_unparent(Inkscape::XML::Node *repr) { + if (repr) { + Inkscape::XML::Node *parent=repr->parent(); + if (parent) { + parent->removeChild(repr); + } + } +} + +bool sp_repr_is_meta_element(const Inkscape::XML::Node *node); + +/* Convenience */ +unsigned sp_repr_get_boolean(Inkscape::XML::Node *repr, char const *key, unsigned *val); +unsigned sp_repr_get_int(Inkscape::XML::Node *repr, char const *key, int *val); +unsigned sp_repr_get_double(Inkscape::XML::Node *repr, char const *key, double *val); +unsigned sp_repr_set_boolean(Inkscape::XML::Node *repr, char const *key, unsigned val); +unsigned sp_repr_set_int(Inkscape::XML::Node *repr, char const *key, int val); +unsigned sp_repr_set_css_double(Inkscape::XML::Node *repr, char const *key, double val); +unsigned sp_repr_set_svg_double(Inkscape::XML::Node *repr, char const *key, double val); +unsigned sp_repr_set_svg_length(Inkscape::XML::Node *repr, char const *key, SVGLength &val); +unsigned sp_repr_set_point(Inkscape::XML::Node *repr, char const *key, Geom::Point const & val); +unsigned sp_repr_get_point(Inkscape::XML::Node *repr, char const *key, Geom::Point *val); + +//c++-style comparison : returns (bool)(a<b) +int sp_repr_compare_position(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second); +bool sp_repr_compare_position_bool(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second); + +// Searching +/** + * @brief Find an element node with the given name. + * + * This function searches the descendants of the specified node depth-first for + * the first XML node with the specified name. + * + * @param repr The node to start from + * @param name The name of the element node to find + * @param maxdepth Maximum search depth, or -1 for an unlimited depth + * @return A pointer to the matching Inkscape::XML::Node + * @relatesalso Inkscape::XML::Node + */ +Inkscape::XML::Node *sp_repr_lookup_name(Inkscape::XML::Node *repr, + char const *name, + int maxdepth = -1); + +Inkscape::XML::Node const *sp_repr_lookup_name(Inkscape::XML::Node const *repr, + char const *name, + int maxdepth = -1); + +std::vector<Inkscape::XML::Node const *> sp_repr_lookup_name_many(Inkscape::XML::Node const *repr, + char const *name, + int maxdepth = -1); + +// Find an element node using an unique attribute. +Inkscape::XML::Node *sp_repr_lookup_child(Inkscape::XML::Node *repr, + char const *key, + char const *value); + +// Find an element node using an unique attribute recursively. +Inkscape::XML::Node *sp_repr_lookup_descendant(Inkscape::XML::Node *repr, + char const *key, + char const *value); + +Inkscape::XML::Node const *sp_repr_lookup_descendant(Inkscape::XML::Node const *repr, + char const *key, + char const *value); + +// Find element nodes using a property value. +std::vector<Inkscape::XML::Node *> sp_repr_lookup_property_many(Inkscape::XML::Node *repr, + Glib::ustring const &property, + Glib::ustring const &value, + int maxdepth = -1); + +inline Inkscape::XML::Node *sp_repr_document_first_child(Inkscape::XML::Document const *doc) { + return const_cast<Inkscape::XML::Node *>(doc->firstChild()); +} + +inline bool sp_repr_is_def(Inkscape::XML::Node const *node) { + return node->parent() != nullptr && + node->parent()->name() != nullptr && + strcmp("svg:defs", node->parent()->name()) == 0; +} + +inline bool sp_repr_is_layer(Inkscape::XML::Node const *node) { + return node->attribute("inkscape:groupmode") != nullptr && + strcmp("layer", node->attribute("inkscape:groupmode")) == 0; +} + +/** + * @brief Visit all descendants recursively. + * + * Traverse all descendants of node and call visitor on it. + * Stop descending when visitor returns false + * + * @param node The root node to start visiting + * @param visitor The visitor lambda (Node *) -> bool + * If visitor returns false child nodes of current node are not visited. + * @relatesalso Inkscape::XML::Node + */ +template <typename Visitor> +void sp_repr_visit_descendants(Inkscape::XML::Node *node, Visitor visitor) { + if (!visitor(node)) { + return; + } + for (Inkscape::XML::Node *child = node->firstChild(); + child != nullptr; + child = child->next()) { + sp_repr_visit_descendants(child, visitor); + } +} + +/** + * @brief Visit descendants of 2 nodes in parallel. + * The assumption is that one a and b trees are the same in terms of structure (like one is + * a duplicate of the other). + * + * @param a first node tree root + * @param b second node tree root + * @param visitor The visitor lambda (Node *, Node *) -> bool + * If visitor returns false child nodes are not visited. + * @relatesalso Inkscape::XML::Node + */ +template <typename Visitor> +void sp_repr_visit_descendants(Inkscape::XML::Node *a, Inkscape::XML::Node *b, Visitor visitor) { + if (!visitor(a, b)) { + return; + } + for (Inkscape::XML::Node *ac = a->firstChild(), *bc = b->firstChild(); + ac != nullptr && bc != nullptr; + ac = ac->next(), bc = bc->next()) { + sp_repr_visit_descendants(ac, bc, visitor); + } +} + +#endif // SEEN_SP_REPR_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/simple-document.cpp b/src/xml/simple-document.cpp new file mode 100644 index 0000000..c1227ac --- /dev/null +++ b/src/xml/simple-document.cpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Garbage collected XML document implementation. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> // g_assert() + +#include "xml/simple-document.h" +#include "xml/event-fns.h" +#include "xml/element-node.h" +#include "xml/text-node.h" +#include "xml/comment-node.h" +#include "xml/pi-node.h" + +namespace Inkscape { + +namespace XML { + +void SimpleDocument::beginTransaction() { + g_assert(!_in_transaction); + _in_transaction = true; +} + +void SimpleDocument::rollback() { + g_assert(_in_transaction); + _in_transaction = false; + Event *log = _log_builder.detach(); + sp_repr_undo_log(log); + sp_repr_free_log(log); +} + +void SimpleDocument::commit() { + g_assert(_in_transaction); + _in_transaction = false; + _log_builder.discard(); +} + +Inkscape::XML::Event *SimpleDocument::commitUndoable() { + g_assert(_in_transaction); + _in_transaction = false; + return _log_builder.detach(); +} + +Node *SimpleDocument::createElement(char const *name) { + return new ElementNode(g_quark_from_string(name), this); +} + +Node *SimpleDocument::createTextNode(char const *content) { + return new TextNode(Util::share_string(content), this); +} + +Node *SimpleDocument::createTextNode(char const *content, bool const is_CData) { + return new TextNode(Util::share_string(content), this, is_CData); +} + +Node *SimpleDocument::createComment(char const *content) { + return new CommentNode(Util::share_string(content), this); +} + +Node *SimpleDocument::createPI(char const *target, char const *content) { + return new PINode(g_quark_from_string(target), Util::share_string(content), this); +} + +void SimpleDocument::notifyChildAdded(Node &parent, + Node &child, + Node *prev) +{ + if (_in_transaction) { + _log_builder.addChild(parent, child, prev); + } +} + +void SimpleDocument::notifyChildRemoved(Node &parent, + Node &child, + Node *prev) +{ + if (_in_transaction) { + _log_builder.removeChild(parent, child, prev); + } +} + +void SimpleDocument::notifyChildOrderChanged(Node &parent, + Node &child, + Node *old_prev, + Node *new_prev) +{ + if (_in_transaction) { + _log_builder.setChildOrder(parent, child, old_prev, new_prev); + } +} + +void SimpleDocument::notifyContentChanged(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content) +{ + if (_in_transaction) { + _log_builder.setContent(node, old_content, new_content); + } +} + +void SimpleDocument::notifyAttributeChanged(Node &node, + GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value) +{ + if (_in_transaction) { + _log_builder.setAttribute(node, name, old_value, new_value); + } +} + +void SimpleDocument::notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) +{ + if (_in_transaction) { + _log_builder.setElementName(node, old_name, new_name); + } +} + +} // end namespace XML +} // end namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/simple-document.h b/src/xml/simple-document.h new file mode 100644 index 0000000..5dc1750 --- /dev/null +++ b/src/xml/simple-document.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape::XML::SimpleDocument - generic XML document implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2004-2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SIMPLE_DOCUMENT_H +#define SEEN_INKSCAPE_XML_SIMPLE_DOCUMENT_H + +#include "xml/document.h" +#include "xml/simple-node.h" +#include "xml/node-observer.h" +#include "xml/log-builder.h" + +namespace Inkscape { + +namespace XML { + +class SimpleDocument : public SimpleNode, + public Document, + public NodeObserver +{ +public: + explicit SimpleDocument() + : SimpleNode(g_quark_from_static_string("xml"), this), + _in_transaction(false), _is_CData(false) {} + + NodeType type() const override { return Inkscape::XML::DOCUMENT_NODE; } + + bool inTransaction() override { return _in_transaction; } + + void beginTransaction() override; + void rollback() override; + void commit() override; + Inkscape::XML::Event *commitUndoable() override; + + Node *createElement(char const *name) override; + Node *createTextNode(char const *content) override; + Node *createTextNode(char const *content, bool const is_CData) override; + Node *createComment(char const *content) override; + Node *createPI(char const *target, char const *content) override; + + void notifyChildAdded(Node &parent, Node &child, Node *prev) override; + + void notifyChildRemoved(Node &parent, Node &child, Node *prev) override; + + void notifyChildOrderChanged(Node &parent, Node &child, + Node *old_prev, Node *new_prev) override; + + void notifyContentChanged(Node &node, + Util::ptr_shared old_content, + Util::ptr_shared new_content) override; + + void notifyAttributeChanged(Node &node, GQuark name, + Util::ptr_shared old_value, + Util::ptr_shared new_value) override; + + void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override; + +protected: + SimpleDocument(SimpleDocument const &doc) + : Node(), SimpleNode(doc), Document(), NodeObserver(), + _in_transaction(false), + _is_CData(false){} + + SimpleNode *_duplicate(Document* /*doc*/) const override + { + return new SimpleDocument(*this); + } + NodeObserver *logger() override { return this; } + +private: + bool _in_transaction; + LogBuilder _log_builder; + bool _is_CData; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/simple-node.cpp b/src/xml/simple-node.cpp new file mode 100644 index 0000000..d6b01f3 --- /dev/null +++ b/src/xml/simple-node.cpp @@ -0,0 +1,825 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Garbage collected XML node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2003-2005 MenTaLguY <mental@rydia.net> + * Copyright 2003 Nathan Hurst + * Copyright 1999-2003 Lauris Kaplinski + * Copyright 2000-2002 Ximian Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include <cstring> +#include <string> + +#include <glib.h> + +#include "preferences.h" + +#include "xml/simple-node.h" +#include "xml/node-event-vector.h" +#include "xml/node-fns.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" +#include "util/format.h" + +#include "attribute-rel-util.h" + +namespace Inkscape { + +namespace XML { + +namespace { + +std::shared_ptr<std::string> stringify_node(Node const &node) { + gchar *string; + switch (node.type()) { + case ELEMENT_NODE: { + char const *id=node.attribute("id"); + if (id) { + string = g_strdup_printf("element(%p)=%s(#%s)", &node, node.name(), id); + } else { + string = g_strdup_printf("element(%p)=%s", &node, node.name()); + } + } break; + case TEXT_NODE: + string = g_strdup_printf("text(%p)=%s", &node, node.content()); + break; + case COMMENT_NODE: + string = g_strdup_printf("comment(%p)=<!--%s-->", &node, node.content()); + break; + case DOCUMENT_NODE: + string = g_strdup_printf("document(%p)", &node); + break; + default: + string = g_strdup_printf("unknown(%p)", &node); + } + std::shared_ptr<std::string> result = std::make_shared<std::string>(string); + g_free(string); + return result; +} + +typedef Debug::SimpleEvent<Debug::Event::XML> DebugXML; + +class DebugXMLNode : public DebugXML { +public: + DebugXMLNode(Node const &node, char const *name) + : DebugXML(name) + { + _addProperty("node", stringify_node(node)); + } +}; + +class DebugAddChild : public DebugXMLNode { +public: + DebugAddChild(Node const &node, Node const &child, Node const *prev) + : DebugXMLNode(node, "add-child") + { + _addProperty("child", stringify_node(child)); + _addProperty("position", prev ? prev->position() + 1 : 0 ); + } +}; + +class DebugRemoveChild : public DebugXMLNode { +public: + DebugRemoveChild(Node const &node, Node const &child) + : DebugXMLNode(node, "remove-child") + { + _addProperty("child", stringify_node(child)); + } +}; + +class DebugSetChildPosition : public DebugXMLNode { +public: + DebugSetChildPosition(Node const &node, Node const &child, + Node const *old_prev, Node const *new_prev) + : DebugXMLNode(node, "set-child-position") + { + _addProperty("child", stringify_node(child)); + + unsigned old_position = ( old_prev ? old_prev->position() : 0 ); + unsigned position = ( new_prev ? new_prev->position() : 0 ); + if ( position > old_position ) { + --position; + } + + _addProperty("position", position); + } +}; + +class DebugSetContent : public DebugXMLNode { +public: + DebugSetContent(Node const &node, + Util::ptr_shared content) + : DebugXMLNode(node, "set-content") + { + _addProperty("content", content.pointer()); + } +}; + +class DebugClearContent : public DebugXMLNode { +public: + DebugClearContent(Node const &node) + : DebugXMLNode(node, "clear-content") + {} +}; + +class DebugSetAttribute : public DebugXMLNode { +public: + DebugSetAttribute(Node const &node, + GQuark name, + Util::ptr_shared value) + : DebugXMLNode(node, "set-attribute") + { + _addProperty("name", g_quark_to_string(name)); + _addProperty("value", value.pointer()); + } +}; + +class DebugClearAttribute : public DebugXMLNode { +public: + DebugClearAttribute(Node const &node, GQuark name) + : DebugXMLNode(node, "clear-attribute") + { + _addProperty("name", g_quark_to_string(name)); + } +}; + +class DebugSetElementName : public DebugXMLNode { +public: + DebugSetElementName(Node const& node, GQuark name) + : DebugXMLNode(node, "set-name") + { + _addProperty("name", g_quark_to_string(name)); + } +}; + +} + +using Util::ptr_shared; +using Util::share_string; +using Util::share_unsafe; +using Util::List; +using Util::MutableList; +using Util::cons; +using Util::rest; +using Util::set_rest; + +SimpleNode::SimpleNode(int code, Document *document) +: Node(), _name(code), _attributes(), _child_count(0), + _cached_positions_valid(false) +{ + g_assert(document != nullptr); + + this->_document = document; + this->_parent = this->_next = this->_prev = nullptr; + this->_first_child = this->_last_child = nullptr; + + _observers.add(_subtree_observers); +} + +SimpleNode::SimpleNode(SimpleNode const &node, Document *document) +: Node(), + _cached_position(node._cached_position), + _name(node._name), _attributes(), _content(node._content), + _child_count(node._child_count), + _cached_positions_valid(node._cached_positions_valid) +{ + g_assert(document != nullptr); + + _document = document; + _parent = _next = _prev = nullptr; + _first_child = _last_child = nullptr; + + for ( SimpleNode *child = node._first_child ; + child != nullptr ; child = child->_next ) + { + SimpleNode *child_copy=dynamic_cast<SimpleNode *>(child->duplicate(document)); + + child_copy->_setParent(this); + if (_last_child) { // not the first iteration + _last_child->_next = child_copy; + child_copy->_prev = _last_child; + } else { + _first_child = child_copy; + } + _last_child = child_copy; + + child_copy->release(); // release to avoid a leak + } + + // We need to keep the order of the attributes that we duplicate + // and for now, we do that by duplicating the list twice. + List<AttributeRecord const> _temp; + for ( List<AttributeRecord const> iter = node._attributes ; + iter ; ++iter ) + { + _temp = cons(*iter, _temp); + } + // At this point temp is an up-sidedown list of attributes, put them + // back in the right way now. + for ( List<AttributeRecord const> iter = _temp ; + iter ; ++iter ) + { + _attributes = cons(*iter, _attributes); + } + + _observers.add(_subtree_observers); +} + +gchar const *SimpleNode::name() const { + return g_quark_to_string(_name); +} + +gchar const *SimpleNode::content() const { + return this->_content; +} + +gchar const *SimpleNode::attribute(gchar const *name) const { + g_return_val_if_fail(name != nullptr, NULL); + + GQuark const key = g_quark_from_string(name); + + for ( List<AttributeRecord const> iter = _attributes ; + iter ; ++iter ) + { + if ( iter->key == key ) { + return iter->value; + } + } + + return nullptr; +} + +unsigned SimpleNode::position() const { + g_return_val_if_fail(_parent != nullptr, 0); + return _parent->_childPosition(*this); +} + +unsigned SimpleNode::_childPosition(SimpleNode const &child) const { + if (!_cached_positions_valid) { + unsigned position=0; + for ( SimpleNode *sibling = _first_child ; + sibling ; sibling = sibling->_next ) + { + sibling->_cached_position = position; + position++; + } + _cached_positions_valid = true; + } + return child._cached_position; +} + +Node *SimpleNode::nthChild(unsigned index) { + SimpleNode *child = _first_child; + for ( ; index > 0 && child ; child = child->_next ) { + index--; + } + return child; +} + +bool SimpleNode::matchAttributeName(gchar const *partial_name) const { + g_return_val_if_fail(partial_name != nullptr, false); + + for ( List<AttributeRecord const> iter = _attributes ; + iter ; ++iter ) + { + gchar const *name = g_quark_to_string(iter->key); + if (std::strstr(name, partial_name)) { + return true; + } + } + + return false; +} + +void SimpleNode::_setParent(SimpleNode *parent) { + if (_parent) { + _subtree_observers.remove(_parent->_subtree_observers); + } + _parent = parent; + if (parent) { + _subtree_observers.add(parent->_subtree_observers); + } +} + +void SimpleNode::setContent(gchar const *content) { + ptr_shared old_content=_content; + ptr_shared new_content = ( content ? share_string(content) : ptr_shared() ); + + Debug::EventTracker<> tracker; + if (new_content) { + tracker.set<DebugSetContent>(*this, new_content); + } else { + tracker.set<DebugClearContent>(*this); + } + + _content = new_content; + + if ( _content != old_content ) { + _document->logger()->notifyContentChanged(*this, old_content, _content); + _observers.notifyContentChanged(*this, old_content, _content); + } +} + +void +SimpleNode::setAttributeImpl(gchar const *name, gchar const *value, bool is_interactive) +{ + g_return_if_fail(name && *name); + + // sanity check: `name` must not contain whitespace + g_assert(std::none_of(name, name + strlen(name), [](char c) { return g_ascii_isspace(c); })); + + // Check usefulness of attributes on elements in the svg namespace, optionally don't add them to tree. + Glib::ustring element = g_quark_to_string(_name); + //g_message("setAttribute: %s: %s: %s", element.c_str(), name, value); + gchar* cleaned_value = g_strdup( value ); + + // Only check elements in SVG name space and don't block setting attribute to NULL. + if( element.substr(0,4) == "svg:" && value != nullptr) { + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if( prefs->getBool("/options/svgoutput/check_on_editing") ) { + + gchar const *id_char = attribute("id"); + Glib::ustring id = (id_char == nullptr ? "" : id_char ); + unsigned int flags = sp_attribute_clean_get_prefs(); + bool attr_warn = flags & SP_ATTR_CLEAN_ATTR_WARN; + bool attr_remove = flags & SP_ATTR_CLEAN_ATTR_REMOVE; + + // Check attributes + if( (attr_warn || attr_remove) && value != nullptr ) { + bool is_useful = sp_attribute_check_attribute( element, id, name, attr_warn ); + if( !is_useful && attr_remove ) { + g_free( cleaned_value ); + return; // Don't add to tree. + } + } + + // Check style properties -- Note: if element is not yet inserted into + // tree (and thus has no parent), default values will not be tested. + if( !strcmp( name, "style" ) && (flags >= SP_ATTR_CLEAN_STYLE_WARN) ) { + g_free( cleaned_value ); + cleaned_value = g_strdup( sp_attribute_clean_style( this, value, flags ).c_str() ); + // if( g_strcmp0( value, cleaned_value ) ) { + // g_warning( "SimpleNode::setAttribute: %s", id.c_str() ); + // g_warning( " original: %s", value); + // g_warning( " cleaned: %s", cleaned_value); + // } + } + } + } + + GQuark const key = g_quark_from_string(name); + + MutableList<AttributeRecord> ref; + MutableList<AttributeRecord> existing; + for ( existing = _attributes ; existing ; ++existing ) { + if ( existing->key == key ) { + break; + } + ref = existing; + } + Debug::EventTracker<> tracker; + + ptr_shared old_value=( existing ? existing->value : ptr_shared() ); + + ptr_shared new_value=ptr_shared(); + if (cleaned_value) { + new_value = share_string(cleaned_value); + tracker.set<DebugSetAttribute>(*this, key, new_value); + if (!existing) { + if (ref) { + set_rest(ref, MutableList<AttributeRecord>(AttributeRecord(key, new_value))); + } else { + _attributes = MutableList<AttributeRecord>(AttributeRecord(key, new_value)); + } + } else { + existing->value = new_value; + } + } else { + tracker.set<DebugClearAttribute>(*this, key); + if (existing) { + if (ref) { + set_rest(ref, rest(existing)); + } else { + _attributes = rest(existing); + } + set_rest(existing, MutableList<AttributeRecord>()); + } + } + + if ( new_value != old_value && (!old_value || !new_value || strcmp(old_value, new_value))) { + _document->logger()->notifyAttributeChanged(*this, key, old_value, new_value); + _observers.notifyAttributeChanged(*this, key, old_value, new_value); + //g_warning( "setAttribute notified: %s: %s: %s: %s", name, element.c_str(), old_value, new_value ); + } + g_free( cleaned_value ); +} + +void SimpleNode::setCodeUnsafe(int code) { + GQuark old_code = static_cast<GQuark>(_name); + GQuark new_code = static_cast<GQuark>(code); + + Debug::EventTracker<> tracker; + tracker.set<DebugSetElementName>(*this, new_code); + + _name = static_cast<int>(new_code); + + if (new_code != old_code) { + _document->logger()->notifyElementNameChanged(*this, old_code, new_code); + _observers.notifyElementNameChanged(*this, old_code, new_code); + } +} + +void SimpleNode::addChild(Node *generic_child, Node *generic_ref) { + g_assert(generic_child); + g_assert(generic_child->document() == _document); + g_assert(!generic_ref || generic_ref->document() == _document); + + SimpleNode *child=dynamic_cast<SimpleNode *>(generic_child); + SimpleNode *ref=dynamic_cast<SimpleNode *>(generic_ref); + + g_assert(!ref || ref->_parent == this); + g_assert(!child->_parent); + + Debug::EventTracker<DebugAddChild> tracker(*this, *child, ref); + + SimpleNode *next; + if (ref) { + next = ref->_next; + ref->_next = child; + + child->_prev = ref; + } else { + if(_first_child) _first_child->_prev = child; + next = _first_child; + _first_child = child; + } + + if (!next) { // appending? + _last_child = child; + // set cached position if possible when appending + if (!ref) { + // if !next && !ref, child is sole child + child->_cached_position = 0; + _cached_positions_valid = true; + } else if (_cached_positions_valid) { + child->_cached_position = ref->_cached_position + 1; + } + } else { + next->_prev = child; + // invalidate cached positions otherwise + _cached_positions_valid = false; + } + + child->_setParent(this); + child->_next = next; + _child_count++; + + _document->logger()->notifyChildAdded(*this, *child, ref); + _observers.notifyChildAdded(*this, *child, ref); +} + +void SimpleNode::removeChild(Node *generic_child) { + g_assert(generic_child); + g_assert(generic_child->document() == _document); + + SimpleNode *child=dynamic_cast<SimpleNode *>(generic_child); + SimpleNode *ref=child->_prev; + SimpleNode *next = child->_next; + + g_assert(child->_parent == this); + + Debug::EventTracker<DebugRemoveChild> tracker(*this, *child); + + if (ref) { + ref->_next = next; + } else { + _first_child = next; + } + if (next) { // removing the last child? + next->_prev = ref; + } else { + // removing any other child invalidates the cached positions + _last_child = ref; + _cached_positions_valid = false; + } + + child->_next = nullptr; + child->_prev = nullptr; + child->_setParent(nullptr); + _child_count--; + + _document->logger()->notifyChildRemoved(*this, *child, ref); + _observers.notifyChildRemoved(*this, *child, ref); +} + +void SimpleNode::changeOrder(Node *generic_child, Node *generic_ref) { + g_assert(generic_child); + g_assert(generic_child->document() == this->_document); + g_assert(!generic_ref || generic_ref->document() == this->_document); + + SimpleNode *const child=dynamic_cast<SimpleNode *>(generic_child); + SimpleNode *const ref=dynamic_cast<SimpleNode *>(generic_ref); + + g_return_if_fail(child->parent() == this); + g_return_if_fail(child != ref); + g_return_if_fail(!ref || ref->parent() == this); + + SimpleNode *const prev= child->_prev; + + Debug::EventTracker<DebugSetChildPosition> tracker(*this, *child, prev, ref); + + if (prev == ref) { return; } + + SimpleNode *next; + + /* Remove from old position. */ + next = child->_next; + if (prev) { + prev->_next = next; + } else { + _first_child = next; + } + if (next) { + next->_prev = prev; + } else { + _last_child = prev; + } + + /* Insert at new position. */ + if (ref) { + next = ref->_next; + ref->_next = child; + } else { + next = _first_child; + _first_child = child; + } + + child->_prev = ref; + child->_next = next; + + if (next) { + next->_prev = child; + } else { + _last_child = child; + } + + _cached_positions_valid = false; + + _document->logger()->notifyChildOrderChanged(*this, *child, prev, ref); + _observers.notifyChildOrderChanged(*this, *child, prev, ref); +} + +void SimpleNode::setPosition(int pos) { + g_return_if_fail(_parent != nullptr); + + // a position beyond the end of the list means the end of the list; + // a negative position is the same as an infinitely large position + + SimpleNode *ref=nullptr; + for ( SimpleNode *sibling = _parent->_first_child ; + sibling && pos ; sibling = sibling->_next ) + { + if ( sibling != this ) { + ref = sibling; + pos--; + } + } + + _parent->changeOrder(this, ref); +} + +namespace { + +void child_added(Node *node, Node *child, Node *ref, void *data) { + reinterpret_cast<NodeObserver *>(data)->notifyChildAdded(*node, *child, ref); +} + +void child_removed(Node *node, Node *child, Node *ref, void *data) { + reinterpret_cast<NodeObserver *>(data)->notifyChildRemoved(*node, *child, ref); +} + +void content_changed(Node *node, gchar const *old_content, gchar const *new_content, void *data) { + reinterpret_cast<NodeObserver *>(data)->notifyContentChanged(*node, Util::share_unsafe((const char *)old_content), Util::share_unsafe((const char *)new_content)); +} + +void attr_changed(Node *node, gchar const *name, gchar const *old_value, gchar const *new_value, bool /*is_interactive*/, void *data) { + reinterpret_cast<NodeObserver *>(data)->notifyAttributeChanged(*node, g_quark_from_string(name), Util::share_unsafe((const char *)old_value), Util::share_unsafe((const char *)new_value)); +} + +void order_changed(Node *node, Node *child, Node *old_ref, Node *new_ref, void *data) { + reinterpret_cast<NodeObserver *>(data)->notifyChildOrderChanged(*node, *child, old_ref, new_ref); +} + +const NodeEventVector OBSERVER_EVENT_VECTOR = { + &child_added, + &child_removed, + &attr_changed, + &content_changed, + &order_changed +}; + +}; + +void SimpleNode::synthesizeEvents(NodeEventVector const *vector, void *data) { + if (vector->attr_changed) { + for ( List<AttributeRecord const> iter = _attributes ; + iter ; ++iter ) + { + vector->attr_changed(this, g_quark_to_string(iter->key), nullptr, iter->value, false, data); + } + } + if (vector->child_added) { + SimpleNode *ref = nullptr; + for ( SimpleNode *child = this->_first_child ; + child ; child = child->_next ) + { + vector->child_added(this, child, ref, data); + ref = child; + } + } + if (vector->content_changed) { + vector->content_changed(this, nullptr, this->_content, data); + } +} + +void SimpleNode::synthesizeEvents(NodeObserver &observer) { + synthesizeEvents(&OBSERVER_EVENT_VECTOR, &observer); +} + +void SimpleNode::recursivePrintTree(unsigned level) { + + if (level == 0) { + std::cout << "XML Node Tree" << std::endl; + } + std::cout << "XML: "; + for (unsigned i = 0; i < level; ++i) { + std::cout << " "; + } + char const *id=attribute("id"); + if (id) { + std::cout << id << std::endl; + } else { + std::cout << name() << std::endl; + } + for (SimpleNode *child = _first_child; child != nullptr; child = child->_next) { + child->recursivePrintTree( level+1 ); + } +} + +Node *SimpleNode::root() { + Node *parent=this; + while (parent->parent()) { + parent = parent->parent(); + } + + if ( parent->type() == DOCUMENT_NODE ) { + for ( Node *child = _document->firstChild() ; + child ; child = child->next() ) + { + if ( child->type() == ELEMENT_NODE ) { + return child; + } + } + return nullptr; + } else if ( parent->type() == ELEMENT_NODE ) { + return parent; + } else { + return nullptr; + } +} + +void SimpleNode::cleanOriginal(Node *src, gchar const *key){ + std::vector<Node *> to_delete; + for ( Node *child = this->firstChild() ; child != nullptr ; child = child->next() ) + { + gchar const *id = child->attribute(key); + if (id) { + Node *rch = sp_repr_lookup_child(src, key, id); + if (rch) { + child->cleanOriginal(rch, key); + } else { + to_delete.push_back(child); + } + } else { + to_delete.push_back(child); + } + } + for (auto & i : to_delete) { + removeChild(i); + } +} + +bool SimpleNode::equal(Node const *other, bool recursive) { + if(strcmp(name(),other->name())!= 0){ + return false; + } + if (!(strcmp("sodipodi:namedview", name()))) { + return true; + } + guint orig_length = 0; + guint other_length = 0; + + if(content() && other->content() && strcmp(content(), other->content()) != 0){ + return false; + } + for (List<AttributeRecord const> orig_attr = attributeList(); orig_attr; ++orig_attr) { + for (List<AttributeRecord const> other_attr = other->attributeList(); other_attr; ++other_attr) { + const gchar * key_orig = g_quark_to_string(orig_attr->key); + const gchar * key_other = g_quark_to_string(other_attr->key); + if (!strcmp(key_orig, key_other) && + !strcmp(orig_attr->value, other_attr->value)) + { + other_length++; + break; + } + } + orig_length++; + } + if (orig_length != other_length) { + return false; + } + if (recursive) { + //NOTE: for faster the childs need to be in the same order + Node const *other_child = other->firstChild(); + for ( Node *child = firstChild(); + child; + child = child->next()) + { + if (!child->equal(other_child, recursive)) { + return false; + } + other_child = other_child->next(); + if(!other_child) { + return false; + } + } + } + return true; +} + +void SimpleNode::mergeFrom(Node const *src, gchar const *key, bool extension, bool clean) { + g_return_if_fail(src != nullptr); + g_return_if_fail(key != nullptr); + g_assert(src != this); + + setContent(src->content()); + if(_parent) { + setPosition(src->position()); + } + + if (clean) { + Node * srcp = const_cast<Node *>(src); + cleanOriginal(srcp, key); + } + + for ( Node const *child = src->firstChild() ; child != nullptr ; child = child->next() ) + { + gchar const *id = child->attribute(key); + if (id) { + Node *rch=sp_repr_lookup_child(this, key, id); + if (rch && (!extension || rch->equal(child, false))) { + rch->mergeFrom(child, key, extension); + continue; + } else { + if(rch) { + removeChild(rch); + } + } + } + { + guint pos = child->position(); + Node *rch=child->duplicate(_document); + addChildAtPos(rch, pos); + rch->release(); + } + } + + for ( List<AttributeRecord const> iter = src->attributeList() ; + iter ; ++iter ) + { + setAttribute(g_quark_to_string(iter->key), iter->value); + } +} + +} + +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/simple-node.h b/src/xml/simple-node.h new file mode 100644 index 0000000..690bdaf --- /dev/null +++ b/src/xml/simple-node.h @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * GC-managed XML node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_NODE_H +#error You have included xml/simple-node.h in your document, which is an implementation. Chances are that you want xml/node.h. Please fix that. +#endif + +#ifndef SEEN_INKSCAPE_XML_SIMPLE_NODE_H +#define SEEN_INKSCAPE_XML_SIMPLE_NODE_H + +#include <cassert> +#include <iostream> + +#include "xml/node.h" +#include "xml/attribute-record.h" +#include "xml/composite-node-observer.h" +#include "util/list-container.h" + +namespace Inkscape { + +namespace XML { + +/** + * @brief Default implementation of the XML node stored in memory. + * + * @see Inkscape::XML::Node + */ +class SimpleNode +: virtual public Node, public Inkscape::GC::Managed<> +{ +public: + char const *name() const override; + int code() const override { return _name; } + void setCodeUnsafe(int code) override; + + Document *document() override { return _document; } + Document const *document() const override { + return const_cast<SimpleNode *>(this)->document(); + } + + Node *duplicate(Document* doc) const override { return _duplicate(doc); } + + Node *root() override; + Node const *root() const override { + return const_cast<SimpleNode *>(this)->root(); + } + + Node *parent() override { return _parent; } + Node const *parent() const override { return _parent; } + + Node *next() override { return _next; } + Node const *next() const override { return _next; } + Node *prev() override { return _prev; } + Node const *prev() const override { return _prev; } + + Node *firstChild() override { return _first_child; } + Node const *firstChild() const override { return _first_child; } + Node *lastChild() override { return _last_child; } + Node const *lastChild() const override { return _last_child; } + + unsigned childCount() const override { return _child_count; } + Node *nthChild(unsigned index) override; + Node const *nthChild(unsigned index) const override { + return const_cast<SimpleNode *>(this)->nthChild(index); + } + + void addChild(Node *child, Node *ref) override; + void appendChild(Node *child) override { + SimpleNode::addChild(child, _last_child); + } + void removeChild(Node *child) override; + void changeOrder(Node *child, Node *ref) override; + + unsigned position() const override; + void setPosition(int pos) override; + + char const *attribute(char const *key) const override; + bool matchAttributeName(char const *partial_name) const override; + + char const *content() const override; + void setContent(char const *value) override; + + void cleanOriginal(Node *src, gchar const *key) override; + bool equal(Node const *other, bool recursive) override; + void mergeFrom(Node const *src, char const *key, bool extension = false, bool clean = false) override; + + Inkscape::Util::List<AttributeRecord const> attributeList() const override { + return _attributes; + } + + void synthesizeEvents(NodeEventVector const *vector, void *data) override; + void synthesizeEvents(NodeObserver &observer) override; + + void addListener(NodeEventVector const *vector, void *data) override { + assert(vector != NULL); + _observers.addListener(*vector, data); + } + void addObserver(NodeObserver &observer) override { + _observers.add(observer); + } + void removeListenerByData(void *data) override { + _observers.removeListenerByData(data); + } + void removeObserver(NodeObserver &observer) override { + _observers.remove(observer); + } + + void addSubtreeObserver(NodeObserver &observer) override { + _subtree_observers.add(observer); + } + void removeSubtreeObserver(NodeObserver &observer) override { + _subtree_observers.remove(observer); + } + + void recursivePrintTree(unsigned level = 0) override; + +protected: + SimpleNode(int code, Document *document); + SimpleNode(SimpleNode const &repr, Document *document); + + virtual SimpleNode *_duplicate(Document *doc) const=0; + void setAttributeImpl(char const *key, char const *value, bool is_interactive) override; + +private: + void operator=(Node const &); // no assign + + void _setParent(SimpleNode *parent); + unsigned _childPosition(SimpleNode const &child) const; + + SimpleNode *_parent; + SimpleNode *_next; + SimpleNode *_prev; + Document *_document; + mutable unsigned _cached_position; + + int _name; + + Inkscape::Util::MutableList<AttributeRecord> _attributes; + + Inkscape::Util::ptr_shared _content; + + unsigned _child_count; + mutable bool _cached_positions_valid; + SimpleNode *_first_child; + SimpleNode *_last_child; + + CompositeNodeObserver _observers; + CompositeNodeObserver _subtree_observers; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/sp-css-attr.h b/src/xml/sp-css-attr.h new file mode 100644 index 0000000..07db9ca --- /dev/null +++ b/src/xml/sp-css-attr.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * SPCSSAttr - interface for CSS Attributes + *//* + * Authors: see git history + * + * Copyright (C) 2010 Authors + * Copyright 2005 Kees Cook <kees@outflux.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SP_SPCSSATTR_H +#define SEEN_INKSCAPE_XML_SP_SPCSSATTR_H + +#include "xml/node.h" + +class SPCSSAttr : virtual public Inkscape::XML::Node { +}; + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/subtree.cpp b/src/xml/subtree.cpp new file mode 100644 index 0000000..5340c2a --- /dev/null +++ b/src/xml/subtree.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * XML::Subtree - proxy for an XML subtree + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xml/node.h" +#include "xml/subtree.h" +#include "xml/node-iterators.h" + +namespace Inkscape { +namespace XML { + +Subtree::Subtree(Node &root) : _root(root) { + _root.addSubtreeObserver(_observers); +} + +Subtree::~Subtree() { + _root.removeSubtreeObserver(_observers); +} + +namespace { + +void synthesize_events_recursive(Node &node, NodeObserver &observer) { + node.synthesizeEvents(observer); + for ( NodeSiblingIterator iter = node.firstChild() ; iter ; ++iter ) { + synthesize_events_recursive(*iter, observer); + } +} + +} + +void Subtree::synthesizeEvents(NodeObserver &observer) { + synthesize_events_recursive(_root, observer); +} + +void Subtree::addObserver(NodeObserver &observer) { + _observers.add(observer); +} + +void Subtree::removeObserver(NodeObserver &observer) { + _observers.remove(observer); +} + +} +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/subtree.h b/src/xml/subtree.h new file mode 100644 index 0000000..ef4002a --- /dev/null +++ b/src/xml/subtree.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Object representing a subtree of the XML document + *//* + * Authors: see git history + * + * Copyright (C) 2015 Authors + * Copyright 2005 MenTaLguY <mental@rydia.net> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_SUBTREE_H +#define SEEN_INKSCAPE_XML_SUBTREE_H + +#include "inkgc/gc-managed.h" +#include "xml/composite-node-observer.h" + +namespace Inkscape { +namespace XML { + +/** + * @brief Represents a node and all its descendants + * + * This is a convenience object for node operations that affect all of the node's descendants. + * Currently the only such operations are adding and removing subtree observers + * and synthesizing events for the entire subtree. + */ +class Subtree : public GC::Managed<GC::SCANNED, GC::MANUAL> { +public: + Subtree(Node &root); + ~Subtree(); + + /** + * @brief Synthesize events for the entire subtree + * + * This method notifies the specified observer of node changes equivalent to creating + * this subtree from scratch. The notifications recurse into the tree depth-first. + * Currently this is the only method that provides extra functionality compared to + * the public methods of Node. + */ + void synthesizeEvents(NodeObserver &observer); + /** + * @brief Add an observer watching for subtree changes + * + * Equivalent to Node::addSubtreeObserver(). + */ + void addObserver(NodeObserver &observer); + /** + * @brief Add an observer watching for subtree changes + * + * Equivalent to Node::removeSubtreeObserver(). + */ + void removeObserver(NodeObserver &observer); + +private: + Node &_root; + CompositeNodeObserver _observers; +}; + +} +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/xml/text-node.h b/src/xml/text-node.h new file mode 100644 index 0000000..58b12f3 --- /dev/null +++ b/src/xml/text-node.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Text node implementation + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_XML_TEXT_NODE_H +#define SEEN_INKSCAPE_XML_TEXT_NODE_H + +#include <glib.h> +#include "xml/simple-node.h" + +namespace Inkscape { + +namespace XML { + +/** + * @brief Text node, e.g. "Some text" in <group>Some text</group> + */ +struct TextNode : public SimpleNode { + TextNode(Util::ptr_shared content, Document *doc) + : SimpleNode(g_quark_from_static_string("string"), doc) + { + setContent(content); + _is_CData = false; + } + TextNode(Util::ptr_shared content, Document *doc, bool is_CData) + : SimpleNode(g_quark_from_static_string("string"), doc) + { + setContent(content); + _is_CData = is_CData; + } + TextNode(TextNode const &other, Document *doc) + : SimpleNode(other, doc) { + _is_CData = other._is_CData; + } + + Inkscape::XML::NodeType type() const override { return Inkscape::XML::TEXT_NODE; } + bool is_CData() const { return _is_CData; } + +protected: + SimpleNode *_duplicate(Document* doc) const override { return new TextNode(*this, doc); } + bool _is_CData; +}; + +} + +} + +#endif +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : -- cgit v1.2.3